You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
tdelibs/tdecore/svgicons/ksvgiconengine.cpp

713 lines
18 KiB

/*
Copyright (C) 2002 Nikolas Zimmermann <wildfox@kde.org>
This file is part of the KDE project
This library is free software; you can redistribute it and/or
modify it under the terms of the GNU Library General Public
License as published by the Free Software Foundation; either
version 2 of the License, or (at your option) any later version.
This library is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
Library General Public License for more details.
You should have received a copy of the GNU Library General Public License
along with this library; see the file COPYING.LIB. If not, write to
the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
Boston, MA 02110-1301, USA.
*/
#include <tqdom.h>
#include <tqfile.h>
#include <tqcolor.h>
#include <tqimage.h>
#include <tqwmatrix.h>
#include <tqregexp.h>
#include <kmdcodec.h>
#include <zlib.h>
#include "ksvgiconpainter.h"
#include "ksvgiconengine.h"
class KSVGIconEngineHelper
{
public:
KSVGIconEngineHelper(KSVGIconEngine *engine)
{
m_engine = engine;
}
~KSVGIconEngineHelper()
{
}
double toPixel(const TQString &s, bool hmode)
{
return m_engine->painter()->toPixel(s, hmode);
}
ArtGradientStop *parseGradientStops(TQDomElement element, int &offsets)
{
if (!element.hasChildNodes())
return 0;
TQValueList<ArtGradientStop> stopList;
float oldOffset = -1, newOffset = -1;
for(TQDomNode node = element.firstChild(); !node.isNull(); node = node.nextSibling())
{
TQDomElement element = node.toElement();
oldOffset = newOffset;
TQString temp = element.attribute("offset");
if(temp.contains("%"))
{
temp = temp.left(temp.length() - 1);
newOffset = temp.toFloat() / 100.0;
}
else
newOffset = temp.toFloat();
// Spec skip double offset specifications
if(oldOffset == newOffset)
continue;
offsets++;
stopList.append(ArtGradientStop());
ArtGradientStop &stop = stopList.last();
stop.offset = newOffset;
TQString parseOpacity;
TQString parseColor;
if(element.hasAttribute("stop-opacity"))
parseOpacity = element.attribute("stop-opacity");
if(element.hasAttribute("stop-color"))
parseColor = element.attribute("stop-color");
if(parseOpacity.isEmpty() || parseColor.isEmpty())
{
TQString style = element.attribute("style");
TQStringList substyles = TQStringList::split(';', style);
for(TQStringList::Iterator it = substyles.begin(); it != substyles.end(); ++it)
{
TQStringList substyle = TQStringList::split(':', (*it));
TQString command = substyle[0];
TQString params = substyle[1];
command = command.stripWhiteSpace();
params = params.stripWhiteSpace();
if(command == "stop-color")
{
parseColor = params;
if(!parseOpacity.isEmpty())
break;
}
else if(command == "stop-opacity")
{
parseOpacity = params;
if(!parseColor.isEmpty())
break;
}
}
}
// Parse color using KSVGIconPainter (which uses Qt)
// Supports all svg-needed color formats
TQColor qStopColor = m_engine->painter()->parseColor(parseColor);
// Convert in a libart suitable form
TQ_UINT32 stopColor = m_engine->painter()->toArtColor(qStopColor);
int opacity = m_engine->painter()->parseOpacity(parseOpacity);
TQ_UINT32 rgba = (stopColor << 8) | opacity;
TQ_UINT32 r, g, b, a;
// Convert from separated to premultiplied alpha
a = rgba & 0xff;
r = (rgba >> 24) * a + 0x80;
r = (r + (r >> 8)) >> 8;
g = ((rgba >> 16) & 0xff) * a + 0x80;
g = (g + (g >> 8)) >> 8;
b = ((rgba >> 8) & 0xff) * a + 0x80;
b = (b + (b >> 8)) >> 8;
stop.color[0] = ART_PIX_MAX_FROM_8(r);
stop.color[1] = ART_PIX_MAX_FROM_8(g);
stop.color[2] = ART_PIX_MAX_FROM_8(b);
stop.color[3] = ART_PIX_MAX_FROM_8(a);
}
if (stopList.isEmpty())
return 0;
ArtGradientStop *stops = new ArtGradientStop[stopList.count()];
TQValueList<ArtGradientStop>::iterator it = stopList.begin();
TQValueList<ArtGradientStop>::iterator end = stopList.end();
for (int i = 0; it != end; ++i, ++it)
stops[i] = *it;
return stops;
}
TQPointArray parsePoints(TQString points)
{
if(points.isEmpty())
return TQPointArray();
points = points.simplifyWhiteSpace();
if(points.contains(",,") || points.contains(", ,"))
return TQPointArray();
points.replace(',', ' ');
points.replace('\r', TQString());
points.replace('\n', TQString());
points = points.simplifyWhiteSpace();
TQStringList pointList = TQStringList::split(' ', points);
TQPointArray array(pointList.count() / 2);
int i = 0;
for(TQStringList::Iterator it = pointList.begin(); it != pointList.end(); it++)
{
float x = (*(it++)).toFloat();
float y = (*(it)).toFloat();
array.setPoint(i, static_cast<int>(x), static_cast<int>(y));
i++;
}
return array;
}
void parseTransform(const TQString &transform)
{
// Combine new and old matrix
TQWMatrix matrix = m_engine->painter()->parseTransform(transform);
TQWMatrix *current = m_engine->painter()->worldMatrix();
#ifdef USE_QT4
printf("[FIXME] *current = matrix * *current locks up under Qt4; bypassing for now\n");
#else // USE_QT4
*current = matrix * *current;
#endif // USE_QT4
}
void parseCommonAttributes(TQDomNode &node)
{
// Set important default attributes
m_engine->painter()->setFillColor("black");
m_engine->painter()->setStrokeColor("none");
m_engine->painter()->setStrokeDashArray("");
m_engine->painter()->setStrokeWidth(1);
m_engine->painter()->setJoinStyle("");
m_engine->painter()->setCapStyle("");
// m_engine->painter()->setFillOpacity(255, true);
// m_engine->painter()->setStrokeOpacity(255, true);
// Collect parent node's attributes
TQPtrList<TQDomNamedNodeMap> applyList;
applyList.setAutoDelete(true);
TQDomNode shape = node.parentNode();
for(; !shape.isNull() ; shape = shape.parentNode())
applyList.prepend(new TQDomNamedNodeMap(shape.attributes()));
// Apply parent attributes
for(TQDomNamedNodeMap *map = applyList.first(); map != 0; map = applyList.next())
{
TQDomNamedNodeMap attr = *map;
for(unsigned int i = 0; i < attr.count(); i++)
{
TQString name, value;
name = attr.item(i).nodeName().lower();
value = attr.item(i).nodeValue();
if(name == "transform")
parseTransform(value);
else if(name == "style")
parseStyle(value);
else
parsePA(name, value);
}
}
// Apply local attributes
TQDomNamedNodeMap attr = node.attributes();
for(unsigned int i = 0; i < attr.count(); i++)
{
TQDomNode current = attr.item(i);
if(current.nodeName().lower() == "transform")
parseTransform(current.nodeValue());
else if(current.nodeName().lower() == "style")
parseStyle(current.nodeValue());
else
parsePA(current.nodeName().lower(), current.nodeValue());
}
}
bool handleTags(TQDomElement element, bool paint)
{
if(element.attribute("display") == "none")
return false;
if(element.tagName() == "linearGradient")
{
ArtGradientLinear *gradient = new ArtGradientLinear();
int offsets = -1;
gradient->stops = parseGradientStops(element, offsets);
gradient->n_stops = offsets + 1;
TQString spread = element.attribute("spreadMethod");
if(spread == "repeat")
gradient->spread = ART_GRADIENT_REPEAT;
else if(spread == "reflect")
gradient->spread = ART_GRADIENT_REFLECT;
else
gradient->spread = ART_GRADIENT_PAD;
m_engine->painter()->addLinearGradient(element.attribute("id"), gradient);
m_engine->painter()->addLinearGradientElement(gradient, element);
return true;
}
else if(element.tagName() == "radialGradient")
{
ArtGradientRadial *gradient = new ArtGradientRadial();
int offsets = -1;
gradient->stops = parseGradientStops(element, offsets);
gradient->n_stops = offsets + 1;
m_engine->painter()->addRadialGradient(element.attribute("id"), gradient);
m_engine->painter()->addRadialGradientElement(gradient, element);
return true;
}
if(!paint)
return true;
// TODO: Default attribute values
if(element.tagName() == "rect")
{
double x = toPixel(element.attribute("x"), true);
double y = toPixel(element.attribute("y"), false);
double w = toPixel(element.attribute("width"), true);
double h = toPixel(element.attribute("height"), false);
double rx = 0.0;
double ry = 0.0;
if(element.hasAttribute("rx"))
rx = toPixel(element.attribute("rx"), true);
if(element.hasAttribute("ry"))
ry = toPixel(element.attribute("ry"), false);
m_engine->painter()->drawRectangle(x, y, w, h, rx, ry);
}
else if(element.tagName() == "switch")
{
TQDomNode iterate = element.firstChild();
while(!iterate.isNull())
{
// Reset matrix
m_engine->painter()->setWorldMatrix(new TQWMatrix(m_initialMatrix));
// Parse common attributes, style / transform
parseCommonAttributes(iterate);
if(handleTags(iterate.toElement(), true))
return true;
iterate = iterate.nextSibling();
}
return true;
}
else if(element.tagName() == "g" || element.tagName() == "defs")
{
TQDomNode iterate = element.firstChild();
while(!iterate.isNull())
{
// Reset matrix
m_engine->painter()->setWorldMatrix(new TQWMatrix(m_initialMatrix));
// Parse common attributes, style / transform
parseCommonAttributes(iterate);
handleTags(iterate.toElement(), (element.tagName() == "defs") ? false : true);
iterate = iterate.nextSibling();
}
return true;
}
else if(element.tagName() == "line")
{
double x1 = toPixel(element.attribute("x1"), true);
double y1 = toPixel(element.attribute("y1"), false);
double x2 = toPixel(element.attribute("x2"), true);
double y2 = toPixel(element.attribute("y2"), false);
m_engine->painter()->drawLine(x1, y1, x2, y2);
return true;
}
else if(element.tagName() == "circle")
{
double cx = toPixel(element.attribute("cx"), true);
double cy = toPixel(element.attribute("cy"), false);
double r = toPixel(element.attribute("r"), true); // TODO: horiz correct?
m_engine->painter()->drawEllipse(cx, cy, r, r);
return true;
}
else if(element.tagName() == "ellipse")
{
double cx = toPixel(element.attribute("cx"), true);
double cy = toPixel(element.attribute("cy"), false);
double rx = toPixel(element.attribute("rx"), true);
double ry = toPixel(element.attribute("ry"), false);
m_engine->painter()->drawEllipse(cx, cy, rx, ry);
return true;
}
else if(element.tagName() == "polyline")
{
TQPointArray polyline = parsePoints(element.attribute("points"));
m_engine->painter()->drawPolyline(polyline);
return true;
}
else if(element.tagName() == "polygon")
{
TQPointArray polygon = parsePoints(element.attribute("points"));
m_engine->painter()->drawPolygon(polygon);
return true;
}
else if(element.tagName() == "path")
{
bool filled = true;
if(element.hasAttribute("fill") && element.attribute("fill").contains("none"))
filled = false;
if(element.attribute("style").contains("fill") && element.attribute("style").stripWhiteSpace().contains("fill:none"))
filled = false;
m_engine->painter()->drawPath(element.attribute("d"), filled);
return true;
}
else if(element.tagName() == "image")
{
double x = toPixel(element.attribute("x"), true);
double y = toPixel(element.attribute("y"), false);
double w = toPixel(element.attribute("width"), true);
double h = toPixel(element.attribute("height"), false);
TQString href = element.attribute("xlink:href");
TQImage image;
if(href.startsWith("data:"))
{
// Get input
TQCString input = TQString(href.remove(TQRegExp("^data:image/.*;base64,"))).utf8();
// Decode into 'output'
TQByteArray output;
KCodecs::base64Decode(input, output);
// Display
image.loadFromData(output);
}
else
image.load(href);
if (!image.isNull())
{
// Scale, if needed
if(image.width() != (int) w || image.height() != (int) h)
image = image.smoothScale((int) w, (int) h, TQ_ScaleFree);
m_engine->painter()->drawImage(x, y, image);
}
return true;
}
return false;
}
void parseStyle(const TQString &style)
{
TQStringList substyles = TQStringList::split(';', style);
for(TQStringList::Iterator it = substyles.begin(); it != substyles.end(); ++it)
{
TQStringList substyle = TQStringList::split(':', (*it));
TQString command = substyle[0];
TQString params = substyle[1];
command = command.stripWhiteSpace();
params = params.stripWhiteSpace();
parsePA(command, params);
}
}
void parsePA(const TQString &command, const TQString &value)
{
if(command == "stroke-width") // TODO: horiz:false correct?
m_engine->painter()->setStrokeWidth(toPixel(value, false));
else if(command == "stroke-miterlimit")
m_engine->painter()->setStrokeMiterLimit(value);
else if(command == "stroke-linecap")
m_engine->painter()->setCapStyle(value);
else if(command == "stroke-linejoin")
m_engine->painter()->setJoinStyle(value);
else if(command == "stroke-dashoffset")
m_engine->painter()->setStrokeDashOffset(value);
else if(command == "stroke-dasharray" && value != "none")
m_engine->painter()->setStrokeDashArray(value);
else if(command == "stroke")
m_engine->painter()->setStrokeColor(value);
else if(command == "fill")
m_engine->painter()->setFillColor(value);
else if(command == "fill-rule")
m_engine->painter()->setFillRule(value);
else if(command == "fill-opacity" || command == "stroke-opacity" || command == "opacity")
{
if(command == "fill-opacity")
m_engine->painter()->setFillOpacity(value);
else if(command == "stroke-value")
m_engine->painter()->setStrokeOpacity(value);
else
{
m_engine->painter()->setOpacity(value);
m_engine->painter()->setFillOpacity(value);
m_engine->painter()->setStrokeOpacity(value);
}
}
}
private:
friend class KSVGIconEngine;
KSVGIconEngine *m_engine;
TQWMatrix m_initialMatrix;
};
struct KSVGIconEngine::Private
{
KSVGIconPainter *painter;
KSVGIconEngineHelper *helper;
double width;
double height;
};
KSVGIconEngine::KSVGIconEngine() : d(new Private())
{
d->painter = 0;
d->helper = new KSVGIconEngineHelper(this);
d->width = 0.0;
d->height = 0.0;
}
KSVGIconEngine::~KSVGIconEngine()
{
if(d->painter)
delete d->painter;
delete d->helper;
delete d;
}
bool KSVGIconEngine::load(int width, int height, const TQString &path)
{
if(path.isNull()) return false;
TQDomDocument svgDocument("svg");
TQFile file(path);
if(path.right(3).upper() == "SVG")
{
// Open SVG Icon
if(!file.open(IO_ReadOnly))
return false;
svgDocument.setContent(&file);
}
else // SVGZ
{
gzFile svgz = gzopen(path.latin1(), "ro");
if(!svgz)
return false;
TQString data;
bool done = false;
TQCString buffer(1024);
int length = 0;
while(!done)
{
int ret = gzread(svgz, buffer.data() + length, 1024);
if(ret == 0)
done = true;
else if(ret == -1)
return false;
else {
buffer.resize(buffer.size()+1024);
length += ret;
}
}
gzclose(svgz);
svgDocument.setContent(buffer);
}
if(svgDocument.isNull())
return false;
// Check for root element
TQDomNode rootNode = svgDocument.namedItem("svg");
if(rootNode.isNull() || !rootNode.isElement())
return false;
// Detect width and height
TQDomElement rootElement = rootNode.toElement();
bool recreatePainter = false;
// Create initial icon painter
d->painter = new KSVGIconPainter(width, height);
d->width = width; // this sets default for no width -> 100% case
if(rootElement.hasAttribute("width"))
{
d->width = d->helper->toPixel(rootElement.attribute("width"), true);
if(!width) // no width set, use default
{
width = d->width;
recreatePainter = true;
}
}
d->height = height; // this sets default for no height -> 100% case
if(rootElement.hasAttribute("height"))
{
d->height = d->helper->toPixel(rootElement.attribute("height"), false);
if(!height) // no height set, use default
{
height = d->height;
recreatePainter = true;
}
}
// Create final icon painter
if(recreatePainter)
{
delete d->painter;
d->painter = new KSVGIconPainter(width, height);
}
d->painter->setDrawWidth(static_cast<int>(d->width));
d->painter->setDrawHeight(static_cast<int>(d->height));
// Set viewport clipping rect
d->painter->setClippingRect(0, 0, width, height);
// Apply viewbox
if(rootElement.hasAttribute("viewBox"))
{
TQStringList points = TQStringList::split(' ', rootElement.attribute("viewBox").simplifyWhiteSpace());
float w = points[2].toFloat();
float h = points[3].toFloat();
double vratiow = width / w;
double vratioh = height / h;
d->width = w;
d->height = h;
d->painter->worldMatrix()->scale(vratiow, vratioh);
}
else
{
// Fit into 'width' and 'height'
// FIXME: Use an aspect ratio
double ratiow = width / d->width;
double ratioh = height / d->height;
d->painter->worldMatrix()->scale(ratiow, ratioh);
}
TQWMatrix initialMatrix = *d->painter->worldMatrix();
d->helper->m_initialMatrix = initialMatrix;
// Apply transform
if(rootElement.hasAttribute("transform"))
d->helper->parseTransform(rootElement.attribute("transform"));
// Go through all elements
TQDomNode svgNode = rootElement.firstChild();
while(!svgNode.isNull())
{
TQDomElement svgChild = svgNode.toElement();
if(!svgChild.isNull())
{
d->helper->parseCommonAttributes(svgNode);
d->helper->handleTags(svgChild, true);
}
svgNode = svgNode.nextSibling();
// Reset matrix
d->painter->setWorldMatrix(new TQWMatrix(initialMatrix));
}
d->painter->finish();
return true;
}
KSVGIconPainter *KSVGIconEngine::painter()
{
return d->painter;
}
TQImage *KSVGIconEngine::image()
{
return d->painter->image();
}
double KSVGIconEngine::width()
{
return d->width;
}
double KSVGIconEngine::height()
{
return d->height;
}