优化
This commit is contained in:
17
src/App.tsx
17
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]
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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(
|
||||
() =>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -537,7 +537,7 @@ export const DEFAULT_PAPER_CONFIG: PaperConfig = {
|
||||
columnGap: 0,
|
||||
rowGap: 0,
|
||||
flow: 'left-right-top-bottom',
|
||||
printColorMode: 'rgb',
|
||||
printColorMode: 'cmyk',
|
||||
};
|
||||
|
||||
/** 根据纸张排版参数反算单个标签尺寸 (mm) */
|
||||
|
||||
Reference in New Issue
Block a user