Compare commits

..
Author SHA1 Message Date
Dylan M. Taylor 31b89415b1 clippy: pass closures without extra borrows to first_amvp_neighbour 2026-10-25 22:48:22 -04:00
Dylan M. Taylor 489febbd79 refactor: split complex functions into helpers (S3776)
Lower the cognitive complexity of segment_at, apply_mmco, derive_amvp,
plotted_extent and look_with_def by moving their per-segment, per-MMCO,
per-neighbour, per-stack and per-preset-style steps into helpers. The
expressions and their evaluation order are unchanged.
2026-10-25 21:29:52 -04:00
Dylan M. Taylor aaf6c1c38a refactor(show): split the key handler into grid, navigation and tool keys (S3776) (#1402) 2026-10-25 21:13:50 -04:00
Dylan M. Taylor 16fe4ebc65 refactor(hevc): split derive_merge into spatial, temporal, combined and zero stages (S3776) (#1401) 2026-10-25 20:55:39 -04:00
Dylan M. Taylor 430d0a91c0 refactor(hevc): split prediction_unit into merge, AMVP and store helpers (S3776) (#1400) 2026-10-25 20:55:35 -04:00
Dylan M. Taylor c93e0a0bac refactor(rtf): split the outline reader into per-construct methods (S3776) (#1399) 2026-10-25 20:55:30 -04:00
Dylan M. Taylor aa3a1baa01 refactor(av1): reduce complexity in cdef and loop restoration (S3776) (#1278) 2026-10-25 20:26:38 -04:00
Dylan M. Taylor edfee40392 refactor(av1): split inverse transform into stages and passes (S3776) (#1277) 2026-10-25 20:26:33 -04:00
Dylan M. Taylor 580b577cf2 fix(h264): use Option::as_ref instead of closure (S1612) (#1268) 2026-10-25 20:26:28 -04:00
Sofia Alves FigueredoandClaude Sonnet 5.5 230b6485d9 fix(windows): keep the shortcut icon after an update (#1114) (#1391)
* fix(windows): keep the shortcut icon after an update (#1114)

The MSI's Start menu shortcut took its icon from the WiX <Icon>, which Windows
Installer caches under %APPDATA%\Microsoft\Installer\{ProductCode}\. Every
update is a major upgrade with a new ProductCode, so the old folder is deleted.
A taskbar pin is a copy of the shortcut that the update never touches, so it
kept pointing at the deleted file and lost its icon.

Drop the Icon from the shortcut so it shows the icon embedded in deckcraft.exe,
whose path survives updates. The <Icon> stays for ARPPRODUCTICON. A test keeps
shortcuts from naming an MSI icon again.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>

* contributors: credit Sofia Alves Figueredo by real name

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>

* fix(windows): make the Start menu shortcut non-advertised (#1114)

Addresses the review comment: without an MSI Icon, a non-advertised shortcut
targets the installed exe directly and takes its embedded icon.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>

* fix(windows): package.ps1 rejects a deckcraft.exe with no embedded icon (#1114)

The shortcuts now take the exe's own icon, and -SkipBuild packages an existing
exe that may come from a build without DECKCRAFT_REQUIRE_WINRES=1. Count its
icons with ExtractIconEx and stop before WiX when there are none.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-25 20:23:43 -04:00
16 changed files with 1038 additions and 703 deletions
+2
View File
@@ -137,6 +137,7 @@ app's JSON control channel (`deckcraft --control PORT`).
| Date | What landed |
|---|---|
| 2026-10-25 | Windows installer: the Start menu shortcut takes the icon embedded in `deckcraft.exe` instead of an MSI `<Icon>`, so a taskbar pin keeps its icon after an update (#1114); a pin that already lost it has to be unpinned and pinned again once |
| 2026-10-24 | AutoFormat as you type: a paragraph that starts with `*`, `-`, `--`, `>`, `<>`, `1.`, `1)`, `I.`, `i.`, `A.`, `a.` or `a)` and a space or tab becomes a bulleted or numbered list in that style; Backspace right after puts the typed text back, and Tools ▸ AutoCorrect and `text.autocorrect {lists}` turn it off |
| 2026-10-24 | Tables: while the caret is in a cell, dragging into another cell selects that range of cells (shaded, merged cells taken in whole), and Shift+arrow grows or shrinks it; formatting applies to the selected cells only, and Delete clears their text and keeps the table |
| 2026-10-24 | Preferences ▸ Measurement units: inches or centimetres. The Format pane's Size and Position fields, Shape Format ▸ Size on the ribbon, the Slide Size dialog and the ruler show and take the chosen unit; the deck still stores points (#1326) |
@@ -293,6 +294,7 @@ app's JSON control channel (`deckcraft --control PORT`).
| Date | Change | Summary |
|---|---|---|
| 2026-10-25 | trivial | Progress log: Windows shortcut icon survives updates (#1114). Scores and milestone names unchanged |
| 2026-10-24 | trivial | Progress log: selecting a range of table cells by dragging. Scores and milestone names unchanged |
| 2026-10-24 | trivial | Progress log: measurement units in centimetres (#1326). Scores unchanged |
| 2026-10-22 | trivial | Progress log: Animation Pane ▸ Play From. Scores unchanged |
+23
View File
@@ -430,4 +430,27 @@ mod tests {
assert_eq!(mac, win, "macOS and Windows offer different file types");
assert_eq!(mac, linux, "macOS and Linux offer different file types");
}
/// #1114: a shortcut that names an MSI `<Icon>` shows a blank icon once an update deletes the
/// cached copy of it, and a taskbar pin (a copy of the shortcut) is never repaired. Shortcuts
/// take the icon embedded in `deckcraft.exe`; only `ARPPRODUCTICON` uses the MSI `<Icon>`.
#[test]
fn windows_shortcuts_use_the_icon_embedded_in_the_exe() {
let wxs = include_str!("../../../packaging/windows/deckcraft.wxs");
let shortcuts: Vec<&str> = wxs.split("<Shortcut").skip(1).filter_map(|s| s.split("/>").next()).collect();
assert!(!shortcuts.is_empty(), "deckcraft.wxs has no shortcut");
for shortcut in shortcuts {
assert!(!shortcut.contains("Icon="), "a shortcut names an MSI <Icon>:{shortcut}");
}
assert!(wxs.contains("<Property Id=\"ARPPRODUCTICON\""), "Apps and features keeps its icon");
}
/// The shortcuts take the exe's own icon, and `package.ps1 -SkipBuild` packages an existing
/// exe without `DECKCRAFT_REQUIRE_WINRES`, so the script has to refuse one with no icon.
#[test]
fn the_windows_package_script_rejects_an_exe_without_an_icon() {
let script = include_str!("../../../packaging/windows/package.ps1");
assert!(script.contains("ExtractIconEx"), "package.ps1 does not count the exe's icons");
assert!(script.contains("has no embedded icon"), "package.ps1 does not reject an iconless exe");
}
}
+3
View File
@@ -65,3 +65,6 @@ sync_github_name = true
[people.Katacc]
sync_github_name = true
[people.SofiaAFigueredo]
real_name = "Sofia Alves Figueredo"
+115 -74
View File
@@ -45,7 +45,7 @@ fn cdef_block(fs: &FrameState, out: &mut FrameBuf, r: usize, c: usize, idx: usiz
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);
filter(fs, out, 0, (r, c), (pri, sec), damping, dir);
if fs.num_planes == 1 {
return true;
}
@@ -53,8 +53,8 @@ fn cdef_block(fs: &FrameState, out: &mut FrameBuf, r: usize, c: usize, idx: usiz
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);
filter(fs, out, 1, (r, c), (pri, sec), damping, dir);
filter(fs, out, 2, (r, c), (pri, sec), damping, dir);
true
}
@@ -124,33 +124,74 @@ fn constrain(diff: i32, threshold: i32, adj: u32) -> i32 {
if diff < 0 { -mag } else { mag }
}
#[allow(clippy::too_many_arguments)]
fn filter(fs: &FrameState, out: &mut FrameBuf, plane: usize, r: usize, c: usize, pri: i32, sec: i32, damping: i32, dir: usize) {
if pri == 0 && sec == 0 {
// Every tap contributes 0 and the clamp to [min, max] keeps x: CdefFrame = CurrFrame.
return;
/// 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 }
}
let coeff_shift = fs.bit_depth - 8;
/// 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 x0 = (c * 4) >> sub_x;
let y0 = (r * 4) >> sub_y;
let w = 8 >> sub_x;
let h = 8 >> sub_y;
let src = &fs.cur.planes[plane];
let (mi_rows, mi_cols) = (fs.fh.mi_rows as isize, fs.fh.mi_cols as isize);
// CdefAvailable per sample: its 4x4 block is inside the frame.
let mut buf = [CDEF_NA; PAD_STRIDE * PAD_STRIDE];
let mut all_avail = true;
for i in 0..h + 4 {
let y = (y0 + i) as isize - 2;
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..w + 4 {
let x = (x0 + j) as isize - 2;
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;
@@ -159,70 +200,70 @@ fn filter(fs: &FrameState, out: &mut FrameBuf, plane: usize, r: usize, c: usize,
}
}
}
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 };
let (pri_adj, sec_adj) = (adj(pri), adj(sec));
(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 mut taps = [(0isize, 0i32, 0i32, 0u32); 12];
let mut n = 0;
for k in 0..2 {
for sign in [-1isize, 1] {
taps[n] = (sign * pri_off[k], pri_taps[k], pri, pri_adj);
taps[n + 1] = (sign * sec_off[k][0], sec_taps[k], sec, sec_adj);
taps[n + 2] = (sign * sec_off[k][1], sec_taps[k], sec, sec_adj);
n += 3;
}
}
let rows = (y0, x0, h);
match w {
8 => filter_rows::<8>(&buf, &taps, dst, rows),
_ => filter_rows::<4>(&buf, &taps, dst, rows),
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..h {
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(y0 + i)[x0..x0 + w];
let drow = &mut dst.row_mut(y0 + i)[x0..x0 + w];
for j in 0..w {
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;
let x = srow[j] as i32;
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 * pri_off[k]) as usize];
if p != CDEF_NA {
if pri != 0 {
sum += pri_taps[k] * constrain(p - x, pri, pri_adj);
}
max = max.max(p);
min = min.min(p);
}
for s in 0..2 {
let q = buf[(ci + sign * sec_off[k][s]) as usize];
if q != CDEF_NA {
if sec != 0 {
sum += sec_taps[k] * constrain(q - x, sec, sec_adj);
}
max = max.max(q);
min = min.min(q);
}
}
}
}
drow[j] = (x + ((8 + sum - (sum < 0) as i32) >> 4)).clamp(min, max) as u16;
drow[j] = filter_sample(&buf, &taps, ci, srow[j] as i32);
}
}
}
+113 -78
View File
@@ -73,57 +73,70 @@ impl LrCtx<'_> {
}
}
/// 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 fh = &fs.fh;
let mut lr = cdef.clone();
let up_w = fh.upscaled_width as i32;
let fr_h = fh.frame_height as i32;
let bd = fs.bit_depth;
for plane in 0..fs.num_planes {
let rtype_frame = fh.lr.frame_restoration_type[plane];
if rtype_frame == RESTORE_NONE as u8 {
if fs.fh.lr.frame_restoration_type[plane] == RESTORE_NONE as u8 {
continue;
}
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;
match fs.lr.lr_type[plane][ui] {
t if t == RESTORE_WIENER as u8 => wiener(fs, &ctx, &mut lr.planes[plane], plane, ui, x, y, w, h, bd),
t if t == RESTORE_SGRPROJ as u8 => self_guided(fs, &ctx, &mut lr.planes[plane], plane, ui, x, y, w, h, bd),
_ => {}
}
}
col += 1;
}
row += 1;
}
restore_plane(fs, cur, cdef, &mut lr.planes[plane], plane);
}
lr
}
#[allow(clippy::too_many_arguments)]
fn wiener(fs: &FrameState, ctx: &LrCtx, out: &mut Plane, plane: usize, ui: usize, x: i32, y: i32, w: i32, h: i32, bd: u32) {
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];
@@ -166,13 +179,13 @@ fn wiener(fs: &FrameState, ctx: &LrCtx, out: &mut Plane, plane: usize, ui: usize
}
}
#[allow(clippy::too_many_arguments)]
fn self_guided(fs: &FrameState, ctx: &LrCtx, out: &mut Plane, plane: usize, ui: usize, x: i32, y: i32, w: i32, h: i32, bd: u32) {
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, x, y, w, h, set, 0, bd, &mut flt0);
box_filter(ctx, x, y, w, h, set, 1, bd, &mut flt1);
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;
@@ -198,8 +211,56 @@ fn round2_64(x: i64, n: u32) -> i64 {
if n == 0 { x } else { (x + (1i64 << (n - 1))) >> n }
}
#[allow(clippy::too_many_arguments)]
fn box_filter(ctx: &LrCtx, x: i32, y: i32, w: i32, h: i32, set: usize, pass: usize, bd: u32, f: &mut [[i32; 4]; 4]) {
/// 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;
@@ -207,35 +268,15 @@ fn box_filter(ctx: &LrCtx, x: i32, y: i32, w: i32, h: i32, set: usize, pass: usi
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 s = ((1i64 << SGRPROJ_MTABLE_BITS) + n2e / 2) / n2e;
let one_over_n = ((1i64 << SGRPROJ_RECIP_BITS) + n / 2) / n;
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 mut a = 0i64;
let mut b = 0i64;
for dy in -r..=r {
for dx in -r..=r {
let c = ctx.sample(x + j + dx, y + i + dy) as i64;
a += c * c;
b += c;
}
}
let a = round2_64(a, 2 * (bd - 8));
let d = round2_64(b, bd - 8);
let p = (a * n - d * d).max(0);
let z = round2_64(p * 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 * one_over_n;
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] = round2_64(b2, SGRPROJ_RECIP_BITS as u32);
b_arr[(i + 1) as usize][(j + 1) as usize] = b2;
}
}
for i in 0..h {
@@ -245,13 +286,7 @@ fn box_filter(ctx: &LrCtx, x: i32, y: i32, w: i32, h: i32, set: usize, pass: usi
let mut b = 0i64;
for dy in -1..=1i32 {
for dx in -1..=1i32 {
let weight = 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
};
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];
}
+47 -3
View File
@@ -165,6 +165,15 @@ pub(crate) fn inverse_dct<T: Lane>(t: &mut [T], n: u32, r: u32) {
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);
@@ -190,6 +199,9 @@ pub(crate) fn inverse_dct<T: Lane>(t: &mut [T], n: u32, r: u32) {
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 {
@@ -221,6 +233,9 @@ pub(crate) fn inverse_dct<T: Lane>(t: &mut [T], n: u32, r: u32) {
}
}
}
}
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);
}
@@ -248,6 +263,9 @@ pub(crate) fn inverse_dct<T: Lane>(t: &mut [T], n: u32, r: u32) {
}
}
}
}
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);
}
@@ -273,6 +291,9 @@ pub(crate) fn inverse_dct<T: Lane>(t: &mut [T], n: u32, r: u32) {
}
}
}
}
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);
@@ -300,6 +321,9 @@ pub(crate) fn inverse_dct<T: Lane>(t: &mut [T], n: u32, r: u32) {
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);
@@ -531,10 +555,9 @@ pub(crate) fn inverse_transform_2d(dequant: &[i32], residual: &mut [i32], tx_sz:
let col_clamp = (bit_depth + 6).max(16);
let rk = row_kind(tx_type);
let ck = col_kind(tx_type);
let mut t = [0i32; 64];
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 {
let p = Tx2d { w, h, log2w, log2h, row_shift, col_shift, row_clamp, col_clamp, rk, ck, rect2 };
if w >= 8 && h >= 8 {
transform_lanes::<8>(dequant, residual, &p);
} else {
@@ -542,6 +565,13 @@ pub(crate) fn inverse_transform_2d(dequant: &[i32], residual: &mut [i32], tx_sz:
}
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
@@ -572,6 +602,11 @@ pub(crate) fn inverse_transform_2d(dequant: &[i32], residual: &mut [i32], tx_sz:
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];
@@ -617,11 +652,16 @@ fn transform_1d<T: Lane>(t: &mut [T], kind: Kind1d, n: u32, r: u32) {
/// 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 t = [[0i32; L]; 64];
let mut i0 = 0;
while i0 < h {
// Rows without coefficients (and rows 32..63) transform to zeros.
@@ -649,6 +689,10 @@ fn transform_lanes<const L: usize>(dequant: &[i32], residual: &mut [i32], p: &Tx
}
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 {
+59 -47
View File
@@ -658,6 +658,62 @@ pub fn vertex_at(doc: &deckcraft_model::Presentation, sel: &crate::Selection, sh
None
}
/// The point at parameter `t` of segment `si`, scaled by `scale` into the shape's box.
fn segment_point(segs: &[Seg], si: usize, p0: Point, t: f64, scale: (f64, f64)) -> Option<Point> {
let q = match segs.get(si)? {
Seg::L(p1) => p0.lerp(*p1, t),
Seg::Q(c, p1) => p0.lerp(*c, t).lerp(c.lerp(*p1, t), t),
Seg::C(c1, c2, p1) => {
let (a, b, c) = (p0.lerp(*c1, t), c1.lerp(*c2, t), c2.lerp(*p1, t));
a.lerp(b, t).lerp(b.lerp(c, t), t)
}
_ => return None,
};
Some(Point::new(q.x * scale.0, q.y * scale.1))
}
/// The vertex that ends segment `si` (a closing edge ends on the start).
fn segment_end_vertex(segs: &[Seg], vs: &[usize], si: usize) -> Option<usize> {
if closes_onto_start(segs, si) {
let start = segs.get(..si).and_then(|s| s.iter().rposition(|x| matches!(x, Seg::M(_))));
start.and_then(|m| vs.iter().position(|v| *v == m))
} else {
vs.iter().position(|v| *v == si)
}
}
/// The distance and `t` of the point on segment `si` nearest `local`, if it lies within `tol`.
fn nearest_on_segment(segs: &[Seg], si: usize, p0: Point, scale: (f64, f64), local: Point, tol: f64) -> Option<(f64, f64)> {
let at = |t: f64| segment_point(segs, si, p0, t, scale);
let mut near: Option<(f64, f64)> = None;
for k in 1..32 {
let t = k as f64 / 32.0;
if let Some(q) = at(t) {
let d = (q - local).hypot();
if d <= tol && near.is_none_or(|n| d < n.0) {
near = Some((d, t));
}
}
}
let (mut d, mut t) = near?;
// Narrow `t` down to the nearest place on the segment, where Add Point puts the point.
let mut step = 1.0 / 32.0;
for _ in 0..4 {
step /= 8.0;
let c = t;
for k in -8..=8 {
let u = (c + f64::from(k) * step).clamp(0.0, 1.0);
if let Some(q) = at(u) {
let e = (q - local).hypot();
if e < d {
(d, t) = (e, u);
}
}
}
}
Some((d, t))
}
/// The segment of `sh`'s outline near slide point `p`: (path, index of the vertex ending it, t).
pub fn segment_at(doc: &deckcraft_model::Presentation, sel: &crate::Selection, sh: &Shape, p: Point, tol: f64) -> Option<(usize, usize, f64)> {
let Geom::Custom { paths } = &sh.geom else { return None };
@@ -665,57 +721,13 @@ pub fn segment_at(doc: &deckcraft_model::Presentation, sel: &crate::Selection, s
let local = x.affine().inverse() * p;
let mut best: Option<(f64, usize, usize, f64)> = None;
for (pi, cp) in paths.iter().enumerate() {
let (sx, sy) = (if cp.w > 0.0 { x.w / cp.w } else { 1.0 }, if cp.h > 0.0 { x.h / cp.h } else { 1.0 });
let scale = (if cp.w > 0.0 { x.w / cp.w } else { 1.0 }, if cp.h > 0.0 { x.h / cp.h } else { 1.0 });
let segs = parse(&cp.d);
let vs = vertices(&segs);
for si in 0..segs.len() {
let Some(p0) = seg_start(&segs, si) else { continue };
let at = |t: f64| -> Option<Point> {
let q = match segs.get(si)? {
Seg::L(p1) => p0.lerp(*p1, t),
Seg::Q(c, p1) => p0.lerp(*c, t).lerp(c.lerp(*p1, t), t),
Seg::C(c1, c2, p1) => {
let (a, b, c) = (p0.lerp(*c1, t), c1.lerp(*c2, t), c2.lerp(*p1, t));
a.lerp(b, t).lerp(b.lerp(c, t), t)
}
_ => return None,
};
Some(Point::new(q.x * sx, q.y * sy))
};
// The vertex that ends this segment (a closing edge ends on the start).
let end_vertex = if closes_onto_start(&segs, si) {
let start = segs.get(..si).and_then(|s| s.iter().rposition(|x| matches!(x, Seg::M(_))));
start.and_then(|m| vs.iter().position(|v| *v == m))
} else {
vs.iter().position(|v| *v == si)
};
let Some(ev) = end_vertex else { continue };
let mut near: Option<(f64, f64)> = None;
for k in 1..32 {
let t = k as f64 / 32.0;
if let Some(q) = at(t) {
let d = (q - local).hypot();
if d <= tol && near.is_none_or(|n| d < n.0) {
near = Some((d, t));
}
}
}
let Some((mut d, mut t)) = near else { continue };
// Narrow `t` down to the nearest place on the segment, where Add Point puts the point.
let mut step = 1.0 / 32.0;
for _ in 0..4 {
step /= 8.0;
let c = t;
for k in -8..=8 {
let u = (c + f64::from(k) * step).clamp(0.0, 1.0);
if let Some(q) = at(u) {
let e = (q - local).hypot();
if e < d {
(d, t) = (e, u);
}
}
}
}
let Some(ev) = segment_end_vertex(&segs, &vs, si) else { continue };
let Some((d, t)) = nearest_on_segment(&segs, si, p0, scale, local, tol) else { continue };
if best.is_none_or(|b| d < b.0) {
best = Some((d, pi, ev, t));
}
+201 -140
View File
@@ -126,155 +126,216 @@ fn advance(rtf: &str, i: usize, n: usize) -> usize {
if rtf.is_char_boundary(i + n) { i + n } else { i + rtf.get(i..).and_then(|r| r.chars().next()).map_or(1, char::len_utf8) }
}
/// Group state: whether the text is being skipped, and the `\uc` fallback count.
#[derive(Clone, Copy)]
struct St {
skip: bool,
uc: usize,
}
/// Parser state for [`rtf_to_outline`]: the cursor, the group stack and the outline so far.
struct Outline<'a> {
rtf: &'a str,
b: &'a [u8],
i: usize,
stack: Vec<St>,
st: St,
level: Option<u32>,
indent: i64,
pending_skip: usize,
line: String,
out: String,
}
/// Read an RTF outline into tab-indented outline text: a paragraph's outline level (or its left
/// indent in 0.25" steps) becomes its number of tabs.
pub fn rtf_to_outline(rtf: &str) -> String {
#[derive(Clone, Copy)]
struct St {
skip: bool,
uc: usize,
}
let b = rtf.as_bytes();
let mut stack: Vec<St> = vec![];
let mut st = St { skip: false, uc: 1 };
let (mut level, mut indent): (Option<u32>, i64) = (None, 0);
let mut line = String::new();
let mut out = String::new();
let mut pending_skip = 0usize;
let mut i = 0;
let flush = |line: &mut String, level: Option<u32>, indent: i64, out: &mut String| {
let text = line.trim();
if !text.is_empty() {
let n = level.map(|l| l as i64).unwrap_or(indent.saturating_add(180) / 360).clamp(0, 9) as usize;
out.push_str(&"\t".repeat(n));
out.push_str(text);
out.push('\n');
}
line.clear();
let mut p = Outline {
rtf,
b: rtf.as_bytes(),
i: 0,
stack: vec![],
st: St { skip: false, uc: 1 },
level: None,
indent: 0,
pending_skip: 0,
line: String::new(),
out: String::new(),
};
while i < b.len() {
let c = b[i];
match c {
b'{' => {
stack.push(st);
i += 1;
// `{\*\dest …}`: an ignorable destination.
if b[i..].starts_with(b"\\*") {
st.skip = true;
}
}
b'}' => {
st = stack.pop().unwrap_or(St { skip: false, uc: 1 });
i += 1;
}
b'\\' => {
i += 1;
let Some(&n) = b.get(i) else { break };
if n.is_ascii_alphabetic() {
let s = i;
while i < b.len() && b[i].is_ascii_alphabetic() {
i += 1;
}
let word = &rtf[s..i];
let ns = i;
if i < b.len() && (b[i] == b'-' || b[i].is_ascii_digit()) {
i += 1;
while i < b.len() && b[i].is_ascii_digit() {
i += 1;
}
}
let arg: Option<i64> = rtf[ns..i].parse().ok();
if i < b.len() && b[i] == b' ' {
i += 1;
}
if pending_skip > 0 {
pending_skip -= 1;
continue;
}
if SKIP.contains(&word) {
st.skip = true;
}
if st.skip {
continue;
}
match word {
"par" => {
flush(&mut line, level, indent, &mut out);
}
"pard" => {
level = None;
indent = 0;
}
"outlinelevel" => level = arg.map(|a| a.clamp(0, 9) as u32),
"li" => indent = arg.unwrap_or(0),
"tab" | "line" => line.push(' '),
"uc" => st.uc = arg.unwrap_or(1).clamp(0, 8) as usize,
"u" => {
let v = arg.unwrap_or(0);
// RTF writes UTF-16 units as signed 16-bit values.
push_utf16(&mut line, (v & 0xffff) as u32);
pending_skip = 0;
// Skip the fallback characters.
let mut k = st.uc;
while k > 0 && i < b.len() {
if b[i] == b'\\' && b.get(i + 1) == Some(&b'\'') {
i = advance(rtf, i, 4);
} else if b[i] == b'{' || b[i] == b'}' || b[i] == b'\\' {
break;
} else {
i = advance(rtf, i, 1);
}
k -= 1;
}
}
"emdash" => line.push('—'),
"endash" => line.push('–'),
"bullet" => line.push('•'),
"lquote" => line.push('‘'),
"rquote" => line.push('’'),
"ldblquote" => line.push('“'),
"rdblquote" => line.push('”'),
_ => {}
}
} else if n == b'\'' {
let hex = rtf.get(i + 1..i + 3).filter(|h| h.bytes().all(|b| b.is_ascii_hexdigit())).and_then(|h| u8::from_str_radix(h, 16).ok());
i = advance(rtf, i, 3);
if !st.skip
&& let Some(h) = hex
&& h != 0
{
line.push(cp1252(h));
}
} else {
i += 1;
if st.skip {
continue;
}
match n {
b'\\' | b'{' | b'}' => line.push(n as char),
b'~' => line.push('\u{a0}'),
b'_' => line.push('\u{2011}'),
b'\n' | b'\r' => flush(&mut line, level, indent, &mut out),
_ => {}
p.run();
p.flush();
finish_surrogates(p.out)
}
impl Outline<'_> {
fn run(&mut self) {
while self.i < self.b.len() {
match self.b[self.i] {
b'{' => self.open_group(),
b'}' => self.close_group(),
b'\\' => {
self.i += 1;
if !self.escape() {
break;
}
}
}
b'\r' | b'\n' => i += 1,
_ => {
let s = i;
while i < b.len() && !matches!(b[i], b'{' | b'}' | b'\\' | b'\r' | b'\n') {
i += 1;
}
if !st.skip
&& let Some(t) = rtf.get(s..i)
{
line.push_str(&t.replace('\u{0}', ""));
}
b'\r' | b'\n' => self.i += 1,
_ => self.text(),
}
}
}
flush(&mut line, level, indent, &mut out);
finish_surrogates(out)
/// `{`: push the group; `{\*\dest …}` is an ignorable destination.
fn open_group(&mut self) {
self.stack.push(self.st);
self.i += 1;
if self.b[self.i..].starts_with(b"\\*") {
self.st.skip = true;
}
}
fn close_group(&mut self) {
self.st = self.stack.pop().unwrap_or(St { skip: false, uc: 1 });
self.i += 1;
}
/// The character after a backslash: a control word, a hex escape or a symbol. False at the
/// end of the input.
fn escape(&mut self) -> bool {
let Some(&n) = self.b.get(self.i) else { return false };
if n.is_ascii_alphabetic() {
self.control_word();
} else if n == b'\'' {
self.hex_escape();
} else {
self.symbol(n);
}
true
}
fn control_word(&mut self) {
let rtf = self.rtf;
let s = self.i;
while self.i < self.b.len() && self.b[self.i].is_ascii_alphabetic() {
self.i += 1;
}
let word = &rtf[s..self.i];
let ns = self.i;
if self.i < self.b.len() && (self.b[self.i] == b'-' || self.b[self.i].is_ascii_digit()) {
self.i += 1;
while self.i < self.b.len() && self.b[self.i].is_ascii_digit() {
self.i += 1;
}
}
let arg: Option<i64> = rtf[ns..self.i].parse().ok();
if self.i < self.b.len() && self.b[self.i] == b' ' {
self.i += 1;
}
if self.pending_skip > 0 {
self.pending_skip -= 1;
return;
}
if SKIP.contains(&word) {
self.st.skip = true;
}
if !self.st.skip {
self.apply(word, arg);
}
}
fn apply(&mut self, word: &str, arg: Option<i64>) {
match word {
"par" => self.flush(),
"pard" => {
self.level = None;
self.indent = 0;
}
"outlinelevel" => self.level = arg.map(|a| a.clamp(0, 9) as u32),
"li" => self.indent = arg.unwrap_or(0),
"tab" | "line" => self.line.push(' '),
"uc" => self.st.uc = arg.unwrap_or(1).clamp(0, 8) as usize,
"u" => self.unicode(arg.unwrap_or(0)),
"emdash" => self.line.push('—'),
"endash" => self.line.push('–'),
"bullet" => self.line.push('•'),
"lquote" => self.line.push('‘'),
"rquote" => self.line.push('’'),
"ldblquote" => self.line.push('“'),
"rdblquote" => self.line.push('”'),
_ => {}
}
}
/// `\uN`: one UTF-16 unit, then the `\uc` fallback characters, which are skipped.
fn unicode(&mut self, v: i64) {
// RTF writes UTF-16 units as signed 16-bit values.
push_utf16(&mut self.line, (v & 0xffff) as u32);
self.pending_skip = 0;
let mut k = self.st.uc;
while k > 0 && self.i < self.b.len() {
if self.b[self.i] == b'\\' && self.b.get(self.i + 1) == Some(&b'\'') {
self.i = advance(self.rtf, self.i, 4);
} else if matches!(self.b[self.i], b'{' | b'}' | b'\\') {
break;
} else {
self.i = advance(self.rtf, self.i, 1);
}
k -= 1;
}
}
/// `\'hh`: one cp1252 byte.
fn hex_escape(&mut self) {
let hex =
self.rtf.get(self.i + 1..self.i + 3).filter(|h| h.bytes().all(|b| b.is_ascii_hexdigit())).and_then(|h| u8::from_str_radix(h, 16).ok());
self.i = advance(self.rtf, self.i, 3);
if !self.st.skip
&& let Some(h) = hex
&& h != 0
{
self.line.push(cp1252(h));
}
}
/// A one-character control symbol (`\\`, `\{`, `\~`, ...).
fn symbol(&mut self, n: u8) {
self.i += 1;
if self.st.skip {
return;
}
match n {
b'\\' | b'{' | b'}' => self.line.push(n as char),
b'~' => self.line.push('\u{a0}'),
b'_' => self.line.push('\u{2011}'),
b'\n' | b'\r' => self.flush(),
_ => {}
}
}
/// Plain text up to the next special character.
fn text(&mut self) {
let s = self.i;
while self.i < self.b.len() && !matches!(self.b[self.i], b'{' | b'}' | b'\\' | b'\r' | b'\n') {
self.i += 1;
}
if !self.st.skip
&& let Some(t) = self.rtf.get(s..self.i)
{
self.line.push_str(&t.replace('\u{0}', ""));
}
}
/// Ends the current line: a non-empty line becomes one outline entry, indented by its level.
fn flush(&mut self) {
let text = self.line.trim();
if !text.is_empty() {
let n = self.level.map(|l| l as i64).unwrap_or(self.indent.saturating_add(180) / 360).clamp(0, 9) as usize;
self.out.push_str(&"\t".repeat(n));
self.out.push_str(text);
self.out.push('\n');
}
self.line.clear();
}
}
/// Push a UTF-16 unit; surrogate halves are parked as private markers (NUL plus four hex digits)
+26 -37
View File
@@ -301,6 +301,25 @@ impl Dpb {
Ok(lists)
}
/// Marks as unused every entry in state `mark` that `hit` selects.
fn unmark_where(&mut self, mark: RefMark, hit: impl Fn(&DpbEntry) -> bool) {
for e in &mut self.entries {
if e.mark == mark && hit(e) {
e.mark = RefMark::Unused;
}
}
}
/// Makes every short-term entry that `hit` selects a long-term one with index `idx`.
fn mark_long_where(&mut self, hit: impl Fn(&DpbEntry) -> bool, idx: u32) {
for e in &mut self.entries {
if e.mark == RefMark::Short && hit(e) {
e.mark = RefMark::Long;
e.long_term_frame_idx = idx;
}
}
}
/// One memory management control operation; returns true for MMCO 5.
fn apply_mmco(&mut self, m: &Mmco, cur_frame_num: u32, max_frame_num: u32, cur_long: &mut Option<u32>) -> bool {
let mut had5 = false;
@@ -308,40 +327,17 @@ impl Dpb {
match m.op {
1 => {
let pn = curr_pic_num - (m.difference_of_pic_nums_minus1 as i32 + 1);
for e in &mut self.entries {
if e.mark == RefMark::Short && Self::frame_num_wrap(e, cur_frame_num, max_frame_num) == pn {
e.mark = RefMark::Unused;
}
}
}
2 => {
for e in &mut self.entries {
if e.mark == RefMark::Long && e.long_term_frame_idx == m.long_term_pic_num {
e.mark = RefMark::Unused;
}
}
self.unmark_where(RefMark::Short, |e| Self::frame_num_wrap(e, cur_frame_num, max_frame_num) == pn);
}
2 => self.unmark_where(RefMark::Long, |e| e.long_term_frame_idx == m.long_term_pic_num),
3 => {
let pn = curr_pic_num - (m.difference_of_pic_nums_minus1 as i32 + 1);
for e in &mut self.entries {
if e.mark == RefMark::Long && e.long_term_frame_idx == m.long_term_frame_idx {
e.mark = RefMark::Unused;
}
}
for e in &mut self.entries {
if e.mark == RefMark::Short && Self::frame_num_wrap(e, cur_frame_num, max_frame_num) == pn {
e.mark = RefMark::Long;
e.long_term_frame_idx = m.long_term_frame_idx;
}
}
self.unmark_where(RefMark::Long, |e| e.long_term_frame_idx == m.long_term_frame_idx);
self.mark_long_where(|e| Self::frame_num_wrap(e, cur_frame_num, max_frame_num) == pn, m.long_term_frame_idx);
}
4 => {
let max = m.max_long_term_frame_idx_plus1;
for e in &mut self.entries {
if e.mark == RefMark::Long && (max == 0 || e.long_term_frame_idx > max - 1) {
e.mark = RefMark::Unused;
}
}
self.unmark_where(RefMark::Long, |e| max == 0 || e.long_term_frame_idx > max - 1);
self.max_long_term_frame_idx = if max == 0 { None } else { Some(max - 1) };
}
5 => {
@@ -353,16 +349,9 @@ impl Dpb {
}
6 => {
// Only an index within the allowed range may be assigned (8.2.5.4.6).
let allowed = match self.max_long_term_frame_idx {
Some(max) => m.long_term_frame_idx <= max,
None => false,
};
let allowed = self.max_long_term_frame_idx.is_some_and(|max| m.long_term_frame_idx <= max);
if allowed {
for e in &mut self.entries {
if e.mark == RefMark::Long && e.long_term_frame_idx == m.long_term_frame_idx {
e.mark = RefMark::Unused;
}
}
self.unmark_where(RefMark::Long, |e| e.long_term_frame_idx == m.long_term_frame_idx);
*cur_long = Some(m.long_term_frame_idx);
}
}
+89 -77
View File
@@ -74,16 +74,39 @@ impl<'a> SliceDecoder<'a> {
/// Merge mode motion (8.5.3.2.2).
pub fn derive_merge(&self, g: PbGeom, part: PartMode, merge_idx: usize) -> MvField {
let PbGeom { xc, yc, ncb, xp: xp0, yp: yp0, w: w0, h: h0, part_idx: part_idx0 } = g;
let (w0, h0) = (g.w, g.h);
// With a parallel merge level above 4x4, an 8x8 coding block shares one candidate set.
let g = if self.pps.log2_parallel_merge_level > 2 && g.ncb == 8 {
PbGeom { xp: g.xc, yp: g.yc, w: g.ncb, h: g.ncb, part_idx: 0, ..g }
} else {
g
};
let max = self.sh.max_num_merge_cand as usize;
let (list, n) = self.spatial_merge_candidates(&g, part);
if merge_idx < n {
return self.finish_merge(list[merge_idx], w0, h0);
}
let mut cands: Vec<MvField> = list[..n].to_vec();
cands.extend(self.temporal_merge_candidate(g.xp, g.yp, g.w, g.h));
if merge_idx < cands.len() {
return self.finish_merge(cands[merge_idx], w0, h0);
}
self.combined_merge_candidates(&mut cands, max);
if merge_idx < cands.len() {
return self.finish_merge(cands[merge_idx], w0, h0);
}
self.zero_merge_candidates(&mut cands, merge_idx);
self.finish_merge(cands[merge_idx], w0, h0)
}
/// The spatial candidates A1, B1, B0, A0 and B2 (8.5.3.2.3): the candidates, and how many.
fn spatial_merge_candidates(&self, g: &PbGeom, part: PartMode) -> ([MvField; 5], usize) {
let PbGeom { xp, yp, w, h, part_idx, .. } = *g;
let pml = self.pps.log2_parallel_merge_level;
let (xp, yp, w, h, part_idx) = if pml > 2 && ncb == 8 { (xc, yc, ncb, ncb, 0) } else { (xp0, yp0, w0, h0, part_idx0) };
let g = PbGeom { xc, yc, ncb, xp, yp, w, h, part_idx };
let same_mer = |xn: i32, yn: i32| (xp >> pml) == (xn >> pml) && (yp >> pml) == (yn >> pml);
let avail = |xn: i32, yn: i32| self.pb_avail(&g, xn, yn) && !same_mer(xn, yn);
let avail = |xn: i32, yn: i32| self.pb_avail(g, xn, yn) && !same_mer(xn, yn);
let mut list: [MvField; 5] = [MvField::default(); 5];
let mut n = 0;
let max = self.sh.max_num_merge_cand as usize;
// spatial candidates (8.5.3.2.3)
let pa1 = (xp - 1, yp + h - 1);
let av_a1 = avail(pa1.0, pa1.1) && !(matches!(part, PartMode::PNx2N | PartMode::PnLx2N | PartMode::PnRx2N) && part_idx == 1);
let m_a1 = if av_a1 { self.mv_at(pa1.0, pa1.1) } else { MvField::default() };
@@ -130,10 +153,11 @@ impl<'a> SliceDecoder<'a> {
list[n] = self.mv_at(pb2.0, pb2.1);
n += 1;
}
if merge_idx < n {
return self.finish_merge(list[merge_idx], w0, h0);
}
// temporal candidate
(list, n)
}
/// The temporal candidate (8.5.3.2.8): list 0 from the collocated picture, and list 1 for B slices.
fn temporal_merge_candidate(&self, xp: i32, yp: i32, w: i32, h: i32) -> Option<MvField> {
let mut col = MvField::default();
let mut col_avail = false;
if let Some(mv) = self.temporal(xp, yp, w, h, 0, 0) {
@@ -148,30 +172,28 @@ impl<'a> SliceDecoder<'a> {
col.ref_idx[1] = 0;
col_avail = true;
}
let mut cands: Vec<MvField> = list[..n].to_vec();
if col_avail {
cands.push(col);
}
if merge_idx < cands.len() {
return self.finish_merge(cands[merge_idx], w0, h0);
}
// combined bi-predictive candidates (8.5.3.2.4)
col_avail.then_some(col)
}
/// Combined bi-predictive candidates (8.5.3.2.4) from pairs of the original candidates.
fn combined_merge_candidates(&self, cands: &mut Vec<MvField>, max: usize) {
let num_orig = cands.len();
if self.sh.is_b() && num_orig > 1 && num_orig < max {
let mut comb = 0;
while comb < num_orig * (num_orig - 1) && cands.len() < max {
let (i0, i1) = COMB[comb];
let (c0, c1) = (cands[i0], cands[i1]);
if c0.pred(0) && c1.pred(1) && (self.ref_pic(0, c0.ref_idx[0]).poc != self.ref_pic(1, c1.ref_idx[1]).poc || c0.mv[0] != c1.mv[1]) {
cands.push(MvField { mv: [c0.mv[0], c1.mv[1]], ref_idx: [c0.ref_idx[0], c1.ref_idx[1]] });
}
comb += 1;
if !(self.sh.is_b() && num_orig > 1 && num_orig < max) {
return;
}
let mut comb = 0;
while comb < num_orig * (num_orig - 1) && cands.len() < max {
let (i0, i1) = COMB[comb];
let (c0, c1) = (cands[i0], cands[i1]);
if c0.pred(0) && c1.pred(1) && (self.ref_pic(0, c0.ref_idx[0]).poc != self.ref_pic(1, c1.ref_idx[1]).poc || c0.mv[0] != c1.mv[1]) {
cands.push(MvField { mv: [c0.mv[0], c1.mv[1]], ref_idx: [c0.ref_idx[0], c1.ref_idx[1]] });
}
comb += 1;
}
if merge_idx < cands.len() {
return self.finish_merge(cands[merge_idx], w0, h0);
}
// zero candidates (8.5.3.2.5)
}
/// Zero candidates (8.5.3.2.5) up to and including `merge_idx`.
fn zero_merge_candidates(&self, cands: &mut Vec<MvField>, merge_idx: usize) {
let num_ref = if self.sh.is_b() { self.sh.num_ref_idx[0].min(self.sh.num_ref_idx[1]) } else { self.sh.num_ref_idx[0] } as usize;
let mut zero_idx = 0;
while cands.len() <= merge_idx {
@@ -179,7 +201,6 @@ impl<'a> SliceDecoder<'a> {
cands.push(MvField { mv: [[0; 2]; 2], ref_idx: [r, if self.sh.is_b() { r } else { -1 }] });
zero_idx += 1;
}
self.finish_merge(cands[merge_idx], w0, h0)
}
fn finish_merge(&self, mut f: MvField, w: i32, h: i32) -> MvField {
@@ -240,68 +261,59 @@ impl<'a> SliceDecoder<'a> {
if cur.long_term || col_diff == cur_diff { Some(mv) } else { Some(scale_mv(mv, col_diff, cur_diff)) }
}
/// AMVP neighbour motion with the target's reference picture (no scaling), 8.5.3.2.7.
fn amvp_exact(&self, f: &MvField, l: usize, tpoc: i32) -> Option<[i16; 2]> {
let y = 1 - l;
if f.pred(l) && self.ref_pic(l, f.ref_idx[l]).poc == tpoc {
Some(f.mv[l])
} else if f.pred(y) && self.ref_pic(y, f.ref_idx[y]).poc == tpoc {
Some(f.mv[y])
} else {
None
}
}
/// AMVP neighbour motion with a long-term-compatible reference, scaled when both are short-term.
fn amvp_scaled(&self, f: &MvField, l: usize, tpoc: i32, tlt: bool, cur_poc: i32) -> Option<[i16; 2]> {
let y = 1 - l;
let pick = if f.pred(l) && self.ref_pic(l, f.ref_idx[l]).long_term == tlt {
Some((f.mv[l], self.ref_pic(l, f.ref_idx[l])))
} else if f.pred(y) && self.ref_pic(y, f.ref_idx[y]).long_term == tlt {
Some((f.mv[y], self.ref_pic(y, f.ref_idx[y])))
} else {
None
};
pick.map(|(mv, r)| if r.poc != tpoc && !r.long_term && !tlt { scale_mv(mv, cur_poc - r.poc, cur_poc - tpoc) } else { mv })
}
/// The first neighbour whose position is available and whose motion `pick` accepts.
fn first_amvp_neighbour(&self, pos: &[(i32, i32)], avail: &[bool], pick: impl Fn(&MvField) -> Option<[i16; 2]>) -> Option<[i16; 2]> {
pos.iter().zip(avail).find_map(|(&(x, y), &ok)| if ok { pick(&self.mv_at(x, y)) } else { None })
}
/// AMVP predictor (8.5.3.2.6, 8.5.3.2.7).
pub fn derive_amvp(&self, g: PbGeom, l: usize, ref_idx: usize, mvp_flag: usize) -> [i16; 2] {
let PbGeom { xp, yp, w, h, .. } = g;
let target = &self.refs[l][ref_idx];
let (tpoc, tlt) = (target.poc, target.long_term);
let cur_poc = self.pic.poc;
let y = 1 - l;
// exact reference match (no scaling)
let unscaled = |f: &MvField| -> Option<[i16; 2]> {
if f.pred(l) && self.ref_pic(l, f.ref_idx[l]).poc == tpoc {
Some(f.mv[l])
} else if f.pred(y) && self.ref_pic(y, f.ref_idx[y]).poc == tpoc {
Some(f.mv[y])
} else {
None
}
};
// long-term-compatible match, scaled when both references are short-term
let scaled = |f: &MvField| -> Option<[i16; 2]> {
let pick = if f.pred(l) && self.ref_pic(l, f.ref_idx[l]).long_term == tlt {
Some((f.mv[l], self.ref_pic(l, f.ref_idx[l])))
} else if f.pred(y) && self.ref_pic(y, f.ref_idx[y]).long_term == tlt {
Some((f.mv[y], self.ref_pic(y, f.ref_idx[y])))
} else {
None
};
pick.map(|(mv, r)| if r.poc != tpoc && !r.long_term && !tlt { scale_mv(mv, cur_poc - r.poc, cur_poc - tpoc) } else { mv })
};
let exact = |f: &MvField| self.amvp_exact(f, l, tpoc);
let scaled = |f: &MvField| self.amvp_scaled(f, l, tpoc, tlt, cur_poc);
let pa = [(xp - 1, yp + h), (xp - 1, yp + h - 1)];
let av_a = [self.pb_avail(&g, pa[0].0, pa[0].1), self.pb_avail(&g, pa[1].0, pa[1].1)];
let is_scaled = av_a[0] || av_a[1];
let mut a = None;
for k in 0..2 {
if av_a[k] && a.is_none() {
a = unscaled(&self.mv_at(pa[k].0, pa[k].1));
}
}
let mut a = self.first_amvp_neighbour(&pa, &av_a, exact);
if a.is_none() {
for k in 0..2 {
if av_a[k] && a.is_none() {
a = scaled(&self.mv_at(pa[k].0, pa[k].1));
}
}
a = self.first_amvp_neighbour(&pa, &av_a, scaled);
}
let pb = [(xp + w, yp - 1), (xp + w - 1, yp - 1), (xp - 1, yp - 1)];
let av_b = [0, 1, 2].map(|k| self.pb_avail(&g, pb[k].0, pb[k].1));
let mut b = None;
for k in 0..3 {
if av_b[k] && b.is_none() {
b = unscaled(&self.mv_at(pb[k].0, pb[k].1));
}
}
let mut b = self.first_amvp_neighbour(&pb, &av_b, exact);
if !is_scaled && b.is_some() {
a = b;
}
if !is_scaled {
b = None;
for k in 0..3 {
if av_b[k] && b.is_none() {
b = scaled(&self.mv_at(pb[k].0, pb[k].1));
}
}
b = self.first_amvp_neighbour(&pb, &av_b, scaled);
}
let mut list: [[i16; 2]; 3] = [[0; 2]; 3];
let mut n = 0;
+86 -64
View File
@@ -1366,84 +1366,106 @@ impl<'a> SliceDecoder<'a> {
/// prediction_unit(): parse, derive motion, store it and predict. Returns merge_flag.
fn prediction_unit(&mut self, g: PbGeom, part: PartMode, skip: bool) -> Result<bool> {
let PbGeom { xc, yc, ncb, xp, yp, w, h, part_idx } = g;
let merge = if skip { true } else { self.dd(MERGE_FLAG) == 1 };
let mvf = if merge {
let idx = if self.sh.max_num_merge_cand > 1 {
let mut i = 0;
if self.dd(MERGE_IDX) == 1 {
i = 1;
while i < self.sh.max_num_merge_cand - 1 && self.bypass() == 1 {
i += 1;
}
}
i
} else {
0
};
self.derive_merge(PbGeom { xc, yc, ncb, xp, yp, w, h, part_idx }, part, idx as usize)
let idx = self.merge_idx();
self.derive_merge(g, part, idx as usize)
} else {
let pred_idc = if self.sh.is_b() {
if w + h != 12 {
let depth = self.pic.blk_at(xc, yc).depth as usize;
if self.dd(INTER_PRED_IDC + depth) == 1 { 2 } else { self.dd(INTER_PRED_IDC + 4) }
} else {
self.dd(INTER_PRED_IDC + 4)
}
} else {
0
};
let mut ref_idx = [-1i8; 2];
let mut mvd = [[0i32; 2]; 2];
let mut mvp_flag = [0u32; 2];
for l in 0..2 {
let used = if l == 0 { pred_idc != 1 } else { pred_idc != 0 };
if !used {
continue;
}
let nref = self.sh.num_ref_idx[l];
let mut ri = 0;
if nref > 1 {
while ri < nref - 1 {
let bin = if ri < 2 { self.dd(REF_IDX + ri as usize) } else { self.bypass() };
if bin == 0 {
break;
}
ri += 1;
}
}
ref_idx[l] = ri as i8;
if l == 1 && self.sh.mvd_l1_zero && pred_idc == 2 {
mvd[1] = [0, 0];
} else {
mvd[l] = self.mvd_coding()?;
}
mvp_flag[l] = self.dd(MVP_FLAG);
}
let mut f = MvField { mv: [[0; 2]; 2], ref_idx };
for l in 0..2 {
if ref_idx[l] < 0 {
continue;
}
let mvp = self.derive_amvp(PbGeom { xc, yc, ncb, xp, yp, w, h, part_idx }, l, ref_idx[l] as usize, mvp_flag[l] as usize);
// mvLX = (mvp + mvd) wrapped to 16 bits (8-94)
f.mv[l] = [(mvp[0] as i32 + mvd[l][0]) as i16, (mvp[1] as i32 + mvd[l][1]) as i16];
}
f
self.amvp_motion(g)?
};
for l in 0..2 {
if mvf.ref_idx[l] >= 0 {
ensure!((mvf.ref_idx[l] as usize) < self.refs[l].len(), "reference index out of range");
}
}
// store motion
self.store_motion(g.xp, g.yp, g.w, g.h, mvf);
self.motion_compensate(g.xp, g.yp, g.w as usize, g.h as usize, &mvf);
Ok(merge)
}
/// merge_idx(): the merge candidate index, truncated unary (0 when there is one candidate).
fn merge_idx(&mut self) -> u32 {
if self.sh.max_num_merge_cand <= 1 {
return 0;
}
let mut i = 0;
if self.dd(MERGE_IDX) == 1 {
i = 1;
while i < self.sh.max_num_merge_cand - 1 && self.bypass() == 1 {
i += 1;
}
}
i
}
/// inter_pred_idc: 0 = list 0, 1 = list 1, 2 = both.
fn inter_pred_idc(&mut self, xc: i32, yc: i32, w: i32, h: i32) -> u32 {
if !self.sh.is_b() {
return 0;
}
if w + h != 12 {
let depth = self.pic.blk_at(xc, yc).depth as usize;
if self.dd(INTER_PRED_IDC + depth) == 1 { 2 } else { self.dd(INTER_PRED_IDC + 4) }
} else {
self.dd(INTER_PRED_IDC + 4)
}
}
/// ref_idx_lX: truncated unary, the first two bins context-coded.
fn ref_idx(&mut self, l: usize) -> u32 {
let nref = self.sh.num_ref_idx[l];
let mut ri = 0;
if nref > 1 {
while ri < nref - 1 {
let bin = if ri < 2 { self.dd(REF_IDX + ri as usize) } else { self.bypass() };
if bin == 0 {
break;
}
ri += 1;
}
}
ri
}
/// The AMVP branch: parse each used list's reference index, mvd and predictor flag, then add
/// every mvd to its predictor.
fn amvp_motion(&mut self, g: PbGeom) -> Result<MvField> {
let pred_idc = self.inter_pred_idc(g.xc, g.yc, g.w, g.h);
let mut ref_idx = [-1i8; 2];
let mut mvd = [[0i32; 2]; 2];
let mut mvp_flag = [0u32; 2];
for l in 0..2 {
let used = if l == 0 { pred_idc != 1 } else { pred_idc != 0 };
if !used {
continue;
}
ref_idx[l] = self.ref_idx(l) as i8;
if l == 1 && self.sh.mvd_l1_zero && pred_idc == 2 {
mvd[1] = [0, 0];
} else {
mvd[l] = self.mvd_coding()?;
}
mvp_flag[l] = self.dd(MVP_FLAG);
}
let mut f = MvField { mv: [[0; 2]; 2], ref_idx };
for l in 0..2 {
if ref_idx[l] < 0 {
continue;
}
let mvp = self.derive_amvp(g, l, ref_idx[l] as usize, mvp_flag[l] as usize);
// mvLX = (mvp + mvd) wrapped to 16 bits (8-94)
f.mv[l] = [(mvp[0] as i32 + mvd[l][0]) as i16, (mvp[1] as i32 + mvd[l][1]) as i16];
}
Ok(f)
}
/// Writes a prediction block's motion into the picture's 4x4 motion field.
fn store_motion(&mut self, xp: i32, yp: i32, w: i32, h: i32, mvf: MvField) {
let w4 = self.pic.w4;
for by in (yp >> 2)..((yp + h) >> 2) {
let o = by as usize * w4;
self.pic.mvf[o + (xp >> 2) as usize..o + ((xp + w) >> 2) as usize].fill(mvf);
}
self.motion_compensate(xp, yp, w as usize, h as usize, &mvf);
Ok(merge)
}
fn mvd_coding(&mut self) -> Result<[i32; 2]> {
+57 -42
View File
@@ -608,64 +608,73 @@ impl Chart {
if ser.is_empty() {
return (!secondary && self.kind != ChartType::Combo).then_some((0.0, 0.0));
}
let (mut lo, mut hi) = (f64::INFINITY, f64::NEG_INFINITY);
let mut ext = (f64::INFINITY, f64::NEG_INFINITY);
let has_stacked_or_percent = ser.iter().any(|s| {
let k = self.series_kind(s);
k.stacked() || k.percent()
});
let mut percent = false;
if has_stacked_or_percent {
let n = ser.iter().map(|s| s.values.len()).max().unwrap_or(0);
let budget = (ser.len() * n).min(10_000_000);
let n_clamped = if ser.is_empty() { 0 } else { n.min(budget / ser.len()) };
for i in 0..n_clamped {
let (mut p, mut m, mut stacked) = (0.0, 0.0, false);
for s in &ser {
let k = self.series_kind(s);
let Some(v) = s.values.get(i).copied().flatten().filter(|v| v.is_finite()) else { continue };
if k.percent() {
percent = true;
} else if k.stacked() {
stacked = true;
if v >= 0.0 { p += v } else { m += v }
} else {
hi = hi.max(v);
lo = lo.min(v);
}
}
if stacked {
hi = hi.max(p);
lo = lo.min(m);
}
}
let percent = if has_stacked_or_percent {
self.fold_stacked(&ser, &mut ext)
} else {
for s in &ser {
for v in s.values.iter().flatten().copied().filter(|v| v.is_finite()) {
hi = hi.max(v);
lo = lo.min(v);
widen(&mut ext, v);
}
}
}
false
};
if !percent {
for s in &ser {
if self.series_kind(s).stacked() {
continue;
}
for y in s.trendline_ys(crate::trendline::series_is_scatter(self.series_kind(s))) {
hi = hi.max(y);
lo = lo.min(y);
}
}
self.fold_trendlines(&ser, &mut ext);
}
if percent {
lo = lo.min(0.0);
hi = hi.max(1.0);
ext.0 = ext.0.min(0.0);
ext.1 = ext.1.max(1.0);
}
let (lo, hi) = ext;
Some(if lo <= hi { (lo, hi) } else { (0.0, 0.0) })
}
/// Widens `ext` by the points of the stacked and 100 % series in `ser`; returns whether a
/// 100 % series is among them.
fn fold_stacked(&self, ser: &[&Series], ext: &mut (f64, f64)) -> bool {
let mut percent = false;
let n = ser.iter().map(|s| s.values.len()).max().unwrap_or(0);
let budget = (ser.len() * n).min(10_000_000);
let n_clamped = if ser.is_empty() { 0 } else { n.min(budget / ser.len()) };
for i in 0..n_clamped {
let (mut p, mut m, mut stacked) = (0.0, 0.0, false);
for s in ser {
let k = self.series_kind(s);
let Some(v) = s.values.get(i).copied().flatten().filter(|v| v.is_finite()) else { continue };
if k.percent() {
percent = true;
} else if k.stacked() {
stacked = true;
if v >= 0.0 { p += v } else { m += v }
} else {
widen(ext, v);
}
}
if stacked {
ext.1 = ext.1.max(p);
ext.0 = ext.0.min(m);
}
}
percent
}
/// Widens `ext` by the trendlines of the series in `ser` that are not stacked.
fn fold_trendlines(&self, ser: &[&Series], ext: &mut (f64, f64)) {
for s in ser {
if self.series_kind(s).stacked() {
continue;
}
for y in s.trendline_ys(crate::trendline::series_is_scatter(self.series_kind(s))) {
widen(ext, y);
}
}
}
/// A waterfall's bars as (from, to) on the value axis: each point floats from the running
/// total, a total point stands on zero and sets it.
pub fn waterfall_bars(&self) -> Vec<(f64, f64)> {
@@ -1346,6 +1355,12 @@ impl Chart {
}
}
/// Widens the (min, max) extent `ext` to include `v`.
fn widen(ext: &mut (f64, f64), v: f64) {
ext.0 = ext.0.min(v);
ext.1 = ext.1.max(v);
}
/// Excel's QUARTILE.EXC position `p·(n+1)` (1-based) on sorted data, clamped to the ends.
fn quantile_exc(v: &[f64], p: f64) -> f64 {
let n = v.len();
+86 -65
View File
@@ -147,6 +147,86 @@ pub fn look(ctx: &Ctx, t: &Table, r: usize, c: usize) -> CellLook {
look_with_def(ctx, resolve_style(ctx, t), t, r, c)
}
/// Which parts of the table a cell sits in, for the preset styles.
#[derive(Clone, Copy)]
struct PresetPart {
header: bool,
total: bool,
first_col: bool,
last_col: bool,
banded: bool,
}
impl PresetPart {
/// In a header, total, first or last column: the parts a preset style makes strong.
fn strong(self) -> bool {
self.header || self.total || self.first_col || self.last_col
}
}
/// Tint `k` of accent `a`.
fn tint(a: Rgba, k: i32) -> Rgba {
deckcraft_color::apply(a, &[ColorTransform::Tint(k)])
}
fn light1_look(a: Rgba, p: PresetPart, on: impl Fn(Rgba) -> Rgba) -> CellLook {
let fill = p.banded.then(|| tint(a, 20000));
CellLook { fill, text: fill.map(on), bold: p.strong(), border: if p.header { Some((a, 1.0)) } else { None }, sides: [None; 4], paint: None }
}
fn light2_look(a: Rgba, p: PresetPart) -> CellLook {
CellLook {
fill: if p.header { Some(a) } else { None },
text: if p.header { Some(Rgba::WHITE) } else { None },
bold: p.header || p.total || p.first_col,
border: Some((a, 1.0)),
sides: [None; 4],
paint: None,
}
}
fn dark1_look(a: Rgba, p: PresetPart) -> CellLook {
CellLook {
fill: Some(if p.header {
Rgba::BLACK
} else if p.banded {
deckcraft_color::apply(a, &[ColorTransform::Shade(60000)])
} else {
deckcraft_color::apply(a, &[ColorTransform::Shade(75000)])
}),
text: Some(Rgba::WHITE),
bold: p.header || p.total || p.first_col,
border: Some((Rgba::WHITE, 1.0)),
sides: [None; 4],
paint: None,
}
}
fn medium4_look(a: Rgba, p: PresetPart, on: impl Fn(Rgba) -> Rgba) -> CellLook {
let fill = if p.header || p.banded { tint(a, 40000) } else { tint(a, 20000) };
CellLook { fill: Some(fill), text: Some(on(fill)), bold: p.strong(), border: Some((a, 0.75)), sides: [None; 4], paint: None }
}
/// medium2: strong header, banded light tints, white grid.
fn medium2_look(a: Rgba, p: PresetPart, on: impl Fn(Rgba) -> Rgba) -> CellLook {
let strong = p.strong();
let fill = if strong {
a
} else if p.banded {
tint(a, 40000)
} else {
tint(a, 20000)
};
CellLook {
fill: Some(fill),
text: Some(if strong { Rgba::WHITE } else { on(fill) }),
bold: strong,
border: Some((Rgba::WHITE, 1.0)),
sides: [None; 4],
paint: None,
}
}
pub fn look_with_def(ctx: &Ctx, def: Option<&TableStyleDef>, t: &Table, r: usize, c: usize) -> CellLook {
if let Some(def) = def {
return look_def(ctx, def, t, r, c);
@@ -160,8 +240,7 @@ pub fn look_with_def(ctx: &Ctx, def: Option<&TableStyleDef>, t: &Table, r: usize
let body_r = if t.first_row { r.wrapping_sub(1) } else { r };
let band = t.band_row && !header && !total && body_r % 2 == 0;
let band_c = t.band_col && c.is_multiple_of(2);
let tint = |k: i32| deckcraft_color::apply(a, &[ColorTransform::Tint(k)]);
let white = Rgba::WHITE;
let p = PresetPart { header, total, first_col, last_col, banded: band || band_c };
let text_color = ctx.color(&ColorRef::scheme(SchemeSlot::Tx1), None);
// Text on a tinted cell never inherits the text colour, which a dark theme makes light and
// unreadable on the light tint: PowerPoint's medium styles set it to the theme's dark colour
@@ -173,69 +252,11 @@ pub fn look_with_def(ctx: &Ctx, def: Option<&TableStyleDef>, t: &Table, r: usize
match kind {
"none" => CellLook { fill: None, text: None, bold: false, border: None, sides: [None; 4], paint: None },
"grid" => CellLook { fill: None, text: None, bold: false, border: Some((text_color, 0.75)), sides: [None; 4], paint: None },
"light1" => {
let fill = (band || band_c).then(|| tint(20000));
CellLook {
fill,
text: fill.map(on),
bold: header || total || first_col || last_col,
border: if header { Some((a, 1.0)) } else { None },
sides: [None; 4],
paint: None,
}
}
"light2" => CellLook {
fill: if header { Some(a) } else { None },
text: if header { Some(white) } else { None },
bold: header || total || first_col,
border: Some((a, 1.0)),
sides: [None; 4],
paint: None,
},
"dark1" => CellLook {
fill: Some(if header {
Rgba::BLACK
} else if band || band_c {
deckcraft_color::apply(a, &[ColorTransform::Shade(60000)])
} else {
deckcraft_color::apply(a, &[ColorTransform::Shade(75000)])
}),
text: Some(white),
bold: header || total || first_col,
border: Some((white, 1.0)),
sides: [None; 4],
paint: None,
},
"medium4" => {
let fill = if header || band || band_c { tint(40000) } else { tint(20000) };
CellLook {
fill: Some(fill),
text: Some(on(fill)),
bold: header || total || first_col || last_col,
border: Some((a, 0.75)),
sides: [None; 4],
paint: None,
}
}
_ => {
// medium2: strong header, banded light tints, white grid.
let strong = header || total || first_col || last_col;
let fill = if strong {
a
} else if band || band_c {
tint(40000)
} else {
tint(20000)
};
CellLook {
fill: Some(fill),
text: Some(if strong { white } else { on(fill) }),
bold: strong,
border: Some((white, 1.0)),
sides: [None; 4],
paint: None,
}
}
"light1" => light1_look(a, p, on),
"light2" => light2_look(a, p),
"dark1" => dark1_look(a, p),
"medium4" => medium4_look(a, p, on),
_ => medium2_look(a, p, on),
}
}
+114 -74
View File
@@ -739,84 +739,19 @@ fn keyboard(ctx: &egui::Context, show: &mut Show, doc: &Presentation, now: f64,
let mut keep = true;
let mut actions = vec![];
for (k, m) in keys {
use egui::Key::*;
let presenter_grid = presenter && show.presenter_grid;
let grid_open = if presenter { show.presenter_grid } else { show.grid };
if grid_open && !matches!(k, Escape | Minus) {
let handled = if grid_open(show, presenter) && !matches!(k, egui::Key::Escape | egui::Key::Minus) {
// See All Slides takes no other keys until a slide is chosen.
if k == G && plain(m) {
if presenter_grid {
show.presenter_grid = false;
} else {
show.grid = false;
}
if k == egui::Key::G && plain(m) {
close_grid(show, presenter);
true
} else {
continue;
false
}
} else {
match k {
Escape | Minus => {
let cancelled = k == Escape && show.clear_goto();
if !cancelled && !show.back_out(presenter) {
keep = false;
}
}
ArrowRight | ArrowDown | Space | PageDown | N => {
if show.ended {
keep = false;
} else {
actions.push(show.state.next(doc));
}
}
Enter => {
if !show.goto.is_empty() {
if let Some(i) = show.take_goto()
&& i < doc.slides.len()
{
actions.push(show.state.goto(doc, i));
}
} else if show.ended {
keep = false;
} else {
actions.push(show.state.next(doc));
}
}
Backspace if !show.goto.is_empty() => {
show.goto.pop();
}
ArrowLeft | ArrowUp | Backspace | PageUp | P if !(k == P && m.command) => {
show.ended = false;
actions.push(show.state.prev(doc));
}
P if m.command => show.set_pen(!show.pen),
L if m.command => show.set_laser(!show.laser),
A if m.command => show.arrow(),
Home => {
if let Some(i) = show.state.first_index(doc) {
actions.push(show.state.goto(doc, i));
}
}
End => {
if let Some(i) = show.state.last_index(doc) {
actions.push(show.state.goto(doc, i));
}
}
B | Period => toggle_blank(show, Color32::BLACK),
W | Comma => toggle_blank(show, Color32::WHITE),
E => show.strokes.clear(),
G if plain(m) => show.open_grid(presenter),
Z if plain(m) => show.zoom_key(),
T if plain(m) => show.timer.toggle(now),
R if plain(m) => show.timer.reset(now),
OpenBracket if plain(m) => show.nudge_notes(-NOTES_STEP),
CloseBracket if plain(m) => show.nudge_notes(NOTES_STEP),
Num0 | Num1 | Num2 | Num3 | Num4 | Num5 | Num6 | Num7 | Num8 | Num9 => {
if let Some(d) = k.name().chars().next() {
show.push_slide_number(d, now);
}
}
_ => continue,
}
navigation_key(show, doc, k, m, presenter, &mut keep, &mut actions) || tool_key(show, k, m, now, presenter)
};
if !handled {
continue;
}
// The same key must not also activate a focused presenter button or be handled again
// by an embedded presenter viewport.
@@ -831,6 +766,111 @@ fn keyboard(ctx: &egui::Context, show: &mut Show, doc: &Presentation, now: f64,
keep
}
/// Whether the grid that the presenter key G opens is showing in this viewport.
fn grid_open(show: &Show, presenter: bool) -> bool {
if presenter { show.presenter_grid } else { show.grid }
}
/// Closes the grid that G opens in this viewport.
fn close_grid(show: &mut Show, presenter: bool) {
if presenter && show.presenter_grid {
show.presenter_grid = false;
} else {
show.grid = false;
}
}
/// Keys that move through the show or end it. False when `k` isn't one of them.
fn navigation_key(
show: &mut Show,
doc: &Presentation,
k: egui::Key,
m: egui::Modifiers,
presenter: bool,
keep: &mut bool,
actions: &mut Vec<ShowAction>,
) -> bool {
use egui::Key::*;
match k {
Escape | Minus => {
let cancelled = k == Escape && show.clear_goto();
if !cancelled && !show.back_out(presenter) {
*keep = false;
}
}
ArrowRight | ArrowDown | Space | PageDown | N => {
if show.ended {
*keep = false;
} else {
actions.push(show.state.next(doc));
}
}
Enter => {
if !show.goto.is_empty() {
if let Some(i) = show.take_goto()
&& i < doc.slides.len()
{
actions.push(show.state.goto(doc, i));
}
} else if show.ended {
*keep = false;
} else {
actions.push(show.state.next(doc));
}
}
Backspace if !show.goto.is_empty() => {
show.goto.pop();
}
ArrowLeft | ArrowUp | Backspace | PageUp => {
show.ended = false;
actions.push(show.state.prev(doc));
}
P if m.command => show.set_pen(!show.pen),
P => {
show.ended = false;
actions.push(show.state.prev(doc));
}
Home => {
if let Some(i) = show.state.first_index(doc) {
actions.push(show.state.goto(doc, i));
}
}
End => {
if let Some(i) = show.state.last_index(doc) {
actions.push(show.state.goto(doc, i));
}
}
_ => return false,
}
true
}
/// Pen, laser, blanking, zoom, the timer, note size and typed slide numbers. False when `k`
/// isn't one of them.
fn tool_key(show: &mut Show, k: egui::Key, m: egui::Modifiers, now: f64, presenter: bool) -> bool {
use egui::Key::*;
match k {
L if m.command => show.set_laser(!show.laser),
A if m.command => show.arrow(),
B | Period => toggle_blank(show, Color32::BLACK),
W | Comma => toggle_blank(show, Color32::WHITE),
E => show.strokes.clear(),
G if plain(m) => show.open_grid(presenter),
Z if plain(m) => show.zoom_key(),
T if plain(m) => show.timer.toggle(now),
R if plain(m) => show.timer.reset(now),
OpenBracket if plain(m) => show.nudge_notes(-NOTES_STEP),
CloseBracket if plain(m) => show.nudge_notes(NOTES_STEP),
Num0 | Num1 | Num2 | Num3 | Num4 | Num5 | Num6 | Num7 | Num8 | Num9 => {
if let Some(d) = k.name().chars().next() {
show.push_slide_number(d, now);
}
}
_ => return false,
}
true
}
/// Draw and advance the active show, handling hidden frames, timing, input, transitions, and
/// presenter/reading presentation.
pub fn ui(app: &mut SlideApp, ui: &mut Ui) {
+8 -2
View File
@@ -58,9 +58,15 @@
<Component Id="DeckcraftApp" Guid="D87EA444-41A4-4C19-922E-66D1A29461FF">
<File Id="DeckcraftNotice" Source="$(var.BinDir)\NOTICE" KeyPath="no" />
<File Id="DeckcraftExe" Source="$(var.BinDir)\deckcraft.exe" KeyPath="yes">
<!-- No Icon here, so the shortcut shows the icon embedded in deckcraft.exe (build.rs). An
MSI Icon is cached under %APPDATA%\Microsoft\Installer\{ProductCode}\, and every
update is a major upgrade with a new ProductCode that deletes the old folder. A
taskbar pin is a copy of this shortcut that the update never touches, so it kept
pointing at the deleted file and lost its icon (#1114). The exe's path survives
updates; the shortcut is not advertised, so it targets that path and its icon is the
exe's own. The Icon above stays for ARPPRODUCTICON (Apps and features). -->
<Shortcut Id="StartMenuShortcut" Directory="ProgramMenuFolder" Name="DeckCraft"
Description="DeckCraft presentations and slide shows" WorkingDirectory="INSTALLFOLDER" Advertise="yes"
Icon="DeckcraftIcon.ico" IconIndex="0" />
Description="DeckCraft presentations and slide shows" WorkingDirectory="INSTALLFOLDER" Advertise="no" />
</File>
<!-- .deckcraft: DeckCraft's own format. -->
+9
View File
@@ -83,6 +83,15 @@ foreach ($check in @(@('deckcraft.exe', 2), @('deckcraft-cli.exe', 3))) {
if ($h.Subsystem -ne $check[1]) { throw "$($check[0]) has PE subsystem $($h.Subsystem), expected $($check[1])" }
Write-Output "ok $($check[0]): $Arch, PE subsystem $($h.Subsystem)"
}
# The Start menu shortcut is not advertised and names no MSI icon, so it shows the icon embedded in
# deckcraft.exe (#1114). -SkipBuild takes whatever binary is already there, which may come from a
# build without DECKCRAFT_REQUIRE_WINRES=1 whose resource compilation failed: refuse an exe with no
# icon instead of shipping shortcuts with a generic one. ExtractIconEx with index -1 counts them.
Add-Type -Namespace DeckCraft -Name Shell -MemberDefinition '[DllImport("shell32.dll", CharSet = CharSet.Unicode)] public static extern uint ExtractIconEx(string file, int index, IntPtr[] large, IntPtr[] small, uint icons);'
$Icons = [DeckCraft.Shell]::ExtractIconEx((Join-Path $Bin 'deckcraft.exe'), -1, $null, $null, 0)
if ($Icons -eq 0) { throw 'deckcraft.exe has no embedded icon: rebuild it with DECKCRAFT_REQUIRE_WINRES=1 (omit -SkipBuild)' }
Write-Output "ok deckcraft.exe: $Icons embedded icon group(s)"
$Stage = Join-Path $TargetDir "windows-package\$Arch"
Remove-Item -Recurse -Force $Stage -ErrorAction SilentlyContinue
New-Item -ItemType Directory -Force -Path $Stage | Out-Null