reamkit 1.3.0 → 1.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +8 -5
- package/dist/esm/core/converter/facade.d.ts +2 -2
- package/dist/esm/core/converter/facade.js +12 -0
- package/dist/esm/core/converter/ream.d.ts +1 -1
- package/dist/esm/core/converter/ream.js +10 -0
- package/dist/esm/core/opc/index.d.ts +2 -0
- package/dist/esm/core/opc/index.js +1 -0
- package/dist/esm/core/opc/opc-writer.d.ts +28 -0
- package/dist/esm/core/opc/opc-writer.js +60 -0
- package/dist/esm/index.d.ts +1 -0
- package/dist/esm/index.js +2 -1
- package/dist/esm/word/document-parser.js +47 -13
- package/dist/esm/word/docx-writer.d.ts +4 -0
- package/dist/esm/word/docx-writer.js +602 -0
- package/dist/esm/word/drawing-parser.d.ts +1 -0
- package/dist/esm/word/drawing-parser.js +37 -2
- package/package.json +4 -3
package/README.md
CHANGED
|
@@ -34,6 +34,7 @@ const doc = Ream.parse(bytes); // docx or xlsx — sniffed
|
|
|
34
34
|
const pdf = await doc.convert('pdf'); // async — fetches a font if needed
|
|
35
35
|
const svg = await doc.convert('svg'); // same parse, different target
|
|
36
36
|
const html = await doc.convert('html'); // flowed HTML — needs no fonts at all
|
|
37
|
+
const docx = await doc.convert('docx'); // write WordprocessingML back out
|
|
37
38
|
|
|
38
39
|
// Hand the bytes to the browser: preview, download, upload, …
|
|
39
40
|
const url = URL.createObjectURL(new Blob([pdf], { type: 'application/pdf' }));
|
|
@@ -150,10 +151,11 @@ automatically from the document's `docProps/core.xml`), `attachments`
|
|
|
150
151
|
|
|
151
152
|
### Lower-level APIs
|
|
152
153
|
|
|
153
|
-
- `docxReader` / `xlsxReader`, `svgWriter`, `htmlWriter` — the `@experimental`
|
|
154
|
-
interfaces of the interlayer, for building custom pipelines (and
|
|
155
|
-
unused formats out of your bundle); `layoutStyledDocument` produces the
|
|
156
|
-
frozen page model (`PageItem` pages in a top-left `Pt` frame)
|
|
154
|
+
- `docxReader` / `xlsxReader`, `svgWriter`, `htmlWriter`, `docxWriter` — the `@experimental`
|
|
155
|
+
reader/writer interfaces of the interlayer, for building custom pipelines (and
|
|
156
|
+
keeping unused formats out of your bundle); `layoutStyledDocument` produces the
|
|
157
|
+
frozen page model (`PageItem` pages in a top-left `Pt` frame) the page-based
|
|
158
|
+
writers consume (`docxWriter` works from the flow model, before layout).
|
|
157
159
|
- `renderStyledPdf` drives the layout engine directly; the typed document
|
|
158
160
|
model is on the `reamkit/document-model` subpath.
|
|
159
161
|
|
|
@@ -170,7 +172,8 @@ Liang hyphenation, OpenType ligatures/kerning + Arabic cursive joining,
|
|
|
170
172
|
BiDi (UAX #9), hyperlinks (PDF link annotations + HTML anchors,
|
|
171
173
|
scheme-allowlisted), tagged PDF, PDF/A-1/2/3 (a/b/u), PDF/UA-1, AES-256
|
|
172
174
|
encryption, digital signatures (PKCS#7/ECDSA/PAdES/RFC 3161), SVG page
|
|
173
|
-
preview, flowed HTML export
|
|
175
|
+
preview, flowed HTML export, and **docx output** (write WordprocessingML back
|
|
176
|
+
out, incl. docx → docx). Reads OOXML Transitional and Strict.
|
|
174
177
|
|
|
175
178
|
See `handoff.md` for the full feature matrix and known limitations.
|
|
176
179
|
|
|
@@ -5,8 +5,8 @@ import { FontProvider } from '../fonts/provider.js';
|
|
|
5
5
|
import { Loss, LossReport } from '../ir/index.js';
|
|
6
6
|
import { FontBytesByVariant } from '../font/index.js';
|
|
7
7
|
export interface ConvertOptions extends ConvertDocxOptions {
|
|
8
|
-
/** Target: 'pdf' (default), 'svg' (page-stack preview) or '
|
|
9
|
-
readonly to?: 'pdf' | 'svg' | 'html';
|
|
8
|
+
/** Target: 'pdf' (default), 'svg' (page-stack preview), 'html' or 'docx' (flowed). */
|
|
9
|
+
readonly to?: 'pdf' | 'svg' | 'html' | 'docx';
|
|
10
10
|
/**
|
|
11
11
|
* Strict mode (handoff v1 §5): throw ConversionLossError on the first
|
|
12
12
|
* recorded loss instead of returning it in the report.
|
|
@@ -6,6 +6,7 @@ import { docxReader } from "../../word/docx-reader.js";
|
|
|
6
6
|
import { xlsxReader } from "../../excel/xlsx-reader.js";
|
|
7
7
|
import { chainProviders } from "../fonts/provider.js";
|
|
8
8
|
import { flowRenderOptions } from "./project.js";
|
|
9
|
+
import { writeDocx } from "../../word/docx-writer.js";
|
|
9
10
|
import { writeHtml } from "../../html/html-writer.js";
|
|
10
11
|
import { writeSvg } from "../../svg/svg-writer.js";
|
|
11
12
|
import { convertDocxToPdf } from "../../word/docx-to-pdf.js";
|
|
@@ -20,6 +21,17 @@ function createConverter(opts = {}) {
|
|
|
20
21
|
const reader = detect(bytes);
|
|
21
22
|
if (!reader) throw new Error("Unrecognized input format (no registered reader sniffs these bytes)");
|
|
22
23
|
const losses = [];
|
|
24
|
+
if (to === "docx") {
|
|
25
|
+
const { doc: flow, losses: readLosses } = reader.read(bytes);
|
|
26
|
+
losses.push(...readLosses);
|
|
27
|
+
const out = writeDocx(flow);
|
|
28
|
+
losses.push(...out.losses);
|
|
29
|
+
if (strict && losses.length > 0) throw new ConversionLossError(losses[0]);
|
|
30
|
+
return {
|
|
31
|
+
bytes: out.bytes,
|
|
32
|
+
losses
|
|
33
|
+
};
|
|
34
|
+
}
|
|
23
35
|
if (to === "html") {
|
|
24
36
|
const { doc: flow, losses: readLosses } = reader.read(bytes);
|
|
25
37
|
losses.push(...readLosses);
|
|
@@ -6,7 +6,7 @@ import { Loss } from '../ir/index.js';
|
|
|
6
6
|
import { DocumentReader } from '../ir/adapters.js';
|
|
7
7
|
import { FlowDoc } from '../ir/flow.js';
|
|
8
8
|
import { SignatureOptions, StyledRenderOptions } from '../../pdf/index.js';
|
|
9
|
-
export type ReamTarget = 'pdf' | 'svg' | 'html';
|
|
9
|
+
export type ReamTarget = 'pdf' | 'svg' | 'html' | 'docx';
|
|
10
10
|
export interface ReamParseOptions {
|
|
11
11
|
readonly readers?: ReadonlyArray<DocumentReader>;
|
|
12
12
|
}
|
|
@@ -6,6 +6,7 @@ import { signPdf } from "../../pdf/signature.js";
|
|
|
6
6
|
import { renderStyledPdf, renderStyledPdfEncrypted } from "../../pdf/styled-page-renderer.js";
|
|
7
7
|
import "../../pdf/index.js";
|
|
8
8
|
import { flowRenderOptions } from "./project.js";
|
|
9
|
+
import { writeDocx } from "../../word/docx-writer.js";
|
|
9
10
|
import { writeHtml } from "../../html/html-writer.js";
|
|
10
11
|
import { writeSvg } from "../../svg/svg-writer.js";
|
|
11
12
|
import { resolveDocxAutoFonts } from "../../word/docx-to-pdf.js";
|
|
@@ -46,6 +47,15 @@ var Ream = class Ream {
|
|
|
46
47
|
losses
|
|
47
48
|
};
|
|
48
49
|
}
|
|
50
|
+
if (to === "docx") {
|
|
51
|
+
const docx = writeDocx(this.flow);
|
|
52
|
+
losses.push(...docx.losses);
|
|
53
|
+
this.enforceStrict(options, losses);
|
|
54
|
+
return {
|
|
55
|
+
bytes: docx.bytes,
|
|
56
|
+
losses
|
|
57
|
+
};
|
|
58
|
+
}
|
|
49
59
|
const { fonts, registriesByFamily } = await this.resolveFonts(options, losses);
|
|
50
60
|
const registry = FontRegistry.fromBytes(fonts);
|
|
51
61
|
if (to === "svg") {
|
|
@@ -4,3 +4,5 @@ export { REL_OFFICE_DOCUMENT, REL_CORE_PROPERTIES, REL_EXTENDED_PROPERTIES, } fr
|
|
|
4
4
|
export { parseCoreProperties } from './core-properties.js';
|
|
5
5
|
export type { CoreProperties } from './core-properties.js';
|
|
6
6
|
export { isOoxmlRel } from './relationship-types.js';
|
|
7
|
+
export { buildOpcPackage, relsPathFor } from './opc-writer.js';
|
|
8
|
+
export type { OpcPart, OpcPartRelationships, OpcWriteOptions } from './opc-writer.js';
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { Relationship } from './relationships.js';
|
|
2
|
+
export interface OpcPart {
|
|
3
|
+
/** Package path, e.g. 'word/document.xml'. No leading slash. */
|
|
4
|
+
readonly path: string;
|
|
5
|
+
readonly data: Uint8Array;
|
|
6
|
+
/** Content type for the [Content_Types].xml Override (XML parts). */
|
|
7
|
+
readonly contentType: string;
|
|
8
|
+
}
|
|
9
|
+
export interface OpcPartRelationships {
|
|
10
|
+
/** The owning part, e.g. 'word/document.xml' (or '' for the package root). */
|
|
11
|
+
readonly sourcePart: string;
|
|
12
|
+
readonly relationships: ReadonlyArray<Relationship>;
|
|
13
|
+
}
|
|
14
|
+
export interface OpcWriteOptions {
|
|
15
|
+
readonly parts: ReadonlyArray<OpcPart>;
|
|
16
|
+
/** Package-level relationships (_rels/.rels). */
|
|
17
|
+
readonly rootRelationships: ReadonlyArray<Relationship>;
|
|
18
|
+
/** Per-part relationships (word/_rels/<part>.rels). */
|
|
19
|
+
readonly partRelationships?: ReadonlyArray<OpcPartRelationships>;
|
|
20
|
+
/**
|
|
21
|
+
* Default content types by lowercase extension, for parts not given an
|
|
22
|
+
* Override (typically binaries: png, jpeg…). `rels` and `xml` are always
|
|
23
|
+
* present and need not be repeated.
|
|
24
|
+
*/
|
|
25
|
+
readonly defaultsByExtension?: Readonly<Record<string, string>>;
|
|
26
|
+
}
|
|
27
|
+
export declare function buildOpcPackage(options: OpcWriteOptions): Uint8Array;
|
|
28
|
+
export declare function relsPathFor(partPath: string): string;
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { zipSync } from "fflate";
|
|
2
|
+
//#region src/core/opc/opc-writer.ts
|
|
3
|
+
var encoder = new TextEncoder();
|
|
4
|
+
var RELS_NS = "http://schemas.openxmlformats.org/package/2006/relationships";
|
|
5
|
+
var CT_NS = "http://schemas.openxmlformats.org/package/2006/content-types";
|
|
6
|
+
var FIXED_MTIME = Date.UTC(1980, 0, 1, 0, 0, 0);
|
|
7
|
+
function buildOpcPackage(options) {
|
|
8
|
+
const entries = {};
|
|
9
|
+
const add = (path, data) => {
|
|
10
|
+
entries[path] = [data, { mtime: FIXED_MTIME }];
|
|
11
|
+
};
|
|
12
|
+
add("[Content_Types].xml", encoder.encode(contentTypesXml(options)));
|
|
13
|
+
add("_rels/.rels", encoder.encode(relationshipsXml(options.rootRelationships)));
|
|
14
|
+
for (const part of options.parts) add(part.path, part.data);
|
|
15
|
+
for (const pr of options.partRelationships ?? []) {
|
|
16
|
+
if (pr.relationships.length === 0) continue;
|
|
17
|
+
add(relsPathFor(pr.sourcePart), encoder.encode(relationshipsXml(pr.relationships)));
|
|
18
|
+
}
|
|
19
|
+
return zipSync(entries);
|
|
20
|
+
}
|
|
21
|
+
function relsPathFor(partPath) {
|
|
22
|
+
const slash = partPath.lastIndexOf("/");
|
|
23
|
+
const dir = slash >= 0 ? partPath.slice(0, slash) : "";
|
|
24
|
+
const name = slash >= 0 ? partPath.slice(slash + 1) : partPath;
|
|
25
|
+
return dir ? `${dir}/_rels/${name}.rels` : `_rels/${name}.rels`;
|
|
26
|
+
}
|
|
27
|
+
function contentTypesXml(options) {
|
|
28
|
+
const defaults = {
|
|
29
|
+
rels: "application/vnd.openxmlformats-package.relationships+xml",
|
|
30
|
+
xml: "application/xml",
|
|
31
|
+
...options.defaultsByExtension ?? {}
|
|
32
|
+
};
|
|
33
|
+
const lines = ["<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"yes\"?>", `<Types xmlns="${CT_NS}">`];
|
|
34
|
+
for (const ext of Object.keys(defaults).sort()) lines.push(`<Default Extension="${ext}" ContentType="${escapeAttr(defaults[ext])}"/>`);
|
|
35
|
+
for (const part of options.parts) {
|
|
36
|
+
const ext = extensionOf(part.path);
|
|
37
|
+
if (ext !== void 0 && defaults[ext] === part.contentType) continue;
|
|
38
|
+
lines.push(`<Override PartName="/${escapeAttr(part.path)}" ContentType="${escapeAttr(part.contentType)}"/>`);
|
|
39
|
+
}
|
|
40
|
+
lines.push("</Types>");
|
|
41
|
+
return lines.join("");
|
|
42
|
+
}
|
|
43
|
+
function relationshipsXml(rels) {
|
|
44
|
+
const lines = ["<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"yes\"?>", `<Relationships xmlns="${RELS_NS}">`];
|
|
45
|
+
for (const r of rels) {
|
|
46
|
+
const mode = r.targetMode === "External" ? " TargetMode=\"External\"" : "";
|
|
47
|
+
lines.push(`<Relationship Id="${escapeAttr(r.id)}" Type="${escapeAttr(r.type)}" Target="${escapeAttr(r.target)}"${mode}/>`);
|
|
48
|
+
}
|
|
49
|
+
lines.push("</Relationships>");
|
|
50
|
+
return lines.join("");
|
|
51
|
+
}
|
|
52
|
+
function extensionOf(path) {
|
|
53
|
+
const dot = path.lastIndexOf(".");
|
|
54
|
+
return dot >= 0 ? path.slice(dot + 1).toLowerCase() : void 0;
|
|
55
|
+
}
|
|
56
|
+
function escapeAttr(s) {
|
|
57
|
+
return s.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """);
|
|
58
|
+
}
|
|
59
|
+
//#endregion
|
|
60
|
+
export { buildOpcPackage, relsPathFor };
|
package/dist/esm/index.d.ts
CHANGED
|
@@ -25,5 +25,6 @@ export type { FontProvider, FontRequest, FontAnswer } from './core/fonts/provide
|
|
|
25
25
|
export { svgWriter, writeSvg } from './svg/svg-writer.js';
|
|
26
26
|
export type { SvgWriteOptions } from './svg/svg-writer.js';
|
|
27
27
|
export { htmlWriter, writeHtml } from './html/html-writer.js';
|
|
28
|
+
export { docxWriter, writeDocx } from './word/docx-writer.js';
|
|
28
29
|
export { layoutStyledDocument } from './layout/styled-layout.js';
|
|
29
30
|
export type { LaidOutDocument } from './layout/page-doc.js';
|
package/dist/esm/index.js
CHANGED
|
@@ -15,8 +15,9 @@ import { createLanguageHyphenator, getHyphenator } from "./core/hyphenation/inde
|
|
|
15
15
|
import { docxReader, readDocx } from "./word/docx-reader.js";
|
|
16
16
|
import { readXlsx, xlsxReader } from "./excel/xlsx-reader.js";
|
|
17
17
|
import { NO_FONT, callerFontProvider, chainProviders, embeddedDocFontProvider, isEmbeddingRestricted, localFontProvider, readOs2FsType, remoteFontProvider } from "./core/fonts/provider.js";
|
|
18
|
+
import { docxWriter, writeDocx } from "./word/docx-writer.js";
|
|
18
19
|
import { htmlWriter, writeHtml } from "./html/html-writer.js";
|
|
19
20
|
import { svgWriter, writeSvg } from "./svg/svg-writer.js";
|
|
20
21
|
import { createConverter } from "./core/converter/facade.js";
|
|
21
22
|
import { Ream } from "./core/converter/ream.js";
|
|
22
|
-
export { ConversionLossError, FEATURES, FontRegistry, NO_FONT, Ream, ResourceStore, callerFontProvider, chainProviders, createConverter, createHyphenator, createLanguageHyphenator, docxReader, eighthPtToPt, embeddedDocFontProvider, emuToPt, fetchFontSet, formatLoss, getHyphenator, halfPtToPt, htmlWriter, inchToPt, isEmbeddingRestricted, layoutStyledDocument, localFontProvider, mmToPt, parseTtf, pt, pxToPt, readDocx, readOs2FsType, readXlsx, remoteFontProvider, renderStyledPdf, resolveFamilyKey, signPdf, splitPatternBundle, subsetTtf, svgWriter, twipsToPt, writeHtml, writeSvg, xlsxReader };
|
|
23
|
+
export { ConversionLossError, FEATURES, FontRegistry, NO_FONT, Ream, ResourceStore, callerFontProvider, chainProviders, createConverter, createHyphenator, createLanguageHyphenator, docxReader, docxWriter, eighthPtToPt, embeddedDocFontProvider, emuToPt, fetchFontSet, formatLoss, getHyphenator, halfPtToPt, htmlWriter, inchToPt, isEmbeddingRestricted, layoutStyledDocument, localFontProvider, mmToPt, parseTtf, pt, pxToPt, readDocx, readOs2FsType, readXlsx, remoteFontProvider, renderStyledPdf, resolveFamilyKey, signPdf, splitPatternBundle, subsetTtf, svgWriter, twipsToPt, writeDocx, writeHtml, writeSvg, xlsxReader };
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { twipsToPt } from "../core/ir/units.js";
|
|
2
|
-
import { poAttr, poChildren, poFindByPath, poIntAttr, poIs, poText } from "../core/po-helpers.js";
|
|
2
|
+
import { poAttr, poChildren, poFindByPath, poFindDescendant, poIntAttr, poIs, poText } from "../core/po-helpers.js";
|
|
3
3
|
import { defaultColorResolver } from "../core/drawingml/colors.js";
|
|
4
4
|
import { parseOMath } from "./omml-parser.js";
|
|
5
|
-
import { expandMcChildren, parseDrawing } from "./drawing-parser.js";
|
|
5
|
+
import { expandMcChildren, parseDrawing, parseVmlPicture } from "./drawing-parser.js";
|
|
6
6
|
import { parseRunProperties } from "./run-properties.js";
|
|
7
7
|
import { parseParagraphProperties } from "./paragraph-properties.js";
|
|
8
8
|
import { poElementToFlat } from "./po-to-flat.js";
|
|
@@ -178,20 +178,44 @@ function parseBodyElements(children, ctx = DEFAULT_PARSE_CONTEXT) {
|
|
|
178
178
|
}
|
|
179
179
|
return out;
|
|
180
180
|
}
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
181
|
+
var COLLAPSE_TRANSPARENT_TAGS = new Set([
|
|
182
|
+
"w:ins",
|
|
183
|
+
"w:moveTo",
|
|
184
|
+
"w:sdt",
|
|
185
|
+
"w:sdtContent",
|
|
186
|
+
"w:smartTag"
|
|
187
|
+
]);
|
|
188
|
+
function scanForLoneDrawing(container, acc) {
|
|
189
|
+
for (const child of expandMcChildren(poChildren(container))) {
|
|
190
|
+
if (poIs(child, "w:pPr")) continue;
|
|
191
|
+
if (poIs(child, "w:r")) {
|
|
192
|
+
for (const rc of expandMcChildren(poChildren(child))) if (poIs(rc, "w:drawing")) {
|
|
193
|
+
if (!acc.drawing) acc.drawing = rc;
|
|
194
|
+
} else if (poIs(rc, "w:pict") || poIs(rc, "w:object")) {
|
|
195
|
+
if (!acc.vml && poFindDescendant(rc, "v:imagedata") !== void 0) acc.vml = rc;
|
|
196
|
+
} else if (poIs(rc, "w:t") && poText(rc).length > 0) acc.hasOther = true;
|
|
197
|
+
else if (poIs(rc, "w:tab") || poIs(rc, "w:br") || poIs(rc, "w:noBreakHyphen")) acc.hasOther = true;
|
|
198
|
+
continue;
|
|
199
|
+
}
|
|
200
|
+
if (poIs(child, "m:oMath") || poIs(child, "m:oMathPara")) {
|
|
201
|
+
acc.hasOther = true;
|
|
202
|
+
continue;
|
|
203
|
+
}
|
|
204
|
+
const tag = elementTag(child);
|
|
205
|
+
if (tag && COLLAPSE_TRANSPARENT_TAGS.has(tag)) scanForLoneDrawing(child, acc);
|
|
206
|
+
else if (tag && RUN_CONTAINER_TAGS.has(tag)) acc.hasOther = true;
|
|
190
207
|
}
|
|
191
|
-
|
|
208
|
+
}
|
|
209
|
+
function tryExtractDrawingFromParagraph(p, ctx) {
|
|
210
|
+
const scan = { hasOther: false };
|
|
211
|
+
scanForLoneDrawing(p, scan);
|
|
212
|
+
const { drawing, vml } = scan;
|
|
213
|
+
if (scan.hasOther || !drawing && !vml) return null;
|
|
214
|
+
const fromVml = drawing === void 0;
|
|
192
215
|
const parseBody = (children) => parseBodyElements(children, ctx);
|
|
193
|
-
const content = parseDrawing(drawing, ctx.resolveColor, parseBody);
|
|
216
|
+
const content = fromVml ? parseVmlPicture(vml) : parseDrawing(drawing, ctx.resolveColor, parseBody);
|
|
194
217
|
if (!content) return null;
|
|
218
|
+
if (fromVml && content.kind === "image" && ctx.resolveImage?.(content.imageId) === void 0) return null;
|
|
195
219
|
const pPrNode = poChildren(p).find((c) => poIs(c, "w:pPr"));
|
|
196
220
|
const paragraphProperties = pPrNode ? parseParagraphProperties(poElementToFlat(pPrNode)) : {};
|
|
197
221
|
if (content.kind === "image") {
|
|
@@ -435,6 +459,16 @@ function parseRun(r, ctx) {
|
|
|
435
459
|
height: content.height
|
|
436
460
|
};
|
|
437
461
|
}
|
|
462
|
+
} else if (poIs(child, "w:pict") || poIs(child, "w:object")) {
|
|
463
|
+
const content = parseVmlPicture(child);
|
|
464
|
+
if (content && content.kind === "image") {
|
|
465
|
+
const resource = ctx.resolveImage?.(content.imageId);
|
|
466
|
+
if (resource) inlineImage = {
|
|
467
|
+
resource,
|
|
468
|
+
width: content.width,
|
|
469
|
+
height: content.height
|
|
470
|
+
};
|
|
471
|
+
}
|
|
438
472
|
}
|
|
439
473
|
}
|
|
440
474
|
return {
|
|
@@ -0,0 +1,602 @@
|
|
|
1
|
+
import { FEATURES } from "../core/ir/features.js";
|
|
2
|
+
import { detectImageFormat } from "../core/images.js";
|
|
3
|
+
import { EMPTY_STYLE_SHEET, resolveParagraphProperties, resolveRunProperties } from "../core/style-cascade/resolver.js";
|
|
4
|
+
import "../core/style-cascade/index.js";
|
|
5
|
+
import { buildOpcPackage } from "../core/opc/opc-writer.js";
|
|
6
|
+
import "../core/opc/index.js";
|
|
7
|
+
//#region src/word/docx-writer.ts
|
|
8
|
+
var encoder = new TextEncoder();
|
|
9
|
+
var DEFAULT_RUN = resolveRunProperties({}, {}, EMPTY_STYLE_SHEET);
|
|
10
|
+
var DEFAULT_PARA = resolveParagraphProperties({}, EMPTY_STYLE_SHEET);
|
|
11
|
+
var DOC_CONTENT_TYPE = "application/vnd.openxmlformats-officedocument.wordprocessingml.document.main+xml";
|
|
12
|
+
var NUMBERING_CONTENT_TYPE = "application/vnd.openxmlformats-officedocument.wordprocessingml.numbering+xml";
|
|
13
|
+
var REL_OFFICE_DOCUMENT = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/officeDocument";
|
|
14
|
+
var REL_NUMBERING = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/numbering";
|
|
15
|
+
var REL_HYPERLINK = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/hyperlink";
|
|
16
|
+
var REL_IMAGE = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/image";
|
|
17
|
+
var NUMBERING_PART = "word/numbering.xml";
|
|
18
|
+
var EMU_PER_PT = 12700;
|
|
19
|
+
var RASTER_MEDIA = {
|
|
20
|
+
png: {
|
|
21
|
+
ext: "png",
|
|
22
|
+
contentType: "image/png"
|
|
23
|
+
},
|
|
24
|
+
jpeg: {
|
|
25
|
+
ext: "jpeg",
|
|
26
|
+
contentType: "image/jpeg"
|
|
27
|
+
},
|
|
28
|
+
jpeg2000: {
|
|
29
|
+
ext: "jp2",
|
|
30
|
+
contentType: "image/jp2"
|
|
31
|
+
}
|
|
32
|
+
};
|
|
33
|
+
function mediaInfo(bytes) {
|
|
34
|
+
const raster = detectImageFormat(bytes);
|
|
35
|
+
if (raster) return RASTER_MEDIA[raster];
|
|
36
|
+
const b = (i) => bytes[i] ?? -1;
|
|
37
|
+
if (b(0) === 71 && b(1) === 73 && b(2) === 70 && b(3) === 56) return {
|
|
38
|
+
ext: "gif",
|
|
39
|
+
contentType: "image/gif"
|
|
40
|
+
};
|
|
41
|
+
if (b(0) === 66 && b(1) === 77) return {
|
|
42
|
+
ext: "bmp",
|
|
43
|
+
contentType: "image/bmp"
|
|
44
|
+
};
|
|
45
|
+
if (b(0) === 73 && b(1) === 73 && b(2) === 42 || b(0) === 77 && b(1) === 77) return {
|
|
46
|
+
ext: "tiff",
|
|
47
|
+
contentType: "image/tiff"
|
|
48
|
+
};
|
|
49
|
+
if (b(0) === 1 && b(1) === 0 && b(2) === 0 && b(3) === 0 && b(40) === 32 && b(41) === 69 && b(42) === 77 && b(43) === 70) return {
|
|
50
|
+
ext: "emf",
|
|
51
|
+
contentType: "image/x-emf"
|
|
52
|
+
};
|
|
53
|
+
if (b(0) === 215 && b(1) === 205 && b(2) === 198 && b(3) === 154 || b(0) === 1 && b(1) === 0 && b(2) === 9 && b(3) === 0) return {
|
|
54
|
+
ext: "wmf",
|
|
55
|
+
contentType: "image/x-wmf"
|
|
56
|
+
};
|
|
57
|
+
if (b(0) === 37 && b(1) === 80 && b(2) === 68 && b(3) === 70) return {
|
|
58
|
+
ext: "pdf",
|
|
59
|
+
contentType: "application/pdf"
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
var HEADER_CONTENT_TYPE = "application/vnd.openxmlformats-officedocument.wordprocessingml.header+xml";
|
|
63
|
+
var FOOTER_CONTENT_TYPE = "application/vnd.openxmlformats-officedocument.wordprocessingml.footer+xml";
|
|
64
|
+
var REL_HEADER = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/header";
|
|
65
|
+
var REL_FOOTER = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/footer";
|
|
66
|
+
function newScope() {
|
|
67
|
+
return {
|
|
68
|
+
rels: [],
|
|
69
|
+
relSeq: 0,
|
|
70
|
+
relIdByResource: /* @__PURE__ */ new Map()
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
function writeDocx(flow) {
|
|
74
|
+
const losses = [];
|
|
75
|
+
const body = [];
|
|
76
|
+
const state = {
|
|
77
|
+
resources: flow.resources,
|
|
78
|
+
mediaParts: [],
|
|
79
|
+
mediaFileByResource: /* @__PURE__ */ new Map(),
|
|
80
|
+
bookmarkSeq: 0,
|
|
81
|
+
drawingSeq: 0
|
|
82
|
+
};
|
|
83
|
+
const docScope = newScope();
|
|
84
|
+
const extraParts = [];
|
|
85
|
+
const extraPartRels = [];
|
|
86
|
+
const hfCache = /* @__PURE__ */ new Map();
|
|
87
|
+
const sections = flow.sections.length > 0 ? flow.sections : flow.section ? [{
|
|
88
|
+
properties: flow.section,
|
|
89
|
+
endIndex: flow.body.length
|
|
90
|
+
}] : [];
|
|
91
|
+
const sectPrByClosingIndex = /* @__PURE__ */ new Map();
|
|
92
|
+
let finalSectPr = "";
|
|
93
|
+
sections.forEach((sec, i) => {
|
|
94
|
+
const refs = emitHeadersFooters(flow, sec.properties, state, docScope, extraParts, extraPartRels, hfCache, losses);
|
|
95
|
+
const sp = sectPrXml(sec.properties, refs);
|
|
96
|
+
if (i === sections.length - 1) finalSectPr = sp;
|
|
97
|
+
else sectPrByClosingIndex.set(sec.endIndex - 1, sp);
|
|
98
|
+
});
|
|
99
|
+
flow.body.forEach((el, idx) => {
|
|
100
|
+
emitBlock(body, el, losses, state, docScope, sectPrByClosingIndex.get(idx));
|
|
101
|
+
});
|
|
102
|
+
if (finalSectPr) body.push(finalSectPr);
|
|
103
|
+
const documentXml = `<?xml version="1.0" encoding="UTF-8" standalone="yes"?><w:document xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main" xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships"><w:body>${body.join("")}</w:body></w:document>`;
|
|
104
|
+
const numberingPart = flow.body.some((el) => el.kind === "paragraph" && el.paragraph.properties.numbering !== void 0) && flow.numbering ? {
|
|
105
|
+
path: NUMBERING_PART,
|
|
106
|
+
data: encoder.encode(numberingXml(flow.numbering)),
|
|
107
|
+
contentType: NUMBERING_CONTENT_TYPE
|
|
108
|
+
} : void 0;
|
|
109
|
+
if (numberingPart) docScope.rels.push({
|
|
110
|
+
id: `rId${++docScope.relSeq}`,
|
|
111
|
+
type: REL_NUMBERING,
|
|
112
|
+
target: "numbering.xml",
|
|
113
|
+
targetMode: "Internal"
|
|
114
|
+
});
|
|
115
|
+
const partRelationships = [...docScope.rels.length > 0 ? [{
|
|
116
|
+
sourcePart: "word/document.xml",
|
|
117
|
+
relationships: docScope.rels
|
|
118
|
+
}] : [], ...extraPartRels];
|
|
119
|
+
return {
|
|
120
|
+
bytes: buildOpcPackage({
|
|
121
|
+
parts: [
|
|
122
|
+
{
|
|
123
|
+
path: "word/document.xml",
|
|
124
|
+
data: encoder.encode(documentXml),
|
|
125
|
+
contentType: DOC_CONTENT_TYPE
|
|
126
|
+
},
|
|
127
|
+
...numberingPart ? [numberingPart] : [],
|
|
128
|
+
...extraParts,
|
|
129
|
+
...state.mediaParts
|
|
130
|
+
],
|
|
131
|
+
rootRelationships: [{
|
|
132
|
+
id: "rId1",
|
|
133
|
+
type: REL_OFFICE_DOCUMENT,
|
|
134
|
+
target: "word/document.xml",
|
|
135
|
+
targetMode: "Internal"
|
|
136
|
+
}],
|
|
137
|
+
...partRelationships.length > 0 ? { partRelationships } : {}
|
|
138
|
+
}),
|
|
139
|
+
losses
|
|
140
|
+
};
|
|
141
|
+
}
|
|
142
|
+
function emitHeadersFooters(flow, section, state, docScope, extraParts, extraPartRels, hfCache, losses) {
|
|
143
|
+
const refs = {
|
|
144
|
+
headers: [],
|
|
145
|
+
footers: []
|
|
146
|
+
};
|
|
147
|
+
if (!section || !flow.headersFooters) return refs;
|
|
148
|
+
const emitOne = (kind, relationshipId, type) => {
|
|
149
|
+
const cached = hfCache.get(relationshipId);
|
|
150
|
+
if (cached !== void 0) return cached;
|
|
151
|
+
const content = flow.headersFooters.get(relationshipId);
|
|
152
|
+
if (!content) return void 0;
|
|
153
|
+
const n = extraParts.filter((p) => p.path.includes(`/${kind}`)).length + 1;
|
|
154
|
+
const path = `word/${kind}${n}.xml`;
|
|
155
|
+
const root = kind === "header" ? "w:hdr" : "w:ftr";
|
|
156
|
+
const scope = newScope();
|
|
157
|
+
const inner = [];
|
|
158
|
+
for (const el of content) emitBlock(inner, el, losses, state, scope);
|
|
159
|
+
const xml = `<?xml version="1.0" encoding="UTF-8" standalone="yes"?><${root} xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main" xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships">${inner.join("")}</${root}>`;
|
|
160
|
+
extraParts.push({
|
|
161
|
+
path,
|
|
162
|
+
data: encoder.encode(xml),
|
|
163
|
+
contentType: kind === "header" ? HEADER_CONTENT_TYPE : FOOTER_CONTENT_TYPE
|
|
164
|
+
});
|
|
165
|
+
if (scope.rels.length > 0) extraPartRels.push({
|
|
166
|
+
sourcePart: path,
|
|
167
|
+
relationships: scope.rels
|
|
168
|
+
});
|
|
169
|
+
const relId = `rId${++docScope.relSeq}`;
|
|
170
|
+
docScope.rels.push({
|
|
171
|
+
id: relId,
|
|
172
|
+
type: kind === "header" ? REL_HEADER : REL_FOOTER,
|
|
173
|
+
target: `${kind}${n}.xml`,
|
|
174
|
+
targetMode: "Internal"
|
|
175
|
+
});
|
|
176
|
+
hfCache.set(relationshipId, relId);
|
|
177
|
+
return relId;
|
|
178
|
+
};
|
|
179
|
+
for (const h of section.headers) {
|
|
180
|
+
const relId = emitOne("header", h.relationshipId, h.type);
|
|
181
|
+
if (relId) refs.headers.push({
|
|
182
|
+
type: h.type,
|
|
183
|
+
relId
|
|
184
|
+
});
|
|
185
|
+
}
|
|
186
|
+
for (const f of section.footers) {
|
|
187
|
+
const relId = emitOne("footer", f.relationshipId, f.type);
|
|
188
|
+
if (relId) refs.footers.push({
|
|
189
|
+
type: f.type,
|
|
190
|
+
relId
|
|
191
|
+
});
|
|
192
|
+
}
|
|
193
|
+
return refs;
|
|
194
|
+
}
|
|
195
|
+
var docxWriter = {
|
|
196
|
+
id: "docx",
|
|
197
|
+
consumes: "flow",
|
|
198
|
+
supports: new Set([FEATURES.text]),
|
|
199
|
+
write: (doc) => writeDocx(doc)
|
|
200
|
+
};
|
|
201
|
+
function emitBlock(out, el, losses, state, scope, closingSectPr) {
|
|
202
|
+
if (el.kind === "paragraph") {
|
|
203
|
+
out.push(paragraphXml(el.paragraph, state, scope, closingSectPr));
|
|
204
|
+
return;
|
|
205
|
+
}
|
|
206
|
+
if (el.kind === "table") {
|
|
207
|
+
out.push(tableXml(el.table, losses, state, scope));
|
|
208
|
+
if (closingSectPr) out.push(`<w:p><w:pPr>${closingSectPr}</w:pPr></w:p>`);
|
|
209
|
+
return;
|
|
210
|
+
}
|
|
211
|
+
if (el.kind === "image") {
|
|
212
|
+
const drawing = drawingXml(el.image.resource, el.image.width, el.image.height, el.image.altText, state, scope);
|
|
213
|
+
if (drawing) out.push(`<w:p>${pPrWithSect(el.image.paragraphProperties, closingSectPr)}<w:r>${drawing}</w:r></w:p>`);
|
|
214
|
+
else {
|
|
215
|
+
losses.push({
|
|
216
|
+
severity: "dropped",
|
|
217
|
+
feature: FEATURES.images,
|
|
218
|
+
detail: "image bytes missing"
|
|
219
|
+
});
|
|
220
|
+
if (closingSectPr) out.push(`<w:p><w:pPr>${closingSectPr}</w:pPr></w:p>`);
|
|
221
|
+
}
|
|
222
|
+
return;
|
|
223
|
+
}
|
|
224
|
+
if (el.kind === "shape") {
|
|
225
|
+
const drawing = shapeDrawingXml(el.shape, losses, state, scope);
|
|
226
|
+
out.push(`<w:p>${pPrWithSect(el.shape.paragraphProperties, closingSectPr)}<w:r>${drawing}</w:r></w:p>`);
|
|
227
|
+
return;
|
|
228
|
+
}
|
|
229
|
+
if (closingSectPr) out.push(`<w:p><w:pPr>${closingSectPr}</w:pPr></w:p>`);
|
|
230
|
+
losses.push({
|
|
231
|
+
severity: "dropped",
|
|
232
|
+
feature: FEATURES.charts,
|
|
233
|
+
detail: `${el.kind} not written by the docx writer (v0)`
|
|
234
|
+
});
|
|
235
|
+
}
|
|
236
|
+
function drawingXml(resource, widthPt, heightPt, altText, state, scope) {
|
|
237
|
+
if (resource === void 0) return "";
|
|
238
|
+
const relId = mediaRelId(resource, state, scope);
|
|
239
|
+
if (relId === void 0) return "";
|
|
240
|
+
const cx = Math.round(widthPt * EMU_PER_PT);
|
|
241
|
+
const cy = Math.round(heightPt * EMU_PER_PT);
|
|
242
|
+
const id = ++state.drawingSeq;
|
|
243
|
+
return `<w:drawing><wp:inline xmlns:wp="http://schemas.openxmlformats.org/drawingml/2006/wordprocessingDrawing"><wp:extent cx="${cx}" cy="${cy}"/><wp:docPr id="${id}" name="Image ${id}"${altText ? ` descr="${escapeAttr(altText)}"` : ""}/><a:graphic xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main"><a:graphicData uri="http://schemas.openxmlformats.org/drawingml/2006/picture"><pic:pic xmlns:pic="http://schemas.openxmlformats.org/drawingml/2006/picture"><pic:nvPicPr><pic:cNvPr id="${id}" name="Image ${id}"/><pic:cNvPicPr/></pic:nvPicPr><pic:blipFill><a:blip r:embed="${relId}"/><a:stretch><a:fillRect/></a:stretch></pic:blipFill><pic:spPr><a:xfrm><a:off x="0" y="0"/><a:ext cx="${cx}" cy="${cy}"/></a:xfrm><a:prstGeom prst="rect"><a:avLst/></a:prstGeom></pic:spPr></pic:pic></a:graphicData></a:graphic></wp:inline></w:drawing>`;
|
|
244
|
+
}
|
|
245
|
+
var WPS_URI = "http://schemas.microsoft.com/office/word/2010/wordprocessingShape";
|
|
246
|
+
function shapeDrawingXml(shape, losses, state, scope) {
|
|
247
|
+
const cx = Math.round(shape.width * EMU_PER_PT);
|
|
248
|
+
const cy = Math.round(shape.height * EMU_PER_PT);
|
|
249
|
+
const id = ++state.drawingSeq;
|
|
250
|
+
const descr = shape.altText ? ` descr="${escapeAttr(shape.altText)}"` : "";
|
|
251
|
+
const spPr = `<wps:spPr>${xfrmXml(shape.transform, cx, cy)}${geomXml(shape.geometry)}${fillXml(shape.fill)}${shape.line ? lineXml(shape.line) : ""}</wps:spPr>`;
|
|
252
|
+
const txbx = shape.text ? txbxXml(shape.text, losses, state, scope) : "";
|
|
253
|
+
return `<w:drawing><wp:inline xmlns:wp="http://schemas.openxmlformats.org/drawingml/2006/wordprocessingDrawing"><wp:extent cx="${cx}" cy="${cy}"/><wp:docPr id="${id}" name="Shape ${id}"${descr}/><a:graphic xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main"><a:graphicData uri="${WPS_URI}"><wps:wsp xmlns:wps="http://schemas.microsoft.com/office/word/2010/wordprocessingShape"><wps:cNvSpPr/>` + spPr + txbx + bodyPrXml(shape.text) + "</wps:wsp></a:graphicData></a:graphic></wp:inline></w:drawing>";
|
|
254
|
+
}
|
|
255
|
+
function xfrmXml(t, cx, cy) {
|
|
256
|
+
return `<a:xfrm${t?.rotation60k !== void 0 ? ` rot="${t.rotation60k}"` : ""}${t?.flipH ? " flipH=\"1\"" : ""}${t?.flipV ? " flipV=\"1\"" : ""}><a:off x="0" y="0"/><a:ext cx="${cx}" cy="${cy}"/></a:xfrm>`;
|
|
257
|
+
}
|
|
258
|
+
function geomXml(g) {
|
|
259
|
+
if (g.kind === "custom" && g.custom) {
|
|
260
|
+
const cmds = g.custom.commands.map((c) => {
|
|
261
|
+
switch (c.cmd) {
|
|
262
|
+
case "move": return `<a:moveTo><a:pt x="${c.x}" y="${c.y}"/></a:moveTo>`;
|
|
263
|
+
case "line": return `<a:lnTo><a:pt x="${c.x}" y="${c.y}"/></a:lnTo>`;
|
|
264
|
+
case "cubic": return `<a:cubicBezTo><a:pt x="${c.x1}" y="${c.y1}"/><a:pt x="${c.x2}" y="${c.y2}"/><a:pt x="${c.x}" y="${c.y}"/></a:cubicBezTo>`;
|
|
265
|
+
case "quad": return `<a:quadBezTo><a:pt x="${c.x1}" y="${c.y1}"/><a:pt x="${c.x}" y="${c.y}"/></a:quadBezTo>`;
|
|
266
|
+
case "arc": return `<a:arcTo wR="${c.wR}" hR="${c.hR}" stAng="${c.stAng}" swAng="${c.swAng}"/>`;
|
|
267
|
+
case "close": return "<a:close/>";
|
|
268
|
+
}
|
|
269
|
+
}).join("");
|
|
270
|
+
const { pathWidth: w, pathHeight: h } = g.custom;
|
|
271
|
+
return `<a:custGeom><a:avLst/><a:gdLst/><a:rect l="0" t="0" r="${w}" b="${h}"/><a:pathLst><a:path w="${w}" h="${h}">${cmds}</a:path></a:pathLst></a:custGeom>`;
|
|
272
|
+
}
|
|
273
|
+
const gds = g.adjust && g.adjust.size > 0 ? [...g.adjust].map(([n, v]) => `<a:gd name="${escapeAttr(n)}" fmla="val ${v}"/>`).join("") : "";
|
|
274
|
+
return `<a:prstGeom prst="${escapeAttr(g.preset ?? "rect")}"><a:avLst>${gds}</a:avLst></a:prstGeom>`;
|
|
275
|
+
}
|
|
276
|
+
function fillXml(f) {
|
|
277
|
+
return f.kind === "solid" && f.colorHex ? `<a:solidFill><a:srgbClr val="${f.colorHex}"/></a:solidFill>` : "<a:noFill/>";
|
|
278
|
+
}
|
|
279
|
+
function lineXml(l) {
|
|
280
|
+
const w = l.width !== void 0 ? ` w="${Math.round(l.width * EMU_PER_PT)}"` : "";
|
|
281
|
+
const cap = l.cap === "round" ? " cap=\"rnd\"" : l.cap === "square" ? " cap=\"sq\"" : l.cap === "flat" ? " cap=\"flat\"" : "";
|
|
282
|
+
const inner = [];
|
|
283
|
+
if (l.fill === "none") inner.push("<a:noFill/>");
|
|
284
|
+
else if (l.colorHex) inner.push(`<a:solidFill><a:srgbClr val="${l.colorHex}"/></a:solidFill>`);
|
|
285
|
+
if (l.dash) inner.push(`<a:prstDash val="${l.dash}"/>`);
|
|
286
|
+
return `<a:ln${w}${cap}>${inner.join("")}</a:ln>`;
|
|
287
|
+
}
|
|
288
|
+
function txbxXml(text, losses, state, scope) {
|
|
289
|
+
const inner = [];
|
|
290
|
+
for (const el of text.content) emitBlock(inner, el, losses, state, scope);
|
|
291
|
+
return `<wps:txbx><w:txbxContent>${inner.join("")}</w:txbxContent></wps:txbx>`;
|
|
292
|
+
}
|
|
293
|
+
function bodyPrXml(text) {
|
|
294
|
+
if (!text) return "<wps:bodyPr/>";
|
|
295
|
+
const ins = (v, name) => v !== void 0 ? ` ${name}="${Math.round(v * EMU_PER_PT)}"` : "";
|
|
296
|
+
const anchor = text.anchor ? ` anchor="${text.anchor}"` : "";
|
|
297
|
+
return "<wps:bodyPr" + ins(text.insetLeft, "lIns") + ins(text.insetTop, "tIns") + ins(text.insetRight, "rIns") + ins(text.insetBottom, "bIns") + anchor + "/>";
|
|
298
|
+
}
|
|
299
|
+
function mediaRelId(resource, state, scope) {
|
|
300
|
+
const existingRel = scope.relIdByResource.get(resource);
|
|
301
|
+
if (existingRel !== void 0) return existingRel;
|
|
302
|
+
let target = state.mediaFileByResource.get(resource);
|
|
303
|
+
if (target === void 0) {
|
|
304
|
+
const bytes = state.resources.get(resource);
|
|
305
|
+
if (!bytes) return void 0;
|
|
306
|
+
const info = mediaInfo(bytes);
|
|
307
|
+
if (!info) return void 0;
|
|
308
|
+
target = `media/image${state.mediaParts.length + 1}.${info.ext}`;
|
|
309
|
+
state.mediaParts.push({
|
|
310
|
+
path: `word/${target}`,
|
|
311
|
+
data: bytes,
|
|
312
|
+
contentType: info.contentType
|
|
313
|
+
});
|
|
314
|
+
state.mediaFileByResource.set(resource, target);
|
|
315
|
+
}
|
|
316
|
+
const relId = `rId${++scope.relSeq}`;
|
|
317
|
+
scope.rels.push({
|
|
318
|
+
id: relId,
|
|
319
|
+
type: REL_IMAGE,
|
|
320
|
+
target,
|
|
321
|
+
targetMode: "Internal"
|
|
322
|
+
});
|
|
323
|
+
scope.relIdByResource.set(resource, relId);
|
|
324
|
+
return relId;
|
|
325
|
+
}
|
|
326
|
+
function tableXml(table, losses, state, scope) {
|
|
327
|
+
const grid = table.grid.map((w) => `<w:gridCol w:w="${twips(w)}"/>`).join("");
|
|
328
|
+
const rows = table.rows.map((row) => rowXml(row, losses, state, scope)).join("");
|
|
329
|
+
return `<w:tbl>${tblPrXml(table.properties)}<w:tblGrid>${grid}</w:tblGrid>${rows}</w:tbl>`;
|
|
330
|
+
}
|
|
331
|
+
function tblPrXml(p) {
|
|
332
|
+
const out = [];
|
|
333
|
+
if (p.widthType !== void 0) {
|
|
334
|
+
const w = p.widthType === "pct" ? Math.round((p.widthFraction ?? 0) * 5e3) : p.widthPt !== void 0 ? twips(p.widthPt) : 0;
|
|
335
|
+
out.push(`<w:tblW w:w="${w}" w:type="${p.widthType}"/>`);
|
|
336
|
+
}
|
|
337
|
+
if (p.alignment && p.alignment !== "left") out.push(`<w:jc w:val="${p.alignment}"/>`);
|
|
338
|
+
const borders = bordersXml("w:tblBorders", p.borders);
|
|
339
|
+
if (borders) out.push(borders);
|
|
340
|
+
const margins = cellMarginsXml("w:tblCellMar", p.defaultCellMargins);
|
|
341
|
+
if (margins) out.push(margins);
|
|
342
|
+
return out.length > 0 ? `<w:tblPr>${out.join("")}</w:tblPr>` : "";
|
|
343
|
+
}
|
|
344
|
+
function rowXml(row, losses, state, scope) {
|
|
345
|
+
const trPr = [];
|
|
346
|
+
if (row.properties.height !== void 0) {
|
|
347
|
+
const rule = row.properties.heightRule ?? "atLeast";
|
|
348
|
+
trPr.push(`<w:trHeight w:val="${twips(row.properties.height)}" w:hRule="${rule}"/>`);
|
|
349
|
+
}
|
|
350
|
+
if (row.properties.isHeader) trPr.push("<w:tblHeader/>");
|
|
351
|
+
if (row.properties.cantSplit) trPr.push("<w:cantSplit/>");
|
|
352
|
+
return `<w:tr>${trPr.length > 0 ? `<w:trPr>${trPr.join("")}</w:trPr>` : ""}${row.cells.map((cell) => cellXml(cell, losses, state, scope)).join("")}</w:tr>`;
|
|
353
|
+
}
|
|
354
|
+
function cellXml(cell, losses, state, scope) {
|
|
355
|
+
const content = [];
|
|
356
|
+
for (const child of cell.content) emitBlock(content, child, losses, state, scope);
|
|
357
|
+
if (content.length === 0) content.push("<w:p/>");
|
|
358
|
+
return `<w:tc>${tcPrXml(cell.properties)}${content.join("")}</w:tc>`;
|
|
359
|
+
}
|
|
360
|
+
function tcPrXml(p) {
|
|
361
|
+
const out = [];
|
|
362
|
+
if (p.width !== void 0) out.push(`<w:tcW w:w="${twips(p.width)}" w:type="dxa"/>`);
|
|
363
|
+
if (p.colSpan !== void 0 && p.colSpan > 1) out.push(`<w:gridSpan w:val="${p.colSpan}"/>`);
|
|
364
|
+
if (p.merge !== void 0) out.push(p.merge === "start" ? "<w:vMerge w:val=\"restart\"/>" : "<w:vMerge w:val=\"continue\"/>");
|
|
365
|
+
const borders = bordersXml("w:tcBorders", p.borders);
|
|
366
|
+
if (borders) out.push(borders);
|
|
367
|
+
const margins = cellMarginsXml("w:tcMar", p.margins);
|
|
368
|
+
if (margins) out.push(margins);
|
|
369
|
+
if (p.shading) out.push(`<w:shd w:val="clear" w:color="auto" w:fill="${p.shading.colorHex}"/>`);
|
|
370
|
+
return out.length > 0 ? `<w:tcPr>${out.join("")}</w:tcPr>` : "";
|
|
371
|
+
}
|
|
372
|
+
var BORDER_SIDES = [
|
|
373
|
+
["top", "w:top"],
|
|
374
|
+
["left", "w:left"],
|
|
375
|
+
["bottom", "w:bottom"],
|
|
376
|
+
["right", "w:right"],
|
|
377
|
+
["insideH", "w:insideH"],
|
|
378
|
+
["insideV", "w:insideV"]
|
|
379
|
+
];
|
|
380
|
+
function bordersXml(tag, borders) {
|
|
381
|
+
if (!borders) return "";
|
|
382
|
+
const sides = BORDER_SIDES.map(([key, el]) => {
|
|
383
|
+
const b = borders[key];
|
|
384
|
+
if (!b) return "";
|
|
385
|
+
const sz = b.width !== void 0 ? ` w:sz="${Math.round(b.width * 8)}"` : "";
|
|
386
|
+
const color = b.colorHex !== void 0 ? ` w:color="${b.colorHex}"` : "";
|
|
387
|
+
return `<${el} w:val="${b.style}"${sz}${color}/>`;
|
|
388
|
+
}).join("");
|
|
389
|
+
return sides ? `<${tag}>${sides}</${tag}>` : "";
|
|
390
|
+
}
|
|
391
|
+
function cellMarginsXml(tag, margins) {
|
|
392
|
+
if (!margins) return "";
|
|
393
|
+
const inner = [
|
|
394
|
+
["top", "w:top"],
|
|
395
|
+
["left", "w:left"],
|
|
396
|
+
["bottom", "w:bottom"],
|
|
397
|
+
["right", "w:right"]
|
|
398
|
+
].map(([key, el]) => {
|
|
399
|
+
const v = margins[key];
|
|
400
|
+
return v !== void 0 ? `<${el} w:w="${twips(v)}" w:type="dxa"/>` : "";
|
|
401
|
+
}).join("");
|
|
402
|
+
return inner ? `<${tag}>${inner}</${tag}>` : "";
|
|
403
|
+
}
|
|
404
|
+
function paragraphXml(p, state, scope, closingSectPr) {
|
|
405
|
+
const visible = p.runs.filter((run) => !run.listMarker && run.math === void 0 && (run.text !== "" || run.inlineImage !== void 0 || run.pageBreak || run.href !== void 0 || run.anchor !== void 0));
|
|
406
|
+
const inner = [];
|
|
407
|
+
let i = 0;
|
|
408
|
+
while (i < visible.length) {
|
|
409
|
+
const run = visible[i];
|
|
410
|
+
const key = run.href ?? run.anchor;
|
|
411
|
+
if (key === void 0) {
|
|
412
|
+
inner.push(runXml(run, state, scope));
|
|
413
|
+
i++;
|
|
414
|
+
continue;
|
|
415
|
+
}
|
|
416
|
+
let j = i + 1;
|
|
417
|
+
while (j < visible.length && (visible[j].href ?? visible[j].anchor) === key) j++;
|
|
418
|
+
const group = visible.slice(i, j).map((r) => runXml(r, state, scope)).join("");
|
|
419
|
+
inner.push(hyperlinkXml(run, group === "" ? "<w:r/>" : group, state, scope));
|
|
420
|
+
i = j;
|
|
421
|
+
}
|
|
422
|
+
const bookmarks = (p.bookmarks ?? []).map((name) => {
|
|
423
|
+
const id = state.bookmarkSeq++;
|
|
424
|
+
return `<w:bookmarkStart w:id="${id}" w:name="${escapeAttr(name)}"/><w:bookmarkEnd w:id="${id}"/>`;
|
|
425
|
+
}).join("");
|
|
426
|
+
const pPrInner = pPrBody(p.properties) + (closingSectPr ?? "");
|
|
427
|
+
return `<w:p>${pPrInner !== "" ? `<w:pPr>${pPrInner}</w:pPr>` : ""}${bookmarks}${inner.join("")}</w:p>`;
|
|
428
|
+
}
|
|
429
|
+
function hyperlinkXml(run, inner, _state, scope) {
|
|
430
|
+
if (run.href !== void 0) {
|
|
431
|
+
const id = `rId${++scope.relSeq}`;
|
|
432
|
+
scope.rels.push({
|
|
433
|
+
id,
|
|
434
|
+
type: REL_HYPERLINK,
|
|
435
|
+
target: run.href,
|
|
436
|
+
targetMode: "External"
|
|
437
|
+
});
|
|
438
|
+
return `<w:hyperlink r:id="${id}">${inner}</w:hyperlink>`;
|
|
439
|
+
}
|
|
440
|
+
return `<w:hyperlink w:anchor="${escapeAttr(run.anchor)}">${inner}</w:hyperlink>`;
|
|
441
|
+
}
|
|
442
|
+
function runXml(run, state, scope) {
|
|
443
|
+
const rPr = rPrXml(run.properties);
|
|
444
|
+
if (run.inlineImage !== void 0) {
|
|
445
|
+
const img = run.inlineImage;
|
|
446
|
+
const drawing = drawingXml(img.resource, img.width, img.height, void 0, state, scope);
|
|
447
|
+
if (drawing) return `<w:r>${rPr}${drawing}</w:r>`;
|
|
448
|
+
if (run.text === "" && !run.pageBreak) return "";
|
|
449
|
+
}
|
|
450
|
+
const brk = run.pageBreak ? "<w:br w:type=\"page\"/>" : "";
|
|
451
|
+
if (run.text === "") return brk ? `<w:r>${rPr}${brk}</w:r>` : "";
|
|
452
|
+
return `<w:r>${rPr}<w:t xml:space="preserve">${escapeXml(run.text)}</w:t>${brk}</w:r>`;
|
|
453
|
+
}
|
|
454
|
+
function rPrXml(r) {
|
|
455
|
+
const out = [];
|
|
456
|
+
if (r.bold !== DEFAULT_RUN.bold) out.push(toggle("w:b", r.bold));
|
|
457
|
+
if (r.italic !== DEFAULT_RUN.italic) out.push(toggle("w:i", r.italic));
|
|
458
|
+
if (r.strike !== DEFAULT_RUN.strike) out.push(toggle("w:strike", r.strike));
|
|
459
|
+
if (r.underline !== DEFAULT_RUN.underline) out.push(`<w:u w:val="${r.underline}"/>`);
|
|
460
|
+
const fonts = rFontsXml(r.fontFamily);
|
|
461
|
+
if (fonts) out.push(fonts);
|
|
462
|
+
if (r.fontSizePt !== DEFAULT_RUN.fontSizePt) out.push(`<w:sz w:val="${Math.round(r.fontSizePt * 2)}"/>`);
|
|
463
|
+
if (r.colorHex !== DEFAULT_RUN.colorHex) out.push(`<w:color w:val="${r.colorHex}"/>`);
|
|
464
|
+
if (r.verticalAlign !== DEFAULT_RUN.verticalAlign) out.push(`<w:vertAlign w:val="${r.verticalAlign}"/>`);
|
|
465
|
+
if (r.rtl !== DEFAULT_RUN.rtl) out.push(toggle("w:rtl", r.rtl));
|
|
466
|
+
if (r.lang !== void 0) out.push(`<w:lang w:val="${escapeAttr(r.lang)}"/>`);
|
|
467
|
+
return out.length > 0 ? `<w:rPr>${out.join("")}</w:rPr>` : "";
|
|
468
|
+
}
|
|
469
|
+
function pPrBody(p) {
|
|
470
|
+
const out = [];
|
|
471
|
+
if (p.numbering) out.push(`<w:numPr><w:ilvl w:val="${p.numbering.ilvl}"/><w:numId w:val="${escapeAttr(p.numbering.numId)}"/></w:numPr>`);
|
|
472
|
+
if (p.outlineLevel !== void 0) out.push(`<w:outlineLvl w:val="${p.outlineLevel}"/>`);
|
|
473
|
+
if (p.pageBreakBefore) out.push("<w:pageBreakBefore/>");
|
|
474
|
+
if (p.bidi !== DEFAULT_PARA.bidi) out.push(toggle("w:bidi", p.bidi));
|
|
475
|
+
const ind = indXml(p);
|
|
476
|
+
if (ind) out.push(ind);
|
|
477
|
+
const spacing = spacingXml(p);
|
|
478
|
+
if (spacing) out.push(spacing);
|
|
479
|
+
if (p.alignment !== DEFAULT_PARA.alignment) out.push(`<w:jc w:val="${p.alignment}"/>`);
|
|
480
|
+
return out.join("");
|
|
481
|
+
}
|
|
482
|
+
function pPrWithSect(p, closingSectPr) {
|
|
483
|
+
const inner = pPrBody(p) + (closingSectPr ?? "");
|
|
484
|
+
return inner !== "" ? `<w:pPr>${inner}</w:pPr>` : "";
|
|
485
|
+
}
|
|
486
|
+
function indXml(p) {
|
|
487
|
+
const attrs = [];
|
|
488
|
+
if (p.indentLeft !== DEFAULT_PARA.indentLeft) attrs.push(`w:left="${twips(p.indentLeft)}"`);
|
|
489
|
+
if (p.indentRight !== DEFAULT_PARA.indentRight) attrs.push(`w:right="${twips(p.indentRight)}"`);
|
|
490
|
+
if (p.indentFirstLine !== DEFAULT_PARA.indentFirstLine) if (p.indentFirstLine < 0) attrs.push(`w:hanging="${twips(-p.indentFirstLine)}"`);
|
|
491
|
+
else attrs.push(`w:firstLine="${twips(p.indentFirstLine)}"`);
|
|
492
|
+
return attrs.length > 0 ? `<w:ind ${attrs.join(" ")}/>` : "";
|
|
493
|
+
}
|
|
494
|
+
function spacingXml(p) {
|
|
495
|
+
const attrs = [];
|
|
496
|
+
if (p.spacingBefore !== DEFAULT_PARA.spacingBefore) attrs.push(`w:before="${twips(p.spacingBefore)}"`);
|
|
497
|
+
if (p.spacingAfter !== DEFAULT_PARA.spacingAfter) attrs.push(`w:after="${twips(p.spacingAfter)}"`);
|
|
498
|
+
if ((p.spacingLineRule !== DEFAULT_PARA.spacingLineRule || p.spacingLine !== DEFAULT_PARA.spacingLine) && p.spacingLine > 0) {
|
|
499
|
+
const lineVal = p.spacingLineRule === "auto" ? Math.round(p.spacingLine * 12) : twips(p.spacingLine);
|
|
500
|
+
attrs.push(`w:line="${lineVal}"`, `w:lineRule="${p.spacingLineRule}"`);
|
|
501
|
+
}
|
|
502
|
+
return attrs.length > 0 ? `<w:spacing ${attrs.join(" ")}/>` : "";
|
|
503
|
+
}
|
|
504
|
+
function rFontsXml(fonts) {
|
|
505
|
+
const d = DEFAULT_RUN.fontFamily;
|
|
506
|
+
const attrs = [];
|
|
507
|
+
if (fonts.ascii && fonts.ascii !== d.ascii) attrs.push(`w:ascii="${escapeAttr(fonts.ascii)}"`);
|
|
508
|
+
if (fonts.hAnsi && fonts.hAnsi !== d.hAnsi) attrs.push(`w:hAnsi="${escapeAttr(fonts.hAnsi)}"`);
|
|
509
|
+
if (fonts.cs && fonts.cs !== d.cs) attrs.push(`w:cs="${escapeAttr(fonts.cs)}"`);
|
|
510
|
+
return attrs.length > 0 ? `<w:rFonts ${attrs.join(" ")}/>` : "";
|
|
511
|
+
}
|
|
512
|
+
function toggle(tag, on) {
|
|
513
|
+
return on ? `<${tag}/>` : `<${tag} w:val="false"/>`;
|
|
514
|
+
}
|
|
515
|
+
function numberingXml(numbering) {
|
|
516
|
+
const out = ["<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"yes\"?>", "<w:numbering xmlns:w=\"http://schemas.openxmlformats.org/wordprocessingml/2006/main\">"];
|
|
517
|
+
for (const abstractNum of numbering.abstractNums.values()) {
|
|
518
|
+
out.push(`<w:abstractNum w:abstractNumId="${escapeAttr(abstractNum.id)}">`);
|
|
519
|
+
for (const level of [...abstractNum.levels.values()].sort((a, b) => a.ilvl - b.ilvl)) out.push(levelXml(level));
|
|
520
|
+
out.push("</w:abstractNum>");
|
|
521
|
+
}
|
|
522
|
+
for (const inst of numbering.numInstances.values()) out.push(`<w:num w:numId="${escapeAttr(inst.numId)}"><w:abstractNumId w:val="${escapeAttr(inst.abstractNumId)}"/></w:num>`);
|
|
523
|
+
out.push("</w:numbering>");
|
|
524
|
+
return out.join("");
|
|
525
|
+
}
|
|
526
|
+
function levelXml(level) {
|
|
527
|
+
const inner = [
|
|
528
|
+
`<w:start w:val="${level.start}"/>`,
|
|
529
|
+
`<w:numFmt w:val="${level.format}"/>`,
|
|
530
|
+
`<w:lvlText w:val="${escapeAttr(level.lvlText)}"/>`
|
|
531
|
+
];
|
|
532
|
+
const pPr = rawParaPrXml(level.paragraphProperties);
|
|
533
|
+
if (pPr) inner.push(pPr);
|
|
534
|
+
const rPr = rawRunPrXml(level.runProperties);
|
|
535
|
+
if (rPr) inner.push(rPr);
|
|
536
|
+
return `<w:lvl w:ilvl="${level.ilvl}">${inner.join("")}</w:lvl>`;
|
|
537
|
+
}
|
|
538
|
+
function rawParaPrXml(p) {
|
|
539
|
+
const attrs = [];
|
|
540
|
+
if (p.indentLeft !== void 0) attrs.push(`w:left="${twips(p.indentLeft)}"`);
|
|
541
|
+
if (p.indentRight !== void 0) attrs.push(`w:right="${twips(p.indentRight)}"`);
|
|
542
|
+
if (p.indentFirstLine !== void 0) if (p.indentFirstLine < 0) attrs.push(`w:hanging="${twips(-p.indentFirstLine)}"`);
|
|
543
|
+
else attrs.push(`w:firstLine="${twips(p.indentFirstLine)}"`);
|
|
544
|
+
const ind = attrs.length > 0 ? `<w:ind ${attrs.join(" ")}/>` : "";
|
|
545
|
+
const jc = p.alignment !== void 0 ? `<w:jc w:val="${p.alignment}"/>` : "";
|
|
546
|
+
return ind || jc ? `<w:pPr>${jc}${ind}</w:pPr>` : "";
|
|
547
|
+
}
|
|
548
|
+
function rawRunPrXml(r) {
|
|
549
|
+
const out = [];
|
|
550
|
+
if (r.bold !== void 0) out.push(toggle("w:b", r.bold));
|
|
551
|
+
if (r.italic !== void 0) out.push(toggle("w:i", r.italic));
|
|
552
|
+
if (r.strike !== void 0) out.push(toggle("w:strike", r.strike));
|
|
553
|
+
if (r.underline !== void 0) out.push(`<w:u w:val="${r.underline}"/>`);
|
|
554
|
+
const fonts = r.fontFamily ? rawRFontsXml(r.fontFamily) : "";
|
|
555
|
+
if (fonts) out.push(fonts);
|
|
556
|
+
if (r.fontSizePt !== void 0) out.push(`<w:sz w:val="${Math.round(r.fontSizePt * 2)}"/>`);
|
|
557
|
+
if (r.colorHex !== void 0) out.push(`<w:color w:val="${r.colorHex}"/>`);
|
|
558
|
+
if (r.verticalAlign !== void 0) out.push(`<w:vertAlign w:val="${r.verticalAlign}"/>`);
|
|
559
|
+
return out.length > 0 ? `<w:rPr>${out.join("")}</w:rPr>` : "";
|
|
560
|
+
}
|
|
561
|
+
function rawRFontsXml(fonts) {
|
|
562
|
+
const attrs = [];
|
|
563
|
+
if (fonts.ascii) attrs.push(`w:ascii="${escapeAttr(fonts.ascii)}"`);
|
|
564
|
+
if (fonts.hAnsi) attrs.push(`w:hAnsi="${escapeAttr(fonts.hAnsi)}"`);
|
|
565
|
+
if (fonts.cs) attrs.push(`w:cs="${escapeAttr(fonts.cs)}"`);
|
|
566
|
+
return attrs.length > 0 ? `<w:rFonts ${attrs.join(" ")}/>` : "";
|
|
567
|
+
}
|
|
568
|
+
function sectPrXml(s, hf) {
|
|
569
|
+
const parts = [];
|
|
570
|
+
for (const h of hf.headers) parts.push(`<w:headerReference w:type="${h.type}" r:id="${h.relId}"/>`);
|
|
571
|
+
for (const f of hf.footers) parts.push(`<w:footerReference w:type="${f.type}" r:id="${f.relId}"/>`);
|
|
572
|
+
if (s.pageSize) {
|
|
573
|
+
const orient = s.pageSize.orientation === "landscape" ? " w:orient=\"landscape\"" : "";
|
|
574
|
+
parts.push(`<w:pgSz w:w="${twips(s.pageSize.width)}" w:h="${twips(s.pageSize.height)}"${orient}/>`);
|
|
575
|
+
}
|
|
576
|
+
if (s.margins) {
|
|
577
|
+
const m = s.margins;
|
|
578
|
+
const header = m.header !== void 0 ? ` w:header="${twips(m.header)}"` : "";
|
|
579
|
+
const footer = m.footer !== void 0 ? ` w:footer="${twips(m.footer)}"` : "";
|
|
580
|
+
parts.push(`<w:pgMar w:top="${twips(m.top)}" w:right="${twips(m.right)}" w:bottom="${twips(m.bottom)}" w:left="${twips(m.left)}"${header}${footer}/>`);
|
|
581
|
+
}
|
|
582
|
+
if (s.columns) parts.push(colsXml(s.columns));
|
|
583
|
+
if (s.titlePg) parts.push("<w:titlePg/>");
|
|
584
|
+
if (parts.length === 0) return "";
|
|
585
|
+
return `<w:sectPr>${parts.join("")}</w:sectPr>`;
|
|
586
|
+
}
|
|
587
|
+
function colsXml(cols) {
|
|
588
|
+
if (cols.explicit && cols.explicit.length > 0) {
|
|
589
|
+
const inner = cols.explicit.map((c) => `<w:col w:w="${twips(c.widthPt)}" w:space="${twips(c.spacePt)}"/>`).join("");
|
|
590
|
+
return `<w:cols w:num="${cols.explicit.length}" w:equalWidth="0">${inner}</w:cols>`;
|
|
591
|
+
}
|
|
592
|
+
return `<w:cols w:num="${cols.count}" w:space="${twips(cols.spacePt)}"/>`;
|
|
593
|
+
}
|
|
594
|
+
var twips = (pt) => Math.round(pt * 20);
|
|
595
|
+
function escapeXml(s) {
|
|
596
|
+
return s.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">");
|
|
597
|
+
}
|
|
598
|
+
function escapeAttr(s) {
|
|
599
|
+
return escapeXml(s).replace(/"/g, """);
|
|
600
|
+
}
|
|
601
|
+
//#endregion
|
|
602
|
+
export { docxWriter, writeDocx };
|
|
@@ -35,3 +35,4 @@ export type DrawingContent = {
|
|
|
35
35
|
export declare function resolveMc(altContent: PoNode): ReadonlyArray<PoNode>;
|
|
36
36
|
export declare function expandMcChildren(children: ReadonlyArray<PoNode>): Array<PoNode>;
|
|
37
37
|
export declare function parseDrawing(drawing: PoNode, resolveColor: ColorResolver, parseBody?: ParseBody): DrawingContent | null;
|
|
38
|
+
export declare function parseVmlPicture(node: PoNode): DrawingContent | null;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { emuToPt } from "../core/ir/units.js";
|
|
1
|
+
import { emuToPt, pt } from "../core/ir/units.js";
|
|
2
2
|
import { poAttr, poChildren, poFindDescendant, poIntAttr, poIs, poTag, poText } from "../core/po-helpers.js";
|
|
3
3
|
import { resolveColorNode } from "../core/drawingml/colors.js";
|
|
4
4
|
//#region src/word/drawing-parser.ts
|
|
@@ -115,6 +115,41 @@ function parseDrawing(drawing, resolveColor, parseBody) {
|
|
|
115
115
|
};
|
|
116
116
|
return null;
|
|
117
117
|
}
|
|
118
|
+
function parseVmlPicture(node) {
|
|
119
|
+
const imagedata = poFindDescendant(node, "v:imagedata");
|
|
120
|
+
if (!imagedata) return null;
|
|
121
|
+
const imageId = poAttr(imagedata, "id");
|
|
122
|
+
if (imageId === void 0) return null;
|
|
123
|
+
const shape = poFindDescendant(node, "v:shape") ?? poFindDescendant(node, "v:rect") ?? poFindDescendant(node, "v:oval");
|
|
124
|
+
const width = vmlStyleLength(shape, "width");
|
|
125
|
+
const height = vmlStyleLength(shape, "height");
|
|
126
|
+
if (width === void 0 || height === void 0) return null;
|
|
127
|
+
const altText = (shape ? poAttr(shape, "alt") : void 0)?.trim() || poAttr(imagedata, "title")?.trim() || void 0;
|
|
128
|
+
return {
|
|
129
|
+
kind: "image",
|
|
130
|
+
imageId,
|
|
131
|
+
width,
|
|
132
|
+
height,
|
|
133
|
+
...altText ? { altText } : {}
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
var VML_UNIT_TO_PT = {
|
|
137
|
+
pt: 1,
|
|
138
|
+
in: 72,
|
|
139
|
+
px: .75,
|
|
140
|
+
cm: 72 / 2.54,
|
|
141
|
+
mm: 72 / 25.4,
|
|
142
|
+
pc: 12
|
|
143
|
+
};
|
|
144
|
+
function vmlStyleLength(shape, prop) {
|
|
145
|
+
const style = shape ? poAttr(shape, "style") : void 0;
|
|
146
|
+
if (style === void 0) return void 0;
|
|
147
|
+
const m = new RegExp(`(?:^|;)\\s*${prop}\\s*:\\s*(-?[0-9.]+)(pt|in|px|cm|mm|pc)?`, "i").exec(style);
|
|
148
|
+
if (!m) return void 0;
|
|
149
|
+
const value = Number.parseFloat(m[1]);
|
|
150
|
+
if (!Number.isFinite(value) || value <= 0) return void 0;
|
|
151
|
+
return pt(value * (VML_UNIT_TO_PT[(m[2] ?? "px").toLowerCase()] ?? 1));
|
|
152
|
+
}
|
|
118
153
|
function parseWsp(graphicData, extentCx, extentCy, resolveColor, parseBody) {
|
|
119
154
|
const wsp = expandMcChildren(poChildren(graphicData)).find((c) => poIs(c, "wps:wsp"));
|
|
120
155
|
if (!wsp) return null;
|
|
@@ -395,4 +430,4 @@ function colorFromGradient(grad, resolveColor) {
|
|
|
395
430
|
return (toHex(r) + toHex(g) + toHex(b)).toUpperCase();
|
|
396
431
|
}
|
|
397
432
|
//#endregion
|
|
398
|
-
export { expandMcChildren, parseDrawing };
|
|
433
|
+
export { expandMcChildren, parseDrawing, parseVmlPicture };
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "reamkit",
|
|
3
|
-
"version": "1.
|
|
4
|
-
"description": "Ream — DOCX/XLSX to PDF
|
|
3
|
+
"version": "1.4.0",
|
|
4
|
+
"description": "Ream — DOCX/XLSX to PDF, SVG, HTML and back to DOCX, built from scratch on the ECMA-376 and ISO 32000 specifications. Parse once, convert anywhere.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "Alex Krassavin <info@reamkit.dev>",
|
|
7
7
|
"repository": {
|
|
@@ -66,7 +66,8 @@
|
|
|
66
66
|
"corpus:build": "tsx scripts/corpus/build-corpus.ts",
|
|
67
67
|
"corpus:fetch": "tsx scripts/corpus/fetch-corpus.ts",
|
|
68
68
|
"corpus:sandbox:build": "docker build -t docgen-losandbox:latest scripts/corpus/sandbox/",
|
|
69
|
-
"corpus": "tsx scripts/corpus/run.ts"
|
|
69
|
+
"corpus": "tsx scripts/corpus/run.ts",
|
|
70
|
+
"corpus:roundtrip": "tsx scripts/corpus/roundtrip.ts"
|
|
70
71
|
},
|
|
71
72
|
"dependencies": {
|
|
72
73
|
"fast-xml-parser": "5.7.0",
|