@@ -14,7 +14,8 @@ import {
1414 resolveCustomThemeMode
1515} from '@renderer/lib/custom-themes'
1616import { withExportTitle } from '@shared/export-title'
17- import { settleExportImages } from '@renderer/lib/export-images'
17+ import { fitExportImageBoxes , settleExportImages } from '@renderer/lib/export-images'
18+ import { fitExportImagesToPages } from '@renderer/lib/export-pagination'
1819import '@renderer/styles/index.css'
1920
2021const PREFS_KEY = 'zen:prefs:v2'
@@ -130,7 +131,12 @@ function loadExportPrefs(): ExportPrefs {
130131// frozen-width content is clipped on the sides. (Prose text always reflows, so
131132// only such fixed-width content was affected, and only when the reading width
132133// exceeded the printable width.)
133- const PDF_PRINTABLE_WIDTH = '7.1in'
134+ // Chromium rounds the 0.7in margin to 67 CSS pixels. Use that exact margin for
135+ // both print and pagination measurement so line wraps/page boundaries agree.
136+ const PDF_PAGE_MARGIN_PX = Math . round ( 0.7 * 96 )
137+ const PDF_PRINTABLE_WIDTH_PX = 8.5 * 96 - 2 * PDF_PAGE_MARGIN_PX
138+ const PDF_PRINTABLE_HEIGHT_PX = 11 * 96 - 2 * PDF_PAGE_MARGIN_PX
139+ const PDF_PRINTABLE_WIDTH = `${ PDF_PRINTABLE_WIDTH_PX } px`
134140
135141/** The clean default: a light theme on a white page, best for printing. */
136142function applyLightExportTheme ( ) : void {
@@ -242,14 +248,6 @@ function ExportNoteWindow({ notePath }: { notePath: string }): JSX.Element {
242248 // When exporting in the user's theme, the page follows the theme background;
243249 // otherwise it's the clean white print page.
244250 const pageBg = prefs . pdfExportUseTheme ? 'rgb(var(--z-bg))' : '#ffffff'
245- // A themed export goes full-bleed: with a non-zero @page margin, paged media
246- // leaves that margin frame unpainted and `color-scheme: dark` fills it with
247- // Chromium's default dark canvas (#121212) — a mismatched frame around the
248- // themed content. So drop the page margin and inset the content with padding
249- // instead, letting --z-bg cover the whole sheet. The light export keeps the
250- // classic per-page margin (white paper margins look correct there).
251- const pageMargin = prefs . pdfExportUseTheme ? '0' : '0.7in'
252- const contentInset = prefs . pdfExportUseTheme ? '0.7in' : '0'
253251
254252 useEffect ( ( ) => {
255253 applyExportPrefs ( prefs )
@@ -347,7 +345,12 @@ function ExportNoteWindow({ notePath }: { notePath: string }): JSX.Element {
347345 < >
348346 < style > { `
349347 @page {
350- margin: ${ pageMargin } ;
348+ size: Letter;
349+ margin: ${ PDF_PAGE_MARGIN_PX } px;
350+ /* Paint the margins as well as the content. Document padding only
351+ insets the first/last page, leaving continuation pages flush with
352+ the paper edge; real page margins repeat on every sheet. */
353+ background: ${ pageBg } ;
351354 }
352355 html,
353356 body,
@@ -378,17 +381,31 @@ function ExportNoteWindow({ notePath }: { notePath: string }): JSX.Element {
378381 print-color-adjust: exact;
379382 }
380383 .export-note-shell .prose-zen {
381- padding: 32px 40px 48px;
384+ /* Measure images and diagrams at the same column width as print. */
385+ width: ${ PDF_PRINTABLE_WIDTH } ;
386+ max-width: ${ PDF_PRINTABLE_WIDTH } ;
387+ padding: 0;
388+ margin: 0;
389+ orphans: 2;
390+ widows: 2;
391+ }
392+ .export-note-shell .prose-zen :is(h1, h2, h3, h4, h5, h6) {
393+ break-inside: avoid;
394+ break-after: avoid;
395+ }
396+ .export-note-shell .local-image-embed,
397+ .export-note-shell img {
398+ break-inside: avoid;
382399 }
383400 /* Keep tall (portrait) images within the printable page height so they
384401 scale down proportionally instead of overflowing the page and being
385402 clipped at the page boundary — a single <img> can't paginate (#231).
386403 Letter page height is 11in - 2 * 0.7in margins = 9.6in; cap a touch
387- under that so an image still fits below a heading/ caption. */
404+ under that to leave space for the image frame and caption. */
388405 .export-note-shell img {
389406 max-width: 100%;
390407 height: auto;
391- max-height: 9.3in ;
408+ max-height: 9in ;
392409 object-fit: contain;
393410 }
394411 /* A standalone local image is rendered as a .local-image-embed figure
@@ -413,7 +430,7 @@ function ExportNoteWindow({ notePath }: { notePath: string }): JSX.Element {
413430 width: auto;
414431 height: auto;
415432 max-width: 100%;
416- max-height: 9.3in ;
433+ max-height: 9in ;
417434 }
418435 @media print {
419436 html,
@@ -428,21 +445,14 @@ function ExportNoteWindow({ notePath }: { notePath: string }): JSX.Element {
428445 min-height: auto;
429446 overflow: visible;
430447 box-sizing: border-box;
431- /* With @page margin dropped for themed (full-bleed) exports, the
432- content inset comes from padding here instead — so the theme
433- background reaches the paper edge. 0 for the light export. */
434- padding: ${ contentInset } ;
448+ padding: 0;
435449 }
436450 .export-note-shell .prose-zen {
437451 max-width: none;
438452 width: 100%;
439453 padding: 0;
440454 margin: 0;
441455 }
442- .export-note-shell img {
443- max-height: 9.3in;
444- break-inside: avoid;
445- }
446456 }
447457 ` } </ style >
448458 < main className = "export-note-shell" >
@@ -453,7 +463,17 @@ function ExportNoteWindow({ notePath }: { notePath: string }): JSX.Element {
453463 // Images load after the DOM is in place, and the preview defers
454464 // the ones below the viewport; print only once they have all
455465 // settled (#769).
456- void settleExportImages ( document ) . then ( ( ) => setExportState ( 'ready' ) )
466+ void Promise . all ( [ settleExportImages ( document ) , document . fonts . ready ] ) . then ( ( ) => {
467+ fitExportImageBoxes ( document , PDF_PRINTABLE_HEIGHT_PX )
468+ const article = document . querySelector < HTMLElement > ( '[data-preview-content]' )
469+ if ( article ) {
470+ fitExportImagesToPages ( article , {
471+ width : PDF_PRINTABLE_WIDTH_PX ,
472+ height : PDF_PRINTABLE_HEIGHT_PX
473+ } )
474+ }
475+ setExportState ( 'ready' )
476+ } )
457477 } }
458478 />
459479 </ main >
0 commit comments