From b7e08620428de4e510dbbaf23b0bb536158ee6cc Mon Sep 17 00:00:00 2001 From: Nate Kelley Date: Fri, 1 Aug 2025 22:05:59 -0600 Subject: [PATCH] update pdf download --- apps/web/next.config.mjs | 1 + .../report/elements/ExportToolbarButton.tsx | 157 +++++++++++------- .../report/elements/FixedToolbarButtons.tsx | 5 +- 3 files changed, 104 insertions(+), 59 deletions(-) diff --git a/apps/web/next.config.mjs b/apps/web/next.config.mjs index 845e83af6..7bff289a9 100644 --- a/apps/web/next.config.mjs +++ b/apps/web/next.config.mjs @@ -37,6 +37,7 @@ const createCspHeader = (isEmbed = false) => { (() => { const connectSources = [ "'self'", + 'data:', // Allow data URLs for PDF exports and other data URI downloads localDomains, 'https://*.vercel.app', 'https://*.supabase.co', diff --git a/apps/web/src/components/ui/report/elements/ExportToolbarButton.tsx b/apps/web/src/components/ui/report/elements/ExportToolbarButton.tsx index b14401523..e257579ce 100644 --- a/apps/web/src/components/ui/report/elements/ExportToolbarButton.tsx +++ b/apps/web/src/components/ui/report/elements/ExportToolbarButton.tsx @@ -19,12 +19,14 @@ import { import { EditorStatic } from './EditorStatic'; import { ToolbarButton } from '@/components/ui/toolbar/Toolbar'; +import { useBusterNotifications } from '@/context/BusterNotifications'; const siteUrl = 'https://platejs.org'; export function ExportToolbarButton({ children, ...props }: DropdownMenuProps) { const editor = useEditorRef(); const [open, setOpen] = React.useState(false); + const { openErrorMessage, openInfoMessage } = useBusterNotifications(); const getCanvas = async () => { const { default: html2canvas } = await import('html2canvas-pro'); @@ -69,80 +71,121 @@ export function ExportToolbarButton({ children, ...props }: DropdownMenuProps) { }; const exportToPdf = async () => { - const canvas = await getCanvas(); + try { + const canvas = await getCanvas(); + const PDFLib = await import('pdf-lib'); + const pdfDoc = await PDFLib.PDFDocument.create(); - const PDFLib = await import('pdf-lib'); - const pdfDoc = await PDFLib.PDFDocument.create(); - const page = pdfDoc.addPage([canvas.width, canvas.height]); - const imageEmbed = await pdfDoc.embedPng(canvas.toDataURL('PNG')); - const { height, width } = imageEmbed.scale(1); - page.drawImage(imageEmbed, { - height, - width, - x: 0, - y: 0 - }); - const pdfBase64 = await pdfDoc.saveAsBase64({ dataUri: true }); + // Standard A4 dimensions in points (595 x 842) + const standardWidth = 595; + const standardHeight = 842; - await downloadFile(pdfBase64, 'plate.pdf'); + // Calculate scaling to fit content within standard width with margins + const margin = 40; // 40 points margin on each side + const availableWidth = standardWidth - margin * 2; + const scaleFactor = Math.min(availableWidth / canvas.width, 1); // Don't scale up, only down + + const scaledWidth = canvas.width * scaleFactor; + const scaledHeight = canvas.height * scaleFactor; + + // Calculate required page height based on scaled content + const requiredHeight = Math.max(standardHeight, scaledHeight + margin * 2); + + const page = pdfDoc.addPage([standardWidth, requiredHeight]); + const imageEmbed = await pdfDoc.embedPng(canvas.toDataURL('PNG')); + + // Center the content horizontally within the standard width + const xPosition = (standardWidth - scaledWidth) / 2; + + page.drawImage(imageEmbed, { + height: scaledHeight, + width: scaledWidth, + x: xPosition, + y: requiredHeight - scaledHeight - margin // Position from top with margin + }); + + const pdfBase64 = await pdfDoc.saveAsBase64({ dataUri: true }); + + await downloadFile(pdfBase64, 'plate.pdf'); + openInfoMessage('PDF exported successfully'); + } catch (error) { + openErrorMessage('Failed to export PDF'); + } }; const exportToImage = async () => { - const canvas = await getCanvas(); - await downloadFile(canvas.toDataURL('image/png'), 'plate.png'); + try { + const canvas = await getCanvas(); + await downloadFile(canvas.toDataURL('image/png'), 'plate.png'); + openInfoMessage('Image exported successfully'); + } catch (error) { + openErrorMessage('Failed to export image'); + } }; const exportToHtml = async () => { - const BaseEditorKit = await import('../editor-base-kit').then((module) => module.BaseEditorKit); + try { + const BaseEditorKit = await import('../editor-base-kit').then( + (module) => module.BaseEditorKit + ); - const editorStatic = createSlateEditor({ - plugins: BaseEditorKit, - value: editor.children - }); + const editorStatic = createSlateEditor({ + plugins: BaseEditorKit, + value: editor.children + }); - const editorHtml = await serializeHtml(editorStatic, { - editorComponent: EditorStatic, - props: { style: { padding: '0 calc(50% - 350px)', paddingBottom: '' } } - }); + const editorHtml = await serializeHtml(editorStatic, { + editorComponent: EditorStatic, + props: { style: { padding: '0 calc(50% - 350px)', paddingBottom: '' } } + }); - const tailwindCss = ``; - const katexCss = ``; + const tailwindCss = ``; + const katexCss = ``; - const html = ` - - - - - - - - - ${tailwindCss} - ${katexCss} - - - - ${editorHtml} - - `; + const html = ` + + + + + + + + + ${tailwindCss} + ${katexCss} + + + + ${editorHtml} + + `; - const url = `data:text/html;charset=utf-8,${encodeURIComponent(html)}`; + const url = `data:text/html;charset=utf-8,${encodeURIComponent(html)}`; - await downloadFile(url, 'plate.html'); + await downloadFile(url, 'plate.html'); + openInfoMessage('HTML exported successfully'); + } catch (error) { + openErrorMessage('Failed to export HTML'); + } }; const exportToMarkdown = async () => { - const md = editor.getApi(MarkdownPlugin).markdown.serialize(); - const url = `data:text/markdown;charset=utf-8,${encodeURIComponent(md)}`; - await downloadFile(url, 'plate.md'); + try { + const md = editor.getApi(MarkdownPlugin).markdown.serialize(); + const url = `data:text/markdown;charset=utf-8,${encodeURIComponent(md)}`; + await downloadFile(url, 'plate.md'); + openInfoMessage('Markdown exported successfully'); + } catch (error) { + openErrorMessage('Failed to export Markdown'); + } }; return ( diff --git a/apps/web/src/components/ui/report/elements/FixedToolbarButtons.tsx b/apps/web/src/components/ui/report/elements/FixedToolbarButtons.tsx index 077b5ba49..121616d2f 100644 --- a/apps/web/src/components/ui/report/elements/FixedToolbarButtons.tsx +++ b/apps/web/src/components/ui/report/elements/FixedToolbarButtons.tsx @@ -12,7 +12,8 @@ import { ArrowUpToLine, TextColor2, BucketPaint, - TextHighlight2 + TextHighlight2, + ArrowDownFromLine } from '@/components/ui/icons'; import { KEYS } from 'platejs'; import { useEditorReadOnly } from 'platejs/react'; @@ -66,7 +67,7 @@ export const FixedToolbarButtons = React.memo(() => { - +