mirror of
https://github.com/storytold/deckcraft.git
synced 2026-10-29 04:56:58 -06:00
Compare commits
1
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ad4d746131 |
@@ -21,27 +21,3 @@ jobs:
|
||||
lint:
|
||||
# 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/packaging-lint.yml@main
|
||||
|
||||
windows-install-policy:
|
||||
timeout-minutes: 10
|
||||
name: Windows installation policy (no app build)
|
||||
runs-on: windows-latest
|
||||
steps:
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
with:
|
||||
persist-credentials: false
|
||||
- name: Install the pinned packaging toolchain
|
||||
shell: pwsh
|
||||
run: |
|
||||
$tools = Join-Path $env:RUNNER_TEMP 'craft-install-policy-wix'
|
||||
dotnet tool install --tool-path $tools wix --version 5.0.2
|
||||
if ($LASTEXITCODE -ne 0) { throw 'WiX installation failed.' }
|
||||
$tools >> $env:GITHUB_PATH
|
||||
$policy = Get-Content -Raw -LiteralPath packaging/windows/install-policy.json | ConvertFrom-Json
|
||||
foreach ($extension in @($policy.extensions)) {
|
||||
& (Join-Path $tools 'wix.exe') extension add -g "WixToolset.$extension.wixext/5.0.2"
|
||||
if ($LASTEXITCODE -ne 0) { throw "WiX extension $extension installation failed." }
|
||||
}
|
||||
- name: Build and check non-executable installer fixtures
|
||||
shell: pwsh
|
||||
run: ./packaging/windows/test-install-policy.ps1 -OutputDirectory (Join-Path $env:RUNNER_TEMP 'craft-install-policy-tests')
|
||||
|
||||
@@ -24,10 +24,8 @@ permissions:
|
||||
|
||||
jobs:
|
||||
windows-arm64:
|
||||
# Immutable supporting workflow revision until the new install contract lands upstream.
|
||||
uses: storytold/craft-common-actions/.github/workflows/windows-arm64.yml@8bb4e5ab95443171104ad88113fe1ef9b0e5ce3f
|
||||
# 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/windows-arm64.yml@main
|
||||
with:
|
||||
app-name: deckcraft
|
||||
app-dir: DeckCraft
|
||||
install-relative-path: Programs/ArtCraft/DeckCraft
|
||||
install-scope: perUser
|
||||
|
||||
@@ -3,10 +3,6 @@
|
||||
Read [AGENTS.md](AGENTS.md) first: its rules (asset policy, clean room, never crash) bind you and
|
||||
win over anything here.
|
||||
|
||||
New workspace crates must include `[lints] workspace = true` in `Cargo.toml` to inherit
|
||||
`[workspace.lints]`. Production crate roots must declare the required crate-level lint attributes
|
||||
before modules or other crate items; crate-level `//!` documentation may precede them.
|
||||
|
||||
## Start every session
|
||||
1. Read `plan/STATUS.md`, then the current milestone in `plan/execution-plan.md`.
|
||||
2. Read `../../craftrules/AGENTS.md` and the standards it lists for your task.
|
||||
|
||||
Generated
+32
-253
@@ -642,15 +642,6 @@ dependencies = [
|
||||
"error-code",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "cobs"
|
||||
version = "0.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0fa961b519f0b462e3a3b4a34b64d119eeaca1d59af726fe450bbba07a9fc0a1"
|
||||
dependencies = [
|
||||
"thiserror 2.0.21",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "codespan-reporting"
|
||||
version = "0.13.1"
|
||||
@@ -820,38 +811,10 @@ dependencies = [
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "craft-fonts-build"
|
||||
version = "0.1.0"
|
||||
source = "git+https://github.com/storytold/craft-libs?rev=92d392cff132eaf53e13afff77f38b5c11b73aa0#92d392cff132eaf53e13afff77f38b5c11b73aa0"
|
||||
|
||||
[[package]]
|
||||
name = "craft-i18n"
|
||||
version = "0.1.0"
|
||||
source = "git+https://github.com/storytold/craft-libs?rev=92d392cff132eaf53e13afff77f38b5c11b73aa0#92d392cff132eaf53e13afff77f38b5c11b73aa0"
|
||||
dependencies = [
|
||||
"fixed_decimal",
|
||||
"icu_decimal",
|
||||
"icu_locale_core",
|
||||
"icu_locale_fallback",
|
||||
"icu_normalizer",
|
||||
"icu_plurals",
|
||||
"icu_properties",
|
||||
"icu_provider",
|
||||
"icu_provider_adapters",
|
||||
"icu_provider_blob",
|
||||
"log",
|
||||
"objc2-foundation 0.3.2",
|
||||
"sys-locale",
|
||||
"unicode-bidi",
|
||||
"windows-registry",
|
||||
"writeable",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "craft-media"
|
||||
version = "0.1.0"
|
||||
source = "git+https://github.com/storytold/craft-libs?rev=92d392cff132eaf53e13afff77f38b5c11b73aa0#92d392cff132eaf53e13afff77f38b5c11b73aa0"
|
||||
source = "git+https://github.com/storytold/craft-libs?rev=f0ad6c1e1d297908915f4be315b0fcfc9be7b5d4#f0ad6c1e1d297908915f4be315b0fcfc9be7b5d4"
|
||||
dependencies = [
|
||||
"thiserror 2.0.21",
|
||||
]
|
||||
@@ -859,36 +822,16 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "craft-pdf"
|
||||
version = "0.1.0"
|
||||
source = "git+https://github.com/storytold/craft-libs?rev=92d392cff132eaf53e13afff77f38b5c11b73aa0#92d392cff132eaf53e13afff77f38b5c11b73aa0"
|
||||
source = "git+https://github.com/storytold/craft-libs?rev=9f75d3dc19b4b514c4f3d4847ed1ec1b0e86b4ef#9f75d3dc19b4b514c4f3d4847ed1ec1b0e86b4ef"
|
||||
dependencies = [
|
||||
"krilla",
|
||||
"thiserror 2.0.21",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "craft-ui"
|
||||
version = "0.1.0"
|
||||
source = "git+https://github.com/storytold/craft-libs?rev=92d392cff132eaf53e13afff77f38b5c11b73aa0#92d392cff132eaf53e13afff77f38b5c11b73aa0"
|
||||
dependencies = [
|
||||
"craft-fonts-build",
|
||||
"craft-i18n",
|
||||
"egui",
|
||||
"epaint",
|
||||
"js-sys",
|
||||
"log",
|
||||
"objc2 0.6.5",
|
||||
"objc2-app-kit 0.3.2",
|
||||
"objc2-foundation 0.3.2",
|
||||
"skrifa 0.44.0",
|
||||
"wasm-bindgen",
|
||||
"wasm-bindgen-futures",
|
||||
"web-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "craft-update"
|
||||
version = "0.1.0"
|
||||
source = "git+https://github.com/storytold/craft-libs?rev=92d392cff132eaf53e13afff77f38b5c11b73aa0#92d392cff132eaf53e13afff77f38b5c11b73aa0"
|
||||
source = "git+https://github.com/storytold/craft-libs?rev=495e5971bc024aa96e6ed318cb6e7cd3a0103184#495e5971bc024aa96e6ed318cb6e7cd3a0103184"
|
||||
dependencies = [
|
||||
"chrono",
|
||||
"dunce",
|
||||
@@ -983,7 +926,7 @@ checksum = "0c87e182de0887fd5361989c677c4e8f5000cd9491d6d563161a8f3a5519fc7f"
|
||||
|
||||
[[package]]
|
||||
name = "deckcraft"
|
||||
version = "0.12.0"
|
||||
version = "0.11.1"
|
||||
dependencies = [
|
||||
"arboard",
|
||||
"chrono",
|
||||
@@ -1005,7 +948,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "deckcraft-anim"
|
||||
version = "0.12.0"
|
||||
version = "0.11.1"
|
||||
dependencies = [
|
||||
"deckcraft-geom",
|
||||
"deckcraft-model",
|
||||
@@ -1014,7 +957,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "deckcraft-av1"
|
||||
version = "0.12.0"
|
||||
version = "0.11.1"
|
||||
dependencies = [
|
||||
"deckcraft-bitstream",
|
||||
"rayon",
|
||||
@@ -1023,14 +966,14 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "deckcraft-bitstream"
|
||||
version = "0.12.0"
|
||||
version = "0.11.1"
|
||||
dependencies = [
|
||||
"craft-media",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "deckcraft-cli"
|
||||
version = "0.12.0"
|
||||
version = "0.11.1"
|
||||
dependencies = [
|
||||
"deckcraft-engine",
|
||||
"deckcraft-mcp",
|
||||
@@ -1040,14 +983,14 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "deckcraft-color"
|
||||
version = "0.12.0"
|
||||
version = "0.11.1"
|
||||
dependencies = [
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "deckcraft-engine"
|
||||
version = "0.12.0"
|
||||
version = "0.11.1"
|
||||
dependencies = [
|
||||
"chrono",
|
||||
"deckcraft-anim",
|
||||
@@ -1073,7 +1016,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "deckcraft-fonts"
|
||||
version = "0.12.0"
|
||||
version = "0.11.1"
|
||||
dependencies = [
|
||||
"epaint_default_fonts",
|
||||
"harfrust 0.13.3",
|
||||
@@ -1085,7 +1028,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "deckcraft-format"
|
||||
version = "0.12.0"
|
||||
version = "0.11.1"
|
||||
dependencies = [
|
||||
"deckcraft-model",
|
||||
"serde",
|
||||
@@ -1096,7 +1039,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "deckcraft-geom"
|
||||
version = "0.12.0"
|
||||
version = "0.11.1"
|
||||
dependencies = [
|
||||
"kurbo",
|
||||
"serde",
|
||||
@@ -1104,7 +1047,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "deckcraft-h264"
|
||||
version = "0.12.0"
|
||||
version = "0.11.1"
|
||||
dependencies = [
|
||||
"deckcraft-bitstream",
|
||||
"rayon",
|
||||
@@ -1113,7 +1056,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "deckcraft-hevc"
|
||||
version = "0.12.0"
|
||||
version = "0.11.1"
|
||||
dependencies = [
|
||||
"deckcraft-bitstream",
|
||||
"rayon",
|
||||
@@ -1122,21 +1065,21 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "deckcraft-isobmff"
|
||||
version = "0.12.0"
|
||||
version = "0.11.1"
|
||||
dependencies = [
|
||||
"craft-media",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "deckcraft-matroska"
|
||||
version = "0.12.0"
|
||||
version = "0.11.1"
|
||||
dependencies = [
|
||||
"craft-media",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "deckcraft-mcp"
|
||||
version = "0.12.0"
|
||||
version = "0.11.1"
|
||||
dependencies = [
|
||||
"deckcraft-engine",
|
||||
"deckcraft-render",
|
||||
@@ -1146,7 +1089,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "deckcraft-media"
|
||||
version = "0.12.0"
|
||||
version = "0.11.1"
|
||||
dependencies = [
|
||||
"deckcraft-av1",
|
||||
"deckcraft-h264",
|
||||
@@ -1164,7 +1107,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "deckcraft-metafile"
|
||||
version = "0.12.0"
|
||||
version = "0.11.1"
|
||||
dependencies = [
|
||||
"image",
|
||||
"kurbo",
|
||||
@@ -1173,7 +1116,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "deckcraft-model"
|
||||
version = "0.12.0"
|
||||
version = "0.11.1"
|
||||
dependencies = [
|
||||
"deckcraft-color",
|
||||
"deckcraft-geom",
|
||||
@@ -1185,15 +1128,15 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "deckcraft-ogg"
|
||||
version = "0.12.0"
|
||||
version = "0.11.1"
|
||||
|
||||
[[package]]
|
||||
name = "deckcraft-opus"
|
||||
version = "0.12.0"
|
||||
version = "0.11.1"
|
||||
|
||||
[[package]]
|
||||
name = "deckcraft-pdf"
|
||||
version = "0.12.0"
|
||||
version = "0.11.1"
|
||||
dependencies = [
|
||||
"craft-pdf",
|
||||
"deckcraft-fonts",
|
||||
@@ -1211,7 +1154,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "deckcraft-pptx"
|
||||
version = "0.12.0"
|
||||
version = "0.11.1"
|
||||
dependencies = [
|
||||
"deckcraft-color",
|
||||
"deckcraft-engine",
|
||||
@@ -1227,7 +1170,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "deckcraft-render"
|
||||
version = "0.12.0"
|
||||
version = "0.11.1"
|
||||
dependencies = [
|
||||
"deckcraft-anim",
|
||||
"deckcraft-color",
|
||||
@@ -1246,7 +1189,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "deckcraft-text"
|
||||
version = "0.12.0"
|
||||
version = "0.11.1"
|
||||
dependencies = [
|
||||
"chrono",
|
||||
"deckcraft-color",
|
||||
@@ -1260,9 +1203,8 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "deckcraft-ui-egui"
|
||||
version = "0.12.0"
|
||||
version = "0.11.1"
|
||||
dependencies = [
|
||||
"craft-ui",
|
||||
"craft-update",
|
||||
"deckcraft-anim",
|
||||
"deckcraft-color",
|
||||
@@ -1285,7 +1227,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "deckcraft-vp9"
|
||||
version = "0.12.0"
|
||||
version = "0.11.1"
|
||||
dependencies = [
|
||||
"deckcraft-bitstream",
|
||||
"rayon",
|
||||
@@ -1294,7 +1236,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "deckcraft-web"
|
||||
version = "0.12.0"
|
||||
version = "0.11.1"
|
||||
dependencies = [
|
||||
"deckcraft-engine",
|
||||
"deckcraft-ui-egui",
|
||||
@@ -1738,17 +1680,6 @@ version = "0.1.14"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "aedcfb3409746eddb02b9e19ebda1c3394f759a152e48ee875a0844d1b955484"
|
||||
|
||||
[[package]]
|
||||
name = "fixed_decimal"
|
||||
version = "0.7.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2214fb00f669ee96bee5bb29dfb1ddabe058b2bc426817efc79afa03c9bc55ae"
|
||||
dependencies = [
|
||||
"displaydoc",
|
||||
"smallvec",
|
||||
"writeable",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "fixedbitset"
|
||||
version = "0.5.7"
|
||||
@@ -2319,23 +2250,6 @@ dependencies = [
|
||||
"zerovec",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "icu_decimal"
|
||||
version = "2.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9eb8f655bba2d0c0459e43a5b0e6cd3cd388109898fb3d85d048165e8b0abd08"
|
||||
dependencies = [
|
||||
"displaydoc",
|
||||
"fixed_decimal",
|
||||
"icu_locale_core",
|
||||
"icu_pattern",
|
||||
"icu_plurals",
|
||||
"icu_provider",
|
||||
"serde",
|
||||
"writeable",
|
||||
"zerovec",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "icu_locale_core"
|
||||
version = "2.3.0"
|
||||
@@ -2344,32 +2258,11 @@ checksum = "d56e28588da92eee5c3201a6eff33fabdd49b62269c8938d4ff050ce4d900deb"
|
||||
dependencies = [
|
||||
"displaydoc",
|
||||
"litemap",
|
||||
"serde",
|
||||
"tinystr",
|
||||
"writeable",
|
||||
"zerovec",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "icu_locale_fallback"
|
||||
version = "2.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "251af8e57c9400e3eb58242fe5b8b1152b2a64fdf4cf632f923c38ccee6f2fa9"
|
||||
dependencies = [
|
||||
"icu_locale_core",
|
||||
"icu_locale_fallback_data",
|
||||
"icu_provider",
|
||||
"potential_utf",
|
||||
"tinystr",
|
||||
"zerovec",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "icu_locale_fallback_data"
|
||||
version = "2.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "decf2a22ec8fa68f1a0c1129a3f8583f8f8bc24e8b9ccbe98ead99f62a4dc3a8"
|
||||
|
||||
[[package]]
|
||||
name = "icu_normalizer"
|
||||
version = "2.3.0"
|
||||
@@ -2381,9 +2274,6 @@ dependencies = [
|
||||
"icu_properties",
|
||||
"icu_provider",
|
||||
"smallvec",
|
||||
"utf16_iter",
|
||||
"utf8_iter",
|
||||
"write16",
|
||||
"zerovec",
|
||||
]
|
||||
|
||||
@@ -2393,32 +2283,6 @@ version = "2.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1563da1ed3e0b3bf3d74c9b85917ac9c56464d2f57242270c09c9e752f8021a0"
|
||||
|
||||
[[package]]
|
||||
name = "icu_pattern"
|
||||
version = "0.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2b3db611446bd92c246b03e4aa0bafa71ddb194af431ddbb42b9d70e4bfce1a0"
|
||||
dependencies = [
|
||||
"displaydoc",
|
||||
"either",
|
||||
"serde",
|
||||
"writeable",
|
||||
"zerovec",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "icu_plurals"
|
||||
version = "2.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e475e6766ef87b1d3c1f97be6363995b0bfaeddbc789671615df598a97ff0593"
|
||||
dependencies = [
|
||||
"displaydoc",
|
||||
"fixed_decimal",
|
||||
"icu_provider",
|
||||
"serde",
|
||||
"zerovec",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "icu_properties"
|
||||
version = "2.3.0"
|
||||
@@ -2448,9 +2312,6 @@ checksum = "d27bbb9d3abbefac45d55f647c9de1d44aafcd1186eb91879afef17c396c3e73"
|
||||
dependencies = [
|
||||
"displaydoc",
|
||||
"icu_locale_core",
|
||||
"postcard",
|
||||
"serde",
|
||||
"stable_deref_trait",
|
||||
"writeable",
|
||||
"yoke",
|
||||
"zerofrom",
|
||||
@@ -2458,30 +2319,6 @@ dependencies = [
|
||||
"zerovec",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "icu_provider_adapters"
|
||||
version = "2.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "df887bc4f55dbc773c7c45a0c7dfa75be886f7b7812830a71ab7271cc1801820"
|
||||
dependencies = [
|
||||
"icu_locale_fallback",
|
||||
"icu_provider",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "icu_provider_blob"
|
||||
version = "2.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5916b44fac319d3f58e4d8d805a627df19e27937b582cd99fcde3bb6b7ce1b0e"
|
||||
dependencies = [
|
||||
"icu_provider",
|
||||
"postcard",
|
||||
"serde",
|
||||
"writeable",
|
||||
"zerotrie",
|
||||
"zerovec",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "idna"
|
||||
version = "1.1.0"
|
||||
@@ -2707,7 +2544,7 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "krilla"
|
||||
version = "0.8.2"
|
||||
source = "git+https://github.com/storytold/craft-libs?rev=92d392cff132eaf53e13afff77f38b5c11b73aa0#92d392cff132eaf53e13afff77f38b5c11b73aa0"
|
||||
source = "git+https://github.com/storytold/craft-libs?rev=9f75d3dc19b4b514c4f3d4847ed1ec1b0e86b4ef#9f75d3dc19b4b514c4f3d4847ed1ec1b0e86b4ef"
|
||||
dependencies = [
|
||||
"base64 0.22.1",
|
||||
"bumpalo",
|
||||
@@ -2808,9 +2645,6 @@ name = "litemap"
|
||||
version = "0.8.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "47d9d19d1d6efa0109d2f65ff4c85cddd50bd572e5a00127ab10987290bcefae"
|
||||
dependencies = [
|
||||
"serde_core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "litrs"
|
||||
@@ -3728,24 +3562,12 @@ dependencies = [
|
||||
"portable-atomic",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "postcard"
|
||||
version = "1.1.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6764c3b5dd454e283a30e6dfe78e9b31096d9e32036b5d1eaac7a6119ccb9a24"
|
||||
dependencies = [
|
||||
"cobs",
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "potential_utf"
|
||||
version = "0.1.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d83eb9bc6d8e5cf568e7a1101d60ee05e81ed50ea106026f3d18deeb046d7661"
|
||||
dependencies = [
|
||||
"serde_core",
|
||||
"writeable",
|
||||
"zerovec",
|
||||
]
|
||||
|
||||
@@ -4702,18 +4524,6 @@ dependencies = [
|
||||
"syn 3.0.6",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "sys-locale"
|
||||
version = "0.3.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8eab9a99a024a169fe8a903cf9d4a3b3601109bcc13bd9e3c6fff259138626c4"
|
||||
dependencies = [
|
||||
"js-sys",
|
||||
"libc",
|
||||
"wasm-bindgen",
|
||||
"web-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tar"
|
||||
version = "0.4.46"
|
||||
@@ -4857,7 +4667,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b1e27c91459209c2986af3dcf603a5a74a4368754ce37414f59acc971167f643"
|
||||
dependencies = [
|
||||
"displaydoc",
|
||||
"serde_core",
|
||||
"zerovec",
|
||||
]
|
||||
|
||||
@@ -5111,12 +4920,6 @@ dependencies = [
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "utf16_iter"
|
||||
version = "1.0.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c8232dd3cdaed5356e0f716d285e4b40b932ac434100fe9b7e0e8e935b9e6246"
|
||||
|
||||
[[package]]
|
||||
name = "utf8-zero"
|
||||
version = "0.8.1"
|
||||
@@ -5764,17 +5567,6 @@ dependencies = [
|
||||
"windows-link",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-registry"
|
||||
version = "0.6.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "02752bf7fbdcce7f2a27a742f798510f3e5ad88dbe84871e5168e2120c3d5720"
|
||||
dependencies = [
|
||||
"windows-link",
|
||||
"windows-result 0.4.1",
|
||||
"windows-strings",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-result"
|
||||
version = "0.1.2"
|
||||
@@ -6090,20 +5882,11 @@ dependencies = [
|
||||
"read-fonts 0.39.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "write16"
|
||||
version = "1.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d1890f4022759daae28ed4fe62859b1236caebfc61ede2f63ed4e695f3f6d936"
|
||||
|
||||
[[package]]
|
||||
name = "writeable"
|
||||
version = "0.6.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3ad82d2a33cdc9674dc7465672f271e096168fcdbe0f799d9e6db8c5892679dc"
|
||||
dependencies = [
|
||||
"either",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "x11-dl"
|
||||
@@ -6176,7 +5959,7 @@ checksum = "9440ea3e5aeabb0ac63af70daf835274065238cdd0cec83418f417eae38bacee"
|
||||
|
||||
[[package]]
|
||||
name = "xtask"
|
||||
version = "0.12.0"
|
||||
version = "0.11.1"
|
||||
dependencies = [
|
||||
"serde_json",
|
||||
]
|
||||
@@ -6364,11 +6147,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4ea269c3bd32f0a32c321907a2ae912ba6f4649bb0fc764a15627e99a7095a3f"
|
||||
dependencies = [
|
||||
"displaydoc",
|
||||
"litemap",
|
||||
"serde_core",
|
||||
"yoke",
|
||||
"zerofrom",
|
||||
"zerovec",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -6377,7 +6157,6 @@ version = "0.11.8"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bb0464e17806c1d976d5cba29399c7f08e516e279e2ba493f63123b5fca67dd8"
|
||||
dependencies = [
|
||||
"serde",
|
||||
"yoke",
|
||||
"zerofrom",
|
||||
"zerovec-derive",
|
||||
|
||||
+5
-6
@@ -4,7 +4,7 @@ members = ["crates/*", "apps/*", "xtask"]
|
||||
default-members = ["crates/*", "apps/deckcraft", "apps/deckcraft-cli"]
|
||||
|
||||
[workspace.package]
|
||||
version = "0.12.0"
|
||||
version = "0.11.1"
|
||||
edition = "2024"
|
||||
license = "MIT OR Apache-2.0"
|
||||
rust-version = "1.95"
|
||||
@@ -59,10 +59,9 @@ 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 = "92d392cff132eaf53e13afff77f38b5c11b73aa0", version = "=0.1.0", default-features = false }
|
||||
craft-pdf = { git = "https://github.com/storytold/craft-libs", rev = "92d392cff132eaf53e13afff77f38b5c11b73aa0", version = "=0.1.0", default-features = false, features = ["write", "write-simple-text"] }
|
||||
craft-update = { git = "https://github.com/storytold/craft-libs", version = "=0.1.0", rev = "92d392cff132eaf53e13afff77f38b5c11b73aa0", features = ["egui"] }
|
||||
craft-ui = { git = "https://github.com/storytold/craft-libs", rev = "92d392cff132eaf53e13afff77f38b5c11b73aa0", version = "=0.1.0" }
|
||||
craft-media = { git = "https://github.com/storytold/craft-libs", rev = "f0ad6c1e1d297908915f4be315b0fcfc9be7b5d4", version = "=0.1.0", default-features = false }
|
||||
craft-pdf = { git = "https://github.com/storytold/craft-libs", rev = "9f75d3dc19b4b514c4f3d4847ed1ec1b0e86b4ef", version = "=0.1.0", default-features = false, features = ["write", "write-simple-text"] }
|
||||
craft-update = { git = "https://github.com/storytold/craft-libs", version = "=0.1.0", rev = "495e5971bc024aa96e6ed318cb6e7cd3a0103184", features = ["egui"] }
|
||||
i_overlay = "9"
|
||||
quick-xml = "0.41"
|
||||
zip = { version = "3", default-features = false, features = ["deflate"] }
|
||||
@@ -108,7 +107,7 @@ codegen-units = 16
|
||||
debug = 0
|
||||
|
||||
[patch.crates-io]
|
||||
krilla = { git = "https://github.com/storytold/craft-libs", rev = "92d392cff132eaf53e13afff77f38b5c11b73aa0", version = "=0.8.2" }
|
||||
krilla = { git = "https://github.com/storytold/craft-libs", rev = "9f75d3dc19b4b514c4f3d4847ed1ec1b0e86b4ef", 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" }
|
||||
|
||||
@@ -73,7 +73,7 @@ fn main() -> ExitCode {
|
||||
Some("app") => report(app(rest)),
|
||||
Some("mcp") => report(mcp(rest)),
|
||||
Some("links") => {
|
||||
use deckcraft_engine::links::{APP_PAGE, DISCORD, GITHUB, WEBSITE};
|
||||
use deckcraft_engine::links::*;
|
||||
outln!("Discord {DISCORD}\nWebsite {WEBSITE}\nApp page {APP_PAGE}\nGitHub {GITHUB}");
|
||||
ExitCode::SUCCESS
|
||||
}
|
||||
@@ -208,7 +208,7 @@ fn bench(args: &[String]) -> Result<(), String> {
|
||||
("save .deckcraft", time_ms(runs, || save_bytes(&doc, "deckcraft").map(|_| ()).map_err(e))?),
|
||||
("save .pptx", time_ms(runs, || save_bytes(&doc, "pptx").map(|_| ()).map_err(e))?),
|
||||
("open .pptx", time_ms(runs, || open_presentation("bench.pptx", &pptx).map(|_| ()).map_err(e))?),
|
||||
("export .pdf", time_ms(runs, || save_bytes(&doc, "pdf").map(|_| ()).map_err(e))?),
|
||||
("export .pdf", time_ms(runs.min(3), || save_bytes(&doc, "pdf").map(|_| ()).map_err(e))?),
|
||||
];
|
||||
let render_total: f64 = per_slide.iter().map(|m| m.0).sum();
|
||||
let render_max = per_slide.iter().map(|m| m.1).fold(0.0, f64::max);
|
||||
|
||||
@@ -2,7 +2,4 @@ allow-unwrap-in-tests = true
|
||||
allow-expect-in-tests = true
|
||||
allow-panic-in-tests = true
|
||||
allow-indexing-slicing-in-tests = true
|
||||
disallowed-types = [
|
||||
{ path = "egui::ComboBox", reason = "use craft_ui::ComboBox: egui's combo box menu can't grow back while open (egui#5225)" },
|
||||
]
|
||||
cognitive-complexity-threshold = 30
|
||||
|
||||
+178
-218
@@ -429,38 +429,6 @@ pub struct FrameHeader {
|
||||
pub spatial_id: u32,
|
||||
}
|
||||
|
||||
/// mark_ref_frames( idLen ): invalidates reference slots whose frame id is too far from the current one.
|
||||
fn mark_ref_frames(st: &mut HeaderState, cur: u32, id_len: u32) {
|
||||
let diff_len = st.seq.delta_frame_id_length_minus_2 + 2;
|
||||
for rs in st.refs.iter_mut() {
|
||||
if cur > (1 << diff_len) {
|
||||
if rs.frame_id > cur || rs.frame_id < cur - (1 << diff_len) {
|
||||
rs.valid = false;
|
||||
}
|
||||
} else if rs.frame_id > cur && rs.frame_id < (1 << id_len) + cur - (1 << diff_len) {
|
||||
rs.valid = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Skips buffer_removal_time for every operating point that includes this layer.
|
||||
fn skip_buffer_removal_times(r: &mut BitReader, seq: &SequenceHeader, temporal_id: u32, spatial_id: u32) -> Result<()> {
|
||||
if !(seq.decoder_model_info_present && r.flag()?) {
|
||||
return Ok(());
|
||||
}
|
||||
for op in 0..=seq.operating_points_cnt_minus_1 {
|
||||
if seq.decoder_model_present_for_this_op[op] {
|
||||
let idc = seq.operating_point_idc[op];
|
||||
let in_t = (idc >> temporal_id) & 1;
|
||||
let in_s = (idc >> (spatial_id + 8)) & 1;
|
||||
if idc == 0 || (in_t == 1 && in_s == 1) {
|
||||
r.f(seq.buffer_removal_time_length_minus_1 + 1)?;
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
impl FrameHeader {
|
||||
pub fn get_relative_dist(&self, seq: &SequenceHeader, a: u32, b: u32) -> i32 {
|
||||
relative_dist(seq, a, b)
|
||||
@@ -526,10 +494,35 @@ impl FrameHeader {
|
||||
} else {
|
||||
h.show_existing_frame = r.flag()?;
|
||||
if h.show_existing_frame {
|
||||
h.parse_show_existing_frame(r, st, id_len)?;
|
||||
h.frame_to_show_map_idx = r.f(3)? as usize;
|
||||
if seq.decoder_model_info_present && !seq.equal_picture_interval {
|
||||
r.f(seq.frame_presentation_time_length_minus_1 + 1)?;
|
||||
}
|
||||
if seq.frame_id_numbers_present {
|
||||
r.f(id_len)?;
|
||||
}
|
||||
let rs = &st.refs[h.frame_to_show_map_idx];
|
||||
if !rs.valid {
|
||||
return Err(Error::Invalid("show_existing_frame of an empty slot"));
|
||||
}
|
||||
h.frame_type = rs.frame_type;
|
||||
if h.frame_type == KEY_FRAME as u8 {
|
||||
h.refresh_frame_flags = all_frames;
|
||||
}
|
||||
if seq.film_grain_params_present {
|
||||
h.film_grain = rs.grain.clone();
|
||||
}
|
||||
return Ok(h);
|
||||
}
|
||||
h.parse_frame_type_and_flags(r, seq)?;
|
||||
h.frame_type = r.f(2)? as u8;
|
||||
h.frame_is_intra = h.frame_type == INTRA_ONLY_FRAME as u8 || h.frame_type == KEY_FRAME as u8;
|
||||
h.show_frame = r.flag()?;
|
||||
if h.show_frame && seq.decoder_model_info_present && !seq.equal_picture_interval {
|
||||
r.f(seq.frame_presentation_time_length_minus_1 + 1)?;
|
||||
}
|
||||
h.showable_frame = if h.show_frame { h.frame_type != KEY_FRAME as u8 } else { r.flag()? };
|
||||
h.error_resilient_mode =
|
||||
if h.frame_type == SWITCH_FRAME as u8 || (h.frame_type == KEY_FRAME as u8 && h.show_frame) { true } else { r.flag()? };
|
||||
}
|
||||
if h.frame_type == KEY_FRAME as u8 && h.show_frame {
|
||||
for rs in st.refs.iter_mut() {
|
||||
@@ -540,11 +533,33 @@ impl FrameHeader {
|
||||
h.order_hints[LAST_FRAME + i] = 0;
|
||||
}
|
||||
}
|
||||
h.parse_screen_content_flags(r, seq)?;
|
||||
h.disable_cdf_update = r.flag()?;
|
||||
h.allow_screen_content_tools = if seq.seq_force_screen_content_tools == SELECT_SCREEN_CONTENT_TOOLS as u32 {
|
||||
r.flag()?
|
||||
} else {
|
||||
seq.seq_force_screen_content_tools != 0
|
||||
};
|
||||
if h.allow_screen_content_tools {
|
||||
h.force_integer_mv = if seq.seq_force_integer_mv == SELECT_INTEGER_MV as u32 { r.flag()? } else { seq.seq_force_integer_mv != 0 };
|
||||
}
|
||||
if h.frame_is_intra {
|
||||
h.force_integer_mv = true;
|
||||
}
|
||||
if seq.frame_id_numbers_present {
|
||||
h.current_frame_id = r.f(id_len)?;
|
||||
mark_ref_frames(st, h.current_frame_id, id_len);
|
||||
st.current_frame_id = h.current_frame_id;
|
||||
// mark_ref_frames( idLen )
|
||||
let diff_len = seq.delta_frame_id_length_minus_2 + 2;
|
||||
let cur = h.current_frame_id;
|
||||
for rs in st.refs.iter_mut() {
|
||||
if cur > (1 << diff_len) {
|
||||
if rs.frame_id > cur || rs.frame_id < cur - (1 << diff_len) {
|
||||
rs.valid = false;
|
||||
}
|
||||
} else if rs.frame_id > cur && rs.frame_id < (1 << id_len) + cur - (1 << diff_len) {
|
||||
rs.valid = false;
|
||||
}
|
||||
}
|
||||
st.current_frame_id = cur;
|
||||
}
|
||||
h.frame_size_override_flag = if h.frame_type == SWITCH_FRAME as u8 {
|
||||
true
|
||||
@@ -555,7 +570,18 @@ impl FrameHeader {
|
||||
};
|
||||
h.order_hint = r.f(seq.order_hint_bits)?;
|
||||
h.primary_ref_frame = if h.frame_is_intra || h.error_resilient_mode { PRIMARY_REF_NONE } else { r.f(3)? as usize };
|
||||
skip_buffer_removal_times(r, seq, temporal_id, spatial_id)?;
|
||||
if seq.decoder_model_info_present && r.flag()? {
|
||||
for op in 0..=seq.operating_points_cnt_minus_1 {
|
||||
if seq.decoder_model_present_for_this_op[op] {
|
||||
let idc = seq.operating_point_idc[op];
|
||||
let in_t = (idc >> temporal_id) & 1;
|
||||
let in_s = (idc >> (spatial_id + 8)) & 1;
|
||||
if idc == 0 || (in_t == 1 && in_s == 1) {
|
||||
r.f(seq.buffer_removal_time_length_minus_1 + 1)?;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
h.refresh_frame_flags =
|
||||
if h.frame_type == SWITCH_FRAME as u8 || (h.frame_type == KEY_FRAME as u8 && h.show_frame) { all_frames } else { r.f(8)? };
|
||||
if (!h.frame_is_intra || h.refresh_frame_flags != all_frames) && h.error_resilient_mode && seq.enable_order_hint {
|
||||
@@ -573,16 +599,126 @@ impl FrameHeader {
|
||||
h.allow_intrabc = r.flag()?;
|
||||
}
|
||||
} else {
|
||||
h.parse_inter_frame_refs(r, st)?;
|
||||
let mut short = false;
|
||||
if seq.enable_order_hint {
|
||||
short = r.flag()?;
|
||||
if short {
|
||||
let last = r.f(3)? as usize;
|
||||
let gold = r.f(3)? as usize;
|
||||
h.set_frame_refs(seq, st.refs, last, gold);
|
||||
}
|
||||
}
|
||||
for i in 0..REFS_PER_FRAME {
|
||||
if !short {
|
||||
h.ref_frame_idx[i] = r.f(3)? as usize;
|
||||
}
|
||||
if seq.frame_id_numbers_present {
|
||||
r.f(seq.delta_frame_id_length_minus_2 + 2)?;
|
||||
}
|
||||
}
|
||||
for i in 0..REFS_PER_FRAME {
|
||||
if !st.refs[h.ref_frame_idx[i]].valid {
|
||||
return Err(Error::Invalid("reference to an empty slot"));
|
||||
}
|
||||
}
|
||||
if h.frame_size_override_flag && !h.error_resilient_mode {
|
||||
// frame_size_with_refs( )
|
||||
let mut found = false;
|
||||
for i in 0..REFS_PER_FRAME {
|
||||
if r.flag()? {
|
||||
let rs = &st.refs[h.ref_frame_idx[i]];
|
||||
h.upscaled_width = rs.upscaled_width;
|
||||
h.frame_width = h.upscaled_width;
|
||||
h.frame_height = rs.frame_height;
|
||||
h.render_width = rs.render_width;
|
||||
h.render_height = rs.render_height;
|
||||
found = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
h.frame_size(r, seq)?;
|
||||
h.render_size(r)?;
|
||||
} else {
|
||||
h.superres_params(r, seq)?;
|
||||
h.compute_image_size();
|
||||
}
|
||||
} else {
|
||||
h.frame_size(r, seq)?;
|
||||
h.render_size(r)?;
|
||||
}
|
||||
h.allow_high_precision_mv = if h.force_integer_mv { false } else { r.flag()? };
|
||||
h.interpolation_filter = if r.flag()? { SWITCHABLE } else { r.f(2)? as u8 };
|
||||
h.is_motion_mode_switchable = r.flag()?;
|
||||
h.use_ref_frame_mvs = if h.error_resilient_mode || !seq.enable_ref_frame_mvs { false } else { r.flag()? };
|
||||
for i in 0..REFS_PER_FRAME {
|
||||
let rf = LAST_FRAME + i;
|
||||
let hint = st.refs[h.ref_frame_idx[i]].order_hint;
|
||||
h.order_hints[rf] = hint;
|
||||
h.ref_frame_sign_bias[rf] = seq.enable_order_hint && relative_dist(seq, hint, h.order_hint) > 0;
|
||||
}
|
||||
}
|
||||
h.disable_frame_end_update_cdf = if seq.reduced_still_picture_header || h.disable_cdf_update { true } else { r.flag()? };
|
||||
h.setup_or_load_previous(st);
|
||||
if h.primary_ref_frame == PRIMARY_REF_NONE {
|
||||
// setup_past_independence( )
|
||||
st.seg_features = SegmentationFeatures::default();
|
||||
st.prev_gm_params = default_gm_params();
|
||||
st.lf_deltas = LoopFilterDeltas::defaults();
|
||||
h.lf.delta_enabled = true;
|
||||
} else {
|
||||
// load_previous( )
|
||||
let prev = &st.refs[h.ref_frame_idx[h.primary_ref_frame]];
|
||||
st.prev_gm_params = prev.gm_params;
|
||||
st.lf_deltas = prev.lf_deltas;
|
||||
st.seg_features = prev.seg_features;
|
||||
}
|
||||
h.prev_gm_params = st.prev_gm_params;
|
||||
h.tile_info(r, seq)?;
|
||||
h.quantization_params(r, seq)?;
|
||||
h.segmentation_params(r, st)?;
|
||||
h.delta_q_lf_params(r)?;
|
||||
h.compute_lossless();
|
||||
// delta_q_params( )
|
||||
if h.quant.base_q_idx > 0 {
|
||||
h.delta_q_present = r.flag()?;
|
||||
}
|
||||
if h.delta_q_present {
|
||||
h.delta_q_res = r.f(2)?;
|
||||
}
|
||||
// delta_lf_params( )
|
||||
if h.delta_q_present {
|
||||
if !h.allow_intrabc {
|
||||
h.delta_lf_present = r.flag()?;
|
||||
}
|
||||
if h.delta_lf_present {
|
||||
h.delta_lf_res = r.f(2)?;
|
||||
h.delta_lf_multi = r.flag()?;
|
||||
}
|
||||
}
|
||||
h.coded_lossless = true;
|
||||
for seg_id in 0..MAX_SEGMENTS {
|
||||
let q = h.get_qindex(true, seg_id, 0);
|
||||
let ll = q == 0
|
||||
&& h.quant.delta_q_y_dc == 0
|
||||
&& h.quant.delta_q_u_ac == 0
|
||||
&& h.quant.delta_q_u_dc == 0
|
||||
&& h.quant.delta_q_v_ac == 0
|
||||
&& h.quant.delta_q_v_dc == 0;
|
||||
h.lossless_array[seg_id] = ll;
|
||||
if !ll {
|
||||
h.coded_lossless = false;
|
||||
}
|
||||
if h.quant.using_qmatrix {
|
||||
if ll {
|
||||
h.seg_qm_level[0][seg_id] = 15;
|
||||
h.seg_qm_level[1][seg_id] = 15;
|
||||
h.seg_qm_level[2][seg_id] = 15;
|
||||
} else {
|
||||
h.seg_qm_level[0][seg_id] = h.quant.qm_y;
|
||||
h.seg_qm_level[1][seg_id] = h.quant.qm_u;
|
||||
h.seg_qm_level[2][seg_id] = h.quant.qm_v;
|
||||
}
|
||||
}
|
||||
}
|
||||
h.all_lossless = h.coded_lossless && h.frame_width == h.upscaled_width;
|
||||
h.loop_filter_params(r, seq, st)?;
|
||||
h.cdef_params(r, seq)?;
|
||||
h.lr_params(r, seq)?;
|
||||
@@ -603,182 +739,6 @@ impl FrameHeader {
|
||||
Ok(h)
|
||||
}
|
||||
|
||||
/// The show_existing_frame branch: picks the slot to show and inherits its frame type and grain.
|
||||
fn parse_show_existing_frame(&mut self, r: &mut BitReader, st: &HeaderState, id_len: u32) -> Result<()> {
|
||||
let seq = st.seq;
|
||||
self.frame_to_show_map_idx = r.f(3)? as usize;
|
||||
if seq.decoder_model_info_present && !seq.equal_picture_interval {
|
||||
r.f(seq.frame_presentation_time_length_minus_1 + 1)?;
|
||||
}
|
||||
if seq.frame_id_numbers_present {
|
||||
r.f(id_len)?;
|
||||
}
|
||||
let rs = &st.refs[self.frame_to_show_map_idx];
|
||||
if !rs.valid {
|
||||
return Err(Error::Invalid("show_existing_frame of an empty slot"));
|
||||
}
|
||||
self.frame_type = rs.frame_type;
|
||||
if self.frame_type == KEY_FRAME as u8 {
|
||||
self.refresh_frame_flags = (1u32 << NUM_REF_FRAMES) - 1;
|
||||
}
|
||||
if seq.film_grain_params_present {
|
||||
self.film_grain = rs.grain.clone();
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// frame_type, show_frame, showable_frame and error_resilient_mode.
|
||||
fn parse_frame_type_and_flags(&mut self, r: &mut BitReader, seq: &SequenceHeader) -> Result<()> {
|
||||
self.frame_type = r.f(2)? as u8;
|
||||
self.frame_is_intra = self.frame_type == INTRA_ONLY_FRAME as u8 || self.frame_type == KEY_FRAME as u8;
|
||||
self.show_frame = r.flag()?;
|
||||
if self.show_frame && seq.decoder_model_info_present && !seq.equal_picture_interval {
|
||||
r.f(seq.frame_presentation_time_length_minus_1 + 1)?;
|
||||
}
|
||||
self.showable_frame = if self.show_frame { self.frame_type != KEY_FRAME as u8 } else { r.flag()? };
|
||||
self.error_resilient_mode =
|
||||
if self.frame_type == SWITCH_FRAME as u8 || (self.frame_type == KEY_FRAME as u8 && self.show_frame) { true } else { r.flag()? };
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// disable_cdf_update, allow_screen_content_tools and force_integer_mv.
|
||||
fn parse_screen_content_flags(&mut self, r: &mut BitReader, seq: &SequenceHeader) -> Result<()> {
|
||||
self.disable_cdf_update = r.flag()?;
|
||||
self.allow_screen_content_tools = if seq.seq_force_screen_content_tools == SELECT_SCREEN_CONTENT_TOOLS as u32 {
|
||||
r.flag()?
|
||||
} else {
|
||||
seq.seq_force_screen_content_tools != 0
|
||||
};
|
||||
if self.allow_screen_content_tools {
|
||||
self.force_integer_mv = if seq.seq_force_integer_mv == SELECT_INTEGER_MV as u32 { r.flag()? } else { seq.seq_force_integer_mv != 0 };
|
||||
}
|
||||
if self.frame_is_intra {
|
||||
self.force_integer_mv = true;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Reference selection, frame size, motion vector precision and filter flags of an inter frame.
|
||||
fn parse_inter_frame_refs(&mut self, r: &mut BitReader, st: &HeaderState) -> Result<()> {
|
||||
let seq = st.seq;
|
||||
let mut short = false;
|
||||
if seq.enable_order_hint {
|
||||
short = r.flag()?;
|
||||
if short {
|
||||
let last = r.f(3)? as usize;
|
||||
let gold = r.f(3)? as usize;
|
||||
self.set_frame_refs(seq, st.refs, last, gold);
|
||||
}
|
||||
}
|
||||
for i in 0..REFS_PER_FRAME {
|
||||
if !short {
|
||||
self.ref_frame_idx[i] = r.f(3)? as usize;
|
||||
}
|
||||
if seq.frame_id_numbers_present {
|
||||
r.f(seq.delta_frame_id_length_minus_2 + 2)?;
|
||||
}
|
||||
}
|
||||
for i in 0..REFS_PER_FRAME {
|
||||
if !st.refs[self.ref_frame_idx[i]].valid {
|
||||
return Err(Error::Invalid("reference to an empty slot"));
|
||||
}
|
||||
}
|
||||
if self.frame_size_override_flag && !self.error_resilient_mode {
|
||||
self.frame_size_with_refs(r, seq, st.refs)?;
|
||||
} else {
|
||||
self.frame_size(r, seq)?;
|
||||
self.render_size(r)?;
|
||||
}
|
||||
self.allow_high_precision_mv = if self.force_integer_mv { false } else { r.flag()? };
|
||||
self.interpolation_filter = if r.flag()? { SWITCHABLE } else { r.f(2)? as u8 };
|
||||
self.is_motion_mode_switchable = r.flag()?;
|
||||
self.use_ref_frame_mvs = if self.error_resilient_mode || !seq.enable_ref_frame_mvs { false } else { r.flag()? };
|
||||
for i in 0..REFS_PER_FRAME {
|
||||
let rf = LAST_FRAME + i;
|
||||
let hint = st.refs[self.ref_frame_idx[i]].order_hint;
|
||||
self.order_hints[rf] = hint;
|
||||
self.ref_frame_sign_bias[rf] = seq.enable_order_hint && relative_dist(seq, hint, self.order_hint) > 0;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// frame_size_with_refs( ).
|
||||
fn frame_size_with_refs(&mut self, r: &mut BitReader, seq: &SequenceHeader, refs: &[RefInfo; NUM_REF_FRAMES]) -> Result<()> {
|
||||
for i in 0..REFS_PER_FRAME {
|
||||
if r.flag()? {
|
||||
let rs = &refs[self.ref_frame_idx[i]];
|
||||
self.upscaled_width = rs.upscaled_width;
|
||||
self.frame_width = self.upscaled_width;
|
||||
self.frame_height = rs.frame_height;
|
||||
self.render_width = rs.render_width;
|
||||
self.render_height = rs.render_height;
|
||||
self.superres_params(r, seq)?;
|
||||
self.compute_image_size();
|
||||
return Ok(());
|
||||
}
|
||||
}
|
||||
self.frame_size(r, seq)?;
|
||||
self.render_size(r)
|
||||
}
|
||||
|
||||
/// setup_past_independence( ) or load_previous( ), depending on primary_ref_frame.
|
||||
fn setup_or_load_previous(&mut self, st: &mut HeaderState) {
|
||||
if self.primary_ref_frame == PRIMARY_REF_NONE {
|
||||
st.seg_features = SegmentationFeatures::default();
|
||||
st.prev_gm_params = default_gm_params();
|
||||
st.lf_deltas = LoopFilterDeltas::defaults();
|
||||
self.lf.delta_enabled = true;
|
||||
} else {
|
||||
let prev = &st.refs[self.ref_frame_idx[self.primary_ref_frame]];
|
||||
st.prev_gm_params = prev.gm_params;
|
||||
st.lf_deltas = prev.lf_deltas;
|
||||
st.seg_features = prev.seg_features;
|
||||
}
|
||||
}
|
||||
|
||||
/// delta_q_params( ) and delta_lf_params( ).
|
||||
fn delta_q_lf_params(&mut self, r: &mut BitReader) -> Result<()> {
|
||||
if self.quant.base_q_idx > 0 {
|
||||
self.delta_q_present = r.flag()?;
|
||||
}
|
||||
if self.delta_q_present {
|
||||
self.delta_q_res = r.f(2)?;
|
||||
if !self.allow_intrabc {
|
||||
self.delta_lf_present = r.flag()?;
|
||||
}
|
||||
if self.delta_lf_present {
|
||||
self.delta_lf_res = r.f(2)?;
|
||||
self.delta_lf_multi = r.flag()?;
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Per-segment lossless flags, quantizer matrix levels and the frame-wide lossless flags.
|
||||
fn compute_lossless(&mut self) {
|
||||
self.coded_lossless = true;
|
||||
for seg_id in 0..MAX_SEGMENTS {
|
||||
let q = self.get_qindex(true, seg_id, 0);
|
||||
let ll = q == 0
|
||||
&& self.quant.delta_q_y_dc == 0
|
||||
&& self.quant.delta_q_u_ac == 0
|
||||
&& self.quant.delta_q_u_dc == 0
|
||||
&& self.quant.delta_q_v_ac == 0
|
||||
&& self.quant.delta_q_v_dc == 0;
|
||||
self.lossless_array[seg_id] = ll;
|
||||
if !ll {
|
||||
self.coded_lossless = false;
|
||||
}
|
||||
if self.quant.using_qmatrix {
|
||||
let levels = if ll { [15; 3] } else { [self.quant.qm_y, self.quant.qm_u, self.quant.qm_v] };
|
||||
for (plane, level) in levels.into_iter().enumerate() {
|
||||
self.seg_qm_level[plane][seg_id] = level;
|
||||
}
|
||||
}
|
||||
}
|
||||
self.all_lossless = self.coded_lossless && self.frame_width == self.upscaled_width;
|
||||
}
|
||||
|
||||
fn superres_params(&mut self, r: &mut BitReader, seq: &SequenceHeader) -> Result<()> {
|
||||
self.use_superres = if seq.enable_superres { r.flag()? } else { false };
|
||||
self.superres_denom = if self.use_superres { r.f(SUPERRES_DENOM_BITS as u32)? + SUPERRES_DENOM_MIN as u32 } else { SUPERRES_NUM as u32 };
|
||||
|
||||
+47
-60
@@ -420,8 +420,20 @@ impl TileDecoder<'_, '_> {
|
||||
let fh = &self.fs.fh;
|
||||
for ref_list in 0..1 + is_compound as usize {
|
||||
let ref_frame = cand_refs[ref_list];
|
||||
let ym = self.b.y_mode;
|
||||
let global =
|
||||
(ym == GLOBALMV || ym == GLOBAL_GLOBALMV) && ref_frame > INTRA_FRAME as i8 && fh.gm_type[ref_frame as usize] as usize > TRANSLATION;
|
||||
let global_valid = global && setup_shear(&fh.gm_params[ref_frame as usize]).0;
|
||||
let ref_idx: i32 = if self.b.use_intrabc { -1 } else { fh.ref_frame_idx[ref_frame as usize - LAST_FRAME] as i32 };
|
||||
let use_warp = self.warp_mode(ref_frame, ref_idx, w, h);
|
||||
let use_warp = if w < 8 || h < 8 || fh.force_integer_mv {
|
||||
0
|
||||
} else if self.b.motion_mode as usize == LOCALWARP && self.b.local_valid {
|
||||
1
|
||||
} else if global && !self.is_scaled_ref(ref_idx) && global_valid {
|
||||
2
|
||||
} else {
|
||||
0
|
||||
};
|
||||
let mv = [cand_mvs[ref_list].row as i32, cand_mvs[ref_list].col as i32];
|
||||
if use_warp != 0 {
|
||||
let params = if use_warp == 1 { self.b.local_warp_params } else { fh.gm_params[ref_frame as usize] };
|
||||
@@ -436,7 +448,40 @@ impl TileDecoder<'_, '_> {
|
||||
}
|
||||
}
|
||||
let ct = self.b.compound_type;
|
||||
self.fill_blend_mask(plane, w, h, &preds, rnd.2);
|
||||
if ct == COMPOUND_WEDGE && plane == 0 {
|
||||
let m = &wedge_masks()[self.b.mi_size][self.b.wedge_index][self.b.wedge_sign];
|
||||
for i in 0..h {
|
||||
for j in 0..w {
|
||||
self.mask[i * PRED_STRIDE + j] = m[i * w + j] as i32;
|
||||
}
|
||||
}
|
||||
} else if ct == COMPOUND_INTRA {
|
||||
let size_scale = MAX_SB_SIZE / h.max(w);
|
||||
let im = self.b.interintra_mode;
|
||||
for i in 0..h {
|
||||
for j in 0..w {
|
||||
self.mask[i * PRED_STRIDE + j] = if im == II_V_PRED {
|
||||
II_WEIGHTS_1D[i * size_scale] as i32
|
||||
} else if im == II_H_PRED {
|
||||
II_WEIGHTS_1D[j * size_scale] as i32
|
||||
} else if im == II_SMOOTH_PRED {
|
||||
II_WEIGHTS_1D[i.min(j) * size_scale] as i32
|
||||
} else {
|
||||
32
|
||||
};
|
||||
}
|
||||
}
|
||||
} else if ct == COMPOUND_DIFFWTD && plane == 0 {
|
||||
for i in 0..h {
|
||||
for j in 0..w {
|
||||
let k = i * PRED_STRIDE + j;
|
||||
let diff = (preds[0][k] - preds[1][k]).abs();
|
||||
let diff = round2(diff, (bd - 8) + rnd.2);
|
||||
let m = (38 + diff / 16).clamp(0, 64);
|
||||
self.mask[k] = if self.b.mask_type == 1 { 64 - m } else { m };
|
||||
}
|
||||
}
|
||||
}
|
||||
let (fwd, bck) = if ct == COMPOUND_DISTANCE { self.distance_weights(cand_refs) } else { (0, 0) };
|
||||
let max = (1i32 << bd) - 1;
|
||||
let is_inter_intra = self.b.is_inter && self.b.ref_frame[1] == INTRA_FRAME as i8;
|
||||
@@ -476,64 +521,6 @@ impl TileDecoder<'_, '_> {
|
||||
}
|
||||
}
|
||||
|
||||
/// Warp selection for one reference: 0 none, 1 local warp, 2 global warp.
|
||||
fn warp_mode(&self, ref_frame: i8, ref_idx: i32, w: usize, h: usize) -> u8 {
|
||||
let fh = &self.fs.fh;
|
||||
let ym = self.b.y_mode;
|
||||
let global =
|
||||
(ym == GLOBALMV || ym == GLOBAL_GLOBALMV) && ref_frame > INTRA_FRAME as i8 && fh.gm_type[ref_frame as usize] as usize > TRANSLATION;
|
||||
let global_valid = global && setup_shear(&fh.gm_params[ref_frame as usize]).0;
|
||||
if w < 8 || h < 8 || fh.force_integer_mv {
|
||||
0
|
||||
} else if self.b.motion_mode as usize == LOCALWARP && self.b.local_valid {
|
||||
1
|
||||
} else if global && !self.is_scaled_ref(ref_idx) && global_valid {
|
||||
2
|
||||
} else {
|
||||
0
|
||||
}
|
||||
}
|
||||
|
||||
/// Builds the blend mask (wedge, inter-intra or difference-weighted) for the current compound type.
|
||||
fn fill_blend_mask(&mut self, plane: usize, w: usize, h: usize, preds: &[Vec<i32>; 2], round_post: u32) {
|
||||
let bd = self.fs.bit_depth;
|
||||
let ct = self.b.compound_type;
|
||||
if ct == COMPOUND_WEDGE && plane == 0 {
|
||||
let m = &wedge_masks()[self.b.mi_size][self.b.wedge_index][self.b.wedge_sign];
|
||||
for i in 0..h {
|
||||
for j in 0..w {
|
||||
self.mask[i * PRED_STRIDE + j] = m[i * w + j] as i32;
|
||||
}
|
||||
}
|
||||
} else if ct == COMPOUND_INTRA {
|
||||
let size_scale = MAX_SB_SIZE / h.max(w);
|
||||
let im = self.b.interintra_mode;
|
||||
for i in 0..h {
|
||||
for j in 0..w {
|
||||
self.mask[i * PRED_STRIDE + j] = if im == II_V_PRED {
|
||||
II_WEIGHTS_1D[i * size_scale] as i32
|
||||
} else if im == II_H_PRED {
|
||||
II_WEIGHTS_1D[j * size_scale] as i32
|
||||
} else if im == II_SMOOTH_PRED {
|
||||
II_WEIGHTS_1D[i.min(j) * size_scale] as i32
|
||||
} else {
|
||||
32
|
||||
};
|
||||
}
|
||||
}
|
||||
} else if ct == COMPOUND_DIFFWTD && plane == 0 {
|
||||
for i in 0..h {
|
||||
for j in 0..w {
|
||||
let k = i * PRED_STRIDE + j;
|
||||
let diff = (preds[0][k] - preds[1][k]).abs();
|
||||
let diff = round2(diff, (bd - 8) + round_post);
|
||||
let m = (38 + diff / 16).clamp(0, 64);
|
||||
self.mask[k] = if self.b.mask_type == 1 { 64 - m } else { m };
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn is_scaled_ref(&self, ref_idx: i32) -> bool {
|
||||
if ref_idx < 0 {
|
||||
return false;
|
||||
|
||||
+64
-83
@@ -1682,87 +1682,6 @@ impl<'a, 'f> TileDecoder<'a, 'f> {
|
||||
}
|
||||
}
|
||||
|
||||
/// eob_pt, eob_extra and the remaining eob bits.
|
||||
fn read_eob(&mut self, tx_sz: usize, tx_sz_ctx: usize, ptype: usize, tx_class: usize) -> usize {
|
||||
let eob_multisize = (TX_WIDTH_LOG2[tx_sz] as usize).min(5) + (TX_HEIGHT_LOG2[tx_sz] as usize).min(5) - 4;
|
||||
let ectx = (tx_class != TX_CLASS_2D) as usize;
|
||||
let eob_pt = 1 + match eob_multisize {
|
||||
0 => self.sd.read_symbol(&mut self.cdf.eob_pt_16[ptype][ectx]),
|
||||
1 => self.sd.read_symbol(&mut self.cdf.eob_pt_32[ptype][ectx]),
|
||||
2 => self.sd.read_symbol(&mut self.cdf.eob_pt_64[ptype][ectx]),
|
||||
3 => self.sd.read_symbol(&mut self.cdf.eob_pt_128[ptype][ectx]),
|
||||
4 => self.sd.read_symbol(&mut self.cdf.eob_pt_256[ptype][ectx]),
|
||||
5 => self.sd.read_symbol(&mut self.cdf.eob_pt_512[ptype]),
|
||||
_ => self.sd.read_symbol(&mut self.cdf.eob_pt_1024[ptype]),
|
||||
};
|
||||
let mut eob = if eob_pt < 2 { eob_pt } else { (1 << (eob_pt - 2)) + 1 };
|
||||
let eob_shift = eob_pt as i32 - 3;
|
||||
if eob_shift >= 0 {
|
||||
let extra = self.sd.read_symbol(&mut self.cdf.eob_extra[tx_sz_ctx][ptype][eob_pt - 3]);
|
||||
if extra == 1 {
|
||||
eob += 1 << eob_shift;
|
||||
}
|
||||
for i in 1..(eob_pt as i32 - 2).max(0) {
|
||||
let sh = (eob_pt as i32 - 2).max(0) - 1 - i;
|
||||
if self.sd.read_literal(1) == 1 {
|
||||
eob += 1 << sh;
|
||||
}
|
||||
}
|
||||
}
|
||||
eob
|
||||
}
|
||||
|
||||
/// Exp-Golomb coded remainder of a coefficient above the base range (read_golomb).
|
||||
fn read_golomb(&mut self) -> u32 {
|
||||
let mut length = 0;
|
||||
loop {
|
||||
length += 1;
|
||||
if self.sd.read_literal(1) == 1 || length > 32 {
|
||||
break;
|
||||
}
|
||||
}
|
||||
let mut x: u32 = 1;
|
||||
for _ in (0..length - 1).rev() {
|
||||
x = (x << 1) | self.sd.read_literal(1);
|
||||
}
|
||||
x
|
||||
}
|
||||
|
||||
/// Reads the signs and Golomb remainders of the first `scan.len()` (= eob) coefficients, applies
|
||||
/// them to `self.quant`, and returns the culLevel and dcCategory contexts. `blk4` is (x4, y4, w4, h4).
|
||||
fn read_signs_and_golomb(&mut self, plane: usize, blk4: (usize, usize, usize, usize), scan: &[u16]) -> (u32, u8) {
|
||||
let (x4, y4, w4, h4) = blk4;
|
||||
let ptype = (plane > 0) as usize;
|
||||
let mut cul_level = 0u32;
|
||||
let mut dc_category = 0u8;
|
||||
for (c, &p) in scan.iter().enumerate() {
|
||||
let pos = p as usize;
|
||||
let sign = if self.quant[pos] != 0 {
|
||||
if c == 0 {
|
||||
let ctx = self.dc_sign_ctx(plane, x4, y4, w4, h4);
|
||||
self.sd.read_symbol(&mut self.cdf.dc_sign[ptype][ctx]) == 1
|
||||
} else {
|
||||
self.sd.read_literal(1) == 1
|
||||
}
|
||||
} else {
|
||||
false
|
||||
};
|
||||
if self.quant[pos] > (NUM_BASE_LEVELS + COEFF_BASE_RANGE) as i32 {
|
||||
let x = self.read_golomb();
|
||||
self.quant[pos] = (x as i64 + COEFF_BASE_RANGE as i64 + NUM_BASE_LEVELS as i64).min(i32::MAX as i64) as i32;
|
||||
}
|
||||
if pos == 0 && self.quant[pos] > 0 {
|
||||
dc_category = if sign { 1 } else { 2 };
|
||||
}
|
||||
self.quant[pos] &= 0xFFFFF;
|
||||
cul_level += self.quant[pos] as u32;
|
||||
if sign {
|
||||
self.quant[pos] = -self.quant[pos];
|
||||
}
|
||||
}
|
||||
(cul_level.min(63), dc_category)
|
||||
}
|
||||
|
||||
/// coeffs( ): returns eob.
|
||||
fn coeffs(&mut self, plane: usize, start_x: usize, start_y: usize, tx_sz: usize) -> usize {
|
||||
let x4 = start_x >> 2;
|
||||
@@ -1788,8 +1707,32 @@ impl<'a, 'f> TileDecoder<'a, 'f> {
|
||||
}
|
||||
self.plane_tx_type = self.compute_tx_type(plane, tx_sz, x4, y4);
|
||||
let scan = self.get_scan(tx_sz);
|
||||
let eob_multisize = (TX_WIDTH_LOG2[tx_sz] as usize).min(5) + (TX_HEIGHT_LOG2[tx_sz] as usize).min(5) - 4;
|
||||
let tx_class = get_tx_class(self.plane_tx_type);
|
||||
eob = self.read_eob(tx_sz, tx_sz_ctx, ptype, tx_class);
|
||||
let ectx = (tx_class != TX_CLASS_2D) as usize;
|
||||
let eob_pt = 1 + match eob_multisize {
|
||||
0 => self.sd.read_symbol(&mut self.cdf.eob_pt_16[ptype][ectx]),
|
||||
1 => self.sd.read_symbol(&mut self.cdf.eob_pt_32[ptype][ectx]),
|
||||
2 => self.sd.read_symbol(&mut self.cdf.eob_pt_64[ptype][ectx]),
|
||||
3 => self.sd.read_symbol(&mut self.cdf.eob_pt_128[ptype][ectx]),
|
||||
4 => self.sd.read_symbol(&mut self.cdf.eob_pt_256[ptype][ectx]),
|
||||
5 => self.sd.read_symbol(&mut self.cdf.eob_pt_512[ptype]),
|
||||
_ => self.sd.read_symbol(&mut self.cdf.eob_pt_1024[ptype]),
|
||||
};
|
||||
eob = if eob_pt < 2 { eob_pt } else { (1 << (eob_pt - 2)) + 1 };
|
||||
let eob_shift = eob_pt as i32 - 3;
|
||||
if eob_shift >= 0 {
|
||||
let extra = self.sd.read_symbol(&mut self.cdf.eob_extra[tx_sz_ctx][ptype][eob_pt - 3]);
|
||||
if extra == 1 {
|
||||
eob += 1 << eob_shift;
|
||||
}
|
||||
for i in 1..(eob_pt as i32 - 2).max(0) {
|
||||
let sh = (eob_pt as i32 - 2).max(0) - 1 - i;
|
||||
if self.sd.read_literal(1) == 1 {
|
||||
eob += 1 << sh;
|
||||
}
|
||||
}
|
||||
}
|
||||
let adj = ADJUSTED_TX_SIZE[tx_sz] as usize;
|
||||
let bwl = TX_WIDTH_LOG2[adj] as usize;
|
||||
let height = TX_HEIGHT[adj] as usize;
|
||||
@@ -1837,7 +1780,45 @@ impl<'a, 'f> TileDecoder<'a, 'f> {
|
||||
lev[li] = level as u8;
|
||||
}
|
||||
self.levels = lev;
|
||||
(cul_level, dc_category) = self.read_signs_and_golomb(plane, (x4, y4, w4, h4), &scan[..eob]);
|
||||
for c in 0..eob {
|
||||
let pos = scan[c] as usize;
|
||||
let sign = if self.quant[pos] != 0 {
|
||||
if c == 0 {
|
||||
let ctx = self.dc_sign_ctx(plane, x4, y4, w4, h4);
|
||||
self.sd.read_symbol(&mut self.cdf.dc_sign[ptype][ctx]) == 1
|
||||
} else {
|
||||
self.sd.read_literal(1) == 1
|
||||
}
|
||||
} else {
|
||||
false
|
||||
};
|
||||
if self.quant[pos] > (NUM_BASE_LEVELS + COEFF_BASE_RANGE) as i32 {
|
||||
let mut length = 0;
|
||||
loop {
|
||||
length += 1;
|
||||
if self.sd.read_literal(1) == 1 {
|
||||
break;
|
||||
}
|
||||
if length > 32 {
|
||||
break;
|
||||
}
|
||||
}
|
||||
let mut x: u32 = 1;
|
||||
for _ in (0..length - 1).rev() {
|
||||
x = (x << 1) | self.sd.read_literal(1);
|
||||
}
|
||||
self.quant[pos] = (x as i64 + COEFF_BASE_RANGE as i64 + NUM_BASE_LEVELS as i64).min(i32::MAX as i64) as i32;
|
||||
}
|
||||
if pos == 0 && self.quant[pos] > 0 {
|
||||
dc_category = if sign { 1 } else { 2 };
|
||||
}
|
||||
self.quant[pos] &= 0xFFFFF;
|
||||
cul_level += self.quant[pos] as u32;
|
||||
if sign {
|
||||
self.quant[pos] = -self.quant[pos];
|
||||
}
|
||||
}
|
||||
cul_level = cul_level.min(63);
|
||||
}
|
||||
for i in 0..w4 {
|
||||
if x4 + i < self.t.above_level[plane].len() {
|
||||
|
||||
@@ -214,10 +214,6 @@ fn preserve_and_placeholders_edit_the_open_master_or_layout() {
|
||||
s.execute("edit.undo", &json!({})).unwrap();
|
||||
assert!(has_footers(&s));
|
||||
|
||||
master_view_closed_rejects(&mut s, blank);
|
||||
}
|
||||
|
||||
fn master_view_closed_rejects(s: &mut Session, blank: usize) {
|
||||
s.execute("view.closeMaster", &json!({})).unwrap();
|
||||
let n = s.doc().unwrap().history.undo.len();
|
||||
let err = s.execute("master.preserve", &json!({})).unwrap_err();
|
||||
@@ -228,15 +224,11 @@ fn master_view_closed_rejects(s: &mut Session, blank: usize) {
|
||||
assert!(err.to_string().contains("give title or footers"), "{err}");
|
||||
assert_eq!(s.doc().unwrap().history.undo.len(), n);
|
||||
|
||||
layout_placeholders_edit_one_undo_step(s, blank);
|
||||
}
|
||||
|
||||
fn layout_placeholders_edit_one_undo_step(s: &mut Session, blank: usize) {
|
||||
s.execute("view.slideMaster", &json!({"master": 0, "layout": blank})).unwrap();
|
||||
assert_eq!(s.doc().unwrap().selection.target, Target::Layout { master: 0, layout: blank });
|
||||
assert!(s.doc().unwrap().doc.masters[0].layouts[blank].preserve);
|
||||
assert!(!has_title(s));
|
||||
assert!(has_footers(s));
|
||||
assert!(!has_title(&s));
|
||||
assert!(has_footers(&s));
|
||||
|
||||
let n = s.doc().unwrap().history.undo.len();
|
||||
s.execute("master.placeholders", &json!({"title": false, "footers": true})).unwrap();
|
||||
@@ -245,19 +237,19 @@ fn layout_placeholders_edit_one_undo_step(s: &mut Session, blank: usize) {
|
||||
s.execute("master.placeholders", &json!({"title": true, "footers": false})).unwrap();
|
||||
assert_eq!(s.doc().unwrap().history.undo.len(), n + 1, "both keys are one undo step");
|
||||
assert_eq!(s.doc().unwrap().history.undo.last().unwrap().label, "Layout Placeholders");
|
||||
assert!(has_title(s));
|
||||
assert!(!has_footers(s));
|
||||
let title = shapes(s).into_iter().find(|sh| sh.ph_type().is_some_and(PhType::is_title)).unwrap();
|
||||
assert!(has_title(&s));
|
||||
assert!(!has_footers(&s));
|
||||
let title = shapes(&s).into_iter().find(|sh| sh.ph_type().is_some_and(PhType::is_title)).unwrap();
|
||||
assert_eq!(title.name, "Title 1");
|
||||
assert_eq!(title.ph.unwrap().idx, 0);
|
||||
assert!(title.text.unwrap().paragraphs.iter().all(|para| para.runs.is_empty()));
|
||||
s.execute("edit.undo", &json!({})).unwrap();
|
||||
assert!(!has_title(s));
|
||||
assert!(has_footers(s));
|
||||
assert!(!has_title(&s));
|
||||
assert!(has_footers(&s));
|
||||
|
||||
s.execute("master.placeholders", &json!({"footers": false})).unwrap();
|
||||
s.execute("master.placeholders", &json!({"footers": true})).unwrap();
|
||||
let restored = shapes(s);
|
||||
let restored = shapes(&s);
|
||||
assert!(restored.iter().any(|sh| sh.ph_type() == Some(PhType::Date)));
|
||||
assert!(restored.iter().any(|sh| sh.ph_type() == Some(PhType::Footer)));
|
||||
let num = restored.iter().find(|sh| sh.ph_type() == Some(PhType::SlideNum)).unwrap();
|
||||
@@ -272,10 +264,6 @@ fn layout_placeholders_edit_one_undo_step(s: &mut Session, blank: usize) {
|
||||
s.execute("edit.undo", &json!({})).unwrap();
|
||||
assert!(s.doc().unwrap().doc.masters[0].layouts[blank].preserve);
|
||||
|
||||
missing_title_placeholder_is_recreated(s);
|
||||
}
|
||||
|
||||
fn missing_title_placeholder_is_recreated(s: &mut Session) {
|
||||
s.edit(|doc, _| {
|
||||
let m = std::sync::Arc::make_mut(doc.masters.get_mut(0).unwrap());
|
||||
m.shapes.retain(|sh| !sh.ph_type().is_some_and(PhType::is_title));
|
||||
@@ -283,7 +271,7 @@ fn missing_title_placeholder_is_recreated(s: &mut Session) {
|
||||
})
|
||||
.unwrap();
|
||||
s.execute("master.placeholders", &json!({"title": true})).unwrap();
|
||||
let title = shapes(s).into_iter().find(|sh| sh.ph_type() == Some(PhType::Title)).unwrap();
|
||||
let title = shapes(&s).into_iter().find(|sh| sh.ph_type() == Some(PhType::Title)).unwrap();
|
||||
assert_eq!(title.name, "Title");
|
||||
let box_ = title.xfrm.unwrap();
|
||||
assert!((box_.x - 60.0).abs() < 0.01 && (box_.y - 29.0).abs() < 0.01);
|
||||
|
||||
@@ -1,6 +1,4 @@
|
||||
use super::{
|
||||
Arc, Result, Session, Value, bad, builtin_color_schemes, builtin_font_schemes, builtin_format_schemes, json, master_index, ok, str_param,
|
||||
};
|
||||
use super::*;
|
||||
|
||||
pub(super) fn colors(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
let name_v = p.get("name");
|
||||
|
||||
@@ -1,7 +1,4 @@
|
||||
use super::{
|
||||
Arc, Fill, NO_ID_SPACE, PhType, Result, Run, RunKind, Session, Shape, ShapeId, Size, Target, TextBody, Value, bad, bool_param, json,
|
||||
master_index, ok, opt_usize_param, rect_param, str_param, usize_param,
|
||||
};
|
||||
use super::*;
|
||||
|
||||
pub(super) fn master_view(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
let st = s.doc()?;
|
||||
|
||||
@@ -1,7 +1,4 @@
|
||||
use super::{
|
||||
Arc, BTreeSet, Background, Fill, NO_ID_SPACE, Presentation, Result, Session, Target, Value, bad, bool_or, bool_param, color_param, color_value,
|
||||
f64_param, ok, usize_param, with_param,
|
||||
};
|
||||
use super::*;
|
||||
|
||||
pub(super) fn background(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
if bool_or(p, "copy", false) {
|
||||
|
||||
@@ -1,7 +1,4 @@
|
||||
use super::{
|
||||
Arc, NO_ID_SPACE, PhType, Result, Run, RunKind, Session, Shape, ShapeId, Size, TextBody, Value, bad, bool_or, bool_param, defaults, f64_param,
|
||||
json, ok, str_param,
|
||||
};
|
||||
use super::*;
|
||||
|
||||
pub(super) fn scale_shapes(shapes: &mut [Shape], sx: f64, sy: f64, k: f64, ox: f64, oy: f64) {
|
||||
for sh in shapes {
|
||||
|
||||
@@ -95,10 +95,7 @@ fn colors_dialog_apply_and_reject() {
|
||||
assert_eq!(s.doc().unwrap().history.undo.last().unwrap().label, "Colors");
|
||||
s.execute("design.colors", &json!({"name": "Harbor"})).unwrap();
|
||||
assert_eq!(undo_len(&s), n + 1, "the same scheme is not an undo step");
|
||||
colors_dialog_rejects(&mut s, n, &harbor);
|
||||
}
|
||||
|
||||
fn colors_dialog_rejects(s: &mut Session, n: usize, harbor: &deckcraft_color::ColorScheme) {
|
||||
let err = s.execute("design.colors", &json!({"name": "Nope"})).unwrap_err().to_string();
|
||||
assert!(err.contains("no colour scheme `Nope`"), "{err}");
|
||||
assert!(s.execute("design.colors", &json!({"name": " "})).is_err());
|
||||
@@ -111,35 +108,32 @@ fn colors_dialog_rejects(s: &mut Session, n: usize, harbor: &deckcraft_color::Co
|
||||
assert!(s.execute("design.colors", &json!({"colors": {"nope": "#112233"}})).is_err());
|
||||
assert!(s.execute("design.colors", &json!({"colors": {"accent1": "zz"}})).is_err());
|
||||
assert!(s.execute("design.colors", &json!({"name": "Harbor", "colors": {}})).is_err());
|
||||
assert_eq!(scheme(s).name, "Harbor");
|
||||
assert_eq!(scheme(s).get(SchemeSlot::Accent1), harbor.get(SchemeSlot::Accent1));
|
||||
assert_eq!(undo_len(s), n + 1);
|
||||
colors_dialog_custom_overrides(s, n, harbor);
|
||||
}
|
||||
assert_eq!(scheme(&s).name, "Harbor");
|
||||
assert_eq!(scheme(&s).get(SchemeSlot::Accent1), harbor.get(SchemeSlot::Accent1));
|
||||
assert_eq!(undo_len(&s), n + 1);
|
||||
|
||||
fn colors_dialog_custom_overrides(s: &mut Session, n: usize, harbor: &deckcraft_color::ColorScheme) {
|
||||
s.execute("design.colors", &json!({"colors": {"accent1": "112233"}})).unwrap();
|
||||
assert_eq!(scheme(s).name, "Custom");
|
||||
assert_eq!(scheme(s).get(SchemeSlot::Accent1).hex(), "112233");
|
||||
assert_eq!(scheme(s).get(SchemeSlot::Accent2), harbor.get(SchemeSlot::Accent2));
|
||||
assert_eq!(undo_len(s), n + 2);
|
||||
assert_eq!(scheme(&s).name, "Custom");
|
||||
assert_eq!(scheme(&s).get(SchemeSlot::Accent1).hex(), "112233");
|
||||
assert_eq!(scheme(&s).get(SchemeSlot::Accent2), harbor.get(SchemeSlot::Accent2));
|
||||
assert_eq!(undo_len(&s), n + 2);
|
||||
|
||||
let meadow = builtin_color_schemes().into_iter().find(|c| c.name == "Meadow").unwrap();
|
||||
s.execute("design.colors", &json!({"name": "Meadow", "colors": {"lt1": "#010203"}})).unwrap();
|
||||
assert_eq!(scheme(s).name, "Meadow");
|
||||
assert_eq!(scheme(s).get(SchemeSlot::Lt1).hex(), "010203");
|
||||
assert_eq!(scheme(s).get(SchemeSlot::Accent1), meadow.get(SchemeSlot::Accent1));
|
||||
assert_eq!(undo_len(s), n + 3);
|
||||
assert_eq!(scheme(&s).name, "Meadow");
|
||||
assert_eq!(scheme(&s).get(SchemeSlot::Lt1).hex(), "010203");
|
||||
assert_eq!(scheme(&s).get(SchemeSlot::Accent1), meadow.get(SchemeSlot::Accent1));
|
||||
assert_eq!(undo_len(&s), n + 3);
|
||||
|
||||
s.execute("design.colors", &json!({"name": "Talk", "colors": {"dk1": "#ABCDEF"}})).unwrap();
|
||||
assert_eq!(scheme(s).name, "Talk");
|
||||
assert_eq!(scheme(s).get(SchemeSlot::Dk1).hex(), "ABCDEF");
|
||||
assert_eq!(scheme(s).get(SchemeSlot::Lt1).hex(), "010203");
|
||||
assert_eq!(scheme(s).get(SchemeSlot::Accent1), meadow.get(SchemeSlot::Accent1));
|
||||
assert_eq!(scheme(&s).name, "Talk");
|
||||
assert_eq!(scheme(&s).get(SchemeSlot::Dk1).hex(), "ABCDEF");
|
||||
assert_eq!(scheme(&s).get(SchemeSlot::Lt1).hex(), "010203");
|
||||
assert_eq!(scheme(&s).get(SchemeSlot::Accent1), meadow.get(SchemeSlot::Accent1));
|
||||
|
||||
s.execute("edit.undo", &json!({})).unwrap();
|
||||
assert_eq!(scheme(s).name, "Meadow");
|
||||
assert_eq!(scheme(s).get(SchemeSlot::Dk1), meadow.get(SchemeSlot::Dk1));
|
||||
assert_eq!(scheme(&s).name, "Meadow");
|
||||
assert_eq!(scheme(&s).get(SchemeSlot::Dk1), meadow.get(SchemeSlot::Dk1));
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -181,10 +175,7 @@ fn fonts_dialog_apply_and_reject() {
|
||||
assert!(s.execute("design.fonts", &json!({"name": "Classic", "major": ""})).is_err());
|
||||
assert_eq!(font_scheme(&s), classic);
|
||||
assert_eq!(undo_len(&s), n + 1);
|
||||
fonts_dialog_custom_overrides(&mut s);
|
||||
}
|
||||
|
||||
fn fonts_dialog_custom_overrides(s: &mut Session) {
|
||||
s.edit(|doc, _| {
|
||||
let fonts = &mut std::sync::Arc::make_mut(&mut doc.masters[0]).theme.fonts;
|
||||
fonts.major.ea = "EA Major".into();
|
||||
@@ -193,33 +184,33 @@ fn fonts_dialog_custom_overrides(s: &mut Session) {
|
||||
Ok(())
|
||||
})
|
||||
.unwrap();
|
||||
let marked = font_scheme(s);
|
||||
let n = undo_len(s);
|
||||
let marked = font_scheme(&s);
|
||||
let n = undo_len(&s);
|
||||
|
||||
s.execute("design.fonts", &json!({"major": " Lato "})).unwrap();
|
||||
let custom = font_scheme(s);
|
||||
let custom = font_scheme(&s);
|
||||
assert_eq!(custom.name, "Custom");
|
||||
assert_eq!(custom.major.latin, "Lato");
|
||||
assert_eq!(custom.minor.latin, "Lato", "an omitted minor copies major");
|
||||
assert_eq!(custom.major.ea, "EA Major");
|
||||
assert_eq!(custom.minor.cs, "CS Minor");
|
||||
assert_eq!(custom.major.scripts, marked.major.scripts);
|
||||
assert_eq!(undo_len(s), n + 1);
|
||||
assert_eq!(undo_len(&s), n + 1);
|
||||
|
||||
s.execute("design.fonts", &json!({"minor": "Roboto"})).unwrap();
|
||||
assert_eq!(font_scheme(s).name, "Custom");
|
||||
assert_eq!(font_scheme(s).major.latin, "Lato");
|
||||
assert_eq!(font_scheme(s).minor.latin, "Roboto");
|
||||
assert_eq!(font_scheme(s).major.ea, "EA Major");
|
||||
assert_eq!(undo_len(s), n + 2);
|
||||
assert_eq!(font_scheme(&s).name, "Custom");
|
||||
assert_eq!(font_scheme(&s).major.latin, "Lato");
|
||||
assert_eq!(font_scheme(&s).minor.latin, "Roboto");
|
||||
assert_eq!(font_scheme(&s).major.ea, "EA Major");
|
||||
assert_eq!(undo_len(&s), n + 2);
|
||||
|
||||
s.execute("design.fonts", &json!({"name": "Classic", "major": "Inter"})).unwrap();
|
||||
assert_eq!(font_scheme(s).name, "Classic");
|
||||
assert_eq!(font_scheme(s).major.latin, "Inter");
|
||||
assert_eq!(font_scheme(s).minor.latin, "Open Sans");
|
||||
assert_eq!(font_scheme(s).major.ea, "");
|
||||
assert!(font_scheme(s).major.scripts.is_empty(), "a builtin is applied before the latin overlay");
|
||||
assert_eq!(undo_len(s), n + 3);
|
||||
assert_eq!(font_scheme(&s).name, "Classic");
|
||||
assert_eq!(font_scheme(&s).major.latin, "Inter");
|
||||
assert_eq!(font_scheme(&s).minor.latin, "Open Sans");
|
||||
assert_eq!(font_scheme(&s).major.ea, "");
|
||||
assert!(font_scheme(&s).major.scripts.is_empty(), "a builtin is applied before the latin overlay");
|
||||
assert_eq!(undo_len(&s), n + 3);
|
||||
|
||||
s.edit(|doc, _| {
|
||||
std::sync::Arc::make_mut(&mut doc.masters[0]).theme.fonts.major.ea = "Kept".into();
|
||||
@@ -227,16 +218,16 @@ fn fonts_dialog_custom_overrides(s: &mut Session) {
|
||||
})
|
||||
.unwrap();
|
||||
s.execute("design.fonts", &json!({"name": "Talk", "minor": "Nunito Sans"})).unwrap();
|
||||
assert_eq!(font_scheme(s).name, "Talk");
|
||||
assert_eq!(font_scheme(s).major.latin, "Inter");
|
||||
assert_eq!(font_scheme(s).minor.latin, "Nunito Sans");
|
||||
assert_eq!(font_scheme(s).major.ea, "Kept");
|
||||
assert_eq!(font_scheme(&s).name, "Talk");
|
||||
assert_eq!(font_scheme(&s).major.latin, "Inter");
|
||||
assert_eq!(font_scheme(&s).minor.latin, "Nunito Sans");
|
||||
assert_eq!(font_scheme(&s).major.ea, "Kept");
|
||||
assert_eq!(s.doc().unwrap().history.undo.last().unwrap().label, "Fonts");
|
||||
|
||||
s.execute("edit.undo", &json!({})).unwrap();
|
||||
assert_eq!(font_scheme(s).name, "Classic");
|
||||
assert_eq!(font_scheme(s).major.ea, "Kept");
|
||||
assert_eq!(font_scheme(s).minor.latin, "Open Sans");
|
||||
assert_eq!(font_scheme(&s).name, "Classic");
|
||||
assert_eq!(font_scheme(&s).major.ea, "Kept");
|
||||
assert_eq!(font_scheme(&s).minor.latin, "Open Sans");
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
@@ -1,7 +1,4 @@
|
||||
use super::{
|
||||
Arc, Caps, ColorRef, ColorTransform, Fill, Result, RunProps, SchemeSlot, Session, Shape, ShapeId, ShapeKind, Strike, Value, bad, bool_or,
|
||||
bool_param, color_param, current_run, ed, edit_shapes, f64_param, format_run, json, ok, on_param, resolve, str_param,
|
||||
};
|
||||
use super::*;
|
||||
|
||||
pub(super) fn bold(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
let v = on_param(p, current_run(s).bold);
|
||||
|
||||
@@ -1,7 +1,4 @@
|
||||
use super::{
|
||||
Bullet, Effects, Fill, Line, ParaProps, Result, RunProps, Session, Shape, Strike, Value, bad, base64_decode, bool_or, current_para, current_run,
|
||||
ed, edit_shapes, format_run, ids_param, json, ok, resolve, str_param,
|
||||
};
|
||||
use super::*;
|
||||
|
||||
/// Formatting picked up by the Format Painter.
|
||||
#[derive(Clone, Debug, Default)]
|
||||
|
||||
@@ -1,7 +1,4 @@
|
||||
use super::{
|
||||
Align, Anchor, AutoFit, Bullet, Result, Session, Spacing, TextDir, Value, bad, bool_or, color_param, current_para, f64_or, f64_param,
|
||||
format_body, format_para, json, ok, on_param, resolve, str_param,
|
||||
};
|
||||
use super::*;
|
||||
|
||||
pub(super) fn align_to(s: &mut Session, a: Align) -> Result<Value> {
|
||||
format_para(s, &move |p| p.props.align = Some(a))
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
use super::paragraph::{clamp_pt, finite_key, tab_align, tabs_params};
|
||||
use super::{Result, Session, TabStop, TextDir, Value, bad, bool_or, current_para, format_para, json, ok, resolve};
|
||||
use super::*;
|
||||
|
||||
/// What the horizontal ruler shows while editing text: the paragraph's indents and tab stops,
|
||||
/// and where its text area sits on the slide.
|
||||
|
||||
@@ -1,6 +1,4 @@
|
||||
use super::{
|
||||
Line, Result, Session, Value, bad, color_param, current_run, ed, edit_shapes, f64_or, format_body, format_run, json, ok, on_param, str_param,
|
||||
};
|
||||
use super::*;
|
||||
|
||||
pub(super) fn text_shadow(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
let v = on_param(p, Some(current_run(s).shadow.is_some()));
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
use super::picture::{content_type, media_bytes};
|
||||
use super::{NO_ID_SPACE, Result, Session, ShapeKind, Value, bad, base64_encode, json};
|
||||
use super::*;
|
||||
|
||||
const ALBUM_MAX_ITEMS: usize = 48;
|
||||
|
||||
|
||||
@@ -1,7 +1,4 @@
|
||||
use super::{
|
||||
Chart, ChartType, Result, RunKind, Session, Shape, ShapeKind, Value, Xfrm, add_shape, bad, bool_param, color_value, edit_shapes, json,
|
||||
rect_param, str_param,
|
||||
};
|
||||
use super::*;
|
||||
|
||||
pub fn chart_type(name: &str) -> ChartType {
|
||||
match name.to_ascii_lowercase().replace(['-', '_', ' '], "").as_str() {
|
||||
|
||||
@@ -1,7 +1,4 @@
|
||||
use super::{
|
||||
Geom, Result, SchemeSlot, Session, Shape, ShapeStyle, Value, add_shape, bad, bool_or, bool_param, default_rect, edit_shapes, json, rect_param,
|
||||
str_param, usize_param,
|
||||
};
|
||||
use super::*;
|
||||
|
||||
pub(super) fn hyperlink(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
use deckcraft_model::text::{Action, Hyperlink};
|
||||
|
||||
@@ -1,7 +1,4 @@
|
||||
use super::{
|
||||
EngineError, MediaClip, NO_ID_SPACE, PictureFill, Result, Session, Shape, ShapeKind, Value, Xfrm, add_shape, bad, base64_decode, default_rect,
|
||||
json, rect_param, str_param, xfrm_of,
|
||||
};
|
||||
use super::*;
|
||||
|
||||
/// Read media bytes from `data` (base64) or `path`.
|
||||
pub(crate) fn media_bytes(p: &Value, cmd: &str) -> Result<(String, Vec<u8>)> {
|
||||
|
||||
@@ -1,7 +1,4 @@
|
||||
use super::{
|
||||
ColorRef, Fill, Result, RunKind, SchemeSlot, Session, TextBody, Value, Xfrm, add_shape, bad, bool_or, bool_param, json, new_text_box, ok,
|
||||
str_param, usize_param,
|
||||
};
|
||||
use super::*;
|
||||
|
||||
pub(super) fn word_art(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
let text = str_param(p, "text").unwrap_or("Your text here");
|
||||
|
||||
@@ -1,8 +1,4 @@
|
||||
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, usize_param,
|
||||
};
|
||||
use super::*;
|
||||
|
||||
pub(super) fn adjust(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
let i = usize_param(p, "index").unwrap_or(0).min(16);
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
use super::{ColorRef, Fill, Geom, Line, NO_ID_SPACE, Result, Session, Value, bad, bool_or, id_param, json, str_param, usize_param};
|
||||
use super::*;
|
||||
|
||||
pub(super) fn sites(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
let id = id_param(p, "id", "shape.sites")?.ok_or_else(|| bad("shape.sites", "missing `id`"))?;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
use super::{NO_ID_SPACE, Result, Session, Value, bad, bool_or, bool_param, edit_geometry, edit_shapes, f64_param, json};
|
||||
use super::*;
|
||||
|
||||
pub(super) fn crop(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
let c = [f64_param(p, "left"), f64_param(p, "top"), f64_param(p, "right"), f64_param(p, "bottom")];
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
use super::{Result, Session, Value, bad, edit_geometry, f64_param, json, str_param, targets, xfrm_of};
|
||||
use super::*;
|
||||
|
||||
/// Keeps a side length in `0..=MAX_PT` so a huge value cannot exhaust memory when rendering.
|
||||
/// An overflow to infinity lands on the limit; NaN becomes 0.
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
use super::nodes::Item;
|
||||
use super::paint::{accent, deco, line_shape, paint, text_shape};
|
||||
use super::{ColorRef, ColorTransform, Diagram, DiagramNode, Fill, Geom, Line, MAX_SHAPES, SchemeSlot, Xfrm};
|
||||
use super::*;
|
||||
|
||||
/// Organization Chart: [`hierarchy`] with elbow connectors, and assistants beside their parent on
|
||||
/// a row of their own between the parent and its other children. `half_circle` draws each node as
|
||||
|
||||
@@ -1,9 +1,6 @@
|
||||
use super::nodes::{add, demote_node, nodes_from_lines, normalize, promote_node, remove, sync_text};
|
||||
use super::paint::layout;
|
||||
use super::{
|
||||
AutoFit, COLORS, Diagram, DiagramNode, EngineError, Geom, MAX_LEVEL, NO_ID_SPACE, Paragraph, Presentation, Result, STYLES, Session, Shape,
|
||||
ShapeId, ShapeKind, TextBody, Value, Xfrm, bad, bool_or, current_shapes, is_kind, json, shape_of, str_param, targets, xfrm_of,
|
||||
};
|
||||
use super::*;
|
||||
|
||||
/// The SmartArt group that `id` is, or is inside.
|
||||
fn diagram_group(shapes: &[Shape], id: ShapeId) -> Option<&Shape> {
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,5 +1,5 @@
|
||||
use super::paint::layout_items;
|
||||
use super::{Diagram, DiagramNode, MAX_LEVEL, MAX_SHAPES, Shape, Xfrm};
|
||||
use super::*;
|
||||
|
||||
/// Nodes from text lines: each leading tab is one outline level.
|
||||
pub fn nodes_from_lines<'a>(lines: impl IntoIterator<Item = &'a str>) -> Vec<DiagramNode> {
|
||||
|
||||
@@ -1,9 +1,6 @@
|
||||
use super::layout::layout_raw;
|
||||
use super::nodes::Item;
|
||||
use super::{
|
||||
Align, AutoFit, ColorRef, ColorTransform, Diagram, DiagramNode, Effects, Fill, Geom, Gradient, GradientShape, GradientStop, Line, MAX_LEVEL,
|
||||
Paragraph, SchemeSlot, Shadow, Shape, ShapeStyle, TextBody, Xfrm,
|
||||
};
|
||||
use super::*;
|
||||
|
||||
fn slot(i: usize) -> SchemeSlot {
|
||||
[SchemeSlot::Accent1, SchemeSlot::Accent2, SchemeSlot::Accent3, SchemeSlot::Accent4, SchemeSlot::Accent5, SchemeSlot::Accent6][i % 6]
|
||||
|
||||
@@ -24,6 +24,8 @@ mod layout;
|
||||
|
||||
use autocorrect::autocorrect_options;
|
||||
pub use autocorrect::{AUTOCORRECT, AutoList, correct_word};
|
||||
#[cfg(test)]
|
||||
use caret::count_param;
|
||||
pub(crate) use caret::{apply_leading_tabs, delete_dir};
|
||||
use caret::{delete, delete_surrounding, get, insert, move_caret, select, select_all, select_para, select_word, set};
|
||||
use editing::{edit, exit};
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
use super::{Arc, Result, Session, TextSel, Value, bad, body_mut, body_of, ed, edit_text, json, str_param};
|
||||
use super::*;
|
||||
|
||||
/// Built-in AutoCorrect replacements (common slips and symbols).
|
||||
pub const AUTOCORRECT: &[(&str, &str)] = &[
|
||||
|
||||
@@ -2,10 +2,7 @@ use super::autocorrect::undo_auto_list;
|
||||
use super::editing::{inherited_aligns, insert_text, promote_rtl_if_needed};
|
||||
use super::ime::{composition_range, model_pos, walk_backward, walk_forward};
|
||||
use super::layout::{ctx_for, fit_text_box, layout_for, pos_param};
|
||||
use super::{
|
||||
DocState, HistoryEntry, Pos, Result, Session, ShapeId, ShapeKind, TextBody, Value, bad, body_of, bool_or, ed, edit_shapes, edit_text, id_param,
|
||||
json, ok, push_undo, str_param, xfrm_of,
|
||||
};
|
||||
use super::*;
|
||||
|
||||
/// A table cell's text laid out the way it is drawn, mapped into slide coordinates.
|
||||
pub(super) fn cell_on_slide(st: &DocState, id: ShapeId, row: usize, col: usize) -> Option<CellText> {
|
||||
|
||||
@@ -1,9 +1,6 @@
|
||||
use super::autocorrect::autocorrect;
|
||||
use super::layout::{ctx_for, pos_param};
|
||||
use super::{
|
||||
Align, Pos, Result, Run, RunKind, RunProps, Session, ShapeId, ShapeKind, TextBody, TextSel, Value, bad, body_of, bool_or, ed, edit_text,
|
||||
id_param, json, ok, resolve, shape_of,
|
||||
};
|
||||
use super::*;
|
||||
|
||||
pub(super) fn edit(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
if bool_or(p, "notes", false) {
|
||||
|
||||
@@ -1,8 +1,5 @@
|
||||
use super::layout::fit_text_box;
|
||||
use super::{
|
||||
DocState, Hyperlink, Pos, Result, RunProps, SELECT_TEXT_OR_SHAPE, Session, ShapeId, ShapeKind, TextBody, TextSel, Value, bad, body_of, ed,
|
||||
edit_shapes, edit_text, json, ok, str_param,
|
||||
};
|
||||
use super::*;
|
||||
|
||||
/// Apply run formatting to the text selection (or, when not editing, to all text of the
|
||||
/// selected shapes). A caret without a range formats the word it's in (PowerPoint behaviour),
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
use super::caret::cell_on_slide;
|
||||
use super::editing::unlinked_at_link_end;
|
||||
use super::layout::{ctx_for, layout_for};
|
||||
use super::{DocState, Paragraph, Result, Session, TextBody, TextSel, Value, bad, body_of, ed, edit_text, json, str_param, xfrm_of};
|
||||
use super::*;
|
||||
|
||||
/// Maximum number of UTF-8 bytes in an outgoing IME surrounding-text query.
|
||||
///
|
||||
@@ -598,9 +598,9 @@ mod ime_surrounding_tests {
|
||||
s.delete_surrounding(usize::MAX, 0, false).unwrap();
|
||||
assert_eq!(text(&s, id), "é😀b", "everything before the caret goes, no panic");
|
||||
let expected = usize::try_from(4_294_967_296u64).unwrap_or(usize::MAX);
|
||||
assert_eq!(super::super::caret::count_param(&json!({"before": 4_294_967_296u64}), "before"), expected);
|
||||
assert_eq!(super::count_param(&json!({"before": 4_294_967_296u64}), "before"), expected);
|
||||
assert!(expected > 0, "the count is clamped, not wrapped to nothing");
|
||||
assert_eq!(super::super::caret::count_param(&json!({}), "before"), 0);
|
||||
assert_eq!(super::count_param(&json!({}), "before"), 0);
|
||||
}
|
||||
|
||||
/// A non-composing input-method deletion is exactly one undo step; the manual history entry
|
||||
|
||||
@@ -1,8 +1,5 @@
|
||||
use super::caret::cell_on_slide;
|
||||
use super::{
|
||||
Affine, Arc, AutoFit, Ctx, DocState, Paragraph, Point, Pos, Presentation, Rect, Result, Selection, Session, Shape, ShapeId, Target, TextBody,
|
||||
TextSel, Value, ed, resolve, xfrm_of,
|
||||
};
|
||||
use super::*;
|
||||
|
||||
/// Refit a shape after its text changed: remember the shrink-on-overflow scale the layout
|
||||
/// computes (#724), then grow/shrink an auto-sizing text box (Resize shape to fit text) to its
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
use super::hit::{EditZone, edit_zone, edited_box, shape_in_front, text_member_at, text_pos};
|
||||
use super::{Drag, Hit, Point, Result, Session, ShapeId, ShapeKind, TextSel, ToolKind, Value, cmd, json, xfrm_of};
|
||||
use super::*;
|
||||
|
||||
impl Session {
|
||||
pub(super) fn double_click(&mut self, p: Point, tol: f64) -> Result<Value> {
|
||||
|
||||
+148
-172
@@ -1,16 +1,5 @@
|
||||
use super::hit::{constrain_45, line_xfrm, resize_box};
|
||||
use super::{
|
||||
Affine, DRAW_TABLE, Drag, Mods, Point, Rect, Result, Session, ShapeId, ShapeKind, TextSel, ToolKind, Value, Vec2, Xfrm, cmd, json, set_warp_adj,
|
||||
};
|
||||
|
||||
struct MoveDrag {
|
||||
ids: Vec<ShapeId>,
|
||||
start: Point,
|
||||
origs: Vec<(ShapeId, Xfrm)>,
|
||||
moved: bool,
|
||||
duplicate: bool,
|
||||
enter_text: Option<(ShapeId, Point)>,
|
||||
}
|
||||
use super::*;
|
||||
|
||||
impl Session {
|
||||
pub(super) fn drag(&mut self, p: Point, mods: Mods, tol: f64) -> Result<Value> {
|
||||
@@ -24,7 +13,55 @@ impl Session {
|
||||
self.tool.guide_drag = Some((index, pos));
|
||||
}
|
||||
Drag::Move { ids, start, origs, moved, duplicate, enter_text } => {
|
||||
return self.drag_move(MoveDrag { ids, start, origs, moved, duplicate, enter_text }, p, mods, tol);
|
||||
let mut d = p - start;
|
||||
if !moved && (d.hypot() < tol.max(1.0) || origs.is_empty()) {
|
||||
return Ok(Value::Null);
|
||||
}
|
||||
if !moved {
|
||||
self.begin(if duplicate { "Duplicate" } else { "Move" });
|
||||
if duplicate {
|
||||
// Alt-drag: drag a copy, leave the original.
|
||||
self.execute_dup_in_place(&ids)?;
|
||||
}
|
||||
}
|
||||
let ids_now = if duplicate { self.doc()?.selection.shapes.clone() } else { ids.clone() };
|
||||
if mods.shift {
|
||||
if d.x.abs() > d.y.abs() {
|
||||
d.y = 0.0;
|
||||
} else {
|
||||
d.x = 0.0;
|
||||
}
|
||||
}
|
||||
// Smart guides on the moved group's bounds.
|
||||
let bounds = origs.iter().map(|(_, x)| x.bounds()).reduce(|a, b| a.union(b)).unwrap_or_default();
|
||||
let moved_b = bounds + d;
|
||||
let (snap, guides) = self.snap_rect(moved_b, &ids_now, tol * 1.5, !mods.cmd);
|
||||
d += snap;
|
||||
self.tool.guides = guides;
|
||||
let origs2: Vec<(ShapeId, Xfrm)> =
|
||||
if duplicate { ids_now.iter().copied().zip(origs.iter().map(|o| o.1)).collect() } else { origs.clone() };
|
||||
self.edit(|doc, sel| {
|
||||
let list = crate::shapes_mut(doc, sel).ok_or_else(|| cmd::bad("move", "no slide"))?;
|
||||
for (id, o) in &origs2 {
|
||||
if let Some(sh) = deckcraft_model::find_shape_mut(list, *id) {
|
||||
sh.xfrm = Some(Xfrm { x: o.x + d.x, y: o.y + d.y, ..*o });
|
||||
}
|
||||
}
|
||||
// A connector dragged away from shapes that stay put comes unglued at those ends.
|
||||
let moved: Vec<ShapeId> = origs2.iter().map(|o| o.0).collect();
|
||||
for sh in list.iter_mut().filter(|s| moved.contains(&s.id)) {
|
||||
if let ShapeKind::Connector { start, end } = &mut sh.kind {
|
||||
for c in [start, end] {
|
||||
if c.is_some_and(|(to, _)| !moved.contains(&to)) {
|
||||
*c = None;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
})?;
|
||||
self.tool.drag =
|
||||
Some(Drag::Move { ids: ids_now, start, origs: origs2, moved: true, duplicate: false, enter_text: enter_text.filter(|_| false) });
|
||||
}
|
||||
Drag::Resize { id, handle, orig, start } => {
|
||||
let nx = resize_box(&orig, handle, p - start, mods.shift || self.is_picture(id), mods.alt);
|
||||
@@ -73,7 +110,39 @@ impl Session {
|
||||
self.set_xfrm(*id, nx)?;
|
||||
}
|
||||
}
|
||||
Drag::Adjust { id, index, orig } => return self.drag_adjust(id, index, orig, p),
|
||||
Drag::Adjust { id, index, orig } => {
|
||||
let st = self.doc()?;
|
||||
let Some(sh) = st.shape(id).cloned() else { return Ok(Value::Null) };
|
||||
let local = orig.affine().inverse() * p;
|
||||
let geo = deckcraft_render::shape_geometry(&sh, orig.w, orig.h);
|
||||
let Some(h) = geo.handles.iter().find(|h| h.index == index) else { return Ok(Value::Null) };
|
||||
let free2d = matches!(sh.geom.preset_name(), Some(n) if n.contains("Callout") && !n.contains("Arrow"));
|
||||
let vals: Vec<(usize, f64)> = if free2d {
|
||||
let dx = (local.x - orig.w / 2.0) / orig.w.max(1e-6) * 1e5;
|
||||
let dy = (local.y - orig.h / 2.0) / orig.h.max(1e-6) * 1e5;
|
||||
vec![(0, dx.round()), (1, dy.round())]
|
||||
} else {
|
||||
vec![(index, h.value_at(local))]
|
||||
};
|
||||
self.edit(|doc, sel| {
|
||||
let list = crate::shapes_mut(doc, sel).ok_or_else(|| cmd::bad("adjust", "no slide"))?;
|
||||
if let Some(sh) = deckcraft_model::find_shape_mut(list, id)
|
||||
&& let deckcraft_model::Geom::Preset { name, adj } = &mut sh.geom
|
||||
{
|
||||
let defaults = deckcraft_geom::preset::info(name).map(|x| x.defaults).unwrap_or(&[]);
|
||||
for (i, v) in &vals {
|
||||
while adj.len() <= *i {
|
||||
let k = adj.len();
|
||||
adj.push(defaults.get(k).copied().unwrap_or(0.0));
|
||||
}
|
||||
if let Some(slot) = adj.get_mut(*i) {
|
||||
*slot = *v;
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
})?;
|
||||
}
|
||||
Drag::Vertex { id, path, index } => {
|
||||
self.tool.drag = Some(Drag::Vertex { id, path, index });
|
||||
self.edit_vertex(id, path, index, p)?;
|
||||
@@ -118,8 +187,71 @@ impl Session {
|
||||
Drag::Marquee { start, .. } => {
|
||||
self.tool.marquee = Some(Rect::from_points(start, p));
|
||||
}
|
||||
Drag::Create { start } => self.drag_create(start, p, mods, tol),
|
||||
Drag::TextSelect { shape, press, .. } => return self.drag_text_select(shape, press, p),
|
||||
Drag::Create { start } => {
|
||||
let line = matches!(&self.tool.kind, ToolKind::Shape { preset } if deckcraft_geom::preset::is_line_like(preset));
|
||||
let x = if line {
|
||||
let mut q = if mods.shift { constrain_45(start, p) } else { p };
|
||||
if let Some((_, _, at)) = self.update_sites(q, tol, None) {
|
||||
q = at;
|
||||
}
|
||||
let s0 = self.glue_at(start, tol).map(|g| g.2).unwrap_or(start);
|
||||
line_xfrm(s0, q, 0.0)
|
||||
} else if matches!(&self.tool.kind, ToolKind::Shape { preset } if preset == DRAW_TABLE)
|
||||
&& crate::cmd::table::table_at(self, &Value::Null, (start.x, start.y)).is_some()
|
||||
{
|
||||
// The pencil inside a table draws a straight border, across or down.
|
||||
let q = if (p.x - start.x).abs() >= (p.y - start.y).abs() { Point::new(p.x, start.y) } else { Point::new(start.x, p.y) };
|
||||
let r = Rect::from_points(start, q);
|
||||
Xfrm::new(r.x0, r.y0, r.width(), r.height())
|
||||
} else {
|
||||
let mut r = Rect::from_points(start, p);
|
||||
if mods.shift {
|
||||
let s = r.width().max(r.height());
|
||||
let sx = if p.x < start.x { -s } else { s };
|
||||
let sy = if p.y < start.y { -s } else { s };
|
||||
r = Rect::from_points(start, start + Vec2::new(sx, sy));
|
||||
}
|
||||
if mods.alt {
|
||||
let d = p - start;
|
||||
r = Rect::new(start.x - d.x.abs(), start.y - d.y.abs(), start.x + d.x.abs(), start.y + d.y.abs());
|
||||
}
|
||||
Xfrm::new(r.x0, r.y0, r.width(), r.height())
|
||||
};
|
||||
self.tool.preview = Some(x);
|
||||
}
|
||||
Drag::TextSelect { shape, press, .. } => {
|
||||
// In a table, leaving the cell the drag started in selects whole cells, as in
|
||||
// PowerPoint; coming back to that cell selects text in it again.
|
||||
let st = self.doc()?;
|
||||
if let Some(start) = press.as_ref().and_then(|t| t.cell)
|
||||
&& let Some(f) = st.shape(shape).and_then(|sh| cmd::table::table_frame(st, sh))
|
||||
&& let Some(cell) = f.cell_at(f.local(p))
|
||||
{
|
||||
if cell != start {
|
||||
self.select(|_, sel| {
|
||||
sel.text = None;
|
||||
sel.shapes = vec![shape];
|
||||
sel.cells = Some((shape, start, cell));
|
||||
})?;
|
||||
return Ok(Value::Null);
|
||||
}
|
||||
if st.selection.text.is_none() {
|
||||
self.select(|_, sel| {
|
||||
sel.cells = None;
|
||||
sel.text = press;
|
||||
})?;
|
||||
}
|
||||
}
|
||||
let st = self.doc()?;
|
||||
if let Some(t) = st.selection.text.as_ref().filter(|t| t.shape == shape) {
|
||||
let pos = cmd::text::text_pos_at(st, t, p);
|
||||
self.select(|_, sel| {
|
||||
if let Some(t) = sel.text.as_mut() {
|
||||
t.caret = pos;
|
||||
}
|
||||
})?;
|
||||
}
|
||||
}
|
||||
Drag::TableBorder { id, column, index, start, size, frame, moved } => {
|
||||
let local = frame.inverse() * p;
|
||||
let d = if column { local.x - start.x } else { local.y - start.y };
|
||||
@@ -161,162 +293,6 @@ impl Session {
|
||||
Ok(Value::Null)
|
||||
}
|
||||
|
||||
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;
|
||||
if !moved && (d.hypot() < tol.max(1.0) || origs.is_empty()) {
|
||||
return Ok(Value::Null);
|
||||
}
|
||||
if !moved {
|
||||
self.begin(if duplicate { "Duplicate" } else { "Move" });
|
||||
if duplicate {
|
||||
// Alt-drag: drag a copy, leave the original.
|
||||
self.execute_dup_in_place(&ids)?;
|
||||
}
|
||||
}
|
||||
let ids_now = if duplicate { self.doc()?.selection.shapes.clone() } else { ids.clone() };
|
||||
if mods.shift {
|
||||
if d.x.abs() > d.y.abs() {
|
||||
d.y = 0.0;
|
||||
} else {
|
||||
d.x = 0.0;
|
||||
}
|
||||
}
|
||||
// Smart guides on the moved group's bounds.
|
||||
let bounds = origs.iter().map(|(_, x)| x.bounds()).reduce(|a, b| a.union(b)).unwrap_or_default();
|
||||
let moved_b = bounds + d;
|
||||
let (snap, guides) = self.snap_rect(moved_b, &ids_now, tol * 1.5, !mods.cmd);
|
||||
d += snap;
|
||||
self.tool.guides = guides;
|
||||
let origs2: Vec<(ShapeId, Xfrm)> = if duplicate { ids_now.iter().copied().zip(origs.iter().map(|o| o.1)).collect() } else { origs.clone() };
|
||||
self.edit(|doc, sel| {
|
||||
let list = crate::shapes_mut(doc, sel).ok_or_else(|| cmd::bad("move", "no slide"))?;
|
||||
for (id, o) in &origs2 {
|
||||
if let Some(sh) = deckcraft_model::find_shape_mut(list, *id) {
|
||||
sh.xfrm = Some(Xfrm { x: o.x + d.x, y: o.y + d.y, ..*o });
|
||||
}
|
||||
}
|
||||
// A connector dragged away from shapes that stay put comes unglued at those ends.
|
||||
let moved: Vec<ShapeId> = origs2.iter().map(|o| o.0).collect();
|
||||
for sh in list.iter_mut().filter(|s| moved.contains(&s.id)) {
|
||||
if let ShapeKind::Connector { start, end } = &mut sh.kind {
|
||||
for c in [start, end] {
|
||||
if c.is_some_and(|(to, _)| !moved.contains(&to)) {
|
||||
*c = None;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
})?;
|
||||
self.tool.drag =
|
||||
Some(Drag::Move { ids: ids_now, start, origs: origs2, moved: true, duplicate: false, enter_text: enter_text.filter(|_| false) });
|
||||
Ok(Value::Null)
|
||||
}
|
||||
|
||||
fn drag_adjust(&mut self, id: ShapeId, index: usize, orig: Xfrm, p: Point) -> Result<Value> {
|
||||
let st = self.doc()?;
|
||||
let Some(sh) = st.shape(id).cloned() else { return Ok(Value::Null) };
|
||||
let local = orig.affine().inverse() * p;
|
||||
let geo = deckcraft_render::shape_geometry(&sh, orig.w, orig.h);
|
||||
let Some(h) = geo.handles.iter().find(|h| h.index == index) else { return Ok(Value::Null) };
|
||||
let free2d = matches!(sh.geom.preset_name(), Some(n) if n.contains("Callout") && !n.contains("Arrow"));
|
||||
let vals: Vec<(usize, f64)> = if free2d {
|
||||
let dx = (local.x - orig.w / 2.0) / orig.w.max(1e-6) * 1e5;
|
||||
let dy = (local.y - orig.h / 2.0) / orig.h.max(1e-6) * 1e5;
|
||||
vec![(0, dx.round()), (1, dy.round())]
|
||||
} else {
|
||||
vec![(index, h.value_at(local))]
|
||||
};
|
||||
self.edit(|doc, sel| {
|
||||
let list = crate::shapes_mut(doc, sel).ok_or_else(|| cmd::bad("adjust", "no slide"))?;
|
||||
if let Some(sh) = deckcraft_model::find_shape_mut(list, id)
|
||||
&& let deckcraft_model::Geom::Preset { name, adj } = &mut sh.geom
|
||||
{
|
||||
let defaults = deckcraft_geom::preset::info(name).map(|x| x.defaults).unwrap_or(&[]);
|
||||
for (i, v) in &vals {
|
||||
while adj.len() <= *i {
|
||||
let k = adj.len();
|
||||
adj.push(defaults.get(k).copied().unwrap_or(0.0));
|
||||
}
|
||||
if let Some(slot) = adj.get_mut(*i) {
|
||||
*slot = *v;
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
})?;
|
||||
Ok(Value::Null)
|
||||
}
|
||||
|
||||
fn drag_create(&mut self, start: Point, p: Point, mods: Mods, tol: f64) {
|
||||
let line = matches!(&self.tool.kind, ToolKind::Shape { preset } if deckcraft_geom::preset::is_line_like(preset));
|
||||
let x = if line {
|
||||
let mut q = if mods.shift { constrain_45(start, p) } else { p };
|
||||
if let Some((_, _, at)) = self.update_sites(q, tol, None) {
|
||||
q = at;
|
||||
}
|
||||
let s0 = self.glue_at(start, tol).map(|g| g.2).unwrap_or(start);
|
||||
line_xfrm(s0, q, 0.0)
|
||||
} else if matches!(&self.tool.kind, ToolKind::Shape { preset } if preset == DRAW_TABLE)
|
||||
&& crate::cmd::table::table_at(self, &Value::Null, (start.x, start.y)).is_some()
|
||||
{
|
||||
// The pencil inside a table draws a straight border, across or down.
|
||||
let q = if (p.x - start.x).abs() >= (p.y - start.y).abs() { Point::new(p.x, start.y) } else { Point::new(start.x, p.y) };
|
||||
let r = Rect::from_points(start, q);
|
||||
Xfrm::new(r.x0, r.y0, r.width(), r.height())
|
||||
} else {
|
||||
let mut r = Rect::from_points(start, p);
|
||||
if mods.shift {
|
||||
let s = r.width().max(r.height());
|
||||
let sx = if p.x < start.x { -s } else { s };
|
||||
let sy = if p.y < start.y { -s } else { s };
|
||||
r = Rect::from_points(start, start + Vec2::new(sx, sy));
|
||||
}
|
||||
if mods.alt {
|
||||
let d = p - start;
|
||||
r = Rect::new(start.x - d.x.abs(), start.y - d.y.abs(), start.x + d.x.abs(), start.y + d.y.abs());
|
||||
}
|
||||
Xfrm::new(r.x0, r.y0, r.width(), r.height())
|
||||
};
|
||||
self.tool.preview = Some(x);
|
||||
}
|
||||
|
||||
fn drag_text_select(&mut self, shape: ShapeId, press: Option<TextSel>, p: Point) -> Result<Value> {
|
||||
// In a table, leaving the cell the drag started in selects whole cells, as in
|
||||
// PowerPoint; coming back to that cell selects text in it again.
|
||||
let st = self.doc()?;
|
||||
if let Some(start) = press.as_ref().and_then(|t| t.cell)
|
||||
&& let Some(f) = st.shape(shape).and_then(|sh| cmd::table::table_frame(st, sh))
|
||||
&& let Some(cell) = f.cell_at(f.local(p))
|
||||
{
|
||||
if cell != start {
|
||||
self.select(|_, sel| {
|
||||
sel.text = None;
|
||||
sel.shapes = vec![shape];
|
||||
sel.cells = Some((shape, start, cell));
|
||||
})?;
|
||||
return Ok(Value::Null);
|
||||
}
|
||||
if st.selection.text.is_none() {
|
||||
self.select(|_, sel| {
|
||||
sel.cells = None;
|
||||
sel.text = press;
|
||||
})?;
|
||||
}
|
||||
}
|
||||
let st = self.doc()?;
|
||||
if let Some(t) = st.selection.text.as_ref().filter(|t| t.shape == shape) {
|
||||
let pos = cmd::text::text_pos_at(st, t, p);
|
||||
self.select(|_, sel| {
|
||||
if let Some(t) = sel.text.as_mut() {
|
||||
t.caret = pos;
|
||||
}
|
||||
})?;
|
||||
}
|
||||
Ok(Value::Null)
|
||||
}
|
||||
|
||||
/// Moves a vertex during a drag (one undo step for the whole drag).
|
||||
fn edit_vertex(&mut self, id: ShapeId, path: usize, index: usize, p: Point) -> Result<()> {
|
||||
let r = cmd::points::move_point_raw(self, id, path, index, p);
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
use super::hit::table_rows;
|
||||
use super::{Drag, Guide, HistoryEntry, Point, Rect, Result, Session, Shape, ShapeId, ShapeKind, ToolKind, Value, Vec2, Xfrm, cmd, json, xfrm_of};
|
||||
use super::*;
|
||||
|
||||
impl Session {
|
||||
/// End the freeform being drawn: create the shape (if it has at least two points).
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
use super::{Affine, Hit, Point, Rect, Shape, ShapeId, ShapeKind, TextSel, Vec2, Xfrm, cmd, xfrm_of};
|
||||
use super::*;
|
||||
|
||||
pub(super) fn is_text_first(st: &crate::DocState, id: ShapeId) -> bool {
|
||||
st.shape(id).is_some_and(|s| s.ph.is_some() || s.text_box)
|
||||
|
||||
@@ -1,8 +1,5 @@
|
||||
use super::hit::{EditZone, contains_rect, edit_zone, edited_box, hit_shape, in_table_cell, is_text_first, shape_in_front};
|
||||
use super::{
|
||||
Arc, DRAW_TABLE, Drag, HistoryEntry, Hit, Mods, Point, PointerEvent, PointerKind, Rect, Result, Session, ShapeId, ShapeKind, TABLE_ERASER,
|
||||
TextTarget, ToolKind, Value, Xfrm, box_handles, cmd, is_freeform_tool, json, wordart_handles, xfrm_of,
|
||||
};
|
||||
use super::*;
|
||||
|
||||
impl Session {
|
||||
pub fn set_tool(&mut self, kind: ToolKind) {
|
||||
|
||||
@@ -4,24 +4,7 @@ use super::builder::{
|
||||
};
|
||||
use super::catalog::{FillMode, Geometry, Handle, info};
|
||||
use super::shapes::{action_button, cloud, math, ribbon, scroll, wedge_rect};
|
||||
use super::{Arc, BezPath, Point, Rect, Vec2, extra};
|
||||
|
||||
/// Size and adjust values shared by every preset-shape builder.
|
||||
struct Ctx<'a> {
|
||||
name: &'a str,
|
||||
w: f64,
|
||||
h: f64,
|
||||
adj: &'a [f64],
|
||||
defaults: &'a [f64],
|
||||
}
|
||||
|
||||
impl Ctx<'_> {
|
||||
/// Adjust value `i`: the caller's value, else the preset default, else 0 (non-finite values become 0).
|
||||
fn a(&self, i: usize) -> f64 {
|
||||
let v = self.adj.get(i).copied().or_else(|| self.defaults.get(i).copied()).unwrap_or(0.0);
|
||||
if v.is_finite() { v } else { 0.0 }
|
||||
}
|
||||
}
|
||||
use super::*;
|
||||
|
||||
/// Builds the geometry for preset `name` at size `w`×`h` with adjust values `adj` (missing
|
||||
/// values use the preset's defaults). Unknown names return `None`; the caller falls back to `rect`.
|
||||
@@ -29,25 +12,17 @@ pub fn build(name: &str, w: f64, h: f64, adj: &[f64]) -> Option<Geometry> {
|
||||
if !(w.is_finite() && h.is_finite()) {
|
||||
return None;
|
||||
}
|
||||
let w = w.max(0.0);
|
||||
let h = h.max(0.0);
|
||||
let defaults = info(name).map(|i| i.defaults).unwrap_or(&[]);
|
||||
let c = Ctx { name, w: w.max(0.0), h: h.max(0.0), adj, defaults };
|
||||
rect_and_triangle_shapes(&c)
|
||||
.or_else(|| polygon_and_symbol_shapes(&c))
|
||||
.or_else(|| bracket_and_arrow_shapes(&c))
|
||||
.or_else(|| flowchart_and_star_shapes(&c))
|
||||
.or_else(|| callout_wave_and_connector_shapes(&c))
|
||||
.or_else(|| extra::build(name, c.w, c.h, &|i| c.a(i)))
|
||||
}
|
||||
|
||||
fn rect_and_triangle_shapes(c: &Ctx) -> Option<Geometry> {
|
||||
let name = c.name;
|
||||
let w = c.w;
|
||||
let h = c.h;
|
||||
let ss = c.w.min(c.h);
|
||||
let full = Rect::new(0.0, 0.0, c.w, c.h);
|
||||
let a = |i: usize| c.a(i);
|
||||
let a = |i: usize| -> f64 {
|
||||
let v = adj.get(i).copied().or_else(|| defaults.get(i).copied()).unwrap_or(0.0);
|
||||
if v.is_finite() { v } else { 0.0 }
|
||||
};
|
||||
let ss = w.min(h);
|
||||
let full = Rect::new(0.0, 0.0, w, h);
|
||||
let mut b = Pb::new();
|
||||
let g = match c.name {
|
||||
let g = match name {
|
||||
"rect" | "textBox" | "flowChartProcess" => simple(corner_rect(w, h, [Corner::Square; 4]), full, w, h),
|
||||
"actionButtonBlank"
|
||||
| "actionButtonBackPrevious"
|
||||
@@ -183,20 +158,6 @@ fn rect_and_triangle_shapes(c: &Ctx) -> Option<Geometry> {
|
||||
sites: mid_sites(w, h),
|
||||
}
|
||||
}
|
||||
_ => return None,
|
||||
};
|
||||
Some(g)
|
||||
}
|
||||
|
||||
fn polygon_and_symbol_shapes(c: &Ctx) -> Option<Geometry> {
|
||||
let name = c.name;
|
||||
let w = c.w;
|
||||
let h = c.h;
|
||||
let ss = c.w.min(c.h);
|
||||
let full = Rect::new(0.0, 0.0, c.w, c.h);
|
||||
let a = |i: usize| c.a(i);
|
||||
let mut b = Pb::new();
|
||||
let g = match c.name {
|
||||
"pentagon" => simple(b.poly(&polygon_regular(5, w, h, -90.0)).take(), Rect::new(w * 0.2, h * 0.27, w * 0.8, h * 0.9), w, h),
|
||||
"heptagon" => simple(b.poly(&polygon_regular(7, w, h, -90.0)).take(), Rect::new(w * 0.16, h * 0.2, w * 0.84, h * 0.86), w, h),
|
||||
"decagon" => simple(b.poly(&polygon_regular(10, w, h, 0.0)).take(), Rect::new(w * 0.1, h * 0.17, w * 0.9, h * 0.83), w, h),
|
||||
@@ -587,20 +548,6 @@ fn polygon_and_symbol_shapes(c: &Ctx) -> Option<Geometry> {
|
||||
let dy4 = (ss * ss - g13 * g13).max(0.0).sqrt() * (h / 2.0) / ssf;
|
||||
simple(b.take(), Rect::new(g12 * w / ssf, h / 2.0 - dy4, g0w, h / 2.0 + dy4), w, h)
|
||||
}
|
||||
_ => return None,
|
||||
};
|
||||
Some(g)
|
||||
}
|
||||
|
||||
fn bracket_and_arrow_shapes(c: &Ctx) -> Option<Geometry> {
|
||||
let name = c.name;
|
||||
let w = c.w;
|
||||
let h = c.h;
|
||||
let ss = c.w.min(c.h);
|
||||
let full = Rect::new(0.0, 0.0, c.w, c.h);
|
||||
let a = |i: usize| c.a(i);
|
||||
let mut b = Pb::new();
|
||||
let g = match c.name {
|
||||
"cloud" | "cloudCallout" => cloud(name, w, h, a(0), a(1)),
|
||||
"bracketPair" => {
|
||||
let r = ss * a(0).clamp(0.0, 50000.0) / 1e5;
|
||||
@@ -879,20 +826,6 @@ fn bracket_and_arrow_shapes(c: &Ctx) -> Option<Geometry> {
|
||||
simple(b.poly(&pts).take(), Rect::from_points(Point::new(tr[0].0, tr[0].1), Point::new(tr[1].0, tr[1].1)).abs(), w, h)
|
||||
}
|
||||
"mathPlus" | "mathMinus" | "mathEqual" | "mathDivide" | "mathMultiply" | "mathNotEqual" => math(name, w, h, a(0), a(1), a(2)),
|
||||
_ => return None,
|
||||
};
|
||||
Some(g)
|
||||
}
|
||||
|
||||
fn flowchart_and_star_shapes(c: &Ctx) -> Option<Geometry> {
|
||||
let name = c.name;
|
||||
let w = c.w;
|
||||
let h = c.h;
|
||||
let ss = c.w.min(c.h);
|
||||
let full = Rect::new(0.0, 0.0, c.w, c.h);
|
||||
let a = |i: usize| c.a(i);
|
||||
let mut b = Pb::new();
|
||||
let g = match c.name {
|
||||
"flowChartPredefinedProcess" => {
|
||||
let r = Pb::new().poly(&[(0.0, 0.0), (w, 0.0), (w, h), (0.0, h)]).take();
|
||||
let mut l = Pb::new();
|
||||
@@ -1101,20 +1034,6 @@ fn flowchart_and_star_shapes(c: &Ctx) -> Option<Geometry> {
|
||||
);
|
||||
simple(b.poly(&pts).take(), Rect::new(w * 5372.0 / 21600.0, h * 6382.0 / 21600.0, w * 14640.0 / 21600.0, h * 15935.0 / 21600.0), w, h)
|
||||
}
|
||||
_ => return None,
|
||||
};
|
||||
Some(g)
|
||||
}
|
||||
|
||||
fn callout_wave_and_connector_shapes(c: &Ctx) -> Option<Geometry> {
|
||||
let name = c.name;
|
||||
let w = c.w;
|
||||
let h = c.h;
|
||||
let ss = c.w.min(c.h);
|
||||
let full = Rect::new(0.0, 0.0, c.w, c.h);
|
||||
let a = |i: usize| c.a(i);
|
||||
let mut b = Pb::new();
|
||||
let g = match c.name {
|
||||
"ribbon" | "ribbon2" => ribbon(name == "ribbon2", w, h, a(0), a(1)),
|
||||
"verticalScroll" | "horizontalScroll" => scroll(name == "verticalScroll", w, h, a(0)),
|
||||
"wave" | "doubleWave" => {
|
||||
@@ -1198,7 +1117,7 @@ fn callout_wave_and_connector_shapes(c: &Ctx) -> Option<Geometry> {
|
||||
let l = Pb::new().m(0.0, 0.0).c(x / 2.0, 0.0, x, h / 4.0, x, h / 2.0).c(x, h * 0.75, (x + w) / 2.0, h, w, h).take();
|
||||
Geometry { paths: vec![stroke_only(l)], text_rect: full, handles: vec![], sites: vec![Point::ZERO, Point::new(w, h)] }
|
||||
}
|
||||
_ => return None,
|
||||
_ => return extra::build(name, w, h, &a),
|
||||
};
|
||||
Some(g)
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
use super::catalog::{FillMode, Geometry, Handle, SubPath};
|
||||
use super::{Arc, BezPath, Point, Rect, Vec2};
|
||||
use super::*;
|
||||
|
||||
// ---------- path builder ----------
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
use super::builder::vis;
|
||||
use super::{BezPath, Deserialize, Point, Rect, Serialize, Vec2};
|
||||
use super::*;
|
||||
|
||||
/// How a sub-path is filled. `Lighten`/`Darken` variants shade the shape's fill (3-D looking faces).
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
use super::builder::{Corner, Pb, corner_rect, fill, mid_sites, shade, simple, stroke_only, tip_handle};
|
||||
use super::catalog::{FillMode, Geometry};
|
||||
use super::{Point, Rect};
|
||||
use super::*;
|
||||
|
||||
pub(super) fn wedge_rect(w: f64, h: f64, a0: f64, a1: f64, r: f64) -> Geometry {
|
||||
let tip = Point::new(w / 2.0 + w * a0 / 1e5, h / 2.0 + h * a1 / 1e5);
|
||||
|
||||
+63
-76
@@ -302,12 +302,71 @@ pub fn deblock_mb(pic: &mut PicState, addr: usize, mb_w: usize) {
|
||||
let top = usable(if my > 0 { Some(addr - mb_w) } else { None });
|
||||
let alpha_off = sq.alpha_offset;
|
||||
let beta_off = sq.beta_offset;
|
||||
let bs = boundary_strengths(pic, q, sq, [left, top]);
|
||||
let t8 = q.transform_8x8;
|
||||
// bS[dir][edge][segment]; dir 0 = vertical edges (x), 1 = horizontal edges (y)
|
||||
let mut bs = [[[0u8; 4]; 4]; 2];
|
||||
if q.kind.is_intra() {
|
||||
for dir in 0..2 {
|
||||
if (if dir == 0 { left } else { top }).is_some() {
|
||||
bs[dir][0] = [4; 4];
|
||||
}
|
||||
for e in 1..4 {
|
||||
if !(t8 && e % 2 == 1) {
|
||||
bs[dir][e] = [3; 4];
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
let qids = ref_ids8(q, sq);
|
||||
let q_uniform = (0..2).all(|l| {
|
||||
let r = q.ref_idx[l];
|
||||
r[1] == r[0] && r[2] == r[0] && r[3] == r[0] && q.mv[l].iter().all(|m| *m == q.mv[l][0])
|
||||
});
|
||||
let qm: [Motion; 16] = std::array::from_fn(|r| block_motion(q, &qids, r));
|
||||
for dir in 0..2 {
|
||||
let neighbor = if dir == 0 { left } else { top };
|
||||
if let Some(n) = neighbor {
|
||||
let p = &pic.mbs[n];
|
||||
if p.kind.is_intra() {
|
||||
bs[dir][0] = [4; 4];
|
||||
} else {
|
||||
let pids = ref_ids8(p, &pic.slices[p.slice_num as usize]);
|
||||
for k in 0..4 {
|
||||
let (rq, rp) = if dir == 0 { (k * 4, k * 4 + 3) } else { (k, 12 + k) };
|
||||
bs[dir][0][k] = if (p.nz_mask >> rp) & 1 != 0 || (q.nz_mask >> rq) & 1 != 0 {
|
||||
2
|
||||
} else {
|
||||
motion_bs(&block_motion(p, &pids, rp), &qm[rq])
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
if q_uniform && q.nz_mask == 0 {
|
||||
// one motion for the whole MB and no coefficients: all internal edges have bS 0
|
||||
continue;
|
||||
}
|
||||
for e in 1..4 {
|
||||
if t8 && e % 2 == 1 {
|
||||
continue;
|
||||
}
|
||||
for k in 0..4 {
|
||||
let (rq, rp) = if dir == 0 { (k * 4 + e, k * 4 + e - 1) } else { (e * 4 + k, e * 4 + k - 4) };
|
||||
bs[dir][e][k] = if (q.nz_mask >> rp) & 1 != 0 || (q.nz_mask >> rq) & 1 != 0 { 2 } else { motion_bs(&qm[rp], &qm[rq]) };
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
let qp_of = |st: &MbState| if st.kind == MbKind::IPcm { 0 } else { st.qp as i32 };
|
||||
let q_qp = qp_of(q);
|
||||
let q_qpc = q.qpc;
|
||||
let p_qp = [left.map(|n| (qp_of(&pic.mbs[n]), pic.mbs[n].qpc)), top.map(|n| (qp_of(&pic.mbs[n]), pic.mbs[n].qpc))];
|
||||
let offsets = (alpha_off, beta_off);
|
||||
let params = |qpp: i32, qpq: i32, b: [u8; 4]| {
|
||||
let qpav = (qpp + qpq + 1) >> 1;
|
||||
let index_a = (qpav + alpha_off).clamp(0, 51) as usize;
|
||||
let index_b = (qpav + beta_off).clamp(0, 51) as usize;
|
||||
let tc = |b: u8| if (1..4).contains(&b) { TC0[index_a][b as usize - 1] as i32 } else { 0 };
|
||||
EdgeParams { bs: b, tc0: [tc(b[0]), tc(b[1]), tc(b[2]), tc(b[3])], alpha: ALPHA[index_a] as i32, beta: BETA[index_b] as i32 }
|
||||
};
|
||||
let width = pic.planes.width;
|
||||
let cwidth = pic.planes.cwidth;
|
||||
// luma
|
||||
@@ -317,7 +376,7 @@ pub fn deblock_mb(pic: &mut PicState, addr: usize, mb_w: usize) {
|
||||
continue;
|
||||
}
|
||||
let qpp = if e == 0 { p_qp[dir].map(|p| p.0).unwrap_or(q_qp) } else { q_qp };
|
||||
let ep = edge_params(offsets, qpp, q_qp, bs[dir][e]);
|
||||
let ep = params(qpp, q_qp, bs[dir][e]);
|
||||
if ep.alpha == 0 || ep.beta == 0 {
|
||||
// filterSamplesFlag needs |p0 - q0| < alpha and |p1 - p0| < beta (8.7.2.2): no
|
||||
// sample of the edge changes (low QPs)
|
||||
@@ -339,7 +398,7 @@ pub fn deblock_mb(pic: &mut PicState, addr: usize, mb_w: usize) {
|
||||
continue;
|
||||
}
|
||||
let qpp = if e == 0 { p_qp[dir].map(|p| p.1[c]).unwrap_or(q_qpc[c]) } else { q_qpc[c] } as i32;
|
||||
let ep = edge_params(offsets, qpp, q_qpc[c] as i32, bs[dir][e]);
|
||||
let ep = params(qpp, q_qpc[c] as i32, bs[dir][e]);
|
||||
if ep.alpha == 0 || ep.beta == 0 {
|
||||
continue;
|
||||
}
|
||||
@@ -354,78 +413,6 @@ pub fn deblock_mb(pic: &mut PicState, addr: usize, mb_w: usize) {
|
||||
}
|
||||
}
|
||||
|
||||
/// Filter parameters of one edge from the neighbouring QPs and the slice's alpha/beta offsets.
|
||||
fn edge_params((alpha_off, beta_off): (i32, i32), qpp: i32, qpq: i32, b: [u8; 4]) -> EdgeParams {
|
||||
let qpav = (qpp + qpq + 1) >> 1;
|
||||
let index_a = (qpav + alpha_off).clamp(0, 51) as usize;
|
||||
let index_b = (qpav + beta_off).clamp(0, 51) as usize;
|
||||
let tc = |b: u8| if (1..4).contains(&b) { TC0[index_a][b as usize - 1] as i32 } else { 0 };
|
||||
EdgeParams { bs: b, tc0: [tc(b[0]), tc(b[1]), tc(b[2]), tc(b[3])], alpha: ALPHA[index_a] as i32, beta: BETA[index_b] as i32 }
|
||||
}
|
||||
|
||||
/// bS[dir][edge][segment]; dir 0 = vertical edges (x), 1 = horizontal edges (y). `neighbors` is
|
||||
/// `[left, top]`.
|
||||
fn boundary_strengths(pic: &PicState, q: &MbState, sq: &SliceInfo, neighbors: [Option<usize>; 2]) -> [[[u8; 4]; 4]; 2] {
|
||||
if q.kind.is_intra() { intra_strengths(q.transform_8x8, neighbors) } else { inter_strengths(pic, q, sq, neighbors) }
|
||||
}
|
||||
|
||||
fn intra_strengths(t8: bool, neighbors: [Option<usize>; 2]) -> [[[u8; 4]; 4]; 2] {
|
||||
let mut bs = [[[0u8; 4]; 4]; 2];
|
||||
for dir in 0..2 {
|
||||
if neighbors[dir].is_some() {
|
||||
bs[dir][0] = [4; 4];
|
||||
}
|
||||
for e in 1..4 {
|
||||
if !(t8 && e % 2 == 1) {
|
||||
bs[dir][e] = [3; 4];
|
||||
}
|
||||
}
|
||||
}
|
||||
bs
|
||||
}
|
||||
|
||||
fn inter_strengths(pic: &PicState, q: &MbState, sq: &SliceInfo, neighbors: [Option<usize>; 2]) -> [[[u8; 4]; 4]; 2] {
|
||||
let mut bs = [[[0u8; 4]; 4]; 2];
|
||||
let t8 = q.transform_8x8;
|
||||
let qids = ref_ids8(q, sq);
|
||||
let q_uniform = (0..2).all(|l| {
|
||||
let r = q.ref_idx[l];
|
||||
r[1] == r[0] && r[2] == r[0] && r[3] == r[0] && q.mv[l].iter().all(|m| *m == q.mv[l][0])
|
||||
});
|
||||
let qm: [Motion; 16] = std::array::from_fn(|r| block_motion(q, &qids, r));
|
||||
for dir in 0..2 {
|
||||
if let Some(n) = neighbors[dir] {
|
||||
bs[dir][0] = edge_strengths_to_neighbor(&pic.mbs[n], &pic.slices[pic.mbs[n].slice_num as usize], q, &qm, dir);
|
||||
}
|
||||
if q_uniform && q.nz_mask == 0 {
|
||||
// one motion for the whole MB and no coefficients: all internal edges have bS 0
|
||||
continue;
|
||||
}
|
||||
for e in 1..4 {
|
||||
if t8 && e % 2 == 1 {
|
||||
continue;
|
||||
}
|
||||
for k in 0..4 {
|
||||
let (rq, rp) = if dir == 0 { (k * 4 + e, k * 4 + e - 1) } else { (e * 4 + k, e * 4 + k - 4) };
|
||||
bs[dir][e][k] = if (q.nz_mask >> rp) & 1 != 0 || (q.nz_mask >> rq) & 1 != 0 { 2 } else { motion_bs(&qm[rp], &qm[rq]) };
|
||||
}
|
||||
}
|
||||
}
|
||||
bs
|
||||
}
|
||||
|
||||
/// bS of the macroblock edge shared with neighbour `p` (left for `dir` 0, top for `dir` 1).
|
||||
fn edge_strengths_to_neighbor(p: &MbState, p_slice: &SliceInfo, q: &MbState, qm: &[Motion; 16], dir: usize) -> [u8; 4] {
|
||||
if p.kind.is_intra() {
|
||||
return [4; 4];
|
||||
}
|
||||
let pids = ref_ids8(p, p_slice);
|
||||
std::array::from_fn(|k| {
|
||||
let (rq, rp) = if dir == 0 { (k * 4, k * 4 + 3) } else { (k, 12 + k) };
|
||||
if (p.nz_mask >> rp) & 1 != 0 || (q.nz_mask >> rq) & 1 != 0 { 2 } else { motion_bs(&block_motion(p, &pids, rp), &qm[rq]) }
|
||||
})
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
+116
-92
@@ -61,102 +61,126 @@ fn load_edge(plane: &[u8], stride: usize, x: usize, y: usize, n: usize, a: Avail
|
||||
|
||||
/// Directional / DC prediction shared by Intra_4x4 and Intra_8x8 (8.3.1.2.x, 8.3.2.2.x).
|
||||
fn pred_nxn(mode: u8, e: &Edge, n: i32, a: Avail, out: &mut [u8], stride: usize) {
|
||||
let mut fill = |value: &dyn Fn(i32, i32) -> i32| {
|
||||
for y in 0..n {
|
||||
for x in 0..n {
|
||||
out[y as usize * stride + x as usize] = value(x, y) as u8;
|
||||
let mut put = |x: i32, y: i32, v: i32| out[y as usize * stride + x as usize] = v as u8;
|
||||
match mode {
|
||||
0 => {
|
||||
for y in 0..n {
|
||||
for x in 0..n {
|
||||
put(x, y, e.t(x));
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
match mode {
|
||||
0 => fill(&|x, _| e.t(x)),
|
||||
1 => fill(&|_, y| e.l(y)),
|
||||
1 => {
|
||||
for y in 0..n {
|
||||
for x in 0..n {
|
||||
put(x, y, e.l(y));
|
||||
}
|
||||
}
|
||||
}
|
||||
2 => {
|
||||
let dc = dc_value(e, n, a);
|
||||
fill(&|_, _| dc);
|
||||
let shift = if n == 4 { 2 } else { 3 };
|
||||
let st: i32 = (0..n).map(|x| e.t(x)).sum();
|
||||
let sl: i32 = (0..n).map(|y| e.l(y)).sum();
|
||||
let dc = match (a.top, a.left) {
|
||||
(true, true) => (st + sl + n) >> (shift + 1),
|
||||
(false, true) => (sl + (n >> 1)) >> shift,
|
||||
(true, false) => (st + (n >> 1)) >> shift,
|
||||
_ => 128,
|
||||
};
|
||||
for y in 0..n {
|
||||
for x in 0..n {
|
||||
put(x, y, dc);
|
||||
}
|
||||
}
|
||||
}
|
||||
3 => {
|
||||
for y in 0..n {
|
||||
for x in 0..n {
|
||||
let v = if x == n - 1 && y == n - 1 {
|
||||
(e.t(2 * n - 2) + 3 * e.t(2 * n - 1) + 2) >> 2
|
||||
} else {
|
||||
(e.t(x + y) + 2 * e.t(x + y + 1) + e.t(x + y + 2) + 2) >> 2
|
||||
};
|
||||
put(x, y, v);
|
||||
}
|
||||
}
|
||||
}
|
||||
4 => {
|
||||
for y in 0..n {
|
||||
for x in 0..n {
|
||||
let v = if x > y {
|
||||
(e.t(x - y - 2) + 2 * e.t(x - y - 1) + e.t(x - y) + 2) >> 2
|
||||
} else if x < y {
|
||||
(e.l(y - x - 2) + 2 * e.l(y - x - 1) + e.l(y - x) + 2) >> 2
|
||||
} else {
|
||||
(e.t(0) + 2 * e.tl + e.l(0) + 2) >> 2
|
||||
};
|
||||
put(x, y, v);
|
||||
}
|
||||
}
|
||||
}
|
||||
5 => {
|
||||
for y in 0..n {
|
||||
for x in 0..n {
|
||||
let z = 2 * x - y;
|
||||
let v = if z >= 0 && z % 2 == 0 {
|
||||
(e.t(x - (y >> 1) - 1) + e.t(x - (y >> 1)) + 1) >> 1
|
||||
} else if z > 0 {
|
||||
(e.t(x - (y >> 1) - 2) + 2 * e.t(x - (y >> 1) - 1) + e.t(x - (y >> 1)) + 2) >> 2
|
||||
} else if z == -1 {
|
||||
(e.l(0) + 2 * e.tl + e.t(0) + 2) >> 2
|
||||
} else {
|
||||
(e.l(y - 2 * x - 1) + 2 * e.l(y - 2 * x - 2) + e.l(y - 2 * x - 3) + 2) >> 2
|
||||
};
|
||||
put(x, y, v);
|
||||
}
|
||||
}
|
||||
}
|
||||
6 => {
|
||||
for y in 0..n {
|
||||
for x in 0..n {
|
||||
let z = 2 * y - x;
|
||||
let v = if z >= 0 && z % 2 == 0 {
|
||||
(e.l(y - (x >> 1) - 1) + e.l(y - (x >> 1)) + 1) >> 1
|
||||
} else if z > 0 {
|
||||
(e.l(y - (x >> 1) - 2) + 2 * e.l(y - (x >> 1) - 1) + e.l(y - (x >> 1)) + 2) >> 2
|
||||
} else if z == -1 {
|
||||
(e.l(0) + 2 * e.tl + e.t(0) + 2) >> 2
|
||||
} else {
|
||||
(e.t(x - 2 * y - 1) + 2 * e.t(x - 2 * y - 2) + e.t(x - 2 * y - 3) + 2) >> 2
|
||||
};
|
||||
put(x, y, v);
|
||||
}
|
||||
}
|
||||
}
|
||||
7 => {
|
||||
for y in 0..n {
|
||||
for x in 0..n {
|
||||
let i = x + (y >> 1);
|
||||
let v = if y % 2 == 0 { (e.t(i) + e.t(i + 1) + 1) >> 1 } else { (e.t(i) + 2 * e.t(i + 1) + e.t(i + 2) + 2) >> 2 };
|
||||
put(x, y, v);
|
||||
}
|
||||
}
|
||||
}
|
||||
_ => {
|
||||
let zmax = 2 * n - 3;
|
||||
for y in 0..n {
|
||||
for x in 0..n {
|
||||
let z = x + 2 * y;
|
||||
let i = y + (x >> 1);
|
||||
let v = if z < zmax && z % 2 == 0 {
|
||||
(e.l(i) + e.l(i + 1) + 1) >> 1
|
||||
} else if z < zmax {
|
||||
(e.l(i) + 2 * e.l(i + 1) + e.l(i + 2) + 2) >> 2
|
||||
} else if z == zmax {
|
||||
(e.l(n - 2) + 3 * e.l(n - 1) + 2) >> 2
|
||||
} else {
|
||||
e.l(n - 1)
|
||||
};
|
||||
put(x, y, v);
|
||||
}
|
||||
}
|
||||
}
|
||||
3 => fill(&|x, y| diagonal_down_left(e, n, x, y)),
|
||||
4 => fill(&|x, y| diagonal_down_right(e, x, y)),
|
||||
5 => fill(&|x, y| vertical_right(e, x, y)),
|
||||
6 => fill(&|x, y| horizontal_down(e, x, y)),
|
||||
7 => fill(&|x, y| vertical_left(e, x, y)),
|
||||
_ => fill(&|x, y| horizontal_up(e, n, x, y)),
|
||||
}
|
||||
}
|
||||
|
||||
fn dc_value(e: &Edge, n: i32, a: Avail) -> i32 {
|
||||
let shift = if n == 4 { 2 } else { 3 };
|
||||
let st: i32 = (0..n).map(|x| e.t(x)).sum();
|
||||
let sl: i32 = (0..n).map(|y| e.l(y)).sum();
|
||||
match (a.top, a.left) {
|
||||
(true, true) => (st + sl + n) >> (shift + 1),
|
||||
(false, true) => (sl + (n >> 1)) >> shift,
|
||||
(true, false) => (st + (n >> 1)) >> shift,
|
||||
_ => 128,
|
||||
}
|
||||
}
|
||||
|
||||
fn diagonal_down_left(e: &Edge, n: i32, x: i32, y: i32) -> i32 {
|
||||
if x == n - 1 && y == n - 1 {
|
||||
(e.t(2 * n - 2) + 3 * e.t(2 * n - 1) + 2) >> 2
|
||||
} else {
|
||||
(e.t(x + y) + 2 * e.t(x + y + 1) + e.t(x + y + 2) + 2) >> 2
|
||||
}
|
||||
}
|
||||
|
||||
fn diagonal_down_right(e: &Edge, x: i32, y: i32) -> i32 {
|
||||
if x > y {
|
||||
(e.t(x - y - 2) + 2 * e.t(x - y - 1) + e.t(x - y) + 2) >> 2
|
||||
} else if x < y {
|
||||
(e.l(y - x - 2) + 2 * e.l(y - x - 1) + e.l(y - x) + 2) >> 2
|
||||
} else {
|
||||
(e.t(0) + 2 * e.tl + e.l(0) + 2) >> 2
|
||||
}
|
||||
}
|
||||
|
||||
fn vertical_right(e: &Edge, x: i32, y: i32) -> i32 {
|
||||
let z = 2 * x - y;
|
||||
if z >= 0 && z % 2 == 0 {
|
||||
(e.t(x - (y >> 1) - 1) + e.t(x - (y >> 1)) + 1) >> 1
|
||||
} else if z > 0 {
|
||||
(e.t(x - (y >> 1) - 2) + 2 * e.t(x - (y >> 1) - 1) + e.t(x - (y >> 1)) + 2) >> 2
|
||||
} else if z == -1 {
|
||||
(e.l(0) + 2 * e.tl + e.t(0) + 2) >> 2
|
||||
} else {
|
||||
(e.l(y - 2 * x - 1) + 2 * e.l(y - 2 * x - 2) + e.l(y - 2 * x - 3) + 2) >> 2
|
||||
}
|
||||
}
|
||||
|
||||
fn horizontal_down(e: &Edge, x: i32, y: i32) -> i32 {
|
||||
let z = 2 * y - x;
|
||||
if z >= 0 && z % 2 == 0 {
|
||||
(e.l(y - (x >> 1) - 1) + e.l(y - (x >> 1)) + 1) >> 1
|
||||
} else if z > 0 {
|
||||
(e.l(y - (x >> 1) - 2) + 2 * e.l(y - (x >> 1) - 1) + e.l(y - (x >> 1)) + 2) >> 2
|
||||
} else if z == -1 {
|
||||
(e.l(0) + 2 * e.tl + e.t(0) + 2) >> 2
|
||||
} else {
|
||||
(e.t(x - 2 * y - 1) + 2 * e.t(x - 2 * y - 2) + e.t(x - 2 * y - 3) + 2) >> 2
|
||||
}
|
||||
}
|
||||
|
||||
fn vertical_left(e: &Edge, x: i32, y: i32) -> i32 {
|
||||
let i = x + (y >> 1);
|
||||
if y % 2 == 0 { (e.t(i) + e.t(i + 1) + 1) >> 1 } else { (e.t(i) + 2 * e.t(i + 1) + e.t(i + 2) + 2) >> 2 }
|
||||
}
|
||||
|
||||
fn horizontal_up(e: &Edge, n: i32, x: i32, y: i32) -> i32 {
|
||||
let zmax = 2 * n - 3;
|
||||
let z = x + 2 * y;
|
||||
let i = y + (x >> 1);
|
||||
if z < zmax && z % 2 == 0 {
|
||||
(e.l(i) + e.l(i + 1) + 1) >> 1
|
||||
} else if z < zmax {
|
||||
(e.l(i) + 2 * e.l(i + 1) + e.l(i + 2) + 2) >> 2
|
||||
} else if z == zmax {
|
||||
(e.l(n - 2) + 3 * e.l(n - 1) + 2) >> 2
|
||||
} else {
|
||||
e.l(n - 1)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+96
-110
@@ -142,107 +142,6 @@ pub struct SliceHeader {
|
||||
pub header_bits: usize,
|
||||
}
|
||||
|
||||
/// ref_pic_list_modification() for list 0 and list 1.
|
||||
fn read_ref_pic_list_mods(r: &mut BitReader, slice_type: SliceType) -> Result<[Vec<RefPicListMod>; 2]> {
|
||||
let mut ref_pic_list_mod = [Vec::new(), Vec::new()];
|
||||
for (list, mods) in ref_pic_list_mod.iter_mut().enumerate() {
|
||||
let present = if list == 0 { !slice_type.is_intra() } else { slice_type.is_b() };
|
||||
if present && r.read_flag()? {
|
||||
loop {
|
||||
let idc = r.read_ue()?;
|
||||
ensure!(idc <= 5, "modification_of_pic_nums_idc out of range");
|
||||
if idc == 3 {
|
||||
break;
|
||||
}
|
||||
let value = r.read_ue()?;
|
||||
mods.push(RefPicListMod { idc, value });
|
||||
ensure!(mods.len() <= 66, "too many ref_pic_list_modification entries");
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(ref_pic_list_mod)
|
||||
}
|
||||
|
||||
/// pred_weight_table().
|
||||
fn read_pred_weight_table(r: &mut BitReader, sps: &Sps, slice_type: SliceType, num_ref_idx_active: [u32; 2]) -> Result<PredWeightTable> {
|
||||
let luma_log2_denom = r.read_ue()?;
|
||||
ensure!(luma_log2_denom <= 7, "luma_log2_weight_denom out of range");
|
||||
let has_chroma = sps.chroma_array_type() != 0;
|
||||
let mut chroma_log2_denom = 0;
|
||||
if has_chroma {
|
||||
chroma_log2_denom = r.read_ue()?;
|
||||
ensure!(chroma_log2_denom <= 7, "chroma_log2_weight_denom out of range");
|
||||
}
|
||||
let mut lists = [Vec::new(), Vec::new()];
|
||||
let nlists = if slice_type.is_b() { 2 } else { 1 };
|
||||
for (l, entries) in lists.iter_mut().enumerate().take(nlists) {
|
||||
for _ in 0..num_ref_idx_active[l] {
|
||||
entries.push(read_weight_entry(r, has_chroma, luma_log2_denom, chroma_log2_denom)?);
|
||||
}
|
||||
}
|
||||
let [l0, l1] = lists;
|
||||
Ok(PredWeightTable { luma_log2_denom, chroma_log2_denom, l0, l1 })
|
||||
}
|
||||
|
||||
fn read_weight_entry(r: &mut BitReader, has_chroma: bool, luma_log2_denom: u32, chroma_log2_denom: u32) -> Result<WeightEntry> {
|
||||
let mut e = WeightEntry { luma_weight: 1 << luma_log2_denom, chroma_weight: [1 << chroma_log2_denom; 2], ..Default::default() };
|
||||
if r.read_flag()? {
|
||||
e.luma_flag = true;
|
||||
e.luma_weight = r.read_se()?;
|
||||
e.luma_offset = r.read_se()?;
|
||||
ensure!((-128..=127).contains(&e.luma_weight) && (-128..=127).contains(&e.luma_offset), "luma weight out of range");
|
||||
}
|
||||
if has_chroma && r.read_flag()? {
|
||||
e.chroma_flag = true;
|
||||
for j in 0..2 {
|
||||
e.chroma_weight[j] = r.read_se()?;
|
||||
e.chroma_offset[j] = r.read_se()?;
|
||||
ensure!((-128..=127).contains(&e.chroma_weight[j]) && (-128..=127).contains(&e.chroma_offset[j]), "chroma weight out of range");
|
||||
}
|
||||
}
|
||||
Ok(e)
|
||||
}
|
||||
|
||||
/// The memory_management_control_operation list of dec_ref_pic_marking().
|
||||
fn read_mmcos(r: &mut BitReader) -> Result<Vec<Mmco>> {
|
||||
let mut mmcos = Vec::new();
|
||||
loop {
|
||||
let op = r.read_ue()?;
|
||||
ensure!(op <= 6, "memory_management_control_operation out of range");
|
||||
if op == 0 {
|
||||
return Ok(mmcos);
|
||||
}
|
||||
let mut m = Mmco { op, difference_of_pic_nums_minus1: 0, long_term_pic_num: 0, long_term_frame_idx: 0, max_long_term_frame_idx_plus1: 0 };
|
||||
if op == 1 || op == 3 {
|
||||
m.difference_of_pic_nums_minus1 = r.read_ue()?;
|
||||
}
|
||||
if op == 2 {
|
||||
m.long_term_pic_num = r.read_ue()?;
|
||||
}
|
||||
if op == 3 || op == 6 {
|
||||
m.long_term_frame_idx = r.read_ue()?;
|
||||
}
|
||||
if op == 4 {
|
||||
m.max_long_term_frame_idx_plus1 = r.read_ue()?;
|
||||
}
|
||||
mmcos.push(m);
|
||||
ensure!(mmcos.len() <= 66, "too many MMCOs");
|
||||
}
|
||||
}
|
||||
|
||||
/// Deblocking filter control: (disable_deblocking_filter_idc, alpha_c0_offset_div2, beta_offset_div2).
|
||||
fn read_deblocking_params(r: &mut BitReader) -> Result<(u32, i32, i32)> {
|
||||
let disable_deblocking_filter_idc = r.read_ue()?;
|
||||
ensure!(disable_deblocking_filter_idc <= 2, "disable_deblocking_filter_idc out of range");
|
||||
if disable_deblocking_filter_idc == 1 {
|
||||
return Ok((disable_deblocking_filter_idc, 0, 0));
|
||||
}
|
||||
let alpha = r.read_se()?;
|
||||
let beta = r.read_se()?;
|
||||
ensure!((-6..=6).contains(&alpha) && (-6..=6).contains(&beta), "deblocking offsets out of range");
|
||||
Ok((disable_deblocking_filter_idc, alpha, beta))
|
||||
}
|
||||
|
||||
impl SliceHeader {
|
||||
pub fn qp(&self, pps: &Pps) -> i32 {
|
||||
pps.pic_init_qp.saturating_add(self.slice_qp_delta)
|
||||
@@ -315,12 +214,59 @@ impl SliceHeader {
|
||||
num_ref_idx_active[0] = 0;
|
||||
}
|
||||
ensure!(nal.nal_unit_type != 20 && nal.nal_unit_type != 21, "MVC slices are not supported");
|
||||
let ref_pic_list_mod = read_ref_pic_list_mods(&mut r, slice_type)?;
|
||||
let pred_weight_table = if (pps.weighted_pred && slice_type.is_p()) || (pps.weighted_bipred_idc == 1 && slice_type.is_b()) {
|
||||
Some(read_pred_weight_table(&mut r, sps, slice_type, num_ref_idx_active)?)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
let mut ref_pic_list_mod = [Vec::new(), Vec::new()];
|
||||
for (list, mods) in ref_pic_list_mod.iter_mut().enumerate() {
|
||||
let present = if list == 0 { !slice_type.is_intra() } else { slice_type.is_b() };
|
||||
if present && r.read_flag()? {
|
||||
loop {
|
||||
let idc = r.read_ue()?;
|
||||
ensure!(idc <= 5, "modification_of_pic_nums_idc out of range");
|
||||
if idc == 3 {
|
||||
break;
|
||||
}
|
||||
let value = r.read_ue()?;
|
||||
mods.push(RefPicListMod { idc, value });
|
||||
ensure!(mods.len() <= 66, "too many ref_pic_list_modification entries");
|
||||
}
|
||||
}
|
||||
}
|
||||
let mut pred_weight_table = None;
|
||||
if (pps.weighted_pred && slice_type.is_p()) || (pps.weighted_bipred_idc == 1 && slice_type.is_b()) {
|
||||
let luma_log2_denom = r.read_ue()?;
|
||||
ensure!(luma_log2_denom <= 7, "luma_log2_weight_denom out of range");
|
||||
let mut chroma_log2_denom = 0;
|
||||
if sps.chroma_array_type() != 0 {
|
||||
chroma_log2_denom = r.read_ue()?;
|
||||
ensure!(chroma_log2_denom <= 7, "chroma_log2_weight_denom out of range");
|
||||
}
|
||||
let mut lists = [Vec::new(), Vec::new()];
|
||||
let nlists = if slice_type.is_b() { 2 } else { 1 };
|
||||
for (l, entries) in lists.iter_mut().enumerate().take(nlists) {
|
||||
for _ in 0..num_ref_idx_active[l] {
|
||||
let mut e = WeightEntry { luma_weight: 1 << luma_log2_denom, chroma_weight: [1 << chroma_log2_denom; 2], ..Default::default() };
|
||||
if r.read_flag()? {
|
||||
e.luma_flag = true;
|
||||
e.luma_weight = r.read_se()?;
|
||||
e.luma_offset = r.read_se()?;
|
||||
ensure!((-128..=127).contains(&e.luma_weight) && (-128..=127).contains(&e.luma_offset), "luma weight out of range");
|
||||
}
|
||||
if sps.chroma_array_type() != 0 && r.read_flag()? {
|
||||
e.chroma_flag = true;
|
||||
for j in 0..2 {
|
||||
e.chroma_weight[j] = r.read_se()?;
|
||||
e.chroma_offset[j] = r.read_se()?;
|
||||
ensure!(
|
||||
(-128..=127).contains(&e.chroma_weight[j]) && (-128..=127).contains(&e.chroma_offset[j]),
|
||||
"chroma weight out of range"
|
||||
);
|
||||
}
|
||||
}
|
||||
entries.push(e);
|
||||
}
|
||||
}
|
||||
let [l0, l1] = lists;
|
||||
pred_weight_table = Some(PredWeightTable { luma_log2_denom, chroma_log2_denom, l0, l1 });
|
||||
}
|
||||
let mut no_output_of_prior_pics = false;
|
||||
let mut long_term_reference = false;
|
||||
let mut adaptive_ref_pic_marking = false;
|
||||
@@ -332,7 +278,34 @@ impl SliceHeader {
|
||||
} else {
|
||||
adaptive_ref_pic_marking = r.read_flag()?;
|
||||
if adaptive_ref_pic_marking {
|
||||
mmcos = read_mmcos(&mut r)?;
|
||||
loop {
|
||||
let op = r.read_ue()?;
|
||||
ensure!(op <= 6, "memory_management_control_operation out of range");
|
||||
if op == 0 {
|
||||
break;
|
||||
}
|
||||
let mut m = Mmco {
|
||||
op,
|
||||
difference_of_pic_nums_minus1: 0,
|
||||
long_term_pic_num: 0,
|
||||
long_term_frame_idx: 0,
|
||||
max_long_term_frame_idx_plus1: 0,
|
||||
};
|
||||
if op == 1 || op == 3 {
|
||||
m.difference_of_pic_nums_minus1 = r.read_ue()?;
|
||||
}
|
||||
if op == 2 {
|
||||
m.long_term_pic_num = r.read_ue()?;
|
||||
}
|
||||
if op == 3 || op == 6 {
|
||||
m.long_term_frame_idx = r.read_ue()?;
|
||||
}
|
||||
if op == 4 {
|
||||
m.max_long_term_frame_idx_plus1 = r.read_ue()?;
|
||||
}
|
||||
mmcos.push(m);
|
||||
ensure!(mmcos.len() <= 66, "too many MMCOs");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -352,8 +325,21 @@ impl SliceHeader {
|
||||
}
|
||||
slice_qs_delta = r.read_se()?;
|
||||
}
|
||||
let (disable_deblocking_filter_idc, slice_alpha_c0_offset_div2, slice_beta_offset_div2) =
|
||||
if pps.deblocking_filter_control_present { read_deblocking_params(&mut r)? } else { (0, 0, 0) };
|
||||
let mut disable_deblocking_filter_idc = 0;
|
||||
let mut slice_alpha_c0_offset_div2 = 0;
|
||||
let mut slice_beta_offset_div2 = 0;
|
||||
if pps.deblocking_filter_control_present {
|
||||
disable_deblocking_filter_idc = r.read_ue()?;
|
||||
ensure!(disable_deblocking_filter_idc <= 2, "disable_deblocking_filter_idc out of range");
|
||||
if disable_deblocking_filter_idc != 1 {
|
||||
slice_alpha_c0_offset_div2 = r.read_se()?;
|
||||
slice_beta_offset_div2 = r.read_se()?;
|
||||
ensure!(
|
||||
(-6..=6).contains(&slice_alpha_c0_offset_div2) && (-6..=6).contains(&slice_beta_offset_div2),
|
||||
"deblocking offsets out of range"
|
||||
);
|
||||
}
|
||||
}
|
||||
let mut slice_group_change_cycle = 0;
|
||||
if pps.num_slice_groups > 1 && (3..=5).contains(&pps.slice_group_map_type) {
|
||||
// Ceil(Log2(PicSizeInMapUnits / SliceGroupChangeRate + 1)); rate is not retained, so read
|
||||
|
||||
+147
-190
@@ -154,194 +154,6 @@ impl SliceHeader {
|
||||
(st + lt) as u32
|
||||
}
|
||||
|
||||
/// Fields present only in independent slice segments.
|
||||
fn parse_independent_fields(&mut self, r: &mut BitReader, nal: NalHeader, pps: &Pps, sps: &Sps) -> Result<()> {
|
||||
r.skip(pps.num_extra_slice_header_bits as usize)?;
|
||||
let st = r.read_ue()?;
|
||||
self.slice_type = match st {
|
||||
0 => SliceType::B,
|
||||
1 => SliceType::P,
|
||||
2 => SliceType::I,
|
||||
_ => return invalid(format!("slice_type {st}")),
|
||||
};
|
||||
if nal.is_irap() {
|
||||
ensure!(self.slice_type == SliceType::I, "IRAP picture with inter slice");
|
||||
}
|
||||
if pps.output_flag_present {
|
||||
self.pic_output = r.read_flag()?;
|
||||
}
|
||||
if sps.separate_colour_plane {
|
||||
r.skip(2)?;
|
||||
}
|
||||
if !nal.is_idr() {
|
||||
self.parse_reference_picture_set(r, sps)?;
|
||||
}
|
||||
if sps.sao {
|
||||
self.sao_luma = r.read_flag()?;
|
||||
if sps.chroma_array_type() != 0 {
|
||||
self.sao_chroma = r.read_flag()?;
|
||||
}
|
||||
}
|
||||
if self.slice_type != SliceType::I {
|
||||
self.parse_inter_params(r, pps, sps)?;
|
||||
}
|
||||
self.parse_qp_and_deblocking(r, pps, sps)
|
||||
}
|
||||
|
||||
/// POC LSB, short-term and long-term reference picture sets and the temporal MVP flag.
|
||||
fn parse_reference_picture_set(&mut self, r: &mut BitReader, sps: &Sps) -> Result<()> {
|
||||
self.poc_lsb = r.read_bits(sps.log2_max_poc_lsb)?;
|
||||
if !r.read_flag()? {
|
||||
let start = r.position();
|
||||
self.st_rps = StRps::parse(r, sps.st_rps.len(), &sps.st_rps, sps.st_rps.len())?;
|
||||
self.st_rps_bits = r.position() - start;
|
||||
} else {
|
||||
ensure!(!sps.st_rps.is_empty(), "no short-term RPS in the SPS");
|
||||
let idx = if sps.st_rps.len() > 1 { r.read_bits(ceil_log2(sps.st_rps.len() as u32))? as usize } else { 0 };
|
||||
ensure!(idx < sps.st_rps.len(), "short_term_ref_pic_set_idx out of range");
|
||||
self.st_rps = sps.st_rps[idx].clone();
|
||||
}
|
||||
if sps.long_term_refs_present {
|
||||
self.parse_long_term_pics(r, sps)?;
|
||||
}
|
||||
if sps.temporal_mvp {
|
||||
self.temporal_mvp = r.read_flag()?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn parse_long_term_pics(&mut self, r: &mut BitReader, sps: &Sps) -> Result<()> {
|
||||
let num_lt_sps = if !sps.lt_ref_pics.is_empty() { r.read_ue()? } else { 0 };
|
||||
ensure!(num_lt_sps as usize <= sps.lt_ref_pics.len(), "num_long_term_sps out of range");
|
||||
let num_lt_pics = r.read_ue()?;
|
||||
ensure!(num_lt_sps + num_lt_pics <= 32, "too many long-term pictures");
|
||||
let mut msb_acc = 0u32;
|
||||
for i in 0..num_lt_sps + num_lt_pics {
|
||||
let (lsb, used) = if i < num_lt_sps {
|
||||
let idx = if sps.lt_ref_pics.len() > 1 { r.read_bits(ceil_log2(sps.lt_ref_pics.len() as u32))? } else { 0 };
|
||||
ensure!((idx as usize) < sps.lt_ref_pics.len(), "lt_idx_sps out of range");
|
||||
sps.lt_ref_pics[idx as usize]
|
||||
} else {
|
||||
(r.read_bits(sps.log2_max_poc_lsb)?, r.read_flag()?)
|
||||
};
|
||||
let msb_present = r.read_flag()?;
|
||||
let cycle = if msb_present { r.read_ue()? } else { 0 };
|
||||
// DeltaPocMsbCycleLt accumulates within each of the two groups (7-52)
|
||||
if i == 0 || i == num_lt_sps {
|
||||
msb_acc = cycle;
|
||||
} else {
|
||||
msb_acc = msb_acc.wrapping_add(cycle);
|
||||
}
|
||||
self.lt.push((lsb, used, msb_present, msb_acc));
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Reference index counts, list modification, collocated picture, prediction weights and merge candidates (P and B slices).
|
||||
fn parse_inter_params(&mut self, r: &mut BitReader, pps: &Pps, sps: &Sps) -> Result<()> {
|
||||
self.num_ref_idx = [pps.num_ref_idx_l0_default, if self.is_b() { pps.num_ref_idx_l1_default } else { 0 }];
|
||||
if r.read_flag()? {
|
||||
self.num_ref_idx[0] = r.read_ue_plus(1)?;
|
||||
if self.is_b() {
|
||||
self.num_ref_idx[1] = r.read_ue_plus(1)?;
|
||||
}
|
||||
}
|
||||
ensure!(self.num_ref_idx[0] <= 15 && self.num_ref_idx[1] <= 15, "num_ref_idx_active out of range");
|
||||
let total = self.num_pic_total_curr();
|
||||
ensure!(total > 0, "inter slice without reference pictures in the RPS");
|
||||
if pps.lists_modification_present && total > 1 {
|
||||
self.parse_list_entries(r, total)?;
|
||||
}
|
||||
if self.is_b() {
|
||||
self.mvd_l1_zero = r.read_flag()?;
|
||||
}
|
||||
if pps.cabac_init_present {
|
||||
self.cabac_init_flag = r.read_flag()?;
|
||||
}
|
||||
if self.temporal_mvp {
|
||||
self.parse_collocated(r)?;
|
||||
}
|
||||
if (pps.weighted_pred && self.slice_type == SliceType::P) || (pps.weighted_bipred && self.is_b()) {
|
||||
self.pwt = Some(parse_pwt(r, self, sps)?);
|
||||
}
|
||||
let five_minus = r.read_ue()?;
|
||||
ensure!(five_minus <= 4, "five_minus_max_num_merge_cand out of range");
|
||||
self.max_num_merge_cand = 5 - five_minus;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn parse_list_entries(&mut self, r: &mut BitReader, total: u32) -> Result<()> {
|
||||
let bits = ceil_log2(total);
|
||||
for l in 0..if self.is_b() { 2 } else { 1 } {
|
||||
if r.read_flag()? {
|
||||
let mut e = Vec::new();
|
||||
for _ in 0..self.num_ref_idx[l] {
|
||||
let v = r.read_bits(bits)?;
|
||||
ensure!(v < total, "list_entry out of range");
|
||||
e.push(v);
|
||||
}
|
||||
self.list_entry[l] = Some(e);
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn parse_collocated(&mut self, r: &mut BitReader) -> Result<()> {
|
||||
if self.is_b() {
|
||||
self.collocated_from_l0 = r.read_flag()?;
|
||||
}
|
||||
let l = if self.collocated_from_l0 { 0 } else { 1 };
|
||||
if self.num_ref_idx[l] > 1 {
|
||||
self.collocated_ref_idx = r.read_ue()?;
|
||||
ensure!(self.collocated_ref_idx < self.num_ref_idx[l], "collocated_ref_idx out of range");
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Slice QP delta, chroma QP offsets and the deblocking / loop filter overrides.
|
||||
fn parse_qp_and_deblocking(&mut self, r: &mut BitReader, pps: &Pps, sps: &Sps) -> Result<()> {
|
||||
self.qp_delta = r.read_se()?;
|
||||
let qp = pps.init_qp.checked_add(self.qp_delta).ok_or_else(|| crate::Error::Invalid("SliceQpY out of range".into()))?;
|
||||
let qp_bd = 6 * (sps.bit_depth_luma as i32 - 8);
|
||||
ensure!((-qp_bd..=51).contains(&qp), "SliceQpY {qp} out of range");
|
||||
if pps.slice_chroma_qp_offsets_present {
|
||||
self.cb_qp_offset = r.read_se()?;
|
||||
self.cr_qp_offset = r.read_se()?;
|
||||
ensure!((-12..=12).contains(&self.cb_qp_offset) && (-12..=12).contains(&self.cr_qp_offset), "slice chroma QP offset out of range");
|
||||
}
|
||||
let override_flag = if pps.deblocking_override_enabled { r.read_flag()? } else { false };
|
||||
if override_flag {
|
||||
self.deblocking_disabled = r.read_flag()?;
|
||||
if !self.deblocking_disabled {
|
||||
self.beta_offset_div2 = r.read_se()?;
|
||||
self.tc_offset_div2 = r.read_se()?;
|
||||
ensure!((-6..=6).contains(&self.beta_offset_div2) && (-6..=6).contains(&self.tc_offset_div2), "deblocking offsets out of range");
|
||||
}
|
||||
}
|
||||
if pps.loop_filter_across_slices && (self.sao_luma || self.sao_chroma || !self.deblocking_disabled) {
|
||||
self.loop_filter_across_slices = r.read_flag()?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// entry_point_offset list (tiles / wavefront substreams).
|
||||
fn parse_entry_points(&mut self, r: &mut BitReader, pps: &Pps, sps: &Sps) -> Result<()> {
|
||||
self.entry_points.clear();
|
||||
if pps.tiles_enabled || pps.entropy_coding_sync {
|
||||
let n = r.read_ue()?;
|
||||
ensure!(n <= sps.pic_width_in_ctbs() * sps.pic_height_in_ctbs(), "num_entry_point_offsets out of range");
|
||||
if n > 0 {
|
||||
let len = r.read_ue_plus(1)?;
|
||||
ensure!(len <= 32, "offset_len_minus1 out of range");
|
||||
for _ in 0..n {
|
||||
let off = r.read_bits(len)?.checked_add(1).ok_or_else(|| crate::Error::Invalid("entry_point_offset out of range".into()))?;
|
||||
self.entry_points.push(off);
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Parse a slice segment header. `prev` is the header of the preceding independent slice segment
|
||||
/// of the same picture (needed for dependent slice segments).
|
||||
pub fn parse(
|
||||
@@ -413,9 +225,154 @@ impl SliceHeader {
|
||||
sh.dependent = dependent;
|
||||
sh.segment_address = segment_address;
|
||||
if !dependent {
|
||||
sh.parse_independent_fields(&mut r, nal, &pps, &sps)?;
|
||||
r.skip(pps.num_extra_slice_header_bits as usize)?;
|
||||
let st = r.read_ue()?;
|
||||
sh.slice_type = match st {
|
||||
0 => SliceType::B,
|
||||
1 => SliceType::P,
|
||||
2 => SliceType::I,
|
||||
_ => return invalid(format!("slice_type {st}")),
|
||||
};
|
||||
if nal.is_irap() {
|
||||
ensure!(sh.slice_type == SliceType::I, "IRAP picture with inter slice");
|
||||
}
|
||||
if pps.output_flag_present {
|
||||
sh.pic_output = r.read_flag()?;
|
||||
}
|
||||
if sps.separate_colour_plane {
|
||||
r.skip(2)?;
|
||||
}
|
||||
if !nal.is_idr() {
|
||||
sh.poc_lsb = r.read_bits(sps.log2_max_poc_lsb)?;
|
||||
let from_sps = r.read_flag()?;
|
||||
if !from_sps {
|
||||
let start = r.position();
|
||||
sh.st_rps = StRps::parse(&mut r, sps.st_rps.len(), &sps.st_rps, sps.st_rps.len())?;
|
||||
sh.st_rps_bits = r.position() - start;
|
||||
} else {
|
||||
ensure!(!sps.st_rps.is_empty(), "no short-term RPS in the SPS");
|
||||
let idx = if sps.st_rps.len() > 1 { r.read_bits(ceil_log2(sps.st_rps.len() as u32))? as usize } else { 0 };
|
||||
ensure!(idx < sps.st_rps.len(), "short_term_ref_pic_set_idx out of range");
|
||||
sh.st_rps = sps.st_rps[idx].clone();
|
||||
}
|
||||
if sps.long_term_refs_present {
|
||||
let num_lt_sps = if !sps.lt_ref_pics.is_empty() { r.read_ue()? } else { 0 };
|
||||
ensure!(num_lt_sps as usize <= sps.lt_ref_pics.len(), "num_long_term_sps out of range");
|
||||
let num_lt_pics = r.read_ue()?;
|
||||
ensure!(num_lt_sps + num_lt_pics <= 32, "too many long-term pictures");
|
||||
let mut msb_acc = 0u32;
|
||||
for i in 0..num_lt_sps + num_lt_pics {
|
||||
let (lsb, used) = if i < num_lt_sps {
|
||||
let idx = if sps.lt_ref_pics.len() > 1 { r.read_bits(ceil_log2(sps.lt_ref_pics.len() as u32))? } else { 0 };
|
||||
ensure!((idx as usize) < sps.lt_ref_pics.len(), "lt_idx_sps out of range");
|
||||
sps.lt_ref_pics[idx as usize]
|
||||
} else {
|
||||
(r.read_bits(sps.log2_max_poc_lsb)?, r.read_flag()?)
|
||||
};
|
||||
let msb_present = r.read_flag()?;
|
||||
let cycle = if msb_present { r.read_ue()? } else { 0 };
|
||||
// DeltaPocMsbCycleLt accumulates within each of the two groups (7-52)
|
||||
if i == 0 || i == num_lt_sps {
|
||||
msb_acc = cycle;
|
||||
} else {
|
||||
msb_acc = msb_acc.wrapping_add(cycle);
|
||||
}
|
||||
sh.lt.push((lsb, used, msb_present, msb_acc));
|
||||
}
|
||||
}
|
||||
if sps.temporal_mvp {
|
||||
sh.temporal_mvp = r.read_flag()?;
|
||||
}
|
||||
}
|
||||
if sps.sao {
|
||||
sh.sao_luma = r.read_flag()?;
|
||||
if sps.chroma_array_type() != 0 {
|
||||
sh.sao_chroma = r.read_flag()?;
|
||||
}
|
||||
}
|
||||
if sh.slice_type != SliceType::I {
|
||||
sh.num_ref_idx = [pps.num_ref_idx_l0_default, if sh.is_b() { pps.num_ref_idx_l1_default } else { 0 }];
|
||||
if r.read_flag()? {
|
||||
sh.num_ref_idx[0] = r.read_ue_plus(1)?;
|
||||
if sh.is_b() {
|
||||
sh.num_ref_idx[1] = r.read_ue_plus(1)?;
|
||||
}
|
||||
}
|
||||
ensure!(sh.num_ref_idx[0] <= 15 && sh.num_ref_idx[1] <= 15, "num_ref_idx_active out of range");
|
||||
let total = sh.num_pic_total_curr();
|
||||
ensure!(total > 0, "inter slice without reference pictures in the RPS");
|
||||
if pps.lists_modification_present && total > 1 {
|
||||
let bits = ceil_log2(total);
|
||||
for l in 0..if sh.is_b() { 2 } else { 1 } {
|
||||
if r.read_flag()? {
|
||||
let mut e = Vec::new();
|
||||
for _ in 0..sh.num_ref_idx[l] {
|
||||
let v = r.read_bits(bits)?;
|
||||
ensure!(v < total, "list_entry out of range");
|
||||
e.push(v);
|
||||
}
|
||||
sh.list_entry[l] = Some(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
if sh.is_b() {
|
||||
sh.mvd_l1_zero = r.read_flag()?;
|
||||
}
|
||||
if pps.cabac_init_present {
|
||||
sh.cabac_init_flag = r.read_flag()?;
|
||||
}
|
||||
if sh.temporal_mvp {
|
||||
if sh.is_b() {
|
||||
sh.collocated_from_l0 = r.read_flag()?;
|
||||
}
|
||||
let l = if sh.collocated_from_l0 { 0 } else { 1 };
|
||||
if sh.num_ref_idx[l] > 1 {
|
||||
sh.collocated_ref_idx = r.read_ue()?;
|
||||
ensure!(sh.collocated_ref_idx < sh.num_ref_idx[l], "collocated_ref_idx out of range");
|
||||
}
|
||||
}
|
||||
if (pps.weighted_pred && sh.slice_type == SliceType::P) || (pps.weighted_bipred && sh.is_b()) {
|
||||
sh.pwt = Some(parse_pwt(&mut r, &sh, &sps)?);
|
||||
}
|
||||
let five_minus = r.read_ue()?;
|
||||
ensure!(five_minus <= 4, "five_minus_max_num_merge_cand out of range");
|
||||
sh.max_num_merge_cand = 5 - five_minus;
|
||||
}
|
||||
sh.qp_delta = r.read_se()?;
|
||||
let qp = pps.init_qp.checked_add(sh.qp_delta).ok_or_else(|| crate::Error::Invalid("SliceQpY out of range".into()))?;
|
||||
let qp_bd = 6 * (sps.bit_depth_luma as i32 - 8);
|
||||
ensure!((-qp_bd..=51).contains(&qp), "SliceQpY {qp} out of range");
|
||||
if pps.slice_chroma_qp_offsets_present {
|
||||
sh.cb_qp_offset = r.read_se()?;
|
||||
sh.cr_qp_offset = r.read_se()?;
|
||||
ensure!((-12..=12).contains(&sh.cb_qp_offset) && (-12..=12).contains(&sh.cr_qp_offset), "slice chroma QP offset out of range");
|
||||
}
|
||||
let override_flag = if pps.deblocking_override_enabled { r.read_flag()? } else { false };
|
||||
if override_flag {
|
||||
sh.deblocking_disabled = r.read_flag()?;
|
||||
if !sh.deblocking_disabled {
|
||||
sh.beta_offset_div2 = r.read_se()?;
|
||||
sh.tc_offset_div2 = r.read_se()?;
|
||||
ensure!((-6..=6).contains(&sh.beta_offset_div2) && (-6..=6).contains(&sh.tc_offset_div2), "deblocking offsets out of range");
|
||||
}
|
||||
}
|
||||
if pps.loop_filter_across_slices && (sh.sao_luma || sh.sao_chroma || !sh.deblocking_disabled) {
|
||||
sh.loop_filter_across_slices = r.read_flag()?;
|
||||
}
|
||||
}
|
||||
sh.entry_points.clear();
|
||||
if pps.tiles_enabled || pps.entropy_coding_sync {
|
||||
let n = r.read_ue()?;
|
||||
ensure!(n <= sps.pic_width_in_ctbs() * sps.pic_height_in_ctbs(), "num_entry_point_offsets out of range");
|
||||
if n > 0 {
|
||||
let len = r.read_ue_plus(1)?;
|
||||
ensure!(len <= 32, "offset_len_minus1 out of range");
|
||||
for _ in 0..n {
|
||||
let off = r.read_bits(len)?.checked_add(1).ok_or_else(|| crate::Error::Invalid("entry_point_offset out of range".into()))?;
|
||||
sh.entry_points.push(off);
|
||||
}
|
||||
}
|
||||
}
|
||||
sh.parse_entry_points(&mut r, &pps, &sps)?;
|
||||
if pps.slice_header_extension_present {
|
||||
let len = r.read_ue()?;
|
||||
ensure!(len <= 256, "slice_segment_header_extension_length out of range");
|
||||
|
||||
+190
-238
@@ -1105,52 +1105,7 @@ impl<'a> SliceDecoder<'a> {
|
||||
let n = 1usize << log2;
|
||||
let bypass = self.cu_transquant_bypass;
|
||||
let ts = if self.pps.transform_skip && !bypass && log2 == 2 { self.dd(TRANSFORM_SKIP + (c > 0) as usize) == 1 } else { false };
|
||||
let scan_idx = self.scan_idx(x0, y0, log2, c);
|
||||
let (mut lx, mut ly) = self.last_sig_coeff_pos(log2, c);
|
||||
if scan_idx == 2 {
|
||||
std::mem::swap(&mut lx, &mut ly);
|
||||
}
|
||||
ensure!((lx as usize) < n && (ly as usize) < n, "last significant coefficient outside the block");
|
||||
let t = TbCtx { c, log2, scan_idx, sdh: self.pps.sign_data_hiding && !bypass };
|
||||
let log2sb = log2 - 2;
|
||||
let sb_scan = scan_order(log2sb, scan_idx);
|
||||
let pos_scan = &SCAN_4[scan_idx];
|
||||
let sb_w = 1usize << log2sb;
|
||||
let last_sb = sb_scan.iter().position(|&(x, y)| x as u32 == lx >> 2 && y as u32 == ly >> 2).unwrap_or(0);
|
||||
let last_pos = pos_scan.iter().position(|&(x, y)| x as u32 == lx & 3 && y as u32 == ly & 3).unwrap_or(0);
|
||||
self.coeffs[..n * n].fill(0);
|
||||
let mut csbf = [0u8; 64];
|
||||
let mut prev_g1: Option<u32> = None;
|
||||
let (mut max_x, mut max_y) = (0usize, 0usize);
|
||||
for i in (0..=last_sb).rev() {
|
||||
let sb = SubBlock { xs: sb_scan[i].0 as usize, ys: sb_scan[i].1 as usize };
|
||||
let right = if sb.xs + 1 < sb_w { csbf[sb.ys * 8 + sb.xs + 1] } else { 0 };
|
||||
let below = if sb.ys + 1 < sb_w { csbf[(sb.ys + 1) * 8 + sb.xs] } else { 0 };
|
||||
let mut infer_dc = false;
|
||||
if i < last_sb && i > 0 {
|
||||
let inc = (right | below).min(1) as usize + if c > 0 { 2 } else { 0 };
|
||||
csbf[sb.ys * 8 + sb.xs] = self.c.decode_decision(CODED_SUB_BLOCK + inc) as u8;
|
||||
infer_dc = true;
|
||||
} else {
|
||||
csbf[sb.ys * 8 + sb.xs] = 1;
|
||||
}
|
||||
if csbf[sb.ys * 8 + sb.xs] == 0 {
|
||||
continue;
|
||||
}
|
||||
let (sig, nsig) = self.sig_coeff_flags(&t, &sb, right + (below << 1), infer_dc, (i == last_sb).then_some(last_pos));
|
||||
if nsig == 0 {
|
||||
continue;
|
||||
}
|
||||
let g = self.greater_flags(&t, i, nsig, &mut prev_g1);
|
||||
let (mx, my) = self.store_levels(&t, &sb, &sig[..nsig], &g)?;
|
||||
max_x = max_x.max(mx);
|
||||
max_y = max_y.max(my);
|
||||
}
|
||||
self.reconstruct((x0, y0), log2, c, qp, ts, (max_x, max_y))
|
||||
}
|
||||
|
||||
/// Decodes last_sig_coeff_{x,y}_prefix / suffix (before any scan-order swap).
|
||||
fn last_sig_coeff_pos(&mut self, log2: u32, c: usize) -> (u32, u32) {
|
||||
// last significant coefficient
|
||||
let cmax = (log2 << 1) - 1;
|
||||
let (off, shift) = if c == 0 { (3 * (log2 - 2) + ((log2 - 1) >> 2), (log2 + 1) >> 2) } else { (15, log2 - 2) };
|
||||
let mut px = 0;
|
||||
@@ -1161,144 +1116,209 @@ impl<'a> SliceDecoder<'a> {
|
||||
while py < cmax && self.dd(LAST_Y_PREFIX + (off + (py >> shift)) as usize) == 1 {
|
||||
py += 1;
|
||||
}
|
||||
let lx = self.last_pos_with_suffix(px);
|
||||
let ly = self.last_pos_with_suffix(py);
|
||||
(lx, ly)
|
||||
}
|
||||
|
||||
fn last_pos_with_suffix(&mut self, prefix: u32) -> u32 {
|
||||
if prefix > 3 {
|
||||
let nb = (prefix >> 1) - 1;
|
||||
(1 << nb) * (2 + (prefix & 1)) + self.bypass_bits(nb)
|
||||
} else {
|
||||
prefix
|
||||
let mut lx = px;
|
||||
if px > 3 {
|
||||
let nb = (px >> 1) - 1;
|
||||
lx = (1 << nb) * (2 + (px & 1)) + self.bypass_bits(nb);
|
||||
}
|
||||
}
|
||||
|
||||
/// scanIdx (7.4.9.11): 0 diagonal, 1 horizontal, 2 vertical.
|
||||
fn scan_idx(&self, x0: i32, y0: i32, log2: u32, c: usize) -> usize {
|
||||
if !(self.cu_intra && (log2 == 2 || (log2 == 3 && c == 0))) {
|
||||
return 0;
|
||||
let mut ly = py;
|
||||
if py > 3 {
|
||||
let nb = (py >> 1) - 1;
|
||||
ly = (1 << nb) * (2 + (py & 1)) + self.bypass_bits(nb);
|
||||
}
|
||||
let m = if c == 0 { self.pic.blk_at(x0, y0).ipm as u32 } else { self.intra_chroma_mode };
|
||||
if (6..=14).contains(&m) {
|
||||
2
|
||||
} else if (22..=30).contains(&m) {
|
||||
1
|
||||
// scanIdx (7.4.9.11)
|
||||
let scan_idx = if self.cu_intra && (log2 == 2 || (log2 == 3 && c == 0)) {
|
||||
let m = if c == 0 { self.pic.blk_at(x0, y0).ipm as u32 } else { self.intra_chroma_mode };
|
||||
if (6..=14).contains(&m) {
|
||||
2
|
||||
} else if (22..=30).contains(&m) {
|
||||
1
|
||||
} else {
|
||||
0
|
||||
}
|
||||
} else {
|
||||
0
|
||||
}
|
||||
}
|
||||
|
||||
/// Significance map of one coded sub-block: scan positions of the significant coefficients, in
|
||||
/// descending order, and their count.
|
||||
fn sig_coeff_flags(&mut self, t: &TbCtx, sb: &SubBlock, prev_csbf: u8, infer_dc: bool, last_pos: Option<usize>) -> ([u8; 16], usize) {
|
||||
let pos_scan = &SCAN_4[t.scan_idx];
|
||||
let c_off_sig = if t.c == 0 { 0 } else { 27 };
|
||||
let mut infer_dc = infer_dc;
|
||||
let mut sig = [0u8; 16];
|
||||
let mut nsig = 0;
|
||||
let start: i32 = match last_pos {
|
||||
Some(lp) => {
|
||||
sig[0] = lp as u8;
|
||||
nsig = 1;
|
||||
lp as i32 - 1
|
||||
}
|
||||
None => 15,
|
||||
};
|
||||
for np in (0..=start).rev() {
|
||||
let (xp, yp) = (pos_scan[np as usize].0 as usize, pos_scan[np as usize].1 as usize);
|
||||
if np > 0 || !infer_dc {
|
||||
let ctx = sig_coeff_ctx(t, sb, prev_csbf, (xp, yp));
|
||||
if self.c.decode_decision(SIG_COEFF + c_off_sig + ctx) == 1 {
|
||||
sig[nsig] = np as u8;
|
||||
nsig += 1;
|
||||
infer_dc = false;
|
||||
}
|
||||
} else {
|
||||
// np == 0 with inferSbDcSigCoeffFlag
|
||||
sig[nsig] = 0;
|
||||
nsig += 1;
|
||||
}
|
||||
if scan_idx == 2 {
|
||||
std::mem::swap(&mut lx, &mut ly);
|
||||
}
|
||||
(sig, nsig)
|
||||
}
|
||||
|
||||
/// coeff_abs_level_greater1_flag / greater2_flag of one sub-block with `nsig` significant coefficients.
|
||||
fn greater_flags(&mut self, t: &TbCtx, sb_index: usize, nsig: usize, prev_g1: &mut Option<u32>) -> GreaterFlags {
|
||||
let mut ctx_set = if sb_index == 0 || t.c > 0 { 0 } else { 2 };
|
||||
if *prev_g1 == Some(0) {
|
||||
ctx_set += 1;
|
||||
}
|
||||
let mut g1ctx = 1u32;
|
||||
let mut g = GreaterFlags { gt1: [0u8; 16], first_g1: None, gt2: 0 };
|
||||
let c_off_g1 = if t.c > 0 { 16 } else { 0 };
|
||||
for k in 0..nsig.min(8) {
|
||||
let inc = ctx_set * 4 + g1ctx.min(3) as usize + c_off_g1;
|
||||
let f = self.c.decode_decision(GT1 + inc);
|
||||
g.gt1[k] = f as u8;
|
||||
if f == 1 {
|
||||
g1ctx = 0;
|
||||
if g.first_g1.is_none() {
|
||||
g.first_g1 = Some(k);
|
||||
}
|
||||
} else if g1ctx > 0 {
|
||||
g1ctx += 1;
|
||||
}
|
||||
}
|
||||
*prev_g1 = Some(g1ctx);
|
||||
if g.first_g1.is_some() {
|
||||
g.gt2 = self.c.decode_decision(GT2 + ctx_set + if t.c > 0 { 4 } else { 0 }) as u8;
|
||||
}
|
||||
g
|
||||
}
|
||||
|
||||
/// Reads signs and remaining levels of one sub-block and stores the coefficients. Returns the
|
||||
/// largest x and y coefficient position written.
|
||||
fn store_levels(&mut self, t: &TbCtx, sb: &SubBlock, sig: &[u8], g: &GreaterFlags) -> Result<(usize, usize)> {
|
||||
let nsig = sig.len();
|
||||
let n = 1usize << t.log2;
|
||||
let pos_scan = &SCAN_4[t.scan_idx];
|
||||
let sign_hidden = t.sdh && sig[0] as i32 - sig[nsig - 1] as i32 > 3;
|
||||
let nsigns = if sign_hidden { nsig - 1 } else { nsig };
|
||||
let mut signs = if nsigns > 0 { self.bypass_bits(nsigns as u32) << (32 - nsigns) } else { 0 };
|
||||
let mut rice = 0u32;
|
||||
let mut sum_abs = 0i32;
|
||||
ensure!((lx as usize) < n && (ly as usize) < n, "last significant coefficient outside the block");
|
||||
let log2sb = log2 - 2;
|
||||
let sb_scan = scan_order(log2sb, scan_idx);
|
||||
let pos_scan = &SCAN_4[scan_idx];
|
||||
let sb_w = 1usize << log2sb;
|
||||
let last_sb = sb_scan.iter().position(|&(x, y)| x as u32 == lx >> 2 && y as u32 == ly >> 2).unwrap_or(0);
|
||||
let last_pos = pos_scan.iter().position(|&(x, y)| x as u32 == lx & 3 && y as u32 == ly & 3).unwrap_or(0);
|
||||
self.coeffs[..n * n].fill(0);
|
||||
let mut csbf = [0u8; 64];
|
||||
let mut prev_g1: Option<u32> = None;
|
||||
let sdh = self.pps.sign_data_hiding && !bypass;
|
||||
let (mut max_x, mut max_y) = (0usize, 0usize);
|
||||
for k in 0..nsig {
|
||||
let mut base = 1 + g.gt1[k] as i32;
|
||||
if Some(k) == g.first_g1 {
|
||||
base += g.gt2 as i32;
|
||||
let c_off_sig = if c == 0 { 0 } else { 27 };
|
||||
for i in (0..=last_sb).rev() {
|
||||
let (xs, ys) = (sb_scan[i].0 as usize, sb_scan[i].1 as usize);
|
||||
let mut infer_dc = false;
|
||||
let right = if xs + 1 < sb_w { csbf[ys * 8 + xs + 1] } else { 0 };
|
||||
let below = if ys + 1 < sb_w { csbf[(ys + 1) * 8 + xs] } else { 0 };
|
||||
if i < last_sb && i > 0 {
|
||||
let inc = (right | below).min(1) as usize + if c > 0 { 2 } else { 0 };
|
||||
csbf[ys * 8 + xs] = self.c.decode_decision(CODED_SUB_BLOCK + inc) as u8;
|
||||
infer_dc = true;
|
||||
} else {
|
||||
csbf[ys * 8 + xs] = 1;
|
||||
}
|
||||
let thresh = if k < 8 { if Some(k) == g.first_g1 { 3 } else { 2 } } else { 1 };
|
||||
let mut level = base;
|
||||
if base == thresh {
|
||||
let rem = self.coeff_abs_level_remaining(rice)?;
|
||||
level = base + rem;
|
||||
if level > 3 * (1 << rice) {
|
||||
rice = (rice + 1).min(4);
|
||||
// significance map
|
||||
let mut sig = [0u8; 16]; // scan positions n of significant coefficients, descending
|
||||
let mut nsig = 0;
|
||||
let start: i32 = if i == last_sb {
|
||||
sig[0] = last_pos as u8;
|
||||
nsig = 1;
|
||||
last_pos as i32 - 1
|
||||
} else {
|
||||
15
|
||||
};
|
||||
if csbf[ys * 8 + xs] != 0 {
|
||||
let prev_csbf = right + (below << 1);
|
||||
let mut np = start;
|
||||
while np >= 0 {
|
||||
let (xp, yp) = (pos_scan[np as usize].0 as usize, pos_scan[np as usize].1 as usize);
|
||||
let (xc, yc) = (xs * 4 + xp, ys * 4 + yp);
|
||||
if np > 0 || !infer_dc {
|
||||
let sig_ctx = if log2 == 2 {
|
||||
CTX_IDX_MAP[(yc << 2) + xc] as usize
|
||||
} else if xc + yc == 0 {
|
||||
0
|
||||
} else {
|
||||
let mut s = match prev_csbf {
|
||||
0 => {
|
||||
if xp + yp == 0 {
|
||||
2
|
||||
} else if xp + yp < 3 {
|
||||
1
|
||||
} else {
|
||||
0
|
||||
}
|
||||
}
|
||||
1 => {
|
||||
if yp == 0 {
|
||||
2
|
||||
} else if yp == 1 {
|
||||
1
|
||||
} else {
|
||||
0
|
||||
}
|
||||
}
|
||||
2 => {
|
||||
if xp == 0 {
|
||||
2
|
||||
} else if xp == 1 {
|
||||
1
|
||||
} else {
|
||||
0
|
||||
}
|
||||
}
|
||||
_ => 2,
|
||||
};
|
||||
if c == 0 {
|
||||
if xs + ys > 0 {
|
||||
s += 3;
|
||||
}
|
||||
s += if log2 == 3 { if scan_idx == 0 { 9 } else { 15 } } else { 21 };
|
||||
} else {
|
||||
s += if log2 == 3 { 9 } else { 12 };
|
||||
}
|
||||
s
|
||||
};
|
||||
if self.c.decode_decision(SIG_COEFF + c_off_sig + sig_ctx) == 1 {
|
||||
sig[nsig] = np as u8;
|
||||
nsig += 1;
|
||||
infer_dc = false;
|
||||
}
|
||||
} else {
|
||||
// np == 0 with inferSbDcSigCoeffFlag
|
||||
sig[nsig] = 0;
|
||||
nsig += 1;
|
||||
}
|
||||
np -= 1;
|
||||
}
|
||||
}
|
||||
let np = sig[k] as usize;
|
||||
let (xc, yc) = (sb.xs * 4 + pos_scan[np].0 as usize, sb.ys * 4 + pos_scan[np].1 as usize);
|
||||
let mut v = level;
|
||||
if !(sign_hidden && k == nsig - 1) {
|
||||
if signs & 0x8000_0000 != 0 {
|
||||
v = -v;
|
||||
}
|
||||
signs <<= 1;
|
||||
if nsig == 0 {
|
||||
continue;
|
||||
}
|
||||
if sign_hidden {
|
||||
sum_abs += level;
|
||||
if k == nsig - 1 && sum_abs & 1 == 1 {
|
||||
v = -v;
|
||||
// greater1 / greater2 flags
|
||||
let mut ctx_set = if i == 0 || c > 0 { 0 } else { 2 };
|
||||
if let Some(g) = prev_g1
|
||||
&& g == 0
|
||||
{
|
||||
ctx_set += 1;
|
||||
}
|
||||
let mut g1ctx = 1u32;
|
||||
let mut gt1 = [0u8; 16];
|
||||
let mut first_g1: Option<usize> = None;
|
||||
let c_off_g1 = if c > 0 { 16 } else { 0 };
|
||||
for k in 0..nsig.min(8) {
|
||||
let inc = ctx_set * 4 + g1ctx.min(3) as usize + c_off_g1;
|
||||
let f = self.c.decode_decision(GT1 + inc);
|
||||
gt1[k] = f as u8;
|
||||
if f == 1 {
|
||||
g1ctx = 0;
|
||||
if first_g1.is_none() {
|
||||
first_g1 = Some(k);
|
||||
}
|
||||
} else if g1ctx > 0 {
|
||||
g1ctx += 1;
|
||||
}
|
||||
}
|
||||
self.coeffs[yc * n + xc] = v;
|
||||
max_x = max_x.max(xc);
|
||||
max_y = max_y.max(yc);
|
||||
prev_g1 = Some(g1ctx);
|
||||
let mut gt2 = 0u8;
|
||||
if let Some(k) = first_g1 {
|
||||
gt2 = self.c.decode_decision(GT2 + ctx_set + if c > 0 { 4 } else { 0 }) as u8;
|
||||
let _ = k;
|
||||
}
|
||||
// signs
|
||||
let last_sig_pos = sig[0] as i32;
|
||||
let first_sig_pos = sig[nsig - 1] as i32;
|
||||
let sign_hidden = sdh && last_sig_pos - first_sig_pos > 3;
|
||||
let nsigns = if sign_hidden { nsig - 1 } else { nsig };
|
||||
let mut signs = if nsigns > 0 { self.bypass_bits(nsigns as u32) << (32 - nsigns) } else { 0 };
|
||||
// remaining levels
|
||||
let mut rice = 0u32;
|
||||
let mut sum_abs = 0i32;
|
||||
for k in 0..nsig {
|
||||
let mut base = 1 + gt1[k] as i32;
|
||||
if Some(k) == first_g1 {
|
||||
base += gt2 as i32;
|
||||
}
|
||||
let thresh = if k < 8 { if Some(k) == first_g1 { 3 } else { 2 } } else { 1 };
|
||||
let mut level = base;
|
||||
if base == thresh {
|
||||
let rem = self.coeff_abs_level_remaining(rice)?;
|
||||
level = base + rem;
|
||||
if level > 3 * (1 << rice) {
|
||||
rice = (rice + 1).min(4);
|
||||
}
|
||||
}
|
||||
let np = sig[k] as usize;
|
||||
let (xc, yc) = (xs * 4 + pos_scan[np].0 as usize, ys * 4 + pos_scan[np].1 as usize);
|
||||
let mut v = level;
|
||||
if !(sign_hidden && k == nsig - 1) {
|
||||
if signs & 0x8000_0000 != 0 {
|
||||
v = -v;
|
||||
}
|
||||
signs <<= 1;
|
||||
}
|
||||
if sign_hidden {
|
||||
sum_abs += level;
|
||||
if k == nsig - 1 && sum_abs & 1 == 1 {
|
||||
v = -v;
|
||||
}
|
||||
}
|
||||
self.coeffs[yc * n + xc] = v;
|
||||
max_x = max_x.max(xc);
|
||||
max_y = max_y.max(yc);
|
||||
}
|
||||
}
|
||||
Ok((max_x, max_y))
|
||||
self.reconstruct((x0, y0), log2, c, qp, ts, (max_x, max_y))
|
||||
}
|
||||
|
||||
fn coeff_abs_level_remaining(&mut self, rice: u32) -> Result<i32> {
|
||||
@@ -1545,71 +1565,3 @@ impl<'a> SliceDecoder<'a> {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Per-transform-block parameters of residual coding.
|
||||
struct TbCtx {
|
||||
c: usize,
|
||||
log2: u32,
|
||||
scan_idx: usize,
|
||||
sdh: bool,
|
||||
}
|
||||
|
||||
/// Position of a 4x4 sub-block in the sub-block grid.
|
||||
struct SubBlock {
|
||||
xs: usize,
|
||||
ys: usize,
|
||||
}
|
||||
|
||||
/// Greater-than-1 / greater-than-2 flags of one sub-block.
|
||||
struct GreaterFlags {
|
||||
gt1: [u8; 16],
|
||||
first_g1: Option<usize>,
|
||||
gt2: u8,
|
||||
}
|
||||
|
||||
/// sigCtx (9.3.4.2.5) for the coefficient at (`xp`, `yp`) inside the sub-block.
|
||||
fn sig_coeff_ctx(t: &TbCtx, sb: &SubBlock, prev_csbf: u8, (xp, yp): (usize, usize)) -> usize {
|
||||
let (xc, yc) = (sb.xs * 4 + xp, sb.ys * 4 + yp);
|
||||
if t.log2 == 2 {
|
||||
CTX_IDX_MAP[(yc << 2) + xc] as usize
|
||||
} else if xc + yc == 0 {
|
||||
0
|
||||
} else {
|
||||
sig_ctx_neighbourhood(prev_csbf, xp, yp) + sig_ctx_offset(t, sb.xs + sb.ys > 0)
|
||||
}
|
||||
}
|
||||
|
||||
/// Base sigCtx from the coded-sub-block flags of the right and below neighbours.
|
||||
fn sig_ctx_neighbourhood(prev_csbf: u8, xp: usize, yp: usize) -> usize {
|
||||
let near = |v: usize| match v {
|
||||
0 => 2,
|
||||
1 => 1,
|
||||
_ => 0,
|
||||
};
|
||||
match prev_csbf {
|
||||
0 => {
|
||||
if xp + yp == 0 {
|
||||
2
|
||||
} else if xp + yp < 3 {
|
||||
1
|
||||
} else {
|
||||
0
|
||||
}
|
||||
}
|
||||
1 => near(yp),
|
||||
2 => near(xp),
|
||||
_ => 2,
|
||||
}
|
||||
}
|
||||
|
||||
/// Offset added to sigCtx for the block size, colour component and sub-block position.
|
||||
fn sig_ctx_offset(t: &TbCtx, not_first_sb: bool) -> usize {
|
||||
if t.c == 0 {
|
||||
let size_off = if t.log2 == 3 { if t.scan_idx == 0 { 9 } else { 15 } } else { 21 };
|
||||
size_off + if not_first_sb { 3 } else { 0 }
|
||||
} else if t.log2 == 3 {
|
||||
9
|
||||
} else {
|
||||
12
|
||||
}
|
||||
}
|
||||
|
||||
@@ -286,20 +286,10 @@ impl Plus {
|
||||
if !self.active {
|
||||
return None;
|
||||
}
|
||||
match typ {
|
||||
0x4008 => self.object(flags, r),
|
||||
0x4009 | 0x4013 | 0x401A..=0x401C | 0x4036 => self.record_image_text_region(cv, typ, flags, r),
|
||||
0x4025..=0x4035 => self.record_state(cv, typ, flags, r),
|
||||
0x4010..=0x4012 | 0x4016..=0x4019 => self.record_curve(cv, typ, flags, r),
|
||||
_ => self.record_shape(cv, typ, flags, r),
|
||||
}
|
||||
}
|
||||
|
||||
/// Rectangle, polygon, ellipse and path records.
|
||||
fn record_shape(&mut self, cv: &mut Canvas, typ: u16, flags: u16, r: Bytes) -> Option<()> {
|
||||
let compressed = flags & 0x4000 != 0;
|
||||
let id = usize::from(flags & 0xFF);
|
||||
match typ {
|
||||
0x4008 => self.object(flags, r),
|
||||
0x400A => {
|
||||
let fill = self.brush_of(flags, r.u32(0)?)?;
|
||||
let p = self.painter();
|
||||
@@ -358,15 +348,6 @@ impl Plus {
|
||||
self.painter().stroke(cv, &ops, pen);
|
||||
Some(())
|
||||
}
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Curve, bezier and arc records.
|
||||
fn record_curve(&mut self, cv: &mut Canvas, typ: u16, flags: u16, r: Bytes) -> Option<()> {
|
||||
let compressed = flags & 0x4000 != 0;
|
||||
let id = usize::from(flags & 0xFF);
|
||||
match typ {
|
||||
0x4019 => {
|
||||
let pen = self.pen(id)?;
|
||||
let pts = points(r, 4, r.u32(0)? as usize, compressed)?;
|
||||
@@ -399,29 +380,6 @@ impl Plus {
|
||||
self.painter().stroke(cv, &curve_ops(&pts, tension, false, offset, segments), pen);
|
||||
Some(())
|
||||
}
|
||||
0x4010 => {
|
||||
let fill = self.brush_of(flags, r.u32(0)?)?;
|
||||
let [start, sweep] = floats::<2>(r, 4)?;
|
||||
let rect = *rects(r, 12, 1, compressed)?.first()?;
|
||||
self.painter().fill(cv, &arc_ops(rect, start, sweep, true), &fill);
|
||||
Some(())
|
||||
}
|
||||
0x4011 | 0x4012 => {
|
||||
let pen = self.pen(id)?;
|
||||
let [start, sweep] = floats::<2>(r, 0)?;
|
||||
let rect = *rects(r, 8, 1, compressed)?.first()?;
|
||||
self.painter().stroke(cv, &arc_ops(rect, start, sweep, typ == 0x4011), pen);
|
||||
Some(())
|
||||
}
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Clear, region fill, image and text records.
|
||||
fn record_image_text_region(&mut self, cv: &mut Canvas, typ: u16, flags: u16, r: Bytes) -> Option<()> {
|
||||
let compressed = flags & 0x4000 != 0;
|
||||
let id = usize::from(flags & 0xFF);
|
||||
match typ {
|
||||
// Clear: the whole picture in one colour, whatever the clip (a transparent clear
|
||||
// leaves nothing to draw).
|
||||
0x4009 => {
|
||||
@@ -461,14 +419,6 @@ impl Plus {
|
||||
}
|
||||
0x401C => self.draw_string(cv, flags, r),
|
||||
0x4036 => self.draw_driver_string(cv, flags, r),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Save / restore, transform and clip records.
|
||||
fn record_state(&mut self, cv: &mut Canvas, typ: u16, flags: u16, r: Bytes) -> Option<()> {
|
||||
let id = usize::from(flags & 0xFF);
|
||||
match typ {
|
||||
0x4025 | 0x4028 => r.u32(0).map(|i| self.push(i)),
|
||||
0x4027 => r.u32(24).map(|i| self.push(i)),
|
||||
0x4026 | 0x4029 => r.u32(0).map(|i| self.pop(i)),
|
||||
@@ -520,6 +470,20 @@ impl Plus {
|
||||
}
|
||||
Some(())
|
||||
}
|
||||
0x4010 => {
|
||||
let fill = self.brush_of(flags, r.u32(0)?)?;
|
||||
let [start, sweep] = floats::<2>(r, 4)?;
|
||||
let rect = *rects(r, 12, 1, compressed)?.first()?;
|
||||
self.painter().fill(cv, &arc_ops(rect, start, sweep, true), &fill);
|
||||
Some(())
|
||||
}
|
||||
0x4011 | 0x4012 => {
|
||||
let pen = self.pen(id)?;
|
||||
let [start, sweep] = floats::<2>(r, 0)?;
|
||||
let rect = *rects(r, 8, 1, compressed)?.first()?;
|
||||
self.painter().stroke(cv, &arc_ops(rect, start, sweep, typ == 0x4011), pen);
|
||||
Some(())
|
||||
}
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -49,7 +49,7 @@ struct B {
|
||||
}
|
||||
|
||||
impl B {
|
||||
fn shape(&mut self, name: &str, kind: PhType, idx: u32, [x, y, w, h]: [f64; 4]) -> Shape {
|
||||
fn shape(&mut self, name: &str, kind: PhType, idx: u32, x: f64, y: f64, w: f64, h: f64) -> Shape {
|
||||
self.next += 1;
|
||||
Shape {
|
||||
id: ShapeId(self.next),
|
||||
@@ -164,9 +164,9 @@ pub fn build_master(size: Size, theme: Theme, first_id: u32) -> (Master, u32) {
|
||||
let (x1, y1, w1, h1) = r(60.0, 500.0, 216.0, 28.0);
|
||||
let (x2, _, w2, _) = r(318.0, 500.0, 324.0, 28.0);
|
||||
let (x3, _, w3, _) = r(684.0, 500.0, 216.0, 28.0);
|
||||
let mut d = b.shape("Date Placeholder", PhType::Date, 10, [x1, y1, w1, h1]);
|
||||
let mut f = b.shape("Footer Placeholder", PhType::Footer, 11, [x2, y1, w2, h1]);
|
||||
let mut n = b.shape("Slide Number Placeholder", PhType::SlideNum, 12, [x3, y1, w3, h1]);
|
||||
let mut d = b.shape("Date Placeholder", PhType::Date, 10, x1, y1, w1, h1);
|
||||
let mut f = b.shape("Footer Placeholder", PhType::Footer, 11, x2, y1, w2, h1);
|
||||
let mut n = b.shape("Slide Number Placeholder", PhType::SlideNum, 12, x3, y1, w3, h1);
|
||||
let small = |align| {
|
||||
let mut ls = ListStyle::default();
|
||||
ls.set(
|
||||
@@ -202,13 +202,13 @@ pub fn build_master(size: Size, theme: Theme, first_id: u32) -> (Master, u32) {
|
||||
};
|
||||
let title_box = |b: &mut B| {
|
||||
let (x, y, ww, hh) = r(60.0, 29.0, 840.0, 104.0);
|
||||
let mut s = b.shape("Title 1", PhType::Title, 0, [x, y, ww, hh]);
|
||||
let mut s = b.shape("Title 1", PhType::Title, 0, x, y, ww, hh);
|
||||
s.text = Some(body_with(Some(Anchor::Middle), Some(AutoFit::Shrink { font_scale: Some(1.0), line_reduction: 0.0 })));
|
||||
s
|
||||
};
|
||||
let body_box = |b: &mut B, name: &str, idx: u32, x: f64, y: f64, ww: f64, hh: f64| {
|
||||
let (x, y, ww, hh) = r(x, y, ww, hh);
|
||||
let mut s = b.shape(name, PhType::Body, idx, [x, y, ww, hh]);
|
||||
let mut s = b.shape(name, PhType::Body, idx, x, y, ww, hh);
|
||||
s.text = Some(body_with(Some(Anchor::Top), Some(AutoFit::Shrink { font_scale: Some(1.0), line_reduction: 0.0 })));
|
||||
s
|
||||
};
|
||||
@@ -251,7 +251,7 @@ pub fn build_master(size: Size, theme: Theme, first_id: u32) -> (Master, u32) {
|
||||
// Title Slide
|
||||
{
|
||||
let (x, y, ww, hh) = r(120.0, 88.0, 720.0, 188.0);
|
||||
let mut t = b.shape("Title 1", PhType::CtrTitle, 0, [x, y, ww, hh]);
|
||||
let mut t = b.shape("Title 1", PhType::CtrTitle, 0, x, y, ww, hh);
|
||||
let mut ls = ListStyle::default();
|
||||
ls.set(
|
||||
0,
|
||||
@@ -270,7 +270,7 @@ pub fn build_master(size: Size, theme: Theme, first_id: u32) -> (Master, u32) {
|
||||
paragraphs: vec![],
|
||||
});
|
||||
let (x, y, ww, hh) = r(120.0, 284.0, 720.0, 130.0);
|
||||
let mut st = b.shape("Subtitle 2", PhType::SubTitle, 1, [x, y, ww, hh]);
|
||||
let mut st = b.shape("Subtitle 2", PhType::SubTitle, 1, x, y, ww, hh);
|
||||
let mut ls = ListStyle::default();
|
||||
ls.set(
|
||||
0,
|
||||
@@ -309,7 +309,7 @@ pub fn build_master(size: Size, theme: Theme, first_id: u32) -> (Master, u32) {
|
||||
// Section Header
|
||||
{
|
||||
let (x, y, ww, hh) = r(65.0, 135.0, 830.0, 225.0);
|
||||
let mut t = b.shape("Title 1", PhType::Title, 0, [x, y, ww, hh]);
|
||||
let mut t = b.shape("Title 1", PhType::Title, 0, x, y, ww, hh);
|
||||
let mut ls = ListStyle::default();
|
||||
ls.set(0, LevelStyle { run: RunProps { size: Some(60.0), ..Default::default() }, ..Default::default() });
|
||||
t.text = Some(TextBody { body: BodyProps { anchor: Some(Anchor::Bottom), ..Default::default() }, list_style: ls, paragraphs: vec![] });
|
||||
@@ -347,7 +347,7 @@ pub fn build_master(size: Size, theme: Theme, first_id: u32) -> (Master, u32) {
|
||||
let t = title_box(&mut b);
|
||||
let head = |b: &mut B, name: &str, idx: u32, x: f64| {
|
||||
let (x, y, ww, hh) = r(x, 133.0, 412.0, 62.0);
|
||||
let mut s = b.shape(name, PhType::Body, idx, [x, y, ww, hh]);
|
||||
let mut s = b.shape(name, PhType::Body, idx, x, y, ww, hh);
|
||||
let mut ls = ListStyle::default();
|
||||
ls.set(
|
||||
0,
|
||||
@@ -381,7 +381,7 @@ pub fn build_master(size: Size, theme: Theme, first_id: u32) -> (Master, u32) {
|
||||
// Content with Caption
|
||||
{
|
||||
let (x, y, ww, hh) = r(60.0, 36.0, 322.0, 126.0);
|
||||
let mut t = b.shape("Title 1", PhType::Title, 0, [x, y, ww, hh]);
|
||||
let mut t = b.shape("Title 1", PhType::Title, 0, x, y, ww, hh);
|
||||
let mut ls = ListStyle::default();
|
||||
ls.set(0, LevelStyle { run: RunProps { size: Some(32.0), ..Default::default() }, ..Default::default() });
|
||||
t.text = Some(TextBody { body: BodyProps { anchor: Some(Anchor::Bottom), ..Default::default() }, list_style: ls, paragraphs: vec![] });
|
||||
@@ -405,12 +405,12 @@ pub fn build_master(size: Size, theme: Theme, first_id: u32) -> (Master, u32) {
|
||||
// Picture with Caption
|
||||
{
|
||||
let (x, y, ww, hh) = r(60.0, 36.0, 322.0, 126.0);
|
||||
let mut t = b.shape("Title 1", PhType::Title, 0, [x, y, ww, hh]);
|
||||
let mut t = b.shape("Title 1", PhType::Title, 0, x, y, ww, hh);
|
||||
let mut ls = ListStyle::default();
|
||||
ls.set(0, LevelStyle { run: RunProps { size: Some(32.0), ..Default::default() }, ..Default::default() });
|
||||
t.text = Some(TextBody { body: BodyProps { anchor: Some(Anchor::Bottom), ..Default::default() }, list_style: ls, paragraphs: vec![] });
|
||||
let (x, y, ww, hh) = r(412.0, 78.0, 488.0, 384.0);
|
||||
let p = b.shape("Picture Placeholder 2", PhType::Picture, 1, [x, y, ww, hh]);
|
||||
let p = b.shape("Picture Placeholder 2", PhType::Picture, 1, x, y, ww, hh);
|
||||
let mut cap = body_box(&mut b, "Text Placeholder 3", 2, 60.0, 162.0, 322.0, 300.0);
|
||||
let mut ls = ListStyle::default();
|
||||
ls.set(
|
||||
@@ -444,7 +444,7 @@ pub fn build_master(size: Size, theme: Theme, first_id: u32) -> (Master, u32) {
|
||||
// Vertical Title and Text
|
||||
{
|
||||
let (x, y, ww, hh) = r(687.0, 29.0, 213.0, 458.0);
|
||||
let mut t = b.shape("Vertical Title 1", PhType::Title, 0, [x, y, ww, hh]);
|
||||
let mut t = b.shape("Vertical Title 1", PhType::Title, 0, x, y, ww, hh);
|
||||
t.text = Some(TextBody { body: BodyProps { vert: Some(TextDir::EaVertical), ..Default::default() }, ..Default::default() });
|
||||
if let Some(p) = t.ph.as_mut() {
|
||||
p.vertical = true;
|
||||
|
||||
+52
-79
@@ -59,80 +59,6 @@ pub struct CeltDecoder {
|
||||
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 {
|
||||
@@ -250,10 +176,34 @@ impl CeltDecoder {
|
||||
}
|
||||
}
|
||||
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 mut tell = dec.tell();
|
||||
let silence = if tell >= total_bits_raw {
|
||||
true
|
||||
} else if tell == 1 {
|
||||
dec.bit_logp(15)
|
||||
} else {
|
||||
false
|
||||
};
|
||||
if silence {
|
||||
// Pretend we've read all the remaining bits.
|
||||
dec.skip_to_end(total_bits_raw);
|
||||
tell = total_bits_raw;
|
||||
}
|
||||
let mut postfilter_gain = 0.0f32;
|
||||
let mut postfilter_pitch = 0i32;
|
||||
let mut postfilter_tapset = 0usize;
|
||||
if start == 0 && tell + 16 <= total_bits_raw {
|
||||
if dec.bit_logp(1) {
|
||||
let octave = dec.uint(6);
|
||||
postfilter_pitch = ((16 << octave) + dec.bits(4 + octave)) as i32 - 1;
|
||||
let qg = dec.bits(3);
|
||||
if dec.tell() + 2 <= total_bits_raw {
|
||||
postfilter_tapset = dec.icdf(&TAPSET_ICDF, 2);
|
||||
}
|
||||
postfilter_gain = 0.09375 * (qg + 1) as f32;
|
||||
}
|
||||
tell = dec.tell();
|
||||
}
|
||||
let is_transient = if lm > 0 && tell + 3 <= total_bits_raw {
|
||||
let t = dec.bit_logp(3);
|
||||
tell = dec.tell();
|
||||
@@ -272,7 +222,30 @@ impl CeltDecoder {
|
||||
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 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);
|
||||
}
|
||||
}
|
||||
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 };
|
||||
|
||||
@@ -269,34 +269,8 @@ struct Split {
|
||||
qalloc: i32,
|
||||
}
|
||||
|
||||
/// Block geometry for one theta decode: `n` samples, `bb` blocks per band, `b0` initial block count.
|
||||
#[derive(Clone, Copy)]
|
||||
struct ThetaShape {
|
||||
n: i32,
|
||||
bb: usize,
|
||||
b0: usize,
|
||||
lm: i32,
|
||||
stereo: bool,
|
||||
}
|
||||
|
||||
/// Mid/side gains and delta for a decoded angle; also narrows `fill` to the blocks that stay coded.
|
||||
fn split_gains(itheta: i32, n: i32, bb: usize, fill: &mut u32) -> (i32, i32, i32) {
|
||||
if itheta == 0 {
|
||||
*fill &= (1 << bb) - 1;
|
||||
(32767, 0, -16384)
|
||||
} else if itheta == 16384 {
|
||||
*fill &= ((1 << bb) - 1) << bb;
|
||||
(0, 32767, 16384)
|
||||
} else {
|
||||
let imid = bitexact_cos(itheta);
|
||||
let iside = bitexact_cos(16384 - itheta);
|
||||
(imid, iside, frac_mul16((n - 1) << 7, bitexact_log2tan(iside, imid)))
|
||||
}
|
||||
}
|
||||
|
||||
impl BandCtx<'_, '_, '_> {
|
||||
fn compute_theta(&mut self, shape: ThetaShape, b: &mut i32, fill: &mut u32) -> Split {
|
||||
let ThetaShape { n, bb, b0, lm, stereo } = shape;
|
||||
fn compute_theta(&mut self, n: i32, b: &mut i32, bb: usize, b0: usize, lm: i32, stereo: bool, fill: &mut u32) -> Split {
|
||||
let i = self.i;
|
||||
let pulse_cap = self.m.log_n[i] + lm * (1 << BITRES);
|
||||
let offset = (pulse_cap >> 1) - if stereo && n == 2 { QTHETA_OFFSET_TWOPHASE } else { QTHETA_OFFSET };
|
||||
@@ -308,64 +282,65 @@ impl BandCtx<'_, '_, '_> {
|
||||
let mut itheta = 0i32;
|
||||
let mut inv = false;
|
||||
if qn != 1 {
|
||||
itheta = self.decode_itheta(qn, n, b0, stereo);
|
||||
if stereo && n > 2 {
|
||||
let p0 = 3u32;
|
||||
let x0 = (qn / 2) as u32;
|
||||
let ft = p0 * (x0 + 1) + x0;
|
||||
let fs = self.dec.decode(ft);
|
||||
let x = if fs < (x0 + 1) * p0 { fs / p0 } else { x0 + 1 + (fs - (x0 + 1) * p0) };
|
||||
let (fl, fh) = if x <= x0 { (p0 * x, p0 * (x + 1)) } else { ((x - 1 - x0) + (x0 + 1) * p0, (x - x0) + (x0 + 1) * p0) };
|
||||
self.dec.update(fl, fh, ft);
|
||||
itheta = x as i32;
|
||||
} else if b0 > 1 || stereo {
|
||||
itheta = self.dec.uint(qn as u32 + 1) as i32;
|
||||
} else {
|
||||
let half = (qn >> 1) as u32;
|
||||
let ft = (half + 1) * (half + 1);
|
||||
let fm = self.dec.decode(ft);
|
||||
let (fl, fs);
|
||||
if fm < (half * (half + 1)) >> 1 {
|
||||
let it = (isqrt32(8 * fm + 1) - 1) >> 1;
|
||||
fs = it + 1;
|
||||
fl = it * (it + 1) >> 1;
|
||||
itheta = it as i32;
|
||||
} else {
|
||||
let q = qn as u32;
|
||||
let it = (2 * (q + 1) - isqrt32(8 * (ft - fm - 1) + 1)) >> 1;
|
||||
fs = q + 1 - it;
|
||||
fl = ft - ((q + 1 - it) * (q + 2 - it) >> 1);
|
||||
itheta = it as i32;
|
||||
}
|
||||
self.dec.update(fl, fl + fs, ft);
|
||||
}
|
||||
itheta = ((itheta as u32 * 16384) / qn as u32) as i32;
|
||||
} else if stereo {
|
||||
inv = self.decode_inversion(*b);
|
||||
if *b > 2 << BITRES && self.remaining_bits > 2 << BITRES {
|
||||
inv = self.dec.bit_logp(2);
|
||||
}
|
||||
if self.disable_inv {
|
||||
inv = false;
|
||||
}
|
||||
itheta = 0;
|
||||
}
|
||||
let qalloc = self.dec.tell_frac() as i32 - tell;
|
||||
*b -= qalloc;
|
||||
let (imid, iside, delta) = split_gains(itheta, n, bb, fill);
|
||||
Split { inv, imid, iside, delta, itheta, qalloc }
|
||||
}
|
||||
|
||||
/// Decodes the quantised stereo/band angle for `qn > 1` and scales it to the 0..=16384 range.
|
||||
fn decode_itheta(&mut self, qn: i32, n: i32, b0: usize, stereo: bool) -> i32 {
|
||||
let itheta = if stereo && n > 2 {
|
||||
self.decode_itheta_stereo_triangle(qn)
|
||||
} else if b0 > 1 || stereo {
|
||||
self.dec.uint(qn as u32 + 1) as i32
|
||||
let (imid, iside, delta);
|
||||
if itheta == 0 {
|
||||
imid = 32767;
|
||||
iside = 0;
|
||||
*fill &= (1 << bb) - 1;
|
||||
delta = -16384;
|
||||
} else if itheta == 16384 {
|
||||
imid = 0;
|
||||
iside = 32767;
|
||||
*fill &= ((1 << bb) - 1) << bb;
|
||||
delta = 16384;
|
||||
} else {
|
||||
self.decode_itheta_triangle(qn)
|
||||
};
|
||||
((itheta as u32 * 16384) / qn as u32) as i32
|
||||
}
|
||||
|
||||
fn decode_itheta_stereo_triangle(&mut self, qn: i32) -> i32 {
|
||||
let p0 = 3u32;
|
||||
let x0 = (qn / 2) as u32;
|
||||
let ft = p0 * (x0 + 1) + x0;
|
||||
let fs = self.dec.decode(ft);
|
||||
let x = if fs < (x0 + 1) * p0 { fs / p0 } else { x0 + 1 + (fs - (x0 + 1) * p0) };
|
||||
let (fl, fh) = if x <= x0 { (p0 * x, p0 * (x + 1)) } else { ((x - 1 - x0) + (x0 + 1) * p0, (x - x0) + (x0 + 1) * p0) };
|
||||
self.dec.update(fl, fh, ft);
|
||||
x as i32
|
||||
}
|
||||
|
||||
fn decode_itheta_triangle(&mut self, qn: i32) -> i32 {
|
||||
let half = (qn >> 1) as u32;
|
||||
let ft = (half + 1) * (half + 1);
|
||||
let fm = self.dec.decode(ft);
|
||||
let (fl, fs, itheta);
|
||||
if fm < (half * (half + 1)) >> 1 {
|
||||
let it = (isqrt32(8 * fm + 1) - 1) >> 1;
|
||||
fs = it + 1;
|
||||
fl = it * (it + 1) >> 1;
|
||||
itheta = it as i32;
|
||||
} else {
|
||||
let q = qn as u32;
|
||||
let it = (2 * (q + 1) - isqrt32(8 * (ft - fm - 1) + 1)) >> 1;
|
||||
fs = q + 1 - it;
|
||||
fl = ft - ((q + 1 - it) * (q + 2 - it) >> 1);
|
||||
itheta = it as i32;
|
||||
imid = bitexact_cos(itheta);
|
||||
iside = bitexact_cos(16384 - itheta);
|
||||
delta = frac_mul16((n - 1) << 7, bitexact_log2tan(iside, imid));
|
||||
}
|
||||
self.dec.update(fl, fl + fs, ft);
|
||||
itheta
|
||||
}
|
||||
|
||||
/// Reads the stereo inversion flag when there are enough bits left (never when inversion is disabled).
|
||||
fn decode_inversion(&mut self, b: i32) -> bool {
|
||||
let inv = b > 2 << BITRES && self.remaining_bits > 2 << BITRES && self.dec.bit_logp(2);
|
||||
inv && !self.disable_inv
|
||||
Split { inv, imid, iside, delta, itheta, qalloc }
|
||||
}
|
||||
|
||||
fn quant_band_n1(&mut self, x: &mut [f32], y: Option<&mut [f32]>, lowband_out: Option<&mut [f32]>) -> u32 {
|
||||
@@ -411,7 +386,7 @@ impl BandCtx<'_, '_, '_> {
|
||||
fill = (fill & 1) | (fill << 1);
|
||||
}
|
||||
bb = (bb + 1) >> 1;
|
||||
let sp = self.compute_theta(ThetaShape { n: n as i32, bb, b0, lm, stereo: false }, &mut b, &mut fill);
|
||||
let sp = self.compute_theta(n as i32, &mut b, bb, b0, lm, false, &mut fill);
|
||||
let mid = sp.imid as f32 / 32768.0;
|
||||
let side = sp.iside as f32 / 32768.0;
|
||||
let mut delta = sp.delta;
|
||||
@@ -599,7 +574,7 @@ impl BandCtx<'_, '_, '_> {
|
||||
if n == 1 {
|
||||
return self.quant_band_n1(x, Some(y), lowband_out);
|
||||
}
|
||||
let sp = self.compute_theta(ThetaShape { n: n as i32, bb, b0: bb, lm, stereo: true }, &mut b, &mut fill);
|
||||
let sp = self.compute_theta(n as i32, &mut b, bb, bb, lm, true, &mut fill);
|
||||
let mid = sp.imid as f32 / 32768.0;
|
||||
let side = sp.iside as f32 / 32768.0;
|
||||
let mut cm;
|
||||
|
||||
+79
-140
@@ -202,10 +202,19 @@ impl StreamDecoder {
|
||||
Some(m) => m,
|
||||
};
|
||||
if audiosize > f20 {
|
||||
self.conceal_long(pcm, audiosize, f20)?;
|
||||
let mut done = 0;
|
||||
while done < audiosize {
|
||||
let r = self.decode_frame(None, &mut pcm[done * ch..], (audiosize - done).min(f20), false)?;
|
||||
done += r;
|
||||
}
|
||||
return Ok(frame_size);
|
||||
} else if audiosize < f20 {
|
||||
if audiosize > f10 {
|
||||
audiosize = f10;
|
||||
} else if mode != Mode::SilkOnly && audiosize > f5 && audiosize < f10 {
|
||||
audiosize = f5;
|
||||
}
|
||||
}
|
||||
audiosize = shorten_concealment(audiosize, mode, fs);
|
||||
}
|
||||
}
|
||||
let data_len = data.map_or(0, <[_]>::len);
|
||||
@@ -239,21 +248,76 @@ impl StreamDecoder {
|
||||
// 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 },
|
||||
)?;
|
||||
pcm_silk.resize(frame_size.max(f10) * ch, 0.0);
|
||||
if self.prev_mode == Some(Mode::CeltOnly) {
|
||||
self.silk.reset();
|
||||
}
|
||||
let payload_ms = (1000 * audiosize / fs).max(10);
|
||||
let internal_rate = if mode == Mode::SilkOnly {
|
||||
match bandwidth {
|
||||
Some(Bandwidth::Narrowband) => 8000,
|
||||
Some(Bandwidth::Mediumband) => 12000,
|
||||
_ => 16000,
|
||||
}
|
||||
} else {
|
||||
16000
|
||||
};
|
||||
if data.is_some() {
|
||||
self.silk.set_stream(self.stream_channels, internal_rate);
|
||||
}
|
||||
let lost = if data.is_none() {
|
||||
LostFlag::Lost
|
||||
} else if fec {
|
||||
LostFlag::Lbrr
|
||||
} else {
|
||||
LostFlag::Normal
|
||||
};
|
||||
let mut decoded = 0;
|
||||
while decoded < frame_size {
|
||||
let first = decoded == 0;
|
||||
let n = match self.silk.decode(dec.as_deref_mut(), lost, first, payload_ms, &mut pcm_silk[decoded * ch..]) {
|
||||
Ok(n) => n,
|
||||
Err(e) => {
|
||||
if lost != LostFlag::Normal {
|
||||
let n = frame_size - decoded;
|
||||
pcm_silk[decoded * ch..(decoded + n) * ch].fill(0.0);
|
||||
n
|
||||
} else {
|
||||
return Err(e);
|
||||
}
|
||||
}
|
||||
};
|
||||
if n == 0 {
|
||||
break;
|
||||
}
|
||||
decoded += n;
|
||||
}
|
||||
}
|
||||
|
||||
let mut start_band = 0;
|
||||
let mut redundancy = Redundancy::none(data_len);
|
||||
let mut redundancy = false;
|
||||
let mut celt_to_silk = false;
|
||||
let mut redundancy_bytes = 0usize;
|
||||
let mut len = data_len;
|
||||
if !fec
|
||||
&& mode != Mode::CeltOnly
|
||||
&& let Some(d) = dec.as_deref_mut()
|
||||
&& d.tell() + 17 + 20 * (self.mode == Some(Mode::Hybrid)) as i32 <= 8 * len as i32
|
||||
{
|
||||
redundancy = Redundancy::parse(d, mode, self.mode == Some(Mode::Hybrid), data_len);
|
||||
redundancy = if mode == Mode::Hybrid { d.bit_logp(12) } else { true };
|
||||
if redundancy {
|
||||
celt_to_silk = d.bit_logp(1);
|
||||
redundancy_bytes = if mode == Mode::Hybrid { d.uint(256) as usize + 2 } else { len - ((d.tell() as usize + 7) >> 3) };
|
||||
if redundancy_bytes > len || (len - redundancy_bytes) * 8 < d.tell() as usize {
|
||||
len = 0;
|
||||
redundancy_bytes = 0;
|
||||
redundancy = false;
|
||||
} else {
|
||||
len -= redundancy_bytes;
|
||||
}
|
||||
d.shrink_storage(redundancy_bytes);
|
||||
}
|
||||
}
|
||||
let Redundancy { present: redundancy, celt_to_silk, bytes: redundancy_bytes, len } = redundancy;
|
||||
if mode != Mode::CeltOnly {
|
||||
start_band = 17;
|
||||
}
|
||||
@@ -311,11 +375,15 @@ impl StreamDecoder {
|
||||
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);
|
||||
pcm[..f2_5 * ch].copy_from_slice(&redundant_audio[..f2_5 * ch]);
|
||||
let cur: Vec<f32> = pcm[ch * f2_5..ch * 2 * f2_5].to_vec();
|
||||
smooth_fade(&redundant_audio[ch * f2_5..], &cur, &mut pcm[ch * f2_5..], f2_5, ch, window, self.rate);
|
||||
}
|
||||
if transition {
|
||||
if audiosize >= f5 {
|
||||
self.fade_in_from(pcm, &pcm_transition, f2_5);
|
||||
pcm[..f2_5 * ch].copy_from_slice(&pcm_transition[..f2_5 * ch]);
|
||||
let cur: Vec<f32> = pcm[ch * f2_5..ch * 2 * f2_5].to_vec();
|
||||
smooth_fade(&pcm_transition[ch * f2_5..], &cur, &mut pcm[ch * f2_5..], f2_5, ch, window, self.rate);
|
||||
} else {
|
||||
let cur: Vec<f32> = pcm[..ch * f2_5].to_vec();
|
||||
smooth_fade(&pcm_transition, &cur, pcm, f2_5, ch, window, self.rate);
|
||||
@@ -335,135 +403,6 @@ impl StreamDecoder {
|
||||
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) {
|
||||
|
||||
+103
-143
@@ -716,7 +716,19 @@ impl SilkDecoder {
|
||||
}
|
||||
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)?;
|
||||
for c in self.ch.iter_mut().take(nci) {
|
||||
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")),
|
||||
};
|
||||
c.n_frames_per_packet = nfpp;
|
||||
c.nb_subfr = nb;
|
||||
let fs_khz = ((self.internal_rate >> 10) + 1) as usize;
|
||||
c.set_fs(fs_khz, self.api_rate);
|
||||
}
|
||||
}
|
||||
if self.api_channels == 2 && nci == 2 && self.channels_internal_prev == 1 {
|
||||
self.stereo.pred_prev_q13 = [0; 2];
|
||||
@@ -727,20 +739,97 @@ impl SilkDecoder {
|
||||
|
||||
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);
|
||||
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 {
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if lost == LostFlag::Normal {
|
||||
self.skip_lbrr_data(d, nci);
|
||||
// Skip LBRR data.
|
||||
for i in 0..self.ch[0].n_frames_per_packet {
|
||||
for n in 0..nci {
|
||||
if self.ch[n].lbrr_flags[i] {
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// Stereo prediction.
|
||||
if nci == 2 {
|
||||
self.decode_stereo_prediction(dec.as_deref_mut(), lost);
|
||||
let fi = self.ch[0].n_frames_decoded;
|
||||
let coded = lost == LostFlag::Normal || (lost == LostFlag::Lbrr && self.ch[0].lbrr_flags[fi]);
|
||||
match dec.as_deref_mut() {
|
||||
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 = if side_inactive { d.icdf(&MID_ONLY, 8) == 1 } else { false };
|
||||
}
|
||||
_ => {
|
||||
self.ms_pred = self.stereo.pred_prev_q13;
|
||||
}
|
||||
}
|
||||
}
|
||||
if nci == 2 && !self.decode_only_middle && self.prev_decode_only_middle {
|
||||
self.reset_side_channel();
|
||||
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;
|
||||
}
|
||||
let l = self.ch[0].frame_length;
|
||||
let mut buf = [[0i16; MAX_FRAME + 2]; 2];
|
||||
self.decode_channel_frames(dec, lost, nci, &mut buf);
|
||||
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])
|
||||
};
|
||||
for n in 0..nci {
|
||||
if n == 0 || has_side {
|
||||
let frame_index = self.ch[0].n_frames_decoded as isize - n as isize;
|
||||
let cond = 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
|
||||
};
|
||||
let c = &mut self.ch[n];
|
||||
c.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;
|
||||
}
|
||||
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);
|
||||
@@ -748,142 +837,6 @@ impl SilkDecoder {
|
||||
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);
|
||||
@@ -907,6 +860,13 @@ impl SilkDecoder {
|
||||
}
|
||||
}
|
||||
}
|
||||
n_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)
|
||||
}
|
||||
}
|
||||
|
||||
+11
-64
@@ -178,7 +178,7 @@ fn export_inner(pres: &Presentation, opts: &PdfOptions) -> Result<Vec<u8>> {
|
||||
let pdf = writer.document_mut();
|
||||
|
||||
let (sw, sh) = (pres.slide_size.width.max(1.0), pres.slide_size.height.max(1.0));
|
||||
let mut ex = Exporter { pres, opts, fonts: HashMap::new(), backdrops: Backdrops::default() };
|
||||
let mut ex = Exporter { pres, opts, fonts: HashMap::new() };
|
||||
// Page plan: per page, the slides on it with their boxes (page points).
|
||||
let pages = plan_pages(pres, opts, &slides, sw, sh);
|
||||
// Bookmarks and slide → page lookup for internal links.
|
||||
@@ -391,7 +391,6 @@ struct Exporter<'a> {
|
||||
pres: &'a Presentation,
|
||||
opts: &'a PdfOptions,
|
||||
fonts: HashMap<u32, Font>,
|
||||
backdrops: Backdrops,
|
||||
}
|
||||
|
||||
fn tf(a: Affine) -> Transform {
|
||||
@@ -407,19 +406,8 @@ fn grey(v: u8, opacity: f32) -> Fill {
|
||||
}
|
||||
}
|
||||
|
||||
/// Premultiplied RGBA (the renderer's pixels) to the straight RGBA the writer stores. Runs of
|
||||
/// pixels that are all fully transparent or fully opaque, most of a slide, are skipped in blocks.
|
||||
/// Premultiplied RGBA (the renderer's pixels) to the straight RGBA the writer stores.
|
||||
fn straight(rgba: &mut [u8]) {
|
||||
let (blocks, rest) = rgba.as_chunks_mut::<64>();
|
||||
for block in blocks {
|
||||
if block.as_chunks::<4>().0.iter().fold(false, |m, px| m | (px[3].wrapping_sub(1) < 254)) {
|
||||
straight_px(block);
|
||||
}
|
||||
}
|
||||
straight_px(rest);
|
||||
}
|
||||
|
||||
fn straight_px(rgba: &mut [u8]) {
|
||||
for px in rgba.as_chunks_mut::<4>().0 {
|
||||
let a = px[3] as u32;
|
||||
if a != 0 && a != 255 {
|
||||
@@ -430,58 +418,17 @@ fn straight_px(rgba: &mut [u8]) {
|
||||
}
|
||||
}
|
||||
|
||||
/// A backdrop raster composited onto white (slides are normally opaque, and opaque blocks are skipped).
|
||||
fn opaque(rgba: &mut [u8]) {
|
||||
let (blocks, rest) = rgba.as_chunks_mut::<64>();
|
||||
for block in blocks {
|
||||
if !block.as_chunks::<4>().0.iter().fold(true, |m, px| m & (px[3] == 255)) {
|
||||
opaque_px(block);
|
||||
/// A backdrop raster as one PDF image, composited onto white (slides are normally opaque). Equal
|
||||
/// backdrops become equal images, which the writer stores once.
|
||||
fn backdrop(mut img: deckcraft_render::Image) -> Image {
|
||||
for px in img.pixels.as_chunks_mut::<4>().0 {
|
||||
let a = px[3] as u32;
|
||||
for c in &mut px[..3] {
|
||||
*c = (*c as u32 + (255 - a)).min(255) as u8;
|
||||
}
|
||||
}
|
||||
opaque_px(rest);
|
||||
}
|
||||
|
||||
fn opaque_px(rgba: &mut [u8]) {
|
||||
for px in rgba.as_chunks_mut::<4>().0 {
|
||||
let k = 255 - px[3];
|
||||
px[0] = px[0].saturating_add(k);
|
||||
px[1] = px[1].saturating_add(k);
|
||||
px[2] = px[2].saturating_add(k);
|
||||
px[3] = 255;
|
||||
}
|
||||
}
|
||||
|
||||
/// Most bytes of opaque backdrop rasters kept to recognise a backdrop seen before.
|
||||
const BACKDROP_CACHE_BYTES: usize = 32 << 20;
|
||||
|
||||
/// Backdrops already handed to the writer, most recent first, with their pixels. Slides that
|
||||
/// share a layout render the same backdrop; reusing its image skips hashing and compressing the
|
||||
/// raster again, and the writer stores it once either way (#845).
|
||||
#[derive(Default)]
|
||||
struct Backdrops(Vec<(Vec<u8>, u32, u32, Image)>);
|
||||
|
||||
impl Backdrops {
|
||||
fn image(&mut self, mut img: deckcraft_render::Image) -> Image {
|
||||
opaque(&mut img.pixels);
|
||||
let (w, h) = (img.width, img.height);
|
||||
if let Some(i) = self.0.iter().position(|(px, pw, ph, _)| (*pw, *ph) == (w, h) && *px == img.pixels) {
|
||||
let hit = self.0.remove(i);
|
||||
let image = hit.3.clone();
|
||||
self.0.insert(0, hit);
|
||||
return image;
|
||||
}
|
||||
if img.pixels.len() > BACKDROP_CACHE_BYTES {
|
||||
return Image::from_rgba8(img.pixels, w, h);
|
||||
}
|
||||
let image = Image::from_rgba8(img.pixels.clone(), w, h);
|
||||
self.0.insert(0, (img.pixels, w, h, image.clone()));
|
||||
let mut total = 0;
|
||||
self.0.retain(|(px, ..)| {
|
||||
total += px.len();
|
||||
total <= BACKDROP_CACHE_BYTES
|
||||
});
|
||||
image
|
||||
}
|
||||
Image::from_rgba8(img.pixels, img.width, img.height)
|
||||
}
|
||||
|
||||
/// Most pixels one slide raster may have; it is RGBA, and the backdrop and the content are two rasters.
|
||||
@@ -523,7 +470,7 @@ impl Exporter<'_> {
|
||||
&& let Some(size) = Size::from_wh(b.width() as f32, b.height() as f32)
|
||||
{
|
||||
s.push_transform(&Transform::from_translate(b.x0 as f32, b.y0 as f32));
|
||||
s.draw_image(self.backdrops.image(back), size);
|
||||
s.draw_image(backdrop(back), size);
|
||||
// Taken, not copied: a print-resolution slide is ~16 MB.
|
||||
let mut rgba = front.pixels;
|
||||
if rgba.as_chunks::<4>().0.iter().any(|px| px[3] != 0) {
|
||||
|
||||
+1
-46
@@ -8,7 +8,7 @@ use kurbo::Rect;
|
||||
|
||||
use super::{
|
||||
MAX_RASTER_PIXELS, PAPERS, PageLayout, PdfError, PdfOptions, Presentation, export, glyph_text_ranges, handout_boxes, master_text_pieces,
|
||||
master_texts, notes_frames, notes_slide_box, opaque, paper_size, raster_scale, straight,
|
||||
master_texts, notes_frames, notes_slide_box, paper_size, raster_scale,
|
||||
};
|
||||
use deckcraft_model::text::TextBody;
|
||||
|
||||
@@ -587,51 +587,6 @@ fn slides_share_one_backdrop_raster_and_keep_their_pixels() {
|
||||
let want_alpha: Vec<u8> = straight.as_chunks::<4>().0.iter().map(|px| px[3]).collect();
|
||||
assert_eq!(rgb, want_rgb, "content colors are straight, not premultiplied");
|
||||
assert_eq!(alpha, Some(want_alpha), "content alpha keeps the shape's coverage");
|
||||
|
||||
// A backdrop that comes back after another one is still stored once, and every page draws its
|
||||
// own slide's backdrop.
|
||||
let mut p = deck(4);
|
||||
for (i, slide) in p.slides.iter_mut().enumerate() {
|
||||
let s = Arc::make_mut(slide);
|
||||
s.shapes.clear();
|
||||
let c = if i % 2 == 0 { deckcraft_model::Rgba::rgb(20, 90, 160) } else { deckcraft_model::Rgba::rgb(230, 230, 210) };
|
||||
s.background = Some(deckcraft_model::Background::Fill { fill: deckcraft_model::Fill::solid(deckcraft_model::ColorRef::rgb(c)) });
|
||||
}
|
||||
let stored = images(&p);
|
||||
assert_eq!(stored.iter().map(|(pages, ..)| pages.clone()).collect::<Vec<_>>(), [vec![0, 2], vec![1, 3]]);
|
||||
for (pages, _, rgb, alpha, _) in &stored {
|
||||
let expect = deckcraft_render::render_slide(&p, pages[0], &RenderOpts { scale: scale(&p), threads: 4, ..Default::default() });
|
||||
let want: Vec<u8> = expect.pixels.as_chunks::<4>().0.iter().flat_map(|px| px[..3].iter().copied()).collect();
|
||||
assert_eq!(rgb, &want, "pages {pages:?} draw their own backdrop");
|
||||
assert!(alpha.is_none());
|
||||
}
|
||||
}
|
||||
|
||||
/// The block-skipping pixel conversions give exactly the per-pixel formulas, in blocks that skip
|
||||
/// and blocks that don't, and in a tail shorter than a block.
|
||||
#[test]
|
||||
fn backdrop_and_content_pixels_convert_per_pixel() {
|
||||
let mut px: Vec<u8> = Vec::new();
|
||||
for a in 0..=255u8 {
|
||||
for c in 0..=255u8 {
|
||||
px.extend([c, c / 2, 255 - c, a]);
|
||||
}
|
||||
}
|
||||
px.extend([0, 0, 0, 0].repeat(32));
|
||||
px.extend([9, 8, 7, 255].repeat(32));
|
||||
px.extend([1, 2, 3, 128, 4, 5, 6, 0]);
|
||||
let (mut o, mut s) = (px.clone(), px.clone());
|
||||
opaque(&mut o);
|
||||
straight(&mut s);
|
||||
for ((p, o), s) in px.chunks(4).zip(o.chunks(4)).zip(s.chunks(4)) {
|
||||
let a = p[3] as u32;
|
||||
for k in 0..3 {
|
||||
assert_eq!(o[k] as u32, (p[k] as u32 + 255 - a).min(255), "{p:?}");
|
||||
let want = if a == 0 || a == 255 { p[k] as u32 } else { ((p[k] as u32 * 255 + a / 2) / a).min(255) };
|
||||
assert_eq!(s[k] as u32, want, "{p:?}");
|
||||
}
|
||||
assert_eq!((o[3], s[3]), (255, p[3]));
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
+167
-239
@@ -413,56 +413,7 @@ pub fn import_checked(bytes: &[u8]) -> Result<(Presentation, Vec<String>), PptxE
|
||||
}
|
||||
let pres_part = Part::new(&pkg, &pres_path, &pres_doc);
|
||||
|
||||
let (master_paths, slide_entries) = master_and_slide_paths(pres_el, &pres_part);
|
||||
let (docs, max_id) = load_part_docs(&pkg, &master_paths, &slide_entries);
|
||||
|
||||
let mut imp = Imp {
|
||||
pkg: &pkg,
|
||||
media: vec![],
|
||||
media_by_part: HashMap::new(),
|
||||
next: max_id.max(1),
|
||||
slide_by_part: HashMap::new(),
|
||||
custom_show_names: HashMap::new(),
|
||||
table_style_ids: Vec::new(),
|
||||
vml_previews: HashMap::new(),
|
||||
kept_sidecar_bytes: 0,
|
||||
kept_charts: HashMap::new(),
|
||||
};
|
||||
// Slide ids first so hyperlinks anywhere can point at slides.
|
||||
let slide_ids = register_slides(&mut imp, &docs, &slide_entries, &mut lost_slides);
|
||||
read_custom_show_names(&mut imp, pres_el);
|
||||
|
||||
let mut p = defaults::blank_presentation(defaults::WIDE, Default::default(), false);
|
||||
p.masters.clear();
|
||||
p.media.clear();
|
||||
p.default_text_style = Default::default();
|
||||
|
||||
read_presentation_settings(&mut imp, &mut p, &pres_part, pres_el);
|
||||
let layout_by_part = read_masters(&mut imp, &mut p, &master_paths, &docs);
|
||||
let file_slide_ids = read_slides(&mut imp, &mut p, &pres_part, &docs, &slide_ids, &layout_by_part);
|
||||
read_sections(&mut p, &pres_part, pres_el, &file_slide_ids);
|
||||
p.tags = tags(&imp, &pres_part, pres_el);
|
||||
read_custom_shows(&mut p, &imp, &pres_part, pres_el);
|
||||
read_guides_and_show(&mut p, &imp, &pres_part);
|
||||
read_notes_master(&mut imp, &mut p, &pres_part);
|
||||
read_handout_master(&mut imp, &mut p, &pres_part);
|
||||
read_document_properties(&mut p, &pkg, &root_rels);
|
||||
|
||||
p.kept_parts = kept_parts(&pkg, &pres_path);
|
||||
|
||||
inherit_placeholder_geometry(&mut p);
|
||||
default_text_anchor(&mut p);
|
||||
p.header_footer = header_footer(&p);
|
||||
p.embedded_fonts = read_embedded_fonts(&mut imp, &pres_part, pres_el);
|
||||
p.media = std::mem::take(&mut imp.media);
|
||||
p.next_id = imp.next;
|
||||
p.fix_next_id();
|
||||
Ok((p, lost_slides))
|
||||
}
|
||||
|
||||
/// Masters, layouts and slides to read, in file order and without duplicates: (master paths,
|
||||
/// (file slide id, slide path)).
|
||||
fn master_and_slide_paths(pres_el: &El, pres_part: &Part) -> (Vec<String>, Vec<(u32, String)>) {
|
||||
// Masters, layouts and slides to read.
|
||||
let master_paths: Vec<String> = pres_el
|
||||
.child("sldMasterIdLst")
|
||||
.map(|l| l.children_named("sldMasterId").filter_map(|m| pres_part.rels.target(m.attr("r:id").unwrap_or(""))).map(String::from).collect())
|
||||
@@ -478,19 +429,15 @@ fn master_and_slide_paths(pres_el: &El, pres_part: &Part) -> (Vec<String>, Vec<(
|
||||
.collect()
|
||||
})
|
||||
.unwrap_or_default();
|
||||
|
||||
let mut docs: HashMap<String, Doc> = HashMap::new();
|
||||
let mut max_id = 0u32;
|
||||
let mut seen_masters = HashSet::new();
|
||||
master_paths.retain(|m| seen_masters.insert(opc::norm(m)));
|
||||
let mut seen_slides = HashSet::new();
|
||||
slide_entries.retain(|(_, p)| seen_slides.insert(opc::norm(p)));
|
||||
(master_paths, slide_entries)
|
||||
}
|
||||
|
||||
/// Loads every master, layout and slide part once. Returns them by normalized path and the
|
||||
/// largest shape id found in them.
|
||||
fn load_part_docs(pkg: &Package, master_paths: &[String], slide_entries: &[(u32, String)]) -> (HashMap<String, Doc>, u32) {
|
||||
let mut all_parts: Vec<String> = master_paths.to_vec();
|
||||
let mut all_parts: Vec<String> = master_paths.clone();
|
||||
let mut seen_layouts = HashSet::new();
|
||||
for m in master_paths {
|
||||
for m in &master_paths {
|
||||
for r in pkg.rels(m).all_of("slideLayout") {
|
||||
let key = opc::norm(&r.target);
|
||||
if seen_layouts.insert(key) {
|
||||
@@ -498,32 +445,35 @@ fn load_part_docs(pkg: &Package, master_paths: &[String], slide_entries: &[(u32,
|
||||
}
|
||||
}
|
||||
}
|
||||
let mut seen_slides = HashSet::new();
|
||||
slide_entries.retain(|(_, p)| seen_slides.insert(opc::norm(p)));
|
||||
all_parts.extend(slide_entries.iter().map(|(_, p)| p.clone()));
|
||||
let mut docs: HashMap<String, Doc> = HashMap::new();
|
||||
let mut max_id = 0u32;
|
||||
for p in &all_parts {
|
||||
let key = opc::norm(p);
|
||||
if docs.contains_key(&key) {
|
||||
continue;
|
||||
}
|
||||
if let Some(d) = load(pkg, p) {
|
||||
if let Some(d) = load(&pkg, p) {
|
||||
max_cnvpr(&d.root, &mut max_id);
|
||||
docs.insert(key, d);
|
||||
}
|
||||
}
|
||||
(docs, max_id)
|
||||
}
|
||||
|
||||
/// Allocates an id for every readable slide; slides that are missing or unreadable are recorded in
|
||||
/// `lost_slides` and left out.
|
||||
fn register_slides(
|
||||
imp: &mut Imp,
|
||||
docs: &HashMap<String, Doc>,
|
||||
slide_entries: &[(u32, String)],
|
||||
lost_slides: &mut Vec<String>,
|
||||
) -> Vec<(u32, String, SlideId)> {
|
||||
let mut imp = Imp {
|
||||
pkg: &pkg,
|
||||
media: vec![],
|
||||
media_by_part: HashMap::new(),
|
||||
next: max_id.max(1),
|
||||
slide_by_part: HashMap::new(),
|
||||
custom_show_names: HashMap::new(),
|
||||
table_style_ids: Vec::new(),
|
||||
vml_previews: HashMap::new(),
|
||||
kept_sidecar_bytes: 0,
|
||||
kept_charts: HashMap::new(),
|
||||
};
|
||||
// Slide ids first so hyperlinks anywhere can point at slides.
|
||||
let mut slide_ids: Vec<(u32, String, SlideId)> = vec![];
|
||||
for (file_id, path) in slide_entries {
|
||||
for (file_id, path) in &slide_entries {
|
||||
if !docs.contains_key(&opc::norm(path)) {
|
||||
log::warn!("pptx: slide {path} is missing or unreadable; skipped");
|
||||
lost_slides.push(path.clone());
|
||||
@@ -533,10 +483,6 @@ fn register_slides(
|
||||
imp.slide_by_part.insert(opc::norm(path), id);
|
||||
slide_ids.push((*file_id, path.clone(), id));
|
||||
}
|
||||
slide_ids
|
||||
}
|
||||
|
||||
fn read_custom_show_names(imp: &mut Imp, pres_el: &El) {
|
||||
if let Some(l) = pres_el.child("custShowLst") {
|
||||
for cs in l.children_named("custShow") {
|
||||
if let (Some(id), Some(name)) = (cs.attr("id"), cs.attr("name")) {
|
||||
@@ -544,10 +490,12 @@ fn read_custom_show_names(imp: &mut Imp, pres_el: &El) {
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Slide and notes size, first slide number, default text style and table styles.
|
||||
fn read_presentation_settings(imp: &mut Imp, p: &mut Presentation, pres_part: &Part, pres_el: &El) {
|
||||
let mut p = defaults::blank_presentation(defaults::WIDE, Default::default(), false);
|
||||
p.masters.clear();
|
||||
p.media.clear();
|
||||
p.default_text_style = Default::default();
|
||||
|
||||
if let Some(sz) = pres_el.child("sldSz") {
|
||||
let w = sz.i64("cx").map(emu_to_pt).unwrap_or(960.0);
|
||||
let h = sz.i64("cy").map(emu_to_pt).unwrap_or(540.0);
|
||||
@@ -560,26 +508,24 @@ fn read_presentation_settings(imp: &mut Imp, p: &mut Presentation, pres_part: &P
|
||||
}
|
||||
p.first_slide_number = pres_el.u32("firstSlideNum").unwrap_or(1).min(9999);
|
||||
if let Some(dts) = pres_el.child("defaultTextStyle") {
|
||||
p.default_text_style = dml::list_style(imp, pres_part, dts);
|
||||
p.default_text_style = dml::list_style(&mut imp, &pres_part, dts);
|
||||
}
|
||||
if let Some(r) = pres_part.rels.first_of("tableStyles")
|
||||
&& let Some(d) = load(imp.pkg, &r.target)
|
||||
&& let Some(d) = load(&pkg, &r.target)
|
||||
{
|
||||
let part = Part::new(imp.pkg, &r.target, &d);
|
||||
p.table_styles = table_styles(imp, &part, &d.root);
|
||||
let part = Part::new(&pkg, &r.target, &d);
|
||||
p.table_styles = table_styles(&mut imp, &part, &d.root);
|
||||
imp.table_style_ids = p.table_styles.iter().map(|s| s.id.clone()).collect();
|
||||
}
|
||||
}
|
||||
|
||||
/// Reads the slide masters and their layouts; a file without a readable master gets a default one.
|
||||
fn read_masters(imp: &mut Imp, p: &mut Presentation, master_paths: &[String], docs: &HashMap<String, Doc>) -> HashMap<String, LayoutId> {
|
||||
// Masters and layouts.
|
||||
let mut layout_by_part: HashMap<String, LayoutId> = HashMap::new();
|
||||
for mp in master_paths {
|
||||
for mp in &master_paths {
|
||||
let Some(doc) = docs.get(&opc::norm(mp)) else {
|
||||
log::warn!("pptx: master {mp} missing; skipped");
|
||||
continue;
|
||||
};
|
||||
let master = read_master(imp, mp, doc, docs, &mut layout_by_part);
|
||||
let master = read_master(&mut imp, mp, doc, &docs, &mut layout_by_part);
|
||||
p.masters.push(Arc::new(master));
|
||||
}
|
||||
if p.masters.is_empty() {
|
||||
@@ -588,111 +534,87 @@ fn read_masters(imp: &mut Imp, p: &mut Presentation, master_paths: &[String], do
|
||||
imp.next = imp.next.saturating_add(200);
|
||||
p.masters.push(Arc::new(m));
|
||||
}
|
||||
layout_by_part
|
||||
}
|
||||
|
||||
/// Reads every registered slide with its notes and comments. Returns the slide ids by file id.
|
||||
fn read_slides(
|
||||
imp: &mut Imp,
|
||||
p: &mut Presentation,
|
||||
pres_part: &Part,
|
||||
docs: &HashMap<String, Doc>,
|
||||
slide_ids: &[(u32, String, SlideId)],
|
||||
layout_by_part: &HashMap<String, LayoutId>,
|
||||
) -> HashMap<u32, SlideId> {
|
||||
let pkg = imp.pkg;
|
||||
let fallback_layout = p.masters.first().and_then(|m| m.layouts.first()).map(|l| l.id).unwrap_or_default();
|
||||
|
||||
// Slides.
|
||||
let mut file_slide_ids: HashMap<u32, SlideId> = HashMap::new();
|
||||
let mut notes_by_target: HashMap<String, TextBody> = HashMap::new();
|
||||
for (file_id, path, id) in slide_ids {
|
||||
for (file_id, path, id) in &slide_ids {
|
||||
let Some(doc) = docs.get(&opc::norm(path)) else { continue };
|
||||
let part = Part::new(pkg, path, doc);
|
||||
let part = Part::new(&pkg, path, doc);
|
||||
let layout = part.rels.first_of("slideLayout").and_then(|r| layout_by_part.get(&opc::norm(&r.target)).copied()).unwrap_or(fallback_layout);
|
||||
let mut slide = read_slide(imp, &part, doc, *id, layout);
|
||||
if let Some(notes) = slide_notes(imp, &part, &mut notes_by_target) {
|
||||
slide.notes = notes;
|
||||
let mut slide = read_slide(&mut imp, &part, doc, *id, layout);
|
||||
// Notes.
|
||||
if let Some(n) = part.rels.first_of("notesSlide") {
|
||||
let n_key = opc::norm(&n.target);
|
||||
if let Some(cached_notes) = notes_by_target.get(&n_key) {
|
||||
slide.notes = cached_notes.clone();
|
||||
} else if let Some(nd) = load(&pkg, &n.target) {
|
||||
let np = Part::new(&pkg, &n.target, &nd);
|
||||
let notes = read_notes(&mut imp, &np, &nd.root);
|
||||
notes_by_target.insert(n_key, notes.clone());
|
||||
slide.notes = notes;
|
||||
}
|
||||
}
|
||||
// Comments. A modern part and the legacy part can describe the same threads (PowerPoint
|
||||
// writes both): the modern one wins, its flattened legacy copy is skipped.
|
||||
let mut parts: Vec<(Doc, bool)> = vec![];
|
||||
for c in part.rels.all_of("comments") {
|
||||
if let Some(cd) = pkg.xml(&c.target) {
|
||||
// The content type names the modern part; a package that types it only through a
|
||||
// Default entry instead of an Override is still modern when its root is p188.
|
||||
let modern = pkg.content_type(&c.target).as_deref() == Some(crate::write::CT_COMMENTS_MODERN) || cd.root.prefix() == "p188";
|
||||
parts.push((cd, modern));
|
||||
}
|
||||
}
|
||||
let has_modern = parts.iter().any(|(_, m)| *m);
|
||||
for (cd, modern) in parts {
|
||||
if has_modern && !modern {
|
||||
continue;
|
||||
}
|
||||
slide.comments.extend(read_comments(&pkg, &pres_part, &cd.root, modern));
|
||||
}
|
||||
read_slide_comments(pkg, pres_part, &part, &mut slide);
|
||||
file_slide_ids.insert(*file_id, *id);
|
||||
p.slides.push(Arc::new(slide));
|
||||
}
|
||||
file_slide_ids
|
||||
}
|
||||
|
||||
/// The slide's notes text, read once per notes part.
|
||||
fn slide_notes(imp: &mut Imp, part: &Part, cache: &mut HashMap<String, TextBody>) -> Option<TextBody> {
|
||||
let n = part.rels.first_of("notesSlide")?;
|
||||
let key = opc::norm(&n.target);
|
||||
if let Some(cached) = cache.get(&key) {
|
||||
return Some(cached.clone());
|
||||
}
|
||||
let pkg = imp.pkg;
|
||||
let nd = load(pkg, &n.target)?;
|
||||
let np = Part::new(pkg, &n.target, &nd);
|
||||
let notes = read_notes(imp, &np, &nd.root);
|
||||
cache.insert(key, notes.clone());
|
||||
Some(notes)
|
||||
}
|
||||
|
||||
/// Comments. A modern part and the legacy part can describe the same threads (PowerPoint
|
||||
/// writes both): the modern one wins, its flattened legacy copy is skipped.
|
||||
fn read_slide_comments(pkg: &Package, pres_part: &Part, part: &Part, slide: &mut Slide) {
|
||||
let mut parts: Vec<(Doc, bool)> = vec![];
|
||||
for c in part.rels.all_of("comments") {
|
||||
if let Some(cd) = pkg.xml(&c.target) {
|
||||
// The content type names the modern part; a package that types it only through a
|
||||
// Default entry instead of an Override is still modern when its root is p188.
|
||||
let modern = pkg.content_type(&c.target).as_deref() == Some(crate::write::CT_COMMENTS_MODERN) || cd.root.prefix() == "p188";
|
||||
parts.push((cd, modern));
|
||||
// Sections.
|
||||
if let Some(ext) = pres_el.child("extLst") {
|
||||
if let Some(sl) = ext.find("sectionLst") {
|
||||
for s in sl.children_named("section").take(10_000) {
|
||||
let slides = s
|
||||
.child("sldIdLst")
|
||||
.map(|l| l.children_named("sldId").filter_map(|x| x.u32("id").and_then(|i| file_slide_ids.get(&i).copied())).collect())
|
||||
.unwrap_or_default();
|
||||
p.sections.push(Section { name: s.attr("name").unwrap_or("").to_string(), slides });
|
||||
}
|
||||
}
|
||||
let kept: String = ext
|
||||
.children_named("ext")
|
||||
.filter(|e| e.attr("uri") != Some(crate::SECTION_EXT_URI) && !has_rel_refs(e))
|
||||
.map(|e| pres_part.keep(e))
|
||||
.collect();
|
||||
if !kept.is_empty() {
|
||||
p.raw_ext = Some(kept);
|
||||
}
|
||||
}
|
||||
let has_modern = parts.iter().any(|(_, m)| *m);
|
||||
for (cd, modern) in parts {
|
||||
if has_modern && !modern {
|
||||
continue;
|
||||
}
|
||||
slide.comments.extend(read_comments(pkg, pres_part, &cd.root, modern));
|
||||
}
|
||||
}
|
||||
|
||||
/// Sections, and the extension elements the model does not understand (kept verbatim).
|
||||
fn read_sections(p: &mut Presentation, pres_part: &Part, pres_el: &El, file_slide_ids: &HashMap<u32, SlideId>) {
|
||||
let Some(ext) = pres_el.child("extLst") else { return };
|
||||
if let Some(sl) = ext.find("sectionLst") {
|
||||
for s in sl.children_named("section").take(10_000) {
|
||||
let slides = s
|
||||
.child("sldIdLst")
|
||||
.map(|l| l.children_named("sldId").filter_map(|x| x.u32("id").and_then(|i| file_slide_ids.get(&i).copied())).collect())
|
||||
p.tags = tags(&imp, &pres_part, pres_el);
|
||||
// Custom shows.
|
||||
if let Some(l) = pres_el.child("custShowLst") {
|
||||
for cs in l.children_named("custShow") {
|
||||
let slides = cs
|
||||
.child("sldLst")
|
||||
.map(|l| {
|
||||
l.children_named("sld")
|
||||
.filter_map(|s| pres_part.rels.target(s.attr("r:id").unwrap_or("")))
|
||||
.filter_map(|t| imp.slide_by_part.get(&opc::norm(t)).copied())
|
||||
.collect()
|
||||
})
|
||||
.unwrap_or_default();
|
||||
p.sections.push(Section { name: s.attr("name").unwrap_or("").to_string(), slides });
|
||||
p.custom_shows.push(CustomShow { name: cs.attr("name").unwrap_or("").to_string(), slides });
|
||||
}
|
||||
}
|
||||
let kept: String =
|
||||
ext.children_named("ext").filter(|e| e.attr("uri") != Some(crate::SECTION_EXT_URI) && !has_rel_refs(e)).map(|e| pres_part.keep(e)).collect();
|
||||
if !kept.is_empty() {
|
||||
p.raw_ext = Some(kept);
|
||||
}
|
||||
}
|
||||
|
||||
fn read_custom_shows(p: &mut Presentation, imp: &Imp, pres_part: &Part, pres_el: &El) {
|
||||
let Some(l) = pres_el.child("custShowLst") else { return };
|
||||
for cs in l.children_named("custShow") {
|
||||
let slides = cs
|
||||
.child("sldLst")
|
||||
.map(|l| {
|
||||
l.children_named("sld")
|
||||
.filter_map(|s| pres_part.rels.target(s.attr("r:id").unwrap_or("")))
|
||||
.filter_map(|t| imp.slide_by_part.get(&opc::norm(t)).copied())
|
||||
.collect()
|
||||
})
|
||||
.unwrap_or_default();
|
||||
p.custom_shows.push(CustomShow { name: cs.attr("name").unwrap_or("").to_string(), slides });
|
||||
}
|
||||
}
|
||||
|
||||
/// Drawing guides (eighths of a point) and slide show settings.
|
||||
fn read_guides_and_show(p: &mut Presentation, imp: &Imp, pres_part: &Part) {
|
||||
let pkg = imp.pkg;
|
||||
// Drawing guides (eighths of a point).
|
||||
if let Some(r) = pres_part.rels.first_of("viewProps")
|
||||
&& let Some(d) = pkg.xml(&r.target)
|
||||
&& let Some(lst) = d.root.find("slideViewPr").and_then(|v| v.find("guideLst"))
|
||||
@@ -706,76 +628,71 @@ fn read_guides_and_show(p: &mut Presentation, imp: &Imp, pres_part: &Part) {
|
||||
.take(1000)
|
||||
.collect();
|
||||
}
|
||||
// Show settings.
|
||||
if let Some(r) = pres_part.rels.first_of("presProps")
|
||||
&& let Some(d) = pkg.xml(&r.target)
|
||||
&& let Some(sp) = d.root.child("showPr")
|
||||
{
|
||||
p.show = read_show(sp, &imp.custom_show_names);
|
||||
}
|
||||
}
|
||||
|
||||
/// Theme, background, shapes and colour map shared by the notes and handout masters.
|
||||
fn read_aux_master(imp: &mut Imp, part: &Part, doc: &Doc, m: &mut Master) {
|
||||
let mut ctx = shapes::IdCtx::default();
|
||||
if let Some(csld) = doc.root.child("cSld") {
|
||||
m.background = background(imp, part, csld);
|
||||
if let Some(tree) = csld.child("spTree") {
|
||||
m.shapes = shapes::sp_tree(imp, part, &mut ctx, tree, 0);
|
||||
// Notes master.
|
||||
if let Some(r) = pres_part.rels.first_of("notesMaster")
|
||||
&& let Some(d) = load(&pkg, &r.target)
|
||||
{
|
||||
let part = Part::new(&pkg, &r.target, &d);
|
||||
let mut ctx = shapes::IdCtx::default();
|
||||
let mut m = Master { id: MasterId(imp.alloc()), name: "Notes Master".into(), ..Default::default() };
|
||||
if let Some(t) = part.rels.first_of("theme")
|
||||
&& let Some(td) = pkg.xml(&t.target)
|
||||
{
|
||||
let tp = Part::new(&pkg, &t.target, &td);
|
||||
m.theme = theme::read_theme(&mut imp, &tp, &td.root);
|
||||
}
|
||||
if let Some(csld) = d.root.child("cSld") {
|
||||
m.background = background(&mut imp, &part, csld);
|
||||
if let Some(tree) = csld.child("spTree") {
|
||||
m.shapes = shapes::sp_tree(&mut imp, &part, &mut ctx, tree, 0);
|
||||
}
|
||||
}
|
||||
if let Some(cm) = d.root.child("clrMap") {
|
||||
m.color_map = color_map(cm);
|
||||
}
|
||||
// Notes text styles are the body's list style; slide masters use title/body/other instead.
|
||||
if let Some(ns) = d.root.child("notesStyle") {
|
||||
m.body_style = dml::list_style(&mut imp, &part, ns);
|
||||
}
|
||||
p.notes_master = Some(Arc::new(m));
|
||||
}
|
||||
if let Some(cm) = doc.root.child("clrMap") {
|
||||
m.color_map = color_map(cm);
|
||||
}
|
||||
}
|
||||
|
||||
/// Reads the part's own theme into the master, if it has one.
|
||||
fn read_master_theme(imp: &mut Imp, part: &Part, m: &mut Master) -> bool {
|
||||
let pkg = imp.pkg;
|
||||
if let Some(t) = part.rels.first_of("theme")
|
||||
&& let Some(td) = pkg.xml(&t.target)
|
||||
// Handout master (header, footer, date and slide number for printed handouts).
|
||||
if let Some(r) = pres_part.rels.first_of("handoutMaster")
|
||||
&& let Some(d) = load(&pkg, &r.target)
|
||||
{
|
||||
let tp = Part::new(pkg, &t.target, &td);
|
||||
m.theme = theme::read_theme(imp, &tp, &td.root);
|
||||
return true;
|
||||
let part = Part::new(&pkg, &r.target, &d);
|
||||
let mut ctx = shapes::IdCtx::default();
|
||||
let mut m = Master { id: MasterId(imp.alloc()), name: "Handout Master".into(), ..Default::default() };
|
||||
if let Some(t) = part.rels.first_of("theme")
|
||||
&& let Some(td) = pkg.xml(&t.target)
|
||||
{
|
||||
let tp = Part::new(&pkg, &t.target, &td);
|
||||
m.theme = theme::read_theme(&mut imp, &tp, &td.root);
|
||||
} else if let Some(first) = p.masters.first() {
|
||||
m.theme = first.theme.clone();
|
||||
}
|
||||
if let Some(csld) = d.root.child("cSld") {
|
||||
if let Some(n) = csld.attr("name").filter(|s| !s.is_empty()) {
|
||||
m.name = n.to_string();
|
||||
}
|
||||
m.background = background(&mut imp, &part, csld);
|
||||
if let Some(tree) = csld.child("spTree") {
|
||||
m.shapes = shapes::sp_tree(&mut imp, &part, &mut ctx, tree, 0);
|
||||
}
|
||||
}
|
||||
if let Some(cm) = d.root.child("clrMap") {
|
||||
m.color_map = color_map(cm);
|
||||
}
|
||||
p.handout_master = Some(Arc::new(m));
|
||||
}
|
||||
false
|
||||
}
|
||||
|
||||
fn read_notes_master(imp: &mut Imp, p: &mut Presentation, pres_part: &Part) {
|
||||
let pkg = imp.pkg;
|
||||
let Some(r) = pres_part.rels.first_of("notesMaster") else { return };
|
||||
let Some(d) = load(pkg, &r.target) else { return };
|
||||
let part = Part::new(pkg, &r.target, &d);
|
||||
let mut m = Master { id: MasterId(imp.alloc()), name: "Notes Master".into(), ..Default::default() };
|
||||
read_master_theme(imp, &part, &mut m);
|
||||
read_aux_master(imp, &part, &d, &mut m);
|
||||
// Notes text styles are the body's list style; slide masters use title/body/other instead.
|
||||
if let Some(ns) = d.root.child("notesStyle") {
|
||||
m.body_style = dml::list_style(imp, &part, ns);
|
||||
}
|
||||
p.notes_master = Some(Arc::new(m));
|
||||
}
|
||||
|
||||
/// Handout master (header, footer, date and slide number for printed handouts).
|
||||
fn read_handout_master(imp: &mut Imp, p: &mut Presentation, pres_part: &Part) {
|
||||
let pkg = imp.pkg;
|
||||
let Some(r) = pres_part.rels.first_of("handoutMaster") else { return };
|
||||
let Some(d) = load(pkg, &r.target) else { return };
|
||||
let part = Part::new(pkg, &r.target, &d);
|
||||
let mut m = Master { id: MasterId(imp.alloc()), name: "Handout Master".into(), ..Default::default() };
|
||||
if !read_master_theme(imp, &part, &mut m)
|
||||
&& let Some(first) = p.masters.first()
|
||||
{
|
||||
m.theme = first.theme.clone();
|
||||
}
|
||||
if let Some(n) = d.root.child("cSld").and_then(|c| c.attr("name")).filter(|s| !s.is_empty()) {
|
||||
m.name = n.to_string();
|
||||
}
|
||||
read_aux_master(imp, &part, &d, &mut m);
|
||||
p.handout_master = Some(Arc::new(m));
|
||||
}
|
||||
|
||||
fn read_document_properties(p: &mut Presentation, pkg: &Package, root_rels: &Rels) {
|
||||
// Document properties.
|
||||
if let Some(r) = root_rels.first_of("core-properties")
|
||||
&& let Some(d) = pkg.xml(&r.target)
|
||||
{
|
||||
@@ -786,6 +703,17 @@ fn read_document_properties(p: &mut Presentation, pkg: &Package, root_rels: &Rel
|
||||
{
|
||||
p.props.company = d.root.child("Company").map(|c| c.text()).unwrap_or_default();
|
||||
}
|
||||
|
||||
p.kept_parts = kept_parts(&pkg, &pres_path);
|
||||
|
||||
inherit_placeholder_geometry(&mut p);
|
||||
default_text_anchor(&mut p);
|
||||
p.header_footer = header_footer(&p);
|
||||
p.embedded_fonts = read_embedded_fonts(&mut imp, &pres_part, pres_el);
|
||||
p.media = std::mem::take(&mut imp.media);
|
||||
p.next_id = imp.next;
|
||||
p.fix_next_id();
|
||||
Ok((p, lost_slides))
|
||||
}
|
||||
|
||||
/// Embedded font faces in one file. Past this, further typefaces are dropped.
|
||||
|
||||
+155
-192
@@ -129,22 +129,167 @@ fn chart_within(root: &El, mut budget: usize) -> Option<Chart> {
|
||||
// (a combo chart's line on the right) is on the secondary axis.
|
||||
let val_map: std::collections::HashMap<&str, bool> =
|
||||
pa.children_named("valAx").filter_map(|a| Some((a.child_val("axId")?, matches!(a.child_val("axPos"), Some("l" | "r"))))).collect();
|
||||
let primary_val = primary_value_axis(pa, &val_map);
|
||||
// A group's value axis: its vertical one when it has two (scatter), else its one.
|
||||
let group_val_axis = |g: &El| {
|
||||
let ids: Vec<(&str, bool)> =
|
||||
g.children_named("axId").filter_map(|a| a.attr("val")).filter_map(|id| val_map.get(id).copied().map(|v| (id, v))).collect();
|
||||
ids.iter().find(|(_, vertical)| *vertical).or(ids.first()).map(|(id, _)| id.to_string())
|
||||
};
|
||||
// The primary value axis is the one on the left (bottom for bar charts); the secondary one
|
||||
// sits on the right or top, or crosses the category axis at its maximum.
|
||||
let secondary_placed = |a: &El| a.child_val("crosses") == Some("max") || matches!(a.child_val("axPos"), Some("r" | "t"));
|
||||
let val_axes_by_id: std::collections::HashMap<&str, &El> = pa.children_named("valAx").filter_map(|a| Some((a.child_val("axId")?, a))).collect();
|
||||
let used: Vec<String> = pa.elements().filter(|g| g.local().ends_with("Chart")).filter_map(group_val_axis).collect();
|
||||
let primary_val: Option<String> =
|
||||
used.iter().find(|id| val_axes_by_id.get(id.as_str()).is_some_and(|a| !secondary_placed(a))).or(used.first()).cloned();
|
||||
for g in pa.elements() {
|
||||
let name = g.local();
|
||||
if !name.ends_with("Chart") {
|
||||
continue;
|
||||
}
|
||||
let Some(kind) = group_chart_type(name, g) else { continue };
|
||||
if name == "stockChart" {
|
||||
c.up_down_bars = g.child("upDownBars").is_some();
|
||||
let grouping = g.child_val("grouping").unwrap_or("clustered");
|
||||
let kind = match name {
|
||||
"barChart" | "bar3DChart" => {
|
||||
let bar = g.child_val("barDir") == Some("bar");
|
||||
match (bar, grouping) {
|
||||
(false, "stacked") => ChartType::StackedColumn,
|
||||
(false, "percentStacked") => ChartType::PercentColumn,
|
||||
(false, _) => ChartType::Column,
|
||||
(true, "stacked") => ChartType::StackedBar,
|
||||
(true, "percentStacked") => ChartType::PercentBar,
|
||||
(true, _) => ChartType::Bar,
|
||||
}
|
||||
}
|
||||
"lineChart" | "line3DChart" => {
|
||||
let markers = g.child_val("marker").is_some_and(|v| v == "1" || v == "true")
|
||||
&& g.children_named("ser").all(|s| s.path(&["marker", "symbol"]).and_then(|m| m.attr("val")) != Some("none"));
|
||||
match grouping {
|
||||
"stacked" | "percentStacked" => ChartType::StackedLine,
|
||||
_ if markers => ChartType::LineMarkers,
|
||||
_ => ChartType::Line,
|
||||
}
|
||||
}
|
||||
"areaChart" | "area3DChart" => {
|
||||
if matches!(grouping, "stacked" | "percentStacked") {
|
||||
ChartType::StackedArea
|
||||
} else {
|
||||
ChartType::Area
|
||||
}
|
||||
}
|
||||
"pieChart" | "pie3DChart" | "ofPieChart" => ChartType::Pie,
|
||||
"doughnutChart" => ChartType::Doughnut,
|
||||
"scatterChart" => ChartType::Scatter,
|
||||
"bubbleChart" => ChartType::Bubble,
|
||||
"radarChart" => {
|
||||
if g.child_val("radarStyle") == Some("filled") {
|
||||
ChartType::FilledRadar
|
||||
} else {
|
||||
ChartType::Radar
|
||||
}
|
||||
}
|
||||
"stockChart" => {
|
||||
c.up_down_bars = g.child("upDownBars").is_some();
|
||||
ChartType::Stock
|
||||
}
|
||||
"surfaceChart" | "surface3DChart" => ChartType::Surface,
|
||||
_ => continue,
|
||||
};
|
||||
// A missing <c:varyColors> means "vary by point" (the schema default is true), as PowerPoint draws it.
|
||||
c.vary_colors = g.child_val("varyColors").is_none_or(|v| v == "1" || v == "true");
|
||||
// A bar group without c:gapWidth / c:overlap takes the schema's defaults (150 %, 0 %),
|
||||
// not the 219 % / -27 % PowerPoint writes into its new charts.
|
||||
let bar_group = g.is("barChart") || g.is("bar3DChart");
|
||||
if let Some(v) = g.child_val("gapWidth").and_then(|v| v.parse::<f64>().ok()) {
|
||||
c.gap_width = (v / 100.0).clamp(0.0, 5.0);
|
||||
} else if bar_group {
|
||||
c.gap_width = 1.5;
|
||||
}
|
||||
if let Some(v) = g.child_val("overlap").and_then(|v| v.parse::<f64>().ok()) {
|
||||
c.overlap = (v / 100.0).clamp(-1.0, 1.0);
|
||||
} else if bar_group {
|
||||
c.overlap = 0.0;
|
||||
}
|
||||
if let Some(v) = g.child_val("holeSize").and_then(|v| v.parse::<f64>().ok()) {
|
||||
c.hole_size = (v / 100.0).clamp(0.0, 0.9);
|
||||
}
|
||||
// `<c:showVal/>` without a val attribute means true, as in the schema's default.
|
||||
// The setting belongs to this plot group: a group that asks for no labels keeps them
|
||||
// off, even when another group of a combo chart asks for them (#868).
|
||||
let group_labels = g.path(&["dLbls", "showVal"]).map(|v| v.bool("val").unwrap_or(true));
|
||||
if group_labels == Some(true) {
|
||||
c.data_labels = true;
|
||||
}
|
||||
let group_labels = read_group_settings(g, &mut c);
|
||||
kinds.push(kind);
|
||||
let val_axis = group_value_axis(&val_map, g);
|
||||
let val_axis = group_val_axis(g);
|
||||
let secondary = val_axis.is_some() && val_axis != primary_val;
|
||||
for s in g.children_named("ser").take(1000) {
|
||||
let ser = read_series(s, g, (kind, secondary, group_labels), &mut c.categories, &mut budget);
|
||||
let mut ser = Series {
|
||||
name: s.child("tx").map(|t| strings(t, &mut budget)).and_then(|v| v.into_iter().next()).unwrap_or_default(),
|
||||
fill: simple_fill(s.child("spPr")),
|
||||
line: simple_line(s.child("spPr")),
|
||||
smooth: s.child("smooth").and_then(|v| v.bool("val")).unwrap_or(false),
|
||||
marker: s.path(&["marker", "symbol"]).and_then(|m| m.attr("val")).filter(|m| *m != "none").map(String::from),
|
||||
kind: Some(kind),
|
||||
secondary_axis: secondary,
|
||||
..Default::default()
|
||||
};
|
||||
// `<c:showVal/>` without a val attribute means true, as in the schema's default.
|
||||
if let Some(d) = s.child("dLbls").and_then(|d| d.child("showVal")).map(|v| v.bool("val").unwrap_or(true)) {
|
||||
ser.data_labels = Some(d);
|
||||
} else if let Some(false) = group_labels {
|
||||
// The group asks for no labels and the series does not say otherwise.
|
||||
ser.data_labels = Some(false);
|
||||
}
|
||||
for t in s.children_named("trendline").take(16) {
|
||||
if let Some(t) = trendline(t) {
|
||||
ser.trendlines.push(t);
|
||||
}
|
||||
}
|
||||
// Data labels' number format: the series' labels', the group's, else the source data's.
|
||||
let label_fmt = |d: Option<&El>| {
|
||||
d.and_then(|d| d.child("numFmt"))
|
||||
.filter(|n| n.attr("sourceLinked") != Some("1"))
|
||||
.and_then(|n| n.attr("formatCode"))
|
||||
.map(str::to_string)
|
||||
};
|
||||
let source =
|
||||
s.child("val").or_else(|| s.child("yVal")).and_then(|v| v.find("numCache")).and_then(|c| c.child("formatCode")).map(|f| f.text());
|
||||
ser.number_format = label_fmt(s.child("dLbls")).or_else(|| label_fmt(g.child("dLbls"))).or(source).filter(|f| !f.is_empty());
|
||||
for dp in s.children_named("dPt") {
|
||||
if let (Some(i), Some(f)) = (dp.child("idx").and_then(|i| i.u32("val")), simple_fill(dp.child("spPr"))) {
|
||||
ser.point_fills.push((i as usize, f));
|
||||
}
|
||||
}
|
||||
if let Some(cat) = s.child("cat").or_else(|| s.child("xVal")) {
|
||||
let cats = strings(cat, &mut budget);
|
||||
if matches!(kind, ChartType::Scatter | ChartType::Bubble) {
|
||||
ser.x_values = cats.iter().map(|v| v.trim().parse::<f64>().ok()).collect();
|
||||
}
|
||||
if c.categories.is_empty() {
|
||||
c.categories = cats;
|
||||
}
|
||||
}
|
||||
if let Some(v) = s.child("val").or_else(|| s.child("yVal")) {
|
||||
ser.values = numbers(v, &mut budget);
|
||||
}
|
||||
// Error bars on the values: a scatter chart's y bars, any other chart's only ones.
|
||||
if let Some(e) = s
|
||||
.children_named("errBars")
|
||||
.find(|e| e.child_val("errDir").is_none_or(|d| d == "y" || !matches!(kind, ChartType::Scatter | ChartType::Bubble)))
|
||||
{
|
||||
let d = ErrorBars::default();
|
||||
ser.error_bars = Some(ErrorBars {
|
||||
bar_type: e.child_val("errBarType").unwrap_or("both").to_string(),
|
||||
val_type: e.child_val("errValType").unwrap_or("fixedVal").to_string(),
|
||||
value: e.child_val("val").and_then(|v| v.parse().ok()).filter(|v: &f64| v.is_finite()).unwrap_or(d.value),
|
||||
plus: e.child("plus").map(|p| numbers(p, &mut budget)).unwrap_or_default(),
|
||||
minus: e.child("minus").map(|p| numbers(p, &mut budget)).unwrap_or_default(),
|
||||
end_cap: e.child("noEndCap").and_then(|v| v.bool("val")) != Some(true),
|
||||
});
|
||||
}
|
||||
if let Some(b) = s.child("bubbleSize") {
|
||||
ser.sizes = numbers(b, &mut budget);
|
||||
}
|
||||
c.series.push(ser);
|
||||
}
|
||||
}
|
||||
@@ -160,189 +305,11 @@ fn chart_within(root: &El, mut budget: usize) -> Option<Chart> {
|
||||
s.secondary_axis = false;
|
||||
}
|
||||
}
|
||||
// Axes.
|
||||
c.lang = root.child_val("lang").map(str::to_string);
|
||||
let (val_ax, cat_ax) = read_axes(&mut c, pa, &primary_val);
|
||||
read_text_styles(&mut c, root, ch, val_ax, cat_ax);
|
||||
Some(c)
|
||||
}
|
||||
|
||||
/// A group's value axis: its vertical one when it has two (scatter), else its one.
|
||||
fn group_value_axis(val_map: &std::collections::HashMap<&str, bool>, g: &El) -> Option<String> {
|
||||
let ids: Vec<(&str, bool)> =
|
||||
g.children_named("axId").filter_map(|a| a.attr("val")).filter_map(|id| val_map.get(id).copied().map(|v| (id, v))).collect();
|
||||
ids.iter().find(|(_, vertical)| *vertical).or(ids.first()).map(|(id, _)| id.to_string())
|
||||
}
|
||||
|
||||
/// The primary value axis is the one on the left (bottom for bar charts); the secondary one
|
||||
/// sits on the right or top, or crosses the category axis at its maximum.
|
||||
fn primary_value_axis(pa: &El, val_map: &std::collections::HashMap<&str, bool>) -> Option<String> {
|
||||
let secondary_placed = |a: &El| a.child_val("crosses") == Some("max") || matches!(a.child_val("axPos"), Some("r" | "t"));
|
||||
let val_axes_by_id: std::collections::HashMap<&str, &El> = pa.children_named("valAx").filter_map(|a| Some((a.child_val("axId")?, a))).collect();
|
||||
let used: Vec<String> = pa.elements().filter(|g| g.local().ends_with("Chart")).filter_map(|g| group_value_axis(val_map, g)).collect();
|
||||
used.iter().find(|id| val_axes_by_id.get(id.as_str()).is_some_and(|a| !secondary_placed(a))).or(used.first()).cloned()
|
||||
}
|
||||
|
||||
/// The chart type of a plot group (`name` is its element name), or `None` for an unknown group.
|
||||
fn group_chart_type(name: &str, g: &El) -> Option<ChartType> {
|
||||
let grouping = g.child_val("grouping").unwrap_or("clustered");
|
||||
Some(match name {
|
||||
"barChart" | "bar3DChart" => {
|
||||
let bar = g.child_val("barDir") == Some("bar");
|
||||
match (bar, grouping) {
|
||||
(false, "stacked") => ChartType::StackedColumn,
|
||||
(false, "percentStacked") => ChartType::PercentColumn,
|
||||
(false, _) => ChartType::Column,
|
||||
(true, "stacked") => ChartType::StackedBar,
|
||||
(true, "percentStacked") => ChartType::PercentBar,
|
||||
(true, _) => ChartType::Bar,
|
||||
}
|
||||
}
|
||||
"lineChart" | "line3DChart" => {
|
||||
let markers = g.child_val("marker").is_some_and(|v| v == "1" || v == "true")
|
||||
&& g.children_named("ser").all(|s| s.path(&["marker", "symbol"]).and_then(|m| m.attr("val")) != Some("none"));
|
||||
match grouping {
|
||||
"stacked" | "percentStacked" => ChartType::StackedLine,
|
||||
_ if markers => ChartType::LineMarkers,
|
||||
_ => ChartType::Line,
|
||||
}
|
||||
}
|
||||
"areaChart" | "area3DChart" => {
|
||||
if matches!(grouping, "stacked" | "percentStacked") {
|
||||
ChartType::StackedArea
|
||||
} else {
|
||||
ChartType::Area
|
||||
}
|
||||
}
|
||||
"pieChart" | "pie3DChart" | "ofPieChart" => ChartType::Pie,
|
||||
"doughnutChart" => ChartType::Doughnut,
|
||||
"scatterChart" => ChartType::Scatter,
|
||||
"bubbleChart" => ChartType::Bubble,
|
||||
"radarChart" => {
|
||||
if g.child_val("radarStyle") == Some("filled") {
|
||||
ChartType::FilledRadar
|
||||
} else {
|
||||
ChartType::Radar
|
||||
}
|
||||
}
|
||||
"stockChart" => ChartType::Stock,
|
||||
"surfaceChart" | "surface3DChart" => ChartType::Surface,
|
||||
_ => return None,
|
||||
})
|
||||
}
|
||||
|
||||
/// Chart-wide settings a plot group carries (vary colours, gap, overlap, hole, data labels).
|
||||
/// Returns the group's own data-label setting.
|
||||
fn read_group_settings(g: &El, c: &mut Chart) -> Option<bool> {
|
||||
// A missing <c:varyColors> means "vary by point" (the schema default is true), as PowerPoint draws it.
|
||||
c.vary_colors = g.child_val("varyColors").is_none_or(|v| v == "1" || v == "true");
|
||||
// A bar group without c:gapWidth / c:overlap takes the schema's defaults (150 %, 0 %),
|
||||
// not the 219 % / -27 % PowerPoint writes into its new charts.
|
||||
let bar_group = g.is("barChart") || g.is("bar3DChart");
|
||||
if let Some(v) = g.child_val("gapWidth").and_then(|v| v.parse::<f64>().ok()) {
|
||||
c.gap_width = (v / 100.0).clamp(0.0, 5.0);
|
||||
} else if bar_group {
|
||||
c.gap_width = 1.5;
|
||||
}
|
||||
if let Some(v) = g.child_val("overlap").and_then(|v| v.parse::<f64>().ok()) {
|
||||
c.overlap = (v / 100.0).clamp(-1.0, 1.0);
|
||||
} else if bar_group {
|
||||
c.overlap = 0.0;
|
||||
}
|
||||
if let Some(v) = g.child_val("holeSize").and_then(|v| v.parse::<f64>().ok()) {
|
||||
c.hole_size = (v / 100.0).clamp(0.0, 0.9);
|
||||
}
|
||||
// `<c:showVal/>` without a val attribute means true, as in the schema's default.
|
||||
// The setting belongs to this plot group: a group that asks for no labels keeps them
|
||||
// off, even when another group of a combo chart asks for them (#868).
|
||||
let group_labels = g.path(&["dLbls", "showVal"]).map(|v| v.bool("val").unwrap_or(true));
|
||||
if group_labels == Some(true) {
|
||||
c.data_labels = true;
|
||||
}
|
||||
group_labels
|
||||
}
|
||||
|
||||
/// Data labels' number format: the series' labels', the group's, else the source data's.
|
||||
fn label_number_format(s: &El, g: &El) -> Option<String> {
|
||||
let label_fmt = |d: Option<&El>| {
|
||||
d.and_then(|d| d.child("numFmt")).filter(|n| n.attr("sourceLinked") != Some("1")).and_then(|n| n.attr("formatCode")).map(str::to_string)
|
||||
};
|
||||
let source = s.child("val").or_else(|| s.child("yVal")).and_then(|v| v.find("numCache")).and_then(|c| c.child("formatCode")).map(|f| f.text());
|
||||
label_fmt(s.child("dLbls")).or_else(|| label_fmt(g.child("dLbls"))).or(source).filter(|f| !f.is_empty())
|
||||
}
|
||||
|
||||
/// Error bars on the values: a scatter chart's y bars, any other chart's only ones.
|
||||
fn read_error_bars(s: &El, kind: ChartType, budget: &mut usize) -> Option<ErrorBars> {
|
||||
let e = s
|
||||
.children_named("errBars")
|
||||
.find(|e| e.child_val("errDir").is_none_or(|d| d == "y" || !matches!(kind, ChartType::Scatter | ChartType::Bubble)))?;
|
||||
let d = ErrorBars::default();
|
||||
Some(ErrorBars {
|
||||
bar_type: e.child_val("errBarType").unwrap_or("both").to_string(),
|
||||
val_type: e.child_val("errValType").unwrap_or("fixedVal").to_string(),
|
||||
value: e.child_val("val").and_then(|v| v.parse().ok()).filter(|v: &f64| v.is_finite()).unwrap_or(d.value),
|
||||
plus: e.child("plus").map(|p| numbers(p, budget)).unwrap_or_default(),
|
||||
minus: e.child("minus").map(|p| numbers(p, budget)).unwrap_or_default(),
|
||||
end_cap: e.child("noEndCap").and_then(|v| v.bool("val")) != Some(true),
|
||||
})
|
||||
}
|
||||
|
||||
/// One `c:ser` of group `g`. `group` is (chart type, on the secondary axis, the group's data-label
|
||||
/// setting); the first series with categories sets `categories`.
|
||||
fn read_series(s: &El, g: &El, group: (ChartType, bool, Option<bool>), categories: &mut Vec<String>, budget: &mut usize) -> Series {
|
||||
let (kind, secondary, group_labels) = group;
|
||||
let mut ser = Series {
|
||||
name: s.child("tx").map(|t| strings(t, budget)).and_then(|v| v.into_iter().next()).unwrap_or_default(),
|
||||
fill: simple_fill(s.child("spPr")),
|
||||
line: simple_line(s.child("spPr")),
|
||||
smooth: s.child("smooth").and_then(|v| v.bool("val")).unwrap_or(false),
|
||||
marker: s.path(&["marker", "symbol"]).and_then(|m| m.attr("val")).filter(|m| *m != "none").map(String::from),
|
||||
kind: Some(kind),
|
||||
secondary_axis: secondary,
|
||||
..Default::default()
|
||||
};
|
||||
// `<c:showVal/>` without a val attribute means true, as in the schema's default.
|
||||
if let Some(d) = s.child("dLbls").and_then(|d| d.child("showVal")).map(|v| v.bool("val").unwrap_or(true)) {
|
||||
ser.data_labels = Some(d);
|
||||
} else if let Some(false) = group_labels {
|
||||
// The group asks for no labels and the series does not say otherwise.
|
||||
ser.data_labels = Some(false);
|
||||
}
|
||||
for t in s.children_named("trendline").take(16) {
|
||||
if let Some(t) = trendline(t) {
|
||||
ser.trendlines.push(t);
|
||||
}
|
||||
}
|
||||
ser.number_format = label_number_format(s, g);
|
||||
for dp in s.children_named("dPt") {
|
||||
if let (Some(i), Some(f)) = (dp.child("idx").and_then(|i| i.u32("val")), simple_fill(dp.child("spPr"))) {
|
||||
ser.point_fills.push((i as usize, f));
|
||||
}
|
||||
}
|
||||
if let Some(cat) = s.child("cat").or_else(|| s.child("xVal")) {
|
||||
let cats = strings(cat, budget);
|
||||
if matches!(kind, ChartType::Scatter | ChartType::Bubble) {
|
||||
ser.x_values = cats.iter().map(|v| v.trim().parse::<f64>().ok()).collect();
|
||||
}
|
||||
if categories.is_empty() {
|
||||
*categories = cats;
|
||||
}
|
||||
}
|
||||
if let Some(v) = s.child("val").or_else(|| s.child("yVal")) {
|
||||
ser.values = numbers(v, budget);
|
||||
}
|
||||
ser.error_bars = read_error_bars(s, kind, budget);
|
||||
if let Some(b) = s.child("bubbleSize") {
|
||||
ser.sizes = numbers(b, budget);
|
||||
}
|
||||
ser
|
||||
}
|
||||
|
||||
/// Axis scaling, titles, number formats and visibility. Returns the primary value axis and the
|
||||
/// category axis.
|
||||
fn read_axes<'a>(c: &mut Chart, pa: &'a El, primary_val: &Option<String>) -> (Option<&'a El>, Option<&'a El>) {
|
||||
let vertical = |a: &&El| matches!(a.child_val("axPos"), Some("l" | "r"));
|
||||
let by_id = |id: &Option<String>| pa.children_named("valAx").find(|a| id.is_some() && a.child_val("axId") == id.as_deref());
|
||||
let val_ax = by_id(primary_val).or_else(|| pa.children_named("valAx").find(vertical)).or_else(|| pa.children_named("valAx").last());
|
||||
let val_ax = by_id(&primary_val).or_else(|| pa.children_named("valAx").find(vertical)).or_else(|| pa.children_named("valAx").last());
|
||||
let num = |a: &El, k: &str| a.child_val(k).and_then(|v| v.parse::<f64>().ok()).filter(|v| v.is_finite());
|
||||
// An axis' number format: its own unless linked to the source (`sourceLinked="1"`, or no
|
||||
// c:numFmt at all), in which case the data's format of the first series plotted on it.
|
||||
@@ -390,11 +357,6 @@ fn read_axes<'a>(c: &mut Chart, pa: &'a El, primary_val: &Option<String>) -> (Op
|
||||
c.cat_reversed = a.child("scaling").and_then(|s| s.child_val("orientation")).is_some_and(|o| o == "maxMin");
|
||||
c.axis_titles.0 = a.child("title").map(|t| rich_text(t).unwrap_or_else(|| "Axis Title".into()));
|
||||
}
|
||||
(val_ax, cat_ax)
|
||||
}
|
||||
|
||||
/// Text sizes and colours as the file sets them.
|
||||
fn read_text_styles(c: &mut Chart, root: &El, ch: &El, val_ax: Option<&El>, cat_ax: Option<&El>) {
|
||||
// Text sizes as the file sets them (PowerPoint draws chart text at these sizes, not scaled to
|
||||
// the chart): the axes' and legend's, else the whole chart's; the title's own.
|
||||
let chart_size = text_size(root.child("txPr"));
|
||||
@@ -402,6 +364,7 @@ fn read_text_styles(c: &mut Chart, root: &El, ch: &El, val_ax: Option<&El>, cat_
|
||||
// Text colour likewise: the axes' and legend's, else the whole chart's.
|
||||
c.text_color = [val_ax, cat_ax, ch.child("legend"), Some(root)].into_iter().flatten().find_map(|e| text_color(e.child("txPr")));
|
||||
c.title_size = ch.child("title").and_then(|t| text_size(t.child("tx").and_then(|x| x.child("rich"))).or_else(|| text_size(t.child("txPr"))));
|
||||
Some(c)
|
||||
}
|
||||
|
||||
fn trend_num(e: &El, k: &str) -> Option<f64> {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
//! Chart parts: a `c:chartSpace` with cached categories and values.
|
||||
//! An unchanged chart also keeps the embedded workbook and the other parts it points at.
|
||||
|
||||
use deckcraft_model::chart::{Chart, ChartType, ErrorBars, Series, Trendline, TrendlineType};
|
||||
use deckcraft_model::chart::{Chart, ChartType, Series, Trendline, TrendlineType};
|
||||
use deckcraft_model::style::Fill;
|
||||
|
||||
use crate::opc::{NS_A, NS_C, NS_R};
|
||||
@@ -204,48 +204,6 @@ fn group_of(k: ChartType) -> Group {
|
||||
}
|
||||
}
|
||||
|
||||
fn error_bars(w: &mut W, e: &ErrorBars, g: Group) {
|
||||
let word = |v: &str, ok: &[&'static str], d: &'static str| ok.iter().copied().find(|o| *o == v).unwrap_or(d);
|
||||
w.open0("c:errBars");
|
||||
if matches!(g, Group::Scatter | Group::Bubble) {
|
||||
w.val("c:errDir", "y");
|
||||
}
|
||||
w.val("c:errBarType", word(&e.bar_type, &["both", "plus", "minus"], "both"));
|
||||
let vt = word(&e.val_type, &["fixedVal", "percentage", "stdDev", "stdErr", "cust"], "fixedVal");
|
||||
w.val("c:errValType", vt);
|
||||
w.val("c:noEndCap", if e.end_cap { 0 } else { 1 });
|
||||
if vt == "cust" {
|
||||
w.open0("c:plus");
|
||||
num_lit(w, &e.plus);
|
||||
w.close("c:plus");
|
||||
w.open0("c:minus");
|
||||
num_lit(w, &e.minus);
|
||||
w.close("c:minus");
|
||||
} else if e.value.is_finite() {
|
||||
w.val("c:val", fmt_num(e.value));
|
||||
}
|
||||
w.close("c:errBars");
|
||||
}
|
||||
|
||||
/// Per-point fills of pie, doughnut and bar series.
|
||||
fn point_fills(w: &mut W, s: &Series, g: Group) {
|
||||
if !matches!(g, Group::Pie | Group::Doughnut | Group::Bar) {
|
||||
return;
|
||||
}
|
||||
for (idx, f) in s.point_fills.iter().take(1000) {
|
||||
w.open0("c:dPt");
|
||||
w.val("c:idx", idx);
|
||||
if g == Group::Bar {
|
||||
w.val("c:invertIfNegative", 0);
|
||||
}
|
||||
if g != Group::Bar {
|
||||
w.val("c:bubble3D", 0);
|
||||
}
|
||||
sp_pr(w, Some(f), None);
|
||||
w.close("c:dPt");
|
||||
}
|
||||
}
|
||||
|
||||
fn series(w: &mut W, c: &Chart, s: &Series, i: usize, kind: ChartType, g: Group) {
|
||||
w.open0("c:ser");
|
||||
w.val("c:idx", i);
|
||||
@@ -295,7 +253,20 @@ fn series(w: &mut W, c: &Chart, s: &Series, i: usize, kind: ChartType, g: Group)
|
||||
w.close("c:marker");
|
||||
}
|
||||
}
|
||||
point_fills(w, s, g);
|
||||
if g == Group::Pie || g == Group::Doughnut || g == Group::Bar {
|
||||
for (idx, f) in s.point_fills.iter().take(1000) {
|
||||
w.open0("c:dPt");
|
||||
w.val("c:idx", idx);
|
||||
if g == Group::Bar {
|
||||
w.val("c:invertIfNegative", 0);
|
||||
}
|
||||
if g != Group::Bar {
|
||||
w.val("c:bubble3D", 0);
|
||||
}
|
||||
sp_pr(w, Some(f), None);
|
||||
w.close("c:dPt");
|
||||
}
|
||||
}
|
||||
if let Some(d) = s.data_labels {
|
||||
data_labels(w, c, d);
|
||||
}
|
||||
@@ -308,7 +279,26 @@ fn series(w: &mut W, c: &Chart, s: &Series, i: usize, kind: ChartType, g: Group)
|
||||
let n = c.categories.len().max(s.values.len());
|
||||
let last = n + 1;
|
||||
if let Some(e) = &s.error_bars {
|
||||
error_bars(w, e, g);
|
||||
let word = |v: &str, ok: &[&'static str], d: &'static str| ok.iter().copied().find(|o| *o == v).unwrap_or(d);
|
||||
w.open0("c:errBars");
|
||||
if matches!(g, Group::Scatter | Group::Bubble) {
|
||||
w.val("c:errDir", "y");
|
||||
}
|
||||
w.val("c:errBarType", word(&e.bar_type, &["both", "plus", "minus"], "both"));
|
||||
let vt = word(&e.val_type, &["fixedVal", "percentage", "stdDev", "stdErr", "cust"], "fixedVal");
|
||||
w.val("c:errValType", vt);
|
||||
w.val("c:noEndCap", if e.end_cap { 0 } else { 1 });
|
||||
if vt == "cust" {
|
||||
w.open0("c:plus");
|
||||
num_lit(w, &e.plus);
|
||||
w.close("c:plus");
|
||||
w.open0("c:minus");
|
||||
num_lit(w, &e.minus);
|
||||
w.close("c:minus");
|
||||
} else if e.value.is_finite() {
|
||||
w.val("c:val", fmt_num(e.value));
|
||||
}
|
||||
w.close("c:errBars");
|
||||
}
|
||||
if matches!(g, Group::Scatter | Group::Bubble) {
|
||||
let xs: Vec<Option<f64>> = if s.x_values.is_empty() { (1..=s.values.len()).map(|v| Some(v as f64)).collect() } else { s.x_values.clone() };
|
||||
|
||||
@@ -159,11 +159,6 @@ fn chart_table_and_text_corpus_round_trips() {
|
||||
assert_eq!(a.data_labels, b.data_labels);
|
||||
}
|
||||
|
||||
merged_table_survives(slide);
|
||||
text_columns_and_warp_survive(slide);
|
||||
}
|
||||
|
||||
fn merged_table_survives(slide: &Slide) {
|
||||
let ShapeKind::Table(t) = &by_name(slide, "Merged table").kind else { panic!("not a table") };
|
||||
let anchor = t.cell(0, 0).expect("anchor");
|
||||
assert_eq!((anchor.grid_span, anchor.row_span), (2, 2));
|
||||
@@ -176,9 +171,7 @@ fn merged_table_survives(slide: &Slide) {
|
||||
assert_eq!(diag.diag_down, Some(Line::solid(ColorRef::rgb(Rgba::rgb(180, 40, 40)), 1.5)));
|
||||
assert_eq!(diag.diag_up, Some(Line::solid(ColorRef::rgb(Rgba::rgb(20, 90, 160)), 2.0)));
|
||||
assert_eq!(plain(&diag.text), "Diag");
|
||||
}
|
||||
|
||||
fn text_columns_and_warp_survive(slide: &Slide) {
|
||||
let cols = by_name(slide, "Columns").text.as_ref().expect("columns");
|
||||
assert_eq!(plain(cols), "Two columns\nof vertical text");
|
||||
assert_eq!(cols.body.columns, Some(3));
|
||||
|
||||
@@ -215,14 +215,7 @@ fn assert_deck(q: &Presentation) {
|
||||
let inherited = resolve::run(&ctx, body, ¶s[1], ¶s[1].runs[0].props);
|
||||
assert_eq!(inherited.size, Some(28.0), "the second paragraph takes the layout list style");
|
||||
assert_eq!(inherited.font.as_deref(), Some("Source Serif 4"));
|
||||
assert_group(q);
|
||||
assert_comments(q);
|
||||
assert_click_links(q);
|
||||
assert_custom_show_links(q);
|
||||
assert_custom_shows(q);
|
||||
}
|
||||
|
||||
fn assert_group(q: &Presentation) {
|
||||
let cluster = shape(q, 0, "Cluster");
|
||||
let g = cluster.xfrm.expect("group box");
|
||||
assert_eq!((g.x, g.y, g.w, g.h, g.rot, g.flip_h), (80.0, 40.0, 220.0, 140.0, 25.0, true));
|
||||
@@ -232,17 +225,13 @@ fn assert_group(q: &Presentation) {
|
||||
assert_eq!(gem.text.as_ref().map(|t| t.text()), Some("Tilted".into()));
|
||||
let gx = gem.xfrm.expect("gem");
|
||||
assert_eq!((gx.x, gx.y, gx.w, gx.h, gx.rot, gx.flip_v), (12.0, 18.0, 80.0, 40.0, 20.0, true));
|
||||
}
|
||||
|
||||
fn assert_comments(q: &Presentation) {
|
||||
let c = &q.slides[0].comments;
|
||||
assert_eq!(c.len(), 1);
|
||||
assert_eq!((c[0].author.as_str(), c[0].initials.as_str(), c[0].text.as_str()), ("Mina", "ML", "Check the quote"));
|
||||
assert_eq!(c[0].date, "2026-03-01T09:30:00.000");
|
||||
assert_eq!((c[0].x, c[0].y), (36.0, 48.0));
|
||||
}
|
||||
|
||||
fn assert_click_links(q: &Presentation) {
|
||||
let click = |name: &str| shape(q, 0, name).click.clone().unwrap_or_else(|| panic!("{name} lost its click"));
|
||||
match click("Site").action {
|
||||
Action::Url { url } => assert_eq!(url, "https://example.com/deck"),
|
||||
@@ -270,10 +259,7 @@ fn assert_click_links(q: &Presentation) {
|
||||
assert!(matches!(hover.action, Action::PreviousSlide));
|
||||
assert!(hover.stop_sound);
|
||||
assert_eq!(hover.tooltip, "Back one");
|
||||
}
|
||||
|
||||
fn assert_custom_show_links(q: &Presentation) {
|
||||
let click = |name: &str| shape(q, 0, name).click.clone().unwrap_or_else(|| panic!("{name} lost its click"));
|
||||
match click("To board").action {
|
||||
Action::CustomShow { name, returns } => {
|
||||
assert_eq!(name, "Board");
|
||||
@@ -290,9 +276,7 @@ fn assert_custom_show_links(q: &Presentation) {
|
||||
}
|
||||
other => panic!("{other:?}"),
|
||||
}
|
||||
}
|
||||
|
||||
fn assert_custom_shows(q: &Presentation) {
|
||||
assert_eq!(q.custom_shows.len(), 2);
|
||||
assert_eq!(q.custom_shows[0].name, "Short");
|
||||
assert_eq!(q.custom_shows[0].slides, vec![q.slides[0].id]);
|
||||
|
||||
@@ -198,92 +198,76 @@ fn object_corpus_round_trips() {
|
||||
let twice = round(&once);
|
||||
|
||||
for q in [&once, &twice] {
|
||||
opaque_parts_survive(q);
|
||||
models_survive(q, &block, &globe, &poster);
|
||||
media_clips_survive(q);
|
||||
fields_and_sections_survive(q);
|
||||
ink_and_timing_survive(q);
|
||||
let diagram = by_name(q, "Diagram");
|
||||
let ShapeKind::Opaque { xml, label, .. } = &diagram.kind else { panic!("{:?}", diagram.kind) };
|
||||
assert_eq!(label, "SmartArt");
|
||||
assert!(xml.contains("Quarter"), "{xml}");
|
||||
assert!(!xml.contains("r:dm"), "diagram parts are not part of this corpus");
|
||||
|
||||
let embedded = by_name(q, "Embedded");
|
||||
let ShapeKind::Opaque { xml, label, .. } = &embedded.kind else { panic!("{:?}", embedded.kind) };
|
||||
assert_eq!(label, "DeckCraft.Note.1");
|
||||
assert!(xml.contains("DeckCraft.Note.1"), "{xml}");
|
||||
assert!(!xml.contains("r:id"), "an embedding part is issue #203");
|
||||
|
||||
let block_shape = by_name(q, "Block");
|
||||
assert!(matches!(block_shape.kind, ShapeKind::Model3d { poster: None, .. }), "{:?}", block_shape.kind);
|
||||
assert_eq!(block_shape.xfrm, Some(Xfrm::new(40.0, 240.0, 120.0, 90.0)));
|
||||
assert_eq!(media_bytes(q, "Block"), block);
|
||||
let ShapeKind::Model3d { media, .. } = &block_shape.kind else { panic!("{:?}", block_shape.kind) };
|
||||
let block_media = q.media(*media).expect("block media");
|
||||
assert_eq!(block_media.content_type, "model/gltf-binary");
|
||||
assert!(block_media.name.to_ascii_lowercase().ends_with(".glb"), "{}", block_media.name);
|
||||
|
||||
let globe_shape = by_name(q, "Globe");
|
||||
let ShapeKind::Model3d { media, poster: Some(poster_media) } = &globe_shape.kind else { panic!("{:?}", globe_shape.kind) };
|
||||
assert_eq!(globe_shape.xfrm, Some(Xfrm::new(180.0, 240.0, 160.0, 120.0)));
|
||||
assert_eq!(q.media(*media).expect("globe").data.as_ref(), globe.as_slice());
|
||||
assert_eq!(q.media(*poster_media).expect("poster").data.as_ref(), poster.as_slice());
|
||||
|
||||
let narration = by_name(q, "Narration");
|
||||
let ShapeKind::Media(m) = &narration.kind else { panic!("{:?}", narration.kind) };
|
||||
assert!(!m.video);
|
||||
assert_eq!(m.trim_start_ms, 400);
|
||||
assert_eq!(m.trim_end_ms, 2600);
|
||||
assert_eq!(m.fade_in_ms, 80);
|
||||
assert_eq!(m.fade_out_ms, 120);
|
||||
assert_eq!(m.bookmarks, vec![("Intro".into(), 400), ("Outro".into(), 2200)]);
|
||||
assert_eq!(q.media(m.media).expect("audio").data.as_ref(), b"audio-bytes-deckcraft".as_slice());
|
||||
|
||||
let film = by_name(q, "Film");
|
||||
let ShapeKind::Media(m) = &film.kind else { panic!("{:?}", film.kind) };
|
||||
assert!(m.video);
|
||||
assert_eq!(m.trim_start_ms, 250);
|
||||
assert_eq!(m.trim_end_ms, 9000);
|
||||
assert_eq!(m.bookmarks, vec![("Mark".into(), 1500)]);
|
||||
assert_eq!(q.media(m.media).expect("video").data.as_ref(), b"video-bytes-deckcraft".as_slice());
|
||||
|
||||
assert_field(q, "Slide number", "slidenum", "4");
|
||||
assert_field(q, "Date", "datetime4", "14 October 2026");
|
||||
assert_eq!(q.first_slide_number, 4);
|
||||
|
||||
assert_eq!(q.sections.iter().map(|s| s.name.as_str()).collect::<Vec<_>>(), ["Open", "Aside"]);
|
||||
let hidden = q.slides.iter().position(|s| s.hidden).expect("a hidden slide");
|
||||
assert_eq!(q.slides.iter().filter(|s| s.hidden).count(), 1);
|
||||
assert!(q.sections[1].slides.contains(&q.slides[hidden].id));
|
||||
assert!(!q.sections[0].slides.contains(&q.slides[hidden].id));
|
||||
|
||||
assert_ink(by_name(q, "Sketch"));
|
||||
|
||||
let timing = q.slides.iter().find(|s| s.animations.iter().any(|a| a.effect == "fade")).expect("timing slide");
|
||||
let fade = timing.animations.iter().find(|a| a.effect == "fade").expect("fade");
|
||||
let pulse = timing.animations.iter().find(|a| a.effect == "pulse").expect("pulse");
|
||||
assert_eq!(fade.repeat, 4);
|
||||
assert!((fade.bounce_end - 0.25).abs() < 1e-9, "{}", fade.bounce_end);
|
||||
assert_eq!(fade.trigger, Some(by_name(q, "Trigger").id));
|
||||
assert_eq!(fade.shape, by_name(q, "Fader").id);
|
||||
assert_eq!(pulse.repeat, u32::MAX);
|
||||
assert_eq!(pulse.class, AnimClass::Emphasis);
|
||||
assert!(pulse.trigger.is_none());
|
||||
}
|
||||
}
|
||||
|
||||
fn opaque_parts_survive(q: &Presentation) {
|
||||
let diagram = by_name(q, "Diagram");
|
||||
let ShapeKind::Opaque { xml, label, .. } = &diagram.kind else { panic!("{:?}", diagram.kind) };
|
||||
assert_eq!(label, "SmartArt");
|
||||
assert!(xml.contains("Quarter"), "{xml}");
|
||||
assert!(!xml.contains("r:dm"), "diagram parts are not part of this corpus");
|
||||
|
||||
let embedded = by_name(q, "Embedded");
|
||||
let ShapeKind::Opaque { xml, label, .. } = &embedded.kind else { panic!("{:?}", embedded.kind) };
|
||||
assert_eq!(label, "DeckCraft.Note.1");
|
||||
assert!(xml.contains("DeckCraft.Note.1"), "{xml}");
|
||||
assert!(!xml.contains("r:id"), "an embedding part is issue #203");
|
||||
}
|
||||
|
||||
fn models_survive(q: &Presentation, block: &[u8], globe: &[u8], poster: &[u8]) {
|
||||
let block_shape = by_name(q, "Block");
|
||||
assert!(matches!(block_shape.kind, ShapeKind::Model3d { poster: None, .. }), "{:?}", block_shape.kind);
|
||||
assert_eq!(block_shape.xfrm, Some(Xfrm::new(40.0, 240.0, 120.0, 90.0)));
|
||||
assert_eq!(media_bytes(q, "Block"), block);
|
||||
let ShapeKind::Model3d { media, .. } = &block_shape.kind else { panic!("{:?}", block_shape.kind) };
|
||||
let block_media = q.media(*media).expect("block media");
|
||||
assert_eq!(block_media.content_type, "model/gltf-binary");
|
||||
assert!(block_media.name.to_ascii_lowercase().ends_with(".glb"), "{}", block_media.name);
|
||||
|
||||
let globe_shape = by_name(q, "Globe");
|
||||
let ShapeKind::Model3d { media, poster: Some(poster_media) } = &globe_shape.kind else { panic!("{:?}", globe_shape.kind) };
|
||||
assert_eq!(globe_shape.xfrm, Some(Xfrm::new(180.0, 240.0, 160.0, 120.0)));
|
||||
assert_eq!(q.media(*media).expect("globe").data.as_ref(), globe);
|
||||
assert_eq!(q.media(*poster_media).expect("poster").data.as_ref(), poster);
|
||||
}
|
||||
|
||||
fn media_clips_survive(q: &Presentation) {
|
||||
let narration = by_name(q, "Narration");
|
||||
let ShapeKind::Media(m) = &narration.kind else { panic!("{:?}", narration.kind) };
|
||||
assert!(!m.video);
|
||||
assert_eq!(m.trim_start_ms, 400);
|
||||
assert_eq!(m.trim_end_ms, 2600);
|
||||
assert_eq!(m.fade_in_ms, 80);
|
||||
assert_eq!(m.fade_out_ms, 120);
|
||||
assert_eq!(m.bookmarks, vec![("Intro".into(), 400), ("Outro".into(), 2200)]);
|
||||
assert_eq!(q.media(m.media).expect("audio").data.as_ref(), b"audio-bytes-deckcraft".as_slice());
|
||||
|
||||
let film = by_name(q, "Film");
|
||||
let ShapeKind::Media(m) = &film.kind else { panic!("{:?}", film.kind) };
|
||||
assert!(m.video);
|
||||
assert_eq!(m.trim_start_ms, 250);
|
||||
assert_eq!(m.trim_end_ms, 9000);
|
||||
assert_eq!(m.bookmarks, vec![("Mark".into(), 1500)]);
|
||||
assert_eq!(q.media(m.media).expect("video").data.as_ref(), b"video-bytes-deckcraft".as_slice());
|
||||
}
|
||||
|
||||
fn fields_and_sections_survive(q: &Presentation) {
|
||||
assert_field(q, "Slide number", "slidenum", "4");
|
||||
assert_field(q, "Date", "datetime4", "14 October 2026");
|
||||
assert_eq!(q.first_slide_number, 4);
|
||||
|
||||
assert_eq!(q.sections.iter().map(|s| s.name.as_str()).collect::<Vec<_>>(), ["Open", "Aside"]);
|
||||
let hidden = q.slides.iter().position(|s| s.hidden).expect("a hidden slide");
|
||||
assert_eq!(q.slides.iter().filter(|s| s.hidden).count(), 1);
|
||||
assert!(q.sections[1].slides.contains(&q.slides[hidden].id));
|
||||
assert!(!q.sections[0].slides.contains(&q.slides[hidden].id));
|
||||
}
|
||||
|
||||
fn ink_and_timing_survive(q: &Presentation) {
|
||||
assert_ink(by_name(q, "Sketch"));
|
||||
|
||||
let timing = q.slides.iter().find(|s| s.animations.iter().any(|a| a.effect == "fade")).expect("timing slide");
|
||||
let fade = timing.animations.iter().find(|a| a.effect == "fade").expect("fade");
|
||||
let pulse = timing.animations.iter().find(|a| a.effect == "pulse").expect("pulse");
|
||||
assert_eq!(fade.repeat, 4);
|
||||
assert!((fade.bounce_end - 0.25).abs() < 1e-9, "{}", fade.bounce_end);
|
||||
assert_eq!(fade.trigger, Some(by_name(q, "Trigger").id));
|
||||
assert_eq!(fade.shape, by_name(q, "Fader").id);
|
||||
assert_eq!(pulse.repeat, u32::MAX);
|
||||
assert_eq!(pulse.class, AnimClass::Emphasis);
|
||||
assert!(pulse.trigger.is_none());
|
||||
}
|
||||
|
||||
/// An exported deck with `parts` added (or replaced) and `edit` applied to every XML part.
|
||||
fn with_parts(parts: &[(&str, Vec<u8>)], edit: impl Fn(&str, String) -> String) -> Vec<u8> {
|
||||
let base = deckcraft_pptx::export(&defaults::new_presentation(None)).expect("export");
|
||||
|
||||
@@ -189,14 +189,9 @@ fn style_kinds_round_trip() {
|
||||
.as_slice()
|
||||
)
|
||||
);
|
||||
text_run_effects_survive(&p, &once, &twice);
|
||||
theme_and_bevel_survive(&once, &twice);
|
||||
connector_glue_survives(&p, &once, &twice);
|
||||
}
|
||||
|
||||
fn text_run_effects_survive(p: &Presentation, once: &Presentation, twice: &Presentation) {
|
||||
let got = run_of(once);
|
||||
let expect = run_of(p);
|
||||
let got = run_of(&once);
|
||||
let expect = run_of(&p);
|
||||
assert_eq!(got.fill, expect.fill, "text gradient");
|
||||
assert_eq!(got.outline, expect.outline, "text outline");
|
||||
assert_eq!(got.shadow, expect.shadow, "text shadow");
|
||||
@@ -204,19 +199,17 @@ fn text_run_effects_survive(p: &Presentation, once: &Presentation, twice: &Prese
|
||||
assert_eq!(got.inner_shadow, expect.inner_shadow, "text inner shadow");
|
||||
assert_eq!(got.reflection, expect.reflection, "text reflection");
|
||||
assert_eq!(got.soft_edge, expect.soft_edge, "text soft edge");
|
||||
assert_eq!(run_of(twice), run_of(once));
|
||||
assert_eq!(run_of(&twice), run_of(&once));
|
||||
assert_eq!(
|
||||
by_name(twice, "Styled text").text.as_ref().map(|t| (&t.body, &t.paragraphs[0].props)),
|
||||
by_name(once, "Styled text").text.as_ref().map(|t| (&t.body, &t.paragraphs[0].props))
|
||||
by_name(&twice, "Styled text").text.as_ref().map(|t| (&t.body, &t.paragraphs[0].props)),
|
||||
by_name(&once, "Styled text").text.as_ref().map(|t| (&t.body, &t.paragraphs[0].props))
|
||||
);
|
||||
|
||||
assert_eq!(by_name(once, "Path gradient").fill, by_name(p, "Path gradient").fill);
|
||||
assert_eq!(by_name(once, "Pattern").fill, by_name(p, "Pattern").fill);
|
||||
assert_eq!(by_name(twice, "Path gradient").fill, by_name(once, "Path gradient").fill);
|
||||
assert_eq!(by_name(twice, "Pattern").fill, by_name(once, "Pattern").fill);
|
||||
}
|
||||
assert_eq!(by_name(&once, "Path gradient").fill, by_name(&p, "Path gradient").fill);
|
||||
assert_eq!(by_name(&once, "Pattern").fill, by_name(&p, "Pattern").fill);
|
||||
assert_eq!(by_name(&twice, "Path gradient").fill, by_name(&once, "Path gradient").fill);
|
||||
assert_eq!(by_name(&twice, "Pattern").fill, by_name(&once, "Pattern").fill);
|
||||
|
||||
fn theme_and_bevel_survive(once: &Presentation, twice: &Presentation) {
|
||||
let theme = &once.masters[0].theme.format.effects[0];
|
||||
assert_eq!(theme.glow, Some(glow()));
|
||||
assert_eq!(theme.reflection, Some(reflection()));
|
||||
@@ -227,24 +220,26 @@ fn theme_and_bevel_survive(once: &Presentation, twice: &Presentation) {
|
||||
assert_eq!(theme2.bevel, theme.bevel);
|
||||
assert_eq!(theme2.raw3d, theme.raw3d);
|
||||
|
||||
let named = by_name(once, "Bevel preset").effects.as_ref().expect("effects");
|
||||
let named = by_name(&once, "Bevel preset").effects.as_ref().expect("effects");
|
||||
assert_eq!(named.bevel.as_deref(), Some("relaxedInset"));
|
||||
assert!(named.raw3d.as_deref().unwrap_or("").contains("relaxedInset"));
|
||||
assert_eq!(by_name(twice, "Bevel preset").effects.as_ref().and_then(|e| e.bevel.as_deref()), Some("relaxedInset"));
|
||||
assert_eq!(by_name(&twice, "Bevel preset").effects.as_ref().and_then(|e| e.bevel.as_deref()), Some("relaxedInset"));
|
||||
|
||||
let bevel = by_name(once, "Bevel").effects.as_ref().expect("effects");
|
||||
let bevel = by_name(&once, "Bevel").effects.as_ref().expect("effects");
|
||||
assert_eq!(bevel.bevel.as_deref(), Some("angle"));
|
||||
let raw = bevel.raw3d.as_deref().unwrap_or("");
|
||||
assert!(raw.contains("bevelT") && raw.contains("angle") && raw.contains("scene3d"), "{raw}");
|
||||
assert_eq!(by_name(twice, "Bevel").effects.as_ref().and_then(|e| e.raw3d.as_deref()), Some(raw));
|
||||
}
|
||||
assert_eq!(by_name(&twice, "Bevel").effects.as_ref().and_then(|e| e.raw3d.as_deref()), Some(raw));
|
||||
|
||||
fn connector_glue_survives(p: &Presentation, once: &Presentation, twice: &Presentation) {
|
||||
let c = by_name(once, "Connector");
|
||||
assert!(matches!(c.kind, ShapeKind::Connector { start: Some((s, 3)), end: Some((e, 1)) } if s == left_of(p) && e == right_of(p)), "{:?}", c.kind);
|
||||
assert_eq!(c.line, by_name(p, "Connector").line);
|
||||
assert_eq!(c.geom, by_name(p, "Connector").geom);
|
||||
assert_eq!(by_name(twice, "Connector").kind, c.kind);
|
||||
let c = by_name(&once, "Connector");
|
||||
assert!(
|
||||
matches!(c.kind, ShapeKind::Connector { start: Some((s, 3)), end: Some((e, 1)) } if s == left_of(&p) && e == right_of(&p)),
|
||||
"{:?}",
|
||||
c.kind
|
||||
);
|
||||
assert_eq!(c.line, by_name(&p, "Connector").line);
|
||||
assert_eq!(c.geom, by_name(&p, "Connector").geom);
|
||||
assert_eq!(by_name(&twice, "Connector").kind, c.kind);
|
||||
}
|
||||
|
||||
fn left_of(p: &Presentation) -> ShapeId {
|
||||
|
||||
@@ -148,13 +148,10 @@ fn effects_and_links_survive_a_round_trip() {
|
||||
assert!(matches!(&links[1].1, Some(Hyperlink { action: Action::Url { url }, .. }) if url.starts_with("mailto:")));
|
||||
let hover = get("Reflected").text.as_ref().and_then(|t| t.paragraphs[0].runs[0].props.hover.clone());
|
||||
assert!(matches!(hover, Some(Hyperlink { action: Action::Url { .. }, ref tooltip, .. }) if tooltip == "Preview"), "{hover:?}");
|
||||
effects_and_links_survive_resave(&a);
|
||||
}
|
||||
|
||||
fn effects_and_links_survive_resave(a: &Presentation) {
|
||||
// Saved and reopened: the same effects and links.
|
||||
let b = round_trip(a);
|
||||
let (ga, gb) = (shapes_named(a, 0), shapes_named(&b, 0));
|
||||
let b = round_trip(&a);
|
||||
let (ga, gb) = (shapes_named(&a, 0), shapes_named(&b, 0));
|
||||
for n in ["Shadowed", "Glowing", "Soft edged", "Reflected"] {
|
||||
assert_eq!(fx(ga(n)), fx(gb(n)), "effects of {n}");
|
||||
assert_eq!(ga(n).click, gb(n).click, "click of {n}");
|
||||
@@ -206,28 +203,24 @@ fn two_masters() -> Presentation {
|
||||
p
|
||||
}
|
||||
|
||||
fn same_master(ma: &deckcraft_model::Master, mb: &deckcraft_model::Master) {
|
||||
assert_eq!(ma.name, mb.name);
|
||||
assert_eq!(ma.theme.name, mb.theme.name);
|
||||
// The writer adds PowerPoint's empty objectDefaults/extraClrSchemeLst, kept as raw XML.
|
||||
let bare = |t: &deckcraft_model::theme::Theme| deckcraft_model::theme::Theme { raw_extra: None, ..t.clone() };
|
||||
assert_eq!(bare(&ma.theme), bare(&mb.theme), "whole theme of {}", ma.name);
|
||||
assert_eq!(ma.theme.colors, mb.theme.colors, "colour scheme of {}", ma.name);
|
||||
assert_eq!(ma.theme.fonts.name, mb.theme.fonts.name);
|
||||
assert_eq!(ma.theme.fonts.major.latin, mb.theme.fonts.major.latin);
|
||||
assert_eq!(ma.theme.fonts.minor.latin, mb.theme.fonts.minor.latin);
|
||||
assert_eq!(ma.background, mb.background, "background of {}", ma.name);
|
||||
let names = |m: &deckcraft_model::Master| m.layouts.iter().map(|l| l.name.clone()).collect::<Vec<_>>();
|
||||
assert_eq!(names(ma), names(mb));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn themes_masters_and_layouts_survive_a_round_trip() {
|
||||
let a = two_masters();
|
||||
let b = round_trip(&a);
|
||||
assert_eq!(b.masters.len(), 2);
|
||||
for (ma, mb) in a.masters.iter().zip(&b.masters) {
|
||||
same_master(ma, mb);
|
||||
assert_eq!(ma.name, mb.name);
|
||||
assert_eq!(ma.theme.name, mb.theme.name);
|
||||
// The writer adds PowerPoint's empty objectDefaults/extraClrSchemeLst, kept as raw XML.
|
||||
let bare = |t: &deckcraft_model::theme::Theme| deckcraft_model::theme::Theme { raw_extra: None, ..t.clone() };
|
||||
assert_eq!(bare(&ma.theme), bare(&mb.theme), "whole theme of {}", ma.name);
|
||||
assert_eq!(ma.theme.colors, mb.theme.colors, "colour scheme of {}", ma.name);
|
||||
assert_eq!(ma.theme.fonts.name, mb.theme.fonts.name);
|
||||
assert_eq!(ma.theme.fonts.major.latin, mb.theme.fonts.major.latin);
|
||||
assert_eq!(ma.theme.fonts.minor.latin, mb.theme.fonts.minor.latin);
|
||||
assert_eq!(ma.background, mb.background, "background of {}", ma.name);
|
||||
let names = |m: &deckcraft_model::Master| m.layouts.iter().map(|l| l.name.clone()).collect::<Vec<_>>();
|
||||
assert_eq!(names(ma), names(mb));
|
||||
}
|
||||
// Each slide stays on its master's layout.
|
||||
let layout_name =
|
||||
|
||||
@@ -10,30 +10,6 @@ fn sample() -> Presentation {
|
||||
s.doc().expect("doc").doc.as_ref().clone()
|
||||
}
|
||||
|
||||
fn same_shapes(a: &deckcraft_model::Slide, b: &deckcraft_model::Slide) {
|
||||
assert_eq!(a.shapes.len(), b.shapes.len(), "shape count on slide '{}'", a.title());
|
||||
for (sa, sb) in a.shapes.iter().zip(b.shapes.iter()) {
|
||||
assert_eq!(sa.kind_name(), sb.kind_name(), "{}", sa.name);
|
||||
assert_eq!(sa.text.as_ref().map(|t| t.text()), sb.text.as_ref().map(|t| t.text()), "{}", sa.name);
|
||||
if let (Some(x), Some(y)) = (sa.xfrm, sb.xfrm) {
|
||||
assert!((x.x - y.x).abs() < 0.01 && (x.w - y.w).abs() < 0.01, "{}: {x:?} vs {y:?}", sa.name);
|
||||
}
|
||||
if let (ShapeKind::Table(ta), ShapeKind::Table(tb)) = (&sa.kind, &sb.kind) {
|
||||
assert_eq!(ta.style, tb.style);
|
||||
assert_eq!(ta.rows.len(), tb.rows.len());
|
||||
}
|
||||
if let (ShapeKind::Chart(ca), ShapeKind::Chart(cb)) = (&sa.kind, &sb.kind) {
|
||||
assert_eq!(ca.kind, cb.kind);
|
||||
assert_eq!(ca.categories, cb.categories);
|
||||
assert_eq!(ca.series.len(), cb.series.len());
|
||||
for (x, y) in ca.series.iter().zip(cb.series.iter()) {
|
||||
assert_eq!(x.name, y.name);
|
||||
assert_eq!(x.values, y.values);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn sample_deck_round_trips() {
|
||||
let p = sample();
|
||||
@@ -49,7 +25,27 @@ fn sample_deck_round_trips() {
|
||||
for (a, b) in p.slides.iter().zip(q.slides.iter()) {
|
||||
assert_eq!(a.title(), b.title());
|
||||
assert_eq!(a.notes_text(), b.notes_text());
|
||||
same_shapes(a, b);
|
||||
assert_eq!(a.shapes.len(), b.shapes.len(), "shape count on slide '{}'", a.title());
|
||||
for (sa, sb) in a.shapes.iter().zip(b.shapes.iter()) {
|
||||
assert_eq!(sa.kind_name(), sb.kind_name(), "{}", sa.name);
|
||||
assert_eq!(sa.text.as_ref().map(|t| t.text()), sb.text.as_ref().map(|t| t.text()), "{}", sa.name);
|
||||
if let (Some(x), Some(y)) = (sa.xfrm, sb.xfrm) {
|
||||
assert!((x.x - y.x).abs() < 0.01 && (x.w - y.w).abs() < 0.01, "{}: {x:?} vs {y:?}", sa.name);
|
||||
}
|
||||
if let (ShapeKind::Table(ta), ShapeKind::Table(tb)) = (&sa.kind, &sb.kind) {
|
||||
assert_eq!(ta.style, tb.style);
|
||||
assert_eq!(ta.rows.len(), tb.rows.len());
|
||||
}
|
||||
if let (ShapeKind::Chart(ca), ShapeKind::Chart(cb)) = (&sa.kind, &sb.kind) {
|
||||
assert_eq!(ca.kind, cb.kind);
|
||||
assert_eq!(ca.categories, cb.categories);
|
||||
assert_eq!(ca.series.len(), cb.series.len());
|
||||
for (x, y) in ca.series.iter().zip(cb.series.iter()) {
|
||||
assert_eq!(x.name, y.name);
|
||||
assert_eq!(x.values, y.values);
|
||||
}
|
||||
}
|
||||
}
|
||||
assert_eq!(a.transition.as_ref().map(|t| (&t.kind, &t.option)), b.transition.as_ref().map(|t| (&t.kind, &t.option)));
|
||||
assert_eq!(a.animations.len(), b.animations.len(), "animations on '{}'", a.title());
|
||||
for (x, y) in a.animations.iter().zip(b.animations.iter()) {
|
||||
|
||||
+345
-595
File diff suppressed because it is too large
Load Diff
@@ -62,23 +62,9 @@ fn funnel(
|
||||
ctx.set_paint(color(series_color(rctx, c, 0), 1.0));
|
||||
ctx.fill_rect(&r);
|
||||
if let Some(name) = c.categories.get(i) {
|
||||
label(
|
||||
ctx,
|
||||
rctx,
|
||||
m,
|
||||
name,
|
||||
Rect::new(area.x0, y, plot.x0 - base * 0.4, y + band),
|
||||
crate::chart::LabelStyle { size: base, align: Align::Right, anchor: Anchor::Middle, col: text_col, bold: false },
|
||||
);
|
||||
label(ctx, rctx, m, name, Rect::new(area.x0, y, plot.x0 - base * 0.4, y + band), base, Align::Right, Anchor::Middle, text_col, false);
|
||||
}
|
||||
label(
|
||||
ctx,
|
||||
rctx,
|
||||
m,
|
||||
&value_label(*v),
|
||||
Rect::new(cx - 60.0, y, cx + 60.0, y + band),
|
||||
crate::chart::LabelStyle { size: base, align: Align::Center, anchor: Anchor::Middle, col: Rgba::WHITE, bold: false },
|
||||
);
|
||||
label(ctx, rctx, m, &value_label(*v), Rect::new(cx - 60.0, y, cx + 60.0, y + band), base, Align::Center, Anchor::Middle, Rgba::WHITE, false);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -132,20 +118,7 @@ fn region_tiles(ctx: &mut RenderContext, rctx: &Ctx, c: &Chart, m: Affine, area:
|
||||
}
|
||||
let inner = t.inset(-base * 0.25);
|
||||
if inner.width() > base && inner.height() > base * 0.8 {
|
||||
label(
|
||||
ctx,
|
||||
rctx,
|
||||
m,
|
||||
&text,
|
||||
inner,
|
||||
crate::chart::LabelStyle {
|
||||
size: base * 0.9,
|
||||
align: Align::Center,
|
||||
anchor: Anchor::Middle,
|
||||
col: if dark { Rgba::WHITE } else { text_col },
|
||||
bold: false,
|
||||
},
|
||||
);
|
||||
label(ctx, rctx, m, &text, inner, base * 0.9, Align::Center, Anchor::Middle, if dark { Rgba::WHITE } else { text_col }, false);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -163,23 +136,9 @@ fn region_tiles(ctx: &mut RenderContext, rctx: &Ctx, c: &Chart, m: Affine, area:
|
||||
ctx.fill_rect(&Rect::new(x0, bar.y0, x1.min(bar.x1), bar.y1));
|
||||
}
|
||||
let ly = Rect::new(bar.x0 - base * 8.0, bar.y0 - base * 0.2, bar.x0 - base * 0.4, bar.y1 + base * 0.2);
|
||||
label(
|
||||
ctx,
|
||||
rctx,
|
||||
m,
|
||||
&c.data_label(s, min),
|
||||
ly,
|
||||
crate::chart::LabelStyle { size: base, align: Align::Right, anchor: Anchor::Middle, col: text_col, bold: false },
|
||||
);
|
||||
label(ctx, rctx, m, &c.data_label(s, min), ly, base, Align::Right, Anchor::Middle, text_col, false);
|
||||
let ry = Rect::new(bar.x1 + base * 0.4, bar.y0 - base * 0.2, bar.x1 + base * 8.0, bar.y1 + base * 0.2);
|
||||
label(
|
||||
ctx,
|
||||
rctx,
|
||||
m,
|
||||
&c.data_label(s, max),
|
||||
ry,
|
||||
crate::chart::LabelStyle { size: base, align: Align::Left, anchor: Anchor::Middle, col: text_col, bold: false },
|
||||
);
|
||||
label(ctx, rctx, m, &c.data_label(s, max), ry, base, Align::Left, Anchor::Middle, text_col, false);
|
||||
}
|
||||
let note = Rect::new(area.x0, area.y1 - note_h, area.x1, area.y1);
|
||||
label(
|
||||
@@ -188,13 +147,11 @@ fn region_tiles(ctx: &mut RenderContext, rctx: &Ctx, c: &Chart, m: Affine, area:
|
||||
m,
|
||||
"Map preview unavailable: regions shown as tiles",
|
||||
note,
|
||||
crate::chart::LabelStyle {
|
||||
size: base * 0.85,
|
||||
align: Align::Center,
|
||||
anchor: Anchor::Middle,
|
||||
col: mix(text_col, Rgba::WHITE, 0.4),
|
||||
bold: false,
|
||||
},
|
||||
base * 0.85,
|
||||
Align::Center,
|
||||
Anchor::Middle,
|
||||
mix(text_col, Rgba::WHITE, 0.4),
|
||||
false,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -291,14 +248,7 @@ fn treemap(
|
||||
}
|
||||
let inner = t.inset(-base * 0.3);
|
||||
if inner.width() > base && inner.height() > base {
|
||||
label(
|
||||
ctx,
|
||||
rctx,
|
||||
m,
|
||||
&text,
|
||||
inner,
|
||||
crate::chart::LabelStyle { size: base, align: Align::Left, anchor: Anchor::Top, col: Rgba::WHITE, bold: false },
|
||||
);
|
||||
label(ctx, rctx, m, &text, inner, base, Align::Left, Anchor::Top, Rgba::WHITE, false);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -339,7 +289,11 @@ fn sunburst(ctx: &mut RenderContext, rctx: &Ctx, c: &Chart, m: Affine, area: Rec
|
||||
m,
|
||||
name,
|
||||
Rect::new(lp.x - 50.0, lp.y - base, lp.x + 50.0, lp.y + base),
|
||||
crate::chart::LabelStyle { size: base, align: Align::Center, anchor: Anchor::Middle, col: Rgba::WHITE, bold: false },
|
||||
base,
|
||||
Align::Center,
|
||||
Anchor::Middle,
|
||||
Rgba::WHITE,
|
||||
false,
|
||||
);
|
||||
}
|
||||
a0 = a1;
|
||||
@@ -390,14 +344,7 @@ fn tree_treemap(ctx: &mut RenderContext, rctx: &Ctx, c: &Chart, m: Affine, area:
|
||||
}
|
||||
let inner = t.inset(-base * 0.3);
|
||||
if inner.width() > base && inner.height() > base {
|
||||
label(
|
||||
ctx,
|
||||
rctx,
|
||||
m,
|
||||
&text,
|
||||
inner,
|
||||
crate::chart::LabelStyle { size: base * 0.9, align: Align::Left, anchor: Anchor::Top, col: Rgba::WHITE, bold: false },
|
||||
);
|
||||
label(ctx, rctx, m, &text, inner, base * 0.9, Align::Left, Anchor::Top, Rgba::WHITE, false);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
@@ -408,14 +355,7 @@ fn tree_treemap(ctx: &mut RenderContext, rctx: &Ctx, c: &Chart, m: Affine, area:
|
||||
ctx.fill_rect(&t);
|
||||
if t.width() > base * 2.0 && banner > base * 0.8 {
|
||||
let head = Rect::new(t.x0 + base * 0.3, t.y0, t.x1 - base * 0.3, t.y0 + banner);
|
||||
label(
|
||||
ctx,
|
||||
rctx,
|
||||
m,
|
||||
&n.name,
|
||||
head,
|
||||
crate::chart::LabelStyle { size: base, align: Align::Left, anchor: Anchor::Middle, col: Rgba::WHITE, bold: true },
|
||||
);
|
||||
label(ctx, rctx, m, &n.name, head, base, Align::Left, Anchor::Middle, Rgba::WHITE, true);
|
||||
}
|
||||
let inner = Rect::new(t.x0 + 1.0, t.y0 + banner, t.x1 - 1.0, t.y1 - 1.0);
|
||||
let k = lighten(fill, 0.12 * (depth + 1) as f64);
|
||||
@@ -497,7 +437,11 @@ fn tree_sunburst(ctx: &mut RenderContext, rctx: &Ctx, c: &Chart, m: Affine, area
|
||||
m,
|
||||
&n.name,
|
||||
Rect::new(lp.x - half, lp.y - base, lp.x + half, lp.y + base),
|
||||
crate::chart::LabelStyle { size: base * 0.9, align: Align::Center, anchor: Anchor::Middle, col: Rgba::WHITE, bold: false },
|
||||
base * 0.9,
|
||||
Align::Center,
|
||||
Anchor::Middle,
|
||||
Rgba::WHITE,
|
||||
false,
|
||||
);
|
||||
}
|
||||
if !n.children.is_empty() {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
use super::{DateTime, Fields, Rgba, ShapeId, Vec2, datetime};
|
||||
use super::*;
|
||||
|
||||
/// Animation state of one shape during a slide show or preview.
|
||||
#[derive(Clone, Debug)]
|
||||
|
||||
+11
-10
@@ -67,14 +67,7 @@ pub fn draw(ctx: &mut RenderContext, rctx: &Ctx, c: &Chart, m: Affine, area: Rec
|
||||
Align::Right => q.x - w + base * 0.3,
|
||||
_ => q.x - w / 2.0,
|
||||
};
|
||||
label(
|
||||
ctx,
|
||||
rctx,
|
||||
m,
|
||||
name,
|
||||
Rect::new(x0, q.y - base * 0.7, x0 + w, q.y + base * 0.7),
|
||||
crate::chart::LabelStyle { size: base, align, anchor: Anchor::Middle, col: text_col, bold: false },
|
||||
);
|
||||
label(ctx, rctx, m, name, Rect::new(x0, q.y - base * 0.7, x0 + w, q.y + base * 0.7), base, align, Anchor::Middle, text_col, false);
|
||||
}
|
||||
}
|
||||
// Value labels up the first spoke.
|
||||
@@ -90,7 +83,11 @@ pub fn draw(ctx: &mut RenderContext, rctx: &Ctx, c: &Chart, m: Affine, area: Rec
|
||||
m,
|
||||
&t,
|
||||
Rect::new(q.x - w - base * 0.3, q.y - base * 0.6, q.x - base * 0.3, q.y + base * 0.6),
|
||||
crate::chart::LabelStyle { size: base * 0.9, align: Align::Right, anchor: Anchor::Middle, col: text_col, bold: false },
|
||||
base * 0.9,
|
||||
Align::Right,
|
||||
Anchor::Middle,
|
||||
text_col,
|
||||
false,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -133,7 +130,11 @@ pub fn draw(ctx: &mut RenderContext, rctx: &Ctx, c: &Chart, m: Affine, area: Rec
|
||||
m,
|
||||
&t,
|
||||
Rect::new(q.x - 40.0, q.y - base * 1.4, q.x + 40.0, q.y - base * 0.2),
|
||||
crate::chart::LabelStyle { size: base * 0.9, align: Align::Center, anchor: Anchor::Bottom, col: text_col, bold: false },
|
||||
base * 0.9,
|
||||
Align::Center,
|
||||
Anchor::Bottom,
|
||||
text_col,
|
||||
false,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
+76
-242
@@ -60,50 +60,6 @@ pub(crate) struct Frame<'a> {
|
||||
pub(crate) fields: SlideFields,
|
||||
}
|
||||
|
||||
/// A shape being drawn: its resolved geometry, fill, line and effects, in shape space `m` at size
|
||||
/// `w` x `h`, shared by the stages of `Renderer::geometry_shape`.
|
||||
struct ShapeDraw<'a, 'ctx> {
|
||||
f: &'a Frame<'a>,
|
||||
rctx: &'a Ctx<'ctx>,
|
||||
s: &'a Shape,
|
||||
st: &'a ShapeState,
|
||||
m: Affine,
|
||||
w: f64,
|
||||
h: f64,
|
||||
depth: usize,
|
||||
geo: Geometry,
|
||||
fades: Vec<deckcraft_geom::morph::Fade>,
|
||||
fill: Option<Fill>,
|
||||
fill_ph: Option<Rgba>,
|
||||
line: Line,
|
||||
line_ph: Option<Rgba>,
|
||||
effects: Option<Effects>,
|
||||
fx_ph: Option<Rgba>,
|
||||
outline: BezPath,
|
||||
is_pic: bool,
|
||||
empty_ph: bool,
|
||||
}
|
||||
|
||||
impl ShapeDraw<'_, '_> {
|
||||
/// How much of the fill and stroke of path `i` shows (morphing outlines fade).
|
||||
fn fade(&self, i: usize) -> deckcraft_geom::morph::Fade {
|
||||
self.fades.get(i).copied().unwrap_or(deckcraft_geom::morph::Fade { fill: 1.0, stroke: 1.0 })
|
||||
}
|
||||
}
|
||||
|
||||
/// What casts a shape's shadow, glow and reflection: its geometry, fill and outline, in shape
|
||||
/// space `m` at size `w` x `h`.
|
||||
struct FxSource<'a> {
|
||||
geo: &'a Geometry,
|
||||
fill: &'a Option<Fill>,
|
||||
fill_alpha: Option<f64>,
|
||||
outline: Option<Caster<'a>>,
|
||||
content: Option<&'a deckcraft_model::style::PictureFill>,
|
||||
m: Affine,
|
||||
w: f64,
|
||||
h: f64,
|
||||
}
|
||||
|
||||
impl Renderer {
|
||||
fn canvas(pres: &Presentation, opts: &RenderOpts, filters: bool) -> (RenderContext, u16, u16, Affine) {
|
||||
let (w, h) = output_size(pres, opts);
|
||||
@@ -585,6 +541,7 @@ impl Renderer {
|
||||
Some(pm) => self.morphs.geometry(pm, s, w, h),
|
||||
None => (self.scaled_geometry(s, m, depth, w, h), Vec::new()),
|
||||
};
|
||||
let fade = |i: usize| fades.get(i).copied().unwrap_or(deckcraft_geom::morph::Fade { fill: 1.0, stroke: 1.0 });
|
||||
let (fill, fill_ph) = resolve::fill(rctx, s);
|
||||
let (line, line_ph) = resolve::line(rctx, s);
|
||||
let (effects, fx_ph) = resolve::effects(rctx, s);
|
||||
@@ -595,56 +552,6 @@ impl Renderer {
|
||||
// Placeholders in the editor: dashed outline and prompt, nothing else when empty.
|
||||
let empty_ph = s.ph.is_some() && s.text.as_ref().is_none_or(TextBody::is_empty) && !is_pic && !matches!(s.kind, ShapeKind::Media(_));
|
||||
let fill = if let Some(t) = st.tint { Some(Fill::solid(deckcraft_model::ColorRef::rgb(t))) } else { fill };
|
||||
let d = ShapeDraw { f, rctx, s, st, m, w, h, depth, geo, fades, fill, fill_ph, line, line_ph, effects, fx_ph, outline, is_pic, empty_ph };
|
||||
self.cast_effects(ctx, &d);
|
||||
let soft = d.effects.as_ref().and_then(|e| e.soft_edge).filter(|r| *r > 0.0);
|
||||
if let Some(r) = soft {
|
||||
ctx.set_transform(Affine::IDENTITY);
|
||||
ctx.push_layer(None, None, None, None, None);
|
||||
let _ = r;
|
||||
}
|
||||
let pt = self.point_scale(&d);
|
||||
self.draw_extrusion(ctx, &d, pt);
|
||||
self.draw_contour(ctx, &d, pt);
|
||||
self.draw_fill(ctx, &d);
|
||||
self.draw_fill_overlay(ctx, &d);
|
||||
self.draw_bevel(ctx, &d, pt);
|
||||
self.draw_outline(ctx, &d);
|
||||
if let Some(r) = soft {
|
||||
self.draw_feather(ctx, &d, r);
|
||||
}
|
||||
self.draw_inner_shadow(ctx, &d);
|
||||
self.draw_text_body(ctx, &d);
|
||||
}
|
||||
|
||||
/// Shape-space units per point for the 3-D sizes, measured without the camera's turn (which
|
||||
/// `shape_at` puts in `m`): its foreshortening is not a child-coordinate scale.
|
||||
fn point_scale(&self, d: &ShapeDraw) -> f64 {
|
||||
let turn = d
|
||||
.effects
|
||||
.as_ref()
|
||||
.and_then(|e| e.raw3d.as_deref())
|
||||
.and_then(camera::from_raw)
|
||||
.and_then(|c| camera::affine(&c, d.w, d.h))
|
||||
.filter(|t| t.determinant().abs() > 1e-9);
|
||||
self.per_point(turn.map_or(d.m, |t| d.m * t.inverse()), d.depth)
|
||||
}
|
||||
|
||||
fn cast_effects(&mut self, ctx: &mut RenderContext, d: &ShapeDraw) {
|
||||
let m = d.m;
|
||||
let w = d.w;
|
||||
let h = d.h;
|
||||
let depth = d.depth;
|
||||
let fill_ph = d.fill_ph;
|
||||
let line_ph = d.line_ph;
|
||||
let fx_ph = d.fx_ph;
|
||||
let is_pic = d.is_pic;
|
||||
let s = d.s;
|
||||
let rctx = d.rctx;
|
||||
let geo = &d.geo;
|
||||
let fill = &d.fill;
|
||||
let effects = &d.effects;
|
||||
let line = &d.line;
|
||||
// Effects below the shape.
|
||||
if let Some(e) = &effects {
|
||||
// A picture (or picture fill) casts its shadow from its pixels, not its frame.
|
||||
@@ -653,7 +560,7 @@ impl Renderer {
|
||||
_ => None,
|
||||
};
|
||||
// What casts the shadow: the fill as see-through as it is, and the outline.
|
||||
let fill_alpha = fill_opacity(rctx, fill, fill_ph);
|
||||
let fill_alpha = fill_opacity(rctx, &fill, fill_ph);
|
||||
let outline = line.fill.as_ref().filter(|f| !f.is_none()).map(|lf| {
|
||||
let a = match lf {
|
||||
Fill::Solid { color: c } => rctx.color(c, line_ph).a as f64 / 255.0,
|
||||
@@ -661,22 +568,28 @@ impl Renderer {
|
||||
};
|
||||
let (base, rel) = self.line_space(m, depth);
|
||||
let lw = line.width.unwrap_or(0.75).max(0.0);
|
||||
let outside = if border_outside(is_pic, effects.as_ref()) { outside_line(line, lw) } else { None };
|
||||
Caster { line: outside.map_or(std::borrow::Cow::Borrowed(line), std::borrow::Cow::Owned), ph: line_ph, alpha: a, base, rel }
|
||||
let outside = if border_outside(is_pic, effects.as_ref()) { outside_line(&line, lw) } else { None };
|
||||
Caster { line: outside.map_or(std::borrow::Cow::Borrowed(&line), std::borrow::Cow::Owned), ph: line_ph, alpha: a, base, rel }
|
||||
});
|
||||
self.effects_below(ctx, rctx, e, fx_ph, FxSource { geo, fill, fill_alpha, outline, content: content.as_ref(), m, w, h });
|
||||
self.effects_below(ctx, rctx, e, fx_ph, &geo, &fill, fill_alpha, outline, content.as_ref(), m, w, h);
|
||||
}
|
||||
}
|
||||
|
||||
fn draw_extrusion(&mut self, ctx: &mut RenderContext, d: &ShapeDraw, pt: f64) {
|
||||
let m = d.m;
|
||||
let w = d.w;
|
||||
let h = d.h;
|
||||
let fill_ph = d.fill_ph;
|
||||
let rctx = d.rctx;
|
||||
let fill = &d.fill;
|
||||
let effects = &d.effects;
|
||||
let outline = &d.outline;
|
||||
let soft = effects.as_ref().and_then(|e| e.soft_edge).filter(|r| *r > 0.0);
|
||||
if let Some(r) = soft {
|
||||
ctx.set_transform(Affine::IDENTITY);
|
||||
ctx.push_layer(None, None, None, None, None);
|
||||
let _ = r;
|
||||
}
|
||||
// Shape-space units per point for the 3-D sizes, measured without the camera's turn (which
|
||||
// `shape_at` puts in `m`): its foreshortening is not a child-coordinate scale.
|
||||
let pt = {
|
||||
let turn = effects
|
||||
.as_ref()
|
||||
.and_then(|e| e.raw3d.as_deref())
|
||||
.and_then(camera::from_raw)
|
||||
.and_then(|c| camera::affine(&c, w, h))
|
||||
.filter(|t| t.determinant().abs() > 1e-9);
|
||||
self.per_point(turn.map_or(m, |t| m * t.inverse()), depth)
|
||||
};
|
||||
// A 3-D extrusion under a turned camera: the sides, swept behind the face.
|
||||
if let Some(raw) = effects.as_ref().and_then(|e| e.raw3d.as_deref())
|
||||
&& let Some(ex) = extrude::from_raw(raw)
|
||||
@@ -691,17 +604,8 @@ impl Renderer {
|
||||
let pre = m * turn.inverse();
|
||||
let lit = light::Lighting::from_raw(raw);
|
||||
let d = ex.depth * pt;
|
||||
extrude::draw(ctx, outline, w, h, &cam, d, pre, turn, extrude::side_color(rctx, &ex, face), &lit);
|
||||
extrude::draw(ctx, &outline, w, h, &cam, d, pre, turn, extrude::side_color(rctx, &ex, face), &lit);
|
||||
}
|
||||
}
|
||||
|
||||
fn draw_contour(&mut self, ctx: &mut RenderContext, d: &ShapeDraw, pt: f64) {
|
||||
let m = d.m;
|
||||
let fill_ph = d.fill_ph;
|
||||
let rctx = d.rctx;
|
||||
let fill = &d.fill;
|
||||
let effects = &d.effects;
|
||||
let outline = &d.outline;
|
||||
// A 3-D contour: a band around the outline, behind the face (and in front of any sides).
|
||||
if let Some(raw) = effects.as_ref().and_then(|e| e.raw3d.as_deref())
|
||||
&& let Some(c) = extrude::contour_from_raw(raw)
|
||||
@@ -711,23 +615,8 @@ impl Renderer {
|
||||
_ => None,
|
||||
};
|
||||
let col = c.color.as_ref().map(|k| rctx.color(k, face)).or(face).unwrap_or(Rgba::rgb(0xff, 0xff, 0xff));
|
||||
extrude::draw_contour(ctx, outline, m, c.width * pt, col, &light::Lighting::from_raw(raw));
|
||||
extrude::draw_contour(ctx, &outline, m, c.width * pt, col, &light::Lighting::from_raw(raw));
|
||||
}
|
||||
}
|
||||
|
||||
fn draw_fill(&mut self, ctx: &mut RenderContext, d: &ShapeDraw) {
|
||||
let m = d.m;
|
||||
let w = d.w;
|
||||
let h = d.h;
|
||||
let fill_ph = d.fill_ph;
|
||||
let empty_ph = d.empty_ph;
|
||||
let s = d.s;
|
||||
let f = d.f;
|
||||
let rctx = d.rctx;
|
||||
let geo = &d.geo;
|
||||
let fill = &d.fill;
|
||||
let outline = &d.outline;
|
||||
let fade = |i: usize| d.fade(i);
|
||||
// Fill.
|
||||
match &s.kind {
|
||||
ShapeKind::Picture { fill: pf } => {
|
||||
@@ -736,11 +625,11 @@ impl Renderer {
|
||||
if s.fill.as_ref().is_some_and(|f| !f.is_none())
|
||||
&& let Some(fl) = &fill
|
||||
{
|
||||
self.fill_geometry(ctx, f, rctx, fl, fill_ph, geo, (w, h), m, &fade);
|
||||
self.fill_geometry(ctx, f, rctx, fl, fill_ph, &geo, (w, h), m, &fade);
|
||||
}
|
||||
ctx.set_transform(m);
|
||||
let bounds = Rect::new(0.0, 0.0, w, h);
|
||||
ctx.push_clip_layer(outline);
|
||||
ctx.push_clip_layer(&outline);
|
||||
let t = f.opts.gif_times.iter().find(|g| g.0 == s.id).map(|g| g.1);
|
||||
paint::picture(ctx, f.pres, &paint::resolved(rctx, pf, None), bounds, m, t);
|
||||
ctx.pop_layer();
|
||||
@@ -753,7 +642,7 @@ impl Renderer {
|
||||
if s.fill.as_ref().is_some_and(|f| !f.is_none())
|
||||
&& let Some(fl) = &fill
|
||||
{
|
||||
self.fill_geometry(ctx, f, rctx, fl, fill_ph, geo, (w, h), m, &fade);
|
||||
self.fill_geometry(ctx, f, rctx, fl, fill_ph, &geo, (w, h), m, &fade);
|
||||
}
|
||||
ctx.set_transform(m);
|
||||
let pf = deckcraft_model::style::PictureFill { media: *media, ..Default::default() };
|
||||
@@ -764,7 +653,7 @@ impl Renderer {
|
||||
let bounds = Rect::new(0.0, 0.0, w, h);
|
||||
if let Some(p) = mc.poster {
|
||||
let pf = deckcraft_model::style::PictureFill { media: p, crop: mc.crop, ..Default::default() };
|
||||
ctx.push_clip_layer(outline);
|
||||
ctx.push_clip_layer(&outline);
|
||||
paint::picture(ctx, f.pres, &pf, bounds, m, None);
|
||||
ctx.pop_layer();
|
||||
} else if mc.video {
|
||||
@@ -779,23 +668,10 @@ impl Renderer {
|
||||
if let Some(fl) = &fill
|
||||
&& !(empty_ph && f.opts.edit && s.fill.is_none() && s.line.is_none() && s.style.is_none())
|
||||
{
|
||||
self.fill_geometry(ctx, f, rctx, fl, fill_ph, geo, (w, h), m, &fade);
|
||||
self.fill_geometry(ctx, f, rctx, fl, fill_ph, &geo, (w, h), m, &fade);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn draw_fill_overlay(&mut self, ctx: &mut RenderContext, d: &ShapeDraw) {
|
||||
let m = d.m;
|
||||
let w = d.w;
|
||||
let h = d.h;
|
||||
let fx_ph = d.fx_ph;
|
||||
let f = d.f;
|
||||
let rctx = d.rctx;
|
||||
let geo = &d.geo;
|
||||
let fill = &d.fill;
|
||||
let effects = &d.effects;
|
||||
let fade = |i: usize| d.fade(i);
|
||||
// `a:fillOverlay`: a fill blended over the shape's fill (PowerPoint's "Flow" themes multiply
|
||||
// a gradient over a tiled picture), inside the filled outline, under the line.
|
||||
if let Some(ov) = effects.as_ref().and_then(|e| e.fill_overlay.as_ref())
|
||||
@@ -815,19 +691,13 @@ impl Renderer {
|
||||
ctx.pop_layer();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn draw_bevel(&mut self, ctx: &mut RenderContext, d: &ShapeDraw, pt: f64) {
|
||||
let m = d.m;
|
||||
let w = d.w;
|
||||
let h = d.h;
|
||||
let fill_ph = d.fill_ph;
|
||||
let is_pic = d.is_pic;
|
||||
let rctx = d.rctx;
|
||||
let geo = &d.geo;
|
||||
let fill = &d.fill;
|
||||
let effects = &d.effects;
|
||||
let outline = &d.outline;
|
||||
// Some presets' outlines default to sharp corners (PowerPoint's own per-shape default).
|
||||
let line = match (&s.geom, line.join) {
|
||||
(Geom::Preset { name, .. }, None) if preset::mitred_by_default(name) => {
|
||||
Line { join: Some(deckcraft_model::style::LineJoin::Miter), ..line }
|
||||
}
|
||||
_ => line,
|
||||
};
|
||||
// A 3-D bevel: the face and a band inside the outline lit as surfaces.
|
||||
if let Some(bv) = effects.as_ref().and_then(|e| e.raw3d.as_deref()).and_then(bevel::from_raw) {
|
||||
let bv = bevel::Bevel { w: bv.w * pt, h: bv.h * pt, ..bv };
|
||||
@@ -847,29 +717,6 @@ impl Renderer {
|
||||
bevel::draw(ctx, &bv, if is_pic { None } else { solid }, &area, Rect::new(0.0, 0.0, w, h), m, 1.0);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn draw_outline(&mut self, ctx: &mut RenderContext, d: &ShapeDraw) {
|
||||
let m = d.m;
|
||||
let w = d.w;
|
||||
let h = d.h;
|
||||
let depth = d.depth;
|
||||
let line_ph = d.line_ph;
|
||||
let is_pic = d.is_pic;
|
||||
let s = d.s;
|
||||
let rctx = d.rctx;
|
||||
let geo = &d.geo;
|
||||
let effects = &d.effects;
|
||||
let outline = &d.outline;
|
||||
let line = d.line.clone();
|
||||
let fade = |i: usize| d.fade(i);
|
||||
// Some presets' outlines default to sharp corners (PowerPoint's own per-shape default).
|
||||
let line = match (&s.geom, line.join) {
|
||||
(Geom::Preset { name, .. }, None) if preset::mitred_by_default(name) => {
|
||||
Line { join: Some(deckcraft_model::style::LineJoin::Miter), ..line }
|
||||
}
|
||||
_ => line,
|
||||
};
|
||||
// Outline.
|
||||
if line.fill.as_ref().is_some_and(|f| !f.is_none()) {
|
||||
let lw = line.width.unwrap_or(0.75).max(0.0);
|
||||
@@ -932,7 +779,7 @@ impl Renderer {
|
||||
stroke(ctx, rctx, &line, line_ph, &path, Some(&whole), base, lw, fade(i).stroke);
|
||||
}
|
||||
if ends {
|
||||
arrowheads(ctx, rctx, &line, line_ph, geo, rel, base, lw);
|
||||
arrowheads(ctx, rctx, &line, line_ph, &geo, rel, base, lw);
|
||||
}
|
||||
if clipped {
|
||||
ctx.pop_layer();
|
||||
@@ -941,46 +788,33 @@ impl Renderer {
|
||||
ctx.pop_layer();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn draw_feather(&mut self, ctx: &mut RenderContext, d: &ShapeDraw, r: f64) {
|
||||
let m = d.m;
|
||||
let outline = &d.outline;
|
||||
// Feather: the shape (fill and line) fades to nothing at its outline. Measured on
|
||||
// PowerPoint's export: the mask is the outline shrunk by 0.85 r, blurred by a third of
|
||||
// r, so it's half opaque 0.85 r inside the edge and transparent at the edge itself.
|
||||
ctx.set_transform(m);
|
||||
ctx.push_layer(
|
||||
None,
|
||||
Some(peniko::BlendMode::new(peniko::Mix::Normal, peniko::Compose::DestIn)),
|
||||
None,
|
||||
None,
|
||||
Some(blur(r / 3.0 * scale_of(m))),
|
||||
);
|
||||
ctx.set_paint(peniko::Color::from_rgba8(0, 0, 0, 255));
|
||||
ctx.fill_path(outline);
|
||||
ctx.push_blend_layer(peniko::BlendMode::new(peniko::Mix::Normal, peniko::Compose::DestOut));
|
||||
ctx.set_stroke(kurbo::Stroke::new(r * 1.7).with_join(kurbo::Join::Round));
|
||||
ctx.stroke_path(outline);
|
||||
ctx.pop_layer();
|
||||
ctx.pop_layer();
|
||||
ctx.pop_layer();
|
||||
}
|
||||
|
||||
fn draw_inner_shadow(&mut self, ctx: &mut RenderContext, d: &ShapeDraw) {
|
||||
let m = d.m;
|
||||
let w = d.w;
|
||||
let h = d.h;
|
||||
let fx_ph = d.fx_ph;
|
||||
let rctx = d.rctx;
|
||||
let effects = &d.effects;
|
||||
let outline = &d.outline;
|
||||
if let Some(r) = soft {
|
||||
// Feather: the shape (fill and line) fades to nothing at its outline. Measured on
|
||||
// PowerPoint's export: the mask is the outline shrunk by 0.85 r, blurred by a third of
|
||||
// r, so it's half opaque 0.85 r inside the edge and transparent at the edge itself.
|
||||
ctx.set_transform(m);
|
||||
ctx.push_layer(
|
||||
None,
|
||||
Some(peniko::BlendMode::new(peniko::Mix::Normal, peniko::Compose::DestIn)),
|
||||
None,
|
||||
None,
|
||||
Some(blur(r / 3.0 * scale_of(m))),
|
||||
);
|
||||
ctx.set_paint(peniko::Color::from_rgba8(0, 0, 0, 255));
|
||||
ctx.fill_path(&outline);
|
||||
ctx.push_blend_layer(peniko::BlendMode::new(peniko::Mix::Normal, peniko::Compose::DestOut));
|
||||
ctx.set_stroke(kurbo::Stroke::new(r * 1.7).with_join(kurbo::Join::Round));
|
||||
ctx.stroke_path(&outline);
|
||||
ctx.pop_layer();
|
||||
ctx.pop_layer();
|
||||
ctx.pop_layer();
|
||||
}
|
||||
// Inner shadow.
|
||||
if let Some(e) = &effects
|
||||
&& let Some(sh) = &e.inner_shadow
|
||||
{
|
||||
ctx.set_transform(m);
|
||||
ctx.push_clip_layer(outline);
|
||||
ctx.push_clip_layer(&outline);
|
||||
let c = rctx.color(&sh.color, fx_ph);
|
||||
let d = Vec2::new(sh.dir.to_radians().cos(), sh.dir.to_radians().sin()) * sh.dist;
|
||||
// As PowerPoint draws it (measured on its export): the shadow falls inside along the
|
||||
@@ -998,19 +832,6 @@ impl Renderer {
|
||||
ctx.pop_layer();
|
||||
ctx.pop_layer();
|
||||
}
|
||||
}
|
||||
|
||||
fn draw_text_body(&mut self, ctx: &mut RenderContext, d: &ShapeDraw) {
|
||||
let m = d.m;
|
||||
let w = d.w;
|
||||
let h = d.h;
|
||||
let depth = d.depth;
|
||||
let empty_ph = d.empty_ph;
|
||||
let s = d.s;
|
||||
let f = d.f;
|
||||
let rctx = d.rctx;
|
||||
let st = d.st;
|
||||
let geo = &d.geo;
|
||||
// Text (or the placeholder prompt in the editor).
|
||||
let skip_text = f.opts.skip_text == Some(s.id);
|
||||
if let Some(body) = &s.text
|
||||
@@ -1021,7 +842,7 @@ impl Renderer {
|
||||
let (tw, th, m) = self.text_space(m, depth, w, h);
|
||||
let text_geo;
|
||||
let geo = if (tw, th) == (w, h) {
|
||||
geo
|
||||
&geo
|
||||
} else {
|
||||
text_geo = shape_geometry(s, tw, th);
|
||||
&text_geo
|
||||
@@ -1057,7 +878,7 @@ impl Renderer {
|
||||
let prompt_body = prompt_body(rctx, s, kind);
|
||||
if !prompt_body.is_empty() {
|
||||
let grey = Rgba::rgb(0x59, 0x59, 0x59);
|
||||
draw_text(ctx, rctx, s, &prompt_body, m, &Opts { rect: tr, fields: &f.fields, prompt_color: Some(grey), no_shrink: false });
|
||||
draw_text(ctx, rctx, s, &prompt_body, tr, m, &f.fields, Some(grey));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1074,8 +895,21 @@ impl Renderer {
|
||||
}
|
||||
}
|
||||
|
||||
fn effects_below(&mut self, ctx: &mut RenderContext, rctx: &Ctx, e: &Effects, ph: Option<Rgba>, src: FxSource) {
|
||||
let FxSource { geo, fill, fill_alpha, outline, content, m, w, h } = src;
|
||||
fn effects_below(
|
||||
&mut self,
|
||||
ctx: &mut RenderContext,
|
||||
rctx: &Ctx,
|
||||
e: &Effects,
|
||||
ph: Option<Rgba>,
|
||||
geo: &Geometry,
|
||||
fill: &Option<Fill>,
|
||||
fill_alpha: Option<f64>,
|
||||
outline: Option<Caster>,
|
||||
content: Option<&deckcraft_model::style::PictureFill>,
|
||||
m: Affine,
|
||||
w: f64,
|
||||
h: f64,
|
||||
) {
|
||||
let silhouette = {
|
||||
let mut p = BezPath::new();
|
||||
for sp in &geo.paths {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
use super::opts::{RenderOpts, SlideFields};
|
||||
use super::raster::{Image, MAX_SIDE};
|
||||
use super::renderer::{Frame, RENDERER, Renderer};
|
||||
use super::{Affine, Ctx, PathMorph, Presentation, Rect, RenderContext, Shape, ShapeId, Slide, TextMorph, resolve};
|
||||
use super::*;
|
||||
|
||||
static PANICS: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0);
|
||||
|
||||
@@ -125,7 +125,8 @@ pub fn render_slide_part(pres: &Presentation, index: usize, part: SlidePart, opt
|
||||
/// to `shapes` only.
|
||||
pub fn render_blend(
|
||||
pres: &Presentation,
|
||||
(old, new): (usize, usize),
|
||||
old: usize,
|
||||
new: usize,
|
||||
mix: f64,
|
||||
shapes: &[(Shape, bool)],
|
||||
text: &[TextMorph],
|
||||
|
||||
@@ -616,14 +616,14 @@ fn blend_ends_match_the_slides_and_mixes_backdrops() {
|
||||
let o = RenderOpts { scale: 0.5, ..Default::default() };
|
||||
let shapes = vec![(sh, false)];
|
||||
// At the ends the frame is the slide itself; the shape stays opaque throughout.
|
||||
assert_eq!(render_blend(&p, (0, 1), 1.0, &shapes, &[], &[], &o).pixels, render_slide(&p, 1, &o).pixels);
|
||||
let mid = render_blend(&p, (0, 1), 0.5, &shapes, &[], &[], &o);
|
||||
assert_eq!(render_blend(&p, 0, 1, 1.0, &shapes, &[], &[], &o).pixels, render_slide(&p, 1, &o).pixels);
|
||||
let mid = render_blend(&p, 0, 1, 0.5, &shapes, &[], &[], &o);
|
||||
let px = mid.pixel(5, 5);
|
||||
assert!((px[0] as i32 - 128).abs() <= 3, "{px:?}");
|
||||
assert_eq!(mid.pixel(100, 75), render_slide(&p, 1, &o).pixel(100, 75));
|
||||
// Hostile input: bad indices and NaN never panic.
|
||||
assert_eq!(render_blend(&p, (0, 9), 0.5, &shapes, &[], &[], &o).width, 0);
|
||||
let _ = render_blend(&p, (0, 1), f64::NAN, &shapes, &[], &[], &o);
|
||||
assert_eq!(render_blend(&p, 0, 9, 0.5, &shapes, &[], &[], &o).width, 0);
|
||||
let _ = render_blend(&p, 0, 1, f64::NAN, &shapes, &[], &[], &o);
|
||||
}
|
||||
|
||||
fn text_shape(id: u32, text: &str, x: f64, y: f64) -> Shape {
|
||||
@@ -668,7 +668,7 @@ fn morph_text_ends_match_the_slides() {
|
||||
sb.xfrm = Some(x);
|
||||
sa.xfrm = Some(x);
|
||||
let tm = TextMorph { old: ShapeId(11), new: ShapeId(10), from, to, chars: false, t };
|
||||
render_blend(&p, (0, 1), t, &[(sb, false), (sa, true)], &[tm], &[], &o)
|
||||
render_blend(&p, 0, 1, t, &[(sb, false), (sa, true)], &[tm], &[], &o)
|
||||
};
|
||||
// Warm up: faces load lazily (a background scan runs), so the first render may still fall back.
|
||||
let _ = (render_slide(&p, 0, &o), render_slide(&p, 1, &o), frame(0.5));
|
||||
@@ -781,7 +781,7 @@ fn morph_path_ends_match_the_slides() {
|
||||
let lerp = |a: f64, b: f64| a + (b - a) * t;
|
||||
let x = Xfrm::new(lerp(from.x, to.x), lerp(from.y, to.y), lerp(from.w, to.w), lerp(from.h, to.h));
|
||||
let pm = PathMorph { id: ShapeId(10), from: Geom::preset("ellipse"), from_box: from, to_box: to, t };
|
||||
render_blend(&p, (0, 1), t, &[(form(10, "star5", x), false)], &[], &[pm], &o)
|
||||
render_blend(&p, 0, 1, t, &[(form(10, "star5", x), false)], &[], &[pm], &o)
|
||||
};
|
||||
// The morph splits the outlines into more curves, which flatten slightly differently: edge pixels
|
||||
// may differ a little, a different shape differs a lot.
|
||||
@@ -792,7 +792,7 @@ fn morph_path_ends_match_the_slides() {
|
||||
let d1 = differ(&frame(1.0), &render_slide(&p, 1, &o));
|
||||
assert!(d1 < 40, "end differs {d1}");
|
||||
// Without the morph the start would show the star.
|
||||
let star = render_blend(&p, (0, 1), 0.0, &[(form(10, "star5", from), false)], &[], &[], &o);
|
||||
let star = render_blend(&p, 0, 1, 0.0, &[(form(10, "star5", from), false)], &[], &[], &o);
|
||||
assert!(differ(&star, &render_slide(&p, 0, &o)) > 400);
|
||||
// Halfway the shape is filled around the middle of its box.
|
||||
let mid = frame(0.5);
|
||||
@@ -818,7 +818,7 @@ fn morph_path_fades_a_fill_only_one_side_has() {
|
||||
let at = |t: f64| {
|
||||
let pm = PathMorph { id: ShapeId(10), from: Geom::preset("rect"), from_box: bx, to_box: bx, t };
|
||||
// A point near the line but not on it: inside the shape all the way.
|
||||
render_blend(&p, (0, 1), t, &[(form(10, "line"), false)], &[], &[pm], &o).pixel(110, 90)
|
||||
render_blend(&p, 0, 1, t, &[(form(10, "line"), false)], &[], &[pm], &o).pixel(110, 90)
|
||||
};
|
||||
let (full, half, none) = (at(0.0), at(0.5), at(1.0));
|
||||
assert_eq!(none, [255, 255, 255, 255]);
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
use super::geometry::{blur, color, pre_translate, scale_of};
|
||||
use super::opts::ParaState;
|
||||
use super::{
|
||||
Affine, Arc, BezPath, Ctx, FontDb, HashMap, Opts, PhType, Rect, RenderContext, Rgba, Shape, TextBody, Vec2, colr, paint, peniko, resolve,
|
||||
};
|
||||
use super::*;
|
||||
|
||||
/// The prompt shown in an empty placeholder, formatted like its content would be.
|
||||
pub(crate) fn prompt_body(rctx: &Ctx, s: &Shape, kind: PhType) -> TextBody {
|
||||
@@ -55,10 +53,10 @@ pub(crate) fn glyph(face: &deckcraft_fonts::FontFace, gid: u32) -> Arc<BezPath>
|
||||
p
|
||||
}
|
||||
|
||||
/// Draw a text body into shape space `m` within the text rect `opts.rect`.
|
||||
pub fn draw_text(ctx: &mut RenderContext, rctx: &Ctx, s: &Shape, body: &TextBody, m: Affine, opts: &Opts) {
|
||||
let l = deckcraft_text::layout(rctx, s, body, opts);
|
||||
draw_layout(ctx, &l, opts.rect, m);
|
||||
/// Draw a text body into shape space `m` within the text rect.
|
||||
pub fn draw_text(ctx: &mut RenderContext, rctx: &Ctx, s: &Shape, body: &TextBody, tr: Rect, m: Affine, fields: &dyn Fields, prompt: Option<Rgba>) {
|
||||
let l = deckcraft_text::layout(rctx, s, body, &Opts { rect: tr, fields, prompt_color: prompt, no_shrink: false });
|
||||
draw_layout(ctx, &l, tr, m);
|
||||
}
|
||||
|
||||
/// Add transient list markers to an editor layout, using the document's own bullet glyphs.
|
||||
|
||||
@@ -87,7 +87,7 @@ pub fn render_transition(pres: &Presentation, index: usize, t: f64, opts: &Rende
|
||||
.iter()
|
||||
.map(|x| crate::PathMorph { id: x.id, from: x.from.clone(), from_box: x.from_box, to_box: x.to_box, t: m.mix })
|
||||
.collect();
|
||||
return render_blend(pres, (from, index), m.mix, &m.shapes, &text, &paths, &o);
|
||||
return render_blend(pres, from, index, m.mix, &m.shapes, &text, &paths, &o);
|
||||
}
|
||||
}
|
||||
let new = render_slide(pres, index, opts);
|
||||
|
||||
+100
-116
@@ -66,16 +66,109 @@ pub(crate) fn levels_lang(text: &str, chars: &[char], rtl: bool, rtl_lang: &[boo
|
||||
if classes.iter().any(|c| matches!(c, LRE | RLE | LRO | RLO | PDF | LRI | RLI | FSI | PDI)) {
|
||||
return levels(text, chars, rtl);
|
||||
}
|
||||
let cls = resolve_nsm(classes, rtl);
|
||||
let mut dir = initial_dirs(&cls, rtl_lang);
|
||||
join_numbers_in_rtl_runs(&mut dir, &cls, rtl_lang);
|
||||
let n = chars.len();
|
||||
let rl = |i: usize| rtl_lang.get(i).copied().unwrap_or(false);
|
||||
let sos = if rtl { Dir::R } else { Dir::L };
|
||||
// W1: combining marks take the class of the character they sit on.
|
||||
let mut cls = classes;
|
||||
for i in 0..n {
|
||||
if cls.get(i) == Some(&NSM) {
|
||||
let prev = if i == 0 { if rtl { R } else { L } } else { cls.get(i - 1).copied().unwrap_or(L) };
|
||||
if let Some(c) = cls.get_mut(i) {
|
||||
*c = prev;
|
||||
}
|
||||
}
|
||||
}
|
||||
let mut dir = vec![Dir::Open; n];
|
||||
// Strong letters and digits.
|
||||
let mut last_strong: Option<BidiClass> = None;
|
||||
for i in 0..n {
|
||||
let c = cls.get(i).copied().unwrap_or(ON);
|
||||
let d = match c {
|
||||
L => Dir::L,
|
||||
R | AL => Dir::R,
|
||||
AN => Dir::Num,
|
||||
EN if !rl(i) => Dir::L,
|
||||
// W7: European digits after left-to-right text are left to right.
|
||||
EN if last_strong == Some(L) => Dir::L,
|
||||
EN => Dir::Num,
|
||||
_ => Dir::Open,
|
||||
};
|
||||
if matches!(c, L | R | AL) {
|
||||
last_strong = Some(c);
|
||||
}
|
||||
if let Some(x) = dir.get_mut(i) {
|
||||
*x = d;
|
||||
}
|
||||
}
|
||||
// W4/W5 in right-to-left runs: a separator between digits, and currency or percent signs next
|
||||
// to them, belong to the number. `after_et[i]` is the first index after `i` that is not ET,
|
||||
// computed once so a long run of currency signs costs linear work, not a scan per sign.
|
||||
let after_et = next_non_et(&cls);
|
||||
for i in 0..n {
|
||||
if !rl(i) || dir.get(i) != Some(&Dir::Open) {
|
||||
continue;
|
||||
}
|
||||
let c = cls.get(i).copied().unwrap_or(ON);
|
||||
let num = |j: Option<usize>| j.and_then(|j| dir.get(j)).is_some_and(|d| *d == Dir::Num);
|
||||
let joined = match c {
|
||||
ES | CS => num(i.checked_sub(1)) && num(Some(i + 1)),
|
||||
ET => num(i.checked_sub(1)) || num(after_et.get(i).copied().flatten()),
|
||||
_ => false,
|
||||
};
|
||||
if joined && let Some(x) = dir.get_mut(i) {
|
||||
*x = Dir::Num;
|
||||
}
|
||||
}
|
||||
// Punctuation in left-to-right runs is left to right.
|
||||
for (i, d) in dir.iter_mut().enumerate() {
|
||||
if !in_rtl_lang(rtl_lang, i) && *d == Dir::Open && !matches!(cls.get(i), Some(WS | S)) {
|
||||
*d = Dir::L;
|
||||
for i in 0..n {
|
||||
if !rl(i)
|
||||
&& dir.get(i) == Some(&Dir::Open)
|
||||
&& !matches!(cls.get(i), Some(WS | S))
|
||||
&& let Some(x) = dir.get_mut(i)
|
||||
{
|
||||
*x = Dir::L;
|
||||
}
|
||||
}
|
||||
// Neutrals in right-to-left runs: the direction around them when both sides agree, else
|
||||
// right to left. Unresolved spaces of left-to-right runs are looked through.
|
||||
let strong_at = |dir: &[Dir], j: usize| match dir.get(j) {
|
||||
Some(Dir::L) => Some(Dir::L),
|
||||
Some(Dir::R | Dir::Num) => Some(Dir::R),
|
||||
_ => None,
|
||||
};
|
||||
// Resolving a span only changes characters before the scan position, so the nearest strong
|
||||
// direction after a span can be computed once up front, and the one before it is carried along:
|
||||
// linear work however many neutral spans the paragraph has.
|
||||
let mut next_strong: Vec<Option<Dir>> = vec![None; n + 1];
|
||||
for j in (0..n).rev() {
|
||||
let here = strong_at(&dir, j).or(next_strong.get(j + 1).copied().flatten());
|
||||
if let Some(x) = next_strong.get_mut(j) {
|
||||
*x = here;
|
||||
}
|
||||
}
|
||||
let mut last_strong: Option<Dir> = None;
|
||||
let mut i = 0;
|
||||
while i < n {
|
||||
if !(rl(i) && dir.get(i) == Some(&Dir::Open)) {
|
||||
last_strong = strong_at(&dir, i).or(last_strong);
|
||||
i += 1;
|
||||
continue;
|
||||
}
|
||||
let start = i;
|
||||
while i < n && rl(i) && dir.get(i) == Some(&Dir::Open) {
|
||||
i += 1;
|
||||
}
|
||||
let before = last_strong.unwrap_or(sos);
|
||||
let after = next_strong.get(i).copied().flatten().unwrap_or(sos);
|
||||
let d = if before == after { before } else { Dir::R };
|
||||
for x in dir.get_mut(start..i).into_iter().flatten() {
|
||||
*x = d;
|
||||
}
|
||||
if start < i {
|
||||
last_strong = Some(if d == Dir::Num { Dir::R } else { d });
|
||||
}
|
||||
}
|
||||
resolve_rtl_neutrals(&mut dir, rtl_lang, if rtl { Dir::R } else { Dir::L });
|
||||
// Spaces in left-to-right runs follow the character before them.
|
||||
let mut prev = Dir::L;
|
||||
for d in dir.iter_mut() {
|
||||
@@ -94,115 +187,6 @@ pub(crate) fn levels_lang(text: &str, chars: &[char], rtl: bool, rtl_lang: &[boo
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn in_rtl_lang(rtl_lang: &[bool], i: usize) -> bool {
|
||||
rtl_lang.get(i).copied().unwrap_or(false)
|
||||
}
|
||||
|
||||
/// W1: combining marks take the class of the character they sit on.
|
||||
fn resolve_nsm(mut cls: Vec<BidiClass>, rtl: bool) -> Vec<BidiClass> {
|
||||
for i in 0..cls.len() {
|
||||
if cls.get(i) == Some(&NSM) {
|
||||
let prev = if i == 0 { if rtl { R } else { L } } else { cls.get(i - 1).copied().unwrap_or(L) };
|
||||
if let Some(c) = cls.get_mut(i) {
|
||||
*c = prev;
|
||||
}
|
||||
}
|
||||
}
|
||||
cls
|
||||
}
|
||||
|
||||
/// Strong letters and digits get a direction; everything else stays open.
|
||||
fn initial_dirs(cls: &[BidiClass], rtl_lang: &[bool]) -> Vec<Dir> {
|
||||
let mut last_strong: Option<BidiClass> = None;
|
||||
cls.iter()
|
||||
.enumerate()
|
||||
.map(|(i, &c)| {
|
||||
let d = match c {
|
||||
L => Dir::L,
|
||||
R | AL => Dir::R,
|
||||
AN => Dir::Num,
|
||||
EN if !in_rtl_lang(rtl_lang, i) => Dir::L,
|
||||
// W7: European digits after left-to-right text are left to right.
|
||||
EN if last_strong == Some(L) => Dir::L,
|
||||
EN => Dir::Num,
|
||||
_ => Dir::Open,
|
||||
};
|
||||
if matches!(c, L | R | AL) {
|
||||
last_strong = Some(c);
|
||||
}
|
||||
d
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// W4/W5 in right-to-left runs: a separator between digits, and currency or percent signs next
|
||||
/// to them, belong to the number. `after_et[i]` is the first index after `i` that is not ET,
|
||||
/// computed once so a long run of currency signs costs linear work, not a scan per sign.
|
||||
fn join_numbers_in_rtl_runs(dir: &mut [Dir], cls: &[BidiClass], rtl_lang: &[bool]) {
|
||||
let after_et = next_non_et(cls);
|
||||
for i in 0..dir.len() {
|
||||
if !in_rtl_lang(rtl_lang, i) || dir.get(i) != Some(&Dir::Open) {
|
||||
continue;
|
||||
}
|
||||
let c = cls.get(i).copied().unwrap_or(ON);
|
||||
let num = |j: Option<usize>| j.and_then(|j| dir.get(j)).is_some_and(|d| *d == Dir::Num);
|
||||
let joined = match c {
|
||||
ES | CS => num(i.checked_sub(1)) && num(Some(i + 1)),
|
||||
ET => num(i.checked_sub(1)) || num(after_et.get(i).copied().flatten()),
|
||||
_ => false,
|
||||
};
|
||||
if joined && let Some(x) = dir.get_mut(i) {
|
||||
*x = Dir::Num;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn strong_at(dir: &[Dir], j: usize) -> Option<Dir> {
|
||||
match dir.get(j) {
|
||||
Some(Dir::L) => Some(Dir::L),
|
||||
Some(Dir::R | Dir::Num) => Some(Dir::R),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Neutrals in right-to-left runs: the direction around them when both sides agree, else right to
|
||||
/// left. Unresolved spaces of left-to-right runs are looked through.
|
||||
///
|
||||
/// Resolving a span only changes characters before the scan position, so the nearest strong
|
||||
/// direction after a span can be computed once up front, and the one before it is carried along:
|
||||
/// linear work however many neutral spans the paragraph has.
|
||||
fn resolve_rtl_neutrals(dir: &mut [Dir], rtl_lang: &[bool], sos: Dir) {
|
||||
let n = dir.len();
|
||||
let rl = |dir: &[Dir], i: usize| in_rtl_lang(rtl_lang, i) && dir.get(i) == Some(&Dir::Open);
|
||||
let mut next_strong: Vec<Option<Dir>> = vec![None; n + 1];
|
||||
for j in (0..n).rev() {
|
||||
let here = strong_at(dir, j).or(next_strong.get(j + 1).copied().flatten());
|
||||
if let Some(x) = next_strong.get_mut(j) {
|
||||
*x = here;
|
||||
}
|
||||
}
|
||||
let mut last_strong: Option<Dir> = None;
|
||||
let mut i = 0;
|
||||
while i < n {
|
||||
if !rl(dir, i) {
|
||||
last_strong = strong_at(dir, i).or(last_strong);
|
||||
i += 1;
|
||||
continue;
|
||||
}
|
||||
let start = i;
|
||||
while i < n && rl(dir, i) {
|
||||
i += 1;
|
||||
}
|
||||
let before = last_strong.unwrap_or(sos);
|
||||
let after = next_strong.get(i).copied().flatten().unwrap_or(sos);
|
||||
let d = if before == after { before } else { Dir::R };
|
||||
for x in dir.get_mut(start..i).into_iter().flatten() {
|
||||
*x = d;
|
||||
}
|
||||
last_strong = Some(if d == Dir::Num { Dir::R } else { d });
|
||||
}
|
||||
}
|
||||
|
||||
/// For each index, the first later index whose class is not ET (`None` when only ET follows).
|
||||
fn next_non_et(cls: &[BidiClass]) -> Vec<Option<usize>> {
|
||||
let mut out = vec![None; cls.len()];
|
||||
|
||||
+11
-14
@@ -1,5 +1,5 @@
|
||||
use super::style::{Cell, ParaShaped};
|
||||
use super::{FontFace, ParaProps};
|
||||
use super::*;
|
||||
|
||||
pub(crate) fn measure_str(face: &FontFace, size: f64, s: &str) -> (f64, Vec<(u32, f64)>) {
|
||||
let g = deckcraft_fonts::shape(face, s, &[], |c| c);
|
||||
@@ -132,19 +132,16 @@ pub(crate) struct RawLine {
|
||||
next: usize,
|
||||
}
|
||||
|
||||
/// Wrapping geometry of one paragraph: line widths, start offsets, wrap flag and tab settings.
|
||||
pub(crate) struct BreakParams<'a> {
|
||||
pub(crate) first_w: f64,
|
||||
pub(crate) rest_w: f64,
|
||||
pub(crate) wrap: bool,
|
||||
pub(crate) tab_stops: &'a [Tab],
|
||||
pub(crate) default_tab: f64,
|
||||
pub(crate) x0_first: f64,
|
||||
pub(crate) x0_rest: f64,
|
||||
}
|
||||
|
||||
pub(crate) fn break_lines(p: &ParaShaped, params: &BreakParams) -> Vec<RawLine> {
|
||||
let BreakParams { first_w, rest_w, wrap, tab_stops, default_tab, x0_first, x0_rest } = *params;
|
||||
pub(crate) fn break_lines(
|
||||
p: &ParaShaped,
|
||||
first_w: f64,
|
||||
rest_w: f64,
|
||||
wrap: bool,
|
||||
tab_stops: &[Tab],
|
||||
default_tab: f64,
|
||||
x0_first: f64,
|
||||
x0_rest: f64,
|
||||
) -> Vec<RawLine> {
|
||||
let text: String = p.cells.iter().map(|c| c.ch).collect();
|
||||
let (allowed, _) = break_opportunities(&text, p.props.latin_line_break.unwrap_or(false));
|
||||
let hang = p.props.hanging_punct.unwrap_or(false);
|
||||
|
||||
+503
-688
File diff suppressed because it is too large
Load Diff
@@ -1,4 +1,4 @@
|
||||
use super::{Arc, FontFace, Point, Rect, Rgba};
|
||||
use super::*;
|
||||
|
||||
/// A run of glyphs in one face, size and colour.
|
||||
#[derive(Clone)]
|
||||
|
||||
@@ -2,10 +2,7 @@ use super::autonum::autonum;
|
||||
use super::run::{RunEffects, TextShadow};
|
||||
use super::style::{Cell, Opts, ParaShaped, Style, face_chain, make_style};
|
||||
use super::symbol::{covers_symbol, map_symbol_bullet, symbol_font_char, symbol_map_char};
|
||||
use super::{
|
||||
Arc, Bullet, Caps, Ctx, Fill, FontDb, FontFace, FontSlot, HashMap, ParaProps, RunKind, RunProps, ScriptTag, Shape, ShapeParams, TextBody,
|
||||
UnicodeSegmentation, bidi, resolve, script,
|
||||
};
|
||||
use super::*;
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub(crate) fn shape_para(
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
use super::run::{RunEffects, Underline};
|
||||
use super::shape::run_effects;
|
||||
use super::{Arc, Caps, Ctx, Fill, FontDb, FontFace, FontSlot, ParaProps, Rect, Rgba, RunProps, ScriptTag, Spacing, Strike, resolve};
|
||||
use super::*;
|
||||
|
||||
/// Values for fields (`slidenum`, `datetime…`, `footer`). `None` keeps the field's saved text.
|
||||
pub trait Fields {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
use super::{FontFace, UnicodeSegmentation};
|
||||
use super::*;
|
||||
|
||||
/// Is `family` a symbol font (characters at U+F000 + code)?
|
||||
pub(crate) fn is_symbol_font(family: &str) -> bool {
|
||||
|
||||
@@ -19,7 +19,6 @@ deckcraft-anim = { workspace = true }
|
||||
deckcraft-engine = { workspace = true }
|
||||
deckcraft-media = { workspace = true }
|
||||
egui = { workspace = true, features = ["default_fonts", "monochrome_emoji_fonts"] }
|
||||
craft-ui = { workspace = true }
|
||||
serde = { workspace = true }
|
||||
serde_json = { workspace = true }
|
||||
log = { workspace = true }
|
||||
|
||||
@@ -2,13 +2,7 @@ use super::clock::{monotonic_secs, now_ms};
|
||||
use super::ime_hooks::ime_in_ui_pass_key;
|
||||
use super::services::{InboxEvent, Services};
|
||||
use super::state::{FilePlace, FilePurpose, ImeDescriptorKey, ImeDestination, ImeTerminalStamp, LocatedPlace, RECENT_MAX, UiState, ViewMode};
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
use super::updates_ui;
|
||||
use super::{
|
||||
AnimPreview, ControlRequest, FileTarget, HashMap, ImeCancelled, ImeComposition, ImeResetPhase, ImeTerminalPending, InputTarget, Mods,
|
||||
PendingFile, Perf, Receiver, Session, SlideApp, Target, UI_COMMANDS, UiRequest, Value, backstage, canvas, capture, dialogs, doc_window,
|
||||
eyedropper, json, media, menus, notes, panes, preview, qat, ribbon, selpane, show, sorter, status, textures, theme, thumbs, toast, uifonts,
|
||||
};
|
||||
use super::*;
|
||||
|
||||
mod clipboard;
|
||||
mod commands;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
use super::{SlideApp, Value, json};
|
||||
use super::*;
|
||||
|
||||
/// Text put on the system clipboard when the copied objects (a rectangle, a picture, slides…)
|
||||
/// carry no text. egui-winit drops Ctrl+V when the system clipboard is empty, so without it the
|
||||
|
||||
@@ -1,6 +1,4 @@
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
use super::updates_ui;
|
||||
use super::{SlideApp, UI_COMMANDS, Value, ViewMode, backstage, dialogs, doc_window, json, media, qat, theme};
|
||||
use super::*;
|
||||
|
||||
fn grid_options_touched(p: &Value) -> bool {
|
||||
["spacing", "snap", "smartGuides", "gridlines", "guides"].iter().any(|k| p.get(*k).is_some())
|
||||
@@ -268,7 +266,7 @@ impl SlideApp {
|
||||
self.dialog = Some(dialogs::Dialog::new(d));
|
||||
}
|
||||
"app.links" => {
|
||||
use deckcraft_engine::links::{APP_PAGE, APPS, DISCORD, GITHUB, WEBSITE};
|
||||
use deckcraft_engine::links::*;
|
||||
return Ok(json!({"discord": DISCORD, "website": WEBSITE, "apps": APPS, "appPage": APP_PAGE, "github": GITHUB}));
|
||||
}
|
||||
_ => return Err(format!("unknown UI command `{id}`")),
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
use super::{SlideApp, json};
|
||||
use super::*;
|
||||
use crate::control;
|
||||
|
||||
impl SlideApp {
|
||||
|
||||
@@ -1,7 +1,4 @@
|
||||
use super::{
|
||||
FilePlace, FilePurpose, FileTarget, InboxEvent, LocatedPlace, PendingFile, RECENT_MAX, SlideApp, Target, UiRequest, Value, ViewMode, dialogs,
|
||||
json, media,
|
||||
};
|
||||
use super::*;
|
||||
|
||||
fn purpose_of(purpose: &str) -> FilePurpose {
|
||||
match purpose {
|
||||
@@ -449,7 +446,7 @@ impl SlideApp {
|
||||
let owner = if self.show.is_some() { media::Owner::Show } else { media::Owner::Editor };
|
||||
// The command resolved `shape` on the editor's slide, also while a show runs.
|
||||
if let Some((uid, doc, slide)) = self.session.active().map(|d| (d.uid, d.doc.clone(), d.selection.slide)) {
|
||||
self.media.request(uid, &doc, slide, &action, (shape, ms), owner);
|
||||
self.media.request(uid, &doc, slide, &action, shape, ms, owner);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
use super::{SlideApp, json, media, monotonic_secs, theme};
|
||||
use super::*;
|
||||
|
||||
impl SlideApp {
|
||||
/// Style, fonts and zoom setup. Runs in a real UI pass, not from `run_logic`, because
|
||||
|
||||
@@ -1,7 +1,4 @@
|
||||
use super::{
|
||||
ImeCancelled, ImeComposition, ImeDescriptorKey, ImeDestination, ImeResetPhase, ImeTerminalPending, ImeTerminalStamp, SlideApp, Target, Value,
|
||||
ViewMode, ime_in_ui_pass_key,
|
||||
};
|
||||
use super::*;
|
||||
|
||||
impl SlideApp {
|
||||
/// Borrow faces for interface text in scripts the interface fonts lack (#101): the open
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
use super::{InputTarget, SlideApp, show};
|
||||
use super::*;
|
||||
|
||||
impl SlideApp {
|
||||
/// What a queued synthetic event is meant for. The control protocol hands out window
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
use super::clipboard::OBJECTS_MARKER;
|
||||
use super::{Mods, SlideApp, Value, backstage, json, menus};
|
||||
use super::*;
|
||||
|
||||
impl SlideApp {
|
||||
/// The app shortcuts that still make sense on the empty File/start screen: create, open,
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
use super::{AnimPreview, SlideApp, Value, ViewMode, json, media, preview, show};
|
||||
use super::*;
|
||||
|
||||
impl SlideApp {
|
||||
/// Whether ⌘A should act on the slide list: always in the sorter, in Normal view once the
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user