This commit is contained in:
iqudoo
2026-06-14 02:52:51 +08:00
parent 6cdae2f69c
commit fff6cb98ae
8 changed files with 325 additions and 256 deletions

View File

@@ -19,14 +19,13 @@ import {
} from './elementBox';
import {
iterTableCells,
getCellRectMm,
getCellAnchor,
getCellData,
getEffectiveCellStyle,
getTableRows,
getTableCols,
getTableRowHeights,
getTableColWidths,
buildTableGridBoundariesPx,
getCellRectPx,
} from './tableUtils';
const GENERATOR_TRANSPARENT_BG = '#00000000';
@@ -90,6 +89,34 @@ function loadImage(src: string): Promise<HTMLImageElement> {
});
}
function loadImageOrNull(src: string): Promise<HTMLImageElement | null> {
const trimmed = src.trim();
if (!trimmed) return Promise.resolve(null);
return loadImage(trimmed).catch(() => null);
}
async function resolveElementImage(
elem: TemplateElement,
primarySrc: string,
mode: ContentResolveMode
): Promise<{ img: HTMLImageElement | null; showPlaceholder: boolean }> {
const primary = primarySrc.trim();
const fallback = (elem.defaultImage ?? '').trim();
let img = await loadImageOrNull(primary);
if (!img && fallback) {
img = await loadImageOrNull(fallback);
}
if (img) return { img, showPlaceholder: false };
if (mode === 'design' && !primary && !fallback) {
return { img: null, showPlaceholder: true };
}
return { img: null, showPlaceholder: false };
}
function drawImageWithFit(
ctx: CanvasRenderingContext2D,
img: HTMLImageElement,
@@ -226,7 +253,7 @@ function layoutVerticalTextColumns(
return columns;
}
function drawTextInRect(
function drawTextContentInBox(
ctx: CanvasRenderingContext2D,
elem: TemplateElement,
boxX: number,
@@ -236,9 +263,15 @@ function drawTextInRect(
value: string,
scale: number
) {
const pad = 2 * scale;
const writableW = Math.max(1, boxW - pad * 2);
const writableH = Math.max(1, boxH - pad * 2);
const pad = resolvePaddingMm(elem);
const padPx = {
top: Math.round(pad.top * scale),
right: Math.round(pad.right * scale),
bottom: Math.round(pad.bottom * scale),
left: Math.round(pad.left * scale),
};
const writableW = Math.max(1, boxW - padPx.left - padPx.right);
const writableH = Math.max(1, boxH - padPx.top - padPx.bottom);
const fontStyle = elem.fontStyle === 'italic' ? 'italic ' : '';
const fontWeight = elem.fontWeight === 'bold' ? 'bold ' : '';
@@ -267,8 +300,8 @@ function drawTextInRect(
const textScaleX = Math.max(0.1, Math.min(5, elem.textScaleX ?? 1));
const textScaleY = Math.max(0.1, Math.min(5, elem.textScaleY ?? 1));
const transformOriginX = boxX + boxW / 2;
const transformOriginY = boxY + boxH / 2;
const transformOriginX = boxX + padPx.left + writableW / 2;
const transformOriginY = boxY + padPx.top + writableH / 2;
ctx.save();
if (textScaleX !== 1 || textScaleY !== 1) {
@@ -285,11 +318,11 @@ function drawTextInRect(
let startX: number;
if (elem.textAlign === 'center') {
startX = boxX + pad + Math.max(0, (writableW - totalColumnsWidth) / 2) + columnWidth / 2;
startX = boxX + padPx.left + Math.max(0, (writableW - totalColumnsWidth) / 2) + columnWidth / 2;
} else if (elem.textAlign === 'right') {
startX = boxX + pad + Math.max(0, writableW - totalColumnsWidth) + columnWidth / 2;
startX = boxX + padPx.left + Math.max(0, writableW - totalColumnsWidth) + columnWidth / 2;
} else {
startX = boxX + pad + columnWidth / 2;
startX = boxX + padPx.left + columnWidth / 2;
}
ctx.textAlign = 'center';
@@ -298,17 +331,18 @@ function drawTextInRect(
const colTextHeight = column.length * lineHeight;
let colStartY: number;
if (vAlign === 'top') {
colStartY = boxY + pad + lineHeight / 2;
colStartY = boxY + padPx.top + lineHeight / 2;
} else if (vAlign === 'bottom') {
colStartY = boxY + boxH - pad - colTextHeight + lineHeight / 2;
colStartY = boxY + boxH - padPx.bottom - colTextHeight + lineHeight / 2;
} else {
colStartY = boxY + pad + Math.max(0, (writableH - colTextHeight) / 2) + lineHeight / 2;
colStartY =
boxY + padPx.top + Math.max(0, (writableH - colTextHeight) / 2) + lineHeight / 2;
}
const colX = startX + col * columnWidth;
for (let i = 0; i < column.length; i++) {
const charY = colStartY + i * lineHeight;
if (charY - lineHeight / 2 <= boxY + boxH - pad) {
if (charY - lineHeight / 2 <= boxY + boxH - padPx.bottom) {
ctx.fillText(column[i], colX, charY);
}
}
@@ -317,31 +351,31 @@ function drawTextInRect(
const lines = layoutHorizontalTextLines(ctx, String(value), writableW, wordWrap);
const totalTextHeight = lines.length * lineHeight;
let textX = boxX + pad;
let textX = boxX + padPx.left;
let textAlign: CanvasTextAlign = 'left';
if (elem.textAlign === 'center') {
textX = boxX + boxW / 2;
textX = boxX + padPx.left + writableW / 2;
textAlign = 'center';
} else if (elem.textAlign === 'right') {
textX = boxX + boxW - pad;
textX = boxX + padPx.left + (writableW - 2);
textAlign = 'right';
} else {
textX = boxX + pad + 2;
textX = boxX + padPx.left + 2;
}
ctx.textAlign = textAlign;
let startY: number;
if (vAlign === 'top') {
startY = boxY + pad + lineHeight / 2;
startY = boxY + padPx.top + lineHeight / 2;
} else if (vAlign === 'bottom') {
startY = boxY + boxH - pad - totalTextHeight + lineHeight / 2;
startY = boxY + boxH - padPx.bottom - totalTextHeight + lineHeight / 2;
} else {
startY = boxY + pad + Math.max(0, (writableH - totalTextHeight) / 2) + lineHeight / 2;
startY = boxY + padPx.top + Math.max(0, (writableH - totalTextHeight) / 2) + lineHeight / 2;
}
for (let i = 0; i < lines.length; i++) {
const lineY = startY + i * lineHeight;
if (lineY - lineHeight / 2 <= boxY + boxH - pad) {
if (lineY - lineHeight / 2 <= boxY + boxH - padPx.bottom) {
const line = lines[i];
ctx.fillText(line, textX, lineY);
drawTextLineDecorations(ctx, elem, textX, lineY, line, fontSizePx, textAlign);
@@ -376,19 +410,12 @@ function drawTableGridLines(
const borderColor = elem.tableBorderColor ?? '#374151';
const rows = getTableRows(elem);
const cols = getTableCols(elem);
const rowHeights = getTableRowHeights(elem);
const colWidths = getTableColWidths(elem);
const pad = resolvePaddingMm(elem);
const { rowBounds, colBounds } = buildTableGridBoundariesPx(elem, scale, ex, ey);
const x0 = pad.left;
const y0 = pad.top;
const totalW = colWidths.reduce((a, b) => a + b, 0);
const totalH = rowHeights.reduce((a, b) => a + b, 0);
const left = ex + x0 * scale;
const top = ey + y0 * scale;
const width = totalW * scale;
const height = totalH * scale;
const left = colBounds[0];
const top = rowBounds[0];
const width = colBounds[colBounds.length - 1] - left;
const height = rowBounds[rowBounds.length - 1] - top;
ctx.strokeStyle = borderColor;
ctx.lineWidth = borderW;
@@ -402,45 +429,31 @@ function drawTableGridLines(
Math.max(0, height - borderW)
);
let yAcc = y0;
for (let r = 1; r < rows; r++) {
yAcc += rowHeights[r - 1];
const y = alignStrokeCoord(ey + yAcc * scale);
let xAcc = x0;
const y = alignStrokeCoord(rowBounds[r]);
for (let c = 0; c < cols; c++) {
const w = colWidths[c];
const anchorAbove = getCellAnchor(elem, r - 1, c);
const anchorBelow = getCellAnchor(elem, r, c);
const sameMerge =
anchorAbove.row === anchorBelow.row && anchorAbove.col === anchorBelow.col;
if (!sameMerge) {
const x1 = alignStrokeCoord(ex + xAcc * scale);
const x2 = alignStrokeCoord(ex + (xAcc + w) * scale);
ctx.moveTo(x1, y);
ctx.lineTo(x2, y);
ctx.moveTo(alignStrokeCoord(colBounds[c]), y);
ctx.lineTo(alignStrokeCoord(colBounds[c + 1]), y);
}
xAcc += w;
}
}
let xAcc = x0;
for (let c = 1; c < cols; c++) {
xAcc += colWidths[c - 1];
const x = alignStrokeCoord(ex + xAcc * scale);
let yAcc2 = y0;
const x = alignStrokeCoord(colBounds[c]);
for (let r = 0; r < rows; r++) {
const h = rowHeights[r];
const anchorLeft = getCellAnchor(elem, r, c - 1);
const anchorRight = getCellAnchor(elem, r, c);
const sameMerge =
anchorLeft.row === anchorRight.row && anchorLeft.col === anchorRight.col;
if (!sameMerge) {
const y1 = alignStrokeCoord(ey + yAcc2 * scale);
const y2 = alignStrokeCoord(ey + (yAcc2 + h) * scale);
ctx.moveTo(x, y1);
ctx.lineTo(x, y2);
ctx.moveTo(x, alignStrokeCoord(rowBounds[r]));
ctx.lineTo(x, alignStrokeCoord(rowBounds[r + 1]));
}
yAcc2 += h;
}
}
@@ -466,19 +479,26 @@ async function drawTableElement(
drawElementFill(ctx, ex, ey, ew, eh, elem, radii);
drawElementBorders(ctx, ex, ey, ew, eh, radii, sides, elem, scale);
const inner = getPaddedRectPx(ex, ey, ew, eh, elem, scale);
const { rowBounds, colBounds } = buildTableGridBoundariesPx(elem, scale, ex, ey);
const gridX = colBounds[0];
const gridY = rowBounds[0];
const gridW = colBounds[colBounds.length - 1] - gridX;
const gridH = rowBounds[rowBounds.length - 1] - gridY;
ctx.save();
ctx.beginPath();
ctx.rect(inner.x, inner.y, inner.w, inner.h);
ctx.rect(gridX, gridY, gridW, gridH);
ctx.clip();
const anchors = iterTableCells(elem);
for (const { row, col } of anchors) {
const rect = getCellRectMm(elem, row, col);
const cellX = ex + Math.round(rect.x * scale);
const cellY = ey + Math.round(rect.y * scale);
const cellW = Math.round(rect.width * scale);
const cellH = Math.round(rect.height * scale);
const { x: cellX, y: cellY, width: cellW, height: cellH } = getCellRectPx(
elem,
row,
col,
scale,
ex,
ey
);
const cellData = getCellData(elem, row, col);
if (cellData?.backgroundColor && cellData.backgroundColor !== 'transparent') {
@@ -493,7 +513,23 @@ async function drawTableElement(
const displayText = String(cellValue ?? '').trim();
if (!displayText) continue;
drawTextInRect(ctx, textElem, cellX, cellY, cellW, cellH, displayText, scale);
drawTextContentInBox(
ctx,
{
...textElem,
padding: 0,
paddingTop: 0,
paddingRight: 0,
paddingBottom: 0,
paddingLeft: 0,
},
cellX,
cellY,
cellW,
cellH,
displayText,
scale
);
}
drawTableGridLines(ctx, elem, ex, ey, scale);
@@ -561,8 +597,8 @@ export async function renderLabelToDataUrl(options: RenderLabelOptions): Promise
const scale = scaleOption;
const canvas = document.createElement('canvas');
canvas.width = Math.max(50, Math.round(widthMm * scale));
canvas.height = Math.max(50, Math.round(heightMm * scale));
canvas.width = Math.max(1, Math.round(widthMm * scale));
canvas.height = Math.max(1, Math.round(heightMm * scale));
const ctx = canvas.getContext('2d');
if (!ctx) return '';
@@ -618,133 +654,8 @@ export async function renderLabelToDataUrl(options: RenderLabelOptions): Promise
drawElementFill(ctx, ex, ey, ew, eh, elem, radii);
const padPx = (() => {
const pad = resolvePaddingMm(elem);
return {
top: Math.round(pad.top * scale),
right: Math.round(pad.right * scale),
bottom: Math.round(pad.bottom * scale),
left: Math.round(pad.left * scale),
};
})();
const writableW = Math.max(1, ew - padPx.left - padPx.right);
const writableH = Math.max(1, eh - padPx.top - padPx.bottom);
drawTextContentInBox(ctx, elem, ex, ey, ew, eh, String(value), scale);
const fontStyle = elem.fontStyle === 'italic' ? 'italic ' : '';
const fontWeight = elem.fontWeight === 'bold' ? 'bold ' : '';
const fontSizePx = Math.max(6, Math.max(1, Math.round(elem.fontSize * 0.352777 * scale)));
const fontFamily =
elem.fontFamily === 'mono'
? 'JetBrains Mono, SFMono-Regular, Consolas, Courier New, monospace'
: elem.fontFamily === 'serif'
? 'Georgia, Times New Roman, serif'
: 'Inter, system-ui, -apple-system, Arial, sans-serif';
ctx.font = `${fontStyle}${fontWeight}${fontSizePx}px ${fontFamily}`.trim();
ctx.fillStyle = elem.textColor || '#111827';
const letterSpacingPx = Math.max(0, (elem.letterSpacing ?? 0) * scale);
if (letterSpacingPx > 0 && 'letterSpacing' in ctx) {
(ctx as CanvasRenderingContext2D & { letterSpacing: string }).letterSpacing =
`${letterSpacingPx}px`;
}
const wordWrap = elem.wordWrap !== false;
const lineHeight = fontSizePx * 1.25;
const columnWidth = lineHeight;
const isVertical = elem.textWritingMode === 'vertical';
ctx.textBaseline = 'middle';
const textScaleX = Math.max(0.1, Math.min(5, elem.textScaleX ?? 1));
const textScaleY = Math.max(0.1, Math.min(5, elem.textScaleY ?? 1));
const transformOriginX = ex + padPx.left + writableW / 2;
const transformOriginY = ey + padPx.top + writableH / 2;
ctx.save();
if (textScaleX !== 1 || textScaleY !== 1) {
ctx.translate(transformOriginX, transformOriginY);
ctx.scale(textScaleX, textScaleY);
ctx.translate(-transformOriginX, -transformOriginY);
}
const vAlign = elem.verticalAlign ?? 'middle';
if (isVertical) {
const columns = layoutVerticalTextColumns(String(value), writableH, lineHeight, wordWrap);
const totalColumnsWidth = columns.length * columnWidth;
let startX: number;
if (elem.textAlign === 'center') {
startX = ex + padPx.left + Math.max(0, (writableW - totalColumnsWidth) / 2) + columnWidth / 2;
} else if (elem.textAlign === 'right') {
startX = ex + padPx.left + Math.max(0, writableW - totalColumnsWidth) + columnWidth / 2;
} else {
startX = ex + padPx.left + columnWidth / 2;
}
ctx.textAlign = 'center';
for (let col = 0; col < columns.length; col++) {
const column = columns[col];
const colTextHeight = column.length * lineHeight;
let colStartY: number;
if (vAlign === 'top') {
colStartY = ey + padPx.top + lineHeight / 2;
} else if (vAlign === 'bottom') {
colStartY = ey + eh - padPx.bottom - colTextHeight + lineHeight / 2;
} else {
colStartY =
ey + padPx.top + Math.max(0, (writableH - colTextHeight) / 2) + lineHeight / 2;
}
const colX = startX + col * columnWidth;
for (let i = 0; i < column.length; i++) {
const charY = colStartY + i * lineHeight;
if (charY - lineHeight / 2 <= ey + eh - padPx.bottom) {
ctx.fillText(column[i], colX, charY);
}
}
}
} else {
const lines = layoutHorizontalTextLines(ctx, String(value), writableW, wordWrap);
const totalTextHeight = lines.length * lineHeight;
let textX = ex + padPx.left;
let textAlign: CanvasTextAlign = 'left';
if (elem.textAlign === 'center') {
textX = ex + padPx.left + writableW / 2;
textAlign = 'center';
} else if (elem.textAlign === 'right') {
textX = ex + padPx.left + (writableW - 2);
textAlign = 'right';
} else {
textX = ex + padPx.left + 2;
}
ctx.textAlign = textAlign;
let startY: number;
if (vAlign === 'top') {
startY = ey + padPx.top + lineHeight / 2;
} else if (vAlign === 'bottom') {
startY = ey + eh - padPx.bottom - totalTextHeight + lineHeight / 2;
} else {
startY =
ey + padPx.top + Math.max(0, (writableH - totalTextHeight) / 2) + lineHeight / 2;
}
for (let i = 0; i < lines.length; i++) {
const lineY = startY + i * lineHeight;
if (lineY - lineHeight / 2 <= ey + eh - padPx.bottom) {
const line = lines[i];
ctx.fillText(line, textX, lineY);
drawTextLineDecorations(ctx, elem, textX, lineY, line, fontSizePx, textAlign);
}
}
}
ctx.restore();
if (letterSpacingPx > 0 && 'letterSpacing' in ctx) {
(ctx as CanvasRenderingContext2D & { letterSpacing: string }).letterSpacing = '0px';
}
ctx.restore();
drawElementBorders(ctx, ex, ey, ew, eh, radii, sides, elem, scale);
} else if (elem.type === 'barcode') {
@@ -813,22 +724,20 @@ export async function renderLabelToDataUrl(options: RenderLabelOptions): Promise
}
}
} else if (elem.type === 'image') {
const primarySrc = value ? String(value).trim() : '';
const { img, showPlaceholder } = await resolveElementImage(elem, primarySrc, mode);
if (!img && !showPlaceholder) {
ctx.restore();
continue;
}
drawElementChrome(ctx, ex, ey, ew, eh, elem, scale);
const inner = getPaddedRectPx(ex, ey, ew, eh, elem, scale);
const src = value ? String(value).trim() : '';
if (!src) {
if (mode === 'design') {
drawImagePlaceholder(ctx, inner.x, inner.y, inner.w, inner.h, '图片');
}
if (img) {
drawImageWithFit(ctx, img, inner.x, inner.y, inner.w, inner.h, elem.imageFit ?? 'contain');
} else {
try {
const img = await loadImage(src);
drawImageWithFit(ctx, img, inner.x, inner.y, inner.w, inner.h, elem.imageFit ?? 'contain');
} catch {
if (mode === 'design') {
drawImagePlaceholder(ctx, inner.x, inner.y, inner.w, inner.h, '加载失败');
}
}
drawImagePlaceholder(ctx, inner.x, inner.y, inner.w, inner.h, '图片');
}
} else if (elem.type === 'table') {
await drawTableElement(