update pdf download

This commit is contained in:
Nate Kelley 2025-08-01 22:05:59 -06:00
parent bcc5566f27
commit b7e0862042
No known key found for this signature in database
GPG Key ID: FD90372AB8D98B4F
3 changed files with 104 additions and 59 deletions

View File

@ -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',

View File

@ -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 = `<link rel="stylesheet" href="${siteUrl}/tailwind.css">`;
const katexCss = `<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/katex@0.16.18/dist/katex.css" integrity="sha384-9PvLvaiSKCPkFKB1ZsEoTjgnJn+O3KvEwtsz37/XrkYft3DTk2gHdYvd9oWgW3tV" crossorigin="anonymous">`;
const tailwindCss = `<link rel="stylesheet" href="${siteUrl}/tailwind.css">`;
const katexCss = `<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/katex@0.16.18/dist/katex.css" integrity="sha384-9PvLvaiSKCPkFKB1ZsEoTjgnJn+O3KvEwtsz37/XrkYft3DTk2gHdYvd9oWgW3tV" crossorigin="anonymous">`;
const html = `<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta name="color-scheme" content="light dark" />
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link
href="https://fonts.googleapis.com/css2?family=Inter:wght@400..700&family=JetBrains+Mono:wght@400..700&display=swap"
rel="stylesheet"
/>
${tailwindCss}
${katexCss}
<style>
:root {
--font-sans: 'Inter', 'Inter Fallback';
--font-mono: 'JetBrains Mono', 'JetBrains Mono Fallback';
}
</style>
</head>
<body>
${editorHtml}
</body>
</html>`;
const html = `<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta name="color-scheme" content="light dark" />
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link
href="https://fonts.googleapis.com/css2?family=Inter:wght@400..700&family=JetBrains+Mono:wght@400..700&display=swap"
rel="stylesheet"
/>
${tailwindCss}
${katexCss}
<style>
:root {
--font-sans: 'Inter', 'Inter Fallback';
--font-mono: 'JetBrains Mono', 'JetBrains Mono Fallback';
}
</style>
</head>
<body>
${editorHtml}
</body>
</html>`;
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 (

View File

@ -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(() => {
<ToolbarGroup>
<ExportToolbarButton>
<ArrowUpToLine />
<ArrowDownFromLine />
</ExportToolbarButton>
<ImportToolbarButton />