blob: 4c0898689303e913eb674921d5b4b569567adddf [file] [log] [blame]
/*
* 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"
#include "SkClipOpPriv.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<const char*>(text);
for (int i = 0; i < count; ++i) {
this->appendUnichar(SkUTF8_NextUnichar(&c8));
}
SkASSERT(reinterpret_cast<const char*>(text) + byteLen == c8);
} break;
case SkPaint::kUTF16_TextEncoding: {
const uint16_t* c16 = reinterpret_cast<const uint16_t*>(text);
for (int i = 0; i < count; ++i) {
this->appendUnichar(SkUTF16_NextUnichar(&c16));
}
SkASSERT(SkIsAlign2(byteLen));
SkASSERT(reinterpret_cast<const uint16_t*>(text) + (byteLen / 2) == c16);
} break;
case SkPaint::kUTF32_TextEncoding: {
SkASSERT(count * sizeof(uint32_t) == byteLen);
const uint32_t* c32 = reinterpret_cast<const uint32_t*>(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("&amp;");
break;
case '"':
fText.append("&quot;");
break;
case '\'':
fText.append("&apos;");
break;
case '<':
fText.append("&lt;");
break;
case '>':
fText.append("&gt;");
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;
};
struct SkSVGDevice::MxCp {
const SkMatrix* fMatrix;
const SkClipStack* fClipStack;
MxCp(const SkMatrix* mx, const SkClipStack* cs) : fMatrix(mx), fClipStack(cs) {}
MxCp(SkSVGDevice* device) : fMatrix(&device->ctm()), fClipStack(&device->cs()) {}
};
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 MxCp& mc, const SkPaint& paint)
: fWriter(writer)
, fResourceBucket(bucket) {
Resources res = this->addResources(mc, paint);
if (!res.fClip.isEmpty()) {
// The clip is in device space. Apply it via a <g> 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 (!mc.fMatrix->isIdentity()) {
this->addAttribute("transform", svg_transform(*mc.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 MxCp&, const SkPaint& paint);
void addClipResources(const MxCp&, 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<AutoElement> 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 MxCp& mc, const SkPaint& paint) {
Resources resources(paint);
// FIXME: this is a weak heuristic and we end up with LOTS of redundant clips.
bool hasClip = !mc.fClipStack->isWideOpen();
bool hasShader = SkToBool(paint.getShader());
if (hasClip || hasShader) {
AutoElement defs("defs", fWriter);
if (hasClip) {
this->addClipResources(mc, &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 MxCp& mc, Resources* resources) {
SkASSERT(!mc.fClipStack->isWideOpen());
SkPath clipPath;
(void) mc.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<SkString> familySet;
sk_sp<SkTypeface> tface(paint.getTypeface() ? paint.refTypeface() : 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");
}
sk_sp<SkTypeface::LocalizedStrings> familyNameIter(tface->createFamilyNameIterator());
SkTypeface::LocalizedString familyString;
if (familyNameIter) {
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 <svg> 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 SkPaint& paint) {
AutoElement rect("rect", fWriter, fResourceBucket.get(), MxCp(this), paint);
rect.addRectAttributes(SkRect::MakeWH(SkIntToScalar(this->width()),
SkIntToScalar(this->height())));
}
void SkSVGDevice::drawPoints(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(), MxCp(this), 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(), MxCp(this), paint);
elem.addPathAttributes(path);
}
break;
}
}
void SkSVGDevice::drawRect(const SkRect& r, const SkPaint& paint) {
AutoElement rect("rect", fWriter, fResourceBucket.get(), MxCp(this), paint);
rect.addRectAttributes(r);
}
void SkSVGDevice::drawOval(const SkRect& oval, const SkPaint& paint) {
AutoElement ellipse("ellipse", fWriter, fResourceBucket.get(), MxCp(this), 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 SkRRect& rr, const SkPaint& paint) {
SkPath path;
path.addRRect(rr);
AutoElement elem("path", fWriter, fResourceBucket.get(), MxCp(this), paint);
elem.addPathAttributes(path);
}
void SkSVGDevice::drawPath(const SkPath& path, const SkPaint& paint,
const SkMatrix* prePathMatrix, bool pathIsMutable) {
AutoElement elem("path", fWriter, fResourceBucket.get(), MxCp(this), paint);
elem.addPathAttributes(path);
// TODO: inverse fill types?
if (path.getFillType() == SkPath::kEvenOdd_FillType) {
elem.addAttribute("fill-rule", "evenodd");
}
}
static sk_sp<SkData> encode(const SkBitmap& src) {
SkDynamicMemoryWStream buf;
return SkEncodeImage(&buf, src, SkEncodedImageFormat::kPNG, 80) ? buf.detachAsData() : nullptr;
}
void SkSVGDevice::drawBitmapCommon(const MxCp& mc, const SkBitmap& bm, const SkPaint& paint) {
sk_sp<SkData> pngData = encode(bm);
if (!pngData) {
return;
}
size_t b64Size = SkBase64::Encode(pngData->data(), pngData->size(), nullptr);
SkAutoTMalloc<char> 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(), mc, paint);
imageUse.addAttribute("xlink:href", SkStringPrintf("#%s", imageID.c_str()));
}
}
void SkSVGDevice::drawBitmap(const SkBitmap& bitmap,
const SkMatrix& matrix, const SkPaint& paint) {
MxCp mc(this);
SkMatrix adjustedMatrix = *mc.fMatrix;
adjustedMatrix.preConcat(matrix);
mc.fMatrix = &adjustedMatrix;
drawBitmapCommon(mc, bitmap, paint);
}
void SkSVGDevice::drawSprite(const SkBitmap& bitmap,
int x, int y, const SkPaint& paint) {
MxCp mc(this);
SkMatrix adjustedMatrix = *mc.fMatrix;
adjustedMatrix.preTranslate(SkIntToScalar(x), SkIntToScalar(y));
mc.fMatrix = &adjustedMatrix;
drawBitmapCommon(mc, bitmap, paint);
}
void SkSVGDevice::drawBitmapRect(const SkBitmap& bm, const SkRect* srcOrNull,
const SkRect& dst, const SkPaint& paint,
SkCanvas::SrcRectConstraint) {
SkClipStack* cs = &this->cs();
SkClipStack::AutoRestore ar(cs, false);
if (srcOrNull && *srcOrNull != SkRect::Make(bm.bounds())) {
cs->save();
cs->clipRect(dst, this->ctm(), kIntersect_SkClipOp, paint.isAntiAlias());
}
SkMatrix adjustedMatrix;
adjustedMatrix.setRectToRect(srcOrNull ? *srcOrNull : SkRect::Make(bm.bounds()),
dst,
SkMatrix::kFill_ScaleToFit);
adjustedMatrix.postConcat(this->ctm());
drawBitmapCommon(MxCp(&adjustedMatrix, cs), bm, paint);
}
void SkSVGDevice::drawText(const void* text, size_t len,
SkScalar x, SkScalar y, const SkPaint& paint) {
AutoElement elem("text", fWriter, fResourceBucket.get(), MxCp(this), 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 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(), MxCp(this), 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 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 SkVertices*, SkBlendMode, const SkPaint&) {
// todo
SkDebugf("unsupported operation: drawVertices()\n");
}
void SkSVGDevice::drawDevice(SkBaseDevice*, int x, int y,
const SkPaint&) {
// todo
SkDebugf("unsupported operation: drawDevice()\n");
}