Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 3 additions & 6 deletions crates/pdf-shading/src/free_form_mesh.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,8 @@ use thiserror::Error;

use crate::{
error::PdfShadingError,
mesh_decoder::{MeshBitWidths, MeshDecoder, read_mesh_bits},
mesh_bit_widths::MeshBitWidths,
mesh_decoder::{MeshDecoder, read_mesh_bits},
model::{MeshTriangle, MeshVertex, Shading},
parse::{parse_functions, required_color_space},
};
Expand Down Expand Up @@ -74,11 +75,7 @@ impl FreeFormMeshConfig {
objects: &dyn ObjectResolver,
) -> Result<Self, PdfShadingError> {
let color_space = required_color_space(dictionary, objects)?;
let widths = MeshBitWidths::new(
dictionary.required_number::<usize>("BitsPerCoordinate", objects)?,
dictionary.required_number::<usize>("BitsPerComponent", objects)?,
dictionary.required_number::<usize>("BitsPerFlag", objects)?,
)?;
let widths = MeshBitWidths::from_dictionary(dictionary, objects)?;
let decode = dictionary.required_vec_of::<f32>("Decode", objects)?;
let bbox = dictionary.optional_bbox(objects)?;
let anti_alias = dictionary.optional_boolean("AntiAlias", objects)?;
Expand Down
1 change: 1 addition & 0 deletions crates/pdf-shading/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ pub mod color_stops;
pub mod error;
mod free_form_mesh;
pub mod mesh;
mod mesh_bit_widths;
mod mesh_decoder;
pub mod model;
pub mod paint;
Expand Down
107 changes: 107 additions & 0 deletions crates/pdf-shading/src/mesh_bit_widths.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
//! Parsing and validation of packed mesh bit widths.

use pdf_object::{
dictionary::Dictionary, object_lookup::ObjectLookupExt, object_resolver::ObjectResolver,
};

use crate::{error::PdfShadingError, mesh_decoder::MeshDecoderError};

/// Coordinate widths permitted by the PDF specification for `/BitsPerCoordinate`.
const VALID_COORDINATE_WIDTHS: [usize; 8] = [1, 2, 4, 8, 12, 16, 24, 32];

/// Color component widths permitted by the PDF specification for `/BitsPerComponent`.
const VALID_COMPONENT_WIDTHS: [usize; 6] = [1, 2, 4, 8, 12, 16];

/// Edge-flag widths permitted by the PDF specification for `/BitsPerFlag`.
const VALID_FLAG_WIDTHS: [usize; 3] = [2, 4, 8];

/// Bit widths used to decode packed fields in a Type 4, 6, or 7 mesh stream.
///
/// Each value is read from the corresponding mesh shading dictionary entry
/// and validated against the widths permitted by the PDF specification.
#[derive(Debug, Clone, Copy)]
pub(crate) struct MeshBitWidths {
/// Number of bits used to encode each coordinate component.
coordinate: usize,
/// Number of bits used to encode each color component or function input.
component: usize,
/// Number of bits used to encode each vertex or patch edge flag.
flag: usize,
}

impl MeshBitWidths {
/// Reads and validates the bit widths declared by a mesh shading dictionary.
///
/// The required `/BitsPerCoordinate`, `/BitsPerComponent`, and
/// `/BitsPerFlag` entries are resolved through `objects` before their
/// values are checked against the widths permitted by the PDF
/// specification.
///
/// Returns an error when an entry is missing, cannot be resolved as a
/// non-negative integer, or specifies an unsupported width.
pub(crate) fn from_dictionary(
dictionary: &Dictionary,
objects: &dyn ObjectResolver,
) -> Result<Self, PdfShadingError> {
let coordinate = dictionary.required_number::<usize>("BitsPerCoordinate", objects)?;
let component = dictionary.required_number::<usize>("BitsPerComponent", objects)?;
let flag = dictionary.required_number::<usize>("BitsPerFlag", objects)?;

validate_allowed_width(
coordinate,
&VALID_COORDINATE_WIDTHS,
MeshDecoderError::InvalidBitsPerCoordinate { value: coordinate },
)?;
validate_allowed_width(
component,
&VALID_COMPONENT_WIDTHS,
MeshDecoderError::InvalidBitsPerComponent { value: component },
)?;
validate_allowed_width(
flag,
&VALID_FLAG_WIDTHS,
MeshDecoderError::InvalidBitsPerFlag { value: flag },
)?;

Ok(Self {
coordinate,
component,
flag,
})
}

/// Returns the number of bits used to encode each coordinate component.
pub(crate) fn coordinate(self) -> usize {
self.coordinate
}

/// Returns the number of bits used to encode each color component or function input.
pub(crate) fn component(self) -> usize {
self.component
}

/// Returns the number of bits used to encode each vertex or patch edge flag.
pub(crate) fn flag(self) -> usize {
self.flag
}
}

/// Checks whether a mesh field width is one of the values permitted for that field.
///
/// Returns `Ok(())` when `width` appears in `allowed`; otherwise converts the
/// field-specific `error` into [`PdfShadingError`].
fn validate_allowed_width(
width: usize,
allowed: &[usize],
error: MeshDecoderError,
) -> Result<(), PdfShadingError> {
if allowed.contains(&width) {
Ok(())
} else {
Err(error.into())
}
}

#[cfg(test)]
#[path = "../tests/mesh_bit_widths.rs"]
mod tests;
122 changes: 26 additions & 96 deletions crates/pdf-shading/src/mesh_decoder.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,7 @@ use pdf_graphics::{color::Color, point::Point};
use pdf_utils::BitReader;
use thiserror::Error;

use crate::error::PdfShadingError;

const VALID_COORDINATE_WIDTHS: [usize; 8] = [1, 2, 4, 8, 12, 16, 24, 32];
const VALID_COMPONENT_WIDTHS: [usize; 6] = [1, 2, 4, 8, 12, 16];
const VALID_FLAG_WIDTHS: [usize; 3] = [2, 4, 8];
use crate::{error::PdfShadingError, mesh_bit_widths::MeshBitWidths};

/// Errors produced while validating and decoding packed mesh samples.
#[derive(Debug, Error, Clone, PartialEq, Eq)]
Expand Down Expand Up @@ -61,50 +57,6 @@ pub enum MeshDecoderError {
SampleRangeNotRepresentable,
}

/// Validated bit widths from a Type 4, 6, or 7 mesh dictionary.
#[derive(Debug, Clone, Copy)]
pub(crate) struct MeshBitWidths {
coordinate: usize,
component: usize,
flag: usize,
}

impl MeshBitWidths {
/// Validates the three PDF mesh bit-width entries.
pub(crate) fn new(
coordinate: usize,
component: usize,
flag: usize,
) -> Result<Self, PdfShadingError> {
validate_allowed_width(
coordinate,
&VALID_COORDINATE_WIDTHS,
MeshDecoderError::InvalidBitsPerCoordinate { value: coordinate },
)?;
validate_allowed_width(
component,
&VALID_COMPONENT_WIDTHS,
MeshDecoderError::InvalidBitsPerComponent { value: component },
)?;
validate_allowed_width(
flag,
&VALID_FLAG_WIDTHS,
MeshDecoderError::InvalidBitsPerFlag { value: flag },
)?;

Ok(Self {
coordinate,
component,
flag,
})
}

/// Returns the width of each edge-flag field.
pub(crate) fn flag(self) -> usize {
self.flag
}
}

/// Decodes mesh coordinates and color inputs according to a `/Decode` array.
///
/// A decoder borrows the parsed dictionary data while a mesh parser owns the
Expand Down Expand Up @@ -159,8 +111,8 @@ impl<'a> MeshDecoder<'a> {
pub(crate) fn read_point(&self, reader: &mut BitReader<'_>) -> Result<Point, PdfShadingError> {
let (x_min, x_max) = decode_pair(self.decode, 0, "X")?;
let (y_min, y_max) = decode_pair(self.decode, 1, "Y")?;
let x = self.read_sample(reader, self.widths.coordinate, x_min, x_max)?;
let y = self.read_sample(reader, self.widths.coordinate, y_min, y_max)?;
let x = self.read_sample(reader, self.widths.coordinate(), x_min, x_max)?;
let y = self.read_sample(reader, self.widths.coordinate(), y_min, y_max)?;
Ok(Point::new(x, y))
}

Expand All @@ -171,27 +123,43 @@ impl<'a> MeshDecoder<'a> {
.map(|component| {
let (min, max) =
decode_pair(self.decode, component.saturating_add(2), "component")?;
self.read_sample(reader, self.widths.component, min, max)
self.read_sample(reader, self.widths.component(), min, max)
})
.collect::<Result<Vec<_>, PdfShadingError>>()?;

let components = self.apply_functions(&inputs)?;
Ok(self.color_space.apply(&components)?)
}

/// Reads a required packed sample and maps it into the supplied decode range.
///
/// The encoded integer is scaled linearly from the range representable by
/// `width` bits into `min..=max`. An error is returned when the stream ends
/// before the sample, contains only part of it, uses an unsupported width,
/// or the encoded value or its range cannot be represented as `f32`.
fn read_sample(
&self,
reader: &mut BitReader<'_>,
width: usize,
min: f32,
max: f32,
) -> Result<f32, PdfShadingError> {
decode_sample(
read_required_mesh_bits(reader, width)?.into(),
width,
min,
max,
)
let code = read_mesh_bits(reader, width)?
.ok_or_else(|| PdfShadingError::from(MeshDecoderError::UnexpectedEndOfStream))?;
let shift = u32::try_from(width)
.map_err(|_| PdfShadingError::from(MeshDecoderError::InvalidBitFieldWidth { width }))?;
let code_max = 1_u64
.checked_shl(shift)
.ok_or_else(|| PdfShadingError::from(MeshDecoderError::InvalidBitFieldWidth { width }))?
.saturating_sub(1);
let code = u64::from(code)
.to_f32()
.ok_or_else(|| PdfShadingError::from(MeshDecoderError::SampleNotRepresentable))?;
let code_max = code_max
.to_f32()
.ok_or_else(|| PdfShadingError::from(MeshDecoderError::SampleRangeNotRepresentable))?;

Ok(min + (code / code_max) * (max - min))
}

fn apply_functions(&self, inputs: &[f32]) -> Result<Vec<f32>, PdfShadingError> {
Expand Down Expand Up @@ -230,27 +198,6 @@ pub(crate) fn read_mesh_bits(
.ok_or_else(|| PdfShadingError::from(MeshDecoderError::TruncatedSample))
}

/// Reads a mesh field that is required to complete the current record.
fn read_required_mesh_bits(
reader: &mut BitReader<'_>,
width: usize,
) -> Result<u32, PdfShadingError> {
read_mesh_bits(reader, width)?
.ok_or_else(|| PdfShadingError::from(MeshDecoderError::UnexpectedEndOfStream))
}

fn validate_allowed_width(
width: usize,
allowed: &[usize],
error: MeshDecoderError,
) -> Result<(), PdfShadingError> {
if allowed.contains(&width) {
Ok(())
} else {
Err(error.into())
}
}

fn decode_pair(
decode: &[f32],
pair_index: usize,
Expand All @@ -269,23 +216,6 @@ fn decode_pair(
Ok((min, max))
}

fn decode_sample(code: u64, width: usize, min: f32, max: f32) -> Result<f32, PdfShadingError> {
let shift = u32::try_from(width)
.map_err(|_| PdfShadingError::from(MeshDecoderError::InvalidBitFieldWidth { width }))?;
let code_max = 1_u64
.checked_shl(shift)
.ok_or_else(|| PdfShadingError::from(MeshDecoderError::InvalidBitFieldWidth { width }))?
.saturating_sub(1);
let code = code
.to_f32()
.ok_or_else(|| PdfShadingError::from(MeshDecoderError::SampleNotRepresentable))?;
let code_max = code_max
.to_f32()
.ok_or_else(|| PdfShadingError::from(MeshDecoderError::SampleRangeNotRepresentable))?;

Ok(min + (code / code_max) * (max - min))
}

#[cfg(test)]
#[path = "../tests/mesh_decoder.rs"]
mod tests;
9 changes: 3 additions & 6 deletions crates/pdf-shading/src/patch_mesh_config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,8 @@ use pdf_object::{

use crate::{
error::PdfShadingError,
mesh_decoder::{MeshBitWidths, MeshDecoder},
mesh_bit_widths::MeshBitWidths,
mesh_decoder::MeshDecoder,
model::{MeshPatch, Shading, ShadingType},
parse::{parse_functions, required_color_space},
};
Expand All @@ -34,11 +35,7 @@ impl PatchMeshConfig {
objects: &dyn ObjectResolver,
) -> Result<Self, PdfShadingError> {
let color_space = required_color_space(dictionary, objects)?;
let widths = MeshBitWidths::new(
dictionary.required_number::<usize>("BitsPerCoordinate", objects)?,
dictionary.required_number::<usize>("BitsPerComponent", objects)?,
dictionary.required_number::<usize>("BitsPerFlag", objects)?,
)?;
let widths = MeshBitWidths::from_dictionary(dictionary, objects)?;
let decode = dictionary.required_vec_of::<f32>("Decode", objects)?;
let bbox = dictionary.optional_bbox(objects)?;
let anti_alias = dictionary.optional_boolean("AntiAlias", objects)?;
Expand Down
Loading
Loading