// ============================================================================= // Privacy Policy PDF Export // Export Datenschutzerklaerung to PDF format // ============================================================================= import { GeneratedPrivacyPolicy, PrivacyPolicySection, CompanyInfo, SupportedLanguage, DataPoint, CATEGORY_METADATA, RETENTION_PERIOD_INFO, } from '../types' // ============================================================================= // TYPES // ============================================================================= export interface PDFExportOptions { language: SupportedLanguage includeTableOfContents: boolean includeDataPointList: boolean companyLogo?: string primaryColor?: string pageSize?: 'A4' | 'LETTER' orientation?: 'portrait' | 'landscape' fontSize?: number } const DEFAULT_OPTIONS: PDFExportOptions = { language: 'de', includeTableOfContents: true, includeDataPointList: true, primaryColor: '#6366f1', pageSize: 'A4', orientation: 'portrait', fontSize: 11, } // ============================================================================= // PDF CONTENT STRUCTURE // ============================================================================= export interface PDFSection { type: 'title' | 'heading' | 'subheading' | 'paragraph' | 'table' | 'list' | 'pagebreak' content?: string items?: string[] table?: { headers: string[] rows: string[][] } style?: { color?: string fontSize?: number bold?: boolean italic?: boolean align?: 'left' | 'center' | 'right' } } // ============================================================================= // PDF CONTENT GENERATION // ============================================================================= /** * Generate PDF content structure for Privacy Policy */ export function generatePDFContent( policy: GeneratedPrivacyPolicy, companyInfo: CompanyInfo, dataPoints: DataPoint[], options: Partial = {} ): PDFSection[] { const opts = { ...DEFAULT_OPTIONS, ...options } const sections: PDFSection[] = [] const lang = opts.language // Title page sections.push({ type: 'title', content: lang === 'de' ? 'Datenschutzerklaerung' : 'Privacy Policy', style: { color: opts.primaryColor, fontSize: 28, bold: true, align: 'center' }, }) sections.push({ type: 'paragraph', content: lang === 'de' ? 'gemaess Art. 13, 14 DSGVO' : 'according to Art. 13, 14 GDPR', style: { fontSize: 14, align: 'center', italic: true }, }) // Company information sections.push({ type: 'paragraph', content: companyInfo.name, style: { fontSize: 16, bold: true, align: 'center' }, }) sections.push({ type: 'paragraph', content: `${companyInfo.address}, ${companyInfo.postalCode} ${companyInfo.city}`, style: { align: 'center' }, }) sections.push({ type: 'paragraph', content: `${lang === 'de' ? 'Stand' : 'Date'}: ${new Date(policy.generatedAt).toLocaleDateString(lang === 'de' ? 'de-DE' : 'en-US')}`, style: { align: 'center' }, }) sections.push({ type: 'paragraph', content: `Version: ${policy.version}`, style: { align: 'center', fontSize: 10 }, }) sections.push({ type: 'pagebreak' }) // Table of Contents if (opts.includeTableOfContents) { sections.push({ type: 'heading', content: lang === 'de' ? 'Inhaltsverzeichnis' : 'Table of Contents', style: { color: opts.primaryColor }, }) const tocItems = policy.sections.map((section, idx) => `${idx + 1}. ${section.title[lang]}` ) if (opts.includeDataPointList) { tocItems.push(lang === 'de' ? 'Anhang: Datenpunktkatalog' : 'Appendix: Data Point Catalog') } sections.push({ type: 'list', items: tocItems, }) sections.push({ type: 'pagebreak' }) } // Privacy Policy Sections policy.sections.forEach((section, idx) => { sections.push({ type: 'heading', content: `${idx + 1}. ${section.title[lang]}`, style: { color: opts.primaryColor }, }) // Convert markdown-like content to paragraphs const content = section.content[lang] const paragraphs = content.split('\n\n') for (const para of paragraphs) { if (para.startsWith('- ')) { // List items const items = para.split('\n').filter(l => l.startsWith('- ')).map(l => l.substring(2)) sections.push({ type: 'list', items, }) } else if (para.startsWith('### ')) { sections.push({ type: 'subheading', content: para.substring(4), }) } else if (para.startsWith('## ')) { sections.push({ type: 'subheading', content: para.substring(3), style: { bold: true }, }) } else if (para.trim()) { sections.push({ type: 'paragraph', content: para.replace(/\*\*(.*?)\*\*/g, '$1'), // Remove markdown bold for plain text }) } } // Add related data points if this section has them if (section.dataPointIds.length > 0 && opts.includeDataPointList) { const relatedDPs = dataPoints.filter(dp => section.dataPointIds.includes(dp.id)) if (relatedDPs.length > 0) { sections.push({ type: 'paragraph', content: lang === 'de' ? `Betroffene Datenkategorien: ${relatedDPs.map(dp => dp.name[lang]).join(', ')}` : `Affected data categories: ${relatedDPs.map(dp => dp.name[lang]).join(', ')}`, style: { italic: true, fontSize: 10 }, }) } } }) // Data Point Catalog Appendix if (opts.includeDataPointList && dataPoints.length > 0) { sections.push({ type: 'pagebreak' }) sections.push({ type: 'heading', content: lang === 'de' ? 'Anhang: Datenpunktkatalog' : 'Appendix: Data Point Catalog', style: { color: opts.primaryColor }, }) sections.push({ type: 'paragraph', content: lang === 'de' ? 'Die folgende Tabelle zeigt alle verarbeiteten personenbezogenen Daten:' : 'The following table shows all processed personal data:', }) // Group by category const categories = [...new Set(dataPoints.map(dp => dp.category))] for (const category of categories) { const categoryDPs = dataPoints.filter(dp => dp.category === category) const categoryMeta = CATEGORY_METADATA[category] sections.push({ type: 'subheading', content: `${categoryMeta.code}. ${categoryMeta.name[lang]}`, }) sections.push({ type: 'table', table: { headers: lang === 'de' ? ['Code', 'Datenpunkt', 'Zweck', 'Loeschfrist'] : ['Code', 'Data Point', 'Purpose', 'Retention'], rows: categoryDPs.map(dp => [ dp.code, dp.name[lang], dp.purpose[lang].substring(0, 50) + (dp.purpose[lang].length > 50 ? '...' : ''), RETENTION_PERIOD_INFO[dp.retentionPeriod]?.label[lang] || dp.retentionPeriod, ]), }, }) } } // Footer sections.push({ type: 'paragraph', content: lang === 'de' ? `Generiert am ${new Date().toLocaleDateString('de-DE')} mit dem Datenschutzerklaerung-Generator` : `Generated on ${new Date().toLocaleDateString('en-US')} with the Privacy Policy Generator`, style: { italic: true, align: 'center', fontSize: 9 }, }) return sections } // ============================================================================= // PDF BLOB GENERATION // ============================================================================= /** * Generate a PDF file as a Blob * This generates HTML that can be printed to PDF or used with a PDF library */ export async function generatePDFBlob( policy: GeneratedPrivacyPolicy, companyInfo: CompanyInfo, dataPoints: DataPoint[], options: Partial = {} ): Promise { const content = generatePDFContent(policy, companyInfo, dataPoints, options) const opts = { ...DEFAULT_OPTIONS, ...options } // Generate HTML for PDF conversion const html = generateHTMLFromContent(content, opts) return new Blob([html], { type: 'text/html' }) } /** * Generate printable HTML from PDF content */ function generateHTMLFromContent( content: PDFSection[], options: PDFExportOptions ): string { const pageWidth = options.pageSize === 'A4' ? '210mm' : '8.5in' const pageHeight = options.pageSize === 'A4' ? '297mm' : '11in' let html = ` ${options.language === 'de' ? 'Datenschutzerklaerung' : 'Privacy Policy'} ` for (const section of content) { switch (section.type) { case 'title': html += `
${escapeHtml(section.content || '')}
\n` break case 'heading': html += `

${escapeHtml(section.content || '')}

\n` break case 'subheading': html += `

${escapeHtml(section.content || '')}

\n` break case 'paragraph': const alignClass = section.style?.align === 'center' ? ' class="center"' : '' html += `${escapeHtml(section.content || '')}

\n` break case 'list': html += '
    \n' for (const item of section.items || []) { html += `
  • ${escapeHtml(item)}
  • \n` } html += '
\n' break case 'table': if (section.table) { html += '\n\n' for (const header of section.table.headers) { html += ` \n` } html += '\n\n' for (const row of section.table.rows) { html += '\n' for (const cell of row) { html += ` \n` } html += '\n' } html += '
${escapeHtml(header)}
${escapeHtml(cell)}
\n' } break case 'pagebreak': html += '
\n' break } } html += '' return html } function getStyleString(style?: PDFSection['style']): string { if (!style) return '' const parts: string[] = [] if (style.color) parts.push(`color: ${style.color}`) if (style.fontSize) parts.push(`font-size: ${style.fontSize}pt`) if (style.bold) parts.push('font-weight: bold') if (style.italic) parts.push('font-style: italic') if (style.align) parts.push(`text-align: ${style.align}`) return parts.join('; ') } function escapeHtml(text: string): string { return text .replace(/&/g, '&') .replace(//g, '>') .replace(/"/g, '"') .replace(/'/g, ''') } // ============================================================================= // FILENAME GENERATION // ============================================================================= /** * Generate a filename for the PDF export */ export function generatePDFFilename( companyInfo: CompanyInfo, language: SupportedLanguage = 'de' ): string { const companyName = companyInfo.name.replace(/[^a-zA-Z0-9]/g, '-') || 'unknown' const date = new Date().toISOString().split('T')[0] const prefix = language === 'de' ? 'Datenschutzerklaerung' : 'Privacy-Policy' return `${prefix}-${companyName}-${date}.html` }