mirror of
https://github.com/storytold/deckcraft.git
synced 2026-11-14 01:42:18 -06:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d8c1747d6d | ||
|
|
dfec3c4d81 |
@@ -1,31 +0,0 @@
|
||||
# This file is machine-generated by `gh actions-lock`.
|
||||
# Do not edit by hand; run `gh actions-lock` to update.
|
||||
# Docs: https://gh.io/actions-lockfile
|
||||
version: 'v0.0.3'
|
||||
workflows:
|
||||
'.github/workflows/ci.yml':
|
||||
- 'actions/checkout@v7.0.1'
|
||||
- 'step-security/harden-runner@v2.22.4'
|
||||
'.github/workflows/freebsd.yml': []
|
||||
'.github/workflows/house-rules.yml': []
|
||||
'.github/workflows/lockfile.yml':
|
||||
- 'actions/checkout@v7.0.1'
|
||||
- 'step-security/harden-runner@v2.22.4'
|
||||
'.github/workflows/nix.yml': []
|
||||
'.github/workflows/packaging-lint.yml':
|
||||
- 'actions/checkout@v7.0.1'
|
||||
- 'step-security/harden-runner@v2.22.4'
|
||||
'.github/workflows/release.yml': []
|
||||
'.github/workflows/sonarcloud.yml': []
|
||||
'.github/workflows/windows-arm64.yml': []
|
||||
dependencies:
|
||||
'actions/checkout@v7.0.1':
|
||||
ref: 'v7.0.1'
|
||||
commit: 'sha1-3d3c42e5aac5ba805825da76410c181273ba90b1'
|
||||
owner_id: 44036562
|
||||
repo_id: 197814629
|
||||
'step-security/harden-runner@v2.22.4':
|
||||
ref: 'v2.22.4'
|
||||
commit: 'sha1-1d88384fd173af6ecb012dfd0264a306b711241a'
|
||||
owner_id: 88700172
|
||||
repo_id: 422287306
|
||||
@@ -26,12 +26,8 @@ jobs:
|
||||
# Same craft-fonts commit as release.yml.
|
||||
CRAFT_FONTS_REF: 37827adc41fd165f9a9234ac40f94dbad353aa69
|
||||
steps:
|
||||
- name: Harden runner
|
||||
uses: step-security/harden-runner@v2.22.4
|
||||
with:
|
||||
egress-policy: audit
|
||||
- name: Check out craft-fonts
|
||||
uses: actions/checkout@v7.0.1
|
||||
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
with:
|
||||
repository: storytold/craft-fonts
|
||||
ref: ${{ env.CRAFT_FONTS_REF }}
|
||||
|
||||
@@ -1,28 +0,0 @@
|
||||
name: House rules
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [main]
|
||||
paths:
|
||||
- AGENTS.md
|
||||
- CLAUDE.md
|
||||
- .github/workflows/house-rules.yml
|
||||
pull_request:
|
||||
paths:
|
||||
- AGENTS.md
|
||||
- CLAUDE.md
|
||||
- .github/workflows/house-rules.yml
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
concurrency:
|
||||
group: house-rules-${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
house-rules:
|
||||
uses: storytold/craft-common-actions/.github/workflows/house-rules.yml@main
|
||||
with:
|
||||
rust: true
|
||||
@@ -1,34 +0,0 @@
|
||||
name: Lockfile
|
||||
|
||||
# Fails when .github/workflows/actions.lock is out of sync with the workflows, or when a locked pin
|
||||
# no longer verifies upstream, so a moved tag cannot slip a different commit into a run.
|
||||
# Docs: https://gh.io/actions-lockfile
|
||||
on:
|
||||
pull_request:
|
||||
push:
|
||||
branches: [main]
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
env:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
|
||||
jobs:
|
||||
lockfile:
|
||||
name: Lockfile is in sync
|
||||
runs-on: ubuntu-24.04
|
||||
timeout-minutes: 10
|
||||
steps:
|
||||
- name: Harden runner
|
||||
uses: step-security/harden-runner@v2.22.4
|
||||
with:
|
||||
egress-policy: audit
|
||||
- uses: actions/checkout@v7.0.1
|
||||
with:
|
||||
persist-credentials: false
|
||||
- name: Install gh-actions-lock
|
||||
run: gh extension install github/gh-actions-lock --pin v0.4.1
|
||||
- name: Verify the lockfile
|
||||
run: gh actions-lock --verify
|
||||
@@ -27,11 +27,7 @@ jobs:
|
||||
name: Windows installation policy (no app build)
|
||||
runs-on: windows-latest
|
||||
steps:
|
||||
- name: Harden runner
|
||||
uses: step-security/harden-runner@v2.22.4
|
||||
with:
|
||||
egress-policy: audit
|
||||
- uses: actions/checkout@v7.0.1
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
with:
|
||||
persist-credentials: false
|
||||
- name: Install the pinned packaging toolchain
|
||||
|
||||
@@ -1,10 +1,6 @@
|
||||
name: Release
|
||||
|
||||
on:
|
||||
# A nightly dry run on main: every release build except macOS, nothing signed or released
|
||||
# (craft-common-actions release.yml `dry-run`). Finds release-only breakage before release day.
|
||||
schedule:
|
||||
- cron: "16 1 * * *"
|
||||
push:
|
||||
branches: [release]
|
||||
workflow_dispatch:
|
||||
@@ -16,25 +12,16 @@ on:
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
packages: write
|
||||
|
||||
concurrency:
|
||||
group: release-${{ github.ref }}
|
||||
cancel-in-progress: false
|
||||
|
||||
jobs:
|
||||
# Regenerates contributors/contributors.json for the release commit; every build bundles it.
|
||||
credits:
|
||||
permissions:
|
||||
contents: read
|
||||
pull-requests: read
|
||||
uses: storytold/craft-common-actions/.github/workflows/refresh-contributors.yml@main
|
||||
release:
|
||||
needs: credits
|
||||
# Intentionally tracked at @main (not SHA-pinned) so org-wide CI improvements, security fixes, and build optimizations propagate without PR churn across all craftapp repos.
|
||||
uses: storytold/craft-common-actions/.github/workflows/release.yml@main
|
||||
with:
|
||||
dry-run: ${{ github.event_name == 'schedule' }}
|
||||
app-name: deckcraft
|
||||
app-title: DeckCraft
|
||||
# Keep identical to ci.yml and freebsd.yml. The shared release workflow sets
|
||||
@@ -42,7 +29,6 @@ jobs:
|
||||
# manifest fails the release instead of silently skipping it.
|
||||
craft-fonts-ref: 37827adc41fd165f9a9234ac40f94dbad353aa69
|
||||
version: ${{ inputs.version }}
|
||||
contributors-artifact: ${{ needs.credits.outputs.artifact-name }}
|
||||
secrets:
|
||||
APPLE_CERTIFICATE: ${{ secrets.APPLE_CERTIFICATE }}
|
||||
APPLE_CERTIFICATE_PASSWORD: ${{ secrets.APPLE_CERTIFICATE_PASSWORD }}
|
||||
@@ -58,4 +44,3 @@ jobs:
|
||||
AZURE_SIGNING_ENDPOINT: ${{ secrets.AZURE_SIGNING_ENDPOINT }}
|
||||
AZURE_SIGNING_ACCOUNT: ${{ secrets.AZURE_SIGNING_ACCOUNT }}
|
||||
AZURE_CERT_PROFILE: ${{ secrets.AZURE_CERT_PROFILE }}
|
||||
LINUX_SIGNING_KEY: ${{ secrets.LINUX_SIGNING_KEY }}
|
||||
|
||||
@@ -1,56 +1,3 @@
|
||||
<!-- craft-house-rules:start core source=dde09ea31e2e457f5b4653a6e76f7cdf55944bdf sha256=540e6cea75d12d0710c3dca89658328e81e508ecef102446ca5bb95b7c0b92bb -->
|
||||
## House rules
|
||||
|
||||
These apply in every Craft repository. A later rule may be stricter, not weaker. The source is `standards/house-rules-core.md` in `storytold/craft-contrib`. Propose changes there, not in a copy.
|
||||
|
||||
1. **A person is accountable.** Whoever runs an agent answers for what it opens and for its review comments.
|
||||
2. **Check, then claim.** Before writing code, read the issue comments, search pull requests for `#N` and the keywords, and check recent commits on `main`. Comment on the issue first. Release a claim you will not finish.
|
||||
3. **Verify, don't guess.** Open a definition before using it. Confirm the cause in the code, even when someone names a location.
|
||||
4. **Keep fixes narrow and tested.** Fix what the issue describes, where the bug is. Add a test that fails without the fix.
|
||||
5. **Review the diff before opening a pull request.** Identifiers exist as used, the test fails without the fix, nothing unrelated changed, and the description is true.
|
||||
6. **Look for a better pull request.** If another fixes the same thing, close yours when it is better or equal, and say why.
|
||||
7. **Report only what ran.** Say what you tested and what you could not run. Without a local build or test, say so and rely on CI before marking ready.
|
||||
8. **`main` changes through a pull request.** Do not push to it. Branch protection enforces this. There is no exception.
|
||||
9. **Stop and ask only about** licensing outside the allowlist, publishing, releases, a new remote, secrets, spending money, destructive git, and anything else hard to undo. Otherwise keep working. `plan/` is a gitignored notebook, not rules. Without it, do not ask for it and do not stop.
|
||||
10. **Keep secrets and personal details out** of the repository, logs and requests.
|
||||
11. **CI stays shared.** Workflows call `storytold/craft-common-actions`. Do not change CI in a feature or fix pull request. If CI cannot cover a change, open an issue there.
|
||||
12. **Guards say why.** A check that looks removable says what it prevents. Read that before changing it.
|
||||
13. **Instructions must be readable by everyone.** No notes about one machine, and no links to files a reader cannot open. `craftrules` is retired. Do not link to it.
|
||||
14. **Keep pull requests moving.** Concurrent pull requests are fine; unanswered actionable feedback is not. Respond in 7 days, a reminder at 14, closure eligible at 30, from when an actionable request waits on the author. Suggestions and unconfirmed bot findings do not start the clock. A fix, explanation or agreed plan resets it; unrelated activity does not. Waiting on a reviewer or maintainer pauses it and does not count against the author. A maintainer decides closure; it is never automatic. Reuse search results, do not poll, and stop at a rate limit.
|
||||
15. **No agent attribution.** A pull request does not say an agent wrote it. A repository rule may not require that.
|
||||
<!-- craft-house-rules:end -->
|
||||
<!-- craft-house-rules:start rust source=dde09ea31e2e457f5b4653a6e76f7cdf55944bdf sha256=387e0ccb4b49e567f60951d7d6fff7305054c89f43ad812b9993dc7c38528013 -->
|
||||
## Rust Craft repositories
|
||||
|
||||
These apply to native Craft apps and to shared Rust libraries. Select them by repository type. A repository that merely contains some Rust does not take this block. Rules later in a repository's file may be stricter, but not weaker. The source is `standards/house-rules-rust.md` in [storytold/craft-contrib](https://github.com/storytold/craft-contrib).
|
||||
|
||||
1. **Never crash.** Non-test code does not panic and has no `unsafe`, except where the repository file names an exception. Return errors, treat input as hostile, and bound recursion and allocations. New production crate roots copy the `deny` attribute from an existing crate: `clippy::unwrap_used`, `clippy::expect_used`, `clippy::panic`, `clippy::unimplemented`, `clippy::todo`, `clippy::unreachable`. Every crash fix comes with a test that panicked before the fix.
|
||||
2. **Clean room.** Learn from observed behaviour and public specifications. Do not copy another product's code, assets, strings or artwork. Check a dependency's licence before adding it.
|
||||
3. **Assets and fonts.** Every asset is original or suitably licensed, and is recorded in `ATTRIBUTION.md` in the same change. Font files live in [storytold/craft-fonts](https://github.com/storytold/craft-fonts), not in the app repository.
|
||||
4. **Rust only for native Craft apps.** No handwritten JavaScript or TypeScript, and no Tauri, Electron or webview shell, in those apps. Shared tooling follows its documented technical scope and maintainer-approved exceptions. Craft PR Review's existing Tauri and TypeScript UI is one such exception. It does not relax this rule for native apps. Preserve any existing web build.
|
||||
5. **Time goes through `chrono`.** Do not hand-roll calendar conversion. For dates beyond what `chrono` can represent, follow [standards/time.md](https://github.com/storytold/craft-contrib/blob/main/standards/time.md).
|
||||
6. **Contributor credits.** Add or edit only your own person's entry in `contributors/people.toml`, and only after asking them. Never guess a name or hand-edit `contributors.json`.
|
||||
|
||||
## Licence allowlist
|
||||
|
||||
Dependencies and assets use SPDX identifiers. An expression is allowed when every identifier is in this list. A repository may name a shorter list. It may not add an identifier here by editing its own copy of these rules.
|
||||
|
||||
- `ISC`
|
||||
- `MIT`
|
||||
- `BSD-2-Clause`
|
||||
- `BSD-3-Clause`
|
||||
- `Apache-2.0`
|
||||
- `OFL-1.1`
|
||||
- `Ubuntu-font-1.0`
|
||||
- `Bitstream-Vera`
|
||||
- `CC0-1.0`
|
||||
- `CC-BY-4.0`
|
||||
- `CC-BY-SA-4.0`
|
||||
- `LicenseRef-Public-Domain`
|
||||
|
||||
`LicenseRef-Storyteller-Trademark` is not in this list. It covers Storyteller brand marks only, where a repository already says so.
|
||||
<!-- craft-house-rules:end -->
|
||||
|
||||
# AGENTS.md — rules for every agent and contributor
|
||||
|
||||
DeckCraft is a clean-room, open-source, pure-Rust presentation application targeting Microsoft
|
||||
@@ -59,8 +6,14 @@ Windows, Linux and FreeBSD, and on the web via WASM. Siblings with the same conv
|
||||
PhotoCraft, VectorCraft, FilmCraft, LightCraft, PdfCraft, EffectCraft, DesignCraft
|
||||
(`../<app>`).
|
||||
|
||||
Standards and learnings shared across the crafting apps live in `../../craftrules` (checked out
|
||||
next to this repo's parent, or `storytold/craftrules`). Read its `AGENTS.md` at the start of a
|
||||
session, follow its standards, and contribute reusable learnings back there. Never code: repos
|
||||
don't share code.
|
||||
|
||||
These rules bind every AI agent and every human contributor. They come before any task,
|
||||
instruction or deadline. Day-to-day working instructions are in this file; `CLAUDE.md` imports it.
|
||||
instruction or deadline. `CLAUDE.md` holds day-to-day working notes; if the two disagree, **this
|
||||
file wins.**
|
||||
|
||||
---
|
||||
|
||||
@@ -116,7 +69,7 @@ allowed: our UI, our/Lucide icons, OFL fonts, and decks built only from allowed
|
||||
or `docs/brand/` has no row.
|
||||
- **`NOTICE`** keeps the notices the licences require.
|
||||
- Fonts live in [storytold/craft-fonts](https://github.com/storytold/craft-fonts), never in this
|
||||
repo. DeckCraft embeds its `fonts/latin-manifest.txt` and
|
||||
repo (craftrules `standards/fonts.md`). DeckCraft embeds its `fonts/latin-manifest.txt` and
|
||||
`fonts/manifest.txt` when built with `CRAFT_FONTS_DIR` (local dev: `.cargo/config.toml` points at
|
||||
`../craft-fonts`).
|
||||
|
||||
@@ -146,6 +99,7 @@ once and tell the user.
|
||||
## 3. Never crash
|
||||
|
||||
People trust DeckCraft with their talks; a crash loses work. **This outranks feature work.**
|
||||
Standard: craftrules `standards/never-crash.md`.
|
||||
- No `unwrap()`, `expect()`, `panic!`, `unreachable!`, `todo!`, `unimplemented!` in non-test
|
||||
code; no `unsafe` (`unsafe_code = "forbid"`). Every production crate root carries
|
||||
`#![deny(clippy::unwrap_used, clippy::expect_used, clippy::panic, clippy::unimplemented, clippy::todo, clippy::unreachable)]`.
|
||||
@@ -177,14 +131,14 @@ People trust DeckCraft with their talks; a crash loses work. **This outranks fea
|
||||
CLI, control channel and MCP do exactly the same thing.
|
||||
- **The UI is thin**: panels read engine state and act through commands. Colours come from theme
|
||||
tokens. **Never use Tauri**; egui only.
|
||||
- **Time goes through `chrono`** (workspace dependency). Never hand-roll UTC/calendar conversion (days-to-date algorithms, leap-year math); a crate owns only its output format string. The one exception is range extension: past the years chrono can represent, shift the date by whole 400-year Gregorian cycles (146,097 days, after which the calendar repeats), let chrono compute the month and day, and add the cycles back to the year (splitting seconds into hours, minutes and seconds is plain arithmetic).
|
||||
- **Never break wasm** (`cargo xtask wasm`).
|
||||
|
||||
## 5. Quality gates (before every commit)
|
||||
|
||||
`cargo xtask ci` = fmt check, clippy `-D warnings`, tests (nextest when installed) and doctests, layering check, asset check, wasm
|
||||
check. Commit after each landed arc with a task id in the message (`M1.2: smart guides`).
|
||||
New workspace crates include `workspace = true` under `[lints]` in `Cargo.toml`. Production crate roots
|
||||
declare the crate-level lint attributes before modules; crate-level `//!` documentation may precede them.
|
||||
check. Commit after each landed arc with a task id in the message (`M1.2: smart guides`) and push
|
||||
to `main`.
|
||||
|
||||
## 6. Running and looking at the app
|
||||
|
||||
@@ -193,19 +147,20 @@ declare the crate-level lint attributes before modules; crate-level `//!` docume
|
||||
`{"id":1,"method":"engine.execute","params":{"command":"shape.insert","params":{"preset":"star5","rect":[100,100,200,200]}}}`
|
||||
then `{"id":2,"method":"ui.screenshot","params":{"path":"/tmp/shot.png"}}`. Methods:
|
||||
`crates/ui-egui/src/control.rs`, docs: `docs/control-protocol.md`.
|
||||
- **For UI work, look at the result** (screenshot, read the PNG). If available, compare with the observations
|
||||
in `plan/powerpoint/01-observed-ui.md`. Otherwise verify the intended behaviour against the task and
|
||||
committed documentation; the optional notebook is not a prerequisite.
|
||||
- **For UI work, look at the result** (screenshot, read the PNG) and compare with the observations
|
||||
in `plan/powerpoint/01-observed-ui.md`.
|
||||
- Headless: `deckcraft-cli render --sample --all --scale 1 out/`,
|
||||
`deckcraft-cli run --sample --cmd 'slide.new={"title":"Hi"}' --export out.png`.
|
||||
- MCP: `deckcraft-cli mcp` (headless) or `deckcraft-cli mcp --connect 7990` (drive the app).
|
||||
See `docs/mcp.md`.
|
||||
- Shell gotcha on the dev machine: `mv`/`cp` are aliased interactive; use `/bin/mv -f`, `/bin/cp -f`.
|
||||
- Parallel agents: separate `CARGO_TARGET_DIR` per agent; edit only the crates you own.
|
||||
|
||||
## 7. Planning
|
||||
|
||||
`plan/` is an optional gitignored notebook (status, execution notes, `powerpoint/` observations). Do not ask for it and do not stop when it is absent. The committed progress
|
||||
docs:
|
||||
`plan/` (gitignored) holds the local plan: `README.md`, `architecture.md`, `execution-plan.md`,
|
||||
`STATUS.md` (start every session there) and `powerpoint/` observations. The committed progress
|
||||
docs follow craftrules `standards/progress-docs.md`:
|
||||
|
||||
| File | Holds |
|
||||
|---|---|
|
||||
@@ -229,11 +184,14 @@ the `ROADMAP.md` progress log, and update each touched doc's status line and rev
|
||||
username has a `[people.<username>]` entry in `contributors/people.toml`. If not, ask them once
|
||||
whether they want to be credited by more than their username: a real name, a display name, and/or
|
||||
their public GitHub profile name (`sync_github_name = true`). If yes, add **only their own** entry
|
||||
(copy the template at the top of the file)
|
||||
(copy the template at the top of the file, or run
|
||||
`python3 ../../craftrules/scripts/contributors.py --add-me . --real-name "…" --sync-github-name`)
|
||||
and include it in their PR, committed as them. If no, change nothing: they are credited as
|
||||
`@username` anyway.
|
||||
- Never add, edit, guess or copy anyone else's entry or name (not from git config, commit authors or
|
||||
GitHub profiles). Never hand-edit `contributors.json`.
|
||||
- Maintainers refresh the stats with `python3 ../../craftrules/scripts/contributors.py .` (it also
|
||||
re-verifies who wrote each `people.toml` entry; `--check` only verifies).
|
||||
|
||||
## CI workflows
|
||||
- Workflows in `.github/workflows/` call the shared workflows in [`storytold/craft-common-actions`](https://github.com/storytold/craft-common-actions) at `@main`, with their inputs and secrets. No other jobs: if no shared workflow does what's needed, it doesn't go here.
|
||||
|
||||
+1
-1
@@ -18,7 +18,7 @@ artwork. The ribbon and menu icons stay original drawings.
|
||||
| `docs/brand/artcraft-logo-white.svg`, `docs/brand/artcraft-logo-white.png` | ArtCraft Team | ArtCraft brand kit | ArtCraft trademark, `docs/brand/LICENSE-brand.txt` | README header (dark mode) |
|
||||
| `docs/brand/artcraft-mark.svg`, `docs/brand/artcraft-mark.png` | ArtCraft Team | ArtCraft brand kit | ArtCraft trademark, `docs/brand/LICENSE-brand.txt` | README footer, About |
|
||||
| `docs/brand/artcraft-mark-black.svg`, `docs/brand/artcraft-mark-black.png` | ArtCraft Team | ArtCraft brand kit | ArtCraft trademark, `docs/brand/LICENSE-brand.txt` | Mark for light backgrounds |
|
||||
| `docs/brand/LICENSE-brand.txt` | ArtCraft Team | ArtCraft brand kit | — (licence text) | Terms for the ArtCraft marks |
|
||||
| `docs/brand/LICENSE-brand.txt` | ArtCraft Team | craftrules `standards/license-files` | — (licence text) | Terms for the ArtCraft marks |
|
||||
| `assets/app-icon/deckcraft-1024.png`, `assets/app-icon/hicolor/*/apps/ai.storyteller.deckcraft.png` | DeckCraft contributors | rendered by DeckCraft from `assets/app-icon/icon-source.deckcraft` (shapes only) | MIT OR Apache-2.0 | Placeholder app icon (presentation board) until the mascot icon is drawn |
|
||||
| `packaging/macos/dmg/background.svg` | @XusBadia | DeckCraft macOS DMG window background, original work for this repository (2026-10-08), derived from the DeckCraft app icon `assets/app-icon/deckcraft-1024.png` (referenced via `<image>`, not copied); text is outlined Inter / JetBrains Mono glyph paths | MIT OR Apache-2.0 (`LICENSE-MIT`, `LICENSE-APACHE`) | Finder window of the macOS DMG; `packaging/macos/dmg/generate.py` renders it to `background.tiff` |
|
||||
| `packaging/macos/dmg/background.tiff` | @XusBadia | Rendered from `packaging/macos/dmg/background.svg` by `packaging/macos/dmg/generate.py` | MIT OR Apache-2.0 (`LICENSE-MIT`, `LICENSE-APACHE`) | 1x + 2x HiDPI TIFF for the macOS DMG window; includes the app icon (see its row) and LittleCMS's built-in sRGB profile ("No copyright, use freely") |
|
||||
|
||||
@@ -1 +1,26 @@
|
||||
@AGENTS.md
|
||||
# DeckCraft — working notes for Claude
|
||||
|
||||
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.
|
||||
3. Run `cargo xtask ci` before you change anything, so you know the tree is green.
|
||||
|
||||
## Habits
|
||||
- Every user-visible action is a command with tests (`crates/engine/src/cmd/*`).
|
||||
- Look at UI changes: `deckcraft --sample --control 7990` + `ui.screenshot`, or
|
||||
`deckcraft-cli render`.
|
||||
- Commit after each landed arc (`M<n>.<k>: what`) and push to `origin main`.
|
||||
- Keep `ROADMAP.md`, the progress docs in `docs/` (AGENTS.md §7) and `plan/STATUS.md` current.
|
||||
- Don't ask the user questions; record genuinely-theirs decisions in `plan/STATUS.md`.
|
||||
|
||||
## CI workflows
|
||||
- Workflows in `.github/workflows/` call the shared workflows in [`storytold/craft-common-actions`](https://github.com/storytold/craft-common-actions) at `@main`, with their inputs and secrets. No other jobs: if no shared workflow does what's needed, it doesn't go here.
|
||||
- Don't add or change CI in a feature or fix PR. If the existing workflows can't build or test a change, link an existing craft-common-actions issue or PR, or open a new issue, or a PR only for a small fix that is safe for every app and does not increase CI time.
|
||||
- Some workflow files here still run their own jobs. Don't add to them. Moving a job into craft-common-actions is its own task, never part of another PR, and the job stays here until a shared workflow runs it.
|
||||
|
||||
Generated
+111
-203
@@ -565,42 +565,18 @@ dependencies = [
|
||||
"thiserror 1.0.69",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "calloop"
|
||||
version = "0.14.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "43b32313e937c8721c9d17ee9bf60699a6402b4b7c2cb6ac1bee0ffcaa4b4e5e"
|
||||
dependencies = [
|
||||
"bitflags 2.13.2",
|
||||
"polling",
|
||||
"rustix 1.1.5",
|
||||
"tracing",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "calloop-wayland-source"
|
||||
version = "0.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "95a66a987056935f7efce4ab5668920b5d0dac4a7c99991a67395f13702ddd20"
|
||||
dependencies = [
|
||||
"calloop 0.13.0",
|
||||
"calloop",
|
||||
"rustix 0.38.44",
|
||||
"wayland-backend",
|
||||
"wayland-client",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "calloop-wayland-source"
|
||||
version = "0.4.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "138efcf0940a02ebf0cc8d1eff41a1682a46b431630f4c52450d6265876021fa"
|
||||
dependencies = [
|
||||
"calloop 0.14.5",
|
||||
"rustix 1.1.5",
|
||||
"wayland-backend",
|
||||
"wayland-client",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "cast"
|
||||
version = "0.3.0"
|
||||
@@ -844,24 +820,15 @@ dependencies = [
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "craft-audio"
|
||||
version = "0.1.0"
|
||||
source = "git+https://github.com/storytold/craft-libs?rev=09dcbc225b95232998498a40ca56e24716571d53#09dcbc225b95232998498a40ca56e24716571d53"
|
||||
dependencies = [
|
||||
"craft-media",
|
||||
"thiserror 2.0.21",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "craft-fonts-build"
|
||||
version = "0.1.0"
|
||||
source = "git+https://github.com/storytold/craft-libs?rev=6e03f65bf45181db77ba6229716e6624db9eb25b#6e03f65bf45181db77ba6229716e6624db9eb25b"
|
||||
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=6e03f65bf45181db77ba6229716e6624db9eb25b#6e03f65bf45181db77ba6229716e6624db9eb25b"
|
||||
source = "git+https://github.com/storytold/craft-libs?rev=92d392cff132eaf53e13afff77f38b5c11b73aa0#92d392cff132eaf53e13afff77f38b5c11b73aa0"
|
||||
dependencies = [
|
||||
"fixed_decimal",
|
||||
"icu_decimal",
|
||||
@@ -884,7 +851,7 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "craft-media"
|
||||
version = "0.1.0"
|
||||
source = "git+https://github.com/storytold/craft-libs?rev=09dcbc225b95232998498a40ca56e24716571d53#09dcbc225b95232998498a40ca56e24716571d53"
|
||||
source = "git+https://github.com/storytold/craft-libs?rev=92d392cff132eaf53e13afff77f38b5c11b73aa0#92d392cff132eaf53e13afff77f38b5c11b73aa0"
|
||||
dependencies = [
|
||||
"thiserror 2.0.21",
|
||||
]
|
||||
@@ -892,7 +859,7 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "craft-pdf"
|
||||
version = "0.1.0"
|
||||
source = "git+https://github.com/storytold/craft-libs?rev=6e03f65bf45181db77ba6229716e6624db9eb25b#6e03f65bf45181db77ba6229716e6624db9eb25b"
|
||||
source = "git+https://github.com/storytold/craft-libs?rev=92d392cff132eaf53e13afff77f38b5c11b73aa0#92d392cff132eaf53e13afff77f38b5c11b73aa0"
|
||||
dependencies = [
|
||||
"krilla",
|
||||
"thiserror 2.0.21",
|
||||
@@ -901,7 +868,7 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "craft-ui"
|
||||
version = "0.1.0"
|
||||
source = "git+https://github.com/storytold/craft-libs?rev=6e03f65bf45181db77ba6229716e6624db9eb25b#6e03f65bf45181db77ba6229716e6624db9eb25b"
|
||||
source = "git+https://github.com/storytold/craft-libs?rev=92d392cff132eaf53e13afff77f38b5c11b73aa0#92d392cff132eaf53e13afff77f38b5c11b73aa0"
|
||||
dependencies = [
|
||||
"craft-fonts-build",
|
||||
"craft-i18n",
|
||||
@@ -921,7 +888,7 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "craft-update"
|
||||
version = "0.1.0"
|
||||
source = "git+https://github.com/storytold/craft-libs?rev=6e03f65bf45181db77ba6229716e6624db9eb25b#6e03f65bf45181db77ba6229716e6624db9eb25b"
|
||||
source = "git+https://github.com/storytold/craft-libs?rev=04aa1703b32d299e54f78fcde4182f2b393a9116#04aa1703b32d299e54f78fcde4182f2b393a9116"
|
||||
dependencies = [
|
||||
"chrono",
|
||||
"dunce",
|
||||
@@ -931,7 +898,6 @@ dependencies = [
|
||||
"image",
|
||||
"log",
|
||||
"plist",
|
||||
"rustix 1.1.5",
|
||||
"semver",
|
||||
"serde",
|
||||
"serde_json",
|
||||
@@ -945,36 +911,6 @@ dependencies = [
|
||||
"zip 9.1.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "craft-video"
|
||||
version = "0.1.0"
|
||||
source = "git+https://github.com/storytold/craft-libs?rev=09dcbc225b95232998498a40ca56e24716571d53#09dcbc225b95232998498a40ca56e24716571d53"
|
||||
dependencies = [
|
||||
"craft-media",
|
||||
"rayon",
|
||||
"thiserror 2.0.21",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "craft-web-api"
|
||||
version = "0.1.0"
|
||||
source = "git+https://github.com/storytold/craft-libs?rev=fd5e2feff6ea25bed371a99cc732b3968d621505#fd5e2feff6ea25bed371a99cc732b3968d621505"
|
||||
dependencies = [
|
||||
"js-sys",
|
||||
"serde_json",
|
||||
"wasm-bindgen",
|
||||
"wasm-bindgen-futures",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "craft-web-host"
|
||||
version = "0.1.0"
|
||||
source = "git+https://github.com/storytold/craft-libs?rev=fd5e2feff6ea25bed371a99cc732b3968d621505#fd5e2feff6ea25bed371a99cc732b3968d621505"
|
||||
dependencies = [
|
||||
"wasm-bindgen",
|
||||
"web-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "crc32fast"
|
||||
version = "1.5.2"
|
||||
@@ -1048,7 +984,7 @@ checksum = "0c87e182de0887fd5361989c677c4e8f5000cd9491d6d563161a8f3a5519fc7f"
|
||||
|
||||
[[package]]
|
||||
name = "deckcraft"
|
||||
version = "0.18.1"
|
||||
version = "0.12.0"
|
||||
dependencies = [
|
||||
"arboard",
|
||||
"chrono",
|
||||
@@ -1070,16 +1006,32 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "deckcraft-anim"
|
||||
version = "0.18.1"
|
||||
version = "0.12.0"
|
||||
dependencies = [
|
||||
"deckcraft-geom",
|
||||
"deckcraft-model",
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "deckcraft-av1"
|
||||
version = "0.12.0"
|
||||
dependencies = [
|
||||
"deckcraft-bitstream",
|
||||
"rayon",
|
||||
"thiserror 2.0.21",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "deckcraft-bitstream"
|
||||
version = "0.12.0"
|
||||
dependencies = [
|
||||
"craft-media",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "deckcraft-cli"
|
||||
version = "0.18.1"
|
||||
version = "0.12.0"
|
||||
dependencies = [
|
||||
"deckcraft-engine",
|
||||
"deckcraft-mcp",
|
||||
@@ -1089,14 +1041,14 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "deckcraft-color"
|
||||
version = "0.18.1"
|
||||
version = "0.12.0"
|
||||
dependencies = [
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "deckcraft-engine"
|
||||
version = "0.18.1"
|
||||
version = "0.12.0"
|
||||
dependencies = [
|
||||
"chrono",
|
||||
"deckcraft-anim",
|
||||
@@ -1122,7 +1074,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "deckcraft-fonts"
|
||||
version = "0.18.1"
|
||||
version = "0.12.0"
|
||||
dependencies = [
|
||||
"epaint_default_fonts",
|
||||
"harfrust 0.13.3",
|
||||
@@ -1134,7 +1086,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "deckcraft-format"
|
||||
version = "0.18.1"
|
||||
version = "0.12.0"
|
||||
dependencies = [
|
||||
"deckcraft-model",
|
||||
"serde",
|
||||
@@ -1145,15 +1097,47 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "deckcraft-geom"
|
||||
version = "0.18.1"
|
||||
version = "0.12.0"
|
||||
dependencies = [
|
||||
"kurbo",
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "deckcraft-h264"
|
||||
version = "0.12.0"
|
||||
dependencies = [
|
||||
"deckcraft-bitstream",
|
||||
"rayon",
|
||||
"thiserror 2.0.21",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "deckcraft-hevc"
|
||||
version = "0.12.0"
|
||||
dependencies = [
|
||||
"deckcraft-bitstream",
|
||||
"rayon",
|
||||
"thiserror 2.0.21",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "deckcraft-isobmff"
|
||||
version = "0.12.0"
|
||||
dependencies = [
|
||||
"craft-media",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "deckcraft-matroska"
|
||||
version = "0.12.0"
|
||||
dependencies = [
|
||||
"craft-media",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "deckcraft-mcp"
|
||||
version = "0.18.1"
|
||||
version = "0.12.0"
|
||||
dependencies = [
|
||||
"deckcraft-engine",
|
||||
"deckcraft-render",
|
||||
@@ -1163,11 +1147,16 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "deckcraft-media"
|
||||
version = "0.18.1"
|
||||
version = "0.12.0"
|
||||
dependencies = [
|
||||
"craft-audio",
|
||||
"craft-media",
|
||||
"craft-video",
|
||||
"deckcraft-av1",
|
||||
"deckcraft-h264",
|
||||
"deckcraft-hevc",
|
||||
"deckcraft-isobmff",
|
||||
"deckcraft-matroska",
|
||||
"deckcraft-ogg",
|
||||
"deckcraft-opus",
|
||||
"deckcraft-vp9",
|
||||
"image",
|
||||
"log",
|
||||
"symphonia",
|
||||
@@ -1176,7 +1165,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "deckcraft-metafile"
|
||||
version = "0.18.1"
|
||||
version = "0.12.0"
|
||||
dependencies = [
|
||||
"image",
|
||||
"kurbo",
|
||||
@@ -1185,7 +1174,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "deckcraft-model"
|
||||
version = "0.18.1"
|
||||
version = "0.12.0"
|
||||
dependencies = [
|
||||
"deckcraft-color",
|
||||
"deckcraft-geom",
|
||||
@@ -1195,9 +1184,17 @@ dependencies = [
|
||||
"unicode-segmentation",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "deckcraft-ogg"
|
||||
version = "0.12.0"
|
||||
|
||||
[[package]]
|
||||
name = "deckcraft-opus"
|
||||
version = "0.12.0"
|
||||
|
||||
[[package]]
|
||||
name = "deckcraft-pdf"
|
||||
version = "0.18.1"
|
||||
version = "0.12.0"
|
||||
dependencies = [
|
||||
"craft-pdf",
|
||||
"deckcraft-fonts",
|
||||
@@ -1215,7 +1212,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "deckcraft-pptx"
|
||||
version = "0.18.1"
|
||||
version = "0.12.0"
|
||||
dependencies = [
|
||||
"deckcraft-color",
|
||||
"deckcraft-engine",
|
||||
@@ -1231,7 +1228,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "deckcraft-render"
|
||||
version = "0.18.1"
|
||||
version = "0.12.0"
|
||||
dependencies = [
|
||||
"deckcraft-anim",
|
||||
"deckcraft-color",
|
||||
@@ -1250,7 +1247,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "deckcraft-text"
|
||||
version = "0.18.1"
|
||||
version = "0.12.0"
|
||||
dependencies = [
|
||||
"chrono",
|
||||
"deckcraft-color",
|
||||
@@ -1264,7 +1261,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "deckcraft-ui-egui"
|
||||
version = "0.18.1"
|
||||
version = "0.12.0"
|
||||
dependencies = [
|
||||
"craft-ui",
|
||||
"craft-update",
|
||||
@@ -1278,7 +1275,6 @@ dependencies = [
|
||||
"deckcraft-render",
|
||||
"deckcraft-text",
|
||||
"egui",
|
||||
"egui_extras",
|
||||
"egui_kittest",
|
||||
"log",
|
||||
"serde",
|
||||
@@ -1289,11 +1285,18 @@ dependencies = [
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "deckcraft-vp9"
|
||||
version = "0.12.0"
|
||||
dependencies = [
|
||||
"deckcraft-bitstream",
|
||||
"rayon",
|
||||
"thiserror 2.0.21",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "deckcraft-web"
|
||||
version = "0.18.1"
|
||||
version = "0.12.0"
|
||||
dependencies = [
|
||||
"craft-web-api",
|
||||
"craft-web-host",
|
||||
"deckcraft-engine",
|
||||
"deckcraft-ui-egui",
|
||||
"eframe",
|
||||
@@ -1504,35 +1507,19 @@ dependencies = [
|
||||
"block2 0.6.2",
|
||||
"bytemuck",
|
||||
"egui",
|
||||
"image",
|
||||
"log",
|
||||
"objc2 0.6.5",
|
||||
"objc2-app-kit 0.3.2",
|
||||
"objc2-foundation 0.3.2",
|
||||
"objc2-ui-kit 0.3.2",
|
||||
"parking_lot",
|
||||
"profiling",
|
||||
"raw-window-handle",
|
||||
"smithay-clipboard",
|
||||
"web-time",
|
||||
"webbrowser",
|
||||
"windows-sys 0.61.2",
|
||||
"winit",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "egui_extras"
|
||||
version = "0.37.0"
|
||||
source = "git+https://github.com/storytold/egui.git?rev=6d518fc799681571963842f95b55a6b68431b15a#6d518fc799681571963842f95b55a6b68431b15a"
|
||||
dependencies = [
|
||||
"ahash",
|
||||
"egui",
|
||||
"enum-map",
|
||||
"itertools",
|
||||
"log",
|
||||
"profiling",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "egui_glow"
|
||||
version = "0.37.0"
|
||||
@@ -1600,26 +1587,6 @@ version = "1.1.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "66b7e2430c6dff6a955451e2cfc438f09cea1965a9d6f87f7e3b90decc014099"
|
||||
|
||||
[[package]]
|
||||
name = "enum-map"
|
||||
version = "3.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d4532960034c15ba2f55e9eee631040ecc473c32e6c4b200828c759840f13a06"
|
||||
dependencies = [
|
||||
"enum-map-derive",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "enum-map-derive"
|
||||
version = "1.0.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7127272f64b3e098f7d81f350d672081b879bff896d219c42370dc858b1af9ff"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 3.0.6",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "enumflags2"
|
||||
version = "0.7.12"
|
||||
@@ -2741,7 +2708,7 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "krilla"
|
||||
version = "0.8.2"
|
||||
source = "git+https://github.com/storytold/craft-libs?rev=6e03f65bf45181db77ba6229716e6624db9eb25b#6e03f65bf45181db77ba6229716e6624db9eb25b"
|
||||
source = "git+https://github.com/storytold/craft-libs?rev=92d392cff132eaf53e13afff77f38b5c11b73aa0#92d392cff132eaf53e13afff77f38b5c11b73aa0"
|
||||
dependencies = [
|
||||
"base64 0.22.1",
|
||||
"bumpalo",
|
||||
@@ -3520,7 +3487,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7d8fae84b431384b68627d0f9b3b1245fcf9f46f6c0e3dc902e9dce64edd1967"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"windows-sys 0.61.2",
|
||||
"windows-sys 0.45.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -4436,8 +4403,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3457dea1f0eb631b4034d61d4d8c32074caa6cd1ab2d59f2327bd8461e2c0016"
|
||||
dependencies = [
|
||||
"bitflags 2.13.2",
|
||||
"calloop 0.13.0",
|
||||
"calloop-wayland-source 0.3.0",
|
||||
"calloop",
|
||||
"calloop-wayland-source",
|
||||
"cursor-icon",
|
||||
"libc",
|
||||
"log",
|
||||
@@ -4454,44 +4421,6 @@ dependencies = [
|
||||
"xkeysym",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "smithay-client-toolkit"
|
||||
version = "0.20.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0512da38f5e2b31201a93524adb8d3136276fa4fe4aafab4e1f727a82b534cc0"
|
||||
dependencies = [
|
||||
"bitflags 2.13.2",
|
||||
"calloop 0.14.5",
|
||||
"calloop-wayland-source 0.4.1",
|
||||
"cursor-icon",
|
||||
"libc",
|
||||
"log",
|
||||
"memmap2",
|
||||
"rustix 1.1.5",
|
||||
"thiserror 2.0.21",
|
||||
"wayland-backend",
|
||||
"wayland-client",
|
||||
"wayland-csd-frame",
|
||||
"wayland-cursor",
|
||||
"wayland-protocols",
|
||||
"wayland-protocols-experimental",
|
||||
"wayland-protocols-misc",
|
||||
"wayland-protocols-wlr",
|
||||
"wayland-scanner",
|
||||
"xkeysym",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "smithay-clipboard"
|
||||
version = "0.7.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "71704c03f739f7745053bde45fa203a46c58d25bc5c4efba1d9a60e9dba81226"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"smithay-client-toolkit 0.20.0",
|
||||
"wayland-backend",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "smol_str"
|
||||
version = "0.2.2"
|
||||
@@ -4999,7 +4928,6 @@ version = "0.1.44"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100"
|
||||
dependencies = [
|
||||
"log",
|
||||
"pin-project-lite",
|
||||
"tracing-attributes",
|
||||
"tracing-core",
|
||||
@@ -5442,32 +5370,6 @@ dependencies = [
|
||||
"wayland-scanner",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wayland-protocols-experimental"
|
||||
version = "20250721.0.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "40a1f863128dcaaec790d7b4b396cc9b9a7a079e878e18c47e6c2d2c5a8dcbb1"
|
||||
dependencies = [
|
||||
"bitflags 2.13.2",
|
||||
"wayland-backend",
|
||||
"wayland-client",
|
||||
"wayland-protocols",
|
||||
"wayland-scanner",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wayland-protocols-misc"
|
||||
version = "0.3.12"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6e9567599ef23e09b8dad6e429e5738d4509dfc46b3b21f32841a304d16b29c8"
|
||||
dependencies = [
|
||||
"bitflags 2.13.2",
|
||||
"wayland-backend",
|
||||
"wayland-client",
|
||||
"wayland-protocols",
|
||||
"wayland-scanner",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wayland-protocols-plasma"
|
||||
version = "0.3.12"
|
||||
@@ -6078,7 +5980,7 @@ dependencies = [
|
||||
"bitflags 2.13.2",
|
||||
"block2 0.5.1",
|
||||
"bytemuck",
|
||||
"calloop 0.13.0",
|
||||
"calloop",
|
||||
"cfg_aliases",
|
||||
"concurrent-queue",
|
||||
"core-foundation 0.9.4",
|
||||
@@ -6099,7 +6001,7 @@ dependencies = [
|
||||
"raw-window-handle",
|
||||
"redox_syscall 0.4.1",
|
||||
"rustix 0.38.44",
|
||||
"smithay-client-toolkit 0.19.2",
|
||||
"smithay-client-toolkit",
|
||||
"smol_str",
|
||||
"tracing",
|
||||
"unicode-segmentation",
|
||||
@@ -6275,7 +6177,7 @@ checksum = "9440ea3e5aeabb0ac63af70daf835274065238cdd0cec83418f417eae38bacee"
|
||||
|
||||
[[package]]
|
||||
name = "xtask"
|
||||
version = "0.18.1"
|
||||
version = "0.12.0"
|
||||
dependencies = [
|
||||
"serde_json",
|
||||
]
|
||||
@@ -6521,6 +6423,7 @@ dependencies = [
|
||||
"nix",
|
||||
"typed-path",
|
||||
"windows-sys 0.61.2",
|
||||
"zopfli",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -6608,6 +6511,11 @@ name = "egui_demo_lib"
|
||||
version = "0.37.0"
|
||||
source = "git+https://github.com/storytold/egui.git?rev=6d518fc799681571963842f95b55a6b68431b15a#6d518fc799681571963842f95b55a6b68431b15a"
|
||||
|
||||
[[patch.unused]]
|
||||
name = "egui_extras"
|
||||
version = "0.37.0"
|
||||
source = "git+https://github.com/storytold/egui.git?rev=6d518fc799681571963842f95b55a6b68431b15a#6d518fc799681571963842f95b55a6b68431b15a"
|
||||
|
||||
[[patch.unused]]
|
||||
name = "egui_inspection"
|
||||
version = "0.37.0"
|
||||
|
||||
+16
-10
@@ -4,7 +4,7 @@ members = ["crates/*", "apps/*", "xtask"]
|
||||
default-members = ["crates/*", "apps/deckcraft", "apps/deckcraft-cli"]
|
||||
|
||||
[workspace.package]
|
||||
version = "0.18.1"
|
||||
version = "0.12.0"
|
||||
edition = "2024"
|
||||
license = "MIT OR Apache-2.0"
|
||||
rust-version = "1.95"
|
||||
@@ -27,6 +27,16 @@ deckcraft-engine = { path = "crates/engine" }
|
||||
deckcraft-mcp = { path = "crates/mcp" }
|
||||
deckcraft-ui-egui = { path = "crates/ui-egui" }
|
||||
deckcraft-media = { path = "crates/media" }
|
||||
# codecs (ported from FilmCraft, same owner and licence)
|
||||
deckcraft-bitstream = { path = "crates/bitstream" }
|
||||
deckcraft-h264 = { path = "crates/h264" }
|
||||
deckcraft-hevc = { path = "crates/hevc" }
|
||||
deckcraft-vp9 = { path = "crates/vp9" }
|
||||
deckcraft-av1 = { path = "crates/av1" }
|
||||
deckcraft-isobmff = { path = "crates/isobmff" }
|
||||
deckcraft-matroska = { path = "crates/matroska" }
|
||||
deckcraft-ogg = { path = "crates/ogg" }
|
||||
deckcraft-opus = { path = "crates/opus" }
|
||||
|
||||
# foundation
|
||||
kurbo = { version = "0.13", features = ["serde"] }
|
||||
@@ -49,15 +59,11 @@ 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, craft-audio and craft-video share types and stay on one rev.
|
||||
craft-media = { git = "https://github.com/storytold/craft-libs", rev = "09dcbc225b95232998498a40ca56e24716571d53", version = "=0.1.0", default-features = false }
|
||||
craft-audio = { git = "https://github.com/storytold/craft-libs", rev = "09dcbc225b95232998498a40ca56e24716571d53", version = "=0.1.0", default-features = false, features = ["opus-decode"] }
|
||||
# H.264, HEVC, VP9 and AV1 decoders.
|
||||
craft-video = { git = "https://github.com/storytold/craft-libs", rev = "09dcbc225b95232998498a40ca56e24716571d53", version = "=0.1.0", default-features = false, features = ["h264-decode", "hevc-decode", "vp9-decode", "av1-decode", "threads"] }
|
||||
craft-pdf = { git = "https://github.com/storytold/craft-libs", rev = "6e03f65bf45181db77ba6229716e6624db9eb25b", version = "=0.1.0", default-features = false, features = ["write", "write-simple-text"] }
|
||||
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: app-specific pretend versions apply to startup and manual checks.
|
||||
craft-update = { git = "https://github.com/storytold/craft-libs", version = "=0.1.0", rev = "6e03f65bf45181db77ba6229716e6624db9eb25b", features = ["egui"] }
|
||||
craft-ui = { git = "https://github.com/storytold/craft-libs", rev = "6e03f65bf45181db77ba6229716e6624db9eb25b", version = "=0.1.0" }
|
||||
craft-update = { git = "https://github.com/storytold/craft-libs", version = "=0.1.0", rev = "04aa1703b32d299e54f78fcde4182f2b393a9116", features = ["egui"] }
|
||||
craft-ui = { git = "https://github.com/storytold/craft-libs", rev = "92d392cff132eaf53e13afff77f38b5c11b73aa0", version = "=0.1.0" }
|
||||
i_overlay = "9"
|
||||
quick-xml = "0.41"
|
||||
zip = { version = "3", default-features = false, features = ["deflate"] }
|
||||
@@ -103,7 +109,7 @@ codegen-units = 16
|
||||
debug = 0
|
||||
|
||||
[patch.crates-io]
|
||||
krilla = { git = "https://github.com/storytold/craft-libs", rev = "6e03f65bf45181db77ba6229716e6624db9eb25b", version = "=0.8.2" }
|
||||
krilla = { git = "https://github.com/storytold/craft-libs", rev = "92d392cff132eaf53e13afff77f38b5c11b73aa0", version = "=0.8.2" }
|
||||
ecolor = { git = "https://github.com/storytold/egui.git", rev = "6d518fc799681571963842f95b55a6b68431b15a" }
|
||||
emath = { git = "https://github.com/storytold/egui.git", rev = "6d518fc799681571963842f95b55a6b68431b15a" }
|
||||
epaint = { git = "https://github.com/storytold/egui.git", rev = "6d518fc799681571963842f95b55a6b68431b15a" }
|
||||
|
||||
@@ -76,9 +76,8 @@ Code dependencies
|
||||
- symphonia (audio decoding for MP3, AAC, ALAC, FLAC, Vorbis, WAV, AIFF, CAF) is used unmodified
|
||||
under the Mozilla Public License 2.0; its source is available at
|
||||
https://github.com/pdeljanov/Symphonia.
|
||||
- The H.264, HEVC, VP9 and AV1 crates under crates/ are ported from FilmCraft (same owner,
|
||||
MIT OR Apache-2.0). The Opus decoder (craft-audio) and the MP4, Matroska and Ogg demuxers
|
||||
(craft-media) come from craft-libs (same owner, MIT OR Apache-2.0).
|
||||
- The H.264, HEVC, VP9, AV1, Opus, MP4, Matroska and Ogg crates under crates/ are ported from
|
||||
FilmCraft (same owner, MIT OR Apache-2.0).
|
||||
|
||||
- Krilla 0.8.2 (https://github.com/LaurenzV/krilla) is used through craft-pdf under
|
||||
MIT OR Apache-2.0. craft-libs vendors that version with page/surface unwind recovery
|
||||
|
||||
@@ -286,15 +286,7 @@ stand on its own.
|
||||
| <img src="https://raw.githubusercontent.com/storytold/pdfcraft/main/assets/app-icon/hicolor/64x64/apps/ai.storyteller.pdfcraft.png" alt="" width="32" height="32"> | **PdfCraft** | Reading, organizing and protecting PDFs | [GitHub](https://github.com/storytold/pdfcraft) | [Website](https://getartcraft.com/apps/pdfcraft) |
|
||||
| <img src="https://raw.githubusercontent.com/storytold/effectcraft/main/assets/app-icon/hicolor/64x64/apps/ai.storyteller.effectcraft.png" alt="" width="32" height="32"> | **EffectCraft** | Motion graphics and visual effects | [GitHub](https://github.com/storytold/effectcraft) | [Website](https://getartcraft.com/apps/effectcraft) |
|
||||
| <img src="https://raw.githubusercontent.com/storytold/designcraft/main/assets/app-icon/hicolor/64x64/apps/ai.storyteller.designcraft.png" alt="" width="32" height="32"> | **DesignCraft** | Page layout and publishing | [GitHub](https://github.com/storytold/designcraft) | [Website](https://getartcraft.com/apps/designcraft) |
|
||||
| <img src="https://raw.githubusercontent.com/storytold/figcraft/main/assets/app-icon/hicolor/64x64/apps/ai.storyteller.figcraft.png" alt="" width="32" height="32"> | **FigCraft** | Interface design and prototyping | [GitHub](https://github.com/storytold/figcraft) | [Website](https://getartcraft.com/apps/figcraft) |
|
||||
| <img src="https://raw.githubusercontent.com/storytold/wordcraft/main/assets/app-icon/hicolor/64x64/apps/ai.storyteller.wordcraft.png" alt="" width="32" height="32"> | **WordCraft** | Word processing and documents | [GitHub](https://github.com/storytold/wordcraft) | [Website](https://getartcraft.com/apps/wordcraft) |
|
||||
| <img src="https://raw.githubusercontent.com/storytold/soundcraft/main/assets/app-icon/hicolor/64x64/apps/ai.storyteller.soundcraft.png" alt="" width="32" height="32"> | **SoundCraft** | Audio recording and editing | [GitHub](https://github.com/storytold/soundcraft) | [Website](https://getartcraft.com/apps/soundcraft) |
|
||||
| <img src="https://raw.githubusercontent.com/storytold/deckcraft/main/assets/app-icon/hicolor/64x64/apps/ai.storyteller.deckcraft.png" alt="" width="32" height="32"> | **DeckCraft** | Presentations and slide decks | [GitHub](https://github.com/storytold/deckcraft) | [Website](https://getartcraft.com/apps/deckcraft) |
|
||||
| <img src="https://raw.githubusercontent.com/storytold/cadcraft/main/assets/app-icon/hicolor/64x64/apps/ai.storyteller.cadcraft.png" alt="" width="32" height="32"> | **CADCraft** | Computer-aided design | [GitHub](https://github.com/storytold/cadcraft) | [Website](https://getartcraft.com/apps/cadcraft) |
|
||||
| <img src="https://raw.githubusercontent.com/storytold/gridcraft/main/assets/app-icon/hicolor/64x64/apps/ai.storyteller.gridcraft.png" alt="" width="32" height="32"> | **GridCraft** | Spreadsheets and data | [GitHub](https://github.com/storytold/gridcraft) | [Website](https://getartcraft.com/apps/gridcraft) |
|
||||
| <img src="https://raw.githubusercontent.com/storytold/texturecraft/main/assets/app-icon/hicolor/64x64/apps/ai.storyteller.texturecraft.png" alt="" width="32" height="32"> | **TextureCraft** | 3D texture painting | [GitHub](https://github.com/storytold/texturecraft) | [Website](https://getartcraft.com/apps/texturecraft) |
|
||||
| <img src="https://raw.githubusercontent.com/storytold/craft-anything/main/assets/app-icon/hicolor/64x64/apps/ai.storyteller.craft-anything.png" alt="" width="32" height="32"> | **Craft Anything** | See how any software works, then build your own | [GitHub](https://github.com/storytold/craft-anything) | [Website](https://getartcraft.com/apps/craft-anything) |
|
||||
| <img src="https://raw.githubusercontent.com/tangency-labs/solidcraft/main/assets/icons/app/linux/exports/hicolor/64x64/apps/io.tangencylabs.solidcraft.png" alt="" width="32" height="32"> | **SolidCraft** | Parametric 3D CAD by Tangency Labs (community app) | [GitHub](https://github.com/tangency-labs/solidcraft) | |
|
||||
| <img src="https://raw.githubusercontent.com/storytold/deckcraft/main/assets/app-icon/hicolor/64x64/apps/ai.storyteller.deckcraft.png" alt="" width="32" height="32"> | **DeckCraft** | **Presentations and slide shows · you are here** | [GitHub](https://github.com/storytold/deckcraft) | [Website](https://getartcraft.com/apps/deckcraft) |
|
||||
|
||||
And [**ArtCraft**](https://getartcraft.com/) itself, our AI image and video studio for artists who want real control.
|
||||
|
||||
|
||||
+2
-39
@@ -2,14 +2,6 @@
|
||||
|
||||
**Stage: alpha** · next: beta, ~20 points and ~120–210 h away
|
||||
|
||||
> **Last reviewed:** 2026-11-04 · **Last updated:** 2026-11-11 · **Change:** trivial (the Opus decoder and the Ogg, MP4 and Matroska demuxers come from craft-libs; parity scores unchanged) · **Target:** Microsoft PowerPoint for Mac 16.113 (Microsoft 365)
|
||||
> **Last reviewed:** 2026-11-02 · **Last updated:** 2026-11-03 · **Change:** trivial (progress log: web `?host=parent` bridge opens and saves through a same-origin framing page; scores unchanged) · **Target:** Microsoft PowerPoint for Mac 16.113 (Microsoft 365)
|
||||
|
||||
> **Last reviewed:** 2026-11-02 · **Last updated:** 2026-11-03 · **Change:** trivial (community PR wave 10: PPTX and native saves keep table cell centred anchoring, text overflow, compatible line spacing, normalised character height, Thai distributed alignment, RTL table direction, alternate run language and reflection transforms; imported effects placed by parent time-node delays; Reuse Slides fits the destination size and brings custom table styles; deleted slides leave custom shows; merged shapes drop their animations; Compress Pictures measures grouped pictures at displayed size; parity scores unchanged) · **Target:** Microsoft PowerPoint for Mac 16.113 (Microsoft 365)
|
||||
> **Last reviewed:** 2026-11-02 · **Last updated:** 2026-11-02 · **Change:** trivial (File › About replaces Account; product links, credits and appearance retained; parity scores unchanged) · **Target:** Microsoft PowerPoint for Mac 16.113 (Microsoft 365)
|
||||
> **Last reviewed:** 2026-10-14 · **Last updated:** 2026-11-01 · **Change:** trivial (progress log: fix for a workflow-breaking regression: Ctrl+C/X/V use the system clipboard again, broken for all text and objects from 0.11.1 to 0.15.0; pasted and duplicated placeholders keep their look; stage and scores unchanged) · **Target:** Microsoft PowerPoint for Mac 16.113 (Microsoft 365)
|
||||
> **Last reviewed:** 2026-10-14 · **Last updated:** 2026-11-01 · **Change:** trivial (locked No Fill and no-outline click-through regression coverage added; No Fill hit-testing follows resolved fill instead of the presence of a style (#1621); scores unchanged) · **Target:** Microsoft PowerPoint for Mac 16.113 (Microsoft 365)
|
||||
> **Last reviewed:** 2026-10-14 · **Last updated:** 2026-10-30 · **Change:** trivial (progress log: read-only `slide.actions` lists shape and table text actions; parity scores unchanged) · **Target:** Microsoft PowerPoint for Mac 16.113 (Microsoft 365)
|
||||
> **Last reviewed:** 2026-10-14 · **Last updated:** 2026-10-27 · **Change:** trivial (progress log: Set Up Slide Show ▸ Keep mouse cursor visible, #1445; stage and scores unchanged) · **Target:** Microsoft PowerPoint for Mac 16.113 (Microsoft 365)
|
||||
> **Last reviewed:** 2026-10-14 · **Last updated:** 2026-10-26 · **Change:** trivial (progress log: dragging several selected slides moves them all; previous: trivial, progress log: Edit Points handle drags with Shift, Ctrl/Cmd or Alt/Option set the point type; previous: trivial, progress log: scroll bars on the zoomed slide pane, #1157; previous: minor, native chart payloads stored in bounded ZIP members; previous: major, community PR wave 2026-10-14: ~60 landed items in the progress log, among them the Font, Paragraph, Photo Album, theme colour and font and Date and Time dialogs, presenter and show controls, the SmartArt Text Pane and PPTX corpus slices; breadth 84% after `cargo xtask parity`, ready stays ~55%, stage stays alpha; duplicated rows and headers folded; previous: minor, hours to ~95% per audience added) · **Target:** Microsoft PowerPoint for Mac 16.113 (Microsoft 365)
|
||||
> **Last reviewed:** 2026-10-14 · **Last updated:** 2026-10-24 · **Change:** trivial (progress log: Edit Points handle drags with Shift, Ctrl/Cmd or Alt/Option set the point type; previous: trivial, progress log: scroll bars on the zoomed slide pane, #1157; previous: minor, native chart payloads stored in bounded ZIP members; previous: major, community PR wave 2026-10-14: ~60 landed items in the progress log, among them the Font, Paragraph, Photo Album, theme colour and font and Date and Time dialogs, presenter and show controls, the SmartArt Text Pane and PPTX corpus slices; breadth 84% after `cargo xtask parity`, ready stays ~55%, stage stays alpha; duplicated rows and headers folded; previous: minor, hours to ~95% per audience added) · **Target:** Microsoft PowerPoint for Mac 16.113 (Microsoft 365)
|
||||
@@ -154,25 +146,6 @@ app's JSON control channel (`deckcraft --control PORT`).
|
||||
|
||||
| Date | What landed |
|
||||
|---|---|
|
||||
| 2026-11-11 | Media: the Opus decoder and the Ogg, MP4/MOV and Matroska demuxers are craft-audio and craft-media from craft-libs; DeckCraft's own copies and re-export crates are removed |
|
||||
| 2026-11-04 | Ogg Opus recovery keeps trailing packets without granules in the total duration, matching their recovered positions; valid end trimming stays covered |
|
||||
| 2026-11-03 | Web: the `?host=parent` bridge (as in GridCraft, WordCraft and PdfCraft) lets a same-origin page that frames DeckCraft send a presentation and store its saves; Save writes an opened .pptx back as .pptx under its own name, as on the desktop |
|
||||
| 2026-11-02 | Slide show on Linux and the BSDs: Presenter View stays off when winit lists exactly one display, as in PowerPoint; if it lists none, the setting is kept. On one display the presenter window covered the show, and the hidden window stalled every frame: slide changes took up to ~1.7 s under Hyprland (#1156) |
|
||||
| 2026-11-02 | Native and PPTX saves retain optional table right-to-left direction, including explicit false and omission; two-save round-trip tests keep labeled columns in order |
|
||||
| 2026-11-02 | Text runs preserve alternate-language metadata independently from the main language through native and PPTX saves |
|
||||
| 2026-11-02 | Picture compression measures displayed dimensions through nested group transforms; checklist score unchanged |
|
||||
| 2026-11-02 | Deleting slides removes their IDs from every custom show; the cleanup is undoable and survives native save/reopen |
|
||||
| 2026-11-02 | Reused slides scale their geometry and content dimensions to the destination slide, including kept source masters and layouts; the reuse picker still lacks thumbnails |
|
||||
| 2026-11-02 | File › About replaces the unused Account page, with version, project links, contributor/model credits and appearance preferences |
|
||||
| 2026-11-02 | PPTX reflection effects keep their authored start position, affine scale/skew, direction and fade, alignment, and rotation flag through native and PPTX saves |
|
||||
| 2026-11-03 | Reuse Slides brings along referenced custom table styles, including grouped tables, and remaps conflicting style ids without changing destination tables |
|
||||
| 2026-11-02 | Tiled picture fills in groups: a tile (`a:blipFill` with `a:tile`) keeps its size, offset and anchor in slide points whatever the group's child-coordinate scale, uniform or stretched unevenly, nested or flipped (also a group's own tile fill passed to its members, and a picture shape's own tile, with its shadow and reflection), and tiles with `rotWithShape="0"` keep their size in points at any zoom, as PowerPoint draws them (measured on its own PNG export of a synthetic probe deck: every grouped case within ~3/255 of the ungrouped control's own difference from PowerPoint, which is ~13/255 from resampling). Before, a group whose child space was ~1800 times smaller than its box showed one flat texel of the texture (a Venn diagram with paper-textured circles and a hatched intersection) |
|
||||
| 2026-11-01 | Ctrl+V of a picture copied in a browser (Copy Image) pastes the picture, not a text box with its link and alt text; the link still pastes as text when no picture came with it |
|
||||
| 2026-11-01 | Copy, Cut, Paste, Duplicate and Alt+drag of a title, body or other placeholder on a slide keep its look: the copy is a regular shape that takes the size, font, bullets, indents, autofit and box its layout and master gave it, instead of 18 pt unbulleted text |
|
||||
| 2026-11-01 | Clipboard: Ctrl+C, Ctrl+X and Ctrl+V use the system clipboard again, so text and pictures paste from other apps and in-app copies paste back. Since the egui 0.37 migration (#1350) the desktop build had a clipboard that only worked inside the app. On native Wayland, text goes through egui's Wayland clipboard; HTML stays on X11, macOS and Windows |
|
||||
| 2026-10-31 | No Fill shape interiors allow selection and editing of underlying objects, even when a quick style remains; hit-testing uses the renderer’s fill resolution. Outlines, rotation, text targets and actual fills keep their hit behaviour (#1621) |
|
||||
| 2026-10-30 | Print-preview tests use the URL emitted by egui and its exact file path; previews are explicitly removed before the macOS process exits |
|
||||
| 2026-10-30 | `slide.actions` lists shape click and hover actions and text-run hyperlinks (including table cells and grouped shapes) for audit; listing never runs an action. The UI pane is a follow-up |
|
||||
| 2026-10-29 | Slide Show: going back to the previous slide plays the transition of the slide being left, reversed, as PowerPoint does, instead of the previous slide's own entrance transition (#1531); a jump (Go to Slide, links) still plays the target slide's transition |
|
||||
| 2026-10-27 | Set Up Slide Show ▸ Keep mouse cursor visible: a full-screen show no longer hides a still pointer, so the choices of a branching deck stay visible and clickable; the setting is kept with the user's preferences (`ui.json`, `showKeepCursor`), off by default (#1445) |
|
||||
| 2026-10-25 | A photo in a picture placeholder is cut into the shape its layout gives the placeholder, as PowerPoint draws it: a layout's round picture placeholder keeps a slide's photo in a circle with the layout's white border, where it came out as a rectangle (the reader only passed `p:sp` placeholders to `inherit_placeholder_geometry`, not `p:pic`) |
|
||||
@@ -330,7 +303,7 @@ app's JSON control channel (`deckcraft --control PORT`).
|
||||
| 2026-10-12 | Render: line widths, dashes and arrowheads inside a group no longer scale with the group's child-coordinate mapping, so a group in a small legacy child space (scale about 1600) draws thin lines instead of covering the slide (#233) |
|
||||
| 2026-10-12 | `deckcraft-cli help` and each subcommand's `--help` explain the CLI to an agent; `describe` spells out a command's parameters and what it acts on; `commands` lists one command per line |
|
||||
| 2026-10-11 | Animated GIF export of the deck: the Export… dialog (Animated GIF), `file.export` format `gif`, CLI and MCP (#171) |
|
||||
| 2026-10-10 | Progress docs restructured; full re-measure (breadth 79%, ready ~55%, mainstream ~53%, essentials ~62%, alpha). Landed: Animation Painter (#54), Morph geometry outlines (#27), animated GIF playback (#39), hyperlink colour and following links in the show (#37, #45), date fields in their own format (#36), flipped shapes keep readable text (#35), show transition fix (#46), start screen from the Home button (#49), MCP export lists PDF (#65), OmaStore manifest (#57) |
|
||||
| 2026-10-10 | Progress docs restructured to the craftrules standard; full re-measure (breadth 79%, ready ~55%, mainstream ~53%, essentials ~62%, alpha). Landed: Animation Painter (#54), Morph geometry outlines (#27), animated GIF playback (#39), hyperlink colour and following links in the show (#37, #45), date fields in their own format (#36), flipped shapes keep readable text (#35), show transition fix (#46), start screen from the Home button (#49), MCP export lists PDF (#65), OmaStore manifest (#57) |
|
||||
| 2026-10-09 | v0.4.0 released. UAX #9 bidi and Arabic shaping (#4), Morph with Words and Characters (#5), tables grow to fit and PDF cell text (#14), rotating log file (#7), DX12 default on Windows (#16), H.264/HEVC overflow hardening (#2), PPTX sniffing anywhere in the zip (#19) |
|
||||
| 2026-10-08 | v0.2.0 and v0.3.0 released: Windows arm64, Flatpak aarch64, AppImage zsync updates, branded DMG |
|
||||
| 2026-10-07 | v0.1.0 released, signed and notarized; About window with contributor credits (#3); PrintCraft renamed PdfCraft |
|
||||
@@ -341,16 +314,6 @@ app's JSON control channel (`deckcraft --control PORT`).
|
||||
|
||||
| Date | Change | Summary |
|
||||
|---|---|---|
|
||||
| 2026-11-11 | trivial | Opus and container code moved to craft-libs crates; parity scores unchanged |
|
||||
| 2026-11-04 | trivial | Ogg Opus recovery duration includes its recovered tail; parity scores unchanged |
|
||||
| 2026-11-02 | trivial | Progress log: run alternate-language metadata round-trips through native and PPTX formats; product parity scores unchanged |
|
||||
| 2026-11-03 | trivial | Reuse Slides brings referenced custom table styles along; scores unchanged |
|
||||
| 2026-11-03 | trivial | Progress log: web `?host=parent` bridge to a framing page; scores unchanged |
|
||||
| 2026-11-02 | trivial | File › About replaces Account; product links, credits and appearance retained |
|
||||
| 2026-11-02 | trivial | Progress log: tiled picture fills keep their size in scaled groups. Scores and milestone names unchanged |
|
||||
| 2026-11-01 | trivial | Workflow-breaking regression fixed: the desktop build uses the system clipboard again (eframe `clipboard` feature) |
|
||||
| 2026-10-31 | trivial | No Fill hit-testing follows resolved fill instead of the presence of a style (#1621); scores unchanged |
|
||||
| 2026-10-30 | trivial | Progress log: read-only slide action inspection; parity scores unchanged |
|
||||
| 2026-10-29 | trivial | Progress log: going back in the slide show reverses the left slide's transition (#1531). Scores and milestone names unchanged |
|
||||
| 2026-10-29 | trivial | Progress log: the Picture Styles row leaves room for Crop and Size (#1232). Scores and milestone names unchanged |
|
||||
| 2026-10-25 | trivial | Progress log: a picture in a picture placeholder takes the layout's shape. Scores and milestone names unchanged |
|
||||
@@ -450,5 +413,5 @@ app's JSON control channel (`deckcraft --control PORT`).
|
||||
| 2026-10-10 | minor | Readiness-by-audience table with hours to ~95% for full, mainstream and essentials |
|
||||
| 2026-10-10 | minor | Added mainstream practitioner (~53%) and essentials user (~62%); features recomputed from written area weights, ready ~56% → ~55% |
|
||||
| 2026-10-10 | minor | Stage checked against the new core-workflow gate: passes, stays alpha |
|
||||
| 2026-10-10 | major | Restructured: stage, two numbers, dimensions, features, languages; parity estimate moved to docs/target-app-parity.md, "Working today" to its evidence section, the alpha checklist to docs/roadmap.md |
|
||||
| 2026-10-10 | major | Restructured to craftrules `standards/progress-docs.md`: stage, two numbers, dimensions, features, languages; parity estimate moved to docs/target-app-parity.md, "Working today" to its evidence section, the alpha checklist to docs/roadmap.md |
|
||||
| 2026-10-07 | major | Alpha checklist (~80% to a self-defined alpha), breadth ~80%, overall ~62%, ~190 h to full parity |
|
||||
|
||||
@@ -16,10 +16,6 @@ path = "src/main.rs"
|
||||
[dependencies]
|
||||
deckcraft-engine = { workspace = true }
|
||||
deckcraft-ui-egui = { workspace = true }
|
||||
# The page command API (storytold/craft-libs#413): window.deckcraft over the control channel.
|
||||
craft-web-api = { git = "https://github.com/storytold/craft-libs", rev = "fd5e2feff6ea25bed371a99cc732b3968d621505", version = "0.1.0" }
|
||||
# The host messages (storytold/craft-libs): one contract and one bridge for the web builds.
|
||||
craft-web-host = { git = "https://github.com/storytold/craft-libs", rev = "fd5e2feff6ea25bed371a99cc732b3968d621505", version = "0.1.0" }
|
||||
egui = { workspace = true }
|
||||
log = { workspace = true }
|
||||
serde_json = { workspace = true }
|
||||
@@ -31,4 +27,4 @@ rfd = { workspace = true }
|
||||
wasm-bindgen = "0.2"
|
||||
wasm-bindgen-futures = "0.4"
|
||||
js-sys = "0.3"
|
||||
web-sys = { version = "0.3", features = ["Window", "Document", "Element", "HtmlElement", "HtmlCanvasElement", "HtmlAnchorElement", "Blob", "BlobPropertyBag", "Url", "Location", "CssStyleDeclaration", "MessageEvent", "Event", "EventTarget", "UrlSearchParams"] }
|
||||
web-sys = { version = "0.3", features = ["Window", "Document", "Element", "HtmlElement", "HtmlCanvasElement", "HtmlAnchorElement", "Blob", "BlobPropertyBag", "Url", "Location", "CssStyleDeclaration"] }
|
||||
|
||||
@@ -1,151 +0,0 @@
|
||||
//! `window.deckcraft`: the page command API (`craft-web-api`, storytold/craft-libs#413) over the
|
||||
//! app's own control channel, the one the desktop control server drives (`docs/control-protocol.md`).
|
||||
//!
|
||||
//! The crate owns the JavaScript side (`request`, `execute`, `commands`, `docInspect`, `uiInspect`,
|
||||
//! `screenshot`, as promises with a timeout). This module is DeckCraft's part: a [`Request`] that
|
||||
//! queues a [`ControlRequest`] for the app, which answers it on its next frame, and waits for the
|
||||
//! reply. A call the page gave up on is released ([`Pending`]), so the app skips it if it has not started it, instead of
|
||||
//! applying a command the page was told had failed.
|
||||
|
||||
use std::sync::Arc;
|
||||
use std::sync::atomic::AtomicU8;
|
||||
use std::sync::mpsc::{Receiver, TryRecvError};
|
||||
|
||||
use deckcraft_ui_egui::ControlRequest;
|
||||
use serde_json::{Value, json};
|
||||
|
||||
/// How often a waiting call looks for the app's reply, in milliseconds (about a frame).
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
const POLL_MS: i32 = 16;
|
||||
|
||||
/// A queued control request whose reply has not arrived. Dropping it before the reply (the page's
|
||||
/// timeout fired, or the call was abandoned) releases the request: the app skips it if it has not started it (one it has taken still finishes).
|
||||
struct Pending {
|
||||
reply: Receiver<Value>,
|
||||
state: Arc<AtomicU8>,
|
||||
answered: bool,
|
||||
}
|
||||
|
||||
impl Pending {
|
||||
/// `Some` once the app has answered or gone away; `None` while the request is still waiting.
|
||||
fn poll(&mut self) -> Option<Value> {
|
||||
match self.reply.try_recv() {
|
||||
Ok(reply) => {
|
||||
self.answered = true;
|
||||
Some(reply)
|
||||
}
|
||||
Err(TryRecvError::Empty) => None,
|
||||
Err(TryRecvError::Disconnected) => {
|
||||
self.answered = true;
|
||||
Some(json!({"ok": false, "error": "the app dropped the request"}))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for Pending {
|
||||
fn drop(&mut self) {
|
||||
if !self.answered {
|
||||
ControlRequest::abandon(&self.state);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
pub use web::install;
|
||||
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
mod web {
|
||||
use std::rc::Rc;
|
||||
use std::sync::mpsc::channel;
|
||||
|
||||
use craft_web_api::methods::Methods;
|
||||
use craft_web_api::page::{PageApi, Request};
|
||||
use deckcraft_ui_egui::{ControlRequest, SlideApp};
|
||||
use serde_json::json;
|
||||
use wasm_bindgen_futures::JsFuture;
|
||||
|
||||
use super::{POLL_MS, Pending};
|
||||
|
||||
/// The app id in `window.<app>`.
|
||||
const APP: &str = "deckcraft";
|
||||
|
||||
/// Give the app its control queue and put `window.deckcraft` on the page. An existing
|
||||
/// `window.deckcraft` keeps the functions it already has.
|
||||
pub fn install(app: &mut SlideApp, ctx: egui::Context) {
|
||||
let (queue, requests) = channel::<ControlRequest>();
|
||||
app.control_rx = Some(requests);
|
||||
let request: Request = Rc::new(move |method, params| {
|
||||
let (control, reply) = ControlRequest::new(method, params);
|
||||
let mut pending = Pending { reply, state: control.state.clone(), answered: false };
|
||||
if queue.send(control).is_err() {
|
||||
pending.answered = true;
|
||||
return Box::pin(async { json!({"ok": false, "error": "the app is not running"}) });
|
||||
}
|
||||
ctx.request_repaint();
|
||||
Box::pin(async move {
|
||||
loop {
|
||||
if let Some(reply) = pending.poll() {
|
||||
return reply;
|
||||
}
|
||||
sleep(POLL_MS).await;
|
||||
}
|
||||
})
|
||||
});
|
||||
if let Err(e) = Rc::new(PageApi::new(APP, Methods::default(), request)).install() {
|
||||
log::warn!("couldn't install window.{APP}: {e:?}");
|
||||
}
|
||||
}
|
||||
|
||||
async fn sleep(ms: i32) {
|
||||
let promise = js_sys::Promise::new(&mut |resolve, _| {
|
||||
if let Some(window) = web_sys::window() {
|
||||
let _ = window.set_timeout_with_callback_and_timeout_and_arguments_0(&resolve, ms);
|
||||
}
|
||||
});
|
||||
let _ = JsFuture::from(promise).await;
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use std::sync::atomic::Ordering;
|
||||
|
||||
use super::*;
|
||||
|
||||
/// `ControlRequest`'s `state` values (`deckcraft_ui_egui::control`): the app has started it, or its
|
||||
/// sender gave up.
|
||||
const TAKEN: u8 = 1;
|
||||
const ABANDONED: u8 = 2;
|
||||
|
||||
fn pending() -> (Pending, ControlRequest) {
|
||||
let (control, reply) = ControlRequest::new("document.inspect", json!({}));
|
||||
(Pending { reply, state: control.state.clone(), answered: false }, control)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_call_the_page_gave_up_on_is_released_so_the_app_skips_it() {
|
||||
let (waiting, control) = pending();
|
||||
drop(waiting);
|
||||
assert_eq!(control.state.load(Ordering::SeqCst), ABANDONED);
|
||||
assert!(!control.take(), "the app must not run a request the page was told had failed");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn an_answered_call_is_not_released() {
|
||||
let (mut waiting, control) = pending();
|
||||
assert!(control.take());
|
||||
control.reply.send(json!({"ok": true, "result": 1})).unwrap_or_default();
|
||||
assert_eq!(waiting.poll(), Some(json!({"ok": true, "result": 1})));
|
||||
drop(waiting);
|
||||
assert_eq!(control.state.load(Ordering::SeqCst), TAKEN);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_request_the_app_never_runs_waits_until_it_is_dropped() {
|
||||
let (mut waiting, control) = pending();
|
||||
assert_eq!(waiting.poll(), None);
|
||||
drop(control);
|
||||
assert_eq!(waiting.poll().and_then(|r| r["ok"].as_bool()), Some(false), "the app went away");
|
||||
}
|
||||
}
|
||||
@@ -1,149 +0,0 @@
|
||||
//! The host bridge, for pages that embed DeckCraft and keep its files (a Nextcloud app, a personal
|
||||
//! file server). The messages and the two transports are `craft-web-host`'s
|
||||
//! (storytold/craft-libs), the same contract as the other Craft apps' bridges, with the
|
||||
//! `deckcraft:` prefix. This module is DeckCraft's part of it.
|
||||
//!
|
||||
//! - `?host=parent` (an `<iframe>`) talks to the same-origin parent over `postMessage`;
|
||||
//! `data-host="page"` on the canvas (the page's own document, no iframe) uses DOM
|
||||
//! `CustomEvent`s on the canvas instead. `?host=parent` wins when both are present. With no
|
||||
//! parent, or a parent on another origin, there is no bridge: the normal app with downloads.
|
||||
//! - `deckcraft:ready` (to the host): sent once the app is running and listening; send a file now. DeckCraft
|
||||
//! reads bytes, so it says `file: false` and a host sends `bytes`.
|
||||
//! - `deckcraft:open` (from the host): `{ name, bytes }`, bytes an `ArrayBuffer` or
|
||||
//! `Uint8Array`, name a bare file name. It opens like a file picked with File › Open on the
|
||||
//! desktop: a presentation sent with its name (.pptx, .potx, .ppsx, .deckcraft) has that name
|
||||
//! as its file, so Save goes back to it; an outline (.txt, .md, .rtf), a file without a name and
|
||||
//! a damaged file open untitled.
|
||||
//! - `deckcraft:save` (to the host): `{ name, saveAs, bytes }` (with the bytes as a transferred `ArrayBuffer` over `postMessage`) for
|
||||
//! every file DeckCraft would otherwise download. The host stores it and reports failures
|
||||
//! itself. `saveAs` says whether the file becomes the presentation's file:
|
||||
//! - Save: `false`. It writes the opened file under its own name and format (a .pptx stays a
|
||||
//! .pptx), as the desktop app does. A name the parent never sent (an untitled presentation, an
|
||||
//! outline, a file sent without a name, a repaired copy of a damaged file, a file the user
|
||||
//! picked or dropped) is a new file: `true`, as for Save As, named `<title>.deckcraft` as
|
||||
//! Save suggests on the desktop.
|
||||
//! - Save As (File › Save As as .pptx or .deckcraft, Ctrl+Shift+S): `true`. The presentation is
|
||||
//! now that file; the next Save sends its name with `false`.
|
||||
//! - Export (PDF, pictures, outline, GIF, handouts, a Save As type that isn't a presentation)
|
||||
//! and Print: `false`. They are copies; the presentation's file stays the same.
|
||||
//! - `deckcraft:dirty` whenever unsaved changes appear or go away, `deckcraft:failed` when the
|
||||
//! app couldn't start or refused a file of 2 GiB or larger.
|
||||
//!
|
||||
//! In host mode the app starts with an empty presentation instead of the sample, which gives way to
|
||||
//! the first file the host sends; the host guards leaving the page, and `?author=` names the user
|
||||
//! for comments.
|
||||
|
||||
use std::cell::RefCell;
|
||||
use std::rc::Rc;
|
||||
|
||||
use craft_web_host::web::{FileData, Host, Message};
|
||||
use deckcraft_ui_egui::{Services, SlideApp};
|
||||
|
||||
/// The app id in the host messages (`deckcraft:ready`).
|
||||
const APP: &str = "deckcraft";
|
||||
|
||||
/// The name `craft-web-host` gives an open request that carries none (it cleans a host's name and
|
||||
/// falls back to this): no name is no file, so the presentation opens untitled.
|
||||
const NAMELESS: &str = "document";
|
||||
|
||||
/// The largest file a host may send. File › Open on the desktop refuses anything over 2 GiB.
|
||||
/// On wasm32 a `Vec` cannot hold 2 GiB (`isize::MAX` is one byte less), so a file of exactly
|
||||
/// 2 GiB is refused too, before its bytes are copied.
|
||||
const MAX_OPEN_BYTES: f64 = 2_147_483_648.0;
|
||||
|
||||
/// The longest user name taken from `?author=` (the preferences' own limit).
|
||||
const MAX_AUTHOR_CHARS: usize = 256;
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct Bridge {
|
||||
host: Rc<Host>,
|
||||
/// Files the host sent (the name, if it sent one), waiting for the next frame.
|
||||
opened: Rc<RefCell<Vec<(Option<String>, Vec<u8>)>>>,
|
||||
}
|
||||
|
||||
impl Bridge {
|
||||
/// The bridge for this page, or `None` when DeckCraft isn't hosted: no `?host=parent` and no
|
||||
/// `data-host="page"` on the canvas, an unframed `?host=parent` page (it stays the normal app
|
||||
/// with downloads), or a parent on another origin.
|
||||
pub fn detect(canvas: &web_sys::HtmlCanvasElement) -> Option<Self> {
|
||||
let host = Host::connect(APP, canvas)?;
|
||||
// An opaque origin (a file:// page in some browsers) can't be a postMessage target.
|
||||
if host.mode() == craft_web_host::Mode::Parent && web_sys::window()?.location().origin().ok()? == "null" {
|
||||
return None;
|
||||
}
|
||||
Some(Self { host: Rc::new(host), opened: Rc::default() })
|
||||
}
|
||||
|
||||
/// Hand saved and exported files to the host instead of downloading them, and hear the files
|
||||
/// it sends.
|
||||
pub fn connect(&self, services: &mut Services, ctx: egui::Context) {
|
||||
self.listen(ctx);
|
||||
let bridge = self.clone();
|
||||
services.host_save = Some(Box::new(move |name: &str, bytes: &[u8], save_as: bool| bridge.post_save(name, bytes, save_as)));
|
||||
// Exports and Print are copies.
|
||||
let bridge = self.clone();
|
||||
services.download = Some(Box::new(move |name: &str, bytes: &[u8]| {
|
||||
if let Err(e) = bridge.post_save(name, bytes, false) {
|
||||
log::error!("couldn't hand {name} to the page: {e}");
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
/// `?author=`: the host's name for the user signs new comments.
|
||||
pub fn set_author(app: &mut SlideApp, author: &str) {
|
||||
let author: String = author.trim().chars().take(MAX_AUTHOR_CHARS).collect();
|
||||
if !author.is_empty() {
|
||||
app.session.prefs.author = author;
|
||||
}
|
||||
}
|
||||
|
||||
pub fn post_ready(&self) {
|
||||
let _ = self.host.post_ready(env!("CARGO_PKG_VERSION"), false);
|
||||
}
|
||||
|
||||
/// The app couldn't start (no WebGPU or WebGL2, for example): let the host say so.
|
||||
pub fn post_failed(&self, error: &str) {
|
||||
let _ = self.host.post_failed(error, None);
|
||||
}
|
||||
|
||||
/// Before each frame: open the files the host sent.
|
||||
pub fn open_files(&self, app: &mut SlideApp) {
|
||||
let opened = std::mem::take(&mut *self.opened.borrow_mut());
|
||||
for (name, bytes) in opened {
|
||||
app.open_from_host(name.as_deref(), &bytes);
|
||||
}
|
||||
}
|
||||
|
||||
/// After each frame: tell the host when unsaved changes appear or go away (the crate does not
|
||||
/// repeat a state it already reported).
|
||||
pub fn report_dirty(&self, app: &SlideApp) {
|
||||
self.host.post_dirty(app.session.documents().iter().any(|d| d.is_dirty()));
|
||||
}
|
||||
|
||||
fn post_save(&self, path: &str, bytes: &[u8], save_as: bool) -> Result<(), String> {
|
||||
let name = std::path::Path::new(path).file_name().map(|n| n.to_string_lossy().to_string()).unwrap_or_else(|| path.to_string());
|
||||
self.host.post_save(&name, bytes, None, save_as)
|
||||
}
|
||||
|
||||
/// Hear `deckcraft:open` and queue the file for the next frame.
|
||||
fn listen(&self, ctx: egui::Context) {
|
||||
let opened = self.opened.clone();
|
||||
let host = self.host.clone();
|
||||
self.host.listen(move |message| {
|
||||
let Message::Open { name, data } = message else { return };
|
||||
if data.size() >= MAX_OPEN_BYTES {
|
||||
log::warn!("host file {name} is 2 GiB or larger; not opened");
|
||||
let _ = host.post_failed(&format!("{name} is 2 GiB or larger, which DeckCraft's web build cannot open"), None);
|
||||
return;
|
||||
}
|
||||
let FileData::Bytes(array) = data else {
|
||||
log::warn!("host file {name} came as a File; DeckCraft reads bytes only; not opened");
|
||||
return;
|
||||
};
|
||||
// No name is no file: the presentation opens untitled, never under an invented name.
|
||||
let name = (name != NAMELESS).then_some(name);
|
||||
opened.borrow_mut().push((name, array.to_vec()));
|
||||
ctx.request_repaint();
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -9,24 +9,15 @@
|
||||
//! - File → Open and Insert → Pictures/Audio/Video use the browser file picker; bytes arrive
|
||||
//! asynchronously through `Services::inbox`, applied to the document, slide and operation
|
||||
//! that started the pick (a dismissed picker or a closed document drops the file);
|
||||
//! - Save and Export trigger a browser download (or go to the framing page with `?host=parent`);
|
||||
//! - Save and Export trigger a browser download;
|
||||
//! - dropped files are read asynchronously by `web::WebShell` and delivered through the inbox;
|
||||
//! - preferences are not persisted.
|
||||
//!
|
||||
//! URL query flags: `?webgl` forces the WebGL2 backend instead of WebGPU; `?blank` starts with an
|
||||
//! empty presentation instead of the sample deck; `?show` starts the slide show.
|
||||
//! `?host=parent` (inside an `<iframe>`) talks to a same-origin parent page instead: it sends the
|
||||
//! presentation to open and stores what DeckCraft saves (`host`), and `&author=` names the user
|
||||
//! for comments.
|
||||
#![deny(clippy::unwrap_used, clippy::expect_used, clippy::panic, clippy::unimplemented, clippy::todo, clippy::unreachable)]
|
||||
#![forbid(unsafe_code)]
|
||||
|
||||
#[cfg(any(target_arch = "wasm32", test))]
|
||||
mod api;
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
mod host;
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
mod presenter;
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
mod web;
|
||||
|
||||
|
||||
@@ -1,306 +0,0 @@
|
||||
//! Same-origin popup transport. Each window starts its own eframe/WASM runner.
|
||||
use deckcraft_ui_egui::presenter::{Command, DeltaCache, Host, Snapshot, View};
|
||||
use std::{
|
||||
cell::RefCell,
|
||||
collections::VecDeque,
|
||||
rc::{Rc, Weak},
|
||||
};
|
||||
use wasm_bindgen::{JsCast, closure::Closure};
|
||||
|
||||
const MAX_MESSAGE: usize = 16 * 1024 * 1024;
|
||||
const MAX_COMMANDS: usize = 64;
|
||||
const PREFIX: &str = "#deckcraft-presenter=";
|
||||
fn error(e: wasm_bindgen::JsValue) -> String {
|
||||
format!("{e:?}")
|
||||
}
|
||||
fn origin() -> Result<String, String> {
|
||||
web_sys::window().ok_or("No browser window")?.location().origin().map_err(error)
|
||||
}
|
||||
pub fn token() -> Option<String> {
|
||||
let hash = web_sys::window()?.location().hash().ok()?;
|
||||
let token = hash.strip_prefix(PREFIX)?;
|
||||
(!token.is_empty() && token.len() < 128).then(|| token.to_owned())
|
||||
}
|
||||
fn same_source(event: &web_sys::MessageEvent, window: &web_sys::Window, expected_origin: &str) -> bool {
|
||||
event.origin() == expected_origin && event.source().is_some_and(|source| js_sys::Object::is(source.as_ref(), window.as_ref()))
|
||||
}
|
||||
fn send(window: &web_sys::Window, origin: &str, value: serde_json::Value) -> Result<(), String> {
|
||||
let text = serde_json::to_string(&value).map_err(|e| e.to_string())?;
|
||||
if text.len() > MAX_MESSAGE {
|
||||
return Err("Presenter message is too large".into());
|
||||
}
|
||||
window.post_message(&wasm_bindgen::JsValue::from_str(&text), origin).map_err(error)
|
||||
}
|
||||
struct State {
|
||||
popup: Option<web_sys::Window>,
|
||||
token: String,
|
||||
commands: VecDeque<Command>,
|
||||
ready: bool,
|
||||
refresh_images: bool,
|
||||
}
|
||||
pub struct BrowserHost {
|
||||
state: Rc<RefCell<State>>,
|
||||
origin: String,
|
||||
listener: Closure<dyn FnMut(web_sys::MessageEvent)>,
|
||||
button: Option<(web_sys::HtmlElement, Closure<dyn FnMut(web_sys::Event)>)>,
|
||||
images: DeltaCache,
|
||||
}
|
||||
impl BrowserHost {
|
||||
pub fn new(ctx: egui::Context) -> Result<Self, String> {
|
||||
let window = web_sys::window().ok_or("No browser window")?;
|
||||
let origin = origin()?;
|
||||
let state = Rc::new(RefCell::new(State { popup: None, token: String::new(), commands: VecDeque::new(), ready: false, refresh_images: true }));
|
||||
let weak = Rc::downgrade(&state);
|
||||
let expected = origin.clone();
|
||||
let listener = Closure::new(move |event: web_sys::MessageEvent| {
|
||||
let Some(state) = weak.upgrade() else { return };
|
||||
let Ok(mut state) = state.try_borrow_mut() else { return };
|
||||
let Some(popup) = state.popup.as_ref() else { return };
|
||||
if !same_source(&event, popup, &expected) {
|
||||
return;
|
||||
}
|
||||
let Some(text) = event.data().as_string().filter(|s| s.len() <= 8192) else { return };
|
||||
let Ok(value) = serde_json::from_str::<serde_json::Value>(&text) else { return };
|
||||
if value.get("token").and_then(|v| v.as_str()) != Some(state.token.as_str()) {
|
||||
return;
|
||||
}
|
||||
match value.get("kind").and_then(|v| v.as_str()) {
|
||||
Some("ready") => {
|
||||
state.ready = true;
|
||||
state.refresh_images = true;
|
||||
}
|
||||
Some("command") if state.commands.len() < MAX_COMMANDS => {
|
||||
if let Some(command) = value.get("command").and_then(|v| serde_json::from_value(v.clone()).ok()) {
|
||||
state.commands.push_back(command);
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
ctx.request_repaint();
|
||||
});
|
||||
window.add_event_listener_with_callback("message", listener.as_ref().unchecked_ref()).map_err(error)?;
|
||||
Ok(Self { state, origin, listener, button: None, images: DeltaCache::default() })
|
||||
}
|
||||
fn remove_button(&mut self) {
|
||||
if let Some((button, callback)) = self.button.take() {
|
||||
let _ = button.remove_event_listener_with_callback("click", callback.as_ref().unchecked_ref());
|
||||
button.remove();
|
||||
}
|
||||
}
|
||||
fn recovery_button(&mut self) {
|
||||
if self.button.is_some() {
|
||||
return;
|
||||
}
|
||||
let Some(document) = web_sys::window().and_then(|w| w.document()) else { return };
|
||||
let Some(body) = document.body() else { return };
|
||||
let Ok(element) = document.create_element("button") else { return };
|
||||
let Ok(button) = element.dyn_into::<web_sys::HtmlElement>() else { return };
|
||||
button.set_text_content(Some("Open presenter window"));
|
||||
let _ = button.set_attribute("style", "position:fixed;top:12px;right:12px;z-index:9999;padding:12px;color:white;background:#333;border:1px solid #aaa;border-radius:4px;cursor:pointer");
|
||||
let weak: Weak<RefCell<State>> = Rc::downgrade(&self.state);
|
||||
let button_copy = button.clone();
|
||||
let callback = Closure::new(move |_event: web_sys::Event| {
|
||||
if let Some(state) = weak.upgrade() {
|
||||
match open_popup(&state) {
|
||||
Ok(()) => {
|
||||
button_copy.set_text_content(Some("Opening presenter window…"));
|
||||
}
|
||||
Err(_) => {
|
||||
button_copy.set_text_content(Some("Popup blocked. Allow popups, then click to retry."));
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
if button.add_event_listener_with_callback("click", callback.as_ref().unchecked_ref()).is_ok() && body.append_child(&button).is_ok() {
|
||||
self.button = Some((button, callback));
|
||||
} else {
|
||||
let _ = button.remove_event_listener_with_callback("click", callback.as_ref().unchecked_ref());
|
||||
button.remove();
|
||||
}
|
||||
}
|
||||
}
|
||||
fn open_popup(state: &Rc<RefCell<State>>) -> Result<(), String> {
|
||||
let window = web_sys::window().ok_or("No browser window")?;
|
||||
let mut state = state.try_borrow_mut().map_err(|_| "Presenter is busy")?;
|
||||
if let Some(popup) = &state.popup
|
||||
&& !popup.closed().unwrap_or(true)
|
||||
{
|
||||
let _ = popup.focus();
|
||||
return Ok(());
|
||||
}
|
||||
let token = format!("{}-{}", js_sys::Date::now(), js_sys::Math::random());
|
||||
let url = web_sys::Url::new(&window.location().href().map_err(error)?).map_err(error)?;
|
||||
url.set_hash(&format!("{PREFIX}{token}"));
|
||||
let popup = window
|
||||
.open_with_url_and_target_and_features(&url.href(), "_blank", "popup,width=1100,height=700")
|
||||
.map_err(error)?
|
||||
.ok_or("Presenter popup was blocked. Use Open presenter window to retry.")?;
|
||||
if popup.closed().unwrap_or(true) {
|
||||
return Err("Browser isolation prevented connecting the presenter window".into());
|
||||
}
|
||||
state.popup = Some(popup);
|
||||
state.token = token;
|
||||
state.commands.clear();
|
||||
state.ready = false;
|
||||
Ok(())
|
||||
}
|
||||
impl Host for BrowserHost {
|
||||
fn open(&mut self) -> Result<(), String> {
|
||||
self.close();
|
||||
let result = open_popup(&self.state);
|
||||
if result.is_err() {
|
||||
self.recovery_button();
|
||||
}
|
||||
result
|
||||
}
|
||||
fn is_open(&self) -> bool {
|
||||
self.state.borrow().popup.as_ref().is_some_and(|w| !w.closed().unwrap_or(true))
|
||||
}
|
||||
fn commands(&mut self) -> Vec<Command> {
|
||||
self.state.borrow_mut().commands.drain(..).collect()
|
||||
}
|
||||
fn update(&mut self, snapshot: Snapshot) {
|
||||
if !self.is_open() {
|
||||
self.recovery_button();
|
||||
return;
|
||||
}
|
||||
self.remove_button();
|
||||
let mut state = self.state.borrow_mut();
|
||||
if !state.ready {
|
||||
return;
|
||||
}
|
||||
if let Some(popup) = state.popup.clone() {
|
||||
let token = state.token.clone();
|
||||
let sent = self.images.send(snapshot, &token, state.refresh_images, |delta, current_changed, next_changed| {
|
||||
send(
|
||||
&popup,
|
||||
&self.origin,
|
||||
serde_json::json!({"kind":"snapshot", "token":token, "snapshot":delta,
|
||||
"currentChanged":current_changed, "nextChanged":next_changed}),
|
||||
)
|
||||
});
|
||||
if sent.is_ok() {
|
||||
state.refresh_images = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
fn close(&mut self) {
|
||||
self.remove_button();
|
||||
let mut state = self.state.borrow_mut();
|
||||
if let Some(window) = state.popup.take() {
|
||||
let _ = window.close();
|
||||
}
|
||||
state.commands.clear();
|
||||
state.ready = false;
|
||||
state.token.clear();
|
||||
}
|
||||
}
|
||||
impl Drop for BrowserHost {
|
||||
fn drop(&mut self) {
|
||||
self.close();
|
||||
if let Some(window) = web_sys::window() {
|
||||
let _ = window.remove_event_listener_with_callback("message", self.listener.as_ref().unchecked_ref());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct Popup {
|
||||
token: String,
|
||||
origin: String,
|
||||
opener: web_sys::Window,
|
||||
snapshot: Rc<RefCell<Option<Snapshot>>>,
|
||||
last_received: Rc<RefCell<f64>>,
|
||||
listener: Closure<dyn FnMut(web_sys::MessageEvent)>,
|
||||
view: View,
|
||||
}
|
||||
impl Popup {
|
||||
pub fn new(token: String, ctx: egui::Context) -> Result<Self, String> {
|
||||
let window = web_sys::window().ok_or("No browser window")?;
|
||||
// `opener` is a WindowProxy from another realm; instanceof Window can fail across
|
||||
// realms. The browser API supplies a WindowProxy or null, and messages still verify
|
||||
// exact source identity and origin before any data is accepted.
|
||||
let opener = window.opener().map_err(error)?;
|
||||
if opener.is_null() || opener.is_undefined() {
|
||||
return Err("The audience window is unavailable".into());
|
||||
}
|
||||
let opener: web_sys::Window = opener.unchecked_into();
|
||||
let origin = origin()?;
|
||||
let snapshot: Rc<RefCell<Option<Snapshot>>> = Rc::new(RefCell::new(None));
|
||||
let last_received = Rc::new(RefCell::new(js_sys::Date::now()));
|
||||
let received = last_received.clone();
|
||||
let target = snapshot.clone();
|
||||
let expected_origin = origin.clone();
|
||||
let expected_opener = opener.clone();
|
||||
let expected_token = token.clone();
|
||||
let listener = Closure::new(move |event: web_sys::MessageEvent| {
|
||||
if !same_source(&event, &expected_opener, &expected_origin) {
|
||||
return;
|
||||
}
|
||||
let Some(text) = event.data().as_string().filter(|s| s.len() <= MAX_MESSAGE) else { return };
|
||||
let Ok(value) = serde_json::from_str::<serde_json::Value>(&text) else { return };
|
||||
if value.get("token").and_then(|v| v.as_str()) != Some(expected_token.as_str())
|
||||
|| value.get("kind").and_then(|v| v.as_str()) != Some("snapshot")
|
||||
{
|
||||
return;
|
||||
}
|
||||
if let Some(mut data) = value.get("snapshot").and_then(|v| serde_json::from_value::<Snapshot>(v.clone()).ok()) {
|
||||
if let Some(previous) = target.borrow().as_ref() {
|
||||
if value.get("currentChanged").and_then(|v| v.as_bool()) == Some(false) {
|
||||
data.current = previous.current.clone();
|
||||
}
|
||||
if value.get("nextChanged").and_then(|v| v.as_bool()) == Some(false) {
|
||||
data.next = previous.next.clone();
|
||||
}
|
||||
if data.slides.is_none() {
|
||||
data.slides = previous.slides.clone();
|
||||
}
|
||||
}
|
||||
*target.borrow_mut() = Some(data);
|
||||
*received.borrow_mut() = js_sys::Date::now();
|
||||
ctx.request_repaint();
|
||||
}
|
||||
});
|
||||
window.add_event_listener_with_callback("message", listener.as_ref().unchecked_ref()).map_err(error)?;
|
||||
if let Some(document) = window.document() {
|
||||
document.set_title("Presenter View — DeckCraft");
|
||||
}
|
||||
let popup = Self { token, origin, opener, snapshot, last_received, listener, view: View::default() };
|
||||
send(&popup.opener, &popup.origin, serde_json::json!({"kind":"ready","token":popup.token}))?;
|
||||
Ok(popup)
|
||||
}
|
||||
}
|
||||
impl eframe::App for Popup {
|
||||
fn ui(&mut self, ui: &mut egui::Ui, _frame: &mut eframe::Frame) {
|
||||
let snapshot = self.snapshot.borrow();
|
||||
let notes = snapshot.as_ref().map_or("", |snapshot| snapshot.notes.as_str());
|
||||
if !self.view.prepare(ui.ctx(), notes) {
|
||||
return;
|
||||
}
|
||||
ui.ctx().request_repaint_after(std::time::Duration::from_millis(500));
|
||||
if self.opener.closed().unwrap_or(true) {
|
||||
ui.heading("The audience window has closed.");
|
||||
return;
|
||||
}
|
||||
if js_sys::Date::now() - *self.last_received.borrow() > 10_000.0 {
|
||||
ui.heading("The audience window is not responding.");
|
||||
ui.label("Return to the audience window to resume the connection.");
|
||||
return;
|
||||
}
|
||||
egui::CentralPanel::default().show(ui, |ui| {
|
||||
if let Some(snapshot) = snapshot.as_ref() {
|
||||
for command in self.view.ui(ui, snapshot) {
|
||||
let _ = send(&self.opener, &self.origin, serde_json::json!({"kind":"command","token":self.token,"command":command}));
|
||||
}
|
||||
} else {
|
||||
ui.label("Waiting for the audience window…");
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
impl Drop for Popup {
|
||||
fn drop(&mut self) {
|
||||
if let Some(window) = web_sys::window() {
|
||||
let _ = window.remove_event_listener_with_callback("message", self.listener.as_ref().unchecked_ref());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -24,18 +24,8 @@ pub fn start() {
|
||||
log::error!("missing <canvas id=\"{CANVAS_ID}\">");
|
||||
return;
|
||||
};
|
||||
// Host modes (`craft-web-host`): `?host=parent`, a same-origin page framing DeckCraft,
|
||||
// opens presentations in it and stores what it saves (`host`); `data-host="page"` on the
|
||||
// canvas opts a page in that runs DeckCraft in its own document. The presenter popup is
|
||||
// never the framed page.
|
||||
let hosted = crate::presenter::token().is_none();
|
||||
let bridge = if hosted { crate::host::Bridge::detect(&canvas) } else { None };
|
||||
if hosted && bridge.is_none() && query_param("host").as_deref() == Some("parent") {
|
||||
log::warn!("?host=parent needs a framing page on the same origin; saves download instead");
|
||||
}
|
||||
let app_bridge = bridge.clone();
|
||||
let mut options = eframe::WebOptions::default();
|
||||
if query_has("webgl")
|
||||
if query().contains("webgl")
|
||||
&& let eframe::egui_wgpu::WgpuSetup::CreateNew(create) = &mut options.wgpu_options.wgpu_setup
|
||||
{
|
||||
create.instance_descriptor.backends = eframe::wgpu::Backends::GL;
|
||||
@@ -49,48 +39,23 @@ pub fn start() {
|
||||
log::info!("deckcraft-web {}: wgpu backend {:?}", env!("CARGO_PKG_VERSION"), rs.adapter.get_info().backend);
|
||||
}
|
||||
deckcraft_ui_egui::install_ime_pass_hooks(&cc.egui_ctx);
|
||||
if let Some(token) = crate::presenter::token() {
|
||||
let presenter = crate::presenter::Popup::new(token, cc.egui_ctx.clone()).map_err(std::io::Error::other)?;
|
||||
return Ok(Box::new(presenter) as Box<dyn eframe::App>);
|
||||
}
|
||||
let inbox: Inbox = Inbox::default();
|
||||
let mut services = services(inbox.clone(), cc.egui_ctx.clone());
|
||||
if let Some(bridge) = &app_bridge {
|
||||
bridge.connect(&mut services, cc.egui_ctx.clone());
|
||||
}
|
||||
let mut app = SlideApp::new(Session::new(), services);
|
||||
// In host mode the parent sends the presentation to edit; no sample meanwhile.
|
||||
if app_bridge.is_some() {
|
||||
if let Err(e) = app.start_for_host() {
|
||||
log::error!("new presentation: {e}");
|
||||
}
|
||||
} else if !query_has("blank") {
|
||||
let mut app = SlideApp::new(Session::new(), services(inbox.clone(), cc.egui_ctx.clone()));
|
||||
let q = query();
|
||||
if !q.contains("blank") {
|
||||
if let Err(e) = deckcraft_engine::sample::open_sample(&mut app.session) {
|
||||
log::error!("sample deck: {e}");
|
||||
}
|
||||
} else if let Err(e) = app.run("file.new", serde_json::json!({})) {
|
||||
log::error!("new presentation: {e}");
|
||||
}
|
||||
if query_has("show") {
|
||||
if q.contains("show") {
|
||||
app.start_show(0, false);
|
||||
}
|
||||
if app_bridge.is_some()
|
||||
&& let Some(author) = query_param("author")
|
||||
{
|
||||
crate::host::Bridge::set_author(&mut app, &author);
|
||||
}
|
||||
crate::api::install(&mut app, cc.egui_ctx.clone());
|
||||
Ok(Box::new(WebShell { app, inbox, bridge: app_bridge }))
|
||||
Ok(Box::new(WebShell { app, inbox }))
|
||||
}),
|
||||
)
|
||||
.await;
|
||||
// Ready only once the runner is up: starting can still fail after the app is made.
|
||||
if let Some(bridge) = &bridge {
|
||||
match &result {
|
||||
Ok(()) => bridge.post_ready(),
|
||||
Err(e) => bridge.post_failed(&format!("{e:?}")),
|
||||
}
|
||||
}
|
||||
if let Some(el) = document.get_element_by_id(LOADING_ID) {
|
||||
match result {
|
||||
Ok(()) => el.remove(),
|
||||
@@ -104,41 +69,17 @@ fn query() -> String {
|
||||
web_sys::window().and_then(|w| w.location().search().ok()).unwrap_or_default()
|
||||
}
|
||||
|
||||
fn query_param(key: &str) -> Option<String> {
|
||||
web_sys::UrlSearchParams::new_with_str(&query()).ok()?.get(key)
|
||||
}
|
||||
|
||||
/// Whether the URL has the flag `key` (`?show`, `&blank=1`). A whole parameter, never a substring:
|
||||
/// `?author=` is free text.
|
||||
fn query_has(key: &str) -> bool {
|
||||
web_sys::UrlSearchParams::new_with_str(&query()).is_ok_and(|q| q.has(key))
|
||||
}
|
||||
|
||||
/// Wraps the app to read dropped files asynchronously (browsers can't read them synchronously)
|
||||
/// and feed them through the inbox.
|
||||
struct WebShell {
|
||||
app: SlideApp,
|
||||
inbox: Inbox,
|
||||
/// The parent page that keeps the files (`?host=parent`).
|
||||
bridge: Option<crate::host::Bridge>,
|
||||
}
|
||||
|
||||
impl eframe::App for WebShell {
|
||||
/// Delegate one eframe logic pass to the wrapped application, opening the files the parent
|
||||
/// page sent first and telling it about unsaved changes after.
|
||||
/// Delegate one eframe logic pass to the wrapped application.
|
||||
fn logic(&mut self, ctx: &egui::Context, _frame: &mut eframe::Frame) {
|
||||
if let Some(bridge) = &self.bridge {
|
||||
bridge.open_files(&mut self.app);
|
||||
}
|
||||
self.app.logic(ctx);
|
||||
if let Some(bridge) = &self.bridge {
|
||||
bridge.report_dirty(&self.app);
|
||||
}
|
||||
if self.app.show.is_none()
|
||||
&& let Some(host) = self.app.services.presenter.as_mut()
|
||||
{
|
||||
host.close();
|
||||
}
|
||||
}
|
||||
|
||||
/// Consume browser drops with fresh input, capture the identity before the asynchronous read,
|
||||
@@ -177,15 +118,7 @@ impl eframe::App for WebShell {
|
||||
/// back to `open_async` (browser picker → inbox) and `download`.
|
||||
fn services(inbox: Inbox, ctx: egui::Context) -> Services {
|
||||
let open_inbox = inbox.clone();
|
||||
let presenter = match crate::presenter::BrowserHost::new(ctx.clone()) {
|
||||
Ok(host) => Some(Box::new(host) as Box<dyn deckcraft_ui_egui::presenter::Host>),
|
||||
Err(error) => {
|
||||
log::error!("Presenter window: {error}");
|
||||
None
|
||||
}
|
||||
};
|
||||
Services {
|
||||
presenter,
|
||||
open_async: Some(Box::new(move |purpose: &str, request: u64| {
|
||||
let inbox = open_inbox.clone();
|
||||
let ctx = ctx.clone();
|
||||
|
||||
@@ -14,9 +14,7 @@ workspace = true
|
||||
deckcraft-engine = { workspace = true }
|
||||
deckcraft-ui-egui = { workspace = true }
|
||||
deckcraft-media = { workspace = true }
|
||||
# `clipboard` is the OS clipboard (Ctrl+C/X/V with other apps). Without it egui simulates one inside
|
||||
# the app; the egui 0.37 migration (#1350) dropped it from this list.
|
||||
eframe = { workspace = true, features = ["default_fonts", "wgpu", "accesskit", "clipboard", "links", "wayland", "x11"] }
|
||||
eframe = { workspace = true, features = ["default_fonts", "wgpu", "accesskit", "links", "wayland", "x11"] }
|
||||
egui = { workspace = true }
|
||||
serde_json = { workspace = true }
|
||||
log = { workspace = true }
|
||||
|
||||
@@ -148,19 +148,7 @@ pub fn rotate(dir: &Path) -> std::io::Result<(PathBuf, File)> {
|
||||
/// One formatted log line: `<timestamp> <LEVEL> [<thread>] <target>: <message>\n`.
|
||||
pub fn format_line(ts: &str, level: log::Level, thread: &str, target: &str, message: &str) -> String {
|
||||
let message = message.strip_suffix('\n').unwrap_or(message);
|
||||
// Messages can embed document-controlled text (a part name, a relationship id): control
|
||||
// characters other than tab are escaped and each continuation line is marked, so such text
|
||||
// cannot start a line that looks like a record of its own.
|
||||
let mut text = String::with_capacity(message.len());
|
||||
for c in message.chars() {
|
||||
match c {
|
||||
'\n' => text.push_str("\n | "),
|
||||
'\t' => text.push(c),
|
||||
c if c.is_control() => text.extend(c.escape_default()),
|
||||
c => text.push(c),
|
||||
}
|
||||
}
|
||||
format!("{ts} {level:<5} [{thread}] {target}: {text}\n")
|
||||
format!("{ts} {level:<5} [{thread}] {target}: {message}\n")
|
||||
}
|
||||
|
||||
/// Where formatted lines go besides standard error: kept in memory until a file is attached,
|
||||
@@ -458,13 +446,8 @@ mod tests {
|
||||
),
|
||||
"2026-10-08T07:59:17.728Z WARN [main] deckcraft_pptx::opc: pptx: skipping oversized part ppt/media/image1.png\n"
|
||||
);
|
||||
// A multi-line message keeps its lines, each continuation marked; the record still ends in
|
||||
// exactly one newline.
|
||||
assert!(format_line("t", log::Level::Error, "w", "x", "a\nb\n").ends_with("x: a\n | b\n"));
|
||||
// Injected line breaks and escape sequences cannot forge a record.
|
||||
let line = format_line("t", log::Level::Warn, "w", "x", "part a\r\n2026 ERROR [m] y: forged\x1b[2J");
|
||||
assert_eq!(line.matches('\n').count(), 2);
|
||||
assert!(!line.contains('\r') && !line.contains('\x1b'));
|
||||
// A multi-line message keeps its lines; the record still ends in exactly one newline.
|
||||
assert!(format_line("t", log::Level::Error, "w", "x", "a\nb\n").ends_with("x: a\nb\n"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
@@ -51,9 +51,6 @@ impl eframe::App for App {
|
||||
}
|
||||
fn on_exit(&mut self) {
|
||||
save_prefs(&self.0);
|
||||
// The window is closing for good: a ready update's helper starts now and installs it once
|
||||
// the process has exited.
|
||||
self.0.start_pending_update();
|
||||
// macOS (#279): end the process here, before eframe tears the window down. AccessKit's
|
||||
// macOS adapter subclasses the window's content view and restores its class on teardown;
|
||||
// AppKit's Touch Bar support observes that view (KVO), so in the next display cycle
|
||||
@@ -63,8 +60,6 @@ impl eframe::App for App {
|
||||
// nothing after it is needed: the preferences are saved and the log file is unbuffered.
|
||||
// `on_exit` only runs once the app exists, never on a failed start. Same as GridCraft #117.
|
||||
#[cfg(target_os = "macos")]
|
||||
self.0.cleanup_print_previews();
|
||||
#[cfg(target_os = "macos")]
|
||||
std::process::exit(0);
|
||||
}
|
||||
}
|
||||
@@ -106,13 +101,7 @@ fn save_prefs(app: &SlideApp) {
|
||||
}
|
||||
}
|
||||
|
||||
/// The desktop services. `native_wayland`: the window is a Wayland client (not Xwayland).
|
||||
fn services(native_wayland: bool) -> Services {
|
||||
// On native Wayland, egui-winit owns the clipboard through winit's data device, which carries
|
||||
// text and PNG but no HTML. `pasteboard` (arboard, built for X11 only) would put a copy on the
|
||||
// Xwayland selection instead and skip egui's Wayland claim, so Ctrl+V would read something
|
||||
// else. There, text goes through egui and pictures arrive as `PasteImage` events.
|
||||
let pasteboard = !native_wayland;
|
||||
fn services() -> Services {
|
||||
Services {
|
||||
pick_open: Some(Box::new(|purpose: &str| {
|
||||
let d = rfd::FileDialog::new();
|
||||
@@ -135,9 +124,9 @@ fn services(native_wayland: bool) -> Services {
|
||||
pick_save: Some(Box::new(|name: &str| rfd::FileDialog::new().set_file_name(name).save_file().map(|p| p.to_string_lossy().to_string()))),
|
||||
read: Some(Box::new(|p: &str| std::fs::read(p).map_err(|e| e.to_string()))),
|
||||
write: Some(Box::new(|p: &str, b: &[u8]| std::fs::write(p, b).map_err(|e| e.to_string()))),
|
||||
clipboard_image: pasteboard.then(|| Box::new(pasteboard::image_png) as Box<dyn FnMut() -> Option<Vec<u8>>>),
|
||||
clipboard_html: pasteboard.then(|| Box::new(pasteboard::html) as Box<dyn FnMut() -> Option<String>>),
|
||||
clipboard_set_html: pasteboard.then(|| Box::new(pasteboard::set_html) as Box<dyn FnMut(&str, &str) -> bool>),
|
||||
clipboard_image: Some(Box::new(pasteboard::image_png)),
|
||||
clipboard_html: Some(Box::new(pasteboard::html)),
|
||||
clipboard_set_html: Some(Box::new(pasteboard::set_html)),
|
||||
// Media is mixed and clocked by the output device; `DECKCRAFT_NO_AUDIO` plays silently.
|
||||
audio_out: std::env::var_os("DECKCRAFT_NO_AUDIO")
|
||||
.is_none()
|
||||
@@ -146,14 +135,6 @@ fn services(native_wayland: bool) -> Services {
|
||||
}
|
||||
}
|
||||
|
||||
/// Displays winit lists for `window`, counting an output Wayland lists twice once. `None` when it
|
||||
/// lists none: Presenter View then stays as it was.
|
||||
#[cfg(all(unix, not(target_os = "macos")))]
|
||||
fn display_count(window: &winit::window::Window) -> Option<usize> {
|
||||
let n = window.available_monitors().collect::<std::collections::BTreeSet<_>>().len();
|
||||
(n > 0).then_some(n)
|
||||
}
|
||||
|
||||
const APP_ID: &str = "ai.storyteller.deckcraft";
|
||||
|
||||
const USAGE: &str = "\
|
||||
@@ -264,11 +245,6 @@ fn main() -> eframe::Result {
|
||||
false
|
||||
}
|
||||
};
|
||||
// `Default` lets winit choose, and winit takes Wayland whenever `WAYLAND_DISPLAY` is set.
|
||||
#[cfg(all(unix, not(target_os = "macos")))]
|
||||
let native_wayland = !x11 && std::env::var_os("WAYLAND_DISPLAY").is_some_and(|w| !w.is_empty());
|
||||
#[cfg(not(all(unix, not(target_os = "macos"))))]
|
||||
let native_wayland = false;
|
||||
#[cfg(all(unix, not(target_os = "macos")))]
|
||||
if x11 {
|
||||
options.event_loop_builder = Some(Box::new(|b| {
|
||||
@@ -290,12 +266,7 @@ fn main() -> eframe::Result {
|
||||
options,
|
||||
Box::new(move |cc| {
|
||||
deckcraft_ui_egui::install_ime_pass_hooks(&cc.egui_ctx);
|
||||
let mut app = SlideApp::new(Session::new(), services(native_wayland));
|
||||
// Presenter View asks winit how many displays there are when a show starts.
|
||||
#[cfg(all(unix, not(target_os = "macos")))]
|
||||
if let Some(window) = cc.winit_window().cloned() {
|
||||
app.services.displays = Some(Box::new(move || display_count(&window)));
|
||||
}
|
||||
let mut app = SlideApp::new(Session::new(), services());
|
||||
load_prefs(&mut app);
|
||||
app.enable_update_checks();
|
||||
// AutoRecover: reopen what a previous run left unsaved, then keep it current.
|
||||
@@ -362,26 +333,6 @@ mod tests {
|
||||
assert_eq!(stdout.contains("on_exit returned"), !cfg!(target_os = "macos"), "{stdout}");
|
||||
}
|
||||
|
||||
/// The egui 0.37 migration (#1350) left `clipboard` out of eframe's features. egui then
|
||||
/// simulated a clipboard inside the app: nothing pasted from other apps, and Ctrl+C/Ctrl+V
|
||||
/// lost text that went out as HTML. Keep the OS clipboard on.
|
||||
#[test]
|
||||
fn eframe_uses_the_os_clipboard() {
|
||||
let manifest = include_str!("../Cargo.toml");
|
||||
let eframe = manifest.lines().find(|l| l.starts_with("eframe = ")).unwrap_or_default();
|
||||
assert!(eframe.contains("\"clipboard\""), "eframe must enable the OS clipboard: {eframe}");
|
||||
}
|
||||
|
||||
/// On native Wayland, the X11-only pasteboard would bypass egui's Wayland clipboard; elsewhere
|
||||
/// it carries HTML and pictures beside egui's text.
|
||||
#[test]
|
||||
fn the_pasteboard_stays_off_native_wayland() {
|
||||
let s = services(true);
|
||||
assert!(s.clipboard_html.is_none() && s.clipboard_set_html.is_none() && s.clipboard_image.is_none());
|
||||
let s = services(false);
|
||||
assert!(s.clipboard_html.is_some() && s.clipboard_set_html.is_some() && s.clipboard_image.is_some());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn help_exits_instead_of_opening_a_file() {
|
||||
assert!(matches!(startup(None, ["--help".to_string()]), Startup::Help));
|
||||
|
||||
@@ -1,10 +1,13 @@
|
||||
# Contributor names for the About window.
|
||||
# Contributor names for the About window (craftrules standards/contributors.md).
|
||||
#
|
||||
# Everyone who contributes is credited by GitHub username automatically. Anything more is up to
|
||||
# you: add YOUR OWN entry below, in your own PR (commit it yourself). Never add someone else; a
|
||||
# maintainer may add a person only when that person asked, recording where in consent_source.
|
||||
#
|
||||
# Copy this block, replacing your-github-username (every field is optional):
|
||||
# Easiest: python3 ../../craftrules/scripts/contributors.py --add-me . \
|
||||
# [--real-name "Ada Lovelace"] [--display-name "Ada"] [--sync-github-name]
|
||||
#
|
||||
# Or copy this block, replacing your-github-username (every field is optional):
|
||||
#
|
||||
# [people.your-github-username]
|
||||
# real_name = "Your Name" # shown when the viewer picks "Real name"
|
||||
@@ -68,8 +71,3 @@ real_name = "Sofia Alves Figueredo"
|
||||
|
||||
[people.dylanmtaylor]
|
||||
sync_github_name = true
|
||||
|
||||
[people.marshiyar]
|
||||
display_name = "Arshiya"
|
||||
real_name = "Arshiya Rahgozar"
|
||||
sync_github_name = false
|
||||
|
||||
+29
-59
@@ -1,7 +1,5 @@
|
||||
//! Morph: matching shapes between two slides, interpolating their boxes and building the frames.
|
||||
|
||||
use std::collections::{HashMap, HashSet, VecDeque};
|
||||
|
||||
use deckcraft_model::resolve::{self, Ctx};
|
||||
use deckcraft_model::{ColorRef, Fill, Geom, Presentation, Rgba, Shape, ShapeId, ShapeKind, Slide, Xfrm, walk};
|
||||
|
||||
@@ -17,66 +15,48 @@ fn text_of(s: &Shape) -> String {
|
||||
/// kind and preset geometry if neither carries text. A text box with neither a name nor its text on
|
||||
/// the other slide stays unpaired and fades in place, as in PowerPoint (#119). Each shape is used
|
||||
/// once; top-level shapes only (groups move whole).
|
||||
///
|
||||
/// Each pass compares a key of the two shapes, so the shapes of `b` are indexed by key once per
|
||||
/// pass: matching costs the sum of the two shape lists, not their product.
|
||||
pub fn morph_pairs(a: &Slide, b: &Slide) -> Vec<(ShapeId, ShapeId)> {
|
||||
let mut pairs: Vec<(ShapeId, ShapeId)> = Vec::new();
|
||||
let mut used_a = vec![false; a.shapes.len()];
|
||||
let mut used_b = vec![false; b.shapes.len()];
|
||||
// Each shape's text, worked out once rather than inside every comparison.
|
||||
let text_a: Vec<String> = a.shapes.iter().map(text_of).collect();
|
||||
let text_b: Vec<String> = b.shapes.iter().map(text_of).collect();
|
||||
let forced = |s: &Shape, _: &str| (s.name.starts_with("!!")).then(|| MorphKey::Name(s.name.clone()));
|
||||
let named = |s: &Shape, _: &str| (!s.name.is_empty()).then(|| MorphKey::Name(s.name.clone()));
|
||||
let same_text = |s: &Shape, t: &str| (!t.trim().is_empty()).then(|| MorphKey::Text(s.kind_name(), t.to_string()));
|
||||
let same_geom = |s: &Shape, t: &str| match s.geom.preset_name() {
|
||||
Some(g) if t.trim().is_empty() => Some(MorphKey::Geom(s.kind_name(), g.to_string())),
|
||||
_ => None,
|
||||
};
|
||||
let passes: [&dyn Fn(&Shape, &str) -> Option<MorphKey>; 4] = [&forced, &named, &same_text, &same_geom];
|
||||
for key in passes {
|
||||
// Unused shapes of `b` by key, in slide order: the front of each queue is the first unused
|
||||
// match, which is the one a search through `b` would find.
|
||||
let mut index: HashMap<MorphKey, VecDeque<usize>> = HashMap::new();
|
||||
for (j, sb) in b.shapes.iter().enumerate() {
|
||||
if used_b.get(j).copied().unwrap_or(true) {
|
||||
continue;
|
||||
let passes: [&dyn Fn(&Shape, &Shape) -> bool; 4] = [
|
||||
&|x, y| x.name.starts_with("!!") && x.name == y.name,
|
||||
&|x, y| !x.name.is_empty() && x.name == y.name,
|
||||
&|x, y| {
|
||||
if x.kind_name() != y.kind_name() {
|
||||
return false;
|
||||
}
|
||||
if let Some(k) = key(sb, text_b.get(j).map_or("", String::as_str)) {
|
||||
index.entry(k).or_default().push_back(j);
|
||||
}
|
||||
}
|
||||
if index.is_empty() {
|
||||
continue;
|
||||
}
|
||||
let tx = text_of(x);
|
||||
!tx.trim().is_empty() && tx == text_of(y)
|
||||
},
|
||||
&|x, y| {
|
||||
x.kind_name() == y.kind_name()
|
||||
&& x.geom.preset_name().is_some()
|
||||
&& x.geom.preset_name() == y.geom.preset_name()
|
||||
&& text_of(x).trim().is_empty()
|
||||
&& text_of(y).trim().is_empty()
|
||||
},
|
||||
];
|
||||
for pass in passes {
|
||||
for (i, sa) in a.shapes.iter().enumerate() {
|
||||
if used_a.get(i).copied().unwrap_or(true) {
|
||||
continue;
|
||||
}
|
||||
let Some(k) = key(sa, text_a.get(i).map_or("", String::as_str)) else { continue };
|
||||
let Some(j) = index.get_mut(&k).and_then(VecDeque::pop_front) else { continue };
|
||||
let Some(sb) = b.shapes.get(j) else { continue };
|
||||
if let Some(u) = used_a.get_mut(i) {
|
||||
*u = true;
|
||||
let found = b.shapes.iter().enumerate().find(|(j, sb)| !used_b.get(*j).copied().unwrap_or(true) && pass(sa, sb));
|
||||
if let Some((j, sb)) = found {
|
||||
if let Some(u) = used_a.get_mut(i) {
|
||||
*u = true;
|
||||
}
|
||||
if let Some(u) = used_b.get_mut(j) {
|
||||
*u = true;
|
||||
}
|
||||
pairs.push((sa.id, sb.id));
|
||||
}
|
||||
if let Some(u) = used_b.get_mut(j) {
|
||||
*u = true;
|
||||
}
|
||||
pairs.push((sa.id, sb.id));
|
||||
}
|
||||
}
|
||||
pairs
|
||||
}
|
||||
|
||||
/// What one [`morph_pairs`] pass matches shapes on.
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Hash)]
|
||||
enum MorphKey {
|
||||
Name(String),
|
||||
Text(&'static str, String),
|
||||
Geom(&'static str, String),
|
||||
}
|
||||
|
||||
fn lerp(a: f64, b: f64, t: f64) -> f64 {
|
||||
let v = a + (b - a) * t;
|
||||
if v.is_finite() {
|
||||
@@ -276,23 +256,13 @@ pub fn morph_frame(pres: &Presentation, a: &Slide, b: &Slide, t: f64, hidden_b:
|
||||
f.shapes.push((sh, old));
|
||||
id
|
||||
};
|
||||
// Lookups by id, built once (the first shape or pair with an id wins, as a search would).
|
||||
let paired_a: HashSet<ShapeId> = pairs.iter().map(|(ia, _)| *ia).collect();
|
||||
let mut partner: HashMap<ShapeId, ShapeId> = HashMap::with_capacity(pairs.len());
|
||||
for (ia, ib) in &pairs {
|
||||
partner.entry(*ib).or_insert(*ia);
|
||||
}
|
||||
let mut a_by_id: HashMap<ShapeId, &Shape> = HashMap::with_capacity(a.shapes.len());
|
||||
for x in &a.shapes {
|
||||
a_by_id.entry(x.id).or_insert(x);
|
||||
}
|
||||
for sa in &a.shapes {
|
||||
if !paired_a.contains(&sa.id) {
|
||||
if !pairs.iter().any(|(ia, _)| *ia == sa.id) {
|
||||
push(sa.clone(), true, 1.0 - s);
|
||||
}
|
||||
}
|
||||
for sb in &b.shapes {
|
||||
let pair = partner.get(&sb.id).and_then(|ia| a_by_id.get(ia).copied());
|
||||
let pair = pairs.iter().find(|(_, ib)| *ib == sb.id).and_then(|(ia, _)| a.shapes.iter().find(|x| x.id == *ia));
|
||||
let Some(sa) = pair else {
|
||||
if !hidden_b(sb.id) {
|
||||
push(sb.clone(), false, s);
|
||||
|
||||
@@ -1155,58 +1155,3 @@ fn many_by_paragraph_animations_on_one_shape_share_paragraph_targets() {
|
||||
assert_eq!(tl.effects().len(), 6_000);
|
||||
assert_eq!(tl.paragraph_targets(ShapeId(1)), vec![0, 1, 2]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn morph_pairs_take_the_first_unused_match_in_slide_order() {
|
||||
let mut a = Slide::default();
|
||||
let mut b = Slide::default();
|
||||
a.shapes = vec![shape(1, "Same"), shape(2, "Same"), shape(3, "Other")];
|
||||
b.shapes = vec![shape(11, "Same"), shape(12, "Same"), shape(13, "Same")];
|
||||
// By name first, in slide order; the shape of `a` left over pairs by kind and geometry with
|
||||
// the shape of `b` left over.
|
||||
assert_eq!(morph_pairs(&a, &b), [(ShapeId(1), ShapeId(11)), (ShapeId(2), ShapeId(12)), (ShapeId(3), ShapeId(13))]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn morph_of_large_slides_stays_linear() {
|
||||
let n = 20_000u32;
|
||||
let text = |i: u32, side: &str| Some(TextBody::from_text(&format!("{side} {i}")));
|
||||
let mut a = Slide::default();
|
||||
let mut b = Slide::default();
|
||||
// Text no shape on the other slide shares: every pass looks for a partner and finds none.
|
||||
a.shapes = (0..n).map(|i| Shape { text: text(i, "a"), ..shape(i + 1, "") }).collect();
|
||||
b.shapes = (0..n).map(|i| Shape { text: text(i, "b"), ..shape(n + i + 1, "") }).collect();
|
||||
// One named pair at opposite ends.
|
||||
a.shapes[0].name = "Logo".into();
|
||||
b.shapes[n as usize - 1].name = "Logo".into();
|
||||
// No wall-clock bound (it would flake on a loaded runner): at this size a quadratic pairing
|
||||
// takes minutes, so a regression shows as a test that hangs rather than a failed assertion.
|
||||
assert_eq!(morph_pairs(&a, &b), [(ShapeId(1), ShapeId(2 * n))]);
|
||||
let p = deckcraft_model::defaults::new_presentation(None);
|
||||
let f = morph_frame(&p, &a, &b, 0.5, &|_| false);
|
||||
// Every shape fades in place; the named pair differs in text, so it cross-fades as two.
|
||||
assert_eq!(f.shapes.len(), 2 * n as usize);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn paragraph_states_of_a_large_build_stay_linear() {
|
||||
let n = 20_000usize;
|
||||
let mut s = slide_with(vec![]);
|
||||
let mut body = TextBody::from_text(&vec!["line"; n].join("\n"));
|
||||
// One first-level paragraph owning every other paragraph.
|
||||
for p in body.paragraphs.iter_mut().skip(1) {
|
||||
p.level = 1;
|
||||
}
|
||||
s.shapes[0].text = Some(body);
|
||||
let mut a = anim(1, AnimClass::Entrance, "fade", AnimStart::OnClick);
|
||||
a.text_build = TextBuild::ByParagraph;
|
||||
s.animations = vec![a];
|
||||
let tl = Timeline::new(&s, W, H);
|
||||
// No wall-clock bound, as above: a per-paragraph scan of the slide would take minutes here.
|
||||
assert_eq!(tl.paragraph_targets(ShapeId(1)).len(), n);
|
||||
for p in 0..n {
|
||||
assert!(!tl.state(ShapeId(1), Some(p), 0, 0.0).visible);
|
||||
assert!(tl.state(ShapeId(1), Some(p), 1, 0.0).visible);
|
||||
}
|
||||
assert_eq!(tl.animated_shapes(), [ShapeId(1)]);
|
||||
}
|
||||
|
||||
+12
-47
@@ -103,41 +103,6 @@ pub struct Timeline {
|
||||
/// By-paragraph builds animate first-level paragraphs; deeper paragraphs follow their parent:
|
||||
/// (shape, member paragraph, owner paragraph).
|
||||
groups: Vec<(ShapeId, usize, usize)>,
|
||||
/// Lookups built with the timeline, so evaluating a shape or paragraph costs its own effects
|
||||
/// rather than a scan of every effect, group and colour on the slide.
|
||||
index: TimelineIndex,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Default)]
|
||||
struct TimelineIndex {
|
||||
/// Effects (indices into `effects`, in order) by target shape and paragraph.
|
||||
by_target: HashMap<(ShapeId, Option<usize>), Vec<usize>>,
|
||||
/// Effects (indices into `effects`, in order) by target shape.
|
||||
by_shape: HashMap<ShapeId, Vec<usize>>,
|
||||
/// Owner paragraph of each member paragraph (the first group recorded wins).
|
||||
owner: HashMap<(ShapeId, usize), usize>,
|
||||
/// Groups (member, owner) by shape, in order.
|
||||
groups_by_shape: HashMap<ShapeId, Vec<(usize, usize)>>,
|
||||
/// Base colour by shape (the first recorded wins).
|
||||
color: HashMap<ShapeId, Rgba>,
|
||||
}
|
||||
|
||||
impl TimelineIndex {
|
||||
fn build(effects: &[Effect], groups: &[(ShapeId, usize, usize)], colors: &[(ShapeId, Rgba)]) -> TimelineIndex {
|
||||
let mut ix = TimelineIndex::default();
|
||||
for (i, e) in effects.iter().enumerate() {
|
||||
ix.by_target.entry((e.timing.shape, e.timing.paragraph)).or_default().push(i);
|
||||
ix.by_shape.entry(e.timing.shape).or_default().push(i);
|
||||
}
|
||||
for &(shape, member, owner) in groups {
|
||||
ix.owner.entry((shape, member)).or_insert(owner);
|
||||
ix.groups_by_shape.entry(shape).or_default().push((member, owner));
|
||||
}
|
||||
for &(shape, c) in colors {
|
||||
ix.color.entry(shape).or_insert(c);
|
||||
}
|
||||
ix
|
||||
}
|
||||
}
|
||||
|
||||
enum Status {
|
||||
@@ -359,7 +324,6 @@ impl Timeline {
|
||||
};
|
||||
tl.colors.push((id, c));
|
||||
}
|
||||
tl.index = TimelineIndex::build(&tl.effects, &tl.groups, &tl.colors);
|
||||
tl
|
||||
}
|
||||
|
||||
@@ -449,12 +413,8 @@ impl Timeline {
|
||||
|
||||
/// Paragraph indices of `shape` animated separately (by-paragraph builds).
|
||||
pub fn paragraph_targets(&self, shape: ShapeId) -> Vec<usize> {
|
||||
let mine = self.index.by_shape.get(&shape).map_or(&[][..], Vec::as_slice);
|
||||
let mut v: Vec<usize> = mine.iter().filter_map(|&i| self.effects.get(i)).filter_map(|e| e.timing.paragraph).collect();
|
||||
let owners: HashSet<usize> = v.iter().copied().collect();
|
||||
if let Some(groups) = self.index.groups_by_shape.get(&shape) {
|
||||
v.extend(groups.iter().filter(|(_, owner)| owners.contains(owner)).map(|(member, _)| *member));
|
||||
}
|
||||
let mut v: Vec<usize> = self.effects.iter().filter(|e| e.timing.shape == shape).filter_map(|e| e.timing.paragraph).collect();
|
||||
v.extend(self.groups.iter().filter(|g| g.0 == shape && v.contains(&g.2)).map(|g| g.1).collect::<Vec<_>>());
|
||||
v.sort_unstable();
|
||||
v.dedup();
|
||||
v
|
||||
@@ -462,8 +422,13 @@ impl Timeline {
|
||||
|
||||
/// Shapes with any effect.
|
||||
pub fn animated_shapes(&self) -> Vec<ShapeId> {
|
||||
let mut seen: HashSet<ShapeId> = HashSet::new();
|
||||
self.effects.iter().map(|e| e.timing.shape).filter(|s| seen.insert(*s)).collect()
|
||||
let mut v: Vec<ShapeId> = Vec::new();
|
||||
for e in &self.effects {
|
||||
if !v.contains(&e.timing.shape) {
|
||||
v.push(e.timing.shape);
|
||||
}
|
||||
}
|
||||
v
|
||||
}
|
||||
|
||||
/// State of `shape` (or of one of its paragraphs, `para`) when `step` main steps have been
|
||||
@@ -491,9 +456,9 @@ impl Timeline {
|
||||
triggers: &[TriggerPlay],
|
||||
stamps: &[StepStamp],
|
||||
) -> AnimState {
|
||||
let para = para.map(|p| self.index.owner.get(&(shape, p)).copied().unwrap_or(p));
|
||||
let mine = self.index.by_target.get(&(shape, para)).map_or(&[][..], Vec::as_slice).iter().filter_map(|&i| self.effects.get(i));
|
||||
let base_color = self.index.color.get(&shape).copied().unwrap_or(Rgba::rgb(0x15, 0x60, 0x82));
|
||||
let para = para.map(|p| self.groups.iter().find(|g| g.0 == shape && g.1 == p).map(|g| g.2).unwrap_or(p));
|
||||
let mine = self.effects.iter().filter(|e| e.timing.shape == shape && e.timing.paragraph == para);
|
||||
let base_color = self.colors.iter().find(|(s, _)| *s == shape).map(|(_, c)| *c).unwrap_or(Rgba::rgb(0x15, 0x60, 0x82));
|
||||
let mut st = AnimState::default();
|
||||
// Initial visibility comes from the first effect in the Animation Pane order.
|
||||
let mut ordered: Vec<&Effect> = mine.collect();
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
[package]
|
||||
name = "deckcraft-av1"
|
||||
description = "Clean-room AV1 decoder (AV1 Bitstream & Decoding Process Specification v1.0.0 with Errata 1)"
|
||||
version.workspace = true
|
||||
edition.workspace = true
|
||||
license.workspace = true
|
||||
rust-version.workspace = true
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
||||
[features]
|
||||
default = ["threads"]
|
||||
threads = ["dep:rayon"]
|
||||
|
||||
[dependencies]
|
||||
deckcraft-bitstream = { workspace = true }
|
||||
thiserror = { workspace = true }
|
||||
rayon = { workspace = true, optional = true }
|
||||
@@ -0,0 +1,124 @@
|
||||
//! MSB-first bit reader for OBU headers and the uncompressed frame header (spec 4.10 descriptors).
|
||||
|
||||
use crate::{Error, Result};
|
||||
|
||||
#[derive(Clone)]
|
||||
pub(crate) struct BitReader<'a> {
|
||||
data: &'a [u8],
|
||||
/// Position in bits.
|
||||
pos: usize,
|
||||
}
|
||||
|
||||
impl<'a> BitReader<'a> {
|
||||
pub fn new(data: &'a [u8]) -> Self {
|
||||
BitReader { data, pos: 0 }
|
||||
}
|
||||
|
||||
/// `get_position()`: bits consumed.
|
||||
pub fn position(&self) -> usize {
|
||||
self.pos
|
||||
}
|
||||
|
||||
pub fn byte_pos(&self) -> usize {
|
||||
self.pos.div_ceil(8)
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn bit(&mut self) -> Result<u32> {
|
||||
let byte = *self.data.get(self.pos >> 3).ok_or(Error::Truncated)?;
|
||||
let b = (byte >> (7 - (self.pos & 7))) & 1;
|
||||
self.pos += 1;
|
||||
Ok(b as u32)
|
||||
}
|
||||
|
||||
/// `f(n)`, n ≤ 32.
|
||||
pub fn f(&mut self, n: u32) -> Result<u32> {
|
||||
let mut x: u64 = 0;
|
||||
for _ in 0..n {
|
||||
x = (x << 1) | self.bit()? as u64;
|
||||
}
|
||||
Ok(x as u32)
|
||||
}
|
||||
|
||||
pub fn flag(&mut self) -> Result<bool> {
|
||||
Ok(self.bit()? == 1)
|
||||
}
|
||||
|
||||
/// `uvlc()`
|
||||
pub fn uvlc(&mut self) -> Result<u32> {
|
||||
let mut leading = 0u32;
|
||||
while self.bit()? == 0 {
|
||||
leading += 1;
|
||||
if leading > 40 {
|
||||
return Err(Error::Invalid("uvlc"));
|
||||
}
|
||||
}
|
||||
if leading >= 32 {
|
||||
return Ok(u32::MAX);
|
||||
}
|
||||
let v = self.f(leading)?;
|
||||
Ok(v + ((1u64 << leading) - 1) as u32)
|
||||
}
|
||||
|
||||
/// `le(n)`
|
||||
pub fn le(&mut self, n: u32) -> Result<u32> {
|
||||
let mut t = 0u32;
|
||||
for i in 0..n {
|
||||
t |= self.f(8)? << (8 * i);
|
||||
}
|
||||
Ok(t)
|
||||
}
|
||||
|
||||
/// `leb128()`
|
||||
pub fn leb128(&mut self) -> Result<u64> {
|
||||
let mut v = 0u64;
|
||||
for i in 0..8 {
|
||||
let b = self.f(8)? as u64;
|
||||
v |= (b & 0x7f) << (7 * i);
|
||||
if b & 0x80 == 0 {
|
||||
break;
|
||||
}
|
||||
}
|
||||
Ok(v)
|
||||
}
|
||||
|
||||
/// `su(n)`
|
||||
pub fn su(&mut self, n: u32) -> Result<i32> {
|
||||
let v = self.f(n)? as i64;
|
||||
let sign = 1i64 << (n - 1);
|
||||
Ok(if v & sign != 0 { (v - 2 * sign) as i32 } else { v as i32 })
|
||||
}
|
||||
|
||||
/// `ns(n)`
|
||||
pub fn ns(&mut self, n: u32) -> Result<u32> {
|
||||
if n <= 1 {
|
||||
return Ok(0);
|
||||
}
|
||||
let w = 32 - n.leading_zeros(); // FloorLog2(n) + 1
|
||||
let m = (1u32 << w) - n;
|
||||
let v = self.f(w - 1)?;
|
||||
if v < m {
|
||||
return Ok(v);
|
||||
}
|
||||
let extra = self.bit()?;
|
||||
Ok((v << 1) - m + extra)
|
||||
}
|
||||
|
||||
/// `byte_alignment()`
|
||||
pub fn byte_align(&mut self) {
|
||||
self.pos = self.pos.div_ceil(8) * 8;
|
||||
}
|
||||
}
|
||||
|
||||
/// Read a leb128 from a byte slice; returns (value, bytes used).
|
||||
pub(crate) fn leb128(data: &[u8]) -> Result<(u64, usize)> {
|
||||
let mut v = 0u64;
|
||||
for i in 0..8 {
|
||||
let b = *data.get(i).ok_or(Error::Truncated)? as u64;
|
||||
v |= (b & 0x7f) << (7 * i);
|
||||
if b & 0x80 == 0 {
|
||||
return Ok((v, i + 1));
|
||||
}
|
||||
}
|
||||
Ok((v, 8))
|
||||
}
|
||||
@@ -0,0 +1,314 @@
|
||||
//! CDEF (7.15).
|
||||
|
||||
use crate::frame::{FrameBuf, Plane};
|
||||
use crate::spec_tables::{CDEF_DIRECTIONS, CDEF_PRI_TAPS, CDEF_SEC_TAPS, CDEF_UV_DIR, DIV_TABLE};
|
||||
use crate::state::FrameState;
|
||||
|
||||
/// Run CDEF on `fs.cur`; returns the CdefFrame, or None when no block is filtered (CdefFrame
|
||||
/// equals CurrFrame).
|
||||
pub(crate) fn apply(fs: &FrameState) -> Option<FrameBuf> {
|
||||
let fh = &fs.fh;
|
||||
if fh.coded_lossless || fh.allow_intrabc || !fs.seq.enable_cdef {
|
||||
return None;
|
||||
}
|
||||
let (mi_rows, mi_cols) = (fh.mi_rows as usize, fh.mi_cols as usize);
|
||||
let mut out = fs.cur.clone();
|
||||
let mut any = false;
|
||||
let mut r = 0;
|
||||
while r < mi_rows {
|
||||
let mut c = 0;
|
||||
while c < mi_cols {
|
||||
let idx = fs.cdef_idx(r & !15, c & !15);
|
||||
if idx >= 0 && cdef_block(fs, &mut out, r, c, idx as usize) {
|
||||
any = true;
|
||||
}
|
||||
c += 2;
|
||||
}
|
||||
r += 2;
|
||||
}
|
||||
any.then_some(out)
|
||||
}
|
||||
|
||||
fn cdef_block(fs: &FrameState, out: &mut FrameBuf, r: usize, c: usize, idx: usize) -> bool {
|
||||
let mi = &fs.mi;
|
||||
let sk = |rr: usize, cc: usize| -> bool { if rr < mi.rows && cc < mi.cols { mi.skip[mi.idx(rr, cc)] } else { true } };
|
||||
let skip = sk(r, c) && sk(r + 1, c) && sk(r, c + 1) && sk(r + 1, c + 1);
|
||||
if skip {
|
||||
return false;
|
||||
}
|
||||
let coeff_shift = fs.bit_depth - 8;
|
||||
let (y_dir, var) = direction(fs, r, c);
|
||||
let cp = &fs.fh.cdef;
|
||||
let mut pri = (cp.y_pri[idx] << coeff_shift) as i32;
|
||||
let sec = (cp.y_sec[idx] << coeff_shift) as i32;
|
||||
let dir = if pri == 0 { 0 } else { y_dir };
|
||||
let var_str = if (var >> 6) != 0 { (31 - ((var >> 6) as u32).leading_zeros()).min(12) as i32 } else { 0 };
|
||||
pri = if var != 0 { (pri * (4 + var_str) + 8) >> 4 } else { 0 };
|
||||
let damping = cp.damping as i32 + coeff_shift as i32;
|
||||
filter(fs, out, 0, (r, c), (pri, sec), damping, dir);
|
||||
if fs.num_planes == 1 {
|
||||
return true;
|
||||
}
|
||||
let pri = (cp.uv_pri[idx] << coeff_shift) as i32;
|
||||
let sec = (cp.uv_sec[idx] << coeff_shift) as i32;
|
||||
let dir = if pri == 0 { 0 } else { CDEF_UV_DIR[fs.ssx][fs.ssy][y_dir] as usize };
|
||||
let damping = cp.damping as i32 + coeff_shift as i32 - 1;
|
||||
filter(fs, out, 1, (r, c), (pri, sec), damping, dir);
|
||||
filter(fs, out, 2, (r, c), (pri, sec), damping, dir);
|
||||
true
|
||||
}
|
||||
|
||||
fn direction(fs: &FrameState, r: usize, c: usize) -> (usize, i32) {
|
||||
let mut cost = [0i32; 8];
|
||||
let mut partial = [[0i32; 15]; 8];
|
||||
let x0 = c << 2;
|
||||
let y0 = r << 2;
|
||||
let pl = &fs.cur.planes[0];
|
||||
let sh = fs.bit_depth - 8;
|
||||
for i in 0..8 {
|
||||
let row = pl.row(y0 + i);
|
||||
for j in 0..8 {
|
||||
let x = (row[x0 + j] as i32 >> sh) - 128;
|
||||
partial[0][i + j] += x;
|
||||
partial[1][i + j / 2] += x;
|
||||
partial[2][i] += x;
|
||||
partial[3][3 + i - j / 2] += x;
|
||||
partial[4][7 + i - j] += x;
|
||||
partial[5][3 - i / 2 + j] += x;
|
||||
partial[6][j] += x;
|
||||
partial[7][i / 2 + j] += x;
|
||||
}
|
||||
}
|
||||
for i in 0..8 {
|
||||
cost[2] += partial[2][i] * partial[2][i];
|
||||
cost[6] += partial[6][i] * partial[6][i];
|
||||
}
|
||||
cost[2] *= DIV_TABLE[8] as i32;
|
||||
cost[6] *= DIV_TABLE[8] as i32;
|
||||
for i in 0..7 {
|
||||
cost[0] += (partial[0][i] * partial[0][i] + partial[0][14 - i] * partial[0][14 - i]) * DIV_TABLE[i + 1] as i32;
|
||||
cost[4] += (partial[4][i] * partial[4][i] + partial[4][14 - i] * partial[4][14 - i]) * DIV_TABLE[i + 1] as i32;
|
||||
}
|
||||
cost[0] += partial[0][7] * partial[0][7] * DIV_TABLE[8] as i32;
|
||||
cost[4] += partial[4][7] * partial[4][7] * DIV_TABLE[8] as i32;
|
||||
let mut i = 1;
|
||||
while i < 8 {
|
||||
for j in 0..5 {
|
||||
cost[i] += partial[i][3 + j] * partial[i][3 + j];
|
||||
}
|
||||
cost[i] *= DIV_TABLE[8] as i32;
|
||||
for j in 0..3 {
|
||||
cost[i] += (partial[i][j] * partial[i][j] + partial[i][10 - j] * partial[i][10 - j]) * DIV_TABLE[2 * j + 2] as i32;
|
||||
}
|
||||
i += 2;
|
||||
}
|
||||
let mut best = 0;
|
||||
let mut y_dir = 0;
|
||||
for (i, &c) in cost.iter().enumerate() {
|
||||
if c > best {
|
||||
best = c;
|
||||
y_dir = i;
|
||||
}
|
||||
}
|
||||
(y_dir, (best - cost[(y_dir + 4) & 7]) >> 10)
|
||||
}
|
||||
|
||||
/// Marks a sample outside the frame (CdefAvailable = 0) in the padded block.
|
||||
const CDEF_NA: i32 = i32::MIN;
|
||||
/// Padded block: 8x8 plus 2 samples on every side, row stride 12.
|
||||
const PAD_STRIDE: usize = 12;
|
||||
|
||||
#[inline(always)]
|
||||
fn constrain(diff: i32, threshold: i32, adj: u32) -> i32 {
|
||||
let mag = (threshold - (diff.abs() >> adj)).clamp(0, diff.abs());
|
||||
if diff < 0 { -mag } else { mag }
|
||||
}
|
||||
|
||||
/// Tap offsets, weights and strengths of one block.
|
||||
struct Taps {
|
||||
pri: i32,
|
||||
sec: i32,
|
||||
pri_adj: u32,
|
||||
sec_adj: u32,
|
||||
pri_off: [isize; 2],
|
||||
sec_off: [[isize; 2]; 2],
|
||||
pri_taps: [i32; 2],
|
||||
sec_taps: [i32; 2],
|
||||
}
|
||||
|
||||
impl Taps {
|
||||
fn new(coeff_shift: u32, (pri, sec): (i32, i32), damping: i32, dir: usize) -> Taps {
|
||||
let pri_tap = ((pri >> coeff_shift) & 1) as usize;
|
||||
let off = |d: usize, k: usize| CDEF_DIRECTIONS[d][k][0] as isize * PAD_STRIDE as isize + CDEF_DIRECTIONS[d][k][1] as isize;
|
||||
let pri_off = [off(dir, 0), off(dir, 1)];
|
||||
let d_lo = (dir + 6) & 7;
|
||||
let d_hi = (dir + 2) & 7;
|
||||
let sec_off = [[off(d_lo, 0), off(d_hi, 0)], [off(d_lo, 1), off(d_hi, 1)]];
|
||||
let pri_taps = [CDEF_PRI_TAPS[pri_tap][0] as i32, CDEF_PRI_TAPS[pri_tap][1] as i32];
|
||||
let sec_taps = [CDEF_SEC_TAPS[pri_tap][0] as i32, CDEF_SEC_TAPS[pri_tap][1] as i32];
|
||||
let adj = |t: i32| if t == 0 { 0 } else { (damping - (31 - (t as u32).leading_zeros()) as i32).max(0) as u32 };
|
||||
Taps { pri, sec, pri_adj: adj(pri), sec_adj: adj(sec), pri_off, sec_off, pri_taps, sec_taps }
|
||||
}
|
||||
|
||||
/// The 12 (offset, weight, strength, adjustment) taps in filtering order.
|
||||
fn tap_list(&self) -> [(isize, i32, i32, u32); 12] {
|
||||
let mut taps = [(0isize, 0i32, 0i32, 0u32); 12];
|
||||
let mut n = 0;
|
||||
for k in 0..2 {
|
||||
for sign in [-1isize, 1] {
|
||||
taps[n] = (sign * self.pri_off[k], self.pri_taps[k], self.pri, self.pri_adj);
|
||||
taps[n + 1] = (sign * self.sec_off[k][0], self.sec_taps[k], self.sec, self.sec_adj);
|
||||
taps[n + 2] = (sign * self.sec_off[k][1], self.sec_taps[k], self.sec, self.sec_adj);
|
||||
n += 3;
|
||||
}
|
||||
}
|
||||
taps
|
||||
}
|
||||
}
|
||||
|
||||
/// Position and size of a block within its plane.
|
||||
#[derive(Clone, Copy)]
|
||||
struct BlockRect {
|
||||
x0: usize,
|
||||
y0: usize,
|
||||
w: usize,
|
||||
h: usize,
|
||||
}
|
||||
|
||||
/// The block with two samples of padding on every side, plus whether every sample is available
|
||||
/// (CdefAvailable: its 4x4 block is inside the frame).
|
||||
fn padded_block(fs: &FrameState, plane: usize, src: &Plane, rect: BlockRect) -> ([i32; PAD_STRIDE * PAD_STRIDE], bool) {
|
||||
let (sub_x, sub_y) = if plane > 0 { (fs.ssx, fs.ssy) } else { (0, 0) };
|
||||
let (mi_rows, mi_cols) = (fs.fh.mi_rows as isize, fs.fh.mi_cols as isize);
|
||||
let mut buf = [CDEF_NA; PAD_STRIDE * PAD_STRIDE];
|
||||
let mut all_avail = true;
|
||||
for i in 0..rect.h + 4 {
|
||||
let y = (rect.y0 + i) as isize - 2;
|
||||
let cand_r = (y << sub_y) >> 2;
|
||||
if y < 0 || cand_r >= mi_rows {
|
||||
all_avail = false;
|
||||
continue;
|
||||
}
|
||||
let row = src.row(y as usize);
|
||||
for j in 0..rect.w + 4 {
|
||||
let x = (rect.x0 + j) as isize - 2;
|
||||
let cand_c = (x << sub_x) >> 2;
|
||||
if x >= 0 && cand_c < mi_cols {
|
||||
buf[i * PAD_STRIDE + j] = row[x as usize] as i32;
|
||||
} else {
|
||||
all_avail = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
(buf, all_avail)
|
||||
}
|
||||
|
||||
/// Filters one sample whose taps may be unavailable (CDEF_NA).
|
||||
fn filter_sample(buf: &[i32; PAD_STRIDE * PAD_STRIDE], taps: &Taps, ci: isize, x: i32) -> u16 {
|
||||
let mut sum = 0i32;
|
||||
let mut max = x;
|
||||
let mut min = x;
|
||||
for k in 0..2 {
|
||||
for sign in [-1isize, 1] {
|
||||
let p = buf[(ci + sign * taps.pri_off[k]) as usize];
|
||||
if p != CDEF_NA {
|
||||
if taps.pri != 0 {
|
||||
sum += taps.pri_taps[k] * constrain(p - x, taps.pri, taps.pri_adj);
|
||||
}
|
||||
max = max.max(p);
|
||||
min = min.min(p);
|
||||
}
|
||||
for s in 0..2 {
|
||||
let q = buf[(ci + sign * taps.sec_off[k][s]) as usize];
|
||||
if q != CDEF_NA {
|
||||
if taps.sec != 0 {
|
||||
sum += taps.sec_taps[k] * constrain(q - x, taps.sec, taps.sec_adj);
|
||||
}
|
||||
max = max.max(q);
|
||||
min = min.min(q);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
(x + ((8 + sum - (sum < 0) as i32) >> 4)).clamp(min, max) as u16
|
||||
}
|
||||
|
||||
fn filter(fs: &FrameState, out: &mut FrameBuf, plane: usize, (r, c): (usize, usize), strengths: (i32, i32), damping: i32, dir: usize) {
|
||||
let (pri, sec) = strengths;
|
||||
if pri == 0 && sec == 0 {
|
||||
// Every tap contributes 0 and the clamp to [min, max] keeps x: CdefFrame = CurrFrame.
|
||||
return;
|
||||
}
|
||||
let coeff_shift = fs.bit_depth - 8;
|
||||
let (sub_x, sub_y) = if plane > 0 { (fs.ssx, fs.ssy) } else { (0, 0) };
|
||||
let rect = BlockRect { x0: (c * 4) >> sub_x, y0: (r * 4) >> sub_y, w: 8 >> sub_x, h: 8 >> sub_y };
|
||||
let src = &fs.cur.planes[plane];
|
||||
let (buf, all_avail) = padded_block(fs, plane, src, rect);
|
||||
let taps = Taps::new(coeff_shift, strengths, damping, dir);
|
||||
let dst = &mut out.planes[plane];
|
||||
if all_avail {
|
||||
// Every tap is available: whole rows at a time (constrain() with threshold 0 is 0).
|
||||
let list = taps.tap_list();
|
||||
let rows = (rect.y0, rect.x0, rect.h);
|
||||
match rect.w {
|
||||
8 => filter_rows::<8>(&buf, &list, dst, rows),
|
||||
_ => filter_rows::<4>(&buf, &list, dst, rows),
|
||||
}
|
||||
return;
|
||||
}
|
||||
for i in 0..rect.h {
|
||||
// The centre sample is read even where CdefAvailable is 0 (8x8 blocks straddling an odd
|
||||
// MiRows / MiCols edge).
|
||||
let srow = &src.row(rect.y0 + i)[rect.x0..rect.x0 + rect.w];
|
||||
let drow = &mut dst.row_mut(rect.y0 + i)[rect.x0..rect.x0 + rect.w];
|
||||
for j in 0..rect.w {
|
||||
let ci = ((i + 2) * PAD_STRIDE + j + 2) as isize;
|
||||
drow[j] = filter_sample(&buf, &taps, ci, srow[j] as i32);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// CDEF of a block whose taps are all available, `W` samples per row. Samples (at most 12
|
||||
/// bits), differences and the weighted tap sum all fit in i16, which doubles the SIMD width.
|
||||
#[inline(always)]
|
||||
fn filter_rows<const W: usize>(
|
||||
buf: &[i32; PAD_STRIDE * PAD_STRIDE],
|
||||
taps: &[(isize, i32, i32, u32); 12],
|
||||
dst: &mut Plane,
|
||||
(y0, x0, h): (usize, usize, usize),
|
||||
) {
|
||||
let mut b16 = [0i16; PAD_STRIDE * PAD_STRIDE];
|
||||
for (d, &s) in b16.iter_mut().zip(buf.iter()) {
|
||||
*d = s as i16;
|
||||
}
|
||||
for i in 0..h {
|
||||
let ci = ((i + 2) * PAD_STRIDE + 2) as isize;
|
||||
let Some(&x) = b16.get(ci as usize..).and_then(<[_]>::first_chunk::<W>) else {
|
||||
return;
|
||||
};
|
||||
let (mut sum, mut max, mut min) = ([0i16; W], x, x);
|
||||
for &(off, weight, thr, adj) in taps {
|
||||
let (weight, thr) = (weight as i16, thr as i16);
|
||||
let start = (ci + off) as usize;
|
||||
let Some(p) = b16.get(start..).and_then(<[_]>::first_chunk::<W>) else {
|
||||
return;
|
||||
};
|
||||
for j in 0..W {
|
||||
// constrain(): .max / .min instead of clamp (whose bound check panics), and
|
||||
// the sign applied branch-free, so the loop vectorises.
|
||||
let d = p[j] - x[j];
|
||||
let ad = d.abs();
|
||||
let mag = (thr - (ad >> adj)).max(0).min(ad);
|
||||
let sign = d >> 15;
|
||||
sum[j] += weight * ((mag ^ sign) - sign);
|
||||
max[j] = max[j].max(p[j]);
|
||||
min[j] = min[j].min(p[j]);
|
||||
}
|
||||
}
|
||||
let drow = &mut dst.row_mut(y0 + i)[x0..x0 + W];
|
||||
for j in 0..W {
|
||||
let s = sum[j];
|
||||
drow[j] = (x[j] + ((8 + s - (s < 0) as i16) >> 4)).max(min[j]).min(max[j]) as u16;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,379 @@
|
||||
//! The adaptive CDF arrays (spec 6.8.2 init_non_coeff_cdfs / init_coeff_cdfs, load_cdfs).
|
||||
|
||||
use crate::spec_tables::{
|
||||
DEFAULT_ANGLE_DELTA_CDF, DEFAULT_CFL_ALPHA_CDF, DEFAULT_CFL_SIGN_CDF, DEFAULT_COEFF_BASE_CDF, DEFAULT_COEFF_BASE_EOB_CDF, DEFAULT_COEFF_BR_CDF,
|
||||
DEFAULT_COMP_BWD_REF_CDF, DEFAULT_COMP_GROUP_IDX_CDF, DEFAULT_COMP_MODE_CDF, DEFAULT_COMP_REF_CDF, DEFAULT_COMP_REF_TYPE_CDF,
|
||||
DEFAULT_COMPOUND_IDX_CDF, DEFAULT_COMPOUND_MODE_CDF, DEFAULT_COMPOUND_TYPE_CDF, DEFAULT_DC_SIGN_CDF, DEFAULT_DELTA_LF_CDF, DEFAULT_DELTA_Q_CDF,
|
||||
DEFAULT_DRL_MODE_CDF, DEFAULT_EOB_EXTRA_CDF, DEFAULT_EOB_PT_16_CDF, DEFAULT_EOB_PT_32_CDF, DEFAULT_EOB_PT_64_CDF, DEFAULT_EOB_PT_128_CDF,
|
||||
DEFAULT_EOB_PT_256_CDF, DEFAULT_EOB_PT_512_CDF, DEFAULT_EOB_PT_1024_CDF, DEFAULT_FILTER_INTRA_CDF, DEFAULT_FILTER_INTRA_MODE_CDF,
|
||||
DEFAULT_INTER_INTRA_CDF, DEFAULT_INTER_INTRA_MODE_CDF, DEFAULT_INTER_TX_TYPE_SET1_CDF, DEFAULT_INTER_TX_TYPE_SET2_CDF,
|
||||
DEFAULT_INTER_TX_TYPE_SET3_CDF, DEFAULT_INTERP_FILTER_CDF, DEFAULT_INTRA_TX_TYPE_SET1_CDF, DEFAULT_INTRA_TX_TYPE_SET2_CDF, DEFAULT_INTRABC_CDF,
|
||||
DEFAULT_IS_INTER_CDF, DEFAULT_MOTION_MODE_CDF, DEFAULT_MV_BIT_CDF, DEFAULT_MV_CLASS_CDF, DEFAULT_MV_CLASS0_BIT_CDF, DEFAULT_MV_CLASS0_FR_CDF,
|
||||
DEFAULT_MV_CLASS0_HP_CDF, DEFAULT_MV_FR_CDF, DEFAULT_MV_HP_CDF, DEFAULT_MV_JOINT_CDF, DEFAULT_MV_SIGN_CDF, DEFAULT_NEW_MV_CDF,
|
||||
DEFAULT_PALETTE_SIZE_2_UV_COLOR_CDF, DEFAULT_PALETTE_SIZE_2_Y_COLOR_CDF, DEFAULT_PALETTE_SIZE_3_UV_COLOR_CDF, DEFAULT_PALETTE_SIZE_3_Y_COLOR_CDF,
|
||||
DEFAULT_PALETTE_SIZE_4_UV_COLOR_CDF, DEFAULT_PALETTE_SIZE_4_Y_COLOR_CDF, DEFAULT_PALETTE_SIZE_5_UV_COLOR_CDF, DEFAULT_PALETTE_SIZE_5_Y_COLOR_CDF,
|
||||
DEFAULT_PALETTE_SIZE_6_UV_COLOR_CDF, DEFAULT_PALETTE_SIZE_6_Y_COLOR_CDF, DEFAULT_PALETTE_SIZE_7_UV_COLOR_CDF, DEFAULT_PALETTE_SIZE_7_Y_COLOR_CDF,
|
||||
DEFAULT_PALETTE_SIZE_8_UV_COLOR_CDF, DEFAULT_PALETTE_SIZE_8_Y_COLOR_CDF, DEFAULT_PALETTE_UV_MODE_CDF, DEFAULT_PALETTE_UV_SIZE_CDF,
|
||||
DEFAULT_PALETTE_Y_MODE_CDF, DEFAULT_PALETTE_Y_SIZE_CDF, DEFAULT_PARTITION_W8_CDF, DEFAULT_PARTITION_W16_CDF, DEFAULT_PARTITION_W32_CDF,
|
||||
DEFAULT_PARTITION_W64_CDF, DEFAULT_PARTITION_W128_CDF, DEFAULT_REF_MV_CDF, DEFAULT_RESTORATION_TYPE_CDF, DEFAULT_SEGMENT_ID_CDF,
|
||||
DEFAULT_SEGMENT_ID_PREDICTED_CDF, DEFAULT_SINGLE_REF_CDF, DEFAULT_SKIP_CDF, DEFAULT_SKIP_MODE_CDF, DEFAULT_TX_8X8_CDF, DEFAULT_TX_16X16_CDF,
|
||||
DEFAULT_TX_32X32_CDF, DEFAULT_TX_64X64_CDF, DEFAULT_TXB_SKIP_CDF, DEFAULT_TXFM_SPLIT_CDF, DEFAULT_UNI_COMP_REF_CDF, DEFAULT_USE_OBMC_CDF,
|
||||
DEFAULT_USE_SGRPROJ_CDF, DEFAULT_USE_WIENER_CDF, DEFAULT_UV_MODE_CFL_ALLOWED_CDF, DEFAULT_UV_MODE_CFL_NOT_ALLOWED_CDF, DEFAULT_WEDGE_INDEX_CDF,
|
||||
DEFAULT_WEDGE_INTER_INTRA_CDF, DEFAULT_Y_MODE_CDF, DEFAULT_ZERO_MV_CDF, FRAME_LF_COUNT, MV_CONTEXTS,
|
||||
};
|
||||
|
||||
/// Every CDF array listed by init_non_coeff_cdfs and init_coeff_cdfs. Each innermost array holds
|
||||
/// N cumulative values (the last is 32768) plus the adaptation counter.
|
||||
#[derive(Clone)]
|
||||
pub(crate) struct Cdfs {
|
||||
pub y_mode: [[u16; 14]; 4],
|
||||
pub uv_mode_cfl_not_allowed: [[u16; 14]; 13],
|
||||
pub uv_mode_cfl_allowed: [[u16; 15]; 13],
|
||||
pub angle_delta: [[u16; 8]; 8],
|
||||
pub intrabc: [u16; 3],
|
||||
pub partition_w8: [[u16; 5]; 4],
|
||||
pub partition_w16: [[u16; 11]; 4],
|
||||
pub partition_w32: [[u16; 11]; 4],
|
||||
pub partition_w64: [[u16; 11]; 4],
|
||||
pub partition_w128: [[u16; 9]; 4],
|
||||
pub segment_id: [[u16; 9]; 3],
|
||||
pub segment_id_predicted: [[u16; 3]; 3],
|
||||
pub tx_8x8: [[u16; 3]; 3],
|
||||
pub tx_16x16: [[u16; 4]; 3],
|
||||
pub tx_32x32: [[u16; 4]; 3],
|
||||
pub tx_64x64: [[u16; 4]; 3],
|
||||
pub txfm_split: [[u16; 3]; 21],
|
||||
pub filter_intra_mode: [u16; 6],
|
||||
pub filter_intra: [[u16; 3]; 22],
|
||||
pub interp_filter: [[u16; 4]; 16],
|
||||
pub motion_mode: [[u16; 4]; 22],
|
||||
pub new_mv: [[u16; 3]; 6],
|
||||
pub zero_mv: [[u16; 3]; 2],
|
||||
pub ref_mv: [[u16; 3]; 6],
|
||||
pub compound_mode: [[u16; 9]; 8],
|
||||
pub drl_mode: [[u16; 3]; 3],
|
||||
pub is_inter: [[u16; 3]; 4],
|
||||
pub comp_mode: [[u16; 3]; 5],
|
||||
pub skip_mode: [[u16; 3]; 3],
|
||||
pub skip: [[u16; 3]; 3],
|
||||
pub comp_ref: [[[u16; 3]; 3]; 3],
|
||||
pub comp_bwd_ref: [[[u16; 3]; 2]; 3],
|
||||
pub single_ref: [[[u16; 3]; 6]; 3],
|
||||
pub mv_joint: [[u16; 5]; MV_CONTEXTS],
|
||||
pub mv_class: [[[u16; 12]; 2]; MV_CONTEXTS],
|
||||
pub mv_class0_bit: [[[u16; 3]; 2]; MV_CONTEXTS],
|
||||
pub mv_fr: [[[u16; 5]; 2]; MV_CONTEXTS],
|
||||
pub mv_class0_fr: [[[[u16; 5]; 2]; 2]; MV_CONTEXTS],
|
||||
pub mv_class0_hp: [[[u16; 3]; 2]; MV_CONTEXTS],
|
||||
pub mv_sign: [[[u16; 3]; 2]; MV_CONTEXTS],
|
||||
pub mv_bit: [[[[u16; 3]; 10]; 2]; MV_CONTEXTS],
|
||||
pub mv_hp: [[[u16; 3]; 2]; MV_CONTEXTS],
|
||||
pub palette_y_mode: [[[u16; 3]; 3]; 7],
|
||||
pub palette_uv_mode: [[u16; 3]; 2],
|
||||
pub palette_y_size: [[u16; 8]; 7],
|
||||
pub palette_uv_size: [[u16; 8]; 7],
|
||||
pub palette_2_y_color: [[u16; 3]; 5],
|
||||
pub palette_2_uv_color: [[u16; 3]; 5],
|
||||
pub palette_3_y_color: [[u16; 4]; 5],
|
||||
pub palette_3_uv_color: [[u16; 4]; 5],
|
||||
pub palette_4_y_color: [[u16; 5]; 5],
|
||||
pub palette_4_uv_color: [[u16; 5]; 5],
|
||||
pub palette_5_y_color: [[u16; 6]; 5],
|
||||
pub palette_5_uv_color: [[u16; 6]; 5],
|
||||
pub palette_6_y_color: [[u16; 7]; 5],
|
||||
pub palette_6_uv_color: [[u16; 7]; 5],
|
||||
pub palette_7_y_color: [[u16; 8]; 5],
|
||||
pub palette_7_uv_color: [[u16; 8]; 5],
|
||||
pub palette_8_y_color: [[u16; 9]; 5],
|
||||
pub palette_8_uv_color: [[u16; 9]; 5],
|
||||
pub delta_q: [u16; 5],
|
||||
pub delta_lf: [u16; 5],
|
||||
pub delta_lf_multi: [[u16; 5]; FRAME_LF_COUNT],
|
||||
pub intra_tx_type_set1: [[[u16; 8]; 13]; 2],
|
||||
pub intra_tx_type_set2: [[[u16; 6]; 13]; 3],
|
||||
pub inter_tx_type_set1: [[u16; 17]; 2],
|
||||
pub inter_tx_type_set2: [u16; 13],
|
||||
pub inter_tx_type_set3: [[u16; 3]; 4],
|
||||
pub use_obmc: [[u16; 3]; 22],
|
||||
pub inter_intra: [[u16; 3]; 3],
|
||||
pub comp_ref_type: [[u16; 3]; 5],
|
||||
pub cfl_sign: [u16; 9],
|
||||
pub uni_comp_ref: [[[u16; 3]; 3]; 3],
|
||||
pub wedge_inter_intra: [[u16; 3]; 22],
|
||||
pub comp_group_idx: [[u16; 3]; 6],
|
||||
pub compound_idx: [[u16; 3]; 6],
|
||||
pub compound_type: [[u16; 3]; 22],
|
||||
pub inter_intra_mode: [[u16; 5]; 3],
|
||||
pub wedge_index: [[u16; 17]; 22],
|
||||
pub cfl_alpha: [[u16; 17]; 6],
|
||||
pub use_wiener: [u16; 3],
|
||||
pub use_sgrproj: [u16; 3],
|
||||
pub restoration_type: [u16; 4],
|
||||
// coefficient CDFs
|
||||
pub txb_skip: [[[u16; 3]; 13]; 5],
|
||||
pub eob_pt_16: [[[u16; 6]; 2]; 2],
|
||||
pub eob_pt_32: [[[u16; 7]; 2]; 2],
|
||||
pub eob_pt_64: [[[u16; 8]; 2]; 2],
|
||||
pub eob_pt_128: [[[u16; 9]; 2]; 2],
|
||||
pub eob_pt_256: [[[u16; 10]; 2]; 2],
|
||||
pub eob_pt_512: [[u16; 11]; 2],
|
||||
pub eob_pt_1024: [[u16; 12]; 2],
|
||||
pub eob_extra: [[[[u16; 3]; 9]; 2]; 5],
|
||||
pub dc_sign: [[[u16; 3]; 3]; 2],
|
||||
pub coeff_base_eob: [[[[u16; 4]; 4]; 2]; 5],
|
||||
pub coeff_base: [[[[u16; 5]; 42]; 2]; 5],
|
||||
pub coeff_br: [[[[u16; 5]; 21]; 2]; 5],
|
||||
}
|
||||
|
||||
impl Cdfs {
|
||||
/// init_non_coeff_cdfs( ) followed by init_coeff_cdfs( ) for `base_q_idx`.
|
||||
pub fn new(base_q_idx: u32) -> Box<Cdfs> {
|
||||
let mut c = Box::new(Cdfs {
|
||||
y_mode: DEFAULT_Y_MODE_CDF,
|
||||
uv_mode_cfl_not_allowed: DEFAULT_UV_MODE_CFL_NOT_ALLOWED_CDF,
|
||||
uv_mode_cfl_allowed: DEFAULT_UV_MODE_CFL_ALLOWED_CDF,
|
||||
angle_delta: DEFAULT_ANGLE_DELTA_CDF,
|
||||
intrabc: DEFAULT_INTRABC_CDF,
|
||||
partition_w8: DEFAULT_PARTITION_W8_CDF,
|
||||
partition_w16: DEFAULT_PARTITION_W16_CDF,
|
||||
partition_w32: DEFAULT_PARTITION_W32_CDF,
|
||||
partition_w64: DEFAULT_PARTITION_W64_CDF,
|
||||
partition_w128: DEFAULT_PARTITION_W128_CDF,
|
||||
segment_id: DEFAULT_SEGMENT_ID_CDF,
|
||||
segment_id_predicted: DEFAULT_SEGMENT_ID_PREDICTED_CDF,
|
||||
tx_8x8: DEFAULT_TX_8X8_CDF,
|
||||
tx_16x16: DEFAULT_TX_16X16_CDF,
|
||||
tx_32x32: DEFAULT_TX_32X32_CDF,
|
||||
tx_64x64: DEFAULT_TX_64X64_CDF,
|
||||
txfm_split: DEFAULT_TXFM_SPLIT_CDF,
|
||||
filter_intra_mode: DEFAULT_FILTER_INTRA_MODE_CDF,
|
||||
filter_intra: DEFAULT_FILTER_INTRA_CDF,
|
||||
interp_filter: DEFAULT_INTERP_FILTER_CDF,
|
||||
motion_mode: DEFAULT_MOTION_MODE_CDF,
|
||||
new_mv: DEFAULT_NEW_MV_CDF,
|
||||
zero_mv: DEFAULT_ZERO_MV_CDF,
|
||||
ref_mv: DEFAULT_REF_MV_CDF,
|
||||
compound_mode: DEFAULT_COMPOUND_MODE_CDF,
|
||||
drl_mode: DEFAULT_DRL_MODE_CDF,
|
||||
is_inter: DEFAULT_IS_INTER_CDF,
|
||||
comp_mode: DEFAULT_COMP_MODE_CDF,
|
||||
skip_mode: DEFAULT_SKIP_MODE_CDF,
|
||||
skip: DEFAULT_SKIP_CDF,
|
||||
comp_ref: DEFAULT_COMP_REF_CDF,
|
||||
comp_bwd_ref: DEFAULT_COMP_BWD_REF_CDF,
|
||||
single_ref: DEFAULT_SINGLE_REF_CDF,
|
||||
mv_joint: [DEFAULT_MV_JOINT_CDF; MV_CONTEXTS],
|
||||
mv_class: [DEFAULT_MV_CLASS_CDF; MV_CONTEXTS],
|
||||
mv_class0_bit: [[DEFAULT_MV_CLASS0_BIT_CDF; 2]; MV_CONTEXTS],
|
||||
mv_fr: [DEFAULT_MV_FR_CDF; MV_CONTEXTS],
|
||||
mv_class0_fr: [DEFAULT_MV_CLASS0_FR_CDF; MV_CONTEXTS],
|
||||
mv_class0_hp: [[DEFAULT_MV_CLASS0_HP_CDF; 2]; MV_CONTEXTS],
|
||||
mv_sign: [[DEFAULT_MV_SIGN_CDF; 2]; MV_CONTEXTS],
|
||||
mv_bit: [[DEFAULT_MV_BIT_CDF; 2]; MV_CONTEXTS],
|
||||
mv_hp: [[DEFAULT_MV_HP_CDF; 2]; MV_CONTEXTS],
|
||||
palette_y_mode: DEFAULT_PALETTE_Y_MODE_CDF,
|
||||
palette_uv_mode: DEFAULT_PALETTE_UV_MODE_CDF,
|
||||
palette_y_size: DEFAULT_PALETTE_Y_SIZE_CDF,
|
||||
palette_uv_size: DEFAULT_PALETTE_UV_SIZE_CDF,
|
||||
palette_2_y_color: DEFAULT_PALETTE_SIZE_2_Y_COLOR_CDF,
|
||||
palette_2_uv_color: DEFAULT_PALETTE_SIZE_2_UV_COLOR_CDF,
|
||||
palette_3_y_color: DEFAULT_PALETTE_SIZE_3_Y_COLOR_CDF,
|
||||
palette_3_uv_color: DEFAULT_PALETTE_SIZE_3_UV_COLOR_CDF,
|
||||
palette_4_y_color: DEFAULT_PALETTE_SIZE_4_Y_COLOR_CDF,
|
||||
palette_4_uv_color: DEFAULT_PALETTE_SIZE_4_UV_COLOR_CDF,
|
||||
palette_5_y_color: DEFAULT_PALETTE_SIZE_5_Y_COLOR_CDF,
|
||||
palette_5_uv_color: DEFAULT_PALETTE_SIZE_5_UV_COLOR_CDF,
|
||||
palette_6_y_color: DEFAULT_PALETTE_SIZE_6_Y_COLOR_CDF,
|
||||
palette_6_uv_color: DEFAULT_PALETTE_SIZE_6_UV_COLOR_CDF,
|
||||
palette_7_y_color: DEFAULT_PALETTE_SIZE_7_Y_COLOR_CDF,
|
||||
palette_7_uv_color: DEFAULT_PALETTE_SIZE_7_UV_COLOR_CDF,
|
||||
palette_8_y_color: DEFAULT_PALETTE_SIZE_8_Y_COLOR_CDF,
|
||||
palette_8_uv_color: DEFAULT_PALETTE_SIZE_8_UV_COLOR_CDF,
|
||||
delta_q: DEFAULT_DELTA_Q_CDF,
|
||||
delta_lf: DEFAULT_DELTA_LF_CDF,
|
||||
delta_lf_multi: [DEFAULT_DELTA_LF_CDF; FRAME_LF_COUNT],
|
||||
intra_tx_type_set1: DEFAULT_INTRA_TX_TYPE_SET1_CDF,
|
||||
intra_tx_type_set2: DEFAULT_INTRA_TX_TYPE_SET2_CDF,
|
||||
inter_tx_type_set1: DEFAULT_INTER_TX_TYPE_SET1_CDF,
|
||||
inter_tx_type_set2: DEFAULT_INTER_TX_TYPE_SET2_CDF,
|
||||
inter_tx_type_set3: DEFAULT_INTER_TX_TYPE_SET3_CDF,
|
||||
use_obmc: DEFAULT_USE_OBMC_CDF,
|
||||
inter_intra: DEFAULT_INTER_INTRA_CDF,
|
||||
comp_ref_type: DEFAULT_COMP_REF_TYPE_CDF,
|
||||
cfl_sign: DEFAULT_CFL_SIGN_CDF,
|
||||
uni_comp_ref: DEFAULT_UNI_COMP_REF_CDF,
|
||||
wedge_inter_intra: DEFAULT_WEDGE_INTER_INTRA_CDF,
|
||||
comp_group_idx: DEFAULT_COMP_GROUP_IDX_CDF,
|
||||
compound_idx: DEFAULT_COMPOUND_IDX_CDF,
|
||||
compound_type: DEFAULT_COMPOUND_TYPE_CDF,
|
||||
inter_intra_mode: DEFAULT_INTER_INTRA_MODE_CDF,
|
||||
wedge_index: DEFAULT_WEDGE_INDEX_CDF,
|
||||
cfl_alpha: DEFAULT_CFL_ALPHA_CDF,
|
||||
use_wiener: DEFAULT_USE_WIENER_CDF,
|
||||
use_sgrproj: DEFAULT_USE_SGRPROJ_CDF,
|
||||
restoration_type: DEFAULT_RESTORATION_TYPE_CDF,
|
||||
txb_skip: DEFAULT_TXB_SKIP_CDF[0],
|
||||
eob_pt_16: DEFAULT_EOB_PT_16_CDF[0],
|
||||
eob_pt_32: DEFAULT_EOB_PT_32_CDF[0],
|
||||
eob_pt_64: DEFAULT_EOB_PT_64_CDF[0],
|
||||
eob_pt_128: DEFAULT_EOB_PT_128_CDF[0],
|
||||
eob_pt_256: DEFAULT_EOB_PT_256_CDF[0],
|
||||
eob_pt_512: DEFAULT_EOB_PT_512_CDF[0],
|
||||
eob_pt_1024: DEFAULT_EOB_PT_1024_CDF[0],
|
||||
eob_extra: DEFAULT_EOB_EXTRA_CDF[0],
|
||||
dc_sign: DEFAULT_DC_SIGN_CDF[0],
|
||||
coeff_base_eob: DEFAULT_COEFF_BASE_EOB_CDF[0],
|
||||
coeff_base: DEFAULT_COEFF_BASE_CDF[0],
|
||||
coeff_br: DEFAULT_COEFF_BR_CDF[0],
|
||||
});
|
||||
c.init_coeff(base_q_idx);
|
||||
c
|
||||
}
|
||||
|
||||
/// init_coeff_cdfs( )
|
||||
pub fn init_coeff(&mut self, base_q_idx: u32) {
|
||||
let idx = match base_q_idx {
|
||||
0..=20 => 0,
|
||||
21..=60 => 1,
|
||||
61..=120 => 2,
|
||||
_ => 3,
|
||||
};
|
||||
self.txb_skip = DEFAULT_TXB_SKIP_CDF[idx];
|
||||
self.eob_pt_16 = DEFAULT_EOB_PT_16_CDF[idx];
|
||||
self.eob_pt_32 = DEFAULT_EOB_PT_32_CDF[idx];
|
||||
self.eob_pt_64 = DEFAULT_EOB_PT_64_CDF[idx];
|
||||
self.eob_pt_128 = DEFAULT_EOB_PT_128_CDF[idx];
|
||||
self.eob_pt_256 = DEFAULT_EOB_PT_256_CDF[idx];
|
||||
self.eob_pt_512 = DEFAULT_EOB_PT_512_CDF[idx];
|
||||
self.eob_pt_1024 = DEFAULT_EOB_PT_1024_CDF[idx];
|
||||
self.eob_extra = DEFAULT_EOB_EXTRA_CDF[idx];
|
||||
self.dc_sign = DEFAULT_DC_SIGN_CDF[idx];
|
||||
self.coeff_base_eob = DEFAULT_COEFF_BASE_EOB_CDF[idx];
|
||||
self.coeff_base = DEFAULT_COEFF_BASE_CDF[idx];
|
||||
self.coeff_br = DEFAULT_COEFF_BR_CDF[idx];
|
||||
}
|
||||
|
||||
/// The counter reset of load_cdfs( ): the last entry of every CDF array is set to 0.
|
||||
pub fn clear_counts(&mut self) {
|
||||
macro_rules! clr {
|
||||
($($f:ident),*) => { $( self.$f.clear_count(); )* };
|
||||
}
|
||||
clr!(
|
||||
y_mode,
|
||||
uv_mode_cfl_not_allowed,
|
||||
uv_mode_cfl_allowed,
|
||||
angle_delta,
|
||||
intrabc,
|
||||
partition_w8,
|
||||
partition_w16,
|
||||
partition_w32,
|
||||
partition_w64,
|
||||
partition_w128,
|
||||
segment_id,
|
||||
segment_id_predicted,
|
||||
tx_8x8,
|
||||
tx_16x16,
|
||||
tx_32x32,
|
||||
tx_64x64,
|
||||
txfm_split,
|
||||
filter_intra_mode,
|
||||
filter_intra,
|
||||
interp_filter,
|
||||
motion_mode,
|
||||
new_mv,
|
||||
zero_mv,
|
||||
ref_mv,
|
||||
compound_mode,
|
||||
drl_mode,
|
||||
is_inter,
|
||||
comp_mode,
|
||||
skip_mode,
|
||||
skip,
|
||||
comp_ref,
|
||||
comp_bwd_ref,
|
||||
single_ref,
|
||||
mv_joint,
|
||||
mv_class,
|
||||
mv_class0_bit,
|
||||
mv_fr,
|
||||
mv_class0_fr,
|
||||
mv_class0_hp,
|
||||
mv_sign,
|
||||
mv_bit,
|
||||
mv_hp,
|
||||
palette_y_mode,
|
||||
palette_uv_mode,
|
||||
palette_y_size,
|
||||
palette_uv_size,
|
||||
palette_2_y_color,
|
||||
palette_2_uv_color,
|
||||
palette_3_y_color,
|
||||
palette_3_uv_color,
|
||||
palette_4_y_color,
|
||||
palette_4_uv_color,
|
||||
palette_5_y_color,
|
||||
palette_5_uv_color,
|
||||
palette_6_y_color,
|
||||
palette_6_uv_color,
|
||||
palette_7_y_color,
|
||||
palette_7_uv_color,
|
||||
palette_8_y_color,
|
||||
palette_8_uv_color,
|
||||
delta_q,
|
||||
delta_lf,
|
||||
delta_lf_multi,
|
||||
intra_tx_type_set1,
|
||||
intra_tx_type_set2,
|
||||
inter_tx_type_set1,
|
||||
inter_tx_type_set2,
|
||||
inter_tx_type_set3,
|
||||
use_obmc,
|
||||
inter_intra,
|
||||
comp_ref_type,
|
||||
cfl_sign,
|
||||
uni_comp_ref,
|
||||
wedge_inter_intra,
|
||||
comp_group_idx,
|
||||
compound_idx,
|
||||
compound_type,
|
||||
inter_intra_mode,
|
||||
wedge_index,
|
||||
cfl_alpha,
|
||||
use_wiener,
|
||||
use_sgrproj,
|
||||
restoration_type,
|
||||
txb_skip,
|
||||
eob_pt_16,
|
||||
eob_pt_32,
|
||||
eob_pt_64,
|
||||
eob_pt_128,
|
||||
eob_pt_256,
|
||||
eob_pt_512,
|
||||
eob_pt_1024,
|
||||
eob_extra,
|
||||
dc_sign,
|
||||
coeff_base_eob,
|
||||
coeff_base,
|
||||
coeff_br
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Zeroes the adaptation counter (last element) of every innermost CDF array.
|
||||
trait ClearCount {
|
||||
fn clear_count(&mut self);
|
||||
}
|
||||
|
||||
impl<const N: usize> ClearCount for [u16; N] {
|
||||
fn clear_count(&mut self) {
|
||||
self[N - 1] = 0;
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: ClearCount, const M: usize> ClearCount for [T; M] {
|
||||
fn clear_count(&mut self) {
|
||||
for e in self.iter_mut() {
|
||||
e.clear_count();
|
||||
}
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,287 @@
|
||||
//! Frame sample buffers and per-4x4 mode info storage.
|
||||
//!
|
||||
//! Tiles decode in parallel into private buffers that cover only the tile's area: a [`Plane`]
|
||||
//! or [`MiInfo`] may hold a region of the frame (origin `ox`, `oy`), addressed with frame
|
||||
//! coordinates throughout.
|
||||
|
||||
/// Recycled plane allocations: every frame allocates (and every evicted reference frees)
|
||||
/// megabytes of planes, and fresh large allocations cost page faults and kernel calls. Buffers
|
||||
/// are zeroed again on reuse, so contents never leak between frames.
|
||||
static PLANE_POOL: std::sync::Mutex<Vec<Vec<u16>>> = std::sync::Mutex::new(Vec::new());
|
||||
const POOL_MIN_LEN: usize = 1 << 16;
|
||||
/// Samples kept at most (96 M samples = 192 MB, a few 4K frames).
|
||||
const POOL_MAX_SAMPLES: usize = 96 << 20;
|
||||
|
||||
/// A zeroed buffer of `len` samples (recycled memory for large ones).
|
||||
fn zeroed(len: usize) -> Vec<u16> {
|
||||
if len >= POOL_MIN_LEN {
|
||||
let found = {
|
||||
let mut pool = PLANE_POOL.lock().unwrap_or_else(std::sync::PoisonError::into_inner);
|
||||
pool.iter().position(|v| v.capacity() >= len && v.capacity() <= 2 * len).map(|i| pool.swap_remove(i))
|
||||
};
|
||||
if let Some(mut v) = found {
|
||||
v.clear();
|
||||
v.resize(len, 0);
|
||||
return v;
|
||||
}
|
||||
}
|
||||
vec![0; len]
|
||||
}
|
||||
|
||||
/// One plane of samples (u16 at every bit depth), or a rectangular region of one.
|
||||
#[derive(Default)]
|
||||
pub struct Plane {
|
||||
pub data: Vec<u16>,
|
||||
pub stride: usize,
|
||||
/// Allocated rows.
|
||||
pub rows: usize,
|
||||
/// Frame coordinates of `data[0]` (0 for whole planes).
|
||||
pub ox: usize,
|
||||
pub oy: usize,
|
||||
}
|
||||
|
||||
impl Clone for Plane {
|
||||
fn clone(&self) -> Plane {
|
||||
let mut data = zeroed(self.data.len());
|
||||
data.copy_from_slice(&self.data);
|
||||
Plane { data, stride: self.stride, rows: self.rows, ox: self.ox, oy: self.oy }
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for Plane {
|
||||
fn drop(&mut self) {
|
||||
if self.data.capacity() >= POOL_MIN_LEN {
|
||||
let v = std::mem::take(&mut self.data);
|
||||
let mut pool = PLANE_POOL.lock().unwrap_or_else(std::sync::PoisonError::into_inner);
|
||||
let kept: usize = pool.iter().map(Vec::capacity).sum();
|
||||
if kept + v.capacity() <= POOL_MAX_SAMPLES {
|
||||
pool.push(v);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Plane {
|
||||
pub fn new(width: usize, height: usize) -> Plane {
|
||||
Plane { data: zeroed(width * height), stride: width, rows: height, ox: 0, oy: 0 }
|
||||
}
|
||||
/// A `width` x `height` region with its top-left sample at frame position (ox, oy).
|
||||
pub fn region(ox: usize, oy: usize, width: usize, height: usize) -> Plane {
|
||||
Plane { data: zeroed(width * height), stride: width, rows: height, ox, oy }
|
||||
}
|
||||
#[inline(always)]
|
||||
pub fn at(&self, x: usize, y: usize) -> u16 {
|
||||
self.data[(y - self.oy) * self.stride + x - self.ox]
|
||||
}
|
||||
#[inline(always)]
|
||||
pub fn set(&mut self, x: usize, y: usize, v: u16) {
|
||||
self.data[(y - self.oy) * self.stride + x - self.ox] = v;
|
||||
}
|
||||
/// Row `y`; index 0 is column `ox` (whole planes: column 0).
|
||||
#[inline(always)]
|
||||
pub fn row(&self, y: usize) -> &[u16] {
|
||||
let o = (y - self.oy) * self.stride;
|
||||
&self.data[o..o + self.stride]
|
||||
}
|
||||
#[inline(always)]
|
||||
pub fn row_mut(&mut self, y: usize) -> &mut [u16] {
|
||||
let o = (y - self.oy) * self.stride;
|
||||
&mut self.data[o..o + self.stride]
|
||||
}
|
||||
/// Row `y` from column `x` (frame coordinates) to the end of the stored row.
|
||||
#[inline(always)]
|
||||
pub fn row_from(&self, y: usize, x: usize) -> &[u16] {
|
||||
let o = (y - self.oy) * self.stride;
|
||||
&self.data[o + x - self.ox..o + self.stride]
|
||||
}
|
||||
#[inline(always)]
|
||||
pub fn row_from_mut(&mut self, y: usize, x: usize) -> &mut [u16] {
|
||||
let o = (y - self.oy) * self.stride;
|
||||
&mut self.data[o + x - self.ox..o + self.stride]
|
||||
}
|
||||
/// Copy this region into the whole plane `dst` (same frame coordinates).
|
||||
pub fn copy_into(&self, dst: &mut Plane) {
|
||||
for y in 0..self.rows {
|
||||
let s = &self.data[y * self.stride..(y + 1) * self.stride];
|
||||
dst.row_from_mut(self.oy + y, self.ox)[..self.stride].copy_from_slice(s);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A frame's three planes, sized to the macroblock-aligned coded area plus margins.
|
||||
#[derive(Clone, Default)]
|
||||
pub struct FrameBuf {
|
||||
pub planes: [Plane; 3],
|
||||
pub num_planes: usize,
|
||||
pub subsampling_x: usize,
|
||||
pub subsampling_y: usize,
|
||||
pub bit_depth: u8,
|
||||
/// Frame dimensions (luma) the samples are valid for.
|
||||
pub width: usize,
|
||||
pub height: usize,
|
||||
}
|
||||
|
||||
impl FrameBuf {
|
||||
/// Allocate a frame for a coded area of `width` x `height` luma samples (rounded up to the
|
||||
/// 128x128 superblock grid plus 160 samples of slack for blocks extending past the edge).
|
||||
pub fn new(width: usize, height: usize, num_planes: usize, subsampling_x: usize, subsampling_y: usize, bit_depth: u8) -> FrameBuf {
|
||||
let aw = width.div_ceil(128) * 128 + 160;
|
||||
let ah = height.div_ceil(128) * 128 + 160;
|
||||
let mut planes: [Plane; 3] = Default::default();
|
||||
planes[0] = Plane::new(aw, ah);
|
||||
for p in planes.iter_mut().take(num_planes).skip(1) {
|
||||
*p = Plane::new(aw >> subsampling_x, ah >> subsampling_y);
|
||||
}
|
||||
FrameBuf { planes, num_planes, subsampling_x, subsampling_y, bit_depth, width, height }
|
||||
}
|
||||
|
||||
/// The region of a frame of this geometry covering luma samples [x0, x1) x [y0, y1)
|
||||
/// (chroma scaled down); the bounds are multiples of 8, or the allocated size.
|
||||
pub fn region_like(&self, x0: usize, y0: usize, x1: usize, y1: usize) -> FrameBuf {
|
||||
let mut planes: [Plane; 3] = Default::default();
|
||||
for (p, pl) in planes.iter_mut().enumerate().take(self.num_planes) {
|
||||
let (sx, sy) = if p == 0 { (0, 0) } else { (self.subsampling_x, self.subsampling_y) };
|
||||
let full = &self.planes[p];
|
||||
let (px0, py0) = (x0 >> sx, y0 >> sy);
|
||||
let px1 = (x1 >> sx).min(full.stride);
|
||||
let py1 = (y1 >> sy).min(full.rows);
|
||||
*pl = Plane::region(px0, py0, px1 - px0, py1 - py0);
|
||||
}
|
||||
FrameBuf {
|
||||
planes,
|
||||
num_planes: self.num_planes,
|
||||
subsampling_x: self.subsampling_x,
|
||||
subsampling_y: self.subsampling_y,
|
||||
bit_depth: self.bit_depth,
|
||||
width: self.width,
|
||||
height: self.height,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn plane_width(&self, plane: usize) -> usize {
|
||||
if plane == 0 { self.width } else { (self.width + self.subsampling_x) >> self.subsampling_x }
|
||||
}
|
||||
|
||||
pub fn plane_height(&self, plane: usize) -> usize {
|
||||
if plane == 0 { self.height } else { (self.height + self.subsampling_y) >> self.subsampling_y }
|
||||
}
|
||||
}
|
||||
|
||||
/// A motion vector (row, col) in 1/8 sample units.
|
||||
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
|
||||
pub struct Mv {
|
||||
pub row: i16,
|
||||
pub col: i16,
|
||||
}
|
||||
|
||||
impl Mv {
|
||||
pub const ZERO: Mv = Mv { row: 0, col: 0 };
|
||||
pub fn new(row: i32, col: i32) -> Mv {
|
||||
Mv { row: row as i16, col: col as i16 }
|
||||
}
|
||||
}
|
||||
|
||||
/// Mode info stored for every 4x4 luma position (the arrays the spec indexes by
|
||||
/// `[ row ][ col ]` in mode-info units).
|
||||
#[derive(Clone, Default)]
|
||||
pub struct MiInfo {
|
||||
/// Frame size in mode info units.
|
||||
pub cols: usize,
|
||||
pub rows: usize,
|
||||
/// Stored region: origin and width (the whole frame unless a tile's private copy).
|
||||
pub ox: usize,
|
||||
pub oy: usize,
|
||||
pub stride: usize,
|
||||
pub region_rows: usize,
|
||||
pub y_mode: Vec<u8>,
|
||||
pub uv_mode: Vec<u8>,
|
||||
pub ref_frame: Vec<[i8; 2]>,
|
||||
pub mv: Vec<[Mv; 2]>,
|
||||
pub is_inter: Vec<bool>,
|
||||
pub skip_mode: Vec<bool>,
|
||||
pub skip: Vec<bool>,
|
||||
pub tx_size: Vec<u8>,
|
||||
pub inter_tx_size: Vec<u8>,
|
||||
pub mi_size: Vec<u8>,
|
||||
pub segment_id: Vec<u8>,
|
||||
pub palette_size: [Vec<u8>; 2],
|
||||
pub palette_colors: [Vec<[u16; 8]>; 2],
|
||||
pub delta_lf: Vec<[i8; 4]>,
|
||||
pub comp_group_idx: Vec<u8>,
|
||||
pub compound_idx: Vec<u8>,
|
||||
pub interp_filter: Vec<[u8; 2]>,
|
||||
pub motion_mode: Vec<u8>,
|
||||
/// TxTypes[ row ][ col ] (luma 4x4 units).
|
||||
pub tx_type: Vec<u8>,
|
||||
/// RefFrames[ row ][ col ] has been written for this frame.
|
||||
pub written: Vec<bool>,
|
||||
}
|
||||
|
||||
impl MiInfo {
|
||||
/// `palette`: whether palette mode can occur (allow_screen_content_tools); without it the
|
||||
/// palette arrays (40 bytes per unit) stay empty.
|
||||
pub fn new(cols: usize, rows: usize, palette: bool) -> MiInfo {
|
||||
MiInfo::region(cols, rows, 0, 0, cols, rows, palette)
|
||||
}
|
||||
|
||||
/// Storage for the w x h mode info units at (ox, oy) of a cols x rows frame.
|
||||
pub fn region(cols: usize, rows: usize, ox: usize, oy: usize, w: usize, h: usize, palette: bool) -> MiInfo {
|
||||
let n = w * h;
|
||||
let np = if palette { n } else { 0 };
|
||||
MiInfo {
|
||||
cols,
|
||||
rows,
|
||||
ox,
|
||||
oy,
|
||||
stride: w,
|
||||
region_rows: h,
|
||||
y_mode: vec![0; n],
|
||||
uv_mode: vec![0; n],
|
||||
ref_frame: vec![[0, -1]; n],
|
||||
mv: vec![[Mv::ZERO; 2]; n],
|
||||
is_inter: vec![false; n],
|
||||
skip_mode: vec![false; n],
|
||||
skip: vec![false; n],
|
||||
tx_size: vec![0; n],
|
||||
inter_tx_size: vec![0; n],
|
||||
mi_size: vec![0; n],
|
||||
segment_id: vec![0; n],
|
||||
palette_size: [vec![0; np], vec![0; np]],
|
||||
palette_colors: [vec![[0; 8]; np], vec![[0; 8]; np]],
|
||||
delta_lf: vec![[0; 4]; n],
|
||||
comp_group_idx: vec![0; n],
|
||||
compound_idx: vec![0; n],
|
||||
interp_filter: vec![[0; 2]; n],
|
||||
motion_mode: vec![0; n],
|
||||
tx_type: vec![0; n],
|
||||
written: vec![false; n],
|
||||
}
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
pub fn idx(&self, row: usize, col: usize) -> usize {
|
||||
debug_assert!(col >= self.ox && col < self.ox + self.stride && row >= self.oy, "mode info ({row}, {col}) outside the stored region");
|
||||
(row - self.oy) * self.stride + col - self.ox
|
||||
}
|
||||
|
||||
/// Copy this region into the whole-frame `dst`.
|
||||
pub fn copy_into(&self, dst: &mut MiInfo) {
|
||||
let (w, h) = (self.stride, self.region_rows);
|
||||
macro_rules! copy {
|
||||
($($f:ident)*) => {$(
|
||||
for y in 0..h {
|
||||
let d = dst.idx(self.oy + y, self.ox);
|
||||
dst.$f[d..d + w].copy_from_slice(&self.$f[y * w..(y + 1) * w]);
|
||||
}
|
||||
)*};
|
||||
}
|
||||
copy!(y_mode uv_mode ref_frame mv is_inter skip_mode skip tx_size inter_tx_size mi_size segment_id delta_lf comp_group_idx compound_idx interp_filter motion_mode tx_type written);
|
||||
for k in (0..2).filter(|&k| !self.palette_size[k].is_empty()) {
|
||||
for y in 0..h {
|
||||
let d = dst.idx(self.oy + y, self.ox);
|
||||
dst.palette_size[k][d..d + w].copy_from_slice(&self.palette_size[k][y * w..(y + 1) * w]);
|
||||
dst.palette_colors[k][d..d + w].copy_from_slice(&self.palette_colors[k][y * w..(y + 1) * w]);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,455 @@
|
||||
//! Film grain synthesis (7.18.3), applied to output pictures only (references stay clean).
|
||||
|
||||
use crate::Picture;
|
||||
use crate::header::{FilmGrainParams, SequenceHeader};
|
||||
use crate::spec_tables::{GAUSSIAN_SEQUENCE, MC_IDENTITY};
|
||||
|
||||
#[inline]
|
||||
/// One plane's scaling lookup (7.18.3.4) from its `n` points. The header rejects points whose
|
||||
/// values don't strictly increase; a pair that still doesn't (defence in depth) is skipped
|
||||
/// instead of dividing by zero. No points: all zero.
|
||||
fn scaling_lut(n: usize, xs: &[u8], ys: &[u8]) -> [i32; 256] {
|
||||
let mut l = [0i32; 256];
|
||||
let n = n.min(xs.len()).min(ys.len());
|
||||
let (Some(&x0), Some(&y0)) = (xs.first(), ys.first()) else { return l };
|
||||
if n == 0 {
|
||||
return l;
|
||||
}
|
||||
for v in l.iter_mut().take(x0 as usize) {
|
||||
*v = y0 as i32;
|
||||
}
|
||||
for i in 0..n - 1 {
|
||||
let (xa, xb, ya, yb) = (xs[i] as i32, xs[i + 1] as i32, ys[i] as i32, ys[i + 1] as i32);
|
||||
let dx = xb - xa;
|
||||
if dx <= 0 {
|
||||
continue;
|
||||
}
|
||||
let delta = (yb - ya) * ((65536 + (dx >> 1)) / dx);
|
||||
for x in 0..dx {
|
||||
if let Some(v) = l.get_mut((xa + x) as usize) {
|
||||
*v = ya + ((x * delta + 32768) >> 16);
|
||||
}
|
||||
}
|
||||
}
|
||||
let (xl, yl) = (xs[n - 1], ys[n - 1]);
|
||||
for v in l.iter_mut().skip(xl as usize) {
|
||||
*v = yl as i32;
|
||||
}
|
||||
l
|
||||
}
|
||||
|
||||
fn round2(x: i32, n: u32) -> i32 {
|
||||
if n == 0 { x } else { (x + (1 << (n - 1))) >> n }
|
||||
}
|
||||
|
||||
struct Rng(u32);
|
||||
|
||||
impl Rng {
|
||||
/// get_random_number( bits ) (7.18.3.2)
|
||||
fn next(&mut self, bits: u32) -> i32 {
|
||||
let r = self.0;
|
||||
let bit = (r ^ (r >> 1) ^ (r >> 3) ^ (r >> 12)) & 1;
|
||||
let r = (r >> 1) | (bit << 15);
|
||||
self.0 = r;
|
||||
((r >> (16 - bits)) & ((1 << bits) - 1)) as i32
|
||||
}
|
||||
}
|
||||
|
||||
/// Picture-level parameters shared by the grain synthesis stages.
|
||||
struct GrainCtx<'a> {
|
||||
g: &'a FilmGrainParams,
|
||||
bd: u32,
|
||||
mono: bool,
|
||||
sub_x: usize,
|
||||
sub_y: usize,
|
||||
w: usize,
|
||||
h: usize,
|
||||
grain_min: i32,
|
||||
grain_max: i32,
|
||||
}
|
||||
|
||||
impl GrainCtx<'_> {
|
||||
fn num_planes(&self) -> usize {
|
||||
if self.mono { 1 } else { 3 }
|
||||
}
|
||||
|
||||
fn grain_shift(&self) -> u32 {
|
||||
12 - self.bd + self.g.grain_scale_shift
|
||||
}
|
||||
|
||||
fn ar_shift(&self) -> u32 {
|
||||
self.g.ar_coeff_shift_minus_6 + 6
|
||||
}
|
||||
|
||||
fn lag(&self) -> i32 {
|
||||
self.g.ar_coeff_lag as i32
|
||||
}
|
||||
|
||||
fn stripe_w(&self) -> usize {
|
||||
self.w + 64
|
||||
}
|
||||
}
|
||||
|
||||
type GrainTemplate = Vec<[i32; 82]>;
|
||||
|
||||
/// The luma, Cb and Cr grain templates (7.18.3.3).
|
||||
struct Templates {
|
||||
luma: GrainTemplate,
|
||||
cb: GrainTemplate,
|
||||
cr: GrainTemplate,
|
||||
}
|
||||
|
||||
impl Templates {
|
||||
fn plane(&self, plane: usize) -> &GrainTemplate {
|
||||
match plane {
|
||||
0 => &self.luma,
|
||||
1 => &self.cb,
|
||||
_ => &self.cr,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn luma_ar_sum(g: &FilmGrainParams, luma: &[[i32; 82]], lag: i32, y: usize, x: usize) -> i32 {
|
||||
let mut s = 0;
|
||||
let mut pos = 0;
|
||||
'outer: for dr in -lag..=0 {
|
||||
for dc in -lag..=lag {
|
||||
if dr == 0 && dc == 0 {
|
||||
break 'outer;
|
||||
}
|
||||
let c = g.ar_coeffs_y_plus_128[pos] as i32 - 128;
|
||||
s += luma[(y as i32 + dr) as usize][(x as i32 + dc) as usize] * c;
|
||||
pos += 1;
|
||||
}
|
||||
}
|
||||
s
|
||||
}
|
||||
|
||||
fn luma_grain(c: &GrainCtx, rng: &mut Rng) -> GrainTemplate {
|
||||
let g = c.g;
|
||||
let shift = c.grain_shift();
|
||||
let mut luma = vec![[0i32; 82]; 73];
|
||||
for row in luma.iter_mut() {
|
||||
for v in row.iter_mut() {
|
||||
let gv = if g.num_y_points > 0 { GAUSSIAN_SEQUENCE[rng.next(11) as usize] as i32 } else { 0 };
|
||||
*v = round2(gv, shift);
|
||||
}
|
||||
}
|
||||
let ar_shift = c.ar_shift();
|
||||
let lag = c.lag();
|
||||
for y in 3..73 {
|
||||
for x in 3..82 - 3 {
|
||||
let s = luma_ar_sum(g, &luma, lag, y, x);
|
||||
luma[y][x] = (luma[y][x] + round2(s, ar_shift)).clamp(c.grain_min, c.grain_max);
|
||||
}
|
||||
}
|
||||
luma
|
||||
}
|
||||
|
||||
/// The luma contribution to the chroma autoregressive filter at chroma position (y, x).
|
||||
fn chroma_luma_term(c: &GrainCtx, luma: &[[i32; 82]], y: usize, x: usize) -> i32 {
|
||||
let mut l = 0;
|
||||
let lx = ((x - 3) << c.sub_x) + 3;
|
||||
let ly = ((y - 3) << c.sub_y) + 3;
|
||||
for i in 0..=c.sub_y {
|
||||
for j in 0..=c.sub_x {
|
||||
l += luma[ly + i][lx + j];
|
||||
}
|
||||
}
|
||||
round2(l, (c.sub_x + c.sub_y) as u32)
|
||||
}
|
||||
|
||||
fn chroma_ar_sums(c: &GrainCtx, t: &Templates, y: usize, x: usize) -> (i32, i32) {
|
||||
let g = c.g;
|
||||
let lag = c.lag();
|
||||
let mut s0 = 0;
|
||||
let mut s1 = 0;
|
||||
let mut pos = 0;
|
||||
'outer: for dr in -lag..=0 {
|
||||
for dc in -lag..=lag {
|
||||
let c0 = g.ar_coeffs_cb_plus_128[pos] as i32 - 128;
|
||||
let c1 = g.ar_coeffs_cr_plus_128[pos] as i32 - 128;
|
||||
if dr == 0 && dc == 0 {
|
||||
if g.num_y_points > 0 {
|
||||
let l = chroma_luma_term(c, &t.luma, y, x);
|
||||
s0 += l * c0;
|
||||
s1 += l * c1;
|
||||
}
|
||||
break 'outer;
|
||||
}
|
||||
let (yy, xx) = ((y as i32 + dr) as usize, (x as i32 + dc) as usize);
|
||||
s0 += t.cb[yy][xx] * c0;
|
||||
s1 += t.cr[yy][xx] * c1;
|
||||
pos += 1;
|
||||
}
|
||||
}
|
||||
(s0, s1)
|
||||
}
|
||||
|
||||
/// Chroma grain templates; zero for monochrome pictures.
|
||||
fn chroma_grain(c: &GrainCtx, rng: &mut Rng, luma: GrainTemplate) -> Templates {
|
||||
let g = c.g;
|
||||
let mut t = Templates { luma, cb: vec![[0i32; 82]; 73], cr: vec![[0i32; 82]; 73] };
|
||||
if c.mono {
|
||||
return t;
|
||||
}
|
||||
let shift = c.grain_shift();
|
||||
let ar_shift = c.ar_shift();
|
||||
let chroma_w = if c.sub_x != 0 { 44 } else { 82 };
|
||||
let chroma_h = if c.sub_y != 0 { 38 } else { 73 };
|
||||
for (arr, seed, on) in [(&mut t.cb, 0xb524u32, g.num_cb_points > 0), (&mut t.cr, 0x49d8, g.num_cr_points > 0)] {
|
||||
rng.0 = g.grain_seed ^ seed;
|
||||
for row in arr.iter_mut().take(chroma_h) {
|
||||
for v in row.iter_mut().take(chroma_w) {
|
||||
let gv = if on || g.chroma_scaling_from_luma { GAUSSIAN_SEQUENCE[rng.next(11) as usize] as i32 } else { 0 };
|
||||
*v = round2(gv, shift);
|
||||
}
|
||||
}
|
||||
}
|
||||
for y in 3..chroma_h {
|
||||
for x in 3..chroma_w - 3 {
|
||||
let (s0, s1) = chroma_ar_sums(c, &t, y, x);
|
||||
t.cb[y][x] = (t.cb[y][x] + round2(s0, ar_shift)).clamp(c.grain_min, c.grain_max);
|
||||
t.cr[y][x] = (t.cr[y][x] + round2(s1, ar_shift)).clamp(c.grain_min, c.grain_max);
|
||||
}
|
||||
}
|
||||
t
|
||||
}
|
||||
|
||||
/// Scaling lookup tables (7.18.3.4), one per plane.
|
||||
fn scaling_luts(g: &FilmGrainParams, num_planes: usize) -> [[i32; 256]; 3] {
|
||||
let mut lut = [[0i32; 256]; 3];
|
||||
for (plane, l) in lut.iter_mut().enumerate().take(num_planes) {
|
||||
let (n, xs, ys) = if plane == 0 || g.chroma_scaling_from_luma {
|
||||
(g.num_y_points, &g.point_y_value, &g.point_y_scaling)
|
||||
} else if plane == 1 {
|
||||
(g.num_cb_points, &g.point_cb_value, &g.point_cb_scaling)
|
||||
} else {
|
||||
(g.num_cr_points, &g.point_cr_value, &g.point_cr_scaling)
|
||||
};
|
||||
*l = scaling_lut(n, xs, ys);
|
||||
}
|
||||
lut
|
||||
}
|
||||
|
||||
fn scale_lookup(lut: &[[i32; 256]; 3], bd: u32, plane: usize, index: i32) -> i32 {
|
||||
let sh = bd - 8;
|
||||
let x = index >> sh;
|
||||
let rem = index - (x << sh);
|
||||
if bd == 8 || x == 255 {
|
||||
lut[plane][x as usize]
|
||||
} else {
|
||||
let start = lut[plane][x as usize];
|
||||
let end = lut[plane][x as usize + 1];
|
||||
start + round2((end - start) * rem, sh)
|
||||
}
|
||||
}
|
||||
|
||||
/// Fills one plane of a noise stripe from the grain template at stripe column `x`.
|
||||
fn fill_stripe_plane(c: &GrainCtx, src: &GrainTemplate, s: &mut [i32], plane: usize, offsets: (usize, usize), x: usize) {
|
||||
let (off_x, off_y) = offsets;
|
||||
let stripe_w = c.stripe_w();
|
||||
let overlap = c.g.overlap_flag;
|
||||
let psx = if plane > 0 { c.sub_x } else { 0 };
|
||||
let psy = if plane > 0 { c.sub_y } else { 0 };
|
||||
let pox = if psx != 0 { 6 + off_x } else { 9 + off_x * 2 };
|
||||
let poy = if psy != 0 { 6 + off_y } else { 9 + off_y * 2 };
|
||||
for i in 0..(34 >> psy) {
|
||||
for j in 0..(34 >> psx) {
|
||||
let mut gv = src[poy + i][pox + j];
|
||||
let (idx, blend) = if psx == 0 { (i * stripe_w + x * 2 + j, j < 2) } else { (i * stripe_w + x + j, j == 0) };
|
||||
if blend && overlap && x > 0 {
|
||||
let old = s[idx];
|
||||
gv = if psx != 0 {
|
||||
old * 23 + gv * 22
|
||||
} else if j == 0 {
|
||||
old * 27 + gv * 17
|
||||
} else {
|
||||
old * 17 + gv * 27
|
||||
};
|
||||
gv = round2(gv, 5).clamp(c.grain_min, c.grain_max);
|
||||
}
|
||||
s[idx] = gv;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Noise stripes (7.18.3.5).
|
||||
struct Stripes {
|
||||
data: Vec<[Vec<i32>; 3]>,
|
||||
stripe_w: usize,
|
||||
overlap: bool,
|
||||
sub_y: usize,
|
||||
grain_min: i32,
|
||||
grain_max: i32,
|
||||
}
|
||||
|
||||
impl Stripes {
|
||||
fn build(c: &GrainCtx, t: &Templates, rng: &mut Rng) -> Stripes {
|
||||
let g = c.g;
|
||||
let stripe_w = c.stripe_w();
|
||||
let n_stripes = (c.h.div_ceil(2)).div_ceil(16).max(1);
|
||||
let mut data: Vec<[Vec<i32>; 3]> = (0..n_stripes).map(|_| [vec![0; 34 * stripe_w], vec![0; 34 * stripe_w], vec![0; 34 * stripe_w]]).collect();
|
||||
for (luma_num, stripe) in data.iter_mut().enumerate() {
|
||||
let mut r = g.grain_seed;
|
||||
r ^= (((luma_num * 37 + 178) & 255) << 8) as u32;
|
||||
r ^= ((luma_num * 173 + 105) & 255) as u32;
|
||||
rng.0 = r;
|
||||
let mut x = 0;
|
||||
while x < c.w.div_ceil(2) {
|
||||
let rand = rng.next(8) as usize;
|
||||
let offsets = (rand >> 4, rand & 15);
|
||||
for (plane, s) in stripe.iter_mut().enumerate().take(c.num_planes()) {
|
||||
fill_stripe_plane(c, t.plane(plane), s, plane, offsets, x);
|
||||
}
|
||||
x += 16;
|
||||
}
|
||||
}
|
||||
Stripes { data, stripe_w, overlap: g.overlap_flag, sub_y: c.sub_y, grain_min: c.grain_min, grain_max: c.grain_max }
|
||||
}
|
||||
|
||||
fn noise_at(&self, plane: usize, y: usize, x: usize) -> i32 {
|
||||
let psy = if plane > 0 { self.sub_y } else { 0 };
|
||||
let luma_num = y >> (5 - psy);
|
||||
let i = y - (luma_num << (5 - psy));
|
||||
let mut gv = self.data[luma_num][plane][i * self.stripe_w + x];
|
||||
if psy == 0 {
|
||||
if i < 2 && luma_num > 0 && self.overlap {
|
||||
let old = self.data[luma_num - 1][plane][(i + 32) * self.stripe_w + x];
|
||||
gv = if i == 0 { old * 27 + gv * 17 } else { old * 17 + gv * 27 };
|
||||
gv = round2(gv, 5).clamp(self.grain_min, self.grain_max);
|
||||
}
|
||||
} else if i < 1 && luma_num > 0 && self.overlap {
|
||||
let old = self.data[luma_num - 1][plane][(i + 16) * self.stripe_w + x];
|
||||
gv = round2(old * 23 + gv * 22, 5).clamp(self.grain_min, self.grain_max);
|
||||
}
|
||||
gv
|
||||
}
|
||||
}
|
||||
|
||||
/// Output clipping range and scaling shift for the blend step (7.18.3.5).
|
||||
struct BlendRange {
|
||||
min_value: i32,
|
||||
max_luma: i32,
|
||||
max_chroma: i32,
|
||||
pix_max: i32,
|
||||
scaling_shift: u32,
|
||||
}
|
||||
|
||||
impl BlendRange {
|
||||
fn new(c: &GrainCtx, matrix_coefficients: usize) -> BlendRange {
|
||||
let bd = c.bd;
|
||||
let (min_value, max_luma, max_chroma) = if c.g.clip_to_restricted_range {
|
||||
let ml = 235 << (bd - 8);
|
||||
(16 << (bd - 8), ml, if matrix_coefficients == MC_IDENTITY { ml } else { 240 << (bd - 8) })
|
||||
} else {
|
||||
let m = (256 << (bd - 8)) - 1;
|
||||
(0, m, m)
|
||||
};
|
||||
BlendRange { min_value, max_luma, max_chroma, pix_max: (1i32 << bd) - 1, scaling_shift: c.g.grain_scaling_minus_8 + 8 }
|
||||
}
|
||||
}
|
||||
|
||||
struct BlendEnv<'a> {
|
||||
c: &'a GrainCtx<'a>,
|
||||
lut: &'a [[i32; 256]; 3],
|
||||
stripes: &'a Stripes,
|
||||
range: &'a BlendRange,
|
||||
}
|
||||
|
||||
impl BlendEnv<'_> {
|
||||
fn blend_chroma(&self, py: &[u16], pu: &mut [u16], pv: &mut [u16]) {
|
||||
let c = self.c;
|
||||
let g = c.g;
|
||||
let (w, h, sub_x, sub_y, bd) = (c.w, c.h, c.sub_x, c.sub_y, c.bd);
|
||||
let cw = (w + sub_x) >> sub_x;
|
||||
let ch = (h + sub_y) >> sub_y;
|
||||
for y in 0..ch {
|
||||
for x in 0..cw {
|
||||
let lx = x << sub_x;
|
||||
let ly = y << sub_y;
|
||||
let lnx = (lx + 1).min(w - 1);
|
||||
let avg = if sub_x != 0 { round2(py[ly * w + lx] as i32 + py[ly * w + lnx] as i32, 1) } else { py[ly * w + lx] as i32 };
|
||||
for (plane, out, on, luma_mult, mult, offset) in [
|
||||
(1usize, &mut *pu, g.num_cb_points > 0, g.cb_luma_mult, g.cb_mult, g.cb_offset),
|
||||
(2, &mut *pv, g.num_cr_points > 0, g.cr_luma_mult, g.cr_mult, g.cr_offset),
|
||||
] {
|
||||
if !(on || g.chroma_scaling_from_luma) {
|
||||
continue;
|
||||
}
|
||||
let orig = out[y * cw + x] as i32;
|
||||
let merged = if g.chroma_scaling_from_luma {
|
||||
avg
|
||||
} else {
|
||||
let combined = avg * (luma_mult as i32 - 128) + orig * (mult as i32 - 128);
|
||||
((combined >> 6) + ((offset as i32 - 256) << (bd - 8))).clamp(0, self.range.pix_max)
|
||||
};
|
||||
let noise = round2(scale_lookup(self.lut, bd, plane, merged) * self.stripes.noise_at(plane, y, x), self.range.scaling_shift);
|
||||
out[y * cw + x] = (orig + noise).clamp(self.range.min_value, self.range.max_chroma) as u16;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn blend_luma(&self, py: &mut [u16]) {
|
||||
let (w, h) = (self.c.w, self.c.h);
|
||||
for y in 0..h {
|
||||
for x in 0..w {
|
||||
let orig = py[y * w + x] as i32;
|
||||
let noise = round2(scale_lookup(self.lut, self.c.bd, 0, orig) * self.stripes.noise_at(0, y, x), self.range.scaling_shift);
|
||||
py[y * w + x] = (orig + noise).clamp(self.range.min_value, self.range.max_luma) as u16;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn apply(pic: &mut Picture, g: &FilmGrainParams, seq: &SequenceHeader) {
|
||||
let _ = seq;
|
||||
let bd = pic.bit_depth as u32;
|
||||
let grain_center = 128i32 << (bd - 8);
|
||||
let c = GrainCtx {
|
||||
g,
|
||||
bd,
|
||||
mono: pic.mono_chrome,
|
||||
sub_x: pic.subsampling_x as usize,
|
||||
sub_y: pic.subsampling_y as usize,
|
||||
w: pic.width as usize,
|
||||
h: pic.height as usize,
|
||||
grain_min: -grain_center,
|
||||
grain_max: (256i32 << (bd - 8)) - 1 - grain_center,
|
||||
};
|
||||
|
||||
// Generate grain (7.18.3.3).
|
||||
let mut rng = Rng(g.grain_seed);
|
||||
let luma = luma_grain(&c, &mut rng);
|
||||
let templates = chroma_grain(&c, &mut rng, luma);
|
||||
let lut = scaling_luts(g, c.num_planes());
|
||||
let stripes = Stripes::build(&c, &templates, &mut rng);
|
||||
|
||||
// Blend (7.18.3.5).
|
||||
let range = BlendRange::new(&c, pic.matrix_coefficients as usize);
|
||||
let env = BlendEnv { c: &c, lut: &lut, stripes: &stripes, range: &range };
|
||||
if !c.mono {
|
||||
let [py, pu, pv] = &mut pic.planes;
|
||||
env.blend_chroma(py, pu, pv);
|
||||
}
|
||||
if g.num_y_points > 0 {
|
||||
env.blend_luma(&mut pic.planes[0]);
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn scaling_lut_interpolates_and_tolerates_duplicate_points() {
|
||||
let l = scaling_lut(2, &[0, 255], &[0, 255]);
|
||||
assert_eq!((l[0], l[128], l[255]), (0, 128, 255));
|
||||
// Duplicate and descending x values don't divide by zero.
|
||||
let l = scaling_lut(4, &[10, 10, 5, 200], &[20, 40, 60, 80]);
|
||||
assert_eq!(l[0], 20);
|
||||
assert_eq!(l[255], 80);
|
||||
assert_eq!(scaling_lut(0, &[], &[]), [0; 256]);
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,839 @@
|
||||
//! Inter prediction (spec 7.11.3): motion vector scaling, block inter prediction, warped
|
||||
//! motion, masks (wedge, difference weight, inter-intra), distance weights and OBMC.
|
||||
|
||||
use std::sync::OnceLock;
|
||||
|
||||
use crate::frame::Plane;
|
||||
use crate::mvpred::round2_signed64;
|
||||
use crate::spec_tables::{
|
||||
BLOCK_8X8, BLOCK_SIZES, COMPOUND_AVERAGE, COMPOUND_DIFFWTD, COMPOUND_DISTANCE, COMPOUND_INTRA, COMPOUND_WEDGE, DIV_LUT, DIV_LUT_BITS,
|
||||
DIV_LUT_PREC_BITS, EIGHTTAP, EIGHTTAP_SHARP, EIGHTTAP_SMOOTH, FILTER_BITS, GLOBAL_GLOBALMV, GLOBALMV, II_H_PRED, II_SMOOTH_PRED, II_V_PRED,
|
||||
II_WEIGHTS_1D, INTRA_FRAME, LAST_FRAME, LOCALWARP, LS_MV_MAX, MASK_MASTER_SIZE, MAX_FRAME_DISTANCE, MAX_SB_SIZE, MI_HEIGHT_LOG2, MI_WIDTH_LOG2,
|
||||
NUM_4X4_BLOCKS_HIGH, NUM_4X4_BLOCKS_WIDE, OBMC, OBMC_MASK_2, OBMC_MASK_4, OBMC_MASK_8, OBMC_MASK_16, OBMC_MASK_32, QUANT_DIST_LOOKUP,
|
||||
QUANT_DIST_WEIGHT, REF_SCALE_SHIFT, SCALE_SUBPEL_BITS, SUBPEL_BITS, SUBPEL_FILTERS, SUBPEL_MASK, TRANSLATION, WARP_PARAM_REDUCE_BITS,
|
||||
WARPED_FILTERS, WARPEDDIFF_PREC_BITS, WARPEDMODEL_NONDIAGAFFINE_CLAMP, WARPEDMODEL_PREC_BITS, WARPEDMODEL_TRANS_CLAMP, WARPEDPIXEL_PREC_SHIFTS,
|
||||
WEDGE_BITS, WEDGE_CODEBOOK, WEDGE_HORIZONTAL, WEDGE_MASTER_OBLIQUE_EVEN, WEDGE_MASTER_OBLIQUE_ODD, WEDGE_MASTER_VERTICAL, WEDGE_OBLIQUE27,
|
||||
WEDGE_OBLIQUE63, WEDGE_OBLIQUE117, WEDGE_OBLIQUE153, WEDGE_TYPES, WEDGE_VERTICAL,
|
||||
};
|
||||
use crate::tile::TileDecoder;
|
||||
|
||||
const PRED_STRIDE: usize = 128;
|
||||
|
||||
#[inline(always)]
|
||||
fn round2(x: i32, n: u32) -> i32 {
|
||||
if n == 0 { x } else { (x + (1 << (n - 1))) >> n }
|
||||
}
|
||||
|
||||
/// Rounding variables (7.11.3.2): (InterRound0, InterRound1, InterPostRound).
|
||||
pub(crate) fn rounding(is_compound: bool, bd: u32) -> (u32, u32, u32) {
|
||||
let mut r0 = 3;
|
||||
let mut r1 = if is_compound { 7 } else { 11 };
|
||||
if bd == 12 {
|
||||
r0 += 2;
|
||||
if !is_compound {
|
||||
r1 -= 2;
|
||||
}
|
||||
}
|
||||
(r0, r1, 2 * FILTER_BITS as u32 - (r0 + r1))
|
||||
}
|
||||
|
||||
/// Resolve divisor process (7.11.3.7): (divShift, divFactor).
|
||||
fn resolve_divisor(d: i64) -> (i32, i64) {
|
||||
let a = d.unsigned_abs();
|
||||
let n = 63 - a.leading_zeros() as i32;
|
||||
let e = a as i64 - (1i64 << n);
|
||||
let f = if n > DIV_LUT_BITS as i32 { round2_signed64(e, (n - DIV_LUT_BITS as i32) as u32) } else { e << (DIV_LUT_BITS as i32 - n) };
|
||||
let shift = n + DIV_LUT_PREC_BITS as i32;
|
||||
let factor = DIV_LUT[f as usize] as i64;
|
||||
(shift, if d < 0 { -factor } else { factor })
|
||||
}
|
||||
|
||||
/// Setup shear process (7.11.3.6): (warpValid, alpha, beta, gamma, delta).
|
||||
pub(crate) fn setup_shear(p: &[i32; 6]) -> (bool, i32, i32, i32, i32) {
|
||||
let prec = 1i64 << WARPEDMODEL_PREC_BITS;
|
||||
let alpha0 = (p[2] as i64 - prec).clamp(-32768, 32767);
|
||||
let beta0 = (p[3] as i64).clamp(-32768, 32767);
|
||||
let (div_shift, div_factor) = resolve_divisor(p[2] as i64);
|
||||
let v = (p[4] as i64) << WARPEDMODEL_PREC_BITS;
|
||||
let gamma0 = round2_signed_wide(v as i128 * div_factor as i128, div_shift as u32).clamp(-32768, 32767);
|
||||
let w = p[3] as i64 * p[4] as i64;
|
||||
let delta0 = (p[5] as i64 - round2_signed_wide(w as i128 * div_factor as i128, div_shift as u32) - prec).clamp(-32768, 32767);
|
||||
let rb = WARP_PARAM_REDUCE_BITS as u32;
|
||||
let red = |x: i64| (round2_signed64(x, rb) << rb) as i32;
|
||||
let (alpha, beta, gamma, delta) = (red(alpha0), red(beta0), red(gamma0), red(delta0));
|
||||
let valid =
|
||||
!(4 * alpha.abs() + 7 * beta.abs() >= (1 << WARPEDMODEL_PREC_BITS) || 4 * gamma.abs() + 4 * delta.abs() >= (1 << WARPEDMODEL_PREC_BITS));
|
||||
(valid, alpha, beta, gamma, delta)
|
||||
}
|
||||
|
||||
fn round2_signed_wide(x: i128, n: u32) -> i64 {
|
||||
if n == 0 {
|
||||
return x as i64;
|
||||
}
|
||||
(if x >= 0 { (x + (1 << (n - 1))) >> n } else { -((-x + (1 << (n - 1))) >> n) }) as i64
|
||||
}
|
||||
|
||||
/// WedgeMasks[ bsize ][ flipSign ][ wedge ] (7.11.3.11), stored as w*h bytes each.
|
||||
fn wedge_masks() -> &'static Vec<Vec<[Vec<u8>; 2]>> {
|
||||
static M: OnceLock<Vec<Vec<[Vec<u8>; 2]>>> = OnceLock::new();
|
||||
M.get_or_init(|| {
|
||||
let n = MASK_MASTER_SIZE;
|
||||
let mut master = vec![[[0u8; 64]; 64]; 6];
|
||||
for j in 0..n {
|
||||
let mut shift = (n / 4) as i32;
|
||||
let mut i = 0;
|
||||
while i < n {
|
||||
master[WEDGE_OBLIQUE63][i][j] = WEDGE_MASTER_OBLIQUE_EVEN[(j as i32 - shift).clamp(0, n as i32 - 1) as usize];
|
||||
shift -= 1;
|
||||
master[WEDGE_OBLIQUE63][i + 1][j] = WEDGE_MASTER_OBLIQUE_ODD[(j as i32 - shift).clamp(0, n as i32 - 1) as usize];
|
||||
master[WEDGE_VERTICAL][i][j] = WEDGE_MASTER_VERTICAL[j];
|
||||
master[WEDGE_VERTICAL][i + 1][j] = WEDGE_MASTER_VERTICAL[j];
|
||||
i += 2;
|
||||
}
|
||||
}
|
||||
for i in 0..n {
|
||||
for j in 0..n {
|
||||
let msk = master[WEDGE_OBLIQUE63][i][j];
|
||||
master[WEDGE_OBLIQUE27][j][i] = msk;
|
||||
master[WEDGE_OBLIQUE117][i][n - 1 - j] = 64 - msk;
|
||||
master[WEDGE_OBLIQUE153][n - 1 - j][i] = 64 - msk;
|
||||
master[WEDGE_HORIZONTAL][j][i] = master[WEDGE_VERTICAL][i][j];
|
||||
}
|
||||
}
|
||||
let mut out = vec![Vec::new(); BLOCK_SIZES];
|
||||
for bsize in BLOCK_8X8..BLOCK_SIZES {
|
||||
if WEDGE_BITS[bsize] == 0 {
|
||||
continue;
|
||||
}
|
||||
let w = 4 * NUM_4X4_BLOCKS_WIDE[bsize] as usize;
|
||||
let h = 4 * NUM_4X4_BLOCKS_HIGH[bsize] as usize;
|
||||
let w4 = NUM_4X4_BLOCKS_WIDE[bsize];
|
||||
let h4 = NUM_4X4_BLOCKS_HIGH[bsize];
|
||||
let shape = if h4 > w4 {
|
||||
0
|
||||
} else if h4 < w4 {
|
||||
1
|
||||
} else {
|
||||
2
|
||||
};
|
||||
for wedge in 0..WEDGE_TYPES {
|
||||
let cb = &WEDGE_CODEBOOK[shape][wedge];
|
||||
let dir = cb[0] as usize;
|
||||
let xoff = n / 2 - ((cb[1] as usize * w) >> 3);
|
||||
let yoff = n / 2 - ((cb[2] as usize * h) >> 3);
|
||||
let mut sum = 0usize;
|
||||
for i in 0..w {
|
||||
sum += master[dir][yoff][xoff + i] as usize;
|
||||
}
|
||||
for i in 1..h {
|
||||
sum += master[dir][yoff + i][xoff] as usize;
|
||||
}
|
||||
let avg = (sum + (w + h - 1) / 2) / (w + h - 1);
|
||||
let flip = (avg < 32) as usize;
|
||||
let mut a = vec![0u8; w * h];
|
||||
let mut b = vec![0u8; w * h];
|
||||
for i in 0..h {
|
||||
for j in 0..w {
|
||||
let m = master[dir][yoff + i][xoff + j];
|
||||
a[i * w + j] = m;
|
||||
b[i * w + j] = 64 - m;
|
||||
}
|
||||
}
|
||||
let pair = if flip == 0 { [a, b] } else { [b, a] };
|
||||
out[bsize].push(pair);
|
||||
}
|
||||
}
|
||||
out
|
||||
})
|
||||
}
|
||||
|
||||
impl TileDecoder<'_, '_> {
|
||||
/// Reference plane and its (lastX, lastY) for refIdx (-1: the current frame for intra BC).
|
||||
fn ref_plane_dims(&self, ref_idx: i32, plane: usize) -> (i32, i32) {
|
||||
let (sx, sy) = if plane > 0 { (self.fs.ssx as i32, self.fs.ssy as i32) } else { (0, 0) };
|
||||
let (uw, fh) = if ref_idx < 0 {
|
||||
((self.fs.fh.mi_cols * 4) as i32, (self.fs.fh.mi_rows * 4) as i32)
|
||||
} else {
|
||||
let ri = &self.fs.ref_info[ref_idx as usize];
|
||||
(ri.upscaled_width as i32, ri.frame_height as i32)
|
||||
};
|
||||
(((uw + sx) >> sx) - 1, ((fh + sy) >> sy) - 1)
|
||||
}
|
||||
|
||||
/// Motion vector scaling process (7.11.3.3): (startX, startY, stepX, stepY).
|
||||
fn mv_scaling(&self, plane: usize, ref_idx: i32, x: i32, y: i32, mv: [i32; 2]) -> (i32, i32, i32, i32) {
|
||||
let fh = &self.fs.fh;
|
||||
let (ref_uw, ref_h) = if ref_idx < 0 {
|
||||
(fh.upscaled_width as i64, fh.frame_height as i64)
|
||||
} else {
|
||||
let ri = &self.fs.ref_info[ref_idx as usize];
|
||||
(ri.upscaled_width as i64, ri.frame_height as i64)
|
||||
};
|
||||
let fw = fh.frame_width as i64;
|
||||
let fhh = fh.frame_height as i64;
|
||||
let rs = REF_SCALE_SHIFT as i64;
|
||||
let x_scale = ((ref_uw << rs) + fw / 2) / fw;
|
||||
let y_scale = ((ref_h << rs) + fhh / 2) / fhh;
|
||||
let (sx, sy) = if plane > 0 { (self.fs.ssx as i32, self.fs.ssy as i32) } else { (0, 0) };
|
||||
let half = 1i64 << (SUBPEL_BITS - 1);
|
||||
let orig_x = ((x as i64) << SUBPEL_BITS) + ((2 * mv[1]) >> sx) as i64 + half;
|
||||
let orig_y = ((y as i64) << SUBPEL_BITS) + ((2 * mv[0]) >> sy) as i64 + half;
|
||||
let base_x = orig_x * x_scale - (half << rs);
|
||||
let base_y = orig_y * y_scale - (half << rs);
|
||||
let off = (1i64 << (SCALE_SUBPEL_BITS - SUBPEL_BITS)) / 2;
|
||||
let sh = (REF_SCALE_SHIFT + SUBPEL_BITS - SCALE_SUBPEL_BITS) as u32;
|
||||
let start_x = round2_signed64(base_x, sh) + off;
|
||||
let start_y = round2_signed64(base_y, sh) + off;
|
||||
let step_x = round2_signed64(x_scale, (REF_SCALE_SHIFT - SCALE_SUBPEL_BITS) as u32);
|
||||
let step_y = round2_signed64(y_scale, (REF_SCALE_SHIFT - SCALE_SUBPEL_BITS) as u32);
|
||||
(start_x as i32, start_y as i32, step_x as i32, step_y as i32)
|
||||
}
|
||||
|
||||
fn ref_plane(&self, ref_idx: i32, plane: usize) -> &Plane {
|
||||
// A missing reference (rejected earlier for valid streams) predicts from the current frame.
|
||||
match usize::try_from(ref_idx).ok().and_then(|i| self.fs.refs.get(i)).and_then(|r| r.as_ref()) {
|
||||
Some(r) => &r.buf.planes[plane],
|
||||
None => &self.t.cur.planes[plane],
|
||||
}
|
||||
}
|
||||
|
||||
/// Block inter prediction process (7.11.3.4) into `pred` (stride PRED_STRIDE); `tmp` holds
|
||||
/// the intermediate (horizontally filtered) rows.
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
fn block_inter_prediction(
|
||||
&self,
|
||||
plane: usize,
|
||||
ref_idx: i32,
|
||||
x: i32,
|
||||
y: i32,
|
||||
x_step: i32,
|
||||
y_step: i32,
|
||||
w: usize,
|
||||
h: usize,
|
||||
filters: [u8; 2],
|
||||
rnd: (u32, u32, u32),
|
||||
pred: &mut [i32],
|
||||
tmp: &mut [i32],
|
||||
) {
|
||||
let refp = self.ref_plane(ref_idx, plane);
|
||||
let (last_x, last_y) = self.ref_plane_dims(ref_idx, plane);
|
||||
let mut fh = filters[1] as usize;
|
||||
if w <= 4 {
|
||||
if fh == EIGHTTAP || fh == EIGHTTAP_SHARP {
|
||||
fh = 4;
|
||||
} else if fh == EIGHTTAP_SMOOTH {
|
||||
fh = 5;
|
||||
}
|
||||
}
|
||||
let mut fv = filters[0] as usize;
|
||||
if h <= 4 {
|
||||
if fv == EIGHTTAP || fv == EIGHTTAP_SHARP {
|
||||
fv = 4;
|
||||
} else if fv == EIGHTTAP_SMOOTH {
|
||||
fv = 5;
|
||||
}
|
||||
}
|
||||
let unit = 1 << SCALE_SUBPEL_BITS;
|
||||
if x_step == unit && y_step == unit {
|
||||
mc_unscaled(
|
||||
refp,
|
||||
last_x,
|
||||
last_y,
|
||||
x,
|
||||
y,
|
||||
w,
|
||||
h,
|
||||
&SUBPEL_FILTERS[fh][((x >> 6) & SUBPEL_MASK as i32) as usize],
|
||||
&SUBPEL_FILTERS[fv][((y >> 6) & SUBPEL_MASK as i32) as usize],
|
||||
rnd,
|
||||
pred,
|
||||
tmp,
|
||||
);
|
||||
return;
|
||||
}
|
||||
let inter_h = ((((h as i32 - 1) * y_step + (1 << SCALE_SUBPEL_BITS) - 1) >> SCALE_SUBPEL_BITS) + 8) as usize;
|
||||
for r in 0..inter_h {
|
||||
let ry = ((y >> 10) + r as i32 - 3).clamp(0, last_y) as usize;
|
||||
let row = refp.row(ry);
|
||||
for c in 0..w {
|
||||
let p = x + x_step * c as i32;
|
||||
let filt = &SUBPEL_FILTERS[fh][((p >> 6) & SUBPEL_MASK as i32) as usize];
|
||||
let mut s = 0i32;
|
||||
let bx = (p >> 10) - 3;
|
||||
for t in 0..8 {
|
||||
s += filt[t] as i32 * row[(bx + t as i32).clamp(0, last_x) as usize] as i32;
|
||||
}
|
||||
tmp[r * w + c] = round2(s, rnd.0);
|
||||
}
|
||||
}
|
||||
for r in 0..h {
|
||||
for c in 0..w {
|
||||
let p = (y & 1023) + y_step * r as i32;
|
||||
let filt = &SUBPEL_FILTERS[fv][((p >> 6) & SUBPEL_MASK as i32) as usize];
|
||||
let base = (p >> 10) as usize;
|
||||
let mut s = 0i32;
|
||||
for t in 0..8 {
|
||||
s += filt[t] as i32 * tmp[(base + t) * w + c];
|
||||
}
|
||||
pred[r * PRED_STRIDE + c] = round2(s, rnd.1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Block warp process (7.11.3.5) for the 8x8 section (i8, j8).
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
fn block_warp(
|
||||
&self,
|
||||
params: &[i32; 6],
|
||||
plane: usize,
|
||||
ref_idx: i32,
|
||||
x: i32,
|
||||
y: i32,
|
||||
i8: usize,
|
||||
j8: usize,
|
||||
w: usize,
|
||||
h: usize,
|
||||
rnd: (u32, u32, u32),
|
||||
pred: &mut [i32],
|
||||
) {
|
||||
let refp = self.ref_plane(ref_idx, plane);
|
||||
let (last_x, last_y) = self.ref_plane_dims(ref_idx, plane);
|
||||
let (sx, sy) = if plane > 0 { (self.fs.ssx as i32, self.fs.ssy as i32) } else { (0, 0) };
|
||||
let src_x = (x + j8 as i32 * 8 + 4) << sx;
|
||||
let src_y = (y + i8 as i32 * 8 + 4) << sy;
|
||||
let dst_x = params[2] as i64 * src_x as i64 + params[3] as i64 * src_y as i64 + params[0] as i64;
|
||||
let dst_y = params[4] as i64 * src_x as i64 + params[5] as i64 * src_y as i64 + params[1] as i64;
|
||||
let (_, alpha, beta, gamma, delta) = setup_shear(params);
|
||||
let x4 = dst_x >> sx;
|
||||
let y4 = dst_y >> sy;
|
||||
let ix4 = (x4 >> WARPEDMODEL_PREC_BITS) as i32;
|
||||
let sx4 = (x4 & ((1 << WARPEDMODEL_PREC_BITS) - 1)) as i32;
|
||||
let iy4 = (y4 >> WARPEDMODEL_PREC_BITS) as i32;
|
||||
let sy4 = (y4 & ((1 << WARPEDMODEL_PREC_BITS) - 1)) as i32;
|
||||
let mut inter = [[0i32; 8]; 15];
|
||||
for i1 in -7..8i32 {
|
||||
let row = refp.row((iy4 + i1).clamp(0, last_y) as usize);
|
||||
for i2 in -4..4i32 {
|
||||
let sxv = sx4 + alpha * i2 + beta * i1;
|
||||
let offs = (round2(sxv, WARPEDDIFF_PREC_BITS as u32) + WARPEDPIXEL_PREC_SHIFTS as i32) as usize;
|
||||
let mut s = 0i32;
|
||||
for i3 in 0..8i32 {
|
||||
s += WARPED_FILTERS[offs][i3 as usize] as i32 * row[(ix4 + i2 - 3 + i3).clamp(0, last_x) as usize] as i32;
|
||||
}
|
||||
inter[(i1 + 7) as usize][(i2 + 4) as usize] = round2(s, rnd.0);
|
||||
}
|
||||
}
|
||||
let lim_i = 4.min(h as i32 - i8 as i32 * 8 - 4);
|
||||
let lim_j = 4.min(w as i32 - j8 as i32 * 8 - 4);
|
||||
for i1 in -4..lim_i {
|
||||
for i2 in -4..lim_j {
|
||||
let syv = sy4 + gamma * i2 + delta * i1;
|
||||
let offs = (round2(syv, WARPEDDIFF_PREC_BITS as u32) + WARPEDPIXEL_PREC_SHIFTS as i32) as usize;
|
||||
let mut s = 0i32;
|
||||
for i3 in 0..8 {
|
||||
s += WARPED_FILTERS[offs][i3] as i32 * inter[(i1 + i3 as i32 + 4) as usize][(i2 + 4) as usize];
|
||||
}
|
||||
pred[(i8 * 8 + (i1 + 4) as usize) * PRED_STRIDE + j8 * 8 + (i2 + 4) as usize] = round2(s, rnd.1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Warp estimation process (7.11.3.8): sets LocalWarpParams / LocalValid.
|
||||
pub(crate) fn warp_estimation(&mut self) {
|
||||
let mut a = [[0i64; 2]; 2];
|
||||
let mut bx = [0i64; 2];
|
||||
let mut by = [0i64; 2];
|
||||
let mid_y = self.b.mi_row as i64 * 4 + self.b.bh4 as i64 * 2 - 1;
|
||||
let mid_x = self.b.mi_col as i64 * 4 + self.b.bw4 as i64 * 2 - 1;
|
||||
let suy = mid_y * 8;
|
||||
let sux = mid_x * 8;
|
||||
let duy = suy + self.b.mv_i[0][0] as i64;
|
||||
let dux = sux + self.b.mv_i[0][1] as i64;
|
||||
let ls = |a: i64, b: i64| ((a * b) >> 2) + (a + b);
|
||||
for i in 0..self.mvs.num_samples {
|
||||
let c = self.mvs.cand_list[i];
|
||||
let sy = c[0] as i64 - suy;
|
||||
let sx = c[1] as i64 - sux;
|
||||
let dy = c[2] as i64 - duy;
|
||||
let dx = c[3] as i64 - dux;
|
||||
if (sx - dx).abs() < LS_MV_MAX as i64 && (sy - dy).abs() < LS_MV_MAX as i64 {
|
||||
a[0][0] += ls(sx, sx) + 8;
|
||||
a[0][1] += ls(sx, sy) + 4;
|
||||
a[1][1] += ls(sy, sy) + 8;
|
||||
bx[0] += ls(sx, dx) + 8;
|
||||
bx[1] += ls(sy, dx) + 4;
|
||||
by[0] += ls(sx, dy) + 4;
|
||||
by[1] += ls(sy, dy) + 8;
|
||||
}
|
||||
}
|
||||
let det = a[0][0] * a[1][1] - a[0][1] * a[0][1];
|
||||
self.b.local_valid = det != 0;
|
||||
if det == 0 {
|
||||
return;
|
||||
}
|
||||
let (mut div_shift, mut div_factor) = resolve_divisor(det);
|
||||
div_shift -= WARPEDMODEL_PREC_BITS as i32;
|
||||
if div_shift < 0 {
|
||||
div_factor <<= -div_shift;
|
||||
div_shift = 0;
|
||||
}
|
||||
let prec = 1i64 << WARPEDMODEL_PREC_BITS;
|
||||
let cl = WARPEDMODEL_NONDIAGAFFINE_CLAMP as i64;
|
||||
let nondiag = |v: i64| round2_signed_wide(v as i128 * div_factor as i128, div_shift as u32).clamp(-cl + 1, cl - 1);
|
||||
let diag = |v: i64| round2_signed_wide(v as i128 * div_factor as i128, div_shift as u32).clamp(prec - cl + 1, prec + cl - 1);
|
||||
let mut p = [0i64; 6];
|
||||
p[2] = diag(a[1][1] * bx[0] - a[0][1] * bx[1]);
|
||||
p[3] = nondiag(-a[0][1] * bx[0] + a[0][0] * bx[1]);
|
||||
p[4] = nondiag(a[1][1] * by[0] - a[0][1] * by[1]);
|
||||
p[5] = diag(-a[0][1] * by[0] + a[0][0] * by[1]);
|
||||
let mvx = self.b.mv_i[0][1] as i64;
|
||||
let mvy = self.b.mv_i[0][0] as i64;
|
||||
let vx = mvx * (1 << (WARPEDMODEL_PREC_BITS - 3)) - (mid_x * (p[2] - prec) + mid_y * p[3]);
|
||||
let vy = mvy * (1 << (WARPEDMODEL_PREC_BITS - 3)) - (mid_x * p[4] + mid_y * (p[5] - prec));
|
||||
let tc = WARPEDMODEL_TRANS_CLAMP as i64;
|
||||
p[0] = vx.clamp(-tc, tc - 1);
|
||||
p[1] = vy.clamp(-tc, tc - 1);
|
||||
for i in 0..6 {
|
||||
self.b.local_warp_params[i] = p[i] as i32;
|
||||
}
|
||||
}
|
||||
|
||||
/// Inter prediction process (7.11.3.1) for a w x h region of `plane` at (x, y).
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub(crate) fn predict_inter(&mut self, plane: usize, x: usize, y: usize, w: usize, h: usize, cand_row: usize, cand_col: usize) {
|
||||
let bd = self.fs.bit_depth;
|
||||
let mi_i = self.t.mi.idx(cand_row, cand_col);
|
||||
let cand_refs = self.t.mi.ref_frame[mi_i];
|
||||
let cand_mvs = self.t.mi.mv[mi_i];
|
||||
let filters = self.t.mi.interp_filter[mi_i];
|
||||
let is_compound = cand_refs[1] > INTRA_FRAME as i8;
|
||||
let rnd = rounding(is_compound, bd);
|
||||
if plane == 0 && self.b.motion_mode as usize == LOCALWARP {
|
||||
self.warp_estimation();
|
||||
if self.b.local_valid {
|
||||
let p = self.b.local_warp_params;
|
||||
self.b.local_valid = setup_shear(&p).0;
|
||||
}
|
||||
}
|
||||
let mut preds = [std::mem::take(&mut self.pred_buf[0]), std::mem::take(&mut self.pred_buf[1])];
|
||||
let mut tmp = std::mem::take(&mut self.mc_tmp);
|
||||
let fh = &self.fs.fh;
|
||||
for ref_list in 0..1 + is_compound as usize {
|
||||
let ref_frame = cand_refs[ref_list];
|
||||
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 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] };
|
||||
for i8 in 0..=((h - 1) >> 3) {
|
||||
for j8 in 0..=((w - 1) >> 3) {
|
||||
self.block_warp(¶ms, plane, ref_idx, x as i32, y as i32, i8, j8, w, h, rnd, &mut preds[ref_list]);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
let (sx, sy, stx, sty) = self.mv_scaling(plane, ref_idx, x as i32, y as i32, mv);
|
||||
self.block_inter_prediction(plane, ref_idx, sx, sy, stx, sty, w, h, filters, rnd, &mut preds[ref_list], &mut tmp);
|
||||
}
|
||||
}
|
||||
let ct = self.b.compound_type;
|
||||
self.fill_blend_mask(plane, w, h, &preds, rnd.2);
|
||||
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;
|
||||
if !is_compound && !is_inter_intra {
|
||||
let pl = &mut self.t.cur.planes[plane];
|
||||
for i in 0..h {
|
||||
let row = &mut pl.row_from_mut(y + i, x)[..w];
|
||||
for j in 0..w {
|
||||
row[j] = preds[0][i * PRED_STRIDE + j].clamp(0, max) as u16;
|
||||
}
|
||||
}
|
||||
} else if ct == COMPOUND_AVERAGE {
|
||||
let pl = &mut self.t.cur.planes[plane];
|
||||
for i in 0..h {
|
||||
let row = &mut pl.row_from_mut(y + i, x)[..w];
|
||||
for j in 0..w {
|
||||
let k = i * PRED_STRIDE + j;
|
||||
row[j] = round2(preds[0][k] + preds[1][k], 1 + rnd.2).clamp(0, max) as u16;
|
||||
}
|
||||
}
|
||||
} else if ct == COMPOUND_DISTANCE {
|
||||
let pl = &mut self.t.cur.planes[plane];
|
||||
for i in 0..h {
|
||||
let row = &mut pl.row_from_mut(y + i, x)[..w];
|
||||
for j in 0..w {
|
||||
let k = i * PRED_STRIDE + j;
|
||||
row[j] = round2(fwd * preds[0][k] + bck * preds[1][k], 4 + rnd.2).clamp(0, max) as u16;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
self.mask_blend(&preds, plane, x, y, w, h, rnd.2, max);
|
||||
}
|
||||
self.pred_buf = preds;
|
||||
self.mc_tmp = tmp;
|
||||
if self.b.motion_mode as usize == OBMC {
|
||||
self.overlapped_motion_compensation(plane, w, h);
|
||||
}
|
||||
}
|
||||
|
||||
/// 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;
|
||||
}
|
||||
let fh = &self.fs.fh;
|
||||
let ri = &self.fs.ref_info[ref_idx as usize];
|
||||
let xs = ((ri.upscaled_width << REF_SCALE_SHIFT) + fh.frame_width / 2) / fh.frame_width;
|
||||
let ys = ((ri.frame_height << REF_SCALE_SHIFT) + fh.frame_height / 2) / fh.frame_height;
|
||||
xs != 1 << REF_SCALE_SHIFT || ys != 1 << REF_SCALE_SHIFT
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
fn mask_blend(&mut self, preds: &[Vec<i32>; 2], plane: usize, dx: usize, dy: usize, w: usize, h: usize, post: u32, max: i32) {
|
||||
let (sx, sy) = if plane > 0 { (self.fs.ssx, self.fs.ssy) } else { (0, 0) };
|
||||
let interintra = self.b.interintra;
|
||||
let wedge_ii = self.b.wedge_interintra;
|
||||
for yy in 0..h {
|
||||
for xx in 0..w {
|
||||
let mk = |r: usize, c: usize| self.mask[r * PRED_STRIDE + c];
|
||||
let m = if (sx == 0 && sy == 0) || (interintra && !wedge_ii) {
|
||||
mk(yy, xx)
|
||||
} else if sx == 1 && sy == 0 {
|
||||
round2(mk(yy, 2 * xx) + mk(yy, 2 * xx + 1), 1)
|
||||
} else {
|
||||
round2(mk(2 * yy, 2 * xx) + mk(2 * yy, 2 * xx + 1) + mk(2 * yy + 1, 2 * xx) + mk(2 * yy + 1, 2 * xx + 1), 2)
|
||||
};
|
||||
let k = yy * PRED_STRIDE + xx;
|
||||
let pl = &mut self.t.cur.planes[plane];
|
||||
if interintra {
|
||||
let p0 = round2(preds[0][k], post).clamp(0, max);
|
||||
let p1 = pl.at(dx + xx, dy + yy) as i32;
|
||||
pl.set(dx + xx, dy + yy, round2(m * p1 + (64 - m) * p0, 6) as u16);
|
||||
} else {
|
||||
let v = round2(m * preds[0][k] + (64 - m) * preds[1][k], 6 + post).clamp(0, max);
|
||||
pl.set(dx + xx, dy + yy, v as u16);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn distance_weights(&self, refs: [i8; 2]) -> (i32, i32) {
|
||||
let fh = &self.fs.fh;
|
||||
let mut dist = [0i32; 2];
|
||||
for (rl, d) in dist.iter_mut().enumerate() {
|
||||
let h = fh.order_hints[refs[rl] as usize];
|
||||
*d = crate::header::relative_dist(&self.fs.seq, h, fh.order_hint).abs().clamp(0, MAX_FRAME_DISTANCE as i32);
|
||||
}
|
||||
let d0 = dist[1];
|
||||
let d1 = dist[0];
|
||||
let order = (d0 <= d1) as usize;
|
||||
if d0 == 0 || d1 == 0 {
|
||||
return (QUANT_DIST_LOOKUP[3][order] as i32, QUANT_DIST_LOOKUP[3][1 - order] as i32);
|
||||
}
|
||||
let mut i = 0;
|
||||
while i < 3 {
|
||||
let c0 = QUANT_DIST_WEIGHT[i][order] as i32;
|
||||
let c1 = QUANT_DIST_WEIGHT[i][1 - order] as i32;
|
||||
if order == 1 {
|
||||
if d0 * c0 > d1 * c1 {
|
||||
break;
|
||||
}
|
||||
} else if d0 * c0 < d1 * c1 {
|
||||
break;
|
||||
}
|
||||
i += 1;
|
||||
}
|
||||
(QUANT_DIST_LOOKUP[i][order] as i32, QUANT_DIST_LOOKUP[i][1 - order] as i32)
|
||||
}
|
||||
|
||||
/// Overlapped motion compensation (7.11.3.9).
|
||||
fn overlapped_motion_compensation(&mut self, plane: usize, w: usize, h: usize) {
|
||||
let (sx, sy) = if plane > 0 { (self.fs.ssx, self.fs.ssy) } else { (0, 0) };
|
||||
let (mi_rows, mi_cols) = (self.fs.fh.mi_rows as usize, self.fs.fh.mi_cols as usize);
|
||||
let bd = self.fs.bit_depth;
|
||||
let max = (1i32 << bd) - 1;
|
||||
let mut obmc = std::mem::take(&mut self.pred_buf[0]);
|
||||
let mut tmp = std::mem::take(&mut self.mc_tmp);
|
||||
let get_mask = |len: usize| -> &'static [u8] {
|
||||
match len {
|
||||
2 => &OBMC_MASK_2,
|
||||
4 => &OBMC_MASK_4,
|
||||
8 => &OBMC_MASK_8,
|
||||
16 => &OBMC_MASK_16,
|
||||
_ => &OBMC_MASK_32,
|
||||
}
|
||||
};
|
||||
let mut jobs: Vec<(usize, usize, usize, usize, usize, usize, usize)> = Vec::new(); // (pass, cand_row, cand_col, x4, y4, predW, predH)
|
||||
if self.b.avail_u && self.fs.plane_residual_size(self.b.mi_size, plane) >= BLOCK_8X8 {
|
||||
let w4 = self.b.bw4;
|
||||
let mut x4 = self.b.mi_col;
|
||||
let y4 = self.b.mi_row;
|
||||
let mut n = 0;
|
||||
let limit = 4.min(MI_WIDTH_LOG2[self.b.mi_size] as usize);
|
||||
while n < limit && x4 < mi_cols.min(self.b.mi_col + w4) {
|
||||
let cr = self.b.mi_row - 1;
|
||||
let cc = x4 | 1;
|
||||
let mi = &self.t.mi;
|
||||
let ci = mi.idx(cr, cc);
|
||||
let step4 = (NUM_4X4_BLOCKS_WIDE[mi.mi_size[ci] as usize] as usize).clamp(2, 16);
|
||||
if mi.ref_frame[ci][0] > INTRA_FRAME as i8 {
|
||||
n += 1;
|
||||
let pw = w.min((step4 * 4) >> sx);
|
||||
let ph = (h >> 1).min(32 >> sy);
|
||||
jobs.push((0, cr, cc, x4, y4, pw, ph));
|
||||
}
|
||||
x4 += step4;
|
||||
}
|
||||
}
|
||||
if self.b.avail_l {
|
||||
let h4 = self.b.bh4;
|
||||
let x4 = self.b.mi_col;
|
||||
let mut y4 = self.b.mi_row;
|
||||
let mut n = 0;
|
||||
let limit = 4.min(MI_HEIGHT_LOG2[self.b.mi_size] as usize);
|
||||
while n < limit && y4 < mi_rows.min(self.b.mi_row + h4) {
|
||||
let cc = self.b.mi_col - 1;
|
||||
let cr = y4 | 1;
|
||||
let mi = &self.t.mi;
|
||||
let ci = mi.idx(cr, cc);
|
||||
let step4 = (NUM_4X4_BLOCKS_HIGH[mi.mi_size[ci] as usize] as usize).clamp(2, 16);
|
||||
if mi.ref_frame[ci][0] > INTRA_FRAME as i8 {
|
||||
n += 1;
|
||||
let pw = (w >> 1).min(32 >> sx);
|
||||
let ph = h.min((step4 * 4) >> sy);
|
||||
jobs.push((1, cr, cc, x4, y4, pw, ph));
|
||||
}
|
||||
y4 += step4;
|
||||
}
|
||||
}
|
||||
// the above pass completes before the left pass reads samples; jobs keep that order
|
||||
for (pass, cr, cc, x4, y4, pw, ph) in jobs {
|
||||
let ci = self.t.mi.idx(cr, cc);
|
||||
let m = self.t.mi.mv[ci][0];
|
||||
let mv = [m.row as i32, m.col as i32];
|
||||
let ref_idx = self.fs.fh.ref_frame_idx[self.t.mi.ref_frame[ci][0] as usize - LAST_FRAME] as i32;
|
||||
let px = (x4 * 4) >> sx;
|
||||
let py = (y4 * 4) >> sy;
|
||||
let (stx, sty, sxs, sys) = self.mv_scaling(plane, ref_idx, px as i32, py as i32, mv);
|
||||
let filters = self.t.mi.interp_filter[ci];
|
||||
let rnd = rounding(false, bd);
|
||||
self.block_inter_prediction(plane, ref_idx, stx, sty, sxs, sys, pw, ph, filters, rnd, &mut obmc, &mut tmp);
|
||||
let mask = get_mask(if pass == 0 { ph } else { pw });
|
||||
let pl = &mut self.t.cur.planes[plane];
|
||||
for i in 0..ph {
|
||||
for j in 0..pw {
|
||||
let mm = if pass == 0 { mask[i] } else { mask[j] } as i32;
|
||||
let o = obmc[i * PRED_STRIDE + j].clamp(0, max);
|
||||
let c = pl.at(px + j, py + i) as i32;
|
||||
pl.set(px + j, py + i, round2(mm * c + (64 - mm) * o, 6) as u16);
|
||||
}
|
||||
}
|
||||
}
|
||||
self.pred_buf[0] = obmc;
|
||||
self.mc_tmp = tmp;
|
||||
}
|
||||
}
|
||||
|
||||
const IDENTITY_TAPS: [i16; 8] = [0, 0, 0, 128, 0, 0, 0, 0];
|
||||
|
||||
/// Horizontal pass of one row for a block `W` wide: taps outer, columns inner, fixed width so
|
||||
/// the row stays in registers.
|
||||
#[inline(always)]
|
||||
fn hpass<const W: usize>(src: &[u16], hf: &[i16; 8], out: &mut [i32], half: i32, sh: u32) {
|
||||
let mut acc = [half; W];
|
||||
for t in 0..8 {
|
||||
let f = hf[t] as i32;
|
||||
let Some(p) = src.get(t..).and_then(<[_]>::first_chunk::<W>) else {
|
||||
return;
|
||||
};
|
||||
for j in 0..W {
|
||||
acc[j] += f * p[j] as i32;
|
||||
}
|
||||
}
|
||||
let Some(o) = out.first_chunk_mut::<W>() else {
|
||||
return;
|
||||
};
|
||||
for j in 0..W {
|
||||
o[j] = acc[j] >> sh;
|
||||
}
|
||||
}
|
||||
|
||||
/// Vertical pass of one output row from 8 intermediate rows (`rows`, stride `W`).
|
||||
#[inline(always)]
|
||||
fn vpass<const W: usize>(rows: &[i32], vf: &[i16; 8], out: &mut [i32], half: i32, sh: u32) {
|
||||
let mut acc = [half; W];
|
||||
for t in 0..8 {
|
||||
let f = vf[t] as i32;
|
||||
let Some(p) = rows.get(t * W..).and_then(<[_]>::first_chunk::<W>) else {
|
||||
return;
|
||||
};
|
||||
for j in 0..W {
|
||||
acc[j] += f * p[j];
|
||||
}
|
||||
}
|
||||
let Some(o) = out.first_chunk_mut::<W>() else {
|
||||
return;
|
||||
};
|
||||
for j in 0..W {
|
||||
o[j] = acc[j] >> sh;
|
||||
}
|
||||
}
|
||||
|
||||
/// Block inter prediction without reference scaling: every column / row uses the same filter
|
||||
/// phase, so the filters run over whole rows (bit-identical to the per-sample process).
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
fn mc_unscaled(
|
||||
refp: &Plane,
|
||||
last_x: i32,
|
||||
last_y: i32,
|
||||
x: i32,
|
||||
y: i32,
|
||||
w: usize,
|
||||
h: usize,
|
||||
hf: &[i16; 8],
|
||||
vf: &[i16; 8],
|
||||
rnd: (u32, u32, u32),
|
||||
pred: &mut [i32],
|
||||
tmp: &mut [i32],
|
||||
) {
|
||||
let x0 = (x >> 10) - 3;
|
||||
let y0 = (y >> 10) - 3;
|
||||
let h_copy = *hf == IDENTITY_TAPS;
|
||||
let v_copy = *vf == IDENTITY_TAPS;
|
||||
// With the identity vertical phase only intermediate rows 3 .. 3 + h have a non-zero tap.
|
||||
let (r_lo, r_hi) = if v_copy { (3, 3 + h) } else { (0, h + 7) };
|
||||
let inside = x0 >= 0 && x0 + w as i32 + 7 <= last_x + 1;
|
||||
let mut edge = [0u16; 128 + 8];
|
||||
let mut acc = [0i32; 128];
|
||||
let (r0, r1) = (rnd.0, rnd.1);
|
||||
let (half0, half1) = (1i32 << (r0 - 1), 1i32 << (r1 - 1));
|
||||
for r in r_lo..r_hi {
|
||||
let ry = (y0 + r as i32).clamp(0, last_y) as usize;
|
||||
let row = refp.row(ry);
|
||||
let src: &[u16] = if inside {
|
||||
&row[x0 as usize..x0 as usize + w + 7]
|
||||
} else {
|
||||
for (k, e) in edge[..w + 7].iter_mut().enumerate() {
|
||||
*e = row[(x0 + k as i32).clamp(0, last_x) as usize];
|
||||
}
|
||||
&edge[..w + 7]
|
||||
};
|
||||
let out = &mut tmp[r * w..r * w + w];
|
||||
if h_copy {
|
||||
// Round2(128 * v, InterRound0) is exact.
|
||||
for (o, &v) in out.iter_mut().zip(&src[3..3 + w]) {
|
||||
*o = (v as i32) << (7 - r0);
|
||||
}
|
||||
} else {
|
||||
match w {
|
||||
2 => hpass::<2>(src, hf, out, half0, r0),
|
||||
4 => hpass::<4>(src, hf, out, half0, r0),
|
||||
8 => hpass::<8>(src, hf, out, half0, r0),
|
||||
16 => hpass::<16>(src, hf, out, half0, r0),
|
||||
32 => hpass::<32>(src, hf, out, half0, r0),
|
||||
64 => hpass::<64>(src, hf, out, half0, r0),
|
||||
_ => {
|
||||
let acc = &mut acc[..w];
|
||||
acc.fill(0);
|
||||
for t in 0..8 {
|
||||
let f = hf[t] as i32;
|
||||
for (a, &v) in acc.iter_mut().zip(&src[t..t + w]) {
|
||||
*a += f * v as i32;
|
||||
}
|
||||
}
|
||||
for (o, &a) in out.iter_mut().zip(acc.iter()) {
|
||||
*o = (a + half0) >> r0;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
for r in 0..h {
|
||||
let out = &mut pred[r * PRED_STRIDE..r * PRED_STRIDE + w];
|
||||
if v_copy {
|
||||
for (o, &v) in out.iter_mut().zip(&tmp[(r + 3) * w..(r + 4) * w]) {
|
||||
*o = (128 * v + half1) >> r1;
|
||||
}
|
||||
} else {
|
||||
let rows = &tmp[r * w..(r + 8) * w];
|
||||
match w {
|
||||
2 => vpass::<2>(rows, vf, out, half1, r1),
|
||||
4 => vpass::<4>(rows, vf, out, half1, r1),
|
||||
8 => vpass::<8>(rows, vf, out, half1, r1),
|
||||
16 => vpass::<16>(rows, vf, out, half1, r1),
|
||||
32 => vpass::<32>(rows, vf, out, half1, r1),
|
||||
64 => vpass::<64>(rows, vf, out, half1, r1),
|
||||
_ => {
|
||||
let acc = &mut acc[..w];
|
||||
acc.fill(0);
|
||||
for t in 0..8 {
|
||||
let f = vf[t] as i32;
|
||||
for (a, &v) in acc.iter_mut().zip(&tmp[(r + t) * w..(r + t + 1) * w]) {
|
||||
*a += f * v;
|
||||
}
|
||||
}
|
||||
for (o, &a) in out.iter_mut().zip(acc.iter()) {
|
||||
*o = (a + half1) >> r1;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,777 @@
|
||||
//! Inter frame mode info syntax (5.11.7 / 5.11.18 – 5.11.33) with its CDF selection (8.3.2).
|
||||
|
||||
use crate::Result;
|
||||
use crate::header::{EIGHTTAP, SWITCHABLE};
|
||||
use crate::mvpred::MvP;
|
||||
use crate::spec_tables::{
|
||||
ALTREF_FRAME, ALTREF2_FRAME, BLOCK_8X8, BLOCK_32X32, BLOCK_64X64, BLOCK_128X128, BWDREF_FRAME, CLASS0_SIZE, COMP_NEWMV_CTXS, COMPOUND_AVERAGE,
|
||||
COMPOUND_DIFFWTD, COMPOUND_DISTANCE, COMPOUND_INTRA, COMPOUND_MODE_CTX_MAP, COMPOUND_REFERENCE, COMPOUND_WEDGE, GLOBAL_GLOBALMV, GLOBALMV,
|
||||
GOLDEN_FRAME, INTRA_FRAME, INTRABC_DELAY_PIXELS, LAST_FRAME, LAST2_FRAME, LAST3_FRAME, LOCALWARP, MV_CLASS_0, MV_INTRABC_CONTEXT,
|
||||
MV_JOINT_HNZVNZ, MV_JOINT_HNZVZ, MV_JOINT_HZVNZ, NEAR_NEARMV, NEAR_NEWMV, NEAREST_NEARESTMV, NEAREST_NEWMV, NEARESTMV, NEARMV, NEW_NEARESTMV,
|
||||
NEW_NEARMV, NEW_NEWMV, NEWMV, NUM_4X4_BLOCKS_HIGH, NUM_4X4_BLOCKS_WIDE, OBMC, REF_SCALE_SHIFT, SEG_LVL_GLOBALMV, SEG_LVL_REF_FRAME, SEG_LVL_SKIP,
|
||||
SIMPLE, SINGLE_REFERENCE, SIZE_GROUP, TRANSLATION, UNIDIR_COMP_REFERENCE, UV_CFL_PRED, WEDGE_BITS,
|
||||
};
|
||||
use crate::tile::TileDecoder;
|
||||
|
||||
fn block_width(bs: usize) -> usize {
|
||||
4 * NUM_4X4_BLOCKS_WIDE[bs] as usize
|
||||
}
|
||||
fn block_height(bs: usize) -> usize {
|
||||
4 * NUM_4X4_BLOCKS_HIGH[bs] as usize
|
||||
}
|
||||
|
||||
/// Neighbour reference frame summary (LeftRefFrame / AboveRefFrame and the derived flags).
|
||||
#[derive(Clone, Copy, Default)]
|
||||
pub(crate) struct Neighbours {
|
||||
pub left: [i8; 2],
|
||||
pub above: [i8; 2],
|
||||
pub left_intra: bool,
|
||||
pub above_intra: bool,
|
||||
pub left_single: bool,
|
||||
pub above_single: bool,
|
||||
}
|
||||
|
||||
impl TileDecoder<'_, '_> {
|
||||
pub(crate) fn inter_frame_mode_info_impl(&mut self) -> Result<()> {
|
||||
self.b.use_intrabc = false;
|
||||
let (r, c) = (self.b.mi_row, self.b.mi_col);
|
||||
let mi = &self.t.mi;
|
||||
let mut n = Neighbours {
|
||||
left: if self.b.avail_l { mi.ref_frame[mi.idx(r, c - 1)] } else { [INTRA_FRAME as i8, -1] },
|
||||
above: if self.b.avail_u { mi.ref_frame[mi.idx(r - 1, c)] } else { [INTRA_FRAME as i8, -1] },
|
||||
..Default::default()
|
||||
};
|
||||
if !self.b.avail_l {
|
||||
n.left[1] = -1;
|
||||
}
|
||||
if !self.b.avail_u {
|
||||
n.above[1] = -1;
|
||||
}
|
||||
n.left_intra = n.left[0] <= INTRA_FRAME as i8;
|
||||
n.above_intra = n.above[0] <= INTRA_FRAME as i8;
|
||||
n.left_single = n.left[1] <= INTRA_FRAME as i8;
|
||||
n.above_single = n.above[1] <= INTRA_FRAME as i8;
|
||||
self.nb = n;
|
||||
self.b.skip = false;
|
||||
self.inter_segment_id(true);
|
||||
self.read_skip_mode();
|
||||
if self.b.skip_mode {
|
||||
self.b.skip = true;
|
||||
} else {
|
||||
self.read_skip();
|
||||
}
|
||||
if !self.fs.fh.seg.seg_id_pre_skip {
|
||||
self.inter_segment_id(false);
|
||||
}
|
||||
self.b.lossless = self.fs.fh.lossless_array[self.b.segment_id];
|
||||
self.read_cdef();
|
||||
self.read_delta_qindex();
|
||||
self.read_delta_lf();
|
||||
self.read_deltas = false;
|
||||
self.read_is_inter();
|
||||
if self.b.is_inter { self.inter_block_mode_info() } else { self.intra_block_mode_info() }
|
||||
}
|
||||
|
||||
fn get_segment_id(&self) -> usize {
|
||||
let (mi_rows, mi_cols) = (self.fs.fh.mi_rows as usize, self.fs.fh.mi_cols as usize);
|
||||
let x_mis = (mi_cols - self.b.mi_col).min(self.b.bw4);
|
||||
let y_mis = (mi_rows - self.b.mi_row).min(self.b.bh4);
|
||||
let mut seg = 7u8;
|
||||
for y in 0..y_mis {
|
||||
for x in 0..x_mis {
|
||||
let i = (self.b.mi_row + y) * self.t.mi.cols + self.b.mi_col + x;
|
||||
seg = seg.min(self.fs.prev_segment_ids[i]);
|
||||
}
|
||||
}
|
||||
seg as usize
|
||||
}
|
||||
|
||||
fn set_seg_pred_context(&mut self, v: u8) {
|
||||
for i in 0..self.b.bw4 {
|
||||
if self.b.mi_col + i < self.t.above_seg_pred.len() {
|
||||
self.t.above_seg_pred[self.b.mi_col + i] = v;
|
||||
}
|
||||
}
|
||||
for i in 0..self.b.bh4 {
|
||||
if self.b.mi_row + i < self.t.left_seg_pred.len() {
|
||||
self.t.left_seg_pred[self.b.mi_row + i] = v;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn inter_segment_id(&mut self, pre_skip: bool) {
|
||||
let seg = &self.fs.fh.seg;
|
||||
if !seg.enabled {
|
||||
self.b.segment_id = 0;
|
||||
return;
|
||||
}
|
||||
let predicted = self.get_segment_id();
|
||||
if seg.update_map {
|
||||
if pre_skip && !seg.seg_id_pre_skip {
|
||||
self.b.segment_id = 0;
|
||||
return;
|
||||
}
|
||||
if !pre_skip && self.b.skip {
|
||||
self.set_seg_pred_context(0);
|
||||
self.read_segment_id();
|
||||
return;
|
||||
}
|
||||
if seg.temporal_update {
|
||||
let ctx = self.t.left_seg_pred[self.b.mi_row] as usize + self.t.above_seg_pred[self.b.mi_col] as usize;
|
||||
let pred_flag = self.sd.read_symbol(&mut self.cdf.segment_id_predicted[ctx]);
|
||||
if pred_flag == 1 {
|
||||
self.b.segment_id = predicted;
|
||||
} else {
|
||||
self.read_segment_id();
|
||||
}
|
||||
self.set_seg_pred_context(pred_flag as u8);
|
||||
} else {
|
||||
self.read_segment_id();
|
||||
}
|
||||
} else {
|
||||
self.b.segment_id = predicted;
|
||||
}
|
||||
}
|
||||
|
||||
fn read_skip_mode(&mut self) {
|
||||
let ms = self.b.mi_size;
|
||||
if self.seg_active(SEG_LVL_SKIP)
|
||||
|| self.seg_active(SEG_LVL_REF_FRAME)
|
||||
|| self.seg_active(SEG_LVL_GLOBALMV)
|
||||
|| !self.fs.fh.skip_mode_present
|
||||
|| block_width(ms) < 8
|
||||
|| block_height(ms) < 8
|
||||
{
|
||||
self.b.skip_mode = false;
|
||||
} else {
|
||||
let (r, c) = (self.b.mi_row, self.b.mi_col);
|
||||
let mi = &self.t.mi;
|
||||
let mut ctx = 0;
|
||||
if self.b.avail_u {
|
||||
ctx += mi.skip_mode[mi.idx(r - 1, c)] as usize;
|
||||
}
|
||||
if self.b.avail_l {
|
||||
ctx += mi.skip_mode[mi.idx(r, c - 1)] as usize;
|
||||
}
|
||||
self.b.skip_mode = self.sd.read_symbol(&mut self.cdf.skip_mode[ctx]) == 1;
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn seg_active(&self, feature: usize) -> bool {
|
||||
self.fs.fh.seg.enabled && self.fs.fh.seg.features.enabled[self.b.segment_id][feature]
|
||||
}
|
||||
|
||||
fn read_is_inter(&mut self) {
|
||||
if self.b.skip_mode {
|
||||
self.b.is_inter = true;
|
||||
} else if self.seg_active(SEG_LVL_REF_FRAME) {
|
||||
self.b.is_inter = self.fs.fh.seg.features.data[self.b.segment_id][SEG_LVL_REF_FRAME] != INTRA_FRAME as i32;
|
||||
} else if self.seg_active(SEG_LVL_GLOBALMV) {
|
||||
self.b.is_inter = true;
|
||||
} else {
|
||||
let n = self.nb;
|
||||
let ctx = if self.b.avail_u && self.b.avail_l {
|
||||
if n.left_intra && n.above_intra { 3 } else { (n.left_intra || n.above_intra) as usize }
|
||||
} else if self.b.avail_u || self.b.avail_l {
|
||||
2 * (if self.b.avail_u { n.above_intra } else { n.left_intra }) as usize
|
||||
} else {
|
||||
0
|
||||
};
|
||||
self.b.is_inter = self.sd.read_symbol(&mut self.cdf.is_inter[ctx]) == 1;
|
||||
}
|
||||
}
|
||||
|
||||
fn intra_block_mode_info(&mut self) -> Result<()> {
|
||||
self.b.ref_frame = [INTRA_FRAME as i8, -1];
|
||||
let ctx = SIZE_GROUP[self.b.mi_size] as usize;
|
||||
self.b.y_mode = self.sd.read_symbol(&mut self.cdf.y_mode[ctx]);
|
||||
self.intra_angle_info_y();
|
||||
if self.b.has_chroma {
|
||||
self.read_uv_mode();
|
||||
if self.b.uv_mode == UV_CFL_PRED {
|
||||
self.read_cfl_alphas();
|
||||
}
|
||||
self.intra_angle_info_uv();
|
||||
}
|
||||
self.b.palette_size_y = 0;
|
||||
self.b.palette_size_uv = 0;
|
||||
let ms = self.b.mi_size;
|
||||
if ms >= BLOCK_8X8 && block_width(ms) <= 64 && block_height(ms) <= 64 && self.fs.fh.allow_screen_content_tools {
|
||||
self.palette_mode_info();
|
||||
}
|
||||
self.filter_intra_mode_info();
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn count_refs(&self, frame_type: i8) -> usize {
|
||||
let n = self.nb;
|
||||
let mut c = 0;
|
||||
if self.b.avail_u {
|
||||
c += (n.above[0] == frame_type) as usize + (n.above[1] == frame_type) as usize;
|
||||
}
|
||||
if self.b.avail_l {
|
||||
c += (n.left[0] == frame_type) as usize + (n.left[1] == frame_type) as usize;
|
||||
}
|
||||
c
|
||||
}
|
||||
|
||||
fn ref_count_ctx(c0: usize, c1: usize) -> usize {
|
||||
match c0.cmp(&c1) {
|
||||
std::cmp::Ordering::Less => 0,
|
||||
std::cmp::Ordering::Equal => 1,
|
||||
std::cmp::Ordering::Greater => 2,
|
||||
}
|
||||
}
|
||||
|
||||
fn ctx_comp_ref(&self) -> usize {
|
||||
Self::ref_count_ctx(
|
||||
self.count_refs(LAST_FRAME as i8) + self.count_refs(LAST2_FRAME as i8),
|
||||
self.count_refs(LAST3_FRAME as i8) + self.count_refs(GOLDEN_FRAME as i8),
|
||||
)
|
||||
}
|
||||
fn ctx_comp_ref_p1(&self) -> usize {
|
||||
Self::ref_count_ctx(self.count_refs(LAST_FRAME as i8), self.count_refs(LAST2_FRAME as i8))
|
||||
}
|
||||
fn ctx_comp_ref_p2(&self) -> usize {
|
||||
Self::ref_count_ctx(self.count_refs(LAST3_FRAME as i8), self.count_refs(GOLDEN_FRAME as i8))
|
||||
}
|
||||
fn ctx_comp_bwdref(&self) -> usize {
|
||||
Self::ref_count_ctx(self.count_refs(BWDREF_FRAME as i8) + self.count_refs(ALTREF2_FRAME as i8), self.count_refs(ALTREF_FRAME as i8))
|
||||
}
|
||||
fn ctx_comp_bwdref_p1(&self) -> usize {
|
||||
Self::ref_count_ctx(self.count_refs(BWDREF_FRAME as i8), self.count_refs(ALTREF2_FRAME as i8))
|
||||
}
|
||||
fn ctx_single_ref_p1(&self) -> usize {
|
||||
let fwd = self.count_refs(LAST_FRAME as i8)
|
||||
+ self.count_refs(LAST2_FRAME as i8)
|
||||
+ self.count_refs(LAST3_FRAME as i8)
|
||||
+ self.count_refs(GOLDEN_FRAME as i8);
|
||||
let bwd = self.count_refs(BWDREF_FRAME as i8) + self.count_refs(ALTREF2_FRAME as i8) + self.count_refs(ALTREF_FRAME as i8);
|
||||
Self::ref_count_ctx(fwd, bwd)
|
||||
}
|
||||
|
||||
fn read_ref_frames(&mut self) {
|
||||
if self.b.skip_mode {
|
||||
let f = self.fs.fh.skip_mode_frame;
|
||||
self.b.ref_frame = [f[0] as i8, f[1] as i8];
|
||||
} else if self.seg_active(SEG_LVL_REF_FRAME) {
|
||||
self.b.ref_frame = [self.fs.fh.seg.features.data[self.b.segment_id][SEG_LVL_REF_FRAME] as i8, -1];
|
||||
} else if self.seg_active(SEG_LVL_SKIP) || self.seg_active(SEG_LVL_GLOBALMV) {
|
||||
self.b.ref_frame = [LAST_FRAME as i8, -1];
|
||||
} else {
|
||||
let comp_mode = if self.fs.fh.reference_select && self.b.bw4.min(self.b.bh4) >= 2 {
|
||||
let ctx = self.comp_mode_ctx();
|
||||
self.sd.read_symbol(&mut self.cdf.comp_mode[ctx])
|
||||
} else {
|
||||
SINGLE_REFERENCE
|
||||
};
|
||||
if comp_mode == COMPOUND_REFERENCE {
|
||||
let ctx = self.comp_ref_type_ctx();
|
||||
let comp_ref_type = self.sd.read_symbol(&mut self.cdf.comp_ref_type[ctx]);
|
||||
if comp_ref_type == UNIDIR_COMP_REFERENCE {
|
||||
let ctx = self.ctx_single_ref_p1();
|
||||
if self.sd.read_symbol(&mut self.cdf.uni_comp_ref[ctx][0]) == 1 {
|
||||
self.b.ref_frame = [BWDREF_FRAME as i8, ALTREF_FRAME as i8];
|
||||
} else {
|
||||
let ctx1 = Self::ref_count_ctx(
|
||||
self.count_refs(LAST2_FRAME as i8),
|
||||
self.count_refs(LAST3_FRAME as i8) + self.count_refs(GOLDEN_FRAME as i8),
|
||||
);
|
||||
if self.sd.read_symbol(&mut self.cdf.uni_comp_ref[ctx1][1]) == 1 {
|
||||
let ctx2 = self.ctx_comp_ref_p2();
|
||||
if self.sd.read_symbol(&mut self.cdf.uni_comp_ref[ctx2][2]) == 1 {
|
||||
self.b.ref_frame = [LAST_FRAME as i8, GOLDEN_FRAME as i8];
|
||||
} else {
|
||||
self.b.ref_frame = [LAST_FRAME as i8, LAST3_FRAME as i8];
|
||||
}
|
||||
} else {
|
||||
self.b.ref_frame = [LAST_FRAME as i8, LAST2_FRAME as i8];
|
||||
}
|
||||
}
|
||||
} else {
|
||||
let ctx = self.ctx_comp_ref();
|
||||
if self.sd.read_symbol(&mut self.cdf.comp_ref[ctx][0]) == 0 {
|
||||
let ctx = self.ctx_comp_ref_p1();
|
||||
let p1 = self.sd.read_symbol(&mut self.cdf.comp_ref[ctx][1]);
|
||||
self.b.ref_frame[0] = if p1 == 1 { LAST2_FRAME } else { LAST_FRAME } as i8;
|
||||
} else {
|
||||
let ctx = self.ctx_comp_ref_p2();
|
||||
let p2 = self.sd.read_symbol(&mut self.cdf.comp_ref[ctx][2]);
|
||||
self.b.ref_frame[0] = if p2 == 1 { GOLDEN_FRAME } else { LAST3_FRAME } as i8;
|
||||
}
|
||||
let ctx = self.ctx_comp_bwdref();
|
||||
if self.sd.read_symbol(&mut self.cdf.comp_bwd_ref[ctx][0]) == 0 {
|
||||
let ctx = self.ctx_comp_bwdref_p1();
|
||||
let p1 = self.sd.read_symbol(&mut self.cdf.comp_bwd_ref[ctx][1]);
|
||||
self.b.ref_frame[1] = if p1 == 1 { ALTREF2_FRAME } else { BWDREF_FRAME } as i8;
|
||||
} else {
|
||||
self.b.ref_frame[1] = ALTREF_FRAME as i8;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
let ctx = self.ctx_single_ref_p1();
|
||||
if self.sd.read_symbol(&mut self.cdf.single_ref[ctx][0]) == 1 {
|
||||
let ctx = self.ctx_comp_bwdref();
|
||||
if self.sd.read_symbol(&mut self.cdf.single_ref[ctx][1]) == 0 {
|
||||
let ctx = self.ctx_comp_bwdref_p1();
|
||||
let p6 = self.sd.read_symbol(&mut self.cdf.single_ref[ctx][5]);
|
||||
self.b.ref_frame[0] = if p6 == 1 { ALTREF2_FRAME } else { BWDREF_FRAME } as i8;
|
||||
} else {
|
||||
self.b.ref_frame[0] = ALTREF_FRAME as i8;
|
||||
}
|
||||
} else {
|
||||
let ctx = self.ctx_comp_ref();
|
||||
if self.sd.read_symbol(&mut self.cdf.single_ref[ctx][2]) == 1 {
|
||||
let ctx = self.ctx_comp_ref_p2();
|
||||
let p5 = self.sd.read_symbol(&mut self.cdf.single_ref[ctx][4]);
|
||||
self.b.ref_frame[0] = if p5 == 1 { GOLDEN_FRAME } else { LAST3_FRAME } as i8;
|
||||
} else {
|
||||
let ctx = self.ctx_comp_ref_p1();
|
||||
let p4 = self.sd.read_symbol(&mut self.cdf.single_ref[ctx][3]);
|
||||
self.b.ref_frame[0] = if p4 == 1 { LAST2_FRAME } else { LAST_FRAME } as i8;
|
||||
}
|
||||
}
|
||||
self.b.ref_frame[1] = -1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn check_backward(r: i8) -> bool {
|
||||
r >= BWDREF_FRAME as i8 && r <= ALTREF_FRAME as i8
|
||||
}
|
||||
|
||||
fn comp_mode_ctx(&self) -> usize {
|
||||
let n = self.nb;
|
||||
let (au, al) = (self.b.avail_u, self.b.avail_l);
|
||||
if au && al {
|
||||
if n.above_single && n.left_single {
|
||||
(Self::check_backward(n.above[0]) ^ Self::check_backward(n.left[0])) as usize
|
||||
} else if n.above_single {
|
||||
2 + (Self::check_backward(n.above[0]) || n.above_intra) as usize
|
||||
} else if n.left_single {
|
||||
2 + (Self::check_backward(n.left[0]) || n.left_intra) as usize
|
||||
} else {
|
||||
4
|
||||
}
|
||||
} else if au {
|
||||
if n.above_single { Self::check_backward(n.above[0]) as usize } else { 3 }
|
||||
} else if al {
|
||||
if n.left_single { Self::check_backward(n.left[0]) as usize } else { 3 }
|
||||
} else {
|
||||
1
|
||||
}
|
||||
}
|
||||
|
||||
fn comp_ref_type_ctx(&self) -> usize {
|
||||
let n = self.nb;
|
||||
let (au, al) = (self.b.avail_u, self.b.avail_l);
|
||||
let samedir = |a: i8, b: i8| (a >= BWDREF_FRAME as i8) == (b >= BWDREF_FRAME as i8);
|
||||
let (above0, above1, left0, left1) = (n.above[0], n.above[1], n.left[0], n.left[1]);
|
||||
let above_comp_inter = au && !n.above_intra && !n.above_single;
|
||||
let left_comp_inter = al && !n.left_intra && !n.left_single;
|
||||
let above_uni = above_comp_inter && samedir(above0, above1);
|
||||
let left_uni = left_comp_inter && samedir(left0, left1);
|
||||
if au && !n.above_intra && al && !n.left_intra {
|
||||
let sd = samedir(above0, left0) as usize;
|
||||
if !above_comp_inter && !left_comp_inter {
|
||||
1 + 2 * sd
|
||||
} else if !above_comp_inter {
|
||||
if !left_uni { 1 } else { 3 + sd }
|
||||
} else if !left_comp_inter {
|
||||
if !above_uni { 1 } else { 3 + sd }
|
||||
} else if !above_uni && !left_uni {
|
||||
0
|
||||
} else if !above_uni || !left_uni {
|
||||
2
|
||||
} else {
|
||||
3 + ((above0 == BWDREF_FRAME as i8) == (left0 == BWDREF_FRAME as i8)) as usize
|
||||
}
|
||||
} else if au && al {
|
||||
if above_comp_inter {
|
||||
1 + 2 * above_uni as usize
|
||||
} else if left_comp_inter {
|
||||
1 + 2 * left_uni as usize
|
||||
} else {
|
||||
2
|
||||
}
|
||||
} else if above_comp_inter {
|
||||
4 * above_uni as usize
|
||||
} else if left_comp_inter {
|
||||
4 * left_uni as usize
|
||||
} else {
|
||||
2
|
||||
}
|
||||
}
|
||||
|
||||
fn inter_block_mode_info(&mut self) -> Result<()> {
|
||||
self.b.palette_size_y = 0;
|
||||
self.b.palette_size_uv = 0;
|
||||
self.read_ref_frames();
|
||||
let is_compound = self.b.ref_frame[1] > INTRA_FRAME as i8;
|
||||
self.find_mv_stack(is_compound);
|
||||
if self.b.skip_mode {
|
||||
self.b.y_mode = NEAREST_NEARESTMV;
|
||||
} else if self.seg_active(SEG_LVL_SKIP) || self.seg_active(SEG_LVL_GLOBALMV) {
|
||||
self.b.y_mode = GLOBALMV;
|
||||
} else if is_compound {
|
||||
let ctx = COMPOUND_MODE_CTX_MAP[self.mvs.ref_mv_context >> 1][self.mvs.new_mv_context.min(COMP_NEWMV_CTXS - 1)] as usize;
|
||||
self.b.y_mode = NEAREST_NEARESTMV + self.sd.read_symbol(&mut self.cdf.compound_mode[ctx]);
|
||||
} else {
|
||||
let new_mv = self.sd.read_symbol(&mut self.cdf.new_mv[self.mvs.new_mv_context]);
|
||||
if new_mv == 0 {
|
||||
self.b.y_mode = NEWMV;
|
||||
} else {
|
||||
let zero_mv = self.sd.read_symbol(&mut self.cdf.zero_mv[self.mvs.zero_mv_context]);
|
||||
if zero_mv == 0 {
|
||||
self.b.y_mode = GLOBALMV;
|
||||
} else {
|
||||
let ref_mv = self.sd.read_symbol(&mut self.cdf.ref_mv[self.mvs.ref_mv_context]);
|
||||
self.b.y_mode = if ref_mv == 0 { NEARESTMV } else { NEARMV };
|
||||
}
|
||||
}
|
||||
}
|
||||
self.b.ref_mv_idx = 0;
|
||||
let ym = self.b.y_mode;
|
||||
if ym == NEWMV || ym == NEW_NEWMV {
|
||||
for idx in 0..2 {
|
||||
if self.mvs.num_mv_found > idx + 1 {
|
||||
let ctx = self.mvs.drl_ctx_stack[idx];
|
||||
let drl = self.sd.read_symbol(&mut self.cdf.drl_mode[ctx]);
|
||||
if drl == 0 {
|
||||
self.b.ref_mv_idx = idx;
|
||||
break;
|
||||
}
|
||||
self.b.ref_mv_idx = idx + 1;
|
||||
}
|
||||
}
|
||||
} else if matches!(ym, NEARMV | NEAR_NEARMV | NEAR_NEWMV | NEW_NEARMV) {
|
||||
self.b.ref_mv_idx = 1;
|
||||
for idx in 1..3 {
|
||||
if self.mvs.num_mv_found > idx + 1 {
|
||||
let ctx = self.mvs.drl_ctx_stack[idx];
|
||||
let drl = self.sd.read_symbol(&mut self.cdf.drl_mode[ctx]);
|
||||
if drl == 0 {
|
||||
self.b.ref_mv_idx = idx;
|
||||
break;
|
||||
}
|
||||
self.b.ref_mv_idx = idx + 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
self.assign_mv(is_compound)?;
|
||||
self.read_interintra_mode(is_compound);
|
||||
self.read_motion_mode(is_compound);
|
||||
self.read_compound_type(is_compound);
|
||||
if self.fs.fh.interpolation_filter == SWITCHABLE {
|
||||
let dual = self.fs.seq.enable_dual_filter;
|
||||
for dir in 0..if dual { 2 } else { 1 } {
|
||||
if self.needs_interp_filter() {
|
||||
let ctx = self.interp_filter_ctx(dir);
|
||||
self.b.interp_filter[dir] = self.sd.read_symbol(&mut self.cdf.interp_filter[ctx]) as u8;
|
||||
} else {
|
||||
self.b.interp_filter[dir] = EIGHTTAP;
|
||||
}
|
||||
}
|
||||
if !dual {
|
||||
self.b.interp_filter[1] = self.b.interp_filter[0];
|
||||
}
|
||||
} else {
|
||||
self.b.interp_filter = [self.fs.fh.interpolation_filter; 2];
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn needs_interp_filter(&self) -> bool {
|
||||
let ms = self.b.mi_size;
|
||||
let large = block_width(ms).min(block_height(ms)) >= 8;
|
||||
let gm = &self.fs.fh.gm_type;
|
||||
if self.b.skip_mode || self.b.motion_mode as usize == LOCALWARP {
|
||||
false
|
||||
} else if large && self.b.y_mode == GLOBALMV {
|
||||
gm[self.b.ref_frame[0] as usize] as usize == TRANSLATION
|
||||
} else if large && self.b.y_mode == GLOBAL_GLOBALMV {
|
||||
gm[self.b.ref_frame[0] as usize] as usize == TRANSLATION || gm[self.b.ref_frame[1] as usize] as usize == TRANSLATION
|
||||
} else {
|
||||
true
|
||||
}
|
||||
}
|
||||
|
||||
fn interp_filter_ctx(&self, dir: usize) -> usize {
|
||||
let mut ctx = ((dir & 1) * 2 + (self.b.ref_frame[1] > INTRA_FRAME as i8) as usize) * 4;
|
||||
let mut left_type = 3u8;
|
||||
let mut above_type = 3u8;
|
||||
let (r, c) = (self.b.mi_row, self.b.mi_col);
|
||||
let mi = &self.t.mi;
|
||||
let rf0 = self.b.ref_frame[0];
|
||||
if self.b.avail_l {
|
||||
let i = mi.idx(r, c - 1);
|
||||
if mi.ref_frame[i][0] == rf0 || mi.ref_frame[i][1] == rf0 {
|
||||
left_type = mi.interp_filter[i][dir];
|
||||
}
|
||||
}
|
||||
if self.b.avail_u {
|
||||
let i = mi.idx(r - 1, c);
|
||||
if mi.ref_frame[i][0] == rf0 || mi.ref_frame[i][1] == rf0 {
|
||||
above_type = mi.interp_filter[i][dir];
|
||||
}
|
||||
}
|
||||
ctx += if left_type == above_type {
|
||||
left_type as usize
|
||||
} else if left_type == 3 {
|
||||
above_type as usize
|
||||
} else if above_type == 3 {
|
||||
left_type as usize
|
||||
} else {
|
||||
3
|
||||
};
|
||||
ctx
|
||||
}
|
||||
|
||||
fn get_mode(&self, ref_list: usize) -> usize {
|
||||
let ym = self.b.y_mode;
|
||||
if ref_list == 0 {
|
||||
if ym < NEAREST_NEARESTMV {
|
||||
ym
|
||||
} else if matches!(ym, NEW_NEWMV | NEW_NEARESTMV | NEW_NEARMV) {
|
||||
NEWMV
|
||||
} else if matches!(ym, NEAREST_NEARESTMV | NEAREST_NEWMV) {
|
||||
NEARESTMV
|
||||
} else if matches!(ym, NEAR_NEARMV | NEAR_NEWMV) {
|
||||
NEARMV
|
||||
} else {
|
||||
GLOBALMV
|
||||
}
|
||||
} else if matches!(ym, NEW_NEWMV | NEAREST_NEWMV | NEAR_NEWMV) {
|
||||
NEWMV
|
||||
} else if matches!(ym, NEAREST_NEARESTMV | NEW_NEARESTMV) {
|
||||
NEARESTMV
|
||||
} else if matches!(ym, NEAR_NEARMV | NEW_NEARMV) {
|
||||
NEARMV
|
||||
} else {
|
||||
GLOBALMV
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn assign_mv(&mut self, is_compound: bool) -> Result<()> {
|
||||
for i in 0..1 + is_compound as usize {
|
||||
let comp_mode = if self.b.use_intrabc { NEWMV } else { self.get_mode(i) };
|
||||
let pred: MvP = if self.b.use_intrabc {
|
||||
let mut p = self.mvs.ref_stack_mv[0][0];
|
||||
if p == [0, 0] {
|
||||
p = self.mvs.ref_stack_mv[1][0];
|
||||
}
|
||||
if p == [0, 0] {
|
||||
let sb_size = if self.fs.seq.use_128x128_superblock { BLOCK_128X128 } else { BLOCK_64X64 };
|
||||
let sb4 = NUM_4X4_BLOCKS_HIGH[sb_size] as i32;
|
||||
if (self.b.mi_row as i32) - sb4 < self.mi_row_start as i32 {
|
||||
p = [0, -(sb4 * 4 + INTRABC_DELAY_PIXELS as i32) * 8];
|
||||
} else {
|
||||
p = [-(sb4 * 4 * 8), 0];
|
||||
}
|
||||
}
|
||||
p
|
||||
} else if comp_mode == GLOBALMV {
|
||||
self.mvs.global_mvs[i]
|
||||
} else {
|
||||
let mut pos = if comp_mode == NEARESTMV { 0 } else { self.b.ref_mv_idx };
|
||||
if comp_mode == NEWMV && self.mvs.num_mv_found <= 1 {
|
||||
pos = 0;
|
||||
}
|
||||
self.mvs.ref_stack_mv[pos][i]
|
||||
};
|
||||
self.b.pred_mv[i] = pred;
|
||||
if comp_mode == NEWMV {
|
||||
self.read_mv(i);
|
||||
} else {
|
||||
self.b.mv_i[i] = pred;
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn read_mv(&mut self, r: usize) {
|
||||
let mut diff = [0i32; 2];
|
||||
let ctx = if self.b.use_intrabc { MV_INTRABC_CONTEXT } else { 0 };
|
||||
let joint = self.sd.read_symbol(&mut self.cdf.mv_joint[ctx]);
|
||||
if joint == MV_JOINT_HZVNZ || joint == MV_JOINT_HNZVNZ {
|
||||
diff[0] = self.read_mv_component(ctx, 0);
|
||||
}
|
||||
if joint == MV_JOINT_HNZVZ || joint == MV_JOINT_HNZVNZ {
|
||||
diff[1] = self.read_mv_component(ctx, 1);
|
||||
}
|
||||
self.b.mv_i[r] = [self.b.pred_mv[r][0] + diff[0], self.b.pred_mv[r][1] + diff[1]];
|
||||
}
|
||||
|
||||
fn read_mv_component(&mut self, ctx: usize, comp: usize) -> i32 {
|
||||
let fh = &self.fs.fh;
|
||||
let (fim, hp) = (fh.force_integer_mv, fh.allow_high_precision_mv);
|
||||
let sign = self.sd.read_symbol(&mut self.cdf.mv_sign[ctx][comp]);
|
||||
let class = self.sd.read_symbol(&mut self.cdf.mv_class[ctx][comp]);
|
||||
let mag = if class == MV_CLASS_0 {
|
||||
let b0 = self.sd.read_symbol(&mut self.cdf.mv_class0_bit[ctx][comp]) as i32;
|
||||
let fr = if fim { 3 } else { self.sd.read_symbol(&mut self.cdf.mv_class0_fr[ctx][comp][b0 as usize]) as i32 };
|
||||
let h = if hp { self.sd.read_symbol(&mut self.cdf.mv_class0_hp[ctx][comp]) as i32 } else { 1 };
|
||||
((b0 << 3) | (fr << 1) | h) + 1
|
||||
} else {
|
||||
let mut d = 0i32;
|
||||
for i in 0..class {
|
||||
d |= (self.sd.read_symbol(&mut self.cdf.mv_bit[ctx][comp][i]) as i32) << i;
|
||||
}
|
||||
let mut mag = (CLASS0_SIZE as i32) << (class + 2);
|
||||
let fr = if fim { 3 } else { self.sd.read_symbol(&mut self.cdf.mv_fr[ctx][comp]) as i32 };
|
||||
let h = if hp { self.sd.read_symbol(&mut self.cdf.mv_hp[ctx][comp]) as i32 } else { 1 };
|
||||
mag += ((d << 3) | (fr << 1) | h) + 1;
|
||||
mag
|
||||
};
|
||||
if sign == 1 { -mag } else { mag }
|
||||
}
|
||||
|
||||
fn read_interintra_mode(&mut self, is_compound: bool) {
|
||||
let ms = self.b.mi_size;
|
||||
self.b.interintra = false;
|
||||
if !self.b.skip_mode && self.fs.seq.enable_interintra_compound && !is_compound && (BLOCK_8X8..=BLOCK_32X32).contains(&ms) {
|
||||
let ctx = SIZE_GROUP[ms] as usize - 1;
|
||||
self.b.interintra = self.sd.read_symbol(&mut self.cdf.inter_intra[ctx]) == 1;
|
||||
if self.b.interintra {
|
||||
self.b.interintra_mode = self.sd.read_symbol(&mut self.cdf.inter_intra_mode[ctx]);
|
||||
self.b.ref_frame[1] = INTRA_FRAME as i8;
|
||||
self.b.angle_delta_y = 0;
|
||||
self.b.angle_delta_uv = 0;
|
||||
self.b.use_filter_intra = false;
|
||||
self.b.wedge_interintra = self.sd.read_symbol(&mut self.cdf.wedge_inter_intra[ms]) == 1;
|
||||
if self.b.wedge_interintra {
|
||||
self.b.wedge_index = self.sd.read_symbol(&mut self.cdf.wedge_index[ms]);
|
||||
self.b.wedge_sign = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn is_scaled(&self, rf: i8) -> bool {
|
||||
let fh = &self.fs.fh;
|
||||
let ri = &self.fs.ref_info[fh.ref_frame_idx[rf as usize - LAST_FRAME]];
|
||||
let fw = fh.frame_width;
|
||||
let fhh = fh.frame_height;
|
||||
let xs = ((ri.upscaled_width << REF_SCALE_SHIFT) + fw / 2) / fw;
|
||||
let ys = ((ri.frame_height << REF_SCALE_SHIFT) + fhh / 2) / fhh;
|
||||
let no = 1 << REF_SCALE_SHIFT;
|
||||
xs != no || ys != no
|
||||
}
|
||||
|
||||
fn read_motion_mode(&mut self, is_compound: bool) {
|
||||
self.b.motion_mode = SIMPLE as u8;
|
||||
let fh = &self.fs.fh;
|
||||
if self.b.skip_mode || !fh.is_motion_mode_switchable {
|
||||
return;
|
||||
}
|
||||
let ms = self.b.mi_size;
|
||||
if block_width(ms).min(block_height(ms)) < 8 {
|
||||
return;
|
||||
}
|
||||
if !fh.force_integer_mv
|
||||
&& (self.b.y_mode == GLOBALMV || self.b.y_mode == GLOBAL_GLOBALMV)
|
||||
&& fh.gm_type[self.b.ref_frame[0] as usize] as usize > TRANSLATION
|
||||
{
|
||||
return;
|
||||
}
|
||||
if is_compound || self.b.ref_frame[1] == INTRA_FRAME as i8 || !self.has_overlappable_candidates() {
|
||||
return;
|
||||
}
|
||||
self.find_warp_samples();
|
||||
let fh = &self.fs.fh;
|
||||
if fh.force_integer_mv || self.mvs.num_samples == 0 || !fh.allow_warped_motion || self.is_scaled(self.b.ref_frame[0]) {
|
||||
let use_obmc = self.sd.read_symbol(&mut self.cdf.use_obmc[ms]);
|
||||
self.b.motion_mode = if use_obmc == 1 { OBMC as u8 } else { SIMPLE as u8 };
|
||||
} else {
|
||||
self.b.motion_mode = self.sd.read_symbol(&mut self.cdf.motion_mode[ms]) as u8;
|
||||
}
|
||||
}
|
||||
|
||||
fn read_compound_type(&mut self, is_compound: bool) {
|
||||
self.b.comp_group_idx = 0;
|
||||
self.b.compound_idx = 1;
|
||||
if self.b.skip_mode {
|
||||
self.b.compound_type = COMPOUND_AVERAGE;
|
||||
return;
|
||||
}
|
||||
if is_compound {
|
||||
let ms = self.b.mi_size;
|
||||
let n = WEDGE_BITS[ms];
|
||||
if self.fs.seq.enable_masked_compound {
|
||||
let ctx = self.comp_group_idx_ctx();
|
||||
self.b.comp_group_idx = self.sd.read_symbol(&mut self.cdf.comp_group_idx[ctx]) as u8;
|
||||
}
|
||||
if self.b.comp_group_idx == 0 {
|
||||
if self.fs.seq.enable_jnt_comp {
|
||||
let ctx = self.compound_idx_ctx();
|
||||
self.b.compound_idx = self.sd.read_symbol(&mut self.cdf.compound_idx[ctx]) as u8;
|
||||
self.b.compound_type = if self.b.compound_idx == 1 { COMPOUND_AVERAGE } else { COMPOUND_DISTANCE };
|
||||
} else {
|
||||
self.b.compound_type = COMPOUND_AVERAGE;
|
||||
}
|
||||
} else if n == 0 {
|
||||
self.b.compound_type = COMPOUND_DIFFWTD;
|
||||
} else {
|
||||
self.b.compound_type = self.sd.read_symbol(&mut self.cdf.compound_type[ms]);
|
||||
}
|
||||
if self.b.compound_type == COMPOUND_WEDGE {
|
||||
self.b.wedge_index = self.sd.read_symbol(&mut self.cdf.wedge_index[ms]);
|
||||
self.b.wedge_sign = self.sd.read_literal(1) as usize;
|
||||
} else if self.b.compound_type == COMPOUND_DIFFWTD {
|
||||
self.b.mask_type = self.sd.read_literal(1) as usize;
|
||||
}
|
||||
} else if self.b.interintra {
|
||||
self.b.compound_type = if self.b.wedge_interintra { COMPOUND_WEDGE } else { COMPOUND_INTRA };
|
||||
} else {
|
||||
self.b.compound_type = COMPOUND_AVERAGE;
|
||||
}
|
||||
}
|
||||
|
||||
fn comp_group_idx_ctx(&self) -> usize {
|
||||
let n = self.nb;
|
||||
let mi = &self.t.mi;
|
||||
let (r, c) = (self.b.mi_row, self.b.mi_col);
|
||||
let mut ctx = 0;
|
||||
if self.b.avail_u {
|
||||
if !n.above_single {
|
||||
ctx += mi.comp_group_idx[mi.idx(r - 1, c)] as usize;
|
||||
} else if n.above[0] == ALTREF_FRAME as i8 {
|
||||
ctx += 3;
|
||||
}
|
||||
}
|
||||
if self.b.avail_l {
|
||||
if !n.left_single {
|
||||
ctx += mi.comp_group_idx[mi.idx(r, c - 1)] as usize;
|
||||
} else if n.left[0] == ALTREF_FRAME as i8 {
|
||||
ctx += 3;
|
||||
}
|
||||
}
|
||||
ctx.min(5)
|
||||
}
|
||||
|
||||
fn compound_idx_ctx(&self) -> usize {
|
||||
let fh = &self.fs.fh;
|
||||
let seq = &self.fs.seq;
|
||||
let fwd = crate::header::relative_dist(seq, fh.order_hints[self.b.ref_frame[0] as usize], fh.order_hint).abs();
|
||||
let bck = crate::header::relative_dist(seq, fh.order_hints[self.b.ref_frame[1] as usize], fh.order_hint).abs();
|
||||
let mut ctx = if fwd == bck { 3 } else { 0 };
|
||||
let n = self.nb;
|
||||
let mi = &self.t.mi;
|
||||
let (r, c) = (self.b.mi_row, self.b.mi_col);
|
||||
if self.b.avail_u {
|
||||
if !n.above_single {
|
||||
ctx += mi.compound_idx[mi.idx(r - 1, c)] as usize;
|
||||
} else if n.above[0] == ALTREF_FRAME as i8 {
|
||||
ctx += 1;
|
||||
}
|
||||
}
|
||||
if self.b.avail_l {
|
||||
if !n.left_single {
|
||||
ctx += mi.compound_idx[mi.idx(r, c - 1)] as usize;
|
||||
} else if n.left[0] == ALTREF_FRAME as i8 {
|
||||
ctx += 1;
|
||||
}
|
||||
}
|
||||
ctx
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,407 @@
|
||||
//! Intra prediction (spec 7.11.2), palette prediction (7.11.4) and chroma from luma (7.11.5).
|
||||
|
||||
use crate::frame::Plane;
|
||||
use crate::spec_tables::{
|
||||
ANGLE_STEP, D67_PRED, DC_PRED, DR_INTRA_DERIVATIVE, INTRA_EDGE_KERNEL, INTRA_EDGE_TAPS, INTRA_FILTER_SCALE_BITS, INTRA_FILTER_TAPS,
|
||||
MODE_TO_ANGLE, SM_WEIGHTS_TX_4X4, SM_WEIGHTS_TX_8X8, SM_WEIGHTS_TX_16X16, SM_WEIGHTS_TX_32X32, SM_WEIGHTS_TX_64X64, SMOOTH_H_PRED, SMOOTH_PRED,
|
||||
SMOOTH_V_PRED, V_PRED,
|
||||
};
|
||||
use crate::transform::round2;
|
||||
|
||||
/// Inputs of the intra prediction process that come from the block.
|
||||
pub(crate) struct IntraParams {
|
||||
pub plane: usize,
|
||||
pub x: usize,
|
||||
pub y: usize,
|
||||
pub have_left: bool,
|
||||
pub have_above: bool,
|
||||
pub have_above_right: bool,
|
||||
pub have_below_left: bool,
|
||||
pub mode: usize,
|
||||
pub log2w: u32,
|
||||
pub log2h: u32,
|
||||
/// Largest valid x / y in the plane: ((MiCols * MI_SIZE) >> subX) - 1.
|
||||
pub max_x: i32,
|
||||
pub max_y: i32,
|
||||
pub bit_depth: u32,
|
||||
pub angle_delta: i32,
|
||||
pub use_filter_intra: bool,
|
||||
pub filter_intra_mode: usize,
|
||||
pub enable_intra_edge_filter: bool,
|
||||
/// Result of the intra filter type process (7.11.2.8).
|
||||
pub filter_type: bool,
|
||||
}
|
||||
|
||||
const EDGE: usize = 32; // offset of index 0 in the edge buffers (they start at index -2 .. -1)
|
||||
|
||||
#[inline(always)]
|
||||
fn clip1(v: i32, bd: u32) -> u16 {
|
||||
v.clamp(0, (1 << bd) - 1) as u16
|
||||
}
|
||||
|
||||
pub(crate) fn is_directional_mode(mode: usize) -> bool {
|
||||
(V_PRED..=D67_PRED).contains(&mode)
|
||||
}
|
||||
|
||||
/// Predict the w x h block at (x, y) of `pl` (writes into the plane).
|
||||
pub(crate) fn predict_intra(pl: &mut Plane, p: &IntraParams) {
|
||||
let w = 1usize << p.log2w;
|
||||
let h = 1usize << p.log2h;
|
||||
let bd = p.bit_depth;
|
||||
let (x, y) = (p.x, p.y);
|
||||
// edge arrays with room for index -2 .. 2*(w+h)
|
||||
let mut above = [0i32; 2 * 128 + 2 * EDGE];
|
||||
let mut left = [0i32; 2 * 128 + 2 * EDGE];
|
||||
let n = w + h;
|
||||
if !p.have_above && p.have_left {
|
||||
let v = pl.at(x - 1, y) as i32;
|
||||
above[EDGE..EDGE + n].iter_mut().for_each(|a| *a = v);
|
||||
} else if !p.have_above && !p.have_left {
|
||||
let v = (1 << (bd - 1)) - 1;
|
||||
above[EDGE..EDGE + n].iter_mut().for_each(|a| *a = v);
|
||||
} else {
|
||||
let above_limit = (p.max_x).min(x as i32 + if p.have_above_right { 2 * w as i32 } else { w as i32 } - 1);
|
||||
let row = pl.row_from(y - 1, x);
|
||||
for i in 0..n {
|
||||
above[EDGE + i] = row[(above_limit.min(x as i32 + i as i32)) as usize - x] as i32;
|
||||
}
|
||||
}
|
||||
if !p.have_left && p.have_above {
|
||||
let v = pl.at(x, y - 1) as i32;
|
||||
left[EDGE..EDGE + n].iter_mut().for_each(|a| *a = v);
|
||||
} else if !p.have_left && !p.have_above {
|
||||
let v = (1 << (bd - 1)) + 1;
|
||||
left[EDGE..EDGE + n].iter_mut().for_each(|a| *a = v);
|
||||
} else {
|
||||
let left_limit = (p.max_y).min(y as i32 + if p.have_below_left { 2 * h as i32 } else { h as i32 } - 1);
|
||||
for i in 0..n {
|
||||
left[EDGE + i] = pl.at(x - 1, left_limit.min(y as i32 + i as i32) as usize) as i32;
|
||||
}
|
||||
}
|
||||
let corner = if p.have_above && p.have_left {
|
||||
pl.at(x - 1, y - 1) as i32
|
||||
} else if p.have_above {
|
||||
pl.at(x, y - 1) as i32
|
||||
} else if p.have_left {
|
||||
pl.at(x - 1, y) as i32
|
||||
} else {
|
||||
1 << (bd - 1)
|
||||
};
|
||||
above[EDGE - 1] = corner;
|
||||
left[EDGE - 1] = corner;
|
||||
|
||||
let mut pred = [0u16; 64 * 64];
|
||||
if p.plane == 0 && p.use_filter_intra {
|
||||
recursive_intra(&above, &left, w, h, p.filter_intra_mode, bd, &mut pred);
|
||||
} else if is_directional_mode(p.mode) {
|
||||
directional(&mut above, &mut left, p, w, h, &mut pred);
|
||||
} else if p.mode == SMOOTH_PRED || p.mode == SMOOTH_V_PRED || p.mode == SMOOTH_H_PRED {
|
||||
smooth(&above, &left, p.mode, p.log2w, p.log2h, w, h, &mut pred);
|
||||
} else if p.mode == DC_PRED {
|
||||
dc(&above, &left, p.have_left, p.have_above, p.log2w, p.log2h, w, h, bd, &mut pred);
|
||||
} else {
|
||||
// PAETH_PRED
|
||||
let tl = above[EDGE - 1];
|
||||
for i in 0..h {
|
||||
for j in 0..w {
|
||||
let a = above[EDGE + j];
|
||||
let l = left[EDGE + i];
|
||||
let base = a + l - tl;
|
||||
let p_left = (base - l).abs();
|
||||
let p_top = (base - a).abs();
|
||||
let p_tl = (base - tl).abs();
|
||||
pred[i * w + j] = if p_left <= p_top && p_left <= p_tl {
|
||||
l as u16
|
||||
} else if p_top <= p_tl {
|
||||
a as u16
|
||||
} else {
|
||||
tl as u16
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
for i in 0..h {
|
||||
pl.row_from_mut(y + i, x)[..w].copy_from_slice(&pred[i * w..i * w + w]);
|
||||
}
|
||||
}
|
||||
|
||||
fn recursive_intra(above: &[i32], left: &[i32], w: usize, h: usize, mode: usize, bd: u32, pred: &mut [u16]) {
|
||||
let w4 = w >> 2;
|
||||
let h2 = h >> 1;
|
||||
for i2 in 0..h2 {
|
||||
for j4 in 0..w4 {
|
||||
let mut pv = [0i32; 7];
|
||||
for (i, v) in pv.iter_mut().enumerate() {
|
||||
*v = if i < 5 {
|
||||
if i2 == 0 {
|
||||
above[EDGE + (j4 << 2) + i - 1]
|
||||
} else if j4 == 0 && i == 0 {
|
||||
left[EDGE + (i2 << 1) - 1]
|
||||
} else {
|
||||
pred[((i2 << 1) - 1) * w + (j4 << 2) + i - 1] as i32
|
||||
}
|
||||
} else if j4 == 0 {
|
||||
left[EDGE + (i2 << 1) + i - 5]
|
||||
} else {
|
||||
pred[((i2 << 1) + i - 5) * w + (j4 << 2) - 1] as i32
|
||||
};
|
||||
}
|
||||
for i1 in 0..2 {
|
||||
for j1 in 0..4 {
|
||||
let mut pr = 0i32;
|
||||
for i in 0..7 {
|
||||
pr += INTRA_FILTER_TAPS[mode][(i1 << 2) + j1][i] as i32 * pv[i];
|
||||
}
|
||||
pred[((i2 << 1) + i1) * w + (j4 << 2) + j1] = clip1(round2_signed(pr, INTRA_FILTER_SCALE_BITS as u32), bd);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
pub(crate) fn round2_signed(x: i32, n: u32) -> i32 {
|
||||
if x >= 0 { round2(x, n) } else { -round2(-x, n) }
|
||||
}
|
||||
|
||||
fn sm_weights(log2: u32) -> &'static [u8] {
|
||||
match log2 {
|
||||
2 => &SM_WEIGHTS_TX_4X4,
|
||||
3 => &SM_WEIGHTS_TX_8X8,
|
||||
4 => &SM_WEIGHTS_TX_16X16,
|
||||
5 => &SM_WEIGHTS_TX_32X32,
|
||||
_ => &SM_WEIGHTS_TX_64X64,
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
fn smooth(above: &[i32], left: &[i32], mode: usize, log2w: u32, log2h: u32, w: usize, h: usize, pred: &mut [u16]) {
|
||||
let wx = sm_weights(log2w);
|
||||
let wy = sm_weights(log2h);
|
||||
for i in 0..h {
|
||||
for j in 0..w {
|
||||
let v = if mode == SMOOTH_PRED {
|
||||
let s = wy[i] as i32 * above[EDGE + j]
|
||||
+ (256 - wy[i] as i32) * left[EDGE + h - 1]
|
||||
+ wx[j] as i32 * left[EDGE + i]
|
||||
+ (256 - wx[j] as i32) * above[EDGE + w - 1];
|
||||
round2(s, 9)
|
||||
} else if mode == SMOOTH_V_PRED {
|
||||
round2(wy[i] as i32 * above[EDGE + j] + (256 - wy[i] as i32) * left[EDGE + h - 1], 8)
|
||||
} else {
|
||||
round2(wx[j] as i32 * left[EDGE + i] + (256 - wx[j] as i32) * above[EDGE + w - 1], 8)
|
||||
};
|
||||
pred[i * w + j] = v as u16;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
fn dc(above: &[i32], left: &[i32], have_left: bool, have_above: bool, log2w: u32, log2h: u32, w: usize, h: usize, bd: u32, pred: &mut [u16]) {
|
||||
let v = if have_left && have_above {
|
||||
let mut sum: i32 = left[EDGE..EDGE + h].iter().sum::<i32>() + above[EDGE..EDGE + w].iter().sum::<i32>();
|
||||
sum += ((w + h) >> 1) as i32;
|
||||
sum / (w + h) as i32
|
||||
} else if have_left {
|
||||
let sum: i32 = left[EDGE..EDGE + h].iter().sum();
|
||||
(sum + (h >> 1) as i32) >> log2h
|
||||
} else if have_above {
|
||||
let sum: i32 = above[EDGE..EDGE + w].iter().sum();
|
||||
(sum + (w >> 1) as i32) >> log2w
|
||||
} else {
|
||||
1 << (bd - 1)
|
||||
};
|
||||
let v = clip1(v, bd);
|
||||
pred[..w * h].iter_mut().for_each(|p| *p = v);
|
||||
}
|
||||
|
||||
fn edge_filter_strength(w: usize, h: usize, filter_type: bool, delta: i32) -> u32 {
|
||||
let d = delta.abs();
|
||||
let blk_wh = w + h;
|
||||
let mut strength = 0;
|
||||
if !filter_type {
|
||||
if blk_wh <= 8 {
|
||||
if d >= 56 {
|
||||
strength = 1;
|
||||
}
|
||||
} else if blk_wh <= 16 {
|
||||
if d >= 40 {
|
||||
strength = 1;
|
||||
}
|
||||
} else if blk_wh <= 24 {
|
||||
if d >= 8 {
|
||||
strength = 1;
|
||||
}
|
||||
if d >= 16 {
|
||||
strength = 2;
|
||||
}
|
||||
if d >= 32 {
|
||||
strength = 3;
|
||||
}
|
||||
} else if blk_wh <= 32 {
|
||||
strength = 1;
|
||||
if d >= 4 {
|
||||
strength = 2;
|
||||
}
|
||||
if d >= 32 {
|
||||
strength = 3;
|
||||
}
|
||||
} else {
|
||||
strength = 3;
|
||||
}
|
||||
} else if blk_wh <= 8 {
|
||||
if d >= 40 {
|
||||
strength = 1;
|
||||
}
|
||||
if d >= 64 {
|
||||
strength = 2;
|
||||
}
|
||||
} else if blk_wh <= 16 {
|
||||
if d >= 20 {
|
||||
strength = 1;
|
||||
}
|
||||
if d >= 48 {
|
||||
strength = 2;
|
||||
}
|
||||
} else if blk_wh <= 24 {
|
||||
if d >= 4 {
|
||||
strength = 3;
|
||||
}
|
||||
} else {
|
||||
strength = 3;
|
||||
}
|
||||
strength
|
||||
}
|
||||
|
||||
fn use_upsample(w: usize, h: usize, filter_type: bool, delta: i32) -> bool {
|
||||
let d = delta.abs();
|
||||
let blk_wh = w + h;
|
||||
if d <= 0 || d >= 40 {
|
||||
false
|
||||
} else if !filter_type {
|
||||
blk_wh <= 16
|
||||
} else {
|
||||
blk_wh <= 8
|
||||
}
|
||||
}
|
||||
|
||||
/// Intra edge filter (7.11.2.12) on buf[-1 .. sz-2] (edge[i] = buf[i - 1]).
|
||||
fn edge_filter(buf: &mut [i32], sz: usize, strength: u32) {
|
||||
if strength == 0 {
|
||||
return;
|
||||
}
|
||||
let mut edge = [0i32; 2 * 128 + 2 * EDGE];
|
||||
for i in 0..sz {
|
||||
edge[i] = buf[EDGE + i - 1];
|
||||
}
|
||||
for i in 1..sz {
|
||||
let mut s = 0;
|
||||
for j in 0..INTRA_EDGE_TAPS {
|
||||
let k = (i as i32 - 2 + j as i32).clamp(0, sz as i32 - 1) as usize;
|
||||
s += INTRA_EDGE_KERNEL[strength as usize - 1][j] as i32 * edge[k];
|
||||
}
|
||||
buf[EDGE + i - 1] = (s + 8) >> 4;
|
||||
}
|
||||
}
|
||||
|
||||
/// Intra edge upsample (7.11.2.11).
|
||||
fn edge_upsample(buf: &mut [i32], num_px: usize, bd: u32) {
|
||||
let mut dup = [0i32; 2 * 128 + 8];
|
||||
dup[0] = buf[EDGE - 1];
|
||||
for i in -1..num_px as i32 {
|
||||
dup[(i + 2) as usize] = buf[(EDGE as i32 + i) as usize];
|
||||
}
|
||||
dup[num_px + 2] = buf[EDGE + num_px - 1];
|
||||
buf[EDGE - 2] = dup[0];
|
||||
for i in 0..num_px {
|
||||
let s = -dup[i] + 9 * dup[i + 1] + 9 * dup[i + 2] - dup[i + 3];
|
||||
let s = clip1(round2(s, 4), bd) as i32;
|
||||
buf[EDGE + 2 * i - 1] = s;
|
||||
buf[EDGE + 2 * i] = dup[i + 2];
|
||||
}
|
||||
}
|
||||
|
||||
fn directional(above: &mut [i32], left: &mut [i32], p: &IntraParams, w: usize, h: usize, pred: &mut [u16]) {
|
||||
let p_angle = MODE_TO_ANGLE[p.mode] as i32 + p.angle_delta * ANGLE_STEP as i32;
|
||||
let mut upsample_above = 0u32;
|
||||
let mut upsample_left = 0u32;
|
||||
if p.enable_intra_edge_filter {
|
||||
if p_angle != 90 && p_angle != 180 {
|
||||
if p_angle > 90 && p_angle < 180 && (w + h) >= 24 {
|
||||
let s = left[EDGE] * 5 + above[EDGE - 1] * 6 + above[EDGE] * 5;
|
||||
let c = round2(s, 4);
|
||||
left[EDGE - 1] = c;
|
||||
above[EDGE - 1] = c;
|
||||
}
|
||||
if p.have_above {
|
||||
let strength = edge_filter_strength(w, h, p.filter_type, p_angle - 90);
|
||||
let num_px = w.min((p.max_x - p.x as i32 + 1) as usize) + if p_angle < 90 { h } else { 0 } + 1;
|
||||
edge_filter(above, num_px, strength);
|
||||
}
|
||||
if p.have_left {
|
||||
let strength = edge_filter_strength(w, h, p.filter_type, p_angle - 180);
|
||||
let num_px = h.min((p.max_y - p.y as i32 + 1) as usize) + if p_angle > 180 { w } else { 0 } + 1;
|
||||
edge_filter(left, num_px, strength);
|
||||
}
|
||||
}
|
||||
if use_upsample(w, h, p.filter_type, p_angle - 90) {
|
||||
upsample_above = 1;
|
||||
let num_px = w + if p_angle < 90 { h } else { 0 };
|
||||
edge_upsample(above, num_px, p.bit_depth);
|
||||
}
|
||||
if use_upsample(w, h, p.filter_type, p_angle - 180) {
|
||||
upsample_left = 1;
|
||||
let num_px = h + if p_angle > 180 { w } else { 0 };
|
||||
edge_upsample(left, num_px, p.bit_depth);
|
||||
}
|
||||
}
|
||||
let dx = if p_angle < 90 {
|
||||
DR_INTRA_DERIVATIVE[p_angle as usize] as i32
|
||||
} else if p_angle > 90 && p_angle < 180 {
|
||||
DR_INTRA_DERIVATIVE[(180 - p_angle) as usize] as i32
|
||||
} else {
|
||||
0
|
||||
};
|
||||
let dy = if p_angle > 90 && p_angle < 180 {
|
||||
DR_INTRA_DERIVATIVE[(p_angle - 90) as usize] as i32
|
||||
} else if p_angle > 180 {
|
||||
DR_INTRA_DERIVATIVE[(270 - p_angle) as usize] as i32
|
||||
} else {
|
||||
0
|
||||
};
|
||||
let a = |i: i32| above[(EDGE as i32 + i) as usize];
|
||||
let l = |i: i32| left[(EDGE as i32 + i) as usize];
|
||||
for i in 0..h as i32 {
|
||||
for j in 0..w as i32 {
|
||||
let v = if p_angle < 90 {
|
||||
let idx = (i + 1) * dx;
|
||||
let base = (idx >> (6 - upsample_above)) + (j << upsample_above);
|
||||
let shift = ((idx << upsample_above) >> 1) & 0x1f;
|
||||
let max_base_x = (w as i32 + h as i32 - 1) << upsample_above;
|
||||
if base < max_base_x { round2(a(base) * (32 - shift) + a(base + 1) * shift, 5) } else { a(max_base_x) }
|
||||
} else if p_angle > 90 && p_angle < 180 {
|
||||
let idx = (j << 6) - (i + 1) * dx;
|
||||
let base = idx >> (6 - upsample_above);
|
||||
if base >= -(1 << upsample_above) {
|
||||
let shift = ((idx << upsample_above) >> 1) & 0x1f;
|
||||
round2(a(base) * (32 - shift) + a(base + 1) * shift, 5)
|
||||
} else {
|
||||
let idx = (i << 6) - (j + 1) * dy;
|
||||
let base = idx >> (6 - upsample_left);
|
||||
let shift = ((idx << upsample_left) >> 1) & 0x1f;
|
||||
round2(l(base) * (32 - shift) + l(base + 1) * shift, 5)
|
||||
}
|
||||
} else if p_angle > 180 {
|
||||
let idx = (j + 1) * dy;
|
||||
let base = (idx >> (6 - upsample_left)) + (i << upsample_left);
|
||||
let shift = ((idx << upsample_left) >> 1) & 0x1f;
|
||||
round2(l(base) * (32 - shift) + l(base + 1) * shift, 5)
|
||||
} else if p_angle == 90 {
|
||||
a(j)
|
||||
} else {
|
||||
l(i)
|
||||
};
|
||||
pred[(i as usize) * w + j as usize] = v as u16;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,123 @@
|
||||
//! Clean-room AV1 decoder, implemented from the AV1 Bitstream & Decoding Process Specification
|
||||
//! v1.0.0 with Errata 1 (AOMediaCodec/av1-spec).
|
||||
//!
|
||||
//! See the crate README for the conformance status of each decoding stage.
|
||||
|
||||
// Items used only by stages still being written (inter prediction, post filters).
|
||||
// Spec-style indexed loops (FilmCraft allows this lint workspace-wide).
|
||||
#![allow(clippy::needless_range_loop)]
|
||||
#![allow(dead_code)]
|
||||
#![cfg_attr(not(test), deny(clippy::unwrap_used, clippy::expect_used, clippy::panic, clippy::unimplemented, clippy::todo, clippy::unreachable))]
|
||||
|
||||
mod bits;
|
||||
mod cdef;
|
||||
mod cdf;
|
||||
mod decoder;
|
||||
mod frame;
|
||||
mod grain;
|
||||
pub mod header;
|
||||
mod inter;
|
||||
mod inter_info;
|
||||
mod intra;
|
||||
mod mvpred;
|
||||
mod par;
|
||||
mod postfilter;
|
||||
mod restoration;
|
||||
mod spec_tables;
|
||||
mod state;
|
||||
mod stats;
|
||||
mod symbol;
|
||||
mod tile;
|
||||
mod transform;
|
||||
|
||||
pub use decoder::Decoder;
|
||||
pub use header::{ColorConfig, FrameHeader, SequenceHeader};
|
||||
pub use stats::{DecodeStats, Stage};
|
||||
|
||||
/// Decoder errors.
|
||||
#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
|
||||
pub enum Error {
|
||||
#[error("truncated AV1 data")]
|
||||
Truncated,
|
||||
#[error("invalid AV1 data: {0}")]
|
||||
Invalid(&'static str),
|
||||
#[error("unsupported AV1 feature: {0}")]
|
||||
Unsupported(&'static str),
|
||||
}
|
||||
|
||||
pub type Result<T> = std::result::Result<T, Error>;
|
||||
|
||||
/// A decoded (shown) frame: planar samples without row padding, `bit_depth` significant bits in
|
||||
/// `u16`. Chroma planes are `((width + ssx) >> ssx) x ((height + ssy) >> ssy)`; they are empty
|
||||
/// for monochrome streams.
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct Picture {
|
||||
pub width: u32,
|
||||
pub height: u32,
|
||||
pub bit_depth: u8,
|
||||
pub subsampling_x: u8,
|
||||
pub subsampling_y: u8,
|
||||
pub mono_chrome: bool,
|
||||
pub planes: [Vec<u16>; 3],
|
||||
pub color_primaries: u8,
|
||||
pub transfer_characteristics: u8,
|
||||
pub matrix_coefficients: u8,
|
||||
pub full_range: bool,
|
||||
/// Timestamp of the temporal unit that showed the picture ([`Decoder::decode_pts`]).
|
||||
pub pts: i64,
|
||||
/// Decoded in draft mode without deblocking / CDEF / loop restoration
|
||||
/// ([`Decoder::set_draft`]): approximate, for reduced-resolution playback only.
|
||||
pub draft: bool,
|
||||
}
|
||||
|
||||
impl Picture {
|
||||
pub fn plane_width(&self, plane: usize) -> u32 {
|
||||
if plane == 0 { self.width } else { (self.width + self.subsampling_x as u32) >> self.subsampling_x }
|
||||
}
|
||||
pub fn plane_height(&self, plane: usize) -> u32 {
|
||||
if plane == 0 { self.height } else { (self.height + self.subsampling_y as u32) >> self.subsampling_y }
|
||||
}
|
||||
}
|
||||
|
||||
/// Whether a temporal unit starts a random access point: its first frame is a shown key frame
|
||||
/// (used for keyframe detection: containers may flag samples as sync that are not).
|
||||
pub fn is_key_frame_unit(data: &[u8]) -> bool {
|
||||
let mut pos = 0;
|
||||
let mut seq_seen = false;
|
||||
while pos < data.len() {
|
||||
let h = data[pos];
|
||||
let obu_type = (h >> 3) & 0xf;
|
||||
let ext = h & 4 != 0;
|
||||
let has_size = h & 2 != 0;
|
||||
let mut p = pos + 1 + ext as usize;
|
||||
let size = if has_size {
|
||||
match bits::leb128(data.get(p..).unwrap_or(&[])) {
|
||||
Ok((v, n)) => {
|
||||
p += n;
|
||||
v as usize
|
||||
}
|
||||
Err(_) => return false,
|
||||
}
|
||||
} else {
|
||||
data.len().saturating_sub(p)
|
||||
};
|
||||
if p > data.len() {
|
||||
return false;
|
||||
}
|
||||
match obu_type {
|
||||
1 => seq_seen = true,
|
||||
3 | 6 => {
|
||||
let b = data.get(p).copied().unwrap_or(0);
|
||||
// show_existing_frame = 0, frame_type = KEY_FRAME (0), show_frame = 1
|
||||
let show_existing = b >> 7;
|
||||
let frame_type = (b >> 5) & 3;
|
||||
let show_frame = (b >> 4) & 1;
|
||||
let _ = seq_seen;
|
||||
return show_existing == 0 && frame_type == 0 && show_frame == 1;
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
pos = p.saturating_add(size);
|
||||
}
|
||||
false
|
||||
}
|
||||
@@ -0,0 +1,737 @@
|
||||
//! Motion vector prediction processes (spec 7.10): find_mv_stack, has_overlappable_candidates
|
||||
//! and find_warp_samples.
|
||||
|
||||
use crate::spec_tables::{
|
||||
GLOBAL_GLOBALMV, GLOBALMV, IDENTITY, INTRA_FRAME, LEAST_SQUARES_SAMPLES_MAX, MAX_REF_MV_STACK_SIZE, MV_BORDER, NEAR_NEWMV, NEAREST_NEWMV,
|
||||
NEW_NEARESTMV, NEW_NEARMV, NEW_NEWMV, NEWMV, NUM_4X4_BLOCKS_HIGH, NUM_4X4_BLOCKS_WIDE, REF_CAT_LEVEL, TRANSLATION, WARPEDMODEL_PREC_BITS,
|
||||
};
|
||||
use crate::tile::TileDecoder;
|
||||
|
||||
pub(crate) type MvP = [i32; 2];
|
||||
pub(crate) const INVALID_MV: i32 = -1 << 15;
|
||||
|
||||
/// Output of find_mv_stack plus its working state.
|
||||
#[derive(Clone, Default)]
|
||||
pub(crate) struct MvStack {
|
||||
pub ref_stack_mv: [[MvP; 2]; MAX_REF_MV_STACK_SIZE + 1],
|
||||
pub weight_stack: [u32; MAX_REF_MV_STACK_SIZE + 1],
|
||||
pub num_mv_found: usize,
|
||||
pub new_mv_count: usize,
|
||||
pub global_mvs: [MvP; 2],
|
||||
pub found_match: bool,
|
||||
pub close_matches: usize,
|
||||
pub total_matches: usize,
|
||||
pub new_mv_context: usize,
|
||||
pub ref_mv_context: usize,
|
||||
pub zero_mv_context: usize,
|
||||
pub drl_ctx_stack: [usize; MAX_REF_MV_STACK_SIZE + 1],
|
||||
ref_id_count: [usize; 2],
|
||||
ref_diff_count: [usize; 2],
|
||||
ref_id_mvs: [[MvP; 2]; 2],
|
||||
ref_diff_mvs: [[MvP; 2]; 2],
|
||||
// warp samples
|
||||
pub num_samples: usize,
|
||||
pub num_samples_scanned: usize,
|
||||
pub cand_list: [[i32; 4]; LEAST_SQUARES_SAMPLES_MAX],
|
||||
}
|
||||
|
||||
fn block_width(bs: usize) -> i32 {
|
||||
4 * NUM_4X4_BLOCKS_WIDE[bs] as i32
|
||||
}
|
||||
fn block_height(bs: usize) -> i32 {
|
||||
4 * NUM_4X4_BLOCKS_HIGH[bs] as i32
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub(crate) fn round2_signed64(x: i64, n: u32) -> i64 {
|
||||
if n == 0 {
|
||||
return x;
|
||||
}
|
||||
if x >= 0 { (x + (1 << (n - 1))) >> n } else { -((-x + (1 << (n - 1))) >> n) }
|
||||
}
|
||||
|
||||
pub(crate) fn has_newmv(mode: usize) -> bool {
|
||||
matches!(mode, NEWMV | NEW_NEWMV | NEAR_NEWMV | NEW_NEARMV | NEAREST_NEWMV | NEW_NEARESTMV)
|
||||
}
|
||||
|
||||
impl TileDecoder<'_, '_> {
|
||||
/// lower_mv_precision( candMv ) (7.10.2.10)
|
||||
pub(crate) fn lower_mv_precision(&self, mv: &mut MvP) {
|
||||
let fh = &self.fs.fh;
|
||||
if fh.allow_high_precision_mv {
|
||||
return;
|
||||
}
|
||||
for c in mv.iter_mut() {
|
||||
if fh.force_integer_mv {
|
||||
let a = c.abs();
|
||||
let a_int = (a + 3) >> 3;
|
||||
*c = if *c > 0 { a_int << 3 } else { -(a_int << 3) };
|
||||
} else if *c & 1 != 0 {
|
||||
*c += if *c > 0 { -1 } else { 1 };
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Setup global MV process (7.10.2.1).
|
||||
pub(crate) fn setup_global_mv(&self, ref_list: usize) -> MvP {
|
||||
let rf = self.b.ref_frame[ref_list];
|
||||
let fh = &self.fs.fh;
|
||||
let mut mv: MvP = [0, 0];
|
||||
if rf > INTRA_FRAME as i8 {
|
||||
let rf = rf as usize;
|
||||
let typ = fh.gm_type[rf] as usize;
|
||||
let gm = &fh.gm_params[rf];
|
||||
if typ == TRANSLATION {
|
||||
mv = [gm[0] >> (WARPEDMODEL_PREC_BITS - 3), gm[1] >> (WARPEDMODEL_PREC_BITS - 3)];
|
||||
} else if typ != IDENTITY {
|
||||
let bw = block_width(self.b.mi_size) as i64;
|
||||
let bh = block_height(self.b.mi_size) as i64;
|
||||
let x = self.b.mi_col as i64 * 4 + bw / 2 - 1;
|
||||
let y = self.b.mi_row as i64 * 4 + bh / 2 - 1;
|
||||
let p = 1i64 << WARPEDMODEL_PREC_BITS;
|
||||
let xc = (gm[2] as i64 - p) * x + gm[3] as i64 * y + gm[0] as i64;
|
||||
let yc = gm[4] as i64 * x + (gm[5] as i64 - p) * y + gm[1] as i64;
|
||||
if fh.allow_high_precision_mv {
|
||||
mv = [round2_signed64(yc, WARPEDMODEL_PREC_BITS as u32 - 3) as i32, round2_signed64(xc, WARPEDMODEL_PREC_BITS as u32 - 3) as i32];
|
||||
} else {
|
||||
mv = [
|
||||
round2_signed64(yc, WARPEDMODEL_PREC_BITS as u32 - 2) as i32 * 2,
|
||||
round2_signed64(xc, WARPEDMODEL_PREC_BITS as u32 - 2) as i32 * 2,
|
||||
];
|
||||
}
|
||||
}
|
||||
}
|
||||
self.lower_mv_precision(&mut mv);
|
||||
mv
|
||||
}
|
||||
|
||||
/// Find MV stack process (7.10.2).
|
||||
pub(crate) fn find_mv_stack(&mut self, is_compound: bool) {
|
||||
let bw4 = self.b.bw4 as isize;
|
||||
let bh4 = self.b.bh4 as isize;
|
||||
let mut s = MvStack { global_mvs: [self.setup_global_mv(0), [0, 0]], ..Default::default() };
|
||||
if is_compound {
|
||||
s.global_mvs[1] = self.setup_global_mv(1);
|
||||
}
|
||||
self.mvs = s;
|
||||
self.mvs.found_match = false;
|
||||
self.scan_row(-1, is_compound);
|
||||
let mut found_above = self.mvs.found_match;
|
||||
self.mvs.found_match = false;
|
||||
self.scan_col(-1, is_compound);
|
||||
let mut found_left = self.mvs.found_match;
|
||||
self.mvs.found_match = false;
|
||||
if bw4.max(bh4) <= 16 {
|
||||
self.scan_point(-1, bw4, is_compound);
|
||||
}
|
||||
if self.mvs.found_match {
|
||||
found_above = true;
|
||||
}
|
||||
self.mvs.close_matches = found_above as usize + found_left as usize;
|
||||
let num_nearest = self.mvs.num_mv_found;
|
||||
let num_new = self.mvs.new_mv_count;
|
||||
for idx in 0..num_nearest {
|
||||
self.mvs.weight_stack[idx] += REF_CAT_LEVEL as u32;
|
||||
}
|
||||
self.mvs.zero_mv_context = 0;
|
||||
if self.fs.fh.use_ref_frame_mvs {
|
||||
self.temporal_scan(is_compound);
|
||||
}
|
||||
self.scan_point(-1, -1, is_compound);
|
||||
if self.mvs.found_match {
|
||||
found_above = true;
|
||||
}
|
||||
self.mvs.found_match = false;
|
||||
self.scan_row(-3, is_compound);
|
||||
if self.mvs.found_match {
|
||||
found_above = true;
|
||||
}
|
||||
self.mvs.found_match = false;
|
||||
self.scan_col(-3, is_compound);
|
||||
if self.mvs.found_match {
|
||||
found_left = true;
|
||||
}
|
||||
self.mvs.found_match = false;
|
||||
if bh4 > 1 {
|
||||
self.scan_row(-5, is_compound);
|
||||
}
|
||||
if self.mvs.found_match {
|
||||
found_above = true;
|
||||
}
|
||||
self.mvs.found_match = false;
|
||||
if bw4 > 1 {
|
||||
self.scan_col(-5, is_compound);
|
||||
}
|
||||
if self.mvs.found_match {
|
||||
found_left = true;
|
||||
}
|
||||
self.mvs.total_matches = found_above as usize + found_left as usize;
|
||||
let n = self.mvs.num_mv_found;
|
||||
self.sort_stack(0, num_nearest, is_compound);
|
||||
self.sort_stack(num_nearest, n, is_compound);
|
||||
if self.mvs.num_mv_found < 2 {
|
||||
self.extra_search(is_compound);
|
||||
}
|
||||
self.context_and_clamping(is_compound, num_new);
|
||||
}
|
||||
|
||||
fn scan_row(&mut self, delta_row: isize, is_compound: bool) {
|
||||
let bw4 = self.b.bw4 as isize;
|
||||
let (mi_row, mi_col) = (self.b.mi_row as isize, self.b.mi_col as isize);
|
||||
let mi_cols = self.fs.fh.mi_cols as isize;
|
||||
let end4 = bw4.min(mi_cols - mi_col).min(16);
|
||||
let mut delta_col = 0;
|
||||
let use_step16 = bw4 >= 16;
|
||||
let mut delta_row = delta_row;
|
||||
if delta_row.abs() > 1 {
|
||||
delta_row += mi_row & 1;
|
||||
delta_col = 1 - (mi_col & 1);
|
||||
}
|
||||
let mut i = 0;
|
||||
while i < end4 {
|
||||
let mv_row = mi_row + delta_row;
|
||||
let mv_col = mi_col + delta_col + i;
|
||||
if !self.inside(mv_row, mv_col) {
|
||||
break;
|
||||
}
|
||||
let mi = &self.t.mi;
|
||||
let mut len = bw4.min(NUM_4X4_BLOCKS_WIDE[mi.mi_size[mi.idx(mv_row as usize, mv_col as usize)] as usize] as isize);
|
||||
if delta_row.abs() > 1 {
|
||||
len = len.max(2);
|
||||
}
|
||||
if use_step16 {
|
||||
len = len.max(4);
|
||||
}
|
||||
let weight = len as u32 * 2;
|
||||
self.add_ref_mv_candidate(mv_row as usize, mv_col as usize, is_compound, weight);
|
||||
i += len;
|
||||
}
|
||||
}
|
||||
|
||||
fn scan_col(&mut self, delta_col: isize, is_compound: bool) {
|
||||
let bh4 = self.b.bh4 as isize;
|
||||
let (mi_row, mi_col) = (self.b.mi_row as isize, self.b.mi_col as isize);
|
||||
let mi_rows = self.fs.fh.mi_rows as isize;
|
||||
let end4 = bh4.min(mi_rows - mi_row).min(16);
|
||||
let mut delta_row = 0;
|
||||
let use_step16 = bh4 >= 16;
|
||||
let mut delta_col = delta_col;
|
||||
if delta_col.abs() > 1 {
|
||||
delta_row = 1 - (mi_row & 1);
|
||||
delta_col += mi_col & 1;
|
||||
}
|
||||
let mut i = 0;
|
||||
while i < end4 {
|
||||
let mv_row = mi_row + delta_row + i;
|
||||
let mv_col = mi_col + delta_col;
|
||||
if !self.inside(mv_row, mv_col) {
|
||||
break;
|
||||
}
|
||||
let mi = &self.t.mi;
|
||||
let mut len = bh4.min(NUM_4X4_BLOCKS_HIGH[mi.mi_size[mi.idx(mv_row as usize, mv_col as usize)] as usize] as isize);
|
||||
if delta_col.abs() > 1 {
|
||||
len = len.max(2);
|
||||
}
|
||||
if use_step16 {
|
||||
len = len.max(4);
|
||||
}
|
||||
let weight = len as u32 * 2;
|
||||
self.add_ref_mv_candidate(mv_row as usize, mv_col as usize, is_compound, weight);
|
||||
i += len;
|
||||
}
|
||||
}
|
||||
|
||||
fn scan_point(&mut self, delta_row: isize, delta_col: isize, is_compound: bool) {
|
||||
let mv_row = self.b.mi_row as isize + delta_row;
|
||||
let mv_col = self.b.mi_col as isize + delta_col;
|
||||
if self.inside(mv_row, mv_col) && self.t.mi.written[self.t.mi.idx(mv_row as usize, mv_col as usize)] {
|
||||
self.add_ref_mv_candidate(mv_row as usize, mv_col as usize, is_compound, 4);
|
||||
}
|
||||
}
|
||||
|
||||
fn temporal_scan(&mut self, is_compound: bool) {
|
||||
let bw4 = self.b.bw4 as isize;
|
||||
let bh4 = self.b.bh4 as isize;
|
||||
let step_w4 = if bw4 >= 16 { 4 } else { 2 };
|
||||
let step_h4 = if bh4 >= 16 { 4 } else { 2 };
|
||||
let mut dr = 0;
|
||||
while dr < bh4.min(16) {
|
||||
let mut dc = 0;
|
||||
while dc < bw4.min(16) {
|
||||
self.add_tpl_ref_mv(dr, dc, is_compound);
|
||||
dc += step_w4;
|
||||
}
|
||||
dr += step_h4;
|
||||
}
|
||||
let allow_extension = (2..16).contains(&bh4) && (2..16).contains(&bw4);
|
||||
if allow_extension {
|
||||
let pos = [(bh4, -2), (bh4, bw4), (bh4 - 2, bw4)];
|
||||
for &(dr, dc) in &pos {
|
||||
let row = (self.b.mi_row as isize & 15) + dr;
|
||||
let col = (self.b.mi_col as isize & 15) + dc;
|
||||
if (0..16).contains(&row) && (0..16).contains(&col) {
|
||||
self.add_tpl_ref_mv(dr, dc, is_compound);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn add_tpl_ref_mv(&mut self, delta_row: isize, delta_col: isize, is_compound: bool) {
|
||||
let mv_row = (self.b.mi_row as isize + delta_row) | 1;
|
||||
let mv_col = (self.b.mi_col as isize + delta_col) | 1;
|
||||
if !self.inside(mv_row, mv_col) {
|
||||
return;
|
||||
}
|
||||
let x8 = (mv_col >> 1) as usize;
|
||||
let y8 = (mv_row >> 1) as usize;
|
||||
let w8 = self.fs.fh.mi_cols as usize >> 1;
|
||||
if delta_row == 0 && delta_col == 0 {
|
||||
self.mvs.zero_mv_context = 1;
|
||||
}
|
||||
let rf0 = self.b.ref_frame[0] as usize;
|
||||
if !is_compound {
|
||||
let mut cand = self.fs.motion_field[rf0][y8 * w8 + x8];
|
||||
if cand[0] == INVALID_MV {
|
||||
return;
|
||||
}
|
||||
self.lower_mv_precision(&mut cand);
|
||||
if delta_row == 0 && delta_col == 0 {
|
||||
let g = self.mvs.global_mvs[0];
|
||||
self.mvs.zero_mv_context = ((cand[0] - g[0]).abs() >= 16 || (cand[1] - g[1]).abs() >= 16) as usize;
|
||||
}
|
||||
let n = self.mvs.num_mv_found;
|
||||
let idx = (0..n).find(|&i| self.mvs.ref_stack_mv[i][0] == cand);
|
||||
if let Some(i) = idx {
|
||||
self.mvs.weight_stack[i] += 2;
|
||||
} else if n < MAX_REF_MV_STACK_SIZE {
|
||||
self.mvs.ref_stack_mv[n][0] = cand;
|
||||
self.mvs.weight_stack[n] = 2;
|
||||
self.mvs.num_mv_found += 1;
|
||||
}
|
||||
} else {
|
||||
let rf1 = self.b.ref_frame[1] as usize;
|
||||
let mut c0 = self.fs.motion_field[rf0][y8 * w8 + x8];
|
||||
if c0[0] == INVALID_MV {
|
||||
return;
|
||||
}
|
||||
let mut c1 = self.fs.motion_field[rf1][y8 * w8 + x8];
|
||||
if c1[0] == INVALID_MV {
|
||||
return;
|
||||
}
|
||||
self.lower_mv_precision(&mut c0);
|
||||
self.lower_mv_precision(&mut c1);
|
||||
if delta_row == 0 && delta_col == 0 {
|
||||
let g = self.mvs.global_mvs;
|
||||
self.mvs.zero_mv_context = ((c0[0] - g[0][0]).abs() >= 16
|
||||
|| (c0[1] - g[0][1]).abs() >= 16
|
||||
|| (c1[0] - g[1][0]).abs() >= 16
|
||||
|| (c1[1] - g[1][1]).abs() >= 16) as usize;
|
||||
}
|
||||
let n = self.mvs.num_mv_found;
|
||||
let idx = (0..n).find(|&i| self.mvs.ref_stack_mv[i][0] == c0 && self.mvs.ref_stack_mv[i][1] == c1);
|
||||
if let Some(i) = idx {
|
||||
self.mvs.weight_stack[i] += 2;
|
||||
} else if n < MAX_REF_MV_STACK_SIZE {
|
||||
self.mvs.ref_stack_mv[n] = [c0, c1];
|
||||
self.mvs.weight_stack[n] = 2;
|
||||
self.mvs.num_mv_found += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn add_ref_mv_candidate(&mut self, mv_row: usize, mv_col: usize, is_compound: bool, weight: u32) {
|
||||
let mi = &self.t.mi;
|
||||
let i = mi.idx(mv_row, mv_col);
|
||||
if !mi.is_inter[i] {
|
||||
return;
|
||||
}
|
||||
let rf = mi.ref_frame[i];
|
||||
if !is_compound {
|
||||
for cand_list in 0..2 {
|
||||
if rf[cand_list] == self.b.ref_frame[0] {
|
||||
self.search_stack(mv_row, mv_col, cand_list, weight);
|
||||
}
|
||||
}
|
||||
} else if rf[0] == self.b.ref_frame[0] && rf[1] == self.b.ref_frame[1] {
|
||||
self.compound_search_stack(mv_row, mv_col, weight);
|
||||
}
|
||||
}
|
||||
|
||||
fn search_stack(&mut self, mv_row: usize, mv_col: usize, cand_list: usize, weight: u32) {
|
||||
let mi = &self.t.mi;
|
||||
let i = mi.idx(mv_row, mv_col);
|
||||
let cand_mode = mi.y_mode[i] as usize;
|
||||
let cand_size = mi.mi_size[i] as usize;
|
||||
let large = block_width(cand_size).min(block_height(cand_size)) >= 8;
|
||||
let gm0 = self.fs.fh.gm_type[self.b.ref_frame[0] as usize] as usize;
|
||||
let mut cand = if (cand_mode == GLOBALMV || cand_mode == GLOBAL_GLOBALMV) && gm0 > TRANSLATION && large {
|
||||
self.mvs.global_mvs[0]
|
||||
} else {
|
||||
let m = mi.mv[i][cand_list];
|
||||
[m.row as i32, m.col as i32]
|
||||
};
|
||||
self.lower_mv_precision(&mut cand);
|
||||
if has_newmv(cand_mode) {
|
||||
self.mvs.new_mv_count += 1;
|
||||
}
|
||||
self.mvs.found_match = true;
|
||||
let n = self.mvs.num_mv_found;
|
||||
if let Some(idx) = (0..n).find(|&k| self.mvs.ref_stack_mv[k][0] == cand) {
|
||||
self.mvs.weight_stack[idx] += weight;
|
||||
} else if n < MAX_REF_MV_STACK_SIZE {
|
||||
self.mvs.ref_stack_mv[n][0] = cand;
|
||||
self.mvs.weight_stack[n] = weight;
|
||||
self.mvs.num_mv_found += 1;
|
||||
}
|
||||
}
|
||||
|
||||
fn compound_search_stack(&mut self, mv_row: usize, mv_col: usize, weight: u32) {
|
||||
let mi = &self.t.mi;
|
||||
let i = mi.idx(mv_row, mv_col);
|
||||
let mut cand = [[mi.mv[i][0].row as i32, mi.mv[i][0].col as i32], [mi.mv[i][1].row as i32, mi.mv[i][1].col as i32]];
|
||||
let cand_mode = mi.y_mode[i] as usize;
|
||||
if cand_mode == GLOBAL_GLOBALMV {
|
||||
for (rl, c) in cand.iter_mut().enumerate() {
|
||||
if self.fs.fh.gm_type[self.b.ref_frame[rl] as usize] as usize > TRANSLATION {
|
||||
*c = self.mvs.global_mvs[rl];
|
||||
}
|
||||
}
|
||||
}
|
||||
for c in cand.iter_mut() {
|
||||
self.lower_mv_precision(c);
|
||||
}
|
||||
self.mvs.found_match = true;
|
||||
let n = self.mvs.num_mv_found;
|
||||
if let Some(idx) = (0..n).find(|&k| self.mvs.ref_stack_mv[k][0] == cand[0] && self.mvs.ref_stack_mv[k][1] == cand[1]) {
|
||||
self.mvs.weight_stack[idx] += weight;
|
||||
} else if n < MAX_REF_MV_STACK_SIZE {
|
||||
self.mvs.ref_stack_mv[n] = cand;
|
||||
self.mvs.weight_stack[n] = weight;
|
||||
self.mvs.num_mv_found += 1;
|
||||
}
|
||||
if has_newmv(cand_mode) {
|
||||
self.mvs.new_mv_count += 1;
|
||||
}
|
||||
}
|
||||
|
||||
fn sort_stack(&mut self, start: usize, end: usize, is_compound: bool) {
|
||||
let mut end = end;
|
||||
let s = &mut self.mvs;
|
||||
while end > start {
|
||||
let mut new_end = start;
|
||||
for idx in start + 1..end {
|
||||
if s.weight_stack[idx - 1] < s.weight_stack[idx] {
|
||||
s.weight_stack.swap(idx - 1, idx);
|
||||
for list in 0..1 + is_compound as usize {
|
||||
let t = s.ref_stack_mv[idx - 1][list];
|
||||
s.ref_stack_mv[idx - 1][list] = s.ref_stack_mv[idx][list];
|
||||
s.ref_stack_mv[idx][list] = t;
|
||||
}
|
||||
new_end = idx;
|
||||
}
|
||||
}
|
||||
end = new_end;
|
||||
}
|
||||
}
|
||||
|
||||
fn extra_search(&mut self, is_compound: bool) {
|
||||
self.mvs.ref_id_count = [0; 2];
|
||||
self.mvs.ref_diff_count = [0; 2];
|
||||
let (mi_row, mi_col) = (self.b.mi_row as isize, self.b.mi_col as isize);
|
||||
let mut w4 = (self.b.bw4 as isize).min(16);
|
||||
let mut h4 = (self.b.bh4 as isize).min(16);
|
||||
w4 = w4.min(self.fs.fh.mi_cols as isize - mi_col);
|
||||
h4 = h4.min(self.fs.fh.mi_rows as isize - mi_row);
|
||||
let num4x4 = w4.min(h4);
|
||||
for pass in 0..2 {
|
||||
let mut idx = 0;
|
||||
while idx < num4x4 && self.mvs.num_mv_found < 2 {
|
||||
let (mv_row, mv_col) = if pass == 0 { (mi_row - 1, mi_col + idx) } else { (mi_row + idx, mi_col - 1) };
|
||||
if !self.inside(mv_row, mv_col) {
|
||||
break;
|
||||
}
|
||||
self.add_extra_mv_candidate(mv_row as usize, mv_col as usize, is_compound);
|
||||
let mi = &self.t.mi;
|
||||
let sz = mi.mi_size[mi.idx(mv_row as usize, mv_col as usize)] as usize;
|
||||
idx += if pass == 0 { NUM_4X4_BLOCKS_WIDE[sz] } else { NUM_4X4_BLOCKS_HIGH[sz] } as isize;
|
||||
}
|
||||
}
|
||||
if is_compound {
|
||||
let mut combined = [[[0i32; 2]; 2]; 2];
|
||||
for list in 0..2 {
|
||||
let mut comp_count = 0;
|
||||
for idx in 0..self.mvs.ref_id_count[list] {
|
||||
combined[comp_count][list] = self.mvs.ref_id_mvs[list][idx];
|
||||
comp_count += 1;
|
||||
}
|
||||
let mut idx = 0;
|
||||
while idx < self.mvs.ref_diff_count[list] && comp_count < 2 {
|
||||
combined[comp_count][list] = self.mvs.ref_diff_mvs[list][idx];
|
||||
comp_count += 1;
|
||||
idx += 1;
|
||||
}
|
||||
while comp_count < 2 {
|
||||
combined[comp_count][list] = self.mvs.global_mvs[list];
|
||||
comp_count += 1;
|
||||
}
|
||||
}
|
||||
let n = self.mvs.num_mv_found;
|
||||
if n == 1 {
|
||||
if combined[0][0] == self.mvs.ref_stack_mv[0][0] && combined[0][1] == self.mvs.ref_stack_mv[0][1] {
|
||||
self.mvs.ref_stack_mv[n] = combined[1];
|
||||
} else {
|
||||
self.mvs.ref_stack_mv[n] = combined[0];
|
||||
}
|
||||
self.mvs.weight_stack[n] = 2;
|
||||
self.mvs.num_mv_found += 1;
|
||||
} else {
|
||||
for c in combined {
|
||||
let n = self.mvs.num_mv_found;
|
||||
self.mvs.ref_stack_mv[n] = c;
|
||||
self.mvs.weight_stack[n] = 2;
|
||||
self.mvs.num_mv_found += 1;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
for idx in self.mvs.num_mv_found..2 {
|
||||
self.mvs.ref_stack_mv[idx][0] = self.mvs.global_mvs[0];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn add_extra_mv_candidate(&mut self, mv_row: usize, mv_col: usize, is_compound: bool) {
|
||||
let mi = &self.t.mi;
|
||||
let i = mi.idx(mv_row, mv_col);
|
||||
let rfs = mi.ref_frame[i];
|
||||
let mvs = mi.mv[i];
|
||||
let bias = &self.fs.fh.ref_frame_sign_bias;
|
||||
if is_compound {
|
||||
for cand_list in 0..2 {
|
||||
let cand_ref = rfs[cand_list];
|
||||
if cand_ref > INTRA_FRAME as i8 {
|
||||
for list in 0..2 {
|
||||
let mut cand = [mvs[cand_list].row as i32, mvs[cand_list].col as i32];
|
||||
if cand_ref == self.b.ref_frame[list] && self.mvs.ref_id_count[list] < 2 {
|
||||
let k = self.mvs.ref_id_count[list];
|
||||
self.mvs.ref_id_mvs[list][k] = cand;
|
||||
self.mvs.ref_id_count[list] += 1;
|
||||
} else if self.mvs.ref_diff_count[list] < 2 {
|
||||
if bias[cand_ref as usize] != bias[self.b.ref_frame[list] as usize] {
|
||||
cand = [-cand[0], -cand[1]];
|
||||
}
|
||||
let k = self.mvs.ref_diff_count[list];
|
||||
self.mvs.ref_diff_mvs[list][k] = cand;
|
||||
self.mvs.ref_diff_count[list] += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
for cand_list in 0..2 {
|
||||
let cand_ref = rfs[cand_list];
|
||||
if cand_ref > INTRA_FRAME as i8 {
|
||||
let mut cand = [mvs[cand_list].row as i32, mvs[cand_list].col as i32];
|
||||
if bias[cand_ref as usize] != bias[self.b.ref_frame[0] as usize] {
|
||||
cand = [-cand[0], -cand[1]];
|
||||
}
|
||||
let n = self.mvs.num_mv_found;
|
||||
if !(0..n).any(|k| self.mvs.ref_stack_mv[k][0] == cand) {
|
||||
self.mvs.ref_stack_mv[n][0] = cand;
|
||||
self.mvs.weight_stack[n] = 2;
|
||||
self.mvs.num_mv_found += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn context_and_clamping(&mut self, is_compound: bool, num_new: usize) {
|
||||
let bw = block_width(self.b.mi_size);
|
||||
let bh = block_height(self.b.mi_size);
|
||||
let n = self.mvs.num_mv_found;
|
||||
for idx in 0..n {
|
||||
let mut z = 0;
|
||||
if idx + 1 < n {
|
||||
let w0 = self.mvs.weight_stack[idx];
|
||||
let w1 = self.mvs.weight_stack[idx + 1];
|
||||
if w0 >= REF_CAT_LEVEL as u32 {
|
||||
if w1 < REF_CAT_LEVEL as u32 {
|
||||
z = 1;
|
||||
}
|
||||
} else {
|
||||
z = 2;
|
||||
}
|
||||
}
|
||||
self.mvs.drl_ctx_stack[idx] = z;
|
||||
}
|
||||
for list in 0..1 + is_compound as usize {
|
||||
for idx in 0..n {
|
||||
let mut mv = self.mvs.ref_stack_mv[idx][list];
|
||||
mv[0] = self.clamp_mv_row(mv[0], MV_BORDER as i32 + bh * 8);
|
||||
mv[1] = self.clamp_mv_col(mv[1], MV_BORDER as i32 + bw * 8);
|
||||
self.mvs.ref_stack_mv[idx][list] = mv;
|
||||
}
|
||||
}
|
||||
let (close, total) = (self.mvs.close_matches, self.mvs.total_matches);
|
||||
if close == 0 {
|
||||
self.mvs.new_mv_context = total.min(1);
|
||||
self.mvs.ref_mv_context = total;
|
||||
} else if close == 1 {
|
||||
self.mvs.new_mv_context = 3 - num_new.min(1);
|
||||
self.mvs.ref_mv_context = 2 + total;
|
||||
} else {
|
||||
self.mvs.new_mv_context = 5 - num_new.min(1);
|
||||
self.mvs.ref_mv_context = 5;
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn clamp_mv_row(&self, mvec: i32, border: i32) -> i32 {
|
||||
let bh4 = self.b.bh4 as i32;
|
||||
let to_top = -((self.b.mi_row as i32 * 4) * 8);
|
||||
let to_bottom = ((self.fs.fh.mi_rows as i32 - bh4 - self.b.mi_row as i32) * 4) * 8;
|
||||
mvec.clamp(to_top - border, to_bottom + border)
|
||||
}
|
||||
|
||||
pub(crate) fn clamp_mv_col(&self, mvec: i32, border: i32) -> i32 {
|
||||
let bw4 = self.b.bw4 as i32;
|
||||
let to_left = -((self.b.mi_col as i32 * 4) * 8);
|
||||
let to_right = ((self.fs.fh.mi_cols as i32 - bw4 - self.b.mi_col as i32) * 4) * 8;
|
||||
mvec.clamp(to_left - border, to_right + border)
|
||||
}
|
||||
|
||||
/// has_overlappable_candidates( ) (7.10.3)
|
||||
pub(crate) fn has_overlappable_candidates(&self) -> bool {
|
||||
let mi = &self.t.mi;
|
||||
let (mi_rows, mi_cols) = (self.fs.fh.mi_rows as usize, self.fs.fh.mi_cols as usize);
|
||||
if self.b.avail_u {
|
||||
let mut x4 = self.b.mi_col;
|
||||
while x4 < mi_cols.min(self.b.mi_col + self.b.bw4) {
|
||||
let c = (x4 | 1).min(mi_cols - 1);
|
||||
if mi.ref_frame[mi.idx(self.b.mi_row - 1, c)][0] > INTRA_FRAME as i8 {
|
||||
return true;
|
||||
}
|
||||
x4 += 2;
|
||||
}
|
||||
}
|
||||
if self.b.avail_l {
|
||||
let mut y4 = self.b.mi_row;
|
||||
while y4 < mi_rows.min(self.b.mi_row + self.b.bh4) {
|
||||
let r = (y4 | 1).min(mi_rows - 1);
|
||||
if mi.ref_frame[mi.idx(r, self.b.mi_col - 1)][0] > INTRA_FRAME as i8 {
|
||||
return true;
|
||||
}
|
||||
y4 += 2;
|
||||
}
|
||||
}
|
||||
false
|
||||
}
|
||||
|
||||
/// Find warp samples process (7.10.4).
|
||||
pub(crate) fn find_warp_samples(&mut self) {
|
||||
self.mvs.num_samples = 0;
|
||||
self.mvs.num_samples_scanned = 0;
|
||||
let w4 = self.b.bw4 as isize;
|
||||
let h4 = self.b.bh4 as isize;
|
||||
let (mi_row, mi_col) = (self.b.mi_row as isize, self.b.mi_col as isize);
|
||||
let (mi_rows, mi_cols) = (self.fs.fh.mi_rows as isize, self.fs.fh.mi_cols as isize);
|
||||
let mut do_top_left = true;
|
||||
let mut do_top_right = true;
|
||||
if self.b.avail_u {
|
||||
let mi = &self.t.mi;
|
||||
let src_w = NUM_4X4_BLOCKS_WIDE[mi.mi_size[mi.idx(mi_row as usize - 1, mi_col as usize)] as usize] as isize;
|
||||
if w4 <= src_w {
|
||||
let col_offset = -(mi_col & (src_w - 1));
|
||||
if col_offset < 0 {
|
||||
do_top_left = false;
|
||||
}
|
||||
if col_offset + src_w > w4 {
|
||||
do_top_right = false;
|
||||
}
|
||||
self.add_sample(-1, 0);
|
||||
} else {
|
||||
let mut i = 0;
|
||||
while i < w4.min(mi_cols - mi_col) {
|
||||
let mi = &self.t.mi;
|
||||
let src_w = NUM_4X4_BLOCKS_WIDE[mi.mi_size[mi.idx(mi_row as usize - 1, (mi_col + i) as usize)] as usize] as isize;
|
||||
let step = w4.min(src_w);
|
||||
self.add_sample(-1, i);
|
||||
i += step;
|
||||
}
|
||||
}
|
||||
}
|
||||
if self.b.avail_l {
|
||||
let mi = &self.t.mi;
|
||||
let src_h = NUM_4X4_BLOCKS_HIGH[mi.mi_size[mi.idx(mi_row as usize, mi_col as usize - 1)] as usize] as isize;
|
||||
if h4 <= src_h {
|
||||
let row_offset = -(mi_row & (src_h - 1));
|
||||
if row_offset < 0 {
|
||||
do_top_left = false;
|
||||
}
|
||||
self.add_sample(0, -1);
|
||||
} else {
|
||||
let mut i = 0;
|
||||
while i < h4.min(mi_rows - mi_row) {
|
||||
let mi = &self.t.mi;
|
||||
let src_h = NUM_4X4_BLOCKS_HIGH[mi.mi_size[mi.idx((mi_row + i) as usize, mi_col as usize - 1)] as usize] as isize;
|
||||
let step = h4.min(src_h);
|
||||
self.add_sample(i, -1);
|
||||
i += step;
|
||||
}
|
||||
}
|
||||
}
|
||||
if do_top_left {
|
||||
self.add_sample(-1, -1);
|
||||
}
|
||||
if do_top_right && w4.max(h4) <= 16 {
|
||||
self.add_sample(-1, w4);
|
||||
}
|
||||
if self.mvs.num_samples == 0 && self.mvs.num_samples_scanned > 0 {
|
||||
self.mvs.num_samples = 1;
|
||||
}
|
||||
}
|
||||
|
||||
fn add_sample(&mut self, delta_row: isize, delta_col: isize) {
|
||||
if self.mvs.num_samples_scanned >= LEAST_SQUARES_SAMPLES_MAX {
|
||||
return;
|
||||
}
|
||||
let mv_row = self.b.mi_row as isize + delta_row;
|
||||
let mv_col = self.b.mi_col as isize + delta_col;
|
||||
if !self.inside(mv_row, mv_col) {
|
||||
return;
|
||||
}
|
||||
let mi = &self.t.mi;
|
||||
let i = mi.idx(mv_row as usize, mv_col as usize);
|
||||
if !mi.written[i] {
|
||||
return;
|
||||
}
|
||||
if mi.ref_frame[i][0] != self.b.ref_frame[0] || mi.ref_frame[i][1] != -1 {
|
||||
return;
|
||||
}
|
||||
let cand_sz = mi.mi_size[i] as usize;
|
||||
let cand_w4 = NUM_4X4_BLOCKS_WIDE[cand_sz] as isize;
|
||||
let cand_h4 = NUM_4X4_BLOCKS_HIGH[cand_sz] as isize;
|
||||
let cand_row = mv_row & !(cand_h4 - 1);
|
||||
let cand_col = mv_col & !(cand_w4 - 1);
|
||||
let mid_y = cand_row * 4 + cand_h4 * 2 - 1;
|
||||
let mid_x = cand_col * 4 + cand_w4 * 2 - 1;
|
||||
let threshold = block_width(self.b.mi_size).max(block_height(self.b.mi_size)).clamp(16, 112);
|
||||
let cm = mi.mv[mi.idx(cand_row as usize, cand_col as usize)][0];
|
||||
let mv_diff_row = (cm.row as i32 - self.b.mv_i[0][0]).abs();
|
||||
let mv_diff_col = (cm.col as i32 - self.b.mv_i[0][1]).abs();
|
||||
let valid = mv_diff_row + mv_diff_col <= threshold;
|
||||
let cand = [mid_y as i32 * 8, mid_x as i32 * 8, mid_y as i32 * 8 + cm.row as i32, mid_x as i32 * 8 + cm.col as i32];
|
||||
self.mvs.num_samples_scanned += 1;
|
||||
if !valid && self.mvs.num_samples_scanned > 1 {
|
||||
return;
|
||||
}
|
||||
self.mvs.cand_list[self.mvs.num_samples] = cand;
|
||||
if valid {
|
||||
self.mvs.num_samples += 1;
|
||||
}
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub(crate) fn inside(&self, r: isize, c: isize) -> bool {
|
||||
c >= self.mi_col_start as isize && c < self.mi_col_end as isize && r >= self.mi_row_start as isize && r < self.mi_row_end as isize
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
//! Optional data parallelism (rayon behind the `threads` feature; sequential otherwise and on
|
||||
//! wasm32).
|
||||
|
||||
#[cfg(feature = "threads")]
|
||||
use std::sync::Arc;
|
||||
|
||||
/// A handle to the decoder's worker pool (or none: run on the calling thread).
|
||||
#[derive(Clone, Default)]
|
||||
pub(crate) struct Pool {
|
||||
#[cfg(feature = "threads")]
|
||||
pool: Option<Arc<rayon::ThreadPool>>,
|
||||
}
|
||||
|
||||
impl Pool {
|
||||
/// A pool of `threads` workers (sequential when `threads` <= 1, without the `threads`
|
||||
/// feature, or on wasm32).
|
||||
pub fn new(threads: usize) -> Pool {
|
||||
#[cfg(feature = "threads")]
|
||||
{
|
||||
let pool = if threads > 1 && cfg!(not(target_arch = "wasm32")) {
|
||||
rayon::ThreadPoolBuilder::new().num_threads(threads).thread_name(|i| format!("av1-{i}")).build().ok().map(Arc::new)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
Pool { pool }
|
||||
}
|
||||
#[cfg(not(feature = "threads"))]
|
||||
{
|
||||
let _ = threads;
|
||||
Pool {}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn is_parallel(&self) -> bool {
|
||||
#[cfg(feature = "threads")]
|
||||
{
|
||||
self.pool.is_some()
|
||||
}
|
||||
#[cfg(not(feature = "threads"))]
|
||||
{
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
/// `f(i, &mut items[i])` for every item, in parallel when possible; results in order.
|
||||
pub fn map_mut<T: Send, R: Send>(&self, items: &mut [T], f: impl Fn(usize, &mut T) -> R + Sync + Send) -> Vec<R> {
|
||||
#[cfg(feature = "threads")]
|
||||
if let Some(pool) = &self.pool
|
||||
&& items.len() > 1
|
||||
{
|
||||
use rayon::prelude::*;
|
||||
return pool.install(|| items.par_iter_mut().enumerate().map(|(i, t)| f(i, t)).collect());
|
||||
}
|
||||
items.iter_mut().enumerate().map(|(i, t)| f(i, t)).collect()
|
||||
}
|
||||
|
||||
/// `f(i, chunk)` for the consecutive `chunk_len`-element chunks of `data`.
|
||||
pub fn chunks<T: Send>(&self, data: &mut [T], chunk_len: usize, f: impl Fn(usize, &mut [T]) + Sync + Send) {
|
||||
let chunk_len = chunk_len.max(1);
|
||||
#[cfg(feature = "threads")]
|
||||
if let Some(pool) = &self.pool
|
||||
&& data.len() > chunk_len
|
||||
{
|
||||
use rayon::prelude::*;
|
||||
pool.install(|| data.par_chunks_mut(chunk_len).enumerate().for_each(|(i, c)| f(i, c)));
|
||||
return;
|
||||
}
|
||||
for (i, c) in data.chunks_mut(chunk_len).enumerate() {
|
||||
f(i, c);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,269 @@
|
||||
//! In-loop post filters run by decode_frame_wrapup: loop filter (7.14), CDEF (7.15),
|
||||
//! super-resolution upscaling (7.16) and loop restoration (7.17).
|
||||
|
||||
use crate::spec_tables::{
|
||||
GLOBAL_GLOBALMV, GLOBALMV, INTRA_FRAME, MAX_LOOP_FILTER, NEARESTMV, NUM_4X4_BLOCKS_HIGH, NUM_4X4_BLOCKS_WIDE, SEG_LVL_ALT_LF_Y_V, TX_HEIGHT,
|
||||
TX_WIDTH,
|
||||
};
|
||||
use crate::state::FrameState;
|
||||
use crate::stats::{DecodeStats, Stage, Timer};
|
||||
|
||||
/// The post-filters run on the frame worker; `_pool` is for row-parallel filtering (not yet used).
|
||||
/// `draft`: only super-resolution (which sets the output size) runs.
|
||||
pub(crate) fn apply(fs: &mut FrameState, stats: &mut DecodeStats, _pool: &crate::par::Pool, draft: bool) {
|
||||
let mut t = Timer::start();
|
||||
let lvl = fs.fh.lf.level;
|
||||
if (lvl[0] != 0 || lvl[1] != 0) && !draft {
|
||||
loop_filter(fs);
|
||||
}
|
||||
stats.add(Stage::LoopFilter, t.lap());
|
||||
let cdef = if draft { None } else { crate::cdef::apply(fs) };
|
||||
stats.add(Stage::Cdef, t.lap());
|
||||
let (up_cur, up_cdef) = if fs.fh.use_superres {
|
||||
let c = crate::restoration::upscale(fs, &fs.cur);
|
||||
let d = cdef.as_ref().map(|f| crate::restoration::upscale(fs, f));
|
||||
(c, d)
|
||||
} else {
|
||||
(std::mem::take(&mut fs.cur), cdef)
|
||||
};
|
||||
stats.add(Stage::Superres, t.lap());
|
||||
fs.cur = if fs.fh.lr.uses_lr && !draft {
|
||||
crate::restoration::loop_restoration(fs, &up_cur, up_cdef.as_ref().unwrap_or(&up_cur))
|
||||
} else {
|
||||
up_cdef.unwrap_or(up_cur)
|
||||
};
|
||||
stats.add(Stage::Restoration, t.lap());
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------------------------
|
||||
// Loop filter (7.14)
|
||||
|
||||
fn loop_filter(fs: &mut FrameState) {
|
||||
let (mi_rows, mi_cols) = (fs.fh.mi_rows as usize, fs.fh.mi_cols as usize);
|
||||
for plane in 0..fs.num_planes {
|
||||
if plane == 0 || fs.fh.lf.level[1 + plane] != 0 {
|
||||
for pass in 0..2 {
|
||||
let row_step = if plane == 0 { 1 } else { 1 << fs.ssy };
|
||||
let col_step = if plane == 0 { 1 } else { 1 << fs.ssx };
|
||||
let mut row = 0;
|
||||
while row < mi_rows {
|
||||
let mut col = 0;
|
||||
while col < mi_cols {
|
||||
edge(fs, plane, pass, row, col);
|
||||
col += col_step;
|
||||
}
|
||||
row += row_step;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn edge(fs: &mut FrameState, plane: usize, pass: usize, row: usize, col: usize) {
|
||||
let (sub_x, sub_y) = if plane == 0 { (0, 0) } else { (fs.ssx, fs.ssy) };
|
||||
let (dx, dy) = if pass == 0 { (1i32, 0i32) } else { (0, 1) };
|
||||
let x = col * 4;
|
||||
let y = row * 4;
|
||||
let row = row | sub_y;
|
||||
let col = col | sub_x;
|
||||
let on_screen = !(x >= fs.fh.frame_width as usize || y >= fs.fh.frame_height as usize || (pass == 0 && x == 0) || (pass == 1 && y == 0));
|
||||
if !on_screen {
|
||||
return;
|
||||
}
|
||||
let xp = x >> sub_x;
|
||||
let yp = y >> sub_y;
|
||||
let prev_row = row - ((dy as usize) << sub_y);
|
||||
let prev_col = col - ((dx as usize) << sub_x);
|
||||
let tx_sz = fs.lf_tx_size(plane, row >> sub_y, col >> sub_x);
|
||||
let is_tx_edge = if pass == 0 { xp.is_multiple_of(TX_WIDTH[tx_sz] as usize) } else { yp.is_multiple_of(TX_HEIGHT[tx_sz] as usize) };
|
||||
if !is_tx_edge {
|
||||
return;
|
||||
}
|
||||
let mi = &fs.mi;
|
||||
let i = mi.idx(row, col);
|
||||
let mi_size = mi.mi_size[i] as usize;
|
||||
let plane_size = fs.plane_residual_size(mi_size, plane);
|
||||
let skip = mi.skip[i];
|
||||
let is_intra = mi.ref_frame[i][0] <= INTRA_FRAME as i8;
|
||||
let is_block_edge = if pass == 0 {
|
||||
xp.is_multiple_of(4 * NUM_4X4_BLOCKS_WIDE[plane_size] as usize)
|
||||
} else {
|
||||
yp.is_multiple_of(4 * NUM_4X4_BLOCKS_HIGH[plane_size] as usize)
|
||||
};
|
||||
if !(is_block_edge || !skip || is_intra) {
|
||||
return;
|
||||
}
|
||||
let prev_tx_sz = fs.lf_tx_size(plane, prev_row >> sub_y, prev_col >> sub_x);
|
||||
// filter size (7.14.3)
|
||||
let base_size = if pass == 0 { TX_WIDTH[prev_tx_sz].min(TX_WIDTH[tx_sz]) } else { TX_HEIGHT[prev_tx_sz].min(TX_HEIGHT[tx_sz]) } as usize;
|
||||
let filter_size = if plane == 0 { 16.min(base_size) } else { 8.min(base_size) };
|
||||
let (mut lvl, mut limit, mut blimit, mut thresh) = strength(fs, row, col, plane, pass);
|
||||
if lvl == 0 {
|
||||
(lvl, limit, blimit, thresh) = strength(fs, prev_row, prev_col, plane, pass);
|
||||
}
|
||||
if lvl == 0 {
|
||||
return;
|
||||
}
|
||||
let t = LfThresh::new(limit, blimit, thresh, fs.bit_depth);
|
||||
let pl = &mut fs.cur.planes[plane];
|
||||
let stride = pl.stride;
|
||||
let (step, along) = if pass == 0 { (1, stride) } else { (stride, 1) };
|
||||
let base = yp * stride + xp;
|
||||
let chroma = plane > 0;
|
||||
for k in 0..4 {
|
||||
sample_filter(&mut pl.data, base + k * along, step, chroma, filter_size, t);
|
||||
}
|
||||
}
|
||||
|
||||
/// Adaptive filter strength (7.14.4): (lvl, limit, blimit, thresh).
|
||||
fn strength(fs: &FrameState, row: usize, col: usize, plane: usize, pass: usize) -> (i32, i32, i32, i32) {
|
||||
let mi = &fs.mi;
|
||||
let i = mi.idx(row, col);
|
||||
let segment = mi.segment_id[i] as usize;
|
||||
let rf = mi.ref_frame[i][0];
|
||||
let mode = mi.y_mode[i] as usize;
|
||||
let mode_type = (mode >= NEARESTMV && mode != GLOBALMV && mode != GLOBAL_GLOBALMV) as usize;
|
||||
let delta_lf = if fs.fh.delta_lf_multi { mi.delta_lf[i][if plane == 0 { pass } else { plane + 1 }] } else { mi.delta_lf[i][0] } as i32;
|
||||
// 7.14.5
|
||||
let idx = if plane == 0 { pass } else { plane + 1 };
|
||||
let base = (delta_lf + fs.fh.lf.level[idx] as i32).clamp(0, MAX_LOOP_FILTER as i32);
|
||||
let mut lvl_seg = base;
|
||||
let feature = SEG_LVL_ALT_LF_Y_V + idx;
|
||||
if fs.fh.seg.enabled && fs.fh.seg.features.enabled[segment][feature] {
|
||||
lvl_seg = (fs.fh.seg.features.data[segment][feature] + lvl_seg).clamp(0, MAX_LOOP_FILTER as i32);
|
||||
}
|
||||
if fs.fh.lf.delta_enabled {
|
||||
let n_shift = lvl_seg >> 5;
|
||||
let d = &fs.fh.lf.deltas;
|
||||
if rf == INTRA_FRAME as i8 {
|
||||
lvl_seg += d.ref_deltas[INTRA_FRAME] << n_shift;
|
||||
} else if rf > INTRA_FRAME as i8 {
|
||||
lvl_seg += (d.ref_deltas[rf as usize] << n_shift) + (d.mode_deltas[mode_type] << n_shift);
|
||||
}
|
||||
lvl_seg = lvl_seg.clamp(0, MAX_LOOP_FILTER as i32);
|
||||
}
|
||||
let lvl = lvl_seg;
|
||||
let sharp = fs.fh.lf.sharpness as i32;
|
||||
let shift = if sharp > 4 {
|
||||
2
|
||||
} else if sharp > 0 {
|
||||
1
|
||||
} else {
|
||||
0
|
||||
};
|
||||
let limit = if sharp > 0 { (lvl >> shift).clamp(1, 9 - sharp) } else { (lvl >> shift).max(1) };
|
||||
let blimit = 2 * (lvl + 2) + limit;
|
||||
let thresh = lvl >> 4;
|
||||
(lvl, limit, blimit, thresh)
|
||||
}
|
||||
|
||||
/// Per-edge thresholds of the sample filter, scaled to the bit depth.
|
||||
#[derive(Clone, Copy)]
|
||||
struct LfThresh {
|
||||
limit: i32,
|
||||
blimit: i32,
|
||||
thresh: i32,
|
||||
/// Flatness threshold 1 << (BitDepth - 8).
|
||||
flat: i32,
|
||||
/// 0x80 << (BitDepth - 8) and 1 << (BitDepth - 1).
|
||||
off: i32,
|
||||
half: i32,
|
||||
}
|
||||
|
||||
impl LfThresh {
|
||||
fn new(limit: i32, blimit: i32, thresh: i32, bd: u32) -> LfThresh {
|
||||
let sh = bd - 8;
|
||||
LfThresh { limit: limit << sh, blimit: blimit << sh, thresh: thresh << sh, flat: 1 << sh, off: 0x80 << sh, half: 1 << (bd - 1) }
|
||||
}
|
||||
}
|
||||
|
||||
/// Sample filtering process (7.14.6) for one line across the edge: `base` indexes q0 and
|
||||
/// `step` is the distance between samples across the edge.
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
#[inline(always)]
|
||||
fn sample_filter(d: &mut [u16], base: usize, step: usize, chroma: bool, filter_size: usize, t: LfThresh) {
|
||||
let at = |d: &[u16], k: isize| -> i32 { d[(base as isize + k * step as isize) as usize] as i32 };
|
||||
let (q0, q1, p0, p1) = (at(d, 0), at(d, 1), at(d, -1), at(d, -2));
|
||||
let lim = t.limit;
|
||||
// filter mask (7.14.6.2)
|
||||
if (p1 - p0).abs() > lim || (q1 - q0).abs() > lim || (p0 - q0).abs() * 2 + (p1 - q1).abs() / 2 > t.blimit {
|
||||
return;
|
||||
}
|
||||
let hev = (p1 - p0).abs() > t.thresh || (q1 - q0).abs() > t.thresh;
|
||||
let mut flat = false;
|
||||
if filter_size > 4 {
|
||||
let (q2, p2) = (at(d, 2), at(d, -3));
|
||||
if (p2 - p1).abs() > lim || (q2 - q1).abs() > lim {
|
||||
return;
|
||||
}
|
||||
let tf = t.flat;
|
||||
flat = (p1 - p0).abs() <= tf && (q1 - q0).abs() <= tf && (p2 - p0).abs() <= tf && (q2 - q0).abs() <= tf;
|
||||
if !chroma {
|
||||
// filterLen >= 8
|
||||
let (q3, p3) = (at(d, 3), at(d, -4));
|
||||
if (p3 - p2).abs() > lim || (q3 - q2).abs() > lim {
|
||||
return;
|
||||
}
|
||||
flat = flat && (p3 - p0).abs() <= tf && (q3 - q0).abs() <= tf;
|
||||
}
|
||||
}
|
||||
if !flat {
|
||||
// narrow filter (7.14.6.3)
|
||||
let c = |v: i32| v.clamp(-t.half, t.half - 1);
|
||||
let off = t.off;
|
||||
let (ps1, ps0, qs0, qs1) = (p1 - off, p0 - off, q0 - off, q1 - off);
|
||||
let mut filter = if hev { c(ps1 - qs1) } else { 0 };
|
||||
filter = c(filter + 3 * (qs0 - ps0));
|
||||
let filter1 = c(filter + 4) >> 3;
|
||||
let filter2 = c(filter + 3) >> 3;
|
||||
d[base] = (c(qs0 - filter1) + off) as u16;
|
||||
d[base - step] = (c(ps0 + filter2) + off) as u16;
|
||||
if !hev {
|
||||
let f = (filter1 + 1) >> 1;
|
||||
d[base + step] = (c(qs1 - f) + off) as u16;
|
||||
d[base - 2 * step] = (c(ps1 + f) + off) as u16;
|
||||
}
|
||||
return;
|
||||
}
|
||||
if chroma {
|
||||
wide::<2, 1, 3>(d, base, step);
|
||||
} else if filter_size == 8 {
|
||||
wide::<3, 0, 3>(d, base, step);
|
||||
} else {
|
||||
let tf = t.flat;
|
||||
let q0 = at(d, 0);
|
||||
let p0 = at(d, -1);
|
||||
let flat2 = (4..7).all(|k| (at(d, k) - q0).abs() <= tf && (at(d, -k - 1) - p0).abs() <= tf);
|
||||
if flat2 { wide::<6, 1, 4>(d, base, step) } else { wide::<3, 0, 3>(d, base, step) }
|
||||
}
|
||||
}
|
||||
|
||||
/// Wide filter process (7.14.6.4) with n = N, the 2-weight taps |j| <= N2 and log2Size = LOG2.
|
||||
#[inline(always)]
|
||||
fn wide<const N: usize, const N2: usize, const LOG2: u32>(d: &mut [u16], base: usize, step: usize) {
|
||||
// v[k] = sample at offset k - (N + 1) from q0, k in 0 .. 2N + 2
|
||||
let mut v = [0i32; 14];
|
||||
let first = base - (N + 1) * step;
|
||||
for (k, x) in v[..2 * N + 2].iter_mut().enumerate() {
|
||||
*x = d[first + k * step] as i32;
|
||||
}
|
||||
// e[m] = v[clamp(m - N, 0, 2N + 1)]: the clamped tap positions as one array, so
|
||||
// F[i] = sum_{jj = 0}^{2N} e[i + 1 + jj] * (2 if |jj - N| <= N2 else 1), a sliding window.
|
||||
let mut e = [0i32; 25];
|
||||
for (m, x) in e[..4 * N + 1].iter_mut().enumerate() {
|
||||
*x = v[m.saturating_sub(N).min(2 * N + 1)];
|
||||
}
|
||||
let mut f = [0i32; 12];
|
||||
let mut win: i32 = e[1..2 * N + 2].iter().sum();
|
||||
for i in 0..2 * N {
|
||||
if i > 0 {
|
||||
win += e[i + 1 + 2 * N] - e[i];
|
||||
}
|
||||
let centre: i32 = e[i + 1 + N - N2..=i + 1 + N + N2].iter().sum();
|
||||
f[i] = (win + centre + (1 << (LOG2 - 1))) >> LOG2;
|
||||
}
|
||||
let out = base - N * step;
|
||||
for i in 0..2 * N {
|
||||
d[out + i * step] = f[i] as u16;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,298 @@
|
||||
//! Super-resolution upscaling (7.16) and loop restoration (7.17).
|
||||
|
||||
use crate::frame::{FrameBuf, Plane};
|
||||
use crate::spec_tables::{
|
||||
FILTER_BITS, RESTORE_NONE, RESTORE_SGRPROJ, RESTORE_WIENER, SGR_PARAMS, SGRPROJ_MTABLE_BITS, SGRPROJ_PRJ_BITS, SGRPROJ_RECIP_BITS,
|
||||
SGRPROJ_RST_BITS, SGRPROJ_SGR_BITS, SUPERRES_EXTRA_BITS, SUPERRES_FILTER_OFFSET, SUPERRES_FILTER_TAPS, SUPERRES_SCALE_BITS, SUPERRES_SCALE_MASK,
|
||||
UPSCALE_FILTER,
|
||||
};
|
||||
use crate::state::FrameState;
|
||||
|
||||
/// Upscale a frame horizontally from FrameWidth to UpscaledWidth (7.16).
|
||||
pub(crate) fn upscale(fs: &FrameState, input: &FrameBuf) -> FrameBuf {
|
||||
let fh = &fs.fh;
|
||||
let up_w = fh.upscaled_width as usize;
|
||||
let mut out = FrameBuf::new(up_w, fh.frame_height as usize, fs.num_planes, fs.ssx, fs.ssy, fs.bit_depth as u8);
|
||||
let max = (1i32 << fs.bit_depth) - 1;
|
||||
for plane in 0..fs.num_planes {
|
||||
let (sub_x, sub_y) = if plane > 0 { (fs.ssx, fs.ssy) } else { (0, 0) };
|
||||
let down_w = ((fh.frame_width as usize) + sub_x) >> sub_x;
|
||||
let up_pw = (up_w + sub_x) >> sub_x;
|
||||
let plane_h = ((fh.frame_height as usize) + sub_y) >> sub_y;
|
||||
let sb = SUPERRES_SCALE_BITS as i64;
|
||||
let step_x = (((down_w as i64) << sb) + (up_pw as i64 / 2)) / up_pw as i64;
|
||||
let err = up_pw as i64 * step_x - ((down_w as i64) << sb);
|
||||
let mut initial = (-(((up_pw - down_w) as i64) << (sb - 1)) + up_pw as i64 / 2) / up_pw as i64 + (1 << (SUPERRES_EXTRA_BITS - 1)) - err / 2;
|
||||
initial &= SUPERRES_SCALE_MASK as i64;
|
||||
let mi_w = fh.mi_cols as usize >> sub_x;
|
||||
let max_x = (mi_w * 4 - 1) as i64;
|
||||
let src = &input.planes[plane];
|
||||
let dst = &mut out.planes[plane];
|
||||
for y in 0..plane_h {
|
||||
let row = src.row(y);
|
||||
for x in 0..up_pw {
|
||||
let src_x = -(1i64 << sb) + initial + x as i64 * step_x;
|
||||
let src_px = src_x >> sb;
|
||||
let subpel = ((src_x & SUPERRES_SCALE_MASK as i64) >> SUPERRES_EXTRA_BITS) as usize;
|
||||
let mut sum = 0i32;
|
||||
for k in 0..SUPERRES_FILTER_TAPS {
|
||||
let sx = (src_px + k as i64 - SUPERRES_FILTER_OFFSET as i64).clamp(0, max_x) as usize;
|
||||
sum += row[sx] as i32 * UPSCALE_FILTER[subpel][k] as i32;
|
||||
}
|
||||
dst.set(x, y, ((sum + 64) >> FILTER_BITS).clamp(0, max) as u16);
|
||||
}
|
||||
}
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
struct LrCtx<'a> {
|
||||
cur: &'a Plane,
|
||||
cdef: &'a Plane,
|
||||
plane_end_x: i32,
|
||||
plane_end_y: i32,
|
||||
stripe_start_y: i32,
|
||||
stripe_end_y: i32,
|
||||
}
|
||||
|
||||
impl LrCtx<'_> {
|
||||
/// Get source sample process (7.17.6).
|
||||
#[inline(always)]
|
||||
fn sample(&self, x: i32, y: i32) -> i32 {
|
||||
let x = x.min(self.plane_end_x).max(0) as usize;
|
||||
let y = y.min(self.plane_end_y).max(0);
|
||||
if y < self.stripe_start_y {
|
||||
let y = (self.stripe_start_y - 2).max(y) as usize;
|
||||
self.cur.at(x, y) as i32
|
||||
} else if y > self.stripe_end_y {
|
||||
let y = (self.stripe_end_y + 2).min(y) as usize;
|
||||
self.cur.at(x, y) as i32
|
||||
} else {
|
||||
self.cdef.at(x, y as usize) as i32
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Position and size of a (at most 4x4) restoration block within its plane.
|
||||
#[derive(Clone, Copy)]
|
||||
struct LrBlock {
|
||||
x: i32,
|
||||
y: i32,
|
||||
w: i32,
|
||||
h: i32,
|
||||
}
|
||||
|
||||
/// Loop restoration (7.17): returns LrFrame given UpscaledCurrFrame and UpscaledCdefFrame.
|
||||
pub(crate) fn loop_restoration(fs: &FrameState, cur: &FrameBuf, cdef: &FrameBuf) -> FrameBuf {
|
||||
let mut lr = cdef.clone();
|
||||
for plane in 0..fs.num_planes {
|
||||
if fs.fh.lr.frame_restoration_type[plane] == RESTORE_NONE as u8 {
|
||||
continue;
|
||||
}
|
||||
restore_plane(fs, cur, cdef, &mut lr.planes[plane], plane);
|
||||
}
|
||||
lr
|
||||
}
|
||||
|
||||
fn restore_plane(fs: &FrameState, cur: &FrameBuf, cdef: &FrameBuf, out: &mut Plane, plane: usize) {
|
||||
let fh = &fs.fh;
|
||||
let up_w = fh.upscaled_width as i32;
|
||||
let fr_h = fh.frame_height as i32;
|
||||
let bd = fs.bit_depth;
|
||||
let (sub_x, sub_y) = if plane > 0 { (fs.ssx as i32, fs.ssy as i32) } else { (0, 0) };
|
||||
let unit_size = fh.lr.loop_restoration_size[plane] as i32;
|
||||
let unit_rows = fs.lr_unit_rows[plane] as i32;
|
||||
let unit_cols = fs.lr_unit_cols[plane] as i32;
|
||||
let plane_end_x = ((up_w + sub_x) >> sub_x) - 1;
|
||||
let plane_end_y = ((fr_h + sub_y) >> sub_y) - 1;
|
||||
let mut row = 0;
|
||||
while row * 4 < fr_h {
|
||||
let luma_y = row * 4;
|
||||
let stripe_num = (luma_y + 8) / 64;
|
||||
let stripe_start_y = (-8 + stripe_num * 64) >> sub_y;
|
||||
let stripe_end_y = stripe_start_y + (64 >> sub_y) - 1;
|
||||
let ctx = LrCtx { cur: &cur.planes[plane], cdef: &cdef.planes[plane], plane_end_x, plane_end_y, stripe_start_y, stripe_end_y };
|
||||
let unit_row = (unit_rows - 1).min(((row * 4 + 8) >> sub_y) / unit_size);
|
||||
let mut col = 0;
|
||||
while col * 4 < up_w {
|
||||
let unit_col = (unit_cols - 1).min(((col * 4) >> sub_x) / unit_size);
|
||||
let x = (col * 4) >> sub_x;
|
||||
let y = (row * 4) >> sub_y;
|
||||
let w = (4 >> sub_x).min(plane_end_x - x + 1);
|
||||
let h = (4 >> sub_y).min(plane_end_y - y + 1);
|
||||
if w > 0 && h > 0 {
|
||||
let ui = (unit_row * unit_cols + unit_col) as usize;
|
||||
let blk = LrBlock { x, y, w, h };
|
||||
match fs.lr.lr_type[plane][ui] {
|
||||
t if t == RESTORE_WIENER as u8 => wiener(fs, &ctx, out, (plane, ui), blk, bd),
|
||||
t if t == RESTORE_SGRPROJ as u8 => self_guided(fs, &ctx, out, (plane, ui), blk, bd),
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
col += 1;
|
||||
}
|
||||
row += 1;
|
||||
}
|
||||
}
|
||||
|
||||
fn wiener(fs: &FrameState, ctx: &LrCtx, out: &mut Plane, (plane, ui): (usize, usize), blk: LrBlock, bd: u32) {
|
||||
let LrBlock { x, y, w, h } = blk;
|
||||
let round0: u32 = if bd == 12 { 5 } else { 3 };
|
||||
let round1: u32 = if bd == 12 { 9 } else { 11 };
|
||||
let coef = &fs.lr.lr_wiener[plane][ui];
|
||||
let mk = |c: &[i8; 3]| -> [i32; 7] {
|
||||
let mut f = [0i32; 7];
|
||||
f[3] = 128;
|
||||
for i in 0..3 {
|
||||
f[i] = c[i] as i32;
|
||||
f[6 - i] = c[i] as i32;
|
||||
f[3] -= 2 * c[i] as i32;
|
||||
}
|
||||
f
|
||||
};
|
||||
let vfilter = mk(&coef[0]);
|
||||
let hfilter = mk(&coef[1]);
|
||||
let fb = FILTER_BITS as u32;
|
||||
let offset = 1i32 << (bd + fb - round0 - 1);
|
||||
let limit = (1i32 << (bd + 1 + fb - round0)) - 1;
|
||||
let mut inter = [[0i32; 4]; 10];
|
||||
for r in 0..(h + 6) as usize {
|
||||
for c in 0..w as usize {
|
||||
let mut s = 0;
|
||||
for t in 0..7 {
|
||||
s += hfilter[t] * ctx.sample(x + c as i32 + t as i32 - 3, y + r as i32 - 3);
|
||||
}
|
||||
let v = (s + (1 << (round0 - 1))) >> round0;
|
||||
inter[r][c] = v.clamp(-offset, limit - offset);
|
||||
}
|
||||
}
|
||||
let max = (1i32 << bd) - 1;
|
||||
for r in 0..h as usize {
|
||||
for c in 0..w as usize {
|
||||
let mut s = 0;
|
||||
for t in 0..7 {
|
||||
s += vfilter[t] * inter[r + t][c];
|
||||
}
|
||||
let v = (s + (1 << (round1 - 1))) >> round1;
|
||||
out.set(x as usize + c, y as usize + r, v.clamp(0, max) as u16);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn self_guided(fs: &FrameState, ctx: &LrCtx, out: &mut Plane, (plane, ui): (usize, usize), blk: LrBlock, bd: u32) {
|
||||
let LrBlock { x, y, w, h } = blk;
|
||||
let set = fs.lr.lr_sgr_set[plane][ui] as usize;
|
||||
let mut flt0 = [[0i32; 4]; 4];
|
||||
let mut flt1 = [[0i32; 4]; 4];
|
||||
box_filter(ctx, blk, (set, 0), bd, &mut flt0);
|
||||
box_filter(ctx, blk, (set, 1), bd, &mut flt1);
|
||||
let xqd = fs.lr.lr_sgr_xqd[plane][ui];
|
||||
let w0 = xqd[0] as i32;
|
||||
let w1 = xqd[1] as i32;
|
||||
let w2 = (1 << SGRPROJ_PRJ_BITS) - w0 - w1;
|
||||
let r0 = SGR_PARAMS[set][0];
|
||||
let r1 = SGR_PARAMS[set][2];
|
||||
let max = (1i32 << bd) - 1;
|
||||
let sh = (SGRPROJ_RST_BITS + SGRPROJ_PRJ_BITS) as u32;
|
||||
for i in 0..h as usize {
|
||||
for j in 0..w as usize {
|
||||
let u = (ctx.cdef.at(x as usize + j, y as usize + i) as i32) << SGRPROJ_RST_BITS;
|
||||
let mut v = w1 * u;
|
||||
v += if r0 != 0 { w0 * flt0[i][j] } else { w0 * u };
|
||||
v += if r1 != 0 { w2 * flt1[i][j] } else { w2 * u };
|
||||
let s = (v + (1 << (sh - 1))) >> sh;
|
||||
out.set(x as usize + j, y as usize + i, s.clamp(0, max) as u16);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
fn round2_64(x: i64, n: u32) -> i64 {
|
||||
if n == 0 { x } else { (x + (1i64 << (n - 1))) >> n }
|
||||
}
|
||||
|
||||
/// Box sums of the samples within `r` of (x, y): (sum of squares, sum).
|
||||
fn box_sums(ctx: &LrCtx, x: i32, y: i32, r: i32) -> (i64, i64) {
|
||||
let mut a = 0i64;
|
||||
let mut b = 0i64;
|
||||
for dy in -r..=r {
|
||||
for dx in -r..=r {
|
||||
let c = ctx.sample(x + dx, y + dy) as i64;
|
||||
a += c * c;
|
||||
b += c;
|
||||
}
|
||||
}
|
||||
(a, b)
|
||||
}
|
||||
|
||||
/// Self-guided filter constants of one pass.
|
||||
struct SgrConsts {
|
||||
n: i64,
|
||||
s: i64,
|
||||
one_over_n: i64,
|
||||
}
|
||||
|
||||
/// (A, B) coefficients of one sample from its box sums.
|
||||
fn sgr_coefficients(a: i64, b: i64, k: &SgrConsts, bd: u32) -> (i64, i64) {
|
||||
let a = round2_64(a, 2 * (bd - 8));
|
||||
let d = round2_64(b, bd - 8);
|
||||
let p = (a * k.n - d * d).max(0);
|
||||
let z = round2_64(p * k.s, SGRPROJ_MTABLE_BITS as u32);
|
||||
let a2 = if z >= 255 {
|
||||
256
|
||||
} else if z == 0 {
|
||||
1
|
||||
} else {
|
||||
((z << SGRPROJ_SGR_BITS) + z / 2) / (z + 1)
|
||||
};
|
||||
let b2 = ((1i64 << SGRPROJ_SGR_BITS) - a2) * b * k.one_over_n;
|
||||
(a2, round2_64(b2, SGRPROJ_RECIP_BITS as u32))
|
||||
}
|
||||
|
||||
fn box_weight(pass: usize, i: i32, dx: i32, dy: i32) -> i64 {
|
||||
if pass == 0 {
|
||||
if ((i + dy) & 1) != 0 { if dx == 0 { 6 } else { 5 } } else { 0 }
|
||||
} else if dx == 0 || dy == 0 {
|
||||
4
|
||||
} else {
|
||||
3
|
||||
}
|
||||
}
|
||||
|
||||
fn box_filter(ctx: &LrCtx, blk: LrBlock, (set, pass): (usize, usize), bd: u32, f: &mut [[i32; 4]; 4]) {
|
||||
let LrBlock { x, y, w, h } = blk;
|
||||
let r = SGR_PARAMS[set][pass * 2] as i32;
|
||||
if r == 0 {
|
||||
return;
|
||||
}
|
||||
let eps = SGR_PARAMS[set][pass * 2 + 1] as i64;
|
||||
let n = ((2 * r + 1) * (2 * r + 1)) as i64;
|
||||
let n2e = n * n * eps;
|
||||
let k = SgrConsts { n, s: ((1i64 << SGRPROJ_MTABLE_BITS) + n2e / 2) / n2e, one_over_n: ((1i64 << SGRPROJ_RECIP_BITS) + n / 2) / n };
|
||||
let mut a_arr = [[0i64; 6]; 6];
|
||||
let mut b_arr = [[0i64; 6]; 6];
|
||||
for i in -1..h + 1 {
|
||||
for j in -1..w + 1 {
|
||||
let (a, b) = box_sums(ctx, x + j, y + i, r);
|
||||
let (a2, b2) = sgr_coefficients(a, b, &k, bd);
|
||||
a_arr[(i + 1) as usize][(j + 1) as usize] = a2;
|
||||
b_arr[(i + 1) as usize][(j + 1) as usize] = b2;
|
||||
}
|
||||
}
|
||||
for i in 0..h {
|
||||
let shift = if pass == 0 && (i & 1) == 1 { 4 } else { 5 };
|
||||
for j in 0..w {
|
||||
let mut a = 0i64;
|
||||
let mut b = 0i64;
|
||||
for dy in -1..=1i32 {
|
||||
for dx in -1..=1i32 {
|
||||
let weight = box_weight(pass, i, dx, dy);
|
||||
a += weight * a_arr[(i + dy + 1) as usize][(j + dx + 1) as usize];
|
||||
b += weight * b_arr[(i + dy + 1) as usize][(j + dx + 1) as usize];
|
||||
}
|
||||
}
|
||||
let v = a * ctx.cdef.at((x + j) as usize, (y + i) as usize) as i64 + b;
|
||||
f[i as usize][j as usize] = round2_64(v, (SGRPROJ_SGR_BITS + shift - SGRPROJ_RST_BITS) as u32) as i32;
|
||||
}
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,294 @@
|
||||
//! Per-frame decoding state shared by the tiles and the post-filters.
|
||||
//!
|
||||
//! [`FrameShared`] holds what tiles only read (headers, references, projected motion field);
|
||||
//! [`TileState`] what a tile writes (reconstruction, mode info, per-block filter parameters,
|
||||
//! entropy contexts). With one tile the frame's own buffers move into the tile state and back;
|
||||
//! with several, each tile decodes into private buffers covering its area (tiles never read
|
||||
//! outside themselves), which are then copied into the frame.
|
||||
|
||||
use crate::decoder::RefFrame;
|
||||
use crate::frame::{FrameBuf, MiInfo};
|
||||
use crate::header::{FrameHeader, RefInfo, SequenceHeader};
|
||||
use crate::spec_tables::{DEFAULT_INTRA_FRAME_Y_MODE_CDF, NUM_REF_FRAMES, RESTORE_NONE, SUBSAMPLED_SIZE};
|
||||
use std::ops::{Deref, DerefMut};
|
||||
use std::sync::Arc;
|
||||
|
||||
/// Read-only (during tile decoding) frame state.
|
||||
pub(crate) struct FrameShared {
|
||||
pub seq: SequenceHeader,
|
||||
pub fh: FrameHeader,
|
||||
pub num_planes: usize,
|
||||
pub ssx: usize,
|
||||
pub ssy: usize,
|
||||
pub bit_depth: u32,
|
||||
pub lr_unit_rows: [usize; 3],
|
||||
pub lr_unit_cols: [usize; 3],
|
||||
pub cdef_cols: usize,
|
||||
/// Reference slots (FrameStore and saved state) used by this frame.
|
||||
pub refs: [Option<Arc<RefFrame>>; NUM_REF_FRAMES],
|
||||
pub ref_info: [RefInfo; NUM_REF_FRAMES],
|
||||
/// MotionFieldMvs[ ref ][ y8 * w8 + x8 ] (7.9); empty when unused.
|
||||
pub motion_field: [Vec<[i32; 2]>; 8],
|
||||
/// PrevSegmentIds (mi units).
|
||||
pub prev_segment_ids: Vec<u8>,
|
||||
}
|
||||
|
||||
impl FrameShared {
|
||||
pub fn plane_residual_size(&self, subsize: usize, plane: usize) -> usize {
|
||||
let sx = if plane > 0 { self.ssx } else { 0 };
|
||||
let sy = if plane > 0 { self.ssy } else { 0 };
|
||||
SUBSAMPLED_SIZE[subsize][sx][sy] as usize
|
||||
}
|
||||
}
|
||||
|
||||
/// A byte per 4x4 unit of a plane (LoopfilterTxSizes), possibly for a region only.
|
||||
#[derive(Clone, Default)]
|
||||
pub(crate) struct Grid8 {
|
||||
pub data: Vec<u8>,
|
||||
pub ox: usize,
|
||||
pub oy: usize,
|
||||
pub stride: usize,
|
||||
pub rows: usize,
|
||||
}
|
||||
|
||||
impl Grid8 {
|
||||
fn new(ox: usize, oy: usize, stride: usize, rows: usize) -> Grid8 {
|
||||
Grid8 { data: vec![0; stride * rows], ox, oy, stride, rows }
|
||||
}
|
||||
#[inline]
|
||||
pub fn get(&self, row: usize, col: usize) -> u8 {
|
||||
self.data[(row - self.oy) * self.stride + col - self.ox]
|
||||
}
|
||||
/// Stores outside the grid are dropped (blocks reaching past the allocated margin).
|
||||
#[inline]
|
||||
pub fn set(&mut self, row: usize, col: usize, v: u8) {
|
||||
if col >= self.ox && col < self.ox + self.stride && row >= self.oy && row < self.oy + self.rows {
|
||||
self.data[(row - self.oy) * self.stride + col - self.ox] = v;
|
||||
}
|
||||
}
|
||||
fn copy_into(&self, dst: &mut Grid8) {
|
||||
for y in 0..self.rows {
|
||||
let d = (self.oy + y - dst.oy) * dst.stride + self.ox - dst.ox;
|
||||
dst.data[d..d + self.stride].copy_from_slice(&self.data[y * self.stride..(y + 1) * self.stride]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Loop restoration parameters per unit.
|
||||
#[derive(Clone, Default)]
|
||||
pub(crate) struct LrUnits {
|
||||
pub lr_type: [Vec<u8>; 3],
|
||||
pub lr_wiener: [Vec<[[i8; 3]; 2]>; 3],
|
||||
pub lr_sgr_set: [Vec<u8>; 3],
|
||||
pub lr_sgr_xqd: [Vec<[i8; 2]>; 3],
|
||||
}
|
||||
|
||||
/// What tile decoding writes.
|
||||
pub(crate) struct TileState {
|
||||
pub cur: FrameBuf,
|
||||
pub mi: MiInfo,
|
||||
/// cdef_idx per 64x64 block (-1: not coded).
|
||||
pub cdef: Vec<i8>,
|
||||
/// LoopfilterTxSizes[ plane ][ row ][ col ] in 4x4 units of each plane.
|
||||
pub lf_tx_size: [Grid8; 3],
|
||||
pub lr: LrUnits,
|
||||
/// LR units this tile coded (per plane), for merging.
|
||||
pub lr_written: [Vec<usize>; 3],
|
||||
pub above_level: [Vec<u8>; 3],
|
||||
pub above_dc: [Vec<u8>; 3],
|
||||
pub above_seg_pred: Vec<u8>,
|
||||
pub left_level: [Vec<u8>; 3],
|
||||
pub left_dc: [Vec<u8>; 3],
|
||||
pub left_seg_pred: Vec<u8>,
|
||||
/// TileIntraFrameYModeCdf (reset per tile).
|
||||
pub intra_frame_y_mode_cdf: [[[u16; 14]; 5]; 5],
|
||||
cdef_cols: usize,
|
||||
}
|
||||
|
||||
impl TileState {
|
||||
fn with(cur: FrameBuf, mi: MiInfo, cdef: Vec<i8>, lf_tx_size: [Grid8; 3], lr: LrUnits, cdef_cols: usize) -> TileState {
|
||||
let (mi_cols, mi_rows) = (mi.cols, mi.rows);
|
||||
TileState {
|
||||
cur,
|
||||
mi,
|
||||
cdef,
|
||||
lf_tx_size,
|
||||
lr,
|
||||
lr_written: Default::default(),
|
||||
above_level: std::array::from_fn(|_| vec![0; mi_cols + 64]),
|
||||
above_dc: std::array::from_fn(|_| vec![0; mi_cols + 64]),
|
||||
above_seg_pred: vec![0; mi_cols + 64],
|
||||
left_level: std::array::from_fn(|_| vec![0; mi_rows + 64]),
|
||||
left_dc: std::array::from_fn(|_| vec![0; mi_rows + 64]),
|
||||
left_seg_pred: vec![0; mi_rows + 64],
|
||||
intra_frame_y_mode_cdf: DEFAULT_INTRA_FRAME_Y_MODE_CDF,
|
||||
cdef_cols,
|
||||
}
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn cdef_idx(&self, r: usize, c: usize) -> i8 {
|
||||
self.cdef[(r >> 4) * self.cdef_cols + (c >> 4)]
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn set_cdef_idx(&mut self, r: usize, c: usize, v: i8) {
|
||||
let i = (r >> 4) * self.cdef_cols + (c >> 4);
|
||||
if i < self.cdef.len() {
|
||||
self.cdef[i] = v;
|
||||
}
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn set_lf_tx_size(&mut self, plane: usize, row: usize, col: usize, v: u8) {
|
||||
self.lf_tx_size[plane].set(row, col, v);
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) struct FrameState {
|
||||
pub sh: FrameShared,
|
||||
pub cur: FrameBuf,
|
||||
pub mi: MiInfo,
|
||||
cdef: Vec<i8>,
|
||||
lf_tx_size: [Grid8; 3],
|
||||
pub lr: LrUnits,
|
||||
}
|
||||
|
||||
impl Deref for FrameState {
|
||||
type Target = FrameShared;
|
||||
fn deref(&self) -> &FrameShared {
|
||||
&self.sh
|
||||
}
|
||||
}
|
||||
|
||||
impl DerefMut for FrameState {
|
||||
fn deref_mut(&mut self) -> &mut FrameShared {
|
||||
&mut self.sh
|
||||
}
|
||||
}
|
||||
|
||||
fn count_units_in_frame(unit_size: usize, frame_size: usize) -> usize {
|
||||
((frame_size + (unit_size >> 1)) / unit_size).max(1)
|
||||
}
|
||||
|
||||
impl FrameState {
|
||||
pub fn new(seq: &SequenceHeader, fh: &FrameHeader) -> FrameState {
|
||||
let c = &seq.color;
|
||||
let (ssx, ssy) = (c.subsampling_x as usize, c.subsampling_y as usize);
|
||||
let mi_cols = fh.mi_cols as usize;
|
||||
let mi_rows = fh.mi_rows as usize;
|
||||
let cur = FrameBuf::new(fh.frame_width as usize, fh.frame_height as usize, c.num_planes, ssx, ssy, c.bit_depth);
|
||||
let cdef_cols = mi_cols.div_ceil(16) + 2;
|
||||
let cdef_rows = mi_rows.div_ceil(16) + 2;
|
||||
let mut lr_unit_rows = [0; 3];
|
||||
let mut lr_unit_cols = [0; 3];
|
||||
let mut lr = LrUnits::default();
|
||||
for plane in 0..c.num_planes {
|
||||
if fh.lr.frame_restoration_type[plane] == RESTORE_NONE as u8 {
|
||||
continue;
|
||||
}
|
||||
let sub_x = if plane == 0 { 0 } else { ssx };
|
||||
let sub_y = if plane == 0 { 0 } else { ssy };
|
||||
let unit = fh.lr.loop_restoration_size[plane] as usize;
|
||||
lr_unit_rows[plane] = count_units_in_frame(unit, (fh.frame_height as usize + sub_y) >> sub_y);
|
||||
lr_unit_cols[plane] = count_units_in_frame(unit, (fh.upscaled_width as usize + sub_x) >> sub_x);
|
||||
let n = lr_unit_rows[plane] * lr_unit_cols[plane];
|
||||
lr.lr_type[plane] = vec![RESTORE_NONE as u8; n];
|
||||
lr.lr_wiener[plane] = vec![[[0; 3]; 2]; n];
|
||||
lr.lr_sgr_set[plane] = vec![0; n];
|
||||
lr.lr_sgr_xqd[plane] = vec![[0; 2]; n];
|
||||
}
|
||||
FrameState {
|
||||
sh: FrameShared {
|
||||
seq: seq.clone(),
|
||||
fh: fh.clone(),
|
||||
num_planes: c.num_planes,
|
||||
ssx,
|
||||
ssy,
|
||||
bit_depth: c.bit_depth as u32,
|
||||
lr_unit_rows,
|
||||
lr_unit_cols,
|
||||
cdef_cols,
|
||||
refs: Default::default(),
|
||||
ref_info: Default::default(),
|
||||
motion_field: Default::default(),
|
||||
prev_segment_ids: vec![0; mi_cols * mi_rows],
|
||||
},
|
||||
cur,
|
||||
mi: MiInfo::new(mi_cols, mi_rows, fh.allow_screen_content_tools),
|
||||
cdef: vec![-1; cdef_cols * cdef_rows],
|
||||
lf_tx_size: std::array::from_fn(|_| Grid8::new(0, 0, mi_cols + 32, mi_rows + 32)),
|
||||
lr,
|
||||
}
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn cdef_idx(&self, r: usize, c: usize) -> i8 {
|
||||
self.cdef[(r >> 4) * self.cdef_cols + (c >> 4)]
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn lf_tx_size(&self, plane: usize, row: usize, col: usize) -> usize {
|
||||
self.lf_tx_size[plane].get(row, col) as usize
|
||||
}
|
||||
|
||||
/// Move the frame's buffers into a tile state covering the whole frame.
|
||||
pub fn take_tile_state(&mut self) -> TileState {
|
||||
TileState::with(
|
||||
std::mem::take(&mut self.cur),
|
||||
std::mem::take(&mut self.mi),
|
||||
std::mem::take(&mut self.cdef),
|
||||
std::mem::take(&mut self.lf_tx_size),
|
||||
std::mem::take(&mut self.lr),
|
||||
self.cdef_cols,
|
||||
)
|
||||
}
|
||||
|
||||
/// Return the buffers taken by [`FrameState::take_tile_state`].
|
||||
pub fn put_tile_state(&mut self, t: TileState) {
|
||||
self.cur = t.cur;
|
||||
self.mi = t.mi;
|
||||
self.cdef = t.cdef;
|
||||
self.lf_tx_size = t.lf_tx_size;
|
||||
self.lr = t.lr;
|
||||
}
|
||||
|
||||
/// Private buffers for the tile covering mode info rows [r0, r1) x columns [c0, c1); tiles
|
||||
/// on the right / bottom frame edge also cover the allocation margins.
|
||||
pub fn tile_state(&self, r0: usize, r1: usize, c0: usize, c1: usize) -> TileState {
|
||||
let (mi_cols, mi_rows) = (self.mi.cols, self.mi.rows);
|
||||
let x1 = if c1 >= mi_cols { usize::MAX } else { c1 * 4 };
|
||||
let y1 = if r1 >= mi_rows { usize::MAX } else { r1 * 4 };
|
||||
let cur = self.cur.region_like(c0 * 4, r0 * 4, x1, y1);
|
||||
let mi = MiInfo::region(mi_cols, mi_rows, c0, r0, c1 - c0, r1 - r0, !self.mi.palette_size[0].is_empty());
|
||||
let lf = std::array::from_fn(|p| {
|
||||
let (sx, sy) = if p == 0 { (0, 0) } else { (self.ssx, self.ssy) };
|
||||
let full = &self.lf_tx_size[p];
|
||||
let (gx0, gy0) = (c0 >> sx, r0 >> sy);
|
||||
let gx1 = if c1 >= mi_cols { full.stride } else { c1 >> sx };
|
||||
let gy1 = if r1 >= mi_rows { full.rows } else { r1 >> sy };
|
||||
Grid8::new(gx0, gy0, gx1 - gx0, gy1 - gy0)
|
||||
});
|
||||
TileState::with(cur, mi, vec![-1; self.cdef.len()], lf, self.lr.clone(), self.cdef_cols)
|
||||
}
|
||||
|
||||
/// Copy a tile's private buffers into the frame.
|
||||
pub fn merge_tile_state(&mut self, t: &TileState) {
|
||||
for p in 0..self.num_planes {
|
||||
t.cur.planes[p].copy_into(&mut self.cur.planes[p]);
|
||||
t.lf_tx_size[p].copy_into(&mut self.lf_tx_size[p]);
|
||||
for &u in &t.lr_written[p] {
|
||||
self.lr.lr_type[p][u] = t.lr.lr_type[p][u];
|
||||
self.lr.lr_wiener[p][u] = t.lr.lr_wiener[p][u];
|
||||
self.lr.lr_sgr_set[p][u] = t.lr.lr_sgr_set[p][u];
|
||||
self.lr.lr_sgr_xqd[p][u] = t.lr.lr_sgr_xqd[p][u];
|
||||
}
|
||||
}
|
||||
t.mi.copy_into(&mut self.mi);
|
||||
for (d, &s) in self.cdef.iter_mut().zip(&t.cdef) {
|
||||
if s != -1 {
|
||||
*d = s;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,114 @@
|
||||
//! Decode statistics: frame / tile counts and the busy time of every decoding stage.
|
||||
|
||||
/// A decoding stage (see [`DecodeStats::stage_secs`]).
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
pub enum Stage {
|
||||
/// Frame header follow-up: reference setup, motion field estimation, CDF initialisation.
|
||||
Setup,
|
||||
/// Tile decoding: mode info, coefficients, prediction and reconstruction.
|
||||
Tiles,
|
||||
/// Deblocking loop filter (7.14).
|
||||
LoopFilter,
|
||||
/// CDEF (7.15).
|
||||
Cdef,
|
||||
/// Super-resolution upscaling (7.16).
|
||||
Superres,
|
||||
/// Loop restoration (7.17).
|
||||
Restoration,
|
||||
/// Reference update, motion vector storage, output copy and film grain synthesis.
|
||||
Output,
|
||||
}
|
||||
|
||||
impl Stage {
|
||||
pub const COUNT: usize = 7;
|
||||
pub const ALL: [Stage; Stage::COUNT] =
|
||||
[Stage::Setup, Stage::Tiles, Stage::LoopFilter, Stage::Cdef, Stage::Superres, Stage::Restoration, Stage::Output];
|
||||
|
||||
pub fn name(self) -> &'static str {
|
||||
match self {
|
||||
Stage::Setup => "setup",
|
||||
Stage::Tiles => "tiles",
|
||||
Stage::LoopFilter => "deblock",
|
||||
Stage::Cdef => "cdef",
|
||||
Stage::Superres => "superres",
|
||||
Stage::Restoration => "restoration",
|
||||
Stage::Output => "output",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Counters accumulated by a [`crate::Decoder`]. Stage times are busy time summed over every
|
||||
/// thread that worked on the stage (wall-clock per piece of work), so with threads they can add
|
||||
/// up to more than the elapsed time.
|
||||
#[derive(Clone, Copy, Debug, Default, PartialEq)]
|
||||
pub struct DecodeStats {
|
||||
/// Frames decoded (excluding show_existing_frame repeats).
|
||||
pub frames: u64,
|
||||
/// Frames decoded in draft mode without in-loop filters.
|
||||
pub draft_frames: u64,
|
||||
/// Tiles decoded.
|
||||
pub tiles: u64,
|
||||
/// Busy seconds per [`Stage`] (index with `stage as usize`).
|
||||
pub stage_secs: [f64; Stage::COUNT],
|
||||
}
|
||||
|
||||
impl DecodeStats {
|
||||
pub fn secs(&self, stage: Stage) -> f64 {
|
||||
self.stage_secs[stage as usize]
|
||||
}
|
||||
|
||||
pub(crate) fn add(&mut self, stage: Stage, secs: f64) {
|
||||
self.stage_secs[stage as usize] += secs;
|
||||
}
|
||||
|
||||
pub(crate) fn merge(&mut self, o: &DecodeStats) {
|
||||
self.frames += o.frames;
|
||||
self.draft_frames += o.draft_frames;
|
||||
self.tiles += o.tiles;
|
||||
for (a, b) in self.stage_secs.iter_mut().zip(o.stage_secs.iter()) {
|
||||
*a += b;
|
||||
}
|
||||
}
|
||||
|
||||
/// Sum of all stage times.
|
||||
pub fn total_secs(&self) -> f64 {
|
||||
self.stage_secs.iter().sum()
|
||||
}
|
||||
}
|
||||
|
||||
/// A wall-clock stopwatch (a no-op on wasm32, which has no clock in std).
|
||||
pub(crate) struct Timer {
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
start: std::time::Instant,
|
||||
}
|
||||
|
||||
impl Timer {
|
||||
#[inline]
|
||||
pub fn start() -> Timer {
|
||||
Timer {
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
start: std::time::Instant::now(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Seconds since `start` (0 on wasm32).
|
||||
#[inline]
|
||||
pub fn secs(&self) -> f64 {
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
{
|
||||
self.start.elapsed().as_secs_f64()
|
||||
}
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
{
|
||||
0.0
|
||||
}
|
||||
}
|
||||
|
||||
/// Seconds since `start`, restarting the stopwatch.
|
||||
#[inline]
|
||||
pub fn lap(&mut self) -> f64 {
|
||||
let s = self.secs();
|
||||
*self = Timer::start();
|
||||
s
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,138 @@
|
||||
//! Symbol (arithmetic) decoder, spec 8.2.
|
||||
//!
|
||||
//! The state follows the specification literally: `SymbolValue` / `SymbolRange` are 15/16-bit
|
||||
//! quantities renormalised by reading `bits` new bits at a time, and `SymbolMaxBits` counts the
|
||||
//! bits still available (going negative while zero padding is consumed).
|
||||
|
||||
const EC_PROB_SHIFT: u32 = 6;
|
||||
const EC_MIN_PROB: u32 = 4;
|
||||
|
||||
pub(crate) struct SymbolDecoder<'a> {
|
||||
data: &'a [u8],
|
||||
/// Next byte of `data` to load into the window.
|
||||
next: usize,
|
||||
/// SymbolValue followed by `k` look-ahead bits: the bitstream with every bit inverted
|
||||
/// (the spec's renormalisation XORs the new bits with ones), zero bits past the end of
|
||||
/// the data reading as ones. SymbolValue = win >> k.
|
||||
win: u64,
|
||||
k: u32,
|
||||
range: u32,
|
||||
pub disable_update: bool,
|
||||
}
|
||||
|
||||
impl<'a> SymbolDecoder<'a> {
|
||||
/// `init_symbol( sz )` over `data` (the tile's bytes).
|
||||
pub fn new(data: &'a [u8], disable_update: bool) -> Self {
|
||||
let mut d = SymbolDecoder { data, next: 0, win: 0, k: 0, range: 1 << 15, disable_update };
|
||||
// SymbolValue = ((1 << 15) - 1) ^ (first 15 bits): 15 inverted bits.
|
||||
let mut filled = 0;
|
||||
while filled < 15 + 32 {
|
||||
d.win = (d.win << 8) | d.inverted_byte() as u64;
|
||||
filled += 8;
|
||||
}
|
||||
d.k = filled - 15;
|
||||
d
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
fn inverted_byte(&mut self) -> u8 {
|
||||
let b = self.data.get(self.next).copied().unwrap_or(0);
|
||||
self.next += 1;
|
||||
!b
|
||||
}
|
||||
|
||||
/// `read_symbol( cdf )`: `cdf` has N + 1 entries (the last is the adaptation counter).
|
||||
#[inline]
|
||||
pub fn read_symbol(&mut self, cdf: &mut [u16]) -> usize {
|
||||
let n = cdf.len() - 1;
|
||||
let symbol = self.decode(cdf, n);
|
||||
if !self.disable_update {
|
||||
update_cdf(cdf, n, symbol);
|
||||
}
|
||||
symbol
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
fn decode(&mut self, cdf: &[u16], n: usize) -> usize {
|
||||
let value = (self.win >> self.k) as u32;
|
||||
let mut cur = self.range;
|
||||
let mut symbol = 0usize;
|
||||
let mut prev;
|
||||
loop {
|
||||
prev = cur;
|
||||
let f = (1u32 << 15) - cdf[symbol] as u32;
|
||||
cur = ((self.range >> 8) * (f >> EC_PROB_SHIFT)) >> (7 - EC_PROB_SHIFT);
|
||||
cur += EC_MIN_PROB * (n - symbol - 1) as u32;
|
||||
if value >= cur {
|
||||
break;
|
||||
}
|
||||
symbol += 1;
|
||||
}
|
||||
self.range = prev - cur;
|
||||
self.win -= (cur as u64) << self.k;
|
||||
self.renormalize();
|
||||
symbol
|
||||
}
|
||||
|
||||
/// Renormalisation (8.2.6): SymbolValue takes `bits` more (inverted) bits, which are
|
||||
/// already in the window.
|
||||
#[inline(always)]
|
||||
fn renormalize(&mut self) {
|
||||
let bits = 15 - (31 - self.range.leading_zeros());
|
||||
self.range <<= bits;
|
||||
self.k -= bits;
|
||||
if self.k < 16 {
|
||||
// SymbolValue < 2^16, so 16 + k <= 64 bits stay in the window.
|
||||
while self.k <= 40 {
|
||||
self.win = (self.win << 8) | self.inverted_byte() as u64;
|
||||
self.k += 8;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// `read_bool()`
|
||||
#[inline]
|
||||
pub fn read_bool(&mut self) -> u32 {
|
||||
let cdf = [1u16 << 14, 1 << 15, 0];
|
||||
self.decode(&cdf, 2) as u32
|
||||
}
|
||||
|
||||
/// `read_literal( n )` (L(n))
|
||||
pub fn read_literal(&mut self, n: u32) -> u32 {
|
||||
let mut x = 0;
|
||||
for _ in 0..n {
|
||||
x = (x << 1) | self.read_bool();
|
||||
}
|
||||
x
|
||||
}
|
||||
|
||||
/// NS(n)
|
||||
pub fn read_ns(&mut self, n: u32) -> u32 {
|
||||
if n <= 1 {
|
||||
return 0;
|
||||
}
|
||||
let w = 32 - n.leading_zeros();
|
||||
let m = (1u32 << w) - n;
|
||||
let v = self.read_literal(w - 1);
|
||||
if v < m {
|
||||
return v;
|
||||
}
|
||||
let extra = self.read_literal(1);
|
||||
(v << 1) - m + extra
|
||||
}
|
||||
}
|
||||
|
||||
/// CDF adaptation (8.2.6).
|
||||
#[inline(always)]
|
||||
pub(crate) fn update_cdf(cdf: &mut [u16], n: usize, symbol: usize) {
|
||||
let count = cdf[n];
|
||||
let rate = 3 + (count > 15) as u32 + (count > 31) as u32 + (31 - (n as u32).leading_zeros()).min(2);
|
||||
// Entries below `symbol` move towards 0, the others towards 1 << 15 (the specification's
|
||||
// comparison with tmp, resolved per entry: c >= 0 and c <= 1 << 15), without branching on
|
||||
// the decoded symbol.
|
||||
for (i, e) in cdf[..n - 1].iter_mut().enumerate() {
|
||||
let c = *e as u32;
|
||||
*e = if i < symbol { c - (c >> rate) } else { c + (((1 << 15) - c) >> rate) } as u16;
|
||||
}
|
||||
cdf[n] += (count < 32) as u16;
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,780 @@
|
||||
//! Inverse transforms (spec 7.13), implemented step by step as specified.
|
||||
|
||||
use crate::spec_tables::{
|
||||
ADST_ADST, ADST_DCT, ADST_FLIPADST, COS128_LOOKUP, DCT_ADST, DCT_DCT, DCT_FLIPADST, FLIPADST_ADST, FLIPADST_DCT, FLIPADST_FLIPADST, H_ADST,
|
||||
H_DCT, H_FLIPADST, TRANSFORM_ROW_SHIFT, TX_HEIGHT_LOG2, TX_WIDTH_LOG2, V_ADST, V_DCT, V_FLIPADST,
|
||||
};
|
||||
|
||||
#[inline(always)]
|
||||
fn round2_64(x: i64, n: u32) -> i32 {
|
||||
if n == 0 { x as i32 } else { ((x + (1i64 << (n - 1))) >> n) as i32 }
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
pub(crate) fn round2(x: i32, n: u32) -> i32 {
|
||||
if n == 0 { x } else { (x + (1 << (n - 1))) >> n }
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
fn cos128(angle: i32) -> i64 {
|
||||
let a = (angle & 255) as usize;
|
||||
(match a {
|
||||
0..=64 => COS128_LOOKUP[a] as i32,
|
||||
65..=128 => -(COS128_LOOKUP[128 - a] as i32),
|
||||
129..=192 => -(COS128_LOOKUP[a - 128] as i32),
|
||||
_ => COS128_LOOKUP[256 - a] as i32,
|
||||
}) as i64
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
fn sin128(angle: i32) -> i64 {
|
||||
cos128(angle - 64)
|
||||
}
|
||||
|
||||
/// One value of the 1D transforms, or `L` independent columns / rows of them processed
|
||||
/// together (lane-wise, so the steps vectorise).
|
||||
pub(crate) trait Lane: Copy {
|
||||
const ZERO: Self;
|
||||
/// (Round2( a * cos - b * sin, 12 ), Round2( a * sin + b * cos, 12 ))
|
||||
fn rotate(a: Self, b: Self, cos: i32, sin: i32) -> (Self, Self);
|
||||
/// (Clamp( x + y ), Clamp( x - y )) to [lo, hi]
|
||||
fn hadamard(x: Self, y: Self, lo: i32, hi: i32) -> (Self, Self);
|
||||
fn neg(self) -> Self;
|
||||
/// Round2( v * m, 12 )
|
||||
fn mul_round12(self, m: i32) -> Self;
|
||||
fn mul(self, k: i32) -> Self;
|
||||
fn adst4(t: &mut [Self]);
|
||||
}
|
||||
|
||||
impl Lane for i32 {
|
||||
const ZERO: i32 = 0;
|
||||
#[inline(always)]
|
||||
fn rotate(a: i32, b: i32, cos: i32, sin: i32) -> (i32, i32) {
|
||||
let (a, b, c, s) = (a as i64, b as i64, cos as i64, sin as i64);
|
||||
(round2_64(a * c - b * s, 12), round2_64(a * s + b * c, 12))
|
||||
}
|
||||
#[inline(always)]
|
||||
fn hadamard(x: i32, y: i32, lo: i32, hi: i32) -> (i32, i32) {
|
||||
((x + y).clamp(lo, hi), (x - y).clamp(lo, hi))
|
||||
}
|
||||
#[inline(always)]
|
||||
fn neg(self) -> i32 {
|
||||
-self
|
||||
}
|
||||
#[inline(always)]
|
||||
fn mul_round12(self, m: i32) -> i32 {
|
||||
round2_64(self as i64 * m as i64, 12)
|
||||
}
|
||||
#[inline(always)]
|
||||
fn mul(self, k: i32) -> i32 {
|
||||
self * k
|
||||
}
|
||||
fn adst4(t: &mut [i32]) {
|
||||
inverse_adst4_scalar(t);
|
||||
}
|
||||
}
|
||||
|
||||
/// Lanes in 32-bit arithmetic: exact for conforming streams up to 10 bits per sample (the
|
||||
/// spec requires every intermediate value to fit in 8 + BitDepth <= 18 bits, so products with
|
||||
/// the 12-bit cosines stay below 2^31). Wrapping operations keep corrupt streams from
|
||||
/// panicking.
|
||||
impl<const L: usize> Lane for [i32; L] {
|
||||
const ZERO: Self = [0; L];
|
||||
#[inline(always)]
|
||||
fn rotate(a: Self, b: Self, cos: i32, sin: i32) -> (Self, Self) {
|
||||
let mut x = [0; L];
|
||||
let mut y = [0; L];
|
||||
for l in 0..L {
|
||||
let p = a[l].wrapping_mul(cos).wrapping_sub(b[l].wrapping_mul(sin));
|
||||
let q = a[l].wrapping_mul(sin).wrapping_add(b[l].wrapping_mul(cos));
|
||||
x[l] = p.wrapping_add(1 << 11) >> 12;
|
||||
y[l] = q.wrapping_add(1 << 11) >> 12;
|
||||
}
|
||||
(x, y)
|
||||
}
|
||||
#[inline(always)]
|
||||
fn hadamard(x: Self, y: Self, lo: i32, hi: i32) -> (Self, Self) {
|
||||
let mut a = [0; L];
|
||||
let mut b = [0; L];
|
||||
for l in 0..L {
|
||||
a[l] = x[l].wrapping_add(y[l]).max(lo).min(hi);
|
||||
b[l] = x[l].wrapping_sub(y[l]).max(lo).min(hi);
|
||||
}
|
||||
(a, b)
|
||||
}
|
||||
#[inline(always)]
|
||||
fn neg(self) -> Self {
|
||||
self.map(i32::wrapping_neg)
|
||||
}
|
||||
#[inline(always)]
|
||||
fn mul_round12(self, m: i32) -> Self {
|
||||
self.map(|v| v.wrapping_mul(m).wrapping_add(1 << 11) >> 12)
|
||||
}
|
||||
#[inline(always)]
|
||||
fn mul(self, k: i32) -> Self {
|
||||
self.map(|v| v.wrapping_mul(k))
|
||||
}
|
||||
fn adst4(t: &mut [Self]) {
|
||||
for l in 0..L {
|
||||
let mut v = [t[0][l], t[1][l], t[2][l], t[3][l]];
|
||||
inverse_adst4_scalar(&mut v);
|
||||
for k in 0..4 {
|
||||
t[k][l] = v[k];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn brev(num_bits: u32, x: usize) -> usize {
|
||||
let mut t = 0;
|
||||
for i in 0..num_bits {
|
||||
let bit = (x >> i) & 1;
|
||||
t += bit << (num_bits - 1 - i);
|
||||
}
|
||||
t
|
||||
}
|
||||
|
||||
/// Butterfly rotation B( a, b, angle, flip ).
|
||||
#[inline(always)]
|
||||
fn bf<T: Lane>(t: &mut [T], a: usize, b: usize, angle: i32, flip: bool) {
|
||||
let (x, y) = T::rotate(t[a], t[b], cos128(angle) as i32, sin128(angle) as i32);
|
||||
t[a] = x;
|
||||
t[b] = y;
|
||||
if flip {
|
||||
t.swap(a, b);
|
||||
}
|
||||
}
|
||||
|
||||
/// Hadamard rotation H( a, b, flip, r ).
|
||||
#[inline(always)]
|
||||
fn hd<T: Lane>(t: &mut [T], a: usize, b: usize, flip: bool, r: u32) {
|
||||
let (a, b) = if flip { (b, a) } else { (a, b) };
|
||||
let lo = -(1i32 << (r - 1));
|
||||
let hi = (1i32 << (r - 1)) - 1;
|
||||
let (x, y) = T::hadamard(t[a], t[b], lo, hi);
|
||||
t[a] = x;
|
||||
t[b] = y;
|
||||
}
|
||||
|
||||
/// Inverse DCT of length 2^n (7.13.2.3), in place.
|
||||
pub(crate) fn inverse_dct<T: Lane>(t: &mut [T], n: u32, r: u32) {
|
||||
// 7.13.2.2 permutation
|
||||
let len = 1usize << n;
|
||||
let mut copy = [T::ZERO; 64];
|
||||
copy[..len].copy_from_slice(&t[..len]);
|
||||
for i in 0..len {
|
||||
t[i] = copy[brev(n, i)];
|
||||
}
|
||||
inverse_dct_stage1(t, n, r);
|
||||
inverse_dct_stage2(t, n, r);
|
||||
inverse_dct_stage3(t, n, r);
|
||||
inverse_dct_stage4(t, n, r);
|
||||
inverse_dct_stage5(t, n, r);
|
||||
inverse_dct_stage6(t, n, r);
|
||||
}
|
||||
|
||||
fn inverse_dct_stage1<T: Lane>(t: &mut [T], n: u32, r: u32) {
|
||||
if n == 6 {
|
||||
for i in 0..16 {
|
||||
bf(t, 32 + i, 63 - i, 63 - 4 * brev(4, i) as i32, false);
|
||||
}
|
||||
}
|
||||
if n >= 5 {
|
||||
for i in 0..8 {
|
||||
bf(t, 16 + i, 31 - i, 6 + ((brev(3, 7 - i) as i32) << 3), false);
|
||||
}
|
||||
}
|
||||
if n == 6 {
|
||||
for i in 0..16 {
|
||||
hd(t, 32 + i * 2, 33 + i * 2, i & 1 == 1, r);
|
||||
}
|
||||
}
|
||||
if n >= 4 {
|
||||
for i in 0..4 {
|
||||
bf(t, 8 + i, 15 - i, 12 + ((brev(2, 3 - i) as i32) << 4), false);
|
||||
}
|
||||
}
|
||||
if n >= 5 {
|
||||
for i in 0..8 {
|
||||
hd(t, 16 + 2 * i, 17 + 2 * i, i & 1 == 1, r);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn inverse_dct_stage2<T: Lane>(t: &mut [T], n: u32, r: u32) {
|
||||
if n == 6 {
|
||||
for i in 0..4 {
|
||||
for j in 0..2 {
|
||||
bf(t, 62 - i * 4 - j, 33 + i * 4 + j, 60 - 16 * brev(2, i) as i32 + 64 * j as i32, true);
|
||||
}
|
||||
}
|
||||
}
|
||||
if n >= 3 {
|
||||
for i in 0..2 {
|
||||
bf(t, 4 + i, 7 - i, 56 - 32 * i as i32, false);
|
||||
}
|
||||
}
|
||||
if n >= 4 {
|
||||
for i in 0..4 {
|
||||
hd(t, 8 + 2 * i, 9 + 2 * i, i & 1 == 1, r);
|
||||
}
|
||||
}
|
||||
if n >= 5 {
|
||||
for i in 0..2 {
|
||||
for j in 0..2 {
|
||||
bf(t, 30 - 4 * i - j, 17 + 4 * i + j, 24 + ((j as i32) << 6) + (((1 - i) as i32) << 5), true);
|
||||
}
|
||||
}
|
||||
}
|
||||
if n == 6 {
|
||||
for i in 0..8 {
|
||||
for j in 0..2 {
|
||||
hd(t, 32 + i * 4 + j, 35 + i * 4 - j, i & 1 == 1, r);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn inverse_dct_stage3<T: Lane>(t: &mut [T], n: u32, r: u32) {
|
||||
for i in 0..2 {
|
||||
bf(t, 2 * i, 2 * i + 1, 32 + 16 * i as i32, i == 0);
|
||||
}
|
||||
if n >= 3 {
|
||||
for i in 0..2 {
|
||||
hd(t, 4 + 2 * i, 5 + 2 * i, i == 1, r);
|
||||
}
|
||||
}
|
||||
if n >= 4 {
|
||||
for i in 0..2 {
|
||||
bf(t, 14 - i, 9 + i, 48 + 64 * i as i32, true);
|
||||
}
|
||||
}
|
||||
if n >= 5 {
|
||||
for i in 0..4 {
|
||||
for j in 0..2 {
|
||||
hd(t, 16 + 4 * i + j, 19 + 4 * i - j, i & 1 == 1, r);
|
||||
}
|
||||
}
|
||||
}
|
||||
if n == 6 {
|
||||
for i in 0..2 {
|
||||
for j in 0..4 {
|
||||
bf(t, 61 - i * 8 - j, 34 + i * 8 + j, 56 - i as i32 * 32 + (j as i32 >> 1) * 64, true);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn inverse_dct_stage4<T: Lane>(t: &mut [T], n: u32, r: u32) {
|
||||
for i in 0..2 {
|
||||
hd(t, i, 3 - i, false, r);
|
||||
}
|
||||
if n >= 3 {
|
||||
bf(t, 6, 5, 32, true);
|
||||
}
|
||||
if n >= 4 {
|
||||
for i in 0..2 {
|
||||
for j in 0..2 {
|
||||
hd(t, 8 + 4 * i + j, 11 + 4 * i - j, i == 1, r);
|
||||
}
|
||||
}
|
||||
}
|
||||
if n >= 5 {
|
||||
for i in 0..4 {
|
||||
bf(t, 29 - i, 18 + i, 48 + (i as i32 >> 1) * 64, true);
|
||||
}
|
||||
}
|
||||
if n == 6 {
|
||||
for i in 0..4 {
|
||||
for j in 0..4 {
|
||||
hd(t, 32 + 8 * i + j, 39 + 8 * i - j, i & 1 == 1, r);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn inverse_dct_stage5<T: Lane>(t: &mut [T], n: u32, r: u32) {
|
||||
if n >= 3 {
|
||||
for i in 0..4 {
|
||||
hd(t, i, 7 - i, false, r);
|
||||
}
|
||||
}
|
||||
if n >= 4 {
|
||||
for i in 0..2 {
|
||||
bf(t, 13 - i, 10 + i, 32, true);
|
||||
}
|
||||
}
|
||||
if n >= 5 {
|
||||
for i in 0..2 {
|
||||
for j in 0..4 {
|
||||
hd(t, 16 + i * 8 + j, 23 + i * 8 - j, i == 1, r);
|
||||
}
|
||||
}
|
||||
}
|
||||
if n == 6 {
|
||||
for i in 0..8 {
|
||||
bf(t, 59 - i, 36 + i, if i < 4 { 48 } else { 112 }, true);
|
||||
}
|
||||
}
|
||||
if n >= 4 {
|
||||
for i in 0..8 {
|
||||
hd(t, i, 15 - i, false, r);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn inverse_dct_stage6<T: Lane>(t: &mut [T], n: u32, r: u32) {
|
||||
if n >= 5 {
|
||||
for i in 0..4 {
|
||||
bf(t, 27 - i, 20 + i, 32, true);
|
||||
}
|
||||
}
|
||||
if n == 6 {
|
||||
for i in 0..8 {
|
||||
hd(t, 32 + i, 47 - i, false, r);
|
||||
hd(t, 48 + i, 63 - i, true, r);
|
||||
}
|
||||
}
|
||||
if n >= 5 {
|
||||
for i in 0..16 {
|
||||
hd(t, i, 31 - i, false, r);
|
||||
}
|
||||
}
|
||||
if n == 6 {
|
||||
for i in 0..8 {
|
||||
bf(t, 55 - i, 40 + i, 32, true);
|
||||
}
|
||||
}
|
||||
if n == 6 {
|
||||
for i in 0..32 {
|
||||
hd(t, i, 63 - i, false, r);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const SINPI_1_9: i64 = 1321;
|
||||
const SINPI_2_9: i64 = 2482;
|
||||
const SINPI_3_9: i64 = 3344;
|
||||
const SINPI_4_9: i64 = 3803;
|
||||
|
||||
fn inverse_adst4_scalar(t: &mut [i32]) {
|
||||
let mut s = [0i64; 7];
|
||||
let mut x = [0i64; 4];
|
||||
let t0 = t[0] as i64;
|
||||
let t1 = t[1] as i64;
|
||||
let t2 = t[2] as i64;
|
||||
let t3 = t[3] as i64;
|
||||
s[0] = SINPI_1_9 * t0;
|
||||
s[1] = SINPI_2_9 * t0;
|
||||
s[2] = SINPI_3_9 * t1;
|
||||
s[3] = SINPI_4_9 * t2;
|
||||
s[4] = SINPI_1_9 * t2;
|
||||
s[5] = SINPI_2_9 * t3;
|
||||
s[6] = SINPI_4_9 * t3;
|
||||
let a7 = t0 - t2;
|
||||
let b7 = a7 + t3;
|
||||
s[0] += s[3];
|
||||
s[1] -= s[4];
|
||||
s[3] = s[2];
|
||||
s[2] = SINPI_3_9 * b7;
|
||||
s[0] += s[5];
|
||||
s[1] -= s[6];
|
||||
x[0] = s[0] + s[3];
|
||||
x[1] = s[1] + s[3];
|
||||
x[2] = s[2];
|
||||
x[3] = s[0] + s[1];
|
||||
x[3] -= s[3];
|
||||
for i in 0..4 {
|
||||
t[i] = round2_64(x[i], 12);
|
||||
}
|
||||
}
|
||||
|
||||
fn adst_in_permute<T: Lane>(t: &mut [T], n: u32) {
|
||||
let n0 = 1usize << n;
|
||||
let mut copy = [T::ZERO; 16];
|
||||
copy[..n0].copy_from_slice(&t[..n0]);
|
||||
for i in 0..n0 {
|
||||
let idx = if i & 1 == 1 { i - 1 } else { n0 - i - 1 };
|
||||
t[i] = copy[idx];
|
||||
}
|
||||
}
|
||||
|
||||
fn adst_out_permute<T: Lane>(t: &mut [T], n: u32) {
|
||||
let n0 = 1usize << n;
|
||||
let mut copy = [T::ZERO; 16];
|
||||
copy[..n0].copy_from_slice(&t[..n0]);
|
||||
for i in 0..n0 {
|
||||
let a = (i >> 3) & 1;
|
||||
let b = ((i >> 2) & 1) ^ ((i >> 3) & 1);
|
||||
let c = ((i >> 1) & 1) ^ ((i >> 2) & 1);
|
||||
let d = (i & 1) ^ ((i >> 1) & 1);
|
||||
let idx = ((d << 3) | (c << 2) | (b << 1) | a) >> (4 - n);
|
||||
t[i] = if i & 1 == 1 { copy[idx].neg() } else { copy[idx] };
|
||||
}
|
||||
}
|
||||
|
||||
fn inverse_adst8<T: Lane>(t: &mut [T], r: u32) {
|
||||
adst_in_permute(t, 3);
|
||||
for i in 0..4 {
|
||||
bf(t, 2 * i, 2 * i + 1, 60 - 16 * i as i32, true);
|
||||
}
|
||||
for i in 0..4 {
|
||||
hd(t, i, 4 + i, false, r);
|
||||
}
|
||||
for i in 0..2 {
|
||||
bf(t, 4 + 3 * i, 5 + i, 48 - 32 * i as i32, true);
|
||||
}
|
||||
for i in 0..2 {
|
||||
for j in 0..2 {
|
||||
hd(t, 4 * j + i, 2 + 4 * j + i, false, r);
|
||||
}
|
||||
}
|
||||
for i in 0..2 {
|
||||
bf(t, 2 + 4 * i, 3 + 4 * i, 32, true);
|
||||
}
|
||||
adst_out_permute(t, 3);
|
||||
}
|
||||
|
||||
fn inverse_adst16<T: Lane>(t: &mut [T], r: u32) {
|
||||
adst_in_permute(t, 4);
|
||||
for i in 0..8 {
|
||||
bf(t, 2 * i, 2 * i + 1, 62 - 8 * i as i32, true);
|
||||
}
|
||||
for i in 0..8 {
|
||||
hd(t, i, 8 + i, false, r);
|
||||
}
|
||||
for i in 0..2 {
|
||||
bf(t, 8 + 2 * i, 9 + 2 * i, 56 - 32 * i as i32, true);
|
||||
bf(t, 13 + 2 * i, 12 + 2 * i, 8 + 32 * i as i32, true);
|
||||
}
|
||||
for i in 0..4 {
|
||||
for j in 0..2 {
|
||||
hd(t, 8 * j + i, 4 + 8 * j + i, false, r);
|
||||
}
|
||||
}
|
||||
for i in 0..2 {
|
||||
for j in 0..2 {
|
||||
bf(t, 4 + 8 * j + 3 * i, 5 + 8 * j + i, 48 - 32 * i as i32, true);
|
||||
}
|
||||
}
|
||||
for i in 0..2 {
|
||||
for j in 0..4 {
|
||||
hd(t, 4 * j + i, 2 + 4 * j + i, false, r);
|
||||
}
|
||||
}
|
||||
for i in 0..4 {
|
||||
bf(t, 2 + 4 * i, 3 + 4 * i, 32, true);
|
||||
}
|
||||
adst_out_permute(t, 4);
|
||||
}
|
||||
|
||||
fn inverse_adst<T: Lane>(t: &mut [T], n: u32, r: u32) {
|
||||
match n {
|
||||
2 => T::adst4(t),
|
||||
3 => inverse_adst8(t, r),
|
||||
_ => inverse_adst16(t, r),
|
||||
}
|
||||
}
|
||||
|
||||
fn inverse_identity<T: Lane>(t: &mut [T], n: u32) {
|
||||
match n {
|
||||
2 => {
|
||||
for v in t[..4].iter_mut() {
|
||||
*v = v.mul_round12(5793);
|
||||
}
|
||||
}
|
||||
3 => {
|
||||
for v in t[..8].iter_mut() {
|
||||
*v = v.mul(2);
|
||||
}
|
||||
}
|
||||
4 => {
|
||||
for v in t[..16].iter_mut() {
|
||||
*v = v.mul_round12(11586);
|
||||
}
|
||||
}
|
||||
_ => {
|
||||
for v in t[..32].iter_mut() {
|
||||
*v = v.mul(4);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn inverse_wht(t: &mut [i32], shift: u32) {
|
||||
let mut a = t[0] >> shift;
|
||||
let mut c = t[1] >> shift;
|
||||
let mut d = t[2] >> shift;
|
||||
let mut b = t[3] >> shift;
|
||||
a += c;
|
||||
d -= b;
|
||||
let e = (a - d) >> 1;
|
||||
b = e - b;
|
||||
c = e - c;
|
||||
a -= b;
|
||||
d += c;
|
||||
t[0] = a;
|
||||
t[1] = b;
|
||||
t[2] = c;
|
||||
t[3] = d;
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, PartialEq, Eq)]
|
||||
enum Kind1d {
|
||||
Dct,
|
||||
Adst,
|
||||
Identity,
|
||||
}
|
||||
|
||||
fn row_kind(tx_type: usize) -> Kind1d {
|
||||
match tx_type {
|
||||
DCT_DCT | ADST_DCT | FLIPADST_DCT | H_DCT => Kind1d::Dct,
|
||||
DCT_ADST | ADST_ADST | DCT_FLIPADST | FLIPADST_FLIPADST | ADST_FLIPADST | FLIPADST_ADST | H_ADST | H_FLIPADST => Kind1d::Adst,
|
||||
_ => Kind1d::Identity,
|
||||
}
|
||||
}
|
||||
|
||||
fn col_kind(tx_type: usize) -> Kind1d {
|
||||
match tx_type {
|
||||
DCT_DCT | DCT_ADST | DCT_FLIPADST | V_DCT => Kind1d::Dct,
|
||||
ADST_DCT | ADST_ADST | FLIPADST_DCT | FLIPADST_FLIPADST | ADST_FLIPADST | FLIPADST_ADST | V_ADST | V_FLIPADST => Kind1d::Adst,
|
||||
_ => Kind1d::Identity,
|
||||
}
|
||||
}
|
||||
|
||||
/// 2D inverse transform (7.13.3). `dequant` is 64x64 (row stride 64, only the top-left 32x32
|
||||
/// may be non-zero); the residual is written to `residual` with row stride `w`.
|
||||
pub(crate) fn inverse_transform_2d(dequant: &[i32], residual: &mut [i32], tx_sz: usize, tx_type: usize, lossless: bool, bit_depth: u32) {
|
||||
let log2w = TX_WIDTH_LOG2[tx_sz] as u32;
|
||||
let log2h = TX_HEIGHT_LOG2[tx_sz] as u32;
|
||||
let w = 1usize << log2w;
|
||||
let h = 1usize << log2h;
|
||||
let row_shift = if lossless { 0 } else { TRANSFORM_ROW_SHIFT[tx_sz] as u32 };
|
||||
let col_shift = if lossless { 0 } else { 4 };
|
||||
let row_clamp = bit_depth + 8;
|
||||
let col_clamp = (bit_depth + 6).max(16);
|
||||
let rk = row_kind(tx_type);
|
||||
let ck = col_kind(tx_type);
|
||||
let rect2 = log2w.abs_diff(log2h) == 1;
|
||||
let p = Tx2d { w, h, log2w, log2h, row_shift, col_shift, row_clamp, col_clamp, rk, ck, rect2 };
|
||||
if !lossless && bit_depth <= 10 {
|
||||
if w >= 8 && h >= 8 {
|
||||
transform_lanes::<8>(dequant, residual, &p);
|
||||
} else {
|
||||
transform_lanes::<4>(dequant, residual, &p);
|
||||
}
|
||||
return;
|
||||
}
|
||||
scalar_row_pass(dequant, residual, &p, lossless);
|
||||
scalar_col_pass(residual, &p, lossless);
|
||||
}
|
||||
|
||||
fn scalar_row_pass(dequant: &[i32], residual: &mut [i32], p: &Tx2d, lossless: bool) {
|
||||
let (w, h, log2w, row_shift, row_clamp, col_clamp, rk, rect2) = (p.w, p.h, p.log2w, p.row_shift, p.row_clamp, p.col_clamp, p.rk, p.rect2);
|
||||
let mut t = [0i32; 64];
|
||||
let in_w = w.min(32);
|
||||
for i in 0..h {
|
||||
// Every 1D transform maps an all-zero row to zeros (each step is linear with
|
||||
// Round2( 0 ) = 0), so rows without coefficients (including rows 32..63) are zero.
|
||||
if i >= 32 || dequant[i * 64..i * 64 + in_w].iter().all(|&v| v == 0) {
|
||||
residual[i * w..i * w + w].fill(0);
|
||||
continue;
|
||||
}
|
||||
t[..in_w].copy_from_slice(&dequant[i * 64..i * 64 + in_w]);
|
||||
t[in_w..w].fill(0);
|
||||
if rect2 {
|
||||
for v in t[..w].iter_mut() {
|
||||
*v = round2_64(*v as i64 * 2896, 12);
|
||||
}
|
||||
}
|
||||
if lossless {
|
||||
inverse_wht(&mut t, 2);
|
||||
} else {
|
||||
match rk {
|
||||
Kind1d::Dct => inverse_dct(&mut t, log2w, row_clamp),
|
||||
Kind1d::Adst => inverse_adst(&mut t, log2w, row_clamp),
|
||||
Kind1d::Identity => inverse_identity(&mut t, log2w),
|
||||
}
|
||||
}
|
||||
let lo = -(1i32 << (col_clamp - 1));
|
||||
let hi = (1i32 << (col_clamp - 1)) - 1;
|
||||
for j in 0..w {
|
||||
residual[i * w + j] = round2(t[j], row_shift).clamp(lo, hi);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn scalar_col_pass(residual: &mut [i32], p: &Tx2d, lossless: bool) {
|
||||
let (w, h, log2h, col_shift, col_clamp, ck) = (p.w, p.h, p.log2h, p.col_shift, p.col_clamp, p.ck);
|
||||
let mut t = [0i32; 64];
|
||||
for j in 0..w {
|
||||
for i in 0..h {
|
||||
t[i] = residual[i * w + j];
|
||||
}
|
||||
if lossless {
|
||||
inverse_wht(&mut t, 0);
|
||||
} else {
|
||||
match ck {
|
||||
Kind1d::Dct => inverse_dct(&mut t, log2h, col_clamp),
|
||||
Kind1d::Adst => inverse_adst(&mut t, log2h, col_clamp),
|
||||
Kind1d::Identity => inverse_identity(&mut t, log2h),
|
||||
}
|
||||
}
|
||||
for i in 0..h {
|
||||
residual[i * w + j] = round2(t[i], col_shift);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct Tx2d {
|
||||
w: usize,
|
||||
h: usize,
|
||||
log2w: u32,
|
||||
log2h: u32,
|
||||
row_shift: u32,
|
||||
col_shift: u32,
|
||||
row_clamp: u32,
|
||||
col_clamp: u32,
|
||||
rk: Kind1d,
|
||||
ck: Kind1d,
|
||||
rect2: bool,
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
fn transform_1d<T: Lane>(t: &mut [T], kind: Kind1d, n: u32, r: u32) {
|
||||
match kind {
|
||||
Kind1d::Dct => inverse_dct(t, n, r),
|
||||
Kind1d::Adst => inverse_adst(t, n, r),
|
||||
Kind1d::Identity => inverse_identity(t, n),
|
||||
}
|
||||
}
|
||||
|
||||
/// The 2D inverse transform with `L` rows (row pass) or columns (column pass) at a time;
|
||||
/// bit-identical to the one-at-a-time process for bit depths up to 10.
|
||||
fn transform_lanes<const L: usize>(dequant: &[i32], residual: &mut [i32], p: &Tx2d) {
|
||||
let mut t = [[0i32; L]; 64];
|
||||
lanes_row_pass(dequant, residual, p, &mut t);
|
||||
lanes_col_pass(residual, p, &mut t);
|
||||
}
|
||||
|
||||
fn lanes_row_pass<const L: usize>(dequant: &[i32], residual: &mut [i32], p: &Tx2d, t: &mut [[i32; L]; 64]) {
|
||||
let (w, h) = (p.w, p.h);
|
||||
let in_w = w.min(32);
|
||||
let lo = -(1i32 << (p.col_clamp - 1));
|
||||
let hi = (1i32 << (p.col_clamp - 1)) - 1;
|
||||
let mut i0 = 0;
|
||||
while i0 < h {
|
||||
// Rows without coefficients (and rows 32..63) transform to zeros.
|
||||
if i0 >= 32 || (i0..i0 + L).all(|i| dequant[i * 64..i * 64 + in_w].iter().all(|&v| v == 0)) {
|
||||
residual[i0 * w..(i0 + L) * w].fill(0);
|
||||
i0 += L;
|
||||
continue;
|
||||
}
|
||||
for (j, tj) in t[..w].iter_mut().enumerate() {
|
||||
for l in 0..L {
|
||||
tj[l] = if j < in_w { dequant[(i0 + l) * 64 + j] } else { 0 };
|
||||
}
|
||||
}
|
||||
if p.rect2 {
|
||||
for v in t[..w].iter_mut() {
|
||||
*v = v.mul_round12(2896);
|
||||
}
|
||||
}
|
||||
transform_1d(&mut t[..], p.rk, p.log2w, p.row_clamp);
|
||||
for l in 0..L {
|
||||
let out = &mut residual[(i0 + l) * w..(i0 + l + 1) * w];
|
||||
for j in 0..w {
|
||||
out[j] = round2(t[j][l], p.row_shift).clamp(lo, hi);
|
||||
}
|
||||
}
|
||||
i0 += L;
|
||||
}
|
||||
}
|
||||
|
||||
fn lanes_col_pass<const L: usize>(residual: &mut [i32], p: &Tx2d, t: &mut [[i32; L]; 64]) {
|
||||
let (w, h) = (p.w, p.h);
|
||||
let mut j0 = 0;
|
||||
while j0 < w {
|
||||
for i in 0..h {
|
||||
t[i].copy_from_slice(&residual[i * w + j0..i * w + j0 + L]);
|
||||
}
|
||||
transform_1d(&mut t[..], p.ck, p.log2h, p.col_clamp);
|
||||
for i in 0..h {
|
||||
let out = &mut residual[i * w + j0..i * w + j0 + L];
|
||||
for l in 0..L {
|
||||
out[l] = round2(t[i][l], p.col_shift);
|
||||
}
|
||||
}
|
||||
j0 += L;
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
/// The DCT step list must implement an orthogonal transform: a DC-only input gives a flat
|
||||
/// output equal to DC * cos(pi/4) (in the 2^12 fixed-point scale).
|
||||
#[test]
|
||||
fn dct_dc_is_flat() {
|
||||
for n in 2..=6 {
|
||||
let len = 1 << n;
|
||||
let mut t = [0i32; 64];
|
||||
t[0] = 4096;
|
||||
inverse_dct(&mut t, n, 24);
|
||||
for v in &t[..len] {
|
||||
assert_eq!(*v, 2896, "n={n}: {:?}", &t[..len]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Compare each inverse DCT basis vector with the real-valued DCT-III.
|
||||
#[test]
|
||||
fn dct_matches_float() {
|
||||
for n in 2..=6u32 {
|
||||
let len = 1usize << n;
|
||||
for k in 0..len {
|
||||
let mut t = [0i32; 64];
|
||||
t[k] = 1 << 12;
|
||||
inverse_dct(&mut t, n, 24);
|
||||
for x in 0..len {
|
||||
let c = if k == 0 { std::f64::consts::FRAC_1_SQRT_2 } else { 1.0 };
|
||||
let want = 4096.0 * c * ((2 * x + 1) as f64 * k as f64 * std::f64::consts::PI / (2 * len) as f64).cos();
|
||||
assert!((t[x] as f64 - want).abs() < 4.0 + 0.5 * n as f64, "n={n} k={k} x={x}: {} vs {want}", t[x]);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// ADST8/16 basis vectors follow sin(pi (2i+1)(2k+1) / 4N) (scaled like the DCT).
|
||||
#[test]
|
||||
fn adst_basis_matches_sine() {
|
||||
for n in 3..=4u32 {
|
||||
let len = 1usize << n;
|
||||
for k in 0..len {
|
||||
let mut t = [0i32; 64];
|
||||
t[k] = 1 << 12;
|
||||
inverse_adst(&mut t, n, 24);
|
||||
for i in 0..len {
|
||||
let want = 4096.0 * ((std::f64::consts::PI * (2 * i + 1) as f64 * (2 * k + 1) as f64) / (4 * len) as f64).sin();
|
||||
assert!((t[i] as f64 - want).abs() < 8.0, "n={n} k={k} i={i}: {} vs {want:.1}", t[i]);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn adst_is_close_to_sine_transform() {
|
||||
for n in 3..=4u32 {
|
||||
let len = 1usize << n;
|
||||
for k in 0..len {
|
||||
let mut t = [0i32; 64];
|
||||
t[k] = 1 << 12;
|
||||
inverse_adst(&mut t, n, 24);
|
||||
let energy: f64 = t[..len].iter().map(|&v| (v as f64 / 4096.0).powi(2)).sum();
|
||||
// like the DCT, the AV1 ADST is scaled by sqrt(N / 2)
|
||||
assert!((energy - len as f64 / 2.0).abs() < 0.02, "n={n} k={k} energy {energy}");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
[package]
|
||||
name = "deckcraft-bitstream"
|
||||
version.workspace = true
|
||||
edition.workspace = true
|
||||
license.workspace = true
|
||||
rust-version.workspace = true
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
||||
[dependencies]
|
||||
craft-media = { workspace = true, features = ["bitstream"] }
|
||||
@@ -0,0 +1,82 @@
|
||||
//! Memory budget for decoded pictures, shared by the video decoders.
|
||||
//!
|
||||
//! A per-axis size cap does not bound memory: a decoder holds the current picture, every
|
||||
//! reference picture and the pictures still being decoded on worker threads. Each decoder checks
|
||||
//! the whole picture store against [`DECODE_MEMORY_BUDGET`] with checked arithmetic before it
|
||||
//! allocates for a new picture size, so a hostile header is an error, not an out-of-memory abort.
|
||||
|
||||
/// Bytes the decoded pictures of one decoder may take: 4 GiB on 64-bit targets, 1 GiB on 32-bit
|
||||
/// ones (wasm32 has a 4 GiB address space in all).
|
||||
pub const DECODE_MEMORY_BUDGET: u64 = if cfg!(target_pointer_width = "64") { 4 << 30 } else { 1 << 30 };
|
||||
|
||||
/// The shape of one decoded picture as a decoder allocates it.
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
pub struct PictureShape {
|
||||
/// Allocated luma samples per row and rows (padding included).
|
||||
pub width: u64,
|
||||
pub height: u64,
|
||||
/// Chroma subsampling shifts (0 or 1); ignored when `chroma_planes` is 0.
|
||||
pub shift_x: u32,
|
||||
pub shift_y: u32,
|
||||
/// Chroma planes (0 for monochrome, 2 otherwise).
|
||||
pub chroma_planes: u64,
|
||||
/// Bytes per sample (1 for 8-bit buffers, 2 for 16-bit ones).
|
||||
pub bytes_per_sample: u64,
|
||||
}
|
||||
|
||||
impl PictureShape {
|
||||
/// Bytes of one picture, `None` if it does not fit in a `u64`.
|
||||
pub fn bytes(&self) -> Option<u64> {
|
||||
let luma = self.width.checked_mul(self.height)?;
|
||||
let cw = self.width.div_ceil(1 << self.shift_x.min(1));
|
||||
let ch = self.height.div_ceil(1 << self.shift_y.min(1));
|
||||
let chroma = cw.checked_mul(ch)?.checked_mul(self.chroma_planes)?;
|
||||
luma.checked_add(chroma)?.checked_mul(self.bytes_per_sample)
|
||||
}
|
||||
|
||||
/// Bytes of `pictures` such pictures, `None` on overflow.
|
||||
pub fn store_bytes(&self, pictures: u64) -> Option<u64> {
|
||||
self.bytes()?.checked_mul(pictures)
|
||||
}
|
||||
|
||||
/// Whether `pictures` such pictures fit in `budget` bytes.
|
||||
pub fn fits(&self, pictures: u64, budget: u64) -> bool {
|
||||
self.store_bytes(pictures).is_some_and(|b| b <= budget)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
fn shape(w: u64, h: u64) -> PictureShape {
|
||||
PictureShape { width: w, height: h, shift_x: 1, shift_y: 1, chroma_planes: 2, bytes_per_sample: 2 }
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn bytes_count_luma_and_subsampled_chroma() {
|
||||
assert_eq!(shape(4, 4).bytes(), Some((16 + 2 * 4) * 2));
|
||||
// Odd sizes round chroma up.
|
||||
assert_eq!(shape(3, 3).bytes(), Some((9 + 2 * 4) * 2));
|
||||
let mono = PictureShape { chroma_planes: 0, bytes_per_sample: 1, ..shape(10, 10) };
|
||||
assert_eq!(mono.bytes(), Some(100));
|
||||
let full = PictureShape { shift_x: 0, shift_y: 0, ..shape(10, 10) };
|
||||
assert_eq!(full.bytes(), Some(600));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn exact_budget_boundary() {
|
||||
let s = shape(4, 4); // 48 bytes
|
||||
assert!(s.fits(10, 480));
|
||||
assert!(!s.fits(10, 479));
|
||||
assert!(!s.fits(11, 480));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn overflow_is_never_within_budget() {
|
||||
let huge = shape(u64::MAX / 2, 3);
|
||||
assert_eq!(huge.bytes(), None);
|
||||
assert!(!huge.fits(1, u64::MAX));
|
||||
assert!(!shape(1 << 31, 1 << 31).fits(u64::MAX, u64::MAX));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
//! Bit-level reading and writing for DeckCraft's codec crates: a re-export of
|
||||
//! [`craft_media::bitstream`] from craft-libs, plus [`budget`].
|
||||
//!
|
||||
//! - [`BitReader`]: MSB-first reader with Exp-Golomb (`ue(v)`, `se(v)`), as used by H.264/HEVC/AV1 headers.
|
||||
//! - [`BitWriter`]: the matching writer (for encoders and header rewriting).
|
||||
//! - [`unescape_rbsp`] / [`escape_rbsp`]: remove/insert H.264/HEVC emulation-prevention bytes.
|
||||
//! - [`annexb_nals`] / [`length_prefixed_nals`]: split Annex-B byte streams and AVCC/HVCC samples into NAL units.
|
||||
//! - [`budget`]: the memory budget video decoders check their picture store against.
|
||||
|
||||
#![cfg_attr(not(test), deny(clippy::unwrap_used, clippy::expect_used, clippy::panic, clippy::unimplemented, clippy::todo, clippy::unreachable))]
|
||||
|
||||
pub use craft_media::bitstream::*;
|
||||
|
||||
pub mod budget;
|
||||
@@ -466,26 +466,22 @@ fn ctx_xfrm(doc: &deckcraft_model::Presentation, sel: &crate::Selection, shape:
|
||||
|
||||
/// Run `f` on each target shape (copy-on-write), materialising inherited boxes first.
|
||||
pub(crate) fn edit_shapes(s: &mut Session, p: &Value, cmd: &str, f: impl Fn(&mut deckcraft_model::Shape) -> Result<()>) -> Result<Value> {
|
||||
let mut ids = targets(s, p)?;
|
||||
let ids = targets(s, p)?;
|
||||
if ids.is_empty() {
|
||||
return Err(bad(cmd, "no shapes selected"));
|
||||
}
|
||||
// A shape named twice is edited once.
|
||||
let mut seen = std::collections::HashSet::new();
|
||||
ids.retain(|id| seen.insert(*id));
|
||||
s.edit(|doc, sel| {
|
||||
// The boxes are read from the shapes in place: no copy of a shape (and its text) per id.
|
||||
let boxes: std::collections::HashMap<ShapeId, Xfrm> = ids
|
||||
.iter()
|
||||
.filter_map(|id| current_shapes(doc, sel).and_then(|v| deckcraft_model::find_shape(v, *id)))
|
||||
.map(|sh| (sh.id, xfrm_of(doc, sel, sh)))
|
||||
.collect();
|
||||
let boxes: Vec<(ShapeId, Xfrm)> = {
|
||||
let st_shapes: Vec<deckcraft_model::Shape> =
|
||||
ids.iter().filter_map(|id| current_shapes(doc, sel).and_then(|v| deckcraft_model::find_shape(v, *id)).cloned()).collect();
|
||||
st_shapes.iter().map(|sh| (sh.id, xfrm_of(doc, sel, sh))).collect()
|
||||
};
|
||||
let shapes = crate::shapes_mut(doc, sel).ok_or_else(|| bad(cmd, "no slide"))?;
|
||||
let mut n = 0;
|
||||
for id in &ids {
|
||||
if let Some(sh) = deckcraft_model::find_shape_mut(shapes, *id) {
|
||||
if sh.xfrm.is_none() {
|
||||
sh.xfrm = boxes.get(id).copied();
|
||||
sh.xfrm = boxes.iter().find(|(i, _)| i == id).map(|(_, x)| *x);
|
||||
}
|
||||
f(sh)?;
|
||||
n += 1;
|
||||
|
||||
@@ -17,7 +17,7 @@ pub fn specs() -> Vec<CommandSpec> {
|
||||
"Animation Styles",
|
||||
["Animations", "Animation"],
|
||||
None,
|
||||
"{effect: fade|fly|zoom|wipe|appear|spin|pulse|fadeOut|lines|… (animation.list), class?: entrance|emphasis|exit|path, option?, path?: [[x,y],…] or OOXML string, closed?: bool, ids?} — replaces the shapes' animations. path (motion paths): points in slide points, as offsets from where the shape sits, or the OOXML path string (slide fractions) animation.get answers",
|
||||
"{effect: fade|fly|zoom|wipe|appear|spin|pulse|fadeOut|lines|… (animation.list), class?: entrance|emphasis|exit|path, option?, ids?} — replaces the shapes' animations",
|
||||
has_selection,
|
||||
set
|
||||
),
|
||||
@@ -26,7 +26,7 @@ pub fn specs() -> Vec<CommandSpec> {
|
||||
"Add Animation",
|
||||
["Animations", "Advanced Animation"],
|
||||
None,
|
||||
"{effect, class?, option?, path?: [[x,y],…] or OOXML string, closed?: bool, start?: onClick|withPrevious|afterPrevious, duration?: ms, delay?: ms, ids?} → {count, path?}",
|
||||
"{effect, class?, option?, start?: onClick|withPrevious|afterPrevious, duration?: ms, delay?: ms, ids?}",
|
||||
has_selection,
|
||||
add
|
||||
),
|
||||
@@ -46,7 +46,7 @@ pub fn specs() -> Vec<CommandSpec> {
|
||||
"Effect Options",
|
||||
["Animations", "Animation"],
|
||||
None,
|
||||
"{option?, textBuild?: asOne|byParagraph|byWord|byLetter, path?: [[x,y],…] or OOXML string, closed?: bool, index?}",
|
||||
"{option?, textBuild?: asOne|byParagraph|byWord|byLetter, index?}",
|
||||
has_slide,
|
||||
options
|
||||
),
|
||||
@@ -129,7 +129,7 @@ fn resolve_option(allowed: &[&str], given: &str) -> Option<String> {
|
||||
cands.into_iter().find(|c| allowed.contains(&c.as_str()))
|
||||
}
|
||||
|
||||
fn build(p: &Value, slide: (f64, f64)) -> Result<Animation> {
|
||||
fn build(p: &Value) -> Result<Animation> {
|
||||
let effect = str_param(p, "effect").ok_or_else(|| bad("animation", "missing `effect`"))?;
|
||||
let class = class_of(str_param(p, "class"));
|
||||
let info = match class {
|
||||
@@ -155,9 +155,7 @@ fn build(p: &Value, slide: (f64, f64)) -> Result<Animation> {
|
||||
a.start = start_of(st);
|
||||
}
|
||||
if a.class == AnimClass::Path {
|
||||
a.path = Some(path_param(p, slide)?.unwrap_or_else(|| default_path(&a.effect, &a.option)));
|
||||
} else if p.get("path").is_some() {
|
||||
return Err(bad("animation", "`path` only fits a motion path (class `path`)"));
|
||||
a.path = Some(default_path(&a.effect, &a.option));
|
||||
}
|
||||
if a.effect == "spin" {
|
||||
a.amount = Some(360.0);
|
||||
@@ -168,97 +166,6 @@ fn build(p: &Value, slide: (f64, f64)) -> Result<Animation> {
|
||||
Ok(a)
|
||||
}
|
||||
|
||||
/// Most points of a motion path given as an array, and longest path string given as text.
|
||||
const MAX_PATH_POINTS: usize = 1_000;
|
||||
const MAX_PATH_TEXT: usize = 16 * 1024;
|
||||
/// Largest offset of a point, in slide points; anything beyond is not a place on any slide.
|
||||
const MAX_PATH_OFFSET: f64 = 1.0e6;
|
||||
|
||||
/// A number of a path string: slide fractions, rounded to 6 places.
|
||||
fn path_num(v: f64) -> String {
|
||||
let t = format!("{:.6}", v);
|
||||
let t = t.trim_end_matches('0').trim_end_matches('.');
|
||||
if t == "-0" || t.is_empty() { "0".into() } else { t.into() }
|
||||
}
|
||||
|
||||
/// Does a raw motion path string draw: only commands and numbers, every number finite, and at
|
||||
/// least two points? (A lone `e`, or `1e999`, would play as an empty or partial path.)
|
||||
fn raw_path_draws(text: &str) -> bool {
|
||||
let cs: Vec<char> = text.chars().collect();
|
||||
let (mut i, mut nums) = (0, 0usize);
|
||||
while let Some(&c) = cs.get(i) {
|
||||
if c.is_ascii_digit() || c == '.' || ((c == '-' || c == '+') && cs.get(i + 1).is_some_and(|d| d.is_ascii_digit() || *d == '.')) {
|
||||
let start = i;
|
||||
i += 1;
|
||||
while cs.get(i).is_some_and(|d| d.is_ascii_digit() || *d == '.') {
|
||||
i += 1;
|
||||
}
|
||||
if cs.get(i).is_some_and(|d| *d == 'e' || *d == 'E') {
|
||||
let n1 = cs.get(i + 1).copied().unwrap_or(' ');
|
||||
let n2 = cs.get(i + 2).copied().unwrap_or(' ');
|
||||
if n1.is_ascii_digit() || ((n1 == '-' || n1 == '+') && n2.is_ascii_digit()) {
|
||||
i += 2;
|
||||
while cs.get(i).is_some_and(char::is_ascii_digit) {
|
||||
i += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
let tok: String = cs.get(start..i).map(|t| t.iter().collect()).unwrap_or_default();
|
||||
match tok.parse::<f64>() {
|
||||
Ok(v) if v.is_finite() => nums += 1,
|
||||
_ => return false,
|
||||
}
|
||||
} else if " ,MmLlCcQqHhVvZzE".contains(c) {
|
||||
i += 1;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
nums >= 4
|
||||
}
|
||||
|
||||
/// The motion path `path` asks for, as the OOXML path string animations store: either points
|
||||
/// (`[[x, y], …]`, slide points as offsets from where the shape sits, joined by lines, `closed?`
|
||||
/// to return to the first) or that string itself.
|
||||
fn path_param(p: &Value, slide: (f64, f64)) -> Result<Option<String>> {
|
||||
let Some(v) = p.get("path") else { return Ok(None) };
|
||||
let refuse = |why: &str| bad("animation", format!("`path` {why}"));
|
||||
match v {
|
||||
Value::String(text) => {
|
||||
let ok = text.len() <= MAX_PATH_TEXT
|
||||
&& text.contains(|c: char| c.is_ascii_digit())
|
||||
&& text.chars().all(|c| c.is_ascii_digit() || " .+-eE,MmLlCcQqHhVvZz".contains(c))
|
||||
&& raw_path_draws(text);
|
||||
if !ok {
|
||||
return Err(refuse("string must be an OOXML motion path such as `M 0 0 L 0.25 0 E` (at most 16 KiB)"));
|
||||
}
|
||||
Ok(Some(text.clone()))
|
||||
}
|
||||
Value::Array(points) => {
|
||||
if !(2..=MAX_PATH_POINTS).contains(&points.len()) {
|
||||
return Err(refuse(&format!("needs 2 to {MAX_PATH_POINTS} points")));
|
||||
}
|
||||
if !(slide.0.is_finite() && slide.1.is_finite() && slide.0 > 0.0 && slide.1 > 0.0) {
|
||||
return Err(refuse("can't be placed: the slide has no size"));
|
||||
}
|
||||
let mut out = String::new();
|
||||
for (k, pt) in points.iter().enumerate() {
|
||||
let xy = pt.as_array().filter(|a| a.len() == 2).and_then(|a| Some((a.first()?.as_f64()?, a.get(1)?.as_f64()?)));
|
||||
let Some((x, y)) = xy.filter(|(x, y)| x.abs() <= MAX_PATH_OFFSET && y.abs() <= MAX_PATH_OFFSET) else {
|
||||
return Err(refuse(&format!("point {k} must be [x, y], two finite numbers within ±{MAX_PATH_OFFSET} points")));
|
||||
};
|
||||
out.push_str(&format!("{} {} {} ", if k == 0 { "M" } else { "L" }, path_num(x / slide.0), path_num(y / slide.1)));
|
||||
}
|
||||
if p.get("closed").and_then(Value::as_bool) == Some(true) {
|
||||
out.push_str("Z ");
|
||||
}
|
||||
out.push('E');
|
||||
Ok(Some(out))
|
||||
}
|
||||
_ => Err(refuse("must be [[x, y], …] or an OOXML path string")),
|
||||
}
|
||||
}
|
||||
|
||||
/// Default motion paths, in slide-fraction units relative to the shape's position.
|
||||
pub fn default_path(effect: &str, option: &str) -> String {
|
||||
let (dx, dy) = match option {
|
||||
@@ -278,7 +185,7 @@ pub fn default_path(effect: &str, option: &str) -> String {
|
||||
|
||||
fn set(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
let ids = targets(s, p)?;
|
||||
let a = build(p, slide_extent(s)?)?;
|
||||
let a = build(p)?;
|
||||
s.edit(|doc, sel| {
|
||||
let sl = doc.slides.get_mut(sel.slide).ok_or_else(|| bad("animation.set", "no slide"))?;
|
||||
let sl = Arc::make_mut(sl);
|
||||
@@ -308,16 +215,9 @@ fn set(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
})
|
||||
}
|
||||
|
||||
/// The slide's width and height in points.
|
||||
fn slide_extent(s: &mut Session) -> Result<(f64, f64)> {
|
||||
let size = &s.doc()?.doc.slide_size;
|
||||
Ok((size.width, size.height))
|
||||
}
|
||||
|
||||
fn add(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
let ids = targets(s, p)?;
|
||||
let a = build(p, slide_extent(s)?)?;
|
||||
let path = a.path.clone();
|
||||
let a = build(p)?;
|
||||
s.edit(|doc, sel| {
|
||||
let sl = Arc::make_mut(doc.slides.get_mut(sel.slide).ok_or_else(|| bad("animation.add", "no slide"))?);
|
||||
for (k, id) in ids.iter().enumerate() {
|
||||
@@ -327,10 +227,7 @@ fn add(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
}
|
||||
sl.animations.push(x);
|
||||
}
|
||||
Ok(match &path {
|
||||
Some(path) => json!({"count": sl.animations.len(), "path": path}),
|
||||
None => json!({"count": sl.animations.len()}),
|
||||
})
|
||||
Ok(json!({"count": sl.animations.len()}))
|
||||
})
|
||||
}
|
||||
|
||||
@@ -423,17 +320,13 @@ fn options(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
"byLetter" => TextBuild::ByLetter,
|
||||
_ => TextBuild::AsOne,
|
||||
});
|
||||
let path = path_param(p, slide_extent(s)?)?;
|
||||
each_target(s, p, |a| {
|
||||
if let (Some(path), AnimClass::Path) = (&path, a.class) {
|
||||
a.path = Some(path.clone());
|
||||
}
|
||||
if let Some(o) = &option {
|
||||
// Each target takes the option in its own effect's terms; one it has no such
|
||||
// option for keeps its own.
|
||||
let allowed = animation_info(&a.effect, a.class).map(|i| i.5).unwrap_or(&[]);
|
||||
if let Some(o) = resolve_option(allowed, o) {
|
||||
if a.class == AnimClass::Path && path.is_none() {
|
||||
if a.class == AnimClass::Path {
|
||||
a.path = Some(default_path(&a.effect, &o));
|
||||
}
|
||||
a.option = o;
|
||||
@@ -713,76 +606,4 @@ mod tests {
|
||||
assert_eq!(s.execute("animation.get", &json!({})).unwrap().as_array().unwrap().len(), 1);
|
||||
}
|
||||
}
|
||||
|
||||
fn path_of(s: &mut Session) -> Option<String> {
|
||||
let all = s.execute("animation.get", &json!({})).unwrap();
|
||||
all.as_array().unwrap().last().and_then(|a| a.get("path")).and_then(Value::as_str).map(String::from)
|
||||
}
|
||||
|
||||
fn shape(s: &mut Session) -> u64 {
|
||||
s.execute("shape.insert", &json!({"preset": "ellipse", "rect": [100.0, 400.0, 20.0, 20.0]})).unwrap().get("id").unwrap().as_u64().unwrap()
|
||||
}
|
||||
|
||||
/// #1576: points in slide points, as offsets from the shape, become the slide-fraction path.
|
||||
#[test]
|
||||
fn a_custom_path_takes_its_points() {
|
||||
let mut s = Session::with_new();
|
||||
let id = shape(&mut s);
|
||||
let w = s.doc().unwrap().doc.slide_size.width;
|
||||
let h = s.doc().unwrap().doc.slide_size.height;
|
||||
let r = s
|
||||
.execute(
|
||||
"animation.add",
|
||||
&json!({"effect": "customPath", "class": "path", "path": [[0, 0], [w / 2.0, -h / 4.0], [w, -h / 2.0]], "ids": [id]}),
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(r["path"], "M 0 0 L 0.5 -0.25 L 1 -0.5 E");
|
||||
assert_eq!(path_of(&mut s).as_deref(), Some("M 0 0 L 0.5 -0.25 L 1 -0.5 E"));
|
||||
// closed shapes end back at the start; set replaces; no path keeps the default
|
||||
s.execute("animation.set", &json!({"effect": "customPath", "path": [[0, 0], [w / 4.0, 0], [w / 4.0, h / 4.0]], "closed": true, "ids": [id]}))
|
||||
.unwrap();
|
||||
assert_eq!(path_of(&mut s).as_deref(), Some("M 0 0 L 0.25 0 L 0.25 0.25 Z E"));
|
||||
s.execute("animation.set", &json!({"effect": "customPath", "ids": [id]})).unwrap();
|
||||
assert_eq!(path_of(&mut s).as_deref(), Some("M 0 0 L 0 0.25 E"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_raw_path_string_is_kept_and_options_edits_it() {
|
||||
let mut s = Session::with_new();
|
||||
let id = shape(&mut s);
|
||||
s.execute("animation.add", &json!({"effect": "customPath", "path": "M 0 0 C 0.1 0 0.2 0.1 0.3 0.1 E", "ids": [id]})).unwrap();
|
||||
assert_eq!(path_of(&mut s).as_deref(), Some("M 0 0 C 0.1 0 0.2 0.1 0.3 0.1 E"));
|
||||
s.execute("animation.options", &json!({"index": 0, "path": [[0, 0], [96, 54]]})).unwrap();
|
||||
assert_eq!(path_of(&mut s).as_deref(), Some("M 0 0 L 0.1 0.1 E"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_bad_path_is_refused_and_changes_nothing() {
|
||||
let mut s = Session::with_new();
|
||||
let id = shape(&mut s);
|
||||
let many: Vec<Value> = (0..2_000).map(|i| json!([i, i])).collect();
|
||||
for bad_path in [
|
||||
json!([[0, 0]]),
|
||||
json!([]),
|
||||
json!([[0, 0], [1]]),
|
||||
json!([[0, 0], ["a", 1]]),
|
||||
json!([[0, 0], [1e300, 1]]),
|
||||
json!([[0, 0], [null, 1]]),
|
||||
json!("M 0 0 L <script> E"),
|
||||
json!("not a path"),
|
||||
json!("eeee1"),
|
||||
json!("M 0 0 L 1e999 0 E"),
|
||||
json!(5),
|
||||
Value::Array(many),
|
||||
Value::String("M 0 0 L 0.1 0.1 ".repeat(10_000)),
|
||||
] {
|
||||
let r = s.execute("animation.add", &json!({"effect": "customPath", "path": bad_path, "ids": [id]}));
|
||||
assert!(r.is_err(), "{bad_path:.60}");
|
||||
}
|
||||
assert!(
|
||||
s.execute("animation.add", &json!({"effect": "fade", "path": [[0, 0], [1, 1]], "ids": [id]})).is_err(),
|
||||
"path only fits a motion path"
|
||||
);
|
||||
assert!(s.execute("animation.get", &json!({})).unwrap().as_array().unwrap().is_empty(), "a refused add left an animation behind");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -116,31 +116,16 @@ fn pack(data: &[u8], crop: [f64; 4], w_pt: f64, h_pt: f64, ppi: f64, delete_crop
|
||||
Some(Packed { bytes, content_type, crop: new_crop })
|
||||
}
|
||||
|
||||
/// Pictures accepted by `want`, with width and height measured in slide space through group transforms.
|
||||
fn pictures_mut<'a>(
|
||||
shapes: &'a mut [Shape],
|
||||
want: &dyn Fn(ShapeId) -> bool,
|
||||
out: &mut Vec<(&'a mut Shape, f64, f64)>,
|
||||
to_slide: deckcraft_geom::Affine,
|
||||
depth: usize,
|
||||
) {
|
||||
/// Picture shapes in `shapes` (groups included) that `want` accepts.
|
||||
fn pictures_mut<'a>(shapes: &'a mut [Shape], want: &dyn Fn(ShapeId) -> bool, out: &mut Vec<&'a mut Shape>, depth: usize) {
|
||||
if depth > 64 {
|
||||
return;
|
||||
}
|
||||
for sh in shapes {
|
||||
let Some(xfrm) = sh.xfrm else { continue };
|
||||
let local_to_slide = to_slide * xfrm.affine();
|
||||
if matches!(&sh.kind, ShapeKind::Picture { .. }) && want(sh.id) {
|
||||
let origin = local_to_slide * deckcraft_geom::Point::ZERO;
|
||||
let across = local_to_slide * deckcraft_geom::Point::new(xfrm.w, 0.0);
|
||||
let down = local_to_slide * deckcraft_geom::Point::new(0.0, xfrm.h);
|
||||
out.push((sh, (across - origin).hypot(), (down - origin).hypot()));
|
||||
continue;
|
||||
}
|
||||
if let ShapeKind::Group { children, child } = &mut sh.kind {
|
||||
let child_rect = deckcraft_geom::Rect::new(child.x, child.y, child.x + child.w, child.y + child.h);
|
||||
let child_to_slide = to_slide * deckcraft_geom::group_child_affine(&xfrm, child_rect);
|
||||
pictures_mut(children, want, out, child_to_slide, depth + 1);
|
||||
if matches!(sh.kind, ShapeKind::Picture { .. }) && want(sh.id) {
|
||||
out.push(sh);
|
||||
} else if let Some(ch) = sh.children_mut() {
|
||||
pictures_mut(ch, want, out, depth + 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -170,9 +155,10 @@ fn compress(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
let Some(slide) = doc.slides.get(i).cloned() else { continue };
|
||||
let mut slide = slide.as_ref().clone();
|
||||
let mut pics = vec![];
|
||||
pictures_mut(&mut slide.shapes, &want, &mut pics, deckcraft_geom::Affine::IDENTITY, 0);
|
||||
pictures_mut(&mut slide.shapes, &want, &mut pics, 0);
|
||||
let mut touched = false;
|
||||
for (sh, w, h) in pics {
|
||||
for sh in pics {
|
||||
let (w, h) = sh.xfrm.map(|x| (x.w, x.h)).unwrap_or((0.0, 0.0));
|
||||
let ShapeKind::Picture { fill } = &mut sh.kind else { continue };
|
||||
let PictureFill { media, crop, .. } = fill.clone();
|
||||
let key = (media.0, crop.map(f64::to_bits), w.to_bits(), h.to_bits());
|
||||
@@ -246,114 +232,6 @@ mod tests {
|
||||
assert!(pack(b"<svg xmlns='http://www.w3.org/2000/svg'/>", [0.0; 4], 72.0, 72.0, 150.0, true).is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn grouped_pictures_use_their_displayed_size() {
|
||||
let mut s = Session::with_new();
|
||||
let original = png(600, 600);
|
||||
s.edit(|doc, _| {
|
||||
let media = doc.add_media("grouped.png", "image/png", original.clone()).ok_or_else(|| bad("test", "media"))?;
|
||||
let picture = Shape {
|
||||
id: ShapeId(70_101),
|
||||
xfrm: Some(deckcraft_geom::Xfrm::new(0.0, 0.0, 72.0, 72.0)),
|
||||
kind: ShapeKind::Picture { fill: PictureFill { media, ..Default::default() } },
|
||||
..Default::default()
|
||||
};
|
||||
let nested = Shape {
|
||||
id: ShapeId(70_102),
|
||||
xfrm: Some(deckcraft_geom::Xfrm::new(0.0, 0.0, 72.0, 72.0)),
|
||||
kind: ShapeKind::Group { children: vec![picture], child: deckcraft_geom::Xfrm::new(0.0, 0.0, 72.0, 72.0) },
|
||||
..Default::default()
|
||||
};
|
||||
let group = Shape {
|
||||
id: ShapeId(70_103),
|
||||
xfrm: Some(deckcraft_geom::Xfrm::new(0.0, 0.0, 720.0, 720.0)),
|
||||
kind: ShapeKind::Group { children: vec![nested], child: deckcraft_geom::Xfrm::new(0.0, 0.0, 72.0, 72.0) },
|
||||
..Default::default()
|
||||
};
|
||||
let slide = doc.slides.first_mut().ok_or_else(|| bad("test", "slide"))?;
|
||||
Arc::make_mut(slide).shapes.push(group);
|
||||
Ok(())
|
||||
})
|
||||
.unwrap();
|
||||
|
||||
let result = s.execute("picture.compress", &json!({"all": true, "ppi": 96, "deleteCropped": false})).unwrap();
|
||||
assert_eq!(result["pictures"], 0, "a 600 px image displayed at 720×720 pt is already below the requested sampling density");
|
||||
assert!(
|
||||
s.doc().unwrap().doc.media.iter().any(|m| m.data.as_slice() == original.as_slice()),
|
||||
"the under-target original media must remain untouched"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn anisotropic_nested_group_scaling_controls_sampling_dimensions() {
|
||||
let mut s = Session::with_new();
|
||||
let original = png(2000, 1000);
|
||||
s.edit(|doc, _| {
|
||||
let media = doc.add_media("scaled-group.png", "image/png", original).ok_or_else(|| bad("test", "media"))?;
|
||||
let picture = Shape {
|
||||
id: ShapeId(70_111),
|
||||
xfrm: Some(deckcraft_geom::Xfrm::new(0.0, 0.0, 72.0, 72.0)),
|
||||
kind: ShapeKind::Picture { fill: PictureFill { media, ..Default::default() } },
|
||||
..Default::default()
|
||||
};
|
||||
let nested = Shape {
|
||||
id: ShapeId(70_112),
|
||||
xfrm: Some(deckcraft_geom::Xfrm::new(0.0, 0.0, 72.0, 72.0)),
|
||||
kind: ShapeKind::Group { children: vec![picture], child: deckcraft_geom::Xfrm::new(0.0, 0.0, 72.0, 72.0) },
|
||||
..Default::default()
|
||||
};
|
||||
let group = Shape {
|
||||
id: ShapeId(70_113),
|
||||
xfrm: Some(deckcraft_geom::Xfrm::new(0.0, 0.0, 720.0, 360.0)),
|
||||
kind: ShapeKind::Group { children: vec![nested], child: deckcraft_geom::Xfrm::new(0.0, 0.0, 72.0, 72.0) },
|
||||
..Default::default()
|
||||
};
|
||||
let slide = doc.slides.first_mut().ok_or_else(|| bad("test", "slide"))?;
|
||||
Arc::make_mut(slide).shapes.push(group);
|
||||
Ok(())
|
||||
})
|
||||
.unwrap();
|
||||
|
||||
let result = s.execute("picture.compress", &json!({"all": true, "ppi": 96, "deleteCropped": false})).unwrap();
|
||||
assert_eq!(result["pictures"], 1);
|
||||
let slide = s.doc().unwrap().doc.slides.first().unwrap();
|
||||
let picture = slide.shapes.iter().find(|sh| sh.id == ShapeId(70_113)).unwrap();
|
||||
let ShapeKind::Group { children, .. } = &picture.kind else { panic!("outer group expected") };
|
||||
let ShapeKind::Group { children, .. } = &children[0].kind else { panic!("nested group expected") };
|
||||
let ShapeKind::Picture { fill } = &children[0].kind else { panic!("picture expected") };
|
||||
let media = s.doc().unwrap().doc.media(fill.media).unwrap();
|
||||
let image = image::load_from_memory(&media.data).unwrap();
|
||||
assert_eq!((image.width(), image.height()), (960, 480));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ungrouped_picture_still_compresses_and_deletes_its_crop() {
|
||||
let mut s = Session::with_new();
|
||||
let original = png(800, 800);
|
||||
s.edit(|doc, _| {
|
||||
let media = doc.add_media("cropped.png", "image/png", original).ok_or_else(|| bad("test", "media"))?;
|
||||
let slide = doc.slides.first_mut().ok_or_else(|| bad("test", "slide"))?;
|
||||
Arc::make_mut(slide).shapes.push(Shape {
|
||||
id: ShapeId(70_104),
|
||||
xfrm: Some(deckcraft_geom::Xfrm::new(0.0, 0.0, 72.0, 72.0)),
|
||||
kind: ShapeKind::Picture { fill: PictureFill { media, crop: [0.5, 0.0, 0.0, 0.0], ..Default::default() } },
|
||||
..Default::default()
|
||||
});
|
||||
Ok(())
|
||||
})
|
||||
.unwrap();
|
||||
|
||||
let result = s.execute("picture.compress", &json!({"all": true, "ppi": 96, "deleteCropped": true})).unwrap();
|
||||
assert_eq!(result["pictures"], 1);
|
||||
let slide = s.doc().unwrap().doc.slides.first().unwrap();
|
||||
let picture = slide.shapes.iter().find(|sh| sh.id == ShapeId(70_104)).unwrap();
|
||||
let ShapeKind::Picture { fill } = &picture.kind else { panic!("picture expected") };
|
||||
assert_eq!(fill.crop, [0.0; 4]);
|
||||
let media = s.doc().unwrap().doc.media(fill.media).unwrap();
|
||||
let image = image::load_from_memory(&media.data).unwrap();
|
||||
assert_eq!((image.width(), image.height()), (96, 192));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pack_deletes_cropped_areas_or_keeps_them() {
|
||||
let src = png(400, 400);
|
||||
|
||||
@@ -1,17 +1,17 @@
|
||||
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,
|
||||
master_index, ok, opt_usize_param, rect_param, str_param, usize_param,
|
||||
};
|
||||
|
||||
pub(super) fn master_view(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
let st = s.doc()?;
|
||||
let mi = opt_usize_param(p, "master", "view.slideMaster")?.unwrap_or_else(|| master_index(&st.doc, &st.selection));
|
||||
let mi = usize_param(p, "master").unwrap_or_else(|| master_index(&st.doc, &st.selection));
|
||||
// A master thumbnail sends `{master}` and means the master itself. `{master, layout}` selects
|
||||
// that layout. With neither key, follow the current slide's layout.
|
||||
let layout = if p.get("master").is_some() && p.get("layout").is_none() {
|
||||
None
|
||||
} else {
|
||||
opt_usize_param(p, "layout", "view.slideMaster")?
|
||||
usize_param(p, "layout")
|
||||
.or_else(|| st.current_slide().and_then(|sl| st.doc.masters.get(mi).and_then(|m| m.layouts.iter().position(|l| l.id == sl.layout))))
|
||||
};
|
||||
let target = match layout {
|
||||
|
||||
@@ -1,14 +1,12 @@
|
||||
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, opt_usize_param, usize_param, with_param,
|
||||
f64_param, ok, usize_param, with_param,
|
||||
};
|
||||
|
||||
pub(super) fn background(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
if bool_or(p, "copy", false) {
|
||||
return copy_slide_background(s);
|
||||
}
|
||||
// Check the index before anything is edited: a picture is added to the media first.
|
||||
let index = opt_usize_param(p, "index", "design.background")?;
|
||||
let props = p.get("pictureProps").filter(|v| v.is_object()).cloned();
|
||||
let transparency = f64_param(p, "transparency");
|
||||
let reset = bool_or(p, "reset", false);
|
||||
@@ -64,6 +62,7 @@ pub(super) fn background(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
));
|
||||
};
|
||||
let all = bool_or(p, "all", false);
|
||||
let index = usize_param(p, "index");
|
||||
if matches!(op, BgOp::Adjust) {
|
||||
let next = {
|
||||
let st = s.doc()?;
|
||||
|
||||
@@ -485,16 +485,3 @@ fn background_all_clears_layout_overrides_even_if_slides_and_master_match() {
|
||||
s.execute("design.background", &json!({"color": "#AA0000", "all": true})).unwrap();
|
||||
assert_eq!(undo_len(&s), n + 1, "now it is truly a no-op");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn background_rejects_invalid_index() {
|
||||
let mut s = Session::with_new();
|
||||
s.execute("slide.new", &json!({})).unwrap();
|
||||
s.execute("slide.go", &json!({"index": 0})).unwrap();
|
||||
let before = s.doc().unwrap().doc.slides[0].background.clone();
|
||||
for bad in [json!(-1), json!(1.5)] {
|
||||
assert!(s.execute("design.background", &json!({"color": "#0000C8", "index": bad})).is_err());
|
||||
assert!(s.execute("design.background", &json!({"transparency": 0.5, "index": bad})).is_err());
|
||||
assert_eq!(s.doc().unwrap().doc.slides[0].background, before);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -107,7 +107,7 @@ fn copy_inner(s: &mut Session, p: &Value) -> Result<String> {
|
||||
s.clipboard = crate::Clipboard { text: Some(TextBody::from_text(&plain)), plain: plain.clone(), ..Default::default() };
|
||||
return Ok(plain);
|
||||
}
|
||||
let mut shapes: Vec<Shape> = st
|
||||
let shapes: Vec<Shape> = st
|
||||
.selection
|
||||
.shapes
|
||||
.iter()
|
||||
@@ -120,33 +120,14 @@ fn copy_inner(s: &mut Session, p: &Value) -> Result<String> {
|
||||
})
|
||||
})
|
||||
.collect();
|
||||
// Copying is the last chance to read what a placeholder inherits: a cut removes it, and the
|
||||
// paste can be in another deck. Slide Master view pastes the originals instead.
|
||||
let placeholders: Vec<Shape> = shapes.iter().filter(|x| x.ph.is_some()).cloned().collect();
|
||||
detach_on_slide(&st.doc, &st.selection, &mut shapes);
|
||||
let mids = crate::media_refs::shapes_media(&shapes);
|
||||
let media = st.doc.media.iter().filter(|m| mids.contains(&m.id)).cloned().collect();
|
||||
let plain = shapes.iter().filter_map(|x| x.text.as_ref().map(|t| t.text())).collect::<Vec<_>>().join("\n");
|
||||
// The theme of what is being edited: a slide's or layout's colour map, else the master's.
|
||||
let source_theme = super::text::ctx_for(&st.doc, &st.selection).map(|ctx| crate::SourceTheme {
|
||||
colors: ctx.master.theme.colors.clone(),
|
||||
color_map: ctx.color_map.clone(),
|
||||
fonts: ctx.master.theme.fonts.clone(),
|
||||
});
|
||||
s.clipboard = crate::Clipboard { shapes, placeholders, media, plain: plain.clone(), source_theme, ..Default::default() };
|
||||
let source_theme = st.current_slide().and_then(|sl| st.doc.master_for(sl)).map(|(m, _)| (m.scheme(), m.theme.fonts.clone()));
|
||||
s.clipboard = crate::Clipboard { shapes, media, plain: plain.clone(), source_theme, ..Default::default() };
|
||||
Ok(plain)
|
||||
}
|
||||
|
||||
/// On a slide, a copy of a placeholder is a regular shape: give `shapes` the look they inherit
|
||||
/// there. In Slide Master view a copy stays a placeholder, so this changes nothing.
|
||||
pub(crate) fn detach_on_slide(doc: &deckcraft_model::Presentation, sel: &crate::Selection, shapes: &mut [Shape]) {
|
||||
if sel.target == crate::Target::Slides
|
||||
&& let Some(ctx) = super::text::ctx_for(doc, sel)
|
||||
{
|
||||
shapes.iter_mut().for_each(|sh| deckcraft_model::resolve::detach_placeholder(&ctx, sh));
|
||||
}
|
||||
}
|
||||
|
||||
fn copying_caret(s: &Session) -> bool {
|
||||
s.active().is_some_and(|d| d.selection.text.as_ref().is_some_and(|t| !t.is_range()))
|
||||
}
|
||||
@@ -338,13 +319,6 @@ fn paste(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
return s.edit(|doc, sel| {
|
||||
let media = bring_media("edit.paste", doc, &clip.media)?;
|
||||
let mut shapes = clip.shapes.clone();
|
||||
if sel.target != crate::Target::Slides {
|
||||
for sh in &mut shapes {
|
||||
if let Some(original) = clip.placeholders.iter().find(|p| p.id == sh.id) {
|
||||
sh.clone_from(original);
|
||||
}
|
||||
}
|
||||
}
|
||||
reid("edit.paste", doc, &mut shapes)?;
|
||||
crate::media_refs::visit_mut::shapes(&mut shapes, &mut remap_media(&media));
|
||||
let (mut taken, mut count) = super::current_names(doc, sel);
|
||||
@@ -366,8 +340,7 @@ fn paste(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
x.x += off;
|
||||
x.y += off;
|
||||
}
|
||||
// A placeholder pasted on a slide becomes a regular shape. One copied from a slide
|
||||
// already has its look; see `copy_inner`.
|
||||
// A pasted placeholder becomes a regular shape keeping its look.
|
||||
if sh.ph.is_some() && sel.target == crate::Target::Slides {
|
||||
sh.ph = None;
|
||||
}
|
||||
@@ -431,9 +404,8 @@ fn paste_special(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
"text" | "keepTextOnly" => paste_text(s, &json!({})),
|
||||
"keepSource" | "keepSourceFormatting" => {
|
||||
let saved = s.clipboard.clone();
|
||||
if let Some(theme) = &saved.source_theme {
|
||||
bake_theme(&mut s.clipboard.shapes, theme);
|
||||
bake_theme(&mut s.clipboard.placeholders, theme);
|
||||
if let Some((scheme, fonts)) = &saved.source_theme {
|
||||
bake_theme(&mut s.clipboard.shapes, scheme, fonts);
|
||||
}
|
||||
let r = paste(s, &json!({}));
|
||||
let pastes = s.clipboard.pastes;
|
||||
@@ -448,8 +420,8 @@ fn paste_special(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
|
||||
/// Replace theme colours and theme fonts with the values they have in `scheme` / `fonts`, so the
|
||||
/// shapes keep their look under another theme.
|
||||
pub(crate) fn bake_theme(shapes: &mut [Shape], theme: &crate::SourceTheme) {
|
||||
fn rec(v: &mut Value, theme: &crate::SourceTheme, depth: usize) {
|
||||
pub(crate) fn bake_theme(shapes: &mut [Shape], scheme: &deckcraft_color::ColorScheme, fonts: &deckcraft_model::theme::FontScheme) {
|
||||
fn rec(v: &mut Value, scheme: &deckcraft_color::ColorScheme, fonts: &deckcraft_model::theme::FontScheme, depth: usize) {
|
||||
if depth > 256 {
|
||||
return;
|
||||
}
|
||||
@@ -458,7 +430,7 @@ pub(crate) fn bake_theme(shapes: &mut [Shape], theme: &crate::SourceTheme) {
|
||||
if o.get("kind").and_then(Value::as_str) == Some("scheme")
|
||||
&& let Some(slot) = o.get("slot").and_then(|x| serde_json::from_value::<deckcraft_color::SchemeSlot>(x.clone()).ok())
|
||||
&& slot != deckcraft_color::SchemeSlot::PhClr
|
||||
&& let Ok(rgb) = serde_json::to_value(theme.colors.get(deckcraft_model::map_slot(&theme.color_map, slot)))
|
||||
&& let Ok(rgb) = serde_json::to_value(scheme.get(slot))
|
||||
{
|
||||
o.clear();
|
||||
o.insert("kind".into(), json!("rgb"));
|
||||
@@ -466,14 +438,14 @@ pub(crate) fn bake_theme(shapes: &mut [Shape], theme: &crate::SourceTheme) {
|
||||
return;
|
||||
}
|
||||
for x in o.values_mut() {
|
||||
rec(x, theme, depth + 1);
|
||||
rec(x, scheme, fonts, depth + 1);
|
||||
}
|
||||
}
|
||||
Value::Array(a) => a.iter_mut().for_each(|x| rec(x, theme, depth + 1)),
|
||||
Value::Array(a) => a.iter_mut().for_each(|x| rec(x, scheme, fonts, depth + 1)),
|
||||
Value::String(t) if t.starts_with('+') => {
|
||||
let set = match t.get(1..3) {
|
||||
Some("mj") => &theme.fonts.major,
|
||||
Some("mn") => &theme.fonts.minor,
|
||||
Some("mj") => &fonts.major,
|
||||
Some("mn") => &fonts.minor,
|
||||
_ => return,
|
||||
};
|
||||
let face = match t.get(3..) {
|
||||
@@ -491,7 +463,7 @@ pub(crate) fn bake_theme(shapes: &mut [Shape], theme: &crate::SourceTheme) {
|
||||
}
|
||||
for sh in shapes.iter_mut() {
|
||||
let Ok(mut v) = serde_json::to_value(&*sh) else { continue };
|
||||
rec(&mut v, theme, 0);
|
||||
rec(&mut v, scheme, fonts, 0);
|
||||
if let Ok(baked) = serde_json::from_value::<Shape>(v) {
|
||||
*sh = baked;
|
||||
}
|
||||
@@ -504,8 +476,8 @@ fn paste_picture(s: &mut Session) -> Result<Value> {
|
||||
if clip.shapes.is_empty() {
|
||||
return Err(bad("edit.pasteSpecial", "copy shapes first to paste them as a picture"));
|
||||
}
|
||||
if let Some(theme) = &clip.source_theme {
|
||||
bake_theme(&mut clip.shapes, theme);
|
||||
if let Some((scheme, fonts)) = &clip.source_theme {
|
||||
bake_theme(&mut clip.shapes, scheme, fonts);
|
||||
}
|
||||
let st = s.doc()?;
|
||||
let mut doc = (*st.doc).clone();
|
||||
@@ -561,7 +533,6 @@ fn duplicate(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
let mut copies: Vec<Shape> = list.iter().filter(|x| ids.contains(&x.id)).cloned().collect();
|
||||
let sources: Vec<ShapeId> = copies.iter().map(|c| c.id).collect();
|
||||
let boxes: Vec<_> = copies.iter().map(|c| xfrm_of(doc, sel, c)).collect();
|
||||
detach_on_slide(doc, sel, &mut copies);
|
||||
// Duplicating the copies of the last Duplicate again repeats how far they were moved
|
||||
// since (#966); anything else gets the usual offset.
|
||||
let (dx, dy) = last
|
||||
|
||||
@@ -253,15 +253,8 @@ pub struct LoadedFile {
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
pub fn read_presentation_file(path: &str) -> Result<LoadedFile> {
|
||||
let bytes = crate::io::read_file(path).map_err(EngineError::Other)?;
|
||||
loaded_file(path, &bytes)
|
||||
}
|
||||
|
||||
/// Parse `bytes` as the presentation file `path`, as [`read_presentation_file`] does after
|
||||
/// reading it. The web host bridge uses it for a file its page sent, which then saves back to
|
||||
/// that name like a file opened on the desktop.
|
||||
pub fn loaded_file(path: &str, bytes: &[u8]) -> Result<LoadedFile> {
|
||||
let (doc, damaged) = open_checked(path, bytes)?;
|
||||
let native = deckcraft_format::sniff(bytes) || deckcraft_pptx::sniff(bytes);
|
||||
let (doc, damaged) = open_checked(path, &bytes)?;
|
||||
let native = deckcraft_format::sniff(&bytes) || deckcraft_pptx::sniff(&bytes);
|
||||
Ok(LoadedFile { path: path.to_string(), doc, native, damaged })
|
||||
}
|
||||
|
||||
@@ -352,7 +345,6 @@ pub fn save_bytes(doc: &Presentation, format: &str) -> Result<Vec<u8>> {
|
||||
"rtf" => Ok(deckcraft_format::rtf::slides_to_rtf(doc).into_bytes()),
|
||||
"pdf" => deckcraft_pdf::export(doc, &Default::default()).map_err(|e| EngineError::Other(e.to_string())),
|
||||
"gif" => gif_bytes(doc, &Value::Null),
|
||||
"png" | "jpeg" | "jpg" => Err(EngineError::Other("saving a presentation as a single image isn't supported; use file.export".into())),
|
||||
_ => deckcraft_format::save(doc).map_err(|e| EngineError::Other(e.to_string())),
|
||||
}
|
||||
}
|
||||
@@ -367,13 +359,7 @@ pub fn pdf_options(p: &Value, count: usize) -> Result<deckcraft_pdf::PdfOptions>
|
||||
let mut o = deckcraft_pdf::PdfOptions { layout: deckcraft_pdf::PageLayout::Slides, ..Default::default() };
|
||||
o.layout = match str_param(p, "layout").unwrap_or("slides") {
|
||||
"notes" => deckcraft_pdf::PageLayout::Notes,
|
||||
"handouts" => {
|
||||
let per_page = opt_usize_param(p, "perPage", "file.export")?.unwrap_or(6);
|
||||
if ![1, 2, 3, 4, 6, 9].contains(&per_page) {
|
||||
return Err(bad("file.export", format!("perPage {per_page} isn't one of 1, 2, 3, 4, 6, 9")));
|
||||
}
|
||||
deckcraft_pdf::PageLayout::Handouts { per_page: per_page as u8 }
|
||||
}
|
||||
"handouts" => deckcraft_pdf::PageLayout::Handouts { per_page: usize_param(p, "perPage").unwrap_or(6).clamp(1, 9) as u8 },
|
||||
_ => deckcraft_pdf::PageLayout::Slides,
|
||||
};
|
||||
o.dpi = f64_or(p, "dpi", o.dpi).clamp(36.0, 600.0);
|
||||
@@ -404,7 +390,6 @@ pub fn pdf_options(p: &Value, count: usize) -> Result<deckcraft_pdf::PdfOptions>
|
||||
pub fn parse_slide_range(text: &str, count: usize) -> Result<Vec<usize>> {
|
||||
let invalid = || bad("file.export", format!("“{text}” isn't a valid slide range: use numbers and ranges such as 1-3, 5"));
|
||||
let mut out: Vec<usize> = vec![];
|
||||
let mut seen: std::collections::HashSet<usize> = std::collections::HashSet::new();
|
||||
for part in text.split([',', ';']).map(str::trim).filter(|p| !p.is_empty()) {
|
||||
let (a, b) = match part.split_once('-') {
|
||||
Some((a, b)) => (a.trim(), b.trim()),
|
||||
@@ -420,7 +405,7 @@ pub fn parse_slide_range(text: &str, count: usize) -> Result<Vec<usize>> {
|
||||
return Err(bad("file.export", format!("the presentation has {count} slides; “{part}” is outside them")));
|
||||
}
|
||||
for i in lo - 1..hi {
|
||||
if seen.insert(i) {
|
||||
if !out.contains(&i) {
|
||||
out.push(i);
|
||||
}
|
||||
}
|
||||
@@ -477,10 +462,7 @@ pub fn format_for_path(path: &str) -> &'static str {
|
||||
}
|
||||
}
|
||||
|
||||
/// The active presentation is saved as it is now: no unsaved changes, and `path` (when given)
|
||||
/// becomes its file. `file.save` calls it after writing; the web host bridge calls it once the
|
||||
/// page has the bytes.
|
||||
pub fn mark_saved(s: &mut Session, path: Option<String>) -> Result<()> {
|
||||
fn mark_saved(s: &mut Session, path: Option<String>) -> Result<()> {
|
||||
let st = s.doc_mut()?;
|
||||
st.saved_doc = st.doc.clone();
|
||||
if path.is_some() {
|
||||
@@ -787,16 +769,6 @@ mod tests {
|
||||
|
||||
use super::{parse_slide_range, write_file};
|
||||
|
||||
#[test]
|
||||
fn broad_and_repeated_slide_ranges_expand_once() {
|
||||
// 200 copies of a 100,000-slide range: each index once, in order, without a scan per index.
|
||||
let text = vec!["1-100000"; 200].join(",");
|
||||
let v = parse_slide_range(&text, 100_000).unwrap();
|
||||
assert_eq!(v.len(), 100_000);
|
||||
assert!(v.windows(2).all(|w| w[0] < w[1]));
|
||||
assert_eq!(parse_slide_range("3, 1-3, 2", 3).unwrap(), vec![2, 0, 1]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn slide_range_rejects_plus_sign() {
|
||||
assert!(parse_slide_range("+2", 3).is_err());
|
||||
|
||||
@@ -8,7 +8,7 @@ use deckcraft_model::{ColorRef, ColorTransform, Effects, Fill, Line, SchemeSlot,
|
||||
use serde_json::{Value, json};
|
||||
use std::sync::Arc;
|
||||
|
||||
use super::text::{format_body, format_para, format_run, format_run_levels};
|
||||
use super::text::{format_body, format_para, format_run};
|
||||
use super::{
|
||||
CommandSpec, always, bad, base64_decode, bool_or, bool_param, cmd, color_param, edit_shapes, f64_or, f64_param, has_doc, has_text,
|
||||
has_text_or_shapes, ids_param, ok, str_param,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
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, format_run_levels, json, ok, on_param, resolve, str_param,
|
||||
bool_param, color_param, current_run, ed, edit_shapes, f64_param, format_run, json, ok, on_param, resolve, str_param,
|
||||
};
|
||||
|
||||
pub(super) fn bold(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
@@ -55,18 +55,12 @@ pub(super) fn size(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
format_run(s, &move |r| r.size = Some(v))
|
||||
}
|
||||
|
||||
/// PowerPoint's font size list, as the Font Size box lists it (grow/shrink steps through it).
|
||||
pub const SIZES: [f64; 24] =
|
||||
[8.0, 9.0, 10.0, 10.5, 11.0, 12.0, 14.0, 16.0, 18.0, 20.0, 24.0, 28.0, 32.0, 36.0, 40.0, 44.0, 48.0, 54.0, 60.0, 66.0, 72.0, 80.0, 88.0, 96.0];
|
||||
/// PowerPoint's font size list (grow/shrink steps through it).
|
||||
pub const SIZES: [f64; 20] = [8.0, 9.0, 10.0, 10.5, 11.0, 12.0, 14.0, 16.0, 18.0, 20.0, 24.0, 28.0, 32.0, 36.0, 40.0, 44.0, 48.0, 54.0, 60.0, 66.0];
|
||||
|
||||
/// Grow steps up the list, then 8 points at a time past its top; shrink steps back down the same
|
||||
/// way. A size past the top used to shrink straight to 66 (#2059).
|
||||
fn step(cur: f64, up: bool) -> f64 {
|
||||
let top = SIZES[SIZES.len() - 1];
|
||||
if up {
|
||||
SIZES.iter().copied().find(|v| *v > cur + 0.01).unwrap_or((cur + 8.0).min(4000.0))
|
||||
} else if cur - 0.01 > top {
|
||||
(cur - 8.0).max(top)
|
||||
} else {
|
||||
SIZES.iter().rev().copied().find(|v| *v < cur - 0.01).unwrap_or((cur - 1.0).max(1.0))
|
||||
}
|
||||
@@ -78,18 +72,10 @@ fn grow_shrink(s: &mut Session, up: bool) -> Result<Value> {
|
||||
let ctx_sizes = current_run(s).size.unwrap_or(18.0);
|
||||
let _ = st;
|
||||
let base = ctx_sizes;
|
||||
// A layout/master level steps from its own effective size, not the first run's.
|
||||
format_run_levels(
|
||||
s,
|
||||
&move |r| {
|
||||
let cur = r.size.unwrap_or(base);
|
||||
r.size = Some(step(cur, up));
|
||||
},
|
||||
&move |r, eff| {
|
||||
let cur = r.size.or(eff.size).unwrap_or(18.0);
|
||||
r.size = Some(step(cur, up));
|
||||
},
|
||||
)
|
||||
format_run(s, &move |r| {
|
||||
let cur = r.size.unwrap_or(base);
|
||||
r.size = Some(step(cur, up));
|
||||
})
|
||||
}
|
||||
|
||||
pub(super) fn grow(s: &mut Session, _p: &Value) -> Result<Value> {
|
||||
@@ -114,7 +100,6 @@ pub(super) fn color(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
edit_shapes(s, &json!({"ids": charts}), "format.color", |sh| {
|
||||
if let ShapeKind::Chart(ch) = &mut sh.kind {
|
||||
ch.text_color = Some(tc.clone());
|
||||
ch.legend_entries.iter_mut().for_each(|e| e.color = None);
|
||||
ch.raw = None;
|
||||
}
|
||||
Ok(())
|
||||
@@ -242,9 +227,6 @@ pub(super) fn language_all(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
|
||||
pub(super) fn highlight(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
let c = color_param(p, "color");
|
||||
if c.is_none() && p.get("color").is_some_and(|v| !v.is_null()) {
|
||||
return Err(bad("format.highlight", "`color` is not a valid colour"));
|
||||
}
|
||||
format_run(s, &move |r| r.highlight = c.clone())
|
||||
}
|
||||
|
||||
|
||||
@@ -14,7 +14,6 @@ pub(super) fn align(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
"justify" | "just" => Align::Justify,
|
||||
"justifyLow" | "justLow" => Align::JustLow,
|
||||
"distributed" | "dist" => Align::Distributed,
|
||||
"thaiDist" => Align::ThaiDistributed,
|
||||
_ => Align::Left,
|
||||
};
|
||||
align_to(s, a)
|
||||
@@ -46,18 +45,12 @@ pub(super) fn bullets(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
format_para(s, &move |para| {
|
||||
if on {
|
||||
para.props.bullet = Some(Bullet::Char { char: ch.clone() });
|
||||
if explicit_char {
|
||||
para.props.bullet_font = Some("Arial".into());
|
||||
para.props.bullet_font_follow_text = None;
|
||||
}
|
||||
para.props.bullet_font = if explicit_char { Some("Arial".into()) } else { para.props.bullet_font.clone() };
|
||||
if col.is_some() {
|
||||
para.props.bullet_color = col.clone();
|
||||
para.props.bullet_color_follow_text = None;
|
||||
}
|
||||
if size.is_some() {
|
||||
para.props.bullet_size = size;
|
||||
para.props.bullet_size_points = None;
|
||||
para.props.bullet_size_follow_text = None;
|
||||
}
|
||||
if para.props.indent.is_none_or(|i| i >= 0.0) && para.props.margin_left.is_none_or(|m| m == 0.0) {
|
||||
para.props.margin_left = Some(18.0 + 36.0 * para.level as f64);
|
||||
@@ -83,12 +76,9 @@ pub(super) fn numbering(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
para.props.bullet = Some(Bullet::AutoNum { scheme: scheme.clone(), start_at: start });
|
||||
if col.is_some() {
|
||||
para.props.bullet_color = col.clone();
|
||||
para.props.bullet_color_follow_text = None;
|
||||
}
|
||||
if size.is_some() {
|
||||
para.props.bullet_size = size;
|
||||
para.props.bullet_size_points = None;
|
||||
para.props.bullet_size_follow_text = None;
|
||||
}
|
||||
para.props.margin_left = Some(27.0 + 36.0 * para.level as f64);
|
||||
para.props.indent = Some(-27.0);
|
||||
@@ -120,7 +110,13 @@ pub(super) fn line_spacing(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
}
|
||||
|
||||
pub(super) fn paragraph(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
let a = str_param(p, "align").map(|a| parse_align("format.paragraph", a)).transpose()?;
|
||||
let a = str_param(p, "align").map(|a| match a {
|
||||
"center" => Align::Center,
|
||||
"right" => Align::Right,
|
||||
"justify" => Align::Justify,
|
||||
"distributed" => Align::Distributed,
|
||||
_ => Align::Left,
|
||||
});
|
||||
let ml = f64_param(p, "indentLeft");
|
||||
let ind = f64_param(p, "indentFirst");
|
||||
let sb = f64_param(p, "spaceBefore");
|
||||
@@ -176,13 +172,12 @@ pub(super) fn clamp_pt(v: f64) -> f64 {
|
||||
fn parse_align(cmd: &str, s: &str) -> Result<Align> {
|
||||
Ok(match s {
|
||||
"left" | "l" => Align::Left,
|
||||
"center" | "centre" | "ctr" => Align::Center,
|
||||
"center" | "ctr" => Align::Center,
|
||||
"right" | "r" => Align::Right,
|
||||
"justify" | "just" => Align::Justify,
|
||||
"justLow" | "justifyLow" => Align::JustLow,
|
||||
"justLow" => Align::JustLow,
|
||||
"distributed" | "dist" => Align::Distributed,
|
||||
"thaiDist" => Align::ThaiDistributed,
|
||||
_ => return Err(bad(cmd, "`align` must be left, center, right, justify, justLow, distributed or thaiDist")),
|
||||
_ => return Err(bad(cmd, "`align` must be left, center, right, justify, justLow or distributed")),
|
||||
})
|
||||
}
|
||||
|
||||
@@ -194,7 +189,6 @@ fn align_name(a: Align) -> &'static str {
|
||||
Align::Justify => "justify",
|
||||
Align::JustLow => "justLow",
|
||||
Align::Distributed => "distributed",
|
||||
Align::ThaiDistributed => "thaiDist",
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -80,12 +80,6 @@ pub(super) fn photo_album_edit(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
let opts = album_opts(p)?;
|
||||
let items = album_items(p)?;
|
||||
let (id, ids) = album_target(s, p)?;
|
||||
// The editor is seeded within the album limits; replacing the album from a trimmed seed would
|
||||
// delete what it left out, so refuse rather than lose it.
|
||||
let st = s.doc()?;
|
||||
if items_from_slides(&st.doc, &album_slides(st, &ids)).1 {
|
||||
return Err(bad("insert.photoAlbumEdit", "this album holds more than the editor can carry; editing it would delete the rest"));
|
||||
}
|
||||
commit_album(s, items, opts, Some((id, ids)))
|
||||
}
|
||||
|
||||
@@ -551,19 +545,6 @@ fn album_target(s: &Session, p: &Value) -> Result<(u32, Vec<u64>)> {
|
||||
Err(bad("insert.photoAlbumEdit", "this slide is not part of a photo album"))
|
||||
}
|
||||
|
||||
/// Whether a slide holds anything besides album items: shapes, notes, comments, a background, a
|
||||
/// transition, animations, a colour map or tags.
|
||||
fn has_slide_content(s: &deckcraft_model::Slide) -> bool {
|
||||
!s.shapes.is_empty()
|
||||
|| !s.notes.text().trim().is_empty()
|
||||
|| !s.comments.is_empty()
|
||||
|| s.background.is_some()
|
||||
|| s.transition.is_some()
|
||||
|| !s.animations.is_empty()
|
||||
|| s.color_map.is_some()
|
||||
|| !s.tags.is_empty()
|
||||
}
|
||||
|
||||
fn pic_count(slide: &deckcraft_model::Slide) -> usize {
|
||||
slide.shapes.iter().filter(|sh| matches!(sh.kind, ShapeKind::Picture { .. })).count()
|
||||
}
|
||||
@@ -611,16 +592,9 @@ fn infer_album(slides: &[&deckcraft_model::Slide], theme_now: &str) -> (String,
|
||||
(layout, frame.into(), captions, any && gray, theme_now.to_string())
|
||||
}
|
||||
|
||||
/// The album's items for the dialog, within the limits `insert.photoAlbum` itself takes (items,
|
||||
/// bytes per picture and in all), checked before a picture is encoded: an album slide set can
|
||||
/// name one large picture many times.
|
||||
///
|
||||
/// The flag is true when something was left out for a limit.
|
||||
fn items_from_slides(doc: &deckcraft_model::Presentation, slides: &[&deckcraft_model::Slide]) -> (Vec<Value>, bool) {
|
||||
fn items_from_slides(doc: &deckcraft_model::Presentation, slides: &[&deckcraft_model::Slide]) -> Vec<Value> {
|
||||
let mut items = Vec::new();
|
||||
let mut total = 0usize;
|
||||
let mut omitted = false;
|
||||
'slides: for slide in slides {
|
||||
for slide in slides {
|
||||
let pics: Vec<&deckcraft_model::Shape> = slide.shapes.iter().filter(|sh| matches!(sh.kind, ShapeKind::Picture { .. })).collect();
|
||||
if pics.is_empty() {
|
||||
let text = slide
|
||||
@@ -631,27 +605,13 @@ fn items_from_slides(doc: &deckcraft_model::Presentation, slides: &[&deckcraft_m
|
||||
.find(|t| !t.trim().is_empty())
|
||||
.unwrap_or_default();
|
||||
if !text.trim().is_empty() {
|
||||
if items.len() >= ALBUM_MAX_ITEMS {
|
||||
omitted = true;
|
||||
break 'slides;
|
||||
}
|
||||
items.push(json!({"text": text.trim()}));
|
||||
} else if items.len() >= ALBUM_MAX_ITEMS && has_slide_content(slide) {
|
||||
// Past the item limit, a slide with other content would be deleted by a replace.
|
||||
omitted = true;
|
||||
break 'slides;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
for sh in pics {
|
||||
let ShapeKind::Picture { fill } = &sh.kind else { continue };
|
||||
let media = doc.media(fill.media);
|
||||
let size = media.map_or(0, |m| m.data.len());
|
||||
if items.len() >= ALBUM_MAX_ITEMS || size > ALBUM_MAX_BYTES || total.saturating_add(size) > ALBUM_MAX_TOTAL {
|
||||
omitted = true;
|
||||
break 'slides;
|
||||
}
|
||||
total += size;
|
||||
let name = if !sh.descr.is_empty() {
|
||||
sh.descr.clone()
|
||||
} else {
|
||||
@@ -666,19 +626,14 @@ fn items_from_slides(doc: &deckcraft_model::Presentation, slides: &[&deckcraft_m
|
||||
}));
|
||||
}
|
||||
}
|
||||
(items, omitted)
|
||||
}
|
||||
|
||||
/// The slides of album `ids` as the editor would seed them.
|
||||
fn album_slides<'a>(st: &'a crate::DocState, ids: &[u64]) -> Vec<&'a deckcraft_model::Slide> {
|
||||
ids.iter().filter_map(|sid| u32::try_from(*sid).ok()).filter_map(|n| st.doc.slide(deckcraft_model::SlideId(n))).collect()
|
||||
items
|
||||
}
|
||||
|
||||
fn album_seed(s: &Session) -> Result<Value> {
|
||||
let (id, ids) = album_target(s, &Value::Null)?;
|
||||
let st = s.doc()?;
|
||||
let slides = album_slides(st, &ids);
|
||||
let (items, _) = items_from_slides(&st.doc, &slides);
|
||||
let slides: Vec<&deckcraft_model::Slide> =
|
||||
ids.iter().filter_map(|sid| u32::try_from(*sid).ok()).filter_map(|n| st.doc.slide(deckcraft_model::SlideId(n))).collect();
|
||||
let theme_now = st.doc.masters.first().map(|m| m.theme.name.clone()).filter(|n| !n.is_empty()).unwrap_or_else(|| "DeckCraft".into());
|
||||
let inferred = infer_album(&slides, &theme_now);
|
||||
let mark = slides.first().and_then(|sl| sl.raw_ext.as_deref().and_then(parse_album_mark));
|
||||
@@ -700,7 +655,7 @@ fn album_seed(s: &Session) -> Result<Value> {
|
||||
"captions": captions,
|
||||
"grayscale": grayscale,
|
||||
"theme": theme,
|
||||
"items": items,
|
||||
"items": items_from_slides(&st.doc, &slides),
|
||||
}))
|
||||
}
|
||||
|
||||
@@ -767,96 +722,3 @@ fn insert_album(s: &mut Session, items: Vec<AlbumItem>, opts: &AlbumOpts, at: Op
|
||||
s.execute("slide.go", &json!({"index": first}))?;
|
||||
Ok(json!({"slides": slides, "first": first, "pictures": pictures, "album": album_id}))
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod seed_tests {
|
||||
use std::sync::Arc;
|
||||
|
||||
use deckcraft_model::style::PictureFill;
|
||||
use deckcraft_model::{Shape, ShapeId, defaults};
|
||||
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn reopening_an_album_seeds_at_most_the_items_an_album_takes() {
|
||||
// One picture named by 500 shapes: the dialog gets 48 items, not 500 encoded copies.
|
||||
let mut p = defaults::new_presentation(None);
|
||||
let img = p.add_media("a.png", "image/png", vec![1; 64 * 1024]).unwrap();
|
||||
let pic = |k: u32| Shape {
|
||||
id: ShapeId(10_000 + k),
|
||||
kind: ShapeKind::Picture { fill: PictureFill { media: img, ..Default::default() } },
|
||||
..Default::default()
|
||||
};
|
||||
Arc::make_mut(&mut p.slides[0]).shapes = (0..500).map(pic).collect();
|
||||
let slides: Vec<&deckcraft_model::Slide> = p.slides.iter().map(|s| s.as_ref()).collect();
|
||||
let (items, omitted) = items_from_slides(&p, &slides);
|
||||
assert_eq!(items.len(), ALBUM_MAX_ITEMS);
|
||||
assert!(omitted);
|
||||
let few: Vec<&deckcraft_model::Slide> = Vec::new();
|
||||
assert!(!items_from_slides(&p, &few).1);
|
||||
}
|
||||
|
||||
/// An album too big for the editor's seed cannot be replaced from that seed: the replace would
|
||||
/// delete what the seed left out.
|
||||
#[test]
|
||||
fn editing_an_album_the_editor_cannot_hold_is_refused() {
|
||||
let mut s = crate::Session::new();
|
||||
s.execute("file.new", &json!({})).unwrap();
|
||||
let texts: Vec<Value> = (0..ALBUM_MAX_ITEMS).map(|i| json!({"text": format!("t{i}")})).collect();
|
||||
s.execute("insert.photoAlbum", &json!({"pictures": texts, "layout": "1"})).unwrap();
|
||||
let st = s.doc_mut().unwrap();
|
||||
let doc = Arc::make_mut(&mut st.doc);
|
||||
let mut extra = doc.slides.last().unwrap().as_ref().clone();
|
||||
extra.id = deckcraft_model::SlideId(doc.alloc_id().unwrap());
|
||||
doc.slides.push(Arc::new(extra));
|
||||
let n = doc.slides.len();
|
||||
let e = s.execute("insert.photoAlbumEdit", &json!({"pictures": [{"text": "x"}], "layout": "1"})).unwrap_err().to_string();
|
||||
assert!(e.contains("delete the rest"), "{e}");
|
||||
assert_eq!(s.doc().unwrap().doc.slides.len(), n);
|
||||
}
|
||||
|
||||
/// Past the item limit, a slide holding only a drawing counts as content the replace would delete.
|
||||
#[test]
|
||||
fn editing_an_album_with_a_drawing_slide_past_the_limit_is_refused() {
|
||||
let mut s = crate::Session::new();
|
||||
s.execute("file.new", &json!({})).unwrap();
|
||||
let texts: Vec<Value> = (0..ALBUM_MAX_ITEMS).map(|i| json!({"text": format!("t{i}")})).collect();
|
||||
s.execute("insert.photoAlbum", &json!({"pictures": texts, "layout": "1"})).unwrap();
|
||||
let st = s.doc_mut().unwrap();
|
||||
let doc = Arc::make_mut(&mut st.doc);
|
||||
let mut extra = doc.slides.last().unwrap().as_ref().clone();
|
||||
extra.id = deckcraft_model::SlideId(doc.alloc_id().unwrap());
|
||||
extra.shapes = vec![Shape { id: ShapeId(90_000), ..Default::default() }];
|
||||
doc.slides.push(Arc::new(extra));
|
||||
let e = s.execute("insert.photoAlbumEdit", &json!({"pictures": [{"text": "x"}], "layout": "1"})).unwrap_err().to_string();
|
||||
assert!(e.contains("delete the rest"), "{e}");
|
||||
}
|
||||
|
||||
/// Notes or comments on a shapeless slide past the limit are content a replace would delete.
|
||||
#[test]
|
||||
fn editing_an_album_with_a_notes_only_slide_past_the_limit_is_refused() {
|
||||
let mut s = crate::Session::new();
|
||||
s.execute("file.new", &json!({})).unwrap();
|
||||
let texts: Vec<Value> = (0..ALBUM_MAX_ITEMS).map(|i| json!({"text": format!("t{i}")})).collect();
|
||||
s.execute("insert.photoAlbum", &json!({"pictures": texts, "layout": "1"})).unwrap();
|
||||
let st = s.doc_mut().unwrap();
|
||||
let doc = Arc::make_mut(&mut st.doc);
|
||||
let mut extra = doc.slides.last().unwrap().as_ref().clone();
|
||||
extra.id = deckcraft_model::SlideId(doc.alloc_id().unwrap());
|
||||
extra.shapes.clear();
|
||||
extra.notes = deckcraft_model::text::TextBody::from_text("speaker notes");
|
||||
doc.slides.push(Arc::new(extra));
|
||||
let e = s.execute("insert.photoAlbumEdit", &json!({"pictures": [{"text": "x"}], "layout": "1"})).unwrap_err().to_string();
|
||||
assert!(e.contains("delete the rest"), "{e}");
|
||||
}
|
||||
|
||||
/// A shapeless slide with a background, transition, colour map or tags counts as content; a plain empty one does not.
|
||||
#[test]
|
||||
fn slide_level_settings_count_as_slide_content() {
|
||||
use deckcraft_model::Slide;
|
||||
assert!(!has_slide_content(&Slide::default()));
|
||||
assert!(has_slide_content(&Slide { tags: vec![("k".into(), "v".into())], ..Default::default() }));
|
||||
assert!(has_slide_content(&Slide { color_map: Some(vec![("bg1".into(), "lt1".into())]), ..Default::default() }));
|
||||
assert!(has_slide_content(&Slide { transition: Some(Default::default()), ..Default::default() }));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -190,7 +190,6 @@ pub(super) fn chart_options(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
with_chart(s, p, "chart.options", |c| {
|
||||
if let Some(t) = &text_color {
|
||||
c.text_color = t.clone();
|
||||
c.legend_entries.iter_mut().for_each(|e| e.color = None);
|
||||
}
|
||||
apply_bin_options(c, bin_count, bin_size);
|
||||
apply_subtotals_and_bars(c, p, subtotals.as_deref());
|
||||
|
||||
@@ -1,18 +1,16 @@
|
||||
//! Inspection for agents: the deck, slides and shapes as JSON (verify work without screenshots).
|
||||
|
||||
use deckcraft_model::resolve::{self, Ctx};
|
||||
use deckcraft_model::text::Hyperlink;
|
||||
use deckcraft_model::{Shape, ShapeKind};
|
||||
use serde_json::{Value, json};
|
||||
|
||||
use super::{CommandSpec, always, bad, cmd, has_doc, id_param, opt_usize_param, str_param, usize_param, xfrm_of};
|
||||
use super::{CommandSpec, always, bad, cmd, has_doc, id_param, str_param, usize_param, xfrm_of};
|
||||
use crate::{Result, Session};
|
||||
|
||||
pub fn specs() -> Vec<CommandSpec> {
|
||||
vec![
|
||||
cmd!(query "document.inspect", "Inspect Presentation", [], None, "{} → slides with titles, layouts, shape counts; selection; size; theme", has_doc, document),
|
||||
cmd!(query "slide.inspect", "Inspect Slide", [], None, "{index?} → the slide's shapes with ids, kinds, boxes, text, fills, animations, transition, notes (text, and notesBody with its formatting)", has_doc, slide),
|
||||
cmd!(query "slide.actions", "Inspect Slide Actions", [], None, "{index?} → {index, slideId, actions: [{owner, event, type, target, hyperlink}]}; lists shape click/hover actions and text-run links/hover actions (including table cells) without executing them", has_doc, actions),
|
||||
cmd!(query "shape.inspect", "Inspect Shape", [], None, "{id} → full shape JSON plus its effective box", has_doc, shape),
|
||||
cmd!(query "commands.list", "List Commands", [], None, "{filter?: substring} → [{id, label, params, enabled}]", always, commands),
|
||||
]
|
||||
@@ -137,70 +135,6 @@ fn slide(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
}))
|
||||
}
|
||||
|
||||
fn actions(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
let st = s.doc()?;
|
||||
let i = opt_usize_param(p, "index", "slide.actions")?.unwrap_or(st.selection.slide);
|
||||
let sl = st.doc.slides.get(i).ok_or_else(|| bad("slide.actions", format!("no slide {i}")))?;
|
||||
let mut found = Vec::new();
|
||||
for sh in &sl.shapes {
|
||||
collect_actions(sh, &mut Vec::new(), &mut found);
|
||||
}
|
||||
Ok(json!({"index": i, "slideId": sl.id, "actions": found}))
|
||||
}
|
||||
|
||||
fn collect_actions(sh: &Shape, path: &mut Vec<Value>, found: &mut Vec<Value>) {
|
||||
path.push(json!({"id": sh.id, "name": sh.name}));
|
||||
let owner = json!({"shapeId": sh.id, "shapeName": sh.name, "path": path});
|
||||
if let Some(link) = &sh.click {
|
||||
found.push(action_entry(owner.clone(), "click", link));
|
||||
}
|
||||
if let Some(link) = &sh.hover {
|
||||
found.push(action_entry(owner.clone(), "hover", link));
|
||||
}
|
||||
if let Some(text) = &sh.text {
|
||||
collect_text_actions(text, &owner, found);
|
||||
}
|
||||
if let ShapeKind::Table(table) = &sh.kind {
|
||||
for (row_index, row) in table.rows.iter().enumerate() {
|
||||
for (cell_index, cell) in row.cells.iter().enumerate() {
|
||||
let mut cell_owner = owner.clone();
|
||||
cell_owner["tableRowIndex"] = json!(row_index);
|
||||
cell_owner["tableCellIndex"] = json!(cell_index);
|
||||
collect_text_actions(&cell.text, &cell_owner, found);
|
||||
}
|
||||
}
|
||||
}
|
||||
if let ShapeKind::Group { children, .. } = &sh.kind {
|
||||
for child in children {
|
||||
collect_actions(child, path, found);
|
||||
}
|
||||
}
|
||||
path.pop();
|
||||
}
|
||||
|
||||
fn collect_text_actions(text: &deckcraft_model::text::TextBody, owner: &Value, found: &mut Vec<Value>) {
|
||||
for (paragraph_index, paragraph) in text.paragraphs.iter().enumerate() {
|
||||
for (run_index, run) in paragraph.runs.iter().enumerate() {
|
||||
for (event, link) in [("textLink", run.props.link.as_ref()), ("textHover", run.props.hover.as_ref())] {
|
||||
if let Some(link) = link {
|
||||
let mut text_owner = owner.clone();
|
||||
text_owner["paragraphIndex"] = json!(paragraph_index);
|
||||
text_owner["runIndex"] = json!(run_index);
|
||||
text_owner["text"] = json!(run.content());
|
||||
found.push(action_entry(text_owner, event, link));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn action_entry(owner: Value, event: &str, link: &Hyperlink) -> Value {
|
||||
let action = serde_json::to_value(&link.action).unwrap_or_default();
|
||||
let kind = action.get("kind").cloned().unwrap_or(Value::Null);
|
||||
let target = ["url", "slide", "name", "path"].iter().find_map(|key| action.get(key).cloned()).unwrap_or(Value::Null);
|
||||
json!({"owner": owner, "event": event, "type": kind, "target": target, "hyperlink": link})
|
||||
}
|
||||
|
||||
fn shape(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
let st = s.doc()?;
|
||||
let id =
|
||||
@@ -220,82 +154,3 @@ fn commands(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
.collect(),
|
||||
))
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use deckcraft_geom::Xfrm;
|
||||
use deckcraft_model::table::Row;
|
||||
use deckcraft_model::text::{Action, Paragraph, Run, RunProps, TextBody};
|
||||
use deckcraft_model::{Cell, Shape, ShapeId, ShapeKind, Table};
|
||||
|
||||
fn link(action: Action) -> Hyperlink {
|
||||
Hyperlink { action, tooltip: String::new(), highlight_click: false, sound: None, stop_sound: false, zoom_return: None }
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn slide_actions_lists_group_shape_and_text_actions() {
|
||||
let mut s = Session::with_new();
|
||||
let mut child = Shape { id: ShapeId(12), name: "Child".into(), ..Default::default() };
|
||||
child.click = Some(link(Action::NextSlide));
|
||||
child.hover = Some(link(Action::Url { url: "https://example.com/hover".into() }));
|
||||
let run_props = RunProps { link: Some(link(Action::Slide { slide: deckcraft_model::SlideId(7) })), ..Default::default() };
|
||||
let mut paragraph = Paragraph::new("linked");
|
||||
paragraph.runs = vec![Run::with("linked", run_props)];
|
||||
child.text = Some(TextBody { paragraphs: vec![paragraph], ..Default::default() });
|
||||
let group = Shape {
|
||||
id: ShapeId(11),
|
||||
name: "Group".into(),
|
||||
kind: ShapeKind::Group { children: vec![child], child: Xfrm::default() },
|
||||
..Default::default()
|
||||
};
|
||||
let st = s.doc_mut().unwrap();
|
||||
let slide_index = st.selection.slide;
|
||||
let presentation = std::sync::Arc::make_mut(&mut st.doc);
|
||||
std::sync::Arc::make_mut(&mut presentation.slides[slide_index]).shapes = vec![group];
|
||||
|
||||
let v = s.execute("slide.actions", &json!({})).unwrap();
|
||||
let actions = v["actions"].as_array().unwrap();
|
||||
assert_eq!(actions.len(), 3, "{v}");
|
||||
assert_eq!(actions[0]["event"], "click");
|
||||
assert_eq!(actions[0]["type"], "nextSlide");
|
||||
assert_eq!(actions[0]["owner"]["path"][0]["name"], "Group");
|
||||
assert_eq!(actions[0]["owner"]["path"][1]["id"], 12);
|
||||
assert_eq!(actions[1]["event"], "hover");
|
||||
assert_eq!(actions[1]["target"], "https://example.com/hover");
|
||||
assert_eq!(actions[2]["event"], "textLink");
|
||||
assert_eq!(actions[2]["owner"]["runIndex"], 0);
|
||||
assert_eq!(actions[2]["target"], 7);
|
||||
|
||||
let cell_props = RunProps { hover: Some(link(Action::Url { url: "https://example.com/cell".into() })), ..Default::default() };
|
||||
let mut cell_text = TextBody::from_text("cell link");
|
||||
cell_text.paragraphs[0].runs = vec![Run::with("cell link", cell_props)];
|
||||
let table =
|
||||
Table { rows: vec![Row { cells: vec![Cell { text: cell_text, ..Default::default() }], ..Default::default() }], ..Default::default() };
|
||||
let table_shape = Shape { id: ShapeId(13), name: "Table".into(), kind: ShapeKind::Table(table), ..Default::default() };
|
||||
let st = s.doc_mut().unwrap();
|
||||
let presentation = std::sync::Arc::make_mut(&mut st.doc);
|
||||
std::sync::Arc::make_mut(&mut presentation.slides[slide_index]).shapes.push(table_shape);
|
||||
let v = s.execute("slide.actions", &json!({})).unwrap();
|
||||
let table_action = v["actions"].as_array().unwrap().last().cloned().unwrap();
|
||||
assert_eq!(table_action["event"], "textHover");
|
||||
assert_eq!(table_action["owner"]["tableRowIndex"], 0);
|
||||
assert_eq!(table_action["owner"]["tableCellIndex"], 0);
|
||||
assert_eq!(table_action["target"], "https://example.com/cell");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn slide_actions_returns_empty_array_for_slide_without_actions() {
|
||||
let mut s = Session::with_new();
|
||||
let v = s.execute("slide.actions", &json!({})).unwrap();
|
||||
assert_eq!(v["actions"], json!([]));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn slide_actions_refuses_a_bad_index_instead_of_using_the_selection() {
|
||||
let mut s = Session::with_new();
|
||||
for index in [json!(-1), json!("0"), json!(1.5)] {
|
||||
assert!(s.execute("slide.actions", &json!({"index": index})).is_err(), "{index}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -246,17 +246,8 @@ fn merge(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
list.insert(at + k, sh);
|
||||
}
|
||||
list.retain(|x| !ids.contains(&x.id));
|
||||
let _ = list;
|
||||
if sel.target == crate::Target::Slides
|
||||
&& let Some(sl) = doc.slides.get_mut(sel.slide)
|
||||
{
|
||||
let sl = std::sync::Arc::make_mut(sl);
|
||||
sl.animations.retain(|a| !ids.contains(&a.shape));
|
||||
for a in sl.animations.iter_mut().filter(|a| a.trigger.is_some_and(|t| ids.contains(&t))) {
|
||||
a.trigger = None;
|
||||
}
|
||||
}
|
||||
sel.shapes = new_ids.clone();
|
||||
let _ = list;
|
||||
Ok(())
|
||||
})?;
|
||||
Ok(json!({"ids": new_ids.iter().map(|i| i.0).collect::<Vec<_>>()}))
|
||||
@@ -347,36 +338,4 @@ mod tests {
|
||||
assert!(matches!(out.kind, ShapeKind::Shape) && out.text.is_none());
|
||||
assert!(matches!(out.fill, Some(deckcraft_model::style::Fill::Solid { .. })), "{:?}", out.fill);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn merge_cleans_animation_targets_and_triggers_for_every_operation_and_undo_restores_them() {
|
||||
for op in ["union", "combine", "fragment", "intersect", "subtract"] {
|
||||
let mut s = Session::with_new();
|
||||
let first = s.execute("shape.insert", &json!({"preset": "rect", "rect": [0, 0, 100, 100]})).expect("first shape")["id"]
|
||||
.as_u64()
|
||||
.expect("id") as u32;
|
||||
let second = s.execute("shape.insert", &json!({"preset": "rect", "rect": [50, 0, 100, 100]})).expect("second shape")["id"]
|
||||
.as_u64()
|
||||
.expect("id") as u32;
|
||||
let unrelated = s.execute("shape.insert", &json!({"preset": "rect", "rect": [200, 0, 100, 100]})).expect("unrelated shape")["id"]
|
||||
.as_u64()
|
||||
.expect("id") as u32;
|
||||
s.execute("animation.add", &json!({"effect": "fade", "ids": [second]})).expect("operand animation");
|
||||
s.execute("animation.add", &json!({"effect": "spin", "ids": [unrelated]})).expect("unrelated animation");
|
||||
s.execute("animation.timing", &json!({"index": 1, "trigger": first})).expect("trigger animation");
|
||||
|
||||
s.execute("shape.merge", &json!({"op": op, "ids": [first, second]})).expect("merge");
|
||||
let animations = &s.doc().expect("doc").doc.slides[0].animations;
|
||||
assert_eq!(animations.len(), 1, "{op} removed animations for merged operands");
|
||||
assert_eq!(animations[0].shape.0, unrelated, "{op} preserved the unrelated effect");
|
||||
assert_eq!(animations[0].trigger, None, "{op} cleared the removed shape trigger");
|
||||
|
||||
s.execute("edit.undo", &json!({})).expect("undo merge");
|
||||
let animations = &s.doc().expect("doc").doc.slides[0].animations;
|
||||
assert_eq!(animations.len(), 2, "{op} undo restored removed effects");
|
||||
assert_eq!(animations[0].shape.0, second);
|
||||
assert_eq!(animations[1].shape.0, unrelated);
|
||||
assert_eq!(animations[1].trigger.map(|id| id.0), Some(first));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -135,14 +135,7 @@ pub fn apply(sh: &mut Shape, id: &str) -> bool {
|
||||
let e = Effects {
|
||||
outer_shadow: lk.shadow.map(shadow),
|
||||
soft_edge: lk.soft_edge,
|
||||
reflection: lk.reflection.then_some(Reflection {
|
||||
blur: 0.5,
|
||||
start_alpha: 0.5,
|
||||
end_alpha: 0.0,
|
||||
end_pos: 0.35,
|
||||
dist: 0.0,
|
||||
..Default::default()
|
||||
}),
|
||||
reflection: lk.reflection.then_some(Reflection { blur: 0.5, start_alpha: 0.5, end_alpha: 0.0, end_pos: 0.35, dist: 0.0 }),
|
||||
bevel: lk.bevel.map(str::to_string),
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
@@ -6,155 +6,16 @@
|
||||
//! renumbered with animations, triggers and connector ends following them. Links to another
|
||||
//! reused slide follow its copy.
|
||||
|
||||
use std::collections::{HashMap, HashSet};
|
||||
use std::collections::HashMap;
|
||||
use std::sync::Arc;
|
||||
|
||||
use deckcraft_model::table::TableStyleDef;
|
||||
use deckcraft_model::text::{Action, Hyperlink, ListStyle, RunProps, Spacing, TextBody};
|
||||
use deckcraft_model::text::{Action, Hyperlink, RunProps, TextBody};
|
||||
use deckcraft_model::{LayoutId, Master, MasterId, MediaId, Presentation, ShapeId, ShapeKind, Slide, SlideId};
|
||||
use serde_json::{Value, json};
|
||||
|
||||
use super::{CommandSpec, NO_ID_SPACE, bad, bool_or, cmd, has_doc, usize_param};
|
||||
use crate::{Result, Session, media_refs};
|
||||
|
||||
/// Scale physical measurements in imported content to the destination slide's coordinate space.
|
||||
/// Text has one size, so it uses the geometric mean of the horizontal and vertical scale.
|
||||
fn scale_text(t: &mut TextBody, sx: f64, sy: f64) {
|
||||
let unit = (sx * sy).sqrt();
|
||||
for v in [&mut t.body.inset_l, &mut t.body.inset_r, &mut t.body.col_spacing].into_iter().flatten() {
|
||||
*v *= sx;
|
||||
}
|
||||
for v in [&mut t.body.inset_t, &mut t.body.inset_b].into_iter().flatten() {
|
||||
*v *= sy;
|
||||
}
|
||||
scale_list(&mut t.list_style, sx, unit);
|
||||
for p in &mut t.paragraphs {
|
||||
for v in [&mut p.props.margin_left, &mut p.props.margin_right, &mut p.props.indent, &mut p.props.default_tab].into_iter().flatten() {
|
||||
*v *= sx;
|
||||
}
|
||||
if let Some(stops) = &mut p.props.tabs {
|
||||
for stop in stops {
|
||||
stop.pos *= sx;
|
||||
}
|
||||
}
|
||||
for spacing in [&mut p.props.line_spacing, &mut p.props.space_before, &mut p.props.space_after] {
|
||||
if let Some(Spacing::Pts(v)) = spacing {
|
||||
*v *= unit;
|
||||
}
|
||||
}
|
||||
if let Some(v) = &mut p.props.bullet_size_points {
|
||||
*v *= unit;
|
||||
}
|
||||
scale_run(&mut p.def_props, unit);
|
||||
scale_run(&mut p.end_props, unit);
|
||||
for r in &mut p.runs {
|
||||
scale_run(&mut r.props, unit);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn scale_list(list: &mut ListStyle, sx: f64, unit: f64) {
|
||||
for level in list.levels.iter_mut().flatten() {
|
||||
for v in
|
||||
[&mut level.para.margin_left, &mut level.para.margin_right, &mut level.para.indent, &mut level.para.default_tab].into_iter().flatten()
|
||||
{
|
||||
*v *= sx;
|
||||
}
|
||||
for spacing in [&mut level.para.line_spacing, &mut level.para.space_before, &mut level.para.space_after] {
|
||||
if let Some(Spacing::Pts(v)) = spacing {
|
||||
*v *= unit;
|
||||
}
|
||||
}
|
||||
if let Some(v) = &mut level.para.bullet_size_points {
|
||||
*v *= unit;
|
||||
}
|
||||
scale_run(&mut level.run, unit);
|
||||
}
|
||||
}
|
||||
|
||||
fn scale_run(run: &mut RunProps, factor: f64) {
|
||||
for v in [&mut run.size, &mut run.spacing, &mut run.kern, &mut run.soft_edge].into_iter().flatten() {
|
||||
*v *= factor;
|
||||
}
|
||||
}
|
||||
|
||||
fn scale_shapes(shapes: &mut [deckcraft_model::Shape], sx: f64, sy: f64, depth: usize) {
|
||||
if depth > 64 {
|
||||
return;
|
||||
}
|
||||
let unit = (sx * sy).sqrt();
|
||||
for shape in shapes {
|
||||
if let Some(x) = &mut shape.xfrm {
|
||||
x.x *= sx;
|
||||
x.w *= sx;
|
||||
x.y *= sy;
|
||||
x.h *= sy;
|
||||
}
|
||||
if let Some(text) = &mut shape.text {
|
||||
scale_text(text, sx, sy);
|
||||
}
|
||||
match &mut shape.kind {
|
||||
ShapeKind::Table(table) => {
|
||||
for col in &mut table.cols {
|
||||
*col *= sx;
|
||||
}
|
||||
for row in &mut table.rows {
|
||||
row.height *= sy;
|
||||
for cell in &mut row.cells {
|
||||
if let Some(margins) = &mut cell.margins {
|
||||
if let Some(v) = margins.get_mut(0) {
|
||||
*v *= sx;
|
||||
}
|
||||
if let Some(v) = margins.get_mut(1) {
|
||||
*v *= sx;
|
||||
}
|
||||
if let Some(v) = margins.get_mut(2) {
|
||||
*v *= sy;
|
||||
}
|
||||
if let Some(v) = margins.get_mut(3) {
|
||||
*v *= sy;
|
||||
}
|
||||
}
|
||||
scale_text(&mut cell.text, sx, sy);
|
||||
}
|
||||
}
|
||||
}
|
||||
ShapeKind::Group { children, child } => {
|
||||
child.x *= sx;
|
||||
child.w *= sx;
|
||||
child.y *= sy;
|
||||
child.h *= sy;
|
||||
scale_shapes(children, sx, sy, depth + 1);
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
if let Some(effects) = &mut shape.effects {
|
||||
for shadow in [&mut effects.outer_shadow, &mut effects.inner_shadow].into_iter().flatten() {
|
||||
shadow.blur *= unit;
|
||||
shadow.dist *= unit;
|
||||
}
|
||||
if let Some(glow) = &mut effects.glow {
|
||||
glow.radius *= unit;
|
||||
}
|
||||
if let Some(reflection) = &mut effects.reflection {
|
||||
reflection.blur *= unit;
|
||||
reflection.dist *= unit;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn scale_master(master: &mut Master, sx: f64, sy: f64) {
|
||||
scale_shapes(&mut master.shapes, sx, sy, 0);
|
||||
let unit = (sx * sy).sqrt();
|
||||
scale_list(&mut master.title_style, sx, unit);
|
||||
scale_list(&mut master.body_style, sx, unit);
|
||||
scale_list(&mut master.other_style, sx, unit);
|
||||
for layout in &mut master.layouts {
|
||||
scale_shapes(&mut layout.shapes, sx, sy, 0);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn specs() -> Vec<CommandSpec> {
|
||||
vec![
|
||||
cmd!(
|
||||
@@ -291,90 +152,6 @@ fn matching_layout(doc: &Presentation, name: &str, kind: deckcraft_model::Layout
|
||||
.unwrap_or_default()
|
||||
}
|
||||
|
||||
fn collect_table_styles(shapes: &[deckcraft_model::Shape], styles: &mut Vec<String>, seen: &mut HashSet<String>, depth: usize) {
|
||||
if depth > 64 {
|
||||
return;
|
||||
}
|
||||
for shape in shapes {
|
||||
if let ShapeKind::Table(table) = &shape.kind
|
||||
&& seen.insert(table.style.to_ascii_lowercase())
|
||||
{
|
||||
styles.push(table.style.clone());
|
||||
}
|
||||
collect_table_styles(shape.children(), styles, seen, depth + 1);
|
||||
}
|
||||
}
|
||||
|
||||
fn remap_table_styles(shapes: &mut [deckcraft_model::Shape], styles: &HashMap<String, String>, depth: usize) {
|
||||
if depth > 64 {
|
||||
return;
|
||||
}
|
||||
for shape in shapes {
|
||||
if let ShapeKind::Table(table) = &mut shape.kind
|
||||
&& let Some(style) = styles.get(&table.style.to_ascii_lowercase())
|
||||
{
|
||||
table.style = style.clone();
|
||||
}
|
||||
if let Some(children) = shape.children_mut() {
|
||||
remap_table_styles(children, styles, depth + 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn same_table_style(a: &TableStyleDef, b: &TableStyleDef) -> bool {
|
||||
a.name == b.name && a.parts == b.parts && a.background == b.background && a.raw == b.raw
|
||||
}
|
||||
|
||||
/// Copy only definitions used on selected slides. Conflicting GUIDs receive a fresh GUID so
|
||||
/// existing destination tables continue to resolve to their original definitions.
|
||||
fn import_table_styles(cmd: &str, doc: &mut Presentation, src: &Presentation, used: &[String]) -> Result<HashMap<String, String>> {
|
||||
let mut map = HashMap::new();
|
||||
let mut source_defs = HashMap::new();
|
||||
let mut source_ids = HashSet::new();
|
||||
for (index, style) in src.table_styles.iter().enumerate() {
|
||||
let key = style.id.to_ascii_lowercase();
|
||||
source_defs.entry(key.clone()).or_insert(index);
|
||||
source_ids.insert(key);
|
||||
}
|
||||
let mut destination_defs = HashMap::new();
|
||||
for (index, style) in doc.table_styles.iter().enumerate() {
|
||||
destination_defs.entry(style.id.to_ascii_lowercase()).or_insert(index);
|
||||
}
|
||||
for id in used {
|
||||
let key = id.to_ascii_lowercase();
|
||||
if map.contains_key(&key) {
|
||||
continue;
|
||||
}
|
||||
let Some(source_index) = source_defs.get(&key).copied() else { continue };
|
||||
let Some(source) = src.table_styles.get(source_index) else { continue };
|
||||
let destination = destination_defs.get(&key).and_then(|index| doc.table_styles.get(*index));
|
||||
let mut imported = source.clone();
|
||||
if let Some(existing) = destination {
|
||||
if same_table_style(existing, source) {
|
||||
map.insert(key, existing.id.clone());
|
||||
continue;
|
||||
}
|
||||
let candidate = loop {
|
||||
let n = doc.alloc_id().ok_or_else(|| bad(cmd, NO_ID_SPACE))?;
|
||||
let candidate = format!("{{00000000-0000-4000-8000-{n:012X}}}");
|
||||
let key = candidate.to_ascii_lowercase();
|
||||
if !destination_defs.contains_key(&key) && !source_ids.contains(&key) {
|
||||
break candidate;
|
||||
}
|
||||
};
|
||||
// The definition's own XML names it too, and PPTX export writes that XML as is.
|
||||
imported.raw = imported.raw.replacen(&format!("styleId=\"{}\"", source.id), &format!("styleId=\"{candidate}\""), 1);
|
||||
imported.id.clone_from(&candidate);
|
||||
map.insert(key, candidate);
|
||||
} else {
|
||||
map.insert(key, imported.id.clone());
|
||||
}
|
||||
destination_defs.insert(imported.id.to_ascii_lowercase(), doc.table_styles.len());
|
||||
doc.table_styles.push(imported);
|
||||
}
|
||||
Ok(map)
|
||||
}
|
||||
|
||||
fn reuse(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
let (name, bytes) = super::insert::media_bytes(p, "slide.reuse")?;
|
||||
let src = super::file::open_presentation(&name, &bytes)?;
|
||||
@@ -399,23 +176,8 @@ fn reuse(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
}
|
||||
let keep = bool_or(p, "keepFormatting", false);
|
||||
let at = usize_param(p, "at");
|
||||
let source_size = src.slide_size;
|
||||
s.edit(|doc, sel| {
|
||||
let at = at.unwrap_or(sel.slide + 1).min(doc.slides.len());
|
||||
let mut used_table_styles = Vec::new();
|
||||
let mut seen_table_styles = HashSet::new();
|
||||
for &i in &picked {
|
||||
if let Some(slide) = src.slides.get(i) {
|
||||
collect_table_styles(&slide.shapes, &mut used_table_styles, &mut seen_table_styles, 0);
|
||||
}
|
||||
}
|
||||
let table_style_map = import_table_styles("slide.reuse", doc, &src, &used_table_styles)?;
|
||||
let scale = |source: f64, destination: f64| {
|
||||
let ratio = destination / source;
|
||||
if source > 0.0 && destination > 0.0 && ratio.is_finite() && ratio > 0.0 { ratio } else { 1.0 }
|
||||
};
|
||||
let sx = scale(source_size.width, doc.slide_size.width);
|
||||
let sy = scale(source_size.height, doc.slide_size.height);
|
||||
// Media: what the slides use is copied under fresh ids.
|
||||
let mut media_map: HashMap<MediaId, MediaId> = HashMap::new();
|
||||
// Keep Source Formatting: bring each source master used, once, with fresh ids.
|
||||
@@ -428,8 +190,7 @@ fn reuse(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
continue;
|
||||
}
|
||||
masters_done.push(m.id);
|
||||
let mut m = import_master("slide.reuse", doc, &src, m, &mut media_map, &mut layout_map)?;
|
||||
scale_master(&mut m, sx, sy);
|
||||
let m = import_master("slide.reuse", doc, &src, m, &mut media_map, &mut layout_map)?;
|
||||
doc.masters.push(Arc::new(m));
|
||||
}
|
||||
}
|
||||
@@ -440,8 +201,6 @@ fn reuse(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
let Some(from) = src.slides.get(i) else { continue };
|
||||
copy_media("slide.reuse", doc, &src, &media_refs::slide_media(from), &mut media_map)?;
|
||||
let mut sl: Slide = from.as_ref().clone();
|
||||
remap_table_styles(&mut sl.shapes, &table_style_map, 0);
|
||||
scale_shapes(&mut sl.shapes, sx, sy, 0);
|
||||
media_refs::visit_mut::slide(&mut sl, &mut remap(&media_map));
|
||||
sl.id = SlideId(doc.alloc_id().ok_or_else(|| bad("slide.reuse", NO_ID_SPACE))?);
|
||||
let map = renumber("slide.reuse", doc, &mut sl.shapes)?;
|
||||
@@ -525,205 +284,13 @@ fn copy_media(cmd: &str, doc: &mut Presentation, src: &Presentation, used: &[Med
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use deckcraft_geom::{Size, Xfrm};
|
||||
use deckcraft_model::style::{Artistic, Fill, PictureFill};
|
||||
use deckcraft_model::table::TableStyleDef;
|
||||
use deckcraft_model::text::{Action, Bullet, Hyperlink, TextBody};
|
||||
use deckcraft_model::{Background, MediaClip, Shape, ShapeKind, Table, defaults};
|
||||
use deckcraft_model::{Background, MediaClip, Shape, ShapeKind, defaults};
|
||||
use serde_json::json;
|
||||
|
||||
use super::*;
|
||||
|
||||
const CUSTOM_STYLE: &str = "{11111111-2222-4333-8444-555555555555}";
|
||||
|
||||
fn styled_table_presentation(id: &str, color: &str) -> Presentation {
|
||||
let mut p = deckcraft_model::defaults::new_presentation(None);
|
||||
let raw = format!(
|
||||
r#"<a:tblStyle xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main" styleId="{id}" styleName="Owned"><a:wholeTbl><a:tcStyle><a:fill><a:solidFill><a:srgbClr val="{color}"/></a:solidFill></a:fill></a:tcStyle></a:wholeTbl></a:tblStyle>"#
|
||||
);
|
||||
p.table_styles.push(TableStyleDef { id: id.into(), name: "Owned".into(), raw, ..Default::default() });
|
||||
let table = deckcraft_model::Table { style: id.into(), ..deckcraft_model::Table::new(1, 1, 100.0, 24.0) };
|
||||
let child = deckcraft_model::Shape { id: ShapeId(p.alloc_id().expect("shape id")), kind: ShapeKind::Table(table), ..Default::default() };
|
||||
let group = deckcraft_model::Shape {
|
||||
id: ShapeId(p.alloc_id().expect("group id")),
|
||||
kind: ShapeKind::Group { children: vec![child], child: Default::default() },
|
||||
..Default::default()
|
||||
};
|
||||
let slide = p.slides.first_mut().expect("default slide");
|
||||
Arc::make_mut(slide).shapes.push(group);
|
||||
p
|
||||
}
|
||||
|
||||
fn first_table_style(shapes: &[deckcraft_model::Shape]) -> Option<String> {
|
||||
for shape in shapes {
|
||||
if let ShapeKind::Table(table) = &shape.kind {
|
||||
return Some(table.style.clone());
|
||||
}
|
||||
if let Some(style) = first_table_style(shape.children()) {
|
||||
return Some(style);
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn reused_grouped_table_styles_survive_native_and_pptx_round_trips_and_undo() {
|
||||
for (name, bytes) in [
|
||||
("source.deckcraft", deckcraft_format::save(&styled_table_presentation(CUSTOM_STYLE, "FF0000")).expect("native source")),
|
||||
("source.pptx", deckcraft_pptx::export(&styled_table_presentation(CUSTOM_STYLE, "FF0000")).expect("pptx source")),
|
||||
] {
|
||||
let data = super::super::base64_encode(&bytes);
|
||||
let mut session = Session::with_new();
|
||||
session.execute("slide.reuse", &json!({"data": data, "name": name, "slides": [0], "keepFormatting": true})).expect("reuse");
|
||||
let doc = &session.doc().expect("doc").doc;
|
||||
assert_eq!(doc.table_styles.iter().filter(|style| style.id == CUSTOM_STYLE).count(), 1);
|
||||
assert!(doc.table_styles.iter().any(|style| style.id == CUSTOM_STYLE && style.raw.contains("FF0000")));
|
||||
assert_eq!(first_table_style(&doc.slides.get(1).expect("reused slide").shapes).as_deref(), Some(CUSTOM_STYLE));
|
||||
|
||||
session.execute("edit.undo", &json!({})).expect("undo reuse");
|
||||
let doc = &session.doc().expect("doc").doc;
|
||||
assert!(doc.table_styles.iter().all(|style| style.id != CUSTOM_STYLE));
|
||||
session.execute("edit.redo", &json!({})).expect("redo reuse");
|
||||
let bytes = crate::cmd::file::save_bytes(&session.doc().expect("doc").doc, "deckcraft").expect("save destination");
|
||||
let reopened = crate::cmd::file::open_presentation("destination.deckcraft", &bytes).expect("reopen destination");
|
||||
assert!(reopened.table_styles.iter().any(|style| style.id == CUSTOM_STYLE));
|
||||
assert_eq!(first_table_style(&reopened.slides.get(1).expect("reused slide").shapes).as_deref(), Some(CUSTOM_STYLE));
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn reused_table_style_definitions_are_deduplicated_or_remapped_on_collision() {
|
||||
let source = styled_table_presentation(CUSTOM_STYLE, "FF0000");
|
||||
let source_bytes = deckcraft_format::save(&source).expect("save source");
|
||||
|
||||
let mut identical = Session::with_new();
|
||||
identical
|
||||
.edit(|doc, _| {
|
||||
doc.table_styles.push(source.table_styles.first().cloned().unwrap_or_default());
|
||||
Ok(())
|
||||
})
|
||||
.expect("seed identical definition");
|
||||
identical
|
||||
.execute("slide.reuse", &json!({"data": super::super::base64_encode(&source_bytes), "name": "source.deckcraft", "slides": [0]}))
|
||||
.expect("reuse identical definition");
|
||||
let doc = &identical.doc().expect("doc").doc;
|
||||
assert_eq!(doc.table_styles.iter().filter(|style| style.id == CUSTOM_STYLE).count(), 1);
|
||||
assert_eq!(first_table_style(&doc.slides.get(1).expect("reused slide").shapes).as_deref(), Some(CUSTOM_STYLE));
|
||||
|
||||
let mut conflicting = Session::with_new();
|
||||
conflicting
|
||||
.edit(|doc, _| {
|
||||
let mut own = source.table_styles.first().cloned().unwrap_or_default();
|
||||
own.raw = own.raw.replace("FF0000", "0000FF");
|
||||
doc.table_styles.push(own);
|
||||
let table = deckcraft_model::Table { style: CUSTOM_STYLE.into(), ..deckcraft_model::Table::new(1, 1, 100.0, 24.0) };
|
||||
let id = ShapeId(doc.alloc_id().expect("destination table id"));
|
||||
if let Some(slide) = doc.slides.first_mut() {
|
||||
Arc::make_mut(slide).shapes.push(Shape { id, kind: ShapeKind::Table(table), ..Default::default() });
|
||||
}
|
||||
Ok(())
|
||||
})
|
||||
.expect("seed conflicting definition");
|
||||
conflicting
|
||||
.execute("slide.reuse", &json!({"data": super::super::base64_encode(&source_bytes), "name": "source.deckcraft", "slides": [0]}))
|
||||
.expect("reuse conflicting definition");
|
||||
let doc = &conflicting.doc().expect("doc").doc;
|
||||
assert_eq!(first_table_style(&doc.slides.first().expect("destination slide").shapes).as_deref(), Some(CUSTOM_STYLE));
|
||||
let imported_style = first_table_style(&doc.slides.get(1).expect("reused slide").shapes).expect("grouped table style");
|
||||
assert_ne!(imported_style, CUSTOM_STYLE);
|
||||
assert!(
|
||||
doc.table_styles.iter().any(|style| style.id == imported_style
|
||||
&& style.raw.contains("FF0000")
|
||||
&& style.raw.contains(&format!("styleId=\"{imported_style}\"")))
|
||||
);
|
||||
assert!(doc.table_styles.iter().any(|style| style.id == CUSTOM_STYLE && style.raw.contains("0000FF")));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn reused_slide_content_fits_destination_size() {
|
||||
let mut src = defaults::new_presentation(None);
|
||||
src.slide_size = Size::new(960.0, 540.0);
|
||||
let slide_id = src.slides.first().map(|s| s.id).unwrap_or_default();
|
||||
if let Some(slide) = src.slides.first_mut().map(Arc::make_mut) {
|
||||
let mut text = TextBody::from_text("scaled");
|
||||
if let Some(run) = text.paragraphs.first_mut().and_then(|p| p.runs.first_mut()) {
|
||||
run.props.size = Some(24.0);
|
||||
}
|
||||
slide.shapes = vec![
|
||||
Shape { xfrm: Some(Xfrm::new(800.0, 400.0, 200.0, 100.0)), ..Default::default() },
|
||||
Shape {
|
||||
xfrm: Some(Xfrm::new(40.0, 50.0, 400.0, 200.0)),
|
||||
text: Some(text),
|
||||
kind: ShapeKind::Table({
|
||||
let mut table = Table::new(2, 2, 400.0, 40.0);
|
||||
table.rows[0].cells[0].text = TextBody::from_text("cell");
|
||||
if let Some(run) = table.rows[0].cells[0].text.paragraphs.first_mut().and_then(|p| p.runs.first_mut()) {
|
||||
run.props.size = Some(18.0);
|
||||
}
|
||||
table
|
||||
}),
|
||||
..Default::default()
|
||||
},
|
||||
Shape {
|
||||
xfrm: Some(Xfrm::new(100.0, 80.0, 300.0, 150.0)),
|
||||
kind: ShapeKind::Group {
|
||||
child: Xfrm::new(0.0, 0.0, 300.0, 150.0),
|
||||
children: vec![Shape { xfrm: Some(Xfrm::new(200.0, 100.0, 80.0, 40.0)), ..Default::default() }],
|
||||
},
|
||||
..Default::default()
|
||||
},
|
||||
];
|
||||
}
|
||||
if let Some(master) = src.masters.first_mut().map(Arc::make_mut) {
|
||||
master.shapes = vec![Shape { xfrm: Some(Xfrm::new(800.0, 400.0, 100.0, 50.0)), ..Default::default() }];
|
||||
if let Some(layout) = master.layouts.first_mut() {
|
||||
layout.shapes = vec![Shape { xfrm: Some(Xfrm::new(700.0, 300.0, 100.0, 50.0)), ..Default::default() }];
|
||||
}
|
||||
}
|
||||
let bytes = deckcraft_format::save(&src).expect("source save");
|
||||
let data = super::super::base64_encode(&bytes);
|
||||
let mut session = Session::with_new();
|
||||
session
|
||||
.edit(|doc, _| {
|
||||
doc.slide_size = Size::new(480.0, 270.0);
|
||||
Ok(())
|
||||
})
|
||||
.expect("destination size");
|
||||
session.execute("slide.reuse", &json!({"data": data, "name": "source.deckcraft", "slides": [0], "keepFormatting": true})).expect("reuse");
|
||||
|
||||
let doc = &session.doc().expect("doc").doc;
|
||||
let reused = doc.slides.get(1).expect("reused slide");
|
||||
let first = reused.shapes.first().and_then(|s| s.xfrm).expect("shape transform");
|
||||
assert_eq!(first, Xfrm::new(400.0, 200.0, 100.0, 50.0));
|
||||
let table_shape = reused.shapes.get(1).expect("table");
|
||||
let table_box = table_shape.xfrm.expect("table transform");
|
||||
assert_eq!(table_box, Xfrm::new(20.0, 25.0, 200.0, 100.0));
|
||||
assert!(matches!(&table_shape.kind, ShapeKind::Table(_)));
|
||||
let ShapeKind::Table(table) = &table_shape.kind else { return };
|
||||
assert_eq!(table.cols, vec![100.0, 100.0]);
|
||||
assert_eq!(table.rows.first().map(|r| r.height), Some(20.0));
|
||||
let font_size = table_shape.text.as_ref().and_then(|t| t.paragraphs.first()).and_then(|p| p.runs.first()).and_then(|r| r.props.size);
|
||||
assert_eq!(font_size, Some(12.0));
|
||||
let cell_size = table.rows[0].cells[0].text.paragraphs.first().and_then(|p| p.runs.first()).and_then(|r| r.props.size);
|
||||
assert_eq!(cell_size, Some(9.0));
|
||||
let group = reused.shapes.get(2).expect("group");
|
||||
assert!(matches!(&group.kind, ShapeKind::Group { .. }));
|
||||
let ShapeKind::Group { children, child } = &group.kind else { return };
|
||||
assert_eq!(group.xfrm, Some(Xfrm::new(50.0, 40.0, 150.0, 75.0)));
|
||||
assert_eq!(*child, Xfrm::new(0.0, 0.0, 150.0, 75.0));
|
||||
assert_eq!(children.first().and_then(|s| s.xfrm), Some(Xfrm::new(100.0, 50.0, 40.0, 20.0)));
|
||||
|
||||
let imported_master = doc.masters.last().expect("imported master");
|
||||
assert_eq!(imported_master.shapes.first().and_then(|s| s.xfrm), Some(Xfrm::new(400.0, 200.0, 50.0, 25.0)));
|
||||
assert_eq!(imported_master.layouts.first().and_then(|l| l.shapes.first()).and_then(|s| s.xfrm), Some(Xfrm::new(350.0, 150.0, 50.0, 25.0)));
|
||||
assert_ne!(reused.id, slide_id);
|
||||
|
||||
let saved = deckcraft_format::save(&doc.clone()).expect("destination save");
|
||||
let reopened = crate::cmd::file::open_presentation("destination.deckcraft", &saved).expect("destination reopen");
|
||||
let reopened_shape = reopened.slides.get(1).and_then(|s| s.shapes.first()).and_then(|s| s.xfrm);
|
||||
assert_eq!(reopened_shape, Some(Xfrm::new(400.0, 200.0, 100.0, 50.0)));
|
||||
}
|
||||
|
||||
/// Every kind of media a slide can hold comes along, each pointing at its own bytes: the
|
||||
/// artistic-effect original, a background picture, poster, sounds and picture bullets too.
|
||||
#[test]
|
||||
|
||||
@@ -219,7 +219,7 @@ fn neighbour(s: &mut Session, p: &Value, forward: bool) -> Result<Value> {
|
||||
let cmd = if forward { "comment.next" } else { "comment.prev" };
|
||||
let (threads, slide, index) = {
|
||||
let st = s.doc()?;
|
||||
(comment_threads(&st.doc), usize_param(p, "slide").unwrap_or(st.selection.slide), super::opt_usize_param(p, "index", cmd)?)
|
||||
(comment_threads(&st.doc), usize_param(p, "slide").unwrap_or(st.selection.slide), usize_param(p, "index"))
|
||||
};
|
||||
if threads.is_empty() {
|
||||
return Err(bad(cmd, "no comments"));
|
||||
@@ -367,7 +367,7 @@ fn spelling(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
}
|
||||
out.push(v);
|
||||
}
|
||||
let rep: std::collections::HashSet<usize> = crate::spell::repeated(para, &default).iter().map(|w| w.start).collect();
|
||||
let rep: Vec<usize> = crate::spell::repeated(para, &default).iter().map(|w| w.start).collect();
|
||||
for w in crate::spell::paragraph_words(para, &default) {
|
||||
if ignore.contains(&w.text.to_lowercase()) || rep.contains(&w.start) {
|
||||
continue;
|
||||
@@ -421,30 +421,23 @@ fn add_to_dictionary(_s: &mut Session, p: &Value) -> Result<Value> {
|
||||
/// Replace whole-word `from` with `to` in `text`; how many were replaced.
|
||||
fn replace_word(text: &mut String, from: &str, to: &str, all: bool) -> usize {
|
||||
let is_word = |c: char| c.is_alphanumeric() || c == '\'' || c == '’';
|
||||
// The result is built in one pass; the text is not edited in place for every match.
|
||||
let mut out = String::new();
|
||||
let (mut n, mut at, mut copied) = (0, 0, 0);
|
||||
let mut n = 0;
|
||||
let mut at = 0;
|
||||
while let Some(i) = text.get(at..).and_then(|t| t.find(from)).map(|i| i + at) {
|
||||
let end = i + from.len();
|
||||
// The character before the match as the edited text would have it.
|
||||
let before = text.get(copied..i).and_then(|t| t.chars().next_back()).or_else(|| out.chars().next_back()).is_some_and(is_word);
|
||||
let before = text.get(..i).and_then(|t| t.chars().next_back()).is_some_and(is_word);
|
||||
let after = text.get(end..).and_then(|t| t.chars().next()).is_some_and(is_word);
|
||||
if before || after {
|
||||
at = end;
|
||||
continue;
|
||||
}
|
||||
out.push_str(text.get(copied..i).unwrap_or_default());
|
||||
out.push_str(to);
|
||||
text.replace_range(i..end, to);
|
||||
n += 1;
|
||||
(at, copied) = (end, end);
|
||||
at = i + to.len();
|
||||
if !all {
|
||||
break;
|
||||
}
|
||||
}
|
||||
if n > 0 {
|
||||
out.push_str(text.get(copied..).unwrap_or_default());
|
||||
*text = out;
|
||||
}
|
||||
n
|
||||
}
|
||||
|
||||
@@ -771,49 +764,6 @@ fn end_compare(s: &mut Session, _: &Value) -> Result<Value> {
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
/// The replacement as it ran before: edited in place for every match.
|
||||
fn replace_word_in_place(text: &mut String, from: &str, to: &str, all: bool) -> usize {
|
||||
let is_word = |c: char| c.is_alphanumeric() || c == '\'' || c == '’';
|
||||
let mut n = 0;
|
||||
let mut at = 0;
|
||||
while let Some(i) = text.get(at..).and_then(|t| t.find(from)).map(|i| i + at) {
|
||||
let end = i + from.len();
|
||||
let before = text.get(..i).and_then(|t| t.chars().next_back()).is_some_and(is_word);
|
||||
let after = text.get(end..).and_then(|t| t.chars().next()).is_some_and(is_word);
|
||||
if before || after {
|
||||
at = end;
|
||||
continue;
|
||||
}
|
||||
text.replace_range(i..end, to);
|
||||
n += 1;
|
||||
at = i + to.len();
|
||||
if !all {
|
||||
break;
|
||||
}
|
||||
}
|
||||
n
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn replace_word_matches_the_in_place_version() {
|
||||
let cases = [
|
||||
("a a b a", "a", "x", true),
|
||||
("aa a. a", "a", "bb", true),
|
||||
("a-a -a-", "-", "ab", true),
|
||||
("foo. foo.", "foo.", "", true),
|
||||
("it's it", "it", "so", true),
|
||||
("x.x. x.", "x.", "y", true),
|
||||
("a a a", "a", "b", false),
|
||||
("none", "z", "q", true),
|
||||
("é é", "é", "e", true),
|
||||
];
|
||||
for (text, from, to, all) in cases {
|
||||
let (mut a, mut b) = (text.to_string(), text.to_string());
|
||||
let (na, nb) = (replace_word(&mut a, from, to, all), replace_word_in_place(&mut b, from, to, all));
|
||||
assert_eq!((na, a), (nb, b), "{text:?} {from:?} -> {to:?}");
|
||||
}
|
||||
}
|
||||
|
||||
use super::*;
|
||||
use serde_json::json;
|
||||
|
||||
@@ -862,40 +812,6 @@ mod tests {
|
||||
assert_eq!(here["text"], "on first");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn malformed_comment_navigation_index_does_not_change_selection() {
|
||||
let mut s = Session::with_new();
|
||||
s.execute("comment.add", &json!({"text": "first"})).unwrap();
|
||||
s.execute("shape.insert", &json!({"preset": "rect", "rect": [10.0, 10.0, 40.0, 40.0]})).unwrap();
|
||||
s.execute("slide.new", &json!({})).unwrap();
|
||||
s.execute("comment.add", &json!({"text": "second"})).unwrap();
|
||||
s.execute("slide.go", &json!({"index": 0})).unwrap();
|
||||
let shape = s.doc().unwrap().doc.slides.first().and_then(|slide| slide.shapes.first()).map(|shape| shape.id).unwrap();
|
||||
s.doc_mut().unwrap().selection.shapes = vec![shape];
|
||||
|
||||
for (command, value) in [
|
||||
("comment.next", json!(-1)),
|
||||
("comment.next", json!(1.5)),
|
||||
("comment.next", json!("bad")),
|
||||
("comment.prev", json!(-1)),
|
||||
("comment.prev", json!(1.5)),
|
||||
("comment.prev", json!("bad")),
|
||||
] {
|
||||
let before = s.doc().unwrap().selection.clone();
|
||||
let error = s.execute(command, &json!({"index": value})).unwrap_err();
|
||||
assert!(error.to_string().contains("valid index"), "{command} {value}: {error}");
|
||||
assert_eq!(s.doc().unwrap().selection, before, "{command} {value}");
|
||||
}
|
||||
|
||||
let zero = s.execute("comment.next", &json!({"slide": 0, "index": 0})).unwrap();
|
||||
assert_eq!(zero["slide"], 1);
|
||||
assert_eq!(zero["index"], 0);
|
||||
s.execute("slide.go", &json!({"index": 0})).unwrap();
|
||||
let omitted = s.execute("comment.next", &json!({})).unwrap();
|
||||
assert_eq!(omitted["slide"], 0);
|
||||
assert_eq!(omitted["index"], 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn timestamps_are_iso_utc() {
|
||||
assert_eq!(iso_utc(0), "1970-01-01T00:00:00Z");
|
||||
|
||||
@@ -135,14 +135,6 @@ fn eye_show_all_and_hide_all() {
|
||||
assert!(!flags(&s, a).0 && flags(&s, g).0 && flags(&s, e).0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn lock_group_with_its_child_targeted_leaves_child_locked() {
|
||||
let (mut s, ids) = deck();
|
||||
let (d, g) = (ids[3], ids[7]);
|
||||
s.execute("shape.lock", &json!({"ids": [g, d, d]})).unwrap();
|
||||
assert!(flags(&s, g).1 && flags(&s, d).1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn lock_cascades_through_groups_and_undoes() {
|
||||
let (mut s, ids) = deck();
|
||||
|
||||
@@ -10,7 +10,7 @@ use serde_json::{Value, json};
|
||||
|
||||
use super::{
|
||||
CommandSpec, NO_ID_SPACE, bad, bool_or, bool_param, cmd, color_param, color_value, edit_geometry, edit_shapes, f64_param, has_selection,
|
||||
has_slide, id_param, ok, opt_usize_param, str_param, targets, usize_param, with_param, xfrm_of,
|
||||
has_slide, id_param, ok, str_param, targets, usize_param, with_param, xfrm_of,
|
||||
};
|
||||
use crate::{Result, Session};
|
||||
|
||||
@@ -103,7 +103,7 @@ pub fn specs() -> Vec<CommandSpec> {
|
||||
"Shape Fill",
|
||||
["Shape Format", "Shape Styles"],
|
||||
None,
|
||||
"{color? | none?: true | background?: true (the slide background shows through; shapes and text boxes only) | gradient?: {stops: [[pos 0..1, color]], kind?: linear|radial|rectangular|path, angle?: deg (linear), focus?: [l, t, r, b] fractions (radial, rectangular and path: where the first stop sits), rotateWithShape?: bool (false keeps linear and radial gradients upright on the slide; rectangular and path gradients turn with the shape when drawn)} | picture?: base64 | pattern?: {preset, fg, bg} | transparency?: 0..1 | reset?: true, ids?}; pictureProps?: {tile?: bool, offsetX?, offsetY?: pt, scaleX?, scaleY?: 0..10, align?: tl|t|tr|l|ctr|r|bl|b|br, mirror?: none|x|y|xy, stretch?: [l, t, r, b] fractions, rotateWithShape?: bool} adjusts a picture fill; a group also formats its members",
|
||||
"{color? | none?: true | background?: true (the slide background shows through; shapes and text boxes only) | gradient?: {stops: [[pos, color]], angle?, kind?: linear|radial} | picture?: base64 | pattern?: {preset, fg, bg} | transparency?: 0..1 | reset?: true, ids?}; pictureProps?: {tile?: bool, offsetX?, offsetY?: pt, scaleX?, scaleY?: 0..10, align?: tl|t|tr|l|ctr|r|bl|b|br, mirror?: none|x|y|xy, stretch?: [l, t, r, b] fractions, rotateWithShape?: bool} adjusts a picture fill; a group also formats its members",
|
||||
has_selection,
|
||||
fill
|
||||
),
|
||||
@@ -374,25 +374,6 @@ fn visible(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
/// (a child can still be unlocked on its own afterwards), as PowerPoint does.
|
||||
fn lock(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
let v = bool_param(p, "locked");
|
||||
// A targeted shape inside another targeted group is reached through the group: editing it again
|
||||
// would toggle it back.
|
||||
let mut ids = super::targets(s, p)?;
|
||||
if ids.len() > 1 {
|
||||
let doc = s.doc()?;
|
||||
// Walk each distinct group once, however often `ids` repeats it.
|
||||
let mut once = std::collections::HashSet::new();
|
||||
ids.retain(|id| once.insert(*id));
|
||||
let mut inside = std::collections::HashSet::new();
|
||||
for id in &ids {
|
||||
if let Some(g) = doc.shape(*id) {
|
||||
deckcraft_model::walk(g.children(), &mut |c, _| {
|
||||
inside.insert(c.id);
|
||||
});
|
||||
}
|
||||
}
|
||||
ids.retain(|id| !inside.contains(id));
|
||||
}
|
||||
let p = &json!({"ids": ids, "locked": p.get("locked")});
|
||||
edit_shapes(s, p, "shape.lock", |sh| {
|
||||
let to = v.unwrap_or(!sh.locked);
|
||||
set_locked(sh, to, 0);
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
use super::{
|
||||
ColorRef, ColorTransform, Dash, Effects, Fill, Geom, Glow, Gradient, GradientShape, GradientStop, Line, LineEnd, PictureFill, PictureMode,
|
||||
Reflection, Result, Rgba, SchemeSlot, Session, Shadow, ShapeStyle, Value, apply_fill, bad, bool_or, color_param, color_value, edit_geometry,
|
||||
edit_shapes, f64_param, inherited_fills, json, opt_usize_param, requested_fill, str_param, targets,
|
||||
edit_shapes, f64_param, inherited_fills, json, requested_fill, str_param, targets, usize_param,
|
||||
};
|
||||
|
||||
pub(super) fn adjust(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
let i = opt_usize_param(p, "index", "shape.adjust")?.unwrap_or(0).min(16);
|
||||
let i = usize_param(p, "index").unwrap_or(0).min(16);
|
||||
let v = f64_param(p, "value").ok_or_else(|| bad("shape.adjust", "missing `value`"))?;
|
||||
edit_geometry(s, p, "shape.adjust", |sh| {
|
||||
if let Geom::Preset { name, adj } = &mut sh.geom {
|
||||
@@ -74,8 +74,7 @@ pub(super) fn change(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
|
||||
/// `{stops: [[pos 0–1, color], …], kind?: linear|radial|rectangular|path, angle?: deg (linear),
|
||||
/// focus?: [l, t, r, b] fractions (radial/rectangular/path: where the first stop sits),
|
||||
/// rotateWithShape?}`. The renderer keeps only linear and radial gradients upright when
|
||||
/// `rotateWithShape` is false; rectangular and path gradients turn with the shape.
|
||||
/// rotateWithShape?}`.
|
||||
pub(crate) fn gradient_param(v: &Value) -> Option<Gradient> {
|
||||
let stops: Vec<GradientStop> = v
|
||||
.get("stops")?
|
||||
@@ -109,13 +108,7 @@ pub(crate) fn gradient_param(v: &Value) -> Option<Gradient> {
|
||||
scaled: false,
|
||||
},
|
||||
};
|
||||
Some(Gradient {
|
||||
tile: [0.0; 4],
|
||||
stops,
|
||||
shape,
|
||||
rotate_with_shape: v.get("rotateWithShape").and_then(Value::as_bool).unwrap_or(true),
|
||||
flip: v.get("flip").and_then(Value::as_str).and_then(deckcraft_model::style::GradientFlip::from_xml),
|
||||
})
|
||||
Some(Gradient { tile: [0.0; 4], stops, shape, rotate_with_shape: v.get("rotateWithShape").and_then(Value::as_bool).unwrap_or(true) })
|
||||
}
|
||||
|
||||
/// Fill and outline apply to a group's visible members as well as its container. Other edits
|
||||
@@ -372,9 +365,9 @@ pub(super) fn effects(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
let soft = p.get("softEdges").map(|v| v.as_f64().filter(|x| *x > 0.0).map(|x| x.min(100.0)));
|
||||
let refl = str_param(p, "reflection").map(|r| match r {
|
||||
"none" => None,
|
||||
"half" => Some(Reflection { blur: 0.5, start_alpha: 0.5, end_alpha: 0.0, end_pos: 0.55, dist: 4.0, ..Default::default() }),
|
||||
"full" => Some(Reflection { blur: 0.5, start_alpha: 0.5, end_alpha: 0.0, end_pos: 0.9, dist: 4.0, ..Default::default() }),
|
||||
_ => Some(Reflection { blur: 0.5, start_alpha: 0.5, end_alpha: 0.0, end_pos: 0.35, dist: 0.0, ..Default::default() }),
|
||||
"half" => Some(Reflection { blur: 0.5, start_alpha: 0.5, end_alpha: 0.0, end_pos: 0.55, dist: 4.0 }),
|
||||
"full" => Some(Reflection { blur: 0.5, start_alpha: 0.5, end_alpha: 0.0, end_pos: 0.9, dist: 4.0 }),
|
||||
_ => Some(Reflection { blur: 0.5, start_alpha: 0.5, end_alpha: 0.0, end_pos: 0.35, dist: 0.0 }),
|
||||
});
|
||||
let three_d = p.get("threeD").filter(|v| v.is_object());
|
||||
let bevel = str_param(p, "bevel").map(|b| if b == "none" { None } else { Some(b.to_string()) });
|
||||
@@ -468,14 +461,7 @@ pub(super) fn effects(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
e.glow = (g.radius > 0.0).then_some(g);
|
||||
}
|
||||
if let Some(v) = &refl_props {
|
||||
let mut r = e.reflection.take().unwrap_or(Reflection {
|
||||
blur: 0.5,
|
||||
start_alpha: 0.5,
|
||||
end_alpha: 0.0,
|
||||
end_pos: 0.35,
|
||||
dist: 0.0,
|
||||
..Default::default()
|
||||
});
|
||||
let mut r = e.reflection.take().unwrap_or(Reflection { blur: 0.5, start_alpha: 0.5, end_alpha: 0.0, end_pos: 0.35, dist: 0.0 });
|
||||
if let Some(t) = num(v, "transparency") {
|
||||
r.start_alpha = 1.0 - t.clamp(0.0, 1.0);
|
||||
}
|
||||
@@ -556,7 +542,7 @@ pub fn quick_style_for(index: usize) -> (ShapeStyle, Option<Fill>, Option<Line>)
|
||||
}
|
||||
|
||||
pub(super) fn quick_style(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
let i = opt_usize_param(p, "index", "shape.quickStyle")?.unwrap_or(1).min(41);
|
||||
let i = usize_param(p, "index").unwrap_or(1).min(41);
|
||||
let (st, f, l) = quick_style_for(i);
|
||||
edit_shapes(s, p, "shape.quickStyle", |sh| {
|
||||
sh.style = Some(st.clone());
|
||||
@@ -569,14 +555,3 @@ pub(super) fn quick_style(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
Ok(())
|
||||
})
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn gradient_parameters_keep_flip_mode() {
|
||||
let gradient = gradient_param(&json!({"stops": [[0, "#ff0000"], [1, "#0000ff"]], "flip": "xy"})).expect("gradient");
|
||||
assert_eq!(gradient.flip, Some(deckcraft_model::style::GradientFlip::Xy));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,9 +3,8 @@ use super::{ColorRef, Fill, Geom, Line, NO_ID_SPACE, Result, Session, Value, bad
|
||||
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`"))?;
|
||||
let st = s.doc()?;
|
||||
// In slide space, as shape.connect uses them, for a shape inside a group too.
|
||||
let v = crate::connect::sites_of(&st.doc, &st.selection, st.shapes(), id).ok_or_else(|| bad("shape.sites", "no such shape"))?;
|
||||
Ok(json!(v.iter().map(|q| [q.x, q.y]).collect::<Vec<_>>()))
|
||||
let sh = st.shape(id).ok_or_else(|| bad("shape.sites", "no such shape"))?;
|
||||
Ok(json!(crate::connect::sites(&st.doc, &st.selection, sh).iter().map(|q| [q.x, q.y]).collect::<Vec<_>>()))
|
||||
}
|
||||
|
||||
pub(super) fn connect(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
@@ -13,13 +12,8 @@ pub(super) fn connect(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
return Err(bad("shape.connect", "missing `from` / `to`"));
|
||||
};
|
||||
let st = s.doc()?;
|
||||
// Anywhere in the slide's tree: a shape inside a group takes a connector too (PowerPoint
|
||||
// glues to grouped shapes), its sites where the group draws them.
|
||||
let site_list = |id| {
|
||||
crate::connect::sites_of(&st.doc, &st.selection, st.shapes(), id)
|
||||
.ok_or_else(|| bad("shape.connect", format!("no shape {} on the current slide", id.0)))
|
||||
};
|
||||
let (a, b) = (site_list(from)?, site_list(to)?);
|
||||
let site_list = |id| st.shape(id).map(|sh| crate::connect::sites(&st.doc, &st.selection, sh)).unwrap_or_default();
|
||||
let (a, b) = (site_list(from), site_list(to));
|
||||
if a.is_empty() || b.is_empty() {
|
||||
return Err(bad("shape.connect", "both shapes need connection sites (lines and groups have none)"));
|
||||
}
|
||||
@@ -41,11 +35,6 @@ pub(super) fn connect(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
};
|
||||
let (p0, p1) = (a[ia], b[ib]);
|
||||
let id = match id_param(p, "id", "shape.connect")? {
|
||||
// The ends are in slide space and rerouting follows the slide's own connectors, so a line
|
||||
// inside a group would be drawn in the group's space and never follow its shapes.
|
||||
Some(id) if !st.shapes().iter().any(|sh| sh.id == id) && st.shape(id).is_some_and(|sh| sh.is_line()) => {
|
||||
return Err(bad("shape.connect", "`id` names a line inside a group; ungroup it first, or leave `id` out for a new connector"));
|
||||
}
|
||||
Some(id) => id,
|
||||
None => {
|
||||
let preset = str_param(p, "preset").unwrap_or("straightConnector1");
|
||||
@@ -104,7 +93,7 @@ pub(super) fn freeform(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
a.iter()
|
||||
.filter_map(|v| Some(deckcraft_geom::Point::new(v.get(0)?.as_f64()?, v.get(1)?.as_f64()?)))
|
||||
.filter(|q| q.x.is_finite() && q.y.is_finite())
|
||||
.take(crate::tools::MAX_GESTURE_POINTS)
|
||||
.take(20_000)
|
||||
.collect()
|
||||
})
|
||||
.unwrap_or_default();
|
||||
|
||||
@@ -191,13 +191,6 @@ pub(super) fn rotate(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
pub(super) fn flip(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
let v = str_param(p, "axis").unwrap_or("horizontal").starts_with('v');
|
||||
edit_geometry(s, p, "shape.flip", |sh| {
|
||||
// PowerPoint greys Flip out for a SmartArt graphic (SmartArt Design > Right to Left mirrors it),
|
||||
// also inside a group: ungrouping would hand the flip to the graphic.
|
||||
let mut graphic = false;
|
||||
deckcraft_model::walk(std::slice::from_ref(&*sh), &mut |x, _| graphic |= x.diagram.is_some());
|
||||
if graphic {
|
||||
return Err(bad("shape.flip", "Flip is not available for a SmartArt graphic; use `smartArt.layout` to change how it is laid out"));
|
||||
}
|
||||
if let Some(x) = sh.xfrm.as_mut() {
|
||||
if v {
|
||||
x.flip_v = !x.flip_v;
|
||||
|
||||
@@ -267,9 +267,6 @@ fn delete(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
for sec in &mut doc.sections {
|
||||
sec.slides.retain(|x| !ids.contains(x));
|
||||
}
|
||||
for show in &mut doc.custom_shows {
|
||||
show.slides.retain(|x| !ids.contains(x));
|
||||
}
|
||||
sel.slide = first.min(doc.slides.len().saturating_sub(1));
|
||||
sel.slides = doc.slides.get(sel.slide).map(|x| vec![x.id]).unwrap_or_default();
|
||||
sel.shapes.clear();
|
||||
@@ -408,9 +405,8 @@ fn select_slides(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
};
|
||||
s.select(|doc, sel| {
|
||||
if add {
|
||||
let mut have: std::collections::HashSet<_> = sel.slides.iter().copied().collect();
|
||||
for i in &ids {
|
||||
if have.insert(*i) {
|
||||
if !sel.slides.contains(i) {
|
||||
sel.slides.push(*i);
|
||||
}
|
||||
}
|
||||
@@ -781,45 +777,6 @@ mod tests {
|
||||
(sel.slide, sel.slides.iter().map(|x| x.0).collect())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn deleting_slide_prunes_every_custom_show_in_one_undo_step() {
|
||||
let (mut s, ids) = deck(3);
|
||||
let shows = vec![
|
||||
deckcraft_model::CustomShow {
|
||||
name: "First".into(),
|
||||
slides: vec![deckcraft_model::SlideId(ids[0]), deckcraft_model::SlideId(ids[1]), deckcraft_model::SlideId(ids[1])],
|
||||
},
|
||||
deckcraft_model::CustomShow { name: "Second".into(), slides: vec![deckcraft_model::SlideId(ids[1]), deckcraft_model::SlideId(ids[2])] },
|
||||
deckcraft_model::CustomShow { name: "Empty after delete".into(), slides: vec![deckcraft_model::SlideId(ids[1])] },
|
||||
];
|
||||
s.edit(|doc, _| {
|
||||
doc.custom_shows = shows;
|
||||
Ok(())
|
||||
})
|
||||
.expect("set custom shows");
|
||||
|
||||
s.execute("slide.delete", &json!({"index": 1})).expect("delete slide");
|
||||
let shows = &s.doc().expect("doc").doc.custom_shows;
|
||||
assert_eq!(shows[0].slides, [deckcraft_model::SlideId(ids[0])]);
|
||||
assert_eq!(shows[1].slides, [deckcraft_model::SlideId(ids[2])]);
|
||||
assert!(shows[2].slides.is_empty());
|
||||
|
||||
s.execute("edit.undo", &json!({})).expect("undo deletion");
|
||||
assert_eq!(order(&s), ids);
|
||||
assert_eq!(
|
||||
s.doc().expect("doc").doc.custom_shows[0].slides,
|
||||
[deckcraft_model::SlideId(ids[0]), deckcraft_model::SlideId(ids[1]), deckcraft_model::SlideId(ids[1])]
|
||||
);
|
||||
assert_eq!(s.doc().expect("doc").doc.custom_shows[1].slides, [deckcraft_model::SlideId(ids[1]), deckcraft_model::SlideId(ids[2])]);
|
||||
|
||||
s.execute("edit.redo", &json!({})).expect("redo deletion");
|
||||
let bytes = deckcraft_format::save(&s.doc().expect("doc").doc).expect("save deck");
|
||||
let reopened = deckcraft_format::load(&bytes).expect("reopen deck");
|
||||
assert_eq!(reopened.custom_shows[0].slides, [deckcraft_model::SlideId(ids[0])]);
|
||||
assert_eq!(reopened.custom_shows[1].slides, [deckcraft_model::SlideId(ids[2])]);
|
||||
assert!(reopened.custom_shows[2].slides.is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn move_from_to_moves_one_slide() {
|
||||
let (mut s, ids) = deck(4);
|
||||
|
||||
@@ -99,7 +99,6 @@ pub(super) fn paint(d: &Diagram, sh: &mut Shape, i: usize, light: bool) {
|
||||
shape: GradientShape::Linear { angle: 90.0, scaled: false },
|
||||
rotate_with_shape: true,
|
||||
tile: [0.0; 4],
|
||||
flip: None,
|
||||
}));
|
||||
sh.line = Some(Line::solid(ColorRef::scheme(SchemeSlot::Lt1), 1.0));
|
||||
sh.effects = Some(Effects { outer_shadow: Some(shadow(8.0, 5.0)), ..Default::default() });
|
||||
|
||||
@@ -253,10 +253,9 @@ fn cell_param(p: &Value, key: &str, cmd: &str) -> Result<Option<(usize, usize)>>
|
||||
None | Some(Value::Null) => return Ok(None),
|
||||
Some(v) => v,
|
||||
};
|
||||
// Exactly [row, col]: an entry with more is as malformed as one with less.
|
||||
let idx = |x: &Value| x.as_u64().and_then(|x| usize::try_from(x).ok());
|
||||
match v.as_array().map(Vec::as_slice) {
|
||||
Some([r, c]) if let (Some(r), Some(c)) = (idx(r), idx(c)) => Ok(Some((r, c))),
|
||||
let idx = |i: usize| v.as_array().and_then(|a| a.get(i)).and_then(Value::as_u64).and_then(|x| usize::try_from(x).ok());
|
||||
match (idx(0), idx(1)) {
|
||||
(Some(r), Some(c)) => Ok(Some((r, c))),
|
||||
_ => Err(bad(cmd, format!("`{key}` is not a valid cell: {v}"))),
|
||||
}
|
||||
}
|
||||
@@ -390,54 +389,33 @@ fn options(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
})
|
||||
}
|
||||
|
||||
/// At most this many `cells` in one table cell command.
|
||||
const MAX_CELLS: usize = 100_000;
|
||||
|
||||
/// The cells a command acts on: `cells`, else the selected cell range, else the cell holding the
|
||||
/// caret, else every cell.
|
||||
fn target_cells(s: &Session, p: &Value, cmd: &str) -> Result<impl Fn(&Table) -> Result<Vec<(usize, usize)>> + use<>> {
|
||||
// `cells` is a list of [row, col]: anything else, or an entry that is not one, is refused, never
|
||||
// dropped or read as absent (which would act on the selection or the whole table).
|
||||
let cells: Option<Vec<(usize, usize)>> = match p.get("cells") {
|
||||
None | Some(Value::Null) => None,
|
||||
Some(Value::Array(a)) if a.len() > MAX_CELLS => {
|
||||
return Err(bad(cmd, format!("`cells` lists {} cells; at most {MAX_CELLS}", a.len())));
|
||||
}
|
||||
Some(Value::Array(a)) => Some(
|
||||
a.iter()
|
||||
.map(|c| {
|
||||
cell_param(&json!({ "cell": c }), "cell", cmd)?.ok_or_else(|| bad(cmd, format!("`cells` entry {c} is not a [row, col] cell")))
|
||||
})
|
||||
.collect::<Result<_>>()?,
|
||||
),
|
||||
Some(v) => return Err(bad(cmd, format!("`cells` is a list of [row, col] cells, not {v}"))),
|
||||
};
|
||||
fn target_cells(s: &Session, p: &Value) -> Result<impl Fn(&Table) -> Vec<(usize, usize)> + use<>> {
|
||||
let cells: Option<Vec<(usize, usize)>> = p
|
||||
.get("cells")
|
||||
.and_then(Value::as_array)
|
||||
.map(|a| a.iter().take(100_000).filter_map(|c| Some((c.get(0)?.as_u64()? as usize, c.get(1)?.as_u64()? as usize))).collect());
|
||||
let st = s.doc()?;
|
||||
// A selected range grows over the merged cells it touches; with only a caret, the caret's cell is used.
|
||||
let sel = st.selection.cells.map(|(_, a, b)| (a, b, true)).or_else(|| st.selection.text.as_ref().and_then(|t| t.cell).map(|c| (c, c, false)));
|
||||
let cmd = cmd.to_string();
|
||||
Ok(move |t: &Table| match (&cells, sel) {
|
||||
// Each listed cell must be in the table, so a command never reports success for a cell
|
||||
// it could not edit.
|
||||
(Some(c), _) => match c.iter().find(|(r, c)| *r >= t.n_rows() || *c >= t.n_cols()) {
|
||||
Some((r, c)) => Err(bad(&cmd, format!("no cell [{r}, {c}]: the table has {} rows and {} columns", t.n_rows(), t.n_cols()))),
|
||||
None => Ok(c.clone()),
|
||||
},
|
||||
(Some(c), _) => c.clone(),
|
||||
(None, Some((a, b, range))) => {
|
||||
let (a, b) = if range { cell_block(t, a, b) } else { (a, b) };
|
||||
// The selection can name cells outside the table; only walk the ones that exist.
|
||||
let (r1, c1) = (a.0.max(b.0).min(t.n_rows().saturating_sub(1)), a.1.max(b.1).min(t.n_cols().saturating_sub(1)));
|
||||
Ok((a.0.min(b.0)..=r1).flat_map(|r| (a.1.min(b.1)..=c1).map(move |c| (r, c))).collect())
|
||||
(a.0.min(b.0)..=r1).flat_map(|r| (a.1.min(b.1)..=c1).map(move |c| (r, c))).collect()
|
||||
}
|
||||
_ => Ok((0..t.n_rows()).flat_map(|r| (0..t.n_cols()).map(move |c| (r, c))).collect()),
|
||||
_ => (0..t.n_rows()).flat_map(|r| (0..t.n_cols()).map(move |c| (r, c))).collect(),
|
||||
})
|
||||
}
|
||||
|
||||
fn cell_fill(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
let fill = if bool_or(p, "none", false) { Some(deckcraft_model::Fill::None) } else { color_param(p, "color").map(deckcraft_model::Fill::solid) };
|
||||
let targets = target_cells(s, p, "table.cellFill")?;
|
||||
let targets = target_cells(s, p)?;
|
||||
with_table(s, p, "table.cellFill", move |t| {
|
||||
for (r, c) in targets(t)? {
|
||||
for (r, c) in targets(t) {
|
||||
if let Some(cell) = t.cell_mut(r, c) {
|
||||
cell.fill.clone_from(&fill);
|
||||
}
|
||||
@@ -469,9 +447,9 @@ fn borders(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
}
|
||||
// "none" clears to no line, so the table style's own lines don't show through either.
|
||||
let line = if which == "none" { Line::none() } else { line };
|
||||
let targets = target_cells(s, p, "table.borders")?;
|
||||
let targets = target_cells(s, p)?;
|
||||
with_table(s, p, "table.borders", move |t| {
|
||||
let list = targets(t)?;
|
||||
let list = targets(t);
|
||||
let (Some(r0), Some(r1), Some(c0), Some(c1)) =
|
||||
(list.iter().map(|x| x.0).min(), list.iter().map(|x| x.0).max(), list.iter().map(|x| x.1).min(), list.iter().map(|x| x.1).max())
|
||||
else {
|
||||
@@ -485,9 +463,9 @@ fn borders(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
}
|
||||
let n = match side {
|
||||
LEFT => c.checked_sub(1).map(|c| (r, c, RIGHT)),
|
||||
RIGHT => c.checked_add(1).filter(|c| *c < cols).map(|c| (r, c, LEFT)),
|
||||
RIGHT => (c + 1 < cols).then_some((r, c + 1, LEFT)),
|
||||
TOP => r.checked_sub(1).map(|r| (r, c, BOTTOM)),
|
||||
_ => r.checked_add(1).filter(|r| *r < rows).map(|r| (r, c, TOP)),
|
||||
_ => (r + 1 < rows).then_some((r + 1, c, TOP)),
|
||||
};
|
||||
if let Some((r, c, side)) = n
|
||||
&& let Some(cell) = t.cell_mut(r, c)
|
||||
@@ -545,9 +523,9 @@ fn cell_margins(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
};
|
||||
let side = |k: &str, i: usize| f64_param(p, k).filter(|v| v.is_finite()).map_or(base[i], |v| v.clamp(0.0, 400.0));
|
||||
let m = [side("left", 0), side("right", 1), side("top", 2), side("bottom", 3)];
|
||||
let targets = target_cells(s, p, "table.cellMargins")?;
|
||||
let targets = target_cells(s, p)?;
|
||||
with_table(s, p, "table.cellMargins", move |t| {
|
||||
for (r, c) in targets(t)? {
|
||||
for (r, c) in targets(t) {
|
||||
if let Some(cell) = t.cell_mut(r, c) {
|
||||
cell.margins = Some(m);
|
||||
}
|
||||
@@ -582,9 +560,9 @@ fn cell_align(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
o => return Err(bad("table.cellAlign", format!("unknown anchor {o:?}; one of top, middle, bottom"))),
|
||||
}),
|
||||
};
|
||||
let targets = target_cells(s, p, "table.cellAlign")?;
|
||||
let targets = target_cells(s, p)?;
|
||||
with_table(s, p, "table.cellAlign", move |t| {
|
||||
for (r, c) in targets(t)? {
|
||||
for (r, c) in targets(t) {
|
||||
if let Some(cell) = t.cell_mut(r, c) {
|
||||
if anchor.is_some() {
|
||||
cell.anchor = anchor;
|
||||
|
||||
@@ -30,7 +30,7 @@ use editing::{edit, exit};
|
||||
pub(crate) use editing::{insert_field, insert_text, paste_body};
|
||||
pub use formatting::format_range;
|
||||
use formatting::{fonts_used, replace_font};
|
||||
pub(crate) use formatting::{format_body, format_para, format_run, format_run_levels, link_text};
|
||||
pub(crate) use formatting::{format_body, format_para, format_run, link_text};
|
||||
pub use ime::{IME_SURROUNDING_MAX_BYTES, ImeSurrounding, cell_range_underlines, composition_underlines, ime_surrounding, range_underlines};
|
||||
use ime::{commit, compose};
|
||||
pub(crate) use layout::ctx_for;
|
||||
|
||||
@@ -8,34 +8,22 @@ use super::{
|
||||
/// selected shapes). A caret without a range formats the word it's in (PowerPoint behaviour),
|
||||
/// or sets the formatting for what is typed next at a word boundary.
|
||||
pub(crate) fn format_run(s: &mut Session, f: &(dyn Fn(&mut RunProps) + Send + Sync)) -> Result<Value> {
|
||||
format_run_levels(s, f, &|r, _| f(r))
|
||||
}
|
||||
|
||||
/// Like [`format_run`], with a separate change for the list-style levels of a layout or master
|
||||
/// placeholder: `lf` gets the level's own run props and the level's effective ones (what a run at
|
||||
/// that level resolves to), so a relative change (grow/shrink) steps each level from its own value.
|
||||
pub(crate) fn format_run_levels(
|
||||
s: &mut Session,
|
||||
f: &(dyn Fn(&mut RunProps) + Send + Sync),
|
||||
lf: &(dyn Fn(&mut RunProps, &RunProps) + Send + Sync),
|
||||
) -> Result<Value> {
|
||||
let st = s.doc()?;
|
||||
if let Some(t) = st.selection.text.clone() {
|
||||
let (a, b) = format_range(st, &t);
|
||||
// Only the levels of the paragraphs the selected range touches.
|
||||
let levels = if a == b { Vec::new() } else { master_levels(st, &t, t.ordered()) };
|
||||
let collapsed = a == b;
|
||||
edit_text(s, |body, _| {
|
||||
if levels.is_empty() {
|
||||
if collapsed {
|
||||
// Next typed text: put the props on an empty paragraph's end mark or keep pending.
|
||||
ed::format(body, a, b, f);
|
||||
} else {
|
||||
format_by_level(body, a, b, lf, &levels);
|
||||
}
|
||||
for (l, eff) in &levels {
|
||||
let mut lvl = body.list_style.level(*l).cloned().unwrap_or_default();
|
||||
lf(&mut lvl.run, eff);
|
||||
body.list_style.set(*l, lvl);
|
||||
ed::format(body, a, b, f);
|
||||
}
|
||||
})?;
|
||||
if collapsed {
|
||||
// Remember as pending typing props by inserting nothing: handled via end mark for
|
||||
// empty paragraphs; mid-paragraph carets keep surrounding formatting.
|
||||
}
|
||||
return ok();
|
||||
}
|
||||
let ids = st.selection.shapes.clone();
|
||||
@@ -43,19 +31,6 @@ pub(crate) fn format_run_levels(
|
||||
return Err(bad("format", SELECT_TEXT_OR_SHAPE));
|
||||
}
|
||||
let cells = st.selection.cells;
|
||||
// Effective run props of every level of the selected placeholders, resolved before editing.
|
||||
let effective: Vec<(ShapeId, Vec<RunProps>)> = if editing_master(st) {
|
||||
let ctx = crate::cmd::text::ctx_for(&st.doc, &st.selection);
|
||||
ids.iter()
|
||||
.filter_map(|id| {
|
||||
let sh = st.shape(*id).filter(|sh| sh.ph.is_some())?;
|
||||
let ctx = ctx.as_ref()?;
|
||||
Some((*id, (0..LIST_LEVELS).map(|l| deckcraft_model::resolve::level_style(ctx, sh, l).run).collect()))
|
||||
})
|
||||
.collect()
|
||||
} else {
|
||||
Vec::new()
|
||||
};
|
||||
edit_shapes(s, &json!({"ids": ids}), "format", |sh| {
|
||||
let id = sh.id;
|
||||
match &mut sh.kind {
|
||||
@@ -72,22 +47,7 @@ pub(crate) fn format_run_levels(
|
||||
}
|
||||
_ => {
|
||||
let body = sh.text.get_or_insert_with(TextBody::default);
|
||||
let eff = effective.iter().find(|(i, _)| *i == id).map(|(_, e)| e);
|
||||
match eff {
|
||||
Some(eff) => {
|
||||
let levels: Vec<(u8, RunProps)> = (0..LIST_LEVELS).zip(eff.iter().cloned()).collect();
|
||||
format_by_level(body, (0, 0), (usize::MAX, usize::MAX), lf, &levels);
|
||||
}
|
||||
None => ed::format_all(body, f),
|
||||
}
|
||||
// The whole shape is selected: every level of its list style.
|
||||
if let Some(eff) = eff {
|
||||
for (l, eff) in (0..LIST_LEVELS).zip(eff) {
|
||||
let mut lvl = body.list_style.level(l).cloned().unwrap_or_default();
|
||||
lf(&mut lvl.run, eff);
|
||||
body.list_style.set(l, lvl);
|
||||
}
|
||||
}
|
||||
ed::format_all(body, f);
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
@@ -98,55 +58,6 @@ pub(crate) fn format_run_levels(
|
||||
ok()
|
||||
}
|
||||
|
||||
/// Format the runs of [a, b) paragraph by paragraph, each with its own level's effective props, so
|
||||
/// a relative change (grow/shrink) steps every paragraph from its own size instead of one shared
|
||||
/// value. `a`/`b` are clamped to the body.
|
||||
fn format_by_level(body: &mut TextBody, a: Pos, b: Pos, lf: &(dyn Fn(&mut RunProps, &RunProps) + Send + Sync), levels: &[(u8, RunProps)]) {
|
||||
let Some(last) = body.paragraphs.len().checked_sub(1) else { return };
|
||||
let (first, end) = (a.0.min(last), b.0.min(last));
|
||||
for pi in first..=end {
|
||||
let p = &body.paragraphs[pi];
|
||||
let from = if pi == first { a.1.min(p.char_len()) } else { 0 };
|
||||
let to = if pi == end { b.1.min(p.char_len()) } else { p.char_len() };
|
||||
if from == to && p.char_len() > 0 {
|
||||
continue;
|
||||
}
|
||||
let lvl = p.level.min(LIST_LEVELS - 1);
|
||||
let Some((_, eff)) = levels.iter().find(|(l, _)| *l == lvl) else { continue };
|
||||
ed::format(body, (pi, from), (pi, to), &|r| lf(r, eff));
|
||||
}
|
||||
}
|
||||
|
||||
/// List-style levels (`lvl1pPr`–`lvl9pPr`) a placeholder passes on to the slides that use it.
|
||||
const LIST_LEVELS: u8 = 9;
|
||||
|
||||
/// Formatting a placeholder on a layout or master (Slide Master view) also sets its list style,
|
||||
/// which is what the placeholders on slides inherit; its own runs may be few or none.
|
||||
fn editing_master(st: &DocState) -> bool {
|
||||
matches!(st.selection.target, crate::Target::Layout { .. } | crate::Target::Master { .. })
|
||||
}
|
||||
|
||||
/// The distinct list-style levels of the paragraphs `range` touches in a layout or master
|
||||
/// placeholder, with each level's effective run props. Empty anywhere else.
|
||||
fn master_levels(st: &DocState, t: &TextSel, range: (Pos, Pos)) -> Vec<(u8, RunProps)> {
|
||||
if !editing_master(st) || t.notes || t.cell.is_some() {
|
||||
return Vec::new();
|
||||
}
|
||||
let (Some(sh), Some(body), Some(ctx)) =
|
||||
(st.shape(t.shape).filter(|sh| sh.ph.is_some()), body_of(st, t), crate::cmd::text::ctx_for(&st.doc, &st.selection))
|
||||
else {
|
||||
return Vec::new();
|
||||
};
|
||||
let mut levels: Vec<u8> =
|
||||
body.paragraphs.iter().enumerate().filter(|(i, _)| (range.0.0..=range.1.0).contains(i)).map(|(_, p)| p.level.min(LIST_LEVELS - 1)).collect();
|
||||
if body.paragraphs.is_empty() {
|
||||
levels.push(0);
|
||||
}
|
||||
levels.sort_unstable();
|
||||
levels.dedup();
|
||||
levels.into_iter().map(|l| (l, deckcraft_model::resolve::level_style(&ctx, sh, l).run)).collect()
|
||||
}
|
||||
|
||||
/// What `format_run` formats while editing: the selection, or for a bare caret the word it's in
|
||||
/// (without its trailing space). Empty when the caret is at a word boundary. Insert Hyperlink
|
||||
/// edits this same span, so the dialog reads it too.
|
||||
@@ -199,8 +110,6 @@ pub(crate) fn format_para(s: &mut Session, f: &(dyn Fn(&mut deckcraft_model::tex
|
||||
let st = s.doc()?;
|
||||
if let Some(t) = st.selection.text.clone() {
|
||||
let (a, b) = t.ordered();
|
||||
// Only the list-style levels of the paragraphs touched, on a layout or master placeholder.
|
||||
let levels: Vec<u8> = master_levels(st, &t, (a, b)).into_iter().map(|(l, _)| l).collect();
|
||||
edit_text(s, |body, _| {
|
||||
if body.paragraphs.is_empty() {
|
||||
body.paragraphs.push(Default::default());
|
||||
@@ -211,9 +120,6 @@ pub(crate) fn format_para(s: &mut Session, f: &(dyn Fn(&mut deckcraft_model::tex
|
||||
f(p);
|
||||
}
|
||||
}
|
||||
for l in levels {
|
||||
format_list_level(body, l, f);
|
||||
}
|
||||
})?;
|
||||
return ok();
|
||||
}
|
||||
@@ -222,10 +128,8 @@ pub(crate) fn format_para(s: &mut Session, f: &(dyn Fn(&mut deckcraft_model::tex
|
||||
return Err(bad("format", SELECT_TEXT_OR_SHAPE));
|
||||
}
|
||||
let cells = st.selection.cells;
|
||||
let on_master = editing_master(st);
|
||||
edit_shapes(s, &json!({"ids": ids}), "format", |sh| {
|
||||
let id = sh.id;
|
||||
let inherited = on_master && sh.ph.is_some();
|
||||
match &mut sh.kind {
|
||||
ShapeKind::Table(t) => {
|
||||
let range = crate::cmd::table::selected_range(cells, id, t);
|
||||
@@ -243,12 +147,6 @@ pub(crate) fn format_para(s: &mut Session, f: &(dyn Fn(&mut deckcraft_model::tex
|
||||
body.paragraphs.push(Default::default());
|
||||
}
|
||||
body.paragraphs.iter_mut().for_each(f);
|
||||
// The whole shape is selected: every level of its list style.
|
||||
if inherited {
|
||||
for l in 0..LIST_LEVELS {
|
||||
format_list_level(body, l, f);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
@@ -259,18 +157,6 @@ pub(crate) fn format_para(s: &mut Session, f: &(dyn Fn(&mut deckcraft_model::tex
|
||||
ok()
|
||||
}
|
||||
|
||||
/// Apply a paragraph change to one level of a body's list style.
|
||||
fn format_list_level(body: &mut TextBody, l: u8, f: &(dyn Fn(&mut deckcraft_model::text::Paragraph) + Send + Sync)) {
|
||||
let mut lvl = body.list_style.level(l).cloned().unwrap_or_default();
|
||||
let mut p = deckcraft_model::text::Paragraph { level: l, props: lvl.para, ..Default::default() };
|
||||
f(&mut p);
|
||||
// A level change (indent/outdent) means nothing for a list-style level.
|
||||
if p.level == l {
|
||||
lvl.para = p.props;
|
||||
body.list_style.set(l, lvl);
|
||||
}
|
||||
}
|
||||
|
||||
/// Apply body (text box) changes to the edited or selected shapes.
|
||||
pub(crate) fn format_body(s: &mut Session, f: &(dyn Fn(&mut deckcraft_model::text::BodyProps) + Send + Sync)) -> Result<Value> {
|
||||
let st = s.doc()?;
|
||||
|
||||
@@ -6,7 +6,7 @@ use deckcraft_model::Transition;
|
||||
use deckcraft_model::anim::TRANSITIONS;
|
||||
use serde_json::{Value, json};
|
||||
|
||||
use super::{CommandSpec, NO_ID_SPACE, always, bad, bool_param, cmd, has_slide, ok, opt_usize_param, str_param};
|
||||
use super::{CommandSpec, NO_ID_SPACE, always, bad, bool_param, cmd, has_slide, ok, str_param, usize_param};
|
||||
use crate::{Result, Session};
|
||||
|
||||
pub fn specs() -> Vec<CommandSpec> {
|
||||
@@ -44,9 +44,9 @@ pub fn specs() -> Vec<CommandSpec> {
|
||||
]
|
||||
}
|
||||
|
||||
fn slide_indices(s: &Session, p: &Value, cmd: &str) -> Result<Vec<usize>> {
|
||||
fn slide_indices(s: &Session, p: &Value) -> Result<Vec<usize>> {
|
||||
let st = s.doc()?;
|
||||
if let Some(i) = opt_usize_param(p, "index", cmd)? {
|
||||
if let Some(i) = usize_param(p, "index") {
|
||||
return Ok(vec![i]);
|
||||
}
|
||||
let v: Vec<usize> = st.selection.slides.iter().filter_map(|id| st.doc.slide_index(*id)).collect();
|
||||
@@ -59,7 +59,7 @@ fn set(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
TRANSITIONS.iter().find(|t| t.0.eq_ignore_ascii_case(kind)).ok_or_else(|| bad("transition.set", format!("unknown transition `{kind}`")))?;
|
||||
let option = str_param(p, "option").map(String::from).or_else(|| info.4.first().map(|o| o.to_string())).unwrap_or_default();
|
||||
let duration = p.get("duration").and_then(Value::as_u64).map(|v| v.min(59_000) as u32);
|
||||
let list = slide_indices(s, p, "transition.set")?;
|
||||
let list = slide_indices(s, p)?;
|
||||
s.edit(|doc, _| {
|
||||
for i in list {
|
||||
if let Some(sl) = doc.slides.get_mut(i) {
|
||||
@@ -83,7 +83,7 @@ fn set(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
|
||||
fn options(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
let option = str_param(p, "option").ok_or_else(|| bad("transition.options", "missing `option`"))?.to_string();
|
||||
let list = slide_indices(s, p, "transition.options")?;
|
||||
let list = slide_indices(s, p)?;
|
||||
s.edit(|doc, _| {
|
||||
for i in list {
|
||||
if let Some(sl) = doc.slides.get_mut(i)
|
||||
@@ -98,7 +98,7 @@ fn options(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
}
|
||||
|
||||
fn timing(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
let list = slide_indices(s, p, "transition.timing")?;
|
||||
let list = slide_indices(s, p)?;
|
||||
s.edit(|doc, _| {
|
||||
for i in list {
|
||||
if let Some(sl) = doc.slides.get_mut(i) {
|
||||
@@ -127,7 +127,7 @@ fn timing(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
|
||||
/// Transitions › Sound: embed an audio file as the slides' transition sound, or `none` to remove it.
|
||||
fn sound(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
let list = slide_indices(s, p, "transition.sound")?;
|
||||
let list = slide_indices(s, p)?;
|
||||
let looping = bool_param(p, "loop");
|
||||
let edit_all = |s: &mut Session, f: &dyn Fn(&mut Transition)| {
|
||||
s.edit(|doc, _| {
|
||||
@@ -215,29 +215,3 @@ fn apply_all(s: &mut Session, _p: &Value) -> Result<Value> {
|
||||
fn list(_s: &mut Session, _p: &Value) -> Result<Value> {
|
||||
Ok(Value::Array(TRANSITIONS.iter().map(|t| json!({"id": t.0, "label": t.1, "category": t.2, "duration": t.3, "options": t.4})).collect()))
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use serde_json::json;
|
||||
|
||||
use crate::Session;
|
||||
|
||||
#[test]
|
||||
fn invalid_index_params_are_rejected_not_defaulted_to_selection() {
|
||||
let mut s = Session::with_new();
|
||||
while s.doc().expect("doc").doc.slides.len() < 3 {
|
||||
s.execute("slide.new", &json!({})).expect("new slide");
|
||||
}
|
||||
for bad in [json!(-1), json!(1.5)] {
|
||||
for (cmd, p) in [
|
||||
("transition.set", json!({"kind": "fade", "index": bad})),
|
||||
("transition.options", json!({"option": "x", "index": bad})),
|
||||
("transition.timing", json!({"duration": 500, "index": bad})),
|
||||
("transition.sound", json!({"none": true, "index": bad})),
|
||||
] {
|
||||
assert!(s.execute(cmd, &p).is_err(), "{cmd} must reject {p}");
|
||||
assert!(s.doc().expect("doc").doc.slides.iter().all(|sl| sl.transition.is_none()), "{cmd} {p}");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+18
-112
@@ -121,10 +121,9 @@ pub fn diff(cur: &Presentation, rev: &Presentation, by_position: bool) -> Vec<Ch
|
||||
let cur_matched: HashSet<usize> = pairs.iter().map(|p| p.0).collect();
|
||||
let rev_matched: HashSet<usize> = pairs.iter().map(|p| p.1).collect();
|
||||
let mut out = vec![];
|
||||
let mut media = Media::new(cur, rev);
|
||||
for (i, j) in &pairs {
|
||||
let (Some(a), Some(b)) = (cur.slides.get(*i), rev.slides.get(*j)) else { continue };
|
||||
let details = slide_details(cur, a, rev, b, &mut media);
|
||||
let details = slide_details(cur, a, rev, b);
|
||||
if !details.is_empty() {
|
||||
out.push(Change { kind: ChangeKind::Changed, slide: *i, number: i.saturating_add(1), only_in: None, revised: Some(*j), details });
|
||||
}
|
||||
@@ -141,18 +140,10 @@ pub fn diff(cur: &Presentation, rev: &Presentation, by_position: bool) -> Vec<Ch
|
||||
});
|
||||
}
|
||||
}
|
||||
// Each revised slide's open match (the first pair naming it).
|
||||
let mut rev_to_cur: HashMap<usize, usize> = HashMap::new();
|
||||
for p in &pairs {
|
||||
rev_to_cur.entry(p.1).or_insert(p.0);
|
||||
}
|
||||
// After the open slide that matches the nearest matched revised slide before it.
|
||||
let mut at = 0;
|
||||
for (j, s) in rev.slides.iter().enumerate() {
|
||||
if let Some(i) = rev_to_cur.get(&j) {
|
||||
at = i + 1;
|
||||
}
|
||||
if !rev_matched.contains(&j) {
|
||||
// After the open slide that matches the revised slide before it.
|
||||
let at = (0..j).rev().find_map(|k| pairs.iter().find(|p| p.1 == k).map(|p| p.0 + 1)).unwrap_or(0);
|
||||
out.push(Change {
|
||||
kind: ChangeKind::Inserted,
|
||||
slide: at,
|
||||
@@ -183,34 +174,8 @@ fn shape_label(s: &Shape) -> String {
|
||||
if s.name.trim().is_empty() { format!("Shape {}", s.id.0) } else { s.name.clone() }
|
||||
}
|
||||
|
||||
/// Both decks' media bytes by id, and which picture pairs were already compared: a deck can
|
||||
/// show one picture many times, and its bytes are compared once per pair.
|
||||
struct Media<'a> {
|
||||
cur: HashMap<MediaId, &'a [u8]>,
|
||||
rev: HashMap<MediaId, &'a [u8]>,
|
||||
same: HashMap<(MediaId, MediaId), bool>,
|
||||
}
|
||||
|
||||
impl<'a> Media<'a> {
|
||||
fn new(cur: &'a Presentation, rev: &'a Presentation) -> Self {
|
||||
// The first item with an id wins, as in `Presentation::media`.
|
||||
let index = |p: &'a Presentation| {
|
||||
let mut m = HashMap::new();
|
||||
for x in &p.media {
|
||||
m.entry(x.id).or_insert(x.data.as_slice());
|
||||
}
|
||||
m
|
||||
};
|
||||
Media { cur: index(cur), rev: index(rev), same: HashMap::new() }
|
||||
}
|
||||
/// Whether the pictures (by media id, `None` for a shape that is not a picture) differ.
|
||||
fn differ(&mut self, a: Option<MediaId>, b: Option<MediaId>) -> bool {
|
||||
let (ba, bb) = (a.and_then(|x| self.cur.get(&x)), b.and_then(|y| self.rev.get(&y)));
|
||||
match (a, b) {
|
||||
(Some(x), Some(y)) if ba.is_some() && bb.is_some() => !*self.same.entry((x, y)).or_insert_with(|| ba == bb),
|
||||
_ => ba.is_some() || bb.is_some(),
|
||||
}
|
||||
}
|
||||
fn media_bytes(p: &Presentation, id: MediaId) -> Option<&[u8]> {
|
||||
p.media(id).map(|m| m.data.as_slice())
|
||||
}
|
||||
|
||||
/// A shape without its ids (shape and media), for comparing across decks.
|
||||
@@ -239,7 +204,7 @@ fn turn(ctx: Option<&Ctx>, s: &Shape) -> (f64, bool, bool) {
|
||||
drawn_box(ctx, s).map_or((0.0, false, false), |x| (x.rot, x.flip_h, x.flip_v))
|
||||
}
|
||||
|
||||
fn shape_details(ca: Option<&Ctx>, a: &Shape, cb: Option<&Ctx>, b: &Shape, media: &mut Media, out: &mut Vec<String>) {
|
||||
fn shape_details(cp: &Presentation, ca: Option<&Ctx>, a: &Shape, rp: &Presentation, cb: Option<&Ctx>, b: &Shape, out: &mut Vec<String>) {
|
||||
let label = shape_label(b);
|
||||
let text = |s: &Shape| s.text.as_ref().map(|t| t.text()).unwrap_or_default();
|
||||
let mut what = vec![];
|
||||
@@ -269,11 +234,11 @@ fn shape_details(ca: Option<&Ctx>, a: &Shape, cb: Option<&Ctx>, b: &Shape, media
|
||||
if a.fill != b.fill || a.line != b.line || a.style != b.style || a.effects != b.effects {
|
||||
what.push("formatting");
|
||||
}
|
||||
let pic = |s: &Shape| match &s.kind {
|
||||
ShapeKind::Picture { fill } => Some(fill.media),
|
||||
let pic = |p: &Presentation, s: &Shape| match &s.kind {
|
||||
ShapeKind::Picture { fill } => media_bytes(p, fill.media).map(|b| b.to_vec()),
|
||||
_ => None,
|
||||
};
|
||||
if media.differ(pic(a), pic(b)) {
|
||||
if pic(cp, a) != pic(rp, b) {
|
||||
what.push("picture");
|
||||
}
|
||||
// Anything the checks above do not cover (alt text, hidden, a link…) is reported too, also
|
||||
@@ -300,32 +265,21 @@ fn shape_details(ca: Option<&Ctx>, a: &Shape, cb: Option<&Ctx>, b: &Shape, media
|
||||
}
|
||||
}
|
||||
|
||||
fn slide_details(cp: &Presentation, a: &Slide, rp: &Presentation, b: &Slide, media: &mut Media) -> Vec<String> {
|
||||
fn slide_details(cp: &Presentation, a: &Slide, rp: &Presentation, b: &Slide) -> Vec<String> {
|
||||
let mut out = vec![];
|
||||
let mut used: HashSet<usize> = HashSet::new();
|
||||
let (ca, cb) = (Ctx::for_slide(cp, a), Ctx::for_slide(rp, b));
|
||||
// The revised shapes by id (first wins) and by name (in order, with a cursor past used ones).
|
||||
let mut by_id: HashMap<ShapeId, usize> = HashMap::new();
|
||||
let mut by_name: HashMap<&str, (Vec<usize>, usize)> = HashMap::new();
|
||||
for (k, x) in b.shapes.iter().enumerate() {
|
||||
by_id.entry(x.id).or_insert(k);
|
||||
if !x.name.is_empty() {
|
||||
by_name.entry(x.name.as_str()).or_default().0.push(k);
|
||||
}
|
||||
}
|
||||
for sa in &a.shapes {
|
||||
let k = by_id.get(&sa.id).copied().or_else(|| {
|
||||
let (ks, next) = by_name.get_mut(sa.name.as_str())?;
|
||||
while ks.get(*next).is_some_and(|k| used.contains(k)) {
|
||||
*next += 1;
|
||||
}
|
||||
ks.get(*next).copied()
|
||||
});
|
||||
let k = b
|
||||
.shapes
|
||||
.iter()
|
||||
.position(|x| x.id == sa.id)
|
||||
.or_else(|| b.shapes.iter().enumerate().position(|(k, x)| !used.contains(&k) && x.name == sa.name && !x.name.is_empty()));
|
||||
match k {
|
||||
Some(k) if !used.contains(&k) => {
|
||||
used.insert(k);
|
||||
if let Some(sb) = b.shapes.get(k) {
|
||||
shape_details(ca.as_ref(), sa, cb.as_ref(), sb, media, &mut out);
|
||||
shape_details(cp, ca.as_ref(), sa, rp, cb.as_ref(), sb, &mut out);
|
||||
}
|
||||
}
|
||||
_ => out.push(format!("{} deleted (only in the deck)", shape_label(sa))),
|
||||
@@ -380,34 +334,19 @@ pub fn adopt(doc: &mut Presentation, rev: &Presentation, slide: &Slide, replacin
|
||||
let mut sl = slide.clone();
|
||||
// Media.
|
||||
let mut media_map: HashMap<MediaId, MediaId> = HashMap::new();
|
||||
// Both decks' media by id (the first item with an id wins, as in `Presentation::media`),
|
||||
// looked up once per reference instead of searched.
|
||||
let mut rev_at: HashMap<MediaId, usize> = HashMap::new();
|
||||
for (i, m) in rev.media.iter().enumerate() {
|
||||
rev_at.entry(m.id).or_insert(i);
|
||||
}
|
||||
let mut doc_at: HashMap<MediaId, usize> = HashMap::new();
|
||||
for (i, m) in doc.media.iter().enumerate() {
|
||||
doc_at.entry(m.id).or_insert(i);
|
||||
}
|
||||
for id in crate::media_refs::slide_media(&sl) {
|
||||
let Some(m) = rev_at.get(&id).and_then(|&i| rev.media.get(i)) else { continue };
|
||||
match doc_at.get(&id).and_then(|&i| doc.media.get(i)) {
|
||||
let Some(m) = rev.media(id) else { continue };
|
||||
match doc.media(id) {
|
||||
Some(x) if x.data == m.data => {}
|
||||
None => {
|
||||
// Keeps the revision's id, so later ids must start past it.
|
||||
doc_at.insert(id, doc.media.len());
|
||||
doc.media.push(m.clone());
|
||||
doc.next_id = doc.next_id.max(id.0);
|
||||
}
|
||||
Some(_) => {
|
||||
let before = doc.media.len();
|
||||
let new = doc
|
||||
.add_media(&m.name, &m.content_type, m.data.as_ref().clone())
|
||||
.ok_or_else(|| crate::cmd::bad("review.accept", crate::cmd::NO_ID_SPACE))?;
|
||||
if doc.media.len() > before {
|
||||
doc_at.entry(new).or_insert(before);
|
||||
}
|
||||
media_map.insert(id, new);
|
||||
}
|
||||
}
|
||||
@@ -580,39 +519,6 @@ mod tests {
|
||||
assert_eq!(s.doc().unwrap().doc.slides.len(), n, "comparing alone changes nothing");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn many_inserted_slides_and_same_named_pictures_compare_by_index() {
|
||||
use std::sync::Arc;
|
||||
|
||||
use deckcraft_model::style::PictureFill;
|
||||
use deckcraft_model::{Shape, ShapeId, ShapeKind, SlideId, defaults};
|
||||
|
||||
// One slide of 3,000 pictures of one image, all named "Box"; the revision has the same
|
||||
// pictures under other ids (matched by name, in order) and 3,000 slides inserted after it.
|
||||
let mut cur = defaults::new_presentation(None);
|
||||
let img = cur.add_media("a.png", "image/png", vec![7; 4096]).unwrap();
|
||||
let mut rev = cur.clone();
|
||||
let pic = |id: u32| Shape {
|
||||
id: ShapeId(id),
|
||||
name: "Box".into(),
|
||||
xfrm: Some(deckcraft_model::Xfrm::new(10.0, 10.0, 50.0, 50.0)),
|
||||
kind: ShapeKind::Picture { fill: PictureFill { media: img, ..Default::default() } },
|
||||
..Default::default()
|
||||
};
|
||||
Arc::make_mut(&mut cur.slides[0]).shapes = (0..3000).map(|k| pic(100_000 + k)).collect();
|
||||
Arc::make_mut(&mut rev.slides[0]).shapes = (0..3000).map(|k| pic(200_000 + k)).collect();
|
||||
let mut base = (*rev.slides[0]).clone();
|
||||
base.shapes.clear();
|
||||
for k in 0..3000 {
|
||||
let mut s = base.clone();
|
||||
s.id = SlideId(300_000 + k);
|
||||
rev.slides.push(Arc::new(s));
|
||||
}
|
||||
let changes = super::diff(&cur, &rev, false);
|
||||
assert_eq!(changes.len(), 3000, "only the inserted slides differ");
|
||||
assert!(changes.iter().all(|c| c.kind == super::ChangeKind::Inserted && c.slide == 1));
|
||||
}
|
||||
|
||||
/// A deck with one slide: a table whose cell has a picture fill, a picture background and a
|
||||
/// transition sound. `tag` starts the bytes of each.
|
||||
fn media_deck(tag: &str) -> deckcraft_model::Presentation {
|
||||
|
||||
@@ -1,21 +1,14 @@
|
||||
//! Connectors glued to shapes: connection sites, snapping line ends to them, and rerouting attached
|
||||
//! connectors whenever the shapes they connect move, resize or rotate.
|
||||
|
||||
use deckcraft_geom::{Affine, Point, Rect, Xfrm};
|
||||
use deckcraft_geom::{Point, Xfrm};
|
||||
use deckcraft_model::{Presentation, Shape, ShapeId, ShapeKind};
|
||||
|
||||
use crate::Selection;
|
||||
use crate::cmd::xfrm_of;
|
||||
|
||||
/// Connection sites of a top-level shape `s` in slide coordinates (index = site index stored on
|
||||
/// connectors). For a shape inside a group use [`sites_of`] or [`sites_in`].
|
||||
/// Connection sites of `s` in slide coordinates (index = site index stored on connectors).
|
||||
pub fn sites(doc: &Presentation, sel: &Selection, s: &Shape) -> Vec<Point> {
|
||||
sites_in(doc, sel, s, Affine::IDENTITY)
|
||||
}
|
||||
|
||||
/// Connection sites of `s` in slide coordinates, `to_abs` mapping the space its box lives in (the
|
||||
/// slide, or the child space of the groups around it) onto the slide.
|
||||
pub fn sites_in(doc: &Presentation, sel: &Selection, s: &Shape, to_abs: Affine) -> Vec<Point> {
|
||||
if s.is_line() || matches!(s.kind, ShapeKind::Group { .. } | ShapeKind::Ink { .. }) {
|
||||
return vec![];
|
||||
}
|
||||
@@ -24,42 +17,10 @@ pub fn sites_in(doc: &Presentation, sel: &Selection, s: &Shape, to_abs: Affine)
|
||||
if local.is_empty() {
|
||||
local = vec![Point::new(x.w / 2.0, 0.0), Point::new(0.0, x.h / 2.0), Point::new(x.w / 2.0, x.h), Point::new(x.w, x.h / 2.0)];
|
||||
}
|
||||
let a = to_abs * x.affine();
|
||||
let a = x.affine();
|
||||
local.into_iter().map(|p| a * p).collect()
|
||||
}
|
||||
|
||||
/// The shape `id` anywhere in `shapes` (a group's member included) with the affine that maps the
|
||||
/// space its box lives in onto the slide: a connector glues to a grouped shape, as in PowerPoint,
|
||||
/// and its sites are where the group draws them.
|
||||
pub fn locate<'a>(doc: &Presentation, sel: &Selection, shapes: &'a [Shape], id: ShapeId) -> Option<(&'a Shape, Affine)> {
|
||||
fn rec<'a>(doc: &Presentation, sel: &Selection, shapes: &'a [Shape], id: ShapeId, to_abs: Affine, depth: usize) -> Option<(&'a Shape, Affine)> {
|
||||
if depth > 64 {
|
||||
return None;
|
||||
}
|
||||
for s in shapes {
|
||||
if s.id == id {
|
||||
return Some((s, to_abs));
|
||||
}
|
||||
if let ShapeKind::Group { children, child } = &s.kind {
|
||||
let x = xfrm_of(doc, sel, s);
|
||||
let ch = Rect::new(child.x, child.y, child.x + child.w, child.y + child.h);
|
||||
let nested = to_abs * deckcraft_geom::group_child_affine(&x, ch);
|
||||
if let Some(f) = rec(doc, sel, children, id, nested, depth + 1) {
|
||||
return Some(f);
|
||||
}
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
rec(doc, sel, shapes, id, Affine::IDENTITY, 0)
|
||||
}
|
||||
|
||||
/// Connection sites of shape `id` anywhere in `shapes`, in slide coordinates; `None` when there is
|
||||
/// no such shape.
|
||||
pub fn sites_of(doc: &Presentation, sel: &Selection, shapes: &[Shape], id: ShapeId) -> Option<Vec<Point>> {
|
||||
locate(doc, sel, shapes, id).map(|(s, a)| sites_in(doc, sel, s, a))
|
||||
}
|
||||
|
||||
/// The nearest connection site within `radius` of `p` on any visible shape except `exclude`.
|
||||
pub fn nearest_site(
|
||||
doc: &Presentation,
|
||||
@@ -107,8 +68,8 @@ pub fn reroute(doc: &mut Presentation, sel: &Selection) {
|
||||
let snapshot: Vec<Shape> = list.clone();
|
||||
let view = doc.clone();
|
||||
let site = |id: ShapeId, idx: u32| -> Option<Point> {
|
||||
// Anywhere in the tree: a connector glued to a shape that is then grouped keeps following it.
|
||||
let v = sites_of(&view, sel, &snapshot, id)?;
|
||||
let s = snapshot.iter().find(|s| s.id == id)?;
|
||||
let v = sites(&view, sel, s);
|
||||
v.get(idx as usize).or(v.first()).copied()
|
||||
};
|
||||
let mut updates: Vec<(usize, Xfrm, bool, bool)> = vec![];
|
||||
|
||||
@@ -85,20 +85,14 @@ impl AutomationRoots {
|
||||
|
||||
/// May the file or folder at `path` be read (or written)? `Err` says why not.
|
||||
pub fn check(&self, path: &str, access: Access) -> Result<(), String> {
|
||||
self.check_path(Path::new(path), access)
|
||||
}
|
||||
|
||||
/// [`Self::check`] for a path that may not be UTF-8: the checked path is the one opened.
|
||||
pub fn check_path(&self, path: &Path, access: Access) -> Result<(), String> {
|
||||
let path_text = path.display();
|
||||
let roots = match access {
|
||||
Access::Read => &self.read,
|
||||
Access::Write => &self.write,
|
||||
};
|
||||
if roots.is_empty() {
|
||||
return match access {
|
||||
Access::Read => Err(format!("automation filesystem access is not granted: read authority is absent: {path_text}")),
|
||||
Access::Write => Err(format!("automation filesystem access is not granted: write authority is absent: {path_text}")),
|
||||
Access::Read => Err(format!("automation filesystem access is not granted: read authority is absent: {path}")),
|
||||
Access::Write => Err(format!("automation filesystem access is not granted: write authority is absent: {path}")),
|
||||
};
|
||||
}
|
||||
let resolved = resolve(path)?;
|
||||
@@ -107,8 +101,8 @@ impl AutomationRoots {
|
||||
}
|
||||
let roots = roots.iter().map(|root| root.shown.display().to_string()).collect::<Vec<_>>().join(", ");
|
||||
match access {
|
||||
Access::Read => Err(format!("automation path rejected: outside the read roots {roots}: {path_text}")),
|
||||
Access::Write => Err(format!("automation path rejected: outside the write roots {roots}: {path_text}")),
|
||||
Access::Read => Err(format!("automation path rejected: outside the read roots {roots}: {path}")),
|
||||
Access::Write => Err(format!("automation path rejected: outside the write roots {roots}: {path}")),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -197,23 +191,17 @@ pub fn check_write(path: &str) -> Result<(), String> {
|
||||
current().map_or(Ok(()), |r| r.check(path, Access::Write))
|
||||
}
|
||||
|
||||
/// May `path` be written here? Always, without roots in force. For a path that may not be UTF-8.
|
||||
pub fn check_write_path(path: &Path) -> Result<(), String> {
|
||||
current().map_or(Ok(()), |r| r.check_path(path, Access::Write))
|
||||
}
|
||||
|
||||
/// `path` made absolute with its links followed; for a path that doesn't exist (yet), its nearest
|
||||
/// existing folder's links followed and the rest appended.
|
||||
fn resolve(path: &Path) -> Result<PathBuf, String> {
|
||||
if path.as_os_str().is_empty() {
|
||||
fn resolve(path: &str) -> Result<PathBuf, String> {
|
||||
if path.is_empty() {
|
||||
return Err("automation path rejected: path is empty".into());
|
||||
}
|
||||
let shown = path.display();
|
||||
// Checked as given: making `…\NUL` absolute gives the device path `\\.\NUL` on Windows.
|
||||
if cfg!(windows) && path.components().any(|c| matches!(c, Component::Normal(n) if is_device_name(&n.to_string_lossy()))) {
|
||||
return Err(format!("automation path rejected: reserved device names are not allowed: {shown}"));
|
||||
if cfg!(windows) && Path::new(path).components().any(|c| matches!(c, Component::Normal(n) if is_device_name(&n.to_string_lossy()))) {
|
||||
return Err(format!("automation path rejected: reserved device names are not allowed: {path}"));
|
||||
}
|
||||
let absolute = std::path::absolute(path).map_err(|e| format!("automation path rejected: cannot resolve {shown}: {e}"))?;
|
||||
let absolute = std::path::absolute(path).map_err(|e| format!("automation path rejected: cannot resolve {path}: {e}"))?;
|
||||
// The names below the nearest existing folder, last first.
|
||||
let mut missing: Vec<OsString> = vec![];
|
||||
let mut at = absolute.as_path();
|
||||
@@ -224,11 +212,11 @@ fn resolve(path: &Path) -> Result<PathBuf, String> {
|
||||
return Ok(found);
|
||||
}
|
||||
Err(e) => match std::fs::symlink_metadata(at) {
|
||||
Ok(m) if m.file_type().is_symlink() => return Err(format!("automation path rejected: a link that leads nowhere: {shown}")),
|
||||
Ok(m) if m.file_type().is_symlink() => return Err(format!("automation path rejected: a link that leads nowhere: {path}")),
|
||||
// There, yet its real path can't be had (no permission): nothing to compare.
|
||||
Ok(_) => return Err(format!("automation path rejected: cannot resolve {shown}: {e}")),
|
||||
Ok(_) => return Err(format!("automation path rejected: cannot resolve {path}: {e}")),
|
||||
Err(e) if e.kind() == std::io::ErrorKind::NotFound => {}
|
||||
Err(e) => return Err(format!("automation path rejected: cannot resolve {shown}: {e}")),
|
||||
Err(e) => return Err(format!("automation path rejected: cannot resolve {path}: {e}")),
|
||||
},
|
||||
}
|
||||
let parent = at.parent();
|
||||
@@ -238,12 +226,12 @@ fn resolve(path: &Path) -> Result<PathBuf, String> {
|
||||
at = parent;
|
||||
}
|
||||
(Some(Component::ParentDir), _) => {
|
||||
return Err(format!("automation path rejected: parent traversal below a folder that doesn't exist: {shown}"));
|
||||
return Err(format!("automation path rejected: parent traversal below a folder that doesn't exist: {path}"));
|
||||
}
|
||||
// Down to the root (or drive) and it doesn't exist either.
|
||||
_ => {
|
||||
let e = std::io::Error::from(std::io::ErrorKind::NotFound);
|
||||
return Err(format!("automation path rejected: cannot resolve {shown}: {e}"));
|
||||
return Err(format!("automation path rejected: cannot resolve {path}: {e}"));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
//! Fonts a presentation uses that this machine doesn't have, so opening it can say so (its text
|
||||
//! draws in a stand-in; see `FontDb::document_face`).
|
||||
|
||||
use std::collections::HashSet;
|
||||
|
||||
use deckcraft_fonts::FontDb;
|
||||
use deckcraft_model::resolve::{self, Ctx};
|
||||
use deckcraft_model::text::{Bullet, TextBody};
|
||||
@@ -14,13 +12,8 @@ use deckcraft_model::{Presentation, Shape, ShapeKind};
|
||||
/// font to load and are left for the round trip.
|
||||
pub fn install_embedded_fonts(p: &Presentation) {
|
||||
let db = FontDb::global();
|
||||
// Faces can name one media item many times; each is copied and enumerated once.
|
||||
let mut seen = HashSet::new();
|
||||
for font in &p.embedded_fonts {
|
||||
for id in font.faces().into_iter().flatten() {
|
||||
if !seen.insert(id) {
|
||||
continue;
|
||||
}
|
||||
let Some(item) = p.media(id) else { continue };
|
||||
let Some(sfnt) = deckcraft_fonts::embedded_sfnt(&item.data) else { continue };
|
||||
db.add_font(sfnt.to_vec());
|
||||
@@ -34,8 +27,12 @@ pub fn install_embedded_fonts(p: &Presentation) {
|
||||
/// and can't be installed, so they aren't reported.
|
||||
pub fn missing_fonts(p: &Presentation) -> Vec<String> {
|
||||
let db = FontDb::global();
|
||||
// used_fonts is already free of case-insensitive duplicates.
|
||||
let mut out: Vec<String> = used_fonts(p).into_iter().filter(|f| !f.starts_with('.') && !db.has_family(f)).collect();
|
||||
let mut out: Vec<String> = Vec::new();
|
||||
for family in used_fonts(p) {
|
||||
if !family.starts_with('.') && !db.has_family(&family) && !out.iter().any(|f| f.eq_ignore_ascii_case(&family)) {
|
||||
out.push(family);
|
||||
}
|
||||
}
|
||||
out.sort_by_key(|f| f.to_lowercase());
|
||||
out
|
||||
}
|
||||
@@ -43,10 +40,9 @@ pub fn missing_fonts(p: &Presentation) -> Vec<String> {
|
||||
/// The families the slides' text runs and bullets are drawn in.
|
||||
pub fn used_fonts(p: &Presentation) -> Vec<String> {
|
||||
let mut out: Vec<String> = Vec::new();
|
||||
let mut seen: HashSet<String> = HashSet::new();
|
||||
let mut add = |f: String| {
|
||||
let f = f.trim().to_string();
|
||||
if !f.is_empty() && seen.insert(f.to_ascii_lowercase()) {
|
||||
if !f.is_empty() && !out.iter().any(|o| o.eq_ignore_ascii_case(&f)) {
|
||||
out.push(f);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
//! Last-resort guard: a panic that escapes a command —
|
||||
//! Last-resort guard (craftrules `standards/never-crash.md`): a panic that escapes a command —
|
||||
//! any edit, import or export, from the UI, the CLI, MCP or the control channel — becomes an
|
||||
//! error, and the document is kept as it was before the command (and saved for crash recovery
|
||||
//! when a recovery directory is set). It is a safety net: commands still must not panic.
|
||||
|
||||
+2
-31
@@ -41,15 +41,12 @@ pub fn write_file(path: &str, bytes: &[u8]) -> Result<(), String> {
|
||||
Ok(canonical) => canonical,
|
||||
Err(_) => std::path::PathBuf::from(path),
|
||||
};
|
||||
let target_str = target_path.to_str().unwrap_or(path);
|
||||
let existing_perms = std::fs::metadata(&target_path).ok().map(|m| m.permissions());
|
||||
|
||||
let mut attempts = 0;
|
||||
let (tmp, mut file) = loop {
|
||||
// A short fixed-size name: appending to the target's name could exceed the file name limit.
|
||||
let tmp_name = format!(".deckcraft.{}.{}.saving", std::process::id(), COUNTER.fetch_add(1, Ordering::Relaxed));
|
||||
let tmp = target_path.with_file_name(tmp_name);
|
||||
// The sibling must pass the same check: when `path` is the write root itself it lands outside.
|
||||
crate::file_access::check_write(&tmp.to_string_lossy())?;
|
||||
let tmp = format!("{target_str}.{}.{}.saving", std::process::id(), COUNTER.fetch_add(1, Ordering::Relaxed));
|
||||
match std::fs::OpenOptions::new().write(true).create_new(true).open(&tmp) {
|
||||
Ok(f) => break (tmp, f),
|
||||
Err(e) if e.kind() == std::io::ErrorKind::AlreadyExists && attempts < 16 => attempts += 1,
|
||||
@@ -123,30 +120,4 @@ mod tests {
|
||||
let _ = std::fs::remove_file(&test_file);
|
||||
let _ = std::fs::remove_dir(&tmp_dir);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn write_file_overwrites_file_with_name_near_the_length_limit() {
|
||||
let dir = std::env::temp_dir().join("deckcraft_io_long_name_test");
|
||||
let _ = std::fs::create_dir_all(&dir);
|
||||
let file = dir.join(format!("{}.pptx", "a".repeat(250)));
|
||||
std::fs::write(&file, b"old").unwrap();
|
||||
let path_str = file.to_str().unwrap();
|
||||
|
||||
write_file(path_str, b"new").unwrap();
|
||||
assert_eq!(read_file(path_str).unwrap(), b"new");
|
||||
|
||||
let _ = std::fs::remove_file(&file);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn write_file_to_the_write_root_itself_creates_no_sibling_outside_it() {
|
||||
let base = std::env::temp_dir().join(format!("deckcraft_io_root_test_{}", std::process::id()));
|
||||
let root = base.join("root");
|
||||
std::fs::create_dir_all(&root).unwrap();
|
||||
let roots = crate::file_access::AutomationRoots::new(&[], std::slice::from_ref(&root)).unwrap().unwrap();
|
||||
let res = crate::file_access::confine(Some(&roots), || write_file(root.to_str().unwrap(), b"x"));
|
||||
assert!(res.unwrap_err().contains("outside the write root"));
|
||||
assert_eq!(std::fs::read_dir(&base).unwrap().count(), 1, "only the root is there");
|
||||
let _ = std::fs::remove_dir_all(&base);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -454,30 +454,17 @@ pub struct MediaStatus {
|
||||
}
|
||||
|
||||
/// Clipboard contents copied within the app.
|
||||
/// The theme a copy drew with. The colours and the map stay apart, as in
|
||||
/// `deckcraft_model::resolve::Ctx::color`: the map renames only `tx1`, `bg1`, `tx2` and `bg2`, and
|
||||
/// a direct `dk1` keeps the theme's colour.
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct SourceTheme {
|
||||
/// The theme's own colours, no colour map applied.
|
||||
pub colors: deckcraft_color::ColorScheme,
|
||||
pub color_map: Vec<(String, String)>,
|
||||
pub fonts: deckcraft_model::theme::FontScheme,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Default)]
|
||||
pub struct Clipboard {
|
||||
pub shapes: Vec<Shape>,
|
||||
/// The copied placeholders as they were, by id. `shapes` holds a slide's placeholders as
|
||||
/// regular shapes; a paste in Slide Master view takes these, which stay placeholders.
|
||||
pub placeholders: Vec<Shape>,
|
||||
pub slides: Vec<Slide>,
|
||||
/// Media the copied shapes/slides reference.
|
||||
pub media: Vec<deckcraft_model::MediaItem>,
|
||||
pub text: Option<deckcraft_model::TextBody>,
|
||||
pub plain: String,
|
||||
/// The theme the shapes were copied under, for Paste Special › Keep Source Formatting.
|
||||
pub source_theme: Option<SourceTheme>,
|
||||
/// The copied shapes' theme colours (colour map applied) and fonts, for Paste Special ›
|
||||
/// Keep Source Formatting.
|
||||
pub source_theme: Option<(deckcraft_color::ColorScheme, deckcraft_model::theme::FontScheme)>,
|
||||
/// How many times the shapes were pasted (each paste offsets further).
|
||||
pub pastes: u32,
|
||||
}
|
||||
@@ -824,32 +811,10 @@ fn clamp_selection(st: &mut DocState) {
|
||||
/// Default for [`History::byte_limit`].
|
||||
pub const UNDO_BYTE_BUDGET: usize = 256 << 20;
|
||||
|
||||
/// A rough size of a text body in memory: its paragraphs and runs, at their inline struct sizes.
|
||||
fn text_cost(t: &deckcraft_model::text::TextBody) -> usize {
|
||||
use deckcraft_model::text::{Paragraph, Run};
|
||||
let (para, run) = (std::mem::size_of::<Paragraph>(), std::mem::size_of::<Run>());
|
||||
t.paragraphs.iter().map(|p| para + p.runs.iter().map(|r| run + r.text.len()).sum::<usize>()).sum()
|
||||
}
|
||||
|
||||
/// A rough size of comments in memory, replies included.
|
||||
fn comments_cost(cs: &[deckcraft_model::Comment], depth: usize) -> usize {
|
||||
let kids = |c: &deckcraft_model::Comment| if depth < 64 { comments_cost(&c.replies, depth + 1) } else { 0 };
|
||||
cs.iter().map(|c| 256 + c.author.len() + c.initials.len() + c.text.len() + c.date.len() + kids(c)).sum()
|
||||
}
|
||||
|
||||
/// A rough size of a slide's own data besides its shapes: notes, comments, tags and kept XML,
|
||||
/// all copied with the slide when an edit changes it.
|
||||
fn slide_extra_cost(s: &deckcraft_model::Slide) -> usize {
|
||||
text_cost(&s.notes)
|
||||
+ comments_cost(&s.comments, 0)
|
||||
+ s.name.len()
|
||||
+ s.tags.iter().map(|(k, v)| 48 + k.len() + v.len()).sum::<usize>()
|
||||
+ s.raw_ext.as_ref().map_or(0, String::len)
|
||||
}
|
||||
|
||||
/// A rough size of a shape in memory: the struct, its text and kept XML, its children.
|
||||
fn shape_cost(sh: &deckcraft_model::Shape, depth: usize) -> usize {
|
||||
let text: usize = sh.text.as_ref().map_or(0, text_cost);
|
||||
let text: usize =
|
||||
sh.text.as_ref().map_or(0, |t| t.paragraphs.iter().map(|p| 96 + p.runs.iter().map(|r| 128 + r.text.len()).sum::<usize>()).sum());
|
||||
let kids: usize = if depth < 64 { sh.children().iter().map(|c| shape_cost(c, depth + 1)).sum() } else { 0 };
|
||||
768 + text + sh.raw_ext.as_ref().map_or(0, String::len) + kids
|
||||
}
|
||||
@@ -867,7 +832,7 @@ pub fn undo_cost(doc: &Presentation, newer: &Presentation) -> usize {
|
||||
let media: HashSet<*const Vec<u8>> = newer.media.iter().map(|m| Arc::as_ptr(&m.data)).collect();
|
||||
let mut n = 64 * doc.slides.len();
|
||||
for s in doc.slides.iter().filter(|s| !slides.contains(&Arc::as_ptr(s))) {
|
||||
n += 512 + shapes_cost(&s.shapes) + slide_extra_cost(s);
|
||||
n += 512 + shapes_cost(&s.shapes);
|
||||
}
|
||||
for m in doc.masters.iter().filter(|m| !masters.contains(&Arc::as_ptr(m))) {
|
||||
n += 4096 + shapes_cost(&m.shapes) + m.layouts.iter().map(|l| 512 + shapes_cost(&l.shapes)).sum::<usize>();
|
||||
|
||||
@@ -259,16 +259,6 @@ mod tests {
|
||||
dir
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn instance_identity_is_stable_nonzero_and_json_safe() {
|
||||
let id = instance_id();
|
||||
assert_eq!(instance_id(), id);
|
||||
assert!(id != 0 && id <= UID_MASK);
|
||||
assert_eq!(id & 0xFF_FFFF, 0);
|
||||
let encoded = serde_json::to_value(id).unwrap();
|
||||
assert_eq!(encoded.as_f64().unwrap() as u64, id);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn recovers_unsaved_presentations() {
|
||||
let dir = temp("recovery");
|
||||
|
||||
+19
-140
@@ -171,9 +171,7 @@ impl Dictionary {
|
||||
["COMPOUNDWORDMAX", n, ..] => d.compound_max = n.parse().unwrap_or(0),
|
||||
["REP", n] => {
|
||||
for _ in 0..n.parse::<usize>().unwrap_or(0) {
|
||||
// The count is the file's claim: stop when the records run out.
|
||||
let Some(line) = lines.next() else { break };
|
||||
let r: Vec<&str> = line.split_whitespace().collect();
|
||||
let r: Vec<&str> = lines.next().unwrap_or("").split_whitespace().collect();
|
||||
if let [_, a, b, ..] = r.as_slice() {
|
||||
d.rep.push((a.replace('_', " "), b.replace('_', " ")));
|
||||
}
|
||||
@@ -184,8 +182,7 @@ impl Dictionary {
|
||||
let Some(flag) = one_flag(Some(flag), mode) else { continue };
|
||||
let cross = cross.eq_ignore_ascii_case("y");
|
||||
for _ in 0..n.parse::<usize>().unwrap_or(0) {
|
||||
let Some(line) = lines.next() else { break };
|
||||
let r: Vec<&str> = line.split_whitespace().collect();
|
||||
let r: Vec<&str> = lines.next().unwrap_or("").split_whitespace().collect();
|
||||
let [_, _, strip, add, rest @ ..] = r.as_slice() else { continue };
|
||||
let (add, cont) = match add.split_once('/') {
|
||||
Some((a, c)) => (a, parse_flags(c, mode)),
|
||||
@@ -307,12 +304,7 @@ impl Dictionary {
|
||||
|
||||
/// `w` made of dictionary parts allowed in compounds (`COMPOUNDFLAG`, or the begin, middle and
|
||||
/// end flags), each at least `COMPOUNDMIN` characters long.
|
||||
fn compounded(&self, w: &[char], index: usize, failed: &mut HashSet<(usize, usize)>) -> bool {
|
||||
// `w` is always a tail of the checked word, so its length names it: a (tail, part index)
|
||||
// that failed once fails again however the earlier parts were split.
|
||||
if failed.contains(&(w.len(), index)) {
|
||||
return false;
|
||||
}
|
||||
fn compounded(&self, w: &[char], index: usize) -> bool {
|
||||
if self.compound.is_none() && self.compound_end.is_none() {
|
||||
return false;
|
||||
}
|
||||
@@ -330,11 +322,10 @@ impl Dictionary {
|
||||
if [self.compound, self.compound_end].into_iter().flatten().any(|f| self.compound_part(rest, f, true)) {
|
||||
return true;
|
||||
}
|
||||
if self.compounded(rest, index + 1, failed) {
|
||||
if self.compounded(rest, index + 1) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
failed.insert((w.len(), index));
|
||||
false
|
||||
}
|
||||
|
||||
@@ -361,7 +352,7 @@ impl Dictionary {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
self.compounded(&w, 0, &mut HashSet::new())
|
||||
self.compounded(&w, 0)
|
||||
}
|
||||
|
||||
/// Is `word` spelt correctly? Capitalised and upper-case forms of a lower-case word pass.
|
||||
@@ -585,15 +576,6 @@ fn cache() -> &'static Cache {
|
||||
CACHE.get_or_init(Default::default)
|
||||
}
|
||||
|
||||
/// Language strings remembered by [`dictionary`]; a string past it is resolved again on each call.
|
||||
const MAX_CACHED_LANGS: usize = 256;
|
||||
|
||||
/// Parsed dictionaries by their affix file, so aliases of one language share it.
|
||||
fn by_files() -> &'static Mutex<HashMap<PathBuf, Arc<Dictionary>>> {
|
||||
static M: OnceLock<Mutex<HashMap<PathBuf, Arc<Dictionary>>>> = OnceLock::new();
|
||||
M.get_or_init(Default::default)
|
||||
}
|
||||
|
||||
/// The dictionary for `lang` (a BCP-47 tag such as `en-US`), loaded once per process.
|
||||
pub fn dictionary(lang: &str) -> Option<Arc<Dictionary>> {
|
||||
let cache = cache();
|
||||
@@ -604,23 +586,10 @@ pub fn dictionary(lang: &str) -> Option<Arc<Dictionary>> {
|
||||
return d.clone();
|
||||
}
|
||||
let loaded = find_in(&search_dirs(), lang).and_then(|(a, d)| {
|
||||
// Tags that resolve to the same files share one parsed dictionary.
|
||||
if let Ok(m) = by_files().lock()
|
||||
&& let Some(dict) = m.get(&a)
|
||||
{
|
||||
return Some(dict.clone());
|
||||
}
|
||||
let (bytes_a, bytes_d) = (std::fs::read(&a).ok()?, std::fs::read(d).ok()?);
|
||||
let dict = Arc::new(Dictionary::parse(&bytes_a, &bytes_d));
|
||||
// A concurrent loader of the same files may have inserted first: share its copy.
|
||||
if let Ok(mut m) = by_files().lock() {
|
||||
return Some(m.entry(a).or_insert(dict).clone());
|
||||
}
|
||||
Some(dict)
|
||||
let (a, d) = (std::fs::read(a).ok()?, std::fs::read(d).ok()?);
|
||||
Some(Arc::new(Dictionary::parse(&a, &d)))
|
||||
});
|
||||
if let Ok(mut c) = cache.lock()
|
||||
&& c.len() < MAX_CACHED_LANGS
|
||||
{
|
||||
if let Ok(mut c) = cache.lock() {
|
||||
c.insert(key, loaded.clone());
|
||||
}
|
||||
loaded
|
||||
@@ -699,8 +668,6 @@ pub fn add_user_word(word: &str) -> std::io::Result<()> {
|
||||
return Ok(());
|
||||
}
|
||||
let path = user_dictionary_path().ok_or_else(|| std::io::Error::new(std::io::ErrorKind::NotFound, "no folder for the user dictionary"))?;
|
||||
// A confined automation client may write only inside its write roots, this file included.
|
||||
crate::file_access::check_write_path(&path).map_err(|e| std::io::Error::new(std::io::ErrorKind::PermissionDenied, e))?;
|
||||
if let Some(d) = path.parent() {
|
||||
std::fs::create_dir_all(d)?;
|
||||
}
|
||||
@@ -733,31 +700,16 @@ pub struct Word {
|
||||
pub repeated: Option<usize>,
|
||||
}
|
||||
|
||||
/// Longest language tag a word is checked in. BCP 47 tags in documents are a few bytes; a longer
|
||||
/// value from a file is no language we have a dictionary for, and copying it into every word of
|
||||
/// a paragraph would multiply it.
|
||||
const MAX_LANG_LEN: usize = 64;
|
||||
|
||||
/// The words of `p`, each with its run's language (`lang` on the run, else `default`). Words
|
||||
/// with digits are skipped; apostrophes inside a word belong to it; hyphenated words are checked
|
||||
/// part by part. A language over [`MAX_LANG_LEN`] counts as none.
|
||||
/// part by part.
|
||||
pub fn paragraph_words(p: &deckcraft_model::Paragraph, default: &str) -> Vec<Word> {
|
||||
let mut chars: Vec<(char, usize)> = vec![];
|
||||
for (ri, r) in p.runs.iter().enumerate() {
|
||||
chars.extend(r.content().chars().map(|c| (c, ri)));
|
||||
}
|
||||
let default = if default.len() > MAX_LANG_LEN { "" } else { default };
|
||||
let lang_of = |ri: usize| {
|
||||
p.runs
|
||||
.get(ri)
|
||||
.and_then(|r| r.props.lang.as_deref())
|
||||
.filter(|l| !l.is_empty())
|
||||
.map(|l| if l.len() > MAX_LANG_LEN { "" } else { l })
|
||||
.unwrap_or(default)
|
||||
.to_string()
|
||||
};
|
||||
// Combining marks (decomposed accents) belong to the letter before them.
|
||||
let inside = |c: char| c.is_alphanumeric() || deckcraft_model::words::is_combining_mark(c) || c == '\'' || c == '’' || c == '-';
|
||||
let lang_of = |ri: usize| p.runs.get(ri).and_then(|r| r.props.lang.clone()).filter(|l| !l.is_empty()).unwrap_or_else(|| default.to_string());
|
||||
let inside = |c: char| c.is_alphanumeric() || c == '\'' || c == '’' || c == '-';
|
||||
let mut out = vec![];
|
||||
let mut i = 0;
|
||||
while i < chars.len() {
|
||||
@@ -773,14 +725,8 @@ pub fn paragraph_words(p: &deckcraft_model::Paragraph, default: &str) -> Vec<Wor
|
||||
while a < b && !chars[a].0.is_alphanumeric() {
|
||||
a += 1;
|
||||
}
|
||||
// Trim back to the last letter or digit, keeping only the marks that sit on it
|
||||
// (a mark on a trailing apostrophe goes with the apostrophe).
|
||||
while b > a {
|
||||
let k = (a..b).rev().find(|&k| !deckcraft_model::words::is_combining_mark(chars[k].0)).unwrap_or(a);
|
||||
if chars[k].0.is_alphanumeric() {
|
||||
break;
|
||||
}
|
||||
b = k;
|
||||
while b > a && !chars[b - 1].0.is_alphanumeric() {
|
||||
b -= 1;
|
||||
}
|
||||
let text: String = chars[a..b].iter().map(|c| c.0).collect();
|
||||
if text.is_empty() || text.chars().any(|c| c.is_ascii_digit()) || !text.chars().any(char::is_alphabetic) {
|
||||
@@ -838,18 +784,15 @@ pub fn misspelt(p: &deckcraft_model::Paragraph, default: &str, ignore: &[String]
|
||||
.chain(repeated(p, default))
|
||||
.collect::<Vec<_>>()
|
||||
.into_iter()
|
||||
.fold((vec![], HashMap::new()), |(mut v, mut at): (Vec<Word>, HashMap<usize, usize>), w| {
|
||||
.fold(vec![], |mut v: Vec<Word>, w| {
|
||||
// A repeated misspelt word is reported once, as repeated.
|
||||
match at.get(&w.start).and_then(|&i| v.get_mut(i)) {
|
||||
Some(x) => *x = w,
|
||||
None => {
|
||||
at.insert(w.start, v.len());
|
||||
v.push(w);
|
||||
}
|
||||
if let Some(i) = v.iter().position(|x| x.start == w.start) {
|
||||
v[i] = w;
|
||||
} else {
|
||||
v.push(w);
|
||||
}
|
||||
(v, at)
|
||||
v
|
||||
})
|
||||
.0
|
||||
}
|
||||
|
||||
/// Use `d` for `lang` from now on in this process (a dictionary the app loaded itself).
|
||||
@@ -876,20 +819,6 @@ SFX D Y 1\nSFX D 0 ed/S .\n";
|
||||
Dictionary::parse(AFF.as_bytes(), DIC.as_bytes())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn nfd_word_not_split() {
|
||||
let p = deckcraft_model::Paragraph::new("Das na\u{ef}ve Caf\u{e9} und cafe\u{301} na\u{308}ive");
|
||||
let w: Vec<String> = paragraph_words(&p, "en-US").into_iter().map(|w| w.text).collect();
|
||||
assert_eq!(w, ["Das", "na\u{ef}ve", "Caf\u{e9}", "und", "cafe\u{301}", "na\u{308}ive"]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_mark_on_trailing_punctuation_is_trimmed_with_it() {
|
||||
let p = deckcraft_model::Paragraph::new("word'\u{301} cafe\u{301}' x");
|
||||
let w: Vec<String> = paragraph_words(&p, "en-US").into_iter().map(|w| w.text).collect();
|
||||
assert_eq!(w, ["word", "cafe\u{301}", "x"]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn curly_apostrophe_matches_ascii_entry() {
|
||||
let d = Dictionary::parse(AFF.as_bytes(), "2\ndon't\nwork\n".as_bytes());
|
||||
@@ -930,14 +859,6 @@ SFX D Y 1\nSFX D 0 ed/S .\n";
|
||||
assert!(d.suggest("workingslides", 5).contains(&"working slides".to_string()));
|
||||
}
|
||||
|
||||
/// An affix header can claim billions of records; parsing stops where the file does.
|
||||
#[test]
|
||||
fn a_header_count_past_the_records_ends_with_them() {
|
||||
let aff = b"SFX S Y 4000000000\nSFX S 0 s .\nREP 4000000000\nREP a b\n";
|
||||
let d = Dictionary::parse(aff, b"1\ncat/S\n".as_slice());
|
||||
assert!(d.check("cats"));
|
||||
}
|
||||
|
||||
/// #1175: compound splitting is quadratic in the word's length, so a word longer than
|
||||
/// `MAX_CHECK_CHARS` is not checked; shorter ones still are.
|
||||
#[test]
|
||||
@@ -972,17 +893,6 @@ SFX D Y 1\nSFX D 0 ed/S .\n";
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn overlapping_compound_roots_with_a_failing_ending_stay_cheap() {
|
||||
// Roots a..aaaaaaaa overlap, so many splits of "aaa…ab" reach the same (tail, part) state; unmemoized that is millions of searches.
|
||||
let d = Dictionary::parse(
|
||||
b"COMPOUNDFLAG X\nCOMPOUNDMIN 1\n".as_slice(),
|
||||
b"8\na/X\naa/X\naaa/X\naaaa/X\naaaaa/X\naaaaaa/X\naaaaaaa/X\naaaaaaaa/X\n".as_slice(),
|
||||
);
|
||||
assert!(!d.check(&format!("{}b", "a".repeat(99))));
|
||||
assert!(d.check(&"a".repeat(20)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn runs_give_words_their_language() {
|
||||
use deckcraft_model::{Paragraph, Run, RunProps};
|
||||
@@ -1022,37 +932,6 @@ SFX D Y 1\nSFX D 0 ed/S .\n";
|
||||
assert_eq!(w, vec!["check", "done"]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_confined_client_cannot_write_the_user_dictionary_outside_its_roots() {
|
||||
let root = std::env::temp_dir().join(format!("deckcraft-dict-root-{}", std::process::id()));
|
||||
std::fs::create_dir_all(&root).unwrap();
|
||||
let roots = crate::file_access::AutomationRoots::new(std::slice::from_ref(&root), std::slice::from_ref(&root)).unwrap();
|
||||
// The user dictionary is a file of its own outside `root`, not whatever HOME points at.
|
||||
let outside = std::env::temp_dir().join(format!("deckcraft-dict-outside-{}", std::process::id()));
|
||||
set_user_dictionary_path(outside.join("user.dic"));
|
||||
let r = crate::file_access::confine(roots.as_ref(), || add_user_word("zqxconfinedword"));
|
||||
assert_eq!(r.map_err(|e| e.kind()), Err(std::io::ErrorKind::PermissionDenied));
|
||||
assert!(!outside.exists(), "the refused write created nothing");
|
||||
let _ = std::fs::remove_dir_all(&root);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_long_paragraph_with_a_huge_language_is_checked_in_linear_work() {
|
||||
use deckcraft_model::{Paragraph, Run, RunProps};
|
||||
// 100,000 copies of one word under a 1 MB language value: every word after the first is
|
||||
// a repeat, and no word carries a copy of the value.
|
||||
let lang = "x".repeat(1 << 20);
|
||||
let p =
|
||||
Paragraph { runs: vec![Run::with("the ".repeat(100_000), RunProps { lang: Some(lang), ..Default::default() })], ..Default::default() };
|
||||
let words = paragraph_words(&p, "en-US");
|
||||
assert_eq!(words.len(), 100_000);
|
||||
assert!(words.iter().all(|w| w.lang.is_empty()));
|
||||
let bad = misspelt(&p, "en-US", &[]);
|
||||
assert_eq!(bad.len(), 99_999);
|
||||
assert!(bad.iter().all(|w| w.repeated.is_some()));
|
||||
assert!(paragraph_words(&Paragraph::new("word"), &"y".repeat(100)).iter().all(|w| w.lang.is_empty()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn long_and_numeric_flags_and_latin1() {
|
||||
let d = Dictionary::parse(b"FLAG long\nSFX Aa Y 1\nSFX Aa 0 s .\n", b"1\ncat/Aa\n");
|
||||
|
||||
+1
-1229
File diff suppressed because it is too large
Load Diff
@@ -17,7 +17,6 @@ mod geometry;
|
||||
mod hit;
|
||||
mod pointer;
|
||||
|
||||
pub(crate) use drag::MAX_GESTURE_POINTS;
|
||||
pub use hit::{line_xfrm, resize_box, table_rows, text_pos};
|
||||
|
||||
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
|
||||
@@ -151,10 +150,8 @@ enum Drag {
|
||||
},
|
||||
Resize {
|
||||
id: ShapeId,
|
||||
orig: Xfrm,
|
||||
ids: Vec<ShapeId>,
|
||||
handle: u8,
|
||||
origs: Vec<(ShapeId, Xfrm)>,
|
||||
orig: Xfrm,
|
||||
start: Point,
|
||||
},
|
||||
LineEnd {
|
||||
@@ -276,8 +273,6 @@ pub struct ToolState {
|
||||
pub path_preview: Vec<Point>,
|
||||
pub path_smooth: bool,
|
||||
pub path_closing: bool,
|
||||
/// Whether the last freeform press added a vertex (it adds none at the point cap).
|
||||
pub press_added_vertex: bool,
|
||||
/// The shape in Edit Points: its vertices are handles instead of the box's.
|
||||
pub edit_points: Option<ShapeId>,
|
||||
/// The vertex (path, index) whose control handles show in Edit Points.
|
||||
|
||||
@@ -4,12 +4,8 @@ use super::{Drag, Hit, Point, Result, Session, ShapeId, ShapeKind, TextSel, Tool
|
||||
impl Session {
|
||||
pub(super) fn double_click(&mut self, p: Point, tol: f64) -> Result<Value> {
|
||||
if let Some(Drag::Freeform { mode, mut points, .. }) = self.tool.drag.clone() {
|
||||
// The double-click's own click added a duplicate vertex (none at the point cap, where
|
||||
// the existing vertices are all kept).
|
||||
while self.tool.press_added_vertex
|
||||
&& points.len() > 2
|
||||
&& points.last().zip(points.get(points.len() - 2)).is_some_and(|(a, b)| (*a - *b).hypot() <= tol)
|
||||
{
|
||||
// The double-click's own click added a duplicate vertex.
|
||||
while points.len() > 2 && points.last().zip(points.get(points.len() - 2)).is_some_and(|(a, b)| (*a - *b).hypot() <= tol) {
|
||||
points.pop();
|
||||
}
|
||||
self.tool.drag = Some(Drag::Freeform { mode, points, pressed: false });
|
||||
|
||||
@@ -12,16 +12,8 @@ struct MoveDrag {
|
||||
enter_text: Option<(ShapeId, Point)>,
|
||||
}
|
||||
|
||||
/// Samples one ink or freeform gesture keeps; later pointer moves extend it no further.
|
||||
pub(crate) const MAX_GESTURE_POINTS: usize = 100_000;
|
||||
|
||||
impl Session {
|
||||
/// Update the active drag interaction using the pointer position and modifiers.
|
||||
pub(super) fn drag(&mut self, p: Point, mods: Mods, tol: f64) -> Result<Value> {
|
||||
if matches!(self.tool.drag, Some(Drag::Ink { .. })) {
|
||||
self.extend_ink(p);
|
||||
return Ok(Value::Null);
|
||||
}
|
||||
let Some(drag) = self.tool.drag.clone() else { return Ok(Value::Null) };
|
||||
match drag {
|
||||
Drag::Guide { index } => {
|
||||
@@ -34,48 +26,10 @@ impl Session {
|
||||
Drag::Move { ids, start, origs, moved, duplicate, enter_text } => {
|
||||
return self.drag_move(MoveDrag { ids, start, origs, moved, duplicate, enter_text }, p, mods, tol);
|
||||
}
|
||||
Drag::Resize { id, orig, ids, handle, origs, start } => {
|
||||
let keep_aspect = mods.shift || self.is_picture(id);
|
||||
let nx = resize_box(&orig, handle, p - start, keep_aspect, mods.alt);
|
||||
let nx = self.snap_resize(nx, handle, &ids, tol);
|
||||
let sx = if orig.w > 1e-9 { (nx.w / orig.w) * if nx.flip_h != orig.flip_h { -1.0 } else { 1.0 } - 1.0 } else { 0.0 };
|
||||
let sy = if orig.h > 1e-9 { (nx.h / orig.h) * if nx.flip_v != orig.flip_v { -1.0 } else { 1.0 } - 1.0 } else { 0.0 };
|
||||
let div = if mods.alt { 2.0 } else { 1.0 };
|
||||
let hx = match handle {
|
||||
0 | 6 | 7 => -1.0,
|
||||
2..=4 => 1.0,
|
||||
_ => 0.0,
|
||||
};
|
||||
let hy = match handle {
|
||||
0..=2 => -1.0,
|
||||
4..=6 => 1.0,
|
||||
_ => 0.0,
|
||||
};
|
||||
for (tid, o) in &origs {
|
||||
let target_nx = if *tid == id {
|
||||
nx
|
||||
} else {
|
||||
let picture = self.is_picture(*tid);
|
||||
let (sx, sy) = if picture && matches!(handle, 0 | 2 | 4 | 6) {
|
||||
// Use the axis with the larger relative change so an unchanged
|
||||
// dimension cannot cancel shrinking. Preserve each axis's flip.
|
||||
let scale_x = 1.0 + sx;
|
||||
let scale_y = 1.0 + sy;
|
||||
let scale = if sx.abs() >= sy.abs() { scale_x.abs() } else { scale_y.abs() };
|
||||
(scale.copysign(scale_x) - 1.0, scale.copysign(scale_y) - 1.0)
|
||||
} else {
|
||||
(sx, sy)
|
||||
};
|
||||
|
||||
let lp = Affine::rotate(o.rot.to_radians())
|
||||
* Point::new(
|
||||
hx * sx * o.w / div * if o.flip_h { -1.0 } else { 1.0 },
|
||||
hy * sy * o.h / div * if o.flip_v { -1.0 } else { 1.0 },
|
||||
);
|
||||
resize_box(o, handle, Vec2::new(lp.x, lp.y), picture, mods.alt)
|
||||
};
|
||||
self.set_xfrm(*tid, target_nx)?;
|
||||
}
|
||||
Drag::Resize { id, handle, orig, start } => {
|
||||
let nx = resize_box(&orig, handle, p - start, mods.shift || self.is_picture(id), mods.alt);
|
||||
let nx = self.snap_resize(nx, &orig, handle, id, tol);
|
||||
self.set_xfrm(id, nx)?;
|
||||
}
|
||||
Drag::LineEnd { id, end, orig } => {
|
||||
let a = orig.affine();
|
||||
@@ -188,7 +142,7 @@ impl Session {
|
||||
}
|
||||
Drag::Freeform { mode, mut points, pressed } => {
|
||||
// Dragging scribbles (except for curves, which only take clicked points).
|
||||
if pressed && mode != "curve" && points.len() < MAX_GESTURE_POINTS && points.last().is_none_or(|l| (*l - p).hypot() >= 1.5) {
|
||||
if pressed && mode != "curve" && points.last().is_none_or(|l| (*l - p).hypot() >= 1.5) {
|
||||
points.push(p);
|
||||
}
|
||||
let mut v = points.clone();
|
||||
@@ -198,24 +152,15 @@ impl Session {
|
||||
self.tool.path_preview = v;
|
||||
self.tool.drag = Some(Drag::Freeform { mode, points, pressed });
|
||||
}
|
||||
// Handled above.
|
||||
Drag::Ink { .. } => {}
|
||||
Drag::Ink { mut points } => {
|
||||
points.push(self.ink_sample(p));
|
||||
self.tool.ink_preview = points.clone();
|
||||
self.tool.drag = Some(Drag::Ink { points });
|
||||
}
|
||||
}
|
||||
Ok(Value::Null)
|
||||
}
|
||||
|
||||
/// Ink grows by one sample per move: extend it (and its preview) in place rather than
|
||||
/// copying the whole stroke twice per event, up to [`MAX_GESTURE_POINTS`].
|
||||
fn extend_ink(&mut self, p: Point) {
|
||||
let sample = self.ink_sample(p);
|
||||
if let Some(Drag::Ink { points }) = self.tool.drag.as_mut()
|
||||
&& points.len() < MAX_GESTURE_POINTS
|
||||
{
|
||||
points.push(sample);
|
||||
self.tool.ink_preview.push(sample);
|
||||
}
|
||||
}
|
||||
|
||||
fn drag_move(&mut self, drag: MoveDrag, p: Point, mods: Mods, tol: f64) -> Result<Value> {
|
||||
let MoveDrag { ids, start, origs, moved, duplicate, enter_text } = drag;
|
||||
let mut d = p - start;
|
||||
@@ -252,12 +197,7 @@ impl Session {
|
||||
}
|
||||
}
|
||||
// A connector dragged away from shapes that stay put comes unglued at those ends.
|
||||
let mut moved: Vec<ShapeId> = vec![];
|
||||
for (id, _) in &origs2 {
|
||||
if let Some(sh) = deckcraft_model::find_shape(list, *id) {
|
||||
deckcraft_model::walk(std::slice::from_ref(sh), &mut |s, _| moved.push(s.id));
|
||||
}
|
||||
}
|
||||
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] {
|
||||
|
||||
@@ -101,7 +101,6 @@ impl Session {
|
||||
let list = cmd::current_shapes(doc, sel).cloned().unwrap_or_default();
|
||||
let mut copies: Vec<Shape> = list.iter().filter(|x| ids.contains(&x.id)).cloned().collect();
|
||||
let boxes: Vec<Xfrm> = copies.iter().map(|c| xfrm_of(doc, sel, c)).collect();
|
||||
cmd::edit::detach_on_slide(doc, sel, &mut copies);
|
||||
cmd::edit::reid("edit.duplicate", doc, &mut copies)?;
|
||||
for (c, b) in copies.iter_mut().zip(boxes) {
|
||||
c.xfrm = Some(b);
|
||||
@@ -229,7 +228,7 @@ impl Session {
|
||||
}
|
||||
|
||||
/// Snap the edges that resize handle `handle` drags in the resized box `x` to the smart guides.
|
||||
pub(super) fn snap_resize(&mut self, x: Xfrm, handle: u8, exclude: &[ShapeId], tol: f64) -> Xfrm {
|
||||
pub(super) fn snap_resize(&mut self, x: Xfrm, _orig: &Xfrm, handle: u8, id: ShapeId, tol: f64) -> Xfrm {
|
||||
if x.rot != 0.0 {
|
||||
self.tool.guides.clear();
|
||||
return x;
|
||||
@@ -242,7 +241,7 @@ impl Session {
|
||||
let (top, bottom) = (matches!(handle, 0..=2), matches!(handle, 4..=6));
|
||||
let (moves_left, moves_right) = if x.flip_h { (right, left) } else { (left, right) };
|
||||
let (moves_top, moves_bottom) = if x.flip_v { (bottom, top) } else { (top, bottom) };
|
||||
let (off, guides) = self.snap_edges(r, exclude, tol * 1.5, true, [moves_left, false, moves_right], [moves_top, false, moves_bottom]);
|
||||
let (off, guides) = self.snap_edges(r, &[id], tol * 1.5, true, [moves_left, false, moves_right], [moves_top, false, moves_bottom]);
|
||||
let mut nx = x;
|
||||
if moves_left {
|
||||
nx.x += off.x;
|
||||
|
||||
@@ -216,7 +216,6 @@ pub(super) fn in_table_cell(st: &crate::DocState, id: ShapeId, p: Point) -> bool
|
||||
})
|
||||
}
|
||||
|
||||
/// Resolve a shape's target at slide point `p`, using its effective fill for interior hits.
|
||||
pub(super) fn hit_shape(st: &crate::DocState, sh: &Shape, p: Point, tol: f64) -> Option<Hit> {
|
||||
let x = xfrm_of(&st.doc, &st.selection, sh);
|
||||
let inv = x.affine().inverse();
|
||||
@@ -268,11 +267,8 @@ pub(super) fn hit_shape(st: &crate::DocState, sh: &Shape, p: Point, tol: f64) ->
|
||||
return None;
|
||||
}
|
||||
let has_text = matches!(sh.kind, ShapeKind::Shape) && (sh.text.as_ref().is_some_and(|t| !t.is_empty()) || sh.ph.is_some() || sh.text_box);
|
||||
// Match rendering: an explicit No Fill overrides the style, and style/theme fills
|
||||
// can themselves be No Fill. Empty, text-less interiors then let clicks through.
|
||||
let fill =
|
||||
cmd::text::ctx_for(&st.doc, &st.selection).map(|ctx| deckcraft_model::resolve::fill(&ctx, sh).0).unwrap_or_else(|| sh.fill.clone());
|
||||
let filled = !matches!(fill, None | Some(deckcraft_model::Fill::None));
|
||||
// Empty interiors of unfilled, text-less shapes don't catch clicks.
|
||||
let filled = !matches!(sh.fill, Some(deckcraft_model::Fill::None)) || sh.style.is_some();
|
||||
let geo = deckcraft_render::shape_geometry(sh, x.w, x.h);
|
||||
let in_geo = {
|
||||
use deckcraft_geom::KurboShape;
|
||||
|
||||
@@ -220,7 +220,6 @@ impl Session {
|
||||
}
|
||||
}
|
||||
|
||||
/// Handle a pointer press and start the applicable tool interaction.
|
||||
fn down(&mut self, p: Point, mods: Mods, tol: f64) -> Result<Value> {
|
||||
if let Some(Drag::Freeform { mode, mut points, .. }) = self.tool.drag.clone() {
|
||||
// Next click of a freeform: close on the start point, else add a vertex.
|
||||
@@ -228,10 +227,7 @@ impl Session {
|
||||
self.tool.drag = Some(Drag::Freeform { mode, points, pressed: false });
|
||||
return self.finish_freeform(true);
|
||||
}
|
||||
self.tool.press_added_vertex = points.len() < super::MAX_GESTURE_POINTS;
|
||||
if self.tool.press_added_vertex {
|
||||
points.push(p);
|
||||
}
|
||||
points.push(p);
|
||||
self.tool.path_preview = points.clone();
|
||||
self.tool.drag = Some(Drag::Freeform { mode, points, pressed: true });
|
||||
return Ok(Value::Null);
|
||||
@@ -307,12 +303,8 @@ impl Session {
|
||||
Some(Hit::TableRow { id, row }) => self.begin_table_border(id, false, row, p)?,
|
||||
Some(Hit::Resize { id, handle }) => {
|
||||
let orig = st.shape(id).map(|s| xfrm_of(&st.doc, &st.selection, s)).unwrap_or_default();
|
||||
let ids: Vec<ShapeId> = if st.selection.shapes.contains(&id) { st.selection.shapes.clone() } else { vec![id] };
|
||||
let origs: Vec<(ShapeId, Xfrm)> =
|
||||
ids.iter().filter_map(|i| st.shape(*i).filter(|s| !s.locked).map(|s| (*i, xfrm_of(&st.doc, &st.selection, s)))).collect();
|
||||
let ids: Vec<ShapeId> = origs.iter().map(|(id, _)| *id).collect();
|
||||
self.begin("Size");
|
||||
self.tool.drag = Some(Drag::Resize { id, orig, ids, handle, origs, start: p });
|
||||
self.tool.drag = Some(Drag::Resize { id, handle, orig, start: p });
|
||||
}
|
||||
Some(Hit::LineEnd { id, end }) => {
|
||||
let orig = st.shape(id).map(|s| xfrm_of(&st.doc, &st.selection, s)).unwrap_or_default();
|
||||
@@ -444,7 +436,6 @@ impl Session {
|
||||
Ok(json!({"hit": serde_json::to_value(&hit).unwrap_or_default()}))
|
||||
}
|
||||
|
||||
/// Handle a pointer release and finalize the active tool interaction.
|
||||
fn up(&mut self, p: Point, mods: Mods) -> Result<Value> {
|
||||
let drag = self.tool.drag.take();
|
||||
self.tool.guides.clear();
|
||||
@@ -567,22 +558,10 @@ impl Session {
|
||||
out = self.add_ink(points)?;
|
||||
}
|
||||
}
|
||||
Some(Drag::Resize { origs, .. }) => {
|
||||
let changed: Vec<ShapeId> = origs
|
||||
.iter()
|
||||
.filter_map(|(id, orig)| {
|
||||
let current = self.active().and_then(|st| st.shape(*id).map(|s| xfrm_of(&st.doc, &st.selection, s)))?;
|
||||
|
||||
(current != *orig).then_some(*id)
|
||||
})
|
||||
.collect();
|
||||
if !changed.is_empty() {
|
||||
for id in changed {
|
||||
let _ = cmd::text::fit_text_box(self, id);
|
||||
self.refit_after_resize(id)?;
|
||||
}
|
||||
}
|
||||
Some(Drag::Resize { id, .. }) => {
|
||||
self.commit();
|
||||
let _ = cmd::text::fit_text_box(self, id);
|
||||
self.refit_after_resize(id)?;
|
||||
}
|
||||
Some(Drag::TextSelect { before, .. }) => {
|
||||
out = self.paint_if_selection_changed(before, out)?;
|
||||
|
||||
@@ -169,14 +169,8 @@ impl FontDb {
|
||||
if bundled.contains(fam) {
|
||||
continue;
|
||||
}
|
||||
if self.is_loaded(fam) || (self.system_fallback_enabled() && self.has_family(fam)) {
|
||||
// With system fallback off, only a face that is already loaded: `face` could load more catalog files.
|
||||
let f = if self.system_fallback_enabled() {
|
||||
self.face(fam, style)
|
||||
} else {
|
||||
let Some(f) = self.find(fam, style) else { continue };
|
||||
f
|
||||
};
|
||||
if self.is_loaded(fam) || self.has_family(fam) {
|
||||
let f = self.face(fam, style);
|
||||
if f.family.eq_ignore_ascii_case(fam) && covers_cluster(&f, cluster) {
|
||||
return f;
|
||||
}
|
||||
@@ -233,9 +227,6 @@ impl FontDb {
|
||||
}
|
||||
}
|
||||
|
||||
/// Most caret records read for one ligature glyph.
|
||||
const MAX_LIGATURE_CARETS: usize = 64;
|
||||
|
||||
/// Caret positions inside a ligature glyph from GDEF `LigCaretList`, in font units from the
|
||||
/// glyph's origin (sorted ascending). Empty when the font has none for `gid`.
|
||||
pub fn ligature_carets(face: &FontFace, gid: u32) -> Vec<i32> {
|
||||
@@ -245,12 +236,9 @@ pub fn ligature_carets(face: &FontFace, gid: u32) -> Vec<i32> {
|
||||
let Ok(cov) = list.coverage() else { return Vec::new() };
|
||||
let Some(idx) = cov.get(skrifa::GlyphId::new(gid)) else { return Vec::new() };
|
||||
let Ok(lig) = list.lig_glyphs().get(idx as usize) else { return Vec::new() };
|
||||
// The limit applies to the records read, not to the carets kept: a font can list thousands of
|
||||
// records it then has to be read through for every ligature laid out.
|
||||
let mut out: Vec<i32> = lig
|
||||
.caret_values()
|
||||
.iter()
|
||||
.take(MAX_LIGATURE_CARETS)
|
||||
.filter_map(|cv| cv.ok())
|
||||
.filter_map(|cv| match cv {
|
||||
skrifa::raw::tables::gdef::CaretValue::Format1(f) => Some(f.coordinate() as i32),
|
||||
@@ -258,6 +246,7 @@ pub fn ligature_carets(face: &FontFace, gid: u32) -> Vec<i32> {
|
||||
// Format 2 is a contour point index; outlines aren't consulted here.
|
||||
skrifa::raw::tables::gdef::CaretValue::Format2(_) => None,
|
||||
})
|
||||
.take(64)
|
||||
.collect();
|
||||
out.sort_unstable();
|
||||
out
|
||||
@@ -317,21 +306,4 @@ mod tests {
|
||||
assert!(c.windows(2).all(|w| w[0] <= w[1]));
|
||||
}
|
||||
}
|
||||
|
||||
/// With system fallback off, the automatic cascades (script families and symbol faces) load no
|
||||
/// installed family: the result comes from what is already loaded.
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
#[test]
|
||||
fn cascades_load_no_installed_family_when_system_fallback_is_off() {
|
||||
let db = FontDb::with_font_dirs(Vec::new());
|
||||
db.set_system_fallback(false);
|
||||
let base = db.face(crate::DEFAULT_FAMILY, "Regular");
|
||||
let loaded = |db: &FontDb| [b"Arab", b"Hebr", b"Thai", b"Deva"].into_iter().flat_map(|s| os_families(*s)).filter(|f| db.is_loaded(f)).count();
|
||||
let before = loaded(&db);
|
||||
for c in ['\u{0628}', '\u{05D0}', '\u{0E01}', '\u{0905}', '\u{2211}', '\u{2603}'] {
|
||||
let f = db.cascade(&[c], std::slice::from_ref(&base), "Regular");
|
||||
assert!(!f.family.is_empty());
|
||||
}
|
||||
assert_eq!(loaded(&db), before, "no installed family was loaded");
|
||||
}
|
||||
}
|
||||
|
||||
+25
-238
@@ -30,15 +30,12 @@ pub fn bundled() -> Vec<&'static [u8]> {
|
||||
/// Open substitutes for families documents often ask for but we can't ship (metric-compatible
|
||||
/// where one exists), tried in order after the family itself.
|
||||
pub fn substitutes(family: &str) -> &'static [&'static str] {
|
||||
let f = family.to_ascii_lowercase();
|
||||
if is_monospace_family(&f) {
|
||||
return &["Liberation Mono", "Courier New", "Menlo", "Courier", "DejaVu Sans Mono"];
|
||||
}
|
||||
match f.as_str() {
|
||||
match family.to_ascii_lowercase().as_str() {
|
||||
"calibri" | "calibri light" => &["Carlito", "Liberation Sans", "Arial", "Inter"],
|
||||
"cambria" | "cambria math" => &["Caladea", "Liberation Serif", "Times New Roman", "Merriweather"],
|
||||
"arial" | "helvetica" | "helvetica neue" | "arial nova" => &["Liberation Sans", "Arial", "Helvetica Neue", "Helvetica", "Inter"],
|
||||
"times new roman" | "times" => &["Liberation Serif", "Times New Roman", "Times", "Merriweather"],
|
||||
"courier new" | "courier" | "consolas" | "menlo" => &["Liberation Mono", "Courier New", "Menlo", "Courier"],
|
||||
"aptos" | "aptos display" | "aptos narrow" | "segoe ui" | "segoe ui light" | "segoe ui semibold" | "tahoma" | "verdana" => {
|
||||
&["Inter", "Helvetica Neue", "Arial", "Liberation Sans"]
|
||||
}
|
||||
@@ -55,77 +52,12 @@ pub fn substitutes(family: &str) -> &'static [&'static str] {
|
||||
}
|
||||
}
|
||||
|
||||
/// Families that are monospaced: when not installed, documents keep a monospaced substitute
|
||||
/// ([`substitutes`]) so code blocks and columns stay aligned instead of falling back to
|
||||
/// proportional Calibri.
|
||||
pub fn is_monospace_family(family: &str) -> bool {
|
||||
let f = family.to_ascii_lowercase();
|
||||
let s = f.trim();
|
||||
if matches!(
|
||||
s,
|
||||
"courier new"
|
||||
| "courier"
|
||||
| "cousine"
|
||||
| "consolas"
|
||||
| "menlo"
|
||||
| "monaco"
|
||||
| "source code pro"
|
||||
| "jetbrains mono"
|
||||
| "cascadia code"
|
||||
| "cascadia code pl"
|
||||
| "cascadia mono"
|
||||
| "cascadia mono pl"
|
||||
| "fira code"
|
||||
| "fira mono"
|
||||
| "inconsolata"
|
||||
| "dejavu sans mono"
|
||||
| "sf mono"
|
||||
| "roboto mono"
|
||||
| "hack"
|
||||
| "ubuntu mono"
|
||||
| "pt mono"
|
||||
| "liberation mono"
|
||||
| "andale mono"
|
||||
| "lucida console"
|
||||
| "lucida sans typewriter"
|
||||
| "fixedsys"
|
||||
| "terminal"
|
||||
| "monospace"
|
||||
| "iosevka"
|
||||
| "monolisa"
|
||||
| "envy code r"
|
||||
| "hasklig"
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
s.starts_with("courier")
|
||||
|| s.starts_with("consolas")
|
||||
|| s.starts_with("inconsolata")
|
||||
|| s.starts_with("iosevka")
|
||||
|| s.ends_with(" mono")
|
||||
|| s.ends_with(" code")
|
||||
|| s.ends_with(" console")
|
||||
|| s.ends_with(" typewriter")
|
||||
|| s.ends_with(" monospace")
|
||||
|| s.split_whitespace().any(|w| w == "mono" || w == "terminal" || w == "monospace")
|
||||
}
|
||||
|
||||
/// Families whose open substitutes ([`substitutes`]) keep their metrics, so text drawn with
|
||||
/// them breaks into the same lines.
|
||||
fn has_metric_twin(family: &str) -> bool {
|
||||
matches!(
|
||||
family.to_ascii_lowercase().as_str(),
|
||||
"calibri"
|
||||
| "calibri light"
|
||||
| "cambria"
|
||||
| "cambria math"
|
||||
| "arial"
|
||||
| "helvetica"
|
||||
| "times new roman"
|
||||
| "times"
|
||||
| "courier new"
|
||||
| "courier"
|
||||
| "cousine"
|
||||
"calibri" | "calibri light" | "cambria" | "cambria math" | "arial" | "helvetica" | "times new roman" | "times" | "courier new" | "courier"
|
||||
)
|
||||
}
|
||||
|
||||
@@ -368,7 +300,7 @@ struct CatalogEntry {
|
||||
/// Process-wide font database.
|
||||
pub struct FontDb {
|
||||
faces: RwLock<Vec<Arc<FontFace>>>,
|
||||
outlines: Mutex<OutlineCache>,
|
||||
outlines: Mutex<HashMap<(u32, u32), Arc<BezPath>>>,
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
catalog: RwLock<Vec<CatalogEntry>>,
|
||||
/// The folders the system font scan reads (the platform's font folders for [`FontDb::global`]).
|
||||
@@ -439,38 +371,6 @@ const SYSTEM_FALLBACKS: &[&str] = &[
|
||||
|
||||
static NEXT_ID: AtomicU32 = AtomicU32::new(1);
|
||||
const OUTLINE_CACHE_MAX: usize = 50_000;
|
||||
/// Most path elements one glyph outline may expand to. Composite and `VARC` glyphs can draw a
|
||||
/// shared outline many times over; a glyph past this draws nothing rather than holding megabytes.
|
||||
const MAX_OUTLINE_ELEMENTS: usize = 1 << 15;
|
||||
/// Most path elements the outline cache holds across all glyphs (about 230 MB at most) before it
|
||||
/// starts over: distinct glyph ids can each expand one dense outline.
|
||||
const OUTLINE_CACHE_MAX_ELEMENTS: usize = 1 << 22;
|
||||
|
||||
/// Glyph outlines by (face, glyph), with the path elements they hold.
|
||||
#[derive(Default)]
|
||||
struct OutlineCache {
|
||||
paths: HashMap<(u32, u32), Arc<BezPath>>,
|
||||
elements: usize,
|
||||
}
|
||||
|
||||
impl OutlineCache {
|
||||
fn get(&self, key: &(u32, u32)) -> Option<Arc<BezPath>> {
|
||||
self.paths.get(key).cloned()
|
||||
}
|
||||
|
||||
fn insert(&mut self, key: (u32, u32), path: Arc<BezPath>) {
|
||||
let n = path.elements().len();
|
||||
if let Some(old) = self.paths.remove(&key) {
|
||||
self.elements = self.elements.saturating_sub(old.elements().len());
|
||||
}
|
||||
if self.paths.len() >= OUTLINE_CACHE_MAX || self.elements.saturating_add(n) > OUTLINE_CACHE_MAX_ELEMENTS {
|
||||
self.paths.clear();
|
||||
self.elements = 0;
|
||||
}
|
||||
self.paths.insert(key, path);
|
||||
self.elements = self.elements.saturating_add(n);
|
||||
}
|
||||
}
|
||||
|
||||
fn name(font: &skrifa::FontRef<'_>, ids: &[StringId]) -> Option<String> {
|
||||
ids.iter().find_map(|id| font.localized_strings(*id).english_or_first().map(|s| s.to_string()).filter(|s| !s.is_empty()))
|
||||
@@ -726,10 +626,6 @@ const MAX_COLLECTION_FACES: u32 = 256;
|
||||
const MAX_COLLECTION_ENTRIES_SCANNED: u32 = 16_384;
|
||||
/// Maximum named instances read from a single face.
|
||||
const MAX_NAMED_INSTANCES_PER_FACE: usize = 1024;
|
||||
/// Maximum faces (named instances counted separately) enumerated from one font file, across all
|
||||
/// of a collection's faces: the per-collection and per-face limits multiply, and each face found
|
||||
/// is then built with its own shaper.
|
||||
const MAX_ENUMERATED_FACES: usize = 4096;
|
||||
|
||||
fn distinct_face_indices(entries: impl IntoIterator<Item = (u32, u32)>) -> Vec<u32> {
|
||||
let mut offsets = std::collections::HashSet::new();
|
||||
@@ -766,30 +662,21 @@ fn enumerate_faces(data: &[u8]) -> Vec<Found> {
|
||||
Err(_) => Vec::new(),
|
||||
};
|
||||
let mut out = Vec::new();
|
||||
// Named instances read and plain faces kept, across the whole file.
|
||||
let mut budget = MAX_ENUMERATED_FACES;
|
||||
for i in indices {
|
||||
if budget == 0 {
|
||||
break;
|
||||
}
|
||||
let Ok(f) = skrifa::FontRef::from_index(data, i) else { continue };
|
||||
let Some((family, style)) = face_names(&f) else { continue };
|
||||
let axes = f.axes();
|
||||
let mut named = Vec::new();
|
||||
let mut seen = std::collections::HashSet::new();
|
||||
let instances = f.named_instances();
|
||||
for ni in instances.iter().take(capped_named_instance_count(instances.len())) {
|
||||
let Some(left) = budget.checked_sub(1) else { break };
|
||||
budget = left;
|
||||
let Some(style) = name(&f, &[ni.subfamily_name_id()]) else { continue };
|
||||
if !seen.insert(style.to_ascii_lowercase()) {
|
||||
if named.iter().any(|(_, s, _): &(u32, String, _)| s.eq_ignore_ascii_case(&style)) {
|
||||
continue;
|
||||
}
|
||||
let coords: Vec<([u8; 4], f32)> = axes.iter().zip(ni.user_coords()).map(|(a, v)| (a.tag().to_be_bytes(), v)).collect();
|
||||
named.push((i, style, coords));
|
||||
}
|
||||
if named.is_empty() {
|
||||
budget = budget.saturating_sub(1);
|
||||
out.push((i, family, style, Vec::new()));
|
||||
} else {
|
||||
out.extend(named.into_iter().map(|(i, s, c)| (i, family.clone(), s, c)));
|
||||
@@ -950,7 +837,7 @@ impl FontDb {
|
||||
}
|
||||
Self {
|
||||
faces: RwLock::new(faces),
|
||||
outlines: Mutex::new(OutlineCache::default()),
|
||||
outlines: Mutex::new(HashMap::new()),
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
catalog: RwLock::new(Vec::new()),
|
||||
font_dirs,
|
||||
@@ -976,17 +863,6 @@ impl FontDb {
|
||||
let _ = on;
|
||||
}
|
||||
|
||||
/// Whether automatic system-font lookups (fallback by character, symbol and script families)
|
||||
/// may touch installed fonts. Lookups of a family the document or host names are not gated.
|
||||
pub(crate) fn system_fallback_enabled(&self) -> bool {
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
{
|
||||
self.sys.lock().unwrap_or_else(std::sync::PoisonError::into_inner).enabled
|
||||
}
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
false
|
||||
}
|
||||
|
||||
/// Process-wide database: the bundled fonts, then the installed system fonts, cataloged the
|
||||
/// first time a lookup by family name needs them (or ahead of time by
|
||||
/// [`FontDb::scan_in_background`]).
|
||||
@@ -1204,7 +1080,6 @@ impl FontDb {
|
||||
/// when it is installed or bundled, or a metric-compatible stand-in (Carlito for Calibri,
|
||||
/// Liberation Sans for Arial…), or the family PowerPoint maps it to (Liberation Sans as Arial,
|
||||
/// see `office_twin`); any other missing family is drawn in Calibri (or Carlito).
|
||||
/// Monospaced families keep a monospaced substitute instead of falling back to Calibri.
|
||||
/// DeckCraft's own default family keeps [`FontDb::face`]'s fallbacks.
|
||||
pub fn document_face(&self, family: &str, style: &str) -> Arc<FontFace> {
|
||||
if family.eq_ignore_ascii_case(FALLBACK_FAMILY) || has_metric_twin(family) || is_office_cloud_font(family) || self.has_family(family) {
|
||||
@@ -1213,9 +1088,6 @@ impl FontDb {
|
||||
if let Some(twin) = office_twin(family) {
|
||||
return self.face(twin, style);
|
||||
}
|
||||
if is_monospace_family(family) {
|
||||
return self.face(family, style);
|
||||
}
|
||||
self.face(MISSING_FONT_FAMILY, style)
|
||||
}
|
||||
|
||||
@@ -1271,8 +1143,9 @@ impl FontDb {
|
||||
fn instance(&self, family: &str, style: &str) -> Option<Arc<FontFace>> {
|
||||
let open = style.find('{')?;
|
||||
let base = self.find(family, style[..open].trim())?;
|
||||
let Some(font) = base.skrifa().filter(|f| !f.axes().is_empty()) else { return Some(base) };
|
||||
let axis_tags: Vec<[u8; 4]> = font.axes().iter().map(|a| a.tag().to_be_bytes()).collect();
|
||||
if base.skrifa().is_none_or(|f| f.axes().is_empty()) {
|
||||
return Some(base);
|
||||
}
|
||||
let mut coords = base.coords.clone();
|
||||
for kv in style[open + 1..].trim_end_matches('}').split(',') {
|
||||
let (k, v) = kv.split_once(':')?;
|
||||
@@ -1282,11 +1155,6 @@ impl FontDb {
|
||||
return None;
|
||||
}
|
||||
let tag = [k[0], k[1], k[2], k[3]];
|
||||
// A tag the font has no axis for changes nothing; keeping it would grow the list with
|
||||
// every distinct tag in the style text.
|
||||
if !axis_tags.contains(&tag) {
|
||||
continue;
|
||||
}
|
||||
match coords.iter_mut().find(|(t, _)| *t == tag) {
|
||||
Some(c) => c.1 = v,
|
||||
None => coords.push((tag, v)),
|
||||
@@ -1298,7 +1166,7 @@ impl FontDb {
|
||||
Some(f)
|
||||
}
|
||||
|
||||
pub(crate) fn find(&self, family: &str, style: &str) -> Option<Arc<FontFace>> {
|
||||
fn find(&self, family: &str, style: &str) -> Option<Arc<FontFace>> {
|
||||
if style.contains('{') {
|
||||
{
|
||||
let faces = self.read_faces();
|
||||
@@ -1484,8 +1352,7 @@ impl FontDb {
|
||||
};
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
{
|
||||
let cataloged: Vec<String> =
|
||||
if self.system_fallback_enabled() { self.read_catalog().iter().map(|e| e.family.clone()).collect() } else { Vec::new() };
|
||||
let cataloged: Vec<String> = self.read_catalog().iter().map(|e| e.family.clone()).collect();
|
||||
for fam in SYSTEM_FALLBACKS.iter().filter(|f| f.contains("Symbol")) {
|
||||
if !self.is_loaded(fam) && cataloged.iter().any(|f| f.eq_ignore_ascii_case(fam)) {
|
||||
self.load_cataloged(fam);
|
||||
@@ -1574,72 +1441,41 @@ impl FontDb {
|
||||
pub fn outline(&self, face: &FontFace, gid: u32) -> Arc<BezPath> {
|
||||
let key = (face.id, gid);
|
||||
if let Some(p) = self.outlines.lock().unwrap_or_else(std::sync::PoisonError::into_inner).get(&key) {
|
||||
return p;
|
||||
return p.clone();
|
||||
}
|
||||
let mut pen = FlipPen::new(MAX_OUTLINE_ELEMENTS);
|
||||
let mut pen = FlipPen(BezPath::new());
|
||||
if let Some(f) = face.skrifa()
|
||||
&& let Some(g) = f.outline_glyphs().get(GlyphId::new(gid))
|
||||
{
|
||||
let _ = g.draw(DrawSettings::unhinted(Size::unscaled(), face.location()), &mut pen);
|
||||
}
|
||||
let p = Arc::new(pen.finish());
|
||||
self.outlines.lock().unwrap_or_else(std::sync::PoisonError::into_inner).insert(key, p.clone());
|
||||
let p = Arc::new(pen.0);
|
||||
let mut cache = self.outlines.lock().unwrap_or_else(std::sync::PoisonError::into_inner);
|
||||
if cache.len() >= OUTLINE_CACHE_MAX {
|
||||
cache.clear();
|
||||
}
|
||||
cache.insert(key, p.clone());
|
||||
p
|
||||
}
|
||||
}
|
||||
|
||||
/// Collects a glyph outline, y flipped, up to `limit` path elements. Past the limit it ignores
|
||||
/// what follows and [`FlipPen::finish`] gives an empty path: a cut-off outline would fill wrongly.
|
||||
struct FlipPen {
|
||||
path: BezPath,
|
||||
limit: usize,
|
||||
over: bool,
|
||||
}
|
||||
|
||||
impl FlipPen {
|
||||
fn new(limit: usize) -> Self {
|
||||
FlipPen { path: BezPath::new(), limit, over: false }
|
||||
}
|
||||
|
||||
/// Room for one more element?
|
||||
fn room(&mut self) -> bool {
|
||||
if !self.over && self.path.elements().len() >= self.limit {
|
||||
self.over = true;
|
||||
self.path = BezPath::new();
|
||||
}
|
||||
!self.over
|
||||
}
|
||||
|
||||
fn finish(self) -> BezPath {
|
||||
if self.over { BezPath::new() } else { self.path }
|
||||
}
|
||||
}
|
||||
struct FlipPen(BezPath);
|
||||
|
||||
impl OutlinePen for FlipPen {
|
||||
fn move_to(&mut self, x: f32, y: f32) {
|
||||
if self.room() {
|
||||
self.path.move_to((x as f64, -y as f64));
|
||||
}
|
||||
self.0.move_to((x as f64, -y as f64));
|
||||
}
|
||||
fn line_to(&mut self, x: f32, y: f32) {
|
||||
if self.room() {
|
||||
self.path.line_to((x as f64, -y as f64));
|
||||
}
|
||||
self.0.line_to((x as f64, -y as f64));
|
||||
}
|
||||
fn quad_to(&mut self, cx0: f32, cy0: f32, x: f32, y: f32) {
|
||||
if self.room() {
|
||||
self.path.quad_to((cx0 as f64, -cy0 as f64), (x as f64, -y as f64));
|
||||
}
|
||||
self.0.quad_to((cx0 as f64, -cy0 as f64), (x as f64, -y as f64));
|
||||
}
|
||||
fn curve_to(&mut self, cx0: f32, cy0: f32, cx1: f32, cy1: f32, x: f32, y: f32) {
|
||||
if self.room() {
|
||||
self.path.curve_to((cx0 as f64, -cy0 as f64), (cx1 as f64, -cy1 as f64), (x as f64, -y as f64));
|
||||
}
|
||||
self.0.curve_to((cx0 as f64, -cy0 as f64), (cx1 as f64, -cy1 as f64), (x as f64, -y as f64));
|
||||
}
|
||||
fn close(&mut self) {
|
||||
if self.room() {
|
||||
self.path.close_path();
|
||||
}
|
||||
self.0.close_path();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1667,55 +1503,6 @@ mod font_dir_tests {
|
||||
assert_eq!(capped_named_instance_count(MAX_NAMED_INSTANCES_PER_FACE + 1), MAX_NAMED_INSTANCES_PER_FACE);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn faces_of_one_file_are_bounded_across_the_collection() {
|
||||
// The per-collection and per-face limits together would allow far more.
|
||||
assert!((MAX_COLLECTION_FACES as usize) * MAX_NAMED_INSTANCES_PER_FACE > 16 * MAX_ENUMERATED_FACES);
|
||||
for data in bundled() {
|
||||
assert!(enumerate_faces(data).len() <= MAX_ENUMERATED_FACES);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn an_outline_past_the_element_limit_draws_nothing() {
|
||||
let mut pen = FlipPen::new(8);
|
||||
pen.move_to(0.0, 0.0);
|
||||
pen.line_to(1.0, 1.0);
|
||||
pen.close();
|
||||
assert_eq!(pen.finish().elements().len(), 3);
|
||||
// A shared outline drawn over and over (composite and VARC glyphs).
|
||||
let mut pen = FlipPen::new(8);
|
||||
for i in 0..1_000_000 {
|
||||
pen.move_to(0.0, 0.0);
|
||||
pen.line_to(i as f32, 1.0);
|
||||
pen.close();
|
||||
}
|
||||
assert_eq!(pen.path.elements().len(), 0);
|
||||
assert!(pen.finish().elements().is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn outline_cache_is_bounded_by_elements() {
|
||||
let mut path = BezPath::new();
|
||||
path.move_to((0.0, 0.0));
|
||||
for i in 1..MAX_OUTLINE_ELEMENTS {
|
||||
path.line_to((i as f64, 0.0));
|
||||
}
|
||||
let path = Arc::new(path);
|
||||
let mut cache = OutlineCache::default();
|
||||
let glyphs = 4 * OUTLINE_CACHE_MAX_ELEMENTS / MAX_OUTLINE_ELEMENTS;
|
||||
for gid in 0..glyphs as u32 {
|
||||
cache.insert((1, gid), path.clone());
|
||||
assert!(cache.elements <= OUTLINE_CACHE_MAX_ELEMENTS);
|
||||
assert_eq!(cache.elements, cache.paths.values().map(|p| p.elements().len()).sum::<usize>());
|
||||
}
|
||||
assert!(cache.get(&(1, glyphs as u32 - 1)).is_some());
|
||||
// Replacing an entry does not count it twice.
|
||||
let before = cache.elements;
|
||||
cache.insert((1, glyphs as u32 - 1), path.clone());
|
||||
assert_eq!(cache.elements, before);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn xdg_data_dirs_contribute_their_fonts_folders() {
|
||||
let home = Path::new("/home/ann");
|
||||
|
||||
@@ -15,8 +15,7 @@ mod shape;
|
||||
|
||||
pub use fallback::{bundled_families, covers_cluster, is_emoji_cluster, ligature_carets};
|
||||
pub use fontdb::{
|
||||
FALLBACK_FAMILY, FaceRef, FontDb, FontFace, LAST_RESORT_FAMILY, MISSING_FONT_FAMILY, base_style, bundled, is_monospace_family, substitutes,
|
||||
system_font_dirs,
|
||||
FALLBACK_FAMILY, FaceRef, FontDb, FontFace, LAST_RESORT_FAMILY, MISSING_FONT_FAMILY, base_style, bundled, substitutes, system_font_dirs,
|
||||
};
|
||||
pub use harfrust::Feature;
|
||||
use harfrust::Tag;
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user