Compare commits
46 Commits
fix/contac
...
v2.3.24
| Author | SHA1 | Date | |
|---|---|---|---|
| 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 (
|
||||
|
||||
9843
lib/TruckBlueprint.tsx
Normal 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,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: '20mm', // Generous area for the footer
|
||||
paddingBottom: '7mm', // 3mm bleed + 4mm safe margin
|
||||
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: '3mm', // Reduced significantly to pull content up
|
||||
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', // Clear separation between name block and contact block
|
||||
},
|
||||
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: '12mm',
|
||||
height: '12mm',
|
||||
border: `0.75pt solid ${COLORS.darkGreen}`,
|
||||
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: '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,34 @@ export const PDFBusinessCard: React.FC<PDFBusinessCardProps> = ({
|
||||
return logoWhitePaths;
|
||||
};
|
||||
|
||||
const getBackLogoPaths = () => {
|
||||
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 +849,90 @@ 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, idx) => ({ ...acc, [idx]: 'none' }), {})}
|
||||
/>
|
||||
<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 +953,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 +963,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>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -116,7 +116,7 @@
|
||||
"prepare": "husky",
|
||||
"preinstall": "npx only-allow pnpm"
|
||||
},
|
||||
"version": "2.3.23",
|
||||
"version": "2.3.24",
|
||||
"pnpm": {
|
||||
"onlyBuiltDependencies": [
|
||||
"@parcel/watcher",
|
||||
|
||||
BIN
public/downloads/business-cards/all_pages_1.png
Normal file
|
After Width: | Height: | Size: 30 KiB |
BIN
public/downloads/business-cards/all_pages_2.png
Normal file
|
After Width: | Height: | Size: 30 KiB |
BIN
public/downloads/business-cards/all_pages_3.png
Normal file
|
After Width: | Height: | Size: 7.4 KiB |
BIN
public/downloads/business-cards/all_pages_4.png
Normal file
|
After Width: | Height: | Size: 10 KiB |
BIN
public/downloads/business-cards/cairo_test.png
Normal file
|
After Width: | Height: | Size: 73 KiB |
BIN
public/downloads/business-cards/debug_white_page.png
Normal file
|
After Width: | Height: | Size: 28 KiB |
BIN
public/downloads/business-cards/magick_page_3.png
Normal file
|
After Width: | Height: | Size: 9.5 KiB |
BIN
public/downloads/business-cards/magick_page_3_fixed.png
Normal file
|
After Width: | Height: | Size: 7.8 KiB |
BIN
public/downloads/business-cards/test_notruck_page_1.png
Normal file
|
After Width: | Height: | Size: 10 KiB |
BIN
public/downloads/business-cards/test_notruck_page_2.png
Normal file
|
After Width: | Height: | Size: 7.4 KiB |
BIN
public/downloads/business-cards/test_page_1.png
Normal file
|
After Width: | Height: | Size: 30 KiB |
BIN
public/downloads/business-cards/test_page_1_crop.png
Normal file
|
After Width: | Height: | Size: 13 KiB |
BIN
public/downloads/business-cards/test_page_2.png
Normal file
|
After Width: | Height: | Size: 35 KiB |
BIN
public/downloads/business-cards/test_page_3.png
Normal file
|
After Width: | Height: | Size: 7.4 KiB |
BIN
public/downloads/business-cards/test_page_3_fixed.png
Normal file
|
After Width: | Height: | Size: 6.8 KiB |
BIN
public/downloads/business-cards/test_page_3_johannes.png
Normal file
|
After Width: | Height: | Size: 7.4 KiB |
@@ -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',
|
||||
},
|
||||
];
|
||||
@@ -71,69 +73,96 @@ 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}`);
|
||||
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
@@ -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
@@ -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!');
|
||||