ksvgiconengine.cpp
00001 /* 00002 Copyright (C) 2002 Nikolas Zimmermann <wildfox@kde.org> 00003 This file is part of the KDE project 00004 00005 This library is free software; you can redistribute it and/or 00006 modify it under the terms of the GNU Library General Public 00007 License as published by the Free Software Foundation; either 00008 version 2 of the License, or (at your option) any later version. 00009 00010 This library is distributed in the hope that it will be useful, 00011 but WITHOUT ANY WARRANTY; without even the implied warranty of 00012 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 00013 Library General Public License for more details. 00014 00015 You should have received a copy of the GNU Library General Public License 00016 along with this library; see the file COPYING.LIB. If not, write to 00017 the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, 00018 Boston, MA 02110-1301, USA. 00019 */ 00020 00021 #include <qdom.h> 00022 #include <qfile.h> 00023 #include <qcolor.h> 00024 #include <qimage.h> 00025 #include <qwmatrix.h> 00026 #include <qregexp.h> 00027 00028 #include <kmdcodec.h> 00029 00030 #include <zlib.h> 00031 00032 #include "ksvgiconpainter.h" 00033 #include "ksvgiconengine.h" 00034 00035 class KSVGIconEngineHelper 00036 { 00037 public: 00038 KSVGIconEngineHelper(KSVGIconEngine *engine) 00039 { 00040 m_engine = engine; 00041 } 00042 00043 ~KSVGIconEngineHelper() 00044 { 00045 } 00046 00047 double toPixel(const QString &s, bool hmode) 00048 { 00049 return m_engine->painter()->toPixel(s, hmode); 00050 } 00051 00052 ArtGradientStop *parseGradientStops(QDomElement element, int &offsets) 00053 { 00054 if (!element.hasChildNodes()) 00055 return 0; 00056 00057 QValueList<ArtGradientStop> stopList; 00058 00059 float oldOffset = -1, newOffset = -1; 00060 for(QDomNode node = element.firstChild(); !node.isNull(); node = node.nextSibling()) 00061 { 00062 QDomElement element = node.toElement(); 00063 00064 oldOffset = newOffset; 00065 QString temp = element.attribute("offset"); 00066 00067 if(temp.contains("%")) 00068 { 00069 temp = temp.left(temp.length() - 1); 00070 newOffset = temp.toFloat() / 100.0; 00071 } 00072 else 00073 newOffset = temp.toFloat(); 00074 00075 // Spec skip double offset specifications 00076 if(oldOffset == newOffset) 00077 continue; 00078 00079 offsets++; 00080 stopList.append(ArtGradientStop()); 00081 00082 ArtGradientStop &stop = stopList.last(); 00083 00084 stop.offset = newOffset; 00085 00086 QString parseOpacity; 00087 QString parseColor; 00088 00089 if(element.hasAttribute("stop-opacity")) 00090 parseOpacity = element.attribute("stop-opacity"); 00091 00092 if(element.hasAttribute("stop-color")) 00093 parseColor = element.attribute("stop-color"); 00094 00095 if(parseOpacity.isEmpty() || parseColor.isEmpty()) 00096 { 00097 QString style = element.attribute("style"); 00098 00099 QStringList substyles = QStringList::split(';', style); 00100 for(QStringList::Iterator it = substyles.begin(); it != substyles.end(); ++it) 00101 { 00102 QStringList substyle = QStringList::split(':', (*it)); 00103 QString command = substyle[0]; 00104 QString params = substyle[1]; 00105 command = command.stripWhiteSpace(); 00106 params = params.stripWhiteSpace(); 00107 00108 if(command == "stop-color") 00109 { 00110 parseColor = params; 00111 00112 if(!parseOpacity.isEmpty()) 00113 break; 00114 } 00115 else if(command == "stop-opacity") 00116 { 00117 parseOpacity = params; 00118 00119 if(!parseColor.isEmpty()) 00120 break; 00121 } 00122 } 00123 } 00124 00125 // Parse color using KSVGIconPainter (which uses Qt) 00126 // Supports all svg-needed color formats 00127 QColor qStopColor = m_engine->painter()->parseColor(parseColor); 00128 00129 // Convert in a libart suitable form 00130 Q_UINT32 stopColor = m_engine->painter()->toArtColor(qStopColor); 00131 00132 int opacity = m_engine->painter()->parseOpacity(parseOpacity); 00133 00134 Q_UINT32 rgba = (stopColor << 8) | opacity; 00135 Q_UINT32 r, g, b, a; 00136 00137 // Convert from separated to premultiplied alpha 00138 a = rgba & 0xff; 00139 r = (rgba >> 24) * a + 0x80; 00140 r = (r + (r >> 8)) >> 8; 00141 g = ((rgba >> 16) & 0xff) * a + 0x80; 00142 g = (g + (g >> 8)) >> 8; 00143 b = ((rgba >> 8) & 0xff) * a + 0x80; 00144 b = (b + (b >> 8)) >> 8; 00145 00146 stop.color[0] = ART_PIX_MAX_FROM_8(r); 00147 stop.color[1] = ART_PIX_MAX_FROM_8(g); 00148 stop.color[2] = ART_PIX_MAX_FROM_8(b); 00149 stop.color[3] = ART_PIX_MAX_FROM_8(a); 00150 } 00151 00152 if (stopList.isEmpty()) 00153 return 0; 00154 00155 ArtGradientStop *stops = new ArtGradientStop[stopList.count()]; 00156 00157 QValueList<ArtGradientStop>::iterator it = stopList.begin(); 00158 QValueList<ArtGradientStop>::iterator end = stopList.end(); 00159 00160 for (int i = 0; it != end; ++i, ++it) 00161 stops[i] = *it; 00162 00163 return stops; 00164 } 00165 00166 QPointArray parsePoints(QString points) 00167 { 00168 if(points.isEmpty()) 00169 return QPointArray(); 00170 00171 points = points.simplifyWhiteSpace(); 00172 00173 if(points.contains(",,") || points.contains(", ,")) 00174 return QPointArray(); 00175 00176 points.replace(',', ' '); 00177 points.replace('\r', QString::null); 00178 points.replace('\n', QString::null); 00179 00180 points = points.simplifyWhiteSpace(); 00181 00182 QStringList pointList = QStringList::split(' ', points); 00183 00184 QPointArray array(pointList.count() / 2); 00185 int i = 0; 00186 00187 for(QStringList::Iterator it = pointList.begin(); it != pointList.end(); it++) 00188 { 00189 float x = (*(it++)).toFloat(); 00190 float y = (*(it)).toFloat(); 00191 00192 array.setPoint(i, static_cast<int>(x), static_cast<int>(y)); 00193 i++; 00194 } 00195 00196 return array; 00197 } 00198 00199 void parseTransform(const QString &transform) 00200 { 00201 // Combine new and old matrix 00202 QWMatrix matrix = m_engine->painter()->parseTransform(transform); 00203 00204 QWMatrix *current = m_engine->painter()->worldMatrix(); 00205 *current = matrix * *current; 00206 } 00207 00208 void parseCommonAttributes(QDomNode &node) 00209 { 00210 // Set important default attributes 00211 m_engine->painter()->setFillColor("black"); 00212 m_engine->painter()->setStrokeColor("none"); 00213 m_engine->painter()->setStrokeDashArray(""); 00214 m_engine->painter()->setStrokeWidth(1); 00215 m_engine->painter()->setJoinStyle(""); 00216 m_engine->painter()->setCapStyle(""); 00217 // m_engine->painter()->setFillOpacity(255, true); 00218 // m_engine->painter()->setStrokeOpacity(255, true); 00219 00220 // Collect parent node's attributes 00221 QPtrList<QDomNamedNodeMap> applyList; 00222 applyList.setAutoDelete(true); 00223 00224 QDomNode shape = node.parentNode(); 00225 for(; !shape.isNull() ; shape = shape.parentNode()) 00226 applyList.prepend(new QDomNamedNodeMap(shape.attributes())); 00227 00228 // Apply parent attributes 00229 for(QDomNamedNodeMap *map = applyList.first(); map != 0; map = applyList.next()) 00230 { 00231 QDomNamedNodeMap attr = *map; 00232 00233 for(unsigned int i = 0; i < attr.count(); i++) 00234 { 00235 QString name, value; 00236 00237 name = attr.item(i).nodeName().lower(); 00238 value = attr.item(i).nodeValue(); 00239 00240 if(name == "transform") 00241 parseTransform(value); 00242 else if(name == "style") 00243 parseStyle(value); 00244 else 00245 parsePA(name, value); 00246 } 00247 } 00248 00249 // Apply local attributes 00250 QDomNamedNodeMap attr = node.attributes(); 00251 00252 for(unsigned int i = 0; i < attr.count(); i++) 00253 { 00254 QDomNode current = attr.item(i); 00255 00256 if(current.nodeName().lower() == "transform") 00257 parseTransform(current.nodeValue()); 00258 else if(current.nodeName().lower() == "style") 00259 parseStyle(current.nodeValue()); 00260 else 00261 parsePA(current.nodeName().lower(), current.nodeValue()); 00262 } 00263 } 00264 00265 bool handleTags(QDomElement element, bool paint) 00266 { 00267 if(element.attribute("display") == "none") 00268 return false; 00269 if(element.tagName() == "linearGradient") 00270 { 00271 ArtGradientLinear *gradient = new ArtGradientLinear(); 00272 00273 int offsets = -1; 00274 gradient->stops = parseGradientStops(element, offsets); 00275 gradient->n_stops = offsets + 1; 00276 00277 QString spread = element.attribute("spreadMethod"); 00278 if(spread == "repeat") 00279 gradient->spread = ART_GRADIENT_REPEAT; 00280 else if(spread == "reflect") 00281 gradient->spread = ART_GRADIENT_REFLECT; 00282 else 00283 gradient->spread = ART_GRADIENT_PAD; 00284 00285 m_engine->painter()->addLinearGradient(element.attribute("id"), gradient); 00286 m_engine->painter()->addLinearGradientElement(gradient, element); 00287 return true; 00288 } 00289 else if(element.tagName() == "radialGradient") 00290 { 00291 ArtGradientRadial *gradient = new ArtGradientRadial(); 00292 00293 int offsets = -1; 00294 gradient->stops = parseGradientStops(element, offsets); 00295 gradient->n_stops = offsets + 1; 00296 00297 m_engine->painter()->addRadialGradient(element.attribute("id"), gradient); 00298 m_engine->painter()->addRadialGradientElement(gradient, element); 00299 return true; 00300 } 00301 00302 if(!paint) 00303 return true; 00304 00305 // TODO: Default attribute values 00306 if(element.tagName() == "rect") 00307 { 00308 double x = toPixel(element.attribute("x"), true); 00309 double y = toPixel(element.attribute("y"), false); 00310 double w = toPixel(element.attribute("width"), true); 00311 double h = toPixel(element.attribute("height"), false); 00312 00313 double rx = 0.0; 00314 double ry = 0.0; 00315 00316 if(element.hasAttribute("rx")) 00317 rx = toPixel(element.attribute("rx"), true); 00318 00319 if(element.hasAttribute("ry")) 00320 ry = toPixel(element.attribute("ry"), false); 00321 00322 m_engine->painter()->drawRectangle(x, y, w, h, rx, ry); 00323 } 00324 else if(element.tagName() == "switch") 00325 { 00326 QDomNode iterate = element.firstChild(); 00327 00328 while(!iterate.isNull()) 00329 { 00330 // Reset matrix 00331 m_engine->painter()->setWorldMatrix(new QWMatrix(m_initialMatrix)); 00332 00333 // Parse common attributes, style / transform 00334 parseCommonAttributes(iterate); 00335 00336 if(handleTags(iterate.toElement(), true)) 00337 return true; 00338 iterate = iterate.nextSibling(); 00339 } 00340 return true; 00341 } 00342 else if(element.tagName() == "g" || element.tagName() == "defs") 00343 { 00344 QDomNode iterate = element.firstChild(); 00345 00346 while(!iterate.isNull()) 00347 { 00348 // Reset matrix 00349 m_engine->painter()->setWorldMatrix(new QWMatrix(m_initialMatrix)); 00350 00351 // Parse common attributes, style / transform 00352 parseCommonAttributes(iterate); 00353 00354 handleTags(iterate.toElement(), (element.tagName() == "defs") ? false : true); 00355 iterate = iterate.nextSibling(); 00356 } 00357 return true; 00358 } 00359 else if(element.tagName() == "line") 00360 { 00361 double x1 = toPixel(element.attribute("x1"), true); 00362 double y1 = toPixel(element.attribute("y1"), false); 00363 double x2 = toPixel(element.attribute("x2"), true); 00364 double y2 = toPixel(element.attribute("y2"), false); 00365 00366 m_engine->painter()->drawLine(x1, y1, x2, y2); 00367 return true; 00368 } 00369 else if(element.tagName() == "circle") 00370 { 00371 double cx = toPixel(element.attribute("cx"), true); 00372 double cy = toPixel(element.attribute("cy"), false); 00373 00374 double r = toPixel(element.attribute("r"), true); // TODO: horiz correct? 00375 00376 m_engine->painter()->drawEllipse(cx, cy, r, r); 00377 return true; 00378 } 00379 else if(element.tagName() == "ellipse") 00380 { 00381 double cx = toPixel(element.attribute("cx"), true); 00382 double cy = toPixel(element.attribute("cy"), false); 00383 00384 double rx = toPixel(element.attribute("rx"), true); 00385 double ry = toPixel(element.attribute("ry"), false); 00386 00387 m_engine->painter()->drawEllipse(cx, cy, rx, ry); 00388 return true; 00389 } 00390 else if(element.tagName() == "polyline") 00391 { 00392 QPointArray polyline = parsePoints(element.attribute("points")); 00393 m_engine->painter()->drawPolyline(polyline); 00394 return true; 00395 } 00396 else if(element.tagName() == "polygon") 00397 { 00398 QPointArray polygon = parsePoints(element.attribute("points")); 00399 m_engine->painter()->drawPolygon(polygon); 00400 return true; 00401 } 00402 else if(element.tagName() == "path") 00403 { 00404 bool filled = true; 00405 00406 if(element.hasAttribute("fill") && element.attribute("fill").contains("none")) 00407 filled = false; 00408 00409 if(element.attribute("style").contains("fill") && element.attribute("style").stripWhiteSpace().contains("fill:none")) 00410 filled = false; 00411 00412 m_engine->painter()->drawPath(element.attribute("d"), filled); 00413 return true; 00414 } 00415 else if(element.tagName() == "image") 00416 { 00417 double x = toPixel(element.attribute("x"), true); 00418 double y = toPixel(element.attribute("y"), false); 00419 double w = toPixel(element.attribute("width"), true); 00420 double h = toPixel(element.attribute("height"), false); 00421 00422 QString href = element.attribute("xlink:href"); 00423 00424 QImage image; 00425 if(href.startsWith("data:")) 00426 { 00427 // Get input 00428 QCString input = href.remove(QRegExp("^data:image/.*;base64,")).utf8(); 00429 00430 // Decode into 'output' 00431 QByteArray output; 00432 KCodecs::base64Decode(input, output); 00433 00434 // Display 00435 image.loadFromData(output); 00436 } 00437 else 00438 image.load(href); 00439 00440 if (!image.isNull()) 00441 { 00442 // Scale, if needed 00443 if(image.width() != (int) w || image.height() != (int) h) 00444 image = image.smoothScale((int) w, (int) h, QImage::ScaleFree); 00445 00446 m_engine->painter()->drawImage(x, y, image); 00447 } 00448 00449 return true; 00450 } 00451 return false; 00452 } 00453 00454 void parseStyle(const QString &style) 00455 { 00456 QStringList substyles = QStringList::split(';', style); 00457 for(QStringList::Iterator it = substyles.begin(); it != substyles.end(); ++it) 00458 { 00459 QStringList substyle = QStringList::split(':', (*it)); 00460 QString command = substyle[0]; 00461 QString params = substyle[1]; 00462 command = command.stripWhiteSpace(); 00463 params = params.stripWhiteSpace(); 00464 00465 parsePA(command, params); 00466 } 00467 } 00468 00469 void parsePA(const QString &command, const QString &value) 00470 { 00471 if(command == "stroke-width") // TODO: horiz:false correct? 00472 m_engine->painter()->setStrokeWidth(toPixel(value, false)); 00473 else if(command == "stroke-miterlimit") 00474 m_engine->painter()->setStrokeMiterLimit(value); 00475 else if(command == "stroke-linecap") 00476 m_engine->painter()->setCapStyle(value); 00477 else if(command == "stroke-linejoin") 00478 m_engine->painter()->setJoinStyle(value); 00479 else if(command == "stroke-dashoffset") 00480 m_engine->painter()->setStrokeDashOffset(value); 00481 else if(command == "stroke-dasharray" && value != "none") 00482 m_engine->painter()->setStrokeDashArray(value); 00483 else if(command == "stroke") 00484 m_engine->painter()->setStrokeColor(value); 00485 else if(command == "fill") 00486 m_engine->painter()->setFillColor(value); 00487 else if(command == "fill-rule") 00488 m_engine->painter()->setFillRule(value); 00489 else if(command == "fill-opacity" || command == "stroke-opacity" || command == "opacity") 00490 { 00491 if(command == "fill-opacity") 00492 m_engine->painter()->setFillOpacity(value); 00493 else if(command == "stroke-value") 00494 m_engine->painter()->setStrokeOpacity(value); 00495 else 00496 { 00497 m_engine->painter()->setOpacity(value); 00498 m_engine->painter()->setFillOpacity(value); 00499 m_engine->painter()->setStrokeOpacity(value); 00500 } 00501 } 00502 } 00503 00504 private: 00505 friend class KSVGIconEngine; 00506 00507 KSVGIconEngine *m_engine; 00508 QWMatrix m_initialMatrix; 00509 }; 00510 00511 struct KSVGIconEngine::Private 00512 { 00513 KSVGIconPainter *painter; 00514 KSVGIconEngineHelper *helper; 00515 00516 double width; 00517 double height; 00518 }; 00519 00520 KSVGIconEngine::KSVGIconEngine() : d(new Private()) 00521 { 00522 d->painter = 0; 00523 d->helper = new KSVGIconEngineHelper(this); 00524 00525 d->width = 0.0; 00526 d->height = 0.0; 00527 } 00528 00529 KSVGIconEngine::~KSVGIconEngine() 00530 { 00531 if(d->painter) 00532 delete d->painter; 00533 00534 delete d->helper; 00535 00536 delete d; 00537 } 00538 00539 bool KSVGIconEngine::load(int width, int height, const QString &path) 00540 { 00541 QDomDocument svgDocument("svg"); 00542 QFile file(path); 00543 00544 if(path.right(3).upper() == "SVG") 00545 { 00546 // Open SVG Icon 00547 if(!file.open(IO_ReadOnly)) 00548 return false; 00549 00550 svgDocument.setContent(&file); 00551 } 00552 else // SVGZ 00553 { 00554 gzFile svgz = gzopen(path.latin1(), "ro"); 00555 if(!svgz) 00556 return false; 00557 00558 QString data; 00559 bool done = false; 00560 00561 QCString buffer(1024); 00562 int length = 0; 00563 00564 while(!done) 00565 { 00566 int ret = gzread(svgz, buffer.data() + length, 1024); 00567 if(ret == 0) 00568 done = true; 00569 else if(ret == -1) 00570 return false; 00571 else { 00572 buffer.resize(buffer.size()+1024); 00573 length += ret; 00574 } 00575 } 00576 00577 gzclose(svgz); 00578 00579 svgDocument.setContent(buffer); 00580 } 00581 00582 if(svgDocument.isNull()) 00583 return false; 00584 00585 // Check for root element 00586 QDomNode rootNode = svgDocument.namedItem("svg"); 00587 if(rootNode.isNull() || !rootNode.isElement()) 00588 return false; 00589 00590 // Detect width and height 00591 QDomElement rootElement = rootNode.toElement(); 00592 00593 // Create icon painter 00594 d->painter = new KSVGIconPainter(width, height); 00595 00596 d->width = width; // this sets default for no width -> 100% case 00597 if(rootElement.hasAttribute("width")) 00598 d->width = d->helper->toPixel(rootElement.attribute("width"), true); 00599 00600 d->height = height; // this sets default for no height -> 100% case 00601 if(rootElement.hasAttribute("height")) 00602 d->height = d->helper->toPixel(rootElement.attribute("height"), false); 00603 00604 // Create icon painter 00605 d->painter->setDrawWidth(static_cast<int>(d->width)); 00606 d->painter->setDrawHeight(static_cast<int>(d->height)); 00607 00608 // Set viewport clipping rect 00609 d->painter->setClippingRect(0, 0, width, height); 00610 00611 // Apply viewbox 00612 if(rootElement.hasAttribute("viewBox")) 00613 { 00614 QStringList points = QStringList::split(' ', rootElement.attribute("viewBox").simplifyWhiteSpace()); 00615 00616 float w = points[2].toFloat(); 00617 float h = points[3].toFloat(); 00618 00619 double vratiow = width / w; 00620 double vratioh = height / h; 00621 00622 d->width = w; 00623 d->height = h; 00624 00625 d->painter->worldMatrix()->scale(vratiow, vratioh); 00626 } 00627 else 00628 { 00629 // Fit into 'width' and 'height' 00630 // FIXME: Use an aspect ratio 00631 double ratiow = width / d->width; 00632 double ratioh = height / d->height; 00633 00634 d->painter->worldMatrix()->scale(ratiow, ratioh); 00635 } 00636 00637 QWMatrix initialMatrix = *d->painter->worldMatrix(); 00638 d->helper->m_initialMatrix = initialMatrix; 00639 00640 // Apply transform 00641 if(rootElement.hasAttribute("transform")) 00642 d->helper->parseTransform(rootElement.attribute("transform")); 00643 00644 // Go through all elements 00645 QDomNode svgNode = rootElement.firstChild(); 00646 while(!svgNode.isNull()) 00647 { 00648 QDomElement svgChild = svgNode.toElement(); 00649 if(!svgChild.isNull()) 00650 { 00651 d->helper->parseCommonAttributes(svgNode); 00652 d->helper->handleTags(svgChild, true); 00653 } 00654 00655 svgNode = svgNode.nextSibling(); 00656 00657 // Reset matrix 00658 d->painter->setWorldMatrix(new QWMatrix(initialMatrix)); 00659 } 00660 00661 d->painter->finish(); 00662 00663 return true; 00664 } 00665 00666 KSVGIconPainter *KSVGIconEngine::painter() 00667 { 00668 return d->painter; 00669 } 00670 00671 QImage *KSVGIconEngine::image() 00672 { 00673 return d->painter->image(); 00674 } 00675 00676 double KSVGIconEngine::width() 00677 { 00678 return d->width; 00679 } 00680 00681 double KSVGIconEngine::height() 00682 { 00683 return d->height; 00684 } 00685 00686 // vim:ts=4:noet