/* * Copyright 2015 Google Inc. * * Use of this source code is governed by a BSD-style license that can be * found in the LICENSE file. */ #include "SkSVGDevice.h" #include "SkBase64.h" #include "SkBitmap.h" #include "SkChecksum.h" #include "SkClipStack.h" #include "SkData.h" #include "SkDraw.h" #include "SkImageEncoder.h" #include "SkPaint.h" #include "SkParsePath.h" #include "SkShader.h" #include "SkStream.h" #include "SkTHash.h" #include "SkTypeface.h" #include "SkUtils.h" #include "SkXMLWriter.h" namespace { static SkString svg_color(SkColor color) { return SkStringPrintf("rgb(%u,%u,%u)", SkColorGetR(color), SkColorGetG(color), SkColorGetB(color)); } static SkScalar svg_opacity(SkColor color) { return SkIntToScalar(SkColorGetA(color)) / SK_AlphaOPAQUE; } // Keep in sync with SkPaint::Cap static const char* cap_map[] = { nullptr, // kButt_Cap (default) "round", // kRound_Cap "square" // kSquare_Cap }; static_assert(SK_ARRAY_COUNT(cap_map) == SkPaint::kCapCount, "missing_cap_map_entry"); static const char* svg_cap(SkPaint::Cap cap) { SkASSERT(cap < SK_ARRAY_COUNT(cap_map)); return cap_map[cap]; } // Keep in sync with SkPaint::Join static const char* join_map[] = { nullptr, // kMiter_Join (default) "round", // kRound_Join "bevel" // kBevel_Join }; static_assert(SK_ARRAY_COUNT(join_map) == SkPaint::kJoinCount, "missing_join_map_entry"); static const char* svg_join(SkPaint::Join join) { SkASSERT(join < SK_ARRAY_COUNT(join_map)); return join_map[join]; } // Keep in sync with SkPaint::Align static const char* text_align_map[] = { nullptr, // kLeft_Align (default) "middle", // kCenter_Align "end" // kRight_Align }; static_assert(SK_ARRAY_COUNT(text_align_map) == SkPaint::kAlignCount, "missing_text_align_map_entry"); static const char* svg_text_align(SkPaint::Align align) { SkASSERT(align < SK_ARRAY_COUNT(text_align_map)); return text_align_map[align]; } static SkString svg_transform(const SkMatrix& t) { SkASSERT(!t.isIdentity()); SkString tstr; switch (t.getType()) { case SkMatrix::kPerspective_Mask: SkDebugf("Can't handle perspective matrices."); break; case SkMatrix::kTranslate_Mask: tstr.printf("translate(%g %g)", t.getTranslateX(), t.getTranslateY()); break; case SkMatrix::kScale_Mask: tstr.printf("scale(%g %g)", t.getScaleX(), t.getScaleY()); break; default: // http://www.w3.org/TR/SVG/coords.html#TransformMatrixDefined // | a c e | // | b d f | // | 0 0 1 | tstr.printf("matrix(%g %g %g %g %g %g)", t.getScaleX(), t.getSkewY(), t.getSkewX(), t.getScaleY(), t.getTranslateX(), t.getTranslateY()); break; } return tstr; } struct Resources { Resources(const SkPaint& paint) : fPaintServer(svg_color(paint.getColor())) {} SkString fPaintServer; SkString fClip; }; class SVGTextBuilder : SkNoncopyable { public: SVGTextBuilder(const void* text, size_t byteLen, const SkPaint& paint, const SkPoint& offset, unsigned scalarsPerPos, const SkScalar pos[] = nullptr) : fOffset(offset) , fScalarsPerPos(scalarsPerPos) , fPos(pos) , fLastCharWasWhitespace(true) // start off in whitespace mode to strip all leading space { SkASSERT(scalarsPerPos <= 2); SkASSERT(scalarsPerPos == 0 || SkToBool(pos)); int count = paint.countText(text, byteLen); switch(paint.getTextEncoding()) { case SkPaint::kGlyphID_TextEncoding: { SkASSERT(count * sizeof(uint16_t) == byteLen); SkAutoSTArray<64, SkUnichar> unichars(count); paint.glyphsToUnichars((const uint16_t*)text, count, unichars.get()); for (int i = 0; i < count; ++i) { this->appendUnichar(unichars[i]); } } break; case SkPaint::kUTF8_TextEncoding: { const char* c8 = reinterpret_cast(text); for (int i = 0; i < count; ++i) { this->appendUnichar(SkUTF8_NextUnichar(&c8)); } SkASSERT(reinterpret_cast(text) + byteLen == c8); } break; case SkPaint::kUTF16_TextEncoding: { const uint16_t* c16 = reinterpret_cast(text); for (int i = 0; i < count; ++i) { this->appendUnichar(SkUTF16_NextUnichar(&c16)); } SkASSERT(SkIsAlign2(byteLen)); SkASSERT(reinterpret_cast(text) + (byteLen / 2) == c16); } break; case SkPaint::kUTF32_TextEncoding: { SkASSERT(count * sizeof(uint32_t) == byteLen); const uint32_t* c32 = reinterpret_cast(text); for (int i = 0; i < count; ++i) { this->appendUnichar(c32[i]); } } break; default: SkFAIL("unknown text encoding"); } if (scalarsPerPos < 2) { SkASSERT(fPosY.isEmpty()); fPosY.appendScalar(offset.y()); // DrawText or DrawPosTextH (fixed Y). } if (scalarsPerPos < 1) { SkASSERT(fPosX.isEmpty()); fPosX.appendScalar(offset.x()); // DrawText (X also fixed). } } const SkString& text() const { return fText; } const SkString& posX() const { return fPosX; } const SkString& posY() const { return fPosY; } private: void appendUnichar(SkUnichar c) { bool discardPos = false; bool isWhitespace = false; switch(c) { case ' ': case '\t': // consolidate whitespace to match SVG's xml:space=default munging // (http://www.w3.org/TR/SVG/text.html#WhiteSpace) if (fLastCharWasWhitespace) { discardPos = true; } else { fText.appendUnichar(c); } isWhitespace = true; break; case '\0': // SkPaint::glyphsToUnichars() returns \0 for inconvertible glyphs, but these // are not legal XML characters (http://www.w3.org/TR/REC-xml/#charsets) discardPos = true; isWhitespace = fLastCharWasWhitespace; // preserve whitespace consolidation break; case '&': fText.append("&"); break; case '"': fText.append("""); break; case '\'': fText.append("'"); break; case '<': fText.append("<"); break; case '>': fText.append(">"); break; default: fText.appendUnichar(c); break; } this->advancePos(discardPos); fLastCharWasWhitespace = isWhitespace; } void advancePos(bool discard) { if (!discard && fScalarsPerPos > 0) { fPosX.appendf("%.8g, ", fOffset.x() + fPos[0]); if (fScalarsPerPos > 1) { SkASSERT(fScalarsPerPos == 2); fPosY.appendf("%.8g, ", fOffset.y() + fPos[1]); } } fPos += fScalarsPerPos; } const SkPoint& fOffset; const unsigned fScalarsPerPos; const SkScalar* fPos; SkString fText, fPosX, fPosY; bool fLastCharWasWhitespace; }; } // For now all this does is serve unique serial IDs, but it will eventually evolve to track // and deduplicate resources. class SkSVGDevice::ResourceBucket : ::SkNoncopyable { public: ResourceBucket() : fGradientCount(0), fClipCount(0), fPathCount(0), fImageCount(0) {} SkString addLinearGradient() { return SkStringPrintf("gradient_%d", fGradientCount++); } SkString addClip() { return SkStringPrintf("clip_%d", fClipCount++); } SkString addPath() { return SkStringPrintf("path_%d", fPathCount++); } SkString addImage() { return SkStringPrintf("img_%d", fImageCount++); } private: uint32_t fGradientCount; uint32_t fClipCount; uint32_t fPathCount; uint32_t fImageCount; }; class SkSVGDevice::AutoElement : ::SkNoncopyable { public: AutoElement(const char name[], SkXMLWriter* writer) : fWriter(writer) , fResourceBucket(nullptr) { fWriter->startElement(name); } AutoElement(const char name[], SkXMLWriter* writer, ResourceBucket* bucket, const SkDraw& draw, const SkPaint& paint) : fWriter(writer) , fResourceBucket(bucket) { Resources res = this->addResources(draw, paint); if (!res.fClip.isEmpty()) { // The clip is in device space. Apply it via a wrapper to avoid local transform // interference. fClipGroup.reset(new AutoElement("g", fWriter)); fClipGroup->addAttribute("clip-path",res.fClip); } fWriter->startElement(name); this->addPaint(paint, res); if (!draw.fMatrix->isIdentity()) { this->addAttribute("transform", svg_transform(*draw.fMatrix)); } } ~AutoElement() { fWriter->endElement(); } void addAttribute(const char name[], const char val[]) { fWriter->addAttribute(name, val); } void addAttribute(const char name[], const SkString& val) { fWriter->addAttribute(name, val.c_str()); } void addAttribute(const char name[], int32_t val) { fWriter->addS32Attribute(name, val); } void addAttribute(const char name[], SkScalar val) { fWriter->addScalarAttribute(name, val); } void addText(const SkString& text) { fWriter->addText(text.c_str(), text.size()); } void addRectAttributes(const SkRect&); void addPathAttributes(const SkPath&); void addTextAttributes(const SkPaint&); private: Resources addResources(const SkDraw& draw, const SkPaint& paint); void addClipResources(const SkDraw& draw, Resources* resources); void addShaderResources(const SkPaint& paint, Resources* resources); void addPaint(const SkPaint& paint, const Resources& resources); SkString addLinearGradientDef(const SkShader::GradientInfo& info, const SkShader* shader); SkXMLWriter* fWriter; ResourceBucket* fResourceBucket; std::unique_ptr fClipGroup; }; void SkSVGDevice::AutoElement::addPaint(const SkPaint& paint, const Resources& resources) { SkPaint::Style style = paint.getStyle(); if (style == SkPaint::kFill_Style || style == SkPaint::kStrokeAndFill_Style) { this->addAttribute("fill", resources.fPaintServer); if (SK_AlphaOPAQUE != SkColorGetA(paint.getColor())) { this->addAttribute("fill-opacity", svg_opacity(paint.getColor())); } } else { SkASSERT(style == SkPaint::kStroke_Style); this->addAttribute("fill", "none"); } if (style == SkPaint::kStroke_Style || style == SkPaint::kStrokeAndFill_Style) { this->addAttribute("stroke", resources.fPaintServer); SkScalar strokeWidth = paint.getStrokeWidth(); if (strokeWidth == 0) { // Hairline stroke strokeWidth = 1; this->addAttribute("vector-effect", "non-scaling-stroke"); } this->addAttribute("stroke-width", strokeWidth); if (const char* cap = svg_cap(paint.getStrokeCap())) { this->addAttribute("stroke-linecap", cap); } if (const char* join = svg_join(paint.getStrokeJoin())) { this->addAttribute("stroke-linejoin", join); } if (paint.getStrokeJoin() == SkPaint::kMiter_Join) { this->addAttribute("stroke-miterlimit", paint.getStrokeMiter()); } if (SK_AlphaOPAQUE != SkColorGetA(paint.getColor())) { this->addAttribute("stroke-opacity", svg_opacity(paint.getColor())); } } else { SkASSERT(style == SkPaint::kFill_Style); this->addAttribute("stroke", "none"); } } Resources SkSVGDevice::AutoElement::addResources(const SkDraw& draw, const SkPaint& paint) { Resources resources(paint); // FIXME: this is a weak heuristic and we end up with LOTS of redundant clips. bool hasClip = !draw.fClipStack->isWideOpen(); bool hasShader = SkToBool(paint.getShader()); if (hasClip || hasShader) { AutoElement defs("defs", fWriter); if (hasClip) { this->addClipResources(draw, &resources); } if (hasShader) { this->addShaderResources(paint, &resources); } } return resources; } void SkSVGDevice::AutoElement::addShaderResources(const SkPaint& paint, Resources* resources) { const SkShader* shader = paint.getShader(); SkASSERT(SkToBool(shader)); SkShader::GradientInfo grInfo; grInfo.fColorCount = 0; if (SkShader::kLinear_GradientType != shader->asAGradient(&grInfo)) { // TODO: non-linear gradient support SkDebugf("unsupported shader type\n"); return; } SkAutoSTArray<16, SkColor> grColors(grInfo.fColorCount); SkAutoSTArray<16, SkScalar> grOffsets(grInfo.fColorCount); grInfo.fColors = grColors.get(); grInfo.fColorOffsets = grOffsets.get(); // One more call to get the actual colors/offsets. shader->asAGradient(&grInfo); SkASSERT(grInfo.fColorCount <= grColors.count()); SkASSERT(grInfo.fColorCount <= grOffsets.count()); resources->fPaintServer.printf("url(#%s)", addLinearGradientDef(grInfo, shader).c_str()); } void SkSVGDevice::AutoElement::addClipResources(const SkDraw& draw, Resources* resources) { SkASSERT(!draw.fClipStack->isWideOpen()); SkPath clipPath; (void) draw.fClipStack->asPath(&clipPath); SkString clipID = fResourceBucket->addClip(); const char* clipRule = clipPath.getFillType() == SkPath::kEvenOdd_FillType ? "evenodd" : "nonzero"; { // clipPath is in device space, but since we're only pushing transform attributes // to the leaf nodes, so are all our elements => SVG userSpaceOnUse == device space. AutoElement clipPathElement("clipPath", fWriter); clipPathElement.addAttribute("id", clipID); SkRect clipRect = SkRect::MakeEmpty(); if (clipPath.isEmpty() || clipPath.isRect(&clipRect)) { AutoElement rectElement("rect", fWriter); rectElement.addRectAttributes(clipRect); rectElement.addAttribute("clip-rule", clipRule); } else { AutoElement pathElement("path", fWriter); pathElement.addPathAttributes(clipPath); pathElement.addAttribute("clip-rule", clipRule); } } resources->fClip.printf("url(#%s)", clipID.c_str()); } SkString SkSVGDevice::AutoElement::addLinearGradientDef(const SkShader::GradientInfo& info, const SkShader* shader) { SkASSERT(fResourceBucket); SkString id = fResourceBucket->addLinearGradient(); { AutoElement gradient("linearGradient", fWriter); gradient.addAttribute("id", id); gradient.addAttribute("gradientUnits", "userSpaceOnUse"); gradient.addAttribute("x1", info.fPoint[0].x()); gradient.addAttribute("y1", info.fPoint[0].y()); gradient.addAttribute("x2", info.fPoint[1].x()); gradient.addAttribute("y2", info.fPoint[1].y()); if (!shader->getLocalMatrix().isIdentity()) { this->addAttribute("gradientTransform", svg_transform(shader->getLocalMatrix())); } SkASSERT(info.fColorCount >= 2); for (int i = 0; i < info.fColorCount; ++i) { SkColor color = info.fColors[i]; SkString colorStr(svg_color(color)); { AutoElement stop("stop", fWriter); stop.addAttribute("offset", info.fColorOffsets[i]); stop.addAttribute("stop-color", colorStr.c_str()); if (SK_AlphaOPAQUE != SkColorGetA(color)) { stop.addAttribute("stop-opacity", svg_opacity(color)); } } } } return id; } void SkSVGDevice::AutoElement::addRectAttributes(const SkRect& rect) { // x, y default to 0 if (rect.x() != 0) { this->addAttribute("x", rect.x()); } if (rect.y() != 0) { this->addAttribute("y", rect.y()); } this->addAttribute("width", rect.width()); this->addAttribute("height", rect.height()); } void SkSVGDevice::AutoElement::addPathAttributes(const SkPath& path) { SkString pathData; SkParsePath::ToSVGString(path, &pathData); this->addAttribute("d", pathData); } void SkSVGDevice::AutoElement::addTextAttributes(const SkPaint& paint) { this->addAttribute("font-size", paint.getTextSize()); if (const char* textAlign = svg_text_align(paint.getTextAlign())) { this->addAttribute("text-anchor", textAlign); } SkString familyName; SkTHashSet familySet; sk_sp tface(paint.getTypeface() ? sk_ref_sp(paint.getTypeface()) : SkTypeface::MakeDefault()); SkASSERT(tface); SkTypeface::Style style = tface->style(); if (style & SkTypeface::kItalic) { this->addAttribute("font-style", "italic"); } if (style & SkTypeface::kBold) { this->addAttribute("font-weight", "bold"); } SkAutoTUnref familyNameIter(tface->createFamilyNameIterator()); SkTypeface::LocalizedString familyString; while (familyNameIter->next(&familyString)) { if (familySet.contains(familyString.fString)) { continue; } familySet.add(familyString.fString); familyName.appendf((familyName.isEmpty() ? "%s" : ", %s"), familyString.fString.c_str()); } if (!familyName.isEmpty()) { this->addAttribute("font-family", familyName); } } SkBaseDevice* SkSVGDevice::Create(const SkISize& size, SkXMLWriter* writer) { if (!writer) { return nullptr; } return new SkSVGDevice(size, writer); } SkSVGDevice::SkSVGDevice(const SkISize& size, SkXMLWriter* writer) : INHERITED(SkImageInfo::MakeUnknown(size.fWidth, size.fHeight), SkSurfaceProps(0, kUnknown_SkPixelGeometry)) , fWriter(writer) , fResourceBucket(new ResourceBucket) { SkASSERT(writer); fWriter->writeHeader(); // The root tag gets closed by the destructor. fRootElement.reset(new AutoElement("svg", fWriter)); fRootElement->addAttribute("xmlns", "http://www.w3.org/2000/svg"); fRootElement->addAttribute("xmlns:xlink", "http://www.w3.org/1999/xlink"); fRootElement->addAttribute("width", size.width()); fRootElement->addAttribute("height", size.height()); } SkSVGDevice::~SkSVGDevice() { } void SkSVGDevice::drawPaint(const SkDraw& draw, const SkPaint& paint) { AutoElement rect("rect", fWriter, fResourceBucket.get(), draw, paint); rect.addRectAttributes(SkRect::MakeWH(SkIntToScalar(this->width()), SkIntToScalar(this->height()))); } void SkSVGDevice::drawPoints(const SkDraw& draw, SkCanvas::PointMode mode, size_t count, const SkPoint pts[], const SkPaint& paint) { SkPath path; switch (mode) { // todo case SkCanvas::kPoints_PointMode: SkDebugf("unsupported operation: drawPoints(kPoints_PointMode)\n"); break; case SkCanvas::kLines_PointMode: count -= 1; for (size_t i = 0; i < count; i += 2) { path.rewind(); path.moveTo(pts[i]); path.lineTo(pts[i+1]); AutoElement elem("path", fWriter, fResourceBucket.get(), draw, paint); elem.addPathAttributes(path); } break; case SkCanvas::kPolygon_PointMode: if (count > 1) { path.addPoly(pts, SkToInt(count), false); path.moveTo(pts[0]); AutoElement elem("path", fWriter, fResourceBucket.get(), draw, paint); elem.addPathAttributes(path); } break; } } void SkSVGDevice::drawRect(const SkDraw& draw, const SkRect& r, const SkPaint& paint) { AutoElement rect("rect", fWriter, fResourceBucket.get(), draw, paint); rect.addRectAttributes(r); } void SkSVGDevice::drawOval(const SkDraw& draw, const SkRect& oval, const SkPaint& paint) { AutoElement ellipse("ellipse", fWriter, fResourceBucket.get(), draw, paint); ellipse.addAttribute("cx", oval.centerX()); ellipse.addAttribute("cy", oval.centerY()); ellipse.addAttribute("rx", oval.width() / 2); ellipse.addAttribute("ry", oval.height() / 2); } void SkSVGDevice::drawRRect(const SkDraw& draw, const SkRRect& rr, const SkPaint& paint) { SkPath path; path.addRRect(rr); AutoElement elem("path", fWriter, fResourceBucket.get(), draw, paint); elem.addPathAttributes(path); } void SkSVGDevice::drawPath(const SkDraw& draw, const SkPath& path, const SkPaint& paint, const SkMatrix* prePathMatrix, bool pathIsMutable) { AutoElement elem("path", fWriter, fResourceBucket.get(), draw, paint); elem.addPathAttributes(path); // TODO: inverse fill types? if (path.getFillType() == SkPath::kEvenOdd_FillType) { elem.addAttribute("fill-rule", "evenodd"); } } void SkSVGDevice::drawBitmapCommon(const SkDraw& draw, const SkBitmap& bm, const SkPaint& paint) { SkAutoTUnref pngData( SkImageEncoder::EncodeData(bm, SkImageEncoder::kPNG_Type, SkImageEncoder::kDefaultQuality)); if (!pngData) { return; } size_t b64Size = SkBase64::Encode(pngData->data(), pngData->size(), nullptr); SkAutoTMalloc b64Data(b64Size); SkBase64::Encode(pngData->data(), pngData->size(), b64Data.get()); SkString svgImageData("data:image/png;base64,"); svgImageData.append(b64Data.get(), b64Size); SkString imageID = fResourceBucket->addImage(); { AutoElement defs("defs", fWriter); { AutoElement image("image", fWriter); image.addAttribute("id", imageID); image.addAttribute("width", bm.width()); image.addAttribute("height", bm.height()); image.addAttribute("xlink:href", svgImageData); } } { AutoElement imageUse("use", fWriter, fResourceBucket.get(), draw, paint); imageUse.addAttribute("xlink:href", SkStringPrintf("#%s", imageID.c_str())); } } void SkSVGDevice::drawBitmap(const SkDraw& draw, const SkBitmap& bitmap, const SkMatrix& matrix, const SkPaint& paint) { SkMatrix adjustedMatrix = *draw.fMatrix; adjustedMatrix.preConcat(matrix); SkDraw adjustedDraw(draw); adjustedDraw.fMatrix = &adjustedMatrix; drawBitmapCommon(adjustedDraw, bitmap, paint); } void SkSVGDevice::drawSprite(const SkDraw& draw, const SkBitmap& bitmap, int x, int y, const SkPaint& paint) { SkMatrix adjustedMatrix = *draw.fMatrix; adjustedMatrix.preTranslate(SkIntToScalar(x), SkIntToScalar(y)); SkDraw adjustedDraw(draw); adjustedDraw.fMatrix = &adjustedMatrix; drawBitmapCommon(adjustedDraw, bitmap, paint); } void SkSVGDevice::drawBitmapRect(const SkDraw& draw, const SkBitmap& bm, const SkRect* srcOrNull, const SkRect& dst, const SkPaint& paint, SkCanvas::SrcRectConstraint) { SkMatrix adjustedMatrix; adjustedMatrix.setRectToRect(srcOrNull ? *srcOrNull : SkRect::Make(bm.bounds()), dst, SkMatrix::kFill_ScaleToFit); adjustedMatrix.postConcat(*draw.fMatrix); SkDraw adjustedDraw(draw); adjustedDraw.fMatrix = &adjustedMatrix; SkClipStack adjustedClipStack; if (srcOrNull && *srcOrNull != SkRect::Make(bm.bounds())) { adjustedClipStack = *draw.fClipStack; adjustedClipStack.clipRect(dst, *draw.fMatrix, SkCanvas::kIntersect_Op, paint.isAntiAlias()); adjustedDraw.fClipStack = &adjustedClipStack; } drawBitmapCommon(adjustedDraw, bm, paint); } void SkSVGDevice::drawText(const SkDraw& draw, const void* text, size_t len, SkScalar x, SkScalar y, const SkPaint& paint) { AutoElement elem("text", fWriter, fResourceBucket.get(), draw, paint); elem.addTextAttributes(paint); SVGTextBuilder builder(text, len, paint, SkPoint::Make(x, y), 0); elem.addAttribute("x", builder.posX()); elem.addAttribute("y", builder.posY()); elem.addText(builder.text()); } void SkSVGDevice::drawPosText(const SkDraw& draw, const void* text, size_t len, const SkScalar pos[], int scalarsPerPos, const SkPoint& offset, const SkPaint& paint) { SkASSERT(scalarsPerPos == 1 || scalarsPerPos == 2); AutoElement elem("text", fWriter, fResourceBucket.get(), draw, paint); elem.addTextAttributes(paint); SVGTextBuilder builder(text, len, paint, offset, scalarsPerPos, pos); elem.addAttribute("x", builder.posX()); elem.addAttribute("y", builder.posY()); elem.addText(builder.text()); } void SkSVGDevice::drawTextOnPath(const SkDraw&, const void* text, size_t len, const SkPath& path, const SkMatrix* matrix, const SkPaint& paint) { SkString pathID = fResourceBucket->addPath(); { AutoElement defs("defs", fWriter); AutoElement pathElement("path", fWriter); pathElement.addAttribute("id", pathID); pathElement.addPathAttributes(path); } { AutoElement textElement("text", fWriter); textElement.addTextAttributes(paint); if (matrix && !matrix->isIdentity()) { textElement.addAttribute("transform", svg_transform(*matrix)); } { AutoElement textPathElement("textPath", fWriter); textPathElement.addAttribute("xlink:href", SkStringPrintf("#%s", pathID.c_str())); if (paint.getTextAlign() != SkPaint::kLeft_Align) { SkASSERT(paint.getTextAlign() == SkPaint::kCenter_Align || paint.getTextAlign() == SkPaint::kRight_Align); textPathElement.addAttribute("startOffset", paint.getTextAlign() == SkPaint::kCenter_Align ? "50%" : "100%"); } SVGTextBuilder builder(text, len, paint, SkPoint::Make(0, 0), 0); textPathElement.addText(builder.text()); } } } void SkSVGDevice::drawVertices(const SkDraw&, SkCanvas::VertexMode, int vertexCount, const SkPoint verts[], const SkPoint texs[], const SkColor colors[], SkBlendMode, const uint16_t indices[], int indexCount, const SkPaint& paint) { // todo SkDebugf("unsupported operation: drawVertices()\n"); } void SkSVGDevice::drawDevice(const SkDraw&, SkBaseDevice*, int x, int y, const SkPaint&) { // todo SkDebugf("unsupported operation: drawDevice()\n"); }