Compare commits

..
Author SHA1 Message Date
Dylan M. Taylor ac3e46b748 Metafile: read WMF, EMF and EMF+ with craft-metafile; delete crates/metafile
The renderer now draws what the shared reader adds: pen caps, joins and
dashes, clip paths (SELECTCLIPPATH) and turned or skewed bitmaps.
2026-11-12 01:09:45 +00:00
FredApps 2127bd1bd8 CI: nightly release dry run (#2052)
release.yml also runs nightly on main with craft-common-actions' dry-run: every release build
except macOS, with nothing signed, pushed or drafted. Breakage that only the release build hits
(riscv64, Flatpak, FreeBSD, packaging) then shows up the next morning instead of on release
day, as it did on 11 Nov 2026.
2026-11-11 23:03:54 +01:00
Dylan M. Taylor eec4f59535 AV1: decode tiles serially when parallel tile metadata copies would be huge (#1841) 2026-11-11 17:00:12 -05:00
Dylan M. Taylorandaiongg 988a492097 PPTX: copy only used, bounded namespace declarations onto imported fragments (#1740)
* pptx: copy only used, bounded namespace declarations onto imported fragments

* address review: index a branch's namespace declarations once, look up each child's prefixes

* address review: oversized declarations keep shadowing; keep MustUnderstand prefixes

* address review: keep declarations for prefixes used in QName attribute values

* Apply review fixes

- crates/pptx/src/read.rs

* address review: keep 400-part cap inclusive for rels part

* address review: budget the namespace bytes copied onto kept fragments per part

* fix test initializer: Part gained ns_budget

---------

Co-authored-by: aiongg <67543387+aiongg@users.noreply.github.com>
2026-11-11 16:15:23 -05:00
Dylan M. Taylorandautocraft-bot[bot] <354215646+autocraft-bot[bot]@users.noreply.github.com> 2406ce46ec chore: bump version to 0.18.1 for the Flatpak update fix (#2048)
Co-authored-by: autocraft-bot[bot] <354215646+autocraft-bot[bot]@users.noreply.github.com>
2026-11-11 15:25:10 -05:00
Dylan M. Taylor 60f91f7806 Video decoders: budget repeated-frame and queued output pictures, drop a failed VP9 sample's outputs (#1778)
* Video decoders: budget repeated-frame and queued output pictures, drop a failed VP9 sample's outputs

* address review: size AV1 repeats by the reference format and after layer selection, budget HEVC queued frames at their own size

* address review: charge the larger of the output queue and the in-flight reservation, not both

* address review: count frames held by the old DPB at their own size in the budget check

* address review: charge queued frames outside the DPB and the in-flight set separately
2026-11-11 15:13:36 -05:00
Dylan M. Taylor 2ef5f31bc8 PPTX export: stop adding chart series once a group's caches pass a point budget (#1804)
* PPTX export: stop adding chart series once a group's caches pass a point budget

* address review: size chart exports from the caches each group writes

* address review: cut each chart data cache at a cap far above Excel's 32,000 points per series, so the first series is bounded without failing export
2026-11-11 15:13:23 -05:00
Dylan M. Taylorandautocraft-bot[bot] <354215646+autocraft-bot[bot]@users.noreply.github.com> c6846f6a75 Photo album: apply the album limits when reopening an album (#1755)
* engine: apply the photo album limits when reopening an album

* address review: refuse an album Update when the editor seed omitted items, instead of deleting them

* Apply review fixes

- crates/engine/src/cmd/insert/album.rs

* address review: refuse an album replace when a slide past the item limit holds other content

* address review: notes and comments on a shapeless slide past the item limit block an album replace

* address review: slide background, transition, animations, colour map and tags count as slide content

---------

Co-authored-by: autocraft-bot[bot] <354215646+autocraft-bot[bot]@users.noreply.github.com>
2026-11-11 14:59:47 -05:00
Dylan M. Taylorandautocraft-bot[bot] <354215646+autocraft-bot[bot]@users.noreply.github.com> a3f2f215b0 Metafile: stop shading on a full canvas, charge nested pictures and clip regions to the budgets (#1777)
* Metafile: stop shading on a full canvas, charge nested pictures and clip regions to the budgets

* address review: count shared tiles once, include text advances, check clip region before keeping an item

* address review: count EMF+ object bytes against the canvas byte budget

* Apply review fixes

- crates/metafile/src/canvas.rs
- crates/metafile/src/tests.rs

* Apply review fixes

- crates/metafile/src/canvas.rs

* address review: count pieces of a continued EMF+ object against the shared byte budget

* Apply review fixes

- crates/metafile/src/canvas.rs

* address review: count the current and saved clips against the shared byte budget

---------

Co-authored-by: autocraft-bot[bot] <354215646+autocraft-bot[bot]@users.noreply.github.com>
2026-11-11 14:59:32 -05:00
Dylan M. Taylor 35166b3841 Media: Opus, Ogg, MP4 and Matroska come from craft-libs (#2047)
DeckCraft kept its own Opus decoder (crates/opus) and Ogg demuxer
(crates/ogg), and two crates that only re-exported craft-media
(crates/isobmff, crates/matroska). craft-libs has all four: craft-audio's
opus module and craft-media's ogg, isobmff and matroska modules. The four
crates are removed and crates/media depends on craft-audio and craft-media
directly.

craft-media and craft-audio are pinned at 113f2360, the head of craft-libs
#810, which carries the two Ogg fixes DeckCraft made to its own copy
(51d19baa, 9d36b593). The pin moves to craft-libs main once #810 merges.
craft-pdf, craft-update and craft-ui keep their pins.

One API difference: craft-media's OpusTiming::of saturates on a hostile
granule position instead of returning an error. The end-trim product in
the Ogg Opus decode now saturates as well; a test with a three-channel
file and the largest granule panicked on it without that.

crates/bitstream stays: it also holds the video decoders' memory budget,
which craft-libs does not have.
2026-11-11 14:47:18 -05:00
Dylan M. Taylor b57b4ccfcf PPTX: charge repeated chart decodes and sidecar copies to presentation-wide budgets (#1790)
* PPTX: charge repeated chart decodes and sidecar copies to presentation-wide budgets

* address review: count all chart vectors and relationship strings; reject a chart that crosses the presentation limit

* address review: charge chart part names/content types, roll back rejected capture

* address review: bound chart decode work across rejected and repeated references, cache refused sidecar captures

* address review: keep a chart past the presentation chart limits as its original part and closure, so a save writes it back
2026-11-11 14:33:58 -05:00
Dylan M. Taylor 2fbdcfe5f5 UI: cap live video decoders and bound slide-show grid thumbnail height (#1801)
* UI: cap live video decoders and bound slide-show grid thumbnail height

* address review: retry video feed creation when playback resumes

* address review: reclaim the decoder of ended videos when the feed cap is reached

* test: cover the video decoder cap and its retry

* address review: seeking a video without a decoder retries the bounded decoder allocation
2026-11-11 14:33:44 -05:00
Dylan M. Taylorandautocraft-bot[bot] <354215646+autocraft-bot[bot]@users.noreply.github.com> 98eac5590d Render: bound bevel edge work, shape-gradient flattening and colour-glyph fills (#1786)
* Render: bound bevel edge work, shape-gradient flattening and colour-glyph fills

* address review: skip shape gradient fill when the outline is over the point limit

* address review: keep every contour of a compound bevel or shape gradient at a coarser raster, bound cubic subdivision before flattening

* Apply review fixes

- crates/render/src/pathgrad.rs

* Apply review fixes

- crates/render/src/pathgrad.rs

---------

Co-authored-by: autocraft-bot[bot] <354215646+autocraft-bot[bot]@users.noreply.github.com>
2026-11-11 14:33:29 -05:00
Dylan M. Taylor 46712d0293 Fonts: honor the disabled system fallback for symbol and script family loads (#1840)
* Fonts: honor the disabled system fallback for symbol and script family loads

* address review: no catalog loading for loaded families when system fallback is off

* test: cascades load no installed family when system fallback is off
2026-11-11 14:33:15 -05:00
Dylan M. Taylor e03c22c445 Notes: place bullets in one pass over the galley rows (#1833)
* Notes: place bullets in one pass over the galley rows

* address review: keep zero-character leading rows in row_span_at
2026-11-11 14:33:01 -05:00
Dylan M. Taylorandaiongg 98208a9eda PDF: gather a paragraph's text once, keep note line widths, count handout rules (#1773)
* pdf: gather a paragraph's text once, keep note line widths, count handout rules

* pdf: spell the rule-count check without a negated comparison

* address review: use a narrow tall page in the bounded-work test

* address review: require the huge-paper export to succeed

* Apply review fixes

- crates/pdf/src/lib.rs

---------

Co-authored-by: aiongg <67543387+aiongg@users.noreply.github.com>
2026-11-11 14:32:47 -05:00
Dylan M. Taylor de52b6b766 MCP: bound a batch's entries and the bytes of results it gathers (#1759)
* mcp: bound a batch's entries and the bytes of results it gathers

* address review: charge the escaped batch reply against the budget

* address review: charge batch error entries against the byte budget

* address review: truncate id in batch limit entry

* address review: count the pretty-printed batch reply against the byte cap
2026-11-11 14:32:33 -05:00
Dylan M. Taylor 7c83fa43ac Spelling: check the automation write roots before appending to the user dictionary (#1754)
* engine: check the automation write roots before appending to the user dictionary

* address review: check the original Path for the dictionary write; isolate the confinement test
2026-11-11 14:32:18 -05:00
Dylan M. Taylor 7fe979aa24 Fix Flatpak permissions for update checks and self-updates (#2045)
* fix(flatpak): allow release checks and self-update helpers

* test: keep Flatpak permission parsing within strict Clippy rules
2026-11-11 14:18:59 -05:00
neilcawse 690171665a pptx: preserve SmartArt node text when dgm:cxnLst has no parent-child connections (#1944)
* pptx: preserve SmartArt node text when dgm:cxnLst has no parent-child connections

* fix(pptx): apply unconnected-point SmartArt outline fallback when type="doc" point is absent

* fix(pptx): restrict SmartArt pt_order fallback to empty parOf graphs and preserve cached Picture/Connector children

* fix(pptx): convert default rect geometry to line for connectors in diagram drawings

* docs(pptx): note flat ptLst order fallback on dgm::outline
2026-11-11 18:48:47 +00:00
Dylan M. Taylor 203fa79211 Merge pull request #2041 from storytold/chore/craft-update-6e03f65
Updates: take craft-update from craft-libs main at 6e03f65bf
2026-11-11 12:43:51 -05:00
Dylan M. Taylor 3e2e73aab7 Updates: take craft-update from craft-libs main at 6e03f65bf 2026-11-11 12:43:42 -05:00
Dylan M. Taylor cf0d7fbb14 ci: run Harden-Runner on every job (#2039)
Adds step-security/harden-runner (audit, pinned to v2.22.4 like
craft-common-actions) as the first step of each job that did not have it,
so every workflow in the org's public repos reports its egress to
StepSecurity. Audit, not block: block-mode allow lists come from each
job's observed traffic.
2026-11-11 12:10:27 -05:00
FredApps 121c59d521 Release: DeckCraft v0.18.0 (#2033) 2026-11-11 16:35:45 +01:00
Dylan M. Taylor a55457ee53 Spelling: stop an affix header's count at the end of the file, share aliased dictionaries (#1794)
* Spelling: stop an affix header's count at the end of the file, share aliased dictionaries

* address review: share the first inserted dictionary on a concurrent load
2026-11-11 04:16:05 -05:00
Dylan M. Taylor 7bd141c1c9 xtask: match attribution globs with a table, not backtracking (#1797) 2026-11-11 04:07:19 -05:00
Dylan M. Taylorandaiongg e3e218a8fc Shape edits: edit each target once and read its box without copying it (#1769)
* engine: edit each target shape once and read its box without copying it

* engine: check the fill colour field in the repeated-ids test

* Apply review fixes

- crates/engine/src/tests.rs

* address review: shape.lock skips targets inside a targeted group

* address review: dedupe shape.lock targets before walking groups

---------

Co-authored-by: aiongg <67543387+aiongg@users.noreply.github.com>
2026-11-11 04:00:58 -05:00
Dylan M. Taylor c532ae749b Logging: escape control characters and mark continuation lines in log records (#1791) 2026-11-11 03:52:36 -05:00
Dylan M. Taylor e0f3639e94 Undo: count a slide's notes, comments and tags in the memory budget (#1768)
* engine: count a slide's notes, comments and tags in the undo memory budget

* engine: drive the undo-budget test through slide.rename

* address review: size text cost by the Paragraph and Run struct sizes
2026-11-11 03:44:11 -05:00
Dylan M. Taylor eb10e2b9b3 Metafile: bound clipping work across all subpaths of one item (#1738)
* metafile: bound clipping work across all subpaths of one drawn item

* address review: charge fill clipping as polygons grow, one budget per item

* address review: charge actual clip work instead of projected polygon growth

* address review: share one clip-work allowance between fill and stroke

* address review: share one clip-work budget across fill and stroke, give region intersection its own budget
2026-11-11 03:35:40 -05:00
Dylan M. Taylorandaiongg 92a0437ab6 Media: check an MP4's sample tables where Symphonia will start reading it (#1737)
* media: check an MP4 for oversized sample tables where symphonia will start reading it

* address review: run MP4 table guard only for MP4 or unknown containers

* Apply review fixes

- crates/media/src/probe.rs
- crates/media/src/mp4guard.rs
- crates/media/src/audio.rs

---------

Co-authored-by: aiongg <67543387+aiongg@users.noreply.github.com>
2026-11-11 03:28:15 -05:00
Dylan M. Taylor 4e87f2b850 H.264: bound decoder work from overlapping slices, gaps and queued output (#1706)
* H.264: bound decoder work from overlapping slices, gaps and queued output

- A macroblock that an earlier slice of the same picture already decoded is
  refused, so overlapping slices cannot redo the picture's reconstruction
  once per slice.
- The memory budget check counts pictures waiting in the output queue that
  have left the DPB (one decode call may carry many access units) and the
  DPB's actual size, counting shared buffers once.
- frame_num gap concealment advances the POC state across the skipped run in
  constant time instead of once per missing frame_num.
- Storing a reference picture evicts until the DPB is under capacity, rather
  than evicting one picture and inserting regardless.

* address review: charge pictures already held at their own size in the decoder budget

* address review: keep the h264 output-queue byte total incrementally
2026-11-11 03:17:58 -05:00
Dylan M. Taylorandaiongg 54846750d4 Ink: extend a stroke in place and cap a gesture's samples (#1770)
* engine: extend an ink stroke in place and cap a gesture's samples

* engine: move the ink extension out of drag

* address review: close scribbles at the release point; keep vertices when a capped press adds none

* Apply review fixes

- crates/engine/src/tools/pointer.rs

* raise ink gesture cap to 100000 points

---------

Co-authored-by: aiongg <67543387+aiongg@users.noreply.github.com>
2026-11-10 18:42:24 -05:00
Dylan M. Taylorandaiongg 1e9c392f67 Spelling: remember failed compound tails (#1835)
* Spelling: remember failed compound tails

* Apply review fixes

- crates/engine/src/spell.rs

* Apply review fixes

- crates/engine/src/spell.rs

---------

Co-authored-by: aiongg <67543387+aiongg@users.noreply.github.com>
2026-11-10 17:54:53 -05:00
Dylan M. Taylorandaiongg c3fb184e9d engine: index font names and embedded faces instead of rescanning (#1749)
Co-authored-by: aiongg <67543387+aiongg@users.noreply.github.com>
2026-11-10 17:54:22 -05:00
Dylan M. Taylor 0623b43032 Merge pull request #1985 from dotCooCoo/chore/craft-update-1b34eba41
Updates: take craft-update from craft-libs main at 1b34eba41
2026-11-08 22:55:36 -05:00
dotCooCoo 5ee82b4e45 Updates: take craft-update from craft-libs main at 1b34eba41 2026-11-08 19:43:26 -08:00
Dylan M. Taylor 61423ea35b Merge pull request #1980 from dotCooCoo/chore/craft-update-updater-fixes
Updates: the updater reports installing, ready, installed and failed, and installs at exit
2026-11-08 22:12:17 -05:00
aiongg c4674f24fb Merge pull request #1982 from Silvertree2010/fix/h13151-polynomial-trendline-fit-fails-f
Make polynomial trendline fits independent of x scale and offset
2026-11-08 15:46:38 -05:00
NetterSilverandClaude Opus 5.5 4ba9a1a5d9 Make polynomial trendline fits independent of x scale and offset
Found while testing crates/model/src/trendline.rs:~419 (solve, used by polyfit)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-11-08 21:33:22 +01:00
dotCooCoo 3504a9da42 xtask: pdf-deps expects craft-pdf and krilla from the moved craft-libs pin
The pdf-deps check compared craft-pdf and krilla against storytold/craft-libs
at c5a1cbfb, so the gate stopped at that step once the pin moved to
dotCooCoo/craft-libs c7c84230. The expected repository and revision now
match the pin in Cargo.toml.
2026-11-08 12:11:03 -08:00
dotCooCoo ccab980657 Updates: the updater reports installing, ready, installed and failed, and installs at exit
craft-media, craft-pdf, craft-ui, craft-update and krilla move from craft-libs
c5a1cbfb to c7c84230, which carries storytold/craft-libs#687 and #696.

The check result window now shows whenever the updater's status is not
idle: after Help > Check for Updates, and at the first start after an
update, to say that it installed or why it failed. Restart to Update in
that window asks the app to close once nothing is unsaved. on_exit starts
the helper of a ready update, since on macOS it ends the process before
the app is dropped.
2026-11-08 11:30:36 -08:00
aiongg 48e2203123 Merge pull request #1969 from Silvertree2010/fix/h11403-sentence-case-capitalizes-the-wo
Don't capitalize the word after a decimal point in sentence case
2026-11-08 03:35:42 -05:00
NetterSilverandClaude Haiku 5.5 76bd26cca0 Don't capitalize the word after a decimal point in sentence case
Found while testing crates/model/src/edit.rs:~505 (change_case_parts, sentence branch)

Co-Authored-By: Claude Haiku 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-11-08 08:26:05 +00:00
Nasim 02fbb6a976 Notes: pin that Paste reaches the focused speaker-notes field, bare and inside the ⌘V batch (refs #1855) (#1856) 2026-11-08 01:59:22 -05:00
aiongg c16db2ef3e Merge pull request #1943 from neilcawse/fix/pptx-body-and-group-3d
pptx: preserve 3-D scene and extrusion XML on text bodies and groups
2026-11-08 01:18:16 -05:00
aiongg 809c44654d Apply review fixes
- crates/pptx/src/write/shapes.rs
2026-11-08 05:59:15 +00:00
aiongg 220b52dfdb Apply review fixes
- docs/file-format-parity.md
2026-11-08 05:43:41 +00:00
Neil Cawse 424f575ca5 fix(pptx): bound and validate body/group raw3d XML on read/write and expand 3-D tests 2026-11-08 05:26:33 +00:00
Neil Cawse aefd54d28e pptx: preserve 3-D scene and extrusion XML on text bodies and groups 2026-11-08 05:24:59 +00:00
Nasim 25809faf1a Accessibility: name ribbon drop-downs by purpose and expose the shown text as their value (#1956) (#1963) 2026-11-08 00:18:14 -05:00
aiongg fc35ad67c6 Merge pull request #1950 from Silvertree2010/fix/h9583-format-paragraph-silently-left-a
Fix format.paragraph silently left-aligning known align names
2026-11-07 15:10:55 -05:00
NetterSilverandClaude Haiku 5.5 57780f9e61 Fix format.paragraph silently left-aligning known align names
Found while testing crates/engine/src/cmd/format/paragraph.rs:123-129

Co-Authored-By: Claude Haiku 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-11-07 20:00:33 +00:00
FredApps 0627fb2589 Merge pull request #1932 from helenkwok/web/page-api
deckcraft-web: window.deckcraft, the page command API from craft-web-api
2026-11-07 15:37:54 +01:00
aiongg 170c5591b5 Merge pull request #1934 from Silvertree2010/fix/h7742-shape-adjust-with-an-invalid-neg
Reject invalid index in shape.adjust and shape.quickStyle
2026-11-07 03:34:42 -05:00
NetterSilverandClaude Haiku 5.5 a883d6b2d9 Reject invalid index in shape.adjust and shape.quickStyle
Found while testing crates/engine/src/cmd/shape/appearance.rs:8 (adjust, index via usize_param); same pattern in quick_style at :559

Co-Authored-By: Claude Haiku 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-11-07 08:14:47 +00:00
Helen Kwok 4b1e50a6b0 deckcraft-web: put each Cargo.toml comment above its own dependency, and say a call the app already took still finishes after a timeout 2026-11-07 10:28:04 +08:00
Helen Kwok c7919102ac deckcraft-web: window.deckcraft, the page command API from craft-web-api
The web shell queues the page's calls as control requests (the channel the desktop control server uses) and answers them on the next frame. The API is installed in every mode; opening, saving, the file name and the unsaved flag stay with the host messages.
2026-11-07 08:05:09 +08:00
FredApps 159ed07eb3 Merge pull request #1918 from storytold/house-rules
Copy the shared house rules into AGENTS.md
2026-11-06 15:00:00 +01:00
133 changed files with 2721 additions and 15206 deletions
+4
View File
@@ -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:
+4
View File
@@ -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
+5
View File
@@ -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
View File
@@ -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
View File
@@ -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" }
+3 -2
View File
@@ -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
View File
@@ -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 |
+3 -1
View File
@@ -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 }
+151
View File
@@ -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");
}
}
+2
View File
@@ -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")]
+1
View File
@@ -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 }))
}),
)
+20 -3
View File
@@ -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]
+3
View File
@@ -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
+55 -5
View File
@@ -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 {
+4
View File
@@ -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.
+7
View File
@@ -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 {
+11 -7
View File
@@ -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;
+3 -9
View File
@@ -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")),
+144 -6
View File
@@ -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() }));
}
}
+8
View File
@@ -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();
+20 -1
View File
@@ -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);
+3 -3
View File
@@ -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());
+1 -1
View File
@@ -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();
+26 -14
View File
@@ -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}"));
}
}
}
+11 -7
View File
@@ -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);
}
};
+25 -3
View File
@@ -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>();
+74 -8
View File
@@ -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};
+124
View File
@@ -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(&notes);
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();
}
+3
View File
@@ -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.
+6 -2
View File
@@ -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 });
+22 -6
View File
@@ -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;
+4 -1
View File
@@ -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);
+25 -2
View File
@@ -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");
}
}
+14 -2
View File
@@ -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);
+8
View File
@@ -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());
}
+1 -1
View File
@@ -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
View File
@@ -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
View File
@@ -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)]);
+8
View File
@@ -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);
+8 -4
View File
@@ -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;
+64 -1
View File
@@ -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);
}
+28 -2
View File
@@ -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
);
-13
View File
@@ -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"] }
-6
View File
@@ -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::*;
-13
View File
@@ -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"] }
-6
View File
@@ -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::*;
+27 -1
View File
@@ -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
View File
@@ -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)),
+70
View File
@@ -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(&notes.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"));
}
+2
View File
@@ -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;
+3 -4
View File
@@ -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 }
+12 -9
View File
@@ -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();
+49
View File
@@ -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");
}
}
+10 -9
View File
@@ -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),
+9 -9
View File
@@ -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)))),
+46
View File
@@ -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");
-14
View File
@@ -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 }
-59
View File
@@ -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
-193
View File
@@ -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![]));
}
}
-339
View File
@@ -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 }))
}
-259
View File
@@ -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);
}
}
-478
View File
@@ -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
-118
View File
@@ -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
}
-306
View File
@@ -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
}
-195
View File
@@ -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
-203
View File
@@ -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
};
-383
View File
@@ -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(())
}
+5
View File
@@ -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()
};
+13 -1
View File
@@ -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"), ["Οδοσ", "ας"]);
+1
View File
@@ -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
}
+6
View File
@@ -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);
}
}
}
+71 -6
View File
@@ -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(&centred, 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(&centred, 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];
-12
View File
@@ -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]
-581
View File
@@ -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;
-307
View File
@@ -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(_))));
}
-12
View File
@@ -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]
-678
View File
@@ -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
-171
View File
@@ -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}");
}
}
}
}
-205
View File
@@ -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}");
}
}
-584
View File
@@ -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[..]);
}
}
-130
View File
@@ -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];
-477
View File
@@ -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];
}
}
}
-196
View File
@@ -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);
}
}
-46
View File
@@ -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>;
-198
View File
@@ -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(&regular), &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());
}
}
-340
View File
@@ -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);
}
}
-497
View File
@@ -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();
}
}
-912
View File
@@ -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(&LTP_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(&LTP_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
}
}
-377
View File
@@ -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);
}
}
-205
View File
@@ -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");
}
}
}
-540
View File
@@ -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
View File
@@ -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);
}
+14
View File
@@ -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
View File
@@ -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());
}
}
+7
View File
@@ -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
View File
@@ -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