M2: a DeviceN colour with twelve inks paints, and a colour that can't be evaluated no longer blanks the page (#4260)

ISO 32000-2 §8.6.6.5: "DeviceN colour spaces (PDF 1.3) may contain an
arbitrary number of colour components", and scn takes one operand per
component. The content parser (vendored hayro-syntax) held ten operands
per instruction; pushing the eleventh failed, and the failure ended the
whole content stream. A twelve-ink background fill therefore drew
nothing, and nothing after it was drawn either: a blank page, no error.

hayro-syntax patch (26): an instruction keeps up to 256 operands (enough
for the largest DeviceN space the interpreter reads, 255 inks, plus a
pattern name). Past that the oldest are dropped, in order, rather than
the stream ending: operators already read the operands nearest to them,
so this is how stray operands are treated below the cap too.

hayro-interpret patch (55): a Separation or DeviceN tint transform that
can't be read no longer makes the colour space unreadable (its paints
were drawn in DeviceGray from the first tint, white for a full tint);
it keeps the space, and like a transform that fails when evaluated it
paints that colour in the alternate space's initial colour. Those
fallbacks, and a cs/CS colour space that can't be read, were silent:
they are reported once per content stream as
InterpreterWarning::ColorFallback, which pdfcraft-render records as
RenderWarning::ColourFallback and pdfcraft-cli render prints.

Tests, each seen failing with its fix reverted: the report's twelve-ink
page paints black and draws what follows; a four-ink control; 300 stray
operands before rg still draw; a calculator tint transform that pops
too much, and an unknown function type, fall back with one warning and
the rest of the page drawn; hayro-syntax keeps all twelve scn operands
and, past the cap, the latest 256 in order (fails without the reorder).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0175FWj4xFhMhSH3UpMU8DSC
This commit is contained in:
mtlcl410
2026-10-24 11:45:23 +08:00
co-authored by Claude Opus 5.5
parent a047c6ba09
commit 0142a96ca3
9 changed files with 322 additions and 37 deletions
+4
View File
@@ -688,6 +688,10 @@ fn render(args: &[String]) -> Result<(), CliError> {
pdfcraft_render::RenderWarning::ContentTruncated => {
let _ = writeln!(std::io::stderr().lock(), "warning: page {page}: content past the page's safety budget was skipped");
}
pdfcraft_render::RenderWarning::ColourFallback => {
let _ =
writeln!(std::io::stderr().lock(), "warning: page {page}: a colour that could not be computed was painted in a fallback colour");
}
}
}
Ok(())
+16 -6
View File
@@ -220,6 +220,9 @@ pub struct RenderedPage {
pub enum RenderWarning {
/// Content was skipped after the per-page decoded-content budget was exhausted.
ContentTruncated,
/// A colour could not be computed (a colour space that cannot be read, or a Separation or
/// DeviceN tint transform that cannot be evaluated) and was painted in a fallback colour.
ColourFallback,
}
/// Counters for work performed by a renderer or render pool.
@@ -402,12 +405,15 @@ fn warning_settings(settings: &InterpreterSettings, warnings: Arc<Mutex<Vec<Rend
InterpreterSettings {
warning_sink: Arc::new(move |warning| {
// Nested forms, Type 3 glyphs and patterns are interpreted again and each reports the
// exhausted budget: once per page is enough.
if matches!(warning, InterpreterWarning::ContentTruncated) {
let mut warnings = lock(&target);
if !warnings.contains(&RenderWarning::ContentTruncated) {
warnings.push(RenderWarning::ContentTruncated);
}
// exhausted budget, or the colours that fell back: once per page is enough.
let warning = match warning {
InterpreterWarning::ContentTruncated => RenderWarning::ContentTruncated,
InterpreterWarning::ColorFallback => RenderWarning::ColourFallback,
InterpreterWarning::UnsupportedFont | InterpreterWarning::ImageDecodeFailure => return,
};
let mut warnings = lock(&target);
if !warnings.contains(&warning) {
warnings.push(warning);
}
}),
..settings.clone()
@@ -8163,6 +8169,10 @@ trailer << /Root 1 0 R >>
// #1201: /DefaultGray, /DefaultRGB and /DefaultCMYK (vendored hayro-interpret patch 38).
mod default_colour_spaces;
// #4260: DeviceN colours with many inks, and tint transforms that cannot be evaluated
// (vendored hayro-syntax patch 26, hayro-interpret patch 55).
mod devicen_inks;
/// #3109 / #3116: clipping-only text (`7 Tr`) installs a clip for subsequent painting
/// but draws no pixels itself.
#[test]
@@ -0,0 +1,123 @@
//! #4260: a DeviceN colour with more inks than the content parser held operands for, and a
//! colour whose tint transform cannot be evaluated (ISO 32000-2 §8.6.6.5).
//!
//! The content parser kept at most ten operands per operator (vendored hayro-syntax patch 26):
//! the eleventh operand of a twelve-ink `scn` ended the whole content stream, so the page came
//! out blank with no error. A tint transform that cannot be evaluated painted the alternate
//! space's initial colour without a word (vendored hayro-interpret patch 55): it now says so.
use super::{RenderWarning, rgb_at};
use crate::{PageRenderer, RenderConfig, RenderRequest, RenderedPage, RequestKind};
use std::sync::Arc;
/// The page is 200 × 200 pt, drawn at 72 dpi.
const SIDE: usize = 200;
const GREEN: [u8; 3] = [0, 255, 0];
const RED: [u8; 3] = [255, 0, 0];
const BLUE: [u8; 3] = [0, 0, 255];
const BLACK: [u8; 3] = [0, 0, 0];
/// The twelve inks of the report's print dieline.
const TWELVE_INKS: &str =
"[/Cyan /Magenta /Yellow /Black /PANTONE156C /PANTONE2227C /PANTONE1565C /PANTONE2975C /PANTONE7463C /Varnish /Dieline /SGKBlack]";
/// The report's tint transform: all inks at 1.0 give CMYK 1 1 1 1.
const TINT: &str = "<< /FunctionType 2 /Domain [0 1] /N 1 /C0 [0 0 0 0] /C1 [1 1 1 1] >>";
/// Render the one page `content` draws with the colour space `CS1` defined as `cs1`.
fn render(content: &str, cs1: &str) -> RenderedPage {
render_with(content, cs1, "")
}
/// [`render`], with `objects` (whole indirect objects from 5 0 obj on) added to the file.
fn render_with(content: &str, cs1: &str, objects: &str) -> RenderedPage {
let pdf = format!(
"%PDF-1.7\n1 0 obj << /Type /Catalog /Pages 2 0 R >> endobj\n\
2 0 obj << /Type /Pages /Kids [3 0 R] /Count 1 >> endobj\n\
3 0 obj << /Type /Page /Parent 2 0 R /MediaBox [0 0 200 200] /Resources << /ColorSpace << /CS1 {cs1} >> >> /Contents 4 0 R >> endobj\n\
4 0 obj << /Length {} >> stream\n{content}\nendstream endobj\n\
{objects}trailer << /Root 1 0 R >>\n%%EOF",
content.len()
);
let mut renderer = PageRenderer::new(Arc::new(pdf.into_bytes()), RenderConfig::default());
let page = renderer.render(RenderRequest { page: 0, kind: RequestKind::Pixels, tile: None, scale: 1.0, tag: 0, preview: false });
assert!(page.error.is_none(), "{:?}", page.error);
assert_eq!((page.width, page.height), (SIDE as u32, SIDE as u32));
page
}
/// The colour at (`x`, `y`) in page points from the bottom left.
fn at(page: &RenderedPage, x: usize, y: usize) -> [u8; 3] {
rgb_at(&page.rgba, SIDE, x, SIDE - 1 - y)
}
/// CMYK 1 1 1 1 through the renderer's CMYK profile: very dark, whatever the exact profile.
fn is_rich_black(rgb: [u8; 3]) -> bool {
rgb.iter().all(|c| *c < 48)
}
/// The report's page: twelve inks, all at 1.0, fill the page black. A green square drawn
/// afterwards shows the rest of the content is still read.
#[test]
fn a_twelve_ink_devicen_colour_paints() {
let content = "/CS1 cs 1 1 1 1 1 1 1 1 1 1 1 1 scn 0 0 200 200 re f 0 1 0 rg 0 0 20 20 re f";
let page = render(content, &format!("[/DeviceN {TWELVE_INKS} /DeviceCMYK {TINT}]"));
assert!(is_rich_black(at(&page, 100, 100)), "the twelve-ink fill paints: {:?}", at(&page, 100, 100));
assert_eq!(at(&page, 10, 10), GREEN, "what follows the twelve-ink colour is drawn");
assert!(page.warnings.is_empty(), "{:?}", page.warnings);
}
/// The control from the report: the same colour with four inks always painted.
#[test]
fn a_four_ink_devicen_colour_paints() {
let content = "/CS1 cs 1 1 1 1 scn 0 0 200 200 re f 0 1 0 rg 0 0 20 20 re f";
let page = render(content, &format!("[/DeviceN [/Cyan /Magenta /Yellow /Black] /DeviceCMYK {TINT}]"));
assert!(is_rich_black(at(&page, 100, 100)), "the four-ink fill paints: {:?}", at(&page, 100, 100));
assert_eq!(at(&page, 10, 10), GREEN);
assert!(page.warnings.is_empty(), "{:?}", page.warnings);
}
/// An operator with more operands than the parser keeps still reads the ones nearest to it,
/// as one with a few stray operands does, and the content after it is drawn.
#[test]
fn an_operator_with_hundreds_of_operands_does_not_end_the_content() {
let strays = "0 ".repeat(300);
let content = format!("1 0 0 rg 0 0 200 200 re f {strays}0 1 0 rg 0 0 20 20 re f");
let page = render(&content, "/DeviceRGB");
assert_eq!(at(&page, 100, 100), RED, "the content before the stray operands is drawn");
assert_eq!(at(&page, 10, 10), GREEN, "the operator after them reads its own three operands");
}
/// A tint transform that fails when it runs (a calculator function that pops more than it is
/// given) paints that one colour in the alternate space's initial colour (black in DeviceRGB)
/// and records a warning; the paints before and after it are drawn as usual.
#[test]
fn a_tint_transform_that_cannot_be_evaluated_draws_the_rest_of_the_page_and_warns() {
let program = "{ pop pop pop }";
let function = format!(
"5 0 obj << /FunctionType 4 /Domain [0 1 0 1] /Range [0 1 0 1 0 1] /Length {} >> stream\n{program}\nendstream endobj\n",
program.len()
);
let content = "1 0 0 rg 0 0 100 200 re f /CS1 cs 0.5 0.5 scn 100 0 100 200 re f 0 0 1 rg 0 0 50 50 re f";
let page = render_with(content, "[/DeviceN [/Spot1 /Spot2] /DeviceRGB 5 0 R]", &function);
assert_eq!(at(&page, 75, 150), RED, "the paint before the colour is drawn");
assert_eq!(at(&page, 150, 150), BLACK, "the colour falls back to the alternate space's initial colour");
assert_eq!(at(&page, 25, 25), BLUE, "the paint after the colour is drawn");
assert!(page.warnings.contains(&RenderWarning::ColourFallback), "{:?}", page.warnings);
}
/// A tint transform that cannot even be read (an unknown function type) keeps the colour space,
/// so its paints fall back the same way, with the same warning, instead of being drawn in
/// DeviceGray from the first tint (white here).
#[test]
fn a_tint_transform_that_cannot_be_read_falls_back_and_warns() {
let cs1 = format!("[/DeviceN {TWELVE_INKS} /DeviceCMYK << /FunctionType 7 /Domain [0 1] >>]");
let content = "/CS1 cs 1 1 1 1 1 1 1 1 1 1 1 1 scn 0 0 200 200 re f 0 1 0 rg 0 0 20 20 re f";
let page = render(content, &cs1);
assert!(is_rich_black(at(&page, 100, 100)), "the colour falls back to CMYK black: {:?}", at(&page, 100, 100));
assert_eq!(at(&page, 10, 10), GREEN, "what follows is drawn");
assert!(page.warnings.contains(&RenderWarning::ColourFallback), "{:?}", page.warnings);
// Reported once per page, however many paints fell back.
assert_eq!(page.warnings.iter().filter(|w| **w == RenderWarning::ColourFallback).count(), 1);
}
+2 -2
View File
File diff suppressed because one or more lines are too long
+54 -10
View File
@@ -28,6 +28,22 @@ pub type ColorComponents = SmallVec<[f32; 4]>;
std::thread_local! {
// PdfCraft patch (34): see `tint_transform_runs`.
static TINT_TRANSFORM_RUNS: core::cell::Cell<u64> = const { core::cell::Cell::new(0) };
// PdfCraft patch (55): see `note_color_fallback`.
static COLOR_FALLBACK: core::cell::Cell<bool> = const { core::cell::Cell::new(false) };
}
/// PdfCraft patch (55): record that a colour could not be computed and was painted in a
/// fallback colour, reported as `InterpreterWarning::ColorFallback` when the content stream
/// being interpreted ends. Per thread, like the page's other budgets: one page is interpreted
/// on one thread.
pub(crate) fn note_color_fallback() {
COLOR_FALLBACK.with(|fell_back| fell_back.set(true));
}
/// PdfCraft patch (55): whether a colour fell back since the last call (or since the page
/// started), clearing it, so each fallback is reported once.
pub(crate) fn take_color_fallback() -> bool {
COLOR_FALLBACK.with(|fell_back| fell_back.replace(false))
}
/// PdfCraft patch (34): how many colours this thread has run through a Separation or DeviceN
@@ -1030,10 +1046,30 @@ impl ToRgb for Indexed {
#[derive(Debug, Clone)]
pub(crate) struct Separation {
alternate_space: ColorSpace,
tint_transform: Function,
/// PdfCraft patch (55): `None` when the tint transform cannot be read (see
/// `eval_tint_transform`).
tint_transform: Option<Function>,
is_none_separation: bool,
}
/// PdfCraft patch (55): `tint_transform` run on the tints of one colour, or, when it cannot be
/// read or evaluated, the initial colour of `alternate_space` (black in every device space), with
/// the fallback recorded. hayro already fell back this way when evaluation failed, silently; a
/// transform it could not read made the whole colour space unreadable, and the space's
/// paints were drawn in DeviceGray from their first tint instead (white for a full tint).
fn eval_tint_transform(
tint_transform: Option<&Function>,
alternate_space: &ColorSpace,
tints: &[f32],
) -> ColorComponents {
tint_transform
.and_then(|f| f.eval(tints.to_smallvec()))
.unwrap_or_else(|| {
note_color_fallback();
alternate_space.initial_color()
})
}
impl Separation {
fn new(array: &Array<'_>, cache: &Cache) -> Option<Self> {
let mut iter = array.flex_iter();
@@ -1041,7 +1077,9 @@ impl Separation {
let _ = iter.next::<Name<'_>>()?;
let name = iter.next::<Name<'_>>()?;
let alternate_space = ColorSpace::new(iter.next::<Object<'_>>()?, cache)?;
let tint_transform = Function::new(&iter.next::<Object<'_>>()?)?;
// PdfCraft patch (55): an unreadable tint transform keeps the space (see
// `eval_tint_transform`).
let tint_transform = iter.next::<Object<'_>>().and_then(|f| Function::new(&f));
// Either I did something wrong, or no other viewers properly handles
// `All`, so let's just ignore it as well.
let is_none_separation = name.as_str() == "None";
@@ -1057,12 +1095,15 @@ impl Separation {
impl ToRgb for Separation {
fn convert_f32(&self, input: &[f32], output: &mut [u8], _: bool) -> Option<()> {
count_tint_transform_runs(input.len());
// PdfCraft patch (55): see `eval_tint_transform`.
let evaluated = input
.iter()
.flat_map(|n| {
self.tint_transform
.eval(smallvec![*n])
.unwrap_or(self.alternate_space.initial_color())
eval_tint_transform(
self.tint_transform.as_ref(),
&self.alternate_space,
&[*n],
)
})
.collect::<Vec<_>>();
self.alternate_space.convert_f32(&evaluated, output, false)
@@ -1077,7 +1118,9 @@ impl ToRgb for Separation {
pub(crate) struct DeviceN {
alternate_space: ColorSpace,
num_components: u8,
tint_transform: Function,
/// PdfCraft patch (55): `None` when the tint transform cannot be read (see
/// `eval_tint_transform`).
tint_transform: Option<Function>,
is_none: bool,
}
@@ -1094,7 +1137,9 @@ impl DeviceN {
let num_components = u8::try_from(names.len()).ok()?;
let all_none = names.iter().all(|n| n.as_str() == "None");
let alternate_space = ColorSpace::new(iter.next::<Object<'_>>()?, cache)?;
let tint_transform = Function::new(&iter.next::<Object<'_>>()?)?;
// PdfCraft patch (55): an unreadable tint transform keeps the space (see
// `eval_tint_transform`).
let tint_transform = iter.next::<Object<'_>>().and_then(|f| Function::new(&f));
if num_components == 0 {
return None;
@@ -1113,12 +1158,11 @@ impl ToRgb for DeviceN {
fn convert_f32(&self, input: &[f32], output: &mut [u8], _: bool) -> Option<()> {
let colours = input.len().checked_div(usize::from(self.num_components));
count_tint_transform_runs(colours.unwrap_or(0));
// PdfCraft patch (55): see `eval_tint_transform`.
let evaluated = input
.chunks_exact(self.num_components as usize)
.flat_map(|n| {
self.tint_transform
.eval(n.to_smallvec())
.unwrap_or(self.alternate_space.initial_color())
eval_tint_transform(self.tint_transform.as_ref(), &self.alternate_space, n)
})
.collect::<Vec<_>>();
self.alternate_space.convert_f32(&evaluated, output, false)
+2
View File
@@ -209,6 +209,8 @@ impl<'a> Context<'a> {
NESTED_PAINTS.with(|n| n.set(0));
CONTENT_LEFT.with(|c| c.set(MAX_PAGE_CONTENT));
CONTENT_TRUNCATED.with(|truncated| truncated.set(false));
// PdfCraft patch (55): a fallback left over from another page is not this page's.
crate::color::take_color_fallback();
crate::encode::reset_mesh_budget();
Self::new_with(initial_transform, bbox, cache, xref, settings, state, 0)
+24 -2
View File
@@ -157,6 +157,10 @@ pub enum InterpreterWarning {
ImageDecodeFailure,
/// PdfCraft patch (18): a page content stream was skipped after exhausting its safety budget.
ContentTruncated,
/// PdfCraft patch (55): a colour could not be computed (a colour space that cannot be
/// read, or a Separation or DeviceN tint transform that cannot be evaluated) and was painted
/// in a fallback colour. Reported at most once per content stream.
ColorFallback,
}
/// interpret the contents of the page and render them into the device.
@@ -562,8 +566,13 @@ fn interpret_window<'a>(
}
TypedInstruction::ColorSpaceStroke(c) => {
// PdfCraft patch (38): see `named_color_space`.
// PdfCraft patch (55): a space that cannot be read is drawn in DeviceGray, as
// before, and the fallback is reported.
let cs = named_color_space(c.0, resources, &defaults, context)
.unwrap_or(ColorSpace::device_gray());
.unwrap_or_else(|| {
crate::color::note_color_fallback();
ColorSpace::device_gray()
});
if !cs.is_pattern() {
context.get_mut().graphics_state.stroke_pattern = None;
@@ -573,8 +582,13 @@ fn interpret_window<'a>(
}
TypedInstruction::ColorSpaceNonStroke(c) => {
// PdfCraft patch (38): see `named_color_space`.
// PdfCraft patch (55): a space that cannot be read is drawn in DeviceGray, as
// before, and the fallback is reported.
let cs = named_color_space(c.0, resources, &defaults, context)
.unwrap_or(ColorSpace::device_gray());
.unwrap_or_else(|| {
crate::color::note_color_fallback();
ColorSpace::device_gray()
});
if !cs.is_pattern() {
context.get_mut().graphics_state.non_stroke_pattern = None;
@@ -932,6 +946,10 @@ pub fn interpret<'a>(
if crate::context::content_was_truncated() {
(context.settings.warning_sink)(InterpreterWarning::ContentTruncated);
}
// PdfCraft patch (55): and that a colour was painted in a fallback colour.
if crate::color::take_color_fallback() {
(context.settings.warning_sink)(InterpreterWarning::ColorFallback);
}
}
/// PdfCraft patch: the bytes of a window of page content. A window grows only while an instruction
@@ -990,4 +1008,8 @@ fn interpret_content<'a>(
if cut || content.truncated() || crate::context::content_was_truncated() {
(context.settings.warning_sink)(InterpreterWarning::ContentTruncated);
}
// PdfCraft patch (55): and that a colour was painted in a fallback colour.
if crate::color::take_color_fallback() {
(context.settings.warning_sink)(InterpreterWarning::ColorFallback);
}
}
+56 -17
View File
@@ -37,12 +37,11 @@ use crate::content::ops::TypedInstruction;
use crate::object;
use crate::object::dict::{Dict, InlineImageDict};
use crate::object::name::{Name, skip_name_like};
use crate::object::{Array, Null, Number, Object, Stream};
use crate::object::{Array, Number, Object, Stream};
use crate::reader::Reader;
use crate::reader::{Readable, ReaderContext, ReaderExt, Skippable};
use crate::trivia::{Comment, is_regular_character, is_white_space_character};
use crate::util::find_needle;
use core::array;
use core::fmt::{Debug, Formatter};
use core::ops::Deref;
use core::marker::PhantomData;
@@ -51,8 +50,18 @@ use smallvec::SmallVec;
// 6 operands are used for example for ctm or cubic curves,
// but anything above should be pretty rare (only for example for
// DeviceN color spaces, or invalid PDF files). So we settle on 10.
// PdfCraft patch (26): this is now only how many operands are held inline; see `MAX_OPERANDS`.
const OPERANDS_THRESHOLD: usize = 10;
/// PdfCraft patch (26): the most operands one instruction keeps. The stack held ten, and the
/// eleventh operand of an instruction ended the whole content stream, so a DeviceN colour of
/// eleven or more inks (ISO 32000-2 §8.6.6.5 allows "an arbitrary number of colour
/// components") blanked the rest of the page (#4260). An `SCN`/`scn` for the largest DeviceN
/// space the interpreter reads (255 inks) takes 255 numbers and a pattern name. Past this, the
/// oldest operands are dropped: operators read the operands nearest to them (see `shift` in the
/// operator macros), so this is how stray operands below an instruction are already treated.
const MAX_OPERANDS: usize = 256;
impl Debug for Operator<'_> {
fn fmt(&self, f: &mut Formatter<'_>) -> core::fmt::Result {
write!(f, "{}", self.0.as_str())
@@ -436,6 +445,8 @@ impl<'a> UntypedIter<'a> {
self.operator = Some(operator);
self.committed = self.reader.offset();
// PdfCraft patch (26): operands in reading order, if some were dropped.
self.stack.settle();
return Some(Instruction {
operands: &self.stack,
operator: self.operator.as_ref().unwrap(),
@@ -594,8 +605,13 @@ impl<'b, 'a> Instruction<'b, 'a> {
pub struct Stack<'a> {
// TODO: Explore using an object pool to avoid repeatedly
// allocating/deallocating objects.
data: [Object<'a>; OPERANDS_THRESHOLD],
len: usize,
// PdfCraft patch (26): up to `MAX_OPERANDS` operands, the first `OPERANDS_THRESHOLD` inline.
data: SmallVec<[Object<'a>; OPERANDS_THRESHOLD]>,
/// PdfCraft patch (26): once `data` is full, the slot of its oldest operand, which the next
/// operand replaces. Always 0 while the stack is not full, and after `settle`.
oldest: usize,
/// PdfCraft patch (26): whether operands were dropped since the stack was last settled.
dropped: bool,
}
impl<'a> Default for Stack<'a> {
@@ -608,31 +624,53 @@ impl<'a> Stack<'a> {
/// Create a new, empty stack.
pub fn new() -> Self {
Self {
data: array::from_fn(|_| Object::Null(Null)),
len: 0,
data: SmallVec::new(),
oldest: 0,
dropped: false,
}
}
/// PdfCraft patch (26): push an operand, dropping the oldest once `MAX_OPERANDS` are held
/// (see `MAX_OPERANDS`). Never fails: it used to, and the failure ended the content stream.
fn push(&mut self, operand: Object<'a>) -> Option<()> {
if self.len >= OPERANDS_THRESHOLD {
return None;
if self.data.len() < MAX_OPERANDS {
self.data.push(operand);
return Some(());
}
self.data[self.len] = operand;
self.len += 1;
self.dropped = true;
if let Some(slot) = self.data.get_mut(self.oldest) {
*slot = operand;
}
self.oldest = (self.oldest + 1) % MAX_OPERANDS;
Some(())
}
/// PdfCraft patch (26): put the operands back in the order they were read, oldest first,
/// after `push` dropped some. Called before the instruction is handed out.
fn settle(&mut self) {
if self.dropped {
warn!("more than {MAX_OPERANDS} operands before an operator; the oldest were dropped");
// Cannot panic: operands are dropped only once `data` holds `MAX_OPERANDS`, and
// `oldest` stays below that.
self.data.rotate_left(self.oldest);
self.oldest = 0;
self.dropped = false;
}
}
fn clear(&mut self) {
self.len = 0;
self.data.clear();
self.oldest = 0;
self.dropped = false;
}
fn len(&self) -> usize {
self.len
self.data.len()
}
fn as_slice(&self) -> &[Object<'a>] {
&self.data[..self.len]
&self.data
}
fn get<'b, T>(&'b self, index: usize) -> Option<T>
@@ -665,11 +703,12 @@ impl Debug for Stack<'_> {
impl Clone for Stack<'_> {
fn clone(&self) -> Self {
let mut stack = Self::new();
for item in self.as_slice() {
stack.push(item.clone()).unwrap();
// PdfCraft patch (26): a plain copy (pushing each operand again could no longer fail).
Self {
data: self.data.clone(),
oldest: self.oldest,
dropped: self.dropped,
}
stack
}
}
+41
View File
@@ -152,6 +152,47 @@ f
assert!(iter.next().is_none());
}
/// PdfCraft patch (26): a twelve-ink DeviceN colour's `scn` keeps all twelve operands, and
/// the instructions after it are still read (#4260). The eleventh operand ended the stream.
#[test]
fn scn_with_twelve_components_keeps_them_all() {
let input = b"1 2 3 4 5 6 7 8 9 10 11 12 scn 0 0 10 10 re f";
let mut iter = TypedIter::new(input);
match iter.next() {
Some(TypedInstruction::NonStrokeColorNamed(NonStrokeColorNamed(nums, None))) => {
let expected = (1..=12).map(Number::from_i32).collect::<Vec<_>>();
assert_eq!(nums.as_slice(), expected.as_slice());
}
other => panic!("unexpected instruction: {other:?}"),
}
assert!(matches!(iter.next(), Some(TypedInstruction::RectPath(_))));
assert!(matches!(iter.next(), Some(TypedInstruction::FillPathNonZero(_))));
assert!(iter.next().is_none());
}
/// PdfCraft patch (26): past the operand cap the oldest operands are dropped, in order, and
/// the operator reads the ones nearest to it.
#[test]
fn operands_past_the_cap_keep_the_latest_in_order() {
let mut input = String::new();
for i in 0..600 {
input.push_str(&format!("{i} "));
}
input.push_str("scn 0 1 0 rg");
let mut iter = TypedIter::new(input.as_bytes());
match iter.next() {
Some(TypedInstruction::NonStrokeColorNamed(NonStrokeColorNamed(nums, None))) => {
let expected = (344..600).map(Number::from_i32).collect::<Vec<_>>();
assert_eq!(nums.as_slice(), expected.as_slice());
}
other => panic!("unexpected instruction: {other:?}"),
}
assert!(matches!(iter.next(), Some(TypedInstruction::NonStrokeColorDeviceRgb(_))));
assert!(iter.next().is_none());
}
#[test]
fn scn() {
let input = b"