@oim-media/spec 0.0.0-stage → 1.27.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (130) hide show
  1. package/BINARY_SPEC.md +186 -0
  2. package/CANONICAL_SPEC.md +409 -0
  3. package/LICENSE +55 -0
  4. package/NOTICE +6 -0
  5. package/README.md +444 -3
  6. package/action-cases.json +13 -0
  7. package/artistic-text-cases.json +690 -0
  8. package/keyframe-cases.json +32 -0
  9. package/keys.json +22 -0
  10. package/marks-cases.json +34 -0
  11. package/package.json +45 -3
  12. package/pattern-cases.json +552 -0
  13. package/schema/oim.schema.json +2920 -0
  14. package/schema/ovm.schema.json +2701 -0
  15. package/text-effect-cases.json +835 -0
  16. package/tools/parity-core.mjs +122 -0
  17. package/tools/parity.mjs +86 -0
  18. package/vectors/README.md +103 -0
  19. package/vectors/accessibility.expected.json +67 -0
  20. package/vectors/accessibility.oim.png +0 -0
  21. package/vectors/artistic-text.expected.json +84 -0
  22. package/vectors/artistic-text.oim.png +0 -0
  23. package/vectors/asymmetric-borders.expected.json +29 -0
  24. package/vectors/asymmetric-borders.oim.png +0 -0
  25. package/vectors/background-pattern-order.expected.json +95 -0
  26. package/vectors/background-pattern-order.oim.png +0 -0
  27. package/vectors/basic-text.expected.json +29 -0
  28. package/vectors/basic-text.oim.png +0 -0
  29. package/vectors/color-blending-opacity.expected.json +47 -0
  30. package/vectors/color-blending-opacity.oim.png +0 -0
  31. package/vectors/crop.expected.json +32 -0
  32. package/vectors/crop.oim.png +0 -0
  33. package/vectors/embedded-font-woff.expected.json +57 -0
  34. package/vectors/embedded-font-woff.oim.png +0 -0
  35. package/vectors/extreme-aspect-ratios.expected.json +29 -0
  36. package/vectors/extreme-aspect-ratios.oim.png +0 -0
  37. package/vectors/filter-effects.expected.json +33 -0
  38. package/vectors/filter-effects.oim.png +0 -0
  39. package/vectors/flip.expected.json +47 -0
  40. package/vectors/flip.oim.png +0 -0
  41. package/vectors/frame-shapes.expected.json +73 -0
  42. package/vectors/frame-shapes.oim.png +0 -0
  43. package/vectors/full-monty-composite.expected.json +96 -0
  44. package/vectors/full-monty-composite.oim.png +0 -0
  45. package/vectors/golden/accessibility.web.png +0 -0
  46. package/vectors/golden/artistic-text.web.png +0 -0
  47. package/vectors/golden/asymmetric-borders.web.png +0 -0
  48. package/vectors/golden/background-pattern-order.web.png +0 -0
  49. package/vectors/golden/basic-text.web.png +0 -0
  50. package/vectors/golden/color-blending-opacity.web.png +0 -0
  51. package/vectors/golden/crop.web.png +0 -0
  52. package/vectors/golden/embedded-font-woff.web.png +0 -0
  53. package/vectors/golden/exceptions.json +3 -0
  54. package/vectors/golden/extreme-aspect-ratios.web.png +0 -0
  55. package/vectors/golden/filter-effects.web.png +0 -0
  56. package/vectors/golden/flip.web.png +0 -0
  57. package/vectors/golden/frame-shapes.web.png +0 -0
  58. package/vectors/golden/full-monty-composite.web.png +0 -0
  59. package/vectors/golden/groups-and-parallax.web.png +0 -0
  60. package/vectors/golden/image-fill.web.png +0 -0
  61. package/vectors/golden/interactive-chained-actions.web.png +0 -0
  62. package/vectors/golden/interactive-hover.web.png +0 -0
  63. package/vectors/golden/interactive.web.png +0 -0
  64. package/vectors/golden/layer-blur.web.png +0 -0
  65. package/vectors/golden/layer-grain.web.png +0 -0
  66. package/vectors/golden/layer-ordering-zindex.web.png +0 -0
  67. package/vectors/golden/legacy-text-chunk.web.png +0 -0
  68. package/vectors/golden/media-filters.web.png +0 -0
  69. package/vectors/golden/multi-container-layout.web.png +0 -0
  70. package/vectors/golden/multilang-cjk-rtl.web.png +0 -0
  71. package/vectors/golden/nested-groups.web.png +0 -0
  72. package/vectors/golden/palettes.web.png +0 -0
  73. package/vectors/golden/parallax-infinite.web.png +0 -0
  74. package/vectors/golden/pattern-color-roles.web.png +0 -0
  75. package/vectors/golden/reedit-english.web.png +0 -0
  76. package/vectors/golden/reveal-and-play.web.png +0 -0
  77. package/vectors/golden/shadow-effects.web.png +0 -0
  78. package/vectors/golden/shape-shadows.web.png +0 -0
  79. package/vectors/golden/strokes-and-borders.web.png +0 -0
  80. package/vectors/golden/text-runs-precalculated.web.png +0 -0
  81. package/vectors/golden/transparent-background.web.png +0 -0
  82. package/vectors/golden/typography-styling.web.png +0 -0
  83. package/vectors/groups-and-parallax.expected.json +90 -0
  84. package/vectors/groups-and-parallax.oim.png +0 -0
  85. package/vectors/image-fill.expected.json +54 -0
  86. package/vectors/image-fill.oim.png +0 -0
  87. package/vectors/interactive-chained-actions.expected.json +77 -0
  88. package/vectors/interactive-chained-actions.oim.png +0 -0
  89. package/vectors/interactive-hover.expected.json +36 -0
  90. package/vectors/interactive-hover.oim.png +0 -0
  91. package/vectors/interactive.expected.json +121 -0
  92. package/vectors/interactive.oim.png +0 -0
  93. package/vectors/layer-blur.expected.json +48 -0
  94. package/vectors/layer-blur.oim.png +0 -0
  95. package/vectors/layer-grain.expected.json +52 -0
  96. package/vectors/layer-grain.oim.png +0 -0
  97. package/vectors/layer-ordering-zindex.expected.json +62 -0
  98. package/vectors/layer-ordering-zindex.oim.png +0 -0
  99. package/vectors/legacy-text-chunk.expected.json +29 -0
  100. package/vectors/legacy-text-chunk.oim.png +0 -0
  101. package/vectors/media-filters.expected.json +50 -0
  102. package/vectors/media-filters.oim.png +0 -0
  103. package/vectors/multi-container-layout.expected.json +45 -0
  104. package/vectors/multi-container-layout.oim.png +0 -0
  105. package/vectors/multilang-cjk-rtl.expected.json +30 -0
  106. package/vectors/multilang-cjk-rtl.oim.png +0 -0
  107. package/vectors/nested-groups.expected.json +68 -0
  108. package/vectors/nested-groups.oim.png +0 -0
  109. package/vectors/palettes.expected.json +57 -0
  110. package/vectors/palettes.oim.png +0 -0
  111. package/vectors/parallax-infinite.expected.json +39 -0
  112. package/vectors/parallax-infinite.oim.png +0 -0
  113. package/vectors/pattern-color-roles.expected.json +68 -0
  114. package/vectors/pattern-color-roles.oim.png +0 -0
  115. package/vectors/reedit-english.expected.json +45 -0
  116. package/vectors/reedit-english.oim.png +0 -0
  117. package/vectors/reveal-and-play.expected.json +107 -0
  118. package/vectors/reveal-and-play.oim.png +0 -0
  119. package/vectors/shadow-effects.expected.json +34 -0
  120. package/vectors/shadow-effects.oim.png +0 -0
  121. package/vectors/shape-shadows.expected.json +62 -0
  122. package/vectors/shape-shadows.oim.png +0 -0
  123. package/vectors/strokes-and-borders.expected.json +31 -0
  124. package/vectors/strokes-and-borders.oim.png +0 -0
  125. package/vectors/text-runs-precalculated.expected.json +46 -0
  126. package/vectors/text-runs-precalculated.oim.png +0 -0
  127. package/vectors/transparent-background.expected.json +30 -0
  128. package/vectors/transparent-background.oim.png +0 -0
  129. package/vectors/typography-styling.expected.json +34 -0
  130. package/vectors/typography-styling.oim.png +0 -0
@@ -0,0 +1,122 @@
1
+ // Copyright (c) 2026 Xue Studios LLC
2
+ // SPDX-License-Identifier: Apache-2.0
3
+ // Visual conformance metric (CANONICAL_SPEC.md, "Visual conformance"). Zero dependencies.
4
+ import zlib from 'node:zlib';
5
+
6
+ export const CHANNEL_TOLERANCE = 40;
7
+ // Calibrated 2026-11-07: passing text noise reaches 0.265 %, a unitless letterSpacing bug 0.509 %.
8
+ export const MAX_AREA = 0.004;
9
+ export const MAX_BLOB = 24 * 24;
10
+
11
+ const SIGNATURE = Buffer.from([137, 80, 78, 71, 13, 10, 26, 10]);
12
+
13
+ const paeth = (a, b, c) => {
14
+ const p = a + b - c, pa = Math.abs(p - a), pb = Math.abs(p - b), pc = Math.abs(p - c);
15
+ return pa <= pb && pa <= pc ? a : pb <= pc ? b : c;
16
+ };
17
+
18
+ /** Decodes an 8-bit, non-interlaced RGB or RGBA PNG to RGBA. */
19
+ export const decodePng = (buf) => {
20
+ if (!buf.subarray(0, 8).equals(SIGNATURE)) throw new Error('unsupported PNG: bad signature');
21
+ let pos = 8, width = 0, height = 0, type = 0;
22
+ const idat = [];
23
+ while (pos < buf.length) {
24
+ const len = buf.readUInt32BE(pos), kind = buf.toString('latin1', pos + 4, pos + 8);
25
+ const data = buf.subarray(pos + 8, pos + 8 + len);
26
+ if (kind === 'IHDR') {
27
+ width = data.readUInt32BE(0); height = data.readUInt32BE(4); type = data[9];
28
+ if (data[8] !== 8 || (type !== 2 && type !== 6) || data[12] !== 0)
29
+ throw new Error(`unsupported PNG: bit depth ${data[8]}, colour type ${type}, interlace ${data[12]}`);
30
+ } else if (kind === 'IDAT') idat.push(data);
31
+ else if (kind === 'IEND') break;
32
+ pos += 12 + len;
33
+ }
34
+ const raw = zlib.inflateSync(Buffer.concat(idat));
35
+ const bpp = type === 6 ? 4 : 3, stride = width * bpp;
36
+ const out = Buffer.alloc(stride * height);
37
+ for (let y = 0; y < height; y++) {
38
+ const f = raw[y * (stride + 1)], src = y * (stride + 1) + 1, row = y * stride;
39
+ for (let i = 0; i < stride; i++) {
40
+ const a = i >= bpp ? out[row + i - bpp] : 0, b = y ? out[row - stride + i] : 0;
41
+ const c = i >= bpp && y ? out[row - stride + i - bpp] : 0;
42
+ const pred = f === 0 ? 0 : f === 1 ? a : f === 2 ? b : f === 3 ? (a + b) >> 1 : f === 4 ? paeth(a, b, c) : NaN;
43
+ if (Number.isNaN(pred)) throw new Error(`unsupported PNG: filter ${f}`);
44
+ out[row + i] = (raw[src + i] + pred) & 255;
45
+ }
46
+ }
47
+ const rgba = new Uint8Array(width * height * 4);
48
+ for (let p = 0; p < width * height; p++) {
49
+ rgba[p * 4] = out[p * bpp]; rgba[p * 4 + 1] = out[p * bpp + 1]; rgba[p * 4 + 2] = out[p * bpp + 2];
50
+ rgba[p * 4 + 3] = bpp === 4 ? out[p * bpp + 3] : 255;
51
+ }
52
+ return { width, height, rgba };
53
+ };
54
+
55
+ const CRC_TABLE = Array.from({ length: 256 }, (_, n) => {
56
+ let c = n;
57
+ for (let k = 0; k < 8; k++) c = c & 1 ? 0xedb88320 ^ (c >>> 1) : c >>> 1;
58
+ return c >>> 0;
59
+ });
60
+ const crc32 = (buf) => {
61
+ let c = 0xffffffff;
62
+ for (const byte of buf) c = CRC_TABLE[(c ^ byte) & 255] ^ (c >>> 8);
63
+ return (c ^ 0xffffffff) >>> 0;
64
+ };
65
+ const chunk = (kind, data) => {
66
+ const head = Buffer.alloc(8); head.writeUInt32BE(data.length); head.write(kind, 4, 'latin1');
67
+ const crc = Buffer.alloc(4); crc.writeUInt32BE(crc32(Buffer.concat([head.subarray(4), data])));
68
+ return Buffer.concat([head, data, crc]);
69
+ };
70
+
71
+ /** Encodes RGBA as an 8-bit RGBA PNG (filter 0). */
72
+ export const encodePng = ({ width, height, rgba }) => {
73
+ const ihdr = Buffer.alloc(13);
74
+ ihdr.writeUInt32BE(width); ihdr.writeUInt32BE(height, 4); ihdr[8] = 8; ihdr[9] = 6;
75
+ const raw = Buffer.alloc((width * 4 + 1) * height);
76
+ for (let y = 0; y < height; y++) raw.set(rgba.subarray(y * width * 4, (y + 1) * width * 4), y * (width * 4 + 1) + 1);
77
+ return Buffer.concat([SIGNATURE, chunk('IHDR', ihdr), chunk('IDAT', zlib.deflateSync(raw)), chunk('IEND', Buffer.alloc(0))]);
78
+ };
79
+
80
+ // Composite over white: a transparent render is a blank page, not black.
81
+ const onWhite = (rgba, i) => {
82
+ const a = rgba[i + 3] / 255;
83
+ return [0, 1, 2].map((k) => rgba[i + k] * a + 255 * (1 - a));
84
+ };
85
+
86
+ /** Compares a render against a golden under the visual conformance metric. */
87
+ export const compare = (golden, render) => {
88
+ const { width: w, height: h } = golden;
89
+ if (w !== render.width || h !== render.height)
90
+ return { ok: false, reason: `size ${render.width}x${render.height}, expected ${w}x${h}`, area: 1, largestBlob: 0, mask: new Uint8Array(0) };
91
+ const diff = new Uint8Array(w * h);
92
+ for (let p = 0; p < w * h; p++) {
93
+ const g = onWhite(golden.rgba, p * 4), r = onWhite(render.rgba, p * 4);
94
+ diff[p] = g.some((v, k) => Math.abs(Math.round(v) - Math.round(r[k])) > CHANNEL_TOLERANCE) ? 1 : 0;
95
+ }
96
+ // 3x3 erosion: antialiasing and hinting leave 1-2 px lines; a defect is an area.
97
+ const mask = new Uint8Array(w * h);
98
+ let count = 0;
99
+ for (let y = 1; y < h - 1; y++)
100
+ for (let x = 1; x < w - 1; x++) {
101
+ let all = 1;
102
+ for (let dy = -1; dy <= 1 && all; dy++) for (let dx = -1; dx <= 1 && all; dx++) all = diff[(y + dy) * w + x + dx];
103
+ if (all) { mask[y * w + x] = 1; count++; }
104
+ }
105
+ let largestBlob = 0;
106
+ const seen = new Uint8Array(w * h), stack = [];
107
+ for (let p = 0; p < w * h; p++) {
108
+ if (!mask[p] || seen[p]) continue;
109
+ let size = 0;
110
+ stack.push(p); seen[p] = 1;
111
+ while (stack.length) {
112
+ const q = stack.pop(), x = q % w;
113
+ size++;
114
+ for (const n of [q - w, q + w, x > 0 ? q - 1 : -1, x < w - 1 ? q + 1 : -1])
115
+ if (n >= 0 && n < w * h && mask[n] && !seen[n]) { seen[n] = 1; stack.push(n); }
116
+ }
117
+ largestBlob = Math.max(largestBlob, size);
118
+ }
119
+ const area = count / (w * h);
120
+ const ok = area <= MAX_AREA && largestBlob <= MAX_BLOB;
121
+ return { ok, reason: ok ? undefined : `area ${(area * 100).toFixed(3)}%, largest blob ${largestBlob}px`, area, largestBlob, mask };
122
+ };
@@ -0,0 +1,86 @@
1
+ #!/usr/bin/env node
2
+ // Copyright (c) 2026 Xue Studios LLC
3
+ // SPDX-License-Identifier: Apache-2.0
4
+ // Visual conformance: compares a renderer's PNGs against the web goldens.
5
+ // Usage: node parity.mjs <renders-dir> --renderer <name> [--out <diff-dir>]
6
+ import fs from 'node:fs';
7
+ import path from 'node:path';
8
+ import { fileURLToPath } from 'node:url';
9
+ import { decodePng, encodePng, compare } from './parity-core.mjs';
10
+
11
+ const HERE = path.dirname(fileURLToPath(import.meta.url));
12
+
13
+ const names = (dir, suffix) =>
14
+ fs.readdirSync(dir).filter((f) => f.endsWith(suffix)).map((f) => f.slice(0, -suffix.length)).sort();
15
+
16
+ const diffImage = (golden, mask) => {
17
+ const rgba = new Uint8Array(golden.rgba.length);
18
+ for (let p = 0; p < mask.length; p++) {
19
+ const i = p * 4;
20
+ if (mask[p]) rgba.set([255, 0, 0, 255], i);
21
+ else rgba.set([golden.rgba[i] * 0.3, golden.rgba[i + 1] * 0.3, golden.rgba[i + 2] * 0.3, 255], i);
22
+ }
23
+ return { width: golden.width, height: golden.height, rgba };
24
+ };
25
+
26
+ export const runParity = ({
27
+ rendersDir,
28
+ renderer,
29
+ outDir,
30
+ goldenDir = path.join(HERE, '..', 'vectors', 'golden'),
31
+ vectorsDir = path.join(HERE, '..', 'vectors'),
32
+ }) => {
33
+ const failures = [], lines = [];
34
+ const vectors = names(vectorsDir, '.oim.png');
35
+ const goldens = names(goldenDir, '.web.png');
36
+ const exceptionsFile = path.join(goldenDir, 'exceptions.json');
37
+ const exceptions = (fs.existsSync(exceptionsFile) ? JSON.parse(fs.readFileSync(exceptionsFile, 'utf8')) : {})[renderer] ?? {};
38
+
39
+ for (const v of vectors) if (!goldens.includes(v)) failures.push(`${v}: no golden for this vector, regenerate the goldens`);
40
+ for (const g of goldens) if (!vectors.includes(g)) failures.push(`${g}: orphan golden, no vector`);
41
+ for (const e of Object.keys(exceptions)) if (!vectors.includes(e)) failures.push(`${e}: unknown vector in exceptions`);
42
+ if (outDir) fs.mkdirSync(outDir, { recursive: true });
43
+
44
+ for (const name of goldens.filter((g) => vectors.includes(g))) {
45
+ const renderPath = path.join(rendersDir, `${name}.png`);
46
+ const excepted = name in exceptions;
47
+ // `broken`: no comparison happened (missing, unreadable or wrong-size render). An exception never covers it.
48
+ let verdict, detail = '', broken = false;
49
+ if (!fs.existsSync(renderPath)) {
50
+ verdict = 'FAIL'; detail = 'missing render'; broken = true;
51
+ } else {
52
+ let golden, r;
53
+ try {
54
+ golden = decodePng(fs.readFileSync(path.join(goldenDir, `${name}.web.png`)));
55
+ r = compare(golden, decodePng(fs.readFileSync(renderPath)));
56
+ } catch (err) {
57
+ r = { ok: false, reason: err.message, area: 1, largestBlob: 0 };
58
+ }
59
+ broken = !r.ok && !r.mask?.length;
60
+ detail = r.reason ?? `area=${(r.area * 100).toFixed(3)}% blob=${r.largestBlob}px`;
61
+ verdict = r.ok ? 'PASS' : 'FAIL';
62
+ if (!r.ok && outDir && r.mask?.length) fs.writeFileSync(path.join(outDir, `${name}.diff.png`), encodePng(diffImage(golden, r.mask)));
63
+ }
64
+ if (excepted && verdict === 'PASS') failures.push(`${name}: passes, remove the exception for ${renderer}`);
65
+ else if (excepted && !broken) verdict = 'EXCEPTED';
66
+ else if (verdict === 'FAIL') failures.push(`${name}: ${detail}`);
67
+ lines.push(`${name.padEnd(32)} ${verdict.padEnd(8)} ${detail}`);
68
+ }
69
+ if (!lines.length) failures.push('no vectors compared: check the vectors and goldens directories');
70
+ return { failures, lines };
71
+ };
72
+
73
+ if (process.argv[1] && fileURLToPath(import.meta.url) === path.resolve(process.argv[1])) {
74
+ const args = process.argv.slice(2);
75
+ const flag = (n) => { const i = args.indexOf(n); return i >= 0 ? args[i + 1] : undefined; };
76
+ const rendersDir = args[0], renderer = flag('--renderer');
77
+ if (!rendersDir || !renderer) {
78
+ console.error('usage: parity.mjs <renders-dir> --renderer <name> [--out <diff-dir>]');
79
+ process.exit(2);
80
+ }
81
+ const { failures, lines } = runParity({ rendersDir, renderer, outDir: flag('--out') });
82
+ lines.forEach((l) => console.log(l));
83
+ failures.forEach((f) => console.error(`✖ ${f}`));
84
+ console.log(failures.length ? `\n${failures.length} failure(s)` : '\nvisual parity: OK');
85
+ process.exit(failures.length ? 1 : 0);
86
+ }
@@ -0,0 +1,103 @@
1
+ # Conformance vectors
2
+
3
+ Each vector is a real `.oim` with the result that a reader must produce when
4
+ opening it. The JSON Schema in `../schema/` says what **shape** a `.oim` has;
5
+ this says what **result** comes out of a specific one.
6
+
7
+ ```
8
+ <name>.oim.png the file, signed, exactly as the system produces it
9
+ <name>.expected.json what a conforming reader must extract from it
10
+ ```
11
+
12
+ ## The Normative Vectors
13
+
14
+ A reduced suite cannot tell apart an implementation that **reads the format**
15
+ from one that **gets a simple case right by chance**. These 37 vectors cover the
16
+ initial 20-vector matrix plus every capability added since contract 1.10.0:
17
+
18
+ | # | Vector | What it covers |
19
+ |---|---|---|
20
+ | 1 | `basic-text` | The minimum: PNG container, `zTXt` chunk, zlib, two languages, geometry in `cqw` |
21
+ | 2 | `groups-and-parallax` | Simple hierarchy (`childIds` / `groupId`) and depth (`parallax`) |
22
+ | 3 | `interactive` | `isInteractive`, `states.active.actions`, per-side corners and `min_player` |
23
+ | 4 | `multilang-cjk-rtl` | Complex multilingual coverage: Spanish, Japanese (CJK: `こんにちは世界`) and Arabic (RTL: `مرحبا بالعالم`) |
24
+ | 5 | `typography-styling` | Advanced typography: `fontFamily`, `fontWeight`, `fontStyle: "italic"`, `textTransform`, `letterSpacing`, `lineHeight` |
25
+ | 6 | `nested-groups` | Multi-level hierarchy: a root group containing a subgroup with child elements |
26
+ | 7 | `asymmetric-borders` | Independent corner radii per corner (`borderRadius: "10px 25px 50px 0px"`) |
27
+ | 8 | `shadow-effects` | Shadow system: `shadowEnabled`, `shadowColor`, `shadowBlur`, `shadowOffsetX`, `shadowOffsetY`, `shadowOpacity` |
28
+ | 9 | `color-blending-opacity` | Visual composition: layer opacity (`opacity: 70`) and blend modes (`blendMode: "multiply"`) |
29
+ | 10 | `strokes-and-borders` | Vector stroke and outlines: `borderWidth`, `borderColor`, `strokeStyle: "dashed"` |
30
+ | 11 | `parallax-infinite` | Continuous horizontal looping parallax effect (`mode: "infinite_horizontal"`, `speed_px_sec`, `direction`) |
31
+ | 12 | `interactive-hover` | Cursor interactive state: `states.hover` with `scale`, `rotation`, `opacity` |
32
+ | 13 | `interactive-chained-actions` | Several interactive actions chained when a layer is activated (`toggle`, `hide`) |
33
+ | 14 | `filter-effects` | CSS visual filters: `filterBlur`, `filterBrightness`, `filterContrast`, `filterSaturate`, `filterHueRotate` |
34
+ | 15 | `text-runs-precalculated` | Precalculated layout for native non-web renderers (`textRuns` with `lines`, `width`, `yOffset`) |
35
+ | 16 | `multi-container-layout` | Several `container` slots organising the spatial layout of the document |
36
+ | 17 | `extreme-aspect-ratios` | Ultra-wide panoramic canvas (`1600x400`, 4:1 ratio) testing container consistency |
37
+ | 18 | `transparent-background` | Strictly transparent background (`fillColor: "transparent"`) under the normative colour rule |
38
+ | 19 | `layer-ordering-zindex` | Strict stacking sequence with slots overlapping at identical coordinates |
39
+ | 20 | `full-monty-composite` | Comprehensive master document combining multilingual content, active/hover interactivity, groups, parallax and shadows |
40
+ | 21 | `legacy-text-chunk` | The same document as `basic-text` in the legacy container: a `tEXt` chunk with base64 of raw deflate |
41
+ | 22 | `layer-blur` | Layer blur (`layerBlur`) on the style of a text and of a shape |
42
+ | 23 | `layer-grain` | Per-layer grain (contract 1.10.0): `grain`, `grainIntensity`, `grainSize` and `grainBlend` |
43
+ | 24 | `flip` | Layer flip (contract 1.14.0): `flipX` and `flipY` on rotated shapes |
44
+ | 25 | `crop` | Image crop (contract 1.15.0): `imageCrop` framing an image at x 20 %, y 70 % and zoom 2 |
45
+ | 26 | `accessibility` | Accessibility (contract 1.16.0): document `description`, per-language `alt` and `decorative` |
46
+ | 27 | `image-fill` | Image fill (contract 1.17.0): shapes filled with an embedded photo and `fillImageCrop` |
47
+ | 28 | `media-filters` | Media filters (contract 1.18.0): `filterPreset` on a photo and on a circular frame with blur and stroke |
48
+ | 29 | `frame-shapes` | Frame shapes (contract 1.19.0): a cloud, a custom SVG path (`svgViewBox`) and a star with a flipped photo |
49
+ | 30 | `palettes` | Named palettes (contract 1.20.0): `dark` and `light`, `defaultPalette: "dark"`, and colour roles |
50
+ | 31 | `pattern-color-roles` | Colour roles in patterns and shadows (contract 1.21.0) |
51
+ | 32 | `reveal-and-play` | Per-layer reveal and the `play` action (contract 1.22.0) |
52
+ | 33 | `reedit-english` | Re-edit permission in English (contract 1.23.0): `reedit: "public"`, signed by the production service |
53
+ | 34 | `embedded-font-woff` | Embedded font (contract 1.27.0): Inter bold as a static WOFF subset (`fonts[].format: "woff"`) |
54
+ | 35 | `background-pattern-order` | A pattern background listed last still paints under every layer; pattern tile size; unitless `letterSpacing` on centred text |
55
+ | 36 | `shape-shadows` | Hard shadows on a circle and on a circular photo frame: the shadow follows the shape, not its box |
56
+ | 37 | `artistic-text` | Artistic text (4.6): a curved 3D title with shaded extrusion, outer and inner strokes and a gradient, and a two-line word, drawn with the embedded Inter subset |
57
+
58
+ ## How to use it from another language
59
+
60
+ 1. Open the `.oim` as a PNG and look for the metadata chunk: `zTXt` (key `OIM_Data`; the one written since 29 Oct 2026), or `tEXt` / `iTXt` (keys `OIM_Data` or `OIM_JSON_DATA`).
61
+ 2. Decompress the payload: if it comes in base64, decode it first; decompression is **raw deflate** (no zlib header, RFC 1951) or standard zlib.
62
+ 3. Interpret the result as UTF-8 JSON: these are the document's metadata.
63
+ 4. Compare with `<name>.expected.json`, ignoring its `description` field.
64
+
65
+ What is compared is what two implementations can genuinely disagree on: the
66
+ geometry **already normalised** to `cqw` or user units, the hierarchy, the depth,
67
+ the typography and the declared interactivity. Not the whole metadata — a full
68
+ dump breaks every time the compiler adds a field, and then it gets regenerated
69
+ without a look, which is a vector that no longer checks anything.
70
+
71
+ ## The signature
72
+
73
+ All 37 vectors are signed with the public key published in `packages/spec/keys.json`.
74
+ The signature covers the canonical serialisation
75
+ of the metadata and the cryptographic hash of the pixels (`image_hash` over the `IDAT` chunks).
76
+ A reader that verifies must rebuild that canonical form following the seven rules
77
+ specified in the `README.md` at the root of `packages/spec`; one that only reads the format
78
+ may ignore it, but then it must not claim to validate authorship.
79
+
80
+ ## How they were generated
81
+
82
+ The vectors are generated by Xue Studios' signing service, through the same
83
+ production pipeline that signs every published `.oim`: the slots are normalised
84
+ to `cqw`, the file is sanitised, signed and embedded. The generator is not part
85
+ of this repository; the output is committed here as-is, so every implementation
86
+ reads the same files.
87
+
88
+ `interactive` is the exception: it comes from a real editor export, and that is
89
+ why it carries things the normalisation pipeline alone does not produce.
90
+
91
+ The base image weighs about 3 KB and is reused across vectors. A published
92
+ `.oim` weighs megabytes because it carries its rasterised canvas inside; the
93
+ image is not what these vectors check.
94
+
95
+ ## Keeping them from rotting
96
+
97
+ `packages/core/src/conformance-vectors.test.ts` runs them in every suite: if the
98
+ reference implementation changes what it produces, the vectors fail. It is
99
+ circular on purpose and in one direction — the reference defines the result, and
100
+ the test prevents it from moving without the vectors moving with it.
101
+
102
+ The day a second reader exists and disagrees, there is a concrete file to discuss
103
+ instead of two opinions.
@@ -0,0 +1,67 @@
1
+ {
2
+ "description": "Accessibility (contract 1.16.0): the work's `description`, a per-language `alt` on an image and `decorative` on a shape.",
3
+ "canvas": {
4
+ "natural_height": 600,
5
+ "natural_width": 800
6
+ },
7
+ "signed": true,
8
+ "key_version": "v1",
9
+ "slots": [
10
+ {
11
+ "id": "titulo",
12
+ "type": "text",
13
+ "position": [
14
+ 10,
15
+ 6.6667
16
+ ],
17
+ "name": "Titulo",
18
+ "content": {
19
+ "en": "Hello",
20
+ "es": "Hola"
21
+ },
22
+ "style": {
23
+ "width": "62.5cqw",
24
+ "height": "10.0cqw",
25
+ "fontSize": "5.0cqw"
26
+ }
27
+ },
28
+ {
29
+ "id": "foto",
30
+ "type": "image",
31
+ "position": [
32
+ 12.5,
33
+ 26.6667
34
+ ],
35
+ "name": "Foto",
36
+ "content": {
37
+ "*": "https://cdn.oim.media/demos/crop.png"
38
+ },
39
+ "style": {
40
+ "width": "37.5cqw",
41
+ "height": "25.0cqw",
42
+ "alt": {
43
+ "en": "A fjord between mountains",
44
+ "es": "Un fiordo entre montañas"
45
+ }
46
+ }
47
+ },
48
+ {
49
+ "id": "adorno",
50
+ "type": "geo",
51
+ "position": [
52
+ 65,
53
+ 26.6667
54
+ ],
55
+ "name": "Adorno",
56
+ "content": {
57
+ "es": ""
58
+ },
59
+ "style": {
60
+ "width": "25.0cqw",
61
+ "height": "25.0cqw",
62
+ "fillColor": "#264653",
63
+ "decorative": true
64
+ }
65
+ }
66
+ ]
67
+ }
Binary file
@@ -0,0 +1,84 @@
1
+ {
2
+ "description": "Artistic text (4.6): a curved 3D title with shaded extrusion, outer and inner strokes and a built-in gradient, and a second two-line word with no curve. Inter bold embedded (the embedded-font-woff subset), so every reader draws the same glyphs.",
3
+ "canvas": {
4
+ "natural_height": 600,
5
+ "natural_width": 800
6
+ },
7
+ "signed": true,
8
+ "key_version": "v1",
9
+ "reedit": "author_only",
10
+ "fonts": [
11
+ {
12
+ "family": "Inter",
13
+ "name": "oim-26dd479f-Inter",
14
+ "weight": 700,
15
+ "style": "normal",
16
+ "format": "woff"
17
+ }
18
+ ],
19
+ "slots": [
20
+ {
21
+ "id": "retro-title",
22
+ "type": "artistic-text",
23
+ "position": [
24
+ 0,
25
+ 10
26
+ ],
27
+ "name": "Retro title",
28
+ "content": {
29
+ "es": "Fuente"
30
+ },
31
+ "style": {
32
+ "width": "100.0cqw",
33
+ "height": "27.5cqw",
34
+ "fontSize": "15.0cqw",
35
+ "fillColor": "#f9a8d4",
36
+ "fontFamily": "Inter",
37
+ "fontWeight": "700",
38
+ "lineHeight": 1.25,
39
+ "artisticPreset": "retro-wave-3d",
40
+ "extrusionDepth": 18,
41
+ "extrusionAngle": 45,
42
+ "extrusionColor": "#DB2777",
43
+ "extrusionShading": true,
44
+ "curveAmount": 60,
45
+ "curveType": "arc",
46
+ "outerStrokeColor": "#1e1b4b",
47
+ "outerStrokeWidth": 4,
48
+ "innerStrokeColor": "#ffffff",
49
+ "innerStrokeWidth": 1,
50
+ "textGradientType": "sunset-gold"
51
+ }
52
+ },
53
+ {
54
+ "id": "two-lines",
55
+ "type": "artistic-text",
56
+ "position": [
57
+ 25,
58
+ 56.6667
59
+ ],
60
+ "name": "Two lines",
61
+ "content": {
62
+ "es": "ciudad\nsur"
63
+ },
64
+ "style": {
65
+ "width": "50.0cqw",
66
+ "height": "25.0cqw",
67
+ "fontSize": "8.0cqw",
68
+ "fillColor": "#fde047",
69
+ "fontFamily": "Inter",
70
+ "fontWeight": "700",
71
+ "lineHeight": 1.25,
72
+ "artisticPreset": "custom",
73
+ "extrusionDepth": 10,
74
+ "extrusionAngle": 135,
75
+ "extrusionColor": "#7c2d12",
76
+ "extrusionShading": false,
77
+ "curveAmount": 0,
78
+ "curveType": "arc",
79
+ "outerStrokeColor": "#000000",
80
+ "outerStrokeWidth": 2
81
+ }
82
+ }
83
+ ]
84
+ }
Binary file
@@ -0,0 +1,29 @@
1
+ {
2
+ "description": "Independent corner radii for each corner.",
3
+ "canvas": {
4
+ "natural_height": 600,
5
+ "natural_width": 800
6
+ },
7
+ "signed": true,
8
+ "key_version": "v1",
9
+ "slots": [
10
+ {
11
+ "id": "caja_curva",
12
+ "type": "geo",
13
+ "position": [
14
+ 12.5,
15
+ 16.6667
16
+ ],
17
+ "name": "Caja Asimétrica",
18
+ "content": {
19
+ "es": ""
20
+ },
21
+ "style": {
22
+ "width": "37.5cqw",
23
+ "height": "18.75cqw",
24
+ "borderRadius": "10px 25px 50px 0px",
25
+ "fillColor": "#3b82f6"
26
+ }
27
+ }
28
+ ]
29
+ }
@@ -0,0 +1,95 @@
1
+ {
2
+ "description": "A background pattern listed after the layers it sits behind, its stored tile, and a text with a unitless letterSpacing.",
3
+ "canvas": {
4
+ "natural_height": 600,
5
+ "natural_width": 800
6
+ },
7
+ "languages": [
8
+ "es"
9
+ ],
10
+ "signed": true,
11
+ "key_version": "v1",
12
+ "palettes": {},
13
+ "reedit": "public",
14
+ "fonts": [
15
+ {
16
+ "family": "Inter",
17
+ "name": "oim-f8947d8f-Inter",
18
+ "weight": 400,
19
+ "style": "normal",
20
+ "format": "woff"
21
+ }
22
+ ],
23
+ "slots": [
24
+ {
25
+ "id": "front-card",
26
+ "type": "geo",
27
+ "position": [
28
+ 25,
29
+ 25
30
+ ],
31
+ "name": "Geo 1",
32
+ "style": {
33
+ "width": "50.0cqw",
34
+ "height": "37.5cqw",
35
+ "fillColor": "#be123c",
36
+ "strokeStyle": "solid",
37
+ "geoShape": "rectangle"
38
+ }
39
+ },
40
+ {
41
+ "id": "spaced-text",
42
+ "type": "text",
43
+ "position": [
44
+ 27.5,
45
+ 43.3333
46
+ ],
47
+ "name": "Text 2",
48
+ "content": {
49
+ "es": "Espaciado"
50
+ },
51
+ "style": {
52
+ "width": "45.0cqw",
53
+ "height": "10.0cqw",
54
+ "fontSize": "6.0cqw",
55
+ "fillColor": "#ffffff",
56
+ "fontFamily": "Inter",
57
+ "letterSpacing": 24,
58
+ "textRuns": {
59
+ "es": {
60
+ "fontSize": 48,
61
+ "lineHeight": 60,
62
+ "lines": [
63
+ {
64
+ "text": "Espaciado",
65
+ "width": 235.2,
66
+ "yOffset": 0
67
+ }
68
+ ]
69
+ }
70
+ }
71
+ }
72
+ },
73
+ {
74
+ "id": "background-pattern-slot",
75
+ "type": "geo",
76
+ "position": [
77
+ 0,
78
+ 0
79
+ ],
80
+ "name": "Fondo Vectorial Animado",
81
+ "content": {
82
+ "en": "<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"160\" height=\"160\" viewBox=\"0 0 160 160\"> <rect width=\"100%\" height=\"100%\" fill=\"#fde68a\" /> <g transform=\"translate(20, 20) scale(1.2000000000000002) rotate(0) translate(-12, -12)\" fill=\"#1d4ed8\" stroke=\"none\" stroke-width=\"0\" stroke-linecap=\"round\" stroke-linejoin=\"round\"> <path d=\"M12 2l3.09 6.26L22 9.27l-5 4.87 1.18 6.88L12 17.77l-6.18 3.25L7 14.14 2 9.27l6.91-1.01L12 2z\" /> </g> <g transform=\"translate(180, 20) scale(1.2000000000000002) rotate(0) translate(-12, -12)\" fill=\"#1d4ed8\" stroke=\"none\" stroke-width=\"0\" stroke-linecap=\"round\" stroke-linejoin=\"round\"> <path d=\"M12 2l3.09 6.26L22 9.27l-5 4.87 1.18 6.88L12 17.77l-6.18 3.25L7 14.14 2 9.27l6.91-1.01L12 2z\" /> </g> <g transform=\"translate(20, 180) scale(1.2000000000000002) rotate(0) translate(-12, -12)\" fill=\"#1d4ed8\" stroke=\"none\" stroke-width=\"0\" stroke-linecap=\"round\" stroke-linejoin=\"round\"> <path d=\"M12 2l3.09 6.26L22 9.27l-5 4.87 1.18 6.88L12 17.77l-6.18 3.25L7 14.14 2 9.27l6.91-1.01L12 2z\" /> </g> <g transform=\"translate(180, 180) scale(1.2000000000000002) rotate(0) translate(-12, -12)\" fill=\"#1d4ed8\" stroke=\"none\" stroke-width=\"0\" stroke-linecap=\"round\" stroke-linejoin=\"round\"> <path d=\"M12 2l3.09 6.26L22 9.27l-5 4.87 1.18 6.88L12 17.77l-6.18 3.25L7 14.14 2 9.27l6.91-1.01L12 2z\" /> </g> <g transform=\"translate(60, 20) scale(1.2000000000000002) rotate(0) translate(-12, -12)\" fill=\"#1d4ed8\" stroke=\"none\" stroke-width=\"0\" stroke-linecap=\"round\" stroke-linejoin=\"round\"> <path d=\"M12 2l3.09 6.26L22 9.27l-5 4.87 1.18 6.88L12 17.77l-6.18 3.25L7 14.14 2 9.27l6.91-1.01L12 2z\" /> </g> <g transform=\"translate(60, 180) scale(1.2000000000000002) rotate(0) translate(-12, -12)\" fill=\"#1d4ed8\" stroke=\"none\" stroke-width=\"0\" stroke-linecap=\"round\" stroke-linejoin=\"round\"> <path d=\"M12 2l3.09 6.26L22 9.27l-5 4.87 1.18 6.88L12 17.77l-6.18 3.25L7 14.14 2 9.27l6.91-1.01L12 2z\" /> </g> <g transform=\"translate(100, 20) scale(1.2000000000000002) rotate(0) translate(-12, -12)\" fill=\"#1d4ed8\" stroke=\"none\" stroke-width=\"0\" stroke-linecap=\"round\" stroke-linejoin=\"round\"> <path d=\"M12 2l3.09 6.26L22 9.27l-5 4.87 1.18 6.88L12 17.77l-6.18 3.25L7 14.14 2 9.27l6.91-1.01L12 2z\" /> </g> <g transform=\"translate(100, 180) scale(1.2000000000000002) rotate(0) translate(-12, -12)\" fill=\"#1d4ed8\" stroke=\"none\" stroke-width=\"0\" stroke-linecap=\"round\" stroke-linejoin=\"round\"> <path d=\"M12 2l3.09 6.26L22 9.27l-5 4.87 1.18 6.88L12 17.77l-6.18 3.25L7 14.14 2 9.27l6.91-1.01L12 2z\" /> </g> <g transform=\"translate(140, 20) scale(1.2000000000000002) rotate(0) translate(-12, -12)\" fill=\"#1d4ed8\" stroke=\"none\" stroke-width=\"0\" stroke-linecap=\"round\" stroke-linejoin=\"round\"> <path d=\"M12 2l3.09 6.26L22 9.27l-5 4.87 1.18 6.88L12 17.77l-6.18 3.25L7 14.14 2 9.27l6.91-1.01L12 2z\" /> </g> <g transform=\"translate(-20, 20) scale(1.2000000000000002) rotate(0) translate(-12, -12)\" fill=\"#1d4ed8\" stroke=\"none\" stroke-width=\"0\" stroke-linecap=\"round\" stroke-linejoin=\"round\"> <path d=\"M12 2l3.09 6.26L22 9.27l-5 4.87 1.18 6.88L12 17.77l-6.18 3.25L7 14.14 2 9.27l6.91-1.01L12 2z\" /> </g> <g transform=\"translate(140, 180) scale(1.2000000000000002) rotate(0) translate(-12, -12)\" fill=\"#1d4ed8\" stroke=\"none\" stroke-width=\"0\" stroke-linecap=\"round\" stroke-linejoin=\"round\"> <path d=\"M12 2l3.09 6.26L22 9.27l-5 4.87 1.18 6.88L12 17.77l-6.18 3.25L7 14.14 2 9.27l6.91-1.01L12 2z\" /> </g> <g transform=\"translate(-20, 180) scale(1.2000000000000002) rotate(0) translate(-12, -12)\" fill=\"#1d4ed8\" stroke=\"none\" stroke-width=\"0\" stroke-linecap=\"round\" stroke-linejoin=\"round\"> <path d=\"M12 2l3.09 6.26L22 9.27l-5 4.87 1.18 6.88L12 17.77l-6.18 3.25L7 14.14 2 9.27l6.91-1.01L12 2z\" /> </g> <g transform=\"translate(20, 60) scale(1.2000000000000002) rotate(0) translate(-12, -12)\" fill=\"#1d4ed8\" stroke=\"none\" stroke-width=\"0\" stroke-linecap=\"round\" stroke-linejoin=\"round\"> <path d=\"M12 2l3.09 6.26L22 9.27l-5 4.87 1.18 6.88L12 17.77l-6.18 3.25L7 14.14 2 9.27l6.91-1.01L12 2z\" /> </g> <g transform=\"translate(180, 60) scale(1.2000000000000002) rotate(0) translate(-12, -12)\" fill=\"#1d4ed8\" stroke=\"none\" stroke-width=\"0\" stroke-linecap=\"round\" stroke-linejoin=\"round\"> <path d=\"M12 2l3.09 6.26L22 9.27l-5 4.87 1.18 6.88L12 17.77l-6.18 3.25L7 14.14 2 9.27l6.91-1.01L12 2z\" /> </g> <g transform=\"translate(60, 60) scale(1.2000000000000002) rotate(0) translate(-12, -12)\" fill=\"#1d4ed8\" stroke=\"none\" stroke-width=\"0\" stroke-linecap=\"round\" stroke-linejoin=\"round\"> <path d=\"M12 2l3.09 6.26L22 9.27l-5 4.87 1.18 6.88L12 17.77l-6.18 3.25L7 14.14 2 9.27l6.91-1.01L12 2z\" /> </g> <g transform=\"translate(100, 60) scale(1.2000000000000002) rotate(0) translate(-12, -12)\" fill=\"#1d4ed8\" stroke=\"none\" stroke-width=\"0\" stroke-linecap=\"round\" stroke-linejoin=\"round\"> <path d=\"M12 2l3.09 6.26L22 9.27l-5 4.87 1.18 6.88L12 17.77l-6.18 3.25L7 14.14 2 9.27l6.91-1.01L12 2z\" /> </g> <g transform=\"translate(140, 60) scale(1.2000000000000002) rotate(0) translate(-12, -12)\" fill=\"#1d4ed8\" stroke=\"none\" stroke-width=\"0\" stroke-linecap=\"round\" stroke-linejoin=\"round\"> <path d=\"M12 2l3.09 6.26L22 9.27l-5 4.87 1.18 6.88L12 17.77l-6.18 3.25L7 14.14 2 9.27l6.91-1.01L12 2z\" /> </g> <g transform=\"translate(-20, 60) scale(1.2000000000000002) rotate(0) translate(-12, -12)\" fill=\"#1d4ed8\" stroke=\"none\" stroke-width=\"0\" stroke-linecap=\"round\" stroke-linejoin=\"round\"> <path d=\"M12 2l3.09 6.26L22 9.27l-5 4.87 1.18 6.88L12 17.77l-6.18 3.25L7 14.14 2 9.27l6.91-1.01L12 2z\" /> </g> <g transform=\"translate(20, 100) scale(1.2000000000000002) rotate(0) translate(-12, -12)\" fill=\"#1d4ed8\" stroke=\"none\" stroke-width=\"0\" stroke-linecap=\"round\" stroke-linejoin=\"round\"> <path d=\"M12 2l3.09 6.26L22 9.27l-5 4.87 1.18 6.88L12 17.77l-6.18 3.25L7 14.14 2 9.27l6.91-1.01L12 2z\" /> </g> <g transform=\"translate(180, 100) scale(1.2000000000000002) rotate(0) translate(-12, -12)\" fill=\"#1d4ed8\" stroke=\"none\" stroke-width=\"0\" stroke-linecap=\"round\" stroke-linejoin=\"round\"> <path d=\"M12 2l3.09 6.26L22 9.27l-5 4.87 1.18 6.88L12 17.77l-6.18 3.25L7 14.14 2 9.27l6.91-1.01L12 2z\" /> </g> <g transform=\"translate(60, 100) scale(1.2000000000000002) rotate(0) translate(-12, -12)\" fill=\"#1d4ed8\" stroke=\"none\" stroke-width=\"0\" stroke-linecap=\"round\" stroke-linejoin=\"round\"> <path d=\"M12 2l3.09 6.26L22 9.27l-5 4.87 1.18 6.88L12 17.77l-6.18 3.25L7 14.14 2 9.27l6.91-1.01L12 2z\" /> </g> <g transform=\"translate(100, 100) scale(1.2000000000000002) rotate(0) translate(-12, -12)\" fill=\"#1d4ed8\" stroke=\"none\" stroke-width=\"0\" stroke-linecap=\"round\" stroke-linejoin=\"round\"> <path d=\"M12 2l3.09 6.26L22 9.27l-5 4.87 1.18 6.88L12 17.77l-6.18 3.25L7 14.14 2 9.27l6.91-1.01L12 2z\" /> </g> <g transform=\"translate(140, 100) scale(1.2000000000000002) rotate(0) translate(-12, -12)\" fill=\"#1d4ed8\" stroke=\"none\" stroke-width=\"0\" stroke-linecap=\"round\" stroke-linejoin=\"round\"> <path d=\"M12 2l3.09 6.26L22 9.27l-5 4.87 1.18 6.88L12 17.77l-6.18 3.25L7 14.14 2 9.27l6.91-1.01L12 2z\" /> </g> <g transform=\"translate(-20, 100) scale(1.2000000000000002) rotate(0) translate(-12, -12)\" fill=\"#1d4ed8\" stroke=\"none\" stroke-width=\"0\" stroke-linecap=\"round\" stroke-linejoin=\"round\"> <path d=\"M12 2l3.09 6.26L22 9.27l-5 4.87 1.18 6.88L12 17.77l-6.18 3.25L7 14.14 2 9.27l6.91-1.01L12 2z\" /> </g> <g transform=\"translate(20, 140) scale(1.2000000000000002) rotate(0) translate(-12, -12)\" fill=\"#1d4ed8\" stroke=\"none\" stroke-width=\"0\" stroke-linecap=\"round\" stroke-linejoin=\"round\"> <path d=\"M12 2l3.09 6.26L22 9.27l-5 4.87 1.18 6.88L12 17.77l-6.18 3.25L7 14.14 2 9.27l6.91-1.01L12 2z\" /> </g> <g transform=\"translate(180, 140) scale(1.2000000000000002) rotate(0) translate(-12, -12)\" fill=\"#1d4ed8\" stroke=\"none\" stroke-width=\"0\" stroke-linecap=\"round\" stroke-linejoin=\"round\"> <path d=\"M12 2l3.09 6.26L22 9.27l-5 4.87 1.18 6.88L12 17.77l-6.18 3.25L7 14.14 2 9.27l6.91-1.01L12 2z\" /> </g> <g transform=\"translate(20, -20) scale(1.2000000000000002) rotate(0) translate(-12, -12)\" fill=\"#1d4ed8\" stroke=\"none\" stroke-width=\"0\" stroke-linecap=\"round\" stroke-linejoin=\"round\"> <path d=\"M12 2l3.09 6.26L22 9.27l-5 4.87 1.18 6.88L12 17.77l-6.18 3.25L7 14.14 2 9.27l6.91-1.01L12 2z\" /> </g> <g transform=\"translate(180, -20) scale(1.2000000000000002) rotate(0) translate(-12, -12)\" fill=\"#1d4ed8\" stroke=\"none\" stroke-width=\"0\" stroke-linecap=\"round\" stroke-linejoin=\"round\"> <path d=\"M12 2l3.09 6.26L22 9.27l-5 4.87 1.18 6.88L12 17.77l-6.18 3.25L7 14.14 2 9.27l6.91-1.01L12 2z\" /> </g> <g transform=\"translate(60, 140) scale(1.2000000000000002) rotate(0) translate(-12, -12)\" fill=\"#1d4ed8\" stroke=\"none\" stroke-width=\"0\" stroke-linecap=\"round\" stroke-linejoin=\"round\"> <path d=\"M12 2l3.09 6.26L22 9.27l-5 4.87 1.18 6.88L12 17.77l-6.18 3.25L7 14.14 2 9.27l6.91-1.01L12 2z\" /> </g> <g transform=\"translate(60, -20) scale(1.2000000000000002) rotate(0) translate(-12, -12)\" fill=\"#1d4ed8\" stroke=\"none\" stroke-width=\"0\" stroke-linecap=\"round\" stroke-linejoin=\"round\"> <path d=\"M12 2l3.09 6.26L22 9.27l-5 4.87 1.18 6.88L12 17.77l-6.18 3.25L7 14.14 2 9.27l6.91-1.01L12 2z\" /> </g> <g transform=\"translate(100, 140) scale(1.2000000000000002) rotate(0) translate(-12, -12)\" fill=\"#1d4ed8\" stroke=\"none\" stroke-width=\"0\" stroke-linecap=\"round\" stroke-linejoin=\"round\"> <path d=\"M12 2l3.09 6.26L22 9.27l-5 4.87 1.18 6.88L12 17.77l-6.18 3.25L7 14.14 2 9.27l6.91-1.01L12 2z\" /> </g> <g transform=\"translate(100, -20) scale(1.2000000000000002) rotate(0) translate(-12, -12)\" fill=\"#1d4ed8\" stroke=\"none\" stroke-width=\"0\" stroke-linecap=\"round\" stroke-linejoin=\"round\"> <path d=\"M12 2l3.09 6.26L22 9.27l-5 4.87 1.18 6.88L12 17.77l-6.18 3.25L7 14.14 2 9.27l6.91-1.01L12 2z\" /> </g> </svg>",
83
+ "es": "<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"160\" height=\"160\" viewBox=\"0 0 160 160\"> <rect width=\"100%\" height=\"100%\" fill=\"#fde68a\" /> <g transform=\"translate(20, 20) scale(1.2000000000000002) rotate(0) translate(-12, -12)\" fill=\"#1d4ed8\" stroke=\"none\" stroke-width=\"0\" stroke-linecap=\"round\" stroke-linejoin=\"round\"> <path d=\"M12 2l3.09 6.26L22 9.27l-5 4.87 1.18 6.88L12 17.77l-6.18 3.25L7 14.14 2 9.27l6.91-1.01L12 2z\" /> </g> <g transform=\"translate(180, 20) scale(1.2000000000000002) rotate(0) translate(-12, -12)\" fill=\"#1d4ed8\" stroke=\"none\" stroke-width=\"0\" stroke-linecap=\"round\" stroke-linejoin=\"round\"> <path d=\"M12 2l3.09 6.26L22 9.27l-5 4.87 1.18 6.88L12 17.77l-6.18 3.25L7 14.14 2 9.27l6.91-1.01L12 2z\" /> </g> <g transform=\"translate(20, 180) scale(1.2000000000000002) rotate(0) translate(-12, -12)\" fill=\"#1d4ed8\" stroke=\"none\" stroke-width=\"0\" stroke-linecap=\"round\" stroke-linejoin=\"round\"> <path d=\"M12 2l3.09 6.26L22 9.27l-5 4.87 1.18 6.88L12 17.77l-6.18 3.25L7 14.14 2 9.27l6.91-1.01L12 2z\" /> </g> <g transform=\"translate(180, 180) scale(1.2000000000000002) rotate(0) translate(-12, -12)\" fill=\"#1d4ed8\" stroke=\"none\" stroke-width=\"0\" stroke-linecap=\"round\" stroke-linejoin=\"round\"> <path d=\"M12 2l3.09 6.26L22 9.27l-5 4.87 1.18 6.88L12 17.77l-6.18 3.25L7 14.14 2 9.27l6.91-1.01L12 2z\" /> </g> <g transform=\"translate(60, 20) scale(1.2000000000000002) rotate(0) translate(-12, -12)\" fill=\"#1d4ed8\" stroke=\"none\" stroke-width=\"0\" stroke-linecap=\"round\" stroke-linejoin=\"round\"> <path d=\"M12 2l3.09 6.26L22 9.27l-5 4.87 1.18 6.88L12 17.77l-6.18 3.25L7 14.14 2 9.27l6.91-1.01L12 2z\" /> </g> <g transform=\"translate(60, 180) scale(1.2000000000000002) rotate(0) translate(-12, -12)\" fill=\"#1d4ed8\" stroke=\"none\" stroke-width=\"0\" stroke-linecap=\"round\" stroke-linejoin=\"round\"> <path d=\"M12 2l3.09 6.26L22 9.27l-5 4.87 1.18 6.88L12 17.77l-6.18 3.25L7 14.14 2 9.27l6.91-1.01L12 2z\" /> </g> <g transform=\"translate(100, 20) scale(1.2000000000000002) rotate(0) translate(-12, -12)\" fill=\"#1d4ed8\" stroke=\"none\" stroke-width=\"0\" stroke-linecap=\"round\" stroke-linejoin=\"round\"> <path d=\"M12 2l3.09 6.26L22 9.27l-5 4.87 1.18 6.88L12 17.77l-6.18 3.25L7 14.14 2 9.27l6.91-1.01L12 2z\" /> </g> <g transform=\"translate(100, 180) scale(1.2000000000000002) rotate(0) translate(-12, -12)\" fill=\"#1d4ed8\" stroke=\"none\" stroke-width=\"0\" stroke-linecap=\"round\" stroke-linejoin=\"round\"> <path d=\"M12 2l3.09 6.26L22 9.27l-5 4.87 1.18 6.88L12 17.77l-6.18 3.25L7 14.14 2 9.27l6.91-1.01L12 2z\" /> </g> <g transform=\"translate(140, 20) scale(1.2000000000000002) rotate(0) translate(-12, -12)\" fill=\"#1d4ed8\" stroke=\"none\" stroke-width=\"0\" stroke-linecap=\"round\" stroke-linejoin=\"round\"> <path d=\"M12 2l3.09 6.26L22 9.27l-5 4.87 1.18 6.88L12 17.77l-6.18 3.25L7 14.14 2 9.27l6.91-1.01L12 2z\" /> </g> <g transform=\"translate(-20, 20) scale(1.2000000000000002) rotate(0) translate(-12, -12)\" fill=\"#1d4ed8\" stroke=\"none\" stroke-width=\"0\" stroke-linecap=\"round\" stroke-linejoin=\"round\"> <path d=\"M12 2l3.09 6.26L22 9.27l-5 4.87 1.18 6.88L12 17.77l-6.18 3.25L7 14.14 2 9.27l6.91-1.01L12 2z\" /> </g> <g transform=\"translate(140, 180) scale(1.2000000000000002) rotate(0) translate(-12, -12)\" fill=\"#1d4ed8\" stroke=\"none\" stroke-width=\"0\" stroke-linecap=\"round\" stroke-linejoin=\"round\"> <path d=\"M12 2l3.09 6.26L22 9.27l-5 4.87 1.18 6.88L12 17.77l-6.18 3.25L7 14.14 2 9.27l6.91-1.01L12 2z\" /> </g> <g transform=\"translate(-20, 180) scale(1.2000000000000002) rotate(0) translate(-12, -12)\" fill=\"#1d4ed8\" stroke=\"none\" stroke-width=\"0\" stroke-linecap=\"round\" stroke-linejoin=\"round\"> <path d=\"M12 2l3.09 6.26L22 9.27l-5 4.87 1.18 6.88L12 17.77l-6.18 3.25L7 14.14 2 9.27l6.91-1.01L12 2z\" /> </g> <g transform=\"translate(20, 60) scale(1.2000000000000002) rotate(0) translate(-12, -12)\" fill=\"#1d4ed8\" stroke=\"none\" stroke-width=\"0\" stroke-linecap=\"round\" stroke-linejoin=\"round\"> <path d=\"M12 2l3.09 6.26L22 9.27l-5 4.87 1.18 6.88L12 17.77l-6.18 3.25L7 14.14 2 9.27l6.91-1.01L12 2z\" /> </g> <g transform=\"translate(180, 60) scale(1.2000000000000002) rotate(0) translate(-12, -12)\" fill=\"#1d4ed8\" stroke=\"none\" stroke-width=\"0\" stroke-linecap=\"round\" stroke-linejoin=\"round\"> <path d=\"M12 2l3.09 6.26L22 9.27l-5 4.87 1.18 6.88L12 17.77l-6.18 3.25L7 14.14 2 9.27l6.91-1.01L12 2z\" /> </g> <g transform=\"translate(60, 60) scale(1.2000000000000002) rotate(0) translate(-12, -12)\" fill=\"#1d4ed8\" stroke=\"none\" stroke-width=\"0\" stroke-linecap=\"round\" stroke-linejoin=\"round\"> <path d=\"M12 2l3.09 6.26L22 9.27l-5 4.87 1.18 6.88L12 17.77l-6.18 3.25L7 14.14 2 9.27l6.91-1.01L12 2z\" /> </g> <g transform=\"translate(100, 60) scale(1.2000000000000002) rotate(0) translate(-12, -12)\" fill=\"#1d4ed8\" stroke=\"none\" stroke-width=\"0\" stroke-linecap=\"round\" stroke-linejoin=\"round\"> <path d=\"M12 2l3.09 6.26L22 9.27l-5 4.87 1.18 6.88L12 17.77l-6.18 3.25L7 14.14 2 9.27l6.91-1.01L12 2z\" /> </g> <g transform=\"translate(140, 60) scale(1.2000000000000002) rotate(0) translate(-12, -12)\" fill=\"#1d4ed8\" stroke=\"none\" stroke-width=\"0\" stroke-linecap=\"round\" stroke-linejoin=\"round\"> <path d=\"M12 2l3.09 6.26L22 9.27l-5 4.87 1.18 6.88L12 17.77l-6.18 3.25L7 14.14 2 9.27l6.91-1.01L12 2z\" /> </g> <g transform=\"translate(-20, 60) scale(1.2000000000000002) rotate(0) translate(-12, -12)\" fill=\"#1d4ed8\" stroke=\"none\" stroke-width=\"0\" stroke-linecap=\"round\" stroke-linejoin=\"round\"> <path d=\"M12 2l3.09 6.26L22 9.27l-5 4.87 1.18 6.88L12 17.77l-6.18 3.25L7 14.14 2 9.27l6.91-1.01L12 2z\" /> </g> <g transform=\"translate(20, 100) scale(1.2000000000000002) rotate(0) translate(-12, -12)\" fill=\"#1d4ed8\" stroke=\"none\" stroke-width=\"0\" stroke-linecap=\"round\" stroke-linejoin=\"round\"> <path d=\"M12 2l3.09 6.26L22 9.27l-5 4.87 1.18 6.88L12 17.77l-6.18 3.25L7 14.14 2 9.27l6.91-1.01L12 2z\" /> </g> <g transform=\"translate(180, 100) scale(1.2000000000000002) rotate(0) translate(-12, -12)\" fill=\"#1d4ed8\" stroke=\"none\" stroke-width=\"0\" stroke-linecap=\"round\" stroke-linejoin=\"round\"> <path d=\"M12 2l3.09 6.26L22 9.27l-5 4.87 1.18 6.88L12 17.77l-6.18 3.25L7 14.14 2 9.27l6.91-1.01L12 2z\" /> </g> <g transform=\"translate(60, 100) scale(1.2000000000000002) rotate(0) translate(-12, -12)\" fill=\"#1d4ed8\" stroke=\"none\" stroke-width=\"0\" stroke-linecap=\"round\" stroke-linejoin=\"round\"> <path d=\"M12 2l3.09 6.26L22 9.27l-5 4.87 1.18 6.88L12 17.77l-6.18 3.25L7 14.14 2 9.27l6.91-1.01L12 2z\" /> </g> <g transform=\"translate(100, 100) scale(1.2000000000000002) rotate(0) translate(-12, -12)\" fill=\"#1d4ed8\" stroke=\"none\" stroke-width=\"0\" stroke-linecap=\"round\" stroke-linejoin=\"round\"> <path d=\"M12 2l3.09 6.26L22 9.27l-5 4.87 1.18 6.88L12 17.77l-6.18 3.25L7 14.14 2 9.27l6.91-1.01L12 2z\" /> </g> <g transform=\"translate(140, 100) scale(1.2000000000000002) rotate(0) translate(-12, -12)\" fill=\"#1d4ed8\" stroke=\"none\" stroke-width=\"0\" stroke-linecap=\"round\" stroke-linejoin=\"round\"> <path d=\"M12 2l3.09 6.26L22 9.27l-5 4.87 1.18 6.88L12 17.77l-6.18 3.25L7 14.14 2 9.27l6.91-1.01L12 2z\" /> </g> <g transform=\"translate(-20, 100) scale(1.2000000000000002) rotate(0) translate(-12, -12)\" fill=\"#1d4ed8\" stroke=\"none\" stroke-width=\"0\" stroke-linecap=\"round\" stroke-linejoin=\"round\"> <path d=\"M12 2l3.09 6.26L22 9.27l-5 4.87 1.18 6.88L12 17.77l-6.18 3.25L7 14.14 2 9.27l6.91-1.01L12 2z\" /> </g> <g transform=\"translate(20, 140) scale(1.2000000000000002) rotate(0) translate(-12, -12)\" fill=\"#1d4ed8\" stroke=\"none\" stroke-width=\"0\" stroke-linecap=\"round\" stroke-linejoin=\"round\"> <path d=\"M12 2l3.09 6.26L22 9.27l-5 4.87 1.18 6.88L12 17.77l-6.18 3.25L7 14.14 2 9.27l6.91-1.01L12 2z\" /> </g> <g transform=\"translate(180, 140) scale(1.2000000000000002) rotate(0) translate(-12, -12)\" fill=\"#1d4ed8\" stroke=\"none\" stroke-width=\"0\" stroke-linecap=\"round\" stroke-linejoin=\"round\"> <path d=\"M12 2l3.09 6.26L22 9.27l-5 4.87 1.18 6.88L12 17.77l-6.18 3.25L7 14.14 2 9.27l6.91-1.01L12 2z\" /> </g> <g transform=\"translate(20, -20) scale(1.2000000000000002) rotate(0) translate(-12, -12)\" fill=\"#1d4ed8\" stroke=\"none\" stroke-width=\"0\" stroke-linecap=\"round\" stroke-linejoin=\"round\"> <path d=\"M12 2l3.09 6.26L22 9.27l-5 4.87 1.18 6.88L12 17.77l-6.18 3.25L7 14.14 2 9.27l6.91-1.01L12 2z\" /> </g> <g transform=\"translate(180, -20) scale(1.2000000000000002) rotate(0) translate(-12, -12)\" fill=\"#1d4ed8\" stroke=\"none\" stroke-width=\"0\" stroke-linecap=\"round\" stroke-linejoin=\"round\"> <path d=\"M12 2l3.09 6.26L22 9.27l-5 4.87 1.18 6.88L12 17.77l-6.18 3.25L7 14.14 2 9.27l6.91-1.01L12 2z\" /> </g> <g transform=\"translate(60, 140) scale(1.2000000000000002) rotate(0) translate(-12, -12)\" fill=\"#1d4ed8\" stroke=\"none\" stroke-width=\"0\" stroke-linecap=\"round\" stroke-linejoin=\"round\"> <path d=\"M12 2l3.09 6.26L22 9.27l-5 4.87 1.18 6.88L12 17.77l-6.18 3.25L7 14.14 2 9.27l6.91-1.01L12 2z\" /> </g> <g transform=\"translate(60, -20) scale(1.2000000000000002) rotate(0) translate(-12, -12)\" fill=\"#1d4ed8\" stroke=\"none\" stroke-width=\"0\" stroke-linecap=\"round\" stroke-linejoin=\"round\"> <path d=\"M12 2l3.09 6.26L22 9.27l-5 4.87 1.18 6.88L12 17.77l-6.18 3.25L7 14.14 2 9.27l6.91-1.01L12 2z\" /> </g> <g transform=\"translate(100, 140) scale(1.2000000000000002) rotate(0) translate(-12, -12)\" fill=\"#1d4ed8\" stroke=\"none\" stroke-width=\"0\" stroke-linecap=\"round\" stroke-linejoin=\"round\"> <path d=\"M12 2l3.09 6.26L22 9.27l-5 4.87 1.18 6.88L12 17.77l-6.18 3.25L7 14.14 2 9.27l6.91-1.01L12 2z\" /> </g> <g transform=\"translate(100, -20) scale(1.2000000000000002) rotate(0) translate(-12, -12)\" fill=\"#1d4ed8\" stroke=\"none\" stroke-width=\"0\" stroke-linecap=\"round\" stroke-linejoin=\"round\"> <path d=\"M12 2l3.09 6.26L22 9.27l-5 4.87 1.18 6.88L12 17.77l-6.18 3.25L7 14.14 2 9.27l6.91-1.01L12 2z\" /> </g> </svg>"
84
+ },
85
+ "style": {
86
+ "width": "100.0cqw",
87
+ "height": "75.0cqw",
88
+ "backgroundType": "pattern",
89
+ "backgroundPatternType": "stars",
90
+ "backgroundPatternColor": "#1d4ed8",
91
+ "backgroundPatternBgColor": "#fde68a"
92
+ }
93
+ }
94
+ ]
95
+ }