|
| 1 | +/** |
| 2 | + * Shared PDF Generation Helpers |
| 3 | + * |
| 4 | + * Centralized utilities for PDF generation across the app |
| 5 | + * Includes color conversion, style injection, and common settings |
| 6 | + */ |
| 7 | + |
| 8 | +import { TAILWIND_COLOR_REPLACEMENTS } from './utils'; |
| 9 | + |
| 10 | +/** |
| 11 | + * Complete OKLCH to RGB color conversion map |
| 12 | + * Includes all Tailwind CSS colors used in the app |
| 13 | + */ |
| 14 | +export const PDF_COLOR_REPLACEMENTS = { |
| 15 | + ...TAILWIND_COLOR_REPLACEMENTS, |
| 16 | + // Add any custom app colors here if needed |
| 17 | +}; |
| 18 | + |
| 19 | +/** |
| 20 | + * Generate comprehensive CSS for PDF rendering |
| 21 | + * Handles OKLCH color conversion and ensures proper styling |
| 22 | + */ |
| 23 | +export function generatePDFColorCSS(): string { |
| 24 | + return ` |
| 25 | + :root, :host, * { |
| 26 | + /* Red colors */ |
| 27 | + --color-red-50: #fef2f2 !important; |
| 28 | + --color-red-100: #fee2e2 !important; |
| 29 | + --color-red-200: #fecaca !important; |
| 30 | + --color-red-300: #fca5a5 !important; |
| 31 | + --color-red-400: #f87171 !important; |
| 32 | + --color-red-500: #ef4444 !important; |
| 33 | + --color-red-600: #dc2626 !important; |
| 34 | + --color-red-700: #b91c1c !important; |
| 35 | + --color-red-800: #991b1b !important; |
| 36 | + --color-red-900: #7f1d1d !important; |
| 37 | +
|
| 38 | + /* Orange colors */ |
| 39 | + --color-orange-50: #fff7ed !important; |
| 40 | + --color-orange-100: #ffedd5 !important; |
| 41 | + --color-orange-200: #fed7aa !important; |
| 42 | + --color-orange-300: #fdba74 !important; |
| 43 | + --color-orange-400: #fb923c !important; |
| 44 | + --color-orange-500: #f97316 !important; |
| 45 | + --color-orange-600: #ea580c !important; |
| 46 | + --color-orange-700: #c2410c !important; |
| 47 | + --color-orange-800: #9a3412 !important; |
| 48 | + --color-orange-900: #7c2d12 !important; |
| 49 | +
|
| 50 | + /* Yellow colors */ |
| 51 | + --color-yellow-50: #fefce8 !important; |
| 52 | + --color-yellow-100: #fef9c3 !important; |
| 53 | + --color-yellow-200: #fef08a !important; |
| 54 | + --color-yellow-300: #fde047 !important; |
| 55 | + --color-yellow-400: #facc15 !important; |
| 56 | + --color-yellow-500: #eab308 !important; |
| 57 | + --color-yellow-600: #ca8a04 !important; |
| 58 | + --color-yellow-700: #a16207 !important; |
| 59 | + --color-yellow-800: #854d0e !important; |
| 60 | + --color-yellow-900: #713f12 !important; |
| 61 | +
|
| 62 | + /* Green colors */ |
| 63 | + --color-green-50: #f0fdf4 !important; |
| 64 | + --color-green-100: #dcfce7 !important; |
| 65 | + --color-green-200: #bbf7d0 !important; |
| 66 | + --color-green-300: #86efac !important; |
| 67 | + --color-green-400: #4ade80 !important; |
| 68 | + --color-green-500: #22c55e !important; |
| 69 | + --color-green-600: #16a34a !important; |
| 70 | + --color-green-700: #15803d !important; |
| 71 | + --color-green-800: #166534 !important; |
| 72 | + --color-green-900: #14532d !important; |
| 73 | +
|
| 74 | + /* Blue colors */ |
| 75 | + --color-blue-50: #eff6ff !important; |
| 76 | + --color-blue-100: #dbeafe !important; |
| 77 | + --color-blue-200: #bfdbfe !important; |
| 78 | + --color-blue-300: #93c5fd !important; |
| 79 | + --color-blue-400: #60a5fa !important; |
| 80 | + --color-blue-500: #3b82f6 !important; |
| 81 | + --color-blue-600: #2563eb !important; |
| 82 | + --color-blue-700: #1d4ed8 !important; |
| 83 | + --color-blue-800: #1e40af !important; |
| 84 | + --color-blue-900: #1e3a8a !important; |
| 85 | +
|
| 86 | + /* Purple colors */ |
| 87 | + --color-purple-50: #faf5ff !important; |
| 88 | + --color-purple-100: #f3e8ff !important; |
| 89 | + --color-purple-200: #e9d5ff !important; |
| 90 | + --color-purple-300: #d8b4fe !important; |
| 91 | + --color-purple-400: #c084fc !important; |
| 92 | + --color-purple-500: #a855f7 !important; |
| 93 | + --color-purple-600: #9333ea !important; |
| 94 | + --color-purple-700: #7e22ce !important; |
| 95 | + --color-purple-800: #6b21a8 !important; |
| 96 | + --color-purple-900: #581c87 !important; |
| 97 | +
|
| 98 | + /* Gray colors */ |
| 99 | + --color-gray-50: #f9fafb !important; |
| 100 | + --color-gray-100: #f3f4f6 !important; |
| 101 | + --color-gray-200: #e5e7eb !important; |
| 102 | + --color-gray-300: #d1d5db !important; |
| 103 | + --color-gray-400: #9ca3af !important; |
| 104 | + --color-gray-500: #6b7280 !important; |
| 105 | + --color-gray-600: #4b5563 !important; |
| 106 | + --color-gray-700: #374151 !important; |
| 107 | + --color-gray-800: #1f2937 !important; |
| 108 | + --color-gray-900: #111827 !important; |
| 109 | +
|
| 110 | + /* Basic colors */ |
| 111 | + --color-black: #000000 !important; |
| 112 | + --color-white: #ffffff !important; |
| 113 | + } |
| 114 | +
|
| 115 | + /* Background color utilities */ |
| 116 | + .bg-red-50 { background-color: #fef2f2 !important; } |
| 117 | + .bg-red-100 { background-color: #fee2e2 !important; } |
| 118 | + .bg-red-600 { background-color: #dc2626 !important; } |
| 119 | + .bg-red-700 { background-color: #b91c1c !important; } |
| 120 | +
|
| 121 | + .bg-orange-50 { background-color: #fff7ed !important; } |
| 122 | + .bg-orange-100 { background-color: #ffedd5 !important; } |
| 123 | + .bg-orange-600 { background-color: #ea580c !important; } |
| 124 | + .bg-orange-700 { background-color: #c2410c !important; } |
| 125 | +
|
| 126 | + .bg-yellow-50 { background-color: #fefce8 !important; } |
| 127 | + .bg-yellow-100 { background-color: #fef9c3 !important; } |
| 128 | + .bg-yellow-200 { background-color: #fef08a !important; } |
| 129 | +
|
| 130 | + .bg-green-50 { background-color: #f0fdf4 !important; } |
| 131 | + .bg-green-100 { background-color: #dcfce7 !important; } |
| 132 | + .bg-green-600 { background-color: #16a34a !important; } |
| 133 | + .bg-green-700 { background-color: #15803d !important; } |
| 134 | +
|
| 135 | + .bg-blue-50 { background-color: #eff6ff !important; } |
| 136 | + .bg-blue-100 { background-color: #dbeafe !important; } |
| 137 | + .bg-blue-200 { background-color: #bfdbfe !important; } |
| 138 | + .bg-blue-600 { background-color: #2563eb !important; } |
| 139 | + .bg-blue-700 { background-color: #1d4ed8 !important; } |
| 140 | +
|
| 141 | + .bg-purple-50 { background-color: #faf5ff !important; } |
| 142 | + .bg-purple-100 { background-color: #f3e8ff !important; } |
| 143 | + .bg-purple-200 { background-color: #e9d5ff !important; } |
| 144 | + .bg-purple-600 { background-color: #9333ea !important; } |
| 145 | +
|
| 146 | + .bg-gray-50 { background-color: #f9fafb !important; } |
| 147 | + .bg-gray-100 { background-color: #f3f4f6 !important; } |
| 148 | + .bg-gray-200 { background-color: #e5e7eb !important; } |
| 149 | +
|
| 150 | + /* Border color utilities */ |
| 151 | + .border-red-200 { border-color: #fecaca !important; } |
| 152 | + .border-red-300 { border-color: #fca5a5 !important; } |
| 153 | +
|
| 154 | + .border-orange-200 { border-color: #fed7aa !important; } |
| 155 | + .border-orange-300 { border-color: #fdba74 !important; } |
| 156 | +
|
| 157 | + .border-yellow-200 { border-color: #fef08a !important; } |
| 158 | +
|
| 159 | + .border-green-200 { border-color: #bbf7d0 !important; } |
| 160 | +
|
| 161 | + .border-blue-200 { border-color: #bfdbfe !important; } |
| 162 | + .border-blue-300 { border-color: #93c5fd !important; } |
| 163 | + .border-blue-400 { border-color: #60a5fa !important; } |
| 164 | +
|
| 165 | + .border-purple-200 { border-color: #e9d5ff !important; } |
| 166 | + .border-purple-300 { border-color: #d8b4fe !important; } |
| 167 | +
|
| 168 | + .border-gray-200 { border-color: #e5e7eb !important; } |
| 169 | + .border-gray-300 { border-color: #d1d5db !important; } |
| 170 | + .border-gray-400 { border-color: #9ca3af !important; } |
| 171 | +
|
| 172 | + /* Text color utilities */ |
| 173 | + .text-red-600 { color: #dc2626 !important; } |
| 174 | + .text-red-700 { color: #b91c1c !important; } |
| 175 | +
|
| 176 | + .text-orange-600 { color: #ea580c !important; } |
| 177 | + .text-orange-700 { color: #c2410c !important; } |
| 178 | +
|
| 179 | + .text-yellow-600 { color: #ca8a04 !important; } |
| 180 | +
|
| 181 | + .text-green-600 { color: #16a34a !important; } |
| 182 | + .text-green-700 { color: #15803d !important; } |
| 183 | +
|
| 184 | + .text-blue-600 { color: #2563eb !important; } |
| 185 | + .text-blue-700 { color: #1d4ed8 !important; } |
| 186 | +
|
| 187 | + .text-purple-600 { color: #9333ea !important; } |
| 188 | +
|
| 189 | + .text-gray-500 { color: #6b7280 !important; } |
| 190 | + .text-gray-600 { color: #4b5563 !important; } |
| 191 | + .text-gray-700 { color: #374151 !important; } |
| 192 | + .text-gray-900 { color: #111827 !important; } |
| 193 | + `; |
| 194 | +} |
| 195 | + |
| 196 | +/** |
| 197 | + * Inject PDF color styles into the document |
| 198 | + * Returns cleanup function to remove styles |
| 199 | + */ |
| 200 | +export function injectPDFStyles(): () => void { |
| 201 | + const styleId = 'pdf-color-override'; |
| 202 | + |
| 203 | + // Remove existing style if present |
| 204 | + const existing = document.getElementById(styleId); |
| 205 | + if (existing) { |
| 206 | + existing.remove(); |
| 207 | + } |
| 208 | + |
| 209 | + const style = document.createElement('style'); |
| 210 | + style.id = styleId; |
| 211 | + style.textContent = generatePDFColorCSS(); |
| 212 | + document.head.appendChild(style); |
| 213 | + |
| 214 | + // Return cleanup function |
| 215 | + return () => { |
| 216 | + const el = document.getElementById(styleId); |
| 217 | + if (el) { |
| 218 | + el.remove(); |
| 219 | + } |
| 220 | + }; |
| 221 | +} |
| 222 | + |
| 223 | +/** |
| 224 | + * Default PDF generator options optimized for the app |
| 225 | + */ |
| 226 | +export const DEFAULT_PDF_OPTIONS = { |
| 227 | + format: 'a4' as const, |
| 228 | + orientation: 'portrait' as const, |
| 229 | + margins: [10, 10, 10, 10] as [number, number, number, number], |
| 230 | + compress: true, |
| 231 | + showPageNumbers: false, |
| 232 | + imageQuality: 0.95, |
| 233 | + scale: 3, |
| 234 | + colorReplacements: PDF_COLOR_REPLACEMENTS, |
| 235 | +}; |
| 236 | + |
| 237 | +/** |
| 238 | + * High-quality PDF options for detailed documents |
| 239 | + */ |
| 240 | +export const HIGH_QUALITY_PDF_OPTIONS = { |
| 241 | + ...DEFAULT_PDF_OPTIONS, |
| 242 | + imageQuality: 0.98, |
| 243 | + scale: 4, |
| 244 | +}; |
| 245 | + |
| 246 | +/** |
| 247 | + * Fast PDF options for quick generation |
| 248 | + */ |
| 249 | +export const FAST_PDF_OPTIONS = { |
| 250 | + ...DEFAULT_PDF_OPTIONS, |
| 251 | + imageQuality: 0.85, |
| 252 | + scale: 2, |
| 253 | +}; |
| 254 | + |
| 255 | +/** |
| 256 | + * Calculate PDF content width in pixels |
| 257 | + * This ensures consistent width across all PDF-rendered components |
| 258 | + * |
| 259 | + * Formula: (Paper Width - Left Margin - Right Margin) × MM_TO_PX |
| 260 | + * For A4 Portrait with 10mm margins: (210 - 10 - 10) × 3.7795 = 718.105px |
| 261 | + */ |
| 262 | +export function getPDFContentWidth(): number { |
| 263 | + const a4WidthMm = 210; // A4 paper width |
| 264 | + const [, marginRight, , marginLeft] = DEFAULT_PDF_OPTIONS.margins; |
| 265 | + const usableWidthMm = a4WidthMm - marginLeft - marginRight; |
| 266 | + const MM_TO_PX = 3.7795275591; // Exact 96 DPI conversion |
| 267 | + return Math.round(usableWidthMm * MM_TO_PX); // 718px |
| 268 | +} |
| 269 | + |
| 270 | +/** |
| 271 | + * PDF content width constant for consistent rendering |
| 272 | + * Use this value for all PDF-targeted content widths |
| 273 | + */ |
| 274 | +export const PDF_CONTENT_WIDTH_PX = getPDFContentWidth(); |
0 commit comments