reamkit 1.5.0 → 1.6.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.
Files changed (39) hide show
  1. package/README.md +2 -2
  2. package/dist/esm/core/converter/facade.js +6 -1
  3. package/dist/esm/core/converter/ream.js +2 -1
  4. package/dist/esm/core/document-model/types.d.ts +9 -1
  5. package/dist/esm/core/spreadsheet-model/index.d.ts +1 -1
  6. package/dist/esm/core/spreadsheet-model/types.d.ts +5 -0
  7. package/dist/esm/excel/column-bands.d.ts +7 -0
  8. package/dist/esm/excel/column-bands.js +87 -0
  9. package/dist/esm/excel/conditional-format.js +22 -0
  10. package/dist/esm/excel/print-model.js +30 -11
  11. package/dist/esm/excel/worksheet-parser.js +26 -0
  12. package/dist/esm/excel/xlsx-writer.js +8 -1
  13. package/dist/esm/html/html-writer.js +100 -5
  14. package/dist/esm/layout/styled-layout.js +88 -19
  15. package/dist/esm/pdf-reader/cmap.d.ts +5 -0
  16. package/dist/esm/pdf-reader/cmap.js +72 -0
  17. package/dist/esm/pdf-reader/content.d.ts +14 -0
  18. package/dist/esm/pdf-reader/content.js +292 -0
  19. package/dist/esm/pdf-reader/document.d.ts +23 -0
  20. package/dist/esm/pdf-reader/document.js +230 -0
  21. package/dist/esm/pdf-reader/flow-build.d.ts +4 -0
  22. package/dist/esm/pdf-reader/flow-build.js +27 -0
  23. package/dist/esm/pdf-reader/font.d.ts +4 -0
  24. package/dist/esm/pdf-reader/font.js +69 -0
  25. package/dist/esm/pdf-reader/layout.d.ts +3 -0
  26. package/dist/esm/pdf-reader/layout.js +69 -0
  27. package/dist/esm/pdf-reader/lexer.d.ts +43 -0
  28. package/dist/esm/pdf-reader/lexer.js +250 -0
  29. package/dist/esm/pdf-reader/parser.d.ts +10 -0
  30. package/dist/esm/pdf-reader/parser.js +86 -0
  31. package/dist/esm/pdf-reader/reader.d.ts +4 -0
  32. package/dist/esm/pdf-reader/reader.js +43 -0
  33. package/dist/esm/pdf-reader/struct-tree.d.ts +14 -0
  34. package/dist/esm/pdf-reader/struct-tree.js +92 -0
  35. package/dist/esm/pdf-reader/tagged.d.ts +3 -0
  36. package/dist/esm/pdf-reader/tagged.js +88 -0
  37. package/dist/esm/pdf-reader/text.d.ts +3 -0
  38. package/dist/esm/pdf-reader/text.js +18 -0
  39. package/package.json +2 -2
package/README.md CHANGED
@@ -30,7 +30,7 @@ import { Ream } from 'reamkit';
30
30
  // e.g. from an <input type="file"> or a fetch() — anything that yields bytes.
31
31
  const bytes = new Uint8Array(await file.arrayBuffer());
32
32
 
33
- const doc = Ream.parse(bytes); // docx or xlsx — sniffed
33
+ const doc = Ream.parse(bytes); // docx, xlsx or pdf — sniffed
34
34
  const pdf = await doc.convert('pdf'); // async — fetches a font if needed
35
35
  const svg = await doc.convert('svg'); // same parse, different target
36
36
  const html = await doc.convert('html'); // flowed HTML — needs no fonts at all
@@ -130,7 +130,7 @@ rendering — inspect or analyze it without converting:
130
130
 
131
131
  ```ts
132
132
  const doc = Ream.parse(bytes);
133
- doc.format; // 'docx' | 'xlsx'
133
+ doc.format; // 'docx' | 'xlsx' | 'pdf'
134
134
  doc.flow.body; // paragraphs / tables / images / charts …
135
135
  doc.losses; // read-time losses
136
136
  ```
@@ -13,6 +13,7 @@ import { writeHtml } from "../../html/html-writer.js";
13
13
  import { writeSvg } from "../../svg/svg-writer.js";
14
14
  import { convertDocxToPdf } from "../../word/docx-to-pdf.js";
15
15
  import { convertXlsxToPdf } from "../../excel/xlsx-to-pdf.js";
16
+ import { pdfReader } from "../../pdf-reader/reader.js";
16
17
  //#region src/core/converter/facade.ts
17
18
  function toFlowDoc(doc) {
18
19
  return doc.kind === "sheet" ? projectSheetDoc(doc) : doc;
@@ -24,7 +25,11 @@ function readToFlow(reader, bytes) {
24
25
  losses
25
26
  };
26
27
  }
27
- var DEFAULT_READERS = [docxReader, xlsxReader];
28
+ var DEFAULT_READERS = [
29
+ docxReader,
30
+ xlsxReader,
31
+ pdfReader
32
+ ];
28
33
  function createConverter(opts = {}) {
29
34
  const readers = opts.readers ?? DEFAULT_READERS;
30
35
  const detect = (bytes) => readers.find((r) => r.sniff(bytes));
@@ -15,7 +15,8 @@ import { DEFAULT_READERS, resolveFontsViaChain, toFlowDoc } from "./facade.js";
15
15
  //#region src/core/converter/ream.ts
16
16
  var SOURCE_MIME = {
17
17
  docx: "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
18
- xlsx: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"
18
+ xlsx: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
19
+ pdf: "application/pdf"
19
20
  };
20
21
  var Ream = class Ream {
21
22
  constructor(flow, sheet, losses, source, readerId) {
@@ -226,10 +226,14 @@ export interface CellDataBar {
226
226
  readonly colorHex: string;
227
227
  readonly startFraction?: number;
228
228
  }
229
- export type CellIconShape = 'circle' | 'square' | 'diamond' | 'triangleUp' | 'triangleDown' | 'triangleRight';
229
+ export type CellIconShape = 'circle' | 'square' | 'diamond' | 'triangleUp' | 'triangleDown' | 'triangleRight' | 'check' | 'cross' | 'exclamation' | 'bars' | 'pie';
230
230
  export interface CellIcon {
231
231
  readonly shape: CellIconShape;
232
232
  readonly colorHex: string;
233
+ readonly fill?: {
234
+ readonly filled: number;
235
+ readonly levels: number;
236
+ };
233
237
  }
234
238
  export interface CellSparkline {
235
239
  readonly kind: 'line' | 'column' | 'winLoss';
@@ -273,6 +277,10 @@ export interface TableProperties {
273
277
  readonly defaultCellMargins?: CellMargins;
274
278
  readonly borders?: CellBorders;
275
279
  readonly alignment?: 'left' | 'center' | 'right';
280
+ readonly frozen?: {
281
+ readonly rows: number;
282
+ readonly cols: number;
283
+ };
276
284
  }
277
285
  export interface TableCell {
278
286
  readonly properties: CellProperties;
@@ -1 +1 @@
1
- export type { CellType, WorksheetCell, ColumnWidth, MergedRange, RowHeight, XlsxPageMargins, XlsxPageSetup, XlsxPrintOptions, ParsedWorksheet, XlsxFont, XlsxFill, XlsxBorderStyleName, XlsxBorderEdge, XlsxBorder, XlsxHorizontalAlign, XlsxVerticalAlign, XlsxCellAlignment, XlsxCellXf, XlsxStyles, DefinedName, Dxf, CfOperator, CfRuleCellIs, CfvoType, Cfvo, CfRuleColorScale, CfRuleDataBar, CfRuleIconSet, CfRule, ConditionalFormat, SparklineKind, ParsedSparkline, ExcelTable, } from './types.js';
1
+ export type { CellType, WorksheetCell, ColumnWidth, MergedRange, RowHeight, XlsxPageMargins, XlsxPageSetup, XlsxPrintOptions, ParsedWorksheet, XlsxFont, XlsxFill, XlsxBorderStyleName, XlsxBorderEdge, XlsxBorder, XlsxHorizontalAlign, XlsxVerticalAlign, XlsxCellAlignment, XlsxCellXf, XlsxStyles, DefinedName, Dxf, CfOperator, CfRuleCellIs, CfvoType, Cfvo, CfRuleColorScale, CfRuleDataBar, CfRuleIconSet, CfRule, ConditionalFormat, SparklineKind, ParsedSparkline, ExcelTable, SheetPane, } from './types.js';
@@ -61,6 +61,11 @@ export interface ParsedWorksheet {
61
61
  readonly sparklines?: ReadonlyArray<ParsedSparkline>;
62
62
  readonly tablePartRelIds?: ReadonlyArray<string>;
63
63
  readonly tables?: ReadonlyArray<ExcelTable>;
64
+ readonly pane?: SheetPane;
65
+ }
66
+ export interface SheetPane {
67
+ readonly frozenRows: number;
68
+ readonly frozenCols: number;
64
69
  }
65
70
  export interface ExcelTable {
66
71
  readonly ref: MergedRange;
@@ -0,0 +1,7 @@
1
+ import { BodyElement, TableProperties, TableRow } from '../core/document-model/index.js';
2
+ export interface ColumnBand {
3
+ readonly start: number;
4
+ readonly end: number;
5
+ }
6
+ export declare function computeColumnBands(columnWidths: ReadonlyArray<number>, contentWidthTwips: number, colBreaks: ReadonlySet<number>): Array<ColumnBand>;
7
+ export declare function bandedTables(rows: ReadonlyArray<TableRow>, columnWidths: ReadonlyArray<number>, bands: ReadonlyArray<ColumnBand>, properties: TableProperties): Array<BodyElement>;
@@ -0,0 +1,87 @@
1
+ import { twipsToPt } from "../core/ir/units.js";
2
+ //#region src/excel/column-bands.ts
3
+ function computeColumnBands(columnWidths, contentWidthTwips, colBreaks) {
4
+ const n = columnWidths.length;
5
+ if (n === 0) return [];
6
+ const bands = [];
7
+ let start = 0;
8
+ let acc = 0;
9
+ for (let c = 0; c < n; c++) {
10
+ const w = columnWidths[c] ?? 0;
11
+ const atBreak = c > start && colBreaks.has(c);
12
+ const overflows = c > start && acc + w > contentWidthTwips;
13
+ if (atBreak || overflows) {
14
+ bands.push({
15
+ start,
16
+ end: c - 1
17
+ });
18
+ start = c;
19
+ acc = 0;
20
+ }
21
+ acc += w;
22
+ }
23
+ bands.push({
24
+ start,
25
+ end: n - 1
26
+ });
27
+ return bands;
28
+ }
29
+ function bandedTables(rows, columnWidths, bands, properties) {
30
+ return bands.map((band, bandIndex) => {
31
+ return {
32
+ kind: "table",
33
+ table: {
34
+ properties,
35
+ grid: columnWidths.slice(band.start, band.end + 1).map((w) => twipsToPt(w)),
36
+ rows: rows.map((row, rowIndex) => {
37
+ const cells = sliceRowCells(row.cells, band);
38
+ return {
39
+ properties: bandIndex > 0 && rowIndex === 0 ? {
40
+ ...row.properties,
41
+ pageBreakBefore: true
42
+ } : row.properties,
43
+ cells
44
+ };
45
+ })
46
+ }
47
+ };
48
+ });
49
+ }
50
+ function sliceRowCells(cells, band) {
51
+ const out = [];
52
+ let col = 0;
53
+ for (const cell of cells) {
54
+ const span = cell.properties.colSpan ?? 1;
55
+ const cellStart = col;
56
+ const cellEnd = col + span - 1;
57
+ col += span;
58
+ if (cellEnd < band.start || cellStart > band.end) continue;
59
+ const clampedSpan = Math.min(cellEnd, band.end) - Math.max(cellStart, band.start) + 1;
60
+ if (cellStart >= band.start) out.push(clampedSpan === span ? cell : withColSpan(cell, clampedSpan));
61
+ else out.push(blankCell(clampedSpan));
62
+ }
63
+ return out;
64
+ }
65
+ function withColSpan(cell, span) {
66
+ const props = { ...cell.properties };
67
+ if (span > 1) props.colSpan = span;
68
+ else delete props.colSpan;
69
+ return {
70
+ ...cell,
71
+ properties: props
72
+ };
73
+ }
74
+ function blankCell(span) {
75
+ return {
76
+ properties: span > 1 ? { colSpan: span } : {},
77
+ content: [{
78
+ kind: "paragraph",
79
+ paragraph: {
80
+ properties: {},
81
+ runs: []
82
+ }
83
+ }]
84
+ };
85
+ }
86
+ //#endregion
87
+ export { bandedTables, computeColumnBands };
@@ -256,6 +256,22 @@ function iconBucket(thresholds, value) {
256
256
  }
257
257
  function iconToCell(setName, count, index, reverse) {
258
258
  const e = reverse ? count - 1 - index : index;
259
+ if (/Rating/i.test(setName)) return {
260
+ shape: "bars",
261
+ colorHex: METER_HEX,
262
+ fill: {
263
+ filled: e + 1,
264
+ levels: count
265
+ }
266
+ };
267
+ if (/Quarters/i.test(setName)) return {
268
+ shape: "pie",
269
+ colorHex: METER_HEX,
270
+ fill: {
271
+ filled: e,
272
+ levels: count - 1
273
+ }
274
+ };
259
275
  const colorHex = /Gray/i.test(setName) ? GRAY_HEX : iconColor(count, e);
260
276
  return {
261
277
  shape: iconShape(setName, e, count),
@@ -263,12 +279,18 @@ function iconToCell(setName, count, index, reverse) {
263
279
  };
264
280
  }
265
281
  var GRAY_HEX = "808080";
282
+ var METER_HEX = "595959";
266
283
  function iconShape(setName, e, count) {
267
284
  if (setName.includes("Arrows")) {
268
285
  if (e <= 0) return "triangleDown";
269
286
  if (e >= count - 1) return "triangleUp";
270
287
  return "triangleRight";
271
288
  }
289
+ if (setName.includes("Symbols")) {
290
+ if (e <= 0) return "cross";
291
+ if (e >= count - 1) return "check";
292
+ return "exclamation";
293
+ }
272
294
  if (setName.includes("Signs")) {
273
295
  if (e <= 0) return "diamond";
274
296
  if (e >= count - 1) return "circle";
@@ -2,6 +2,7 @@ import { eighthPtToPt, halfPtToPt, twipsToPt } from "../core/ir/units.js";
2
2
  import { parseAreaRef, parseTitleRowRange } from "./defined-name-ref.js";
3
3
  import { applyNumberFormat } from "./number-format.js";
4
4
  import "./index.js";
5
+ import { bandedTables, computeColumnBands } from "./column-bands.js";
5
6
  import { buildConditionalFormatter } from "./conditional-format.js";
6
7
  var TWIPS_PER_POINT = 20;
7
8
  var TWIPS_PER_INCH = 1440;
@@ -291,20 +292,38 @@ function worksheetToBody(worksheet, sharedStrings, styles, date1904, print) {
291
292
  width: eighthPtToPt(4)
292
293
  };
293
294
  const centered = worksheet.printOptions?.horizontalCentered === true;
295
+ const tableProperties = {
296
+ ...print.gridLines ? { borders: {
297
+ top: thin,
298
+ bottom: thin,
299
+ left: thin,
300
+ right: thin,
301
+ insideH: thin,
302
+ insideV: thin
303
+ } } : {},
304
+ ...centered ? { alignment: "center" } : {}
305
+ };
306
+ const contentWidthTwips = sheetContentWidthTwips(worksheet);
307
+ const colBreaksLocal = /* @__PURE__ */ new Set();
308
+ for (const brk of worksheet.colBreaks ?? []) {
309
+ const local = brk - colStart;
310
+ if (local > 0 && local < colCount) colBreaksLocal.add(local);
311
+ }
312
+ if (!scaled && colCount > 1 && (totalGridTwips > contentWidthTwips || colBreaksLocal.size > 0)) {
313
+ const bands = computeColumnBands(columnWidths, contentWidthTwips, colBreaksLocal);
314
+ if (bands.length > 1) return bandedTables(rows, columnWidths, bands, tableProperties);
315
+ }
316
+ const frozen = worksheet.pane && (worksheet.pane.frozenRows > 0 || worksheet.pane.frozenCols > 0) ? {
317
+ rows: worksheet.pane.frozenRows,
318
+ cols: worksheet.pane.frozenCols
319
+ } : void 0;
294
320
  return [{
295
321
  kind: "table",
296
322
  table: {
297
- properties: {
298
- ...print.gridLines ? { borders: {
299
- top: thin,
300
- bottom: thin,
301
- left: thin,
302
- right: thin,
303
- insideH: thin,
304
- insideV: thin
305
- } } : {},
306
- ...centered ? { alignment: "center" } : {}
307
- },
323
+ properties: frozen ? {
324
+ ...tableProperties,
325
+ frozen
326
+ } : tableProperties,
308
327
  grid: columnWidths.map((w) => twipsToPt(w)),
309
328
  rows
310
329
  }
@@ -33,6 +33,7 @@ function parseWorksheet(data) {
33
33
  const printOptions = parsePrintOptions(wsObj);
34
34
  const rowBreaks = parseBreaks(wsObj, "rowBreaks");
35
35
  const colBreaks = parseBreaks(wsObj, "colBreaks");
36
+ const pane = parsePane(wsObj);
36
37
  const drawingNode = wsObj["drawing"];
37
38
  const drawingRelId = drawingNode && typeof drawingNode === "object" ? strAttr(drawingNode, "id") : void 0;
38
39
  const conditionalFormats = parseConditionalFormatting(wsObj);
@@ -45,6 +46,7 @@ function parseWorksheet(data) {
45
46
  ...printOptions ? { printOptions } : {},
46
47
  ...rowBreaks.length > 0 ? { rowBreaks } : {},
47
48
  ...colBreaks.length > 0 ? { colBreaks } : {},
49
+ ...pane ? { pane } : {},
48
50
  ...drawingRelId !== void 0 ? { drawingRelId } : {},
49
51
  ...conditionalFormats.length > 0 ? { conditionalFormats } : {},
50
52
  ...sparklines.length > 0 ? { sparklines } : {},
@@ -181,6 +183,30 @@ function parsePrintOptions(ws) {
181
183
  ...verticalCentered !== void 0 ? { verticalCentered } : {}
182
184
  };
183
185
  }
186
+ function parsePane(ws) {
187
+ const views = ws["sheetViews"];
188
+ if (!views || typeof views !== "object") return void 0;
189
+ const viewRaw = views["sheetView"];
190
+ const view = Array.isArray(viewRaw) ? viewRaw[0] : viewRaw;
191
+ if (!view || typeof view !== "object") return void 0;
192
+ const paneNode = view["pane"];
193
+ if (!paneNode || typeof paneNode !== "object") return void 0;
194
+ const pane = paneNode;
195
+ const state = strAttr(pane, "state");
196
+ if (state !== "frozen" && state !== "frozenSplit") return void 0;
197
+ const count = (key) => {
198
+ const raw = strAttr(pane, key);
199
+ const n = raw !== void 0 ? Number(raw) : NaN;
200
+ return Number.isFinite(n) && n > 0 ? Math.floor(n) : 0;
201
+ };
202
+ const frozenCols = count("xSplit");
203
+ const frozenRows = count("ySplit");
204
+ if (frozenCols === 0 && frozenRows === 0) return void 0;
205
+ return {
206
+ frozenRows,
207
+ frozenCols
208
+ };
209
+ }
184
210
  function parseBreaks(ws, tag) {
185
211
  const node = ws[tag];
186
212
  if (!node || typeof node !== "object") return [];
@@ -136,7 +136,14 @@ function worksheetXml(grid, tableRelIds = []) {
136
136
  return `<row r="${r + 1}"${heightAttrs}>${cells.map(cellXml).join("")}</row>`;
137
137
  }).join("");
138
138
  const mergesXml = grid.merges.length > 0 ? `<mergeCells count="${grid.merges.length}">${grid.merges.map((m) => `<mergeCell ref="${cellRef(m.startRow, m.startColumn)}:${cellRef(m.endRow, m.endColumn)}"/>`).join("")}</mergeCells>` : "";
139
- return XML_DECL + `<worksheet xmlns="${MAIN_NS}" xmlns:r="${R_NS}">` + (grid.fitToPage ? "<sheetPr><pageSetUpPr fitToPage=\"1\"/></sheetPr>" : "") + dimension + colsXml + `<sheetData>${rowsXml}</sheetData>` + mergesXml + conditionalFormattingXml(grid.conditionalFormats) + printOptionsXml(grid.printOptions) + pageMarginsXml(grid.pageMargins) + pageSetupXml(grid.pageSetup) + breaksXml("rowBreaks", grid.rowBreaks) + breaksXml("colBreaks", grid.colBreaks) + (tableRelIds.length > 0 ? `<tableParts count="${tableRelIds.length}">${tableRelIds.map((id) => `<tablePart r:id="${id}"/>`).join("")}</tableParts>` : "") + sparklineExtXml(grid.sparklines) + "</worksheet>";
139
+ return XML_DECL + `<worksheet xmlns="${MAIN_NS}" xmlns:r="${R_NS}">` + (grid.fitToPage ? "<sheetPr><pageSetUpPr fitToPage=\"1\"/></sheetPr>" : "") + dimension + sheetViewsXml(grid.pane) + colsXml + `<sheetData>${rowsXml}</sheetData>` + mergesXml + conditionalFormattingXml(grid.conditionalFormats) + printOptionsXml(grid.printOptions) + pageMarginsXml(grid.pageMargins) + pageSetupXml(grid.pageSetup) + breaksXml("rowBreaks", grid.rowBreaks) + breaksXml("colBreaks", grid.colBreaks) + (tableRelIds.length > 0 ? `<tableParts count="${tableRelIds.length}">${tableRelIds.map((id) => `<tablePart r:id="${id}"/>`).join("")}</tableParts>` : "") + sparklineExtXml(grid.sparklines) + "</worksheet>";
140
+ }
141
+ function sheetViewsXml(pane) {
142
+ if (!pane || pane.frozenRows <= 0 && pane.frozenCols <= 0) return "";
143
+ const { frozenRows, frozenCols } = pane;
144
+ const topLeftCell = cellRef(frozenRows, frozenCols);
145
+ const activePane = frozenRows > 0 && frozenCols > 0 ? "bottomRight" : frozenRows > 0 ? "bottomLeft" : "topRight";
146
+ return `<sheetViews><sheetView workbookViewId="0"><pane${(frozenCols > 0 ? ` xSplit="${frozenCols}"` : "") + (frozenRows > 0 ? ` ySplit="${frozenRows}"` : "") + ` topLeftCell="${topLeftCell}" activePane="${activePane}" state="frozen"`}/></sheetView></sheetViews>`;
140
147
  }
141
148
  function tableXml(t, id) {
142
149
  const ref = rangeRef(t.ref);
@@ -356,6 +356,21 @@ function emitTable(out, table, ctx) {
356
356
  return start;
357
357
  });
358
358
  });
359
+ const frozen = table.properties.frozen;
360
+ const leftOffsets = [];
361
+ const topOffsets = [];
362
+ if (frozen) {
363
+ let left = 0;
364
+ for (const w of table.grid) {
365
+ leftOffsets.push(left);
366
+ left += w;
367
+ }
368
+ let top = 0;
369
+ for (const row of table.rows) {
370
+ topOffsets.push(top);
371
+ top += row.properties.height ?? 15;
372
+ }
373
+ }
359
374
  for (let ri = 0; ri < table.rows.length; ri++) {
360
375
  const row = table.rows[ri];
361
376
  out.push("<tr>");
@@ -363,14 +378,17 @@ function emitTable(out, table, ctx) {
363
378
  const cell = row.cells[ci];
364
379
  const merge = cell.properties.merge;
365
380
  if (merge === "middle" || merge === "end") continue;
366
- const rowSpan = merge === "start" ? mergeRowSpan(table, colStarts, ri, colStarts[ri][ci]) : 1;
381
+ const cs = colStarts[ri][ci];
382
+ const rowSpan = merge === "start" ? mergeRowSpan(table, colStarts, ri, cs) : 1;
367
383
  emitCell(out, cell, table, ctx, {
368
384
  isHeader: row.properties.isHeader === true,
369
385
  firstRow: ri === 0,
370
386
  lastRow: ri === table.rows.length - 1,
371
- firstCol: colStarts[ri][ci] === 0,
372
- lastCol: colStarts[ri][ci] + (cell.properties.colSpan ?? 1) >= table.grid.length,
373
- rowSpan
387
+ firstCol: cs === 0,
388
+ lastCol: cs + (cell.properties.colSpan ?? 1) >= table.grid.length,
389
+ rowSpan,
390
+ ...frozen && ri < frozen.rows ? { stickyTop: topOffsets[ri] } : {},
391
+ ...frozen && cs < frozen.cols ? { stickyLeft: leftOffsets[cs] } : {}
374
392
  });
375
393
  }
376
394
  out.push("</tr>");
@@ -417,6 +435,14 @@ function emitCell(out, cell, table, ctx, pos) {
417
435
  const padBottom = margins?.bottom ?? 0;
418
436
  const padLeft = margins?.left ?? 5.4;
419
437
  css.push(`padding:${fmt(padTop)}pt ${fmt(padRight)}pt ${fmt(padBottom)}pt ${fmt(padLeft)}pt`);
438
+ if (pos.stickyTop !== void 0 || pos.stickyLeft !== void 0) {
439
+ css.push("position:sticky");
440
+ if (pos.stickyTop !== void 0) css.push(`top:${fmt(pos.stickyTop)}pt`);
441
+ if (pos.stickyLeft !== void 0) css.push(`left:${fmt(pos.stickyLeft)}pt`);
442
+ const z = pos.stickyTop !== void 0 && pos.stickyLeft !== void 0 ? 3 : pos.stickyTop !== void 0 ? 2 : 1;
443
+ css.push(`z-index:${z}`);
444
+ if (!cell.properties.shading) css.push("background-color:#fff");
445
+ }
420
446
  out.push(`<${tag}${attrs.length > 0 ? ` ${attrs.join(" ")}` : ""} style="${css.join(";")}">`);
421
447
  if (cell.properties.icon) out.push(cellIconSvg(cell.properties.icon));
422
448
  if (cell.properties.sparkline) out.push(cellSparklineSvg(cell.properties.sparkline));
@@ -436,8 +462,77 @@ function cellSparklineSvg(sp) {
436
462
  }
437
463
  return `<svg width="${W}" height="${H}" viewBox="0 0 ${W} ${H}" style="vertical-align:middle"><g transform="translate(0,${H}) scale(1,-1)">${parts.join("")}</g></svg>`;
438
464
  }
465
+ var ICON_EMPTY_HEX = "BFBFBF";
439
466
  function cellIconSvg(icon) {
440
- return `<svg width="10" height="10" viewBox="0 0 10 10" fill="${`#${icon.colorHex}`}" style="vertical-align:middle;margin-right:3px">${icon.shape === "square" ? `<rect x="1" y="1" width="8" height="8"/>` : icon.shape === "diamond" ? `<polygon points="5,1 9,5 5,9 1,5"/>` : icon.shape === "triangleUp" ? `<polygon points="5,1 9,9 1,9"/>` : icon.shape === "triangleDown" ? `<polygon points="1,1 9,1 5,9"/>` : icon.shape === "triangleRight" ? `<polygon points="9,5 1,1 1,9"/>` : `<circle cx="5" cy="5" r="4"/>`}</svg>`;
467
+ const fill = `#${icon.colorHex}`;
468
+ let body;
469
+ switch (icon.shape) {
470
+ case "square":
471
+ body = `<rect x="1" y="1" width="8" height="8" fill="${fill}"/>`;
472
+ break;
473
+ case "diamond":
474
+ body = `<polygon points="5,1 9,5 5,9 1,5" fill="${fill}"/>`;
475
+ break;
476
+ case "triangleUp":
477
+ body = `<polygon points="5,1 9,9 1,9" fill="${fill}"/>`;
478
+ break;
479
+ case "triangleDown":
480
+ body = `<polygon points="1,1 9,1 5,9" fill="${fill}"/>`;
481
+ break;
482
+ case "triangleRight":
483
+ body = `<polygon points="9,5 1,1 1,9" fill="${fill}"/>`;
484
+ break;
485
+ case "check":
486
+ body = `<polyline points="1.6,5 4,7.4 8.4,2.6" fill="none" stroke="${fill}" stroke-width="1.6"/>`;
487
+ break;
488
+ case "cross":
489
+ body = `<path d="M2.4 2.4 L7.6 7.6 M2.4 7.6 L7.6 2.4" fill="none" stroke="${fill}" stroke-width="1.6"/>`;
490
+ break;
491
+ case "exclamation":
492
+ body = `<rect x="4.1" y="1" width="1.8" height="5.6" fill="${fill}"/><rect x="4.1" y="7.4" width="1.8" height="1.6" fill="${fill}"/>`;
493
+ break;
494
+ case "bars":
495
+ body = barsIconSvg(icon.fill, fill, `#${ICON_EMPTY_HEX}`);
496
+ break;
497
+ case "pie":
498
+ body = pieIconSvg(icon.fill, fill, `#${ICON_EMPTY_HEX}`);
499
+ break;
500
+ case "circle":
501
+ body = `<circle cx="5" cy="5" r="4" fill="${fill}"/>`;
502
+ break;
503
+ }
504
+ return `<svg width="10" height="10" viewBox="0 0 10 10" style="vertical-align:middle;margin-right:3px">${body}</svg>`;
505
+ }
506
+ var round2 = (n) => (Math.round(n * 100) / 100).toString();
507
+ function barsIconSvg(fill, color, empty) {
508
+ const n = Math.max(1, fill?.levels ?? 4);
509
+ const filled = fill?.filled ?? 0;
510
+ const gap = 10 * .12;
511
+ const bw = (10 - gap * (n - 1)) / n;
512
+ let out = "";
513
+ for (let i = 0; i < n; i++) {
514
+ const h = 10 * (.32 + .68 * (i + 1) / n);
515
+ out += `<rect x="${round2(i * (bw + gap))}" y="${round2(10 - h)}" width="${round2(bw)}" height="${round2(h)}" fill="${i < filled ? color : empty}"/>`;
516
+ }
517
+ return out;
518
+ }
519
+ function pieIconSvg(fill, color, empty) {
520
+ const cx = 5;
521
+ const cy = 5;
522
+ const r = 4.4;
523
+ const levels = Math.max(1, fill?.levels ?? 4);
524
+ const filled = Math.max(0, Math.min(levels, fill?.filled ?? 0));
525
+ let out = `<circle cx="${cx}" cy="${cy}" r="${r}" fill="${empty}"/>`;
526
+ if (filled >= levels) out += `<circle cx="${cx}" cy="${cy}" r="${r}" fill="${color}"/>`;
527
+ else if (filled > 0) {
528
+ const frac = filled / levels;
529
+ const ang = -Math.PI / 2 + frac * 2 * Math.PI;
530
+ const ex = cx + r * Math.cos(ang);
531
+ const ey = cy + r * Math.sin(ang);
532
+ const large = frac > .5 ? 1 : 0;
533
+ out += `<path d="M${cx} ${cy} L${cx} ${round2(cy - r)} A${r} ${r} 0 ${large} 1 ${round2(ex)} ${round2(ey)} Z" fill="${color}"/>`;
534
+ }
535
+ return out;
441
536
  }
442
537
  function pushBorder(css, side, border) {
443
538
  if (!border || border.style === "none") return;
@@ -563,11 +563,35 @@ function rectAtPath(x, y, w, h) {
563
563
  }
564
564
  var CF_ICON_SIZE_PT = 9;
565
565
  var CF_ICON_GUTTER_PT = 12;
566
- function buildCellIconShape(icon, size) {
567
- return {
568
- paths: [cellIconPath(icon.shape, size)],
569
- fillColorHex: icon.colorHex
570
- };
566
+ var CF_ICON_EMPTY_HEX = "BFBFBF";
567
+ function buildCellIconShape(icon, s) {
568
+ const color = icon.colorHex;
569
+ switch (icon.shape) {
570
+ case "bars": return barsIconPrims(s, color, icon.fill);
571
+ case "pie": return pieIconPrims(s, color, icon.fill);
572
+ case "check": return [{
573
+ paths: [checkPath(s)],
574
+ stroke: {
575
+ colorHex: color,
576
+ widthPt: s * .16
577
+ }
578
+ }];
579
+ case "cross": return [{
580
+ paths: crossPaths(s),
581
+ stroke: {
582
+ colorHex: color,
583
+ widthPt: s * .16
584
+ }
585
+ }];
586
+ case "exclamation": return [{
587
+ paths: [rectAtPath(s * .41, s * .34, s * .18, s * .56), rectAtPath(s * .41, s * .1, s * .18, s * .16)],
588
+ fillColorHex: color
589
+ }];
590
+ default: return [{
591
+ paths: [cellIconPath(icon.shape, s)],
592
+ fillColorHex: color
593
+ }];
594
+ }
571
595
  }
572
596
  function cellIconPath(shape, s) {
573
597
  switch (shape) {
@@ -579,6 +603,52 @@ function cellIconPath(shape, s) {
579
603
  case "circle": return circlePath(s / 2, s / 2, s * .42);
580
604
  }
581
605
  }
606
+ function barsIconPrims(s, color, fill) {
607
+ const n = Math.max(1, fill?.levels ?? 4);
608
+ const filled = fill?.filled ?? 0;
609
+ const gap = s * .12;
610
+ const bw = (s - gap * (n - 1)) / n;
611
+ const prims = [];
612
+ for (let i = 0; i < n; i++) {
613
+ const h = s * (.32 + .68 * (i + 1) / n);
614
+ prims.push({
615
+ paths: [rectAtPath(i * (bw + gap), 0, bw, h)],
616
+ fillColorHex: i < filled ? color : CF_ICON_EMPTY_HEX
617
+ });
618
+ }
619
+ return prims;
620
+ }
621
+ function pieIconPrims(s, color, fill) {
622
+ const cx = s / 2;
623
+ const cy = s / 2;
624
+ const r = s * .46;
625
+ const levels = Math.max(1, fill?.levels ?? 4);
626
+ const filled = Math.max(0, Math.min(levels, fill?.filled ?? 0));
627
+ const prims = [{
628
+ paths: [circlePath(cx, cy, r)],
629
+ fillColorHex: CF_ICON_EMPTY_HEX
630
+ }];
631
+ if (filled >= levels) prims.push({
632
+ paths: [circlePath(cx, cy, r)],
633
+ fillColorHex: color
634
+ });
635
+ else if (filled > 0) {
636
+ const start = Math.PI / 2;
637
+ const sweep = -(filled / levels) * 2 * Math.PI;
638
+ const [sx, sy] = arcPoint(cx, cy, r, r, start);
639
+ prims.push({
640
+ paths: [new PathBuilder().moveTo(cx, cy).lineTo(sx, sy).append(arcToBeziers(cx, cy, r, r, start, sweep)).close().build()],
641
+ fillColorHex: color
642
+ });
643
+ }
644
+ return prims;
645
+ }
646
+ function checkPath(s) {
647
+ return new PathBuilder().moveTo(s * .16, s * .5).lineTo(s * .4, s * .26).lineTo(s * .84, s * .78).build();
648
+ }
649
+ function crossPaths(s) {
650
+ return [new PathBuilder().moveTo(s * .24, s * .24).lineTo(s * .76, s * .76).build(), new PathBuilder().moveTo(s * .24, s * .76).lineTo(s * .76, s * .24).build()];
651
+ }
582
652
  function trianglePath(a, b, c) {
583
653
  return new PathBuilder().moveTo(a[0], a[1]).lineTo(b[0], b[1]).lineTo(c[0], c[1]).close().build();
584
654
  }
@@ -2129,22 +2199,21 @@ function emitRowChunk(out, row, marginLeft, cursorY, pageHeight, colCount, cellS
2129
2199
  if (cell.icon && cell.mergeRole !== "middle" && cell.mergeRole !== "end") {
2130
2200
  const iconSize = Math.min(CF_ICON_SIZE_PT, Math.max(0, row.heightPt - 1));
2131
2201
  if (iconSize > 0) {
2132
- const iconX = cellX + (CF_ICON_GUTTER_PT - iconSize) / 2;
2133
- const iconBottomYUp = rowBottom + (row.heightPt - iconSize) / 2;
2134
- const { paths, fillColorHex } = buildCellIconShape(cell.icon, iconSize);
2135
- out.push({
2202
+ const iconTransform = flipTransform([
2203
+ 1,
2204
+ 0,
2205
+ 0,
2206
+ 1,
2207
+ cellX + (CF_ICON_GUTTER_PT - iconSize) / 2,
2208
+ rowBottom + (row.heightPt - iconSize) / 2
2209
+ ], pageHeight);
2210
+ for (const prim of buildCellIconShape(cell.icon, iconSize)) out.push({
2136
2211
  type: "shape",
2137
2212
  shape: {
2138
- paths,
2139
- fillColorHex,
2140
- transform: flipTransform([
2141
- 1,
2142
- 0,
2143
- 0,
2144
- 1,
2145
- iconX,
2146
- iconBottomYUp
2147
- ], pageHeight)
2213
+ paths: prim.paths,
2214
+ ...prim.fillColorHex ? { fillColorHex: prim.fillColorHex } : {},
2215
+ ...prim.stroke ? { stroke: prim.stroke } : {},
2216
+ transform: iconTransform
2148
2217
  }
2149
2218
  });
2150
2219
  }
@@ -0,0 +1,5 @@
1
+ export interface ToUnicode {
2
+ readonly map: ReadonlyMap<number, string>;
3
+ readonly codeBytes: 1 | 2;
4
+ }
5
+ export declare function parseToUnicodeCMap(bytes: Uint8Array): ToUnicode;