This commit is contained in:
iqudoo
2026-06-23 15:26:51 +08:00
parent 1b04e34ff0
commit 33d3e5537c
9 changed files with 79 additions and 43 deletions

View File

@@ -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]

View File

@@ -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 ≈ 280300% */
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典型 280300% */
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);
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);
return [rgb.r, rgb.g, rgb.b];
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

View File

@@ -37,7 +37,7 @@ export const CanvasLabelImage: React.FC<CanvasLabelImageProps> = ({
referenceBackgroundOpacity,
referenceBackgroundFit,
renderScale = 16,
printColorMode = 'rgb',
printColorMode = 'cmyk',
}) => {
const [dataUrl, setDataUrl] = useState<string>('');
const [initialLoading, setInitialLoading] = useState<boolean>(true);

View File

@@ -236,7 +236,7 @@ export const LabelDesigner: React.FC<LabelDesignerProps> = ({
() => template.variableDefaults ?? null,
[template.variableDefaults]
);
const printColorMode = template.paper?.printColorMode ?? 'rgb';
const printColorMode = template.paper?.printColorMode ?? 'cmyk';
const showReferencePreview = useMemo(
() =>

View File

@@ -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<PaperSettingsPanelProps> = ({
paper,
@@ -525,7 +525,7 @@ export const PaperSettingsPanel: React.FC<PaperSettingsPanelProps> = ({
<div>
<label className={labelClass}></label>
<FieldSelect
value={draftPaper.printColorMode ?? 'rgb'}
value={draftPaper.printColorMode ?? 'cmyk'}
onChange={(e) =>
patchDraftPaper({ printColorMode: e.target.value as PrintColorMode })
}
@@ -538,7 +538,7 @@ export const PaperSettingsPanel: React.FC<PaperSettingsPanelProps> = ({
))}
</FieldSelect>
<p className={`${isPs || isMobile ? 'text-[9px] text-[#666]' : 'text-[10px] text-gray-500'} mt-1 leading-relaxed`}>
CMYK RGB PNG
/使 CMYK
</p>
</div>
</div>
@@ -886,7 +886,7 @@ export const LayoutPreview: React.FC<LayoutPreviewProps> = ({
rowIndex={item.sourceIndex}
showBorder={false}
mode="output"
printColorMode={paper.printColorMode ?? 'rgb'}
printColorMode={paper.printColorMode ?? 'cmyk'}
{...referenceBackgroundRender}
/>
</div>

View File

@@ -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);
}
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);
}
return imageFormat === 'jpeg'
? canvas.toDataURL('image/jpeg', jpegQuality)

View File

@@ -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;

View File

@@ -340,9 +340,9 @@ export type PaperType = 'A4' | 'A5' | 'A6' | 'custom';
export type PrintColorMode = 'rgb' | 'grayscale' | 'cmyk';
export const PRINT_COLOR_MODE_LABELS: Record<PrintColorMode, string> = {
rgb: '标准 RGB',
rgb: '标准(轻度印刷补偿)',
grayscale: '黑白(灰度)',
cmyk: 'CMYK印刷模拟)',
cmyk: 'CMYK 印刷模拟(推荐',
};
export interface PaperConfig {

View File

@@ -537,7 +537,7 @@ export const DEFAULT_PAPER_CONFIG: PaperConfig = {
columnGap: 0,
rowGap: 0,
flow: 'left-right-top-bottom',
printColorMode: 'rgb',
printColorMode: 'cmyk',
};
/** 根据纸张排版参数反算单个标签尺寸 (mm) */