PDF & PNG Export
Purpose
This document explains the PDF and PNG export functionality that allows users to download their weekly schedule as image files or PDF documents. The export system captures the rendered timeline view and converts it to downloadable formats using client-side libraries.
For information about sharing schedules via URL parameters, see Shareable URLs & Configuration Sharing. For syncing schedules to external calendar applications, see Google Calendar Integration.
Overview
The export system enables users to download their personalized timetables in two formats:
| Format | Use Case | Library Used |
|---|---|---|
| PNG | Quick sharing, social media, backgrounds | html-to-image |
| Printing, formal documentation, archival | jsPDF + html-to-image |
The export process involves three main components:
- Action Buttons (website/components/action-buttons.tsx) - UI triggers that initiate downloads
- Download Utilities (website/utils/download.ts) - Core export functions using image capture libraries
- Timeline Download Mode (website/app/timeline/page.tsx) - Special rendering mode optimized for capture
Sources: website/utils/download.ts1-150 website/components/action-buttons.tsx1-152
Export Pipeline Architecture
The following diagram illustrates the complete flow from user clicking a download button to receiving the file:

Sources: website/components/action-buttons.tsx54-102 website/utils/download.ts1-150
Download Functions
downloadAsPng Function
The downloadAsPng function in website/utils/download.ts1-50 captures the target element as a PNG image using the html-to-image library.
Function Signature:
export const downloadAsPng = async (
elementId: string,
filename: string,
callbacks?: {
onProgress?: (msg: string) => void;
onSuccess?: () => void;
onError?: (err: Error) => void;
}
): Promise<void>
Key Implementation Details:
| Aspect | Configuration | Purpose |
|---|---|---|
| Quality | quality: 1 (maximum) |
Lossless image capture |
| Background | backgroundColor: '#131010' |
Match app dark theme |
| Pixel Ratio | `window.devicePixelRatio | |
| Transform | scale(1) |
Prevent layout distortion |
The function uses requestAnimationFrame to ensure the DOM is fully rendered before capture, synchronizing with the browser's rendering pipeline.
Sources: website/utils/download.ts1-50
downloadAsPdf Function
The downloadAsPdf function converts the captured PNG to a PDF document with A4 landscape orientation using jsPDF.
Function Signature:
export const downloadAsPdf = async (
elementId: string,
filename: string,
callbacks?: {
onProgress?: (msg: string) => void;
onSuccess?: () => void;
onError?: (err: Error) => void;
}
): Promise<void>
PDF Configuration:
// A4 dimensions in mm (landscape)
const a4Width = 297;
const a4Height = 210;
const pdf = new jsPDF({
orientation: 'landscape',
unit: 'mm',
format: [a4Width, a4Height],
});
The captured PNG image is first obtained using html-to-image.toPng(), then embedded into the PDF. The image is scaled proportionally to fit the A4 width while maintaining aspect ratio to prevent distortion.
Sources: website/utils/download.ts52-150
Timeline Download Mode
The Timeline page (website/app/timeline/page.tsx) supports a special "download mode" that optimizes the layout for capture.
Activation
Download mode is activated via URL query parameter:
const searchParams = useSearchParams();
const isDownloadMode = searchParams.get("download") === "1";
When a user clicks the PNG or PDF export button in ActionButtons, the component navigates to /timeline?download=1 website/components/action-buttons.tsx64 triggering this mode.
Layout Adjustments in Download Mode
When isDownloadMode is true, the Timeline component applies optimizations for clean image/PDF capture:
| Modification | Normal Mode | Download Mode | Purpose |
|---|---|---|---|
| Container Width | Responsive | Fixed wide layout | Ensure full schedule visibility without scrolling |
| Display Width | Auto | Extended | Accommodate all days in single view |
| Title Placement | Outside grid | Inside #schedule-display |
Include title in captured image |
| Welcome Banner | Shown (dismissible) | Hidden | Clean export without UI overlays |
| Stats Section | Visible | Hidden | Focus on schedule content only |
| Current Time Indicator | Shown (animated) | Hidden | Avoid time-specific dynamic elements |
| Navigation Controls | Visible | Hidden | Remove interactive UI from export |
The #schedule-display element ID is specifically targeted by the download functions to capture the entire schedule grid including headers and all events.
Sources: website/components/action-buttons.tsx64 website/utils/download.ts1-150
Component Architecture

Sources: website/components/action-buttons.tsx1-152 website/utils/download.ts1-150
Action Buttons Implementation
The ActionButtons component (website/components/action-buttons.tsx) manages the download workflow and user feedback.
State Management
const [loading, setLoading] = useState<null | "png" | "pdf">(null);
const [progressMsg, setProgressMsg] = useState("");
The loading state tracks which export is in progress, disabling buttons during operation website/components/action-buttons.tsx22-23
Download Handler
The handleDownload function orchestrates the export process:

Key Steps:
- Navigation website/components/action-buttons.tsx64: Navigate to
/timeline?download=1using Next.js router to activate download mode - Wait website/components/action-buttons.tsx65: 500ms delay ensures rendering completes
- Element Retrieval website/components/action-buttons.tsx67-68: Get
#schedule-displayDOM element - Download Execution website/components/action-buttons.tsx78-101: Call appropriate download function with progress/success/error callbacks
Sources: website/components/action-buttons.tsx54-102
Display Schedule Selection
The component respects custom edits by using editedSchedule from UserContext when available:
const { editedSchedule } = useContext(UserContext);
const displaySchedule = editedSchedule || schedule;
This ensures that any user modifications made through the EditEventDialog (adding custom events, editing existing classes) are included in the exported PNG/PDF file website/components/action-buttons.tsx26
Sources: website/components/action-buttons.tsx19 website/components/action-buttons.tsx26
Image Capture Configuration
Quality Settings
Both export functions use identical image capture settings for consistency:
| Parameter | Value | Purpose |
|---|---|---|
quality |
1 |
Maximum quality (no compression) |
backgroundColor |
'#131010' |
Match app background color |
pixelRatio |
`window.devicePixelRatio |
The pixelRatio setting ensures sharp images on high-resolution displays (e.g., Retina screens) by using the device's native pixel density or defaulting to 2x for high-quality output.
Sources: website/utils/download.ts1-150
Error Handling
Both download functions implement try-catch error handling:
try {
// Capture and download logic
if (onSuccess) onSuccess();
} catch (err) {
console.error('Error downloading PNG:', err);
if (onError) onError(err instanceof Error ? err : new Error('Unknown error'));
}
Errors are:
- Logged to console for debugging purposes
- Passed to
onErrorcallback with proper Error typing - Displayed to user via toast notifications website/components/action-buttons.tsx93-99
Sources: website/utils/download.ts1-150 website/components/action-buttons.tsx93-99
Toast Notification Integration
The export system provides real-time feedback through the useToast hook:
Progress Feedback
onProgress: (msg) => {
setProgressMsg(msg);
toast({
title: msg,
variant: "default",
});
}
Progress Messages:
"Preparing image..."- Image capture initiated"Saving image..."- PNG download started"Preparing PDF..."- PDF generation initiated"Saving PDF..."- PDF save started
Sources: website/components/action-buttons.tsx79-85 website/utils/download.ts1-150
Success Notification
onSuccess: () => {
toast({
title: `${type.toUpperCase()} ready!`,
description: `Your schedule has been downloaded as a ${type.toUpperCase()}.`,
});
setLoading(null);
}
Sources: website/components/action-buttons.tsx86-92
Error Notification
onError: (err) => {
toast({
title: `Failed to download ${type.toUpperCase()}`,
description: err.message,
variant: "destructive",
});
setLoading(null);
}
Sources: website/components/action-buttons.tsx93-99
UI Components
Button States
The export buttons support three visual states:
| State | Condition | Visual Indicator |
|---|---|---|
| Enabled | loading === null |
Standard button styling |
| Loading (Self) | loading === type |
Spinner icon (Loader2) |
| Disabled (Other) | loading !== null |
Disabled state |
Button Implementation:
<Button
onClick={() => handleDownload(downloadAsPng, "png")}
disabled={loading !== null}
className="backdrop-blur-md bg-[#F0BB78]/30 border border-[#F0BB78]/20"
>
{loading === "png" ? (
<Loader2 className="w-4 h-4 mr-2 animate-spin" />
) : (
<Download className="w-3 h-3 sm:w-4 sm:h-4 mr-2" />
)}
PNG
</Button>
Sources: website/components/action-buttons.tsx117-140
Download Mode Element Targeting
The export system specifically targets the #schedule-display element:
const element = document.getElementById("schedule-display");
if (!element) {
toast({
title: "Schedule not found!",
description: "Please navigate to the timeline page first.",
variant: "destructive",
});
setLoading(null);
return;
}
This element is defined in the Timeline page and wraps the entire schedule grid, including:
- Day headers (Monday through Saturday)
- Time column with hourly slots
- Event blocks with subject names, locations, and types
- Title header (included in download mode only)
The download utilities receive this element ID as "schedule-display" website/components/action-buttons.tsx78
Sources: website/components/action-buttons.tsx67-76 website/components/action-buttons.tsx78
Library Dependencies
html-to-image
The html-to-image library provides the toPng function used for DOM-to-image conversion.
Import:
import { toPng } from 'html-to-image';
Key Features:
- Converts DOM nodes to PNG images using canvas rendering
- Supports custom dimensions, quality settings, and pixel ratios
- Handles complex CSS styles and background colors
- Returns base64-encoded data URLs
jsPDF
The jsPDF library generates PDF documents from the captured PNG image.
Import:
import jsPDF from 'jspdf';
Key Features:
- Creates PDF documents programmatically
- Supports landscape/portrait orientation
- Embeds images with automatic scaling
- Outputs directly to browser download
Sources: website/utils/download.ts1-150
Performance Considerations
Rendering Delay
The 500ms delay website/components/action-buttons.tsx65 after Next.js navigation ensures:
- Next.js completes the client-side route transition
- Timeline component renders with
isDownloadMode=true - Tailwind CSS styles are fully applied to the DOM
- Layout calculations and reflows stabilize
- All event blocks and grid elements are positioned correctly
Animation Frame Wait
Both download functions use requestAnimationFrame to synchronize with the browser's rendering pipeline:
await new Promise((resolve) => requestAnimationFrame(resolve));
This ensures the capture happens after the browser has completed layout and painting, resulting in accurate screenshots without visual glitches or missing elements.
Sources: website/components/action-buttons.tsx65 website/utils/download.ts1-150
File Naming
Both download functions accept a filename parameter and append the appropriate extension:
- PNG:
${filename}.pngsrc/utils/download.ts38 - PDF:
${filename}.pdfsrc/utils/download.ts97
The default filename used is "schedule" src/components/action-buttons.tsx50 resulting in:
schedule.pngschedule.pdf
Sources: src/utils/download.ts38 src/utils/download.ts97 src/components/action-buttons.tsx50