Skip to content

Commit

Permalink
Feat/embedded fonts (#2174)
Browse files Browse the repository at this point in the history
* #239 Embedded fonts

* Add boilerplate for font table

* Fix linting

* Fix linting

* Fix odttf naming

* Correct writing of fonts to relationships and font table

new uuid function

* Add font to run

* Add demo

Fix tests

* Add character set support

* Add tests

* Write tests
  • Loading branch information
dolanmiu committed Dec 30, 2023
1 parent fa40129 commit 010ef05
Show file tree
Hide file tree
Showing 28 changed files with 794 additions and 30 deletions.
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

0 comments on commit 010ef05

Please sign in to comment.