Files
docx-js/src/export/packer/next-compiler.ts
Dolan 010ef05ce3 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
2023-12-30 02:23:54 +00:00

487 lines
18 KiB
TypeScript

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";
import { NumberingReplacer } from "./numbering-replacer";
import { PrettifyType } from "./packer";
interface IXmlifyedFile {
readonly data: string;
readonly path: string;
}
interface IXmlifyedFileMapping {
readonly Document: IXmlifyedFile;
readonly Styles: IXmlifyedFile;
readonly Properties: IXmlifyedFile;
readonly Numbering: IXmlifyedFile;
readonly Relationships: IXmlifyedFile;
readonly FileRelationships: IXmlifyedFile;
readonly Headers: readonly IXmlifyedFile[];
readonly Footers: readonly IXmlifyedFile[];
readonly HeaderRelationships: readonly IXmlifyedFile[];
readonly FooterRelationships: readonly IXmlifyedFile[];
readonly ContentTypes: IXmlifyedFile;
readonly CustomProperties: IXmlifyedFile;
readonly AppProperties: IXmlifyedFile;
readonly FootNotes: IXmlifyedFile;
readonly FootNotesRelationships: IXmlifyedFile;
readonly Settings: IXmlifyedFile;
readonly Comments?: IXmlifyedFile;
readonly FontTable?: IXmlifyedFile;
readonly FontTableRelationships?: IXmlifyedFile;
}
export class Compiler {
private readonly formatter: Formatter;
private readonly imageReplacer: ImageReplacer;
private readonly numberingReplacer: NumberingReplacer;
public constructor() {
this.formatter = new Formatter();
this.imageReplacer = new ImageReplacer();
this.numberingReplacer = new NumberingReplacer();
}
public compile(file: File, prettifyXml?: (typeof PrettifyType)[keyof typeof PrettifyType]): JSZip {
const zip = new JSZip();
const xmlifiedFileMapping = this.xmlifyFile(file, prettifyXml);
const map = new Map<string, IXmlifyedFile | readonly IXmlifyedFile[]>(Object.entries(xmlifiedFileMapping));
for (const [, obj] of map) {
if (Array.isArray(obj)) {
for (const subFile of obj as readonly IXmlifyedFile[]) {
zip.file(subFile.path, subFile.data);
}
} else {
zip.file((obj as IXmlifyedFile).path, (obj as IXmlifyedFile).data);
}
}
for (const { stream, fileName } of file.Media.Array) {
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;
}
private xmlifyFile(file: File, prettify?: (typeof PrettifyType)[keyof typeof PrettifyType]): IXmlifyedFileMapping {
const documentRelationshipCount = file.Document.Relationships.RelationshipCount + 1;
const documentXmlData = xml(
this.formatter.format(file.Document.View, {
viewWrapper: file.Document,
file,
stack: [],
}),
{
indent: prettify,
declaration: {
standalone: "yes",
encoding: "UTF-8",
},
},
);
const documentMediaDatas = this.imageReplacer.getMediaData(documentXmlData, file.Media);
return {
Relationships: {
data: (() => {
documentMediaDatas.forEach((mediaData, i) => {
file.Document.Relationships.createRelationship(
documentRelationshipCount + i,
"http://schemas.openxmlformats.org/officeDocument/2006/relationships/image",
`media/${mediaData.fileName}`,
);
});
return xml(
this.formatter.format(file.Document.Relationships, {
viewWrapper: file.Document,
file,
stack: [],
}),
{
indent: prettify,
declaration: {
encoding: "UTF-8",
},
},
);
})(),
path: "word/_rels/document.xml.rels",
},
Document: {
data: (() => {
const xmlData = this.imageReplacer.replace(documentXmlData, documentMediaDatas, documentRelationshipCount);
const referenedXmlData = this.numberingReplacer.replace(xmlData, file.Numbering.ConcreteNumbering);
return referenedXmlData;
})(),
path: "word/document.xml",
},
Styles: {
data: (() => {
const xmlStyles = xml(
this.formatter.format(file.Styles, {
viewWrapper: file.Document,
file,
stack: [],
}),
{
indent: prettify,
declaration: {
standalone: "yes",
encoding: "UTF-8",
},
},
);
const referencedXmlStyles = this.numberingReplacer.replace(xmlStyles, file.Numbering.ConcreteNumbering);
return referencedXmlStyles;
})(),
path: "word/styles.xml",
},
Properties: {
data: xml(
this.formatter.format(file.CoreProperties, {
viewWrapper: file.Document,
file,
stack: [],
}),
{
indent: prettify,
declaration: {
standalone: "yes",
encoding: "UTF-8",
},
},
),
path: "docProps/core.xml",
},
Numbering: {
data: xml(
this.formatter.format(file.Numbering, {
viewWrapper: file.Document,
file,
stack: [],
}),
{
indent: prettify,
declaration: {
standalone: "yes",
encoding: "UTF-8",
},
},
),
path: "word/numbering.xml",
},
FileRelationships: {
data: xml(
this.formatter.format(file.FileRelationships, {
viewWrapper: file.Document,
file,
stack: [],
}),
{
indent: prettify,
declaration: {
encoding: "UTF-8",
},
},
),
path: "_rels/.rels",
},
HeaderRelationships: file.Headers.map((headerWrapper, index) => {
const xmlData = xml(
this.formatter.format(headerWrapper.View, {
viewWrapper: headerWrapper,
file,
stack: [],
}),
{
indent: prettify,
declaration: {
encoding: "UTF-8",
},
},
);
const mediaDatas = this.imageReplacer.getMediaData(xmlData, file.Media);
mediaDatas.forEach((mediaData, i) => {
headerWrapper.Relationships.createRelationship(
i,
"http://schemas.openxmlformats.org/officeDocument/2006/relationships/image",
`media/${mediaData.fileName}`,
);
});
return {
data: xml(
this.formatter.format(headerWrapper.Relationships, {
viewWrapper: headerWrapper,
file,
stack: [],
}),
{
indent: prettify,
declaration: {
encoding: "UTF-8",
},
},
),
path: `word/_rels/header${index + 1}.xml.rels`,
};
}),
FooterRelationships: file.Footers.map((footerWrapper, index) => {
const xmlData = xml(
this.formatter.format(footerWrapper.View, {
viewWrapper: footerWrapper,
file,
stack: [],
}),
{
indent: prettify,
declaration: {
encoding: "UTF-8",
},
},
);
const mediaDatas = this.imageReplacer.getMediaData(xmlData, file.Media);
mediaDatas.forEach((mediaData, i) => {
footerWrapper.Relationships.createRelationship(
i,
"http://schemas.openxmlformats.org/officeDocument/2006/relationships/image",
`media/${mediaData.fileName}`,
);
});
return {
data: xml(
this.formatter.format(footerWrapper.Relationships, {
viewWrapper: footerWrapper,
file,
stack: [],
}),
{
indent: prettify,
declaration: {
encoding: "UTF-8",
},
},
),
path: `word/_rels/footer${index + 1}.xml.rels`,
};
}),
Headers: file.Headers.map((headerWrapper, index) => {
const tempXmlData = xml(
this.formatter.format(headerWrapper.View, {
viewWrapper: headerWrapper,
file,
stack: [],
}),
{
indent: prettify,
declaration: {
encoding: "UTF-8",
},
},
);
const mediaDatas = this.imageReplacer.getMediaData(tempXmlData, file.Media);
// TODO: 0 needs to be changed when headers get relationships of their own
const xmlData = this.imageReplacer.replace(tempXmlData, mediaDatas, 0);
const referenedXmlData = this.numberingReplacer.replace(xmlData, file.Numbering.ConcreteNumbering);
return {
data: referenedXmlData,
path: `word/header${index + 1}.xml`,
};
}),
Footers: file.Footers.map((footerWrapper, index) => {
const tempXmlData = xml(
this.formatter.format(footerWrapper.View, {
viewWrapper: footerWrapper,
file,
stack: [],
}),
{
indent: prettify,
declaration: {
encoding: "UTF-8",
},
},
);
const mediaDatas = this.imageReplacer.getMediaData(tempXmlData, file.Media);
// TODO: 0 needs to be changed when headers get relationships of their own
const xmlData = this.imageReplacer.replace(tempXmlData, mediaDatas, 0);
const referenedXmlData = this.numberingReplacer.replace(xmlData, file.Numbering.ConcreteNumbering);
return {
data: referenedXmlData,
path: `word/footer${index + 1}.xml`,
};
}),
ContentTypes: {
data: xml(
this.formatter.format(file.ContentTypes, {
viewWrapper: file.Document,
file,
stack: [],
}),
{
indent: prettify,
declaration: {
encoding: "UTF-8",
},
},
),
path: "[Content_Types].xml",
},
CustomProperties: {
data: xml(
this.formatter.format(file.CustomProperties, {
viewWrapper: file.Document,
file,
stack: [],
}),
{
indent: prettify,
declaration: {
standalone: "yes",
encoding: "UTF-8",
},
},
),
path: "docProps/custom.xml",
},
AppProperties: {
data: xml(
this.formatter.format(file.AppProperties, {
viewWrapper: file.Document,
file,
stack: [],
}),
{
indent: prettify,
declaration: {
standalone: "yes",
encoding: "UTF-8",
},
},
),
path: "docProps/app.xml",
},
FootNotes: {
data: xml(
this.formatter.format(file.FootNotes.View, {
viewWrapper: file.FootNotes,
file,
stack: [],
}),
{
indent: prettify,
declaration: {
encoding: "UTF-8",
},
},
),
path: "word/footnotes.xml",
},
FootNotesRelationships: {
data: xml(
this.formatter.format(file.FootNotes.Relationships, {
viewWrapper: file.FootNotes,
file,
stack: [],
}),
{
indent: prettify,
declaration: {
encoding: "UTF-8",
},
},
),
path: "word/_rels/footnotes.xml.rels",
},
Settings: {
data: xml(
this.formatter.format(file.Settings, {
viewWrapper: file.Document,
file,
stack: [],
}),
{
indent: prettify,
declaration: {
standalone: "yes",
encoding: "UTF-8",
},
},
),
path: "word/settings.xml",
},
Comments: {
data: xml(
this.formatter.format(file.Comments, {
viewWrapper: file.Document,
file,
stack: [],
}),
{
indent: prettify,
declaration: {
standalone: "yes",
encoding: "UTF-8",
},
},
),
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",
},
};
}
}