Especificación del Formato .oim
El formato de archivo .oim (Object Interactive Media) es una especificación abierta y nativa para el navegador web que encapsula capas vectoriales interactivas, diccionarios de texto multilingüe, animaciones por fotogramas clave (keyframes) y firmas criptográficas de autenticidad directamente dentro de un contenedor estándar PNG (Portable Network Graphics).
Arquitectura del Formato
Todo documento .oim opera bajo el Modelo de Separación en 3 Capas:
+-------------------------------------------------------------------------+
| Capa +1: Capa Dinámica Interactiva |
| - Renderizada en tiempo real en Shadow DOM (<oim-player>) |
| - 100% accesible, indexable por buscadores (SEO), texto seleccionable |
| - Formas geométricas (geo), Lottie, keyframes WAAPI, Parallax 2.5D |
| - Filtros y máscaras vivos, resueltos en ejecución |
+-------------------------------------------------------------------------+
| Capa 0: Separador (simbólica, no almacena nada) |
+-------------------------------------------------------------------------+
| Capa −1: Horneada (Píxeles) |
| - Píxeles PNG sin pérdidas (IDAT), decodificados por GPU / <img> |
| - Lo que el autor fijó en la imagen (puede incluir texto) |
| - Sellada criptográficamente con el hash SHA-256 (image_hash) |
+-------------------------------------------------------------------------+Cada archivo .oim es un PNG válido al 100%. Si se abre en un visor de imágenes tradicional del sistema operativo o en una etiqueta estándar <img>, se visualiza la imagen horneada de la Capa −1 sin errores ni artefactos.
Estructura del Contenedor Físico PNG
.oim cumple rigurosamente con la norma ISO/IEC 15948:2004 (RFC 2083).
+-------------------------------------------------------------+
| Firma PNG (8 bytes): \x89PNG\r\n\x1a\n |
+-------------------------------------------------------------+
| Chunk IHDR: Dimensiones, profundidad de color, compresión |
+-------------------------------------------------------------+
| Chunk zTXt: Clave "OIM_Data\0", método 0 |
| Carga: zlib(MetadataJSON), en binario |
+-------------------------------------------------------------+
| Chunks IDAT: Flujo de píxeles de la Capa −1 (horneada) |
+-------------------------------------------------------------+
| Chunk IEND: Fin del archivo |
+-------------------------------------------------------------+El Chunk de Metadatos zTXt
- Palabra Clave (Keyword):
OIM_Data(ASCII terminado en byte nulo0x00). (Soporte de compatibilidad hacia atrás:OIM_JSON_DATA). - Carga Útil: un byte de método de compresión (
0) seguido del JSON comprimido con zlib, en binario (sin Base64). Los archivos firmados antes del 29 de octubre de 2026 llevan en su lugar un chunktEXtcon Base64 de Deflate crudo; los lectores lo siguen aceptando. - Sanitización de Chunks: Para evitar ataques de inyección y contrabando de cargas (payload smuggling), el compilador de OIM purga todos los chunks de texto preexistentes (
tEXt,zTXt,iTXt) de la imagen base. La firma solo cubre los metadatos autorizados.
Pipeline de Compresión y Codificación
+-------------------+ +-------------------+ +-------------------+
| JSON Metadatos | ---> | Deflate Crudo | ---> | RFC 4648 Base64 |
| (Cadena UTF-8) | | (wbits = -15) | | (Cadena ASCII) |
+-------------------+ +-------------------+ +-------------------+- Compresión Deflate Raw: El JSON se comprime con Deflate configurando
wbits=-15(RFC 1951) y nivel 9 de compresión. Se eliminan las cabeceras zlib de 2 bytes y la cola Adler-32 de 4 bytes, optimizando el tamaño. - Codificación Base64: El flujo binario resultante se convierte en texto ASCII mediante Base64 estándar (RFC 4648).
- Protocolo de Descompresión con Fallback: Los clientes implementan tres pasos defensivos:
- Detección de JSON en texto plano (
rawText.trim().startsWith('{')). - Intento con zlib estándar (
deflate). - Fallback a Deflate crudo (
deflate-raw).
- Detección de JSON en texto plano (
Límites de Seguridad y Restricciones Estrictas
| Restricción | Ámbito de Validación | Límite / Regla | Justificación Técnica |
|---|---|---|---|
| Defensa Zip Bomb | Empaquetado y Demuxer | Máximo 10 MB (10.485.760 bytes) | Evita ataques de denegación de servicio por descompresión explosiva. |
| Capacidad de Slots | Compilador y Backend | Máximo 150 slots | Garantiza rendimiento de 60 FPS en el DOM y acota la memoria RAM. |
| Identificadores Únicos | Validador de Esquema | ID único por slot (1-128 car.) | Evita colisiones de estado y errores en la resolución de acciones. |
| Sin Redirecciones | Esquema y Compilador | link y target prohibidos | El archivo no secuestra la navegación; los clics emiten oim:action. |
| Sanitización de Color | Resolutor CSS | Expresión regular positiva | Bloquea inyecciones CSS, balizas de rastreo y ataques de estilo. |
| Límite de Keyframes | Esquema de Animación | Máximo 240 keyframes ($t \le 60s$) | Previene fugas de memoria en bucles de animación WAAPI. |
| Privacidad RGPD | Pipeline Backend | creator_user_id excluido | Los datos personales se gestionan en registros revocables, no en el fichero. |
Esquema Zod Oficial (@oim-media/core)
import { z } from "zod";
export const keyframeSchema = z.object({
t: z.number().min(0).max(60), // Marca de tiempo en segundos
o: z.number().min(0).max(1).optional(), // Opacidad normalizada (0.0 a 1.0)
s: z.number().min(0).max(10).optional(), // Escala (0.0 a 10.0)
r: z.number().min(-3600).max(3600).optional(), // Rotación en grados
x: z.number().optional(), // Desplazamiento X (px o cqw)
y: z.number().optional(), // Desplazamiento Y (px o cqw)
b: z.number().min(0).max(50).optional(), // Desenfoque en píxeles
e: z.union([ // Curva de aceleración
z.enum(["linear", "ease-in", "ease-out", "ease-in-out", "steps(1, end)"]),
z.tuple([z.number(), z.number(), z.number(), z.number()]), // Bezier [p1x, p1y, p2x, p2y]
]).optional(),
});
export const stateActionSchema = z.object({
target: z.string().min(1).max(128),
type: z.enum(["show", "hide", "toggle", "setText", "play"]), // play (1.22.0): reproduce desde el principio la animación del destino
value: z.string().max(10000).optional(),
});
// Cómo entra y sale la capa cuando una acción la muestra u oculta (1.22.0)
export const revealSchema = z.object({
effect: z.enum(["fade", "slide-up", "slide-down", "slide-left", "slide-right", "scale"]),
duration: z.number().min(0).max(2000), // ms
easing: keyframeEasingSchema.optional(), // sin ella = ease-out
});
export const interactiveStateSchema = z.object({
scale: z.number().min(0).max(10).optional(),
rotation: z.number().min(-360).max(360).optional(),
opacity: z.number().min(0).max(100).optional(), // Opacidad en porcentaje (0 a 100)
color: z.string().max(64).optional(),
transitionDuration: z.number().min(0).max(10000).optional(), // Duración en ms
actions: z.array(stateActionSchema).max(64).optional(),
}).passthrough();
export const slotStatesSchema = z.object({
hover: interactiveStateSchema.optional(), // Entrada/salida del cursor
active: interactiveStateSchema.optional(), // Clic persistente
}).passthrough();
export const slotSchema = z.object({
id: z.string().min(1).max(128),
name: z.string().max(256).optional(), // Nombre legible asignado por el autor
type: z.enum([
"text", "image", "lottie", "zone", "geo", "draw",
"filter", "container", "group", "component", "mask", "svg", "pattern"
]).optional(),
align: z.enum(["left", "center", "right"]).optional(),
content: z.record(z.string(), z.string().max(10000000)).optional(), // Multilingüe
style: styleSchema.optional(),
position: z.tuple([z.number().min(0).max(100), z.number().min(0).max(100)]).optional(),
parallax: parallaxSchema.optional(),
tracking_data: z.array(z.array(z.number()).min(2)).optional(),
});
export const OimMetadataSchema = z.object({
oim: z.string().optional(), // Versión de formato ("1.0")
version: z.string().optional(),
min_player: z.string().max(16).optional(), // Versión mínima de player requerida
oim_id: z.string().max(128).optional(), // UUID del documento
media_type: z.enum(["image", "video"]).optional(),
image_hash: z.string().max(64).optional(), // Hash SHA-256 de la imagen horneada (Capa −1)
immutable_lock: z.boolean().optional(), // Candado de solo lectura
reedit: z.enum(["public", "author_only", "sealed"]).optional(), // Permiso de reedición (1.23.0). El `reedicion` heredado (valores en español) se sigue leyendo hasta 2.0.0.
canvas: z.object({
natural_width: z.number().positive(),
natural_height: z.number().positive(),
}).optional(),
lang: z.array(z.string().max(5)).optional(), // Idiomas disponibles
slots: z.array(slotSchema), // Máximo 150 slots
capabilities: z.array(z.enum(["vector_parallax", "infinite_parallax", "path_morph", "physics_2d"])).optional(),
grain: z.object({ // Velo de grano analógico procedural
enabled: z.boolean(),
intensity: z.number().min(1).max(100),
baseFrequency: z.number().min(0.1).max(2.0),
blendMode: z.enum(["overlay", "screen", "multiply", "soft-light"]),
}).optional(),
signature: z.string().optional(), // Firma digital ECDSA P-256
key_version: z.string().optional(),
timestamp: z.string().datetime({ offset: true }).optional(),
}).passthrough();Grano por capa (contrato 1.10.0)
Una capa puede llevar su propio grano fotográfico: ruido fractal monocromo que solo aparece donde la capa tiene tinta (dentro de las letras de un texto, en el área de una forma). Es distinto del grain del documento de arriba, que cubre todo el lienzo.
Campo de style | Tipo | Por defecto | Significado |
|---|---|---|---|
grain | booleano | false | Activa el grano de la capa. |
grainIntensity | 1–100 | 20 | Opacidad del grano. |
grainSize | 1–100 | 50 | Tamaño del grano: 1 finísimo, 100 grueso. |
grainBlend | overlay | soft-light | multiply | overlay | Cómo se funde con la capa. |
Tipos de capa que admiten grano: text, artistic-text, geo, svg, draw, image, pattern. Los lectores convierten grainSize en el baseFrequency de feTurbulence con grainFrequency(size) de @oim-media/core (2.0 con tamaño 1, 0.2 con tamaño 100) y construyen el filtro con buildLayerGrainFilter, así que todos pintan los mismos píxeles. El cambio es aditivo y no declara ninguna capacidad: un lector que no lo conoce pinta la capa sin grano.
Marcas de texto (contrato 1.11.0)
Un texto puede dar formato a algunas de sus frases: color, negrita, cursiva, subrayado o resaltado. La frase va envuelta en una etiqueta dentro de content, y el estilo de la marca vive una sola vez en style.marks de la capa, compartido por todos los idiomas:
"content": { "es": "Te irás. {hl}No te llevarás nada.{/hl}", "en": "You will leave. {hl}You will take nothing.{/hl}" },
"style": { "marks": { "hl": { "color": "#5a7d1f", "bold": true,
"highlight": { "style": "marker", "color": "rgba(217,249,157,.6)" } } } }| Campo de la marca | Tipo | Significado |
|---|---|---|
color / colorRole | color CSS / rol dinámico | Color del texto; el rol gana si el lector lo conoce. |
bold, italic, underline | booleano | Peso, estilo, subrayado. |
highlight | { style, color?, colorRole?, paddingX?, paddingY?, borderRadius? } | Resaltado detrás de la frase, línea a línea. style: marker | box | pill | underline-thick. |
- Los nombres cumplen
^[a-z][a-z0-9-]{0,23}$; como mucho 8 marcas por capa. {nombre}abre,{/nombre}cierra; las marcas no se anidan ni se solapan.- Una llave literal va doblada:
{{es{y}}es}. El texto que escribe una persona se escapa siempre, así que nunca puede convertirse en una marca. - El backend no firma un texto con etiquetas mal formadas (
OIM_E_TEXT_MARKS_INVALID): un.oimfirmado solo lleva textos bien formados. - Los lectores usan la única interpretación de
@oim-media/core:parseMarks(texto, Object.keys(marks))para los tramos ymarkStyle(marca)para el estilo. Un archivo con marcas declara la capacidadtext_marks. style.textHighlightestá retirado (10 oct 2026): resaltaba el texto entero. Los generadores modernos de OIM ya no lo escriben y emiten marcas en su lugar; los players lo siguen leyendo para que los archivos ya publicados se vean igual. Quien escriba archivos nuevos usa una marca.- Capitular (
style.dropCap): los lectores dimensionan la primera letra condropCapStyle(dropCap)de@oim-media/core(2,2× el tamaño de letra con 2 líneas, 3,2× con 3; interlineado 0,85; 8 px de separación por defecto; negrita).
Animaciones de texto (style.textAnimationType)
| Valor | Efecto | Unidad |
|---|---|---|
typewriter | las letras aparecen una a una, con cursor opcional | letra |
wave-chars | las letras suben y bajan en una onda senoidal, sin parar | letra |
glitch-cyberpunk, glitch-slice | ráfagas de desdoble de color / desplazamiento horizontal | texto |
fade-chars | cada letra aparece con un fundido, una vez | letra |
slide-up | cada palabra sube 0,6 em mientras aparece, una vez | palabra |
bounce-chars | cada letra crece de más y se asienta, una vez | letra |
reveal-mask | cada palabra sube a la vista desde debajo de su propia caja de recorte, una vez (1.13.0) | palabra |
scale-in | cada letra crece de 0,6 a su tamaño mientras aparece, una vez (1.13.0) | letra |
rise-words | cada palabra sube 0,8 em y se endereza desde 8°, una vez (1.13.0) | palabra |
- Los seis efectos de entrada (
fade-chars,slide-up,bounce-chars,reveal-mask,scale-in,rise-words) se definen como datos en@oim-media/core(text-effects.ts): cada unidad tarda 0,35 s y empieza a lostextAnimationDelay + índice × textAnimationSpeedsegundos (desfase por defecto 0,05 s). Los lectores muestreanentranceFrame/entranceKeyframes; un fotograma solo lleva opacidad, un desplazamiento vertical en em, una escala y una rotación, yentranceClipsdice si cada unidad va tras un recorte rectangular de su propia caja, así que cualquier renderizador, web o nativo, puede dibujarlo. blur-charsestá retirado (10 oct 2026): el desenfoque por letra no es portable. Sigue siendo válido en el esquema; los lectores dibujan el texto quieto.- El texto animado se dibuja sin sus marcas, y una exportación estática muestra el estado final.
Retirado en el contrato 1.12.0
Estas claves siguen validando, así que los archivos ya publicados siguen funcionando, pero ningún archivo nuevo debe escribirlas. Los lectores las ignoran salvo que se indique.
| Clave | Por qué | En su lugar |
|---|---|---|
style.textTransform | Ningún renderizador la aplicaba. | Escribir el texto en la caja deseada. |
style.verticalAlign, style.shader_fx | Nadie las escribía ni las leía. | — |
style.blendMode | Nunca se vio en el editor ni en el player. Ahora es una lista cerrada de 9 valores. | — (la capa se pinta normal) |
style.grain, grainIntensity, grainSize, grainBlend | El grano de una capa es un efecto de textura. Los players lo siguen pintando en archivos antiguos. | style.effects con type: "texture" |
grain y effects en la raíz | El grano del fondo va horneado en la imagen base. Los players los siguen pintando en archivos antiguos. | — (horneado) |
elements, layers en la raíz | slots es el único nombre. Los lectores todavía los aceptan. | slots |
p, s, st, c, t en el slot | Alias cortos que ningún escritor emitía. Los lectores ya no los leen. | position, style, content, tracking_data |
La textura de una capa se pinta sobre los píxeles de la propia capa (su SVG o su imagen), no sobre toda su caja: layerTextureMask en @oim-media/core.
Accesibilidad (contrato 1.16.0)
style.alt de la capa ({ [idioma]: string }, ≤ 300 cada uno) describe una imagen; style.decorative (boolean) hace que el lector salte la capa y gana sobre alt; description en la raíz del documento ({ [idioma]: string }, ≤ 1000 cada uno) resume la obra. Ver Accesibilidad.
Relleno de imagen (contrato 1.17.0)
Una forma con área (geo, no line ni scribble) puede rellenarse con una foto. style.fillType vale "image" y style.fillImage es una URI data:image/… incrustada: en un .oim firmado siempre va incrustada, y el player nunca la carga desde la red. El opcional style.fillImageCrop ({ x: 0–100, y: 0–100, zoom: 1–10 }) se centra con zoom 1 por defecto. Sigue la misma regla que imageCrop: x e y son el punto de la imagen que queda sobre el mismo punto del marco, y zoom escala alrededor de él.
El editor escribe siempre también style.fillColor. Se pinta debajo de la foto y se muestra cuando la foto falta o no carga; los lectores anteriores al 1.17.0 muestran ese color. La capacidad image_fill sube min_player a 1.17.0, y solo cuando se usa. alt y decorative (1.16.0) también se aplican a los marcos con foto.
Filtros de medios (contrato 1.18.0)
style.filterPreset, filterBlur (0–100 px), filterBrightness, filterContrast, filterSaturate (0–500 %, 100 = sin cambio) y filterHueRotate (0–360°) se aplican también a las capas de imagen y a la foto de un marco (fillType: "image"). En un marco el filtro solo afecta a la foto: nunca al trazo ni al color de debajo, y el desenfoque no sale de la forma. duotone queda reservado a las capas de filtro. La capacidad media_filters sube min_player a 1.18.0, y solo cuando hay un filtro activo.
Revelado y reproducción (contrato 1.22.0)
style.reveal dice cómo entra y sale una capa cuando una acción la muestra u oculta: effect (fade, slide-up, slide-down, slide-left, slide-right, scale), duration (0–2000 ms; 0 = al instante) y un easing opcional del mismo tipo que la e de un fotograma clave (por defecto ease-out). Ocultar reproduce el mismo efecto al revés. Sin reveal la capa aparece en 300 ms y desaparece en 250 ms con ease, como hasta ahora. Un deslizamiento recorre el 4 % del ancho de la imagen; scale crece desde 0,92.
style.animation.trigger es "load" (por defecto: arranca al cargar y se repite) o "action": la capa reposa en su primer fotograma clave hasta que una acción play la reproduce una vez. La acción play ({ "target": "<id de capa>", "type": "play" }) reinicia la animación del destino cada vez; en una capa que se repite, rebobina el bucle. No muestra una capa oculta (combínala con show) y salir de un hover no la deshace. Un lector anterior a la 1.22.0 ignora las tres cosas: funde, reproduce al cargar y se salta play.
Formas de marco y marcos SVG propios (contrato 1.19.0)
style.geoShape admite doce formas cerradas más para los marcos con foto: pill, octagon, pentagon, cross, moon, cloud, drop, bolt, bubble, flower, burst y arch. Un geo con style.svgPath y fillType: "image" pinta la foto a través de ese contorno en lugar de una forma del catálogo. svgPath solo admite datos de trazado (comandos M L H V C S Q T A Z, números y separadores), de 20 000 caracteres como máximo. El opcional style.svgViewBox ("minX minY ancho alto") indica su caja de coordenadas; sin él, la caja es 0–100. style.fillImageFlipX y fillImageFlipY voltean la foto dentro del marco, no su contorno. La capacidad frame_shapes sube min_player a 1.19.0, y solo cuando se usa alguna de estas. Un lector sin ella pinta el marco como un rectángulo con la foto entera.
Reglas de pintado para lectores
El player web es el renderizador de referencia. Las reglas que sigue sin que el esquema las diga están en CANONICAL_SPEC.md, sección 4.8. En resumen: un lector con motor de párrafos ignora style.textRuns, letterSpacing necesita unidad CSS (un número suelto se ignora), los trazos salen solo de las claves stroke*, opacity va de 0 a 100, los filtros de medios solo afectan a imágenes y las claves retiradas no se pintan.
Ejemplo Completo del Contrato JSON
{
"oim": "1.0",
"version": "5.3",
"min_player": "1.0",
"oim_id": "oim_7f8a9b1c2d3e",
"media_type": "image",
"image_hash": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
"reedit": "public",
"immutable_lock": false,
"canvas": {
"natural_width": 1200,
"natural_height": 630
},
"lang": ["es", "en"],
"slots": [
{
"id": "slot_titular",
"name": "Titular Principal",
"type": "text",
"position": [50.0, 25.0],
"align": "center",
"content": {
"es": "Medios Interactivos para la Web Abierta",
"en": "Interactive Media for the Open Web"
},
"style": {
"font": "Bricolage Grotesque",
"fontSize": "4.5cqw",
"fontWeight": 800,
"color": "#f59e0b",
"animation": {
"duration": 3.0,
"keyframes": [
{ "t": 0, "o": 0, "y": 20 },
{ "t": 0.8, "o": 1, "y": 0, "e": "ease-out" }
]
}
}
},
{
"id": "btn_explorar",
"name": "Botón de Interacción",
"type": "geo",
"position": [50.0, 75.0],
"style": {
"isInteractive": true,
"states": {
"hover": {
"scale": 1.05,
"opacity": 95,
"transitionDuration": 200
},
"active": {
"actions": [
{ "target": "slot_titular", "type": "toggle" }
]
}
}
}
}
],
"signature": "MEQCID...firma_ecdsa_64_bytes_crudos...",
"key_version": "v1",
"timestamp": "2026-09-03T03:00:00.000000+00:00"
}Implementación de Referencia en TypeScript
import { decompressBytes } from "@oim-media/core";
export async function extractOIM(blob: Blob): Promise<{ image: Blob; metadata: any }> {
const arrayBuffer = await blob.arrayBuffer();
const view = new DataView(arrayBuffer);
let offset = 8; // Saltar cabecera mágica de 8 bytes de PNG
let metadata: any = null;
const originalChunks: Uint8Array[] = [new Uint8Array(arrayBuffer, 0, 8)];
while (offset < arrayBuffer.byteLength) {
const length = view.getUint32(offset, false);
const type = String.fromCharCode(
view.getUint8(offset + 4), view.getUint8(offset + 5),
view.getUint8(offset + 6), view.getUint8(offset + 7)
);
const totalChunkSize = 12 + length;
if (type === "zTXt") {
const dataBytes = new Uint8Array(arrayBuffer, offset + 8, length);
const nullIdx = dataBytes.indexOf(0);
if (nullIdx !== -1) {
const keyword = new TextDecoder().decode(dataBytes.slice(0, nullIdx));
if (keyword === "OIM_Data" || keyword === "OIM_JSON_DATA") {
// Saltar el NUL y el byte del método de compresión (0 = zlib)
const bytes = dataBytes.slice(nullIdx + 2);
const decompressed = await decompressBytes(bytes, "deflate");
metadata = JSON.parse(decompressed);
offset += totalChunkSize;
continue; // No incluir el chunk en la imagen ráster limpia
}
}
}
originalChunks.push(new Uint8Array(arrayBuffer, offset, totalChunkSize));
offset += totalChunkSize;
}
const cleanImageBlob = new Blob(originalChunks, { type: "image/png" });
return { image: cleanImageBlob, metadata };
}Licencia del Formato, Patentes y Gobernanza de Conformidad
Las especificaciones .oim y .ovm, los vectores canónicos de conformidad (@oim-media/spec), el motor nuclear (@oim-media/core), la herramienta de línea de comandos (@oim-media/cli) y los reproductores web (@oim-media/player-oim, @oim-media/player-ovm) son publicados por Xue Studios LLC bajo la Licencia Apache, Versión 2.0 (Apache-2.0):
- Concesión de Derechos de Autor y Patentes (Apache-2.0 §§ 2–3): Cualquier persona o empresa puede implementar lectores, escritores, exportadores, validadores o reproductores independientes de archivos
.oimy.ovm—tanto de código abierto como comerciales— libres de regalías. Conforme a la Sección 3 deApache-2.0, cada contribuyente otorga una licencia de patente perpetua, mundial y libre de regalías para implementar la especificación, sujeta a la cláusula de rescisión defensiva automática si una entidad inicia litigios de patentes alegando que el formato o los paquetes de referencia infringen alguna patente. - Política de Marcas y Conformidad del Estándar (Apache-2.0 § 6): Aunque el código y la especificación son abiertos, las denominaciones OIM™, Object Interactive Media™, OVM™, OIM Media™ y los logotipos asociados son marcas comerciales de Xue Studios LLC. Las implementaciones de terceros pueden declarar compatibilidad descriptiva (por ejemplo, «compatible con el formato
.oim»), pero únicamente pueden ostentar la denominación «OIM Certified» u «OIM Conformant» si superan el 100 % de la batería oficial de vectores canónicos de@oim-media/specsin truncar el chunkOIM_Data, sin alterar elimage_hashy respetando los indicadoresreediteimmutable_lock. - Marco Legal Completo: Consulta los Términos de Servicio — Sección 2 (Estándar Abierto y Política de Conformidad de Marcas), la Política de Privacidad y el archivo de Licencias de Terceros.