/* Copyright (C) 2002 Nikolas Zimmermann 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 #include #include #include #include #include #include #include #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 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::iterator it = stopList.begin(); TQValueList::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(x), static_cast(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 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(); // Create 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); d->height = height; // this sets default for no height -> 100% case if(rootElement.hasAttribute("height")) d->height = d->helper->toPixel(rootElement.attribute("height"), false); // Create icon painter d->painter->setDrawWidth(static_cast(d->width)); d->painter->setDrawHeight(static_cast(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; }