{"slug": "parsing-adobe-illustrator-ai-files-in-the-browser-with-pdf-js-four-things-the-t", "title": "Parsing Adobe Illustrator .ai files in the browser with pdf.js — four things the docs don't tell you", "summary": "A developer building ArtboardLab, a browser-based .ai file viewer and converter, discovered that Adobe Illustrator .ai files are actually PDF containers when saved with PDF compatibility enabled, allowing pdf.js to parse them. The developer encountered four undocumented pitfalls: CJK text disappears without setting cMapUrl, transparent PNG exports render as solid black, and other silent failures. The project, ArtboardLab, is a free tool that processes files entirely in the browser without uploading.", "body_md": "An `.ai`\n\nfile is not really an Illustrator format. If it was saved with **Create PDF Compatible File** turned on (the default since Illustrator 9), the bytes on disk are a PDF container — every artboard is a page. That single fact is what makes a browser-side `.ai`\n\nviewer possible at all: you don't need to reverse-engineer a proprietary format, you point pdf.js at it and it opens.\n\nDetection is a byte-signature check on the head of the file, not the extension:\n\n``` js\nexport function detectAiKind(bytes: Uint8Array): AiFileKind {\n  const head = new TextDecoder('latin1').decode(bytes.subarray(0, 1024));\n  if (head.includes('%PDF-')) return 'pdf';       // modern, PDF-compatible\n  if (head.includes('%!PS-Adobe')) return 'postscript'; // Illustrator 8 and older\n  return 'unknown';\n}\n```\n\n`%PDF-`\n\nis searched for rather than matched at offset 0, because the PDF spec tolerates leading bytes before the header. Illustrator 8 and older wrote PostScript, and no browser will read those — that path gets an honest error message instead of a spinner.\n\nSo far, so easy. Everything after that was a series of failures that produced no error message at all. I built [ArtboardLab](https://artboardlab.com/tools/ai-to-svg/), a free browser-based tool set where nothing gets uploaded, and these four lessons all come from its `.ai`\n\nviewer/converter.\n\n`cMapUrl`\n\nand CJK text silently disappears\nThe first Korean-language `.ai`\n\nfile I tested rendered perfectly — except every single glyph of Korean text was gone. No exception, no console warning, no fallback box. Vector shapes fine, Latin text fine, Hangul just not there.\n\nCID-keyed fonts (Korean, Japanese, Chinese, and other multi-byte encodings) need Adobe's CMap tables to map character codes to glyph IDs. pdf.js ships those tables, but it does not bundle them into the library — it fetches them at runtime from wherever `cMapUrl`\n\npoints. If you don't set the option, there's nothing to fetch, and the failure mode is silence.\n\n``` js\nconst task = pdfjs.getDocument({\n  data: buffer,\n  // CJK (and other CID) fonts need Adobe CMap data — without cMapUrl,\n  // Korean/Japanese/Chinese text silently drops every glyph.\n  cMapUrl: '/pdf-assets/cmaps/',\n  cMapPacked: true,\n  // substitutes for non-embedded standard fonts\n  standardFontDataUrl: '/pdf-assets/standard_fonts/',\n  // JPX (JPEG 2000) image decoding\n  wasmUrl: '/pdf-assets/wasm/',\n});\n```\n\nTwo related options have the same shape of problem. `standardFontDataUrl`\n\nsupplies substitutes for the standard fonts a document references but doesn't embed, and `wasmUrl`\n\npoints at the WASM used for JPX (JPEG 2000) images, which show up in print-oriented artwork more often than you'd expect.\n\nAll three directories are copied into the site's static assets and served from the same origin — 198 files, mostly `.bcmap`\n\ntables, about 3.3 MB. Nothing goes to a CDN. Check that those assets actually shipped: a missing CMap directory doesn't break the page, it just quietly deletes other people's languages.\n\nExporting a PNG with a transparent background sounds like a one-liner. pdf.js's `render()`\n\ntakes a `background`\n\noption, so pass a transparent color and you're done.\n\nIt comes out solid black. `'rgba(0,0,0,0)'`\n\n, the string `'transparent'`\n\n, even a transparent `CanvasPattern`\n\n— all of them paint opaque black in pdf.js v6. I verified each variant before accepting the conclusion, because it feels too basic to be broken.\n\nThe fix is the classic compositing trick: render the page twice, once on white and once on black, and derive alpha from the difference between the two. A fully opaque pixel looks identical on both backgrounds; a fully transparent one differs by the full range; anything in between lands proportionally in the middle. The black composite is already premultiplied by that alpha, so dividing it back out recovers the color.\n\n``` js\nconst white = await renderToImageData(page, viewport, '#ffffff');\nconst black = await renderToImageData(page, viewport, '#000000');\nconst w = white.data;\nconst b = black.data;\n\nfor (let i = 0; i < w.length; i += 4) {\n  const diff = (w[i] - b[i] + (w[i+1] - b[i+1]) + (w[i+2] - b[i+2])) / 3;\n  const alpha = 255 - Math.max(0, Math.min(255, Math.round(diff)));\n  if (alpha === 0) {\n    b[i] = b[i+1] = b[i+2] = b[i+3] = 0;\n  } else {\n    b[i]   = Math.min(255, Math.round((b[i]   * 255) / alpha));\n    b[i+1] = Math.min(255, Math.round((b[i+1] * 255) / alpha));\n    b[i+2] = Math.min(255, Math.round((b[i+2] * 255) / alpha));\n    b[i+3] = alpha;\n  }\n}\n```\n\nThe cost is what it looks like: every transparent render is two renders. Thumbnails and the preview want real alpha too — so the UI checkerboard shows what's actually transparent — which means the doubling applies across the whole pipeline, not just export.\n\nTwo practical notes. Read pixels back with `getContext('2d', { willReadFrequently: true })`\n\n— this is a full-surface `getImageData`\n\non every render. And zero out `canvas.width`\n\n/`canvas.height`\n\nwhen done: at export scale these backing stores are enormous. There's also a hard cap of 8192 px on the long edge, with the scale reduced proportionally and a notice shown, rather than letting a huge artboard hit the canvas limit.\n\npdf.js used to have an SVG backend, `SVGGraphics`\n\n. It was removed after the 3.x line. If you want an `.ai`\n\n→ SVG path that keeps paths as vectors, you either write a PDF-operator-to-SVG converter yourself or you keep an old build around. I kept the old build: `pdfjs-legacy`\n\nis an npm alias for `pdfjs-dist@3.11.174`\n\n, living alongside v6 in the same `package.json`\n\n.\n\nThat's a real cost, so it's paid only on demand: the legacy module is imported with `await import(...)`\n\ninside the export handler, never at module scope — its worker file alone is about 1.1 MB minified. It runs as a fully separate instance: its own worker, its own `GlobalWorkerOptions`\n\n, its own document. Nothing is shared with the v6 pipeline.\n\nThree options are not optional on that build, and each fixes a different failure:\n\n``` js\nconst task = legacy.getDocument({\n  data: new Uint8Array(await file.arrayBuffer()),\n  // 3.11.174 predates the CVE-2024-4367 fix — font programs must never\n  // be able to reach eval().\n  isEvalSupported: false,\n  // without this, pdf.js drops the translated font data and SVGGraphics'\n  // embedFonts pass throws `addFontStyle: No font data available`\n  fontExtraProperties: true,\n  // with OffscreenCanvas the worker returns images as ImageBitmap and leaves\n  // imgData.data null; SVGGraphics predates bitmaps and reads .data\n  // unconditionally, so any raster image throws `null.subarray`.\n  isOffscreenCanvasSupported: false,\n  cMapUrl: '/pdf-assets/cmaps/',\n  cMapPacked: true,\n  standardFontDataUrl: '/pdf-assets/standard_fonts/',\n});\n```\n\n`isEvalSupported: false`\n\n`eval()`\n\n. Running a stranger's `.ai`\n\nfile through a known-vulnerable parser is exactly the scenario the CVE describes, so this one is non-negotiable. (v6 removed the option entirely; font programs can't get to `eval`\n\nthere at all.)`fontExtraProperties: true`\n\n`SVGGraphics.embedFonts`\n\nneeds that data to write `@font-face`\n\nrules, and without it you get `addFontStyle: No font data available`\n\n.`isOffscreenCanvasSupported: false`\n\n`ImageBitmap`\n\nwith `imgData.data`\n\nleft null. `SVGGraphics`\n\nwas written before bitmaps existed and reads `.data`\n\nunconditionally, so any placed raster image throws `null.subarray`\n\n.Two smaller things in the same file. `SVGGraphics`\n\nis constructed with `forceDataSchema = true`\n\nso fonts and images are inlined as base64 `data:`\n\nURIs — the default `blob:`\n\nURLs die with the page session, which would leave the downloaded `.svg`\n\nfull of unresolvable references the moment the user closes the tab. And the 3.x TypeScript types claim `getSVG()`\n\nresolves to `void`\n\n; it actually resolves to the SVG root element, which needs a cast.\n\nOne non-obvious detail: the SVG path re-reads the original `File`\n\nfrom disk instead of reusing the buffer. The `ArrayBuffer`\n\nhanded to the v6 worker at open time was transferred, so it's detached. Same for the PDF export path.\n\n`PDFPageProxy`\n\nproduce black output\nOpening a file kicks off three things at once: render the preview, build thumbnails for every artboard, and scan the document for warnings. Run them concurrently and output starts coming back all black — not an exception, just wrong pixels, and not reliably reproducible.\n\n`render()`\n\nand `getOperatorList()`\n\non the *same* `PDFPageProxy`\n\ndon't tolerate overlapping. And note that trap 2 doubled the number of renders per page, which makes the overlap far easier to hit.\n\nThe fix is unglamorous: one promise chain that every piece of page work goes through.\n\n``` js\nlet pageWork: Promise<void> = Promise.resolve();\n\nfunction enqueue(work: () => Promise<void>): Promise<void> {\n  pageWork = pageWork.then(work).catch(() => {});\n  return pageWork;\n}\n```\n\nTwo consequences worth stating, since both bit me later:\n\nThe `.catch(() => {})`\n\nis deliberate — one broken page must not poison the chain for everything queued behind it. But that means a caller can't learn about failure from the returned promise, so anything that needs to report an error records it inside its own task and reads it afterwards.\n\nAnd the SVG export deliberately does *not* go through this chain. It opens its own document on its own worker, so it shares no `PDFPageProxy`\n\nwith the v6 pipeline — and routing it through `enqueue`\n\nwould swallow exactly the errors the UI needs to show.\n\nThere's also an `openToken`\n\ncounter, bumped on every open/reset, checked before each state write. Async work belonging to a file the user already replaced bails out instead of writing stale thumbnails into the new document's UI.\n\nSome of this is solved. Some is as good as it gets in a browser, and the UI says so rather than pretending:\n\n`commonObjs._objs`\n\n), wrapped in a try/catch — if the internal shape changes, the warning is skipped silently rather than breaking the page.The general lesson, if there is one: pdf.js's hardest bugs here didn't throw. Missing glyphs, black canvases, and silently corrupted renders all look like a working app until someone opens a file in a language you don't read.", "url": "https://wpnews.pro/news/parsing-adobe-illustrator-ai-files-in-the-browser-with-pdf-js-four-things-the-t", "canonical_source": "https://dev.to/kyungju_leebenjie_519b/parsing-adobe-illustrator-ai-files-in-the-browser-with-pdfjs-four-things-the-docs-dont-tell-you-17nk", "published_at": "2026-08-21 04:42:22+00:00", "updated_at": "2026-08-21 05:14:06.654407+00:00", "lang": "en", "topics": ["developer-tools"], "entities": ["ArtboardLab", "pdf.js", "Adobe Illustrator", "Adobe"], "alternates": {"html": "https://wpnews.pro/news/parsing-adobe-illustrator-ai-files-in-the-browser-with-pdf-js-four-things-the-t", "markdown": "https://wpnews.pro/news/parsing-adobe-illustrator-ai-files-in-the-browser-with-pdf-js-four-things-the-t.md", "text": "https://wpnews.pro/news/parsing-adobe-illustrator-ai-files-in-the-browser-with-pdf-js-four-things-the-t.txt", "jsonld": "https://wpnews.pro/news/parsing-adobe-illustrator-ai-files-in-the-browser-with-pdf-js-four-things-the-t.jsonld"}}