This commit is contained in:
iqudoo
2026-06-12 20:58:51 +08:00
parent c9a187bf9b
commit 239e2eeebd
9 changed files with 499 additions and 340 deletions

View File

@@ -10,6 +10,8 @@ import {
import {
resolveCornerRadiiPx,
resolveBorderSides,
resolvePaddingMm,
getPaddedRectPx,
traceRoundedRect,
drawElementFill,
drawElementBorders,
@@ -213,23 +215,6 @@ function layoutVerticalTextColumns(
return columns;
}
function getPaddedRect(
x: number,
y: number,
w: number,
h: number,
paddingMm: number,
scale: number
) {
const paddingPx = Math.round(paddingMm * scale);
return {
x: x + paddingPx,
y: y + paddingPx,
w: Math.max(1, w - 2 * paddingPx),
h: Math.max(1, h - 2 * paddingPx),
};
}
function drawImagePlaceholder(
ctx: CanvasRenderingContext2D,
x: number,
@@ -343,10 +328,17 @@ export async function renderLabelToDataUrl(options: RenderLabelOptions): Promise
drawElementFill(ctx, ex, ey, ew, eh, elem, radii);
const paddingMm = elem.padding || 0;
const paddingPx = Math.round(paddingMm * scale);
const writableW = Math.max(1, ew - 2 * paddingPx);
const writableH = Math.max(1, eh - 2 * paddingPx);
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);
const fontStyle = elem.fontStyle === 'italic' ? 'italic ' : '';
const fontWeight = elem.fontWeight === 'bold' ? 'bold ' : '';
@@ -375,8 +367,8 @@ export async function renderLabelToDataUrl(options: RenderLabelOptions): Promise
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 + paddingPx + writableW / 2;
const transformOriginY = ey + paddingPx + writableH / 2;
const transformOriginX = ex + padPx.left + writableW / 2;
const transformOriginY = ey + padPx.top + writableH / 2;
ctx.save();
if (textScaleX !== 1 || textScaleY !== 1) {
@@ -393,11 +385,11 @@ export async function renderLabelToDataUrl(options: RenderLabelOptions): Promise
let startX: number;
if (elem.textAlign === 'center') {
startX = ex + paddingPx + Math.max(0, (writableW - totalColumnsWidth) / 2) + columnWidth / 2;
startX = ex + padPx.left + Math.max(0, (writableW - totalColumnsWidth) / 2) + columnWidth / 2;
} else if (elem.textAlign === 'right') {
startX = ex + paddingPx + Math.max(0, writableW - totalColumnsWidth) + columnWidth / 2;
startX = ex + padPx.left + Math.max(0, writableW - totalColumnsWidth) + columnWidth / 2;
} else {
startX = ex + paddingPx + columnWidth / 2;
startX = ex + padPx.left + columnWidth / 2;
}
ctx.textAlign = 'center';
@@ -406,18 +398,18 @@ export async function renderLabelToDataUrl(options: RenderLabelOptions): Promise
const colTextHeight = column.length * lineHeight;
let colStartY: number;
if (vAlign === 'top') {
colStartY = ey + paddingPx + lineHeight / 2;
colStartY = ey + padPx.top + lineHeight / 2;
} else if (vAlign === 'bottom') {
colStartY = ey + eh - paddingPx - colTextHeight + lineHeight / 2;
colStartY = ey + eh - padPx.bottom - colTextHeight + lineHeight / 2;
} else {
colStartY =
ey + paddingPx + Math.max(0, (writableH - colTextHeight) / 2) + lineHeight / 2;
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 - paddingPx) {
if (charY - lineHeight / 2 <= ey + eh - padPx.bottom) {
ctx.fillText(column[i], colX, charY);
}
}
@@ -426,32 +418,32 @@ export async function renderLabelToDataUrl(options: RenderLabelOptions): Promise
const lines = layoutHorizontalTextLines(ctx, String(value), writableW, wordWrap);
const totalTextHeight = lines.length * lineHeight;
let textX = ex + paddingPx;
let textX = ex + padPx.left;
let textAlign: CanvasTextAlign = 'left';
if (elem.textAlign === 'center') {
textX = ex + paddingPx + writableW / 2;
textX = ex + padPx.left + writableW / 2;
textAlign = 'center';
} else if (elem.textAlign === 'right') {
textX = ex + paddingPx + (writableW - 2);
textX = ex + padPx.left + (writableW - 2);
textAlign = 'right';
} else {
textX = ex + paddingPx + 2;
textX = ex + padPx.left + 2;
}
ctx.textAlign = textAlign;
let startY: number;
if (vAlign === 'top') {
startY = ey + paddingPx + lineHeight / 2;
startY = ey + padPx.top + lineHeight / 2;
} else if (vAlign === 'bottom') {
startY = ey + eh - paddingPx - totalTextHeight + lineHeight / 2;
startY = ey + eh - padPx.bottom - totalTextHeight + lineHeight / 2;
} else {
startY =
ey + paddingPx + Math.max(0, (writableH - totalTextHeight) / 2) + lineHeight / 2;
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 - paddingPx) {
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);
@@ -468,7 +460,7 @@ export async function renderLabelToDataUrl(options: RenderLabelOptions): Promise
} else if (elem.type === 'barcode') {
try {
drawElementChrome(ctx, ex, ey, ew, eh, elem, scale);
const inner = getPaddedRect(ex, ey, ew, eh, elem.padding || 0, scale);
const inner = getPaddedRectPx(ex, ey, ew, eh, elem, scale);
const tempCanvas = document.createElement('canvas');
const barcodeValue = value ? String(value).trim() : mode === 'design' ? 'SAMPLE' : '';
@@ -488,7 +480,7 @@ export async function renderLabelToDataUrl(options: RenderLabelOptions): Promise
} catch (error) {
console.warn('Barcode drawing failed', error);
if (mode === 'design') {
const inner = getPaddedRect(ex, ey, ew, eh, elem.padding || 0, scale);
const inner = getPaddedRectPx(ex, ey, ew, eh, elem, scale);
ctx.fillStyle = '#f3f4f6';
ctx.fillRect(inner.x, inner.y, inner.w, inner.h);
ctx.strokeStyle = '#ef4444';
@@ -503,7 +495,7 @@ export async function renderLabelToDataUrl(options: RenderLabelOptions): Promise
} else if (elem.type === 'qrcode') {
try {
drawElementChrome(ctx, ex, ey, ew, eh, elem, scale);
const inner = getPaddedRect(ex, ey, ew, eh, elem.padding || 0, scale);
const inner = getPaddedRectPx(ex, ey, ew, eh, elem, scale);
const qrValue = value ? String(value).trim() : mode === 'design' ? 'SAMPLE QR' : '';
if (qrValue) {
@@ -522,7 +514,7 @@ export async function renderLabelToDataUrl(options: RenderLabelOptions): Promise
} catch (error) {
console.warn('QR code drawing failed', error);
if (mode === 'design') {
const inner = getPaddedRect(ex, ey, ew, eh, elem.padding || 0, scale);
const inner = getPaddedRectPx(ex, ey, ew, eh, elem, scale);
ctx.fillStyle = '#f3f4f6';
ctx.fillRect(inner.x, inner.y, inner.w, inner.h);
ctx.strokeStyle = '#ef4444';
@@ -532,7 +524,7 @@ export async function renderLabelToDataUrl(options: RenderLabelOptions): Promise
}
} else if (elem.type === 'image') {
drawElementChrome(ctx, ex, ey, ew, eh, elem, scale);
const inner = getPaddedRect(ex, ey, ew, eh, elem.padding || 0, scale);
const inner = getPaddedRectPx(ex, ey, ew, eh, elem, scale);
const src = value ? String(value).trim() : '';
if (!src) {
if (mode === 'design') {