From 33d3e5537c7b828f1d06b4c6d8809d679df7e923 Mon Sep 17 00:00:00 2001 From: iqudoo Date: Tue, 23 Jun 2026 15:26:51 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BC=98=E5=8C=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/App.tsx | 17 +++---- src/colorUtils.ts | 77 +++++++++++++++++++++++------ src/components/CanvasLabelImage.tsx | 2 +- src/components/LabelDesigner.tsx | 2 +- src/components/PreviewGrid.tsx | 8 +-- src/labelRenderer.ts | 8 +-- src/pdfExport.ts | 2 +- src/types.ts | 4 +- src/utils.ts | 2 +- 9 files changed, 79 insertions(+), 43 deletions(-) diff --git a/src/App.tsx b/src/App.tsx index bee5e9f..2274ab2 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -770,19 +770,14 @@ export default function App() { ]); const pdfExportImage = useMemo(() => { - const printMode = workingTemplate.paper?.printColorMode ?? 'rgb'; - const needsLosslessColor = printMode !== 'rgb'; - const hasCode = workingTemplate.elements.some( - (e) => e.type === 'barcode' || e.type === 'qrcode' - ); - const usePng = needsLosslessColor || hasCode; + const printMode = workingTemplate.paper?.printColorMode ?? 'cmyk'; return { - imageFormat: usePng ? ('png' as const) : ('jpeg' as const), - jsPdfFormat: usePng ? ('PNG' as const) : ('JPEG' as const), - scale: needsLosslessColor ? 12 : 10, + imageFormat: 'png' as const, + jsPdfFormat: 'PNG' as const, + scale: printMode === 'cmyk' ? 14 : 12, jpegQuality: 0.95, }; - }, [workingTemplate.elements, workingTemplate.paper?.printColorMode]); + }, [workingTemplate.paper?.printColorMode]); const handleExportImageFileNamePatternChange = (pattern: string) => { if (!activeTemplate) return; @@ -830,7 +825,7 @@ export default function App() { scale: pdfExportImage.scale, imageFormat: pdfExportImage.imageFormat, jpegQuality: pdfExportImage.jpegQuality, - printColorMode: activeTemplate!.paper?.printColorMode ?? 'rgb', + printColorMode: activeTemplate!.paper?.printColorMode ?? 'cmyk', ...resolveReferenceBackgroundForOutput(activeTemplate!), }), [activeTemplate, pdfExportImage] diff --git a/src/colorUtils.ts b/src/colorUtils.ts index 2e77eba..a979a46 100644 --- a/src/colorUtils.ts +++ b/src/colorUtils.ts @@ -19,6 +19,10 @@ export function isTransparentColorValue(value?: string): boolean { return trimmed === '' || trimmed === 'transparent'; } +function clampByte(n: number): number { + return Math.max(0, Math.min(255, Math.round(n))); +} + function parseHexColor(color: string): { r: number; g: number; b: number; a: number } | null { const trimmed = color.trim(); if (!trimmed.startsWith('#')) return null; @@ -83,37 +87,81 @@ function rgbToCmyk(r: number, g: number, b: number) { function cmykToRgb(c: number, m: number, y: number, k: number) { return { - r: Math.round(255 * (1 - c) * (1 - k)), - g: Math.round(255 * (1 - m) * (1 - k)), - b: Math.round(255 * (1 - y) * (1 - k)), + r: 255 * (1 - c) * (1 - k), + g: 255 * (1 - m) * (1 - k), + b: 255 * (1 - y) * (1 - k), }; } -/** 总墨量限制,避免过饱和(典型印刷 TAC ≈ 280–300%) */ -function limitTotalInk(c: number, m: number, y: number, k: number, maxInk = 2.85) { +/** 灰成分替换(GCR),中性色更多走 K 通道,印刷更稳定 */ +function applyGrayComponentReplacement(c: number, m: number, y: number, k: number) { + const gray = Math.min(c, m, y); + if (gray <= 0.01) return { c, m, y, k }; + const gcr = gray * 0.85; + return { + c: c - gcr, + m: m - gcr, + y: y - gcr, + k: Math.min(1, k + gcr), + }; +} + +/** 总墨量限制(TAC),典型 280–300% */ +function limitTotalInk(c: number, m: number, y: number, k: number, maxInk = 2.8) { const total = c + m + y + k; if (total <= maxInk || total <= 0) return { c, m, y, k }; const scale = maxInk / total; return { c: c * scale, m: m * scale, y: y * scale, k: k * scale }; } +/** 网点扩大 / 纸张吸墨:印刷比屏幕略深 */ +function applyDotGain(r: number, g: number, b: number, amount: number): [number, number, number] { + const factor = 1 - amount; + return [clampByte(r * factor), clampByte(g * factor), clampByte(b * factor)]; +} + +/** 轻度去饱和,逼近油墨叠色效果 */ +function applyDesaturation(r: number, g: number, b: number, saturation: number): [number, number, number] { + const lum = 0.299 * r + 0.587 * g + 0.114 * b; + return [ + clampByte(lum + (r - lum) * saturation), + clampByte(lum + (g - lum) * saturation), + clampByte(lum + (b - lum) * saturation), + ]; +} + function transformRgbPixel(r: number, g: number, b: number, mode: PrintColorMode): [number, number, number] { if (mode === 'grayscale') { const lum = Math.round(0.299 * r + 0.587 * g + 0.114 * b); return [lum, lum, lum]; } - let { c, m, y, k } = rgbToCmyk(r, g, b); - ({ c, m, y, k } = limitTotalInk(c, m, y, k)); - const rgb = cmykToRgb(c, m, y, k); - return [rgb.r, rgb.g, rgb.b]; + let nr = r; + let ng = g; + let nb = b; + + if (mode === 'cmyk') { + let { c, m, y, k } = rgbToCmyk(nr, ng, nb); + ({ c, m, y, k } = applyGrayComponentReplacement(c, m, y, k)); + ({ c, m, y, k } = limitTotalInk(c, m, y, k)); + const rgb = cmykToRgb(c, m, y, k); + nr = rgb.r; + ng = rgb.g; + nb = rgb.b; + [nr, ng, nb] = applyDotGain(nr, ng, nb, 0.07); + return [nr, ng, nb]; + } + + // rgb:轻度印刷补偿(设计预览与导出一致,缩小与实机色差) + [nr, ng, nb] = applyDesaturation(nr, ng, nb, 0.9); + [nr, ng, nb] = applyDotGain(nr, ng, nb, 0.04); + return [nr, ng, nb]; } export function applyPrintColorModeToImageData( imageData: ImageData, mode: PrintColorMode = 'rgb' ): void { - if (mode === 'rgb') return; const { data } = imageData; for (let i = 0; i < data.length; i += 4) { const alpha = data[i + 3]; @@ -129,7 +177,6 @@ export function applyPrintColorModeToCanvas( canvas: HTMLCanvasElement, mode: PrintColorMode = 'rgb' ): void { - if (mode === 'rgb') return; const ctx = canvas.getContext('2d', { willReadFrequently: true }); if (!ctx) return; const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height); @@ -138,14 +185,14 @@ export function applyPrintColorModeToCanvas( } export function applyPrintColorMode(color: string, mode: PrintColorMode = 'rgb'): string { - if (mode === 'rgb' || isTransparentColorValue(color)) return color; + if (isTransparentColorValue(color)) return color; const parsed = parseCssColor(color); if (!parsed) return color; const [r, g, b] = transformRgbPixel(parsed.r, parsed.g, parsed.b, mode); return formatRgba(r, g, b, parsed.a); } -/** 解析颜色字面量或 {变量},并应用印刷色彩模式 */ +/** 解析颜色字面量或 {变量} */ export function resolveRenderableColor( raw: string | undefined, ctx: RenderColorContext, @@ -167,12 +214,10 @@ export function resolveRenderableColor( } if (!resolved || isTransparentColorValue(resolved)) return 'transparent'; - - // 印刷色彩模式在整页画布合成后统一处理,避免逐色 + 全图双重转换 return resolved; } -/** 为渲染解析元素上的颜色字段(含变量与印刷模式) */ +/** 为渲染解析元素上的颜色字段(含变量) */ export function resolveElementColorsForRender( elem: TemplateElement, ctx: RenderColorContext diff --git a/src/components/CanvasLabelImage.tsx b/src/components/CanvasLabelImage.tsx index 0df42ff..9d46a89 100644 --- a/src/components/CanvasLabelImage.tsx +++ b/src/components/CanvasLabelImage.tsx @@ -37,7 +37,7 @@ export const CanvasLabelImage: React.FC = ({ referenceBackgroundOpacity, referenceBackgroundFit, renderScale = 16, - printColorMode = 'rgb', + printColorMode = 'cmyk', }) => { const [dataUrl, setDataUrl] = useState(''); const [initialLoading, setInitialLoading] = useState(true); diff --git a/src/components/LabelDesigner.tsx b/src/components/LabelDesigner.tsx index d4ed070..084e057 100644 --- a/src/components/LabelDesigner.tsx +++ b/src/components/LabelDesigner.tsx @@ -236,7 +236,7 @@ export const LabelDesigner: React.FC = ({ () => template.variableDefaults ?? null, [template.variableDefaults] ); - const printColorMode = template.paper?.printColorMode ?? 'rgb'; + const printColorMode = template.paper?.printColorMode ?? 'cmyk'; const showReferencePreview = useMemo( () => diff --git a/src/components/PreviewGrid.tsx b/src/components/PreviewGrid.tsx index ecb9267..1e0dc1b 100644 --- a/src/components/PreviewGrid.tsx +++ b/src/components/PreviewGrid.tsx @@ -67,7 +67,7 @@ const paperEquals = (a: PaperConfig, b: PaperConfig) => a.columnGap === b.columnGap && a.rowGap === b.rowGap && a.flow === b.flow && - (a.printColorMode ?? 'rgb') === (b.printColorMode ?? 'rgb'); + (a.printColorMode ?? 'cmyk') === (b.printColorMode ?? 'cmyk'); export const PaperSettingsPanel: React.FC = ({ paper, @@ -525,7 +525,7 @@ export const PaperSettingsPanel: React.FC = ({
patchDraftPaper({ printColorMode: e.target.value as PrintColorMode }) } @@ -538,7 +538,7 @@ export const PaperSettingsPanel: React.FC = ({ ))}

- 预览与导出时对整页位图统一处理;CMYK 含总墨量限制,色彩模式非 RGB 时强制 PNG 导出以减少损色 + 设计预览、排版预览与导出/打印使用同一套色彩处理;推荐 CMYK 模式。实机仍受打印机驱动影响,请以打样为准

@@ -886,7 +886,7 @@ export const LayoutPreview: React.FC = ({ rowIndex={item.sourceIndex} showBorder={false} mode="output" - printColorMode={paper.printColorMode ?? 'rgb'} + printColorMode={paper.printColorMode ?? 'cmyk'} {...referenceBackgroundRender} /> diff --git a/src/labelRenderer.ts b/src/labelRenderer.ts index 69be61f..8f97966 100644 --- a/src/labelRenderer.ts +++ b/src/labelRenderer.ts @@ -795,9 +795,7 @@ export async function renderLayoutPageBackgroundDataUrl( drawImageWithFit(ctx, img, 0, 0, canvas.width, canvas.height, options.fit); ctx.restore(); - if (printColorMode !== 'rgb') { - applyPrintColorModeToCanvas(canvas, printColorMode); - } + applyPrintColorModeToCanvas(canvas, printColorMode); return imageFormat === 'jpeg' ? canvas.toDataURL('image/jpeg', 0.95) @@ -1044,9 +1042,7 @@ export async function renderLabelToDataUrl(options: RenderLabelOptions): Promise ctx.restore(); - if (printColorMode !== 'rgb') { - applyPrintColorModeToCanvas(canvas, printColorMode); - } + applyPrintColorModeToCanvas(canvas, printColorMode); return imageFormat === 'jpeg' ? canvas.toDataURL('image/jpeg', jpegQuality) diff --git a/src/pdfExport.ts b/src/pdfExport.ts index 8bbf821..b722398 100644 --- a/src/pdfExport.ts +++ b/src/pdfExport.ts @@ -44,7 +44,7 @@ async function addPageBackgroundIfNeeded( const options = resolveLayoutPageBackgroundForOutput(template); if (!options) return; - const printColorMode = paper.printColorMode ?? 'rgb'; + const printColorMode = paper.printColorMode ?? 'cmyk'; const key = `${options.background}|${options.opacity}|${options.fit}|${paperW}|${paperH}|${imageFormat}|${printColorMode}`; if (cache.key !== key) { cache.key = key; diff --git a/src/types.ts b/src/types.ts index d9bb71b..ef66d2e 100644 --- a/src/types.ts +++ b/src/types.ts @@ -340,9 +340,9 @@ export type PaperType = 'A4' | 'A5' | 'A6' | 'custom'; export type PrintColorMode = 'rgb' | 'grayscale' | 'cmyk'; export const PRINT_COLOR_MODE_LABELS: Record = { - rgb: '标准 RGB', + rgb: '标准(轻度印刷补偿)', grayscale: '黑白(灰度)', - cmyk: 'CMYK(印刷模拟)', + cmyk: 'CMYK 印刷模拟(推荐)', }; export interface PaperConfig { diff --git a/src/utils.ts b/src/utils.ts index 57c44d3..269dc18 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -537,7 +537,7 @@ export const DEFAULT_PAPER_CONFIG: PaperConfig = { columnGap: 0, rowGap: 0, flow: 'left-right-top-bottom', - printColorMode: 'rgb', + printColorMode: 'cmyk', }; /** 根据纸张排版参数反算单个标签尺寸 (mm) */