OIM Metadata Schema
Complete reference for the Zod-validated metadata structure in .oim files.
Schema Overview
typescript
import { z } from "zod";
export const OimMetadataSchema = z.object({
oim: z.string().optional(), // Format identifier
version: z.string().optional(), // Schema version
image: z.string().optional(), // Base64 image (legacy)
oim_id: z.string().max(128).optional(), // Unique tracking ID
media_type: z.enum(["image", "video"]).optional(),
creator_user_id: z.string().max(128).optional(),
image_hash: z.string().max(64).optional(), // SHA-256 of original image
immutable_lock: z.boolean().optional(), // Seal file from edits
allow_external_urls: z.boolean().optional(),
redirect_membership_verified: z.boolean().optional(),
terms_accepted: z.boolean().optional(),
canvas: z.object({
natural_width: z.number().positive(),
natural_height: z.number().positive()
}).optional(),
lang: z.array(z.string().max(5)).optional(),
zones: z.array(zoneSchema).optional(),
slots: z.array(slotSchema), // Required
elements: z.array(slotSchema).optional(),
layers: z.array(slotSchema).optional(),
signature: z.string().optional(), // ECDSA signature
key_version: z.string().optional(),
timestamp: z.string().datetime().optional(),
}).passthrough();Field Reference
Top-Level Fields
| Field | Type | Required | Description |
|---|---|---|---|
oim | string | Format identifier (e.g., "1.0") | |
version | string | Schema version | |
image | string | Base64-encoded image (legacy, avoid) | |
oim_id | string (≤128) | Unique ID for document re-editing | |
media_type | "image" | "video" | Type of media content | |
creator_user_id | string (≤128) | Creator tracking identifier | |
image_hash | string (≤64) | SHA-256 hash of the baked image (Layer −1) | |
immutable_lock | boolean | If true, file cannot be edited | |
allow_external_urls | boolean | Permit external redirection URLs | |
redirect_membership_verified | boolean | Paid membership verification flag | |
terms_accepted | boolean | Terms & conditions acceptance flag | |
canvas | object | Canvas dimensions { natural_width, natural_height } | |
lang | string[] (≤5 chars each) | Available language codes | |
zones | Zone[] | Named bounding box zones | |
slots | Slot[] | Text/image overlay definitions | |
elements | Slot[] | Alternative slot array | |
layers | Slot[] | Layer-based slot array | |
signature | string | Cryptographic ECDSA signature | |
key_version | string | Key version used for signature | |
timestamp | ISO datetime | Package creation timestamp |
Slot Schema
typescript
const slotSchema = z.object({
id: z.string().min(1).max(128),
type: z.enum(["text", "image", "lottie", "zone", "geo", "shape", "draw", "filter", "container", "group", "component", "mask", "svg", "pattern", "artistic-text"]).optional(),
content: z.record(z.string(), z.string().max(10000000)).optional(),
style: styleSchema.optional(),
position: z.tuple([z.number().min(0).max(100), z.number().min(0).max(100)]).optional(),
tracking_data: z.array(z.array(z.number()).min(2)).optional(),
zone_id: z.string().max(128).optional(),
link: z.string().max(2048).optional(),
target: z.enum(["_blank", "_self"]).optional(),
interaction: interactionSchema.optional(),
// Abbreviated keys for size optimization
p: positionSchema.optional(), // alias for position
s: z.tuple([z.number(), z.number()]).optional(), // alias for size/dimensions
st: styleSchema.optional(), // alias for style
c: contentSchema.optional(), // alias for content
t: z.array(trackingKeyframeSchema).optional(), // alias for tracking_data
});| Field | Type | Required | Description |
|---|---|---|---|
id | string (1-128) | Unique slot identifier | |
type | "text" | "image" | "lottie" | "zone" | Slot content type | |
content | Record<lang, text> | Language-to-content dictionary (up to 10MB for base64 images) | |
style | Style | Visual styling (see below) | |
position | [x, y] (0-100) | Normalized position coordinates | |
tracking_data | number[][] | Keyframe animation data | |
zone_id | string (≤128) | Target containment zone | |
link | string (≤2048) | Redirection URL for hotspot/e-commerce | |
target | "_blank" | "_self" | Link opening behavior | |
p | [x, y] | Retired in 1.12.0: the schema still accepts it, but no writer emits it; use position | |
s | [w, h] | Retired in 1.12.0 | |
st | Style | Retired in 1.12.0: the schema still accepts it, but no writer emits it; use style | |
c | Record<lang, text> | Retired in 1.12.0: the schema still accepts it, but no writer emits it; use content | |
t | number[][] | Retired in 1.12.0: the schema still accepts it, but no writer emits it; use tracking_data |
Style Schema
typescript
const styleSchema = z.object({
font: z.string().max(64).optional(),
color: z.string().max(64).optional(),
size: z.string().max(16).optional(),
fontSize: z.string().max(16).optional(),
fontWeight: z.union([z.string(), z.number()]).optional(),
fontStyle: z.enum(["normal", "italic", "oblique"]).optional(),
textDecoration: z.string().max(64).optional(),
textTransform: z.enum(["none", "uppercase", "lowercase", "capitalize"]).optional(),
fontSizeOverrides: z.record(z.string(), z.string()).optional(),
fontSizes: z.record(z.string(), z.string()).optional(),
maxWidths: z.record(z.string(), z.string()).optional(),
outline: z.string().max(128).optional(),
objectFit: z.enum(["contain", "cover", "fill", "none", "scale-down"]).optional(),
verticalAlign: z.enum(["top", "center", "bottom", "mixed", "justify"]).optional(),
animation: z.object({
loop: z.enum(["none", "pulse", "float", "bounce", "spin", "glow", "custom"]).optional(),
hover: z.enum(["none", "grow", "shrink", "lift", "glow", "custom"]).optional(),
custom_loop_css: z.string().max(500).optional(),
custom_keyframes: z.string().max(2000).optional(),
custom_hover_css: z.string().max(500).optional(),
duration: z.number().min(0.1).max(20).optional(),
delay: z.number().min(0).max(10).optional(),
}).optional(),
shader_fx: z.object({
blur: z.number().min(0).max(10).optional(),
grain: z.number().min(0).max(1).optional(),
crt_curve: z.boolean().optional(),
}).optional(),
}).passthrough();Style Fields
| Field | Type | Description |
|---|---|---|
font | string (≤64) | Font family name |
color | string (≤64) | CSS color value |
size / fontSize | string (≤16) | Font size (e.g., "48px") |
fontWeight | string | number | Font weight (e.g., "700" or 700) |
fontStyle | "normal" | "italic" | "oblique" | Font style |
textDecoration | string (≤64) | Text decoration |
textTransform | "none" | "uppercase" | "lowercase" | "capitalize" | Text transform |
outline | string (≤128) | Text outline CSS |
objectFit | "contain" | "cover" | "fill" | "none" | "scale-down" | Image fit mode |
verticalAlign | "top" | "center" | "bottom" | "mixed" | "justify" | Vertical alignment |
Animation
| Field | Type | Description |
|---|---|---|
loop | enum | Loop animation type |
hover | enum | Hover animation type |
custom_loop_css | string (≤500) | Custom CSS for loop |
custom_keyframes | string (≤2000) | Custom @keyframes definition |
custom_hover_css | string (≤500) | Custom CSS for hover |
duration | number (0.1-20) | Animation duration in seconds |
delay | number (0-10) | Animation delay in seconds |
Shader Effects
| Field | Type | Description |
|---|---|---|
blur | number (0-10) | Blur intensity |
grain | number (0-1) | Film grain intensity |
crt_curve | boolean | CRT curve effect |
Zone Schema
typescript
const zoneSchema = z.object({
id: z.string().min(1).max(128),
position: z.tuple([z.number().min(0).max(100), z.number().min(0).max(100)]),
size: z.tuple([z.number().min(0).max(100), z.number().min(0).max(100)]),
style: z.record(z.string(), z.any()).optional(),
});| Field | Type | Description |
|---|---|---|
id | string (1-128) | Zone identifier |
position | [x, y] (0-100) | Zone origin (top-left) |
size | [width, height] (0-100) | Zone dimensions |
style | Record<string, any> | Zone-specific styles |
Complete Example
json
{
"oim": "1.0",
"version": "1.0",
"lang": ["en", "es"],
"canvas": {
"natural_width": 1920,
"natural_height": 1080
},
"slots": [
{
"id": "main_title",
"type": "text",
"content": {
"en": "Welcome to OIM",
"es": "Bienvenido a OIM"
},
"position": [50, 20],
"style": {
"font": "Bricolage Grotesque",
"fontSize": "48px",
"fontWeight": "800",
"color": "#f59e0b",
"textTransform": "uppercase"
}
},
{
"id": "cta_button",
"type": "text",
"content": {
"en": "Learn More",
"es": "Saber Más"
},
"position": [50, 80],
"style": {
"fontSize": "24px",
"color": "#ffffff",
"animation": {
"loop": "pulse",
"duration": 2.0
}
},
"link": "https://example.com",
"target": "_blank"
}
],
"zones": [
{
"id": "header_zone",
"position": [0, 0],
"size": [100, 30]
}
]
}Security Constraints
| Constraint | Purpose |
|---|---|
| String max lengths | Prevent buffer overflow |
| Color regex validation | Prevent CSS injection |
| Max 50 slots | Prevent resource exhaustion |
content ≤ 10MB | Allow base64 images but cap size |
position 0-100 | Normalized coordinates only |
.passthrough() | Allow forward-compatible fields |
TypeScript Types
typescript
export type OIMMetadata = z.infer<typeof OimMetadataSchema>;
export type Slot = z.infer<typeof slotSchema>;
export type Style = z.infer<typeof styleSchema>;
export type Zone = z.infer<typeof zoneSchema>;