Compare commits
57 Commits
fix/contac
...
v2.3.25
| Author | SHA1 | Date | |
|---|---|---|---|
| 52439cc5aa | |||
| 61bf8517d8 | |||
| 85d9e3a930 | |||
| 2f7d6befe4 | |||
| a6ba484faa | |||
| d7178a9100 | |||
| 1e39dc8567 | |||
| 3c2aa8b71e | |||
| 6e3befdfd9 | |||
| 5377b3c81a | |||
| 941625a4e0 | |||
| 9b251101d4 | |||
| ff0d880bde | |||
| b78a848ec2 | |||
| e72879b222 | |||
| 5a2b0c543e | |||
| 1203c20166 | |||
| 9944d3313b | |||
| d1cbfc41c8 | |||
| 09d435a1bc | |||
| 8f34ac2a6d | |||
| e5ecf16a66 | |||
| 12eadba0ee | |||
| 0ad975f049 | |||
| b53ea82be8 | |||
| 3d7a061858 | |||
| 44ad62de1c | |||
| 092325baa9 | |||
| a0910dd932 | |||
| db251687d1 | |||
| eaf17315ff | |||
| 6bab5c9173 | |||
| cf371e48ef | |||
| ec0c71a6a6 | |||
| 1b7fecff4a | |||
| b7f2d932a9 | |||
| aa3b0b05b8 | |||
| 4c6e8cb28b | |||
| 175cb10e01 | |||
| 5e8a175a53 | |||
| 9f64304539 | |||
| 6367efdf6f | |||
| 22ae008f29 | |||
| 118319ceab | |||
| 5d94337df1 | |||
| 3798d37fcd | |||
| efe3cbc58e | |||
| f798afe917 | |||
| 1623e04fce | |||
| b77eb2087e | |||
| 7e7acff670 | |||
| 069400dd86 | |||
| 91f5cb8de3 | |||
| 702313e42c | |||
| adafe8ac02 | |||
| 3599880f3a | |||
| 79f0231e26 |
@@ -22,6 +22,8 @@ import MeetTheTeam from './home/MeetTheTeam';
|
||||
import GallerySection from './home/GallerySection';
|
||||
import VideoSection from './home/VideoSection';
|
||||
import CTA from './home/CTA';
|
||||
import { AgbHistoryBlock } from './AgbHistoryBlock';
|
||||
import { PDFDownloadBlock } from './PDFDownloadBlock';
|
||||
function ContactSection(props: any) {
|
||||
return (
|
||||
<div className="p-8 border-2 border-dashed border-primary my-8 text-center text-primary font-bold">
|
||||
@@ -94,6 +96,10 @@ async function Block(props: any) {
|
||||
return <VideoSection data={data} />;
|
||||
case 'homeCTA':
|
||||
return <CTA data={data} />;
|
||||
case 'agbHistory':
|
||||
return <AgbHistoryBlock {...data} />;
|
||||
case 'pdfDownload':
|
||||
return <PDFDownloadBlock {...data} />;
|
||||
|
||||
default:
|
||||
return (
|
||||
|
||||
@@ -3,7 +3,11 @@
|
||||
import React from 'react';
|
||||
import { usePathname } from 'next/navigation';
|
||||
|
||||
export const PDFDownloadBlock: React.FC<{ label: string; style: string }> = ({ label, style }) => {
|
||||
export const PDFDownloadBlock: React.FC<{ label: string; style: string; url?: string }> = ({
|
||||
label,
|
||||
style,
|
||||
url,
|
||||
}) => {
|
||||
const pathname = usePathname();
|
||||
|
||||
// Extract slug from pathname
|
||||
@@ -12,7 +16,7 @@ export const PDFDownloadBlock: React.FC<{ label: string; style: string }> = ({ l
|
||||
// We want the page slug.
|
||||
const slug = segments[segments.length - 1] || 'home';
|
||||
|
||||
const href = `/api/pages/${slug}/pdf`;
|
||||
const href = url || `/api/pages/${slug}/pdf`;
|
||||
|
||||
return (
|
||||
<div className="my-8">
|
||||
|
||||
@@ -247,7 +247,7 @@ _status: published
|
||||
|
||||
Stand: April 2026
|
||||
|
||||
<Block type="pdfDownload" data={{"id":"69a9b09ea220fecfb9d54bb8","label":"Als PDF herunterladen","style":"primary","blockName":"","blockType":"pdfDownload"}} />
|
||||
<Block type="pdfDownload" data={{"id":"69a9b09ea220fecfb9d54bb8","label":"Als PDF herunterladen","style":"primary","url":"/AVB-KLZ-4-2026.pdf","blockName":"","blockType":"pdfDownload"}} />
|
||||
|
||||
<Block type="agbHistory" data={{"id":"69a9b09ea220fecfb9d54bb9","blockName":"AGB History","blockType":"agbHistory","title":"Vorherige Versionen"}} />
|
||||
|
||||
|
||||
9843
lib/TruckBlueprint.tsx
Normal file
9843
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,17 @@ export interface PDFBusinessCardProps {
|
||||
person: PersonData;
|
||||
qrCodeDataUrl: string;
|
||||
variant?: 'blue' | 'white' | 'black';
|
||||
isViaprinto?: boolean;
|
||||
withTruck?: boolean;
|
||||
withCropMarks?: boolean;
|
||||
}
|
||||
|
||||
// Mintel Aesthetics Colors - EXACT PROD COLORS
|
||||
// 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];
|
||||
|
||||
const COLORS = {
|
||||
primary: '#011dff', // Brand Royal Blue
|
||||
primary: '#000a66', // Brand Dark Blue (from website)
|
||||
blue: '#000a66',
|
||||
green: '#82ed20', // Brand Accent Green
|
||||
darkGreen: '#4A9E00', // Darker green for white background visibility
|
||||
white: '#ffffff',
|
||||
@@ -42,13 +66,105 @@ const COLORS = {
|
||||
lightGray: '#f8f9fa',
|
||||
};
|
||||
|
||||
const CropMarks = () => (
|
||||
<View style={{ position: 'absolute', top: 0, left: 0, right: 0, bottom: 0, zIndex: 9999 }}>
|
||||
{/* Top Left */}
|
||||
<View
|
||||
style={{
|
||||
position: 'absolute',
|
||||
left: '13mm',
|
||||
top: '0mm',
|
||||
width: '0.25pt',
|
||||
height: '10mm',
|
||||
backgroundColor: '#000',
|
||||
}}
|
||||
/>
|
||||
<View
|
||||
style={{
|
||||
position: 'absolute',
|
||||
left: '0mm',
|
||||
top: '13mm',
|
||||
width: '10mm',
|
||||
height: '0.25pt',
|
||||
backgroundColor: '#000',
|
||||
}}
|
||||
/>
|
||||
|
||||
{/* Top Right */}
|
||||
<View
|
||||
style={{
|
||||
position: 'absolute',
|
||||
right: '13mm',
|
||||
top: '0mm',
|
||||
width: '0.25pt',
|
||||
height: '10mm',
|
||||
backgroundColor: '#000',
|
||||
}}
|
||||
/>
|
||||
<View
|
||||
style={{
|
||||
position: 'absolute',
|
||||
right: '0mm',
|
||||
top: '13mm',
|
||||
width: '10mm',
|
||||
height: '0.25pt',
|
||||
backgroundColor: '#000',
|
||||
}}
|
||||
/>
|
||||
|
||||
{/* Bottom Left */}
|
||||
<View
|
||||
style={{
|
||||
position: 'absolute',
|
||||
left: '13mm',
|
||||
bottom: '0mm',
|
||||
width: '0.25pt',
|
||||
height: '10mm',
|
||||
backgroundColor: '#000',
|
||||
}}
|
||||
/>
|
||||
<View
|
||||
style={{
|
||||
position: 'absolute',
|
||||
left: '0mm',
|
||||
bottom: '13mm',
|
||||
width: '10mm',
|
||||
height: '0.25pt',
|
||||
backgroundColor: '#000',
|
||||
}}
|
||||
/>
|
||||
|
||||
{/* Bottom Right */}
|
||||
<View
|
||||
style={{
|
||||
position: 'absolute',
|
||||
right: '13mm',
|
||||
bottom: '0mm',
|
||||
width: '0.25pt',
|
||||
height: '10mm',
|
||||
backgroundColor: '#000',
|
||||
}}
|
||||
/>
|
||||
<View
|
||||
style={{
|
||||
position: 'absolute',
|
||||
right: '0mm',
|
||||
bottom: '13mm',
|
||||
width: '10mm',
|
||||
height: '0.25pt',
|
||||
backgroundColor: '#000',
|
||||
}}
|
||||
/>
|
||||
</View>
|
||||
);
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
pageBleed: {
|
||||
width: '91mm',
|
||||
height: '61mm',
|
||||
backgroundColor: COLORS.primary,
|
||||
position: 'relative',
|
||||
fontFamily: 'Helvetica',
|
||||
fontFamily: 'Inter',
|
||||
overflow: 'hidden',
|
||||
},
|
||||
pageBleedBack: {
|
||||
@@ -56,7 +172,7 @@ const styles = StyleSheet.create({
|
||||
height: '61mm',
|
||||
backgroundColor: COLORS.white,
|
||||
position: 'relative',
|
||||
fontFamily: 'Helvetica',
|
||||
fontFamily: 'Inter',
|
||||
overflow: 'hidden',
|
||||
},
|
||||
|
||||
@@ -66,100 +182,97 @@ const styles = StyleSheet.create({
|
||||
top: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
bottom: '16mm', // Leave space for the bar below
|
||||
bottom: '20mm', // Truck stops above the footer area to avoid text conflict
|
||||
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
|
||||
paddingTop: '3mm', // Compensate for top bleed for perfect optical centering
|
||||
paddingBottom: '20mm', // Leave space for the footer area
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
},
|
||||
frontLogo: {
|
||||
width: 175,
|
||||
width: 145, // Reduced for a more elegant, premium look with more negative space
|
||||
zIndex: 100,
|
||||
},
|
||||
frontTagline: {
|
||||
color: COLORS.white,
|
||||
fontSize: 6,
|
||||
marginTop: 10,
|
||||
letterSpacing: 1.5,
|
||||
fontSize: 5.5,
|
||||
marginTop: 8,
|
||||
letterSpacing: 2,
|
||||
fontWeight: 'bold',
|
||||
textTransform: 'uppercase',
|
||||
zIndex: 100,
|
||||
},
|
||||
frontGreenBarBottom: {
|
||||
frontFooterArea: {
|
||||
position: 'absolute',
|
||||
bottom: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
height: '16mm',
|
||||
borderTop: `2pt solid ${COLORS.green}`,
|
||||
backgroundColor: '#000a66',
|
||||
height: '22mm', // Increased height to accommodate more bottom padding
|
||||
paddingBottom: '10mm', // Increased bottom padding to move icons further from edge
|
||||
zIndex: 50,
|
||||
flexDirection: 'column',
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
gap: 6, // Increased gap for more spacing under tagline
|
||||
gap: 6, // Harmonious spacing to icons
|
||||
},
|
||||
frontIconsGroup: {
|
||||
flexDirection: 'row',
|
||||
gap: 12,
|
||||
gap: 14, // Wider gaps between icon groups for visual clarity at small size
|
||||
},
|
||||
frontBrandIconWrapper: {
|
||||
alignItems: 'center',
|
||||
gap: 2,
|
||||
gap: 1.5,
|
||||
},
|
||||
frontBrandIcon: {
|
||||
width: 18,
|
||||
height: 18,
|
||||
opacity: 0.9,
|
||||
width: 12, // Smaller for compact bar
|
||||
height: 12,
|
||||
opacity: 0.85,
|
||||
},
|
||||
frontBrandText: {
|
||||
fontSize: 5.5,
|
||||
fontSize: 4, // Tiny but readable at 300dpi print
|
||||
color: COLORS.white,
|
||||
textTransform: 'uppercase',
|
||||
letterSpacing: 0.5,
|
||||
opacity: 0.9,
|
||||
letterSpacing: 0.6,
|
||||
// Full opacity — white on blue needs to pop cleanly
|
||||
},
|
||||
frontTaglineBar: {
|
||||
color: COLORS.green,
|
||||
fontSize: 7,
|
||||
letterSpacing: 1.5,
|
||||
fontSize: 7, // Larger — as requested
|
||||
letterSpacing: 0.5, // Harmonious tracking for mixed case
|
||||
fontWeight: 'bold',
|
||||
textTransform: 'uppercase',
|
||||
},
|
||||
|
||||
// -- WHITE PAGE (Vorderseite) --
|
||||
backContainer: {
|
||||
flex: 1,
|
||||
padding: '8mm',
|
||||
paddingTop: '6mm', // Pushed down as requested
|
||||
paddingLeft: '6mm',
|
||||
paddingRight: '6mm',
|
||||
flexDirection: 'column',
|
||||
justifyContent: 'space-between',
|
||||
},
|
||||
backHeader: {
|
||||
marginBottom: 'auto',
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
marginBottom: '5mm', // Generous breathing room
|
||||
width: '100%',
|
||||
},
|
||||
backLogo: {
|
||||
width: 70,
|
||||
width: 60,
|
||||
},
|
||||
accentLineHorizontal: {
|
||||
flex: 1,
|
||||
height: 1.5,
|
||||
backgroundColor: COLORS.primary, // Brand blue line
|
||||
marginLeft: 15,
|
||||
marginRight: 0,
|
||||
marginLeft: 12,
|
||||
marginRight: 12,
|
||||
},
|
||||
accentDot: {
|
||||
width: 4,
|
||||
@@ -178,41 +291,41 @@ const styles = StyleSheet.create({
|
||||
top: -5,
|
||||
},
|
||||
backBody: {
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'flex-end',
|
||||
flex: 1,
|
||||
flexDirection: 'column',
|
||||
justifyContent: 'flex-start',
|
||||
alignItems: 'flex-start',
|
||||
width: '100%',
|
||||
},
|
||||
leftColumn: {
|
||||
flex: 1,
|
||||
width: '100%', // Take full width since right column is removed
|
||||
flexDirection: 'column',
|
||||
paddingRight: '4mm',
|
||||
paddingBottom: '2mm',
|
||||
},
|
||||
nameSection: {
|
||||
marginBottom: '5mm',
|
||||
marginBottom: '2.5mm', // Reduced to fix the 'Lücke' under the role
|
||||
},
|
||||
nameText: {
|
||||
fontSize: 16,
|
||||
fontSize: 13,
|
||||
fontWeight: 'bold',
|
||||
color: COLORS.neutralDark,
|
||||
marginBottom: 2,
|
||||
letterSpacing: -0.2,
|
||||
marginBottom: 1.5,
|
||||
letterSpacing: -0.15,
|
||||
},
|
||||
roleText: {
|
||||
fontSize: 9,
|
||||
fontSize: 6.5,
|
||||
fontWeight: 'bold',
|
||||
color: COLORS.primary,
|
||||
letterSpacing: 0.5,
|
||||
color: COLORS.darkGreen,
|
||||
letterSpacing: 1.2,
|
||||
textTransform: 'uppercase',
|
||||
},
|
||||
contactSection: {
|
||||
flexDirection: 'column',
|
||||
gap: 5,
|
||||
gap: 0,
|
||||
},
|
||||
contactRow: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
marginBottom: 5, // Even rhythm between contact lines
|
||||
},
|
||||
iconWrapper: {
|
||||
width: 12,
|
||||
@@ -220,35 +333,48 @@ const styles = StyleSheet.create({
|
||||
alignItems: 'flex-start',
|
||||
},
|
||||
contactText: {
|
||||
fontSize: 9,
|
||||
fontSize: 7.5,
|
||||
color: COLORS.neutralDark,
|
||||
fontWeight: 'normal',
|
||||
paddingTop: 1,
|
||||
paddingTop: 0.5,
|
||||
letterSpacing: 0.1,
|
||||
},
|
||||
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: '10mm',
|
||||
height: '10mm',
|
||||
border: `0.75pt solid ${COLORS.darkGreen}`,
|
||||
padding: 2,
|
||||
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: 'flex-start',
|
||||
alignItems: 'center',
|
||||
position: 'absolute',
|
||||
bottom: '7mm', // 3mm bleed + 4mm safe margin
|
||||
left: '6mm', // Match backContainer paddingLeft for flush alignment
|
||||
},
|
||||
addressFooterText: {
|
||||
fontSize: 6.5,
|
||||
color: COLORS.grayText,
|
||||
letterSpacing: 0.4,
|
||||
fontWeight: 'normal',
|
||||
},
|
||||
});
|
||||
|
||||
@@ -280,6 +406,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 +450,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>
|
||||
);
|
||||
@@ -550,7 +724,7 @@ export const IconHighVoltage = ({ style, color = 'white' }: { style: any; color?
|
||||
style={{ fill: 'none', stroke: color, strokeWidth: '0.33px' }}
|
||||
/>
|
||||
<Path
|
||||
d="M28.597,16.426c-0,-1.03 -0.837,-1.867 -1.868,-1.867c-1.03,0 -1.867,0.837 -1.867,1.867c0,1.031 0.837,1.868 1.867,1.868c1.031,-0 1.868,-0.837 1.868,-1.868Z"
|
||||
d="M28.597,16.426c-0,-1.03 -0.837,-1.868 -1.868,-1.867c-1.03,0 -1.867,0.837 -1.867,1.867c0,1.031 0.837,1.868 1.867,1.868c1.031,-0 1.868,-0.837 1.868,-1.868Z"
|
||||
style={{ fill: 'none', stroke: color, strokeWidth: '0.33px' }}
|
||||
/>
|
||||
<Path
|
||||
@@ -610,6 +784,9 @@ export const PDFBusinessCard: React.FC<PDFBusinessCardProps> = ({
|
||||
person,
|
||||
qrCodeDataUrl,
|
||||
variant = 'blue',
|
||||
isViaprinto = false,
|
||||
withTruck = true,
|
||||
withCropMarks = false,
|
||||
}) => {
|
||||
const isWhiteVariant = variant === 'white' || variant === 'black';
|
||||
|
||||
@@ -637,10 +814,36 @@ export const PDFBusinessCard: React.FC<PDFBusinessCardProps> = ({
|
||||
return logoWhitePaths;
|
||||
};
|
||||
|
||||
const getBackLogoPaths = () => {
|
||||
// We use the original black logo to preserve the exact colors of the Truck and KLZ.
|
||||
// The tagline will be hidden via pathOverrides in the JSX.
|
||||
return logoBlackWithTaglinePaths;
|
||||
};
|
||||
|
||||
const bgImagePath = isWhiteVariant
|
||||
? path.join(process.cwd(), 'public', 'media', 'cable_drums_truck.png')
|
||||
: path.join(process.cwd(), 'public', 'cable_drums_bg.png');
|
||||
|
||||
const renderPageWrapper = (children: React.ReactNode, bleedStyle: any) => {
|
||||
if (withCropMarks) {
|
||||
return (
|
||||
<Page size={[315, 230]} style={{ backgroundColor: '#ffffff' }}>
|
||||
<View style={{ flex: 1, justifyContent: 'center', alignItems: 'center' }}>
|
||||
<CropMarks />
|
||||
<View style={[bleedStyle, { overflow: 'hidden', position: 'relative' }]}>
|
||||
{children}
|
||||
</View>
|
||||
</View>
|
||||
</Page>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<Page size={[258, 173]} style={bleedStyle}>
|
||||
{children}
|
||||
</Page>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<Document
|
||||
title={`Business Card - ${person.name}`}
|
||||
@@ -648,75 +851,100 @@ export const PDFBusinessCard: React.FC<PDFBusinessCardProps> = ({
|
||||
creator="KLZ Print Engine"
|
||||
>
|
||||
{/* VORDERSEITE (Blue page with logo, truck and tagline) */}
|
||||
<Page
|
||||
size={[258, 173]}
|
||||
style={[styles.pageBleed, isWhiteVariant && { backgroundColor: '#ffffff' }]}
|
||||
>
|
||||
<View style={styles.truckWrapper}>
|
||||
<Image
|
||||
src={bgImagePath}
|
||||
style={[styles.frontBackgroundImage, isWhiteVariant && { opacity: 0.2 }]}
|
||||
/>
|
||||
</View>
|
||||
<View
|
||||
style={[
|
||||
styles.frontGreenBarBottom,
|
||||
isWhiteVariant && { backgroundColor: '#ffffff', borderTopColor: COLORS.darkGreen },
|
||||
]}
|
||||
>
|
||||
<Text style={[styles.frontTaglineBar, isWhiteVariant && { color: COLORS.black }]}>
|
||||
VON 0,6/1,0 KV BIS 64/110 KV
|
||||
</Text>
|
||||
<View style={styles.frontIconsGroup}>
|
||||
<View style={styles.frontBrandIconWrapper}>
|
||||
<IconLowVoltage
|
||||
style={styles.frontBrandIcon}
|
||||
color={isWhiteVariant ? COLORS.black : 'white'}
|
||||
/>
|
||||
<Text style={[styles.frontBrandText, isWhiteVariant && { color: COLORS.black }]}>
|
||||
Niederspannung
|
||||
</Text>
|
||||
</View>
|
||||
<View style={styles.frontBrandIconWrapper}>
|
||||
<IconMediumVoltage
|
||||
style={styles.frontBrandIcon}
|
||||
color={isWhiteVariant ? COLORS.black : 'white'}
|
||||
/>
|
||||
<Text style={[styles.frontBrandText, isWhiteVariant && { color: COLORS.black }]}>
|
||||
Mittelspannung
|
||||
</Text>
|
||||
</View>
|
||||
<View style={styles.frontBrandIconWrapper}>
|
||||
<IconHighVoltage
|
||||
style={styles.frontBrandIcon}
|
||||
color={isWhiteVariant ? COLORS.black : 'white'}
|
||||
/>
|
||||
<Text style={[styles.frontBrandText, isWhiteVariant && { color: COLORS.black }]}>
|
||||
Hochspannung
|
||||
</Text>
|
||||
</View>
|
||||
<View style={styles.frontBrandIconWrapper}>
|
||||
<IconSolar
|
||||
style={styles.frontBrandIcon}
|
||||
color={isWhiteVariant ? COLORS.black : 'white'}
|
||||
/>
|
||||
<Text style={[styles.frontBrandText, isWhiteVariant && { color: COLORS.black }]}>
|
||||
Solar
|
||||
</Text>
|
||||
{renderPageWrapper(
|
||||
<>
|
||||
<View style={styles.frontFooterArea}>
|
||||
<Text style={[styles.frontTaglineBar, isWhiteVariant && { color: COLORS.black }]}>
|
||||
Von 0,6/1,0 kV bis 64/110 kV
|
||||
</Text>
|
||||
<View style={styles.frontIconsGroup}>
|
||||
<View style={styles.frontBrandIconWrapper}>
|
||||
<IconLowVoltage
|
||||
style={styles.frontBrandIcon}
|
||||
color={isWhiteVariant ? COLORS.black : 'white'}
|
||||
/>
|
||||
<Text style={[styles.frontBrandText, isWhiteVariant && { color: COLORS.black }]}>
|
||||
Niederspannung
|
||||
</Text>
|
||||
</View>
|
||||
<View style={styles.frontBrandIconWrapper}>
|
||||
<IconMediumVoltage
|
||||
style={styles.frontBrandIcon}
|
||||
color={isWhiteVariant ? COLORS.black : 'white'}
|
||||
/>
|
||||
<Text style={[styles.frontBrandText, isWhiteVariant && { color: COLORS.black }]}>
|
||||
Mittelspannung
|
||||
</Text>
|
||||
</View>
|
||||
<View style={styles.frontBrandIconWrapper}>
|
||||
<IconHighVoltage
|
||||
style={styles.frontBrandIcon}
|
||||
color={isWhiteVariant ? COLORS.black : 'white'}
|
||||
/>
|
||||
<Text style={[styles.frontBrandText, isWhiteVariant && { color: COLORS.black }]}>
|
||||
Hochspannung
|
||||
</Text>
|
||||
</View>
|
||||
|
||||
<View style={styles.frontBrandIconWrapper}>
|
||||
<IconSolar
|
||||
style={styles.frontBrandIcon}
|
||||
color={isWhiteVariant ? COLORS.black : 'white'}
|
||||
/>
|
||||
<Text style={[styles.frontBrandText, isWhiteVariant && { color: COLORS.black }]}>
|
||||
Solar
|
||||
</Text>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
<View style={styles.frontContainer}>
|
||||
<VectorLogo paths={getFrontLogoPaths()} style={styles.frontLogo} />
|
||||
</View>
|
||||
</Page>
|
||||
<View style={styles.frontContainer}>
|
||||
{/* 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' : COLORS.primary}
|
||||
/>
|
||||
|
||||
{/* RÜCKSEITE (White page with contact info) */}
|
||||
<Page size={[258, 173]} style={styles.pageBleedBack}>
|
||||
{/* 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>
|
||||
</>,
|
||||
[styles.pageBleed, isWhiteVariant && { backgroundColor: '#ffffff' }],
|
||||
)}
|
||||
|
||||
{renderPageWrapper(
|
||||
<View style={styles.backContainer}>
|
||||
<View style={styles.backHeader}>
|
||||
<VectorLogo paths={logoBluePaths} style={styles.backLogo} />
|
||||
<VectorLogo
|
||||
paths={getBackLogoPaths()}
|
||||
style={styles.backLogo}
|
||||
overrideWhiteColor={COLORS.black}
|
||||
pathOverrides={{
|
||||
...(KLZ_PATHS.reduce(
|
||||
(acc: Record<number, string>, idx) => ({ ...acc, [idx]: 'none' }),
|
||||
{} as Record<number, string>,
|
||||
) as any),
|
||||
// Paths 0-24 are the "Cables for a greener tomorrow" tagline in the 40-path logo.
|
||||
...(Array.from({ length: 25 }).reduce(
|
||||
(acc: Record<number, string>, _, i) => ({ ...acc, [i]: 'none' }),
|
||||
{} as Record<number, string>,
|
||||
) as any),
|
||||
}}
|
||||
/>
|
||||
<View style={styles.accentLineHorizontal} />
|
||||
<View style={styles.qrContainer}>
|
||||
<View style={[styles.qrCodeWrapper, { borderColor: COLORS.darkGreen }]}>
|
||||
<Image src={qrCodeDataUrl} style={styles.qrCode} />
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
<View style={styles.backBody}>
|
||||
@@ -737,7 +965,7 @@ export const PDFBusinessCard: React.FC<PDFBusinessCardProps> = ({
|
||||
<View style={styles.iconWrapper}>
|
||||
<EmailIcon />
|
||||
</View>
|
||||
<Text style={styles.contactText}>{person.email}</Text>
|
||||
<Text style={styles.contactText}>{person.email.toLowerCase()}</Text>
|
||||
</View>
|
||||
<View style={styles.contactRow}>
|
||||
<View style={styles.iconWrapper}>
|
||||
@@ -747,18 +975,16 @@ 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>
|
||||
</Page>
|
||||
|
||||
<View style={styles.addressFooter}>
|
||||
<Text style={styles.addressFooterText}>
|
||||
KLZ Vertriebs GmbH · Raiffeisenstraße 22 · 73630 Remshalden
|
||||
</Text>
|
||||
</View>
|
||||
</View>,
|
||||
styles.pageBleedBack,
|
||||
)}
|
||||
</Document>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -329,6 +329,42 @@ const renderLexicalNode = (node: any, idx: number): React.ReactNode => {
|
||||
}
|
||||
};
|
||||
|
||||
const renderMarkdown = (markdown: string) => {
|
||||
const blocks = markdown.split('\n\n').filter((b) => b.trim());
|
||||
return blocks.map((block, idx) => {
|
||||
if (block.startsWith('### ')) {
|
||||
return (
|
||||
<Text key={idx} style={styles.heading3} wrap={false} minPresenceAhead={100}>
|
||||
{block.replace('### ', '').trim()}
|
||||
</Text>
|
||||
);
|
||||
}
|
||||
if (block.startsWith('## ')) {
|
||||
return (
|
||||
<Text key={idx} style={styles.heading2} wrap={false} minPresenceAhead={100}>
|
||||
{block.replace('## ', '').trim()}
|
||||
</Text>
|
||||
);
|
||||
}
|
||||
if (block.startsWith('# ')) {
|
||||
return (
|
||||
<View key={idx} style={styles.heading1Wrapper} wrap={false} minPresenceAhead={100}>
|
||||
<Text style={styles.heading1}>{block.replace('# ', '').trim()}</Text>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
// Ignore block components like <Block ... />
|
||||
if (block.startsWith('<Block')) return null;
|
||||
|
||||
return (
|
||||
<Text key={idx} style={styles.paragraph} minPresenceAhead={15}>
|
||||
{block.trim()}
|
||||
</Text>
|
||||
);
|
||||
});
|
||||
};
|
||||
|
||||
interface PDFPageProps {
|
||||
page: any;
|
||||
locale?: string;
|
||||
@@ -360,9 +396,11 @@ export const PDFPage: React.FC<PDFPageProps> = ({ page, locale = 'de' }) => {
|
||||
|
||||
<View style={styles.content}>
|
||||
<View>
|
||||
{page.content?.root?.children?.map((node: any, i: number) =>
|
||||
renderLexicalNode(node, i),
|
||||
)}
|
||||
{typeof page.content === 'string'
|
||||
? renderMarkdown(page.content)
|
||||
: page.content?.root?.children?.map((node: any, i: number) =>
|
||||
renderLexicalNode(node, i),
|
||||
)}
|
||||
</View>
|
||||
</View>
|
||||
|
||||
|
||||
@@ -116,7 +116,7 @@
|
||||
"prepare": "husky",
|
||||
"preinstall": "npx only-allow pnpm"
|
||||
},
|
||||
"version": "2.3.23",
|
||||
"version": "2.3.25",
|
||||
"pnpm": {
|
||||
"onlyBuiltDependencies": [
|
||||
"@parcel/watcher",
|
||||
|
||||
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.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
10
public/v/johannesgleich.vcf
Normal file
10
public/v/johannesgleich.vcf
Normal file
@@ -0,0 +1,10 @@
|
||||
BEGIN:VCARD
|
||||
VERSION:3.0
|
||||
N:Gleich;Johannes;;;
|
||||
FN:Johannes Gleich
|
||||
ORG:KLZ Vertriebs GmbH
|
||||
TITLE:Senior Key Account Manager
|
||||
TEL;TYPE=WORK,VOICE:+49 172 739 1109
|
||||
EMAIL;TYPE=PREF,INTERNET:johannes.gleich@klz-cables.com
|
||||
URL:https://klz-cables.com
|
||||
END:VCARD
|
||||
10
public/v/klausmintel.vcf
Normal file
10
public/v/klausmintel.vcf
Normal file
@@ -0,0 +1,10 @@
|
||||
BEGIN:VCARD
|
||||
VERSION:3.0
|
||||
N:Mintel;Klaus;;;
|
||||
FN:Klaus Mintel
|
||||
ORG:KLZ Vertriebs GmbH
|
||||
TITLE:Managing Director
|
||||
TEL;TYPE=WORK,VOICE:+49 151 1775 2873
|
||||
EMAIL;TYPE=PREF,INTERNET:klaus.mintel@klz-cables.com
|
||||
URL:https://klz-cables.com
|
||||
END:VCARD
|
||||
10
public/v/michaelbodemer.vcf
Normal file
10
public/v/michaelbodemer.vcf
Normal file
@@ -0,0 +1,10 @@
|
||||
BEGIN:VCARD
|
||||
VERSION:3.0
|
||||
N:Bodemer;Michael;;;
|
||||
FN:Michael Bodemer
|
||||
ORG:KLZ Vertriebs GmbH
|
||||
TITLE:Owner & Managing Director
|
||||
TEL;TYPE=WORK,VOICE:+49 151 462 467 98
|
||||
EMAIL;TYPE=PREF,INTERNET:michael.bodemer@klz-cables.com
|
||||
URL:https://klz-cables.com
|
||||
END:VCARD
|
||||
@@ -19,13 +19,15 @@ 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',
|
||||
role: 'Owner & Managing Director',
|
||||
phone: '+49 151 462 467 98',
|
||||
email: 'michael.bodemer@klz-cables.com',
|
||||
},
|
||||
{
|
||||
@@ -36,8 +38,8 @@ const PEOPLE: PersonData[] = [
|
||||
},
|
||||
{
|
||||
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',
|
||||
},
|
||||
];
|
||||
@@ -50,90 +52,119 @@ async function generateBusinessCards() {
|
||||
for (const person of PEOPLE) {
|
||||
console.log(`\nGenerating Business Card for: ${person.name}`);
|
||||
|
||||
// Generate VCard QR Code
|
||||
const lastName = person.name.split(' ').slice(1).join(' ');
|
||||
const firstName = person.name.split(' ')[0];
|
||||
const vcard = `BEGIN:VCARD
|
||||
VERSION:3.0
|
||||
N:${lastName};${firstName};;;
|
||||
FN:${person.name}
|
||||
ORG:KLZ Vertriebs GmbH
|
||||
TITLE:${person.role}
|
||||
TEL;TYPE=WORK,VOICE:${person.phone}
|
||||
EMAIL;TYPE=PREF,INTERNET:${person.email}
|
||||
URL:https://klz-cables.com
|
||||
END:VCARD`;
|
||||
|
||||
const qrCodeDataUrl = await QRCode.toDataURL(vcard, {
|
||||
// To make the QR code extremely coarse (minimalist), we cannot encode all contact details into it directly.
|
||||
// Instead, we create a physical .vcf file and encode ONLY its URL into the QR Code (e.g. 35 chars vs 140 chars).
|
||||
const shortId = person.name.toLowerCase().replace(/[^a-z0-9]/g, ''); // "michaelbodemer"
|
||||
const vcfUrl = `https://klz-cables.com/v/${shortId}.vcf`;
|
||||
|
||||
const vcardFull = `BEGIN:VCARD\r\nVERSION:3.0\r\nN:${lastName};${firstName};;;\r\nFN:${person.name}\r\nORG:KLZ Vertriebs GmbH\r\nTITLE:${person.role}\r\nTEL;TYPE=WORK,VOICE:${person.phone}\r\nEMAIL;TYPE=PREF,INTERNET:${person.email}\r\nURL:https://klz-cables.com\r\nEND:VCARD`;
|
||||
|
||||
// Save the physical .vcf file so it can be served by Next.js from /public/v/
|
||||
const vcardDir = path.join(process.cwd(), 'public/v');
|
||||
if (!fs.existsSync(vcardDir)) fs.mkdirSync(vcardDir, { recursive: true });
|
||||
fs.writeFileSync(path.join(vcardDir, `${shortId}.vcf`), vcardFull);
|
||||
|
||||
const qrCodeDataUrl = await QRCode.toDataURL(vcfUrl, {
|
||||
width: 400,
|
||||
margin: 0,
|
||||
errorCorrectionLevel: 'L', // Low error correction = less dots
|
||||
color: { dark: '#000000', light: '#ffffff' },
|
||||
});
|
||||
|
||||
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}`);
|
||||
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?`);
|
||||
}
|
||||
|
||||
// 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);
|
||||
// 3. NO TRUCK PDF (RGB)
|
||||
console.log(`- Rendering No Truck Variant (RGB)...`);
|
||||
const noTruckFileName = `KLZ_Visitenkarte_${safeName}_NoTruck.pdf`;
|
||||
const tempNoTruckPath = path.join(CONFIG.outputDir, `temp_${noTruckFileName}`);
|
||||
const finalNoTruckPath = path.join(CONFIG.outputDir, noTruckFileName);
|
||||
|
||||
console.log(`- Rendering Black Variant...`);
|
||||
const bufferBlack = await renderToBuffer(
|
||||
buffer = await renderToBuffer(
|
||||
React.createElement(PDFBusinessCard, {
|
||||
person,
|
||||
qrCodeDataUrl,
|
||||
variant: 'black',
|
||||
variant: 'blue',
|
||||
isViaprinto: false,
|
||||
withTruck: false,
|
||||
}),
|
||||
);
|
||||
fs.writeFileSync(tempNoTruckPath, buffer);
|
||||
const gsNoTruckCommand = `gs -dNoOutputFonts -sDEVICE=pdfwrite -o "${finalNoTruckPath}" "${tempNoTruckPath}"`;
|
||||
execSync(gsNoTruckCommand, { stdio: 'inherit' });
|
||||
fs.unlinkSync(tempNoTruckPath);
|
||||
// 4. CROP MARKS PDF (RGB)
|
||||
console.log(`- Rendering Crop Marks Variant (RGB)...`);
|
||||
const cropMarksFileName = `KLZ_Visitenkarte_${safeName}_CropMarks.pdf`;
|
||||
const tempCropMarksPath = path.join(CONFIG.outputDir, `temp_${cropMarksFileName}`);
|
||||
const finalCropMarksPath = path.join(CONFIG.outputDir, cropMarksFileName);
|
||||
|
||||
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}`);
|
||||
buffer = await renderToBuffer(
|
||||
React.createElement(PDFBusinessCard, {
|
||||
person,
|
||||
qrCodeDataUrl,
|
||||
variant: 'blue',
|
||||
isViaprinto: false,
|
||||
withCropMarks: true,
|
||||
}),
|
||||
);
|
||||
fs.writeFileSync(tempCropMarksPath, buffer);
|
||||
const gsCropMarksCommand = `gs -dNoOutputFonts -sDEVICE=pdfwrite -o "${finalCropMarksPath}" "${tempCropMarksPath}"`;
|
||||
execSync(gsCropMarksCommand, { stdio: 'inherit' });
|
||||
fs.unlinkSync(tempCropMarksPath);
|
||||
console.log(`✓ Crop Marks Print File generated: ${cropMarksFileName}`);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
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!');
|
||||
Reference in New Issue
Block a user