max / balanced_breakfast
14 files changed,
+882 insertions,
-3 deletions
| @@ -199,6 +199,7 @@ | |||
| 199 | 199 | "bb-db", | |
| 200 | 200 | "bb-feed", | |
| 201 | 201 | "bb-interface", | |
| 202 | + | "bb-pdf", | |
| 202 | 203 | "chrono", | |
| 203 | 204 | "futures", | |
| 204 | 205 | "open", | |
| @@ -210,6 +211,7 @@ | |||
| 210 | 211 | "serde_json", | |
| 211 | 212 | "sha2", | |
| 212 | 213 | "sqlx", | |
| 214 | + | "supernote-push", | |
| 213 | 215 | "synckit-client", | |
| 214 | 216 | "tagtree", | |
| 215 | 217 | "tauri", | |
| @@ -260,7 +262,7 @@ | |||
| 260 | 262 | "futures", | |
| 261 | 263 | "keyring", | |
| 262 | 264 | "parking_lot", | |
| 263 | - | "pter", | |
| 265 | + | "pter 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)", | |
| 264 | 266 | "rand 0.8.5", | |
| 265 | 267 | "readable-readability", | |
| 266 | 268 | "regex", | |
| @@ -316,6 +318,16 @@ | |||
| 316 | 318 | "serde_json", | |
| 317 | 319 | ] | |
| 318 | 320 | ||
| 321 | + | [[package]] | |
| 322 | + | name = "bb-pdf" | |
| 323 | + | version = "0.3.1" | |
| 324 | + | dependencies = [ | |
| 325 | + | "printpdf", | |
| 326 | + | "pter 0.1.0", | |
| 327 | + | "pulldown-cmark", | |
| 328 | + | "thiserror 1.0.69", | |
| 329 | + | ] | |
| 330 | + | ||
| 319 | 331 | [[package]] | |
| 320 | 332 | name = "bitflags" | |
| 321 | 333 | version = "1.3.2" | |
| @@ -379,6 +391,17 @@ | |||
| 379 | 391 | "alloc-stdlib", | |
| 380 | 392 | ] | |
| 381 | 393 | ||
| 394 | + | [[package]] | |
| 395 | + | name = "bstr" | |
| 396 | + | version = "1.13.0" | |
| 397 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 398 | + | checksum = "1f7dc094d718f2e1c1559ad110e27eeaae14a5465d3d56dd6dbd793079fbd530" | |
| 399 | + | dependencies = [ | |
| 400 | + | "memchr", | |
| 401 | + | "regex-automata", | |
| 402 | + | "serde_core", | |
| 403 | + | ] | |
| 404 | + | ||
| 382 | 405 | [[package]] | |
| 383 | 406 | name = "bumpalo" | |
| 384 | 407 | version = "3.19.1" | |
| @@ -2038,7 +2061,7 @@ | |||
| 2038 | 2061 | "libc", | |
| 2039 | 2062 | "percent-encoding", | |
| 2040 | 2063 | "pin-project-lite", | |
| 2041 | - | "socket2", | |
| 2064 | + | "socket2 0.6.2", | |
| 2042 | 2065 | "system-configuration", | |
| 2043 | 2066 | "tokio", | |
| 2044 | 2067 | "tower-service", | |
| @@ -2188,6 +2211,16 @@ | |||
| 2188 | 2211 | "icu_properties", | |
| 2189 | 2212 | ] | |
| 2190 | 2213 | ||
| 2214 | + | [[package]] | |
| 2215 | + | name = "if-addrs" | |
| 2216 | + | version = "0.13.4" | |
| 2217 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 2218 | + | checksum = "69b2eeee38fef3aa9b4cc5f1beea8a2444fc00e7377cafae396de3f5c2065e24" | |
| 2219 | + | dependencies = [ | |
| 2220 | + | "libc", | |
| 2221 | + | "windows-sys 0.59.0", | |
| 2222 | + | ] | |
| 2223 | + | ||
| 2191 | 2224 | [[package]] | |
| 2192 | 2225 | name = "indexmap" | |
| 2193 | 2226 | version = "1.9.3" | |
| @@ -2480,6 +2513,12 @@ | |||
| 2480 | 2513 | "vcpkg", | |
| 2481 | 2514 | ] | |
| 2482 | 2515 | ||
| 2516 | + | [[package]] | |
| 2517 | + | name = "linked-hash-map" | |
| 2518 | + | version = "0.5.6" | |
| 2519 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 2520 | + | checksum = "0717cef1bc8b636c6e1c1bbdefc09e6322da8a9321966e8928ef80d20f7f770f" | |
| 2521 | + | ||
| 2483 | 2522 | [[package]] | |
| 2484 | 2523 | name = "linux-keyutils" | |
| 2485 | 2524 | version = "0.2.5" | |
| @@ -2517,6 +2556,23 @@ | |||
| 2517 | 2556 | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 2518 | 2557 | checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" | |
| 2519 | 2558 | ||
| 2559 | + | [[package]] | |
| 2560 | + | name = "lopdf" | |
| 2561 | + | version = "0.31.0" | |
| 2562 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 2563 | + | checksum = "07c8e1b6184b1b32ea5f72f572ebdc40e5da1d2921fa469947ff7c480ad1f85a" | |
| 2564 | + | dependencies = [ | |
| 2565 | + | "encoding_rs", | |
| 2566 | + | "flate2", | |
| 2567 | + | "itoa 1.0.17", | |
| 2568 | + | "linked-hash-map", | |
| 2569 | + | "log", | |
| 2570 | + | "md5", | |
| 2571 | + | "pom", | |
| 2572 | + | "time", | |
| 2573 | + | "weezl", | |
| 2574 | + | ] | |
| 2575 | + | ||
| 2520 | 2576 | [[package]] | |
| 2521 | 2577 | name = "mac" | |
| 2522 | 2578 | version = "0.1.1" | |
| @@ -2626,6 +2682,25 @@ | |||
| 2626 | 2682 | "digest", | |
| 2627 | 2683 | ] | |
| 2628 | 2684 | ||
| 2685 | + | [[package]] | |
| 2686 | + | name = "md5" | |
| 2687 | + | version = "0.7.0" | |
| 2688 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 2689 | + | checksum = "490cc448043f947bae3cbee9c203358d62dbee0db12107a74be5c30ccfd09771" | |
| 2690 | + | ||
| 2691 | + | [[package]] | |
| 2692 | + | name = "mdns-sd" | |
| 2693 | + | version = "0.11.5" | |
| 2694 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 2695 | + | checksum = "8fe7c11a1eb3cfbfcf702d1601c1f5f4c102cdc8665b8a557783ef634741676e" | |
| 2696 | + | dependencies = [ | |
| 2697 | + | "flume", | |
| 2698 | + | "if-addrs", | |
| 2699 | + | "log", | |
| 2700 | + | "polling", | |
| 2701 | + | "socket2 0.5.10", | |
| 2702 | + | ] | |
| 2703 | + | ||
| 2629 | 2704 | [[package]] | |
| 2630 | 2705 | name = "memchr" | |
| 2631 | 2706 | version = "2.8.0" | |
| @@ -3181,6 +3256,15 @@ | |||
| 3181 | 3256 | "thiserror 2.0.18", | |
| 3182 | 3257 | ] | |
| 3183 | 3258 | ||
| 3259 | + | [[package]] | |
| 3260 | + | name = "owned_ttf_parser" | |
| 3261 | + | version = "0.19.0" | |
| 3262 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 3263 | + | checksum = "706de7e2214113d63a8238d1910463cfce781129a6f263d13fdb09ff64355ba4" | |
| 3264 | + | dependencies = [ | |
| 3265 | + | "ttf-parser", | |
| 3266 | + | ] | |
| 3267 | + | ||
| 3184 | 3268 | [[package]] | |
| 3185 | 3269 | name = "pango" | |
| 3186 | 3270 | version = "0.18.3" | |
| @@ -3535,6 +3619,22 @@ | |||
| 3535 | 3619 | "miniz_oxide", | |
| 3536 | 3620 | ] | |
| 3537 | 3621 | ||
| 3622 | + | [[package]] | |
| 3623 | + | name = "polling" | |
| 3624 | + | version = "2.8.0" | |
| 3625 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 3626 | + | checksum = "4b2d323e8ca7996b3e23126511a523f7e62924d93ecd5ae73b333815b0eb3dce" | |
| 3627 | + | dependencies = [ | |
| 3628 | + | "autocfg", | |
| 3629 | + | "bitflags 1.3.2", | |
| 3630 | + | "cfg-if", | |
| 3631 | + | "concurrent-queue", | |
| 3632 | + | "libc", | |
| 3633 | + | "log", | |
| 3634 | + | "pin-project-lite", | |
| 3635 | + | "windows-sys 0.48.0", | |
| 3636 | + | ] | |
| 3637 | + | ||
| 3538 | 3638 | [[package]] | |
| 3539 | 3639 | name = "poly1305" | |
| 3540 | 3640 | version = "0.8.0" | |
| @@ -3558,6 +3658,15 @@ | |||
| 3558 | 3658 | "universal-hash", | |
| 3559 | 3659 | ] | |
| 3560 | 3660 | ||
| 3661 | + | [[package]] | |
| 3662 | + | name = "pom" | |
| 3663 | + | version = "3.4.0" | |
| 3664 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 3665 | + | checksum = "6c972d8f86e943ad532d0b04e8965a749ad1d18bb981a9c7b3ae72fe7fd7744b" | |
| 3666 | + | dependencies = [ | |
| 3667 | + | "bstr", | |
| 3668 | + | ] | |
| 3669 | + | ||
| 3561 | 3670 | [[package]] | |
| 3562 | 3671 | name = "portable-atomic" | |
| 3563 | 3672 | version = "1.13.1" | |
| @@ -3594,6 +3703,18 @@ | |||
| 3594 | 3703 | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 3595 | 3704 | checksum = "925383efa346730478fb4838dbe9137d2a47675ad789c546d150a6e1dd4ab31c" | |
| 3596 | 3705 | ||
| 3706 | + | [[package]] | |
| 3707 | + | name = "printpdf" | |
| 3708 | + | version = "0.7.0" | |
| 3709 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 3710 | + | checksum = "c30a4cc87c3ca9a98f4970db158a7153f8d1ec8076e005751173c57836380b1d" | |
| 3711 | + | dependencies = [ | |
| 3712 | + | "js-sys", | |
| 3713 | + | "lopdf", | |
| 3714 | + | "owned_ttf_parser", | |
| 3715 | + | "time", | |
| 3716 | + | ] | |
| 3717 | + | ||
| 3597 | 3718 | [[package]] | |
| 3598 | 3719 | name = "proc-macro-crate" | |
| 3599 | 3720 | version = "1.3.1" | |
| @@ -3662,6 +3783,13 @@ | |||
| 3662 | 3783 | "unicode-ident", | |
| 3663 | 3784 | ] | |
| 3664 | 3785 | ||
| 3786 | + | [[package]] | |
| 3787 | + | name = "pter" | |
| 3788 | + | version = "0.1.0" | |
| 3789 | + | dependencies = [ | |
| 3790 | + | "scraper", | |
| 3791 | + | ] | |
| 3792 | + | ||
| 3665 | 3793 | [[package]] | |
| 3666 | 3794 | name = "pter" | |
| 3667 | 3795 | version = "0.1.0" | |
| @@ -4742,6 +4870,16 @@ | |||
| 4742 | 4870 | "version_check", | |
| 4743 | 4871 | ] | |
| 4744 | 4872 | ||
| 4873 | + | [[package]] | |
| 4874 | + | name = "socket2" | |
| 4875 | + | version = "0.5.10" | |
| 4876 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 4877 | + | checksum = "e22376abed350d73dd1cd119b57ffccad95b4e585a7cda43e286245ce23c0678" | |
| 4878 | + | dependencies = [ | |
| 4879 | + | "libc", | |
| 4880 | + | "windows-sys 0.52.0", | |
| 4881 | + | ] | |
| 4882 | + | ||
| 4745 | 4883 | [[package]] | |
| 4746 | 4884 | name = "socket2" | |
| 4747 | 4885 | version = "0.6.2" | |
| @@ -5105,6 +5243,18 @@ | |||
| 5105 | 5243 | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 5106 | 5244 | checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" | |
| 5107 | 5245 | ||
| 5246 | + | [[package]] | |
| 5247 | + | name = "supernote-push" | |
| 5248 | + | version = "0.1.0" | |
| 5249 | + | dependencies = [ | |
| 5250 | + | "mdns-sd", | |
| 5251 | + | "percent-encoding", | |
| 5252 | + | "serde", | |
| 5253 | + | "serde_json", | |
| 5254 | + | "thiserror 2.0.18", | |
| 5255 | + | "ureq", | |
| 5256 | + | ] | |
| 5257 | + | ||
| 5108 | 5258 | [[package]] | |
| 5109 | 5259 | name = "swift-rs" | |
| 5110 | 5260 | version = "1.0.7" | |
| @@ -5813,7 +5963,7 @@ | |||
| 5813 | 5963 | "libc", | |
| 5814 | 5964 | "mio", | |
| 5815 | 5965 | "pin-project-lite", | |
| 5816 | - | "socket2", | |
| 5966 | + | "socket2 0.6.2", | |
| 5817 | 5967 | "tokio-macros", | |
| 5818 | 5968 | "windows-sys 0.61.2", | |
| 5819 | 5969 | ] | |
| @@ -6104,6 +6254,12 @@ | |||
| 6104 | 6254 | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 6105 | 6255 | checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" | |
| 6106 | 6256 | ||
| 6257 | + | [[package]] | |
| 6258 | + | name = "ttf-parser" | |
| 6259 | + | version = "0.19.2" | |
| 6260 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 6261 | + | checksum = "49d64318d8311fc2668e48b63969f4343e0a85c4a109aa8460d6672e364b8bd1" | |
| 6262 | + | ||
| 6107 | 6263 | [[package]] | |
| 6108 | 6264 | name = "typeid" | |
| 6109 | 6265 | version = "1.0.3" | |
| @@ -6617,6 +6773,12 @@ | |||
| 6617 | 6773 | "windows-core 0.61.2", | |
| 6618 | 6774 | ] | |
| 6619 | 6775 | ||
| 6776 | + | [[package]] | |
| 6777 | + | name = "weezl" | |
| 6778 | + | version = "0.1.12" | |
| 6779 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 6780 | + | checksum = "a28ac98ddc8b9274cb41bb4d9d4d5c425b6020c50c46f25559911905610b4a88" | |
| 6781 | + | ||
| 6620 | 6782 | [[package]] | |
| 6621 | 6783 | name = "whoami" | |
| 6622 | 6784 | version = "1.6.1" |
| @@ -5,6 +5,7 @@ | |||
| 5 | 5 | "crates/bb-core", | |
| 6 | 6 | "crates/bb-feed", | |
| 7 | 7 | "crates/bb-db", | |
| 8 | + | "crates/bb-pdf", | |
| 8 | 9 | "src-tauri", | |
| 9 | 10 | ] | |
| 10 | 11 | default-members = ["src-tauri"] | |
| @@ -57,6 +58,10 @@ | |||
| 57 | 58 | bb-core = { path = "crates/bb-core" } | |
| 58 | 59 | bb-feed = { path = "crates/bb-feed" } | |
| 59 | 60 | bb-db = { path = "crates/bb-db" } | |
| 61 | + | bb-pdf = { path = "crates/bb-pdf" } | |
| 62 | + | ||
| 63 | + | # External sibling crates (shared under ~/Code/Libraries) | |
| 64 | + | supernote-push = { path = "../../Libraries/supernote-push" } | |
| 60 | 65 | synckit-client = { path = "../../MNW/shared/synckit-client" } | |
| 61 | 66 | tauri = "2.10.2" | |
| 62 | 67 | tauri-plugin-updater = "2" |
| @@ -20,6 +20,10 @@ | |||
| 20 | 20 | bb-core.workspace = true | |
| 21 | 21 | bb-feed.workspace = true | |
| 22 | 22 | bb-db.workspace = true | |
| 23 | + | bb-pdf.workspace = true | |
| 24 | + | ||
| 25 | + | # Supernote push (LAN e-ink upload) | |
| 26 | + | supernote-push.workspace = true | |
| 23 | 27 | ||
| 24 | 28 | # Tauri | |
| 25 | 29 | tauri = { version = "2.10.2", features = [] } |
| @@ -128,6 +128,7 @@ | |||
| 128 | 128 | commands::is_bookmarked, | |
| 129 | 129 | commands::get_bookmark_count, | |
| 130 | 130 | commands::export_bookmark_html, | |
| 131 | + | commands::push_item_to_supernote, | |
| 131 | 132 | ]) | |
| 132 | 133 | } | |
| 133 | 134 |
| @@ -69,6 +69,13 @@ | |||
| 69 | 69 | * @returns {Promise<number>} Count of items marked read. | |
| 70 | 70 | */ | |
| 71 | 71 | markAllRead: (sourceId) => invoke('mark_all_read', { sourceId: sourceId || null }), | |
| 72 | + | /** | |
| 73 | + | * Render the item as PDF and push it to the configured Supernote. | |
| 74 | + | * Fails if the item has no eligible body or Supernote is unconfigured. | |
| 75 | + | * @param {string} id - Item external_id. | |
| 76 | + | * @returns {Promise<void>} | |
| 77 | + | */ | |
| 78 | + | pushToSupernote: (id) => invoke('push_item_to_supernote', { id }), | |
| 72 | 79 | }, | |
| 73 | 80 | ||
| 74 | 81 | // --- Plugins: Rhai busser plugins --- |
| @@ -132,6 +132,14 @@ | |||
| 132 | 132 | actions.appendChild(openBtn); | |
| 133 | 133 | } | |
| 134 | 134 | ||
| 135 | + | if (item.supernoteEligible) { | |
| 136 | + | const snBtn = document.createElement('button'); | |
| 137 | + | snBtn.className = 'btn'; | |
| 138 | + | snBtn.textContent = 'Send to Supernote'; | |
| 139 | + | snBtn.addEventListener('click', pushToSupernote); | |
| 140 | + | actions.appendChild(snBtn); | |
| 141 | + | } | |
| 142 | + | ||
| 135 | 143 | // Plugin-declared custom action buttons | |
| 136 | 144 | if (item.actions && item.actions.length > 0) { | |
| 137 | 145 | for (const action of item.actions) { | |
| @@ -226,6 +234,22 @@ | |||
| 226 | 234 | } | |
| 227 | 235 | } | |
| 228 | 236 | ||
| 237 | + | /** | |
| 238 | + | * Push the current item to the configured Supernote as a PDF. | |
| 239 | + | * Surfaces backend errors (missing config, empty body, unreachable device) | |
| 240 | + | * via a toast. | |
| 241 | + | */ | |
| 242 | + | async function pushToSupernote() { | |
| 243 | + | if (!currentItem) return; | |
| 244 | + | BB.ui.showToast('Rendering and pushing to Supernote...'); | |
| 245 | + | try { | |
| 246 | + | await BB.api.items.pushToSupernote(currentItem.id); | |
| 247 | + | BB.ui.showToast('Sent to Supernote', 'success'); | |
| 248 | + | } catch (err) { | |
| 249 | + | BB.ui.showToast('Supernote push failed: ' + getErrorMessage(err), 'error'); | |
| 250 | + | } | |
| 251 | + | } | |
| 252 | + | ||
| 229 | 253 | /** | |
| 230 | 254 | * Open the current item's URL in the system browser via Tauri shell API. | |
| 231 | 255 | * No-op if no item is selected or the item has no URL. |
| @@ -99,6 +99,9 @@ | |||
| 99 | 99 | pub is_read: bool, | |
| 100 | 100 | /// Whether the user has starred/favourited this item. | |
| 101 | 101 | pub is_starred: bool, | |
| 102 | + | /// Whether the body is worth converting to PDF and pushing to a Supernote. | |
| 103 | + | /// Frontend uses this to gate the "Send to Supernote" button. | |
| 104 | + | pub supernote_eligible: bool, | |
| 102 | 105 | } | |
| 103 | 106 | ||
| 104 | 107 | /// Paginated list of item summaries. | |
| @@ -202,6 +205,7 @@ | |||
| 202 | 205 | }).collect(), | |
| 203 | 206 | is_read: item.is_read, | |
| 204 | 207 | is_starred: item.is_starred, | |
| 208 | + | supernote_eligible: crate::commands::supernote::is_eligible(item.body.as_deref()), | |
| 205 | 209 | } | |
| 206 | 210 | } | |
| 207 | 211 |
| @@ -8,6 +8,7 @@ | |||
| 8 | 8 | pub mod plugins; | |
| 9 | 9 | pub mod query_feeds; | |
| 10 | 10 | pub mod sources; | |
| 11 | + | pub mod supernote; | |
| 11 | 12 | pub mod sync; | |
| 12 | 13 | pub mod themes; | |
| 13 | 14 | ||
| @@ -19,5 +20,6 @@ | |||
| 19 | 20 | pub use plugins::*; | |
| 20 | 21 | pub use query_feeds::*; | |
| 21 | 22 | pub use sources::*; | |
| 23 | + | pub use supernote::*; | |
| 22 | 24 | pub use sync::*; | |
| 23 | 25 | pub use themes::*; |
| @@ -1,0 +1,12 @@ | |||
| 1 | + | [package] | |
| 2 | + | name = "bb-pdf" | |
| 3 | + | version.workspace = true | |
| 4 | + | edition.workspace = true | |
| 5 | + | license-file.workspace = true | |
| 6 | + | description = "Render Balanced Breakfast article HTML to a Supernote-friendly PDF." | |
| 7 | + | ||
| 8 | + | [dependencies] | |
| 9 | + | pter = { path = "../../../../Libraries/pter" } | |
| 10 | + | pulldown-cmark = { version = "0.12", default-features = false } | |
| 11 | + | printpdf = { version = "0.7" } | |
| 12 | + | thiserror = { workspace = true } |
| @@ -1,0 +1,18 @@ | |||
| 1 | + | //! Render an article HTML file to PDF on disk. | |
| 2 | + | //! | |
| 3 | + | //! cargo run --example render -- <input.html> <output.pdf> [title] | |
| 4 | + | ||
| 5 | + | use std::path::PathBuf; | |
| 6 | + | ||
| 7 | + | fn main() -> Result<(), Box<dyn std::error::Error>> { | |
| 8 | + | let mut args = std::env::args().skip(1); | |
| 9 | + | let input: PathBuf = args.next().ok_or("usage: render <input.html> <output.pdf> [title]")?.into(); | |
| 10 | + | let output: PathBuf = args.next().ok_or("missing <output.pdf>")?.into(); | |
| 11 | + | let title = args.next().unwrap_or_else(|| "Article".to_string()); | |
| 12 | + | ||
| 13 | + | let html = std::fs::read_to_string(&input)?; | |
| 14 | + | let bytes = bb_pdf::render_article(&html, &title, None)?; | |
| 15 | + | std::fs::write(&output, &bytes)?; | |
| 16 | + | println!("wrote {} bytes to {}", bytes.len(), output.display()); | |
| 17 | + | Ok(()) | |
| 18 | + | } |
| @@ -1,0 +1,76 @@ | |||
| 1 | + | //! Page geometry and text-width estimation. | |
| 2 | + | //! | |
| 3 | + | //! We use approximate Helvetica metrics rather than pulling real AFM tables | |
| 4 | + | //! or a font-shaping crate. The approximation slightly overcounts width so | |
| 5 | + | //! we err on the side of shorter lines rather than clipping the right edge. | |
| 6 | + | ||
| 7 | + | use printpdf::Mm; | |
| 8 | + | ||
| 9 | + | pub const PAGE_W: Mm = Mm(148.0); | |
| 10 | + | pub const PAGE_H: Mm = Mm(210.0); | |
| 11 | + | pub const MARGIN: Mm = Mm(14.0); | |
| 12 | + | ||
| 13 | + | pub const BODY_PT: f32 = 11.0; | |
| 14 | + | pub const H1_PT: f32 = 20.0; | |
| 15 | + | pub const H2_PT: f32 = 16.0; | |
| 16 | + | pub const H3_PT: f32 = 13.0; | |
| 17 | + | pub const MONO_PT: f32 = 10.0; | |
| 18 | + | ||
| 19 | + | pub const LINE_GAP: f32 = 1.35; | |
| 20 | + | pub const BLOCK_GAP_MM: f32 = 3.5; | |
| 21 | + | pub const HEADING_GAP_MM: f32 = 5.5; | |
| 22 | + | pub const LIST_INDENT_MM: f32 = 6.0; | |
| 23 | + | pub const QUOTE_INDENT_MM: f32 = 5.0; | |
| 24 | + | ||
| 25 | + | pub fn content_width_mm() -> f32 { | |
| 26 | + | PAGE_W.0 - MARGIN.0 * 2.0 | |
| 27 | + | } | |
| 28 | + | ||
| 29 | + | pub fn content_top_mm() -> f32 { | |
| 30 | + | PAGE_H.0 - MARGIN.0 | |
| 31 | + | } | |
| 32 | + | ||
| 33 | + | pub fn content_bottom_mm() -> f32 { | |
| 34 | + | MARGIN.0 | |
| 35 | + | } | |
| 36 | + | ||
| 37 | + | /// Approximate width in mm of `text` rendered in Helvetica at `pt`. | |
| 38 | + | /// | |
| 39 | + | /// Uses a per-family average advance width of 0.52em (bold slightly wider). | |
| 40 | + | /// 1pt = 25.4/72 mm. | |
| 41 | + | pub fn text_width_mm(text: &str, pt: f32, bold: bool, mono: bool) -> f32 { | |
| 42 | + | let avg_em = if mono { | |
| 43 | + | 0.60 | |
| 44 | + | } else if bold { | |
| 45 | + | 0.56 | |
| 46 | + | } else { | |
| 47 | + | 0.52 | |
| 48 | + | }; | |
| 49 | + | let mm_per_pt = 25.4 / 72.0; | |
| 50 | + | text.chars().count() as f32 * pt * avg_em * mm_per_pt | |
| 51 | + | } | |
| 52 | + | ||
| 53 | + | pub fn line_height_mm(pt: f32) -> f32 { | |
| 54 | + | pt * LINE_GAP * 25.4 / 72.0 | |
| 55 | + | } | |
| 56 | + | ||
| 57 | + | #[cfg(test)] | |
| 58 | + | mod tests { | |
| 59 | + | use super::*; | |
| 60 | + | ||
| 61 | + | #[test] | |
| 62 | + | fn width_grows_with_length_and_size() { | |
| 63 | + | let a = text_width_mm("hello", BODY_PT, false, false); | |
| 64 | + | let b = text_width_mm("hello world", BODY_PT, false, false); | |
| 65 | + | let c = text_width_mm("hello", H1_PT, false, false); | |
| 66 | + | assert!(b > a); | |
| 67 | + | assert!(c > a); | |
| 68 | + | } | |
| 69 | + | ||
| 70 | + | #[test] | |
| 71 | + | fn bold_wider_than_regular() { | |
| 72 | + | let r = text_width_mm("hello", BODY_PT, false, false); | |
| 73 | + | let b = text_width_mm("hello", BODY_PT, true, false); | |
| 74 | + | assert!(b > r); | |
| 75 | + | } | |
| 76 | + | } |