From 010ef05ce34728364bb15eb474da407868462858 Mon Sep 17 00:00:00 2001 From: Dolan Date: Sat, 30 Dec 2023 02:23:54 +0000 Subject: [PATCH] Feat/embedded fonts (#2174) * #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 --- .cspell.json | 1 + demo/91-custom-fonts.ts | 40 ++++ demo/92-declarative-custom-fonts.ts | 44 ++++ demo/assets/Pacifico.ttf | Bin 0 -> 75568 bytes demo/tsconfig.json | 10 + src/export/packer/next-compiler.spec.ts | 19 +- src/export/packer/next-compiler.ts | 42 ++++ src/file/content-types/content-types.spec.ts | 31 ++- src/file/content-types/content-types.ts | 2 + src/file/core-properties/properties.ts | 2 + src/file/document-wrapper.ts | 3 +- src/file/file.ts | 8 + src/file/fonts/create-regular-font.ts | 33 +++ src/file/fonts/font-table.ts | 44 ++++ src/file/fonts/font-wrapper.ts | 36 +++ src/file/fonts/font.spec.ts | 223 ++++++++++++++++++ src/file/fonts/font.ts | 156 ++++++++++++ src/file/fonts/index.ts | 1 + src/file/fonts/obfuscate-ttf-to-odttf.ts | 22 ++ .../fonts/obsfuscate-ttf-to-odtts.spec.ts | 14 ++ src/file/index.ts | 1 + src/file/paragraph/run/image-run.ts | 2 +- .../relationship/relationship.ts | 3 +- .../xml-components/imported-xml-component.ts | 1 + src/file/xml-components/simple-elements.ts | 24 +- src/util/convenience-functions.spec.ts | 50 +++- src/util/convenience-functions.ts | 6 +- vite.config.ts | 6 +- 28 files changed, 794 insertions(+), 30 deletions(-) create mode 100644 demo/91-custom-fonts.ts create mode 100644 demo/92-declarative-custom-fonts.ts create mode 100644 demo/assets/Pacifico.ttf create mode 100644 demo/tsconfig.json create mode 100644 src/file/fonts/create-regular-font.ts create mode 100644 src/file/fonts/font-table.ts create mode 100644 src/file/fonts/font-wrapper.ts create mode 100644 src/file/fonts/font.spec.ts create mode 100644 src/file/fonts/font.ts create mode 100644 src/file/fonts/index.ts create mode 100644 src/file/fonts/obfuscate-ttf-to-odttf.ts create mode 100644 src/file/fonts/obsfuscate-ttf-to-odtts.spec.ts diff --git a/.cspell.json b/.cspell.json index 1e8efa7a1e..93ca72762a 100644 --- a/.cspell.json +++ b/.cspell.json @@ -21,6 +21,7 @@ "iife", "Initializable", "iroha", + "JOHAB", "jsonify", "jszip", "NUMPAGES", diff --git a/demo/91-custom-fonts.ts b/demo/91-custom-fonts.ts new file mode 100644 index 0000000000..e19c2e2750 --- /dev/null +++ b/demo/91-custom-fonts.ts @@ -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); +}); diff --git a/demo/92-declarative-custom-fonts.ts b/demo/92-declarative-custom-fonts.ts new file mode 100644 index 0000000000..524d242a12 --- /dev/null +++ b/demo/92-declarative-custom-fonts.ts @@ -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); +}); diff --git a/demo/assets/Pacifico.ttf b/demo/assets/Pacifico.ttf new file mode 100644 index 0000000000000000000000000000000000000000..6d47cdc9acee83109bbbf1d444ea9a0c67ade028 GIT binary patch literal 75568 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 literal 0 HcmV?d00001 diff --git a/demo/tsconfig.json b/demo/tsconfig.json new file mode 100644 index 0000000000..70206942be --- /dev/null +++ b/demo/tsconfig.json @@ -0,0 +1,10 @@ +{ + "extends": "../tsconfig.json", + "compilerOptions": { + "rootDir": "./", + "paths": { + "docx": ["../build"] + } + }, + "include": ["../demo"] +} \ No newline at end of file diff --git a/src/export/packer/next-compiler.spec.ts b/src/export/packer/next-compiler.spec.ts index 3ddfe34fb6..6713ee2734 100644 --- a/src/export/packer/next-compiler.spec.ts +++ b/src/export/packer/next-compiler.spec.ts @@ -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"); @@ -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"); }, @@ -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"); @@ -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: [ { @@ -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); + }); }); }); diff --git a/src/export/packer/next-compiler.ts b/src/export/packer/next-compiler.ts index e1ec950af2..0b0434a0b2 100644 --- a/src/export/packer/next-compiler.ts +++ b/src/export/packer/next-compiler.ts @@ -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"; @@ -31,6 +32,8 @@ interface IXmlifyedFileMapping { readonly FootNotesRelationships: IXmlifyedFile; readonly Settings: IXmlifyedFile; readonly Comments?: IXmlifyedFile; + readonly FontTable?: IXmlifyedFile; + readonly FontTableRelationships?: IXmlifyedFile; } export class Compiler { @@ -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; } @@ -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", + }, }; } } diff --git a/src/file/content-types/content-types.spec.ts b/src/file/content-types/content-types.spec.ts index 54e70889b2..a0203d0dae 100644 --- a/src/file/content-types/content-types.spec.ts +++ b/src/file/content-types/content-types.spec.ts @@ -29,7 +29,16 @@ 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", @@ -37,7 +46,7 @@ describe("ContentTypes", () => { }, }, }); - expect(tree["Types"][9]).to.deep.equal({ + expect(tree["Types"][10]).to.deep.equal({ Override: { _attr: { ContentType: "application/vnd.openxmlformats-officedocument.wordprocessingml.styles+xml", @@ -45,7 +54,7 @@ describe("ContentTypes", () => { }, }, }); - expect(tree["Types"][10]).to.deep.equal({ + expect(tree["Types"][11]).to.deep.equal({ Override: { _attr: { ContentType: "application/vnd.openxmlformats-package.core-properties+xml", @@ -53,7 +62,7 @@ describe("ContentTypes", () => { }, }, }); - expect(tree["Types"][11]).to.deep.equal({ + expect(tree["Types"][12]).to.deep.equal({ Override: { _attr: { ContentType: "application/vnd.openxmlformats-officedocument.custom-properties+xml", @@ -61,7 +70,7 @@ describe("ContentTypes", () => { }, }, }); - expect(tree["Types"][12]).to.deep.equal({ + expect(tree["Types"][13]).to.deep.equal({ Override: { _attr: { ContentType: "application/vnd.openxmlformats-officedocument.extended-properties+xml", @@ -69,7 +78,7 @@ describe("ContentTypes", () => { }, }, }); - expect(tree["Types"][13]).to.deep.equal({ + expect(tree["Types"][14]).to.deep.equal({ Override: { _attr: { ContentType: "application/vnd.openxmlformats-officedocument.wordprocessingml.numbering+xml", @@ -77,7 +86,7 @@ describe("ContentTypes", () => { }, }, }); - expect(tree["Types"][14]).to.deep.equal({ + expect(tree["Types"][15]).to.deep.equal({ Override: { _attr: { ContentType: "application/vnd.openxmlformats-officedocument.wordprocessingml.footnotes+xml", @@ -85,7 +94,7 @@ describe("ContentTypes", () => { }, }, }); - expect(tree["Types"][15]).to.deep.equal({ + expect(tree["Types"][16]).to.deep.equal({ Override: { _attr: { ContentType: "application/vnd.openxmlformats-officedocument.wordprocessingml.settings+xml", @@ -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", @@ -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", @@ -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", @@ -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", diff --git a/src/file/content-types/content-types.ts b/src/file/content-types/content-types.ts index 76cd7e819b..399581f7d6 100644 --- a/src/file/content-types/content-types.ts +++ b/src/file/content-types/content-types.ts @@ -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"), @@ -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 { diff --git a/src/file/core-properties/properties.ts b/src/file/core-properties/properties.ts index d262802142..d51b1f23b7 100644 --- a/src/file/core-properties/properties.ts +++ b/src/file/core-properties/properties.ts @@ -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"; @@ -40,6 +41,7 @@ export interface IPropertiesOptions { readonly customProperties?: readonly ICustomPropertyOptions[]; readonly evenAndOddHeaderAndFooters?: boolean; readonly defaultTabStop?: number; + readonly fonts?: readonly FontOptions[]; } // diff --git a/src/file/document-wrapper.ts b/src/file/document-wrapper.ts index 578e8c8f58..e60b2a1927 100644 --- a/src/file/document-wrapper.ts +++ b/src/file/document-wrapper.ts @@ -1,3 +1,4 @@ +import { XmlComponent } from "./xml-components"; import { Document, IDocumentOptions } from "./document"; import { Footer } from "./footer/footer"; import { FootNotes } from "./footnotes"; @@ -5,7 +6,7 @@ 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; } diff --git a/src/file/file.ts b/src/file/file.ts index 1046d874a5..5aee17d7d1 100644 --- a/src/file/file.ts +++ b/src/file/file.ts @@ -17,6 +17,7 @@ import { Styles } from "./styles"; import { ExternalStylesFactory } from "./styles/external-styles-factory"; import { DefaultStylesFactory } from "./styles/factory"; import { FileChild } from "./file-child"; +import { FontWrapper } from "./fonts/font-wrapper"; export interface ISectionOptions { readonly headers?: { @@ -53,6 +54,7 @@ export class File { private readonly appProperties: AppProperties; private readonly styles: Styles; private readonly comments: Comments; + private readonly fontWrapper: FontWrapper; public constructor(options: IPropertiesOptions) { this.coreProperties = new CoreProperties({ @@ -109,6 +111,8 @@ export class File { this.footnotesWrapper.View.createFootNote(parseFloat(key), options.footnotes[key].children); } } + + this.fontWrapper = new FontWrapper(options.fonts ?? []); } private addSection({ headers = {}, footers = {}, children, properties }: ISectionOptions): void { @@ -292,4 +296,8 @@ export class File { public get Comments(): Comments { return this.comments; } + + public get FontTable(): FontWrapper { + return this.fontWrapper; + } } diff --git a/src/file/fonts/create-regular-font.ts b/src/file/fonts/create-regular-font.ts new file mode 100644 index 0000000000..e72bd6541c --- /dev/null +++ b/src/file/fonts/create-regular-font.ts @@ -0,0 +1,33 @@ +import { XmlComponent } from "@file/xml-components"; + +import { CharacterSet, createFont } from "./font"; + +export const createRegularFont = ({ + name, + index, + fontKey, + characterSet, +}: { + readonly name: string; + readonly index: number; + readonly fontKey: string; + readonly characterSet?: (typeof CharacterSet)[keyof typeof CharacterSet]; +}): XmlComponent => + createFont({ + name, + sig: { + usb0: "E0002AFF", + usb1: "C000247B", + usb2: "00000009", + usb3: "00000000", + csb0: "000001FF", + csb1: "00000000", + }, + charset: characterSet, + family: "auto", + pitch: "variable", + embedRegular: { + fontKey, + id: `rId${index}`, + }, + }); diff --git a/src/file/fonts/font-table.ts b/src/file/fonts/font-table.ts new file mode 100644 index 0000000000..fce632a9e2 --- /dev/null +++ b/src/file/fonts/font-table.ts @@ -0,0 +1,44 @@ +import { BuilderElement, XmlComponent } from "@file/xml-components"; + +import { createRegularFont } from "./create-regular-font"; +import { FontOptionsWithKey } from "./font-wrapper"; +import { CharacterSet } from "./font"; + +// +// +// +// +// + +export type FontOptions = { + readonly name: string; + readonly data: Buffer; + readonly characterSet?: (typeof CharacterSet)[keyof typeof CharacterSet]; +}; + +export const createFontTable = (fonts: readonly FontOptionsWithKey[]): XmlComponent => + // https://c-rex.net/projects/samples/ooxml/e1/Part4/OOXML_P4_DOCX_Font_topic_ID0ERNCU.html + // http://www.datypic.com/sc/ooxml/e-w_fonts.html + new BuilderElement({ + name: "w:fonts", + attributes: { + mc: { key: "xmlns:mc", value: "http://schemas.openxmlformats.org/markup-compatibility/2006" }, + r: { key: "xmlns:r", value: "http://schemas.openxmlformats.org/officeDocument/2006/relationships" }, + w: { key: "xmlns:w", value: "http://schemas.openxmlformats.org/wordprocessingml/2006/main" }, + w14: { key: "xmlns:w14", value: "http://schemas.microsoft.com/office/word/2010/wordml" }, + w15: { key: "xmlns:w15", value: "http://schemas.microsoft.com/office/word/2012/wordml" }, + w16cex: { key: "xmlns:w16cex", value: "http://schemas.microsoft.com/office/word/2018/wordml/cex" }, + w16cid: { key: "xmlns:w16cid", value: "http://schemas.microsoft.com/office/word/2016/wordml/cid" }, + w16: { key: "xmlns:w16", value: "http://schemas.microsoft.com/office/word/2018/wordml" }, + w16sdtdh: { key: "xmlns:w16sdtdh", value: "http://schemas.microsoft.com/office/word/2020/wordml/sdtdatahash" }, + w16se: { key: "xmlns:w16se", value: "http://schemas.microsoft.com/office/word/2015/wordml/symex" }, + Ignorable: { key: "mc:Ignorable", value: "w14 w15 w16se w16cid w16 w16cex w16sdtdh" }, + }, + children: fonts.map((font, i) => + createRegularFont({ + name: font.name, + index: i + 1, + fontKey: font.fontKey, + }), + ), + }); diff --git a/src/file/fonts/font-wrapper.ts b/src/file/fonts/font-wrapper.ts new file mode 100644 index 0000000000..5013df398d --- /dev/null +++ b/src/file/fonts/font-wrapper.ts @@ -0,0 +1,36 @@ +import { IViewWrapper } from "@file/document-wrapper"; +import { Relationships } from "@file/relationships"; +import { XmlComponent } from "@file/xml-components"; +import { uniqueUuid } from "@util/convenience-functions"; + +import { FontOptions, createFontTable } from "./font-table"; + +export type FontOptionsWithKey = FontOptions & { readonly fontKey: string }; + +export class FontWrapper implements IViewWrapper { + private readonly fontTable: XmlComponent; + private readonly relationships: Relationships; + public readonly fontOptionsWithKey: readonly FontOptionsWithKey[] = []; + + public constructor(public readonly options: readonly FontOptions[]) { + this.fontOptionsWithKey = options.map((o) => ({ ...o, fontKey: uniqueUuid() })); + this.fontTable = createFontTable(this.fontOptionsWithKey); + this.relationships = new Relationships(); + + for (let i = 0; i < options.length; i++) { + this.relationships.createRelationship( + i + 1, + "http://schemas.openxmlformats.org/officeDocument/2006/relationships/font", + `fonts/${options[i].name}.odttf`, + ); + } + } + + public get View(): XmlComponent { + return this.fontTable; + } + + public get Relationships(): Relationships { + return this.relationships; + } +} diff --git a/src/file/fonts/font.spec.ts b/src/file/fonts/font.spec.ts new file mode 100644 index 0000000000..bfb0bc2efc --- /dev/null +++ b/src/file/fonts/font.spec.ts @@ -0,0 +1,223 @@ +import { describe, expect, it } from "vitest"; + +import { Formatter } from "@export/formatter"; + +import { createFont } from "./font"; + +describe("font", () => { + it("should work", () => { + const tree = new Formatter().format( + createFont({ + name: "Times New Roman", + altName: "Times New Roman", + family: "roman", + charset: "00", + panose1: "02020603050405020304", + pitch: "variable", + embedRegular: { + id: "rId0", + fontKey: "00000000-0000-0000-0000-000000000000", + }, + }), + ); + + expect(tree).to.deep.equal({ + "w:font": [ + { + _attr: { + "w:name": "Times New Roman", + }, + }, + { + "w:altName": { + _attr: { + "w:val": "Times New Roman", + }, + }, + }, + { + "w:panose1": { + _attr: { + "w:val": "02020603050405020304", + }, + }, + }, + { + "w:charset": { + _attr: { + "w:val": "00", + }, + }, + }, + { + "w:family": { + _attr: { + "w:val": "roman", + }, + }, + }, + { + "w:pitch": { + _attr: { + "w:val": "variable", + }, + }, + }, + { + "w:embedRegular": { + _attr: { + "r:id": "rId0", + "w:fontKey": "{00000000-0000-0000-0000-000000000000}", + }, + }, + }, + ], + }); + }); + + it("should work for embedBold", () => { + const tree = new Formatter().format( + createFont({ + name: "Times New Roman", + embedBold: { + id: "rId0", + fontKey: "00000000-0000-0000-0000-000000000000", + }, + }), + ); + + expect(tree).toStrictEqual({ + "w:font": expect.arrayContaining([ + { + "w:embedBold": { + _attr: { + "r:id": "rId0", + "w:fontKey": "{00000000-0000-0000-0000-000000000000}", + }, + }, + }, + ]), + }); + }); + + it("should work for embedBoldItalic", () => { + const tree = new Formatter().format( + createFont({ + name: "Times New Roman", + embedBoldItalic: { + id: "rId0", + fontKey: "00000000-0000-0000-0000-000000000000", + }, + }), + ); + + expect(tree).toStrictEqual({ + "w:font": expect.arrayContaining([ + { + "w:embedBoldItalic": { + _attr: { + "r:id": "rId0", + "w:fontKey": "{00000000-0000-0000-0000-000000000000}", + }, + }, + }, + ]), + }); + }); + + it("should work for embedItalic", () => { + const tree = new Formatter().format( + createFont({ + name: "Times New Roman", + embedItalic: { + id: "rId0", + fontKey: "00000000-0000-0000-0000-000000000000", + }, + }), + ); + + expect(tree).toStrictEqual({ + "w:font": expect.arrayContaining([ + { + "w:embedItalic": { + _attr: { + "r:id": "rId0", + "w:fontKey": "{00000000-0000-0000-0000-000000000000}", + }, + }, + }, + ]), + }); + }); + + it("should work for notTrueType", () => { + const tree = new Formatter().format( + createFont({ + name: "Times New Roman", + embedRegular: { + id: "rId0", + fontKey: "00000000-0000-0000-0000-000000000000", + subsetted: true, + }, + }), + ); + + expect(tree).toStrictEqual({ + "w:font": expect.arrayContaining([ + { + "w:embedRegular": [ + { + _attr: { + "r:id": "rId0", + "w:fontKey": "{00000000-0000-0000-0000-000000000000}", + }, + }, + { + "w:subsetted": {}, + }, + ], + }, + ]), + }); + }); + + it("should work for subsetted", () => { + const tree = new Formatter().format( + createFont({ + name: "Times New Roman", + notTrueType: true, + }), + ); + + expect(tree).toStrictEqual({ + "w:font": expect.arrayContaining([ + { + "w:notTrueType": {}, + }, + ]), + }); + }); + + it("should work without fontKey", () => { + const tree = new Formatter().format( + createFont({ + name: "Times New Roman", + embedItalic: { + id: "rId0", + }, + }), + ); + + expect(tree).toStrictEqual({ + "w:font": expect.arrayContaining([ + { + "w:embedItalic": { + _attr: { + "r:id": "rId0", + }, + }, + }, + ]), + }); + }); +}); diff --git a/src/file/fonts/font.ts b/src/file/fonts/font.ts new file mode 100644 index 0000000000..618bcd6293 --- /dev/null +++ b/src/file/fonts/font.ts @@ -0,0 +1,156 @@ +import { BuilderElement, createStringElement, OnOffElement, XmlComponent } from "@file/xml-components"; + +// +// +// +// +// +// +// +// +// +// +// +// +// +// +// +// + +// +// +// +// +// +// +// +// + +// http://www.datypic.com/sc/ooxml/e-w_embedRegular-1.html +export interface IFontRelationshipOptions { + /** + * Relationship to Part + */ + readonly id: string; + /** + * Embedded Font Obfuscation Key + */ + readonly fontKey?: string; + /** + * Embedded Font Is Subsetted + */ + readonly subsetted?: boolean; +} + +export const CharacterSet = { + ANSI: "00", + DEFAULT: "01", + SYMBOL: "02", + MAC: "4D", + JIS: "80", + HANGUL: "81", + JOHAB: "82", + GB_2312: "86", + CHINESEBIG5: "88", + GREEK: "A1", + TURKISH: "A2", + VIETNAMESE: "A3", + HEBREW: "B1", + ARABIC: "B2", + BALTIC: "BA", + RUSSIAN: "CC", + THAI: "DE", + EASTEUROPE: "EE", + OEM: "FF", +} as const; + +export type FontOptions = { + readonly name: string; + readonly altName?: string; + readonly panose1?: string; + readonly charset?: (typeof CharacterSet)[keyof typeof CharacterSet]; + readonly family?: string; + readonly notTrueType?: boolean; + readonly pitch?: string; + readonly sig?: { + readonly usb0: string; + readonly usb1: string; + readonly usb2: string; + readonly usb3: string; + readonly csb0: string; + readonly csb1: string; + }; + readonly embedRegular?: IFontRelationshipOptions; + readonly embedBold?: IFontRelationshipOptions; + readonly embedItalic?: IFontRelationshipOptions; + readonly embedBoldItalic?: IFontRelationshipOptions; +}; + +const createFontRelationship = ({ id, fontKey, subsetted }: IFontRelationshipOptions, name: string): XmlComponent => + new BuilderElement({ + name, + attributes: { + id: { key: "r:id", value: id }, + ...(fontKey ? { fontKey: { key: "w:fontKey", value: `{${fontKey}}` } } : {}), + }, + children: [...(subsetted ? [new OnOffElement("w:subsetted", subsetted)] : [])], + }); + +export const createFont = ({ + name, + altName, + panose1, + charset, + family, + notTrueType, + pitch, + sig, + embedRegular, + embedBold, + embedItalic, + embedBoldItalic, +}: FontOptions): XmlComponent => + // http://www.datypic.com/sc/ooxml/e-w_font-1.html + new BuilderElement({ + name: "w:font", + attributes: { + name: { key: "w:name", value: name }, + }, + children: [ + // http://www.datypic.com/sc/ooxml/e-w_altName-1.html + ...(altName ? [createStringElement("w:altName", altName)] : []), + // http://www.datypic.com/sc/ooxml/e-w_panose1-1.html + ...(panose1 ? [createStringElement("w:panose1", panose1)] : []), + // http://www.datypic.com/sc/ooxml/e-w_charset-1.html + ...(charset ? [createStringElement("w:charset", charset)] : []), + // http://www.datypic.com/sc/ooxml/e-w_family-1.html + ...(family ? [createStringElement("w:family", family)] : []), + // http://www.datypic.com/sc/ooxml/e-w_notTrueType-1.html + ...(notTrueType ? [new OnOffElement("w:notTrueType", notTrueType)] : []), + ...(pitch ? [createStringElement("w:pitch", pitch)] : []), + // http://www.datypic.com/sc/ooxml/e-w_sig-1.html + ...(sig + ? [ + new BuilderElement({ + name: "w:sig", + attributes: { + usb0: { key: "w:usb0", value: sig.usb0 }, + usb1: { key: "w:usb1", value: sig.usb1 }, + usb2: { key: "w:usb2", value: sig.usb2 }, + usb3: { key: "w:usb3", value: sig.usb3 }, + csb0: { key: "w:csb0", value: sig.csb0 }, + csb1: { key: "w:csb1", value: sig.csb1 }, + }, + }), + ] + : []), + // http://www.datypic.com/sc/ooxml/e-w_embedRegular-1.html + ...(embedRegular ? [createFontRelationship(embedRegular, "w:embedRegular")] : []), + // http://www.datypic.com/sc/ooxml/e-w_embedBold-1.html + ...(embedBold ? [createFontRelationship(embedBold, "w:embedBold")] : []), + // http://www.datypic.com/sc/ooxml/e-w_embedItalic-1.html + ...(embedItalic ? [createFontRelationship(embedItalic, "w:embedItalic")] : []), + // http://www.datypic.com/sc/ooxml/e-w_embedBoldItalic-1.html + ...(embedBoldItalic ? [createFontRelationship(embedBoldItalic, "w:embedBoldItalic")] : []), + ], + }); diff --git a/src/file/fonts/index.ts b/src/file/fonts/index.ts new file mode 100644 index 0000000000..4476a0a9e1 --- /dev/null +++ b/src/file/fonts/index.ts @@ -0,0 +1 @@ +export { CharacterSet } from "./font"; diff --git a/src/file/fonts/obfuscate-ttf-to-odttf.ts b/src/file/fonts/obfuscate-ttf-to-odttf.ts new file mode 100644 index 0000000000..cf11ad0b89 --- /dev/null +++ b/src/file/fonts/obfuscate-ttf-to-odttf.ts @@ -0,0 +1,22 @@ +const obfuscatedStartOffset = 0; +const obfuscatedEndOffset = 32; +const guidSize = 32; + +export const obfuscate = (buf: Buffer, fontKey: string): Buffer => { + const guid = fontKey.replace(/-/g, ""); + if (guid.length !== guidSize) { + throw new Error(`Error: Cannot extract GUID from font filename: ${fontKey}`); + } + + const hexStrings = guid.replace(/(..)/g, "$1 ").trim().split(" "); + const hexNumbers = hexStrings.map((hexString) => parseInt(hexString, 16)); + // eslint-disable-next-line functional/immutable-data + hexNumbers.reverse(); + + const bytesToObfuscate = buf.slice(obfuscatedStartOffset, obfuscatedEndOffset); + // eslint-disable-next-line no-bitwise + const obfuscatedBytes = bytesToObfuscate.map((byte, i) => byte ^ hexNumbers[i % hexNumbers.length]); + + const out = Buffer.concat([buf.slice(0, obfuscatedStartOffset), obfuscatedBytes, buf.slice(obfuscatedEndOffset)]); + return out; +}; diff --git a/src/file/fonts/obsfuscate-ttf-to-odtts.spec.ts b/src/file/fonts/obsfuscate-ttf-to-odtts.spec.ts new file mode 100644 index 0000000000..54412bbcd9 --- /dev/null +++ b/src/file/fonts/obsfuscate-ttf-to-odtts.spec.ts @@ -0,0 +1,14 @@ +import { describe, expect, it } from "vitest"; + +import { obfuscate } from "./obfuscate-ttf-to-odttf"; + +describe("obfuscate", () => { + it("should work", () => { + const buffer = obfuscate(Buffer.from(""), "00000000-0000-0000-0000-000000000000"); + expect(buffer).toBeDefined(); + }); + + it("should throw error if uuid is not correct", () => { + expect(() => obfuscate(Buffer.from(""), "bad-uuid")).toThrowError(); + }); +}); diff --git a/src/file/index.ts b/src/file/index.ts index 6cf75e4f24..db03f4ec7b 100644 --- a/src/file/index.ts +++ b/src/file/index.ts @@ -18,3 +18,4 @@ export * from "./shared"; export * from "./border"; export * from "./vertical-align"; export * from "./checkbox"; +export * from "./fonts"; diff --git a/src/file/paragraph/run/image-run.ts b/src/file/paragraph/run/image-run.ts index 6945ff8e7e..7a674a13c8 100644 --- a/src/file/paragraph/run/image-run.ts +++ b/src/file/paragraph/run/image-run.ts @@ -70,8 +70,8 @@ export class ImageRun extends Run { .split("") .map((c) => c.charCodeAt(0)), ); + /* c8 ignore next 6 */ } else { - /* c8 ignore next 4 */ // Not possible to test this branch in NodeJS // eslint-disable-next-line @typescript-eslint/no-require-imports, @typescript-eslint/no-var-requires const b = require("buf" + "fer"); diff --git a/src/file/relationships/relationship/relationship.ts b/src/file/relationships/relationship/relationship.ts index 099c7c11f1..4734d61d58 100644 --- a/src/file/relationships/relationship/relationship.ts +++ b/src/file/relationships/relationship/relationship.ts @@ -17,7 +17,8 @@ export type RelationshipType = | "http://schemas.openxmlformats.org/officeDocument/2006/relationships/extended-properties" | "http://schemas.openxmlformats.org/officeDocument/2006/relationships/hyperlink" | "http://schemas.openxmlformats.org/officeDocument/2006/relationships/footnotes" - | "http://schemas.openxmlformats.org/officeDocument/2006/relationships/comments"; + | "http://schemas.openxmlformats.org/officeDocument/2006/relationships/comments" + | "http://schemas.openxmlformats.org/officeDocument/2006/relationships/font"; export const TargetModeType = { EXTERNAL: "External", diff --git a/src/file/xml-components/imported-xml-component.ts b/src/file/xml-components/imported-xml-component.ts index 202ae2d339..e90c169dd9 100644 --- a/src/file/xml-components/imported-xml-component.ts +++ b/src/file/xml-components/imported-xml-component.ts @@ -30,6 +30,7 @@ export const convertToXmlComponent = (element: XmlElement): ImportedXmlComponent return element.text as string; default: return undefined; + /* c8 ignore next 2 */ } }; diff --git a/src/file/xml-components/simple-elements.ts b/src/file/xml-components/simple-elements.ts index 898c65e567..3a4651e4c9 100644 --- a/src/file/xml-components/simple-elements.ts +++ b/src/file/xml-components/simple-elements.ts @@ -55,6 +55,14 @@ export class StringValueElement extends XmlComponent { } } +export const createStringElement = (name: string, value: string): XmlComponent => + new BuilderElement({ + name, + attributes: { + value: { key: "w:val", value }, + }, + }); + // This represents various number element types. export class NumberValueElement extends XmlComponent { public constructor(name: string, val: number) { @@ -82,19 +90,23 @@ export class StringContainer extends XmlComponent { } export class BuilderElement extends XmlComponent { - public constructor(options: { + public constructor({ + name, + attributes, + children, + }: { readonly name: string; readonly attributes?: AttributePayload; readonly children?: readonly XmlComponent[]; }) { - super(options.name); + super(name); - if (options.attributes) { - this.root.push(new NextAttributeComponent(options.attributes)); + if (attributes) { + this.root.push(new NextAttributeComponent(attributes)); } - for (const child of options.children ?? []) { - this.root.push(child); + if (children) { + this.root.push(...children); } } } diff --git a/src/util/convenience-functions.spec.ts b/src/util/convenience-functions.spec.ts index 533bae4f09..26b52e7851 100644 --- a/src/util/convenience-functions.spec.ts +++ b/src/util/convenience-functions.spec.ts @@ -1,6 +1,16 @@ import { describe, expect, it } from "vitest"; -import { convertInchesToTwip, convertMillimetersToTwip, uniqueId, uniqueNumericIdCreator } from "./convenience-functions"; +import { + abstractNumUniqueNumericIdGen, + bookmarkUniqueNumericIdGen, + concreteNumUniqueNumericIdGen, + convertInchesToTwip, + convertMillimetersToTwip, + docPropertiesUniqueNumericIdGen, + uniqueId, + uniqueNumericIdCreator, + uniqueUuid, +} from "./convenience-functions"; describe("Utility", () => { describe("#convertMillimetersToTwip", () => { @@ -24,9 +34,47 @@ describe("Utility", () => { }); }); + describe("#abstractNumUniqueNumericIdGen", () => { + it("should generate a unique incrementing ID", () => { + const uniqueNumericId = abstractNumUniqueNumericIdGen(); + expect(uniqueNumericId()).to.equal(1); + expect(uniqueNumericId()).to.equal(2); + }); + }); + + describe("#concreteNumUniqueNumericIdGen", () => { + it("should generate a unique incrementing ID", () => { + const uniqueNumericId = concreteNumUniqueNumericIdGen(); + expect(uniqueNumericId()).to.equal(2); + expect(uniqueNumericId()).to.equal(3); + }); + }); + + describe("#docPropertiesUniqueNumericIdGen", () => { + it("should generate a unique incrementing ID", () => { + const uniqueNumericId = docPropertiesUniqueNumericIdGen(); + expect(uniqueNumericId()).to.equal(1); + expect(uniqueNumericId()).to.equal(2); + }); + }); + + describe("#bookmarkUniqueNumericIdGen", () => { + it("should generate a unique incrementing ID", () => { + const uniqueNumericId = bookmarkUniqueNumericIdGen(); + expect(uniqueNumericId()).to.equal(1); + expect(uniqueNumericId()).to.equal(2); + }); + }); + describe("#uniqueId", () => { it("should generate a unique pseudorandom ID", () => { expect(uniqueId()).to.not.be.empty; }); }); + + describe("#uniqueUuid", () => { + it("should generate a unique pseudorandom ID", () => { + expect(uniqueUuid()).to.not.be.empty; + }); + }); }); diff --git a/src/util/convenience-functions.ts b/src/util/convenience-functions.ts index b6683b0d15..bf3c5774a3 100644 --- a/src/util/convenience-functions.ts +++ b/src/util/convenience-functions.ts @@ -1,4 +1,4 @@ -import { nanoid } from "nanoid/non-secure"; +import { nanoid, customAlphabet } from "nanoid/non-secure"; // Twip - twentieths of a point export const convertMillimetersToTwip = (millimeters: number): number => Math.floor((millimeters / 25.4) * 72 * 20); @@ -23,3 +23,7 @@ export const docPropertiesUniqueNumericIdGen = (): UniqueNumericIdCreator => uni export const bookmarkUniqueNumericIdGen = (): UniqueNumericIdCreator => uniqueNumericIdCreator(); export const uniqueId = (): string => nanoid().toLowerCase(); + +const generateUuidPart = (count: number): string => customAlphabet("1234567890abcdef", count)(); +export const uniqueUuid = (): string => + `${generateUuidPart(8)}-${generateUuidPart(4)}-${generateUuidPart(4)}-${generateUuidPart(4)}-${generateUuidPart(12)}`; diff --git a/vite.config.ts b/vite.config.ts index f53e3f356b..d2fdcd127e 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -62,10 +62,10 @@ export default defineConfig({ provider: "v8", reporter: ["text", "json", "html"], thresholds: { - statements: 99.96, - branches: 98.98, + statements: 99.98, + branches: 99.15, functions: 100, - lines: 99.96, + lines: 99.98, }, exclude: [ ...configDefaults.exclude,