@@ -181,7 +181,12 @@ std::string svg_path_fragment(const pdf::PathElement &path,
181181// / or "" when it carries no pass-through bytes. The image fills the unit square
182182// / in user space (ISO 32000-1 8.10.5); the transform maps that square — through
183183// / a vertical flip (the image's first row is its top, SVG draws y-down) and the
184- // / CTM — into the page box. `clip_id` installs a clip via `clip-path`.
184+ // / CTM — into the page box. `clip_id`, when non-empty, installs a clip via a
185+ // / wrapping `<g clip-path>`. The clip geometry is in the page viewBox
186+ // / (`userSpaceOnUse`), but the `<image>` carries its own `transform`, so a
187+ // / `clip-path` placed *on the image* would be resolved in the image's
188+ // / post-transform unit-square space and clip the whole image away. The `<g>`
189+ // / carries no transform, so the clip is read in the viewBox where it lives.
185190std::string svg_image_fragment (const pdf::ImageElement &image,
186191 const util::math::Transform2D &to_box,
187192 const std::string &clip_id) {
@@ -194,13 +199,18 @@ std::string svg_image_fragment(const pdf::ImageElement &image,
194199 const util::math::Transform2D m = flip * image.transform * to_box;
195200
196201 std::ostringstream f;
202+ // The clip wraps the image in a transform-free `<g>` rather than sitting on
203+ // the `<image>`: see the function comment.
204+ if (!clip_id.empty ()) {
205+ f << " <g clip-path=\" url(#" << clip_id << " )\" >" ;
206+ }
197207 f << R"( <image width="1" height="1" preserveAspectRatio="none" transform="matrix()"
198208 << m.a << ' ,' << m.b << ' ,' << m.c << ' ,' << m.d << ' ,' << round2 (m.e )
199209 << ' ,' << round2 (m.f ) << " )\" " ;
210+ f << " href=\" " << file_to_url (image.data , image.mime ) << " \" />" ;
200211 if (!clip_id.empty ()) {
201- f << " clip-path= \" url(# " << clip_id << " ) \" " ;
212+ f << " </g> " ;
202213 }
203- f << " href=\" " << file_to_url (image.data , image.mime ) << " \" />" ;
204214 return std::move (f).str ();
205215}
206216
@@ -621,7 +631,29 @@ class HtmlServiceImpl final : public HtmlService {
621631 // Tr 3 (invisible) and Tr 7 (clip-only) paint nothing; keep them
622632 // selectable via the transparent `.i` class.
623633 const bool invisible =
624- text.rendering_mode == 3 || text.rendering_mode == 7 ;
634+ text.rendering_mode == pdf::TextRenderingMode::invisible ||
635+ text.rendering_mode == pdf::TextRenderingMode::clip;
636+
637+ // The run's visible paint colour, folded onto the visible span as an
638+ // interned colour class — but only when it is not the default black, so
639+ // the overwhelmingly common black run adds nothing. The per-font
640+ // `.fvN`/`.gvN` classes declare `color:#000`; this class is emitted
641+ // after them in <head> (equal specificity), so it overrides. Invisible
642+ // runs (Tr 3/7) stay transparent via `.i`, so they take no colour
643+ // class. The fill modes paint with the non-stroking colour, the
644+ // stroke-only modes (Tr 1/5) with the stroking colour.
645+ std::string color_suffix;
646+ if (!invisible) {
647+ const pdf::GraphicsState::Color &paint =
648+ (text.rendering_mode == pdf::TextRenderingMode::stroke ||
649+ text.rendering_mode == pdf::TextRenderingMode::stroke_clip)
650+ ? text.stroke_color
651+ : text.fill_color ;
652+ if (std::string css = device_color_to_css (paint);
653+ css != " rgb(0,0,0)" ) {
654+ color_suffix = ' ' + styles.intern (" k" , " color:" + std::move (css));
655+ }
656+ }
625657
626658 // Placement and spacing are shared by both layers of a run; build them
627659 // once on `base`.
@@ -740,17 +772,18 @@ class HtmlServiceImpl final : public HtmlService {
740772 std::string classes = std::move (base);
741773 classes += ' ' ;
742774 classes += font_class (font, invisible, /* nested=*/ false );
775+ classes += color_suffix;
743776 page_out.items .push_back (
744777 SpanOut{std::move (classes), escape_text (text.text ), {}, {}});
745778 } else {
746779 // Dual layer (a glyph lost its scalar to an earlier one): a
747780 // transparent selectable Unicode span with the PUA glyph layer
748781 // nested inside, the latter folded into the combined `.gvN` /
749- // `.giN` class.
750- page_out.items .push_back (
751- SpanOut{ base + " i" , escape_text (text.text ),
752- font_class (font, invisible, /* nested=*/ true ),
753- escape_text (glyph_run (*text.font , text.codes ))});
782+ // `.giN` class. The colour rides the visible (nested) layer.
783+ page_out.items .push_back (SpanOut{
784+ base + " i" , escape_text (text.text ),
785+ font_class (font, invisible, /* nested=*/ true ) + color_suffix ,
786+ escape_text (glyph_run (*text.font , text.codes ))});
754787 }
755788 } else if (font != 0 ) {
756789 // The visible glyph layer: PUA code points in the embedded font,
@@ -763,16 +796,17 @@ class HtmlServiceImpl final : public HtmlService {
763796 // Unicode (for copy/search) with the glyph layer nested inside.
764797 // The nested child overlays the run origin and inherits the
765798 // placement via the combined `.gvN` / `.giN` class.
766- page_out.items .push_back (
767- SpanOut{ base + " i" , escape_text (text.text ),
768- font_class (font, invisible, /* nested=*/ true ),
769- std::move (glyph_text)});
799+ page_out.items .push_back (SpanOut{
800+ base + " i" , escape_text (text.text ),
801+ font_class (font, invisible, /* nested=*/ true ) + color_suffix ,
802+ std::move (glyph_text)});
770803 } else {
771804 // Display-only run: nothing is extractable (the `no_unicode` case),
772805 // so the glyph layer stands alone and carries the placement itself
773806 // (`base`), `.g` (unselectable) and the combined paint+font class.
774807 std::string glyph_classes = base + " g " ;
775808 glyph_classes += font_class (font, invisible, /* nested=*/ false );
809+ glyph_classes += color_suffix;
776810 page_out.items .push_back (SpanOut{
777811 std::move (glyph_classes), std::move (glyph_text), {}, {}});
778812 }
@@ -783,6 +817,7 @@ class HtmlServiceImpl final : public HtmlService {
783817 if (invisible) {
784818 classes += " i" ;
785819 }
820+ classes += color_suffix;
786821 page_out.items .push_back (
787822 SpanOut{std::move (classes), escape_text (text.text ), {}, {}});
788823 }
0 commit comments