mirror of
https://github.com/storytold/craft-libs.git
synced 2026-11-13 22:12:12 +00:00
Merge pull request #1174 from dylanmtaylor/codex/vector-file-imports
Add shared vector path import APIs
This commit is contained in:
12 files changed
+1144
-9
No files matched your search
Generated
+363
-9
@@ -1394,6 +1394,7 @@ version = "0.1.0"
|
||||
dependencies = [
|
||||
"craft-codepage",
|
||||
"encoding_rs",
|
||||
"kurbo 0.13.1",
|
||||
"log",
|
||||
"thiserror 2.0.21",
|
||||
]
|
||||
@@ -1410,6 +1411,12 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "craft-eps"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"kurbo 0.13.1",
|
||||
"thiserror 2.0.21",
|
||||
"vectorcraft-doc",
|
||||
"vectorcraft-eps",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "craft-epub"
|
||||
@@ -1971,6 +1978,17 @@ dependencies = [
|
||||
"zeroize",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "craft-pdf"
|
||||
version = "0.1.0"
|
||||
source = "git+https://github.com/storytold/craft-libs?rev=92d392cff132eaf53e13afff77f38b5c11b73aa0#92d392cff132eaf53e13afff77f38b5c11b73aa0"
|
||||
dependencies = [
|
||||
"hayro-interpret",
|
||||
"hayro-syntax",
|
||||
"krilla",
|
||||
"thiserror 2.0.21",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "craft-pdn"
|
||||
version = "0.1.0"
|
||||
@@ -2027,7 +2045,7 @@ name = "craft-plate-test"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"craft-cms",
|
||||
"craft-pdf",
|
||||
"craft-pdf 0.1.0",
|
||||
"hayro-syntax",
|
||||
"serde",
|
||||
"serde_json",
|
||||
@@ -2088,7 +2106,7 @@ name = "craft-prepress"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"craft-cms",
|
||||
"craft-pdf",
|
||||
"craft-pdf 0.1.0",
|
||||
"craft-plate-test",
|
||||
"kurbo 0.13.1",
|
||||
"linesweeper",
|
||||
@@ -3497,7 +3515,7 @@ dependencies = [
|
||||
"emath",
|
||||
"epaint_default_fonts",
|
||||
"font-types 0.12.7",
|
||||
"harfrust",
|
||||
"harfrust 0.12.0",
|
||||
"log",
|
||||
"nohash-hasher",
|
||||
"parking_lot",
|
||||
@@ -3725,6 +3743,12 @@ version = "1.0.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1"
|
||||
|
||||
[[package]]
|
||||
name = "foldhash"
|
||||
version = "0.1.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2"
|
||||
|
||||
[[package]]
|
||||
name = "foldhash"
|
||||
version = "0.2.0"
|
||||
@@ -4023,7 +4047,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d99fc21d493812643aae86d53b7bbd02f376434a90317e8a790bc209fdd6605e"
|
||||
dependencies = [
|
||||
"bytemuck",
|
||||
"foldhash",
|
||||
"foldhash 0.2.0",
|
||||
"hashbrown 0.17.1",
|
||||
"log",
|
||||
"peniko",
|
||||
@@ -4040,7 +4064,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "282a26c1e23de04bdab3e34a21b6f877a479b96737792516b9b8b8f69b6661be"
|
||||
dependencies = [
|
||||
"bytemuck",
|
||||
"foldhash",
|
||||
"foldhash 0.2.0",
|
||||
"hashbrown 0.17.1",
|
||||
"log",
|
||||
"peniko",
|
||||
@@ -4215,6 +4239,28 @@ dependencies = [
|
||||
"smallvec",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "harfrust"
|
||||
version = "0.13.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "948d0741125ba89cd3e1c23e5642415b6ade7e1d29d67ba25fb925b533e989d6"
|
||||
dependencies = [
|
||||
"bitflags 2.13.2",
|
||||
"bytemuck",
|
||||
"once_cell",
|
||||
"read-fonts 0.43.3",
|
||||
"smallvec",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "hashbrown"
|
||||
version = "0.15.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1"
|
||||
dependencies = [
|
||||
"foldhash 0.1.5",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "hashbrown"
|
||||
version = "0.16.1"
|
||||
@@ -4223,7 +4269,7 @@ checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100"
|
||||
dependencies = [
|
||||
"allocator-api2",
|
||||
"equivalent",
|
||||
"foldhash",
|
||||
"foldhash 0.2.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -4232,7 +4278,7 @@ version = "0.17.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a"
|
||||
dependencies = [
|
||||
"foldhash",
|
||||
"foldhash 0.2.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -6145,6 +6191,15 @@ dependencies = [
|
||||
"libredox",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ordered-channel"
|
||||
version = "1.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "95be4d57809897b5a7539fc15a7dfe0e84141bc3dfaa2e9b1b27caa90acf61ab"
|
||||
dependencies = [
|
||||
"crossbeam-channel",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ordered-float"
|
||||
version = "5.5.0"
|
||||
@@ -7095,6 +7150,17 @@ dependencies = [
|
||||
"once_cell",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "read-fonts"
|
||||
version = "0.43.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "005c8acf251756c478b0bf402885bfd88a1476020c4c7e6060edc1aa68da38ea"
|
||||
dependencies = [
|
||||
"bytemuck",
|
||||
"font-types 0.12.7",
|
||||
"once_cell",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "read-fonts"
|
||||
version = "0.44.0"
|
||||
@@ -7113,7 +7179,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4edc8db0d972de88e53a085b1f01fb3fbdae7bb0cb7a0ce2b2f8285a412fe3ef"
|
||||
dependencies = [
|
||||
"bytemuck",
|
||||
"foldhash",
|
||||
"foldhash 0.2.0",
|
||||
"font-types 0.12.7",
|
||||
"once_cell",
|
||||
"yoke",
|
||||
@@ -8263,6 +8329,16 @@ dependencies = [
|
||||
"float-cmp",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "string-interner"
|
||||
version = "0.19.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "23de088478b31c349c9ba67816fa55d9355232d63c3afea8bf513e31f0f1d2c0"
|
||||
dependencies = [
|
||||
"hashbrown 0.15.5",
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "stringprep"
|
||||
version = "0.1.5"
|
||||
@@ -8706,6 +8782,15 @@ dependencies = [
|
||||
"syn 3.0.6",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "thread_local"
|
||||
version = "1.1.10"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1ad99c4c6d32803332c548b1af0540b357b3f5fc0be8f6c6bfe8b2e6ae784070"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tiff"
|
||||
version = "0.11.3"
|
||||
@@ -9231,7 +9316,7 @@ dependencies = [
|
||||
"base64 0.23.1",
|
||||
"data-url",
|
||||
"fontdb",
|
||||
"harfrust",
|
||||
"harfrust 0.12.0",
|
||||
"imagesize 0.15.0",
|
||||
"kurbo 0.13.1",
|
||||
"log",
|
||||
@@ -9278,6 +9363,221 @@ dependencies = [
|
||||
"wasm-bindgen",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "vectorcraft-brush"
|
||||
version = "0.18.0"
|
||||
source = "git+https://github.com/storytold/vectorcraft.git?rev=6b3a6b8788ebcb420d69449199cbdf323888d27e#6b3a6b8788ebcb420d69449199cbdf323888d27e"
|
||||
dependencies = [
|
||||
"kurbo 0.13.1",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"vectorcraft-color",
|
||||
"vectorcraft-doc",
|
||||
"vectorcraft-geom",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "vectorcraft-color"
|
||||
version = "0.18.0"
|
||||
source = "git+https://github.com/storytold/vectorcraft.git?rev=6b3a6b8788ebcb420d69449199cbdf323888d27e#6b3a6b8788ebcb420d69449199cbdf323888d27e"
|
||||
dependencies = [
|
||||
"kurbo 0.13.1",
|
||||
"moxcms",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"thiserror 2.0.21",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "vectorcraft-doc"
|
||||
version = "0.18.0"
|
||||
source = "git+https://github.com/storytold/vectorcraft.git?rev=6b3a6b8788ebcb420d69449199cbdf323888d27e#6b3a6b8788ebcb420d69449199cbdf323888d27e"
|
||||
dependencies = [
|
||||
"chrono",
|
||||
"hayro-ccitt",
|
||||
"image",
|
||||
"kurbo 0.13.1",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"thiserror 2.0.21",
|
||||
"tiff",
|
||||
"vectorcraft-color",
|
||||
"vectorcraft-geom",
|
||||
"zune-jpeg",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "vectorcraft-effects"
|
||||
version = "0.18.0"
|
||||
source = "git+https://github.com/storytold/vectorcraft.git?rev=6b3a6b8788ebcb420d69449199cbdf323888d27e#6b3a6b8788ebcb420d69449199cbdf323888d27e"
|
||||
dependencies = [
|
||||
"image",
|
||||
"kurbo 0.13.1",
|
||||
"serde_json",
|
||||
"vectorcraft-doc",
|
||||
"vectorcraft-geom",
|
||||
"vectorcraft-pathops",
|
||||
"vectorcraft-plugins",
|
||||
"vectorcraft-text",
|
||||
"vectorcraft-three-d",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "vectorcraft-eps"
|
||||
version = "0.18.0"
|
||||
source = "git+https://github.com/storytold/vectorcraft.git?rev=6b3a6b8788ebcb420d69449199cbdf323888d27e#6b3a6b8788ebcb420d69449199cbdf323888d27e"
|
||||
dependencies = [
|
||||
"flate2",
|
||||
"hayro-ccitt",
|
||||
"image",
|
||||
"kurbo 0.13.1",
|
||||
"memchr",
|
||||
"ruzstd",
|
||||
"serde_json",
|
||||
"vectorcraft-brush",
|
||||
"vectorcraft-color",
|
||||
"vectorcraft-doc",
|
||||
"vectorcraft-effects",
|
||||
"vectorcraft-geom",
|
||||
"vectorcraft-metafile",
|
||||
"vectorcraft-pdf",
|
||||
"vectorcraft-render",
|
||||
"vectorcraft-text",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "vectorcraft-geom"
|
||||
version = "0.18.0"
|
||||
source = "git+https://github.com/storytold/vectorcraft.git?rev=6b3a6b8788ebcb420d69449199cbdf323888d27e#6b3a6b8788ebcb420d69449199cbdf323888d27e"
|
||||
dependencies = [
|
||||
"kurbo 0.13.1",
|
||||
"serde",
|
||||
"thiserror 2.0.21",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "vectorcraft-metafile"
|
||||
version = "0.18.0"
|
||||
source = "git+https://github.com/storytold/vectorcraft.git?rev=6b3a6b8788ebcb420d69449199cbdf323888d27e#6b3a6b8788ebcb420d69449199cbdf323888d27e"
|
||||
dependencies = [
|
||||
"image",
|
||||
"kurbo 0.13.1",
|
||||
"vectorcraft-brush",
|
||||
"vectorcraft-color",
|
||||
"vectorcraft-doc",
|
||||
"vectorcraft-effects",
|
||||
"vectorcraft-geom",
|
||||
"vectorcraft-text",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "vectorcraft-pathops"
|
||||
version = "0.18.0"
|
||||
source = "git+https://github.com/storytold/vectorcraft.git?rev=6b3a6b8788ebcb420d69449199cbdf323888d27e#6b3a6b8788ebcb420d69449199cbdf323888d27e"
|
||||
dependencies = [
|
||||
"kurbo 0.13.1",
|
||||
"linesweeper",
|
||||
"thiserror 2.0.21",
|
||||
"vectorcraft-geom",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "vectorcraft-pdf"
|
||||
version = "0.18.0"
|
||||
source = "git+https://github.com/storytold/vectorcraft.git?rev=6b3a6b8788ebcb420d69449199cbdf323888d27e#6b3a6b8788ebcb420d69449199cbdf323888d27e"
|
||||
dependencies = [
|
||||
"aes 0.8.4",
|
||||
"chrono",
|
||||
"craft-pdf 0.1.0 (git+https://github.com/storytold/craft-libs?rev=92d392cff132eaf53e13afff77f38b5c11b73aa0)",
|
||||
"flate2",
|
||||
"getrandom 0.4.3",
|
||||
"image",
|
||||
"jpeg-encoder 0.6.1",
|
||||
"kurbo 0.13.1",
|
||||
"log",
|
||||
"md-5 0.10.6",
|
||||
"roxmltree 0.21.2",
|
||||
"serde",
|
||||
"sha2 0.10.9",
|
||||
"skrifa 0.47.0",
|
||||
"smallvec",
|
||||
"thiserror 2.0.21",
|
||||
"vectorcraft-brush",
|
||||
"vectorcraft-color",
|
||||
"vectorcraft-doc",
|
||||
"vectorcraft-effects",
|
||||
"vectorcraft-geom",
|
||||
"vectorcraft-text",
|
||||
"weezl 0.2.1",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "vectorcraft-plugins"
|
||||
version = "0.18.0"
|
||||
source = "git+https://github.com/storytold/vectorcraft.git?rev=6b3a6b8788ebcb420d69449199cbdf323888d27e#6b3a6b8788ebcb420d69449199cbdf323888d27e"
|
||||
dependencies = [
|
||||
"serde",
|
||||
"serde_json",
|
||||
"thiserror 2.0.21",
|
||||
"vectorcraft-color",
|
||||
"vectorcraft-doc",
|
||||
"vectorcraft-geom",
|
||||
"wasmi",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "vectorcraft-render"
|
||||
version = "0.18.0"
|
||||
source = "git+https://github.com/storytold/vectorcraft.git?rev=6b3a6b8788ebcb420d69449199cbdf323888d27e#6b3a6b8788ebcb420d69449199cbdf323888d27e"
|
||||
dependencies = [
|
||||
"flate2",
|
||||
"gif",
|
||||
"image",
|
||||
"jpeg-encoder 0.6.1",
|
||||
"log",
|
||||
"png 0.18.1",
|
||||
"vectorcraft-brush",
|
||||
"vectorcraft-color",
|
||||
"vectorcraft-doc",
|
||||
"vectorcraft-effects",
|
||||
"vectorcraft-geom",
|
||||
"vectorcraft-text",
|
||||
"vello_common 0.2.0",
|
||||
"vello_cpu 0.2.0",
|
||||
"weezl 0.2.1",
|
||||
"winsafe",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "vectorcraft-text"
|
||||
version = "0.18.0"
|
||||
source = "git+https://github.com/storytold/vectorcraft.git?rev=6b3a6b8788ebcb420d69449199cbdf323888d27e#6b3a6b8788ebcb420d69449199cbdf323888d27e"
|
||||
dependencies = [
|
||||
"harfrust 0.13.3",
|
||||
"icu_segmenter",
|
||||
"kurbo 0.13.1",
|
||||
"log",
|
||||
"skrifa 0.47.0",
|
||||
"subsetter",
|
||||
"unicode-bidi",
|
||||
"unicode-general-category",
|
||||
"unicode-script",
|
||||
"unicode-segmentation",
|
||||
"vectorcraft-doc",
|
||||
"vectorcraft-geom",
|
||||
"vello_cpu 0.2.0",
|
||||
"windows-registry",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "vectorcraft-three-d"
|
||||
version = "0.18.0"
|
||||
source = "git+https://github.com/storytold/vectorcraft.git?rev=6b3a6b8788ebcb420d69449199cbdf323888d27e#6b3a6b8788ebcb420d69449199cbdf323888d27e"
|
||||
dependencies = [
|
||||
"vectorcraft-geom",
|
||||
"vectorcraft-pathops",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "vello_common"
|
||||
version = "0.0.8"
|
||||
@@ -9348,9 +9648,13 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ace506ca414548966fcf3c283be94eb1cad8254de488791ee698bf438b0890a9"
|
||||
dependencies = [
|
||||
"bytemuck",
|
||||
"crossbeam-channel",
|
||||
"glifo 0.3.0",
|
||||
"hashbrown 0.17.1",
|
||||
"ordered-channel",
|
||||
"png 0.18.1",
|
||||
"rayon",
|
||||
"thread_local",
|
||||
"vello_common 0.2.0",
|
||||
]
|
||||
|
||||
@@ -9508,6 +9812,56 @@ version = "0.2.129"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4f692aa943ccd88363733b77063f32cfed5bc6cbea8e6e8b251b302f881606fe"
|
||||
|
||||
[[package]]
|
||||
name = "wasmi"
|
||||
version = "2.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "78693fcdd618e0fc34af59c6b8efa9ac5d58c68df940beff4bedddb6acfe7c27"
|
||||
dependencies = [
|
||||
"spin",
|
||||
"wasmi_collections",
|
||||
"wasmi_core",
|
||||
"wasmi_ir",
|
||||
"wasmparser",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wasmi_collections"
|
||||
version = "2.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8a8be2aa467cf2d29e96ff759472c36eeb44a3c81c67fc9cb76c9a24c519c557"
|
||||
dependencies = [
|
||||
"string-interner",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wasmi_core"
|
||||
version = "2.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "69372d5fda3ea3d1e0aa6603c7888110e0187e88ea17cd8fc2e2df0a0e1f37fa"
|
||||
dependencies = [
|
||||
"libm",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wasmi_ir"
|
||||
version = "2.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8f17b774caa13c618c7244f1ee51fe23c5e7b8538a471fa46d9949779758aed6"
|
||||
dependencies = [
|
||||
"wasmi_core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wasmparser"
|
||||
version = "0.228.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4abf1132c1fdf747d56bbc1bb52152400c70f336870f968b85e89ea422198ae3"
|
||||
dependencies = [
|
||||
"bitflags 2.13.2",
|
||||
"indexmap",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wayland-backend"
|
||||
version = "0.3.17"
|
||||
|
||||
@@ -44,6 +44,8 @@ craft-dxf = { path = "crates/dxf" }
|
||||
craft-drawingml = { path = "crates/drawingml" }
|
||||
craft-eps = { path = "crates/eps" }
|
||||
craft-file-formats = { path = "crates/file-formats" }
|
||||
vectorcraft-doc = { git = "https://github.com/storytold/vectorcraft.git", rev = "6b3a6b8788ebcb420d69449199cbdf323888d27e" }
|
||||
vectorcraft-eps = { git = "https://github.com/storytold/vectorcraft.git", rev = "6b3a6b8788ebcb420d69449199cbdf323888d27e" }
|
||||
craft-doc = { path = "crates/doc" }
|
||||
craft-docpath = { path = "crates/docpath" }
|
||||
craft-dpx = { path = "crates/dpx" }
|
||||
|
||||
@@ -15,3 +15,4 @@ craft-codepage = { workspace = true }
|
||||
thiserror = { workspace = true }
|
||||
log = "0.4"
|
||||
encoding_rs = "0.8.35"
|
||||
kurbo = { workspace = true }
|
||||
@@ -11,6 +11,7 @@
|
||||
use std::io::Write as _;
|
||||
|
||||
pub mod model;
|
||||
pub mod paths;
|
||||
mod version;
|
||||
pub use version::Version;
|
||||
|
||||
|
||||
@@ -0,0 +1,321 @@
|
||||
//! Bounded conversion of common planar DXF entities into neutral Bézier paths.
|
||||
//!
|
||||
//! This is geometry extraction, not a full CAD scene renderer: unsupported entities such as
|
||||
//! text, dimensions, meshes, and block inserts are reported as warnings and omitted.
|
||||
|
||||
use kurbo::{BezPath, Point, Rect, Shape as _};
|
||||
|
||||
/// Limits on the number of paths returned by [`import`]. Parsing also uses the crate's tag cap.
|
||||
pub const MAX_PATHS: usize = 200_000;
|
||||
/// Aggregate number of Bézier elements a drawing may generate during conversion.
|
||||
pub const MAX_PATH_ELEMENTS: usize = 2_000_000;
|
||||
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
pub enum Error {
|
||||
#[error("DXF parse failed: {0}")]
|
||||
Parse(#[from] crate::DxfError),
|
||||
#[error("DXF contains more than {MAX_PATHS} paths")]
|
||||
TooManyPaths,
|
||||
#[error("DXF curve exceeds the subdivision limit")]
|
||||
TooManySegments,
|
||||
#[error("DXF contains a non-finite path coordinate")]
|
||||
NonFinite,
|
||||
}
|
||||
|
||||
/// Geometry in source DXF coordinates. `unit_to_mm` maps a source coordinate to millimetres;
|
||||
/// unitless drawings use `1.0`, as the coordinates are then interpreted as millimetres.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Imported {
|
||||
pub paths: Vec<BezPath>,
|
||||
pub bounds: Option<Rect>,
|
||||
pub unit_to_mm: f64,
|
||||
pub warnings: Vec<String>,
|
||||
}
|
||||
|
||||
/// Parse bounded DXF input and extract visible planar linework.
|
||||
pub fn import(bytes: &[u8]) -> Result<Imported, Error> {
|
||||
let tags = crate::parse_with_limit(bytes, crate::max_tags())?;
|
||||
let drawing = crate::model::Document::read(&tags);
|
||||
let unit_to_mm = units_to_mm(drawing.header.insunits(), drawing.header.measurement());
|
||||
let layer_visibility: std::collections::HashMap<_, _> =
|
||||
drawing.layers().into_iter().map(|layer| (layer.name, layer.on && !layer.frozen)).collect();
|
||||
let mut paths = Vec::new();
|
||||
let mut warnings = Vec::new();
|
||||
let mut unsupported = 0usize;
|
||||
let mut elements = 0usize;
|
||||
for record in &drawing.entities {
|
||||
let entity = crate::model::Entity::read(record);
|
||||
if !entity.common.visible || record.paper_space() || layer_visibility.get(&entity.common.layer) == Some(&false) {
|
||||
continue;
|
||||
}
|
||||
if excessive_sweep(&entity.kind) {
|
||||
return Err(Error::TooManySegments);
|
||||
}
|
||||
let remaining = MAX_PATH_ELEMENTS.saturating_sub(elements);
|
||||
if estimated_elements(&entity.kind) > remaining {
|
||||
return Err(Error::TooManySegments);
|
||||
}
|
||||
let converted = entity_paths(&entity.kind, entity.common.extrusion);
|
||||
if converted.iter().flat_map(|path| path.elements()).any(|element| {
|
||||
let finite = |p: Point| p.x.is_finite() && p.y.is_finite();
|
||||
match *element {
|
||||
kurbo::PathEl::MoveTo(p) | kurbo::PathEl::LineTo(p) => !finite(p),
|
||||
kurbo::PathEl::QuadTo(a, b) => !finite(a) || !finite(b),
|
||||
kurbo::PathEl::CurveTo(a, b, c) => !finite(a) || !finite(b) || !finite(c),
|
||||
kurbo::PathEl::ClosePath => false,
|
||||
}
|
||||
}) {
|
||||
return Err(Error::NonFinite);
|
||||
}
|
||||
if converted.is_empty() {
|
||||
unsupported = unsupported.saturating_add(1);
|
||||
continue;
|
||||
}
|
||||
elements = elements.saturating_add(converted.iter().map(|path| path.elements().len()).sum::<usize>());
|
||||
if elements > MAX_PATH_ELEMENTS {
|
||||
return Err(Error::TooManySegments);
|
||||
}
|
||||
if paths.len().saturating_add(converted.len()) > MAX_PATHS {
|
||||
return Err(Error::TooManyPaths);
|
||||
}
|
||||
paths.extend(converted);
|
||||
}
|
||||
if unsupported > 0 {
|
||||
warnings.push(format!("{unsupported} visible DXF entities are unsupported and were omitted"));
|
||||
}
|
||||
if drawing.skipped.iter().any(|(_, n)| *n > 0) {
|
||||
warnings.push("some unsupported or stray DXF records were skipped".into());
|
||||
}
|
||||
let bounds = paths.iter().map(|path| path.bounding_box()).reduce(|a, b| a.union(b));
|
||||
Ok(Imported { paths, bounds, unit_to_mm, warnings })
|
||||
}
|
||||
|
||||
fn estimated_elements(kind: &crate::model::EntityKind) -> usize {
|
||||
use crate::model::EntityKind as E;
|
||||
let arc_count = |sweep: f64| (sweep.abs() / std::f64::consts::FRAC_PI_2).ceil().max(1.0) as usize;
|
||||
let bulge_count = |bulge: f64| {
|
||||
if bulge.is_finite() && bulge.abs() > 1e-12 { arc_count(4.0 * bulge.atan()) } else { 1 }
|
||||
};
|
||||
let polyline_count = |vertices: &[(Point, f64)], closed: bool| {
|
||||
if vertices.is_empty() {
|
||||
return 0;
|
||||
}
|
||||
let mut count = 1usize;
|
||||
for pair in vertices.windows(2) {
|
||||
count = count.saturating_add(bulge_count(pair[0].1));
|
||||
}
|
||||
if closed && vertices.len() > 1 {
|
||||
count = count.saturating_add(bulge_count(vertices.last().map_or(0.0, |vertex| vertex.1)));
|
||||
count = count.saturating_add(1);
|
||||
}
|
||||
count
|
||||
};
|
||||
match kind {
|
||||
E::Line { .. } => 2,
|
||||
E::Circle { .. } => 6,
|
||||
E::Arc { start_angle, end_angle, .. } => 1usize.saturating_add(arc_count((end_angle - start_angle).to_radians())),
|
||||
E::LwPolyline(poly) => {
|
||||
let vertices: Vec<_> = poly.vertices.iter().map(|v| (Point::new(v.x, v.y), v.bulge)).collect();
|
||||
polyline_count(&vertices, poly.closed)
|
||||
}
|
||||
E::Polyline(poly) if !poly.is_mesh() && !poly.is_polyface() => {
|
||||
let vertices: Vec<_> = poly
|
||||
.vertices
|
||||
.iter()
|
||||
.filter(|vertex| !vertex.is_face())
|
||||
.map(|vertex| (Point::new(vertex.location[0], vertex.location[1]), vertex.bulge))
|
||||
.collect();
|
||||
polyline_count(&vertices, poly.closed())
|
||||
}
|
||||
E::Ellipse { start, end, .. } => 1usize.saturating_add(arc_count(end - start)),
|
||||
_ => 0,
|
||||
}
|
||||
}
|
||||
|
||||
fn units_to_mm(code: i64, _measurement: Option<i64>) -> f64 {
|
||||
match code {
|
||||
0 => 1.0,
|
||||
1 => 25.4,
|
||||
2 => 304.8,
|
||||
3 => 1_609_344.0,
|
||||
5 => 10.0,
|
||||
6 => 1_000.0,
|
||||
7 => 1_000_000.0,
|
||||
8 => 0.000_025_4,
|
||||
9 => 0.0254,
|
||||
10 => 914.4,
|
||||
11 => 1e-7,
|
||||
12 => 1e-6,
|
||||
13 => 0.001,
|
||||
14 => 100.0,
|
||||
15 => 10_000.0,
|
||||
16 => 100_000.0,
|
||||
17 => 1e12,
|
||||
18 => 149_597_870_700_000.0,
|
||||
19 => 9.460_730_472_580_8e18,
|
||||
20 => 3.085_677_581_491_367e19,
|
||||
21 => 1200.0 / 3937.0 * 1000.0,
|
||||
22 => 1200.0 / 3937.0 * 1000.0 / 12.0,
|
||||
23 => 1200.0 / 3937.0 * 1000.0 * 3.0,
|
||||
24 => 1200.0 / 3937.0 * 1000.0 * 5280.0,
|
||||
_ => 1.0,
|
||||
}
|
||||
}
|
||||
|
||||
fn entity_paths(kind: &crate::model::EntityKind, normal: crate::model::Point) -> Vec<BezPath> {
|
||||
use crate::model::EntityKind as E;
|
||||
match kind {
|
||||
E::Line { start, end } => vec![line(*start, *end)],
|
||||
E::Circle { center, radius } if radius.is_finite() && *radius > 0.0 => {
|
||||
vec![map_ocs_path(circle(Point::new(center[0], center[1]), *radius), normal, center[2])]
|
||||
}
|
||||
E::Arc { center, radius, start_angle, end_angle }
|
||||
if radius.is_finite() && *radius > 0.0 && start_angle.is_finite() && end_angle.is_finite() =>
|
||||
{
|
||||
vec![map_ocs_path(arc(Point::new(center[0], center[1]), *radius, start_angle.to_radians(), end_angle.to_radians()), normal, center[2])]
|
||||
}
|
||||
E::LwPolyline(poly) => {
|
||||
vec![map_ocs_path(polyline(poly.vertices.iter().map(|v| (Point::new(v.x, v.y), v.bulge)), poly.closed), normal, poly.elevation)]
|
||||
}
|
||||
E::Polyline(poly) if !poly.is_mesh() && !poly.is_polyface() => {
|
||||
let is_3d = poly.flags & 8 != 0;
|
||||
let z = if is_3d { 0.0 } else { poly.elevation };
|
||||
let path =
|
||||
polyline(poly.vertices.iter().filter(|v| !v.is_face()).map(|v| (Point::new(v.location[0], v.location[1]), v.bulge)), poly.closed());
|
||||
vec![if is_3d { path } else { map_ocs_path(path, normal, z) }]
|
||||
}
|
||||
E::Ellipse { center, major, ratio, start, end } if ratio.is_finite() && *ratio > 0.0 => {
|
||||
vec![ellipse(*center, *major, normal, *ratio, *start, *end)]
|
||||
}
|
||||
_ => Vec::new(),
|
||||
}
|
||||
}
|
||||
|
||||
fn map_ocs_path(path: BezPath, normal: crate::model::Point, z: f64) -> BezPath {
|
||||
let [x, y, n] = crate::model::ocs_axes(normal);
|
||||
let transform = kurbo::Affine::new([x[0], x[1], y[0], y[1], n[0] * z, n[1] * z]);
|
||||
transform * path
|
||||
}
|
||||
|
||||
fn excessive_sweep(kind: &crate::model::EntityKind) -> bool {
|
||||
let cap = 4096.0 * std::f64::consts::FRAC_PI_2;
|
||||
let over = |sweep: f64| !sweep.is_finite() || sweep.abs() > cap;
|
||||
match kind {
|
||||
crate::model::EntityKind::Arc { start_angle, end_angle, .. } => over((end_angle - start_angle).to_radians()),
|
||||
crate::model::EntityKind::Ellipse { start, end, .. } => over(end - start),
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
|
||||
fn line(start: crate::model::Point, end: crate::model::Point) -> BezPath {
|
||||
let mut path = BezPath::new();
|
||||
path.move_to((start[0], start[1]));
|
||||
path.line_to((end[0], end[1]));
|
||||
path
|
||||
}
|
||||
|
||||
fn polyline(vertices: impl IntoIterator<Item = (Point, f64)>, closed: bool) -> BezPath {
|
||||
let vertices = vertices.into_iter().collect::<Vec<_>>();
|
||||
let mut path = BezPath::new();
|
||||
let Some((first, _)) = vertices.first().copied() else { return path };
|
||||
path.move_to(first);
|
||||
for i in 1..vertices.len() {
|
||||
let (a, bulge) = vertices[i - 1];
|
||||
let (b, _) = vertices[i];
|
||||
if bulge.is_finite() && bulge.abs() > 1e-12 {
|
||||
append_bulge(&mut path, a, b, bulge);
|
||||
} else {
|
||||
path.line_to(b);
|
||||
}
|
||||
}
|
||||
if closed && vertices.len() > 1 {
|
||||
let (a, bulge) = *vertices.last().unwrap_or(&(first, 0.0));
|
||||
if bulge.is_finite() && bulge.abs() > 1e-12 {
|
||||
append_bulge(&mut path, a, first, bulge);
|
||||
} else {
|
||||
path.line_to(first);
|
||||
}
|
||||
path.close_path();
|
||||
}
|
||||
path
|
||||
}
|
||||
|
||||
fn append_bulge(path: &mut BezPath, a: Point, b: Point, bulge: f64) {
|
||||
let chord = b - a;
|
||||
let length = chord.hypot();
|
||||
if !(length.is_finite() && length > 0.0) {
|
||||
path.line_to(b);
|
||||
return;
|
||||
}
|
||||
let center = a + chord * 0.5 + kurbo::Vec2::new(-chord.y, chord.x) * ((1.0 - bulge * bulge) / (4.0 * bulge));
|
||||
let start = (a.y - center.y).atan2(a.x - center.x);
|
||||
let sweep = 4.0 * bulge.atan();
|
||||
let radius = length * (1.0 + bulge * bulge) / (4.0 * bulge.abs());
|
||||
arc_segments(path, center, start, sweep, radius);
|
||||
}
|
||||
|
||||
fn circle(center: Point, radius: f64) -> BezPath {
|
||||
let mut path = BezPath::new();
|
||||
path.move_to((center.x + radius, center.y));
|
||||
arc_segments(&mut path, center, 0.0, std::f64::consts::TAU, radius);
|
||||
path.close_path();
|
||||
path
|
||||
}
|
||||
|
||||
fn arc(center: Point, radius: f64, start: f64, end: f64) -> BezPath {
|
||||
let mut path = BezPath::new();
|
||||
path.move_to((center.x + radius * start.cos(), center.y + radius * start.sin()));
|
||||
let sweep = if end >= start { end - start } else { end - start + std::f64::consts::TAU };
|
||||
arc_segments(&mut path, center, start, sweep, radius);
|
||||
path
|
||||
}
|
||||
|
||||
fn arc_segments(path: &mut BezPath, center: Point, start: f64, sweep: f64, radius: f64) {
|
||||
let count = (sweep.abs() / (std::f64::consts::FRAC_PI_2)).ceil().max(1.0) as usize;
|
||||
if count > 4096 {
|
||||
return;
|
||||
}
|
||||
let step = sweep / count as f64;
|
||||
for i in 0..count {
|
||||
let a = start + step * i as f64;
|
||||
let b = a + step;
|
||||
let k = 4.0 / 3.0 * (step / 4.0).tan();
|
||||
let p1 = Point::new(center.x + radius * (a.cos() - k * a.sin()), center.y + radius * (a.sin() + k * a.cos()));
|
||||
let p2 = Point::new(center.x + radius * (b.cos() + k * b.sin()), center.y + radius * (b.sin() - k * b.cos()));
|
||||
let p3 = Point::new(center.x + radius * b.cos(), center.y + radius * b.sin());
|
||||
path.curve_to(p1, p2, p3);
|
||||
}
|
||||
}
|
||||
|
||||
fn ellipse(center: crate::model::Point, major: crate::model::Point, normal: crate::model::Point, ratio: f64, start: f64, end: f64) -> BezPath {
|
||||
let minor = cross(normal, major);
|
||||
let minor_len = minor.iter().map(|v| v * v).sum::<f64>().sqrt();
|
||||
if !(minor_len.is_finite() && minor_len > 0.0) {
|
||||
return BezPath::new();
|
||||
}
|
||||
let major_len = major.iter().map(|v| v * v).sum::<f64>().sqrt();
|
||||
let bx = [minor[0] / minor_len * major_len * ratio, minor[1] / minor_len * major_len * ratio, minor[2] / minor_len * major_len * ratio];
|
||||
let point = |t: f64| Point::new(center[0] + major[0] * t.cos() + bx[0] * t.sin(), center[1] + major[1] * t.cos() + bx[1] * t.sin());
|
||||
let mut path = BezPath::new();
|
||||
path.move_to(point(start));
|
||||
let sweep = if end >= start { end - start } else { end - start + std::f64::consts::TAU };
|
||||
let count = (sweep.abs() / std::f64::consts::FRAC_PI_2).ceil().max(1.0) as usize;
|
||||
if count > 4096 {
|
||||
return path;
|
||||
}
|
||||
let step = sweep / count as f64;
|
||||
for i in 0..count {
|
||||
let a = start + step * i as f64;
|
||||
let b = a + step;
|
||||
let k = 4.0 / 3.0 * (step / 4.0).tan();
|
||||
let derivative = |t: f64| Point::new(-major[0] * t.sin() + bx[0] * t.cos(), -major[1] * t.sin() + bx[1] * t.cos());
|
||||
let p1 = point(a) + derivative(a).to_vec2() * k;
|
||||
let p2 = point(b) - derivative(b).to_vec2() * k;
|
||||
path.curve_to(p1, p2, point(b));
|
||||
}
|
||||
path
|
||||
}
|
||||
|
||||
fn cross(a: crate::model::Point, b: crate::model::Point) -> crate::model::Point {
|
||||
[a[1] * b[2] - a[2] * b[1], a[2] * b[0] - a[0] * b[2], a[0] * b[1] - a[1] * b[0]]
|
||||
}
|
||||
@@ -11,3 +11,11 @@ repository.workspace = true
|
||||
workspace = true
|
||||
|
||||
[dependencies]
|
||||
kurbo = { workspace = true, optional = true }
|
||||
vectorcraft-doc = { workspace = true, optional = true }
|
||||
vectorcraft-eps = { workspace = true, optional = true }
|
||||
thiserror = { workspace = true, optional = true }
|
||||
|
||||
[features]
|
||||
default = []
|
||||
paths = ["dep:kurbo", "dep:thiserror", "dep:vectorcraft-doc", "dep:vectorcraft-eps"]
|
||||
@@ -18,6 +18,8 @@
|
||||
pub mod ascii85;
|
||||
mod container;
|
||||
mod dsc;
|
||||
#[cfg(feature = "paths")]
|
||||
pub mod paths;
|
||||
mod preview;
|
||||
mod rectangles;
|
||||
|
||||
|
||||
@@ -0,0 +1,103 @@
|
||||
//! Neutral path extraction for EPS and PostScript files.
|
||||
//!
|
||||
//! This facade keeps the PostScript interpreter and its format decisions in shared dependencies;
|
||||
//! applications receive Bézier paths, page bounds and warnings for their own document adapters.
|
||||
|
||||
use kurbo::{BezPath, Rect};
|
||||
|
||||
/// Limits on the extracted geometry. The interpreter has its own execution and allocation caps.
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub struct Limits {
|
||||
pub max_input_bytes: usize,
|
||||
pub max_paths: usize,
|
||||
pub max_path_elements: usize,
|
||||
}
|
||||
|
||||
impl Default for Limits {
|
||||
fn default() -> Self {
|
||||
Self { max_input_bytes: 64 * 1024 * 1024, max_paths: 200_000, max_path_elements: 2_000_000 }
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
pub enum Error {
|
||||
#[error("PostScript input exceeds {limit} bytes")]
|
||||
InputLimit { limit: usize },
|
||||
#[error("PostScript import failed: {0}")]
|
||||
Parse(String),
|
||||
#[error("PostScript path output exceeds configured limit")]
|
||||
OutputLimit,
|
||||
}
|
||||
|
||||
/// First-page path geometry normalized to the top-left origin in PostScript points, plus notes.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Imported {
|
||||
pub paths: Vec<BezPath>,
|
||||
/// Origin-zero page bounds in PostScript points, using the same accepted DSC box as the
|
||||
/// interpreter.
|
||||
pub page_bounds: Rect,
|
||||
pub warnings: Vec<String>,
|
||||
pub preview_fallback: bool,
|
||||
}
|
||||
|
||||
/// Interpret EPS/PostScript bytes and return their editable vector path geometry.
|
||||
pub fn import(bytes: &[u8], limits: Limits) -> Result<Imported, Error> {
|
||||
if bytes.len() > limits.max_input_bytes {
|
||||
return Err(Error::InputLimit { limit: limits.max_input_bytes });
|
||||
}
|
||||
let source = vectorcraft_eps::import(bytes).map_err(Error::Parse)?;
|
||||
let page_bounds = vectorcraft_eps::page(bytes).map_err(Error::Parse)?;
|
||||
let mut paths = Vec::new();
|
||||
let mut total_elements = 0usize;
|
||||
let mut clipped = false;
|
||||
let mut omitted = false;
|
||||
for layer in &source.document.layers {
|
||||
collect(layer, &mut paths, &mut total_elements, limits, &mut clipped, &mut omitted)?;
|
||||
}
|
||||
let mut warnings = source.warnings;
|
||||
if clipped {
|
||||
warnings.push("EPS clipping groups are returned as unclipped artwork".into());
|
||||
}
|
||||
if omitted {
|
||||
warnings.push("visible EPS text or image content is not returned as paths".into());
|
||||
}
|
||||
Ok(Imported { paths, page_bounds, warnings, preview_fallback: source.preview })
|
||||
}
|
||||
|
||||
fn collect(
|
||||
node: &vectorcraft_doc::Node,
|
||||
paths: &mut Vec<BezPath>,
|
||||
elements: &mut usize,
|
||||
limits: Limits,
|
||||
clipped: &mut bool,
|
||||
omitted: &mut bool,
|
||||
) -> Result<(), Error> {
|
||||
if !node.visible {
|
||||
return Ok(());
|
||||
}
|
||||
if let Some(data) = node.path_data() {
|
||||
if matches!(node.kind, vectorcraft_doc::NodeKind::Path { clipping: true, .. }) {
|
||||
return Ok(());
|
||||
}
|
||||
let path = data.to_bezpath();
|
||||
if !path.elements().is_empty() {
|
||||
*elements = elements.saturating_add(path.elements().len());
|
||||
if paths.len() >= limits.max_paths || *elements > limits.max_path_elements {
|
||||
return Err(Error::OutputLimit);
|
||||
}
|
||||
paths.push(path);
|
||||
}
|
||||
} else if let Some(children) = node.children() {
|
||||
let is_clip = matches!(node.kind, vectorcraft_doc::NodeKind::Group { clip: true, .. } | vectorcraft_doc::NodeKind::Layer { clip: true, .. });
|
||||
*clipped |= is_clip;
|
||||
for (index, child) in children.iter().enumerate() {
|
||||
if is_clip && index == 0 {
|
||||
continue;
|
||||
}
|
||||
collect(child, paths, elements, limits, clipped, omitted)?;
|
||||
}
|
||||
} else {
|
||||
*omitted = true;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
@@ -14,6 +14,7 @@ workspace = true
|
||||
default = []
|
||||
read = ["dep:hayro-syntax", "dep:thiserror"]
|
||||
interpret = ["read", "dep:hayro-interpret"]
|
||||
paths = ["interpret", "dep:kurbo"]
|
||||
raster = ["interpret", "dep:hayro", "dep:bytemuck"]
|
||||
write = ["dep:krilla", "dep:thiserror"]
|
||||
write-pdf = ["write", "krilla/pdf"]
|
||||
@@ -40,6 +41,7 @@ stringprep = { version = "0.1", optional = true }
|
||||
zeroize = { version = "1", optional = true }
|
||||
# PDF date strings in the object layer (`cos`).
|
||||
chrono = { workspace = true, optional = true }
|
||||
kurbo = { workspace = true, optional = true }
|
||||
|
||||
# On the web, a rewrite's fresh file identifier (`cos`) takes its entropy from the browser
|
||||
# (`crypto.getRandomValues`); native builds compile neither.
|
||||
|
||||
@@ -21,6 +21,8 @@ pub use link::safe_url;
|
||||
mod read;
|
||||
#[cfg(feature = "read")]
|
||||
pub use read::*;
|
||||
#[cfg(feature = "paths")]
|
||||
pub mod paths;
|
||||
#[cfg(feature = "raster")]
|
||||
pub mod raster;
|
||||
#[cfg(feature = "write")]
|
||||
|
||||
@@ -0,0 +1,184 @@
|
||||
//! Bounded extraction of page vector paths from PDF content streams.
|
||||
//!
|
||||
//! Paths are returned in page-oriented points with `/UserUnit`, rotation and crop origin applied.
|
||||
//! Text glyphs and raster images are counted as omitted content; clipping and paint are not applied.
|
||||
|
||||
use hayro_interpret::font::Glyph;
|
||||
use hayro_interpret::{
|
||||
BlendMode, ClipPath, Device, GlyphDrawMode, Image, InterpreterCache, InterpreterSettings, Paint, PathDrawMode, RectExt, SoftMask, TransformExt,
|
||||
};
|
||||
use kurbo::{Affine, BezPath, Rect};
|
||||
use std::sync::{
|
||||
Arc,
|
||||
atomic::{AtomicBool, Ordering},
|
||||
};
|
||||
|
||||
/// Limits for vector path extraction. They bound the output model, not interpreter peak memory.
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub struct Limits {
|
||||
pub max_input_bytes: usize,
|
||||
pub max_pages: usize,
|
||||
pub max_paths: usize,
|
||||
pub max_path_elements: usize,
|
||||
}
|
||||
|
||||
impl Default for Limits {
|
||||
fn default() -> Self {
|
||||
Self { max_input_bytes: 64 * 1024 * 1024, max_pages: 1000, max_paths: 200_000, max_path_elements: 2_000_000 }
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
pub enum Error {
|
||||
#[error(transparent)]
|
||||
Open(#[from] crate::read::OpenError),
|
||||
#[error(transparent)]
|
||||
Interpret(#[from] crate::read::ExecutionError),
|
||||
#[error("PDF contains more than {limit} pages")]
|
||||
Pages { limit: usize },
|
||||
#[error("PDF has no page at index {index}")]
|
||||
PageIndex { index: usize },
|
||||
#[error("PDF path output exceeds configured limit")]
|
||||
OutputLimit,
|
||||
}
|
||||
|
||||
/// Extracted paths for one PDF page, normalized to an origin-zero page in physical points.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Page {
|
||||
pub crop_box: Rect,
|
||||
pub paths: Vec<BezPath>,
|
||||
pub omitted_text: usize,
|
||||
pub omitted_images: usize,
|
||||
pub clips_seen: usize,
|
||||
}
|
||||
|
||||
/// Pages from one PDF. Paths from separate pages are never combined.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Imported {
|
||||
pub pages: Vec<Page>,
|
||||
pub warnings: Vec<String>,
|
||||
}
|
||||
|
||||
/// Open and interpret vector content using shared PDF parsing and interpreter APIs.
|
||||
pub fn import(bytes: &[u8], limits: Limits) -> Result<Imported, Error> {
|
||||
import_selected(bytes, limits, None)
|
||||
}
|
||||
|
||||
/// Interpret only the requested page and apply path and element limits to that page's output.
|
||||
pub fn import_page(bytes: &[u8], page_index: usize, limits: Limits) -> Result<Imported, Error> {
|
||||
import_selected(bytes, limits, Some(page_index))
|
||||
}
|
||||
|
||||
fn import_selected(bytes: &[u8], limits: Limits, selected: Option<usize>) -> Result<Imported, Error> {
|
||||
let options = crate::read::OpenOptions { max_input_bytes: limits.max_input_bytes, ..Default::default() };
|
||||
let pdf = crate::read::open_slice_with_options(bytes, None, &options)?;
|
||||
if pdf.pages().len() > limits.max_pages {
|
||||
return Err(Error::Pages { limit: limits.max_pages });
|
||||
}
|
||||
if selected.is_some_and(|index| index >= pdf.pages().len()) {
|
||||
return Err(Error::PageIndex { index: selected.unwrap_or_default() });
|
||||
}
|
||||
let cache = InterpreterCache::new();
|
||||
let truncated = Arc::new(AtomicBool::new(false));
|
||||
let truncation_flag = truncated.clone();
|
||||
let settings = InterpreterSettings {
|
||||
warning_sink: Arc::new(move |warning| {
|
||||
if matches!(warning, hayro_interpret::InterpreterWarning::ContentTruncated) {
|
||||
truncation_flag.store(true, Ordering::Relaxed);
|
||||
}
|
||||
}),
|
||||
..InterpreterSettings::default()
|
||||
};
|
||||
let mut pages = Vec::with_capacity(pdf.pages().len());
|
||||
let mut total_paths = 0usize;
|
||||
let mut total_elements = 0usize;
|
||||
for (page_index, page) in pdf.pages().iter().enumerate() {
|
||||
if selected.is_some_and(|index| index != page_index) {
|
||||
continue;
|
||||
}
|
||||
let source_crop_box = page.intersected_crop_box().to_kurbo();
|
||||
let (width, height) = page.render_dimensions();
|
||||
let crop_box = Rect::new(0.0, 0.0, f64::from(width), f64::from(height));
|
||||
let initial_transform = page.initial_transform(true).to_kurbo();
|
||||
let mut context = hayro_interpret::Context::new(initial_transform, source_crop_box, &cache, pdf.xref(), settings.clone());
|
||||
let mut device = Collector {
|
||||
max_paths: if selected.is_some() { limits.max_paths } else { limits.max_paths.saturating_sub(total_paths) },
|
||||
max_elements: if selected.is_some() { limits.max_path_elements } else { limits.max_path_elements.saturating_sub(total_elements) },
|
||||
..Collector::default()
|
||||
};
|
||||
crate::read::interpret_page(page, &mut context, &mut device)?;
|
||||
if device.exceeded {
|
||||
return Err(Error::OutputLimit);
|
||||
}
|
||||
if device.paths.len() > limits.max_paths || device.paths.iter().map(|path| path.elements().len()).sum::<usize>() > limits.max_path_elements {
|
||||
return Err(Error::OutputLimit);
|
||||
}
|
||||
total_paths = total_paths.saturating_add(device.paths.len());
|
||||
total_elements = device.paths.iter().fold(total_elements, |count, path| count.saturating_add(path.elements().len()));
|
||||
if total_paths > limits.max_paths || total_elements > limits.max_path_elements {
|
||||
return Err(Error::OutputLimit);
|
||||
}
|
||||
pages.push(Page {
|
||||
crop_box,
|
||||
paths: device.paths,
|
||||
omitted_text: device.omitted_text,
|
||||
omitted_images: device.omitted_images,
|
||||
clips_seen: device.clips_seen,
|
||||
});
|
||||
}
|
||||
let mut warnings = Vec::new();
|
||||
if pages.iter().any(|page| page.omitted_text > 0) {
|
||||
warnings.push("text glyphs are not returned as paths".into());
|
||||
}
|
||||
if pages.iter().any(|page| page.omitted_images > 0) {
|
||||
warnings.push("raster images are not returned as paths".into());
|
||||
}
|
||||
if pages.iter().any(|page| page.clips_seen > 0) {
|
||||
warnings.push("clip paths are not applied to extracted geometry".into());
|
||||
}
|
||||
if truncated.load(Ordering::Relaxed) {
|
||||
warnings.push("PDF content was truncated by the interpreter".into());
|
||||
}
|
||||
Ok(Imported { pages, warnings })
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
struct Collector {
|
||||
paths: Vec<BezPath>,
|
||||
omitted_text: usize,
|
||||
omitted_images: usize,
|
||||
clips_seen: usize,
|
||||
max_paths: usize,
|
||||
max_elements: usize,
|
||||
elements: usize,
|
||||
exceeded: bool,
|
||||
}
|
||||
|
||||
impl Device<'_> for Collector {
|
||||
fn set_soft_mask(&mut self, _: Option<SoftMask<'_>>) {}
|
||||
fn set_blend_mode(&mut self, _: BlendMode) {}
|
||||
fn draw_path(&mut self, path: &BezPath, transform: Affine, _: &Paint<'_>, _: &PathDrawMode) {
|
||||
if path.is_empty() {
|
||||
return;
|
||||
}
|
||||
let count = path.elements().len();
|
||||
if self.paths.len() >= self.max_paths || self.elements.saturating_add(count) > self.max_elements {
|
||||
self.exceeded = true;
|
||||
return;
|
||||
}
|
||||
self.elements = self.elements.saturating_add(count);
|
||||
self.paths.push(transform * path.clone());
|
||||
}
|
||||
fn push_clip_path(&mut self, _: &ClipPath) {
|
||||
self.clips_seen = self.clips_seen.saturating_add(1);
|
||||
}
|
||||
fn push_transparency_group(&mut self, _: f32, _: Option<SoftMask<'_>>, _: BlendMode) {}
|
||||
fn draw_glyph(&mut self, _: &Glyph<'_>, _: Affine, _: Affine, _: &Paint<'_>, _: &GlyphDrawMode) {
|
||||
self.omitted_text = self.omitted_text.saturating_add(1);
|
||||
}
|
||||
fn draw_image(&mut self, _: Image<'_, '_>, _: Affine) {
|
||||
self.omitted_images = self.omitted_images.saturating_add(1);
|
||||
}
|
||||
fn pop_clip_path(&mut self) {}
|
||||
fn pop_transparency_group(&mut self) {}
|
||||
}
|
||||
@@ -30,6 +30,161 @@ use kurbo::{BezPath, PathEl, Point};
|
||||
/// The SVG namespace.
|
||||
pub const SVG_NS: &str = "http://www.w3.org/2000/svg";
|
||||
|
||||
/// A structured SVG scene. Applications provide geometry, styles and attributes; this module
|
||||
/// owns the SVG element tree and serializes it with XML escaping.
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
pub struct Scene {
|
||||
pub width_mm: f64,
|
||||
pub height_mm: f64,
|
||||
pub view_box: [f64; 4],
|
||||
pub elements: Vec<Element>,
|
||||
}
|
||||
|
||||
/// Structured SVG elements supported by the path-document writer.
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
pub enum Element {
|
||||
Group { attributes: Vec<Attribute>, children: Vec<Element> },
|
||||
Path { attributes: Vec<Attribute>, path: BezPath },
|
||||
Rect { attributes: Vec<Attribute>, x: f64, y: f64, width: f64, height: f64 },
|
||||
Text { attributes: Vec<Attribute>, x: f64, y: f64, text: String },
|
||||
Title(String),
|
||||
Style(String),
|
||||
NestedSvg { attributes: Vec<Attribute>, children: Vec<Element> },
|
||||
}
|
||||
|
||||
/// An escaped SVG attribute. Element names and attribute names are API constants and must be
|
||||
/// valid XML names; values are always escaped by the shared writer.
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
pub struct Attribute {
|
||||
pub name: String,
|
||||
pub value: Value,
|
||||
}
|
||||
|
||||
/// Attribute values written by the shared serializer.
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
pub enum Value {
|
||||
Text(String),
|
||||
Number(f64),
|
||||
Millimetres(f64),
|
||||
ViewBox([f64; 4]),
|
||||
TranslateScale { x: f64, y: f64, scale_x: f64, scale_y: f64 },
|
||||
}
|
||||
|
||||
impl Attribute {
|
||||
pub fn text(name: impl Into<String>, value: impl Into<String>) -> Self {
|
||||
Self { name: name.into(), value: Value::Text(value.into()) }
|
||||
}
|
||||
pub fn number(name: impl Into<String>, value: f64) -> Self {
|
||||
Self { name: name.into(), value: Value::Number(value) }
|
||||
}
|
||||
pub fn millimetres(name: impl Into<String>, value: f64) -> Self {
|
||||
Self { name: name.into(), value: Value::Millimetres(value) }
|
||||
}
|
||||
pub fn view_box(bounds: [f64; 4]) -> Self {
|
||||
Self { name: "viewBox".into(), value: Value::ViewBox(bounds) }
|
||||
}
|
||||
pub fn translate_scale(x: f64, y: f64, scale_x: f64, scale_y: f64) -> Self {
|
||||
Self { name: "transform".into(), value: Value::TranslateScale { x, y, scale_x, scale_y } }
|
||||
}
|
||||
}
|
||||
|
||||
impl Scene {
|
||||
/// Serialize the scene as an SVG 1.1 document.
|
||||
pub fn write(&self) -> String {
|
||||
let mut writer = SvgWriter::new();
|
||||
writer
|
||||
.root()
|
||||
.attr("version", "1.1")
|
||||
.attr("width", format!("{}mm", num(self.width_mm, 3)))
|
||||
.attr("height", format!("{}mm", num(self.height_mm, 3)))
|
||||
.attr("viewBox", view_box_data(self.view_box))
|
||||
.open();
|
||||
write_elements(&mut writer, &self.elements, 1);
|
||||
writer.line(0);
|
||||
writer.close();
|
||||
writer.finish()
|
||||
}
|
||||
}
|
||||
|
||||
fn write_attributes<'a>(mut tag: craft_xml::Tag<'a>, attributes: &'a [Attribute]) -> craft_xml::Tag<'a> {
|
||||
// SVG consumers disagree about duplicate attributes; make the last supplied value win.
|
||||
for (index, attribute) in attributes.iter().enumerate() {
|
||||
if attributes[index + 1..].iter().any(|later| later.name == attribute.name) {
|
||||
continue;
|
||||
}
|
||||
tag = match &attribute.value {
|
||||
Value::Text(value) => tag.attr(&attribute.name, value),
|
||||
Value::Number(value) => tag.attr(&attribute.name, num(*value, 3)),
|
||||
Value::Millimetres(value) => tag.attr(&attribute.name, format!("{}mm", num(*value, 3))),
|
||||
Value::ViewBox(bounds) => tag.attr(&attribute.name, view_box_data(*bounds)),
|
||||
Value::TranslateScale { x, y, scale_x, scale_y } => {
|
||||
tag.attr(&attribute.name, format!("translate({} {}) scale({} {})", num(*x, 3), num(*y, 3), shortest(*scale_x), shortest(*scale_y)))
|
||||
}
|
||||
};
|
||||
}
|
||||
tag
|
||||
}
|
||||
|
||||
fn shortest(value: f64) -> String {
|
||||
if value.is_finite() { value.to_string() } else { "0".into() }
|
||||
}
|
||||
|
||||
fn write_elements(writer: &mut SvgWriter, elements: &[Element], depth: usize) {
|
||||
for element in elements {
|
||||
writer.line(depth);
|
||||
match element {
|
||||
Element::Group { attributes, children } => {
|
||||
write_attributes(writer.tag("g"), attributes).open();
|
||||
write_elements(writer, children, depth + 1);
|
||||
writer.line(depth);
|
||||
writer.close();
|
||||
}
|
||||
Element::Path { attributes, path } => {
|
||||
let mut attrs = vec![Attribute::text("d", path_data(path))];
|
||||
attrs.extend(attributes.iter().cloned());
|
||||
write_attributes(writer.tag("path"), &attrs).empty();
|
||||
}
|
||||
Element::Rect { attributes, x, y, width, height } => {
|
||||
let mut attrs = vec![
|
||||
Attribute::number("x", *x),
|
||||
Attribute::number("y", *y),
|
||||
Attribute::number("width", *width),
|
||||
Attribute::number("height", *height),
|
||||
];
|
||||
attrs.extend(attributes.iter().cloned());
|
||||
write_attributes(writer.tag("rect"), &attrs).empty();
|
||||
}
|
||||
Element::Text { attributes, x, y, text } => {
|
||||
let mut attrs = vec![Attribute::number("x", *x), Attribute::number("y", *y)];
|
||||
attrs.extend(attributes.iter().cloned());
|
||||
write_attributes(writer.tag("text"), &attrs).open();
|
||||
writer.text(text);
|
||||
writer.close();
|
||||
}
|
||||
Element::Title(text) => {
|
||||
writer.tag("title").open();
|
||||
writer.text(text);
|
||||
writer.close();
|
||||
}
|
||||
Element::Style(css) => {
|
||||
writer.tag("style").open();
|
||||
writer.text(css);
|
||||
writer.close();
|
||||
}
|
||||
Element::NestedSvg { attributes, children } => {
|
||||
write_attributes(writer.tag("svg"), attributes).open();
|
||||
write_elements(writer, children, depth + 1);
|
||||
writer.line(depth);
|
||||
writer.close();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn view_box_data(bounds: [f64; 4]) -> String {
|
||||
format!("{} {} {} {}", shortest(bounds[0]), shortest(bounds[1]), shortest(bounds[2]), shortest(bounds[3]))
|
||||
}
|
||||
|
||||
/// A number written with a fixed count of decimals. A value that is not finite is written as
|
||||
/// zero, because SVG has no spelling for it.
|
||||
#[derive(Debug, Clone, Copy, PartialEq)]
|
||||
|
||||
Reference in new issue
Block a user