Skip to content

feat: add print options to PDF export#2296

Merged
aalemayhu merged 2 commits into
mainfrom
feat/pdf-print-options
May 15, 2026
Merged

feat: add print options to PDF export#2296
aalemayhu merged 2 commits into
mainfrom
feat/pdf-print-options

Conversation

@aalemayhu
Copy link
Copy Markdown
Contributor

@aalemayhu aalemayhu commented May 15, 2026

What

Adds four user-configurable print options to the existing PDF export flow on the Print Page: background color (native color picker, default white), paper size (A4/Letter/Legal, default A4), orientation (portrait/landscape, default portrait), and margins (narrow 0.5 cm / normal 1 cm / wide 2 cm, default normal).

Controls render inline in a responsive grid above the drop zone. Defaults match today's behaviour exactly, so existing users see no change.

Why

The PDF export flow was useful but inflexible — users printing for binders needed wider margins, users printing in dark-mode needed a non-white background, and users outside A4 regions needed Letter/Legal. This closes the gap without adding clicks for users who don't care.

How

  • PdfOptions type + DEFAULT_PDF_OPTIONS exported from ExportApkgToPdfUseCase.ts; the execute method accepts an optional third argument defaulting to those values.
  • buildHtml now takes backgroundColor and inlines it on <body style="background:...">. The value is validated before it reaches this point so no re-escaping is needed.
  • PdfRenderService.renderHtml accepts an optional PdfOptions; maps the margins enum to cm strings and toggles landscape mode in puppeteer's page.pdf() call.
  • parsePdfOptions(body) in ApkgController.ts validates all four fields at the HTTP boundary: backgroundColor must match /^#[0-9a-f]{6}$/i or the handler returns 400; enum fields silently default when absent/invalid (they cannot produce XSS).
  • PrintForm.tsx gains four controls (<select> × 3, <input type="color">) rendered in a CSS grid above the drop zone; values are appended to FormData before each upload call.

Measuring success

Log line: no new instrumentation needed — the existing console.error in exportPdf will surface any failures. Measure via the existing PDF download count metric; an increase after ship confirms uptake. If parsePdfOptions rejects a bad backgroundColor, the server returns 400 and the form shows the generic error message.

Testing

  • Unit tests added: 3 new cases in ExportApkgToPdfUseCase.test.ts
    • default options produce background:#ffffff in generated HTML
    • custom backgroundColor is reflected in the HTML body style
    • options are forwarded to pdfRenderService.renderHtml as a second argument
  • parsePdfOptions is a pure exported function and can be unit-tested; no new harness was fabricated for the controller path (consistent with existing pattern in this codebase)
  • No Vitest for the form controls — they are purely presentational (select + color picker with useState); all 463 web tests still pass
  • Manually verified: server tsc clean, web tsc clean, web vitest 463/463 green, Biome lint clean

Changelog

{ type: 'feature', title: 'Printable PDFs — pick background color, paper size (A4, Letter, Legal), orientation, and margins before you download', date: '2026-05-15' }

Risks

  • puppeteer format field: puppeteer's PDFOptions.format accepts 'A4' | 'Letter' | 'Legal' (and others) as strings matching our enum — confirmed against the puppeteer type definitions. No cast needed.
  • Rollback: the pdfOptions parameter defaults to DEFAULT_PDF_OPTIONS, which replicates today's hardcoded values exactly. Reverting to the previous execute(fileBuffer, unlimitedAccess) call signature requires only removing the third argument — no migration, no DB change.
  • sonar-scanner is not installed in this environment. Noted explicitly per CLAUDE.md. The two new user-controlled values that reach the HTML/puppeteer call both pass through strict validation first: backgroundColor via allowlist regex, enum values via Array.includes. No taint path to an unsanitized sink.

Goal alignment

Makes the print-as-PDF flow more useful (binder margins, low-ink background, non-A4 paper) without adding steps for users who don't configure anything. Defaults are identical to today. Reduces friction for students printing study sheets — directly supports the 300K-user scale goal by improving retention of paying subscribers.


View in Codesmith
Need help on this PR? Tag @codesmith with what you need.

  • Let Codesmith autofix CI failures and bot reviews

aalemayhu and others added 2 commits May 15, 2026 21:47
…, margins)

- PdfOptions type (backgroundColor, paperSize, orientation, margins) with
  defaults that exactly match today's behaviour (white, A4, portrait, normal)
- buildHtml now accepts backgroundColor and inlines it on <body>
- PdfRenderService.renderHtml accepts optional PdfOptions; maps margins enum
  to cm values and toggles landscape mode via puppeteer page.pdf()
- parsePdfOptions helper in ApkgController validates all four fields at the
  HTTP boundary: backgroundColor must pass /^#[0-9a-f]{6}$/i or the request
  is rejected 400; enum fields default silently when absent/invalid
- PrintForm gains four controls rendered in a responsive grid above the drop
  zone; values are appended to FormData before each upload

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@aalemayhu aalemayhu merged commit d2a1bf6 into main May 15, 2026
9 checks passed
@aalemayhu aalemayhu deleted the feat/pdf-print-options branch May 15, 2026 19:52
aalemayhu added a commit that referenced this pull request May 15, 2026
## What
Designer-pass polish on #2296.

- Changelog: `Printable PDFs — pick background color, paper size (A4,
Letter, Legal), orientation, and margins before you download` → `PDF
export — pick paper size (A4, Letter, Legal), orientation, margins, and
page color`.
- Color swatch: full-width control → 3 rem swatch + muted tabular-nums
hex readout (`#FFFFFF`).
- Field label: `Background color` → `Page background`.

## Why
Three things landed in #2296 that drifted from the existing in-product
voice and the rest of the changelog file:

1. The feature is "PDF export" everywhere else in product; "Printable
PDFs" reintroduces a second name.
2. `before you download` is filler — the reader of the What's New page
already knows when settings apply.
3. A full-width native `<input type="color">` swatch alongside three
selects reads visually heavier than the selects and breaks the
restrained productivity-tool feel.

## How
- One-line edit in `web/src/pages/WhatsNewPage/changelog.ts`.
- Color picker wrapped in a flex row: 3 rem `<input type="color">` plus
a `<span>` showing the uppercase hex in `--color-text-secondary`,
tabular-nums for digit alignment.

## Testing
- `pnpm --filter 2anki-web typecheck` — green.
- `pnpm --filter 2anki-web lint` — green.
- Pure presentational change in a single component + a string; no logic
to unit-test. No backend changes.

## Risks
None — defaults preserved, FormData wiring unchanged, validation
untouched.

## Goal alignment
Mission: the print page is cleaner and the changelog reads consistently
with the rest of the file. No behaviour change.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

<!-- codesmith:footer -->
---
<a
href="https://app.blacksmith.sh/2anki/codesmith/server/pr/2297"><picture><source
media="(prefers-color-scheme: dark)"
srcset="https://pr-comments-assets.blacksmith.sh/codesmith/view-in-codesmith-dark.svg"><source
media="(prefers-color-scheme: light)"
srcset="https://pr-comments-assets.blacksmith.sh/codesmith/view-in-codesmith-light.svg"><img
alt="View in Codesmith"
src="https://pr-comments-assets.blacksmith.sh/codesmith/view-in-codesmith-dark.svg"></picture></a>
<sup>Need help on this PR? Tag <code>@codesmith</code> with what you
need.</sup>

- [ ] Let Codesmith autofix CI failures and bot reviews
<!-- /codesmith:footer -->

Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
@sonarqubecloud
Copy link
Copy Markdown

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant