reamkit 1.30.0 → 1.32.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 +70 -24
- package/dist/esm/core/bidi/index.d.ts +10 -0
- package/dist/esm/core/bidi/index.js +18 -2
- package/dist/esm/core/converter/facade.js +10 -5
- package/dist/esm/core/converter/project.js +4 -1
- package/dist/esm/core/converter/ream.js +17 -6
- package/dist/esm/core/converter/sheet-images.d.ts +17 -0
- package/dist/esm/core/converter/sheet-images.js +33 -0
- package/dist/esm/core/crypto/offcrypto.js +1 -1
- package/dist/esm/core/document-model/index.d.ts +1 -1
- package/dist/esm/core/document-model/types.d.ts +175 -0
- package/dist/esm/core/drawingml/chart-geometry.d.ts +21 -8
- package/dist/esm/core/drawingml/chart-geometry.js +198 -108
- package/dist/esm/core/drawingml/chart-parser.js +73 -2
- package/dist/esm/core/drawingml/colors.d.ts +6 -0
- package/dist/esm/core/drawingml/colors.js +23 -3
- package/dist/esm/core/drawingml/shape-render.d.ts +3 -1
- package/dist/esm/core/drawingml/shape-render.js +4 -2
- package/dist/esm/core/font/index.d.ts +2 -0
- package/dist/esm/core/font/measure.d.ts +9 -0
- package/dist/esm/core/font/measure.js +9 -1
- package/dist/esm/core/font/ttf-build.d.ts +113 -0
- package/dist/esm/core/font/ttf-build.js +1224 -0
- package/dist/esm/core/font/ttf-subset.d.ts +19 -0
- package/dist/esm/core/font/ttf-subset.js +14 -1
- package/dist/esm/core/fonts/families.js +4 -2
- package/dist/esm/core/fonts/provider.d.ts +17 -0
- package/dist/esm/core/fonts/provider.js +27 -2
- package/dist/esm/core/fonts/remote-fonts.js +8 -1
- package/dist/esm/core/fonts/scripts.js +4 -2
- package/dist/esm/core/ir/flow.d.ts +77 -1
- package/dist/esm/core/ir/sheet.d.ts +43 -0
- package/dist/esm/core/number-format.js +16 -11
- package/dist/esm/core/numbering/index.d.ts +1 -1
- package/dist/esm/core/numbering/state.d.ts +11 -1
- package/dist/esm/core/numbering/state.js +10 -1
- package/dist/esm/core/spreadsheet-model/index.d.ts +1 -1
- package/dist/esm/core/spreadsheet-model/types.d.ts +96 -0
- package/dist/esm/core/style-cascade/resolver.js +39 -6
- package/dist/esm/core/style-cascade/types.d.ts +19 -1
- package/dist/esm/core/style-cascade/types.js +3 -0
- package/dist/esm/excel/column-bands.d.ts +39 -6
- package/dist/esm/excel/column-bands.js +41 -17
- package/dist/esm/excel/comments-parser.js +19 -0
- package/dist/esm/excel/conditional-format.d.ts +7 -3
- package/dist/esm/excel/conditional-format.js +77 -39
- package/dist/esm/excel/formula/context.d.ts +18 -0
- package/dist/esm/excel/formula/eval.js +51 -0
- package/dist/esm/excel/formula/index.d.ts +2 -2
- package/dist/esm/excel/formula/lexer.d.ts +1 -1
- package/dist/esm/excel/formula/lexer.js +41 -0
- package/dist/esm/excel/formula/parser.d.ts +25 -0
- package/dist/esm/excel/formula/parser.js +95 -0
- package/dist/esm/excel/print-model.d.ts +38 -1
- package/dist/esm/excel/print-model.js +522 -80
- package/dist/esm/excel/shared-strings-parser.d.ts +9 -0
- package/dist/esm/excel/shared-strings-parser.js +11 -1
- package/dist/esm/excel/sheet-drawing.d.ts +38 -2
- package/dist/esm/excel/sheet-drawing.js +47 -13
- package/dist/esm/excel/sheet-shape-parser.d.ts +6 -2
- package/dist/esm/excel/sheet-shape-parser.js +18 -6
- package/dist/esm/excel/sheet-to-flow.d.ts +17 -0
- package/dist/esm/excel/sheet-to-flow.js +434 -47
- package/dist/esm/excel/styles-parser.d.ts +9 -0
- package/dist/esm/excel/styles-parser.js +80 -26
- package/dist/esm/excel/table-parser.js +5 -0
- package/dist/esm/excel/table-style-presets.d.ts +26 -0
- package/dist/esm/excel/table-style-presets.js +1555 -0
- package/dist/esm/excel/table-style.d.ts +13 -0
- package/dist/esm/excel/table-style.js +75 -0
- package/dist/esm/excel/tint.d.ts +10 -0
- package/dist/esm/excel/tint.js +75 -0
- package/dist/esm/excel/vml-drawing.d.ts +41 -3
- package/dist/esm/excel/vml-drawing.js +143 -10
- package/dist/esm/excel/worksheet-parser.js +89 -15
- package/dist/esm/excel/xls/biff-reader.js +10 -2
- package/dist/esm/excel/xlsx-reader.js +195 -14
- package/dist/esm/excel/xlsx-writer.js +8 -7
- package/dist/esm/html/html-writer.js +202 -20
- package/dist/esm/index.d.ts +2 -2
- package/dist/esm/layout/fit-pages.d.ts +11 -0
- package/dist/esm/layout/fit-pages.js +86 -0
- package/dist/esm/layout/line-paint.d.ts +58 -0
- package/dist/esm/layout/line-paint.js +107 -0
- package/dist/esm/layout/page-doc.d.ts +61 -3
- package/dist/esm/layout/page-doc.js +141 -1
- package/dist/esm/layout/styled-layout.d.ts +55 -7
- package/dist/esm/layout/styled-layout.js +1154 -169
- package/dist/esm/layout/turned-section.d.ts +38 -0
- package/dist/esm/layout/turned-section.js +205 -0
- package/dist/esm/markdown/markdown-writer.js +17 -6
- package/dist/esm/pdf/styled-page-emitter.js +114 -60
- package/dist/esm/pdf/styled-page-renderer.js +1 -1
- package/dist/esm/pdf-reader/cff-outline.d.ts +27 -0
- package/dist/esm/pdf-reader/cff-outline.js +169 -21
- package/dist/esm/pdf-reader/content.d.ts +53 -0
- package/dist/esm/pdf-reader/content.js +9 -1
- package/dist/esm/pdf-reader/display.d.ts +36 -0
- package/dist/esm/pdf-reader/display.js +66 -1
- package/dist/esm/pdf-reader/document.js +5 -1
- package/dist/esm/pdf-reader/embedded-fonts.d.ts +2 -1
- package/dist/esm/pdf-reader/embedded-fonts.js +32 -2
- package/dist/esm/pdf-reader/encodings.d.ts +8 -0
- package/dist/esm/pdf-reader/encodings.js +25 -3
- package/dist/esm/pdf-reader/face-outlines.d.ts +78 -0
- package/dist/esm/pdf-reader/face-outlines.js +362 -0
- package/dist/esm/pdf-reader/figures.d.ts +52 -0
- package/dist/esm/pdf-reader/figures.js +433 -0
- package/dist/esm/pdf-reader/flow-build.d.ts +131 -5
- package/dist/esm/pdf-reader/flow-build.js +345 -26
- package/dist/esm/pdf-reader/font.js +321 -36
- package/dist/esm/pdf-reader/glyf-outline.d.ts +33 -0
- package/dist/esm/pdf-reader/glyf-outline.js +135 -1
- package/dist/esm/pdf-reader/glyph-names.js +154 -1
- package/dist/esm/pdf-reader/layout.d.ts +93 -0
- package/dist/esm/pdf-reader/layout.js +1703 -214
- package/dist/esm/pdf-reader/page-numbers.d.ts +53 -0
- package/dist/esm/pdf-reader/page-numbers.js +167 -0
- package/dist/esm/pdf-reader/tagged.js +182 -23
- package/dist/esm/pdf-reader/text.d.ts +6 -3
- package/dist/esm/pdf-reader/text.js +98 -9
- package/dist/esm/pdf-reader/type1-outline.d.ts +11 -0
- package/dist/esm/pdf-reader/type1-outline.js +63 -8
- package/dist/esm/pdf-reader/vector.js +71 -1
- package/dist/esm/svg/svg-text.d.ts +58 -0
- package/dist/esm/svg/svg-text.js +322 -0
- package/dist/esm/svg/svg-writer.js +135 -71
- package/dist/esm/word/doc/doc-reader.js +6 -2
- package/dist/esm/word/doc/doc-text.d.ts +6 -0
- package/dist/esm/word/doc/doc-text.js +19 -1
- package/dist/esm/word/document-parser.d.ts +2 -2
- package/dist/esm/word/document-parser.js +11 -1
- package/dist/esm/word/docx-reader.js +5 -3
- package/dist/esm/word/docx-writer.js +218 -23
- package/dist/esm/word/drawing-parser.d.ts +5 -3
- package/dist/esm/word/drawing-parser.js +49 -8
- package/dist/esm/word/font-embed.d.ts +30 -0
- package/dist/esm/word/font-embed.js +173 -0
- package/dist/esm/word/font-table.d.ts +10 -0
- package/dist/esm/word/font-table.js +13 -1
- package/dist/esm/word/index.js +1 -1
- package/dist/esm/word/numbering-parser.d.ts +3 -1
- package/dist/esm/word/numbering-parser.js +2 -1
- package/dist/esm/word/paragraph-properties.d.ts +7 -6
- package/dist/esm/word/paragraph-properties.js +14 -2
- package/dist/esm/word/run-properties.js +26 -0
- package/package.json +12 -3
|
@@ -0,0 +1,322 @@
|
|
|
1
|
+
import { svgPathData } from "../core/vector.js";
|
|
2
|
+
import { reverseByCodePoint } from "../core/bidi/index.js";
|
|
3
|
+
import { FAUX_ITALIC_SHEAR, computeJustifyExtra, lineVisualOrder } from "../layout/line-paint.js";
|
|
4
|
+
import { warpGlyphMatrix } from "../core/drawingml/text-warp.js";
|
|
5
|
+
import { sanitizeHref } from "../core/links.js";
|
|
6
|
+
import { cffOutlineSource, openTypeCff } from "../pdf-reader/cff-outline.js";
|
|
7
|
+
import { outlineSource } from "../pdf-reader/glyf-outline.js";
|
|
8
|
+
//#region src/svg/svg-text.ts
|
|
9
|
+
/** The colour of a comment range's highlight — the PDF emitter's. */
|
|
10
|
+
var HIGHLIGHT_HEX = "fff3a3";
|
|
11
|
+
/** The glyph outlines of a page's faces, each defined once and used by id. */
|
|
12
|
+
var SvgGlyphs = class {
|
|
13
|
+
sources = /* @__PURE__ */ new Map();
|
|
14
|
+
ids = /* @__PURE__ */ new Map();
|
|
15
|
+
defs = [];
|
|
16
|
+
faceIds = /* @__PURE__ */ new Map();
|
|
17
|
+
/**
|
|
18
|
+
* The id of the path that draws glyph `gid` of `font`, defining it on first
|
|
19
|
+
* use: `null` for a glyph with no ink (a space), `undefined` where the face
|
|
20
|
+
* carries no outlines this reads — the caller then sets the token as text.
|
|
21
|
+
*
|
|
22
|
+
* @param font The face.
|
|
23
|
+
* @param gid The glyph.
|
|
24
|
+
* @returns The `<path>` id, `null`, or `undefined`.
|
|
25
|
+
*/
|
|
26
|
+
use(font, gid) {
|
|
27
|
+
const source = this.sourceOf(font);
|
|
28
|
+
if (!source) return void 0;
|
|
29
|
+
let face = this.faceIds.get(font);
|
|
30
|
+
if (face === void 0) {
|
|
31
|
+
face = this.faceIds.size;
|
|
32
|
+
this.faceIds.set(font, face);
|
|
33
|
+
}
|
|
34
|
+
const key = `g${String(face)}-${String(gid)}`;
|
|
35
|
+
const known = this.ids.get(key);
|
|
36
|
+
if (known !== void 0) return known;
|
|
37
|
+
const path = source.path(gid);
|
|
38
|
+
if (!path || path.length === 0) {
|
|
39
|
+
this.ids.set(key, null);
|
|
40
|
+
return null;
|
|
41
|
+
}
|
|
42
|
+
this.defs.push(`<path id="${key}" d="${svgPathData(path, fmtEm)}"/>`);
|
|
43
|
+
this.ids.set(key, key);
|
|
44
|
+
return key;
|
|
45
|
+
}
|
|
46
|
+
/** The `<defs>` holding every glyph used, or nothing when none was. */
|
|
47
|
+
defsXml() {
|
|
48
|
+
return this.defs.length > 0 ? `<defs>${this.defs.join("")}</defs>` : "";
|
|
49
|
+
}
|
|
50
|
+
sourceOf(font) {
|
|
51
|
+
let source = this.sources.get(font);
|
|
52
|
+
if (source === void 0) {
|
|
53
|
+
const raw = font.parsed.raw;
|
|
54
|
+
const cff = font.parsed.tables.has("CFF ") ? openTypeCff(raw) : void 0;
|
|
55
|
+
source = (cff ? cffOutlineSource(cff) : outlineSource(raw)) ?? null;
|
|
56
|
+
this.sources.set(font, source);
|
|
57
|
+
}
|
|
58
|
+
return source ?? void 0;
|
|
59
|
+
}
|
|
60
|
+
};
|
|
61
|
+
/**
|
|
62
|
+
* Draw one laid-out line: what stands behind its glyphs, then the glyphs, a
|
|
63
|
+
* picture or formula wherever one stands in it.
|
|
64
|
+
*
|
|
65
|
+
* @param out The SVG fragment sink.
|
|
66
|
+
* @param item The line, as the layout placed it.
|
|
67
|
+
* @param ctx The glyph definitions and the writer's own drawing.
|
|
68
|
+
*/
|
|
69
|
+
function emitSvgLine(out, item, ctx) {
|
|
70
|
+
const clip = item.clip;
|
|
71
|
+
if (clip) {
|
|
72
|
+
const id = `clip${String(ctx.ids.n++)}`;
|
|
73
|
+
out.push(`<clipPath id="${id}"><rect x="${fmt(clip.x)}" y="${fmt(clip.y)}" width="${fmt(clip.width)}" height="${fmt(clip.height)}"/></clipPath>`, `<g clip-path="url(#${id})">`);
|
|
74
|
+
}
|
|
75
|
+
if (item.warp) emitWarped(out, item, ctx);
|
|
76
|
+
else if (item.rotationDeg) {
|
|
77
|
+
out.push(`<g transform="rotate(${fmt(-item.rotationDeg)} ${fmt(item.originX)} ${fmt(item.baselineY)})">`);
|
|
78
|
+
emitFlat(out, item, ctx);
|
|
79
|
+
out.push("</g>");
|
|
80
|
+
} else emitFlat(out, item, ctx);
|
|
81
|
+
if (clip) out.push("</g>");
|
|
82
|
+
}
|
|
83
|
+
/** A line set along its baseline, token by token. */
|
|
84
|
+
function emitFlat(out, item, ctx) {
|
|
85
|
+
const line = item.line;
|
|
86
|
+
const originX = item.originX;
|
|
87
|
+
const baseline = item.baselineY;
|
|
88
|
+
const extraPerSpace = computeJustifyExtra(line);
|
|
89
|
+
const advance = (tok) => tok.widthPt + (tok.kind === "text" && tok.isSpace ? extraPerSpace : 0);
|
|
90
|
+
const lineFs = line.maxFontSizePt || 12;
|
|
91
|
+
const boxAscent = Math.max(lineFs, line.mathAscentPt ?? 0);
|
|
92
|
+
const boxDescent = Math.max(lineFs * .2, line.mathDescentPt ?? 0);
|
|
93
|
+
let x = originX;
|
|
94
|
+
for (const tok of line.tokens) {
|
|
95
|
+
const w = advance(tok);
|
|
96
|
+
if (tok.kind === "text") {
|
|
97
|
+
const hex = tok.resolvedRun.shadingColorHex ?? (tok.highlight === true ? HIGHLIGHT_HEX : void 0);
|
|
98
|
+
if (hex !== void 0) out.push(`<rect x="${fmt(x)}" y="${fmt(baseline - boxAscent)}" width="${fmt(w)}" height="${fmt(boxAscent + boxDescent)}" fill="#${hex}"/>`);
|
|
99
|
+
}
|
|
100
|
+
x += w;
|
|
101
|
+
}
|
|
102
|
+
x = originX;
|
|
103
|
+
for (const tok of line.tokens) {
|
|
104
|
+
const w = advance(tok);
|
|
105
|
+
if (tok.kind === "text" && (tok.resolvedRun.underline !== "none" || tok.resolvedRun.strike)) emitDecoration(out, tok, x, w, baseline);
|
|
106
|
+
x += w;
|
|
107
|
+
}
|
|
108
|
+
const order = line.tokens.some((t) => t.bidiLevel % 2 === 1) ? lineVisualOrder(line) : line.tokens.map((_, i) => i);
|
|
109
|
+
x = originX;
|
|
110
|
+
for (const i of order) {
|
|
111
|
+
const tok = line.tokens[i];
|
|
112
|
+
if (tok.kind === "image") emitImageToken(out, tok, x, baseline, ctx);
|
|
113
|
+
else if (tok.kind === "math") emitMathToken(out, tok, x, baseline, ctx);
|
|
114
|
+
else emitTextToken(out, tok, x, baseline, ctx);
|
|
115
|
+
x += advance(tok);
|
|
116
|
+
}
|
|
117
|
+
const words = line.tokens.map((t) => t.kind === "text" ? t.text : "").join("");
|
|
118
|
+
if (words.trim().length > 0) out.push(`<text x="${fmt(originX)}" y="${fmt(baseline)}" font-size="${fmt(lineFs)}" textLength="${fmt(x - originX)}" lengthAdjust="spacingAndGlyphs" fill-opacity="0" xml:space="preserve">${escapeXml(words)}</text>`);
|
|
119
|
+
}
|
|
120
|
+
/**
|
|
121
|
+
* One run's glyphs from `x` along the baseline: the shaped glyphs of its text
|
|
122
|
+
* (reversed for a right-to-left run, as the PDF shows it), each at the advance
|
|
123
|
+
* the shaper gave it plus the run's letter spacing.
|
|
124
|
+
*/
|
|
125
|
+
function emitTextToken(out, tok, x, baseline, ctx) {
|
|
126
|
+
if (tok.text.length === 0) return;
|
|
127
|
+
const size = tok.fontSizePt;
|
|
128
|
+
const scale = (tok.synthetic?.widthScale ?? 1) * (tok.resolvedRun.widthScale ?? 1);
|
|
129
|
+
const slant = tok.synthetic?.italic === true ? FAUX_ITALIC_SHEAR : 0;
|
|
130
|
+
const rise = tok.risePt ?? 0;
|
|
131
|
+
const text = tok.bidiLevel % 2 === 1 ? reverseByCodePoint(tok.text) : tok.text;
|
|
132
|
+
const run = tok.font.measure.glyphRun(text);
|
|
133
|
+
const upem = tok.font.parsed.unitsPerEm;
|
|
134
|
+
const spacingEm = (tok.resolvedRun.letterSpacingPt ?? 0) / (size * scale);
|
|
135
|
+
const uses = [];
|
|
136
|
+
let pen = 0;
|
|
137
|
+
for (let g = 0; g < run.gids.length; g++) {
|
|
138
|
+
const id = ctx.glyphs.use(tok.font, run.gids[g]);
|
|
139
|
+
if (id === void 0) {
|
|
140
|
+
emitAsText(out, tok, x, baseline);
|
|
141
|
+
return;
|
|
142
|
+
}
|
|
143
|
+
if (id !== null) uses.push(`<use href="#${id}"${pen !== 0 ? ` x="${fmtEm(pen)}"` : ""}/>`);
|
|
144
|
+
pen += (run.advances[g] ?? 0) / upem + spacingEm;
|
|
145
|
+
}
|
|
146
|
+
if (uses.length === 0) return;
|
|
147
|
+
const outline = tok.resolvedRun.textOutline;
|
|
148
|
+
const strokeWidth = outline !== void 0 ? outline.widthPt : tok.synthetic?.bold === true ? size * .03 : 0;
|
|
149
|
+
const stroke = strokeWidth > 0 ? ` stroke="#${outline?.colorHex ?? tok.resolvedRun.colorHex}" stroke-width="${fmtEm(strokeWidth / size)}"` : "";
|
|
150
|
+
const group = `<g transform="matrix(${fmt(size * scale)} 0 ${fmt(slant * size)} ${fmt(-size)} ${fmt(x)} ${fmt(baseline - rise)})" fill="#${tok.resolvedRun.colorHex}"${stroke}>${uses.join("")}</g>`;
|
|
151
|
+
const href = tok.href !== void 0 ? sanitizeHref(tok.href) : void 0;
|
|
152
|
+
out.push(href !== void 0 ? `<a href="${escapeXml(href)}">${group}</a>` : group);
|
|
153
|
+
}
|
|
154
|
+
/** A run of a face whose outlines cannot be read: its characters, for the viewer's fonts to set. */
|
|
155
|
+
function emitAsText(out, tok, x, baseline) {
|
|
156
|
+
if (tok.text.trim().length === 0) return;
|
|
157
|
+
out.push(`<text x="${fmt(x)}" y="${fmt(baseline - (tok.risePt ?? 0))}" font-family="sans-serif" font-size="${fmt(tok.fontSizePt)}" fill="#${tok.resolvedRun.colorHex}">${escapeXml(tok.text)}</text>`);
|
|
158
|
+
}
|
|
159
|
+
/** A run's underline and strikethrough, where the PDF emitter draws them. */
|
|
160
|
+
function emitDecoration(out, tok, x, w, baseline) {
|
|
161
|
+
const size = tok.fontSizePt;
|
|
162
|
+
const thickness = Math.max(.4, size * .06);
|
|
163
|
+
const style = tok.resolvedRun.underline;
|
|
164
|
+
if (style !== "none") {
|
|
165
|
+
const hex = tok.resolvedRun.underlineColorHex ?? tok.resolvedRun.colorHex;
|
|
166
|
+
const t = style === "thick" || style === "dottedHeavy" || style === "dashHeavy" ? thickness * 2 : thickness;
|
|
167
|
+
const top = baseline + size * .12;
|
|
168
|
+
const dashed = style === "dotted" || style === "dottedHeavy" ? [t, t * 2] : style === "dash" || style === "dashHeavy" ? [t * 4, t * 3] : void 0;
|
|
169
|
+
if (dashed) out.push(`<line x1="${fmt(x)}" y1="${fmt(top + t / 2)}" x2="${fmt(x + w)}" y2="${fmt(top + t / 2)}" stroke="#${hex}" stroke-width="${fmt(t)}" stroke-dasharray="${dashed.map(fmt).join(" ")}"/>`);
|
|
170
|
+
else {
|
|
171
|
+
out.push(`<rect x="${fmt(x)}" y="${fmt(top)}" width="${fmt(w)}" height="${fmt(t)}" fill="#${hex}"/>`);
|
|
172
|
+
if (style === "double") out.push(`<rect x="${fmt(x)}" y="${fmt(top + t * 2)}" width="${fmt(w)}" height="${fmt(t)}" fill="#${hex}"/>`);
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
if (tok.resolvedRun.strike) out.push(`<rect x="${fmt(x)}" y="${fmt(baseline - size * .26 - thickness)}" width="${fmt(w)}" height="${fmt(thickness)}" fill="#${tok.resolvedRun.colorHex}"/>`);
|
|
176
|
+
}
|
|
177
|
+
/**
|
|
178
|
+
* An inline picture standing on the baseline, its bottom edge on it — or a
|
|
179
|
+
* metafile, which draws itself from that corner in a frame of its own (y up).
|
|
180
|
+
*/
|
|
181
|
+
function emitImageToken(out, tok, x, baseline, ctx) {
|
|
182
|
+
const b = tok.drawBox;
|
|
183
|
+
const left = x + (b?.dxPt ?? 0);
|
|
184
|
+
const bottom = baseline - (b?.dyPt ?? 0);
|
|
185
|
+
const width = b?.widthPt ?? tok.widthPt;
|
|
186
|
+
const height = b?.heightPt ?? tok.heightPt;
|
|
187
|
+
const meta = tok.metafile;
|
|
188
|
+
if (meta) {
|
|
189
|
+
for (const img of meta.images ?? []) {
|
|
190
|
+
if (img.resourceName === "") continue;
|
|
191
|
+
ctx.image(out, {
|
|
192
|
+
resourceName: img.resourceName,
|
|
193
|
+
x: left + img.x,
|
|
194
|
+
y: bottom - img.y - img.height,
|
|
195
|
+
width: img.width,
|
|
196
|
+
height: img.height,
|
|
197
|
+
...img.rotationDeg !== void 0 ? { rotationDeg: img.rotationDeg } : {}
|
|
198
|
+
});
|
|
199
|
+
}
|
|
200
|
+
for (const sh of meta.shapes) ctx.shape(out, {
|
|
201
|
+
...sh,
|
|
202
|
+
transform: [
|
|
203
|
+
1,
|
|
204
|
+
0,
|
|
205
|
+
0,
|
|
206
|
+
-1,
|
|
207
|
+
left,
|
|
208
|
+
bottom
|
|
209
|
+
]
|
|
210
|
+
});
|
|
211
|
+
for (const t of meta.texts) {
|
|
212
|
+
const first = t.line.tokens.find((k) => k.kind === "text");
|
|
213
|
+
if (first?.kind !== "text") continue;
|
|
214
|
+
emitTextToken(out, first, left + t.x, bottom - t.y, ctx);
|
|
215
|
+
}
|
|
216
|
+
return;
|
|
217
|
+
}
|
|
218
|
+
if (!tok.imageResourceName) return;
|
|
219
|
+
const shadow = tok.shadow;
|
|
220
|
+
if (shadow) {
|
|
221
|
+
const id = `shadow${String(ctx.ids.n++)}`;
|
|
222
|
+
const blur = shadow.blurPt > 0;
|
|
223
|
+
if (blur) out.push(`<filter id="${id}" x="-50%" y="-50%" width="200%" height="200%"><feGaussianBlur stdDeviation="${fmt(shadow.blurPt / 2)}"/></filter>`);
|
|
224
|
+
out.push(`<rect x="${fmt(left + shadow.dxPt)}" y="${fmt(bottom - height + shadow.dyPt)}" width="${fmt(width)}" height="${fmt(height)}" fill="#${shadow.colorHex}" opacity="${fmt(shadow.alpha)}"${blur ? ` filter="url(#${id})"` : ""}/>`);
|
|
225
|
+
}
|
|
226
|
+
ctx.image(out, {
|
|
227
|
+
resourceName: tok.imageResourceName,
|
|
228
|
+
x: left,
|
|
229
|
+
y: bottom - height,
|
|
230
|
+
width,
|
|
231
|
+
height,
|
|
232
|
+
...tok.crop ? { crop: tok.crop } : {},
|
|
233
|
+
...tok.rotationDeg !== void 0 ? { rotationDeg: tok.rotationDeg } : {},
|
|
234
|
+
...tok.flipH === true ? { flipH: true } : {},
|
|
235
|
+
...tok.flipV === true ? { flipV: true } : {}
|
|
236
|
+
});
|
|
237
|
+
const frame = tok.outline;
|
|
238
|
+
if (frame) out.push(`<rect x="${fmt(left)}" y="${fmt(bottom - height)}" width="${fmt(width)}" height="${fmt(height)}" fill="none" stroke="#${frame.colorHex}" stroke-width="${fmt(frame.widthPt)}"/>`);
|
|
239
|
+
}
|
|
240
|
+
/** An inline formula: its glyphs, rules and paths, placed from its origin on the baseline (y up). */
|
|
241
|
+
function emitMathToken(out, tok, x, baseline, ctx) {
|
|
242
|
+
for (const it of tok.items) if (it.kind === "glyph") {
|
|
243
|
+
const run = it.font.measure.glyphRun(it.text);
|
|
244
|
+
const upem = it.font.parsed.unitsPerEm;
|
|
245
|
+
const uses = [];
|
|
246
|
+
let pen = 0;
|
|
247
|
+
for (let g = 0; g < run.gids.length; g++) {
|
|
248
|
+
const id = ctx.glyphs.use(it.font, run.gids[g]);
|
|
249
|
+
if (id) uses.push(`<use href="#${id}"${pen !== 0 ? ` x="${fmtEm(pen)}"` : ""}/>`);
|
|
250
|
+
pen += (run.advances[g] ?? 0) / upem;
|
|
251
|
+
}
|
|
252
|
+
if (uses.length > 0) out.push(`<g transform="matrix(${fmt(it.sizePt)} 0 0 ${fmt(-it.sizePt)} ${fmt(x + it.x)} ${fmt(baseline - it.y)})" fill="#000000">${uses.join("")}</g>`);
|
|
253
|
+
} else if (it.kind === "rule") out.push(`<rect x="${fmt(x + it.x)}" y="${fmt(baseline - it.y - it.h)}" width="${fmt(it.w)}" height="${fmt(it.h)}" fill="#000000"/>`);
|
|
254
|
+
else ctx.shape(out, {
|
|
255
|
+
paths: [{ segments: it.segments }],
|
|
256
|
+
...it.fill === true ? { fillColorHex: "000000" } : {},
|
|
257
|
+
...it.strokeWidthPt !== void 0 ? { stroke: {
|
|
258
|
+
colorHex: "000000",
|
|
259
|
+
widthPt: it.strokeWidthPt
|
|
260
|
+
} } : {},
|
|
261
|
+
transform: [
|
|
262
|
+
1,
|
|
263
|
+
0,
|
|
264
|
+
0,
|
|
265
|
+
-1,
|
|
266
|
+
x,
|
|
267
|
+
baseline
|
|
268
|
+
]
|
|
269
|
+
});
|
|
270
|
+
}
|
|
271
|
+
/**
|
|
272
|
+
* §20.1.9.10 — WordArt: every glyph at its own point on the warp's curve, each
|
|
273
|
+
* with its own matrix. The token's glyphs are rescaled to the width it was
|
|
274
|
+
* measured at, as the PDF does, so the block still ends where the layout said.
|
|
275
|
+
*/
|
|
276
|
+
function emitWarped(out, item, ctx) {
|
|
277
|
+
const warp = item.warp;
|
|
278
|
+
let cursor = item.originX;
|
|
279
|
+
for (const tok of item.line.tokens) {
|
|
280
|
+
if (tok.kind !== "text" || tok.isSpace) {
|
|
281
|
+
cursor += tok.widthPt;
|
|
282
|
+
continue;
|
|
283
|
+
}
|
|
284
|
+
const size = tok.fontSizePt;
|
|
285
|
+
const chars = [...tok.text];
|
|
286
|
+
const raw = chars.map((c) => tok.font.measure.textWidthPt(c, size));
|
|
287
|
+
const total = raw.reduce((a, b) => a + b, 0);
|
|
288
|
+
const k = total > 0 ? tok.widthPt / total : 1;
|
|
289
|
+
for (let i = 0; i < chars.length; i++) {
|
|
290
|
+
const adv = raw[i] * k;
|
|
291
|
+
const m = warpGlyphMatrix(warp, cursor, adv, item.baselineY);
|
|
292
|
+
cursor += adv;
|
|
293
|
+
if (!m) continue;
|
|
294
|
+
const run = tok.font.measure.glyphRun(chars[i]);
|
|
295
|
+
const upem = tok.font.parsed.unitsPerEm;
|
|
296
|
+
const uses = [];
|
|
297
|
+
let pen = 0;
|
|
298
|
+
for (let g = 0; g < run.gids.length; g++) {
|
|
299
|
+
const id = ctx.glyphs.use(tok.font, run.gids[g]);
|
|
300
|
+
if (id) uses.push(`<use href="#${id}"${pen !== 0 ? ` x="${fmtEm(pen)}"` : ""}/>`);
|
|
301
|
+
pen += (run.advances[g] ?? 0) / upem;
|
|
302
|
+
}
|
|
303
|
+
if (uses.length === 0) continue;
|
|
304
|
+
out.push(`<g transform="matrix(${fmt(m[0] * size)} ${fmt(m[1] * size)} ${fmt(m[2] * size)} ${fmt(m[3] * size)} ${fmt(m[4])} ${fmt(m[5])})" fill="#${tok.resolvedRun.colorHex}">${uses.join("")}</g>`);
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
function escapeXml(s) {
|
|
309
|
+
return s.replaceAll("&", "&").replaceAll("<", "<").replaceAll(">", ">").replaceAll("\"", """);
|
|
310
|
+
}
|
|
311
|
+
/** A page coordinate, to the hundredth of a point. */
|
|
312
|
+
function fmt(n) {
|
|
313
|
+
const r = Math.round(n * 100) / 100;
|
|
314
|
+
return Object.is(r, -0) ? "0" : String(r);
|
|
315
|
+
}
|
|
316
|
+
/** A coordinate in a one-unit em, to a ten-thousandth of it. */
|
|
317
|
+
function fmtEm(n) {
|
|
318
|
+
const r = Math.round(n * 1e4) / 1e4;
|
|
319
|
+
return Object.is(r, -0) ? "0" : String(r);
|
|
320
|
+
}
|
|
321
|
+
//#endregion
|
|
322
|
+
export { SvgGlyphs, emitSvgLine };
|
|
@@ -1,8 +1,10 @@
|
|
|
1
|
+
import { pageLayers, paintPlan } from "../layout/page-doc.js";
|
|
1
2
|
import { FEATURES } from "../core/ir/features.js";
|
|
2
3
|
import { svgPathData } from "../core/vector.js";
|
|
3
4
|
import { gradientSvgDef } from "../core/drawingml/shape-render.js";
|
|
4
|
-
import {
|
|
5
|
+
import { BORDER_DASHES, washVeil } from "../layout/line-paint.js";
|
|
5
6
|
import { toBase64 } from "../core/bytes.js";
|
|
7
|
+
import { SvgGlyphs, emitSvgLine } from "./svg-text.js";
|
|
6
8
|
//#region src/svg/svg-writer.ts
|
|
7
9
|
var PAGE_GAP = 12;
|
|
8
10
|
/**
|
|
@@ -23,17 +25,25 @@ function writeSvg(laid, opts = {}) {
|
|
|
23
25
|
const width = Math.max(1, ...laid.pages.map((p) => p.width));
|
|
24
26
|
const height = laid.pages.reduce((s, p) => s + p.height, 0) + gap * Math.max(0, laid.pages.length - 1);
|
|
25
27
|
const parts = [];
|
|
26
|
-
|
|
28
|
+
const ids = { n: 0 };
|
|
29
|
+
const glyphs = new SvgGlyphs();
|
|
30
|
+
const ctx = {
|
|
31
|
+
glyphs,
|
|
32
|
+
ids,
|
|
33
|
+
laid,
|
|
34
|
+
image: (out, placed) => emitPlaced(out, placed, laid, ids),
|
|
35
|
+
shape: (out, shape) => emitShape(out, shape, ids)
|
|
36
|
+
};
|
|
27
37
|
let yOffset = 0;
|
|
28
|
-
const idc = { n: 0 };
|
|
29
38
|
laid.pages.forEach((page, i) => {
|
|
30
39
|
parts.push(`<g transform="translate(0 ${fmt(yOffset)})" data-page="${i + 1}">`);
|
|
31
40
|
parts.push(`<rect x="0" y="0" width="${fmt(page.width)}" height="${fmt(page.height)}" fill="#ffffff" stroke="#cccccc"/>`);
|
|
32
|
-
emitPage(parts, page,
|
|
41
|
+
emitPage(parts, page, ctx);
|
|
33
42
|
parts.push("</g>");
|
|
34
43
|
yOffset += page.height + gap;
|
|
35
44
|
});
|
|
36
45
|
parts.push("</svg>");
|
|
46
|
+
parts.unshift(`<svg xmlns="http://www.w3.org/2000/svg" width="${fmt(width)}" height="${fmt(height)}" viewBox="0 0 ${fmt(width)} ${fmt(height)}">`, glyphs.defsXml());
|
|
37
47
|
return {
|
|
38
48
|
bytes: new TextEncoder().encode(parts.join("\n")),
|
|
39
49
|
losses
|
|
@@ -54,11 +64,24 @@ var svgWriter = {
|
|
|
54
64
|
]),
|
|
55
65
|
write: (doc, opts) => writeSvg(doc, opts ?? {})
|
|
56
66
|
};
|
|
57
|
-
function emitPage(out, page,
|
|
58
|
-
const
|
|
59
|
-
|
|
67
|
+
function emitPage(out, page, ctx) {
|
|
68
|
+
for (const layer of pageLayers(page.commands)) {
|
|
69
|
+
const w = layer.window;
|
|
70
|
+
if (!w) {
|
|
71
|
+
emitLayer(out, layer.items, ctx);
|
|
72
|
+
continue;
|
|
73
|
+
}
|
|
74
|
+
const id = `win${String(ctx.ids.n++)}`;
|
|
75
|
+
out.push(`<clipPath id="${id}"><rect x="${fmt(w.x)}" y="${fmt(w.y)}" width="${fmt(w.width)}" height="${fmt(w.height)}"/></clipPath>`, `<g clip-path="url(#${id})">`);
|
|
76
|
+
emitLayer(out, layer.items, ctx);
|
|
77
|
+
out.push("</g>");
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
function emitLayer(out, commands, ctx) {
|
|
81
|
+
const plan = paintPlan(commands);
|
|
82
|
+
for (const item of plan.behind) emitPageItem(out, item, ctx);
|
|
60
83
|
for (const f of plan.fills) out.push(`<rect x="${fmt(f.x)}" y="${fmt(f.y)}" width="${fmt(f.width)}" height="${fmt(f.height)}" fill="#${f.fillColorHex}"/>`);
|
|
61
|
-
for (const img of plan.images) emitImage(out, img,
|
|
84
|
+
for (const img of plan.images) emitImage(out, img, ctx);
|
|
62
85
|
for (const b of plan.borders) {
|
|
63
86
|
const x2 = b.x + b.width;
|
|
64
87
|
const yTop = b.y;
|
|
@@ -84,104 +107,148 @@ function emitPage(out, page, laid, losses, idc) {
|
|
|
84
107
|
x2,
|
|
85
108
|
yBottom
|
|
86
109
|
];
|
|
87
|
-
|
|
110
|
+
const dash = b.borderStyle !== void 0 ? BORDER_DASHES.get(b.borderStyle) : void 0;
|
|
111
|
+
out.push(`<line x1="${fmt(ax)}" y1="${fmt(ay)}" x2="${fmt(bx)}" y2="${fmt(by)}" stroke="#${b.borderColorHex}" stroke-width="${fmt(b.borderSizePt)}"${dash ? ` stroke-dasharray="${dash.map(fmt).join(" ")}"` : ""}/>`);
|
|
88
112
|
}
|
|
89
|
-
for (const sh of plan.shapes) emitShape(out, sh.shape,
|
|
90
|
-
for (const picture of plan.pictures) for (const item of picture) emitPageItem(out, item,
|
|
91
|
-
for (const run of plan.ordered) for (const item of run) emitPageItem(out, item,
|
|
92
|
-
for (const t of plan.lines)
|
|
113
|
+
for (const sh of plan.shapes) emitShape(out, sh.shape, ctx.ids);
|
|
114
|
+
for (const picture of plan.pictures) for (const item of picture) emitPageItem(out, item, ctx);
|
|
115
|
+
for (const run of plan.ordered) for (const item of run) emitPageItem(out, item, ctx);
|
|
116
|
+
for (const t of plan.lines) emitSvgLine(out, t, ctx);
|
|
93
117
|
}
|
|
94
118
|
/**
|
|
95
|
-
* One picture
|
|
96
|
-
* dark end and its light end —
|
|
97
|
-
*
|
|
98
|
-
*
|
|
119
|
+
* One picture item, its placement and its effects: §20.1.8.23 `a:duotone`
|
|
120
|
+
* recolours it between its dark end and its light end — luminance into every
|
|
121
|
+
* channel, then a two-entry transfer table per channel — and a picture FILL
|
|
122
|
+
* (§20.1.8.14) is clipped to the outline of the shape it fills.
|
|
99
123
|
*
|
|
100
124
|
* @param out The SVG fragment sink.
|
|
101
125
|
* @param img The image item.
|
|
102
|
-
* @param
|
|
103
|
-
* @param idc The shared id counter, for the filter's id.
|
|
126
|
+
* @param ctx The document, its id counter, its drawing.
|
|
104
127
|
*/
|
|
105
|
-
function emitImage(out, img,
|
|
106
|
-
|
|
107
|
-
if (!href) return;
|
|
108
|
-
let filter = "";
|
|
128
|
+
function emitImage(out, img, ctx) {
|
|
129
|
+
let filter;
|
|
109
130
|
if (img.duotone) {
|
|
110
|
-
const id = `duo${String(
|
|
131
|
+
const id = `duo${String(ctx.ids.n++)}`;
|
|
111
132
|
const chan = (hex, at) => parseInt(hex.slice(at, at + 2), 16) / 255;
|
|
112
133
|
const table = (at) => `${fmt(chan(img.duotone.shadowHex, at))} ${fmt(chan(img.duotone.highlightHex, at))}`;
|
|
113
134
|
out.push(`<filter id="${id}" color-interpolation-filters="sRGB"><feColorMatrix type="matrix" values="0.2126 0.7152 0.0722 0 0 0.2126 0.7152 0.0722 0 0 0.2126 0.7152 0.0722 0 0 0 0 0 1 0"/><feComponentTransfer><feFuncR type="table" tableValues="${table(0)}"/><feFuncG type="table" tableValues="${table(2)}"/><feFuncB type="table" tableValues="${table(4)}"/></feComponentTransfer></filter>`);
|
|
114
|
-
filter =
|
|
135
|
+
filter = id;
|
|
136
|
+
}
|
|
137
|
+
let clipped = false;
|
|
138
|
+
if (img.clip) {
|
|
139
|
+
const id = `pic${String(ctx.ids.n++)}`;
|
|
140
|
+
const [a, b, c, d, e, f] = img.clip.transform;
|
|
141
|
+
out.push(`<clipPath id="${id}">` + img.clip.paths.map((p) => `<path d="${pathData(p.segments)}" transform="matrix(${fmt(a)} ${fmt(b)} ${fmt(c)} ${fmt(d)} ${fmt(e)} ${fmt(f)})"/>`).join("") + `</clipPath>`, `<g clip-path="url(#${id})">`);
|
|
142
|
+
clipped = true;
|
|
143
|
+
}
|
|
144
|
+
emitPlaced(out, {
|
|
145
|
+
resourceName: img.imageResourceName,
|
|
146
|
+
x: img.x,
|
|
147
|
+
y: img.y,
|
|
148
|
+
width: img.width,
|
|
149
|
+
height: img.height,
|
|
150
|
+
...img.crop ? { crop: img.crop } : {},
|
|
151
|
+
...img.rotationDeg !== void 0 ? { rotationDeg: img.rotationDeg } : {},
|
|
152
|
+
...img.flipH === true ? { flipH: true } : {},
|
|
153
|
+
...img.flipV === true ? { flipV: true } : {},
|
|
154
|
+
...img.alpha !== void 0 ? { alpha: img.alpha } : {}
|
|
155
|
+
}, ctx.laid, ctx.ids, filter, img.wash);
|
|
156
|
+
if (clipped) out.push("</g>");
|
|
157
|
+
}
|
|
158
|
+
/**
|
|
159
|
+
* A picture in its box, as the PDF places one: turned about the box's centre
|
|
160
|
+
* (clockwise, as DrawingML measures), mirrored about it, and — cropped by
|
|
161
|
+
* `a:srcRect` (§20.1.8.55) — drawn larger than the box with the box as its clip,
|
|
162
|
+
* so the part that was kept fills it. A wash (§14.1.2.10) is the veil laid over
|
|
163
|
+
* it.
|
|
164
|
+
*/
|
|
165
|
+
function emitPlaced(out, placed, laid, ids, filter, wash) {
|
|
166
|
+
const href = imageHref(placed.resourceName, laid);
|
|
167
|
+
if (!href) return;
|
|
168
|
+
const { x, y, width, height } = placed;
|
|
169
|
+
const cx = x + width / 2;
|
|
170
|
+
const cy = y + height / 2;
|
|
171
|
+
const turns = [];
|
|
172
|
+
if (placed.rotationDeg) turns.push(`rotate(${fmt(placed.rotationDeg)} ${fmt(cx)} ${fmt(cy)})`);
|
|
173
|
+
if (placed.flipH === true || placed.flipV === true) {
|
|
174
|
+
const sx = placed.flipH === true ? -1 : 1;
|
|
175
|
+
const sy = placed.flipV === true ? -1 : 1;
|
|
176
|
+
turns.push(`matrix(${String(sx)} 0 0 ${String(sy)} ${fmt(sx === -1 ? 2 * cx : 0)} ${fmt(sy === -1 ? 2 * cy : 0)})`);
|
|
115
177
|
}
|
|
116
|
-
|
|
178
|
+
const open = turns.length > 0 ? `<g transform="${turns.join(" ")}">` : "";
|
|
179
|
+
if (open) out.push(open);
|
|
180
|
+
const crop = placed.crop;
|
|
181
|
+
let clipId;
|
|
182
|
+
let drawn = {
|
|
183
|
+
x,
|
|
184
|
+
y,
|
|
185
|
+
width,
|
|
186
|
+
height
|
|
187
|
+
};
|
|
188
|
+
if (crop) {
|
|
189
|
+
const keptW = 1 - crop.left - crop.right;
|
|
190
|
+
const keptH = 1 - crop.top - crop.bottom;
|
|
191
|
+
if (keptW > 0 && keptH > 0) {
|
|
192
|
+
const fullW = width / keptW;
|
|
193
|
+
const fullH = height / keptH;
|
|
194
|
+
drawn = {
|
|
195
|
+
x: x - crop.left * fullW,
|
|
196
|
+
y: y - crop.top * fullH,
|
|
197
|
+
width: fullW,
|
|
198
|
+
height: fullH
|
|
199
|
+
};
|
|
200
|
+
clipId = `crop${String(ids.n++)}`;
|
|
201
|
+
out.push(`<clipPath id="${clipId}"><rect x="${fmt(x)}" y="${fmt(y)}" width="${fmt(width)}" height="${fmt(height)}"/></clipPath>`);
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
const alpha = placed.alpha !== void 0 && placed.alpha < 1 ? ` opacity="${fmt(placed.alpha)}"` : "";
|
|
205
|
+
out.push(`<image x="${fmt(drawn.x)}" y="${fmt(drawn.y)}" width="${fmt(drawn.width)}" height="${fmt(drawn.height)}" href="${href}" preserveAspectRatio="none"${clipId ? ` clip-path="url(#${clipId})"` : ""}${filter ? ` filter="url(#${filter})"` : ""}${alpha}/>`);
|
|
206
|
+
const veil = washVeil(wash);
|
|
207
|
+
if (veil) {
|
|
208
|
+
const grey = Math.round(veil.grey * 255).toString(16).padStart(2, "0");
|
|
209
|
+
out.push(`<rect x="${fmt(x)}" y="${fmt(y)}" width="${fmt(width)}" height="${fmt(height)}" fill="#${grey}${grey}${grey}" opacity="${fmt(veil.alpha)}"/>`);
|
|
210
|
+
}
|
|
211
|
+
if (open) out.push("</g>");
|
|
117
212
|
}
|
|
118
213
|
/**
|
|
119
214
|
* One page item, whatever its kind — for the runs that paint in their own order
|
|
120
215
|
* (what stands behind the content, and a picture's own primitives) rather than
|
|
121
216
|
* in the by-kind passes.
|
|
122
217
|
*
|
|
123
|
-
* @param out
|
|
124
|
-
* @param item
|
|
125
|
-
* @param
|
|
126
|
-
* @param losses Where an unrenderable item records itself.
|
|
127
|
-
* @param idc The shared id counter for generated defs.
|
|
218
|
+
* @param out The SVG fragment sink.
|
|
219
|
+
* @param item The item to draw.
|
|
220
|
+
* @param ctx The document, its glyphs, its id counter and its drawing.
|
|
128
221
|
*/
|
|
129
|
-
function emitPageItem(out, item,
|
|
222
|
+
function emitPageItem(out, item, ctx) {
|
|
130
223
|
if (item.type === "shape") {
|
|
131
|
-
emitShape(out, item.shape,
|
|
224
|
+
emitShape(out, item.shape, ctx.ids);
|
|
132
225
|
return;
|
|
133
226
|
}
|
|
134
227
|
if (item.type === "line") {
|
|
135
|
-
|
|
228
|
+
emitSvgLine(out, item, ctx);
|
|
136
229
|
return;
|
|
137
230
|
}
|
|
138
231
|
if (item.type === "image") {
|
|
139
|
-
emitImage(out, item,
|
|
232
|
+
emitImage(out, item, ctx);
|
|
140
233
|
return;
|
|
141
234
|
}
|
|
142
235
|
if (item.type === "fill") out.push(`<rect x="${fmt(item.x)}" y="${fmt(item.y)}" width="${fmt(item.width)}" height="${fmt(item.height)}" fill="#${item.fillColorHex}"/>`);
|
|
143
236
|
}
|
|
144
|
-
function
|
|
145
|
-
const clip = item.clip;
|
|
146
|
-
if (clip) {
|
|
147
|
-
const id = `clip${idc.n++}`;
|
|
148
|
-
out.push(`<clipPath id="${id}"><rect x="${fmt(clip.x)}" y="${fmt(clip.y)}" width="${fmt(clip.width)}" height="${fmt(clip.height)}"/></clipPath>`, `<g clip-path="url(#${id})">`);
|
|
149
|
-
}
|
|
150
|
-
const y = item.baselineY;
|
|
151
|
-
let x = item.originX;
|
|
152
|
-
const rot = item.rotationDeg ? ` transform="rotate(${fmt(-item.rotationDeg)} ${fmt(item.originX)} ${fmt(item.baselineY)})"` : "";
|
|
153
|
-
for (const tok of item.line.tokens) {
|
|
154
|
-
if (tok.kind === "image") {
|
|
155
|
-
x += tok.widthPt;
|
|
156
|
-
continue;
|
|
157
|
-
}
|
|
158
|
-
if (tok.kind === "math") {
|
|
159
|
-
losses.push({
|
|
160
|
-
severity: "dropped",
|
|
161
|
-
feature: FEATURES.math,
|
|
162
|
-
detail: "inline math box not rendered by the SVG writer (v0)"
|
|
163
|
-
});
|
|
164
|
-
x += tok.widthPt;
|
|
165
|
-
continue;
|
|
166
|
-
}
|
|
167
|
-
if (tok.text.trim().length > 0) out.push(`<text x="${fmt(x)}" y="${fmt(y)}"${rot} font-family="sans-serif" font-size="${fmt(tok.fontSizePt)}" fill="#${tok.resolvedRun.colorHex}">${escapeXml(tok.text)}</text>`);
|
|
168
|
-
x += tok.widthPt;
|
|
169
|
-
}
|
|
170
|
-
if (clip) out.push("</g>");
|
|
171
|
-
}
|
|
172
|
-
function emitShape(out, shape, idc) {
|
|
237
|
+
function emitShape(out, shape, ids) {
|
|
173
238
|
const [a, b, c, d, e, f] = shape.transform;
|
|
174
239
|
const transform = `matrix(${fmt(a)} ${fmt(b)} ${fmt(c)} ${fmt(d)} ${fmt(e)} ${fmt(f)})`;
|
|
175
240
|
let fill;
|
|
176
241
|
if (shape.fillGradient) {
|
|
177
|
-
const id = `grad${
|
|
242
|
+
const id = `grad${String(ids.n++)}`;
|
|
178
243
|
out.push(gradientSvgDef(id, shape.fillGradient));
|
|
179
244
|
fill = `url(#${id})`;
|
|
180
245
|
} else fill = shape.fillColorHex ? `#${shape.fillColorHex}` : "none";
|
|
181
|
-
const
|
|
246
|
+
const fillAlpha = shape.fillAlpha !== void 0 && shape.fillAlpha < 1 && fill !== "none" ? ` fill-opacity="${fmt(shape.fillAlpha)}"` : "";
|
|
247
|
+
const s = shape.stroke;
|
|
248
|
+
const stroke = s ? ` stroke="#${s.colorHex}" stroke-width="${fmt(s.widthPt)}"` + (s.cap !== void 0 ? ` stroke-linecap="${s.cap}"` : "") + (s.join !== void 0 ? ` stroke-linejoin="${s.join}"` : "") + (s.dash && s.dash.length > 0 ? ` stroke-dasharray="${s.dash.map(fmt).join(" ")}"` : "") : "";
|
|
182
249
|
const shadow = shape.shadow;
|
|
183
250
|
if (shadow) {
|
|
184
|
-
const id = `shadow${
|
|
251
|
+
const id = `shadow${String(ids.n++)}`;
|
|
185
252
|
if (shadow.blurPt > 0) out.push(`<filter id="${id}" x="-50%" y="-50%" width="200%" height="200%"><feGaussianBlur stdDeviation="${fmt(shadow.blurPt / 2)}"/></filter>`);
|
|
186
253
|
const shTransform = `matrix(${fmt(a)} ${fmt(b)} ${fmt(c)} ${fmt(d)} ${fmt(e + shadow.dxPt)} ${fmt(f + shadow.dyPt)})`;
|
|
187
254
|
out.push(`<g opacity="${fmt(shadow.alpha)}"${shadow.blurPt > 0 ? ` filter="url(#${id})"` : ""}>`);
|
|
@@ -194,7 +261,7 @@ function emitShape(out, shape, idc) {
|
|
|
194
261
|
for (const path of shape.paths) {
|
|
195
262
|
const d2 = pathData(path.segments);
|
|
196
263
|
const rule = path.fillRule === "evenodd" ? " fill-rule=\"evenodd\"" : "";
|
|
197
|
-
out.push(`<path d="${d2}" fill="${fill}"${rule}${stroke} transform="${transform}"/>`);
|
|
264
|
+
out.push(`<path d="${d2}" fill="${fill}"${fillAlpha}${rule}${stroke} transform="${transform}"/>`);
|
|
198
265
|
}
|
|
199
266
|
}
|
|
200
267
|
function pathData(segments) {
|
|
@@ -209,9 +276,6 @@ function imageHref(resourceName, laid) {
|
|
|
209
276
|
return `data:${res.prepared.mimeType};base64,${toBase64(bytes)}`;
|
|
210
277
|
}
|
|
211
278
|
}
|
|
212
|
-
function escapeXml(s) {
|
|
213
|
-
return s.replaceAll("&", "&").replaceAll("<", "<").replaceAll(">", ">").replaceAll("\"", """);
|
|
214
|
-
}
|
|
215
279
|
function fmt(n) {
|
|
216
280
|
const r = Math.round(n * 100) / 100;
|
|
217
281
|
return Object.is(r, -0) ? "0" : String(r);
|
|
@@ -178,7 +178,8 @@ function readDoc(bytes) {
|
|
|
178
178
|
},
|
|
179
179
|
styles: EMPTY_STYLE_SHEET,
|
|
180
180
|
resources,
|
|
181
|
-
...headersFooters.size > 0 ? { headersFooters } : {}
|
|
181
|
+
...headersFooters.size > 0 ? { headersFooters } : {},
|
|
182
|
+
typesetBy: "word"
|
|
182
183
|
},
|
|
183
184
|
losses: [content.encrypted ? DOC_ENCRYPTED_LOSS : DOC_TEXT_LOSS]
|
|
184
185
|
};
|
|
@@ -406,7 +407,10 @@ function toParaProperties(p) {
|
|
|
406
407
|
...p.indentRightTwips !== void 0 ? { indentRight: pt(p.indentRightTwips / 20) } : {},
|
|
407
408
|
...p.indentFirstTwips !== void 0 ? { indentFirstLine: pt(p.indentFirstTwips / 20) } : {},
|
|
408
409
|
...p.spaceBeforeTwips !== void 0 ? { spacingBefore: pt(p.spaceBeforeTwips / 20) } : {},
|
|
409
|
-
...p.spaceAfterTwips !== void 0 ? { spacingAfter: pt(p.spaceAfterTwips / 20) } : {}
|
|
410
|
+
...p.spaceAfterTwips !== void 0 ? { spacingAfter: pt(p.spaceAfterTwips / 20) } : {},
|
|
411
|
+
...p.widowControl !== void 0 ? { widowControl: p.widowControl } : {},
|
|
412
|
+
...p.keepNext !== void 0 ? { keepNext: p.keepNext } : {},
|
|
413
|
+
...p.keepLines !== void 0 ? { keepLines: p.keepLines } : {}
|
|
410
414
|
};
|
|
411
415
|
}
|
|
412
416
|
function underlineStyle(kul) {
|
|
@@ -74,6 +74,12 @@ export interface DocParaProps {
|
|
|
74
74
|
readonly listIlfo?: number;
|
|
75
75
|
/** `sprmPIlvl` — list level (0–8). */
|
|
76
76
|
readonly listIlvl?: number;
|
|
77
|
+
/** `sprmPFWidowControl` — no first or last line left alone on a page. */
|
|
78
|
+
readonly widowControl?: boolean;
|
|
79
|
+
/** `sprmPFKeepFollow` — on the same page as the start of the next paragraph. */
|
|
80
|
+
readonly keepNext?: boolean;
|
|
81
|
+
/** `sprmPFKeep` — every line on one page. */
|
|
82
|
+
readonly keepLines?: boolean;
|
|
77
83
|
}
|
|
78
84
|
/** One maximal same-formatting run of text, or an embedded picture. */
|
|
79
85
|
export interface DocRun {
|