documonster 0.7.0 → 0.8.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/browser/modules/pdf/builder/image-utils.d.ts +1 -1
- package/dist/browser/modules/pdf/builder/image-utils.js +58 -9
- package/dist/browser/modules/pdf/core/pdf-writer.d.ts +8 -0
- package/dist/browser/modules/pdf/core/pdf-writer.js +3 -0
- package/dist/browser/modules/pdf/excel-bridge.js +126 -19
- package/dist/browser/modules/pdf/index.d.ts +1 -1
- package/dist/browser/modules/pdf/render/chart-surface.d.ts +6 -1
- package/dist/browser/modules/pdf/render/chart-surface.js +86 -47
- package/dist/browser/modules/pdf/render/constants.d.ts +69 -0
- package/dist/browser/modules/pdf/render/constants.js +45 -0
- package/dist/browser/modules/pdf/render/layout-engine.d.ts +5 -1
- package/dist/browser/modules/pdf/render/layout-engine.js +586 -205
- package/dist/browser/modules/pdf/render/page-renderer.js +180 -3
- package/dist/browser/modules/pdf/render/pdf-exporter.js +176 -35
- package/dist/browser/modules/pdf/render/style-converter.d.ts +11 -0
- package/dist/browser/modules/pdf/render/style-converter.js +24 -0
- package/dist/browser/modules/pdf/types.d.ts +284 -5
- package/dist/cjs/modules/pdf/builder/image-utils.js +58 -9
- package/dist/cjs/modules/pdf/core/pdf-writer.js +3 -0
- package/dist/cjs/modules/pdf/excel-bridge.js +125 -18
- package/dist/cjs/modules/pdf/render/chart-surface.js +86 -47
- package/dist/cjs/modules/pdf/render/constants.js +46 -1
- package/dist/cjs/modules/pdf/render/layout-engine.js +583 -202
- package/dist/cjs/modules/pdf/render/page-renderer.js +179 -2
- package/dist/cjs/modules/pdf/render/pdf-exporter.js +175 -34
- package/dist/cjs/modules/pdf/render/style-converter.js +26 -0
- package/dist/esm/modules/pdf/builder/image-utils.js +58 -9
- package/dist/esm/modules/pdf/core/pdf-writer.js +3 -0
- package/dist/esm/modules/pdf/excel-bridge.js +126 -19
- package/dist/esm/modules/pdf/render/chart-surface.js +86 -47
- package/dist/esm/modules/pdf/render/constants.js +45 -0
- package/dist/esm/modules/pdf/render/layout-engine.js +586 -205
- package/dist/esm/modules/pdf/render/page-renderer.js +180 -3
- package/dist/esm/modules/pdf/render/pdf-exporter.js +176 -35
- package/dist/esm/modules/pdf/render/style-converter.js +24 -0
- package/dist/iife/documonster.archive.iife.js +1 -1
- package/dist/iife/documonster.archive.iife.min.js +1 -1
- package/dist/iife/documonster.csv.iife.js +1 -1
- package/dist/iife/documonster.csv.iife.min.js +1 -1
- package/dist/iife/documonster.excel.iife.js +1 -1
- package/dist/iife/documonster.excel.iife.min.js +1 -1
- package/dist/iife/documonster.formula.iife.js +1 -1
- package/dist/iife/documonster.formula.iife.min.js +1 -1
- package/dist/iife/documonster.markdown.iife.js +1 -1
- package/dist/iife/documonster.markdown.iife.min.js +1 -1
- package/dist/iife/documonster.pdf.iife.js +1457 -664
- package/dist/iife/documonster.pdf.iife.js.map +1 -1
- package/dist/iife/documonster.pdf.iife.min.js +33 -32
- package/dist/iife/documonster.stream.iife.js +1 -1
- package/dist/iife/documonster.stream.iife.min.js +1 -1
- package/dist/iife/documonster.word.iife.js +1 -1
- package/dist/iife/documonster.word.iife.min.js +1 -1
- package/dist/iife/documonster.xml.iife.js +1 -1
- package/dist/iife/documonster.xml.iife.min.js +1 -1
- package/dist/types/modules/pdf/builder/image-utils.d.ts +1 -1
- package/dist/types/modules/pdf/core/pdf-writer.d.ts +8 -0
- package/dist/types/modules/pdf/index.d.ts +1 -1
- package/dist/types/modules/pdf/render/chart-surface.d.ts +6 -1
- package/dist/types/modules/pdf/render/constants.d.ts +69 -0
- package/dist/types/modules/pdf/render/layout-engine.d.ts +5 -1
- package/dist/types/modules/pdf/render/style-converter.d.ts +11 -0
- package/dist/types/modules/pdf/types.d.ts +284 -5
- package/package.json +1 -1
|
@@ -12,7 +12,8 @@
|
|
|
12
12
|
import { parseImageDimensions } from "../builder/image-utils.js";
|
|
13
13
|
import { PdfContentStream } from "../core/pdf-stream.js";
|
|
14
14
|
import { resolvePdfFontName } from "../font/font-manager.js";
|
|
15
|
-
import { CELL_PADDING_H, CELL_PADDING_V, LINE_HEIGHT_FACTOR, INDENT_WIDTH } from "./constants.js";
|
|
15
|
+
import { CELL_PADDING_H, CELL_PADDING_V, LINE_HEIGHT_FACTOR, INDENT_WIDTH, HEADING_FILL, HEADING_RULE, HEADING_TEXT, COMMENT_FILL, COMMENT_BORDER, COMMENT_MARKER_COLOR } from "./constants.js";
|
|
16
|
+
import { toGrayscale } from "./style-converter.js";
|
|
16
17
|
function computeCellPadding(cell, scaleFactor = 1) {
|
|
17
18
|
return {
|
|
18
19
|
left: (CELL_PADDING_H + cell.borderInsets.left) * scaleFactor,
|
|
@@ -101,6 +102,14 @@ export function renderPage(page, options, fontManager, totalPages) {
|
|
|
101
102
|
drawCellText(stream, cell, fontManager, alphaValues, sf);
|
|
102
103
|
}
|
|
103
104
|
}
|
|
105
|
+
// --- Step 5b: Draw printed row/column headings ---
|
|
106
|
+
if (options.showRowColHeaders) {
|
|
107
|
+
drawRowColHeadings(stream, page, options, fontManager);
|
|
108
|
+
}
|
|
109
|
+
// --- Step 5c: Draw comment boxes over the grid ---
|
|
110
|
+
if (page.commentBoxes?.length) {
|
|
111
|
+
drawCommentBoxes(stream, page, options, fontManager);
|
|
112
|
+
}
|
|
104
113
|
// --- Step 5: Draw page header (sheet name) ---
|
|
105
114
|
const excelHeader = selectHeaderFooter(page, "header");
|
|
106
115
|
const excelFooter = selectHeaderFooter(page, "footer");
|
|
@@ -1343,7 +1352,11 @@ function drawHeaderFooterSection(stream, page, fontManager, runs, anchorX, basel
|
|
|
1343
1352
|
: run.subscript
|
|
1344
1353
|
? -fontSize * HEADER_FOOTER_METRICS.SUBSCRIPT_DROP
|
|
1345
1354
|
: 0;
|
|
1346
|
-
|
|
1355
|
+
// Header/footer runs carry their own `&K` colors, which bypass the cell
|
|
1356
|
+
// style pipeline — apply black-and-white here.
|
|
1357
|
+
const bw = page.options.blackAndWhite;
|
|
1358
|
+
const rawColor = run.color ?? { r: 0, g: 0, b: 0 };
|
|
1359
|
+
const color = bw ? toGrayscale(rawColor) : rawColor;
|
|
1347
1360
|
stream.save();
|
|
1348
1361
|
stream.setFillColor(color);
|
|
1349
1362
|
stream.setStrokeColor(color);
|
|
@@ -1392,6 +1405,169 @@ function resolveLineRuns(line, page, fontManager) {
|
|
|
1392
1405
|
return { run, text, resourceName, fontSize, width };
|
|
1393
1406
|
});
|
|
1394
1407
|
}
|
|
1408
|
+
// =============================================================================
|
|
1409
|
+
// Row / Column Headings
|
|
1410
|
+
// =============================================================================
|
|
1411
|
+
/** Convert a 1-based column number to its Excel letters (1 → "A", 27 → "AA"). */
|
|
1412
|
+
function columnNumberToLetters(col) {
|
|
1413
|
+
let n = col;
|
|
1414
|
+
let out = "";
|
|
1415
|
+
while (n > 0) {
|
|
1416
|
+
const rem = (n - 1) % 26;
|
|
1417
|
+
out = String.fromCharCode(65 + rem) + out;
|
|
1418
|
+
n = Math.floor((n - 1) / 26);
|
|
1419
|
+
}
|
|
1420
|
+
return out;
|
|
1421
|
+
}
|
|
1422
|
+
/**
|
|
1423
|
+
* Draw Excel's printed row numbers and column letters around the grid.
|
|
1424
|
+
*
|
|
1425
|
+
* The bands occupy space reserved by the layout engine (see
|
|
1426
|
+
* `computeHeadingMetrics`), so the grid origin already accounts for them and
|
|
1427
|
+
* nothing here can push content off the page.
|
|
1428
|
+
*/
|
|
1429
|
+
function drawRowColHeadings(stream, page, options, fontManager) {
|
|
1430
|
+
const headings = page.headings;
|
|
1431
|
+
if (!headings || page.columnWidths.length === 0 || page.rowHeights.length === 0) {
|
|
1432
|
+
return;
|
|
1433
|
+
}
|
|
1434
|
+
const { gutterWidth, bandHeight, fontSize } = headings;
|
|
1435
|
+
const resourceName = fontManager.hasEmbeddedFont()
|
|
1436
|
+
? fontManager.getEmbeddedResourceName()
|
|
1437
|
+
: fontManager.ensureFont(resolvePdfFontName(options.defaultFontFamily, false, false));
|
|
1438
|
+
// The heading palette is already neutral gray, so black-and-white needs no
|
|
1439
|
+
// conversion here.
|
|
1440
|
+
const fill = HEADING_FILL;
|
|
1441
|
+
const rule = HEADING_RULE;
|
|
1442
|
+
const textColor = HEADING_TEXT;
|
|
1443
|
+
const gridLeft = page.columnOffsets[0];
|
|
1444
|
+
const lastCol = page.columnWidths.length - 1;
|
|
1445
|
+
const gridRight = page.columnOffsets[lastCol] + page.columnWidths[lastCol];
|
|
1446
|
+
const gridTop = page.rowYPositions[0];
|
|
1447
|
+
const lastRow = page.rowHeights.length - 1;
|
|
1448
|
+
const gridBottom = page.rowYPositions[lastRow] - page.rowHeights[lastRow];
|
|
1449
|
+
stream.save();
|
|
1450
|
+
// Band backgrounds: column letters above the grid, row numbers to its left,
|
|
1451
|
+
// plus the corner square where the two meet.
|
|
1452
|
+
stream.fillRect(gridLeft, gridTop, gridRight - gridLeft, bandHeight, fill);
|
|
1453
|
+
stream.fillRect(gridLeft - gutterWidth, gridBottom, gutterWidth, gridTop - gridBottom, fill);
|
|
1454
|
+
stream.fillRect(gridLeft - gutterWidth, gridTop, gutterWidth, bandHeight, fill);
|
|
1455
|
+
// Rules: one per column boundary through the top band, one per row boundary
|
|
1456
|
+
// through the gutter, and the two outer edges.
|
|
1457
|
+
stream.setStrokeColor(rule);
|
|
1458
|
+
stream.setLineWidth(0.25);
|
|
1459
|
+
for (let i = 0; i <= page.columnWidths.length; i++) {
|
|
1460
|
+
const x = i < page.columnWidths.length
|
|
1461
|
+
? page.columnOffsets[i]
|
|
1462
|
+
: page.columnOffsets[i - 1] + page.columnWidths[i - 1];
|
|
1463
|
+
stream.moveTo(x, gridTop);
|
|
1464
|
+
stream.lineTo(x, gridTop + bandHeight);
|
|
1465
|
+
}
|
|
1466
|
+
for (let i = 0; i <= page.rowYPositions.length; i++) {
|
|
1467
|
+
const y = i < page.rowYPositions.length
|
|
1468
|
+
? page.rowYPositions[i]
|
|
1469
|
+
: page.rowYPositions[i - 1] - page.rowHeights[i - 1];
|
|
1470
|
+
stream.moveTo(gridLeft - gutterWidth, y);
|
|
1471
|
+
stream.lineTo(gridLeft, y);
|
|
1472
|
+
}
|
|
1473
|
+
stream.moveTo(gridLeft - gutterWidth, gridTop + bandHeight);
|
|
1474
|
+
stream.lineTo(gridRight, gridTop + bandHeight);
|
|
1475
|
+
stream.moveTo(gridLeft - gutterWidth, gridBottom);
|
|
1476
|
+
stream.lineTo(gridLeft - gutterWidth, gridTop + bandHeight);
|
|
1477
|
+
stream.stroke();
|
|
1478
|
+
// Labels, centered in their band cell.
|
|
1479
|
+
stream.setFillColor(textColor);
|
|
1480
|
+
const baselineOffset = (bandHeight - fontSize) / 2 + fontSize * 0.2;
|
|
1481
|
+
for (let i = 0; i < page.sheetCols.length; i++) {
|
|
1482
|
+
const label = columnNumberToLetters(page.sheetCols[i]);
|
|
1483
|
+
fontManager.trackText(label);
|
|
1484
|
+
const w = fontManager.measureText(label, resourceName, fontSize);
|
|
1485
|
+
const x = page.columnOffsets[i] + (page.columnWidths[i] - w) / 2;
|
|
1486
|
+
emitTextWithMatrix(stream, fontManager, {
|
|
1487
|
+
text: label,
|
|
1488
|
+
matrix: [1, 0, 0, 1, x, gridTop + baselineOffset],
|
|
1489
|
+
resourceName,
|
|
1490
|
+
fontSize
|
|
1491
|
+
});
|
|
1492
|
+
}
|
|
1493
|
+
for (let i = 0; i < page.sheetRows.length; i++) {
|
|
1494
|
+
const label = String(page.sheetRows[i]);
|
|
1495
|
+
fontManager.trackText(label);
|
|
1496
|
+
const w = fontManager.measureText(label, resourceName, fontSize);
|
|
1497
|
+
const rowH = page.rowHeights[i];
|
|
1498
|
+
const x = gridLeft - gutterWidth + (gutterWidth - w) / 2;
|
|
1499
|
+
const y = page.rowYPositions[i] - rowH + (rowH - fontSize) / 2 + fontSize * 0.2;
|
|
1500
|
+
emitTextWithMatrix(stream, fontManager, {
|
|
1501
|
+
text: label,
|
|
1502
|
+
matrix: [1, 0, 0, 1, x, y],
|
|
1503
|
+
resourceName,
|
|
1504
|
+
fontSize
|
|
1505
|
+
});
|
|
1506
|
+
}
|
|
1507
|
+
stream.restore();
|
|
1508
|
+
}
|
|
1509
|
+
/**
|
|
1510
|
+
* Draw Excel's on-sheet comment boxes and their cell corner markers.
|
|
1511
|
+
*
|
|
1512
|
+
* Positioned by the layout engine, so this only has to paint: a pale note-yellow
|
|
1513
|
+
* box with a thin border, the wrapped body inside it, and the small red triangle
|
|
1514
|
+
* Excel puts in the commented cell's top-right corner.
|
|
1515
|
+
*/
|
|
1516
|
+
function drawCommentBoxes(stream, page, options, fontManager) {
|
|
1517
|
+
const boxes = page.commentBoxes;
|
|
1518
|
+
if (!boxes?.length) {
|
|
1519
|
+
return;
|
|
1520
|
+
}
|
|
1521
|
+
const bw = options.blackAndWhite;
|
|
1522
|
+
const fill = bw ? toGrayscale(COMMENT_FILL) : COMMENT_FILL;
|
|
1523
|
+
const border = bw ? toGrayscale(COMMENT_BORDER) : COMMENT_BORDER;
|
|
1524
|
+
const markerColor = bw ? toGrayscale(COMMENT_MARKER_COLOR) : COMMENT_MARKER_COLOR;
|
|
1525
|
+
const resourceName = fontManager.hasEmbeddedFont()
|
|
1526
|
+
? fontManager.getEmbeddedResourceName()
|
|
1527
|
+
: fontManager.ensureFont(resolvePdfFontName(options.defaultFontFamily, false, false));
|
|
1528
|
+
for (const box of boxes) {
|
|
1529
|
+
const { rect } = box;
|
|
1530
|
+
stream.save();
|
|
1531
|
+
stream.fillRect(rect.x, rect.y, rect.width, rect.height, fill);
|
|
1532
|
+
stream.setStrokeColor(border);
|
|
1533
|
+
stream.setLineWidth(0.5);
|
|
1534
|
+
stream.rect(rect.x, rect.y, rect.width, rect.height);
|
|
1535
|
+
stream.stroke();
|
|
1536
|
+
// Body text, wrapped to the box and clipped by simply stopping once the
|
|
1537
|
+
// remaining height is used up.
|
|
1538
|
+
const lineHeight = box.fontSize * LINE_HEIGHT_FACTOR;
|
|
1539
|
+
const innerWidth = Math.max(rect.width - 2 * CELL_PADDING_H, 1);
|
|
1540
|
+
const measure = (t) => fontManager.measureText(t, resourceName, box.fontSize);
|
|
1541
|
+
const lines = box.text.split("\n").flatMap(part => wrapTextLines(part, measure, innerWidth));
|
|
1542
|
+
stream.setFillColor(bw ? toGrayscale({ r: 0, g: 0, b: 0 }) : { r: 0, g: 0, b: 0 });
|
|
1543
|
+
let baseline = rect.y + rect.height - CELL_PADDING_V - box.fontSize;
|
|
1544
|
+
for (const line of lines) {
|
|
1545
|
+
if (baseline < rect.y + CELL_PADDING_V) {
|
|
1546
|
+
break;
|
|
1547
|
+
}
|
|
1548
|
+
fontManager.trackText(line);
|
|
1549
|
+
emitTextWithMatrix(stream, fontManager, {
|
|
1550
|
+
text: line,
|
|
1551
|
+
matrix: [1, 0, 0, 1, rect.x + CELL_PADDING_H, baseline],
|
|
1552
|
+
resourceName,
|
|
1553
|
+
fontSize: box.fontSize
|
|
1554
|
+
});
|
|
1555
|
+
baseline -= lineHeight;
|
|
1556
|
+
}
|
|
1557
|
+
stream.restore();
|
|
1558
|
+
if (box.marker) {
|
|
1559
|
+
const { x, y, size } = box.marker;
|
|
1560
|
+
stream.save();
|
|
1561
|
+
stream.setFillColor(markerColor);
|
|
1562
|
+
stream.moveTo(x - size, y);
|
|
1563
|
+
stream.lineTo(x, y);
|
|
1564
|
+
stream.lineTo(x, y - size);
|
|
1565
|
+
stream.closePath();
|
|
1566
|
+
stream.fill();
|
|
1567
|
+
stream.restore();
|
|
1568
|
+
}
|
|
1569
|
+
}
|
|
1570
|
+
}
|
|
1395
1571
|
function drawPageHeader(stream, page, options, fontManager) {
|
|
1396
1572
|
const headerFontSize = 10;
|
|
1397
1573
|
const headerText = page.sheetName;
|
|
@@ -1495,7 +1671,8 @@ function clamp01(v) {
|
|
|
1495
1671
|
*/
|
|
1496
1672
|
function renderTextWatermark(stream, page, watermark, fontManager) {
|
|
1497
1673
|
const fontSize = watermark.fontSize ?? TEXT_WM_DEFAULTS.fontSize;
|
|
1498
|
-
const
|
|
1674
|
+
const rawColor = watermark.color ?? TEXT_WM_DEFAULTS.color;
|
|
1675
|
+
const color = page.options.blackAndWhite ? toGrayscale(rawColor) : rawColor;
|
|
1499
1676
|
const opacity = watermark.opacity ?? TEXT_WM_DEFAULTS.opacity;
|
|
1500
1677
|
const rotation = watermark.rotation ?? TEXT_WM_DEFAULTS.rotation;
|
|
1501
1678
|
const fontFamily = watermark.fontFamily ?? TEXT_WM_DEFAULTS.fontFamily;
|
|
@@ -19,7 +19,7 @@ import { parseTtf } from "../font/ttf-parser.js";
|
|
|
19
19
|
import { createChartSurface } from "./chart-surface.js";
|
|
20
20
|
import { layoutChartsheet, layoutSheet } from "./layout-engine.js";
|
|
21
21
|
import { renderPage, alphaGsName, renderWatermark, resolveHeaderFooterRunText, selectHeaderFooter } from "./page-renderer.js";
|
|
22
|
-
import { argbToPdfColor } from "./style-converter.js";
|
|
22
|
+
import { argbToPdfColor, toGrayscale } from "./style-converter.js";
|
|
23
23
|
import { PageSizes, PdfCellType, isPdfChartsheet } from "../types.js";
|
|
24
24
|
import { yieldToEventLoop } from "../../../utils/utils.base.js";
|
|
25
25
|
// =============================================================================
|
|
@@ -145,7 +145,9 @@ function preflightCharts(allPages, fontManager) {
|
|
|
145
145
|
}
|
|
146
146
|
const stream = new PdfContentStream();
|
|
147
147
|
const alphaValues = new Set();
|
|
148
|
-
|
|
148
|
+
// Black-and-white is a per-sheet print option, and each page carries the
|
|
149
|
+
// options of the sheet that produced it.
|
|
150
|
+
const surface = createChartSurface(stream, fontManager, alphaValues, page.options.blackAndWhite);
|
|
149
151
|
chart.drawVector(surface, constrainChartAspectRatio(chart.rect));
|
|
150
152
|
cache.set(chart, { stream, alphaValues });
|
|
151
153
|
}
|
|
@@ -267,6 +269,10 @@ function renderSinglePage(page, fontManager, writer, fontObjectMap, chartCache,
|
|
|
267
269
|
const { stream: contentStream, alphaValues } = renderPage(page, page.options, fontManager, totalPages);
|
|
268
270
|
// Handle images: create XObject Image entries and draw them
|
|
269
271
|
const imageXObjects = new Map();
|
|
272
|
+
// Raster payloads are converted when the XObject is written — a real pixel
|
|
273
|
+
// conversion for PNG, a `/DeviceN` luma space for JPEG — so no overlay is
|
|
274
|
+
// painted and transparency is preserved.
|
|
275
|
+
const grayscaleRaster = page.options.blackAndWhite;
|
|
270
276
|
let headerImageContentObjNum;
|
|
271
277
|
const headerContent = selectHeaderFooter(page, "header");
|
|
272
278
|
const footerContent = selectHeaderFooter(page, "footer");
|
|
@@ -282,23 +288,28 @@ function renderSinglePage(page, fontManager, writer, fontObjectMap, chartCache,
|
|
|
282
288
|
const rightEdge = alignWithMargins
|
|
283
289
|
? page.width - page.options.margins.right
|
|
284
290
|
: page.width - 18;
|
|
291
|
+
// `scaleWithDoc` governs the whole header/footer, not just its text, so a
|
|
292
|
+
// logo has to shrink with the grid the same way the runs do.
|
|
293
|
+
const hfScale = page.headerFooter?.scaleWithDoc === false ? 1 : page.scaleFactor;
|
|
285
294
|
for (let index = 0; index < headerImages.length; index++) {
|
|
286
295
|
const headerImage = headerImages[index];
|
|
287
296
|
if (!activePositions.has(headerImage.position)) {
|
|
288
297
|
continue;
|
|
289
298
|
}
|
|
290
299
|
const imageName = `HfIm${index + 1}`;
|
|
291
|
-
imageXObjects.set(imageName, getOrWriteImageObject(writer, imageObjectCache, headerImage.data, headerImage.format));
|
|
300
|
+
imageXObjects.set(imageName, getOrWriteImageObject(writer, imageObjectCache, headerImage.data, headerImage.format, grayscaleRaster));
|
|
301
|
+
const drawWidth = headerImage.width * hfScale;
|
|
302
|
+
const drawHeight = headerImage.height * hfScale;
|
|
292
303
|
const placement = headerFooterImagePlacement(headerImage.position);
|
|
293
304
|
const imageX = placement.section === "left"
|
|
294
305
|
? leftEdge
|
|
295
306
|
: placement.section === "right"
|
|
296
|
-
? rightEdge -
|
|
297
|
-
: (page.width -
|
|
307
|
+
? rightEdge - drawWidth
|
|
308
|
+
: (page.width - drawWidth) / 2;
|
|
298
309
|
const imageY = placement.kind === "header"
|
|
299
|
-
? page.height - page.options.headerMargin -
|
|
310
|
+
? page.height - page.options.headerMargin - drawHeight
|
|
300
311
|
: page.options.footerMargin;
|
|
301
|
-
imageStream.drawImage(imageName, imageX, imageY,
|
|
312
|
+
imageStream.drawImage(imageName, imageX, imageY, drawWidth, drawHeight);
|
|
302
313
|
}
|
|
303
314
|
headerImageContentObjNum = writer.allocObject();
|
|
304
315
|
writer.addStreamObject(headerImageContentObjNum, new PdfDict(), imageStream);
|
|
@@ -307,7 +318,7 @@ function renderSinglePage(page, fontManager, writer, fontObjectMap, chartCache,
|
|
|
307
318
|
for (let imgIdx = 0; imgIdx < page.images.length; imgIdx++) {
|
|
308
319
|
const img = page.images[imgIdx];
|
|
309
320
|
const imgName = `Im${imgIdx + 1}`;
|
|
310
|
-
const imgObjNum = getOrWriteImageObject(writer, imageObjectCache, img.data, img.format);
|
|
321
|
+
const imgObjNum = getOrWriteImageObject(writer, imageObjectCache, img.data, img.format, grayscaleRaster);
|
|
311
322
|
imageXObjects.set(imgName, imgObjNum);
|
|
312
323
|
contentStream.drawImage(imgName, img.rect.x, img.rect.y, img.rect.width, img.rect.height);
|
|
313
324
|
}
|
|
@@ -336,7 +347,7 @@ function renderSinglePage(page, fontManager, writer, fontObjectMap, chartCache,
|
|
|
336
347
|
}
|
|
337
348
|
if (chart.raster) {
|
|
338
349
|
const imgName = `Im${++rasterCounter}`;
|
|
339
|
-
const imgObjNum = getOrWriteImageObject(writer, imageObjectCache, chart.raster.data, chart.raster.format);
|
|
350
|
+
const imgObjNum = getOrWriteImageObject(writer, imageObjectCache, chart.raster.data, chart.raster.format, grayscaleRaster);
|
|
340
351
|
imageXObjects.set(imgName, imgObjNum);
|
|
341
352
|
contentStream.drawImage(imgName, chart.rect.x, chart.rect.y, chart.rect.width, chart.rect.height);
|
|
342
353
|
}
|
|
@@ -356,7 +367,7 @@ function renderSinglePage(page, fontManager, writer, fontObjectMap, chartCache,
|
|
|
356
367
|
}
|
|
357
368
|
// Register watermark image XObjects
|
|
358
369
|
for (const wmImg of wmResult.imageXObjects) {
|
|
359
|
-
const imgObjNum = getOrWriteImageObject(writer, imageObjectCache, wmImg.data, wmImg.format);
|
|
370
|
+
const imgObjNum = getOrWriteImageObject(writer, imageObjectCache, wmImg.data, wmImg.format, grayscaleRaster);
|
|
360
371
|
imageXObjects.set(wmImg.name, imgObjNum);
|
|
361
372
|
}
|
|
362
373
|
// Write watermark content stream object
|
|
@@ -437,6 +448,8 @@ function renderSinglePage(page, fontManager, writer, fontObjectMap, chartCache,
|
|
|
437
448
|
height: page.height,
|
|
438
449
|
contentsRef: contentsRef,
|
|
439
450
|
resourcesRef: resourcesObjNum,
|
|
451
|
+
// Constant alpha needs a defined backdrop.
|
|
452
|
+
transparencyGroup: alphaValues.size > 0,
|
|
440
453
|
annotRefs: annotRefs.length > 0 ? annotRefs : undefined
|
|
441
454
|
});
|
|
442
455
|
pageObjNums.push(pageObjNum);
|
|
@@ -450,13 +463,18 @@ function renderSinglePage(page, fontManager, writer, fontObjectMap, chartCache,
|
|
|
450
463
|
});
|
|
451
464
|
}
|
|
452
465
|
}
|
|
453
|
-
function getOrWriteImageObject(writer, cache, data, format) {
|
|
454
|
-
|
|
466
|
+
function getOrWriteImageObject(writer, cache, data, format, grayscale = false) {
|
|
467
|
+
let variants = cache.get(data);
|
|
468
|
+
if (variants === undefined) {
|
|
469
|
+
variants = new Map();
|
|
470
|
+
cache.set(data, variants);
|
|
471
|
+
}
|
|
472
|
+
const existing = variants.get(grayscale);
|
|
455
473
|
if (existing !== undefined) {
|
|
456
474
|
return existing;
|
|
457
475
|
}
|
|
458
|
-
const objectNumber = writeImageXObject(writer, data, format);
|
|
459
|
-
|
|
476
|
+
const objectNumber = writeImageXObject(writer, data, format, grayscale);
|
|
477
|
+
variants.set(grayscale, objectNumber);
|
|
460
478
|
return objectNumber;
|
|
461
479
|
}
|
|
462
480
|
function headerFooterImagePositions(content, kind) {
|
|
@@ -540,6 +558,110 @@ function selectSheets(workbook, sheets) {
|
|
|
540
558
|
// =============================================================================
|
|
541
559
|
// Options Resolution
|
|
542
560
|
// =============================================================================
|
|
561
|
+
/**
|
|
562
|
+
* Parse an Excel "Columns to repeat at left" reference (`printTitlesColumn`)
|
|
563
|
+
* into an absolute column band, e.g. `"A:B"` → `{ first: 1, last: 2 }`.
|
|
564
|
+
*
|
|
565
|
+
* Returns `null` for anything that is not a column range.
|
|
566
|
+
*/
|
|
567
|
+
function parseTitlesColumnBand(ref) {
|
|
568
|
+
const match = ref
|
|
569
|
+
.replace(/\$/g, "")
|
|
570
|
+
.toUpperCase()
|
|
571
|
+
.match(/^([A-Z]+)(?::([A-Z]+))?$/);
|
|
572
|
+
if (!match) {
|
|
573
|
+
return null;
|
|
574
|
+
}
|
|
575
|
+
const first = columnLettersToNumber(match[1]);
|
|
576
|
+
const last = match[2] ? columnLettersToNumber(match[2]) : first;
|
|
577
|
+
if (last < first) {
|
|
578
|
+
return null;
|
|
579
|
+
}
|
|
580
|
+
return { first, last };
|
|
581
|
+
}
|
|
582
|
+
/**
|
|
583
|
+
* Parse an Excel "Rows to repeat at top" reference (`printTitlesRow`) into an
|
|
584
|
+
* absolute row band, e.g. `"1:3"` → `{ first: 1, last: 3 }`.
|
|
585
|
+
*/
|
|
586
|
+
function parseTitlesRowBand(ref) {
|
|
587
|
+
const match = ref.replace(/\$/g, "").match(/^(\d+)(?::(\d+))?$/);
|
|
588
|
+
if (!match) {
|
|
589
|
+
return null;
|
|
590
|
+
}
|
|
591
|
+
const first = parseInt(match[1], 10);
|
|
592
|
+
const last = match[2] ? parseInt(match[2], 10) : first;
|
|
593
|
+
if (first < 1 || last < first) {
|
|
594
|
+
return null;
|
|
595
|
+
}
|
|
596
|
+
return { first, last };
|
|
597
|
+
}
|
|
598
|
+
function columnLettersToNumber(letters) {
|
|
599
|
+
let n = 0;
|
|
600
|
+
for (let i = 0; i < letters.length; i++) {
|
|
601
|
+
n = n * 26 + (letters.charCodeAt(i) - 64);
|
|
602
|
+
}
|
|
603
|
+
return n;
|
|
604
|
+
}
|
|
605
|
+
function normalizePageOrder(value) {
|
|
606
|
+
return value === "overThenDown" ? "overThenDown" : "downThenOver";
|
|
607
|
+
}
|
|
608
|
+
/** Excel writes `None` / `asDisplayed` / `atEnd`. */
|
|
609
|
+
function normalizeCommentMode(value) {
|
|
610
|
+
if (value === "atEnd" || value === "asDisplayed") {
|
|
611
|
+
return value;
|
|
612
|
+
}
|
|
613
|
+
return "none";
|
|
614
|
+
}
|
|
615
|
+
function normalizeErrorMode(value) {
|
|
616
|
+
switch (value) {
|
|
617
|
+
case "blank":
|
|
618
|
+
case "dash":
|
|
619
|
+
case "NA":
|
|
620
|
+
return value;
|
|
621
|
+
default:
|
|
622
|
+
return "displayed";
|
|
623
|
+
}
|
|
624
|
+
}
|
|
625
|
+
/**
|
|
626
|
+
* Reconcile documonster's scaling options with Excel's two mutually exclusive
|
|
627
|
+
* scaling modes (Page Setup → Scaling):
|
|
628
|
+
*
|
|
629
|
+
* - **"Adjust to N %"** — `pageSetup.scale`, with `pageSetup.fitToPage` off.
|
|
630
|
+
* - **"Fit to N pages wide by M tall"** — `pageSetup.fitToWidth` /
|
|
631
|
+
* `fitToHeight`, with `pageSetup.fitToPage` on. Excel ignores `scale` here.
|
|
632
|
+
*
|
|
633
|
+
* Neither mode ever enlarges: Excel caps fit-to-page scaling at 100%.
|
|
634
|
+
*
|
|
635
|
+
* `PdfExportOptions.fitToPage` is documonster's own convenience default
|
|
636
|
+
* ("shrink to one page wide"). It only applies when neither the caller nor the
|
|
637
|
+
* sheet expresses a scaling intent, so a sheet that asks for 80% is not
|
|
638
|
+
* shrunk twice.
|
|
639
|
+
*/
|
|
640
|
+
function resolveScaling(options, ps) {
|
|
641
|
+
const clampPages = (n) => n === undefined || !Number.isFinite(n) || n <= 0 ? 0 : Math.floor(n);
|
|
642
|
+
const callerFit = options?.fitToWidth !== undefined || options?.fitToHeight !== undefined;
|
|
643
|
+
const sheetFitMode = ps?.fitToPage === true;
|
|
644
|
+
let fitToWidth = 0;
|
|
645
|
+
let fitToHeight = 0;
|
|
646
|
+
if (callerFit) {
|
|
647
|
+
fitToWidth = clampPages(options?.fitToWidth);
|
|
648
|
+
fitToHeight = clampPages(options?.fitToHeight);
|
|
649
|
+
}
|
|
650
|
+
else if (sheetFitMode && options?.scale === undefined && options?.fitToPage !== false) {
|
|
651
|
+
// `fitToPage: false` is the caller saying "do not auto-scale"; it has to
|
|
652
|
+
// beat the sheet's own fit-to-N mode, or an explicit option would lose to
|
|
653
|
+
// the workbook it is meant to override.
|
|
654
|
+
fitToWidth = clampPages(ps?.fitToWidth);
|
|
655
|
+
fitToHeight = clampPages(ps?.fitToHeight);
|
|
656
|
+
}
|
|
657
|
+
// A sheet in "Adjust to N %" mode with a non-default percentage.
|
|
658
|
+
const sheetScalePercent = !sheetFitMode && ps?.scale !== undefined && ps.scale !== 100 ? ps.scale / 100 : undefined;
|
|
659
|
+
// Excel's Page Setup allows 10–400%, so the ceiling is 4.0 rather than 3.0.
|
|
660
|
+
const scale = Math.max(0.1, Math.min(4.0, options?.scale ?? sheetScalePercent ?? 1.0));
|
|
661
|
+
const hasFitConstraint = fitToWidth > 0 || fitToHeight > 0;
|
|
662
|
+
const fitToPage = options?.fitToPage ?? !(hasFitConstraint || sheetScalePercent !== undefined || callerFit);
|
|
663
|
+
return { fitToPage, scale, fitToWidth, fitToHeight };
|
|
664
|
+
}
|
|
543
665
|
/**
|
|
544
666
|
* Resolve user options with defaults.
|
|
545
667
|
*/
|
|
@@ -560,40 +682,59 @@ function resolveOptions(options, sheet) {
|
|
|
560
682
|
chartsheetOrientation ??
|
|
561
683
|
(ps?.orientation === "landscape" ? "landscape" : "portrait");
|
|
562
684
|
const margins = resolveMargins(options?.margins, ps?.margins);
|
|
685
|
+
const blackAndWhite = options?.blackAndWhite ?? ps?.blackAndWhite ?? false;
|
|
563
686
|
const gridLineColorStr = options?.gridLineColor ?? "FFD0D0D0";
|
|
564
|
-
const
|
|
687
|
+
const gridLineColorRaw = argbToPdfColor(gridLineColorStr) ?? {
|
|
565
688
|
r: 0.816,
|
|
566
689
|
g: 0.816,
|
|
567
690
|
b: 0.816
|
|
568
691
|
};
|
|
569
|
-
|
|
570
|
-
//
|
|
571
|
-
// repeated
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
692
|
+
const gridLineColor = blackAndWhite ? toGrayscale(gridLineColorRaw) : gridLineColorRaw;
|
|
693
|
+
// Repeated title rows/columns fall back to the sheet's print titles. Only
|
|
694
|
+
// PdfSheetData carries them — chartsheets have no repeated bands. An
|
|
695
|
+
// explicitly passed `false` suppresses the sheet setting, so "not provided"
|
|
696
|
+
// (undefined) and "provided as off" must stay distinguishable.
|
|
697
|
+
const isWorksheet = Boolean(sheet) && !isPdfChartsheet(sheet);
|
|
698
|
+
// A caller-supplied count means "the first N rows/columns of the sheet"; the
|
|
699
|
+
// sheet's own print titles are absolute ranges that may sit outside the print
|
|
700
|
+
// area, so both normalise to an absolute band.
|
|
701
|
+
const countToBand = (n) => typeof n === "number" && n > 0 ? { first: 1, last: n } : false;
|
|
702
|
+
let repeatRows = false;
|
|
703
|
+
if (options?.repeatRows !== undefined) {
|
|
704
|
+
repeatRows = countToBand(options.repeatRows);
|
|
705
|
+
}
|
|
706
|
+
else if (isWorksheet && ps?.printTitlesRow) {
|
|
707
|
+
repeatRows = parseTitlesRowBand(ps.printTitlesRow) ?? false;
|
|
708
|
+
}
|
|
709
|
+
let repeatCols = false;
|
|
710
|
+
if (options?.repeatCols !== undefined) {
|
|
711
|
+
repeatCols = countToBand(options.repeatCols);
|
|
712
|
+
}
|
|
713
|
+
else if (isWorksheet && ps?.printTitlesColumn) {
|
|
714
|
+
repeatCols = parseTitlesColumnBand(ps.printTitlesColumn) ?? false;
|
|
715
|
+
}
|
|
716
|
+
const { fitToPage, scale, fitToWidth, fitToHeight } = resolveScaling(options, ps);
|
|
580
717
|
return {
|
|
581
718
|
pageSize,
|
|
582
719
|
orientation,
|
|
583
720
|
margins,
|
|
584
721
|
ignorePrintArea: options?.ignorePrintArea ?? false,
|
|
585
|
-
fitToPage
|
|
586
|
-
scale
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
((options?.fitToPage !== undefined ? options.fitToPage : true)
|
|
590
|
-
? 1.0
|
|
591
|
-
: ps?.scale
|
|
592
|
-
? ps.scale / 100
|
|
593
|
-
: 1.0))),
|
|
722
|
+
fitToPage,
|
|
723
|
+
scale,
|
|
724
|
+
fitToWidth,
|
|
725
|
+
fitToHeight,
|
|
594
726
|
showGridLines: options?.showGridLines ?? ps?.showGridLines ?? false,
|
|
595
727
|
gridLineColor,
|
|
728
|
+
showRowColHeaders: options?.showRowColHeaders ?? ps?.showRowColHeaders ?? false,
|
|
729
|
+
horizontalCentered: options?.horizontalCentered ?? ps?.horizontalCentered ?? false,
|
|
730
|
+
verticalCentered: options?.verticalCentered ?? ps?.verticalCentered ?? false,
|
|
731
|
+
pageOrder: options?.pageOrder ?? normalizePageOrder(ps?.pageOrder),
|
|
732
|
+
blackAndWhite,
|
|
733
|
+
draft: options?.draft ?? ps?.draft ?? false,
|
|
734
|
+
errors: options?.errors ?? normalizeErrorMode(ps?.errors),
|
|
735
|
+
cellComments: options?.cellComments ?? normalizeCommentMode(ps?.cellComments),
|
|
596
736
|
repeatRows,
|
|
737
|
+
repeatCols,
|
|
597
738
|
defaultFontFamily: options?.defaultFontFamily ?? "Helvetica",
|
|
598
739
|
defaultFontSize: options?.defaultFontSize ?? 11,
|
|
599
740
|
showSheetNames: options?.showSheetNames ?? false,
|
|
@@ -27,6 +27,17 @@ export declare function excelColorToPdf(color: Partial<PdfColorData> | undefined
|
|
|
27
27
|
* @see OOXML §18.8.19 - tint formula
|
|
28
28
|
*/
|
|
29
29
|
export declare function applyTint(color: PdfColor, tint: number): PdfColor;
|
|
30
|
+
/**
|
|
31
|
+
* Convert a color to its luminance-preserving grayscale equivalent, using the
|
|
32
|
+
* Rec. 601 luma weights. Backs Excel's "Black and white" print option: hues are
|
|
33
|
+
* discarded while relative lightness — and therefore contrast — is kept.
|
|
34
|
+
*
|
|
35
|
+
* Opacity is carried through untouched; dropping it would make transparent
|
|
36
|
+
* chart fills opaque.
|
|
37
|
+
*/
|
|
38
|
+
export declare function toGrayscale(color: PdfColor): PdfColor;
|
|
39
|
+
/** Grayscale every edge of a border set, preserving `null` edges. */
|
|
40
|
+
export declare function grayscaleBorders(borders: LayoutBorders): LayoutBorders;
|
|
30
41
|
/**
|
|
31
42
|
* Default colors used in PDF rendering.
|
|
32
43
|
*/
|
|
@@ -165,6 +165,30 @@ function indexedColorToPdf(index) {
|
|
|
165
165
|
export function applyTint(color, tint) {
|
|
166
166
|
return applyTintRgb01(color, tint);
|
|
167
167
|
}
|
|
168
|
+
/**
|
|
169
|
+
* Convert a color to its luminance-preserving grayscale equivalent, using the
|
|
170
|
+
* Rec. 601 luma weights. Backs Excel's "Black and white" print option: hues are
|
|
171
|
+
* discarded while relative lightness — and therefore contrast — is kept.
|
|
172
|
+
*
|
|
173
|
+
* Opacity is carried through untouched; dropping it would make transparent
|
|
174
|
+
* chart fills opaque.
|
|
175
|
+
*/
|
|
176
|
+
export function toGrayscale(color) {
|
|
177
|
+
const luma = 0.299 * color.r + 0.587 * color.g + 0.114 * color.b;
|
|
178
|
+
return color.a === undefined
|
|
179
|
+
? { r: luma, g: luma, b: luma }
|
|
180
|
+
: { r: luma, g: luma, b: luma, a: color.a };
|
|
181
|
+
}
|
|
182
|
+
/** Grayscale every edge of a border set, preserving `null` edges. */
|
|
183
|
+
export function grayscaleBorders(borders) {
|
|
184
|
+
const convert = (b) => b === null ? null : { ...b, color: toGrayscale(b.color) };
|
|
185
|
+
return {
|
|
186
|
+
top: convert(borders.top),
|
|
187
|
+
right: convert(borders.right),
|
|
188
|
+
bottom: convert(borders.bottom),
|
|
189
|
+
left: convert(borders.left)
|
|
190
|
+
};
|
|
191
|
+
}
|
|
168
192
|
/**
|
|
169
193
|
* Default colors used in PDF rendering.
|
|
170
194
|
*/
|