@sliiu/present-renderer 0.0.0-stage → 0.1.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/COPYRIGHT +5 -0
- package/LICENSE +661 -0
- package/THIRD-PARTY-NOTICES.md +221 -0
- package/dist/cjs/arrow-geom.js +38 -0
- package/dist/cjs/canvas-backend.js +1368 -0
- package/dist/cjs/cell-borders.js +25 -0
- package/dist/cjs/chart-canvas.js +1059 -0
- package/dist/cjs/chart-ex-render.js +275 -0
- package/dist/cjs/chart-geom.js +313 -0
- package/dist/cjs/chart-svg.js +1110 -0
- package/dist/cjs/compose.js +74 -0
- package/dist/cjs/effect-clip.js +652 -0
- package/dist/cjs/entrance-hide.js +63 -0
- package/dist/cjs/font-stack.js +103 -0
- package/dist/cjs/hit-geom.js +50 -0
- package/dist/cjs/hybrid-backend.js +93 -0
- package/dist/cjs/hybrid-segments.js +54 -0
- package/dist/cjs/index.js +70 -0
- package/dist/cjs/linked-src.js +116 -0
- package/dist/cjs/metafile-preview.js +142 -0
- package/dist/cjs/model3d-frame.js +199 -0
- package/dist/cjs/model3d-render.js +78 -0
- package/dist/cjs/morph-transition.js +57 -0
- package/dist/cjs/node-affine.js +79 -0
- package/dist/cjs/ole-render.js +291 -0
- package/dist/cjs/package.json +1 -0
- package/dist/cjs/paint-canvas.js +701 -0
- package/dist/cjs/paint-svg.js +674 -0
- package/dist/cjs/scene-node.js +762 -0
- package/dist/cjs/selection.js +130 -0
- package/dist/cjs/shape-effects.js +66 -0
- package/dist/cjs/shape-front.js +38 -0
- package/dist/cjs/stage-chrome.js +15 -0
- package/dist/cjs/svg-backend.js +2172 -0
- package/dist/cjs/text-autofit.js +48 -0
- package/dist/cjs/text-hit.js +284 -0
- package/dist/cjs/text-layout.js +408 -0
- package/dist/cjs/text-shared.js +238 -0
- package/dist/cjs/text-stacked.js +82 -0
- package/dist/cjs/text-warp.js +377 -0
- package/dist/cjs/text-wordart.js +33 -0
- package/dist/cjs/video-frame.js +113 -0
- package/dist/cjs/webgl-model3d-backend.js +285 -0
- package/dist/cjs/webgl-shape-backend.js +360 -0
- package/dist/esm/arrow-geom.js +33 -0
- package/dist/esm/canvas-backend.js +1363 -0
- package/dist/esm/cell-borders.js +21 -0
- package/dist/esm/chart-canvas.js +1056 -0
- package/dist/esm/chart-ex-render.js +261 -0
- package/dist/esm/chart-geom.js +296 -0
- package/dist/esm/chart-svg.js +1107 -0
- package/dist/esm/compose.js +71 -0
- package/dist/esm/effect-clip.js +648 -0
- package/dist/esm/entrance-hide.js +59 -0
- package/dist/esm/font-stack.js +100 -0
- package/dist/esm/hit-geom.js +45 -0
- package/dist/esm/hybrid-backend.js +89 -0
- package/dist/esm/hybrid-segments.js +51 -0
- package/dist/esm/index.js +24 -0
- package/dist/esm/linked-src.js +109 -0
- package/dist/esm/metafile-preview.js +135 -0
- package/dist/esm/model3d-frame.js +158 -0
- package/dist/esm/model3d-render.js +73 -0
- package/dist/esm/morph-transition.js +54 -0
- package/dist/esm/node-affine.js +71 -0
- package/dist/esm/ole-render.js +286 -0
- package/dist/esm/paint-canvas.js +693 -0
- package/dist/esm/paint-svg.js +670 -0
- package/dist/esm/scene-node.js +750 -0
- package/dist/esm/selection.js +122 -0
- package/dist/esm/shape-effects.js +60 -0
- package/dist/esm/shape-front.js +35 -0
- package/dist/esm/stage-chrome.js +12 -0
- package/dist/esm/svg-backend.js +2168 -0
- package/dist/esm/text-autofit.js +44 -0
- package/dist/esm/text-hit.js +280 -0
- package/dist/esm/text-layout.js +400 -0
- package/dist/esm/text-shared.js +221 -0
- package/dist/esm/text-stacked.js +78 -0
- package/dist/esm/text-warp.js +371 -0
- package/dist/esm/text-wordart.js +30 -0
- package/dist/esm/video-frame.js +109 -0
- package/dist/esm/webgl-model3d-backend.js +248 -0
- package/dist/esm/webgl-shape-backend.js +323 -0
- package/dist/types/arrow-geom.d.ts +10 -0
- package/dist/types/canvas-backend.d.ts +55 -0
- package/dist/types/cell-borders.d.ts +9 -0
- package/dist/types/chart-canvas.d.ts +2 -0
- package/dist/types/chart-ex-render.d.ts +79 -0
- package/dist/types/chart-geom.d.ts +112 -0
- package/dist/types/chart-svg.d.ts +2 -0
- package/dist/types/compose.d.ts +2 -0
- package/dist/types/effect-clip.d.ts +10 -0
- package/dist/types/entrance-hide.d.ts +16 -0
- package/dist/types/font-stack.d.ts +9 -0
- package/dist/types/hit-geom.d.ts +15 -0
- package/dist/types/hybrid-backend.d.ts +21 -0
- package/dist/types/hybrid-segments.d.ts +11 -0
- package/dist/types/index.d.ts +24 -0
- package/dist/types/linked-src.d.ts +13 -0
- package/dist/types/metafile-preview.d.ts +11 -0
- package/dist/types/model3d-frame.d.ts +8 -0
- package/dist/types/model3d-render.d.ts +4 -0
- package/dist/types/morph-transition.d.ts +15 -0
- package/dist/types/node-affine.d.ts +22 -0
- package/dist/types/ole-render.d.ts +7 -0
- package/dist/types/paint-canvas.d.ts +22 -0
- package/dist/types/paint-svg.d.ts +19 -0
- package/dist/types/scene-node.d.ts +128 -0
- package/dist/types/selection.d.ts +22 -0
- package/dist/types/shape-effects.d.ts +13 -0
- package/dist/types/shape-front.d.ts +4 -0
- package/dist/types/stage-chrome.d.ts +8 -0
- package/dist/types/svg-backend.d.ts +88 -0
- package/dist/types/text-autofit.d.ts +7 -0
- package/dist/types/text-hit.d.ts +7 -0
- package/dist/types/text-layout.d.ts +35 -0
- package/dist/types/text-shared.d.ts +86 -0
- package/dist/types/text-stacked.d.ts +11 -0
- package/dist/types/text-warp.d.ts +37 -0
- package/dist/types/text-wordart.d.ts +18 -0
- package/dist/types/video-frame.d.ts +2 -0
- package/dist/types/webgl-model3d-backend.d.ts +25 -0
- package/dist/types/webgl-shape-backend.d.ts +33 -0
- package/package.json +33 -4
- package/README.md +0 -3
|
@@ -0,0 +1,408 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.isEastAsianChar = isEastAsianChar;
|
|
4
|
+
exports.mapVisualAlign = mapVisualAlign;
|
|
5
|
+
exports.mapVisualAlignWithAnchorCtr = mapVisualAlignWithAnchorCtr;
|
|
6
|
+
exports.applyKinsoku = applyKinsoku;
|
|
7
|
+
exports.glueUnbrokenEastAsian = glueUnbrokenEastAsian;
|
|
8
|
+
exports.wrapParagraphIntoLines = wrapParagraphIntoLines;
|
|
9
|
+
const font_stack_1 = require("./font-stack");
|
|
10
|
+
const core_1 = require("@sliiu/present-core");
|
|
11
|
+
const text_shared_1 = require("./text-shared");
|
|
12
|
+
// 与 backend 保持一致的"东亚 / CJK"判定(用于 eaVert 直立判断与 wrapping 时的"逐字可断"决策)。
|
|
13
|
+
function isEastAsianChar(cp) {
|
|
14
|
+
if (cp >= 0x1100 && cp <= 0x11ff)
|
|
15
|
+
return true; // Hangul Jamo
|
|
16
|
+
if (cp >= 0x2e80 && cp <= 0x2fff)
|
|
17
|
+
return true; // CJK Radicals
|
|
18
|
+
if (cp >= 0x3000 && cp <= 0x33ff)
|
|
19
|
+
return true; // CJK Symbols / Hiragana / Katakana / Bopomofo
|
|
20
|
+
if (cp >= 0x3400 && cp <= 0x9fff)
|
|
21
|
+
return true; // CJK Unified Ideographs
|
|
22
|
+
if (cp >= 0xac00 && cp <= 0xd7af)
|
|
23
|
+
return true; // Hangul Syllables
|
|
24
|
+
if (cp >= 0xf900 && cp <= 0xfaff)
|
|
25
|
+
return true; // CJK Compat Ideographs
|
|
26
|
+
if (cp >= 0xff00 && cp <= 0xffef)
|
|
27
|
+
return true; // Halfwidth / Fullwidth
|
|
28
|
+
if (cp >= 0x20000 && cp <= 0x3ffff)
|
|
29
|
+
return true; // CJK Extensions B+
|
|
30
|
+
return false;
|
|
31
|
+
}
|
|
32
|
+
// ECMA-376 §21.1.2.2.6 pPr@algn — 段落水平对齐。RTL 时 start/end 视觉互换。
|
|
33
|
+
function mapVisualAlign(align, rtl) {
|
|
34
|
+
if (!rtl)
|
|
35
|
+
return align;
|
|
36
|
+
if (align === "start")
|
|
37
|
+
return "end";
|
|
38
|
+
if (align === "end")
|
|
39
|
+
return "start";
|
|
40
|
+
return align;
|
|
41
|
+
}
|
|
42
|
+
// ECMA-376 §21.1.2.1.1 bodyPr@anchorCtr — 仅当 true 且段落数 == 1 时,
|
|
43
|
+
// 按实际行宽水平居中,忽略该段 pPr@algn。多段时规范明确 non-applicable。
|
|
44
|
+
function mapVisualAlignWithAnchorCtr(s, p) {
|
|
45
|
+
const rtl = !!p.rtl;
|
|
46
|
+
// 文本框 bodyPr@anchorCtr:仅单段时按行宽居中。
|
|
47
|
+
// 表格 tcPr@anchorCtr:单元格内所有段落水平居中(与段落数无关)。
|
|
48
|
+
if (s.anchorCtr && (s.cellMeta || (s.paragraphs?.length ?? 0) === 1)) {
|
|
49
|
+
return mapVisualAlign("middle", rtl);
|
|
50
|
+
}
|
|
51
|
+
return mapVisualAlign(p.align ?? "start", rtl);
|
|
52
|
+
}
|
|
53
|
+
// 视为"可断行空白"的字符:ASCII 空格 / 全角空格 \u3000 / 零宽空格 \u200B /
|
|
54
|
+
// Tab 单独成 token(制表位)。NBSP 仍归为可见 word。
|
|
55
|
+
function isBreakSpace(ch) {
|
|
56
|
+
return ch === " " || ch === "\u3000" || ch === "\u200B";
|
|
57
|
+
}
|
|
58
|
+
// ECMA-376 §19.2.1.20 kinsoku — PowerPoint's default East Asian line-break prohibitions:
|
|
59
|
+
// closing punctuation may not start a line, opening brackets may not end one. The tokenizer
|
|
60
|
+
// emits one token per CJK character, so without this "。" happily drops onto a line of its own.
|
|
61
|
+
// Applied whenever the neighbour is a CJK token (a deck-level <p:kinsoku> override only
|
|
62
|
+
// widens the sets; the defaults are what matters for generated and typical Chinese text).
|
|
63
|
+
const KINSOKU_NO_START = new Set(Array.from("!%),.:;>?]}¢¨°·ˇˉ―‖’”…‰′″›℃∶、。〃〉》」』】〕〗〞︶︺︾﹀﹄﹚﹜﹞!"%'),.:;?]`|}~¢"));
|
|
64
|
+
const KINSOKU_NO_END = new Set(Array.from("$(£¥·‘“〈《「『【〔〖〝﹙﹛$([{£¥"));
|
|
65
|
+
function isSingleChar(t, set) {
|
|
66
|
+
return t.kind !== "ws" && t.kind !== "tab" && set.has(t.text);
|
|
67
|
+
}
|
|
68
|
+
/** Glue prohibited line-start / line-end characters to their CJK neighbour so they wrap together. */
|
|
69
|
+
function applyKinsoku(tokens) {
|
|
70
|
+
const out = [];
|
|
71
|
+
for (const t of tokens) {
|
|
72
|
+
const prev = out[out.length - 1];
|
|
73
|
+
if (prev && prev.kind !== "ws" && prev.kind !== "tab" && t.kind !== "ws" && t.kind !== "tab") {
|
|
74
|
+
const glueStart = isSingleChar(t, KINSOKU_NO_START) && (prev.kind === "cjk" || t.kind === "cjk");
|
|
75
|
+
const glueEnd = isSingleChar(prev, KINSOKU_NO_END) && (prev.kind === "cjk" || t.kind === "cjk");
|
|
76
|
+
if (glueStart || glueEnd) {
|
|
77
|
+
prev.text += t.text;
|
|
78
|
+
continue;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
out.push({ ...t });
|
|
82
|
+
}
|
|
83
|
+
return out;
|
|
84
|
+
}
|
|
85
|
+
/** 标点留在前一段,下一段可以从这里换行。 */
|
|
86
|
+
const EA_BREAK_AFTER = new Set(Array.from(",。!?;、:,.!?;:…"));
|
|
87
|
+
function endsWithEaBreak(text) {
|
|
88
|
+
const chars = Array.from(text);
|
|
89
|
+
const last = chars[chars.length - 1];
|
|
90
|
+
return !!last && EA_BREAK_AFTER.has(last);
|
|
91
|
+
}
|
|
92
|
+
/** 行末可以挂出边的收口标点,和禁则里不能出现在行首的那一组相同。 */
|
|
93
|
+
function splitHangingSuffix(text) {
|
|
94
|
+
const chars = Array.from(text);
|
|
95
|
+
let i = chars.length;
|
|
96
|
+
while (i > 0 && KINSOKU_NO_START.has(chars[i - 1]))
|
|
97
|
+
i--;
|
|
98
|
+
if (i === chars.length)
|
|
99
|
+
return { body: text, hang: "" };
|
|
100
|
+
return { body: chars.slice(0, i).join(""), hang: chars.slice(i).join("") };
|
|
101
|
+
}
|
|
102
|
+
/** eaLnBrk 为假:相邻汉字粘成一块,标点后面仍可断。调用前先走过禁则。 */
|
|
103
|
+
function glueUnbrokenEastAsian(tokens) {
|
|
104
|
+
const out = [];
|
|
105
|
+
for (const t of tokens) {
|
|
106
|
+
const prev = out[out.length - 1];
|
|
107
|
+
if (prev && prev.kind === "cjk" && t.kind === "cjk" && !endsWithEaBreak(prev.text)) {
|
|
108
|
+
prev.text += t.text;
|
|
109
|
+
continue;
|
|
110
|
+
}
|
|
111
|
+
out.push({ ...t });
|
|
112
|
+
}
|
|
113
|
+
return out;
|
|
114
|
+
}
|
|
115
|
+
function tokenizeText(text, eaLnBrk) {
|
|
116
|
+
const tokens = applyKinsoku(tokenizeRaw(text));
|
|
117
|
+
return eaLnBrk === false ? glueUnbrokenEastAsian(tokens) : tokens;
|
|
118
|
+
}
|
|
119
|
+
function tokenizeRaw(text) {
|
|
120
|
+
if (!text)
|
|
121
|
+
return [];
|
|
122
|
+
const out = [];
|
|
123
|
+
let buf = "";
|
|
124
|
+
let bufKind = null;
|
|
125
|
+
const flush = () => {
|
|
126
|
+
if (buf) {
|
|
127
|
+
out.push({ text: buf, kind: bufKind });
|
|
128
|
+
buf = "";
|
|
129
|
+
bufKind = null;
|
|
130
|
+
}
|
|
131
|
+
};
|
|
132
|
+
for (const ch of Array.from(text)) {
|
|
133
|
+
if (ch === "\t") {
|
|
134
|
+
flush();
|
|
135
|
+
out.push({ text: "\t", kind: "tab" });
|
|
136
|
+
continue;
|
|
137
|
+
}
|
|
138
|
+
const cp = ch.codePointAt(0);
|
|
139
|
+
if (isEastAsianChar(cp)) {
|
|
140
|
+
flush();
|
|
141
|
+
out.push({ text: ch, kind: "cjk" });
|
|
142
|
+
continue;
|
|
143
|
+
}
|
|
144
|
+
const isWs = isBreakSpace(ch);
|
|
145
|
+
const targetKind = isWs ? "ws" : "word";
|
|
146
|
+
if (bufKind && bufKind !== targetKind)
|
|
147
|
+
flush();
|
|
148
|
+
buf += ch;
|
|
149
|
+
bufKind = targetKind;
|
|
150
|
+
}
|
|
151
|
+
flush();
|
|
152
|
+
return out;
|
|
153
|
+
}
|
|
154
|
+
function nextTabStop(x, p) {
|
|
155
|
+
const stops = (p.tabStops ?? [])
|
|
156
|
+
.filter((t) => t.pos > x + 0.5)
|
|
157
|
+
.sort((a, b) => a.pos - b.pos);
|
|
158
|
+
if (stops.length) {
|
|
159
|
+
const s = stops[0];
|
|
160
|
+
return { pos: s.pos, align: s.align ?? "l" };
|
|
161
|
+
}
|
|
162
|
+
const sz = p.defTabSz && p.defTabSz > 1 ? p.defTabSz : 36;
|
|
163
|
+
return { pos: Math.floor(x / sz + 1) * sz, align: "l" };
|
|
164
|
+
}
|
|
165
|
+
function tabAlignedAdvance(x, p, fieldWidth, decWidth) {
|
|
166
|
+
const stop = nextTabStop(x, p);
|
|
167
|
+
let target = stop.pos;
|
|
168
|
+
if (stop.align === "r")
|
|
169
|
+
target = stop.pos - fieldWidth;
|
|
170
|
+
else if (stop.align === "ctr")
|
|
171
|
+
target = stop.pos - fieldWidth / 2;
|
|
172
|
+
else if (stop.align === "dec")
|
|
173
|
+
target = stop.pos - decWidth;
|
|
174
|
+
return Math.max(1, target - x);
|
|
175
|
+
}
|
|
176
|
+
function decimalPrefixWidth(text, fullWidth, measure) {
|
|
177
|
+
const i = text.search(/[.,]/);
|
|
178
|
+
if (i < 0)
|
|
179
|
+
return fullWidth;
|
|
180
|
+
return measure(text.slice(0, i));
|
|
181
|
+
}
|
|
182
|
+
// 浏览器 canvas 在 measureText 里对 \t 的处理不一致。含 Tab 时按制表位展开。
|
|
183
|
+
function measureTextWithWhitespace(ctx, text, startX = 0, para) {
|
|
184
|
+
if (!text.includes("\t"))
|
|
185
|
+
return ctx.measureText(text).width;
|
|
186
|
+
if (!para) {
|
|
187
|
+
const spaceW = ctx.measureText(" ").width;
|
|
188
|
+
let x = startX;
|
|
189
|
+
let buf = "";
|
|
190
|
+
for (const ch of text) {
|
|
191
|
+
if (ch === "\t") {
|
|
192
|
+
if (buf) {
|
|
193
|
+
x += ctx.measureText(buf).width;
|
|
194
|
+
buf = "";
|
|
195
|
+
}
|
|
196
|
+
x += spaceW * 4;
|
|
197
|
+
}
|
|
198
|
+
else {
|
|
199
|
+
buf += ch;
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
if (buf)
|
|
203
|
+
x += ctx.measureText(buf).width;
|
|
204
|
+
return x - startX;
|
|
205
|
+
}
|
|
206
|
+
const parts = text.split("\t");
|
|
207
|
+
let x = startX;
|
|
208
|
+
for (let i = 0; i < parts.length; i++) {
|
|
209
|
+
const field = parts[i];
|
|
210
|
+
if (i > 0) {
|
|
211
|
+
const fw = ctx.measureText(field).width;
|
|
212
|
+
const dw = decimalPrefixWidth(field, fw, (s) => ctx.measureText(s).width);
|
|
213
|
+
x += tabAlignedAdvance(x, para, fw, dw);
|
|
214
|
+
}
|
|
215
|
+
if (field)
|
|
216
|
+
x += ctx.measureText(field).width;
|
|
217
|
+
}
|
|
218
|
+
return x - startX;
|
|
219
|
+
}
|
|
220
|
+
function setMeasureFontFor(ctx, run, fs) {
|
|
221
|
+
const it = run.italic ? "italic " : "";
|
|
222
|
+
const fw = run.fontWeight ?? "normal";
|
|
223
|
+
const fa = (0, font_stack_1.withFontFallback)(run.fontFamily);
|
|
224
|
+
ctx.font = `${it}${fw} ${fs}px ${fa}`;
|
|
225
|
+
}
|
|
226
|
+
// 把一个段落根据 maxWidth 贪心打包成多行 visual line;保证返回至少一行。
|
|
227
|
+
// firstLineMaxWidth 用来描述"首行可用宽度"(通常等于 maxWidth;若有 first-line indent
|
|
228
|
+
// 则减去 indent;hanging-indent 不影响 firstLineMaxWidth)。
|
|
229
|
+
// noWrap=true 时整段返回单行(OOXML wrap="none")。
|
|
230
|
+
function wrapParagraphIntoLines(p, maxWidth, fallbackFs, ctx, firstLineMaxWidth, noWrap, fieldCtx, compatLnSpc) {
|
|
231
|
+
const lineHFactor = (0, text_shared_1.lineHeightFactor)(p, compatLnSpc);
|
|
232
|
+
const lines = [];
|
|
233
|
+
let cur = { segments: [], width: 0, maxFs: 0, hang: 0 };
|
|
234
|
+
const flush = () => {
|
|
235
|
+
lines.push(cur);
|
|
236
|
+
cur = { segments: [], width: 0, maxFs: 0, hang: 0 };
|
|
237
|
+
};
|
|
238
|
+
const append = (run, text, width, fs) => {
|
|
239
|
+
if (!text && width <= 0)
|
|
240
|
+
return;
|
|
241
|
+
const last = cur.segments[cur.segments.length - 1];
|
|
242
|
+
if (text && last && last.run === run && last.text) {
|
|
243
|
+
last.text += text;
|
|
244
|
+
last.width += width;
|
|
245
|
+
}
|
|
246
|
+
else {
|
|
247
|
+
cur.segments.push({ run, text, width });
|
|
248
|
+
}
|
|
249
|
+
cur.width += width;
|
|
250
|
+
if (fs > cur.maxFs)
|
|
251
|
+
cur.maxFs = fs;
|
|
252
|
+
};
|
|
253
|
+
const limit = () => (lines.length === 0 ? firstLineMaxWidth : maxWidth);
|
|
254
|
+
const measureRunText = (run, text) => {
|
|
255
|
+
const fs = run.fontSize ?? fallbackFs;
|
|
256
|
+
setMeasureFontFor(ctx, run, fs);
|
|
257
|
+
const ls = run.letterSpacing ?? 0;
|
|
258
|
+
return ctx.measureText(text).width + ls * Array.from(text).length;
|
|
259
|
+
};
|
|
260
|
+
const fieldMetrics = (runIdx, restThisRun, closedInRun) => {
|
|
261
|
+
const chunks = [{ run: p.runs[runIdx], text: restThisRun }];
|
|
262
|
+
if (!closedInRun) {
|
|
263
|
+
for (let i = runIdx + 1; i < p.runs.length; i++) {
|
|
264
|
+
const run = p.runs[i];
|
|
265
|
+
const t = fieldCtx ? (0, core_1.runDisplayText)(run, fieldCtx) : run.text;
|
|
266
|
+
const k = t.indexOf("\t");
|
|
267
|
+
chunks.push({ run, text: k >= 0 ? t.slice(0, k) : t });
|
|
268
|
+
if (k >= 0)
|
|
269
|
+
break;
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
let width = 0;
|
|
273
|
+
let joined = "";
|
|
274
|
+
for (const c of chunks) {
|
|
275
|
+
width += measureRunText(c.run, c.text);
|
|
276
|
+
joined += c.text;
|
|
277
|
+
}
|
|
278
|
+
const decWidth = decimalPrefixWidth(joined, width, (s) => {
|
|
279
|
+
const first = chunks[0]?.run;
|
|
280
|
+
return first ? measureRunText(first, s) : ctx.measureText(s).width;
|
|
281
|
+
});
|
|
282
|
+
return { width, decWidth };
|
|
283
|
+
};
|
|
284
|
+
for (let ri = 0; ri < p.runs.length; ri++) {
|
|
285
|
+
const run = p.runs[ri];
|
|
286
|
+
const runText = fieldCtx ? (0, core_1.runDisplayText)(run, fieldCtx) : run.text;
|
|
287
|
+
const fs = run.fontSize ?? fallbackFs;
|
|
288
|
+
setMeasureFontFor(ctx, run, fs);
|
|
289
|
+
const ls = run.letterSpacing ?? 0;
|
|
290
|
+
if (noWrap) {
|
|
291
|
+
if (runText.includes("\t")) {
|
|
292
|
+
let x = cur.width;
|
|
293
|
+
let buf = "";
|
|
294
|
+
const flushBuf = () => {
|
|
295
|
+
if (!buf)
|
|
296
|
+
return;
|
|
297
|
+
const w = ctx.measureText(buf).width + ls * Array.from(buf).length;
|
|
298
|
+
append(run, buf, w, fs);
|
|
299
|
+
x += w;
|
|
300
|
+
buf = "";
|
|
301
|
+
};
|
|
302
|
+
const chars = Array.from(runText);
|
|
303
|
+
for (let ci = 0; ci < chars.length; ci++) {
|
|
304
|
+
if (chars[ci] === "\t") {
|
|
305
|
+
flushBuf();
|
|
306
|
+
const rest = chars.slice(ci + 1).join("");
|
|
307
|
+
const closed = rest.includes("\t");
|
|
308
|
+
const restField = closed ? rest.slice(0, rest.indexOf("\t")) : rest;
|
|
309
|
+
const { width, decWidth } = fieldMetrics(ri, restField, closed);
|
|
310
|
+
const adv = tabAlignedAdvance(x, p, width, decWidth);
|
|
311
|
+
append(run, "", adv, fs);
|
|
312
|
+
x += adv;
|
|
313
|
+
}
|
|
314
|
+
else {
|
|
315
|
+
buf += chars[ci];
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
flushBuf();
|
|
319
|
+
}
|
|
320
|
+
else {
|
|
321
|
+
const w = measureTextWithWhitespace(ctx, runText, cur.width, p) + ls * Array.from(runText).length;
|
|
322
|
+
append(run, runText, w, fs);
|
|
323
|
+
}
|
|
324
|
+
continue;
|
|
325
|
+
}
|
|
326
|
+
const tokens = tokenizeText(runText, p.eaLnBrk);
|
|
327
|
+
for (let ti = 0; ti < tokens.length; ti++) {
|
|
328
|
+
const tok = tokens[ti];
|
|
329
|
+
if (tok.kind === "tab") {
|
|
330
|
+
let rest = "";
|
|
331
|
+
let closed = false;
|
|
332
|
+
for (let k = ti + 1; k < tokens.length; k++) {
|
|
333
|
+
if (tokens[k].kind === "tab") {
|
|
334
|
+
closed = true;
|
|
335
|
+
break;
|
|
336
|
+
}
|
|
337
|
+
rest += tokens[k].text;
|
|
338
|
+
}
|
|
339
|
+
const { width, decWidth } = fieldMetrics(ri, rest, closed);
|
|
340
|
+
const adv = tabAlignedAdvance(cur.width, p, width, decWidth);
|
|
341
|
+
append(run, "", adv, fs);
|
|
342
|
+
continue;
|
|
343
|
+
}
|
|
344
|
+
const chars = Array.from(tok.text).length;
|
|
345
|
+
const tw = measureTextWithWhitespace(ctx, tok.text, cur.width, p) + ls * chars;
|
|
346
|
+
const lim = limit();
|
|
347
|
+
const splitWord = p.latinLnBrk === true && tok.kind === "word";
|
|
348
|
+
const keepWhole = (p.eaLnBrk === false && tok.kind === "cjk") ||
|
|
349
|
+
(tok.kind === "word" && p.latinLnBrk !== true);
|
|
350
|
+
if (p.hangingPunct && tok.kind !== "ws") {
|
|
351
|
+
const hung = splitHangingSuffix(tok.text);
|
|
352
|
+
if (hung.hang) {
|
|
353
|
+
const hangW = measureRunText(run, hung.hang);
|
|
354
|
+
const bodyW = Math.max(0, tw - hangW);
|
|
355
|
+
const bodyFits = cur.width + bodyW <= lim + 0.5;
|
|
356
|
+
const onlyHang = hung.body.length === 0 && cur.segments.length > 0;
|
|
357
|
+
if ((hung.body.length > 0 && bodyFits) || onlyHang) {
|
|
358
|
+
append(run, tok.text, tw, fs);
|
|
359
|
+
cur.hang += hangW;
|
|
360
|
+
continue;
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
const wouldOverflow = cur.width + tw > lim && cur.segments.length > 0;
|
|
365
|
+
if (wouldOverflow && !splitWord) {
|
|
366
|
+
flush();
|
|
367
|
+
if (tok.kind === "ws")
|
|
368
|
+
continue;
|
|
369
|
+
}
|
|
370
|
+
const lineW = limit();
|
|
371
|
+
if (!keepWhole && tok.kind !== "ws" && lineW > 0 && cur.width + tw > lineW) {
|
|
372
|
+
for (const ch of Array.from(tok.text)) {
|
|
373
|
+
const cw = measureTextWithWhitespace(ctx, ch) + ls;
|
|
374
|
+
const L = limit();
|
|
375
|
+
if (cur.width + cw > L && cur.segments.length > 0)
|
|
376
|
+
flush();
|
|
377
|
+
append(run, ch, cw, fs);
|
|
378
|
+
}
|
|
379
|
+
continue;
|
|
380
|
+
}
|
|
381
|
+
append(run, tok.text, tw, fs);
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
flush();
|
|
385
|
+
if (lines.length === 0)
|
|
386
|
+
lines.push({ segments: [], width: 0, hang: 0, maxFs: fallbackFs });
|
|
387
|
+
return lines.map((l) => {
|
|
388
|
+
const fs = l.maxFs || fallbackFs;
|
|
389
|
+
let height = p.lineHeightPx ?? fs * lineHFactor;
|
|
390
|
+
if (compatLnSpc && p.lineHeightPx === undefined && p.lineHeight === undefined) {
|
|
391
|
+
const m = ctx.measureText("Hg");
|
|
392
|
+
const a = m.actualBoundingBoxAscent;
|
|
393
|
+
const d = m.actualBoundingBoxDescent;
|
|
394
|
+
if (a && d)
|
|
395
|
+
height = a + d;
|
|
396
|
+
else
|
|
397
|
+
height = fs;
|
|
398
|
+
}
|
|
399
|
+
return {
|
|
400
|
+
segments: l.segments,
|
|
401
|
+
width: l.width,
|
|
402
|
+
hang: l.hang,
|
|
403
|
+
height,
|
|
404
|
+
ascent: height - fs * 0.25,
|
|
405
|
+
maxFs: fs,
|
|
406
|
+
};
|
|
407
|
+
});
|
|
408
|
+
}
|
|
@@ -0,0 +1,238 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.FALLBACK_FONT_SIZE = exports.DEFAULT_LINE_HEIGHT_FACTOR = exports.DEFAULT_TEXT_INSETS = void 0;
|
|
4
|
+
exports.vertRotateDeg = vertRotateDeg;
|
|
5
|
+
exports.isVerticalStack = isVerticalStack;
|
|
6
|
+
exports.stackedColumnsLtr = stackedColumnsLtr;
|
|
7
|
+
exports.textTotalRotationDeg = textTotalRotationDeg;
|
|
8
|
+
exports.paragraphFallbackFontSize = paragraphFallbackFontSize;
|
|
9
|
+
exports.lineHeightFactor = lineHeightFactor;
|
|
10
|
+
exports.paragraphLineHeightPx = paragraphLineHeightPx;
|
|
11
|
+
exports.newBulletNumState = newBulletNumState;
|
|
12
|
+
exports.formatBulletNumber = formatBulletNumber;
|
|
13
|
+
exports.bulletFontSize = bulletFontSize;
|
|
14
|
+
exports.advanceBulletLabel = advanceBulletLabel;
|
|
15
|
+
exports.hexWithAlpha = hexWithAlpha;
|
|
16
|
+
exports.textEffectsCssFilter = textEffectsCssFilter;
|
|
17
|
+
exports.pickRunEffectsCssFilter = pickRunEffectsCssFilter;
|
|
18
|
+
/**
|
|
19
|
+
* §21.1.2.1.1 bodyPr@lIns/tIns/rIns/bIns 的 OOXML 缺省值:
|
|
20
|
+
* 左右 91440 EMU = 7.2pt ≈ 9.6px,上下 45720 EMU = 3.6pt ≈ 4.8px。
|
|
21
|
+
*/
|
|
22
|
+
exports.DEFAULT_TEXT_INSETS = { l: 9.6, t: 4.8, r: 9.6, b: 4.8 };
|
|
23
|
+
/** 未写 lnSpc 时的行框系数。§21.1.2.2.5 只说由字号决定,这里取 1.2。 */
|
|
24
|
+
exports.DEFAULT_LINE_HEIGHT_FACTOR = 1.2;
|
|
25
|
+
/** 继承链都没写 sz 时的绘制字号(px)。schema 不给 sz 缺省值;Office 用 18pt。 */
|
|
26
|
+
exports.FALLBACK_FONT_SIZE = (1800 / 100) * (96 / 72);
|
|
27
|
+
/**
|
|
28
|
+
* §21.1.2.1.1 bodyPr@vert — 整框旋转角度。
|
|
29
|
+
* 仅 vert/vert270 走"外层 rotate"路径;其余竖排模式靠自定义竖排 layout。
|
|
30
|
+
*/
|
|
31
|
+
function vertRotateDeg(v) {
|
|
32
|
+
if (v === "vert")
|
|
33
|
+
return 90;
|
|
34
|
+
if (v === "vert270")
|
|
35
|
+
return -90;
|
|
36
|
+
return 0;
|
|
37
|
+
}
|
|
38
|
+
function isVerticalStack(v) {
|
|
39
|
+
return v === "wordArtVert" || v === "wordArtVertRtl" || v === "eaVert" || v === "mongolianVert";
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* 竖排堆叠的列推进方向。
|
|
43
|
+
* wordArtVertRtl = 从右到左(WPS「行顺序:从右到左」)。
|
|
44
|
+
* wordArtVert = 从左到右(WPS「行顺序:从左到右」)。rtlCol 对这两种 vert 无效。
|
|
45
|
+
*/
|
|
46
|
+
function stackedColumnsLtr(vert, rtlCol) {
|
|
47
|
+
if (vert === "wordArtVertRtl")
|
|
48
|
+
return false;
|
|
49
|
+
if (vert === "wordArtVert")
|
|
50
|
+
return true;
|
|
51
|
+
if (vert === "mongolianVert")
|
|
52
|
+
return !rtlCol;
|
|
53
|
+
return !!rtlCol;
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* §21.1.2.1.1 bodyPr@rot / @vert / @upright 叠加后,文字内容相对 slide 的总旋转角。
|
|
57
|
+
* 渲染端把 shape 自身的 rotation 加在 scene node 上、把 bodyRot+vert 加在文本组上,
|
|
58
|
+
* upright 再抵消掉 shape 的 rotation —— 三者之和就是文字最终看起来的旋转角度。
|
|
59
|
+
*/
|
|
60
|
+
function textTotalRotationDeg(s) {
|
|
61
|
+
const shapeRot = s.transform?.rotation ?? 0;
|
|
62
|
+
return (shapeRot + (s.bodyRot ?? 0) + vertRotateDeg(s.vert) + (s.upright ? -shapeRot : 0));
|
|
63
|
+
}
|
|
64
|
+
/** 段落缺省字号:取段内最大显式字号,全缺省时用 FALLBACK_FONT_SIZE。 */
|
|
65
|
+
function paragraphFallbackFontSize(p) {
|
|
66
|
+
const sizes = p.runs.map((r) => r.fontSize ?? 0).filter((n) => n > 0);
|
|
67
|
+
return sizes.length ? Math.max(...sizes) : exports.FALLBACK_FONT_SIZE;
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* §21.1.2.2.5 spcPct 是最大字号的百分比:120000 = 1.2 × 字号,不再乘第二层系数。
|
|
71
|
+
* 未写 lnSpc 时用 DEFAULT_LINE_HEIGHT_FACTOR;compatLnSpc 时按 1 × 字号。
|
|
72
|
+
*/
|
|
73
|
+
function lineHeightFactor(p, compatLnSpc) {
|
|
74
|
+
if (p.lineHeight != null)
|
|
75
|
+
return p.lineHeight;
|
|
76
|
+
return compatLnSpc ? 1 : exports.DEFAULT_LINE_HEIGHT_FACTOR;
|
|
77
|
+
}
|
|
78
|
+
/** 段落行高(px):显式 lineHeightPx 优先,否则按 §21.1.2.2.5 的百分比系数算。 */
|
|
79
|
+
function paragraphLineHeightPx(p, fallbackFs, compatLnSpc) {
|
|
80
|
+
if (typeof p.lineHeightPx === "number")
|
|
81
|
+
return p.lineHeightPx;
|
|
82
|
+
const fs = fallbackFs ?? paragraphFallbackFontSize(p);
|
|
83
|
+
return fs * lineHeightFactor(p, compatLnSpc);
|
|
84
|
+
}
|
|
85
|
+
function newBulletNumState() {
|
|
86
|
+
return { lastLevel: -1, lastType: "", count: 0 };
|
|
87
|
+
}
|
|
88
|
+
const NUM_LATIN_LC = "abcdefghijklmnopqrstuvwxyz";
|
|
89
|
+
const NUM_LATIN_UC = NUM_LATIN_LC.toUpperCase();
|
|
90
|
+
const NUM_ROMAN = [
|
|
91
|
+
["", "i", "ii", "iii", "iv", "v", "vi", "vii", "viii", "ix"],
|
|
92
|
+
["", "x", "xx", "xxx", "xl", "l", "lx", "lxx", "lxxx", "xc"],
|
|
93
|
+
["", "c", "cc", "ccc", "cd", "d", "dc", "dcc", "dccc", "cm"],
|
|
94
|
+
["", "m", "mm", "mmm"],
|
|
95
|
+
];
|
|
96
|
+
/** §21.1.2.4.1 buAutoNum@type — 把序号按 ST_TextAutonumberScheme 格式化。 */
|
|
97
|
+
function formatBulletNumber(n, type) {
|
|
98
|
+
const idx = Math.max(1, n);
|
|
99
|
+
const base = (() => {
|
|
100
|
+
if (type.startsWith("alphaLc")) {
|
|
101
|
+
const k = (idx - 1) % NUM_LATIN_LC.length;
|
|
102
|
+
const reps = Math.max(1, Math.floor((idx - 1) / NUM_LATIN_LC.length) + 1);
|
|
103
|
+
return NUM_LATIN_LC[k].repeat(reps);
|
|
104
|
+
}
|
|
105
|
+
if (type.startsWith("alphaUc")) {
|
|
106
|
+
const k = (idx - 1) % NUM_LATIN_UC.length;
|
|
107
|
+
const reps = Math.max(1, Math.floor((idx - 1) / NUM_LATIN_UC.length) + 1);
|
|
108
|
+
return NUM_LATIN_UC[k].repeat(reps);
|
|
109
|
+
}
|
|
110
|
+
if (type.startsWith("romanLc") || type.startsWith("romanUc")) {
|
|
111
|
+
let v = Math.min(idx, 3999);
|
|
112
|
+
const out = [];
|
|
113
|
+
let pos = 0;
|
|
114
|
+
while (v > 0 && pos < NUM_ROMAN.length) {
|
|
115
|
+
const digit = v % 10;
|
|
116
|
+
out.unshift(NUM_ROMAN[pos]?.[digit] ?? "");
|
|
117
|
+
v = Math.floor(v / 10);
|
|
118
|
+
pos += 1;
|
|
119
|
+
}
|
|
120
|
+
const s = out.join("");
|
|
121
|
+
return type.startsWith("romanUc") ? s.toUpperCase() : s;
|
|
122
|
+
}
|
|
123
|
+
return String(idx);
|
|
124
|
+
})();
|
|
125
|
+
if (type.endsWith("Period"))
|
|
126
|
+
return `${base}.`;
|
|
127
|
+
if (type.endsWith("ParenR"))
|
|
128
|
+
return `${base})`;
|
|
129
|
+
if (type.endsWith("ParenBoth"))
|
|
130
|
+
return `(${base})`;
|
|
131
|
+
if (type.endsWith("Plain"))
|
|
132
|
+
return base;
|
|
133
|
+
return `${base}.`;
|
|
134
|
+
}
|
|
135
|
+
/**
|
|
136
|
+
* §21.1.2.4.5 buSzPts 优先于 §21.1.2.4.4 buSzPct;两者都缺省时跟随 run 字号(buSzTx)。
|
|
137
|
+
*/
|
|
138
|
+
function bulletFontSize(bullet, runFs) {
|
|
139
|
+
const sizePts = "sizePts" in bullet ? bullet.sizePts : undefined;
|
|
140
|
+
const sizePct = "sizePct" in bullet ? bullet.sizePct : undefined;
|
|
141
|
+
if (sizePts !== undefined)
|
|
142
|
+
return sizePts * (96 / 72);
|
|
143
|
+
if (sizePct)
|
|
144
|
+
return runFs * sizePct;
|
|
145
|
+
return runFs;
|
|
146
|
+
}
|
|
147
|
+
/**
|
|
148
|
+
* 推进自动编号状态机,返回该段 bullet 的显示文本。
|
|
149
|
+
* none / picture 返回 null(picture bullet 由各消费者自行绘制图片)。
|
|
150
|
+
*
|
|
151
|
+
* 编号连续性规则:连续的同 level buAutoNum 段落累加;一旦遇到 buChar / buNone /
|
|
152
|
+
* level 变化就重置,与 PowerPoint 表现一致。
|
|
153
|
+
*/
|
|
154
|
+
function advanceBulletLabel(p, state) {
|
|
155
|
+
const bullet = p.bullet;
|
|
156
|
+
const lvl = p.level ?? 0;
|
|
157
|
+
if (!bullet || bullet.kind === "none") {
|
|
158
|
+
if (state.lastType === "number") {
|
|
159
|
+
state.lastType = "";
|
|
160
|
+
state.count = 0;
|
|
161
|
+
}
|
|
162
|
+
state.lastLevel = lvl;
|
|
163
|
+
return null;
|
|
164
|
+
}
|
|
165
|
+
if (bullet.kind === "char") {
|
|
166
|
+
if (state.lastType === "number") {
|
|
167
|
+
state.lastType = "";
|
|
168
|
+
state.count = 0;
|
|
169
|
+
}
|
|
170
|
+
state.lastLevel = lvl;
|
|
171
|
+
return bullet.char;
|
|
172
|
+
}
|
|
173
|
+
if (bullet.kind === "number") {
|
|
174
|
+
if (state.lastType !== "number" || state.lastLevel !== lvl) {
|
|
175
|
+
state.count = (bullet.startAt ?? 1) - 1;
|
|
176
|
+
state.lastType = "number";
|
|
177
|
+
}
|
|
178
|
+
state.count += 1;
|
|
179
|
+
state.lastLevel = lvl;
|
|
180
|
+
return formatBulletNumber(state.count, bullet.numType);
|
|
181
|
+
}
|
|
182
|
+
state.lastLevel = lvl;
|
|
183
|
+
return null;
|
|
184
|
+
}
|
|
185
|
+
// ============================================================================
|
|
186
|
+
// 颜色 / 效果
|
|
187
|
+
// ============================================================================
|
|
188
|
+
function hexWithAlpha(color, alpha) {
|
|
189
|
+
if (alpha >= 1)
|
|
190
|
+
return color;
|
|
191
|
+
if (/^#([0-9a-f]{6})$/i.test(color)) {
|
|
192
|
+
const a = Math.round(alpha * 255)
|
|
193
|
+
.toString(16)
|
|
194
|
+
.padStart(2, "0");
|
|
195
|
+
return color + a;
|
|
196
|
+
}
|
|
197
|
+
return color;
|
|
198
|
+
}
|
|
199
|
+
/**
|
|
200
|
+
* §21.1.2.3.9 rPr/effectLst → CSS `filter` 串(drop-shadow 链式叠加)。
|
|
201
|
+
*
|
|
202
|
+
* `scale` 用来把 slide 单位换算成目标坐标系的 px:SVG 里 filter 走 user-space
|
|
203
|
+
* 会随 viewBox 自动缩放,所以后端传 1;HTML overlay(编辑器)按屏幕 px 绘制,
|
|
204
|
+
* 必须传当前 stageScale,否则缩放后阴影 / 光晕的偏移和模糊半径会明显不对。
|
|
205
|
+
*
|
|
206
|
+
* inner shadow 与 reflection 不走这条 CSS 链:SVG 用 feComposite / 镜像 <use>,
|
|
207
|
+
* Canvas 在 drawText 里单独画。这里若把 reflection 收成 drop-shadow,倒影会变成一团光。
|
|
208
|
+
*/
|
|
209
|
+
function textEffectsCssFilter(effects, scale = 1) {
|
|
210
|
+
if (!effects || effects.length === 0)
|
|
211
|
+
return undefined;
|
|
212
|
+
const parts = [];
|
|
213
|
+
for (const fx of effects) {
|
|
214
|
+
if (fx.kind === "outer") {
|
|
215
|
+
const c = hexWithAlpha(fx.color, fx.alpha);
|
|
216
|
+
parts.push(`drop-shadow(${fx.dx * scale}px ${fx.dy * scale}px ${fx.blur * scale}px ${c})`);
|
|
217
|
+
}
|
|
218
|
+
else if (fx.kind === "glow") {
|
|
219
|
+
const c = hexWithAlpha(fx.color, fx.alpha);
|
|
220
|
+
const r = fx.radius * scale;
|
|
221
|
+
parts.push(`drop-shadow(0 0 ${r}px ${c})`);
|
|
222
|
+
parts.push(`drop-shadow(0 0 ${r}px ${c})`);
|
|
223
|
+
}
|
|
224
|
+
else if (fx.kind === "blur") {
|
|
225
|
+
parts.push(`blur(${Math.max(0.3, fx.radius * scale)}px)`);
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
return parts.length ? parts.join(" ") : undefined;
|
|
229
|
+
}
|
|
230
|
+
/** 取 paragraph 内首个声明 effects 的 run 的 CSS filter(paragraph 级近似)。 */
|
|
231
|
+
function pickRunEffectsCssFilter(runs, scale = 1) {
|
|
232
|
+
for (const r of runs) {
|
|
233
|
+
const f = textEffectsCssFilter(r.effects, scale);
|
|
234
|
+
if (f)
|
|
235
|
+
return f;
|
|
236
|
+
}
|
|
237
|
+
return undefined;
|
|
238
|
+
}
|