blob: 22e2f85587ab06f26daa494532b38267a8407bc6 [file] [log] [blame]
Xavier Phane29cdaf2020-03-26 16:15:14 +00001/*
2 * Copyright 2019 Google Inc.
3 *
4 * Use of this source code is governed by a BSD-style license that can be
5 * found in the LICENSE file.
6 */
7
Florin Malitab3418102020-10-15 18:10:29 -04008#include "modules/svg/include/SkSVGText.h"
Xavier Phane29cdaf2020-03-26 16:15:14 +00009
Florin Malitafebb1b82020-12-17 11:03:47 -050010#include <limits>
Florin Malita7dc984a2020-12-08 11:37:15 -050011
Xavier Phane29cdaf2020-03-26 16:15:14 +000012#include "include/core/SkCanvas.h"
Florin Malita512ff752020-12-06 11:50:52 -050013#include "include/core/SkFont.h"
Florin Malita39fe8c82020-10-20 10:43:03 -040014#include "include/core/SkFontMgr.h"
Tyler Freemane9663db2020-04-14 14:37:13 -070015#include "include/core/SkFontStyle.h"
16#include "include/core/SkString.h"
Florin Malita7dc984a2020-12-08 11:37:15 -050017#include "modules/skshaper/include/SkShaper.h"
Florin Malitab3418102020-10-15 18:10:29 -040018#include "modules/svg/include/SkSVGRenderContext.h"
19#include "modules/svg/include/SkSVGValue.h"
Florin Malita9c1f1be2020-12-09 13:02:50 -050020#include "src/utils/SkUTF.h"
Xavier Phane29cdaf2020-03-26 16:15:14 +000021
Florin Malita512ff752020-12-06 11:50:52 -050022namespace {
Xavier Phane29cdaf2020-03-26 16:15:14 +000023
Florin Malita512ff752020-12-06 11:50:52 -050024static SkFont ResolveFont(const SkSVGRenderContext& ctx) {
Florin Malita39fe8c82020-10-20 10:43:03 -040025 auto weight = [](const SkSVGFontWeight& w) {
26 switch (w.type()) {
27 case SkSVGFontWeight::Type::k100: return SkFontStyle::kThin_Weight;
28 case SkSVGFontWeight::Type::k200: return SkFontStyle::kExtraLight_Weight;
29 case SkSVGFontWeight::Type::k300: return SkFontStyle::kLight_Weight;
30 case SkSVGFontWeight::Type::k400: return SkFontStyle::kNormal_Weight;
31 case SkSVGFontWeight::Type::k500: return SkFontStyle::kMedium_Weight;
32 case SkSVGFontWeight::Type::k600: return SkFontStyle::kSemiBold_Weight;
33 case SkSVGFontWeight::Type::k700: return SkFontStyle::kBold_Weight;
34 case SkSVGFontWeight::Type::k800: return SkFontStyle::kExtraBold_Weight;
35 case SkSVGFontWeight::Type::k900: return SkFontStyle::kBlack_Weight;
36 case SkSVGFontWeight::Type::kNormal: return SkFontStyle::kNormal_Weight;
37 case SkSVGFontWeight::Type::kBold: return SkFontStyle::kBold_Weight;
38 case SkSVGFontWeight::Type::kBolder: return SkFontStyle::kExtraBold_Weight;
39 case SkSVGFontWeight::Type::kLighter: return SkFontStyle::kLight_Weight;
40 case SkSVGFontWeight::Type::kInherit: {
41 SkASSERT(false);
42 return SkFontStyle::kNormal_Weight;
43 }
44 }
45 SkUNREACHABLE;
46 };
47
48 auto slant = [](const SkSVGFontStyle& s) {
49 switch (s.type()) {
50 case SkSVGFontStyle::Type::kNormal: return SkFontStyle::kUpright_Slant;
51 case SkSVGFontStyle::Type::kItalic: return SkFontStyle::kItalic_Slant;
52 case SkSVGFontStyle::Type::kOblique: return SkFontStyle::kOblique_Slant;
53 case SkSVGFontStyle::Type::kInherit: {
54 SkASSERT(false);
55 return SkFontStyle::kUpright_Slant;
56 }
57 }
58 SkUNREACHABLE;
59 };
60
61 const auto& family = ctx.presentationContext().fInherited.fFontFamily->family();
62 const SkFontStyle style(weight(*ctx.presentationContext().fInherited.fFontWeight),
63 SkFontStyle::kNormal_Width,
64 slant(*ctx.presentationContext().fInherited.fFontStyle));
65
66 const auto size =
67 ctx.lengthContext().resolve(ctx.presentationContext().fInherited.fFontSize->size(),
68 SkSVGLengthContext::LengthType::kVertical);
69
Florin Malita7006e152020-11-10 15:24:59 -050070 // TODO: we likely want matchFamilyStyle here, but switching away from legacyMakeTypeface
71 // changes all the results when using the default fontmgr.
72 auto tf = ctx.fontMgr()->legacyMakeTypeface(family.c_str(), style);
73
74 SkFont font(std::move(tf), size);
Florin Malita39fe8c82020-10-20 10:43:03 -040075 font.setHinting(SkFontHinting::kNone);
76 font.setSubpixel(true);
77 font.setLinearMetrics(true);
78 font.setBaselineSnap(false);
79 font.setEdging(SkFont::Edging::kAntiAlias);
80
81 return font;
82}
83
Florin Malitafebb1b82020-12-17 11:03:47 -050084static float ComputeAlignmentFactor(const SkSVGPresentationContext& pctx) {
85 switch (pctx.fInherited.fTextAnchor->type()) {
Florin Malita7dc984a2020-12-08 11:37:15 -050086 case SkSVGTextAnchor::Type::kStart : return 0.0f;
87 case SkSVGTextAnchor::Type::kMiddle: return -0.5f;
88 case SkSVGTextAnchor::Type::kEnd : return -1.0f;
89 case SkSVGTextAnchor::Type::kInherit:
90 SkASSERT(false);
91 return 0.0f;
92 }
93 SkUNREACHABLE;
94}
95
Florin Malitafebb1b82020-12-17 11:03:47 -050096// Helper for encoding optional positional attributes.
97class PosAttrs {
98public:
99 // TODO: dx, dy, rotate
100 enum Attr : size_t {
101 kX = 0,
102 kY = 1,
103 };
104
105 float operator[](Attr a) const { return fStorage[a]; }
106 float& operator[](Attr a) { return fStorage[a]; }
107
108 bool has(Attr a) const { return fStorage[a] != kNone; }
109 bool hasAny() const { return this->has(kX) || this->has(kY); }
110
111private:
112 static constexpr auto kNone = std::numeric_limits<float>::infinity();
113
114 float fStorage[2] = { kNone, kNone };
115};
116
Florin Malita512ff752020-12-06 11:50:52 -0500117} // namespace
118
Florin Malita7dc984a2020-12-08 11:37:15 -0500119// SkSVGTextContext is responsible for sequencing input text chars into "chunks".
120// A single text chunk can span multiple structural elements (<text>, <tspan>, etc),
121// and per [1] new chunks are emitted
122//
123// a) for each top level text element (<text>, <textPath>)
124// b) whenever a character with an explicit absolute position is encountered
125//
126// The implementation queues shaped run data until a full text chunk is resolved, at which
127// point we have enough information to perform final alignment and rendering.
128//
129// [1] https://www.w3.org/TR/SVG11/text.html#TextLayoutIntroduction
130class SkSVGTextContext final : SkShaper::RunHandler {
131public:
Florin Malitafebb1b82020-12-17 11:03:47 -0500132
133 // Helper for cascading position attribute resolution (x, y, dx, dy, rotate) [1]:
134 // - each text position element can specify an arbitrary-length attribute array
135 // - for each character, we look up a given attribute first in its local attribute array,
136 // then in the ancestor chain (cascading/fallback) - and return the first value encountered.
137 // - the lookup is based on character index relative to the text content subtree
138 // (i.e. the index crosses chunk boundaries)
139 //
140 // [1] https://www.w3.org/TR/SVG11/text.html#TSpanElementXAttribute
141 class ScopedPosResolver {
142 public:
143 ScopedPosResolver(const SkSVGTextContainer& txt, const SkSVGLengthContext& lctx,
144 SkSVGTextContext* tctx, size_t charIndexOffset)
145 : fTextContext(tctx)
146 , fParent(tctx->fPosResolver)
147 , fCharIndexOffset(charIndexOffset)
148 , fX(ResolveLengths(lctx, txt.getX(), SkSVGLengthContext::LengthType::kHorizontal))
149 , fY(ResolveLengths(lctx, txt.getY(), SkSVGLengthContext::LengthType::kVertical))
150 {
151 fTextContext->fPosResolver = this;
152 }
153
154 ScopedPosResolver(const SkSVGTextContainer& txt, const SkSVGLengthContext& lctx,
155 SkSVGTextContext* tctx)
156 : ScopedPosResolver(txt, lctx, tctx, tctx->fCurrentCharIndex) {}
157
158 ~ScopedPosResolver() {
159 fTextContext->fPosResolver = fParent;
160 }
161
162 PosAttrs resolve(size_t charIndex) const {
163 PosAttrs attrs;
164
165 if (charIndex < fLastPosIndex) {
166 SkASSERT(charIndex >= fCharIndexOffset);
167 const auto localCharIndex = charIndex - fCharIndexOffset;
168
169 const auto hasAllLocal = localCharIndex < fX.size() &&
170 localCharIndex < fY.size();
171 if (!hasAllLocal && fParent) {
172 attrs = fParent->resolve(charIndex);
173 }
174
175 if (localCharIndex < fX.size()) {
176 attrs[PosAttrs::kX] = fX[localCharIndex];
177 }
178 if (localCharIndex < fY.size()) {
179 attrs[PosAttrs::kY] = fY[localCharIndex];
180 }
181
182 if (!attrs.hasAny()) {
183 // Once we stop producing explicit position data, there is no reason to
184 // continue trying for higher indices. We can suppress future lookups.
185 fLastPosIndex = charIndex;
186 }
187 }
188
189 return attrs;
190 }
191
192 private:
193 static std::vector<float> ResolveLengths(const SkSVGLengthContext& lctx,
194 const std::vector<SkSVGLength>& lengths,
195 SkSVGLengthContext::LengthType lt) {
196 std::vector<float> resolved;
197 resolved.reserve(lengths.size());
198
199 for (const auto& l : lengths) {
200 resolved.push_back(lctx.resolve(l, lt));
201 }
202
203 return resolved;
204 }
205
206 SkSVGTextContext* fTextContext;
207 const ScopedPosResolver* fParent; // parent resolver (fallback)
208 const size_t fCharIndexOffset; // start index for the current resolver
209 const std::vector<float> fX,
210 fY;
211
212 // cache for the last known index with explicit positioning
213 mutable size_t fLastPosIndex = std::numeric_limits<size_t>::max();
214
215 };
216
217 SkSVGTextContext(const SkSVGPresentationContext& pctx, sk_sp<SkFontMgr> fmgr)
218 : fShaper(SkShaper::Make(std::move(fmgr)))
219 , fChunkPos{ 0, 0 }
220 , fChunkAlignmentFactor(ComputeAlignmentFactor(pctx))
Florin Malita7dc984a2020-12-08 11:37:15 -0500221 {}
222
223 // Queues codepoints for rendering.
Florin Malitaadc68892020-12-15 10:52:26 -0500224 void appendFragment(const SkString& txt, const SkSVGRenderContext& ctx, SkSVGXmlSpace xs) {
Florin Malita9c1f1be2020-12-09 13:02:50 -0500225 // https://www.w3.org/TR/SVG11/text.html#WhiteSpace
226 // https://www.w3.org/TR/2008/REC-xml-20081126/#NT-S
227 auto filterWSDefault = [this](SkUnichar ch) -> SkUnichar {
228 // Remove all newline chars.
229 if (ch == '\n') {
230 return -1;
231 }
232
233 // Convert tab chars to space.
234 if (ch == '\t') {
235 ch = ' ';
236 }
237
238 // Consolidate contiguous space chars and strip leading spaces (fPrevCharSpace
239 // starts off as true).
240 if (fPrevCharSpace && ch == ' ') {
241 return -1;
242 }
243
244 // TODO: Strip trailing WS? Doing this across chunks would require another buffering
245 // layer. In general, trailing WS should have no rendering side effects. Skipping
246 // for now.
247 return ch;
248 };
249 auto filterWSPreserve = [](SkUnichar ch) -> SkUnichar {
250 // Convert newline and tab chars to space.
251 if (ch == '\n' || ch == '\t') {
252 ch = ' ';
253 }
254 return ch;
255 };
256
Florin Malitafebb1b82020-12-17 11:03:47 -0500257 // Stash paints for access from SkShaper callbacks.
258 fCurrentFill = ctx.fillPaint();
259 fCurrentStroke = ctx.strokePaint();
260
261 const auto font = ResolveFont(ctx);
262
Florin Malita9c1f1be2020-12-09 13:02:50 -0500263 SkSTArray<128, char, true> filtered;
264 filtered.reserve_back(SkToInt(txt.size()));
265
Florin Malitafebb1b82020-12-17 11:03:47 -0500266 auto shapePending = [&filtered, &font, this]() {
267 // TODO: directionality hints?
268 const auto LTR = true;
269 // Initiate shaping: this will generate a series of runs via callbacks.
270 fShaper->shape(filtered.data(), filtered.size(), font, LTR, SK_ScalarMax, this);
271 filtered.reset();
272 };
273
Florin Malita9c1f1be2020-12-09 13:02:50 -0500274 const char* ch_ptr = txt.c_str();
275 const char* ch_end = ch_ptr + txt.size();
276
277 while (ch_ptr < ch_end) {
278 auto ch = SkUTF::NextUTF8(&ch_ptr, ch_end);
Florin Malitaadc68892020-12-15 10:52:26 -0500279 ch = (xs == SkSVGXmlSpace::kDefault)
Florin Malita9c1f1be2020-12-09 13:02:50 -0500280 ? filterWSDefault(ch)
281 : filterWSPreserve(ch);
282
283 if (ch < 0) {
284 // invalid utf or char filtered out
285 continue;
286 }
287
Florin Malitafebb1b82020-12-17 11:03:47 -0500288 SkASSERT(fPosResolver);
289 const auto pos = fPosResolver->resolve(fCurrentCharIndex++);
290
291 // Absolute position adjustments define a new chunk.
292 // (https://www.w3.org/TR/SVG11/text.html#TextLayoutIntroduction)
293 if (pos.has(PosAttrs::kX) || pos.has(PosAttrs::kY)) {
294 shapePending();
295 this->flushChunk(ctx);
296
297 // New chunk position.
298 if (pos.has(PosAttrs::kX)) {
299 fChunkPos.fX = pos[PosAttrs::kX];
300 }
301 if (pos.has(PosAttrs::kY)) {
302 fChunkPos.fY = pos[PosAttrs::kY];
303 }
304 }
305
Florin Malita9c1f1be2020-12-09 13:02:50 -0500306 char utf8_buf[SkUTF::kMaxBytesInUTF8Sequence];
307 filtered.push_back_n(SkToInt(SkUTF::ToUTF8(ch, utf8_buf)), utf8_buf);
308
309 fPrevCharSpace = (ch == ' ');
310 }
311
Florin Malitafebb1b82020-12-17 11:03:47 -0500312 // Note: at this point we have shaped and buffered the current fragment The active
313 // text chunk continues until an explicit or implicit flush.
314 shapePending();
Florin Malita7dc984a2020-12-08 11:37:15 -0500315 }
316
317 // Perform actual rendering for queued codepoints.
318 void flushChunk(const SkSVGRenderContext& ctx) {
319 // The final rendering offset is determined by cumulative chunk advances and alignment.
320 const auto pos = fChunkPos + fChunkAdvance * fChunkAlignmentFactor;
321
322 SkTextBlobBuilder blobBuilder;
323
324 for (const auto& run : fRuns) {
325 const auto& buf = blobBuilder.allocRunPos(run.font, SkToInt(run.glyphCount));
326 std::copy(run.glyphs .get(), run.glyphs .get() + run.glyphCount, buf.glyphs);
327 std::copy(run.glyphPos.get(), run.glyphPos.get() + run.glyphCount, buf.points());
328
329 // Technically, blobs with compatible paints could be merged --
330 // but likely not worth the effort.
331 const auto blob = blobBuilder.make();
332 if (run.fillPaint) {
333 ctx.canvas()->drawTextBlob(blob, pos.fX, pos.fY, *run.fillPaint);
334 }
335 if (run.strokePaint) {
336 ctx.canvas()->drawTextBlob(blob, pos.fX, pos.fY, *run.strokePaint);
337 }
338 }
339
340 fChunkPos += fChunkAdvance;
341 fChunkAdvance = {0,0};
Florin Malitafebb1b82020-12-17 11:03:47 -0500342 fChunkAlignmentFactor = ComputeAlignmentFactor(ctx.presentationContext());
Florin Malita7dc984a2020-12-08 11:37:15 -0500343
344 fRuns.clear();
345 }
346
347private:
348 struct RunRec {
349 SkFont font;
350 std::unique_ptr<SkPaint> fillPaint,
351 strokePaint;
352 std::unique_ptr<SkGlyphID[]> glyphs;
353 std::unique_ptr<SkPoint[]> glyphPos;
354 size_t glyphCount;
355 SkVector advance;
356 };
357
358 // SkShaper callbacks
359 void beginLine() override {}
360 void runInfo(const RunInfo&) override {}
361 void commitRunInfo() override {}
362 Buffer runBuffer(const RunInfo& ri) override {
363 SkASSERT(ri.glyphCount);
364
365 fRuns.push_back({
366 ri.fFont,
367 fCurrentFill ? std::make_unique<SkPaint>(*fCurrentFill) : nullptr,
368 fCurrentStroke ? std::make_unique<SkPaint>(*fCurrentStroke) : nullptr,
369 std::make_unique<SkGlyphID[]>(ri.glyphCount),
370 std::make_unique<SkPoint[] >(ri.glyphCount),
371 ri.glyphCount,
372 ri.fAdvance,
373 });
374
375 return {
376 fRuns.back().glyphs.get(),
377 fRuns.back().glyphPos.get(),
378 nullptr,
379 nullptr,
380 fChunkAdvance,
381 };
382 }
383 void commitRunBuffer(const RunInfo& ri) override {
384 fChunkAdvance += ri.fAdvance;
385 }
386 void commitLine() override {}
387
388 // http://www.w3.org/TR/SVG11/text.html#TextLayout
389 const std::unique_ptr<SkShaper> fShaper;
390 std::vector<RunRec> fRuns;
Florin Malitafebb1b82020-12-17 11:03:47 -0500391 const ScopedPosResolver* fPosResolver = nullptr;
Florin Malita7dc984a2020-12-08 11:37:15 -0500392
393 SkPoint fChunkPos; // current text chunk position
394 SkVector fChunkAdvance = {0,0}; // cumulative advance
395 float fChunkAlignmentFactor; // current chunk alignment
396
Florin Malitafebb1b82020-12-17 11:03:47 -0500397 // tracks the global text subtree char index (cross chunks). Used for position resolution.
398 size_t fCurrentCharIndex = 0;
399
Florin Malita7dc984a2020-12-08 11:37:15 -0500400 // cached for access from SkShaper callbacks.
401 const SkPaint* fCurrentFill;
402 const SkPaint* fCurrentStroke;
Florin Malita9c1f1be2020-12-09 13:02:50 -0500403
404 bool fPrevCharSpace = true; // WS filter state
Florin Malita512ff752020-12-06 11:50:52 -0500405};
406
Florin Malitaadc68892020-12-15 10:52:26 -0500407void SkSVGTextFragment::renderText(const SkSVGRenderContext& ctx, SkSVGTextContext* tctx,
408 SkSVGXmlSpace xs) const {
409 SkSVGRenderContext localContext(ctx, this);
410
411 if (this->onPrepareToRender(&localContext)) {
412 this->onRenderText(localContext, tctx, xs);
413 }
414}
415
416SkPath SkSVGTextFragment::onAsPath(const SkSVGRenderContext&) const {
417 // TODO
418 return SkPath();
419}
420
Florin Malita512ff752020-12-06 11:50:52 -0500421void SkSVGTextContainer::appendChild(sk_sp<SkSVGNode> child) {
422 // Only allow text nodes.
423 switch (child->tag()) {
424 case SkSVGTag::kText:
425 case SkSVGTag::kTextLiteral:
426 case SkSVGTag::kTSpan:
Florin Malitaadc68892020-12-15 10:52:26 -0500427 fChildren.push_back(
428 sk_sp<SkSVGTextFragment>(static_cast<SkSVGTextFragment*>(child.release())));
Florin Malita512ff752020-12-06 11:50:52 -0500429 break;
430 default:
431 break;
432 }
433}
434
Florin Malitaadc68892020-12-15 10:52:26 -0500435void SkSVGTextContainer::onRenderText(const SkSVGRenderContext& ctx, SkSVGTextContext* tctx,
436 SkSVGXmlSpace) const {
Florin Malitafebb1b82020-12-17 11:03:47 -0500437 const SkSVGTextContext::ScopedPosResolver resolver(*this, ctx.lengthContext(), tctx);
438
Florin Malitaadc68892020-12-15 10:52:26 -0500439 for (const auto& frag : fChildren) {
440 // Containers always override xml:space with the local value.
441 frag->renderText(ctx, tctx, this->getXmlSpace());
442 }
Florin Malita9c1f1be2020-12-09 13:02:50 -0500443}
444
445// https://www.w3.org/TR/SVG11/text.html#WhiteSpace
446template <>
447bool SkSVGAttributeParser::parse(SkSVGXmlSpace* xs) {
448 static constexpr std::tuple<const char*, SkSVGXmlSpace> gXmlSpaceMap[] = {
449 {"default" , SkSVGXmlSpace::kDefault },
450 {"preserve", SkSVGXmlSpace::kPreserve},
451 };
452
453 return this->parseEnumMap(gXmlSpaceMap, xs) && this->parseEOSToken();
454}
455
Florin Malita512ff752020-12-06 11:50:52 -0500456bool SkSVGTextContainer::parseAndSetAttribute(const char* name, const char* value) {
457 return INHERITED::parseAndSetAttribute(name, value) ||
Florin Malitafebb1b82020-12-17 11:03:47 -0500458 this->setX(SkSVGAttributeParser::parse<std::vector<SkSVGLength>>("x", name, value)) ||
459 this->setY(SkSVGAttributeParser::parse<std::vector<SkSVGLength>>("y", name, value)) ||
Florin Malita9c1f1be2020-12-09 13:02:50 -0500460 this->setXmlSpace(SkSVGAttributeParser::parse<SkSVGXmlSpace>("xml:space", name, value));
Florin Malita512ff752020-12-06 11:50:52 -0500461}
462
Florin Malitaadc68892020-12-15 10:52:26 -0500463void SkSVGTextContainer::onRender(const SkSVGRenderContext& ctx) const {
464 // Root text nodes establish a new text layout context.
Florin Malitafebb1b82020-12-17 11:03:47 -0500465 SkSVGTextContext tctx(ctx.presentationContext(), ctx.fontMgr());
Florin Malita512ff752020-12-06 11:50:52 -0500466
Florin Malitaadc68892020-12-15 10:52:26 -0500467 this->onRenderText(ctx, &tctx, this->getXmlSpace());
Florin Malita512ff752020-12-06 11:50:52 -0500468
Florin Malita7dc984a2020-12-08 11:37:15 -0500469 tctx.flushChunk(ctx);
Florin Malita512ff752020-12-06 11:50:52 -0500470}
471
Florin Malitaadc68892020-12-15 10:52:26 -0500472void SkSVGTextLiteral::onRenderText(const SkSVGRenderContext& ctx, SkSVGTextContext* tctx,
473 SkSVGXmlSpace xs) const {
474 SkASSERT(tctx);
Florin Malita512ff752020-12-06 11:50:52 -0500475
Florin Malitaadc68892020-12-15 10:52:26 -0500476 tctx->appendFragment(this->getText(), ctx, xs);
Xavier Phane29cdaf2020-03-26 16:15:14 +0000477}