Shading patterns use the stroke or fill alpha current at paint time (#3176)

This commit is contained in:
MarekSpodBiedry
2026-10-18 13:52:43 +02:00
parent 50f261d70c
commit b744866dee
3 changed files with 44 additions and 3 deletions
+32
View File
@@ -6760,6 +6760,38 @@ trailer << /Root 1 0 R >>
assert_eq!(px(75, 50), vec![255, 255, 255, 255], "outside the mask");
}
/// Issue #3176: `CA` / `ca` belong to the graphics state at paint time, so a `gs` after
/// `SCN` / `scn` still fades a shading pattern, and a stroke takes the stroke alpha. The
/// pattern took the fill alpha current at `scn`/`SCN`, so both shapes came out opaque.
#[test]
fn shading_pattern_uses_the_alpha_current_at_paint_time() {
let content = "/Pattern CS /P0 SCN /S1 gs 10 w 0 75 m 100 75 l S /Pattern cs /P0 scn /F1 gs 0 0 100 50 re f";
let pdf = format!(
"%PDF-1.7
1 0 obj << /Type /Catalog /Pages 2 0 R >> endobj
2 0 obj << /Type /Pages /Kids [3 0 R] /Count 1 >> endobj
3 0 obj << /Type /Page /Parent 2 0 R /MediaBox [0 0 100 100] /Contents 4 0 R
/Resources << /ExtGState << /S1 << /CA 0.25 >> /F1 << /ca 0.25 >> >> /Pattern << /P0 5 0 R >> >> >> endobj
4 0 obj << /Length {} >> stream
{content}
endstream endobj
5 0 obj << /PatternType 2 /Shading << /ShadingType 2 /ColorSpace /DeviceRGB /Coords [0 0 100 0] /Extend [true true]
/Function << /FunctionType 2 /Domain [0 1] /C0 [1 0 0] /C1 [1 0 0] /N 1 >> >> >> endobj
trailer << /Root 1 0 R >>
%%EOF",
content.len()
);
let mut r = PageRenderer::new(Arc::new(pdf.into_bytes()), RenderConfig::default());
let p = r.render(RenderRequest { page: 0, kind: RequestKind::Pixels, tile: None, scale: 1.0, tag: 0, ..Default::default() });
assert!(p.error.is_none(), "{:?}", p.error);
let px = |x: u32, y: u32| p.rgba[((y * p.width + x) * 4) as usize..][..4].to_vec();
// Red at 25% over white is about (255, 191, 191); opaque red would be (255, 0, 0).
for (what, x, y) in [("the stroke under /CA 0.25", 50, 25), ("the fill under /ca 0.25", 50, 75)] {
let c = px(x, y);
assert!(c[0] >= 250 &&(185..=197).contains(&c[1]) && (185..=197).contains(&c[2]), "{what}: {c:?}");
}
}
/// ISO 32000-2 §8.5.4: `W` / `W*` clip with the current path once whichever path-painting
/// operator ends it has painted it, `S`, `f` or `B` as much as `n`. The vendored interpreter
/// applied the clip only on `n`: after `re W* S` everything later painted outside the clip,
+2
View File
@@ -159,6 +159,8 @@ pub(crate) fn get_paint<'a>(context: &Context<'a>, is_stroke: bool) -> Paint<'a>
pattern.set_transfer_function(tf.clone());
}
// `CA`/`ca` can be set by a `gs` after the pattern colour operator, so use the alpha current at paint time.
pattern.set_alpha(data.alpha);
pattern.pre_concat_transform(context.root_transform());
Paint::Pattern(Box::new(pattern))
+10 -3
View File
@@ -43,7 +43,6 @@ impl<'a> Pattern<'a> {
Object::Dict(dict) => Some(Self::Shading(ShadingPattern::new(
&dict,
&ctx.interpreter_cache.object_cache,
ctx.get().graphics_state.non_stroke_alpha,
)?)),
Object::Stream(stream) => Some(Self::Tiling(Box::new(TilingPattern::new(
stream, ctx, resources,
@@ -63,6 +62,14 @@ impl<'a> Pattern<'a> {
}
}
/// Set the alpha of the graphics state in effect when the pattern is painted. The `gs` operator
/// can come after `scn`/`SCN`, so the alpha must not be captured when the pattern is selected.
pub(crate) fn set_alpha(&mut self, alpha: f32) {
if let Self::Shading(p) = self {
p.opacity = alpha;
}
}
pub(crate) fn set_transfer_function(&mut self, tf: ActiveTransferFunction) {
if let Self::Shading(p) = self {
p.transfer_function = Some(tf);
@@ -93,7 +100,7 @@ pub struct ShadingPattern {
}
impl ShadingPattern {
pub(crate) fn new(dict: &Dict<'_>, cache: &Cache, opacity: f32) -> Option<Self> {
pub(crate) fn new(dict: &Dict<'_>, cache: &Cache) -> Option<Self> {
let shading = dict.get::<Object<'_>>(SHADING).and_then(|o| {
let (dict, stream) = dict_or_stream(&o)?;
@@ -110,7 +117,7 @@ impl ShadingPattern {
Some(Self {
shading: Arc::new(shading),
opacity,
opacity: 1.0,
matrix,
transfer_function: None,
})