SVG 作为为可缩放矢量图形(Scalable Vector Graphics),易于编辑和维护,基于XML的文本文件存储,在网页设计、图标制作、数据可视化和其他图形相关的领域应用广泛。在应用工程中总会有动态修改路径、绘制颜色等需求,这就需要能够动态的解析 svg 文件,获取对应的路径、颜色等参数。有许多解析 svg 文件的类库,例如:svgHelper。svgHelper 通过 QDomDocument 对 svg 文件进行了解析并提取出了路径和颜色信息,虽然该方法可行,但是具体的解析过程还是相当繁琐,稍有不慎就会出现错误。另外,渐变色、画刷类型、字体、是否显示等属性该库就无法表示。下面通过对SVG库源码进行分析来找到一种通过 render() 方法就可以获取到解析后的路径信息。
QT 提供了svg 库用于svg 的解析,但是大部分代码被封装起来,只提供了 QSvgRenderer 类和 QSvgGenerator 类给用户使用。而 QSvgRenderer 类能使用的只有 render() 方法。从 svg 库的文件目录可以看出来,svg 图像对应的节点、样式、字体等信息都有对应的描述类。

QSvgRenderer 加载 svg 文件后,由 QSvgTinyDocument 类负责解析文件。QSvgTinyDocument 通过 QSvgHandler 执行具体的解析工作,QSvgTinyDocument 存储了解析后的 SVG 结构、样式等信息。

QSvgTinyDocument 提供了 size()、 width() 、height() 、viewBox()、QSvgNode *namedNode(const QString &id) const; QSvgPaintStyleProperty *namedStyle(const QString &id) const;等方法,可以获取 SVG 解析后的相关信息。但是这些方法都在私有头文件中隐藏了起来。

如果要使用 QT 的 svg 库只有通过 render() 方法,该方法的具体执行由 QSvgTinyDocument 类的 draw() 方法负责。

QSvgTinyDocument 类的 draw() 方法调用 QSvgNode 类的 draw() 方法绘制具体的 node,而 QSvgNode 类的 draw() 方法中通过虚函数 drawCommand() 将具体的图形绘制工作交给了其子类。QSvgNode 子类定义在 qsvggraphics_p.h 文件中,其定义了 SVG 支持的各种图形、路径、动画等的描述类,这些类都继承自 QSvgNode 并实现了虚函数 drawCommand(QPainter *p, QSvgExtraStates &states) 定义了对应图像如何在 QPainter 上绘制。
void QSvgTinyDocument::draw(QPainter *p, const QString &id,
const QRectF &bounds)
{
QSvgNode *node = scopeNode(id);
if (!node) {
qCDebug(lcSvgHandler, "Couldn't find node %s. Skipping rendering.", qPrintable(id));
return;
}
if (m_time == 0)
m_time = QDateTime::currentMSecsSinceEpoch();
if (node->displayMode() == QSvgNode::NoneMode)
return;
p->save();
const QRectF elementBounds = node->transformedBounds();
mapSourceToTarget(p, bounds, elementBounds);
QTransform originalTransform = p->worldTransform();
//XXX set default style on the painter
QPen pen(Qt::NoBrush, 1, Qt::SolidLine, Qt::FlatCap, Qt::SvgMiterJoin);
pen.setMiterLimit(4);
p->setPen(pen);
p->setBrush(Qt::black);
p->setRenderHint(QPainter::Antialiasing);
p->setRenderHint(QPainter::SmoothPixmapTransform);
QStack<QSvgNode*> parentApplyStack;
QSvgNode *parent = node->parent();
while (parent) {
parentApplyStack.push(parent);
parent = parent->parent();
}
for (int i = parentApplyStack.size() - 1; i >= 0; --i)
parentApplyStack[i]->applyStyle(p, m_states);
// Reset the world transform so that our parents don't affect
// the position
QTransform currentTransform = p->worldTransform();
p->setWorldTransform(originalTransform);
// 此处调用节点的 draw() 方法
node->draw(p, m_states);
p->setWorldTransform(currentTransform);
for (int i = 0; i < parentApplyStack.size(); ++i)
parentApplyStack[i]->revertStyle(p, m_states);
//p->fillRect(bounds.adjusted(-5, -5, 5, 5), QColor(0, 0, 255, 100));
p->restore();
}
drawCommand 方法通过 QPainter 提供的绘制函数进行图形绘制。QPainter 的绘制动作由 QPaintDevice 通过 QPaintEngine 实现。

通过以上分析可知,如果实现一个 SVG 的绘制引擎,那么所有的绘制动作都可以被该引擎截获并重新解释。GitHub项目 Compelling Data Designer 通过继承 QPaintEngine 实现了 SVG 的绘制引擎,通过 QSvgRenderer 的 render()方法重新获取了 QPainterPath 及其填充、线条等属性。
// svgpathdevice.h
class SvgEngine : public QPaintEngine
{
public:
SvgEngine();
QList<PainterPathEx> getSvgPath();
// QPaintEngine interface
bool begin(QPaintDevice *pdev) override;
bool end() override;
void updateState(const QPaintEngineState &state) override;
void drawPath(const QPainterPath &path) override;
void drawPolygon(const QPointF *points, int pointCount, PolygonDrawMode mode) override;
void drawPixmap(const QRectF &r, const QPixmap &pm, const QRectF &sr) override;
void drawTextItem(const QPointF &p, const QTextItem &textItem) override;
QPaintEngine::Type type() const override;
private:
QList<PainterPathEx> pathList;
};
class SvgPathDevice : public QPaintDevice
{
public:
SvgPathDevice(int w, int h);
SvgPathDevice(const QSize &size);
~SvgPathDevice();
QList<PainterPathEx> getSvgPath();
qreal devicePixelRatio() const;
void setDevicePixelRatio(qreal scaleFactor);
// QPaintDevice interface
QPaintEngine *paintEngine() const override;
int devType() const override;
protected:
int metric(PaintDeviceMetric metric) const override;
private:
SvgEngine *engine;
qreal pixelRatio{1};
int width;
int height;
int qt_defaultDpiX() const;
int qt_defaultDpiY() const;
};
// endtypefactory.cpp
QList<PainterPathEx> EndTypeFactory::extractPath(QSvgRenderer *render, QString id)
{
if (!render->elementExists(id)) {
return QList<PainterPathEx>();
}
render->setAspectRatioMode(Qt::KeepAspectRatio);
auto size = render->defaultSize();
SvgPathDevice svgPath(size);
QPainter p(&svgPath);
render->render(&p, id, QRectF{0, 0, size.width()*1.0, size.height()*1.0});
p.end();
return svgPath.getSvgPath();
}
该项目中线条的终端形状全部使用 SVG 文件进行定义,使用 EndTypeFactory 类作为终端形状的工厂类管理所有 svg 图形。EndTypeFactory 类加载 svg 图像时,通过 SvgPathDevice 类获取了图像的 PainterPath,保证了后续绘制过程中使用矢量图进行绘制。具体代码可以查看 plugins/lineplugin 目录下的 svgpathdevice 和 endtypefactory 类文件。
