reamkit 1.15.4 → 1.17.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/dist/esm/core/bidi/segments.js +1 -2
- package/dist/esm/core/converter/facade.d.ts +5 -0
- package/dist/esm/core/converter/project.js +5 -1
- package/dist/esm/core/converter/ream.d.ts +6 -0
- package/dist/esm/core/converter/ream.js +34 -8
- package/dist/esm/core/document-model/index.d.ts +1 -1
- package/dist/esm/core/document-model/types.d.ts +659 -7
- package/dist/esm/core/drawingml/chart-geometry.d.ts +34 -10
- package/dist/esm/core/drawingml/chart-geometry.js +424 -92
- package/dist/esm/core/drawingml/chart-parser.js +301 -11
- package/dist/esm/core/drawingml/colors.d.ts +7 -7
- package/dist/esm/core/drawingml/colors.js +84 -18
- package/dist/esm/core/drawingml/preset-geometry.js +240 -6
- package/dist/esm/core/drawingml/shape-render.d.ts +12 -0
- package/dist/esm/core/drawingml/shape-render.js +116 -3
- package/dist/esm/core/drawingml/sparkline-geometry.js +6 -4
- package/dist/esm/core/drawingml/theme-parser.d.ts +51 -0
- package/dist/esm/core/drawingml/theme-parser.js +92 -1
- package/dist/esm/core/font/measure.d.ts +5 -0
- package/dist/esm/core/font/measure.js +21 -1
- package/dist/esm/core/font/ttf-subset.js +26 -1
- package/dist/esm/core/images.d.ts +2 -2
- package/dist/esm/core/images.js +353 -43
- package/dist/esm/core/indexed-colors.d.ts +11 -0
- package/dist/esm/core/indexed-colors.js +81 -0
- package/dist/esm/core/ir/flow.d.ts +23 -1
- package/dist/esm/core/ir/sheet.d.ts +61 -0
- package/dist/esm/{excel → core}/number-format.d.ts +37 -0
- package/dist/esm/core/number-format.js +800 -0
- package/dist/esm/core/numbering/apply.d.ts +5 -3
- package/dist/esm/core/numbering/apply.js +42 -11
- package/dist/esm/core/numbering/index.js +2 -0
- package/dist/esm/core/numbering/state.d.ts +13 -1
- package/dist/esm/core/numbering/state.js +256 -15
- package/dist/esm/core/opc/alternate-content.d.ts +13 -0
- package/dist/esm/core/opc/alternate-content.js +72 -0
- package/dist/esm/core/opc/core-properties.js +1 -0
- package/dist/esm/core/opc/package.d.ts +14 -1
- package/dist/esm/core/opc/package.js +43 -4
- package/dist/esm/core/opc/relationships.js +1 -0
- package/dist/esm/core/opc/xml-entities.d.ts +12 -0
- package/dist/esm/core/opc/xml-entities.js +91 -0
- package/dist/esm/core/po-helpers.js +16 -1
- package/dist/esm/core/spreadsheet-model/index.d.ts +1 -1
- package/dist/esm/core/spreadsheet-model/types.d.ts +137 -0
- package/dist/esm/core/style-cascade/resolver.js +66 -11
- package/dist/esm/core/style-cascade/table.js +35 -11
- package/dist/esm/core/style-cascade/types.d.ts +42 -1
- package/dist/esm/core/style-cascade/types.js +4 -0
- package/dist/esm/core/vector.d.ts +30 -0
- package/dist/esm/excel/activex-parser.js +1 -0
- package/dist/esm/excel/column-bands.d.ts +26 -1
- package/dist/esm/excel/column-bands.js +156 -20
- package/dist/esm/excel/comments-parser.js +5 -3
- package/dist/esm/excel/conditional-format.d.ts +25 -2
- package/dist/esm/excel/conditional-format.js +63 -19
- package/dist/esm/excel/escaped-text.d.ts +9 -0
- package/dist/esm/excel/escaped-text.js +20 -0
- package/dist/esm/excel/form-control-parser.js +1 -0
- package/dist/esm/excel/formula/dates.js +1 -1
- package/dist/esm/excel/header-footer.d.ts +9 -1
- package/dist/esm/excel/header-footer.js +162 -14
- package/dist/esm/excel/index.d.ts +1 -1
- package/dist/esm/excel/index.js +2 -2
- package/dist/esm/excel/pivot-table-parser.js +1 -0
- package/dist/esm/excel/print-model.d.ts +75 -0
- package/dist/esm/excel/print-model.js +899 -77
- package/dist/esm/excel/printer-settings.d.ts +16 -0
- package/dist/esm/excel/printer-settings.js +36 -0
- package/dist/esm/excel/rich-value.d.ts +13 -0
- package/dist/esm/excel/rich-value.js +120 -0
- package/dist/esm/excel/shared-strings-parser.js +11 -7
- package/dist/esm/excel/sheet-drawing.d.ts +11 -0
- package/dist/esm/excel/sheet-drawing.js +26 -3
- package/dist/esm/excel/sheet-shape-parser.d.ts +53 -1
- package/dist/esm/excel/sheet-shape-parser.js +461 -32
- package/dist/esm/excel/sheet-to-flow.d.ts +23 -2
- package/dist/esm/excel/sheet-to-flow.js +553 -36
- package/dist/esm/excel/slicer-parser.js +1 -0
- package/dist/esm/excel/styles-parser.d.ts +30 -2
- package/dist/esm/excel/styles-parser.js +177 -40
- package/dist/esm/excel/table-parser.js +1 -0
- package/dist/esm/excel/vml-drawing.d.ts +68 -0
- package/dist/esm/excel/vml-drawing.js +158 -0
- package/dist/esm/excel/workbook-parser.d.ts +5 -0
- package/dist/esm/excel/workbook-parser.js +5 -1
- package/dist/esm/excel/worksheet-parser.d.ts +2 -1
- package/dist/esm/excel/worksheet-parser.js +150 -61
- package/dist/esm/excel/xlsx-reader.js +242 -11
- package/dist/esm/excel/xlsx-to-pdf.js +12 -1
- package/dist/esm/html/html-writer.js +29 -6
- package/dist/esm/layout/page-doc.d.ts +87 -1
- package/dist/esm/layout/styled-layout.d.ts +46 -2
- package/dist/esm/layout/styled-layout.js +2345 -274
- package/dist/esm/pdf/cid-font.js +20 -9
- package/dist/esm/pdf/objects.d.ts +13 -0
- package/dist/esm/pdf/objects.js +18 -1
- package/dist/esm/pdf/shading.js +76 -6
- package/dist/esm/pdf/styled-page-emitter.js +355 -49
- package/dist/esm/pdf/text-page-renderer.js +1 -0
- package/dist/esm/pdf/vector-graphics.d.ts +31 -2
- package/dist/esm/pdf/vector-graphics.js +103 -3
- package/dist/esm/pptx/pptx-reader.js +2 -0
- package/dist/esm/pptx/slide-parser.d.ts +21 -4
- package/dist/esm/pptx/slide-parser.js +98 -24
- package/dist/esm/svg/svg-writer.js +23 -4
- package/dist/esm/word/document-parser.d.ts +96 -7
- package/dist/esm/word/document-parser.js +538 -116
- package/dist/esm/word/docx-reader.js +135 -43
- package/dist/esm/word/docx-writer.js +6 -1
- package/dist/esm/word/drawing-parser.d.ts +86 -5
- package/dist/esm/word/drawing-parser.js +1590 -54
- package/dist/esm/word/index.d.ts +4 -4
- package/dist/esm/word/numbering-parser.d.ts +2 -1
- package/dist/esm/word/numbering-parser.js +108 -21
- package/dist/esm/word/paragraph-properties.d.ts +11 -1
- package/dist/esm/word/paragraph-properties.js +219 -10
- package/dist/esm/word/run-properties.js +45 -1
- package/dist/esm/word/settings-parser.d.ts +24 -0
- package/dist/esm/word/settings-parser.js +32 -5
- package/dist/esm/word/shading.d.ts +11 -0
- package/dist/esm/word/shading.js +64 -0
- package/dist/esm/word/styles-parser.js +9 -2
- package/dist/esm/word/table-parser.d.ts +9 -1
- package/dist/esm/word/table-parser.js +182 -30
- package/package.json +7 -3
- package/dist/esm/excel/number-format.js +0 -476
|
@@ -10,17 +10,18 @@ import { FORCED_BREAK, breakLines } from "../core/line-breaker/knuth-plass.js";
|
|
|
10
10
|
import { greedyBreakLines } from "../core/line-breaker/greedy.js";
|
|
11
11
|
import { cjkBreakBetween, splitCjkSegment } from "../core/line-breaker/cjk.js";
|
|
12
12
|
import "../core/line-breaker/index.js";
|
|
13
|
-
import { applyNumbering, applyNumberingToHeadersFooters } from "../core/numbering/apply.js";
|
|
14
13
|
import { DEFAULT_RESOLVED_PARAGRAPH, DEFAULT_RESOLVED_RUN } from "../core/style-cascade/types.js";
|
|
15
14
|
import { resolveParagraphProperties, resolveRunProperties } from "../core/style-cascade/resolver.js";
|
|
16
15
|
import "../core/style-cascade/index.js";
|
|
16
|
+
import { applyNumbering, applyNumberingToHeadersFooters } from "../core/numbering/apply.js";
|
|
17
|
+
import "../core/numbering/index.js";
|
|
17
18
|
import { PathBuilder, flipTransform } from "../core/vector.js";
|
|
18
19
|
import { buildSparkline } from "../core/drawingml/sparkline-geometry.js";
|
|
19
20
|
import { arcPoint, arcToBeziers } from "../core/arc-to-bezier.js";
|
|
20
|
-
import { buildChartScene } from "../core/drawingml/chart-geometry.js";
|
|
21
|
+
import { buildChartScene, legendSeriesName } from "../core/drawingml/chart-geometry.js";
|
|
21
22
|
import { layoutMath, mathGlyphSegments, variantStyle } from "./math-layout.js";
|
|
22
23
|
import { rectPath } from "../core/drawingml/preset-geometry.js";
|
|
23
|
-
import { buildShapePaths, buildShapeTransform, buildStroke, gradientToSolid } from "../core/drawingml/shape-render.js";
|
|
24
|
+
import { buildShapePaths, buildShapeTransform, buildStroke, gradientToSolid, lineEndPaths } from "../core/drawingml/shape-render.js";
|
|
24
25
|
import { StructTreeBuilder } from "../pdf/struct-tree.js";
|
|
25
26
|
//#region src/layout/styled-layout.ts
|
|
26
27
|
function parsePdfAProfile(pdfA) {
|
|
@@ -33,6 +34,14 @@ function parsePdfAProfile(pdfA) {
|
|
|
33
34
|
version: part === 1 ? "1.4" : "1.7"
|
|
34
35
|
};
|
|
35
36
|
}
|
|
37
|
+
/** §17.6.4 — how heavy Word draws the rule between columns. */
|
|
38
|
+
var COLUMN_SEPARATOR_PT = .5;
|
|
39
|
+
/**
|
|
40
|
+
* How far a space may be squeezed below its natural width — the shrink the
|
|
41
|
+
* line breaker offers when it weighs a line, and therefore the shrink the
|
|
42
|
+
* emitter owes it back when it draws one.
|
|
43
|
+
*/
|
|
44
|
+
var GLUE_SHRINK_RATIO = .3;
|
|
36
45
|
var TWIP_TO_PT = 1 / 20;
|
|
37
46
|
var EIGHTH_PT = 1 / 8;
|
|
38
47
|
var DEFAULT_CELL_PADDING_TWIPS = 108;
|
|
@@ -40,6 +49,7 @@ var DEFAULT_BORDER_SIZE_EIGHTH = 4;
|
|
|
40
49
|
var FOOTNOTE_RULE_PT = .75;
|
|
41
50
|
var FOOTNOTE_RULE_GAP_ABOVE = 4;
|
|
42
51
|
var FOOTNOTE_SEPARATOR_HEIGHT = 10;
|
|
52
|
+
var LINE_NUMBER_GAP_PT = 18;
|
|
43
53
|
var FLOAT_TEXT_GAP = 6;
|
|
44
54
|
var MIN_WRAP_WIDTH = 36;
|
|
45
55
|
function isOutOfFlowFloat(f) {
|
|
@@ -257,21 +267,48 @@ function commentTailBlocks(comment, n, opts) {
|
|
|
257
267
|
*/
|
|
258
268
|
function layoutStyledDocument(body, options) {
|
|
259
269
|
const sectionList = resolveSectionList(body, options);
|
|
260
|
-
const noteAssigned = assignNoteNumbers(applyNumbering(body, options.numbering), options.comments ? new Set(options.comments.keys()) : void 0);
|
|
270
|
+
const noteAssigned = assignNoteNumbers(applyNumbering(body, options.numbering, options.styles), options.comments ? new Set(options.comments.keys()) : void 0);
|
|
261
271
|
const numberedBody = noteAssigned.body;
|
|
262
|
-
const numberedHeadersFooters = applyNumberingToHeadersFooters(options.headersFooters, options.numbering);
|
|
272
|
+
const numberedHeadersFooters = applyNumberingToHeadersFooters(options.headersFooters, options.numbering, options.styles);
|
|
263
273
|
const pdfaProfile = options.pdfA ? parsePdfAProfile(options.pdfA) : void 0;
|
|
264
274
|
const tagged = options.tagged === true || options.pdfUA === true || (pdfaProfile?.tagged ?? false);
|
|
265
275
|
const structBuilder = tagged ? new StructTreeBuilder() : void 0;
|
|
266
276
|
const fontResources = collectFontResources(numberedBody, numberedHeadersFooters, options);
|
|
267
277
|
const imageResources = collectImageResources(numberedBody, numberedHeadersFooters, options);
|
|
268
|
-
const sectionCtxs = sectionList.map((s) => buildSectionContext(s, options, numberedHeadersFooters, fontResources));
|
|
278
|
+
const sectionCtxs = sectionList.map((s) => buildSectionContext(s, options, numberedHeadersFooters, fontResources, imageResources));
|
|
269
279
|
let sectionIdx = 0;
|
|
280
|
+
const bodyFrames = frameGroups(numberedBody);
|
|
270
281
|
const blocks = numberedBody.map((el, idx) => {
|
|
271
282
|
while (sectionIdx < sectionCtxs.length - 1 && idx >= sectionCtxs[sectionIdx].endIndex) sectionIdx++;
|
|
272
283
|
const ctx = sectionCtxs[sectionIdx];
|
|
273
|
-
|
|
284
|
+
const width = ctx.columns ? ctx.columns[0].widthPt : ctx.contentWidth;
|
|
285
|
+
const box = {
|
|
286
|
+
pageWidthPt: ctx.pageWidth,
|
|
287
|
+
pageHeightPt: ctx.pageHeight,
|
|
288
|
+
marginLeftPt: ctx.marginLeft,
|
|
289
|
+
marginRightPt: ctx.pageWidth - ctx.marginLeft - ctx.contentWidth,
|
|
290
|
+
marginTopPt: ctx.marginTop,
|
|
291
|
+
marginBottomPt: ctx.marginBottom
|
|
292
|
+
};
|
|
293
|
+
const group = bodyFrames.starts.get(idx);
|
|
294
|
+
if (group) return layoutFrameBlock(group, ctx.options ?? options, fontResources, imageResources, width);
|
|
295
|
+
if (bodyFrames.continued.has(idx)) return emptyBlock(options);
|
|
296
|
+
return layoutBodyElement(el, ctx.options ?? options, fontResources, imageResources, width, ctx.pageContentHeight, box);
|
|
274
297
|
});
|
|
298
|
+
for (let i = 0; i < blocks.length; i++) {
|
|
299
|
+
const here = blocks[i];
|
|
300
|
+
const next = blocks[i + 1];
|
|
301
|
+
if (here?.kind !== "paragraph" || next?.kind !== "paragraph") continue;
|
|
302
|
+
if (here.source?.properties.styleId !== next.source?.properties.styleId) continue;
|
|
303
|
+
if (here.resolved.contextualSpacing) blocks[i] = {
|
|
304
|
+
...here,
|
|
305
|
+
spacingAfterPt: 0
|
|
306
|
+
};
|
|
307
|
+
if (next.resolved.contextualSpacing) blocks[i + 1] = {
|
|
308
|
+
...next,
|
|
309
|
+
spacingBeforePt: 0
|
|
310
|
+
};
|
|
311
|
+
}
|
|
275
312
|
const noteBlockCache = /* @__PURE__ */ new Map();
|
|
276
313
|
const notePlan = options.footnotes && noteAssigned.footnotes.size > 0 ? {
|
|
277
314
|
numbers: noteAssigned.footnotes,
|
|
@@ -286,7 +323,7 @@ function layoutStyledDocument(body, options) {
|
|
|
286
323
|
const n = noteAssigned.footnotes.get(id);
|
|
287
324
|
let laid = void 0;
|
|
288
325
|
if (content && n !== void 0 && !noteAssigned.deferredFootnotes.includes(id)) {
|
|
289
|
-
const noteBlocks = substituteNoteNumber(content, n).map((el) => layoutBodyElement(el, options, fontResources, imageResources, sectionCtx.contentWidth, sectionCtx.pageContentHeight));
|
|
326
|
+
const noteBlocks = substituteNoteNumber(content, n).map((el) => layoutBodyElement(el, sectionCtx.options ?? options, fontResources, imageResources, sectionCtx.contentWidth, sectionCtx.pageContentHeight));
|
|
290
327
|
laid = {
|
|
291
328
|
blocks: noteBlocks,
|
|
292
329
|
heightPt: noteBlocks.reduce((sum, b) => sum + (b.kind === "paragraph" ? b.spacingBeforePt + b.heightPt + b.spacingAfterPt : 0), 0)
|
|
@@ -314,7 +351,24 @@ function layoutStyledDocument(body, options) {
|
|
|
314
351
|
n
|
|
315
352
|
});
|
|
316
353
|
}
|
|
317
|
-
|
|
354
|
+
if (tailNotes.length > 0) blocks.push(layoutShapeBlock({
|
|
355
|
+
width: pt(FOOTNOTE_RULE_WIDTH),
|
|
356
|
+
height: pt(FOOTNOTE_RULE_PT),
|
|
357
|
+
geometry: {
|
|
358
|
+
kind: "preset",
|
|
359
|
+
preset: "rect",
|
|
360
|
+
adjust: /* @__PURE__ */ new Map()
|
|
361
|
+
},
|
|
362
|
+
fill: {
|
|
363
|
+
kind: "solid",
|
|
364
|
+
colorHex: "000000"
|
|
365
|
+
},
|
|
366
|
+
paragraphProperties: {
|
|
367
|
+
spacingBefore: pt(FOOTNOTE_SEPARATOR_HEIGHT),
|
|
368
|
+
spacingAfter: pt(FOOTNOTE_RULE_GAP_ABOVE)
|
|
369
|
+
}
|
|
370
|
+
}, options, fontResources, imageResources, lastCtx.contentWidth));
|
|
371
|
+
for (const note of tailNotes.sort((a, b) => a.n - b.n)) for (const el of substituteNoteNumber(note.content, note.n)) blocks.push(layoutBodyElement(el, lastCtx.options ?? options, fontResources, imageResources, lastCtx.contentWidth, lastCtx.pageContentHeight));
|
|
318
372
|
const commentNums = noteAssigned.comments;
|
|
319
373
|
const threadDepth = (id) => {
|
|
320
374
|
let depth = 0;
|
|
@@ -346,7 +400,7 @@ function layoutStyledDocument(body, options) {
|
|
|
346
400
|
const commentNotes = options.commentAnnotations && options.comments ? buildCommentNotes(noteAssigned.comments, options.comments) : void 0;
|
|
347
401
|
const reflowParagraph = (paragraph, width, widths) => layoutParagraphBlock(paragraph, options, fontResources, imageResources, width, widths);
|
|
348
402
|
return {
|
|
349
|
-
pages: paginateSections(blocks, sectionCtxs, structBuilder, options.language ?? "en-US", notePlan, bookmarks, reflowParagraph),
|
|
403
|
+
pages: paginateSections(blocks, sectionCtxs, structBuilder, options.language ?? "en-US", notePlan, bookmarks, reflowParagraph, options.pageBackgroundFill?.imageResource !== void 0 ? imageResources.get(options.pageBackgroundFill.imageResource)?.resourceName : void 0),
|
|
350
404
|
resources: options.resources ?? new ResourceStore(),
|
|
351
405
|
fontResources,
|
|
352
406
|
imageResources,
|
|
@@ -367,14 +421,16 @@ function resolvePageDimensions(options, section) {
|
|
|
367
421
|
const sectionRight = section?.margins?.right !== void 0 ? section.margins.right : void 0;
|
|
368
422
|
const sectionTop = section?.margins?.top !== void 0 ? section.margins.top : void 0;
|
|
369
423
|
const sectionBottom = section?.margins?.bottom !== void 0 ? section.margins.bottom : void 0;
|
|
424
|
+
const gutter = section?.margins?.gutter ?? 0;
|
|
425
|
+
const atTop = options.gutterAtTop === true;
|
|
370
426
|
const headerOffsetPt = section?.margins?.header ?? 720 * TWIP_TO_PT;
|
|
371
427
|
const footerOffsetPt = section?.margins?.footer ?? 720 * TWIP_TO_PT;
|
|
372
428
|
return {
|
|
373
429
|
pageWidth: options.pageWidth ?? sectionPageWidth ?? 595,
|
|
374
430
|
pageHeight: options.pageHeight ?? sectionPageHeight ?? 842,
|
|
375
|
-
marginLeft: options.marginLeft ?? sectionLeft ?? 72,
|
|
431
|
+
marginLeft: (options.marginLeft ?? sectionLeft ?? 72) + (atTop ? 0 : gutter),
|
|
376
432
|
marginRight: options.marginRight ?? sectionRight ?? 72,
|
|
377
|
-
marginTop: options.marginTop ?? sectionTop ?? 72,
|
|
433
|
+
marginTop: (options.marginTop ?? sectionTop ?? 72) + (atTop ? gutter : 0),
|
|
378
434
|
marginBottom: options.marginBottom ?? sectionBottom ?? 72,
|
|
379
435
|
headerOffsetPt,
|
|
380
436
|
footerOffsetPt
|
|
@@ -394,27 +450,50 @@ function resolveSectionList(body, options) {
|
|
|
394
450
|
endIndex: body.length
|
|
395
451
|
}];
|
|
396
452
|
}
|
|
397
|
-
function buildSectionContext(section, options, headersFooters, fontResources) {
|
|
453
|
+
function buildSectionContext(section, options, headersFooters, fontResources, imageResources) {
|
|
398
454
|
const dims = resolvePageDimensions(options, section.properties);
|
|
399
455
|
const contentWidth = dims.pageWidth - dims.marginLeft - dims.marginRight;
|
|
400
|
-
const
|
|
401
|
-
|
|
456
|
+
const secOptions = section.properties.gridLinePitchPt !== void 0 ? {
|
|
457
|
+
...options,
|
|
458
|
+
gridLinePitchPt: section.properties.gridLinePitchPt
|
|
459
|
+
} : options;
|
|
460
|
+
const relativeBox = {
|
|
461
|
+
pageWidthPt: dims.pageWidth,
|
|
462
|
+
pageHeightPt: dims.pageHeight,
|
|
463
|
+
marginLeftPt: dims.marginLeft,
|
|
464
|
+
marginRightPt: dims.marginRight,
|
|
465
|
+
marginTopPt: dims.marginTop,
|
|
466
|
+
marginBottomPt: dims.marginBottom
|
|
467
|
+
};
|
|
468
|
+
const headerSet = layoutHeaderSet(section.properties, headersFooters, secOptions, fontResources, imageResources, contentWidth, dims.marginLeft, dims.pageHeight, dims.headerOffsetPt, {
|
|
469
|
+
top: dims.marginTop,
|
|
470
|
+
bottom: dims.marginBottom
|
|
471
|
+
}, relativeBox);
|
|
472
|
+
const footerSet = layoutFooterSet(section.properties, headersFooters, secOptions, fontResources, imageResources, contentWidth, dims.marginLeft, dims.pageHeight, dims.footerOffsetPt, {
|
|
473
|
+
top: dims.marginTop,
|
|
474
|
+
bottom: dims.marginBottom
|
|
475
|
+
}, relativeBox);
|
|
402
476
|
const columns = buildColumnGeometry(section.properties.columns, contentWidth);
|
|
477
|
+
const headerHeight = Math.max(headerSet.default.heightPt ?? 0, section.properties.titlePg === true ? headerSet.first.heightPt ?? 0 : 0, section.properties.evenAndOddHeaders === true ? headerSet.even.heightPt ?? 0 : 0);
|
|
478
|
+
const headerBottom = headerHeight > 0 ? dims.headerOffsetPt + headerHeight : 0;
|
|
479
|
+
const marginTop = Math.max(dims.marginTop, headerBottom);
|
|
403
480
|
return {
|
|
404
481
|
endIndex: section.endIndex,
|
|
405
482
|
properties: section.properties,
|
|
406
483
|
pageWidth: dims.pageWidth,
|
|
407
484
|
pageHeight: dims.pageHeight,
|
|
408
485
|
marginLeft: dims.marginLeft,
|
|
409
|
-
marginTop
|
|
486
|
+
marginTop,
|
|
410
487
|
marginBottom: dims.marginBottom,
|
|
411
488
|
contentWidth,
|
|
412
|
-
pageContentHeight: dims.pageHeight -
|
|
489
|
+
pageContentHeight: dims.pageHeight - marginTop - dims.marginBottom,
|
|
413
490
|
...columns ? { columns } : {},
|
|
414
491
|
headerSet,
|
|
415
492
|
footerSet,
|
|
416
493
|
titlePg: section.properties.titlePg === true,
|
|
417
|
-
evenAndOddHeaders: section.properties.evenAndOddHeaders === true
|
|
494
|
+
evenAndOddHeaders: section.properties.evenAndOddHeaders === true,
|
|
495
|
+
continuous: section.properties.sectionStart === "continuous",
|
|
496
|
+
options: secOptions
|
|
418
497
|
};
|
|
419
498
|
}
|
|
420
499
|
function buildColumnGeometry(cols, contentWidth) {
|
|
@@ -449,20 +528,30 @@ function markPagination(cmds) {
|
|
|
449
528
|
artifact: "pagination"
|
|
450
529
|
}));
|
|
451
530
|
}
|
|
452
|
-
function layoutHeaderSet(section, headersFooters, options, fontResources, contentWidth, marginLeft, pageHeight, headerOffsetPt) {
|
|
531
|
+
function layoutHeaderSet(section, headersFooters, options, fontResources, imageResources, contentWidth, marginLeft, pageHeight, headerOffsetPt, bandMargins, relativeBox) {
|
|
453
532
|
const band = (type) => {
|
|
454
533
|
const ref = refByType(section.headers, type);
|
|
455
534
|
if (!ref) return { commands: [] };
|
|
456
535
|
const content = headersFooters.get(ref.relationshipId);
|
|
457
536
|
if (!content) return { commands: [] };
|
|
537
|
+
let measured = 0;
|
|
458
538
|
const render = (c) => {
|
|
459
|
-
|
|
539
|
+
const blocks = laidOutBlocksFor(c, options, fontResources, contentWidth, imageResources, relativeBox);
|
|
540
|
+
measured = blocksHeight(blocks);
|
|
541
|
+
return markPagination(drawBlocksSequentially(blocks, marginLeft, pageHeight - headerOffsetPt, pageHeight, contentWidth, void 0, bandMargins));
|
|
460
542
|
};
|
|
461
|
-
if (contentHasPageFields(content))
|
|
462
|
-
|
|
463
|
-
|
|
543
|
+
if (contentHasPageFields(content)) {
|
|
544
|
+
render(substitutePageFields(content, 1, 1));
|
|
545
|
+
return {
|
|
546
|
+
commands: [],
|
|
547
|
+
heightPt: measured,
|
|
548
|
+
renderDynamic: (n, total) => render(substitutePageFields(content, n, total))
|
|
549
|
+
};
|
|
550
|
+
}
|
|
551
|
+
return {
|
|
552
|
+
commands: render(content),
|
|
553
|
+
heightPt: measured
|
|
464
554
|
};
|
|
465
|
-
return { commands: render(content) };
|
|
466
555
|
};
|
|
467
556
|
return {
|
|
468
557
|
default: band("default"),
|
|
@@ -470,15 +559,37 @@ function layoutHeaderSet(section, headersFooters, options, fontResources, conten
|
|
|
470
559
|
even: band("even")
|
|
471
560
|
};
|
|
472
561
|
}
|
|
473
|
-
|
|
562
|
+
/**
|
|
563
|
+
* §17.6.4 — the height each column of a BALANCED band is given: the band's
|
|
564
|
+
* share, but never more than the page has room for, and never so little that
|
|
565
|
+
* the band could not fit the columns at all.
|
|
566
|
+
*
|
|
567
|
+
* @param bandHeightPt The band's whole laid-out height.
|
|
568
|
+
* @param colCount How many columns it has to share.
|
|
569
|
+
* @param roomPt What the page has left below the band's top.
|
|
570
|
+
* @returns The per-column height, or 0 when the band should just fill.
|
|
571
|
+
*/
|
|
572
|
+
function balancedColumnHeight(bandHeightPt, colCount, roomPt) {
|
|
573
|
+
if (bandHeightPt <= 0 || colCount < 2 || roomPt <= 0) return 0;
|
|
574
|
+
if (bandHeightPt > roomPt * colCount) return 0;
|
|
575
|
+
return Math.min(roomPt, bandHeightPt / colCount);
|
|
576
|
+
}
|
|
577
|
+
/** Total laid-out height of a run of blocks, paragraph spacing included. */
|
|
578
|
+
function blocksHeight(blocks) {
|
|
579
|
+
return blocks.reduce((sum, b) => {
|
|
580
|
+
if (b.kind !== "paragraph" && b.kind !== "table" && isOutOfFlowFloat(b.float)) return sum;
|
|
581
|
+
return sum + (b.kind === "paragraph" ? b.spacingBeforePt + b.heightPt + b.spacingAfterPt : b.heightPt);
|
|
582
|
+
}, 0);
|
|
583
|
+
}
|
|
584
|
+
function layoutFooterSet(section, headersFooters, options, fontResources, imageResources, contentWidth, marginLeft, pageHeight, footerOffsetPt, bandMargins, relativeBox) {
|
|
474
585
|
const band = (type) => {
|
|
475
586
|
const ref = refByType(section.footers, type);
|
|
476
587
|
if (!ref) return { commands: [] };
|
|
477
588
|
const content = headersFooters.get(ref.relationshipId);
|
|
478
589
|
if (!content) return { commands: [] };
|
|
479
590
|
const render = (c) => {
|
|
480
|
-
const blocks = laidOutBlocksFor(c, options, fontResources, contentWidth);
|
|
481
|
-
return markPagination(drawBlocksSequentially(blocks, marginLeft, footerOffsetPt + blocks
|
|
591
|
+
const blocks = laidOutBlocksFor(c, options, fontResources, contentWidth, imageResources, relativeBox);
|
|
592
|
+
return markPagination(drawBlocksSequentially(blocks, marginLeft, footerOffsetPt + blocksHeight(blocks), pageHeight, contentWidth, void 0, bandMargins));
|
|
482
593
|
};
|
|
483
594
|
if (contentHasPageFields(content)) return {
|
|
484
595
|
commands: [],
|
|
@@ -514,7 +625,7 @@ function substitutePageFields(content, pageNumber, totalPages) {
|
|
|
514
625
|
}
|
|
515
626
|
function pickBand(set, band) {
|
|
516
627
|
const has = (e) => e.commands.length > 0 || e.renderDynamic !== void 0;
|
|
517
|
-
if (band === "first") return
|
|
628
|
+
if (band === "first") return set.first;
|
|
518
629
|
if (band === "even") return has(set.even) ? set.even : set.default;
|
|
519
630
|
return set.default;
|
|
520
631
|
}
|
|
@@ -523,24 +634,236 @@ function bandForPage(pageInSection, globalPageIdx, titlePg, evenAndOddHeaders) {
|
|
|
523
634
|
if (evenAndOddHeaders && (globalPageIdx + 1) % 2 === 0) return "even";
|
|
524
635
|
return "default";
|
|
525
636
|
}
|
|
526
|
-
|
|
527
|
-
|
|
637
|
+
/**
|
|
638
|
+
* §17.3.1.11 — the runs of consecutive paragraphs that carry the SAME frame.
|
|
639
|
+
* Word treats them as one floating text frame, so they lay out together in one
|
|
640
|
+
* box; the entries after the first become empty blocks, which keeps every
|
|
641
|
+
* caller's element indexing (and its section bounds) intact.
|
|
642
|
+
*
|
|
643
|
+
* @param elements The body elements, in order.
|
|
644
|
+
* @returns `starts`: the index each frame begins at → its paragraphs;
|
|
645
|
+
* `continued`: the indices swallowed by a frame that began earlier.
|
|
646
|
+
*/
|
|
647
|
+
function frameGroups(elements) {
|
|
648
|
+
const starts = /* @__PURE__ */ new Map();
|
|
649
|
+
const continued = /* @__PURE__ */ new Set();
|
|
650
|
+
const key = (p) => p.properties.frame ? JSON.stringify(p.properties.frame) : void 0;
|
|
651
|
+
let i = 0;
|
|
652
|
+
while (i < elements.length) {
|
|
653
|
+
const el = elements[i];
|
|
654
|
+
const k = el.kind === "paragraph" ? key(el.paragraph) : void 0;
|
|
655
|
+
if (k === void 0) {
|
|
656
|
+
i++;
|
|
657
|
+
continue;
|
|
658
|
+
}
|
|
659
|
+
const members = [el.paragraph];
|
|
660
|
+
let j = i + 1;
|
|
661
|
+
while (j < elements.length) {
|
|
662
|
+
const next = elements[j];
|
|
663
|
+
if (next.kind !== "paragraph" || key(next.paragraph) !== k) break;
|
|
664
|
+
members.push(next.paragraph);
|
|
665
|
+
continued.add(j);
|
|
666
|
+
j++;
|
|
667
|
+
}
|
|
668
|
+
starts.set(i, members);
|
|
669
|
+
i = j;
|
|
670
|
+
}
|
|
671
|
+
return {
|
|
672
|
+
starts,
|
|
673
|
+
continued
|
|
674
|
+
};
|
|
675
|
+
}
|
|
676
|
+
/** A block that occupies no space at all — the tail of a frame group. */
|
|
677
|
+
function emptyBlock(options) {
|
|
678
|
+
return {
|
|
679
|
+
kind: "paragraph",
|
|
680
|
+
resolved: resolveParagraphProperties({}, options.styles),
|
|
681
|
+
lines: [],
|
|
682
|
+
heightPt: 0,
|
|
683
|
+
spacingBeforePt: 0,
|
|
684
|
+
spacingAfterPt: 0
|
|
685
|
+
};
|
|
686
|
+
}
|
|
687
|
+
/**
|
|
688
|
+
* A text frame as the floating box it is: the paragraphs inside it lay out at
|
|
689
|
+
* the frame's width, the box takes the height the frame states or the text
|
|
690
|
+
* needs, and the body flows past it as its `w:wrap` says.
|
|
691
|
+
*
|
|
692
|
+
* @param paragraphs The frame's paragraphs, in order.
|
|
693
|
+
* @param options Render options.
|
|
694
|
+
* @param fontResources Fonts by variant.
|
|
695
|
+
* @param imageResources Images by resource id.
|
|
696
|
+
* @param contentWidth The width available where the frame is anchored.
|
|
697
|
+
* @returns The laid-out frame.
|
|
698
|
+
*/
|
|
699
|
+
function layoutFrameBlock(paragraphs, options, fontResources, imageResources, contentWidth) {
|
|
700
|
+
const frame = paragraphs[0].properties.frame;
|
|
701
|
+
const exact = frame.heightRule === "exact" && frame.heightPt !== void 0;
|
|
702
|
+
const raw = (widthPt, pin) => layoutShapeBlock(frameShape(frame, paragraphs, widthPt, pin), options, fontResources, imageResources, contentWidth);
|
|
703
|
+
const atLeast = !exact && frame.heightPt !== void 0 && frame.heightRule !== "auto" ? frame.heightPt : 0;
|
|
704
|
+
const build = (widthPt) => {
|
|
705
|
+
const laid = raw(widthPt, exact);
|
|
706
|
+
return laid.kind === "shape" && atLeast > laid.heightPt ? raw(widthPt, true) : laid;
|
|
707
|
+
};
|
|
708
|
+
if (frame.widthPt !== void 0) return build(frame.widthPt);
|
|
709
|
+
const measured = build(contentWidth);
|
|
710
|
+
const natural = measured.kind === "shape" ? Math.max(1, ...measured.textLines.map((l) => l.contentWidthPt)) : contentWidth;
|
|
711
|
+
return build(Math.min(contentWidth, natural));
|
|
712
|
+
}
|
|
713
|
+
var MAX_FIT_PASSES = 4;
|
|
714
|
+
function scaleTextBody(text, k) {
|
|
715
|
+
return {
|
|
716
|
+
...text,
|
|
717
|
+
content: text.content.map((el) => el.kind === "paragraph" ? {
|
|
718
|
+
...el,
|
|
719
|
+
paragraph: {
|
|
720
|
+
...el.paragraph,
|
|
721
|
+
runs: el.paragraph.runs.map((run) => ({
|
|
722
|
+
...run,
|
|
723
|
+
properties: {
|
|
724
|
+
...run.properties,
|
|
725
|
+
fontSizePt: pt((run.properties.fontSizePt ?? 11) * k)
|
|
726
|
+
}
|
|
727
|
+
}))
|
|
728
|
+
}
|
|
729
|
+
} : el)
|
|
730
|
+
};
|
|
731
|
+
}
|
|
732
|
+
function frameShape(frame, paragraphs, widthPt, exact) {
|
|
733
|
+
const lead = paragraphs[0]?.properties;
|
|
734
|
+
const edge = lead?.borders?.top ?? lead?.borders?.left;
|
|
735
|
+
const vertical = lead?.textDirection === "btLr" ? "vert270" : lead?.textDirection === "tbRl" ? "vert" : void 0;
|
|
736
|
+
const painted = lead?.shading !== void 0 || edge !== void 0 && edge.style !== "none";
|
|
737
|
+
return {
|
|
738
|
+
width: pt(widthPt),
|
|
739
|
+
height: pt(exact ? frame.heightPt ?? 0 : 0),
|
|
740
|
+
geometry: painted ? {
|
|
741
|
+
kind: "preset",
|
|
742
|
+
preset: "rect",
|
|
743
|
+
adjust: /* @__PURE__ */ new Map()
|
|
744
|
+
} : {
|
|
745
|
+
kind: "custom",
|
|
746
|
+
custom: {
|
|
747
|
+
pathWidth: 0,
|
|
748
|
+
pathHeight: 0,
|
|
749
|
+
commands: []
|
|
750
|
+
}
|
|
751
|
+
},
|
|
752
|
+
fill: lead?.shading ? {
|
|
753
|
+
kind: "solid",
|
|
754
|
+
colorHex: lead.shading.colorHex
|
|
755
|
+
} : { kind: "none" },
|
|
756
|
+
...edge && edge.style !== "none" ? { line: {
|
|
757
|
+
fill: "solid",
|
|
758
|
+
colorHex: edge.colorHex ?? "000000",
|
|
759
|
+
width: edge.width ?? pt(.75)
|
|
760
|
+
} } : {},
|
|
761
|
+
float: frameFloat(frame),
|
|
762
|
+
paragraphProperties: {},
|
|
763
|
+
text: {
|
|
764
|
+
content: paragraphs.map((paragraph) => ({
|
|
765
|
+
kind: "paragraph",
|
|
766
|
+
paragraph
|
|
767
|
+
})),
|
|
768
|
+
insetLeft: pt(0),
|
|
769
|
+
insetRight: pt(0),
|
|
770
|
+
insetTop: pt(0),
|
|
771
|
+
insetBottom: pt(0),
|
|
772
|
+
...vertical ? { vertical } : {},
|
|
773
|
+
...exact ? {} : { autoFit: true }
|
|
774
|
+
}
|
|
775
|
+
};
|
|
776
|
+
}
|
|
777
|
+
/** §17.3.1.11 → §20.4.2.3: the frame's anchor, in the terms floats are placed in. */
|
|
778
|
+
function frameFloat(frame) {
|
|
779
|
+
const wrap = frame.wrap === "none" ? "none" : frame.wrap === "notBeside" ? "topAndBottom" : "square";
|
|
780
|
+
const hRel = frame.hAnchor === "page" ? "page" : frame.hAnchor === "margin" ? "margin" : "column";
|
|
781
|
+
const vRel = frame.vAnchor === "page" ? "page" : frame.vAnchor === "margin" ? "margin" : "paragraph";
|
|
782
|
+
const align = frame.xAlign === "left" || frame.xAlign === "center" || frame.xAlign === "right" ? frame.xAlign : void 0;
|
|
783
|
+
const vAlign = frame.yAlign === "top" || frame.yAlign === "center" || frame.yAlign === "bottom" ? frame.yAlign : void 0;
|
|
784
|
+
return {
|
|
785
|
+
wrap,
|
|
786
|
+
posH: {
|
|
787
|
+
relativeFrom: hRel,
|
|
788
|
+
...align ? { align } : { offsetPt: frame.xPt ?? pt(0) }
|
|
789
|
+
},
|
|
790
|
+
posV: {
|
|
791
|
+
relativeFrom: vRel,
|
|
792
|
+
...vAlign ? { align: vAlign } : { offsetPt: frame.yPt ?? pt(0) }
|
|
793
|
+
}
|
|
794
|
+
};
|
|
795
|
+
}
|
|
796
|
+
/**
|
|
797
|
+
* A drawing's width when it states one as a share of the page or the margins.
|
|
798
|
+
*
|
|
799
|
+
* @param rel The relative size, if any.
|
|
800
|
+
* @param content The margin box's width.
|
|
801
|
+
* @param box The page box, when the caller knows it.
|
|
802
|
+
* @returns The width in points, or `undefined` when none is stated.
|
|
803
|
+
*/
|
|
804
|
+
function relativeWidth(rel, content, box) {
|
|
805
|
+
if (!rel?.widthPct) return void 0;
|
|
806
|
+
const from = rel.widthFrom;
|
|
807
|
+
return (box === void 0 || from === void 0 || from === "margin" ? content : from === "page" ? box.pageWidthPt : from === "leftMargin" ? box.marginLeftPt : box.marginRightPt) * rel.widthPct;
|
|
808
|
+
}
|
|
809
|
+
/** The height twin of {@link relativeWidth}; the margin box's height is the page's content height. */
|
|
810
|
+
function relativeHeight(rel, contentHeight, box) {
|
|
811
|
+
if (!rel?.heightPct) return void 0;
|
|
812
|
+
const from = rel.heightFrom;
|
|
813
|
+
const base = box === void 0 || from === void 0 || from === "margin" ? contentHeight : from === "page" ? box.pageHeightPt : from === "topMargin" ? box.marginTopPt : box.marginBottomPt;
|
|
814
|
+
return base === void 0 ? void 0 : base * rel.heightPct;
|
|
528
815
|
}
|
|
529
|
-
function
|
|
816
|
+
function laidOutBlocksFor(elements, options, fontResources, contentWidth, imageResources, box) {
|
|
817
|
+
const frames = frameGroups(elements);
|
|
818
|
+
return elements.map((el, idx) => {
|
|
819
|
+
const group = frames.starts.get(idx);
|
|
820
|
+
if (group) return layoutFrameBlock(group, options, fontResources, imageResources, contentWidth);
|
|
821
|
+
if (frames.continued.has(idx)) return emptyBlock(options);
|
|
822
|
+
return layoutBodyElement(el, options, fontResources, imageResources, contentWidth, void 0, box);
|
|
823
|
+
});
|
|
824
|
+
}
|
|
825
|
+
function layoutBodyElement(el, options, fontResources, imageResources, contentWidth, maxHeight, box) {
|
|
530
826
|
if (el.kind === "paragraph") return layoutParagraphBlock(el.paragraph, options, fontResources, imageResources, contentWidth);
|
|
531
827
|
if (el.kind === "table") return layoutTableBlock(el.table, options, fontResources, imageResources, contentWidth);
|
|
532
|
-
if (el.kind === "image") return layoutImageBlock(el.image, imageResources, contentWidth);
|
|
828
|
+
if (el.kind === "image") return layoutImageBlock(el.image, imageResources, contentWidth, maxHeight, box);
|
|
533
829
|
if (el.kind === "chart") return layoutChartBlock(el.chart, options, fontResources, contentWidth, maxHeight);
|
|
534
|
-
return layoutShapeBlock(el.shape, options, fontResources, imageResources, contentWidth, maxHeight);
|
|
830
|
+
return layoutShapeBlock(el.shape, options, fontResources, imageResources, contentWidth, maxHeight, box);
|
|
535
831
|
}
|
|
536
|
-
function layoutImageBlock(image, imageResources, contentWidth) {
|
|
537
|
-
let widthPt = image.width;
|
|
538
|
-
let heightPt = image.height;
|
|
539
|
-
if (widthPt > contentWidth) {
|
|
832
|
+
function layoutImageBlock(image, imageResources, contentWidth, maxHeight, box) {
|
|
833
|
+
let widthPt = relativeWidth(image.relativeSize, contentWidth, box) ?? image.width;
|
|
834
|
+
let heightPt = relativeHeight(image.relativeSize, maxHeight, box) ?? (image.relativeSize?.widthPct && image.width > 0 ? image.height * (widthPt / image.width) : image.height);
|
|
835
|
+
if (widthPt > contentWidth && !image.float) {
|
|
540
836
|
const scale = contentWidth / widthPt;
|
|
541
837
|
widthPt = contentWidth;
|
|
542
838
|
heightPt = heightPt * scale;
|
|
543
839
|
}
|
|
840
|
+
let drawInset;
|
|
841
|
+
const eff = image.effectExtent;
|
|
842
|
+
if (eff) {
|
|
843
|
+
const s = image.width > 0 ? widthPt / image.width : 1;
|
|
844
|
+
let box = {
|
|
845
|
+
dxPt: eff.leftPt * s,
|
|
846
|
+
dyTopPt: eff.topPt * s,
|
|
847
|
+
widthPt,
|
|
848
|
+
heightPt
|
|
849
|
+
};
|
|
850
|
+
let reservedW = widthPt + (eff.leftPt + eff.rightPt) * s;
|
|
851
|
+
let reservedH = heightPt + (eff.topPt + eff.bottomPt) * s;
|
|
852
|
+
if (reservedW > contentWidth) {
|
|
853
|
+
const k = contentWidth / reservedW;
|
|
854
|
+
reservedW = contentWidth;
|
|
855
|
+
reservedH *= k;
|
|
856
|
+
box = {
|
|
857
|
+
dxPt: box.dxPt * k,
|
|
858
|
+
dyTopPt: box.dyTopPt * k,
|
|
859
|
+
widthPt: box.widthPt * k,
|
|
860
|
+
heightPt: box.heightPt * k
|
|
861
|
+
};
|
|
862
|
+
}
|
|
863
|
+
widthPt = reservedW;
|
|
864
|
+
heightPt = reservedH;
|
|
865
|
+
drawInset = box;
|
|
866
|
+
}
|
|
544
867
|
const res = image.resource ? imageResources?.get(image.resource) : void 0;
|
|
545
868
|
const resolvedAlignment = image.paragraphProperties.alignment ?? "left";
|
|
546
869
|
return {
|
|
@@ -549,27 +872,33 @@ function layoutImageBlock(image, imageResources, contentWidth) {
|
|
|
549
872
|
heightPt,
|
|
550
873
|
resolvedAlignment,
|
|
551
874
|
resourceName: res?.resourceName ?? "",
|
|
875
|
+
...image.outline ? { outline: image.outline } : {},
|
|
876
|
+
...image.crop ? { crop: image.crop } : {},
|
|
877
|
+
...image.rotation60k ? { rotationDeg: image.rotation60k / 6e4 } : {},
|
|
878
|
+
...image.flipH ? { flipH: true } : {},
|
|
879
|
+
...image.flipV ? { flipV: true } : {},
|
|
880
|
+
...drawInset ? { drawInset } : {},
|
|
552
881
|
spacingBeforePt: image.paragraphProperties.spacingBefore ?? 0,
|
|
553
882
|
spacingAfterPt: image.paragraphProperties.spacingAfter ?? 0,
|
|
554
883
|
...image.altText ? { altText: image.altText } : {},
|
|
555
884
|
...image.float ? { float: image.float } : {}
|
|
556
885
|
};
|
|
557
886
|
}
|
|
558
|
-
function layoutShapeBlock(shape, options, fontResources, imageResources, contentWidth, maxHeight) {
|
|
559
|
-
let widthPt = shape.width;
|
|
560
|
-
let heightPt = shape.height;
|
|
561
|
-
if (widthPt > contentWidth && widthPt > 0) {
|
|
887
|
+
function layoutShapeBlock(shape, options, fontResources, imageResources, contentWidth, maxHeight, box, fitPass = 0) {
|
|
888
|
+
let widthPt = relativeWidth(shape.relativeSize, contentWidth, box) ?? shape.width;
|
|
889
|
+
let heightPt = relativeHeight(shape.relativeSize, maxHeight, box) ?? (shape.relativeSize?.widthPct && shape.width > 0 ? shape.height * (widthPt / shape.width) : shape.height);
|
|
890
|
+
if (!shape.float && widthPt > contentWidth && widthPt > 0) {
|
|
562
891
|
const scale = contentWidth / widthPt;
|
|
563
892
|
widthPt = contentWidth;
|
|
564
893
|
heightPt *= scale;
|
|
565
894
|
}
|
|
566
|
-
if (maxHeight !== void 0 && heightPt > maxHeight && heightPt > 0) {
|
|
895
|
+
if (!shape.float && maxHeight !== void 0 && heightPt > maxHeight && heightPt > 0) {
|
|
567
896
|
const scale = maxHeight / heightPt;
|
|
568
897
|
widthPt *= scale;
|
|
569
898
|
heightPt = maxHeight;
|
|
570
899
|
}
|
|
571
|
-
const paths = buildShapePaths(shape.geometry, widthPt, heightPt);
|
|
572
900
|
const fillGradient = shape.fill.kind === "gradient" ? shape.fill.gradient : void 0;
|
|
901
|
+
const fillImageResourceName = shape.fill.kind === "picture" && shape.fill.imageResource !== void 0 ? imageResources?.get(shape.fill.imageResource)?.resourceName : void 0;
|
|
573
902
|
const fillColorHex = shape.fill.kind === "solid" ? shape.fill.colorHex : fillGradient ? gradientToSolid(fillGradient) : void 0;
|
|
574
903
|
const stroke = buildStroke(shape.line);
|
|
575
904
|
const t = shape.transform;
|
|
@@ -580,27 +909,154 @@ function layoutShapeBlock(shape, options, fontResources, imageResources, content
|
|
|
580
909
|
const insetTopPt = text?.insetTop ?? 3.6;
|
|
581
910
|
const insetBottomPt = text?.insetBottom ?? 3.6;
|
|
582
911
|
const textLines = [];
|
|
912
|
+
const textCharts = /* @__PURE__ */ new Map();
|
|
913
|
+
const textShapes = /* @__PURE__ */ new Map();
|
|
914
|
+
const textTables = /* @__PURE__ */ new Map();
|
|
915
|
+
const textLineGaps = /* @__PURE__ */ new Map();
|
|
583
916
|
let textHeightPt = 0;
|
|
917
|
+
const vertical = text?.vertical;
|
|
584
918
|
if (text && text.content.length > 0) {
|
|
585
|
-
const innerWidth = Math.max(1, widthPt - insetLeftPt - insetRightPt);
|
|
919
|
+
const innerWidth = vertical ? Math.max(1, heightPt - insetTopPt - insetBottomPt) : Math.max(1, widthPt - insetLeftPt - insetRightPt);
|
|
586
920
|
for (const el of text.content) {
|
|
587
|
-
if (el.kind
|
|
921
|
+
if (el.kind === "image") {
|
|
922
|
+
const res = el.image.resource ? imageResources?.get(el.image.resource) : void 0;
|
|
923
|
+
const scaled = Math.min(1, innerWidth / Math.max(1, el.image.width));
|
|
924
|
+
const line = {
|
|
925
|
+
tokens: [{
|
|
926
|
+
kind: "image",
|
|
927
|
+
imageResourceName: res?.resourceName ?? "",
|
|
928
|
+
widthPt: el.image.width * scaled,
|
|
929
|
+
heightPt: el.image.height * scaled,
|
|
930
|
+
isSpace: false,
|
|
931
|
+
bidiLevel: 0
|
|
932
|
+
}],
|
|
933
|
+
contentWidthPt: el.image.width * scaled,
|
|
934
|
+
maxFontSizePt: el.image.height * scaled,
|
|
935
|
+
availableWidthPt: innerWidth,
|
|
936
|
+
firstLine: true,
|
|
937
|
+
resolved: resolveParagraphProperties(el.image.paragraphProperties, options.styles),
|
|
938
|
+
isLastInParagraph: true
|
|
939
|
+
};
|
|
940
|
+
textLines.push(line);
|
|
941
|
+
textHeightPt += computeLineHeight(line, line.resolved);
|
|
942
|
+
continue;
|
|
943
|
+
}
|
|
944
|
+
if (el.kind === "chart") {
|
|
945
|
+
const laid = layoutChartBlock(el.chart, options, fontResources, innerWidth);
|
|
946
|
+
const line = {
|
|
947
|
+
tokens: [],
|
|
948
|
+
contentWidthPt: laid.widthPt,
|
|
949
|
+
maxFontSizePt: laid.heightPt,
|
|
950
|
+
availableWidthPt: innerWidth,
|
|
951
|
+
firstLine: true,
|
|
952
|
+
resolved: resolveParagraphProperties(el.chart.paragraphProperties, options.styles),
|
|
953
|
+
isLastInParagraph: true
|
|
954
|
+
};
|
|
955
|
+
textCharts.set(textLines.length, laid);
|
|
956
|
+
textLines.push(line);
|
|
957
|
+
textHeightPt += computeLineHeight(line, line.resolved);
|
|
958
|
+
continue;
|
|
959
|
+
}
|
|
960
|
+
if (el.kind === "shape") {
|
|
961
|
+
const laid = layoutShapeBlock(el.shape, options, fontResources, imageResources, innerWidth);
|
|
962
|
+
const line = {
|
|
963
|
+
tokens: [],
|
|
964
|
+
contentWidthPt: laid.widthPt,
|
|
965
|
+
maxFontSizePt: laid.heightPt,
|
|
966
|
+
availableWidthPt: innerWidth,
|
|
967
|
+
firstLine: true,
|
|
968
|
+
resolved: resolveParagraphProperties(el.shape.paragraphProperties, options.styles),
|
|
969
|
+
isLastInParagraph: true
|
|
970
|
+
};
|
|
971
|
+
textShapes.set(textLines.length, laid);
|
|
972
|
+
textLines.push(line);
|
|
973
|
+
textHeightPt += computeLineHeight(line, line.resolved);
|
|
974
|
+
continue;
|
|
975
|
+
}
|
|
976
|
+
if (el.kind === "table") {
|
|
977
|
+
const laid = layoutTableBlock(el.table, options, fontResources, imageResources, innerWidth);
|
|
978
|
+
const line = {
|
|
979
|
+
tokens: [],
|
|
980
|
+
contentWidthPt: laid.totalWidthPt,
|
|
981
|
+
maxFontSizePt: laid.heightPt,
|
|
982
|
+
metricHeightPt: laid.heightPt,
|
|
983
|
+
availableWidthPt: innerWidth,
|
|
984
|
+
firstLine: true,
|
|
985
|
+
resolved: resolveParagraphProperties({}, options.styles),
|
|
986
|
+
isLastInParagraph: true
|
|
987
|
+
};
|
|
988
|
+
textTables.set(textLines.length, laid);
|
|
989
|
+
textLines.push(line);
|
|
990
|
+
textHeightPt += computeLineHeight(line, line.resolved);
|
|
991
|
+
continue;
|
|
992
|
+
}
|
|
588
993
|
const blk = layoutParagraphBlock(el.paragraph, options, fontResources, imageResources, innerWidth);
|
|
994
|
+
const before = textLines.length > 0 ? blk.spacingBeforePt : 0;
|
|
995
|
+
if (before > 0) {
|
|
996
|
+
const last = textLines.length - 1;
|
|
997
|
+
textLineGaps.set(last, (textLineGaps.get(last) ?? 0) + before);
|
|
998
|
+
textHeightPt += before;
|
|
999
|
+
}
|
|
589
1000
|
for (const line of blk.lines) {
|
|
590
1001
|
textLines.push(line);
|
|
591
1002
|
textHeightPt += computeLineHeight(line, blk.resolved);
|
|
592
1003
|
}
|
|
1004
|
+
if (blk.spacingAfterPt > 0) {
|
|
1005
|
+
const last = textLines.length - 1;
|
|
1006
|
+
textLineGaps.set(last, (textLineGaps.get(last) ?? 0) + blk.spacingAfterPt);
|
|
1007
|
+
}
|
|
593
1008
|
textHeightPt += blk.spacingAfterPt;
|
|
594
1009
|
}
|
|
595
1010
|
}
|
|
1011
|
+
if (text?.autoFit && !vertical && textLines.length > 0) heightPt = textHeightPt + insetTopPt + insetBottomPt;
|
|
1012
|
+
if (text?.fitToBox === true && textLines.length > 0 && fitPass < MAX_FIT_PASSES) {
|
|
1013
|
+
const innerW = Math.max(1, widthPt - insetLeftPt - insetRightPt);
|
|
1014
|
+
const innerH = Math.max(1, heightPt - insetTopPt - insetBottomPt);
|
|
1015
|
+
let widest = 0;
|
|
1016
|
+
let running = 0;
|
|
1017
|
+
let paragraphs = 0;
|
|
1018
|
+
for (const l of textLines) {
|
|
1019
|
+
running += l.contentWidthPt;
|
|
1020
|
+
if (l.isLastInParagraph) {
|
|
1021
|
+
widest = Math.max(widest, running);
|
|
1022
|
+
running = 0;
|
|
1023
|
+
paragraphs++;
|
|
1024
|
+
}
|
|
1025
|
+
}
|
|
1026
|
+
widest = Math.max(1, widest, running);
|
|
1027
|
+
const perLine = textHeightPt / Math.max(1, textLines.length);
|
|
1028
|
+
const naturalHeight = Math.max(1, perLine * Math.max(1, paragraphs || textLines.length));
|
|
1029
|
+
const k = Math.min(innerW / widest, innerH / naturalHeight);
|
|
1030
|
+
if (Number.isFinite(k) && k > .05 && k < 20 && Math.abs(k - 1) > .02) return layoutShapeBlock({
|
|
1031
|
+
...shape,
|
|
1032
|
+
text: scaleTextBody(text, k)
|
|
1033
|
+
}, options, fontResources, imageResources, contentWidth, maxHeight, box, fitPass + 1);
|
|
1034
|
+
}
|
|
1035
|
+
const paths = buildShapePaths(shape.geometry, widthPt, heightPt);
|
|
1036
|
+
const markerPaths = lineEndPaths(paths, shape.line, stroke?.widthPt ?? 0);
|
|
1037
|
+
const scale = shape.width > 0 ? widthPt / shape.width : 1;
|
|
1038
|
+
const children = (shape.children ?? []).map((child) => ({
|
|
1039
|
+
laid: layoutShapeBlock({
|
|
1040
|
+
...child.shape,
|
|
1041
|
+
width: pt(child.shape.width * scale),
|
|
1042
|
+
height: pt(child.shape.height * scale)
|
|
1043
|
+
}, options, fontResources, imageResources, Number.POSITIVE_INFINITY),
|
|
1044
|
+
xPt: child.xPt * scale,
|
|
1045
|
+
yPt: child.yPt * scale
|
|
1046
|
+
}));
|
|
596
1047
|
return {
|
|
597
1048
|
kind: "shape",
|
|
598
1049
|
widthPt,
|
|
599
1050
|
heightPt,
|
|
1051
|
+
...children.length > 0 ? { children } : {},
|
|
600
1052
|
paths,
|
|
1053
|
+
...markerPaths.length > 0 ? { markerPaths } : {},
|
|
1054
|
+
...fillImageResourceName ? { fillImageResourceName } : {},
|
|
1055
|
+
...fillImageResourceName && shape.fill.imageCrop ? { fillImageCrop: shape.fill.imageCrop } : {},
|
|
601
1056
|
...fillColorHex ? { fillColorHex } : {},
|
|
602
1057
|
...fillGradient ? { fillGradient } : {},
|
|
603
1058
|
...stroke ? { stroke } : {},
|
|
1059
|
+
...shape.shadow ? { shadow: shape.shadow } : {},
|
|
604
1060
|
rotation60k: t?.rotation60k ?? 0,
|
|
605
1061
|
flipH: t?.flipH ?? false,
|
|
606
1062
|
flipV: t?.flipV ?? false,
|
|
@@ -608,12 +1064,17 @@ function layoutShapeBlock(shape, options, fontResources, imageResources, content
|
|
|
608
1064
|
spacingBeforePt: pp.spacingBefore ?? 0,
|
|
609
1065
|
spacingAfterPt: pp.spacingAfter ?? 0,
|
|
610
1066
|
textLines,
|
|
1067
|
+
...textCharts.size > 0 ? { textCharts } : {},
|
|
1068
|
+
...textShapes.size > 0 ? { textShapes } : {},
|
|
1069
|
+
...textTables.size > 0 ? { textTables } : {},
|
|
1070
|
+
...textLineGaps.size > 0 ? { textLineGaps } : {},
|
|
611
1071
|
textHeightPt,
|
|
612
1072
|
insetLeftPt,
|
|
613
1073
|
insetRightPt,
|
|
614
1074
|
insetTopPt,
|
|
615
1075
|
insetBottomPt,
|
|
616
1076
|
anchor: text?.anchor ?? "t",
|
|
1077
|
+
...vertical ? { vertical } : {},
|
|
617
1078
|
...shape.altText ? { altText: shape.altText } : {},
|
|
618
1079
|
...shape.float ? { float: shape.float } : {}
|
|
619
1080
|
};
|
|
@@ -666,6 +1127,9 @@ function buildChartLayout(chart, widthPt, heightPt, font) {
|
|
|
666
1127
|
texts: []
|
|
667
1128
|
};
|
|
668
1129
|
const shapes = [];
|
|
1130
|
+
if (scene.background) shapes.push(rectPrim(scene.background));
|
|
1131
|
+
if (scene.plotBackground) shapes.push(rectPrim(scene.plotBackground));
|
|
1132
|
+
for (const g of scene.gridlines ?? []) shapes.push(polylinePrim(g));
|
|
669
1133
|
for (const pg of scene.polygons ?? []) shapes.push(polygonPrim(pg));
|
|
670
1134
|
for (const r of scene.rects) shapes.push(rectPrim(r));
|
|
671
1135
|
for (const p of scene.polylines) shapes.push(polylinePrim(p));
|
|
@@ -682,7 +1146,6 @@ var CF_ICON_SIZE_PT = 9;
|
|
|
682
1146
|
var CF_ICON_GUTTER_PT = 12;
|
|
683
1147
|
var CF_ICON_EMPTY_HEX = "BFBFBF";
|
|
684
1148
|
var DROPDOWN_BUTTON_PT = 11;
|
|
685
|
-
var DROPDOWN_GUTTER_PT = 13;
|
|
686
1149
|
var DROPDOWN_FILL_HEX = "F0F0F0";
|
|
687
1150
|
var DROPDOWN_BORDER_HEX = "B0B0B0";
|
|
688
1151
|
var DROPDOWN_ARROW_HEX = "595959";
|
|
@@ -791,14 +1254,20 @@ function circlePath(cx, cy, r) {
|
|
|
791
1254
|
const k = .5522847498307936 * r;
|
|
792
1255
|
return new PathBuilder().moveTo(cx + r, cy).cubicTo(cx + r, cy + k, cx + k, cy + r, cx, cy + r).cubicTo(cx - k, cy + r, cx - r, cy + k, cx - r, cy).cubicTo(cx - r, cy - k, cx - k, cy - r, cx, cy - r).cubicTo(cx + k, cy - r, cx + r, cy - k, cx + r, cy).close().build();
|
|
793
1256
|
}
|
|
1257
|
+
function strokeOfRect(r) {
|
|
1258
|
+
if (!r.strokeHex) return {};
|
|
1259
|
+
const stroke = buildStroke({
|
|
1260
|
+
colorHex: r.strokeHex,
|
|
1261
|
+
width: pt(r.strokeWidthPt ?? 1),
|
|
1262
|
+
...r.strokeDash ? { dash: r.strokeDash } : {}
|
|
1263
|
+
});
|
|
1264
|
+
return stroke ? { stroke } : {};
|
|
1265
|
+
}
|
|
794
1266
|
function rectPrim(r) {
|
|
795
1267
|
return {
|
|
796
1268
|
paths: [rectAtPath(r.x, r.y, r.w, r.h)],
|
|
797
1269
|
...r.fillHex ? { fillColorHex: r.fillHex } : {},
|
|
798
|
-
...r
|
|
799
|
-
colorHex: r.strokeHex,
|
|
800
|
-
widthPt: r.strokeWidthPt ?? 1
|
|
801
|
-
} } : {}
|
|
1270
|
+
...strokeOfRect(r)
|
|
802
1271
|
};
|
|
803
1272
|
}
|
|
804
1273
|
function polylinePrim(p) {
|
|
@@ -839,9 +1308,16 @@ function wedgePrim(w) {
|
|
|
839
1308
|
function labelPrim(l, font) {
|
|
840
1309
|
const line = makeChartLabelLine(l.text, font, l.sizePt, l.colorHex);
|
|
841
1310
|
const w = line.contentWidthPt;
|
|
1311
|
+
const shift = l.align === "center" ? -w / 2 : l.align === "right" ? -w : 0;
|
|
1312
|
+
if (l.rotationDeg) return {
|
|
1313
|
+
line,
|
|
1314
|
+
x: l.x,
|
|
1315
|
+
y: l.y + shift,
|
|
1316
|
+
rotationDeg: l.rotationDeg
|
|
1317
|
+
};
|
|
842
1318
|
return {
|
|
843
1319
|
line,
|
|
844
|
-
x: l.
|
|
1320
|
+
x: l.x + shift,
|
|
845
1321
|
y: l.y
|
|
846
1322
|
};
|
|
847
1323
|
}
|
|
@@ -881,10 +1357,16 @@ function collectImageResources(body, headersFooters, options) {
|
|
|
881
1357
|
for (const run of el.paragraph.runs) if (run.inlineImage?.resource) seen.add(run.inlineImage.resource);
|
|
882
1358
|
} else if (el.kind === "table") for (const row of el.table.rows) for (const cell of row.cells) visit(cell.content);
|
|
883
1359
|
else if (el.kind === "shape") {
|
|
884
|
-
|
|
1360
|
+
const shapeImages = (sh) => {
|
|
1361
|
+
if (sh.fill.imageResource) seen.add(sh.fill.imageResource);
|
|
1362
|
+
if (sh.text) visit(sh.text.content);
|
|
1363
|
+
for (const child of sh.children ?? []) shapeImages(child.shape);
|
|
1364
|
+
};
|
|
1365
|
+
shapeImages(el.shape);
|
|
885
1366
|
}
|
|
886
1367
|
};
|
|
887
1368
|
visit(body);
|
|
1369
|
+
if (options.pageBackgroundFill?.imageResource !== void 0) seen.add(options.pageBackgroundFill.imageResource);
|
|
888
1370
|
for (const hf of headersFooters.values()) visit(hf);
|
|
889
1371
|
for (const note of options.footnotes?.values() ?? []) visit(note);
|
|
890
1372
|
for (const note of options.endnotes?.values() ?? []) visit(note);
|
|
@@ -908,12 +1390,257 @@ function collectImageResources(body, headersFooters, options) {
|
|
|
908
1390
|
}
|
|
909
1391
|
return out;
|
|
910
1392
|
}
|
|
911
|
-
function
|
|
1393
|
+
function chartPageItems(laid, x, bottomYUp, pageHeight, structId) {
|
|
1394
|
+
const fig = structId !== void 0 ? { structId } : {};
|
|
1395
|
+
const out = [];
|
|
1396
|
+
for (const sh of laid.layout.shapes) out.push({
|
|
1397
|
+
type: "shape",
|
|
1398
|
+
shape: {
|
|
1399
|
+
paths: sh.paths,
|
|
1400
|
+
...sh.fillColorHex ? { fillColorHex: sh.fillColorHex } : {},
|
|
1401
|
+
...sh.stroke ? { stroke: sh.stroke } : {},
|
|
1402
|
+
transform: flipTransform([
|
|
1403
|
+
1,
|
|
1404
|
+
0,
|
|
1405
|
+
0,
|
|
1406
|
+
1,
|
|
1407
|
+
x,
|
|
1408
|
+
bottomYUp
|
|
1409
|
+
], pageHeight)
|
|
1410
|
+
},
|
|
1411
|
+
...fig
|
|
1412
|
+
});
|
|
1413
|
+
for (const t of laid.layout.texts) out.push({
|
|
1414
|
+
type: "line",
|
|
1415
|
+
line: t.line,
|
|
1416
|
+
originX: pt(x + t.x),
|
|
1417
|
+
baselineY: pt(pageHeight - (bottomYUp + t.y)),
|
|
1418
|
+
...t.rotationDeg ? { rotationDeg: t.rotationDeg } : {},
|
|
1419
|
+
...fig
|
|
1420
|
+
});
|
|
1421
|
+
return out;
|
|
1422
|
+
}
|
|
1423
|
+
function emitShapeText(sh, x, bottomYUp, sink, pageHeight, figId) {
|
|
1424
|
+
if (sh.textLines.length === 0) return;
|
|
1425
|
+
if (sh.vertical) {
|
|
1426
|
+
const up = sh.vertical === "vert270";
|
|
1427
|
+
const innerHeight = Math.max(1, sh.heightPt - sh.insetTopPt - sh.insetBottomPt);
|
|
1428
|
+
let across = sh.anchor === "b" ? sh.widthPt - sh.insetRightPt - sh.textHeightPt : sh.anchor === "ctr" ? (sh.widthPt - sh.textHeightPt) / 2 : sh.insetLeftPt;
|
|
1429
|
+
for (const line of sh.textLines) {
|
|
1430
|
+
const h = computeLineHeight(line, line.resolved);
|
|
1431
|
+
const lineOffset = alignmentOffset(line.resolved.alignment, line.contentWidthPt, innerHeight);
|
|
1432
|
+
const off = lineBaselineOffset(line, line.resolved);
|
|
1433
|
+
const baselineAcross = up ? across + h - off : across + off;
|
|
1434
|
+
sink.push({
|
|
1435
|
+
type: "line",
|
|
1436
|
+
line,
|
|
1437
|
+
originX: pt(up ? x + baselineAcross : x + sh.widthPt - baselineAcross),
|
|
1438
|
+
baselineY: pt(pageHeight - (up ? bottomYUp + sh.insetBottomPt + lineOffset : bottomYUp + sh.heightPt - sh.insetTopPt - lineOffset)),
|
|
1439
|
+
rotationDeg: up ? 90 : -90,
|
|
1440
|
+
...figId !== void 0 ? { structId: figId } : {}
|
|
1441
|
+
});
|
|
1442
|
+
across += h;
|
|
1443
|
+
}
|
|
1444
|
+
return;
|
|
1445
|
+
}
|
|
1446
|
+
const shapeTop = bottomYUp + sh.heightPt;
|
|
1447
|
+
const innerWidth = Math.max(1, sh.widthPt - sh.insetLeftPt - sh.insetRightPt);
|
|
1448
|
+
let textY;
|
|
1449
|
+
if (sh.anchor === "b") textY = bottomYUp + sh.insetBottomPt + sh.textHeightPt;
|
|
1450
|
+
else if (sh.anchor === "ctr") textY = bottomYUp + (sh.heightPt + sh.textHeightPt) / 2;
|
|
1451
|
+
else textY = shapeTop - sh.insetTopPt;
|
|
1452
|
+
sh.textLines.forEach((line, i) => {
|
|
1453
|
+
textY -= computeLineHeight(line, line.resolved);
|
|
1454
|
+
const lineOffset = alignmentOffset(line.resolved.alignment, line.contentWidthPt, innerWidth);
|
|
1455
|
+
const nested = sh.textCharts?.get(i);
|
|
1456
|
+
if (nested) {
|
|
1457
|
+
sink.push(...chartPageItems(nested, x + sh.insetLeftPt + lineOffset, textY, pageHeight, figId));
|
|
1458
|
+
return;
|
|
1459
|
+
}
|
|
1460
|
+
const drawing = sh.textShapes?.get(i);
|
|
1461
|
+
if (drawing) {
|
|
1462
|
+
emitShapeItems(drawing, x + sh.insetLeftPt + lineOffset, textY, sink, pageHeight, figId);
|
|
1463
|
+
return;
|
|
1464
|
+
}
|
|
1465
|
+
const table = sh.textTables?.get(i);
|
|
1466
|
+
if (table) {
|
|
1467
|
+
let rowY = textY + table.heightPt;
|
|
1468
|
+
for (const row of table.rows) {
|
|
1469
|
+
emitRowChunk(sink, row, x + sh.insetLeftPt + lineOffset + table.xOffsetPt, rowY, pageHeight, table.colCount);
|
|
1470
|
+
rowY -= row.heightPt;
|
|
1471
|
+
}
|
|
1472
|
+
return;
|
|
1473
|
+
}
|
|
1474
|
+
sink.push({
|
|
1475
|
+
type: "line",
|
|
1476
|
+
line,
|
|
1477
|
+
originX: pt(x + sh.insetLeftPt + lineOffset),
|
|
1478
|
+
baselineY: pt(pageHeight - (textY + lineBaselineOffset(line, line.resolved))),
|
|
1479
|
+
...figId !== void 0 ? { structId: figId } : {}
|
|
1480
|
+
});
|
|
1481
|
+
textY -= sh.textLineGaps?.get(i) ?? 0;
|
|
1482
|
+
});
|
|
1483
|
+
}
|
|
1484
|
+
function emitShapeItems(sh, x, bottomYUp, sink, pageHeight, figId) {
|
|
1485
|
+
if (sh.fillImageResourceName) sink.push({
|
|
1486
|
+
type: "image",
|
|
1487
|
+
x: pt(x),
|
|
1488
|
+
y: pt(pageHeight - (bottomYUp + sh.heightPt)),
|
|
1489
|
+
width: pt(sh.widthPt),
|
|
1490
|
+
height: pt(sh.heightPt),
|
|
1491
|
+
imageResourceName: sh.fillImageResourceName,
|
|
1492
|
+
...sh.fillImageCrop ? { crop: sh.fillImageCrop } : {},
|
|
1493
|
+
clip: {
|
|
1494
|
+
paths: sh.paths,
|
|
1495
|
+
transform: flipTransform(buildShapeTransform(x, bottomYUp, sh.widthPt, sh.heightPt, sh.rotation60k, sh.flipH, sh.flipV), pageHeight)
|
|
1496
|
+
},
|
|
1497
|
+
...figId !== void 0 ? { structId: figId } : {}
|
|
1498
|
+
});
|
|
1499
|
+
if (sh.markerPaths && sh.stroke) sink.push({
|
|
1500
|
+
type: "shape",
|
|
1501
|
+
shape: {
|
|
1502
|
+
paths: sh.markerPaths,
|
|
1503
|
+
fillColorHex: sh.stroke.colorHex,
|
|
1504
|
+
transform: flipTransform(buildShapeTransform(x, bottomYUp, sh.widthPt, sh.heightPt, sh.rotation60k, sh.flipH, sh.flipV), pageHeight)
|
|
1505
|
+
},
|
|
1506
|
+
...figId !== void 0 ? { structId: figId } : {}
|
|
1507
|
+
});
|
|
1508
|
+
if (sh.paths.some((path) => path.segments.length > 0)) sink.push({
|
|
1509
|
+
type: "shape",
|
|
1510
|
+
shape: {
|
|
1511
|
+
paths: sh.paths,
|
|
1512
|
+
...sh.fillColorHex ? { fillColorHex: sh.fillColorHex } : {},
|
|
1513
|
+
...sh.fillGradient ? { fillGradient: sh.fillGradient } : {},
|
|
1514
|
+
...sh.stroke ? { stroke: sh.stroke } : {},
|
|
1515
|
+
...sh.shadow ? { shadow: sh.shadow } : {},
|
|
1516
|
+
transform: flipTransform(buildShapeTransform(x, bottomYUp, sh.widthPt, sh.heightPt, sh.rotation60k, sh.flipH, sh.flipV), pageHeight)
|
|
1517
|
+
},
|
|
1518
|
+
...figId !== void 0 ? { structId: figId } : {}
|
|
1519
|
+
});
|
|
1520
|
+
for (const child of sh.children ?? []) emitShapeItems(child.laid, x + child.xPt, bottomYUp + sh.heightPt - child.yPt - child.laid.heightPt, sink, pageHeight, figId);
|
|
1521
|
+
emitShapeText(sh, x, bottomYUp, sink, pageHeight, figId);
|
|
1522
|
+
}
|
|
1523
|
+
/**
|
|
1524
|
+
* §20.1.2.2.24 — the four rules of a picture's own frame, around the box the
|
|
1525
|
+
* picture is drawn in. Borders paint after images and before shapes, so the
|
|
1526
|
+
* frame lands on top of the picture the way Word draws it.
|
|
1527
|
+
*
|
|
1528
|
+
* @param outline The picture's frame.
|
|
1529
|
+
* @param x Left edge of the picture box.
|
|
1530
|
+
* @param topY Distance from the page TOP down to the box's top edge.
|
|
1531
|
+
* @param widthPt Box width.
|
|
1532
|
+
* @param heightPt Box height.
|
|
1533
|
+
* @param structId Tagged-PDF structure node the picture belongs to.
|
|
1534
|
+
* @returns One {@link BorderItem} per side.
|
|
1535
|
+
*/
|
|
1536
|
+
function pictureOutlineItems(outline, x, topY, widthPt, heightPt, structId) {
|
|
1537
|
+
return [
|
|
1538
|
+
"top",
|
|
1539
|
+
"bottom",
|
|
1540
|
+
"left",
|
|
1541
|
+
"right"
|
|
1542
|
+
].map((side) => ({
|
|
1543
|
+
type: "border",
|
|
1544
|
+
side,
|
|
1545
|
+
x: pt(x),
|
|
1546
|
+
y: pt(topY),
|
|
1547
|
+
width: pt(widthPt),
|
|
1548
|
+
height: pt(heightPt),
|
|
1549
|
+
borderSizePt: outline.widthPt,
|
|
1550
|
+
borderColorHex: outline.colorHex,
|
|
1551
|
+
...structId !== void 0 ? { structId } : {}
|
|
1552
|
+
}));
|
|
1553
|
+
}
|
|
1554
|
+
function bandAnchor(float, widthPt, contentWidth, bandLeftPt = 0) {
|
|
1555
|
+
if (!float) return void 0;
|
|
1556
|
+
const align = float.posH?.align;
|
|
1557
|
+
if (align === "right") return contentWidth - widthPt;
|
|
1558
|
+
if (align === "center") return (contentWidth - widthPt) / 2;
|
|
1559
|
+
if (align === "left") return 0;
|
|
1560
|
+
const offset = float.posH?.offsetPt ?? 0;
|
|
1561
|
+
return float.posH?.relativeFrom === "page" ? offset - bandLeftPt : offset;
|
|
1562
|
+
}
|
|
1563
|
+
/**
|
|
1564
|
+
* §20.4.2.3 — how far DOWN the page a band's anchored drawing starts, in the
|
|
1565
|
+
* band emitter's y-UP frame. A `page`-relative anchor is measured from the
|
|
1566
|
+
* paper's own top edge, whatever the band's cursor has reached: n773061.docx
|
|
1567
|
+
* hangs its header text box 92pt down the page and, measured from the header's
|
|
1568
|
+
* first paragraph, it landed 60pt lower than Word and LibreOffice put it.
|
|
1569
|
+
*
|
|
1570
|
+
* @param float The anchor.
|
|
1571
|
+
* @param cursorY The band's cursor (y-up), where a paragraph-relative
|
|
1572
|
+
* anchor is measured from.
|
|
1573
|
+
* @param pageHeight The page height.
|
|
1574
|
+
* @returns The drawing's top edge, y-up.
|
|
1575
|
+
*/
|
|
1576
|
+
function bandAnchorTop(float, cursorY, pageHeight, heightPt = 0, margins) {
|
|
1577
|
+
const align = float.posV?.align;
|
|
1578
|
+
if (align !== void 0) {
|
|
1579
|
+
const page = float.posV?.relativeFrom === "page" || !margins;
|
|
1580
|
+
const topYUp = page ? pageHeight : pageHeight - margins.top;
|
|
1581
|
+
const bottomYUp = page ? 0 : margins.bottom;
|
|
1582
|
+
if (align === "top") return topYUp;
|
|
1583
|
+
if (align === "bottom") return bottomYUp + heightPt;
|
|
1584
|
+
return bottomYUp + (topYUp - bottomYUp + heightPt) / 2;
|
|
1585
|
+
}
|
|
1586
|
+
const offset = float.posV?.offsetPt ?? 0;
|
|
1587
|
+
return float.posV?.relativeFrom === "page" ? pageHeight - offset : cursorY - offset;
|
|
1588
|
+
}
|
|
1589
|
+
function drawBlocksSequentially(blocks, startX, startY, pageHeight, contentWidth, structId, bandMargins) {
|
|
912
1590
|
const out = [];
|
|
913
1591
|
let cursorY = startY;
|
|
914
|
-
|
|
915
|
-
|
|
1592
|
+
const isBehind = (b) => "float" in b && b.float?.behind === true;
|
|
1593
|
+
const ordered = blocks.some(isBehind) ? [...blocks.filter(isBehind), ...blocks.filter((b) => !isBehind(b))] : blocks;
|
|
1594
|
+
for (const block of ordered) {
|
|
1595
|
+
if (block.kind === "table") {
|
|
1596
|
+
const tableX = startX + block.xOffsetPt;
|
|
1597
|
+
for (const row of block.rows) {
|
|
1598
|
+
emitRowChunk(out, row, tableX, cursorY, pageHeight, block.colCount);
|
|
1599
|
+
cursorY -= row.heightPt;
|
|
1600
|
+
}
|
|
1601
|
+
continue;
|
|
1602
|
+
}
|
|
1603
|
+
if (block.kind === "chart") {
|
|
1604
|
+
cursorY -= block.spacingBeforePt + block.heightPt;
|
|
1605
|
+
const offset = alignmentOffset(block.resolvedAlignment, block.widthPt, contentWidth);
|
|
1606
|
+
out.push(...chartPageItems(block, startX + offset, cursorY, pageHeight, structId));
|
|
1607
|
+
cursorY -= block.spacingAfterPt;
|
|
1608
|
+
continue;
|
|
1609
|
+
}
|
|
1610
|
+
if (block.kind === "image") {
|
|
1611
|
+
cursorY -= block.spacingBeforePt;
|
|
1612
|
+
const anchored = bandAnchor(block.float, block.widthPt, contentWidth, startX);
|
|
1613
|
+
const offset = anchored ?? alignmentOffset(block.resolvedAlignment, block.widthPt, contentWidth);
|
|
1614
|
+
const top = anchored === void 0 || !block.float ? cursorY : bandAnchorTop(block.float, cursorY, pageHeight, block.heightPt, bandMargins);
|
|
1615
|
+
out.push({
|
|
1616
|
+
type: "image",
|
|
1617
|
+
x: pt(startX + offset),
|
|
1618
|
+
y: pt(pageHeight - top),
|
|
1619
|
+
width: pt(block.widthPt),
|
|
1620
|
+
height: pt(block.heightPt),
|
|
1621
|
+
imageResourceName: block.resourceName,
|
|
1622
|
+
...block.crop ? { crop: block.crop } : {},
|
|
1623
|
+
...block.rotationDeg ? { rotationDeg: block.rotationDeg } : {},
|
|
1624
|
+
...block.flipH ? { flipH: true } : {},
|
|
1625
|
+
...block.flipV ? { flipV: true } : {}
|
|
1626
|
+
});
|
|
1627
|
+
if (block.outline) out.push(...pictureOutlineItems(block.outline, startX + offset, pageHeight - top, block.widthPt, block.heightPt, structId));
|
|
1628
|
+
if (anchored === void 0) cursorY -= block.heightPt;
|
|
1629
|
+
cursorY -= block.spacingAfterPt;
|
|
1630
|
+
continue;
|
|
1631
|
+
}
|
|
1632
|
+
if (block.kind === "shape") {
|
|
1633
|
+
cursorY -= block.spacingBeforePt;
|
|
1634
|
+
const anchored = bandAnchor(block.float, block.widthPt, contentWidth, startX);
|
|
1635
|
+
const offset = anchored ?? alignmentOffset(block.resolvedAlignment, block.widthPt, contentWidth);
|
|
1636
|
+
const top = anchored === void 0 || !block.float ? cursorY : bandAnchorTop(block.float, cursorY, pageHeight, block.heightPt, bandMargins);
|
|
1637
|
+
emitShapeItems(block, startX + offset, top - block.heightPt, out, pageHeight, structId);
|
|
1638
|
+
if (anchored === void 0) cursorY -= block.heightPt;
|
|
1639
|
+
cursorY -= block.spacingAfterPt;
|
|
1640
|
+
continue;
|
|
1641
|
+
}
|
|
916
1642
|
cursorY -= block.spacingBeforePt;
|
|
1643
|
+
const bandTopY = cursorY;
|
|
917
1644
|
for (const line of block.lines) {
|
|
918
1645
|
const h = computeLineHeight(line, block.resolved);
|
|
919
1646
|
cursorY -= h;
|
|
@@ -923,14 +1650,60 @@ function drawBlocksSequentially(blocks, startX, startY, pageHeight, structId) {
|
|
|
923
1650
|
type: "line",
|
|
924
1651
|
line,
|
|
925
1652
|
originX: pt(startX + indentLeft + offset),
|
|
926
|
-
baselineY: pt(pageHeight - (cursorY +
|
|
1653
|
+
baselineY: pt(pageHeight - (cursorY + lineBaselineOffset(line, block.resolved))),
|
|
927
1654
|
...structId !== void 0 ? { structId } : {}
|
|
928
1655
|
});
|
|
929
1656
|
}
|
|
1657
|
+
out.push(...paragraphDecoration(block.resolved, startX + block.resolved.indentLeft, startX + contentWidth - block.resolved.indentRight, bandTopY, cursorY, pageHeight));
|
|
930
1658
|
cursorY -= block.spacingAfterPt;
|
|
931
1659
|
}
|
|
932
1660
|
return out;
|
|
933
1661
|
}
|
|
1662
|
+
/**
|
|
1663
|
+
* §17.3.1.31 `w:shd` and §17.3.1.24 `w:pBdr` — the band behind a paragraph and
|
|
1664
|
+
* the rules around it, for a paragraph occupying `topY`…`bottomY` (both in the
|
|
1665
|
+
* y-up page frame) between `x0` and `x1`.
|
|
1666
|
+
*
|
|
1667
|
+
* @returns The fill and border items, in paint order.
|
|
1668
|
+
*/
|
|
1669
|
+
function paragraphDecoration(resolved, x0, x1, topY, bottomY, pageHeight) {
|
|
1670
|
+
const out = [];
|
|
1671
|
+
const width = Math.max(0, x1 - x0);
|
|
1672
|
+
if (resolved.shading) out.push({
|
|
1673
|
+
type: "fill",
|
|
1674
|
+
x: pt(x0),
|
|
1675
|
+
y: pt(pageHeight - topY),
|
|
1676
|
+
width: pt(width),
|
|
1677
|
+
height: pt(Math.max(0, topY - bottomY)),
|
|
1678
|
+
fillColorHex: resolved.shading.colorHex
|
|
1679
|
+
});
|
|
1680
|
+
const borders = resolved.borders;
|
|
1681
|
+
if (!borders) return out;
|
|
1682
|
+
for (const side of [
|
|
1683
|
+
"top",
|
|
1684
|
+
"bottom",
|
|
1685
|
+
"left",
|
|
1686
|
+
"right"
|
|
1687
|
+
]) {
|
|
1688
|
+
const edge = borders[side];
|
|
1689
|
+
if (!edge || edge.style === "none") continue;
|
|
1690
|
+
const gap = edge.spacePt ?? 0;
|
|
1691
|
+
const top = topY + gap;
|
|
1692
|
+
const bottom = bottomY - gap;
|
|
1693
|
+
out.push({
|
|
1694
|
+
type: "border",
|
|
1695
|
+
side,
|
|
1696
|
+
x: pt(x0),
|
|
1697
|
+
y: pt(pageHeight - top),
|
|
1698
|
+
width: pt(width),
|
|
1699
|
+
height: pt(Math.max(0, top - bottom)),
|
|
1700
|
+
borderSizePt: edge.width ?? DEFAULT_BORDER_SIZE_EIGHTH * EIGHTH_PT,
|
|
1701
|
+
borderColorHex: edge.colorHex ?? "000000",
|
|
1702
|
+
...edge.style !== "single" ? { borderStyle: edge.style } : {}
|
|
1703
|
+
});
|
|
1704
|
+
}
|
|
1705
|
+
return out;
|
|
1706
|
+
}
|
|
934
1707
|
function runFontKeyAndParsed(options, ascii, bold, italic) {
|
|
935
1708
|
if (ascii && options.embeddedFonts) {
|
|
936
1709
|
const name = ascii.trim().toLowerCase();
|
|
@@ -981,6 +1754,10 @@ function collectFontResources(body, headersFooters, options) {
|
|
|
981
1754
|
}
|
|
982
1755
|
const shaped = shapeText(run.text, parsed.glyphForCodepoint, parsed.advanceWidths, parsed.ligatures, parsed.kerning, parsed.joiningForms);
|
|
983
1756
|
for (const g of shaped.gids) bucket.gids.add(g);
|
|
1757
|
+
if (run.text.includes(" ")) for (const stop of resolveParagraphProperties(para.properties, options.styles).tabs) {
|
|
1758
|
+
const ch = stop.leader === void 0 ? void 0 : TAB_LEADER_CHARS.get(stop.leader);
|
|
1759
|
+
if (ch !== void 0) bucket.gids.add(parsed.glyphForCodepoint(ch.codePointAt(0)));
|
|
1760
|
+
}
|
|
984
1761
|
};
|
|
985
1762
|
const addMath = (node) => {
|
|
986
1763
|
for (const seg of mathGlyphSegments(node)) {
|
|
@@ -998,7 +1775,7 @@ function collectFontResources(body, headersFooters, options) {
|
|
|
998
1775
|
}
|
|
999
1776
|
};
|
|
1000
1777
|
const visit = (elements) => {
|
|
1001
|
-
for (const el of elements) if (el.kind === "paragraph") for (const run of el.paragraph
|
|
1778
|
+
for (const el of elements) if (el.kind === "paragraph") for (const run of displayCapsRuns(el.paragraph, options)) {
|
|
1002
1779
|
if (run.math) {
|
|
1003
1780
|
addMath(run.math);
|
|
1004
1781
|
continue;
|
|
@@ -1020,9 +1797,22 @@ function collectFontResources(body, headersFooters, options) {
|
|
|
1020
1797
|
}
|
|
1021
1798
|
addRun(run, el.paragraph);
|
|
1022
1799
|
}
|
|
1023
|
-
else if (el.kind === "table") for (const row of el.table.rows) for (const cell of row.cells)
|
|
1800
|
+
else if (el.kind === "table") for (const row of el.table.rows) for (const cell of row.cells) {
|
|
1801
|
+
if (cell.properties.hashOnOverflow === true) for (const inner of cell.content) {
|
|
1802
|
+
if (inner.kind !== "paragraph") continue;
|
|
1803
|
+
for (const run of inner.paragraph.runs) addRun({
|
|
1804
|
+
text: "#",
|
|
1805
|
+
properties: run.properties
|
|
1806
|
+
}, inner.paragraph);
|
|
1807
|
+
}
|
|
1808
|
+
visit(cell.content);
|
|
1809
|
+
}
|
|
1024
1810
|
else if (el.kind === "shape") {
|
|
1025
|
-
|
|
1811
|
+
const shapeText = (sh) => {
|
|
1812
|
+
if (sh.text) visit(sh.text.content);
|
|
1813
|
+
for (const child of sh.children ?? []) shapeText(child.shape);
|
|
1814
|
+
};
|
|
1815
|
+
shapeText(el.shape);
|
|
1026
1816
|
} else if (el.kind === "chart") {
|
|
1027
1817
|
const chart = options.charts?.get(el.chart.chartRelId);
|
|
1028
1818
|
if (chart) {
|
|
@@ -1040,21 +1830,52 @@ function collectFontResources(body, headersFooters, options) {
|
|
|
1040
1830
|
};
|
|
1041
1831
|
if (chart.title) add(chart.title);
|
|
1042
1832
|
for (const c of chart.categories) add(c);
|
|
1043
|
-
|
|
1833
|
+
chart.series.forEach((sr, i) => add(legendSeriesName(sr, i)));
|
|
1834
|
+
if (chart.catAxisTitle) add(chart.catAxisTitle);
|
|
1835
|
+
if (chart.valAxisTitle) add(chart.valAxisTitle);
|
|
1836
|
+
for (const sr of chart.series) for (const label of sr.pointLabels ?? []) add(label.text);
|
|
1044
1837
|
add("0123456789.,-%() ");
|
|
1838
|
+
if (chart.numberFormat) add(chart.numberFormat);
|
|
1045
1839
|
}
|
|
1046
1840
|
}
|
|
1047
1841
|
};
|
|
1048
1842
|
visit(body);
|
|
1843
|
+
if ((options.sections ?? []).some((sec) => sec.properties.lineNumbering)) visit([{
|
|
1844
|
+
kind: "paragraph",
|
|
1845
|
+
paragraph: {
|
|
1846
|
+
properties: {},
|
|
1847
|
+
runs: [{
|
|
1848
|
+
text: "0123456789",
|
|
1849
|
+
properties: {}
|
|
1850
|
+
}]
|
|
1851
|
+
}
|
|
1852
|
+
}]);
|
|
1049
1853
|
for (const hf of headersFooters.values()) visit(hf);
|
|
1050
1854
|
for (const note of options.footnotes?.values() ?? []) visit(note);
|
|
1051
1855
|
for (const note of options.endnotes?.values() ?? []) visit(note);
|
|
1052
|
-
for (const comment of options.comments?.values() ?? [])
|
|
1856
|
+
for (const comment of options.comments?.values() ?? []) {
|
|
1857
|
+
visit(comment.content);
|
|
1858
|
+
visit(commentTailBlocks(comment, 1, {
|
|
1859
|
+
parentN: 1,
|
|
1860
|
+
depth: 1,
|
|
1861
|
+
done: true
|
|
1862
|
+
}));
|
|
1863
|
+
visit([{
|
|
1864
|
+
kind: "paragraph",
|
|
1865
|
+
paragraph: {
|
|
1866
|
+
properties: {},
|
|
1867
|
+
runs: [{
|
|
1868
|
+
text: "0123456789",
|
|
1869
|
+
properties: {}
|
|
1870
|
+
}]
|
|
1871
|
+
}
|
|
1872
|
+
}]);
|
|
1873
|
+
}
|
|
1053
1874
|
if (used.size === 0) {
|
|
1054
1875
|
const regular = options.registry.resolveByStyle(false, false);
|
|
1055
1876
|
used.set(regular.variant, {
|
|
1056
1877
|
parsed: regular.parsed,
|
|
1057
|
-
gids: new Set(
|
|
1878
|
+
gids: /* @__PURE__ */ new Set()
|
|
1058
1879
|
});
|
|
1059
1880
|
}
|
|
1060
1881
|
const out = /* @__PURE__ */ new Map();
|
|
@@ -1071,17 +1892,83 @@ function collectFontResources(body, headersFooters, options) {
|
|
|
1071
1892
|
}
|
|
1072
1893
|
return out;
|
|
1073
1894
|
}
|
|
1895
|
+
/**
|
|
1896
|
+
* The one line an empty paragraph still occupies — no tokens, but as tall as
|
|
1897
|
+
* the paragraph mark's own font (§17.3.1.31 `w:rPr` on the `w:pPr`, which is
|
|
1898
|
+
* exactly the run properties Word keeps for a paragraph that holds no runs).
|
|
1899
|
+
*/
|
|
1900
|
+
function emptyLine(paragraph, options, resolved, availableWidthPt, fontResources) {
|
|
1901
|
+
const mark = resolveRunProperties(paragraph.properties.runProperties ?? {}, paragraph.properties, options.styles);
|
|
1902
|
+
const fontSizePt = mark.fontSizePt;
|
|
1903
|
+
const profile = options.layoutProfile ?? "ream";
|
|
1904
|
+
const { variant } = options.registry.resolveByStyle(mark.bold, mark.italic);
|
|
1905
|
+
const parsed = fontResources.get(variant)?.parsed;
|
|
1906
|
+
const metric = profile !== "ream" && parsed ? fontLeadingPt(parsed, fontSizePt, profile) : void 0;
|
|
1907
|
+
return {
|
|
1908
|
+
tokens: [],
|
|
1909
|
+
contentWidthPt: 0,
|
|
1910
|
+
maxFontSizePt: fontSizePt,
|
|
1911
|
+
availableWidthPt,
|
|
1912
|
+
firstLine: true,
|
|
1913
|
+
resolved,
|
|
1914
|
+
isLastInParagraph: true,
|
|
1915
|
+
...metric && metric.ascentPt + metric.descentPt > 0 ? {
|
|
1916
|
+
metricHeightPt: metric.ascentPt + metric.descentPt + metric.lineGapPt,
|
|
1917
|
+
metricDescentPt: metric.descentPt
|
|
1918
|
+
} : {}
|
|
1919
|
+
};
|
|
1920
|
+
}
|
|
1074
1921
|
function layoutParagraphBlock(paragraph, options, fontResources, imageResources, contentWidth, lineWidths) {
|
|
1075
|
-
const
|
|
1922
|
+
const cascaded = resolveParagraphProperties(paragraph.properties, options.styles);
|
|
1923
|
+
const baseResolved = options.gridLinePitchPt !== void 0 && cascaded.snapToGrid !== false ? {
|
|
1924
|
+
...cascaded,
|
|
1925
|
+
gridLinePitchPt: options.gridLinePitchPt
|
|
1926
|
+
} : cascaded;
|
|
1927
|
+
if (paragraph.properties.sectionBreak === true && paragraph.runs.length === 0) return {
|
|
1928
|
+
kind: "paragraph",
|
|
1929
|
+
resolved: baseResolved,
|
|
1930
|
+
lines: [],
|
|
1931
|
+
heightPt: 0,
|
|
1932
|
+
spacingBeforePt: 0,
|
|
1933
|
+
spacingAfterPt: 0
|
|
1934
|
+
};
|
|
1076
1935
|
const baseDir = paragraphBaseDirection(paragraph, baseResolved);
|
|
1077
|
-
const
|
|
1936
|
+
const swapAlign = (a) => a === "left" ? "right" : a === "right" ? "left" : a;
|
|
1937
|
+
const resolved = baseResolved.bidi === true ? {
|
|
1938
|
+
...baseResolved,
|
|
1939
|
+
alignment: swapAlign(baseResolved.alignment),
|
|
1940
|
+
indentLeft: baseResolved.indentRight,
|
|
1941
|
+
indentRight: baseResolved.indentLeft
|
|
1942
|
+
} : baseDir === "rtl" && baseResolved.alignment === "left" ? {
|
|
1078
1943
|
...baseResolved,
|
|
1079
1944
|
alignment: "right"
|
|
1080
1945
|
} : baseResolved;
|
|
1081
|
-
const
|
|
1946
|
+
const tokens = tokenizeParagraph(paragraph, options, fontResources, imageResources, contentWidth, baseDir);
|
|
1947
|
+
const firstLineWidth = paragraphMaxWidth(resolved, contentWidth, true);
|
|
1948
|
+
const otherWidth = paragraphMaxWidth(resolved, contentWidth, false);
|
|
1949
|
+
const runWrap = (part, firstWidth, widths) => wrap(part, firstWidth, otherWidth, resolved, options.hyphenator, options.layoutProfile ?? "ream", options.doNotExpandShiftReturn === true, widths);
|
|
1950
|
+
const parts = splitAtColumnBreaks(tokens);
|
|
1951
|
+
const columnBreakLines = /* @__PURE__ */ new Set();
|
|
1952
|
+
let wrapped;
|
|
1953
|
+
if (parts.length === 1) wrapped = runWrap(tokens, firstLineWidth, lineWidths);
|
|
1954
|
+
else {
|
|
1955
|
+
wrapped = [];
|
|
1956
|
+
for (const [i, part] of parts.entries()) {
|
|
1957
|
+
const lines = runWrap(part, i === 0 ? firstLineWidth : otherWidth, lineWidths?.slice(wrapped.length));
|
|
1958
|
+
if (i > 0 && lines.length > 0) columnBreakLines.add(wrapped.length);
|
|
1959
|
+
wrapped.push(...i === 0 ? lines : lines.map((l) => ({
|
|
1960
|
+
...l,
|
|
1961
|
+
firstLine: false
|
|
1962
|
+
})));
|
|
1963
|
+
}
|
|
1964
|
+
}
|
|
1965
|
+
const lines = wrapped.length > 0 ? wrapped : [emptyLine(paragraph, options, resolved, Math.max(firstLineWidth, 0), fontResources)];
|
|
1082
1966
|
let heightPt = 0;
|
|
1083
1967
|
for (const line of lines) heightPt += computeLineHeight(line, resolved);
|
|
1084
1968
|
const numbering = paragraph.properties.numbering;
|
|
1969
|
+
const breakIdx = paragraph.runs.findIndex((r) => r.pageBreak === true);
|
|
1970
|
+
const leadingPageBreak = breakIdx >= 0 && paragraph.runs.slice(0, breakIdx + 1).every((r) => r.text === "" && r.inlineImage === void 0);
|
|
1971
|
+
const trailingPageBreak = paragraph.runs.some((r, i) => r.pageBreak === true && (!leadingPageBreak || i > breakIdx));
|
|
1085
1972
|
return {
|
|
1086
1973
|
kind: "paragraph",
|
|
1087
1974
|
resolved,
|
|
@@ -1093,11 +1980,23 @@ function layoutParagraphBlock(paragraph, options, fontResources, imageResources,
|
|
|
1093
1980
|
numId: numbering.numId,
|
|
1094
1981
|
level: numbering.ilvl
|
|
1095
1982
|
} } : {},
|
|
1096
|
-
...
|
|
1983
|
+
...leadingPageBreak ? { pageBreakBefore: true } : {},
|
|
1984
|
+
...trailingPageBreak ? { pageBreakAfter: true } : {},
|
|
1985
|
+
...columnBreakLines.size > 0 ? { columnBreakLines } : {},
|
|
1097
1986
|
...paragraph.bookmarks && paragraph.bookmarks.length > 0 ? { bookmarks: paragraph.bookmarks } : {},
|
|
1098
1987
|
source: paragraph
|
|
1099
1988
|
};
|
|
1100
1989
|
}
|
|
1990
|
+
/**
|
|
1991
|
+
* Word and Excel both draw a super/subscript at about two thirds of the base
|
|
1992
|
+
* size, raised a third of it or dropped a sixth. The base line height does not
|
|
1993
|
+
* change: a superscript inside a line of body text must not open the leading.
|
|
1994
|
+
*/
|
|
1995
|
+
var SCRIPT_SCALE = .66;
|
|
1996
|
+
var SCRIPT_OFFSET = {
|
|
1997
|
+
superscript: 1 / 3,
|
|
1998
|
+
subscript: -1 / 6
|
|
1999
|
+
};
|
|
1101
2000
|
function resolveMathItems(items, fontFor) {
|
|
1102
2001
|
return items.map((it) => {
|
|
1103
2002
|
if (it.kind === "glyph") return {
|
|
@@ -1123,13 +2022,76 @@ function resolveMathItems(items, fontFor) {
|
|
|
1123
2022
|
};
|
|
1124
2023
|
});
|
|
1125
2024
|
}
|
|
2025
|
+
/**
|
|
2026
|
+
* §17.3.2.5 `w:caps` / §17.3.2.33 `w:smallCaps` — the paragraph's runs as they
|
|
2027
|
+
* are DISPLAYED. A capitals run is upper-cased; a small-capitals run is too,
|
|
2028
|
+
* and the letters that were lower case are split out to be set smaller.
|
|
2029
|
+
*
|
|
2030
|
+
* @param paragraph The paragraph.
|
|
2031
|
+
* @param options The render options (for the style cascade).
|
|
2032
|
+
* @returns The runs to lay out — the same array when none asks for capitals.
|
|
2033
|
+
*/
|
|
2034
|
+
function displayCapsRuns(paragraph, options) {
|
|
2035
|
+
let any = false;
|
|
2036
|
+
const out = [];
|
|
2037
|
+
for (const raw of paragraph.runs) {
|
|
2038
|
+
const run = raw.text.includes(SOFT_HYPHEN) ? (any = true, {
|
|
2039
|
+
...raw,
|
|
2040
|
+
text: raw.text.replaceAll(SOFT_HYPHEN, "")
|
|
2041
|
+
}) : raw;
|
|
2042
|
+
const resolved = run.text.length > 0 ? resolveRunProperties(run.properties, paragraph.properties, options.styles) : void 0;
|
|
2043
|
+
if (!resolved || !resolved.caps && !resolved.smallCaps) {
|
|
2044
|
+
out.push(run);
|
|
2045
|
+
continue;
|
|
2046
|
+
}
|
|
2047
|
+
any = true;
|
|
2048
|
+
if (resolved.caps) {
|
|
2049
|
+
out.push({
|
|
2050
|
+
...run,
|
|
2051
|
+
text: run.text.toUpperCase()
|
|
2052
|
+
});
|
|
2053
|
+
continue;
|
|
2054
|
+
}
|
|
2055
|
+
let start = 0;
|
|
2056
|
+
const wasLower = (i) => {
|
|
2057
|
+
const ch = run.text[i];
|
|
2058
|
+
return ch.toLowerCase() === ch && ch.toUpperCase() !== ch;
|
|
2059
|
+
};
|
|
2060
|
+
for (let i = 1; i <= run.text.length; i++) {
|
|
2061
|
+
if (i < run.text.length && wasLower(i) === wasLower(start)) continue;
|
|
2062
|
+
const segment = run.text.slice(start, i).toUpperCase();
|
|
2063
|
+
out.push(wasLower(start) ? {
|
|
2064
|
+
...run,
|
|
2065
|
+
text: segment,
|
|
2066
|
+
properties: {
|
|
2067
|
+
...run.properties,
|
|
2068
|
+
fontSizePt: pt(resolved.fontSizePt * .8)
|
|
2069
|
+
}
|
|
2070
|
+
} : {
|
|
2071
|
+
...run,
|
|
2072
|
+
text: segment
|
|
2073
|
+
});
|
|
2074
|
+
start = i;
|
|
2075
|
+
}
|
|
2076
|
+
}
|
|
2077
|
+
return any ? out : paragraph.runs;
|
|
2078
|
+
}
|
|
1126
2079
|
function tokenizeParagraph(paragraph, options, fontResources, imageResources, contentWidth, baseDir) {
|
|
1127
|
-
const plans = paragraph.
|
|
2080
|
+
const plans = displayCapsRuns(paragraph, options).map((run) => {
|
|
1128
2081
|
if (run.inlineImage) {
|
|
1129
|
-
const
|
|
2082
|
+
const eff = run.inlineImage.effectExtent;
|
|
2083
|
+
const padW = (eff?.leftPt ?? 0) + (eff?.rightPt ?? 0);
|
|
2084
|
+
const padH = (eff?.topPt ?? 0) + (eff?.bottomPt ?? 0);
|
|
2085
|
+
const naturalW = run.inlineImage.width + padW;
|
|
1130
2086
|
const widthPt = Math.min(naturalW, contentWidth);
|
|
1131
2087
|
const scale = naturalW > 0 ? widthPt / naturalW : 1;
|
|
1132
|
-
const heightPt = run.inlineImage.height * scale;
|
|
2088
|
+
const heightPt = (run.inlineImage.height + padH) * scale;
|
|
2089
|
+
const drawBox = eff ? {
|
|
2090
|
+
dxPt: eff.leftPt * scale,
|
|
2091
|
+
dyPt: eff.bottomPt * scale,
|
|
2092
|
+
widthPt: run.inlineImage.width * scale,
|
|
2093
|
+
heightPt: run.inlineImage.height * scale
|
|
2094
|
+
} : void 0;
|
|
1133
2095
|
const res = run.inlineImage.resource ? imageResources?.get(run.inlineImage.resource) : void 0;
|
|
1134
2096
|
const resolvedRun = resolveRunProperties(run.properties, paragraph.properties, options.styles);
|
|
1135
2097
|
const { fontKey } = runFontKeyAndParsed(options, resolvedRun.fontFamily.ascii, resolvedRun.bold, resolvedRun.italic);
|
|
@@ -1141,7 +2103,14 @@ function tokenizeParagraph(paragraph, options, fontResources, imageResources, co
|
|
|
1141
2103
|
isImage: true,
|
|
1142
2104
|
imageWidthPt: widthPt,
|
|
1143
2105
|
imageHeightPt: heightPt,
|
|
1144
|
-
imageResourceName: res?.resourceName ?? ""
|
|
2106
|
+
imageResourceName: res?.resourceName ?? "",
|
|
2107
|
+
...run.inlineImage.outline ? { imageOutline: run.inlineImage.outline } : {},
|
|
2108
|
+
...run.inlineImage.shadow ? { imageShadow: run.inlineImage.shadow } : {},
|
|
2109
|
+
...run.inlineImage.crop ? { imageCrop: run.inlineImage.crop } : {},
|
|
2110
|
+
...run.inlineImage.rotation60k ? { imageRotationDeg: run.inlineImage.rotation60k / 6e4 } : {},
|
|
2111
|
+
...run.inlineImage.flipH ? { imageFlipH: true } : {},
|
|
2112
|
+
...run.inlineImage.flipV ? { imageFlipV: true } : {},
|
|
2113
|
+
...drawBox ? { imageDrawBox: drawBox } : {}
|
|
1145
2114
|
};
|
|
1146
2115
|
}
|
|
1147
2116
|
if (run.math) {
|
|
@@ -1174,11 +2143,13 @@ function tokenizeParagraph(paragraph, options, fontResources, imageResources, co
|
|
|
1174
2143
|
}
|
|
1175
2144
|
const resolvedRun = resolveRunProperties(run.properties, paragraph.properties, options.styles);
|
|
1176
2145
|
const { fontKey } = runFontKeyAndParsed(options, resolvedRun.fontFamily.ascii, resolvedRun.bold, resolvedRun.italic);
|
|
2146
|
+
const script = SCRIPT_OFFSET[resolvedRun.verticalAlign] ?? 0;
|
|
1177
2147
|
return {
|
|
1178
2148
|
run,
|
|
1179
2149
|
resolvedRun,
|
|
1180
2150
|
font: lookupFont(fontResources, fontKey),
|
|
1181
|
-
fontSizePt: resolvedRun.fontSizePt,
|
|
2151
|
+
fontSizePt: script === 0 ? resolvedRun.fontSizePt : resolvedRun.fontSizePt * SCRIPT_SCALE,
|
|
2152
|
+
...script === 0 ? {} : { risePt: resolvedRun.fontSizePt * script },
|
|
1182
2153
|
isImage: false,
|
|
1183
2154
|
imageWidthPt: 0,
|
|
1184
2155
|
imageHeightPt: 0,
|
|
@@ -1194,6 +2165,19 @@ function tokenizeParagraph(paragraph, options, fontResources, imageResources, co
|
|
|
1194
2165
|
...plan.resolvedRun.rtl ? { rtl: true } : {}
|
|
1195
2166
|
})), baseDir));
|
|
1196
2167
|
}
|
|
2168
|
+
/**
|
|
2169
|
+
* Split a paragraph's tokens at every §17.3.3.1 column break, dropping the
|
|
2170
|
+
* break token itself (its line ends there anyway). One part ⇒ no break.
|
|
2171
|
+
*
|
|
2172
|
+
* @param tokens The paragraph's tokens in order.
|
|
2173
|
+
* @returns The parts, in order; always at least one.
|
|
2174
|
+
*/
|
|
2175
|
+
function splitAtColumnBreaks(tokens) {
|
|
2176
|
+
const parts = [[]];
|
|
2177
|
+
for (const t of tokens) if (t.kind === "text" && t.columnBreak) parts.push([]);
|
|
2178
|
+
else parts[parts.length - 1].push(t);
|
|
2179
|
+
return parts.filter((p, i) => i === 0 || p.length > 0);
|
|
2180
|
+
}
|
|
1197
2181
|
function tokenizePlansLtr(plans) {
|
|
1198
2182
|
const tokens = [];
|
|
1199
2183
|
for (const plan of plans) {
|
|
@@ -1203,6 +2187,13 @@ function tokenizePlansLtr(plans) {
|
|
|
1203
2187
|
imageResourceName: plan.imageResourceName,
|
|
1204
2188
|
widthPt: plan.imageWidthPt,
|
|
1205
2189
|
heightPt: plan.imageHeightPt,
|
|
2190
|
+
...plan.imageOutline ? { outline: plan.imageOutline } : {},
|
|
2191
|
+
...plan.imageShadow ? { shadow: plan.imageShadow } : {},
|
|
2192
|
+
...plan.imageCrop ? { crop: plan.imageCrop } : {},
|
|
2193
|
+
...plan.imageRotationDeg ? { rotationDeg: plan.imageRotationDeg } : {},
|
|
2194
|
+
...plan.imageFlipH ? { flipH: true } : {},
|
|
2195
|
+
...plan.imageFlipV ? { flipV: true } : {},
|
|
2196
|
+
...plan.imageDrawBox ? { drawBox: plan.imageDrawBox } : {},
|
|
1206
2197
|
isSpace: false,
|
|
1207
2198
|
bidiLevel: 0
|
|
1208
2199
|
});
|
|
@@ -1225,20 +2216,37 @@ function tokenizePlansLtr(plans) {
|
|
|
1225
2216
|
kind: "text",
|
|
1226
2217
|
text: t.text,
|
|
1227
2218
|
isSpace: t.isSpace,
|
|
2219
|
+
...t.tab ? { tab: true } : {},
|
|
1228
2220
|
...plan.run.href !== void 0 ? { href: plan.run.href } : {},
|
|
1229
2221
|
...plan.run.footnoteRef !== void 0 ? { footnoteRef: plan.run.footnoteRef } : {},
|
|
1230
2222
|
...plan.run.anchor !== void 0 ? { anchor: plan.run.anchor } : {},
|
|
1231
2223
|
...plan.run.listMarker ? { listMarker: true } : {},
|
|
2224
|
+
...plan.run.columnBreak ? { columnBreak: true } : {},
|
|
1232
2225
|
...highlight ? { highlight: true } : {},
|
|
1233
2226
|
resolvedRun: plan.resolvedRun,
|
|
1234
2227
|
font: plan.font,
|
|
1235
2228
|
fontSizePt: plan.fontSizePt,
|
|
1236
|
-
|
|
2229
|
+
...plan.risePt !== void 0 ? { risePt: plan.risePt } : {},
|
|
2230
|
+
widthPt: measureRunText(plan, t.text),
|
|
1237
2231
|
bidiLevel: 0
|
|
1238
2232
|
});
|
|
1239
2233
|
}
|
|
1240
2234
|
return tokens;
|
|
1241
2235
|
}
|
|
2236
|
+
/**
|
|
2237
|
+
* §17.3.2.35 — a run's text measured with the space it puts between its
|
|
2238
|
+
* characters. PDF adds the same amount per glyph through `Tc`, so the width the
|
|
2239
|
+
* line is broken at and the width the page draws agree.
|
|
2240
|
+
*
|
|
2241
|
+
* @param plan The run being tokenized.
|
|
2242
|
+
* @param text The token's text.
|
|
2243
|
+
* @returns The advance width in points.
|
|
2244
|
+
*/
|
|
2245
|
+
function measureRunText(plan, text) {
|
|
2246
|
+
const base = plan.font.measure.textWidthPt(text, plan.fontSizePt);
|
|
2247
|
+
const extra = plan.resolvedRun.letterSpacingPt;
|
|
2248
|
+
return extra === void 0 || extra === 0 ? base : base + extra * [...text].length;
|
|
2249
|
+
}
|
|
1242
2250
|
function tokenizePlansBidi(plans, realLevels) {
|
|
1243
2251
|
const tokens = [];
|
|
1244
2252
|
let realIdx = 0;
|
|
@@ -1250,6 +2258,13 @@ function tokenizePlansBidi(plans, realLevels) {
|
|
|
1250
2258
|
imageResourceName: plan.imageResourceName,
|
|
1251
2259
|
widthPt: plan.imageWidthPt,
|
|
1252
2260
|
heightPt: plan.imageHeightPt,
|
|
2261
|
+
...plan.imageOutline ? { outline: plan.imageOutline } : {},
|
|
2262
|
+
...plan.imageShadow ? { shadow: plan.imageShadow } : {},
|
|
2263
|
+
...plan.imageCrop ? { crop: plan.imageCrop } : {},
|
|
2264
|
+
...plan.imageRotationDeg ? { rotationDeg: plan.imageRotationDeg } : {},
|
|
2265
|
+
...plan.imageFlipH ? { flipH: true } : {},
|
|
2266
|
+
...plan.imageFlipV ? { flipV: true } : {},
|
|
2267
|
+
...plan.imageDrawBox ? { drawBox: plan.imageDrawBox } : {},
|
|
1253
2268
|
isSpace: false,
|
|
1254
2269
|
bidiLevel: realLevels[realIdx] ?? 0
|
|
1255
2270
|
});
|
|
@@ -1284,11 +2299,13 @@ function tokenizePlansBidi(plans, realLevels) {
|
|
|
1284
2299
|
...plan.run.footnoteRef !== void 0 ? { footnoteRef: plan.run.footnoteRef } : {},
|
|
1285
2300
|
...plan.run.anchor !== void 0 ? { anchor: plan.run.anchor } : {},
|
|
1286
2301
|
...plan.run.listMarker ? { listMarker: true } : {},
|
|
2302
|
+
...plan.run.columnBreak ? { columnBreak: true } : {},
|
|
1287
2303
|
...(plan.run.commentRangeRefs?.length ?? 0) > 0 ? { highlight: true } : {},
|
|
1288
2304
|
resolvedRun: plan.resolvedRun,
|
|
1289
2305
|
font: plan.font,
|
|
1290
2306
|
fontSizePt: plan.fontSizePt,
|
|
1291
|
-
|
|
2307
|
+
...plan.risePt !== void 0 ? { risePt: plan.risePt } : {},
|
|
2308
|
+
widthPt: measureRunText(plan, text),
|
|
1292
2309
|
bidiLevel: curLevel
|
|
1293
2310
|
});
|
|
1294
2311
|
};
|
|
@@ -1315,6 +2332,7 @@ function paragraphBaseDirection(paragraph, resolved) {
|
|
|
1315
2332
|
if (resolved.bidi) return "rtl";
|
|
1316
2333
|
let text = "";
|
|
1317
2334
|
for (const run of paragraph.runs) {
|
|
2335
|
+
if (run.listMarker === true) continue;
|
|
1318
2336
|
if (!run.inlineImage) text += run.text;
|
|
1319
2337
|
if (text.length > 64) break;
|
|
1320
2338
|
}
|
|
@@ -1327,16 +2345,29 @@ function paragraphMaxWidth(p, contentWidth, firstLine) {
|
|
|
1327
2345
|
const firstLineExtra = firstLine ? p.indentFirstLine : 0;
|
|
1328
2346
|
return Math.max(1, contentWidth - indentLeft - indentRight - firstLineExtra);
|
|
1329
2347
|
}
|
|
2348
|
+
var HYPHENS = new Set([
|
|
2349
|
+
45,
|
|
2350
|
+
8208,
|
|
2351
|
+
8211,
|
|
2352
|
+
8212
|
|
2353
|
+
]);
|
|
2354
|
+
/** §17.3.3.29 — the optional hyphen, printed only where a line breaks on it. */
|
|
2355
|
+
var SOFT_HYPHEN = "";
|
|
1330
2356
|
function tokenizeText(text) {
|
|
1331
2357
|
const out = [];
|
|
1332
2358
|
if (text.length === 0) return out;
|
|
1333
|
-
const re = /(\s+)|(\S+)/g;
|
|
2359
|
+
const re = /(\t)|(\s+)|(\S+)/g;
|
|
1334
2360
|
let m;
|
|
1335
2361
|
while ((m = re.exec(text)) !== null) if (m[1] !== void 0) out.push({
|
|
1336
|
-
text:
|
|
2362
|
+
text: "",
|
|
2363
|
+
isSpace: true,
|
|
2364
|
+
tab: true
|
|
2365
|
+
});
|
|
2366
|
+
else if (m[2] !== void 0) out.push({
|
|
2367
|
+
text: m[2],
|
|
1337
2368
|
isSpace: true
|
|
1338
2369
|
});
|
|
1339
|
-
else if (m[
|
|
2370
|
+
else if (m[3] !== void 0) for (const piece of splitCjkSegment(m[3])) out.push({
|
|
1340
2371
|
text: piece,
|
|
1341
2372
|
isSpace: false
|
|
1342
2373
|
});
|
|
@@ -1347,17 +2378,54 @@ function lastCodePoint(s) {
|
|
|
1347
2378
|
const chars = [...s];
|
|
1348
2379
|
return chars.length > 0 ? chars[chars.length - 1].codePointAt(0) : 0;
|
|
1349
2380
|
}
|
|
1350
|
-
function paragraphItemStream(tokens, hyphenator) {
|
|
2381
|
+
function paragraphItemStream(tokens, hyphenator, maxLineWidthPt) {
|
|
1351
2382
|
const entries = [];
|
|
1352
2383
|
let prevBoxEndCp = null;
|
|
2384
|
+
let prevWasImage = false;
|
|
1353
2385
|
for (const tok of tokens) {
|
|
2386
|
+
const breaks = tok.isSpace ? tok.text.match(/\r\n|[\r\n]/gu)?.length ?? 0 : 0;
|
|
2387
|
+
if (breaks > 0) {
|
|
2388
|
+
for (let i = 0; i < breaks; i++) {
|
|
2389
|
+
entries.push({
|
|
2390
|
+
item: {
|
|
2391
|
+
type: "glue",
|
|
2392
|
+
width: 0,
|
|
2393
|
+
stretch: 1e6,
|
|
2394
|
+
shrink: 0
|
|
2395
|
+
},
|
|
2396
|
+
token: null
|
|
2397
|
+
});
|
|
2398
|
+
entries.push({
|
|
2399
|
+
item: {
|
|
2400
|
+
type: "penalty",
|
|
2401
|
+
width: 0,
|
|
2402
|
+
penalty: FORCED_BREAK,
|
|
2403
|
+
flagged: false
|
|
2404
|
+
},
|
|
2405
|
+
token: null,
|
|
2406
|
+
softBreak: true
|
|
2407
|
+
});
|
|
2408
|
+
}
|
|
2409
|
+
prevBoxEndCp = null;
|
|
2410
|
+
continue;
|
|
2411
|
+
}
|
|
1354
2412
|
if (tok.isSpace || tok.kind === "image" || tok.kind === "math") {
|
|
2413
|
+
if (tok.kind === "image" && (prevWasImage || prevBoxEndCp !== null)) entries.push({
|
|
2414
|
+
item: {
|
|
2415
|
+
type: "penalty",
|
|
2416
|
+
width: 0,
|
|
2417
|
+
penalty: 0,
|
|
2418
|
+
flagged: false
|
|
2419
|
+
},
|
|
2420
|
+
token: null
|
|
2421
|
+
});
|
|
2422
|
+
prevWasImage = tok.kind === "image";
|
|
1355
2423
|
entries.push({
|
|
1356
2424
|
item: tok.isSpace ? {
|
|
1357
2425
|
type: "glue",
|
|
1358
2426
|
width: tok.widthPt,
|
|
1359
2427
|
stretch: tok.widthPt * .6,
|
|
1360
|
-
shrink: tok.widthPt *
|
|
2428
|
+
shrink: tok.widthPt * GLUE_SHRINK_RATIO
|
|
1361
2429
|
} : {
|
|
1362
2430
|
type: "box",
|
|
1363
2431
|
width: tok.widthPt
|
|
@@ -1368,7 +2436,7 @@ function paragraphItemStream(tokens, hyphenator) {
|
|
|
1368
2436
|
continue;
|
|
1369
2437
|
}
|
|
1370
2438
|
const startCp = tok.text.codePointAt(0) ?? 0;
|
|
1371
|
-
if (prevBoxEndCp !== null && cjkBreakBetween(prevBoxEndCp, startCp)) entries.push({
|
|
2439
|
+
if (prevWasImage || prevBoxEndCp !== null && cjkBreakBetween(prevBoxEndCp, startCp)) entries.push({
|
|
1372
2440
|
item: {
|
|
1373
2441
|
type: "penalty",
|
|
1374
2442
|
width: 0,
|
|
@@ -1377,9 +2445,33 @@ function paragraphItemStream(tokens, hyphenator) {
|
|
|
1377
2445
|
},
|
|
1378
2446
|
token: null
|
|
1379
2447
|
});
|
|
2448
|
+
prevWasImage = false;
|
|
1380
2449
|
prevBoxEndCp = lastCodePoint(tok.text);
|
|
1381
2450
|
const positions = hyphenator ? hyphenator.hyphenate(tok.text) : [];
|
|
1382
2451
|
if (positions.length === 0) {
|
|
2452
|
+
const tooWide = maxLineWidthPt !== void 0 && maxLineWidthPt > 0;
|
|
2453
|
+
const chunks = (tooWide && tok.widthPt > maxLineWidthPt ? splitAtHyphens(tok) : void 0) ?? (tooWide && tok.widthPt > maxLineWidthPt * 1.1 ? splitToWidth(tok, maxLineWidthPt) : void 0);
|
|
2454
|
+
if (chunks) {
|
|
2455
|
+
chunks.forEach((chunk, ci) => {
|
|
2456
|
+
if (ci > 0) entries.push({
|
|
2457
|
+
item: {
|
|
2458
|
+
type: "penalty",
|
|
2459
|
+
width: 0,
|
|
2460
|
+
penalty: 0,
|
|
2461
|
+
flagged: false
|
|
2462
|
+
},
|
|
2463
|
+
token: null
|
|
2464
|
+
});
|
|
2465
|
+
entries.push({
|
|
2466
|
+
item: {
|
|
2467
|
+
type: "box",
|
|
2468
|
+
width: chunk.widthPt
|
|
2469
|
+
},
|
|
2470
|
+
token: chunk
|
|
2471
|
+
});
|
|
2472
|
+
});
|
|
2473
|
+
continue;
|
|
2474
|
+
}
|
|
1383
2475
|
entries.push({
|
|
1384
2476
|
item: {
|
|
1385
2477
|
type: "box",
|
|
@@ -1445,17 +2537,165 @@ function paragraphItemStream(tokens, hyphenator) {
|
|
|
1445
2537
|
});
|
|
1446
2538
|
return entries;
|
|
1447
2539
|
}
|
|
1448
|
-
|
|
1449
|
-
|
|
1450
|
-
|
|
1451
|
-
|
|
1452
|
-
|
|
1453
|
-
|
|
1454
|
-
|
|
1455
|
-
|
|
1456
|
-
|
|
2540
|
+
/** Word's own default when a document names none (§17.15.1.25): half an inch. */
|
|
2541
|
+
var DEFAULT_TAB_STOP_PT = 36;
|
|
2542
|
+
var TAB_LEADER_CHARS = new Map([
|
|
2543
|
+
["dot", "."],
|
|
2544
|
+
["hyphen", "-"],
|
|
2545
|
+
["underscore", "_"],
|
|
2546
|
+
["middleDot", "·"]
|
|
2547
|
+
]);
|
|
2548
|
+
/**
|
|
2549
|
+
* Give each tab on a line the width that carries it to its stop, and the
|
|
2550
|
+
* leader that fills the way there.
|
|
2551
|
+
*
|
|
2552
|
+
* §17.3.1.37 — a tab advances to a POSITION, not by a fixed amount, and where
|
|
2553
|
+
* that position is depends on how far the line has already run. Measured as
|
|
2554
|
+
* ordinary whitespace it collapsed to a space: the page numbers of
|
|
2555
|
+
* FDO77715.docx's index sat against their titles with no dot leader between,
|
|
2556
|
+
* and its header ran its left and right halves together on one line.
|
|
2557
|
+
*
|
|
2558
|
+
* A `right`, `center` or `decimal` stop positions the text AFTER the tab, so
|
|
2559
|
+
* each is resolved against the segment that follows it.
|
|
2560
|
+
*
|
|
2561
|
+
* @param tokens The line's tokens, replaced in place.
|
|
2562
|
+
* @param resolved The paragraph's resolved properties: the stops and indents.
|
|
2563
|
+
* @param isFirst Whether this is the paragraph's first line.
|
|
2564
|
+
*/
|
|
2565
|
+
function resolveTabs(tokens, resolved, isFirst, availableWidthPt) {
|
|
2566
|
+
const stops = resolved.tabs;
|
|
2567
|
+
let x = resolved.indentLeft + (isFirst ? resolved.indentFirstLine : 0);
|
|
2568
|
+
for (let i = 0; i < tokens.length; i++) {
|
|
2569
|
+
const tok = tokens[i];
|
|
2570
|
+
if (!tok) continue;
|
|
2571
|
+
if (!(tok.kind === "text" && tok.tab === true)) {
|
|
2572
|
+
x += tok.widthPt;
|
|
2573
|
+
continue;
|
|
2574
|
+
}
|
|
2575
|
+
let segment = 0;
|
|
2576
|
+
let decimalAt = 0;
|
|
2577
|
+
let seenDecimal = false;
|
|
2578
|
+
for (let j = i + 1; j < tokens.length; j++) {
|
|
2579
|
+
const next = tokens[j];
|
|
2580
|
+
if (next.kind === "text" && next.tab === true) break;
|
|
2581
|
+
if (!seenDecimal && next.kind === "text") {
|
|
2582
|
+
const dot = next.text.indexOf(".");
|
|
2583
|
+
if (dot >= 0) {
|
|
2584
|
+
decimalAt = segment + next.font.measure.textWidthPt(next.text.slice(0, dot), next.fontSizePt);
|
|
2585
|
+
seenDecimal = true;
|
|
2586
|
+
}
|
|
2587
|
+
}
|
|
2588
|
+
segment += next.widthPt;
|
|
2589
|
+
}
|
|
2590
|
+
const stopAt = (sp) => sp.relativeTo === "center" ? availableWidthPt / 2 : sp.relativeTo === "right" ? availableWidthPt : sp.positionPt;
|
|
2591
|
+
const stop = stops.find((sp) => stopAt(sp) > x + .01);
|
|
2592
|
+
const hangingStop = isFirst && resolved.indentFirstLine < 0 && resolved.indentLeft > x + .01 ? resolved.indentLeft : void 0;
|
|
2593
|
+
const explicitAt = stop ? stopAt(stop) : void 0;
|
|
2594
|
+
const position = explicitAt !== void 0 && (hangingStop === void 0 || explicitAt <= hangingStop) ? explicitAt : hangingStop ?? (Math.floor(x / DEFAULT_TAB_STOP_PT) + 1) * DEFAULT_TAB_STOP_PT;
|
|
2595
|
+
const alignment = stop?.alignment ?? "left";
|
|
2596
|
+
if (segment === 0 && alignment !== "left") {
|
|
2597
|
+
const after = tokens[i + 1];
|
|
2598
|
+
if (after?.kind === "text" && after.tab === true) {
|
|
2599
|
+
tokens[i] = {
|
|
2600
|
+
...tok,
|
|
2601
|
+
widthPt: 0,
|
|
2602
|
+
text: ""
|
|
2603
|
+
};
|
|
2604
|
+
continue;
|
|
2605
|
+
}
|
|
2606
|
+
}
|
|
2607
|
+
let target = position;
|
|
2608
|
+
if (alignment === "right") target = position - segment;
|
|
2609
|
+
else if (alignment === "center") target = position - segment / 2;
|
|
2610
|
+
else if (alignment === "decimal") target = position - (seenDecimal ? decimalAt : segment);
|
|
2611
|
+
const width = Math.max(0, target - x);
|
|
2612
|
+
const leaderChar = stop?.leader === void 0 ? void 0 : TAB_LEADER_CHARS.get(stop.leader);
|
|
2613
|
+
const text = leaderChar === void 0 ? "" : fillLeader(tok, leaderChar, width);
|
|
2614
|
+
tokens[i] = {
|
|
2615
|
+
...tok,
|
|
2616
|
+
widthPt: width,
|
|
2617
|
+
text
|
|
2618
|
+
};
|
|
2619
|
+
x += width;
|
|
2620
|
+
}
|
|
2621
|
+
}
|
|
2622
|
+
/** As many leader characters as fit the gap, measured in the tab's own font. */
|
|
2623
|
+
function fillLeader(tab, leader, widthPt) {
|
|
2624
|
+
const unit = tab.font.measure.textWidthPt(leader, tab.fontSizePt);
|
|
2625
|
+
if (!(unit > 0)) return "";
|
|
2626
|
+
return leader.repeat(Math.max(0, Math.floor(widthPt / unit)));
|
|
2627
|
+
}
|
|
2628
|
+
/**
|
|
2629
|
+
* Cut an unbreakable word into the widest pieces that fit `widthPt`, measured
|
|
2630
|
+
* with the token's own font. Each piece keeps the run's formatting, so the line
|
|
2631
|
+
* builder reassembles them exactly as it does hyphenation fragments.
|
|
2632
|
+
*
|
|
2633
|
+
* @param tok The text token.
|
|
2634
|
+
* @param widthPt The line width to fit each piece into.
|
|
2635
|
+
* @returns The pieces in order, or undefined when even one character overruns
|
|
2636
|
+
* (nothing can be gained by splitting then).
|
|
2637
|
+
*/
|
|
2638
|
+
function splitAtHyphens(tok) {
|
|
2639
|
+
if (tok.kind !== "text") return void 0;
|
|
2640
|
+
const parts = [];
|
|
2641
|
+
let start = 0;
|
|
2642
|
+
for (let i = 0; i < tok.text.length; i++) {
|
|
2643
|
+
if (!HYPHENS.has(tok.text.codePointAt(i) ?? 0)) continue;
|
|
2644
|
+
if (i === start || i === tok.text.length - 1) continue;
|
|
2645
|
+
parts.push(tok.text.slice(start, i + 1));
|
|
2646
|
+
start = i + 1;
|
|
2647
|
+
}
|
|
2648
|
+
if (parts.length === 0) return void 0;
|
|
2649
|
+
parts.push(tok.text.slice(start));
|
|
2650
|
+
return parts.map((text) => ({
|
|
2651
|
+
...tok,
|
|
2652
|
+
text,
|
|
2653
|
+
widthPt: tok.font.measure.textWidthPt(text, tok.fontSizePt)
|
|
2654
|
+
}));
|
|
2655
|
+
}
|
|
2656
|
+
function splitToWidth(tok, widthPt) {
|
|
2657
|
+
if (tok.kind !== "text") return void 0;
|
|
2658
|
+
const chars = [...tok.text];
|
|
2659
|
+
if (tok.font.measure.textWidthPt(chars.slice(0, MIN_SPLIT_CHARS).join(""), tok.fontSizePt) > widthPt) return;
|
|
2660
|
+
const out = [];
|
|
2661
|
+
let start = 0;
|
|
2662
|
+
while (start < chars.length) {
|
|
2663
|
+
let end = start + 1;
|
|
2664
|
+
let text = chars[start];
|
|
2665
|
+
let width = tok.font.measure.textWidthPt(text, tok.fontSizePt);
|
|
2666
|
+
if (width > widthPt && start === 0 && chars.length === 1) return void 0;
|
|
2667
|
+
while (end < chars.length) {
|
|
2668
|
+
const next = text + chars[end];
|
|
2669
|
+
const nextWidth = tok.font.measure.textWidthPt(next, tok.fontSizePt);
|
|
2670
|
+
if (nextWidth > widthPt) break;
|
|
2671
|
+
text = next;
|
|
2672
|
+
width = nextWidth;
|
|
2673
|
+
end++;
|
|
2674
|
+
}
|
|
2675
|
+
out.push({
|
|
2676
|
+
...tok,
|
|
2677
|
+
text,
|
|
2678
|
+
widthPt: width
|
|
2679
|
+
});
|
|
2680
|
+
start = end;
|
|
2681
|
+
}
|
|
2682
|
+
return out.length > 1 ? out : void 0;
|
|
2683
|
+
}
|
|
2684
|
+
/** The fewest characters a mid-word break may leave on a line. */
|
|
2685
|
+
var MIN_SPLIT_CHARS = 6;
|
|
2686
|
+
function lineFromRange(entries, start, breakIdx, availableWidthPt, isFirst, resolved, profile, allowEmpty = false) {
|
|
2687
|
+
let st = start;
|
|
2688
|
+
let et = breakIdx;
|
|
2689
|
+
while (st < et && (entries[st].token === null || !isFirst && entries[st].token?.isSpace === true)) st++;
|
|
2690
|
+
const paints = (t) => t !== null && t !== void 0 && t.kind === "text" && (t.resolvedRun.underline !== "none" || t.resolvedRun.strike);
|
|
2691
|
+
while (et > st && (entries[et - 1].token === null || entries[et - 1].token?.isSpace === true && !paints(entries[et - 1].token))) et--;
|
|
2692
|
+
if (st >= et && !allowEmpty) return null;
|
|
2693
|
+
const lineTokens = [];
|
|
2694
|
+
for (let i = st; i < et; i++) {
|
|
2695
|
+
const ft = entries[i].token;
|
|
1457
2696
|
if (ft) lineTokens.push(ft);
|
|
1458
2697
|
}
|
|
2698
|
+
resolveTabs(lineTokens, resolved, isFirst, availableWidthPt);
|
|
1459
2699
|
const hyphenWidth = entries[breakIdx]?.hyphenWidthPt;
|
|
1460
2700
|
if (hyphenWidth !== void 0) {
|
|
1461
2701
|
const lastIdx = lineTokens.length - 1;
|
|
@@ -1475,8 +2715,10 @@ function lineFromRange(entries, start, breakIdx, availableWidthPt, isFirst, reso
|
|
|
1475
2715
|
let metricAscent = 0;
|
|
1476
2716
|
let metricDescent = 0;
|
|
1477
2717
|
let metricLineGap = 0;
|
|
2718
|
+
const prints = lineTokens.some((t) => !t.isSpace);
|
|
1478
2719
|
for (const t of lineTokens) {
|
|
1479
2720
|
contentWidth += t.widthPt;
|
|
2721
|
+
if (prints && t.isSpace) continue;
|
|
1480
2722
|
const sz = tokenLineSize(t);
|
|
1481
2723
|
if (sz > maxSize) maxSize = sz;
|
|
1482
2724
|
if (t.kind === "math") {
|
|
@@ -1506,23 +2748,51 @@ function lineFromRange(entries, start, breakIdx, availableWidthPt, isFirst, reso
|
|
|
1506
2748
|
} : {}
|
|
1507
2749
|
};
|
|
1508
2750
|
}
|
|
1509
|
-
function
|
|
2751
|
+
function withLeadingTabWidths(tokens, resolved, firstLineWidth) {
|
|
2752
|
+
const first = tokens[0];
|
|
2753
|
+
if (!(first?.kind === "text" && first.tab === true)) return tokens;
|
|
2754
|
+
const probe = tokens.map((t) => ({ ...t }));
|
|
2755
|
+
resolveTabs(probe, resolved, true, firstLineWidth);
|
|
2756
|
+
const out = tokens.slice();
|
|
2757
|
+
for (let i = 0; i < out.length; i++) {
|
|
2758
|
+
const tok = out[i];
|
|
2759
|
+
if (!(tok.kind === "text" && tok.tab === true)) break;
|
|
2760
|
+
out[i] = {
|
|
2761
|
+
...tok,
|
|
2762
|
+
widthPt: probe[i].widthPt
|
|
2763
|
+
};
|
|
2764
|
+
}
|
|
2765
|
+
return out;
|
|
2766
|
+
}
|
|
2767
|
+
function wrap(tokens, firstLineWidth, otherWidth, resolved, hyphenator, profile, noExpandShiftReturn, lineWidths) {
|
|
1510
2768
|
if (tokens.length === 0) return [];
|
|
1511
2769
|
const widths = lineWidths ?? [firstLineWidth, otherWidth];
|
|
1512
|
-
const entries = paragraphItemStream(tokens, hyphenator);
|
|
2770
|
+
const entries = paragraphItemStream(withLeadingTabWidths(tokens, resolved, widths[0] ?? firstLineWidth), hyphenator, Math.max(0, ...widths));
|
|
1513
2771
|
const items = entries.map((e) => e.item);
|
|
1514
2772
|
const breaks = profile === "ream" ? breakLines(items, widths).breaks : greedyBreakLines(items, widths);
|
|
1515
|
-
const
|
|
1516
|
-
|
|
1517
|
-
|
|
1518
|
-
|
|
1519
|
-
|
|
1520
|
-
const
|
|
1521
|
-
|
|
1522
|
-
|
|
1523
|
-
|
|
1524
|
-
|
|
1525
|
-
|
|
2773
|
+
const buildLines = (at) => {
|
|
2774
|
+
const out = [];
|
|
2775
|
+
let start = 0;
|
|
2776
|
+
let isFirst = true;
|
|
2777
|
+
let lineIdx = 0;
|
|
2778
|
+
for (const [i, breakIdx] of at.entries()) {
|
|
2779
|
+
const width = lineIdx < widths.length ? widths[lineIdx] : widths[widths.length - 1];
|
|
2780
|
+
const line = lineFromRange(entries, start, breakIdx, width, isFirst, resolved, profile, i < at.length - 1);
|
|
2781
|
+
if (line) out.push(noExpandShiftReturn && entries[breakIdx]?.softBreak ? {
|
|
2782
|
+
...line,
|
|
2783
|
+
noJustify: true
|
|
2784
|
+
} : line);
|
|
2785
|
+
start = breakIdx + 1;
|
|
2786
|
+
isFirst = false;
|
|
2787
|
+
lineIdx++;
|
|
2788
|
+
}
|
|
2789
|
+
return out;
|
|
2790
|
+
};
|
|
2791
|
+
let lines = buildLines(breaks);
|
|
2792
|
+
const spill = (ls) => Math.max(0, ...ls.map((l) => l.contentWidthPt - l.availableWidthPt));
|
|
2793
|
+
if (profile === "ream" && lines.length === 1 && spill(lines) > .01) {
|
|
2794
|
+
const greedy = buildLines(greedyBreakLines(items, widths));
|
|
2795
|
+
if (greedy.length > 1 && spill(greedy) < spill(lines) - .01) lines = greedy;
|
|
1526
2796
|
}
|
|
1527
2797
|
if (lines.length > 0) lines[lines.length - 1].isLastInParagraph = true;
|
|
1528
2798
|
return lines;
|
|
@@ -1545,11 +2815,17 @@ function fontLeadingPt(parsed, sizePt, profile) {
|
|
|
1545
2815
|
};
|
|
1546
2816
|
}
|
|
1547
2817
|
function computeLineHeight(line, p) {
|
|
2818
|
+
const natural = naturalLineHeight(line, p);
|
|
2819
|
+
const pitch = p.gridLinePitchPt;
|
|
2820
|
+
if (pitch !== void 0 && pitch > 0) return Math.ceil(natural / pitch - 1e-6) * pitch;
|
|
2821
|
+
return natural;
|
|
2822
|
+
}
|
|
2823
|
+
function naturalLineHeight(line, p) {
|
|
1548
2824
|
const fontSize = line.maxFontSizePt || 12;
|
|
1549
2825
|
const natural = line.metricHeightPt ?? fontSize * 1.2;
|
|
1550
2826
|
const mathH = (line.mathAscentPt ?? 0) + (line.mathDescentPt ?? 0);
|
|
1551
2827
|
const mathNeed = mathH > 0 ? mathH * 1.05 : 0;
|
|
1552
|
-
if (p.spacingLineRule === "exact" && p.spacingLine
|
|
2828
|
+
if (p.spacingLineRule === "exact" && p.spacingLine >= 0) return Math.max(p.spacingLine, mathNeed);
|
|
1553
2829
|
if (p.spacingLineRule === "atLeast" && p.spacingLine > 0) return Math.max(natural, p.spacingLine, mathNeed);
|
|
1554
2830
|
const lineTwips = Math.round(p.spacingLine * 20);
|
|
1555
2831
|
const multiple = lineTwips > 0 ? lineTwips / 240 : 1;
|
|
@@ -1560,6 +2836,37 @@ function lineDescent(line) {
|
|
|
1560
2836
|
const base = line.metricDescentPt ?? fs * .2;
|
|
1561
2837
|
return Math.max(base, line.mathDescentPt ?? 0);
|
|
1562
2838
|
}
|
|
2839
|
+
/**
|
|
2840
|
+
* §17.3.1.33 — how far the baseline stands above the BOTTOM of the line box,
|
|
2841
|
+
* which is what says where the extra height of a spaced-out line goes.
|
|
2842
|
+
*
|
|
2843
|
+
* A line given a fixed height (`exact`, `atLeast`) hangs from the bottom of its
|
|
2844
|
+
* box: the space a taller `w:line` asks for opens ABOVE the text, which is what
|
|
2845
|
+
* makes "at least 24pt" push a heading down its own line.
|
|
2846
|
+
*
|
|
2847
|
+
* `auto` — Word's "Multiple", the 1.15 every modern template sets — scales the
|
|
2848
|
+
* line instead, and the space it gains falls BELOW: the first line of a spaced
|
|
2849
|
+
* paragraph starts exactly where a single-spaced one does. Read the other way
|
|
2850
|
+
* round, fdo80800.docx's 1.5-line cells sat 6.6pt under the plain cell beside
|
|
2851
|
+
* them, where Word and LibreOffice write all three on one baseline.
|
|
2852
|
+
*
|
|
2853
|
+
* §17.6.5 — the extra a document GRID adds is not the paragraph's at all, and
|
|
2854
|
+
* is split evenly: the line is centred on its grid row.
|
|
2855
|
+
*
|
|
2856
|
+
* @param line The laid-out line.
|
|
2857
|
+
* @param p The owning paragraph's resolved properties.
|
|
2858
|
+
* @returns The distance from the line box's bottom edge up to the baseline.
|
|
2859
|
+
*/
|
|
2860
|
+
function lineBaselineOffset(line, p) {
|
|
2861
|
+
const descent = lineDescent(line);
|
|
2862
|
+
const natural = naturalLineHeight(line, p);
|
|
2863
|
+
const singled = naturalLineHeight(line, {
|
|
2864
|
+
...p,
|
|
2865
|
+
spacingLineRule: "auto",
|
|
2866
|
+
spacingLine: pt(0)
|
|
2867
|
+
});
|
|
2868
|
+
return descent + (p.spacingLineRule === "auto" ? Math.max(0, natural - singled) : 0) + (computeLineHeight(line, p) - natural) / 2;
|
|
2869
|
+
}
|
|
1563
2870
|
function alignmentOffset(alignment, lineWidth, available) {
|
|
1564
2871
|
if (alignment === "right") return Math.max(0, available - lineWidth);
|
|
1565
2872
|
if (alignment === "center") return Math.max(0, (available - lineWidth) / 2);
|
|
@@ -1574,7 +2881,7 @@ function layoutTableBlock(table, options, fontResources, imageResources, content
|
|
|
1574
2881
|
for (let r = 0; r < table.rows.length; r++) {
|
|
1575
2882
|
const rl = layoutTableRow(table.rows[r], r, table.rows.length, colCount, table.properties, columnWidthsPt, options, fontResources, imageResources, mergeRoles[r] ?? [], aboveBordersByCol);
|
|
1576
2883
|
const nextAbove = [];
|
|
1577
|
-
let ci = 0;
|
|
2884
|
+
let ci = table.rows[r].properties.gridBefore ?? 0;
|
|
1578
2885
|
for (const cell of table.rows[r].cells) {
|
|
1579
2886
|
const span = Math.max(1, cell.properties.colSpan ?? 1);
|
|
1580
2887
|
for (let k = 0; k < span; k++) nextAbove[ci + k] = cell.properties.borders;
|
|
@@ -1588,17 +2895,46 @@ function layoutTableBlock(table, options, fontResources, imageResources, content
|
|
|
1588
2895
|
...rp.pageBreakBefore ? { breakBefore: true } : {}
|
|
1589
2896
|
} : rl);
|
|
1590
2897
|
}
|
|
2898
|
+
growRowsForMerges(rows);
|
|
1591
2899
|
const heightPt = rows.reduce((s, r) => s + r.heightPt, 0);
|
|
1592
2900
|
const totalWidthPt = columnWidthsPt.reduce((s, w) => s + w, 0);
|
|
2901
|
+
const xOffsetPt = (table.properties.indentPt ?? 0) + tableXOffset(table.properties.alignment, contentWidth, totalWidthPt);
|
|
1593
2902
|
return {
|
|
1594
2903
|
kind: "table",
|
|
2904
|
+
...table.properties.float ? { float: table.properties.float } : {},
|
|
1595
2905
|
rows,
|
|
1596
2906
|
heightPt,
|
|
1597
2907
|
totalWidthPt,
|
|
1598
2908
|
colCount,
|
|
1599
|
-
xOffsetPt
|
|
2909
|
+
xOffsetPt
|
|
1600
2910
|
};
|
|
1601
2911
|
}
|
|
2912
|
+
/**
|
|
2913
|
+
* §17.4.85 `w:vMerge` — make every vertical merge fit. Each row is already as
|
|
2914
|
+
* tall as its own (unmerged) cells; a merge that needs more than its rows add
|
|
2915
|
+
* up to grows the LAST row of the run, which is where Word puts the slack.
|
|
2916
|
+
*
|
|
2917
|
+
* @param rows The table's laid-out rows, adjusted in place.
|
|
2918
|
+
*/
|
|
2919
|
+
function growRowsForMerges(rows) {
|
|
2920
|
+
for (let r = 0; r < rows.length; r++) for (const cell of rows[r].cells) {
|
|
2921
|
+
if (cell.mergeRole !== "start") continue;
|
|
2922
|
+
let last = r;
|
|
2923
|
+
for (let k = r + 1; k < rows.length; k++) {
|
|
2924
|
+
const below = rows[k].cells.find((c) => c.colStart === cell.colStart);
|
|
2925
|
+
if (!below || below.mergeRole !== "middle" && below.mergeRole !== "end") break;
|
|
2926
|
+
last = k;
|
|
2927
|
+
if (below.mergeRole === "end") break;
|
|
2928
|
+
}
|
|
2929
|
+
let have = 0;
|
|
2930
|
+
for (let k = r; k <= last; k++) have += rows[k].heightPt;
|
|
2931
|
+
const deficit = cell.totalHeightPt - have;
|
|
2932
|
+
if (deficit > .01) rows[last] = {
|
|
2933
|
+
...rows[last],
|
|
2934
|
+
heightPt: rows[last].heightPt + deficit
|
|
2935
|
+
};
|
|
2936
|
+
}
|
|
2937
|
+
}
|
|
1602
2938
|
function tableXOffset(alignment, contentWidth, totalWidthPt) {
|
|
1603
2939
|
const slack = contentWidth - totalWidthPt;
|
|
1604
2940
|
if (slack <= 0 || !alignment || alignment === "left") return 0;
|
|
@@ -1607,15 +2943,18 @@ function tableXOffset(alignment, contentWidth, totalWidthPt) {
|
|
|
1607
2943
|
function computeColumnWidths(table, options, fontResources, contentWidth) {
|
|
1608
2944
|
let colCount = table.grid.length;
|
|
1609
2945
|
for (const row of table.rows) {
|
|
1610
|
-
let rowCols = 0;
|
|
2946
|
+
let rowCols = row.properties.gridBefore ?? 0;
|
|
1611
2947
|
for (const cell of row.cells) rowCols += Math.max(1, cell.properties.colSpan ?? 1);
|
|
1612
2948
|
if (rowCols > colCount) colCount = rowCols;
|
|
1613
2949
|
}
|
|
1614
2950
|
if (colCount === 0) return [];
|
|
1615
|
-
|
|
2951
|
+
const gridShort = gridColTwips(table).length < colCount;
|
|
2952
|
+
const declared = declaredColumnWidths(table, colCount, contentWidth, gridShort);
|
|
2953
|
+
if (declared) return declared;
|
|
2954
|
+
if (gridColTwips(table).some((w) => w > 0)) return gridWidthsScaled(table, contentWidth, colCount);
|
|
1616
2955
|
const colNaturalWidths = new Array(colCount).fill(0);
|
|
1617
2956
|
for (const row of table.rows) {
|
|
1618
|
-
let colIdx = 0;
|
|
2957
|
+
let colIdx = row.properties.gridBefore ?? 0;
|
|
1619
2958
|
for (let i = 0; i < row.cells.length; i++) {
|
|
1620
2959
|
const cell = row.cells[i];
|
|
1621
2960
|
const span = Math.max(1, cell.properties.colSpan ?? 1);
|
|
@@ -1648,6 +2987,81 @@ function computeColumnWidths(table, options, fontResources, contentWidth) {
|
|
|
1648
2987
|
function gridColTwips(table) {
|
|
1649
2988
|
return table.grid.map((w) => Math.round(w * 20));
|
|
1650
2989
|
}
|
|
2990
|
+
/**
|
|
2991
|
+
* §17.4.53 — column widths taken from the cells' own `w:tcW` for a table laid
|
|
2992
|
+
* out `fixed`. Each cell shares its preferred width over the columns it spans
|
|
2993
|
+
* and the widest claim wins; a column nobody declares keeps its grid share.
|
|
2994
|
+
* Returns `undefined` (⇒ the grid decides) unless the table says `fixed` and
|
|
2995
|
+
* enough of it is declared to trust.
|
|
2996
|
+
*/
|
|
2997
|
+
function declaredColumnWidths(table, colCount, contentWidth, gridShort = false) {
|
|
2998
|
+
if (table.properties.layout !== "fixed" && !gridShort) return void 0;
|
|
2999
|
+
const target = totalTableTarget(table, contentWidth);
|
|
3000
|
+
const declaredWidth = (cell) => {
|
|
3001
|
+
const p = cell.properties;
|
|
3002
|
+
if (p.widthFraction !== void 0) return p.widthFraction * target;
|
|
3003
|
+
if (p.width !== void 0 && p.widthType !== "auto" && p.widthType !== "nil") return p.width;
|
|
3004
|
+
};
|
|
3005
|
+
const claimed = new Array(colCount).fill(0);
|
|
3006
|
+
const spanning = [];
|
|
3007
|
+
let anyDeclared = false;
|
|
3008
|
+
let reach = 0;
|
|
3009
|
+
for (const row of table.rows) {
|
|
3010
|
+
if (row.cells.length < 2) continue;
|
|
3011
|
+
let span = row.properties.gridBefore ?? 0;
|
|
3012
|
+
for (const cell of row.cells) span += Math.max(1, cell.properties.colSpan ?? 1);
|
|
3013
|
+
if (span > reach) reach = span;
|
|
3014
|
+
}
|
|
3015
|
+
const phantomFrom = reach > 0 && reach < colCount ? reach : colCount;
|
|
3016
|
+
for (const row of table.rows) {
|
|
3017
|
+
let colIdx = row.properties.gridBefore ?? 0;
|
|
3018
|
+
for (const cell of row.cells) {
|
|
3019
|
+
const span = Math.max(1, cell.properties.colSpan ?? 1);
|
|
3020
|
+
const widthPt = declaredWidth(cell);
|
|
3021
|
+
if (widthPt !== void 0 && widthPt > 0) {
|
|
3022
|
+
anyDeclared = true;
|
|
3023
|
+
if (span === 1) {
|
|
3024
|
+
if (widthPt > claimed[colIdx]) claimed[colIdx] = widthPt;
|
|
3025
|
+
} else spanning.push({
|
|
3026
|
+
start: colIdx,
|
|
3027
|
+
span,
|
|
3028
|
+
widthPt
|
|
3029
|
+
});
|
|
3030
|
+
}
|
|
3031
|
+
colIdx += span;
|
|
3032
|
+
}
|
|
3033
|
+
}
|
|
3034
|
+
if (!anyDeclared) return void 0;
|
|
3035
|
+
for (const s of spanning) {
|
|
3036
|
+
const end = Math.min(s.start + s.span, phantomFrom);
|
|
3037
|
+
let have = 0;
|
|
3038
|
+
const blank = [];
|
|
3039
|
+
for (let i = s.start; i < end; i++) {
|
|
3040
|
+
have += claimed[i];
|
|
3041
|
+
if (claimed[i] === 0) blank.push(i);
|
|
3042
|
+
}
|
|
3043
|
+
const deficit = s.widthPt - have;
|
|
3044
|
+
if (deficit <= 0) continue;
|
|
3045
|
+
if (blank.length > 0) {
|
|
3046
|
+
const each = deficit / blank.length;
|
|
3047
|
+
for (const i of blank) claimed[i] = each;
|
|
3048
|
+
} else if (have > 0) for (let i = s.start; i < end; i++) claimed[i] = claimed[i] * (s.widthPt / have);
|
|
3049
|
+
}
|
|
3050
|
+
const gridTwips = gridColTwips(table);
|
|
3051
|
+
const rest = [];
|
|
3052
|
+
let claimedTotal = 0;
|
|
3053
|
+
for (let i = 0; i < phantomFrom; i++) if (claimed[i] > 0) claimedTotal += claimed[i];
|
|
3054
|
+
else rest.push(i);
|
|
3055
|
+
if (rest.length > 0) {
|
|
3056
|
+
const leftover = Math.max(0, target - claimedTotal);
|
|
3057
|
+
const restGrid = rest.reduce((s, i) => s + (gridTwips[i] ?? 0), 0);
|
|
3058
|
+
for (const i of rest) claimed[i] = restGrid > 0 ? (gridTwips[i] ?? 0) / restGrid * leftover : leftover / rest.length;
|
|
3059
|
+
}
|
|
3060
|
+
const total = claimed.reduce((s, w) => s + w, 0);
|
|
3061
|
+
if (total <= 0) return void 0;
|
|
3062
|
+
const scale = total > target ? target / total : 1;
|
|
3063
|
+
return claimed.map((w) => w * scale);
|
|
3064
|
+
}
|
|
1651
3065
|
function gridWidthsScaled(table, contentWidth, colCount) {
|
|
1652
3066
|
const gridTwips = gridColTwips(table);
|
|
1653
3067
|
const totalGridTwips = gridTwips.reduce((s, w) => s + w, 0);
|
|
@@ -1683,28 +3097,70 @@ function measureSingleLine(paragraph, options, fontResources) {
|
|
|
1683
3097
|
}
|
|
1684
3098
|
return total;
|
|
1685
3099
|
}
|
|
3100
|
+
/**
|
|
3101
|
+
* The above-neighbour a cell resolves its top edge against.
|
|
3102
|
+
*
|
|
3103
|
+
* A spanning cell covers several columns and paints ONE top across all of them,
|
|
3104
|
+
* so taking the first column's neighbour throws away any rule the others
|
|
3105
|
+
* declare. tdf100034.xlsx rules the bottom of its two header cells, and a label
|
|
3106
|
+
* overflowing from two columns to their left spanned over both and wiped that
|
|
3107
|
+
* rule off the page. Take the heaviest bottom under the span instead — it is
|
|
3108
|
+
* the one edge the span can draw, so it should be the strongest claim on it.
|
|
3109
|
+
*/
|
|
3110
|
+
function aboveOfSpan(aboveBordersByCol, colIdx, span) {
|
|
3111
|
+
const first = aboveBordersByCol[colIdx];
|
|
3112
|
+
if (span <= 1) return first;
|
|
3113
|
+
let bottom = first?.bottom;
|
|
3114
|
+
for (let k = 1; k < span; k++) {
|
|
3115
|
+
const b = aboveBordersByCol[colIdx + k]?.bottom;
|
|
3116
|
+
if (heavierBorder(bottom, b) === b) bottom = b;
|
|
3117
|
+
}
|
|
3118
|
+
if (bottom === first?.bottom) return first;
|
|
3119
|
+
return {
|
|
3120
|
+
...first,
|
|
3121
|
+
...bottom ? { bottom } : {}
|
|
3122
|
+
};
|
|
3123
|
+
}
|
|
1686
3124
|
function layoutTableRow(row, rowIdx, rowCount, colCount, tableProps, columnWidthsPt, options, fontResources, imageResources, rowMergeRoles, aboveBordersByCol) {
|
|
1687
3125
|
const cells = [];
|
|
1688
3126
|
const columnXOffsets = [];
|
|
1689
|
-
|
|
1690
|
-
let
|
|
3127
|
+
const skip = Math.min(Math.max(0, row.properties.gridBefore ?? 0), columnWidthsPt.length);
|
|
3128
|
+
let cursorX = columnWidthsPt.slice(0, skip).reduce((sum, w) => sum + w, 0);
|
|
3129
|
+
let colIdx = skip;
|
|
1691
3130
|
for (let cellIdx = 0; cellIdx < row.cells.length; cellIdx++) {
|
|
1692
3131
|
const cell = row.cells[cellIdx];
|
|
1693
3132
|
const span = Math.max(1, cell.properties.colSpan ?? 1);
|
|
1694
3133
|
let widthPt = 0;
|
|
1695
3134
|
for (let k = 0; k < span && colIdx + k < columnWidthsPt.length; k++) widthPt += columnWidthsPt[colIdx + k];
|
|
3135
|
+
const paintSpan = Math.min(span, Math.max(1, cell.properties.paintColumns ?? span));
|
|
3136
|
+
let paintWidthPt = 0;
|
|
3137
|
+
for (let k = 0; k < paintSpan && colIdx + k < columnWidthsPt.length; k++) paintWidthPt += columnWidthsPt[colIdx + k];
|
|
1696
3138
|
columnXOffsets.push(cursorX);
|
|
1697
3139
|
const mergeRole = rowMergeRoles[cellIdx] ?? "standalone";
|
|
1698
3140
|
const leftNeighbor = cellIdx > 0 ? row.cells[cellIdx - 1].properties.borders : void 0;
|
|
1699
|
-
const cellLayout = layoutTableCell(cell, tableProps, widthPt, options, fontResources, imageResources, rowIdx, colIdx, span, rowCount, colCount, mergeRole, leftNeighbor, aboveBordersByCol
|
|
1700
|
-
cells.push(
|
|
3141
|
+
const cellLayout = layoutTableCell(cell, tableProps, widthPt, options, fontResources, imageResources, rowIdx, colIdx, span, rowCount, colCount, mergeRole, leftNeighbor, aboveOfSpan(aboveBordersByCol, colIdx, span));
|
|
3142
|
+
cells.push(paintWidthPt < widthPt ? {
|
|
3143
|
+
...cellLayout,
|
|
3144
|
+
paintWidthPt
|
|
3145
|
+
} : cellLayout);
|
|
1701
3146
|
cursorX += widthPt;
|
|
1702
3147
|
colIdx += span;
|
|
1703
3148
|
}
|
|
1704
3149
|
let heightPt = 0;
|
|
1705
|
-
for (const c of cells)
|
|
1706
|
-
|
|
1707
|
-
|
|
3150
|
+
for (const c of cells) {
|
|
3151
|
+
if (c.mergeRole === "start") continue;
|
|
3152
|
+
if (c.totalHeightPt > heightPt) heightPt = c.totalHeightPt;
|
|
3153
|
+
}
|
|
3154
|
+
if (row.properties.height && row.properties.heightRule === "exact") {
|
|
3155
|
+
heightPt = row.properties.height;
|
|
3156
|
+
return {
|
|
3157
|
+
heightPt,
|
|
3158
|
+
cells: cells.map((c) => clipCellToHeight(c, heightPt)),
|
|
3159
|
+
columnXOffsets,
|
|
3160
|
+
rowIdx,
|
|
3161
|
+
rowCount
|
|
3162
|
+
};
|
|
3163
|
+
} else if (row.properties.height && row.properties.heightRule === "atLeast") heightPt = Math.max(heightPt, row.properties.height);
|
|
1708
3164
|
return {
|
|
1709
3165
|
heightPt,
|
|
1710
3166
|
cells,
|
|
@@ -1713,6 +3169,59 @@ function layoutTableRow(row, rowIdx, rowCount, colCount, tableProps, columnWidth
|
|
|
1713
3169
|
rowCount
|
|
1714
3170
|
};
|
|
1715
3171
|
}
|
|
3172
|
+
/**
|
|
3173
|
+
* A cell with only the lines that fit inside `heightPt`.
|
|
3174
|
+
*
|
|
3175
|
+
* Whole lines only: half a line of text reads as a rendering fault, where a
|
|
3176
|
+
* missing one reads as the clipping it is. A cell that already fits, or whose
|
|
3177
|
+
* first line does not, is returned untouched — the first line always draws, as
|
|
3178
|
+
* it does in Excel.
|
|
3179
|
+
*
|
|
3180
|
+
* @param cell The laid-out cell.
|
|
3181
|
+
* @param heightPt The row's pinned height.
|
|
3182
|
+
* @returns The cell, clipped.
|
|
3183
|
+
*/
|
|
3184
|
+
function clipCellToHeight(cell, heightPt) {
|
|
3185
|
+
const room = heightPt - cell.padTopPt - cell.padBottomPt;
|
|
3186
|
+
if (cell.lines.length <= 1 || cell.contentHeightPt <= room) return cell;
|
|
3187
|
+
const kept = [];
|
|
3188
|
+
let used = 0;
|
|
3189
|
+
for (const line of cell.lines) {
|
|
3190
|
+
const h = computeLineHeight(line, line.resolved);
|
|
3191
|
+
if (kept.length > 0 && used + h > room) break;
|
|
3192
|
+
kept.push(line);
|
|
3193
|
+
used += h;
|
|
3194
|
+
}
|
|
3195
|
+
return {
|
|
3196
|
+
...cell,
|
|
3197
|
+
lines: kept,
|
|
3198
|
+
contentHeightPt: used
|
|
3199
|
+
};
|
|
3200
|
+
}
|
|
3201
|
+
/**
|
|
3202
|
+
* The one run a picture BLOCK is made of, so a context that lays out paragraphs
|
|
3203
|
+
* (a table cell) can draw it without a block path of its own.
|
|
3204
|
+
*
|
|
3205
|
+
* @param image The image block.
|
|
3206
|
+
* @returns A run carrying it as an inline picture.
|
|
3207
|
+
*/
|
|
3208
|
+
function imageRun(image) {
|
|
3209
|
+
return {
|
|
3210
|
+
text: "",
|
|
3211
|
+
properties: {},
|
|
3212
|
+
inlineImage: {
|
|
3213
|
+
...image.resource ? { resource: image.resource } : {},
|
|
3214
|
+
...image.outline ? { outline: image.outline } : {},
|
|
3215
|
+
width: image.width,
|
|
3216
|
+
height: image.height,
|
|
3217
|
+
...image.crop ? { crop: image.crop } : {},
|
|
3218
|
+
...image.rotation60k ? { rotation60k: image.rotation60k } : {},
|
|
3219
|
+
...image.flipH ? { flipH: true } : {},
|
|
3220
|
+
...image.flipV ? { flipV: true } : {},
|
|
3221
|
+
...image.effectExtent ? { effectExtent: image.effectExtent } : {}
|
|
3222
|
+
}
|
|
3223
|
+
};
|
|
3224
|
+
}
|
|
1716
3225
|
function layoutTableCell(cell, tableProps, widthPt, options, fontResources, imageResources, rowIdx, colStart, colSpan, rowCount, colCount, mergeRole, leftNeighborBorders, aboveNeighborBorders) {
|
|
1717
3226
|
const cellMar = cell.properties.margins;
|
|
1718
3227
|
const tableMar = tableProps.defaultCellMargins;
|
|
@@ -1721,24 +3230,66 @@ function layoutTableCell(cell, tableProps, widthPt, options, fontResources, imag
|
|
|
1721
3230
|
const padLeftBase = cellMar?.left ?? tableMar?.left ?? DEFAULT_CELL_PADDING_TWIPS * TWIP_TO_PT;
|
|
1722
3231
|
const padRightBase = cellMar?.right ?? tableMar?.right ?? DEFAULT_CELL_PADDING_TWIPS * TWIP_TO_PT;
|
|
1723
3232
|
const padLeftPt = padLeftBase + (cell.properties.icon ? CF_ICON_GUTTER_PT : 0);
|
|
1724
|
-
const padRightPt = padRightBase
|
|
3233
|
+
const padRightPt = padRightBase;
|
|
1725
3234
|
const innerWidth = Math.max(1, widthPt - padLeftPt - padRightPt);
|
|
1726
3235
|
const lines = [];
|
|
1727
3236
|
const nestedTables = [];
|
|
3237
|
+
const cellShapes = [];
|
|
3238
|
+
const lineGaps = /* @__PURE__ */ new Map();
|
|
1728
3239
|
let contentHeightPt = 0;
|
|
3240
|
+
let clipped = false;
|
|
3241
|
+
let pendingGapPt = 0;
|
|
3242
|
+
const openParagraph = (spacingBeforePt) => {
|
|
3243
|
+
const gap = pendingGapPt + spacingBeforePt;
|
|
3244
|
+
pendingGapPt = 0;
|
|
3245
|
+
if (gap <= 0) return;
|
|
3246
|
+
lineGaps.set(lines.length, (lineGaps.get(lines.length) ?? 0) + gap);
|
|
3247
|
+
contentHeightPt += gap;
|
|
3248
|
+
};
|
|
1729
3249
|
if (mergeRole !== "middle" && mergeRole !== "end") {
|
|
1730
3250
|
for (const el of cell.content) if (el.kind === "paragraph") {
|
|
1731
|
-
const
|
|
1732
|
-
|
|
3251
|
+
const noWrap = cell.properties.noWrap === true;
|
|
3252
|
+
const block = layoutParagraphBlock(el.paragraph, options, fontResources, imageResources, noWrap ? NO_WRAP_MEASURE_WIDTH : innerWidth);
|
|
3253
|
+
const cutAt = widthPt - padLeftPt;
|
|
3254
|
+
if (noWrap && (block.lines[0]?.contentWidthPt ?? 0) > cutAt) clipped = true;
|
|
3255
|
+
const keep = noWrap ? clipLineToWidth(block.lines, cutAt, cell.properties.hashOnOverflow === true) : block.lines;
|
|
3256
|
+
const markOnly = cell.properties.hideMark === true && el.paragraph.runs.every((r) => r.text === "");
|
|
3257
|
+
if (!markOnly) openParagraph(block.spacingBeforePt);
|
|
3258
|
+
for (const line of keep) {
|
|
1733
3259
|
lines.push(line);
|
|
1734
|
-
contentHeightPt += computeLineHeight(line, block.resolved);
|
|
3260
|
+
if (!markOnly) contentHeightPt += computeLineHeight(line, block.resolved);
|
|
1735
3261
|
}
|
|
1736
|
-
|
|
3262
|
+
if (!markOnly) pendingGapPt = block.spacingAfterPt;
|
|
1737
3263
|
} else if (el.kind === "table") {
|
|
1738
3264
|
const nested = layoutTableBlock(el.table, options, fontResources, imageResources, innerWidth);
|
|
1739
|
-
nestedTables.push(
|
|
3265
|
+
nestedTables.push({
|
|
3266
|
+
block: nested,
|
|
3267
|
+
afterLine: lines.length
|
|
3268
|
+
});
|
|
1740
3269
|
contentHeightPt += nested.heightPt;
|
|
3270
|
+
} else if (el.kind === "image" && !isOutOfFlowFloat(el.image.float)) {
|
|
3271
|
+
const block = layoutParagraphBlock({
|
|
3272
|
+
properties: el.image.paragraphProperties,
|
|
3273
|
+
runs: [imageRun(el.image)]
|
|
3274
|
+
}, options, fontResources, imageResources, innerWidth);
|
|
3275
|
+
openParagraph(block.spacingBeforePt);
|
|
3276
|
+
for (const line of block.lines) {
|
|
3277
|
+
lines.push(line);
|
|
3278
|
+
contentHeightPt += computeLineHeight(line, block.resolved);
|
|
3279
|
+
}
|
|
3280
|
+
contentHeightPt += block.spacingAfterPt;
|
|
3281
|
+
pendingGapPt = block.spacingAfterPt;
|
|
3282
|
+
} else if (el.kind === "shape" && !isOutOfFlowFloat(el.shape.float)) {
|
|
3283
|
+
const block = layoutShapeBlock(el.shape, options, fontResources, imageResources, innerWidth);
|
|
3284
|
+
openParagraph(block.spacingBeforePt);
|
|
3285
|
+
cellShapes.push({
|
|
3286
|
+
block,
|
|
3287
|
+
afterLine: lines.length
|
|
3288
|
+
});
|
|
3289
|
+
contentHeightPt += block.heightPt;
|
|
3290
|
+
pendingGapPt = block.spacingAfterPt;
|
|
1741
3291
|
}
|
|
3292
|
+
contentHeightPt += pendingGapPt;
|
|
1742
3293
|
}
|
|
1743
3294
|
const colEnd = colStart + colSpan - 1;
|
|
1744
3295
|
const borders = resolveCellBorders(cell.properties.borders, tableProps.borders, rowIdx, colStart, colEnd, rowCount, colCount, mergeRole, leftNeighborBorders, aboveNeighborBorders);
|
|
@@ -1754,14 +3305,17 @@ function layoutTableCell(cell, tableProps, widthPt, options, fontResources, imag
|
|
|
1754
3305
|
...cell.properties.dataBar ? { dataBar: cell.properties.dataBar } : {},
|
|
1755
3306
|
...cell.properties.icon ? { icon: cell.properties.icon } : {},
|
|
1756
3307
|
...cell.properties.sparkline ? { sparkline: cell.properties.sparkline } : {},
|
|
1757
|
-
...cell.properties.dropdown ? { dropdown: true } : {},
|
|
1758
3308
|
lines,
|
|
3309
|
+
...lineGaps.size > 0 ? { lineGaps } : {},
|
|
1759
3310
|
...nestedTables.length > 0 ? { nestedTables } : {},
|
|
3311
|
+
...cellShapes.length > 0 ? { shapes: cellShapes } : {},
|
|
1760
3312
|
contentHeightPt,
|
|
1761
3313
|
totalHeightPt,
|
|
3314
|
+
...cell.properties.verticalAlign ? { verticalAlign: cell.properties.verticalAlign } : {},
|
|
1762
3315
|
colStart,
|
|
1763
3316
|
colSpan,
|
|
1764
|
-
mergeRole
|
|
3317
|
+
mergeRole,
|
|
3318
|
+
...clipped ? { clipped: true } : {}
|
|
1765
3319
|
};
|
|
1766
3320
|
}
|
|
1767
3321
|
var BORDER_STYLE_RANK = {
|
|
@@ -1769,6 +3323,9 @@ var BORDER_STYLE_RANK = {
|
|
|
1769
3323
|
thick: 4,
|
|
1770
3324
|
single: 3,
|
|
1771
3325
|
dashed: 2,
|
|
3326
|
+
dashSmallGap: 2,
|
|
3327
|
+
dashDot: 2,
|
|
3328
|
+
dashDotDot: 2,
|
|
1772
3329
|
dotted: 1,
|
|
1773
3330
|
none: 0
|
|
1774
3331
|
};
|
|
@@ -1874,7 +3431,7 @@ function listItemParagraphNode(builder, stack, list, wantLbl) {
|
|
|
1874
3431
|
* Push order into `current`/`pages` IS the byte order of the emitted PDF, so the
|
|
1875
3432
|
* bodies move unchanged.
|
|
1876
3433
|
*/
|
|
1877
|
-
var PageAssembler = class {
|
|
3434
|
+
var PageAssembler = class PageAssembler {
|
|
1878
3435
|
/**
|
|
1879
3436
|
* @param sectionCtxs The per-section render contexts, in order; assembly
|
|
1880
3437
|
* starts on the first section's first page.
|
|
@@ -1884,20 +3441,29 @@ var PageAssembler = class {
|
|
|
1884
3441
|
* `undefined` when there are none.
|
|
1885
3442
|
* @param bookmarkPositions Out-param: bookmark name → its GoTo destination.
|
|
1886
3443
|
*/
|
|
1887
|
-
constructor(sectionCtxs, builder, notes, bookmarkPositions) {
|
|
3444
|
+
constructor(sectionCtxs, builder, notes, bookmarkPositions, backgroundImageName) {
|
|
1888
3445
|
this.sectionCtxs = sectionCtxs;
|
|
1889
3446
|
this.builder = builder;
|
|
1890
3447
|
this.notes = notes;
|
|
1891
3448
|
this.bookmarkPositions = bookmarkPositions;
|
|
3449
|
+
this.backgroundImageName = backgroundImageName;
|
|
1892
3450
|
this.ctx = sectionCtxs[0];
|
|
3451
|
+
this.pageStartCtx = this.ctx;
|
|
3452
|
+
this.pageNumber = this.ctx.properties.pageNumberStart ?? 1;
|
|
1893
3453
|
this.cursorY = this.ctx.pageHeight - this.ctx.marginTop;
|
|
1894
3454
|
this.colStartY = this.cursorY;
|
|
3455
|
+
this.bandTopY = this.cursorY;
|
|
1895
3456
|
}
|
|
1896
3457
|
pages = [];
|
|
1897
3458
|
ctx;
|
|
1898
3459
|
secIdx = 0;
|
|
1899
3460
|
pageInSection = 0;
|
|
1900
3461
|
globalPageIdx = 0;
|
|
3462
|
+
/**
|
|
3463
|
+
* §17.6.12 — the number the NEXT page prints for a PAGE field. Counts up with
|
|
3464
|
+
* the pages and restarts wherever a section names a `w:pgNumType w:start`.
|
|
3465
|
+
*/
|
|
3466
|
+
pageNumber = 1;
|
|
1901
3467
|
current = [];
|
|
1902
3468
|
pendingPageBreak = false;
|
|
1903
3469
|
cursorY;
|
|
@@ -1911,6 +3477,12 @@ var PageAssembler = class {
|
|
|
1911
3477
|
colStartLen = 0;
|
|
1912
3478
|
/** The cursor's y at the top of the current column — see {@link colHasContent}. */
|
|
1913
3479
|
colStartY;
|
|
3480
|
+
/**
|
|
3481
|
+
* Where this page's column band begins. The top margin on an ordinary page;
|
|
3482
|
+
* further down when a §17.6.22 `continuous` section started mid-page, whose
|
|
3483
|
+
* columns run beside each other from there, not from the paper's top.
|
|
3484
|
+
*/
|
|
3485
|
+
bandTopY;
|
|
1914
3486
|
/** The current column's left edge (`marginLeft` plus the column x-offset). */
|
|
1915
3487
|
colLeft = () => this.ctx.marginLeft + (this.ctx.columns?.[this.colIdx]?.xOffsetPt ?? 0);
|
|
1916
3488
|
/** The current column's width (the section content width when single-column). */
|
|
@@ -1929,20 +3501,131 @@ var PageAssembler = class {
|
|
|
1929
3501
|
* Consumed space counts as use, whether or not any ink went with it.
|
|
1930
3502
|
*/
|
|
1931
3503
|
colHasContent = () => this.current.length > this.colStartLen || this.cursorY < this.colStartY;
|
|
3504
|
+
/**
|
|
3505
|
+
* Whether this page has ANYTHING on it — including drawings, which is the
|
|
3506
|
+
* whole point: a page carrying only floats has an empty `current`, so a page
|
|
3507
|
+
* break asked for after it was silently dropped and the next band's drawings
|
|
3508
|
+
* landed on top of the first's. tdf111980_radioButtons.xlsx has no cells at
|
|
3509
|
+
* all and eleven controls wider than its page.
|
|
3510
|
+
*/
|
|
3511
|
+
pageHasContent = () => this.current.length > 0 || this.floatsBehind.length > 0 || this.floatsFront.length > 0;
|
|
1932
3512
|
/** Overflow step: next column on this page, or a fresh page after the last. */
|
|
1933
3513
|
advanceColumn = () => {
|
|
1934
3514
|
if (this.ctx.columns && this.colIdx + 1 < this.ctx.columns.length) {
|
|
1935
3515
|
this.colIdx++;
|
|
1936
3516
|
this.colStartLen = this.current.length;
|
|
1937
|
-
this.cursorY = this.
|
|
3517
|
+
this.cursorY = this.bandTopY;
|
|
1938
3518
|
this.colStartY = this.cursorY;
|
|
3519
|
+
const isLast = this.colIdx + 1 >= this.ctx.columns.length;
|
|
3520
|
+
this.balanceBottomY = this.balanceHeightPt > 0 && !isLast ? this.cursorY - this.balanceHeightPt : void 0;
|
|
1939
3521
|
} else this.flushPage();
|
|
1940
3522
|
};
|
|
1941
3523
|
/**
|
|
3524
|
+
* §17.6.8 — the running line count for the margin numbers. Reset by the
|
|
3525
|
+
* section's own `w:restart` rule.
|
|
3526
|
+
*/
|
|
3527
|
+
lineNumber = 0;
|
|
3528
|
+
/**
|
|
3529
|
+
* §17.6.8 — the margin number for the body line just placed, if the section
|
|
3530
|
+
* asks for one at this count. Pushed at the line's own baseline, right-
|
|
3531
|
+
* aligned a quarter inch off the text.
|
|
3532
|
+
*
|
|
3533
|
+
* @param baselineY The line's baseline, in page (y-down) coordinates.
|
|
3534
|
+
* @param line The line being numbered — its font draws the digits.
|
|
3535
|
+
*/
|
|
3536
|
+
emitLineNumber = (baselineY, line) => {
|
|
3537
|
+
const ln = this.ctx.properties.lineNumbering;
|
|
3538
|
+
if (!ln) return;
|
|
3539
|
+
this.lineNumber++;
|
|
3540
|
+
const n = ln.start + this.lineNumber - 1;
|
|
3541
|
+
if ((n - ln.start + 1) % ln.countBy !== 0) return;
|
|
3542
|
+
const token = line.tokens.find((t) => t.kind === "text");
|
|
3543
|
+
if (!token) return;
|
|
3544
|
+
const text = String(n);
|
|
3545
|
+
const sizePt = token.fontSizePt;
|
|
3546
|
+
const widthPt = token.font.measure.textWidthPt(text, sizePt);
|
|
3547
|
+
const digits = {
|
|
3548
|
+
kind: "text",
|
|
3549
|
+
text,
|
|
3550
|
+
isSpace: false,
|
|
3551
|
+
resolvedRun: token.resolvedRun,
|
|
3552
|
+
font: token.font,
|
|
3553
|
+
fontSizePt: sizePt,
|
|
3554
|
+
widthPt,
|
|
3555
|
+
bidiLevel: 0
|
|
3556
|
+
};
|
|
3557
|
+
const numbered = {
|
|
3558
|
+
...line,
|
|
3559
|
+
tokens: [digits],
|
|
3560
|
+
contentWidthPt: widthPt
|
|
3561
|
+
};
|
|
3562
|
+
this.current.push({
|
|
3563
|
+
type: "line",
|
|
3564
|
+
line: numbered,
|
|
3565
|
+
originX: pt(this.colLeft() - (ln.distancePt ?? LINE_NUMBER_GAP_PT) - widthPt),
|
|
3566
|
+
baselineY: pt(baselineY)
|
|
3567
|
+
});
|
|
3568
|
+
};
|
|
3569
|
+
/**
|
|
3570
|
+
* §17.6.12 — a section that names its own first page number restarts the
|
|
3571
|
+
* count there; one that names none carries on from the section before.
|
|
3572
|
+
*
|
|
3573
|
+
* @param next The section being entered.
|
|
3574
|
+
*/
|
|
3575
|
+
restartPageNumbers = (next) => {
|
|
3576
|
+
const start = next.properties.pageNumberStart;
|
|
3577
|
+
if (start !== void 0) this.pageNumber = start;
|
|
3578
|
+
if ((next.properties.lineNumbering?.restart ?? "newPage") !== "continuous") this.lineNumber = 0;
|
|
3579
|
+
};
|
|
3580
|
+
/**
|
|
3581
|
+
* §17.6.22 — start a `continuous` section on the page in hand: its columns
|
|
3582
|
+
* begin where the section does, not at the top of the paper.
|
|
3583
|
+
*
|
|
3584
|
+
* @param next The section to continue into.
|
|
3585
|
+
*/
|
|
3586
|
+
startBandSection = (next, bandHeightPt) => {
|
|
3587
|
+
this.secIdx++;
|
|
3588
|
+
this.ctx = next;
|
|
3589
|
+
this.restartPageNumbers(next);
|
|
3590
|
+
this.colIdx = 0;
|
|
3591
|
+
this.colStartLen = this.current.length;
|
|
3592
|
+
this.cursorY = Math.min(this.cursorY, next.pageHeight - next.marginTop);
|
|
3593
|
+
this.bandTopY = this.cursorY;
|
|
3594
|
+
this.colStartY = this.cursorY;
|
|
3595
|
+
this.beginBalancedBand(bandHeightPt);
|
|
3596
|
+
};
|
|
3597
|
+
/**
|
|
3598
|
+
* §17.6.4 — even the columns of the band that starts at the cursor: each gets
|
|
3599
|
+
* the same share of `bandHeightPt`. A band too tall for the page fills column
|
|
3600
|
+
* after column as before; there is nothing to even out when it will spill
|
|
3601
|
+
* anyway.
|
|
3602
|
+
*
|
|
3603
|
+
* @param bandHeightPt The height of everything the band holds.
|
|
3604
|
+
*/
|
|
3605
|
+
beginBalancedBand = (bandHeightPt) => {
|
|
3606
|
+
const colCount = this.ctx.columns?.length ?? 1;
|
|
3607
|
+
const room = this.cursorY - (this.ctx.marginBottom + this.noteReserve);
|
|
3608
|
+
const share = colCount > 1 ? balancedColumnHeight(bandHeightPt, colCount, room) : 0;
|
|
3609
|
+
this.balanceHeightPt = share;
|
|
3610
|
+
this.balanceBottomY = share > 0 ? this.cursorY - share : void 0;
|
|
3611
|
+
};
|
|
3612
|
+
/**
|
|
1942
3613
|
* §20.4.2.3 out-of-flow drawings (wrap `'none'`): they render at their anchored
|
|
1943
3614
|
* position without moving the cursor. `behindDoc` sinks below the body text,
|
|
1944
3615
|
* everything else above it; both flush with the page.
|
|
1945
3616
|
*/
|
|
3617
|
+
/**
|
|
3618
|
+
* §20.4.2.3 — a float layer, drawn in `relativeHeight` order. A stable sort
|
|
3619
|
+
* by z leaves the ones that state none where the document put them:
|
|
3620
|
+
* dml-rectangle-relsize.docx writes its blue bar first and gives it the
|
|
3621
|
+
* higher z, and drawn in document order the white rectangle covered it.
|
|
3622
|
+
*/
|
|
3623
|
+
/** A float layer that stamps each item it takes with the float's z-order. */
|
|
3624
|
+
static zSink = (target, z) => z === void 0 ? target : { push: (...items) => target.push(...items.map((i) => ({
|
|
3625
|
+
...i,
|
|
3626
|
+
z
|
|
3627
|
+
}))) };
|
|
3628
|
+
static byZ = (items) => items.every((i) => i.z === void 0) ? [...items] : [...items].sort((a, b) => (a.z ?? 0) - (b.z ?? 0));
|
|
1946
3629
|
floatsBehind = [];
|
|
1947
3630
|
floatsFront = [];
|
|
1948
3631
|
/**
|
|
@@ -1950,12 +3633,26 @@ var PageAssembler = class {
|
|
|
1950
3633
|
* must flow around. Page-scoped, like the float graphics.
|
|
1951
3634
|
*/
|
|
1952
3635
|
exclusions = [];
|
|
3636
|
+
/**
|
|
3637
|
+
* Claim the area a side-wrapping float keeps to itself: its own box grown by
|
|
3638
|
+
* the §20.4.2.3 `distT/B/L/R` stand-off the anchor asks for.
|
|
3639
|
+
*/
|
|
3640
|
+
excludeFloat = (f, x, topYUp, w, h) => {
|
|
3641
|
+
const d = f.wrapDist;
|
|
3642
|
+
this.exclusions.push({
|
|
3643
|
+
x0: x - (d?.leftPt ?? 0),
|
|
3644
|
+
x1: x + w + (d?.rightPt ?? 0),
|
|
3645
|
+
topYUp: topYUp + (d?.topPt ?? 0),
|
|
3646
|
+
bottomYUp: topYUp - h - (d?.bottomPt ?? 0)
|
|
3647
|
+
});
|
|
3648
|
+
};
|
|
1953
3649
|
/** The float's left edge on the page, resolved from its horizontal anchor. */
|
|
1954
3650
|
floatX = (f, widthPt) => {
|
|
1955
3651
|
const h = f.posH;
|
|
1956
3652
|
if (!h) return this.colLeft();
|
|
1957
|
-
const
|
|
1958
|
-
const
|
|
3653
|
+
const textRight = this.ctx.marginLeft + this.ctx.contentWidth;
|
|
3654
|
+
const base = h.relativeFrom === "page" || h.relativeFrom === "leftMargin" ? 0 : h.relativeFrom === "column" ? this.colLeft() : h.relativeFrom === "rightMargin" ? textRight : this.ctx.marginLeft;
|
|
3655
|
+
const span = h.relativeFrom === "page" ? this.ctx.pageWidth : h.relativeFrom === "column" ? this.colWidth() : h.relativeFrom === "leftMargin" ? this.ctx.marginLeft : h.relativeFrom === "rightMargin" ? this.ctx.pageWidth - textRight : this.ctx.contentWidth;
|
|
1959
3656
|
if (h.align === "center") return base + (span - widthPt) / 2;
|
|
1960
3657
|
if (h.align === "right") return base + span - widthPt;
|
|
1961
3658
|
return base + (h.offsetPt ?? 0);
|
|
@@ -1964,10 +3661,19 @@ var PageAssembler = class {
|
|
|
1964
3661
|
* The drawing's TOP in the y-up cursor frame. Paragraph/line-relative offsets
|
|
1965
3662
|
* hang off the anchoring paragraph's current position.
|
|
1966
3663
|
*/
|
|
1967
|
-
floatTopYUp = (f) => {
|
|
3664
|
+
floatTopYUp = (f, heightPt = 0) => {
|
|
1968
3665
|
const v = f.posV;
|
|
1969
3666
|
if (!v) return this.cursorY;
|
|
1970
|
-
if (v.
|
|
3667
|
+
if (v.align !== void 0) {
|
|
3668
|
+
const page = v.relativeFrom === "page";
|
|
3669
|
+
const topYUp = page ? this.ctx.pageHeight : this.ctx.pageHeight - this.ctx.marginTop;
|
|
3670
|
+
const bottomYUp = page ? 0 : this.ctx.marginBottom;
|
|
3671
|
+
if (v.align === "top") return topYUp;
|
|
3672
|
+
if (v.align === "bottom") return bottomYUp + heightPt;
|
|
3673
|
+
return bottomYUp + (topYUp - bottomYUp + heightPt) / 2;
|
|
3674
|
+
}
|
|
3675
|
+
if (v.relativeFrom === "page" || v.relativeFrom === "topMargin") return this.ctx.pageHeight - (v.offsetPt ?? 0);
|
|
3676
|
+
if (v.relativeFrom === "bottomMargin") return this.ctx.marginBottom - (v.offsetPt ?? 0);
|
|
1971
3677
|
if (v.relativeFrom === "margin") return this.ctx.pageHeight - this.ctx.marginTop - (v.offsetPt ?? 0);
|
|
1972
3678
|
return this.cursorY - (v.offsetPt ?? 0);
|
|
1973
3679
|
};
|
|
@@ -1986,7 +3692,21 @@ var PageAssembler = class {
|
|
|
1986
3692
|
noteReserve = 0;
|
|
1987
3693
|
placedNotes = /* @__PURE__ */ new Set();
|
|
1988
3694
|
/** The page's usable bottom: the margin plus whatever the notes band has claimed so far. */
|
|
1989
|
-
|
|
3695
|
+
/**
|
|
3696
|
+
* §17.6.4 — where the current column must stop. Normally the page's bottom
|
|
3697
|
+
* margin (plus any footnote reserve); inside a BALANCED band it is the line
|
|
3698
|
+
* the band's share of the height reaches, so the columns come out even.
|
|
3699
|
+
*/
|
|
3700
|
+
bottomLimit = () => Math.max(this.ctx.marginBottom + this.noteReserve, this.balanceBottomY ?? Number.NEGATIVE_INFINITY);
|
|
3701
|
+
/**
|
|
3702
|
+
* §17.6.4 — a `continuous` section of several columns whose content fits the
|
|
3703
|
+
* page is BALANCED: Word evens the columns out rather than filling the first
|
|
3704
|
+
* to the bottom. Set while such a band is being laid out; the height each
|
|
3705
|
+
* column is given is `balanceHeightPt`. IndexFieldFlagF.docx sets its index
|
|
3706
|
+
* that way and we filled column one and left the rest empty.
|
|
3707
|
+
*/
|
|
3708
|
+
balanceBottomY;
|
|
3709
|
+
balanceHeightPt = 0;
|
|
1990
3710
|
/**
|
|
1991
3711
|
* The first exclusion a line spanning `[yTop-h, yTop]` collides with in the
|
|
1992
3712
|
* current column (x-overlap with the column required).
|
|
@@ -2014,15 +3734,17 @@ var PageAssembler = class {
|
|
|
2014
3734
|
const cl = this.colLeft();
|
|
2015
3735
|
const leftRoom = e.x0 - FLOAT_TEXT_GAP - cl;
|
|
2016
3736
|
const rightRoom = cl + full - (e.x1 + FLOAT_TEXT_GAP);
|
|
2017
|
-
if (rightRoom
|
|
2018
|
-
|
|
2019
|
-
|
|
2020
|
-
|
|
2021
|
-
|
|
2022
|
-
|
|
2023
|
-
|
|
3737
|
+
if (Math.max(leftRoom, rightRoom) < MIN_WRAP_WIDTH) return {
|
|
3738
|
+
width: full,
|
|
3739
|
+
xOffset: 0,
|
|
3740
|
+
blockedUntilYUp: e.bottomYUp
|
|
3741
|
+
};
|
|
3742
|
+
if (rightRoom >= leftRoom) return {
|
|
3743
|
+
width: rightRoom,
|
|
3744
|
+
xOffset: full - rightRoom
|
|
3745
|
+
};
|
|
2024
3746
|
return {
|
|
2025
|
-
width:
|
|
3747
|
+
width: leftRoom,
|
|
2026
3748
|
xOffset: 0
|
|
2027
3749
|
};
|
|
2028
3750
|
};
|
|
@@ -2041,9 +3763,15 @@ var PageAssembler = class {
|
|
|
2041
3763
|
let y = startY;
|
|
2042
3764
|
for (let i = 0; i < 200; i++) {
|
|
2043
3765
|
const g = this.lineGeometryAt(y, h0);
|
|
3766
|
+
if (g.blockedUntilYUp !== void 0) {
|
|
3767
|
+
widths.push(this.colWidth());
|
|
3768
|
+
y = g.blockedUntilYUp - h0;
|
|
3769
|
+
if (y < this.bottomLimit()) break;
|
|
3770
|
+
continue;
|
|
3771
|
+
}
|
|
2044
3772
|
widths.push(g.width);
|
|
2045
3773
|
if (g.width < this.colWidth()) narrowed = true;
|
|
2046
|
-
else if (
|
|
3774
|
+
else if (narrowed) break;
|
|
2047
3775
|
y -= h0;
|
|
2048
3776
|
if (y < this.bottomLimit()) break;
|
|
2049
3777
|
}
|
|
@@ -2095,7 +3823,7 @@ var PageAssembler = class {
|
|
|
2095
3823
|
const noteNode = this.builder.create("Note", this.builder.root);
|
|
2096
3824
|
structId = this.builder.create("P", noteNode).id;
|
|
2097
3825
|
}
|
|
2098
|
-
out.push(...drawBlocksSequentially(note.blocks, this.ctx.marginLeft, cursor, this.ctx.pageHeight, structId));
|
|
3826
|
+
out.push(...drawBlocksSequentially(note.blocks, this.ctx.marginLeft, cursor, this.ctx.pageHeight, this.ctx.contentWidth, structId));
|
|
2099
3827
|
cursor -= note.heightPt;
|
|
2100
3828
|
}
|
|
2101
3829
|
return out;
|
|
@@ -2114,28 +3842,31 @@ var PageAssembler = class {
|
|
|
2114
3842
|
* guarantee one page for a header/footer-only document).
|
|
2115
3843
|
*/
|
|
2116
3844
|
flushPage = (force = false) => {
|
|
2117
|
-
if (this.
|
|
3845
|
+
if (!this.pageHasContent() && this.cursorY >= this.colStartY && !force) return;
|
|
2118
3846
|
const band = bandForPage(this.pageInSection, this.globalPageIdx, this.ctx.titlePg, this.ctx.evenAndOddHeaders);
|
|
2119
3847
|
const header = pickBand(this.ctx.headerSet, band);
|
|
2120
3848
|
const footer = pickBand(this.ctx.footerSet, band);
|
|
2121
3849
|
if (header.renderDynamic) this.dynBands.push({
|
|
2122
3850
|
pageIdx: this.pages.length,
|
|
2123
|
-
pageNumber: this.
|
|
3851
|
+
pageNumber: this.pageNumber,
|
|
2124
3852
|
position: "header",
|
|
2125
3853
|
render: header.renderDynamic
|
|
2126
3854
|
});
|
|
2127
3855
|
if (footer.renderDynamic) this.dynBands.push({
|
|
2128
3856
|
pageIdx: this.pages.length,
|
|
2129
|
-
pageNumber: this.
|
|
3857
|
+
pageNumber: this.pageNumber,
|
|
2130
3858
|
position: "footer",
|
|
2131
3859
|
render: footer.renderDynamic
|
|
2132
3860
|
});
|
|
2133
3861
|
this.pages.push({
|
|
2134
3862
|
commands: [
|
|
3863
|
+
...this.pageBackgroundItems(),
|
|
3864
|
+
...this.pageBorderItems(),
|
|
3865
|
+
...this.columnSeparatorItems(),
|
|
2135
3866
|
...header.commands,
|
|
2136
|
-
...this.floatsBehind,
|
|
3867
|
+
...PageAssembler.byZ(this.floatsBehind),
|
|
2137
3868
|
...this.current,
|
|
2138
|
-
...this.floatsFront,
|
|
3869
|
+
...PageAssembler.byZ(this.floatsFront),
|
|
2139
3870
|
...this.renderNotesBand(),
|
|
2140
3871
|
...footer.commands
|
|
2141
3872
|
],
|
|
@@ -2152,35 +3883,212 @@ var PageAssembler = class {
|
|
|
2152
3883
|
this.colStartLen = 0;
|
|
2153
3884
|
this.pageInSection++;
|
|
2154
3885
|
this.globalPageIdx++;
|
|
3886
|
+
this.pageNumber++;
|
|
3887
|
+
if ((this.ctx.properties.lineNumbering?.restart ?? "newPage") === "newPage") this.lineNumber = 0;
|
|
2155
3888
|
this.cursorY = this.ctx.pageHeight - this.ctx.marginTop;
|
|
2156
3889
|
this.colStartY = this.cursorY;
|
|
3890
|
+
this.bandTopY = this.cursorY;
|
|
3891
|
+
this.balanceBottomY = void 0;
|
|
3892
|
+
this.balanceHeightPt = 0;
|
|
3893
|
+
this.pageStartCtx = this.ctx;
|
|
3894
|
+
};
|
|
3895
|
+
/**
|
|
3896
|
+
* §17.6.4 `w:cols w:sep` — the rule down the middle of every gutter, run the
|
|
3897
|
+
* height of the text area. multi-column-separator-with-line.docx asks for one
|
|
3898
|
+
* and we drew its two columns with nothing between them.
|
|
3899
|
+
*
|
|
3900
|
+
* @returns One border item per gutter, or none when the section asks for no rule.
|
|
3901
|
+
*/
|
|
3902
|
+
columnSeparatorItems = () => {
|
|
3903
|
+
const ctx = this.pageStartCtx;
|
|
3904
|
+
const cols = ctx.columns;
|
|
3905
|
+
if (!cols || cols.length < 2 || ctx.properties.columns?.separator !== true) return [];
|
|
3906
|
+
let deepest = ctx.marginTop;
|
|
3907
|
+
for (const item of this.current) {
|
|
3908
|
+
const bottom = item.type === "line" ? item.baselineY : item.type === "shape" ? 0 : item.y + item.height;
|
|
3909
|
+
if (bottom > deepest) deepest = bottom;
|
|
3910
|
+
}
|
|
3911
|
+
const height = Math.min(deepest, ctx.pageHeight - ctx.marginBottom) - ctx.marginTop;
|
|
3912
|
+
if (height <= 0) return [];
|
|
3913
|
+
const out = [];
|
|
3914
|
+
for (let i = 1; i < cols.length; i++) {
|
|
3915
|
+
const left = cols[i - 1];
|
|
3916
|
+
const right = cols[i];
|
|
3917
|
+
const x = ctx.marginLeft + (left.xOffsetPt + left.widthPt + right.xOffsetPt) / 2;
|
|
3918
|
+
out.push({
|
|
3919
|
+
type: "border",
|
|
3920
|
+
side: "left",
|
|
3921
|
+
x: pt(x),
|
|
3922
|
+
y: pt(ctx.marginTop),
|
|
3923
|
+
width: pt(0),
|
|
3924
|
+
height: pt(height),
|
|
3925
|
+
borderSizePt: COLUMN_SEPARATOR_PT,
|
|
3926
|
+
borderColorHex: "000000"
|
|
3927
|
+
});
|
|
3928
|
+
}
|
|
3929
|
+
return out;
|
|
3930
|
+
};
|
|
3931
|
+
/** The section context the CURRENT page began in (see {@link columnSeparatorItems}). */
|
|
3932
|
+
pageStartCtx;
|
|
3933
|
+
/**
|
|
3934
|
+
* §17.6.10 `w:pgBorders` — the rules around the page. `@w:offsetFrom` says
|
|
3935
|
+
* what each edge's space is measured from: the paper's own edge, or the text
|
|
3936
|
+
* margin the rule stands outside of. a4andborders.docx frames every page and
|
|
3937
|
+
* we drew nothing.
|
|
3938
|
+
*
|
|
3939
|
+
* @returns The border items for this page, in paint order.
|
|
3940
|
+
*/
|
|
3941
|
+
/**
|
|
3942
|
+
* §17.2.1 — what the page is painted before anything is drawn on it: the
|
|
3943
|
+
* `v:background`'s PICTURE or GRADIENT when it has one, else the flat
|
|
3944
|
+
* `w:background @w:color`. Read as the colour alone, fill.docx's five-stop
|
|
3945
|
+
* sweep came out the navy of its fallback and tdf126533_pageBitmap.docx's
|
|
3946
|
+
* paper came out plain.
|
|
3947
|
+
*
|
|
3948
|
+
* @returns The background items for this page (at most one).
|
|
3949
|
+
*/
|
|
3950
|
+
pageBackgroundItems = () => {
|
|
3951
|
+
const options = this.ctx.options;
|
|
3952
|
+
const w = this.ctx.pageWidth;
|
|
3953
|
+
const h = this.ctx.pageHeight;
|
|
3954
|
+
const fill = options?.pageBackgroundFill;
|
|
3955
|
+
if (fill?.kind === "picture" && this.backgroundImageName !== void 0) return [{
|
|
3956
|
+
type: "image",
|
|
3957
|
+
x: pt(0),
|
|
3958
|
+
y: pt(0),
|
|
3959
|
+
width: pt(w),
|
|
3960
|
+
height: pt(h),
|
|
3961
|
+
imageResourceName: this.backgroundImageName
|
|
3962
|
+
}];
|
|
3963
|
+
if (fill?.kind === "gradient" && fill.gradient) return [{
|
|
3964
|
+
type: "shape",
|
|
3965
|
+
shape: {
|
|
3966
|
+
paths: [rectAtPath(0, 0, w, h)],
|
|
3967
|
+
fillColorHex: gradientToSolid(fill.gradient),
|
|
3968
|
+
fillGradient: fill.gradient,
|
|
3969
|
+
transform: flipTransform([
|
|
3970
|
+
1,
|
|
3971
|
+
0,
|
|
3972
|
+
0,
|
|
3973
|
+
1,
|
|
3974
|
+
0,
|
|
3975
|
+
0
|
|
3976
|
+
], h)
|
|
3977
|
+
}
|
|
3978
|
+
}];
|
|
3979
|
+
if (options?.pageBackgroundColorHex !== void 0) return [{
|
|
3980
|
+
type: "fill",
|
|
3981
|
+
x: pt(0),
|
|
3982
|
+
y: pt(0),
|
|
3983
|
+
width: pt(w),
|
|
3984
|
+
height: pt(h),
|
|
3985
|
+
fillColorHex: options.pageBackgroundColorHex
|
|
3986
|
+
}];
|
|
3987
|
+
return [];
|
|
3988
|
+
};
|
|
3989
|
+
pageBorderItems = () => {
|
|
3990
|
+
const pg = this.ctx.properties.pageBorders;
|
|
3991
|
+
if (!pg) return [];
|
|
3992
|
+
const H = this.ctx.pageHeight;
|
|
3993
|
+
const W = this.ctx.pageWidth;
|
|
3994
|
+
const out = [];
|
|
3995
|
+
for (const side of [
|
|
3996
|
+
"top",
|
|
3997
|
+
"bottom",
|
|
3998
|
+
"left",
|
|
3999
|
+
"right"
|
|
4000
|
+
]) {
|
|
4001
|
+
const edge = pg.borders[side];
|
|
4002
|
+
if (!edge || edge.style === "none") continue;
|
|
4003
|
+
const gap = edge.spacePt ?? 0;
|
|
4004
|
+
const x0 = pg.offsetFrom === "page" ? gap : this.ctx.marginLeft - gap;
|
|
4005
|
+
const x1 = pg.offsetFrom === "page" ? W - gap : W - this.ctx.marginLeft + gap;
|
|
4006
|
+
const yTop = pg.offsetFrom === "page" ? gap : this.ctx.marginTop - gap;
|
|
4007
|
+
const yBottom = pg.offsetFrom === "page" ? H - gap : H - this.ctx.marginBottom + gap;
|
|
4008
|
+
out.push({
|
|
4009
|
+
type: "border",
|
|
4010
|
+
side,
|
|
4011
|
+
x: pt(Math.max(0, x0)),
|
|
4012
|
+
y: pt(Math.max(0, yTop)),
|
|
4013
|
+
width: pt(Math.max(0, x1 - x0)),
|
|
4014
|
+
height: pt(Math.max(0, yBottom - yTop)),
|
|
4015
|
+
borderSizePt: edge.width ?? DEFAULT_BORDER_SIZE_EIGHTH * EIGHTH_PT,
|
|
4016
|
+
borderColorHex: edge.colorHex ?? "000000",
|
|
4017
|
+
...edge.style !== "single" ? { borderStyle: edge.style } : {}
|
|
4018
|
+
});
|
|
4019
|
+
}
|
|
4020
|
+
return out;
|
|
4021
|
+
};
|
|
4022
|
+
/**
|
|
4023
|
+
* Throw the in-progress page away and reset for the next one.
|
|
4024
|
+
*
|
|
4025
|
+
* A section boundary reached with a page that draws nothing is the one place
|
|
4026
|
+
* this is wanted: {@link flushPage} keeps such a page whenever the cursor
|
|
4027
|
+
* moved at all, which is right in the middle of a flow and wrong at a
|
|
4028
|
+
* boundary, where the space consumed was the section break itself.
|
|
4029
|
+
*/
|
|
4030
|
+
dropPage = () => {
|
|
4031
|
+
this.current = [];
|
|
4032
|
+
this.floatsBehind = [];
|
|
4033
|
+
this.floatsFront = [];
|
|
4034
|
+
this.exclusions = [];
|
|
4035
|
+
this.pageNotes = [];
|
|
4036
|
+
this.noteReserve = 0;
|
|
4037
|
+
this.colIdx = 0;
|
|
4038
|
+
this.colStartLen = 0;
|
|
4039
|
+
this.cursorY = this.ctx.pageHeight - this.ctx.marginTop;
|
|
4040
|
+
this.colStartY = this.cursorY;
|
|
4041
|
+
this.bandTopY = this.cursorY;
|
|
4042
|
+
this.balanceBottomY = void 0;
|
|
4043
|
+
this.balanceHeightPt = 0;
|
|
2157
4044
|
};
|
|
2158
4045
|
};
|
|
2159
|
-
function paginateSections(blocks, sectionCtxs, builder, defaultLang = "en-US", notes, bookmarkPositions, reflowParagraph) {
|
|
4046
|
+
function paginateSections(blocks, sectionCtxs, builder, defaultLang = "en-US", notes, bookmarkPositions, reflowParagraph, backgroundImageName) {
|
|
2160
4047
|
if (sectionCtxs.length === 0) return [];
|
|
2161
|
-
const asm = new PageAssembler(sectionCtxs, builder, notes, bookmarkPositions);
|
|
4048
|
+
const asm = new PageAssembler(sectionCtxs, builder, notes, bookmarkPositions, backgroundImageName);
|
|
4049
|
+
const balanceIfEndsContinuous = (fromBlock) => {
|
|
4050
|
+
const here = asm.ctx;
|
|
4051
|
+
const next = sectionCtxs[asm.secIdx + 1];
|
|
4052
|
+
if ((here.columns?.length ?? 1) < 2) return;
|
|
4053
|
+
if (!next?.continuous) return;
|
|
4054
|
+
if (next.pageWidth !== here.pageWidth || next.pageHeight !== here.pageHeight) return;
|
|
4055
|
+
const end = Math.min(here.endIndex, blocks.length);
|
|
4056
|
+
asm.beginBalancedBand(blocksHeight(blocks.slice(fromBlock, end)));
|
|
4057
|
+
};
|
|
4058
|
+
balanceIfEndsContinuous(0);
|
|
2162
4059
|
for (let blockIdx = 0; blockIdx < blocks.length; blockIdx++) {
|
|
2163
4060
|
while (asm.secIdx < sectionCtxs.length - 1 && blockIdx >= asm.ctx.endIndex) {
|
|
2164
|
-
asm.
|
|
4061
|
+
const next = sectionCtxs[asm.secIdx + 1];
|
|
4062
|
+
if (next.continuous && next.pageWidth === asm.ctx.pageWidth && next.pageHeight === asm.ctx.pageHeight) {
|
|
4063
|
+
const end = Math.min(next.endIndex, blocks.length);
|
|
4064
|
+
asm.startBandSection(next, blocksHeight(blocks.slice(blockIdx, end)));
|
|
4065
|
+
continue;
|
|
4066
|
+
}
|
|
4067
|
+
if (asm.pageHasContent() || asm.pageInSection === 0) asm.flushPage(true);
|
|
4068
|
+
else asm.dropPage();
|
|
2165
4069
|
asm.secIdx++;
|
|
2166
4070
|
asm.ctx = sectionCtxs[asm.secIdx];
|
|
4071
|
+
asm.restartPageNumbers(asm.ctx);
|
|
2167
4072
|
asm.pageInSection = 0;
|
|
2168
4073
|
asm.cursorY = asm.ctx.pageHeight - asm.ctx.marginTop;
|
|
4074
|
+
balanceIfEndsContinuous(blockIdx);
|
|
2169
4075
|
}
|
|
2170
4076
|
const block = blocks[blockIdx];
|
|
2171
4077
|
if (asm.pendingPageBreak) {
|
|
2172
4078
|
asm.pendingPageBreak = false;
|
|
2173
|
-
|
|
4079
|
+
asm.flushPage(true);
|
|
2174
4080
|
}
|
|
2175
4081
|
if (builder && !(block.kind === "paragraph" && block.list)) asm.listStack.length = 0;
|
|
2176
4082
|
if (block.kind === "paragraph") {
|
|
2177
|
-
if (block.resolved.pageBreakBefore && asm.
|
|
4083
|
+
if ((block.resolved.pageBreakBefore || block.pageBreakBefore) && asm.pageHasContent()) asm.flushPage();
|
|
2178
4084
|
asm.cursorY -= block.spacingBeforePt;
|
|
2179
4085
|
let pb = block;
|
|
2180
4086
|
if (reflowParagraph && pb.source && asm.exclusions.length > 0) {
|
|
2181
4087
|
const widths = asm.lineWidthsFor(block, asm.cursorY);
|
|
2182
4088
|
if (widths) pb = reflowParagraph(pb.source, asm.colWidth(), widths);
|
|
2183
4089
|
}
|
|
4090
|
+
const borderTopY = asm.cursorY;
|
|
4091
|
+
const borderPage = asm.pages.length;
|
|
2184
4092
|
let structId;
|
|
2185
4093
|
let markerLblId;
|
|
2186
4094
|
const firstLine = pb.lines[0];
|
|
@@ -2195,7 +4103,8 @@ function paginateSections(blocks, sectionCtxs, builder, defaultLang = "en-US", n
|
|
|
2195
4103
|
if (lang && lang !== defaultLang) builder.node(structId).lang = lang;
|
|
2196
4104
|
}
|
|
2197
4105
|
let firstLineOfBlock = true;
|
|
2198
|
-
for (const line of pb.lines) {
|
|
4106
|
+
for (const [lineIdx, line] of pb.lines.entries()) {
|
|
4107
|
+
if (pb.columnBreakLines?.has(lineIdx) && asm.colHasContent()) asm.advanceColumn();
|
|
2199
4108
|
const h = computeLineHeight(line, pb.resolved);
|
|
2200
4109
|
let newNotes = asm.lineFootnotes(line);
|
|
2201
4110
|
const addedReserve = (sub) => sub.reduce((sum, x) => sum + x.heightPt, 0) + (asm.pageNotes.length === 0 && sub.length > 0 ? FOOTNOTE_SEPARATOR_HEIGHT : 0);
|
|
@@ -2223,10 +4132,15 @@ function paginateSections(blocks, sectionCtxs, builder, defaultLang = "en-US", n
|
|
|
2223
4132
|
});
|
|
2224
4133
|
}
|
|
2225
4134
|
}
|
|
4135
|
+
for (let guard = 0; guard < 8 && asm.exclusions.length > 0; guard++) {
|
|
4136
|
+
const blocked = asm.lineGeometryAt(asm.cursorY, h).blockedUntilYUp;
|
|
4137
|
+
if (blocked === void 0 || blocked >= asm.cursorY) break;
|
|
4138
|
+
asm.cursorY = blocked;
|
|
4139
|
+
}
|
|
2226
4140
|
asm.cursorY -= h;
|
|
2227
4141
|
const indentLeft = pb.resolved.indentLeft + (line.firstLine ? pb.resolved.indentFirstLine : 0);
|
|
2228
4142
|
const offset = alignmentOffset(pb.resolved.alignment, line.contentWidthPt, line.availableWidthPt);
|
|
2229
|
-
const baselineY = pt(asm.ctx.pageHeight - (asm.cursorY +
|
|
4143
|
+
const baselineY = pt(asm.ctx.pageHeight - (asm.cursorY + lineBaselineOffset(line, pb.resolved)));
|
|
2230
4144
|
const exclusionShift = asm.exclusions.length > 0 ? asm.lineGeometryAt(asm.cursorY + h, h).xOffset : 0;
|
|
2231
4145
|
const originX = asm.colLeft() + indentLeft + offset + exclusionShift;
|
|
2232
4146
|
if (markerLblId !== void 0 && line === firstLine) {
|
|
@@ -2266,86 +4180,53 @@ function paginateSections(blocks, sectionCtxs, builder, defaultLang = "en-US", n
|
|
|
2266
4180
|
baselineY,
|
|
2267
4181
|
...structId !== void 0 ? { structId } : {}
|
|
2268
4182
|
});
|
|
4183
|
+
asm.emitLineNumber(baselineY, line);
|
|
2269
4184
|
}
|
|
4185
|
+
if (asm.pages.length === borderPage) asm.current.push(...paragraphDecoration(pb.resolved, asm.colLeft() + pb.resolved.indentLeft, asm.colLeft() + asm.colWidth() - pb.resolved.indentRight, borderTopY, asm.cursorY, asm.ctx.pageHeight));
|
|
2270
4186
|
asm.cursorY -= pb.spacingAfterPt;
|
|
2271
4187
|
if (pb.pageBreakAfter) asm.pendingPageBreak = true;
|
|
2272
4188
|
} else if (block.kind === "image") {
|
|
2273
4189
|
const figId = builder ? createFigure(builder, block.altText, "Image") : void 0;
|
|
2274
4190
|
const emitImageAt = (x, topYUp, sink) => {
|
|
4191
|
+
const ins = block.drawInset;
|
|
2275
4192
|
sink.push({
|
|
2276
4193
|
type: "image",
|
|
2277
|
-
x: pt(x),
|
|
2278
|
-
y: pt(asm.ctx.pageHeight - topYUp),
|
|
2279
|
-
width: pt(block.widthPt),
|
|
2280
|
-
height: pt(block.heightPt),
|
|
4194
|
+
x: pt(x + (ins?.dxPt ?? 0)),
|
|
4195
|
+
y: pt(asm.ctx.pageHeight - (topYUp - (ins?.dyTopPt ?? 0))),
|
|
4196
|
+
width: pt(ins?.widthPt ?? block.widthPt),
|
|
4197
|
+
height: pt(ins?.heightPt ?? block.heightPt),
|
|
2281
4198
|
imageResourceName: block.resourceName,
|
|
4199
|
+
...block.crop ? { crop: block.crop } : {},
|
|
4200
|
+
...block.rotationDeg ? { rotationDeg: block.rotationDeg } : {},
|
|
4201
|
+
...block.flipH ? { flipH: true } : {},
|
|
4202
|
+
...block.flipV ? { flipV: true } : {},
|
|
4203
|
+
...block.flipH ? { flipH: true } : {},
|
|
4204
|
+
...block.flipV ? { flipV: true } : {},
|
|
2282
4205
|
...figId !== void 0 ? { structId: figId } : {}
|
|
2283
4206
|
});
|
|
4207
|
+
if (block.outline) sink.push(...pictureOutlineItems(block.outline, x + (ins?.dxPt ?? 0), asm.ctx.pageHeight - (topYUp - (ins?.dyTopPt ?? 0)), ins?.widthPt ?? block.widthPt, ins?.heightPt ?? block.heightPt, figId));
|
|
2284
4208
|
};
|
|
2285
4209
|
if (isOutOfFlowFloat(block.float)) {
|
|
2286
4210
|
const fx = asm.floatX(block.float, block.widthPt);
|
|
2287
|
-
const fy = asm.floatTopYUp(block.float);
|
|
2288
|
-
emitImageAt(fx, fy, block.float.behind ? asm.floatsBehind : asm.floatsFront);
|
|
2289
|
-
if (block.float.wrap !== "none") asm.
|
|
2290
|
-
x0: fx,
|
|
2291
|
-
x1: fx + block.widthPt,
|
|
2292
|
-
topYUp: fy,
|
|
2293
|
-
bottomYUp: fy - block.heightPt
|
|
2294
|
-
});
|
|
4211
|
+
const fy = asm.floatTopYUp(block.float, block.heightPt);
|
|
4212
|
+
emitImageAt(fx, fy, PageAssembler.zSink(block.float.behind ? asm.floatsBehind : asm.floatsFront, block.float.zOrder));
|
|
4213
|
+
if (block.float.wrap !== "none") asm.excludeFloat(block.float, fx, fy, block.widthPt, block.heightPt);
|
|
2295
4214
|
} else {
|
|
2296
4215
|
asm.cursorY -= block.spacingBeforePt;
|
|
2297
4216
|
if (asm.cursorY - block.heightPt < asm.bottomLimit() && asm.colHasContent()) asm.advanceColumn();
|
|
2298
4217
|
asm.cursorY -= block.heightPt;
|
|
2299
|
-
|
|
4218
|
+
const offset = alignmentOffset(block.resolvedAlignment, block.widthPt, asm.colWidth());
|
|
4219
|
+
emitImageAt(asm.colLeft() + offset, asm.cursorY + block.heightPt, asm.current);
|
|
2300
4220
|
asm.cursorY -= block.spacingAfterPt;
|
|
2301
4221
|
}
|
|
2302
4222
|
} else if (block.kind === "shape") {
|
|
2303
4223
|
const figId = builder ? createFigure(builder, block.altText, "Shape") : void 0;
|
|
2304
|
-
const emitShapeAt = (x, bottomYUp, sink) =>
|
|
2305
|
-
const transform = flipTransform(buildShapeTransform(x, bottomYUp, block.widthPt, block.heightPt, block.rotation60k, block.flipH, block.flipV), asm.ctx.pageHeight);
|
|
2306
|
-
sink.push({
|
|
2307
|
-
type: "shape",
|
|
2308
|
-
shape: {
|
|
2309
|
-
paths: block.paths,
|
|
2310
|
-
...block.fillColorHex ? { fillColorHex: block.fillColorHex } : {},
|
|
2311
|
-
...block.fillGradient ? { fillGradient: block.fillGradient } : {},
|
|
2312
|
-
...block.stroke ? { stroke: block.stroke } : {},
|
|
2313
|
-
transform
|
|
2314
|
-
},
|
|
2315
|
-
...figId !== void 0 ? { structId: figId } : {}
|
|
2316
|
-
});
|
|
2317
|
-
if (block.textLines.length > 0) {
|
|
2318
|
-
const shapeBottom = bottomYUp;
|
|
2319
|
-
const shapeTop = bottomYUp + block.heightPt;
|
|
2320
|
-
const innerWidth = Math.max(1, block.widthPt - block.insetLeftPt - block.insetRightPt);
|
|
2321
|
-
let textY;
|
|
2322
|
-
if (block.anchor === "b") textY = shapeBottom + block.insetBottomPt + block.textHeightPt;
|
|
2323
|
-
else if (block.anchor === "ctr") textY = shapeBottom + (block.heightPt + block.textHeightPt) / 2;
|
|
2324
|
-
else textY = shapeTop - block.insetTopPt;
|
|
2325
|
-
for (const line of block.textLines) {
|
|
2326
|
-
const h = computeLineHeight(line, line.resolved);
|
|
2327
|
-
textY -= h;
|
|
2328
|
-
const lineOffset = alignmentOffset(line.resolved.alignment, line.contentWidthPt, innerWidth);
|
|
2329
|
-
sink.push({
|
|
2330
|
-
type: "line",
|
|
2331
|
-
line,
|
|
2332
|
-
originX: pt(x + block.insetLeftPt + lineOffset),
|
|
2333
|
-
baselineY: pt(asm.ctx.pageHeight - (textY + lineDescent(line))),
|
|
2334
|
-
...figId !== void 0 ? { structId: figId } : {}
|
|
2335
|
-
});
|
|
2336
|
-
}
|
|
2337
|
-
}
|
|
2338
|
-
};
|
|
4224
|
+
const emitShapeAt = (x, bottomYUp, sink) => emitShapeItems(block, x, bottomYUp, sink, asm.ctx.pageHeight, figId);
|
|
2339
4225
|
if (isOutOfFlowFloat(block.float)) {
|
|
2340
4226
|
const fx = asm.floatX(block.float, block.widthPt);
|
|
2341
|
-
const fy = asm.floatTopYUp(block.float);
|
|
2342
|
-
emitShapeAt(fx, fy - block.heightPt, block.float.behind ? asm.floatsBehind : asm.floatsFront);
|
|
2343
|
-
if (block.float.wrap !== "none") asm.
|
|
2344
|
-
x0: fx,
|
|
2345
|
-
x1: fx + block.widthPt,
|
|
2346
|
-
topYUp: fy,
|
|
2347
|
-
bottomYUp: fy - block.heightPt
|
|
2348
|
-
});
|
|
4227
|
+
const fy = asm.floatTopYUp(block.float, block.heightPt);
|
|
4228
|
+
emitShapeAt(fx, fy - block.heightPt, PageAssembler.zSink(block.float.behind ? asm.floatsBehind : asm.floatsFront, block.float.zOrder));
|
|
4229
|
+
if (block.float.wrap !== "none") asm.excludeFloat(block.float, fx, fy, block.widthPt, block.heightPt);
|
|
2349
4230
|
} else {
|
|
2350
4231
|
asm.cursorY -= block.spacingBeforePt;
|
|
2351
4232
|
if (asm.cursorY - block.heightPt < asm.bottomLimit() && asm.colHasContent()) asm.advanceColumn();
|
|
@@ -2356,43 +4237,14 @@ function paginateSections(blocks, sectionCtxs, builder, defaultLang = "en-US", n
|
|
|
2356
4237
|
}
|
|
2357
4238
|
} else if (block.kind === "chart") {
|
|
2358
4239
|
const figId = builder ? createFigure(builder, block.altText, "Chart") : void 0;
|
|
2359
|
-
const fig = figId !== void 0 ? { structId: figId } : {};
|
|
2360
4240
|
const emitChartAt = (x, bottomYUp, sink) => {
|
|
2361
|
-
|
|
2362
|
-
type: "shape",
|
|
2363
|
-
shape: {
|
|
2364
|
-
paths: s.paths,
|
|
2365
|
-
...s.fillColorHex ? { fillColorHex: s.fillColorHex } : {},
|
|
2366
|
-
...s.stroke ? { stroke: s.stroke } : {},
|
|
2367
|
-
transform: flipTransform([
|
|
2368
|
-
1,
|
|
2369
|
-
0,
|
|
2370
|
-
0,
|
|
2371
|
-
1,
|
|
2372
|
-
x,
|
|
2373
|
-
bottomYUp
|
|
2374
|
-
], asm.ctx.pageHeight)
|
|
2375
|
-
},
|
|
2376
|
-
...fig
|
|
2377
|
-
});
|
|
2378
|
-
for (const t of block.layout.texts) sink.push({
|
|
2379
|
-
type: "line",
|
|
2380
|
-
line: t.line,
|
|
2381
|
-
originX: pt(x + t.x),
|
|
2382
|
-
baselineY: pt(asm.ctx.pageHeight - (bottomYUp + t.y)),
|
|
2383
|
-
...fig
|
|
2384
|
-
});
|
|
4241
|
+
sink.push(...chartPageItems(block, x, bottomYUp, asm.ctx.pageHeight, figId));
|
|
2385
4242
|
};
|
|
2386
4243
|
if (isOutOfFlowFloat(block.float)) {
|
|
2387
4244
|
const fx = asm.floatX(block.float, block.widthPt);
|
|
2388
|
-
const fy = asm.floatTopYUp(block.float);
|
|
2389
|
-
emitChartAt(fx, fy - block.heightPt, block.float.behind ? asm.floatsBehind : asm.floatsFront);
|
|
2390
|
-
if (block.float.wrap !== "none") asm.
|
|
2391
|
-
x0: fx,
|
|
2392
|
-
x1: fx + block.widthPt,
|
|
2393
|
-
topYUp: fy,
|
|
2394
|
-
bottomYUp: fy - block.heightPt
|
|
2395
|
-
});
|
|
4245
|
+
const fy = asm.floatTopYUp(block.float, block.heightPt);
|
|
4246
|
+
emitChartAt(fx, fy - block.heightPt, PageAssembler.zSink(block.float.behind ? asm.floatsBehind : asm.floatsFront, block.float.zOrder));
|
|
4247
|
+
if (block.float.wrap !== "none") asm.excludeFloat(block.float, fx, fy, block.widthPt, block.heightPt);
|
|
2396
4248
|
} else {
|
|
2397
4249
|
asm.cursorY -= block.spacingBeforePt;
|
|
2398
4250
|
if (asm.cursorY - block.heightPt < asm.bottomLimit() && asm.colHasContent()) asm.advanceColumn();
|
|
@@ -2403,6 +4255,19 @@ function paginateSections(blocks, sectionCtxs, builder, defaultLang = "en-US", n
|
|
|
2403
4255
|
}
|
|
2404
4256
|
} else {
|
|
2405
4257
|
const colCount = block.rows.reduce((max, r) => Math.max(max, r.cells.reduce((s, c) => Math.max(s, c.colStart + c.colSpan), 0)), 0);
|
|
4258
|
+
const anchorY = isOutOfFlowFloat(block.float) ? asm.floatTopYUp(block.float, block.heightPt) : 0;
|
|
4259
|
+
if (isOutOfFlowFloat(block.float) && anchorY - block.heightPt >= asm.ctx.marginBottom) {
|
|
4260
|
+
const fx = asm.floatX(block.float, block.totalWidthPt);
|
|
4261
|
+
const fy = anchorY;
|
|
4262
|
+
const sink = PageAssembler.zSink(block.float.behind ? asm.floatsBehind : asm.floatsFront, block.float.zOrder);
|
|
4263
|
+
let rowY = fy;
|
|
4264
|
+
for (const row of block.rows) {
|
|
4265
|
+
emitRowChunk(sink, row, fx, rowY, asm.ctx.pageHeight, colCount);
|
|
4266
|
+
rowY -= row.heightPt;
|
|
4267
|
+
}
|
|
4268
|
+
if (block.float.wrap !== "none") asm.excludeFloat(block.float, fx, fy, block.totalWidthPt, block.heightPt);
|
|
4269
|
+
continue;
|
|
4270
|
+
}
|
|
2406
4271
|
const headerRows = [];
|
|
2407
4272
|
for (const r of block.rows) if (r.isHeader) headerRows.push(r);
|
|
2408
4273
|
else break;
|
|
@@ -2439,8 +4304,16 @@ function paginateSections(blocks, sectionCtxs, builder, defaultLang = "en-US", n
|
|
|
2439
4304
|
asm.cursorY -= hr.heightPt;
|
|
2440
4305
|
}
|
|
2441
4306
|
}
|
|
2442
|
-
|
|
4307
|
+
const mergedHeights = chunks.length === 1 && row.cells.some((c) => c.mergeRole === "start") ? row.cells.map((c) => c.mergeRole === "start" ? mergedBoxHeightPt(block.rows, ri, c.colStart, asm.cursorY - asm.bottomLimit()) : void 0) : void 0;
|
|
4308
|
+
emitRowChunk(asm.current, chunk, tableX, asm.cursorY, asm.ctx.pageHeight, colCount, cellStructIds, mergedHeights);
|
|
2443
4309
|
asm.cursorY -= chunk.heightPt;
|
|
4310
|
+
if (ci === chunks.length - 1 && ri < block.rows.length - 1) {
|
|
4311
|
+
const next = block.rows[ri + 1];
|
|
4312
|
+
const nextHeight = Math.min(next.heightPt, asm.ctx.pageContentHeight);
|
|
4313
|
+
const nextForced = next.breakBefore && ri + 1 >= headerRows.length && asm.current.length > 0;
|
|
4314
|
+
const nextOverflow = asm.cursorY - nextHeight < asm.bottomLimit() && asm.colHasContent();
|
|
4315
|
+
if (nextForced || nextOverflow) emitRowBottomEdge(asm.current, chunk, tableX, asm.cursorY, asm.ctx.pageHeight);
|
|
4316
|
+
}
|
|
2444
4317
|
}
|
|
2445
4318
|
}
|
|
2446
4319
|
}
|
|
@@ -2453,8 +4326,44 @@ function paginateSections(blocks, sectionCtxs, builder, defaultLang = "en-US", n
|
|
|
2453
4326
|
if (d.position === "header") page.commands.unshift(...cmds);
|
|
2454
4327
|
else page.commands.push(...cmds);
|
|
2455
4328
|
}
|
|
4329
|
+
for (const page of asm.pages) coalesceVerticalFills(page.commands);
|
|
2456
4330
|
return asm.pages;
|
|
2457
4331
|
}
|
|
4332
|
+
function coalesceVerticalFills(commands) {
|
|
4333
|
+
const touching = (a, b) => Math.abs(a.x - b.x) < .01 && Math.abs(a.width - b.width) < .01 && a.fillColorHex === b.fillColorHex && Math.min(a.y + a.height, b.y + b.height) >= Math.max(a.y, b.y) - .01;
|
|
4334
|
+
const open = /* @__PURE__ */ new Map();
|
|
4335
|
+
const dead = /* @__PURE__ */ new Set();
|
|
4336
|
+
for (const item of commands) {
|
|
4337
|
+
if (item.type !== "fill") continue;
|
|
4338
|
+
const fill = item;
|
|
4339
|
+
const k = `${fill.x.toFixed(2)}|${fill.width.toFixed(2)}|${fill.fillColorHex}`;
|
|
4340
|
+
const prev = open.get(k);
|
|
4341
|
+
if (prev && touching(prev, fill)) {
|
|
4342
|
+
const top = Math.max(prev.y + prev.height, fill.y + fill.height);
|
|
4343
|
+
const bottom = Math.min(prev.y, fill.y);
|
|
4344
|
+
prev.y = pt(bottom);
|
|
4345
|
+
prev.height = pt(top - bottom);
|
|
4346
|
+
dead.add(item);
|
|
4347
|
+
continue;
|
|
4348
|
+
}
|
|
4349
|
+
open.set(k, fill);
|
|
4350
|
+
}
|
|
4351
|
+
if (dead.size === 0) return;
|
|
4352
|
+
const kept = commands.filter((c) => !dead.has(c));
|
|
4353
|
+
commands.length = 0;
|
|
4354
|
+
commands.push(...kept);
|
|
4355
|
+
}
|
|
4356
|
+
function mergedBoxHeightPt(rows, startRowIdx, colStart, availablePt) {
|
|
4357
|
+
let height = rows[startRowIdx].heightPt;
|
|
4358
|
+
for (let r = startRowIdx + 1; r < rows.length; r++) {
|
|
4359
|
+
const cont = rows[r].cells.find((c) => c.colStart === colStart && (c.mergeRole === "middle" || c.mergeRole === "end"));
|
|
4360
|
+
if (!cont) break;
|
|
4361
|
+
if (height + rows[r].heightPt > availablePt) break;
|
|
4362
|
+
height += rows[r].heightPt;
|
|
4363
|
+
if (cont.mergeRole === "end") break;
|
|
4364
|
+
}
|
|
4365
|
+
return height;
|
|
4366
|
+
}
|
|
2458
4367
|
function tableCellRowSpan(rows, startRowIdx, colStart) {
|
|
2459
4368
|
let span = 1;
|
|
2460
4369
|
for (let r = startRowIdx + 1; r < rows.length; r++) {
|
|
@@ -2465,32 +4374,74 @@ function tableCellRowSpan(rows, startRowIdx, colStart) {
|
|
|
2465
4374
|
}
|
|
2466
4375
|
return span;
|
|
2467
4376
|
}
|
|
2468
|
-
function emitRowChunk(out, row, marginLeft, cursorY, pageHeight, colCount, cellStructIds) {
|
|
4377
|
+
function emitRowChunk(out, row, marginLeft, cursorY, pageHeight, colCount, cellStructIds, mergedHeights) {
|
|
2469
4378
|
const rowTop = cursorY;
|
|
2470
4379
|
const rowBottom = cursorY - row.heightPt;
|
|
4380
|
+
{
|
|
4381
|
+
let i = 0;
|
|
4382
|
+
while (i < row.cells.length) {
|
|
4383
|
+
const cell = row.cells[i];
|
|
4384
|
+
if (!cell.shadingColorHex) {
|
|
4385
|
+
i++;
|
|
4386
|
+
continue;
|
|
4387
|
+
}
|
|
4388
|
+
let width = cell.paintWidthPt ?? cell.widthPt;
|
|
4389
|
+
let j = i + 1;
|
|
4390
|
+
while (j < row.cells.length && cell.paintWidthPt === void 0) {
|
|
4391
|
+
const next = row.cells[j];
|
|
4392
|
+
if (next.mergeRole === "middle" || next.mergeRole === "end") break;
|
|
4393
|
+
if (next.shadingColorHex !== cell.shadingColorHex) break;
|
|
4394
|
+
width += next.paintWidthPt ?? next.widthPt;
|
|
4395
|
+
j++;
|
|
4396
|
+
}
|
|
4397
|
+
out.push({
|
|
4398
|
+
type: "fill",
|
|
4399
|
+
x: pt(marginLeft + (row.columnXOffsets[i] ?? 0)),
|
|
4400
|
+
y: pt(pageHeight - rowBottom - row.heightPt - FILL_SEAM_PT),
|
|
4401
|
+
width: pt(width),
|
|
4402
|
+
height: pt(row.heightPt + FILL_SEAM_PT),
|
|
4403
|
+
fillColorHex: cell.shadingColorHex
|
|
4404
|
+
});
|
|
4405
|
+
i = j;
|
|
4406
|
+
}
|
|
4407
|
+
}
|
|
2471
4408
|
for (let i = 0; i < row.cells.length; i++) {
|
|
2472
4409
|
const cell = row.cells[i];
|
|
2473
4410
|
const cellX = marginLeft + (row.columnXOffsets[i] ?? 0);
|
|
2474
4411
|
const structId = cellStructIds?.[i];
|
|
2475
|
-
if (cell.shadingColorHex && cell.mergeRole !== "middle" && cell.mergeRole !== "end") out.push({
|
|
2476
|
-
type: "fill",
|
|
2477
|
-
x: pt(cellX),
|
|
2478
|
-
y: pt(pageHeight - rowBottom - row.heightPt),
|
|
2479
|
-
width: pt(cell.widthPt),
|
|
2480
|
-
height: pt(row.heightPt),
|
|
2481
|
-
fillColorHex: cell.shadingColorHex
|
|
2482
|
-
});
|
|
2483
4412
|
if (cell.dataBar && cell.mergeRole !== "middle" && cell.mergeRole !== "end") {
|
|
2484
4413
|
const start = Math.max(0, Math.min(1, cell.dataBar.startFraction ?? 0));
|
|
2485
4414
|
const barWidth = cell.widthPt * Math.max(0, Math.min(1, cell.dataBar.fraction));
|
|
2486
|
-
if (barWidth > 0)
|
|
2487
|
-
|
|
2488
|
-
|
|
2489
|
-
|
|
2490
|
-
|
|
2491
|
-
|
|
2492
|
-
|
|
2493
|
-
|
|
4415
|
+
if (barWidth > 0) {
|
|
4416
|
+
const barX = cellX + cell.widthPt * start;
|
|
4417
|
+
const barBottomYUp = rowBottom;
|
|
4418
|
+
out.push({
|
|
4419
|
+
type: "shape",
|
|
4420
|
+
shape: {
|
|
4421
|
+
paths: [rectAtPath(0, 0, barWidth, row.heightPt)],
|
|
4422
|
+
fillColorHex: cell.dataBar.colorHex,
|
|
4423
|
+
fillGradient: {
|
|
4424
|
+
kind: "linear",
|
|
4425
|
+
angle: cell.dataBar.negative ? 180 : 0,
|
|
4426
|
+
stops: [{
|
|
4427
|
+
offset: 0,
|
|
4428
|
+
colorHex: cell.dataBar.colorHex
|
|
4429
|
+
}, {
|
|
4430
|
+
offset: 1,
|
|
4431
|
+
colorHex: "FFFFFF"
|
|
4432
|
+
}]
|
|
4433
|
+
},
|
|
4434
|
+
transform: flipTransform([
|
|
4435
|
+
1,
|
|
4436
|
+
0,
|
|
4437
|
+
0,
|
|
4438
|
+
1,
|
|
4439
|
+
barX,
|
|
4440
|
+
barBottomYUp
|
|
4441
|
+
], pageHeight)
|
|
4442
|
+
}
|
|
4443
|
+
});
|
|
4444
|
+
}
|
|
2494
4445
|
}
|
|
2495
4446
|
if (cell.icon && cell.mergeRole !== "middle" && cell.mergeRole !== "end") {
|
|
2496
4447
|
const iconSize = Math.min(CF_ICON_SIZE_PT, Math.max(0, row.heightPt - 1));
|
|
@@ -2592,27 +4543,49 @@ function emitRowChunk(out, row, marginLeft, cursorY, pageHeight, colCount, cellS
|
|
|
2592
4543
|
if (diagUp) pushDiagonal(diagUp, 0, h);
|
|
2593
4544
|
}
|
|
2594
4545
|
if (cell.mergeRole === "middle" || cell.mergeRole === "end") continue;
|
|
2595
|
-
|
|
2596
|
-
|
|
4546
|
+
const boxHeightPt = mergedHeights?.[i] ?? row.heightPt;
|
|
4547
|
+
const slack = cell.verticalAlign !== void 0 ? boxHeightPt - cell.padTopPt - cell.contentHeightPt - cell.padBottomPt : 0;
|
|
4548
|
+
const vOffset = slack <= 0 ? 0 : cell.verticalAlign === "bottom" ? slack : cell.verticalAlign === "center" ? slack / 2 : 0;
|
|
4549
|
+
let textY = rowTop - cell.padTopPt - vOffset;
|
|
4550
|
+
const nestedX = cellX + cell.padLeftPt;
|
|
4551
|
+
const drawNestedBefore = (n) => {
|
|
4552
|
+
for (const nt of cell.nestedTables ?? []) {
|
|
4553
|
+
if (nt.afterLine !== n) continue;
|
|
4554
|
+
for (const nrow of nt.block.rows) {
|
|
4555
|
+
const nestedIds = structId !== void 0 ? nrow.cells.map(() => structId) : void 0;
|
|
4556
|
+
emitRowChunk(out, nrow, nestedX, textY, pageHeight, nt.block.colCount, nestedIds);
|
|
4557
|
+
textY -= nrow.heightPt;
|
|
4558
|
+
}
|
|
4559
|
+
}
|
|
4560
|
+
for (const cs of cell.shapes ?? []) {
|
|
4561
|
+
if (cs.afterLine !== n) continue;
|
|
4562
|
+
const offset = alignmentOffset(cs.block.resolvedAlignment, cs.block.widthPt, cell.widthPt - cell.padLeftPt - cell.padRightPt);
|
|
4563
|
+
textY -= cs.block.heightPt;
|
|
4564
|
+
emitShapeItems(cs.block, nestedX + offset, textY, out, pageHeight, structId);
|
|
4565
|
+
}
|
|
4566
|
+
};
|
|
4567
|
+
for (const [lineIdx, line] of cell.lines.entries()) {
|
|
4568
|
+
drawNestedBefore(lineIdx);
|
|
4569
|
+
textY -= cell.lineGaps?.get(lineIdx) ?? 0;
|
|
2597
4570
|
const h = computeLineHeight(line, line.resolved);
|
|
2598
4571
|
textY -= h;
|
|
2599
4572
|
const offset = alignmentOffset(line.resolved.alignment, line.contentWidthPt, cell.widthPt - cell.padLeftPt - cell.padRightPt);
|
|
4573
|
+
const indentLeft = line.resolved.indentLeft + (line.firstLine ? line.resolved.indentFirstLine : 0);
|
|
2600
4574
|
out.push({
|
|
2601
4575
|
type: "line",
|
|
2602
4576
|
line,
|
|
2603
|
-
originX: pt(cellX + cell.padLeftPt + offset),
|
|
2604
|
-
baselineY: pt(pageHeight - (textY +
|
|
4577
|
+
originX: pt(cellX + cell.padLeftPt + indentLeft + offset),
|
|
4578
|
+
baselineY: pt(pageHeight - (textY + lineBaselineOffset(line, line.resolved))),
|
|
4579
|
+
...cell.clipped ? { clip: {
|
|
4580
|
+
x: pt(cellX),
|
|
4581
|
+
y: pt(pageHeight - rowTop),
|
|
4582
|
+
width: pt(cell.widthPt),
|
|
4583
|
+
height: pt(boxHeightPt)
|
|
4584
|
+
} } : {},
|
|
2605
4585
|
...structId !== void 0 ? { structId } : {}
|
|
2606
4586
|
});
|
|
2607
4587
|
}
|
|
2608
|
-
|
|
2609
|
-
const nestedX = cellX + cell.padLeftPt;
|
|
2610
|
-
for (const nt of cell.nestedTables) for (const nrow of nt.rows) {
|
|
2611
|
-
const nestedIds = structId !== void 0 ? nrow.cells.map(() => structId) : void 0;
|
|
2612
|
-
emitRowChunk(out, nrow, nestedX, textY, pageHeight, nt.colCount, nestedIds);
|
|
2613
|
-
textY -= nrow.heightPt;
|
|
2614
|
-
}
|
|
2615
|
-
}
|
|
4588
|
+
drawNestedBefore(cell.lines.length);
|
|
2616
4589
|
}
|
|
2617
4590
|
}
|
|
2618
4591
|
function splitRowIntoChunks(row, capacity) {
|
|
@@ -2697,6 +4670,7 @@ function splitRowIntoChunks(row, capacity) {
|
|
|
2697
4670
|
}
|
|
2698
4671
|
return out.length > 0 ? out : [row];
|
|
2699
4672
|
}
|
|
4673
|
+
var FILL_SEAM_PT = .07;
|
|
2700
4674
|
function emitCellBorders(out, cell, cellX, cellY, rowHeight, pageHeight, rowIdx, rowCount, colCount) {
|
|
2701
4675
|
const pushSide = (side, border) => {
|
|
2702
4676
|
if (!border || border.style === "none") return;
|
|
@@ -2706,10 +4680,11 @@ function emitCellBorders(out, cell, cellX, cellY, rowHeight, pageHeight, rowIdx,
|
|
|
2706
4680
|
side,
|
|
2707
4681
|
x: pt(cellX),
|
|
2708
4682
|
y: pt(pageHeight - cellY - rowHeight),
|
|
2709
|
-
width: pt(cell.widthPt),
|
|
4683
|
+
width: pt(cell.paintWidthPt ?? cell.widthPt),
|
|
2710
4684
|
height: pt(rowHeight),
|
|
2711
4685
|
borderSizePt: sz,
|
|
2712
|
-
borderColorHex: border.colorHex ?? "000000"
|
|
4686
|
+
borderColorHex: border.colorHex ?? "000000",
|
|
4687
|
+
...border.style !== "single" ? { borderStyle: border.style } : {}
|
|
2713
4688
|
});
|
|
2714
4689
|
};
|
|
2715
4690
|
pushSide("top", cell.borders.top);
|
|
@@ -2717,5 +4692,101 @@ function emitCellBorders(out, cell, cellX, cellY, rowHeight, pageHeight, rowIdx,
|
|
|
2717
4692
|
if (rowIdx === rowCount - 1) pushSide("bottom", cell.borders.bottom);
|
|
2718
4693
|
if (cell.colStart + cell.colSpan - 1 === colCount - 1) pushSide("right", cell.borders.right);
|
|
2719
4694
|
}
|
|
4695
|
+
var NO_WRAP_MEASURE_WIDTH = 1e6;
|
|
4696
|
+
/**
|
|
4697
|
+
* One line, trimmed to the glyph that STRADDLES `widthPt` — what a non-wrapping
|
|
4698
|
+
* spreadsheet cell shows. Excel and LibreOffice paint that glyph and cut it
|
|
4699
|
+
* through the middle, and so do we now: the caller marks the cell and the
|
|
4700
|
+
* emitters clip the line to its box (TextLineItem.clip). Stopping at the last
|
|
4701
|
+
* glyph that wholly fits is what dropped a character off every cut cell.
|
|
4702
|
+
*/
|
|
4703
|
+
function clipLineToWidth(lines, widthPt, hashOnOverflow = false) {
|
|
4704
|
+
const line = lines[0];
|
|
4705
|
+
if (!line) return [];
|
|
4706
|
+
if (line.contentWidthPt <= widthPt) return [line];
|
|
4707
|
+
if (hashOnOverflow) return [hashFill(line, widthPt)];
|
|
4708
|
+
const tokens = [];
|
|
4709
|
+
let used = 0;
|
|
4710
|
+
for (const token of line.tokens) {
|
|
4711
|
+
const advance = token.widthPt;
|
|
4712
|
+
if (used + advance <= widthPt) {
|
|
4713
|
+
tokens.push(token);
|
|
4714
|
+
used += advance;
|
|
4715
|
+
continue;
|
|
4716
|
+
}
|
|
4717
|
+
if (token.kind === "text") {
|
|
4718
|
+
const room = widthPt - used;
|
|
4719
|
+
let text = "";
|
|
4720
|
+
let textWidth = 0;
|
|
4721
|
+
for (const ch of token.text) {
|
|
4722
|
+
if (textWidth >= room) break;
|
|
4723
|
+
text += ch;
|
|
4724
|
+
textWidth = token.font.measure.textWidthPt(text, token.fontSizePt);
|
|
4725
|
+
}
|
|
4726
|
+
if (text.length > 0) {
|
|
4727
|
+
tokens.push({
|
|
4728
|
+
...token,
|
|
4729
|
+
text,
|
|
4730
|
+
widthPt: textWidth
|
|
4731
|
+
});
|
|
4732
|
+
used += textWidth;
|
|
4733
|
+
}
|
|
4734
|
+
}
|
|
4735
|
+
break;
|
|
4736
|
+
}
|
|
4737
|
+
return [{
|
|
4738
|
+
...line,
|
|
4739
|
+
tokens,
|
|
4740
|
+
contentWidthPt: used
|
|
4741
|
+
}];
|
|
4742
|
+
}
|
|
4743
|
+
/**
|
|
4744
|
+
* The line as a row of `#` filling `widthPt` — what Excel and LibreOffice put in
|
|
4745
|
+
* a cell whose number does not fit. At least one, so the cell is never blank:
|
|
4746
|
+
* a column too narrow even for a single `#` still has to say something.
|
|
4747
|
+
*/
|
|
4748
|
+
function hashFill(line, widthPt) {
|
|
4749
|
+
const first = line.tokens.find((t) => t.kind === "text");
|
|
4750
|
+
if (!first) return line;
|
|
4751
|
+
const one = first.font.measure.textWidthPt("#", first.fontSizePt);
|
|
4752
|
+
const count = one > 0 ? Math.max(1, Math.floor(widthPt / one)) : 1;
|
|
4753
|
+
const text = "#".repeat(count);
|
|
4754
|
+
const token = {
|
|
4755
|
+
...first,
|
|
4756
|
+
text,
|
|
4757
|
+
widthPt: first.font.measure.textWidthPt(text, first.fontSizePt)
|
|
4758
|
+
};
|
|
4759
|
+
return {
|
|
4760
|
+
...line,
|
|
4761
|
+
tokens: [token],
|
|
4762
|
+
contentWidthPt: token.widthPt
|
|
4763
|
+
};
|
|
4764
|
+
}
|
|
4765
|
+
/**
|
|
4766
|
+
* The bottom edge of a row that ends a page — the rule the row below it owed it
|
|
4767
|
+
* and drew on the next page instead. See the call site: a table that spills over
|
|
4768
|
+
* a break is closed on both sides of it, the way Excel and Word print one.
|
|
4769
|
+
*
|
|
4770
|
+
* A cell whose bottom is suppressed (a vertical merge continuing past the break)
|
|
4771
|
+
* stays open; inventing an edge there would cut the merge in half.
|
|
4772
|
+
*/
|
|
4773
|
+
function emitRowBottomEdge(out, row, marginLeft, rowBottom, pageHeight) {
|
|
4774
|
+
for (let i = 0; i < row.cells.length; i++) {
|
|
4775
|
+
const cell = row.cells[i];
|
|
4776
|
+
const border = cell.borders.bottom;
|
|
4777
|
+
if (!border || border.style === "none") continue;
|
|
4778
|
+
out.push({
|
|
4779
|
+
type: "border",
|
|
4780
|
+
side: "bottom",
|
|
4781
|
+
x: pt(marginLeft + (row.columnXOffsets[i] ?? 0)),
|
|
4782
|
+
y: pt(pageHeight - rowBottom - row.heightPt),
|
|
4783
|
+
width: pt(cell.widthPt),
|
|
4784
|
+
height: pt(row.heightPt),
|
|
4785
|
+
borderSizePt: border.width ?? DEFAULT_BORDER_SIZE_EIGHTH * EIGHTH_PT,
|
|
4786
|
+
borderColorHex: border.colorHex ?? "000000",
|
|
4787
|
+
...border.style !== "single" ? { borderStyle: border.style } : {}
|
|
4788
|
+
});
|
|
4789
|
+
}
|
|
4790
|
+
}
|
|
2720
4791
|
//#endregion
|
|
2721
|
-
export { layoutStyledDocument };
|
|
4792
|
+
export { GLUE_SHRINK_RATIO, layoutStyledDocument };
|