From 884c02872ce9d7432c3b67a920011cf385ce70b2 Mon Sep 17 00:00:00 2001 From: PgBiel <9021226+PgBiel@users.noreply.github.com> Date: Wed, 4 Dec 2024 07:00:04 -0300 Subject: [PATCH] Fix text fill within `clip: true` blocks in PNG export (#5502) --- crates/typst-render/src/text.rs | 36 ++++++++++++------ tests/ref/block-clip-text.png | Bin 1268 -> 908 bytes tests/ref/block-clipping-multiple-pages.png | Bin 2047 -> 2029 bytes .../issue-5499-text-fill-in-clip-block.png | Bin 0 -> 1502 bytes tests/suite/text/font.typ | 31 +++++++++++++++ 5 files changed, 55 insertions(+), 12 deletions(-) create mode 100644 tests/ref/issue-5499-text-fill-in-clip-block.png diff --git a/crates/typst-render/src/text.rs b/crates/typst-render/src/text.rs index c017b1ce3..b47659cba 100644 --- a/crates/typst-render/src/text.rs +++ b/crates/typst-render/src/text.rs @@ -164,30 +164,42 @@ fn write_bitmap( // If we have a clip mask we first render to a pixmap that we then blend // with our canvas if state.mask.is_some() { + let cw = canvas.width() as i32; + let ch = canvas.height() as i32; let mw = bitmap.width; let mh = bitmap.height; + let left = bitmap.left; + let top = bitmap.top; + // Pad the pixmap with 1 pixel in each dimension so that we do // not get any problem with floating point errors along their border let mut pixmap = sk::Pixmap::new(mw + 2, mh + 2)?; + let pixels = bytemuck::cast_slice_mut::(pixmap.data_mut()); for x in 0..mw { for y in 0..mh { let alpha = bitmap.coverage[(y * mw + x) as usize]; - let color = sampler.sample((x, y)); - pixmap.pixels_mut()[((y + 1) * (mw + 2) + (x + 1)) as usize] = - sk::ColorU8::from_rgba( - color.red(), - color.green(), - color.blue(), - alpha, - ) - .premultiply(); + + // To sample at the correct position, we need to convert each + // pixel's position in the bitmap (x and y) to its final + // expected position in the canvas. Due to padding, this + // pixel's position in the pixmap will be (x + 1, y + 1). + // Then, when drawing the pixmap to the canvas, we place its + // top-left corner at position (left - 1, top - 1). Therefore, + // the final position of this pixel in the canvas is given by + // (left - 1 + x + 1, top - 1 + y + 1) = (left + x, top + y). + let sample_pos = ( + (left + x as i32).clamp(0, cw) as u32, + (top + y as i32).clamp(0, ch) as u32, + ); + let color = sampler.sample(sample_pos); + let color = bytemuck::cast(color); + + let applied = alpha_mul(color, alpha as u32); + pixels[((y + 1) * (mw + 2) + (x + 1)) as usize] = applied; } } - let left = bitmap.left; - let top = bitmap.top; - canvas.draw_pixmap( left - 1, top - 1, diff --git a/tests/ref/block-clip-text.png b/tests/ref/block-clip-text.png index 744ce0f26e89c0882275e557028828573b42e9a6..8c82bc30934ebef06296654c08655e8a7d4df64d 100644 GIT binary patch delta 886 zcmV-+1Bv|f35*AjBYy)^Nkl=>m7;Zv_ysyR zF+82LLD6BLz^yLQ_lL$%ceIXDB)&8QJ9mj+QbQOEYfMmR46W9}P9r%Z5Wj(Ud^tW( z=Rk*g)6IR}yG`HE-#$M*e*5hGe!mtFp%y4u!3utyVC_LUGk;68`!WXBUVJLmY!kCN z#63l2zgsLmdPdAIQ5XY@=NOG)wx!(Nw3&nDj7Kv+%_#W!p}L*8b_H`!^e+4+05Da# zwIftf_cezW)#q8;ST6@&*!tP3!0}?l5ljHknR~6jwgG3uxS507p;8X~0u+ANQB2v< z+we?j{J9TWXn*>64n8df-uyJ=wEw~D!0bNo2eE@)=m;3+$Nieo^%#|?+V1fw%cz=5cXw{5{lZt%VKmYZi&(^%@ zD@G28%l_EU&jTjh_4MxB{N{rPW~Ak-&S+Q`OjZvx39_@BaeJe*h+qIZ2|t z1&dc-!O=1dhQ<2dM8*#z*nGS;d9kXz@DAMWB&9FUf9u;M1MZAJpXvhZpTu8Z`%ilT zX#+aUynoEGLMpnhLh=AH)6H9SgW*K&O)cwvZDU+o{^He{Z&+U`TD81L)7C`gyuQq_ za#@TyKQ<#T+UAJ0X|p9710x>LrUW5g{Y2ao@{hL@sr8<=$egQnb(1g@o;pkT2F7K= zI>#3gw&A5c65*jtFcCg`uSD3p#6$c~MSa22Hh;%-=Pu98sV#UKxUk=~^P;`w(;8{; zU^+Z|@=P|R8+-%n__l;-&aA~cQQL@pYhYzaCT!wLGOi}}XbdJ`qJJIloZi-jy_b|> zS+G9^7Bg(7d@tR}bmg)n)MI5wOF!hCBH1qs)~$8TZ~gIoYEMqV7>%XdM`ZEm(_EVf)%V_1uIy=5!)ml9tK^^#lusZ zgxVw?4UW_%A=D(^b M07*qoM6N<$g6LPa;Q#;t delta 1249 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 diff --git a/tests/ref/issue-5499-text-fill-in-clip-block.png b/tests/ref/issue-5499-text-fill-in-clip-block.png new file mode 100644 index 0000000000000000000000000000000000000000..5f7962d3b1b38af687b2996873bbbcaa201246ff GIT binary patch literal 1502 zcmV<41tI#0P)~zEXbeIlG(uw#8lf@&1@vrOdyyg%VM9T=qFMY^qvtu`{kSq;6R!+_u zPoS4s?w)?B)3~W{iPyzXLsykQ0Q#0R;v6ZfTF4vxfGo!=~xD#i~^zj9h;WxJ1=Jqz?Xe>ozPt|?tmoZ7-1iSh#yQ`E+As5U&F z9NOo*h&w>TP%daKuNV5A#;B|_`}nvu|WbddE$-%C)`FK6T4gjSiHeie=V5XG_%pnphTL6pjpXyfbTkfGoFDROUKv=5j%02;(UxVkkFRzJFvbgap{pa%kS<9HE< z0R0P;I_M5hVkusR1MF<*xoF89%!RY^W#`6TXqaj=oifI5j7Mc+)Na|v#`rGi9F?vYJunQY#VbjFJ@l!d|5-URDW;~Rtz+ALaB;)T+?Gey z&G>gxi^8=5eKbe~xPFOThI9LGVbZTR7>q$j3Ti3RhCs9D5hd71XE zN+%sTxcg~aYI&Qu1O&Dwh6;dZvy@dP}-*A#IT^)=~2EBS=R2uX6Gz*Lpe{*7FYpFnTZ8fYEm9S?^kDM zh)|&Nnd}HC3tIVZdcF2Jf~1_?2-b#2Go6%MK{N52^4Ol}EpGK1juq;M;W=`KgYTHs zJ1IkH>Z`_b^w6MTB0k!a?Oyq7)p5Kp2h#%R4+i8Dj@mtkgOOcmG~dOayW+f7V`g%!Z3YOKDhjn3Ta|J{2mYN@_vUohuobSXL9_)8%wl zB)Vx$3k8^7w#!=ms$+(6=681Cfchrv$_D?ZOl9Lu^<&>E)SRkqvlA|3ilX~ z)&#;BAN49vJ<6h>;|mME86q?Wp%EH`&3_>F`LSqmbp)m-J&Coo#{JQ_KN|N(