Skip to main content

max / balanced_breakfast

Bump printpdf 0.7 to 0.11.3 printpdf 0.8 replaced the imperative document/layer/use_text builder with an Op-sequence model, so render.rs is rewritten against it: - Renderer now accumulates Vec<Op> per page and flushes PdfPage::new on overflow, instead of mutating a live PdfDocumentReference. - Text is emitted as StartTextSection / SetFont / SetTextCursor / ShowText / EndTextSection. Builtin fonts are PdfFontHandle::Builtin, no per-doc registration. - Geometry is unchanged: bottom-left origin, baseline y, A5 (148x210mm). - Save is now infallible (PdfDocument::save returns bytes + warnings). Public render_article signature is unchanged; callers untouched. 7 tests pass, clippy clean. Verified output: A5 page size, correct text flow, all block types present (examples/sample.rs renders a representative article).
Author: Max Johnson <me@maxj.phd> · 2026-07-22 22:28 UTC
Commit: 9074f67744602634b44eeda3d16ff14e34469d40
Parent: 5182057
4 files changed, +319 insertions, -123 deletions
M Cargo.lock +230 -60
@@ -78,6 +78,12 @@ dependencies = [
78 78 ]
79 79
80 80 [[package]]
81 + name = "aliasable"
82 + version = "0.1.3"
83 + source = "registry+https://github.com/rust-lang/crates.io-index"
84 + checksum = "250f629c0161ad8107cf89319e990051fae62832fd343083bea452d93e2205fd"
85 +
86 + [[package]]
81 87 name = "alloc-no-stdlib"
82 88 version = "2.0.4"
83 89 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -99,6 +105,29 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
99 105 checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923"
100 106
101 107 [[package]]
108 + name = "allsorts-azul"
109 + version = "0.17.1"
110 + source = "registry+https://github.com/rust-lang/crates.io-index"
111 + checksum = "c652f26321a22e93b603f5589b07f49a99c20d53d88de889a04b45079abf25ca"
112 + dependencies = [
113 + "bitflags 2.13.1",
114 + "brotli-decompressor",
115 + "encoding_rs",
116 + "enumflags2",
117 + "flate2",
118 + "glyph-names",
119 + "log",
120 + "ouroboros",
121 + "pathfinder_geometry",
122 + "rustc-hash",
123 + "tinyvec",
124 + "ucd-trie",
125 + "unicode-canonical-combining-class",
126 + "unicode-general-category",
127 + "unicode-joining-type",
128 + ]
129 +
130 + [[package]]
102 131 name = "ammonia"
103 132 version = "4.1.2"
104 133 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -159,6 +188,12 @@ dependencies = [
159 188 ]
160 189
161 190 [[package]]
191 + name = "arrayvec"
192 + version = "0.7.8"
193 + source = "registry+https://github.com/rust-lang/crates.io-index"
194 + checksum = "d3fb67a6e08acf24fdeccbac2cb6ac4305825bd1f117462e0e6f2f193345ad56"
195 +
196 + [[package]]
162 197 name = "assert-json-diff"
163 198 version = "2.0.2"
164 199 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -218,7 +253,7 @@ dependencies = [
218 253 "futures-io",
219 254 "futures-lite",
220 255 "parking",
221 - "polling 3.11.0",
256 + "polling",
222 257 "rustix",
223 258 "slab",
224 259 "windows-sys 0.61.2",
@@ -344,6 +379,60 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
344 379 checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8"
345 380
346 381 [[package]]
382 + name = "azul-core"
383 + version = "0.0.10"
384 + source = "registry+https://github.com/rust-lang/crates.io-index"
385 + checksum = "469d0a85e428ad578b51edbeb6577d379395e1ea6eea8959b61eec6e80bc3aad"
386 + dependencies = [
387 + "azul-css",
388 + "gl-context-loader",
389 + "libm",
390 + "rust-fontconfig",
391 + ]
392 +
393 + [[package]]
394 + name = "azul-css"
395 + version = "0.0.10"
396 + source = "registry+https://github.com/rust-lang/crates.io-index"
397 + checksum = "b328c57926ea9aae5bdc2aae2923f6b1771a2ad1904e613b6886135505533c8b"
398 + dependencies = [
399 + "azul-simplecss",
400 + "libm",
401 + ]
402 +
403 + [[package]]
404 + name = "azul-layout"
405 + version = "0.0.11"
406 + source = "registry+https://github.com/rust-lang/crates.io-index"
407 + checksum = "fccff6f166dca1b1b328f4fc9eac193c1416cf5198d8117f188218a9d89c8ddf"
408 + dependencies = [
409 + "allsorts-azul",
410 + "azul-core",
411 + "azul-css",
412 + "base64 0.22.1",
413 + "hyphenation",
414 + "lru",
415 + "roxmltree",
416 + "rust-fontconfig",
417 + "serde",
418 + "smallvec",
419 + "taffy",
420 + "thiserror 2.0.18",
421 + "tinyvec",
422 + "unicode-bidi",
423 + "unicode-normalization",
424 + "unicode-segmentation",
425 + "xmlparser",
426 + "xmlwriter",
427 + ]
428 +
429 + [[package]]
430 + name = "azul-simplecss"
431 + version = "0.2.0"
432 + source = "registry+https://github.com/rust-lang/crates.io-index"
433 + checksum = "8259ebd5ee48a37fd8931116405456fd67efbb5435b4789e45bdbccf5d7dea7e"
434 +
435 + [[package]]
347 436 name = "balanced-breakfast-desktop"
348 437 version = "0.3.3"
349 438 dependencies = [
@@ -482,6 +571,15 @@ dependencies = [
482 571 ]
483 572
484 573 [[package]]
574 + name = "bincode"
575 + version = "1.3.3"
576 + source = "registry+https://github.com/rust-lang/crates.io-index"
577 + checksum = "b1f45e9417d87227c7a56d22e471c6206462cba514c7590c09aff4cf6d1ddcad"
578 + dependencies = [
579 + "serde",
580 + ]
581 +
582 + [[package]]
485 583 name = "bitflags"
486 584 version = "1.3.2"
487 585 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -533,6 +631,15 @@ dependencies = [
533 631 ]
534 632
535 633 [[package]]
634 + name = "block-padding"
635 + version = "0.4.2"
636 + source = "registry+https://github.com/rust-lang/crates.io-index"
637 + checksum = "710f1dd022ef4e93f8a438b4ba958de7f64308434fa6a87104481645cc30068b"
638 + dependencies = [
639 + "hybrid-array",
640 + ]
641 +
642 + [[package]]
536 643 name = "block2"
537 644 version = "0.6.2"
538 645 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -576,17 +683,6 @@ dependencies = [
576 683 ]
577 684
578 685 [[package]]
579 - name = "bstr"
580 - version = "1.13.0"
581 - source = "registry+https://github.com/rust-lang/crates.io-index"
582 - checksum = "1f7dc094d718f2e1c1559ad110e27eeaae14a5465d3d56dd6dbd793079fbd530"
583 - dependencies = [
584 - "memchr",
585 - "regex-automata",
586 - "serde_core",
587 - ]
588 -
589 - [[package]]
590 686 name = "bumpalo"
591 687 version = "3.19.1"
592 688 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -690,6 +786,15 @@ dependencies = [
690 786 ]
691 787
692 788 [[package]]
789 + name = "cbc"
790 + version = "0.2.1"
791 + source = "registry+https://github.com/rust-lang/crates.io-index"
792 + checksum = "ce2dc9ee5f88d11e0beb842c88b33c8a5cf0d1329c4b19494af42b07dbfe8896"
793 + dependencies = [
794 + "cipher 0.5.2",
795 + ]
796 +
797 + [[package]]
693 798 name = "cc"
694 799 version = "1.2.55"
695 800 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -1384,6 +1489,15 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
1384 1489 checksum = "d0881ea181b1df73ff77ffaaf9c7544ecc11e82fba9b5f27b262a3c73a332555"
1385 1490
1386 1491 [[package]]
1492 + name = "ecb"
1493 + version = "0.2.0"
1494 + source = "registry+https://github.com/rust-lang/crates.io-index"
1495 + checksum = "fbfbf3db731928d6912bc3beda911d55b834cee3df6131ba79b337a1298a3fa9"
1496 + dependencies = [
1497 + "cipher 0.5.2",
1498 + ]
1499 +
1500 + [[package]]
1387 1501 name = "ego-tree"
1388 1502 version = "0.11.0"
1389 1503 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -1526,9 +1640,9 @@ checksum = "7360491ce676a36bf9bb3c56c1aa791658183a54d2744120f27285738d90465a"
1526 1640
1527 1641 [[package]]
1528 1642 name = "fastrand"
1529 - version = "2.3.0"
1643 + version = "2.5.0"
1530 1644 source = "registry+https://github.com/rust-lang/crates.io-index"
1531 - checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be"
1645 + checksum = "da7c62ceae207dd37ea5b845da6a0696c799f85e97da1ab5b7910be3c1c80223"
1532 1646
1533 1647 [[package]]
1534 1648 name = "fdeflate"
@@ -1578,17 +1692,6 @@ dependencies = [
1578 1692
1579 1693 [[package]]
1580 1694 name = "flume"
1581 - version = "0.11.1"
1582 - source = "registry+https://github.com/rust-lang/crates.io-index"
1583 - checksum = "da0e4dd2a88388a1f4ccc7c9ce104604dab68d9f408dc34cd45823d5a9069095"
1584 - dependencies = [
1585 - "futures-core",
1586 - "futures-sink",
1587 - "spin 0.9.8",
1588 - ]
1589 -
1590 - [[package]]
1591 - name = "flume"
1592 1695 version = "0.12.0"
1593 1696 source = "registry+https://github.com/rust-lang/crates.io-index"
1594 1697 checksum = "5e139bc46ca777eb5efaf62df0ab8cc5fd400866427e56c68b22e414e53bd3be"
@@ -1662,6 +1765,12 @@ dependencies = [
1662 1765 ]
1663 1766
1664 1767 [[package]]
1768 + name = "fst"
1769 + version = "0.4.7"
1770 + source = "registry+https://github.com/rust-lang/crates.io-index"
1771 + checksum = "7ab85b9b05e3978cc9a9cf8fea7f01b494e1a09ed3037e16ba39edc7a29eb61a"
1772 +
1773 + [[package]]
1665 1774 name = "futf"
1666 1775 version = "0.1.5"
1667 1776 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -1951,9 +2060,11 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
1951 2060 checksum = "300e883d756b2e4ec94e02791f39b04b522276138852cfc41d9fb7e904106099"
1952 2061 dependencies = [
1953 2062 "cfg-if",
2063 + "js-sys",
1954 2064 "libc",
1955 2065 "r-efi 6.0.0",
1956 2066 "rand_core 0.10.1",
2067 + "wasm-bindgen",
1957 2068 ]
1958 2069
1959 2070 [[package]]
@@ -1998,6 +2109,12 @@ dependencies = [
1998 2109 ]
1999 2110
2000 2111 [[package]]
2112 + name = "gl-context-loader"
2113 + version = "0.1.10"
2114 + source = "registry+https://github.com/rust-lang/crates.io-index"
2115 + checksum = "5ca63cb2488b847c12d1ca56034ba0f15613972c0dc0f8a957f525707567b23d"
2116 +
2117 + [[package]]
2001 2118 name = "glib"
2002 2119 version = "0.18.5"
2003 2120 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -2051,6 +2168,12 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
2051 2168 checksum = "0cc23270f6e1808e30a928bdc84dea0b9b4136a8bc82338574f23baf47bbd280"
2052 2169
2053 2170 [[package]]
2171 + name = "glyph-names"
2172 + version = "0.2.0"
2173 + source = "registry+https://github.com/rust-lang/crates.io-index"
2174 + checksum = "c3531d702d6c1a3ba92a5fb55a404c7b8c476c8e7ca249951077afcbe4bc807f"
2175 +
2176 + [[package]]
2054 2177 name = "gobject-sys"
2055 2178 version = "0.18.0"
2056 2179 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -2062,6 +2185,12 @@ dependencies = [
2062 2185 ]
2063 2186
2064 2187 [[package]]
2188 + name = "grid"
2189 + version = "1.0.1"
2190 + source = "registry+https://github.com/rust-lang/crates.io-index"
2191 + checksum = "b40ca9252762c466af32d0b1002e91e4e1bc5398f77455e55474deb466355ff5"
2192 +
2193 + [[package]]
2065 2194 name = "gtk"
2066 2195 version = "0.18.2"
2067 2196 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -2125,7 +2254,7 @@ dependencies = [
2125 2254 "futures-core",
2126 2255 "futures-sink",
2127 2256 "http",
2128 - "indexmap 2.13.0",
2257 + "indexmap 2.14.0",
2129 2258 "slab",
2130 2259 "tokio",
2131 2260 "tokio-util",
@@ -2150,6 +2279,12 @@ dependencies = [
2150 2279 ]
2151 2280
2152 2281 [[package]]
2282 + name = "hashbrown"
2283 + version = "0.17.1"
2284 + source = "registry+https://github.com/rust-lang/crates.io-index"
2285 + checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a"
2286 +
2287 + [[package]]
2153 2288 name = "hashlink"
2154 2289 version = "0.11.1"
2155 2290 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -2391,7 +2526,7 @@ dependencies = [
2391 2526 "libc",
2392 2527 "percent-encoding",
2393 2528 "pin-project-lite",
2394 - "socket2 0.6.2",
2529 + "socket2",
2395 2530 "system-configuration",
2396 2531 "tokio",
2397 2532 "tower-service",
@@ -2400,6 +2535,29 @@ dependencies = [
2400 2535 ]
2401 2536
2402 2537 [[package]]
2538 + name = "hyphenation"
2539 + version = "0.8.4"
2540 + source = "registry+https://github.com/rust-lang/crates.io-index"
2541 + checksum = "bcf4dd4c44ae85155502a52c48739c8a48185d1449fff1963cffee63c28a50f0"
2542 + dependencies = [
2543 + "bincode",
2544 + "fst",
2545 + "hyphenation_commons",
2546 + "pocket-resources",
2547 + "serde",
2548 + ]
2549 +
2550 + [[package]]
2551 + name = "hyphenation_commons"
2552 + version = "0.8.4"
2553 + source = "registry+https://github.com/rust-lang/crates.io-index"
2554 + checksum = "5febe7a2ade5c7d98eb8b75f946c046b335324b06a14ea0998271504134c05bf"
2555 + dependencies = [
2556 + "fst",
2557 + "serde",
2558 + ]
2559 +
2560 + [[package]]
2403 2561 name = "iana-time-zone"
2404 2562 version = "0.1.65"
2405 2563 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -2543,12 +2701,12 @@ dependencies = [
2543 2701
2544 2702 [[package]]
2545 2703 name = "if-addrs"
2546 - version = "0.13.4"
2704 + version = "0.15.0"
2547 2705 source = "registry+https://github.com/rust-lang/crates.io-index"
2548 - checksum = "69b2eeee38fef3aa9b4cc5f1beea8a2444fc00e7377cafae396de3f5c2065e24"
2706 + checksum = "c0a05c691e1fae256cf7013d99dad472dc52d5543322761f83ec8d47eab40d2b"
2549 2707 dependencies = [
2550 2708 "libc",
2551 - "windows-sys 0.59.0",
2709 + "windows-sys 0.61.2",
2552 2710 ]
2553 2711
2554 2712 [[package]]
@@ -2583,12 +2741,12 @@ dependencies = [
2583 2741
2584 2742 [[package]]
2585 2743 name = "indexmap"
2586 - version = "2.13.0"
2744 + version = "2.14.0"
2587 2745 source = "registry+https://github.com/rust-lang/crates.io-index"
2588 - checksum = "7714e70437a7dc3ac8eb7e6f8df75fd8eb422675fc7678aff7364301092b1017"
2746 + checksum = "d466e9454f08e4a911e14806c24e16fba1b4c121d1ea474396f396069cf949d9"
2589 2747 dependencies = [
2590 2748 "equivalent",
2591 - "hashbrown 0.16.1",
2749 + "hashbrown 0.17.1",
2592 2750 "serde",
2593 2751 "serde_core",
2594 2752 ]
@@ -2608,7 +2766,7 @@ version = "0.1.4"
2608 2766 source = "registry+https://github.com/rust-lang/crates.io-index"
2609 2767 checksum = "879f10e63c20629ecabbb64a8010319738c66a5cd0c29b02d63d272b03751d01"
2610 2768 dependencies = [
2611 - "block-padding",
2769 + "block-padding 0.3.3",
2612 2770 "generic-array",
2613 2771 ]
2614 2772
@@ -2618,6 +2776,7 @@ version = "0.2.2"
2618 2776 source = "registry+https://github.com/rust-lang/crates.io-index"
2619 2777 checksum = "4250ce6452e92010fdf7268ccc5d14faa80bb12fc741938534c58f16804e03c7"
2620 2778 dependencies = [
2779 + "block-padding 0.4.2",
2621 2780 "hybrid-array",
2622 2781 ]
2623 2782
@@ -2797,7 +2956,7 @@ checksum = "02cb977175687f33fa4afa0c95c112b987ea1443e5a51c8f8ff27dc618270cc2"
2797 2956 dependencies = [
2798 2957 "cssparser 0.29.6",
2799 2958 "html5ever 0.29.1",
2800 - "indexmap 2.13.0",
2959 + "indexmap 2.14.0",
2801 2960 "selectors 0.24.0",
2802 2961 ]
2803 2962
@@ -2833,9 +2992,9 @@ dependencies = [
2833 2992
2834 2993 [[package]]
2835 2994 name = "libc"
2836 - version = "0.2.180"
2995 + version = "0.2.189"
2837 2996 source = "registry+https://github.com/rust-lang/crates.io-index"
2838 - checksum = "bcc35a38544a891a5f7c865aca548a982ccb3b8650a5b06d0fd33a10283c56fc"
2997 + checksum = "3eaf3ede3fee6db1a4c2ee091bf8a8b4dccdc6d17f656fb07896ee72867612f2"
2839 2998
2840 2999 [[package]]
2841 3000 name = "libloading"
@@ -2848,6 +3007,12 @@ dependencies = [
2848 3007 ]
2849 3008
2850 3009 [[package]]
3010 + name = "libm"
3011 + version = "0.2.16"
3012 + source = "registry+https://github.com/rust-lang/crates.io-index"
3013 + checksum = "b6d2cec3eae94f9f509c767b45932f1ada8350c4bdb85af2fcab4a3c14807981"
3014 +
3015 + [[package]]
2851 3016 name = "libredox"
2852 3017 version = "0.1.12"
2853 3018 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -2870,12 +3035,6 @@ dependencies = [
2870 3035 ]
2871 3036
2872 3037 [[package]]
2873 - name = "linked-hash-map"
2874 - version = "0.5.6"
2875 - source = "registry+https://github.com/rust-lang/crates.io-index"
2876 - checksum = "0717cef1bc8b636c6e1c1bbdefc09e6322da8a9321966e8928ef80d20f7f770f"
2877 -
2878 - [[package]]
2879 3038 name = "linux-raw-sys"
2880 3039 version = "0.11.0"
2881 3040 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -2904,22 +3063,37 @@ checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897"
2904 3063
2905 3064 [[package]]
2906 3065 name = "lopdf"
2907 - version = "0.31.0"
3066 + version = "0.44.0"
2908 3067 source = "registry+https://github.com/rust-lang/crates.io-index"
2909 - checksum = "07c8e1b6184b1b32ea5f72f572ebdc40e5da1d2921fa469947ff7c480ad1f85a"
3068 + checksum = "5e2ec995d822e05cabc3f06d196ee43650af3fe4fe38012cacb35e0c3d113b68"
2910 3069 dependencies = [
3070 + "aes 0.9.1",
3071 + "bitflags 2.13.1",
3072 + "cbc 0.2.1",
3073 + "ecb",
2911 3074 "encoding_rs",
2912 3075 "flate2",
3076 + "getrandom 0.4.3",
3077 + "indexmap 2.14.0",
2913 3078 "itoa 1.0.17",
2914 - "linked-hash-map",
2915 3079 "log",
2916 - "md5",
2917 - "pom",
2918 - "time",
3080 + "md-5",
3081 + "nom",
3082 + "rand 0.10.2",
3083 + "rangemap",
3084 + "sha2 0.11.0",
3085 + "stringprep",
3086 + "thiserror 2.0.18",
2919 3087 "weezl",
2920 3088 ]
2921 3089
2922 3090 [[package]]
3091 + name = "lru"
3092 + version = "0.16.4"
3093 + source = "registry+https://github.com/rust-lang/crates.io-index"
3094 + checksum = "7f66e8d5d03f609abc3a39e6f08e4164ebf1447a732906d39eb9b99b7919ef39"
3095 +
3096 + [[package]]
2923 3097 name = "mac"
2924 3098 version = "0.1.1"
2925 3099 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -3040,22 +3214,18 @@ dependencies = [
3040 3214 ]
3041 3215
3042 3216 [[package]]
3043 - name = "md5"
3044 - version = "0.7.0"
3045 - source = "registry+https://github.com/rust-lang/crates.io-index"
3046 - checksum = "490cc448043f947bae3cbee9c203358d62dbee0db12107a74be5c30ccfd09771"
3047 -
3048 - [[package]]
3049 3217 name = "mdns-sd"
3050 - version = "0.11.5"
3218 + version = "0.20.2"
3051 3219 source = "registry+https://github.com/rust-lang/crates.io-index"
3052 - checksum = "8fe7c11a1eb3cfbfcf702d1601c1f5f4c102cdc8665b8a557783ef634741676e"
3220 + checksum = "f18d8ec9d1869796fb2910d95f4d957072df0b6a22e247a1d760d8b4c805e17a"
3053 3221 dependencies = [
3054 - "flume 0.11.1",
3222 + "fastrand",
3223 + "flume",
3055 3224 "if-addrs",
3056 3225 "log",
3057 - "polling 2.8.0",
3058 - "socket2 0.5.10",
3226 + "mio",
3227 + "socket-pktinfo",
3228 + "socket2",
3059 3229 ]
Lines truncated
@@ -8,5 +8,5 @@ description = "Render Balanced Breakfast article HTML to a Supernote-friendly PD
8 8 [dependencies]
9 9 pter = { path = "../../../../Libraries/pter" }
10 10 pulldown-cmark = { version = "0.13", default-features = false }
11 - printpdf = { version = "0.7" }
11 + printpdf = { version = "0.11" }
12 12 thiserror = { workspace = true }
@@ -0,0 +1,33 @@
1 + //! Render a representative article to a PDF for visual inspection.
2 + //! Usage: cargo run -p bb-pdf --example sample -- /path/to/out.pdf
3 +
4 + fn main() {
5 + let out = std::env::args()
6 + .nth(1)
7 + .unwrap_or_else(|| "bb-pdf-sample.pdf".to_string());
8 +
9 + let html = r#"
10 + <h1>The Morning Read</h1>
11 + <p>This is a <strong>bold</strong> lead paragraph with an <em>italic</em>
12 + aside, long enough to force line wrapping across the A5 column so we can
13 + see how the body text flows on the Supernote page.</p>
14 + <h2>A second-level heading</h2>
15 + <p>Ordinary body copy follows, then a list:</p>
16 + <ul><li>first bullet item</li><li>second bullet item that runs long enough to wrap onto a second line for good measure</li></ul>
17 + <ol><li>numbered one</li><li>numbered two</li></ol>
18 + <blockquote>A quoted passage, rendered in italic with a left indent.</blockquote>
19 + <h3>Code</h3>
20 + <pre><code>fn main() { println!("monospace"); }</code></pre>
21 + <p>And a closing paragraph to round things out. Lorem ipsum dolor sit amet,
22 + consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore.</p>
23 + "#;
24 +
25 + let bytes = bb_pdf::render_article(
26 + html,
27 + "Sample Article",
28 + Some("https://example.com/morning-read"),
29 + )
30 + .expect("render");
31 + std::fs::write(&out, &bytes).expect("write");
32 + println!("wrote {} ({} bytes)", out, bytes.len());
33 + }
@@ -1,8 +1,5 @@
1 - use std::io::BufWriter;
2 -
3 1 use printpdf::{
4 - BuiltinFont, IndirectFontRef, Mm, PdfDocument, PdfDocumentReference, PdfLayerIndex,
5 - PdfPageIndex,
2 + BuiltinFont, Mm, Op, PdfDocument, PdfFontHandle, PdfPage, PdfSaveOptions, Point, Pt, TextItem,
6 3 };
7 4 use pulldown_cmark::{Event, HeadingLevel, Options, Parser, Tag, TagEnd};
8 5
@@ -15,7 +12,7 @@ pub fn render_article(html: &str, title: &str, url: Option<&str>) -> Result<Vec<
15 12 return Err(Error::Empty);
16 13 }
17 14
18 - let mut r = Renderer::new(title)?;
15 + let mut r = Renderer::new(title);
19 16 if !title.trim().is_empty() {
20 17 r.draw_heading(title, H1_PT);
21 18 if let Some(u) = url {
@@ -54,54 +51,37 @@ impl Style {
54 51 }
55 52
56 53 struct Renderer {
57 - doc: PdfDocumentReference,
58 - page_idx: PdfPageIndex,
59 - layer_idx: PdfLayerIndex,
60 - fonts: Fonts,
54 + title: String,
55 + /// Pages already flushed. The page currently being filled lives in `ops`.
56 + pages: Vec<PdfPage>,
57 + /// Content operations for the page being filled.
58 + ops: Vec<Op>,
61 59 y_mm: f32,
62 60 }
63 61
64 - struct Fonts {
65 - regular: IndirectFontRef,
66 - bold: IndirectFontRef,
67 - italic: IndirectFontRef,
68 - mono: IndirectFontRef,
69 - }
70 -
71 62 impl Renderer {
72 - fn new(title: &str) -> Result<Self, Error> {
63 + fn new(title: &str) -> Self {
73 64 let doc_title = if title.trim().is_empty() {
74 65 "Balanced Breakfast"
75 66 } else {
76 67 title
77 68 };
78 - let (doc, page_idx, layer_idx) = PdfDocument::new(doc_title, PAGE_W, PAGE_H, "L1");
79 - let regular = doc.add_builtin_font(BuiltinFont::Helvetica).map_err(err)?;
80 - let bold = doc
81 - .add_builtin_font(BuiltinFont::HelveticaBold)
82 - .map_err(err)?;
83 - let italic = doc
84 - .add_builtin_font(BuiltinFont::HelveticaOblique)
85 - .map_err(err)?;
86 - let mono = doc.add_builtin_font(BuiltinFont::Courier).map_err(err)?;
87 - Ok(Self {
88 - doc,
89 - page_idx,
90 - layer_idx,
91 - fonts: Fonts {
92 - regular,
93 - bold,
94 - italic,
95 - mono,
96 - },
69 + Self {
70 + title: doc_title.to_string(),
71 + pages: Vec::new(),
72 + ops: Vec::new(),
97 73 y_mm: content_top_mm(),
98 - })
74 + }
99 75 }
100 76
101 - fn finish(self) -> Result<Vec<u8>, Error> {
102 - let mut buf = Vec::new();
103 - self.doc.save(&mut BufWriter::new(&mut buf)).map_err(err)?;
104 - Ok(buf)
77 + fn finish(mut self) -> Result<Vec<u8>, Error> {
78 + // Flush the page still being filled.
79 + self.pages
80 + .push(PdfPage::new(PAGE_W, PAGE_H, std::mem::take(&mut self.ops)));
81 + let mut doc = PdfDocument::new(&self.title);
82 + doc.with_pages(self.pages);
83 + let mut warnings = Vec::new();
84 + Ok(doc.save(&PdfSaveOptions::default(), &mut warnings))
105 85 }
106 86
107 87 fn render_markdown(&mut self, md: &str) {
@@ -265,41 +245,58 @@ impl Renderer {
265 245 self.ensure_room(line_h);
266 246 let x = MARGIN.0 + text_indent;
267 247 let font = self.font_for(style);
268 - let layer = self.doc.get_page(self.page_idx).get_layer(self.layer_idx);
269 248 let baseline_y = self.y_mm - style.size_pt * 25.4 / 72.0;
270 249
271 250 if first && !leader.is_empty() {
272 - layer.use_text(
273 - &leader,
274 - style.size_pt,
275 - Mm(x - LIST_INDENT_MM),
276 - Mm(baseline_y),
277 - font,
278 - );
251 + self.draw_text(&leader, style.size_pt, x - LIST_INDENT_MM, baseline_y, font);
279 252 }
280 - layer.use_text(&line, style.size_pt, Mm(x), Mm(baseline_y), font);
253 + self.draw_text(&line, style.size_pt, x, baseline_y, font);
281 254 self.y_mm -= line_h;
282 255 first = false;
283 256 }
284 257 }
285 258
286 - fn font_for(&self, style: Style) -> &IndirectFontRef {
259 + /// Emit a single positioned text run into the current page. The origin is
260 + /// the page's bottom-left corner and `baseline_mm` is the text baseline,
261 + /// matching the geometry the layout math assumes.
262 + fn draw_text(
263 + &mut self,
264 + text: &str,
265 + size_pt: f32,
266 + x_mm: f32,
267 + baseline_mm: f32,
268 + font: BuiltinFont,
269 + ) {
270 + self.ops.push(Op::StartTextSection);
271 + self.ops.push(Op::SetFont {
272 + font: PdfFontHandle::Builtin(font),
273 + size: Pt(size_pt),
274 + });
275 + self.ops.push(Op::SetTextCursor {
276 + pos: Point::new(Mm(x_mm), Mm(baseline_mm)),
277 + });
278 + self.ops.push(Op::ShowText {
279 + items: vec![TextItem::Text(text.to_string())],
280 + });
281 + self.ops.push(Op::EndTextSection);
282 + }
283 +
284 + fn font_for(&self, style: Style) -> BuiltinFont {
287 285 if style.mono {
288 - &self.fonts.mono
286 + BuiltinFont::Courier
289 287 } else if style.bold {
290 - &self.fonts.bold
288 + BuiltinFont::HelveticaBold
291 289 } else if style.italic {
292 - &self.fonts.italic
290 + BuiltinFont::HelveticaOblique
293 291 } else {
294 - &self.fonts.regular
292 + BuiltinFont::Helvetica
295 293 }
296 294 }
297 295
298 296 fn ensure_room(&mut self, needed_mm: f32) {
299 297 if self.y_mm - needed_mm < content_bottom_mm() {
300 - let (page, layer) = self.doc.add_page(PAGE_W, PAGE_H, "L1");
301 - self.page_idx = page;
302 - self.layer_idx = layer;
298 + let ops = std::mem::take(&mut self.ops);
299 + self.pages.push(PdfPage::new(PAGE_W, PAGE_H, ops));
303 300 self.y_mm = content_top_mm();
304 301 }
305 302 }
@@ -340,10 +337,6 @@ fn wrap(text: &str, style: Style, max_width_mm: f32) -> Vec<String> {
340 337 lines
341 338 }
342 339
343 - fn err<E: std::fmt::Display>(e: E) -> Error {
344 - Error::PrintPdf(e.to_string())
345 - }
346 -
347 340 #[cfg(test)]
348 341 mod tests {
349 342 use super::*;