Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 23 additions & 0 deletions src/export/packer/next-compiler.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -253,5 +253,28 @@ describe("Compiler", () => {

compiler.compile(file);
});

it("should write embedded fonts to sequential filenames in the zip (no spaces or special chars from family name)", () => {
// Regression for https://github.com/dolanmiu/docx/issues/3019 —
// fonts whose user-facing family name contains spaces / non-ASCII
// used to be written into the package zip with the family name as
// the filename (e.g. `EB Garamond.odttf`). Word rejects those
// paths and shows a "found unreadable content" recovery prompt on
// open. Sequential names side-step that.
const file = new File({
sections: [],
fonts: [
{ name: "EB Garamond", data: Buffer.from("") },
{ name: "Source Serif 4", data: Buffer.from("") },
],
});

const zip = compiler.compile(file);
const fileNames = Object.keys(zip.files);
expect(fileNames).to.include("word/fonts/font1.odttf");
expect(fileNames).to.include("word/fonts/font2.odttf");
expect(fileNames).to.not.include("word/fonts/EB Garamond.odttf");
expect(fileNames).to.not.include("word/fonts/Source Serif 4.odttf");
});
});
});
9 changes: 6 additions & 3 deletions src/export/packer/next-compiler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -155,9 +155,12 @@ export class Compiler {
}
}

for (const { data: buffer, name, fontKey } of file.FontTable.fontOptionsWithKey) {
const [nameWithoutExtension] = name.split(".");
zip.file(`word/fonts/${nameWithoutExtension}.odttf`, obfuscate(buffer, fontKey));
// Sequential filenames (font1.odttf, font2.odttf, …) — must match the
// Target paths set in FontWrapper. Word rejects embedded-font paths
// containing spaces or non-ASCII when those characters appear in the
// package zip entry; see https://github.com/dolanmiu/docx/issues/3019.
for (const [i, { data: buffer, fontKey }] of file.FontTable.fontOptionsWithKey.entries()) {
zip.file(`word/fonts/font${i + 1}.odttf`, obfuscate(buffer, fontKey));
}

return zip;
Expand Down
27 changes: 27 additions & 0 deletions src/file/fonts/font-wrapper.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import { describe, expect, it } from "vitest";

import { Formatter } from "@export/formatter";

import { FontWrapper } from "./font-wrapper";

describe("FontWrapper", () => {
it("emits sequential `fonts/font<N>.odttf` relationship Targets for each embedded font", () => {
// Regression for https://github.com/dolanmiu/docx/issues/3019 —
// relationship Targets used to embed the user-facing family name
// (e.g. `fonts/EB Garamond.odttf`), which Word rejected when the
// name contained spaces or non-ASCII chars. Sequential filenames
// decouple the package path from the family name.
const wrapper = new FontWrapper([
{ name: "EB Garamond", data: Buffer.from("") },
{ name: "Source Serif 4", data: Buffer.from("") },
{ name: "Crimson Pro", data: Buffer.from("") },
]);

const tree = new Formatter().format(wrapper.Relationships);
const targets = JSON.stringify(tree).match(/fonts\/font\d+\.odttf/g) ?? [];

expect(targets).to.deep.equal(["fonts/font1.odttf", "fonts/font2.odttf", "fonts/font3.odttf"]);
expect(JSON.stringify(tree)).to.not.include("EB Garamond.odttf");
expect(JSON.stringify(tree)).to.not.include("Source Serif 4.odttf");
});
});
7 changes: 6 additions & 1 deletion src/file/fonts/font-wrapper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,10 +43,15 @@ export class FontWrapper implements IViewWrapper {
this.relationships = new Relationships();

for (let i = 0; i < options.length; i++) {
// Use sequential filenames (`font1.odttf`, `font2.odttf` …) rather
// than the user-facing family name. Word treats the embedded-font
// path as a literal filename and rejects spaces / non-ASCII in
// the docx package — see https://github.com/dolanmiu/docx/issues/3019.
// The user-facing family name lives only in <w:font name="..."/>.
this.relationships.addRelationship(
i + 1,
"http://schemas.openxmlformats.org/officeDocument/2006/relationships/font",
`fonts/${options[i].name}.odttf`,
`fonts/font${i + 1}.odttf`,
);
}
}
Expand Down
Loading