import * as AD from "../../abstract-document/index.js"; import { Packer, Document, ISectionOptions, Paragraph, Table, Bookmark, PageOrientation, Header, Footer, InternalHyperlink, ExternalHyperlink, TextRun, UnderlineType, AlignmentType, WidthType, BorderStyle, TableRow, TableCell, VerticalAlign, ImageRun, SymbolRun, PageBreak, SequentialIdentifier, FootnoteReferenceRun, InsertedTextRun, DeletedTextRun, Math as MathDocX, PageNumber, } from "docx"; import { renderImage } from "./render-image.js"; import { Readable } from "stream"; const abstractDocToDocxFontRatio = 2; const abstractDocPixelToDocxDXARatio = 20; export function exportToHTML5Blob(doc: AD.AbstractDoc.AbstractDoc): Promise { return new Promise((resolve) => { const docx = createDocument(doc); Packer.toBlob(docx).then((blob) => { resolve(blob); }); }); } export function exportToStream(blobStream: NodeJS.WritableStream, doc: AD.AbstractDoc.AbstractDoc): void { const docx = createDocument(doc); Packer.toBuffer(docx).then((buffer) => { const readableStream = new Readable(); readableStream.push(buffer); readableStream.push(null); readableStream.pipe(blobStream); }); } /** * On the client side the stream can be a BlobStream from the blob-stream package. * On the server-side the stream can be a file stream from the fs package. * @param blobStream * @param doc */ function createDocument(doc: AD.AbstractDoc.AbstractDoc): Document { const docx = new Document({ sections: doc.children.map((s) => renderSection(s, doc)), }); return docx; } function renderSection(section: AD.Section.Section, parentResources: AD.Resources.Resources): ISectionOptions { const pageWidth = AD.PageStyle.getWidth(section.page.style); const pageHeight = AD.PageStyle.getHeight(section.page.style); const pageContentMargins = AD.LayoutFoundation.orDefault(section.page.style.contentMargins); const contentAvailableWidth = pageWidth - (pageContentMargins.left + pageContentMargins.right); const resources = AD.Resources.mergeResources([parentResources, section]); const headerChildren = section.page.header.reduce((sofar, c) => { sofar.push(...renderSectionElement(c, resources, contentAvailableWidth)); return sofar; }, [] as Array); const footerChildren = [ ...section.page.footer.reduce((sofar, c) => { sofar.push(...renderSectionElement(c, resources, contentAvailableWidth)); return sofar; }, [] as Array), ]; const contentChildren = [ new Paragraph({ spacing: { before: 0, after: 0, line: 1 }, children: [ new Bookmark({ id: section.id, children: [], }), ], }), ...section.children.reduce((sofar, c) => { sofar.push(...renderSectionElement(c, resources, contentAvailableWidth)); return sofar; }, [] as Array), ]; return { properties: { page: { size: { //DOC JS does the orientation after the width and height are set width: AD.PageStyle.getPaperWidth(section.page.style.paperSize) * abstractDocPixelToDocxDXARatio, height: AD.PageStyle.getPaperHeight(section.page.style.paperSize) * abstractDocPixelToDocxDXARatio, orientation: section.page.style.orientation === "Landscape" ? PageOrientation.LANDSCAPE : PageOrientation.PORTRAIT, }, margin: { bottom: pageContentMargins.bottom * abstractDocPixelToDocxDXARatio, top: pageContentMargins.top * abstractDocPixelToDocxDXARatio, right: pageContentMargins.right * abstractDocPixelToDocxDXARatio, left: pageContentMargins.left * abstractDocPixelToDocxDXARatio, header: pageContentMargins.top * abstractDocPixelToDocxDXARatio, footer: pageContentMargins.bottom * abstractDocPixelToDocxDXARatio, }, }, }, headers: { default: new Header({ children: headerChildren, }), }, footers: { default: new Footer({ children: footerChildren, }), }, children: contentChildren, }; } function renderHyperLink( hyperLink: AD.HyperLink.HyperLink, style: AD.TextStyle.TextStyle ): InternalHyperlink | ExternalHyperlink { const fontSize = AD.TextStyle.calculateFontSize(style, 10) * 2; const textRun = new TextRun({ text: hyperLink.text, font: style.fontFamily || "Helvetica", size: fontSize, color: style.color || "blue", bold: style.bold || style.fontWeight === "bold", characterSpacing: style.characterSpacing, underline: style.underline ? { color: style.color || "blue", type: UnderlineType.SINGLE, } : undefined, }); if (hyperLink.target.startsWith("#") && !hyperLink.target.startsWith("#page=")) { return new InternalHyperlink({ anchor: hyperLink.target, child: textRun, }); } else { return new ExternalHyperlink({ link: hyperLink.target, child: textRun, }); } } function renderSectionElement( element: AD.SectionElement.SectionElement, parentResources: AD.Resources.Resources, contentAvailableWidth: number, keepNext: boolean = false ): ReadonlyArray /*| DOCXJS.TableOfContents | DOCXJS.HyperlinkRef */ { const resources = AD.Resources.mergeResources([parentResources, element]); switch (element.type) { case "Paragraph": return [renderParagraph(element, resources, keepNext)]; case "Group": return [...renderGroup(element, parentResources, contentAvailableWidth)]; case "Table": const table = renderTable(element, resources, contentAvailableWidth, keepNext); return table ? [table, new Paragraph({ keepNext: keepNext, children: [new TextRun({ text: ".", size: 0.000001 })] })] : []; case "PageBreak": return [ new Paragraph({ pageBreakBefore: true, }), ]; default: return [new Paragraph({})]; } } function renderTable( table: AD.Table.Table, resources: AD.Resources.Resources, contentAvailableWidth: number, keepNext: boolean ): Table | undefined { const style = AD.Resources.getStyle( undefined, table.style, "TableStyle", table.styleName, resources ) as AD.TableStyle.TableStyle; const styleMargins = AD.LayoutFoundation.orDefault(style.margins); if (table.children.length === 0) { return undefined; } const tableWidthWithoutInfinity = table.columnWidths.reduce( (sofar, c) => (Number.isFinite(c) ? sofar + c : sofar), 0 ); const amountOfInfinity = table.columnWidths.reduce((sofar, c) => (!Number.isFinite(c) ? sofar + 1 : sofar), 0); const infinityCellWidth = (contentAvailableWidth - tableWidthWithoutInfinity) / amountOfInfinity; const columnWidths = table.columnWidths.map((w) => Number.isFinite(w) ? w * abstractDocPixelToDocxDXARatio : infinityCellWidth * abstractDocPixelToDocxDXARatio ); return new Table({ alignment: style.alignment === "Left" ? AlignmentType.LEFT : style.alignment === "Right" ? AlignmentType.RIGHT : AlignmentType.CENTER, margins: { top: styleMargins.top * abstractDocPixelToDocxDXARatio, bottom: styleMargins.bottom * abstractDocPixelToDocxDXARatio, left: styleMargins.left * abstractDocPixelToDocxDXARatio, right: styleMargins.right * abstractDocPixelToDocxDXARatio, }, width: { type: WidthType.DXA, size: columnWidths.reduce((a, b) => a + b), }, borders: { top: { color: style.cellStyle.borderColor ?? "", size: 0, style: BorderStyle.NONE, }, right: { color: style.cellStyle.borderColor ?? "", size: 0, style: BorderStyle.NONE, }, bottom: { color: style.cellStyle.borderColor ?? "", size: 0, style: BorderStyle.NONE, }, left: { color: style.cellStyle.borderColor ?? "", size: 0, style: BorderStyle.NONE, }, insideHorizontal: { color: style.cellStyle.borderColor ?? "", size: 0, style: BorderStyle.NONE, }, insideVertical: { color: style.cellStyle.borderColor ?? "", size: 0, style: BorderStyle.NONE, }, }, rows: table.children.map((c) => renderRow(c, resources, style.cellStyle, columnWidths, keepNext)), }); } function renderRow( row: AD.TableRow.TableRow, resources: AD.Resources.Resources, tableCellStyle: AD.TableCellStyle.TableCellStyle, columnWidths: ReadonlyArray, keepNext: boolean ): TableRow { return new TableRow({ cantSplit: true, children: row.children.map((c, ix) => renderCell(c, resources, tableCellStyle, columnWidths[ix], keepNext)), }); } function renderCell( cell: AD.TableCell.TableCell, resources: AD.Resources.Resources, tableCellStyle: AD.TableCellStyle.TableCellStyle, width: number, keepNext: boolean ): TableCell { const style = AD.Resources.getStyle( tableCellStyle, cell.style, "TableCellStyle", cell.styleName, resources ) as AD.TableCellStyle.TableCellStyle; const stylePadding = AD.LayoutFoundation.orDefault(style.padding); const styleBorders = AD.LayoutFoundation.orDefault(style.borders); return new TableCell({ verticalAlign: (style.verticalAlignment && style.verticalAlignment === "Top" ? VerticalAlign.TOP : style.verticalAlignment === "Bottom" ? VerticalAlign.BOTTOM : VerticalAlign.CENTER) || undefined, shading: { fill: style.background ? style.background : undefined, }, columnSpan: cell.columnSpan, rowSpan: cell.rowSpan, width: { type: WidthType.DXA, size: width, }, margins: { top: Math.max(stylePadding.top, 0) * abstractDocPixelToDocxDXARatio, bottom: Math.max(stylePadding.bottom, 0) * abstractDocPixelToDocxDXARatio, left: Math.max(stylePadding.left, 0) * abstractDocPixelToDocxDXARatio, right: Math.max(stylePadding.right, 0) * abstractDocPixelToDocxDXARatio, }, borders: { top: { color: style.borderColor ?? "", size: styleBorders.top, style: styleBorders.top ? BorderStyle.SINGLE : BorderStyle.NONE, }, right: { color: style.borderColor ?? "", size: styleBorders.right, style: styleBorders.right ? BorderStyle.SINGLE : BorderStyle.NONE, }, bottom: { color: style.borderColor ?? "", size: styleBorders.bottom, style: styleBorders.bottom ? BorderStyle.SINGLE : BorderStyle.NONE, }, left: { color: style.borderColor ?? "", size: styleBorders.left, style: styleBorders.left ? BorderStyle.SINGLE : BorderStyle.NONE, }, }, children: cell.children.reduce((sofar, c) => { sofar.push(...renderSectionElement(c, resources, width, keepNext)); return sofar; }, [] as Array), }); } function renderAtom( resources: AD.Resources.Resources, textStyle: AD.TextStyle.TextStyle, atom: AD.Atom.Atom ): | TextRun | ImageRun | SymbolRun | Bookmark | PageBreak | SequentialIdentifier | FootnoteReferenceRun | InsertedTextRun | DeletedTextRun | InternalHyperlink | ExternalHyperlink | MathDocX { switch (atom.type) { case "TextField": return renderTextField(resources, textStyle, atom); case "TextRun": return renderTextRun(resources, textStyle, atom); case "Image": return renderImage(atom, textStyle, resources); case "HyperLink": return renderHyperLink(atom, textStyle); case "TocSeparator": return new TextRun({ text: "..." }); default: return new TextRun({ text: "missed" }); // TODO } } function renderTextField( resources: AD.Resources.Resources, textStyle: AD.TextStyle.TextStyle, textField: AD.TextField.TextField ): TextRun { const style = AD.Resources.getStyle( textStyle, textField.style, "TextStyle", textField.styleName, resources ) as AD.TextStyle.TextStyle; switch (textField.fieldType) { case "Date": return renderText(style, new Date(Date.now()).toDateString()); case "PageNumber": return renderPageNumber(style); case "TotalPages": return renderTotalPages(style); default: return renderText(style, ""); } } function renderTextRun( resources: AD.Resources.Resources, textStyle: AD.TextStyle.TextStyle, textRun: AD.TextRun.TextRun ): TextRun { const style = AD.Resources.getNestedStyle( textStyle, textRun.style, "TextStyle", textRun.styleName, resources, textRun.nestedStyleNames || [] ) as AD.TextStyle.TextStyle; return renderText(style, textRun.text); } function renderPageNumber(style: AD.TextStyle.TextStyle): TextRun { const fontSize = AD.TextStyle.calculateFontSize(style, 10) * abstractDocToDocxFontRatio; return new TextRun({ font: style.fontFamily || "Helvetica", size: fontSize, color: style.color || "black", bold: style.bold || style.fontWeight === "bold", characterSpacing: style.characterSpacing, underline: style.underline ? { color: style.color, type: UnderlineType.SINGLE, } : undefined, children: [PageNumber.CURRENT], }); } function renderTotalPages(style: AD.TextStyle.TextStyle): TextRun { const fontSize = AD.TextStyle.calculateFontSize(style, 10) * abstractDocToDocxFontRatio; return new TextRun({ font: style.fontFamily || "Helvetica", size: fontSize, color: style.color || "black", bold: style.bold || style.fontWeight === "bold", characterSpacing: style.characterSpacing, underline: style.underline ? { color: style.color, type: UnderlineType.SINGLE, } : undefined, children: [PageNumber.TOTAL_PAGES], }); } function renderText(style: AD.TextStyle.TextStyle, text: string): TextRun { const fontSize = AD.TextStyle.calculateFontSize(style, 10) * abstractDocToDocxFontRatio; return new TextRun({ text: text, font: style.fontFamily || "Helvetica", size: fontSize, color: style.color || "black", bold: style.bold || style.fontWeight === "bold", characterSpacing: style.characterSpacing, underline: style.underline ? { color: style.color, type: UnderlineType.SINGLE, } : undefined, }); } function renderGroup( group: AD.Group.Group, resources: AD.Resources.Resources, availabelWidth: number ): Array { let sofar = Array(); let keepNext = true; for (let index = 0; index < group.children.length; index++) { if (index == group.children.length - 1) { keepNext = false; } sofar.push(...renderSectionElement(group.children[index], resources, availabelWidth, keepNext)); } return sofar; } function renderParagraph( paragraph: AD.Paragraph.Paragraph, resources: AD.Resources.Resources, keepNext: boolean ): Paragraph { const style = AD.Resources.getStyle( undefined, paragraph.style, "ParagraphStyle", paragraph.styleName, resources ) as AD.ParagraphStyle.ParagraphStyle; const styleMargins = AD.LayoutFoundation.orDefault(style.margins); return new Paragraph({ keepNext: keepNext, alignment: (style.alignment && (style.alignment === "Center" ? AlignmentType.CENTER : style.alignment === "End" ? AlignmentType.END : AlignmentType.START)) || undefined, spacing: { before: Math.max(styleMargins.top, 0) * abstractDocPixelToDocxDXARatio, after: Math.max(styleMargins.bottom, 0) * abstractDocPixelToDocxDXARatio, }, indent: { left: Math.max(styleMargins.left, 0) * abstractDocPixelToDocxDXARatio, right: Math.max(styleMargins.right, 0) * abstractDocPixelToDocxDXARatio, }, children: paragraph.children.map((atom) => renderAtom(resources, style.textStyle, atom)), }); }