Inkscape
Vector Graphics Editor
Loading...
Searching...
No Matches
build-text.cpp
Go to the documentation of this file.
1// SPDX-License-Identifier: GPL-2.0-or-later
2/*
3 * Specific functionality for text handling
4 *
5 * Authors:
6 * Martin Owens <doctormo@geek-2.com>
7 *
8 * Copyright (C) 2024 Authors
9 *
10 * Released under GNU GPL v2+, read the file 'COPYING' for more information.
11 */
12
13#include "build-text.h"
14
15#include <codecvt>
16#include <locale>
17
18#include "build-drawing.h"
21#include "style.h"
22
24
25static std::string unicodeToUtf8(std::vector<gunichar> const &chars)
26{
27 std::string text;
28 std::string utf8_code;
29 static std::wstring_convert<std::codecvt_utf8<char32_t>, char32_t> conv1;
30 // Note std::wstring_convert and std::codecvt_utf are deprecated and will be removed in C++26.
31 for (auto c : chars) {
32 utf8_code = conv1.to_bytes(c);
33 text += utf8_code;
34 }
35 return text;
36}
37
38TextContext::TextContext(Document &doc, capypdf::DrawContext &ctx, bool soft_mask)
39 : _doc(doc)
40 , _ctx(ctx)
41 , _tx(ctx.text_new())
42 , _soft_mask(soft_mask)
43{}
44
45// Because soft masks negate the use of draw opacities, we must fold them in.
46std::optional<double> TextContext::get_softmask(double opacity) const
47{
48 if (_soft_mask) {
49 return opacity;
50 }
51 return {};
52}
53
57bool TextContext::set_text_style(std::shared_ptr<FontInstance> const &font, SPStyle *style)
58{
59 auto font_filename = font->GetFilename();
60 if (font_filename != last_font) {
61 if (auto font_id = _doc.get_font(font_filename)) {
62 // Transformation has consumed the font size
63 _tx.cmd_Tf(*font_id, 1); // style->font_size.computed);
64 last_font = font_filename;
65 } else {
66 return false;
67 }
68 }
69 if (style->letter_spacing.set && style->letter_spacing.computed != last_letter_spacing) {
70 _tx.cmd_Tc(style->letter_spacing.computed / 1000);
71 last_letter_spacing = style->letter_spacing.computed;
72 }
73 return true;
74}
75
79void TextContext::set_paint_style(StyleMap const &map, SPStyle const *style, SPStyle const *context_style)
80{
81 // NOTE: The pattern and gradients applied to tspans are currently not positioned correctly
82 // but this bug is left in because it is not trivial to fix and is not possible to make an
83 // SVG with the senario using Inkscape at the present time.
84 if (map.contains(SPAttr::FILL)) {
85 if (auto color = _doc.get_paint(style->fill, context_style, get_softmask(style->fill_opacity))) {
86 _tx.set_nonstroke(*color);
87 }
88 }
89 if (map.contains(SPAttr::STROKE)) {
90 if (auto color = _doc.get_paint(style->stroke, context_style, get_softmask(style->stroke_opacity))) {
91 _tx.set_stroke(*color);
92 }
93 }
94 if (map.contains(SPAttr::STROKE_WIDTH)) {
95 _tx.cmd_w(style->stroke_width.computed);
96 }
97 if (map.contains(SPAttr::STROKE_MITERLIMIT)) {
98 _tx.cmd_M(style->stroke_miterlimit.value);
99 }
100 if (map.contains(SPAttr::STROKE_LINECAP)) {
101 _tx.cmd_J(get_linecap(style->stroke_linecap.computed));
102 }
103 if (map.contains(SPAttr::STROKE_LINEJOIN)) {
104 _tx.cmd_j(get_linejoin(style->stroke_linejoin.computed));
105 }
106 if (map.contains(SPAttr::STROKE_DASHARRAY)) {
107 auto values = style->stroke_dasharray.get_computed();
108 if (values.size() > 1) {
109 _tx.cmd_d(values.data(), values.size(), style->stroke_dashoffset.computed);
110 }
111 }
112 if (!_soft_mask) {
113 auto soft_mask = _doc.style_to_transparency_mask(style, nullptr);
114
115 if (soft_mask || last_ca < 1.0 || last_CA < 1.0 || style->fill_opacity < 1.0 || style->stroke_opacity < 1.0) {
116 auto gstate = capypdf::GraphicsState();
117 if (soft_mask) {
118 auto smask = capypdf::SoftMask(CAPY_SOFT_MASK_LUMINOSITY, *soft_mask);
119 gstate.set_SMask(_doc.generator().add_soft_mask(smask));
120 last_ca = 0.0; // Force new gstate for next tspan
121 gstate.set_ca(1.0);
122 gstate.set_CA(1.0);
123 } else {
124 gstate.set_ca(style->fill_opacity);
125 gstate.set_CA(style->stroke_opacity);
126 last_ca = style->fill_opacity;
127 last_CA = style->stroke_opacity;
128 }
129 auto gsid = _doc.generator().add_graphics_state(gstate);
130 _tx.cmd_gs(gsid);
131 }
132 }
133}
134
135void TextContext::set_text_mode(CapyPDF_Text_Mode mode)
136{
137 if (last_text_mode != mode) {
138 _tx.cmd_Tr(mode);
140 }
141}
142
144{
145 _ctx.render_text_obj(_tx);
146}
147
152{
153 double prev_advance = 0.0;
154 Geom::Affine prev_Tm;
155 std::unique_ptr<capypdf::TextSequence> seq;
156
157 for (auto &glyph : layout.glyphs()) {
158 if (glyph.hidden) {
159 continue;
160 }
161
162 // If this glyph is in this span (this is bad)
163 if (&glyph.span(&layout) == &span) {
164 auto Tm = glyph.transform(layout);
165 auto delta = Tm * prev_Tm.inverse();
166 auto tr = delta.translation();
167 auto chars = glyph.characters(&layout);
168 auto text = unicodeToUtf8(chars);
169
170 // Our layout engine has a bug where CR/LFs are given an out of memory glyph code.
171 if (chars.size() == 1 && (chars[0] == '\n' || chars[0] == '\r')) {
172 continue; // We don't know why Glyphs would have this not set to hidden
173 }
174
175 // TODO: We could detect vertical text and support top-down progression and kerning
176 // but this is not within the scope of this work so is left for a future adventure.
177
178 // Each time the glyph position changes, we calculate it's change and if it's not
179 // A simple progression we can control with kerning, we have to finish the sequence
180 // and add a new transform for the next glyph.
181 if (seq && delta.isTranslation() && Geom::are_near(tr[Geom::Y], Geom::EPSILON)) {
182 // The kerning is the change in glyph position minus the glyph's advance.
183 // This is because the transform is known-good and will position glyphs perfectly
184 // But the kerning is *relative* to the glyph_width
185 int kerning = (tr[Geom::X] - prev_advance) * -1000;
186 // Kerning is the left-ward shift in integer thous, negate for rightward shift;
187 if (kerning != 0) {
188 seq->append_kerning(kerning);
189 }
190 } else {
191 // Finish previous sequence
192 if (seq) {
193 _tx.cmd_TJ(*seq);
194 }
195 // Start a new sequence of sequential glyphs
196 seq = std::make_unique<capypdf::TextSequence>();
197 _tx.cmd_Tm(Tm[0], Tm[1], Tm[2], Tm[3], Tm[4], Tm[5]);
198 }
199
200 if (chars.size() == 1) {
201 // std::cout << " seq->append_raw_glyph(" << glyph.glyph << ", '" << chars[0] << "');\n";
202 seq->append_raw_glyph(glyph.glyph, chars[0]);
203 } else if (chars.size() > 1) {
204 // std::cout << " seq->append_ligature_glyph(" << glyph.glyph << ", \"" << text.c_str() << "\");\n";
205 seq->append_ligature_glyph(glyph.glyph, text.c_str());
206 }
207
208 prev_advance = span.font->Advance(glyph.glyph, false); // glyph.advance;
209 prev_Tm = Tm;
210 }
211 }
212 if (seq) {
213 _tx.cmd_TJ(*seq);
214 }
215}
216
221{
223
224 if (layout.getActualLength() == 0) {
225 return;
226 }
227
228 auto tx = TextContext(_doc, _ctx, _soft_mask);
229 tx.set_text_mode(CAPY_TEXT_CLIP);
230
231 auto &input_stream = layout.input_stream();
232 for (Layout::Span const &span : layout.spans()) {
233 auto text_source = static_cast<Layout::InputStreamTextSource const *>(input_stream[span.in_input_stream_item]);
234 if (text_source->Type() != Layout::InputStreamItemType::TEXT_SOURCE || span.line(&layout).hidden ||
235 !span.font) {
236 continue;
237 }
238 if (!tx.set_text_style(span.font, text_source->style)) {
239 std::cerr << "Can't clip to text, no font available\n";
240 continue;
241 }
242 tx.render_text(layout, span);
243 }
244 try {
245 tx.finalize();
246 } catch (std::exception const &err) {
247 std::cerr << "Can't output text block:" << err.what() << "\n";
248 }
249}
250
256void DrawContext::paint_text_layout(Text::Layout const &layout, SPStyle const *context_style)
257{
259
260 if (layout.getActualLength() == 0) {
261 return;
262 }
263
264 auto tx = TextContext(_doc, _ctx, _soft_mask);
265
266 // Copy the paint style memory as the entire text block has a continuous style which does
267 // inherit from what was set just before this call, but may also modify styles in a linear fashion.
268 StyleMemory text_paint_memory = _doc.paint_memory();
269
270 auto &input_stream = layout.input_stream();
271 for (Layout::Span const &span : layout.spans()) {
272 auto text_source = static_cast<Layout::InputStreamTextSource const *>(input_stream[span.in_input_stream_item]);
273
274 if (text_source->Type() != Layout::InputStreamItemType::TEXT_SOURCE || span.line(&layout).hidden ||
275 !span.font) {
276 // Hidden spans correspond to text overlfow.
277 continue;
278 }
279
280 // This non-scoped memory means the PDF lacks style scope within the Text Block.
281 auto style = text_source->style;
282 auto style_map = text_paint_memory.get_changes_and_remember(style);
283 tx.set_paint_style(style_map, style, context_style);
284
285 if (!tx.set_text_style(span.font, style)) {
286 std::cerr << "Can't export text, no font available\n";
287 continue;
288 }
289
290 for (auto layer : get_paint_layers(style, context_style)) {
291 switch (layer) {
292 case PAINT_FILLSTROKE:
293 tx.set_text_mode(CAPY_TEXT_FILL_STROKE);
294 tx.render_text(layout, span);
295 break;
296 case PAINT_FILL:
297 tx.set_text_mode(CAPY_TEXT_FILL);
298 tx.render_text(layout, span);
299 break;
300 case PAINT_STROKE:
301 tx.set_text_mode(CAPY_TEXT_STROKE);
302 tx.render_text(layout, span);
303 break;
304 case PAINT_MARKERS:
305 break; // NOT ALLOWED
306 }
307 }
308 }
309
310 try {
311 tx.finalize();
312 } catch (std::exception const &err) {
313 std::cerr << "Can't output text block:" << err.what() << "\n";
314 }
315}
316
317} // namespace Inkscape::Extension::Internal::PdfBuilder
@ STROKE_LINECAP
@ STROKE_WIDTH
@ STROKE_LINEJOIN
@ STROKE_MITERLIMIT
@ STROKE_DASHARRAY
3x3 matrix representing an affine transformation.
Definition affine.h:70
Point translation() const
Gets the translation imparted by the Affine.
Definition affine.cpp:41
Affine inverse() const
Compute the inverse matrix.
Definition affine.cpp:388
std::optional< CapyPDF_FontId > get_font(std::string const &filename)
Load a font and cache the results.
std::optional< capypdf::Color > get_paint(SPIPaint const &paint, SPStyle const *context_style, std::optional< double > opacity)
Generate a solid color, gradient or pattern based on the SPIPaint.
std::optional< CapyPDF_TransparencyGroupId > style_to_transparency_mask(SPStyle const *style, SPStyle const *context_style)
Render gradient transparencies into a transparency mask.
void clip_text_layout(Text::Layout const &layout)
Use the text object as a clipping mask in the PDF.
void paint_text_layout(Text::Layout const &layout, SPStyle const *context_style)
Paint the given layout into the PDF document Drawing content.
std::optional< double > get_softmask(double opacity) const
bool set_text_style(std::shared_ptr< FontInstance > const &font, SPStyle *style)
Set the text/font style, these are comment between clipping and painting.
TextContext(Document &doc, capypdf::DrawContext &ctx, bool soft_mask)
void render_text(Text::Layout const &layout, Text::Layout::Span const &span)
Render the text span into the text context.
void set_paint_style(StyleMap const &map, SPStyle const *style, SPStyle const *context_style)
Set the painting style, this is not applicable to clipping.
StyleMap get_changes_and_remember(SPStyle const *style)
Modify the changes scope without managing the FILO stack.
Generates the layout for either wrapped or non-wrapped text and stores the result.
Definition Layout-TNG.h:144
std::vector< Layout::Glyph > const & glyphs() const
Publically allow access to the Layout::show logic outside of the Layout engine.
Definition Layout-TNG.h:898
std::vector< InputStreamItem * > const & input_stream() const
Definition Layout-TNG.h:900
double getActualLength() const
Get actual length of layout, by summing span lengths.
std::vector< Layout::Span > const & spans() const
Definition Layout-TNG.h:899
An SVG style object.
Definition style.h:45
T< SPAttr::FILL, SPIPaint > fill
fill
Definition style.h:240
T< SPAttr::STROKE_DASHARRAY, SPIDashArray > stroke_dasharray
stroke-dasharray
Definition style.h:257
T< SPAttr::STROKE, SPIPaint > stroke
stroke
Definition style.h:247
T< SPAttr::LETTER_SPACING, SPILengthOrNormal > letter_spacing
letter spacing (css2 16.4)
Definition style.h:153
T< SPAttr::STROKE_WIDTH, SPILength > stroke_width
stroke-width
Definition style.h:249
T< SPAttr::FILL_OPACITY, SPIScale24 > fill_opacity
fill-opacity
Definition style.h:242
T< SPAttr::STROKE_LINEJOIN, SPIEnum< SPStrokeJoinType > > stroke_linejoin
stroke-linejoin
Definition style.h:253
T< SPAttr::STROKE_OPACITY, SPIScale24 > stroke_opacity
stroke-opacity
Definition style.h:261
T< SPAttr::STROKE_MITERLIMIT, SPIFloat > stroke_miterlimit
stroke-miterlimit
Definition style.h:255
T< SPAttr::STROKE_LINECAP, SPIEnum< SPStrokeCapType > > stroke_linecap
stroke-linecap
Definition style.h:251
T< SPAttr::STROKE_DASHOFFSET, SPILength > stroke_dashoffset
stroke-dashoffset
Definition style.h:259
Inkscape::XML::Document * _doc
Reference to the clipboard's Inkscape::XML::Document.
std::unordered_map< std::string, std::unique_ptr< SPDocument > > map
double c[8][4]
The data describing a single loaded font.
constexpr Coord EPSILON
Default "acceptably small" value.
Definition coord.h:84
@ Y
Definition coord.h:48
@ X
Definition coord.h:48
bool are_near(Affine const &a1, Affine const &a2, Coord eps=EPSILON)
CapyPDF_Line_Join get_linejoin(SPStrokeJoinType mode)
static std::string unicodeToUtf8(std::vector< gunichar > const &chars)
CapyPDF_Line_Cap get_linecap(SPStrokeCapType mode)
std::vector< PaintLayer > get_paint_layers(SPStyle const *style, SPStyle const *context_style)
Get a PDF specific layer painting pattern for fill, stroke and markers.
static void err(const char *fmt,...)
Definition pov-out.cpp:57
std::map< SPAttr, std::string > StyleMap
int mode
std::shared_ptr< FontInstance > font
Definition Layout-TNG.h:857
SPStyle - a style object for SPItem objects.
int delta