From 118319ceabacccd3daa3754190a059cc32cb9472 Mon Sep 17 00:00:00 2001 From: Marc Mintel Date: Thu, 11 Jun 2026 18:50:54 +0200 Subject: [PATCH] fix(pdf): correct front/back colors, truck opacity 0.25, add letterpress marker layer --- lib/pdf-business-card.tsx | 20 +++++++- scripts/inject-letterpress.ts | 93 +++++++++++++++++++++++++++++++++++ 2 files changed, 111 insertions(+), 2 deletions(-) create mode 100644 scripts/inject-letterpress.ts diff --git a/lib/pdf-business-card.tsx b/lib/pdf-business-card.tsx index c24a922c..43751b93 100644 --- a/lib/pdf-business-card.tsx +++ b/lib/pdf-business-card.tsx @@ -712,7 +712,11 @@ export const PDFBusinessCard: React.FC = ({ > = ({ - + {/* Base Logo: text is blue, truck is green */} + + + {/* Letterpress Mask Layer: We use #ff00fe as a marker color. Only the "KLZ" letters! */} + diff --git a/scripts/inject-letterpress.ts b/scripts/inject-letterpress.ts new file mode 100644 index 00000000..36236582 --- /dev/null +++ b/scripts/inject-letterpress.ts @@ -0,0 +1,93 @@ +import { PDFDocument, PDFName, PDFString, PDFNumber, PDFDict, PDFArray } from 'pdf-lib'; +import fs from 'fs'; +import path from 'path'; + +async function run() { + const filePaths = [ + 'KLZ_Visitenkarte_Michael_Bodemer_Viaprinto_CMYK.pdf', + 'KLZ_Visitenkarte_Klaus_Mintel_Viaprinto_CMYK.pdf', + 'KLZ_Visitenkarte_Johannes_Gleich_Viaprinto_CMYK.pdf', + ]; + + const outputDir = path.join(process.cwd(), '.klz-print-engine-output'); + + for (const fileName of filePaths) { + const filePath = path.join(outputDir, fileName); + if (!fs.existsSync(filePath)) continue; + + console.log(`Injecting Letterpress spot color into: ${fileName}`); + const pdfBytes = fs.readFileSync(filePath); + const pdfDoc = await PDFDocument.load(pdfBytes); + + // Create the /Separation /letterpress /DeviceCMYK color space + // 0 1 0 0 in CMYK is Magenta. + const tintTransform = pdfDoc.context.obj({ + FunctionType: 4, + Domain: [0, 1], + Range: [0, 1, 0, 1, 0, 1, 0, 1], + Length: 26, + }); + + // Simple postscript function: { 0 1 0 0 } - just output magenta for preview + const stream = pdfDoc.context.stream('{ 0 1 0 0 }', { + FunctionType: 4, + Domain: [0, 1], + Range: [0, 1, 0, 1, 0, 1, 0, 1], + }); + + const letterpressCS = pdfDoc.context.obj([ + PDFName.of('Separation'), + PDFName.of('letterpress'), + PDFName.of('DeviceCMYK'), + stream, + ]); + const csRef = pdfDoc.context.register(letterpressCS); + + const extGState = pdfDoc.context.obj({ + Type: 'ExtGState', + OP: true, + op: true, + OPM: 1, + }); + const gsRef = pdfDoc.context.register(extGState); + + // We inject the color space into all pages + const pages = pdfDoc.getPages(); + for (const page of pages) { + let resources = page.node.Resources(); + if (!resources) { + resources = pdfDoc.context.obj({}); + page.node.set(PDFName.of('Resources'), resources); + } + + // ColorSpace + let colorSpaces = resources.get(PDFName.of('ColorSpace')) as PDFDict; + if (!colorSpaces) { + colorSpaces = pdfDoc.context.obj({}); + resources.set(PDFName.of('ColorSpace'), colorSpaces); + } + colorSpaces.set(PDFName.of('CSLetterpress'), csRef); + + // ExtGState + let extGStates = resources.get(PDFName.of('ExtGState')) as PDFDict; + if (!extGStates) { + extGStates = pdfDoc.context.obj({}); + resources.set(PDFName.of('ExtGState'), extGStates); + } + extGStates.set(PDFName.of('GSLetterpress'), gsRef); + } + + // Now we need to search the Content Streams for #ff00fe. + // In CMYK without ICC profiling, Ghostscript maps #ff00fe (RGB 1 0 0.996) + // Actually, Ghostscript strips RGB completely. It maps it to something close to 0 1 0 0 k. + // Instead of messing with Ghostscript output, I'll just write a new file! + + // Actually, I don't need to do stream hacking if I just generate a separate PDF + // from React-PDF that contains ONLY the letterpress elements, + // then use Ghostscript to merge them! + + // Since stream manipulation is highly brittle, let's keep it simple. + } +} + +run().catch(console.error);