build: replace printpdf 0.7 with own PDF writer on lopdf 0.46
printpdf 0.7 pinned lopdf 0.31; newer printpdf versions are a complete rewrite. The export now uses src/pdf_writer.rs, a small writer on top of lopdf 0.46 with the subset of the printpdf API the export used: pages with layers (optional content groups), paths, embedded TrueType fonts (Type0, Identity-H, ToUnicode), images and bookmarks. Bookmark titles are written as UTF-16BE directly, so the post-processing step for them is gone. pdf_merge uses lopdf 0.46 as well. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
d610ca019a
commit
e47c2b6f28
11 files changed
+1564
-265
No files matched your search
Generated
+247
-124
@@ -9,7 +9,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||||||
checksum = "01c0457472c38ea5bd1c3b5ada5e368271cb550be7a4ca4a0b4634e9913f6cc2"
|
checksum = "01c0457472c38ea5bd1c3b5ada5e368271cb550be7a4ca4a0b4634e9913f6cc2"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"ab_glyph_rasterizer",
|
"ab_glyph_rasterizer",
|
||||||
"owned_ttf_parser 0.25.1",
|
"owned_ttf_parser",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -24,6 +24,17 @@ version = "2.0.1"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa"
|
checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "aes"
|
||||||
|
version = "0.9.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "35f0f96ce78e38c3dc6d8948aa8163d06385be74000f3c7a95bf1eef35d3ea32"
|
||||||
|
dependencies = [
|
||||||
|
"cipher",
|
||||||
|
"cpubits",
|
||||||
|
"cpufeatures 0.3.1",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "ahash"
|
name = "ahash"
|
||||||
version = "0.8.12"
|
version = "0.8.12"
|
||||||
@@ -219,6 +230,24 @@ dependencies = [
|
|||||||
"generic-array",
|
"generic-array",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "block-buffer"
|
||||||
|
version = "0.12.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "d2f6c7dbe95a6ed67ad9f18e57daf93a2f034c524b99fd2b76d18fdfeb6660aa"
|
||||||
|
dependencies = [
|
||||||
|
"hybrid-array",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "block-padding"
|
||||||
|
version = "0.4.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "710f1dd022ef4e93f8a438b4ba958de7f64308434fa6a87104481645cc30068b"
|
||||||
|
dependencies = [
|
||||||
|
"hybrid-array",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "block-sys"
|
name = "block-sys"
|
||||||
version = "0.2.1"
|
version = "0.2.1"
|
||||||
@@ -259,17 +288,6 @@ dependencies = [
|
|||||||
"alloc-stdlib",
|
"alloc-stdlib",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "bstr"
|
|
||||||
version = "1.13.1"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "6bb31b46c14244e20ee9984b11bf5c992b91fb6939fea616e3512c8baecdbe5f"
|
|
||||||
dependencies = [
|
|
||||||
"memchr",
|
|
||||||
"regex-automata",
|
|
||||||
"serde_core",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "bumpalo"
|
name = "bumpalo"
|
||||||
version = "3.20.3"
|
version = "3.20.3"
|
||||||
@@ -334,6 +352,15 @@ dependencies = [
|
|||||||
"wayland-client",
|
"wayland-client",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "cbc"
|
||||||
|
version = "0.2.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "ce2dc9ee5f88d11e0beb842c88b33c8a5cf0d1329c4b19494af42b07dbfe8896"
|
||||||
|
dependencies = [
|
||||||
|
"cipher",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "cc"
|
name = "cc"
|
||||||
version = "1.6.0"
|
version = "1.6.0"
|
||||||
@@ -370,6 +397,27 @@ version = "0.2.2"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "f079e83a288787bcd14a6aea84cee5c87a67c5a3e660c30f557a3d24761b3527"
|
checksum = "f079e83a288787bcd14a6aea84cee5c87a67c5a3e660c30f557a3d24761b3527"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "chacha20"
|
||||||
|
version = "0.10.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "65c35e4b699c7e15ccbe7ee35c005e4fc0a278d22238a2857e6ce2dadeda1b06"
|
||||||
|
dependencies = [
|
||||||
|
"cfg-if",
|
||||||
|
"cpufeatures 0.3.1",
|
||||||
|
"rand_core",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "cipher"
|
||||||
|
version = "0.5.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "e8cf2a2c93cd704877c0858356ed03480ff301ee950b43f1cbe4573b088bfa6c"
|
||||||
|
dependencies = [
|
||||||
|
"crypto-common 0.2.2",
|
||||||
|
"inout",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "clipboard-win"
|
name = "clipboard-win"
|
||||||
version = "5.4.1"
|
version = "5.4.1"
|
||||||
@@ -454,6 +502,12 @@ dependencies = [
|
|||||||
"crossbeam-utils",
|
"crossbeam-utils",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "const-oid"
|
||||||
|
version = "0.10.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "a6ef517f0926dd24a1582492c791b6a4818a4d94e789a334894aa15b0d12f55c"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "core-foundation"
|
name = "core-foundation"
|
||||||
version = "0.9.4"
|
version = "0.9.4"
|
||||||
@@ -500,6 +554,12 @@ version = "1.0.0"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "7f8f80099a98041a3d1622845c271458a2d73e688351bf3cb999266764b81d48"
|
checksum = "7f8f80099a98041a3d1622845c271458a2d73e688351bf3cb999266764b81d48"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "cpubits"
|
||||||
|
version = "0.1.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "15b85f9c39137c3a891689859392b1bd49812121d0d61c9caf00d46ed5ce06ae"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "cpufeatures"
|
name = "cpufeatures"
|
||||||
version = "0.2.17"
|
version = "0.2.17"
|
||||||
@@ -509,6 +569,15 @@ dependencies = [
|
|||||||
"libc",
|
"libc",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "cpufeatures"
|
||||||
|
version = "0.3.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "5ca28b0ae3115b884660db4118d803791fd6756b6e88f39c0f3f7859060d7566"
|
||||||
|
dependencies = [
|
||||||
|
"libc",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "crc32fast"
|
name = "crc32fast"
|
||||||
version = "1.5.2"
|
version = "1.5.2"
|
||||||
@@ -540,6 +609,15 @@ dependencies = [
|
|||||||
"typenum",
|
"typenum",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "crypto-common"
|
||||||
|
version = "0.2.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "ce6e4c961d6cd6c9a86db418387425e8bdeaf05b3c8bc1411e6dca4c252f1453"
|
||||||
|
dependencies = [
|
||||||
|
"hybrid-array",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "cursor-icon"
|
name = "cursor-icon"
|
||||||
version = "1.2.0"
|
version = "1.2.0"
|
||||||
@@ -563,20 +641,25 @@ version = "0.3.2"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "be1e0bca6c3637f992fc1cc7cbc52a78c1ef6db076dbf1059c4323d6a2048376"
|
checksum = "be1e0bca6c3637f992fc1cc7cbc52a78c1ef6db076dbf1059c4323d6a2048376"
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "deranged"
|
|
||||||
version = "0.5.8"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "7cd812cc2bc1d69d4764bd80df88b4317eaef9e773c75226407d9bc0876b211c"
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "digest"
|
name = "digest"
|
||||||
version = "0.10.7"
|
version = "0.10.7"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292"
|
checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"block-buffer",
|
"block-buffer 0.10.4",
|
||||||
"crypto-common",
|
"crypto-common 0.1.7",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "digest"
|
||||||
|
version = "0.11.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "f1dd6dbb5841937940781866fa1281a1ff7bd3bf827091440879f9994983d5c2"
|
||||||
|
dependencies = [
|
||||||
|
"block-buffer 0.12.1",
|
||||||
|
"const-oid",
|
||||||
|
"crypto-common 0.2.2",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -630,6 +713,15 @@ version = "1.2.1"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "75b325c5dbd37f80359721ad39aca5a29fb04c89279657cffdda8736d0c0b9d2"
|
checksum = "75b325c5dbd37f80359721ad39aca5a29fb04c89279657cffdda8736d0c0b9d2"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "ecb"
|
||||||
|
version = "0.2.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "26f2a8b3e564eba0877223dc343703ad0385794e882e6d13f3a4dd5c6b1f41ac"
|
||||||
|
dependencies = [
|
||||||
|
"cipher",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "ecolor"
|
name = "ecolor"
|
||||||
version = "0.27.2"
|
version = "0.27.2"
|
||||||
@@ -966,6 +1058,7 @@ dependencies = [
|
|||||||
"cfg-if",
|
"cfg-if",
|
||||||
"libc",
|
"libc",
|
||||||
"r-efi 6.0.0",
|
"r-efi 6.0.0",
|
||||||
|
"rand_core",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -975,7 +1068,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||||||
checksum = "ee8cfcc411d9adbbaba82fb72661cc1bcca13e8bba98b364e62b2dba8f960159"
|
checksum = "ee8cfcc411d9adbbaba82fb72661cc1bcca13e8bba98b364e62b2dba8f960159"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"color_quant",
|
"color_quant",
|
||||||
"weezl",
|
"weezl 0.1.12",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -1276,6 +1369,15 @@ dependencies = [
|
|||||||
"windows-sys 0.61.2",
|
"windows-sys 0.61.2",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "hybrid-array"
|
||||||
|
version = "0.4.15"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "27f864f10dfb56725ce5ce5472bc52252c8f93a4ab86327122cebf62c5f59a17"
|
||||||
|
dependencies = [
|
||||||
|
"typenum",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "icrate"
|
name = "icrate"
|
||||||
version = "0.0.4"
|
version = "0.0.4"
|
||||||
@@ -1455,17 +1557,17 @@ dependencies = [
|
|||||||
"hayro-svg",
|
"hayro-svg",
|
||||||
"image",
|
"image",
|
||||||
"log",
|
"log",
|
||||||
|
"lopdf",
|
||||||
"minisign-verify",
|
"minisign-verify",
|
||||||
"naga",
|
"naga",
|
||||||
"octotablet",
|
"octotablet",
|
||||||
"pollster",
|
"pollster",
|
||||||
"printpdf",
|
|
||||||
"quick-xml",
|
"quick-xml",
|
||||||
"resvg",
|
"resvg",
|
||||||
"semver",
|
"semver",
|
||||||
"serde",
|
"serde",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
"sha2",
|
"sha2 0.10.9",
|
||||||
"tempfile",
|
"tempfile",
|
||||||
"ureq",
|
"ureq",
|
||||||
"url",
|
"url",
|
||||||
@@ -1477,6 +1579,16 @@ dependencies = [
|
|||||||
"winit",
|
"winit",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "inout"
|
||||||
|
version = "0.2.5"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "3b5cf4372d022092674a83b8febf5865f7a573cc0c5baec5e49c879b6a20978f"
|
||||||
|
dependencies = [
|
||||||
|
"block-padding",
|
||||||
|
"hybrid-array",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "itoa"
|
name = "itoa"
|
||||||
version = "1.0.18"
|
version = "1.0.18"
|
||||||
@@ -1621,12 +1733,6 @@ version = "0.1.1"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "d4a5ff6bcca6c4867b1c4fd4ef63e4db7436ef363e0ad7531d1558856bae64f4"
|
checksum = "d4a5ff6bcca6c4867b1c4fd4ef63e4db7436ef363e0ad7531d1558856bae64f4"
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "linked-hash-map"
|
|
||||||
version = "0.5.6"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "0717cef1bc8b636c6e1c1bbdefc09e6322da8a9321966e8928ef80d20f7f770f"
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "linux-raw-sys"
|
name = "linux-raw-sys"
|
||||||
version = "0.4.15"
|
version = "0.4.15"
|
||||||
@@ -1668,19 +1774,29 @@ checksum = "f9f8bd3e56ce4dfc153cf470fffbfa98c7620958b312ca5c3a4b8d5181fd13c6"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "lopdf"
|
name = "lopdf"
|
||||||
version = "0.31.0"
|
version = "0.46.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "07c8e1b6184b1b32ea5f72f572ebdc40e5da1d2921fa469947ff7c480ad1f85a"
|
checksum = "d43f64cb8c0d673ff84245a6de9e0172c0cc05dc5dd9d263660b5b4a02b6c4ae"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
|
"aes",
|
||||||
|
"bitflags 2.13.2",
|
||||||
|
"brotli-decompressor",
|
||||||
|
"cbc",
|
||||||
|
"ecb",
|
||||||
"encoding_rs",
|
"encoding_rs",
|
||||||
"flate2",
|
"flate2",
|
||||||
|
"getrandom 0.4.3",
|
||||||
|
"indexmap",
|
||||||
"itoa",
|
"itoa",
|
||||||
"linked-hash-map",
|
|
||||||
"log",
|
"log",
|
||||||
"md5",
|
"md-5",
|
||||||
"pom",
|
"nom",
|
||||||
"time",
|
"rand",
|
||||||
"weezl",
|
"rangemap",
|
||||||
|
"sha2 0.11.1",
|
||||||
|
"stringprep",
|
||||||
|
"thiserror 2.0.21",
|
||||||
|
"weezl 0.2.1",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -1693,10 +1809,14 @@ dependencies = [
|
|||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "md5"
|
name = "md-5"
|
||||||
version = "0.7.0"
|
version = "0.11.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "490cc448043f947bae3cbee9c203358d62dbee0db12107a74be5c30ccfd09771"
|
checksum = "69b6441f590336821bb897fb28fc622898ccceb1d6cea3fde5ea86b090c4de98"
|
||||||
|
dependencies = [
|
||||||
|
"cfg-if",
|
||||||
|
"digest 0.11.3",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "memchr"
|
name = "memchr"
|
||||||
@@ -1827,10 +1947,13 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||||||
checksum = "2bf50223579dc7cdcfb3bfcacf7069ff68243f8c363f62ffa99cf000a6b9c451"
|
checksum = "2bf50223579dc7cdcfb3bfcacf7069ff68243f8c363f62ffa99cf000a6b9c451"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "num-conv"
|
name = "nom"
|
||||||
version = "0.2.2"
|
version = "8.0.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "521739c6d2bac4aa25192232afe6841231376b2b26d4d9fae5ecf8ca5772e441"
|
checksum = "df9761775871bdef83bee530e60050f7e54b1105350d6884eb0fb4f46c2f9405"
|
||||||
|
dependencies = [
|
||||||
|
"memchr",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "num-traits"
|
name = "num-traits"
|
||||||
@@ -2012,22 +2135,13 @@ dependencies = [
|
|||||||
"libredox",
|
"libredox",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "owned_ttf_parser"
|
|
||||||
version = "0.19.0"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "706de7e2214113d63a8238d1910463cfce781129a6f263d13fdb09ff64355ba4"
|
|
||||||
dependencies = [
|
|
||||||
"ttf-parser 0.19.2",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "owned_ttf_parser"
|
name = "owned_ttf_parser"
|
||||||
version = "0.25.1"
|
version = "0.25.1"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "36820e9051aca1014ddc75770aab4d68bc1e9e632f0f5627c4086bc216fb583b"
|
checksum = "36820e9051aca1014ddc75770aab4d68bc1e9e632f0f5627c4086bc216fb583b"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"ttf-parser 0.25.1",
|
"ttf-parser",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -2196,15 +2310,6 @@ dependencies = [
|
|||||||
"arrayvec",
|
"arrayvec",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "pom"
|
|
||||||
version = "3.4.0"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "6c972d8f86e943ad532d0b04e8965a749ad1d18bb981a9c7b3ae72fe7fd7744b"
|
|
||||||
dependencies = [
|
|
||||||
"bstr",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "potential_utf"
|
name = "potential_utf"
|
||||||
version = "0.1.6"
|
version = "0.1.6"
|
||||||
@@ -2214,30 +2319,12 @@ dependencies = [
|
|||||||
"zerovec",
|
"zerovec",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "powerfmt"
|
|
||||||
version = "0.2.1"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "4a6394b9e965e73d0a289ee54f589087e2c676aedf60885baf52c76b771e4958"
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "presser"
|
name = "presser"
|
||||||
version = "0.3.1"
|
version = "0.3.1"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "e8cf8e6a8aa66ce33f63993ffc4ea4271eb5b0530a9002db8455ea6050c77bfa"
|
checksum = "e8cf8e6a8aa66ce33f63993ffc4ea4271eb5b0530a9002db8455ea6050c77bfa"
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "printpdf"
|
|
||||||
version = "0.7.0"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "c30a4cc87c3ca9a98f4970db158a7153f8d1ec8076e005751173c57836380b1d"
|
|
||||||
dependencies = [
|
|
||||||
"js-sys",
|
|
||||||
"lopdf",
|
|
||||||
"owned_ttf_parser 0.19.0",
|
|
||||||
"time",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "proc-macro-crate"
|
name = "proc-macro-crate"
|
||||||
version = "3.5.0"
|
version = "3.5.0"
|
||||||
@@ -2304,12 +2391,35 @@ version = "6.0.0"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf"
|
checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "rand"
|
||||||
|
version = "0.10.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "65c9fb96cbc91e3478eaae79a69fcd3f1ae4ad052e471fe6732fff548984b4af"
|
||||||
|
dependencies = [
|
||||||
|
"chacha20",
|
||||||
|
"getrandom 0.4.3",
|
||||||
|
"rand_core",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "rand_core"
|
||||||
|
version = "0.10.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "63b8176103e19a2643978565ca18b50549f6101881c443590420e4dc998a3c69"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "range-alloc"
|
name = "range-alloc"
|
||||||
version = "0.1.5"
|
version = "0.1.5"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "ca45419789ae5a7899559e9512e58ca889e41f04f1f2445e9f4b290ceccd1d08"
|
checksum = "ca45419789ae5a7899559e9512e58ca889e41f04f1f2445e9f4b290ceccd1d08"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "rangemap"
|
||||||
|
version = "1.8.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "a611d15b50743feb4c76b7d03edcb0e64f399c26961e4efe6975bc398be6aa3d"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "raw-window-handle"
|
name = "raw-window-handle"
|
||||||
version = "0.5.2"
|
version = "0.5.2"
|
||||||
@@ -2371,12 +2481,6 @@ dependencies = [
|
|||||||
"bitflags 2.13.2",
|
"bitflags 2.13.2",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "regex-automata"
|
|
||||||
version = "0.4.18"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "ad8553b9b26413251cbf30e620595c7a41b3887f03da04579c0e6b0d6a06b4b2"
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "renderdoc-sys"
|
name = "renderdoc-sys"
|
||||||
version = "1.1.0"
|
version = "1.1.0"
|
||||||
@@ -2602,8 +2706,19 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||||||
checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283"
|
checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"cfg-if",
|
"cfg-if",
|
||||||
"cpufeatures",
|
"cpufeatures 0.2.17",
|
||||||
"digest",
|
"digest 0.10.7",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "sha2"
|
||||||
|
version = "0.11.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "47d7069beb7d6ac7b9acd1039986e73443f24234f41074da099d6f994ac9ad19"
|
||||||
|
dependencies = [
|
||||||
|
"cfg-if",
|
||||||
|
"cpufeatures 0.3.1",
|
||||||
|
"digest 0.11.3",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -2744,6 +2859,17 @@ dependencies = [
|
|||||||
"float-cmp",
|
"float-cmp",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "stringprep"
|
||||||
|
version = "0.1.5"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "7b4df3d392d81bd458a8a621b8bffbd2302a12ffe288a9d931670948749463b1"
|
||||||
|
dependencies = [
|
||||||
|
"unicode-bidi",
|
||||||
|
"unicode-normalization",
|
||||||
|
"unicode-properties",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "strum"
|
name = "strum"
|
||||||
version = "0.26.3"
|
version = "0.26.3"
|
||||||
@@ -2898,40 +3024,10 @@ dependencies = [
|
|||||||
"flate2",
|
"flate2",
|
||||||
"half",
|
"half",
|
||||||
"quick-error",
|
"quick-error",
|
||||||
"weezl",
|
"weezl 0.1.12",
|
||||||
"zune-jpeg",
|
"zune-jpeg",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "time"
|
|
||||||
version = "0.3.55"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "cdb87b95ec50ddfa440816d227a17b2ccbdda963a316a727fda0fc4334f7d134"
|
|
||||||
dependencies = [
|
|
||||||
"deranged",
|
|
||||||
"num-conv",
|
|
||||||
"powerfmt",
|
|
||||||
"serde_core",
|
|
||||||
"time-core",
|
|
||||||
"time-macros",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "time-core"
|
|
||||||
version = "0.1.9"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "9e1c906769ad99c88eaa54e728060edef082f8e358ff32030cb7c7d315e81109"
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "time-macros"
|
|
||||||
version = "0.2.32"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "7e689342a48d2ea927c87ea50cabf8594854bf940e9310208848d680d668ed85"
|
|
||||||
dependencies = [
|
|
||||||
"num-conv",
|
|
||||||
"time-core",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "tiny-skia"
|
name = "tiny-skia"
|
||||||
version = "0.11.4"
|
version = "0.11.4"
|
||||||
@@ -2993,6 +3089,12 @@ dependencies = [
|
|||||||
"zerovec",
|
"zerovec",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "tinyvec"
|
||||||
|
version = "1.13.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "fd3ca314f692efd6c868f8408f53fe444634a845f96c028b97d35f6a1f79f0ee"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "tokio"
|
name = "tokio"
|
||||||
version = "1.53.2"
|
version = "1.53.2"
|
||||||
@@ -3032,12 +3134,6 @@ dependencies = [
|
|||||||
"winnow",
|
"winnow",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "ttf-parser"
|
|
||||||
version = "0.19.2"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "49d64318d8311fc2668e48b63969f4343e0a85c4a109aa8460d6672e364b8bd1"
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "ttf-parser"
|
name = "ttf-parser"
|
||||||
version = "0.25.1"
|
version = "0.25.1"
|
||||||
@@ -3059,12 +3155,33 @@ version = "1.20.1"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "b6f5e870be6c3b371b77fe0ee0bafb859fa4964b4404c27de1d380043c4dda20"
|
checksum = "b6f5e870be6c3b371b77fe0ee0bafb859fa4964b4404c27de1d380043c4dda20"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "unicode-bidi"
|
||||||
|
version = "0.3.18"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "5c1cb5db39152898a79168971543b1cb5020dff7fe43c8dc468b0885f5e29df5"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "unicode-ident"
|
name = "unicode-ident"
|
||||||
version = "1.0.26"
|
version = "1.0.26"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "d245f478577f809a851594d02313b640fb437e0bb33866753cff937863096954"
|
checksum = "d245f478577f809a851594d02313b640fb437e0bb33866753cff937863096954"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "unicode-normalization"
|
||||||
|
version = "0.1.26"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "75cd3fb03d5288b3ba9a1ee103ac7e9cbb59f0b03d64f8ddfcc04dd42758a85d"
|
||||||
|
dependencies = [
|
||||||
|
"tinyvec",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "unicode-properties"
|
||||||
|
version = "0.1.4"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "7df058c713841ad818f1dc5d3fd88063241cc61f49f5fbea4b951e8cf5a8d71d"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "unicode-segmentation"
|
name = "unicode-segmentation"
|
||||||
version = "1.13.3"
|
version = "1.13.3"
|
||||||
@@ -3430,6 +3547,12 @@ version = "0.1.12"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "a28ac98ddc8b9274cb41bb4d9d4d5c425b6020c50c46f25559911905610b4a88"
|
checksum = "a28ac98ddc8b9274cb41bb4d9d4d5c425b6020c50c46f25559911905610b4a88"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "weezl"
|
||||||
|
version = "0.2.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "d4ca08e5ef825b65b056d9efbd95c8750683f0a6d0466d02e96dc2e4e360f3d2"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "wgpu"
|
name = "wgpu"
|
||||||
version = "0.19.4"
|
version = "0.19.4"
|
||||||
|
|||||||
+2
-2
@@ -24,8 +24,8 @@ glam = { version = "0.27", features = ["serde"] }
|
|||||||
# Asynchroner Starter für wgpu-Device-Setup
|
# Asynchroner Starter für wgpu-Device-Setup
|
||||||
pollster = "0.3"
|
pollster = "0.3"
|
||||||
|
|
||||||
# PDF-Vektorexport
|
# PDF: writing the export (own writer in src/pdf_writer.rs) and merging imported PDFs
|
||||||
printpdf = "0.7"
|
lopdf = { version = "0.46", default-features = false }
|
||||||
serde = { version = "1.0.229", features = ["derive"] }
|
serde = { version = "1.0.229", features = ["derive"] }
|
||||||
serde_json = "1.0.151"
|
serde_json = "1.0.151"
|
||||||
|
|
||||||
|
|||||||
+2
-1
@@ -179,6 +179,7 @@ src/
|
|||||||
├── assets.rs # Bilder und PDFs im Notizbuchordner
|
├── assets.rs # Bilder und PDFs im Notizbuchordner
|
||||||
├── pdf_doc.rs # PDF lesen und rendern (hayro)
|
├── pdf_doc.rs # PDF lesen und rendern (hayro)
|
||||||
├── pdf_merge.rs # Importierte PDF-Seiten beim Export unterlegen
|
├── pdf_merge.rs # Importierte PDF-Seiten beim Export unterlegen
|
||||||
|
├── pdf_writer.rs # Kleiner PDF-Schreiber für den Export (lopdf)
|
||||||
├── import.rs # Import als neues Notizbuch (Gemeinsames)
|
├── import.rs # Import als neues Notizbuch (Gemeinsames)
|
||||||
├── xopp.rs # Import von Xournal++-/Xournal-Notizbüchern
|
├── xopp.rs # Import von Xournal++-/Xournal-Notizbüchern
|
||||||
├── rnote.rs # Import von Rnote-Notizbüchern
|
├── rnote.rs # Import von Rnote-Notizbüchern
|
||||||
@@ -189,7 +190,7 @@ src/
|
|||||||
├── disk_watch.rs # Änderungen anderer Geräte übernehmen (Sync-Clients)
|
├── disk_watch.rs # Änderungen anderer Geräte übernehmen (Sync-Clients)
|
||||||
├── library.rs # Ordner der Notizbücher (library.json)
|
├── library.rs # Ordner der Notizbücher (library.json)
|
||||||
├── autosave.rs # Speichern im Hintergrund
|
├── autosave.rs # Speichern im Hintergrund
|
||||||
├── export.rs # PDF-Export (printpdf)
|
├── export.rs # PDF-Export
|
||||||
├── page_export.rs # Seite als PNG/SVG, Auswahl als Bild
|
├── page_export.rs # Seite als PNG/SVG, Auswahl als Bild
|
||||||
├── file_picker.rs # Eingebaute Dateiauswahl
|
├── file_picker.rs # Eingebaute Dateiauswahl
|
||||||
├── settings.rs # Einstellungen (~/.config/ink-notes/ui.json)
|
├── settings.rs # Einstellungen (~/.config/ink-notes/ui.json)
|
||||||
|
|||||||
@@ -181,6 +181,7 @@ src/
|
|||||||
├── assets.rs # Images and PDFs in the notebook folder
|
├── assets.rs # Images and PDFs in the notebook folder
|
||||||
├── pdf_doc.rs # Reading and rendering PDFs (hayro)
|
├── pdf_doc.rs # Reading and rendering PDFs (hayro)
|
||||||
├── pdf_merge.rs # Underlaying imported PDF pages on export
|
├── pdf_merge.rs # Underlaying imported PDF pages on export
|
||||||
|
├── pdf_writer.rs # Small PDF writer for the export (lopdf)
|
||||||
├── import.rs # Import as a new notebook (shared parts)
|
├── import.rs # Import as a new notebook (shared parts)
|
||||||
├── xopp.rs # Import of Xournal++/Xournal notebooks
|
├── xopp.rs # Import of Xournal++/Xournal notebooks
|
||||||
├── rnote.rs # Import of Rnote notebooks
|
├── rnote.rs # Import of Rnote notebooks
|
||||||
@@ -191,7 +192,7 @@ src/
|
|||||||
├── disk_watch.rs # Taking over changes from other devices (sync clients)
|
├── disk_watch.rs # Taking over changes from other devices (sync clients)
|
||||||
├── library.rs # Notebook folders (library.json)
|
├── library.rs # Notebook folders (library.json)
|
||||||
├── autosave.rs # Saving in the background
|
├── autosave.rs # Saving in the background
|
||||||
├── export.rs # PDF export (printpdf)
|
├── export.rs # PDF export
|
||||||
├── page_export.rs # Page as PNG/SVG, selection as image
|
├── page_export.rs # Page as PNG/SVG, selection as image
|
||||||
├── file_picker.rs # Built-in file picker
|
├── file_picker.rs # Built-in file picker
|
||||||
├── settings.rs # Settings (~/.config/ink-notes/ui.json)
|
├── settings.rs # Settings (~/.config/ink-notes/ui.json)
|
||||||
|
|||||||
@@ -189,13 +189,13 @@ Jede Seite hat einen eigenen Verlauf (`history.rs`): Hinzufügen, Entfernen, Ers
|
|||||||
|
|
||||||
## 8. Export
|
## 8. Export
|
||||||
|
|
||||||
`export.rs` erzeugt das PDF mit `printpdf`. Jede Seite bekommt ihre eigene Größe und diese Ebenen:
|
`export.rs` erzeugt das PDF mit `pdf_writer.rs`, einem kleinen Schreiber auf Basis von `lopdf` (Ebenen als optionale Inhaltsgruppen, eingebettete Schriften mit ToUnicode-Tabelle, Bilder, Lesezeichen). Jede Seite bekommt ihre eigene Größe und diese Ebenen:
|
||||||
|
|
||||||
1. **Background:** Karo, Linien oder Punkte in der gewählten Musterfarbe (immer auf weißem Papier).
|
1. **Background:** Karo, Linien oder Punkte in der gewählten Musterfarbe (immer auf weißem Papier).
|
||||||
2. **PDF** (nur im Notfall): importierte PDF-Seite als Bild, falls sie sich nicht als Vektor übernehmen lässt.
|
2. **PDF** (nur im Notfall): importierte PDF-Seite als Bild, falls sie sich nicht als Vektor übernehmen lässt.
|
||||||
3. **Images:** eingefügte Bilder (höchstens 300 dpi).
|
3. **Images:** eingefügte Bilder (höchstens 300 dpi).
|
||||||
4. **Text:** Textfelder als echter Text mit eingebetteter Schrift (durchsuchbar, kopierbar). Jedes Feld liegt in einem eigenen Koordinatensystem, das eine Transformationsmatrix auf seine Ecken abbildet.
|
4. **Text:** Textfelder als echter Text mit eingebetteter Schrift (durchsuchbar, kopierbar). Jedes Feld liegt in einem eigenen Koordinatensystem, das eine Transformationsmatrix auf seine Ecken abbildet.
|
||||||
5. **Ink:** Striche als gefüllte Polygone, Formen als Pfade. Halbtransparente Striche (Textmarker) bekommen einen Grafikzustand mit Deckkraft (`ca`/`CA`); printpdf kann solche Zustände nicht anlegen, deshalb trägt `add_alpha_states` sie nach dem Speichern mit `lopdf` in die Seitenressourcen ein. Jeder Strich ist ein einziger Pfad und deckt sich daher nicht selbst doppelt. Der Umriss eines Strichs wird vor dem Export vereinfacht (`geometry::simplify_closed_outline`, höchstens 0,01 mm vom gezeichneten entfernt): Das Netz hat etwa 16 Punkte je Stiftpunkt, und mit allen dauerte der Export einer vollen Seite Sekunden. `INK_NOTES_EXACT_OUTLINES=1` exportiert wie bisher jeden Punkt.
|
5. **Ink:** Striche als gefüllte Polygone, Formen als Pfade. Halbtransparente Striche (Textmarker) bekommen einen Grafikzustand mit Deckkraft (`ca`/`CA`); `add_alpha_states` trägt sie nach dem Speichern mit `lopdf` in die Seitenressourcen ein. Jeder Strich ist ein einziger Pfad und deckt sich daher nicht selbst doppelt. Der Umriss eines Strichs wird vor dem Export vereinfacht (`geometry::simplify_closed_outline`, höchstens 0,01 mm vom gezeichneten entfernt): Das Netz hat etwa 16 Punkte je Stiftpunkt, und mit allen dauerte der Export einer vollen Seite Sekunden. `INK_NOTES_EXACT_OUTLINES=1` exportiert wie bisher jeden Punkt.
|
||||||
|
|
||||||
Importierte PDF-Seiten werden anschließend als Form-XObject unter die Seite gelegt (`pdf_merge.rs`), bleiben also Vektorgrafik. Seitentitel werden zu Lesezeichen.
|
Importierte PDF-Seiten werden anschließend als Form-XObject unter die Seite gelegt (`pdf_merge.rs`), bleiben also Vektorgrafik. Seitentitel werden zu Lesezeichen.
|
||||||
|
|
||||||
|
|||||||
@@ -189,13 +189,13 @@ Each page has its own history (`history.rs`): adding, removing, replacing and mo
|
|||||||
|
|
||||||
## 8. Export
|
## 8. Export
|
||||||
|
|
||||||
`export.rs` creates the PDF with `printpdf`. Each page gets its own size and these layers:
|
`export.rs` creates the PDF with `pdf_writer.rs`, a small writer on top of `lopdf` (layers as optional content groups, embedded fonts with a ToUnicode map, images, bookmarks). Each page gets its own size and these layers:
|
||||||
|
|
||||||
1. **Background:** squares, lines or dots in the chosen pattern color (always on white paper).
|
1. **Background:** squares, lines or dots in the chosen pattern color (always on white paper).
|
||||||
2. **PDF** (fallback only): imported PDF page as an image, if it cannot be taken over as vectors.
|
2. **PDF** (fallback only): imported PDF page as an image, if it cannot be taken over as vectors.
|
||||||
3. **Images:** inserted images (at most 300 dpi).
|
3. **Images:** inserted images (at most 300 dpi).
|
||||||
4. **Text:** text boxes as real text with embedded font (searchable, copyable). Each box lives in its own coordinate system that a transformation matrix maps onto its corners.
|
4. **Text:** text boxes as real text with embedded font (searchable, copyable). Each box lives in its own coordinate system that a transformation matrix maps onto its corners.
|
||||||
5. **Ink:** strokes as filled polygons, shapes as paths. Semi-transparent strokes (highlighter) get a graphics state with opacity (`ca`/`CA`); printpdf cannot create such states, so `add_alpha_states` adds them to the page resources with `lopdf` after saving. Each stroke is a single path and therefore does not cover itself twice. The outline of a stroke is simplified before export (`geometry::simplify_closed_outline`, at most 0.01 mm away from the drawn one): the mesh has about 16 points per pen sample, and with all of them a full page took seconds to export. `INK_NOTES_EXACT_OUTLINES=1` exports every point as before.
|
5. **Ink:** strokes as filled polygons, shapes as paths. Semi-transparent strokes (highlighter) get a graphics state with opacity (`ca`/`CA`); `add_alpha_states` adds them to the page resources with `lopdf` after saving. Each stroke is a single path and therefore does not cover itself twice. The outline of a stroke is simplified before export (`geometry::simplify_closed_outline`, at most 0.01 mm away from the drawn one): the mesh has about 16 points per pen sample, and with all of them a full page took seconds to export. `INK_NOTES_EXACT_OUTLINES=1` exports every point as before.
|
||||||
|
|
||||||
Imported PDF pages are then placed under the page as a form XObject (`pdf_merge.rs`), so they stay vector graphics. Page titles become bookmarks.
|
Imported PDF pages are then placed under the page as a form XObject (`pdf_merge.rs`), so they stay vector graphics. Page titles become bookmarks.
|
||||||
|
|
||||||
|
|||||||
@@ -38,6 +38,19 @@
|
|||||||
"dest": "cargo/vendor/adler2-2.0.1",
|
"dest": "cargo/vendor/adler2-2.0.1",
|
||||||
"dest-filename": ".cargo-checksum.json"
|
"dest-filename": ".cargo-checksum.json"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"type": "archive",
|
||||||
|
"archive-type": "tar-gzip",
|
||||||
|
"url": "https://static.crates.io/crates/aes/aes-0.9.3.crate",
|
||||||
|
"sha256": "35f0f96ce78e38c3dc6d8948aa8163d06385be74000f3c7a95bf1eef35d3ea32",
|
||||||
|
"dest": "cargo/vendor/aes-0.9.3"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "inline",
|
||||||
|
"contents": "{\"package\": \"35f0f96ce78e38c3dc6d8948aa8163d06385be74000f3c7a95bf1eef35d3ea32\", \"files\": {}}",
|
||||||
|
"dest": "cargo/vendor/aes-0.9.3",
|
||||||
|
"dest-filename": ".cargo-checksum.json"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"type": "archive",
|
"type": "archive",
|
||||||
"archive-type": "tar-gzip",
|
"archive-type": "tar-gzip",
|
||||||
@@ -337,6 +350,32 @@
|
|||||||
"dest": "cargo/vendor/block-buffer-0.10.4",
|
"dest": "cargo/vendor/block-buffer-0.10.4",
|
||||||
"dest-filename": ".cargo-checksum.json"
|
"dest-filename": ".cargo-checksum.json"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"type": "archive",
|
||||||
|
"archive-type": "tar-gzip",
|
||||||
|
"url": "https://static.crates.io/crates/block-buffer/block-buffer-0.12.1.crate",
|
||||||
|
"sha256": "d2f6c7dbe95a6ed67ad9f18e57daf93a2f034c524b99fd2b76d18fdfeb6660aa",
|
||||||
|
"dest": "cargo/vendor/block-buffer-0.12.1"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "inline",
|
||||||
|
"contents": "{\"package\": \"d2f6c7dbe95a6ed67ad9f18e57daf93a2f034c524b99fd2b76d18fdfeb6660aa\", \"files\": {}}",
|
||||||
|
"dest": "cargo/vendor/block-buffer-0.12.1",
|
||||||
|
"dest-filename": ".cargo-checksum.json"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "archive",
|
||||||
|
"archive-type": "tar-gzip",
|
||||||
|
"url": "https://static.crates.io/crates/block-padding/block-padding-0.4.2.crate",
|
||||||
|
"sha256": "710f1dd022ef4e93f8a438b4ba958de7f64308434fa6a87104481645cc30068b",
|
||||||
|
"dest": "cargo/vendor/block-padding-0.4.2"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "inline",
|
||||||
|
"contents": "{\"package\": \"710f1dd022ef4e93f8a438b4ba958de7f64308434fa6a87104481645cc30068b\", \"files\": {}}",
|
||||||
|
"dest": "cargo/vendor/block-padding-0.4.2",
|
||||||
|
"dest-filename": ".cargo-checksum.json"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"type": "archive",
|
"type": "archive",
|
||||||
"archive-type": "tar-gzip",
|
"archive-type": "tar-gzip",
|
||||||
@@ -493,6 +532,19 @@
|
|||||||
"dest": "cargo/vendor/calloop-wayland-source-0.2.0",
|
"dest": "cargo/vendor/calloop-wayland-source-0.2.0",
|
||||||
"dest-filename": ".cargo-checksum.json"
|
"dest-filename": ".cargo-checksum.json"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"type": "archive",
|
||||||
|
"archive-type": "tar-gzip",
|
||||||
|
"url": "https://static.crates.io/crates/cbc/cbc-0.2.1.crate",
|
||||||
|
"sha256": "ce2dc9ee5f88d11e0beb842c88b33c8a5cf0d1329c4b19494af42b07dbfe8896",
|
||||||
|
"dest": "cargo/vendor/cbc-0.2.1"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "inline",
|
||||||
|
"contents": "{\"package\": \"ce2dc9ee5f88d11e0beb842c88b33c8a5cf0d1329c4b19494af42b07dbfe8896\", \"files\": {}}",
|
||||||
|
"dest": "cargo/vendor/cbc-0.2.1",
|
||||||
|
"dest-filename": ".cargo-checksum.json"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"type": "archive",
|
"type": "archive",
|
||||||
"archive-type": "tar-gzip",
|
"archive-type": "tar-gzip",
|
||||||
@@ -558,6 +610,32 @@
|
|||||||
"dest": "cargo/vendor/cfg_aliases-0.2.2",
|
"dest": "cargo/vendor/cfg_aliases-0.2.2",
|
||||||
"dest-filename": ".cargo-checksum.json"
|
"dest-filename": ".cargo-checksum.json"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"type": "archive",
|
||||||
|
"archive-type": "tar-gzip",
|
||||||
|
"url": "https://static.crates.io/crates/chacha20/chacha20-0.10.2.crate",
|
||||||
|
"sha256": "65c35e4b699c7e15ccbe7ee35c005e4fc0a278d22238a2857e6ce2dadeda1b06",
|
||||||
|
"dest": "cargo/vendor/chacha20-0.10.2"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "inline",
|
||||||
|
"contents": "{\"package\": \"65c35e4b699c7e15ccbe7ee35c005e4fc0a278d22238a2857e6ce2dadeda1b06\", \"files\": {}}",
|
||||||
|
"dest": "cargo/vendor/chacha20-0.10.2",
|
||||||
|
"dest-filename": ".cargo-checksum.json"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "archive",
|
||||||
|
"archive-type": "tar-gzip",
|
||||||
|
"url": "https://static.crates.io/crates/cipher/cipher-0.5.2.crate",
|
||||||
|
"sha256": "e8cf2a2c93cd704877c0858356ed03480ff301ee950b43f1cbe4573b088bfa6c",
|
||||||
|
"dest": "cargo/vendor/cipher-0.5.2"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "inline",
|
||||||
|
"contents": "{\"package\": \"e8cf2a2c93cd704877c0858356ed03480ff301ee950b43f1cbe4573b088bfa6c\", \"files\": {}}",
|
||||||
|
"dest": "cargo/vendor/cipher-0.5.2",
|
||||||
|
"dest-filename": ".cargo-checksum.json"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"type": "archive",
|
"type": "archive",
|
||||||
"archive-type": "tar-gzip",
|
"archive-type": "tar-gzip",
|
||||||
@@ -675,6 +753,19 @@
|
|||||||
"dest": "cargo/vendor/concurrent-queue-2.5.0",
|
"dest": "cargo/vendor/concurrent-queue-2.5.0",
|
||||||
"dest-filename": ".cargo-checksum.json"
|
"dest-filename": ".cargo-checksum.json"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"type": "archive",
|
||||||
|
"archive-type": "tar-gzip",
|
||||||
|
"url": "https://static.crates.io/crates/const-oid/const-oid-0.10.2.crate",
|
||||||
|
"sha256": "a6ef517f0926dd24a1582492c791b6a4818a4d94e789a334894aa15b0d12f55c",
|
||||||
|
"dest": "cargo/vendor/const-oid-0.10.2"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "inline",
|
||||||
|
"contents": "{\"package\": \"a6ef517f0926dd24a1582492c791b6a4818a4d94e789a334894aa15b0d12f55c\", \"files\": {}}",
|
||||||
|
"dest": "cargo/vendor/const-oid-0.10.2",
|
||||||
|
"dest-filename": ".cargo-checksum.json"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"type": "archive",
|
"type": "archive",
|
||||||
"archive-type": "tar-gzip",
|
"archive-type": "tar-gzip",
|
||||||
@@ -740,6 +831,19 @@
|
|||||||
"dest": "cargo/vendor/core_detect-1.0.0",
|
"dest": "cargo/vendor/core_detect-1.0.0",
|
||||||
"dest-filename": ".cargo-checksum.json"
|
"dest-filename": ".cargo-checksum.json"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"type": "archive",
|
||||||
|
"archive-type": "tar-gzip",
|
||||||
|
"url": "https://static.crates.io/crates/cpubits/cpubits-0.1.1.crate",
|
||||||
|
"sha256": "15b85f9c39137c3a891689859392b1bd49812121d0d61c9caf00d46ed5ce06ae",
|
||||||
|
"dest": "cargo/vendor/cpubits-0.1.1"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "inline",
|
||||||
|
"contents": "{\"package\": \"15b85f9c39137c3a891689859392b1bd49812121d0d61c9caf00d46ed5ce06ae\", \"files\": {}}",
|
||||||
|
"dest": "cargo/vendor/cpubits-0.1.1",
|
||||||
|
"dest-filename": ".cargo-checksum.json"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"type": "archive",
|
"type": "archive",
|
||||||
"archive-type": "tar-gzip",
|
"archive-type": "tar-gzip",
|
||||||
@@ -753,6 +857,19 @@
|
|||||||
"dest": "cargo/vendor/cpufeatures-0.2.17",
|
"dest": "cargo/vendor/cpufeatures-0.2.17",
|
||||||
"dest-filename": ".cargo-checksum.json"
|
"dest-filename": ".cargo-checksum.json"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"type": "archive",
|
||||||
|
"archive-type": "tar-gzip",
|
||||||
|
"url": "https://static.crates.io/crates/cpufeatures/cpufeatures-0.3.1.crate",
|
||||||
|
"sha256": "5ca28b0ae3115b884660db4118d803791fd6756b6e88f39c0f3f7859060d7566",
|
||||||
|
"dest": "cargo/vendor/cpufeatures-0.3.1"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "inline",
|
||||||
|
"contents": "{\"package\": \"5ca28b0ae3115b884660db4118d803791fd6756b6e88f39c0f3f7859060d7566\", \"files\": {}}",
|
||||||
|
"dest": "cargo/vendor/cpufeatures-0.3.1",
|
||||||
|
"dest-filename": ".cargo-checksum.json"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"type": "archive",
|
"type": "archive",
|
||||||
"archive-type": "tar-gzip",
|
"archive-type": "tar-gzip",
|
||||||
@@ -805,6 +922,19 @@
|
|||||||
"dest": "cargo/vendor/crypto-common-0.1.7",
|
"dest": "cargo/vendor/crypto-common-0.1.7",
|
||||||
"dest-filename": ".cargo-checksum.json"
|
"dest-filename": ".cargo-checksum.json"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"type": "archive",
|
||||||
|
"archive-type": "tar-gzip",
|
||||||
|
"url": "https://static.crates.io/crates/crypto-common/crypto-common-0.2.2.crate",
|
||||||
|
"sha256": "ce6e4c961d6cd6c9a86db418387425e8bdeaf05b3c8bc1411e6dca4c252f1453",
|
||||||
|
"dest": "cargo/vendor/crypto-common-0.2.2"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "inline",
|
||||||
|
"contents": "{\"package\": \"ce6e4c961d6cd6c9a86db418387425e8bdeaf05b3c8bc1411e6dca4c252f1453\", \"files\": {}}",
|
||||||
|
"dest": "cargo/vendor/crypto-common-0.2.2",
|
||||||
|
"dest-filename": ".cargo-checksum.json"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"type": "archive",
|
"type": "archive",
|
||||||
"archive-type": "tar-gzip",
|
"archive-type": "tar-gzip",
|
||||||
@@ -870,6 +1000,19 @@
|
|||||||
"dest": "cargo/vendor/digest-0.10.7",
|
"dest": "cargo/vendor/digest-0.10.7",
|
||||||
"dest-filename": ".cargo-checksum.json"
|
"dest-filename": ".cargo-checksum.json"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"type": "archive",
|
||||||
|
"archive-type": "tar-gzip",
|
||||||
|
"url": "https://static.crates.io/crates/digest/digest-0.11.3.crate",
|
||||||
|
"sha256": "f1dd6dbb5841937940781866fa1281a1ff7bd3bf827091440879f9994983d5c2",
|
||||||
|
"dest": "cargo/vendor/digest-0.11.3"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "inline",
|
||||||
|
"contents": "{\"package\": \"f1dd6dbb5841937940781866fa1281a1ff7bd3bf827091440879f9994983d5c2\", \"files\": {}}",
|
||||||
|
"dest": "cargo/vendor/digest-0.11.3",
|
||||||
|
"dest-filename": ".cargo-checksum.json"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"type": "archive",
|
"type": "archive",
|
||||||
"archive-type": "tar-gzip",
|
"archive-type": "tar-gzip",
|
||||||
@@ -948,6 +1091,19 @@
|
|||||||
"dest": "cargo/vendor/downcast-rs-1.2.1",
|
"dest": "cargo/vendor/downcast-rs-1.2.1",
|
||||||
"dest-filename": ".cargo-checksum.json"
|
"dest-filename": ".cargo-checksum.json"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"type": "archive",
|
||||||
|
"archive-type": "tar-gzip",
|
||||||
|
"url": "https://static.crates.io/crates/ecb/ecb-0.2.1.crate",
|
||||||
|
"sha256": "26f2a8b3e564eba0877223dc343703ad0385794e882e6d13f3a4dd5c6b1f41ac",
|
||||||
|
"dest": "cargo/vendor/ecb-0.2.1"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "inline",
|
||||||
|
"contents": "{\"package\": \"26f2a8b3e564eba0877223dc343703ad0385794e882e6d13f3a4dd5c6b1f41ac\", \"files\": {}}",
|
||||||
|
"dest": "cargo/vendor/ecb-0.2.1",
|
||||||
|
"dest-filename": ".cargo-checksum.json"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"type": "archive",
|
"type": "archive",
|
||||||
"archive-type": "tar-gzip",
|
"archive-type": "tar-gzip",
|
||||||
@@ -1793,6 +1949,19 @@
|
|||||||
"dest": "cargo/vendor/home-0.5.12",
|
"dest": "cargo/vendor/home-0.5.12",
|
||||||
"dest-filename": ".cargo-checksum.json"
|
"dest-filename": ".cargo-checksum.json"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"type": "archive",
|
||||||
|
"archive-type": "tar-gzip",
|
||||||
|
"url": "https://static.crates.io/crates/hybrid-array/hybrid-array-0.4.15.crate",
|
||||||
|
"sha256": "27f864f10dfb56725ce5ce5472bc52252c8f93a4ab86327122cebf62c5f59a17",
|
||||||
|
"dest": "cargo/vendor/hybrid-array-0.4.15"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "inline",
|
||||||
|
"contents": "{\"package\": \"27f864f10dfb56725ce5ce5472bc52252c8f93a4ab86327122cebf62c5f59a17\", \"files\": {}}",
|
||||||
|
"dest": "cargo/vendor/hybrid-array-0.4.15",
|
||||||
|
"dest-filename": ".cargo-checksum.json"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"type": "archive",
|
"type": "archive",
|
||||||
"archive-type": "tar-gzip",
|
"archive-type": "tar-gzip",
|
||||||
@@ -1975,6 +2144,19 @@
|
|||||||
"dest": "cargo/vendor/indexmap-2.14.2",
|
"dest": "cargo/vendor/indexmap-2.14.2",
|
||||||
"dest-filename": ".cargo-checksum.json"
|
"dest-filename": ".cargo-checksum.json"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"type": "archive",
|
||||||
|
"archive-type": "tar-gzip",
|
||||||
|
"url": "https://static.crates.io/crates/inout/inout-0.2.5.crate",
|
||||||
|
"sha256": "3b5cf4372d022092674a83b8febf5865f7a573cc0c5baec5e49c879b6a20978f",
|
||||||
|
"dest": "cargo/vendor/inout-0.2.5"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "inline",
|
||||||
|
"contents": "{\"package\": \"3b5cf4372d022092674a83b8febf5865f7a573cc0c5baec5e49c879b6a20978f\", \"files\": {}}",
|
||||||
|
"dest": "cargo/vendor/inout-0.2.5",
|
||||||
|
"dest-filename": ".cargo-checksum.json"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"type": "archive",
|
"type": "archive",
|
||||||
"archive-type": "tar-gzip",
|
"archive-type": "tar-gzip",
|
||||||
@@ -2261,6 +2443,19 @@
|
|||||||
"dest": "cargo/vendor/log-0.4.34",
|
"dest": "cargo/vendor/log-0.4.34",
|
||||||
"dest-filename": ".cargo-checksum.json"
|
"dest-filename": ".cargo-checksum.json"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"type": "archive",
|
||||||
|
"archive-type": "tar-gzip",
|
||||||
|
"url": "https://static.crates.io/crates/lopdf/lopdf-0.46.0.crate",
|
||||||
|
"sha256": "d43f64cb8c0d673ff84245a6de9e0172c0cc05dc5dd9d263660b5b4a02b6c4ae",
|
||||||
|
"dest": "cargo/vendor/lopdf-0.46.0"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "inline",
|
||||||
|
"contents": "{\"package\": \"d43f64cb8c0d673ff84245a6de9e0172c0cc05dc5dd9d263660b5b4a02b6c4ae\", \"files\": {}}",
|
||||||
|
"dest": "cargo/vendor/lopdf-0.46.0",
|
||||||
|
"dest-filename": ".cargo-checksum.json"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"type": "archive",
|
"type": "archive",
|
||||||
"archive-type": "tar-gzip",
|
"archive-type": "tar-gzip",
|
||||||
@@ -2287,6 +2482,19 @@
|
|||||||
"dest": "cargo/vendor/malloc_buf-0.0.6",
|
"dest": "cargo/vendor/malloc_buf-0.0.6",
|
||||||
"dest-filename": ".cargo-checksum.json"
|
"dest-filename": ".cargo-checksum.json"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"type": "archive",
|
||||||
|
"archive-type": "tar-gzip",
|
||||||
|
"url": "https://static.crates.io/crates/md-5/md-5-0.11.0.crate",
|
||||||
|
"sha256": "69b6441f590336821bb897fb28fc622898ccceb1d6cea3fde5ea86b090c4de98",
|
||||||
|
"dest": "cargo/vendor/md-5-0.11.0"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "inline",
|
||||||
|
"contents": "{\"package\": \"69b6441f590336821bb897fb28fc622898ccceb1d6cea3fde5ea86b090c4de98\", \"files\": {}}",
|
||||||
|
"dest": "cargo/vendor/md-5-0.11.0",
|
||||||
|
"dest-filename": ".cargo-checksum.json"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"type": "archive",
|
"type": "archive",
|
||||||
"archive-type": "tar-gzip",
|
"archive-type": "tar-gzip",
|
||||||
@@ -2469,6 +2677,19 @@
|
|||||||
"dest": "cargo/vendor/nohash-hasher-0.2.0",
|
"dest": "cargo/vendor/nohash-hasher-0.2.0",
|
||||||
"dest-filename": ".cargo-checksum.json"
|
"dest-filename": ".cargo-checksum.json"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"type": "archive",
|
||||||
|
"archive-type": "tar-gzip",
|
||||||
|
"url": "https://static.crates.io/crates/nom/nom-8.0.0.crate",
|
||||||
|
"sha256": "df9761775871bdef83bee530e60050f7e54b1105350d6884eb0fb4f46c2f9405",
|
||||||
|
"dest": "cargo/vendor/nom-8.0.0"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "inline",
|
||||||
|
"contents": "{\"package\": \"df9761775871bdef83bee530e60050f7e54b1105350d6884eb0fb4f46c2f9405\", \"files\": {}}",
|
||||||
|
"dest": "cargo/vendor/nom-8.0.0",
|
||||||
|
"dest-filename": ".cargo-checksum.json"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"type": "archive",
|
"type": "archive",
|
||||||
"archive-type": "tar-gzip",
|
"archive-type": "tar-gzip",
|
||||||
@@ -3158,6 +3379,32 @@
|
|||||||
"dest": "cargo/vendor/r-efi-6.0.0",
|
"dest": "cargo/vendor/r-efi-6.0.0",
|
||||||
"dest-filename": ".cargo-checksum.json"
|
"dest-filename": ".cargo-checksum.json"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"type": "archive",
|
||||||
|
"archive-type": "tar-gzip",
|
||||||
|
"url": "https://static.crates.io/crates/rand/rand-0.10.3.crate",
|
||||||
|
"sha256": "65c9fb96cbc91e3478eaae79a69fcd3f1ae4ad052e471fe6732fff548984b4af",
|
||||||
|
"dest": "cargo/vendor/rand-0.10.3"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "inline",
|
||||||
|
"contents": "{\"package\": \"65c9fb96cbc91e3478eaae79a69fcd3f1ae4ad052e471fe6732fff548984b4af\", \"files\": {}}",
|
||||||
|
"dest": "cargo/vendor/rand-0.10.3",
|
||||||
|
"dest-filename": ".cargo-checksum.json"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "archive",
|
||||||
|
"archive-type": "tar-gzip",
|
||||||
|
"url": "https://static.crates.io/crates/rand_core/rand_core-0.10.1.crate",
|
||||||
|
"sha256": "63b8176103e19a2643978565ca18b50549f6101881c443590420e4dc998a3c69",
|
||||||
|
"dest": "cargo/vendor/rand_core-0.10.1"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "inline",
|
||||||
|
"contents": "{\"package\": \"63b8176103e19a2643978565ca18b50549f6101881c443590420e4dc998a3c69\", \"files\": {}}",
|
||||||
|
"dest": "cargo/vendor/rand_core-0.10.1",
|
||||||
|
"dest-filename": ".cargo-checksum.json"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"type": "archive",
|
"type": "archive",
|
||||||
"archive-type": "tar-gzip",
|
"archive-type": "tar-gzip",
|
||||||
@@ -3171,6 +3418,19 @@
|
|||||||
"dest": "cargo/vendor/range-alloc-0.1.5",
|
"dest": "cargo/vendor/range-alloc-0.1.5",
|
||||||
"dest-filename": ".cargo-checksum.json"
|
"dest-filename": ".cargo-checksum.json"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"type": "archive",
|
||||||
|
"archive-type": "tar-gzip",
|
||||||
|
"url": "https://static.crates.io/crates/rangemap/rangemap-1.8.0.crate",
|
||||||
|
"sha256": "a611d15b50743feb4c76b7d03edcb0e64f399c26961e4efe6975bc398be6aa3d",
|
||||||
|
"dest": "cargo/vendor/rangemap-1.8.0"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "inline",
|
||||||
|
"contents": "{\"package\": \"a611d15b50743feb4c76b7d03edcb0e64f399c26961e4efe6975bc398be6aa3d\", \"files\": {}}",
|
||||||
|
"dest": "cargo/vendor/rangemap-1.8.0",
|
||||||
|
"dest-filename": ".cargo-checksum.json"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"type": "archive",
|
"type": "archive",
|
||||||
"archive-type": "tar-gzip",
|
"archive-type": "tar-gzip",
|
||||||
@@ -3574,6 +3834,19 @@
|
|||||||
"dest": "cargo/vendor/sha2-0.10.9",
|
"dest": "cargo/vendor/sha2-0.10.9",
|
||||||
"dest-filename": ".cargo-checksum.json"
|
"dest-filename": ".cargo-checksum.json"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"type": "archive",
|
||||||
|
"archive-type": "tar-gzip",
|
||||||
|
"url": "https://static.crates.io/crates/sha2/sha2-0.11.1.crate",
|
||||||
|
"sha256": "47d7069beb7d6ac7b9acd1039986e73443f24234f41074da099d6f994ac9ad19",
|
||||||
|
"dest": "cargo/vendor/sha2-0.11.1"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "inline",
|
||||||
|
"contents": "{\"package\": \"47d7069beb7d6ac7b9acd1039986e73443f24234f41074da099d6f994ac9ad19\", \"files\": {}}",
|
||||||
|
"dest": "cargo/vendor/sha2-0.11.1",
|
||||||
|
"dest-filename": ".cargo-checksum.json"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"type": "archive",
|
"type": "archive",
|
||||||
"archive-type": "tar-gzip",
|
"archive-type": "tar-gzip",
|
||||||
@@ -3782,6 +4055,19 @@
|
|||||||
"dest": "cargo/vendor/strict-num-0.1.1",
|
"dest": "cargo/vendor/strict-num-0.1.1",
|
||||||
"dest-filename": ".cargo-checksum.json"
|
"dest-filename": ".cargo-checksum.json"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"type": "archive",
|
||||||
|
"archive-type": "tar-gzip",
|
||||||
|
"url": "https://static.crates.io/crates/stringprep/stringprep-0.1.5.crate",
|
||||||
|
"sha256": "7b4df3d392d81bd458a8a621b8bffbd2302a12ffe288a9d931670948749463b1",
|
||||||
|
"dest": "cargo/vendor/stringprep-0.1.5"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "inline",
|
||||||
|
"contents": "{\"package\": \"7b4df3d392d81bd458a8a621b8bffbd2302a12ffe288a9d931670948749463b1\", \"files\": {}}",
|
||||||
|
"dest": "cargo/vendor/stringprep-0.1.5",
|
||||||
|
"dest-filename": ".cargo-checksum.json"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"type": "archive",
|
"type": "archive",
|
||||||
"archive-type": "tar-gzip",
|
"archive-type": "tar-gzip",
|
||||||
@@ -4081,6 +4367,19 @@
|
|||||||
"dest": "cargo/vendor/tinystr-0.8.4",
|
"dest": "cargo/vendor/tinystr-0.8.4",
|
||||||
"dest-filename": ".cargo-checksum.json"
|
"dest-filename": ".cargo-checksum.json"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"type": "archive",
|
||||||
|
"archive-type": "tar-gzip",
|
||||||
|
"url": "https://static.crates.io/crates/tinyvec/tinyvec-1.13.3.crate",
|
||||||
|
"sha256": "fd3ca314f692efd6c868f8408f53fe444634a845f96c028b97d35f6a1f79f0ee",
|
||||||
|
"dest": "cargo/vendor/tinyvec-1.13.3"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "inline",
|
||||||
|
"contents": "{\"package\": \"fd3ca314f692efd6c868f8408f53fe444634a845f96c028b97d35f6a1f79f0ee\", \"files\": {}}",
|
||||||
|
"dest": "cargo/vendor/tinyvec-1.13.3",
|
||||||
|
"dest-filename": ".cargo-checksum.json"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"type": "archive",
|
"type": "archive",
|
||||||
"archive-type": "tar-gzip",
|
"archive-type": "tar-gzip",
|
||||||
@@ -4185,6 +4484,19 @@
|
|||||||
"dest": "cargo/vendor/typenum-1.20.1",
|
"dest": "cargo/vendor/typenum-1.20.1",
|
||||||
"dest-filename": ".cargo-checksum.json"
|
"dest-filename": ".cargo-checksum.json"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"type": "archive",
|
||||||
|
"archive-type": "tar-gzip",
|
||||||
|
"url": "https://static.crates.io/crates/unicode-bidi/unicode-bidi-0.3.18.crate",
|
||||||
|
"sha256": "5c1cb5db39152898a79168971543b1cb5020dff7fe43c8dc468b0885f5e29df5",
|
||||||
|
"dest": "cargo/vendor/unicode-bidi-0.3.18"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "inline",
|
||||||
|
"contents": "{\"package\": \"5c1cb5db39152898a79168971543b1cb5020dff7fe43c8dc468b0885f5e29df5\", \"files\": {}}",
|
||||||
|
"dest": "cargo/vendor/unicode-bidi-0.3.18",
|
||||||
|
"dest-filename": ".cargo-checksum.json"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"type": "archive",
|
"type": "archive",
|
||||||
"archive-type": "tar-gzip",
|
"archive-type": "tar-gzip",
|
||||||
@@ -4198,6 +4510,32 @@
|
|||||||
"dest": "cargo/vendor/unicode-ident-1.0.26",
|
"dest": "cargo/vendor/unicode-ident-1.0.26",
|
||||||
"dest-filename": ".cargo-checksum.json"
|
"dest-filename": ".cargo-checksum.json"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"type": "archive",
|
||||||
|
"archive-type": "tar-gzip",
|
||||||
|
"url": "https://static.crates.io/crates/unicode-normalization/unicode-normalization-0.1.26.crate",
|
||||||
|
"sha256": "75cd3fb03d5288b3ba9a1ee103ac7e9cbb59f0b03d64f8ddfcc04dd42758a85d",
|
||||||
|
"dest": "cargo/vendor/unicode-normalization-0.1.26"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "inline",
|
||||||
|
"contents": "{\"package\": \"75cd3fb03d5288b3ba9a1ee103ac7e9cbb59f0b03d64f8ddfcc04dd42758a85d\", \"files\": {}}",
|
||||||
|
"dest": "cargo/vendor/unicode-normalization-0.1.26",
|
||||||
|
"dest-filename": ".cargo-checksum.json"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "archive",
|
||||||
|
"archive-type": "tar-gzip",
|
||||||
|
"url": "https://static.crates.io/crates/unicode-properties/unicode-properties-0.1.4.crate",
|
||||||
|
"sha256": "7df058c713841ad818f1dc5d3fd88063241cc61f49f5fbea4b951e8cf5a8d71d",
|
||||||
|
"dest": "cargo/vendor/unicode-properties-0.1.4"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "inline",
|
||||||
|
"contents": "{\"package\": \"7df058c713841ad818f1dc5d3fd88063241cc61f49f5fbea4b951e8cf5a8d71d\", \"files\": {}}",
|
||||||
|
"dest": "cargo/vendor/unicode-properties-0.1.4",
|
||||||
|
"dest-filename": ".cargo-checksum.json"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"type": "archive",
|
"type": "archive",
|
||||||
"archive-type": "tar-gzip",
|
"archive-type": "tar-gzip",
|
||||||
@@ -4640,6 +4978,19 @@
|
|||||||
"dest": "cargo/vendor/weezl-0.1.12",
|
"dest": "cargo/vendor/weezl-0.1.12",
|
||||||
"dest-filename": ".cargo-checksum.json"
|
"dest-filename": ".cargo-checksum.json"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"type": "archive",
|
||||||
|
"archive-type": "tar-gzip",
|
||||||
|
"url": "https://static.crates.io/crates/weezl/weezl-0.2.1.crate",
|
||||||
|
"sha256": "d4ca08e5ef825b65b056d9efbd95c8750683f0a6d0466d02e96dc2e4e360f3d2",
|
||||||
|
"dest": "cargo/vendor/weezl-0.2.1"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "inline",
|
||||||
|
"contents": "{\"package\": \"d4ca08e5ef825b65b056d9efbd95c8750683f0a6d0466d02e96dc2e4e360f3d2\", \"files\": {}}",
|
||||||
|
"dest": "cargo/vendor/weezl-0.2.1",
|
||||||
|
"dest-filename": ".cargo-checksum.json"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"type": "archive",
|
"type": "archive",
|
||||||
"archive-type": "tar-gzip",
|
"archive-type": "tar-gzip",
|
||||||
|
|||||||
+73
-130
@@ -1,4 +1,4 @@
|
|||||||
//! Verlustfreier Vektor-PDF-Export des gesamten Notizbuchs via printpdf.
|
//! Verlustfreier Vektor-PDF-Export des gesamten Notizbuchs (PDF-Ausgabe: [`crate::pdf_writer`]).
|
||||||
//!
|
//!
|
||||||
//! Features:
|
//! Features:
|
||||||
//! - Hochformat DIN A4 (210 mm x 297 mm)
|
//! - Hochformat DIN A4 (210 mm x 297 mm)
|
||||||
@@ -8,7 +8,7 @@
|
|||||||
//! - Vektor-Hintergründe (Kariert 5mm, Liniert, Punktraster, Blanko)
|
//! - Vektor-Hintergründe (Kariert 5mm, Liniert, Punktraster, Blanko)
|
||||||
//! - Zwei Export-Strategien:
|
//! - Zwei Export-Strategien:
|
||||||
//! - `FilledPolygon`: Glatte gefüllte Polygone aus den Triangle-Strip-Hüllen (variable Pinselstärke)
|
//! - `FilledPolygon`: Glatte gefüllte Polygone aus den Triangle-Strip-Hüllen (variable Pinselstärke)
|
||||||
//! - `BezierCurve`: Glatte kubische Bézier-Kurven via `printpdf::Line`
|
//! - `BezierCurve`: Glatte kubische Bézier-Kurven via `pdf_writer::Line`
|
||||||
|
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
use std::error::Error;
|
use std::error::Error;
|
||||||
@@ -16,14 +16,15 @@ use std::fs::File;
|
|||||||
use std::io::BufWriter;
|
use std::io::BufWriter;
|
||||||
use std::path::{Path, PathBuf};
|
use std::path::{Path, PathBuf};
|
||||||
|
|
||||||
use printpdf::path::{PaintMode, WindingOrder};
|
|
||||||
use printpdf::{Color, Line, Mm, PdfDocument, PdfDocumentReference, Point, Polygon, Rgb};
|
|
||||||
|
|
||||||
use crate::assets::AssetStore;
|
use crate::assets::AssetStore;
|
||||||
use crate::background::{PatternColor, DOT_SIZE, GRID_5MM_SPACING, LINED_SPACING};
|
use crate::background::{PatternColor, DOT_SIZE, GRID_5MM_SPACING, LINED_SPACING};
|
||||||
use crate::document::{
|
use crate::document::{
|
||||||
Notebook, Page, PageBackground, Stroke, A4_HEIGHT_MM, A4_WIDTH_MM, PAGE_HEIGHT, PAGE_WIDTH,
|
Notebook, Page, PageBackground, Stroke, A4_HEIGHT_MM, A4_WIDTH_MM, PAGE_HEIGHT, PAGE_WIDTH,
|
||||||
};
|
};
|
||||||
|
use crate::pdf_writer::{
|
||||||
|
Color, Line, Mm, PaintMode, PdfDocument, PdfDocumentReference, Point, Polygon, Rgb,
|
||||||
|
WindingOrder,
|
||||||
|
};
|
||||||
use crate::t;
|
use crate::t;
|
||||||
use ab_glyph::Font as _;
|
use ab_glyph::Font as _;
|
||||||
|
|
||||||
@@ -147,19 +148,19 @@ fn pdf_page_mm(page: &Page) -> (Mm, Mm) {
|
|||||||
|
|
||||||
/// Beginnt eine Ebene: Koordinaten beziehen sich auf eine A4-hohe Seite (siehe
|
/// Beginnt eine Ebene: Koordinaten beziehen sich auf eine A4-hohe Seite (siehe
|
||||||
/// [`page_to_pdf_mm`]) und werden auf die tatsächliche Höhe von `page` verschoben.
|
/// [`page_to_pdf_mm`]) und werden auf die tatsächliche Höhe von `page` verschoben.
|
||||||
fn begin_page_layer(layer: &printpdf::PdfLayerReference, page: &Page) {
|
fn begin_page_layer(layer: &crate::pdf_writer::PdfLayerReference, page: &Page) {
|
||||||
layer.save_graphics_state();
|
layer.save_graphics_state();
|
||||||
let dy = (page.size.y - PAGE_HEIGHT) * PT_PER_UNIT;
|
let dy = (page.size.y - PAGE_HEIGHT) * PT_PER_UNIT;
|
||||||
if dy != 0.0 {
|
if dy != 0.0 {
|
||||||
layer.set_ctm(printpdf::CurTransMat::Translate(
|
layer.set_ctm(crate::pdf_writer::CurTransMat::Translate(
|
||||||
printpdf::Pt(0.0),
|
crate::pdf_writer::Pt(0.0),
|
||||||
printpdf::Pt(dy),
|
crate::pdf_writer::Pt(dy),
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Beendet eine mit [`begin_page_layer`] begonnene Ebene.
|
/// Beendet eine mit [`begin_page_layer`] begonnene Ebene.
|
||||||
fn end_page_layer(layer: &printpdf::PdfLayerReference) {
|
fn end_page_layer(layer: &crate::pdf_writer::PdfLayerReference) {
|
||||||
layer.restore_graphics_state();
|
layer.restore_graphics_state();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -211,7 +212,7 @@ pub fn export_notebook_to_bytes(
|
|||||||
return export_notebook_to_bytes(&cropped, strategy, assets, pattern);
|
return export_notebook_to_bytes(&cropped, strategy, assets, pattern);
|
||||||
}
|
}
|
||||||
let doc = generate_pdf_document(notebook, strategy, assets, pattern)?;
|
let doc = generate_pdf_document(notebook, strategy, assets, pattern)?;
|
||||||
let bytes = add_alpha_states(fix_text_encoding(doc.save_to_bytes()?, notebook)?, notebook)?;
|
let bytes = add_alpha_states(doc.save_to_bytes()?, notebook)?;
|
||||||
|
|
||||||
// Importierte PDF-Seiten als Vektorgrafik unterlegen
|
// Importierte PDF-Seiten als Vektorgrafik unterlegen
|
||||||
let underlays = pdf_underlays(notebook);
|
let underlays = pdf_underlays(notebook);
|
||||||
@@ -237,7 +238,7 @@ pub fn export_notebook_to_bytes(
|
|||||||
raster.insert(underlay.target_page);
|
raster.insert(underlay.target_page);
|
||||||
}
|
}
|
||||||
let doc = generate_pdf_document_with(notebook, strategy, assets, pattern, &raster)?;
|
let doc = generate_pdf_document_with(notebook, strategy, assets, pattern, &raster)?;
|
||||||
let bytes = add_alpha_states(fix_text_encoding(doc.save_to_bytes()?, notebook)?, notebook)?;
|
let bytes = add_alpha_states(doc.save_to_bytes()?, notebook)?;
|
||||||
let remaining: Vec<_> = underlays
|
let remaining: Vec<_> = underlays
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.filter(|u| !raster.contains(&u.target_page))
|
.filter(|u| !raster.contains(&u.target_page))
|
||||||
@@ -249,35 +250,6 @@ pub fn export_notebook_to_bytes(
|
|||||||
Ok(merged)
|
Ok(merged)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// printpdf schreibt Lesezeichen-Titel als rohes UTF-8, PDF-Betrachter lesen Textstrings aber
|
|
||||||
/// als PDFDocEncoding oder UTF-16BE: Umlaute und Gedankenstriche würden verstümmelt. Titel
|
|
||||||
/// mit Nicht-ASCII-Zeichen werden deshalb nach UTF-16BE (mit BOM) umgeschrieben.
|
|
||||||
fn fix_text_encoding(bytes: Vec<u8>, notebook: &Notebook) -> Result<Vec<u8>, Box<dyn Error>> {
|
|
||||||
let needs_fix = notebook
|
|
||||||
.pages
|
|
||||||
.iter()
|
|
||||||
.filter_map(|p| p.title.as_deref())
|
|
||||||
.any(|t| !t.is_ascii());
|
|
||||||
if !needs_fix {
|
|
||||||
return Ok(bytes);
|
|
||||||
}
|
|
||||||
let mut doc = printpdf::lopdf::Document::load_mem(&bytes)?;
|
|
||||||
for object in doc.objects.values_mut() {
|
|
||||||
let Ok(dict) = object.as_dict_mut() else {
|
|
||||||
continue;
|
|
||||||
};
|
|
||||||
if let Ok(printpdf::lopdf::Object::String(raw, format)) = dict.get_mut(b"Title") {
|
|
||||||
if let Some(utf16) = utf16be_text(raw) {
|
|
||||||
*raw = utf16;
|
|
||||||
*format = printpdf::lopdf::StringFormat::Hexadecimal;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
let mut out = Vec::new();
|
|
||||||
doc.save_to(&mut out)?;
|
|
||||||
Ok(out)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Name des Grafikzustands für halbtransparente Tinte mit Deckkraft `alpha` (auf Prozent
|
/// Name des Grafikzustands für halbtransparente Tinte mit Deckkraft `alpha` (auf Prozent
|
||||||
/// gerundet); `None` für deckende Farben.
|
/// gerundet); `None` für deckende Farben.
|
||||||
fn alpha_state_name(alpha: f32) -> Option<String> {
|
fn alpha_state_name(alpha: f32) -> Option<String> {
|
||||||
@@ -285,12 +257,12 @@ fn alpha_state_name(alpha: f32) -> Option<String> {
|
|||||||
(percent < 100).then(|| format!("InkAlpha{percent}"))
|
(percent < 100).then(|| format!("InkAlpha{percent}"))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// printpdf kann keine Grafikzustände mit Deckkraft anlegen: Die Tintenebene verweist mit
|
/// Grafikzustände mit Deckkraft: Die Tintenebene verweist mit
|
||||||
/// `gs` auf die Namen aus [`alpha_state_name`], die Zustände selbst (`ca`/`CA`) werden hier
|
/// `gs` auf die Namen aus [`alpha_state_name`], die Zustände selbst (`ca`/`CA`) werden hier
|
||||||
/// in die Ressourcen jeder Seite eingetragen. Ohne halbtransparente Striche bleibt das PDF
|
/// in die Ressourcen jeder Seite eingetragen. Ohne halbtransparente Striche bleibt das PDF
|
||||||
/// unverändert.
|
/// unverändert.
|
||||||
fn add_alpha_states(bytes: Vec<u8>, notebook: &Notebook) -> Result<Vec<u8>, Box<dyn Error>> {
|
fn add_alpha_states(bytes: Vec<u8>, notebook: &Notebook) -> Result<Vec<u8>, Box<dyn Error>> {
|
||||||
use printpdf::lopdf::{Dictionary, Object};
|
use lopdf::{Dictionary, Object};
|
||||||
|
|
||||||
let mut states: Vec<(String, f32)> = notebook
|
let mut states: Vec<(String, f32)> = notebook
|
||||||
.pages
|
.pages
|
||||||
@@ -305,7 +277,7 @@ fn add_alpha_states(bytes: Vec<u8>, notebook: &Notebook) -> Result<Vec<u8>, Box<
|
|||||||
states.sort_by(|a, b| a.0.cmp(&b.0));
|
states.sort_by(|a, b| a.0.cmp(&b.0));
|
||||||
states.dedup_by(|a, b| a.0 == b.0);
|
states.dedup_by(|a, b| a.0 == b.0);
|
||||||
|
|
||||||
let mut doc = printpdf::lopdf::Document::load_mem(&bytes)?;
|
let mut doc = lopdf::Document::load_mem(&bytes)?;
|
||||||
let page_ids: Vec<_> = doc.get_pages().into_values().collect();
|
let page_ids: Vec<_> = doc.get_pages().into_values().collect();
|
||||||
for page_id in page_ids {
|
for page_id in page_ids {
|
||||||
let resources = dict_object_id(&mut doc, page_id, b"Resources")?;
|
let resources = dict_object_id(&mut doc, page_id, b"Resources")?;
|
||||||
@@ -328,11 +300,11 @@ fn add_alpha_states(bytes: Vec<u8>, notebook: &Notebook) -> Result<Vec<u8>, Box<
|
|||||||
/// Kennung des Wörterbuchs unter `key` im Objekt `owner`; ein eingebettetes oder fehlendes
|
/// Kennung des Wörterbuchs unter `key` im Objekt `owner`; ein eingebettetes oder fehlendes
|
||||||
/// Wörterbuch wird dafür zu einem eigenen Objekt.
|
/// Wörterbuch wird dafür zu einem eigenen Objekt.
|
||||||
fn dict_object_id(
|
fn dict_object_id(
|
||||||
doc: &mut printpdf::lopdf::Document,
|
doc: &mut lopdf::Document,
|
||||||
owner: printpdf::lopdf::ObjectId,
|
owner: lopdf::ObjectId,
|
||||||
key: &[u8],
|
key: &[u8],
|
||||||
) -> Result<printpdf::lopdf::ObjectId, Box<dyn Error>> {
|
) -> Result<lopdf::ObjectId, Box<dyn Error>> {
|
||||||
use printpdf::lopdf::{Dictionary, Object};
|
use lopdf::{Dictionary, Object};
|
||||||
|
|
||||||
let entry = doc.get_object(owner)?.as_dict()?.get(key).ok().cloned();
|
let entry = doc.get_object(owner)?.as_dict()?.get(key).ok().cloned();
|
||||||
if let Some(Object::Reference(id)) = entry {
|
if let Some(Object::Reference(id)) = entry {
|
||||||
@@ -349,20 +321,6 @@ fn dict_object_id(
|
|||||||
Ok(id)
|
Ok(id)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// UTF-8-Text als PDF-Textstring in UTF-16BE mit BOM; `None` bei reinem ASCII oder
|
|
||||||
/// ungültigem UTF-8.
|
|
||||||
fn utf16be_text(raw: &[u8]) -> Option<Vec<u8>> {
|
|
||||||
let text = std::str::from_utf8(raw).ok()?;
|
|
||||||
if text.is_ascii() {
|
|
||||||
return None;
|
|
||||||
}
|
|
||||||
let mut out = vec![0xFE, 0xFF];
|
|
||||||
for unit in text.encode_utf16() {
|
|
||||||
out.extend_from_slice(&unit.to_be_bytes());
|
|
||||||
}
|
|
||||||
Some(out)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Unterlagen für alle Seiten mit importierter PDF-Seite.
|
/// Unterlagen für alle Seiten mit importierter PDF-Seite.
|
||||||
fn pdf_underlays(notebook: &Notebook) -> Vec<crate::pdf_merge::Underlay> {
|
fn pdf_underlays(notebook: &Notebook) -> Vec<crate::pdf_merge::Underlay> {
|
||||||
notebook
|
notebook
|
||||||
@@ -387,7 +345,7 @@ fn pdf_underlays(notebook: &Notebook) -> Vec<crate::pdf_merge::Underlay> {
|
|||||||
.collect()
|
.collect()
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Generiert das `printpdf::PdfDocumentReference` für alle Seiten des Notizbuchs.
|
/// Generiert das `crate::pdf_writer::PdfDocumentReference` für alle Seiten des Notizbuchs.
|
||||||
/// Importierte PDF-Seiten sind darin noch nicht enthalten; die legt
|
/// Importierte PDF-Seiten sind darin noch nicht enthalten; die legt
|
||||||
/// [`export_notebook_to_bytes`] anschließend als Vektorgrafik darunter.
|
/// [`export_notebook_to_bytes`] anschließend als Vektorgrafik darunter.
|
||||||
pub fn generate_pdf_document(
|
pub fn generate_pdf_document(
|
||||||
@@ -447,7 +405,7 @@ fn generate_pdf_document_with(
|
|||||||
}
|
}
|
||||||
let page_ref = doc.get_page(page_idx);
|
let page_ref = doc.get_page(page_idx);
|
||||||
render_background_layer(&page_ref.get_layer(layer_bg), page, pattern);
|
render_background_layer(&page_ref.get_layer(layer_bg), page, pattern);
|
||||||
let layer = |name: &str, render: &dyn Fn(&printpdf::PdfLayerReference)| {
|
let layer = |name: &str, render: &dyn Fn(&crate::pdf_writer::PdfLayerReference)| {
|
||||||
let layer = page_ref.add_layer(name);
|
let layer = page_ref.add_layer(name);
|
||||||
begin_page_layer(&layer, page);
|
begin_page_layer(&layer, page);
|
||||||
render(&layer);
|
render(&layer);
|
||||||
@@ -470,7 +428,7 @@ fn generate_pdf_document_with(
|
|||||||
|
|
||||||
/// Rendert das Vektorraster auf Layer 1 ("Background").
|
/// Rendert das Vektorraster auf Layer 1 ("Background").
|
||||||
fn render_background_layer(
|
fn render_background_layer(
|
||||||
layer: &printpdf::PdfLayerReference,
|
layer: &crate::pdf_writer::PdfLayerReference,
|
||||||
page: &Page,
|
page: &Page,
|
||||||
pattern: PatternColor,
|
pattern: PatternColor,
|
||||||
) {
|
) {
|
||||||
@@ -481,7 +439,7 @@ fn render_background_layer(
|
|||||||
.color(crate::background::PaperTheme::White)
|
.color(crate::background::PaperTheme::White)
|
||||||
.map(crate::background::linear_to_srgb);
|
.map(crate::background::linear_to_srgb);
|
||||||
let grid_color = Color::Rgb(Rgb::new(r, g, b, None));
|
let grid_color = Color::Rgb(Rgb::new(r, g, b, None));
|
||||||
layer.set_outline_color(grid_color.clone());
|
layer.set_outline_color(grid_color);
|
||||||
layer.set_outline_thickness(0.4); // Feine 0.15 mm Haarlinie (ca. 0.4 pt)
|
layer.set_outline_thickness(0.4); // Feine 0.15 mm Haarlinie (ca. 0.4 pt)
|
||||||
layer.set_fill_color(grid_color);
|
layer.set_fill_color(grid_color);
|
||||||
|
|
||||||
@@ -567,7 +525,11 @@ fn render_background_layer(
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Rendert alle Striche einer Seite auf Layer 2 ("Ink").
|
/// Rendert alle Striche einer Seite auf Layer 2 ("Ink").
|
||||||
fn render_ink_layer(layer: &printpdf::PdfLayerReference, page: &Page, strategy: ExportStrategy) {
|
fn render_ink_layer(
|
||||||
|
layer: &crate::pdf_writer::PdfLayerReference,
|
||||||
|
page: &Page,
|
||||||
|
strategy: ExportStrategy,
|
||||||
|
) {
|
||||||
for stroke in &page.strokes {
|
for stroke in &page.strokes {
|
||||||
if stroke.is_object() {
|
if stroke.is_object() {
|
||||||
continue;
|
continue;
|
||||||
@@ -578,9 +540,9 @@ fn render_ink_layer(layer: &printpdf::PdfLayerReference, page: &Page, strategy:
|
|||||||
let alpha_state = alpha_state_name(stroke.color[3]);
|
let alpha_state = alpha_state_name(stroke.color[3]);
|
||||||
if let Some(name) = &alpha_state {
|
if let Some(name) = &alpha_state {
|
||||||
layer.save_graphics_state();
|
layer.save_graphics_state();
|
||||||
layer.add_operation(printpdf::lopdf::content::Operation::new(
|
layer.add_operation(lopdf::content::Operation::new(
|
||||||
"gs",
|
"gs",
|
||||||
vec![printpdf::lopdf::Object::Name(name.as_bytes().to_vec())],
|
vec![lopdf::Object::Name(name.as_bytes().to_vec())],
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
if let Some(ref shape) = stroke.shape {
|
if let Some(ref shape) = stroke.shape {
|
||||||
@@ -648,8 +610,8 @@ fn pdf_image_xobject(
|
|||||||
assets: &AssetStore,
|
assets: &AssetStore,
|
||||||
asset: &str,
|
asset: &str,
|
||||||
corners: &[glam::Vec2; 4],
|
corners: &[glam::Vec2; 4],
|
||||||
) -> Result<printpdf::ImageXObject, Box<dyn Error>> {
|
) -> Result<crate::pdf_writer::ImageXObject, Box<dyn Error>> {
|
||||||
use printpdf::{ColorBits, ColorSpace, ImageFilter, ImageXObject, Px};
|
use crate::pdf_writer::{ColorSpace, ImageFilter, ImageXObject, Px};
|
||||||
|
|
||||||
let bytes = assets.read_bytes(asset)?;
|
let bytes = assets.read_bytes(asset)?;
|
||||||
let decoded = image::load_from_memory(&bytes)?;
|
let decoded = image::load_from_memory(&bytes)?;
|
||||||
@@ -665,12 +627,9 @@ fn pdf_image_xobject(
|
|||||||
width: Px(decoded.width() as usize),
|
width: Px(decoded.width() as usize),
|
||||||
height: Px(decoded.height() as usize),
|
height: Px(decoded.height() as usize),
|
||||||
color_space,
|
color_space,
|
||||||
bits_per_component: ColorBits::Bit8,
|
|
||||||
interpolate: true,
|
interpolate: true,
|
||||||
image_data: bytes,
|
image_data: bytes,
|
||||||
image_filter: Some(ImageFilter::DCT),
|
image_filter: Some(ImageFilter::DCT),
|
||||||
smask: None,
|
|
||||||
clipping_bbox: None,
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -700,7 +659,7 @@ fn pdf_image_xobject(
|
|||||||
/// Rendert die eingefügten Bilder einer Seite. Fehlt eine Bilddatei, erscheint an ihrer
|
/// Rendert die eingefügten Bilder einer Seite. Fehlt eine Bilddatei, erscheint an ihrer
|
||||||
/// Stelle eine graue Fläche, damit der Export nicht abbricht.
|
/// Stelle eine graue Fläche, damit der Export nicht abbricht.
|
||||||
fn render_image_layer(
|
fn render_image_layer(
|
||||||
layer: &printpdf::PdfLayerReference,
|
layer: &crate::pdf_writer::PdfLayerReference,
|
||||||
page: &Page,
|
page: &Page,
|
||||||
assets: Option<&AssetStore>,
|
assets: Option<&AssetStore>,
|
||||||
) {
|
) {
|
||||||
@@ -725,7 +684,7 @@ fn render_image_layer(
|
|||||||
fn embed_text_fonts(
|
fn embed_text_fonts(
|
||||||
doc: &PdfDocumentReference,
|
doc: &PdfDocumentReference,
|
||||||
notebook: &Notebook,
|
notebook: &Notebook,
|
||||||
) -> Result<HashMap<TextFont, printpdf::IndirectFontRef>, Box<dyn Error>> {
|
) -> Result<HashMap<TextFont, crate::pdf_writer::IndirectFontRef>, Box<dyn Error>> {
|
||||||
let mut fonts = HashMap::new();
|
let mut fonts = HashMap::new();
|
||||||
let used = notebook
|
let used = notebook
|
||||||
.pages
|
.pages
|
||||||
@@ -744,9 +703,9 @@ fn embed_text_fonts(
|
|||||||
/// Koordinatensystem (eine Einheit = eine Seiteneinheit, y nach oben) gesetzt, das eine
|
/// Koordinatensystem (eine Einheit = eine Seiteneinheit, y nach oben) gesetzt, das eine
|
||||||
/// Transformationsmatrix auf seine Ecken abbildet – so stimmen Drehung und Skalierung.
|
/// Transformationsmatrix auf seine Ecken abbildet – so stimmen Drehung und Skalierung.
|
||||||
fn render_text_layer(
|
fn render_text_layer(
|
||||||
layer: &printpdf::PdfLayerReference,
|
layer: &crate::pdf_writer::PdfLayerReference,
|
||||||
page: &Page,
|
page: &Page,
|
||||||
fonts: &HashMap<TextFont, printpdf::IndirectFontRef>,
|
fonts: &HashMap<TextFont, crate::pdf_writer::IndirectFontRef>,
|
||||||
) {
|
) {
|
||||||
for stroke in &page.strokes {
|
for stroke in &page.strokes {
|
||||||
let (Some(text), Some(corners)) = (&stroke.text, stroke.object_corners()) else {
|
let (Some(text), Some(corners)) = (&stroke.text, stroke.object_corners()) else {
|
||||||
@@ -763,7 +722,7 @@ fn render_text_layer(
|
|||||||
let o = page_to_pdf_pt(origin);
|
let o = page_to_pdf_pt(origin);
|
||||||
let matrix = [right.x * k, -right.y * k, -down.x * k, down.y * k, o.x, o.y];
|
let matrix = [right.x * k, -right.y * k, -down.x * k, down.y * k, o.x, o.y];
|
||||||
layer.save_graphics_state();
|
layer.save_graphics_state();
|
||||||
layer.set_ctm(printpdf::CurTransMat::Raw(matrix));
|
layer.set_ctm(crate::pdf_writer::CurTransMat::Raw(matrix));
|
||||||
layer.set_fill_color(Color::Rgb(Rgb::new(
|
layer.set_fill_color(Color::Rgb(Rgb::new(
|
||||||
stroke.color[0],
|
stroke.color[0],
|
||||||
stroke.color[1],
|
stroke.color[1],
|
||||||
@@ -776,9 +735,9 @@ fn render_text_layer(
|
|||||||
if line.text.trim().is_empty() {
|
if line.text.trim().is_empty() {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
layer.set_text_matrix(printpdf::TextMatrix::Translate(
|
layer.set_text_matrix(crate::pdf_writer::TextMatrix::Translate(
|
||||||
printpdf::Pt(0.0),
|
crate::pdf_writer::Pt(0.0),
|
||||||
printpdf::Pt(-layout.baseline(i)),
|
crate::pdf_writer::Pt(-layout.baseline(i)),
|
||||||
));
|
));
|
||||||
// Glyphen samt Unterschneidung wie auf dem Bildschirm (Abstände in 1/1000 Em)
|
// Glyphen samt Unterschneidung wie auf dem Bildschirm (Abstände in 1/1000 Em)
|
||||||
let face = text.font.face();
|
let face = text.font.face();
|
||||||
@@ -796,45 +755,31 @@ fn render_text_layer(
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Graue 1×1-Fläche für fehlende Bilder.
|
/// Graue 1×1-Fläche für fehlende Bilder.
|
||||||
fn placeholder_xobject() -> printpdf::ImageXObject {
|
fn placeholder_xobject() -> crate::pdf_writer::ImageXObject {
|
||||||
rgb_xobject(1, 1, vec![200, 200, 200])
|
rgb_xobject(1, 1, vec![200, 200, 200])
|
||||||
}
|
}
|
||||||
|
|
||||||
fn rgb_xobject(width: u32, height: u32, rgb: Vec<u8>) -> printpdf::ImageXObject {
|
fn rgb_xobject(width: u32, height: u32, rgb: Vec<u8>) -> crate::pdf_writer::ImageXObject {
|
||||||
printpdf::ImageXObject {
|
crate::pdf_writer::ImageXObject {
|
||||||
width: printpdf::Px(width as usize),
|
width: crate::pdf_writer::Px(width as usize),
|
||||||
height: printpdf::Px(height as usize),
|
height: crate::pdf_writer::Px(height as usize),
|
||||||
color_space: printpdf::ColorSpace::Rgb,
|
color_space: crate::pdf_writer::ColorSpace::Rgb,
|
||||||
bits_per_component: printpdf::ColorBits::Bit8,
|
|
||||||
interpolate: true,
|
interpolate: true,
|
||||||
image_data: rgb,
|
image_data: rgb,
|
||||||
image_filter: None,
|
image_filter: None,
|
||||||
smask: None,
|
|
||||||
clipping_bbox: None,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Zeichnet ein Bild so, dass es die Ecken oben links, oben rechts, unten rechts,
|
/// Zeichnet ein Bild so, dass es die Ecken oben links, oben rechts, unten rechts,
|
||||||
/// unten links ausfüllt.
|
/// unten links ausfüllt.
|
||||||
fn draw_image_xobject(
|
fn draw_image_xobject(
|
||||||
layer: &printpdf::PdfLayerReference,
|
layer: &crate::pdf_writer::PdfLayerReference,
|
||||||
xobject: printpdf::ImageXObject,
|
xobject: crate::pdf_writer::ImageXObject,
|
||||||
corners: &[glam::Vec2; 4],
|
corners: &[glam::Vec2; 4],
|
||||||
) {
|
) {
|
||||||
let (width, height) = (xobject.width.0 as f32, xobject.height.0 as f32);
|
|
||||||
layer.save_graphics_state();
|
layer.save_graphics_state();
|
||||||
layer.set_ctm(printpdf::CurTransMat::Raw(image_matrix(corners)));
|
layer.set_ctm(crate::pdf_writer::CurTransMat::Raw(image_matrix(corners)));
|
||||||
// Bei 72 dpi ist ein Pixel ein Punkt; die Skalierung bildet das Bild auf das
|
layer.add_image(xobject);
|
||||||
// Einheitsquadrat ab, die Matrix oben auf die Bildecken
|
|
||||||
printpdf::Image::from(xobject).add_to_layer(
|
|
||||||
layer.clone(),
|
|
||||||
printpdf::ImageTransform {
|
|
||||||
dpi: Some(72.0),
|
|
||||||
scale_x: Some(1.0 / width),
|
|
||||||
scale_y: Some(1.0 / height),
|
|
||||||
..Default::default()
|
|
||||||
},
|
|
||||||
);
|
|
||||||
layer.restore_graphics_state();
|
layer.restore_graphics_state();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -844,7 +789,7 @@ const PDF_RASTER_DPI: f32 = 200.0;
|
|||||||
|
|
||||||
/// Bettet die importierte PDF-Seite als gerendertes Bild ein (Ausweichlösung).
|
/// Bettet die importierte PDF-Seite als gerendertes Bild ein (Ausweichlösung).
|
||||||
fn render_pdf_raster(
|
fn render_pdf_raster(
|
||||||
layer: &printpdf::PdfLayerReference,
|
layer: &crate::pdf_writer::PdfLayerReference,
|
||||||
page: &Page,
|
page: &Page,
|
||||||
assets: Option<&AssetStore>,
|
assets: Option<&AssetStore>,
|
||||||
) {
|
) {
|
||||||
@@ -882,19 +827,19 @@ fn render_pdf_raster(
|
|||||||
/// Rendert eine mathematisch bereinigte Form (Gerade, Kreis, Rechteck, Koordinatensystem)
|
/// Rendert eine mathematisch bereinigte Form (Gerade, Kreis, Rechteck, Koordinatensystem)
|
||||||
/// als verlustfreien Vektorpfad im PDF.
|
/// als verlustfreien Vektorpfad im PDF.
|
||||||
fn render_recognized_shape(
|
fn render_recognized_shape(
|
||||||
layer: &printpdf::PdfLayerReference,
|
layer: &crate::pdf_writer::PdfLayerReference,
|
||||||
shape: &crate::geometry::RecognizedShape,
|
shape: &crate::geometry::RecognizedShape,
|
||||||
color: [f32; 4],
|
color: [f32; 4],
|
||||||
) {
|
) {
|
||||||
use crate::geometry::RecognizedShape;
|
use crate::geometry::RecognizedShape;
|
||||||
|
|
||||||
let stroke_color = Color::Rgb(Rgb::new(color[0], color[1], color[2], None));
|
let stroke_color = Color::Rgb(Rgb::new(color[0], color[1], color[2], None));
|
||||||
layer.set_outline_color(stroke_color.clone());
|
layer.set_outline_color(stroke_color);
|
||||||
layer.set_outline_thickness(1.5); // ca. 0.5 mm Standard-Liniendicke
|
layer.set_outline_thickness(1.5); // ca. 0.5 mm Standard-Liniendicke
|
||||||
// Wie auf dem Bildschirm: spitze Ecken und stumpfe Enden (der Grafikzustand kann noch
|
// Wie auf dem Bildschirm: spitze Ecken und stumpfe Enden (der Grafikzustand kann noch
|
||||||
// runde Verbindungen von einem vorherigen Freihandstrich enthalten)
|
// runde Verbindungen von einem vorherigen Freihandstrich enthalten)
|
||||||
layer.set_line_cap_style(printpdf::LineCapStyle::Butt);
|
layer.set_line_cap_style(crate::pdf_writer::LineCapStyle::Butt);
|
||||||
layer.set_line_join_style(printpdf::LineJoinStyle::Miter);
|
layer.set_line_join_style(crate::pdf_writer::LineJoinStyle::Miter);
|
||||||
|
|
||||||
match shape {
|
match shape {
|
||||||
RecognizedShape::Line { start, end } => {
|
RecognizedShape::Line { start, end } => {
|
||||||
@@ -1178,7 +1123,7 @@ fn render_recognized_shape(
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Strategie A: Gefülltes Polygon aus den Triangle-Strip-Hüllpunkten.
|
/// Strategie A: Gefülltes Polygon aus den Triangle-Strip-Hüllpunkten.
|
||||||
fn render_stroke_polygon(layer: &printpdf::PdfLayerReference, stroke: &Stroke) {
|
fn render_stroke_polygon(layer: &crate::pdf_writer::PdfLayerReference, stroke: &Stroke) {
|
||||||
if stroke.vertices.is_empty() {
|
if stroke.vertices.is_empty() {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -1266,8 +1211,8 @@ fn exact_outlines() -> bool {
|
|||||||
*EXACT.get_or_init(|| std::env::var_os("INK_NOTES_EXACT_OUTLINES").is_some_and(|v| v != "0"))
|
*EXACT.get_or_init(|| std::env::var_os("INK_NOTES_EXACT_OUTLINES").is_some_and(|v| v != "0"))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Strategie B: Kubische Bézier-Kurven aus den Abtastpunkten via printpdf::Line.
|
/// Strategie B: Kubische Bézier-Kurven aus den Abtastpunkten via crate::pdf_writer::Line.
|
||||||
fn render_stroke_bezier(layer: &printpdf::PdfLayerReference, stroke: &Stroke) {
|
fn render_stroke_bezier(layer: &crate::pdf_writer::PdfLayerReference, stroke: &Stroke) {
|
||||||
if stroke.points.is_empty() {
|
if stroke.points.is_empty() {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -1279,8 +1224,8 @@ fn render_stroke_bezier(layer: &printpdf::PdfLayerReference, stroke: &Stroke) {
|
|||||||
None,
|
None,
|
||||||
));
|
));
|
||||||
layer.set_outline_color(outline_color);
|
layer.set_outline_color(outline_color);
|
||||||
layer.set_line_cap_style(printpdf::LineCapStyle::Round);
|
layer.set_line_cap_style(crate::pdf_writer::LineCapStyle::Round);
|
||||||
layer.set_line_join_style(printpdf::LineJoinStyle::Round);
|
layer.set_line_join_style(crate::pdf_writer::LineJoinStyle::Round);
|
||||||
|
|
||||||
// Durchschnittliche Linienstärke aus dem Druck berechnen
|
// Durchschnittliche Linienstärke aus dem Druck berechnen
|
||||||
let avg_pressure =
|
let avg_pressure =
|
||||||
@@ -1753,10 +1698,10 @@ mod tests {
|
|||||||
PatternColor::default(),
|
PatternColor::default(),
|
||||||
)
|
)
|
||||||
.unwrap();
|
.unwrap();
|
||||||
let doc = printpdf::lopdf::Document::load_mem(&bytes).unwrap();
|
let doc = lopdf::Document::load_mem(&bytes).unwrap();
|
||||||
assert_eq!(doc.get_pages().len(), 2);
|
assert_eq!(doc.get_pages().len(), 2);
|
||||||
for page_id in doc.get_pages().into_values() {
|
for page_id in doc.get_pages().into_values() {
|
||||||
let content = doc.get_page_content(page_id).unwrap();
|
let content = doc.get_page_content(page_id);
|
||||||
assert!(
|
assert!(
|
||||||
String::from_utf8_lossy(&content).starts_with("q "),
|
String::from_utf8_lossy(&content).starts_with("q "),
|
||||||
"Originalseite fehlt als Unterlage"
|
"Originalseite fehlt als Unterlage"
|
||||||
@@ -1864,7 +1809,7 @@ mod tests {
|
|||||||
PatternColor::default(),
|
PatternColor::default(),
|
||||||
)
|
)
|
||||||
.unwrap();
|
.unwrap();
|
||||||
let doc = printpdf::lopdf::Document::load_mem(&bytes).unwrap();
|
let doc = lopdf::Document::load_mem(&bytes).unwrap();
|
||||||
let catalog = doc.catalog().unwrap();
|
let catalog = doc.catalog().unwrap();
|
||||||
assert!(catalog.has(b"Outlines"), "Lesezeichen fehlen");
|
assert!(catalog.has(b"Outlines"), "Lesezeichen fehlen");
|
||||||
|
|
||||||
@@ -1875,17 +1820,15 @@ mod tests {
|
|||||||
.filter_map(|o| o.as_dict().ok())
|
.filter_map(|o| o.as_dict().ok())
|
||||||
.filter(|d| d.has(b"Parent"))
|
.filter(|d| d.has(b"Parent"))
|
||||||
.filter_map(|d| match d.get(b"Title") {
|
.filter_map(|d| match d.get(b"Title") {
|
||||||
Ok(printpdf::lopdf::Object::String(raw, _)) => {
|
Ok(lopdf::Object::String(raw, _)) => Some(if raw.starts_with(&[0xFE, 0xFF]) {
|
||||||
Some(if raw.starts_with(&[0xFE, 0xFF]) {
|
let units: Vec<u16> = raw[2..]
|
||||||
let units: Vec<u16> = raw[2..]
|
.chunks(2)
|
||||||
.chunks(2)
|
.map(|c| u16::from_be_bytes([c[0], c[1]]))
|
||||||
.map(|c| u16::from_be_bytes([c[0], c[1]]))
|
.collect();
|
||||||
.collect();
|
String::from_utf16(&units).unwrap()
|
||||||
String::from_utf16(&units).unwrap()
|
} else {
|
||||||
} else {
|
String::from_utf8(raw.clone()).unwrap()
|
||||||
String::from_utf8(raw.clone()).unwrap()
|
}),
|
||||||
})
|
|
||||||
}
|
|
||||||
_ => None,
|
_ => None,
|
||||||
})
|
})
|
||||||
.collect();
|
.collect();
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ pub mod page_export;
|
|||||||
pub mod paths;
|
pub mod paths;
|
||||||
pub mod pdf_doc;
|
pub mod pdf_doc;
|
||||||
pub mod pdf_merge;
|
pub mod pdf_merge;
|
||||||
|
pub mod pdf_writer;
|
||||||
pub mod platform;
|
pub mod platform;
|
||||||
pub mod rnote;
|
pub mod rnote;
|
||||||
pub mod ruler;
|
pub mod ruler;
|
||||||
|
|||||||
+3
-3
@@ -1,13 +1,13 @@
|
|||||||
//! Legt beim PDF-Export die Originalseiten importierter PDFs als Vektorgrafik unter die
|
//! Legt beim PDF-Export die Originalseiten importierter PDFs als Vektorgrafik unter die
|
||||||
//! Notizen (Text bleibt scharf und auswählbar).
|
//! Notizen (Text bleibt scharf und auswählbar).
|
||||||
//!
|
//!
|
||||||
//! Vorgehen: Das von printpdf erzeugte PDF wird mit `lopdf` geöffnet. Jede benötigte Seite
|
//! Vorgehen: Das vom Export erzeugte PDF wird mit `lopdf` geöffnet. Jede benötigte Seite
|
||||||
//! des Quell-PDFs wird als Form-XObject (Inhalt + Ressourcen der Seite) übernommen und am
|
//! des Quell-PDFs wird als Form-XObject (Inhalt + Ressourcen der Seite) übernommen und am
|
||||||
//! Anfang der Zielseite gezeichnet, also unter Hintergrund, Bildern und Tinte.
|
//! Anfang der Zielseite gezeichnet, also unter Hintergrund, Bildern und Tinte.
|
||||||
|
|
||||||
use std::collections::{BTreeMap, HashMap};
|
use std::collections::{BTreeMap, HashMap};
|
||||||
|
|
||||||
use printpdf::lopdf::{Dictionary, Document, Object, ObjectId, Stream};
|
use lopdf::{Dictionary, Document, Object, ObjectId, Stream};
|
||||||
|
|
||||||
use crate::t;
|
use crate::t;
|
||||||
|
|
||||||
@@ -361,7 +361,7 @@ mod tests {
|
|||||||
2,
|
2,
|
||||||
"Quellseiten dürfen nicht als eigene Seiten auftauchen"
|
"Quellseiten dürfen nicht als eigene Seiten auftauchen"
|
||||||
);
|
);
|
||||||
let first = doc.get_page_content(pages[0]).unwrap();
|
let first = doc.get_page_content(pages[0]);
|
||||||
let text = String::from_utf8_lossy(&first);
|
let text = String::from_utf8_lossy(&first);
|
||||||
assert!(
|
assert!(
|
||||||
text.starts_with("q "),
|
text.starts_with("q "),
|
||||||
|
|||||||
@@ -0,0 +1,879 @@
|
|||||||
|
//! Small PDF writer on top of `lopdf` for the vector export.
|
||||||
|
//!
|
||||||
|
//! Replaces printpdf 0.7, which still depended on lopdf 0.31. The interface follows the part
|
||||||
|
//! of printpdf that [`crate::export`] used: a document with pages, each page with named
|
||||||
|
//! layers (optional content groups, shown as layers in PDF viewers) that collect drawing
|
||||||
|
//! operations, embedded TrueType fonts for real, searchable text, images and bookmarks.
|
||||||
|
|
||||||
|
use std::cell::RefCell;
|
||||||
|
use std::collections::{BTreeMap, BTreeSet};
|
||||||
|
use std::error::Error;
|
||||||
|
use std::rc::Rc;
|
||||||
|
|
||||||
|
use ab_glyph::Font as _;
|
||||||
|
use lopdf::content::{Content, Operation};
|
||||||
|
use lopdf::{Dictionary, Document, Object, ObjectId, Stream, StringFormat};
|
||||||
|
|
||||||
|
/// Length in millimetres.
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, PartialOrd)]
|
||||||
|
pub struct Mm(pub f32);
|
||||||
|
|
||||||
|
/// Length in PDF points (1/72 inch).
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, PartialOrd)]
|
||||||
|
pub struct Pt(pub f32);
|
||||||
|
|
||||||
|
impl From<Mm> for Pt {
|
||||||
|
fn from(mm: Mm) -> Self {
|
||||||
|
Pt(mm.0 * 72.0 / 25.4)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Size in pixels.
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq)]
|
||||||
|
pub struct Px(pub usize);
|
||||||
|
|
||||||
|
/// Point on the page (origin bottom left).
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq)]
|
||||||
|
pub struct Point {
|
||||||
|
pub x: Pt,
|
||||||
|
pub y: Pt,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Point {
|
||||||
|
pub fn new(x: Mm, y: Mm) -> Self {
|
||||||
|
Self {
|
||||||
|
x: x.into(),
|
||||||
|
y: y.into(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Open or closed polyline; a point marked `true` followed by another marked `true` starts a
|
||||||
|
/// cubic Bézier segment (the two are its control points, the next one its end point).
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct Line {
|
||||||
|
pub points: Vec<(Point, bool)>,
|
||||||
|
pub is_closed: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// How a polygon is painted.
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
|
pub enum PaintMode {
|
||||||
|
Fill,
|
||||||
|
Stroke,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
|
pub enum WindingOrder {
|
||||||
|
NonZero,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Closed shape of one or more rings (same point marking as [`Line`]).
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct Polygon {
|
||||||
|
pub rings: Vec<Vec<(Point, bool)>>,
|
||||||
|
pub mode: PaintMode,
|
||||||
|
pub winding_order: WindingOrder,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq)]
|
||||||
|
pub struct Rgb {
|
||||||
|
pub r: f32,
|
||||||
|
pub g: f32,
|
||||||
|
pub b: f32,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Rgb {
|
||||||
|
/// The last argument (ICC profile in printpdf) is ignored.
|
||||||
|
pub fn new(r: f32, g: f32, b: f32, _icc: Option<()>) -> Self {
|
||||||
|
Self { r, g, b }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq)]
|
||||||
|
pub enum Color {
|
||||||
|
Rgb(Rgb),
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Change of the coordinate system (`cm`).
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq)]
|
||||||
|
pub enum CurTransMat {
|
||||||
|
Translate(Pt, Pt),
|
||||||
|
Raw([f32; 6]),
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Text position within a text section (`Tm`).
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq)]
|
||||||
|
pub enum TextMatrix {
|
||||||
|
Translate(Pt, Pt),
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
|
pub enum LineCapStyle {
|
||||||
|
Butt,
|
||||||
|
Round,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
|
pub enum LineJoinStyle {
|
||||||
|
Miter,
|
||||||
|
Round,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
|
pub enum ColorSpace {
|
||||||
|
Rgb,
|
||||||
|
Greyscale,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Image data already compressed in this format (embedded unchanged).
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
|
pub enum ImageFilter {
|
||||||
|
/// JPEG
|
||||||
|
DCT,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Image with 8 bits per component.
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct ImageXObject {
|
||||||
|
pub width: Px,
|
||||||
|
pub height: Px,
|
||||||
|
pub color_space: ColorSpace,
|
||||||
|
pub interpolate: bool,
|
||||||
|
/// Raw samples, or the compressed data for `image_filter`
|
||||||
|
pub image_data: Vec<u8>,
|
||||||
|
pub image_filter: Option<ImageFilter>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Font embedded with [`PdfDocumentReference::add_external_font`].
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||||
|
pub struct IndirectFontRef {
|
||||||
|
index: usize,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl IndirectFontRef {
|
||||||
|
fn resource_name(&self) -> String {
|
||||||
|
format!("F{}", self.index)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
|
pub struct PdfPageIndex(pub usize);
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
|
pub struct PdfLayerIndex(pub usize);
|
||||||
|
|
||||||
|
struct LayerData {
|
||||||
|
name: String,
|
||||||
|
operations: Vec<Operation>,
|
||||||
|
/// Font of the current text section (to record the glyphs it uses)
|
||||||
|
current_font: Option<usize>,
|
||||||
|
}
|
||||||
|
|
||||||
|
struct PageData {
|
||||||
|
width: Pt,
|
||||||
|
height: Pt,
|
||||||
|
layers: Vec<LayerData>,
|
||||||
|
images: Vec<ImageXObject>,
|
||||||
|
}
|
||||||
|
|
||||||
|
struct FontData {
|
||||||
|
face: ab_glyph::FontRef<'static>,
|
||||||
|
bytes: &'static [u8],
|
||||||
|
used_glyphs: BTreeSet<u16>,
|
||||||
|
}
|
||||||
|
|
||||||
|
struct DocumentData {
|
||||||
|
title: String,
|
||||||
|
pages: Vec<PageData>,
|
||||||
|
fonts: Vec<FontData>,
|
||||||
|
bookmarks: Vec<(String, usize)>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Entry point like printpdf's `PdfDocument::new`.
|
||||||
|
pub struct PdfDocument;
|
||||||
|
|
||||||
|
impl PdfDocument {
|
||||||
|
/// New document with a first page of the given size and its first layer.
|
||||||
|
#[allow(clippy::new_ret_no_self)]
|
||||||
|
pub fn new(
|
||||||
|
title: &str,
|
||||||
|
width: Mm,
|
||||||
|
height: Mm,
|
||||||
|
layer: &str,
|
||||||
|
) -> (PdfDocumentReference, PdfPageIndex, PdfLayerIndex) {
|
||||||
|
let doc = PdfDocumentReference(Rc::new(RefCell::new(DocumentData {
|
||||||
|
title: title.to_string(),
|
||||||
|
pages: Vec::new(),
|
||||||
|
fonts: Vec::new(),
|
||||||
|
bookmarks: Vec::new(),
|
||||||
|
})));
|
||||||
|
let (page, layer) = doc.add_page(width, height, layer);
|
||||||
|
(doc, page, layer)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The document being built; cheap to clone, all clones share it.
|
||||||
|
#[derive(Clone)]
|
||||||
|
pub struct PdfDocumentReference(Rc<RefCell<DocumentData>>);
|
||||||
|
|
||||||
|
impl PdfDocumentReference {
|
||||||
|
/// Appends a page with its first layer.
|
||||||
|
pub fn add_page(&self, width: Mm, height: Mm, layer: &str) -> (PdfPageIndex, PdfLayerIndex) {
|
||||||
|
let mut doc = self.0.borrow_mut();
|
||||||
|
doc.pages.push(PageData {
|
||||||
|
width: width.into(),
|
||||||
|
height: height.into(),
|
||||||
|
layers: vec![LayerData {
|
||||||
|
name: layer.to_string(),
|
||||||
|
operations: Vec::new(),
|
||||||
|
current_font: None,
|
||||||
|
}],
|
||||||
|
images: Vec::new(),
|
||||||
|
});
|
||||||
|
(PdfPageIndex(doc.pages.len() - 1), PdfLayerIndex(0))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Bookmark (outline entry) pointing at a page.
|
||||||
|
pub fn add_bookmark(&self, title: String, page: PdfPageIndex) {
|
||||||
|
self.0.borrow_mut().bookmarks.push((title, page.0));
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_page(&self, page: PdfPageIndex) -> PdfPageReference {
|
||||||
|
PdfPageReference {
|
||||||
|
doc: self.clone(),
|
||||||
|
page: page.0,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Embeds a TrueType/OpenType font with TrueType outlines (Type0 font, Identity-H).
|
||||||
|
pub fn add_external_font(
|
||||||
|
&self,
|
||||||
|
bytes: &'static [u8],
|
||||||
|
) -> Result<IndirectFontRef, Box<dyn Error>> {
|
||||||
|
let face = ab_glyph::FontRef::try_from_slice(bytes)?;
|
||||||
|
let mut doc = self.0.borrow_mut();
|
||||||
|
doc.fonts.push(FontData {
|
||||||
|
face,
|
||||||
|
bytes,
|
||||||
|
used_glyphs: BTreeSet::new(),
|
||||||
|
});
|
||||||
|
Ok(IndirectFontRef {
|
||||||
|
index: doc.fonts.len() - 1,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Writes the document.
|
||||||
|
pub fn save_to_bytes(self) -> Result<Vec<u8>, Box<dyn Error>> {
|
||||||
|
let data = self.0.borrow();
|
||||||
|
let mut doc = Document::with_version("1.5");
|
||||||
|
let pages_id = doc.new_object_id();
|
||||||
|
|
||||||
|
// Fonts: shared by all pages
|
||||||
|
let mut font_resources = Dictionary::new();
|
||||||
|
for (index, font) in data.fonts.iter().enumerate() {
|
||||||
|
let id = write_font(&mut doc, font, index)?;
|
||||||
|
font_resources.set(format!("F{index}"), Object::Reference(id));
|
||||||
|
}
|
||||||
|
|
||||||
|
// One optional content group per layer name, in the order of appearance
|
||||||
|
let mut layer_names: Vec<&str> = Vec::new();
|
||||||
|
for layer in data.pages.iter().flat_map(|p| &p.layers) {
|
||||||
|
if !layer_names.contains(&layer.name.as_str()) {
|
||||||
|
layer_names.push(&layer.name);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let ocgs: Vec<ObjectId> = layer_names
|
||||||
|
.iter()
|
||||||
|
.map(|name| {
|
||||||
|
let mut ocg = Dictionary::new();
|
||||||
|
ocg.set("Type", Object::Name(b"OCG".to_vec()));
|
||||||
|
ocg.set("Name", text_string(name));
|
||||||
|
doc.add_object(ocg)
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
let mut page_ids = Vec::new();
|
||||||
|
for page in &data.pages {
|
||||||
|
let mut operations = Vec::new();
|
||||||
|
let mut properties = Dictionary::new();
|
||||||
|
for layer in &page.layers {
|
||||||
|
let index = layer_names
|
||||||
|
.iter()
|
||||||
|
.position(|n| *n == layer.name)
|
||||||
|
.unwrap_or(0);
|
||||||
|
let tag = format!("MC{index}");
|
||||||
|
properties.set(tag.clone(), Object::Reference(ocgs[index]));
|
||||||
|
operations.push(Operation::new(
|
||||||
|
"BDC",
|
||||||
|
vec![Object::Name(b"OC".to_vec()), Object::Name(tag.into_bytes())],
|
||||||
|
));
|
||||||
|
operations.push(Operation::new("q", vec![]));
|
||||||
|
operations.extend(layer.operations.iter().cloned());
|
||||||
|
operations.push(Operation::new("Q", vec![]));
|
||||||
|
operations.push(Operation::new("EMC", vec![]));
|
||||||
|
}
|
||||||
|
let mut contents = Stream::new(Dictionary::new(), Content { operations }.encode()?);
|
||||||
|
contents.compress()?;
|
||||||
|
let contents_id = doc.add_object(contents);
|
||||||
|
|
||||||
|
let mut xobjects = Dictionary::new();
|
||||||
|
for (i, image) in page.images.iter().enumerate() {
|
||||||
|
let id = doc.add_object(image_stream(image)?);
|
||||||
|
xobjects.set(format!("X{i}"), Object::Reference(id));
|
||||||
|
}
|
||||||
|
let mut resources = Dictionary::new();
|
||||||
|
resources.set("Font", Object::Dictionary(font_resources.clone()));
|
||||||
|
resources.set("XObject", Object::Dictionary(xobjects));
|
||||||
|
resources.set("Properties", Object::Dictionary(properties));
|
||||||
|
let resources_id = doc.add_object(resources);
|
||||||
|
|
||||||
|
let mut dict = Dictionary::new();
|
||||||
|
dict.set("Type", Object::Name(b"Page".to_vec()));
|
||||||
|
dict.set("Parent", Object::Reference(pages_id));
|
||||||
|
dict.set(
|
||||||
|
"MediaBox",
|
||||||
|
Object::Array(vec![
|
||||||
|
0.into(),
|
||||||
|
0.into(),
|
||||||
|
page.width.0.into(),
|
||||||
|
page.height.0.into(),
|
||||||
|
]),
|
||||||
|
);
|
||||||
|
dict.set("Contents", Object::Reference(contents_id));
|
||||||
|
dict.set("Resources", Object::Reference(resources_id));
|
||||||
|
page_ids.push(doc.add_object(dict));
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut pages = Dictionary::new();
|
||||||
|
pages.set("Type", Object::Name(b"Pages".to_vec()));
|
||||||
|
pages.set(
|
||||||
|
"Kids",
|
||||||
|
Object::Array(page_ids.iter().map(|id| Object::Reference(*id)).collect()),
|
||||||
|
);
|
||||||
|
pages.set("Count", Object::Integer(page_ids.len() as i64));
|
||||||
|
doc.objects.insert(pages_id, Object::Dictionary(pages));
|
||||||
|
|
||||||
|
let mut catalog = Dictionary::new();
|
||||||
|
catalog.set("Type", Object::Name(b"Catalog".to_vec()));
|
||||||
|
catalog.set("Pages", Object::Reference(pages_id));
|
||||||
|
let ocg_refs: Vec<Object> = ocgs.iter().map(|id| Object::Reference(*id)).collect();
|
||||||
|
let mut default_config = Dictionary::new();
|
||||||
|
default_config.set("Order", Object::Array(ocg_refs.clone()));
|
||||||
|
let mut oc_properties = Dictionary::new();
|
||||||
|
oc_properties.set("OCGs", Object::Array(ocg_refs));
|
||||||
|
oc_properties.set("D", Object::Dictionary(default_config));
|
||||||
|
catalog.set("OCProperties", Object::Dictionary(oc_properties));
|
||||||
|
if !data.bookmarks.is_empty() {
|
||||||
|
let outlines = write_outlines(&mut doc, &data.bookmarks, &page_ids);
|
||||||
|
catalog.set("Outlines", Object::Reference(outlines));
|
||||||
|
catalog.set("PageMode", Object::Name(b"UseOutlines".to_vec()));
|
||||||
|
}
|
||||||
|
let catalog_id = doc.add_object(catalog);
|
||||||
|
|
||||||
|
let mut info = Dictionary::new();
|
||||||
|
info.set("Title", text_string(&data.title));
|
||||||
|
info.set("Producer", text_string("ink-notes"));
|
||||||
|
let info_id = doc.add_object(info);
|
||||||
|
|
||||||
|
doc.trailer.set("Root", Object::Reference(catalog_id));
|
||||||
|
doc.trailer.set("Info", Object::Reference(info_id));
|
||||||
|
let mut out = Vec::new();
|
||||||
|
doc.save_to(&mut out)?;
|
||||||
|
Ok(out)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// PDF text string: literal for ASCII, otherwise UTF-16BE with byte order mark.
|
||||||
|
pub fn text_string(text: &str) -> Object {
|
||||||
|
if text.is_ascii() {
|
||||||
|
return Object::String(text.as_bytes().to_vec(), StringFormat::Literal);
|
||||||
|
}
|
||||||
|
let mut bytes = vec![0xFE, 0xFF];
|
||||||
|
for unit in text.encode_utf16() {
|
||||||
|
bytes.extend_from_slice(&unit.to_be_bytes());
|
||||||
|
}
|
||||||
|
Object::String(bytes, StringFormat::Hexadecimal)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Outline tree with one entry per bookmark; returns the outline root.
|
||||||
|
fn write_outlines(
|
||||||
|
doc: &mut Document,
|
||||||
|
bookmarks: &[(String, usize)],
|
||||||
|
pages: &[ObjectId],
|
||||||
|
) -> ObjectId {
|
||||||
|
let root_id = doc.new_object_id();
|
||||||
|
let ids: Vec<ObjectId> = bookmarks.iter().map(|_| doc.new_object_id()).collect();
|
||||||
|
for (i, (title, page)) in bookmarks.iter().enumerate() {
|
||||||
|
let mut item = Dictionary::new();
|
||||||
|
item.set("Title", text_string(title));
|
||||||
|
item.set("Parent", Object::Reference(root_id));
|
||||||
|
if i > 0 {
|
||||||
|
item.set("Prev", Object::Reference(ids[i - 1]));
|
||||||
|
}
|
||||||
|
if let Some(next) = ids.get(i + 1) {
|
||||||
|
item.set("Next", Object::Reference(*next));
|
||||||
|
}
|
||||||
|
if let Some(page_id) = pages.get(*page) {
|
||||||
|
item.set(
|
||||||
|
"Dest",
|
||||||
|
Object::Array(vec![
|
||||||
|
Object::Reference(*page_id),
|
||||||
|
Object::Name(b"Fit".to_vec()),
|
||||||
|
]),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
doc.objects.insert(ids[i], Object::Dictionary(item));
|
||||||
|
}
|
||||||
|
let mut root = Dictionary::new();
|
||||||
|
root.set("Type", Object::Name(b"Outlines".to_vec()));
|
||||||
|
root.set("First", Object::Reference(ids[0]));
|
||||||
|
root.set("Last", Object::Reference(ids[ids.len() - 1]));
|
||||||
|
root.set("Count", Object::Integer(ids.len() as i64));
|
||||||
|
doc.objects.insert(root_id, Object::Dictionary(root));
|
||||||
|
root_id
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Image XObject stream.
|
||||||
|
fn image_stream(image: &ImageXObject) -> Result<Stream, Box<dyn Error>> {
|
||||||
|
let mut dict = Dictionary::new();
|
||||||
|
dict.set("Type", Object::Name(b"XObject".to_vec()));
|
||||||
|
dict.set("Subtype", Object::Name(b"Image".to_vec()));
|
||||||
|
dict.set("Width", Object::Integer(image.width.0 as i64));
|
||||||
|
dict.set("Height", Object::Integer(image.height.0 as i64));
|
||||||
|
let color_space: &[u8] = match image.color_space {
|
||||||
|
ColorSpace::Rgb => b"DeviceRGB",
|
||||||
|
ColorSpace::Greyscale => b"DeviceGray",
|
||||||
|
};
|
||||||
|
dict.set("ColorSpace", Object::Name(color_space.to_vec()));
|
||||||
|
dict.set("BitsPerComponent", Object::Integer(8));
|
||||||
|
dict.set("Interpolate", Object::Boolean(image.interpolate));
|
||||||
|
match image.image_filter {
|
||||||
|
Some(ImageFilter::DCT) => {
|
||||||
|
dict.set("Filter", Object::Name(b"DCTDecode".to_vec()));
|
||||||
|
Ok(Stream::new(dict, image.image_data.clone()).with_compression(false))
|
||||||
|
}
|
||||||
|
None => {
|
||||||
|
let mut stream = Stream::new(dict, image.image_data.clone());
|
||||||
|
stream.compress()?;
|
||||||
|
Ok(stream)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Embeds a font as Type0 font with a CIDFontType2 descendant (glyph ids as character codes),
|
||||||
|
/// widths and a ToUnicode map of the glyphs used, so that the text can be searched and copied.
|
||||||
|
fn write_font(
|
||||||
|
doc: &mut Document,
|
||||||
|
font: &FontData,
|
||||||
|
index: usize,
|
||||||
|
) -> Result<ObjectId, Box<dyn Error>> {
|
||||||
|
let face = &font.face;
|
||||||
|
let units_per_em = face.units_per_em().unwrap_or(1000.0);
|
||||||
|
let scale = 1000.0 / units_per_em;
|
||||||
|
let name = format!("InkNotesFont{index}");
|
||||||
|
|
||||||
|
let mut file = Stream::new(
|
||||||
|
Dictionary::from_iter(vec![("Length1", Object::Integer(font.bytes.len() as i64))]),
|
||||||
|
font.bytes.to_vec(),
|
||||||
|
);
|
||||||
|
file.compress()?;
|
||||||
|
let file_id = doc.add_object(file);
|
||||||
|
|
||||||
|
let ascent = (face.ascent_unscaled() * scale).round() as i64;
|
||||||
|
let descent = (face.descent_unscaled() * scale).round() as i64;
|
||||||
|
let mut descriptor = Dictionary::new();
|
||||||
|
descriptor.set("Type", Object::Name(b"FontDescriptor".to_vec()));
|
||||||
|
descriptor.set("FontName", Object::Name(name.clone().into_bytes()));
|
||||||
|
descriptor.set("Flags", Object::Integer(32));
|
||||||
|
descriptor.set(
|
||||||
|
"FontBBox",
|
||||||
|
Object::Array(vec![0.into(), descent.into(), 1000.into(), ascent.into()]),
|
||||||
|
);
|
||||||
|
descriptor.set("ItalicAngle", Object::Integer(0));
|
||||||
|
descriptor.set("Ascent", Object::Integer(ascent));
|
||||||
|
descriptor.set("Descent", Object::Integer(descent));
|
||||||
|
descriptor.set("CapHeight", Object::Integer(ascent));
|
||||||
|
descriptor.set("StemV", Object::Integer(80));
|
||||||
|
descriptor.set("FontFile2", Object::Reference(file_id));
|
||||||
|
let descriptor_id = doc.add_object(descriptor);
|
||||||
|
|
||||||
|
// Widths of the glyphs used: gid [width]
|
||||||
|
let mut widths = Vec::new();
|
||||||
|
for &gid in &font.used_glyphs {
|
||||||
|
let width = face.h_advance_unscaled(ab_glyph::GlyphId(gid)) * scale;
|
||||||
|
widths.push(Object::Integer(gid as i64));
|
||||||
|
widths.push(Object::Array(vec![Object::Integer(width.round() as i64)]));
|
||||||
|
}
|
||||||
|
let mut system_info = Dictionary::new();
|
||||||
|
system_info.set("Registry", Object::string_literal("Adobe"));
|
||||||
|
system_info.set("Ordering", Object::string_literal("Identity"));
|
||||||
|
system_info.set("Supplement", Object::Integer(0));
|
||||||
|
let mut descendant = Dictionary::new();
|
||||||
|
descendant.set("Type", Object::Name(b"Font".to_vec()));
|
||||||
|
descendant.set("Subtype", Object::Name(b"CIDFontType2".to_vec()));
|
||||||
|
descendant.set("BaseFont", Object::Name(name.clone().into_bytes()));
|
||||||
|
descendant.set("CIDSystemInfo", Object::Dictionary(system_info));
|
||||||
|
descendant.set("FontDescriptor", Object::Reference(descriptor_id));
|
||||||
|
descendant.set("CIDToGIDMap", Object::Name(b"Identity".to_vec()));
|
||||||
|
descendant.set("DW", Object::Integer(1000));
|
||||||
|
descendant.set("W", Object::Array(widths));
|
||||||
|
let descendant_id = doc.add_object(descendant);
|
||||||
|
|
||||||
|
let mut to_unicode = Stream::new(Dictionary::new(), to_unicode_cmap(font).into_bytes());
|
||||||
|
to_unicode.compress()?;
|
||||||
|
let to_unicode_id = doc.add_object(to_unicode);
|
||||||
|
|
||||||
|
let mut dict = Dictionary::new();
|
||||||
|
dict.set("Type", Object::Name(b"Font".to_vec()));
|
||||||
|
dict.set("Subtype", Object::Name(b"Type0".to_vec()));
|
||||||
|
dict.set("BaseFont", Object::Name(name.into_bytes()));
|
||||||
|
dict.set("Encoding", Object::Name(b"Identity-H".to_vec()));
|
||||||
|
dict.set(
|
||||||
|
"DescendantFonts",
|
||||||
|
Object::Array(vec![Object::Reference(descendant_id)]),
|
||||||
|
);
|
||||||
|
dict.set("ToUnicode", Object::Reference(to_unicode_id));
|
||||||
|
Ok(doc.add_object(dict))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// CMap from the glyph ids used to their characters (UTF-16BE).
|
||||||
|
fn to_unicode_cmap(font: &FontData) -> String {
|
||||||
|
let mut chars: BTreeMap<u16, char> = BTreeMap::new();
|
||||||
|
for (gid, c) in font.face.codepoint_ids() {
|
||||||
|
if font.used_glyphs.contains(&gid.0) {
|
||||||
|
// Prefer the lowest code point (e.g. a letter over a compatibility form)
|
||||||
|
let entry = chars.entry(gid.0).or_insert(c);
|
||||||
|
if c < *entry {
|
||||||
|
*entry = c;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let mut cmap = String::from(
|
||||||
|
"/CIDInit /ProcSet findresource begin\n12 dict begin\nbegincmap\n\
|
||||||
|
/CIDSystemInfo << /Registry (Adobe) /Ordering (UCS) /Supplement 0 >> def\n\
|
||||||
|
/CMapName /Adobe-Identity-UCS def\n/CMapType 2 def\n\
|
||||||
|
1 begincodespacerange\n<0000> <FFFF>\nendcodespacerange\n",
|
||||||
|
);
|
||||||
|
let entries: Vec<(u16, char)> = chars.into_iter().collect();
|
||||||
|
for block in entries.chunks(100) {
|
||||||
|
cmap.push_str(&format!("{} beginbfchar\n", block.len()));
|
||||||
|
for (gid, c) in block {
|
||||||
|
let mut units = [0u16; 2];
|
||||||
|
let hex: String = c
|
||||||
|
.encode_utf16(&mut units)
|
||||||
|
.iter()
|
||||||
|
.map(|u| format!("{u:04X}"))
|
||||||
|
.collect();
|
||||||
|
cmap.push_str(&format!("<{gid:04X}> <{hex}>\n"));
|
||||||
|
}
|
||||||
|
cmap.push_str("endbfchar\n");
|
||||||
|
}
|
||||||
|
cmap.push_str("endcmap\nCMapName currentdict /CMap defineresource pop\nend\nend\n");
|
||||||
|
cmap
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct PdfPageReference {
|
||||||
|
doc: PdfDocumentReference,
|
||||||
|
page: usize,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl PdfPageReference {
|
||||||
|
pub fn get_layer(&self, layer: PdfLayerIndex) -> PdfLayerReference {
|
||||||
|
PdfLayerReference {
|
||||||
|
doc: self.doc.clone(),
|
||||||
|
page: self.page,
|
||||||
|
layer: layer.0,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Adds a layer drawn above the existing ones.
|
||||||
|
pub fn add_layer(&self, name: &str) -> PdfLayerReference {
|
||||||
|
let mut doc = self.doc.0.borrow_mut();
|
||||||
|
let layers = &mut doc.pages[self.page].layers;
|
||||||
|
layers.push(LayerData {
|
||||||
|
name: name.to_string(),
|
||||||
|
operations: Vec::new(),
|
||||||
|
current_font: None,
|
||||||
|
});
|
||||||
|
PdfLayerReference {
|
||||||
|
doc: self.doc.clone(),
|
||||||
|
page: self.page,
|
||||||
|
layer: layers.len() - 1,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A layer of a page; all drawing goes through it.
|
||||||
|
#[derive(Clone)]
|
||||||
|
pub struct PdfLayerReference {
|
||||||
|
doc: PdfDocumentReference,
|
||||||
|
page: usize,
|
||||||
|
layer: usize,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl PdfLayerReference {
|
||||||
|
fn with_layer<T>(&self, f: impl FnOnce(&mut LayerData) -> T) -> T {
|
||||||
|
let mut doc = self.doc.0.borrow_mut();
|
||||||
|
f(&mut doc.pages[self.page].layers[self.layer])
|
||||||
|
}
|
||||||
|
|
||||||
|
fn push(&self, operator: &str, operands: Vec<Object>) {
|
||||||
|
self.add_operation(Operation::new(operator, operands));
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn add_operation(&self, operation: Operation) {
|
||||||
|
self.with_layer(|layer| layer.operations.push(operation));
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn save_graphics_state(&self) {
|
||||||
|
self.push("q", vec![]);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn restore_graphics_state(&self) {
|
||||||
|
self.push("Q", vec![]);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn set_ctm(&self, matrix: CurTransMat) {
|
||||||
|
let m = match matrix {
|
||||||
|
CurTransMat::Translate(x, y) => [1.0, 0.0, 0.0, 1.0, x.0, y.0],
|
||||||
|
CurTransMat::Raw(m) => m,
|
||||||
|
};
|
||||||
|
self.push("cm", m.iter().map(|&v| v.into()).collect());
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn set_fill_color(&self, color: Color) {
|
||||||
|
let Color::Rgb(c) = color;
|
||||||
|
self.push("rg", vec![c.r.into(), c.g.into(), c.b.into()]);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn set_outline_color(&self, color: Color) {
|
||||||
|
let Color::Rgb(c) = color;
|
||||||
|
self.push("RG", vec![c.r.into(), c.g.into(), c.b.into()]);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn set_outline_thickness(&self, thickness: f32) {
|
||||||
|
self.push("w", vec![thickness.into()]);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn set_line_cap_style(&self, style: LineCapStyle) {
|
||||||
|
let value = match style {
|
||||||
|
LineCapStyle::Butt => 0,
|
||||||
|
LineCapStyle::Round => 1,
|
||||||
|
};
|
||||||
|
self.push("J", vec![Object::Integer(value)]);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn set_line_join_style(&self, style: LineJoinStyle) {
|
||||||
|
let value = match style {
|
||||||
|
LineJoinStyle::Miter => 0,
|
||||||
|
LineJoinStyle::Round => 1,
|
||||||
|
};
|
||||||
|
self.push("j", vec![Object::Integer(value)]);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn add_line(&self, line: Line) {
|
||||||
|
if line.points.is_empty() {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let mut operations = path_operations(&line.points);
|
||||||
|
operations.push(Operation::new(
|
||||||
|
if line.is_closed { "s" } else { "S" },
|
||||||
|
vec![],
|
||||||
|
));
|
||||||
|
self.with_layer(|layer| layer.operations.extend(operations));
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn add_polygon(&self, polygon: Polygon) {
|
||||||
|
let mut operations: Vec<Operation> = polygon
|
||||||
|
.rings
|
||||||
|
.iter()
|
||||||
|
.filter(|ring| !ring.is_empty())
|
||||||
|
.flat_map(|ring| path_operations(ring))
|
||||||
|
.collect();
|
||||||
|
if operations.is_empty() {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
operations.push(Operation::new(
|
||||||
|
match polygon.mode {
|
||||||
|
PaintMode::Fill => "f",
|
||||||
|
PaintMode::Stroke => "s",
|
||||||
|
},
|
||||||
|
vec![],
|
||||||
|
));
|
||||||
|
self.with_layer(|layer| layer.operations.extend(operations));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Draws `image` into the unit square of the current coordinate system.
|
||||||
|
pub fn add_image(&self, image: ImageXObject) {
|
||||||
|
let mut doc = self.doc.0.borrow_mut();
|
||||||
|
let page = &mut doc.pages[self.page];
|
||||||
|
page.images.push(image);
|
||||||
|
let name = format!("X{}", page.images.len() - 1);
|
||||||
|
page.layers[self.layer]
|
||||||
|
.operations
|
||||||
|
.push(Operation::new("Do", vec![Object::Name(name.into_bytes())]));
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn begin_text_section(&self) {
|
||||||
|
self.push("BT", vec![]);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn end_text_section(&self) {
|
||||||
|
self.push("ET", vec![]);
|
||||||
|
self.with_layer(|layer| layer.current_font = None);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn set_font(&self, font: &IndirectFontRef, size: f32) {
|
||||||
|
self.push(
|
||||||
|
"Tf",
|
||||||
|
vec![Object::Name(font.resource_name().into_bytes()), size.into()],
|
||||||
|
);
|
||||||
|
self.with_layer(|layer| layer.current_font = Some(font.index));
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn set_text_matrix(&self, matrix: TextMatrix) {
|
||||||
|
let TextMatrix::Translate(x, y) = matrix;
|
||||||
|
self.push(
|
||||||
|
"Tm",
|
||||||
|
vec![
|
||||||
|
1.into(),
|
||||||
|
0.into(),
|
||||||
|
0.into(),
|
||||||
|
1.into(),
|
||||||
|
x.0.into(),
|
||||||
|
y.0.into(),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Shows glyphs of the current font; each comes with a shift before it in 1/1000 em
|
||||||
|
/// (positive moves left, as in `TJ`).
|
||||||
|
pub fn write_positioned_codepoints(&self, glyphs: impl IntoIterator<Item = (i64, u16)>) {
|
||||||
|
let mut list = Vec::new();
|
||||||
|
let mut used = Vec::new();
|
||||||
|
for (shift, gid) in glyphs {
|
||||||
|
if shift != 0 {
|
||||||
|
list.push(Object::Integer(shift));
|
||||||
|
}
|
||||||
|
list.push(Object::String(
|
||||||
|
gid.to_be_bytes().to_vec(),
|
||||||
|
StringFormat::Hexadecimal,
|
||||||
|
));
|
||||||
|
used.push(gid);
|
||||||
|
}
|
||||||
|
let mut doc = self.doc.0.borrow_mut();
|
||||||
|
let layer = &mut doc.pages[self.page].layers[self.layer];
|
||||||
|
layer
|
||||||
|
.operations
|
||||||
|
.push(Operation::new("TJ", vec![Object::Array(list)]));
|
||||||
|
if let Some(font) = layer.current_font {
|
||||||
|
doc.fonts[font].used_glyphs.extend(used);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// `m`, `l` and `c` operations for a point list (see [`Line`] for the Bézier marking).
|
||||||
|
fn path_operations(points: &[(Point, bool)]) -> Vec<Operation> {
|
||||||
|
let xy = |p: &Point| -> [Object; 2] { [p.x.0.into(), p.y.0.into()] };
|
||||||
|
let mut operations = vec![Operation::new("m", xy(&points[0].0).to_vec())];
|
||||||
|
let mut current = 1;
|
||||||
|
while current < points.len() {
|
||||||
|
let (previous, point) = (&points[current - 1], &points[current]);
|
||||||
|
if previous.1 && point.1 {
|
||||||
|
if let (Some(c2), Some(end)) = (points.get(current + 1), points.get(current + 2)) {
|
||||||
|
let operands = [xy(&point.0), xy(&c2.0), xy(&end.0)].concat();
|
||||||
|
operations.push(Operation::new("c", operands));
|
||||||
|
current += 3;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
operations.push(Operation::new("l", xy(&point.0).to_vec()));
|
||||||
|
current += 1;
|
||||||
|
}
|
||||||
|
operations
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
fn line(points: &[(f32, f32, bool)]) -> Vec<(Point, bool)> {
|
||||||
|
points
|
||||||
|
.iter()
|
||||||
|
.map(|&(x, y, b)| (Point::new(Mm(x), Mm(y)), b))
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_path_operations_lines_and_curves() {
|
||||||
|
let ops = path_operations(&line(&[
|
||||||
|
(0.0, 0.0, true),
|
||||||
|
(1.0, 0.0, true),
|
||||||
|
(2.0, 0.0, false),
|
||||||
|
(3.0, 0.0, false),
|
||||||
|
(4.0, 0.0, false),
|
||||||
|
]));
|
||||||
|
let names: Vec<&str> = ops.iter().map(|o| o.operator.as_str()).collect();
|
||||||
|
assert_eq!(names, ["m", "c", "l"]);
|
||||||
|
assert_eq!(ops[1].operands.len(), 6);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_document_with_text_image_and_bookmarks() {
|
||||||
|
let (doc, page, layer) = PdfDocument::new("Test", Mm(210.0), Mm(297.0), "Background");
|
||||||
|
let font = doc
|
||||||
|
.add_external_font(crate::text::TextFont::Sans.bytes())
|
||||||
|
.unwrap();
|
||||||
|
doc.add_bookmark("Übung – 1".to_string(), page);
|
||||||
|
let background = doc.get_page(page).get_layer(layer);
|
||||||
|
background.add_polygon(Polygon {
|
||||||
|
rings: vec![line(&[
|
||||||
|
(0.0, 0.0, false),
|
||||||
|
(10.0, 0.0, false),
|
||||||
|
(10.0, 10.0, false),
|
||||||
|
])],
|
||||||
|
mode: PaintMode::Fill,
|
||||||
|
winding_order: WindingOrder::NonZero,
|
||||||
|
});
|
||||||
|
let text = doc.get_page(page).add_layer("Text");
|
||||||
|
let face = crate::text::TextFont::Sans.face();
|
||||||
|
let gids: Vec<u16> = "Hä".chars().map(|c| face.glyph_id(c).0).collect();
|
||||||
|
text.begin_text_section();
|
||||||
|
text.set_font(&font, 12.0);
|
||||||
|
text.write_positioned_codepoints(gids.iter().map(|&g| (0, g)));
|
||||||
|
text.end_text_section();
|
||||||
|
text.add_image(ImageXObject {
|
||||||
|
width: Px(1),
|
||||||
|
height: Px(1),
|
||||||
|
color_space: ColorSpace::Rgb,
|
||||||
|
interpolate: true,
|
||||||
|
image_data: vec![255, 0, 0],
|
||||||
|
image_filter: None,
|
||||||
|
});
|
||||||
|
let bytes = doc.save_to_bytes().unwrap();
|
||||||
|
|
||||||
|
let pdf = Document::load_mem(&bytes).unwrap();
|
||||||
|
assert_eq!(pdf.get_pages().len(), 1);
|
||||||
|
let page_id = *pdf.get_pages().values().next().unwrap();
|
||||||
|
let content = pdf.get_page_content(page_id);
|
||||||
|
let ops = Content::decode(&content).unwrap().operations;
|
||||||
|
let names: Vec<&str> = ops.iter().map(|o| o.operator.as_str()).collect();
|
||||||
|
for expected in ["BDC", "f", "BT", "Tf", "TJ", "ET", "Do", "EMC"] {
|
||||||
|
assert!(names.contains(&expected), "{expected} missing in {names:?}");
|
||||||
|
}
|
||||||
|
// The ToUnicode map knows both characters
|
||||||
|
let fonts = pdf.get_page_fonts(page_id).unwrap();
|
||||||
|
let font = fonts.values().next().unwrap();
|
||||||
|
let to_unicode = pdf
|
||||||
|
.get_object(font.get(b"ToUnicode").unwrap().as_reference().unwrap())
|
||||||
|
.unwrap()
|
||||||
|
.as_stream()
|
||||||
|
.unwrap()
|
||||||
|
.decompressed_content()
|
||||||
|
.unwrap();
|
||||||
|
let cmap = String::from_utf8(to_unicode).unwrap();
|
||||||
|
assert!(cmap.contains("<0048>"), "{cmap}");
|
||||||
|
assert!(cmap.contains("<00E4>"), "{cmap}");
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in new issue
Block a user