Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feat/embedded fonts #2174

Merged
merged 11 commits into from
Dec 30, 2023
Merged
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
1 change: 1 addition & 0 deletions .cspell.json
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
"iife",
"Initializable",
"iroha",
"JOHAB",
"jsonify",
"jszip",
"NUMPAGES",
Expand Down
40 changes: 40 additions & 0 deletions demo/91-custom-fonts.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
// Simple example to add text to a document

import * as fs from "fs";
import { CharacterSet, Document, Packer, Paragraph, Tab, TextRun } from "docx";

const font = fs.readFileSync("./demo/assets/Pacifico.ttf");

const doc = new Document({
sections: [
{
properties: {},
children: [
new Paragraph({
run: {
font: "Pacifico",
},
children: [
new TextRun("Hello World"),
new TextRun({
text: "Foo Bar",
bold: true,
size: 40,
font: "Pacifico",
}),
new TextRun({
children: [new Tab(), "Github is the best"],
bold: true,
font: "Pacifico",
}),
],
}),
],
},
],
fonts: [{ name: "Pacifico", data: font, characterSet: CharacterSet.ANSI }],
});

Packer.toBuffer(doc).then((buffer) => {
fs.writeFileSync("My Document.docx", buffer);
});
44 changes: 44 additions & 0 deletions demo/92-declarative-custom-fonts.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
// Simple example to add text to a document

import * as fs from "fs";
import { Document, Packer, Paragraph, Tab, TextRun } from "docx";

const font = fs.readFileSync("./demo/assets/Pacifico.ttf");

const doc = new Document({
styles: {
default: {
document: {
run: {
font: "Pacifico",
},
},
},
},
sections: [
{
properties: {},
children: [
new Paragraph({
children: [
new TextRun("Hello World"),
new TextRun({
text: "Foo Bar",
bold: true,
size: 40,
}),
new TextRun({
children: [new Tab(), "Github is the best"],
bold: true,
}),
],
}),
],
},
],
fonts: [{ name: "Pacifico", data: font, characterSet: "00" }],
});

Packer.toBuffer(doc).then((buffer) => {
fs.writeFileSync("My Document.docx", buffer);
});
Binary file added demo/assets/Pacifico.ttf
Binary file not shown.
10 changes: 10 additions & 0 deletions demo/tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
{
"extends": "../tsconfig.json",
"compilerOptions": {
"rootDir": "./",
"paths": {
"docx": ["../build"]
}
},
"include": ["../demo"]
}
19 changes: 14 additions & 5 deletions src/export/packer/next-compiler.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ describe("Compiler", () => {
const fileNames = Object.keys(zipFile.files).map((f) => zipFile.files[f].name);

expect(fileNames).is.an.instanceof(Array);
expect(fileNames).has.length(17);
expect(fileNames).has.length(19);
expect(fileNames).to.include("word/document.xml");
expect(fileNames).to.include("word/styles.xml");
expect(fileNames).to.include("docProps/core.xml");
Expand All @@ -47,7 +47,9 @@ describe("Compiler", () => {
expect(fileNames).to.include("word/_rels/footnotes.xml.rels");
expect(fileNames).to.include("word/settings.xml");
expect(fileNames).to.include("word/comments.xml");
expect(fileNames).to.include("word/fontTable.xml");
expect(fileNames).to.include("word/_rels/document.xml.rels");
expect(fileNames).to.include("word/_rels/fontTable.xml.rels");
expect(fileNames).to.include("[Content_Types].xml");
expect(fileNames).to.include("_rels/.rels");
},
Expand Down Expand Up @@ -94,7 +96,7 @@ describe("Compiler", () => {
const fileNames = Object.keys(zipFile.files).map((f) => zipFile.files[f].name);

expect(fileNames).is.an.instanceof(Array);
expect(fileNames).has.length(25);
expect(fileNames).has.length(27);

expect(fileNames).to.include("word/header1.xml");
expect(fileNames).to.include("word/_rels/header1.xml.rels");
Expand Down Expand Up @@ -127,12 +129,10 @@ describe("Compiler", () => {
const spy = vi.spyOn(compiler["formatter"], "format");

compiler.compile(file);
expect(spy).toBeCalledTimes(13);
expect(spy).toBeCalledTimes(15);
});

it("should work with media datas", () => {
// This test is required because before, there was a case where Document was formatted twice, which was inefficient
// This also caused issues such as running prepForXml multiple times as format() was ran multiple times.
const file = new File({
sections: [
{
Expand Down Expand Up @@ -182,5 +182,14 @@ describe("Compiler", () => {

compiler.compile(file);
});

it("should work with fonts", () => {
const file = new File({
sections: [],
fonts: [{ name: "Pacifico", data: Buffer.from("") }],
});

compiler.compile(file);
});
});
});
42 changes: 42 additions & 0 deletions src/export/packer/next-compiler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import JSZip from "jszip";
import xml from "xml";

import { File } from "@file/file";
import { obfuscate } from "@file/fonts/obfuscate-ttf-to-odttf";

import { Formatter } from "../formatter";
import { ImageReplacer } from "./image-replacer";
Expand Down Expand Up @@ -31,6 +32,8 @@ interface IXmlifyedFileMapping {
readonly FootNotesRelationships: IXmlifyedFile;
readonly Settings: IXmlifyedFile;
readonly Comments?: IXmlifyedFile;
readonly FontTable?: IXmlifyedFile;
readonly FontTableRelationships?: IXmlifyedFile;
}

export class Compiler {
Expand Down Expand Up @@ -63,6 +66,11 @@ export class Compiler {
zip.file(`word/media/${fileName}`, stream);
}

for (const { data: buffer, name, fontKey } of file.FontTable.fontOptionsWithKey) {
const [nameWithoutExtension] = name.split(".");
zip.file(`word/fonts/${nameWithoutExtension}.odttf`, obfuscate(buffer, fontKey));
}

return zip;
}

Expand Down Expand Up @@ -439,6 +447,40 @@ export class Compiler {
),
path: "word/comments.xml",
},
FontTable: {
data: xml(
this.formatter.format(file.FontTable.View, {
viewWrapper: file.Document,
file,
stack: [],
}),
{
indent: prettify,
declaration: {
standalone: "yes",
encoding: "UTF-8",
},
},
),
path: "word/fontTable.xml",
},
FontTableRelationships: {
data: (() =>
xml(
this.formatter.format(file.FontTable.Relationships, {
viewWrapper: file.Document,
file,
stack: [],
}),
{
indent: prettify,
declaration: {
encoding: "UTF-8",
},
},
))(),
path: "word/_rels/fontTable.xml.rels",
},
};
}
}
31 changes: 20 additions & 11 deletions src/file/content-types/content-types.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,63 +29,72 @@ describe("ContentTypes", () => {
Default: { _attr: { ContentType: "application/vnd.openxmlformats-package.relationships+xml", Extension: "rels" } },
});
expect(tree["Types"][7]).to.deep.equal({ Default: { _attr: { ContentType: "application/xml", Extension: "xml" } } });

expect(tree["Types"][8]).to.deep.equal({
Default: {
_attr: {
ContentType: "application/vnd.openxmlformats-officedocument.obfuscatedFont",
Extension: "odttf",
},
},
});
expect(tree["Types"][9]).to.deep.equal({
Override: {
_attr: {
ContentType: "application/vnd.openxmlformats-officedocument.wordprocessingml.document.main+xml",
PartName: "/word/document.xml",
},
},
});
expect(tree["Types"][9]).to.deep.equal({
expect(tree["Types"][10]).to.deep.equal({
Override: {
_attr: {
ContentType: "application/vnd.openxmlformats-officedocument.wordprocessingml.styles+xml",
PartName: "/word/styles.xml",
},
},
});
expect(tree["Types"][10]).to.deep.equal({
expect(tree["Types"][11]).to.deep.equal({
Override: {
_attr: {
ContentType: "application/vnd.openxmlformats-package.core-properties+xml",
PartName: "/docProps/core.xml",
},
},
});
expect(tree["Types"][11]).to.deep.equal({
expect(tree["Types"][12]).to.deep.equal({
Override: {
_attr: {
ContentType: "application/vnd.openxmlformats-officedocument.custom-properties+xml",
PartName: "/docProps/custom.xml",
},
},
});
expect(tree["Types"][12]).to.deep.equal({
expect(tree["Types"][13]).to.deep.equal({
Override: {
_attr: {
ContentType: "application/vnd.openxmlformats-officedocument.extended-properties+xml",
PartName: "/docProps/app.xml",
},
},
});
expect(tree["Types"][13]).to.deep.equal({
expect(tree["Types"][14]).to.deep.equal({
Override: {
_attr: {
ContentType: "application/vnd.openxmlformats-officedocument.wordprocessingml.numbering+xml",
PartName: "/word/numbering.xml",
},
},
});
expect(tree["Types"][14]).to.deep.equal({
expect(tree["Types"][15]).to.deep.equal({
Override: {
_attr: {
ContentType: "application/vnd.openxmlformats-officedocument.wordprocessingml.footnotes+xml",
PartName: "/word/footnotes.xml",
},
},
});
expect(tree["Types"][15]).to.deep.equal({
expect(tree["Types"][16]).to.deep.equal({
Override: {
_attr: {
ContentType: "application/vnd.openxmlformats-officedocument.wordprocessingml.settings+xml",
Expand All @@ -102,7 +111,7 @@ describe("ContentTypes", () => {
contentTypes.addFooter(102);
const tree = new Formatter().format(contentTypes);

expect(tree["Types"][17]).to.deep.equal({
expect(tree["Types"][19]).to.deep.equal({
Override: {
_attr: {
ContentType: "application/vnd.openxmlformats-officedocument.wordprocessingml.footer+xml",
Expand All @@ -111,7 +120,7 @@ describe("ContentTypes", () => {
},
});

expect(tree["Types"][18]).to.deep.equal({
expect(tree["Types"][20]).to.deep.equal({
Override: {
_attr: {
ContentType: "application/vnd.openxmlformats-officedocument.wordprocessingml.footer+xml",
Expand All @@ -128,7 +137,7 @@ describe("ContentTypes", () => {
contentTypes.addHeader(202);
const tree = new Formatter().format(contentTypes);

expect(tree["Types"][17]).to.deep.equal({
expect(tree["Types"][19]).to.deep.equal({
Override: {
_attr: {
ContentType: "application/vnd.openxmlformats-officedocument.wordprocessingml.header+xml",
Expand All @@ -137,7 +146,7 @@ describe("ContentTypes", () => {
},
});

expect(tree["Types"][18]).to.deep.equal({
expect(tree["Types"][20]).to.deep.equal({
Override: {
_attr: {
ContentType: "application/vnd.openxmlformats-officedocument.wordprocessingml.header+xml",
Expand Down
2 changes: 2 additions & 0 deletions src/file/content-types/content-types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ export class ContentTypes extends XmlComponent {
this.root.push(new Default("image/gif", "gif"));
this.root.push(new Default("application/vnd.openxmlformats-package.relationships+xml", "rels"));
this.root.push(new Default("application/xml", "xml"));
this.root.push(new Default("application/vnd.openxmlformats-officedocument.obfuscatedFont", "odttf"));

this.root.push(
new Override("application/vnd.openxmlformats-officedocument.wordprocessingml.document.main+xml", "/word/document.xml"),
Expand All @@ -33,6 +34,7 @@ export class ContentTypes extends XmlComponent {
this.root.push(new Override("application/vnd.openxmlformats-officedocument.wordprocessingml.footnotes+xml", "/word/footnotes.xml"));
this.root.push(new Override("application/vnd.openxmlformats-officedocument.wordprocessingml.settings+xml", "/word/settings.xml"));
this.root.push(new Override("application/vnd.openxmlformats-officedocument.wordprocessingml.comments+xml", "/word/comments.xml"));
this.root.push(new Override("application/vnd.openxmlformats-officedocument.wordprocessingml.fontTable+xml", "/word/fontTable.xml"));
}

public addFooter(index: number): void {
Expand Down
2 changes: 2 additions & 0 deletions src/file/core-properties/properties.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { ICommentsOptions } from "@file/paragraph/run/comment-run";
import { ICompatibilityOptions } from "@file/settings/compatibility";
import { FontOptions } from "@file/fonts/font-table";
import { StringContainer, XmlComponent } from "@file/xml-components";
import { dateTimeValue } from "@util/values";

Expand Down Expand Up @@ -40,6 +41,7 @@ export interface IPropertiesOptions {
readonly customProperties?: readonly ICustomPropertyOptions[];
readonly evenAndOddHeaderAndFooters?: boolean;
readonly defaultTabStop?: number;
readonly fonts?: readonly FontOptions[];
}

// <xs:element name="coreProperties" type="CT_CoreProperties"/>
Expand Down
3 changes: 2 additions & 1 deletion src/file/document-wrapper.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
import { XmlComponent } from "./xml-components";
import { Document, IDocumentOptions } from "./document";
import { Footer } from "./footer/footer";
import { FootNotes } from "./footnotes";
import { Header } from "./header/header";
import { Relationships } from "./relationships";

export interface IViewWrapper {
readonly View: Document | Footer | Header | FootNotes;
readonly View: Document | Footer | Header | FootNotes | XmlComponent;
readonly Relationships: Relationships;
}

Expand Down
Loading