Compare commits
22 Commits
feat/busin
...
feat/busin
| Author | SHA1 | Date | |
|---|---|---|---|
| aa3b0b05b8 | |||
| 4c6e8cb28b | |||
| 175cb10e01 | |||
| 5e8a175a53 | |||
| 9f64304539 | |||
| 6367efdf6f | |||
| 22ae008f29 | |||
| 118319ceab | |||
| 5d94337df1 | |||
| 3798d37fcd | |||
| efe3cbc58e | |||
| f798afe917 | |||
| 1623e04fce | |||
| b77eb2087e | |||
| 7e7acff670 | |||
| 069400dd86 | |||
| 91f5cb8de3 | |||
| 702313e42c | |||
| adafe8ac02 | |||
| 3599880f3a | |||
| 79f0231e26 | |||
| cdf2af79ca |
9933
lib/TruckBlueprint.tsx
Normal file
9933
lib/TruckBlueprint.tsx
Normal file
File diff suppressed because one or more lines are too long
@@ -16,6 +16,24 @@ import {
|
||||
Circle,
|
||||
} from '@react-pdf/renderer';
|
||||
import * as path from 'path';
|
||||
import { TruckBlueprint } from './TruckBlueprint';
|
||||
|
||||
// Register embedded fonts so Ghostscript -dNoOutputFonts can convert them to
|
||||
// outlines (curves). Built-in PDF fonts like Helvetica are NEVER embedded and
|
||||
// therefore cannot be outlined — always use a physical font file here.
|
||||
Font.register({
|
||||
family: 'Inter',
|
||||
fonts: [
|
||||
{
|
||||
src: path.join(process.cwd(), 'public', 'fonts', 'Inter-Regular.woff'),
|
||||
fontWeight: 'normal',
|
||||
},
|
||||
{
|
||||
src: path.join(process.cwd(), 'public', 'fonts', 'Inter-Bold.woff'),
|
||||
fontWeight: 'bold',
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
export interface PersonData {
|
||||
name: string;
|
||||
@@ -28,11 +46,16 @@ export interface PDFBusinessCardProps {
|
||||
person: PersonData;
|
||||
qrCodeDataUrl: string;
|
||||
variant?: 'blue' | 'white' | 'black';
|
||||
isViaprinto?: boolean;
|
||||
}
|
||||
|
||||
// Die Buchstaben K, L, Z im SVG (inklusive dem kleinen Stück vom Z oben links)
|
||||
export const KLZ_PATHS = [25, 26, 27, 28, 29];
|
||||
|
||||
// Mintel Aesthetics Colors - EXACT PROD COLORS
|
||||
const COLORS = {
|
||||
primary: '#011dff', // Brand Royal Blue
|
||||
blue: '#000a66',
|
||||
green: '#82ed20', // Brand Accent Green
|
||||
darkGreen: '#4A9E00', // Darker green for white background visibility
|
||||
white: '#ffffff',
|
||||
@@ -48,7 +71,7 @@ const styles = StyleSheet.create({
|
||||
height: '61mm',
|
||||
backgroundColor: COLORS.primary,
|
||||
position: 'relative',
|
||||
fontFamily: 'Helvetica',
|
||||
fontFamily: 'Inter',
|
||||
overflow: 'hidden',
|
||||
},
|
||||
pageBleedBack: {
|
||||
@@ -56,7 +79,7 @@ const styles = StyleSheet.create({
|
||||
height: '61mm',
|
||||
backgroundColor: COLORS.white,
|
||||
position: 'relative',
|
||||
fontFamily: 'Helvetica',
|
||||
fontFamily: 'Inter',
|
||||
overflow: 'hidden',
|
||||
},
|
||||
|
||||
@@ -66,20 +89,17 @@ const styles = StyleSheet.create({
|
||||
top: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
bottom: '16mm', // Leave space for the bar below
|
||||
bottom: '18mm', // Leave space for the bar below
|
||||
zIndex: 1,
|
||||
},
|
||||
frontBackgroundImage: {
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
objectFit: 'contain',
|
||||
objectPositionX: 'center',
|
||||
objectPositionY: 'center',
|
||||
opacity: 0.4, // Less transparent, more contrast
|
||||
},
|
||||
frontContainer: {
|
||||
flex: 1,
|
||||
paddingBottom: '16mm', // Push the logo up to center it in the area above the bar
|
||||
paddingBottom: '18mm', // Push the logo up to center it in the area above the bar
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
},
|
||||
@@ -101,14 +121,14 @@ const styles = StyleSheet.create({
|
||||
bottom: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
height: '16mm',
|
||||
height: '18mm',
|
||||
borderTop: `2pt solid ${COLORS.green}`,
|
||||
backgroundColor: '#000a66',
|
||||
zIndex: 50,
|
||||
flexDirection: 'column',
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
gap: 6, // Increased gap for more spacing under tagline
|
||||
gap: 5,
|
||||
},
|
||||
frontIconsGroup: {
|
||||
flexDirection: 'row',
|
||||
@@ -141,25 +161,27 @@ const styles = StyleSheet.create({
|
||||
// -- WHITE PAGE (Vorderseite) --
|
||||
backContainer: {
|
||||
flex: 1,
|
||||
padding: '8mm',
|
||||
paddingTop: '6mm',
|
||||
paddingBottom: '6mm',
|
||||
paddingLeft: '6mm',
|
||||
paddingRight: '6mm',
|
||||
flexDirection: 'column',
|
||||
justifyContent: 'space-between',
|
||||
},
|
||||
backHeader: {
|
||||
marginBottom: 'auto',
|
||||
marginBottom: '5mm',
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
width: '100%',
|
||||
},
|
||||
backLogo: {
|
||||
width: 70,
|
||||
width: 60,
|
||||
},
|
||||
accentLineHorizontal: {
|
||||
flex: 1,
|
||||
height: 1.5,
|
||||
backgroundColor: COLORS.primary, // Brand blue line
|
||||
backgroundColor: '#011dff', // Brand blue line
|
||||
marginLeft: 15,
|
||||
marginRight: 0,
|
||||
marginRight: 10,
|
||||
},
|
||||
accentDot: {
|
||||
width: 4,
|
||||
@@ -178,37 +200,37 @@ const styles = StyleSheet.create({
|
||||
top: -5,
|
||||
},
|
||||
backBody: {
|
||||
flex: 1,
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'flex-end',
|
||||
alignItems: 'flex-start',
|
||||
width: '100%',
|
||||
},
|
||||
leftColumn: {
|
||||
flex: 1,
|
||||
flexDirection: 'column',
|
||||
paddingRight: '4mm',
|
||||
paddingBottom: '2mm',
|
||||
},
|
||||
nameSection: {
|
||||
marginBottom: '5mm',
|
||||
marginBottom: '4mm',
|
||||
},
|
||||
nameText: {
|
||||
fontSize: 16,
|
||||
fontSize: 14,
|
||||
fontWeight: 'bold',
|
||||
color: COLORS.neutralDark,
|
||||
marginBottom: 2,
|
||||
letterSpacing: -0.2,
|
||||
},
|
||||
roleText: {
|
||||
fontSize: 9,
|
||||
fontSize: 7.5,
|
||||
fontWeight: 'bold',
|
||||
color: COLORS.primary,
|
||||
color: '#011dff',
|
||||
letterSpacing: 0.5,
|
||||
textTransform: 'uppercase',
|
||||
},
|
||||
contactSection: {
|
||||
flexDirection: 'column',
|
||||
gap: 5,
|
||||
gap: 4,
|
||||
},
|
||||
contactRow: {
|
||||
flexDirection: 'row',
|
||||
@@ -220,7 +242,7 @@ const styles = StyleSheet.create({
|
||||
alignItems: 'flex-start',
|
||||
},
|
||||
contactText: {
|
||||
fontSize: 9,
|
||||
fontSize: 8,
|
||||
color: COLORS.neutralDark,
|
||||
fontWeight: 'normal',
|
||||
paddingTop: 1,
|
||||
@@ -228,27 +250,39 @@ const styles = StyleSheet.create({
|
||||
rightColumn: {
|
||||
flexDirection: 'column',
|
||||
alignItems: 'flex-end',
|
||||
justifyContent: 'flex-end',
|
||||
justifyContent: 'flex-start',
|
||||
},
|
||||
// Ultra Minimalist QR Area
|
||||
qrContainer: {
|
||||
flexDirection: 'column',
|
||||
alignItems: 'center',
|
||||
gap: 4,
|
||||
gap: 3,
|
||||
},
|
||||
qrCodeWrapper: {
|
||||
width: '15mm',
|
||||
height: '15mm',
|
||||
width: '12mm',
|
||||
height: '12mm',
|
||||
border: '0.75pt solid #011dff',
|
||||
padding: 2.5,
|
||||
backgroundColor: '#ffffff',
|
||||
},
|
||||
qrCode: {
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
},
|
||||
qrLabel: {
|
||||
fontSize: 5,
|
||||
color: COLORS.neutralDark, // Subtle grey instead of loud blue
|
||||
fontWeight: 'bold',
|
||||
letterSpacing: 1.2,
|
||||
addressFooter: {
|
||||
width: '100%',
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
paddingTop: '2.5mm',
|
||||
marginTop: 'auto',
|
||||
marginBottom: '2mm', // Force extra space from the bottom edge
|
||||
},
|
||||
addressFooterText: {
|
||||
fontSize: 7, // increased from 6
|
||||
color: COLORS.grayText,
|
||||
letterSpacing: 0.3,
|
||||
fontWeight: 'normal',
|
||||
},
|
||||
});
|
||||
|
||||
@@ -280,6 +314,15 @@ const GlobeIcon = () => (
|
||||
</Svg>
|
||||
);
|
||||
|
||||
const LocationIcon = () => (
|
||||
<Svg viewBox="0 0 24 24" width={9} height={9}>
|
||||
<Path
|
||||
d="M12 2C8.13 2 5 5.13 5 9c0 5.25 7 13 7 13s7-7.75 7-13c0-3.87-3.13-7-7-7zm0 9.5c-1.38 0-2.5-1.12-2.5-2.5s1.12-2.5 2.5-2.5 2.5 1.12 2.5 2.5-1.12 2.5-2.5 2.5z"
|
||||
fill="#000000"
|
||||
/>
|
||||
</Svg>
|
||||
);
|
||||
|
||||
const ScanIcon = () => (
|
||||
<Svg viewBox="0 0 24 24" width={9} height={9}>
|
||||
<Path
|
||||
@@ -315,12 +358,51 @@ const parseSvgPaths = (filePath: string) => {
|
||||
}
|
||||
};
|
||||
|
||||
const VectorLogo = ({ paths, style }: { paths: { d: string; fill: string }[]; style: any }) => (
|
||||
const VectorLogo = ({
|
||||
paths,
|
||||
style,
|
||||
overrideColor,
|
||||
overrideWhiteColor,
|
||||
pathOverrides,
|
||||
solidPaths,
|
||||
solidBackgroundColor = '#000a66',
|
||||
}: {
|
||||
paths: { d: string; fill: string }[];
|
||||
style: any;
|
||||
overrideColor?: string;
|
||||
overrideWhiteColor?: string;
|
||||
pathOverrides?: Record<number, string>;
|
||||
solidPaths?: number[];
|
||||
solidBackgroundColor?: string;
|
||||
}) => (
|
||||
<Svg viewBox="0 0 295 99" style={style}>
|
||||
<G {...({ fillRule: 'evenodd', clipRule: 'evenodd' } as any)}>
|
||||
{paths.map((p, i) => (
|
||||
<Path key={i} d={p.d} fill={p.fill} />
|
||||
))}
|
||||
{/* Background layer: solid fills to prevent see-through */}
|
||||
{solidPaths &&
|
||||
solidBackgroundColor &&
|
||||
paths.map((p, i) => {
|
||||
if (!solidPaths.includes(i)) return null;
|
||||
let d = p.d;
|
||||
const zIndex = d.toUpperCase().indexOf('Z');
|
||||
if (zIndex !== -1) {
|
||||
d = d.substring(0, zIndex + 1);
|
||||
}
|
||||
return <Path key={`bg-${i}`} d={d} fill={solidBackgroundColor} />;
|
||||
})}
|
||||
{paths.map((p, i) => {
|
||||
let fill = overrideColor || p.fill;
|
||||
const d = p.d;
|
||||
|
||||
if (pathOverrides && pathOverrides[i]) {
|
||||
fill = pathOverrides[i];
|
||||
} else if (
|
||||
overrideWhiteColor &&
|
||||
(p.fill === 'white' || p.fill === '#ffffff' || p.fill === '#fff')
|
||||
) {
|
||||
fill = overrideWhiteColor;
|
||||
}
|
||||
return <Path key={i} d={d} fill={fill} />;
|
||||
})}
|
||||
</G>
|
||||
</Svg>
|
||||
);
|
||||
@@ -610,6 +692,7 @@ export const PDFBusinessCard: React.FC<PDFBusinessCardProps> = ({
|
||||
person,
|
||||
qrCodeDataUrl,
|
||||
variant = 'blue',
|
||||
isViaprinto = false,
|
||||
}) => {
|
||||
const isWhiteVariant = variant === 'white' || variant === 'black';
|
||||
|
||||
@@ -637,6 +720,11 @@ export const PDFBusinessCard: React.FC<PDFBusinessCardProps> = ({
|
||||
return logoWhitePaths;
|
||||
};
|
||||
|
||||
const getBackLogoPaths = () => {
|
||||
if (variant === 'black') return logoBlackWithTaglinePaths;
|
||||
return logoBlueWithTaglinePaths;
|
||||
};
|
||||
|
||||
const bgImagePath = isWhiteVariant
|
||||
? path.join(process.cwd(), 'public', 'media', 'cable_drums_truck.png')
|
||||
: path.join(process.cwd(), 'public', 'cable_drums_bg.png');
|
||||
@@ -653,9 +741,10 @@ export const PDFBusinessCard: React.FC<PDFBusinessCardProps> = ({
|
||||
style={[styles.pageBleed, isWhiteVariant && { backgroundColor: '#ffffff' }]}
|
||||
>
|
||||
<View style={styles.truckWrapper}>
|
||||
<Image
|
||||
src={bgImagePath}
|
||||
style={[styles.frontBackgroundImage, isWhiteVariant && { opacity: 0.2 }]}
|
||||
<TruckBlueprint
|
||||
style={styles.frontBackgroundImage}
|
||||
opacity={isWhiteVariant ? 0.2 : 0.25}
|
||||
backgroundColor={isWhiteVariant ? '#ffffff' : '#011dff'}
|
||||
/>
|
||||
</View>
|
||||
<View
|
||||
@@ -695,6 +784,7 @@ export const PDFBusinessCard: React.FC<PDFBusinessCardProps> = ({
|
||||
Hochspannung
|
||||
</Text>
|
||||
</View>
|
||||
|
||||
<View style={styles.frontBrandIconWrapper}>
|
||||
<IconSolar
|
||||
style={styles.frontBrandIcon}
|
||||
@@ -707,16 +797,42 @@ export const PDFBusinessCard: React.FC<PDFBusinessCardProps> = ({
|
||||
</View>
|
||||
</View>
|
||||
<View style={styles.frontContainer}>
|
||||
<VectorLogo paths={getFrontLogoPaths()} style={styles.frontLogo} />
|
||||
{/* Base Logo: text is white, truck is green. Fill KLZ background to hide truck, keep outline white */}
|
||||
<VectorLogo
|
||||
paths={getFrontLogoPaths()}
|
||||
style={styles.frontLogo}
|
||||
solidPaths={KLZ_PATHS}
|
||||
solidBackgroundColor={isWhiteVariant ? '#ffffff' : '#011dff'}
|
||||
/>
|
||||
|
||||
{/* Letterpress Mask Layer: We use #ff00fe as a marker color. Only the "KLZ" letters! */}
|
||||
{isViaprinto && (
|
||||
<VectorLogo
|
||||
paths={getFrontLogoPaths().filter((_, i) => KLZ_PATHS.includes(i))}
|
||||
style={[styles.frontLogo, { position: 'absolute' }]}
|
||||
overrideColor="#ff00fe"
|
||||
solidPaths={KLZ_PATHS.map((_, i) => i)}
|
||||
/>
|
||||
)}
|
||||
</View>
|
||||
</Page>
|
||||
|
||||
{/* RÜCKSEITE (White page with contact info) */}
|
||||
{/* VORDERSEITE (White page with contact info) */}
|
||||
<Page size={[258, 173]} style={styles.pageBleedBack}>
|
||||
<View style={styles.backContainer}>
|
||||
<View style={styles.backHeader}>
|
||||
<VectorLogo paths={logoBluePaths} style={styles.backLogo} />
|
||||
<VectorLogo
|
||||
paths={getBackLogoPaths()}
|
||||
style={styles.backLogo}
|
||||
overrideWhiteColor={'#011dff'}
|
||||
pathOverrides={KLZ_PATHS.reduce((acc, idx) => ({ ...acc, [idx]: 'none' }), {})}
|
||||
/>
|
||||
<View style={styles.accentLineHorizontal} />
|
||||
<View style={styles.qrContainer}>
|
||||
<View style={styles.qrCodeWrapper}>
|
||||
<Image src={qrCodeDataUrl} style={styles.qrCode} />
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
<View style={styles.backBody}>
|
||||
@@ -747,17 +863,14 @@ export const PDFBusinessCard: React.FC<PDFBusinessCardProps> = ({
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
<View style={styles.rightColumn}>
|
||||
<View style={styles.qrContainer}>
|
||||
<View style={styles.qrCodeWrapper}>
|
||||
<Image src={qrCodeDataUrl} style={styles.qrCode} />
|
||||
</View>
|
||||
<Text style={styles.qrLabel}>ABSPEICHERN</Text>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
<View style={styles.addressFooter}>
|
||||
<Text style={styles.addressFooterText}>
|
||||
KLZ Vertriebs GmbH · Raiffeisenstraße 22 · 73630 Remshalden
|
||||
</Text>
|
||||
</View>
|
||||
</Page>
|
||||
</Document>
|
||||
);
|
||||
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
public/downloads/business-cards/test_page_2.png
Normal file
BIN
public/downloads/business-cards/test_page_2.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 6.4 KiB |
@@ -19,25 +19,27 @@ const CONFIG = {
|
||||
truckImage: path.join(process.cwd(), 'public/truck.png'),
|
||||
fontRegular: path.join(process.cwd(), 'public/fonts/Inter-Regular.ttf'),
|
||||
fontBold: path.join(process.cwd(), 'public/fonts/Inter-Bold.ttf'),
|
||||
ghostscriptScript:
|
||||
'/Users/marcmintel/.gemini/config/plugins/mintel-ui-standards-plugin/skills/mintel-print-workflow/scripts/convert-cmyk.sh',
|
||||
};
|
||||
|
||||
const PEOPLE: PersonData[] = [
|
||||
{
|
||||
name: 'Michael Bodemer',
|
||||
role: 'Managing Director',
|
||||
phone: '+49 171 785 2420',
|
||||
phone: '+49 151 462 467 98',
|
||||
email: 'michael.bodemer@klz-cables.com',
|
||||
},
|
||||
{
|
||||
name: 'Klaus Mintel',
|
||||
role: 'Managing Director',
|
||||
phone: '+49 151 1775 2873',
|
||||
email: 'klaus.mintel@klz-cables.com',
|
||||
email: 'Klaus.mintel@klz-cables.com',
|
||||
},
|
||||
{
|
||||
name: 'Johannes Gleich',
|
||||
role: 'Platzhalter Rolle',
|
||||
phone: '+49 000 0000000',
|
||||
role: 'Senior Key Account Manager',
|
||||
phone: '+49 172 739 1109',
|
||||
email: 'johannes.gleich@klz-cables.com',
|
||||
},
|
||||
];
|
||||
@@ -71,69 +73,56 @@ END:VCARD`;
|
||||
});
|
||||
|
||||
const safeName = person.name.replace(/\s+/g, '_');
|
||||
const finalFileName = `KLZ_Visitenkarte_${safeName}.pdf`;
|
||||
const rgbPath = path.join(CONFIG.outputDir, `RGB_${finalFileName}`);
|
||||
const finalPath = path.join(CONFIG.outputDir, finalFileName);
|
||||
|
||||
console.log(`- Rendering React-PDF layout (RGB ONLY)...`);
|
||||
const buffer = await renderToBuffer(
|
||||
// 1. STANDARD DIGITAL PDF (RGB, no letterpress mask)
|
||||
console.log(`- Rendering standard digital PDF (RGB ONLY, Blue Variant)...`);
|
||||
const finalFileName = `KLZ_Visitenkarte_${safeName}.pdf`;
|
||||
const tempFileName = `temp_${finalFileName}`;
|
||||
const finalPath = path.join(CONFIG.outputDir, finalFileName);
|
||||
const tempPath = path.join(CONFIG.outputDir, tempFileName);
|
||||
|
||||
let buffer = await renderToBuffer(
|
||||
React.createElement(PDFBusinessCard, {
|
||||
person,
|
||||
qrCodeDataUrl,
|
||||
variant: 'blue',
|
||||
isViaprinto: false,
|
||||
}),
|
||||
);
|
||||
fs.writeFileSync(tempPath, buffer);
|
||||
|
||||
fs.writeFileSync(rgbPath, buffer);
|
||||
|
||||
console.log(`- Outlining fonts via Ghostscript (Keeping RGB)...`);
|
||||
const gsCommand = `gs -dNoOutputFonts -sDEVICE=pdfwrite -o "${finalPath}" "${rgbPath}"`;
|
||||
execSync(gsCommand, { stdio: 'ignore' });
|
||||
|
||||
fs.unlinkSync(rgbPath);
|
||||
|
||||
// Convert fonts to outlines via ghostscript
|
||||
const gsCommand = `gs -dNoOutputFonts -sDEVICE=pdfwrite -o "${finalPath}" "${tempPath}"`;
|
||||
execSync(gsCommand, { stdio: 'inherit' });
|
||||
fs.unlinkSync(tempPath);
|
||||
console.log(`✓ Final Print File generated (Fonts to Paths, RGB preserved): ${finalFileName}`);
|
||||
|
||||
// Generate White Variant
|
||||
const finalFileNameWhite = `KLZ_Visitenkarte_${safeName}_White.pdf`;
|
||||
const rgbPathWhite = path.join(CONFIG.outputDir, `RGB_${finalFileNameWhite}`);
|
||||
const finalPathWhite = path.join(CONFIG.outputDir, finalFileNameWhite);
|
||||
|
||||
console.log(`- Rendering White Variant...`);
|
||||
const bufferWhite = await renderToBuffer(
|
||||
// 2. VIAPRINTO PDF (Letterpress mask, CMYK)
|
||||
console.log(`- Rendering Viaprinto PDF (Letterpress mask)...`);
|
||||
const tempViaprintoPath = path.join(CONFIG.outputDir, `temp_viaprinto_${safeName}.pdf`);
|
||||
buffer = await renderToBuffer(
|
||||
React.createElement(PDFBusinessCard, {
|
||||
person,
|
||||
qrCodeDataUrl,
|
||||
variant: 'white',
|
||||
variant: 'blue',
|
||||
isViaprinto: true,
|
||||
}),
|
||||
);
|
||||
fs.writeFileSync(tempViaprintoPath, buffer);
|
||||
|
||||
fs.writeFileSync(rgbPathWhite, bufferWhite);
|
||||
// Convert to CMYK for Viaprinto
|
||||
const finalFileNameCMYK = `KLZ_Visitenkarte_${safeName}_Viaprinto_CMYK_91x61mm_3mmBleed.pdf`;
|
||||
const finalPathCMYK = path.join(CONFIG.outputDir, finalFileNameCMYK);
|
||||
|
||||
const gsCommandWhite = `gs -dNoOutputFonts -sDEVICE=pdfwrite -o "${finalPathWhite}" "${rgbPathWhite}"`;
|
||||
execSync(gsCommandWhite, { stdio: 'ignore' });
|
||||
fs.unlinkSync(rgbPathWhite);
|
||||
console.log(`✓ White Variant generated: ${finalFileNameWhite}`);
|
||||
|
||||
// Generate Black Variant
|
||||
const finalFileNameBlack = `KLZ_Visitenkarte_${safeName}_Black.pdf`;
|
||||
const rgbPathBlack = path.join(CONFIG.outputDir, `RGB_${finalFileNameBlack}`);
|
||||
const finalPathBlack = path.join(CONFIG.outputDir, finalFileNameBlack);
|
||||
|
||||
console.log(`- Rendering Black Variant...`);
|
||||
const bufferBlack = await renderToBuffer(
|
||||
React.createElement(PDFBusinessCard, {
|
||||
person,
|
||||
qrCodeDataUrl,
|
||||
variant: 'black',
|
||||
}),
|
||||
);
|
||||
|
||||
fs.writeFileSync(rgbPathBlack, bufferBlack);
|
||||
|
||||
const gsCommandBlack = `gs -dNoOutputFonts -sDEVICE=pdfwrite -o "${finalPathBlack}" "${rgbPathBlack}"`;
|
||||
execSync(gsCommandBlack, { stdio: 'ignore' });
|
||||
fs.unlinkSync(rgbPathBlack);
|
||||
console.log(`✓ Black Variant generated: ${finalFileNameBlack}`);
|
||||
console.log(`- Converting to CMYK for Viaprinto...`);
|
||||
const cmykCommand = `bash "${CONFIG.ghostscriptScript}" "${tempViaprintoPath}" "${finalPathCMYK}"`;
|
||||
try {
|
||||
execSync(cmykCommand, { stdio: 'inherit' });
|
||||
fs.unlinkSync(tempViaprintoPath);
|
||||
console.log(`✓ CMYK Print File generated: ${finalFileNameCMYK}`);
|
||||
} catch (e) {
|
||||
console.error(`❌ CMYK conversion failed! Is Ghostscript installed?`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
93
scripts/inject-letterpress.ts
Normal file
93
scripts/inject-letterpress.ts
Normal file
@@ -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);
|
||||
64
scripts/svg-to-react-pdf.ts
Normal file
64
scripts/svg-to-react-pdf.ts
Normal file
@@ -0,0 +1,64 @@
|
||||
import fs from 'fs';
|
||||
|
||||
const svgFile = process.argv[2];
|
||||
const outFile = process.argv[3];
|
||||
|
||||
let content = fs.readFileSync(svgFile, 'utf8');
|
||||
|
||||
content = content.replace(/<\?xml.*\?>/g, '');
|
||||
content = content.replace(/<!DOCTYPE.*>/g, '');
|
||||
content = content.replace(/xmlns:xlink="[^"]+"/g, '');
|
||||
content = content.replace(/xmlns:serif="[^"]+"/g, '');
|
||||
content = content.replace(/xml:space="preserve"/g, '');
|
||||
content = content.replace(/xmlns="[^"]+"/g, '');
|
||||
content = content.replace(/version="[^"]+"/g, '');
|
||||
|
||||
// Convert attributes to camelCase
|
||||
content = content.replace(/fill-rule/g, 'fillRule');
|
||||
content = content.replace(/clip-rule/g, 'clipRule');
|
||||
content = content.replace(/stroke-linejoin/g, 'strokeLinejoin');
|
||||
content = content.replace(/stroke-miterlimit/g, 'strokeMiterlimit');
|
||||
|
||||
// Convert style="fill:#xxxx;fill-rule:nonzero;" to fill="#xxxx" fillRule="nonzero"
|
||||
content = content.replace(/style="([^"]+)"/g, (match, styleStr) => {
|
||||
const parts = styleStr.split(';').filter(Boolean);
|
||||
let attrs = [];
|
||||
for (const part of parts) {
|
||||
const [key, val] = part.split(':');
|
||||
if (key === 'fill') attrs.push(`fill="${val}"`);
|
||||
if (key === 'fillRule' || key === 'fill-rule') attrs.push(`fillRule="${val}"`);
|
||||
if (key === 'clipRule' || key === 'clip-rule') attrs.push(`clipRule="${val}"`);
|
||||
if (key === 'strokeLinejoin' || key === 'stroke-linejoin')
|
||||
attrs.push(`strokeLinejoin="${val}"`);
|
||||
if (key === 'strokeMiterlimit' || key === 'stroke-miterlimit')
|
||||
attrs.push(`strokeMiterlimit="${val}"`);
|
||||
}
|
||||
return attrs.join(' ');
|
||||
});
|
||||
|
||||
// React components
|
||||
content = content.replace(/<svg/g, '<Svg');
|
||||
content = content.replace(/<\/svg>/g, '</Svg>');
|
||||
content = content.replace(/<g/g, '<G');
|
||||
content = content.replace(/<\/g>/g, '</G>');
|
||||
content = content.replace(/<path/g, '<Path');
|
||||
content = content.replace(/<\/path>/g, '</Path>');
|
||||
|
||||
// In React PDF, width/height on Svg can be removed and controlled via style.
|
||||
content = content.replace(/width="100%" height="100%" /, '');
|
||||
|
||||
const final = `
|
||||
import React from 'react';
|
||||
import { Svg, G, Path } from '@react-pdf/renderer';
|
||||
|
||||
export const TruckBlueprint = ({ style, opacity = 1 }: { style?: any, opacity?: number }) => (
|
||||
<Svg viewBox="0 0 1615 974" style={[style, { opacity }]}>
|
||||
<G fillRule="evenodd" clipRule="evenodd" strokeLinejoin="round" strokeMiterlimit="2">
|
||||
${content.match(/<G[\s\S]*<\/G>/)?.[0] || content}
|
||||
</G>
|
||||
</Svg>
|
||||
);
|
||||
`;
|
||||
|
||||
fs.writeFileSync(outFile, final);
|
||||
console.log('Done!');
|
||||
@@ -34,7 +34,9 @@ describe('Contact Form E2E', () => {
|
||||
if (!isServerUp) skip();
|
||||
|
||||
page.on('console', (msg) => console.log('PAGE LOG:', msg.text()));
|
||||
page.on('pageerror', (err) => console.error('PAGE ERROR:', err.message));
|
||||
page.on('pageerror', (err) =>
|
||||
console.error('PAGE ERROR:', err instanceof Error ? err.message : String(err)),
|
||||
);
|
||||
|
||||
await page.goto(`${BASE_URL}/de/kontakt`);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user