Compare commits

...
11 Commits
Author SHA1 Message Date
Dylan M. TaylorandClaude Sonnet 5.5 51e8266b38 refactor(h264): split dpb functions (S3776)
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-24 08:49:52 +00:00
Dylan M. Taylor c955407373 security(engine): check regular file and size bound on read, preserve permissions and symlinks on save (#1265)
* security(engine): check regular file and size bound on read, preserve permissions and symlinks on save

* style: format recovery.rs write_atomic
2026-10-24 04:45:48 -04:00
Dylan M. TaylorandClaude Sonnet 5.5 54962c6389 refactor: explicit imports for table modules (SonarCloud S2208) (#1262)
* refactor: replace wildcard table imports with explicit names (SonarCloud S2208)

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

* style: rustfmt

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

---------

Co-authored-by: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-24 04:43:36 -04:00
Dylan M. Taylor 6189fa8bf6 perf(engine): bound HTML entity semicolon scan window and formatting stack search (#1267) 2026-10-24 04:41:43 -04:00
Dylan M. Taylor 713f605566 perf(chart): optimize axis plotted extent scan and cache non-bar error bar stats (#1266) 2026-10-24 04:41:23 -04:00
Dylan M. Taylor 9641817a15 security: clamp alpha_num bullet expansion and sanitize filename logging (#1264) 2026-10-24 04:40:29 -04:00
Dylan M. Taylor 1e097e3edd security: use exclusive temporary directories for macOS cert import and media fixtures (#1263) 2026-10-24 04:40:26 -04:00
Dylan M. TaylorandClaude Sonnet 5.5 d94790d97a refactor: replace redundant closures with method paths (SonarCloud S1612) (#1261)
Skipped where the receiver type is generic or not evident without a compiler (wrapping_neg, total_cmp, exists, to_vec, as_ref, split_first, Option::into_inner).

Co-authored-by: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-24 04:27:07 -04:00
5fadda5ecf chore: resolve SonarCloud maintainability issues (#1255)
* chore: resolve SonarCloud maintainability issues

* fix: address shellcheck SC2059 and fix generic cast ambiguity

* style: apply rustfmt formatting

* fix: restore compile and behaviour in Sonar refactors

- fonts: char::is_alphanumeric takes &self, so filter needs a closure
- h264: keep the mvd accumulator as i32 (prefix is usize)
- model: annotate the non-stacked range accumulators as f64
- engine: chart.options still clears the raw chart XML for upDownBars
  and quartiles; smartart goes back to the glob import (cmd! macro)
- web: keep the original viewport meta (behaviour change)

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

* Assign positional parameters to locals in sha256 verification

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Dylan Taylor <dylan@craftmaker.dev>
2026-10-23 23:28:09 -04:00
aiongg b913c4f9d0 PDF: pin craft-pdf and krilla to craft-libs main (#1253)
* PDF: pin craft-pdf and krilla to craft-libs main (PNG decode bound)

* PDF: pin craft-pdf and krilla to craft-libs at 9f75d3dc (#254)
2026-10-23 22:37:28 -04:00
aiongg 9aef21e627 Media: a clip opened from a .pptx gets its length and size from its headers (#1241) (#1250) 2026-10-23 19:56:24 -04:00
87 changed files with 993 additions and 592 deletions
Generated
+2 -2
View File
@@ -816,7 +816,7 @@ dependencies = [
[[package]]
name = "craft-pdf"
version = "0.1.0"
source = "git+https://github.com/storytold/craft-libs?rev=c3487320b2d1de657b860b3c8abb44e1f2b32bd8#c3487320b2d1de657b860b3c8abb44e1f2b32bd8"
source = "git+https://github.com/storytold/craft-libs?rev=9f75d3dc19b4b514c4f3d4847ed1ec1b0e86b4ef#9f75d3dc19b4b514c4f3d4847ed1ec1b0e86b4ef"
dependencies = [
"krilla",
"thiserror 2.0.21",
@@ -2429,7 +2429,7 @@ dependencies = [
[[package]]
name = "krilla"
version = "0.8.2"
source = "git+https://github.com/storytold/craft-libs?rev=c3487320b2d1de657b860b3c8abb44e1f2b32bd8#c3487320b2d1de657b860b3c8abb44e1f2b32bd8"
source = "git+https://github.com/storytold/craft-libs?rev=9f75d3dc19b4b514c4f3d4847ed1ec1b0e86b4ef#9f75d3dc19b4b514c4f3d4847ed1ec1b0e86b4ef"
dependencies = [
"base64",
"bumpalo",
+2 -2
View File
@@ -60,7 +60,7 @@ vello_cpu = "0.2"
vello_common = { version = "0.2", default-features = false }
image = { version = "0.25", default-features = false, features = ["png", "jpeg", "webp", "gif", "tiff", "bmp"] }
craft-media = { git = "https://github.com/storytold/craft-libs", rev = "f0ad6c1e1d297908915f4be315b0fcfc9be7b5d4", version = "=0.1.0", default-features = false }
craft-pdf = { git = "https://github.com/storytold/craft-libs", rev = "c3487320b2d1de657b860b3c8abb44e1f2b32bd8", version = "=0.1.0", default-features = false, features = ["write"] }
craft-pdf = { git = "https://github.com/storytold/craft-libs", rev = "9f75d3dc19b4b514c4f3d4847ed1ec1b0e86b4ef", version = "=0.1.0", default-features = false, features = ["write", "write-simple-text"] }
i_overlay = "9"
quick-xml = "0.41"
zip = { version = "3", default-features = false, features = ["deflate"] }
@@ -103,4 +103,4 @@ codegen-units = 16
debug = 0
[patch.crates-io]
krilla = { git = "https://github.com/storytold/craft-libs", rev = "c3487320b2d1de657b860b3c8abb44e1f2b32bd8", version = "=0.8.2" }
krilla = { git = "https://github.com/storytold/craft-libs", rev = "9f75d3dc19b4b514c4f3d4847ed1ec1b0e86b4ef", version = "=0.8.2" }
+1 -1
View File
@@ -407,7 +407,7 @@ fn frame_paths(out: &str, ts: &[f64]) -> Result<Vec<String>, String> {
return Ok(vec![out.to_string()]);
}
let p = std::path::Path::new(out);
let stem = p.file_stem().and_then(|x| x.to_str()).unwrap_or("frame");
let stem = p.file_stem().and_then(std::ffi::OsStr::to_str).unwrap_or("frame");
let mut paths: Vec<String> = Vec::with_capacity(ts.len());
for t in ts {
let path = p.with_file_name(format!("{stem}-t{t:.3}.png")).to_string_lossy().into_owned();
+3 -2
View File
@@ -41,8 +41,9 @@ pub fn poll(inbox: &Inbox, app: &mut SlideApp, ctx: &egui::Context) {
Event::Open(paths) => {
for p in paths.iter().filter_map(|p| p.to_str()) {
if let Err(e) = app.open_path(p) {
log::error!("{p}: {e}");
app.set_status(format!("Couldn't open {p}: {e}"));
let safe_p = p.replace(['\r', '\n'], " ");
log::error!("{safe_p}: {e}");
app.set_status(format!("Couldn't open {safe_p}: {e}"));
}
}
}
+1 -1
View File
@@ -49,7 +49,7 @@ fn tokenize(s: &str) -> Vec<Tok> {
n1.is_ascii_digit() || ((n1 == '-' || n1 == '+') && n2.is_ascii_digit())
} {
i += 2;
while cs.get(i).is_some_and(|d| d.is_ascii_digit()) {
while cs.get(i).is_some_and(char::is_ascii_digit) {
i += 1;
}
break;
+4 -1
View File
@@ -9,7 +9,10 @@ use std::f64::consts::PI;
use crate::easing::ease;
use crate::transition::{FULL, Layer, R, Source};
use crate::transition_geom::*;
use crate::transition_geom::{
Cam, V3, boundary, card, center, clip, dir, edge, ein, eout, facing_shade, fan, field, hash64, moved, offset, poly_layers, pow_inout, q2,
rect_poly, rnd, rot, rot_quad, scale_rect, scaled, soft_rect, ss, v3, win,
};
use Source::{Black, New, NewBack, NewContent, Old, OldBack, OldContent, White};
+3 -3
View File
@@ -1,7 +1,7 @@
//! CDEF (7.15).
use crate::frame::{FrameBuf, Plane};
use crate::spec_tables::*;
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
@@ -242,14 +242,14 @@ fn filter_rows<const W: usize>(
}
for i in 0..h {
let ci = ((i + 2) * PAD_STRIDE + 2) as isize;
let Some(&x) = b16.get(ci as usize..).and_then(|r| r.first_chunk::<W>()) else {
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(|r| r.first_chunk::<W>()) else {
let Some(p) = b16.get(start..).and_then(<[_]>::first_chunk::<W>) else {
return;
};
for j in 0..W {
+21 -1
View File
@@ -1,6 +1,26 @@
//! The adaptive CDF arrays (spec 6.8.2 init_non_coeff_cdfs / init_coeff_cdfs, load_cdfs).
use crate::spec_tables::*;
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.
+8 -4
View File
@@ -17,7 +17,11 @@ use crate::cdf::Cdfs;
use crate::frame::FrameBuf;
use crate::header::{FilmGrainParams, FrameHeader, HeaderState, LoopFilterDeltas, RefInfo, SegmentationFeatures, SequenceHeader, default_gm_params};
use crate::par::Pool;
use crate::spec_tables::*;
use crate::spec_tables::{
ALTREF_FRAME, ALTREF2_FRAME, BWDREF_FRAME, DEFAULT_INTRA_FRAME_Y_MODE_CDF, DIV_MULT, GOLDEN_FRAME, INTRA_FRAME, INTRA_ONLY_FRAME, KEY_FRAME,
LAST_FRAME, LAST2_FRAME, MAX_FRAME_DISTANCE, MAX_OFFSET_HEIGHT, MAX_OFFSET_WIDTH, MFMV_STACK_SIZE, NUM_REF_FRAMES, PRIMARY_REF_NONE,
REFMVS_LIMIT, REFS_PER_FRAME,
};
use crate::state::{FrameShared, FrameState, TileState};
use crate::stats::{DecodeStats, Stage, Timer};
use crate::tile::TileDecoder;
@@ -61,7 +65,7 @@ impl<T: Clone> Slot<T> {
}
fn set(&self, v: Result<T>) {
let mut g = self.value.lock().unwrap_or_else(|e| e.into_inner());
let mut g = self.value.lock().unwrap_or_else(std::sync::PoisonError::into_inner);
*g = Some(v);
self.cv.notify_all();
}
@@ -71,12 +75,12 @@ impl<T: Clone> Slot<T> {
}
fn wait(&self) -> Result<T> {
let mut g = self.value.lock().unwrap_or_else(|e| e.into_inner());
let mut g = self.value.lock().unwrap_or_else(std::sync::PoisonError::into_inner);
loop {
if let Some(v) = g.as_ref() {
return v.clone();
}
g = self.cv.wait(g).unwrap_or_else(|e| e.into_inner());
g = self.cv.wait(g).unwrap_or_else(std::sync::PoisonError::into_inner);
}
}
}
+3 -3
View File
@@ -16,7 +16,7 @@ const POOL_MAX_SAMPLES: usize = 96 << 20;
fn zeroed(len: usize) -> Vec<u16> {
if len >= POOL_MIN_LEN {
let found = {
let mut pool = PLANE_POOL.lock().unwrap_or_else(|e| e.into_inner());
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 {
@@ -52,8 +52,8 @@ 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(|e| e.into_inner());
let kept: usize = pool.iter().map(|b| b.capacity()).sum();
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);
}
+8 -1
View File
@@ -1,7 +1,14 @@
//! Sequence header (5.5) and uncompressed frame header (5.9) parsing.
use crate::bits::BitReader;
use crate::spec_tables::*;
use crate::spec_tables::{
AFFINE, ALTREF_FRAME, ALTREF2_FRAME, BWDREF_FRAME, GM_ABS_ALPHA_BITS, GM_ABS_TRANS_BITS, GM_ABS_TRANS_ONLY_BITS, GM_ALPHA_PREC_BITS,
GM_TRANS_ONLY_PREC_BITS, GM_TRANS_PREC_BITS, GOLDEN_FRAME, IDENTITY, INTER_FRAME, INTRA_ONLY_FRAME, KEY_FRAME, LAST_FRAME, MAX_SEGMENTS,
MAX_TILE_AREA, MAX_TILE_COLS, MAX_TILE_ROWS, MAX_TILE_WIDTH, NUM_REF_FRAMES, ONLY_4X4, PRIMARY_REF_NONE, REF_FRAME_LIST, REFS_PER_FRAME,
RESTORATION_TILESIZE_MAX, RESTORE_NONE, RESTORE_SGRPROJ, RESTORE_SWITCHABLE, RESTORE_WIENER, ROTZOOM, SEG_LVL_ALT_Q, SEG_LVL_MAX,
SEG_LVL_REF_FRAME, SELECT_INTEGER_MV, SELECT_SCREEN_CONTENT_TOOLS, SUPERRES_DENOM_BITS, SUPERRES_DENOM_MIN, SUPERRES_NUM, SWITCH_FRAME,
TOTAL_REFS_PER_FRAME, TRANSLATION, TX_MODE_LARGEST, TX_MODE_SELECT, WARPEDMODEL_PREC_BITS,
};
use crate::{Error, Result};
pub const EIGHTTAP: u8 = 0;
+12 -3
View File
@@ -5,7 +5,16 @@ use std::sync::OnceLock;
use crate::frame::Plane;
use crate::mvpred::round2_signed64;
use crate::spec_tables::*;
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;
@@ -678,7 +687,7 @@ fn hpass<const W: usize>(src: &[u16], hf: &[i16; 8], out: &mut [i32], half: i32,
let mut acc = [half; W];
for t in 0..8 {
let f = hf[t] as i32;
let Some(p) = src.get(t..).and_then(|s| s.first_chunk::<W>()) else {
let Some(p) = src.get(t..).and_then(<[_]>::first_chunk::<W>) else {
return;
};
for j in 0..W {
@@ -699,7 +708,7 @@ fn vpass<const W: usize>(rows: &[i32], vf: &[i16; 8], out: &mut [i32], half: i32
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(|s| s.first_chunk::<W>()) else {
let Some(p) = rows.get(t * W..).and_then(<[_]>::first_chunk::<W>) else {
return;
};
for j in 0..W {
+8 -1
View File
@@ -3,7 +3,14 @@
use crate::Result;
use crate::header::{EIGHTTAP, SWITCHABLE};
use crate::mvpred::MvP;
use crate::spec_tables::*;
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 {
+5 -1
View File
@@ -1,7 +1,11 @@
//! 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::*;
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.
+4 -1
View File
@@ -1,7 +1,10 @@
//! Motion vector prediction processes (spec 7.10): find_mv_stack, has_overlappable_candidates
//! and find_warp_samples.
use crate::spec_tables::*;
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];
+4 -1
View File
@@ -1,7 +1,10 @@
//! 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::*;
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};
+5 -1
View File
@@ -1,7 +1,11 @@
//! Super-resolution upscaling (7.16) and loop restoration (7.17).
use crate::frame::{FrameBuf, Plane};
use crate::spec_tables::*;
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).
+1 -1
View File
@@ -9,7 +9,7 @@
use crate::decoder::RefFrame;
use crate::frame::{FrameBuf, MiInfo};
use crate::header::{FrameHeader, RefInfo, SequenceHeader};
use crate::spec_tables::*;
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;
+23 -5
View File
@@ -6,7 +6,27 @@ use crate::cdf::Cdfs;
use crate::frame::{MiInfo, Mv};
use crate::header::{FrameHeader, NONE, SequenceHeader};
use crate::intra::{IntraParams, is_directional_mode, predict_intra, round2_signed};
use crate::spec_tables::*;
use crate::spec_tables::{
AC_QLOOKUP, ADJUSTED_TX_SIZE, ADST_FLIPADST, BLOCK_4X4, BLOCK_8X8, BLOCK_64X64, BLOCK_128X128, BR_CDF_SIZE, CFL_SIGN_NEG, CFL_SIGN_ZERO,
COEFF_BASE_CTX_OFFSET, COEFF_BASE_POS_CTX_OFFSET, COEFF_BASE_RANGE, COMPOUND_AVERAGE, DC_PRED, DC_QLOOKUP, DCT_DCT, DCT_FLIPADST,
DEFAULT_SCAN_4X4, DEFAULT_SCAN_4X8, DEFAULT_SCAN_4X16, DEFAULT_SCAN_8X4, DEFAULT_SCAN_8X8, DEFAULT_SCAN_8X16, DEFAULT_SCAN_8X32,
DEFAULT_SCAN_16X4, DEFAULT_SCAN_16X8, DEFAULT_SCAN_16X16, DEFAULT_SCAN_16X32, DEFAULT_SCAN_32X8, DEFAULT_SCAN_32X16, DEFAULT_SCAN_32X32,
DELTA_LF_SMALL, DELTA_Q_SMALL, FILTER_INTRA_MODE_TO_INTRA_DIR, FLIPADST_ADST, FLIPADST_DCT, FLIPADST_FLIPADST, FRAME_LF_COUNT, H_ADST, H_DCT,
H_FLIPADST, H_PRED, IDTX, II_DC_PRED, II_H_PRED, II_V_PRED, INTRA_FRAME, INTRA_MODE_CONTEXT, MAG_REF_OFFSET_WITH_TX_CLASS, MAX_ANGLE_DELTA,
MAX_LOOP_FILTER, MAX_TX_DEPTH_TABLE, MAX_TX_SIZE_RECT, MAX_VARTX_DEPTH, MCOL_SCAN_4X4, MCOL_SCAN_4X8, MCOL_SCAN_4X16, MCOL_SCAN_8X4,
MCOL_SCAN_8X8, MCOL_SCAN_8X16, MCOL_SCAN_16X4, MCOL_SCAN_16X8, MCOL_SCAN_16X16, MI_HEIGHT_LOG2, MI_WIDTH_LOG2, MODE_TO_TXFM, MROW_SCAN_4X4,
MROW_SCAN_4X8, MROW_SCAN_4X16, MROW_SCAN_8X4, MROW_SCAN_8X8, MROW_SCAN_8X16, MROW_SCAN_16X4, MROW_SCAN_16X8, MROW_SCAN_16X16,
NUM_4X4_BLOCKS_HIGH, NUM_4X4_BLOCKS_WIDE, NUM_BASE_LEVELS, PALETTE_COLOR_CONTEXT, PALETTE_COLOR_HASH_MULTIPLIERS, PALETTE_COLORS,
PALETTE_NUM_NEIGHBORS, PARTITION_HORZ, PARTITION_HORZ_4, PARTITION_HORZ_A, PARTITION_HORZ_B, PARTITION_NONE, PARTITION_SPLIT, PARTITION_SUBSIZE,
PARTITION_VERT, PARTITION_VERT_4, PARTITION_VERT_A, PARTITION_VERT_B, QM_OFFSET, QUANTIZER_MATRIX, RESTORE_NONE, RESTORE_SGRPROJ, RESTORE_WIENER,
SEG_LVL_SKIP, SGR_PARAMS, SGRPROJ_PARAMS_BITS, SGRPROJ_PRJ_BITS, SGRPROJ_PRJ_SUBEXP_K, SIG_REF_DIFF_OFFSET, SIG_REF_DIFF_OFFSET_NUM, SIMPLE,
SMOOTH_H_PRED, SMOOTH_PRED, SMOOTH_V_PRED, SPLIT_TX_SIZE, SUPERRES_NUM, TX_4X4, TX_4X8, TX_4X16, TX_8X4, TX_8X8, TX_8X16, TX_8X32, TX_16X4,
TX_16X8, TX_16X16, TX_16X32, TX_16X64, TX_32X8, TX_32X16, TX_32X32, TX_32X64, TX_64X16, TX_64X32, TX_64X64, TX_CLASS_2D, TX_CLASS_HORIZ,
TX_CLASS_VERT, TX_HEIGHT, TX_HEIGHT_LOG2, TX_MODE_SELECT, TX_SET_DCTONLY, TX_SET_INTER_1, TX_SET_INTER_2, TX_SET_INTER_3, TX_SET_INTRA_1,
TX_SET_INTRA_2, TX_SIZE_SQR, TX_SIZE_SQR_UP, TX_SIZES, TX_SIZES_ALL, TX_TYPE_IN_SET_INTER, TX_TYPE_IN_SET_INTRA, TX_TYPE_INTER_INV_SET1,
TX_TYPE_INTER_INV_SET2, TX_TYPE_INTER_INV_SET3, TX_TYPE_INTRA_INV_SET1, TX_TYPE_INTRA_INV_SET2, TX_WIDTH, TX_WIDTH_LOG2, UV_CFL_PRED, V_ADST,
V_DCT, V_FLIPADST, V_PRED,
};
use crate::state::{FrameShared, TileState};
use crate::symbol::SymbolDecoder;
use crate::transform::inverse_transform_2d;
@@ -2222,18 +2242,16 @@ fn neg_deinterleave(diff: i32, r: i32, max: i32) -> i32 {
if diff <= 2 * r {
if diff & 1 == 1 {
return r + ((diff + 1) >> 1);
} else {
return r - (diff >> 1);
}
return r - (diff >> 1);
}
diff
} else {
if diff <= 2 * (max - r - 1) {
if diff & 1 == 1 {
return r + ((diff + 1) >> 1);
} else {
return r - (diff >> 1);
}
return r - (diff >> 1);
}
max - (diff + 1)
}
+4 -1
View File
@@ -1,6 +1,9 @@
//! Inverse transforms (spec 7.13), implemented step by step as specified.
use crate::spec_tables::*;
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 {
+42 -1
View File
@@ -1,6 +1,10 @@
//! File menu: new, open, save, export, close, properties.
use deckcraft_model::{Presentation, defaults};
use std::collections::HashMap;
use std::sync::Arc;
use deckcraft_media::MediaInfo;
use deckcraft_model::{MediaId, MediaItem, Presentation, Shape, ShapeKind, defaults};
use serde_json::{Value, json};
use super::*;
@@ -156,6 +160,7 @@ fn open_other(name: &str, bytes: &[u8]) -> Result<(Presentation, Vec<String>)> {
if deckcraft_pptx::sniff(bytes) {
let (mut p, lost_slides) = deckcraft_pptx::import_checked(bytes).map_err(|e| EngineError::Other(e.to_string()))?;
deckcraft_format::repair(&mut p);
probe_clips(&mut p);
return Ok((p, lost_slides));
}
if deckcraft_pptx::is_encrypted(bytes) {
@@ -178,6 +183,42 @@ fn open_other(name: &str, bytes: &[u8]) -> Result<(Presentation, Vec<String>)> {
Err(EngineError::Other(format!("{name}: not a presentation DeckCraft can read")))
}
/// Fill in each audio and video clip's length and picture size from its bytes: a `.pptx` doesn't
/// store them. Headers only, so opening never decodes a whole audio file.
fn probe_clips(p: &mut Presentation) {
fn rec(shapes: &mut [Shape], media: &[MediaItem], probed: &mut HashMap<MediaId, Option<MediaInfo>>, depth: usize) {
if depth > 64 {
return;
}
for s in shapes {
if let ShapeKind::Media(m) = &mut s.kind
&& (m.duration_ms == 0 || m.width == 0 && m.height == 0)
&& let Some(info) = probed
.entry(m.media)
.or_insert_with(|| media.iter().find(|i| i.id == m.media).and_then(|i| deckcraft_media::probe_headers(&i.data).ok()))
{
if m.duration_ms == 0 {
m.duration_ms = info.duration_ms.min(u32::MAX as u64) as u32;
}
if m.width == 0
&& m.height == 0
&& let Some(v) = &info.video
{
(m.width, m.height) = (v.width, v.height);
}
}
if let Some(children) = s.children_mut() {
rec(children, media, probed, depth + 1);
}
}
}
let mut probed = HashMap::new();
// Just read, so no slide is shared yet and `make_mut` doesn't copy.
for slide in &mut p.slides {
rec(&mut Arc::make_mut(slide).shapes, &p.media, &mut probed, 0);
}
}
pub fn add_opened(s: &mut Session, name: &str, path: Option<String>, p: Presentation) -> usize {
let title = std::path::Path::new(name).file_stem().map(|n| n.to_string_lossy().to_string()).unwrap_or_else(|| name.to_string());
crate::fonts::install_embedded_fonts(&p);
+37 -29
View File
@@ -577,7 +577,7 @@ fn commit_album(s: &mut Session, items: Vec<AlbumItem>, opts: AlbumOpts, replace
}
fn album_norm(s: &str) -> String {
s.chars().filter(|c| c.is_ascii_alphanumeric()).collect::<String>().to_ascii_lowercase()
s.chars().filter(char::is_ascii_alphanumeric).collect::<String>().to_ascii_lowercase()
}
fn album_layout(p: &Value) -> Result<AlbumLayout> {
@@ -770,7 +770,7 @@ fn image_px(bytes: &[u8]) -> (f64, f64) {
}
fn file_stem(name: &str) -> String {
let base = std::path::Path::new(name).file_name().and_then(|n| n.to_str()).unwrap_or(name);
let base = std::path::Path::new(name).file_name().and_then(std::ffi::OsStr::to_str).unwrap_or(name);
let stem = match base.rsplit_once('.') {
Some((s, ext)) if !s.is_empty() && !ext.is_empty() => s,
_ => base,
@@ -1761,6 +1761,39 @@ fn chart_data(s: &mut Session, p: &Value) -> Result<Value> {
})
}
fn apply_bin_options(c: &mut deckcraft_model::chart::Chart, bin_count: Option<Option<u32>>, bin_size: Option<Option<f64>>) {
if let Some(n) = bin_count {
c.bin_count = n;
if n.is_some() {
c.bin_size = None;
}
}
if let Some(w) = bin_size {
c.bin_size = w;
if w.is_some() {
c.bin_count = None;
}
}
}
fn apply_subtotals_and_bars(c: &mut deckcraft_model::chart::Chart, p: &Value, subtotals: Option<&[usize]>) {
if let Some(t) = subtotals {
let n = c.series.first().map_or(0, |s| s.values.len());
let mut t: Vec<usize> = t.iter().copied().filter(|i| *i < n).collect();
t.sort_unstable();
t.dedup();
c.subtotals = t;
}
if let Some(b) = p.get("upDownBars").and_then(Value::as_bool) {
c.up_down_bars = b;
c.raw = None;
}
if let Some(q) = p.get("quartiles").and_then(Value::as_str) {
c.inclusive_quartiles = q.eq_ignore_ascii_case("inclusive");
c.raw = None;
}
}
fn chart_options(s: &mut Session, p: &Value) -> Result<Value> {
// Histogram bins: a count or a width (setting one clears the other; null for automatic).
let bin_count = p.get("binCount").map(|v| v.as_u64().map(|n| n.clamp(1, deckcraft_model::chart::MAX_BINS as u64) as u32));
@@ -1772,33 +1805,8 @@ fn chart_options(s: &mut Session, p: &Value) -> Result<Value> {
let subtotals: Option<Vec<usize>> =
p.get("subtotals").and_then(Value::as_array).map(|a| a.iter().filter_map(Value::as_u64).map(|i| i as usize).collect());
with_chart(s, p, "chart.options", |c| {
if let Some(n) = bin_count {
c.bin_count = n;
if n.is_some() {
c.bin_size = None;
}
}
if let Some(w) = bin_size {
c.bin_size = w;
if w.is_some() {
c.bin_count = None;
}
}
if let Some(t) = &subtotals {
let n = c.series.first().map_or(0, |s| s.values.len());
let mut t: Vec<usize> = t.iter().copied().filter(|i| *i < n).collect();
t.sort_unstable();
t.dedup();
c.subtotals = t;
}
if let Some(b) = p.get("upDownBars").and_then(Value::as_bool) {
c.up_down_bars = b;
c.raw = None;
}
if let Some(q) = p.get("quartiles").and_then(Value::as_str) {
c.inclusive_quartiles = q.eq_ignore_ascii_case("inclusive");
c.raw = None;
}
apply_bin_options(c, bin_count, bin_size);
apply_subtotals_and_bars(c, p, subtotals.as_deref());
if bin_count.is_some() || bin_size.is_some() || subtotals.is_some() {
c.raw = None;
}
+15
View File
@@ -228,6 +228,21 @@ mod tests {
assert!(s.execute("media.posterFrame", &json!({})).is_err(), "audio has no poster frame");
}
#[test]
fn a_video_reopened_from_pptx_keeps_its_length_and_size() {
let mut s = Session::with_new();
let clip = include_bytes!("../../../media/tests/fixtures/clip-h264.mp4");
s.execute("insert.video", &json!({"name": "clip.mp4", "data": base64_encode(clip)})).expect("insert");
let before = s.execute("media.info", &json!({})).expect("info");
assert!(before["durationMs"].as_u64().is_some_and(|ms| ms > 0));
let saved = s.execute("file.saveBytes", &json!({"format": "pptx"})).expect("save");
s.execute("file.openBytes", &json!({"name": "deck.pptx", "data": saved["data"]})).expect("open");
let after = s.execute("media.info", &json!({})).expect("info");
for key in ["durationMs", "width", "height"] {
assert_eq!(after[key], before[key], "{key}");
}
}
#[test]
fn media_commands_need_media() {
let mut s = Session::with_new();
+20 -12
View File
@@ -107,14 +107,16 @@ pub fn html_to_body(html: &str) -> Option<TextBody> {
}
if closing {
let kind = tag_kind(&name);
if !kind.is_empty()
&& let Some(pos) = stack.iter().rposition(|e| e.0 == kind)
{
let (_, db, di, du) = stack[pos];
bold -= db;
italic -= di;
underline -= du;
stack.truncate(pos);
if !kind.is_empty() {
let search_start = stack.len().saturating_sub(64);
if let Some(offset) = stack[search_start..].iter().rposition(|e| e.0 == kind) {
let pos = search_start + offset;
let (_, db, di, du) = stack[pos];
bold -= db;
italic -= di;
underline -= du;
stack.truncate(pos);
}
}
if is_block(&name) {
new_para(&mut paragraphs);
@@ -130,7 +132,7 @@ pub fn html_to_body(html: &str) -> Option<TextBody> {
bold += db;
italic += di;
underline += du;
if !token.ends_with('/') {
if !token.ends_with('/') && stack.len() < 128 {
stack.push((tag_kind(&name), db, di, du));
} else {
bold -= db;
@@ -302,9 +304,11 @@ fn decode_text(raw: &str) -> String {
while let Some(at) = rest.find('&') {
out.push_str(&collapse_space(&rest[..at]));
rest = &rest[at..];
let Some(end) = rest.find(';') else {
out.push_str(&collapse_space(rest));
break;
let search_window = rest.len().min(32);
let Some(end) = rest[..search_window].find(';') else {
out.push('&');
rest = &rest[1..];
continue;
};
let entity = &rest[1..end];
let decoded = match entity {
@@ -391,6 +395,10 @@ mod tests {
assert_eq!(text("<p>R&D costs; up</p>"), "R&D costs; up");
assert_eq!(text("<p>AT&T; Q&A</p>"), "AT&T; Q&A");
assert_eq!(text("<p>A &amp; B &#38; C &#x26; D</p>"), "A & B & C & D");
// Verify many bare ampersands do not cause quadratic scan freeze
let hostile = format!("<p>{}</p>", "&".repeat(2000));
assert_eq!(text(&hostile), "&".repeat(2000));
}
#[test]
+2 -2
View File
@@ -539,9 +539,9 @@ fn section_add(s: &mut Session, p: &Value) -> Result<Value> {
if doc.sections.is_empty() {
// The slides before `at` go into a "Default Section".
if at > 0 {
doc.sections.push(Section { name: "Default Section".into(), slides: ids.get(..at).map(|x| x.to_vec()).unwrap_or_default() });
doc.sections.push(Section { name: "Default Section".into(), slides: ids.get(..at).map(<[_]>::to_vec).unwrap_or_default() });
}
doc.sections.push(Section { name, slides: ids.get(at..).map(|x| x.to_vec()).unwrap_or_default() });
doc.sections.push(Section { name, slides: ids.get(at..).map(<[_]>::to_vec).unwrap_or_default() });
} else {
let Some(start) = ids.get(at).copied() else { return Err(bad("section.add", "no slide")) };
let si = doc.section_of(start).unwrap_or(0);
+1 -1
View File
@@ -13,7 +13,7 @@ use crate::{EngineError, Result, Session};
pub fn panic_message(payload: &(dyn std::any::Any + Send)) -> String {
payload
.downcast_ref::<&str>()
.map(|s| s.to_string())
.map(ToString::to_string)
.or_else(|| payload.downcast_ref::<String>().cloned())
.unwrap_or_else(|| "unknown panic".into())
}
+1 -1
View File
@@ -60,7 +60,7 @@ pub fn stroke_width(icon: &Icon, w: f64, h: f64) -> f64 {
}
fn parsed() -> &'static HashMap<&'static str, BezPath> {
static CACHE: OnceLock<HashMap<&'static str, BezPath>> = OnceLock::new();
static CACHE: OnceLock<HashMap<&str, BezPath>> = OnceLock::new();
CACHE.get_or_init(|| {
let mut m = HashMap::new();
for icon in ICONS {
+59 -5
View File
@@ -8,6 +8,14 @@
#[cfg(not(target_arch = "wasm32"))]
pub fn read_file(path: &str) -> Result<Vec<u8>, String> {
crate::file_access::check_read(path)?;
let meta = std::fs::metadata(path).map_err(|e| format!("{path}: {e}"))?;
if !meta.is_file() {
return Err(format!("{path}: not a regular file"));
}
const MAX_FILE_SIZE: u64 = 2 << 30; // 2 GiB
if meta.len() > MAX_FILE_SIZE {
return Err(format!("{path}: file size {} exceeds 2 GiB limit", meta.len()));
}
std::fs::read(path).map_err(|e| format!("{path}: {e}"))
}
@@ -28,21 +36,29 @@ pub fn write_file(path: &str, bytes: &[u8]) -> Result<(), String> {
static COUNTER: AtomicU64 = AtomicU64::new(0);
crate::file_access::check_write(path)?;
let err = |e: std::io::Error| format!("{path}: {e}");
// A uniquely named temporary sibling, created exclusively (never following or reusing an
// existing file or symlink), is renamed over the destination: concurrent saves never share
// temporary bytes.
let target_path = match std::fs::canonicalize(path) {
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 {
let tmp = format!("{path}.{}.{}.saving", std::process::id(), COUNTER.fetch_add(1, Ordering::Relaxed));
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,
Err(e) => return Err(err(e)),
}
};
if let Some(perms) = existing_perms {
let _ = file.set_permissions(perms);
}
let written = file.write_all(bytes).and_then(|()| file.sync_all());
drop(file);
let result = written.and_then(|()| std::fs::rename(&tmp, path));
let result = written.and_then(|()| std::fs::rename(&tmp, &target_path));
if let Err(e) = result {
let _ = std::fs::remove_file(&tmp);
return Err(err(e));
@@ -67,3 +83,41 @@ pub fn file_len(path: &str) -> Option<u64> {
pub fn file_len(_path: &str) -> Option<u64> {
None
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn read_file_rejects_directory() {
let dir = std::env::temp_dir();
let res = read_file(dir.to_str().unwrap());
assert!(res.is_err());
}
#[test]
fn write_file_preserves_permissions_and_writes_content() {
let tmp_dir = std::env::temp_dir().join("deckcraft_io_test");
let _ = std::fs::create_dir_all(&tmp_dir);
let test_file = tmp_dir.join("test_write.txt");
let path_str = test_file.to_str().unwrap();
assert!(write_file(path_str, b"hello world").is_ok());
let read = read_file(path_str).unwrap();
assert_eq!(read, b"hello world");
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt;
let perms = std::fs::Permissions::from_mode(0o640);
std::fs::set_permissions(&test_file, perms).unwrap();
assert!(write_file(path_str, b"updated world").is_ok());
let new_meta = std::fs::metadata(&test_file).unwrap();
assert_eq!(new_meta.permissions().mode() & 0o777, 0o640);
}
let _ = std::fs::remove_file(&test_file);
let _ = std::fs::remove_dir(&tmp_dir);
}
}
+15 -3
View File
@@ -85,8 +85,20 @@ fn owner_alive(dir: &Path, owner: u64) -> bool {
/// Write `bytes` to `path` through a temporary file and a rename, so a crash never leaves it torn.
fn write_atomic(path: &Path, bytes: &[u8]) -> Result<()> {
use std::io::Write;
let tmp = path.with_extension(format!("{}.tmp", path.extension().and_then(|e| e.to_str()).unwrap_or("")));
std::fs::write(&tmp, bytes).and_then(|_| std::fs::rename(&tmp, path)).map_err(|e| EngineError::Other(format!("{}: {e}", path.display())))
let mut opts = std::fs::OpenOptions::new();
opts.write(true).create(true).truncate(true);
#[cfg(unix)]
{
use std::os::unix::fs::OpenOptionsExt;
opts.mode(0o600);
}
let mut file = opts.open(&tmp).map_err(|e| EngineError::Other(format!("{}: {e}", tmp.display())))?;
file.write_all(bytes).and_then(|()| file.sync_all()).and_then(|()| std::fs::rename(&tmp, path)).map_err(|e| {
let _ = std::fs::remove_file(&tmp);
EngineError::Other(format!("{}: {e}", path.display()))
})
}
/// Presentations reopened per start; the rest stay on disk for the next one, so a folder
@@ -152,13 +164,13 @@ pub fn list(dir: &Path) -> Vec<(u64, Value)> {
let Ok(rd) = std::fs::read_dir(dir) else { return out };
for e in rd.flatten() {
let p = e.path();
if p.extension().and_then(|x| x.to_str()) != Some("deckcraft") {
if p.extension().and_then(std::ffi::OsStr::to_str) != Some("deckcraft") {
continue;
}
if !std::fs::metadata(&p).is_ok_and(|m| m.is_file()) {
continue;
}
let Some(uid) = p.file_stem().and_then(|s| s.to_str()).and_then(|s| s.parse::<u64>().ok()) else { continue };
let Some(uid) = p.file_stem().and_then(std::ffi::OsStr::to_str).and_then(|s| s.parse::<u64>().ok()) else { continue };
// Metadata is a file on disk: anything but an object (corrupt, hand-edited) counts as none.
let meta = read_regular(&p.with_extension("json"), MAX_META_BYTES)
.and_then(|t| serde_json::from_slice::<Value>(&t).ok())
+28 -27
View File
@@ -161,12 +161,12 @@ pub struct FaceRef(&'static FontFace);
impl FaceRef {
/// The handle for a loaded face (memoised per face).
pub fn of(face: &Arc<FontFace>) -> FaceRef {
static LEAKED: RwLock<Vec<Option<&'static FontFace>>> = RwLock::new(Vec::new());
static LEAKED: RwLock<Vec<Option<&FontFace>>> = RwLock::new(Vec::new());
let id = face.id as usize;
if let Some(Some(f)) = LEAKED.read().unwrap_or_else(|e| e.into_inner()).get(id) {
if let Some(Some(f)) = LEAKED.read().unwrap_or_else(std::sync::PoisonError::into_inner).get(id) {
return FaceRef(f);
}
let mut w = LEAKED.write().unwrap_or_else(|e| e.into_inner());
let mut w = LEAKED.write().unwrap_or_else(std::sync::PoisonError::into_inner);
if w.len() <= id {
w.resize(id + 1, None);
}
@@ -743,7 +743,7 @@ pub fn base_style(style: &str) -> &str {
}
fn norm(s: &str) -> String {
s.chars().filter(|c| c.is_alphanumeric()).flat_map(|c| c.to_lowercase()).collect()
s.chars().filter(|c| c.is_alphanumeric()).flat_map(char::to_lowercase).collect()
}
/// Weight implied by a style name.
@@ -807,7 +807,7 @@ impl FontDb {
pub fn set_system_fallback(&self, on: bool) {
#[cfg(not(target_arch = "wasm32"))]
{
self.sys.lock().unwrap_or_else(|e| e.into_inner()).enabled = on;
self.sys.lock().unwrap_or_else(std::sync::PoisonError::into_inner).enabled = on;
}
#[cfg(target_arch = "wasm32")]
let _ = on;
@@ -822,14 +822,14 @@ impl FontDb {
}
pub(crate) fn read_faces(&self) -> std::sync::RwLockReadGuard<'_, Vec<Arc<FontFace>>> {
self.faces.read().unwrap_or_else(|e| e.into_inner())
self.faces.read().unwrap_or_else(std::sync::PoisonError::into_inner)
}
/// The system font catalog, scanned first if it hasn't been yet.
#[cfg(not(target_arch = "wasm32"))]
fn read_catalog(&self) -> std::sync::RwLockReadGuard<'_, Vec<CatalogEntry>> {
self.ensure_catalog();
self.catalog.read().unwrap_or_else(|e| e.into_inner())
self.catalog.read().unwrap_or_else(std::sync::PoisonError::into_inner)
}
/// Family names available (loaded plus cataloged system fonts), sorted and deduplicated.
@@ -885,7 +885,7 @@ impl FontDb {
}
}
}
let mut faces = self.faces.write().unwrap_or_else(|e| e.into_inner());
let mut faces = self.faces.write().unwrap_or_else(std::sync::PoisonError::into_inner);
let mut added = 0;
for f in new {
// Checked again under the lock: another thread may have added the same face.
@@ -923,7 +923,7 @@ impl FontDb {
#[cfg(not(target_arch = "wasm32"))]
pub fn load_system_fonts(&self) -> usize {
// Families load again from the new catalog when next looked up.
self.loaded_families.lock().unwrap_or_else(|e| e.into_inner()).clear();
self.loaded_families.lock().unwrap_or_else(std::sync::PoisonError::into_inner).clear();
// The first scan, or a rescan once it is done (never both at once).
let mut first = None;
self.cataloged.get_or_init(|| first = Some(self.scan_font_dirs()));
@@ -959,7 +959,7 @@ impl FontDb {
}
continue;
}
let ext = p.extension().and_then(|e| e.to_str()).map(|e| e.to_ascii_lowercase());
let ext = p.extension().and_then(std::ffi::OsStr::to_str).map(str::to_ascii_lowercase);
if !matches!(ext.as_deref(), Some("ttf" | "otf" | "ttc" | "otc")) {
continue;
}
@@ -975,7 +975,7 @@ impl FontDb {
}
let n = found.len();
log::debug!("cataloged {n} system font faces");
*self.catalog.write().unwrap_or_else(|e| e.into_inner()) = found;
*self.catalog.write().unwrap_or_else(std::sync::PoisonError::into_inner) = found;
n
}
@@ -983,7 +983,7 @@ impl FontDb {
/// it is loaded.
#[cfg(not(target_arch = "wasm32"))]
fn load_cataloged(&self, family: &str) -> bool {
let loading = self.loading.lock().unwrap_or_else(|e| e.into_inner());
let loading = self.loading.lock().unwrap_or_else(std::sync::PoisonError::into_inner);
self.load_family(family, &loading)
}
@@ -1047,7 +1047,7 @@ impl FontDb {
// them rather than finding none and falling back to another family.
#[cfg(not(target_arch = "wasm32"))]
{
let mut loaded = self.loaded_families.lock().unwrap_or_else(|e| e.into_inner());
let mut loaded = self.loaded_families.lock().unwrap_or_else(std::sync::PoisonError::into_inner);
let key = family.to_lowercase();
if !loaded.contains(&key) {
self.load_cataloged(family);
@@ -1112,7 +1112,7 @@ impl FontDb {
}
let f = make_face(base.bytes.clone(), base.index, base.family.clone(), style.to_string(), coords)?;
let f = Arc::new(f);
self.faces.write().unwrap_or_else(|e| e.into_inner()).push(f.clone());
self.faces.write().unwrap_or_else(std::sync::PoisonError::into_inner).push(f.clone());
Some(f)
}
@@ -1214,20 +1214,20 @@ impl FontDb {
}
let seen = self.read_faces().len();
{
let sys = self.sys.lock().unwrap_or_else(|e| e.into_inner());
let sys = self.sys.lock().unwrap_or_else(std::sync::PoisonError::into_inner);
if !sys.enabled || sys.misses.contains(&c) {
return false;
}
}
let covered = |db: &FontDb| db.read_faces().iter().any(|f| f.covers(c) && accept(f));
let cataloged: Vec<String> = self.read_catalog().iter().map(|e| e.family.clone()).collect();
let loading = self.loading.lock().unwrap_or_else(|e| e.into_inner());
let loading = self.loading.lock().unwrap_or_else(std::sync::PoisonError::into_inner);
// Another thread may have loaded a face for `c` (faces are only ever added), or found
// there is none, while this one waited.
if self.read_faces().len() != seen && covered(self) {
return true;
}
if self.sys.lock().unwrap_or_else(|e| e.into_inner()).misses.contains(&c) {
if self.sys.lock().unwrap_or_else(std::sync::PoisonError::into_inner).misses.contains(&c) {
return false;
}
for fam in SYSTEM_FALLBACKS {
@@ -1238,7 +1238,8 @@ impl FontDb {
}
}
}
let mut paths: Vec<std::path::PathBuf> = self.catalog.read().unwrap_or_else(|e| e.into_inner()).iter().map(|e| e.path.clone()).collect();
let mut paths: Vec<std::path::PathBuf> =
self.catalog.read().unwrap_or_else(std::sync::PoisonError::into_inner).iter().map(|e| e.path.clone()).collect();
paths.sort();
paths.dedup();
for p in paths {
@@ -1253,7 +1254,7 @@ impl FontDb {
}
}
if remember_miss {
self.sys.lock().unwrap_or_else(|e| e.into_inner()).misses.insert(c);
self.sys.lock().unwrap_or_else(std::sync::PoisonError::into_inner).misses.insert(c);
}
false
}
@@ -1326,10 +1327,10 @@ impl FontDb {
if c.is_control() || c.is_whitespace() {
return false;
}
if !self.sys.lock().unwrap_or_else(|e| e.into_inner()).enabled {
if !self.sys.lock().unwrap_or_else(std::sync::PoisonError::into_inner).enabled {
return false;
}
if MISSES.lock().unwrap_or_else(|e| e.into_inner()).get_or_insert_with(Default::default).contains(&c) {
if MISSES.lock().unwrap_or_else(std::sync::PoisonError::into_inner).get_or_insert_with(Default::default).contains(&c) {
return false;
}
let mut paths: Vec<std::path::PathBuf> = self.read_catalog().iter().map(|e| e.path.clone()).collect();
@@ -1347,7 +1348,7 @@ impl FontDb {
return true;
}
}
MISSES.lock().unwrap_or_else(|e| e.into_inner()).get_or_insert_with(Default::default).insert(c);
MISSES.lock().unwrap_or_else(std::sync::PoisonError::into_inner).get_or_insert_with(Default::default).insert(c);
false
}
@@ -1359,12 +1360,12 @@ impl FontDb {
return false;
}
{
let sys = self.sys.lock().unwrap_or_else(|e| e.into_inner());
let sys = self.sys.lock().unwrap_or_else(std::sync::PoisonError::into_inner);
if !sys.enabled {
return false;
}
}
if MISSES.lock().unwrap_or_else(|e| e.into_inner()).get_or_insert_with(Default::default).contains(&c) {
if MISSES.lock().unwrap_or_else(std::sync::PoisonError::into_inner).get_or_insert_with(Default::default).contains(&c) {
return false;
}
let mut paths: Vec<std::path::PathBuf> = self.read_catalog().iter().map(|e| e.path.clone()).collect();
@@ -1382,14 +1383,14 @@ impl FontDb {
return true;
}
}
MISSES.lock().unwrap_or_else(|e| e.into_inner()).get_or_insert_with(Default::default).insert(c);
MISSES.lock().unwrap_or_else(std::sync::PoisonError::into_inner).get_or_insert_with(Default::default).insert(c);
false
}
/// Glyph outline in font units, y-down (flipped), cached per (face, glyph).
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(|e| e.into_inner()).get(&key) {
if let Some(p) = self.outlines.lock().unwrap_or_else(std::sync::PoisonError::into_inner).get(&key) {
return p.clone();
}
let mut pen = FlipPen(BezPath::new());
@@ -1399,7 +1400,7 @@ impl FontDb {
let _ = g.draw(DrawSettings::unhinted(Size::unscaled(), face.location()), &mut pen);
}
let p = Arc::new(pen.0);
let mut cache = self.outlines.lock().unwrap_or_else(|e| e.into_inner());
let mut cache = self.outlines.lock().unwrap_or_else(std::sync::PoisonError::into_inner);
if cache.len() >= OUTLINE_CACHE_MAX {
cache.clear();
}
+26 -26
View File
@@ -1070,33 +1070,33 @@ mod tests {
assert_eq!(md_list_item(not), None, "{not:?}");
}
}
}
/// #814: slides_to_outline includes table cell text, which it used to skip.
#[test]
fn outline_includes_table_text() {
use deckcraft_model::table::{Cell, Row, Table};
use deckcraft_model::{Shape, ShapeKind, Xfrm};
let mut p = defaults::new_presentation(None);
let layout = defaults::layout_of_kind(&p, LayoutType::TitleAndContent).unwrap();
let mut s = defaults::new_slide(&mut p, layout).expect("slide");
s.shapes.retain(|sh| !sh.ph_type().is_some_and(|k| k.is_title()));
let mut body = Shape { xfrm: Some(Xfrm::new(0.0, 0.0, 600.0, 100.0)), ..Default::default() };
body.text = Some(TextBody::from_text("Text box body"));
s.shapes.push(body);
/// #814: slides_to_outline includes table cell text, which it used to skip.
#[test]
fn outline_includes_table_text() {
use deckcraft_model::table::{Cell, Row, Table};
use deckcraft_model::{Shape, ShapeKind, Xfrm};
let mut p = defaults::new_presentation(None);
let layout = defaults::layout_of_kind(&p, LayoutType::TitleAndContent).unwrap();
let mut s = defaults::new_slide(&mut p, layout).expect("slide");
s.shapes.retain(|sh| !sh.ph_type().is_some_and(|k| k.is_title()));
let mut body = Shape { xfrm: Some(Xfrm::new(0.0, 0.0, 600.0, 100.0)), ..Default::default() };
body.text = Some(TextBody::from_text("Text box body"));
s.shapes.push(body);
let mut table = Table { cols: vec![200.0, 200.0], ..Default::default() };
for texts in [["Cell A1", "Cell B1"], ["Cell A2", "Cell B2"]] {
let cells: Vec<Cell> = texts.iter().map(|t| Cell { text: TextBody::from_text(t), ..Default::default() }).collect();
table.rows.push(Row { height: 30.0, cells });
let mut table = Table { cols: vec![200.0, 200.0], ..Default::default() };
for texts in [["Cell A1", "Cell B1"], ["Cell A2", "Cell B2"]] {
let cells: Vec<Cell> = texts.iter().map(|t| Cell { text: TextBody::from_text(t), ..Default::default() }).collect();
table.rows.push(Row { height: 30.0, cells });
}
let mut tbl = Shape { xfrm: Some(Xfrm::new(0.0, 100.0, 400.0, 60.0)), ..Default::default() };
tbl.kind = ShapeKind::Table(table);
s.shapes.push(tbl);
p.slides = vec![std::sync::Arc::new(s)];
let out = slides_to_outline(&p);
assert!(out.contains("Text box body"), "{out}");
assert!(out.contains("Cell A1\tCell B1"), "{out}");
assert!(out.contains("Cell A2\tCell B2"), "{out}");
}
let mut tbl = Shape { xfrm: Some(Xfrm::new(0.0, 100.0, 400.0, 60.0)), ..Default::default() };
tbl.kind = ShapeKind::Table(table);
s.shapes.push(tbl);
p.slides = vec![std::sync::Arc::new(s)];
let out = slides_to_outline(&p);
assert!(out.contains("Text box body"), "{out}");
assert!(out.contains("Cell A1\tCell B1"), "{out}");
assert!(out.contains("Cell A2\tCell B2"), "{out}");
}
+1 -1
View File
@@ -3,7 +3,7 @@
use crate::cabac::Cabac;
use crate::error::{Result, ensure, invalid};
use crate::mbtypes::*;
use crate::mbtypes::{MbTypeInfo, Part, SubMbInfo, b_mb_type, b_sub_mb_type, intra_mb_type, p_mb_type, p_sub_mb_type};
use crate::picture::MbKind;
use crate::slice::SliceType;
use crate::slicedec::{MbCur, SliceDecoder};
+1 -1
View File
@@ -1,6 +1,6 @@
//! CAVLC residual block parsing (9.2).
use crate::cavlc_tables::*;
use crate::cavlc_tables::{COEFF_TOKEN, RUN_BEFORE, TOTAL_ZEROS_2X2, TOTAL_ZEROS_2X4, TOTAL_ZEROS_4X4};
use crate::error::{Result, ensure};
use deckcraft_bitstream::BitReader;
use std::sync::OnceLock;
+3 -3
View File
@@ -232,7 +232,7 @@ fn filter_vertical<const N: usize>(pix: &mut [u8], stride: usize, x: usize, y: u
let mut w = [0u64; N];
for (i, w) in w.iter_mut().enumerate() {
let o = (y + i) * stride + x - 4;
let Some(&b) = pix.get(o..).and_then(|p| p.first_chunk::<8>()) else {
let Some(&b) = pix.get(o..).and_then(<[_]>::first_chunk::<8>) else {
return;
};
*w = u64::from_le_bytes(b);
@@ -259,7 +259,7 @@ fn filter_horizontal<const N: usize>(pix: &mut [u8], stride: usize, x: usize, y:
let base = (y - 4) * stride + x;
let mut s = [[0i16; N]; 8];
for (k, row) in s.iter_mut().enumerate() {
let Some(src) = pix.get(base + k * stride..).and_then(|p| p.first_chunk::<N>()) else {
let Some(src) = pix.get(base + k * stride..).and_then(<[_]>::first_chunk::<N>) else {
return;
};
for i in 0..N {
@@ -269,7 +269,7 @@ fn filter_horizontal<const N: usize>(pix: &mut [u8], stride: usize, x: usize, y:
filter_lanes(&mut s, ep, chroma);
let (k0, k1) = if chroma { (3, 5) } else { (1, 7) };
for (k, row) in s.iter().enumerate().take(k1).skip(k0) {
let Some(dst) = pix.get_mut(base + k * stride..).and_then(|p| p.first_chunk_mut::<N>()) else {
let Some(dst) = pix.get_mut(base + k * stride..).and_then(<[_]>::first_chunk_mut::<N>) else {
return;
};
for i in 0..N {
+199 -152
View File
@@ -3,7 +3,7 @@
use crate::error::{Result, ensure};
use crate::picture::{FrameRef, RefPic};
use crate::slice::{Mmco, SliceHeader, SliceType};
use crate::slice::{Mmco, RefPicListMod, SliceHeader, SliceType};
use std::sync::Arc;
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
@@ -174,6 +174,103 @@ impl Dpb {
}
}
/// Initial reference picture lists (8.2.4.2) as DPB entries.
fn initial_entry_lists<'a>(
sh: &SliceHeader,
cur_poc: i32,
short: &[&'a DpbEntry],
long: &[&'a DpbEntry],
pic_num: impl Fn(&DpbEntry) -> i32,
) -> [Vec<&'a DpbEntry>; 2] {
let mut entry_lists: [Vec<&'a DpbEntry>; 2] = [Vec::new(), Vec::new()];
if sh.slice_type != SliceType::B {
let mut s = short.to_vec();
s.sort_by_key(|e| std::cmp::Reverse(pic_num(e)));
entry_lists[0] = s.into_iter().chain(long.iter().copied()).collect();
} else {
let mut before: Vec<&'a DpbEntry> = short.iter().copied().filter(|e| e.poc < cur_poc).collect();
let mut after: Vec<&'a DpbEntry> = short.iter().copied().filter(|e| e.poc > cur_poc).collect();
before.sort_by_key(|e| std::cmp::Reverse(e.poc));
after.sort_by_key(|e| e.poc);
entry_lists[0] = before.iter().chain(after.iter()).chain(long.iter()).copied().collect();
entry_lists[1] = after.iter().chain(before.iter()).chain(long.iter()).copied().collect();
if entry_lists[1].len() > 1 && entry_lists[0].len() == entry_lists[1].len() {
let same = entry_lists[0].iter().zip(entry_lists[1].iter()).all(|(a, b)| std::ptr::eq(*a, *b));
if same {
entry_lists[1].swap(0, 1);
}
}
}
entry_lists
}
/// Reference picture list modification (8.2.4.3) of one list; `n` is num_ref_idx_active.
fn modify_ref_list<'a>(
list: &mut Vec<Option<&'a DpbEntry>>,
mods: &[RefPicListMod],
n: usize,
cur_fn: u32,
max_frame_num: u32,
short: &[&'a DpbEntry],
long: &[&'a DpbEntry],
) -> Result<()> {
let max_pic_num = max_frame_num as i32;
let curr_pic_num = cur_fn as i32;
let mut pred = curr_pic_num;
let mut ref_idx = 0usize;
for m in mods {
ensure!(ref_idx < n, "too many ref_pic_list_modification entries");
list.push(None); // temporarily one longer
let (target, is_long): (Option<&DpbEntry>, bool) = match m.idc {
0 | 1 => {
let d = m.value as i32 + 1;
let no_wrap = if m.idc == 0 {
if pred - d < 0 { pred - d + max_pic_num } else { pred - d }
} else if pred + d >= max_pic_num {
pred + d - max_pic_num
} else {
pred + d
};
pred = no_wrap;
let pn = if no_wrap > curr_pic_num { no_wrap - max_pic_num } else { no_wrap };
(short.iter().copied().find(|e| Self::frame_num_wrap(e, cur_fn, max_frame_num) == pn), false)
}
2 => (long.iter().copied().find(|e| e.long_term_frame_idx == m.value), true),
_ => continue,
};
let Some(target) = target else {
// Missing picture: keep the list unchanged (error concealment).
list.pop();
ref_idx += 1;
continue;
};
for c in (ref_idx + 1..=n).rev() {
list[c] = list[c - 1];
}
list[ref_idx] = Some(target);
ref_idx += 1;
let mut nidx = ref_idx;
for c in ref_idx..=n {
let keep = match list[c] {
None => true,
Some(e) => {
if is_long {
!(e.mark == RefMark::Long && e.long_term_frame_idx == target.long_term_frame_idx)
} else {
!(e.mark == RefMark::Short && std::ptr::eq(e, target))
}
}
};
if keep {
list[nidx] = list[c];
nidx += 1;
}
}
list.truncate(n);
}
Ok(())
}
/// Initial reference picture lists (8.2.4.2) + modification (8.2.4.3) for a frame slice.
pub fn build_ref_lists(&self, sh: &SliceHeader, cur_poc: i32, max_frame_num: u32) -> Result<[Vec<RefPic>; 2]> {
let mut lists: [Vec<RefPic>; 2] = [Vec::new(), Vec::new()];
@@ -186,25 +283,7 @@ impl Dpb {
let mut long: Vec<&DpbEntry> = self.entries.iter().filter(|e| e.mark == RefMark::Long).collect();
long.sort_by_key(|e| e.long_term_frame_idx);
let to_ref = |e: &&DpbEntry| RefPic { frame: e.frame.clone(), long_term: e.mark == RefMark::Long };
let mut entry_lists: [Vec<&DpbEntry>; 2] = [Vec::new(), Vec::new()];
if sh.slice_type != SliceType::B {
let mut s = short.clone();
s.sort_by_key(|e| std::cmp::Reverse(pic_num(e)));
entry_lists[0] = s.into_iter().chain(long.iter().copied()).collect();
} else {
let mut before: Vec<&DpbEntry> = short.iter().copied().filter(|e| e.poc < cur_poc).collect();
let mut after: Vec<&DpbEntry> = short.iter().copied().filter(|e| e.poc > cur_poc).collect();
before.sort_by_key(|e| std::cmp::Reverse(e.poc));
after.sort_by_key(|e| e.poc);
entry_lists[0] = before.iter().chain(after.iter()).chain(long.iter()).copied().collect();
entry_lists[1] = after.iter().chain(before.iter()).chain(long.iter()).copied().collect();
if entry_lists[1].len() > 1 && entry_lists[0].len() == entry_lists[1].len() {
let same = entry_lists[0].iter().zip(entry_lists[1].iter()).all(|(a, b)| std::ptr::eq(*a, *b));
if same {
entry_lists[1].swap(0, 1);
}
}
}
let entry_lists = Self::initial_entry_lists(sh, cur_poc, &short, &long, pic_num);
let nlists = if sh.slice_type == SliceType::B { 2 } else { 1 };
for l in 0..nlists {
let n = sh.num_ref_idx_active[l] as usize;
@@ -212,60 +291,7 @@ impl Dpb {
list.truncate(n);
list.resize(n, None);
if !sh.ref_pic_list_mod[l].is_empty() {
let max_pic_num = max_frame_num as i32;
let curr_pic_num = cur_fn as i32;
let mut pred = curr_pic_num;
let mut ref_idx = 0usize;
for m in &sh.ref_pic_list_mod[l] {
ensure!(ref_idx < n, "too many ref_pic_list_modification entries");
list.push(None); // temporarily one longer
let (target, is_long): (Option<&DpbEntry>, bool) = match m.idc {
0 | 1 => {
let d = m.value as i32 + 1;
let no_wrap = if m.idc == 0 {
if pred - d < 0 { pred - d + max_pic_num } else { pred - d }
} else if pred + d >= max_pic_num {
pred + d - max_pic_num
} else {
pred + d
};
pred = no_wrap;
let pn = if no_wrap > curr_pic_num { no_wrap - max_pic_num } else { no_wrap };
(short.iter().copied().find(|e| pic_num(e) == pn), false)
}
2 => (long.iter().copied().find(|e| e.long_term_frame_idx == m.value), true),
_ => continue,
};
let Some(target) = target else {
// Missing picture: keep the list unchanged (error concealment).
list.pop();
ref_idx += 1;
continue;
};
for c in (ref_idx + 1..=n).rev() {
list[c] = list[c - 1];
}
list[ref_idx] = Some(target);
ref_idx += 1;
let mut nidx = ref_idx;
for c in ref_idx..=n {
let keep = match list[c] {
None => true,
Some(e) => {
if is_long {
!(e.mark == RefMark::Long && e.long_term_frame_idx == target.long_term_frame_idx)
} else {
!(e.mark == RefMark::Short && std::ptr::eq(e, target))
}
}
};
if keep {
list[nidx] = list[c];
nidx += 1;
}
}
list.truncate(n);
}
Self::modify_ref_list(&mut list, &sh.ref_pic_list_mod[l], n, cur_fn, max_frame_num, &short, &long)?;
}
// Fill missing entries (should not happen in conforming streams) with the first valid entry.
let fallback = list.iter().flatten().next().copied().or_else(|| self.entries.last());
@@ -275,85 +301,81 @@ impl Dpb {
Ok(lists)
}
/// 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;
let curr_pic_num = cur_frame_num as i32;
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;
}
}
}
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;
}
}
}
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.max_long_term_frame_idx = if max == 0 { None } else { Some(max - 1) };
}
5 => {
for e in &mut self.entries {
e.mark = RefMark::Unused;
}
self.max_long_term_frame_idx = None;
had5 = true;
}
6 => {
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;
}
}
*cur_long = Some(m.long_term_frame_idx);
}
_ => {}
}
had5
}
/// Adaptive memory control (8.2.5.4). Returns true if MMCO 5 was executed.
fn apply_mmcos(&mut self, mmcos: &[Mmco], cur_frame_num: u32, max_frame_num: u32, cur_long: &mut Option<u32>) -> bool {
let mut had5 = false;
let curr_pic_num = cur_frame_num as i32;
for m in mmcos {
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;
}
}
}
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;
}
}
}
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.max_long_term_frame_idx = if max == 0 { None } else { Some(max - 1) };
}
5 => {
for e in &mut self.entries {
e.mark = RefMark::Unused;
}
self.max_long_term_frame_idx = None;
had5 = true;
}
6 => {
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;
}
}
*cur_long = Some(m.long_term_frame_idx);
}
_ => {}
}
had5 |= self.apply_mmco(m, cur_frame_num, max_frame_num, cur_long);
}
self.prune();
had5
}
/// Reference marking for the just-decoded picture and insertion into the DPB with output bumping.
/// Returns pictures ready for output.
#[allow(clippy::too_many_arguments)]
pub fn store_picture(
&mut self,
sh: &SliceHeader,
frame: FrameRef,
poc: i32,
max_frame_num: u32,
max_num_ref_frames: usize,
meta: Arc<OutputMeta>,
out: &mut Vec<Output>,
) {
/// Reference marking of the current picture (8.2.5); returns (mark, long-term idx, MMCO 5 executed).
fn mark_current(&mut self, sh: &SliceHeader, max_frame_num: u32, max_num_ref_frames: usize) -> (RefMark, u32, bool) {
let mut mark = RefMark::Unused;
let mut long_idx = 0;
let mut had5 = false;
@@ -385,12 +407,12 @@ impl Dpb {
}
}
}
if had5 {
// All earlier pictures are output before the current one (C.4.4).
self.flush(out);
}
let frame_num = if had5 { 0 } else { sh.frame_num };
// Bumping until there is space (C.4.5.1 / C.4.5.2).
(mark, long_idx, had5)
}
/// Bumping until there is space (C.4.5.1 / C.4.5.2). Returns true when the (non-reference) current
/// picture has the smallest POC and is to be output directly instead of being stored.
fn bump_for_space(&mut self, mark: RefMark, poc: i32, out: &mut Vec<Output>) -> bool {
if mark == RefMark::Unused {
loop {
let waiting = self.entries.len();
@@ -400,8 +422,7 @@ impl Dpb {
// non-reference picture with the smallest POC is output directly
let min_poc = self.entries.iter().filter(|e| e.needed_for_output).map(|e| e.poc).min();
if min_poc.is_none_or(|m| poc < m) {
out.push(Output { frame, poc, meta });
return;
return true;
}
if !self.bump(out) {
break;
@@ -414,6 +435,32 @@ impl Dpb {
}
}
}
false
}
/// Reference marking for the just-decoded picture and insertion into the DPB with output bumping.
/// Returns pictures ready for output.
#[allow(clippy::too_many_arguments)]
pub fn store_picture(
&mut self,
sh: &SliceHeader,
frame: FrameRef,
poc: i32,
max_frame_num: u32,
max_num_ref_frames: usize,
meta: Arc<OutputMeta>,
out: &mut Vec<Output>,
) {
let (mark, long_idx, had5) = self.mark_current(sh, max_frame_num, max_num_ref_frames);
if had5 {
// All earlier pictures are output before the current one (C.4.4).
self.flush(out);
}
let frame_num = if had5 { 0 } else { sh.frame_num };
if self.bump_for_space(mark, poc, out) {
out.push(Output { frame, poc, meta });
return;
}
self.entries.push(DpbEntry {
frame,
poc,
+1 -1
View File
@@ -1,7 +1,7 @@
//! Sequence and picture parameter sets (7.3.2.1, 7.3.2.2) including VUI and scaling lists.
use crate::error::{Result, ensure, unsupported};
use crate::tables::*;
use crate::tables::{DEFAULT_4X4_INTER, DEFAULT_4X4_INTRA, DEFAULT_8X8_INTER, DEFAULT_8X8_INTRA, ZIGZAG4, ZIGZAG8};
use deckcraft_bitstream::BitReader;
/// Scaling matrices after applying fall-back rules, stored in raster order (weightScale).
+1 -1
View File
@@ -301,7 +301,7 @@ fn plane_allocator() -> fn(usize) -> Vec<u8> {
fn copy_line<const N: usize>(line: &[u8], x0: i32, out: &mut [u8], w: usize) {
if w <= N
&& x0 >= 0
&& let Some(src) = line.get(x0 as usize..).and_then(|l| l.first_chunk::<N>())
&& let Some(src) = line.get(x0 as usize..).and_then(<[_]>::first_chunk::<N>)
&& let Some(dst) = out.first_chunk_mut::<N>()
{
*dst = *src;
+6 -6
View File
@@ -5,11 +5,11 @@ use crate::deblock;
use crate::error::{Result, ensure, invalid};
use crate::inter::{self, Weight};
use crate::intra::{self, Avail};
use crate::mbtypes::*;
use crate::mbtypes::{CBP_ME, MbTypeInfo, Part, SubMbInfo, SubPart, b_mb_type, b_sub_mb_type, intra_mb_type, p_mb_type, p_sub_mb_type};
use crate::params::{Pps, Sps};
use crate::picture::*;
use crate::picture::{Frame, FrameRef, MbKind, MbState, Planes, RefPic};
use crate::slice::{SliceHeader, SliceType};
use crate::tables::*;
use crate::tables::{BLK4_XY, QPC_TABLE, RASTER_TO_BLK4, ZIGZAG4, ZIGZAG8};
use crate::transform::{self, LevelScale};
use deckcraft_bitstream::BitReader;
@@ -907,7 +907,7 @@ impl<'a> SliceDecoder<'a> {
let stride = self.pic.planes.width;
let px = self.mb_x * 16 + (raster & 3) * 4;
let py = self.mb_y * 16 + (raster >> 2) * 4;
let Some(blk) = self.s.coef.get_mut(raster * 16..).and_then(|c| c.first_chunk_mut::<16>()) else {
let Some(blk) = self.s.coef.get_mut(raster * 16..).and_then(<[_]>::first_chunk_mut::<16>) else {
return;
};
transform::idct4_add(blk, &mut self.pic.planes.y[py * stride + px..], stride);
@@ -921,7 +921,7 @@ impl<'a> SliceDecoder<'a> {
let stride = self.pic.planes.width;
let px = self.mb_x * 16 + (b8 & 1) * 8;
let py = self.mb_y * 16 + (b8 >> 1) * 8;
let Some(blk) = self.s.coef.get_mut(b8 * 64..).and_then(|c| c.first_chunk_mut::<64>()) else {
let Some(blk) = self.s.coef.get_mut(b8 * 64..).and_then(<[_]>::first_chunk_mut::<64>) else {
return;
};
transform::idct8_add(blk, &mut self.pic.planes.y[py * stride + px..], stride);
@@ -940,7 +940,7 @@ impl<'a> SliceDecoder<'a> {
}
let px = self.mb_x * 8 + (b & 1) * 4;
let py = self.mb_y * 8 + (b >> 1) * 4;
let Some(blk) = self.s.coef_c[c].get_mut(b * 16..).and_then(|c| c.first_chunk_mut::<16>()) else {
let Some(blk) = self.s.coef_c[c].get_mut(b * 16..).and_then(<[_]>::first_chunk_mut::<16>) else {
continue;
};
let plane = if c == 0 { &mut self.pic.planes.cb } else { &mut self.pic.planes.cr };
+1 -1
View File
@@ -107,7 +107,7 @@ struct Pic {
impl Pic {
fn is_p(&self) -> bool {
!self.idr && !self.b && self.mbs.iter().any(|m| m.is_none())
!self.idr && !self.b && self.mbs.iter().any(Option::is_none)
}
fn inter(&self) -> bool {
self.b || self.is_p()
+2 -2
View File
@@ -117,7 +117,7 @@ pub fn idct4_add(d: &[i32; 16], dst: &mut [u8], stride: usize) {
o[3][j] = e - h;
}
for (i, row) in o.iter().enumerate() {
let Some(line) = dst.get_mut(i * stride..).and_then(|d| d.first_chunk_mut::<4>()) else {
let Some(line) = dst.get_mut(i * stride..).and_then(<[_]>::first_chunk_mut::<4>) else {
return;
};
for (p, &v) in line.iter_mut().zip(row) {
@@ -173,7 +173,7 @@ pub fn idct8_add(d: &[i32; 64], dst: &mut [u8], stride: usize) {
}
}
for (i, row) in o.iter().enumerate() {
let Some(line) = dst.get_mut(i * stride..).and_then(|d| d.first_chunk_mut::<8>()) else {
let Some(line) = dst.get_mut(i * stride..).and_then(<[_]>::first_chunk_mut::<8>) else {
return;
};
for (p, &v) in line.iter_mut().zip(row) {
+1 -1
View File
@@ -25,7 +25,7 @@ fn filt_w<const T: usize, const W: usize>(src: &[i16], step: usize, f: &[i8; T],
let mut acc = [0i32; W];
for k in 0..T {
let c = f[k] as i32;
let Some(p) = src.get(k * step..).and_then(|s| s.first_chunk::<W>()) else {
let Some(p) = src.get(k * step..).and_then(<[_]>::first_chunk::<W>) else {
return;
};
for j in 0..W {
+5 -1
View File
@@ -10,7 +10,11 @@ use crate::intra::{self, Refs};
use crate::params::{Layout, Pps, Sps};
use crate::picture::{FrameRef, RefPic};
use crate::slice::{SliceHeader, SliceType};
use crate::spec_tables::*;
use crate::spec_tables::{
ABS_MVD_GT0, ABS_MVD_GT1, CBF_CHROMA, CBF_LUMA, CODED_SUB_BLOCK, CU_QP_DELTA_ABS, CU_SKIP, CU_TRANSQUANT_BYPASS, GT1, GT2, INTER_PRED_IDC,
INTRA_CHROMA, LAST_X_PREFIX, LAST_Y_PREFIX, MERGE_FLAG, MERGE_IDX, MVP_FLAG, PART_MODE, PRED_MODE, PREV_INTRA_LUMA, REF_IDX, RQT_ROOT_CBF,
SAO_MERGE, SAO_TYPE, SIG_COEFF, SPLIT_CU, SPLIT_TRANSFORM, TRANSFORM_SKIP,
};
use crate::tables::{CTX_IDX_MAP, LEVEL_SCALE, SCAN_4, qpc_420, scan_order};
use crate::transform;
use std::sync::Arc;
+1 -1
View File
@@ -7,7 +7,7 @@
use crate::Decoder;
use crate::cabac::{Contexts, NEXT_STATE, RANGE_TAB_LPS, init_contexts};
use crate::spec_tables::*;
use crate::spec_tables::{CU_SKIP, NUM_CTX, PRED_MODE, SPLIT_CU};
use deckcraft_bitstream::{BitWriter, escape_rbsp};
const W: usize = 96;
+1 -1
View File
@@ -31,7 +31,7 @@ pub use audio::Pcm;
pub use clip::ClipParams;
pub use feed::VideoFeed;
pub use player::{AudioOut, PlayState, Player, VoiceSpec, VoiceStatus};
pub use probe::{AudioInfo, MediaInfo, VideoInfo, probe};
pub use probe::{AudioInfo, MediaInfo, VideoInfo, probe, probe_headers};
pub use video::{Frame, VideoDecoder, poster_frame, poster_png};
/// Shared media bytes (the model's `MediaItem::data`).
+1 -1
View File
@@ -254,7 +254,7 @@ impl Player {
}
fn with<R>(&self, f: impl FnOnce(&mut Mixer) -> R) -> R {
let mut g = self.mixer.lock().unwrap_or_else(|e| e.into_inner());
let mut g = self.mixer.lock().unwrap_or_else(std::sync::PoisonError::into_inner);
f(&mut g)
}
+13 -6
View File
@@ -48,6 +48,19 @@ fn ms(secs: f64) -> u64 {
/// Probe `bytes`.
pub fn probe(bytes: &Bytes) -> Result<MediaInfo> {
let mut info = probe_headers(bytes)?;
if info.duration_ms == 0 && info.audio.as_ref().is_some_and(|a| a.decodable) && info.video.is_none() {
// No length in the header (raw MP3 / ADTS without an index): decode to measure.
if let Ok(pcm) = crate::audio::decode(bytes) {
info.duration_ms = ms(pcm.duration());
}
}
Ok(info)
}
/// [`probe`] from the headers alone, without decoding: a raw MP3 or ADTS stream without an
/// index reports a `duration_ms` of 0.
pub fn probe_headers(bytes: &Bytes) -> Result<MediaInfo> {
let c = Container::sniff(bytes);
let mut info = match c {
Container::Mp4 => probe_mp4(bytes)?,
@@ -73,12 +86,6 @@ pub fn probe(bytes: &Bytes) -> Result<MediaInfo> {
info.duration_ms = ms(n as f64 / rate as f64);
}
}
if info.duration_ms == 0 && info.audio.as_ref().is_some_and(|a| a.decodable) && info.video.is_none() {
// No length in the header (raw MP3 / ADTS without an index): decode to measure.
if let Ok(pcm) = crate::audio::decode(bytes) {
info.duration_ms = ms(pcm.duration());
}
}
Ok(info)
}
+6 -4
View File
@@ -28,11 +28,13 @@ for f in range(6):
sys.stdout.buffer.write(bytes(v for y in range(h) for x in range(w) for v in ((x * 4 + f * 20) % 256, (y * 5) % 256, (f * 40) % 256)))
" > "$out"
}
frames 64 48 /tmp/deckcraft-fixture-64x48.rgb
frames 64 64 /tmp/deckcraft-fixture-64x64.rgb
VID="-f rawvideo -pix_fmt rgb24 -s 64x48 -r 10 -i /tmp/deckcraft-fixture-64x48.rgb"
TMP_DIR="$(mktemp -d "${TMPDIR:-/tmp}/deckcraft-fixture.XXXXXX")"
trap 'rm -rf "$TMP_DIR"' EXIT
frames 64 48 "$TMP_DIR/deckcraft-fixture-64x48.rgb"
frames 64 64 "$TMP_DIR/deckcraft-fixture-64x64.rgb"
VID="-f rawvideo -pix_fmt rgb24 -s 64x48 -r 10 -i $TMP_DIR/deckcraft-fixture-64x48.rgb"
$FF $VID $TONE44 -c:v libx264 -pix_fmt yuv420p -g 5 -bf 2 -c:a aac -b:a 48k -shortest $META clip-h264.mp4
$FF $VID -c:v libx264 -pix_fmt yuv420p -g 5 -bf 0 $META clip-h264.mov
$FF $VID -c:v libx265 -pix_fmt yuv420p -x265-params log-level=error:keyint=5 -tag:v hvc1 $META clip-hevc.mp4
$FF $VID $TONE -c:v libvpx-vp9 -pix_fmt yuv420p -g 5 -b:v 100k -c:a libopus -b:a 32k -shortest $META clip-vp9.webm
$FF -f rawvideo -pix_fmt rgb24 -s 64x64 -r 10 -i /tmp/deckcraft-fixture-64x64.rgb -c:v libsvtav1 -pix_fmt yuv420p -g 5 -svtav1-params loglevel=0 $META clip-av1.mkv
$FF -f rawvideo -pix_fmt rgb24 -s 64x64 -r 10 -i "$TMP_DIR/deckcraft-fixture-64x64.rgb" -c:v libsvtav1 -pix_fmt yuv420p -g 5 -svtav1-params loglevel=0 $META clip-av1.mkv
+1 -1
View File
@@ -1187,7 +1187,7 @@ fn image(r: Bytes) -> Option<ImageData> {
fn metafile(r: Bytes) -> Option<Picture> {
let size = r.u32(12)? as usize;
let data = r.slice(16, size).or_else(|| r.tail(16).map(|t| t.0))?;
let depth = NESTING.with(|n| n.get());
let depth = NESTING.with(std::cell::Cell::get);
if depth >= MAX_NESTING {
return None;
}
+2 -2
View File
@@ -1217,7 +1217,7 @@ fn savedc_restores_text_colour_and_font() {
fn efont(height: i32, esc: i32, weight: i32, italic: u8, charset: u8, face: &str) -> Vec<u8> {
let mut v = cat(vec![h32(1), e32(height), e32(0), e32(esc), e32(esc), e32(weight)]);
v.extend([italic, 0, 0, charset, 0, 0, 0, 0]);
let mut name: Vec<u8> = face.encode_utf16().flat_map(|u| u.to_le_bytes()).collect();
let mut name: Vec<u8> = face.encode_utf16().flat_map(u16::to_le_bytes).collect();
name.resize(64, 0);
v.extend(name);
erec(82, &v)
@@ -1716,7 +1716,7 @@ fn plus_pic(recs: Vec<Vec<u8>>) -> Picture {
}
fn u16s(s: &str) -> Vec<u8> {
s.encode_utf16().flat_map(|u| u.to_le_bytes()).collect()
s.encode_utf16().flat_map(u16::to_le_bytes).collect()
}
/// A 2 x 2 32bpp ARGB bitmap image object: red, green / blue, white.
+118 -71
View File
@@ -491,50 +491,66 @@ impl Default for Chart {
}
impl Chart {
fn sample_treemap_sunburst(kind: ChartType) -> Self {
let mut c = Chart {
kind,
categories: ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug"].iter().map(ToString::to_string).collect(),
category_levels: vec![["Q1", "Q1", "Q1", "Q2", "Q2", "Q2", "Q3", "Q3"].iter().map(ToString::to_string).collect()],
vary_colors: true,
..Default::default()
};
let v = [4.2, 2.8, 3.6, 4.7, 2.6, 4.1, 1.9, 3.0];
c.series.push(Series { name: "Series 1".into(), values: v.iter().map(|x| Some(*x)).collect(), ..Default::default() });
c
}
fn sample_observation_chart(kind: ChartType) -> Self {
let obs = [12.0, 15.0, 17.0, 18.0, 21.0, 22.0, 22.0, 24.0, 25.0, 27.0, 28.0, 31.0, 33.0, 36.0, 41.0, 48.0];
let n_series = if kind == ChartType::BoxWhisker { 2 } else { 1 };
let categories = if kind == ChartType::BoxWhisker {
(0..obs.len()).map(|i| if i % 2 == 0 { "Group A" } else { "Group B" }.to_string()).collect()
} else {
vec![]
};
let mut c =
Chart { kind, categories, gridlines: true, legend: (kind == ChartType::BoxWhisker).then(|| "b".to_string()), ..Default::default() };
for s in 0..n_series {
c.series.push(Series {
name: format!("Series {}", s + 1),
values: obs.iter().map(|v| Some(v + s as f64 * 6.0)).collect(),
..Default::default()
});
}
c
}
fn sample_stock() -> Self {
let mut c = Chart {
kind: ChartType::Stock,
categories: ["Mon", "Tue", "Wed", "Thu", "Fri"].iter().map(ToString::to_string).collect(),
..Default::default()
};
let hlc = [("High", [55.0, 57.0, 57.0, 58.0, 58.0]), ("Low", [11.0, 12.0, 13.0, 11.0, 25.0]), ("Close", [25.0, 38.0, 50.0, 35.0, 43.0])];
for (name, v) in hlc {
c.series.push(Series { name: name.into(), values: v.iter().map(|x| Some(*x)).collect(), ..Default::default() });
}
c
}
/// The sample data a new chart starts with (our own numbers).
pub fn sample(kind: ChartType) -> Self {
let cats = ["Q1", "Q2", "Q3", "Q4"];
let data = [[4.2, 2.6, 2.1], [2.8, 4.1, 2.4], [3.6, 1.9, 3.3], [4.7, 3.0, 5.1]];
let mut c = Chart { kind, categories: cats.iter().map(|s| s.to_string()).collect(), ..Default::default() };
if matches!(kind, ChartType::Treemap | ChartType::Sunburst) {
// A two-level hierarchy: months inside quarters.
c.categories = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug"].iter().map(|s| s.to_string()).collect();
c.category_levels = vec![["Q1", "Q1", "Q1", "Q2", "Q2", "Q2", "Q3", "Q3"].iter().map(|s| s.to_string()).collect()];
let v = [4.2, 2.8, 3.6, 4.7, 2.6, 4.1, 1.9, 3.0];
c.series.push(Series { name: "Series 1".into(), values: v.iter().map(|x| Some(*x)).collect(), ..Default::default() });
c.vary_colors = true;
return c;
return Self::sample_treemap_sunburst(kind);
}
if matches!(kind, ChartType::Histogram | ChartType::Pareto | ChartType::BoxWhisker) {
// These plot raw observations: a histogram bins one series, a box chart summarises
// each category's points.
let obs = [12.0, 15.0, 17.0, 18.0, 21.0, 22.0, 22.0, 24.0, 25.0, 27.0, 28.0, 31.0, 33.0, 36.0, 41.0, 48.0];
let n_series = if kind == ChartType::BoxWhisker { 2 } else { 1 };
c.categories = if kind == ChartType::BoxWhisker {
(0..obs.len()).map(|i| if i % 2 == 0 { "Group A" } else { "Group B" }.to_string()).collect()
} else {
vec![]
};
for s in 0..n_series {
c.series.push(Series {
name: format!("Series {}", s + 1),
values: obs.iter().map(|v| Some(v + s as f64 * 6.0)).collect(),
..Default::default()
});
}
c.gridlines = true;
c.legend = (kind == ChartType::BoxWhisker).then(|| "b".to_string());
return c;
return Self::sample_observation_chart(kind);
}
if kind == ChartType::Stock {
// High-low-close over five days, the series in the order PowerPoint expects.
c.categories = ["Mon", "Tue", "Wed", "Thu", "Fri"].iter().map(|s| s.to_string()).collect();
let hlc = [("High", [55.0, 57.0, 57.0, 58.0, 58.0]), ("Low", [11.0, 12.0, 13.0, 11.0, 25.0]), ("Close", [25.0, 38.0, 50.0, 35.0, 43.0])];
for (name, v) in hlc {
c.series.push(Series { name: name.into(), values: v.iter().map(|x| Some(*x)).collect(), ..Default::default() });
}
return c;
return Self::sample_stock();
}
let cats = ["Q1", "Q2", "Q3", "Q4"];
let data = [[4.2, 2.6, 2.1], [2.8, 4.1, 2.4], [3.6, 1.9, 3.3], [4.7, 3.0, 5.1]];
let mut c = Chart { kind, categories: cats.iter().map(ToString::to_string).collect(), ..Default::default() };
let n_series =
if matches!(kind, ChartType::Pie | ChartType::Doughnut | ChartType::Funnel | ChartType::Waterfall | ChartType::Treemap) { 1 } else { 3 };
for s in 0..n_series {
@@ -593,27 +609,44 @@ impl Chart {
return (!secondary && self.kind != ChartType::Combo).then_some((0.0, 0.0));
}
let (mut lo, mut hi) = (f64::INFINITY, f64::NEG_INFINITY);
let n = ser.iter().map(|s| s.values.len()).max().unwrap_or(0);
let has_stacked_or_percent = ser.iter().any(|s| {
let k = self.series_kind(s);
k.stacked() || k.percent()
});
let mut percent = false;
for i in 0..n {
let (mut p, mut m, mut stacked) = (0.0, 0.0, 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);
}
}
} else {
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 {
for v in s.values.iter().flatten().copied().filter(|v| v.is_finite()) {
hi = hi.max(v);
lo = lo.min(v);
}
}
if stacked {
hi = hi.max(p);
lo = lo.min(m);
}
}
if !percent {
for s in &ser {
@@ -699,38 +732,52 @@ impl Chart {
crate::numfmt::format(code.map_or("General", String::as_str), v, self.lang.as_deref())
}
/// Value range across series (stacked sums for stacked types).
pub fn value_range(&self) -> (f64, f64) {
fn waterfall_value_range(&self) -> (f64, f64) {
let mut lo: f64 = 0.0;
let mut hi: f64 = 0.0;
if self.kind == ChartType::Waterfall {
for (a, b) in self.waterfall_bars() {
lo = lo.min(a.min(b));
hi = hi.max(a.max(b));
for (a, b) in self.waterfall_bars() {
lo = lo.min(a.min(b));
hi = hi.max(a.max(b));
}
(lo, hi)
}
fn stacked_value_range(&self, n: usize) -> (f64, f64) {
let mut lo: f64 = 0.0;
let mut hi: f64 = 0.0;
for i in 0..n {
let (mut p, mut m) = (0.0, 0.0);
for s in &self.series {
let v = s.values.get(i).copied().flatten().unwrap_or(0.0);
if v >= 0.0 { p += v } else { m += v }
}
return (lo, hi);
hi = hi.max(p);
lo = lo.min(m);
}
(lo, hi)
}
/// Value range across series (stacked sums for stacked types).
pub fn value_range(&self) -> (f64, f64) {
if self.kind == ChartType::Waterfall {
return self.waterfall_value_range();
}
let n = self.categories.len().max(self.series.iter().map(|s| s.values.len()).max().unwrap_or(0));
if self.kind.stacked() {
for i in 0..n {
let (mut p, mut m) = (0.0, 0.0);
for s in &self.series {
let v = s.values.get(i).copied().flatten().unwrap_or(0.0);
if v >= 0.0 { p += v } else { m += v }
}
hi = hi.max(p);
lo = lo.min(m);
}
let (mut lo, mut hi) = if self.kind.stacked() {
self.stacked_value_range(n)
} else {
let mut l: f64 = 0.0;
let mut h: f64 = 0.0;
for s in &self.series {
for v in s.values.iter().flatten() {
if v.is_finite() {
hi = hi.max(*v);
lo = lo.min(*v);
h = h.max(*v);
l = l.min(*v);
}
}
}
}
(l, h)
};
if self.kind.percent() {
return (0.0, 1.0);
}
+3 -3
View File
@@ -1066,13 +1066,13 @@ impl Presentation {
self.slides.iter().position(|s| s.id == id)
}
pub fn slide(&self, id: SlideId) -> Option<&Slide> {
self.slides.iter().find(|s| s.id == id).map(|s| s.as_ref())
self.slides.iter().find(|s| s.id == id).map(AsRef::as_ref)
}
pub fn slide_mut(&mut self, id: SlideId) -> Option<&mut Slide> {
self.slides.iter_mut().find(|s| s.id == id).map(Arc::make_mut)
}
pub fn slide_at(&self, i: usize) -> Option<&Slide> {
self.slides.get(i).map(|s| s.as_ref())
self.slides.get(i).map(AsRef::as_ref)
}
/// Master and layout for a layout id.
pub fn layout(&self, id: LayoutId) -> Option<(&Master, &Layout)> {
@@ -1086,7 +1086,7 @@ impl Presentation {
self.masters.first().map(|m| (m.as_ref(), m.layouts.first()))
}
pub fn first_master(&self) -> Option<&Master> {
self.masters.first().map(|m| m.as_ref())
self.masters.first().map(AsRef::as_ref)
}
pub fn media(&self, id: MediaId) -> Option<&MediaItem> {
self.media.iter().find(|m| m.id == id)
+1 -1
View File
@@ -275,7 +275,7 @@ pub enum Dash {
}
impl Dash {
pub const PRESETS: [&'static str; 11] =
pub const PRESETS: [&str; 11] =
["solid", "sysDot", "sysDash", "dash", "dashDot", "lgDash", "lgDashDot", "lgDashDotDot", "dot", "sysDashDot", "sysDashDotDot"];
/// Dash/space lengths in multiples of the line width.
pub fn pattern(&self) -> Vec<f64> {
+2 -2
View File
@@ -530,7 +530,7 @@ impl Paragraph {
true
}
pub fn text(&self) -> String {
self.runs.iter().map(|r| r.content()).collect()
self.runs.iter().map(Run::content).collect()
}
pub fn char_len(&self) -> usize {
self.runs.iter().map(Run::char_len).sum()
@@ -576,7 +576,7 @@ pub struct ListStyle {
impl ListStyle {
pub fn level(&self, l: u8) -> Option<&LevelStyle> {
self.levels.get(l as usize).and_then(|x| x.as_ref())
self.levels.get(l as usize).and_then(Option::as_ref)
}
pub fn set(&mut self, l: u8, s: LevelStyle) {
let i = (l as usize).min(8);
+87 -74
View File
@@ -57,22 +57,7 @@ impl Trendline {
let yhat: Vec<f64> = xs.iter().map(|x| poly_y(&coeff, *x)).collect();
(Model::Poly(coeff), ys, yhat)
}
TrendlineType::Exp => {
let pairs: Vec<(f64, f64)> = pairs.into_iter().filter(|(_, y)| *y > 0.0).collect();
if pairs.len() < 2 {
return None;
}
let (xs, ln_y): (Vec<f64>, Vec<f64>) = pairs.iter().map(|(x, y)| (*x, y.ln())).unzip();
let fixed = intercept.filter(|c| *c > 0.0).map(|c| c.ln());
let (ln_c, b) = linreg(&xs, &ln_y, fixed)?;
let c = ln_c.exp();
if !c.is_finite() || c <= 0.0 {
return None;
}
let ys: Vec<f64> = pairs.iter().map(|(_, y)| *y).collect();
let yhat: Vec<f64> = xs.iter().map(|x| c * (b * x).exp()).collect();
(Model::Exp { c, b }, ys, yhat)
}
TrendlineType::Exp => fit_exp(&pairs, intercept)?,
TrendlineType::Log => {
let pairs: Vec<(f64, f64)> = pairs.into_iter().filter(|(x, _)| *x > 0.0).collect();
if pairs.len() < 2 {
@@ -83,43 +68,10 @@ impl Trendline {
let yhat: Vec<f64> = ln_x.iter().map(|u| b + c * u).collect();
(Model::Log { b, c }, ys, yhat)
}
TrendlineType::Power => {
let pairs: Vec<(f64, f64)> = pairs.into_iter().filter(|(x, y)| *x > 0.0 && *y > 0.0).collect();
if pairs.len() < 2 {
return None;
}
let (ln_x, ln_y): (Vec<f64>, Vec<f64>) = pairs.iter().map(|(x, y)| (x.ln(), y.ln())).unzip();
let fixed = intercept.filter(|c| *c > 0.0).map(|c| c.ln());
let (ln_c, b) = linreg(&ln_x, &ln_y, fixed)?;
let c = ln_c.exp();
if !c.is_finite() || c <= 0.0 {
return None;
}
let ys: Vec<f64> = pairs.iter().map(|(_, y)| *y).collect();
let yhat: Vec<f64> = pairs.iter().map(|(x, _)| c * x.powf(b)).collect();
(Model::Power { c, b }, ys, yhat)
}
TrendlineType::Power => fit_power(&pairs, intercept)?,
TrendlineType::MovingAvg => {
let period = self.period.unwrap_or(2).clamp(2, 255) as usize;
if pairs.len() <= period {
return None;
}
let mut pts = Vec::new();
let mut ys = Vec::new();
let mut yhat = Vec::new();
for i in period - 1..pairs.len() {
let window = &pairs[i + 1 - period..=i];
let avg = window.iter().map(|(_, y)| y).sum::<f64>() / period as f64;
if avg.is_finite() {
pts.push((pairs[i].0, avg));
ys.push(pairs[i].1);
yhat.push(avg);
}
}
if pts.len() < 2 {
return None;
}
(Model::Moving(pts), ys, yhat)
fit_moving_avg(&pairs, period)?
}
};
if yhat.iter().any(|v| !v.is_finite()) {
@@ -325,6 +277,89 @@ fn poly_y(coeff: &[f64], x: f64) -> f64 {
acc
}
fn fit_exp(pairs: &[(f64, f64)], intercept: Option<f64>) -> Option<(Model, Vec<f64>, Vec<f64>)> {
let pairs: Vec<(f64, f64)> = pairs.iter().copied().filter(|(_, y)| *y > 0.0).collect();
if pairs.len() < 2 {
return None;
}
let (xs, ln_y): (Vec<f64>, Vec<f64>) = pairs.iter().map(|(x, y)| (*x, y.ln())).unzip();
let fixed = intercept.filter(|c| *c > 0.0).map(f64::ln);
let (ln_c, b) = linreg(&xs, &ln_y, fixed)?;
let c = ln_c.exp();
if !c.is_finite() || c <= 0.0 {
return None;
}
let ys: Vec<f64> = pairs.iter().map(|(_, y)| *y).collect();
let yhat: Vec<f64> = xs.iter().map(|x| c * (b * x).exp()).collect();
Some((Model::Exp { c, b }, ys, yhat))
}
fn fit_power(pairs: &[(f64, f64)], intercept: Option<f64>) -> Option<(Model, Vec<f64>, Vec<f64>)> {
let pairs: Vec<(f64, f64)> = pairs.iter().copied().filter(|(x, y)| *x > 0.0 && *y > 0.0).collect();
if pairs.len() < 2 {
return None;
}
let (ln_x, ln_y): (Vec<f64>, Vec<f64>) = pairs.iter().map(|(x, y)| (x.ln(), y.ln())).unzip();
let fixed = intercept.filter(|c| *c > 0.0).map(f64::ln);
let (ln_c, b) = linreg(&ln_x, &ln_y, fixed)?;
let c = ln_c.exp();
if !c.is_finite() || c <= 0.0 {
return None;
}
let ys: Vec<f64> = pairs.iter().map(|(_, y)| *y).collect();
let yhat: Vec<f64> = pairs.iter().map(|(x, _)| c * x.powf(b)).collect();
Some((Model::Power { c, b }, ys, yhat))
}
fn fit_moving_avg(pairs: &[(f64, f64)], period: usize) -> Option<(Model, Vec<f64>, Vec<f64>)> {
if pairs.len() <= period {
return None;
}
let mut pts = Vec::new();
let mut ys = Vec::new();
let mut yhat = Vec::new();
for i in period - 1..pairs.len() {
let window = &pairs[i + 1 - period..=i];
let avg = window.iter().map(|(_, y)| y).sum::<f64>() / period as f64;
if avg.is_finite() {
pts.push((pairs[i].0, avg));
ys.push(pairs[i].1);
yhat.push(avg);
}
}
if pts.len() < 2 {
return None;
}
Some((Model::Moving(pts), ys, yhat))
}
#[allow(clippy::needless_range_loop)]
fn polyfit_with_intercept(xs: &[f64], ys: &[f64], order: usize, b0: f64) -> Option<Vec<f64>> {
let m = order;
let mut ata = vec![vec![0.0; m]; m];
let mut atb = vec![0.0; m];
for (x, y) in xs.iter().zip(ys) {
let dy = y - b0;
for i in 0..m {
let xi = x.powi((i + 1) as i32);
atb[i] += dy * xi;
for j in i..m {
ata[i][j] += xi * x.powi((j + 1) as i32);
}
}
}
for i in 0..m {
for j in 0..i {
ata[i][j] = ata[j][i];
}
}
let solved = solve(ata, atb)?;
let mut coeff = Vec::with_capacity(order + 1);
coeff.push(b0);
coeff.extend(solved);
coeff.iter().all(|v| v.is_finite()).then_some(coeff)
}
/// Polynomial through the normal equations. `x` is centred when the intercept is free, then
/// expanded back to powers of `x`, so order 6 on category numbers stays solvable.
#[allow(clippy::needless_range_loop)] // the index is the power in the normal equations
@@ -334,29 +369,7 @@ fn polyfit(xs: &[f64], ys: &[f64], order: usize, intercept: Option<f64>) -> Opti
return None;
}
if let Some(b0) = intercept {
let m = order;
let mut ata = vec![vec![0.0; m]; m];
let mut atb = vec![0.0; m];
for (x, y) in xs.iter().zip(ys) {
let dy = y - b0;
for i in 0..m {
let xi = x.powi((i + 1) as i32);
atb[i] += dy * xi;
for j in i..m {
ata[i][j] += xi * x.powi((j + 1) as i32);
}
}
}
for i in 0..m {
for j in 0..i {
ata[i][j] = ata[j][i];
}
}
let solved = solve(ata, atb)?;
let mut coeff = Vec::with_capacity(order + 1);
coeff.push(b0);
coeff.extend(solved);
return coeff.iter().all(|v| v.is_finite()).then_some(coeff);
return polyfit_with_intercept(xs, ys, order, b0);
}
let xm = xs.iter().sum::<f64>() / n as f64;
let us: Vec<f64> = xs.iter().map(|x| x - xm).collect();
+2 -2
View File
@@ -2,7 +2,7 @@
//! (RFC 6716 §4.3.4 – §4.3.6).
use super::rate::{Mode, QTHETA_OFFSET, QTHETA_OFFSET_TWOPHASE, get_pulses};
use super::tables::*;
use super::tables::{E_MEANS, EBANDS, NB_EBANDS, ORDERY, SHORT_MDCT};
use crate::range::{BITRES, RangeDecoder, ilog};
pub const SPREAD_NONE: usize = 0;
@@ -71,7 +71,7 @@ fn exp_rotation1(x: &mut [f32], len: usize, stride: usize, c: f32, s: f32) {
x[i + stride] = c * x2 + s * x1;
x[i] = c * x1 + ms * x2;
}
if len >= 2 * stride + 1 {
if len > 2 * stride {
for i in (0..=len - 2 * stride - 1).rev() {
let x1 = x[i];
let x2 = x[i + stride];
+1 -1
View File
@@ -2,7 +2,7 @@
//! (RFC 6716 §4.3.2).
use super::rate::MAX_FINE_BITS;
use super::tables::*;
use super::tables::{BETA_COEF, BETA_INTRA, E_PROB_MODEL, NB_EBANDS, PRED_COEF, SMALL_ENERGY_ICDF};
use crate::range::RangeDecoder;
const LAPLACE_MINP: u32 = 1;
+2 -2
View File
@@ -12,7 +12,7 @@ use bands::{anti_collapse, denormalise_bands, quant_all_bands, renormalise_vecto
use energy::{unquant_coarse_energy, unquant_energy_finalise, unquant_fine_energy};
use mdct::ImdctScratch;
use rate::{Mode, compute_allocation};
use tables::*;
use tables::{COMB_GAINS, EBANDS, MAX_LM, NB_EBANDS, OVERLAP, PREEMPH, SHORT_MDCT, SPREAD_ICDF, TAPSET_ICDF, TF_SELECT_TABLE, TRIM_ICDF};
use crate::range::{BITRES, RangeDecoder};
@@ -578,7 +578,7 @@ fn tf_decode(start: usize, end: usize, is_transient: bool, tf_res: &mut [i32; NB
let mut budget = dec.storage() as i32 * 8;
let mut tell = dec.tell();
let mut logp: u32 = if is_transient { 2 } else { 4 };
let tf_select_rsv = lm > 0 && tell + logp as i32 + 1 <= budget;
let tf_select_rsv = lm > 0 && tell + (logp as i32) < budget;
budget -= tf_select_rsv as i32;
let mut tf_changed = 0;
let mut curr = 0;
+1 -1
View File
@@ -1,6 +1,6 @@
//! Pulse cache and bit allocation (RFC 6716 §4.3.3, §4.3.4.1).
use super::tables::*;
use super::tables::{ALLOC, EBANDS, LOG2_FRAC_TABLE, MAX_LM, NB_EBANDS, OVERLAP};
use crate::range::{BITRES, RangeDecoder};
const MAX_PSEUDO: i32 = 40;
+1 -1
View File
@@ -217,7 +217,7 @@ impl StreamDecoder {
}
}
}
let data_len = data.map_or(0, |d| d.len());
let data_len = data.map_or(0, <[_]>::len);
let mut dec_storage;
let mut dec: Option<&mut RangeDecoder> = match data {
Some(d) => {
+4 -1
View File
@@ -1,7 +1,10 @@
//! Fixed-point NLSF decoding, NLSF→LPC conversion and filter stabilisation
//! (RFC 6716 §4.2.7.5, with the RFC 8251 §6/§7 overflow fixes).
use super::tables::*;
use super::tables::{
COS_Q12, LSF_ORDER_NBMB, LSF_ORDER_WB, NLSF_CB_NBMB, NLSF_CB_WB, NLSF_MIN_SPACING_NBMB, NLSF_MIN_SPACING_WB, NLSF_PRED_NBMB, NLSF_PRED_WB,
NLSF_PSEL_NBMB, NLSF_PSEL_WB,
};
pub const MAX_LPC_ORDER: usize = 16;
+7 -2
View File
@@ -9,9 +9,14 @@ mod resampler;
#[allow(dead_code)]
pub(crate) mod tables;
use lpc::*;
use lpc::{MAX_LPC_ORDER, bwexpander, div32_varq, inverse32_varq, nlsf_decode, nlsf2a, rshift_round, sat16, smlawb, smulwb, smulww};
use resampler::Resampler;
use tables::*;
use tables::{
CB_NB_10, CB_NB_20, CB_WB_10, CB_WB_20, DELTA_GAIN, EXC_LSB, EXC_SIGN, GAIN_MSB, LBRR_FLAGS, LTP_FILTER, LTP_PERIODICITY, LTP_SCALE, LTP_TAPS_0,
LTP_TAPS_1, LTP_TAPS_2, MID_ONLY, NLSF_EXT, NLSF_INTERP, NLSF_SEL_NBMB, NLSF_SEL_WB, NLSF_STAGE1, NLSF_STAGE2, PITCH_CONTOUR, PITCH_DELTA,
PITCH_HIGH, PITCH_LOW, PULSE_COUNT, RATE_LEVEL, SHELL_2, SHELL_4, SHELL_8, SHELL_16, STEREO_PRED_JOINT, STEREO_WEIGHTS_Q13, TYPE_ACTIVE,
TYPE_INACTIVE, UNIFORM3, UNIFORM4, UNIFORM5, UNIFORM8,
};
use crate::range::RangeDecoder;
use crate::{Error, Result};
+1 -1
View File
@@ -29,4 +29,4 @@ wasm-bindgen-test = "0.3"
# slide renders (the same bytes, in parallel). The root patch selects the same pinned
# backend as craft-pdf; this dependency only enables its native compression feature.
[target.'cfg(not(target_arch = "wasm32"))'.dependencies]
krilla = { version = "=0.8.2", features = ["rayon"] }
krilla = { version = "=0.8.2", default-features = false, features = ["rayon"] }
+29 -21
View File
@@ -131,6 +131,33 @@ pub fn export(pres: &Presentation, opts: &PdfOptions) -> Result<Vec<u8>> {
write::guard(|| export_inner(pres, opts))
}
fn plan_pages(pres: &Presentation, opts: &PdfOptions, slides: &[usize], sw: f64, sh: f64) -> Vec<Vec<(usize, Rect)>> {
match opts.layout {
PageLayout::Slides => slides.iter().map(|&i| vec![(i, Rect::new(0.0, 0.0, sw, sh))]).collect(),
PageLayout::Notes => {
let b = notes_frames(pres, opts.paper).0;
slides.iter().map(|&i| vec![(i, b)]).collect()
}
PageLayout::Handouts { per_page } => {
let boxes = handout_boxes(per_page, opts.paper, sw / sh);
slides.chunks(boxes.len()).map(|c| c.iter().zip(&boxes).map(|(i, b)| (*i, *b)).collect()).collect()
}
}
}
fn build_outline(pres: &Presentation, pages: &[Vec<(usize, Rect)>]) -> Outline {
let mut outline = Outline::new();
for (p, v) in pages.iter().enumerate() {
for (i, _) in v {
let t = pres.slides[*i].title();
let t = t.lines().next().unwrap_or("").trim().to_string();
let label = if t.is_empty() { format!("Slide {}", i + 1) } else { format!("{}. {t}", i + 1) };
outline.push_child(OutlineNode::new(label, XyzDestination::new(p, Point::from_xy(0.0, 0.0))));
}
}
outline
}
fn export_inner(pres: &Presentation, opts: &PdfOptions) -> Result<Vec<u8>> {
let slides: Vec<usize> = match &opts.slides {
Some(v) => v.iter().copied().filter(|i| *i < pres.slides.len()).collect(),
@@ -153,29 +180,10 @@ fn export_inner(pres: &Presentation, opts: &PdfOptions) -> Result<Vec<u8>> {
let (sw, sh) = (pres.slide_size.width.max(1.0), pres.slide_size.height.max(1.0));
let mut ex = Exporter { pres, opts, fonts: HashMap::new() };
// Page plan: per page, the slides on it with their boxes (page points).
let pages: Vec<Vec<(usize, Rect)>> = match opts.layout {
PageLayout::Slides => slides.iter().map(|&i| vec![(i, Rect::new(0.0, 0.0, sw, sh))]).collect(),
PageLayout::Notes => {
let b = notes_frames(pres, opts.paper).0;
slides.iter().map(|&i| vec![(i, b)]).collect()
}
PageLayout::Handouts { per_page } => {
let boxes = handout_boxes(per_page, opts.paper, sw / sh);
slides.chunks(boxes.len()).map(|c| c.iter().zip(&boxes).map(|(i, b)| (*i, *b)).collect()).collect()
}
};
let pages = plan_pages(pres, opts, &slides, sw, sh);
// Bookmarks and slide → page lookup for internal links.
let page_of: HashMap<usize, usize> = pages.iter().enumerate().flat_map(|(p, v)| v.iter().map(move |(i, _)| (*i, p))).collect();
let mut outline = Outline::new();
for (p, v) in pages.iter().enumerate() {
for (i, _) in v {
let t = pres.slides[*i].title();
let t = t.lines().next().unwrap_or("").trim().to_string();
let label = if t.is_empty() { format!("Slide {}", i + 1) } else { format!("{}. {t}", i + 1) };
outline.push_child(OutlineNode::new(label, XyzDestination::new(p, Point::from_xy(0.0, 0.0))));
}
}
pdf.set_outline(outline);
pdf.set_outline(build_outline(pres, &pages));
for v in &pages {
let (pw, ph) = match opts.layout {
+1 -1
View File
@@ -437,7 +437,7 @@ fn next_rid(rels: &str) -> String {
let mut max = 0u32;
let mut rest = rels;
while let Some(i) = rest.find("rId") {
let digits: String = rest[i + 3..].chars().take_while(|c| c.is_ascii_digit()).collect();
let digits: String = rest[i + 3..].chars().take_while(char::is_ascii_digit).collect();
if let Ok(n) = digits.parse::<u32>() {
max = max.max(n);
}
+2 -1
View File
@@ -696,6 +696,7 @@ fn draw_inner(ctx: &mut RenderContext, rctx: &Ctx, c: &Chart, m: Affine, w: f64,
let stacked = kind.stacked();
let area_kind = matches!(kind, ChartType::Area | ChartType::StackedArea | ChartType::FilledRadar);
let col = if area_kind { series_fill(rctx, c, si, Some(s)) } else { series_line(rctx, c, si, s) };
let series_stats = s.error_bars.as_ref().map(|_| deckcraft_model::chart::SeriesStats::of(&s.values));
let mut pts = vec![];
for i in 0..ncat {
let Some(v) = s.values.get(i).copied().flatten() else { continue };
@@ -719,7 +720,7 @@ fn draw_inner(ctx: &mut RenderContext, rctx: &Ctx, c: &Chart, m: Affine, w: f64,
}
_ => plot.x0 + band * (slot(i) + 0.5),
};
if let Some((lo, hi)) = s.error_bars.as_ref().and_then(|e| e.range(&s.values, i, v)) {
if let Some((lo, hi)) = s.error_bars.as_ref().and_then(|e| series_stats.as_ref().and_then(|st| e.range_in(st, i, v))) {
err_marks.push((x, to_axis(sec, lo), to_axis(sec, hi), base * 0.4, s.error_bars.as_ref().is_some_and(|e| e.end_cap)));
}
pts.push(Point::new(x, to_axis(sec, v)));
+1 -1
View File
@@ -76,7 +76,7 @@ impl Anim {
static CACHE: Mutex<Option<Cache>> = Mutex::new(None);
fn with<R>(f: impl FnOnce(&mut Cache) -> R) -> R {
let mut g = CACHE.lock().unwrap_or_else(|e| e.into_inner());
let mut g = CACHE.lock().unwrap_or_else(std::sync::PoisonError::into_inner);
f(g.get_or_insert_with(Cache::default))
}
+1 -1
View File
@@ -208,7 +208,7 @@ pub(crate) fn draw(
}
}
let unmatched_b =
tb.iter().enumerate().filter(|(j, _)| pairs.get(*j).is_none_or(|p| p.is_none())).flat_map(|(_, tok)| tok.glyphs.iter().copied());
tb.iter().enumerate().filter(|(j, _)| pairs.get(*j).is_none_or(Option::is_none)).flat_map(|(_, tok)| tok.glyphs.iter().copied());
for g in unmatched_b.chain(loose_b) {
if let Some((run, gid, pos)) = glyph_at(&lb, g) {
draw_glyph(ctx, run, gid, place_b * Affine::translate(pos.to_vec2()), t);
+5 -6
View File
@@ -572,11 +572,10 @@ fn alpha_num(n: u32, upper: bool) -> String {
if n == 0 {
return String::new();
}
let mut n = n - 1;
let n = n - 1;
let c = (b'a' + (n % 26) as u8) as char;
let reps = n / 26 + 1;
n = reps;
let s: String = std::iter::repeat_n(c, n as usize).collect();
let reps = (n / 26 + 1).min(50);
let s: String = std::iter::repeat_n(c, reps as usize).collect();
if upper { s.to_uppercase() } else { s }
}
@@ -1090,7 +1089,7 @@ fn shape_item(
for g in &glyphs {
let Some(&h) = char_at_byte.get(&g.cluster) else { continue };
let Some(cell) = cells.get_mut(h) else { continue };
if matches!(cell.ch, '\t' | '\u{b}' | '\n') {
if matches!(cell.ch, '\t'..='\u{b}') {
continue;
}
let s = scale_of(cell);
@@ -1738,7 +1737,7 @@ fn tab_segment(cells: &[Cell], i: usize) -> (f64, f64) {
let mut w = 0.0;
let mut dec = None;
for c in cells.iter().skip(i + 1) {
if matches!(c.ch, '\t' | '\u{b}' | '\n') {
if matches!(c.ch, '\t'..='\u{b}') {
break;
}
if c.ch == '.' && dec.is_none() {
+1
View File
@@ -394,6 +394,7 @@ fn autonum_formats() {
assert_eq!(autonum("alphaLcParenR", 2), "b)");
assert_eq!(autonum("arabicParenBoth", 1), "(1)");
assert_eq!(autonum("alphaUcPeriod", 27), "AA.");
assert_eq!(autonum("alphaUcPeriod", u32::MAX).len(), 51); // 50 letters + '.'
}
#[test]
+3 -3
View File
@@ -572,8 +572,8 @@ fn template_tiles(app: &mut SlideApp, ui: &mut Ui, query: &str, limit: usize) ->
/// File name and folder of a recent path, split as text (nothing on disk is read).
pub fn split_path(path: &str) -> (String, String) {
let p = std::path::Path::new(path);
let name = p.file_name().and_then(|n| n.to_str()).unwrap_or(path).to_string();
let folder = p.parent().and_then(|f| f.to_str()).unwrap_or("").to_string();
let name = p.file_name().and_then(std::ffi::OsStr::to_str).unwrap_or(path).to_string();
let folder = p.parent().and_then(std::path::Path::to_str).unwrap_or("").to_string();
(name, folder)
}
@@ -827,7 +827,7 @@ fn info(app: &mut SlideApp, ui: &mut Ui, bs: &mut Backstage) {
app.set_status("Copied the path");
}
#[cfg(not(target_arch = "wasm32"))]
if let Some(dir) = std::path::Path::new(p).parent().and_then(|d| d.to_str())
if let Some(dir) = std::path::Path::new(p).parent().and_then(std::path::Path::to_str)
&& ui.button("Open file location").clicked()
{
app.open_url(&format!("file:///{}", dir.replace('\\', "/").trim_start_matches('/')));
+4 -5
View File
@@ -272,7 +272,7 @@ fn pick_album_picture(app: &mut SlideApp) -> Option<Value> {
app.set_status("That picture is larger than 32 MB");
return None;
}
let name = std::path::Path::new(&path).file_name().and_then(|n| n.to_str()).filter(|s| !s.is_empty()).unwrap_or("picture").to_string();
let name = std::path::Path::new(&path).file_name().and_then(std::ffi::OsStr::to_str).filter(|s| !s.is_empty()).unwrap_or("picture").to_string();
Some(json!({"name": name, "data": deckcraft_engine::cmd::base64_encode(&loaded)}))
}
@@ -2027,7 +2027,7 @@ fn body(app: &mut SlideApp, ui: &mut Ui, d: &mut Dialog) -> bool {
});
ui.horizontal(|ui| {
if ui.button("+ Row").clicked() {
let n = grid.first().map(|r| r.len()).unwrap_or(2);
let n = grid.first().map(Vec::len).unwrap_or(2);
grid.push(vec![String::new(); n]);
}
if ui.button("+ Series").clicked() {
@@ -2943,7 +2943,7 @@ fn paragraph_return_params(d: &mut Dialog) -> Value {
let line_value = finite_pt(&d.get("lineValue", "1")).unwrap_or(1.0);
let before_same = d.get("spaceBefore", "") == d.get("spaceBeforeSeed", "");
let after_same = d.get("spaceAfter", "") == d.get("spaceAfterSeed", "");
let v = json!({
json!({
"align": d.get("align", "left"),
"indentLeft": before,
"indentRight": after,
@@ -2957,8 +2957,7 @@ fn paragraph_return_params(d: &mut Dialog) -> Value {
"lineValue": line_value,
"tabs": d.params.get("tabs").cloned().unwrap_or_else(|| json!([])),
"defaultTab": d.params.get("defaultTab").cloned().unwrap_or_else(|| json!(72.0)),
});
v
})
}
fn line_preset_label(kind: &str, value: &str) -> &'static str {
+34 -26
View File
@@ -1349,6 +1349,38 @@ pub fn table_grid_picker(ui: &mut Ui) -> Option<(usize, usize)> {
if resp.clicked() && hover.0 > 0 { Some(hover) } else { None }
}
fn render_icon_categories(app: &mut SlideApp, ui: &mut Ui, icons: &[deckcraft_engine::icons::Icon], color: Color32, hover: Color32) {
let mut cats: Vec<&str> = Vec::new();
for icon in icons {
if !cats.contains(&icon.category) {
cats.push(icon.category);
}
}
for cat in cats {
ui.label(egui::RichText::new(cat).small().strong());
ui.horizontal_wrapped(|ui| {
for icon in icons.iter().filter(|i| i.category == cat) {
icon_cell(app, ui, icon, color, hover);
}
});
}
}
fn render_filtered_icons(app: &mut SlideApp, ui: &mut Ui, icons: &[deckcraft_engine::icons::Icon], q: &str, color: Color32, hover: Color32) {
let mut n = 0;
ui.horizontal_wrapped(|ui| {
for icon in icons {
if icon.name.contains(q) || icon.label.to_lowercase().contains(q) {
icon_cell(app, ui, icon, color, hover);
n += 1;
}
}
});
if n == 0 {
ui.label("No icons match.");
}
}
/// Lucide stroke icons (Insert › Icons). Search filters the gallery; a click inserts the outline.
fn icon_library(app: &mut SlideApp, ui: &mut Ui) {
let t = Tokens::get(ui.ctx());
@@ -1361,33 +1393,9 @@ fn icon_library(app: &mut SlideApp, ui: &mut Ui) {
let icons = deckcraft_engine::icons::icons();
egui::ScrollArea::vertical().id_salt("icon-scroll").max_height(420.0).show(ui, |ui| {
if q.is_empty() {
let mut cats: Vec<&str> = Vec::new();
for icon in icons {
if !cats.contains(&icon.category) {
cats.push(icon.category);
}
}
for cat in cats {
ui.label(egui::RichText::new(cat).small().strong());
ui.horizontal_wrapped(|ui| {
for icon in icons.iter().filter(|i| i.category == cat) {
icon_cell(app, ui, icon, color, hover);
}
});
}
render_icon_categories(app, ui, icons, color, hover);
} else {
let mut n = 0;
ui.horizontal_wrapped(|ui| {
for icon in icons {
if icon.name.contains(&q) || icon.label.to_lowercase().contains(&q) {
icon_cell(app, ui, icon, color, hover);
n += 1;
}
}
});
if n == 0 {
ui.label("No icons match.");
}
render_filtered_icons(app, ui, icons, &q, color, hover);
}
});
}
+1 -1
View File
@@ -29,7 +29,7 @@ impl<'a> BoolDecoder<'a> {
fn fill(&mut self) {
// Room for whole bytes below the top 8 bits and the `count` valid bits.
let mut shift = 48 - self.count;
if let Some(&bytes) = self.data.get(self.pos..).and_then(|d| d.first_chunk::<8>()) {
if let Some(&bytes) = self.data.get(self.pos..).and_then(<[_]>::first_chunk::<8>) {
let v = u64::from_be_bytes(bytes);
let n = ((shift + 8) / 8) as usize; // bytes that fit
let n = n.min(7);
+2 -2
View File
@@ -8,7 +8,7 @@ use crate::frame::{BAND_PAD, Band, Frame, FrameInfo, MiGrid, MiInfo, Pools};
use crate::header::{FrameHeader, HeaderState, KEY_FRAME, RefInfo, parse_compressed, parse_uncompressed, split_superframe};
use crate::loopfilter::{LfFrame, PlaneView, filter_superblock};
use crate::probs::{Counts, adapt_coef_probs, adapt_noncoef_probs};
use crate::tables::*;
use crate::tables::{AC_QLOOKUP, DC_QLOOKUP, SEG_LVL_ALT_Q, SWITCHABLE, TX_MODE_SELECT};
use crate::tile::{FrameShared, RefUse, Strip, TileDecoder};
use crate::{ColorInfo, Picture, Plane};
use std::collections::VecDeque;
@@ -347,7 +347,7 @@ impl Decoder {
let sz = if last {
data.len() - pos
} else {
let Some(&b) = data.get(pos..).and_then(|d| d.first_chunk::<4>()) else {
let Some(&b) = data.get(pos..).and_then(<[_]>::first_chunk::<4>) else {
return Err(Error::Invalid("tile size beyond frame data".into()));
};
let s = u32::from_be_bytes(b) as usize;
+4 -1
View File
@@ -4,7 +4,10 @@
use crate::boolcoder::BoolDecoder;
use crate::error::{Error, Result, ensure};
use crate::probs::FrameContext;
use crate::tables::*;
use crate::tables::{
ALTREF_FRAME, GOLDEN_FRAME, INV_MAP_TABLE, LAST_FRAME, LITERAL_TO_TYPE, MAX_SEGMENTS, MAX_TILE_WIDTH_B64, MIN_TILE_WIDTH_B64,
SEGMENTATION_FEATURE_BITS, SEGMENTATION_FEATURE_SIGNED, SWITCHABLE, TX_MODE_SELECT, TX_MODE_TO_BIGGEST_TX_SIZE,
};
use deckcraft_bitstream::BitReader;
pub const KEY_FRAME: u8 = 0;
+4 -4
View File
@@ -148,7 +148,7 @@ fn filter_h_n<const W: usize>(s: &[u16], f: &[i16], max: i32, o: &mut [u16]) {
let mut acc = [64u16; W];
for t in 0..8 {
let c = f[t] as u16;
let Some(p) = s.get(t..).and_then(|s| s.first_chunk::<W>()) else {
let Some(p) = s.get(t..).and_then(<[_]>::first_chunk::<W>) else {
return;
};
for j in 0..W {
@@ -161,7 +161,7 @@ fn filter_h_n<const W: usize>(s: &[u16], f: &[i16], max: i32, o: &mut [u16]) {
let mut acc = [64i32; W];
for t in 0..8 {
let c = f[t] as i32;
let Some(p) = s.get(t..).and_then(|s| s.first_chunk::<W>()) else {
let Some(p) = s.get(t..).and_then(<[_]>::first_chunk::<W>) else {
return;
};
for j in 0..W {
@@ -212,7 +212,7 @@ fn filter_v_n<const W: usize>(s: &[u16], ss: usize, f: &[i16], max: i32, o: &mut
let mut acc = [64u16; W];
for t in 0..8 {
let c = f[t] as u16;
let Some(p) = s.get(t * ss..).and_then(|s| s.first_chunk::<W>()) else {
let Some(p) = s.get(t * ss..).and_then(<[_]>::first_chunk::<W>) else {
return;
};
for j in 0..W {
@@ -225,7 +225,7 @@ fn filter_v_n<const W: usize>(s: &[u16], ss: usize, f: &[i16], max: i32, o: &mut
let mut acc = [64i32; W];
for t in 0..8 {
let c = f[t] as i32;
let Some(p) = s.get(t * ss..).and_then(|s| s.first_chunk::<W>()) else {
let Some(p) = s.get(t * ss..).and_then(<[_]>::first_chunk::<W>) else {
return;
};
for j in 0..W {
+1 -1
View File
@@ -1,6 +1,6 @@
//! Intra prediction process (8.5.1).
use crate::tables::*;
use crate::tables::{D45_PRED, D63_PRED, D117_PRED, D135_PRED, D153_PRED, D207_PRED, H_PRED, TM_PRED, V_PRED};
/// Edge availability and geometry of one intra-predicted transform block.
pub struct IntraEdge {
+4 -1
View File
@@ -2,7 +2,10 @@
use crate::frame::{MiGrid, MiInfo};
use crate::header::{LoopFilterParams, Segmentation};
use crate::tables::*;
use crate::tables::{
BLOCK_8X8, BLOCK_16X16, INTRA_FRAME, MAX_LOOP_FILTER, MAX_TXSIZE, NEARESTMV, NEARMV, NEWMV, NUM_8X8_HIGH, NUM_8X8_WIDE, SEG_LVL_ALT_L,
SS_SIZE_LOOKUP, TX_4X4, TX_8X8, TX_16X16,
};
/// Frame-level loop filter setup (8.8.1) plus the per-level limits of 8.8.4.
pub struct LfFrame {
+8 -1
View File
@@ -1,7 +1,14 @@
//! Probability tables (a "frame context"), syntax element counters and the backward adaptation
//! processes of section 8.4.
use crate::tables::*;
use crate::tables::{
BINARY_TREE, COUNT_SAT, DEFAULT_COEF_PROBS, DEFAULT_COMP_MODE_PROB, DEFAULT_COMP_REF_PROB, DEFAULT_INTER_MODE_PROBS, DEFAULT_INTERP_FILTER_PROBS,
DEFAULT_IS_INTER_PROB, DEFAULT_MV_BITS_PROB, DEFAULT_MV_CLASS_PROBS, DEFAULT_MV_CLASS0_BIT_PROB, DEFAULT_MV_CLASS0_FR_PROBS,
DEFAULT_MV_CLASS0_HP_PROB, DEFAULT_MV_FR_PROBS, DEFAULT_MV_HP_PROB, DEFAULT_MV_JOINT_PROBS, DEFAULT_MV_SIGN_PROB, DEFAULT_PARTITION_PROBS,
DEFAULT_SINGLE_REF_PROB, DEFAULT_SKIP_PROB, DEFAULT_TX_PROBS, DEFAULT_UV_MODE_PROBS, DEFAULT_Y_MODE_PROBS, INTER_MODE_TREE, INTERP_FILTER_TREE,
INTRA_MODE_TREE, MAX_UPDATE_FACTOR, MV_CLASS_TREE, MV_FR_TREE, MV_JOINT_TREE, PARTITION_TREE, SMALL_TOKEN_TREE, TX_SIZE_8_TREE, TX_SIZE_16_TREE,
TX_SIZE_32_TREE,
};
/// coef_probs[txSz][plane > 0][is_inter][band][ctx][node].
pub type CoefProbs = [[[[[[u8; 3]; 6]; 6]; 2]; 2]; 4];
+8 -1
View File
@@ -6,7 +6,14 @@ use crate::boolcoder::BoolDecoder;
use crate::frame::{Frame, MiGrid, MiInfo, Mv};
use crate::header::{FrameHeader, Segmentation};
use crate::probs::{Counts, FrameContext};
use crate::tables::*;
use crate::tables::{
ALTREF_FRAME, B_HEIGHT_LOG2, B_WIDTH_LOG2, BLOCK_8X8, BLOCK_64X64, BLOCK_INVALID, BORDERINPIXELS, COMPANDED_MVREF_THRESH, COUNTER_TO_CONTEXT,
DC_PRED, GOLDEN_FRAME, IDX_N_COLUMN_TO_SUBBLOCK, INTER_MODE_TREE, INTERP_EXTEND, INTERP_FILTER_TREE, INTRA_FRAME, INTRA_MODE_TREE,
KF_PARTITION_PROBS, KF_UV_MODE_PROBS, KF_Y_MODE_PROBS, LAST_FRAME, MAX_TXSIZE, MI_WIDTH_LOG2, MODE_2_COUNTER, MV_BORDER, MV_CLASS_TREE,
MV_FR_TREE, MV_JOINT_TREE, MV_REF_BLOCKS, NEARESTMV, NEARMV, NEWMV, NONE, NUM_4X4_HIGH, NUM_4X4_WIDE, NUM_8X8_HIGH, NUM_8X8_WIDE, PARTITION_TREE,
SEG_LVL_REF_FRAME, SEG_LVL_SKIP, SEGMENT_TREE, SIZE_GROUP, SS_SIZE_LOOKUP, SUBSIZE_LOOKUP, SWITCHABLE, TX_MODE_SELECT,
TX_MODE_TO_BIGGEST_TX_SIZE, TX_SIZE_8_TREE, TX_SIZE_16_TREE, TX_SIZE_32_TREE, ZEROMV,
};
mod recon;
+4 -1
View File
@@ -7,7 +7,10 @@ use crate::error::Error;
use crate::frame::Mv;
use crate::inter::{self, RefPlane};
use crate::intra::{self, IntraEdge};
use crate::tables::*;
use crate::tables::{
BLOCK_8X8, CAT_PROBS, COEFBAND_4X4, COEFBAND_8X8PLUS, DCT_DCT, ENERGY_CLASS, EXTRA_BITS, INTERP_EXTEND, INTRA_FRAME, MAX_TXSIZE, MODE2TXFM,
NUM_4X4_HIGH, NUM_4X4_WIDE, NUM_8X8_HIGH, NUM_8X8_WIDE, SS_SIZE_LOOKUP, TX_4X4, TX_32X32, neighbors, pareto_full, scan,
};
use crate::transform;
/// round_mv_comp_q2 / q4 (8.5.2.1).
+3 -3
View File
@@ -161,7 +161,7 @@ pub fn inverse_transform_add(
if lossless {
let mut t = [0i32; 16];
for i in 0..4 {
let c = coefs.get(i * 4..).and_then(|c| c.first_chunk::<4>()).copied().unwrap_or_default();
let c = coefs.get(i * 4..).and_then(<[_]>::first_chunk::<4>).copied().unwrap_or_default();
let r = iwht4(c, 2);
t[i * 4..i * 4 + 4].copy_from_slice(&r);
}
@@ -235,7 +235,7 @@ fn itx_narrow<const N: usize>(coefs: &[i32], p: &TxParams, dst: &mut [u16], stri
add_cols(&y, 0, p, dst, stride);
} else {
for c0 in (0..N).step_by(8) {
let x: [Cols<8>; N] = std::array::from_fn(|i| Cols(t[i].get(c0..).and_then(|r| r.first_chunk::<8>()).copied().unwrap_or_default()));
let x: [Cols<8>; N] = std::array::from_fn(|i| Cols(t[i].get(c0..).and_then(<[_]>::first_chunk::<8>).copied().unwrap_or_default()));
let y = col_tx8(x, p.col_adst);
add_cols(&y, c0, p, dst, stride);
}
@@ -246,7 +246,7 @@ fn itx_narrow<const N: usize>(coefs: &[i32], p: &TxParams, dst: &mut [u16], stri
#[inline(always)]
fn add_cols<const N: usize, const W: usize>(y: &[Cols<W>; N], c0: usize, p: &TxParams, dst: &mut [u16], stride: usize) {
for (i, v) in y.iter().enumerate() {
let Some(d) = dst.get_mut(i * stride + c0..).and_then(|d| d.first_chunk_mut::<W>()) else {
let Some(d) = dst.get_mut(i * stride + c0..).and_then(<[_]>::first_chunk_mut::<W>) else {
return;
};
for j in 0..W {
+10 -4
View File
@@ -19,6 +19,12 @@ set -euo pipefail
HERE="$ROOT/packaging/linux"
APP_ID=ai.storyteller.deckcraft
verify_sha256() {
local expected="$1"
local file="$2"
printf '%s %s\n' "$expected" "$file" | sha256sum -c -
}
SKIP_BUILD=0
FORMATS="appimage deb rpm tar"
while [[ $# -gt 0 ]]; do
@@ -137,21 +143,21 @@ if has appimage; then
download="$TOOL.download"
curl --proto '=https' -fsSL -o "$download" \
"https://github.com/AppImage/appimagetool/releases/download/$APPIMAGETOOL_VERSION/appimagetool-$ARCH.AppImage"
printf '%s %s\n' "$APPIMAGETOOL_SHA256" "$download" | sha256sum -c -
verify_sha256 "$APPIMAGETOOL_SHA256" "$download"
chmod 755 "$download"
mv "$download" "$TOOL"
fi
printf '%s %s\n' "$APPIMAGETOOL_SHA256" "$TOOL" | sha256sum -c -
verify_sha256 "$APPIMAGETOOL_SHA256" "$TOOL"
fi
RUNTIME="$CARGO_TARGET_DIR/appimage-runtime-$APPIMAGE_RUNTIME_VERSION-$ARCH"
if [[ ! -f "$RUNTIME" ]]; then
download="$RUNTIME.download"
curl --proto '=https' -fsSL -o "$download" \
"https://github.com/AppImage/type2-runtime/releases/download/$APPIMAGE_RUNTIME_VERSION/runtime-$ARCH"
printf '%s %s\n' "$APPIMAGE_RUNTIME_SHA256" "$download" | sha256sum -c -
verify_sha256 "$APPIMAGE_RUNTIME_SHA256" "$download"
mv "$download" "$RUNTIME"
fi
printf '%s %s\n' "$APPIMAGE_RUNTIME_SHA256" "$RUNTIME" | sha256sum -c -
verify_sha256 "$APPIMAGE_RUNTIME_SHA256" "$RUNTIME"
# Absolute, because appimagetool runs in $DIST below (CARGO_TARGET_DIR or APPIMAGETOOL may be
# relative).
OUT="$(cd "$DIST" && pwd)/$BASENAME.AppImage"
+6 -4
View File
@@ -17,12 +17,14 @@ if [[ -z "${APPLE_CERTIFICATE:-}" ]]; then
exit 0
fi
TMP="${RUNNER_TEMP:-${TMPDIR:-/tmp}}"
KEYCHAIN="$TMP/deckcraft-signing.keychain-db"
CERT="$TMP/deckcraft-signing.p12"
TMP_BASE="${RUNNER_TEMP:-${TMPDIR:-/tmp}}"
TMP_DIR="$(mktemp -d "$TMP_BASE/deckcraft-cert.XXXXXX")"
chmod 0700 "$TMP_DIR"
KEYCHAIN="$TMP_DIR/deckcraft-signing.keychain-db"
CERT="$TMP_DIR/deckcraft-signing.p12"
KC_PASS="${KEYCHAIN_PASSWORD:-$(openssl rand -hex 24)}"
trap 'rm -f "$CERT"' EXIT
trap 'rm -rf "$TMP_DIR"' EXIT
printf '%s' "$APPLE_CERTIFICATE" | base64 --decode >"$CERT"
security create-keychain -p "$KC_PASS" "$KEYCHAIN"
security set-keychain-settings -lut 21600 "$KEYCHAIN"
+1 -1
View File
@@ -51,7 +51,7 @@ const ASSET_EXT: &[&str] = &[
const ASSET_DIRS: &[&str] = &["assets/", "docs/images/", "docs/brand/", "examples/"];
pub fn is_asset(path: &str) -> bool {
let ext = Path::new(path).extension().and_then(|e| e.to_str()).map(str::to_ascii_lowercase).unwrap_or_default();
let ext = Path::new(path).extension().and_then(std::ffi::OsStr::to_str).map(str::to_ascii_lowercase).unwrap_or_default();
ASSET_DIRS.iter().any(|d| path.starts_with(d)) || ASSET_EXT.contains(&ext.as_str())
}
+1 -1
View File
@@ -207,7 +207,7 @@ pub fn check(crates: &[Crate]) -> Vec<Violation> {
}
}
}
out.sort_by_key(|v| v.to_string());
out.sort_by_key(ToString::to_string);
out.dedup();
out
}
+11 -10
View File
@@ -6,7 +6,7 @@ use std::process::Command;
use serde_json::Value;
const SHARED_REV: &str = "c3487320b2d1de657b860b3c8abb44e1f2b32bd8";
const SHARED_REV: &str = "9f75d3dc19b4b514c4f3d4847ed1ec1b0e86b4ef";
const REGISTRY: &str = "registry+https://github.com/rust-lang/crates.io-index";
pub fn run(root: &Path) -> Result<(), String> {
@@ -72,8 +72,8 @@ fn validate(metadata: &Value) -> Result<(), String> {
};
let source = format!("git+https://github.com/storytold/craft-libs?rev={SHARED_REV}#{SHARED_REV}");
for (name, version, expected_source, expected_features) in [
("craft-pdf", "0.1.0", source.as_str(), vec!["write"]),
("krilla", "0.8.2", source.as_str(), vec!["default", "raster-images", "rayon", "simple-text"]),
("craft-pdf", "0.1.0", source.as_str(), vec!["write", "write-simple-text"]),
("krilla", "0.8.2", source.as_str(), vec!["raster-images", "rayon", "simple-text"]),
("hayro-syntax", "0.7.2", REGISTRY, vec!["std"]),
] {
let package = one_package(name)?;
@@ -119,7 +119,7 @@ fn validate(metadata: &Value) -> Result<(), String> {
}
fn validate_target_features(tree: &str, parallel: bool) -> Result<(), String> {
let mut expected = BTreeSet::from(["default", "raster-images", "simple-text"]);
let mut expected = BTreeSet::from(["raster-images", "simple-text"]);
if parallel {
expected.insert("rayon");
}
@@ -153,8 +153,8 @@ mod tests {
],
"resolve":{"nodes":[
{"id":"app", "deps":[{"pkg":"writer", "dep_kinds":[{"kind":null}]}, {"pkg":"parser", "dep_kinds":[{"kind":"dev"}]}]},
{"id":"writer", "features":["write"], "deps":[{"pkg":"backend", "dep_kinds":[{"kind":null}]}]},
{"id":"backend", "features":["default","raster-images","rayon","simple-text"], "deps":[]},
{"id":"writer", "features":["write","write-simple-text"], "deps":[{"pkg":"backend", "dep_kinds":[{"kind":null}]}]},
{"id":"backend", "features":["raster-images","rayon","simple-text"], "deps":[]},
{"id":"parser", "features":["std"], "deps":[]}
]}
})
@@ -162,8 +162,8 @@ mod tests {
#[test]
fn parallel_compression_is_required_only_on_native_targets() {
let serial = "krilla v0.8.2 (pinned)|default,raster-images,simple-text\n";
let parallel = "krilla v0.8.2 (pinned)|default,raster-images,rayon,simple-text\n";
let serial = "krilla v0.8.2 (pinned)|raster-images,simple-text\n";
let parallel = "krilla v0.8.2 (pinned)|raster-images,rayon,simple-text\n";
assert!(validate_target_features(serial, false).is_ok());
assert!(validate_target_features(parallel, true).is_ok());
assert!(validate_target_features(serial, true).is_err());
@@ -189,8 +189,9 @@ mod tests {
fn rejects_import_features_and_missing_text_shaping() {
for (node, features) in [
(1, json!([])),
(1, json!(["write", "write-pdf"])),
(2, json!(["default", "raster-images", "rayon", "simple-text", "pdf"])),
(1, json!(["write"])),
(1, json!(["write", "write-simple-text", "write-pdf"])),
(2, json!(["raster-images", "rayon", "simple-text", "pdf"])),
(2, json!(["raster-images"])),
(3, json!(["std", "images"])),
] {