diff --git a/jxl/resources/test/GrayscalePatchesModular.jxl b/jxl/resources/test/GrayscalePatchesModular.jxl new file mode 100644 index 0000000..789863e Binary files /dev/null and b/jxl/resources/test/GrayscalePatchesModular.jxl differ diff --git a/jxl/resources/test/GrayscalePatchesVarDCT.jxl b/jxl/resources/test/GrayscalePatchesVarDCT.jxl new file mode 100644 index 0000000..37d134c Binary files /dev/null and b/jxl/resources/test/GrayscalePatchesVarDCT.jxl differ diff --git a/jxl/src/error.rs b/jxl/src/error.rs index dcd448d..e981956 100644 --- a/jxl/src/error.rs +++ b/jxl/src/error.rs @@ -111,6 +111,10 @@ pub enum Error { InvalidPredictor(u32), #[error("Invalid modular mode property: {0}")] InvalidProperty(u32), + #[error("Too many patches: {0}, limit is {1}")] + PatchesTooMany(u32, u32), + #[error("Reference too large: {0}, limit is {1}")] + PatchesRefTooLarge(u32, u32), #[error("Too many splines: {0}, limit is {1}")] SplinesTooMany(u32, u32), #[error("Too many control points for splines: {0}, limit is {1}")] diff --git a/jxl/src/features/mod.rs b/jxl/src/features/mod.rs index 5494033..3fd4e96 100644 --- a/jxl/src/features/mod.rs +++ b/jxl/src/features/mod.rs @@ -3,5 +3,6 @@ // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. +pub mod patches; pub mod noise; pub mod spline; diff --git a/jxl/src/features/patches.rs b/jxl/src/features/patches.rs new file mode 100644 index 0000000..83640d2 --- /dev/null +++ b/jxl/src/features/patches.rs @@ -0,0 +1,130 @@ +// Copyright (c) the JPEG XL Project Authors. All rights reserved. +// +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +// TODO(firsching): remove once we use this! +#![allow(dead_code)] + +use crate::{ + bit_reader::BitReader, + entropy_coding::decode::Histograms, + error::{Error, Result}, + util::tracing_wrappers::*, +}; + +// TODO(firsching): move to some common place? +const MAX_NUM_REFERENCE_FRAMES : u32 = 4; + +// Context numbers as specified in Section C.4.5, Listing C.2: +const NUM_REF_PATCH_CONTEXT: usize = 0; +const REFERENCE_FRAME_CONTEXT: usize = 1; +const PATCH_SIZE_CONTEXT: usize = 2; +const PATCH_REFERENCE_POSITION_CONTEXT: usize = 3; +const PATCH_POSITION_CONTEXT: usize = 4; +const PATCH_BLEND_MODE_CONTEXT: usize = 5; +const PATCH_OFFSET_CONTEXT: usize = 6; +const PATCH_COUNT_CONTEXT: usize = 7; +const PATCH_ALPHA_CHANNEL_CONTEXT: usize = 8; +const PATCH_CLAMP_CONTEXT: usize = 9; +const NUM_PATCH_DICTIONARY_CONTEXTS: usize = 10; + +// Blend modes +// The new values are the old ones. Useful to skip some channels. +const PATCH_BLEND_MODE_NONE: u8 = 0; +// The new values (in the crop) replace the old ones: sample = new +const PATCH_BLEND_MODE_REPLACE: u8 = 1; +// The new values (in the crop) get added to the old ones: sample = old + new +const PATCH_BLEND_MODE_ADD: u8 = 2; +// The new values (in the crop) get multiplied by the old ones: +// sample = old * new +// This blend mode is only supported if BlendColorSpace is kEncoded. The +// range of the new value matters for multiplication purposes, and its +// nominal range of 0..1 is computed the same way as this is done for the +// alpha values in kBlend and kAlphaWeightedAdd. +const PATCH_BLEND_MODE_MUL: u8 = 3; +// The new values (in the crop) replace the old ones if alpha>0: +// For first alpha channel: +// alpha = old + new * (1 - old) +// For other channels if !alpha_associated: +// sample = ((1 - new_alpha) * old * old_alpha + new_alpha * new) / alpha +// For other channels if alpha_associated: +// sample = (1 - new_alpha) * old + new +// The alpha formula applies to the alpha used for the division in the other +// channels formula, and applies to the alpha channel itself if its +// blend_channel value matches itself. +// If using kBlendAbove, new is the patch and old is the original image; if +// using kBlendBelow, the meaning is inverted. +const PATCH_BLEND_MODE_BLEND_ABOVE: u8 = 4; +const PATCH_BLEND_MODE_BLEND_BELOW: u8 = 5; +// The new values (in the crop) are added to the old ones if alpha>0: +// For first alpha channel: sample = sample = old + new * (1 - old) +// For other channels: sample = old + alpha * new +const PATCH_BLEND_MODE_ALPHA_WEIGHTED_ADD_ABOVE: u8 = 6; +const PATCH_BLEND_MODE_ALPHA_WEIGHTED_ADD_BELOW: u8 = 7; +const PATCH_BLEND_MODE_NUM_BLEND_MODES: u8 = 8; + +#[derive(Debug, Clone, Copy)] +struct PatchBlending { + mode: u8, + alpha_channel: u32, + clamp: bool, +} + +#[derive(Debug, Clone, Copy)] +pub struct PatchReferencePosition { + // Not using `ref` like in the spec here, because it is a keyword. + reference: u8, + x0: usize, + y0: usize, + xsize: usize, + ysize: usize, +} + +#[derive(Debug, Clone, Copy, PartialEq)] +pub struct PatchPosition { + x: usize, + y: usize, + ref_pos_idx: usize, +} + +#[derive(Debug, Default)] +pub struct PatchesDictionary { + pub positions: Vec, + pub ref_positions: Vec, + blendings: Vec, + num_patches: Vec, + sorted_patches_y0: Vec<(usize, usize)>, + sorted_patches_y1: Vec<(usize, usize)>, +} + +impl PatchesDictionary { + #[instrument(level = "debug", skip(br), ret, err)] + pub fn read(br: &mut BitReader, xsize: u32, ysize: u32) -> Result { + trace!(pos = br.total_bits_read()); + let patches_histograms = Histograms::decode(NUM_PATCH_DICTIONARY_CONTEXTS, br, true)?; + let mut patches_reader = patches_histograms.make_reader(br)?; + let num_ref_patch = 1 + patches_reader.read(br, NUM_REF_PATCH_CONTEXT)?; + let num_pixels = xsize * ysize; + let max_ref_patches = 1024 + num_pixels / 4; + let max_patches = max_ref_patches * 4; + let _max_blending_infos = max_patches * 4; + if num_ref_patch > max_ref_patches { + return Err(Error::PatchesTooMany(num_ref_patch, max_ref_patches)); + } + + let mut ref_positions: Vec = Vec::with_capacity(num_ref_patch as usize); + for _ in 0..num_ref_patch { + let reference = patches_reader.read(br, REFERENCE_FRAME_CONTEXT)?; + if reference >= MAX_NUM_REFERENCE_FRAMES { + return Err(Error::PatchesRefTooLarge(reference, MAX_NUM_REFERENCE_FRAMES)); + } + + // TODO: fill in correct numbers here + ref_positions.push(PatchReferencePosition {reference:0, x0:0, y0:0, xsize:0, ysize: 0}) + + } + + todo!("implement patch decoding") + } +} diff --git a/jxl/src/features/spline.rs b/jxl/src/features/spline.rs index 09ef836..bff8729 100644 --- a/jxl/src/features/spline.rs +++ b/jxl/src/features/spline.rs @@ -75,7 +75,7 @@ impl QuantizedSpline { max_control_points, )); } - let mut control_points = Vec::with_capacity(num_control_points as usize); + let mut control_points: Vec<(i64, i64)> = Vec::with_capacity(num_control_points as usize); for _ in 0..num_control_points { let x = splines_reader.read_signed(br, CONTROL_POINTS_CONTEXT)? as i64; let y = splines_reader.read_signed(br, CONTROL_POINTS_CONTEXT)? as i64; diff --git a/jxl/src/frame.rs b/jxl/src/frame.rs index f0a6690..ba0555f 100644 --- a/jxl/src/frame.rs +++ b/jxl/src/frame.rs @@ -7,6 +7,7 @@ use crate::{ bit_reader::BitReader, error::Result, features::{noise::Noise, spline::Splines}, + features::patches::PatchesDictionary, headers::{ color_encoding::ColorSpace, encodings::UnconditionalCoder, @@ -32,7 +33,7 @@ pub enum Section { #[allow(dead_code)] pub struct LfGlobalState { - // TODO(veluca93): patches + patches: Option, // TODO(veluca93): splines splines: Option, noise: Option, @@ -135,11 +136,19 @@ impl Frame { assert!(self.lf_global.is_none()); trace!(pos = br.total_bits_read()); - if self.header.has_patches() { + let patches = if self.header.has_patches() { info!("decoding patches"); - todo!("patches not implemented"); - } + Some(PatchesDictionary::read( + br, + self.header.width, + self.header.height, + )?) + } else { + None + }; + let splines = if self.header.has_splines() { + info!("decoding splines"); Some(Splines::read(br, self.header.width * self.header.height)?) } else { None @@ -181,6 +190,7 @@ impl Frame { )?; self.lf_global = Some(LfGlobalState { + patches, splines, noise, lf_quant, @@ -214,6 +224,16 @@ mod test_frame { Ok(result) } + + #[test] + fn patches() -> Result<(), Error> { + let frame = read_frame(include_bytes!("../resources/test/GrayscalePatchesVarDCT.jxl"))?; + let lf_global = frame.lf_global.unwrap(); + let patches_dict = lf_global.patches.unwrap(); + // TODO continue here + Ok(()) + } + #[test] fn splines() -> Result<(), Error> { let frame = read_frame(include_bytes!("../resources/test/splines.jxl"))?;