mirror of
https://github.com/storytold/deckcraft.git
synced 2026-11-12 08:51:27 -06:00
Compare commits
59
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ac3e46b748 | ||
|
|
2127bd1bd8 | ||
|
|
eec4f59535 | ||
|
|
988a492097 | ||
|
|
2406ce46ec | ||
|
|
60f91f7806 | ||
|
|
2ef5f31bc8 | ||
|
|
c6846f6a75 | ||
|
|
a3f2f215b0 | ||
|
|
35166b3841 | ||
|
|
b57b4ccfcf | ||
|
|
2fbdcfe5f5 | ||
|
|
98eac5590d | ||
|
|
46712d0293 | ||
|
|
e03c22c445 | ||
|
|
98208a9eda | ||
|
|
de52b6b766 | ||
|
|
7c83fa43ac | ||
|
|
7fe979aa24 | ||
|
|
690171665a | ||
|
|
203fa79211 | ||
|
|
3e2e73aab7 | ||
|
|
cf0d7fbb14 | ||
|
|
121c59d521 | ||
|
|
a55457ee53 | ||
|
|
7bd141c1c9 | ||
|
|
e3e218a8fc | ||
|
|
c532ae749b | ||
|
|
e0f3639e94 | ||
|
|
eb10e2b9b3 | ||
|
|
92a0437ab6 | ||
|
|
4e87f2b850 | ||
|
|
54846750d4 | ||
|
|
1e9c392f67 | ||
|
|
c3fb184e9d | ||
|
|
0623b43032 | ||
|
|
5ee82b4e45 | ||
|
|
61423ea35b | ||
|
|
c4674f24fb | ||
|
|
4ba9a1a5d9 | ||
|
|
3504a9da42 | ||
|
|
ccab980657 | ||
|
|
48e2203123 | ||
|
|
76bd26cca0 | ||
|
|
02fbb6a976 | ||
|
|
c16db2ef3e | ||
|
|
809c44654d | ||
|
|
220b52dfdb | ||
|
|
424f575ca5 | ||
|
|
aefd54d28e | ||
|
|
25809faf1a | ||
|
|
fc35ad67c6 | ||
|
|
57780f9e61 | ||
|
|
0627fb2589 | ||
|
|
170c5591b5 | ||
|
|
a883d6b2d9 | ||
|
|
4b1e50a6b0 | ||
|
|
c7919102ac | ||
|
|
159ed07eb3 |
@@ -26,6 +26,10 @@ jobs:
|
||||
# Same craft-fonts commit as release.yml.
|
||||
CRAFT_FONTS_REF: 37827adc41fd165f9a9234ac40f94dbad353aa69
|
||||
steps:
|
||||
- name: Harden runner
|
||||
uses: step-security/harden-runner@1d88384fd173af6ecb012dfd0264a306b711241a # v2.22.4
|
||||
with:
|
||||
egress-policy: audit
|
||||
- name: Check out craft-fonts
|
||||
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
with:
|
||||
|
||||
@@ -27,6 +27,10 @@ jobs:
|
||||
name: Windows installation policy (no app build)
|
||||
runs-on: windows-latest
|
||||
steps:
|
||||
- name: Harden runner
|
||||
uses: step-security/harden-runner@1d88384fd173af6ecb012dfd0264a306b711241a # v2.22.4
|
||||
with:
|
||||
egress-policy: audit
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
@@ -1,6 +1,10 @@
|
||||
name: Release
|
||||
|
||||
on:
|
||||
# A nightly dry run on main: every release build except macOS, nothing signed or released
|
||||
# (craft-common-actions release.yml `dry-run`). Finds release-only breakage before release day.
|
||||
schedule:
|
||||
- cron: "16 1 * * *"
|
||||
push:
|
||||
branches: [release]
|
||||
workflow_dispatch:
|
||||
@@ -30,6 +34,7 @@ jobs:
|
||||
# Intentionally tracked at @main (not SHA-pinned) so org-wide CI improvements, security fixes, and build optimizations propagate without PR churn across all craftapp repos.
|
||||
uses: storytold/craft-common-actions/.github/workflows/release.yml@main
|
||||
with:
|
||||
dry-run: ${{ github.event_name == 'schedule' }}
|
||||
app-name: deckcraft
|
||||
app-title: DeckCraft
|
||||
# Keep identical to ci.yml and freebsd.yml. The shared release workflow sets
|
||||
|
||||
Generated
+67
-68
@@ -844,15 +844,24 @@ dependencies = [
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "craft-audio"
|
||||
version = "0.1.0"
|
||||
source = "git+https://github.com/storytold/craft-libs?rev=75895c0321d8cb6e175a42466cba050352b117c5#75895c0321d8cb6e175a42466cba050352b117c5"
|
||||
dependencies = [
|
||||
"craft-media",
|
||||
"thiserror 2.0.21",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "craft-fonts-build"
|
||||
version = "0.1.0"
|
||||
source = "git+https://github.com/storytold/craft-libs?rev=c5a1cbfb5f9049f9270955caffb5bee5997aebf7#c5a1cbfb5f9049f9270955caffb5bee5997aebf7"
|
||||
source = "git+https://github.com/storytold/craft-libs?rev=6e03f65bf45181db77ba6229716e6624db9eb25b#6e03f65bf45181db77ba6229716e6624db9eb25b"
|
||||
|
||||
[[package]]
|
||||
name = "craft-i18n"
|
||||
version = "0.1.0"
|
||||
source = "git+https://github.com/storytold/craft-libs?rev=c5a1cbfb5f9049f9270955caffb5bee5997aebf7#c5a1cbfb5f9049f9270955caffb5bee5997aebf7"
|
||||
source = "git+https://github.com/storytold/craft-libs?rev=6e03f65bf45181db77ba6229716e6624db9eb25b#6e03f65bf45181db77ba6229716e6624db9eb25b"
|
||||
dependencies = [
|
||||
"fixed_decimal",
|
||||
"icu_decimal",
|
||||
@@ -875,15 +884,26 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "craft-media"
|
||||
version = "0.1.0"
|
||||
source = "git+https://github.com/storytold/craft-libs?rev=c5a1cbfb5f9049f9270955caffb5bee5997aebf7#c5a1cbfb5f9049f9270955caffb5bee5997aebf7"
|
||||
source = "git+https://github.com/storytold/craft-libs?rev=75895c0321d8cb6e175a42466cba050352b117c5#75895c0321d8cb6e175a42466cba050352b117c5"
|
||||
dependencies = [
|
||||
"thiserror 2.0.21",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "craft-metafile"
|
||||
version = "0.1.0"
|
||||
source = "git+https://github.com/storytold/craft-libs?rev=8079878ae2b166c1ea8229e40eff7e08c916e532#8079878ae2b166c1ea8229e40eff7e08c916e532"
|
||||
dependencies = [
|
||||
"encoding_rs",
|
||||
"image",
|
||||
"kurbo",
|
||||
"thiserror 2.0.21",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "craft-pdf"
|
||||
version = "0.1.0"
|
||||
source = "git+https://github.com/storytold/craft-libs?rev=c5a1cbfb5f9049f9270955caffb5bee5997aebf7#c5a1cbfb5f9049f9270955caffb5bee5997aebf7"
|
||||
source = "git+https://github.com/storytold/craft-libs?rev=6e03f65bf45181db77ba6229716e6624db9eb25b#6e03f65bf45181db77ba6229716e6624db9eb25b"
|
||||
dependencies = [
|
||||
"krilla",
|
||||
"thiserror 2.0.21",
|
||||
@@ -892,7 +912,7 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "craft-ui"
|
||||
version = "0.1.0"
|
||||
source = "git+https://github.com/storytold/craft-libs?rev=c5a1cbfb5f9049f9270955caffb5bee5997aebf7#c5a1cbfb5f9049f9270955caffb5bee5997aebf7"
|
||||
source = "git+https://github.com/storytold/craft-libs?rev=6e03f65bf45181db77ba6229716e6624db9eb25b#6e03f65bf45181db77ba6229716e6624db9eb25b"
|
||||
dependencies = [
|
||||
"craft-fonts-build",
|
||||
"craft-i18n",
|
||||
@@ -912,7 +932,7 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "craft-update"
|
||||
version = "0.1.0"
|
||||
source = "git+https://github.com/storytold/craft-libs?rev=c5a1cbfb5f9049f9270955caffb5bee5997aebf7#c5a1cbfb5f9049f9270955caffb5bee5997aebf7"
|
||||
source = "git+https://github.com/storytold/craft-libs?rev=6e03f65bf45181db77ba6229716e6624db9eb25b#6e03f65bf45181db77ba6229716e6624db9eb25b"
|
||||
dependencies = [
|
||||
"chrono",
|
||||
"dunce",
|
||||
@@ -922,6 +942,7 @@ dependencies = [
|
||||
"image",
|
||||
"log",
|
||||
"plist",
|
||||
"rustix 1.1.5",
|
||||
"semver",
|
||||
"serde",
|
||||
"serde_json",
|
||||
@@ -935,10 +956,21 @@ dependencies = [
|
||||
"zip 9.1.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "craft-web-api"
|
||||
version = "0.1.0"
|
||||
source = "git+https://github.com/storytold/craft-libs?rev=fd5e2feff6ea25bed371a99cc732b3968d621505#fd5e2feff6ea25bed371a99cc732b3968d621505"
|
||||
dependencies = [
|
||||
"js-sys",
|
||||
"serde_json",
|
||||
"wasm-bindgen",
|
||||
"wasm-bindgen-futures",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "craft-web-host"
|
||||
version = "0.1.0"
|
||||
source = "git+https://github.com/storytold/craft-libs?rev=2e2312ddaf21bc3d85cd3395d0955c7d239306f6#2e2312ddaf21bc3d85cd3395d0955c7d239306f6"
|
||||
source = "git+https://github.com/storytold/craft-libs?rev=fd5e2feff6ea25bed371a99cc732b3968d621505#fd5e2feff6ea25bed371a99cc732b3968d621505"
|
||||
dependencies = [
|
||||
"wasm-bindgen",
|
||||
"web-sys",
|
||||
@@ -1017,7 +1049,7 @@ checksum = "0c87e182de0887fd5361989c677c4e8f5000cd9491d6d563161a8f3a5519fc7f"
|
||||
|
||||
[[package]]
|
||||
name = "deckcraft"
|
||||
version = "0.15.0"
|
||||
version = "0.18.1"
|
||||
dependencies = [
|
||||
"arboard",
|
||||
"chrono",
|
||||
@@ -1039,7 +1071,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "deckcraft-anim"
|
||||
version = "0.15.0"
|
||||
version = "0.18.1"
|
||||
dependencies = [
|
||||
"deckcraft-geom",
|
||||
"deckcraft-model",
|
||||
@@ -1048,7 +1080,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "deckcraft-av1"
|
||||
version = "0.15.0"
|
||||
version = "0.18.1"
|
||||
dependencies = [
|
||||
"deckcraft-bitstream",
|
||||
"rayon",
|
||||
@@ -1057,14 +1089,14 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "deckcraft-bitstream"
|
||||
version = "0.15.0"
|
||||
version = "0.18.1"
|
||||
dependencies = [
|
||||
"craft-media",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "deckcraft-cli"
|
||||
version = "0.15.0"
|
||||
version = "0.18.1"
|
||||
dependencies = [
|
||||
"deckcraft-engine",
|
||||
"deckcraft-mcp",
|
||||
@@ -1074,14 +1106,14 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "deckcraft-color"
|
||||
version = "0.15.0"
|
||||
version = "0.18.1"
|
||||
dependencies = [
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "deckcraft-engine"
|
||||
version = "0.15.0"
|
||||
version = "0.18.1"
|
||||
dependencies = [
|
||||
"chrono",
|
||||
"deckcraft-anim",
|
||||
@@ -1107,7 +1139,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "deckcraft-fonts"
|
||||
version = "0.15.0"
|
||||
version = "0.18.1"
|
||||
dependencies = [
|
||||
"epaint_default_fonts",
|
||||
"harfrust 0.13.3",
|
||||
@@ -1119,7 +1151,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "deckcraft-format"
|
||||
version = "0.15.0"
|
||||
version = "0.18.1"
|
||||
dependencies = [
|
||||
"deckcraft-model",
|
||||
"serde",
|
||||
@@ -1130,7 +1162,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "deckcraft-geom"
|
||||
version = "0.15.0"
|
||||
version = "0.18.1"
|
||||
dependencies = [
|
||||
"kurbo",
|
||||
"serde",
|
||||
@@ -1138,7 +1170,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "deckcraft-h264"
|
||||
version = "0.15.0"
|
||||
version = "0.18.1"
|
||||
dependencies = [
|
||||
"deckcraft-bitstream",
|
||||
"rayon",
|
||||
@@ -1147,30 +1179,16 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "deckcraft-hevc"
|
||||
version = "0.15.0"
|
||||
version = "0.18.1"
|
||||
dependencies = [
|
||||
"deckcraft-bitstream",
|
||||
"rayon",
|
||||
"thiserror 2.0.21",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "deckcraft-isobmff"
|
||||
version = "0.15.0"
|
||||
dependencies = [
|
||||
"craft-media",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "deckcraft-matroska"
|
||||
version = "0.15.0"
|
||||
dependencies = [
|
||||
"craft-media",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "deckcraft-mcp"
|
||||
version = "0.15.0"
|
||||
version = "0.18.1"
|
||||
dependencies = [
|
||||
"deckcraft-engine",
|
||||
"deckcraft-render",
|
||||
@@ -1180,15 +1198,13 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "deckcraft-media"
|
||||
version = "0.15.0"
|
||||
version = "0.18.1"
|
||||
dependencies = [
|
||||
"craft-audio",
|
||||
"craft-media",
|
||||
"deckcraft-av1",
|
||||
"deckcraft-h264",
|
||||
"deckcraft-hevc",
|
||||
"deckcraft-isobmff",
|
||||
"deckcraft-matroska",
|
||||
"deckcraft-ogg",
|
||||
"deckcraft-opus",
|
||||
"deckcraft-vp9",
|
||||
"image",
|
||||
"log",
|
||||
@@ -1196,18 +1212,9 @@ dependencies = [
|
||||
"thiserror 2.0.21",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "deckcraft-metafile"
|
||||
version = "0.15.0"
|
||||
dependencies = [
|
||||
"image",
|
||||
"kurbo",
|
||||
"thiserror 2.0.21",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "deckcraft-model"
|
||||
version = "0.15.0"
|
||||
version = "0.18.1"
|
||||
dependencies = [
|
||||
"deckcraft-color",
|
||||
"deckcraft-geom",
|
||||
@@ -1217,17 +1224,9 @@ dependencies = [
|
||||
"unicode-segmentation",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "deckcraft-ogg"
|
||||
version = "0.15.0"
|
||||
|
||||
[[package]]
|
||||
name = "deckcraft-opus"
|
||||
version = "0.15.0"
|
||||
|
||||
[[package]]
|
||||
name = "deckcraft-pdf"
|
||||
version = "0.15.0"
|
||||
version = "0.18.1"
|
||||
dependencies = [
|
||||
"craft-pdf",
|
||||
"deckcraft-fonts",
|
||||
@@ -1245,7 +1244,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "deckcraft-pptx"
|
||||
version = "0.15.0"
|
||||
version = "0.18.1"
|
||||
dependencies = [
|
||||
"deckcraft-color",
|
||||
"deckcraft-engine",
|
||||
@@ -1261,13 +1260,13 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "deckcraft-render"
|
||||
version = "0.15.0"
|
||||
version = "0.18.1"
|
||||
dependencies = [
|
||||
"craft-metafile",
|
||||
"deckcraft-anim",
|
||||
"deckcraft-color",
|
||||
"deckcraft-fonts",
|
||||
"deckcraft-geom",
|
||||
"deckcraft-metafile",
|
||||
"deckcraft-model",
|
||||
"deckcraft-text",
|
||||
"image",
|
||||
@@ -1280,7 +1279,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "deckcraft-text"
|
||||
version = "0.15.0"
|
||||
version = "0.18.1"
|
||||
dependencies = [
|
||||
"chrono",
|
||||
"deckcraft-color",
|
||||
@@ -1294,7 +1293,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "deckcraft-ui-egui"
|
||||
version = "0.15.0"
|
||||
version = "0.18.1"
|
||||
dependencies = [
|
||||
"craft-ui",
|
||||
"craft-update",
|
||||
@@ -1320,7 +1319,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "deckcraft-vp9"
|
||||
version = "0.15.0"
|
||||
version = "0.18.1"
|
||||
dependencies = [
|
||||
"deckcraft-bitstream",
|
||||
"rayon",
|
||||
@@ -1329,8 +1328,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "deckcraft-web"
|
||||
version = "0.15.0"
|
||||
version = "0.18.1"
|
||||
dependencies = [
|
||||
"craft-web-api",
|
||||
"craft-web-host",
|
||||
"deckcraft-engine",
|
||||
"deckcraft-ui-egui",
|
||||
@@ -2779,7 +2779,7 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "krilla"
|
||||
version = "0.8.2"
|
||||
source = "git+https://github.com/storytold/craft-libs?rev=c5a1cbfb5f9049f9270955caffb5bee5997aebf7#c5a1cbfb5f9049f9270955caffb5bee5997aebf7"
|
||||
source = "git+https://github.com/storytold/craft-libs?rev=6e03f65bf45181db77ba6229716e6624db9eb25b#6e03f65bf45181db77ba6229716e6624db9eb25b"
|
||||
dependencies = [
|
||||
"base64 0.22.1",
|
||||
"bumpalo",
|
||||
@@ -6313,7 +6313,7 @@ checksum = "9440ea3e5aeabb0ac63af70daf835274065238cdd0cec83418f417eae38bacee"
|
||||
|
||||
[[package]]
|
||||
name = "xtask"
|
||||
version = "0.15.0"
|
||||
version = "0.18.1"
|
||||
dependencies = [
|
||||
"serde_json",
|
||||
]
|
||||
@@ -6559,7 +6559,6 @@ dependencies = [
|
||||
"nix",
|
||||
"typed-path",
|
||||
"windows-sys 0.61.2",
|
||||
"zopfli",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
||||
+11
-12
@@ -4,7 +4,7 @@ members = ["crates/*", "apps/*", "xtask"]
|
||||
default-members = ["crates/*", "apps/deckcraft", "apps/deckcraft-cli"]
|
||||
|
||||
[workspace.package]
|
||||
version = "0.15.0"
|
||||
version = "0.18.1"
|
||||
edition = "2024"
|
||||
license = "MIT OR Apache-2.0"
|
||||
rust-version = "1.95"
|
||||
@@ -14,7 +14,6 @@ repository = "https://github.com/storytold/deckcraft"
|
||||
# internal
|
||||
deckcraft-geom = { path = "crates/geom" }
|
||||
deckcraft-color = { path = "crates/color" }
|
||||
deckcraft-metafile = { path = "crates/metafile" }
|
||||
deckcraft-model = { path = "crates/model" }
|
||||
deckcraft-fonts = { path = "crates/fonts" }
|
||||
deckcraft-text = { path = "crates/text" }
|
||||
@@ -27,16 +26,12 @@ deckcraft-engine = { path = "crates/engine" }
|
||||
deckcraft-mcp = { path = "crates/mcp" }
|
||||
deckcraft-ui-egui = { path = "crates/ui-egui" }
|
||||
deckcraft-media = { path = "crates/media" }
|
||||
# codecs (ported from FilmCraft, same owner and licence)
|
||||
# video codecs (ported from FilmCraft, same owner and licence)
|
||||
deckcraft-bitstream = { path = "crates/bitstream" }
|
||||
deckcraft-h264 = { path = "crates/h264" }
|
||||
deckcraft-hevc = { path = "crates/hevc" }
|
||||
deckcraft-vp9 = { path = "crates/vp9" }
|
||||
deckcraft-av1 = { path = "crates/av1" }
|
||||
deckcraft-isobmff = { path = "crates/isobmff" }
|
||||
deckcraft-matroska = { path = "crates/matroska" }
|
||||
deckcraft-ogg = { path = "crates/ogg" }
|
||||
deckcraft-opus = { path = "crates/opus" }
|
||||
|
||||
# foundation
|
||||
kurbo = { version = "0.13", features = ["serde"] }
|
||||
@@ -59,11 +54,15 @@ harfrust = "0.13"
|
||||
vello_cpu = "0.2"
|
||||
vello_common = { version = "0.2", default-features = false }
|
||||
image = { version = "0.25", default-features = false, features = ["png", "jpeg", "webp", "gif", "tiff", "bmp"] }
|
||||
craft-media = { git = "https://github.com/storytold/craft-libs", rev = "c5a1cbfb5f9049f9270955caffb5bee5997aebf7", version = "=0.1.0", default-features = false }
|
||||
craft-pdf = { git = "https://github.com/storytold/craft-libs", rev = "c5a1cbfb5f9049f9270955caffb5bee5997aebf7", version = "=0.1.0", default-features = false, features = ["write", "write-simple-text"] }
|
||||
# craft-media and craft-audio share types and stay on one rev.
|
||||
craft-media = { git = "https://github.com/storytold/craft-libs", rev = "75895c0321d8cb6e175a42466cba050352b117c5", version = "=0.1.0", default-features = false }
|
||||
craft-audio = { git = "https://github.com/storytold/craft-libs", rev = "75895c0321d8cb6e175a42466cba050352b117c5", version = "=0.1.0", default-features = false, features = ["opus-decode"] }
|
||||
# WMF, EMF and EMF+ reader.
|
||||
craft-metafile = { git = "https://github.com/storytold/craft-libs", rev = "8079878ae2b166c1ea8229e40eff7e08c916e532", version = "=0.1.0" }
|
||||
craft-pdf = { git = "https://github.com/storytold/craft-libs", rev = "6e03f65bf45181db77ba6229716e6624db9eb25b", version = "=0.1.0", default-features = false, features = ["write", "write-simple-text"] }
|
||||
# craft-update: app-specific pretend versions apply to startup and manual checks.
|
||||
craft-update = { git = "https://github.com/storytold/craft-libs", version = "=0.1.0", rev = "c5a1cbfb5f9049f9270955caffb5bee5997aebf7", features = ["egui"] }
|
||||
craft-ui = { git = "https://github.com/storytold/craft-libs", rev = "c5a1cbfb5f9049f9270955caffb5bee5997aebf7", version = "=0.1.0" }
|
||||
craft-update = { git = "https://github.com/storytold/craft-libs", version = "=0.1.0", rev = "6e03f65bf45181db77ba6229716e6624db9eb25b", features = ["egui"] }
|
||||
craft-ui = { git = "https://github.com/storytold/craft-libs", rev = "6e03f65bf45181db77ba6229716e6624db9eb25b", version = "=0.1.0" }
|
||||
i_overlay = "9"
|
||||
quick-xml = "0.41"
|
||||
zip = { version = "3", default-features = false, features = ["deflate"] }
|
||||
@@ -109,7 +108,7 @@ codegen-units = 16
|
||||
debug = 0
|
||||
|
||||
[patch.crates-io]
|
||||
krilla = { git = "https://github.com/storytold/craft-libs", rev = "c5a1cbfb5f9049f9270955caffb5bee5997aebf7", version = "=0.8.2" }
|
||||
krilla = { git = "https://github.com/storytold/craft-libs", rev = "6e03f65bf45181db77ba6229716e6624db9eb25b", version = "=0.8.2" }
|
||||
ecolor = { git = "https://github.com/storytold/egui.git", rev = "6d518fc799681571963842f95b55a6b68431b15a" }
|
||||
emath = { git = "https://github.com/storytold/egui.git", rev = "6d518fc799681571963842f95b55a6b68431b15a" }
|
||||
epaint = { git = "https://github.com/storytold/egui.git", rev = "6d518fc799681571963842f95b55a6b68431b15a" }
|
||||
|
||||
@@ -76,8 +76,9 @@ Code dependencies
|
||||
- symphonia (audio decoding for MP3, AAC, ALAC, FLAC, Vorbis, WAV, AIFF, CAF) is used unmodified
|
||||
under the Mozilla Public License 2.0; its source is available at
|
||||
https://github.com/pdeljanov/Symphonia.
|
||||
- The H.264, HEVC, VP9, AV1, Opus, MP4, Matroska and Ogg crates under crates/ are ported from
|
||||
FilmCraft (same owner, MIT OR Apache-2.0).
|
||||
- The H.264, HEVC, VP9 and AV1 crates under crates/ are ported from FilmCraft (same owner,
|
||||
MIT OR Apache-2.0). The Opus decoder (craft-audio) and the MP4, Matroska and Ogg demuxers
|
||||
(craft-media) come from craft-libs (same owner, MIT OR Apache-2.0).
|
||||
|
||||
- Krilla 0.8.2 (https://github.com/LaurenzV/krilla) is used through craft-pdf under
|
||||
MIT OR Apache-2.0. craft-libs vendors that version with page/surface unwind recovery
|
||||
|
||||
+3
-1
@@ -2,7 +2,7 @@
|
||||
|
||||
**Stage: alpha** · next: beta, ~20 points and ~120–210 h away
|
||||
|
||||
> **Last reviewed:** 2026-11-04 · **Last updated:** 2026-11-04 · **Change:** trivial (Ogg Opus recovery includes trailing packets without granules in its reported duration; parity scores unchanged) · **Target:** Microsoft PowerPoint for Mac 16.113 (Microsoft 365)
|
||||
> **Last reviewed:** 2026-11-04 · **Last updated:** 2026-11-11 · **Change:** trivial (the Opus decoder and the Ogg, MP4 and Matroska demuxers come from craft-libs; parity scores unchanged) · **Target:** Microsoft PowerPoint for Mac 16.113 (Microsoft 365)
|
||||
> **Last reviewed:** 2026-11-02 · **Last updated:** 2026-11-03 · **Change:** trivial (progress log: web `?host=parent` bridge opens and saves through a same-origin framing page; scores unchanged) · **Target:** Microsoft PowerPoint for Mac 16.113 (Microsoft 365)
|
||||
|
||||
> **Last reviewed:** 2026-11-02 · **Last updated:** 2026-11-03 · **Change:** trivial (community PR wave 10: PPTX and native saves keep table cell centred anchoring, text overflow, compatible line spacing, normalised character height, Thai distributed alignment, RTL table direction, alternate run language and reflection transforms; imported effects placed by parent time-node delays; Reuse Slides fits the destination size and brings custom table styles; deleted slides leave custom shows; merged shapes drop their animations; Compress Pictures measures grouped pictures at displayed size; parity scores unchanged) · **Target:** Microsoft PowerPoint for Mac 16.113 (Microsoft 365)
|
||||
@@ -154,6 +154,7 @@ app's JSON control channel (`deckcraft --control PORT`).
|
||||
|
||||
| Date | What landed |
|
||||
|---|---|
|
||||
| 2026-11-11 | Media: the Opus decoder and the Ogg, MP4/MOV and Matroska demuxers are craft-audio and craft-media from craft-libs; DeckCraft's own copies and re-export crates are removed |
|
||||
| 2026-11-04 | Ogg Opus recovery keeps trailing packets without granules in the total duration, matching their recovered positions; valid end trimming stays covered |
|
||||
| 2026-11-03 | Web: the `?host=parent` bridge (as in GridCraft, WordCraft and PdfCraft) lets a same-origin page that frames DeckCraft send a presentation and store its saves; Save writes an opened .pptx back as .pptx under its own name, as on the desktop |
|
||||
| 2026-11-02 | Slide show on Linux and the BSDs: Presenter View stays off when winit lists exactly one display, as in PowerPoint; if it lists none, the setting is kept. On one display the presenter window covered the show, and the hidden window stalled every frame: slide changes took up to ~1.7 s under Hyprland (#1156) |
|
||||
@@ -340,6 +341,7 @@ app's JSON control channel (`deckcraft --control PORT`).
|
||||
|
||||
| Date | Change | Summary |
|
||||
|---|---|---|
|
||||
| 2026-11-11 | trivial | Opus and container code moved to craft-libs crates; parity scores unchanged |
|
||||
| 2026-11-04 | trivial | Ogg Opus recovery duration includes its recovered tail; parity scores unchanged |
|
||||
| 2026-11-02 | trivial | Progress log: run alternate-language metadata round-trips through native and PPTX formats; product parity scores unchanged |
|
||||
| 2026-11-03 | trivial | Reuse Slides brings referenced custom table styles along; scores unchanged |
|
||||
|
||||
@@ -16,8 +16,10 @@ path = "src/main.rs"
|
||||
[dependencies]
|
||||
deckcraft-engine = { workspace = true }
|
||||
deckcraft-ui-egui = { workspace = true }
|
||||
# The page command API (storytold/craft-libs#413): window.deckcraft over the control channel.
|
||||
craft-web-api = { git = "https://github.com/storytold/craft-libs", rev = "fd5e2feff6ea25bed371a99cc732b3968d621505", version = "0.1.0" }
|
||||
# The host messages (storytold/craft-libs): one contract and one bridge for the web builds.
|
||||
craft-web-host = { git = "https://github.com/storytold/craft-libs", rev = "2e2312ddaf21bc3d85cd3395d0955c7d239306f6", version = "0.1.0" }
|
||||
craft-web-host = { git = "https://github.com/storytold/craft-libs", rev = "fd5e2feff6ea25bed371a99cc732b3968d621505", version = "0.1.0" }
|
||||
egui = { workspace = true }
|
||||
log = { workspace = true }
|
||||
serde_json = { workspace = true }
|
||||
|
||||
@@ -0,0 +1,151 @@
|
||||
//! `window.deckcraft`: the page command API (`craft-web-api`, storytold/craft-libs#413) over the
|
||||
//! app's own control channel, the one the desktop control server drives (`docs/control-protocol.md`).
|
||||
//!
|
||||
//! The crate owns the JavaScript side (`request`, `execute`, `commands`, `docInspect`, `uiInspect`,
|
||||
//! `screenshot`, as promises with a timeout). This module is DeckCraft's part: a [`Request`] that
|
||||
//! queues a [`ControlRequest`] for the app, which answers it on its next frame, and waits for the
|
||||
//! reply. A call the page gave up on is released ([`Pending`]), so the app skips it if it has not started it, instead of
|
||||
//! applying a command the page was told had failed.
|
||||
|
||||
use std::sync::Arc;
|
||||
use std::sync::atomic::AtomicU8;
|
||||
use std::sync::mpsc::{Receiver, TryRecvError};
|
||||
|
||||
use deckcraft_ui_egui::ControlRequest;
|
||||
use serde_json::{Value, json};
|
||||
|
||||
/// How often a waiting call looks for the app's reply, in milliseconds (about a frame).
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
const POLL_MS: i32 = 16;
|
||||
|
||||
/// A queued control request whose reply has not arrived. Dropping it before the reply (the page's
|
||||
/// timeout fired, or the call was abandoned) releases the request: the app skips it if it has not started it (one it has taken still finishes).
|
||||
struct Pending {
|
||||
reply: Receiver<Value>,
|
||||
state: Arc<AtomicU8>,
|
||||
answered: bool,
|
||||
}
|
||||
|
||||
impl Pending {
|
||||
/// `Some` once the app has answered or gone away; `None` while the request is still waiting.
|
||||
fn poll(&mut self) -> Option<Value> {
|
||||
match self.reply.try_recv() {
|
||||
Ok(reply) => {
|
||||
self.answered = true;
|
||||
Some(reply)
|
||||
}
|
||||
Err(TryRecvError::Empty) => None,
|
||||
Err(TryRecvError::Disconnected) => {
|
||||
self.answered = true;
|
||||
Some(json!({"ok": false, "error": "the app dropped the request"}))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for Pending {
|
||||
fn drop(&mut self) {
|
||||
if !self.answered {
|
||||
ControlRequest::abandon(&self.state);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
pub use web::install;
|
||||
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
mod web {
|
||||
use std::rc::Rc;
|
||||
use std::sync::mpsc::channel;
|
||||
|
||||
use craft_web_api::methods::Methods;
|
||||
use craft_web_api::page::{PageApi, Request};
|
||||
use deckcraft_ui_egui::{ControlRequest, SlideApp};
|
||||
use serde_json::json;
|
||||
use wasm_bindgen_futures::JsFuture;
|
||||
|
||||
use super::{POLL_MS, Pending};
|
||||
|
||||
/// The app id in `window.<app>`.
|
||||
const APP: &str = "deckcraft";
|
||||
|
||||
/// Give the app its control queue and put `window.deckcraft` on the page. An existing
|
||||
/// `window.deckcraft` keeps the functions it already has.
|
||||
pub fn install(app: &mut SlideApp, ctx: egui::Context) {
|
||||
let (queue, requests) = channel::<ControlRequest>();
|
||||
app.control_rx = Some(requests);
|
||||
let request: Request = Rc::new(move |method, params| {
|
||||
let (control, reply) = ControlRequest::new(method, params);
|
||||
let mut pending = Pending { reply, state: control.state.clone(), answered: false };
|
||||
if queue.send(control).is_err() {
|
||||
pending.answered = true;
|
||||
return Box::pin(async { json!({"ok": false, "error": "the app is not running"}) });
|
||||
}
|
||||
ctx.request_repaint();
|
||||
Box::pin(async move {
|
||||
loop {
|
||||
if let Some(reply) = pending.poll() {
|
||||
return reply;
|
||||
}
|
||||
sleep(POLL_MS).await;
|
||||
}
|
||||
})
|
||||
});
|
||||
if let Err(e) = Rc::new(PageApi::new(APP, Methods::default(), request)).install() {
|
||||
log::warn!("couldn't install window.{APP}: {e:?}");
|
||||
}
|
||||
}
|
||||
|
||||
async fn sleep(ms: i32) {
|
||||
let promise = js_sys::Promise::new(&mut |resolve, _| {
|
||||
if let Some(window) = web_sys::window() {
|
||||
let _ = window.set_timeout_with_callback_and_timeout_and_arguments_0(&resolve, ms);
|
||||
}
|
||||
});
|
||||
let _ = JsFuture::from(promise).await;
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use std::sync::atomic::Ordering;
|
||||
|
||||
use super::*;
|
||||
|
||||
/// `ControlRequest`'s `state` values (`deckcraft_ui_egui::control`): the app has started it, or its
|
||||
/// sender gave up.
|
||||
const TAKEN: u8 = 1;
|
||||
const ABANDONED: u8 = 2;
|
||||
|
||||
fn pending() -> (Pending, ControlRequest) {
|
||||
let (control, reply) = ControlRequest::new("document.inspect", json!({}));
|
||||
(Pending { reply, state: control.state.clone(), answered: false }, control)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_call_the_page_gave_up_on_is_released_so_the_app_skips_it() {
|
||||
let (waiting, control) = pending();
|
||||
drop(waiting);
|
||||
assert_eq!(control.state.load(Ordering::SeqCst), ABANDONED);
|
||||
assert!(!control.take(), "the app must not run a request the page was told had failed");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn an_answered_call_is_not_released() {
|
||||
let (mut waiting, control) = pending();
|
||||
assert!(control.take());
|
||||
control.reply.send(json!({"ok": true, "result": 1})).unwrap_or_default();
|
||||
assert_eq!(waiting.poll(), Some(json!({"ok": true, "result": 1})));
|
||||
drop(waiting);
|
||||
assert_eq!(control.state.load(Ordering::SeqCst), TAKEN);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_request_the_app_never_runs_waits_until_it_is_dropped() {
|
||||
let (mut waiting, control) = pending();
|
||||
assert_eq!(waiting.poll(), None);
|
||||
drop(control);
|
||||
assert_eq!(waiting.poll().and_then(|r| r["ok"].as_bool()), Some(false), "the app went away");
|
||||
}
|
||||
}
|
||||
@@ -21,6 +21,8 @@
|
||||
#![deny(clippy::unwrap_used, clippy::expect_used, clippy::panic, clippy::unimplemented, clippy::todo, clippy::unreachable)]
|
||||
#![forbid(unsafe_code)]
|
||||
|
||||
#[cfg(any(target_arch = "wasm32", test))]
|
||||
mod api;
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
mod host;
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
|
||||
@@ -79,6 +79,7 @@ pub fn start() {
|
||||
{
|
||||
crate::host::Bridge::set_author(&mut app, &author);
|
||||
}
|
||||
crate::api::install(&mut app, cc.egui_ctx.clone());
|
||||
Ok(Box::new(WebShell { app, inbox, bridge: app_bridge }))
|
||||
}),
|
||||
)
|
||||
|
||||
@@ -148,7 +148,19 @@ pub fn rotate(dir: &Path) -> std::io::Result<(PathBuf, File)> {
|
||||
/// One formatted log line: `<timestamp> <LEVEL> [<thread>] <target>: <message>\n`.
|
||||
pub fn format_line(ts: &str, level: log::Level, thread: &str, target: &str, message: &str) -> String {
|
||||
let message = message.strip_suffix('\n').unwrap_or(message);
|
||||
format!("{ts} {level:<5} [{thread}] {target}: {message}\n")
|
||||
// Messages can embed document-controlled text (a part name, a relationship id): control
|
||||
// characters other than tab are escaped and each continuation line is marked, so such text
|
||||
// cannot start a line that looks like a record of its own.
|
||||
let mut text = String::with_capacity(message.len());
|
||||
for c in message.chars() {
|
||||
match c {
|
||||
'\n' => text.push_str("\n | "),
|
||||
'\t' => text.push(c),
|
||||
c if c.is_control() => text.extend(c.escape_default()),
|
||||
c => text.push(c),
|
||||
}
|
||||
}
|
||||
format!("{ts} {level:<5} [{thread}] {target}: {text}\n")
|
||||
}
|
||||
|
||||
/// Where formatted lines go besides standard error: kept in memory until a file is attached,
|
||||
@@ -446,8 +458,13 @@ mod tests {
|
||||
),
|
||||
"2026-10-08T07:59:17.728Z WARN [main] deckcraft_pptx::opc: pptx: skipping oversized part ppt/media/image1.png\n"
|
||||
);
|
||||
// A multi-line message keeps its lines; the record still ends in exactly one newline.
|
||||
assert!(format_line("t", log::Level::Error, "w", "x", "a\nb\n").ends_with("x: a\nb\n"));
|
||||
// A multi-line message keeps its lines, each continuation marked; the record still ends in
|
||||
// exactly one newline.
|
||||
assert!(format_line("t", log::Level::Error, "w", "x", "a\nb\n").ends_with("x: a\n | b\n"));
|
||||
// Injected line breaks and escape sequences cannot forge a record.
|
||||
let line = format_line("t", log::Level::Warn, "w", "x", "part a\r\n2026 ERROR [m] y: forged\x1b[2J");
|
||||
assert_eq!(line.matches('\n').count(), 2);
|
||||
assert!(!line.contains('\r') && !line.contains('\x1b'));
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
@@ -51,6 +51,9 @@ impl eframe::App for App {
|
||||
}
|
||||
fn on_exit(&mut self) {
|
||||
save_prefs(&self.0);
|
||||
// The window is closing for good: a ready update's helper starts now and installs it once
|
||||
// the process has exited.
|
||||
self.0.start_pending_update();
|
||||
// macOS (#279): end the process here, before eframe tears the window down. AccessKit's
|
||||
// macOS adapter subclasses the window's content view and restores its class on teardown;
|
||||
// AppKit's Touch Bar support observes that view (KVO), so in the next display cycle
|
||||
|
||||
@@ -118,6 +118,8 @@ enum Output {
|
||||
grain: FilmGrainParams,
|
||||
apply_film_grain: bool,
|
||||
pts: i64,
|
||||
/// What copying the picture out costs, in the reference's own format.
|
||||
bytes: u64,
|
||||
},
|
||||
}
|
||||
|
||||
@@ -132,7 +134,7 @@ impl Output {
|
||||
fn take(self) -> Result<Picture> {
|
||||
match self {
|
||||
Output::Decoded(s) => s.wait().map(|p| Arc::try_unwrap(p).unwrap_or_else(|p| (*p).clone())),
|
||||
Output::Existing { slot, seq, width, height, grain, apply_film_grain, pts } => {
|
||||
Output::Existing { slot, seq, width, height, grain, apply_film_grain, pts, .. } => {
|
||||
let rf = slot.wait()?;
|
||||
Ok(output_picture(&seq, &rf, width, height, &grain, apply_film_grain, pts))
|
||||
}
|
||||
@@ -379,8 +381,8 @@ impl Decoder {
|
||||
// Output policy (7.18.1 note): with scalability, show one frame per temporal unit, the
|
||||
// highest spatial layer present.
|
||||
let scalable = self.seq.as_ref().is_some_and(|s| s.op_idc != 0);
|
||||
if !scalable {
|
||||
self.out.extend(out.into_iter().map(|(o, _, _)| o));
|
||||
let kept: Vec<Output> = if !scalable {
|
||||
out.into_iter().map(|(o, _, _)| o).collect()
|
||||
} else {
|
||||
let mut last: Option<(usize, u32)> = None;
|
||||
let mut sel: Vec<Output> = Vec::new();
|
||||
@@ -400,8 +402,27 @@ impl Decoder {
|
||||
}
|
||||
}
|
||||
}
|
||||
self.out.extend(sel);
|
||||
sel
|
||||
};
|
||||
// Every repeat is copied out whole, so the repeats this call keeps (after the layer
|
||||
// selection) must not queue more than half the decoder budget; the first one over it and
|
||||
// everything after is dropped.
|
||||
let mut r = r;
|
||||
let mut repeat_bytes = 0u64;
|
||||
let mut ok = Vec::with_capacity(kept.len());
|
||||
for o in kept {
|
||||
if let Output::Existing { bytes, .. } = &o {
|
||||
repeat_bytes = repeat_bytes.saturating_add(*bytes);
|
||||
if repeat_bytes > self.memory_budget / 2 {
|
||||
if r.is_ok() {
|
||||
r = Err(Error::Invalid("repeated frame outputs exceed the decoder memory budget"));
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
ok.push(o);
|
||||
}
|
||||
self.out.extend(ok);
|
||||
r
|
||||
}
|
||||
|
||||
@@ -503,6 +524,15 @@ impl Decoder {
|
||||
let idx = fh.frame_to_show_map_idx;
|
||||
let slot = self.ref_frames[idx].clone().ok_or(Error::Invalid("show_existing_frame of an empty slot"))?;
|
||||
let info = self.refs[idx].clone();
|
||||
// Sized in the reference's own format: a new sequence header may differ from it.
|
||||
let shape = deckcraft_bitstream::budget::PictureShape {
|
||||
width: u64::from(info.upscaled_width),
|
||||
height: u64::from(info.frame_height),
|
||||
shift_x: u32::from(info.subsampling_x),
|
||||
shift_y: u32::from(info.subsampling_y),
|
||||
chroma_planes: if info.mono_chrome { 0 } else { 2 },
|
||||
bytes_per_sample: 2,
|
||||
};
|
||||
let o = Output::Existing {
|
||||
slot: slot.clone(),
|
||||
seq: seq.clone(),
|
||||
@@ -511,6 +541,7 @@ impl Decoder {
|
||||
grain: fh.film_grain.clone(),
|
||||
apply_film_grain: self.apply_film_grain,
|
||||
pts: self.pts,
|
||||
bytes: shape.bytes().unwrap_or(u64::MAX),
|
||||
};
|
||||
if fh.frame_type == KEY_FRAME as u8 {
|
||||
// reference frame loading process (7.21) then refresh every slot (7.20)
|
||||
@@ -629,6 +660,9 @@ impl Decoder {
|
||||
ri.lf_deltas = fh.lf.deltas;
|
||||
ri.seg_features = fh.seg.features;
|
||||
ri.grain = fh.film_grain.clone();
|
||||
ri.subsampling_x = job.seq.color.subsampling_x;
|
||||
ri.subsampling_y = job.seq.color.subsampling_y;
|
||||
ri.mono_chrome = job.seq.color.mono_chrome;
|
||||
self.ref_frames[i] = Some(job.slot.clone());
|
||||
}
|
||||
}
|
||||
@@ -777,6 +811,14 @@ fn decode_frame(job: &FrameJob, pool: &Pool, stats: &mut DecodeStats) -> Result<
|
||||
Ok((rf, pic))
|
||||
}
|
||||
|
||||
/// Bytes all parallel tile states may hold of whole-frame CDEF and restoration metadata; past it
|
||||
/// the tiles decode one after another on the frame's own metadata.
|
||||
const TILE_METADATA_BUDGET: usize = 256 << 20;
|
||||
|
||||
fn tile_metadata_fits(tiles: usize, per_tile: usize) -> bool {
|
||||
tiles.checked_mul(per_tile).is_some_and(|n| n <= TILE_METADATA_BUDGET)
|
||||
}
|
||||
|
||||
/// Decode the tiles of a frame (tile number, data), in parallel when several; returns the final
|
||||
/// CDFs of tile context_update_tile_id.
|
||||
fn decode_tiles(pool: &Pool, fs: &mut FrameState, cdfs: &Cdfs, tiles: &[(usize, Vec<u8>)]) -> Result<Option<Box<Cdfs>>> {
|
||||
@@ -790,7 +832,7 @@ fn decode_tiles(pool: &Pool, fs: &mut FrameState, cdfs: &Cdfs, tiles: &[(usize,
|
||||
};
|
||||
// Intra block copy reads the current frame through the tile's own buffer: keep whole-frame
|
||||
// buffers (tiles one after another) for it.
|
||||
if tiles.len() > 1 && pool.is_parallel() && !fs.fh.allow_intrabc {
|
||||
if tiles.len() > 1 && pool.is_parallel() && !fs.fh.allow_intrabc && tile_metadata_fits(tiles.len(), fs.tile_metadata_bytes()) {
|
||||
let mut states: Vec<TileState> = tiles
|
||||
.iter()
|
||||
.map(|(num, _)| {
|
||||
@@ -1075,6 +1117,14 @@ mod guard_tests {
|
||||
#[cfg(test)]
|
||||
mod budget_tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn tile_metadata_copies_are_budgeted() {
|
||||
assert!(tile_metadata_fits(64, 1 << 20));
|
||||
assert!(!tile_metadata_fits(4096, 1 << 20));
|
||||
assert!(!tile_metadata_fits(usize::MAX, 2));
|
||||
}
|
||||
|
||||
use deckcraft_bitstream::budget::DECODE_MEMORY_BUDGET;
|
||||
|
||||
fn color(mono: bool) -> crate::header::ColorConfig {
|
||||
|
||||
@@ -360,6 +360,10 @@ pub struct RefInfo {
|
||||
pub lf_deltas: LoopFilterDeltas,
|
||||
pub seg_features: SegmentationFeatures,
|
||||
pub grain: FilmGrainParams,
|
||||
/// The colour format the frame was decoded in (a later sequence header may change it).
|
||||
pub subsampling_x: u8,
|
||||
pub subsampling_y: u8,
|
||||
pub mono_chrome: bool,
|
||||
}
|
||||
|
||||
/// A parsed uncompressed frame header with its derived variables.
|
||||
|
||||
@@ -253,6 +253,13 @@ impl FrameState {
|
||||
self.lr = t.lr;
|
||||
}
|
||||
|
||||
/// Bytes of CDEF and loop-restoration metadata each parallel tile state copies whole.
|
||||
pub fn tile_metadata_bytes(&self) -> usize {
|
||||
let lr = &self.lr;
|
||||
// 1 (type) + 6 (Wiener) + 1 (SGR set) + 2 (SGR weights) bytes per unit.
|
||||
self.cdef.len() + lr.lr_type.iter().map(|v| v.len() * 10).sum::<usize>()
|
||||
}
|
||||
|
||||
/// Private buffers for the tile covering mode info rows [r0, r1) x columns [c0, c1); tiles
|
||||
/// on the right / bottom frame edge also cover the allocation margins.
|
||||
pub fn tile_state(&self, r0: usize, r1: usize, c0: usize, c1: usize) -> TileState {
|
||||
|
||||
@@ -466,22 +466,26 @@ fn ctx_xfrm(doc: &deckcraft_model::Presentation, sel: &crate::Selection, shape:
|
||||
|
||||
/// Run `f` on each target shape (copy-on-write), materialising inherited boxes first.
|
||||
pub(crate) fn edit_shapes(s: &mut Session, p: &Value, cmd: &str, f: impl Fn(&mut deckcraft_model::Shape) -> Result<()>) -> Result<Value> {
|
||||
let ids = targets(s, p)?;
|
||||
let mut ids = targets(s, p)?;
|
||||
if ids.is_empty() {
|
||||
return Err(bad(cmd, "no shapes selected"));
|
||||
}
|
||||
// A shape named twice is edited once.
|
||||
let mut seen = std::collections::HashSet::new();
|
||||
ids.retain(|id| seen.insert(*id));
|
||||
s.edit(|doc, sel| {
|
||||
let boxes: Vec<(ShapeId, Xfrm)> = {
|
||||
let st_shapes: Vec<deckcraft_model::Shape> =
|
||||
ids.iter().filter_map(|id| current_shapes(doc, sel).and_then(|v| deckcraft_model::find_shape(v, *id)).cloned()).collect();
|
||||
st_shapes.iter().map(|sh| (sh.id, xfrm_of(doc, sel, sh))).collect()
|
||||
};
|
||||
// The boxes are read from the shapes in place: no copy of a shape (and its text) per id.
|
||||
let boxes: std::collections::HashMap<ShapeId, Xfrm> = ids
|
||||
.iter()
|
||||
.filter_map(|id| current_shapes(doc, sel).and_then(|v| deckcraft_model::find_shape(v, *id)))
|
||||
.map(|sh| (sh.id, xfrm_of(doc, sel, sh)))
|
||||
.collect();
|
||||
let shapes = crate::shapes_mut(doc, sel).ok_or_else(|| bad(cmd, "no slide"))?;
|
||||
let mut n = 0;
|
||||
for id in &ids {
|
||||
if let Some(sh) = deckcraft_model::find_shape_mut(shapes, *id) {
|
||||
if sh.xfrm.is_none() {
|
||||
sh.xfrm = boxes.iter().find(|(i, _)| i == id).map(|(_, x)| *x);
|
||||
sh.xfrm = boxes.get(id).copied();
|
||||
}
|
||||
f(sh)?;
|
||||
n += 1;
|
||||
|
||||
@@ -120,13 +120,7 @@ pub(super) fn line_spacing(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
}
|
||||
|
||||
pub(super) fn paragraph(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
let a = str_param(p, "align").map(|a| match a {
|
||||
"center" => Align::Center,
|
||||
"right" => Align::Right,
|
||||
"justify" => Align::Justify,
|
||||
"distributed" => Align::Distributed,
|
||||
_ => Align::Left,
|
||||
});
|
||||
let a = str_param(p, "align").map(|a| parse_align("format.paragraph", a)).transpose()?;
|
||||
let ml = f64_param(p, "indentLeft");
|
||||
let ind = f64_param(p, "indentFirst");
|
||||
let sb = f64_param(p, "spaceBefore");
|
||||
@@ -182,10 +176,10 @@ pub(super) fn clamp_pt(v: f64) -> f64 {
|
||||
fn parse_align(cmd: &str, s: &str) -> Result<Align> {
|
||||
Ok(match s {
|
||||
"left" | "l" => Align::Left,
|
||||
"center" | "ctr" => Align::Center,
|
||||
"center" | "centre" | "ctr" => Align::Center,
|
||||
"right" | "r" => Align::Right,
|
||||
"justify" | "just" => Align::Justify,
|
||||
"justLow" => Align::JustLow,
|
||||
"justLow" | "justifyLow" => Align::JustLow,
|
||||
"distributed" | "dist" => Align::Distributed,
|
||||
"thaiDist" => Align::ThaiDistributed,
|
||||
_ => return Err(bad(cmd, "`align` must be left, center, right, justify, justLow, distributed or thaiDist")),
|
||||
|
||||
@@ -80,6 +80,12 @@ pub(super) fn photo_album_edit(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
let opts = album_opts(p)?;
|
||||
let items = album_items(p)?;
|
||||
let (id, ids) = album_target(s, p)?;
|
||||
// The editor is seeded within the album limits; replacing the album from a trimmed seed would
|
||||
// delete what it left out, so refuse rather than lose it.
|
||||
let st = s.doc()?;
|
||||
if items_from_slides(&st.doc, &album_slides(st, &ids)).1 {
|
||||
return Err(bad("insert.photoAlbumEdit", "this album holds more than the editor can carry; editing it would delete the rest"));
|
||||
}
|
||||
commit_album(s, items, opts, Some((id, ids)))
|
||||
}
|
||||
|
||||
@@ -545,6 +551,19 @@ fn album_target(s: &Session, p: &Value) -> Result<(u32, Vec<u64>)> {
|
||||
Err(bad("insert.photoAlbumEdit", "this slide is not part of a photo album"))
|
||||
}
|
||||
|
||||
/// Whether a slide holds anything besides album items: shapes, notes, comments, a background, a
|
||||
/// transition, animations, a colour map or tags.
|
||||
fn has_slide_content(s: &deckcraft_model::Slide) -> bool {
|
||||
!s.shapes.is_empty()
|
||||
|| !s.notes.text().trim().is_empty()
|
||||
|| !s.comments.is_empty()
|
||||
|| s.background.is_some()
|
||||
|| s.transition.is_some()
|
||||
|| !s.animations.is_empty()
|
||||
|| s.color_map.is_some()
|
||||
|| !s.tags.is_empty()
|
||||
}
|
||||
|
||||
fn pic_count(slide: &deckcraft_model::Slide) -> usize {
|
||||
slide.shapes.iter().filter(|sh| matches!(sh.kind, ShapeKind::Picture { .. })).count()
|
||||
}
|
||||
@@ -592,9 +611,16 @@ fn infer_album(slides: &[&deckcraft_model::Slide], theme_now: &str) -> (String,
|
||||
(layout, frame.into(), captions, any && gray, theme_now.to_string())
|
||||
}
|
||||
|
||||
fn items_from_slides(doc: &deckcraft_model::Presentation, slides: &[&deckcraft_model::Slide]) -> Vec<Value> {
|
||||
/// The album's items for the dialog, within the limits `insert.photoAlbum` itself takes (items,
|
||||
/// bytes per picture and in all), checked before a picture is encoded: an album slide set can
|
||||
/// name one large picture many times.
|
||||
///
|
||||
/// The flag is true when something was left out for a limit.
|
||||
fn items_from_slides(doc: &deckcraft_model::Presentation, slides: &[&deckcraft_model::Slide]) -> (Vec<Value>, bool) {
|
||||
let mut items = Vec::new();
|
||||
for slide in slides {
|
||||
let mut total = 0usize;
|
||||
let mut omitted = false;
|
||||
'slides: for slide in slides {
|
||||
let pics: Vec<&deckcraft_model::Shape> = slide.shapes.iter().filter(|sh| matches!(sh.kind, ShapeKind::Picture { .. })).collect();
|
||||
if pics.is_empty() {
|
||||
let text = slide
|
||||
@@ -605,13 +631,27 @@ fn items_from_slides(doc: &deckcraft_model::Presentation, slides: &[&deckcraft_m
|
||||
.find(|t| !t.trim().is_empty())
|
||||
.unwrap_or_default();
|
||||
if !text.trim().is_empty() {
|
||||
if items.len() >= ALBUM_MAX_ITEMS {
|
||||
omitted = true;
|
||||
break 'slides;
|
||||
}
|
||||
items.push(json!({"text": text.trim()}));
|
||||
} else if items.len() >= ALBUM_MAX_ITEMS && has_slide_content(slide) {
|
||||
// Past the item limit, a slide with other content would be deleted by a replace.
|
||||
omitted = true;
|
||||
break 'slides;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
for sh in pics {
|
||||
let ShapeKind::Picture { fill } = &sh.kind else { continue };
|
||||
let media = doc.media(fill.media);
|
||||
let size = media.map_or(0, |m| m.data.len());
|
||||
if items.len() >= ALBUM_MAX_ITEMS || size > ALBUM_MAX_BYTES || total.saturating_add(size) > ALBUM_MAX_TOTAL {
|
||||
omitted = true;
|
||||
break 'slides;
|
||||
}
|
||||
total += size;
|
||||
let name = if !sh.descr.is_empty() {
|
||||
sh.descr.clone()
|
||||
} else {
|
||||
@@ -626,14 +666,19 @@ fn items_from_slides(doc: &deckcraft_model::Presentation, slides: &[&deckcraft_m
|
||||
}));
|
||||
}
|
||||
}
|
||||
items
|
||||
(items, omitted)
|
||||
}
|
||||
|
||||
/// The slides of album `ids` as the editor would seed them.
|
||||
fn album_slides<'a>(st: &'a crate::DocState, ids: &[u64]) -> Vec<&'a deckcraft_model::Slide> {
|
||||
ids.iter().filter_map(|sid| u32::try_from(*sid).ok()).filter_map(|n| st.doc.slide(deckcraft_model::SlideId(n))).collect()
|
||||
}
|
||||
|
||||
fn album_seed(s: &Session) -> Result<Value> {
|
||||
let (id, ids) = album_target(s, &Value::Null)?;
|
||||
let st = s.doc()?;
|
||||
let slides: Vec<&deckcraft_model::Slide> =
|
||||
ids.iter().filter_map(|sid| u32::try_from(*sid).ok()).filter_map(|n| st.doc.slide(deckcraft_model::SlideId(n))).collect();
|
||||
let slides = album_slides(st, &ids);
|
||||
let (items, _) = items_from_slides(&st.doc, &slides);
|
||||
let theme_now = st.doc.masters.first().map(|m| m.theme.name.clone()).filter(|n| !n.is_empty()).unwrap_or_else(|| "DeckCraft".into());
|
||||
let inferred = infer_album(&slides, &theme_now);
|
||||
let mark = slides.first().and_then(|sl| sl.raw_ext.as_deref().and_then(parse_album_mark));
|
||||
@@ -655,7 +700,7 @@ fn album_seed(s: &Session) -> Result<Value> {
|
||||
"captions": captions,
|
||||
"grayscale": grayscale,
|
||||
"theme": theme,
|
||||
"items": items_from_slides(&st.doc, &slides),
|
||||
"items": items,
|
||||
}))
|
||||
}
|
||||
|
||||
@@ -722,3 +767,96 @@ fn insert_album(s: &mut Session, items: Vec<AlbumItem>, opts: &AlbumOpts, at: Op
|
||||
s.execute("slide.go", &json!({"index": first}))?;
|
||||
Ok(json!({"slides": slides, "first": first, "pictures": pictures, "album": album_id}))
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod seed_tests {
|
||||
use std::sync::Arc;
|
||||
|
||||
use deckcraft_model::style::PictureFill;
|
||||
use deckcraft_model::{Shape, ShapeId, defaults};
|
||||
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn reopening_an_album_seeds_at_most_the_items_an_album_takes() {
|
||||
// One picture named by 500 shapes: the dialog gets 48 items, not 500 encoded copies.
|
||||
let mut p = defaults::new_presentation(None);
|
||||
let img = p.add_media("a.png", "image/png", vec![1; 64 * 1024]).unwrap();
|
||||
let pic = |k: u32| Shape {
|
||||
id: ShapeId(10_000 + k),
|
||||
kind: ShapeKind::Picture { fill: PictureFill { media: img, ..Default::default() } },
|
||||
..Default::default()
|
||||
};
|
||||
Arc::make_mut(&mut p.slides[0]).shapes = (0..500).map(pic).collect();
|
||||
let slides: Vec<&deckcraft_model::Slide> = p.slides.iter().map(|s| s.as_ref()).collect();
|
||||
let (items, omitted) = items_from_slides(&p, &slides);
|
||||
assert_eq!(items.len(), ALBUM_MAX_ITEMS);
|
||||
assert!(omitted);
|
||||
let few: Vec<&deckcraft_model::Slide> = Vec::new();
|
||||
assert!(!items_from_slides(&p, &few).1);
|
||||
}
|
||||
|
||||
/// An album too big for the editor's seed cannot be replaced from that seed: the replace would
|
||||
/// delete what the seed left out.
|
||||
#[test]
|
||||
fn editing_an_album_the_editor_cannot_hold_is_refused() {
|
||||
let mut s = crate::Session::new();
|
||||
s.execute("file.new", &json!({})).unwrap();
|
||||
let texts: Vec<Value> = (0..ALBUM_MAX_ITEMS).map(|i| json!({"text": format!("t{i}")})).collect();
|
||||
s.execute("insert.photoAlbum", &json!({"pictures": texts, "layout": "1"})).unwrap();
|
||||
let st = s.doc_mut().unwrap();
|
||||
let doc = Arc::make_mut(&mut st.doc);
|
||||
let mut extra = doc.slides.last().unwrap().as_ref().clone();
|
||||
extra.id = deckcraft_model::SlideId(doc.alloc_id().unwrap());
|
||||
doc.slides.push(Arc::new(extra));
|
||||
let n = doc.slides.len();
|
||||
let e = s.execute("insert.photoAlbumEdit", &json!({"pictures": [{"text": "x"}], "layout": "1"})).unwrap_err().to_string();
|
||||
assert!(e.contains("delete the rest"), "{e}");
|
||||
assert_eq!(s.doc().unwrap().doc.slides.len(), n);
|
||||
}
|
||||
|
||||
/// Past the item limit, a slide holding only a drawing counts as content the replace would delete.
|
||||
#[test]
|
||||
fn editing_an_album_with_a_drawing_slide_past_the_limit_is_refused() {
|
||||
let mut s = crate::Session::new();
|
||||
s.execute("file.new", &json!({})).unwrap();
|
||||
let texts: Vec<Value> = (0..ALBUM_MAX_ITEMS).map(|i| json!({"text": format!("t{i}")})).collect();
|
||||
s.execute("insert.photoAlbum", &json!({"pictures": texts, "layout": "1"})).unwrap();
|
||||
let st = s.doc_mut().unwrap();
|
||||
let doc = Arc::make_mut(&mut st.doc);
|
||||
let mut extra = doc.slides.last().unwrap().as_ref().clone();
|
||||
extra.id = deckcraft_model::SlideId(doc.alloc_id().unwrap());
|
||||
extra.shapes = vec![Shape { id: ShapeId(90_000), ..Default::default() }];
|
||||
doc.slides.push(Arc::new(extra));
|
||||
let e = s.execute("insert.photoAlbumEdit", &json!({"pictures": [{"text": "x"}], "layout": "1"})).unwrap_err().to_string();
|
||||
assert!(e.contains("delete the rest"), "{e}");
|
||||
}
|
||||
|
||||
/// Notes or comments on a shapeless slide past the limit are content a replace would delete.
|
||||
#[test]
|
||||
fn editing_an_album_with_a_notes_only_slide_past_the_limit_is_refused() {
|
||||
let mut s = crate::Session::new();
|
||||
s.execute("file.new", &json!({})).unwrap();
|
||||
let texts: Vec<Value> = (0..ALBUM_MAX_ITEMS).map(|i| json!({"text": format!("t{i}")})).collect();
|
||||
s.execute("insert.photoAlbum", &json!({"pictures": texts, "layout": "1"})).unwrap();
|
||||
let st = s.doc_mut().unwrap();
|
||||
let doc = Arc::make_mut(&mut st.doc);
|
||||
let mut extra = doc.slides.last().unwrap().as_ref().clone();
|
||||
extra.id = deckcraft_model::SlideId(doc.alloc_id().unwrap());
|
||||
extra.shapes.clear();
|
||||
extra.notes = deckcraft_model::text::TextBody::from_text("speaker notes");
|
||||
doc.slides.push(Arc::new(extra));
|
||||
let e = s.execute("insert.photoAlbumEdit", &json!({"pictures": [{"text": "x"}], "layout": "1"})).unwrap_err().to_string();
|
||||
assert!(e.contains("delete the rest"), "{e}");
|
||||
}
|
||||
|
||||
/// A shapeless slide with a background, transition, colour map or tags counts as content; a plain empty one does not.
|
||||
#[test]
|
||||
fn slide_level_settings_count_as_slide_content() {
|
||||
use deckcraft_model::Slide;
|
||||
assert!(!has_slide_content(&Slide::default()));
|
||||
assert!(has_slide_content(&Slide { tags: vec![("k".into(), "v".into())], ..Default::default() }));
|
||||
assert!(has_slide_content(&Slide { color_map: Some(vec![("bg1".into(), "lt1".into())]), ..Default::default() }));
|
||||
assert!(has_slide_content(&Slide { transition: Some(Default::default()), ..Default::default() }));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -135,6 +135,14 @@ fn eye_show_all_and_hide_all() {
|
||||
assert!(!flags(&s, a).0 && flags(&s, g).0 && flags(&s, e).0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn lock_group_with_its_child_targeted_leaves_child_locked() {
|
||||
let (mut s, ids) = deck();
|
||||
let (d, g) = (ids[3], ids[7]);
|
||||
s.execute("shape.lock", &json!({"ids": [g, d, d]})).unwrap();
|
||||
assert!(flags(&s, g).1 && flags(&s, d).1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn lock_cascades_through_groups_and_undoes() {
|
||||
let (mut s, ids) = deck();
|
||||
|
||||
@@ -10,7 +10,7 @@ use serde_json::{Value, json};
|
||||
|
||||
use super::{
|
||||
CommandSpec, NO_ID_SPACE, bad, bool_or, bool_param, cmd, color_param, color_value, edit_geometry, edit_shapes, f64_param, has_selection,
|
||||
has_slide, id_param, ok, str_param, targets, usize_param, with_param, xfrm_of,
|
||||
has_slide, id_param, ok, opt_usize_param, str_param, targets, usize_param, with_param, xfrm_of,
|
||||
};
|
||||
use crate::{Result, Session};
|
||||
|
||||
@@ -374,6 +374,25 @@ fn visible(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
/// (a child can still be unlocked on its own afterwards), as PowerPoint does.
|
||||
fn lock(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
let v = bool_param(p, "locked");
|
||||
// A targeted shape inside another targeted group is reached through the group: editing it again
|
||||
// would toggle it back.
|
||||
let mut ids = super::targets(s, p)?;
|
||||
if ids.len() > 1 {
|
||||
let doc = s.doc()?;
|
||||
// Walk each distinct group once, however often `ids` repeats it.
|
||||
let mut once = std::collections::HashSet::new();
|
||||
ids.retain(|id| once.insert(*id));
|
||||
let mut inside = std::collections::HashSet::new();
|
||||
for id in &ids {
|
||||
if let Some(g) = doc.shape(*id) {
|
||||
deckcraft_model::walk(g.children(), &mut |c, _| {
|
||||
inside.insert(c.id);
|
||||
});
|
||||
}
|
||||
}
|
||||
ids.retain(|id| !inside.contains(id));
|
||||
}
|
||||
let p = &json!({"ids": ids, "locked": p.get("locked")});
|
||||
edit_shapes(s, p, "shape.lock", |sh| {
|
||||
let to = v.unwrap_or(!sh.locked);
|
||||
set_locked(sh, to, 0);
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
use super::{
|
||||
ColorRef, ColorTransform, Dash, Effects, Fill, Geom, Glow, Gradient, GradientShape, GradientStop, Line, LineEnd, PictureFill, PictureMode,
|
||||
Reflection, Result, Rgba, SchemeSlot, Session, Shadow, ShapeStyle, Value, apply_fill, bad, bool_or, color_param, color_value, edit_geometry,
|
||||
edit_shapes, f64_param, inherited_fills, json, requested_fill, str_param, targets, usize_param,
|
||||
edit_shapes, f64_param, inherited_fills, json, opt_usize_param, requested_fill, str_param, targets,
|
||||
};
|
||||
|
||||
pub(super) fn adjust(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
let i = usize_param(p, "index").unwrap_or(0).min(16);
|
||||
let i = opt_usize_param(p, "index", "shape.adjust")?.unwrap_or(0).min(16);
|
||||
let v = f64_param(p, "value").ok_or_else(|| bad("shape.adjust", "missing `value`"))?;
|
||||
edit_geometry(s, p, "shape.adjust", |sh| {
|
||||
if let Geom::Preset { name, adj } = &mut sh.geom {
|
||||
@@ -556,7 +556,7 @@ pub fn quick_style_for(index: usize) -> (ShapeStyle, Option<Fill>, Option<Line>)
|
||||
}
|
||||
|
||||
pub(super) fn quick_style(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
let i = usize_param(p, "index").unwrap_or(1).min(41);
|
||||
let i = opt_usize_param(p, "index", "shape.quickStyle")?.unwrap_or(1).min(41);
|
||||
let (st, f, l) = quick_style_for(i);
|
||||
edit_shapes(s, p, "shape.quickStyle", |sh| {
|
||||
sh.style = Some(st.clone());
|
||||
|
||||
@@ -104,7 +104,7 @@ pub(super) fn freeform(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
a.iter()
|
||||
.filter_map(|v| Some(deckcraft_geom::Point::new(v.get(0)?.as_f64()?, v.get(1)?.as_f64()?)))
|
||||
.filter(|q| q.x.is_finite() && q.y.is_finite())
|
||||
.take(20_000)
|
||||
.take(crate::tools::MAX_GESTURE_POINTS)
|
||||
.collect()
|
||||
})
|
||||
.unwrap_or_default();
|
||||
|
||||
@@ -85,14 +85,20 @@ impl AutomationRoots {
|
||||
|
||||
/// May the file or folder at `path` be read (or written)? `Err` says why not.
|
||||
pub fn check(&self, path: &str, access: Access) -> Result<(), String> {
|
||||
self.check_path(Path::new(path), access)
|
||||
}
|
||||
|
||||
/// [`Self::check`] for a path that may not be UTF-8: the checked path is the one opened.
|
||||
pub fn check_path(&self, path: &Path, access: Access) -> Result<(), String> {
|
||||
let path_text = path.display();
|
||||
let roots = match access {
|
||||
Access::Read => &self.read,
|
||||
Access::Write => &self.write,
|
||||
};
|
||||
if roots.is_empty() {
|
||||
return match access {
|
||||
Access::Read => Err(format!("automation filesystem access is not granted: read authority is absent: {path}")),
|
||||
Access::Write => Err(format!("automation filesystem access is not granted: write authority is absent: {path}")),
|
||||
Access::Read => Err(format!("automation filesystem access is not granted: read authority is absent: {path_text}")),
|
||||
Access::Write => Err(format!("automation filesystem access is not granted: write authority is absent: {path_text}")),
|
||||
};
|
||||
}
|
||||
let resolved = resolve(path)?;
|
||||
@@ -101,8 +107,8 @@ impl AutomationRoots {
|
||||
}
|
||||
let roots = roots.iter().map(|root| root.shown.display().to_string()).collect::<Vec<_>>().join(", ");
|
||||
match access {
|
||||
Access::Read => Err(format!("automation path rejected: outside the read roots {roots}: {path}")),
|
||||
Access::Write => Err(format!("automation path rejected: outside the write roots {roots}: {path}")),
|
||||
Access::Read => Err(format!("automation path rejected: outside the read roots {roots}: {path_text}")),
|
||||
Access::Write => Err(format!("automation path rejected: outside the write roots {roots}: {path_text}")),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -191,17 +197,23 @@ pub fn check_write(path: &str) -> Result<(), String> {
|
||||
current().map_or(Ok(()), |r| r.check(path, Access::Write))
|
||||
}
|
||||
|
||||
/// May `path` be written here? Always, without roots in force. For a path that may not be UTF-8.
|
||||
pub fn check_write_path(path: &Path) -> Result<(), String> {
|
||||
current().map_or(Ok(()), |r| r.check_path(path, Access::Write))
|
||||
}
|
||||
|
||||
/// `path` made absolute with its links followed; for a path that doesn't exist (yet), its nearest
|
||||
/// existing folder's links followed and the rest appended.
|
||||
fn resolve(path: &str) -> Result<PathBuf, String> {
|
||||
if path.is_empty() {
|
||||
fn resolve(path: &Path) -> Result<PathBuf, String> {
|
||||
if path.as_os_str().is_empty() {
|
||||
return Err("automation path rejected: path is empty".into());
|
||||
}
|
||||
let shown = path.display();
|
||||
// Checked as given: making `…\NUL` absolute gives the device path `\\.\NUL` on Windows.
|
||||
if cfg!(windows) && Path::new(path).components().any(|c| matches!(c, Component::Normal(n) if is_device_name(&n.to_string_lossy()))) {
|
||||
return Err(format!("automation path rejected: reserved device names are not allowed: {path}"));
|
||||
if cfg!(windows) && path.components().any(|c| matches!(c, Component::Normal(n) if is_device_name(&n.to_string_lossy()))) {
|
||||
return Err(format!("automation path rejected: reserved device names are not allowed: {shown}"));
|
||||
}
|
||||
let absolute = std::path::absolute(path).map_err(|e| format!("automation path rejected: cannot resolve {path}: {e}"))?;
|
||||
let absolute = std::path::absolute(path).map_err(|e| format!("automation path rejected: cannot resolve {shown}: {e}"))?;
|
||||
// The names below the nearest existing folder, last first.
|
||||
let mut missing: Vec<OsString> = vec![];
|
||||
let mut at = absolute.as_path();
|
||||
@@ -212,11 +224,11 @@ fn resolve(path: &str) -> Result<PathBuf, String> {
|
||||
return Ok(found);
|
||||
}
|
||||
Err(e) => match std::fs::symlink_metadata(at) {
|
||||
Ok(m) if m.file_type().is_symlink() => return Err(format!("automation path rejected: a link that leads nowhere: {path}")),
|
||||
Ok(m) if m.file_type().is_symlink() => return Err(format!("automation path rejected: a link that leads nowhere: {shown}")),
|
||||
// There, yet its real path can't be had (no permission): nothing to compare.
|
||||
Ok(_) => return Err(format!("automation path rejected: cannot resolve {path}: {e}")),
|
||||
Ok(_) => return Err(format!("automation path rejected: cannot resolve {shown}: {e}")),
|
||||
Err(e) if e.kind() == std::io::ErrorKind::NotFound => {}
|
||||
Err(e) => return Err(format!("automation path rejected: cannot resolve {path}: {e}")),
|
||||
Err(e) => return Err(format!("automation path rejected: cannot resolve {shown}: {e}")),
|
||||
},
|
||||
}
|
||||
let parent = at.parent();
|
||||
@@ -226,12 +238,12 @@ fn resolve(path: &str) -> Result<PathBuf, String> {
|
||||
at = parent;
|
||||
}
|
||||
(Some(Component::ParentDir), _) => {
|
||||
return Err(format!("automation path rejected: parent traversal below a folder that doesn't exist: {path}"));
|
||||
return Err(format!("automation path rejected: parent traversal below a folder that doesn't exist: {shown}"));
|
||||
}
|
||||
// Down to the root (or drive) and it doesn't exist either.
|
||||
_ => {
|
||||
let e = std::io::Error::from(std::io::ErrorKind::NotFound);
|
||||
return Err(format!("automation path rejected: cannot resolve {path}: {e}"));
|
||||
return Err(format!("automation path rejected: cannot resolve {shown}: {e}"));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
//! Fonts a presentation uses that this machine doesn't have, so opening it can say so (its text
|
||||
//! draws in a stand-in; see `FontDb::document_face`).
|
||||
|
||||
use std::collections::HashSet;
|
||||
|
||||
use deckcraft_fonts::FontDb;
|
||||
use deckcraft_model::resolve::{self, Ctx};
|
||||
use deckcraft_model::text::{Bullet, TextBody};
|
||||
@@ -12,8 +14,13 @@ use deckcraft_model::{Presentation, Shape, ShapeKind};
|
||||
/// font to load and are left for the round trip.
|
||||
pub fn install_embedded_fonts(p: &Presentation) {
|
||||
let db = FontDb::global();
|
||||
// Faces can name one media item many times; each is copied and enumerated once.
|
||||
let mut seen = HashSet::new();
|
||||
for font in &p.embedded_fonts {
|
||||
for id in font.faces().into_iter().flatten() {
|
||||
if !seen.insert(id) {
|
||||
continue;
|
||||
}
|
||||
let Some(item) = p.media(id) else { continue };
|
||||
let Some(sfnt) = deckcraft_fonts::embedded_sfnt(&item.data) else { continue };
|
||||
db.add_font(sfnt.to_vec());
|
||||
@@ -27,12 +34,8 @@ pub fn install_embedded_fonts(p: &Presentation) {
|
||||
/// and can't be installed, so they aren't reported.
|
||||
pub fn missing_fonts(p: &Presentation) -> Vec<String> {
|
||||
let db = FontDb::global();
|
||||
let mut out: Vec<String> = Vec::new();
|
||||
for family in used_fonts(p) {
|
||||
if !family.starts_with('.') && !db.has_family(&family) && !out.iter().any(|f| f.eq_ignore_ascii_case(&family)) {
|
||||
out.push(family);
|
||||
}
|
||||
}
|
||||
// used_fonts is already free of case-insensitive duplicates.
|
||||
let mut out: Vec<String> = used_fonts(p).into_iter().filter(|f| !f.starts_with('.') && !db.has_family(f)).collect();
|
||||
out.sort_by_key(|f| f.to_lowercase());
|
||||
out
|
||||
}
|
||||
@@ -40,9 +43,10 @@ pub fn missing_fonts(p: &Presentation) -> Vec<String> {
|
||||
/// The families the slides' text runs and bullets are drawn in.
|
||||
pub fn used_fonts(p: &Presentation) -> Vec<String> {
|
||||
let mut out: Vec<String> = Vec::new();
|
||||
let mut seen: HashSet<String> = HashSet::new();
|
||||
let mut add = |f: String| {
|
||||
let f = f.trim().to_string();
|
||||
if !f.is_empty() && !out.iter().any(|o| o.eq_ignore_ascii_case(&f)) {
|
||||
if !f.is_empty() && seen.insert(f.to_ascii_lowercase()) {
|
||||
out.push(f);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -824,10 +824,32 @@ fn clamp_selection(st: &mut DocState) {
|
||||
/// Default for [`History::byte_limit`].
|
||||
pub const UNDO_BYTE_BUDGET: usize = 256 << 20;
|
||||
|
||||
/// A rough size of a text body in memory: its paragraphs and runs, at their inline struct sizes.
|
||||
fn text_cost(t: &deckcraft_model::text::TextBody) -> usize {
|
||||
use deckcraft_model::text::{Paragraph, Run};
|
||||
let (para, run) = (std::mem::size_of::<Paragraph>(), std::mem::size_of::<Run>());
|
||||
t.paragraphs.iter().map(|p| para + p.runs.iter().map(|r| run + r.text.len()).sum::<usize>()).sum()
|
||||
}
|
||||
|
||||
/// A rough size of comments in memory, replies included.
|
||||
fn comments_cost(cs: &[deckcraft_model::Comment], depth: usize) -> usize {
|
||||
let kids = |c: &deckcraft_model::Comment| if depth < 64 { comments_cost(&c.replies, depth + 1) } else { 0 };
|
||||
cs.iter().map(|c| 256 + c.author.len() + c.initials.len() + c.text.len() + c.date.len() + kids(c)).sum()
|
||||
}
|
||||
|
||||
/// A rough size of a slide's own data besides its shapes: notes, comments, tags and kept XML,
|
||||
/// all copied with the slide when an edit changes it.
|
||||
fn slide_extra_cost(s: &deckcraft_model::Slide) -> usize {
|
||||
text_cost(&s.notes)
|
||||
+ comments_cost(&s.comments, 0)
|
||||
+ s.name.len()
|
||||
+ s.tags.iter().map(|(k, v)| 48 + k.len() + v.len()).sum::<usize>()
|
||||
+ s.raw_ext.as_ref().map_or(0, String::len)
|
||||
}
|
||||
|
||||
/// A rough size of a shape in memory: the struct, its text and kept XML, its children.
|
||||
fn shape_cost(sh: &deckcraft_model::Shape, depth: usize) -> usize {
|
||||
let text: usize =
|
||||
sh.text.as_ref().map_or(0, |t| t.paragraphs.iter().map(|p| 96 + p.runs.iter().map(|r| 128 + r.text.len()).sum::<usize>()).sum());
|
||||
let text: usize = sh.text.as_ref().map_or(0, text_cost);
|
||||
let kids: usize = if depth < 64 { sh.children().iter().map(|c| shape_cost(c, depth + 1)).sum() } else { 0 };
|
||||
768 + text + sh.raw_ext.as_ref().map_or(0, String::len) + kids
|
||||
}
|
||||
@@ -845,7 +867,7 @@ pub fn undo_cost(doc: &Presentation, newer: &Presentation) -> usize {
|
||||
let media: HashSet<*const Vec<u8>> = newer.media.iter().map(|m| Arc::as_ptr(&m.data)).collect();
|
||||
let mut n = 64 * doc.slides.len();
|
||||
for s in doc.slides.iter().filter(|s| !slides.contains(&Arc::as_ptr(s))) {
|
||||
n += 512 + shapes_cost(&s.shapes);
|
||||
n += 512 + shapes_cost(&s.shapes) + slide_extra_cost(s);
|
||||
}
|
||||
for m in doc.masters.iter().filter(|m| !masters.contains(&Arc::as_ptr(m))) {
|
||||
n += 4096 + shapes_cost(&m.shapes) + m.layouts.iter().map(|l| 512 + shapes_cost(&l.shapes)).sum::<usize>();
|
||||
|
||||
@@ -171,7 +171,9 @@ impl Dictionary {
|
||||
["COMPOUNDWORDMAX", n, ..] => d.compound_max = n.parse().unwrap_or(0),
|
||||
["REP", n] => {
|
||||
for _ in 0..n.parse::<usize>().unwrap_or(0) {
|
||||
let r: Vec<&str> = lines.next().unwrap_or("").split_whitespace().collect();
|
||||
// The count is the file's claim: stop when the records run out.
|
||||
let Some(line) = lines.next() else { break };
|
||||
let r: Vec<&str> = line.split_whitespace().collect();
|
||||
if let [_, a, b, ..] = r.as_slice() {
|
||||
d.rep.push((a.replace('_', " "), b.replace('_', " ")));
|
||||
}
|
||||
@@ -182,7 +184,8 @@ impl Dictionary {
|
||||
let Some(flag) = one_flag(Some(flag), mode) else { continue };
|
||||
let cross = cross.eq_ignore_ascii_case("y");
|
||||
for _ in 0..n.parse::<usize>().unwrap_or(0) {
|
||||
let r: Vec<&str> = lines.next().unwrap_or("").split_whitespace().collect();
|
||||
let Some(line) = lines.next() else { break };
|
||||
let r: Vec<&str> = line.split_whitespace().collect();
|
||||
let [_, _, strip, add, rest @ ..] = r.as_slice() else { continue };
|
||||
let (add, cont) = match add.split_once('/') {
|
||||
Some((a, c)) => (a, parse_flags(c, mode)),
|
||||
@@ -304,7 +307,12 @@ impl Dictionary {
|
||||
|
||||
/// `w` made of dictionary parts allowed in compounds (`COMPOUNDFLAG`, or the begin, middle and
|
||||
/// end flags), each at least `COMPOUNDMIN` characters long.
|
||||
fn compounded(&self, w: &[char], index: usize) -> bool {
|
||||
fn compounded(&self, w: &[char], index: usize, failed: &mut HashSet<(usize, usize)>) -> bool {
|
||||
// `w` is always a tail of the checked word, so its length names it: a (tail, part index)
|
||||
// that failed once fails again however the earlier parts were split.
|
||||
if failed.contains(&(w.len(), index)) {
|
||||
return false;
|
||||
}
|
||||
if self.compound.is_none() && self.compound_end.is_none() {
|
||||
return false;
|
||||
}
|
||||
@@ -322,10 +330,11 @@ impl Dictionary {
|
||||
if [self.compound, self.compound_end].into_iter().flatten().any(|f| self.compound_part(rest, f, true)) {
|
||||
return true;
|
||||
}
|
||||
if self.compounded(rest, index + 1) {
|
||||
if self.compounded(rest, index + 1, failed) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
failed.insert((w.len(), index));
|
||||
false
|
||||
}
|
||||
|
||||
@@ -352,7 +361,7 @@ impl Dictionary {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
self.compounded(&w, 0)
|
||||
self.compounded(&w, 0, &mut HashSet::new())
|
||||
}
|
||||
|
||||
/// Is `word` spelt correctly? Capitalised and upper-case forms of a lower-case word pass.
|
||||
@@ -576,6 +585,15 @@ fn cache() -> &'static Cache {
|
||||
CACHE.get_or_init(Default::default)
|
||||
}
|
||||
|
||||
/// Language strings remembered by [`dictionary`]; a string past it is resolved again on each call.
|
||||
const MAX_CACHED_LANGS: usize = 256;
|
||||
|
||||
/// Parsed dictionaries by their affix file, so aliases of one language share it.
|
||||
fn by_files() -> &'static Mutex<HashMap<PathBuf, Arc<Dictionary>>> {
|
||||
static M: OnceLock<Mutex<HashMap<PathBuf, Arc<Dictionary>>>> = OnceLock::new();
|
||||
M.get_or_init(Default::default)
|
||||
}
|
||||
|
||||
/// The dictionary for `lang` (a BCP-47 tag such as `en-US`), loaded once per process.
|
||||
pub fn dictionary(lang: &str) -> Option<Arc<Dictionary>> {
|
||||
let cache = cache();
|
||||
@@ -586,10 +604,23 @@ pub fn dictionary(lang: &str) -> Option<Arc<Dictionary>> {
|
||||
return d.clone();
|
||||
}
|
||||
let loaded = find_in(&search_dirs(), lang).and_then(|(a, d)| {
|
||||
let (a, d) = (std::fs::read(a).ok()?, std::fs::read(d).ok()?);
|
||||
Some(Arc::new(Dictionary::parse(&a, &d)))
|
||||
// Tags that resolve to the same files share one parsed dictionary.
|
||||
if let Ok(m) = by_files().lock()
|
||||
&& let Some(dict) = m.get(&a)
|
||||
{
|
||||
return Some(dict.clone());
|
||||
}
|
||||
let (bytes_a, bytes_d) = (std::fs::read(&a).ok()?, std::fs::read(d).ok()?);
|
||||
let dict = Arc::new(Dictionary::parse(&bytes_a, &bytes_d));
|
||||
// A concurrent loader of the same files may have inserted first: share its copy.
|
||||
if let Ok(mut m) = by_files().lock() {
|
||||
return Some(m.entry(a).or_insert(dict).clone());
|
||||
}
|
||||
Some(dict)
|
||||
});
|
||||
if let Ok(mut c) = cache.lock() {
|
||||
if let Ok(mut c) = cache.lock()
|
||||
&& c.len() < MAX_CACHED_LANGS
|
||||
{
|
||||
c.insert(key, loaded.clone());
|
||||
}
|
||||
loaded
|
||||
@@ -668,6 +699,8 @@ pub fn add_user_word(word: &str) -> std::io::Result<()> {
|
||||
return Ok(());
|
||||
}
|
||||
let path = user_dictionary_path().ok_or_else(|| std::io::Error::new(std::io::ErrorKind::NotFound, "no folder for the user dictionary"))?;
|
||||
// A confined automation client may write only inside its write roots, this file included.
|
||||
crate::file_access::check_write_path(&path).map_err(|e| std::io::Error::new(std::io::ErrorKind::PermissionDenied, e))?;
|
||||
if let Some(d) = path.parent() {
|
||||
std::fs::create_dir_all(d)?;
|
||||
}
|
||||
@@ -897,6 +930,14 @@ SFX D Y 1\nSFX D 0 ed/S .\n";
|
||||
assert!(d.suggest("workingslides", 5).contains(&"working slides".to_string()));
|
||||
}
|
||||
|
||||
/// An affix header can claim billions of records; parsing stops where the file does.
|
||||
#[test]
|
||||
fn a_header_count_past_the_records_ends_with_them() {
|
||||
let aff = b"SFX S Y 4000000000\nSFX S 0 s .\nREP 4000000000\nREP a b\n";
|
||||
let d = Dictionary::parse(aff, b"1\ncat/S\n".as_slice());
|
||||
assert!(d.check("cats"));
|
||||
}
|
||||
|
||||
/// #1175: compound splitting is quadratic in the word's length, so a word longer than
|
||||
/// `MAX_CHECK_CHARS` is not checked; shorter ones still are.
|
||||
#[test]
|
||||
@@ -931,6 +972,17 @@ SFX D Y 1\nSFX D 0 ed/S .\n";
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn overlapping_compound_roots_with_a_failing_ending_stay_cheap() {
|
||||
// Roots a..aaaaaaaa overlap, so many splits of "aaa…ab" reach the same (tail, part) state; unmemoized that is millions of searches.
|
||||
let d = Dictionary::parse(
|
||||
b"COMPOUNDFLAG X\nCOMPOUNDMIN 1\n".as_slice(),
|
||||
b"8\na/X\naa/X\naaa/X\naaaa/X\naaaaa/X\naaaaaa/X\naaaaaaa/X\naaaaaaaa/X\n".as_slice(),
|
||||
);
|
||||
assert!(!d.check(&format!("{}b", "a".repeat(99))));
|
||||
assert!(d.check(&"a".repeat(20)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn runs_give_words_their_language() {
|
||||
use deckcraft_model::{Paragraph, Run, RunProps};
|
||||
@@ -970,6 +1022,20 @@ SFX D Y 1\nSFX D 0 ed/S .\n";
|
||||
assert_eq!(w, vec!["check", "done"]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_confined_client_cannot_write_the_user_dictionary_outside_its_roots() {
|
||||
let root = std::env::temp_dir().join(format!("deckcraft-dict-root-{}", std::process::id()));
|
||||
std::fs::create_dir_all(&root).unwrap();
|
||||
let roots = crate::file_access::AutomationRoots::new(std::slice::from_ref(&root), std::slice::from_ref(&root)).unwrap();
|
||||
// The user dictionary is a file of its own outside `root`, not whatever HOME points at.
|
||||
let outside = std::env::temp_dir().join(format!("deckcraft-dict-outside-{}", std::process::id()));
|
||||
set_user_dictionary_path(outside.join("user.dic"));
|
||||
let r = crate::file_access::confine(roots.as_ref(), || add_user_word("zqxconfinedword"));
|
||||
assert_eq!(r.map_err(|e| e.kind()), Err(std::io::ErrorKind::PermissionDenied));
|
||||
assert!(!outside.exists(), "the refused write created nothing");
|
||||
let _ = std::fs::remove_dir_all(&root);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_long_paragraph_with_a_huge_language_is_checked_in_linear_work() {
|
||||
use deckcraft_model::{Paragraph, Run, RunProps};
|
||||
|
||||
@@ -1713,6 +1713,31 @@ fn tab_moves_between_cells_skipping_merged_ones() {
|
||||
assert_eq!((cell(&s), table_of(&s, id).n_rows()), (Some((2, 0)), 3), "Tab in the last cell adds a row");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn format_paragraph_align_names_match_format_align() {
|
||||
use deckcraft_model::text::Align;
|
||||
let mut s = session();
|
||||
let title = s.doc().unwrap().current_slide().unwrap().shapes[0].id.0;
|
||||
s.execute("text.edit", &json!({"id": title})).unwrap();
|
||||
let para = |s: &Session| s.doc().unwrap().current_slide().unwrap().shapes[0].text.as_ref().unwrap().paragraphs[0].props.align;
|
||||
for (name, want) in [
|
||||
("justLow", Align::JustLow),
|
||||
("justifyLow", Align::JustLow),
|
||||
("thaiDist", Align::ThaiDistributed),
|
||||
("ctr", Align::Center),
|
||||
("centre", Align::Center),
|
||||
("r", Align::Right),
|
||||
("just", Align::Justify),
|
||||
("dist", Align::Distributed),
|
||||
("distributed", Align::Distributed),
|
||||
("left", Align::Left),
|
||||
] {
|
||||
s.execute("format.paragraph", &json!({"align": name})).unwrap();
|
||||
assert_eq!(para(&s), Some(want), "{name}");
|
||||
}
|
||||
assert!(s.execute("format.paragraph", &json!({"align": "bogus"})).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn right_to_left_paragraphs() {
|
||||
let mut s = session();
|
||||
@@ -3720,6 +3745,25 @@ fn undo_history_is_capped_by_memory_as_well_as_steps() {
|
||||
assert_eq!(s.doc().unwrap().history.undo.len(), 120);
|
||||
}
|
||||
|
||||
/// A slide's notes count toward the undo budget: small edits to a slide with large notes keep a
|
||||
/// copy of those notes per step, and must not slip past the budget as if the slide were empty.
|
||||
#[test]
|
||||
fn undo_history_counts_slide_notes() {
|
||||
let mut s = Session::with_new();
|
||||
let notes = "n".repeat(1 << 20);
|
||||
s.edit(|doc, _| {
|
||||
std::sync::Arc::make_mut(&mut doc.slides[0]).notes = deckcraft_model::text::TextBody::from_text(¬es);
|
||||
Ok(())
|
||||
})
|
||||
.unwrap();
|
||||
s.doc_mut().unwrap().history = History { limit: 500, byte_limit: 8 << 20, ..Default::default() };
|
||||
for i in 0..40 {
|
||||
s.execute("slide.rename", &json!({"name": format!("v{i}"), "index": 0})).unwrap();
|
||||
}
|
||||
let n = s.doc().unwrap().history.undo.len();
|
||||
assert!((1..=8).contains(&n), "{n} steps of 1 MB notes kept under an 8 MB budget");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn picture_styles_replace_each_other_in_one_step() {
|
||||
const PNG: &[u8] = &[
|
||||
@@ -5408,6 +5452,38 @@ fn pen_pressure_is_kept_and_a_mouse_draws_at_full_width() {
|
||||
assert_eq!(got, vec![vec![0.2, 1.0, 0.4], vec![1.0, 1.0]]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_long_ink_stroke_is_extended_in_place_and_capped() {
|
||||
let mut s = session();
|
||||
s.set_tool(tools::ToolKind::Ink { mode: "pen".into(), color: deckcraft_model::Rgba::rgb(20, 20, 20), width: 4.0 });
|
||||
let ev = |kind, x: f64| tools::PointerEvent { kind, x, y: 80.0, mods: Mods::default(), tol: 3.0, pressure: Some(0.5) };
|
||||
s.pointer(ev(PointerKind::Down, 0.0)).unwrap();
|
||||
let n = tools::MAX_GESTURE_POINTS + 5_000;
|
||||
for i in 1..n {
|
||||
s.pointer(ev(PointerKind::Drag, (i % 500) as f64)).unwrap();
|
||||
}
|
||||
assert_eq!(s.tool.ink_preview.len(), tools::MAX_GESTURE_POINTS);
|
||||
s.pointer(ev(PointerKind::Up, 0.0)).unwrap();
|
||||
let kept: usize = ink_pressures(&s).iter().map(Vec::len).sum();
|
||||
assert!((2..=tools::MAX_GESTURE_POINTS).contains(&kept), "{kept}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn double_clicking_a_capped_freeform_keeps_its_vertices() {
|
||||
let mut s = session();
|
||||
let ev = |kind, x: f64, y: f64| tools::PointerEvent { kind, x, y, mods: Default::default(), tol: 3.0, pressure: None };
|
||||
s.set_tool(tools::ToolKind::Shape { preset: "freeform".into() });
|
||||
s.pointer(ev(tools::PointerKind::Down, 100.0, 100.0)).unwrap();
|
||||
// Densely clicked vertices, each within the double-click tolerance of the last.
|
||||
for i in 0..tools::MAX_GESTURE_POINTS + 10 {
|
||||
s.pointer(ev(tools::PointerKind::Down, 300.0 + i as f64 * 0.001, 100.0)).unwrap();
|
||||
}
|
||||
s.pointer(ev(tools::PointerKind::DoubleClick, 300.0, 100.0)).unwrap();
|
||||
let st = s.doc().unwrap();
|
||||
let deckcraft_model::Geom::Custom { paths } = &st.shapes().last().unwrap().geom else { panic!("no freeform") };
|
||||
assert_eq!(paths[0].d.matches(" L ").count(), tools::MAX_GESTURE_POINTS - 1);
|
||||
}
|
||||
|
||||
fn ink_pressures(s: &Session) -> Vec<Vec<f32>> {
|
||||
let st = s.doc().unwrap();
|
||||
let Some(sh) = st.shapes().iter().rev().find(|sh| matches!(sh.kind, deckcraft_model::ShapeKind::Ink { .. })) else {
|
||||
@@ -6845,6 +6921,39 @@ fn master_body_placeholder_format_keeps_per_level_formatting() {
|
||||
assert_eq!(shown(&s), vec![Some(28.0), Some(24.0)]);
|
||||
}
|
||||
|
||||
/// A shape named many times in one edit is edited once and not copied per mention.
|
||||
#[test]
|
||||
fn repeated_target_ids_edit_the_shape_once() {
|
||||
let mut s = session();
|
||||
let r = s.execute("shape.insert", &json!({"preset": "rect", "rect": [100, 100, 300, 100], "text": "x".repeat(100_000)})).unwrap();
|
||||
let id = r["id"].as_u64().unwrap();
|
||||
let edited = s.execute("shape.fill", &json!({"ids": vec![id; 50_000], "color": "#ff0000"})).unwrap();
|
||||
assert_eq!(edited, json!(1));
|
||||
let v = s.execute("shape.inspect", &json!({"id": id})).unwrap();
|
||||
assert_eq!(v["shape"]["fill"]["color"]["base"]["rgb"], json!({"a": 255, "b": 0, "g": 0, "r": 255}));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn many_distinct_fonts_are_listed_once_each_without_a_quadratic_scan() {
|
||||
// 30,000 runs in 15,000 families, each written twice in different case.
|
||||
let mut p = deckcraft_model::Presentation::default();
|
||||
let slide = std::sync::Arc::make_mut(&mut p.slides[0]);
|
||||
let mut body = deckcraft_model::text::TextBody::from_text("x");
|
||||
let run = body.paragraphs[0].runs[0].clone();
|
||||
body.paragraphs[0].runs = (0..30_000)
|
||||
.map(|i| {
|
||||
let mut r = run.clone();
|
||||
let name = format!("Qqq Font {}", i / 2);
|
||||
r.props.font = Some(if i % 2 == 0 { name } else { name.to_uppercase() });
|
||||
r
|
||||
})
|
||||
.collect();
|
||||
slide.shapes[0].text = Some(body);
|
||||
let used = crate::fonts::used_fonts(&p);
|
||||
assert_eq!(used.iter().filter(|f| f.starts_with("Qqq Font ")).count(), 15_000);
|
||||
assert_eq!(crate::fonts::missing_fonts(&p).iter().filter(|f| f.to_ascii_lowercase().starts_with("qqq font ")).count(), 15_000);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn slide_master_view_rejects_invalid_indices() {
|
||||
let mut s = Session::with_new();
|
||||
@@ -7029,3 +7138,18 @@ fn outline_export_skips_footer_date_and_slide_number() {
|
||||
assert!(rtf.contains("Agenda") && rtf.contains("First point"), "{rtf:?}");
|
||||
assert!(!rtf.contains("Company Confidential") && !rtf.contains("1 May 2026"), "{rtf:?}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn shape_adjust_and_quick_style_reject_invalid_index() {
|
||||
let mut s = session();
|
||||
let r = s.execute("shape.insert", &json!({"preset": "roundRect", "rect": [100, 100, 200, 100]})).unwrap();
|
||||
let id = deckcraft_model::ShapeId(r["id"].as_u64().unwrap() as u32);
|
||||
let before = s.doc().unwrap().shape(id).unwrap().clone();
|
||||
for bad in [json!(-1), json!(1.5)] {
|
||||
assert!(s.execute("shape.adjust", &json!({"index": bad, "value": 0.4})).is_err(), "adjust accepted {bad}");
|
||||
assert!(s.execute("shape.quickStyle", &json!({"index": bad})).is_err(), "quickStyle accepted {bad}");
|
||||
}
|
||||
assert_eq!(s.doc().unwrap().shape(id).unwrap().geom, before.geom);
|
||||
assert_eq!(s.doc().unwrap().shape(id).unwrap().style, before.style);
|
||||
s.execute("shape.adjust", &json!({"value": 0.4})).unwrap();
|
||||
}
|
||||
|
||||
@@ -17,6 +17,7 @@ mod geometry;
|
||||
mod hit;
|
||||
mod pointer;
|
||||
|
||||
pub(crate) use drag::MAX_GESTURE_POINTS;
|
||||
pub use hit::{line_xfrm, resize_box, table_rows, text_pos};
|
||||
|
||||
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
|
||||
@@ -275,6 +276,8 @@ pub struct ToolState {
|
||||
pub path_preview: Vec<Point>,
|
||||
pub path_smooth: bool,
|
||||
pub path_closing: bool,
|
||||
/// Whether the last freeform press added a vertex (it adds none at the point cap).
|
||||
pub press_added_vertex: bool,
|
||||
/// The shape in Edit Points: its vertices are handles instead of the box's.
|
||||
pub edit_points: Option<ShapeId>,
|
||||
/// The vertex (path, index) whose control handles show in Edit Points.
|
||||
|
||||
@@ -4,8 +4,12 @@ use super::{Drag, Hit, Point, Result, Session, ShapeId, ShapeKind, TextSel, Tool
|
||||
impl Session {
|
||||
pub(super) fn double_click(&mut self, p: Point, tol: f64) -> Result<Value> {
|
||||
if let Some(Drag::Freeform { mode, mut points, .. }) = self.tool.drag.clone() {
|
||||
// The double-click's own click added a duplicate vertex.
|
||||
while points.len() > 2 && points.last().zip(points.get(points.len() - 2)).is_some_and(|(a, b)| (*a - *b).hypot() <= tol) {
|
||||
// The double-click's own click added a duplicate vertex (none at the point cap, where
|
||||
// the existing vertices are all kept).
|
||||
while self.tool.press_added_vertex
|
||||
&& points.len() > 2
|
||||
&& points.last().zip(points.get(points.len() - 2)).is_some_and(|(a, b)| (*a - *b).hypot() <= tol)
|
||||
{
|
||||
points.pop();
|
||||
}
|
||||
self.tool.drag = Some(Drag::Freeform { mode, points, pressed: false });
|
||||
|
||||
@@ -12,9 +12,16 @@ struct MoveDrag {
|
||||
enter_text: Option<(ShapeId, Point)>,
|
||||
}
|
||||
|
||||
/// Samples one ink or freeform gesture keeps; later pointer moves extend it no further.
|
||||
pub(crate) const MAX_GESTURE_POINTS: usize = 100_000;
|
||||
|
||||
impl Session {
|
||||
/// Update the active drag interaction using the pointer position and modifiers.
|
||||
pub(super) fn drag(&mut self, p: Point, mods: Mods, tol: f64) -> Result<Value> {
|
||||
if matches!(self.tool.drag, Some(Drag::Ink { .. })) {
|
||||
self.extend_ink(p);
|
||||
return Ok(Value::Null);
|
||||
}
|
||||
let Some(drag) = self.tool.drag.clone() else { return Ok(Value::Null) };
|
||||
match drag {
|
||||
Drag::Guide { index } => {
|
||||
@@ -181,7 +188,7 @@ impl Session {
|
||||
}
|
||||
Drag::Freeform { mode, mut points, pressed } => {
|
||||
// Dragging scribbles (except for curves, which only take clicked points).
|
||||
if pressed && mode != "curve" && points.last().is_none_or(|l| (*l - p).hypot() >= 1.5) {
|
||||
if pressed && mode != "curve" && points.len() < MAX_GESTURE_POINTS && points.last().is_none_or(|l| (*l - p).hypot() >= 1.5) {
|
||||
points.push(p);
|
||||
}
|
||||
let mut v = points.clone();
|
||||
@@ -191,15 +198,24 @@ impl Session {
|
||||
self.tool.path_preview = v;
|
||||
self.tool.drag = Some(Drag::Freeform { mode, points, pressed });
|
||||
}
|
||||
Drag::Ink { mut points } => {
|
||||
points.push(self.ink_sample(p));
|
||||
self.tool.ink_preview = points.clone();
|
||||
self.tool.drag = Some(Drag::Ink { points });
|
||||
}
|
||||
// Handled above.
|
||||
Drag::Ink { .. } => {}
|
||||
}
|
||||
Ok(Value::Null)
|
||||
}
|
||||
|
||||
/// Ink grows by one sample per move: extend it (and its preview) in place rather than
|
||||
/// copying the whole stroke twice per event, up to [`MAX_GESTURE_POINTS`].
|
||||
fn extend_ink(&mut self, p: Point) {
|
||||
let sample = self.ink_sample(p);
|
||||
if let Some(Drag::Ink { points }) = self.tool.drag.as_mut()
|
||||
&& points.len() < MAX_GESTURE_POINTS
|
||||
{
|
||||
points.push(sample);
|
||||
self.tool.ink_preview.push(sample);
|
||||
}
|
||||
}
|
||||
|
||||
fn drag_move(&mut self, drag: MoveDrag, p: Point, mods: Mods, tol: f64) -> Result<Value> {
|
||||
let MoveDrag { ids, start, origs, moved, duplicate, enter_text } = drag;
|
||||
let mut d = p - start;
|
||||
|
||||
@@ -228,7 +228,10 @@ impl Session {
|
||||
self.tool.drag = Some(Drag::Freeform { mode, points, pressed: false });
|
||||
return self.finish_freeform(true);
|
||||
}
|
||||
points.push(p);
|
||||
self.tool.press_added_vertex = points.len() < super::MAX_GESTURE_POINTS;
|
||||
if self.tool.press_added_vertex {
|
||||
points.push(p);
|
||||
}
|
||||
self.tool.path_preview = points.clone();
|
||||
self.tool.drag = Some(Drag::Freeform { mode, points, pressed: true });
|
||||
return Ok(Value::Null);
|
||||
|
||||
@@ -169,8 +169,14 @@ impl FontDb {
|
||||
if bundled.contains(fam) {
|
||||
continue;
|
||||
}
|
||||
if self.is_loaded(fam) || self.has_family(fam) {
|
||||
let f = self.face(fam, style);
|
||||
if self.is_loaded(fam) || (self.system_fallback_enabled() && self.has_family(fam)) {
|
||||
// With system fallback off, only a face that is already loaded: `face` could load more catalog files.
|
||||
let f = if self.system_fallback_enabled() {
|
||||
self.face(fam, style)
|
||||
} else {
|
||||
let Some(f) = self.find(fam, style) else { continue };
|
||||
f
|
||||
};
|
||||
if f.family.eq_ignore_ascii_case(fam) && covers_cluster(&f, cluster) {
|
||||
return f;
|
||||
}
|
||||
@@ -311,4 +317,21 @@ mod tests {
|
||||
assert!(c.windows(2).all(|w| w[0] <= w[1]));
|
||||
}
|
||||
}
|
||||
|
||||
/// With system fallback off, the automatic cascades (script families and symbol faces) load no
|
||||
/// installed family: the result comes from what is already loaded.
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
#[test]
|
||||
fn cascades_load_no_installed_family_when_system_fallback_is_off() {
|
||||
let db = FontDb::with_font_dirs(Vec::new());
|
||||
db.set_system_fallback(false);
|
||||
let base = db.face(crate::DEFAULT_FAMILY, "Regular");
|
||||
let loaded = |db: &FontDb| [b"Arab", b"Hebr", b"Thai", b"Deva"].into_iter().flat_map(|s| os_families(*s)).filter(|f| db.is_loaded(f)).count();
|
||||
let before = loaded(&db);
|
||||
for c in ['\u{0628}', '\u{05D0}', '\u{0E01}', '\u{0905}', '\u{2211}', '\u{2603}'] {
|
||||
let f = db.cascade(&[c], std::slice::from_ref(&base), "Regular");
|
||||
assert!(!f.family.is_empty());
|
||||
}
|
||||
assert_eq!(loaded(&db), before, "no installed family was loaded");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -976,6 +976,17 @@ impl FontDb {
|
||||
let _ = on;
|
||||
}
|
||||
|
||||
/// Whether automatic system-font lookups (fallback by character, symbol and script families)
|
||||
/// may touch installed fonts. Lookups of a family the document or host names are not gated.
|
||||
pub(crate) fn system_fallback_enabled(&self) -> bool {
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
{
|
||||
self.sys.lock().unwrap_or_else(std::sync::PoisonError::into_inner).enabled
|
||||
}
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
false
|
||||
}
|
||||
|
||||
/// Process-wide database: the bundled fonts, then the installed system fonts, cataloged the
|
||||
/// first time a lookup by family name needs them (or ahead of time by
|
||||
/// [`FontDb::scan_in_background`]).
|
||||
@@ -1287,7 +1298,7 @@ impl FontDb {
|
||||
Some(f)
|
||||
}
|
||||
|
||||
fn find(&self, family: &str, style: &str) -> Option<Arc<FontFace>> {
|
||||
pub(crate) fn find(&self, family: &str, style: &str) -> Option<Arc<FontFace>> {
|
||||
if style.contains('{') {
|
||||
{
|
||||
let faces = self.read_faces();
|
||||
@@ -1473,7 +1484,8 @@ impl FontDb {
|
||||
};
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
{
|
||||
let cataloged: Vec<String> = self.read_catalog().iter().map(|e| e.family.clone()).collect();
|
||||
let cataloged: Vec<String> =
|
||||
if self.system_fallback_enabled() { self.read_catalog().iter().map(|e| e.family.clone()).collect() } else { Vec::new() };
|
||||
for fam in SYSTEM_FALLBACKS.iter().filter(|f| f.contains("Symbol")) {
|
||||
if !self.is_loaded(fam) && cataloged.iter().any(|f| f.eq_ignore_ascii_case(fam)) {
|
||||
self.load_cataloged(fam);
|
||||
|
||||
@@ -425,3 +425,11 @@ fn the_font_scan_skips_fifos_folders_and_link_loops() {
|
||||
assert!(fifo || n >= 1);
|
||||
let _ = std::fs::remove_dir_all(&dir);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn disabling_system_fallback_gates_automatic_family_loads() {
|
||||
let db = FontDb::with_font_dirs(Vec::new());
|
||||
assert!(cfg!(target_arch = "wasm32") || db.system_fallback_enabled());
|
||||
db.set_system_fallback(false);
|
||||
assert!(!db.system_fallback_enabled());
|
||||
}
|
||||
|
||||
@@ -48,7 +48,7 @@ impl SliceDecoder<'_> {
|
||||
let intra_slice = self.sh.slice_type.is_intra();
|
||||
loop {
|
||||
ensure!(addr < total, "macroblock address {addr} out of range");
|
||||
self.start_mb(addr);
|
||||
self.start_mb(addr)?;
|
||||
let skip = !intra_slice && self.decode_skip_flag(&mut c);
|
||||
if skip {
|
||||
self.decode_skip()?;
|
||||
|
||||
+43
-10
@@ -15,7 +15,7 @@ use crate::slicedec::{PicState, SliceDecoder};
|
||||
use crate::transform::LevelScale;
|
||||
use crate::{ColorInfo, Picture};
|
||||
use deckcraft_bitstream::{BitReader, annexb_nals, length_prefixed_nals, unescape_rbsp};
|
||||
use std::collections::VecDeque;
|
||||
use std::collections::{HashMap, VecDeque};
|
||||
use std::sync::{Arc, Mutex};
|
||||
|
||||
/// One slice of a picture job.
|
||||
@@ -90,6 +90,10 @@ pub struct Decoder {
|
||||
ls_cache: Vec<(Arc<Pps>, Arc<LevelScale>)>,
|
||||
/// Pictures leaving the DPB, converted once their decoding job has finished.
|
||||
out_queue: VecDeque<Output>,
|
||||
/// Queued entries per frame id and their summed bytes, kept in step with `out_queue` so the
|
||||
/// budget check does not rescan it.
|
||||
queued_ids: HashMap<u32, u64>,
|
||||
queued_bytes: u64,
|
||||
shared: Arc<Shared>,
|
||||
#[cfg(feature = "threads")]
|
||||
pool: Option<rayon::ThreadPool>,
|
||||
@@ -175,6 +179,8 @@ impl Decoder {
|
||||
active_sps: None,
|
||||
ls_cache: Vec::new(),
|
||||
out_queue: VecDeque::new(),
|
||||
queued_ids: HashMap::new(),
|
||||
queued_bytes: 0,
|
||||
shared: Arc::new(Shared::default()),
|
||||
#[cfg(feature = "threads")]
|
||||
pool,
|
||||
@@ -283,7 +289,7 @@ impl Decoder {
|
||||
if !o.frame.is_published(o.frame.mb_h() - 1) && self.out_queue.len() <= max_queue {
|
||||
break;
|
||||
}
|
||||
let Some(o) = self.out_queue.pop_front() else { break };
|
||||
let Some(o) = self.pop_out() else { break };
|
||||
pics.push(make_picture(&o));
|
||||
}
|
||||
Ok(pics)
|
||||
@@ -295,7 +301,7 @@ impl Decoder {
|
||||
let mut outs = Vec::new();
|
||||
self.dpb.flush(&mut outs);
|
||||
self.emit(outs);
|
||||
let pics = self.out_queue.drain(..).map(|o| make_picture(&o)).collect();
|
||||
let pics = std::iter::from_fn(|| self.pop_out()).map(|o| make_picture(&o)).collect();
|
||||
while let Some(f) = self.in_flight.pop_front() {
|
||||
f.wait_complete();
|
||||
}
|
||||
@@ -451,13 +457,24 @@ impl Decoder {
|
||||
chroma_planes: if chroma == 0 { 0 } else { 2 },
|
||||
bytes_per_sample: 1,
|
||||
};
|
||||
let pictures = (sps.max_dpb_frames() + 1 + self.max_in_flight) as u64;
|
||||
ensure!(
|
||||
shape.fits(pictures, self.memory_budget),
|
||||
"{}x{} pictures with {pictures} in the decoder exceed the memory budget",
|
||||
sps.width(),
|
||||
sps.height()
|
||||
);
|
||||
// Pictures already held count at their own size, not the incoming SPS's: after a
|
||||
// resolution change, larger pictures of the old size can still be in the DPB or waiting
|
||||
// for output (one decode call may carry many access units). A buffer shared with a DPB
|
||||
// entry is counted once.
|
||||
let pic = shape.bytes().unwrap_or(u64::MAX);
|
||||
let held = |frames: &mut dyn Iterator<Item = &Arc<Frame>>| frames.fold(0u64, |n, f| n.saturating_add(f.bytes()));
|
||||
// Queued bytes minus the queue entries that share a DPB frame (each distinct DPB frame once).
|
||||
let mut ids: Vec<(u32, u64)> = self.dpb.entries.iter().map(|e| (e.frame.id, e.frame.bytes())).collect();
|
||||
ids.sort_unstable();
|
||||
ids.dedup();
|
||||
let shared = ids.iter().fold(0u64, |n, (id, b)| n.saturating_add(self.queued_ids.get(id).copied().unwrap_or(0).saturating_mul(*b)));
|
||||
let queued = self.queued_bytes.saturating_sub(shared);
|
||||
let in_dpb = held(&mut self.dpb.entries.iter().map(|e| &e.frame));
|
||||
let dpb = sps.max_dpb_frames().max(self.dpb.entries.len()) as u64;
|
||||
// The DPB (the larger of its new slots and what it holds now), the picture being decoded,
|
||||
// and what is in flight or queued.
|
||||
let bytes = pic.saturating_mul(dpb).max(in_dpb).saturating_add(pic).saturating_add(pic.saturating_mul(self.max_in_flight as u64).max(queued));
|
||||
ensure!(bytes <= self.memory_budget, "{}x{} pictures with {bytes} bytes in the decoder exceed the memory budget", sps.width(), sps.height());
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -562,8 +579,24 @@ impl Decoder {
|
||||
}
|
||||
|
||||
fn emit(&mut self, outs: Vec<Output>) {
|
||||
for o in &outs {
|
||||
*self.queued_ids.entry(o.frame.id).or_insert(0) += 1;
|
||||
self.queued_bytes = self.queued_bytes.saturating_add(o.frame.bytes());
|
||||
}
|
||||
self.out_queue.extend(outs);
|
||||
}
|
||||
|
||||
fn pop_out(&mut self) -> Option<Output> {
|
||||
let o = self.out_queue.pop_front()?;
|
||||
if let Some(n) = self.queued_ids.get_mut(&o.frame.id) {
|
||||
*n -= 1;
|
||||
if *n == 0 {
|
||||
self.queued_ids.remove(&o.frame.id);
|
||||
}
|
||||
}
|
||||
self.queued_bytes = self.queued_bytes.saturating_sub(o.frame.bytes());
|
||||
Some(o)
|
||||
}
|
||||
}
|
||||
|
||||
/// Decode all slices of one picture and publish its rows.
|
||||
|
||||
+53
-10
@@ -139,8 +139,9 @@ impl Dpb {
|
||||
let mut unused = (prev_ref_frame_num + 1) % max_frame_num;
|
||||
// Only the last `max_num_ref_frames` "non-existing" frames can survive the sliding window, and
|
||||
// inserting more than that evicts every earlier short-term reference. Skip straight to that tail
|
||||
// (keeping the POC state walk for every missing frame_num) so a hostile gap of up to 65535
|
||||
// frames costs at most `max_num_ref_frames` frame allocations instead of one per missing frame.
|
||||
// (advancing the POC state across the skipped run in constant time) so a hostile gap of up to
|
||||
// 65535 frames costs at most `max_num_ref_frames` frame allocations and POC updates instead of
|
||||
// one per missing frame.
|
||||
let gap = frame_num.wrapping_add(max_frame_num).wrapping_sub(unused) % max_frame_num;
|
||||
let keep = u32::try_from(max_num_ref_frames.max(1)).unwrap_or(u32::MAX);
|
||||
let skip = gap.saturating_sub(keep);
|
||||
@@ -149,10 +150,17 @@ impl Dpb {
|
||||
e.mark = RefMark::Unused;
|
||||
}
|
||||
self.prune();
|
||||
for _ in 0..skip {
|
||||
poc_state(unused);
|
||||
unused = (unused + 1) % max_frame_num;
|
||||
// The POC state only advances FrameNumOffset where frame_num decreases, so walking the
|
||||
// skipped run is the same as visiting its first value, the wrap (if the run crosses
|
||||
// it) and its last value: constant work however long the gap.
|
||||
let last = (unused + (skip - 1) % max_frame_num) % max_frame_num;
|
||||
poc_state(unused);
|
||||
if last < unused {
|
||||
poc_state(max_frame_num - 1);
|
||||
poc_state(0);
|
||||
}
|
||||
poc_state(last);
|
||||
unused = (last + 1) % max_frame_num;
|
||||
}
|
||||
let mut guard = 0;
|
||||
while unused != frame_num && guard < max_frame_num {
|
||||
@@ -438,14 +446,15 @@ impl Dpb {
|
||||
}
|
||||
}
|
||||
} else {
|
||||
while self.entries.len() >= self.capacity {
|
||||
// Evict until there is room (frame_num gap concealment can leave the DPB over-full), so
|
||||
// the store never holds more pictures than the memory budget was checked for.
|
||||
while self.entries.len() >= self.capacity.max(1) {
|
||||
if !self.bump(out) {
|
||||
if let Some(pos) = self.entries.iter().position(|e| !e.needed_for_output) {
|
||||
self.entries.remove(pos);
|
||||
} else {
|
||||
self.entries.remove(0);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -546,6 +555,37 @@ mod tests {
|
||||
(made, walk, left)
|
||||
}
|
||||
|
||||
/// (FrameNumOffset, prev frame_num) after `walk`, by the rule of `PocState::update_gap_frame`.
|
||||
fn poc_after(walk: &[u32], prev: u32, max: u32) -> (u64, u32) {
|
||||
walk.iter().fold((0, prev), |(off, prev), &f| (if prev > f { off + u64::from(max) } else { off }, f))
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn gap_walk_matches_every_frame_num_for_any_skip() {
|
||||
for (prev, cur) in [(1, 9), (10, 3), (14, 0), (15, 14), (0, 15), (7, 6)] {
|
||||
for refs in 1..4 {
|
||||
let mut dpb = Dpb::new();
|
||||
let (_, walk, _) = fill(&mut dpb, prev, cur, 16, refs);
|
||||
let every: Vec<u32> = (1..16).map(|i| (prev + i) % 16).take_while(|&f| f != cur).collect();
|
||||
assert_eq!(poc_after(&walk, prev, 16), poc_after(&every, prev, 16), "{prev} -> {cur}, {refs} refs");
|
||||
assert_eq!(walk.last(), every.last(), "{prev} -> {cur}, {refs} refs");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn reference_store_evicts_down_to_capacity() {
|
||||
let mut dpb = Dpb::new();
|
||||
dpb.capacity = 2;
|
||||
// Gap concealment can leave more non-output references than the capacity.
|
||||
for i in 0..5 {
|
||||
short(&mut dpb, i, i);
|
||||
}
|
||||
let mut out = Vec::new();
|
||||
assert!(!dpb.bump_for_space(RefMark::Short, 0, &mut out));
|
||||
assert!(dpb.entries.len() < 2, "{} pictures left before storing into a 2-picture DPB", dpb.entries.len());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn huge_gap_makes_only_the_frames_that_stay_referenced() {
|
||||
let mut dpb = Dpb::new();
|
||||
@@ -553,8 +593,9 @@ mod tests {
|
||||
short(&mut dpb, 2, 1);
|
||||
let (made, walk, left) = fill(&mut dpb, 1, 65535, 65536, 4);
|
||||
assert_eq!(made, 4);
|
||||
// The POC state still walks every missing frame_num, in order.
|
||||
assert_eq!(walk.len(), 65533);
|
||||
// The POC state ends where walking every missing frame_num would leave it, in a few steps.
|
||||
assert_eq!(poc_after(&walk, 1, 65536), poc_after(&(2..65535).collect::<Vec<_>>(), 1, 65536));
|
||||
assert!(walk.len() <= 8, "{} POC updates", walk.len());
|
||||
assert_eq!(walk.first(), Some(&2));
|
||||
assert_eq!(walk.last(), Some(&65534));
|
||||
assert_eq!(left, vec![(65531, true), (65532, true), (65533, true), (65534, true)]);
|
||||
@@ -566,7 +607,9 @@ mod tests {
|
||||
short(&mut dpb, 1, 65000);
|
||||
let (made, walk, left) = fill(&mut dpb, 65000, 3, 65536, 2);
|
||||
assert_eq!(made, 2);
|
||||
assert_eq!(walk.len(), 538);
|
||||
let every: Vec<u32> = (65001..65536).chain(0..3).collect();
|
||||
assert_eq!(poc_after(&walk, 65000, 65536), poc_after(&every, 65000, 65536));
|
||||
assert!(walk.len() <= 6, "{} POC updates", walk.len());
|
||||
assert_eq!(walk.first(), Some(&65001));
|
||||
assert_eq!(walk.last(), Some(&2));
|
||||
assert_eq!(left, vec![(1, true), (2, true)]);
|
||||
|
||||
@@ -160,6 +160,14 @@ impl Frame {
|
||||
self.rows.len()
|
||||
}
|
||||
|
||||
/// Sample bytes this picture holds (luma and both chroma planes), as the decoder's memory
|
||||
/// budget counts a picture.
|
||||
pub fn bytes(&self) -> u64 {
|
||||
(self.width as u64)
|
||||
.saturating_mul(self.height as u64)
|
||||
.saturating_add((self.cwidth as u64).saturating_mul(self.cheight as u64).saturating_mul(2))
|
||||
}
|
||||
|
||||
/// Publish MB row `r` (ignored if already published).
|
||||
pub fn publish(&self, r: usize, row: FrameRow) {
|
||||
let _ = self.rows[r].set(row);
|
||||
|
||||
@@ -346,8 +346,11 @@ impl<'a> SliceDecoder<'a> {
|
||||
// Neighbour helpers
|
||||
// ------------------------------------------------------------------------------------------
|
||||
|
||||
/// Set up the current macroblock address and neighbour availability.
|
||||
pub fn start_mb(&mut self, addr: usize) {
|
||||
/// Set up the current macroblock address and neighbour availability. A macroblock an earlier
|
||||
/// slice of this picture already decoded is refused: overlapping slices would otherwise redo
|
||||
/// the picture's reconstruction once per slice.
|
||||
pub fn start_mb(&mut self, addr: usize) -> Result<()> {
|
||||
ensure!(self.pic.mbs[addr].slice_num == u32::MAX, "macroblock {addr} overlaps an earlier slice");
|
||||
self.mb_addr = addr;
|
||||
self.mb_x = addr % self.mb_w;
|
||||
self.mb_y = addr / self.mb_w;
|
||||
@@ -364,6 +367,7 @@ impl<'a> SliceDecoder<'a> {
|
||||
];
|
||||
self.mv_done = 0;
|
||||
self.pic.mbs[addr] = MbState { slice_num: sn, ..MbState::default() };
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[inline]
|
||||
@@ -1467,7 +1471,7 @@ impl<'a> SliceDecoder<'a> {
|
||||
let run = r.read_ue()? as usize;
|
||||
ensure!(addr + run <= total, "mb_skip_run out of range");
|
||||
for _ in 0..run {
|
||||
self.start_mb(addr);
|
||||
self.start_mb(addr)?;
|
||||
self.decode_skip()?;
|
||||
self.pic.mb_done(addr);
|
||||
addr += 1;
|
||||
@@ -1477,7 +1481,7 @@ impl<'a> SliceDecoder<'a> {
|
||||
}
|
||||
ensure!(addr < total, "macroblock address {addr} out of range");
|
||||
}
|
||||
self.start_mb(addr);
|
||||
self.start_mb(addr)?;
|
||||
self.decode_mb_cavlc(r)?;
|
||||
self.pic.mb_done(addr);
|
||||
addr += 1;
|
||||
|
||||
@@ -113,6 +113,7 @@ fn avcc_rejects_a_valid_picture_slice_without_starting_a_picture() {
|
||||
poc_lsb: Some(0),
|
||||
poc_delta: None,
|
||||
wt: None,
|
||||
first_mb: 0,
|
||||
},
|
||||
false,
|
||||
);
|
||||
@@ -153,6 +154,8 @@ struct Pic {
|
||||
/// Explicit weights (luma, cb, cr) as (weight, offset) for refIdx 0 of list 0 and list 1;
|
||||
/// log2 denominators are 5 (luma) and 3 (chroma).
|
||||
wt: Option<[[(i32, i32); 3]; 2]>,
|
||||
/// first_mb_in_slice (`mbs` then lists the slice's macroblocks from there).
|
||||
first_mb: u32,
|
||||
}
|
||||
|
||||
impl Pic {
|
||||
@@ -165,7 +168,7 @@ impl Pic {
|
||||
}
|
||||
|
||||
fn slice_header(w: &mut BitWriter, p: &Pic, cabac: bool) {
|
||||
w.write_ue(0); // first_mb
|
||||
w.write_ue(p.first_mb);
|
||||
w.write_ue(if p.b {
|
||||
6
|
||||
} else if p.is_p() {
|
||||
@@ -660,3 +663,63 @@ fn frame_num_gap_is_filled_when_the_sps_allows_gaps() {
|
||||
assert_eq!(out.len(), 2);
|
||||
assert_eq!(dec.stats().frame_num_gaps, 1);
|
||||
}
|
||||
|
||||
/// An IDR picture followed by `pictures` reference P pictures of P_Skip macroblocks.
|
||||
fn skip_stream(pictures: u32) -> (Vec<u8>, Vec<Vec<u8>>) {
|
||||
let mut seed = 11u32;
|
||||
let a: Vec<Option<Vec<u8>>> = (0..NMB).map(|_| Some(random_mb(&mut seed))).collect();
|
||||
let mut head = sps(66, 2, false, PocType::T2);
|
||||
head.extend(pps(false, false));
|
||||
head.extend(slice_nal(&Pic { idr: true, frame_num: 0, ref_idc: 3, mbs: a, ..Default::default() }, false));
|
||||
let ps = (1..=pictures).map(|k| slice_nal(&Pic { frame_num: k % 16, ref_idc: 2, mbs: vec![None; NMB], ..Default::default() }, false)).collect();
|
||||
(head, ps)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn overlapping_slices_are_not_reconstructed_twice() {
|
||||
let (head, ps) = skip_stream(1);
|
||||
let mut once = Decoder::with_threads(1);
|
||||
let mut out = once.decode(&[head.clone(), ps[0].clone()].concat(), 0).unwrap();
|
||||
out.extend(once.flush());
|
||||
assert!(once.take_error().is_none());
|
||||
assert_eq!(out.len(), 2);
|
||||
// A second slice of the same picture starting at macroblock 1 covers those macroblocks again.
|
||||
let again = slice_nal(&Pic { frame_num: 1, ref_idc: 2, mbs: vec![None; NMB - 1], first_mb: 1, ..Default::default() }, false);
|
||||
let mut twice = Decoder::with_threads(1);
|
||||
let result = twice.decode(&[head, ps[0].clone(), again].concat(), 0);
|
||||
twice.flush();
|
||||
let err = result.err().or_else(|| twice.take_error()).expect("overlapping slice refused");
|
||||
assert!(err.to_string().contains("overlaps"), "{err}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn output_queue_counts_against_the_memory_budget() {
|
||||
let (head, ps) = skip_stream(40);
|
||||
let sps_nal = sps(66, 2, false, PocType::T2);
|
||||
let parsed = crate::params::Sps::parse(&deckcraft_bitstream::unescape_rbsp(&sps_nal[5..])).unwrap();
|
||||
let one = (NMB * 256 + 2 * NMB * 64) as u64;
|
||||
let budget = (parsed.max_dpb_frames() + 1 + 3) as u64 * one;
|
||||
// One access unit per call: output drains as it goes, and the stream fits.
|
||||
let mut dec = Decoder::with_threads(1);
|
||||
dec.set_memory_budget(budget);
|
||||
let mut out = dec.decode(&head, 0).unwrap();
|
||||
for (i, p) in ps.iter().enumerate() {
|
||||
out.extend(dec.decode(p, i as i64 + 1).unwrap());
|
||||
}
|
||||
out.extend(dec.flush());
|
||||
assert!(dec.take_error().is_none());
|
||||
assert_eq!(out.len(), 41);
|
||||
// Every access unit in one call: the pictures held for output stop fitting the budget.
|
||||
let mut dec = Decoder::with_threads(1);
|
||||
dec.set_memory_budget(budget);
|
||||
let err = dec.decode(&[head, ps.concat()].concat(), 0).expect_err("output queue is over budget");
|
||||
assert!(err.to_string().contains("memory budget"), "{err}");
|
||||
}
|
||||
|
||||
/// A held picture is charged at its own size (luma and both 4:2:0 chroma planes), the same
|
||||
/// count the budget uses for the incoming SPS's pictures.
|
||||
#[test]
|
||||
fn a_held_frame_is_charged_at_its_own_size() {
|
||||
let f = crate::picture::Frame::new(0, 0, 2, 3);
|
||||
assert_eq!(f.bytes(), (2 * 3 * 256 + 2 * 2 * 3 * 64) as u64);
|
||||
}
|
||||
|
||||
@@ -404,10 +404,36 @@ impl Decoder {
|
||||
chroma_planes: if chroma == 0 { 0 } else { 2 },
|
||||
bytes_per_sample: 2,
|
||||
};
|
||||
let frame_bytes = |f: &crate::picture::Frame| {
|
||||
(f.width as u64)
|
||||
.saturating_mul(f.height as u64)
|
||||
.saturating_add((f.cwidth as u64).saturating_mul(f.cheight as u64).saturating_mul(2))
|
||||
.saturating_mul(2)
|
||||
};
|
||||
// Output frames waiting to be returned stay allocated after they leave the DPB, at their
|
||||
// own size: a resolution change in this call can leave pictures of the previous SPS.
|
||||
let queued: u64 = self
|
||||
.out_queue
|
||||
.iter()
|
||||
.filter(|o| !self.dpb.entries.iter().any(|e| Arc::ptr_eq(&e.frame, &o.frame)) && !self.in_flight.iter().any(|f| Arc::ptr_eq(f, &o.frame)))
|
||||
.map(|o| frame_bytes(&o.frame))
|
||||
.fold(0, u64::saturating_add);
|
||||
// Frames the DPB holds now count at their own size too (an SPS change flushes them into the
|
||||
// queue before the new pictures are made): the DPB part is the larger of its new slots and that.
|
||||
let held: u64 = self.dpb.entries.iter().map(|e| frame_bytes(&e.frame)).fold(0, u64::saturating_add);
|
||||
// A queued frame still being decoded is one of the in-flight pictures already reserved, so the
|
||||
// queue and the in-flight reservation overlap: charge the larger, not both.
|
||||
let pictures = u64::from(sps.max_dec_pic_buffering) + 1 + self.max_in_flight as u64;
|
||||
let slots = shape.store_bytes(u64::from(sps.max_dec_pic_buffering));
|
||||
let flight = shape.store_bytes(self.max_in_flight as u64);
|
||||
let total = shape
|
||||
.store_bytes(1)
|
||||
.zip(slots)
|
||||
.zip(flight)
|
||||
.map(|((one, slots), flight)| slots.max(held).saturating_add(one).saturating_add(flight).saturating_add(queued));
|
||||
ensure!(
|
||||
shape.fits(pictures, self.memory_budget),
|
||||
"{}x{} pictures with {pictures} in the decoder exceed the memory budget",
|
||||
total.is_some_and(|b| b <= self.memory_budget),
|
||||
"{}x{} pictures with {pictures} in the decoder and {queued} bytes queued exceed the memory budget",
|
||||
sps.width,
|
||||
sps.height
|
||||
);
|
||||
|
||||
@@ -1,13 +0,0 @@
|
||||
[package]
|
||||
name = "deckcraft-isobmff"
|
||||
description = "Clean-room ISO BMFF (MP4) and QuickTime (MOV) demuxer and muxer"
|
||||
version.workspace = true
|
||||
edition.workspace = true
|
||||
license.workspace = true
|
||||
rust-version.workspace = true
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
||||
[dependencies]
|
||||
craft-media = { workspace = true, features = ["isobmff"] }
|
||||
@@ -1,6 +0,0 @@
|
||||
//! ISO BMFF (MP4) and QuickTime (MOV) demuxer and muxer: a re-export of [`craft_media::isobmff`]
|
||||
//! from craft-libs. Supported boxes and limitations are in the craft-media README.
|
||||
|
||||
#![cfg_attr(not(test), deny(clippy::unwrap_used, clippy::expect_used, clippy::panic, clippy::unimplemented, clippy::todo, clippy::unreachable))]
|
||||
|
||||
pub use craft_media::isobmff::*;
|
||||
@@ -1,13 +0,0 @@
|
||||
[package]
|
||||
name = "deckcraft-matroska"
|
||||
description = "Clean-room Matroska (MKV) and WebM demuxer and muxer"
|
||||
version.workspace = true
|
||||
edition.workspace = true
|
||||
license.workspace = true
|
||||
rust-version.workspace = true
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
||||
[dependencies]
|
||||
craft-media = { workspace = true, features = ["matroska"] }
|
||||
@@ -1,6 +0,0 @@
|
||||
//! Matroska (MKV) and WebM demuxer and muxer: a re-export of [`craft_media::matroska`] from
|
||||
//! craft-libs. Supported elements and limitations are in the craft-media README.
|
||||
|
||||
#![cfg_attr(not(test), deny(clippy::unwrap_used, clippy::expect_used, clippy::panic, clippy::unimplemented, clippy::todo, clippy::unreachable))]
|
||||
|
||||
pub use craft_media::matroska::*;
|
||||
@@ -16,6 +16,12 @@ const INVALID_REQUEST: i64 = -32600;
|
||||
const METHOD_NOT_FOUND: i64 = -32601;
|
||||
const INVALID_PARAMS: i64 = -32602;
|
||||
const INTERNAL_ERROR: i64 = -32603;
|
||||
|
||||
/// Entries one batch (a JSON-RPC batch, or the `batch` tool's commands) may hold.
|
||||
pub(crate) const MAX_BATCH_ITEMS: usize = 1000;
|
||||
/// Bytes of results one batch may gather before the rest of it is refused: each result can copy
|
||||
/// much of the document (an inspection holds every note), so a batch holds at most this much.
|
||||
pub(crate) const MAX_BATCH_BYTES: usize = 64 << 20;
|
||||
const RESOURCE_NOT_FOUND: i64 = -32002;
|
||||
/// Keep request frames in step with the connected backend's reply limit.
|
||||
const MAX_FRAME_BYTES: usize = 16 * 1024 * 1024;
|
||||
@@ -93,8 +99,28 @@ impl Server {
|
||||
// Batches were removed in 2025-06-18; still answer older clients sensibly.
|
||||
if batch.is_empty() {
|
||||
Some(error(Value::Null, INVALID_REQUEST, "empty batch"))
|
||||
} else if batch.len() > MAX_BATCH_ITEMS {
|
||||
Some(error(Value::Null, INVALID_REQUEST, format!("a batch holds at most {MAX_BATCH_ITEMS} messages")))
|
||||
} else {
|
||||
let replies: Vec<Value> = batch.into_iter().filter_map(|m| self.handle(m)).collect();
|
||||
let mut used = 0usize;
|
||||
let mut replies: Vec<Value> = Vec::new();
|
||||
for m in batch {
|
||||
if used > MAX_BATCH_BYTES {
|
||||
// Not run: answer each remaining request with a short error.
|
||||
if let Some(id) = m.get("id").filter(|id| id.is_string() || id.is_number()) {
|
||||
replies.push(error(id.clone(), INTERNAL_ERROR, "the batch's replies passed 64 MiB; not run"));
|
||||
}
|
||||
continue;
|
||||
}
|
||||
if let Some(r) = self.handle(m) {
|
||||
used = used.saturating_add(r.to_string().len());
|
||||
replies.push(if used > MAX_BATCH_BYTES {
|
||||
error(r.get("id").cloned().unwrap_or(Value::Null), INTERNAL_ERROR, "the batch's replies passed 64 MiB; reply dropped")
|
||||
} else {
|
||||
r
|
||||
});
|
||||
}
|
||||
}
|
||||
(!replies.is_empty()).then_some(Value::Array(replies))
|
||||
}
|
||||
}
|
||||
|
||||
+29
-10
@@ -357,21 +357,40 @@ pub fn call_tool(b: &mut dyn Backend, name: &str, a: &Value) -> ToolResult {
|
||||
wrap(exec(b, c, a.get("params").cloned().unwrap_or(json!({}))))
|
||||
}
|
||||
"batch" => {
|
||||
use crate::server::{MAX_BATCH_BYTES, MAX_BATCH_ITEMS};
|
||||
let keep = a.get("keepGoing").and_then(Value::as_bool).unwrap_or(false);
|
||||
let commands = a.get("commands").and_then(Value::as_array).map(Vec::as_slice).unwrap_or_default();
|
||||
if commands.len() > MAX_BATCH_ITEMS {
|
||||
return ToolResult::error(format!("a batch holds at most {MAX_BATCH_ITEMS} commands"));
|
||||
}
|
||||
let mut out = vec![];
|
||||
for c in a.get("commands").and_then(Value::as_array).cloned().unwrap_or_default() {
|
||||
let mut used = 0usize;
|
||||
for c in commands {
|
||||
let id = c.get("command").and_then(Value::as_str).unwrap_or("");
|
||||
match exec(b, id, c.get("params").cloned().unwrap_or(json!({}))) {
|
||||
Ok(v) => out.push(json!({"command": id, "ok": true, "result": v})),
|
||||
Err(e) => {
|
||||
out.push(json!({"command": id, "ok": false, "error": e}));
|
||||
if !keep {
|
||||
return ToolResult { content: vec![json!({"type": "text", "text": Value::Array(out).to_string()})], is_error: true };
|
||||
}
|
||||
}
|
||||
let (entry, failed) = match exec(b, id, c.get("params").cloned().unwrap_or(json!({}))) {
|
||||
Ok(v) => (json!({"command": id, "ok": true, "result": v}), false),
|
||||
Err(e) => (json!({"command": id, "ok": false, "error": e}), true),
|
||||
};
|
||||
// What the reply carries: the entry as text, escaped again inside the JSON-RPC reply.
|
||||
// Failures count too: with keepGoing, a remote backend's large errors add up.
|
||||
used = used.saturating_add(json!(entry.to_string()).to_string().len());
|
||||
if used > MAX_BATCH_BYTES {
|
||||
let shown: String = id.chars().take(128).collect();
|
||||
out.push(json!({"command": shown, "ok": false, "error": "the batch's results passed 64 MiB; stopped here"}));
|
||||
return ToolResult { content: vec![json!({"type": "text", "text": Value::Array(out).to_string()})], is_error: true };
|
||||
}
|
||||
out.push(entry);
|
||||
if failed && !keep {
|
||||
return ToolResult { content: vec![json!({"type": "text", "text": Value::Array(out).to_string()})], is_error: true };
|
||||
}
|
||||
}
|
||||
ToolResult::json(&Value::Array(out))
|
||||
// The reply is pretty-printed, which is longer than the compact entries counted above.
|
||||
let reply = ToolResult::json(&Value::Array(out));
|
||||
let text = reply.content[0]["text"].as_str().unwrap_or_default();
|
||||
if json!(text).to_string().len() > MAX_BATCH_BYTES {
|
||||
return ToolResult::error("the batch's results passed 64 MiB; nothing is returned");
|
||||
}
|
||||
reply
|
||||
}
|
||||
"new_presentation" => wrap(exec(b, "file.new", a)),
|
||||
"open_presentation" => wrap(exec(b, "file.open", a)),
|
||||
|
||||
@@ -0,0 +1,70 @@
|
||||
//! A batch (JSON-RPC or the `batch` tool) is bounded in entries and in the bytes of results it
|
||||
//! gathers, so a short request cannot copy a large document into memory hundreds of times.
|
||||
|
||||
use deckcraft_mcp::{Headless, Server};
|
||||
use serde_json::{Value, json};
|
||||
|
||||
fn server_with_notes(bytes: usize) -> Server {
|
||||
let mut server = Server::new(Box::new(Headless::with_document()));
|
||||
let line = json!({"jsonrpc": "2.0", "id": 1, "method": "tools/call", "params": {"name": "run_command", "arguments": {"command": "slide.notes", "params": {"text": "n".repeat(bytes)}}}});
|
||||
let reply: Value = serde_json::from_str(&server.handle_line(&line.to_string()).unwrap()).unwrap();
|
||||
assert_ne!(reply["result"]["isError"], true, "{reply}");
|
||||
server
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn json_rpc_batches_are_bounded_in_entries_and_reply_bytes() {
|
||||
let mut server = server_with_notes(1 << 20);
|
||||
let ping = |i: usize| json!({"jsonrpc": "2.0", "id": i, "method": "ping"});
|
||||
let too_many: Vec<Value> = (0..1001).map(ping).collect();
|
||||
let reply: Value = serde_json::from_str(&server.handle_line(&Value::Array(too_many).to_string()).unwrap()).unwrap();
|
||||
assert_eq!(reply["error"]["code"], -32600, "{reply}");
|
||||
// 100 inspections of a slide with 1 MB of notes: about 64 are answered, the rest refused.
|
||||
let inspect = |i: usize| json!({"jsonrpc": "2.0", "id": i, "method": "tools/call", "params": {"name": "inspect_slide", "arguments": {}}});
|
||||
let line = Value::Array((0..100).map(inspect).collect()).to_string();
|
||||
let reply = server.handle_line(&line).unwrap();
|
||||
assert!(reply.len() < (64 << 20) + (4 << 20), "{} bytes", reply.len());
|
||||
let replies: Vec<Value> = serde_json::from_str(&reply).unwrap();
|
||||
assert_eq!(replies.len(), 100, "every request is answered");
|
||||
assert!(replies.last().unwrap()["error"]["message"].as_str().unwrap().contains("64 MiB"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn the_batch_tool_is_bounded_in_commands_and_result_bytes() {
|
||||
let mut server = server_with_notes(1 << 20);
|
||||
let call = |server: &mut Server, commands: Vec<Value>| {
|
||||
let line = json!({"jsonrpc": "2.0", "id": 1, "method": "tools/call", "params": {"name": "batch", "arguments": {"commands": commands, "keepGoing": true}}});
|
||||
let reply: Value = serde_json::from_str(&server.handle_line(&line.to_string()).unwrap()).unwrap();
|
||||
(reply["result"]["isError"] == true, reply["result"]["content"][0]["text"].as_str().unwrap_or_default().to_string())
|
||||
};
|
||||
let (err, text) = call(&mut server, vec![json!({"command": "document.inspect"}); 1001]);
|
||||
assert!(err && text.contains("at most 1000"), "{text}");
|
||||
let (err, text) = call(&mut server, vec![json!({"command": "slide.inspect"}); 100]);
|
||||
assert!(err && text.contains("64 MiB"), "{}", &text[text.len().saturating_sub(200)..]);
|
||||
assert!(text.len() < (64 << 20) + (4 << 20));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn the_batch_tool_charges_the_escaped_reply() {
|
||||
// Quotes escape twice on the way out (`\\\"`), so 30 results of ~2 MiB serialize to ~120 MiB.
|
||||
let mut server = Server::new(Box::new(Headless::with_document()));
|
||||
let notes = json!({"jsonrpc": "2.0", "id": 1, "method": "tools/call", "params": {"name": "run_command", "arguments": {"command": "slide.notes", "params": {"text": "\"".repeat(1 << 20)}}}});
|
||||
server.handle_line(¬es.to_string()).unwrap();
|
||||
let commands = vec![json!({"command": "slide.inspect"}); 30];
|
||||
let line = json!({"jsonrpc": "2.0", "id": 2, "method": "tools/call", "params": {"name": "batch", "arguments": {"commands": commands, "keepGoing": true}}});
|
||||
let reply = server.handle_line(&line.to_string()).unwrap();
|
||||
assert!(reply.len() < (64 << 20) + (4 << 20), "{} bytes", reply.len());
|
||||
assert!(reply.contains("64 MiB"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn the_batch_tool_charges_failures_too() {
|
||||
// Each unknown command's error echoes its ~1 MiB id; with keepGoing none of them is a result.
|
||||
let mut server = Server::new(Box::new(Headless::with_document()));
|
||||
let id = "x".repeat(1 << 20);
|
||||
let commands = vec![json!({"command": id}); 70];
|
||||
let line = json!({"jsonrpc": "2.0", "id": 2, "method": "tools/call", "params": {"name": "batch", "arguments": {"commands": commands, "keepGoing": true}}});
|
||||
let reply = server.handle_line(&line.to_string()).unwrap();
|
||||
assert!(reply.len() < (64 << 20) + (4 << 20), "{} bytes", reply.len());
|
||||
assert!(reply.contains("64 MiB"));
|
||||
}
|
||||
@@ -1,3 +1,5 @@
|
||||
// Integration tests: unwrapping and panicking on failure is fine here, unlike in shipped code (AGENTS.md).
|
||||
#[allow(clippy::unwrap_used, clippy::expect_used, clippy::panic)]
|
||||
mod automation_roots;
|
||||
#[allow(clippy::unwrap_used, clippy::expect_used, clippy::panic)]
|
||||
mod batch_budget;
|
||||
|
||||
@@ -9,10 +9,9 @@ description = "DeckCraft media: probing, audio decoding (MP3, AAC/M4A, WAV, AIFF
|
||||
workspace = true
|
||||
|
||||
[dependencies]
|
||||
deckcraft-isobmff = { workspace = true }
|
||||
deckcraft-matroska = { workspace = true }
|
||||
deckcraft-ogg = { workspace = true }
|
||||
deckcraft-opus = { workspace = true }
|
||||
# Containers (MP4/MOV, Matroska/WebM, Ogg) and the Opus decoder, shared with the other Craft apps.
|
||||
craft-media = { workspace = true, features = ["isobmff", "matroska", "ogg"] }
|
||||
craft-audio = { workspace = true }
|
||||
deckcraft-h264 = { workspace = true }
|
||||
deckcraft-hevc = { workspace = true }
|
||||
deckcraft-vp9 = { workspace = true }
|
||||
|
||||
@@ -81,7 +81,9 @@ pub fn decode(bytes: &Bytes) -> Result<Pcm> {
|
||||
Some(r) => r,
|
||||
None => contained(|| decode_symphonia(bytes, "mkv")),
|
||||
},
|
||||
Container::Mp4 if !crate::mp4guard::tables_fit(bytes) => Err(MediaError::Corrupt("the MP4's sample tables do not fit the file".into())),
|
||||
Container::Mp4 | Container::Unknown | Container::Mp3 if !crate::mp4guard::probe_fits(bytes) => {
|
||||
Err(MediaError::Corrupt("the MP4's sample tables do not fit the file".into()))
|
||||
}
|
||||
c => contained(|| decode_symphonia(bytes, c.name())),
|
||||
}
|
||||
}
|
||||
@@ -89,15 +91,15 @@ pub fn decode(bytes: &Bytes) -> Result<Pcm> {
|
||||
/// Ogg Opus with our decoder; `None` when the file holds no Opus stream.
|
||||
fn decode_ogg_opus(bytes: &Bytes) -> Option<Result<Pcm>> {
|
||||
let src: &[u8] = bytes;
|
||||
let file = deckcraft_ogg::open(src).ok()?;
|
||||
let s = file.stream_of(deckcraft_ogg::Codec::Opus)?;
|
||||
let file = craft_media::ogg::open(src).ok()?;
|
||||
let s = file.stream_of(craft_media::ogg::Codec::Opus)?;
|
||||
let stream = file.streams.get(s)?;
|
||||
Some((|| {
|
||||
let head = stream.headers.first().ok_or(MediaError::Corrupt("Opus header missing".into()))?;
|
||||
let mut dec = deckcraft_opus::Decoder::new(head).map_err(|e| MediaError::Corrupt(e.to_string()))?;
|
||||
let mut dec = craft_audio::opus::Decoder::new(head).map_err(|e| MediaError::Corrupt(e.to_string()))?;
|
||||
dec.set_trim_pre_skip(true);
|
||||
let channels = dec.channels() as u16;
|
||||
let timing = deckcraft_ogg::OpusTiming::of(stream, dec.pre_skip() as u32).map_err(|e| MediaError::Corrupt(e.to_string()))?;
|
||||
let timing = craft_media::ogg::OpusTiming::of(stream, dec.pre_skip() as u32);
|
||||
let mut out = Vec::new();
|
||||
for i in 0..stream.packets.len() {
|
||||
if MAX_PCM_SAMPLES.saturating_sub(out.len()) < channels.max(1) as usize {
|
||||
@@ -113,7 +115,8 @@ fn decode_ogg_opus(bytes: &Bytes) -> Option<Result<Pcm>> {
|
||||
}
|
||||
}
|
||||
// End trimming (RFC 7845 §4.5).
|
||||
let total = timing.total.max(0) as usize * channels.max(1) as usize;
|
||||
// the timing saturates on a hostile granule, so this product must too
|
||||
let total = usize::try_from(timing.total).unwrap_or(0).saturating_mul(channels.max(1) as usize);
|
||||
if total > 0 && total < out.len() {
|
||||
out.truncate(total);
|
||||
}
|
||||
@@ -124,13 +127,13 @@ fn decode_ogg_opus(bytes: &Bytes) -> Option<Result<Pcm>> {
|
||||
/// WebM / Matroska Opus with our decoder; `None` when there is no Opus track.
|
||||
fn decode_mkv_opus(bytes: &Bytes) -> Option<Result<Pcm>> {
|
||||
let src: &[u8] = bytes;
|
||||
let file = deckcraft_matroska::open(src).ok()?;
|
||||
let file = craft_media::matroska::open(src).ok()?;
|
||||
let (ti, head) = file.tracks.iter().enumerate().find_map(|(i, t)| match &t.codec {
|
||||
deckcraft_matroska::Codec::Opus { head } => Some((i, head.clone())),
|
||||
craft_media::matroska::Codec::Opus { head } => Some((i, head.clone())),
|
||||
_ => None,
|
||||
})?;
|
||||
Some((|| {
|
||||
let mut dec = deckcraft_opus::Decoder::new(&head).map_err(|e| MediaError::Corrupt(e.to_string()))?;
|
||||
let mut dec = craft_audio::opus::Decoder::new(&head).map_err(|e| MediaError::Corrupt(e.to_string()))?;
|
||||
dec.set_trim_pre_skip(true);
|
||||
let channels = dec.channels() as u16;
|
||||
let mut out = Vec::new();
|
||||
|
||||
@@ -12,6 +12,29 @@ pub(crate) fn tables_fit(b: &[u8]) -> bool {
|
||||
boxes_fit(b, b.len() as u64, 0)
|
||||
}
|
||||
|
||||
/// Bytes symphonia's probe scans for a format marker before giving up.
|
||||
const PROBE_SEARCH_LIMIT: usize = 1024 * 1024;
|
||||
|
||||
/// Whether symphonia can be given `b`, whatever the sniffer made of it: its probe takes the first
|
||||
/// `ftyp` within its search window as the start of an MP4 (the size field 4 bytes before it), so
|
||||
/// the table check must start where that parser will, not at offset 0.
|
||||
pub(crate) fn probe_fits(b: &[u8]) -> bool {
|
||||
if crate::Container::sniff(b) == crate::Container::Mp4 && !tables_fit(b) {
|
||||
return false;
|
||||
}
|
||||
// The probe reads leading ID3v2 tags as metadata and searches again after the last one.
|
||||
let mut rest = b;
|
||||
while rest.starts_with(b"ID3") {
|
||||
let size = rest.get(6..10).map(|s| s.iter().fold(0usize, |a, &x| (a << 7) | (x & 0x7f) as usize)).unwrap_or(0);
|
||||
rest = rest.get(10 + size..).unwrap_or(&[]);
|
||||
}
|
||||
let head = &rest[..rest.len().min(PROBE_SEARCH_LIMIT + 16)];
|
||||
match head.windows(4).position(|w| w == b"ftyp") {
|
||||
Some(p) => tables_fit(&rest[p.saturating_sub(4)..]),
|
||||
None => true,
|
||||
}
|
||||
}
|
||||
|
||||
fn be32(b: &[u8], at: usize) -> Option<u64> {
|
||||
b.get(at..at.checked_add(4)?).map(|s| u32::from_be_bytes([s[0], s[1], s[2], s[3]]) as u64)
|
||||
}
|
||||
@@ -74,3 +97,29 @@ fn table_fits(kind: &[u8], body: &[u8], file_len: u64) -> bool {
|
||||
};
|
||||
be32(body, 4).is_some_and(|count| count <= body.len().saturating_sub(8) as u64 / entry)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
/// An MP4 whose `stsc` count asks for far more entries than the file holds.
|
||||
fn mp4_with_huge_stsc() -> Vec<u8> {
|
||||
let stsc = [&24u32.to_be_bytes()[..], b"stsc", &[0, 0, 0, 0], &u32::MAX.to_be_bytes(), &[0; 8]].concat();
|
||||
let stbl = [&(stsc.len() as u32 + 8).to_be_bytes()[..], b"stbl", &stsc].concat();
|
||||
let moov = [&(stbl.len() as u32 + 8).to_be_bytes()[..], b"moov", &stbl].concat();
|
||||
[&16u32.to_be_bytes()[..], b"ftyp", b"isom", &[0; 4], &moov].concat()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_prefixed_mp4_is_checked_where_symphonia_starts_it() {
|
||||
let mp4 = mp4_with_huge_stsc();
|
||||
assert!(!tables_fit(&mp4));
|
||||
assert!(!probe_fits(&mp4));
|
||||
let prefixed = [&b"junk before the file "[..], &mp4].concat();
|
||||
assert_eq!(crate::Container::sniff(&prefixed), crate::Container::Unknown);
|
||||
assert!(tables_fit(&prefixed), "offset 0 sees no MP4 boxes");
|
||||
assert!(!probe_fits(&prefixed));
|
||||
let far = [&vec![0u8; PROBE_SEARCH_LIMIT + 16][..], &mp4].concat();
|
||||
assert!(probe_fits(&far), "past the probe window symphonia never reads it as an MP4");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -71,7 +71,8 @@ pub fn probe_headers(bytes: &Bytes) -> Result<MediaInfo> {
|
||||
};
|
||||
if info.audio.is_none() && info.video.is_none() || info.audio.as_ref().is_some_and(|a| a.sample_rate == 0) {
|
||||
// Plain audio files (and anything our demuxers didn't describe): symphonia's probe.
|
||||
if c == Container::Mp4 && !crate::mp4guard::tables_fit(bytes) {
|
||||
// Symphonia opens an MP4 wherever its probe finds an `ftyp` (after leading ID3v2 tags too); other known containers (a WAV's audio bytes can spell an `ftyp`) skip the check.
|
||||
if matches!(c, Container::Mp4 | Container::Unknown | Container::Mp3) && !crate::mp4guard::probe_fits(bytes) {
|
||||
return Err(MediaError::Corrupt("the MP4's sample tables do not fit the file".into()));
|
||||
}
|
||||
let (codec, rate, channels, frames) = std::panic::catch_unwind(|| crate::audio::probe_symphonia(bytes, c.name()))
|
||||
@@ -105,9 +106,9 @@ fn codec_label(short: &str) -> String {
|
||||
}
|
||||
|
||||
fn probe_mp4(bytes: &Bytes) -> Result<MediaInfo> {
|
||||
use deckcraft_isobmff::{CodecConfig, TrackKind};
|
||||
use craft_media::isobmff::{CodecConfig, TrackKind};
|
||||
let src: &[u8] = bytes;
|
||||
let f = deckcraft_isobmff::open(src).map_err(|e| MediaError::Corrupt(e.to_string()))?;
|
||||
let f = craft_media::isobmff::open(src).map_err(|e| MediaError::Corrupt(e.to_string()))?;
|
||||
let mut info = MediaInfo { container: if f.is_quicktime { "mov" } else { "mp4" }, ..Default::default() };
|
||||
let mut dur = if f.timescale > 0 { f.duration as f64 / f.timescale as f64 } else { 0.0 };
|
||||
if let Some(ti) = f.track_of_kind(TrackKind::Video) {
|
||||
@@ -145,9 +146,9 @@ fn probe_mp4(bytes: &Bytes) -> Result<MediaInfo> {
|
||||
}
|
||||
|
||||
fn probe_mkv(bytes: &Bytes) -> Result<MediaInfo> {
|
||||
use deckcraft_matroska::{Codec, TrackKind};
|
||||
use craft_media::matroska::{Codec, TrackKind};
|
||||
let src: &[u8] = bytes;
|
||||
let f = deckcraft_matroska::open(src).map_err(|e| MediaError::Corrupt(e.to_string()))?;
|
||||
let f = craft_media::matroska::open(src).map_err(|e| MediaError::Corrupt(e.to_string()))?;
|
||||
let mut info = MediaInfo { container: if f.is_webm() { "webm" } else { "matroska" }, ..Default::default() };
|
||||
let dur = f.duration_ns().map(|n| n as f64 / 1e9).unwrap_or(0.0);
|
||||
info.duration_ms = ms(dur);
|
||||
@@ -178,11 +179,11 @@ fn probe_mkv(bytes: &Bytes) -> Result<MediaInfo> {
|
||||
|
||||
fn probe_ogg(bytes: &Bytes) -> Option<MediaInfo> {
|
||||
let src: &[u8] = bytes;
|
||||
let f = deckcraft_ogg::open(src).ok()?;
|
||||
let s = f.stream_of(deckcraft_ogg::Codec::Opus)?;
|
||||
let f = craft_media::ogg::open(src).ok()?;
|
||||
let s = f.stream_of(craft_media::ogg::Codec::Opus)?;
|
||||
let st = f.streams.get(s)?;
|
||||
let head = deckcraft_opus::OpusHead::parse(st.headers.first()?).ok()?;
|
||||
let timing = deckcraft_ogg::OpusTiming::of(st, head.pre_skip as u32).ok()?;
|
||||
let head = craft_audio::opus::OpusHead::parse(st.headers.first()?).ok()?;
|
||||
let timing = craft_media::ogg::OpusTiming::of(st, head.pre_skip as u32);
|
||||
Some(MediaInfo {
|
||||
container: "ogg",
|
||||
duration_ms: ms(timing.total as f64 / 48_000.0),
|
||||
|
||||
@@ -3,8 +3,8 @@
|
||||
|
||||
use std::collections::VecDeque;
|
||||
|
||||
use deckcraft_isobmff::CodecConfig;
|
||||
use deckcraft_matroska::Codec as MkvCodec;
|
||||
use craft_media::isobmff::CodecConfig;
|
||||
use craft_media::matroska::Codec as MkvCodec;
|
||||
|
||||
use crate::yuv::{self, Matrix, Planes};
|
||||
use crate::{Bytes, Container, MediaError, Result};
|
||||
@@ -257,8 +257,8 @@ struct Track {
|
||||
}
|
||||
|
||||
enum Demux {
|
||||
Mp4(Box<deckcraft_isobmff::Mp4File>, usize),
|
||||
Mkv(Box<deckcraft_matroska::MkvFile>, usize),
|
||||
Mp4(Box<craft_media::isobmff::Mp4File>, usize),
|
||||
Mkv(Box<craft_media::matroska::MkvFile>, usize),
|
||||
}
|
||||
|
||||
impl Demux {
|
||||
@@ -273,8 +273,8 @@ impl Demux {
|
||||
fn open_track(bytes: &[u8]) -> Result<(Demux, Track, Box<dyn Codec>)> {
|
||||
match Container::sniff(bytes) {
|
||||
Container::Mp4 => {
|
||||
let f = deckcraft_isobmff::open(bytes).map_err(dec_err)?;
|
||||
let ti = f.track_of_kind(deckcraft_isobmff::TrackKind::Video).ok_or(MediaError::Missing("video track"))?;
|
||||
let f = craft_media::isobmff::open(bytes).map_err(dec_err)?;
|
||||
let ti = f.track_of_kind(craft_media::isobmff::TrackKind::Video).ok_or(MediaError::Missing("video track"))?;
|
||||
let t = &f.tracks[ti];
|
||||
let entry = t.entries.first().ok_or(MediaError::Missing("video track"))?;
|
||||
let codec: Box<dyn Codec> = match &entry.codec {
|
||||
@@ -290,11 +290,11 @@ fn open_track(bytes: &[u8]) -> Result<(Demux, Track, Box<dyn Codec>)> {
|
||||
Ok((Demux::Mp4(Box::new(f), ti), track, codec))
|
||||
}
|
||||
Container::Matroska => {
|
||||
let f = deckcraft_matroska::open(bytes).map_err(dec_err)?;
|
||||
let f = craft_media::matroska::open(bytes).map_err(dec_err)?;
|
||||
let ti = f
|
||||
.tracks
|
||||
.iter()
|
||||
.position(|t| t.kind == deckcraft_matroska::TrackKind::Video && !t.samples.is_empty())
|
||||
.position(|t| t.kind == craft_media::matroska::TrackKind::Video && !t.samples.is_empty())
|
||||
.ok_or(MediaError::Missing("video track"))?;
|
||||
let t = &f.tracks[ti];
|
||||
let codec: Box<dyn Codec> = match &t.codec {
|
||||
@@ -303,7 +303,7 @@ fn open_track(bytes: &[u8]) -> Result<(Demux, Track, Box<dyn Codec>)> {
|
||||
MkvCodec::Vp9 { .. } => Box::new(Vp9 { dec: deckcraft_vp9::Decoder::new() }),
|
||||
MkvCodec::Av1 { av1c } => Box::new(Av1 {
|
||||
dec: deckcraft_av1::Decoder::new(),
|
||||
config_obus: deckcraft_isobmff::Av1Config::parse(av1c).map(|c| c.config_obus).unwrap_or_default(),
|
||||
config_obus: craft_media::isobmff::Av1Config::parse(av1c).map(|c| c.config_obus).unwrap_or_default(),
|
||||
primed: false,
|
||||
}),
|
||||
other => return Err(MediaError::Unsupported(format!("{} video", mkv_codec_name(other)))),
|
||||
|
||||
@@ -166,6 +166,52 @@ fn ogg_opus() {
|
||||
check_tone("opus", &fixture("tone.opus"), 48_000, 1);
|
||||
}
|
||||
|
||||
/// Ogg Opus timing saturates on a hostile granule position instead of failing, so the end-trim
|
||||
/// arithmetic on its total must not overflow either: a three-channel file that ends with a page
|
||||
/// carrying the largest granule still probes and decodes.
|
||||
#[test]
|
||||
fn ogg_opus_with_a_huge_granule_does_not_overflow() {
|
||||
fn seal(mut page: Vec<u8>) -> Vec<u8> {
|
||||
page[22..26].fill(0);
|
||||
let crc = craft_media::ogg::crc32(&page);
|
||||
page[22..26].copy_from_slice(&crc.to_le_bytes());
|
||||
page
|
||||
}
|
||||
let f = fixture("tone.opus");
|
||||
let mut pages: Vec<Vec<u8>> = Vec::new();
|
||||
let mut at = 0;
|
||||
while at + 27 <= f.len() && &f[at..at + 4] == b"OggS" {
|
||||
let nseg = f[at + 26] as usize;
|
||||
let end = at + 27 + nseg + f[at + 27..at + 27 + nseg].iter().map(|&b| b as usize).sum::<usize>();
|
||||
pages.push(f[at..end].to_vec());
|
||||
at = end;
|
||||
}
|
||||
assert_eq!(at, f.len(), "the fixture is whole pages");
|
||||
// an OpusHead for three channels (mapping family 1), each playing the one mono stream
|
||||
let head = pages[0][28..].to_vec();
|
||||
let mut first = pages[0][..26].to_vec();
|
||||
first.extend_from_slice(&[1, 24]);
|
||||
first.extend_from_slice(&head[..9]);
|
||||
first.push(3);
|
||||
first.extend_from_slice(&head[10..18]);
|
||||
first.extend_from_slice(&[1, 1, 0, 0, 0, 0]);
|
||||
pages[0] = seal(first);
|
||||
// the last page once more, as the next page of the stream, with the largest granule
|
||||
let last = pages.len() - 1;
|
||||
let mut extra = pages[last].clone();
|
||||
pages[last][5] &= !0x04;
|
||||
pages[last] = seal(pages[last].clone());
|
||||
extra[6..14].copy_from_slice(&i64::MAX.to_le_bytes());
|
||||
let seq = u32::from_le_bytes([extra[18], extra[19], extra[20], extra[21]]) + 1;
|
||||
extra[18..22].copy_from_slice(&seq.to_le_bytes());
|
||||
pages.push(seal(extra));
|
||||
let b: Bytes = Arc::new(pages.concat());
|
||||
assert!(probe(&b).is_ok_and(|i| i.duration_ms > 1 << 40), "the granule is taken at its word");
|
||||
let p = audio::decode(&b).expect("decode");
|
||||
assert_eq!(p.channels, 3);
|
||||
assert!(!p.samples.is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn wma_is_probed_but_not_decoded() {
|
||||
let b = fixture("tone.wma");
|
||||
|
||||
@@ -1,14 +0,0 @@
|
||||
[package]
|
||||
name = "deckcraft-metafile"
|
||||
version.workspace = true
|
||||
edition.workspace = true
|
||||
license.workspace = true
|
||||
description = "DeckCraft metafiles (ported from WordCraft): parses Windows WMF and EMF records into a neutral vector model (paths and bitmaps)"
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
||||
[dependencies]
|
||||
kurbo = { workspace = true }
|
||||
thiserror = { workspace = true }
|
||||
image = { workspace = true }
|
||||
@@ -1,59 +0,0 @@
|
||||
//! Bounds-checked little-endian access to hostile input. Every read returns `None` past the end, so
|
||||
//! a truncated or lying size field stops the parse instead of panicking.
|
||||
|
||||
#[derive(Clone, Copy)]
|
||||
pub(crate) struct Bytes<'a>(pub(crate) &'a [u8]);
|
||||
|
||||
impl<'a> Bytes<'a> {
|
||||
pub fn slice(&self, off: usize, len: usize) -> Option<&'a [u8]> {
|
||||
self.0.get(off..off.checked_add(len)?)
|
||||
}
|
||||
|
||||
pub fn tail(&self, off: usize) -> Option<Bytes<'a>> {
|
||||
self.0.get(off..).map(Bytes)
|
||||
}
|
||||
|
||||
fn arr<const N: usize>(&self, off: usize) -> Option<[u8; N]> {
|
||||
self.slice(off, N)?.try_into().ok()
|
||||
}
|
||||
|
||||
pub fn u16(&self, off: usize) -> Option<u16> {
|
||||
Some(u16::from_le_bytes(self.arr(off)?))
|
||||
}
|
||||
|
||||
pub fn i16(&self, off: usize) -> Option<i16> {
|
||||
Some(i16::from_le_bytes(self.arr(off)?))
|
||||
}
|
||||
|
||||
pub fn u32(&self, off: usize) -> Option<u32> {
|
||||
Some(u32::from_le_bytes(self.arr(off)?))
|
||||
}
|
||||
|
||||
pub fn i32(&self, off: usize) -> Option<i32> {
|
||||
Some(i32::from_le_bytes(self.arr(off)?))
|
||||
}
|
||||
|
||||
pub fn f32(&self, off: usize) -> Option<f32> {
|
||||
Some(f32::from_le_bytes(self.arr(off)?))
|
||||
}
|
||||
|
||||
/// `n` consecutive (x, y) pairs of 16- or 32-bit signed integers starting at `off`.
|
||||
pub fn points(&self, off: usize, n: usize, wide: bool) -> Option<Vec<(f64, f64)>> {
|
||||
let sz = if wide { 8 } else { 4 };
|
||||
let raw = self.slice(off, n.checked_mul(sz)?)?;
|
||||
let mut out = Vec::with_capacity(n);
|
||||
for c in raw.chunks_exact(sz) {
|
||||
let p = if wide {
|
||||
let x = i32::from_le_bytes(c.get(0..4)?.try_into().ok()?);
|
||||
let y = i32::from_le_bytes(c.get(4..8)?.try_into().ok()?);
|
||||
(f64::from(x), f64::from(y))
|
||||
} else {
|
||||
let x = i16::from_le_bytes(c.get(0..2)?.try_into().ok()?);
|
||||
let y = i16::from_le_bytes(c.get(2..4)?.try_into().ok()?);
|
||||
(f64::from(x), f64::from(y))
|
||||
};
|
||||
out.push(p);
|
||||
}
|
||||
Some(out)
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,193 +0,0 @@
|
||||
//! Clipping regions for EMF and WMF (MS-EMF 2.3.12, MS-WMF 2.3.5).
|
||||
//!
|
||||
//! A region is a set of axis-aligned rectangles in device units: `None` is the default clip (the
|
||||
//! whole surface), `Some([])` draws nothing, and `Some(rects)` is their union. That is how GDI
|
||||
//! stores a region (`RGNDATA` is a list of rectangles). Counts are capped so a hostile file cannot
|
||||
//! grow the set without bound; past the cap, extra rectangles are dropped, which can only hide
|
||||
//! drawing.
|
||||
|
||||
/// Rectangles kept from one region. Past this, further rectangles are dropped.
|
||||
pub(crate) const MAX_CLIP_RECTS: usize = 64;
|
||||
|
||||
pub(crate) const RGN_AND: u32 = 1;
|
||||
pub(crate) const RGN_OR: u32 = 2;
|
||||
pub(crate) const RGN_XOR: u32 = 3;
|
||||
pub(crate) const RGN_DIFF: u32 = 4;
|
||||
pub(crate) const RGN_COPY: u32 = 5;
|
||||
|
||||
/// A rectangle `[x0, y0, x1, y1]` with a positive area, or none.
|
||||
pub(crate) fn normalize(rect: [f64; 4]) -> Option<[f64; 4]> {
|
||||
let [l, t, r, b] = rect;
|
||||
if ![l, t, r, b].iter().all(|v| v.is_finite()) {
|
||||
return None;
|
||||
}
|
||||
let (x0, x1) = (l.min(r), l.max(r));
|
||||
let (y0, y1) = (t.min(b), t.max(b));
|
||||
(x1 - x0 > 1e-6 && y1 - y0 > 1e-6).then_some([x0, y0, x1, y1])
|
||||
}
|
||||
|
||||
fn intersection(a: [f64; 4], b: [f64; 4]) -> Option<[f64; 4]> {
|
||||
let (x0, y0) = (a[0].max(b[0]), a[1].max(b[1]));
|
||||
let (x1, y1) = (a[2].min(b[2]), a[3].min(b[3]));
|
||||
(x1 - x0 > 1e-6 && y1 - y0 > 1e-6 && [x0, y0, x1, y1].iter().all(|v| v.is_finite())).then_some([x0, y0, x1, y1])
|
||||
}
|
||||
|
||||
/// `a` with `b` removed, as up to four rectangles.
|
||||
fn subtract_one(a: [f64; 4], b: [f64; 4]) -> Vec<[f64; 4]> {
|
||||
let Some(i) = intersection(a, b) else { return vec![a] };
|
||||
let mut out = Vec::new();
|
||||
if i[1] > a[1]
|
||||
&& let Some(r) = normalize([a[0], a[1], a[2], i[1]])
|
||||
{
|
||||
out.push(r);
|
||||
}
|
||||
if i[3] < a[3]
|
||||
&& let Some(r) = normalize([a[0], i[3], a[2], a[3]])
|
||||
{
|
||||
out.push(r);
|
||||
}
|
||||
if i[0] > a[0]
|
||||
&& let Some(r) = normalize([a[0], i[1], i[0], i[3]])
|
||||
{
|
||||
out.push(r);
|
||||
}
|
||||
if i[2] < a[2]
|
||||
&& let Some(r) = normalize([i[2], i[1], a[2], i[3]])
|
||||
{
|
||||
out.push(r);
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
fn cap(mut rects: Vec<[f64; 4]>) -> Vec<[f64; 4]> {
|
||||
rects.truncate(MAX_CLIP_RECTS);
|
||||
rects
|
||||
}
|
||||
|
||||
fn subtract_all(subject: Vec<[f64; 4]>, cuts: &[[f64; 4]]) -> Vec<[f64; 4]> {
|
||||
let mut cur = subject;
|
||||
for cut in cuts {
|
||||
let mut next = Vec::new();
|
||||
for s in cur {
|
||||
for piece in subtract_one(s, *cut) {
|
||||
if next.len() >= MAX_CLIP_RECTS {
|
||||
return next;
|
||||
}
|
||||
next.push(piece);
|
||||
}
|
||||
}
|
||||
cur = next;
|
||||
}
|
||||
cur
|
||||
}
|
||||
|
||||
/// Intersects the current region with one rectangle.
|
||||
pub(crate) fn intersect(current: Option<Vec<[f64; 4]>>, rect: [f64; 4]) -> Option<Vec<[f64; 4]>> {
|
||||
let Some(rect) = normalize(rect) else { return Some(Vec::new()) };
|
||||
Some(match current {
|
||||
None => vec![rect],
|
||||
Some(rects) => cap(rects.iter().filter_map(|r| intersection(*r, rect)).collect()),
|
||||
})
|
||||
}
|
||||
|
||||
/// Removes one rectangle. `surface` is the device bounds, used when the clip is still the default
|
||||
/// (the whole surface) so the remainder can be named.
|
||||
pub(crate) fn exclude(current: Option<Vec<[f64; 4]>>, rect: [f64; 4], surface: Option<[f64; 4]>) -> Option<Vec<[f64; 4]>> {
|
||||
let Some(rect) = normalize(rect) else { return current };
|
||||
match current {
|
||||
Some(rects) => Some(subtract_all(rects, &[rect])),
|
||||
None => surface.and_then(normalize).map(|s| subtract_all(vec![s], &[rect])),
|
||||
}
|
||||
}
|
||||
|
||||
/// Combines `incoming` with the current region in a region mode (`RGN_*`). An unknown mode leaves
|
||||
/// the region as it was. `RGN_COPY` replaces it. An `RGN_OR` or `RGN_XOR` against the default clip
|
||||
/// stays the default clip: the whole surface already covers the incoming rectangles.
|
||||
pub(crate) fn combine(current: Option<Vec<[f64; 4]>>, incoming: Vec<[f64; 4]>, mode: u32, surface: Option<[f64; 4]>) -> Option<Vec<[f64; 4]>> {
|
||||
let incoming: Vec<[f64; 4]> = cap(incoming.iter().copied().filter_map(normalize).collect());
|
||||
match mode {
|
||||
RGN_COPY => Some(incoming),
|
||||
RGN_AND => match current {
|
||||
None => Some(incoming),
|
||||
Some(rects) => {
|
||||
let mut out = Vec::new();
|
||||
for a in &rects {
|
||||
for b in &incoming {
|
||||
if out.len() >= MAX_CLIP_RECTS {
|
||||
return Some(out);
|
||||
}
|
||||
if let Some(r) = intersection(*a, *b) {
|
||||
out.push(r);
|
||||
}
|
||||
}
|
||||
}
|
||||
Some(out)
|
||||
}
|
||||
},
|
||||
RGN_OR => match current {
|
||||
None => None,
|
||||
Some(mut rects) => {
|
||||
rects.extend(incoming);
|
||||
Some(cap(rects))
|
||||
}
|
||||
},
|
||||
RGN_DIFF => match current {
|
||||
Some(rects) => Some(subtract_all(rects, &incoming)),
|
||||
None => surface.and_then(normalize).map(|s| subtract_all(vec![s], &incoming)),
|
||||
},
|
||||
RGN_XOR => {
|
||||
let base = current?;
|
||||
let mut out = subtract_all(base.clone(), &incoming);
|
||||
let rest = subtract_all(incoming, &base);
|
||||
for r in rest {
|
||||
if out.len() >= MAX_CLIP_RECTS {
|
||||
break;
|
||||
}
|
||||
out.push(r);
|
||||
}
|
||||
Some(out)
|
||||
}
|
||||
_ => current,
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn intersect_shrinks_to_the_overlap() {
|
||||
let r = intersect(None, [0.0, 0.0, 100.0, 50.0]);
|
||||
let r = intersect(r, [40.0, 10.0, 80.0, 40.0]);
|
||||
assert_eq!(r, Some(vec![[40.0, 10.0, 80.0, 40.0]]));
|
||||
assert_eq!(intersect(r, [0.0, 0.0, 10.0, 10.0]), Some(vec![]));
|
||||
assert_eq!(intersect(Some(vec![[0.0, 0.0, 10.0, 20.0]]), [40.0, 0.0, 80.0, 20.0]), Some(vec![]));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn modes_copy_and_or_diff_and_xor() {
|
||||
let a = Some(vec![[0.0, 0.0, 100.0, 40.0]]);
|
||||
assert_eq!(combine(a.clone(), vec![[10.0, 10.0, 30.0, 20.0]], RGN_COPY, None), Some(vec![[10.0, 10.0, 30.0, 20.0]]));
|
||||
let and = combine(a.clone(), vec![[50.0, 0.0, 150.0, 40.0]], RGN_AND, None);
|
||||
assert_eq!(and, Some(vec![[50.0, 0.0, 100.0, 40.0]]));
|
||||
let or = combine(a.clone(), vec![[200.0, 0.0, 210.0, 10.0]], RGN_OR, None).unwrap();
|
||||
assert_eq!(or.len(), 2);
|
||||
let diff = combine(a.clone(), vec![[20.0, 0.0, 40.0, 40.0]], RGN_DIFF, None).unwrap();
|
||||
assert_eq!(diff, vec![[0.0, 0.0, 20.0, 40.0], [40.0, 0.0, 100.0, 40.0]]);
|
||||
let xor = combine(a, vec![[80.0, 0.0, 120.0, 40.0]], RGN_XOR, None).unwrap();
|
||||
assert!(xor.contains(&[0.0, 0.0, 80.0, 40.0]));
|
||||
assert!(xor.contains(&[100.0, 0.0, 120.0, 40.0]));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn excluding_from_the_default_clip_uses_the_surface() {
|
||||
let r = exclude(None, [10.0, 0.0, 20.0, 40.0], Some([0.0, 0.0, 40.0, 40.0])).unwrap();
|
||||
assert_eq!(r, vec![[0.0, 0.0, 10.0, 40.0], [20.0, 0.0, 40.0, 40.0]]);
|
||||
assert_eq!(exclude(None, [10.0, 0.0, 20.0, 40.0], None), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_zero_area_rectangle_clears_an_intersection() {
|
||||
assert_eq!(intersect(Some(vec![[0.0, 0.0, 10.0, 10.0]]), [5.0, 5.0, 5.0, 8.0]), Some(vec![]));
|
||||
}
|
||||
}
|
||||
@@ -1,339 +0,0 @@
|
||||
//! Clipping drawn output to a convex device polygon (EMF+ clip rectangles, and clip paths and
|
||||
//! regions that are convex; others are reduced to their bounding rectangle). Shapes wholly inside
|
||||
//! pass unchanged; shapes crossing the edge are flattened and cut: fills with Sutherland–Hodgman
|
||||
//! per subpath, outlines with Cyrus–Beck per segment. Bitmaps are cropped (and masked outside a
|
||||
//! non-rectangular polygon) and text gets the polygon's bounding rectangle.
|
||||
|
||||
use crate::canvas::Raw;
|
||||
use crate::dib::Rgba;
|
||||
use crate::geom::{Op, P};
|
||||
|
||||
/// Line segments per Bézier when a crossing shape is flattened.
|
||||
const CURVE_STEPS: usize = 16;
|
||||
|
||||
/// The bounding box `[l, t, r, b]` of the points in `ops` (control points included).
|
||||
pub(crate) fn ops_bbox(ops: &[Op]) -> Option<[f64; 4]> {
|
||||
let mut b: Option<[f64; 4]> = None;
|
||||
let mut add = |p: P| {
|
||||
let v = b.get_or_insert([p.0, p.1, p.0, p.1]);
|
||||
*v = [v[0].min(p.0), v[1].min(p.1), v[2].max(p.0), v[3].max(p.1)];
|
||||
};
|
||||
for op in ops {
|
||||
match *op {
|
||||
Op::M(p) | Op::L(p) => add(p),
|
||||
Op::C(a, c, d) => {
|
||||
add(a);
|
||||
add(c);
|
||||
add(d);
|
||||
}
|
||||
Op::Z => {}
|
||||
}
|
||||
}
|
||||
b
|
||||
}
|
||||
|
||||
pub(crate) fn intersect(a: [f64; 4], b: [f64; 4]) -> [f64; 4] {
|
||||
let r = [a[0].max(b[0]), a[1].max(b[1]), a[2].min(b[2]), a[3].min(b[3])];
|
||||
// An empty intersection stays empty (zero size) so it clips everything away.
|
||||
if r[2] < r[0] || r[3] < r[1] { [r[0], r[1], r[0], r[1]] } else { r }
|
||||
}
|
||||
|
||||
/// A rectangle `[l, t, r, b]` as a polygon.
|
||||
pub(crate) fn rect_poly([l, t, r, b]: [f64; 4]) -> Vec<P> {
|
||||
vec![(l, t), (r, t), (r, b), (l, b)]
|
||||
}
|
||||
|
||||
/// The bounding box of a point list.
|
||||
pub(crate) fn pts_bbox(pts: &[P]) -> Option<[f64; 4]> {
|
||||
let (&f, rest) = pts.split_first()?;
|
||||
Some(rest.iter().fold([f.0, f.1, f.0, f.1], |v, p| [v[0].min(p.0), v[1].min(p.1), v[2].max(p.0), v[3].max(p.1)]))
|
||||
}
|
||||
|
||||
fn cross(a: P, b: P, p: P) -> f64 {
|
||||
(b.0 - a.0) * (p.1 - a.1) - (b.1 - a.1) * (p.0 - a.0)
|
||||
}
|
||||
|
||||
/// A convex clip polygon with positive orientation, or None when `pts` is not convex (or
|
||||
/// degenerate); duplicate and collinear points are dropped.
|
||||
pub(crate) fn convex(pts: &[P]) -> Option<Vec<P>> {
|
||||
let mut v: Vec<P> = Vec::with_capacity(pts.len());
|
||||
for &p in pts {
|
||||
if !(p.0.is_finite() && p.1.is_finite()) {
|
||||
return None;
|
||||
}
|
||||
if v.last().is_none_or(|q: &P| (q.0 - p.0).abs() > 1e-9 || (q.1 - p.1).abs() > 1e-9) {
|
||||
v.push(p);
|
||||
}
|
||||
}
|
||||
while v.len() > 1 && v.first() == v.last() {
|
||||
v.pop();
|
||||
}
|
||||
let area: f64 = (0..v.len()).map(|i| cross((0.0, 0.0), v[i], v[(i + 1) % v.len()])).sum();
|
||||
if v.len() < 3 || area.abs() < 1e-12 {
|
||||
return None;
|
||||
}
|
||||
if area < 0.0 {
|
||||
v.reverse();
|
||||
}
|
||||
let n = v.len();
|
||||
let mut out = Vec::with_capacity(n);
|
||||
for i in 0..n {
|
||||
let c = cross(v[(i + n - 1) % n], v[i], v[(i + 1) % n]);
|
||||
if c < -1e-9 * (1.0 + area.abs()) {
|
||||
return None;
|
||||
}
|
||||
if c.abs() > 1e-12 {
|
||||
out.push(v[i]);
|
||||
}
|
||||
}
|
||||
(out.len() >= 3).then_some(out)
|
||||
}
|
||||
|
||||
fn inside_poly(clip: &[P], p: P) -> bool {
|
||||
(0..clip.len()).all(|i| cross(clip[i], clip[(i + 1) % clip.len()], p) >= -1e-9)
|
||||
}
|
||||
|
||||
/// Whether the clip polygon is an axis-aligned rectangle.
|
||||
fn is_rect(clip: &[P]) -> bool {
|
||||
pts_bbox(clip).is_some_and(|b| clip.len() == 4 && clip.iter().all(|p| (p.0 == b[0] || p.0 == b[2]) && (p.1 == b[1] || p.1 == b[3])))
|
||||
}
|
||||
|
||||
/// Sutherland–Hodgman: `poly` cut by convex `clip`.
|
||||
pub(crate) fn clip_polygon(poly: &[P], clip: &[P]) -> Vec<P> {
|
||||
if poly.len().saturating_mul(clip.len()) > 1_000_000 {
|
||||
return Vec::new();
|
||||
}
|
||||
let mut poly = poly.to_vec();
|
||||
for i in 0..clip.len() {
|
||||
if poly.len().saturating_mul(clip.len() - i) > 1_000_000 {
|
||||
return Vec::new();
|
||||
}
|
||||
let (a, b) = (clip[i], clip[(i + 1) % clip.len()]);
|
||||
let keep = |p: P| cross(a, b, p) >= 0.0;
|
||||
let cut = |p: P, q: P| {
|
||||
let (cp, cq) = (cross(a, b, p), cross(a, b, q));
|
||||
let t = cp / (cp - cq);
|
||||
(p.0 + (q.0 - p.0) * t, p.1 + (q.1 - p.1) * t)
|
||||
};
|
||||
let mut next = Vec::with_capacity(poly.len() + 4);
|
||||
for j in 0..poly.len() {
|
||||
let (p, q) = (poly[(j + poly.len() - 1) % poly.len()], poly[j]);
|
||||
match (keep(p), keep(q)) {
|
||||
(true, true) => next.push(q),
|
||||
(true, false) => next.push(cut(p, q)),
|
||||
(false, true) => {
|
||||
next.push(cut(p, q));
|
||||
next.push(q);
|
||||
}
|
||||
(false, false) => {}
|
||||
}
|
||||
}
|
||||
poly = next;
|
||||
if poly.is_empty() {
|
||||
break;
|
||||
}
|
||||
}
|
||||
poly
|
||||
}
|
||||
|
||||
fn disjoint(b: [f64; 4], c: [f64; 4]) -> bool {
|
||||
b[2] < c[0] || b[0] > c[2] || b[3] < c[1] || b[1] > c[3] || c[2] <= c[0] || c[3] <= c[1]
|
||||
}
|
||||
|
||||
fn bbox_inside(b: [f64; 4], clip: &[P]) -> bool {
|
||||
rect_poly(b).into_iter().all(|p| inside_poly(clip, p))
|
||||
}
|
||||
|
||||
/// Clips one drawn item to a convex device polygon; a shape that is both filled and stroked comes
|
||||
/// back as two items. An empty polygon clips everything away.
|
||||
pub(crate) fn clip_raw(raw: Raw, clip: &[P]) -> Vec<Raw> {
|
||||
let Some(cb) = pts_bbox(clip).filter(|_| clip.len() >= 3) else { return Vec::new() };
|
||||
match raw {
|
||||
Raw::Path { ops, fill, stroke, even_odd, region } => {
|
||||
let Some(b) = ops_bbox(&ops) else { return Vec::new() };
|
||||
if bbox_inside(b, clip) {
|
||||
return vec![Raw::Path { ops, fill, stroke, even_odd, region }];
|
||||
}
|
||||
if disjoint(b, cb) {
|
||||
return Vec::new();
|
||||
}
|
||||
let subs = flatten(&ops);
|
||||
let mut out = Vec::new();
|
||||
if fill.is_some() {
|
||||
let f = clip_fill(&subs, clip);
|
||||
if !f.is_empty() {
|
||||
out.push(Raw::Path { ops: f, fill, stroke: None, even_odd, region: region.clone() });
|
||||
}
|
||||
}
|
||||
if stroke.is_some() {
|
||||
let s = clip_stroke(&subs, clip);
|
||||
if !s.is_empty() {
|
||||
out.push(Raw::Path { ops: s, fill: None, stroke, even_odd, region });
|
||||
}
|
||||
}
|
||||
out
|
||||
}
|
||||
Raw::Pattern { ops, even_odd, tile, region } => {
|
||||
let Some(b) = ops_bbox(&ops) else { return Vec::new() };
|
||||
if bbox_inside(b, clip) {
|
||||
return vec![Raw::Pattern { ops, even_odd, tile, region }];
|
||||
}
|
||||
if disjoint(b, cb) {
|
||||
return Vec::new();
|
||||
}
|
||||
let f = clip_fill(&flatten(&ops), clip);
|
||||
if f.is_empty() { Vec::new() } else { vec![Raw::Pattern { ops: f, even_odd, tile, region }] }
|
||||
}
|
||||
Raw::Bitmap { rect, img, region } => crop(rect, img, clip).map(|(rect, img)| vec![Raw::Bitmap { rect, img, region }]).unwrap_or_default(),
|
||||
Raw::Text { text: mut t, region } => {
|
||||
// Text clips to a rectangle: the one around the polygon.
|
||||
let cur = t.clip.map_or(cb, |[a, b, d, e]| intersect(cb, [f64::from(a), f64::from(b), f64::from(d), f64::from(e)]));
|
||||
t.clip = Some(cur.map(|v| v as f32));
|
||||
vec![Raw::Text { text: t, region }]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Subpaths as point lists with their closed flag.
|
||||
pub(crate) fn flatten(ops: &[Op]) -> Vec<(Vec<P>, bool)> {
|
||||
let mut subs: Vec<(Vec<P>, bool)> = Vec::new();
|
||||
for op in ops {
|
||||
match *op {
|
||||
Op::M(p) => subs.push((vec![p], false)),
|
||||
Op::L(p) => match subs.last_mut() {
|
||||
Some(s) => s.0.push(p),
|
||||
None => subs.push((vec![p], false)),
|
||||
},
|
||||
Op::C(a, b, d) => {
|
||||
let Some(s) = subs.last_mut() else { continue };
|
||||
let Some(&p0) = s.0.last() else { continue };
|
||||
for i in 1..=CURVE_STEPS {
|
||||
let t = i as f64 / CURVE_STEPS as f64;
|
||||
let u = 1.0 - t;
|
||||
let k = [u * u * u, 3.0 * u * u * t, 3.0 * u * t * t, t * t * t];
|
||||
s.0.push((k[0] * p0.0 + k[1] * a.0 + k[2] * b.0 + k[3] * d.0, k[0] * p0.1 + k[1] * a.1 + k[2] * b.1 + k[3] * d.1));
|
||||
}
|
||||
}
|
||||
Op::Z => {
|
||||
if let Some(s) = subs.last_mut() {
|
||||
s.1 = true;
|
||||
// Drawing after a close starts from the subpath's first point.
|
||||
let first = s.0.first().copied();
|
||||
if let Some(f) = first {
|
||||
subs.push((vec![f], false));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
subs.retain(|s| s.0.len() > 1);
|
||||
subs
|
||||
}
|
||||
|
||||
fn clip_fill(subs: &[(Vec<P>, bool)], clip: &[P]) -> Vec<Op> {
|
||||
let mut ops = Vec::new();
|
||||
for (pts, _) in subs {
|
||||
let poly = clip_polygon(pts, clip);
|
||||
if poly.len() >= 3 {
|
||||
ops.push(Op::M(poly[0]));
|
||||
ops.extend(poly[1..].iter().map(|p| Op::L(*p)));
|
||||
ops.push(Op::Z);
|
||||
}
|
||||
}
|
||||
ops
|
||||
}
|
||||
|
||||
fn clip_stroke(subs: &[(Vec<P>, bool)], clip: &[P]) -> Vec<Op> {
|
||||
let mut ops = Vec::new();
|
||||
for (pts, closed) in subs {
|
||||
let mut pts = pts.clone();
|
||||
if *closed && let Some(&f) = pts.first() {
|
||||
pts.push(f);
|
||||
}
|
||||
let mut last: Option<P> = None;
|
||||
for w in pts.windows(2) {
|
||||
let Some((a, b)) = segment(w[0], w[1], clip) else {
|
||||
last = None;
|
||||
continue;
|
||||
};
|
||||
if last != Some(a) {
|
||||
ops.push(Op::M(a));
|
||||
}
|
||||
ops.push(Op::L(b));
|
||||
last = Some(b);
|
||||
}
|
||||
}
|
||||
ops
|
||||
}
|
||||
|
||||
/// Cyrus–Beck: the part of segment `a`–`b` inside convex `clip`.
|
||||
fn segment(a: P, b: P, clip: &[P]) -> Option<(P, P)> {
|
||||
let d = (b.0 - a.0, b.1 - a.1);
|
||||
let (mut t0, mut t1) = (0.0f64, 1.0f64);
|
||||
for i in 0..clip.len() {
|
||||
let (e0, e1) = (clip[i], clip[(i + 1) % clip.len()]);
|
||||
// Inside is cross(e0, e1, p) >= 0, linear in t along the segment.
|
||||
let f0 = cross(e0, e1, a);
|
||||
let df = (e1.0 - e0.0) * d.1 - (e1.1 - e0.1) * d.0;
|
||||
if df == 0.0 {
|
||||
if f0 < 0.0 {
|
||||
return None;
|
||||
}
|
||||
} else {
|
||||
let t = -f0 / df;
|
||||
if df > 0.0 {
|
||||
t0 = t0.max(t);
|
||||
} else {
|
||||
t1 = t1.min(t);
|
||||
}
|
||||
}
|
||||
}
|
||||
(t0 <= t1).then_some(((a.0 + d.0 * t0, a.1 + d.1 * t0), (a.0 + d.0 * t1, a.1 + d.1 * t1)))
|
||||
}
|
||||
|
||||
/// Crops a bitmap drawn into `rect` = `[x, y, w, h]` (device, positive extents) to the clip
|
||||
/// polygon's box; pixels outside a non-rectangular polygon turn transparent.
|
||||
fn crop(rect: [f64; 4], img: Rgba, clip: &[P]) -> Option<([f64; 4], Rgba)> {
|
||||
let [x, y, w, h] = rect;
|
||||
let b = [x, y, x + w, y + h];
|
||||
if bbox_inside(b, clip) {
|
||||
return Some((rect, img));
|
||||
}
|
||||
let c = pts_bbox(clip)?;
|
||||
if disjoint(b, c) || w <= 0.0 || h <= 0.0 || img.w == 0 || img.h == 0 {
|
||||
return None;
|
||||
}
|
||||
let i = intersect(b, c);
|
||||
let (kx, ky) = (img.w as f64 / w, img.h as f64 / h);
|
||||
let x0 = (((i[0] - x) * kx).floor().max(0.0) as usize).min(img.w);
|
||||
let x1 = (((i[2] - x) * kx).ceil().max(0.0) as usize).min(img.w);
|
||||
let y0 = (((i[1] - y) * ky).floor().max(0.0) as usize).min(img.h);
|
||||
let y1 = (((i[3] - y) * ky).ceil().max(0.0) as usize).min(img.h);
|
||||
if x1 <= x0 || y1 <= y0 {
|
||||
return None;
|
||||
}
|
||||
let mask = !is_rect(clip);
|
||||
let total_pixels = (x1 - x0).saturating_mul(y1 - y0);
|
||||
if mask && total_pixels.saturating_mul(clip.len()) > 5_000_000 {
|
||||
return None;
|
||||
}
|
||||
let mut px = Vec::with_capacity((x1 - x0) * (y1 - y0) * 4);
|
||||
for row in y0..y1 {
|
||||
// Rows are top-down unless the image says otherwise.
|
||||
let r = if img.bottom_up { img.h - 1 - row } else { row };
|
||||
let s = (r * img.w + x0) * 4;
|
||||
let start = px.len();
|
||||
px.extend_from_slice(img.px.get(s..s + (x1 - x0) * 4)?);
|
||||
if mask {
|
||||
let cy = y + (row as f64 + 0.5) / ky;
|
||||
for (k, p) in px[start..].as_chunks_mut::<4>().0.iter_mut().enumerate() {
|
||||
let cx = x + ((x0 + k) as f64 + 0.5) / kx;
|
||||
if !inside_poly(clip, (cx, cy)) {
|
||||
p[3] = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
let out = [x + x0 as f64 / kx, y + y0 as f64 / ky, (x1 - x0) as f64 / kx, (y1 - y0) as f64 / ky];
|
||||
Some((out, Rgba { w: x1 - x0, h: y1 - y0, bottom_up: false, px }))
|
||||
}
|
||||
@@ -1,259 +0,0 @@
|
||||
//! Device-independent bitmaps. BITMAPINFOHEADER DIBs at 1/4/8/16/24/32 bpp with BI_RGB or BI_BITFIELDS
|
||||
//! decode to RGBA8 rows, top-down. Compressed (RLE, JPEG, PNG) and V4/V5-only features are not decoded.
|
||||
|
||||
use crate::{MAX_PIXELS, bytes::Bytes};
|
||||
|
||||
const BI_RGB: u32 = 0;
|
||||
const BI_BITFIELDS: u32 = 3;
|
||||
/// Most palette entries a DIB can carry (the colour table never exceeds 2^8).
|
||||
const MAX_PALETTE: usize = 256;
|
||||
const ROP_SRCAND: u32 = 0x0088_00C6;
|
||||
const ROP_SRCPAINT: u32 = 0x00EE_0086;
|
||||
const ROP_SRCINVERT: u32 = 0x0066_0046;
|
||||
|
||||
/// A decoded picture. `bottom_up` records the DIB's native row order, which source rectangles use.
|
||||
pub(crate) struct Rgba {
|
||||
pub w: usize,
|
||||
pub h: usize,
|
||||
pub bottom_up: bool,
|
||||
pub px: Vec<u8>,
|
||||
}
|
||||
|
||||
/// How a decoded bitmap combines with the destination.
|
||||
#[derive(Clone, Copy)]
|
||||
pub(crate) enum Blend {
|
||||
/// A raster operation (BITBLT, STRETCHDIB and friends).
|
||||
Rop(u32),
|
||||
/// EMR_ALPHABLEND: a constant alpha (0..=255), times the per-pixel alpha when `per_pixel` is set
|
||||
/// (AC_SRC_ALPHA: the source colours are premultiplied by it).
|
||||
Alpha { constant: u8, per_pixel: bool },
|
||||
}
|
||||
|
||||
impl Blend {
|
||||
/// Whether the decoder must keep the 32-bit alpha byte of each pixel.
|
||||
pub fn per_pixel(self) -> bool {
|
||||
matches!(self, Blend::Alpha { per_pixel: true, .. })
|
||||
}
|
||||
}
|
||||
|
||||
struct Header {
|
||||
w: usize,
|
||||
h: usize,
|
||||
top_down: bool,
|
||||
bpp: usize,
|
||||
masks: [u32; 3],
|
||||
pal_off: usize,
|
||||
pal_n: usize,
|
||||
}
|
||||
|
||||
impl Header {
|
||||
fn read(info: Bytes) -> Option<Header> {
|
||||
let size = info.u32(0)? as usize;
|
||||
let w = info.i32(4)?;
|
||||
let h = info.i32(8)?;
|
||||
let bpp = info.u16(14)? as usize;
|
||||
let comp = info.u32(16)?;
|
||||
let clr_used = info.u32(32)? as usize;
|
||||
if size < 40 || w <= 0 || h == 0 || !matches!(bpp, 1 | 4 | 8 | 16 | 24 | 32) || !matches!(comp, BI_RGB | BI_BITFIELDS) {
|
||||
return None;
|
||||
}
|
||||
let masks = if comp == BI_BITFIELDS {
|
||||
[info.u32(40)?, info.u32(44)?, info.u32(48)?]
|
||||
} else if bpp == 16 {
|
||||
[0x7C00, 0x03E0, 0x001F]
|
||||
} else {
|
||||
[0xFF_0000, 0xFF00, 0xFF]
|
||||
};
|
||||
let masks_len = if comp == BI_BITFIELDS && size == 40 { 12 } else { 0 };
|
||||
let pal_off = size.checked_add(masks_len)?;
|
||||
// Indexed formats take 2^bpp entries when clrUsed is 0. Deeper formats carry only the
|
||||
// entries clrUsed names (WMF DIBs can have them), still capped.
|
||||
let pal_n = match (bpp <= 8, clr_used) {
|
||||
(true, 0) => 1usize << bpp,
|
||||
(true, n) => n.min(1usize << bpp),
|
||||
(false, n) => n.min(MAX_PALETTE),
|
||||
};
|
||||
Some(Header { w: w as usize, h: h.unsigned_abs() as usize, top_down: h < 0, bpp, masks, pal_off, pal_n })
|
||||
}
|
||||
}
|
||||
|
||||
/// Byte length of the info part (header, masks, palette) of a DIB, so the pixel bits follow it.
|
||||
pub(crate) fn info_len(dib: Bytes) -> Option<usize> {
|
||||
let h = Header::read(dib)?;
|
||||
h.pal_off.checked_add(h.pal_n.checked_mul(4)?)
|
||||
}
|
||||
|
||||
/// Decodes a DIB given as its info part (header plus masks and palette) and its pixel bits. The
|
||||
/// 32-bit alpha byte is kept only when `keep_alpha` is set (per-pixel alpha blends); otherwise pixels
|
||||
/// are opaque.
|
||||
pub(crate) fn decode(info: Bytes, bits: Bytes, keep_alpha: bool) -> Option<Rgba> {
|
||||
let hd = Header::read(info)?;
|
||||
let pixels = hd.w.checked_mul(hd.h)?;
|
||||
if pixels == 0 || pixels > MAX_PIXELS {
|
||||
return None;
|
||||
}
|
||||
let stride = hd.w.checked_mul(hd.bpp)?.checked_add(31)? / 32 * 4;
|
||||
let data = bits.slice(0, stride.checked_mul(hd.h)?)?;
|
||||
let pal = info.slice(hd.pal_off, hd.pal_n * 4)?;
|
||||
let mut px = vec![0u8; pixels * 4];
|
||||
for y in 0..hd.h {
|
||||
let src_row = if hd.top_down { y } else { hd.h - 1 - y };
|
||||
let row = src_row * stride;
|
||||
for x in 0..hd.w {
|
||||
let p = pixel(data, row, x, &hd, pal, keep_alpha)?;
|
||||
let o = (y * hd.w + x) * 4;
|
||||
px.get_mut(o..o + 4)?.copy_from_slice(&p);
|
||||
}
|
||||
}
|
||||
Some(Rgba { w: hd.w, h: hd.h, bottom_up: !hd.top_down, px })
|
||||
}
|
||||
|
||||
fn pixel(data: &[u8], row: usize, x: usize, hd: &Header, pal: &[u8], keep_alpha: bool) -> Option<[u8; 4]> {
|
||||
let [m_r, m_g, m_b] = hd.masks;
|
||||
let [r, g, b] = match hd.bpp {
|
||||
1 => {
|
||||
let b = *data.get(row + x / 8)?;
|
||||
palette(pal, usize::from((b >> (7 - x % 8)) & 1))
|
||||
}
|
||||
4 => {
|
||||
let b = *data.get(row + x / 2)?;
|
||||
palette(pal, usize::from(if x.is_multiple_of(2) { b >> 4 } else { b & 0x0F }))
|
||||
}
|
||||
8 => palette(pal, usize::from(*data.get(row + x)?)),
|
||||
24 => {
|
||||
let [b, g, r] = data.get(row + x * 3..row + x * 3 + 3)?.try_into().ok()?;
|
||||
[r, g, b]
|
||||
}
|
||||
16 => {
|
||||
let o = row + x * 2;
|
||||
let v = u32::from(u16::from_le_bytes(data.get(o..o + 2)?.try_into().ok()?));
|
||||
[field(v, m_r), field(v, m_g), field(v, m_b)]
|
||||
}
|
||||
_ => {
|
||||
let o = row + x * 4;
|
||||
let v = u32::from_le_bytes(data.get(o..o + 4)?.try_into().ok()?);
|
||||
let a = if keep_alpha { (v >> 24) as u8 } else { 255 };
|
||||
return Some([field(v, m_r), field(v, m_g), field(v, m_b), a]);
|
||||
}
|
||||
};
|
||||
Some([r, g, b, 255])
|
||||
}
|
||||
|
||||
/// Palette entries are BGRX quads; an index past the table reads as black rather than failing the image.
|
||||
fn palette(pal: &[u8], i: usize) -> [u8; 3] {
|
||||
pal.get(i * 4..i * 4 + 4).and_then(|c| <[u8; 4]>::try_from(c).ok()).map_or([0, 0, 0], |c| [c[2], c[1], c[0]])
|
||||
}
|
||||
|
||||
/// Extracts one channel from a masked pixel value and scales it to 8 bits.
|
||||
fn field(v: u32, mask: u32) -> u8 {
|
||||
if mask == 0 {
|
||||
return 0;
|
||||
}
|
||||
let shift = mask.trailing_zeros();
|
||||
let max = u64::from(mask >> shift);
|
||||
let val = u64::from((v & mask) >> shift);
|
||||
((val * 255 + max / 2) / max) as u8
|
||||
}
|
||||
|
||||
/// Clips a source rectangle `src` = `[x, y, w, h]` to the bitmap and returns the cropped pixels (top-down)
|
||||
/// with the part of the destination `dst` that they fill. `src` is in DIB pixel coordinates (`y` counted
|
||||
/// from the DIB's origin, so bottom-up images count from the bottom); a non-positive source size means the
|
||||
/// whole bitmap. Clipping shrinks the destination by the same ratio and shifts it by the clipped-off part,
|
||||
/// so the rest of the image keeps its place. A negative destination extent is kept (the caller mirrors).
|
||||
pub(crate) fn clip_blit(img: &Rgba, src: [i64; 4], dst: [f64; 4]) -> Option<(Rgba, [f64; 4])> {
|
||||
let (iw, ih) = (img.w as i64, img.h as i64);
|
||||
let [sx, sy, sw, sh] = if src[2] > 0 && src[3] > 0 { src } else { [0, 0, iw, ih] };
|
||||
// The request's top row in top-down order.
|
||||
let top = if img.bottom_up { ih.saturating_sub(sy).saturating_sub(sh) } else { sy };
|
||||
let (x0, x1) = (sx.max(0), sx.saturating_add(sw).min(iw));
|
||||
let (r0, r1) = (top.max(0), top.saturating_add(sh).min(ih));
|
||||
if x1 <= x0 || r1 <= r0 {
|
||||
return None;
|
||||
}
|
||||
let (kx, ky) = (dst[2] / sw as f64, dst[3] / sh as f64);
|
||||
let out = [dst[0] + (x0 - sx) as f64 * kx, dst[1] + (r0 - top) as f64 * ky, (x1 - x0) as f64 * kx, (r1 - r0) as f64 * ky];
|
||||
let (x0, x1, r0, r1) = (x0 as usize, x1 as usize, r0 as usize, r1 as usize);
|
||||
let (ow, oh) = (x1 - x0, r1 - r0);
|
||||
let mut px = Vec::with_capacity(ow * oh * 4);
|
||||
for row in r0..r1 {
|
||||
let s = (row * img.w + x0) * 4;
|
||||
px.extend_from_slice(img.px.get(s..s + ow * 4)?);
|
||||
}
|
||||
Some((Rgba { w: ow, h: oh, bottom_up: false, px }, out))
|
||||
}
|
||||
|
||||
/// Applies a blend to decoded pixels before they are drawn. The picture has no destination pixels, so
|
||||
/// raster operations are drawn as they look over white paper: SRCAND (destination AND source) leaves
|
||||
/// white source pixels out and draws the rest; SRCPAINT and SRCINVERT leave black ones out. Other
|
||||
/// operations draw the source as copied. Constant alpha scales every pixel's alpha, after a
|
||||
/// per-pixel (premultiplied) source is made straight.
|
||||
pub(crate) fn prepare(img: &mut Rgba, blend: Blend) {
|
||||
match blend {
|
||||
Blend::Rop(ROP_SRCAND) => clear_where(img, [255, 255, 255]),
|
||||
Blend::Rop(ROP_SRCPAINT | ROP_SRCINVERT) => clear_where(img, [0, 0, 0]),
|
||||
Blend::Rop(_) => {}
|
||||
Blend::Alpha { constant, per_pixel } => {
|
||||
for p in img.px.as_chunks_mut::<4>().0 {
|
||||
if per_pixel {
|
||||
unpremultiply(p);
|
||||
}
|
||||
p[3] = (u16::from(p[3]) * u16::from(constant) / 255) as u8;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Makes the pixels of colour `rgb` transparent: where a raster operation keeps the destination.
|
||||
fn clear_where(img: &mut Rgba, rgb: [u8; 3]) {
|
||||
for p in img.px.as_chunks_mut::<4>().0 {
|
||||
if p[..3] == rgb {
|
||||
p[3] = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// AC_SRC_ALPHA sources are premultiplied; the model holds straight RGBA, so divide the colour by the
|
||||
/// alpha (rounded, capped at 255). A transparent pixel keeps its colour bytes.
|
||||
fn unpremultiply(p: &mut [u8; 4]) {
|
||||
let a = u32::from(p[3]);
|
||||
if a == 0 || a == 255 {
|
||||
return;
|
||||
}
|
||||
for c in &mut p[..3] {
|
||||
*c = ((u32::from(*c) * 255 + a / 2) / a).min(255) as u8;
|
||||
}
|
||||
}
|
||||
|
||||
/// Mirrors a decoded image left-right and/or top-bottom. Rows come from the buffer as chunks, so a
|
||||
/// short buffer flips only the rows it has.
|
||||
pub(crate) fn flip(img: &mut Rgba, horizontal: bool, vertical: bool) {
|
||||
let (w, h) = (img.w, img.h);
|
||||
if w == 0 || h == 0 {
|
||||
return;
|
||||
}
|
||||
let row = w * 4;
|
||||
if horizontal {
|
||||
for line in img.px.chunks_exact_mut(row) {
|
||||
for x in 0..w / 2 {
|
||||
for c in 0..4 {
|
||||
line.swap(x * 4 + c, (w - 1 - x) * 4 + c);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if vertical {
|
||||
// Rows the buffer actually holds: a short buffer flips only those.
|
||||
let n = h.min(img.px.len() / row);
|
||||
for y in 0..n / 2 {
|
||||
swap_rows(&mut img.px, y * row, (n - 1 - y) * row, row);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Swaps the `len` bytes at `a` with those at `b` (`a < b`); does nothing when either is out of range.
|
||||
fn swap_rows(px: &mut [u8], a: usize, b: usize, len: usize) {
|
||||
let Some((head, tail)) = px.split_at_mut_checked(b) else { return };
|
||||
if let (Some(top), Some(bottom)) = (head.get_mut(a..a.saturating_add(len)), tail.get_mut(..len)) {
|
||||
top.swap_with_slice(bottom);
|
||||
}
|
||||
}
|
||||
@@ -1,478 +0,0 @@
|
||||
//! EMF reader (MS-EMF). Records are (type, size in bytes) followed by a payload; offsets inside a
|
||||
//! record (bitmap info and bits) are relative to the record start, so each handler reads the record slice.
|
||||
|
||||
use std::sync::Arc;
|
||||
|
||||
use crate::bytes::Bytes;
|
||||
use crate::canvas::{BS_HATCHED, BS_NULL, Brush, Canvas, LogFont, Obj, PS_NULL, PS_STYLE_MASK, Pen, Xform, colorref};
|
||||
use crate::dib::{self, Blend};
|
||||
use crate::geom::{self, ArcKind, P};
|
||||
use crate::text::{ETO_GLYPH_INDEX, ETO_PDY, Font, MAX_CHARS, decode_bytes, decode_utf16};
|
||||
use crate::{Error, Frame, MAX_PATH_OPS};
|
||||
|
||||
/// ExtTextOut options: no rectangle in the record (EMR_SMALLTEXTOUT, EmrText), and 8-bit
|
||||
/// characters (EMR_SMALLTEXTOUT).
|
||||
const ETO_NO_RECT: u32 = 0x0100;
|
||||
const ETO_SMALL_CHARS: u32 = 0x0200;
|
||||
|
||||
const EMF_SIGNATURE: u32 = 0x464D_4520;
|
||||
/// Hundredths of a millimetre to points.
|
||||
const HMM_TO_PT: f64 = 72.0 / 2540.0;
|
||||
/// SRCCOPY, the plain copy of a bitmap.
|
||||
const SRCCOPY: u32 = 0x00CC_0020;
|
||||
/// LogPenEx pen type bits; PS_COSMETIC pens are one device pixel wide.
|
||||
const PS_TYPE_MASK: u32 = 0x000F_0000;
|
||||
const PS_COSMETIC: u32 = 0;
|
||||
|
||||
pub(crate) fn is_emf(b: Bytes) -> bool {
|
||||
b.u32(0) == Some(1) && b.u32(40) == Some(EMF_SIGNATURE)
|
||||
}
|
||||
|
||||
/// The picture frame: `rclFrame` (0.01 mm) mapped to device units through `szlDevice` and
|
||||
/// `szlMillimeters`, sized in points. Without a usable frame, the device bounds stand in (1 unit = 1 point).
|
||||
pub(crate) fn parse(input: Bytes) -> Result<(Canvas, Option<Frame>), Error> {
|
||||
if !is_emf(input) {
|
||||
return Err(Error::BadHeader);
|
||||
}
|
||||
let hsize = input.u32(4).ok_or(Error::BadHeader)? as usize;
|
||||
let bounds = rect32(input, 8).ok_or(Error::BadHeader)?;
|
||||
let [l, t, r, b] = bounds;
|
||||
let frame = frame_from_header(input).or_else(|| (r > l && b > t).then_some(Frame { rect: [l, t, r - l, b - t], size: None }));
|
||||
let mut cv = Canvas::new();
|
||||
if r > l && b > t {
|
||||
cv.set_clip_surface([l, t, r, b]);
|
||||
}
|
||||
if let Some(px_mm) = device_px_per_mm(input) {
|
||||
cv.set_device_mm(px_mm);
|
||||
}
|
||||
let mut plus = crate::emfplus::Plus::new();
|
||||
plus.frame = frame.as_ref().map(|f| f.rect);
|
||||
let mut parsed = 0usize;
|
||||
let mut pos = hsize.max(88);
|
||||
while !cv.full() {
|
||||
let (Some(typ), Some(size)) = (input.u32(pos), pos.checked_add(4).and_then(|o| input.u32(o))) else { break };
|
||||
let size = size as usize;
|
||||
if size < 8 || !size.is_multiple_of(4) {
|
||||
break;
|
||||
}
|
||||
let Some(rec) = input.slice(pos, size) else { break };
|
||||
if typ == 14 {
|
||||
break;
|
||||
}
|
||||
if typ == 70 && Bytes(rec).u32(12) == Some(crate::emfplus::EMF_PLUS) {
|
||||
if let Some(data) = Bytes(rec).tail(16) {
|
||||
plus.comment(&mut cv, data);
|
||||
}
|
||||
} else {
|
||||
record(&mut cv, typ, Bytes(rec));
|
||||
}
|
||||
parsed += 1;
|
||||
let Some(next) = pos.checked_add(size) else { break };
|
||||
pos = next;
|
||||
}
|
||||
if parsed == 0 {
|
||||
return Err(Error::NoRecords);
|
||||
}
|
||||
Ok((cv, frame))
|
||||
}
|
||||
|
||||
/// Device pixels per millimetre on each axis, from `szlDevice` (72) and `szlMillimeters` (80).
|
||||
fn device_px_per_mm(input: Bytes) -> Option<(f64, f64)> {
|
||||
let (dw, dh) = (input.i32(72)?, input.i32(76)?);
|
||||
let (mw, mh) = (input.i32(80)?, input.i32(84)?);
|
||||
(dw > 0 && dh > 0 && mw > 0 && mh > 0).then(|| (f64::from(dw) / f64::from(mw), f64::from(dh) / f64::from(mh)))
|
||||
}
|
||||
|
||||
/// The frame from `rclFrame` (offset 24), `szlDevice` (72) and `szlMillimeters` (80) in the header.
|
||||
fn frame_from_header(input: Bytes) -> Option<Frame> {
|
||||
let [fl, ft, fr, fb] = rect32(input, 24)?;
|
||||
if fr <= fl || fb <= ft {
|
||||
return None;
|
||||
}
|
||||
// Device units per hundredth of a millimetre.
|
||||
let (px_mm_x, px_mm_y) = device_px_per_mm(input)?;
|
||||
let (sx, sy) = (px_mm_x / 100.0, px_mm_y / 100.0);
|
||||
let rect = [fl * sx, ft * sy, (fr - fl) * sx, (fb - ft) * sy];
|
||||
Some(Frame { rect, size: Some(((fr - fl) * HMM_TO_PT, (fb - ft) * HMM_TO_PT)) })
|
||||
}
|
||||
|
||||
fn record(cv: &mut Canvas, typ: u32, r: Bytes) {
|
||||
if cv.full() {
|
||||
return;
|
||||
}
|
||||
let _ = match typ {
|
||||
9 => xy32(r, 8).map(|p| cv.set_window_ext(p)),
|
||||
10 => xy32(r, 8).map(|p| cv.set_window_org(p)),
|
||||
11 => xy32(r, 8).map(|p| cv.set_viewport_ext(p)),
|
||||
12 => xy32(r, 8).map(|p| cv.set_viewport_org(p)),
|
||||
17 => r.u32(8).map(|m| cv.set_map_mode(m)),
|
||||
19 => r.u32(8).map(|m| cv.set_polyfill(m)),
|
||||
20 => r.u32(8).map(|m| cv.set_rop2(m)),
|
||||
33 => {
|
||||
cv.save_dc();
|
||||
Some(())
|
||||
}
|
||||
34 => r.i32(8).map(|n| cv.restore_dc(n)),
|
||||
35 => xform(r, 8).map(|x| cv.modify_world(x, 4)),
|
||||
36 => xform(r, 8).zip(r.u32(32)).map(|(x, m)| cv.modify_world(x, m)),
|
||||
37 => r.u32(8).map(|i| cv.select(i)),
|
||||
38 => create_pen(cv, r),
|
||||
95 => create_ext_pen(cv, r),
|
||||
39 => create_brush(cv, r),
|
||||
93 | 94 => create_dib_brush(cv, r, typ == 93),
|
||||
82 => create_font(cv, r),
|
||||
18 => r.u32(8).map(|m| cv.set_bk_mode(m)),
|
||||
22 => r.u32(8).map(|a| cv.set_text_align(a)),
|
||||
24 => r.u32(8).map(|c| cv.set_text_color(c)),
|
||||
25 => r.u32(8).map(|c| cv.set_bk_color(c)),
|
||||
83 | 84 => ext_text_out(cv, r, typ == 84),
|
||||
108 => small_text_out(cv, r),
|
||||
40 => r.u32(8).map(|i| cv.delete_object(i)),
|
||||
29 => rect32(r, 8).map(|rect| cv.exclude_clip_rect(rect)),
|
||||
30 => rect32(r, 8).map(|rect| cv.intersect_clip_rect(rect)),
|
||||
75 => ext_select_clip(cv, r),
|
||||
27 => xy32(r, 8).map(|p| cv.move_to(p)),
|
||||
54 => xy32(r, 8).map(|p| cv.line_to(p)),
|
||||
42 => rect32(r, 8).map(|[l, t, rr, b]| cv.shape(&geom::ellipse(l, t, rr, b), true, true)),
|
||||
43 => rect32(r, 8).map(|[l, t, rr, b]| cv.shape(&geom::rect(l, t, rr, b), true, true)),
|
||||
44 => {
|
||||
let (cw, ch) = (r.i32(24), r.i32(28));
|
||||
let (Some(cw), Some(ch)) = (cw, ch) else { return };
|
||||
rect32(r, 8).map(|[l, t, rr, b]| {
|
||||
cv.shape(&geom::round_rect(l, t, rr, b, f64::from(cw), f64::from(ch)), true, true);
|
||||
})
|
||||
}
|
||||
45..=47 => arc32(r).map(|(bounds, s, e)| {
|
||||
let kind = match typ {
|
||||
45 => ArcKind::Arc,
|
||||
46 => ArcKind::Chord,
|
||||
_ => ArcKind::Pie,
|
||||
};
|
||||
let [l, t, rr, b] = bounds;
|
||||
cv.shape(&geom::arc_shape(l, t, rr, b, s, e, kind), kind != ArcKind::Arc, true);
|
||||
}),
|
||||
4 | 87 => counted(r, typ == 4).map(|pts| cv.poly(&pts, false, false, true)),
|
||||
3 | 86 => counted(r, typ == 3).map(|pts| cv.poly(&pts, true, true, true)),
|
||||
6 | 89 => counted(r, typ == 6).map(|pts| cv.line_chain(&pts, false, true)),
|
||||
// POLYBEZIER starts its own figure at the first point and leaves the current point alone;
|
||||
// POLYBEZIERTO continues from the current point.
|
||||
2 | 85 => counted(r, typ == 2).and_then(|pts| {
|
||||
let from = *pts.first()?;
|
||||
cv.bezier(from, pts.get(1..)?);
|
||||
Some(())
|
||||
}),
|
||||
5 | 88 => counted(r, typ == 5).map(|pts| cv.bezier_to(&pts)),
|
||||
7 | 90 => poly_groups(r, typ == 7).map(|(counts, pts)| cv.shape(&geom::subpaths(&pts, &counts, false), false, true)),
|
||||
8 | 91 => poly_groups(r, typ == 8).map(|(counts, pts)| cv.shape(&geom::subpaths(&pts, &counts, true), true, true)),
|
||||
59 => {
|
||||
cv.begin_path();
|
||||
Some(())
|
||||
}
|
||||
60 => {
|
||||
cv.end_path();
|
||||
Some(())
|
||||
}
|
||||
61 => {
|
||||
cv.close_figure();
|
||||
Some(())
|
||||
}
|
||||
62 => {
|
||||
cv.paint_path(true, false);
|
||||
Some(())
|
||||
}
|
||||
63 => {
|
||||
cv.paint_path(true, true);
|
||||
Some(())
|
||||
}
|
||||
64 => {
|
||||
cv.paint_path(false, true);
|
||||
Some(())
|
||||
}
|
||||
76 | 77 => bitblt(cv, r, typ == 77),
|
||||
80 => set_dibits(cv, r),
|
||||
81 => stretch_dibits(cv, r),
|
||||
114 => alphablend(cv, r),
|
||||
_ => Some(()),
|
||||
};
|
||||
}
|
||||
|
||||
/// EMR_EXTSELECTCLIPRGN: `cbRgnData` bytes of `RGNDATA` (rectangles in logical units) and a region
|
||||
/// mode. No region data selects the default clip.
|
||||
fn ext_select_clip(cv: &mut Canvas, r: Bytes) -> Option<()> {
|
||||
let cb = r.u32(8)? as usize;
|
||||
let mode = r.u32(12)?;
|
||||
if cb == 0 {
|
||||
cv.reset_clip();
|
||||
return Some(());
|
||||
}
|
||||
let header = 16;
|
||||
if cb < 32 {
|
||||
return Some(());
|
||||
}
|
||||
// RDH_RECTANGLES is the only region type GDI writes.
|
||||
if r.u32(header + 4)? != 1 {
|
||||
return Some(());
|
||||
}
|
||||
let n = (r.u32(header + 8)? as usize).min(crate::clip::MAX_CLIP_RECTS);
|
||||
let end = header.checked_add(cb)?;
|
||||
let mut rects = Vec::new();
|
||||
for i in 0..n {
|
||||
let o = header + 32 + i * 16;
|
||||
if o + 16 > end {
|
||||
break;
|
||||
}
|
||||
rects.push(rect32(r, o)?);
|
||||
}
|
||||
cv.select_clip_rects(&rects, mode);
|
||||
Some(())
|
||||
}
|
||||
|
||||
/// Two little-endian i32 values at `off`, as a point.
|
||||
fn xy32(r: Bytes, off: usize) -> Option<P> {
|
||||
Some((f64::from(r.i32(off)?), f64::from(r.i32(off + 4)?)))
|
||||
}
|
||||
|
||||
/// RECTL (left, top, right, bottom) as i32 values widened to f64.
|
||||
fn rect32(r: Bytes, off: usize) -> Option<[f64; 4]> {
|
||||
Some([f64::from(r.i32(off)?), f64::from(r.i32(off + 4)?), f64::from(r.i32(off + 8)?), f64::from(r.i32(off + 12)?)])
|
||||
}
|
||||
|
||||
/// An i32 field as f64; 0 past the end.
|
||||
fn i32s(r: Bytes, off: usize) -> f64 {
|
||||
r.i32(off).map_or(0.0, f64::from)
|
||||
}
|
||||
|
||||
/// XFORM: six f32 values in M11, M12, M21, M22, Dx, Dy order.
|
||||
fn xform(r: Bytes, off: usize) -> Option<Xform> {
|
||||
let mut x = [0.0f64; 6];
|
||||
for (i, v) in x.iter_mut().enumerate() {
|
||||
*v = f64::from(r.f32(off + i * 4)?);
|
||||
}
|
||||
Some(x)
|
||||
}
|
||||
|
||||
/// ARC, CHORD, PIE: bounds at 8, start point at 24, end point at 32.
|
||||
fn arc32(r: Bytes) -> Option<([f64; 4], P, P)> {
|
||||
Some((rect32(r, 8)?, xy32(r, 24)?, xy32(r, 32)?))
|
||||
}
|
||||
|
||||
/// A point count from a record, rejected when it exceeds the path budget (before any allocation).
|
||||
fn budget(n: u32) -> Option<usize> {
|
||||
let n = n as usize;
|
||||
(n <= MAX_PATH_OPS).then_some(n)
|
||||
}
|
||||
|
||||
/// Point count at 24, then the points (32-bit or 16-bit) at 28.
|
||||
fn counted(r: Bytes, wide: bool) -> Option<Vec<P>> {
|
||||
let n = budget(r.u32(24)?)?;
|
||||
r.tail(28)?.points(0, n, wide)
|
||||
}
|
||||
|
||||
/// Subpath count at 24, total point count at 28, per-subpath counts at 32, then the points.
|
||||
fn poly_groups(r: Bytes, wide: bool) -> Option<(Vec<usize>, Vec<P>)> {
|
||||
let n = budget(r.u32(24)?)?;
|
||||
// The record must hold the n counts before anything is allocated for them.
|
||||
let raw = r.slice(32, n.checked_mul(4)?)?;
|
||||
let counts: Vec<usize> = raw.as_chunks::<4>().0.iter().map(|c| u32::from_le_bytes(*c) as usize).collect();
|
||||
let total = counts.iter().try_fold(0usize, |a, c| a.checked_add(*c))?;
|
||||
let total = budget(u32::try_from(total).ok()?)?;
|
||||
let pts = r.tail(n.checked_mul(4)?.checked_add(32)?)?.points(0, total, wide)?;
|
||||
Some((counts, pts))
|
||||
}
|
||||
|
||||
/// EMR_CREATEPEN: handle, LogPen (style, width x/y, colour).
|
||||
fn create_pen(cv: &mut Canvas, r: Bytes) -> Option<()> {
|
||||
let slot = r.u32(8)? as usize;
|
||||
let style = r.u32(12)?;
|
||||
let width = r.i32(16)?;
|
||||
let color = r.u32(24)?;
|
||||
cv.add_object(Some(slot), Obj::Pen(Pen { style, width: f64::from(width), color: colorref(color) }));
|
||||
Some(())
|
||||
}
|
||||
|
||||
/// EMR_EXTCREATEPEN: handle, bitmap offsets (unused), LogPenEx (style, width, brush style, colour).
|
||||
/// A cosmetic pen's width is in device pixels (always 1), so it is a hairline; a BS_NULL brush draws
|
||||
/// no line at all.
|
||||
fn create_ext_pen(cv: &mut Canvas, r: Bytes) -> Option<()> {
|
||||
let slot = r.u32(8)? as usize;
|
||||
let (style, width, brush, color) = (r.u32(28)?, r.u32(32)?, r.u32(36)?, r.u32(40)?);
|
||||
let width = if style & PS_TYPE_MASK == PS_COSMETIC { 0.0 } else { f64::from(width) };
|
||||
let style = if brush == BS_NULL { PS_NULL } else { style & PS_STYLE_MASK };
|
||||
cv.add_object(Some(slot), Obj::Pen(Pen { style, width, color: colorref(color) }));
|
||||
Some(())
|
||||
}
|
||||
|
||||
/// EMR_CREATEBRUSHINDIRECT: handle, LogBrush32 (style, colour, hatch).
|
||||
fn create_brush(cv: &mut Canvas, r: Bytes) -> Option<()> {
|
||||
let slot = r.u32(8)? as usize;
|
||||
let style = r.u32(12)?;
|
||||
let color = r.u32(16)?;
|
||||
let mut b = Brush::new(style, colorref(color));
|
||||
if style == BS_HATCHED {
|
||||
b.hatch = r.u32(20).unwrap_or(0);
|
||||
}
|
||||
cv.add_object(Some(slot), Obj::Brush(b));
|
||||
Some(())
|
||||
}
|
||||
|
||||
/// EMR_CREATEDIBPATTERNBRUSHPT (94) and EMR_CREATEMONOBRUSH (93): handle, colour usage, then the
|
||||
/// bitmap info and bits as offset/size pairs. A mono brush (or one whose palette holds indices,
|
||||
/// DIB_PAL_COLORS) takes the text and background colours. An undecodable bitmap leaves the slot
|
||||
/// empty.
|
||||
fn create_dib_brush(cv: &mut Canvas, r: Bytes, mono: bool) -> Option<()> {
|
||||
let slot = r.u32(8)? as usize;
|
||||
let usage = r.u32(12)?;
|
||||
let (bo, bl) = (r.u32(16)? as usize, r.u32(20)? as usize);
|
||||
let (po, pl) = (r.u32(24)? as usize, r.u32(28)? as usize);
|
||||
let img = dib::decode(Bytes(r.slice(bo, bl)?), Bytes(r.slice(po, pl)?), false)?;
|
||||
let b = Brush::pattern(img, mono || usage == 1)?;
|
||||
cv.add_object(Some(slot), Obj::Brush(b));
|
||||
Some(())
|
||||
}
|
||||
|
||||
/// EMR_EXTCREATEFONTINDIRECTW: handle, then a LogFont (MS-EMF 2.2.13): height, width, escapement,
|
||||
/// orientation, weight (i32 each), italic, underline, strike-out, charset and four more bytes, then
|
||||
/// the face name in 32 UTF-16 units (NUL-terminated).
|
||||
fn create_font(cv: &mut Canvas, r: Bytes) -> Option<()> {
|
||||
let slot = r.u32(8)? as usize;
|
||||
let height = r.i32(12)?;
|
||||
let width = r.i32(16).unwrap_or(0);
|
||||
let escapement = r.i32(20).unwrap_or(0);
|
||||
let weight = r.i32(28).unwrap_or(0);
|
||||
let byte = |o: usize| r.0.get(o).copied().unwrap_or(0);
|
||||
let units: Vec<u16> = (0..32).map_while(|i| r.u16(40 + 2 * i)).take_while(|u| *u != 0).collect();
|
||||
let (family, _) = decode_utf16(&units, &[]);
|
||||
let font = Font {
|
||||
family: family.trim().to_string(),
|
||||
height: height as f32,
|
||||
width: width as f32,
|
||||
weight: weight.clamp(0, 1000) as u16,
|
||||
italic: byte(32) != 0,
|
||||
underline: byte(33) != 0,
|
||||
strike: byte(34) != 0,
|
||||
charset: byte(35),
|
||||
};
|
||||
cv.add_object(Some(slot), Obj::Font(Arc::new(LogFont { font, escapement: f64::from(escapement) })));
|
||||
Some(())
|
||||
}
|
||||
|
||||
/// The dx array at `off` for `n` characters: i32 advances, or (dx, dy) pairs under ETO_PDY (only
|
||||
/// dx is used). Empty when absent or short.
|
||||
fn dx_array(r: Bytes, off: usize, n: usize, opts: u32) -> Vec<f32> {
|
||||
let step = if opts & ETO_PDY != 0 { 8 } else { 4 };
|
||||
if off == 0 {
|
||||
return Vec::new();
|
||||
}
|
||||
let dx: Vec<f32> = (0..n).map_while(|i| r.i32(off.checked_add(i.checked_mul(step)?)?)).map(|v| v as f32).collect();
|
||||
if dx.len() == n { dx } else { Vec::new() }
|
||||
}
|
||||
|
||||
/// EMR_EXTTEXTOUTA (83) and EMR_EXTTEXTOUTW (84): bounds, graphics mode and scales, then an EmrText
|
||||
/// at 36: reference point, character count, string offset, options, the rectangle (absent under
|
||||
/// ETO_NO_RECT) and the dx offset. Offsets count from the record start. Glyph-index strings name
|
||||
/// glyphs of a font the reader cannot match, so they are left out.
|
||||
fn ext_text_out(cv: &mut Canvas, r: Bytes, wide: bool) -> Option<()> {
|
||||
let at = xy32(r, 36)?;
|
||||
let n = (r.u32(44)? as usize).min(MAX_CHARS);
|
||||
let off = r.u32(48)? as usize;
|
||||
let opts = r.u32(52)?;
|
||||
let (rect, dx_off) = if opts & ETO_NO_RECT != 0 { (None, r.u32(56)?) } else { (rect32(r, 56), r.u32(72)?) };
|
||||
if opts & ETO_GLYPH_INDEX != 0 {
|
||||
return None;
|
||||
}
|
||||
let dx = dx_array(r, dx_off as usize, n, opts);
|
||||
let (text, dx) = if wide {
|
||||
let raw = r.slice(off, n.checked_mul(2)?)?;
|
||||
let units: Vec<u16> = raw.as_chunks::<2>().0.iter().map(|c| u16::from_le_bytes(*c)).collect();
|
||||
decode_utf16(&units, &dx)
|
||||
} else {
|
||||
decode_bytes(r.slice(off, n)?, cv.font_charset(), &dx)
|
||||
};
|
||||
cv.text(at, text, dx, opts, rect);
|
||||
Some(())
|
||||
}
|
||||
|
||||
/// EMR_SMALLTEXTOUT (MS-EMF 2.3.5.37): x, y, character count, options, graphics mode, scales, the
|
||||
/// bounds (absent under ETO_NO_RECT), then the characters: 8-bit under ETO_SMALL_CHARS, else UTF-16.
|
||||
fn small_text_out(cv: &mut Canvas, r: Bytes) -> Option<()> {
|
||||
let at = xy32(r, 8)?;
|
||||
let n = (r.u32(16)? as usize).min(MAX_CHARS);
|
||||
let opts = r.u32(20)?;
|
||||
if opts & ETO_GLYPH_INDEX != 0 {
|
||||
return None;
|
||||
}
|
||||
let (rect, off) = if opts & ETO_NO_RECT != 0 { (None, 36) } else { (rect32(r, 36), 52) };
|
||||
let (text, _) = if opts & ETO_SMALL_CHARS != 0 {
|
||||
decode_bytes(r.slice(off, n)?, cv.font_charset(), &[])
|
||||
} else {
|
||||
let raw = r.slice(off, n.checked_mul(2)?)?;
|
||||
let units: Vec<u16> = raw.as_chunks::<2>().0.iter().map(|c| u16::from_le_bytes(*c)).collect();
|
||||
decode_utf16(&units, &[])
|
||||
};
|
||||
cv.text(at, text, Vec::new(), opts, rect);
|
||||
Some(())
|
||||
}
|
||||
|
||||
/// BITBLT (76) and STRETCHBLT (77): dest at 24..40, rop at 40, source x/y at 44, bmi at 84, bits at
|
||||
/// 92; STRETCHBLT adds the source w/h at 100. A source-less rop fills the destination instead.
|
||||
fn bitblt(cv: &mut Canvas, r: Bytes, stretch: bool) -> Option<()> {
|
||||
let dst = [i32s(r, 24), i32s(r, 28), i32s(r, 32), i32s(r, 36)];
|
||||
let rop = r.u32(40)?;
|
||||
if Canvas::sourceless(rop) {
|
||||
cv.fill_rop(dst, rop);
|
||||
return Some(());
|
||||
}
|
||||
let (sw, sh) = if stretch { (i32s(r, 100), i32s(r, 104)) } else { (dst[2], dst[3]) };
|
||||
let src = [i32s(r, 44), i32s(r, 48), sw, sh];
|
||||
blit(cv, r, dst, src, (84, 92), Blend::Rop(rop))
|
||||
}
|
||||
|
||||
/// ALPHABLEND: laid out like STRETCHBLT, with a BLENDFUNCTION at 40. Constant alpha at 42 applies to
|
||||
/// every pixel; AlphaFormat at 43 with AC_SRC_ALPHA (1) also uses the per-pixel alpha.
|
||||
fn alphablend(cv: &mut Canvas, r: Bytes) -> Option<()> {
|
||||
let dst = [i32s(r, 24), i32s(r, 28), i32s(r, 32), i32s(r, 36)];
|
||||
let constant = *r.0.get(42)?;
|
||||
let per_pixel = (*r.0.get(43)? & 0x01) != 0;
|
||||
let src = [i32s(r, 44), i32s(r, 48), i32s(r, 100), i32s(r, 104)];
|
||||
blit(cv, r, dst, src, (84, 92), Blend::Alpha { constant, per_pixel })
|
||||
}
|
||||
|
||||
/// STRETCHDIBITS: dest x/y at 24, src x/y/w/h at 32..48, bmi offset/size at 48, bits at 56, rop at 68,
|
||||
/// dest w/h at 72.
|
||||
fn stretch_dibits(cv: &mut Canvas, r: Bytes) -> Option<()> {
|
||||
let dst = [i32s(r, 24), i32s(r, 28), i32s(r, 72), i32s(r, 76)];
|
||||
let src = [i32s(r, 32), i32s(r, 36), i32s(r, 40), i32s(r, 44)];
|
||||
let rop = r.u32(68)?;
|
||||
blit(cv, r, dst, src, (48, 56), Blend::Rop(rop))
|
||||
}
|
||||
|
||||
/// SETDIBITSTODEVICE (MS-EMF 2.3.1.5): dest x/y at 24, src x/y at 32, size (cxSrc, cySrc) at 40, bmi
|
||||
/// at 48, bits at 56, start scan at 68. The image keeps its own size; the bits begin at scan line
|
||||
/// `iStartScan`, so the source y counts from there.
|
||||
fn set_dibits(cv: &mut Canvas, r: Bytes) -> Option<()> {
|
||||
let (cx, cy) = (i32s(r, 40), i32s(r, 44));
|
||||
if cx <= 0.0 || cy <= 0.0 {
|
||||
return None;
|
||||
}
|
||||
let dst = [i32s(r, 24), i32s(r, 28), cx, cy];
|
||||
let src = [i32s(r, 32), i32s(r, 36) - f64::from(r.u32(68)?), cx, cy];
|
||||
blit(cv, r, dst, src, (48, 56), Blend::Rop(SRCCOPY))
|
||||
}
|
||||
|
||||
/// Decodes the bitmap at `at` = (offset of the bmi offset/length pair, offset of the bits pair), both
|
||||
/// inside the record, and stretches the source rectangle `src` into `dst`. Both are `[x, y, w, h]`; a
|
||||
/// non-positive source size means the whole bitmap. `blend` decides how it combines.
|
||||
fn blit(cv: &mut Canvas, r: Bytes, dst: [f64; 4], src: [f64; 4], at: (usize, usize), blend: Blend) -> Option<()> {
|
||||
let (bo, bl) = (r.u32(at.0)? as usize, r.u32(at.0 + 4)? as usize);
|
||||
let (po, pl) = (r.u32(at.1)? as usize, r.u32(at.1 + 4)? as usize);
|
||||
if pl == 0 {
|
||||
return None;
|
||||
}
|
||||
let info = Bytes(r.slice(bo, bl)?);
|
||||
let pixels = Bytes(r.slice(po, pl)?);
|
||||
let mut img = dib::decode(info, pixels, blend.per_pixel())?;
|
||||
dib::prepare(&mut img, blend);
|
||||
let (img, dst) = dib::clip_blit(&img, src.map(|v| v as i64), dst)?;
|
||||
cv.bitmap(dst, img);
|
||||
Some(())
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,118 +0,0 @@
|
||||
//! Path operations in `f64` and the shape builders that turn WMF/EMF rectangles, ellipses and arcs
|
||||
//! into cubic Béziers (the classic 4/3·tan(Δ/4) approximation, at most 90° per piece).
|
||||
|
||||
use std::f64::consts::{FRAC_PI_2, PI, TAU};
|
||||
|
||||
pub(crate) type P = (f64, f64);
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq)]
|
||||
pub(crate) enum Op {
|
||||
M(P),
|
||||
L(P),
|
||||
C(P, P, P),
|
||||
Z,
|
||||
}
|
||||
|
||||
pub(crate) fn rect(l: f64, t: f64, r: f64, b: f64) -> Vec<Op> {
|
||||
vec![Op::M((l, t)), Op::L((r, t)), Op::L((r, b)), Op::L((l, b)), Op::Z]
|
||||
}
|
||||
|
||||
/// Corner radii are half the EMF/WMF corner ellipse size, clamped so the corners never overlap.
|
||||
pub(crate) fn round_rect(l: f64, t: f64, r: f64, b: f64, ew: f64, eh: f64) -> Vec<Op> {
|
||||
// Reversed corners (right < left, bottom < top) describe the same rectangle.
|
||||
let (l, r) = (l.min(r), l.max(r));
|
||||
let (t, b) = (t.min(b), t.max(b));
|
||||
let rx = (ew / 2.0).abs().min((r - l) / 2.0);
|
||||
let ry = (eh / 2.0).abs().min((b - t) / 2.0);
|
||||
let mut ops = vec![Op::M((l + rx, t)), Op::L((r - rx, t))];
|
||||
ops.extend(arc(r - rx, t + ry, rx, ry, -FRAC_PI_2, FRAC_PI_2));
|
||||
ops.push(Op::L((r, b - ry)));
|
||||
ops.extend(arc(r - rx, b - ry, rx, ry, 0.0, FRAC_PI_2));
|
||||
ops.push(Op::L((l + rx, b)));
|
||||
ops.extend(arc(l + rx, b - ry, rx, ry, FRAC_PI_2, FRAC_PI_2));
|
||||
ops.push(Op::L((l, t + ry)));
|
||||
ops.extend(arc(l + rx, t + ry, rx, ry, PI, FRAC_PI_2));
|
||||
ops.push(Op::Z);
|
||||
ops
|
||||
}
|
||||
|
||||
pub(crate) fn ellipse(l: f64, t: f64, r: f64, b: f64) -> Vec<Op> {
|
||||
let (cx, cy, rx, ry) = center(l, t, r, b);
|
||||
let mut ops = vec![Op::M((cx + rx, cy))];
|
||||
ops.extend(arc(cx, cy, rx, ry, 0.0, TAU));
|
||||
ops.push(Op::Z);
|
||||
ops
|
||||
}
|
||||
|
||||
/// Which closed or open shape an ARC/CHORD/PIE record draws.
|
||||
#[derive(Clone, Copy, PartialEq, Eq)]
|
||||
pub(crate) enum ArcKind {
|
||||
Arc,
|
||||
Chord,
|
||||
Pie,
|
||||
}
|
||||
|
||||
/// ARC, CHORD and PIE: the arc runs counter-clockwise on screen from the start point to the end point.
|
||||
pub(crate) fn arc_shape(l: f64, t: f64, r: f64, b: f64, s: P, e: P, kind: ArcKind) -> Vec<Op> {
|
||||
let (cx, cy, rx, ry) = center(l, t, r, b);
|
||||
let ang = |p: P| ((p.1 - cy) / ry.max(f64::MIN_POSITIVE)).atan2((p.0 - cx) / rx.max(f64::MIN_POSITIVE));
|
||||
let (a0, a1) = (ang(s), ang(e));
|
||||
let mut d = (a0 - a1).rem_euclid(TAU);
|
||||
if d < 1e-12 {
|
||||
d = TAU;
|
||||
}
|
||||
let sweep = -d;
|
||||
let start = (cx + rx * a0.cos(), cy + ry * a0.sin());
|
||||
let mut ops = if kind == ArcKind::Pie { vec![Op::M((cx, cy)), Op::L(start)] } else { vec![Op::M(start)] };
|
||||
ops.extend(arc(cx, cy, rx, ry, a0, sweep));
|
||||
match kind {
|
||||
ArcKind::Arc => {}
|
||||
ArcKind::Chord => ops.push(Op::Z),
|
||||
ArcKind::Pie => ops.extend([Op::L((cx, cy)), Op::Z]),
|
||||
}
|
||||
ops
|
||||
}
|
||||
|
||||
fn center(l: f64, t: f64, r: f64, b: f64) -> (f64, f64, f64, f64) {
|
||||
((l + r) / 2.0, (t + b) / 2.0, (r - l).abs() / 2.0, (b - t).abs() / 2.0)
|
||||
}
|
||||
|
||||
/// Cubic pieces along an ellipse from parameter `a0` over signed `sweep` radians. The current point
|
||||
/// must already be at `(cx + rx cos a0, cy + ry sin a0)`.
|
||||
fn arc(cx: f64, cy: f64, rx: f64, ry: f64, a0: f64, sweep: f64) -> Vec<Op> {
|
||||
let n = ((sweep.abs() / FRAC_PI_2).ceil() as usize).clamp(1, 8);
|
||||
let step = sweep / n as f64;
|
||||
let k = 4.0 / 3.0 * (step / 4.0).tan();
|
||||
let pt = |t: f64| (cx + rx * t.cos(), cy + ry * t.sin());
|
||||
let mut ops = Vec::with_capacity(n);
|
||||
for i in 0..n {
|
||||
let t0 = a0 + step * i as f64;
|
||||
let t1 = t0 + step;
|
||||
let (p0, p3) = (pt(t0), pt(t1));
|
||||
let c1 = (p0.0 - k * rx * t0.sin(), p0.1 + k * ry * t0.cos());
|
||||
let c2 = (p3.0 + k * rx * t1.sin(), p3.1 - k * ry * t1.cos());
|
||||
ops.push(Op::C(c1, c2, p3));
|
||||
}
|
||||
ops
|
||||
}
|
||||
|
||||
/// Consecutive subpaths: `counts[i]` points each, taken in order from `pts`. Counts that run past the
|
||||
/// point list are cut short rather than failing the whole record.
|
||||
pub(crate) fn subpaths(pts: &[P], counts: &[usize], close: bool) -> Vec<Op> {
|
||||
let mut ops = Vec::with_capacity(pts.len() + counts.len());
|
||||
let mut at = 0usize;
|
||||
for &c in counts {
|
||||
let end = at.saturating_add(c).min(pts.len());
|
||||
let Some((first, rest)) = pts.get(at..end).and_then(|s| s.split_first()) else {
|
||||
at = end;
|
||||
continue;
|
||||
};
|
||||
ops.push(Op::M(*first));
|
||||
ops.extend(rest.iter().map(|p| Op::L(*p)));
|
||||
if close {
|
||||
ops.push(Op::Z);
|
||||
}
|
||||
at = end;
|
||||
}
|
||||
ops
|
||||
}
|
||||
@@ -1,306 +0,0 @@
|
||||
//! DeckCraft metafiles (ported from WordCraft's `crates/metafile`): reads Windows Metafiles (WMF)
|
||||
//! and Enhanced Metafiles (EMF) into a neutral vector model of filled and stroked paths, pattern
|
||||
//! fills, RGBA bitmaps and text strings, in a picture frame with y down.
|
||||
//!
|
||||
//! Text stays font-agnostic: a [`Text`] item names the font the file asks for and carries the
|
||||
//! string, its advances and alignment; the renderer resolves the font and draws the glyphs.
|
||||
//!
|
||||
//! The reader is pure Rust over byte slices (no file system), so it builds for wasm. Every read is
|
||||
//! bounds-checked and every count is capped; hostile input yields a shorter picture or an [`Error`],
|
||||
//! never a panic.
|
||||
//!
|
||||
//! Records follow MS-WMF and MS-EMF. Rectangle clip regions (`INTERSECTCLIPRECT`,
|
||||
//! `EXCLUDECLIPRECT`, `EXTSELECTCLIPRGN`) limit what later records draw. Records outside the
|
||||
//! supported set are skipped silently.
|
||||
#![deny(clippy::unwrap_used, clippy::expect_used, clippy::panic, clippy::unimplemented, clippy::todo, clippy::unreachable)]
|
||||
#![forbid(unsafe_code)]
|
||||
|
||||
mod bytes;
|
||||
mod canvas;
|
||||
mod clip;
|
||||
mod cliprect;
|
||||
mod dib;
|
||||
mod emf;
|
||||
mod emfplus;
|
||||
mod geom;
|
||||
mod place;
|
||||
mod text;
|
||||
mod wmf;
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests;
|
||||
|
||||
use std::collections::HashMap;
|
||||
use std::sync::Arc;
|
||||
|
||||
use bytes::Bytes;
|
||||
use canvas::Raw;
|
||||
use geom::{Op, P};
|
||||
pub use place::{MAX_BITMAP_SIDE, PlacedItem};
|
||||
pub use text::{Font, HAlign, SYMBOL_CHARSET, Text, VAlign};
|
||||
|
||||
/// Items kept from one file. Past the cap the parse stops and keeps what it has.
|
||||
pub(crate) const MAX_ITEMS: usize = 200_000;
|
||||
/// Object table slots (WMF handles, EMF object indices).
|
||||
pub(crate) const MAX_OBJECTS: usize = 4096;
|
||||
/// Largest bitmap in pixels (about 40 megapixels, 160 MB of RGBA).
|
||||
pub(crate) const MAX_PIXELS: usize = 40_000_000;
|
||||
/// DC save-stack depth.
|
||||
pub(crate) const MAX_STACK: usize = 1024;
|
||||
/// Points in one open path bracket.
|
||||
pub(crate) const MAX_PATH_OPS: usize = 2_000_000;
|
||||
/// Bytes of path ops and pixels kept from one file (about 256 MB).
|
||||
pub(crate) const MAX_BYTES: usize = 256 * 1024 * 1024;
|
||||
|
||||
/// A parsed metafile. Items use coordinates in `0..width` and `0..height`, y down.
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub struct Picture {
|
||||
/// Size of the drawing in its own units: points for placeable WMF and EMF (from the frame), device
|
||||
/// or logical units otherwise.
|
||||
pub width: f32,
|
||||
pub height: f32,
|
||||
pub items: Vec<Item>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub enum Item {
|
||||
/// A filled and/or stroked path, in picture coordinates.
|
||||
Path {
|
||||
segs: Vec<Seg>,
|
||||
fill: Option<[u8; 4]>,
|
||||
/// Colour and width in picture units.
|
||||
stroke: Option<([u8; 4], f32)>,
|
||||
even_odd: bool,
|
||||
/// Clip region in picture units, each rectangle `[x0, y0, x1, y1]`. None is the default clip.
|
||||
region: Option<Vec<[f32; 4]>>,
|
||||
},
|
||||
/// A bitmap stretched into `rect` = `[x, y, w, h]` (picture units), RGBA8 rows top-down.
|
||||
Bitmap {
|
||||
rect: [f32; 4],
|
||||
width: u32,
|
||||
height: u32,
|
||||
rgba: Vec<u8>,
|
||||
/// Clip region in picture units. None is the default clip.
|
||||
region: Option<Vec<[f32; 4]>>,
|
||||
},
|
||||
/// A path filled with a repeating tile (pattern and hatch brushes). Items filled with the same
|
||||
/// brush share its tile.
|
||||
Pattern {
|
||||
segs: Vec<Seg>,
|
||||
even_odd: bool,
|
||||
tile: Arc<Tile>,
|
||||
/// Clip region in picture units. None is the default clip.
|
||||
region: Option<Vec<[f32; 4]>>,
|
||||
},
|
||||
/// A text string.
|
||||
Text(Box<Text>),
|
||||
}
|
||||
|
||||
/// A brush tile: RGBA8 rows top-down (transparent where the brush leaves the background), each
|
||||
/// pixel `cell` picture units, with a tile's top-left corner at `origin`.
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub struct Tile {
|
||||
pub width: u32,
|
||||
pub height: u32,
|
||||
pub rgba: Vec<u8>,
|
||||
pub cell: [f32; 2],
|
||||
pub origin: [f32; 2],
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq)]
|
||||
pub enum Seg {
|
||||
Move(f32, f32),
|
||||
Line(f32, f32),
|
||||
Cubic(f32, f32, f32, f32, f32, f32),
|
||||
Close,
|
||||
}
|
||||
|
||||
#[derive(Debug, thiserror::Error, Clone, PartialEq, Eq)]
|
||||
pub enum Error {
|
||||
#[error("not a WMF or EMF file")]
|
||||
NotMetafile,
|
||||
#[error("metafile header is truncated or invalid")]
|
||||
BadHeader,
|
||||
#[error("metafile has no readable records")]
|
||||
NoRecords,
|
||||
}
|
||||
|
||||
/// The picture frame in device units (`rect` = `[x, y, w, h]`) and, when the file states one, its
|
||||
/// size in picture units.
|
||||
pub(crate) struct Frame {
|
||||
pub rect: [f64; 4],
|
||||
pub size: Option<(f64, f64)>,
|
||||
}
|
||||
|
||||
/// Parses a WMF (placeable or plain) or EMF file.
|
||||
pub fn parse(bytes: &[u8]) -> Result<Picture, Error> {
|
||||
let b = Bytes(bytes);
|
||||
let (cv, frame) = if emf::is_emf(b) {
|
||||
emf::parse(b)?
|
||||
} else if wmf::is_wmf(b) {
|
||||
// A WMF converted from an EMF can carry the original in MFCOMMENT escapes; PowerPoint draws
|
||||
// the WMF's own records (its dx rounding shows), so the copy is not used.
|
||||
wmf::parse(b)?
|
||||
} else {
|
||||
return Err(Error::NotMetafile);
|
||||
};
|
||||
Ok(build(cv.items, frame))
|
||||
}
|
||||
|
||||
/// True when the bytes start like a WMF (placeable or plain header) or an EMF.
|
||||
pub fn is_metafile(bytes: &[u8]) -> bool {
|
||||
let b = Bytes(bytes);
|
||||
emf::is_emf(b) || wmf::is_wmf(b)
|
||||
}
|
||||
|
||||
/// Maps device-space output into the picture frame. Without a usable frame the frame is the bounding
|
||||
/// box of the drawing, so the origin is still the top-left of what was drawn.
|
||||
fn build(raw: Vec<Raw>, frame: Option<Frame>) -> Picture {
|
||||
let frame = frame.filter(|f| f.rect.iter().all(|v| v.is_finite()) && f.rect[2] > 0.0 && f.rect[3] > 0.0);
|
||||
let (rect, size) = match frame {
|
||||
Some(f) => (f.rect, f.size.unwrap_or((f.rect[2], f.rect[3]))),
|
||||
None => match bbox(&raw) {
|
||||
Some([x0, y0, x1, y1]) => ([x0, y0, x1 - x0, y1 - y0], (x1 - x0, y1 - y0)),
|
||||
None => return Picture { width: 0.0, height: 0.0, items: Vec::new() },
|
||||
},
|
||||
};
|
||||
let sx = if rect[2] > 0.0 { size.0 / rect[2] } else { 1.0 };
|
||||
let sy = if rect[3] > 0.0 { size.1 / rect[3] } else { 1.0 };
|
||||
let tf = move |p: P| ((p.0 - rect[0]) * sx, (p.1 - rect[1]) * sy);
|
||||
let stroke_scale = (sx + sy) / 2.0;
|
||||
// No stroke is wider than the picture's diagonal: anything wider is a mapping artefact.
|
||||
let diag = size.0.hypot(size.1);
|
||||
// Tiles shared by several fills stay shared.
|
||||
let mut tiles: HashMap<*const canvas::RawTile, (Arc<canvas::RawTile>, Arc<Tile>)> = HashMap::new();
|
||||
let items = raw.into_iter().filter_map(|r| match r {
|
||||
Raw::Path { ops, fill, stroke, even_odd, region } => {
|
||||
let segs = ops.iter().map(|op| seg(op, tf)).collect::<Option<Vec<Seg>>>()?;
|
||||
if segs.len() < 2 {
|
||||
return None;
|
||||
}
|
||||
let stroke = stroke.and_then(|(c, w)| Some((c, f((w * stroke_scale).min(diag))?)));
|
||||
Some(Item::Path { segs, fill, stroke, even_odd, region: map_region(region, &tf)? })
|
||||
}
|
||||
Raw::Bitmap { rect: r, img, region } => {
|
||||
let (x, y) = tf((r[0], r[1]));
|
||||
let rect = [f(x)?, f(y)?, f(r[2] * sx)?, f(r[3] * sy)?];
|
||||
Some(Item::Bitmap { rect, width: img.w as u32, height: img.h as u32, rgba: img.px, region: map_region(region, &tf)? })
|
||||
}
|
||||
Raw::Pattern { ops, even_odd, tile, region } => {
|
||||
let segs = ops.iter().map(|op| seg(op, tf)).collect::<Option<Vec<Seg>>>()?;
|
||||
if segs.len() < 2 {
|
||||
return None;
|
||||
}
|
||||
let key = Arc::as_ptr(&tile);
|
||||
let tile = match tiles.get(&key) {
|
||||
Some((_, t)) => t.clone(),
|
||||
None => {
|
||||
let (ox, oy) = tf((0.0, 0.0));
|
||||
let t = Arc::new(Tile {
|
||||
width: u32::try_from(tile.w).ok()?,
|
||||
height: u32::try_from(tile.h).ok()?,
|
||||
rgba: tile.px.clone(),
|
||||
cell: [f(tile.cell * sx)?, f(tile.cell * sy)?],
|
||||
origin: [f(ox)?, f(oy)?],
|
||||
});
|
||||
// The raw tile is kept with its key, so the address is never reused by another.
|
||||
tiles.insert(key, (tile.clone(), t.clone()));
|
||||
t
|
||||
}
|
||||
};
|
||||
Some(Item::Pattern { segs, even_odd, tile, region: map_region(region, &tf)? })
|
||||
}
|
||||
Raw::Text { mut text, region } => {
|
||||
// Device to picture: scale by (sx, sy) after moving the frame's corner to the origin.
|
||||
let [a, b, c, d, e, g] = text.transform;
|
||||
let (e, g) = tf((e, g));
|
||||
text.transform = [a * sx, b * sy, c * sx, d * sy, e, g];
|
||||
if !text.transform.iter().all(|v| v.is_finite()) {
|
||||
return None;
|
||||
}
|
||||
if let Some([x0, y0, x1, y1]) = text.clip {
|
||||
let (p, q) = (tf((f64::from(x0), f64::from(y0))), tf((f64::from(x1), f64::from(y1))));
|
||||
text.clip = Some([f(p.0.min(q.0))?, f(p.1.min(q.1))?, f(p.0.max(q.0))?, f(p.1.max(q.1))?]);
|
||||
}
|
||||
text.region = map_region(region, &tf)?;
|
||||
Some(Item::Text(text))
|
||||
}
|
||||
});
|
||||
let items = items.collect();
|
||||
Picture { width: f(size.0).unwrap_or(0.0), height: f(size.1).unwrap_or(0.0), items }
|
||||
}
|
||||
|
||||
fn map_region(region: Option<Vec<[f64; 4]>>, tf: &impl Fn(P) -> P) -> Option<Option<Vec<[f32; 4]>>> {
|
||||
let Some(rects) = region else { return Some(None) };
|
||||
let mut out = Vec::with_capacity(rects.len());
|
||||
for [x0, y0, x1, y1] in rects {
|
||||
let (p, q) = (tf((x0, y0)), tf((x1, y1)));
|
||||
out.push([f(p.0.min(q.0))?, f(p.1.min(q.1))?, f(p.0.max(q.0))?, f(p.1.max(q.1))?]);
|
||||
}
|
||||
Some(Some(out))
|
||||
}
|
||||
|
||||
fn f(v: f64) -> Option<f32> {
|
||||
let v = v as f32;
|
||||
v.is_finite().then_some(v)
|
||||
}
|
||||
|
||||
fn seg(op: &Op, tf: impl Fn(P) -> P) -> Option<Seg> {
|
||||
let pt = |p: P| {
|
||||
let (x, y) = tf(p);
|
||||
Some((f(x)?, f(y)?))
|
||||
};
|
||||
Some(match *op {
|
||||
Op::M(p) => {
|
||||
let (x, y) = pt(p)?;
|
||||
Seg::Move(x, y)
|
||||
}
|
||||
Op::L(p) => {
|
||||
let (x, y) = pt(p)?;
|
||||
Seg::Line(x, y)
|
||||
}
|
||||
Op::C(a, b, c) => {
|
||||
let (x1, y1) = pt(a)?;
|
||||
let (x2, y2) = pt(b)?;
|
||||
let (x3, y3) = pt(c)?;
|
||||
Seg::Cubic(x1, y1, x2, y2, x3, y3)
|
||||
}
|
||||
Op::Z => Seg::Close,
|
||||
})
|
||||
}
|
||||
|
||||
/// Bounding box `[x0, y0, x1, y1]` over the finite points of the output.
|
||||
fn bbox(raw: &[Raw]) -> Option<[f64; 4]> {
|
||||
let mut b: Option<[f64; 4]> = None;
|
||||
let mut grow = |p: P| {
|
||||
if p.0.is_finite() && p.1.is_finite() {
|
||||
b = Some(match b {
|
||||
None => [p.0, p.1, p.0, p.1],
|
||||
Some([x0, y0, x1, y1]) => [x0.min(p.0), y0.min(p.1), x1.max(p.0), y1.max(p.1)],
|
||||
});
|
||||
}
|
||||
};
|
||||
for r in raw {
|
||||
match r {
|
||||
Raw::Path { ops, .. } | Raw::Pattern { ops, .. } => {
|
||||
for op in ops {
|
||||
match *op {
|
||||
Op::M(p) | Op::L(p) => grow(p),
|
||||
Op::C(a, c, d) => {
|
||||
grow(a);
|
||||
grow(c);
|
||||
grow(d);
|
||||
}
|
||||
Op::Z => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
Raw::Bitmap { rect, .. } => {
|
||||
grow((rect[0], rect[1]));
|
||||
grow((rect[0] + rect[2], rect[1] + rect[3]));
|
||||
}
|
||||
Raw::Text { text: t, .. } => grow((t.transform[4], t.transform[5])),
|
||||
}
|
||||
}
|
||||
b
|
||||
}
|
||||
@@ -1,195 +0,0 @@
|
||||
//! Placing a picture on a target (a page or a device): the fitted transform, paths as kurbo paths in
|
||||
//! target units and bitmaps as target rectangles. The rasteriser and the PDF writer only paint the result.
|
||||
|
||||
use std::sync::Arc;
|
||||
|
||||
use kurbo::{Affine, BezPath, Rect, Shape};
|
||||
|
||||
use crate::{Item, Picture, Seg};
|
||||
|
||||
/// Largest bitmap side the renderers draw: within the PDF image limit and the rasteriser's 16-bit sides.
|
||||
pub const MAX_BITMAP_SIDE: u32 = 30_000;
|
||||
/// Placed coordinates past this (target units) are far off any page; such items are left out.
|
||||
const MAX_COORD: f64 = 1e6;
|
||||
/// Smallest placed bitmap side (target units), so the stretch onto it is never singular.
|
||||
const MIN_BITMAP_SIDE: f64 = 1e-6;
|
||||
/// Maximum aggregate path elements retained in placed output.
|
||||
const MAX_PLACED_ELEMENTS: usize = 500_000;
|
||||
|
||||
/// One item of a placed picture, in target units.
|
||||
pub enum PlacedItem {
|
||||
/// A filled and/or stroked path. The stroke width is in target units; 0 is a cosmetic hairline that
|
||||
/// the renderer draws at its minimum width.
|
||||
Path { path: BezPath, fill: Option<[u8; 4]>, stroke: Option<([u8; 4], f64)>, even_odd: bool, region: Option<Arc<BezPath>> },
|
||||
/// `Picture::items[index]`, a bitmap, stretched into `rect`.
|
||||
Bitmap { index: usize, rect: Rect, region: Option<Arc<BezPath>> },
|
||||
/// `Picture::items[index]`, a pattern fill: `path` (target units) filled with the item's tile
|
||||
/// repeated; `tile` maps tile pixels to target units.
|
||||
Pattern { index: usize, path: BezPath, even_odd: bool, tile: Affine, region: Option<Arc<BezPath>> },
|
||||
/// `Picture::items[index]`, a text string: `transform` maps its text space to target units;
|
||||
/// `clip` (target units) bounds what it draws. `region` is the device-context clip.
|
||||
Text { index: usize, transform: Affine, clip: Option<Rect>, region: Option<Arc<BezPath>> },
|
||||
}
|
||||
|
||||
impl Picture {
|
||||
/// Whether the picture has a finite, positive size, so it can be fitted into a frame.
|
||||
pub fn has_size(&self) -> bool {
|
||||
self.width.is_finite() && self.height.is_finite() && self.width > 0.0 && self.height > 0.0
|
||||
}
|
||||
|
||||
/// The transform that fits the picture into `r` with the crop fractions `crop` (left, top, right,
|
||||
/// bottom) removed first: the visible part fills `r`. None for a degenerate size.
|
||||
pub fn fit(&self, r: Rect, crop: [f32; 4]) -> Option<Affine> {
|
||||
let [cl, ct, cr, cb] = crop.map(|v| if v.is_finite() { f64::from(v.clamp(0.0, 0.95)) } else { 0.0 });
|
||||
let (w, h) = (f64::from(self.width), f64::from(self.height));
|
||||
let sx = r.width() / (w * (1.0 - cl - cr).max(0.05));
|
||||
let sy = r.height() / (h * (1.0 - ct - cb).max(0.05));
|
||||
(sx.is_finite() && sy.is_finite() && sx > 0.0 && sy > 0.0)
|
||||
.then(|| Affine::translate((r.x0 - cl * w * sx, r.y0 - ct * h * sy)) * Affine::scale_non_uniform(sx, sy))
|
||||
}
|
||||
|
||||
/// The items placed into `r` with `crop` applied. Items that are not finite or reach past ±1e6
|
||||
/// target units are left out (they are far off any page), as are bitmaps with no area. Stroke
|
||||
/// widths are scaled to target units (along the mean axis scale, so a non-uniform fit keeps
|
||||
/// strokes even).
|
||||
pub fn place(&self, r: Rect, crop: [f32; 4]) -> Vec<PlacedItem> {
|
||||
let Some(tf) = self.fit(r, crop) else { return Vec::new() };
|
||||
let c = tf.as_coeffs();
|
||||
let k = (c[0].abs() + c[3].abs()) / 2.0;
|
||||
let mut out = Vec::with_capacity(self.items.len());
|
||||
let mut total_elements = 0usize;
|
||||
let mut cached_region: Option<(*const [f32; 4], usize, Option<Option<Arc<BezPath>>>)> = None;
|
||||
let mut get_region = |reg: &Option<Vec<[f32; 4]>>| -> Option<Option<Arc<BezPath>>> {
|
||||
let (ptr, len) = match reg {
|
||||
None => (std::ptr::null(), 0),
|
||||
Some(v) => (v.as_ptr(), v.len()),
|
||||
};
|
||||
if let Some((c_ptr, c_len, ref res)) = cached_region
|
||||
&& c_ptr == ptr
|
||||
&& c_len == len
|
||||
{
|
||||
return res.clone();
|
||||
}
|
||||
let res = placed_region(&tf, reg);
|
||||
cached_region = Some((ptr, len, res.clone()));
|
||||
res
|
||||
};
|
||||
|
||||
for (index, item) in self.items.iter().enumerate() {
|
||||
if total_elements >= MAX_PLACED_ELEMENTS {
|
||||
break;
|
||||
}
|
||||
match item {
|
||||
Item::Path { segs, fill, stroke, even_odd, region } => {
|
||||
let path = tf * bez(segs);
|
||||
let Some(region) = get_region(region) else { continue };
|
||||
if in_range(path.bounding_box()) {
|
||||
total_elements = total_elements.saturating_add(path.elements().len());
|
||||
let stroke = stroke.map(|(c, w)| (c, f64::from(w) * k));
|
||||
out.push(PlacedItem::Path { path, fill: *fill, stroke, even_odd: *even_odd, region });
|
||||
}
|
||||
}
|
||||
Item::Pattern { segs, even_odd, tile: t, region } => {
|
||||
let path = tf * bez(segs);
|
||||
let Some(region) = get_region(region) else { continue };
|
||||
let tile = tf
|
||||
* Affine::translate((f64::from(t.origin[0]), f64::from(t.origin[1])))
|
||||
* Affine::scale_non_uniform(f64::from(t.cell[0]), f64::from(t.cell[1]));
|
||||
let det = tile.determinant();
|
||||
if in_range(path.bounding_box()) && det.is_finite() && det.abs() > 1e-12 {
|
||||
total_elements = total_elements.saturating_add(path.elements().len());
|
||||
out.push(PlacedItem::Pattern { index, path, even_odd: *even_odd, tile, region });
|
||||
}
|
||||
}
|
||||
Item::Text(t) => {
|
||||
let transform = tf * Affine::new(t.transform);
|
||||
let Some(region) = get_region(&t.region) else { continue };
|
||||
let clip =
|
||||
t.clip.map(|[x0, y0, x1, y1]| tf.transform_rect_bbox(Rect::new(f64::from(x0), f64::from(y0), f64::from(x1), f64::from(y1))));
|
||||
let c = transform.as_coeffs();
|
||||
let origin = Rect::new(c[4], c[5], c[4], c[5]);
|
||||
if in_range(origin) && c.iter().all(|v| v.is_finite() && v.abs() <= MAX_COORD) && clip.is_none_or(in_range) {
|
||||
out.push(PlacedItem::Text { index, transform, clip, region });
|
||||
}
|
||||
}
|
||||
Item::Bitmap { rect: [x, y, w, h], region, .. } => {
|
||||
let (x0, y0) = (f64::from(*x), f64::from(*y));
|
||||
let rect = tf.transform_rect_bbox(Rect::new(x0, y0, x0 + f64::from(*w), y0 + f64::from(*h)));
|
||||
let Some(region) = get_region(region) else { continue };
|
||||
if in_range(rect) && rect.width() > MIN_BITMAP_SIDE && rect.height() > MIN_BITMAP_SIDE {
|
||||
out.push(PlacedItem::Bitmap { index, rect, region });
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
out
|
||||
}
|
||||
}
|
||||
|
||||
impl Item {
|
||||
/// Takes a bitmap's pixels out of the item (its geometry stays) and returns them as (width, height,
|
||||
/// RGBA) when a renderer can draw them: both sides in 1..=[`MAX_BITMAP_SIDE`] and exactly that many
|
||||
/// pixels. None for paths and for bitmaps that cannot be drawn.
|
||||
pub fn take_pixels(&mut self) -> Option<(u32, u32, Vec<u8>)> {
|
||||
let Item::Bitmap { width, height, rgba, .. } = self else { return None };
|
||||
let (w, h, px) = (*width, *height, std::mem::take(rgba));
|
||||
let sides = (1..=MAX_BITMAP_SIDE).contains(&w) && (1..=MAX_BITMAP_SIDE).contains(&h);
|
||||
(sides && px.len() == (w as usize).saturating_mul(h as usize).saturating_mul(4)).then_some((w, h, px))
|
||||
}
|
||||
}
|
||||
|
||||
/// The device-context clip in target units. `Some(None)` is the default clip. `None` means the
|
||||
/// region does not meet the page, so the item draws nothing.
|
||||
fn placed_region(tf: &Affine, region: &Option<Vec<[f32; 4]>>) -> Option<Option<Arc<BezPath>>> {
|
||||
let Some(rects) = region else { return Some(None) };
|
||||
let mut path = BezPath::new();
|
||||
for [x0, y0, x1, y1] in rects {
|
||||
let r = tf.transform_rect_bbox(Rect::new(f64::from(*x0), f64::from(*y0), f64::from(*x1), f64::from(*y1)));
|
||||
if in_range(r) && r.width() > 1e-9 && r.height() > 1e-9 {
|
||||
path.extend(r.to_path(0.1));
|
||||
}
|
||||
}
|
||||
(!path.is_empty()).then_some(Some(Arc::new(path)))
|
||||
}
|
||||
|
||||
/// Segments as a kurbo path. A segment after a close opens a new subpath at the last start point.
|
||||
fn bez(segs: &[Seg]) -> BezPath {
|
||||
let mut p = BezPath::new();
|
||||
let mut start = (0.0, 0.0);
|
||||
let mut open = false;
|
||||
for s in segs {
|
||||
match *s {
|
||||
Seg::Move(x, y) => {
|
||||
start = (f64::from(x), f64::from(y));
|
||||
p.move_to(start);
|
||||
open = true;
|
||||
}
|
||||
Seg::Line(x, y) => {
|
||||
if !open {
|
||||
p.move_to(start);
|
||||
}
|
||||
p.line_to((f64::from(x), f64::from(y)));
|
||||
open = true;
|
||||
}
|
||||
Seg::Cubic(a, b, c, d, e, f) => {
|
||||
if !open {
|
||||
p.move_to(start);
|
||||
}
|
||||
p.curve_to((f64::from(a), f64::from(b)), (f64::from(c), f64::from(d)), (f64::from(e), f64::from(f)));
|
||||
open = true;
|
||||
}
|
||||
Seg::Close => {
|
||||
if open {
|
||||
p.close_path();
|
||||
}
|
||||
open = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
p
|
||||
}
|
||||
|
||||
/// Whether a placed box is finite and within ±[`MAX_COORD`].
|
||||
fn in_range(b: Rect) -> bool {
|
||||
[b.x0, b.y0, b.x1, b.y1].iter().all(|v| v.is_finite() && v.abs() < MAX_COORD)
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,203 +0,0 @@
|
||||
//! Text records: the font (LOGFONT), the text string decoded from its character set, and the
|
||||
//! neutral [`Text`] item. The metafile crate knows no fonts, so measuring, alignment against the
|
||||
//! font's metrics and glyph drawing are left to the renderer.
|
||||
|
||||
/// A font chosen by a CREATEFONTINDIRECT record (MS-WMF 2.2.1.2 Font, MS-EMF 2.2.13 LogFont).
|
||||
#[derive(Debug, Clone, PartialEq, Default)]
|
||||
pub struct Font {
|
||||
/// The face name the file asks for (`"Arial"`, `"Wingdings"`); may be empty.
|
||||
pub family: String,
|
||||
/// LOGFONT height in text units: negative is the character (em) height, positive the cell
|
||||
/// height (ascent plus descent), 0 the default size.
|
||||
pub height: f32,
|
||||
/// Average character width in text units; 0 keeps the font's own proportions.
|
||||
pub width: f32,
|
||||
/// 100..=900 (400 regular, 700 bold); 0 is the default weight.
|
||||
pub weight: u16,
|
||||
pub italic: bool,
|
||||
pub underline: bool,
|
||||
pub strike: bool,
|
||||
/// LOGFONT character set (0 ANSI, 2 SYMBOL, 238 EASTEUROPE…).
|
||||
pub charset: u8,
|
||||
}
|
||||
|
||||
/// SYMBOL_CHARSET: byte codes are the font's own symbol codes.
|
||||
pub const SYMBOL_CHARSET: u8 = 2;
|
||||
|
||||
/// Where the reference point sits on the string horizontally (TA_LEFT, TA_CENTER, TA_RIGHT).
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
|
||||
pub enum HAlign {
|
||||
#[default]
|
||||
Left,
|
||||
Center,
|
||||
Right,
|
||||
}
|
||||
|
||||
/// Where the reference point sits vertically (TA_TOP, TA_BASELINE, TA_BOTTOM): the top of the
|
||||
/// character cell, the baseline or the bottom of the cell.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
|
||||
pub enum VAlign {
|
||||
#[default]
|
||||
Top,
|
||||
Baseline,
|
||||
Bottom,
|
||||
}
|
||||
|
||||
/// One string drawn by EXTTEXTOUT, TEXTOUT or SMALLTEXTOUT.
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub struct Text {
|
||||
/// Text space to picture space as affine coefficients `[a, b, c, d, e, f]` (`x' = a·x + c·y + e`,
|
||||
/// `y' = b·x + d·y + f`, kurbo's order). Text space has x along the baseline and y down, in the
|
||||
/// metafile's logical units, with the reference point at the origin. Escapement and the
|
||||
/// mapping are folded in; glyphs are never mirrored.
|
||||
pub transform: [f64; 6],
|
||||
pub text: String,
|
||||
/// Advance after each character of `text`, in text units (the dx array). Empty when the file
|
||||
/// gives none: the font's own advances apply.
|
||||
pub dx: Vec<f32>,
|
||||
pub font: Font,
|
||||
pub color: [u8; 4],
|
||||
/// Colour filled behind the character cells (OPAQUE background mode); None when transparent.
|
||||
pub background: Option<[u8; 4]>,
|
||||
pub h_align: HAlign,
|
||||
pub v_align: VAlign,
|
||||
/// Clip rectangle `[x0, y0, x1, y1]` in picture units (ETO_CLIPPED).
|
||||
pub clip: Option<[f32; 4]>,
|
||||
/// Clip region from the device context, in picture units. None is the default clip. Drawn
|
||||
/// together with [`Text::clip`] when both are set.
|
||||
pub region: Option<Vec<[f32; 4]>>,
|
||||
}
|
||||
|
||||
/// TA_UPDATECP: the string starts at the current position, which moves past it.
|
||||
pub(crate) const TA_UPDATECP: u32 = 0x0001;
|
||||
/// ETO_OPAQUE fills the rectangle with the background colour; ETO_CLIPPED clips the text to it.
|
||||
pub(crate) const ETO_OPAQUE: u32 = 0x0002;
|
||||
pub(crate) const ETO_CLIPPED: u32 = 0x0004;
|
||||
/// The string holds glyph indices, not characters (drawn only when the font matches).
|
||||
pub(crate) const ETO_GLYPH_INDEX: u32 = 0x0010;
|
||||
/// The dx array holds (dx, dy) pairs.
|
||||
pub(crate) const ETO_PDY: u32 = 0x2000;
|
||||
/// Longest string kept from one record.
|
||||
pub(crate) const MAX_CHARS: usize = 16_384;
|
||||
|
||||
/// Horizontal and vertical alignment from a TA_* word (the vertical bits are 0 top, 8 bottom,
|
||||
/// 24 baseline; the horizontal ones 0 left, 2 right, 6 center).
|
||||
pub(crate) fn align(ta: u32) -> (HAlign, VAlign) {
|
||||
let h = match ta & 0x06 {
|
||||
6 => HAlign::Center,
|
||||
2 => HAlign::Right,
|
||||
_ => HAlign::Left,
|
||||
};
|
||||
let v = match ta & 0x18 {
|
||||
0x18 => VAlign::Baseline,
|
||||
0x08 => VAlign::Bottom,
|
||||
_ => VAlign::Top,
|
||||
};
|
||||
(h, v)
|
||||
}
|
||||
|
||||
/// Decodes 8-bit text in `charset` to one character per byte, and merges `dx` to match: a
|
||||
/// double-byte character set's lead and trail bytes become one blank (its glyphs are not
|
||||
/// decoded) whose advance is the sum of both. `dx` may be empty.
|
||||
pub(crate) fn decode_bytes(bytes: &[u8], charset: u8, dx: &[f32]) -> (String, Vec<f32>) {
|
||||
let mut s = String::with_capacity(bytes.len());
|
||||
let mut out_dx = Vec::with_capacity(dx.len().min(bytes.len()));
|
||||
let dbcs = matches!(charset, 128 | 129 | 130 | 134 | 136);
|
||||
let mut i = 0;
|
||||
while let Some(&b) = bytes.get(i) {
|
||||
let n = if dbcs && b >= 0x81 && i + 1 < bytes.len() { 2 } else { 1 };
|
||||
let c = if n == 2 { ' ' } else { byte_char(b, charset) };
|
||||
s.push(c);
|
||||
if !dx.is_empty() {
|
||||
out_dx.push((i..i + n).filter_map(|k| dx.get(k)).sum());
|
||||
}
|
||||
i += n;
|
||||
}
|
||||
(s, out_dx)
|
||||
}
|
||||
|
||||
/// UTF-16 text to a string with one dx entry per character: a surrogate pair takes the sum of
|
||||
/// its two advances. Unpaired surrogates become U+FFFD.
|
||||
pub(crate) fn decode_utf16(units: &[u16], dx: &[f32]) -> (String, Vec<f32>) {
|
||||
let mut s = String::with_capacity(units.len());
|
||||
let mut out_dx = Vec::with_capacity(dx.len().min(units.len()));
|
||||
let mut i = 0;
|
||||
for r in char::decode_utf16(units.iter().copied()) {
|
||||
let c = r.unwrap_or(char::REPLACEMENT_CHARACTER);
|
||||
let n = c.len_utf16();
|
||||
s.push(c);
|
||||
if !dx.is_empty() {
|
||||
out_dx.push((i..i + n).filter_map(|k| dx.get(k)).sum());
|
||||
}
|
||||
i += n;
|
||||
}
|
||||
(s, out_dx)
|
||||
}
|
||||
|
||||
/// One byte in a single-byte Windows code page. Symbol fonts keep their codes in the private-use
|
||||
/// range (U+F000 + code), where symbol fonts map them.
|
||||
fn byte_char(b: u8, charset: u8) -> char {
|
||||
if b < 0x80 && charset != SYMBOL_CHARSET {
|
||||
return char::from(b);
|
||||
}
|
||||
let cp = match charset {
|
||||
SYMBOL_CHARSET => 0xF000 | u32::from(b),
|
||||
238 => high(b, &CP1250),
|
||||
204 => high(b, &CP1251),
|
||||
_ => high(b, &CP1252),
|
||||
};
|
||||
char::from_u32(cp).unwrap_or(char::REPLACEMENT_CHARACTER)
|
||||
}
|
||||
|
||||
/// The code point of byte `b` (0x80..=0xFF) from a table of the upper half; 0 entries are
|
||||
/// undefined bytes, drawn as U+FFFD.
|
||||
fn high(b: u8, table: &[u16; 128]) -> u32 {
|
||||
match table.get(usize::from(b.wrapping_sub(0x80))) {
|
||||
Some(0) | None => 0xFFFD,
|
||||
Some(v) => u32::from(*v),
|
||||
}
|
||||
}
|
||||
|
||||
/// Windows-1252 (Western) 0x80..=0xFF: the upper half of Latin-1 with typographic marks in 0x80..=0x9F.
|
||||
const CP1252: [u16; 128] = {
|
||||
let mut t = [0u16; 128];
|
||||
let low: [u16; 32] = [
|
||||
0x20AC, 0, 0x201A, 0x0192, 0x201E, 0x2026, 0x2020, 0x2021, 0x02C6, 0x2030, 0x0160, 0x2039, 0x0152, 0, 0x017D, 0, 0, 0x2018, 0x2019, 0x201C,
|
||||
0x201D, 0x2022, 0x2013, 0x2014, 0x02DC, 0x2122, 0x0161, 0x203A, 0x0153, 0, 0x017E, 0x0178,
|
||||
];
|
||||
let mut i = 0;
|
||||
while i < 128 {
|
||||
t[i] = if i < 32 { low[i] } else { 0x80 + i as u16 };
|
||||
i += 1;
|
||||
}
|
||||
t
|
||||
};
|
||||
|
||||
/// Windows-1250 (Central European) 0x80..=0xFF.
|
||||
const CP1250: [u16; 128] = [
|
||||
0x20AC, 0, 0x201A, 0, 0x201E, 0x2026, 0x2020, 0x2021, 0, 0x2030, 0x0160, 0x2039, 0x015A, 0x0164, 0x017D, 0x0179, //
|
||||
0, 0x2018, 0x2019, 0x201C, 0x201D, 0x2022, 0x2013, 0x2014, 0, 0x2122, 0x0161, 0x203A, 0x015B, 0x0165, 0x017E, 0x017A, //
|
||||
0x00A0, 0x02C7, 0x02D8, 0x0141, 0x00A4, 0x0104, 0x00A6, 0x00A7, 0x00A8, 0x00A9, 0x015E, 0x00AB, 0x00AC, 0x00AD, 0x00AE, 0x017B, //
|
||||
0x00B0, 0x00B1, 0x02DB, 0x0142, 0x00B4, 0x00B5, 0x00B6, 0x00B7, 0x00B8, 0x0105, 0x015F, 0x00BB, 0x013D, 0x02DD, 0x013E, 0x017C, //
|
||||
0x0154, 0x00C1, 0x00C2, 0x0102, 0x00C4, 0x0139, 0x0106, 0x00C7, 0x010C, 0x00C9, 0x0118, 0x00CB, 0x011A, 0x00CD, 0x00CE, 0x010E, //
|
||||
0x0110, 0x0143, 0x0147, 0x00D3, 0x00D4, 0x0150, 0x00D6, 0x00D7, 0x0158, 0x016E, 0x00DA, 0x0170, 0x00DC, 0x00DD, 0x0162, 0x00DF, //
|
||||
0x0155, 0x00E1, 0x00E2, 0x0103, 0x00E4, 0x013A, 0x0107, 0x00E7, 0x010D, 0x00E9, 0x0119, 0x00EB, 0x011B, 0x00ED, 0x00EE, 0x010F, //
|
||||
0x0111, 0x0144, 0x0148, 0x00F3, 0x00F4, 0x0151, 0x00F6, 0x00F7, 0x0159, 0x016F, 0x00FA, 0x0171, 0x00FC, 0x00FD, 0x0163, 0x02D9,
|
||||
];
|
||||
|
||||
/// Windows-1251 (Cyrillic) 0x80..=0xFF; 0xC0..=0xFF are А..я in order.
|
||||
const CP1251: [u16; 128] = {
|
||||
let low: [u16; 64] = [
|
||||
0x0402, 0x0403, 0x201A, 0x0453, 0x201E, 0x2026, 0x2020, 0x2021, 0x20AC, 0x2030, 0x0409, 0x2039, 0x040A, 0x040C, 0x040B, 0x040F, //
|
||||
0x0452, 0x2018, 0x2019, 0x201C, 0x201D, 0x2022, 0x2013, 0x2014, 0, 0x2122, 0x0459, 0x203A, 0x045A, 0x045C, 0x045B, 0x045F, //
|
||||
0x00A0, 0x040E, 0x045E, 0x0408, 0x00A4, 0x0490, 0x00A6, 0x00A7, 0x0401, 0x00A9, 0x0404, 0x00AB, 0x00AC, 0x00AD, 0x00AE, 0x0407, //
|
||||
0x00B0, 0x00B1, 0x0406, 0x0456, 0x0491, 0x00B5, 0x00B6, 0x00B7, 0x0451, 0x2116, 0x0454, 0x00BB, 0x0458, 0x0405, 0x0455, 0x0457,
|
||||
];
|
||||
let mut t = [0u16; 128];
|
||||
let mut i = 0;
|
||||
while i < 128 {
|
||||
t[i] = if i < 64 { low[i] } else { 0x0410 + (i as u16 - 64) };
|
||||
i += 1;
|
||||
}
|
||||
t
|
||||
};
|
||||
@@ -1,383 +0,0 @@
|
||||
//! WMF reader (MS-WMF). A file is an optional 22-byte placeable header, a 9-word standard header, then
|
||||
//! records of (size in 16-bit words, function, parameters). Parameters are read at byte 6 of each record.
|
||||
|
||||
use std::sync::Arc;
|
||||
|
||||
use crate::bytes::Bytes;
|
||||
use crate::canvas::{BS_HATCHED, Brush, Canvas, LogFont, Obj, Pen, colorref};
|
||||
use crate::dib;
|
||||
use crate::geom::{self, ArcKind, P};
|
||||
use crate::text::{ETO_CLIPPED, ETO_OPAQUE, Font, MAX_CHARS, decode_bytes};
|
||||
use crate::{Error, Frame, MAX_PATH_OPS};
|
||||
|
||||
const PLACEABLE_KEY: u32 = 0x9AC6_CDD7;
|
||||
const DEFAULT_INCH: f64 = 1440.0;
|
||||
/// A placeable WMF plays into a target sized in points; brushes are laid out in 96-dpi pixels.
|
||||
const PT_PER_PX: f64 = 72.0 / 96.0;
|
||||
|
||||
pub(crate) fn is_wmf(b: Bytes) -> bool {
|
||||
if b.u32(0) == Some(PLACEABLE_KEY) {
|
||||
return true;
|
||||
}
|
||||
matches!(b.u16(0), Some(1 | 2)) && b.u16(2) == Some(9) && matches!(b.u16(4), Some(0x100 | 0x300))
|
||||
}
|
||||
|
||||
/// Reads all records. Returns the drawing state and the picture frame, or an error when no record
|
||||
/// could be read at all.
|
||||
pub(crate) fn parse(input: Bytes) -> Result<(Canvas, Option<Frame>), Error> {
|
||||
let placeable = input.u32(0) == Some(PLACEABLE_KEY);
|
||||
// Placeable bounds: logical corners (left, top, right, bottom) and their size in points.
|
||||
let (hdr, bounds) = if placeable {
|
||||
let [l, t, r, b] = [6, 8, 10, 12].map(|o| input.i16(o).map(f64::from));
|
||||
let (Some(l), Some(t), Some(r), Some(b)) = (l, t, r, b) else { return Err(Error::BadHeader) };
|
||||
let inch = input.u16(14).filter(|v| *v > 0).map_or(DEFAULT_INCH, f64::from);
|
||||
let (w, h) = (r - l, b - t);
|
||||
let bounds = (w > 0.0 && h > 0.0).then_some(([l, t, r, b], (w * 72.0 / inch, h * 72.0 / inch)));
|
||||
(22, bounds)
|
||||
} else {
|
||||
(0, None)
|
||||
};
|
||||
let hsize = input.u16(hdr + 2).ok_or(Error::BadHeader)? as usize;
|
||||
if hsize < 9 {
|
||||
return Err(Error::BadHeader);
|
||||
}
|
||||
let mut pos = hdr.checked_add(hsize.checked_mul(2).ok_or(Error::BadHeader)?).ok_or(Error::BadHeader)?;
|
||||
// Played into a target like Word does: the placeable bounds are the first window, the target (the
|
||||
// picture size) the viewport. A plain WMF's target is its first window.
|
||||
let mut cv = Canvas::new();
|
||||
if let Some((c, size)) = bounds {
|
||||
cv.play_into(Some([c[0], c[1], c[2] - c[0], c[3] - c[1]]), size);
|
||||
cv.set_clip_surface([0.0, 0.0, size.0, size.1]);
|
||||
cv.pattern_px = PT_PER_PX;
|
||||
}
|
||||
let mut player_viewport = bounds.is_some();
|
||||
let mut parsed = 0usize;
|
||||
while !cv.full() {
|
||||
let (Some(words), Some(func)) = (input.u32(pos), pos.checked_add(4).and_then(|o| input.u16(o))) else { break };
|
||||
let Some(len) = (words as usize).checked_mul(2).filter(|l| *l >= 6) else { break };
|
||||
let Some(rec) = input.slice(pos, len) else { break };
|
||||
if func == 0 {
|
||||
break;
|
||||
}
|
||||
record(&mut cv, func, Bytes(rec));
|
||||
if func == 0x020C && cv.window_set && !player_viewport && !cv.viewport_set {
|
||||
let we = cv.map.window_ext();
|
||||
cv.play_into(None, (we.0.abs(), we.1.abs()));
|
||||
player_viewport = true;
|
||||
}
|
||||
parsed += 1;
|
||||
let Some(next) = pos.checked_add(len) else { break };
|
||||
pos = next;
|
||||
}
|
||||
if parsed == 0 {
|
||||
return Err(Error::NoRecords);
|
||||
}
|
||||
let frame = match bounds {
|
||||
// Played into its target: the frame is the target.
|
||||
Some((_, size)) if !cv.viewport_set && cv.map.uses_extents() => Some(Frame { rect: [0.0, 0.0, size.0, size.1], size: Some(size) }),
|
||||
// The file set its own viewport, so the drawing is in its device units and the logical bounds
|
||||
// go through the final mapping.
|
||||
Some((c, size)) => {
|
||||
let (a, b) = (cv.map.device((c[0], c[1])), cv.map.device((c[2], c[3])));
|
||||
let rect = [a.0.min(b.0), a.1.min(b.1), (b.0 - a.0).abs(), (b.1 - a.1).abs()];
|
||||
(rect[2] > 0.0 && rect[3] > 0.0).then_some(Frame { rect, size: Some(size) })
|
||||
}
|
||||
None if player_viewport && !cv.viewport_set && cv.map.uses_extents() => {
|
||||
let ve = cv.map.viewport_ext();
|
||||
Some(Frame { rect: [0.0, 0.0, ve.0, ve.1], size: None })
|
||||
}
|
||||
None if cv.window_set => Some(Frame { rect: cv.map.window_rect(), size: None }),
|
||||
None => None,
|
||||
};
|
||||
Ok((cv, frame))
|
||||
}
|
||||
|
||||
fn record(cv: &mut Canvas, func: u16, r: Bytes) {
|
||||
if cv.full() {
|
||||
return;
|
||||
}
|
||||
let _ = match func {
|
||||
0x0103 => r.u16(6).map(|m| cv.set_map_mode(u32::from(m))),
|
||||
0x0106 => r.u16(6).map(|m| cv.set_polyfill(u32::from(m))),
|
||||
0x0104 => r.u16(6).map(|m| cv.set_rop2(u32::from(m))),
|
||||
// Y is stored before X in these records.
|
||||
0x020B => xy16(r, 6).map(|p| cv.set_window_org(p)),
|
||||
0x020C => xy16(r, 6).map(|p| cv.set_window_ext(p)),
|
||||
0x020D => xy16(r, 6).map(|p| cv.set_viewport_org(p)),
|
||||
0x020E => xy16(r, 6).map(|p| cv.set_viewport_ext(p)),
|
||||
0x001E => {
|
||||
cv.save_dc();
|
||||
Some(())
|
||||
}
|
||||
0x0127 => r.i16(6).map(|n| cv.restore_dc(i32::from(n))),
|
||||
0x0214 => xy16(r, 6).map(|p| cv.move_to(p)),
|
||||
0x0213 => xy16(r, 6).map(|p| cv.line_to(p)),
|
||||
0x0325 => counted_points(r).map(|pts| cv.poly(&pts, false, false, true)),
|
||||
0x0324 => counted_points(r).map(|pts| cv.poly(&pts, true, true, true)),
|
||||
0x0538 => polypolygon(r).map(|ops| cv.shape(&ops, true, true)),
|
||||
0x041B => rect16(r, 6).map(|[l, t, rr, b]| cv.shape(&geom::rect(l, t, rr, b), true, true)),
|
||||
0x061C => {
|
||||
let [ew, eh] = [r.i16(8), r.i16(6)];
|
||||
let (Some(ew), Some(eh)) = (ew, eh) else { return };
|
||||
rect16(r, 10).map(|[l, t, rr, b]| {
|
||||
cv.shape(&geom::round_rect(l, t, rr, b, f64::from(ew), f64::from(eh)), true, true);
|
||||
})
|
||||
}
|
||||
0x0415 => rect16(r, 6).map(|rect| cv.exclude_clip_rect(rect)),
|
||||
0x0416 => rect16(r, 6).map(|rect| cv.intersect_clip_rect(rect)),
|
||||
0x0418 => rect16(r, 6).map(|[l, t, rr, b]| cv.shape(&geom::ellipse(l, t, rr, b), true, true)),
|
||||
0x0817 | 0x081A | 0x0830 => arc16(r).map(|(bounds, s, e)| {
|
||||
let kind = match func {
|
||||
0x0817 => ArcKind::Arc,
|
||||
0x081A => ArcKind::Pie,
|
||||
_ => ArcKind::Chord,
|
||||
};
|
||||
let [l, t, rr, b] = bounds;
|
||||
cv.shape(&geom::arc_shape(l, t, rr, b, s, e, kind), kind != ArcKind::Arc, true);
|
||||
}),
|
||||
0x02FA => create_pen(cv, r),
|
||||
0x02FC => create_brush(cv, r),
|
||||
0x02FB => {
|
||||
let f = font(r).unwrap_or_default();
|
||||
cv.add_object(None, Obj::Font(Arc::new(f)));
|
||||
Some(())
|
||||
}
|
||||
0x0142 => {
|
||||
// A pattern the reader cannot decode still takes its handle slot.
|
||||
let b = dib_pattern_brush(r).map_or(Obj::Other, Obj::Brush);
|
||||
cv.add_object(None, b);
|
||||
Some(())
|
||||
}
|
||||
// Palettes, old-style pattern brushes and regions take a handle slot too, so later
|
||||
// SELECTOBJECT handles count them; selecting one draws nothing.
|
||||
0x00F7 | 0x01F9 | 0x06FF => {
|
||||
cv.add_object(None, Obj::Other);
|
||||
Some(())
|
||||
}
|
||||
0x0209 => r.u32(6).map(|c| cv.set_text_color(c)),
|
||||
0x0201 => r.u32(6).map(|c| cv.set_bk_color(c)),
|
||||
0x0102 => r.u16(6).map(|m| cv.set_bk_mode(u32::from(m))),
|
||||
0x012E => r.u16(6).map(|a| cv.set_text_align(u32::from(a))),
|
||||
0x0A32 => ext_text_out(cv, r),
|
||||
0x0521 => text_out(cv, r),
|
||||
// PATBLT: rop(4), height, width, y, x.
|
||||
0x061D => r.u32(6).map(|rop| cv.fill_rop([i16s(r, 16), i16s(r, 14), i16s(r, 12), i16s(r, 10)], rop)),
|
||||
0x012D => r.u16(6).map(|h| cv.select(wmf_handle(h))),
|
||||
0x01F0 => r.u16(6).map(|h| cv.delete_object(u32::from(h))),
|
||||
// STRETCHDIB: rop(4) usage(2) srcH srcW ySrc xSrc destH destW yDest xDest, then the DIB.
|
||||
0x0F43 => {
|
||||
let rop = r.u32(6).unwrap_or(0);
|
||||
dib_blit(cv, r, 28, rop, [i16s(r, 26), i16s(r, 24), i16s(r, 22), i16s(r, 20)], [i16s(r, 18), i16s(r, 16), i16s(r, 14), i16s(r, 12)])
|
||||
}
|
||||
0x0B41 => dib_stretchblt(cv, r),
|
||||
0x0940 => dib_bitblt(cv, r),
|
||||
_ => Some(()),
|
||||
};
|
||||
}
|
||||
|
||||
/// A 16-bit WMF handle as a canvas object index: stock objects are `0x8000 | index`, which the
|
||||
/// canvas reads with the 32-bit stock flag.
|
||||
fn wmf_handle(h: u16) -> u32 {
|
||||
let h = u32::from(h);
|
||||
if h & 0x8000 != 0 { (h & 0x7FFF) | 0x8000_0000 } else { h }
|
||||
}
|
||||
|
||||
/// An i16 field as f64; 0 past the end.
|
||||
fn i16s(r: Bytes, off: usize) -> f64 {
|
||||
r.i16(off).map_or(0.0, f64::from)
|
||||
}
|
||||
|
||||
/// CREATEPENINDIRECT: style, width (x, y), colour. Only the width's x component is used.
|
||||
fn create_pen(cv: &mut Canvas, r: Bytes) -> Option<()> {
|
||||
let style = r.u16(6)?;
|
||||
let width = r.i16(8)?;
|
||||
let color = r.u32(12)?;
|
||||
cv.add_object(None, Obj::Pen(Pen { style: u32::from(style), width: f64::from(width), color: colorref(color) }));
|
||||
Some(())
|
||||
}
|
||||
|
||||
/// CREATEBRUSHINDIRECT: style, colour, hatch.
|
||||
fn create_brush(cv: &mut Canvas, r: Bytes) -> Option<()> {
|
||||
let style = u32::from(r.u16(6)?);
|
||||
let color = r.u32(8)?;
|
||||
let mut b = Brush::new(style, colorref(color));
|
||||
if style == BS_HATCHED {
|
||||
b.hatch = u32::from(r.u16(12).unwrap_or(0));
|
||||
}
|
||||
cv.add_object(None, Obj::Brush(b));
|
||||
Some(())
|
||||
}
|
||||
|
||||
/// DIBCREATEPATTERNBRUSH: style, colour usage, then a DIB (MS-WMF 2.3.4.8). The DIB's own palette
|
||||
/// gives the colours; with DIB_PAL_COLORS (indices into a palette the reader does not keep) a
|
||||
/// monochrome DIB takes the text and background colours.
|
||||
fn dib_pattern_brush(r: Bytes) -> Option<Brush> {
|
||||
let usage = r.u16(8)?;
|
||||
let dib = r.tail(10)?;
|
||||
let n = dib::info_len(dib)?;
|
||||
let img = dib::decode(Bytes(dib.slice(0, n)?), dib.tail(n)?, false)?;
|
||||
Brush::pattern(img, usage == 1)
|
||||
}
|
||||
|
||||
/// Font object (MS-WMF 2.2.1.2): height, width, escapement, orientation, weight (i16 each), italic,
|
||||
/// underline, strike-out and charset bytes, three precision/quality bytes, pitch and family, then the
|
||||
/// face name (up to 32 bytes, NUL-terminated, in the font's character set).
|
||||
fn font(r: Bytes) -> Option<LogFont> {
|
||||
let height = r.i16(6)?;
|
||||
let width = r.i16(8).unwrap_or(0);
|
||||
let escapement = r.i16(10).unwrap_or(0);
|
||||
let weight = r.i16(14).unwrap_or(0);
|
||||
let byte = |o: usize| r.0.get(o).copied().unwrap_or(0);
|
||||
let charset = byte(19);
|
||||
let name = r.0.get(24..).unwrap_or(&[]);
|
||||
let name = name.get(..32).unwrap_or(name);
|
||||
let name = name.split(|b| *b == 0).next().unwrap_or(&[]);
|
||||
// Face names are plain ASCII in practice; other bytes go through the Western code page.
|
||||
let (family, _) = decode_bytes(name, 0, &[]);
|
||||
Some(LogFont {
|
||||
font: Font {
|
||||
family: family.trim().to_string(),
|
||||
height: f32::from(height),
|
||||
width: f32::from(width),
|
||||
weight: weight.clamp(0, 1000) as u16,
|
||||
italic: byte(16) != 0,
|
||||
underline: byte(17) != 0,
|
||||
strike: byte(18) != 0,
|
||||
charset,
|
||||
},
|
||||
escapement: f64::from(escapement),
|
||||
})
|
||||
}
|
||||
|
||||
/// The charset of the selected font, for decoding 8-bit text.
|
||||
fn charset(cv: &Canvas) -> u8 {
|
||||
cv.font_charset()
|
||||
}
|
||||
|
||||
/// EXTTEXTOUT (MS-WMF 2.3.3.5): y, x, string length, options, the rectangle (left, top, right,
|
||||
/// bottom) when ETO_OPAQUE or ETO_CLIPPED is set, the string padded to a word, then an optional dx
|
||||
/// array of i16 advances.
|
||||
fn ext_text_out(cv: &mut Canvas, r: Bytes) -> Option<()> {
|
||||
let (y, x) = (r.i16(6)?, r.i16(8)?);
|
||||
let n = usize::from(r.u16(10)?).min(MAX_CHARS);
|
||||
let opts = u32::from(r.u16(12)?);
|
||||
let has_rect = opts & (ETO_OPAQUE | ETO_CLIPPED) != 0;
|
||||
let rect = if has_rect { Some([i16s(r, 14), i16s(r, 16), i16s(r, 18), i16s(r, 20)]) } else { None };
|
||||
let at = if has_rect { 22 } else { 14 };
|
||||
// A string running past the record is cut to what the record holds.
|
||||
let avail = r.0.len().saturating_sub(at);
|
||||
let n = n.min(avail);
|
||||
let bytes = r.slice(at, n)?;
|
||||
let dx_at = at + n.next_multiple_of(2);
|
||||
let dx: Vec<f32> = if r.0.len() >= dx_at + 2 * n { (0..n).filter_map(|i| r.i16(dx_at + 2 * i)).map(f32::from).collect() } else { Vec::new() };
|
||||
let (text, dx) = decode_bytes(bytes, charset(cv), &dx);
|
||||
cv.text((f64::from(x), f64::from(y)), text, dx, opts, rect);
|
||||
Some(())
|
||||
}
|
||||
|
||||
/// TEXTOUT (MS-WMF 2.3.3.20): string length, the string padded to a word, then y and x.
|
||||
fn text_out(cv: &mut Canvas, r: Bytes) -> Option<()> {
|
||||
let n = usize::from(r.u16(6)?).min(MAX_CHARS);
|
||||
let bytes = r.slice(8, n)?;
|
||||
let at = 8 + n.next_multiple_of(2);
|
||||
let (y, x) = (r.i16(at)?, r.i16(at + 2)?);
|
||||
let (text, _) = decode_bytes(bytes, charset(cv), &[]);
|
||||
cv.text((f64::from(x), f64::from(y)), text, Vec::new(), 0, None);
|
||||
Some(())
|
||||
}
|
||||
|
||||
/// (x, y) from a Y-then-X pair stored at `off`.
|
||||
fn xy16(r: Bytes, off: usize) -> Option<P> {
|
||||
Some((f64::from(r.i16(off + 2)?), f64::from(r.i16(off)?)))
|
||||
}
|
||||
|
||||
/// Rectangle stored as bottom, right, top, left (the order MS-WMF uses for RECTANGLE and ELLIPSE).
|
||||
fn rect16(r: Bytes, off: usize) -> Option<[f64; 4]> {
|
||||
let (b, rr, t, l) = (r.i16(off)?, r.i16(off + 2)?, r.i16(off + 4)?, r.i16(off + 6)?);
|
||||
Some([f64::from(l), f64::from(t), f64::from(rr), f64::from(b)])
|
||||
}
|
||||
|
||||
/// ARC, PIE and CHORD: end Y, end X, start Y, start X, then the bounding rectangle (bottom, right, top, left).
|
||||
fn arc16(r: Bytes) -> Option<([f64; 4], P, P)> {
|
||||
let end = xy16(r, 6)?;
|
||||
let start = xy16(r, 10)?;
|
||||
let bounds = rect16(r, 14)?;
|
||||
Some((bounds, start, end))
|
||||
}
|
||||
|
||||
/// A count of 16-bit points, then the points (POLYLINE, POLYGON).
|
||||
fn counted_points(r: Bytes) -> Option<Vec<P>> {
|
||||
let n = r.u16(6)? as usize;
|
||||
r.tail(8)?.points(0, n, false)
|
||||
}
|
||||
|
||||
/// POLYPOLYGON: polygon count, one 16-bit point count per polygon, then all points.
|
||||
fn polypolygon(r: Bytes) -> Option<Vec<geom::Op>> {
|
||||
let n = r.u16(6)? as usize;
|
||||
let mut counts = Vec::with_capacity(n);
|
||||
for i in 0..n {
|
||||
counts.push(r.u16(i.checked_mul(2)?.checked_add(8)?)? as usize);
|
||||
}
|
||||
let total = counts.iter().try_fold(0usize, |a, c| a.checked_add(*c))?;
|
||||
if total > MAX_PATH_OPS {
|
||||
return None;
|
||||
}
|
||||
let pts = r.tail(n.checked_mul(2)?.checked_add(8)?)?.points(0, total, false)?;
|
||||
Some(geom::subpaths(&pts, &counts, true))
|
||||
}
|
||||
|
||||
/// Whether a DIBBITBLT or DIBSTRETCHBLT record carries a bitmap: without one it is exactly
|
||||
/// `(function >> 8) + 3` words long (MS-WMF 2.3.1.2, 2.3.1.3).
|
||||
fn has_bitmap(func: u16, r: Bytes) -> bool {
|
||||
r.0.len() / 2 > usize::from(func >> 8) + 3
|
||||
}
|
||||
|
||||
/// META_DIBBITBLT: rop(4), ySrc, xSrc, then height, width, yDest, xDest and the DIB at 22. The
|
||||
/// bitmap-less form has a reserved word before the height, so its fields sit 2 bytes later; it, or
|
||||
/// a source-less raster operation, fills the destination.
|
||||
fn dib_bitblt(cv: &mut Canvas, r: Bytes) -> Option<()> {
|
||||
let rop = r.u32(6)?;
|
||||
if !has_bitmap(0x0940, r) {
|
||||
cv.fill_rop([i16s(r, 22), i16s(r, 20), i16s(r, 18), i16s(r, 16)], rop);
|
||||
return Some(());
|
||||
}
|
||||
let dst = [i16s(r, 20), i16s(r, 18), i16s(r, 16), i16s(r, 14)];
|
||||
if Canvas::sourceless(rop) {
|
||||
cv.fill_rop(dst, rop);
|
||||
return Some(());
|
||||
}
|
||||
let src = [i16s(r, 12), i16s(r, 10), dst[2], dst[3]];
|
||||
dib_blit(cv, r, 22, rop, dst, src)
|
||||
}
|
||||
|
||||
/// META_DIBSTRETCHBLT: rop(4), srcH, srcW, ySrc, xSrc, then destH, destW, yDest, xDest and the DIB at
|
||||
/// 26. The bitmap-less form has a reserved word after xSrc, so the destination sits 2 bytes later;
|
||||
/// it, or a source-less raster operation, fills the destination.
|
||||
fn dib_stretchblt(cv: &mut Canvas, r: Bytes) -> Option<()> {
|
||||
let rop = r.u32(6)?;
|
||||
if !has_bitmap(0x0B41, r) {
|
||||
cv.fill_rop([i16s(r, 26), i16s(r, 24), i16s(r, 22), i16s(r, 20)], rop);
|
||||
return Some(());
|
||||
}
|
||||
let dst = [i16s(r, 24), i16s(r, 22), i16s(r, 20), i16s(r, 18)];
|
||||
if Canvas::sourceless(rop) {
|
||||
cv.fill_rop(dst, rop);
|
||||
return Some(());
|
||||
}
|
||||
let src = [i16s(r, 16), i16s(r, 14), i16s(r, 12), i16s(r, 10)];
|
||||
dib_blit(cv, r, 26, rop, dst, src)
|
||||
}
|
||||
|
||||
/// A DIB at `at`, stretched from source rectangle `src` into `dst` (both in logical units, pixels for `src`).
|
||||
/// The raster operation `rop` decides how the pixels combine (see [`dib::prepare`]).
|
||||
fn dib_blit(cv: &mut Canvas, r: Bytes, at: usize, rop: u32, dst: [f64; 4], src: [f64; 4]) -> Option<()> {
|
||||
let dib = r.tail(at)?;
|
||||
let n = dib::info_len(dib)?;
|
||||
let info = dib.slice(0, n)?;
|
||||
let bits = dib.tail(n)?;
|
||||
let mut img = dib::decode(Bytes(info), bits, false)?;
|
||||
dib::prepare(&mut img, dib::Blend::Rop(rop));
|
||||
let (img, dst) = dib::clip_blit(&img, src.map(|v| v as i64), dst)?;
|
||||
cv.bitmap(dst, img);
|
||||
Some(())
|
||||
}
|
||||
@@ -392,6 +392,9 @@ pub struct Chart {
|
||||
/// Original chart XML part kept for round-trip of unmodelled options.
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub raw: Option<String>,
|
||||
/// The chart was too large to model: `raw` and `kept` are written back unchanged on save.
|
||||
#[serde(skip_serializing_if = "std::ops::Not::not")]
|
||||
pub verbatim: bool,
|
||||
/// Package parts the original chart still points at (embedded workbook, styles, drawings).
|
||||
/// The PPTX writer clears this before comparing modelled state. Native saves keep it too.
|
||||
#[serde(skip_serializing_if = "ChartKept::is_empty")]
|
||||
@@ -485,6 +488,7 @@ impl Default for Chart {
|
||||
bin_count: None,
|
||||
bin_size: None,
|
||||
raw: None,
|
||||
verbatim: false,
|
||||
kept: ChartKept::default(),
|
||||
}
|
||||
}
|
||||
@@ -1311,6 +1315,7 @@ impl Chart {
|
||||
series: vec![],
|
||||
gap_width: 0.0,
|
||||
raw: None,
|
||||
verbatim: false,
|
||||
subtotals: vec![],
|
||||
..self.clone()
|
||||
};
|
||||
|
||||
@@ -498,6 +498,11 @@ pub fn next_case(s: &str) -> &'static str {
|
||||
}
|
||||
}
|
||||
|
||||
/// Whether the `.` at `i` sits between two ASCII digits (a decimal point or version number).
|
||||
fn is_decimal_point(all: &[char], i: usize) -> bool {
|
||||
i > 0 && all[i - 1].is_ascii_digit() && all.get(i + 1).is_some_and(|n| n.is_ascii_digit())
|
||||
}
|
||||
|
||||
/// Change case: `upper`, `lower`, `sentence`, `title`, `toggle`.
|
||||
pub fn change_case(s: &str, mode: &str) -> String {
|
||||
change_case_parts(&[s], mode).concat()
|
||||
@@ -532,7 +537,7 @@ pub fn change_case_parts(parts: &[&str], mode: &str) -> Vec<String> {
|
||||
} else {
|
||||
out.extend(c.to_lowercase());
|
||||
}
|
||||
if (title && c.is_whitespace()) || (!title && matches!(c, '.' | '!' | '?')) {
|
||||
if (title && c.is_whitespace()) || (!title && matches!(c, '.' | '!' | '?') && !(c == '.' && is_decimal_point(&all, i))) {
|
||||
start = true;
|
||||
}
|
||||
}
|
||||
@@ -558,6 +563,13 @@ mod tests {
|
||||
assert_eq!(change_case("ΟΔΟΣ", "lower"), "οδος");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn sentence_case_decimal_point_is_not_a_boundary() {
|
||||
assert_eq!(change_case("I HAVE 3.5 APPLES", "sentence"), "I have 3.5 apples");
|
||||
assert_eq!(change_case_parts(&["V 1.", "2 BETA"], "sentence").concat(), "V 1.2 beta");
|
||||
assert_eq!(change_case("HELLO. WORLD", "sentence"), "Hello. World");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn title_sigma_across_runs_is_not_final() {
|
||||
assert_eq!(change_case_parts(&["ΟΔΟΣ", "ΑΣ"], "title"), ["Οδοσ", "ας"]);
|
||||
|
||||
@@ -314,6 +314,7 @@ pub fn body(ctx: &Ctx, shape: &Shape) -> BodyProps {
|
||||
compat_ln_spc: None,
|
||||
vert_overflow: None,
|
||||
horz_overflow: None,
|
||||
raw3d: None,
|
||||
});
|
||||
b
|
||||
}
|
||||
|
||||
@@ -124,6 +124,9 @@ pub struct BodyProps {
|
||||
pub compat_ln_spc: Option<bool>,
|
||||
pub vert_overflow: Option<TextOverflow>,
|
||||
pub horz_overflow: Option<TextOverflow>,
|
||||
/// Unmodelled 3-D XML on the text body (`a:scene3d`, `a:sp3d`, `a:flatTx`).
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub raw3d: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
||||
@@ -187,6 +190,9 @@ impl BodyProps {
|
||||
self.compat_ln_spc = self.compat_ln_spc.or(p.compat_ln_spc);
|
||||
self.vert_overflow = self.vert_overflow.or(p.vert_overflow);
|
||||
self.horz_overflow = self.horz_overflow.or(p.horz_overflow);
|
||||
if self.raw3d.is_none() {
|
||||
self.raw3d.clone_from(&p.raw3d);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -337,16 +337,31 @@ fn fit_moving_avg(pairs: &[(f64, f64)], period: usize) -> Option<(Model, Vec<f64
|
||||
|
||||
#[allow(clippy::needless_range_loop)]
|
||||
fn polyfit_with_intercept(xs: &[f64], ys: &[f64], order: usize, b0: f64) -> Option<Vec<f64>> {
|
||||
// Fit `y - b0 = (x / sx) * q(u)` with `u = (x - xm) / su` and `q` of degree `order - 1`. The
|
||||
// basis `(x / sx) * u^j` spans the same polynomials as `x^(j + 1)`, but keeps the pivots in
|
||||
// `solve` independent of the units and the offset of x.
|
||||
let n = xs.len() as f64;
|
||||
let sx = xs.iter().fold(0.0_f64, |acc, x| acc.max(x.abs()));
|
||||
if sx <= 0.0 || !sx.is_finite() {
|
||||
return None;
|
||||
}
|
||||
let xm = xs.iter().sum::<f64>() / n;
|
||||
let su = match xs.iter().fold(0.0_f64, |acc, x| acc.max((x - xm).abs())) {
|
||||
s if s > 0.0 && s.is_finite() => s,
|
||||
_ => 1.0,
|
||||
};
|
||||
let m = order;
|
||||
let mut ata = vec![vec![0.0; m]; m];
|
||||
let mut atb = vec![0.0; m];
|
||||
for (x, y) in xs.iter().zip(ys) {
|
||||
let v = x / sx;
|
||||
let u = (x - xm) / su;
|
||||
let dy = y - b0;
|
||||
for i in 0..m {
|
||||
let xi = x.powi((i + 1) as i32);
|
||||
let xi = v * u.powi(i as i32);
|
||||
atb[i] += dy * xi;
|
||||
for j in i..m {
|
||||
ata[i][j] += xi * x.powi((j + 1) as i32);
|
||||
ata[i][j] += xi * v * u.powi(j as i32);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -355,10 +370,11 @@ fn polyfit_with_intercept(xs: &[f64], ys: &[f64], order: usize, b0: f64) -> Opti
|
||||
ata[i][j] = ata[j][i];
|
||||
}
|
||||
}
|
||||
let solved = solve(ata, atb)?;
|
||||
let centred: Vec<f64> = solve(ata, atb)?.iter().enumerate().map(|(k, c)| c / su.powi(k as i32)).collect();
|
||||
let q = expand_shift(¢red, xm);
|
||||
let mut coeff = Vec::with_capacity(order + 1);
|
||||
coeff.push(b0);
|
||||
coeff.extend(solved);
|
||||
coeff.extend(q.iter().map(|c| c / sx));
|
||||
coeff.iter().all(|v| v.is_finite()).then_some(coeff)
|
||||
}
|
||||
|
||||
@@ -374,7 +390,13 @@ fn polyfit(xs: &[f64], ys: &[f64], order: usize, intercept: Option<f64>) -> Opti
|
||||
return polyfit_with_intercept(xs, ys, order, b0);
|
||||
}
|
||||
let xm = xs.iter().sum::<f64>() / n as f64;
|
||||
let us: Vec<f64> = xs.iter().map(|x| x - xm).collect();
|
||||
// Scale the centred x to unit range so the pivot threshold in `solve` does not depend on the
|
||||
// units of x.
|
||||
let s = xs.iter().fold(0.0_f64, |acc, x| acc.max((x - xm).abs()));
|
||||
if s <= 0.0 || !s.is_finite() {
|
||||
return None;
|
||||
}
|
||||
let us: Vec<f64> = xs.iter().map(|x| (x - xm) / s).collect();
|
||||
let m = order + 1;
|
||||
let mut ata = vec![vec![0.0; m]; m];
|
||||
let mut atb = vec![0.0; m];
|
||||
@@ -393,7 +415,7 @@ fn polyfit(xs: &[f64], ys: &[f64], order: usize, intercept: Option<f64>) -> Opti
|
||||
ata[i][j] = ata[j][i];
|
||||
}
|
||||
}
|
||||
let centred = solve(ata, atb)?;
|
||||
let centred: Vec<f64> = solve(ata, atb)?.iter().enumerate().map(|(k, c)| c / s.powi(k as i32)).collect();
|
||||
let coeff = expand_shift(¢red, xm);
|
||||
coeff.iter().all(|v| v.is_finite()).then_some(coeff)
|
||||
}
|
||||
@@ -571,6 +593,49 @@ mod tests {
|
||||
assert!((fit.y(3e-10).unwrap() - 3.0).abs() < 1e-6);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn poly_fits_tiny_x_values() {
|
||||
let xs = [1e-5, 2e-5, 3e-5, 4e-5, 5e-5];
|
||||
let f = |x: f64| 1.0 + 2.0 * x + 3.0 * x * x;
|
||||
let ys: Vec<f64> = xs.iter().map(|x| f(*x)).collect();
|
||||
let fit = line(TrendlineType::Poly).fit(&xs, &ys).expect("poly fit of tiny x values");
|
||||
assert!((fit.y(3e-5).unwrap() - f(3e-5)).abs() < 1e-9);
|
||||
let big: Vec<f64> = xs.iter().map(|x| x * 1e5).collect();
|
||||
let scaled = line(TrendlineType::Poly).fit(&big, &ys).expect("poly fit of scaled x values");
|
||||
for x in xs {
|
||||
assert!((fit.y(x).unwrap() - scaled.y(x * 1e5).unwrap()).abs() < 1e-9, "{x}");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn poly_with_intercept_fits_tiny_x_values() {
|
||||
let xs = [1e-5, 2e-5, 3e-5, 4e-5, 5e-5];
|
||||
let f = |x: f64| 1.0 + 2.0 * x + 3.0 * x * x;
|
||||
let ys: Vec<f64> = xs.iter().map(|x| f(*x)).collect();
|
||||
let mut poly = line(TrendlineType::Poly);
|
||||
poly.intercept = Some(1.0);
|
||||
let fit = poly.fit(&xs, &ys).expect("poly fit with intercept of tiny x values");
|
||||
assert!((fit.y(3e-5).unwrap() - f(3e-5)).abs() < 1e-9);
|
||||
let big: Vec<f64> = xs.iter().map(|x| x * 1e5).collect();
|
||||
let scaled = poly.fit(&big, &ys).expect("poly fit with intercept of scaled x values");
|
||||
for x in xs {
|
||||
assert!((fit.y(x).unwrap() - scaled.y(x * 1e5).unwrap()).abs() < 1e-9, "{x}");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn poly_with_intercept_fits_offset_but_narrow_x_range() {
|
||||
let xs = [45000.00, 45000.01, 45000.02, 45000.03, 45000.04];
|
||||
let f = |x: f64| 1.0 + x * (1.0 + 100.0 * (x - 45000.02));
|
||||
let ys: Vec<f64> = xs.iter().map(|x| f(*x)).collect();
|
||||
let mut poly = line(TrendlineType::Poly);
|
||||
poly.intercept = Some(1.0);
|
||||
let fit = poly.fit(&xs, &ys).expect("poly fit with intercept of offset x values");
|
||||
for x in xs {
|
||||
assert!((fit.y(x).unwrap() - f(x)).abs() < 1e-2, "{x}");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn linear_fits_offset_but_narrow_x_range() {
|
||||
let xs = [45000.00, 45000.01, 45000.02, 45000.03, 45000.04];
|
||||
|
||||
@@ -1,12 +0,0 @@
|
||||
[package]
|
||||
name = "deckcraft-ogg"
|
||||
description = "Clean-room Ogg (RFC 3533) demuxer with Ogg Opus (RFC 7845) and Ogg Vorbis stream mapping"
|
||||
version.workspace = true
|
||||
edition.workspace = true
|
||||
license.workspace = true
|
||||
rust-version.workspace = true
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
||||
[dependencies]
|
||||
@@ -1,581 +0,0 @@
|
||||
//! Clean-room Ogg demuxer, written from RFC 3533 (the Ogg encapsulation format), RFC 7845 (Ogg
|
||||
//! Opus) and the Vorbis I specification's Ogg mapping (section A).
|
||||
//!
|
||||
//! [`open`] reads every page (capture pattern, header, lacing values, CRC-32 checked; damaged
|
||||
//! regions are skipped by searching for the next `OggS`), groups them by logical bitstream and
|
||||
//! assembles packets — including packets that continue across pages — as lists of byte ranges,
|
||||
//! with the granule position of the page each packet completes on. Codec headers (Opus:
|
||||
//! `OpusHead` + `OpusTags`; Vorbis: identification, comment, setup) are collected separately from
|
||||
//! the audio packets.
|
||||
//!
|
||||
//! [`OpusTiming`] implements RFC 7845 §4: packet start positions from granule positions and
|
||||
//! TOC-derived durations, the pre-skip, and end trimming of the final page, so a player can seek
|
||||
//! to any 48 kHz sample.
|
||||
//!
|
||||
//! Layer L0: no dependencies beyond `std`, no `unsafe`, builds for `wasm32-unknown-unknown`.
|
||||
|
||||
#![cfg_attr(not(test), deny(clippy::unwrap_used, clippy::expect_used, clippy::panic, clippy::unimplemented, clippy::todo, clippy::unreachable))]
|
||||
|
||||
use std::collections::HashMap;
|
||||
use std::fmt;
|
||||
use std::io;
|
||||
|
||||
/// Random-access, read-only byte source (same shape as the other FilmCraft demuxers').
|
||||
pub trait ByteSource {
|
||||
fn len(&self) -> u64;
|
||||
fn read_at(&self, offset: u64, buf: &mut [u8]) -> io::Result<()>;
|
||||
fn is_empty(&self) -> bool {
|
||||
self.len() == 0
|
||||
}
|
||||
}
|
||||
|
||||
impl ByteSource for [u8] {
|
||||
fn len(&self) -> u64 {
|
||||
<[u8]>::len(self) as u64
|
||||
}
|
||||
fn read_at(&self, offset: u64, buf: &mut [u8]) -> io::Result<()> {
|
||||
let eof = || io::Error::new(io::ErrorKind::UnexpectedEof, "read past end");
|
||||
let a = usize::try_from(offset).map_err(|_| eof())?;
|
||||
buf.copy_from_slice(self.get(a..a.checked_add(buf.len()).ok_or_else(eof)?).ok_or_else(eof)?);
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl ByteSource for Vec<u8> {
|
||||
fn len(&self) -> u64 {
|
||||
self.as_slice().len() as u64
|
||||
}
|
||||
fn read_at(&self, offset: u64, buf: &mut [u8]) -> io::Result<()> {
|
||||
self.as_slice().read_at(offset, buf)
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: ByteSource + ?Sized> ByteSource for &T {
|
||||
fn len(&self) -> u64 {
|
||||
(**self).len()
|
||||
}
|
||||
fn read_at(&self, offset: u64, buf: &mut [u8]) -> io::Result<()> {
|
||||
(**self).read_at(offset, buf)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum Error {
|
||||
Io(io::Error),
|
||||
/// No Ogg page found.
|
||||
NotOgg,
|
||||
Invalid(String),
|
||||
}
|
||||
|
||||
impl fmt::Display for Error {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
match self {
|
||||
Error::Io(e) => write!(f, "I/O error: {e}"),
|
||||
Error::NotOgg => write!(f, "not an Ogg file"),
|
||||
Error::Invalid(s) => write!(f, "invalid Ogg: {s}"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl std::error::Error for Error {}
|
||||
|
||||
impl From<io::Error> for Error {
|
||||
fn from(e: io::Error) -> Self {
|
||||
Error::Io(e)
|
||||
}
|
||||
}
|
||||
|
||||
pub type Result<T> = std::result::Result<T, Error>;
|
||||
|
||||
/// The capture pattern at the start of a file.
|
||||
pub fn sniff(head: &[u8]) -> bool {
|
||||
head.len() >= 27 && &head[..4] == b"OggS" && head[4] == 0
|
||||
}
|
||||
|
||||
/// CRC-32 of RFC 3533 §6: polynomial 0x04C11DB7, initial value 0, no reflection, no final XOR.
|
||||
pub fn crc32(data: &[u8]) -> u32 {
|
||||
static TABLE: std::sync::OnceLock<[u32; 256]> = std::sync::OnceLock::new();
|
||||
let t = TABLE.get_or_init(|| {
|
||||
let mut t = [0u32; 256];
|
||||
for (i, e) in t.iter_mut().enumerate() {
|
||||
let mut r = (i as u32) << 24;
|
||||
for _ in 0..8 {
|
||||
r = if r & 0x8000_0000 != 0 { (r << 1) ^ 0x04C1_1DB7 } else { r << 1 };
|
||||
}
|
||||
*e = r;
|
||||
}
|
||||
t
|
||||
});
|
||||
data.iter().fold(0u32, |c, &b| (c << 8) ^ t[((c >> 24) as u8 ^ b) as usize])
|
||||
}
|
||||
|
||||
/// The codec of a logical bitstream (from its first packet).
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
pub enum Codec {
|
||||
Opus,
|
||||
Vorbis,
|
||||
Flac,
|
||||
Theora,
|
||||
Speex,
|
||||
Unknown,
|
||||
}
|
||||
|
||||
impl Codec {
|
||||
fn of(first: &[u8]) -> Codec {
|
||||
if first.starts_with(b"OpusHead") {
|
||||
Codec::Opus
|
||||
} else if first.starts_with(b"\x01vorbis") {
|
||||
Codec::Vorbis
|
||||
} else if first.starts_with(b"\x7fFLAC") {
|
||||
Codec::Flac
|
||||
} else if first.starts_with(b"\x80theora") {
|
||||
Codec::Theora
|
||||
} else if first.starts_with(b"Speex ") {
|
||||
Codec::Speex
|
||||
} else {
|
||||
Codec::Unknown
|
||||
}
|
||||
}
|
||||
/// Header packets before the audio packets.
|
||||
fn header_count(self) -> usize {
|
||||
match self {
|
||||
Codec::Opus => 2,
|
||||
Codec::Vorbis | Codec::Theora => 3,
|
||||
Codec::Speex => 2,
|
||||
Codec::Flac | Codec::Unknown => 1,
|
||||
}
|
||||
}
|
||||
pub fn name(self) -> &'static str {
|
||||
match self {
|
||||
Codec::Opus => "Opus",
|
||||
Codec::Vorbis => "Vorbis",
|
||||
Codec::Flac => "FLAC",
|
||||
Codec::Theora => "Theora",
|
||||
Codec::Speex => "Speex",
|
||||
Codec::Unknown => "unknown",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A packet: its byte ranges (a packet may span pages) and the granule position of the page it
|
||||
/// completes on when it is that page's last completed packet.
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub struct Packet {
|
||||
pub parts: Vec<(u64, u32)>,
|
||||
pub size: u32,
|
||||
/// Granule position of the completing page if this is the last packet completed on it.
|
||||
pub granule: Option<i64>,
|
||||
/// Index of the page (within the stream) the packet completes on.
|
||||
pub page: u32,
|
||||
/// The packet completes on the stream's end-of-stream page.
|
||||
pub eos: bool,
|
||||
/// The first bytes of the packet (TOC for Opus, packet type for Vorbis).
|
||||
pub prefix: [u8; 2],
|
||||
}
|
||||
|
||||
/// A logical bitstream.
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct Stream {
|
||||
pub serial: u32,
|
||||
pub codec: Codec,
|
||||
/// Codec header packets (contents).
|
||||
pub headers: Vec<Vec<u8>>,
|
||||
/// Data packets after the headers.
|
||||
pub packets: Vec<Packet>,
|
||||
pub pages: u32,
|
||||
/// Granule position of the last page with one.
|
||||
pub last_granule: Option<i64>,
|
||||
pub saw_eos: bool,
|
||||
}
|
||||
|
||||
/// An opened Ogg file.
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct OggFile {
|
||||
pub streams: Vec<Stream>,
|
||||
/// Pages dropped for a bad CRC, lost sync, truncation…
|
||||
pub warnings: Vec<String>,
|
||||
}
|
||||
|
||||
/// Source bytes damage recovery may read per file: the windows the scan for the next capture
|
||||
/// pattern fills and the pages read only to find their CRC wrong.
|
||||
const RECOVERY_BUDGET: u64 = 32 << 20;
|
||||
/// Warnings kept per file (not counting the one about the budget); the rest are counted in one last entry.
|
||||
const MAX_WARNINGS: usize = 256;
|
||||
|
||||
/// Record a warning, keeping at most `MAX_WARNINGS` and one entry saying the rest were dropped.
|
||||
fn warn(warnings: &mut Vec<String>, msg: String) {
|
||||
match warnings.len().cmp(&MAX_WARNINGS) {
|
||||
std::cmp::Ordering::Less => warnings.push(msg),
|
||||
std::cmp::Ordering::Equal => warnings.push("further warnings omitted".into()),
|
||||
std::cmp::Ordering::Greater => {}
|
||||
}
|
||||
}
|
||||
|
||||
struct Partial {
|
||||
parts: Vec<(u64, u32)>,
|
||||
size: u32,
|
||||
prefix: Vec<u8>,
|
||||
}
|
||||
|
||||
/// Read every page and assemble the packets of every logical bitstream.
|
||||
pub fn open(src: &(impl ByteSource + ?Sized)) -> Result<OggFile> {
|
||||
let len = src.len();
|
||||
let mut streams: Vec<Stream> = Vec::new();
|
||||
let mut by_serial: HashMap<u32, usize> = HashMap::new();
|
||||
let mut partial: HashMap<u32, Partial> = HashMap::new();
|
||||
let mut warnings = Vec::new();
|
||||
let mut pos = 0u64;
|
||||
let mut found_any = false;
|
||||
let mut hdr = [0u8; 27 + 255];
|
||||
let mut scanner = CaptureScanner::new();
|
||||
while pos + 27 <= len {
|
||||
src.read_at(pos, &mut hdr[..27])?;
|
||||
if &hdr[..4] != b"OggS" || hdr[4] != 0 {
|
||||
match scanner.find(src, pos + 1, len)? {
|
||||
Some(p) => {
|
||||
if found_any {
|
||||
warn(&mut warnings, format!("lost sync at byte {pos}; resumed at {p}"));
|
||||
}
|
||||
pos = p;
|
||||
continue;
|
||||
}
|
||||
None => break,
|
||||
}
|
||||
}
|
||||
let nseg = hdr[26] as usize;
|
||||
if pos + 27 + nseg as u64 > len {
|
||||
warn(&mut warnings, format!("truncated page header at byte {pos}"));
|
||||
break;
|
||||
}
|
||||
src.read_at(pos + 27, &mut hdr[27..27 + nseg])?;
|
||||
let body_len: u64 = hdr[27..27 + nseg].iter().map(|&b| b as u64).sum();
|
||||
let page_len = 27 + nseg as u64 + body_len;
|
||||
if pos + page_len > len {
|
||||
warn(&mut warnings, format!("truncated page at byte {pos}"));
|
||||
break;
|
||||
}
|
||||
let mut page = vec![0u8; page_len as usize];
|
||||
src.read_at(pos, &mut page)?;
|
||||
let stored_crc = u32::from_le_bytes([page[22], page[23], page[24], page[25]]);
|
||||
page[22..26].fill(0);
|
||||
if crc32(&page) != stored_crc {
|
||||
warn(&mut warnings, format!("bad CRC on page at byte {pos}"));
|
||||
// a page read only to find it damaged is recovery work, charged like the scan's windows
|
||||
if scanner.charge(page_len) {
|
||||
break;
|
||||
}
|
||||
pos += 1;
|
||||
continue;
|
||||
}
|
||||
found_any = true;
|
||||
let flags = page[5];
|
||||
let granule = i64::from_le_bytes(page[6..14].try_into().unwrap_or([0; 8]));
|
||||
let serial = u32::from_le_bytes([page[14], page[15], page[16], page[17]]);
|
||||
let lacing = page[27..27 + nseg].to_vec();
|
||||
let body_off = pos + 27 + nseg as u64;
|
||||
let si = *by_serial.entry(serial).or_insert_with(|| {
|
||||
streams.push(Stream {
|
||||
serial,
|
||||
codec: Codec::Unknown,
|
||||
headers: Vec::new(),
|
||||
packets: Vec::new(),
|
||||
pages: 0,
|
||||
last_granule: None,
|
||||
saw_eos: false,
|
||||
});
|
||||
streams.len() - 1
|
||||
});
|
||||
let st = &mut streams[si];
|
||||
let page_index = st.pages;
|
||||
st.pages += 1;
|
||||
if flags & 0x04 != 0 {
|
||||
st.saw_eos = true;
|
||||
}
|
||||
// a continued page without a pending packet (lost a page): drop the fragment
|
||||
let continued = flags & 0x01 != 0;
|
||||
if !continued && partial.remove(&serial).is_some() {
|
||||
warn(&mut warnings, format!("incomplete packet before page at byte {pos}"));
|
||||
}
|
||||
let mut completed: Vec<Packet> = Vec::new();
|
||||
let mut off = 0u64;
|
||||
let mut seg_start = 0u64;
|
||||
let mut skip_fragment = continued && !partial.contains_key(&serial);
|
||||
for (k, &l) in lacing.iter().enumerate() {
|
||||
off += l as u64;
|
||||
if l < 255 || k + 1 == nseg {
|
||||
let (a, n) = (seg_start, off - seg_start);
|
||||
seg_start = off;
|
||||
if skip_fragment {
|
||||
if l < 255 {
|
||||
skip_fragment = false;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
let p = partial.entry(serial).or_insert_with(|| Partial { parts: Vec::new(), size: 0, prefix: Vec::new() });
|
||||
if n > 0 {
|
||||
p.parts.push((body_off + a, n as u32));
|
||||
p.size += n as u32;
|
||||
let s = (body_off - pos + a) as usize;
|
||||
let need = 2usize.saturating_sub(p.prefix.len()).min(n as usize);
|
||||
p.prefix.extend_from_slice(&page[s..s + need]);
|
||||
}
|
||||
if l < 255
|
||||
&& let Some(p) = partial.remove(&serial)
|
||||
{
|
||||
let mut prefix = [0u8; 2];
|
||||
prefix[..p.prefix.len().min(2)].copy_from_slice(&p.prefix[..p.prefix.len().min(2)]);
|
||||
completed.push(Packet { parts: p.parts, size: p.size, granule: None, page: page_index, eos: flags & 0x04 != 0, prefix });
|
||||
}
|
||||
}
|
||||
}
|
||||
if granule != -1 {
|
||||
st.last_granule = Some(granule);
|
||||
if let Some(last) = completed.last_mut() {
|
||||
last.granule = Some(granule);
|
||||
}
|
||||
}
|
||||
for pk in completed {
|
||||
if st.headers.is_empty() && st.packets.is_empty() {
|
||||
let data = read_parts(src, &pk.parts)?;
|
||||
st.codec = Codec::of(&data);
|
||||
}
|
||||
if st.headers.len() < st.codec.header_count() && st.packets.is_empty() {
|
||||
st.headers.push(read_parts(src, &pk.parts)?);
|
||||
} else {
|
||||
st.packets.push(pk);
|
||||
}
|
||||
}
|
||||
pos += page_len;
|
||||
}
|
||||
if !found_any {
|
||||
return Err(Error::NotOgg);
|
||||
}
|
||||
if !partial.is_empty() {
|
||||
warn(&mut warnings, "file ends inside a packet".into());
|
||||
}
|
||||
if scanner.spent() {
|
||||
// once per file and last, so it is not subject to the cap on the others
|
||||
warnings.push("recovery budget spent; the rest of the damaged data was not searched".into());
|
||||
}
|
||||
Ok(OggFile { streams, warnings })
|
||||
}
|
||||
|
||||
struct CaptureScanner {
|
||||
buf: Vec<u8>,
|
||||
buf_start: u64,
|
||||
budget: u64,
|
||||
}
|
||||
|
||||
impl CaptureScanner {
|
||||
fn new() -> Self {
|
||||
Self { buf: Vec::new(), buf_start: 0, budget: RECOVERY_BUDGET }
|
||||
}
|
||||
|
||||
/// Charge `bytes` of recovery work; true once the budget is spent.
|
||||
fn charge(&mut self, bytes: u64) -> bool {
|
||||
self.budget = self.budget.saturating_sub(bytes);
|
||||
self.spent()
|
||||
}
|
||||
|
||||
/// Nothing left to search with: fewer bytes than a capture pattern can still be charged for.
|
||||
fn spent(&self) -> bool {
|
||||
self.budget < 4
|
||||
}
|
||||
|
||||
fn find(&mut self, src: &(impl ByteSource + ?Sized), from: u64, len: u64) -> Result<Option<u64>> {
|
||||
let mut at = from;
|
||||
while at + 4 <= len {
|
||||
let buf_end = self.buf_start + self.buf.len() as u64;
|
||||
if at >= self.buf_start && at + 4 <= buf_end {
|
||||
let off = (at - self.buf_start) as usize;
|
||||
if let Some(i) = self.buf[off..].windows(4).position(|w| w == b"OggS") {
|
||||
return Ok(Some(at + i as u64));
|
||||
}
|
||||
at = buf_end.saturating_sub(3);
|
||||
continue;
|
||||
}
|
||||
if self.spent() {
|
||||
return Ok(None);
|
||||
}
|
||||
let n = ((len - at) as usize).min(64 * 1024).min(usize::try_from(self.budget).unwrap_or(usize::MAX));
|
||||
if n < 4 {
|
||||
break;
|
||||
}
|
||||
self.buf.resize(n, 0);
|
||||
src.read_at(at, &mut self.buf)?;
|
||||
self.buf_start = at;
|
||||
self.budget = self.budget.saturating_sub(n as u64);
|
||||
if let Some(i) = self.buf.windows(4).position(|w| w == b"OggS") {
|
||||
return Ok(Some(at + i as u64));
|
||||
}
|
||||
at += (n - 3) as u64;
|
||||
}
|
||||
Ok(None)
|
||||
}
|
||||
}
|
||||
|
||||
fn read_parts(src: &(impl ByteSource + ?Sized), parts: &[(u64, u32)]) -> Result<Vec<u8>> {
|
||||
// Ranges come from a scan of some source; check them against this one before sizing a buffer.
|
||||
let len = src.len();
|
||||
if parts.iter().any(|&(o, n)| o.checked_add(u64::from(n)).is_none_or(|end| end > len)) {
|
||||
return Err(Error::Invalid("packet range lies outside the source".into()));
|
||||
}
|
||||
let mut v = Vec::with_capacity(parts.iter().map(|p| p.1 as usize).sum());
|
||||
for &(o, n) in parts {
|
||||
let a = v.len();
|
||||
v.resize(a + n as usize, 0);
|
||||
src.read_at(o, &mut v[a..])?;
|
||||
}
|
||||
Ok(v)
|
||||
}
|
||||
|
||||
impl OggFile {
|
||||
/// The first stream of `codec`.
|
||||
pub fn stream_of(&self, codec: Codec) -> Option<usize> {
|
||||
self.streams.iter().position(|s| s.codec == codec)
|
||||
}
|
||||
/// The bytes of packet `i` of stream `s`.
|
||||
pub fn read_packet(&self, src: &(impl ByteSource + ?Sized), s: usize, i: usize) -> Result<Vec<u8>> {
|
||||
let p = self.streams.get(s).and_then(|st| st.packets.get(i)).ok_or_else(|| Error::Invalid(format!("no packet {i} in stream {s}")))?;
|
||||
read_parts(src, &p.parts)
|
||||
}
|
||||
}
|
||||
|
||||
/// Duration of an Opus packet in 48 kHz samples from its TOC (RFC 6716 §3.1); `None` if invalid.
|
||||
pub fn opus_packet_samples(prefix: [u8; 2], size: u32) -> Option<u32> {
|
||||
if size == 0 {
|
||||
return None;
|
||||
}
|
||||
let toc = prefix[0];
|
||||
let config = toc >> 3;
|
||||
let frame = match config {
|
||||
0..=11 => [480, 960, 1920, 2880][(config & 3) as usize],
|
||||
12..=15 => [480, 960][(config & 1) as usize],
|
||||
_ => [120, 240, 480, 960][(config & 3) as usize],
|
||||
};
|
||||
let frames = match toc & 3 {
|
||||
0 => 1,
|
||||
1 | 2 => 2,
|
||||
_ => {
|
||||
if size < 2 {
|
||||
return None;
|
||||
}
|
||||
(prefix[1] & 0x3F) as u32
|
||||
}
|
||||
};
|
||||
let n = frame * frames;
|
||||
(n > 0 && n <= 5760).then_some(n)
|
||||
}
|
||||
|
||||
/// RFC 7845 §4 timing of an Opus stream.
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub struct OpusTiming {
|
||||
pub pre_skip: u32,
|
||||
/// Start of each packet's decoded output in output samples (48 kHz): 0 is the first sample
|
||||
/// after the pre-skip; the pre-skip samples have negative positions.
|
||||
pub starts: Vec<i64>,
|
||||
/// Decoded samples of each packet.
|
||||
pub durations: Vec<u32>,
|
||||
/// Playable samples (end trimming applied).
|
||||
pub total: i64,
|
||||
}
|
||||
|
||||
fn timing_add(a: i64, b: i64) -> Result<i64> {
|
||||
a.checked_add(b).ok_or_else(|| Error::Invalid("opus timing overflow".into()))
|
||||
}
|
||||
|
||||
fn timing_sub(a: i64, b: i64) -> Result<i64> {
|
||||
a.checked_sub(b).ok_or_else(|| Error::Invalid("opus timing overflow".into()))
|
||||
}
|
||||
|
||||
impl OpusTiming {
|
||||
/// Timing of the Opus stream `s` (`pre_skip` from its `OpusHead`).
|
||||
///
|
||||
/// A granule or a run of packet durations that does not fit in `i64` is an error. Ordinary
|
||||
/// files, including a truncated tail and end trimming, are unchanged.
|
||||
pub fn of(stream: &Stream, pre_skip: u32) -> Result<OpusTiming> {
|
||||
let durations: Vec<u32> = stream.packets.iter().map(|p| opus_packet_samples(p.prefix, p.size).unwrap_or(0)).collect();
|
||||
let n = durations.len();
|
||||
let mut ends: Vec<Option<i64>> = vec![None; n];
|
||||
let mut i = 0;
|
||||
let mut cursor: Option<i64> = None;
|
||||
let mut trimmed_end: Option<i64> = None;
|
||||
while i < n {
|
||||
// packets completing on the same page: i..j, the last carries the page granule
|
||||
let mut j = i;
|
||||
while j < n && stream.packets[j].granule.is_none() {
|
||||
j += 1;
|
||||
}
|
||||
if j == n {
|
||||
// trailing packets without a granule (truncated file): continue from the cursor
|
||||
let mut c = cursor.unwrap_or(0);
|
||||
for k in i..n {
|
||||
c = timing_add(c, i64::from(durations[k]))?;
|
||||
ends[k] = Some(c);
|
||||
}
|
||||
cursor = Some(c);
|
||||
break;
|
||||
}
|
||||
let Some(g) = stream.packets[j].granule else { break };
|
||||
let sum = durations[i..=j].iter().try_fold(0i64, |a, &d| timing_add(a, i64::from(d)))?;
|
||||
let forward = match cursor {
|
||||
Some(c) => Some(timing_add(c, sum)?),
|
||||
None => None,
|
||||
};
|
||||
let end = match forward {
|
||||
// end trimming on the last page (RFC 7845 §4.5): keep the forward positions
|
||||
Some(f) if stream.packets[j].eos && g < f => {
|
||||
trimmed_end = Some(g);
|
||||
f
|
||||
}
|
||||
Some(f) if f == g => f,
|
||||
// first page, or a discontinuity: trust the granule
|
||||
_ => g,
|
||||
};
|
||||
let mut e = end;
|
||||
for k in (i..=j).rev() {
|
||||
ends[k] = Some(e);
|
||||
e = timing_sub(e, i64::from(durations[k]))?;
|
||||
}
|
||||
cursor = Some(end);
|
||||
i = j + 1;
|
||||
}
|
||||
let mut raw_starts = Vec::with_capacity(n);
|
||||
for (e, &d) in ends.iter().zip(&durations) {
|
||||
raw_starts.push(timing_sub(e.unwrap_or(0), i64::from(d))?);
|
||||
}
|
||||
let base = raw_starts.first().copied().unwrap_or(0).max(0);
|
||||
let shift = timing_add(base, i64::from(pre_skip))?;
|
||||
let mut starts = Vec::with_capacity(n);
|
||||
for s in &raw_starts {
|
||||
starts.push(timing_sub(*s, shift)?);
|
||||
}
|
||||
let end_all = trimmed_end.or(cursor).unwrap_or(0);
|
||||
let total = timing_sub(end_all, shift)?.max(0);
|
||||
Ok(OpusTiming { pre_skip, starts, durations, total })
|
||||
}
|
||||
}
|
||||
|
||||
/// Vorbis identification header fields (Vorbis I §4.2.2).
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
pub struct VorbisInfo {
|
||||
pub channels: u8,
|
||||
pub sample_rate: u32,
|
||||
pub bitrate_nominal: i32,
|
||||
}
|
||||
|
||||
impl VorbisInfo {
|
||||
pub fn parse(h: &[u8]) -> Option<VorbisInfo> {
|
||||
if h.len() < 30 || !h.starts_with(b"\x01vorbis") {
|
||||
return None;
|
||||
}
|
||||
Some(VorbisInfo {
|
||||
channels: h[11],
|
||||
sample_rate: u32::from_le_bytes([h[12], h[13], h[14], h[15]]),
|
||||
bitrate_nominal: i32::from_le_bytes([h[20], h[21], h[22], h[23]]),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests;
|
||||
@@ -1,307 +0,0 @@
|
||||
//! Synthetic Ogg streams: a small page writer (RFC 3533) and Opus-like packets.
|
||||
|
||||
use std::cell::Cell;
|
||||
use std::io;
|
||||
|
||||
use super::{ByteSource, CaptureScanner, Codec, Error, OpusTiming, Packet, Stream, crc32, open, opus_packet_samples, sniff};
|
||||
|
||||
/// Write one page holding `segments` (lacing already computed by `lace`).
|
||||
fn page(out: &mut Vec<u8>, flags: u8, granule: i64, serial: u32, seq: u32, lacing: &[u8], body: &[u8]) {
|
||||
let start = out.len();
|
||||
out.extend_from_slice(b"OggS");
|
||||
out.push(0);
|
||||
out.push(flags);
|
||||
out.extend_from_slice(&granule.to_le_bytes());
|
||||
out.extend_from_slice(&serial.to_le_bytes());
|
||||
out.extend_from_slice(&seq.to_le_bytes());
|
||||
out.extend_from_slice(&[0; 4]);
|
||||
out.push(lacing.len() as u8);
|
||||
out.extend_from_slice(lacing);
|
||||
out.extend_from_slice(body);
|
||||
let crc = crc32(&out[start..]);
|
||||
out[start + 22..start + 26].copy_from_slice(&crc.to_le_bytes());
|
||||
}
|
||||
|
||||
fn lace(packets: &[&[u8]], last_continues: bool) -> (Vec<u8>, Vec<u8>) {
|
||||
let mut l = Vec::new();
|
||||
let mut b = Vec::new();
|
||||
for (i, p) in packets.iter().enumerate() {
|
||||
let mut n = p.len();
|
||||
while n >= 255 {
|
||||
l.push(255);
|
||||
n -= 255;
|
||||
}
|
||||
if !(last_continues && i + 1 == packets.len()) {
|
||||
l.push(n as u8);
|
||||
}
|
||||
b.extend_from_slice(p);
|
||||
}
|
||||
(l, b)
|
||||
}
|
||||
|
||||
fn opus_head(pre_skip: u16) -> Vec<u8> {
|
||||
let mut h = b"OpusHead".to_vec();
|
||||
h.push(1);
|
||||
h.push(2);
|
||||
h.extend_from_slice(&pre_skip.to_le_bytes());
|
||||
h.extend_from_slice(&48_000u32.to_le_bytes());
|
||||
h.extend_from_slice(&0i16.to_le_bytes());
|
||||
h.push(0);
|
||||
h
|
||||
}
|
||||
|
||||
/// A CELT 20 ms packet (960 samples): TOC config 31, code 0.
|
||||
fn pkt(i: usize, len: usize) -> Vec<u8> {
|
||||
let mut p = vec![0xF8];
|
||||
p.extend((0..len.saturating_sub(1)).map(|k| (i * 7 + k) as u8));
|
||||
p
|
||||
}
|
||||
|
||||
/// Header pages, then pages of 3 packets each; the last page's granule trims `trim` samples.
|
||||
fn opus_file(npk: usize, pre_skip: u16, trim: i64) -> Vec<u8> {
|
||||
let mut f = Vec::new();
|
||||
let head = opus_head(pre_skip);
|
||||
let (l, b) = lace(&[&head], false);
|
||||
page(&mut f, 0x02, 0, 9, 0, &l, &b);
|
||||
let tags = b"OpusTags\x00\x00\x00\x00\x00\x00\x00\x00".to_vec();
|
||||
let (l, b) = lace(&[&tags], false);
|
||||
page(&mut f, 0, 0, 9, 1, &l, &b);
|
||||
let pkts: Vec<Vec<u8>> = (0..npk).map(|i| pkt(i, 40 + i % 5)).collect();
|
||||
let mut done = 0;
|
||||
for (seq, chunk) in (2..).zip(pkts.chunks(3)) {
|
||||
done += chunk.len();
|
||||
let last = done == npk;
|
||||
let mut g = done as i64 * 960;
|
||||
if last {
|
||||
g -= trim;
|
||||
}
|
||||
let refs: Vec<&[u8]> = chunk.iter().map(Vec::as_slice).collect();
|
||||
let (l, b) = lace(&refs, false);
|
||||
page(&mut f, if last { 0x04 } else { 0 }, g, 9, seq, &l, &b);
|
||||
}
|
||||
f
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn crc_matches_known_value() {
|
||||
// CRC-32/CKSUM parameters without its final XOR (check value 0x765E7680 ^ 0xFFFFFFFF)
|
||||
assert_eq!(crc32(b"123456789"), 0x89A1_897F);
|
||||
assert_eq!(crc32(b""), 0);
|
||||
assert_ne!(crc32(b"OggS"), crc32(b"OggT"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn opus_pages_packets_and_timing() {
|
||||
let f = opus_file(10, 312, 500);
|
||||
assert!(sniff(&f));
|
||||
let o = open(&f).unwrap();
|
||||
assert!(o.warnings.is_empty(), "{:?}", o.warnings);
|
||||
let s = o.stream_of(Codec::Opus).unwrap();
|
||||
let st = &o.streams[s];
|
||||
assert_eq!(st.headers.len(), 2);
|
||||
assert_eq!(st.packets.len(), 10);
|
||||
assert!(st.saw_eos);
|
||||
assert_eq!(o.read_packet(&f, s, 4).unwrap(), pkt(4, 44));
|
||||
let t = OpusTiming::of(st, 312).unwrap();
|
||||
assert_eq!(t.durations, vec![960; 10]);
|
||||
assert_eq!(t.starts[0], -312);
|
||||
assert_eq!(t.starts[9], 9 * 960 - 312);
|
||||
// end trimming: the last page granule is 500 short
|
||||
assert_eq!(t.total, 10 * 960 - 500 - 312);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn packets_spanning_pages() {
|
||||
let mut f = Vec::new();
|
||||
let head = opus_head(0);
|
||||
let (l, b) = lace(&[&head], false);
|
||||
page(&mut f, 0x02, 0, 1, 0, &l, &b);
|
||||
let (l, b) = lace(&[b"OpusTags"], false);
|
||||
page(&mut f, 0, 0, 1, 1, &l, &b);
|
||||
let big = pkt(0, 700);
|
||||
// first 510 bytes on one page (two 255 lacing values, packet continues), rest on the next
|
||||
let (l, b) = (vec![255, 255], big[..510].to_vec());
|
||||
page(&mut f, 0, -1, 1, 2, &l, &b);
|
||||
let rest = &big[510..];
|
||||
let small = pkt(1, 30);
|
||||
let (l, b) = lace(&[rest, &small], false);
|
||||
page(&mut f, 0x01 | 0x04, 1920, 1, 3, &l, &b);
|
||||
let o = open(&f).unwrap();
|
||||
let st = &o.streams[0];
|
||||
assert_eq!(st.packets.len(), 2);
|
||||
assert_eq!(st.packets[0].parts.len(), 2);
|
||||
assert_eq!(o.read_packet(&f, 0, 0).unwrap(), big);
|
||||
assert_eq!(o.read_packet(&f, 0, 1).unwrap(), small);
|
||||
let t = OpusTiming::of(st, 0).unwrap();
|
||||
assert_eq!(t.starts, vec![0, 960]);
|
||||
assert_eq!(t.total, 1920);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn corrupt_pages_are_skipped_and_truncation_is_safe() {
|
||||
let f = opus_file(12, 312, 0);
|
||||
// flip a byte inside the third data page's body: that page fails its CRC and is dropped
|
||||
let mut g = f.clone();
|
||||
let third = g.windows(4).enumerate().filter(|(_, w)| *w == b"OggS").map(|(i, _)| i).nth(4).unwrap();
|
||||
g[third + 40] ^= 0xFF;
|
||||
let o = open(&g).unwrap();
|
||||
assert!(o.warnings.iter().any(|w| w.contains("CRC")), "{:?}", o.warnings);
|
||||
assert_eq!(o.streams[0].packets.len(), 9);
|
||||
let t = OpusTiming::of(&o.streams[0], 312).unwrap();
|
||||
// the granules after the gap put the later packets at their true positions
|
||||
assert_eq!(*t.starts.last().unwrap(), 11 * 960 - 312);
|
||||
for cut in 0..f.len() {
|
||||
if let Ok(o) = open(&f[..cut].to_vec()) {
|
||||
for st in &o.streams {
|
||||
let t = OpusTiming::of(st, 312).unwrap();
|
||||
assert_eq!(t.starts.len(), st.packets.len());
|
||||
}
|
||||
}
|
||||
}
|
||||
let mut seed = 99u64;
|
||||
for _ in 0..300 {
|
||||
let mut g = f.clone();
|
||||
seed = seed.wrapping_mul(6364136223846793005).wrapping_add(1442695040888963407);
|
||||
let at = (seed >> 33) as usize % g.len();
|
||||
g[at] = (seed >> 20) as u8;
|
||||
if let Ok(o) = open(&g) {
|
||||
for st in &o.streams {
|
||||
let _ = OpusTiming::of(st, 312);
|
||||
}
|
||||
}
|
||||
}
|
||||
assert!(matches!(open(&b"not ogg at all, no capture pattern".to_vec()), Err(Error::NotOgg)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn opus_toc_durations() {
|
||||
assert_eq!(opus_packet_samples([0x00, 0], 10), Some(480)); // SILK NB 10 ms
|
||||
assert_eq!(opus_packet_samples([0x18, 0], 10), Some(2880)); // SILK NB 60 ms
|
||||
assert_eq!(opus_packet_samples([0x78 | 1, 0], 10), Some(1920)); // hybrid 20 ms × 2
|
||||
assert_eq!(opus_packet_samples([0x80 | 3, 6], 10), Some(720)); // CELT 2.5 ms × 6
|
||||
assert_eq!(opus_packet_samples([0xF8 | 3, 7], 10), None); // 140 ms: invalid
|
||||
assert_eq!(opus_packet_samples([0xF8, 0], 0), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn opus_timing_includes_packets_after_the_last_known_granule() {
|
||||
let f = opus_file(6, 312, 0);
|
||||
let o = open(&f).unwrap();
|
||||
let mut stream = o.streams[0].clone();
|
||||
// Missing granules on complete-packet pages are non-conforming. The existing recovery
|
||||
// fallback still assigns their packet positions; total must include the same samples.
|
||||
stream.packets.last_mut().unwrap().granule = None;
|
||||
let timing = OpusTiming::of(&stream, 312).unwrap();
|
||||
assert_eq!(timing.starts, vec![-312, 648, 1608, 2568, 3528, 4488]);
|
||||
assert_eq!(timing.total, 6 * 960 - 312);
|
||||
|
||||
// With no usable granule, every complete packet still contributes its TOC duration.
|
||||
for packet in &mut stream.packets {
|
||||
packet.granule = None;
|
||||
}
|
||||
let timing = OpusTiming::of(&stream, 312).unwrap();
|
||||
assert_eq!(timing.starts, vec![-312, 648, 1608, 2568, 3528, 4488]);
|
||||
assert_eq!(timing.total, 6 * 960 - 312);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn opus_timing_rejects_a_granule_that_overflows() {
|
||||
let packet = |granule: i64| Stream {
|
||||
serial: 1,
|
||||
codec: Codec::Opus,
|
||||
headers: vec![],
|
||||
packets: vec![Packet { parts: vec![], size: 10, granule: Some(granule), page: 0, eos: true, prefix: [0x00, 0] }],
|
||||
pages: 1,
|
||||
last_granule: Some(granule),
|
||||
saw_eos: true,
|
||||
};
|
||||
let ok = OpusTiming::of(&packet(480), 0).unwrap();
|
||||
assert_eq!(ok.starts, vec![0]);
|
||||
assert_eq!(ok.total, 480);
|
||||
assert!(OpusTiming::of(&packet(i64::MIN), 0).is_err());
|
||||
}
|
||||
|
||||
/// A source that counts the bytes read from it.
|
||||
struct Counting<'a> {
|
||||
data: &'a [u8],
|
||||
read: Cell<u64>,
|
||||
}
|
||||
|
||||
impl ByteSource for Counting<'_> {
|
||||
fn len(&self) -> u64 {
|
||||
self.data.len() as u64
|
||||
}
|
||||
fn read_at(&self, offset: u64, buf: &mut [u8]) -> io::Result<()> {
|
||||
self.read.set(self.read.get() + buf.len() as u64);
|
||||
self.data.read_at(offset, buf)
|
||||
}
|
||||
}
|
||||
|
||||
/// A valid file followed by `len` bytes of 0xFF with a capture pattern every 64 bytes. Each one
|
||||
/// claims 255 full lacing values, so it reads as a page of about 64 KiB that fails its CRC.
|
||||
fn dense_fake_pages(len: usize) -> Vec<u8> {
|
||||
let mut f = opus_file(6, 312, 0);
|
||||
let start = f.len();
|
||||
f.resize(start + len, 0xFF);
|
||||
for at in (start..start + len - 64).step_by(64) {
|
||||
f[at..at + 5].copy_from_slice(b"OggS\0");
|
||||
f[at + 26] = 255;
|
||||
}
|
||||
f
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn recovery_reads_a_bounded_amount_of_a_damaged_file() {
|
||||
let f = dense_fake_pages(1 << 20);
|
||||
let src = Counting { data: &f, read: Cell::new(0) };
|
||||
let o = open(&src).unwrap();
|
||||
assert_eq!(o.streams[0].packets.len(), 6, "the good pages are kept");
|
||||
let read = src.read.get();
|
||||
assert!(read <= 40 << 20, "read {read} bytes of a {} byte file", f.len());
|
||||
assert!(o.warnings.iter().any(|w| w.contains("recovery budget")), "{:?}", o.warnings);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn warnings_are_capped() {
|
||||
// 100,000 pages with no segments and a wrong CRC, 27 bytes each
|
||||
let mut f = opus_file(6, 312, 0);
|
||||
for _ in 0..100_000 {
|
||||
let at = f.len();
|
||||
f.extend_from_slice(b"OggS\0\0");
|
||||
f.resize(at + 27, 0);
|
||||
f[at + 22] = 1;
|
||||
}
|
||||
let o = open(&f).unwrap();
|
||||
assert!(o.warnings.len() <= 257, "{} warnings", o.warnings.len());
|
||||
assert!(o.warnings.iter().any(|w| w.contains("omitted")), "{:?}", o.warnings.last());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_file_of_good_pages_is_not_charged_for_recovery() {
|
||||
let f = opus_file(2000, 312, 0);
|
||||
let src = Counting { data: &f, read: Cell::new(0) };
|
||||
let o = open(&src).unwrap();
|
||||
assert_eq!(o.streams[0].packets.len(), 2000);
|
||||
assert!(o.warnings.is_empty(), "{:?}", o.warnings);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_budget_too_small_to_search_is_spent() {
|
||||
// 1 to 3 bytes left cannot hold a capture pattern: the scan would stop without saying why
|
||||
let mut scanner = CaptureScanner::new();
|
||||
assert!(!scanner.spent());
|
||||
assert!(scanner.charge(super::RECOVERY_BUDGET - 3));
|
||||
assert!(scanner.spent());
|
||||
let f = opus_file(6, 312, 0);
|
||||
assert_eq!(scanner.find(&f, 0, f.len() as u64).unwrap(), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn read_packet_checks_its_ranges_against_the_source_before_allocating() {
|
||||
let f = opus_file(10, 312, 500);
|
||||
let o = open(&f).unwrap();
|
||||
let s = o.stream_of(Codec::Opus).unwrap();
|
||||
// The last packet lies past the end of a source cut short: an error, not a read of missing bytes.
|
||||
let short = &f[..f.len() / 2];
|
||||
assert!(matches!(o.read_packet(&short, s, 9), Err(Error::Invalid(_))));
|
||||
}
|
||||
@@ -1,12 +0,0 @@
|
||||
[package]
|
||||
name = "deckcraft-opus"
|
||||
description = "Clean-room pure-Rust Opus decoder (RFC 6716 / RFC 8251; OpusHead, multistream surround)"
|
||||
version.workspace = true
|
||||
edition.workspace = true
|
||||
license.workspace = true
|
||||
rust-version.workspace = true
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
||||
[dependencies]
|
||||
@@ -1,678 +0,0 @@
|
||||
//! CELT layer decoder (RFC 6716 §4.3).
|
||||
|
||||
pub(crate) mod bands;
|
||||
pub(crate) mod energy;
|
||||
pub(crate) mod mdct;
|
||||
pub(crate) mod rate;
|
||||
pub(crate) mod tables;
|
||||
|
||||
use std::sync::OnceLock;
|
||||
|
||||
use bands::{anti_collapse, denormalise_bands, quant_all_bands, renormalise_vector};
|
||||
use energy::{unquant_coarse_energy, unquant_energy_finalise, unquant_fine_energy};
|
||||
use mdct::ImdctScratch;
|
||||
use rate::{Mode, compute_allocation};
|
||||
use tables::{COMB_GAINS, EBANDS, MAX_LM, NB_EBANDS, OVERLAP, PREEMPH, SHORT_MDCT, SPREAD_ICDF, TAPSET_ICDF, TF_SELECT_TABLE, TRIM_ICDF};
|
||||
|
||||
use crate::range::{BITRES, RangeDecoder};
|
||||
|
||||
const DECODE_BUFFER_SIZE: usize = 2048;
|
||||
const COMBFILTER_MINPERIOD: i32 = 15;
|
||||
const PLC_PITCH_LAG_MAX: usize = 720;
|
||||
const PLC_PITCH_LAG_MIN: usize = 100;
|
||||
|
||||
pub(crate) fn mode() -> &'static Mode {
|
||||
static MODE: OnceLock<Mode> = OnceLock::new();
|
||||
MODE.get_or_init(Mode::new)
|
||||
}
|
||||
|
||||
/// CELT decoder state for one (mono or stereo) stream.
|
||||
pub struct CeltDecoder {
|
||||
m: &'static Mode,
|
||||
/// Output channels.
|
||||
channels: usize,
|
||||
/// Channels coded in the stream.
|
||||
pub stream_channels: usize,
|
||||
downsample: usize,
|
||||
pub start: usize,
|
||||
pub end: usize,
|
||||
pub disable_inv: bool,
|
||||
pub rng: u32,
|
||||
loss_count: u32,
|
||||
skip_plc: bool,
|
||||
postfilter_period: i32,
|
||||
postfilter_period_old: i32,
|
||||
postfilter_gain: f32,
|
||||
postfilter_gain_old: f32,
|
||||
postfilter_tapset: usize,
|
||||
postfilter_tapset_old: usize,
|
||||
preemph_mem: [f32; 2],
|
||||
decode_mem: Vec<f32>,
|
||||
old_band_e: [f32; 2 * NB_EBANDS],
|
||||
old_log_e: [f32; 2 * NB_EBANDS],
|
||||
old_log_e2: [f32; 2 * NB_EBANDS],
|
||||
background_log_e: [f32; 2 * NB_EBANDS],
|
||||
// scratch
|
||||
x: Vec<f32>,
|
||||
freq: Vec<f32>,
|
||||
imdct: ImdctScratch,
|
||||
pcm_scratch: Vec<f32>,
|
||||
}
|
||||
|
||||
/// Detects the silence flag, advancing the decoder to the end of the frame when set. Returns the flag and the new bit position.
|
||||
fn read_silence(dec: &mut RangeDecoder, total_bits_raw: i32) -> (bool, i32) {
|
||||
let tell = dec.tell();
|
||||
let silence = tell >= total_bits_raw || (tell == 1 && dec.bit_logp(15));
|
||||
if silence {
|
||||
// Pretend we've read all the remaining bits.
|
||||
dec.skip_to_end(total_bits_raw);
|
||||
return (true, total_bits_raw);
|
||||
}
|
||||
(false, tell)
|
||||
}
|
||||
|
||||
struct PostfilterParams {
|
||||
gain: f32,
|
||||
pitch: i32,
|
||||
tapset: usize,
|
||||
tell: i32,
|
||||
}
|
||||
|
||||
/// Reads the optional post-filter header (pitch, gain, tapset) at the start of a frame.
|
||||
fn read_postfilter_params(dec: &mut RangeDecoder, start: usize, tell: i32, total_bits_raw: i32) -> PostfilterParams {
|
||||
let mut p = PostfilterParams { gain: 0.0, pitch: 0, tapset: 0, tell };
|
||||
if start == 0 && tell + 16 <= total_bits_raw {
|
||||
if dec.bit_logp(1) {
|
||||
let octave = dec.uint(6);
|
||||
p.pitch = ((16 << octave) + dec.bits(4 + octave)) as i32 - 1;
|
||||
let qg = dec.bits(3);
|
||||
if dec.tell() + 2 <= total_bits_raw {
|
||||
p.tapset = dec.icdf(&TAPSET_ICDF, 2);
|
||||
}
|
||||
p.gain = 0.09375 * (qg + 1) as f32;
|
||||
}
|
||||
p.tell = dec.tell();
|
||||
}
|
||||
p
|
||||
}
|
||||
|
||||
/// Decodes per-band dynamic allocation boosts. Returns the offsets, the fractional bit position and the remaining bit budget.
|
||||
fn decode_dynalloc(
|
||||
dec: &mut RangeDecoder,
|
||||
start: usize,
|
||||
end: usize,
|
||||
c: usize,
|
||||
lm: usize,
|
||||
cap: &[i32],
|
||||
total_bits_raw: i32,
|
||||
) -> ([i32; NB_EBANDS], i32, i32) {
|
||||
let mut offsets = [0i32; NB_EBANDS];
|
||||
let mut dynalloc_logp = 6;
|
||||
let mut total_bits = total_bits_raw << BITRES;
|
||||
let mut tellf = dec.tell_frac() as i32;
|
||||
for i in start..end {
|
||||
let width = (c as i32 * (EBANDS[i + 1] - EBANDS[i])) << lm;
|
||||
let quanta = (width << BITRES).min((6 << BITRES).max(width));
|
||||
let mut loop_logp = dynalloc_logp;
|
||||
let mut boost = 0;
|
||||
while tellf + (loop_logp << BITRES) < total_bits && boost < cap[i] {
|
||||
let flag = dec.bit_logp(loop_logp as u32);
|
||||
tellf = dec.tell_frac() as i32;
|
||||
if !flag {
|
||||
break;
|
||||
}
|
||||
boost += quanta;
|
||||
total_bits -= quanta;
|
||||
loop_logp = 1;
|
||||
}
|
||||
offsets[i] = boost;
|
||||
if boost > 0 {
|
||||
dynalloc_logp = 2.max(dynalloc_logp - 1);
|
||||
}
|
||||
}
|
||||
(offsets, tellf, total_bits)
|
||||
}
|
||||
|
||||
impl CeltDecoder {
|
||||
/// `sample_rate` is the output rate (48000, 24000, 16000, 12000 or 8000).
|
||||
pub fn new(sample_rate: u32, channels: usize) -> CeltDecoder {
|
||||
let downsample = (48000 / sample_rate) as usize;
|
||||
let mut d = CeltDecoder {
|
||||
m: mode(),
|
||||
channels,
|
||||
stream_channels: channels,
|
||||
downsample,
|
||||
start: 0,
|
||||
end: NB_EBANDS,
|
||||
disable_inv: channels == 1,
|
||||
rng: 0,
|
||||
loss_count: 0,
|
||||
skip_plc: true,
|
||||
postfilter_period: 0,
|
||||
postfilter_period_old: 0,
|
||||
postfilter_gain: 0.0,
|
||||
postfilter_gain_old: 0.0,
|
||||
postfilter_tapset: 0,
|
||||
postfilter_tapset_old: 0,
|
||||
preemph_mem: [0.0; 2],
|
||||
decode_mem: vec![0.0; channels * (DECODE_BUFFER_SIZE + OVERLAP)],
|
||||
old_band_e: [0.0; 2 * NB_EBANDS],
|
||||
old_log_e: [0.0; 2 * NB_EBANDS],
|
||||
old_log_e2: [0.0; 2 * NB_EBANDS],
|
||||
background_log_e: [0.0; 2 * NB_EBANDS],
|
||||
x: Vec::new(),
|
||||
freq: Vec::new(),
|
||||
imdct: ImdctScratch::default(),
|
||||
pcm_scratch: Vec::new(),
|
||||
};
|
||||
d.reset();
|
||||
d
|
||||
}
|
||||
|
||||
/// `OPUS_RESET_STATE`.
|
||||
pub fn reset(&mut self) {
|
||||
self.rng = 0;
|
||||
self.loss_count = 0;
|
||||
self.skip_plc = true;
|
||||
self.postfilter_period = 0;
|
||||
self.postfilter_period_old = 0;
|
||||
self.postfilter_gain = 0.0;
|
||||
self.postfilter_gain_old = 0.0;
|
||||
self.postfilter_tapset = 0;
|
||||
self.postfilter_tapset_old = 0;
|
||||
self.preemph_mem = [0.0; 2];
|
||||
self.decode_mem.fill(0.0);
|
||||
self.old_band_e = [0.0; 2 * NB_EBANDS];
|
||||
self.old_log_e = [-28.0; 2 * NB_EBANDS];
|
||||
self.old_log_e2 = [-28.0; 2 * NB_EBANDS];
|
||||
self.background_log_e = [0.0; 2 * NB_EBANDS];
|
||||
}
|
||||
|
||||
fn mem_len(&self) -> usize {
|
||||
DECODE_BUFFER_SIZE + OVERLAP
|
||||
}
|
||||
|
||||
/// Decodes one CELT frame of `frame_size` samples (at the output rate) into interleaved `pcm`
|
||||
/// (`channels` wide). With `accum`, the output is added to `pcm`. `data = None` (or fewer than
|
||||
/// two bytes) runs the packet-loss concealment.
|
||||
pub fn decode(
|
||||
&mut self,
|
||||
data: Option<&[u8]>,
|
||||
frame_size: usize,
|
||||
pcm: &mut [f32],
|
||||
accum: bool,
|
||||
dec: Option<&mut RangeDecoder>,
|
||||
) -> Result<usize, crate::Error> {
|
||||
let n_full = frame_size * self.downsample;
|
||||
let lm = match n_full {
|
||||
120 => 0,
|
||||
240 => 1,
|
||||
480 => 2,
|
||||
960 => 3,
|
||||
_ => return Err(crate::Error::InvalidArgument("CELT frame size")),
|
||||
};
|
||||
let data = match data {
|
||||
Some(d) if d.len() > 1 => d,
|
||||
_ => {
|
||||
self.decode_lost(n_full, lm);
|
||||
self.deemphasis(n_full, pcm, accum);
|
||||
return Ok(frame_size);
|
||||
}
|
||||
};
|
||||
if data.len() > 1275 {
|
||||
return Err(crate::Error::InvalidPacket("CELT frame too long"));
|
||||
}
|
||||
match dec {
|
||||
Some(d) => self.decode_frame(d, data.len(), n_full, lm),
|
||||
None => {
|
||||
let mut own = RangeDecoder::new(data);
|
||||
self.decode_frame(&mut own, data.len(), n_full, lm)
|
||||
}
|
||||
}
|
||||
self.deemphasis(n_full, pcm, accum);
|
||||
Ok(frame_size)
|
||||
}
|
||||
|
||||
fn decode_frame(&mut self, dec: &mut RangeDecoder, len: usize, n: usize, lm: usize) {
|
||||
let cc = self.channels;
|
||||
let c = self.stream_channels;
|
||||
let m = self.m;
|
||||
let mm = 1usize << lm;
|
||||
let start = self.start;
|
||||
let end = self.end;
|
||||
let eff_end = end.min(NB_EBANDS);
|
||||
if self.loss_count == 0 {
|
||||
self.skip_plc = false;
|
||||
}
|
||||
if c == 1 {
|
||||
for i in 0..NB_EBANDS {
|
||||
self.old_band_e[i] = self.old_band_e[i].max(self.old_band_e[NB_EBANDS + i]);
|
||||
}
|
||||
}
|
||||
let total_bits_raw = len as i32 * 8;
|
||||
let (silence, mut tell) = read_silence(dec, total_bits_raw);
|
||||
let pf = read_postfilter_params(dec, start, tell, total_bits_raw);
|
||||
tell = pf.tell;
|
||||
let (postfilter_gain, postfilter_pitch, postfilter_tapset) = (pf.gain, pf.pitch, pf.tapset);
|
||||
let is_transient = if lm > 0 && tell + 3 <= total_bits_raw {
|
||||
let t = dec.bit_logp(3);
|
||||
tell = dec.tell();
|
||||
t
|
||||
} else {
|
||||
false
|
||||
};
|
||||
let short_blocks = is_transient;
|
||||
let intra = if tell + 3 <= total_bits_raw { dec.bit_logp(3) } else { false };
|
||||
unquant_coarse_energy(start, end, &mut self.old_band_e, intra, dec, c, lm);
|
||||
let mut tf_res = [0i32; NB_EBANDS];
|
||||
tf_decode(start, end, is_transient, &mut tf_res, lm, dec);
|
||||
tell = dec.tell();
|
||||
let mut spread = 2usize;
|
||||
if tell + 4 <= total_bits_raw {
|
||||
spread = dec.icdf(&SPREAD_ICDF, 5);
|
||||
}
|
||||
let cap = m.init_caps(lm, c);
|
||||
let (offsets, tellf, total_bits) = decode_dynalloc(dec, start, end, c, lm, &cap, total_bits_raw);
|
||||
let alloc_trim = if tellf + (6 << BITRES) <= total_bits { dec.icdf(&TRIM_ICDF, 7) as i32 } else { 5 };
|
||||
let mut bits = ((len as i32 * 8) << BITRES) - dec.tell_frac() as i32 - 1;
|
||||
let anti_collapse_rsv = if is_transient && lm >= 2 && bits >= ((lm as i32 + 2) << BITRES) { 1 << BITRES } else { 0 };
|
||||
bits -= anti_collapse_rsv;
|
||||
let alloc = compute_allocation(m, start, end, &offsets, &cap, alloc_trim, bits, c, lm, dec);
|
||||
unquant_fine_energy(start, end, &mut self.old_band_e, &alloc.fine_quant, dec, c);
|
||||
|
||||
// Shift the decode memory by one frame.
|
||||
let ml = self.mem_len();
|
||||
for ch in 0..cc {
|
||||
let mem = &mut self.decode_mem[ch * ml..(ch + 1) * ml];
|
||||
mem.copy_within(n..DECODE_BUFFER_SIZE + OVERLAP / 2, 0);
|
||||
}
|
||||
|
||||
let mut collapse_masks = vec![0u32; c * NB_EBANDS];
|
||||
let mut x = std::mem::take(&mut self.x);
|
||||
x.clear();
|
||||
x.resize(c * n, 0.0);
|
||||
{
|
||||
let (xa, xb) = x.split_at_mut(n);
|
||||
let yb = if c == 2 { Some(xb) } else { None };
|
||||
let mut seed = self.rng;
|
||||
quant_all_bands(
|
||||
m,
|
||||
start,
|
||||
end,
|
||||
xa,
|
||||
yb,
|
||||
&mut collapse_masks,
|
||||
&alloc.pulses,
|
||||
short_blocks,
|
||||
spread,
|
||||
alloc.dual_stereo,
|
||||
alloc.intensity,
|
||||
&tf_res,
|
||||
((len as i32) * (8 << BITRES)) - anti_collapse_rsv,
|
||||
alloc.balance,
|
||||
dec,
|
||||
lm,
|
||||
alloc.coded_bands,
|
||||
&mut seed,
|
||||
self.disable_inv,
|
||||
);
|
||||
self.rng = seed;
|
||||
}
|
||||
let mut anti_collapse_on = false;
|
||||
if anti_collapse_rsv > 0 {
|
||||
anti_collapse_on = dec.bits(1) != 0;
|
||||
}
|
||||
let bits_left = len as i32 * 8 - dec.tell();
|
||||
unquant_energy_finalise(start, end, &mut self.old_band_e, &alloc.fine_quant, &alloc.fine_priority, bits_left, dec, c);
|
||||
if anti_collapse_on {
|
||||
anti_collapse(
|
||||
&mut x,
|
||||
&collapse_masks,
|
||||
lm,
|
||||
c,
|
||||
n,
|
||||
start,
|
||||
end,
|
||||
&self.old_band_e,
|
||||
&self.old_log_e,
|
||||
&self.old_log_e2,
|
||||
&alloc.pulses,
|
||||
self.rng,
|
||||
);
|
||||
}
|
||||
if silence {
|
||||
for v in self.old_band_e[..c * NB_EBANDS].iter_mut() {
|
||||
*v = -28.0;
|
||||
}
|
||||
}
|
||||
self.synthesis(&x, n, start, eff_end, c, is_transient, lm, silence);
|
||||
self.x = x;
|
||||
|
||||
// Post-filter.
|
||||
for ch in 0..cc {
|
||||
self.postfilter_period = self.postfilter_period.max(COMBFILTER_MINPERIOD);
|
||||
self.postfilter_period_old = self.postfilter_period_old.max(COMBFILTER_MINPERIOD);
|
||||
let mem = &mut self.decode_mem[ch * ml..(ch + 1) * ml];
|
||||
let off = DECODE_BUFFER_SIZE - n;
|
||||
comb_filter(
|
||||
mem,
|
||||
off,
|
||||
self.postfilter_period_old,
|
||||
self.postfilter_period,
|
||||
SHORT_MDCT,
|
||||
self.postfilter_gain_old,
|
||||
self.postfilter_gain,
|
||||
self.postfilter_tapset_old,
|
||||
self.postfilter_tapset,
|
||||
&m.window,
|
||||
);
|
||||
if lm != 0 {
|
||||
comb_filter(
|
||||
mem,
|
||||
off + SHORT_MDCT,
|
||||
self.postfilter_period,
|
||||
postfilter_pitch,
|
||||
n - SHORT_MDCT,
|
||||
self.postfilter_gain,
|
||||
postfilter_gain,
|
||||
self.postfilter_tapset,
|
||||
postfilter_tapset,
|
||||
&m.window,
|
||||
);
|
||||
}
|
||||
}
|
||||
self.postfilter_period_old = self.postfilter_period;
|
||||
self.postfilter_gain_old = self.postfilter_gain;
|
||||
self.postfilter_tapset_old = self.postfilter_tapset;
|
||||
self.postfilter_period = postfilter_pitch;
|
||||
self.postfilter_gain = postfilter_gain;
|
||||
self.postfilter_tapset = postfilter_tapset;
|
||||
if lm != 0 {
|
||||
self.postfilter_period_old = self.postfilter_period;
|
||||
self.postfilter_gain_old = self.postfilter_gain;
|
||||
self.postfilter_tapset_old = self.postfilter_tapset;
|
||||
}
|
||||
|
||||
if c == 1 {
|
||||
let (a, b) = self.old_band_e.split_at_mut(NB_EBANDS);
|
||||
b.copy_from_slice(a);
|
||||
}
|
||||
if !is_transient {
|
||||
self.old_log_e2 = self.old_log_e;
|
||||
self.old_log_e = self.old_band_e;
|
||||
for i in 0..2 * NB_EBANDS {
|
||||
self.background_log_e[i] = (self.background_log_e[i] + mm as f32 * 0.001).min(self.old_band_e[i]);
|
||||
}
|
||||
} else {
|
||||
for i in 0..2 * NB_EBANDS {
|
||||
self.old_log_e[i] = self.old_log_e[i].min(self.old_band_e[i]);
|
||||
}
|
||||
}
|
||||
for ch in 0..2 {
|
||||
for i in (0..start).chain(end..NB_EBANDS) {
|
||||
self.old_band_e[ch * NB_EBANDS + i] = 0.0;
|
||||
self.old_log_e[ch * NB_EBANDS + i] = -28.0;
|
||||
self.old_log_e2[ch * NB_EBANDS + i] = -28.0;
|
||||
}
|
||||
}
|
||||
self.rng = dec.rng();
|
||||
self.loss_count = 0;
|
||||
}
|
||||
|
||||
/// Denormalisation + IMDCT into the decode memory (`celt_synthesis`).
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
fn synthesis(&mut self, x: &[f32], n: usize, start: usize, eff_end: usize, c: usize, is_transient: bool, lm: usize, silence: bool) {
|
||||
let cc = self.channels;
|
||||
let m = self.m;
|
||||
let mm = 1usize << lm;
|
||||
let (bcount, nb, shift) = if is_transient { (mm, SHORT_MDCT, MAX_LM) } else { (1, SHORT_MDCT << lm, MAX_LM - lm) };
|
||||
let ml = self.mem_len();
|
||||
let mut freq = std::mem::take(&mut self.freq);
|
||||
freq.clear();
|
||||
freq.resize(2 * n, 0.0);
|
||||
let (f0, f1) = freq.split_at_mut(n);
|
||||
let out_off = DECODE_BUFFER_SIZE - n;
|
||||
if cc == 2 && c == 1 {
|
||||
denormalise_bands(x, f0, &self.old_band_e, start, eff_end, mm, self.downsample, silence);
|
||||
f1.copy_from_slice(f0);
|
||||
for (ch, f) in [(0usize, &*f1), (1usize, &*f0)] {
|
||||
let mem = &mut self.decode_mem[ch * ml..(ch + 1) * ml];
|
||||
for b in 0..bcount {
|
||||
m.mdct[shift].backward(&f[b..], bcount, &mut mem[out_off + nb * b..], &m.window, OVERLAP, &mut self.imdct);
|
||||
}
|
||||
}
|
||||
} else if cc == 1 && c == 2 {
|
||||
denormalise_bands(x, f0, &self.old_band_e, start, eff_end, mm, self.downsample, silence);
|
||||
denormalise_bands(&x[n..], f1, &self.old_band_e[NB_EBANDS..], start, eff_end, mm, self.downsample, silence);
|
||||
for i in 0..n {
|
||||
f0[i] = 0.5 * (f0[i] + f1[i]);
|
||||
}
|
||||
let mem = &mut self.decode_mem[..ml];
|
||||
for b in 0..bcount {
|
||||
m.mdct[shift].backward(&f0[b..], bcount, &mut mem[out_off + nb * b..], &m.window, OVERLAP, &mut self.imdct);
|
||||
}
|
||||
} else {
|
||||
for ch in 0..cc {
|
||||
denormalise_bands(&x[ch * n..], f0, &self.old_band_e[ch * NB_EBANDS..], start, eff_end, mm, self.downsample, silence);
|
||||
let mem = &mut self.decode_mem[ch * ml..(ch + 1) * ml];
|
||||
for b in 0..bcount {
|
||||
m.mdct[shift].backward(&f0[b..], bcount, &mut mem[out_off + nb * b..], &m.window, OVERLAP, &mut self.imdct);
|
||||
}
|
||||
}
|
||||
}
|
||||
self.freq = freq;
|
||||
}
|
||||
|
||||
fn deemphasis(&mut self, n: usize, pcm: &mut [f32], accum: bool) {
|
||||
let cc = self.channels;
|
||||
let ml = self.mem_len();
|
||||
let ds = self.downsample;
|
||||
let out_off = DECODE_BUFFER_SIZE - n;
|
||||
let mut scratch = std::mem::take(&mut self.pcm_scratch);
|
||||
for ch in 0..cc {
|
||||
let mem = &self.decode_mem[ch * ml + out_off..ch * ml + out_off + n];
|
||||
let mut mstate = self.preemph_mem[ch];
|
||||
scratch.clear();
|
||||
for &v in mem {
|
||||
let tmp = v + 1e-30 + mstate;
|
||||
mstate = PREEMPH * tmp;
|
||||
scratch.push(tmp);
|
||||
}
|
||||
self.preemph_mem[ch] = mstate;
|
||||
for j in 0..n / ds {
|
||||
let v = scratch[j * ds] * (1.0 / 32768.0);
|
||||
let o = &mut pcm[j * cc + ch];
|
||||
if accum {
|
||||
*o += v;
|
||||
} else {
|
||||
*o = v;
|
||||
}
|
||||
}
|
||||
}
|
||||
self.pcm_scratch = scratch;
|
||||
}
|
||||
|
||||
/// Packet-loss concealment: pitch-periodic extension for the first lost frames, then
|
||||
/// decaying shaped noise.
|
||||
fn decode_lost(&mut self, n: usize, lm: usize) {
|
||||
let cc = self.channels;
|
||||
let ml = self.mem_len();
|
||||
let m = self.m;
|
||||
let pitch_based = self.loss_count < 5 && self.start == 0 && !self.skip_plc;
|
||||
if !pitch_based {
|
||||
let decay = if self.loss_count == 0 { 1.5 } else { 0.5 };
|
||||
for ch in 0..cc {
|
||||
for i in self.start..self.end {
|
||||
let k = ch * NB_EBANDS + i;
|
||||
self.old_band_e[k] = self.background_log_e[k].max(self.old_band_e[k] - decay);
|
||||
}
|
||||
}
|
||||
let mut seed = self.rng;
|
||||
let mut x = std::mem::take(&mut self.x);
|
||||
x.clear();
|
||||
x.resize(cc * n, 0.0);
|
||||
let eff_end = self.end.min(NB_EBANDS);
|
||||
for ch in 0..cc {
|
||||
for i in self.start..eff_end {
|
||||
let off = n * ch + ((EBANDS[i] as usize) << lm);
|
||||
let blen = ((EBANDS[i + 1] - EBANDS[i]) as usize) << lm;
|
||||
for v in x[off..off + blen].iter_mut() {
|
||||
seed = bands::lcg_rand(seed);
|
||||
*v = ((seed as i32) >> 20) as f32;
|
||||
}
|
||||
renormalise_vector(&mut x[off..off + blen], 1.0);
|
||||
}
|
||||
}
|
||||
self.rng = seed;
|
||||
for ch in 0..cc {
|
||||
let mem = &mut self.decode_mem[ch * ml..(ch + 1) * ml];
|
||||
mem.copy_within(n..DECODE_BUFFER_SIZE + OVERLAP / 2, 0);
|
||||
}
|
||||
let saved_sc = self.stream_channels;
|
||||
self.stream_channels = cc;
|
||||
self.synthesis(&x, n, self.start, eff_end, cc, false, lm, false);
|
||||
self.stream_channels = saved_sc;
|
||||
self.x = x;
|
||||
} else {
|
||||
// Pitch search on the (mono-mixed) history.
|
||||
let hist_len = 1024usize;
|
||||
let base = DECODE_BUFFER_SIZE - hist_len;
|
||||
let mut mono = vec![0f32; hist_len];
|
||||
for ch in 0..cc {
|
||||
for (i, v) in mono.iter_mut().enumerate() {
|
||||
*v += self.decode_mem[ch * ml + base + i];
|
||||
}
|
||||
}
|
||||
let period = if self.loss_count == 0 { find_pitch(&mono) } else { self.postfilter_period_old.max(PLC_PITCH_LAG_MIN as i32) as usize };
|
||||
let period = period.clamp(PLC_PITCH_LAG_MIN, PLC_PITCH_LAG_MAX);
|
||||
// Remember the period for subsequent losses in the (otherwise unused) old period slot.
|
||||
self.postfilter_period_old = period as i32;
|
||||
let fade = 0.8f32.powi(self.loss_count as i32 + 1);
|
||||
let mut ext = vec![0f32; n + OVERLAP];
|
||||
for ch in 0..cc {
|
||||
let mem = &mut self.decode_mem[ch * ml..(ch + 1) * ml];
|
||||
// Energy decay across the last two periods.
|
||||
let e1: f32 = mem[DECODE_BUFFER_SIZE - period..DECODE_BUFFER_SIZE].iter().map(|v| v * v).sum();
|
||||
let e0: f32 = mem[DECODE_BUFFER_SIZE - 2 * period..DECODE_BUFFER_SIZE - period].iter().map(|v| v * v).sum();
|
||||
let decay = if e0 > 0.0 { (e1 / e0).sqrt().min(1.0) } else { 0.0 };
|
||||
let mut g = 1.0f32;
|
||||
for (i, e) in ext.iter_mut().enumerate() {
|
||||
if i > 0 && i % period == 0 {
|
||||
g *= decay;
|
||||
}
|
||||
let src = DECODE_BUFFER_SIZE - period + (i % period);
|
||||
*e = mem[src] * g * fade;
|
||||
}
|
||||
mem.copy_within(n..DECODE_BUFFER_SIZE + OVERLAP / 2, 0);
|
||||
let out_off = DECODE_BUFFER_SIZE - n;
|
||||
// Cross-fade the start with the previous overlap so the transition is smooth.
|
||||
mem[out_off..out_off + n].copy_from_slice(&ext[..n]);
|
||||
let tail = &ext[n..n + OVERLAP];
|
||||
for i in 0..OVERLAP / 2 {
|
||||
mem[DECODE_BUFFER_SIZE + i] = m.window[i] * tail[OVERLAP - 1 - i] + m.window[OVERLAP - 1 - i] * tail[i];
|
||||
}
|
||||
}
|
||||
}
|
||||
self.loss_count += 1;
|
||||
}
|
||||
}
|
||||
|
||||
fn find_pitch(x: &[f32]) -> usize {
|
||||
let n = x.len();
|
||||
let win = 512.min(n - PLC_PITCH_LAG_MAX);
|
||||
let tgt = &x[n - win..];
|
||||
let mut best = PLC_PITCH_LAG_MIN;
|
||||
let mut best_score = f32::MIN;
|
||||
let e_t: f32 = tgt.iter().map(|v| v * v).sum::<f32>() + 1e-9;
|
||||
for lag in PLC_PITCH_LAG_MIN..=PLC_PITCH_LAG_MAX {
|
||||
let src = &x[n - win - lag..n - lag];
|
||||
let mut xy = 0f32;
|
||||
let mut yy = 1e-9f32;
|
||||
for (a, b) in tgt.iter().zip(src) {
|
||||
xy += a * b;
|
||||
yy += b * b;
|
||||
}
|
||||
let score = xy / (e_t * yy).sqrt();
|
||||
if score > best_score {
|
||||
best_score = score;
|
||||
best = lag;
|
||||
}
|
||||
}
|
||||
best
|
||||
}
|
||||
|
||||
fn tf_decode(start: usize, end: usize, is_transient: bool, tf_res: &mut [i32; NB_EBANDS], lm: usize, dec: &mut RangeDecoder) {
|
||||
let mut budget = dec.storage() as i32 * 8;
|
||||
let mut tell = dec.tell();
|
||||
let mut logp: u32 = if is_transient { 2 } else { 4 };
|
||||
let tf_select_rsv = lm > 0 && tell + (logp as i32) < budget;
|
||||
budget -= tf_select_rsv as i32;
|
||||
let mut tf_changed = 0;
|
||||
let mut curr = 0;
|
||||
for r in tf_res.iter_mut().take(end).skip(start) {
|
||||
if tell + logp as i32 <= budget {
|
||||
curr ^= dec.bit_logp(logp) as i32;
|
||||
tell = dec.tell();
|
||||
tf_changed |= curr;
|
||||
}
|
||||
*r = curr;
|
||||
logp = if is_transient { 4 } else { 5 };
|
||||
}
|
||||
let mut tf_select = 0;
|
||||
let ti = 4 * is_transient as usize;
|
||||
if tf_select_rsv && TF_SELECT_TABLE[lm][ti + tf_changed as usize] != TF_SELECT_TABLE[lm][ti + 2 + tf_changed as usize] {
|
||||
tf_select = dec.bit_logp(1) as usize;
|
||||
}
|
||||
for r in tf_res.iter_mut().take(end).skip(start) {
|
||||
*r = TF_SELECT_TABLE[lm][ti + 2 * tf_select + *r as usize] as i32;
|
||||
}
|
||||
}
|
||||
|
||||
/// Pitch post-filter (in place on `buf[off .. off + n]`, reading history before `off`).
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
fn comb_filter(buf: &mut [f32], off: usize, t0: i32, t1: i32, n: usize, g0: f32, g1: f32, tapset0: usize, tapset1: usize, window: &[f32; OVERLAP]) {
|
||||
if g0 == 0.0 && g1 == 0.0 {
|
||||
return;
|
||||
}
|
||||
let t0 = t0.max(COMBFILTER_MINPERIOD) as usize;
|
||||
let t1 = t1.max(COMBFILTER_MINPERIOD) as usize;
|
||||
let g00 = g0 * COMB_GAINS[tapset0][0];
|
||||
let g01 = g0 * COMB_GAINS[tapset0][1];
|
||||
let g02 = g0 * COMB_GAINS[tapset0][2];
|
||||
let g10 = g1 * COMB_GAINS[tapset1][0];
|
||||
let g11 = g1 * COMB_GAINS[tapset1][1];
|
||||
let g12 = g1 * COMB_GAINS[tapset1][2];
|
||||
let mut x1 = buf[off + 1 - t1];
|
||||
let mut x2 = buf[off - t1];
|
||||
let mut x3 = buf[off - t1 - 1];
|
||||
let mut x4 = buf[off - t1 - 2];
|
||||
let overlap = if g0 == g1 && t0 == t1 && tapset0 == tapset1 { 0 } else { OVERLAP.min(n) };
|
||||
let mut i = 0;
|
||||
while i < overlap {
|
||||
let p = off + i;
|
||||
let x0 = buf[p + 2 - t1];
|
||||
let f = window[i] * window[i];
|
||||
let a = 1.0 - f;
|
||||
buf[p] = buf[p]
|
||||
+ a * g00 * buf[p - t0]
|
||||
+ a * g01 * (buf[p + 1 - t0] + buf[p - t0 - 1])
|
||||
+ a * g02 * (buf[p + 2 - t0] + buf[p - t0 - 2])
|
||||
+ f * g10 * x2
|
||||
+ f * g11 * (x1 + x3)
|
||||
+ f * g12 * (x0 + x4);
|
||||
x4 = x3;
|
||||
x3 = x2;
|
||||
x2 = x1;
|
||||
x1 = x0;
|
||||
i += 1;
|
||||
}
|
||||
if g1 == 0.0 {
|
||||
return;
|
||||
}
|
||||
// Constant filter for the rest of the block.
|
||||
let t = t1;
|
||||
for i in overlap..n {
|
||||
let p = off + i;
|
||||
buf[p] = buf[p] + g10 * buf[p - t] + g11 * (buf[p + 1 - t] + buf[p - t - 1]) + g12 * (buf[p + 2 - t] + buf[p - t - 2]);
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,171 +0,0 @@
|
||||
//! Band energy decoding: Laplace-coded coarse energy, fine energy and the final refinement
|
||||
//! (RFC 6716 §4.3.2).
|
||||
|
||||
use super::rate::MAX_FINE_BITS;
|
||||
use super::tables::{BETA_COEF, BETA_INTRA, E_PROB_MODEL, NB_EBANDS, PRED_COEF, SMALL_ENERGY_ICDF};
|
||||
use crate::range::RangeDecoder;
|
||||
|
||||
const LAPLACE_MINP: u32 = 1;
|
||||
const LAPLACE_NMIN: u32 = 16;
|
||||
|
||||
fn laplace_get_freq1(fs0: u32, decay: u32) -> u32 {
|
||||
let ft = 32768 - LAPLACE_MINP * (2 * LAPLACE_NMIN) - fs0;
|
||||
(ft * (16384 - decay)) >> 15
|
||||
}
|
||||
|
||||
/// Decodes a Laplace-distributed integer (`fs` = probability of 0 in Q15, `decay` in Q14).
|
||||
pub fn laplace_decode(dec: &mut RangeDecoder, mut fs: u32, decay: u32) -> i32 {
|
||||
let mut val = 0i32;
|
||||
let fm = dec.decode_bin(15);
|
||||
let mut fl = 0u32;
|
||||
if fm >= fs {
|
||||
val += 1;
|
||||
fl = fs;
|
||||
fs = laplace_get_freq1(fs, decay) + LAPLACE_MINP;
|
||||
while fs > LAPLACE_MINP && fm >= fl + 2 * fs {
|
||||
fs *= 2;
|
||||
fl += fs;
|
||||
fs = (((fs - 2 * LAPLACE_MINP) * decay) >> 15) + LAPLACE_MINP;
|
||||
val += 1;
|
||||
}
|
||||
if fs <= LAPLACE_MINP {
|
||||
let di = (fm - fl) >> 1;
|
||||
val += di as i32;
|
||||
fl += 2 * di * LAPLACE_MINP;
|
||||
}
|
||||
if fm < fl + fs {
|
||||
val = -val;
|
||||
} else {
|
||||
fl += fs;
|
||||
}
|
||||
}
|
||||
dec.update_bin(fl, (fl + fs).min(32768), 15);
|
||||
val
|
||||
}
|
||||
|
||||
/// `unquant_coarse_energy`.
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub fn unquant_coarse_energy(start: usize, end: usize, old_e: &mut [f32], intra: bool, dec: &mut RangeDecoder, c: usize, lm: usize) {
|
||||
let prob = &E_PROB_MODEL[lm][intra as usize];
|
||||
let (coef, beta) = if intra { (0.0, BETA_INTRA) } else { (PRED_COEF[lm], BETA_COEF[lm]) };
|
||||
let budget = dec.storage() as i32 * 8;
|
||||
let mut prev = [0f32; 2];
|
||||
for i in start..end {
|
||||
for ch in 0..c {
|
||||
let tell = dec.tell();
|
||||
let qi = if budget - tell >= 15 {
|
||||
let pi = 2 * i.min(20);
|
||||
laplace_decode(dec, (prob[pi] as u32) << 7, (prob[pi + 1] as u32) << 6)
|
||||
} else if budget - tell >= 2 {
|
||||
let q = dec.icdf(&SMALL_ENERGY_ICDF, 2) as i32;
|
||||
(q >> 1) ^ -(q & 1)
|
||||
} else if budget - tell >= 1 {
|
||||
-(dec.bit_logp(1) as i32)
|
||||
} else {
|
||||
-1
|
||||
};
|
||||
let q = qi as f32;
|
||||
let idx = i + ch * NB_EBANDS;
|
||||
old_e[idx] = old_e[idx].max(-9.0);
|
||||
let tmp = coef * old_e[idx] + prev[ch] + q;
|
||||
old_e[idx] = tmp;
|
||||
prev[ch] = prev[ch] + q - beta * q;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn unquant_fine_energy(start: usize, end: usize, old_e: &mut [f32], fine_quant: &[i32; NB_EBANDS], dec: &mut RangeDecoder, c: usize) {
|
||||
for i in start..end {
|
||||
let fq = fine_quant[i];
|
||||
if fq <= 0 {
|
||||
continue;
|
||||
}
|
||||
for ch in 0..c {
|
||||
let q2 = dec.bits(fq as u32);
|
||||
let offset = (q2 as f32 + 0.5) * (1 << (14 - fq)) as f32 / 16384.0 - 0.5;
|
||||
old_e[i + ch * NB_EBANDS] += offset;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub fn unquant_energy_finalise(
|
||||
start: usize,
|
||||
end: usize,
|
||||
old_e: &mut [f32],
|
||||
fine_quant: &[i32; NB_EBANDS],
|
||||
fine_priority: &[i32; NB_EBANDS],
|
||||
mut bits_left: i32,
|
||||
dec: &mut RangeDecoder,
|
||||
c: usize,
|
||||
) {
|
||||
for prio in 0..2 {
|
||||
let mut i = start;
|
||||
while i < end && bits_left >= c as i32 {
|
||||
if fine_quant[i] >= MAX_FINE_BITS || fine_priority[i] != prio {
|
||||
i += 1;
|
||||
continue;
|
||||
}
|
||||
for ch in 0..c {
|
||||
let q2 = dec.bits(1);
|
||||
let offset = (q2 as f32 - 0.5) * (1 << (14 - fine_quant[i] - 1)) as f32 / 16384.0;
|
||||
old_e[i + ch * NB_EBANDS] += offset;
|
||||
bits_left -= 1;
|
||||
}
|
||||
i += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::range::enc::RangeEncoder;
|
||||
|
||||
fn laplace_encode(e: &mut RangeEncoder, value: i32, mut fs: u32, decay: u32) {
|
||||
let mut fl = 0u32;
|
||||
let mut val = value;
|
||||
if val != 0 {
|
||||
let s: i32 = if val < 0 { -1 } else { 0 };
|
||||
val = val.abs();
|
||||
fl = fs;
|
||||
fs = laplace_get_freq1(fs, decay);
|
||||
let mut i = 1;
|
||||
while fs > 0 && i < val {
|
||||
fs *= 2;
|
||||
fl += fs + 2 * LAPLACE_MINP;
|
||||
fs = (fs * decay) >> 15;
|
||||
i += 1;
|
||||
}
|
||||
if fs == 0 {
|
||||
let ndi_max = (32768 - fl + LAPLACE_MINP - 1) as i32;
|
||||
let ndi_max = (ndi_max - s) >> 1;
|
||||
let di = (val - i).min(ndi_max - 1);
|
||||
fl = (fl as i32 + (2 * di + 1 + s) * LAPLACE_MINP as i32) as u32;
|
||||
fs = LAPLACE_MINP.min(32768 - fl);
|
||||
} else {
|
||||
fs += LAPLACE_MINP;
|
||||
if s == 0 {
|
||||
fl += fs;
|
||||
}
|
||||
}
|
||||
}
|
||||
e.encode(fl, fl + fs, 32768);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn laplace_roundtrip() {
|
||||
for &(fs, decay) in &[(72u32 << 7, 127u32 << 6), (24 << 7, 179 << 6), (177 << 7, 11 << 6)] {
|
||||
let vals: Vec<i32> = (-12..=12).collect();
|
||||
let mut e = RangeEncoder::new(400);
|
||||
for &v in &vals {
|
||||
laplace_encode(&mut e, v, fs, decay);
|
||||
}
|
||||
let buf = e.done();
|
||||
let mut d = RangeDecoder::new(&buf);
|
||||
for &v in &vals {
|
||||
assert_eq!(laplace_decode(&mut d, fs, decay), v, "fs={fs} v={v}");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,205 +0,0 @@
|
||||
//! Inverse MDCT with the low-overlap window and in-place TDAC (RFC 6716 §4.3.7).
|
||||
//!
|
||||
//! The IMDCT is computed through an N/4-point complex FFT (pre-rotation, FFT, post-rotation),
|
||||
//! producing the N/2 "folded" time samples; the window overlap region is then unfolded against the
|
||||
//! folded tail of the previous block, which the caller keeps in place in the output buffer.
|
||||
|
||||
#[derive(Clone, Copy, Default, Debug)]
|
||||
pub struct Cpx {
|
||||
pub re: f32,
|
||||
pub im: f32,
|
||||
}
|
||||
|
||||
impl Cpx {
|
||||
#[inline]
|
||||
fn mul(self, o: Cpx) -> Cpx {
|
||||
Cpx { re: self.re * o.re - self.im * o.im, im: self.re * o.im + self.im * o.re }
|
||||
}
|
||||
#[inline]
|
||||
fn add(self, o: Cpx) -> Cpx {
|
||||
Cpx { re: self.re + o.re, im: self.im + o.im }
|
||||
}
|
||||
}
|
||||
|
||||
/// Mixed-radix (2, 3, 4, 5) forward complex FFT, unscaled.
|
||||
pub struct Fft {
|
||||
n: usize,
|
||||
factors: Vec<usize>,
|
||||
twiddle: Vec<Cpx>,
|
||||
}
|
||||
|
||||
impl Fft {
|
||||
pub fn new(n: usize) -> Fft {
|
||||
let mut factors = Vec::new();
|
||||
let mut m = n;
|
||||
for p in [4usize, 2, 3, 5] {
|
||||
while m.is_multiple_of(p) {
|
||||
factors.push(p);
|
||||
m /= p;
|
||||
}
|
||||
}
|
||||
assert_eq!(m, 1, "unsupported FFT size {n}");
|
||||
let twiddle = (0..n)
|
||||
.map(|k| {
|
||||
let ph = -2.0 * std::f64::consts::PI * k as f64 / n as f64;
|
||||
Cpx { re: ph.cos() as f32, im: ph.sin() as f32 }
|
||||
})
|
||||
.collect();
|
||||
Fft { n, factors, twiddle }
|
||||
}
|
||||
|
||||
pub fn process(&self, data: &mut [Cpx], scratch: &mut Vec<Cpx>) {
|
||||
scratch.clear();
|
||||
scratch.extend_from_slice(&data[..self.n]);
|
||||
self.rec(&mut data[..self.n], scratch, 1, self.n, 0, 1);
|
||||
}
|
||||
|
||||
fn rec(&self, out: &mut [Cpx], inp: &[Cpx], in_stride: usize, n: usize, fi: usize, tw_stride: usize) {
|
||||
let p = self.factors[fi];
|
||||
let m = n / p;
|
||||
if m == 1 {
|
||||
for (i, o) in out.iter_mut().enumerate().take(p) {
|
||||
*o = inp[i * in_stride];
|
||||
}
|
||||
} else {
|
||||
for q in 0..p {
|
||||
self.rec(&mut out[q * m..(q + 1) * m], &inp[q * in_stride..], in_stride * p, m, fi + 1, tw_stride * p);
|
||||
}
|
||||
}
|
||||
let big = self.n;
|
||||
let mut t = [Cpx::default(); 5];
|
||||
for k in 0..m {
|
||||
for q in 0..p {
|
||||
let w = self.twiddle[(q * k * tw_stride) % big];
|
||||
t[q] = out[q * m + k].mul(w);
|
||||
}
|
||||
for q2 in 0..p {
|
||||
let mut acc = Cpx::default();
|
||||
for (q, tq) in t.iter().enumerate().take(p) {
|
||||
let w = self.twiddle[((q * q2) % p) * (big / p)];
|
||||
acc = acc.add(tq.mul(w));
|
||||
}
|
||||
out[q2 * m + k] = acc;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct Imdct {
|
||||
/// Full MDCT length (2x the number of coefficients).
|
||||
n: usize,
|
||||
trig: Vec<f32>,
|
||||
fft: Fft,
|
||||
}
|
||||
|
||||
impl Imdct {
|
||||
pub fn new(n: usize) -> Imdct {
|
||||
let trig = (0..n / 2).map(|i| (2.0 * std::f64::consts::PI * (i as f64 + 0.125) / n as f64).cos() as f32).collect();
|
||||
Imdct { n, trig, fft: Fft::new(n / 4) }
|
||||
}
|
||||
|
||||
/// Inverse transform of `n/2` coefficients read from `input` at `stride`.
|
||||
///
|
||||
/// Writes the folded block to `out[overlap/2 .. overlap/2 + n/2]` and unfolds the first
|
||||
/// `overlap` samples against the previous block's folded tail already stored in
|
||||
/// `out[0 .. overlap/2]`.
|
||||
pub fn backward(&self, input: &[f32], stride: usize, out: &mut [f32], window: &[f32], overlap: usize, work: &mut ImdctScratch) {
|
||||
let n2 = self.n >> 1;
|
||||
let n4 = self.n >> 2;
|
||||
let t = &self.trig;
|
||||
let z = &mut work.z;
|
||||
z.clear();
|
||||
for i in 0..n4 {
|
||||
let x1 = input[2 * i * stride];
|
||||
let x2 = input[stride * (n2 - 1 - 2 * i)];
|
||||
let yr = x2 * t[i] + x1 * t[n4 + i];
|
||||
let yi = x1 * t[i] - x2 * t[n4 + i];
|
||||
z.push(Cpx { re: yi, im: yr });
|
||||
}
|
||||
self.fft.process(z, &mut work.scratch);
|
||||
let base = overlap >> 1;
|
||||
let y = &mut out[base..base + n2];
|
||||
for i in 0..n4.div_ceil(2) {
|
||||
let front = z[i];
|
||||
let back = z[n4 - 1 - i];
|
||||
let (re, im) = (front.im, front.re);
|
||||
let (t0, t1) = (t[i], t[n4 + i]);
|
||||
let yr0 = re * t0 + im * t1;
|
||||
let yi0 = re * t1 - im * t0;
|
||||
let (re, im) = (back.im, back.re);
|
||||
let (t0, t1) = (t[n4 - i - 1], t[n2 - i - 1]);
|
||||
let yr1 = re * t0 + im * t1;
|
||||
let yi1 = re * t1 - im * t0;
|
||||
y[2 * i] = yr0;
|
||||
y[n2 - 1 - 2 * i] = yi0;
|
||||
y[n2 - 2 - 2 * i] = yr1;
|
||||
y[2 * i + 1] = yi1;
|
||||
}
|
||||
// Mirror on both sides for TDAC.
|
||||
for i in 0..overlap / 2 {
|
||||
let x1 = out[overlap - 1 - i];
|
||||
let x2 = out[i];
|
||||
let w1 = window[i];
|
||||
let w2 = window[overlap - 1 - i];
|
||||
out[i] = x2 * w2 - x1 * w1;
|
||||
out[overlap - 1 - i] = x2 * w1 + x1 * w2;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct ImdctScratch {
|
||||
z: Vec<Cpx>,
|
||||
scratch: Vec<Cpx>,
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn fft_matches_dft() {
|
||||
for &n in &[60usize, 120, 240, 480] {
|
||||
let fft = Fft::new(n);
|
||||
let x: Vec<Cpx> = (0..n).map(|i| Cpx { re: ((i * 7) % 13) as f32 - 6.0, im: ((i * 3) % 5) as f32 - 2.0 }).collect();
|
||||
let mut y = x.clone();
|
||||
fft.process(&mut y, &mut Vec::new());
|
||||
for k in [0, 1, 7, n / 2, n - 1] {
|
||||
let mut acc = (0f64, 0f64);
|
||||
for (i, v) in x.iter().enumerate() {
|
||||
let ph = -2.0 * std::f64::consts::PI * (i * k) as f64 / n as f64;
|
||||
acc.0 += v.re as f64 * ph.cos() - v.im as f64 * ph.sin();
|
||||
acc.1 += v.re as f64 * ph.sin() + v.im as f64 * ph.cos();
|
||||
}
|
||||
assert!((acc.0 - y[k].re as f64).abs() < 1e-2 && (acc.1 - y[k].im as f64).abs() < 1e-2, "n={n} k={k}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// The folded output must equal the direct IMDCT formula folded by TDAC symmetry.
|
||||
#[test]
|
||||
fn imdct_matches_direct_formula() {
|
||||
let n = 240;
|
||||
let n2 = n / 2;
|
||||
let im = Imdct::new(n);
|
||||
let x: Vec<f32> = (0..n2).map(|k| ((k * 37 % 11) as f32 - 5.0) / 5.0).collect();
|
||||
let overlap = 0;
|
||||
let mut out = vec![0f32; n2];
|
||||
im.backward(&x, 1, &mut out, &[], overlap, &mut ImdctScratch::default());
|
||||
// Direct IMDCT y[t] = sum X[k] cos(2pi/N (t + 1/2 + N/4)(k + 1/2)), t in 0..N.
|
||||
let y: Vec<f64> = (0..n)
|
||||
.map(|t| {
|
||||
(0..n2)
|
||||
.map(|k| x[k] as f64 * (2.0 * std::f64::consts::PI / n as f64 * (t as f64 + 0.5 + n as f64 / 4.0) * (k as f64 + 0.5)).cos())
|
||||
.sum()
|
||||
})
|
||||
.collect();
|
||||
// The folded half: out[i] relates to y in the middle half; check proportionality.
|
||||
let mid: Vec<f64> = (0..n2).map(|i| y[n / 4 + i]).collect();
|
||||
let dot: f64 = mid.iter().zip(&out).map(|(a, b)| a * *b as f64).sum();
|
||||
let ea: f64 = mid.iter().map(|a| a * a).sum();
|
||||
let eb: f64 = out.iter().map(|b| (*b as f64).powi(2)).sum();
|
||||
let corr = dot.abs() / (ea.sqrt() * eb.sqrt());
|
||||
assert!(corr > 0.999, "corr {corr}");
|
||||
}
|
||||
}
|
||||
@@ -1,584 +0,0 @@
|
||||
//! Pulse cache and bit allocation (RFC 6716 §4.3.3, §4.3.4.1).
|
||||
|
||||
use super::tables::{ALLOC, EBANDS, LOG2_FRAC_TABLE, MAX_LM, NB_EBANDS, OVERLAP};
|
||||
use crate::range::{BITRES, RangeDecoder};
|
||||
|
||||
const MAX_PSEUDO: i32 = 40;
|
||||
const LOG_MAX_PSEUDO: i32 = 6;
|
||||
pub const MAX_FINE_BITS: i32 = 8;
|
||||
const FINE_OFFSET: i32 = 21;
|
||||
pub const QTHETA_OFFSET: i32 = 4;
|
||||
pub const QTHETA_OFFSET_TWOPHASE: i32 = 16;
|
||||
const ALLOC_STEPS: i32 = 6;
|
||||
|
||||
/// Number of pulses represented by pseudo-pulse index `i`.
|
||||
pub fn get_pulses(i: i32) -> i32 {
|
||||
if i < 8 { i } else { (8 + (i & 7)) << ((i >> 3) - 1) }
|
||||
}
|
||||
|
||||
/// Conservative (rounded up) log2 of `val` with `frac` fractional bits.
|
||||
pub fn log2_frac(mut val: u32, mut frac: u32) -> i32 {
|
||||
let mut l = crate::range::ilog(val);
|
||||
if val & val.wrapping_sub(1) != 0 {
|
||||
if l > 16 {
|
||||
val = ((val - 1) >> (l - 16)) + 1;
|
||||
} else {
|
||||
val <<= 16 - l;
|
||||
}
|
||||
let mut l = (l - 1) << frac;
|
||||
loop {
|
||||
let b = (val >> 16) as i32;
|
||||
l += b << frac;
|
||||
val = (val + b as u32) >> b;
|
||||
val = (val * val + 0x7FFF) >> 15;
|
||||
if frac == 0 {
|
||||
break;
|
||||
}
|
||||
frac -= 1;
|
||||
}
|
||||
l + (val > 0x8000) as i32
|
||||
} else {
|
||||
l -= 1;
|
||||
l << frac
|
||||
}
|
||||
}
|
||||
|
||||
/// PVQ codebook size V(N, K) as u128 (saturating far above 2^32).
|
||||
pub fn pvq_v(n: usize, k: usize) -> u128 {
|
||||
// V(N,K) = V(N-1,K) + V(N,K-1) + V(N-1,K-1); computed row by row.
|
||||
if k == 0 {
|
||||
return 1;
|
||||
}
|
||||
if n == 0 {
|
||||
return 0;
|
||||
}
|
||||
let mut row = vec![0u128; k + 1]; // V(0, k)
|
||||
row[0] = 1;
|
||||
for _ in 0..n {
|
||||
let mut prev_diag = row[0]; // V(n-1, 0)
|
||||
// V(n, 0) = 1
|
||||
for kk in 1..=k {
|
||||
let up = row[kk]; // V(n-1, kk)
|
||||
let v = up.saturating_add(row[kk - 1]).saturating_add(prev_diag);
|
||||
prev_diag = up;
|
||||
row[kk] = v.min(u128::MAX >> 8);
|
||||
}
|
||||
row[0] = 1;
|
||||
}
|
||||
row[k]
|
||||
}
|
||||
|
||||
fn fits_in32(n: usize, k: i32) -> bool {
|
||||
pvq_v(n, k as usize) <= u32::MAX as u128
|
||||
}
|
||||
|
||||
/// Precomputed per-mode data shared by all CELT decoders.
|
||||
pub struct Mode {
|
||||
/// log2 of band width in 1/8 bits.
|
||||
pub log_n: [i32; NB_EBANDS],
|
||||
/// Index into `cache_bits` for `(LM+1)*NB_EBANDS + band` (-1 when unused).
|
||||
pub cache_index: Vec<i16>,
|
||||
/// `cache[0]` = max pseudo-pulses; `cache[k]` = bits(k) - 1 in 1/8 bits.
|
||||
pub cache_bits: Vec<u8>,
|
||||
/// Per-band maximum allocation: `[(2*LM + C-1)*NB_EBANDS + band]`.
|
||||
pub caps: Vec<u8>,
|
||||
pub window: [f32; OVERLAP],
|
||||
pub mdct: [super::mdct::Imdct; MAX_LM + 1],
|
||||
}
|
||||
|
||||
impl Mode {
|
||||
pub fn new() -> Mode {
|
||||
let mut log_n = [0i32; NB_EBANDS];
|
||||
for (i, l) in log_n.iter_mut().enumerate() {
|
||||
*l = log2_frac((EBANDS[i + 1] - EBANDS[i]) as u32, BITRES);
|
||||
}
|
||||
let (cache_index, cache_bits) = compute_pulse_cache(MAX_LM as i32);
|
||||
let mut m = Mode {
|
||||
log_n,
|
||||
cache_index,
|
||||
cache_bits,
|
||||
caps: Vec::new(),
|
||||
window: [0.0; OVERLAP],
|
||||
mdct: [super::mdct::Imdct::new(1920), super::mdct::Imdct::new(960), super::mdct::Imdct::new(480), super::mdct::Imdct::new(240)],
|
||||
};
|
||||
m.caps = compute_caps(&m);
|
||||
for i in 0..OVERLAP {
|
||||
let x = std::f64::consts::FRAC_PI_2 * (i as f64 + 0.5) / OVERLAP as f64;
|
||||
let s = x.sin();
|
||||
m.window[i] = (std::f64::consts::FRAC_PI_2 * s * s).sin() as f32;
|
||||
}
|
||||
m
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn cache(&self, lm: i32, band: usize) -> &[u8] {
|
||||
let idx = self.cache_index[((lm + 1) as usize) * NB_EBANDS + band];
|
||||
&self.cache_bits[idx.max(0) as usize..]
|
||||
}
|
||||
|
||||
pub fn bits2pulses(&self, band: usize, lm: i32, bits: i32) -> i32 {
|
||||
let cache = self.cache(lm, band);
|
||||
let mut lo = 0i32;
|
||||
let mut hi = cache[0] as i32;
|
||||
let bits = bits - 1;
|
||||
for _ in 0..LOG_MAX_PSEUDO {
|
||||
let mid = (lo + hi + 1) >> 1;
|
||||
if cache[mid as usize] as i32 >= bits {
|
||||
hi = mid;
|
||||
} else {
|
||||
lo = mid;
|
||||
}
|
||||
}
|
||||
let lo_bits = if lo == 0 { -1 } else { cache[lo as usize] as i32 };
|
||||
if bits - lo_bits <= cache[hi as usize] as i32 - bits { lo } else { hi }
|
||||
}
|
||||
|
||||
pub fn pulses2bits(&self, band: usize, lm: i32, pulses: i32) -> i32 {
|
||||
if pulses == 0 { 0 } else { self.cache(lm, band)[pulses as usize] as i32 + 1 }
|
||||
}
|
||||
|
||||
/// Per-band allocation caps for this frame (`init_caps`).
|
||||
pub fn init_caps(&self, lm: usize, c: usize) -> [i32; NB_EBANDS] {
|
||||
let mut cap = [0i32; NB_EBANDS];
|
||||
for (i, cp) in cap.iter_mut().enumerate() {
|
||||
let n = (EBANDS[i + 1] - EBANDS[i]) << lm;
|
||||
*cp = (self.caps[NB_EBANDS * (2 * lm + c - 1) + i] as i32 + 64) * c as i32 * n >> 2;
|
||||
}
|
||||
cap
|
||||
}
|
||||
}
|
||||
|
||||
fn compute_pulse_cache(lm_max: i32) -> (Vec<i16>, Vec<u8>) {
|
||||
let nb = NB_EBANDS;
|
||||
let rows = (lm_max + 2) as usize;
|
||||
let mut index = vec![-1i16; nb * rows];
|
||||
let mut entries: Vec<(usize, i32, usize)> = Vec::new(); // (N, K, offset)
|
||||
let mut curr = 0usize;
|
||||
for i in 0..rows {
|
||||
for j in 0..nb {
|
||||
let n = (((EBANDS[j + 1] - EBANDS[j]) << i) >> 1) as usize;
|
||||
index[i * nb + j] = -1;
|
||||
// Reuse the entry of an earlier band with the same size.
|
||||
'search: for k in 0..=i {
|
||||
for nn in 0..nb {
|
||||
if k == i && nn >= j {
|
||||
break;
|
||||
}
|
||||
if n == (((EBANDS[nn + 1] - EBANDS[nn]) << k) >> 1) as usize {
|
||||
index[i * nb + j] = index[k * nb + nn];
|
||||
break 'search;
|
||||
}
|
||||
}
|
||||
}
|
||||
if index[i * nb + j] == -1 && n != 0 {
|
||||
let mut k = 0;
|
||||
while k < MAX_PSEUDO && fits_in32(n, get_pulses(k + 1)) {
|
||||
k += 1;
|
||||
}
|
||||
index[i * nb + j] = curr as i16;
|
||||
entries.push((n, k, curr));
|
||||
curr += (k + 1) as usize;
|
||||
}
|
||||
}
|
||||
}
|
||||
let mut bits = vec![0u8; curr];
|
||||
for &(n, k, off) in &entries {
|
||||
let maxk = get_pulses(k) as usize;
|
||||
let tmp = required_bits(n, maxk, BITRES);
|
||||
for j in 1..=k {
|
||||
bits[off + j as usize] = (tmp[get_pulses(j) as usize] - 1) as u8;
|
||||
}
|
||||
bits[off] = k as u8;
|
||||
}
|
||||
(index, bits)
|
||||
}
|
||||
|
||||
/// Bits (1/2^frac units, rounded up) needed for each K in `0..=maxk` for dimension `n`.
|
||||
fn required_bits(n: usize, maxk: usize, frac: u32) -> Vec<i32> {
|
||||
let mut out = vec![0i32; maxk + 1];
|
||||
if n == 1 {
|
||||
for o in out.iter_mut().skip(1) {
|
||||
*o = 1 << frac;
|
||||
}
|
||||
} else {
|
||||
for (k, o) in out.iter_mut().enumerate().skip(1) {
|
||||
let v = pvq_v(n, k);
|
||||
*o = log2_frac(v as u32, frac);
|
||||
}
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
fn compute_caps(m: &Mode) -> Vec<u8> {
|
||||
let nb = NB_EBANDS;
|
||||
let mut caps = vec![0u8; (MAX_LM + 1) * 2 * nb];
|
||||
for i in 0..=MAX_LM as i32 {
|
||||
for c in 1..=2i32 {
|
||||
for j in 0..nb {
|
||||
let mut n0 = EBANDS[j + 1] - EBANDS[j];
|
||||
let max_bits = if n0 << i == 1 {
|
||||
c * (1 + MAX_FINE_BITS) << BITRES
|
||||
} else {
|
||||
let mut lm0 = 0;
|
||||
if n0 > 2 {
|
||||
n0 >>= 1;
|
||||
lm0 -= 1;
|
||||
} else if n0 <= 1 {
|
||||
lm0 = i.min(1);
|
||||
n0 <<= lm0;
|
||||
}
|
||||
let pcache = m.cache(lm0, j);
|
||||
let mut mb = pcache[pcache[0] as usize] as i32 + 1;
|
||||
let mut n = n0;
|
||||
for k in 0..(i - lm0) {
|
||||
mb <<= 1;
|
||||
let offset = ((m.log_n[j] + ((lm0 + k) << BITRES)) >> 1) - QTHETA_OFFSET;
|
||||
let num = 459 * ((2 * n - 1) * offset + mb);
|
||||
let den = ((2 * n - 1) << 9) - 459;
|
||||
let qb = ((num + (den >> 1)) / den).min(57);
|
||||
mb += qb;
|
||||
n <<= 1;
|
||||
}
|
||||
if c == 2 {
|
||||
mb <<= 1;
|
||||
let offset = ((m.log_n[j] + (i << BITRES)) >> 1) - if n == 2 { QTHETA_OFFSET_TWOPHASE } else { QTHETA_OFFSET };
|
||||
let ndof = 2 * n - 1 - (n == 2) as i32;
|
||||
let p = if n == 2 { 512 } else { 487 };
|
||||
let num = p * (mb + ndof * offset);
|
||||
let den = (ndof << 9) - p;
|
||||
let qb = ((num + (den >> 1)) / den).min(if n == 2 { 64 } else { 61 });
|
||||
mb += qb;
|
||||
}
|
||||
let ndof = c * n + if c == 2 && n > 2 { 1 } else { 0 };
|
||||
let mut offset = ((m.log_n[j] + (i << BITRES)) >> 1) - FINE_OFFSET;
|
||||
if n == 2 {
|
||||
offset += (1 << BITRES) >> 2;
|
||||
}
|
||||
let num = mb + ndof * offset;
|
||||
let den = (ndof - 1) << BITRES;
|
||||
let qb = ((num + (den >> 1)) / den).min(MAX_FINE_BITS);
|
||||
mb += c * qb << BITRES;
|
||||
mb
|
||||
};
|
||||
let w = c * ((EBANDS[j + 1] - EBANDS[j]) << i);
|
||||
let v = (4 * max_bits / w) - 64;
|
||||
caps[((i * 2 + c - 1) as usize) * nb + j] = v.clamp(0, 255) as u8;
|
||||
}
|
||||
}
|
||||
}
|
||||
caps
|
||||
}
|
||||
|
||||
/// Result of the allocation process.
|
||||
pub struct Allocation {
|
||||
pub coded_bands: usize,
|
||||
pub intensity: usize,
|
||||
pub dual_stereo: bool,
|
||||
pub balance: i32,
|
||||
pub pulses: [i32; NB_EBANDS],
|
||||
pub fine_quant: [i32; NB_EBANDS],
|
||||
pub fine_priority: [i32; NB_EBANDS],
|
||||
}
|
||||
|
||||
/// `clt_compute_allocation` (decoder side).
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub fn compute_allocation(
|
||||
m: &Mode,
|
||||
start: usize,
|
||||
end: usize,
|
||||
offsets: &[i32; NB_EBANDS],
|
||||
cap: &[i32; NB_EBANDS],
|
||||
alloc_trim: i32,
|
||||
total: i32,
|
||||
c: usize,
|
||||
lm: usize,
|
||||
dec: &mut RangeDecoder,
|
||||
) -> Allocation {
|
||||
let ci = c as i32;
|
||||
let lmi = lm as i32;
|
||||
let mut total = total.max(0);
|
||||
let mut skip_start = start;
|
||||
let skip_rsv = if total >= 1 << BITRES { 1 << BITRES } else { 0 };
|
||||
total -= skip_rsv;
|
||||
let mut intensity_rsv = 0;
|
||||
let mut dual_stereo_rsv = 0;
|
||||
if c == 2 {
|
||||
intensity_rsv = LOG2_FRAC_TABLE[end - start];
|
||||
if intensity_rsv > total {
|
||||
intensity_rsv = 0;
|
||||
} else {
|
||||
total -= intensity_rsv;
|
||||
dual_stereo_rsv = if total >= 1 << BITRES { 1 << BITRES } else { 0 };
|
||||
total -= dual_stereo_rsv;
|
||||
}
|
||||
}
|
||||
let mut bits1 = [0i32; NB_EBANDS];
|
||||
let mut bits2 = [0i32; NB_EBANDS];
|
||||
let mut thresh = [0i32; NB_EBANDS];
|
||||
let mut trim_offset = [0i32; NB_EBANDS];
|
||||
for j in start..end {
|
||||
let w = EBANDS[j + 1] - EBANDS[j];
|
||||
thresh[j] = (ci << BITRES).max((3 * w << lmi << BITRES) >> 4);
|
||||
trim_offset[j] = ci * w * (alloc_trim - 5 - lmi) * (end as i32 - j as i32 - 1) * (1 << (lmi + BITRES as i32)) >> 6;
|
||||
if w << lmi == 1 {
|
||||
trim_offset[j] -= ci << BITRES;
|
||||
}
|
||||
}
|
||||
let nb_alloc = ALLOC.len() as i32;
|
||||
let mut lo = 1;
|
||||
let mut hi = nb_alloc - 1;
|
||||
loop {
|
||||
let mut done = false;
|
||||
let mut psum = 0;
|
||||
let mid = (lo + hi) >> 1;
|
||||
for j in (start..end).rev() {
|
||||
let n = EBANDS[j + 1] - EBANDS[j];
|
||||
let mut bitsj = ci * n * (ALLOC[mid as usize][j] as i32) << lmi >> 2;
|
||||
if bitsj > 0 {
|
||||
bitsj = (bitsj + trim_offset[j]).max(0);
|
||||
}
|
||||
bitsj += offsets[j];
|
||||
if bitsj >= thresh[j] || done {
|
||||
done = true;
|
||||
psum += bitsj.min(cap[j]);
|
||||
} else if bitsj >= ci << BITRES {
|
||||
psum += ci << BITRES;
|
||||
}
|
||||
}
|
||||
if psum > total {
|
||||
hi = mid - 1;
|
||||
} else {
|
||||
lo = mid + 1;
|
||||
}
|
||||
if lo > hi {
|
||||
break;
|
||||
}
|
||||
}
|
||||
hi = lo;
|
||||
lo -= 1;
|
||||
for j in start..end {
|
||||
let n = EBANDS[j + 1] - EBANDS[j];
|
||||
let mut bits1j = ci * n * (ALLOC[lo as usize][j] as i32) << lmi >> 2;
|
||||
let mut bits2j = if hi >= nb_alloc { cap[j] } else { ci * n * (ALLOC[hi as usize][j] as i32) << lmi >> 2 };
|
||||
if bits1j > 0 {
|
||||
bits1j = (bits1j + trim_offset[j]).max(0);
|
||||
}
|
||||
if bits2j > 0 {
|
||||
bits2j = (bits2j + trim_offset[j]).max(0);
|
||||
}
|
||||
if lo > 0 {
|
||||
bits1j += offsets[j];
|
||||
}
|
||||
bits2j += offsets[j];
|
||||
if offsets[j] > 0 {
|
||||
skip_start = j;
|
||||
}
|
||||
bits2j = (bits2j - bits1j).max(0);
|
||||
bits1[j] = bits1j;
|
||||
bits2[j] = bits2j;
|
||||
}
|
||||
interp_bits2pulses(m, start, end, skip_start, &bits1, &bits2, &thresh, cap, total, skip_rsv, intensity_rsv, dual_stereo_rsv, c, lm, dec)
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
fn interp_bits2pulses(
|
||||
m: &Mode,
|
||||
start: usize,
|
||||
end: usize,
|
||||
skip_start: usize,
|
||||
bits1: &[i32; NB_EBANDS],
|
||||
bits2: &[i32; NB_EBANDS],
|
||||
thresh: &[i32; NB_EBANDS],
|
||||
cap: &[i32; NB_EBANDS],
|
||||
mut total: i32,
|
||||
skip_rsv: i32,
|
||||
mut intensity_rsv: i32,
|
||||
mut dual_stereo_rsv: i32,
|
||||
c: usize,
|
||||
lm: usize,
|
||||
dec: &mut RangeDecoder,
|
||||
) -> Allocation {
|
||||
let ci = c as i32;
|
||||
let alloc_floor = ci << BITRES;
|
||||
let stereo = (c > 1) as i32;
|
||||
let log_m = (lm as i32) << BITRES;
|
||||
let mut lo = 0;
|
||||
let mut hi = 1 << ALLOC_STEPS;
|
||||
for _ in 0..ALLOC_STEPS {
|
||||
let mid = (lo + hi) >> 1;
|
||||
let mut psum = 0;
|
||||
let mut done = false;
|
||||
for j in (start..end).rev() {
|
||||
let tmp = bits1[j] + (mid * bits2[j] >> ALLOC_STEPS);
|
||||
if tmp >= thresh[j] || done {
|
||||
done = true;
|
||||
psum += tmp.min(cap[j]);
|
||||
} else if tmp >= alloc_floor {
|
||||
psum += alloc_floor;
|
||||
}
|
||||
}
|
||||
if psum > total {
|
||||
hi = mid;
|
||||
} else {
|
||||
lo = mid;
|
||||
}
|
||||
}
|
||||
let mut bits = [0i32; NB_EBANDS];
|
||||
let mut psum = 0;
|
||||
let mut done = false;
|
||||
for j in (start..end).rev() {
|
||||
let mut tmp = bits1[j] + (lo * bits2[j] >> ALLOC_STEPS);
|
||||
if tmp < thresh[j] && !done {
|
||||
tmp = if tmp >= alloc_floor { alloc_floor } else { 0 };
|
||||
} else {
|
||||
done = true;
|
||||
}
|
||||
tmp = tmp.min(cap[j]);
|
||||
bits[j] = tmp;
|
||||
psum += tmp;
|
||||
}
|
||||
let s = start;
|
||||
let mut coded_bands = end;
|
||||
loop {
|
||||
let j = coded_bands - 1;
|
||||
if j <= skip_start {
|
||||
total += skip_rsv;
|
||||
break;
|
||||
}
|
||||
let mut left = total - psum;
|
||||
let span = EBANDS[coded_bands] - EBANDS[s];
|
||||
let percoeff = left / span;
|
||||
left -= span * percoeff;
|
||||
let rem = (left - (EBANDS[j] - EBANDS[s])).max(0);
|
||||
let band_width = EBANDS[coded_bands] - EBANDS[j];
|
||||
let mut band_bits = bits[j] + percoeff * band_width + rem;
|
||||
if band_bits >= thresh[j].max(alloc_floor + (1 << BITRES)) {
|
||||
if dec.bit_logp(1) {
|
||||
break;
|
||||
}
|
||||
psum += 1 << BITRES;
|
||||
band_bits -= 1 << BITRES;
|
||||
}
|
||||
psum -= bits[j] + intensity_rsv;
|
||||
if intensity_rsv > 0 {
|
||||
intensity_rsv = LOG2_FRAC_TABLE[j - s];
|
||||
}
|
||||
psum += intensity_rsv;
|
||||
if band_bits >= alloc_floor {
|
||||
psum += alloc_floor;
|
||||
bits[j] = alloc_floor;
|
||||
} else {
|
||||
bits[j] = 0;
|
||||
}
|
||||
coded_bands -= 1;
|
||||
}
|
||||
let intensity = if intensity_rsv > 0 { s + dec.uint((coded_bands + 1 - s) as u32) as usize } else { 0 };
|
||||
if intensity <= s {
|
||||
total += dual_stereo_rsv;
|
||||
dual_stereo_rsv = 0;
|
||||
}
|
||||
let dual_stereo = if dual_stereo_rsv > 0 { dec.bit_logp(1) } else { false };
|
||||
let mut left = total - psum;
|
||||
let span = EBANDS[coded_bands] - EBANDS[s];
|
||||
let percoeff = left / span;
|
||||
left -= span * percoeff;
|
||||
for j in s..coded_bands {
|
||||
bits[j] += percoeff * (EBANDS[j + 1] - EBANDS[j]);
|
||||
}
|
||||
for j in s..coded_bands {
|
||||
let tmp = left.min(EBANDS[j + 1] - EBANDS[j]);
|
||||
bits[j] += tmp;
|
||||
left -= tmp;
|
||||
}
|
||||
let mut ebits = [0i32; NB_EBANDS];
|
||||
let mut fine_priority = [0i32; NB_EBANDS];
|
||||
let mut balance = 0;
|
||||
for j in s..coded_bands {
|
||||
let n0 = EBANDS[j + 1] - EBANDS[j];
|
||||
let n = n0 << lm;
|
||||
let bit = bits[j] + balance;
|
||||
let mut excess;
|
||||
if n > 1 {
|
||||
excess = (bit - cap[j]).max(0);
|
||||
bits[j] = bit - excess;
|
||||
let den = ci * n + if c == 2 && n > 2 && !dual_stereo && j < intensity { 1 } else { 0 };
|
||||
let nclogn = den * (m.log_n[j] + log_m);
|
||||
let mut offset = (nclogn >> 1) - den * FINE_OFFSET;
|
||||
if n == 2 {
|
||||
offset += (den << BITRES) >> 2;
|
||||
}
|
||||
if bits[j] + offset < den * 2 << BITRES {
|
||||
offset += nclogn >> 2;
|
||||
} else if bits[j] + offset < den * 3 << BITRES {
|
||||
offset += nclogn >> 3;
|
||||
}
|
||||
let mut eb = (bits[j] + offset + (den << (BITRES - 1))).max(0);
|
||||
eb = (eb / den) >> BITRES;
|
||||
if ci * eb > (bits[j] >> BITRES) {
|
||||
eb = bits[j] >> stereo >> BITRES;
|
||||
}
|
||||
eb = eb.min(MAX_FINE_BITS);
|
||||
ebits[j] = eb;
|
||||
fine_priority[j] = (eb * (den << BITRES) >= bits[j] + offset) as i32;
|
||||
bits[j] -= ci * eb << BITRES;
|
||||
} else {
|
||||
excess = (bit - (ci << BITRES)).max(0);
|
||||
bits[j] = bit - excess;
|
||||
ebits[j] = 0;
|
||||
fine_priority[j] = 1;
|
||||
}
|
||||
if excess > 0 {
|
||||
let extra_fine = (excess >> (stereo + BITRES as i32)).min(MAX_FINE_BITS - ebits[j]);
|
||||
ebits[j] += extra_fine;
|
||||
let extra_bits = extra_fine * ci << BITRES;
|
||||
fine_priority[j] = (extra_bits >= excess - balance) as i32;
|
||||
excess -= extra_bits;
|
||||
}
|
||||
balance = excess;
|
||||
}
|
||||
for j in coded_bands..end {
|
||||
ebits[j] = bits[j] >> stereo >> BITRES;
|
||||
bits[j] = 0;
|
||||
fine_priority[j] = (ebits[j] < 1) as i32;
|
||||
}
|
||||
Allocation { coded_bands, intensity, dual_stereo, balance, pulses: bits, fine_quant: ebits, fine_priority }
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn log2_frac_values() {
|
||||
assert_eq!(log2_frac(1, 3), 0);
|
||||
assert_eq!(log2_frac(2, 3), 8);
|
||||
assert_eq!(log2_frac(4, 3), 16);
|
||||
assert_eq!(log2_frac(6, 3), 21);
|
||||
assert_eq!(log2_frac(18, 3), 34);
|
||||
assert_eq!(log2_frac(22, 3), 36);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pvq_counts() {
|
||||
// V(N,1) = 2N, V(1,K) = 2 (K>0), V(2,K) = 4K.
|
||||
assert_eq!(pvq_v(5, 1), 10);
|
||||
assert_eq!(pvq_v(1, 7), 2);
|
||||
assert_eq!(pvq_v(2, 9), 36);
|
||||
assert_eq!(pvq_v(3, 2), 18);
|
||||
assert_eq!(pvq_v(0, 0), 1);
|
||||
assert_eq!(pvq_v(0, 3), 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn caps_match_reference_shape() {
|
||||
let m = Mode::new();
|
||||
// 1-bin bands at LM=0 (mono and stereo) are capped at 224.
|
||||
assert!(m.caps[..8].iter().all(|&c| c == 224));
|
||||
assert!(m.caps[21..29].iter().all(|&c| c == 224));
|
||||
let first_rows: [u8; 42] = [
|
||||
224, 224, 224, 224, 224, 224, 224, 224, 160, 160, 160, 160, 185, 185, 185, 178, 178, 168, 134, 61, 37, 224, 224, 224, 224, 224, 224, 224,
|
||||
224, 240, 240, 240, 240, 207, 207, 207, 198, 198, 183, 144, 66, 40,
|
||||
];
|
||||
assert_eq!(&m.caps[..42], &first_rows[..]);
|
||||
}
|
||||
}
|
||||
@@ -1,130 +0,0 @@
|
||||
//! Constant tables of the 48 kHz CELT mode (RFC 6716 §4.3).
|
||||
|
||||
/// Number of energy bands.
|
||||
pub const NB_EBANDS: usize = 21;
|
||||
/// Band edges in units of 2.5 ms MDCT bins (Table 55).
|
||||
pub const EBANDS: [i32; NB_EBANDS + 1] = [0, 1, 2, 3, 4, 5, 6, 7, 8, 10, 12, 14, 16, 20, 24, 28, 34, 40, 48, 60, 78, 100];
|
||||
/// Overlap of the low-overlap window (samples at 48 kHz).
|
||||
pub const OVERLAP: usize = 120;
|
||||
/// Size of the shortest MDCT (2.5 ms).
|
||||
pub const SHORT_MDCT: usize = 120;
|
||||
/// Largest LM (20 ms frames).
|
||||
pub const MAX_LM: usize = 3;
|
||||
/// Pre-emphasis coefficient.
|
||||
pub const PREEMPH: f32 = 0.850_006_1;
|
||||
|
||||
/// Static allocation table (Table 57), indexed `[quality][band]`, in 1/32 bit per MDCT bin.
|
||||
pub const ALLOC: [[u8; NB_EBANDS]; 11] = transpose([
|
||||
[0, 90, 110, 118, 126, 134, 144, 152, 162, 172, 200],
|
||||
[0, 80, 100, 110, 119, 127, 137, 145, 155, 165, 200],
|
||||
[0, 75, 90, 103, 112, 120, 130, 138, 148, 158, 200],
|
||||
[0, 69, 84, 93, 104, 114, 124, 132, 142, 152, 200],
|
||||
[0, 63, 78, 86, 95, 103, 113, 123, 133, 143, 200],
|
||||
[0, 56, 71, 80, 89, 97, 107, 117, 127, 137, 200],
|
||||
[0, 49, 65, 75, 83, 91, 101, 111, 121, 131, 200],
|
||||
[0, 40, 58, 70, 78, 85, 95, 105, 115, 125, 200],
|
||||
[0, 34, 51, 65, 72, 78, 88, 98, 108, 118, 198],
|
||||
[0, 29, 45, 59, 66, 72, 82, 92, 102, 112, 193],
|
||||
[0, 20, 39, 53, 60, 66, 76, 86, 96, 106, 188],
|
||||
[0, 18, 32, 47, 54, 60, 70, 80, 90, 100, 183],
|
||||
[0, 10, 26, 40, 47, 54, 64, 74, 84, 94, 178],
|
||||
[0, 0, 20, 31, 39, 47, 57, 67, 77, 87, 173],
|
||||
[0, 0, 12, 23, 32, 41, 51, 61, 71, 81, 168],
|
||||
[0, 0, 0, 15, 25, 35, 45, 55, 65, 75, 163],
|
||||
[0, 0, 0, 4, 17, 29, 39, 49, 59, 69, 158],
|
||||
[0, 0, 0, 0, 12, 23, 33, 43, 53, 63, 153],
|
||||
[0, 0, 0, 0, 1, 16, 26, 36, 46, 56, 148],
|
||||
[0, 0, 0, 0, 0, 10, 15, 20, 30, 45, 129],
|
||||
[0, 0, 0, 0, 0, 1, 1, 1, 1, 20, 104],
|
||||
]);
|
||||
|
||||
const fn transpose(t: [[u8; 11]; NB_EBANDS]) -> [[u8; NB_EBANDS]; 11] {
|
||||
let mut out = [[0u8; NB_EBANDS]; 11];
|
||||
let mut b = 0;
|
||||
while b < NB_EBANDS {
|
||||
let mut q = 0;
|
||||
while q < 11 {
|
||||
out[q][b] = t[b][q];
|
||||
q += 1;
|
||||
}
|
||||
b += 1;
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
/// Mean band energy (log2 domain) removed before coarse quantisation.
|
||||
pub const E_MEANS: [f32; 25] = [
|
||||
6.4375, 6.25, 5.75, 5.3125, 5.0625, 4.8125, 4.5, 4.375, 4.875, 4.6875, 4.5625, 4.4375, 4.875, 4.625, 4.3125, 4.5, 4.375, 4.625, 4.75, 4.4375,
|
||||
3.75, 3.75, 3.75, 3.75, 3.75,
|
||||
];
|
||||
|
||||
/// Inter-frame prediction coefficient (alpha) per LM, Q15.
|
||||
pub const PRED_COEF: [f32; 4] = [29440.0 / 32768.0, 26112.0 / 32768.0, 21248.0 / 32768.0, 16384.0 / 32768.0];
|
||||
/// Inter-band prediction coefficient (beta) per LM for inter frames, Q15.
|
||||
pub const BETA_COEF: [f32; 4] = [30147.0 / 32768.0, 22282.0 / 32768.0, 12124.0 / 32768.0, 6554.0 / 32768.0];
|
||||
/// Beta for intra frames.
|
||||
pub const BETA_INTRA: f32 = 4915.0 / 32768.0;
|
||||
|
||||
/// Laplace model parameters for coarse energy: `[LM][intra][2*band + {0: prob, 1: decay}]`.
|
||||
pub const E_PROB_MODEL: [[[u8; 42]; 2]; 4] = [
|
||||
[
|
||||
[
|
||||
72, 127, 65, 129, 66, 128, 65, 128, 64, 128, 62, 128, 64, 128, 64, 128, 92, 78, 92, 79, 92, 78, 90, 79, 116, 41, 115, 40, 114, 40, 132,
|
||||
26, 132, 26, 145, 17, 161, 12, 176, 10, 177, 11,
|
||||
],
|
||||
[
|
||||
24, 179, 48, 138, 54, 135, 54, 132, 53, 134, 56, 133, 55, 132, 55, 132, 61, 114, 70, 96, 74, 88, 75, 88, 87, 74, 89, 66, 91, 67, 100, 59,
|
||||
108, 50, 120, 40, 122, 37, 97, 43, 78, 50,
|
||||
],
|
||||
],
|
||||
[
|
||||
[
|
||||
83, 78, 84, 81, 88, 75, 86, 74, 87, 71, 90, 73, 93, 74, 93, 74, 109, 40, 114, 36, 117, 34, 117, 34, 143, 17, 145, 18, 146, 19, 162, 12,
|
||||
165, 10, 178, 7, 189, 6, 190, 8, 177, 9,
|
||||
],
|
||||
[
|
||||
23, 178, 54, 115, 63, 102, 66, 98, 69, 99, 74, 89, 71, 91, 73, 91, 78, 89, 86, 80, 92, 66, 93, 64, 102, 59, 103, 60, 104, 60, 117, 52,
|
||||
123, 44, 138, 35, 133, 31, 97, 38, 77, 45,
|
||||
],
|
||||
],
|
||||
[
|
||||
[
|
||||
61, 90, 93, 60, 105, 42, 107, 41, 110, 45, 116, 38, 113, 38, 112, 38, 124, 26, 132, 27, 136, 19, 140, 20, 155, 14, 159, 16, 158, 18, 170,
|
||||
13, 177, 10, 187, 8, 192, 6, 175, 9, 159, 10,
|
||||
],
|
||||
[
|
||||
21, 178, 59, 110, 71, 86, 75, 85, 84, 83, 91, 66, 88, 73, 87, 72, 92, 75, 98, 72, 105, 58, 107, 54, 115, 52, 114, 55, 112, 56, 129, 51,
|
||||
132, 40, 150, 33, 140, 29, 98, 35, 77, 42,
|
||||
],
|
||||
],
|
||||
[
|
||||
[
|
||||
42, 121, 96, 66, 108, 43, 111, 40, 117, 44, 123, 32, 120, 36, 119, 33, 127, 33, 134, 34, 139, 21, 147, 23, 152, 20, 158, 25, 154, 26,
|
||||
166, 21, 173, 16, 184, 13, 184, 10, 150, 13, 139, 15,
|
||||
],
|
||||
[
|
||||
22, 178, 63, 114, 74, 82, 84, 83, 92, 82, 103, 62, 96, 72, 96, 67, 101, 73, 107, 72, 113, 55, 118, 52, 125, 52, 118, 52, 117, 55, 135,
|
||||
49, 137, 39, 157, 32, 145, 29, 97, 33, 77, 40,
|
||||
],
|
||||
],
|
||||
];
|
||||
|
||||
/// Inverse CDF for coarse energy when only a few bits remain.
|
||||
pub const SMALL_ENERGY_ICDF: [u8; 3] = [2, 1, 0];
|
||||
/// Conservative log2 in 1/8 bits of the number of coded bands (intensity reservation).
|
||||
pub const LOG2_FRAC_TABLE: [i32; 24] = [0, 8, 13, 16, 19, 21, 23, 24, 26, 27, 28, 29, 30, 31, 32, 32, 33, 34, 34, 35, 36, 36, 37, 37];
|
||||
/// TF resolution changes: `[LM][4*transient + 2*tf_select + tf_flag]`.
|
||||
pub const TF_SELECT_TABLE: [[i8; 8]; 4] =
|
||||
[[0, -1, 0, -1, 0, -1, 0, -1], [0, -1, 0, -2, 1, 0, 1, -1], [0, -2, 0, -3, 2, 0, 1, -1], [0, -2, 0, -3, 3, 0, 1, -1]];
|
||||
/// Spreading decision `{7, 2, 21, 2}/32`.
|
||||
pub const SPREAD_ICDF: [u8; 4] = [25, 23, 2, 0];
|
||||
/// Allocation trim (Table 58).
|
||||
pub const TRIM_ICDF: [u8; 11] = [126, 124, 119, 109, 87, 41, 19, 9, 4, 2, 0];
|
||||
/// Post-filter tapset `{2, 1, 1}/4`.
|
||||
pub const TAPSET_ICDF: [u8; 3] = [2, 1, 0];
|
||||
/// Post-filter tap gains per tapset.
|
||||
pub const COMB_GAINS: [[f32; 3]; 3] =
|
||||
[[0.306_640_62, 0.217_041_02, 0.129_638_67], [0.463_867_2, 0.268_066_4, 0.0], [0.799_804_7, 0.100_097_656, 0.0]];
|
||||
|
||||
/// Hadamard "sequency" ordering for 2, 4, 8 and 16 blocks, concatenated.
|
||||
pub const ORDERY: [usize; 30] = [1, 0, 3, 0, 2, 1, 7, 0, 4, 3, 6, 1, 5, 2, 15, 0, 8, 7, 12, 3, 11, 4, 14, 1, 9, 6, 13, 2, 10, 5];
|
||||
@@ -1,477 +0,0 @@
|
||||
//! Single-stream Opus decoder: mode switching, hybrid summation, redundancy and transitions
|
||||
//! (RFC 6716 §4, §4.5).
|
||||
|
||||
use crate::celt::CeltDecoder;
|
||||
use crate::celt::tables::OVERLAP;
|
||||
use crate::packet::{Bandwidth, Mode, Packet};
|
||||
use crate::range::RangeDecoder;
|
||||
use crate::silk::{LostFlag, SilkDecoder};
|
||||
use crate::{Error, Result};
|
||||
|
||||
/// Output sample rates supported by the decoder.
|
||||
pub const SAMPLE_RATES: [u32; 5] = [8000, 12000, 16000, 24000, 48000];
|
||||
|
||||
/// Decoder for one elementary Opus stream (mono or stereo), like `OpusDecoder`.
|
||||
pub struct StreamDecoder {
|
||||
rate: u32,
|
||||
channels: usize,
|
||||
celt: CeltDecoder,
|
||||
silk: SilkDecoder,
|
||||
stream_channels: usize,
|
||||
bandwidth: Bandwidth,
|
||||
mode: Option<Mode>,
|
||||
prev_mode: Option<Mode>,
|
||||
frame_size: usize,
|
||||
prev_redundancy: bool,
|
||||
last_packet_duration: usize,
|
||||
range_final: u32,
|
||||
/// Output gain in Q8 dB (OpusHead output gain).
|
||||
gain_q8: i16,
|
||||
}
|
||||
|
||||
impl StreamDecoder {
|
||||
/// Creates a decoder producing `channels` (1 or 2) at `rate` (8/12/16/24/48 kHz).
|
||||
pub fn new(rate: u32, channels: usize) -> Result<StreamDecoder> {
|
||||
if !SAMPLE_RATES.contains(&rate) {
|
||||
return Err(Error::InvalidArgument("sample rate must be 8000, 12000, 16000, 24000 or 48000"));
|
||||
}
|
||||
if !(1..=2).contains(&channels) {
|
||||
return Err(Error::InvalidArgument("channels must be 1 or 2"));
|
||||
}
|
||||
Ok(StreamDecoder {
|
||||
rate,
|
||||
channels,
|
||||
celt: CeltDecoder::new(rate, channels),
|
||||
silk: SilkDecoder::new(rate, channels),
|
||||
stream_channels: channels,
|
||||
bandwidth: Bandwidth::Fullband,
|
||||
mode: None,
|
||||
prev_mode: None,
|
||||
frame_size: rate as usize / 400,
|
||||
prev_redundancy: false,
|
||||
last_packet_duration: 0,
|
||||
range_final: 0,
|
||||
gain_q8: 0,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn sample_rate(&self) -> u32 {
|
||||
self.rate
|
||||
}
|
||||
|
||||
pub fn channels(&self) -> usize {
|
||||
self.channels
|
||||
}
|
||||
|
||||
/// Final range coder state of the last decoded frame (for conformance checks).
|
||||
pub fn final_range(&self) -> u32 {
|
||||
self.range_final
|
||||
}
|
||||
|
||||
/// Duration (samples per channel) of the last decoded packet.
|
||||
pub fn last_packet_duration(&self) -> usize {
|
||||
self.last_packet_duration
|
||||
}
|
||||
|
||||
/// Sets the output gain in Q8 dB (as in the OpusHead header).
|
||||
pub fn set_gain_q8(&mut self, gain: i16) {
|
||||
self.gain_q8 = gain;
|
||||
}
|
||||
|
||||
/// Disables the 180-degree intensity-stereo phase inversion (RFC 8251 §10).
|
||||
pub fn set_phase_inversion_disabled(&mut self, disabled: bool) {
|
||||
self.celt.disable_inv = disabled;
|
||||
}
|
||||
|
||||
/// `OPUS_RESET_STATE`.
|
||||
pub fn reset(&mut self) {
|
||||
self.celt.reset();
|
||||
self.silk.reset();
|
||||
self.stream_channels = self.channels;
|
||||
self.bandwidth = Bandwidth::Fullband;
|
||||
self.mode = None;
|
||||
self.prev_mode = None;
|
||||
self.frame_size = self.rate as usize / 400;
|
||||
self.prev_redundancy = false;
|
||||
self.last_packet_duration = 0;
|
||||
self.range_final = 0;
|
||||
}
|
||||
|
||||
/// Decodes a packet into interleaved samples appended to `out`. `None` conceals one lost
|
||||
/// packet of the last packet's duration. Returns samples per channel.
|
||||
pub fn decode_interleaved(&mut self, packet: Option<&[u8]>, out: &mut Vec<f32>) -> Result<usize> {
|
||||
match packet {
|
||||
Some(p) if !p.is_empty() => self.decode_packet(p, out),
|
||||
_ => {
|
||||
let n = if self.last_packet_duration > 0 { self.last_packet_duration } else { self.rate as usize / 50 };
|
||||
self.conceal(n, out)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Conceals `n` samples (a multiple of 2.5 ms).
|
||||
pub fn conceal(&mut self, n: usize, out: &mut Vec<f32>) -> Result<usize> {
|
||||
let f2_5 = self.rate as usize / 400;
|
||||
if n == 0 || !n.is_multiple_of(f2_5) {
|
||||
return Err(Error::InvalidArgument("concealment length must be a multiple of 2.5 ms"));
|
||||
}
|
||||
let start = out.len();
|
||||
out.resize(start + n * self.channels, 0.0);
|
||||
let mut done = 0;
|
||||
while done < n {
|
||||
let r = self.decode_frame(None, &mut out[start + done * self.channels..], n - done, false)?;
|
||||
done += r;
|
||||
}
|
||||
self.last_packet_duration = n;
|
||||
Ok(n)
|
||||
}
|
||||
|
||||
/// Decodes the in-band FEC (LBRR) of `packet` for the previous (lost) packet of `n` samples.
|
||||
pub fn decode_fec(&mut self, packet: &[u8], n: usize, out: &mut Vec<f32>) -> Result<usize> {
|
||||
let p = Packet::parse(packet)?;
|
||||
let pfs = p.toc.frame_samples(self.rate);
|
||||
if n < pfs || p.toc.mode == Mode::CeltOnly || self.mode == Some(Mode::CeltOnly) {
|
||||
return self.conceal(n, out);
|
||||
}
|
||||
let start = out.len();
|
||||
if n > pfs {
|
||||
self.conceal(n - pfs, out)?;
|
||||
}
|
||||
self.mode = Some(p.toc.mode);
|
||||
self.bandwidth = p.toc.bandwidth;
|
||||
self.frame_size = pfs;
|
||||
self.stream_channels = p.toc.channels();
|
||||
let off = out.len();
|
||||
out.resize(off + pfs * self.channels, 0.0);
|
||||
self.decode_frame(Some(p.frames[0]), &mut out[off..], pfs, true)?;
|
||||
self.last_packet_duration = n;
|
||||
debug_assert_eq!(out.len() - start, n * self.channels);
|
||||
Ok(n)
|
||||
}
|
||||
|
||||
fn decode_packet(&mut self, data: &[u8], out: &mut Vec<f32>) -> Result<usize> {
|
||||
let p = Packet::parse(data)?;
|
||||
let fs = p.toc.frame_samples(self.rate);
|
||||
self.mode = Some(p.toc.mode);
|
||||
self.bandwidth = p.toc.bandwidth;
|
||||
self.frame_size = fs;
|
||||
self.stream_channels = p.toc.channels();
|
||||
let start = out.len();
|
||||
out.resize(start + fs * p.frames.len() * self.channels, 0.0);
|
||||
let mut nb = 0;
|
||||
for f in &p.frames {
|
||||
let r = self.decode_frame(Some(f), &mut out[start + nb * self.channels..], fs, false)?;
|
||||
nb += r;
|
||||
}
|
||||
out.truncate(start + nb * self.channels);
|
||||
self.last_packet_duration = nb;
|
||||
Ok(nb)
|
||||
}
|
||||
|
||||
/// `opus_decode_frame`: decodes one frame (or conceals when `data` is `None`/≤1 byte).
|
||||
fn decode_frame(&mut self, data: Option<&[u8]>, pcm: &mut [f32], frame_size: usize, fec: bool) -> Result<usize> {
|
||||
let ch = self.channels;
|
||||
let fs = self.rate as usize;
|
||||
let f20 = fs / 50;
|
||||
let f10 = f20 >> 1;
|
||||
let f5 = f10 >> 1;
|
||||
let f2_5 = f5 >> 1;
|
||||
let mut frame_size = frame_size.min(fs / 25 * 3);
|
||||
let data = match data {
|
||||
Some(d) if d.len() > 1 => Some(d),
|
||||
_ => {
|
||||
frame_size = frame_size.min(self.frame_size);
|
||||
None
|
||||
}
|
||||
};
|
||||
let (mut audiosize, mode, bandwidth);
|
||||
match data {
|
||||
Some(_) => {
|
||||
audiosize = self.frame_size;
|
||||
mode = self.mode.unwrap_or(Mode::CeltOnly);
|
||||
bandwidth = Some(self.bandwidth);
|
||||
}
|
||||
None => {
|
||||
audiosize = frame_size;
|
||||
bandwidth = None;
|
||||
mode = match self.prev_mode {
|
||||
None => {
|
||||
pcm[..audiosize * ch].fill(0.0);
|
||||
return Ok(audiosize);
|
||||
}
|
||||
Some(m) => m,
|
||||
};
|
||||
if audiosize > f20 {
|
||||
self.conceal_long(pcm, audiosize, f20)?;
|
||||
return Ok(frame_size);
|
||||
}
|
||||
audiosize = shorten_concealment(audiosize, mode, fs);
|
||||
}
|
||||
}
|
||||
let data_len = data.map_or(0, <[_]>::len);
|
||||
let mut dec_storage;
|
||||
let mut dec: Option<&mut RangeDecoder> = match data {
|
||||
Some(d) => {
|
||||
dec_storage = RangeDecoder::new(d);
|
||||
Some(&mut dec_storage)
|
||||
}
|
||||
None => None,
|
||||
};
|
||||
let mut transition = false;
|
||||
if data.is_some()
|
||||
&& self.prev_mode.is_some()
|
||||
&& ((mode == Mode::CeltOnly && self.prev_mode != Some(Mode::CeltOnly) && !self.prev_redundancy)
|
||||
|| (mode != Mode::CeltOnly && self.prev_mode == Some(Mode::CeltOnly)))
|
||||
{
|
||||
transition = true;
|
||||
}
|
||||
let mut pcm_transition = Vec::new();
|
||||
if transition && mode == Mode::CeltOnly {
|
||||
pcm_transition.resize(f5 * ch, 0.0);
|
||||
let n = f5.min(audiosize);
|
||||
self.decode_frame(None, &mut pcm_transition, n, false)?;
|
||||
}
|
||||
if audiosize > frame_size {
|
||||
return Err(Error::InvalidArgument("frame larger than output buffer"));
|
||||
}
|
||||
frame_size = audiosize;
|
||||
|
||||
// SILK.
|
||||
let mut pcm_silk: Vec<f32> = Vec::new();
|
||||
if mode != Mode::CeltOnly {
|
||||
pcm_silk = self.decode_silk_layer(
|
||||
dec.as_deref_mut(),
|
||||
SilkLayer { mode, bandwidth, lost: lost_flag(data.is_some(), fec), frame_size, audiosize, f10 },
|
||||
)?;
|
||||
}
|
||||
|
||||
let mut start_band = 0;
|
||||
let mut redundancy = Redundancy::none(data_len);
|
||||
if !fec
|
||||
&& mode != Mode::CeltOnly
|
||||
&& let Some(d) = dec.as_deref_mut()
|
||||
{
|
||||
redundancy = Redundancy::parse(d, mode, self.mode == Some(Mode::Hybrid), data_len);
|
||||
}
|
||||
let Redundancy { present: redundancy, celt_to_silk, bytes: redundancy_bytes, len } = redundancy;
|
||||
if mode != Mode::CeltOnly {
|
||||
start_band = 17;
|
||||
}
|
||||
if redundancy {
|
||||
transition = false;
|
||||
}
|
||||
if transition && mode != Mode::CeltOnly {
|
||||
pcm_transition.resize(f5 * ch, 0.0);
|
||||
let n = f5.min(audiosize);
|
||||
self.decode_frame(None, &mut pcm_transition, n, false)?;
|
||||
}
|
||||
if let Some(bw) = bandwidth {
|
||||
self.celt.end = bw.celt_end_band();
|
||||
}
|
||||
self.celt.stream_channels = self.stream_channels;
|
||||
let mut redundant_audio = vec![0f32; if redundancy { f5 * ch } else { 0 }];
|
||||
let mut redundant_rng = 0u32;
|
||||
let red_data = data.map(|d| &d[len..len + redundancy_bytes]);
|
||||
if redundancy && celt_to_silk {
|
||||
self.celt.start = 0;
|
||||
self.celt.decode(red_data, f5, &mut redundant_audio, false, None)?;
|
||||
redundant_rng = self.celt.rng;
|
||||
}
|
||||
self.celt.start = start_band;
|
||||
|
||||
if mode != Mode::SilkOnly {
|
||||
let celt_frame = f20.min(frame_size);
|
||||
if self.prev_mode.is_some() && Some(mode) != self.prev_mode && !self.prev_redundancy {
|
||||
self.celt.reset();
|
||||
}
|
||||
let cdata = if fec { None } else { data.map(|d| &d[..len]) };
|
||||
let cdec = if fec { None } else { dec.as_deref_mut() };
|
||||
self.celt.decode(cdata, celt_frame, pcm, false, cdec)?;
|
||||
} else {
|
||||
pcm[..frame_size * ch].fill(0.0);
|
||||
if self.prev_mode == Some(Mode::Hybrid) && !(redundancy && celt_to_silk && self.prev_redundancy) {
|
||||
self.celt.start = 0;
|
||||
let silence = [0xFFu8, 0xFF];
|
||||
self.celt.decode(Some(&silence), f2_5, pcm, true, None)?;
|
||||
}
|
||||
}
|
||||
if mode != Mode::CeltOnly {
|
||||
for (o, s) in pcm[..frame_size * ch].iter_mut().zip(&pcm_silk) {
|
||||
*o += *s;
|
||||
}
|
||||
}
|
||||
let window = &crate::celt::mode().window;
|
||||
if redundancy && !celt_to_silk {
|
||||
self.celt.reset();
|
||||
self.celt.start = 0;
|
||||
self.celt.decode(red_data, f5, &mut redundant_audio, false, None)?;
|
||||
redundant_rng = self.celt.rng;
|
||||
let o = ch * (frame_size - f2_5);
|
||||
let tail: Vec<f32> = pcm[o..o + f2_5 * ch].to_vec();
|
||||
smooth_fade(&tail, &redundant_audio[ch * f2_5..], &mut pcm[o..], f2_5, ch, window, self.rate);
|
||||
}
|
||||
if redundancy && celt_to_silk {
|
||||
self.fade_in_from(pcm, &redundant_audio, f2_5);
|
||||
}
|
||||
if transition {
|
||||
if audiosize >= f5 {
|
||||
self.fade_in_from(pcm, &pcm_transition, f2_5);
|
||||
} else {
|
||||
let cur: Vec<f32> = pcm[..ch * f2_5].to_vec();
|
||||
smooth_fade(&pcm_transition, &cur, pcm, f2_5, ch, window, self.rate);
|
||||
}
|
||||
}
|
||||
if self.gain_q8 != 0 {
|
||||
let g = (6.488_141e-4f32 * self.gain_q8 as f32).exp2();
|
||||
for v in pcm[..frame_size * ch].iter_mut() {
|
||||
*v *= g;
|
||||
}
|
||||
}
|
||||
self.range_final = match dec.as_deref() {
|
||||
Some(d) if data_len > 1 => d.rng() ^ redundant_rng,
|
||||
_ => 0,
|
||||
};
|
||||
self.prev_mode = Some(mode);
|
||||
self.prev_redundancy = redundancy && !celt_to_silk;
|
||||
Ok(audiosize)
|
||||
}
|
||||
|
||||
/// Conceals a lost span longer than 20 ms by decoding it in 20 ms pieces.
|
||||
fn conceal_long(&mut self, pcm: &mut [f32], audiosize: usize, f20: usize) -> Result<()> {
|
||||
let mut done = 0;
|
||||
while done < audiosize {
|
||||
done += self.decode_frame(None, &mut pcm[done * self.channels..], (audiosize - done).min(f20), false)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Replaces the first `f2_5` samples of `pcm` with `src` and cross-fades the following `f2_5` samples from `src` into `pcm`.
|
||||
fn fade_in_from(&self, pcm: &mut [f32], src: &[f32], f2_5: usize) {
|
||||
let ch = self.channels;
|
||||
let window = &crate::celt::mode().window;
|
||||
pcm[..f2_5 * ch].copy_from_slice(&src[..f2_5 * ch]);
|
||||
let cur: Vec<f32> = pcm[ch * f2_5..ch * 2 * f2_5].to_vec();
|
||||
smooth_fade(&src[ch * f2_5..], &cur, &mut pcm[ch * f2_5..], f2_5, ch, window, self.rate);
|
||||
}
|
||||
|
||||
/// Runs the SILK layer over the frame, zero-filling spans it cannot decode when the packet was lost.
|
||||
fn decode_silk_layer(&mut self, mut dec: Option<&mut RangeDecoder>, l: SilkLayer) -> Result<Vec<f32>> {
|
||||
let ch = self.channels;
|
||||
let mut pcm_silk = vec![0.0; l.frame_size.max(l.f10) * ch];
|
||||
if self.prev_mode == Some(Mode::CeltOnly) {
|
||||
self.silk.reset();
|
||||
}
|
||||
let payload_ms = (1000 * l.audiosize / self.rate as usize).max(10);
|
||||
if l.lost != LostFlag::Lost {
|
||||
self.silk.set_stream(self.stream_channels, silk_internal_rate(l.mode, l.bandwidth));
|
||||
}
|
||||
let mut decoded = 0;
|
||||
while decoded < l.frame_size {
|
||||
let first = decoded == 0;
|
||||
let out = &mut pcm_silk[decoded * ch..];
|
||||
let n = match self.silk.decode(dec.as_deref_mut(), l.lost, first, payload_ms, out) {
|
||||
Ok(n) => n,
|
||||
Err(_) if l.lost != LostFlag::Normal => {
|
||||
let n = l.frame_size - decoded;
|
||||
pcm_silk[decoded * ch..(decoded + n) * ch].fill(0.0);
|
||||
n
|
||||
}
|
||||
Err(e) => return Err(e),
|
||||
};
|
||||
if n == 0 {
|
||||
break;
|
||||
}
|
||||
decoded += n;
|
||||
}
|
||||
Ok(pcm_silk)
|
||||
}
|
||||
}
|
||||
|
||||
/// Inputs for one SILK-layer decode.
|
||||
struct SilkLayer {
|
||||
mode: Mode,
|
||||
bandwidth: Option<Bandwidth>,
|
||||
lost: LostFlag,
|
||||
frame_size: usize,
|
||||
audiosize: usize,
|
||||
f10: usize,
|
||||
}
|
||||
|
||||
fn lost_flag(has_data: bool, fec: bool) -> LostFlag {
|
||||
if !has_data {
|
||||
LostFlag::Lost
|
||||
} else if fec {
|
||||
LostFlag::Lbrr
|
||||
} else {
|
||||
LostFlag::Normal
|
||||
}
|
||||
}
|
||||
|
||||
fn silk_internal_rate(mode: Mode, bandwidth: Option<Bandwidth>) -> u32 {
|
||||
match (mode, bandwidth) {
|
||||
(Mode::SilkOnly, Some(Bandwidth::Narrowband)) => 8000,
|
||||
(Mode::SilkOnly, Some(Bandwidth::Mediumband)) => 12000,
|
||||
_ => 16000,
|
||||
}
|
||||
}
|
||||
|
||||
/// Picks the shorter concealment length for spans of at most 20 ms (10 ms, or 5 ms outside SILK-only mode).
|
||||
fn shorten_concealment(audiosize: usize, mode: Mode, rate: usize) -> usize {
|
||||
let f20 = rate / 50;
|
||||
let f10 = f20 >> 1;
|
||||
let f5 = f10 >> 1;
|
||||
if audiosize >= f20 {
|
||||
audiosize
|
||||
} else if audiosize > f10 {
|
||||
f10
|
||||
} else if mode != Mode::SilkOnly && audiosize > f5 && audiosize < f10 {
|
||||
f5
|
||||
} else {
|
||||
audiosize
|
||||
}
|
||||
}
|
||||
|
||||
/// In-band redundancy header of a packet: whether a redundant CELT frame follows, its direction and size.
|
||||
struct Redundancy {
|
||||
present: bool,
|
||||
celt_to_silk: bool,
|
||||
bytes: usize,
|
||||
/// Payload length left for the main frame.
|
||||
len: usize,
|
||||
}
|
||||
|
||||
impl Redundancy {
|
||||
fn none(len: usize) -> Self {
|
||||
Self { present: false, celt_to_silk: false, bytes: 0, len }
|
||||
}
|
||||
|
||||
fn parse(d: &mut RangeDecoder, mode: Mode, prev_hybrid: bool, len: usize) -> Self {
|
||||
let mut r = Self::none(len);
|
||||
if d.tell() + 17 + 20 * prev_hybrid as i32 > 8 * len as i32 {
|
||||
return r;
|
||||
}
|
||||
r.present = if mode == Mode::Hybrid { d.bit_logp(12) } else { true };
|
||||
if !r.present {
|
||||
return r;
|
||||
}
|
||||
r.celt_to_silk = d.bit_logp(1);
|
||||
r.bytes = if mode == Mode::Hybrid { d.uint(256) as usize + 2 } else { len - ((d.tell() as usize + 7) >> 3) };
|
||||
if r.bytes > len || (len - r.bytes) * 8 < d.tell() as usize {
|
||||
r = Self { celt_to_silk: r.celt_to_silk, ..Self::none(0) };
|
||||
} else {
|
||||
r.len -= r.bytes;
|
||||
}
|
||||
d.shrink_storage(r.bytes);
|
||||
r
|
||||
}
|
||||
}
|
||||
|
||||
fn smooth_fade(in1: &[f32], in2: &[f32], out: &mut [f32], overlap: usize, ch: usize, window: &[f32; OVERLAP], rate: u32) {
|
||||
let inc = (48000 / rate) as usize;
|
||||
for c in 0..ch {
|
||||
for i in 0..overlap {
|
||||
let w = window[i * inc] * window[i * inc];
|
||||
out[i * ch + c] = w * in2[i * ch + c] + (1.0 - w) * in1[i * ch + c];
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,196 +0,0 @@
|
||||
//! Opus identification header (`OpusHead`, RFC 7845 §5.1) and the ISO-BMFF `dOps` box payload.
|
||||
|
||||
use crate::{Error, Result};
|
||||
|
||||
/// Decoder configuration: channel layout, multistream mapping, pre-skip and output gain.
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub struct OpusHead {
|
||||
/// Output channel count (1..=255).
|
||||
pub channels: usize,
|
||||
/// Samples (at 48 kHz) to discard from the start of the decoded stream.
|
||||
pub pre_skip: u16,
|
||||
/// Sample rate of the original input (informational only).
|
||||
pub input_sample_rate: u32,
|
||||
/// Output gain in Q7.8 dB.
|
||||
pub output_gain: i16,
|
||||
/// Channel mapping family (0: mono/stereo, 1: Vorbis order surround, 255: unspecified order).
|
||||
pub mapping_family: u8,
|
||||
/// Number of elementary streams.
|
||||
pub streams: usize,
|
||||
/// Number of coupled (stereo) streams; these come first.
|
||||
pub coupled_streams: usize,
|
||||
/// Output channel → decoded channel index (255 = silence).
|
||||
pub mapping: Vec<u8>,
|
||||
}
|
||||
|
||||
impl OpusHead {
|
||||
/// A family-0 configuration for plain mono/stereo streams (no header present).
|
||||
pub fn simple(channels: usize) -> Result<OpusHead> {
|
||||
if !(1..=2).contains(&channels) {
|
||||
return Err(Error::InvalidArgument("family 0 supports 1 or 2 channels"));
|
||||
}
|
||||
Ok(OpusHead {
|
||||
channels,
|
||||
pre_skip: 0,
|
||||
input_sample_rate: 48000,
|
||||
output_gain: 0,
|
||||
mapping_family: 0,
|
||||
streams: 1,
|
||||
coupled_streams: channels - 1,
|
||||
mapping: (0..channels as u8).collect(),
|
||||
})
|
||||
}
|
||||
|
||||
/// Parses an `OpusHead` packet (Ogg first packet, Matroska `CodecPrivate`).
|
||||
pub fn parse(data: &[u8]) -> Result<OpusHead> {
|
||||
if data.len() < 19 || &data[..8] != b"OpusHead" {
|
||||
return Err(Error::InvalidHeader("missing OpusHead magic"));
|
||||
}
|
||||
let version = data[8];
|
||||
if version >> 4 != 0 {
|
||||
return Err(Error::Unsupported("OpusHead major version"));
|
||||
}
|
||||
let channels = data[9] as usize;
|
||||
let pre_skip = u16::from_le_bytes([data[10], data[11]]);
|
||||
let input_sample_rate = u32::from_le_bytes([data[12], data[13], data[14], data[15]]);
|
||||
let output_gain = i16::from_le_bytes([data[16], data[17]]);
|
||||
let family = data[18];
|
||||
Self::build(channels, pre_skip, input_sample_rate, output_gain, family, &data[19..])
|
||||
}
|
||||
|
||||
/// Parses the payload of an ISO-BMFF `dOps` box (Opus in MP4, big-endian fields).
|
||||
pub fn from_dops(data: &[u8]) -> Result<OpusHead> {
|
||||
if data.len() < 11 {
|
||||
return Err(Error::InvalidHeader("dOps too short"));
|
||||
}
|
||||
if data[0] != 0 {
|
||||
return Err(Error::Unsupported("dOps version"));
|
||||
}
|
||||
let channels = data[1] as usize;
|
||||
let pre_skip = u16::from_be_bytes([data[2], data[3]]);
|
||||
let input_sample_rate = u32::from_be_bytes([data[4], data[5], data[6], data[7]]);
|
||||
let output_gain = i16::from_be_bytes([data[8], data[9]]);
|
||||
Self::build(channels, pre_skip, input_sample_rate, output_gain, data[10], &data[11..])
|
||||
}
|
||||
|
||||
/// Serialises as an `OpusHead` packet.
|
||||
pub fn to_bytes(&self) -> Vec<u8> {
|
||||
let mut v = b"OpusHead".to_vec();
|
||||
v.push(1);
|
||||
v.push(self.channels as u8);
|
||||
v.extend(self.pre_skip.to_le_bytes());
|
||||
v.extend(self.input_sample_rate.to_le_bytes());
|
||||
v.extend(self.output_gain.to_le_bytes());
|
||||
v.push(self.mapping_family);
|
||||
if self.mapping_family != 0 {
|
||||
v.push(self.streams as u8);
|
||||
v.push(self.coupled_streams as u8);
|
||||
v.extend(&self.mapping);
|
||||
}
|
||||
v
|
||||
}
|
||||
|
||||
fn build(channels: usize, pre_skip: u16, input_sample_rate: u32, output_gain: i16, family: u8, rest: &[u8]) -> Result<OpusHead> {
|
||||
if channels == 0 {
|
||||
return Err(Error::InvalidHeader("zero channels"));
|
||||
}
|
||||
let (streams, coupled, mapping) = if family == 0 {
|
||||
if channels > 2 {
|
||||
return Err(Error::InvalidHeader("family 0 allows at most 2 channels"));
|
||||
}
|
||||
(1, channels - 1, (0..channels as u8).collect::<Vec<_>>())
|
||||
} else {
|
||||
if family == 3 {
|
||||
return Err(Error::Unsupported("channel mapping family 3 (projection)"));
|
||||
}
|
||||
if rest.len() < 2 + channels {
|
||||
return Err(Error::InvalidHeader("truncated channel mapping table"));
|
||||
}
|
||||
let streams = rest[0] as usize;
|
||||
let coupled = rest[1] as usize;
|
||||
if streams == 0 || coupled > streams || streams + coupled > 255 {
|
||||
return Err(Error::InvalidHeader("bad stream counts"));
|
||||
}
|
||||
if family == 1 && !(1..=8).contains(&channels) {
|
||||
return Err(Error::InvalidHeader("family 1 allows 1 to 8 channels"));
|
||||
}
|
||||
let mapping = rest[2..2 + channels].to_vec();
|
||||
if mapping.iter().any(|&m| m != 255 && m as usize >= streams + coupled) {
|
||||
return Err(Error::InvalidHeader("channel mapping out of range"));
|
||||
}
|
||||
(streams, coupled, mapping)
|
||||
};
|
||||
Ok(OpusHead { channels, pre_skip, input_sample_rate, output_gain, mapping_family: family, streams, coupled_streams: coupled, mapping })
|
||||
}
|
||||
|
||||
/// Checks the invariants the decoder indexes by, for headers built by hand rather than parsed.
|
||||
pub fn validate(&self) -> Result<()> {
|
||||
if self.channels == 0 {
|
||||
return Err(Error::InvalidHeader("zero channels"));
|
||||
}
|
||||
if self.streams == 0 || self.coupled_streams > self.streams || self.streams + self.coupled_streams > 255 {
|
||||
return Err(Error::InvalidHeader("bad stream counts"));
|
||||
}
|
||||
if self.mapping.len() != self.channels {
|
||||
return Err(Error::InvalidHeader("channel mapping length differs from channel count"));
|
||||
}
|
||||
if self.mapping.iter().any(|&m| m != 255 && m as usize >= self.streams + self.coupled_streams) {
|
||||
return Err(Error::InvalidHeader("channel mapping out of range"));
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Linear output gain factor.
|
||||
pub fn gain_linear(&self) -> f32 {
|
||||
10f32.powf(self.output_gain as f32 / (20.0 * 256.0))
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn parse_family0_and_roundtrip() {
|
||||
let mut h = b"OpusHead".to_vec();
|
||||
h.extend([1, 2, 0x38, 0x01, 0x80, 0xBB, 0, 0, 0x00, 0x01, 0]);
|
||||
let head = OpusHead::parse(&h).unwrap();
|
||||
assert_eq!(head.channels, 2);
|
||||
assert_eq!(head.pre_skip, 312);
|
||||
assert_eq!(head.input_sample_rate, 48000);
|
||||
assert_eq!(head.output_gain, 256);
|
||||
assert_eq!((head.streams, head.coupled_streams), (1, 1));
|
||||
assert_eq!(head.to_bytes(), h);
|
||||
assert!((head.gain_linear() - 1.122).abs() < 1e-3);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_family1_51() {
|
||||
let mut h = b"OpusHead".to_vec();
|
||||
h.extend([1, 6, 0x38, 0x01, 0x80, 0xBB, 0, 0, 0, 0, 1, 4, 2, 0, 4, 1, 2, 3, 5]);
|
||||
let head = OpusHead::parse(&h).unwrap();
|
||||
assert_eq!((head.channels, head.streams, head.coupled_streams), (6, 4, 2));
|
||||
assert_eq!(head.mapping, vec![0, 4, 1, 2, 3, 5]);
|
||||
assert_eq!(OpusHead::parse(&head.to_bytes()).unwrap(), head);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rejects_garbage() {
|
||||
assert!(OpusHead::parse(b"OpusHea").is_err());
|
||||
assert!(OpusHead::parse(b"OpusTags...........").is_err());
|
||||
let mut h = b"OpusHead".to_vec();
|
||||
h.extend([1, 3, 0, 0, 0, 0, 0, 0, 0, 0, 0]);
|
||||
assert!(OpusHead::parse(&h).is_err());
|
||||
let mut h = b"OpusHead".to_vec();
|
||||
h.extend([1, 2, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 5]);
|
||||
assert!(OpusHead::parse(&h).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn dops() {
|
||||
let d = [0u8, 2, 0x01, 0x38, 0, 0, 0xBB, 0x80, 0, 0, 0];
|
||||
let head = OpusHead::from_dops(&d).unwrap();
|
||||
assert_eq!(head.pre_skip, 312);
|
||||
assert_eq!(head.channels, 2);
|
||||
}
|
||||
}
|
||||
@@ -1,46 +0,0 @@
|
||||
//! Clean-room pure-Rust Opus decoder (RFC 6716, updated by RFC 8251).
|
||||
//!
|
||||
//! Layer L0: no dependencies beyond `std`; no `unsafe`; builds for `wasm32-unknown-unknown`.
|
||||
|
||||
// Spec-style indexed loops (FilmCraft allows this lint workspace-wide).
|
||||
#![allow(clippy::needless_range_loop)]
|
||||
#![cfg_attr(not(test), deny(clippy::unwrap_used, clippy::expect_used, clippy::panic, clippy::unimplemented, clippy::todo, clippy::unreachable))]
|
||||
// The fixed-point parts mirror the normative integer arithmetic; keep C-like shift expressions.
|
||||
#![allow(clippy::precedence, clippy::int_plus_one)]
|
||||
|
||||
pub(crate) mod celt;
|
||||
mod decoder;
|
||||
mod header;
|
||||
mod multistream;
|
||||
pub mod packet;
|
||||
pub(crate) mod range;
|
||||
pub(crate) mod silk;
|
||||
|
||||
pub use decoder::{SAMPLE_RATES, StreamDecoder};
|
||||
pub use header::OpusHead;
|
||||
pub use multistream::Decoder;
|
||||
pub use packet::{Bandwidth, Mode, Packet, Toc};
|
||||
|
||||
/// Errors from header parsing and decoding.
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub enum Error {
|
||||
InvalidArgument(&'static str),
|
||||
InvalidPacket(&'static str),
|
||||
InvalidHeader(&'static str),
|
||||
Unsupported(&'static str),
|
||||
}
|
||||
|
||||
impl std::fmt::Display for Error {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
Error::InvalidArgument(s) => write!(f, "invalid argument: {s}"),
|
||||
Error::InvalidPacket(s) => write!(f, "invalid packet: {s}"),
|
||||
Error::InvalidHeader(s) => write!(f, "invalid header: {s}"),
|
||||
Error::Unsupported(s) => write!(f, "unsupported: {s}"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl std::error::Error for Error {}
|
||||
|
||||
pub type Result<T> = std::result::Result<T, Error>;
|
||||
@@ -1,198 +0,0 @@
|
||||
//! Public decoder: OpusHead-configured (multistream) decoding to planar f32.
|
||||
|
||||
use crate::decoder::StreamDecoder;
|
||||
use crate::header::OpusHead;
|
||||
use crate::packet::Packet;
|
||||
use crate::{Error, Result};
|
||||
|
||||
/// Opus decoder for an `OpusHead` configuration (mono, stereo, or multistream surround).
|
||||
///
|
||||
/// Produces planar f32 at the chosen output rate. Pre-skip is *not* removed unless
|
||||
/// [`Decoder::set_trim_pre_skip`] is enabled (containers usually handle it via edit lists /
|
||||
/// granule positions). The header's output gain is applied.
|
||||
pub struct Decoder {
|
||||
head: OpusHead,
|
||||
rate: u32,
|
||||
streams: Vec<StreamDecoder>,
|
||||
trim_pre_skip: bool,
|
||||
skip_remaining: usize,
|
||||
scratch: Vec<Vec<f32>>,
|
||||
}
|
||||
|
||||
impl Decoder {
|
||||
/// Creates a 48 kHz decoder from an `OpusHead` packet (Ogg / Matroska `CodecPrivate`).
|
||||
pub fn new(opus_head: &[u8]) -> Result<Decoder> {
|
||||
Self::from_head(OpusHead::parse(opus_head)?, 48000)
|
||||
}
|
||||
|
||||
/// Creates a decoder for plain mono/stereo packets without a header.
|
||||
pub fn simple(sample_rate: u32, channels: usize) -> Result<Decoder> {
|
||||
Self::from_head(OpusHead::simple(channels)?, sample_rate)
|
||||
}
|
||||
|
||||
/// Creates a decoder for a parsed header at `sample_rate` (8/12/16/24/48 kHz).
|
||||
pub fn from_head(head: OpusHead, sample_rate: u32) -> Result<Decoder> {
|
||||
head.validate()?;
|
||||
let mut streams = Vec::with_capacity(head.streams);
|
||||
for s in 0..head.streams {
|
||||
let ch = if s < head.coupled_streams { 2 } else { 1 };
|
||||
let mut d = StreamDecoder::new(sample_rate, ch)?;
|
||||
d.set_gain_q8(head.output_gain);
|
||||
streams.push(d);
|
||||
}
|
||||
Ok(Decoder { rate: sample_rate, streams, trim_pre_skip: false, skip_remaining: 0, scratch: vec![Vec::new(); head.streams], head })
|
||||
}
|
||||
|
||||
pub fn head(&self) -> &OpusHead {
|
||||
&self.head
|
||||
}
|
||||
|
||||
pub fn sample_rate(&self) -> u32 {
|
||||
self.rate
|
||||
}
|
||||
|
||||
pub fn channels(&self) -> usize {
|
||||
self.head.channels
|
||||
}
|
||||
|
||||
/// Pre-skip in samples at the output rate.
|
||||
pub fn pre_skip(&self) -> usize {
|
||||
self.head.pre_skip as usize * self.rate as usize / 48000
|
||||
}
|
||||
|
||||
/// When enabled, the first [`Self::pre_skip`] output samples are dropped.
|
||||
pub fn set_trim_pre_skip(&mut self, enable: bool) {
|
||||
self.trim_pre_skip = enable;
|
||||
self.skip_remaining = if enable { self.pre_skip() } else { 0 };
|
||||
}
|
||||
|
||||
/// Disables intensity-stereo phase inversion (useful when downmixing; RFC 8251 section 10).
|
||||
pub fn set_phase_inversion_disabled(&mut self, disabled: bool) {
|
||||
for s in &mut self.streams {
|
||||
s.set_phase_inversion_disabled(disabled);
|
||||
}
|
||||
}
|
||||
|
||||
/// Final range coder state of the first stream (conformance testing).
|
||||
pub fn final_range(&self) -> u32 {
|
||||
self.streams.first().map_or(0, |s| s.final_range())
|
||||
}
|
||||
|
||||
pub fn reset(&mut self) {
|
||||
for s in &mut self.streams {
|
||||
s.reset();
|
||||
}
|
||||
self.skip_remaining = if self.trim_pre_skip { self.pre_skip() } else { 0 };
|
||||
}
|
||||
|
||||
/// Decodes one packet (`None` = lost packet, concealed) into planar channels.
|
||||
pub fn decode(&mut self, packet: Option<&[u8]>) -> Result<Vec<Vec<f32>>> {
|
||||
let n = self.streams.len();
|
||||
for b in &mut self.scratch {
|
||||
b.clear();
|
||||
}
|
||||
let mut samples = None;
|
||||
match packet {
|
||||
Some(data) if !data.is_empty() => {
|
||||
let mut off = 0;
|
||||
for s in 0..n {
|
||||
let rest = &data[off..];
|
||||
if rest.is_empty() {
|
||||
return Err(Error::InvalidPacket("truncated multistream packet"));
|
||||
}
|
||||
let used = if s + 1 < n { Packet::parse_self_delimited(rest)?.1 } else { rest.len() };
|
||||
let pkt = &rest[..used];
|
||||
let r = if s + 1 < n {
|
||||
// Re-frame the self-delimited packet as a regular one for the stream decoder.
|
||||
let (p, _) = Packet::parse_self_delimited(pkt)?;
|
||||
let regular = reframe(&p);
|
||||
self.streams[s].decode_interleaved(Some(®ular), &mut self.scratch[s])?
|
||||
} else {
|
||||
self.streams[s].decode_interleaved(Some(pkt), &mut self.scratch[s])?
|
||||
};
|
||||
if *samples.get_or_insert(r) != r {
|
||||
return Err(Error::InvalidPacket("multistream frame durations differ"));
|
||||
}
|
||||
off += used;
|
||||
}
|
||||
}
|
||||
_ => {
|
||||
let dur = match self.streams[0].last_packet_duration() {
|
||||
0 => self.rate as usize / 50,
|
||||
d => d,
|
||||
};
|
||||
for s in 0..n {
|
||||
self.streams[s].conceal(dur, &mut self.scratch[s])?;
|
||||
}
|
||||
samples = Some(dur);
|
||||
}
|
||||
}
|
||||
let samples = samples.unwrap_or(0);
|
||||
let mut out = vec![vec![0f32; samples]; self.head.channels];
|
||||
for (c, o) in out.iter_mut().enumerate() {
|
||||
let m = self.head.mapping[c] as usize;
|
||||
if m == 255 {
|
||||
continue;
|
||||
}
|
||||
let (s, ch, nch) = if m < 2 * self.head.coupled_streams { (m / 2, m % 2, 2) } else { (m - self.head.coupled_streams, 0, 1) };
|
||||
let src = &self.scratch[s];
|
||||
for (i, v) in o.iter_mut().enumerate() {
|
||||
*v = src[i * nch + ch];
|
||||
}
|
||||
}
|
||||
if self.skip_remaining > 0 {
|
||||
let k = self.skip_remaining.min(samples);
|
||||
for o in &mut out {
|
||||
o.drain(..k);
|
||||
}
|
||||
self.skip_remaining -= k;
|
||||
}
|
||||
Ok(out)
|
||||
}
|
||||
}
|
||||
|
||||
/// Rebuilds a regular (code 3 VBR) packet from a parsed self-delimited one.
|
||||
fn reframe(p: &Packet) -> Vec<u8> {
|
||||
let mut v = vec![(p.toc.config << 3) | if p.toc.stereo { 4 } else { 0 } | 3];
|
||||
v.push(0x80 | p.frames.len() as u8);
|
||||
for f in &p.frames[..p.frames.len() - 1] {
|
||||
let s = f.len();
|
||||
if s < 252 {
|
||||
v.push(s as u8);
|
||||
} else {
|
||||
let b0 = 252 + (s & 3);
|
||||
v.push(b0 as u8);
|
||||
v.push(((s - b0) >> 2) as u8);
|
||||
}
|
||||
}
|
||||
for f in &p.frames {
|
||||
v.extend_from_slice(f);
|
||||
}
|
||||
v
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
fn head(channels: usize, streams: usize, coupled: usize, mapping: Vec<u8>) -> OpusHead {
|
||||
OpusHead { channels, pre_skip: 0, input_sample_rate: 48000, output_gain: 0, mapping_family: 1, streams, coupled_streams: coupled, mapping }
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn from_head_rejects_inconsistent_headers() {
|
||||
assert!(Decoder::from_head(head(1, 0, 0, vec![0]), 48000).is_err(), "zero streams");
|
||||
assert!(Decoder::from_head(head(2, 1, 0, vec![0]), 48000).is_err(), "short mapping");
|
||||
assert!(Decoder::from_head(head(1, 1, 0, vec![1]), 48000).is_err(), "mapping index out of range");
|
||||
assert!(Decoder::from_head(head(1, 1, 2, vec![0]), 48000).is_err(), "more coupled than streams");
|
||||
assert!(Decoder::from_head(head(0, 1, 0, vec![]), 48000).is_err(), "zero channels");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn from_head_accepts_valid_headers() {
|
||||
assert!(Decoder::from_head(head(1, 1, 0, vec![0]), 48000).is_ok());
|
||||
assert!(Decoder::from_head(head(2, 1, 1, vec![0, 1]), 48000).is_ok());
|
||||
assert!(Decoder::from_head(head(3, 2, 1, vec![0, 1, 2]), 48000).is_ok());
|
||||
assert!(Decoder::from_head(head(2, 1, 0, vec![0, 255]), 48000).is_ok());
|
||||
}
|
||||
}
|
||||
@@ -1,340 +0,0 @@
|
||||
//! TOC byte and frame packing (RFC 6716 §3).
|
||||
|
||||
use crate::Error;
|
||||
|
||||
/// Coding mode of an Opus frame.
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
pub enum Mode {
|
||||
SilkOnly,
|
||||
Hybrid,
|
||||
CeltOnly,
|
||||
}
|
||||
|
||||
/// Audio bandwidth.
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)]
|
||||
pub enum Bandwidth {
|
||||
/// 4 kHz.
|
||||
Narrowband,
|
||||
/// 6 kHz.
|
||||
Mediumband,
|
||||
/// 8 kHz.
|
||||
Wideband,
|
||||
/// 12 kHz.
|
||||
SuperWideband,
|
||||
/// 20 kHz.
|
||||
Fullband,
|
||||
}
|
||||
|
||||
impl Bandwidth {
|
||||
/// Number of CELT bands coded for this bandwidth.
|
||||
pub fn celt_end_band(self) -> usize {
|
||||
match self {
|
||||
Bandwidth::Narrowband => 13,
|
||||
Bandwidth::Mediumband | Bandwidth::Wideband => 17,
|
||||
Bandwidth::SuperWideband => 19,
|
||||
Bandwidth::Fullband => 21,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Decoded table-of-contents byte.
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
pub struct Toc {
|
||||
pub config: u8,
|
||||
pub mode: Mode,
|
||||
pub bandwidth: Bandwidth,
|
||||
pub stereo: bool,
|
||||
/// Frame count code (0..=3).
|
||||
pub code: u8,
|
||||
}
|
||||
|
||||
impl Toc {
|
||||
pub fn parse(b: u8) -> Toc {
|
||||
let config = b >> 3;
|
||||
let (mode, bandwidth) = match config {
|
||||
0..=3 => (Mode::SilkOnly, Bandwidth::Narrowband),
|
||||
4..=7 => (Mode::SilkOnly, Bandwidth::Mediumband),
|
||||
8..=11 => (Mode::SilkOnly, Bandwidth::Wideband),
|
||||
12..=13 => (Mode::Hybrid, Bandwidth::SuperWideband),
|
||||
14..=15 => (Mode::Hybrid, Bandwidth::Fullband),
|
||||
16..=19 => (Mode::CeltOnly, Bandwidth::Narrowband),
|
||||
20..=23 => (Mode::CeltOnly, Bandwidth::Wideband),
|
||||
24..=27 => (Mode::CeltOnly, Bandwidth::SuperWideband),
|
||||
_ => (Mode::CeltOnly, Bandwidth::Fullband),
|
||||
};
|
||||
Toc { config, mode, bandwidth, stereo: b & 4 != 0, code: b & 3 }
|
||||
}
|
||||
|
||||
/// Samples per frame at 48 kHz.
|
||||
pub fn frame_samples_48k(&self) -> usize {
|
||||
self.frame_samples(48000)
|
||||
}
|
||||
|
||||
/// Samples per frame at `rate`.
|
||||
pub fn frame_samples(&self, rate: u32) -> usize {
|
||||
let rate = rate as usize;
|
||||
let c = self.config as usize;
|
||||
match self.mode {
|
||||
Mode::CeltOnly => (rate << (c & 3)) / 400,
|
||||
Mode::Hybrid => {
|
||||
if c & 1 != 0 {
|
||||
rate / 50
|
||||
} else {
|
||||
rate / 100
|
||||
}
|
||||
}
|
||||
Mode::SilkOnly => match c & 3 {
|
||||
3 => rate * 60 / 1000,
|
||||
s => (rate << s) / 100,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
pub fn channels(&self) -> usize {
|
||||
if self.stereo { 2 } else { 1 }
|
||||
}
|
||||
}
|
||||
|
||||
/// A parsed packet: TOC plus the byte ranges of each frame.
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct Packet<'a> {
|
||||
pub toc: Toc,
|
||||
pub frames: Vec<&'a [u8]>,
|
||||
/// Bytes of padding (code 3 only).
|
||||
pub padding: usize,
|
||||
}
|
||||
|
||||
fn parse_size(data: &[u8]) -> Option<(usize, usize)> {
|
||||
match data.first() {
|
||||
None => None,
|
||||
Some(&b) if b < 252 => Some((b as usize, 1)),
|
||||
Some(&b) => data.get(1).map(|&b1| (4 * b1 as usize + b as usize, 2)),
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> Packet<'a> {
|
||||
/// Parses a packet (RFC 6716 section 3.2), enforcing requirements R1 to R7.
|
||||
pub fn parse(data: &'a [u8]) -> Result<Packet<'a>, Error> {
|
||||
Self::parse_impl(data, false).map(|(p, _)| p)
|
||||
}
|
||||
|
||||
/// Parses a self-delimited packet (RFC 6716 Appendix B, used for all but the last stream of
|
||||
/// a multistream packet); returns the packet and the number of bytes it occupies.
|
||||
pub fn parse_self_delimited(data: &'a [u8]) -> Result<(Packet<'a>, usize), Error> {
|
||||
Self::parse_impl(data, true)
|
||||
}
|
||||
|
||||
fn parse_impl(data: &'a [u8], self_delimited: bool) -> Result<(Packet<'a>, usize), Error> {
|
||||
const BAD: Error = Error::InvalidPacket("malformed packet framing");
|
||||
if data.is_empty() {
|
||||
return Err(Error::InvalidPacket("empty packet"));
|
||||
}
|
||||
let toc = Toc::parse(data[0]);
|
||||
let fs = toc.frame_samples_48k();
|
||||
let mut pos = 1usize;
|
||||
let mut len = data.len() as isize - 1;
|
||||
let mut sizes: Vec<isize> = Vec::new();
|
||||
let mut padding = 0usize;
|
||||
let mut cbr = false;
|
||||
let count;
|
||||
let mut last_size = len;
|
||||
let read_size = |pos: &mut usize, len: &mut isize| -> Result<isize, Error> {
|
||||
let avail = &data[*pos..(*pos + (*len).max(0) as usize).min(data.len())];
|
||||
let (s, n) = parse_size(avail).ok_or(BAD)?;
|
||||
*pos += n;
|
||||
*len -= n as isize;
|
||||
Ok(s as isize)
|
||||
};
|
||||
match toc.code {
|
||||
0 => count = 1,
|
||||
1 => {
|
||||
count = 2;
|
||||
cbr = true;
|
||||
if !self_delimited {
|
||||
if len & 1 != 0 {
|
||||
return Err(BAD);
|
||||
}
|
||||
last_size = len / 2;
|
||||
sizes.push(last_size);
|
||||
}
|
||||
}
|
||||
2 => {
|
||||
count = 2;
|
||||
let s0 = read_size(&mut pos, &mut len)?;
|
||||
if s0 > len {
|
||||
return Err(BAD);
|
||||
}
|
||||
sizes.push(s0);
|
||||
last_size = len - s0;
|
||||
}
|
||||
_ => {
|
||||
if len < 1 {
|
||||
return Err(BAD);
|
||||
}
|
||||
let ch = data[pos];
|
||||
pos += 1;
|
||||
len -= 1;
|
||||
count = (ch & 0x3F) as usize;
|
||||
if count == 0 || fs * count > 5760 {
|
||||
return Err(BAD);
|
||||
}
|
||||
if ch & 0x40 != 0 {
|
||||
loop {
|
||||
if len <= 0 {
|
||||
return Err(BAD);
|
||||
}
|
||||
let b = data[pos];
|
||||
pos += 1;
|
||||
len -= 1;
|
||||
let tmp = if b == 255 { 254 } else { b as isize };
|
||||
len -= tmp;
|
||||
padding += tmp as usize;
|
||||
if b != 255 {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
if len < 0 {
|
||||
return Err(BAD);
|
||||
}
|
||||
cbr = ch & 0x80 == 0;
|
||||
if !cbr {
|
||||
last_size = len;
|
||||
for _ in 0..count - 1 {
|
||||
let before = len;
|
||||
let s = read_size(&mut pos, &mut len)?;
|
||||
if s > len {
|
||||
return Err(BAD);
|
||||
}
|
||||
sizes.push(s);
|
||||
last_size -= (before - len) + s;
|
||||
}
|
||||
if last_size < 0 {
|
||||
return Err(BAD);
|
||||
}
|
||||
} else if !self_delimited {
|
||||
last_size = len / count as isize;
|
||||
if last_size * count as isize != len {
|
||||
return Err(BAD);
|
||||
}
|
||||
for _ in 0..count - 1 {
|
||||
sizes.push(last_size);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if self_delimited {
|
||||
let before = len;
|
||||
let s = read_size(&mut pos, &mut len)?;
|
||||
if s > len {
|
||||
return Err(BAD);
|
||||
}
|
||||
if cbr {
|
||||
if s * count as isize > len {
|
||||
return Err(BAD);
|
||||
}
|
||||
sizes = vec![s; count - 1];
|
||||
} else if (before - len) + s > last_size {
|
||||
return Err(BAD);
|
||||
}
|
||||
last_size = s;
|
||||
}
|
||||
if last_size > 1275 || sizes.iter().any(|&s| s > 1275) {
|
||||
return Err(BAD);
|
||||
}
|
||||
sizes.push(last_size);
|
||||
let mut frames = Vec::with_capacity(count);
|
||||
for &s in &sizes {
|
||||
let s = s as usize;
|
||||
if pos + s > data.len() {
|
||||
return Err(BAD);
|
||||
}
|
||||
frames.push(&data[pos..pos + s]);
|
||||
pos += s;
|
||||
}
|
||||
let total = if self_delimited { pos + padding } else { data.len() };
|
||||
if total > data.len() {
|
||||
return Err(BAD);
|
||||
}
|
||||
Ok((Packet { toc, frames, padding }, total))
|
||||
}
|
||||
}
|
||||
|
||||
/// Number of samples (at 48 kHz) in a packet, or an error for malformed framing.
|
||||
pub fn packet_samples_48k(data: &[u8]) -> Result<usize, Error> {
|
||||
let p = Packet::parse(data)?;
|
||||
Ok(p.toc.frame_samples_48k() * p.frames.len())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn toc_configs() {
|
||||
for cfg in 0u8..32 {
|
||||
let t = Toc::parse(cfg << 3);
|
||||
let ms10 = t.frame_samples(48000) * 10 / 48; // tenths of ms
|
||||
let expect = match cfg {
|
||||
0..=11 => [100, 200, 400, 600][cfg as usize & 3],
|
||||
12..=15 => [100, 200][cfg as usize & 1],
|
||||
_ => [25, 50, 100, 200][cfg as usize & 3],
|
||||
};
|
||||
assert_eq!(ms10, expect, "config {cfg}");
|
||||
}
|
||||
assert!(Toc::parse(0x04).stereo);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn frame_codes() {
|
||||
// code 0
|
||||
let p = Packet::parse(&[0x00, 1, 2, 3]).unwrap();
|
||||
assert_eq!(p.frames, vec![&[1u8, 2, 3][..]]);
|
||||
// code 1 (odd payload is invalid)
|
||||
assert!(Packet::parse(&[0x01, 1, 2, 3]).is_err());
|
||||
let p = Packet::parse(&[0x01, 1, 2, 3, 4]).unwrap();
|
||||
assert_eq!(p.frames.len(), 2);
|
||||
// code 2
|
||||
let p = Packet::parse(&[0x02, 1, 9, 8, 7]).unwrap();
|
||||
assert_eq!(p.frames, vec![&[9u8][..], &[8u8, 7][..]]);
|
||||
assert!(Packet::parse(&[0x02, 5, 1]).is_err());
|
||||
// code 3 CBR with padding: 2 frames of 2 bytes, 3 bytes padding
|
||||
let p = Packet::parse(&[0x03, 0x42, 3, 1, 2, 3, 4, 0, 0, 0]).unwrap();
|
||||
assert_eq!(p.frames, vec![&[1u8, 2][..], &[3u8, 4][..]]);
|
||||
assert_eq!(p.padding, 3);
|
||||
// code 3 VBR
|
||||
let p = Packet::parse(&[0x03, 0x83, 1, 2, 10, 20, 21, 30]).unwrap();
|
||||
assert_eq!(p.frames, vec![&[10u8][..], &[20u8, 21][..], &[30u8][..]]);
|
||||
// too many frames (> 120 ms)
|
||||
assert!(Packet::parse(&[0x03 | (3 << 3), 0x03, 0]).is_err());
|
||||
// zero frames
|
||||
assert!(Packet::parse(&[0x03, 0x00]).is_err());
|
||||
assert!(Packet::parse(&[]).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn two_byte_sizes() {
|
||||
let mut pkt = vec![0x02u8, 252, 1];
|
||||
pkt.extend(std::iter::repeat_n(7u8, 256));
|
||||
pkt.extend([1, 2]);
|
||||
let p = Packet::parse(&pkt).unwrap();
|
||||
assert_eq!(p.frames[0].len(), 256);
|
||||
assert_eq!(p.frames[1], &[1, 2]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn self_delimited() {
|
||||
// code 0 with explicit size, followed by trailing data of the next stream.
|
||||
let (p, used) = Packet::parse_self_delimited(&[0x00, 2, 5, 6, 0xAA, 0xBB]).unwrap();
|
||||
assert_eq!(p.frames, vec![&[5u8, 6][..]]);
|
||||
assert_eq!(used, 4);
|
||||
// code 1: one size for both frames.
|
||||
let (p, used) = Packet::parse_self_delimited(&[0x01, 1, 5, 6, 9]).unwrap();
|
||||
assert_eq!(p.frames, vec![&[5u8][..], &[6u8][..]]);
|
||||
assert_eq!(used, 4);
|
||||
// code 3 VBR, 2 frames: size of frame 0, then size of last frame.
|
||||
let (p, used) = Packet::parse_self_delimited(&[0x03, 0x82, 1, 2, 7, 8, 9, 0xFF]).unwrap();
|
||||
assert_eq!(p.frames, vec![&[7u8][..], &[8u8, 9][..]]);
|
||||
assert_eq!(used, 7);
|
||||
}
|
||||
}
|
||||
@@ -1,497 +0,0 @@
|
||||
//! Range decoder (RFC 6716 §4.1).
|
||||
//!
|
||||
//! The decoder reads range-coded symbols from the front of the buffer and raw bits from the back.
|
||||
//! Reading past either end yields zeros (never panics); callers detect corruption through
|
||||
//! [`RangeDecoder::tell`] exceeding the budget or through out-of-range values.
|
||||
|
||||
const SYM_BITS: u32 = 8;
|
||||
const CODE_BITS: u32 = 32;
|
||||
const SYM_MAX: u32 = (1 << SYM_BITS) - 1;
|
||||
#[cfg(test)]
|
||||
const CODE_SHIFT: u32 = CODE_BITS - SYM_BITS - 1;
|
||||
const CODE_TOP: u32 = 1 << (CODE_BITS - 1);
|
||||
const CODE_BOT: u32 = CODE_TOP >> SYM_BITS;
|
||||
const CODE_EXTRA: u32 = (CODE_BITS - 2) % SYM_BITS + 1;
|
||||
/// Bit resolution of [`RangeDecoder::tell_frac`] (1/8 bit).
|
||||
pub const BITRES: u32 = 3;
|
||||
|
||||
/// Number of bits needed to represent `x` (0 for 0).
|
||||
#[inline]
|
||||
pub fn ilog(x: u32) -> i32 {
|
||||
(32 - x.leading_zeros()) as i32
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct RangeDecoder<'a> {
|
||||
buf: &'a [u8],
|
||||
/// Bytes available to the decoder (may be reduced, e.g. for redundancy).
|
||||
storage: usize,
|
||||
offs: usize,
|
||||
end_offs: usize,
|
||||
end_window: u32,
|
||||
nend_bits: i32,
|
||||
nbits_total: i32,
|
||||
rng: u32,
|
||||
val: u32,
|
||||
rem: u32,
|
||||
/// Set when an out-of-range value was decoded.
|
||||
pub error: bool,
|
||||
}
|
||||
|
||||
impl<'a> RangeDecoder<'a> {
|
||||
pub fn new(buf: &'a [u8]) -> Self {
|
||||
let mut d = RangeDecoder {
|
||||
buf,
|
||||
storage: buf.len(),
|
||||
offs: 0,
|
||||
end_offs: 0,
|
||||
end_window: 0,
|
||||
nend_bits: 0,
|
||||
nbits_total: (CODE_BITS + 1 - ((CODE_BITS - CODE_EXTRA) / SYM_BITS) * SYM_BITS) as i32,
|
||||
rng: 1 << CODE_EXTRA,
|
||||
val: 0,
|
||||
rem: 0,
|
||||
error: false,
|
||||
};
|
||||
d.rem = d.read_byte();
|
||||
d.val = d.rng - 1 - (d.rem >> (SYM_BITS - CODE_EXTRA));
|
||||
d.normalize();
|
||||
d
|
||||
}
|
||||
|
||||
/// Size of the buffer in bytes as seen by the decoder.
|
||||
pub fn storage(&self) -> usize {
|
||||
self.storage
|
||||
}
|
||||
|
||||
/// Shrinks the buffer seen by the decoder (raw bits are then read from the new end).
|
||||
pub fn shrink_storage(&mut self, by: usize) {
|
||||
self.storage = self.storage.saturating_sub(by);
|
||||
}
|
||||
|
||||
pub fn rng(&self) -> u32 {
|
||||
self.rng
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn read_byte(&mut self) -> u32 {
|
||||
if self.offs < self.storage {
|
||||
let b = self.buf[self.offs];
|
||||
self.offs += 1;
|
||||
b as u32
|
||||
} else {
|
||||
0
|
||||
}
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn read_byte_from_end(&mut self) -> u32 {
|
||||
if self.end_offs < self.storage {
|
||||
self.end_offs += 1;
|
||||
self.buf[self.storage - self.end_offs] as u32
|
||||
} else {
|
||||
0
|
||||
}
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn normalize(&mut self) {
|
||||
while self.rng <= CODE_BOT {
|
||||
self.nbits_total += SYM_BITS as i32;
|
||||
self.rng <<= SYM_BITS;
|
||||
let sym = self.rem;
|
||||
self.rem = self.read_byte();
|
||||
let sym = ((sym << SYM_BITS) | self.rem) >> (SYM_BITS - CODE_EXTRA);
|
||||
self.val = ((self.val << SYM_BITS).wrapping_add(SYM_MAX & !sym)) & (CODE_TOP - 1);
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the cumulative frequency of the next symbol for total `ft` (call [`Self::update`] next).
|
||||
#[inline]
|
||||
pub fn decode(&mut self, ft: u32) -> u32 {
|
||||
let ext = self.rng / ft;
|
||||
let s = self.val / ext;
|
||||
// stash ext in rem-free form: recomputed by update()
|
||||
ft - (s + 1).min(ft)
|
||||
}
|
||||
|
||||
/// Like [`Self::decode`] with `ft = 1 << bits`.
|
||||
#[inline]
|
||||
pub fn decode_bin(&mut self, bits: u32) -> u32 {
|
||||
let ext = self.rng >> bits;
|
||||
let s = self.val / ext;
|
||||
(1 << bits) - (s + 1).min(1 << bits)
|
||||
}
|
||||
|
||||
/// Consumes the symbol with cumulative range `[fl, fh)` of total `ft`.
|
||||
#[inline]
|
||||
pub fn update(&mut self, fl: u32, fh: u32, ft: u32) {
|
||||
let ext = self.rng / ft;
|
||||
let s = ext * (ft - fh);
|
||||
self.val = self.val.wrapping_sub(s);
|
||||
self.rng = if fl > 0 { ext * (fh - fl) } else { self.rng - s };
|
||||
self.normalize();
|
||||
}
|
||||
|
||||
/// Like [`Self::update`] for a symbol decoded with [`Self::decode_bin`].
|
||||
#[inline]
|
||||
pub fn update_bin(&mut self, fl: u32, fh: u32, bits: u32) {
|
||||
let ext = self.rng >> bits;
|
||||
let ft = 1u32 << bits;
|
||||
let s = ext * (ft - fh);
|
||||
self.val = self.val.wrapping_sub(s);
|
||||
self.rng = if fl > 0 { ext * (fh - fl) } else { self.rng - s };
|
||||
self.normalize();
|
||||
}
|
||||
|
||||
/// Decodes a binary symbol whose probability of being 1 is `1/2^logp`.
|
||||
#[inline]
|
||||
pub fn bit_logp(&mut self, logp: u32) -> bool {
|
||||
let r = self.rng;
|
||||
let d = self.val;
|
||||
let s = r >> logp;
|
||||
let ret = d < s;
|
||||
if !ret {
|
||||
self.val = d - s;
|
||||
}
|
||||
self.rng = if ret { s } else { r - s };
|
||||
self.normalize();
|
||||
ret
|
||||
}
|
||||
|
||||
/// Decodes a symbol with an "inverse CDF" table (`icdf[i] = ft - cdf[i+1]`, `ft = 1 << ftb`).
|
||||
#[inline]
|
||||
pub fn icdf(&mut self, icdf: &[u8], ftb: u32) -> usize {
|
||||
let mut s = self.rng;
|
||||
let d = self.val;
|
||||
let r = s >> ftb;
|
||||
let mut ret = 0usize;
|
||||
let mut t;
|
||||
loop {
|
||||
t = s;
|
||||
let v = icdf.get(ret).copied().unwrap_or(0) as u32;
|
||||
s = r * v;
|
||||
if d >= s || ret + 1 >= icdf.len() {
|
||||
break;
|
||||
}
|
||||
ret += 1;
|
||||
}
|
||||
// If the table is exhausted without d >= s, force the last symbol (only on corrupt tables).
|
||||
if d < s {
|
||||
s = 0;
|
||||
}
|
||||
self.val = d - s;
|
||||
self.rng = t - s;
|
||||
self.normalize();
|
||||
ret
|
||||
}
|
||||
|
||||
/// Decodes a uniformly distributed integer in `[0, ft)` (`ft >= 2`).
|
||||
pub fn uint(&mut self, ft: u32) -> u32 {
|
||||
debug_assert!(ft > 1);
|
||||
let ft1 = ft - 1;
|
||||
let ftb = ilog(ft1);
|
||||
if ftb > 8 {
|
||||
let ftb = (ftb - 8) as u32;
|
||||
let top = (ft1 >> ftb) + 1;
|
||||
let s = self.decode(top);
|
||||
self.update(s, s + 1, top);
|
||||
let t = (s << ftb) | self.bits(ftb);
|
||||
if t <= ft1 {
|
||||
return t;
|
||||
}
|
||||
self.error = true;
|
||||
ft1
|
||||
} else {
|
||||
let s = self.decode(ft);
|
||||
self.update(s, s + 1, ft);
|
||||
s
|
||||
}
|
||||
}
|
||||
|
||||
/// Reads `bits` raw bits (0..=25) from the end of the buffer.
|
||||
pub fn bits(&mut self, bits: u32) -> u32 {
|
||||
if bits == 0 {
|
||||
return 0;
|
||||
}
|
||||
let mut window = self.end_window;
|
||||
let mut available = self.nend_bits;
|
||||
if (available as u32) < bits {
|
||||
loop {
|
||||
window |= self.read_byte_from_end() << available;
|
||||
available += SYM_BITS as i32;
|
||||
if available > (CODE_BITS - SYM_BITS) as i32 {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
let ret = window & ((1u32 << bits) - 1);
|
||||
window >>= bits;
|
||||
available -= bits as i32;
|
||||
self.end_window = window;
|
||||
self.nend_bits = available;
|
||||
self.nbits_total += bits as i32;
|
||||
ret
|
||||
}
|
||||
|
||||
/// Advances the bit counter so that [`Self::tell`] reports `total` (CELT silence frames).
|
||||
pub fn skip_to_end(&mut self, total: i32) {
|
||||
let t = self.tell();
|
||||
self.nbits_total += total - t;
|
||||
}
|
||||
|
||||
/// Number of bits consumed so far (rounded up).
|
||||
#[inline]
|
||||
pub fn tell(&self) -> i32 {
|
||||
self.nbits_total - ilog(self.rng)
|
||||
}
|
||||
|
||||
/// Number of 1/8 bits consumed so far (rounded up).
|
||||
pub fn tell_frac(&self) -> u32 {
|
||||
let nbits = (self.nbits_total as u32) << BITRES;
|
||||
let mut l = ilog(self.rng) as u32;
|
||||
let mut r = self.rng >> (l - 16);
|
||||
for _ in 0..BITRES {
|
||||
r = (r * r) >> 15;
|
||||
let b = r >> 16;
|
||||
l = (l << 1) | b;
|
||||
r >>= b;
|
||||
}
|
||||
nbits.wrapping_sub(l)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
pub(crate) mod enc {
|
||||
//! Minimal range encoder (RFC 6716 §5.1), used only to build test streams.
|
||||
use super::*;
|
||||
|
||||
pub struct RangeEncoder {
|
||||
out: Vec<u8>,
|
||||
end: Vec<u8>,
|
||||
end_window: u32,
|
||||
nend_bits: i32,
|
||||
rng: u32,
|
||||
low: u32,
|
||||
rem: i32,
|
||||
ext: u32,
|
||||
size: usize,
|
||||
}
|
||||
|
||||
impl RangeEncoder {
|
||||
pub fn new(size: usize) -> Self {
|
||||
RangeEncoder { out: Vec::new(), end: Vec::new(), end_window: 0, nend_bits: 0, rng: CODE_TOP, low: 0, rem: -1, ext: 0, size }
|
||||
}
|
||||
fn carry_out(&mut self, c: u32) {
|
||||
if c != SYM_MAX {
|
||||
let carry = c >> SYM_BITS;
|
||||
if self.rem >= 0 {
|
||||
self.out.push((self.rem as u32 + carry) as u8);
|
||||
}
|
||||
if self.ext > 0 {
|
||||
let sym = (SYM_MAX + carry) & SYM_MAX;
|
||||
for _ in 0..self.ext {
|
||||
self.out.push(sym as u8);
|
||||
}
|
||||
self.ext = 0;
|
||||
}
|
||||
self.rem = (c & SYM_MAX) as i32;
|
||||
} else {
|
||||
self.ext += 1;
|
||||
}
|
||||
}
|
||||
fn normalize(&mut self) {
|
||||
while self.rng <= CODE_BOT {
|
||||
self.carry_out(self.low >> CODE_SHIFT);
|
||||
self.low = (self.low << SYM_BITS) & (CODE_TOP - 1);
|
||||
self.rng <<= SYM_BITS;
|
||||
}
|
||||
}
|
||||
pub fn encode(&mut self, fl: u32, fh: u32, ft: u32) {
|
||||
let r = self.rng / ft;
|
||||
if fl > 0 {
|
||||
self.low += self.rng - r * (ft - fl);
|
||||
self.rng = r * (fh - fl);
|
||||
} else {
|
||||
self.rng -= r * (ft - fh);
|
||||
}
|
||||
self.normalize();
|
||||
}
|
||||
pub fn bit_logp(&mut self, val: bool, logp: u32) {
|
||||
let r = self.rng;
|
||||
let s = r >> logp;
|
||||
let r2 = r - s;
|
||||
if val {
|
||||
self.low += r2;
|
||||
self.rng = s;
|
||||
} else {
|
||||
self.rng = r2;
|
||||
}
|
||||
self.normalize();
|
||||
}
|
||||
pub fn icdf(&mut self, s: usize, icdf: &[u8], ftb: u32) {
|
||||
let r = self.rng >> ftb;
|
||||
if s > 0 {
|
||||
self.low += self.rng - r * icdf[s - 1] as u32;
|
||||
self.rng = r * (icdf[s - 1] as u32 - icdf[s] as u32);
|
||||
} else {
|
||||
self.rng -= r * icdf[s] as u32;
|
||||
}
|
||||
self.normalize();
|
||||
}
|
||||
pub fn uint(&mut self, fl: u32, ft: u32) {
|
||||
let ft1 = ft - 1;
|
||||
let ftb = ilog(ft1);
|
||||
if ftb > 8 {
|
||||
let ftb = (ftb - 8) as u32;
|
||||
let top = (ft1 >> ftb) + 1;
|
||||
let v = fl >> ftb;
|
||||
self.encode(v, v + 1, top);
|
||||
self.bits(fl & ((1 << ftb) - 1), ftb);
|
||||
} else {
|
||||
self.encode(fl, fl + 1, ft);
|
||||
}
|
||||
}
|
||||
pub fn bits(&mut self, val: u32, bits: u32) {
|
||||
let mut window = self.end_window;
|
||||
let mut used = self.nend_bits;
|
||||
if used as u32 + bits > CODE_BITS {
|
||||
while used >= SYM_BITS as i32 {
|
||||
self.end.push((window & SYM_MAX) as u8);
|
||||
window >>= SYM_BITS;
|
||||
used -= SYM_BITS as i32;
|
||||
}
|
||||
}
|
||||
window |= val << used;
|
||||
used += bits as i32;
|
||||
self.end_window = window;
|
||||
self.nend_bits = used;
|
||||
}
|
||||
/// Finalises and returns a buffer of exactly `size` bytes.
|
||||
pub fn done(mut self) -> Vec<u8> {
|
||||
let mut l = CODE_BITS as i32 - ilog(self.rng);
|
||||
let mut msk = (CODE_TOP - 1) >> l;
|
||||
let mut end = (self.low + msk) & !msk;
|
||||
if (end | msk) >= self.low + self.rng {
|
||||
l += 1;
|
||||
msk >>= 1;
|
||||
end = (self.low + msk) & !msk;
|
||||
}
|
||||
while l > 0 {
|
||||
self.carry_out(end >> CODE_SHIFT);
|
||||
end = (end << SYM_BITS) & (CODE_TOP - 1);
|
||||
l -= SYM_BITS as i32;
|
||||
}
|
||||
if self.rem >= 0 || self.ext > 0 {
|
||||
self.carry_out(0);
|
||||
}
|
||||
let mut window = self.end_window;
|
||||
let mut used = self.nend_bits;
|
||||
while used > 0 {
|
||||
self.end.push((window & SYM_MAX) as u8);
|
||||
window >>= SYM_BITS;
|
||||
used -= SYM_BITS as i32;
|
||||
}
|
||||
assert!(self.out.len() + self.end.len() <= self.size, "range encoder overflow");
|
||||
let mut buf = self.out;
|
||||
buf.resize(self.size - self.end.len(), 0);
|
||||
for &b in self.end.iter().rev() {
|
||||
buf.push(b);
|
||||
}
|
||||
buf
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::enc::RangeEncoder;
|
||||
use super::*;
|
||||
|
||||
struct Lcg(u32);
|
||||
impl Lcg {
|
||||
fn next(&mut self) -> u32 {
|
||||
self.0 = self.0.wrapping_mul(1664525).wrapping_add(1013904223);
|
||||
self.0 >> 8
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn roundtrip_mixed_symbols() {
|
||||
let mut rng = Lcg(1);
|
||||
for trial in 0..200 {
|
||||
let n = 1 + (rng.next() % 300) as usize;
|
||||
let mut ops = Vec::new();
|
||||
for _ in 0..n {
|
||||
match rng.next() % 5 {
|
||||
0 => {
|
||||
let ft = 2 + rng.next() % 1000;
|
||||
let v = rng.next() % ft;
|
||||
ops.push((0u8, v, ft));
|
||||
}
|
||||
1 => {
|
||||
let logp = 1 + rng.next() % 15;
|
||||
ops.push((1, rng.next().is_multiple_of(7) as u32, logp));
|
||||
}
|
||||
2 => {
|
||||
let bits = 1 + rng.next() % 24;
|
||||
ops.push((2, rng.next() & ((1 << bits) - 1), bits));
|
||||
}
|
||||
3 => {
|
||||
let ft = 2 + rng.next() % 100_000_000;
|
||||
ops.push((3, rng.next() % ft, ft));
|
||||
}
|
||||
_ => ops.push((4, rng.next() % 4, 0)),
|
||||
}
|
||||
}
|
||||
let icdf: [u8; 4] = [200, 120, 30, 0];
|
||||
let mut e = RangeEncoder::new(8 * n + 16);
|
||||
for &(k, v, p) in &ops {
|
||||
match k {
|
||||
0 => e.encode(v, v + 1, p),
|
||||
1 => e.bit_logp(v != 0, p),
|
||||
2 => e.bits(v, p),
|
||||
3 => e.uint(v, p),
|
||||
_ => e.icdf(v as usize, &icdf, 8),
|
||||
}
|
||||
}
|
||||
let buf = e.done();
|
||||
let mut d = RangeDecoder::new(&buf);
|
||||
for (i, &(k, v, p)) in ops.iter().enumerate() {
|
||||
let got = match k {
|
||||
0 => {
|
||||
let s = d.decode(p);
|
||||
d.update(s, s + 1, p);
|
||||
s
|
||||
}
|
||||
1 => d.bit_logp(p) as u32,
|
||||
2 => d.bits(p),
|
||||
3 => d.uint(p),
|
||||
_ => d.icdf(&icdf, 8) as u32,
|
||||
};
|
||||
assert_eq!(got, v, "trial {trial} op {i} kind {k}");
|
||||
}
|
||||
assert!(!d.error);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn tell_starts_at_one_bit() {
|
||||
let buf = [0u8; 8];
|
||||
let d = RangeDecoder::new(&buf);
|
||||
assert_eq!(d.tell(), 1);
|
||||
assert_eq!(d.tell_frac(), 8);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn empty_and_garbage_never_panic() {
|
||||
let mut d = RangeDecoder::new(&[]);
|
||||
for i in 0..100 {
|
||||
d.bits(1 + i % 20);
|
||||
d.uint(3 + i * 77);
|
||||
d.icdf(&[250, 100, 0], 8);
|
||||
d.bit_logp(1 + i % 15);
|
||||
}
|
||||
let _ = d.tell_frac();
|
||||
}
|
||||
}
|
||||
@@ -1,912 +0,0 @@
|
||||
//! SILK layer decoder (RFC 6716 §4.2).
|
||||
//!
|
||||
//! Parameter decoding and reconstruction follow the normative fixed-point arithmetic so that the
|
||||
//! output matches the reference decoder sample for sample at the internal rate. Resampling to the
|
||||
//! output rate uses our own linear-phase interpolator with the delays of RFC 6716 Table 54.
|
||||
|
||||
pub(crate) mod lpc;
|
||||
mod resampler;
|
||||
#[allow(dead_code)]
|
||||
pub(crate) mod tables;
|
||||
|
||||
use lpc::{MAX_LPC_ORDER, bwexpander, div32_varq, inverse32_varq, nlsf_decode, nlsf2a, rshift_round, sat16, smlawb, smulwb, smulww};
|
||||
use resampler::Resampler;
|
||||
use tables::{
|
||||
CB_NB_10, CB_NB_20, CB_WB_10, CB_WB_20, DELTA_GAIN, EXC_LSB, EXC_SIGN, GAIN_MSB, LBRR_FLAGS, LTP_FILTER, LTP_PERIODICITY, LTP_SCALE, LTP_TAPS_0,
|
||||
LTP_TAPS_1, LTP_TAPS_2, MID_ONLY, NLSF_EXT, NLSF_INTERP, NLSF_SEL_NBMB, NLSF_SEL_WB, NLSF_STAGE1, NLSF_STAGE2, PITCH_CONTOUR, PITCH_DELTA,
|
||||
PITCH_HIGH, PITCH_LOW, PULSE_COUNT, RATE_LEVEL, SHELL_2, SHELL_4, SHELL_8, SHELL_16, STEREO_PRED_JOINT, STEREO_WEIGHTS_Q13, TYPE_ACTIVE,
|
||||
TYPE_INACTIVE, UNIFORM3, UNIFORM4, UNIFORM5, UNIFORM8,
|
||||
};
|
||||
|
||||
use crate::range::RangeDecoder;
|
||||
use crate::{Error, Result};
|
||||
|
||||
const MAX_FRAME: usize = 320;
|
||||
const MAX_SUBFR: usize = 80;
|
||||
const LTP_ORDER: usize = 5;
|
||||
const OUT_BUF: usize = MAX_FRAME + 2 * MAX_SUBFR;
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
pub enum LostFlag {
|
||||
Normal,
|
||||
Lost,
|
||||
Lbrr,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, PartialEq, Eq)]
|
||||
enum Cond {
|
||||
Independently,
|
||||
IndependentlyNoLtpScaling,
|
||||
Conditionally,
|
||||
}
|
||||
|
||||
#[derive(Clone, Default)]
|
||||
struct Indices {
|
||||
gains: [i32; 4],
|
||||
ltp_index: [usize; 4],
|
||||
nlsf: [i32; 17],
|
||||
lag_index: i32,
|
||||
contour_index: usize,
|
||||
signal_type: usize,
|
||||
quant_offset_type: usize,
|
||||
nlsf_interp_q2: i32,
|
||||
per_index: usize,
|
||||
ltp_scale_index: usize,
|
||||
seed: i32,
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
struct Channel {
|
||||
fs_khz: usize,
|
||||
nb_subfr: usize,
|
||||
frame_length: usize,
|
||||
subfr_length: usize,
|
||||
ltp_mem_length: usize,
|
||||
lpc_order: usize,
|
||||
prev_gain_q16: i32,
|
||||
s_lpc_q14: [i32; MAX_LPC_ORDER],
|
||||
out_buf: [i16; OUT_BUF],
|
||||
lag_prev: i32,
|
||||
last_gain_index: i32,
|
||||
prev_signal_type: usize,
|
||||
first_frame_after_reset: bool,
|
||||
prev_nlsf_q15: [i32; MAX_LPC_ORDER],
|
||||
ec_prev_signal_type: usize,
|
||||
ec_prev_lag_index: i32,
|
||||
vad_flags: [bool; 3],
|
||||
lbrr_flag: bool,
|
||||
lbrr_flags: [bool; 3],
|
||||
n_frames_decoded: usize,
|
||||
n_frames_per_packet: usize,
|
||||
loss_cnt: u32,
|
||||
indices: Indices,
|
||||
resampler: Resampler,
|
||||
// PLC memory.
|
||||
plc_lpc_q12: [i32; MAX_LPC_ORDER],
|
||||
plc_gain_q16: i32,
|
||||
plc_pitch: i32,
|
||||
plc_rand: u32,
|
||||
}
|
||||
|
||||
impl Channel {
|
||||
fn new() -> Channel {
|
||||
Channel {
|
||||
fs_khz: 0,
|
||||
nb_subfr: 4,
|
||||
frame_length: 0,
|
||||
subfr_length: 0,
|
||||
ltp_mem_length: 0,
|
||||
lpc_order: 0,
|
||||
prev_gain_q16: 65536,
|
||||
s_lpc_q14: [0; MAX_LPC_ORDER],
|
||||
out_buf: [0; OUT_BUF],
|
||||
lag_prev: 100,
|
||||
last_gain_index: 10,
|
||||
prev_signal_type: 0,
|
||||
first_frame_after_reset: true,
|
||||
prev_nlsf_q15: [0; MAX_LPC_ORDER],
|
||||
ec_prev_signal_type: 0,
|
||||
ec_prev_lag_index: 0,
|
||||
vad_flags: [false; 3],
|
||||
lbrr_flag: false,
|
||||
lbrr_flags: [false; 3],
|
||||
n_frames_decoded: 0,
|
||||
n_frames_per_packet: 1,
|
||||
loss_cnt: 0,
|
||||
indices: Indices::default(),
|
||||
resampler: Resampler::new(16000, 16000),
|
||||
plc_lpc_q12: [0; MAX_LPC_ORDER],
|
||||
plc_gain_q16: 0,
|
||||
plc_pitch: 0,
|
||||
plc_rand: 22222,
|
||||
}
|
||||
}
|
||||
|
||||
fn set_fs(&mut self, fs_khz: usize, api_rate: u32) {
|
||||
self.subfr_length = 5 * fs_khz;
|
||||
let frame_length = self.nb_subfr * self.subfr_length;
|
||||
if self.fs_khz != fs_khz || self.resampler.rates() != (fs_khz as u32 * 1000, api_rate) {
|
||||
self.resampler = Resampler::new(fs_khz as u32 * 1000, api_rate);
|
||||
}
|
||||
if self.fs_khz != fs_khz || frame_length != self.frame_length {
|
||||
if self.fs_khz != fs_khz {
|
||||
self.ltp_mem_length = 20 * fs_khz;
|
||||
self.lpc_order = if fs_khz == 16 { 16 } else { 10 };
|
||||
self.first_frame_after_reset = true;
|
||||
self.lag_prev = 100;
|
||||
self.last_gain_index = 10;
|
||||
self.prev_signal_type = 0;
|
||||
self.out_buf = [0; OUT_BUF];
|
||||
self.s_lpc_q14 = [0; MAX_LPC_ORDER];
|
||||
}
|
||||
self.fs_khz = fs_khz;
|
||||
self.frame_length = frame_length;
|
||||
}
|
||||
}
|
||||
|
||||
fn decode_indices(&mut self, dec: &mut RangeDecoder, frame_index: usize, decode_lbrr: bool, cond: Cond) {
|
||||
let ix = if decode_lbrr || self.vad_flags[frame_index] { dec.icdf(&TYPE_ACTIVE, 8) + 2 } else { dec.icdf(&TYPE_INACTIVE, 8) };
|
||||
let ind = &mut self.indices;
|
||||
ind.signal_type = ix >> 1;
|
||||
ind.quant_offset_type = ix & 1;
|
||||
if cond == Cond::Conditionally {
|
||||
ind.gains[0] = dec.icdf(&DELTA_GAIN, 8) as i32;
|
||||
} else {
|
||||
ind.gains[0] = (dec.icdf(GAIN_MSB[ind.signal_type], 8) as i32) << 3;
|
||||
ind.gains[0] += dec.icdf(&UNIFORM8, 8) as i32;
|
||||
}
|
||||
for i in 1..self.nb_subfr {
|
||||
ind.gains[i] = dec.icdf(&DELTA_GAIN, 8) as i32;
|
||||
}
|
||||
let wb = self.lpc_order == 16;
|
||||
let s1 = (ind.signal_type >> 1) + if wb { 2 } else { 0 };
|
||||
let i1 = dec.icdf(NLSF_STAGE1[s1], 8);
|
||||
ind.nlsf[0] = i1 as i32;
|
||||
for i in 0..self.lpc_order {
|
||||
let cb = if wb { 8 + NLSF_SEL_WB[i1][i] as usize } else { NLSF_SEL_NBMB[i1][i] as usize };
|
||||
let mut v = dec.icdf(NLSF_STAGE2[cb], 8) as i32;
|
||||
if v == 0 {
|
||||
v -= dec.icdf(&NLSF_EXT, 8) as i32;
|
||||
} else if v == 8 {
|
||||
v += dec.icdf(&NLSF_EXT, 8) as i32;
|
||||
}
|
||||
ind.nlsf[i + 1] = v - 4;
|
||||
}
|
||||
ind.nlsf_interp_q2 = if self.nb_subfr == 4 { dec.icdf(&NLSF_INTERP, 8) as i32 } else { 4 };
|
||||
if ind.signal_type == 2 {
|
||||
let mut absolute = true;
|
||||
if cond == Cond::Conditionally && self.ec_prev_signal_type == 2 {
|
||||
let d = dec.icdf(&PITCH_DELTA, 8) as i32;
|
||||
if d > 0 {
|
||||
ind.lag_index = self.ec_prev_lag_index + d - 9;
|
||||
absolute = false;
|
||||
}
|
||||
}
|
||||
if absolute {
|
||||
ind.lag_index = dec.icdf(&PITCH_HIGH, 8) as i32 * (self.fs_khz as i32 >> 1);
|
||||
let low = match self.fs_khz {
|
||||
8 => PITCH_LOW[0],
|
||||
12 => PITCH_LOW[1],
|
||||
_ => PITCH_LOW[2],
|
||||
};
|
||||
ind.lag_index += dec.icdf(low, 8) as i32;
|
||||
}
|
||||
self.ec_prev_lag_index = ind.lag_index;
|
||||
let contour = match (self.fs_khz, self.nb_subfr) {
|
||||
(8, 2) => PITCH_CONTOUR[0],
|
||||
(8, _) => PITCH_CONTOUR[1],
|
||||
(_, 2) => PITCH_CONTOUR[2],
|
||||
_ => PITCH_CONTOUR[3],
|
||||
};
|
||||
ind.contour_index = dec.icdf(contour, 8);
|
||||
ind.per_index = dec.icdf(<P_PERIODICITY, 8);
|
||||
for k in 0..self.nb_subfr {
|
||||
ind.ltp_index[k] = dec.icdf(LTP_FILTER[ind.per_index], 8);
|
||||
}
|
||||
ind.ltp_scale_index = if cond == Cond::Independently { dec.icdf(<P_SCALE, 8) } else { 0 };
|
||||
}
|
||||
self.ec_prev_signal_type = ind.signal_type;
|
||||
ind.seed = dec.icdf(&UNIFORM4, 8) as i32;
|
||||
}
|
||||
|
||||
fn decode_frame(&mut self, dec: Option<&mut RangeDecoder>, out: &mut [i16], lost: LostFlag, cond: Cond) {
|
||||
let l = self.frame_length;
|
||||
let decode = match (lost, &dec) {
|
||||
(LostFlag::Normal, Some(_)) => true,
|
||||
(LostFlag::Lbrr, Some(_)) => self.lbrr_flags[self.n_frames_decoded],
|
||||
_ => false,
|
||||
};
|
||||
if decode && let Some(dec) = dec {
|
||||
self.decode_indices(dec, self.n_frames_decoded, lost == LostFlag::Lbrr, cond);
|
||||
let mut pulses = [0i32; MAX_FRAME + 16];
|
||||
decode_pulses(dec, &mut pulses, self.indices.signal_type, self.indices.quant_offset_type, l);
|
||||
let ctrl = self.decode_parameters(cond);
|
||||
self.decode_core(&ctrl, &pulses, out);
|
||||
self.plc_lpc_q12 = ctrl.pred_coef_q12[1];
|
||||
self.plc_gain_q16 = ctrl.gains_q16[self.nb_subfr - 1];
|
||||
self.plc_pitch = if self.indices.signal_type == 2 { ctrl.pitch_l[self.nb_subfr - 1] } else { 0 };
|
||||
self.loss_cnt = 0;
|
||||
self.prev_signal_type = self.indices.signal_type;
|
||||
self.first_frame_after_reset = false;
|
||||
self.lag_prev = ctrl.pitch_l[self.nb_subfr - 1];
|
||||
} else {
|
||||
self.conceal(out);
|
||||
}
|
||||
// Update the output buffer.
|
||||
let mv = self.ltp_mem_length - l;
|
||||
self.out_buf.copy_within(l..l + mv, 0);
|
||||
self.out_buf[mv..mv + l].copy_from_slice(&out[..l]);
|
||||
}
|
||||
|
||||
fn decode_parameters(&mut self, cond: Cond) -> Ctrl {
|
||||
let mut ctrl = Ctrl::default();
|
||||
let nb = self.nb_subfr;
|
||||
// Gains.
|
||||
let mut prev = self.last_gain_index;
|
||||
for k in 0..nb {
|
||||
let ind = self.indices.gains[k];
|
||||
if k == 0 && cond != Cond::Conditionally {
|
||||
prev = ind.max(prev - 16);
|
||||
} else {
|
||||
let tmp = ind - 4;
|
||||
let thr = 2 * 36 - 64 + prev;
|
||||
if tmp > thr {
|
||||
prev += (tmp << 1) - thr;
|
||||
} else {
|
||||
prev += tmp;
|
||||
}
|
||||
}
|
||||
prev = prev.clamp(0, 63);
|
||||
ctrl.gains_q16[k] = log2lin((smulwb(0x1D1C71, prev) + 2090).min(3967));
|
||||
}
|
||||
self.last_gain_index = prev;
|
||||
// LPC.
|
||||
let order = self.lpc_order;
|
||||
let nlsf = nlsf_decode(&self.indices.nlsf, order == 16);
|
||||
ctrl.pred_coef_q12[1] = nlsf2a(&nlsf[..order]);
|
||||
if self.first_frame_after_reset {
|
||||
self.indices.nlsf_interp_q2 = 4;
|
||||
}
|
||||
if self.indices.nlsf_interp_q2 < 4 {
|
||||
let mut n0 = [0i32; MAX_LPC_ORDER];
|
||||
for i in 0..order {
|
||||
n0[i] = self.prev_nlsf_q15[i] + ((self.indices.nlsf_interp_q2 * (nlsf[i] - self.prev_nlsf_q15[i])) >> 2);
|
||||
}
|
||||
ctrl.pred_coef_q12[0] = nlsf2a(&n0[..order]);
|
||||
} else {
|
||||
ctrl.pred_coef_q12[0] = ctrl.pred_coef_q12[1];
|
||||
}
|
||||
self.prev_nlsf_q15 = nlsf;
|
||||
if self.loss_cnt > 0 {
|
||||
bwexpander(&mut ctrl.pred_coef_q12[0][..order], 63570);
|
||||
bwexpander(&mut ctrl.pred_coef_q12[1][..order], 63570);
|
||||
}
|
||||
if self.indices.signal_type == 2 {
|
||||
let fs = self.fs_khz as i32;
|
||||
let min_lag = 2 * fs;
|
||||
let max_lag = 18 * fs;
|
||||
let lag = min_lag + self.indices.lag_index;
|
||||
for k in 0..nb {
|
||||
let off = match (self.fs_khz, nb) {
|
||||
(8, 2) => CB_NB_10[self.indices.contour_index][k],
|
||||
(8, _) => CB_NB_20[self.indices.contour_index][k],
|
||||
(_, 2) => CB_WB_10[self.indices.contour_index][k],
|
||||
_ => CB_WB_20[self.indices.contour_index][k],
|
||||
};
|
||||
ctrl.pitch_l[k] = (lag + off).clamp(min_lag, max_lag);
|
||||
}
|
||||
for k in 0..nb {
|
||||
let ix = self.indices.ltp_index[k];
|
||||
let taps = match self.indices.per_index {
|
||||
0 => LTP_TAPS_0[ix.min(7)],
|
||||
1 => LTP_TAPS_1[ix.min(15)],
|
||||
_ => LTP_TAPS_2[ix.min(31)],
|
||||
};
|
||||
for i in 0..LTP_ORDER {
|
||||
ctrl.ltp_coef_q14[k * LTP_ORDER + i] = taps[i] << 7;
|
||||
}
|
||||
}
|
||||
ctrl.ltp_scale_q14 = [15565, 12288, 8192][self.indices.ltp_scale_index];
|
||||
}
|
||||
ctrl
|
||||
}
|
||||
|
||||
fn decode_core(&mut self, ctrl: &Ctrl, pulses: &[i32], xq: &mut [i16]) {
|
||||
let ind = &self.indices;
|
||||
let l = self.frame_length;
|
||||
let sub = self.subfr_length;
|
||||
let order = self.lpc_order;
|
||||
let ltp_mem = self.ltp_mem_length;
|
||||
let mut s_ltp = [0i16; MAX_FRAME];
|
||||
let mut s_ltp_q15 = [0i32; 2 * MAX_FRAME];
|
||||
let mut res_q14 = [0i32; MAX_SUBFR];
|
||||
let mut s_lpc = [0i32; MAX_SUBFR + MAX_LPC_ORDER];
|
||||
let offset_q10 = [[100, 240], [32, 100]][ind.signal_type >> 1][ind.quant_offset_type];
|
||||
let interp = ind.nlsf_interp_q2 < 4;
|
||||
let mut exc = [0i32; MAX_FRAME];
|
||||
let mut seed = ind.seed;
|
||||
for i in 0..l {
|
||||
seed = seed.wrapping_mul(196_314_165).wrapping_add(907_633_515);
|
||||
let mut e = pulses[i] << 14;
|
||||
if e > 0 {
|
||||
e -= 80 << 4;
|
||||
} else if e < 0 {
|
||||
e += 80 << 4;
|
||||
}
|
||||
e += offset_q10 << 4;
|
||||
if seed < 0 {
|
||||
e = -e;
|
||||
}
|
||||
exc[i] = e;
|
||||
seed = seed.wrapping_add(pulses[i]);
|
||||
}
|
||||
s_lpc[..MAX_LPC_ORDER].copy_from_slice(&self.s_lpc_q14);
|
||||
let mut ltp_buf_idx = ltp_mem;
|
||||
for k in 0..self.nb_subfr {
|
||||
let a_q12 = &ctrl.pred_coef_q12[k >> 1];
|
||||
let mut b_q14 = [0i32; LTP_ORDER];
|
||||
b_q14.copy_from_slice(&ctrl.ltp_coef_q14[k * LTP_ORDER..(k + 1) * LTP_ORDER]);
|
||||
let mut signal_type = ind.signal_type;
|
||||
let gain_q10 = ctrl.gains_q16[k] >> 6;
|
||||
let mut inv_gain_q31 = inverse32_varq(ctrl.gains_q16[k], 47);
|
||||
let gain_adj_q16 = if ctrl.gains_q16[k] != self.prev_gain_q16 {
|
||||
let g = div32_varq(self.prev_gain_q16, ctrl.gains_q16[k], 16);
|
||||
for v in s_lpc[..MAX_LPC_ORDER].iter_mut() {
|
||||
*v = smulww(g, *v);
|
||||
}
|
||||
g
|
||||
} else {
|
||||
1 << 16
|
||||
};
|
||||
self.prev_gain_q16 = ctrl.gains_q16[k];
|
||||
let mut lag = ctrl.pitch_l[k];
|
||||
if self.loss_cnt > 0 && self.prev_signal_type == 2 && signal_type != 2 && k < 2 {
|
||||
b_q14 = [0; LTP_ORDER];
|
||||
b_q14[LTP_ORDER / 2] = 1 << 12;
|
||||
signal_type = 2;
|
||||
lag = self.lag_prev;
|
||||
}
|
||||
if signal_type == 2 {
|
||||
let lag_u = lag as usize;
|
||||
if k == 0 || (k == 2 && interp) {
|
||||
let start_idx = ltp_mem as isize - lag as isize - order as isize - (LTP_ORDER / 2) as isize;
|
||||
let start_idx = start_idx.max(0) as usize;
|
||||
if k == 2 {
|
||||
self.out_buf[ltp_mem..ltp_mem + 2 * sub].copy_from_slice(&xq[..2 * sub]);
|
||||
}
|
||||
let src_off = start_idx + k * sub;
|
||||
lpc_analysis_filter(&mut s_ltp[start_idx..ltp_mem], &self.out_buf[src_off..src_off + ltp_mem - start_idx], a_q12, order);
|
||||
if k == 0 {
|
||||
inv_gain_q31 = smulwb(inv_gain_q31, ctrl.ltp_scale_q14) << 2;
|
||||
}
|
||||
for i in 0..(lag_u + LTP_ORDER / 2).min(ltp_mem) {
|
||||
s_ltp_q15[ltp_buf_idx - i - 1] = smulwb(inv_gain_q31, s_ltp[ltp_mem - i - 1] as i32);
|
||||
}
|
||||
} else if gain_adj_q16 != 1 << 16 {
|
||||
for i in 0..(lag_u + LTP_ORDER / 2).min(ltp_buf_idx) {
|
||||
let p = ltp_buf_idx - i - 1;
|
||||
s_ltp_q15[p] = smulww(gain_adj_q16, s_ltp_q15[p]);
|
||||
}
|
||||
}
|
||||
let base = ltp_buf_idx as isize - lag as isize + (LTP_ORDER / 2) as isize;
|
||||
for i in 0..sub {
|
||||
let p = base + i as isize;
|
||||
let g = |o: isize| -> i32 { if p - o >= 0 { s_ltp_q15[(p - o) as usize] } else { 0 } };
|
||||
let mut pred = 2i32;
|
||||
pred = smlawb(pred, g(0), b_q14[0]);
|
||||
pred = smlawb(pred, g(1), b_q14[1]);
|
||||
pred = smlawb(pred, g(2), b_q14[2]);
|
||||
pred = smlawb(pred, g(3), b_q14[3]);
|
||||
pred = smlawb(pred, g(4), b_q14[4]);
|
||||
res_q14[i] = exc[k * sub + i].wrapping_add(pred << 1);
|
||||
s_ltp_q15[ltp_buf_idx] = res_q14[i] << 1;
|
||||
ltp_buf_idx += 1;
|
||||
}
|
||||
} else {
|
||||
res_q14[..sub].copy_from_slice(&exc[k * sub..(k + 1) * sub]);
|
||||
}
|
||||
for i in 0..sub {
|
||||
let mut pred = (order >> 1) as i32;
|
||||
for j in 0..order {
|
||||
pred = smlawb(pred, s_lpc[MAX_LPC_ORDER + i - 1 - j], a_q12[j]);
|
||||
}
|
||||
let v = res_q14[i].saturating_add(pred.saturating_mul(16));
|
||||
s_lpc[MAX_LPC_ORDER + i] = v;
|
||||
xq[k * sub + i] = sat16(rshift_round(smulww(v, gain_q10), 8)) as i16;
|
||||
}
|
||||
s_lpc.copy_within(sub..sub + MAX_LPC_ORDER, 0);
|
||||
}
|
||||
self.s_lpc_q14.copy_from_slice(&s_lpc[..MAX_LPC_ORDER]);
|
||||
}
|
||||
|
||||
/// Basic concealment: pitch-periodic repetition for voiced history, otherwise LPC-shaped
|
||||
/// noise at the residual level of the history; both fade out over successive losses.
|
||||
fn conceal(&mut self, out: &mut [i16]) {
|
||||
let l = self.frame_length;
|
||||
if l == 0 {
|
||||
return;
|
||||
}
|
||||
self.loss_cnt += 1;
|
||||
let order = self.lpc_order;
|
||||
let mem = self.ltp_mem_length;
|
||||
let fade = 0.85f32.powi(self.loss_cnt as i32);
|
||||
let hist: Vec<f32> = self.out_buf[..mem].iter().map(|&v| v as f32).collect();
|
||||
let pitch = self.plc_pitch as usize;
|
||||
let mut y = hist.clone();
|
||||
if pitch > 0 && pitch < mem {
|
||||
for _ in 0..l {
|
||||
let v = y[y.len() - pitch] * fade;
|
||||
y.push(v);
|
||||
}
|
||||
} else {
|
||||
let mut a12 = self.plc_lpc_q12;
|
||||
bwexpander(&mut a12[..order], 64880);
|
||||
self.plc_lpc_q12 = a12;
|
||||
let a: Vec<f32> = a12[..order].iter().map(|&v| v as f32 / 4096.0).collect();
|
||||
let mut e = 0f32;
|
||||
for i in order..mem {
|
||||
let mut r = hist[i];
|
||||
for j in 0..order {
|
||||
r -= a[j] * hist[i - 1 - j];
|
||||
}
|
||||
e += r * r;
|
||||
}
|
||||
let rms = (e / (mem - order).max(1) as f32).sqrt() * fade;
|
||||
for _ in 0..l {
|
||||
self.plc_rand = self.plc_rand.wrapping_mul(196_314_165).wrapping_add(907_633_515);
|
||||
let mut v = ((self.plc_rand as i32) >> 16) as f32 / 32768.0 * rms * 1.7;
|
||||
for j in 0..order {
|
||||
v += a[j] * y[y.len() - 1 - j];
|
||||
}
|
||||
y.push(v.clamp(-32768.0, 32767.0));
|
||||
}
|
||||
}
|
||||
for (o, v) in out.iter_mut().zip(&y[mem..mem + l]) {
|
||||
*o = v.clamp(-32768.0, 32767.0) as i16;
|
||||
}
|
||||
// Keep the LPC synthesis state consistent for the next decoded frame.
|
||||
let g = self.prev_gain_q16.max(1) as i64;
|
||||
for j in 0..MAX_LPC_ORDER {
|
||||
let v = out[l - MAX_LPC_ORDER + j] as i64;
|
||||
self.s_lpc_q14[j] = ((v << 30) / g).clamp(i32::MIN as i64, i32::MAX as i64) as i32;
|
||||
}
|
||||
self.lag_prev = self.plc_pitch.max(1);
|
||||
self.first_frame_after_reset = false;
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
struct Ctrl {
|
||||
gains_q16: [i32; 4],
|
||||
pred_coef_q12: [[i32; MAX_LPC_ORDER]; 2],
|
||||
pitch_l: [i32; 4],
|
||||
ltp_coef_q14: [i32; 4 * LTP_ORDER],
|
||||
ltp_scale_q14: i32,
|
||||
}
|
||||
|
||||
fn log2lin(in_log_q7: i32) -> i32 {
|
||||
if in_log_q7 < 0 {
|
||||
return 0;
|
||||
}
|
||||
if in_log_q7 >= 3967 {
|
||||
return i32::MAX;
|
||||
}
|
||||
let out = 1i32 << (in_log_q7 >> 7);
|
||||
let frac = in_log_q7 & 0x7F;
|
||||
if in_log_q7 < 2048 {
|
||||
out + ((out * smlawb(frac, frac * (128 - frac), -174)) >> 7)
|
||||
} else {
|
||||
out + (out >> 7) * smlawb(frac, frac * (128 - frac), -174)
|
||||
}
|
||||
}
|
||||
|
||||
fn lpc_analysis_filter(out: &mut [i16], input: &[i16], b: &[i32], d: usize) {
|
||||
let len = out.len();
|
||||
for ix in d..len {
|
||||
let mut acc: i32 = 0;
|
||||
for j in 0..d {
|
||||
acc = acc.wrapping_add((input[ix - 1 - j] as i32).wrapping_mul(b[j]));
|
||||
}
|
||||
let v = ((input[ix] as i32) << 12).wrapping_sub(acc);
|
||||
out[ix] = sat16(rshift_round(v, 12)) as i16;
|
||||
}
|
||||
for v in out.iter_mut().take(d.min(len)) {
|
||||
*v = 0;
|
||||
}
|
||||
}
|
||||
|
||||
fn decode_pulses(dec: &mut RangeDecoder, pulses: &mut [i32], signal_type: usize, quant_offset_type: usize, frame_length: usize) {
|
||||
let rate_level = dec.icdf(RATE_LEVEL[signal_type >> 1], 8);
|
||||
let mut iter = frame_length >> 4;
|
||||
if iter * 16 < frame_length {
|
||||
iter += 1;
|
||||
}
|
||||
let mut sum_pulses = [0i32; 20];
|
||||
let mut n_lshifts = [0i32; 20];
|
||||
for i in 0..iter {
|
||||
let mut s = dec.icdf(PULSE_COUNT[rate_level], 8) as i32;
|
||||
while s == 17 {
|
||||
n_lshifts[i] += 1;
|
||||
let tab = if n_lshifts[i] == 10 { PULSE_COUNT[10] } else { PULSE_COUNT[9] };
|
||||
s = dec.icdf(tab, 8) as i32;
|
||||
}
|
||||
sum_pulses[i] = s;
|
||||
}
|
||||
for i in 0..iter {
|
||||
let blk = &mut pulses[i * 16..(i + 1) * 16];
|
||||
if sum_pulses[i] > 0 {
|
||||
shell_decode(dec, blk, sum_pulses[i]);
|
||||
} else {
|
||||
blk.fill(0);
|
||||
}
|
||||
}
|
||||
for i in 0..iter {
|
||||
let nls = n_lshifts[i];
|
||||
if nls > 0 {
|
||||
for p in pulses[i * 16..(i + 1) * 16].iter_mut() {
|
||||
let mut a = *p;
|
||||
for _ in 0..nls {
|
||||
a = (a << 1) + dec.icdf(&EXC_LSB, 8) as i32;
|
||||
}
|
||||
*p = a;
|
||||
}
|
||||
sum_pulses[i] |= nls << 5;
|
||||
}
|
||||
}
|
||||
// Signs.
|
||||
let base = 7 * (quant_offset_type + (signal_type << 1));
|
||||
let len = (frame_length + 8) >> 4;
|
||||
for i in 0..len {
|
||||
let p = sum_pulses[i];
|
||||
if p > 0 {
|
||||
let tab = EXC_SIGN[base + ((p & 0x1F) as usize).min(6)];
|
||||
for q in pulses[i * 16..(i + 1) * 16].iter_mut() {
|
||||
if *q > 0 && dec.icdf(tab, 8) == 0 {
|
||||
*q = -*q;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn shell_decode(dec: &mut RangeDecoder, out: &mut [i32], total: i32) {
|
||||
fn split(dec: &mut RangeDecoder, out: &mut [i32], n: usize, count: i32) {
|
||||
if n == 1 {
|
||||
out[0] = count;
|
||||
return;
|
||||
}
|
||||
if count == 0 {
|
||||
out[..n].fill(0);
|
||||
return;
|
||||
}
|
||||
let tab = match n {
|
||||
16 => SHELL_16[(count - 1) as usize],
|
||||
8 => SHELL_8[(count - 1) as usize],
|
||||
4 => SHELL_4[(count - 1) as usize],
|
||||
_ => SHELL_2[(count - 1) as usize],
|
||||
};
|
||||
let left = dec.icdf(tab, 8) as i32;
|
||||
let h = n / 2;
|
||||
let (a, b) = out.split_at_mut(h);
|
||||
split(dec, a, h, left);
|
||||
split(dec, b, h, count - left);
|
||||
}
|
||||
split(dec, out, 16, total.min(16));
|
||||
}
|
||||
|
||||
#[derive(Default, Clone)]
|
||||
struct Stereo {
|
||||
pred_prev_q13: [i32; 2],
|
||||
s_mid: [i16; 2],
|
||||
s_side: [i16; 2],
|
||||
}
|
||||
|
||||
fn stereo_decode_pred(dec: &mut RangeDecoder) -> [i32; 2] {
|
||||
let n = dec.icdf(&STEREO_PRED_JOINT, 8);
|
||||
let mut ix = [[0usize; 3]; 2];
|
||||
ix[0][2] = n / 5;
|
||||
ix[1][2] = n - 5 * ix[0][2];
|
||||
for v in ix.iter_mut() {
|
||||
v[0] = dec.icdf(&UNIFORM3, 8);
|
||||
v[1] = dec.icdf(&UNIFORM5, 8);
|
||||
}
|
||||
let mut pred = [0i32; 2];
|
||||
for (p, v) in pred.iter_mut().zip(ix.iter_mut()) {
|
||||
v[0] += 3 * v[2];
|
||||
let low = STEREO_WEIGHTS_Q13[v[0]];
|
||||
let step = smulwb(STEREO_WEIGHTS_Q13[v[0] + 1] - low, 6554);
|
||||
*p = low + step * (2 * v[1] as i32 + 1);
|
||||
}
|
||||
pred[0] -= pred[1];
|
||||
pred
|
||||
}
|
||||
|
||||
fn stereo_ms_to_lr(st: &mut Stereo, x1: &mut [i16], x2: &mut [i16], pred_q13: [i32; 2], fs_khz: usize, len: usize) {
|
||||
x1[..2].copy_from_slice(&st.s_mid);
|
||||
x2[..2].copy_from_slice(&st.s_side);
|
||||
st.s_mid.copy_from_slice(&x1[len..len + 2]);
|
||||
st.s_side.copy_from_slice(&x2[len..len + 2]);
|
||||
let mut p0 = st.pred_prev_q13[0];
|
||||
let mut p1 = st.pred_prev_q13[1];
|
||||
let denom_q16 = (1 << 16) / (8 * fs_khz as i32);
|
||||
let d0 = rshift_round((pred_q13[0] - st.pred_prev_q13[0]) as i16 as i32 * denom_q16 as i16 as i32, 16);
|
||||
let d1 = rshift_round((pred_q13[1] - st.pred_prev_q13[1]) as i16 as i32 * denom_q16 as i16 as i32, 16);
|
||||
let interp = 8 * fs_khz;
|
||||
for n in 0..len {
|
||||
if n < interp {
|
||||
p0 += d0;
|
||||
p1 += d1;
|
||||
} else if n == interp {
|
||||
p0 = pred_q13[0];
|
||||
p1 = pred_q13[1];
|
||||
}
|
||||
let sum = ((x1[n] as i32 + x1[n + 2] as i32) + ((x1[n + 1] as i32) << 1)) << 9;
|
||||
let mut s = smlawb((x2[n + 1] as i32) << 8, sum, p0);
|
||||
s = smlawb(s, (x1[n + 1] as i32) << 11, p1);
|
||||
x2[n + 1] = sat16(rshift_round(s, 8)) as i16;
|
||||
}
|
||||
st.pred_prev_q13 = pred_q13;
|
||||
for n in 0..len {
|
||||
let a = x1[n + 1] as i32;
|
||||
let b = x2[n + 1] as i32;
|
||||
x1[n + 1] = sat16(a + b) as i16;
|
||||
x2[n + 1] = sat16(a - b) as i16;
|
||||
}
|
||||
}
|
||||
|
||||
/// SILK decoder for up to two internal channels (`silk_decoder`).
|
||||
pub struct SilkDecoder {
|
||||
api_rate: u32,
|
||||
api_channels: usize,
|
||||
ch: [Channel; 2],
|
||||
stereo: Stereo,
|
||||
channels_internal: usize,
|
||||
channels_internal_prev: usize,
|
||||
internal_rate: u32,
|
||||
prev_decode_only_middle: bool,
|
||||
ms_pred: [i32; 2],
|
||||
decode_only_middle: bool,
|
||||
}
|
||||
|
||||
impl SilkDecoder {
|
||||
pub fn new(api_rate: u32, api_channels: usize) -> SilkDecoder {
|
||||
SilkDecoder {
|
||||
api_rate,
|
||||
api_channels,
|
||||
ch: [Channel::new(), Channel::new()],
|
||||
stereo: Stereo::default(),
|
||||
channels_internal: api_channels,
|
||||
channels_internal_prev: 0,
|
||||
internal_rate: 16000,
|
||||
prev_decode_only_middle: false,
|
||||
ms_pred: [0; 2],
|
||||
decode_only_middle: false,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn reset(&mut self) {
|
||||
self.ch = [Channel::new(), Channel::new()];
|
||||
self.stereo = Stereo::default();
|
||||
self.prev_decode_only_middle = false;
|
||||
self.channels_internal_prev = 0;
|
||||
}
|
||||
|
||||
pub fn set_stream(&mut self, channels: usize, internal_rate: u32) {
|
||||
self.channels_internal = channels;
|
||||
self.internal_rate = internal_rate;
|
||||
}
|
||||
|
||||
/// Decodes one SILK frame (10 or 20 ms) into interleaved `out` at the API rate.
|
||||
pub fn decode(
|
||||
&mut self,
|
||||
mut dec: Option<&mut RangeDecoder>,
|
||||
lost: LostFlag,
|
||||
new_packet: bool,
|
||||
payload_ms: usize,
|
||||
out: &mut [f32],
|
||||
) -> Result<usize> {
|
||||
let nci = self.channels_internal;
|
||||
if new_packet {
|
||||
for c in self.ch.iter_mut() {
|
||||
c.n_frames_decoded = 0;
|
||||
}
|
||||
}
|
||||
if nci > self.channels_internal_prev && self.channels_internal_prev != 0 {
|
||||
self.ch[1] = Channel::new();
|
||||
}
|
||||
let stereo_to_mono = nci == 1 && self.channels_internal_prev == 2 && self.internal_rate == 1000 * self.ch[0].fs_khz as u32;
|
||||
if self.ch[0].n_frames_decoded == 0 {
|
||||
self.configure_channels(nci, payload_ms)?;
|
||||
}
|
||||
if self.api_channels == 2 && nci == 2 && self.channels_internal_prev == 1 {
|
||||
self.stereo.pred_prev_q13 = [0; 2];
|
||||
self.stereo.s_side = [0; 2];
|
||||
self.ch[1].resampler = self.ch[0].resampler.clone();
|
||||
}
|
||||
self.channels_internal_prev = nci;
|
||||
|
||||
if lost != LostFlag::Lost && self.ch[0].n_frames_decoded == 0 {
|
||||
let d = dec.as_deref_mut().ok_or(Error::InvalidPacket("missing SILK data"))?;
|
||||
self.read_packet_flags(d, nci);
|
||||
if lost == LostFlag::Normal {
|
||||
self.skip_lbrr_data(d, nci);
|
||||
}
|
||||
}
|
||||
if nci == 2 {
|
||||
self.decode_stereo_prediction(dec.as_deref_mut(), lost);
|
||||
}
|
||||
if nci == 2 && !self.decode_only_middle && self.prev_decode_only_middle {
|
||||
self.reset_side_channel();
|
||||
}
|
||||
let l = self.ch[0].frame_length;
|
||||
let mut buf = [[0i16; MAX_FRAME + 2]; 2];
|
||||
self.decode_channel_frames(dec, lost, nci, &mut buf);
|
||||
if self.api_channels == 2 && nci == 2 {
|
||||
let (a, b) = buf.split_at_mut(1);
|
||||
stereo_ms_to_lr(&mut self.stereo, &mut a[0], &mut b[0], self.ms_pred, self.ch[0].fs_khz, l);
|
||||
} else {
|
||||
buf[0][..2].copy_from_slice(&self.stereo.s_mid);
|
||||
self.stereo.s_mid.copy_from_slice(&buf[0][l..l + 2]);
|
||||
}
|
||||
let n_out = self.resample_to_output(&buf, nci, stereo_to_mono, out);
|
||||
if lost == LostFlag::Lost {
|
||||
for c in self.ch.iter_mut().take(nci) {
|
||||
c.last_gain_index = 10;
|
||||
}
|
||||
} else {
|
||||
self.prev_decode_only_middle = self.decode_only_middle;
|
||||
}
|
||||
Ok(n_out)
|
||||
}
|
||||
|
||||
/// Applies the packet's frame layout and internal sampling rate to the first `nci` channels.
|
||||
fn configure_channels(&mut self, nci: usize, payload_ms: usize) -> Result<()> {
|
||||
let (nfpp, nb) = match payload_ms {
|
||||
10 => (1, 2),
|
||||
20 => (1, 4),
|
||||
40 => (2, 4),
|
||||
60 => (3, 4),
|
||||
_ => return Err(Error::InvalidPacket("bad SILK frame size")),
|
||||
};
|
||||
let fs_khz = ((self.internal_rate >> 10) + 1) as usize;
|
||||
for c in self.ch.iter_mut().take(nci) {
|
||||
c.n_frames_per_packet = nfpp;
|
||||
c.nb_subfr = nb;
|
||||
c.set_fs(fs_khz, self.api_rate);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Reads the per-frame VAD flags and LBRR flags at the start of a packet.
|
||||
fn read_packet_flags(&mut self, d: &mut RangeDecoder, nci: usize) {
|
||||
for c in self.ch.iter_mut().take(nci) {
|
||||
for i in 0..c.n_frames_per_packet {
|
||||
c.vad_flags[i] = d.bit_logp(1);
|
||||
}
|
||||
c.lbrr_flag = d.bit_logp(1);
|
||||
}
|
||||
for c in self.ch.iter_mut().take(nci) {
|
||||
c.lbrr_flags = [false; 3];
|
||||
if !c.lbrr_flag {
|
||||
continue;
|
||||
}
|
||||
if c.n_frames_per_packet == 1 {
|
||||
c.lbrr_flags[0] = true;
|
||||
} else {
|
||||
let sym = d.icdf(LBRR_FLAGS[c.n_frames_per_packet - 2], 8) + 1;
|
||||
for i in 0..c.n_frames_per_packet {
|
||||
c.lbrr_flags[i] = (sym >> i) & 1 != 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Consumes (without using) the in-band LBRR frames of a normally decoded packet.
|
||||
fn skip_lbrr_data(&mut self, d: &mut RangeDecoder, nci: usize) {
|
||||
for i in 0..self.ch[0].n_frames_per_packet {
|
||||
for n in 0..nci {
|
||||
if !self.ch[n].lbrr_flags[i] {
|
||||
continue;
|
||||
}
|
||||
if nci == 2 && n == 0 {
|
||||
stereo_decode_pred(d);
|
||||
if !self.ch[1].lbrr_flags[i] {
|
||||
d.icdf(&MID_ONLY, 8);
|
||||
}
|
||||
}
|
||||
let cond = if i > 0 && self.ch[n].lbrr_flags[i - 1] { Cond::Conditionally } else { Cond::Independently };
|
||||
let c = &mut self.ch[n];
|
||||
c.decode_indices(d, i, true, cond);
|
||||
let mut pulses = [0i32; MAX_FRAME + 16];
|
||||
decode_pulses(d, &mut pulses, c.indices.signal_type, c.indices.quant_offset_type, c.frame_length);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Reads the mid/side prediction weights, or reuses the previous ones when this frame carries none.
|
||||
fn decode_stereo_prediction(&mut self, dec: Option<&mut RangeDecoder>, lost: LostFlag) {
|
||||
let fi = self.ch[0].n_frames_decoded;
|
||||
let coded = lost == LostFlag::Normal || (lost == LostFlag::Lbrr && self.ch[0].lbrr_flags[fi]);
|
||||
match dec {
|
||||
Some(d) if coded => {
|
||||
self.ms_pred = stereo_decode_pred(d);
|
||||
let side_inactive = (lost == LostFlag::Normal && !self.ch[1].vad_flags[fi]) || (lost == LostFlag::Lbrr && !self.ch[1].lbrr_flags[fi]);
|
||||
self.decode_only_middle = side_inactive && d.icdf(&MID_ONLY, 8) == 1;
|
||||
}
|
||||
_ => self.ms_pred = self.stereo.pred_prev_q13,
|
||||
}
|
||||
}
|
||||
|
||||
/// Resets the side channel's decoder state when it resumes after a mid-only stretch.
|
||||
fn reset_side_channel(&mut self) {
|
||||
let c = &mut self.ch[1];
|
||||
c.out_buf = [0; OUT_BUF];
|
||||
c.s_lpc_q14 = [0; MAX_LPC_ORDER];
|
||||
c.lag_prev = 100;
|
||||
c.last_gain_index = 10;
|
||||
c.prev_signal_type = 0;
|
||||
c.first_frame_after_reset = true;
|
||||
}
|
||||
|
||||
/// Frame-coding condition for channel `n` of the frame currently being decoded.
|
||||
fn frame_condition(&self, n: usize, lost: LostFlag) -> Cond {
|
||||
let frame_index = self.ch[0].n_frames_decoded as isize - n as isize;
|
||||
if frame_index <= 0 {
|
||||
Cond::Independently
|
||||
} else if lost == LostFlag::Lbrr {
|
||||
if self.ch[n].lbrr_flags[frame_index as usize - 1] { Cond::Conditionally } else { Cond::Independently }
|
||||
} else if n > 0 && self.prev_decode_only_middle {
|
||||
Cond::IndependentlyNoLtpScaling
|
||||
} else {
|
||||
Cond::Conditionally
|
||||
}
|
||||
}
|
||||
|
||||
/// Decodes the current frame of every active channel into `buf` (silence for an inactive side channel).
|
||||
fn decode_channel_frames(&mut self, mut dec: Option<&mut RangeDecoder>, lost: LostFlag, nci: usize, buf: &mut [[i16; MAX_FRAME + 2]; 2]) {
|
||||
let has_side = if lost == LostFlag::Normal {
|
||||
!self.decode_only_middle
|
||||
} else {
|
||||
!self.prev_decode_only_middle || (nci == 2 && lost == LostFlag::Lbrr && self.ch[1].lbrr_flags[self.ch[1].n_frames_decoded])
|
||||
};
|
||||
let l = self.ch[0].frame_length;
|
||||
for n in 0..nci {
|
||||
if n == 0 || has_side {
|
||||
let cond = self.frame_condition(n, lost);
|
||||
self.ch[n].decode_frame(dec.as_deref_mut(), &mut buf[n][2..], lost, cond);
|
||||
} else {
|
||||
buf[n][2..2 + l].fill(0);
|
||||
}
|
||||
self.ch[n].n_frames_decoded += 1;
|
||||
}
|
||||
}
|
||||
|
||||
/// Resamples the decoded channels to the API rate into interleaved `out`; returns samples per channel.
|
||||
fn resample_to_output(&mut self, buf: &[[i16; MAX_FRAME + 2]; 2], nci: usize, stereo_to_mono: bool, out: &mut [f32]) -> usize {
|
||||
let l = self.ch[0].frame_length;
|
||||
let n_out = l * self.api_rate as usize / (self.ch[0].fs_khz * 1000);
|
||||
let ach = self.api_channels;
|
||||
let mut tmp = Vec::with_capacity(n_out);
|
||||
for n in 0..ach.min(nci) {
|
||||
tmp.clear();
|
||||
self.ch[n].resampler.process(&buf[n][1..1 + l], &mut tmp);
|
||||
for (i, &v) in tmp.iter().enumerate().take(n_out) {
|
||||
out[i * ach + n] = v / 32768.0;
|
||||
}
|
||||
}
|
||||
if ach == 2 && nci == 1 {
|
||||
if stereo_to_mono {
|
||||
tmp.clear();
|
||||
self.ch[1].resampler.process(&buf[0][1..1 + l], &mut tmp);
|
||||
for (i, &v) in tmp.iter().enumerate().take(n_out) {
|
||||
out[i * 2 + 1] = v / 32768.0;
|
||||
}
|
||||
} else {
|
||||
for i in 0..n_out {
|
||||
out[i * 2 + 1] = out[i * 2];
|
||||
}
|
||||
}
|
||||
}
|
||||
n_out
|
||||
}
|
||||
}
|
||||
@@ -1,377 +0,0 @@
|
||||
//! Fixed-point NLSF decoding, NLSF→LPC conversion and filter stabilisation
|
||||
//! (RFC 6716 §4.2.7.5, with the RFC 8251 §6/§7 overflow fixes).
|
||||
|
||||
use super::tables::{
|
||||
COS_Q12, LSF_ORDER_NBMB, LSF_ORDER_WB, NLSF_CB_NBMB, NLSF_CB_WB, NLSF_MIN_SPACING_NBMB, NLSF_MIN_SPACING_WB, NLSF_PRED_NBMB, NLSF_PRED_WB,
|
||||
NLSF_PSEL_NBMB, NLSF_PSEL_WB,
|
||||
};
|
||||
|
||||
pub const MAX_LPC_ORDER: usize = 16;
|
||||
|
||||
#[inline]
|
||||
pub fn smulwb(a: i32, b: i32) -> i32 {
|
||||
((a as i64 * (b as i16) as i64) >> 16) as i32
|
||||
}
|
||||
#[inline]
|
||||
pub fn smlawb(a: i32, b: i32, c: i32) -> i32 {
|
||||
a.wrapping_add(smulwb(b, c))
|
||||
}
|
||||
#[inline]
|
||||
pub fn smulww(a: i32, b: i32) -> i32 {
|
||||
((a as i64 * b as i64) >> 16) as i32
|
||||
}
|
||||
#[inline]
|
||||
pub fn smmul(a: i32, b: i32) -> i32 {
|
||||
((a as i64 * b as i64) >> 32) as i32
|
||||
}
|
||||
#[inline]
|
||||
pub fn rshift_round(a: i32, s: u32) -> i32 {
|
||||
if s == 1 { (a >> 1) + (a & 1) } else { ((a >> (s - 1)) + 1) >> 1 }
|
||||
}
|
||||
#[inline]
|
||||
pub fn rshift_round64(a: i64, s: u32) -> i64 {
|
||||
if s == 1 { (a >> 1) + (a & 1) } else { ((a >> (s - 1)) + 1) >> 1 }
|
||||
}
|
||||
#[inline]
|
||||
pub fn sat16(a: i32) -> i32 {
|
||||
a.clamp(-32768, 32767)
|
||||
}
|
||||
|
||||
/// `silk_INVERSE32_varQ`: approximation of `(1 << q_res) / b32`.
|
||||
pub fn inverse32_varq(b32: i32, q_res: i32) -> i32 {
|
||||
let b_headrm = b32.unsigned_abs().leading_zeros() as i32 - 1;
|
||||
let b32_nrm = b32.wrapping_shl(b_headrm as u32);
|
||||
let b32_inv = (i32::MAX >> 2) / (b32_nrm >> 16);
|
||||
let mut result = b32_inv << 16;
|
||||
let err_q32 = ((1i32 << 29).wrapping_sub(smulwb(b32_nrm, b32_inv))).wrapping_shl(3);
|
||||
result = result.wrapping_add(((err_q32 as i64 * b32_inv as i64) >> 16) as i32);
|
||||
let lshift = 61 - b_headrm - q_res;
|
||||
if lshift <= 0 {
|
||||
let s = (-lshift) as u32;
|
||||
let lim_hi = i32::MAX >> s;
|
||||
let lim_lo = i32::MIN >> s;
|
||||
result.clamp(lim_lo, lim_hi) << s
|
||||
} else if lshift < 32 {
|
||||
result >> lshift
|
||||
} else {
|
||||
0
|
||||
}
|
||||
}
|
||||
|
||||
/// `silk_DIV32_varQ`: approximation of `(a32 << q_res) / b32`.
|
||||
pub fn div32_varq(a32: i32, b32: i32, q_res: i32) -> i32 {
|
||||
let a_headrm = a32.unsigned_abs().leading_zeros() as i32 - 1;
|
||||
let a32_nrm = a32.wrapping_shl(a_headrm as u32);
|
||||
let b_headrm = b32.unsigned_abs().leading_zeros() as i32 - 1;
|
||||
let b32_nrm = b32.wrapping_shl(b_headrm as u32);
|
||||
let b32_inv = (i32::MAX >> 2) / (b32_nrm >> 16);
|
||||
let mut result = smulwb(a32_nrm, b32_inv);
|
||||
let a32_nrm = a32_nrm.wrapping_sub(((smmul(b32_nrm, result) as i64) << 3) as i32);
|
||||
result = smlawb(result, a32_nrm, b32_inv);
|
||||
let lshift = 29 + a_headrm - b_headrm - q_res;
|
||||
if lshift < 0 {
|
||||
let s = (-lshift) as u32;
|
||||
let lim_hi = i32::MAX >> s;
|
||||
let lim_lo = i32::MIN >> s;
|
||||
result.clamp(lim_lo, lim_hi) << s
|
||||
} else if lshift < 32 {
|
||||
result >> lshift
|
||||
} else {
|
||||
0
|
||||
}
|
||||
}
|
||||
|
||||
fn sqrt_approx(x: i32) -> i32 {
|
||||
if x <= 0 {
|
||||
return 0;
|
||||
}
|
||||
let lz = x.leading_zeros() as i32;
|
||||
let frac = (x.rotate_right((24 - lz).rem_euclid(32) as u32)) & 0x7f;
|
||||
let mut y = if lz & 1 != 0 { 32768 } else { 46214 };
|
||||
y >>= lz >> 1;
|
||||
smlawb(y, y, 213 * frac)
|
||||
}
|
||||
|
||||
/// Decodes the stage-1/stage-2 indices into stabilised NLSFs (Q15).
|
||||
pub fn nlsf_decode(indices: &[i32], wb: bool) -> [i32; MAX_LPC_ORDER] {
|
||||
let order = if wb { 16 } else { 10 };
|
||||
let i1 = indices[0] as usize;
|
||||
let cb1: &[i32] = if wb { &NLSF_CB_WB[i1] } else { &NLSF_CB_NBMB[i1] };
|
||||
let qstep = if wb { 9830 } else { 11796 };
|
||||
// Residual dequantisation with backwards prediction.
|
||||
let mut res_q10 = [0i32; MAX_LPC_ORDER];
|
||||
let mut out_q10 = 0i32;
|
||||
for i in (0..order).rev() {
|
||||
let pred = if i + 1 < order {
|
||||
let sel = if wb { NLSF_PSEL_WB[i1][i] } else { NLSF_PSEL_NBMB[i1][i] } as usize;
|
||||
let w = if wb { NLSF_PRED_WB[sel][i] } else { NLSF_PRED_NBMB[sel][i] };
|
||||
(out_q10 * w) >> 8
|
||||
} else {
|
||||
0
|
||||
};
|
||||
let mut v = indices[i + 1] << 10;
|
||||
if v > 0 {
|
||||
v -= 102;
|
||||
} else if v < 0 {
|
||||
v += 102;
|
||||
}
|
||||
out_q10 = smlawb(pred, v, qstep);
|
||||
res_q10[i] = out_q10;
|
||||
}
|
||||
// Laroia weights.
|
||||
let mut nlsf = [0i32; MAX_LPC_ORDER];
|
||||
for k in 0..order {
|
||||
let prev = if k == 0 { 0 } else { cb1[k - 1] };
|
||||
let next = if k + 1 == order { 256 } else { cb1[k + 1] };
|
||||
let w = (1024 / (cb1[k] - prev).max(1) + 1024 / (next - cb1[k]).max(1)).min(32767);
|
||||
let w_q9 = sqrt_approx(w << 16);
|
||||
let v = (cb1[k] << 7) + (res_q10[k] << 14) / w_q9.max(1);
|
||||
nlsf[k] = v.clamp(0, 32767);
|
||||
}
|
||||
let dmin: &[i32] = if wb { &NLSF_MIN_SPACING_WB } else { &NLSF_MIN_SPACING_NBMB };
|
||||
nlsf_stabilize(&mut nlsf[..order], dmin);
|
||||
nlsf
|
||||
}
|
||||
|
||||
pub fn nlsf_stabilize(nlsf: &mut [i32], dmin: &[i32]) {
|
||||
let l = nlsf.len();
|
||||
for _ in 0..20 {
|
||||
let mut min_diff = nlsf[0] - dmin[0];
|
||||
let mut idx = 0;
|
||||
for i in 1..l {
|
||||
let d = nlsf[i] - (nlsf[i - 1] + dmin[i]);
|
||||
if d < min_diff {
|
||||
min_diff = d;
|
||||
idx = i;
|
||||
}
|
||||
}
|
||||
let d = 32768 - (nlsf[l - 1] + dmin[l]);
|
||||
if d < min_diff {
|
||||
min_diff = d;
|
||||
idx = l;
|
||||
}
|
||||
if min_diff >= 0 {
|
||||
return;
|
||||
}
|
||||
if idx == 0 {
|
||||
nlsf[0] = dmin[0];
|
||||
} else if idx == l {
|
||||
nlsf[l - 1] = 32768 - dmin[l];
|
||||
} else {
|
||||
let mut min_center = dmin[..idx].iter().sum::<i32>();
|
||||
min_center += dmin[idx] >> 1;
|
||||
let mut max_center = 32768 - dmin[idx + 1..=l].iter().sum::<i32>();
|
||||
max_center -= dmin[idx] >> 1;
|
||||
let c = rshift_round(nlsf[idx - 1] + nlsf[idx], 1);
|
||||
let c = if min_center > max_center { c.clamp(max_center, min_center) } else { c.clamp(min_center, max_center) };
|
||||
nlsf[idx - 1] = c - (dmin[idx] >> 1);
|
||||
nlsf[idx] = nlsf[idx - 1] + dmin[idx];
|
||||
}
|
||||
}
|
||||
// Fallback.
|
||||
nlsf.sort_unstable();
|
||||
nlsf[0] = nlsf[0].max(dmin[0]);
|
||||
for i in 1..l {
|
||||
nlsf[i] = nlsf[i].max(sat16(nlsf[i - 1] + dmin[i]));
|
||||
}
|
||||
nlsf[l - 1] = nlsf[l - 1].min(32768 - dmin[l]);
|
||||
for i in (0..l - 1).rev() {
|
||||
nlsf[i] = nlsf[i].min(nlsf[i + 1] - dmin[i + 1]);
|
||||
}
|
||||
}
|
||||
|
||||
fn bwexpander_32(ar: &mut [i32], mut chirp_q16: i32) {
|
||||
let d = ar.len();
|
||||
let chirp_minus_one = chirp_q16 - 65536;
|
||||
for v in ar.iter_mut().take(d - 1) {
|
||||
*v = smulww(chirp_q16, *v);
|
||||
chirp_q16 += rshift_round(chirp_q16.wrapping_mul(chirp_minus_one), 16);
|
||||
}
|
||||
ar[d - 1] = smulww(chirp_q16, ar[d - 1]);
|
||||
}
|
||||
|
||||
/// `silk_bwexpander` on Q12 coefficients (16-bit).
|
||||
pub fn bwexpander(ar: &mut [i32], mut chirp_q16: i32) {
|
||||
let d = ar.len();
|
||||
let chirp_minus_one = chirp_q16 - 65536;
|
||||
for v in ar.iter_mut().take(d - 1) {
|
||||
*v = rshift_round(chirp_q16.wrapping_mul(*v), 16);
|
||||
chirp_q16 += rshift_round(chirp_q16.wrapping_mul(chirp_minus_one), 16);
|
||||
}
|
||||
ar[d - 1] = rshift_round(chirp_q16.wrapping_mul(ar[d - 1]), 16);
|
||||
}
|
||||
|
||||
fn lpc_inverse_pred_gain(a_q12: &[i32]) -> i32 {
|
||||
const QA: u32 = 24;
|
||||
const A_LIMIT: i32 = 16_773_022;
|
||||
let order = a_q12.len();
|
||||
let mut a = [0i32; MAX_LPC_ORDER];
|
||||
let mut dc = 0;
|
||||
for k in 0..order {
|
||||
dc += a_q12[k];
|
||||
a[k] = a_q12[k] << (QA - 12);
|
||||
}
|
||||
if dc >= 4096 {
|
||||
return 0;
|
||||
}
|
||||
let mut inv_gain: i32 = 1 << 30;
|
||||
let mul32_frac_q31 = |x: i32, y: i32| rshift_round64(x as i64 * y as i64, 31) as i32;
|
||||
for k in (1..order).rev() {
|
||||
if a[k] > A_LIMIT || a[k] < -A_LIMIT {
|
||||
return 0;
|
||||
}
|
||||
let rc_q31 = -(a[k] << (31 - QA));
|
||||
let rc_mult1 = (1i32 << 30) - smmul(rc_q31, rc_q31);
|
||||
inv_gain = smmul(inv_gain, rc_mult1) << 2;
|
||||
if inv_gain < 107_374 {
|
||||
return 0;
|
||||
}
|
||||
let mult2q = 32 - rc_mult1.unsigned_abs().leading_zeros() as i32;
|
||||
let rc_mult2 = inverse32_varq(rc_mult1, mult2q + 30);
|
||||
for n in 0..(k + 1) >> 1 {
|
||||
let t1 = a[n];
|
||||
let t2 = a[k - n - 1];
|
||||
let v1 = rshift_round64(t1.saturating_sub(mul32_frac_q31(t2, rc_q31)) as i64 * rc_mult2 as i64, mult2q as u32);
|
||||
if v1 > i32::MAX as i64 || v1 < i32::MIN as i64 {
|
||||
return 0;
|
||||
}
|
||||
let v2 = rshift_round64(t2.saturating_sub(mul32_frac_q31(t1, rc_q31)) as i64 * rc_mult2 as i64, mult2q as u32);
|
||||
if v2 > i32::MAX as i64 || v2 < i32::MIN as i64 {
|
||||
return 0;
|
||||
}
|
||||
a[n] = v1 as i32;
|
||||
a[k - n - 1] = v2 as i32;
|
||||
}
|
||||
}
|
||||
if a[0] > A_LIMIT || a[0] < -A_LIMIT {
|
||||
return 0;
|
||||
}
|
||||
let rc_q31 = -(a[0] << (31 - QA));
|
||||
let rc_mult1 = (1i32 << 30) - smmul(rc_q31, rc_q31);
|
||||
inv_gain = smmul(inv_gain, rc_mult1) << 2;
|
||||
if inv_gain < 107_374 {
|
||||
return 0;
|
||||
}
|
||||
inv_gain
|
||||
}
|
||||
|
||||
/// Converts NLSFs (Q15) to stable Q12 LPC coefficients (`silk_NLSF2A`).
|
||||
pub fn nlsf2a(nlsf: &[i32]) -> [i32; MAX_LPC_ORDER] {
|
||||
let d = nlsf.len();
|
||||
let ordering: &[usize] = if d == 16 { &LSF_ORDER_WB } else { &LSF_ORDER_NBMB };
|
||||
let mut c_q17 = [0i32; MAX_LPC_ORDER];
|
||||
for k in 0..d {
|
||||
let i = (nlsf[k] >> 8) as usize;
|
||||
let f = nlsf[k] & 255;
|
||||
let c0 = COS_Q12[i];
|
||||
let c1 = COS_Q12[(i + 1).min(128)];
|
||||
c_q17[ordering[k]] = (c0 * 256 + (c1 - c0) * f + 4) >> 3;
|
||||
}
|
||||
let d2 = d / 2;
|
||||
let find_poly = |off: usize| -> [i64; 10] {
|
||||
let mut out = [0i64; 10];
|
||||
out[0] = 1 << 16;
|
||||
out[1] = -(c_q17[off] as i64);
|
||||
for k in 1..d2 {
|
||||
let ftmp = c_q17[2 * k + off] as i64;
|
||||
out[k + 1] = 2 * out[k - 1] - ((ftmp * out[k] + 32768) >> 16);
|
||||
for n in (2..=k).rev() {
|
||||
out[n] += out[n - 2] - ((ftmp * out[n - 1] + 32768) >> 16);
|
||||
}
|
||||
out[1] -= ftmp;
|
||||
}
|
||||
out
|
||||
};
|
||||
let p = find_poly(0);
|
||||
let q = find_poly(1);
|
||||
let mut a32 = [0i32; MAX_LPC_ORDER];
|
||||
for k in 0..d2 {
|
||||
let ptmp = p[k + 1] + p[k];
|
||||
let qtmp = q[k + 1] - q[k];
|
||||
a32[k] = (-qtmp - ptmp) as i32;
|
||||
a32[d - k - 1] = (qtmp - ptmp) as i32;
|
||||
}
|
||||
let a32 = &mut a32[..d];
|
||||
// Limit the dynamic range (silk_LPC_fit with QIN = 17, QOUT = 12).
|
||||
let mut a_q12 = [0i32; MAX_LPC_ORDER];
|
||||
let mut i = 0;
|
||||
while i < 10 {
|
||||
let mut maxabs = 0i32;
|
||||
let mut idx = 0;
|
||||
for (k, &v) in a32.iter().enumerate() {
|
||||
let av = v.saturating_abs();
|
||||
if av > maxabs {
|
||||
maxabs = av;
|
||||
idx = k;
|
||||
}
|
||||
}
|
||||
let maxabs = rshift_round(maxabs, 5);
|
||||
if maxabs > 32767 {
|
||||
let maxabs = maxabs.min(163_838);
|
||||
let chirp = 65470 - ((maxabs - 32767) << 14) / ((maxabs * (idx as i32 + 1)) >> 2);
|
||||
bwexpander_32(a32, chirp);
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
i += 1;
|
||||
}
|
||||
if i == 10 {
|
||||
for k in 0..d {
|
||||
a_q12[k] = sat16(rshift_round(a32[k], 5));
|
||||
a32[k] = a_q12[k] << 5;
|
||||
}
|
||||
} else {
|
||||
for k in 0..d {
|
||||
a_q12[k] = rshift_round(a32[k], 5);
|
||||
}
|
||||
}
|
||||
for i in 0..16 {
|
||||
if lpc_inverse_pred_gain(&a_q12[..d]) == 0 {
|
||||
bwexpander_32(a32, 65536 - (2 << i));
|
||||
for k in 0..d {
|
||||
a_q12[k] = rshift_round(a32[k], 5);
|
||||
}
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
a_q12
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn flat_nlsf_gives_small_lpc() {
|
||||
// Uniformly spaced NLSFs correspond to an (almost) flat spectrum.
|
||||
let nlsf: Vec<i32> = (1..=16).map(|k| k * 32768 / 17).collect();
|
||||
let a = nlsf2a(&nlsf);
|
||||
assert!(a.iter().all(|v| v.abs() < 600), "{a:?}");
|
||||
let nlsf: Vec<i32> = (1..=10).map(|k| k * 32768 / 11).collect();
|
||||
let a = nlsf2a(&nlsf);
|
||||
assert!(a[..10].iter().all(|v| v.abs() < 600), "{a:?}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn stabilize_enforces_spacing() {
|
||||
let mut v = [100, 90, 80, 30000, 30001, 30002, 31000, 32000, 32700, 32760];
|
||||
nlsf_stabilize(&mut v, &NLSF_MIN_SPACING_NBMB);
|
||||
assert!(v[0] >= NLSF_MIN_SPACING_NBMB[0]);
|
||||
for i in 1..10 {
|
||||
assert!(v[i] - v[i - 1] >= NLSF_MIN_SPACING_NBMB[i], "{v:?}");
|
||||
}
|
||||
assert!(32768 - v[9] >= NLSF_MIN_SPACING_NBMB[10]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn inverse_approximations() {
|
||||
for &b in &[65536, 100_000, 1 << 20, 81920, 1_686_110_208] {
|
||||
let inv = inverse32_varq(b, 47) as f64;
|
||||
let exact = 2f64.powi(47) / b as f64;
|
||||
assert!((inv - exact).abs() / exact < 1e-3, "{b}");
|
||||
}
|
||||
let q = div32_varq(65536, 131072, 16) as f64;
|
||||
assert!((q - 32768.0).abs() < 2.0);
|
||||
}
|
||||
}
|
||||
@@ -1,205 +0,0 @@
|
||||
//! SILK output resampler (RFC 6716 §4.2.9: non-normative filter, normative delay budget).
|
||||
//!
|
||||
//! A windowed-sinc polyphase interpolator whose group delay matches the delay allocation of
|
||||
//! RFC 6716 Table 54 (and the integer delays used when no rate conversion is needed), so that the
|
||||
//! SILK output lines up with the CELT layer in Hybrid mode.
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct Resampler {
|
||||
fin: u32,
|
||||
fout: u32,
|
||||
/// Pure-delay mode (equal rates): delay line.
|
||||
copy_delay: usize,
|
||||
/// Polyphase taps per output phase.
|
||||
phases: Vec<(i64, Vec<f32>)>,
|
||||
period: u64,
|
||||
/// Total delay in units of 1/fout input samples.
|
||||
delay_q: i64,
|
||||
half: i64,
|
||||
future: i64,
|
||||
hist: Vec<f32>,
|
||||
/// Absolute input index of `hist[0]`.
|
||||
hist_start: i64,
|
||||
n_in: i64,
|
||||
n_out: u64,
|
||||
}
|
||||
|
||||
fn gcd(a: u32, b: u32) -> u32 {
|
||||
if b == 0 { a } else { gcd(b, a % b) }
|
||||
}
|
||||
|
||||
/// Least-squares fractional-delay interpolator: taps at offsets `-past+1 ..= future` (in input
|
||||
/// samples) approximating a delay of `frac` samples over `[0, wp]`, with zero response weighted
|
||||
/// over `[ws, pi]` when `ws < pi` (decimation).
|
||||
fn design_fractional(past: i64, future: i64, frac: f64, wp: f64, ws: f64) -> Vec<f64> {
|
||||
let offs: Vec<f64> = ((-past + 1)..=future).map(|m| m as f64).collect();
|
||||
let n = offs.len();
|
||||
// \int_a^b cos(w x) dw
|
||||
let ic = |x: f64, a: f64, b: f64| if x.abs() < 1e-12 { b - a } else { ((b * x).sin() - (a * x).sin()) / x };
|
||||
let stop_w = 30.0;
|
||||
const LOW_W: f64 = 0.05;
|
||||
let mut q = vec![vec![0f64; n + 1]; n];
|
||||
for i in 0..n {
|
||||
for k in 0..n {
|
||||
let d = offs[i] - offs[k];
|
||||
let mut v = ic(d, 0.0, 0.6 * wp) + LOW_W * ic(d, 0.6 * wp, wp);
|
||||
if ws < std::f64::consts::PI {
|
||||
v += stop_w * ic(d, ws, std::f64::consts::PI);
|
||||
}
|
||||
q[i][k] = v + if i == k { 1e-9 } else { 0.0 };
|
||||
}
|
||||
q[i][n] = ic(offs[i] - frac, 0.0, 0.6 * wp) + LOW_W * ic(offs[i] - frac, 0.6 * wp, wp);
|
||||
}
|
||||
// Gaussian elimination with partial pivoting.
|
||||
for c in 0..n {
|
||||
let p = (c..n).max_by(|&a, &b| q[a][c].abs().total_cmp(&q[b][c].abs())).unwrap_or(c);
|
||||
q.swap(c, p);
|
||||
let piv = q[c][c];
|
||||
for r in 0..n {
|
||||
if r != c {
|
||||
let f = q[r][c] / piv;
|
||||
if f != 0.0 {
|
||||
for k in c..=n {
|
||||
q[r][k] -= f * q[c][k];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
(0..n).map(|i| q[i][n] / q[i][i]).collect()
|
||||
}
|
||||
|
||||
impl Resampler {
|
||||
pub fn new(fin: u32, fout: u32) -> Resampler {
|
||||
let mut r = Resampler {
|
||||
fin,
|
||||
fout,
|
||||
copy_delay: 0,
|
||||
phases: Vec::new(),
|
||||
period: 1,
|
||||
delay_q: 0,
|
||||
half: 0,
|
||||
future: 0,
|
||||
hist: Vec::new(),
|
||||
hist_start: 0,
|
||||
n_in: 0,
|
||||
n_out: 0,
|
||||
};
|
||||
if fin == fout {
|
||||
r.copy_delay = match fin {
|
||||
8000 => 4,
|
||||
12000 => 9,
|
||||
_ => 12,
|
||||
};
|
||||
r.hist = vec![0.0; r.copy_delay];
|
||||
return r;
|
||||
}
|
||||
// Delay in milliseconds per internal bandwidth (RFC 6716 Table 54).
|
||||
let delay_ms = match fin {
|
||||
8000 => 0.538,
|
||||
12000 => 0.692,
|
||||
_ => 0.706,
|
||||
};
|
||||
let d_in = delay_ms * fin as f64 / 1000.0; // input samples
|
||||
// Future taps are limited by the delay budget; past taps are free.
|
||||
let future = (d_in.floor() as i64).max(1);
|
||||
let past = 24i64;
|
||||
r.half = past;
|
||||
r.future = future;
|
||||
r.delay_q = (d_in * fout as f64).round() as i64;
|
||||
let g = gcd(fin, fout);
|
||||
r.period = (fout / g) as u64;
|
||||
let pi = std::f64::consts::PI;
|
||||
// Passband edge and (for decimation) stopband edge in radians per input sample.
|
||||
let (wp, ws) = if fout > fin { (0.94 * pi, pi) } else { (0.88 * pi * fout as f64 / fin as f64, pi * fout as f64 / fin as f64) };
|
||||
for j in 0..r.period as i64 {
|
||||
let num = j * fin as i64 - r.delay_q;
|
||||
let base = num.div_euclid(fout as i64);
|
||||
let frac = num.rem_euclid(fout as i64) as f64 / fout as f64;
|
||||
let taps = design_fractional(past, future, frac, wp, ws);
|
||||
r.phases.push((base, taps.iter().map(|&t| t as f32).collect()));
|
||||
}
|
||||
r.hist = vec![0.0; (past + future + 4) as usize];
|
||||
r.hist_start = -(past + future + 4);
|
||||
r
|
||||
}
|
||||
|
||||
pub fn rates(&self) -> (u32, u32) {
|
||||
(self.fin, self.fout)
|
||||
}
|
||||
|
||||
/// Resamples `input` (16-bit scale) and appends the output samples.
|
||||
pub fn process(&mut self, input: &[i16], out: &mut Vec<f32>) {
|
||||
if self.fin == self.fout {
|
||||
for &v in input {
|
||||
self.hist.push(v as f32);
|
||||
}
|
||||
let n = self.hist.len() - self.copy_delay;
|
||||
out.extend_from_slice(&self.hist[..n]);
|
||||
self.hist.drain(..n);
|
||||
return;
|
||||
}
|
||||
self.hist.extend(input.iter().map(|&v| v as f32));
|
||||
self.n_in += input.len() as i64;
|
||||
let total_out = (self.n_in as u64 * self.fout as u64) / self.fin as u64;
|
||||
while self.n_out < total_out {
|
||||
let j = self.n_out;
|
||||
let cycle = (j / self.period) as i64;
|
||||
let (base0, ref taps) = self.phases[(j % self.period) as usize];
|
||||
let base = base0 + cycle * self.fin as i64 * self.period as i64 / self.fout as i64;
|
||||
let first = base - self.half + 1;
|
||||
let mut acc = 0f32;
|
||||
for (m, &t) in taps.iter().enumerate() {
|
||||
let idx = first + m as i64 - self.hist_start;
|
||||
if idx >= 0 && (idx as usize) < self.hist.len() {
|
||||
acc += self.hist[idx as usize] * t;
|
||||
}
|
||||
}
|
||||
out.push(acc);
|
||||
self.n_out += 1;
|
||||
}
|
||||
// Trim history, keeping enough for the next taps.
|
||||
let keep_from = self.n_in - (self.half + self.future + 4);
|
||||
if keep_from > self.hist_start {
|
||||
let drop = (keep_from - self.hist_start) as usize;
|
||||
self.hist.drain(..drop.min(self.hist.len()));
|
||||
self.hist_start = keep_from;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn upsample_sine_and_delay() {
|
||||
for &(fin, fout) in &[(16000u32, 48000u32), (8000, 48000), (12000, 48000), (16000, 24000), (16000, 8000), (12000, 16000)] {
|
||||
let mut r = Resampler::new(fin, fout);
|
||||
let f = 440.0;
|
||||
let n = fin as usize / 10;
|
||||
let x: Vec<i16> = (0..n).map(|i| ((2.0 * std::f64::consts::PI * f * i as f64 / fin as f64).sin() * 10000.0) as i16).collect();
|
||||
let mut y = Vec::new();
|
||||
for c in x.chunks(fin as usize / 50) {
|
||||
r.process(c, &mut y);
|
||||
}
|
||||
assert_eq!(y.len(), n * fout as usize / fin as usize);
|
||||
// Compare against the ideal delayed sine.
|
||||
let delay_ms = match fin {
|
||||
8000 => 0.538,
|
||||
12000 => 0.692,
|
||||
_ => 0.706,
|
||||
};
|
||||
let mut err = 0f64;
|
||||
let mut sig = 0f64;
|
||||
for (j, &v) in y.iter().enumerate().skip(fout as usize / 100) {
|
||||
let t = j as f64 / fout as f64 - delay_ms / 1000.0;
|
||||
let ideal = (2.0 * std::f64::consts::PI * f * t).sin() * 10000.0;
|
||||
err += (v as f64 - ideal).powi(2);
|
||||
sig += ideal * ideal;
|
||||
}
|
||||
let snr = 10.0 * (sig / err).log10();
|
||||
assert!(snr > 35.0, "{fin}->{fout}: {snr} dB");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,540 +0,0 @@
|
||||
//! SILK probability tables and codebooks, transcribed from RFC 6716 §4.2 (Tables 4–53).
|
||||
//!
|
||||
//! PDFs are written as in the RFC (`{...}/256`) and converted at compile time to the inverse-CDF
|
||||
//! form consumed by the range decoder.
|
||||
|
||||
/// Converts a PDF (summing to 256) into an inverse CDF (`256 - cdf[i+1]`).
|
||||
pub const fn icdf<const N: usize>(pdf: &[u8; N]) -> [u8; N] {
|
||||
let mut out = [0u8; N];
|
||||
let mut acc: u32 = 0;
|
||||
let mut i = 0;
|
||||
while i < N {
|
||||
acc += pdf[i] as u32;
|
||||
out[i] = (256 - acc) as u8;
|
||||
i += 1;
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
pub const STEREO_PRED_JOINT: [u8; 25] = icdf(&[7, 2, 1, 1, 1, 10, 24, 8, 1, 1, 3, 23, 92, 23, 3, 1, 1, 8, 24, 10, 1, 1, 1, 2, 7]);
|
||||
pub const UNIFORM3: [u8; 3] = icdf(&[85, 86, 85]);
|
||||
pub const UNIFORM5: [u8; 5] = icdf(&[51, 51, 52, 51, 51]);
|
||||
pub const STEREO_WEIGHTS_Q13: [i32; 16] = [-13732, -10050, -8266, -7526, -6500, -5000, -2950, -820, 820, 2950, 5000, 6500, 7526, 8266, 10050, 13732];
|
||||
pub const MID_ONLY: [u8; 2] = icdf(&[192, 64]);
|
||||
pub const TYPE_INACTIVE: [u8; 2] = icdf(&[26, 230]);
|
||||
pub const TYPE_ACTIVE: [u8; 4] = icdf(&[24, 74, 148, 10]);
|
||||
pub const GAIN_MSB: [&[u8]; 3] =
|
||||
[&icdf::<8>(&[32, 112, 68, 29, 12, 1, 1, 1]), &icdf::<8>(&[2, 17, 45, 60, 62, 47, 19, 4]), &icdf::<8>(&[1, 3, 26, 71, 94, 50, 9, 2])];
|
||||
pub const UNIFORM8: [u8; 8] = icdf(&[32, 32, 32, 32, 32, 32, 32, 32]);
|
||||
pub const DELTA_GAIN: [u8; 41] =
|
||||
icdf(&[6, 5, 11, 31, 132, 21, 8, 4, 3, 2, 2, 2, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1]);
|
||||
pub const NLSF_STAGE1: [&[u8]; 4] = [
|
||||
&icdf::<32>(&[44, 34, 30, 19, 21, 12, 11, 3, 3, 2, 16, 2, 2, 1, 5, 2, 1, 3, 3, 1, 1, 2, 2, 2, 3, 1, 9, 9, 2, 7, 2, 1]),
|
||||
&icdf::<32>(&[1, 10, 1, 8, 3, 8, 8, 14, 13, 14, 1, 14, 12, 13, 11, 11, 12, 11, 10, 10, 11, 8, 9, 8, 7, 8, 1, 1, 6, 1, 6, 5]),
|
||||
&icdf::<32>(&[31, 21, 3, 17, 1, 8, 17, 4, 1, 18, 16, 4, 2, 3, 1, 10, 1, 3, 16, 11, 16, 2, 2, 3, 2, 11, 1, 4, 9, 8, 7, 3]),
|
||||
&icdf::<32>(&[1, 4, 16, 5, 18, 11, 5, 14, 15, 1, 3, 12, 13, 14, 14, 6, 14, 12, 2, 6, 1, 12, 12, 11, 10, 3, 10, 5, 1, 1, 1, 3]),
|
||||
];
|
||||
pub const NLSF_STAGE2: [&[u8]; 16] = [
|
||||
&icdf::<9>(&[1, 1, 1, 15, 224, 11, 1, 1, 1]),
|
||||
&icdf::<9>(&[1, 1, 2, 34, 183, 32, 1, 1, 1]),
|
||||
&icdf::<9>(&[1, 1, 4, 42, 149, 55, 2, 1, 1]),
|
||||
&icdf::<9>(&[1, 1, 8, 52, 123, 61, 8, 1, 1]),
|
||||
&icdf::<9>(&[1, 3, 16, 53, 101, 74, 6, 1, 1]),
|
||||
&icdf::<9>(&[1, 3, 17, 55, 90, 73, 15, 1, 1]),
|
||||
&icdf::<9>(&[1, 7, 24, 53, 74, 67, 26, 3, 1]),
|
||||
&icdf::<9>(&[1, 1, 18, 63, 78, 58, 30, 6, 1]),
|
||||
&icdf::<9>(&[1, 1, 1, 9, 232, 9, 1, 1, 1]),
|
||||
&icdf::<9>(&[1, 1, 2, 28, 186, 35, 1, 1, 1]),
|
||||
&icdf::<9>(&[1, 1, 3, 42, 152, 53, 2, 1, 1]),
|
||||
&icdf::<9>(&[1, 1, 10, 49, 126, 65, 2, 1, 1]),
|
||||
&icdf::<9>(&[1, 4, 19, 48, 100, 77, 5, 1, 1]),
|
||||
&icdf::<9>(&[1, 1, 14, 54, 100, 72, 12, 1, 1]),
|
||||
&icdf::<9>(&[1, 1, 15, 61, 87, 61, 25, 4, 1]),
|
||||
&icdf::<9>(&[1, 7, 21, 50, 77, 81, 17, 1, 1]),
|
||||
];
|
||||
pub const NLSF_SEL_NBMB: [[u8; 10]; 32] = [
|
||||
[0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
|
||||
[1, 3, 1, 2, 2, 1, 2, 1, 1, 1],
|
||||
[2, 1, 1, 1, 1, 1, 1, 1, 1, 1],
|
||||
[1, 2, 2, 2, 2, 1, 2, 1, 1, 1],
|
||||
[2, 3, 3, 3, 3, 2, 2, 2, 2, 2],
|
||||
[0, 5, 3, 3, 2, 2, 2, 2, 1, 1],
|
||||
[0, 2, 2, 2, 2, 2, 2, 2, 2, 1],
|
||||
[2, 3, 6, 4, 4, 4, 5, 4, 5, 5],
|
||||
[2, 4, 5, 5, 4, 5, 4, 6, 4, 4],
|
||||
[2, 4, 4, 7, 4, 5, 4, 5, 5, 4],
|
||||
[4, 3, 3, 3, 2, 3, 2, 2, 2, 2],
|
||||
[1, 5, 5, 6, 4, 5, 4, 5, 5, 5],
|
||||
[2, 7, 4, 6, 5, 5, 5, 5, 5, 5],
|
||||
[2, 7, 5, 5, 5, 5, 5, 6, 5, 4],
|
||||
[3, 3, 5, 4, 4, 5, 4, 5, 4, 4],
|
||||
[2, 3, 3, 5, 5, 4, 4, 4, 4, 4],
|
||||
[2, 4, 4, 6, 4, 5, 4, 5, 5, 5],
|
||||
[2, 5, 4, 6, 5, 5, 5, 4, 5, 4],
|
||||
[2, 7, 4, 5, 4, 5, 4, 5, 5, 5],
|
||||
[2, 5, 4, 6, 7, 6, 5, 6, 5, 4],
|
||||
[3, 6, 7, 4, 6, 5, 5, 6, 4, 5],
|
||||
[2, 7, 6, 4, 4, 4, 5, 4, 5, 5],
|
||||
[4, 5, 5, 4, 6, 6, 5, 6, 5, 4],
|
||||
[2, 5, 5, 6, 5, 6, 4, 6, 4, 4],
|
||||
[4, 5, 5, 5, 3, 7, 4, 5, 5, 4],
|
||||
[2, 3, 4, 5, 5, 6, 4, 5, 5, 4],
|
||||
[2, 3, 2, 3, 3, 4, 2, 3, 3, 3],
|
||||
[1, 1, 2, 2, 2, 2, 2, 3, 2, 2],
|
||||
[4, 5, 5, 6, 6, 6, 5, 6, 4, 5],
|
||||
[3, 5, 5, 4, 4, 4, 4, 3, 3, 2],
|
||||
[2, 5, 3, 7, 5, 5, 4, 4, 5, 4],
|
||||
[4, 4, 5, 4, 5, 6, 5, 6, 5, 4],
|
||||
];
|
||||
pub const NLSF_SEL_WB: [[u8; 16]; 32] = [
|
||||
[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
|
||||
[2, 3, 3, 3, 3, 3, 2, 2, 2, 2, 2, 1, 1, 1, 0, 3],
|
||||
[2, 5, 5, 3, 7, 4, 4, 5, 2, 5, 4, 5, 5, 4, 3, 3],
|
||||
[0, 2, 1, 2, 2, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 1],
|
||||
[0, 6, 5, 4, 6, 4, 7, 5, 4, 4, 4, 5, 5, 4, 4, 3],
|
||||
[0, 3, 5, 5, 4, 3, 3, 5, 3, 3, 3, 3, 3, 3, 2, 4],
|
||||
[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
|
||||
[0, 2, 6, 3, 7, 2, 5, 3, 4, 5, 5, 4, 3, 3, 2, 3],
|
||||
[0, 6, 2, 6, 6, 4, 5, 4, 6, 5, 4, 4, 5, 3, 3, 3],
|
||||
[2, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
|
||||
[0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1],
|
||||
[2, 2, 3, 4, 5, 3, 3, 3, 3, 3, 3, 3, 2, 2, 1, 3],
|
||||
[2, 2, 3, 3, 4, 3, 3, 3, 3, 3, 3, 3, 3, 2, 1, 3],
|
||||
[3, 4, 4, 4, 6, 4, 4, 5, 3, 5, 4, 4, 5, 4, 3, 4],
|
||||
[0, 6, 4, 5, 4, 7, 5, 2, 6, 5, 7, 4, 4, 3, 5, 3],
|
||||
[0, 1, 0, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 1, 0],
|
||||
[1, 6, 5, 7, 5, 4, 5, 3, 4, 5, 4, 4, 4, 3, 3, 4],
|
||||
[1, 3, 3, 4, 4, 3, 3, 5, 2, 3, 3, 5, 5, 5, 3, 4],
|
||||
[2, 3, 3, 2, 2, 2, 3, 2, 1, 2, 1, 2, 1, 1, 1, 4],
|
||||
[0, 2, 3, 5, 3, 3, 2, 2, 2, 1, 1, 0, 0, 0, 0, 0],
|
||||
[3, 4, 3, 5, 3, 3, 2, 2, 1, 1, 1, 1, 1, 2, 2, 4],
|
||||
[2, 6, 3, 7, 7, 4, 5, 4, 5, 3, 5, 3, 3, 2, 3, 3],
|
||||
[2, 3, 5, 6, 6, 3, 5, 3, 4, 4, 3, 3, 3, 3, 2, 4],
|
||||
[1, 3, 3, 4, 4, 4, 4, 3, 5, 5, 5, 3, 1, 1, 1, 1],
|
||||
[2, 5, 3, 6, 6, 4, 7, 4, 4, 5, 3, 4, 4, 3, 3, 3],
|
||||
[0, 6, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
|
||||
[0, 6, 6, 3, 5, 2, 5, 5, 3, 4, 4, 7, 7, 4, 4, 4],
|
||||
[3, 3, 7, 3, 5, 4, 3, 3, 3, 2, 2, 3, 3, 3, 2, 3],
|
||||
[0, 0, 1, 0, 0, 0, 2, 1, 2, 1, 1, 2, 2, 2, 1, 1],
|
||||
[0, 3, 2, 5, 3, 3, 2, 3, 2, 1, 0, 0, 1, 0, 0, 1],
|
||||
[3, 5, 5, 4, 7, 5, 3, 3, 2, 3, 2, 2, 1, 0, 1, 0],
|
||||
[2, 3, 5, 3, 4, 3, 3, 3, 2, 1, 2, 6, 4, 0, 0, 0],
|
||||
];
|
||||
pub const NLSF_EXT: [u8; 7] = icdf(&[156, 60, 24, 9, 4, 2, 1]);
|
||||
pub const NLSF_PRED_NBMB: [[i32; 9]; 2] = [[179, 138, 140, 148, 151, 149, 153, 151, 163], [116, 67, 82, 59, 92, 72, 100, 89, 92]];
|
||||
pub const NLSF_PRED_WB: [[i32; 15]; 2] = [
|
||||
[175, 148, 160, 176, 178, 173, 174, 164, 177, 174, 196, 182, 198, 192, 182],
|
||||
[68, 62, 66, 60, 72, 117, 85, 90, 118, 136, 151, 142, 160, 142, 155],
|
||||
];
|
||||
pub const NLSF_PSEL_NBMB: [[u8; 9]; 32] = [
|
||||
[0, 1, 0, 0, 0, 0, 0, 0, 0],
|
||||
[1, 0, 0, 0, 0, 0, 0, 0, 0],
|
||||
[0, 0, 0, 0, 0, 0, 0, 0, 0],
|
||||
[1, 1, 1, 0, 0, 0, 0, 1, 0],
|
||||
[0, 1, 0, 0, 0, 0, 0, 0, 0],
|
||||
[0, 1, 0, 0, 0, 0, 0, 0, 0],
|
||||
[1, 0, 1, 1, 0, 0, 0, 1, 0],
|
||||
[0, 1, 1, 0, 0, 1, 1, 0, 0],
|
||||
[0, 0, 1, 1, 0, 1, 0, 1, 1],
|
||||
[0, 0, 1, 1, 0, 0, 1, 1, 1],
|
||||
[0, 0, 0, 0, 0, 0, 0, 0, 0],
|
||||
[0, 1, 0, 1, 1, 1, 1, 1, 0],
|
||||
[0, 1, 0, 1, 1, 1, 1, 1, 0],
|
||||
[0, 1, 1, 1, 1, 1, 1, 1, 0],
|
||||
[1, 0, 1, 1, 0, 1, 1, 1, 1],
|
||||
[0, 1, 1, 1, 1, 1, 0, 1, 0],
|
||||
[0, 0, 1, 1, 0, 1, 0, 1, 0],
|
||||
[0, 0, 1, 1, 1, 0, 1, 1, 1],
|
||||
[0, 1, 1, 0, 0, 1, 1, 1, 0],
|
||||
[0, 0, 0, 1, 1, 1, 0, 1, 0],
|
||||
[0, 1, 1, 0, 0, 1, 0, 1, 0],
|
||||
[0, 1, 1, 0, 0, 0, 1, 1, 0],
|
||||
[0, 0, 0, 0, 0, 1, 1, 1, 1],
|
||||
[0, 0, 1, 1, 0, 0, 0, 1, 1],
|
||||
[0, 0, 0, 1, 0, 1, 1, 1, 1],
|
||||
[0, 1, 1, 1, 1, 1, 1, 1, 0],
|
||||
[0, 0, 0, 0, 0, 0, 0, 0, 0],
|
||||
[0, 0, 0, 0, 0, 0, 0, 0, 0],
|
||||
[0, 0, 1, 0, 1, 1, 0, 1, 0],
|
||||
[1, 0, 0, 1, 0, 0, 0, 0, 0],
|
||||
[0, 0, 0, 1, 1, 0, 1, 0, 1],
|
||||
[1, 0, 1, 1, 0, 1, 1, 1, 1],
|
||||
];
|
||||
pub const NLSF_PSEL_WB: [[u8; 15]; 32] = [
|
||||
[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1],
|
||||
[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
|
||||
[0, 0, 1, 0, 0, 1, 1, 1, 0, 1, 1, 1, 1, 0, 0],
|
||||
[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0],
|
||||
[0, 1, 1, 0, 1, 0, 1, 1, 0, 1, 1, 1, 1, 1, 0],
|
||||
[0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
|
||||
[1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 1, 0],
|
||||
[0, 1, 1, 0, 0, 0, 1, 0, 1, 1, 1, 0, 1, 0, 1],
|
||||
[0, 1, 0, 1, 1, 0, 1, 0, 1, 0, 1, 1, 1, 1, 1],
|
||||
[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1],
|
||||
[0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
|
||||
[0, 0, 1, 0, 1, 1, 1, 1, 1, 1, 1, 0, 1, 0, 0],
|
||||
[0, 0, 1, 0, 0, 1, 0, 1, 0, 1, 0, 0, 1, 0, 0],
|
||||
[0, 0, 0, 0, 1, 1, 0, 1, 0, 1, 1, 1, 1, 0, 0],
|
||||
[0, 1, 0, 0, 0, 1, 1, 0, 1, 1, 1, 0, 1, 1, 1],
|
||||
[0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0],
|
||||
[0, 1, 1, 0, 1, 0, 1, 1, 1, 1, 1, 0, 1, 0, 0],
|
||||
[0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 1, 1, 1, 0, 0],
|
||||
[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1],
|
||||
[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0],
|
||||
[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
|
||||
[0, 1, 0, 1, 0, 1, 1, 0, 1, 0, 1, 0, 1, 1, 0],
|
||||
[0, 0, 1, 1, 1, 1, 0, 1, 1, 0, 0, 1, 1, 0, 0],
|
||||
[0, 1, 1, 0, 1, 0, 1, 0, 1, 0, 0, 0, 0, 1, 0],
|
||||
[0, 0, 0, 1, 1, 0, 1, 0, 1, 1, 1, 1, 1, 1, 1],
|
||||
[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1],
|
||||
[0, 1, 1, 0, 0, 0, 1, 1, 0, 0, 1, 1, 1, 1, 1],
|
||||
[0, 0, 0, 0, 0, 1, 0, 1, 1, 1, 1, 0, 1, 1, 1],
|
||||
[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1],
|
||||
[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1],
|
||||
[1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0],
|
||||
[0, 0, 1, 0, 0, 1, 1, 1, 0, 0, 1, 0, 0, 1, 0],
|
||||
];
|
||||
pub const NLSF_CB_NBMB: [[i32; 10]; 32] = [
|
||||
[12, 35, 60, 83, 108, 132, 157, 180, 206, 228],
|
||||
[15, 32, 55, 77, 101, 125, 151, 175, 201, 225],
|
||||
[19, 42, 66, 89, 114, 137, 162, 184, 209, 230],
|
||||
[12, 25, 50, 72, 97, 120, 147, 172, 200, 223],
|
||||
[26, 44, 69, 90, 114, 135, 159, 180, 205, 225],
|
||||
[13, 22, 53, 80, 106, 130, 156, 180, 205, 228],
|
||||
[15, 25, 44, 64, 90, 115, 142, 168, 196, 222],
|
||||
[19, 24, 62, 82, 100, 120, 145, 168, 190, 214],
|
||||
[22, 31, 50, 79, 103, 120, 151, 170, 203, 227],
|
||||
[21, 29, 45, 65, 106, 124, 150, 171, 196, 224],
|
||||
[30, 49, 75, 97, 121, 142, 165, 186, 209, 229],
|
||||
[19, 25, 52, 70, 93, 116, 143, 166, 192, 219],
|
||||
[26, 34, 62, 75, 97, 118, 145, 167, 194, 217],
|
||||
[25, 33, 56, 70, 91, 113, 143, 165, 196, 223],
|
||||
[21, 34, 51, 72, 97, 117, 145, 171, 196, 222],
|
||||
[20, 29, 50, 67, 90, 117, 144, 168, 197, 221],
|
||||
[22, 31, 48, 66, 95, 117, 146, 168, 196, 222],
|
||||
[24, 33, 51, 77, 116, 134, 158, 180, 200, 224],
|
||||
[21, 28, 70, 87, 106, 124, 149, 170, 194, 217],
|
||||
[26, 33, 53, 64, 83, 117, 152, 173, 204, 225],
|
||||
[27, 34, 65, 95, 108, 129, 155, 174, 210, 225],
|
||||
[20, 26, 72, 99, 113, 131, 154, 176, 200, 219],
|
||||
[34, 43, 61, 78, 93, 114, 155, 177, 205, 229],
|
||||
[23, 29, 54, 97, 124, 138, 163, 179, 209, 229],
|
||||
[30, 38, 56, 89, 118, 129, 158, 178, 200, 231],
|
||||
[21, 29, 49, 63, 85, 111, 142, 163, 193, 222],
|
||||
[27, 48, 77, 103, 133, 158, 179, 196, 215, 232],
|
||||
[29, 47, 74, 99, 124, 151, 176, 198, 220, 237],
|
||||
[33, 42, 61, 76, 93, 121, 155, 174, 207, 225],
|
||||
[29, 53, 87, 112, 136, 154, 170, 188, 208, 227],
|
||||
[24, 30, 52, 84, 131, 150, 166, 186, 203, 229],
|
||||
[37, 48, 64, 84, 104, 118, 156, 177, 201, 230],
|
||||
];
|
||||
pub const NLSF_CB_WB: [[i32; 16]; 32] = [
|
||||
[7, 23, 38, 54, 69, 85, 100, 116, 131, 147, 162, 178, 193, 208, 223, 239],
|
||||
[13, 25, 41, 55, 69, 83, 98, 112, 127, 142, 157, 171, 187, 203, 220, 236],
|
||||
[15, 21, 34, 51, 61, 78, 92, 106, 126, 136, 152, 167, 185, 205, 225, 240],
|
||||
[10, 21, 36, 50, 63, 79, 95, 110, 126, 141, 157, 173, 189, 205, 221, 237],
|
||||
[17, 20, 37, 51, 59, 78, 89, 107, 123, 134, 150, 164, 184, 205, 224, 240],
|
||||
[10, 15, 32, 51, 67, 81, 96, 112, 129, 142, 158, 173, 189, 204, 220, 236],
|
||||
[8, 21, 37, 51, 65, 79, 98, 113, 126, 138, 155, 168, 179, 192, 209, 218],
|
||||
[12, 15, 34, 55, 63, 78, 87, 108, 118, 131, 148, 167, 185, 203, 219, 236],
|
||||
[16, 19, 32, 36, 56, 79, 91, 108, 118, 136, 154, 171, 186, 204, 220, 237],
|
||||
[11, 28, 43, 58, 74, 89, 105, 120, 135, 150, 165, 180, 196, 211, 226, 241],
|
||||
[6, 16, 33, 46, 60, 75, 92, 107, 123, 137, 156, 169, 185, 199, 214, 225],
|
||||
[11, 19, 30, 44, 57, 74, 89, 105, 121, 135, 152, 169, 186, 202, 218, 234],
|
||||
[12, 19, 29, 46, 57, 71, 88, 100, 120, 132, 148, 165, 182, 199, 216, 233],
|
||||
[17, 23, 35, 46, 56, 77, 92, 106, 123, 134, 152, 167, 185, 204, 222, 237],
|
||||
[14, 17, 45, 53, 63, 75, 89, 107, 115, 132, 151, 171, 188, 206, 221, 240],
|
||||
[9, 16, 29, 40, 56, 71, 88, 103, 119, 137, 154, 171, 189, 205, 222, 237],
|
||||
[16, 19, 36, 48, 57, 76, 87, 105, 118, 132, 150, 167, 185, 202, 218, 236],
|
||||
[12, 17, 29, 54, 71, 81, 94, 104, 126, 136, 149, 164, 182, 201, 221, 237],
|
||||
[15, 28, 47, 62, 79, 97, 115, 129, 142, 155, 168, 180, 194, 208, 223, 238],
|
||||
[8, 14, 30, 45, 62, 78, 94, 111, 127, 143, 159, 175, 192, 207, 223, 239],
|
||||
[17, 30, 49, 62, 79, 92, 107, 119, 132, 145, 160, 174, 190, 204, 220, 235],
|
||||
[14, 19, 36, 45, 61, 76, 91, 108, 121, 138, 154, 172, 189, 205, 222, 238],
|
||||
[12, 18, 31, 45, 60, 76, 91, 107, 123, 138, 154, 171, 187, 204, 221, 236],
|
||||
[13, 17, 31, 43, 53, 70, 83, 103, 114, 131, 149, 167, 185, 203, 220, 237],
|
||||
[17, 22, 35, 42, 58, 78, 93, 110, 125, 139, 155, 170, 188, 206, 224, 240],
|
||||
[8, 15, 34, 50, 67, 83, 99, 115, 131, 146, 162, 178, 193, 209, 224, 239],
|
||||
[13, 16, 41, 66, 73, 86, 95, 111, 128, 137, 150, 163, 183, 206, 225, 241],
|
||||
[17, 25, 37, 52, 63, 75, 92, 102, 119, 132, 144, 160, 175, 191, 212, 231],
|
||||
[19, 31, 49, 65, 83, 100, 117, 133, 147, 161, 174, 187, 200, 213, 227, 242],
|
||||
[18, 31, 52, 68, 88, 103, 117, 126, 138, 149, 163, 177, 192, 207, 223, 239],
|
||||
[16, 29, 47, 61, 76, 90, 106, 119, 133, 147, 161, 176, 193, 209, 224, 240],
|
||||
[15, 21, 35, 50, 61, 73, 86, 97, 110, 119, 129, 141, 175, 198, 218, 237],
|
||||
];
|
||||
pub const NLSF_MIN_SPACING_NBMB: [i32; 11] = [250, 3, 6, 3, 3, 3, 4, 3, 3, 3, 461];
|
||||
pub const NLSF_MIN_SPACING_WB: [i32; 17] = [100, 3, 40, 3, 3, 3, 5, 14, 14, 10, 11, 3, 8, 9, 7, 3, 347];
|
||||
pub const NLSF_INTERP: [u8; 5] = icdf(&[13, 22, 29, 11, 181]);
|
||||
pub const LSF_ORDER_NBMB: [usize; 10] = [0, 9, 6, 3, 4, 5, 8, 1, 2, 7];
|
||||
pub const LSF_ORDER_WB: [usize; 16] = [0, 15, 8, 7, 4, 11, 12, 3, 2, 13, 10, 5, 6, 9, 14, 1];
|
||||
pub const COS_Q12: [i32; 129] = [
|
||||
4096, 4095, 4091, 4085, 4076, 4065, 4052, 4036, 4017, 3997, 3973, 3948, 3920, 3889, 3857, 3822, 3784, 3745, 3703, 3659, 3613, 3564, 3513, 3461,
|
||||
3406, 3349, 3290, 3229, 3166, 3102, 3035, 2967, 2896, 2824, 2751, 2676, 2599, 2520, 2440, 2359, 2276, 2191, 2106, 2019, 1931, 1842, 1751, 1660,
|
||||
1568, 1474, 1380, 1285, 1189, 1093, 995, 897, 799, 700, 601, 501, 401, 301, 201, 101, 0, -101, -201, -301, -401, -501, -601, -700, -799, -897,
|
||||
-995, -1093, -1189, -1285, -1380, -1474, -1568, -1660, -1751, -1842, -1931, -2019, -2106, -2191, -2276, -2359, -2440, -2520, -2599, -2676, -2751,
|
||||
-2824, -2896, -2967, -3035, -3102, -3166, -3229, -3290, -3349, -3406, -3461, -3513, -3564, -3613, -3659, -3703, -3745, -3784, -3822, -3857,
|
||||
-3889, -3920, -3948, -3973, -3997, -4017, -4036, -4052, -4065, -4076, -4085, -4091, -4095, -4096,
|
||||
];
|
||||
pub const PITCH_HIGH: [u8; 32] = icdf(&[3, 3, 6, 11, 21, 30, 32, 19, 11, 10, 12, 13, 13, 12, 11, 9, 8, 7, 6, 4, 2, 2, 2, 1, 1, 1, 1, 1, 1, 1, 1, 1]);
|
||||
pub const PITCH_LOW: [&[u8]; 3] =
|
||||
[&icdf::<4>(&[64, 64, 64, 64]), &icdf::<6>(&[43, 42, 43, 43, 42, 43]), &icdf::<8>(&[32, 32, 32, 32, 32, 32, 32, 32])];
|
||||
pub const PITCH_DELTA: [u8; 21] = icdf(&[46, 2, 2, 3, 4, 6, 10, 15, 26, 38, 30, 22, 15, 10, 7, 6, 4, 4, 2, 2, 2]);
|
||||
pub const PITCH_CONTOUR: [&[u8]; 4] = [
|
||||
&icdf::<3>(&[143, 50, 63]),
|
||||
&icdf::<11>(&[68, 12, 21, 17, 19, 22, 30, 24, 17, 16, 10]),
|
||||
&icdf::<12>(&[91, 46, 39, 19, 14, 12, 8, 7, 6, 5, 5, 4]),
|
||||
&icdf::<34>(&[33, 22, 18, 16, 15, 14, 14, 13, 13, 10, 9, 9, 8, 6, 6, 6, 5, 4, 4, 4, 3, 3, 3, 2, 2, 2, 2, 2, 2, 2, 1, 1, 1, 1]),
|
||||
];
|
||||
pub const CB_NB_10: [[i32; 2]; 3] = [[0, 0], [1, 0], [0, 1]];
|
||||
pub const CB_NB_20: [[i32; 4]; 11] = [
|
||||
[0, 0, 0, 0],
|
||||
[2, 1, 0, -1],
|
||||
[-1, 0, 1, 2],
|
||||
[-1, 0, 0, 1],
|
||||
[-1, 0, 0, 0],
|
||||
[0, 0, 0, 1],
|
||||
[0, 0, 1, 1],
|
||||
[1, 1, 0, 0],
|
||||
[1, 0, 0, 0],
|
||||
[0, 0, 0, -1],
|
||||
[1, 0, 0, -1],
|
||||
];
|
||||
pub const CB_WB_10: [[i32; 2]; 12] = [[0, 0], [0, 1], [1, 0], [-1, 1], [1, -1], [-1, 2], [2, -1], [-2, 2], [2, -2], [-2, 3], [3, -2], [-3, 3]];
|
||||
pub const CB_WB_20: [[i32; 4]; 34] = [
|
||||
[0, 0, 0, 0],
|
||||
[0, 0, 1, 1],
|
||||
[1, 1, 0, 0],
|
||||
[-1, 0, 0, 0],
|
||||
[0, 0, 0, 1],
|
||||
[1, 0, 0, 0],
|
||||
[-1, 0, 0, 1],
|
||||
[0, 0, 0, -1],
|
||||
[-1, 0, 1, 2],
|
||||
[1, 0, 0, -1],
|
||||
[-2, -1, 1, 2],
|
||||
[2, 1, 0, -1],
|
||||
[-2, 0, 0, 2],
|
||||
[-2, 0, 1, 3],
|
||||
[2, 1, -1, -2],
|
||||
[-3, -1, 1, 3],
|
||||
[2, 0, 0, -2],
|
||||
[3, 1, 0, -2],
|
||||
[-3, -1, 2, 4],
|
||||
[-4, -1, 1, 4],
|
||||
[3, 1, -1, -3],
|
||||
[-4, -1, 2, 5],
|
||||
[4, 2, -1, -3],
|
||||
[4, 1, -1, -4],
|
||||
[-5, -1, 2, 6],
|
||||
[5, 2, -1, -4],
|
||||
[-6, -2, 2, 6],
|
||||
[-5, -2, 2, 5],
|
||||
[6, 2, -1, -5],
|
||||
[-7, -2, 3, 8],
|
||||
[6, 2, -2, -6],
|
||||
[5, 2, -2, -5],
|
||||
[8, 3, -2, -7],
|
||||
[-9, -3, 3, 9],
|
||||
];
|
||||
pub const LTP_PERIODICITY: [u8; 3] = icdf(&[77, 80, 99]);
|
||||
pub const LTP_FILTER: [&[u8]; 3] = [
|
||||
&icdf::<8>(&[185, 15, 13, 13, 9, 9, 6, 6]),
|
||||
&icdf::<16>(&[57, 34, 21, 20, 15, 13, 12, 13, 10, 10, 9, 10, 9, 8, 7, 8]),
|
||||
&icdf::<32>(&[15, 16, 14, 12, 12, 12, 11, 11, 11, 10, 9, 9, 9, 9, 8, 8, 8, 8, 7, 7, 6, 6, 5, 4, 5, 4, 4, 4, 3, 4, 3, 2]),
|
||||
];
|
||||
pub const LTP_TAPS_0: [[i32; 5]; 8] = [
|
||||
[4, 6, 24, 7, 5],
|
||||
[0, 0, 2, 0, 0],
|
||||
[12, 28, 41, 13, -4],
|
||||
[-9, 15, 42, 25, 14],
|
||||
[1, -2, 62, 41, -9],
|
||||
[-10, 37, 65, -4, 3],
|
||||
[-6, 4, 66, 7, -8],
|
||||
[16, 14, 38, -3, 33],
|
||||
];
|
||||
pub const LTP_TAPS_1: [[i32; 5]; 16] = [
|
||||
[13, 22, 39, 23, 12],
|
||||
[-1, 36, 64, 27, -6],
|
||||
[-7, 10, 55, 43, 17],
|
||||
[1, 1, 8, 1, 1],
|
||||
[6, -11, 74, 53, -9],
|
||||
[-12, 55, 76, -12, 8],
|
||||
[-3, 3, 93, 27, -4],
|
||||
[26, 39, 59, 3, -8],
|
||||
[2, 0, 77, 11, 9],
|
||||
[-8, 22, 44, -6, 7],
|
||||
[40, 9, 26, 3, 9],
|
||||
[-7, 20, 101, -7, 4],
|
||||
[3, -8, 42, 26, 0],
|
||||
[-15, 33, 68, 2, 23],
|
||||
[-2, 55, 46, -2, 15],
|
||||
[3, -1, 21, 16, 41],
|
||||
];
|
||||
pub const LTP_TAPS_2: [[i32; 5]; 32] = [
|
||||
[-6, 27, 61, 39, 5],
|
||||
[-11, 42, 88, 4, 1],
|
||||
[-2, 60, 65, 6, -4],
|
||||
[-1, -5, 73, 56, 1],
|
||||
[-9, 19, 94, 29, -9],
|
||||
[0, 12, 99, 6, 4],
|
||||
[8, -19, 102, 46, -13],
|
||||
[3, 2, 13, 3, 2],
|
||||
[9, -21, 84, 72, -18],
|
||||
[-11, 46, 104, -22, 8],
|
||||
[18, 38, 48, 23, 0],
|
||||
[-16, 70, 83, -21, 11],
|
||||
[5, -11, 117, 22, -8],
|
||||
[-6, 23, 117, -12, 3],
|
||||
[3, -8, 95, 28, 4],
|
||||
[-10, 15, 77, 60, -15],
|
||||
[-1, 4, 124, 2, -4],
|
||||
[3, 38, 84, 24, -25],
|
||||
[2, 13, 42, 13, 31],
|
||||
[21, -4, 56, 46, -1],
|
||||
[-1, 35, 79, -13, 19],
|
||||
[-7, 65, 88, -9, -14],
|
||||
[20, 4, 81, 49, -29],
|
||||
[20, 0, 75, 3, -17],
|
||||
[5, -9, 44, 92, -8],
|
||||
[1, -3, 22, 69, 31],
|
||||
[-6, 95, 41, -12, 5],
|
||||
[39, 67, 16, -4, 1],
|
||||
[0, -6, 120, 55, -36],
|
||||
[-13, 44, 122, 4, -24],
|
||||
[81, 5, 11, 3, 7],
|
||||
[2, 0, 9, 10, 88],
|
||||
];
|
||||
pub const LTP_SCALE: [u8; 3] = icdf(&[128, 64, 64]);
|
||||
pub const UNIFORM4: [u8; 4] = icdf(&[64, 64, 64, 64]);
|
||||
pub const RATE_LEVEL: [&[u8]; 2] = [&icdf::<9>(&[15, 51, 12, 46, 45, 13, 33, 27, 14]), &icdf::<9>(&[33, 30, 36, 17, 34, 49, 18, 21, 18])];
|
||||
pub const PULSE_COUNT: [&[u8]; 11] = [
|
||||
&icdf::<18>(&[131, 74, 25, 8, 3, 3, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1]),
|
||||
&icdf::<18>(&[58, 93, 60, 23, 7, 3, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1]),
|
||||
&icdf::<18>(&[43, 51, 46, 33, 24, 16, 11, 8, 6, 3, 3, 3, 2, 1, 1, 2, 1, 2]),
|
||||
&icdf::<18>(&[17, 52, 71, 57, 31, 12, 5, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1]),
|
||||
&icdf::<18>(&[6, 21, 41, 53, 49, 35, 21, 11, 6, 3, 2, 2, 1, 1, 1, 1, 1, 1]),
|
||||
&icdf::<18>(&[7, 14, 22, 28, 29, 28, 25, 20, 17, 13, 11, 9, 7, 5, 4, 4, 3, 10]),
|
||||
&icdf::<18>(&[2, 5, 14, 29, 42, 46, 41, 31, 19, 11, 6, 3, 2, 1, 1, 1, 1, 1]),
|
||||
&icdf::<18>(&[1, 2, 4, 10, 19, 29, 35, 37, 34, 28, 20, 14, 8, 5, 4, 2, 2, 2]),
|
||||
&icdf::<18>(&[1, 2, 2, 5, 9, 14, 20, 24, 27, 28, 26, 23, 20, 15, 11, 8, 6, 15]),
|
||||
&icdf::<18>(&[1, 1, 1, 6, 27, 58, 56, 39, 25, 14, 10, 6, 3, 3, 2, 1, 1, 2]),
|
||||
&icdf::<18>(&[2, 1, 6, 27, 58, 56, 39, 25, 14, 10, 6, 3, 3, 2, 1, 1, 2, 0]),
|
||||
];
|
||||
pub const SHELL_16: [&[u8]; 16] = [
|
||||
&icdf::<2>(&[126, 130]),
|
||||
&icdf::<3>(&[56, 142, 58]),
|
||||
&icdf::<4>(&[25, 101, 104, 26]),
|
||||
&icdf::<5>(&[12, 60, 108, 64, 12]),
|
||||
&icdf::<6>(&[7, 35, 84, 87, 37, 6]),
|
||||
&icdf::<7>(&[4, 20, 59, 86, 63, 21, 3]),
|
||||
&icdf::<8>(&[3, 12, 38, 72, 75, 42, 12, 2]),
|
||||
&icdf::<9>(&[2, 8, 25, 54, 73, 59, 27, 7, 1]),
|
||||
&icdf::<10>(&[2, 5, 17, 39, 63, 65, 42, 18, 4, 1]),
|
||||
&icdf::<11>(&[1, 4, 12, 28, 49, 63, 54, 30, 11, 3, 1]),
|
||||
&icdf::<12>(&[1, 4, 8, 20, 37, 55, 57, 41, 22, 8, 2, 1]),
|
||||
&icdf::<13>(&[1, 3, 7, 15, 28, 44, 53, 48, 33, 16, 6, 1, 1]),
|
||||
&icdf::<14>(&[1, 2, 6, 12, 21, 35, 47, 48, 40, 25, 12, 5, 1, 1]),
|
||||
&icdf::<15>(&[1, 1, 4, 10, 17, 27, 37, 47, 43, 33, 21, 9, 4, 1, 1]),
|
||||
&icdf::<16>(&[1, 1, 1, 8, 14, 22, 33, 40, 43, 38, 28, 16, 8, 1, 1, 1]),
|
||||
&icdf::<17>(&[1, 1, 1, 1, 13, 18, 27, 36, 41, 41, 34, 24, 14, 1, 1, 1, 1]),
|
||||
];
|
||||
pub const SHELL_8: [&[u8]; 16] = [
|
||||
&icdf::<2>(&[127, 129]),
|
||||
&icdf::<3>(&[53, 149, 54]),
|
||||
&icdf::<4>(&[22, 105, 106, 23]),
|
||||
&icdf::<5>(&[11, 61, 111, 63, 10]),
|
||||
&icdf::<6>(&[6, 35, 86, 88, 36, 5]),
|
||||
&icdf::<7>(&[4, 20, 59, 87, 62, 21, 3]),
|
||||
&icdf::<8>(&[3, 13, 40, 71, 73, 41, 13, 2]),
|
||||
&icdf::<9>(&[3, 9, 27, 53, 70, 56, 28, 9, 1]),
|
||||
&icdf::<10>(&[3, 8, 19, 37, 57, 61, 44, 20, 6, 1]),
|
||||
&icdf::<11>(&[3, 7, 15, 28, 44, 54, 49, 33, 17, 5, 1]),
|
||||
&icdf::<12>(&[1, 7, 13, 22, 34, 46, 48, 38, 28, 14, 4, 1]),
|
||||
&icdf::<13>(&[1, 1, 11, 22, 27, 35, 42, 47, 33, 25, 10, 1, 1]),
|
||||
&icdf::<14>(&[1, 1, 6, 14, 26, 37, 43, 43, 37, 26, 14, 6, 1, 1]),
|
||||
&icdf::<15>(&[1, 1, 4, 10, 20, 31, 40, 42, 40, 31, 20, 10, 4, 1, 1]),
|
||||
&icdf::<16>(&[1, 1, 3, 8, 16, 26, 35, 38, 38, 35, 26, 16, 8, 3, 1, 1]),
|
||||
&icdf::<17>(&[1, 1, 2, 6, 12, 21, 30, 36, 38, 36, 30, 21, 12, 6, 2, 1, 1]),
|
||||
];
|
||||
pub const SHELL_4: [&[u8]; 16] = [
|
||||
&icdf::<2>(&[127, 129]),
|
||||
&icdf::<3>(&[49, 157, 50]),
|
||||
&icdf::<4>(&[20, 107, 109, 20]),
|
||||
&icdf::<5>(&[11, 60, 113, 62, 10]),
|
||||
&icdf::<6>(&[7, 36, 84, 87, 36, 6]),
|
||||
&icdf::<7>(&[6, 24, 57, 82, 60, 23, 4]),
|
||||
&icdf::<8>(&[5, 18, 39, 64, 68, 42, 16, 4]),
|
||||
&icdf::<9>(&[6, 14, 29, 47, 61, 52, 30, 14, 3]),
|
||||
&icdf::<10>(&[1, 15, 23, 35, 51, 50, 40, 30, 10, 1]),
|
||||
&icdf::<11>(&[1, 1, 21, 32, 42, 52, 46, 41, 18, 1, 1]),
|
||||
&icdf::<12>(&[1, 6, 16, 27, 36, 42, 42, 36, 27, 16, 6, 1]),
|
||||
&icdf::<13>(&[1, 5, 12, 21, 31, 38, 40, 38, 31, 21, 12, 5, 1]),
|
||||
&icdf::<14>(&[1, 3, 9, 17, 26, 34, 38, 38, 34, 26, 17, 9, 3, 1]),
|
||||
&icdf::<15>(&[1, 3, 7, 14, 22, 29, 34, 36, 34, 29, 22, 14, 7, 3, 1]),
|
||||
&icdf::<16>(&[1, 2, 5, 11, 18, 25, 31, 35, 35, 31, 25, 18, 11, 5, 2, 1]),
|
||||
&icdf::<17>(&[1, 1, 4, 9, 15, 21, 28, 32, 34, 32, 28, 21, 15, 9, 4, 1, 1]),
|
||||
];
|
||||
pub const SHELL_2: [&[u8]; 16] = [
|
||||
&icdf::<2>(&[128, 128]),
|
||||
&icdf::<3>(&[42, 172, 42]),
|
||||
&icdf::<4>(&[21, 107, 107, 21]),
|
||||
&icdf::<5>(&[12, 60, 112, 61, 11]),
|
||||
&icdf::<6>(&[8, 34, 86, 86, 35, 7]),
|
||||
&icdf::<7>(&[8, 23, 55, 90, 55, 20, 5]),
|
||||
&icdf::<8>(&[5, 15, 38, 72, 72, 36, 15, 3]),
|
||||
&icdf::<9>(&[6, 12, 27, 52, 77, 47, 20, 10, 5]),
|
||||
&icdf::<10>(&[6, 19, 28, 35, 40, 40, 35, 28, 19, 6]),
|
||||
&icdf::<11>(&[4, 14, 22, 31, 37, 40, 37, 31, 22, 14, 4]),
|
||||
&icdf::<12>(&[3, 10, 18, 26, 33, 38, 38, 33, 26, 18, 10, 3]),
|
||||
&icdf::<13>(&[2, 8, 13, 21, 29, 36, 38, 36, 29, 21, 13, 8, 2]),
|
||||
&icdf::<14>(&[1, 5, 10, 17, 25, 32, 38, 38, 32, 25, 17, 10, 5, 1]),
|
||||
&icdf::<15>(&[1, 4, 7, 13, 21, 29, 35, 36, 35, 29, 21, 13, 7, 4, 1]),
|
||||
&icdf::<16>(&[1, 2, 5, 10, 17, 25, 32, 36, 36, 32, 25, 17, 10, 5, 2, 1]),
|
||||
&icdf::<17>(&[1, 2, 4, 7, 13, 21, 28, 34, 36, 34, 28, 21, 13, 7, 4, 2, 1]),
|
||||
];
|
||||
pub const EXC_LSB: [u8; 2] = icdf(&[136, 120]);
|
||||
pub const EXC_SIGN: [&[u8]; 42] = [
|
||||
&icdf::<2>(&[2, 254]),
|
||||
&icdf::<2>(&[207, 49]),
|
||||
&icdf::<2>(&[189, 67]),
|
||||
&icdf::<2>(&[179, 77]),
|
||||
&icdf::<2>(&[174, 82]),
|
||||
&icdf::<2>(&[163, 93]),
|
||||
&icdf::<2>(&[157, 99]),
|
||||
&icdf::<2>(&[58, 198]),
|
||||
&icdf::<2>(&[245, 11]),
|
||||
&icdf::<2>(&[238, 18]),
|
||||
&icdf::<2>(&[232, 24]),
|
||||
&icdf::<2>(&[225, 31]),
|
||||
&icdf::<2>(&[220, 36]),
|
||||
&icdf::<2>(&[211, 45]),
|
||||
&icdf::<2>(&[1, 255]),
|
||||
&icdf::<2>(&[210, 46]),
|
||||
&icdf::<2>(&[190, 66]),
|
||||
&icdf::<2>(&[178, 78]),
|
||||
&icdf::<2>(&[169, 87]),
|
||||
&icdf::<2>(&[162, 94]),
|
||||
&icdf::<2>(&[152, 104]),
|
||||
&icdf::<2>(&[48, 208]),
|
||||
&icdf::<2>(&[242, 14]),
|
||||
&icdf::<2>(&[235, 21]),
|
||||
&icdf::<2>(&[224, 32]),
|
||||
&icdf::<2>(&[214, 42]),
|
||||
&icdf::<2>(&[205, 51]),
|
||||
&icdf::<2>(&[190, 66]),
|
||||
&icdf::<2>(&[1, 255]),
|
||||
&icdf::<2>(&[162, 94]),
|
||||
&icdf::<2>(&[152, 104]),
|
||||
&icdf::<2>(&[147, 109]),
|
||||
&icdf::<2>(&[144, 112]),
|
||||
&icdf::<2>(&[141, 115]),
|
||||
&icdf::<2>(&[138, 118]),
|
||||
&icdf::<2>(&[8, 248]),
|
||||
&icdf::<2>(&[203, 53]),
|
||||
&icdf::<2>(&[187, 69]),
|
||||
&icdf::<2>(&[176, 80]),
|
||||
&icdf::<2>(&[168, 88]),
|
||||
&icdf::<2>(&[161, 95]),
|
||||
&icdf::<2>(&[154, 102]),
|
||||
];
|
||||
pub const LBRR_FLAGS: [&[u8]; 2] = [&icdf::<3>(&[53, 53, 150]), &icdf::<7>(&[41, 20, 29, 41, 15, 28, 82])];
|
||||
+37
-26
@@ -301,18 +301,14 @@ pub fn master_text_pieces(pres: &Presentation, m: &deckcraft_model::Master, pape
|
||||
let (kx, ky) = (paper.0 / pres.notes_size.width.max(1.0), paper.1 / pres.notes_size.height.max(1.0));
|
||||
let mut out = vec![];
|
||||
for pt in deckcraft_render::place_master_texts(pres, m, n) {
|
||||
let mut para_chars: HashMap<usize, Vec<char>> = HashMap::new();
|
||||
for run in &pt.layout.runs {
|
||||
let Some(g) = run.glyphs.first() else { continue };
|
||||
let text: String = pt
|
||||
.body
|
||||
.paragraphs
|
||||
.get(run.para)
|
||||
.map(|p| p.runs.iter().map(|r| r.content()).collect::<String>())
|
||||
.unwrap_or_default()
|
||||
.chars()
|
||||
.skip(run.chars.0)
|
||||
.take(run.chars.1.saturating_sub(run.chars.0))
|
||||
.collect();
|
||||
let chars = para_chars.entry(run.para).or_insert_with(|| {
|
||||
pt.body.paragraphs.get(run.para).map(|p| p.runs.iter().flat_map(|r| r.content().chars()).collect()).unwrap_or_default()
|
||||
});
|
||||
let (a, b) = (run.chars.0.min(chars.len()), run.chars.1.min(chars.len()));
|
||||
let text: String = chars.get(a..b.max(a)).unwrap_or_default().iter().collect();
|
||||
if text.trim().is_empty() {
|
||||
continue;
|
||||
}
|
||||
@@ -498,6 +494,9 @@ impl Backdrops {
|
||||
/// Most pixels one slide raster may have; it is RGBA, and the backdrop and the content are two rasters.
|
||||
const MAX_RASTER_PIXELS: f64 = 64_000_000.0;
|
||||
|
||||
/// Most ruled note lines one handout area gets (a letter page holds about 30).
|
||||
const MAX_NOTE_LINES: usize = 500;
|
||||
|
||||
/// Render scale for a slide of `slide` units drawn `box_width` points wide at `dpi`, with the total pixel count capped.
|
||||
fn raster_scale(box_width: f64, slide: (f64, f64), dpi: f64) -> f64 {
|
||||
let scale = (box_width / 72.0 * dpi.clamp(36.0, 600.0)) / slide.0;
|
||||
@@ -574,20 +573,17 @@ impl Exporter<'_> {
|
||||
|
||||
/// Invisible glyphs exactly over the rasterised text.
|
||||
fn text_layer(&mut self, s: &mut Surface, pt: &PlacedText, view: Affine, visible: bool) -> Result<()> {
|
||||
// Each paragraph's characters, gathered once: a long paragraph wraps into many runs.
|
||||
let mut para_chars: HashMap<usize, Vec<char>> = HashMap::new();
|
||||
for run in &pt.layout.runs {
|
||||
if run.glyphs.is_empty() || run.size <= 0.0 {
|
||||
continue;
|
||||
}
|
||||
let text: String = pt
|
||||
.body
|
||||
.paragraphs
|
||||
.get(run.para)
|
||||
.map(|p| p.runs.iter().map(|r| r.content()).collect::<String>())
|
||||
.unwrap_or_default()
|
||||
.chars()
|
||||
.skip(run.chars.0)
|
||||
.take(run.chars.1.saturating_sub(run.chars.0))
|
||||
.collect();
|
||||
let chars = para_chars.entry(run.para).or_insert_with(|| {
|
||||
pt.body.paragraphs.get(run.para).map(|p| p.runs.iter().flat_map(|r| r.content().chars()).collect()).unwrap_or_default()
|
||||
});
|
||||
let (a, b) = (run.chars.0.min(chars.len()), run.chars.1.min(chars.len()));
|
||||
let text: String = chars.get(a..b.max(a)).unwrap_or_default().iter().collect();
|
||||
if text.trim().is_empty() {
|
||||
continue;
|
||||
}
|
||||
@@ -667,19 +663,28 @@ impl Exporter<'_> {
|
||||
let lh = size * 1.3;
|
||||
let mut y = area.y0 + size;
|
||||
s.set_fill(Some(grey(0x20, 1.0)));
|
||||
let space = width(" ");
|
||||
'outer: for para in text.split('\n') {
|
||||
// The line's width is kept as words are added, not measured again for each word.
|
||||
let mut line = String::new();
|
||||
let mut line_w = 0.0;
|
||||
for word in para.split(' ') {
|
||||
let cand = if line.is_empty() { word.to_string() } else { format!("{line} {word}") };
|
||||
if width(&cand) > area.width() && !line.is_empty() {
|
||||
let word_w = width(word);
|
||||
let cand_w = if line.is_empty() { word_w } else { line_w + space + word_w };
|
||||
if cand_w > area.width() && !line.is_empty() {
|
||||
if y > area.y1 {
|
||||
break 'outer;
|
||||
}
|
||||
s.draw_text(Point::from_xy(area.x0 as f32, y as f32), font.clone(), size as f32, &line, false, TextDirection::Auto);
|
||||
y += lh;
|
||||
line = word.to_string();
|
||||
line_w = word_w;
|
||||
} else {
|
||||
line = cand;
|
||||
if !line.is_empty() {
|
||||
line.push(' ');
|
||||
}
|
||||
line.push_str(word);
|
||||
line_w = cand_w;
|
||||
}
|
||||
}
|
||||
if y > area.y1 {
|
||||
@@ -696,20 +701,26 @@ impl Exporter<'_> {
|
||||
|
||||
/// Ruled lines for handwritten notes (3-per-page handouts).
|
||||
fn note_lines(&self, s: &mut Surface, r: Rect) {
|
||||
let mut y = r.y0 + 18.0;
|
||||
// Counted, not stepped: at huge coordinates adding 22 leaves y unchanged, and a huge page
|
||||
// would want millions of rules.
|
||||
let first = r.y0 + 18.0;
|
||||
let count = ((r.y1 - first) / 22.0).floor() + 1.0;
|
||||
if count.is_nan() || count < 1.0 {
|
||||
return;
|
||||
}
|
||||
s.set_stroke(Some(krilla::paint::Stroke {
|
||||
paint: krilla::color::rgb::Color::new(0xB0, 0xB0, 0xB0).into(),
|
||||
width: 0.5,
|
||||
..Default::default()
|
||||
}));
|
||||
while y <= r.y1 {
|
||||
for i in 0..(count.min(MAX_NOTE_LINES as f64) as usize) {
|
||||
let y = first + 22.0 * i as f64;
|
||||
let mut p = kurbo::BezPath::new();
|
||||
p.move_to((r.x0, y));
|
||||
p.line_to((r.x1, y));
|
||||
if let Some(path) = to_path(&p) {
|
||||
s.draw_path(&path);
|
||||
}
|
||||
y += 22.0;
|
||||
}
|
||||
s.set_stroke(None);
|
||||
}
|
||||
|
||||
@@ -728,3 +728,17 @@ fn raster_scale_caps_total_pixels_not_each_side() {
|
||||
let s = raster_scale(720.0, (960.0, 540.0), 200.0);
|
||||
assert!((s - 720.0 / 72.0 * 200.0 / 960.0).abs() < 1e-12);
|
||||
}
|
||||
|
||||
#[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
|
||||
#[cfg_attr(not(target_arch = "wasm32"), test)]
|
||||
fn huge_paper_and_long_notes_export_in_bounded_work() {
|
||||
// At 1e19 pt adding a rule's 22 pt leaves y unchanged: the rules are counted, not stepped. A narrow page keeps the raster small.
|
||||
let p = deck(1);
|
||||
let bytes = export(&p, &PdfOptions { paper: (612.0, 1.0e19), ..opts(PageLayout::Handouts { per_page: 3 }) }).expect("export");
|
||||
assert!(bytes.starts_with(b"%PDF-"));
|
||||
// 20,000 short words of notes, wrapped with the line width kept as words are added.
|
||||
let mut p = deck(1);
|
||||
Arc::make_mut(&mut p.slides[0]).notes = TextBody::from_text(&"ab ".repeat(20_000));
|
||||
let bytes = export(&p, &opts(PageLayout::Notes)).expect("export");
|
||||
assert!(bytes.starts_with(b"%PDF-"));
|
||||
}
|
||||
|
||||
+31
-3
@@ -403,7 +403,7 @@ pub fn data_xml(d: &Diagram, seed: u32, drawing_rel: &str, pics: &[(usize, Strin
|
||||
w.finish()
|
||||
}
|
||||
|
||||
/// The outline of a data part: the `parOf` tree under the document point, depth first.
|
||||
/// The outline of a data part: the `parOf` tree under the document point, depth first, or the data points in `ptLst` order when no `parOf` connections exist.
|
||||
pub fn outline(data: &El) -> Vec<DiagramNode> {
|
||||
outline_ids(data).into_iter().map(|(_, n)| n).collect()
|
||||
}
|
||||
@@ -429,11 +429,13 @@ pub fn node_pictures(data: &El) -> Vec<(usize, String)> {
|
||||
out
|
||||
}
|
||||
|
||||
/// Extracts diagram outline `(modelId, DiagramNode)` pairs in tree order from a `dgm:dataModel` element.
|
||||
fn outline_ids(data: &El) -> Vec<(String, DiagramNode)> {
|
||||
let Some(pts) = data.child("ptLst") else { return vec![] };
|
||||
let mut texts: HashMap<&str, String> = HashMap::new();
|
||||
let mut assistants: std::collections::HashSet<&str> = std::collections::HashSet::new();
|
||||
let mut titles: HashMap<&str, String> = HashMap::new();
|
||||
let mut pt_order: Vec<&str> = Vec::new();
|
||||
let mut doc: Option<&str> = None;
|
||||
for pt in pts.children_named("pt").take(MAX_NODES * 4) {
|
||||
let Some(id) = pt.attr("modelId") else { continue };
|
||||
@@ -442,6 +444,7 @@ fn outline_ids(data: &El) -> Vec<(String, DiagramNode)> {
|
||||
ty @ ("node" | "asst") => {
|
||||
let t = pt.child("t").map(paragraphs).unwrap_or_default();
|
||||
texts.insert(id, t);
|
||||
pt_order.push(id);
|
||||
if ty == "asst" {
|
||||
assistants.insert(id);
|
||||
}
|
||||
@@ -459,7 +462,6 @@ fn outline_ids(data: &El) -> Vec<(String, DiagramNode)> {
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
let Some(doc) = doc else { return vec![] };
|
||||
let mut kids: HashMap<&str, Vec<(i64, &str)>> = HashMap::new();
|
||||
if let Some(cx) = data.child("cxnLst") {
|
||||
for c in cx.children_named("cxn").take(MAX_NODES * 4) {
|
||||
@@ -478,7 +480,7 @@ fn outline_ids(data: &El) -> Vec<(String, DiagramNode)> {
|
||||
let mut out = vec![];
|
||||
let mut seen = std::collections::HashSet::new();
|
||||
// Depth first, iteratively; a node reached twice (a cycle in a hostile file) is skipped.
|
||||
let mut stack: Vec<(&str, u8)> = kids.get(doc).map(|v| v.iter().rev().map(|(_, id)| (*id, 0u8)).collect()).unwrap_or_default();
|
||||
let mut stack: Vec<(&str, u8)> = doc.and_then(|d| kids.get(d)).map(|v| v.iter().rev().map(|(_, id)| (*id, 0u8)).collect()).unwrap_or_default();
|
||||
while let Some((id, level)) = stack.pop() {
|
||||
if out.len() >= MAX_NODES || !seen.insert(id) {
|
||||
continue;
|
||||
@@ -497,6 +499,23 @@ fn outline_ids(data: &El) -> Vec<(String, DiagramNode)> {
|
||||
stack.extend(v.iter().rev().map(|(_, c)| (*c, level.saturating_add(1))));
|
||||
}
|
||||
}
|
||||
if out.is_empty() && kids.is_empty() {
|
||||
for id in pt_order {
|
||||
if out.len() >= MAX_NODES || !seen.insert(id) {
|
||||
continue;
|
||||
}
|
||||
out.push((
|
||||
id.to_string(),
|
||||
DiagramNode {
|
||||
text: texts.get(id).cloned().unwrap_or_default(),
|
||||
level: 0,
|
||||
picture: None,
|
||||
assistant: assistants.contains(id),
|
||||
title: titles.get(id).cloned().unwrap_or_default(),
|
||||
},
|
||||
));
|
||||
}
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
@@ -878,5 +897,14 @@ mod tests {
|
||||
assert_eq!(colors_for(&cs_id("accent4")), "accent4");
|
||||
assert_eq!(colors_for(&cs_id("")), "");
|
||||
assert_eq!(style_for(&qs_id("outline")), "outline");
|
||||
|
||||
let unconnected = br#"<dgm:dataModel xmlns:dgm="d"><dgm:ptLst><dgm:pt modelId="0" type="doc"/><dgm:pt modelId="1" type="node"><dgm:t><a:p xmlns:a="a"><a:r><a:t>Alpha</a:t></a:r></a:p></dgm:t></dgm:pt><dgm:pt modelId="2" type="node"><dgm:t><a:p xmlns:a="a"><a:r><a:t>Beta</a:t></a:r></a:p></dgm:t></dgm:pt></dgm:ptLst><dgm:cxnLst/></dgm:dataModel>"#;
|
||||
let unconnected_doc = crate::xml::parse(unconnected).unwrap();
|
||||
assert_eq!(outline(&unconnected_doc.root), nodes(&[("Alpha", 0), ("Beta", 0)]));
|
||||
|
||||
// When parOf connections exist but no root (type="doc") reaches them, do not flatten the hierarchy to level 0.
|
||||
let unrooted_hierarchy = br#"<dgm:dataModel xmlns:dgm="d"><dgm:ptLst><dgm:pt modelId="1" type="node"><dgm:t><a:p xmlns:a="a"><a:r><a:t>Parent</a:t></a:r></a:p></dgm:t></dgm:pt><dgm:pt modelId="2" type="node"><dgm:t><a:p xmlns:a="a"><a:r><a:t>Child</a:t></a:r></a:p></dgm:t></dgm:pt></dgm:ptLst><dgm:cxnLst><dgm:cxn srcId="1" destId="2"/></dgm:cxnLst></dgm:dataModel>"#;
|
||||
let unrooted_doc = crate::xml::parse(unrooted_hierarchy).unwrap();
|
||||
assert!(outline(&unrooted_doc.root).is_empty());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -70,6 +70,13 @@ pub fn import_checked(bytes: &[u8]) -> Result<(Presentation, Vec<String>), PptxE
|
||||
read::import_checked(bytes)
|
||||
}
|
||||
|
||||
/// [`import`] with a lower presentation-wide chart point budget, so tests reach the over-budget
|
||||
/// path without a multi-million-point chart.
|
||||
#[doc(hidden)]
|
||||
pub fn import_with_chart_budget(bytes: &[u8], points: usize) -> Result<Presentation, PptxError> {
|
||||
read::import_with_chart_points(bytes, points).map(|(p, _)| p)
|
||||
}
|
||||
|
||||
/// Read the theme of a `.thmx` file, or of a `.pptx` / `.potx`'s first master, with its media.
|
||||
pub fn import_theme(bytes: &[u8]) -> Result<(deckcraft_model::theme::Theme, Vec<deckcraft_model::MediaItem>), PptxError> {
|
||||
read::import_theme(bytes)
|
||||
|
||||
+216
-26
@@ -10,6 +10,7 @@ mod timing;
|
||||
pub(crate) use chart::{chart_from as chart_from_el, read_chart_xml};
|
||||
pub(crate) use chartex::{chartex_from, read_chartex_xml};
|
||||
|
||||
use std::cell::Cell;
|
||||
use std::collections::{HashMap, HashSet};
|
||||
use std::sync::Arc;
|
||||
|
||||
@@ -38,8 +39,17 @@ pub struct Imp<'a> {
|
||||
pub vml_previews: HashMap<String, HashMap<String, String>>,
|
||||
/// Aggregate byte count of retained chart sidecars across the presentation.
|
||||
pub kept_sidecar_bytes: usize,
|
||||
/// Data slots of the charts decoded so far (see `chart::MAX_PRESENTATION_CHART_POINTS`).
|
||||
pub chart_points: usize,
|
||||
/// Points the presentation may hold across modelled charts.
|
||||
pub chart_points_cap: usize,
|
||||
/// Cached kept sidecars by normalized chart part name.
|
||||
pub kept_charts: HashMap<String, deckcraft_model::chart::ChartKept>,
|
||||
/// Chart parts whose sidecar capture was refused, with the retained-byte total it started from:
|
||||
/// the same start gives the same refusal, so the copy is not repeated.
|
||||
pub rejected_kept: HashMap<String, usize>,
|
||||
/// Bytes of chart parts decoded so far, rejected ones included (see `chart::MAX_CHART_DECODE_BYTES`).
|
||||
pub chart_decode_bytes: usize,
|
||||
/// Add-in tags parts, parsed once each, by normalized part name.
|
||||
pub tags_by_part: HashMap<String, Arc<[(String, String)]>>,
|
||||
/// Bytes of tags copied into the model so far (see [`MAX_TAG_BYTES`]).
|
||||
@@ -90,21 +100,22 @@ pub struct Part {
|
||||
pub ns: Vec<(String, String)>,
|
||||
/// Prefixes bound below the root only (on a wrapper such as `mc:Choice`), each to one namespace.
|
||||
pub inner_ns: Vec<(String, String)>,
|
||||
/// Bytes of namespace declarations this part may still copy onto kept fragments.
|
||||
ns_budget: Cell<usize>,
|
||||
}
|
||||
|
||||
impl Part {
|
||||
pub fn new(pkg: &Package, name: &str, doc: &Doc) -> Self {
|
||||
let ns = doc.ns_decls();
|
||||
Part { name: name.to_string(), rels: pkg.rels(name), inner_ns: inner_decls(&doc.root, &ns), ns }
|
||||
Part { name: name.to_string(), rels: pkg.rels(name), inner_ns: inner_decls(&doc.root, &ns), ns, ns_budget: Cell::new(MAX_COPIED_NS_BYTES) }
|
||||
}
|
||||
/// Serialize an element with the namespace declarations it needs from the part root.
|
||||
pub fn keep(&self, el: &El) -> String {
|
||||
keep_xml(el, &self.ns)
|
||||
keep_with(el, &[&self.ns], Some(&self.ns_budget))
|
||||
}
|
||||
/// Like [`Part::keep`], also declaring prefixes that only an enclosing wrapper binds.
|
||||
pub fn keep_inner(&self, el: &El) -> String {
|
||||
let ns: Vec<(String, String)> = self.ns.iter().chain(&self.inner_ns).cloned().collect();
|
||||
keep_xml(el, &ns)
|
||||
keep_with(el, &[&self.ns, &self.inner_ns], Some(&self.ns_budget))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -139,36 +150,81 @@ fn inner_decls(root: &El, ns: &[(String, String)]) -> Vec<(String, String)> {
|
||||
out
|
||||
}
|
||||
|
||||
/// Serialize `el`, declaring every prefix it uses that the source root declared.
|
||||
pub fn keep_xml(el: &El, ns: &[(String, String)]) -> String {
|
||||
let mut used = HashSet::new();
|
||||
fn rec(e: &El, used: &mut HashSet<String>, depth: usize) {
|
||||
/// Longest namespace name copied onto a kept fragment or a resolved `mc` branch. Real ones are
|
||||
/// URIs of well under 100 bytes; a longer one is not copied, so one declaration cannot be
|
||||
/// multiplied by every fragment that uses its prefix.
|
||||
const MAX_NS_VALUE: usize = 2048;
|
||||
|
||||
/// Namespace declaration bytes one part may copy onto all its kept fragments together; past it a
|
||||
/// fragment keeps its markup but is not given further declarations.
|
||||
const MAX_COPIED_NS_BYTES: usize = 8 << 20;
|
||||
|
||||
/// Prefixes `el` and its descendants use in element and attribute names, and those listed in
|
||||
/// `mc:Ignorable` or `mc:MustUnderstand` (which must stay declared for the markup-compatibility rules to hold),
|
||||
/// and those starting a QName-like attribute value.
|
||||
fn used_prefixes(el: &El) -> HashSet<&str> {
|
||||
fn rec<'a>(e: &'a El, used: &mut HashSet<&'a str>, depth: usize) {
|
||||
if depth > crate::xml::MAX_DEPTH {
|
||||
return;
|
||||
}
|
||||
if let Some((p, _)) = e.name.split_once(':') {
|
||||
used.insert(p.to_string());
|
||||
used.insert(p);
|
||||
}
|
||||
for (k, _) in &e.attrs {
|
||||
if let Some((p, _)) = k.split_once(':')
|
||||
for (k, v) in &e.attrs {
|
||||
if let Some((p, local)) = k.split_once(':')
|
||||
&& p != "xmlns"
|
||||
&& p != "xml"
|
||||
{
|
||||
used.insert(p.to_string());
|
||||
used.insert(p);
|
||||
if local == "Ignorable" || local == "MustUnderstand" {
|
||||
used.extend(v.split_whitespace());
|
||||
}
|
||||
}
|
||||
// QName-valued attributes (`xsi:type="q:Item"`) need their prefix declared too.
|
||||
if k != "xmlns" && !k.starts_with("xmlns:") {
|
||||
for t in v.split_whitespace() {
|
||||
if let Some((p, _)) = t.split_once(':')
|
||||
&& !p.is_empty()
|
||||
{
|
||||
used.insert(p);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
for c in e.elements() {
|
||||
rec(c, used, depth + 1);
|
||||
}
|
||||
}
|
||||
let mut used = HashSet::new();
|
||||
rec(el, &mut used, 0);
|
||||
let mut el = el.clone();
|
||||
for (k, v) in ns {
|
||||
used
|
||||
}
|
||||
|
||||
/// Serialize `el`, declaring every prefix it uses that the source root declared.
|
||||
pub fn keep_xml(el: &El, ns: &[(String, String)]) -> String {
|
||||
keep_with(el, &[ns], None)
|
||||
}
|
||||
|
||||
fn keep_with(el: &El, decls: &[&[(String, String)]], budget: Option<&Cell<usize>>) -> String {
|
||||
let used = used_prefixes(el);
|
||||
let mut have: HashSet<&str> = el.attrs.iter().map(|(a, _)| a.as_str()).collect();
|
||||
let mut extra = Vec::new();
|
||||
for (k, v) in decls.iter().flat_map(|d| d.iter()) {
|
||||
let p = k.strip_prefix("xmlns:").unwrap_or("");
|
||||
if !p.is_empty() && used.contains(p) && !el.attrs.iter().any(|(a, _)| a == k) {
|
||||
el.attrs.push((k.clone(), v.clone()));
|
||||
if !p.is_empty() && used.contains(p) && v.len() <= MAX_NS_VALUE && !have.contains(k.as_str()) {
|
||||
let cost = k.len() + v.len();
|
||||
if let Some(b) = budget {
|
||||
if b.get() < cost {
|
||||
continue;
|
||||
}
|
||||
b.set(b.get() - cost);
|
||||
}
|
||||
have.insert(k);
|
||||
extra.push((k.clone(), v.clone()));
|
||||
}
|
||||
}
|
||||
let mut el = el.clone();
|
||||
el.attrs.extend(extra);
|
||||
el.to_xml()
|
||||
}
|
||||
|
||||
@@ -317,10 +373,18 @@ fn kept_parts(pkg: &Package, pres_path: &str) -> Vec<deckcraft_model::KeptPart>
|
||||
// The part's own relationships (customXml item → itemProps) and their targets.
|
||||
let rp = opc::rels_path(&name);
|
||||
if let Some(rels_xml) = text(&rp) {
|
||||
let mut capped = false;
|
||||
for sub in pkg.rels(&name).items.iter().filter(|s| !s.external && keepable(&s.target)) {
|
||||
if let Some(x) = text(&sub.target)
|
||||
&& !out.iter().any(|k| k.name.eq_ignore_ascii_case(&sub.target))
|
||||
{
|
||||
// Checked before the part is read: repeated relationships to one large part
|
||||
// must not copy it again each time.
|
||||
if out.iter().any(|k| k.name.eq_ignore_ascii_case(&sub.target)) {
|
||||
continue;
|
||||
}
|
||||
if out.len() >= 400 {
|
||||
capped = true;
|
||||
continue;
|
||||
}
|
||||
if let Some(x) = text(&sub.target) {
|
||||
out.push(KeptPart {
|
||||
content_type: pkg.content_type(&sub.target).unwrap_or_default(),
|
||||
name: sub.target.clone(),
|
||||
@@ -329,7 +393,9 @@ fn kept_parts(pkg: &Package, pres_path: &str) -> Vec<deckcraft_model::KeptPart>
|
||||
});
|
||||
}
|
||||
}
|
||||
out.push(KeptPart { name: rp, xml: rels_xml, ..Default::default() });
|
||||
if !capped && out.len() < 400 {
|
||||
out.push(KeptPart { name: rp, xml: rels_xml, ..Default::default() });
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -366,15 +432,39 @@ fn resolve_mc_rels(el: &mut El, depth: usize, rels: Option<&Rels>) {
|
||||
});
|
||||
let branch = choice.or_else(|| ac.child("Fallback"));
|
||||
if let Some(b) = branch {
|
||||
// The branch's namespace declarations (the first usable one of each name,
|
||||
// in order), indexed by prefix once: each child then looks up the prefixes
|
||||
// it uses instead of scanning every declaration.
|
||||
let mut decls: Vec<(&String, &String)> = Vec::new();
|
||||
// An oversized declaration still shadows an outer one of its name: its
|
||||
// content is left unbound rather than bound to the outer namespace.
|
||||
let mut by_prefix: HashMap<&str, Option<usize>> = HashMap::new();
|
||||
for (k, v) in b.attrs.iter().chain(ac.attrs.iter()) {
|
||||
let prefix = if k == "xmlns" { Some("") } else { k.strip_prefix("xmlns:") };
|
||||
if let Some(p) = prefix
|
||||
&& !by_prefix.contains_key(p)
|
||||
{
|
||||
by_prefix.insert(p, (v.len() <= MAX_NS_VALUE).then_some(decls.len()));
|
||||
if v.len() <= MAX_NS_VALUE {
|
||||
decls.push((k, v));
|
||||
}
|
||||
}
|
||||
}
|
||||
for n in &b.children {
|
||||
if let Node::El(e) = n {
|
||||
// Carry the namespace declarations from the branch that its content
|
||||
// uses (all of them per child made children x declarations copies).
|
||||
let used = used_prefixes(e);
|
||||
let have: HashSet<&str> = e.attrs.iter().map(|(a, _)| a.as_str()).collect();
|
||||
let mut wanted: Vec<usize> = std::iter::once("")
|
||||
.chain(used.iter().copied())
|
||||
.filter_map(|p| by_prefix.get(p).copied().flatten())
|
||||
.filter(|&i| !have.contains(decls[i].0.as_str()))
|
||||
.collect();
|
||||
wanted.sort_unstable();
|
||||
wanted.dedup();
|
||||
let mut e = e.clone();
|
||||
// Carry namespace declarations from the branch onto its content.
|
||||
for (k, v) in b.attrs.iter().chain(ac.attrs.iter()) {
|
||||
if k.starts_with("xmlns") && !e.attrs.iter().any(|(a, _)| a == k) {
|
||||
e.attrs.push((k.clone(), v.clone()));
|
||||
}
|
||||
}
|
||||
e.attrs.extend(wanted.into_iter().map(|i| (decls[i].0.clone(), decls[i].1.clone())));
|
||||
out.push(Node::El(e));
|
||||
}
|
||||
}
|
||||
@@ -445,7 +535,11 @@ pub fn import_theme(bytes: &[u8]) -> Result<(deckcraft_model::theme::Theme, Vec<
|
||||
table_style_ids: Vec::new(),
|
||||
vml_previews: HashMap::new(),
|
||||
kept_sidecar_bytes: 0,
|
||||
chart_points: 0,
|
||||
chart_points_cap: chart::MAX_PRESENTATION_CHART_POINTS,
|
||||
kept_charts: HashMap::new(),
|
||||
rejected_kept: HashMap::new(),
|
||||
chart_decode_bytes: 0,
|
||||
tags_by_part: HashMap::new(),
|
||||
tag_bytes: 0,
|
||||
tags_capped: false,
|
||||
@@ -463,6 +557,11 @@ pub fn import(bytes: &[u8]) -> Result<Presentation, PptxError> {
|
||||
/// [`import`], plus the parts of the slide list the file names but that couldn't be read (a
|
||||
/// missing or truncated slide): those slides are left out, so a caller can say so (#203).
|
||||
pub fn import_checked(bytes: &[u8]) -> Result<(Presentation, Vec<String>), PptxError> {
|
||||
import_with_chart_points(bytes, chart::MAX_PRESENTATION_CHART_POINTS)
|
||||
}
|
||||
|
||||
/// [`import_checked`] with the presentation's chart point budget set (tests lower it).
|
||||
pub(crate) fn import_with_chart_points(bytes: &[u8], chart_points_cap: usize) -> Result<(Presentation, Vec<String>), PptxError> {
|
||||
let mut lost_slides: Vec<String> = vec![];
|
||||
let pkg = Package::open(bytes)?;
|
||||
let root_rels = pkg.rels("");
|
||||
@@ -487,7 +586,11 @@ pub fn import_checked(bytes: &[u8]) -> Result<(Presentation, Vec<String>), PptxE
|
||||
table_style_ids: Vec::new(),
|
||||
vml_previews: HashMap::new(),
|
||||
kept_sidecar_bytes: 0,
|
||||
chart_points: 0,
|
||||
chart_points_cap,
|
||||
kept_charts: HashMap::new(),
|
||||
rejected_kept: HashMap::new(),
|
||||
chart_decode_bytes: 0,
|
||||
tags_by_part: HashMap::new(),
|
||||
tag_bytes: 0,
|
||||
tags_capped: false,
|
||||
@@ -1423,3 +1526,90 @@ fn header_footer(p: &Presentation) -> HeaderFooter {
|
||||
}
|
||||
hf
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn mc_branches_carry_only_the_declarations_their_content_uses() {
|
||||
// 2,000 declarations on the branch and 2,000 children: each child gets the one it uses
|
||||
// (and the default), not all 2,000.
|
||||
let decls: String = (0..2000).map(|i| format!(r#" xmlns:n{i}="urn:n{i}""#)).collect();
|
||||
let kids: String = (0..2000).map(|i| format!("<n{i}:x/>")).collect();
|
||||
let x = format!(
|
||||
r#"<r xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"><mc:AlternateContent><mc:Fallback xmlns="urn:d"{decls}>{kids}</mc:Fallback></mc:AlternateContent></r>"#
|
||||
);
|
||||
let mut d = crate::xml::parse(x.as_bytes()).unwrap();
|
||||
resolve_mc(&mut d.root, 0);
|
||||
let kids: Vec<&El> = d.root.elements().collect();
|
||||
assert_eq!(kids.len(), 2000);
|
||||
assert_eq!(kids[7].attrs, vec![("xmlns".to_string(), "urn:d".to_string()), ("xmlns:n7".to_string(), "urn:n7".to_string())]);
|
||||
}
|
||||
|
||||
/// The branch's own declaration of a prefix wins over the `AlternateContent`'s; an oversized
|
||||
/// one still shadows it (its content stays unbound); a child's own declaration is left alone.
|
||||
#[test]
|
||||
fn mc_branch_declarations_keep_their_precedence() {
|
||||
let long = "u".repeat(MAX_NS_VALUE + 1);
|
||||
let x = format!(
|
||||
r#"<r xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"><mc:AlternateContent xmlns:p="urn:ac" xmlns:q="urn:q-short"><mc:Fallback xmlns:p="urn:b" xmlns:q="{long}"><p:x/><q:y/><p:z xmlns:p="urn:own"/></mc:Fallback></mc:AlternateContent></r>"#
|
||||
);
|
||||
let mut d = crate::xml::parse(x.as_bytes()).unwrap();
|
||||
resolve_mc(&mut d.root, 0);
|
||||
let kids: Vec<&El> = d.root.elements().collect();
|
||||
let attrs = |i: usize| kids[i].attrs.iter().map(|(k, v)| format!("{k}={v}")).collect::<Vec<_>>();
|
||||
assert_eq!(attrs(0), ["xmlns:p=urn:b"]);
|
||||
assert!(attrs(1).is_empty(), "{:?}", attrs(1));
|
||||
assert_eq!(attrs(2), ["xmlns:p=urn:own"]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn mc_branches_keep_prefixes_named_by_must_understand() {
|
||||
let x = r#"<r xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"><mc:AlternateContent><mc:Fallback xmlns:q="urn:q"><a mc:MustUnderstand="q"/></mc:Fallback></mc:AlternateContent></r>"#;
|
||||
let mut d = crate::xml::parse(x.as_bytes()).unwrap();
|
||||
resolve_mc(&mut d.root, 0);
|
||||
let kid = d.root.elements().next().unwrap();
|
||||
assert!(kid.attrs.iter().any(|(k, v)| k == "xmlns:q" && v == "urn:q"), "{:?}", kid.attrs);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn mc_branches_keep_prefixes_used_in_qname_values() {
|
||||
let x = r#"<r xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"><mc:AlternateContent><mc:Fallback xmlns:q="urn:q" xmlns:z="urn:z"><n xmlns:xsi="urn:xsi" xsi:type="q:Item"/></mc:Fallback></mc:AlternateContent></r>"#;
|
||||
let mut d = crate::xml::parse(x.as_bytes()).unwrap();
|
||||
resolve_mc(&mut d.root, 0);
|
||||
let kid = d.root.elements().next().unwrap();
|
||||
assert!(kid.attrs.iter().any(|(k, v)| k == "xmlns:q" && v == "urn:q"), "{:?}", kid.attrs);
|
||||
assert!(!kid.attrs.iter().any(|(k, _)| k == "xmlns:z"), "unused declarations stay out: {:?}", kid.attrs);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn kept_fragments_skip_oversized_namespace_names() {
|
||||
let big = "u".repeat(MAX_NS_VALUE + 1);
|
||||
let ns = vec![("xmlns:a".to_string(), "urn:a".to_string()), ("xmlns:b".to_string(), big)];
|
||||
let d = crate::xml::parse(br#"<a:x xmlns:a="urn:a" xmlns:b="urn:b"><b:y/></a:x>"#).unwrap();
|
||||
let mut el = d.root.clone();
|
||||
el.attrs.clear();
|
||||
let out = keep_xml(&el, &ns);
|
||||
assert!(out.contains(r#"xmlns:a="urn:a""#), "{out}");
|
||||
assert!(!out.contains("uuuu"), "the oversized declaration is not copied");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn copied_namespace_bytes_are_budgeted_per_part() {
|
||||
let d = crate::xml::parse(br#"<a:x xmlns:a="urn:a"><a:y/></a:x>"#).unwrap();
|
||||
let mut el = d.root.clone();
|
||||
el.attrs.clear();
|
||||
let part = Part {
|
||||
name: String::new(),
|
||||
rels: Default::default(),
|
||||
ns: vec![("xmlns:a".to_string(), "urn:a".to_string())],
|
||||
inner_ns: vec![],
|
||||
ns_budget: Cell::new(15),
|
||||
};
|
||||
assert!(part.keep(&el).contains(r#"xmlns:a="urn:a""#), "within budget");
|
||||
assert!(part.ns_budget.get() < 15);
|
||||
let again = part.keep(&el);
|
||||
assert!(!again.contains("xmlns:a"), "budget spent, no further declarations: {again}");
|
||||
}
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user