Repository navigation
feat(images)!: refuse HEIC/HEIF input and drop the LGPL libheif dependency - #56
Merged
Merged
Conversation
…dency heic-convert pulled in libheif-js (LGPL-3.0, bundling the libde265 HEVC decoder), the project's only copyleft dependency. It obliged us to ship LGPL-3.0.txt and relinking rights with every binary, and bundling an HEVC decoder carries patent-pool exposure. No permissively licensed HEVC decoder exists, so swapping libraries wouldn't help; HEIC demand is low, so we drop the transcode instead. HEIC is still DETECTED (extension, image/heic|heif MIME, or an ftyp HEIF brand in the header, so a mislabeled .jpg is caught): Word can't display it, so embedding it would ship a broken image. Every image ingress (images add from a path, data: URI, or http(s) URL; markdown images in insert/edit/create; images replace) now fails IMAGE_SOURCE (exit 1, nothing written) with a hint giving the conversion command: sips on macOS, heif-convert or ImageMagick on Linux, else export as JPEG/PNG. ImageSourceError gained an optional hint that the insert, markdown-import, and images-replace error paths now forward. - Remove heic-convert + @types/heic-convert; libheif-js and heic-decode leave the lockfile. Compiled binary shrinks ~1.5 MiB, dist/index.js ~1.5 MiB. - Delete LGPL-3.0.txt; drop the libheif section and LGPL language from NOTICES, the npm files list, the release staging step, and the docs. BREAKING CHANGE: inserting or replacing with a HEIC/HEIF image is now refused instead of transcoded to JPEG. Convert the file first. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Inline --markdown (and the positional content edit/insert fold into it) decodes a typed \n / \t / \r into real whitespace. A link or image destination can't contain whitespace, so decoding one there only ever broke it. Windows paths are full of these sequences: in `` the `\t` became a TAB, GFM stopped seeing an image, and insert/edit wrote the syntax as literal text at exit 0. The new HEIC refusal test caught it on the Windows CI runner, whose fixture path runs through D:\a\docx-cli\docx-cli\tests\... decodeInlineMarkdownEscapes leaves every `](dest` destination verbatim and decodes the rest (label, title, surrounding prose) as before. insert, edit, and footnotes/endnotes add+edit use it for inline markdown; --text and the find/replace positionals keep decodeInlineEscapes. Regression tests use a literal backslash path, so they fail without the fix on every OS, plus a native nested-path embed that is the real Windows case. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Why
heic-convertpulled inlibheif-js(LGPL-3.0), docx-cli's only copyleft dependency. libheif-js also bundles the libde265 HEVC decoder. Keeping it meant:LGPL-3.0.txt, an acknowledgement, and the right to relink with a modified libheif.Swapping libraries doesn't help, because no permissively licensed HEVC decoder exists. HEIC demand looks low, so this PR drops the transcode, and docx-cli's own dependency tree is now entirely permissive.
Behavior change (breaking for anyone inserting HEIC)
HEIC is still detected: by extension, by
image/heic/image/heifMIME, or by anftypHEIF brand in the header. The header check means a.jpgthat's really HEIC is still caught. Word can't display HEIC, so embedding one would ship a broken image. Detected HEIC used to be transcoded to JPEG. Now it's refused withIMAGE_SOURCE(exit 1, nothing written) and a hint that gives the conversion command:IMAGE_SOURCEis the code the existing unsupported-format refusals insource.tsalready use. Every ingress goes through the same refusal, and each one is tested:images addfrom a path, adata:image/heicURI, or HEIC bytes behind a.jpglabelininsertincreate --fromimages replace --with, with both a.heicfile and a mislabeled.jpgImageSourceErrorgained an optionalhint, which the insert, markdown-import and images-replace error paths now pass along.images replacevets its--withextension before loading the file, so it calls the sameheicExtensionRefusalinstead of its generic "Unsupported replacement image type" usage error. I ran the hint'ssipscommand on the test asset and inserted the result: exit 0, 96×64 JPEG.License cleanup
heic-convertand@types/heic-convert.libheif-js,heic-decodeand heic-convert's privatepngjs@6are gone frombun.lock.LGPL-3.0.txt. Removed the libheif section and the LGPL language fromNOTICES; every other attribution is unchanged.fileslist, the release.yml staging step and its comment, README, SECURITY.md, REFERENCE.md, CONTRIBUTING.md (Stack section and the project tree),src/core/image/CLAUDE.md,src/core/render/CLAUDE.md, and theimages add --helptext.License audit
I walked the full production dependency closure from
package.jsondependencies(102 packages) and read eachpackage.jsonlicense field:diff,highlight.js,jpeg-js), 1 ISC (inherits)pako:MIT AND Zlibjszip:MIT OR GPL-3.0-or-later, and NOTICES already elects MITNo GPL, LGPL, AGPL or MPL remains.
libxml2-wasm(MIT, libxml2 is MIT) and@hyzyla/pdfium(MIT wrapper; PDFium under BSD-3 or Apache-2.0) are fine.stringson the new binary finds no libheif or libde265 code; the only match is the hint text.Two gaps this audit turned up (the Bun runtime's LGPL parts, and missing NOTICES attributions) are now fixed in later commits; see "Added: attribution and
createatomicity" below.Size
dist/docx(darwin-arm64)dist/index.jsOut of scope
sips,heif-convert, ImageMagick). That would bend the invariant that onlydocx renderneeds an external application. It's a possible follow-up, and the hint tells users how to run the conversion themselves.readimage reference () back in now fails the hash-reuse path. The old transcode only supplied the dimensions there. Such docs should be vanishingly rare, since Word can't display them.Also fixed: Windows paths in inline markdown images (second commit)
The new HEIC test for
insertfailed on the Windows runner with exit 0. The cause is a real bug that predates this PR. Inline--markdown(and the positional contentedit/insertfold into it) decodes a typed\n/\t/\rinto real whitespace, and that decode also ran inside link and image destinations. A destination can't contain whitespace, so on Windowslost its image when\tbecame a TAB.insert/editthen wrote the syntax as literal text and exited 0. That happened for every image format, not just HEIC.create --frompassed because it reads markdown from a file, which is never decoded.The new
decodeInlineMarkdownEscapesleaves each](destdestination verbatim and decodes everything else as before.insert,editandfootnotes/endnotes add+editnow use it for inline markdown.--textand the find/replace positionals keepdecodeInlineEscapes. The regression tests use a literal backslash path, so they fail without the fix on every OS, not only Windows. A nested-directory native-path embed covers the real Windows case. The root CLAUDE.md invariant records the carve-out.Still out of scope:
--textand the find/replace positionals still decode a typed\tinside a Windows path in plain prose (--text "saved to C:\temp"). That's the documented trade-off of the inline decode, and--text-fileis the verbatim channel.Added: attribution and
createatomicity (commits 3–5)docs(notices): attribute every bundled package and guard against driftTHIRD-PARTY-LICENSES(new, generated bybun run licenses→scripts/third-party-licenses.ts). It walks the production dependency closure from the installednode_modulesand reproduces each package's license text. That's 102 packages, sorted, with no versions so a patch bump doesn't churn the file. The generator throws on any license outside a permissive allowlist (MIT, ISC, BSD-2/3, Apache-2.0, 0BSD,MIT AND Zlib,MIT OR GPL-3.0-or-later). It also throws on a required dependency it can't resolve, on a package with no license text it can find, and on two installed copies of one package with different texts. Three packages ship no LICENSE file:remark-mathand@nodable/entitiesuse their README's MIT grant, andisarraygets the standard MIT terms under itspackage.jsonauthor, labeled as such.NOTICES:diff(jsdiff) with its BSD-3-Clause text verbatim.libxml2-wasm(MIT), plus the libxml2 C library it compiles in, which is version 2.15.1 from the fork commit its submodule pins. That section reproduces libxml2'sCopyrightfile and the separate notices indict.c,list.candtimsort.h.THIRD-PARTY-LICENSESfor every other MIT/ISC package.tests/cli/installer.test.ts, offline). The first requires the committed file to equal a fresh generation. The second requires NOTICES to have a line-start entry for every production package that isn't plain MIT/ISC. The third requires the npmfileslist and the release staging step to both carryLICENSE,NOTICESandTHIRD-PARTY-LICENSES. I checked that they fail on a stale file or a missing entry.docs(notices): state that the binaries embed the Bun runtime and its LGPL partsbun build --compilecopies the Bun runtime into each standalone binary.strings dist/docxshows both.LGPL-2.0.txt(WebKit'sSource/JavaScriptCore/COPYING.LIB) andLGPL-2.1.txt(tinycc'sCOPYING) ship verbatim with every release binary via the release staging step. They're binaries only: the npm package (dist/index.js) embeds no Bun.fix(create): build the document in memory and write it oncecreateused to save the blank package and then reopen it for--from/--text-file, page geometry and headers/footers. So any failure (an unreadable source, a refused image, a bad footnote) left a blank .docx behind, and under--forceit had already overwritten the old file.Document(Document.fromPackageover the unsavedPkg), andsave()runs once at the end through Pkg's temp-file-and-rename. A failure writes nothing, and a--forcetarget stays byte-identical.--dry-runruns the same build and skips only the save, so it fails where the real run would. Its ack now reportsblocks.--from/--text-file→ no file--force→ bytes unchanged--dry-run, both failing and succeeding → no filecreate --fromtest now asserts that no file exists.Follow-ups, not changed here:
setup-buninstalls the latest Bun, so the embedded runtime version isn't pinned or recorded. Pinning it would make NOTICES' source pointer exact.src/core/raw/schemascarry no license headers.Checks
bun run check(biome + knip + tsc) passes, and the pre-commit full suite gives 2137 pass / 0 fail.bun run build:binarysucceeds (98,738,562 B), and a smoke test of--version,createandreadon the binary passes. No version bump, tag or release.🤖 Generated with Claude Code