Compare commits

...

22 Commits

Author SHA1 Message Date
aa3b0b05b8 feat: align QR code to blue line, add border, remove label
Some checks failed
Build & Deploy / 🔍 Prepare (push) Successful in 15s
Build & Deploy / 🧪 QA (push) Failing after 1m44s
Build & Deploy / 🚀 Deploy (push) Has been skipped
Build & Deploy / 🏗️ Build (push) Has been skipped
Build & Deploy / 🧪 Post-Deploy Verification (push) Has been skipped
Build & Deploy / 🔔 Notify (push) Successful in 2s
2026-06-11 23:21:40 +02:00
4c6e8cb28b fix: resolve background fill and text colors on both business card sides 2026-06-11 23:20:03 +02:00
175cb10e01 fix(pdf): make KLZ paths solid to prevent see-through, add dimensions to viaprinto filename
Some checks failed
Build & Deploy / 🔍 Prepare (push) Successful in 11s
Build & Deploy / 🧪 QA (push) Failing after 1m20s
Build & Deploy / 🏗️ Build (push) Has been skipped
Build & Deploy / 🚀 Deploy (push) Has been skipped
Build & Deploy / 🧪 Post-Deploy Verification (push) Has been skipped
Build & Deploy / 🔔 Notify (push) Successful in 3s
2026-06-11 19:17:37 +02:00
5e8a175a53 feat(pdf): name KLZ_PATHS and use visible blue fill for letterpress mask layer
Some checks failed
Build & Deploy / 🔍 Prepare (push) Successful in 16s
Build & Deploy / 🧪 QA (push) Failing after 1m21s
Build & Deploy / 🏗️ Build (push) Has been skipped
Build & Deploy / 🚀 Deploy (push) Has been skipped
Build & Deploy / 🧪 Post-Deploy Verification (push) Has been skipped
Build & Deploy / 🔔 Notify (push) Successful in 2s
2026-06-11 19:15:32 +02:00
9f64304539 fix(pdf): include missing Z piece (path 29) in letterpress mask
Some checks failed
Build & Deploy / 🔍 Prepare (push) Successful in 16s
Build & Deploy / 🧪 QA (push) Has been cancelled
Build & Deploy / 🏗️ Build (push) Has been cancelled
Build & Deploy / 🚀 Deploy (push) Has been cancelled
Build & Deploy / 🧪 Post-Deploy Verification (push) Has been cancelled
Build & Deploy / 🔔 Notify (push) Has been cancelled
2026-06-11 19:14:22 +02:00
6367efdf6f fix(pdf): fix white-on-white logo and correct letterpress masking
Some checks failed
Build & Deploy / 🔍 Prepare (push) Successful in 11s
Build & Deploy / 🏗️ Build (push) Has been cancelled
Build & Deploy / 🚀 Deploy (push) Has been cancelled
Build & Deploy / 🧪 Post-Deploy Verification (push) Has been cancelled
Build & Deploy / 🔔 Notify (push) Has been cancelled
Build & Deploy / 🧪 QA (push) Has been cancelled
2026-06-11 19:13:32 +02:00
22ae008f29 fix(pdf): exact indices for KLZ letterpress and define COLORS.blue
Some checks failed
Build & Deploy / 🔍 Prepare (push) Successful in 11s
Build & Deploy / 🧪 QA (push) Failing after 1m20s
Build & Deploy / 🏗️ Build (push) Has been skipped
Build & Deploy / 🚀 Deploy (push) Has been skipped
Build & Deploy / 🧪 Post-Deploy Verification (push) Has been skipped
Build & Deploy / 🔔 Notify (push) Successful in 3s
2026-06-11 18:55:05 +02:00
118319ceab fix(pdf): correct front/back colors, truck opacity 0.25, add letterpress marker layer
Some checks failed
Build & Deploy / 🔍 Prepare (push) Successful in 11s
Build & Deploy / 🧪 QA (push) Failing after 1m21s
Build & Deploy / 🏗️ Build (push) Has been skipped
Build & Deploy / 🚀 Deploy (push) Has been skipped
Build & Deploy / 🧪 Post-Deploy Verification (push) Has been skipped
Build & Deploy / 🔔 Notify (push) Successful in 2s
2026-06-11 18:50:54 +02:00
5d94337df1 fix(pdf): render truck as vector, fix back logo coloring
Some checks failed
Build & Deploy / 🔍 Prepare (push) Successful in 11s
Build & Deploy / 🧪 QA (push) Failing after 1m21s
Build & Deploy / 🏗️ Build (push) Has been skipped
Build & Deploy / 🚀 Deploy (push) Has been skipped
Build & Deploy / 🧪 Post-Deploy Verification (push) Has been skipped
Build & Deploy / 🔔 Notify (push) Successful in 2s
2026-06-11 18:44:27 +02:00
3798d37fcd fix: enforce Mintel blue on back logo, increase address font size, update contact data
Some checks failed
Build & Deploy / 🔍 Prepare (push) Successful in 11s
Build & Deploy / 🧪 QA (push) Failing after 1m24s
Build & Deploy / 🏗️ Build (push) Has been skipped
Build & Deploy / 🚀 Deploy (push) Has been skipped
Build & Deploy / 🧪 Post-Deploy Verification (push) Has been skipped
Build & Deploy / 🔔 Notify (push) Successful in 2s
2026-06-11 18:40:33 +02:00
efe3cbc58e feat: switch default variant back to blue, maintain CMYK output for Viaprinto
All checks were successful
Build & Deploy / 🔍 Prepare (push) Successful in 15s
Build & Deploy / 🧪 QA (push) Successful in 1m32s
Build & Deploy / 🏗️ Build (push) Successful in 2m40s
Build & Deploy / 🚀 Deploy (push) Successful in 18s
Build & Deploy / 🧪 Post-Deploy Verification (push) Has been skipped
Build & Deploy / 🔔 Notify (push) Successful in 2s
2026-06-11 18:21:16 +02:00
f798afe917 feat: generate only white variant by default and automatically build CMYK version for Viaprinto
Some checks failed
Build & Deploy / 🔍 Prepare (push) Successful in 19s
Build & Deploy / 🏗️ Build (push) Has been cancelled
Build & Deploy / 🚀 Deploy (push) Has been cancelled
Build & Deploy / 🧪 Post-Deploy Verification (push) Has been cancelled
Build & Deploy / 🔔 Notify (push) Has been cancelled
Build & Deploy / 🧪 QA (push) Has been cancelled
2026-06-11 18:20:29 +02:00
1623e04fce chore: commit newly generated business card PDFs with final layout
Some checks failed
Build & Deploy / 🔍 Prepare (push) Successful in 14s
Build & Deploy / 🧪 QA (push) Successful in 1m21s
Build & Deploy / 🚀 Deploy (push) Has been cancelled
Build & Deploy / 🏗️ Build (push) Has been cancelled
Build & Deploy / 🧪 Post-Deploy Verification (push) Has been cancelled
Build & Deploy / 🔔 Notify (push) Has been cancelled
2026-06-11 18:17:32 +02:00
b77eb2087e style: remove address footer top border and add explicit bottom margin
All checks were successful
Build & Deploy / 🔍 Prepare (push) Successful in 25s
Build & Deploy / 🧪 QA (push) Successful in 1m27s
Build & Deploy / 🏗️ Build (push) Successful in 2m45s
Build & Deploy / 🚀 Deploy (push) Successful in 21s
Build & Deploy / 🧪 Post-Deploy Verification (push) Has been skipped
Build & Deploy / 🔔 Notify (push) Successful in 4s
2026-06-11 18:07:05 +02:00
7e7acff670 style: fix overall vertical spacing, increase bottom bar height for icon breathing room
Some checks failed
Build & Deploy / 🔍 Prepare (push) Successful in 19s
Build & Deploy / 🏗️ Build (push) Has been cancelled
Build & Deploy / 🚀 Deploy (push) Has been cancelled
Build & Deploy / 🧪 Post-Deploy Verification (push) Has been cancelled
Build & Deploy / 🔔 Notify (push) Has been cancelled
Build & Deploy / 🧪 QA (push) Has been cancelled
2026-06-11 18:05:22 +02:00
069400dd86 fix: register Inter woff font for curve outlining, fix tagline spacing
Some checks failed
Build & Deploy / 🔍 Prepare (push) Successful in 24s
Build & Deploy / 🧪 QA (push) Successful in 1m23s
Build & Deploy / 🚀 Deploy (push) Has been cancelled
Build & Deploy / 🧪 Post-Deploy Verification (push) Has been cancelled
Build & Deploy / 🔔 Notify (push) Has been cancelled
Build & Deploy / 🏗️ Build (push) Has been cancelled
2026-06-11 18:02:44 +02:00
91f5cb8de3 fix: resolve layout overflow and overlapping footer
Some checks failed
Build & Deploy / 🔍 Prepare (push) Successful in 13s
Build & Deploy / 🧪 QA (push) Successful in 1m22s
Build & Deploy / 🚀 Deploy (push) Has been cancelled
Build & Deploy / 🧪 Post-Deploy Verification (push) Has been cancelled
Build & Deploy / 🔔 Notify (push) Has been cancelled
Build & Deploy / 🏗️ Build (push) Has been cancelled
2026-06-11 17:58:37 +02:00
702313e42c style: sleek full-width address footer and smaller QR code
All checks were successful
Build & Deploy / 🔍 Prepare (push) Successful in 17s
Build & Deploy / 🧪 QA (push) Successful in 1m48s
Build & Deploy / 🏗️ Build (push) Successful in 2m59s
Build & Deploy / 🚀 Deploy (push) Successful in 21s
Build & Deploy / 🧪 Post-Deploy Verification (push) Has been skipped
Build & Deploy / 🔔 Notify (push) Successful in 2s
2026-06-11 17:07:33 +02:00
adafe8ac02 fix: add company address to business card layout
Some checks failed
Build & Deploy / 🔍 Prepare (push) Successful in 14s
Build & Deploy / 🧪 QA (push) Successful in 1m21s
Build & Deploy / 🚀 Deploy (push) Has been cancelled
Build & Deploy / 🧪 Post-Deploy Verification (push) Has been cancelled
Build & Deploy / 🔔 Notify (push) Has been cancelled
Build & Deploy / 🏗️ Build (push) Has been cancelled
2026-06-11 17:04:54 +02:00
3599880f3a fix: format phone number and adjust role font size to prevent wrapping
Some checks failed
Build & Deploy / 🔍 Prepare (push) Successful in 12s
Build & Deploy / 🧪 QA (push) Successful in 1m24s
Build & Deploy / 🚀 Deploy (push) Has been cancelled
Build & Deploy / 🧪 Post-Deploy Verification (push) Has been cancelled
Build & Deploy / 🔔 Notify (push) Has been cancelled
Build & Deploy / 🏗️ Build (push) Has been cancelled
2026-06-11 17:03:05 +02:00
79f0231e26 feat: update data for Johannes Gleich business card
All checks were successful
Build & Deploy / 🔍 Prepare (push) Successful in 13s
Build & Deploy / 🧪 QA (push) Successful in 1m23s
Build & Deploy / 🏗️ Build (push) Successful in 2m59s
Build & Deploy / 🚀 Deploy (push) Successful in 21s
Build & Deploy / 🧪 Post-Deploy Verification (push) Has been skipped
Build & Deploy / 🔔 Notify (push) Successful in 3s
2026-06-11 15:57:12 +02:00
cdf2af79ca fix(test): resolve typescript compiler error in e2e test
All checks were successful
Build & Deploy / 🔍 Prepare (push) Successful in 15s
Build & Deploy / 🧪 QA (push) Successful in 1m31s
Build & Deploy / 🏗️ Build (push) Successful in 3m7s
Build & Deploy / 🚀 Deploy (push) Successful in 24s
Build & Deploy / 🧪 Post-Deploy Verification (push) Successful in 1m37s
Build & Deploy / 🔔 Notify (push) Successful in 3s
Nightly QA / 💨 Smoke & Health (push) Successful in 4m58s
2026-06-11 13:51:43 +02:00
19 changed files with 10297 additions and 103 deletions

9933
lib/TruckBlueprint.tsx Normal file

File diff suppressed because one or more lines are too long

View File

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

After

Width:  |  Height:  |  Size: 6.4 KiB

View File

@@ -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?`);
}
}
}

View 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);

View 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!');

View File

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