Compare commits

..
Author SHA1 Message Date
Dylan M. Taylor ad4d746131 chore(clippy): enforce coding standards via workspace lints 2026-10-27 01:35:06 +00:00
187 changed files with 6658 additions and 9449 deletions
-24
View File
@@ -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')
+2 -4
View File
@@ -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
-4
View File
@@ -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
View File
@@ -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
View File
@@ -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" }
+2 -2
View File
@@ -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);
-3
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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 -3
View File
@@ -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 -4
View File
@@ -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 -4
View File
@@ -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 {
+39 -48
View File
@@ -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 -4
View File
@@ -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 -4
View File
@@ -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 -4
View File
@@ -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 -1
View File
@@ -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 -3
View File
@@ -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 -1
View File
@@ -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 -4
View File
@@ -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 -4
View File
@@ -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 -4
View File
@@ -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 -4
View File
@@ -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 -5
View File
@@ -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 -1
View File
@@ -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 -1
View File
@@ -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 -1
View File
@@ -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 -1
View File
@@ -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 -4
View File
@@ -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 -1
View File
@@ -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 -4
View File
@@ -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]
+2
View File
@@ -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 -1
View File
@@ -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)] = &[
+1 -4
View File
@@ -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 -4
View File
@@ -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 -4
View File
@@ -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),
+3 -3
View File
@@ -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 -4
View File
@@ -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 -1
View File
@@ -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
View File
@@ -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 -1
View File
@@ -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 -1
View File
@@ -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 -4
View File
@@ -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) {
+11 -92
View File
@@ -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 -1
View File
@@ -1,5 +1,5 @@
use super::catalog::{FillMode, Geometry, Handle, SubPath};
use super::{Arc, BezPath, Point, Rect, Vec2};
use super::*;
// ---------- path builder ----------
+1 -1
View File
@@ -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 -1
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
}
}
+15 -51
View File
@@ -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,
}
}
+14 -14
View File
@@ -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
View File
@@ -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 };
+56 -81
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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> {
+35 -45
View File
@@ -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() };
-7
View File
@@ -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));
-16
View File
@@ -215,14 +215,7 @@ fn assert_deck(q: &Presentation) {
let inherited = resolve::run(&ctx, body, &paras[1], &paras[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]);
+67 -83
View File
@@ -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");
+22 -27
View File
@@ -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 {
+14 -21
View File
@@ -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 =
+21 -25
View File
@@ -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
View File
File diff suppressed because it is too large Load Diff
+23 -79
View File
@@ -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 -1
View File
@@ -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
View File
@@ -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
View File
@@ -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 {
+3 -2
View File
@@ -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],
+8 -8
View File
@@ -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]);
+5 -7
View File
@@ -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.
+1 -1
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
File diff suppressed because it is too large Load Diff
+1 -1
View File
@@ -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)]
+1 -4
View File
@@ -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 -1
View File
@@ -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 -1
View File
@@ -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 {
-1
View File
@@ -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 }
+1 -7
View File
@@ -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 -1
View File
@@ -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
+2 -4
View File
@@ -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 -1
View File
@@ -1,4 +1,4 @@
use super::{SlideApp, json};
use super::*;
use crate::control;
impl SlideApp {
+2 -5
View File
@@ -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 -1
View File
@@ -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 -4
View File
@@ -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 -1
View File
@@ -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 -1
View File
@@ -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 -1
View File
@@ -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