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
This commit is contained in:
@ -21,6 +21,7 @@
|
|||||||
"iife",
|
"iife",
|
||||||
"Initializable",
|
"Initializable",
|
||||||
"iroha",
|
"iroha",
|
||||||
|
"JOHAB",
|
||||||
"jsonify",
|
"jsonify",
|
||||||
"jszip",
|
"jszip",
|
||||||
"NUMPAGES",
|
"NUMPAGES",
|
||||||
|
40
demo/91-custom-fonts.ts
Normal file
40
demo/91-custom-fonts.ts
Normal file
@ -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
demo/92-declarative-custom-fonts.ts
Normal file
44
demo/92-declarative-custom-fonts.ts
Normal file
@ -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);
|
||||||
|
});
|
BIN
demo/assets/Pacifico.ttf
Normal file
BIN
demo/assets/Pacifico.ttf
Normal file
Binary file not shown.
10
demo/tsconfig.json
Normal file
10
demo/tsconfig.json
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
{
|
||||||
|
"extends": "../tsconfig.json",
|
||||||
|
"compilerOptions": {
|
||||||
|
"rootDir": "./",
|
||||||
|
"paths": {
|
||||||
|
"docx": ["../build"]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"include": ["../demo"]
|
||||||
|
}
|
@ -36,7 +36,7 @@ describe("Compiler", () => {
|
|||||||
const fileNames = Object.keys(zipFile.files).map((f) => zipFile.files[f].name);
|
const fileNames = Object.keys(zipFile.files).map((f) => zipFile.files[f].name);
|
||||||
|
|
||||||
expect(fileNames).is.an.instanceof(Array);
|
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/document.xml");
|
||||||
expect(fileNames).to.include("word/styles.xml");
|
expect(fileNames).to.include("word/styles.xml");
|
||||||
expect(fileNames).to.include("docProps/core.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/_rels/footnotes.xml.rels");
|
||||||
expect(fileNames).to.include("word/settings.xml");
|
expect(fileNames).to.include("word/settings.xml");
|
||||||
expect(fileNames).to.include("word/comments.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/document.xml.rels");
|
||||||
|
expect(fileNames).to.include("word/_rels/fontTable.xml.rels");
|
||||||
expect(fileNames).to.include("[Content_Types].xml");
|
expect(fileNames).to.include("[Content_Types].xml");
|
||||||
expect(fileNames).to.include("_rels/.rels");
|
expect(fileNames).to.include("_rels/.rels");
|
||||||
},
|
},
|
||||||
@ -94,7 +96,7 @@ describe("Compiler", () => {
|
|||||||
const fileNames = Object.keys(zipFile.files).map((f) => zipFile.files[f].name);
|
const fileNames = Object.keys(zipFile.files).map((f) => zipFile.files[f].name);
|
||||||
|
|
||||||
expect(fileNames).is.an.instanceof(Array);
|
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/header1.xml");
|
||||||
expect(fileNames).to.include("word/_rels/header1.xml.rels");
|
expect(fileNames).to.include("word/_rels/header1.xml.rels");
|
||||||
@ -127,12 +129,10 @@ describe("Compiler", () => {
|
|||||||
const spy = vi.spyOn(compiler["formatter"], "format");
|
const spy = vi.spyOn(compiler["formatter"], "format");
|
||||||
|
|
||||||
compiler.compile(file);
|
compiler.compile(file);
|
||||||
expect(spy).toBeCalledTimes(13);
|
expect(spy).toBeCalledTimes(15);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should work with media datas", () => {
|
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({
|
const file = new File({
|
||||||
sections: [
|
sections: [
|
||||||
{
|
{
|
||||||
@ -182,5 +182,14 @@ describe("Compiler", () => {
|
|||||||
|
|
||||||
compiler.compile(file);
|
compiler.compile(file);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("should work with fonts", () => {
|
||||||
|
const file = new File({
|
||||||
|
sections: [],
|
||||||
|
fonts: [{ name: "Pacifico", data: Buffer.from("") }],
|
||||||
|
});
|
||||||
|
|
||||||
|
compiler.compile(file);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -2,6 +2,7 @@ import JSZip from "jszip";
|
|||||||
import xml from "xml";
|
import xml from "xml";
|
||||||
|
|
||||||
import { File } from "@file/file";
|
import { File } from "@file/file";
|
||||||
|
import { obfuscate } from "@file/fonts/obfuscate-ttf-to-odttf";
|
||||||
|
|
||||||
import { Formatter } from "../formatter";
|
import { Formatter } from "../formatter";
|
||||||
import { ImageReplacer } from "./image-replacer";
|
import { ImageReplacer } from "./image-replacer";
|
||||||
@ -31,6 +32,8 @@ interface IXmlifyedFileMapping {
|
|||||||
readonly FootNotesRelationships: IXmlifyedFile;
|
readonly FootNotesRelationships: IXmlifyedFile;
|
||||||
readonly Settings: IXmlifyedFile;
|
readonly Settings: IXmlifyedFile;
|
||||||
readonly Comments?: IXmlifyedFile;
|
readonly Comments?: IXmlifyedFile;
|
||||||
|
readonly FontTable?: IXmlifyedFile;
|
||||||
|
readonly FontTableRelationships?: IXmlifyedFile;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class Compiler {
|
export class Compiler {
|
||||||
@ -63,6 +66,11 @@ export class Compiler {
|
|||||||
zip.file(`word/media/${fileName}`, stream);
|
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;
|
return zip;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -439,6 +447,40 @@ export class Compiler {
|
|||||||
),
|
),
|
||||||
path: "word/comments.xml",
|
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",
|
||||||
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -29,7 +29,16 @@ describe("ContentTypes", () => {
|
|||||||
Default: { _attr: { ContentType: "application/vnd.openxmlformats-package.relationships+xml", Extension: "rels" } },
|
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"][7]).to.deep.equal({ Default: { _attr: { ContentType: "application/xml", Extension: "xml" } } });
|
||||||
|
|
||||||
expect(tree["Types"][8]).to.deep.equal({
|
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: {
|
Override: {
|
||||||
_attr: {
|
_attr: {
|
||||||
ContentType: "application/vnd.openxmlformats-officedocument.wordprocessingml.document.main+xml",
|
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: {
|
Override: {
|
||||||
_attr: {
|
_attr: {
|
||||||
ContentType: "application/vnd.openxmlformats-officedocument.wordprocessingml.styles+xml",
|
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: {
|
Override: {
|
||||||
_attr: {
|
_attr: {
|
||||||
ContentType: "application/vnd.openxmlformats-package.core-properties+xml",
|
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: {
|
Override: {
|
||||||
_attr: {
|
_attr: {
|
||||||
ContentType: "application/vnd.openxmlformats-officedocument.custom-properties+xml",
|
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: {
|
Override: {
|
||||||
_attr: {
|
_attr: {
|
||||||
ContentType: "application/vnd.openxmlformats-officedocument.extended-properties+xml",
|
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: {
|
Override: {
|
||||||
_attr: {
|
_attr: {
|
||||||
ContentType: "application/vnd.openxmlformats-officedocument.wordprocessingml.numbering+xml",
|
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: {
|
Override: {
|
||||||
_attr: {
|
_attr: {
|
||||||
ContentType: "application/vnd.openxmlformats-officedocument.wordprocessingml.footnotes+xml",
|
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: {
|
Override: {
|
||||||
_attr: {
|
_attr: {
|
||||||
ContentType: "application/vnd.openxmlformats-officedocument.wordprocessingml.settings+xml",
|
ContentType: "application/vnd.openxmlformats-officedocument.wordprocessingml.settings+xml",
|
||||||
@ -102,7 +111,7 @@ describe("ContentTypes", () => {
|
|||||||
contentTypes.addFooter(102);
|
contentTypes.addFooter(102);
|
||||||
const tree = new Formatter().format(contentTypes);
|
const tree = new Formatter().format(contentTypes);
|
||||||
|
|
||||||
expect(tree["Types"][17]).to.deep.equal({
|
expect(tree["Types"][19]).to.deep.equal({
|
||||||
Override: {
|
Override: {
|
||||||
_attr: {
|
_attr: {
|
||||||
ContentType: "application/vnd.openxmlformats-officedocument.wordprocessingml.footer+xml",
|
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: {
|
Override: {
|
||||||
_attr: {
|
_attr: {
|
||||||
ContentType: "application/vnd.openxmlformats-officedocument.wordprocessingml.footer+xml",
|
ContentType: "application/vnd.openxmlformats-officedocument.wordprocessingml.footer+xml",
|
||||||
@ -128,7 +137,7 @@ describe("ContentTypes", () => {
|
|||||||
contentTypes.addHeader(202);
|
contentTypes.addHeader(202);
|
||||||
const tree = new Formatter().format(contentTypes);
|
const tree = new Formatter().format(contentTypes);
|
||||||
|
|
||||||
expect(tree["Types"][17]).to.deep.equal({
|
expect(tree["Types"][19]).to.deep.equal({
|
||||||
Override: {
|
Override: {
|
||||||
_attr: {
|
_attr: {
|
||||||
ContentType: "application/vnd.openxmlformats-officedocument.wordprocessingml.header+xml",
|
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: {
|
Override: {
|
||||||
_attr: {
|
_attr: {
|
||||||
ContentType: "application/vnd.openxmlformats-officedocument.wordprocessingml.header+xml",
|
ContentType: "application/vnd.openxmlformats-officedocument.wordprocessingml.header+xml",
|
||||||
|
@ -20,6 +20,7 @@ export class ContentTypes extends XmlComponent {
|
|||||||
this.root.push(new Default("image/gif", "gif"));
|
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/vnd.openxmlformats-package.relationships+xml", "rels"));
|
||||||
this.root.push(new Default("application/xml", "xml"));
|
this.root.push(new Default("application/xml", "xml"));
|
||||||
|
this.root.push(new Default("application/vnd.openxmlformats-officedocument.obfuscatedFont", "odttf"));
|
||||||
|
|
||||||
this.root.push(
|
this.root.push(
|
||||||
new Override("application/vnd.openxmlformats-officedocument.wordprocessingml.document.main+xml", "/word/document.xml"),
|
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.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.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.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 {
|
public addFooter(index: number): void {
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
import { ICommentsOptions } from "@file/paragraph/run/comment-run";
|
import { ICommentsOptions } from "@file/paragraph/run/comment-run";
|
||||||
import { ICompatibilityOptions } from "@file/settings/compatibility";
|
import { ICompatibilityOptions } from "@file/settings/compatibility";
|
||||||
|
import { FontOptions } from "@file/fonts/font-table";
|
||||||
import { StringContainer, XmlComponent } from "@file/xml-components";
|
import { StringContainer, XmlComponent } from "@file/xml-components";
|
||||||
import { dateTimeValue } from "@util/values";
|
import { dateTimeValue } from "@util/values";
|
||||||
|
|
||||||
@ -40,6 +41,7 @@ export interface IPropertiesOptions {
|
|||||||
readonly customProperties?: readonly ICustomPropertyOptions[];
|
readonly customProperties?: readonly ICustomPropertyOptions[];
|
||||||
readonly evenAndOddHeaderAndFooters?: boolean;
|
readonly evenAndOddHeaderAndFooters?: boolean;
|
||||||
readonly defaultTabStop?: number;
|
readonly defaultTabStop?: number;
|
||||||
|
readonly fonts?: readonly FontOptions[];
|
||||||
}
|
}
|
||||||
|
|
||||||
// <xs:element name="coreProperties" type="CT_CoreProperties"/>
|
// <xs:element name="coreProperties" type="CT_CoreProperties"/>
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
|
import { XmlComponent } from "./xml-components";
|
||||||
import { Document, IDocumentOptions } from "./document";
|
import { Document, IDocumentOptions } from "./document";
|
||||||
import { Footer } from "./footer/footer";
|
import { Footer } from "./footer/footer";
|
||||||
import { FootNotes } from "./footnotes";
|
import { FootNotes } from "./footnotes";
|
||||||
@ -5,7 +6,7 @@ import { Header } from "./header/header";
|
|||||||
import { Relationships } from "./relationships";
|
import { Relationships } from "./relationships";
|
||||||
|
|
||||||
export interface IViewWrapper {
|
export interface IViewWrapper {
|
||||||
readonly View: Document | Footer | Header | FootNotes;
|
readonly View: Document | Footer | Header | FootNotes | XmlComponent;
|
||||||
readonly Relationships: Relationships;
|
readonly Relationships: Relationships;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -17,6 +17,7 @@ import { Styles } from "./styles";
|
|||||||
import { ExternalStylesFactory } from "./styles/external-styles-factory";
|
import { ExternalStylesFactory } from "./styles/external-styles-factory";
|
||||||
import { DefaultStylesFactory } from "./styles/factory";
|
import { DefaultStylesFactory } from "./styles/factory";
|
||||||
import { FileChild } from "./file-child";
|
import { FileChild } from "./file-child";
|
||||||
|
import { FontWrapper } from "./fonts/font-wrapper";
|
||||||
|
|
||||||
export interface ISectionOptions {
|
export interface ISectionOptions {
|
||||||
readonly headers?: {
|
readonly headers?: {
|
||||||
@ -53,6 +54,7 @@ export class File {
|
|||||||
private readonly appProperties: AppProperties;
|
private readonly appProperties: AppProperties;
|
||||||
private readonly styles: Styles;
|
private readonly styles: Styles;
|
||||||
private readonly comments: Comments;
|
private readonly comments: Comments;
|
||||||
|
private readonly fontWrapper: FontWrapper;
|
||||||
|
|
||||||
public constructor(options: IPropertiesOptions) {
|
public constructor(options: IPropertiesOptions) {
|
||||||
this.coreProperties = new CoreProperties({
|
this.coreProperties = new CoreProperties({
|
||||||
@ -109,6 +111,8 @@ export class File {
|
|||||||
this.footnotesWrapper.View.createFootNote(parseFloat(key), options.footnotes[key].children);
|
this.footnotesWrapper.View.createFootNote(parseFloat(key), options.footnotes[key].children);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this.fontWrapper = new FontWrapper(options.fonts ?? []);
|
||||||
}
|
}
|
||||||
|
|
||||||
private addSection({ headers = {}, footers = {}, children, properties }: ISectionOptions): void {
|
private addSection({ headers = {}, footers = {}, children, properties }: ISectionOptions): void {
|
||||||
@ -292,4 +296,8 @@ export class File {
|
|||||||
public get Comments(): Comments {
|
public get Comments(): Comments {
|
||||||
return this.comments;
|
return this.comments;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public get FontTable(): FontWrapper {
|
||||||
|
return this.fontWrapper;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
33
src/file/fonts/create-regular-font.ts
Normal file
33
src/file/fonts/create-regular-font.ts
Normal file
@ -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}`,
|
||||||
|
},
|
||||||
|
});
|
44
src/file/fonts/font-table.ts
Normal file
44
src/file/fonts/font-table.ts
Normal file
@ -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";
|
||||||
|
|
||||||
|
// <xsd:complexType name="CT_FontsList">
|
||||||
|
// <xsd:sequence>
|
||||||
|
// <xsd:element name="font" type="CT_Font" minOccurs="0" maxOccurs="unbounded"/>
|
||||||
|
// </xsd:sequence>
|
||||||
|
// </xsd:complexType>
|
||||||
|
|
||||||
|
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,
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
});
|
36
src/file/fonts/font-wrapper.ts
Normal file
36
src/file/fonts/font-wrapper.ts
Normal file
@ -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;
|
||||||
|
}
|
||||||
|
}
|
223
src/file/fonts/font.spec.ts
Normal file
223
src/file/fonts/font.spec.ts
Normal file
@ -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",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]),
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
156
src/file/fonts/font.ts
Normal file
156
src/file/fonts/font.ts
Normal file
@ -0,0 +1,156 @@
|
|||||||
|
import { BuilderElement, createStringElement, OnOffElement, XmlComponent } from "@file/xml-components";
|
||||||
|
|
||||||
|
// <xsd:complexType name="CT_Font">
|
||||||
|
// <xsd:sequence>
|
||||||
|
// <xsd:element name="altName" type="CT_String" minOccurs="0" maxOccurs="1"/>
|
||||||
|
// <xsd:element name="panose1" type="CT_Panose" minOccurs="0" maxOccurs="1"/>
|
||||||
|
// <xsd:element name="charset" type="CT_Charset" minOccurs="0" maxOccurs="1"/>
|
||||||
|
// <xsd:element name="family" type="CT_FontFamily" minOccurs="0" maxOccurs="1"/>
|
||||||
|
// <xsd:element name="notTrueType" type="CT_OnOff" minOccurs="0" maxOccurs="1"/>
|
||||||
|
// <xsd:element name="pitch" type="CT_Pitch" minOccurs="0" maxOccurs="1"/>
|
||||||
|
// <xsd:element name="sig" type="CT_FontSig" minOccurs="0" maxOccurs="1"/>
|
||||||
|
// <xsd:element name="embedRegular" type="CT_FontRel" minOccurs="0" maxOccurs="1"/>
|
||||||
|
// <xsd:element name="embedBold" type="CT_FontRel" minOccurs="0" maxOccurs="1"/>
|
||||||
|
// <xsd:element name="embedItalic" type="CT_FontRel" minOccurs="0" maxOccurs="1"/>
|
||||||
|
// <xsd:element name="embedBoldItalic" type="CT_FontRel" minOccurs="0" maxOccurs="1"/>
|
||||||
|
// </xsd:sequence>
|
||||||
|
// <xsd:attribute name="name" type="s:ST_String" use="required"/>
|
||||||
|
// </xsd:complexType>
|
||||||
|
|
||||||
|
// <xsd:complexType name="CT_FontRel">
|
||||||
|
// <xsd:complexContent>
|
||||||
|
// <xsd:extension base="CT_Rel">
|
||||||
|
// <xsd:attribute name="fontKey" type="s:ST_Guid" />
|
||||||
|
// <xsd:attribute name="subsetted" type="s:ST_OnOff" />
|
||||||
|
// </xsd:extension>
|
||||||
|
// </xsd:complexContent>
|
||||||
|
// </xsd:complexType>
|
||||||
|
|
||||||
|
// 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")] : []),
|
||||||
|
],
|
||||||
|
});
|
1
src/file/fonts/index.ts
Normal file
1
src/file/fonts/index.ts
Normal file
@ -0,0 +1 @@
|
|||||||
|
export { CharacterSet } from "./font";
|
22
src/file/fonts/obfuscate-ttf-to-odttf.ts
Normal file
22
src/file/fonts/obfuscate-ttf-to-odttf.ts
Normal file
@ -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;
|
||||||
|
};
|
14
src/file/fonts/obsfuscate-ttf-to-odtts.spec.ts
Normal file
14
src/file/fonts/obsfuscate-ttf-to-odtts.spec.ts
Normal file
@ -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();
|
||||||
|
});
|
||||||
|
});
|
@ -18,3 +18,4 @@ export * from "./shared";
|
|||||||
export * from "./border";
|
export * from "./border";
|
||||||
export * from "./vertical-align";
|
export * from "./vertical-align";
|
||||||
export * from "./checkbox";
|
export * from "./checkbox";
|
||||||
|
export * from "./fonts";
|
||||||
|
@ -70,8 +70,8 @@ export class ImageRun extends Run {
|
|||||||
.split("")
|
.split("")
|
||||||
.map((c) => c.charCodeAt(0)),
|
.map((c) => c.charCodeAt(0)),
|
||||||
);
|
);
|
||||||
|
/* c8 ignore next 6 */
|
||||||
} else {
|
} else {
|
||||||
/* c8 ignore next 4 */
|
|
||||||
// Not possible to test this branch in NodeJS
|
// Not possible to test this branch in NodeJS
|
||||||
// eslint-disable-next-line @typescript-eslint/no-require-imports, @typescript-eslint/no-var-requires
|
// eslint-disable-next-line @typescript-eslint/no-require-imports, @typescript-eslint/no-var-requires
|
||||||
const b = require("buf" + "fer");
|
const b = require("buf" + "fer");
|
||||||
|
@ -17,7 +17,8 @@ export type RelationshipType =
|
|||||||
| "http://schemas.openxmlformats.org/officeDocument/2006/relationships/extended-properties"
|
| "http://schemas.openxmlformats.org/officeDocument/2006/relationships/extended-properties"
|
||||||
| "http://schemas.openxmlformats.org/officeDocument/2006/relationships/hyperlink"
|
| "http://schemas.openxmlformats.org/officeDocument/2006/relationships/hyperlink"
|
||||||
| "http://schemas.openxmlformats.org/officeDocument/2006/relationships/footnotes"
|
| "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 = {
|
export const TargetModeType = {
|
||||||
EXTERNAL: "External",
|
EXTERNAL: "External",
|
||||||
|
@ -30,6 +30,7 @@ export const convertToXmlComponent = (element: XmlElement): ImportedXmlComponent
|
|||||||
return element.text as string;
|
return element.text as string;
|
||||||
default:
|
default:
|
||||||
return undefined;
|
return undefined;
|
||||||
|
/* c8 ignore next 2 */
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -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.
|
// This represents various number element types.
|
||||||
export class NumberValueElement extends XmlComponent {
|
export class NumberValueElement extends XmlComponent {
|
||||||
public constructor(name: string, val: number) {
|
public constructor(name: string, val: number) {
|
||||||
@ -82,19 +90,23 @@ export class StringContainer extends XmlComponent {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export class BuilderElement<T extends AttributeData> extends XmlComponent {
|
export class BuilderElement<T extends AttributeData> extends XmlComponent {
|
||||||
public constructor(options: {
|
public constructor({
|
||||||
|
name,
|
||||||
|
attributes,
|
||||||
|
children,
|
||||||
|
}: {
|
||||||
readonly name: string;
|
readonly name: string;
|
||||||
readonly attributes?: AttributePayload<T>;
|
readonly attributes?: AttributePayload<T>;
|
||||||
readonly children?: readonly XmlComponent[];
|
readonly children?: readonly XmlComponent[];
|
||||||
}) {
|
}) {
|
||||||
super(options.name);
|
super(name);
|
||||||
|
|
||||||
if (options.attributes) {
|
if (attributes) {
|
||||||
this.root.push(new NextAttributeComponent(options.attributes));
|
this.root.push(new NextAttributeComponent(attributes));
|
||||||
}
|
}
|
||||||
|
|
||||||
for (const child of options.children ?? []) {
|
if (children) {
|
||||||
this.root.push(child);
|
this.root.push(...children);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,6 +1,16 @@
|
|||||||
import { describe, expect, it } from "vitest";
|
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("Utility", () => {
|
||||||
describe("#convertMillimetersToTwip", () => {
|
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", () => {
|
describe("#uniqueId", () => {
|
||||||
it("should generate a unique pseudorandom ID", () => {
|
it("should generate a unique pseudorandom ID", () => {
|
||||||
expect(uniqueId()).to.not.be.empty;
|
expect(uniqueId()).to.not.be.empty;
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe("#uniqueUuid", () => {
|
||||||
|
it("should generate a unique pseudorandom ID", () => {
|
||||||
|
expect(uniqueUuid()).to.not.be.empty;
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import { nanoid } from "nanoid/non-secure";
|
import { nanoid, customAlphabet } from "nanoid/non-secure";
|
||||||
|
|
||||||
// Twip - twentieths of a point
|
// Twip - twentieths of a point
|
||||||
export const convertMillimetersToTwip = (millimeters: number): number => Math.floor((millimeters / 25.4) * 72 * 20);
|
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 bookmarkUniqueNumericIdGen = (): UniqueNumericIdCreator => uniqueNumericIdCreator();
|
||||||
|
|
||||||
export const uniqueId = (): string => nanoid().toLowerCase();
|
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)}`;
|
||||||
|
@ -62,10 +62,10 @@ export default defineConfig({
|
|||||||
provider: "v8",
|
provider: "v8",
|
||||||
reporter: ["text", "json", "html"],
|
reporter: ["text", "json", "html"],
|
||||||
thresholds: {
|
thresholds: {
|
||||||
statements: 99.96,
|
statements: 99.98,
|
||||||
branches: 98.98,
|
branches: 99.15,
|
||||||
functions: 100,
|
functions: 100,
|
||||||
lines: 99.96,
|
lines: 99.98,
|
||||||
},
|
},
|
||||||
exclude: [
|
exclude: [
|
||||||
...configDefaults.exclude,
|
...configDefaults.exclude,
|
||||||
|
Reference in New Issue
Block a user