Skip to content

Commit e5fdbb5

Browse files
authored
Merge pull request #250
pdf-shading: isolate mesh bit widths
2 parents 182aa3b + b2add7e commit e5fdbb5

7 files changed

Lines changed: 213 additions & 150 deletions

File tree

crates/pdf-shading/src/free_form_mesh.rs

Lines changed: 3 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,8 @@ use thiserror::Error;
1212

1313
use crate::{
1414
error::PdfShadingError,
15-
mesh_decoder::{MeshBitWidths, MeshDecoder, read_mesh_bits},
15+
mesh_bit_widths::MeshBitWidths,
16+
mesh_decoder::{MeshDecoder, read_mesh_bits},
1617
model::{MeshTriangle, MeshVertex, Shading},
1718
parse::{parse_functions, required_color_space},
1819
};
@@ -74,11 +75,7 @@ impl FreeFormMeshConfig {
7475
objects: &dyn ObjectResolver,
7576
) -> Result<Self, PdfShadingError> {
7677
let color_space = required_color_space(dictionary, objects)?;
77-
let widths = MeshBitWidths::new(
78-
dictionary.required_number::<usize>("BitsPerCoordinate", objects)?,
79-
dictionary.required_number::<usize>("BitsPerComponent", objects)?,
80-
dictionary.required_number::<usize>("BitsPerFlag", objects)?,
81-
)?;
78+
let widths = MeshBitWidths::from_dictionary(dictionary, objects)?;
8279
let decode = dictionary.required_vec_of::<f32>("Decode", objects)?;
8380
let bbox = dictionary.optional_bbox(objects)?;
8481
let anti_alias = dictionary.optional_boolean("AntiAlias", objects)?;

crates/pdf-shading/src/lib.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ pub mod color_stops;
44
pub mod error;
55
mod free_form_mesh;
66
pub mod mesh;
7+
mod mesh_bit_widths;
78
mod mesh_decoder;
89
pub mod model;
910
pub mod paint;
Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
1+
//! Parsing and validation of packed mesh bit widths.
2+
3+
use pdf_object::{
4+
dictionary::Dictionary, object_lookup::ObjectLookupExt, object_resolver::ObjectResolver,
5+
};
6+
7+
use crate::{error::PdfShadingError, mesh_decoder::MeshDecoderError};
8+
9+
/// Coordinate widths permitted by the PDF specification for `/BitsPerCoordinate`.
10+
const VALID_COORDINATE_WIDTHS: [usize; 8] = [1, 2, 4, 8, 12, 16, 24, 32];
11+
12+
/// Color component widths permitted by the PDF specification for `/BitsPerComponent`.
13+
const VALID_COMPONENT_WIDTHS: [usize; 6] = [1, 2, 4, 8, 12, 16];
14+
15+
/// Edge-flag widths permitted by the PDF specification for `/BitsPerFlag`.
16+
const VALID_FLAG_WIDTHS: [usize; 3] = [2, 4, 8];
17+
18+
/// Bit widths used to decode packed fields in a Type 4, 6, or 7 mesh stream.
19+
///
20+
/// Each value is read from the corresponding mesh shading dictionary entry
21+
/// and validated against the widths permitted by the PDF specification.
22+
#[derive(Debug, Clone, Copy)]
23+
pub(crate) struct MeshBitWidths {
24+
/// Number of bits used to encode each coordinate component.
25+
coordinate: usize,
26+
/// Number of bits used to encode each color component or function input.
27+
component: usize,
28+
/// Number of bits used to encode each vertex or patch edge flag.
29+
flag: usize,
30+
}
31+
32+
impl MeshBitWidths {
33+
/// Reads and validates the bit widths declared by a mesh shading dictionary.
34+
///
35+
/// The required `/BitsPerCoordinate`, `/BitsPerComponent`, and
36+
/// `/BitsPerFlag` entries are resolved through `objects` before their
37+
/// values are checked against the widths permitted by the PDF
38+
/// specification.
39+
///
40+
/// Returns an error when an entry is missing, cannot be resolved as a
41+
/// non-negative integer, or specifies an unsupported width.
42+
pub(crate) fn from_dictionary(
43+
dictionary: &Dictionary,
44+
objects: &dyn ObjectResolver,
45+
) -> Result<Self, PdfShadingError> {
46+
let coordinate = dictionary.required_number::<usize>("BitsPerCoordinate", objects)?;
47+
let component = dictionary.required_number::<usize>("BitsPerComponent", objects)?;
48+
let flag = dictionary.required_number::<usize>("BitsPerFlag", objects)?;
49+
50+
validate_allowed_width(
51+
coordinate,
52+
&VALID_COORDINATE_WIDTHS,
53+
MeshDecoderError::InvalidBitsPerCoordinate { value: coordinate },
54+
)?;
55+
validate_allowed_width(
56+
component,
57+
&VALID_COMPONENT_WIDTHS,
58+
MeshDecoderError::InvalidBitsPerComponent { value: component },
59+
)?;
60+
validate_allowed_width(
61+
flag,
62+
&VALID_FLAG_WIDTHS,
63+
MeshDecoderError::InvalidBitsPerFlag { value: flag },
64+
)?;
65+
66+
Ok(Self {
67+
coordinate,
68+
component,
69+
flag,
70+
})
71+
}
72+
73+
/// Returns the number of bits used to encode each coordinate component.
74+
pub(crate) fn coordinate(self) -> usize {
75+
self.coordinate
76+
}
77+
78+
/// Returns the number of bits used to encode each color component or function input.
79+
pub(crate) fn component(self) -> usize {
80+
self.component
81+
}
82+
83+
/// Returns the number of bits used to encode each vertex or patch edge flag.
84+
pub(crate) fn flag(self) -> usize {
85+
self.flag
86+
}
87+
}
88+
89+
/// Checks whether a mesh field width is one of the values permitted for that field.
90+
///
91+
/// Returns `Ok(())` when `width` appears in `allowed`; otherwise converts the
92+
/// field-specific `error` into [`PdfShadingError`].
93+
fn validate_allowed_width(
94+
width: usize,
95+
allowed: &[usize],
96+
error: MeshDecoderError,
97+
) -> Result<(), PdfShadingError> {
98+
if allowed.contains(&width) {
99+
Ok(())
100+
} else {
101+
Err(error.into())
102+
}
103+
}
104+
105+
#[cfg(test)]
106+
#[path = "../tests/mesh_bit_widths.rs"]
107+
mod tests;

crates/pdf-shading/src/mesh_decoder.rs

Lines changed: 26 additions & 96 deletions
Original file line numberDiff line numberDiff line change
@@ -7,11 +7,7 @@ use pdf_graphics::{color::Color, point::Point};
77
use pdf_utils::BitReader;
88
use thiserror::Error;
99

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

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

64-
/// Validated bit widths from a Type 4, 6, or 7 mesh dictionary.
65-
#[derive(Debug, Clone, Copy)]
66-
pub(crate) struct MeshBitWidths {
67-
coordinate: usize,
68-
component: usize,
69-
flag: usize,
70-
}
71-
72-
impl MeshBitWidths {
73-
/// Validates the three PDF mesh bit-width entries.
74-
pub(crate) fn new(
75-
coordinate: usize,
76-
component: usize,
77-
flag: usize,
78-
) -> Result<Self, PdfShadingError> {
79-
validate_allowed_width(
80-
coordinate,
81-
&VALID_COORDINATE_WIDTHS,
82-
MeshDecoderError::InvalidBitsPerCoordinate { value: coordinate },
83-
)?;
84-
validate_allowed_width(
85-
component,
86-
&VALID_COMPONENT_WIDTHS,
87-
MeshDecoderError::InvalidBitsPerComponent { value: component },
88-
)?;
89-
validate_allowed_width(
90-
flag,
91-
&VALID_FLAG_WIDTHS,
92-
MeshDecoderError::InvalidBitsPerFlag { value: flag },
93-
)?;
94-
95-
Ok(Self {
96-
coordinate,
97-
component,
98-
flag,
99-
})
100-
}
101-
102-
/// Returns the width of each edge-flag field.
103-
pub(crate) fn flag(self) -> usize {
104-
self.flag
105-
}
106-
}
107-
10860
/// Decodes mesh coordinates and color inputs according to a `/Decode` array.
10961
///
11062
/// A decoder borrows the parsed dictionary data while a mesh parser owns the
@@ -159,8 +111,8 @@ impl<'a> MeshDecoder<'a> {
159111
pub(crate) fn read_point(&self, reader: &mut BitReader<'_>) -> Result<Point, PdfShadingError> {
160112
let (x_min, x_max) = decode_pair(self.decode, 0, "X")?;
161113
let (y_min, y_max) = decode_pair(self.decode, 1, "Y")?;
162-
let x = self.read_sample(reader, self.widths.coordinate, x_min, x_max)?;
163-
let y = self.read_sample(reader, self.widths.coordinate, y_min, y_max)?;
114+
let x = self.read_sample(reader, self.widths.coordinate(), x_min, x_max)?;
115+
let y = self.read_sample(reader, self.widths.coordinate(), y_min, y_max)?;
164116
Ok(Point::new(x, y))
165117
}
166118

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

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

134+
/// Reads a required packed sample and maps it into the supplied decode range.
135+
///
136+
/// The encoded integer is scaled linearly from the range representable by
137+
/// `width` bits into `min..=max`. An error is returned when the stream ends
138+
/// before the sample, contains only part of it, uses an unsupported width,
139+
/// or the encoded value or its range cannot be represented as `f32`.
182140
fn read_sample(
183141
&self,
184142
reader: &mut BitReader<'_>,
185143
width: usize,
186144
min: f32,
187145
max: f32,
188146
) -> Result<f32, PdfShadingError> {
189-
decode_sample(
190-
read_required_mesh_bits(reader, width)?.into(),
191-
width,
192-
min,
193-
max,
194-
)
147+
let code = read_mesh_bits(reader, width)?
148+
.ok_or_else(|| PdfShadingError::from(MeshDecoderError::UnexpectedEndOfStream))?;
149+
let shift = u32::try_from(width)
150+
.map_err(|_| PdfShadingError::from(MeshDecoderError::InvalidBitFieldWidth { width }))?;
151+
let code_max = 1_u64
152+
.checked_shl(shift)
153+
.ok_or_else(|| PdfShadingError::from(MeshDecoderError::InvalidBitFieldWidth { width }))?
154+
.saturating_sub(1);
155+
let code = u64::from(code)
156+
.to_f32()
157+
.ok_or_else(|| PdfShadingError::from(MeshDecoderError::SampleNotRepresentable))?;
158+
let code_max = code_max
159+
.to_f32()
160+
.ok_or_else(|| PdfShadingError::from(MeshDecoderError::SampleRangeNotRepresentable))?;
161+
162+
Ok(min + (code / code_max) * (max - min))
195163
}
196164

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

233-
/// Reads a mesh field that is required to complete the current record.
234-
fn read_required_mesh_bits(
235-
reader: &mut BitReader<'_>,
236-
width: usize,
237-
) -> Result<u32, PdfShadingError> {
238-
read_mesh_bits(reader, width)?
239-
.ok_or_else(|| PdfShadingError::from(MeshDecoderError::UnexpectedEndOfStream))
240-
}
241-
242-
fn validate_allowed_width(
243-
width: usize,
244-
allowed: &[usize],
245-
error: MeshDecoderError,
246-
) -> Result<(), PdfShadingError> {
247-
if allowed.contains(&width) {
248-
Ok(())
249-
} else {
250-
Err(error.into())
251-
}
252-
}
253-
254201
fn decode_pair(
255202
decode: &[f32],
256203
pair_index: usize,
@@ -269,23 +216,6 @@ fn decode_pair(
269216
Ok((min, max))
270217
}
271218

272-
fn decode_sample(code: u64, width: usize, min: f32, max: f32) -> Result<f32, PdfShadingError> {
273-
let shift = u32::try_from(width)
274-
.map_err(|_| PdfShadingError::from(MeshDecoderError::InvalidBitFieldWidth { width }))?;
275-
let code_max = 1_u64
276-
.checked_shl(shift)
277-
.ok_or_else(|| PdfShadingError::from(MeshDecoderError::InvalidBitFieldWidth { width }))?
278-
.saturating_sub(1);
279-
let code = code
280-
.to_f32()
281-
.ok_or_else(|| PdfShadingError::from(MeshDecoderError::SampleNotRepresentable))?;
282-
let code_max = code_max
283-
.to_f32()
284-
.ok_or_else(|| PdfShadingError::from(MeshDecoderError::SampleRangeNotRepresentable))?;
285-
286-
Ok(min + (code / code_max) * (max - min))
287-
}
288-
289219
#[cfg(test)]
290220
#[path = "../tests/mesh_decoder.rs"]
291221
mod tests;

crates/pdf-shading/src/patch_mesh_config.rs

Lines changed: 3 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,8 @@ use pdf_object::{
99

1010
use crate::{
1111
error::PdfShadingError,
12-
mesh_decoder::{MeshBitWidths, MeshDecoder},
12+
mesh_bit_widths::MeshBitWidths,
13+
mesh_decoder::MeshDecoder,
1314
model::{MeshPatch, Shading, ShadingType},
1415
parse::{parse_functions, required_color_space},
1516
};
@@ -34,11 +35,7 @@ impl PatchMeshConfig {
3435
objects: &dyn ObjectResolver,
3536
) -> Result<Self, PdfShadingError> {
3637
let color_space = required_color_space(dictionary, objects)?;
37-
let widths = MeshBitWidths::new(
38-
dictionary.required_number::<usize>("BitsPerCoordinate", objects)?,
39-
dictionary.required_number::<usize>("BitsPerComponent", objects)?,
40-
dictionary.required_number::<usize>("BitsPerFlag", objects)?,
41-
)?;
38+
let widths = MeshBitWidths::from_dictionary(dictionary, objects)?;
4239
let decode = dictionary.required_vec_of::<f32>("Decode", objects)?;
4340
let bbox = dictionary.optional_bbox(objects)?;
4441
let anti_alias = dictionary.optional_boolean("AntiAlias", objects)?;

0 commit comments

Comments
 (0)