From 009833c1d6be271d611c183aae781e41b71ca927 Mon Sep 17 00:00:00 2001 From: Krzysztof Kosi??ski Date: Sun, 5 Jul 2015 03:47:36 +0200 Subject: Fix compilation failure in make check (bzr r14228) --- ...PLEASE DON'T MAKE CHANGES IN THESE FILES.README | 4 +- src/2geom/forward.h | 2 - src/2geom/path.h | 1 + src/display/curve-test.h | 71 ++++++------ src/display/curve.cpp | 18 +-- src/svg/svg-path-geom-test.h | 121 +-------------------- 6 files changed, 47 insertions(+), 170 deletions(-) diff --git a/src/2geom/!PLEASE DON'T MAKE CHANGES IN THESE FILES.README b/src/2geom/!PLEASE DON'T MAKE CHANGES IN THESE FILES.README index 074921de2..e94a549f6 100644 --- a/src/2geom/!PLEASE DON'T MAKE CHANGES IN THESE FILES.README +++ b/src/2geom/!PLEASE DON'T MAKE CHANGES IN THESE FILES.README @@ -2,11 +2,11 @@ This is an in-tree copy of lib2geom, a 2D geometry library started by Inkscape developers. If you want to change code in 2Geom, you should commit it first to upstream repository and then execute this command: -rsync -r --existing /path/to/lib2geom/src/2geom /path/to/inkscape/src/2geom +rsync -r --existing --exclude CMakeLists.txt /path/to/lib2geom/src/2geom/ /path/to/inkscape/src/2geom/ The command above will only update existing files. If you add new files to 2Geom, you'll need to copy the new files manually. Same if you remove -some files. +some files. Note that the trailing slashes are required! 2geom's BZR repo = lp:lib2geom http://lib2geom.sourceforge.net diff --git a/src/2geom/forward.h b/src/2geom/forward.h index cf9c75988..27909242c 100644 --- a/src/2geom/forward.h +++ b/src/2geom/forward.h @@ -72,8 +72,6 @@ class ConvexHull; class Curve; class SBasisCurve; class BezierCurve; -class HLineSegment; -class VLineSegment; template class BezierCurveN; typedef BezierCurveN<1> LineSegment; typedef BezierCurveN<2> QuadraticBezier; diff --git a/src/2geom/path.h b/src/2geom/path.h index a2d1e751e..3ca43e0e5 100644 --- a/src/2geom/path.h +++ b/src/2geom/path.h @@ -389,6 +389,7 @@ public: swap(other._curves, _curves); swap(other._closing_seg, _closing_seg); swap(other._closed, _closed); + swap(other._exception_on_stitch, _exception_on_stitch); } /** @brief Swap contents of two paths. * @relates Path */ diff --git a/src/display/curve-test.h b/src/display/curve-test.h index 34137f3c8..f921a5361 100644 --- a/src/display/curve-test.h +++ b/src/display/curve-test.h @@ -16,15 +16,16 @@ public: CurveTest() : path4(Geom::Point(3,5)) // Just a moveto { // Closed path - path1.append(Geom::HLineSegment(Geom::Point(0,0),1)); - path1.append(Geom::VLineSegment(Geom::Point(1,0),1)); + path1.append(Geom::LineSegment(Geom::Point(0,0),Geom::Point(1,0))); + path1.append(Geom::LineSegment(Geom::Point(1,0),Geom::Point(1,1))); path1.close(); // Closed path (ClosingSegment is zero length) path2.append(Geom::LineSegment(Geom::Point(2,0),Geom::Point(3,0))); - // TODO fix path2.append(Geom::BezierCurve<3>(Geom::Point(3,0),Geom::Point(2,1),Geom::Point(1,1),Geom::Point(2,0))); + path2.append(Geom::CubicBezier(Geom::Point(3,0),Geom::Point(2,1),Geom::Point(1,1),Geom::Point(2,0))); path2.close(); // Open path - path3.append(Geom::SVGEllipticalArc(Geom::Point(4,0),1,2,M_PI,false,false,Geom::Point(5,1))); + path3.setStitching(true); + path3.append(Geom::EllipticalArc(Geom::Point(4,0),1,2,M_PI,false,false,Geom::Point(5,1))); path3.append(Geom::LineSegment(Geom::Point(5,1),Geom::Point(5,2))); path3.append(Geom::LineSegment(Geom::Point(6,4),Geom::Point(2,4))); } @@ -49,17 +50,17 @@ public: TS_ASSERT_EQUALS(curve.get_segment_count() , 0u); } { // Individual paths - Geom::PathVector pv(1, Geom::Path()); + Geom::PathVector pv((Geom::Path())); pv[0] = path1; TS_ASSERT_EQUALS(SPCurve(pv).get_segment_count() , 3u); pv[0] = path2; - TS_ASSERT_EQUALS(SPCurve(pv).get_segment_count() , 3u); + TS_ASSERT_EQUALS(SPCurve(pv).get_segment_count() , 2u); pv[0] = path3; TS_ASSERT_EQUALS(SPCurve(pv).get_segment_count() , 4u); pv[0] = path4; TS_ASSERT_EQUALS(SPCurve(pv).get_segment_count() , 0u); pv[0].close(); - TS_ASSERT_EQUALS(SPCurve(pv).get_segment_count() , 1u); + TS_ASSERT_EQUALS(SPCurve(pv).get_segment_count() , 0u); } { // Combination Geom::PathVector pv; @@ -68,7 +69,7 @@ public: pv.push_back(path3); pv.push_back(path4); SPCurve curve(pv); - TS_ASSERT_EQUALS(curve.get_segment_count() , 10u); + TS_ASSERT_EQUALS(curve.get_segment_count() , 9u); } } @@ -86,7 +87,7 @@ public: TS_ASSERT_EQUALS(curve.nodes_in_path() , 1u); } { // Individual paths - Geom::PathVector pv(1, Geom::Path()); + Geom::PathVector pv((Geom::Path())); pv[0] = path1; TS_ASSERT_EQUALS(SPCurve(pv).nodes_in_path() , 3u); pv[0] = path2; @@ -112,28 +113,28 @@ public: void testIsEmpty() { TS_ASSERT(SPCurve(Geom::PathVector()).is_empty()); - TS_ASSERT(!SPCurve(Geom::PathVector(1, path1)).is_empty()); - TS_ASSERT(!SPCurve(Geom::PathVector(1, path2)).is_empty()); - TS_ASSERT(!SPCurve(Geom::PathVector(1, path3)).is_empty()); - TS_ASSERT(!SPCurve(Geom::PathVector(1, path4)).is_empty()); + TS_ASSERT(!SPCurve(path1).is_empty()); + TS_ASSERT(!SPCurve(path2).is_empty()); + TS_ASSERT(!SPCurve(path3).is_empty()); + TS_ASSERT(!SPCurve(path4).is_empty()); } void testIsClosed() { TS_ASSERT(!SPCurve(Geom::PathVector()).is_closed()); - Geom::PathVector pv(1, Geom::Path()); + Geom::PathVector pv((Geom::Path())); TS_ASSERT(!SPCurve(pv).is_closed()); pv[0].close(); TS_ASSERT(SPCurve(pv).is_closed()); - TS_ASSERT(SPCurve(Geom::PathVector(1, path1)).is_closed()); - TS_ASSERT(SPCurve(Geom::PathVector(1, path2)).is_closed()); - TS_ASSERT(!SPCurve(Geom::PathVector(1, path3)).is_closed()); - TS_ASSERT(!SPCurve(Geom::PathVector(1, path4)).is_closed()); + TS_ASSERT(SPCurve(path1).is_closed()); + TS_ASSERT(SPCurve(path2).is_closed()); + TS_ASSERT(!SPCurve(path3).is_closed()); + TS_ASSERT(!SPCurve(path4).is_closed()); } void testLastFirstSegment() { - Geom::PathVector pv(1, path4); + Geom::PathVector pv(path4); TS_ASSERT_EQUALS(SPCurve(pv).first_segment() , (void*)0); TS_ASSERT_EQUALS(SPCurve(pv).last_segment() , (void*)0); pv[0].close(); @@ -185,10 +186,10 @@ public: void testFirstPoint() { - TS_ASSERT_EQUALS(*(SPCurve(Geom::PathVector(1, path1)).first_point()) , Geom::Point(0,0)); - TS_ASSERT_EQUALS(*(SPCurve(Geom::PathVector(1, path2)).first_point()) , Geom::Point(2,0)); - TS_ASSERT_EQUALS(*(SPCurve(Geom::PathVector(1, path3)).first_point()) , Geom::Point(4,0)); - TS_ASSERT_EQUALS(*(SPCurve(Geom::PathVector(1, path4)).first_point()) , Geom::Point(3,5)); + TS_ASSERT_EQUALS(*(SPCurve(path1).first_point()) , Geom::Point(0,0)); + TS_ASSERT_EQUALS(*(SPCurve(path2).first_point()) , Geom::Point(2,0)); + TS_ASSERT_EQUALS(*(SPCurve(path3).first_point()) , Geom::Point(4,0)); + TS_ASSERT_EQUALS(*(SPCurve(path4).first_point()) , Geom::Point(3,5)); Geom::PathVector pv; TS_ASSERT(!SPCurve(pv).first_point()); pv.push_back(path1); @@ -201,10 +202,10 @@ public: void testLastPoint() { - TS_ASSERT_EQUALS(*(SPCurve(Geom::PathVector(1, path1)).last_point()) , Geom::Point(0,0)); - TS_ASSERT_EQUALS(*(SPCurve(Geom::PathVector(1, path2)).last_point()) , Geom::Point(2,0)); - TS_ASSERT_EQUALS(*(SPCurve(Geom::PathVector(1, path3)).last_point()) , Geom::Point(8,4)); - TS_ASSERT_EQUALS(*(SPCurve(Geom::PathVector(1, path4)).last_point()) , Geom::Point(3,5)); + TS_ASSERT_EQUALS(*(SPCurve(path1).last_point()) , Geom::Point(0,0)); + TS_ASSERT_EQUALS(*(SPCurve(path2).last_point()) , Geom::Point(2,0)); + TS_ASSERT_EQUALS(*(SPCurve(path3).last_point()) , Geom::Point(8,4)); + TS_ASSERT_EQUALS(*(SPCurve(path4).last_point()) , Geom::Point(3,5)); Geom::PathVector pv; TS_ASSERT(!SPCurve(pv).last_point()); pv.push_back(path1); @@ -217,10 +218,10 @@ public: void testSecondPoint() { - TS_ASSERT_EQUALS( *(SPCurve(Geom::PathVector(1, path1)).second_point()) , Geom::Point(1,0)); - TS_ASSERT_EQUALS( *(SPCurve(Geom::PathVector(1, path2)).second_point()) , Geom::Point(3,0)); - TS_ASSERT_EQUALS( *(SPCurve(Geom::PathVector(1, path3)).second_point()) , Geom::Point(5,1)); - TS_ASSERT_EQUALS( *(SPCurve(Geom::PathVector(1, path4)).second_point()) , Geom::Point(3,5)); + TS_ASSERT_EQUALS( *(SPCurve(path1).second_point()) , Geom::Point(1,0)); + TS_ASSERT_EQUALS( *(SPCurve(path2).second_point()) , Geom::Point(3,0)); + TS_ASSERT_EQUALS( *(SPCurve(path3).second_point()) , Geom::Point(5,1)); + TS_ASSERT_EQUALS( *(SPCurve(path4).second_point()) , Geom::Point(3,5)); Geom::PathVector pv; pv.push_back(path1); pv.push_back(path2); @@ -232,10 +233,10 @@ public: void testPenultimatePoint() { - TS_ASSERT_EQUALS( *(SPCurve(Geom::PathVector(1, path1)).penultimate_point()) , Geom::Point(1,1)); - TS_ASSERT_EQUALS( *(SPCurve(Geom::PathVector(1, path2)).penultimate_point()) , Geom::Point(3,0)); - TS_ASSERT_EQUALS( *(SPCurve(Geom::PathVector(1, path3)).penultimate_point()) , Geom::Point(6,4)); - TS_ASSERT_EQUALS( *(SPCurve(Geom::PathVector(1, path4)).penultimate_point()) , Geom::Point(3,5)); + TS_ASSERT_EQUALS( *(SPCurve(Geom::PathVector(path1)).penultimate_point()) , Geom::Point(1,1)); + TS_ASSERT_EQUALS( *(SPCurve(Geom::PathVector(path2)).penultimate_point()) , Geom::Point(3,0)); + TS_ASSERT_EQUALS( *(SPCurve(Geom::PathVector(path3)).penultimate_point()) , Geom::Point(6,4)); + TS_ASSERT_EQUALS( *(SPCurve(Geom::PathVector(path4)).penultimate_point()) , Geom::Point(3,5)); Geom::PathVector pv; pv.push_back(path1); pv.push_back(path2); diff --git a/src/display/curve.cpp b/src/display/curve.cpp index d236d81cf..2f422c151 100644 --- a/src/display/curve.cpp +++ b/src/display/curve.cpp @@ -627,20 +627,10 @@ SPCurve::nodes_in_path() const { size_t nr = 0; for(Geom::PathVector::const_iterator it = _pathv.begin(); it != _pathv.end(); ++it) { - nr += (*it).size(); - - nr++; // count last node (this works also for closed paths because although they don't have a 'last node', they do have an extra segment - - // do not count closing knot double for zero-length closing line segments - // however, if the path is only a moveto, and is closed, do not subtract 1 (otherwise the result will be zero nodes) - if ( it->closed() - && ((*it).size() != 0) ) - { - Geom::Curve const &c = it->back_closed(); - if (are_near(c.initialPoint(), c.finalPoint())) { - nr--; - } - } + // if the path does not have any segments, it is a naked moveto, + // and therefore any path has at least one valid node + size_t psize = std::max(1ul, it->size_closed()); + nr += psize; } return nr; diff --git a/src/svg/svg-path-geom-test.h b/src/svg/svg-path-geom-test.h index 3558b4e55..c3972133d 100644 --- a/src/svg/svg-path-geom-test.h +++ b/src/svg/svg-path-geom-test.h @@ -453,38 +453,6 @@ private: return false; } } - else if(Geom::HLineSegment const *la = dynamic_cast(ca)) - { - Geom::HLineSegment const *lb = dynamic_cast(cb); - if (!Geom::are_near((*la).initialPoint(),(*lb).initialPoint(), eps)) { - char temp[200]; - sprintf(temp, "Different start of segment: (%g,%g) != (%g,%g), subpath: %u, segment: %u", (*la).initialPoint()[Geom::X], (*la).initialPoint()[Geom::Y], (*lb).initialPoint()[Geom::X], (*lb).initialPoint()[Geom::Y], static_cast(i), static_cast(j)); - TS_FAIL(temp); - return false; - } - if (!Geom::are_near((*la).finalPoint(),(*lb).finalPoint(), eps)) { - char temp[200]; - sprintf(temp, "Different end of segment: (%g,%g) != (%g,%g), subpath: %u, segment: %u", (*la).finalPoint()[Geom::X], (*la).finalPoint()[Geom::Y], (*lb).finalPoint()[Geom::X], (*lb).finalPoint()[Geom::Y], static_cast(i), static_cast(j)); - TS_FAIL(temp); - return false; - } - } - else if(Geom::VLineSegment const *la = dynamic_cast(ca)) - { - Geom::VLineSegment const *lb = dynamic_cast(cb); - if (!Geom::are_near((*la).initialPoint(),(*lb).initialPoint(), eps)) { - char temp[200]; - sprintf(temp, "Different start of segment: (%g,%g) != (%g,%g), subpath: %u, segment: %u", (*la).initialPoint()[Geom::X], (*la).initialPoint()[Geom::Y], (*lb).initialPoint()[Geom::X], (*lb).initialPoint()[Geom::Y], static_cast(i), static_cast(j)); - TS_FAIL(temp); - return false; - } - if (!Geom::are_near((*la).finalPoint(),(*lb).finalPoint(), eps)) { - char temp[200]; - sprintf(temp, "Different end of segment: (%g,%g) != (%g,%g), subpath: %u, segment: %u", (*la).finalPoint()[Geom::X], (*la).finalPoint()[Geom::Y], (*lb).finalPoint()[Geom::X], (*lb).finalPoint()[Geom::Y], static_cast(i), static_cast(j)); - TS_FAIL(temp); - return false; - } - } else if(Geom::CubicBezier const *la = dynamic_cast(ca)) { Geom::CubicBezier const *lb = dynamic_cast(cb); @@ -522,91 +490,10 @@ private: } else // not same type { - if(Geom::LineSegment const *la = dynamic_cast(ca)) - { - if (Geom::HLineSegment const *lb = dynamic_cast(cb)) { - if (!Geom::are_near((*la).initialPoint(),(*lb).initialPoint(), eps)) { - char temp[200]; - sprintf(temp, "Different start of segment: (%g,%g) != (%g,%g), subpath: %u, segment: %u", (*la).initialPoint()[Geom::X], (*la).initialPoint()[Geom::Y], (*lb).initialPoint()[Geom::X], (*lb).initialPoint()[Geom::Y], static_cast(i), static_cast(j)); - TS_FAIL(temp); - return false; - } - if (!Geom::are_near((*la).finalPoint(),(*lb).finalPoint(), eps)) { - char temp[200]; - sprintf(temp, "Different end of segment: (%g,%g) != (%g,%g), subpath: %u, segment: %u", (*la).finalPoint()[Geom::X], (*la).finalPoint()[Geom::Y], (*lb).finalPoint()[Geom::X], (*lb).finalPoint()[Geom::Y], static_cast(i), static_cast(j)); - TS_FAIL(temp); - return false; - } - char temp[200]; - sprintf(temp, "A LineSegment and an HLineSegment have been considered equal. Subpath: %u, segment: %u", static_cast(i), static_cast(j)); - TS_TRACE(temp); - } else if (Geom::VLineSegment const *lb = dynamic_cast(cb)) { - if (!Geom::are_near((*la).initialPoint(),(*lb).initialPoint(), eps)) { - char temp[200]; - sprintf(temp, "Different start of segment: (%g,%g) != (%g,%g), subpath: %u, segment: %u", (*la).initialPoint()[Geom::X], (*la).initialPoint()[Geom::Y], (*lb).initialPoint()[Geom::X], (*lb).initialPoint()[Geom::Y], static_cast(i), static_cast(j)); - TS_FAIL(temp); - return false; - } - if (!Geom::are_near((*la).finalPoint(),(*lb).finalPoint(), eps)) { - char temp[200]; - sprintf(temp, "Different end of segment: (%g,%g) != (%g,%g), subpath: %u, segment: %u", (*la).finalPoint()[Geom::X], (*la).finalPoint()[Geom::Y], (*lb).finalPoint()[Geom::X], (*lb).finalPoint()[Geom::Y], static_cast(i), static_cast(j)); - TS_FAIL(temp); - return false; - } - char temp[200]; - sprintf(temp, "A LineSegment and a VLineSegment have been considered equal. Subpath: %u, segment: %u", static_cast(i), static_cast(j)); - TS_TRACE(temp); - } else { - char temp[200]; - sprintf(temp, "Different curve types: %s != %s, subpath: %u, segment: %u", typeid(*ca).name(), typeid(*cb).name(), static_cast(i), static_cast(j)); - TS_FAIL(temp); - } - } - else if(Geom::LineSegment const *lb = dynamic_cast(cb)) - { - if (Geom::HLineSegment const *la = dynamic_cast(ca)) { - if (!Geom::are_near((*la).initialPoint(),(*lb).initialPoint(), eps)) { - char temp[200]; - sprintf(temp, "Different start of segment: (%g,%g) != (%g,%g), subpath: %u, segment: %u", (*la).initialPoint()[Geom::X], (*la).initialPoint()[Geom::Y], (*lb).initialPoint()[Geom::X], (*lb).initialPoint()[Geom::Y], static_cast(i), static_cast(j)); - TS_FAIL(temp); - return false; - } - if (!Geom::are_near((*la).finalPoint(),(*lb).finalPoint(), eps)) { - char temp[200]; - sprintf(temp, "Different end of segment: (%g,%g) != (%g,%g), subpath: %u, segment: %u", (*la).finalPoint()[Geom::X], (*la).finalPoint()[Geom::Y], (*lb).finalPoint()[Geom::X], (*lb).finalPoint()[Geom::Y], static_cast(i), static_cast(j)); - TS_FAIL(temp); - return false; - } - char temp[200]; - sprintf(temp, "An HLineSegment and a LineSegment have been considered equal. Subpath: %u, segment: %u", static_cast(i), static_cast(j)); - TS_TRACE(temp); - } else if (Geom::VLineSegment const *la = dynamic_cast(ca)) { - if (!Geom::are_near((*la).initialPoint(),(*lb).initialPoint(), eps)) { - char temp[200]; - sprintf(temp, "Different start of segment: (%g,%g) != (%g,%g), subpath: %u, segment: %u", (*la).initialPoint()[Geom::X], (*la).initialPoint()[Geom::Y], (*lb).initialPoint()[Geom::X], (*lb).initialPoint()[Geom::Y], static_cast(i), static_cast(j)); - TS_FAIL(temp); - return false; - } - if (!Geom::are_near((*la).finalPoint(),(*lb).finalPoint(), eps)) { - char temp[200]; - sprintf(temp, "Different end of segment: (%g,%g) != (%g,%g), subpath: %u, segment: %u", (*la).finalPoint()[Geom::X], (*la).finalPoint()[Geom::Y], (*lb).finalPoint()[Geom::X], (*lb).finalPoint()[Geom::Y], static_cast(i), static_cast(j)); - TS_FAIL(temp); - return false; - } - char temp[200]; - sprintf(temp, "A VLineSegment and a LineSegment have been considered equal. Subpath: %u, segment: %u", static_cast(i), static_cast(j)); - TS_TRACE(temp); - } else { - char temp[200]; - sprintf(temp, "Different curve types: %s != %s, subpath: %u, segment: %u", typeid(*ca).name(), typeid(*cb).name(), static_cast(i), static_cast(j)); - TS_FAIL(temp); - return false; - } - } else { - char temp[200]; - sprintf(temp, "Different curve types: %s != %s, subpath: %u, segment: %u", typeid(*ca).name(), typeid(*cb).name(), static_cast(i), static_cast(j)); - TS_FAIL(temp); - } + char temp[200]; + sprintf(temp, "Different curve types: %s != %s, subpath: %u, segment: %u", typeid(*ca).name(), typeid(*cb).name(), static_cast(i), static_cast(j)); + TS_FAIL(temp); + return false; } } } -- cgit v1.2.3 From 87e7de3977d4aff931588b894d0c02a4ecdf4dc2 Mon Sep 17 00:00:00 2001 From: Nicolas Dufour Date: Sun, 5 Jul 2015 11:18:18 +0200 Subject: Colors. Fix for Bug #1457069 (ICC profile filename with [] causes a crash). Fixed bugs: - https://launchpad.net/bugs/1457069 (bzr r14229) --- src/color-profile.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/color-profile.cpp b/src/color-profile.cpp index 85a6fa876..690a72654 100644 --- a/src/color-profile.cpp +++ b/src/color-profile.cpp @@ -313,7 +313,7 @@ void ColorProfile::set(unsigned key, gchar const *value) { //# 1. Get complete URI of document gchar const *docbase = doc->getURI(); - gchar* escaped = g_uri_escape_string(this->href, "!*'();:@=+$,/?#[]", TRUE); + gchar* escaped = g_uri_escape_string(this->href, "!*'();:@=+$,/?#", TRUE); //g_message("docbase:%s\n", docbase); //org::w3c::dom::URI docUri(docbase); -- cgit v1.2.3 From 9d3a473d605143b5ebc753c732eb19aeced034b2 Mon Sep 17 00:00:00 2001 From: su_v Date: Sun, 5 Jul 2015 12:27:22 +0200 Subject: SVG font editor: fix defunct Kerning tab (bug #1406543) (bzr r14230) --- src/sp-factory.cpp | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/sp-factory.cpp b/src/sp-factory.cpp index e48646f8d..55e673c4a 100644 --- a/src/sp-factory.cpp +++ b/src/sp-factory.cpp @@ -27,6 +27,7 @@ #include "sp-font.h" #include "sp-font-face.h" #include "sp-glyph.h" +#include "sp-glyph-kerning.h" #include "sp-guide.h" #include "sp-hatch.h" #include "sp-hatch-path.h" @@ -150,6 +151,10 @@ SPObject *SPFactory::createObject(std::string const& id) ret = new SPFontFace; else if (id == "svg:glyph") ret = new SPGlyph; + else if (id == "svg:hkern") + ret = new SPHkern; + else if (id == "svg:vkern") + ret = new SPVkern; else if (id == "sodipodi:guide") ret = new SPGuide; else if (id == "svg:hatch") -- cgit v1.2.3 From 076eab9954fac28735dcf10882c32b79a0915c26 Mon Sep 17 00:00:00 2001 From: su_v Date: Sun, 5 Jul 2015 12:49:45 +0200 Subject: icons: add missing icon 'dialog-templates' used for 'File > New from Template' (bzr r14231) --- share/icons/icons.svg | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/share/icons/icons.svg b/share/icons/icons.svg index 78ae37681..35ef6ab8c 100644 --- a/share/icons/icons.svg +++ b/share/icons/icons.svg @@ -861,6 +861,11 @@ + + + + + @@ -3941,4 +3946,12 @@ http://www.inkscape.org/ + + + + + + + + -- cgit v1.2.3 From b020354dc251990e7113eb402be5ff9c0e1faa7c Mon Sep 17 00:00:00 2001 From: Krzysztof Kosi??ski Date: Sun, 5 Jul 2015 21:02:33 +0200 Subject: Hopefully fix build failure on i386 (bzr r14232) --- src/display/curve.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/display/curve.cpp b/src/display/curve.cpp index 2f422c151..386c63166 100644 --- a/src/display/curve.cpp +++ b/src/display/curve.cpp @@ -629,7 +629,7 @@ SPCurve::nodes_in_path() const for(Geom::PathVector::const_iterator it = _pathv.begin(); it != _pathv.end(); ++it) { // if the path does not have any segments, it is a naked moveto, // and therefore any path has at least one valid node - size_t psize = std::max(1ul, it->size_closed()); + size_t psize = std::max(1, it->size_closed()); nr += psize; } -- cgit v1.2.3 From ddd25c3640413081276af5af1c81aa2ac81346a1 Mon Sep 17 00:00:00 2001 From: Nicolas Dufour Date: Mon, 6 Jul 2015 10:47:18 +0200 Subject: 2geom. Fixes make distcheck failure. (bzr r14233) --- src/2geom/Makefile_insert | 1 - 1 file changed, 1 deletion(-) diff --git a/src/2geom/Makefile_insert b/src/2geom/Makefile_insert index 01f48ab39..e3c6836fd 100644 --- a/src/2geom/Makefile_insert +++ b/src/2geom/Makefile_insert @@ -57,7 +57,6 @@ 2geom/generic-rect.h \ 2geom/geom.cpp \ 2geom/geom.h \ - 2geom/hvlinesegment.h \ 2geom/intersection.h \ 2geom/intersection-graph.cpp \ 2geom/intersection-graph.h \ -- cgit v1.2.3 From b07139c96968e618b1b7667706cae7a963c28363 Mon Sep 17 00:00:00 2001 From: Nicolas Dufour Date: Mon, 6 Jul 2015 10:49:46 +0200 Subject: Extensions. Fix for Bug #760429 (Scour extension files organization). Fixed bugs: - https://launchpad.net/bugs/760429 (bzr r14234) --- configure.ac | 1 + po/inkscape.pot | 1960 ++++++++++-------- share/extensions/CMakeLists.txt | 3 + share/extensions/Makefile.am | 3 +- share/extensions/scour.inkscape.py | 78 - share/extensions/scour.inx | 8 +- share/extensions/scour.py | 3235 ------------------------------ share/extensions/scour/Makefile.am | 13 + share/extensions/scour/scour.inkscape.py | 78 + share/extensions/scour/scour.py | 3235 ++++++++++++++++++++++++++++++ share/extensions/scour/svg_regex.py | 286 +++ share/extensions/scour/svg_transform.py | 236 +++ share/extensions/scour/yocto_css.py | 75 + share/extensions/svg_regex.py | 286 --- share/extensions/svg_transform.py | 236 --- share/extensions/yocto_css.py | 75 - 16 files changed, 5074 insertions(+), 4734 deletions(-) delete mode 100755 share/extensions/scour.inkscape.py delete mode 100644 share/extensions/scour.py create mode 100644 share/extensions/scour/Makefile.am create mode 100755 share/extensions/scour/scour.inkscape.py create mode 100644 share/extensions/scour/scour.py create mode 100755 share/extensions/scour/svg_regex.py create mode 100755 share/extensions/scour/svg_transform.py create mode 100755 share/extensions/scour/yocto_css.py delete mode 100755 share/extensions/svg_regex.py delete mode 100755 share/extensions/svg_transform.py delete mode 100755 share/extensions/yocto_css.py diff --git a/configure.ac b/configure.ac index f5d16457b..aeda9981d 100644 --- a/configure.ac +++ b/configure.ac @@ -1153,6 +1153,7 @@ share/extensions/Makefile share/extensions/alphabet_soup/Makefile share/extensions/Barcode/Makefile share/extensions/Poly3DObjects/Makefile +share/extensions/scour/Makefile share/extensions/test/Makefile share/extensions/xaml2svg/Makefile share/extensions/ink2canvas/Makefile diff --git a/po/inkscape.pot b/po/inkscape.pot index f75ec959e..dbb505b18 100644 --- a/po/inkscape.pot +++ b/po/inkscape.pot @@ -8,7 +8,7 @@ msgid "" msgstr "" "Project-Id-Version: PACKAGE VERSION\n" "Report-Msgid-Bugs-To: inkscape-devel@lists.sourceforge.net\n" -"POT-Creation-Date: 2015-05-18 12:45+0200\n" +"POT-Creation-Date: 2015-07-06 10:27+0200\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME \n" "Language-Team: LANGUAGE \n" @@ -3308,1032 +3308,1032 @@ msgstr "" msgid "Old paint (bitmap)" msgstr "" -#. Symbols: ./MapSymbolsNPS.svg +#. Symbols: ./AigaSymbols.svg #: ../share/symbols/symbols.h:2 msgctxt "Symbol" -msgid "United States National Park Service Map Symbols" +msgid "AIGA Symbol Signs" msgstr "" +#. Symbols: ./AigaSymbols.svg #. Symbols: ./MapSymbolsNPS.svg #: ../share/symbols/symbols.h:3 ../share/symbols/symbols.h:4 +#: ../share/symbols/symbols.h:281 ../share/symbols/symbols.h:282 msgctxt "Symbol" -msgid "Airport" +msgid "Telephone" msgstr "" -#. Symbols: ./MapSymbolsNPS.svg +#. Symbols: ./AigaSymbols.svg #: ../share/symbols/symbols.h:5 ../share/symbols/symbols.h:6 msgctxt "Symbol" -msgid "Amphitheatre" +msgid "Mail" msgstr "" -#. Symbols: ./MapSymbolsNPS.svg +#. Symbols: ./AigaSymbols.svg #: ../share/symbols/symbols.h:7 ../share/symbols/symbols.h:8 msgctxt "Symbol" -msgid "Bicycle Trail" +msgid "Currency Exchange" msgstr "" -#. Symbols: ./MapSymbolsNPS.svg +#. Symbols: ./AigaSymbols.svg #: ../share/symbols/symbols.h:9 ../share/symbols/symbols.h:10 msgctxt "Symbol" -msgid "Boat Launch" +msgid "Currency Exchange - Euro" msgstr "" -#. Symbols: ./MapSymbolsNPS.svg +#. Symbols: ./AigaSymbols.svg #: ../share/symbols/symbols.h:11 ../share/symbols/symbols.h:12 msgctxt "Symbol" -msgid "Boat Tour" +msgid "Cashier" msgstr "" +#. Symbols: ./AigaSymbols.svg #. Symbols: ./MapSymbolsNPS.svg #: ../share/symbols/symbols.h:13 ../share/symbols/symbols.h:14 +#: ../share/symbols/symbols.h:213 ../share/symbols/symbols.h:214 msgctxt "Symbol" -msgid "Bus Stop" +msgid "First Aid" msgstr "" -#. Symbols: ./MapSymbolsNPS.svg +#. Symbols: ./AigaSymbols.svg #: ../share/symbols/symbols.h:15 ../share/symbols/symbols.h:16 msgctxt "Symbol" -msgid "Campfire" +msgid "Lost and Found" msgstr "" -#. Symbols: ./MapSymbolsNPS.svg +#. Symbols: ./AigaSymbols.svg #: ../share/symbols/symbols.h:17 ../share/symbols/symbols.h:18 msgctxt "Symbol" -msgid "Campground" +msgid "Coat Check" msgstr "" -#. Symbols: ./MapSymbolsNPS.svg +#. Symbols: ./AigaSymbols.svg #: ../share/symbols/symbols.h:19 ../share/symbols/symbols.h:20 msgctxt "Symbol" -msgid "CanoeAccess" +msgid "Baggage Lockers" msgstr "" -#. Symbols: ./MapSymbolsNPS.svg +#. Symbols: ./AigaSymbols.svg #: ../share/symbols/symbols.h:21 ../share/symbols/symbols.h:22 msgctxt "Symbol" -msgid "Crosscountry Ski Trail" +msgid "Escalator" msgstr "" -#. Symbols: ./MapSymbolsNPS.svg +#. Symbols: ./AigaSymbols.svg #: ../share/symbols/symbols.h:23 ../share/symbols/symbols.h:24 msgctxt "Symbol" -msgid "Downhill Skiing" +msgid "Escalator Down" msgstr "" -#. Symbols: ./MapSymbolsNPS.svg +#. Symbols: ./AigaSymbols.svg #: ../share/symbols/symbols.h:25 ../share/symbols/symbols.h:26 msgctxt "Symbol" -msgid "Drinking Water" +msgid "Escalator Up" msgstr "" -#. Symbols: ./MapSymbolsNPS.svg #. Symbols: ./AigaSymbols.svg #: ../share/symbols/symbols.h:27 ../share/symbols/symbols.h:28 -#: ../share/symbols/symbols.h:172 ../share/symbols/symbols.h:173 msgctxt "Symbol" -msgid "First Aid" +msgid "Stairs" msgstr "" -#. Symbols: ./MapSymbolsNPS.svg +#. Symbols: ./AigaSymbols.svg #: ../share/symbols/symbols.h:29 ../share/symbols/symbols.h:30 msgctxt "Symbol" -msgid "Fishing" +msgid "Stairs Down" msgstr "" -#. Symbols: ./MapSymbolsNPS.svg +#. Symbols: ./AigaSymbols.svg #: ../share/symbols/symbols.h:31 ../share/symbols/symbols.h:32 msgctxt "Symbol" -msgid "Food Service" +msgid "Stairs Up" msgstr "" -#. Symbols: ./MapSymbolsNPS.svg +#. Symbols: ./AigaSymbols.svg #: ../share/symbols/symbols.h:33 ../share/symbols/symbols.h:34 msgctxt "Symbol" -msgid "Four Wheel Drive Road" +msgid "Elevator" msgstr "" -#. Symbols: ./MapSymbolsNPS.svg +#. Symbols: ./AigaSymbols.svg #: ../share/symbols/symbols.h:35 ../share/symbols/symbols.h:36 msgctxt "Symbol" -msgid "Gas Station" +msgid "Toilets - Men" msgstr "" -#. Symbols: ./MapSymbolsNPS.svg +#. Symbols: ./AigaSymbols.svg #: ../share/symbols/symbols.h:37 ../share/symbols/symbols.h:38 msgctxt "Symbol" -msgid "Golfing" +msgid "Toilets - Women" msgstr "" -#. Symbols: ./MapSymbolsNPS.svg +#. Symbols: ./AigaSymbols.svg #: ../share/symbols/symbols.h:39 ../share/symbols/symbols.h:40 msgctxt "Symbol" -msgid "Horseback Riding" +msgid "Toilets" msgstr "" -#. Symbols: ./MapSymbolsNPS.svg +#. Symbols: ./AigaSymbols.svg #: ../share/symbols/symbols.h:41 ../share/symbols/symbols.h:42 msgctxt "Symbol" -msgid "Hospital" +msgid "Nursery" msgstr "" -#. Symbols: ./MapSymbolsNPS.svg +#. Symbols: ./AigaSymbols.svg #: ../share/symbols/symbols.h:43 ../share/symbols/symbols.h:44 msgctxt "Symbol" -msgid "Ice Skating" +msgid "Drinking Fountain" msgstr "" -#. Symbols: ./MapSymbolsNPS.svg #. Symbols: ./AigaSymbols.svg #: ../share/symbols/symbols.h:45 ../share/symbols/symbols.h:46 -#: ../share/symbols/symbols.h:206 ../share/symbols/symbols.h:207 msgctxt "Symbol" -msgid "Information" +msgid "Waiting Room" msgstr "" +#. Symbols: ./AigaSymbols.svg #. Symbols: ./MapSymbolsNPS.svg #: ../share/symbols/symbols.h:47 ../share/symbols/symbols.h:48 +#: ../share/symbols/symbols.h:231 ../share/symbols/symbols.h:232 msgctxt "Symbol" -msgid "Litter Receptacle" +msgid "Information" msgstr "" -#. Symbols: ./MapSymbolsNPS.svg +#. Symbols: ./AigaSymbols.svg #: ../share/symbols/symbols.h:49 ../share/symbols/symbols.h:50 msgctxt "Symbol" -msgid "Lodging" +msgid "Hotel Information" msgstr "" -#. Symbols: ./MapSymbolsNPS.svg +#. Symbols: ./AigaSymbols.svg #: ../share/symbols/symbols.h:51 ../share/symbols/symbols.h:52 msgctxt "Symbol" -msgid "Marina" +msgid "Air Transportation" msgstr "" -#. Symbols: ./MapSymbolsNPS.svg +#. Symbols: ./AigaSymbols.svg #: ../share/symbols/symbols.h:53 ../share/symbols/symbols.h:54 msgctxt "Symbol" -msgid "Motorbike Trail" +msgid "Heliport" msgstr "" -#. Symbols: ./MapSymbolsNPS.svg +#. Symbols: ./AigaSymbols.svg #: ../share/symbols/symbols.h:55 ../share/symbols/symbols.h:56 msgctxt "Symbol" -msgid "Radiator Water" +msgid "Taxi" msgstr "" -#. Symbols: ./MapSymbolsNPS.svg +#. Symbols: ./AigaSymbols.svg #: ../share/symbols/symbols.h:57 ../share/symbols/symbols.h:58 msgctxt "Symbol" -msgid "Recycling" +msgid "Bus" msgstr "" -#. Symbols: ./MapSymbolsNPS.svg #. Symbols: ./AigaSymbols.svg #: ../share/symbols/symbols.h:59 ../share/symbols/symbols.h:60 -#: ../share/symbols/symbols.h:258 ../share/symbols/symbols.h:259 msgctxt "Symbol" -msgid "Parking" +msgid "Ground Transportation" msgstr "" -#. Symbols: ./MapSymbolsNPS.svg +#. Symbols: ./AigaSymbols.svg #: ../share/symbols/symbols.h:61 ../share/symbols/symbols.h:62 msgctxt "Symbol" -msgid "Pets On Leash" +msgid "Rail Transportation" msgstr "" -#. Symbols: ./MapSymbolsNPS.svg +#. Symbols: ./AigaSymbols.svg #: ../share/symbols/symbols.h:63 ../share/symbols/symbols.h:64 msgctxt "Symbol" -msgid "Picnic Area" +msgid "Water Transportation" msgstr "" -#. Symbols: ./MapSymbolsNPS.svg +#. Symbols: ./AigaSymbols.svg #: ../share/symbols/symbols.h:65 ../share/symbols/symbols.h:66 msgctxt "Symbol" -msgid "Post Office" +msgid "Car Rental" msgstr "" -#. Symbols: ./MapSymbolsNPS.svg +#. Symbols: ./AigaSymbols.svg #: ../share/symbols/symbols.h:67 ../share/symbols/symbols.h:68 msgctxt "Symbol" -msgid "Ranger Station" +msgid "Restaurant" msgstr "" -#. Symbols: ./MapSymbolsNPS.svg +#. Symbols: ./AigaSymbols.svg #: ../share/symbols/symbols.h:69 ../share/symbols/symbols.h:70 msgctxt "Symbol" -msgid "RV Campground" +msgid "Coffeeshop" msgstr "" -#. Symbols: ./MapSymbolsNPS.svg +#. Symbols: ./AigaSymbols.svg #: ../share/symbols/symbols.h:71 ../share/symbols/symbols.h:72 msgctxt "Symbol" -msgid "Restrooms" +msgid "Bar" msgstr "" -#. Symbols: ./MapSymbolsNPS.svg +#. Symbols: ./AigaSymbols.svg #: ../share/symbols/symbols.h:73 ../share/symbols/symbols.h:74 msgctxt "Symbol" -msgid "Sailing" +msgid "Shops" msgstr "" -#. Symbols: ./MapSymbolsNPS.svg +#. Symbols: ./AigaSymbols.svg #: ../share/symbols/symbols.h:75 ../share/symbols/symbols.h:76 msgctxt "Symbol" -msgid "Sanitary Disposal Station" +msgid "Barber Shop - Beauty Salon" msgstr "" -#. Symbols: ./MapSymbolsNPS.svg +#. Symbols: ./AigaSymbols.svg #: ../share/symbols/symbols.h:77 ../share/symbols/symbols.h:78 msgctxt "Symbol" -msgid "Scuba Diving" +msgid "Barber Shop" msgstr "" -#. Symbols: ./MapSymbolsNPS.svg +#. Symbols: ./AigaSymbols.svg #: ../share/symbols/symbols.h:79 ../share/symbols/symbols.h:80 msgctxt "Symbol" -msgid "Self Guided Trail" +msgid "Beauty Salon" msgstr "" -#. Symbols: ./MapSymbolsNPS.svg +#. Symbols: ./AigaSymbols.svg #: ../share/symbols/symbols.h:81 ../share/symbols/symbols.h:82 msgctxt "Symbol" -msgid "Shelter" +msgid "Ticket Purchase" msgstr "" -#. Symbols: ./MapSymbolsNPS.svg +#. Symbols: ./AigaSymbols.svg #: ../share/symbols/symbols.h:83 ../share/symbols/symbols.h:84 msgctxt "Symbol" -msgid "Showers" +msgid "Baggage Check In" msgstr "" -#. Symbols: ./MapSymbolsNPS.svg +#. Symbols: ./AigaSymbols.svg #: ../share/symbols/symbols.h:85 ../share/symbols/symbols.h:86 msgctxt "Symbol" -msgid "Sledding" +msgid "Baggage Claim" msgstr "" -#. Symbols: ./MapSymbolsNPS.svg +#. Symbols: ./AigaSymbols.svg #: ../share/symbols/symbols.h:87 ../share/symbols/symbols.h:88 msgctxt "Symbol" -msgid "SnowmobileTrail" +msgid "Customs" msgstr "" -#. Symbols: ./MapSymbolsNPS.svg +#. Symbols: ./AigaSymbols.svg #: ../share/symbols/symbols.h:89 ../share/symbols/symbols.h:90 msgctxt "Symbol" -msgid "Stable" +msgid "Immigration" msgstr "" -#. Symbols: ./MapSymbolsNPS.svg +#. Symbols: ./AigaSymbols.svg #: ../share/symbols/symbols.h:91 ../share/symbols/symbols.h:92 msgctxt "Symbol" -msgid "Store" +msgid "Departing Flights" msgstr "" -#. Symbols: ./MapSymbolsNPS.svg +#. Symbols: ./AigaSymbols.svg #: ../share/symbols/symbols.h:93 ../share/symbols/symbols.h:94 msgctxt "Symbol" -msgid "Swimming" +msgid "Arriving Flights" msgstr "" -#. Symbols: ./MapSymbolsNPS.svg #. Symbols: ./AigaSymbols.svg #: ../share/symbols/symbols.h:95 ../share/symbols/symbols.h:96 -#: ../share/symbols/symbols.h:162 ../share/symbols/symbols.h:163 msgctxt "Symbol" -msgid "Telephone" +msgid "Smoking" msgstr "" -#. Symbols: ./MapSymbolsNPS.svg +#. Symbols: ./AigaSymbols.svg #: ../share/symbols/symbols.h:97 ../share/symbols/symbols.h:98 msgctxt "Symbol" -msgid "Emergency Telephone" +msgid "No Smoking" msgstr "" +#. Symbols: ./AigaSymbols.svg #. Symbols: ./MapSymbolsNPS.svg #: ../share/symbols/symbols.h:99 ../share/symbols/symbols.h:100 +#: ../share/symbols/symbols.h:245 ../share/symbols/symbols.h:246 msgctxt "Symbol" -msgid "Trailhead" +msgid "Parking" msgstr "" -#. Symbols: ./MapSymbolsNPS.svg +#. Symbols: ./AigaSymbols.svg #: ../share/symbols/symbols.h:101 ../share/symbols/symbols.h:102 msgctxt "Symbol" -msgid "Wheelchair Accessible" +msgid "No Parking" msgstr "" -#. Symbols: ./MapSymbolsNPS.svg +#. Symbols: ./AigaSymbols.svg #: ../share/symbols/symbols.h:103 ../share/symbols/symbols.h:104 msgctxt "Symbol" -msgid "Wind Surfing" +msgid "No Dogs" msgstr "" -#. Symbols: ./MapSymbolsNPS.svg -#: ../share/symbols/symbols.h:105 +#. Symbols: ./AigaSymbols.svg +#: ../share/symbols/symbols.h:105 ../share/symbols/symbols.h:106 msgctxt "Symbol" -msgid "Blank" +msgid "No Entry" +msgstr "" + +#. Symbols: ./AigaSymbols.svg +#: ../share/symbols/symbols.h:107 ../share/symbols/symbols.h:108 +msgctxt "Symbol" +msgid "Exit" +msgstr "" + +#. Symbols: ./AigaSymbols.svg +#: ../share/symbols/symbols.h:109 ../share/symbols/symbols.h:110 +msgctxt "Symbol" +msgid "Fire Extinguisher" +msgstr "" + +#. Symbols: ./AigaSymbols.svg +#: ../share/symbols/symbols.h:111 ../share/symbols/symbols.h:112 +msgctxt "Symbol" +msgid "Right Arrow" +msgstr "" + +#. Symbols: ./AigaSymbols.svg +#: ../share/symbols/symbols.h:113 ../share/symbols/symbols.h:114 +msgctxt "Symbol" +msgid "Forward and Right Arrow" +msgstr "" + +#. Symbols: ./AigaSymbols.svg +#: ../share/symbols/symbols.h:115 ../share/symbols/symbols.h:116 +msgctxt "Symbol" +msgid "Up Arrow" +msgstr "" + +#. Symbols: ./AigaSymbols.svg +#: ../share/symbols/symbols.h:117 ../share/symbols/symbols.h:118 +msgctxt "Symbol" +msgid "Forward and Left Arrow" +msgstr "" + +#. Symbols: ./AigaSymbols.svg +#: ../share/symbols/symbols.h:119 ../share/symbols/symbols.h:120 +msgctxt "Symbol" +msgid "Left Arrow" +msgstr "" + +#. Symbols: ./AigaSymbols.svg +#: ../share/symbols/symbols.h:121 ../share/symbols/symbols.h:122 +msgctxt "Symbol" +msgid "Left and Down Arrow" +msgstr "" + +#. Symbols: ./AigaSymbols.svg +#: ../share/symbols/symbols.h:123 ../share/symbols/symbols.h:124 +msgctxt "Symbol" +msgid "Down Arrow" +msgstr "" + +#. Symbols: ./AigaSymbols.svg +#: ../share/symbols/symbols.h:125 ../share/symbols/symbols.h:126 +msgctxt "Symbol" +msgid "Right and Down Arrow" +msgstr "" + +#. Symbols: ./AigaSymbols.svg +#: ../share/symbols/symbols.h:127 ../share/symbols/symbols.h:128 +msgctxt "Symbol" +msgid "NPS Wheelchair Accessible - 1996" +msgstr "" + +#. Symbols: ./AigaSymbols.svg +#: ../share/symbols/symbols.h:129 ../share/symbols/symbols.h:130 +msgctxt "Symbol" +msgid "NPS Wheelchair Accessible" +msgstr "" + +#. Symbols: ./AigaSymbols.svg +#: ../share/symbols/symbols.h:131 ../share/symbols/symbols.h:132 +msgctxt "Symbol" +msgid "New Wheelchair Accessible" +msgstr "" + +#. Symbols: ./BalloonSymbols.svg +#: ../share/symbols/symbols.h:133 +msgctxt "Symbol" +msgid "Word Balloons" +msgstr "" + +#. Symbols: ./BalloonSymbols.svg +#: ../share/symbols/symbols.h:134 +msgctxt "Symbol" +msgid "Thought Balloon" +msgstr "" + +#. Symbols: ./BalloonSymbols.svg +#: ../share/symbols/symbols.h:135 +msgctxt "Symbol" +msgid "Dream Speaking" +msgstr "" + +#. Symbols: ./BalloonSymbols.svg +#: ../share/symbols/symbols.h:136 +msgctxt "Symbol" +msgid "Rounded Balloon" +msgstr "" + +#. Symbols: ./BalloonSymbols.svg +#: ../share/symbols/symbols.h:137 +msgctxt "Symbol" +msgid "Squared Balloon" +msgstr "" + +#. Symbols: ./BalloonSymbols.svg +#: ../share/symbols/symbols.h:138 +msgctxt "Symbol" +msgid "Over the Phone" +msgstr "" + +#. Symbols: ./BalloonSymbols.svg +#: ../share/symbols/symbols.h:139 +msgctxt "Symbol" +msgid "Hip Balloon" +msgstr "" + +#. Symbols: ./BalloonSymbols.svg +#: ../share/symbols/symbols.h:140 +msgctxt "Symbol" +msgid "Circle Balloon" +msgstr "" + +#. Symbols: ./BalloonSymbols.svg +#: ../share/symbols/symbols.h:141 +msgctxt "Symbol" +msgid "Exclaim Balloon" msgstr "" #. Symbols: ./FlowSymbols.svg -#: ../share/symbols/symbols.h:106 +#: ../share/symbols/symbols.h:142 msgctxt "Symbol" msgid "Flow Chart Shapes" msgstr "" #. Symbols: ./FlowSymbols.svg -#: ../share/symbols/symbols.h:107 +#: ../share/symbols/symbols.h:143 msgctxt "Symbol" msgid "Process" msgstr "" #. Symbols: ./FlowSymbols.svg -#: ../share/symbols/symbols.h:108 +#: ../share/symbols/symbols.h:144 msgctxt "Symbol" msgid "Input/Output" msgstr "" #. Symbols: ./FlowSymbols.svg -#: ../share/symbols/symbols.h:109 +#: ../share/symbols/symbols.h:145 msgctxt "Symbol" msgid "Document" msgstr "" #. Symbols: ./FlowSymbols.svg -#: ../share/symbols/symbols.h:110 +#: ../share/symbols/symbols.h:146 msgctxt "Symbol" msgid "Manual Operation" msgstr "" #. Symbols: ./FlowSymbols.svg -#: ../share/symbols/symbols.h:111 +#: ../share/symbols/symbols.h:147 msgctxt "Symbol" msgid "Preparation" msgstr "" #. Symbols: ./FlowSymbols.svg -#: ../share/symbols/symbols.h:112 +#: ../share/symbols/symbols.h:148 msgctxt "Symbol" msgid "Merge" msgstr "" #. Symbols: ./FlowSymbols.svg -#: ../share/symbols/symbols.h:113 +#: ../share/symbols/symbols.h:149 msgctxt "Symbol" msgid "Decision" msgstr "" #. Symbols: ./FlowSymbols.svg -#: ../share/symbols/symbols.h:114 +#: ../share/symbols/symbols.h:150 msgctxt "Symbol" msgid "Magnetic Tape" msgstr "" #. Symbols: ./FlowSymbols.svg -#: ../share/symbols/symbols.h:115 +#: ../share/symbols/symbols.h:151 msgctxt "Symbol" msgid "Display" msgstr "" #. Symbols: ./FlowSymbols.svg -#: ../share/symbols/symbols.h:116 +#: ../share/symbols/symbols.h:152 msgctxt "Symbol" msgid "Auxiliary Operation" msgstr "" #. Symbols: ./FlowSymbols.svg -#: ../share/symbols/symbols.h:117 +#: ../share/symbols/symbols.h:153 msgctxt "Symbol" msgid "Manual Input" msgstr "" #. Symbols: ./FlowSymbols.svg -#: ../share/symbols/symbols.h:118 +#: ../share/symbols/symbols.h:154 msgctxt "Symbol" msgid "Extract" msgstr "" #. Symbols: ./FlowSymbols.svg -#: ../share/symbols/symbols.h:119 +#: ../share/symbols/symbols.h:155 msgctxt "Symbol" msgid "Terminal/Interrupt" msgstr "" #. Symbols: ./FlowSymbols.svg -#: ../share/symbols/symbols.h:120 +#: ../share/symbols/symbols.h:156 msgctxt "Symbol" msgid "Punched Card" msgstr "" #. Symbols: ./FlowSymbols.svg -#: ../share/symbols/symbols.h:121 +#: ../share/symbols/symbols.h:157 msgctxt "Symbol" msgid "Punch Tape" msgstr "" #. Symbols: ./FlowSymbols.svg -#: ../share/symbols/symbols.h:122 +#: ../share/symbols/symbols.h:158 msgctxt "Symbol" msgid "Online Storage" msgstr "" #. Symbols: ./FlowSymbols.svg -#: ../share/symbols/symbols.h:123 +#: ../share/symbols/symbols.h:159 msgctxt "Symbol" msgid "Keying" msgstr "" #. Symbols: ./FlowSymbols.svg -#: ../share/symbols/symbols.h:124 +#: ../share/symbols/symbols.h:160 msgctxt "Symbol" msgid "Sort" msgstr "" #. Symbols: ./FlowSymbols.svg -#: ../share/symbols/symbols.h:125 +#: ../share/symbols/symbols.h:161 msgctxt "Symbol" msgid "Connector" msgstr "" #. Symbols: ./FlowSymbols.svg -#: ../share/symbols/symbols.h:126 +#: ../share/symbols/symbols.h:162 msgctxt "Symbol" msgid "Off-Page Connector" msgstr "" #. Symbols: ./FlowSymbols.svg -#: ../share/symbols/symbols.h:127 +#: ../share/symbols/symbols.h:163 msgctxt "Symbol" msgid "Transmittal Tape" msgstr "" #. Symbols: ./FlowSymbols.svg -#: ../share/symbols/symbols.h:128 +#: ../share/symbols/symbols.h:164 msgctxt "Symbol" msgid "Communication Link" msgstr "" #. Symbols: ./FlowSymbols.svg -#: ../share/symbols/symbols.h:129 +#: ../share/symbols/symbols.h:165 msgctxt "Symbol" msgid "Collate" msgstr "" #. Symbols: ./FlowSymbols.svg -#: ../share/symbols/symbols.h:130 +#: ../share/symbols/symbols.h:166 msgctxt "Symbol" msgid "Comment/Annotation" msgstr "" #. Symbols: ./FlowSymbols.svg -#: ../share/symbols/symbols.h:131 +#: ../share/symbols/symbols.h:167 msgctxt "Symbol" msgid "Core" msgstr "" #. Symbols: ./FlowSymbols.svg -#: ../share/symbols/symbols.h:132 +#: ../share/symbols/symbols.h:168 msgctxt "Symbol" msgid "Predefined Process" msgstr "" #. Symbols: ./FlowSymbols.svg -#: ../share/symbols/symbols.h:133 +#: ../share/symbols/symbols.h:169 msgctxt "Symbol" msgid "Magnetic Disk (Database)" msgstr "" #. Symbols: ./FlowSymbols.svg -#: ../share/symbols/symbols.h:134 +#: ../share/symbols/symbols.h:170 msgctxt "Symbol" msgid "Magnetic Drum (Direct Access)" msgstr "" #. Symbols: ./FlowSymbols.svg -#: ../share/symbols/symbols.h:135 +#: ../share/symbols/symbols.h:171 msgctxt "Symbol" msgid "Offline Storage" msgstr "" #. Symbols: ./FlowSymbols.svg -#: ../share/symbols/symbols.h:136 +#: ../share/symbols/symbols.h:172 msgctxt "Symbol" msgid "Logical Or" msgstr "" #. Symbols: ./FlowSymbols.svg -#: ../share/symbols/symbols.h:137 +#: ../share/symbols/symbols.h:173 msgctxt "Symbol" msgid "Logical And" msgstr "" #. Symbols: ./FlowSymbols.svg -#: ../share/symbols/symbols.h:138 +#: ../share/symbols/symbols.h:174 msgctxt "Symbol" msgid "Delay" msgstr "" #. Symbols: ./FlowSymbols.svg -#: ../share/symbols/symbols.h:139 +#: ../share/symbols/symbols.h:175 msgctxt "Symbol" msgid "Loop Limit Begin" msgstr "" #. Symbols: ./FlowSymbols.svg -#: ../share/symbols/symbols.h:140 +#: ../share/symbols/symbols.h:176 msgctxt "Symbol" msgid "Loop Limit End" msgstr "" -#. Symbols: ./BalloonSymbols.svg -#: ../share/symbols/symbols.h:141 -msgctxt "Symbol" -msgid "Word Balloons" -msgstr "" - -#. Symbols: ./BalloonSymbols.svg -#: ../share/symbols/symbols.h:142 -msgctxt "Symbol" -msgid "Thought Balloon" -msgstr "" - -#. Symbols: ./BalloonSymbols.svg -#: ../share/symbols/symbols.h:143 -msgctxt "Symbol" -msgid "Dream Speaking" -msgstr "" - -#. Symbols: ./BalloonSymbols.svg -#: ../share/symbols/symbols.h:144 -msgctxt "Symbol" -msgid "Rounded Balloon" -msgstr "" - -#. Symbols: ./BalloonSymbols.svg -#: ../share/symbols/symbols.h:145 -msgctxt "Symbol" -msgid "Squared Balloon" -msgstr "" - -#. Symbols: ./BalloonSymbols.svg -#: ../share/symbols/symbols.h:146 -msgctxt "Symbol" -msgid "Over the Phone" -msgstr "" - -#. Symbols: ./BalloonSymbols.svg -#: ../share/symbols/symbols.h:147 -msgctxt "Symbol" -msgid "Hip Balloon" -msgstr "" - -#. Symbols: ./BalloonSymbols.svg -#: ../share/symbols/symbols.h:148 -msgctxt "Symbol" -msgid "Circle Balloon" -msgstr "" - -#. Symbols: ./BalloonSymbols.svg -#: ../share/symbols/symbols.h:149 -msgctxt "Symbol" -msgid "Exclaim Balloon" -msgstr "" - #. Symbols: ./LogicSymbols.svg -#: ../share/symbols/symbols.h:150 +#: ../share/symbols/symbols.h:177 msgctxt "Symbol" msgid "Logic Symbols" msgstr "" #. Symbols: ./LogicSymbols.svg -#: ../share/symbols/symbols.h:151 +#: ../share/symbols/symbols.h:178 msgctxt "Symbol" msgid "Xnor Gate" msgstr "" #. Symbols: ./LogicSymbols.svg -#: ../share/symbols/symbols.h:152 +#: ../share/symbols/symbols.h:179 msgctxt "Symbol" msgid "Xor Gate" msgstr "" #. Symbols: ./LogicSymbols.svg -#: ../share/symbols/symbols.h:153 +#: ../share/symbols/symbols.h:180 msgctxt "Symbol" msgid "Nor Gate" msgstr "" #. Symbols: ./LogicSymbols.svg -#: ../share/symbols/symbols.h:154 +#: ../share/symbols/symbols.h:181 msgctxt "Symbol" msgid "Or Gate" msgstr "" #. Symbols: ./LogicSymbols.svg -#: ../share/symbols/symbols.h:155 +#: ../share/symbols/symbols.h:182 msgctxt "Symbol" msgid "Nand Gate" msgstr "" #. Symbols: ./LogicSymbols.svg -#: ../share/symbols/symbols.h:156 +#: ../share/symbols/symbols.h:183 msgctxt "Symbol" msgid "And Gate" msgstr "" #. Symbols: ./LogicSymbols.svg -#: ../share/symbols/symbols.h:157 +#: ../share/symbols/symbols.h:184 msgctxt "Symbol" msgid "Buffer" msgstr "" #. Symbols: ./LogicSymbols.svg -#: ../share/symbols/symbols.h:158 +#: ../share/symbols/symbols.h:185 msgctxt "Symbol" msgid "Not Gate" msgstr "" #. Symbols: ./LogicSymbols.svg -#: ../share/symbols/symbols.h:159 +#: ../share/symbols/symbols.h:186 msgctxt "Symbol" msgid "Buffer Small" msgstr "" #. Symbols: ./LogicSymbols.svg -#: ../share/symbols/symbols.h:160 +#: ../share/symbols/symbols.h:187 msgctxt "Symbol" msgid "Not Gate Small" msgstr "" -#. Symbols: ./AigaSymbols.svg -#: ../share/symbols/symbols.h:161 -msgctxt "Symbol" -msgid "AIGA Symbol Signs" -msgstr "" - -#. Symbols: ./AigaSymbols.svg -#: ../share/symbols/symbols.h:164 ../share/symbols/symbols.h:165 -msgctxt "Symbol" -msgid "Mail" -msgstr "" - -#. Symbols: ./AigaSymbols.svg -#: ../share/symbols/symbols.h:166 ../share/symbols/symbols.h:167 -msgctxt "Symbol" -msgid "Currency Exchange" -msgstr "" - -#. Symbols: ./AigaSymbols.svg -#: ../share/symbols/symbols.h:168 ../share/symbols/symbols.h:169 -msgctxt "Symbol" -msgid "Currency Exchange - Euro" -msgstr "" - -#. Symbols: ./AigaSymbols.svg -#: ../share/symbols/symbols.h:170 ../share/symbols/symbols.h:171 -msgctxt "Symbol" -msgid "Cashier" -msgstr "" - -#. Symbols: ./AigaSymbols.svg -#: ../share/symbols/symbols.h:174 ../share/symbols/symbols.h:175 -msgctxt "Symbol" -msgid "Lost and Found" -msgstr "" - -#. Symbols: ./AigaSymbols.svg -#: ../share/symbols/symbols.h:176 ../share/symbols/symbols.h:177 -msgctxt "Symbol" -msgid "Coat Check" -msgstr "" - -#. Symbols: ./AigaSymbols.svg -#: ../share/symbols/symbols.h:178 ../share/symbols/symbols.h:179 -msgctxt "Symbol" -msgid "Baggage Lockers" -msgstr "" - -#. Symbols: ./AigaSymbols.svg -#: ../share/symbols/symbols.h:180 ../share/symbols/symbols.h:181 -msgctxt "Symbol" -msgid "Escalator" -msgstr "" - -#. Symbols: ./AigaSymbols.svg -#: ../share/symbols/symbols.h:182 ../share/symbols/symbols.h:183 -msgctxt "Symbol" -msgid "Escalator Down" -msgstr "" - -#. Symbols: ./AigaSymbols.svg -#: ../share/symbols/symbols.h:184 ../share/symbols/symbols.h:185 -msgctxt "Symbol" -msgid "Escalator Up" -msgstr "" - -#. Symbols: ./AigaSymbols.svg -#: ../share/symbols/symbols.h:186 ../share/symbols/symbols.h:187 -msgctxt "Symbol" -msgid "Stairs" -msgstr "" - -#. Symbols: ./AigaSymbols.svg -#: ../share/symbols/symbols.h:188 ../share/symbols/symbols.h:189 -msgctxt "Symbol" -msgid "Stairs Down" -msgstr "" - -#. Symbols: ./AigaSymbols.svg -#: ../share/symbols/symbols.h:190 ../share/symbols/symbols.h:191 +#. Symbols: ./MapSymbolsNPS.svg +#: ../share/symbols/symbols.h:188 msgctxt "Symbol" -msgid "Stairs Up" +msgid "United States National Park Service Map Symbols" msgstr "" -#. Symbols: ./AigaSymbols.svg -#: ../share/symbols/symbols.h:192 ../share/symbols/symbols.h:193 +#. Symbols: ./MapSymbolsNPS.svg +#: ../share/symbols/symbols.h:189 ../share/symbols/symbols.h:190 msgctxt "Symbol" -msgid "Elevator" +msgid "Airport" msgstr "" -#. Symbols: ./AigaSymbols.svg -#: ../share/symbols/symbols.h:194 ../share/symbols/symbols.h:195 +#. Symbols: ./MapSymbolsNPS.svg +#: ../share/symbols/symbols.h:191 ../share/symbols/symbols.h:192 msgctxt "Symbol" -msgid "Toilets - Men" +msgid "Amphitheatre" msgstr "" -#. Symbols: ./AigaSymbols.svg -#: ../share/symbols/symbols.h:196 ../share/symbols/symbols.h:197 +#. Symbols: ./MapSymbolsNPS.svg +#: ../share/symbols/symbols.h:193 ../share/symbols/symbols.h:194 msgctxt "Symbol" -msgid "Toilets - Women" +msgid "Bicycle Trail" msgstr "" -#. Symbols: ./AigaSymbols.svg -#: ../share/symbols/symbols.h:198 ../share/symbols/symbols.h:199 +#. Symbols: ./MapSymbolsNPS.svg +#: ../share/symbols/symbols.h:195 ../share/symbols/symbols.h:196 msgctxt "Symbol" -msgid "Toilets" +msgid "Boat Launch" msgstr "" -#. Symbols: ./AigaSymbols.svg -#: ../share/symbols/symbols.h:200 ../share/symbols/symbols.h:201 +#. Symbols: ./MapSymbolsNPS.svg +#: ../share/symbols/symbols.h:197 ../share/symbols/symbols.h:198 msgctxt "Symbol" -msgid "Nursery" +msgid "Boat Tour" msgstr "" -#. Symbols: ./AigaSymbols.svg -#: ../share/symbols/symbols.h:202 ../share/symbols/symbols.h:203 +#. Symbols: ./MapSymbolsNPS.svg +#: ../share/symbols/symbols.h:199 ../share/symbols/symbols.h:200 msgctxt "Symbol" -msgid "Drinking Fountain" +msgid "Bus Stop" msgstr "" -#. Symbols: ./AigaSymbols.svg -#: ../share/symbols/symbols.h:204 ../share/symbols/symbols.h:205 +#. Symbols: ./MapSymbolsNPS.svg +#: ../share/symbols/symbols.h:201 ../share/symbols/symbols.h:202 msgctxt "Symbol" -msgid "Waiting Room" +msgid "Campfire" msgstr "" -#. Symbols: ./AigaSymbols.svg -#: ../share/symbols/symbols.h:208 ../share/symbols/symbols.h:209 +#. Symbols: ./MapSymbolsNPS.svg +#: ../share/symbols/symbols.h:203 ../share/symbols/symbols.h:204 msgctxt "Symbol" -msgid "Hotel Information" +msgid "Campground" msgstr "" -#. Symbols: ./AigaSymbols.svg -#: ../share/symbols/symbols.h:210 ../share/symbols/symbols.h:211 +#. Symbols: ./MapSymbolsNPS.svg +#: ../share/symbols/symbols.h:205 ../share/symbols/symbols.h:206 msgctxt "Symbol" -msgid "Air Transportation" +msgid "CanoeAccess" msgstr "" -#. Symbols: ./AigaSymbols.svg -#: ../share/symbols/symbols.h:212 ../share/symbols/symbols.h:213 +#. Symbols: ./MapSymbolsNPS.svg +#: ../share/symbols/symbols.h:207 ../share/symbols/symbols.h:208 msgctxt "Symbol" -msgid "Heliport" +msgid "Crosscountry Ski Trail" msgstr "" -#. Symbols: ./AigaSymbols.svg -#: ../share/symbols/symbols.h:214 ../share/symbols/symbols.h:215 +#. Symbols: ./MapSymbolsNPS.svg +#: ../share/symbols/symbols.h:209 ../share/symbols/symbols.h:210 msgctxt "Symbol" -msgid "Taxi" +msgid "Downhill Skiing" msgstr "" -#. Symbols: ./AigaSymbols.svg -#: ../share/symbols/symbols.h:216 ../share/symbols/symbols.h:217 +#. Symbols: ./MapSymbolsNPS.svg +#: ../share/symbols/symbols.h:211 ../share/symbols/symbols.h:212 msgctxt "Symbol" -msgid "Bus" +msgid "Drinking Water" msgstr "" -#. Symbols: ./AigaSymbols.svg -#: ../share/symbols/symbols.h:218 ../share/symbols/symbols.h:219 +#. Symbols: ./MapSymbolsNPS.svg +#: ../share/symbols/symbols.h:215 ../share/symbols/symbols.h:216 msgctxt "Symbol" -msgid "Ground Transportation" +msgid "Fishing" msgstr "" -#. Symbols: ./AigaSymbols.svg -#: ../share/symbols/symbols.h:220 ../share/symbols/symbols.h:221 +#. Symbols: ./MapSymbolsNPS.svg +#: ../share/symbols/symbols.h:217 ../share/symbols/symbols.h:218 msgctxt "Symbol" -msgid "Rail Transportation" +msgid "Food Service" msgstr "" -#. Symbols: ./AigaSymbols.svg -#: ../share/symbols/symbols.h:222 ../share/symbols/symbols.h:223 +#. Symbols: ./MapSymbolsNPS.svg +#: ../share/symbols/symbols.h:219 ../share/symbols/symbols.h:220 msgctxt "Symbol" -msgid "Water Transportation" +msgid "Four Wheel Drive Road" msgstr "" -#. Symbols: ./AigaSymbols.svg -#: ../share/symbols/symbols.h:224 ../share/symbols/symbols.h:225 +#. Symbols: ./MapSymbolsNPS.svg +#: ../share/symbols/symbols.h:221 ../share/symbols/symbols.h:222 msgctxt "Symbol" -msgid "Car Rental" +msgid "Gas Station" msgstr "" -#. Symbols: ./AigaSymbols.svg -#: ../share/symbols/symbols.h:226 ../share/symbols/symbols.h:227 +#. Symbols: ./MapSymbolsNPS.svg +#: ../share/symbols/symbols.h:223 ../share/symbols/symbols.h:224 msgctxt "Symbol" -msgid "Restaurant" +msgid "Golfing" msgstr "" -#. Symbols: ./AigaSymbols.svg -#: ../share/symbols/symbols.h:228 ../share/symbols/symbols.h:229 +#. Symbols: ./MapSymbolsNPS.svg +#: ../share/symbols/symbols.h:225 ../share/symbols/symbols.h:226 msgctxt "Symbol" -msgid "Coffeeshop" +msgid "Horseback Riding" msgstr "" -#. Symbols: ./AigaSymbols.svg -#: ../share/symbols/symbols.h:230 ../share/symbols/symbols.h:231 +#. Symbols: ./MapSymbolsNPS.svg +#: ../share/symbols/symbols.h:227 ../share/symbols/symbols.h:228 msgctxt "Symbol" -msgid "Bar" +msgid "Hospital" msgstr "" -#. Symbols: ./AigaSymbols.svg -#: ../share/symbols/symbols.h:232 ../share/symbols/symbols.h:233 +#. Symbols: ./MapSymbolsNPS.svg +#: ../share/symbols/symbols.h:229 ../share/symbols/symbols.h:230 msgctxt "Symbol" -msgid "Shops" +msgid "Ice Skating" msgstr "" -#. Symbols: ./AigaSymbols.svg -#: ../share/symbols/symbols.h:234 ../share/symbols/symbols.h:235 +#. Symbols: ./MapSymbolsNPS.svg +#: ../share/symbols/symbols.h:233 ../share/symbols/symbols.h:234 msgctxt "Symbol" -msgid "Barber Shop - Beauty Salon" +msgid "Litter Receptacle" msgstr "" -#. Symbols: ./AigaSymbols.svg -#: ../share/symbols/symbols.h:236 ../share/symbols/symbols.h:237 +#. Symbols: ./MapSymbolsNPS.svg +#: ../share/symbols/symbols.h:235 ../share/symbols/symbols.h:236 msgctxt "Symbol" -msgid "Barber Shop" +msgid "Lodging" msgstr "" -#. Symbols: ./AigaSymbols.svg -#: ../share/symbols/symbols.h:238 ../share/symbols/symbols.h:239 +#. Symbols: ./MapSymbolsNPS.svg +#: ../share/symbols/symbols.h:237 ../share/symbols/symbols.h:238 msgctxt "Symbol" -msgid "Beauty Salon" +msgid "Marina" msgstr "" -#. Symbols: ./AigaSymbols.svg -#: ../share/symbols/symbols.h:240 ../share/symbols/symbols.h:241 +#. Symbols: ./MapSymbolsNPS.svg +#: ../share/symbols/symbols.h:239 ../share/symbols/symbols.h:240 msgctxt "Symbol" -msgid "Ticket Purchase" +msgid "Motorbike Trail" msgstr "" -#. Symbols: ./AigaSymbols.svg -#: ../share/symbols/symbols.h:242 ../share/symbols/symbols.h:243 +#. Symbols: ./MapSymbolsNPS.svg +#: ../share/symbols/symbols.h:241 ../share/symbols/symbols.h:242 msgctxt "Symbol" -msgid "Baggage Check In" +msgid "Radiator Water" msgstr "" -#. Symbols: ./AigaSymbols.svg -#: ../share/symbols/symbols.h:244 ../share/symbols/symbols.h:245 +#. Symbols: ./MapSymbolsNPS.svg +#: ../share/symbols/symbols.h:243 ../share/symbols/symbols.h:244 msgctxt "Symbol" -msgid "Baggage Claim" +msgid "Recycling" msgstr "" -#. Symbols: ./AigaSymbols.svg -#: ../share/symbols/symbols.h:246 ../share/symbols/symbols.h:247 +#. Symbols: ./MapSymbolsNPS.svg +#: ../share/symbols/symbols.h:247 ../share/symbols/symbols.h:248 msgctxt "Symbol" -msgid "Customs" +msgid "Pets On Leash" msgstr "" -#. Symbols: ./AigaSymbols.svg -#: ../share/symbols/symbols.h:248 ../share/symbols/symbols.h:249 +#. Symbols: ./MapSymbolsNPS.svg +#: ../share/symbols/symbols.h:249 ../share/symbols/symbols.h:250 msgctxt "Symbol" -msgid "Immigration" +msgid "Picnic Area" msgstr "" -#. Symbols: ./AigaSymbols.svg -#: ../share/symbols/symbols.h:250 ../share/symbols/symbols.h:251 +#. Symbols: ./MapSymbolsNPS.svg +#: ../share/symbols/symbols.h:251 ../share/symbols/symbols.h:252 msgctxt "Symbol" -msgid "Departing Flights" +msgid "Post Office" msgstr "" -#. Symbols: ./AigaSymbols.svg -#: ../share/symbols/symbols.h:252 ../share/symbols/symbols.h:253 +#. Symbols: ./MapSymbolsNPS.svg +#: ../share/symbols/symbols.h:253 ../share/symbols/symbols.h:254 msgctxt "Symbol" -msgid "Arriving Flights" +msgid "Ranger Station" msgstr "" -#. Symbols: ./AigaSymbols.svg -#: ../share/symbols/symbols.h:254 ../share/symbols/symbols.h:255 +#. Symbols: ./MapSymbolsNPS.svg +#: ../share/symbols/symbols.h:255 ../share/symbols/symbols.h:256 msgctxt "Symbol" -msgid "Smoking" +msgid "RV Campground" msgstr "" -#. Symbols: ./AigaSymbols.svg -#: ../share/symbols/symbols.h:256 ../share/symbols/symbols.h:257 +#. Symbols: ./MapSymbolsNPS.svg +#: ../share/symbols/symbols.h:257 ../share/symbols/symbols.h:258 msgctxt "Symbol" -msgid "No Smoking" +msgid "Restrooms" msgstr "" -#. Symbols: ./AigaSymbols.svg -#: ../share/symbols/symbols.h:260 ../share/symbols/symbols.h:261 +#. Symbols: ./MapSymbolsNPS.svg +#: ../share/symbols/symbols.h:259 ../share/symbols/symbols.h:260 msgctxt "Symbol" -msgid "No Parking" +msgid "Sailing" msgstr "" -#. Symbols: ./AigaSymbols.svg -#: ../share/symbols/symbols.h:262 ../share/symbols/symbols.h:263 +#. Symbols: ./MapSymbolsNPS.svg +#: ../share/symbols/symbols.h:261 ../share/symbols/symbols.h:262 msgctxt "Symbol" -msgid "No Dogs" +msgid "Sanitary Disposal Station" msgstr "" -#. Symbols: ./AigaSymbols.svg -#: ../share/symbols/symbols.h:264 ../share/symbols/symbols.h:265 +#. Symbols: ./MapSymbolsNPS.svg +#: ../share/symbols/symbols.h:263 ../share/symbols/symbols.h:264 msgctxt "Symbol" -msgid "No Entry" +msgid "Scuba Diving" msgstr "" -#. Symbols: ./AigaSymbols.svg -#: ../share/symbols/symbols.h:266 ../share/symbols/symbols.h:267 +#. Symbols: ./MapSymbolsNPS.svg +#: ../share/symbols/symbols.h:265 ../share/symbols/symbols.h:266 msgctxt "Symbol" -msgid "Exit" +msgid "Self Guided Trail" msgstr "" -#. Symbols: ./AigaSymbols.svg -#: ../share/symbols/symbols.h:268 ../share/symbols/symbols.h:269 +#. Symbols: ./MapSymbolsNPS.svg +#: ../share/symbols/symbols.h:267 ../share/symbols/symbols.h:268 msgctxt "Symbol" -msgid "Fire Extinguisher" +msgid "Shelter" msgstr "" -#. Symbols: ./AigaSymbols.svg -#: ../share/symbols/symbols.h:270 ../share/symbols/symbols.h:271 +#. Symbols: ./MapSymbolsNPS.svg +#: ../share/symbols/symbols.h:269 ../share/symbols/symbols.h:270 msgctxt "Symbol" -msgid "Right Arrow" +msgid "Showers" msgstr "" -#. Symbols: ./AigaSymbols.svg -#: ../share/symbols/symbols.h:272 ../share/symbols/symbols.h:273 +#. Symbols: ./MapSymbolsNPS.svg +#: ../share/symbols/symbols.h:271 ../share/symbols/symbols.h:272 msgctxt "Symbol" -msgid "Forward and Right Arrow" +msgid "Sledding" msgstr "" -#. Symbols: ./AigaSymbols.svg -#: ../share/symbols/symbols.h:274 ../share/symbols/symbols.h:275 +#. Symbols: ./MapSymbolsNPS.svg +#: ../share/symbols/symbols.h:273 ../share/symbols/symbols.h:274 msgctxt "Symbol" -msgid "Up Arrow" +msgid "SnowmobileTrail" msgstr "" -#. Symbols: ./AigaSymbols.svg -#: ../share/symbols/symbols.h:276 ../share/symbols/symbols.h:277 +#. Symbols: ./MapSymbolsNPS.svg +#: ../share/symbols/symbols.h:275 ../share/symbols/symbols.h:276 msgctxt "Symbol" -msgid "Forward and Left Arrow" +msgid "Stable" msgstr "" -#. Symbols: ./AigaSymbols.svg -#: ../share/symbols/symbols.h:278 ../share/symbols/symbols.h:279 +#. Symbols: ./MapSymbolsNPS.svg +#: ../share/symbols/symbols.h:277 ../share/symbols/symbols.h:278 msgctxt "Symbol" -msgid "Left Arrow" +msgid "Store" msgstr "" -#. Symbols: ./AigaSymbols.svg -#: ../share/symbols/symbols.h:280 ../share/symbols/symbols.h:281 +#. Symbols: ./MapSymbolsNPS.svg +#: ../share/symbols/symbols.h:279 ../share/symbols/symbols.h:280 msgctxt "Symbol" -msgid "Left and Down Arrow" +msgid "Swimming" msgstr "" -#. Symbols: ./AigaSymbols.svg -#: ../share/symbols/symbols.h:282 ../share/symbols/symbols.h:283 +#. Symbols: ./MapSymbolsNPS.svg +#: ../share/symbols/symbols.h:283 ../share/symbols/symbols.h:284 msgctxt "Symbol" -msgid "Down Arrow" +msgid "Emergency Telephone" msgstr "" -#. Symbols: ./AigaSymbols.svg -#: ../share/symbols/symbols.h:284 ../share/symbols/symbols.h:285 +#. Symbols: ./MapSymbolsNPS.svg +#: ../share/symbols/symbols.h:285 ../share/symbols/symbols.h:286 msgctxt "Symbol" -msgid "Right and Down Arrow" +msgid "Trailhead" msgstr "" -#. Symbols: ./AigaSymbols.svg -#: ../share/symbols/symbols.h:286 ../share/symbols/symbols.h:287 +#. Symbols: ./MapSymbolsNPS.svg +#: ../share/symbols/symbols.h:287 ../share/symbols/symbols.h:288 msgctxt "Symbol" -msgid "NPS Wheelchair Accessible - 1996" +msgid "Wheelchair Accessible" msgstr "" -#. Symbols: ./AigaSymbols.svg -#: ../share/symbols/symbols.h:288 ../share/symbols/symbols.h:289 +#. Symbols: ./MapSymbolsNPS.svg +#: ../share/symbols/symbols.h:289 ../share/symbols/symbols.h:290 msgctxt "Symbol" -msgid "NPS Wheelchair Accessible" +msgid "Wind Surfing" msgstr "" -#. Symbols: ./AigaSymbols.svg -#: ../share/symbols/symbols.h:290 ../share/symbols/symbols.h:291 +#. Symbols: ./MapSymbolsNPS.svg +#: ../share/symbols/symbols.h:291 msgctxt "Symbol" -msgid "New Wheelchair Accessible" +msgid "Blank" msgstr "" #: ../share/templates/templates.h:1 @@ -4737,11 +4737,11 @@ msgstr "" msgid "Bounding box side midpoint" msgstr "" -#: ../src/display/snap-indicator.cpp:196 ../src/ui/tool/node.cpp:1506 +#: ../src/display/snap-indicator.cpp:196 ../src/ui/tool/node.cpp:1507 msgid "Smooth node" msgstr "" -#: ../src/display/snap-indicator.cpp:199 ../src/ui/tool/node.cpp:1505 +#: ../src/display/snap-indicator.cpp:199 ../src/ui/tool/node.cpp:1506 msgid "Cusp node" msgstr "" @@ -4793,7 +4793,7 @@ msgstr "" msgid "Multiple of grid spacing" msgstr "" -#: ../src/display/snap-indicator.cpp:268 +#: ../src/display/snap-indicator.cpp:281 msgid " to " msgstr "" @@ -4983,7 +4983,7 @@ msgstr "" #: ../src/extension/internal/bitmap/raise.cpp:42 #: ../src/extension/internal/bitmap/sample.cpp:41 #: ../src/extension/internal/bluredge.cpp:136 -#: ../src/ui/dialog/lpe-powerstroke-properties.cpp:59 +#: ../src/ui/dialog/lpe-powerstroke-properties.cpp:66 #: ../src/ui/dialog/object-attributes.cpp:68 #: ../src/ui/dialog/object-attributes.cpp:77 #: ../src/ui/widget/page-sizer.cpp:249 @@ -5433,7 +5433,7 @@ msgstr "" #: ../src/extension/internal/bitmap/opacity.cpp:40 #: ../src/ui/dialog/filter-effects-dialog.cpp:2900 -#: ../src/ui/dialog/objects.cpp:1619 ../src/widgets/dropper-toolbar.cpp:83 +#: ../src/ui/dialog/objects.cpp:1625 ../src/widgets/dropper-toolbar.cpp:83 msgid "Opacity:" msgstr "" @@ -5724,14 +5724,14 @@ msgid "Output page size:" msgstr "" #: ../src/extension/internal/cdr-input.cpp:116 -#: ../src/extension/internal/pdfinput/pdf-input.cpp:87 +#: ../src/extension/internal/pdfinput/pdf-input.cpp:92 #: ../src/extension/internal/vsd-input.cpp:116 msgid "Select page:" msgstr "" #. Display total number of pages #: ../src/extension/internal/cdr-input.cpp:128 -#: ../src/extension/internal/pdfinput/pdf-input.cpp:106 +#: ../src/extension/internal/pdfinput/pdf-input.cpp:111 #: ../src/extension/internal/vsd-input.cpp:128 #, c-format msgid "out of %i" @@ -5807,42 +5807,42 @@ msgid "EMF Output" msgstr "" #: ../src/extension/internal/emf-inout.cpp:3600 -#: ../src/extension/internal/wmf-inout.cpp:3174 +#: ../src/extension/internal/wmf-inout.cpp:3176 msgid "Convert texts to paths" msgstr "" #: ../src/extension/internal/emf-inout.cpp:3601 -#: ../src/extension/internal/wmf-inout.cpp:3175 +#: ../src/extension/internal/wmf-inout.cpp:3177 msgid "Map Unicode to Symbol font" msgstr "" #: ../src/extension/internal/emf-inout.cpp:3602 -#: ../src/extension/internal/wmf-inout.cpp:3176 +#: ../src/extension/internal/wmf-inout.cpp:3178 msgid "Map Unicode to Wingdings" msgstr "" #: ../src/extension/internal/emf-inout.cpp:3603 -#: ../src/extension/internal/wmf-inout.cpp:3177 +#: ../src/extension/internal/wmf-inout.cpp:3179 msgid "Map Unicode to Zapf Dingbats" msgstr "" #: ../src/extension/internal/emf-inout.cpp:3604 -#: ../src/extension/internal/wmf-inout.cpp:3178 +#: ../src/extension/internal/wmf-inout.cpp:3180 msgid "Use MS Unicode PUA (0xF020-0xF0FF) for converted characters" msgstr "" #: ../src/extension/internal/emf-inout.cpp:3605 -#: ../src/extension/internal/wmf-inout.cpp:3179 +#: ../src/extension/internal/wmf-inout.cpp:3181 msgid "Compensate for PPT font bug" msgstr "" #: ../src/extension/internal/emf-inout.cpp:3606 -#: ../src/extension/internal/wmf-inout.cpp:3180 +#: ../src/extension/internal/wmf-inout.cpp:3182 msgid "Convert dashed/dotted lines to single lines" msgstr "" #: ../src/extension/internal/emf-inout.cpp:3607 -#: ../src/extension/internal/wmf-inout.cpp:3181 +#: ../src/extension/internal/wmf-inout.cpp:3183 msgid "Convert gradients to colored polygon series" msgstr "" @@ -6582,7 +6582,7 @@ msgstr "" #: ../src/extension/internal/filter/color.h:505 ../src/filter-enums.cpp:113 #: ../src/live_effects/lpe-interpolate_points.cpp:25 -#: ../src/live_effects/lpe-powerstroke.cpp:194 +#: ../src/live_effects/lpe-powerstroke.cpp:130 msgid "Linear" msgstr "" @@ -6764,7 +6764,7 @@ msgstr "" #: ../src/extension/internal/filter/color.h:1119 #: ../src/extension/internal/filter/paint.h:356 ../src/filter-enums.cpp:33 -#: ../src/live_effects/effect.cpp:110 +#: ../src/live_effects/effect.cpp:107 #: ../src/ui/dialog/filter-effects-dialog.cpp:1048 #: ../src/widgets/gradient-toolbar.cpp:1162 msgid "Offset" @@ -7014,7 +7014,7 @@ msgid "Blur and displace edges of shapes and pictures" msgstr "" #: ../src/extension/internal/filter/distort.h:190 -#: ../src/live_effects/effect.cpp:140 +#: ../src/live_effects/effect.cpp:137 msgid "Roughen" msgstr "" @@ -7138,7 +7138,7 @@ msgstr "" #: ../src/extension/internal/filter/morphology.h:179 #: ../src/ui/dialog/layer-properties.cpp:185 -#: ../src/ui/dialog/lpe-powerstroke-properties.cpp:55 +#: ../src/ui/dialog/lpe-powerstroke-properties.cpp:59 msgid "Position:" msgstr "" @@ -7335,7 +7335,7 @@ msgstr "" #: ../src/extension/internal/filter/paint.h:496 #: ../src/extension/internal/filter/paint.h:590 #: ../src/extension/internal/filter/paint.h:976 -#: ../src/live_effects/effect.cpp:151 ../src/splivarot.cpp:2201 +#: ../src/live_effects/effect.cpp:148 ../src/splivarot.cpp:2204 msgid "Simplify" msgstr "" @@ -7834,15 +7834,15 @@ msgstr "" msgid "Draw a path which is a grid" msgstr "" -#: ../src/extension/internal/javafx-out.cpp:966 +#: ../src/extension/internal/javafx-out.cpp:963 msgid "JavaFX Output" msgstr "" -#: ../src/extension/internal/javafx-out.cpp:971 +#: ../src/extension/internal/javafx-out.cpp:968 msgid "JavaFX (*.fx)" msgstr "" -#: ../src/extension/internal/javafx-out.cpp:972 +#: ../src/extension/internal/javafx-out.cpp:969 msgid "JavaFX Raytracer File" msgstr "" @@ -7858,150 +7858,165 @@ msgstr "" msgid "LaTeX PSTricks File" msgstr "" -#: ../src/extension/internal/latex-pstricks.cpp:331 +#: ../src/extension/internal/latex-pstricks.cpp:330 msgid "LaTeX Print" msgstr "" -#: ../src/extension/internal/odf.cpp:2142 +#: ../src/extension/internal/odf.cpp:2141 msgid "OpenDocument Drawing Output" msgstr "" -#: ../src/extension/internal/odf.cpp:2147 +#: ../src/extension/internal/odf.cpp:2146 msgid "OpenDocument drawing (*.odg)" msgstr "" -#: ../src/extension/internal/odf.cpp:2148 +#: ../src/extension/internal/odf.cpp:2147 msgid "OpenDocument drawing file" msgstr "" #. TRANSLATORS: The following are document crop settings for PDF import #. more info: http://www.acrobatusers.com/tech_corners/javascript_corner/tips/2006/page_bounds/ -#: ../src/extension/internal/pdfinput/pdf-input.cpp:71 +#: ../src/extension/internal/pdfinput/pdf-input.cpp:77 msgid "media box" msgstr "" -#: ../src/extension/internal/pdfinput/pdf-input.cpp:72 +#: ../src/extension/internal/pdfinput/pdf-input.cpp:78 msgid "crop box" msgstr "" -#: ../src/extension/internal/pdfinput/pdf-input.cpp:73 +#: ../src/extension/internal/pdfinput/pdf-input.cpp:79 msgid "trim box" msgstr "" -#: ../src/extension/internal/pdfinput/pdf-input.cpp:74 +#: ../src/extension/internal/pdfinput/pdf-input.cpp:80 msgid "bleed box" msgstr "" -#: ../src/extension/internal/pdfinput/pdf-input.cpp:75 +#: ../src/extension/internal/pdfinput/pdf-input.cpp:81 msgid "art box" msgstr "" #. Crop settings -#: ../src/extension/internal/pdfinput/pdf-input.cpp:112 +#: ../src/extension/internal/pdfinput/pdf-input.cpp:117 msgid "Clip to:" msgstr "" -#: ../src/extension/internal/pdfinput/pdf-input.cpp:123 +#: ../src/extension/internal/pdfinput/pdf-input.cpp:128 msgid "Page settings" msgstr "" -#: ../src/extension/internal/pdfinput/pdf-input.cpp:124 +#: ../src/extension/internal/pdfinput/pdf-input.cpp:129 msgid "Precision of approximating gradient meshes:" msgstr "" -#: ../src/extension/internal/pdfinput/pdf-input.cpp:125 +#: ../src/extension/internal/pdfinput/pdf-input.cpp:130 msgid "" "Note: setting the precision too high may result in a large SVG file " "and slow performance." msgstr "" -#: ../src/extension/internal/pdfinput/pdf-input.cpp:128 -msgid "import via Poppler" +#: ../src/extension/internal/pdfinput/pdf-input.cpp:134 +msgid "Poppler/Cairo import" msgstr "" -#: ../src/extension/internal/pdfinput/pdf-input.cpp:138 -msgid "rough" +#: ../src/extension/internal/pdfinput/pdf-input.cpp:135 +msgid "" +"Import via external library. Text consists of groups containing cloned " +"glyphs where each glyph is a path. Images are stored internally. Meshes " +"cause entire document to be rendered as a raster image." msgstr "" -#. Text options -#: ../src/extension/internal/pdfinput/pdf-input.cpp:142 -msgid "Text handling:" +#: ../src/extension/internal/pdfinput/pdf-input.cpp:136 +msgid "Internal import" msgstr "" -#: ../src/extension/internal/pdfinput/pdf-input.cpp:144 -#: ../src/extension/internal/pdfinput/pdf-input.cpp:145 -msgid "Import text as text" +#: ../src/extension/internal/pdfinput/pdf-input.cpp:137 +msgid "" +"Import via internal (Poppler derived) library. Text is stored as text but " +"white space is missing. Meshes are converted to tiles, the number depends on " +"the precision set below." +msgstr "" + +#: ../src/extension/internal/pdfinput/pdf-input.cpp:148 +msgid "rough" msgstr "" -#: ../src/extension/internal/pdfinput/pdf-input.cpp:146 +#. Text options +#. _labelText = Gtk::manage(new class Gtk::Label(_("Text handling:"))); +#. _textHandlingCombo = Gtk::manage(new class Gtk::ComboBoxText()); +#. _textHandlingCombo->append(_("Import text as text")); +#. _textHandlingCombo->set_active_text(_("Import text as text")); +#. hbox5 = Gtk::manage(new class Gtk::HBox(false, 4)); +#. Font option +#: ../src/extension/internal/pdfinput/pdf-input.cpp:159 msgid "Replace PDF fonts by closest-named installed fonts" msgstr "" -#: ../src/extension/internal/pdfinput/pdf-input.cpp:149 +#: ../src/extension/internal/pdfinput/pdf-input.cpp:161 msgid "Embed images" msgstr "" -#: ../src/extension/internal/pdfinput/pdf-input.cpp:151 +#: ../src/extension/internal/pdfinput/pdf-input.cpp:163 msgid "Import settings" msgstr "" -#: ../src/extension/internal/pdfinput/pdf-input.cpp:268 +#: ../src/extension/internal/pdfinput/pdf-input.cpp:290 msgid "PDF Import Settings" msgstr "" -#: ../src/extension/internal/pdfinput/pdf-input.cpp:431 +#: ../src/extension/internal/pdfinput/pdf-input.cpp:437 msgctxt "PDF input precision" msgid "rough" msgstr "" -#: ../src/extension/internal/pdfinput/pdf-input.cpp:432 +#: ../src/extension/internal/pdfinput/pdf-input.cpp:438 msgctxt "PDF input precision" msgid "medium" msgstr "" -#: ../src/extension/internal/pdfinput/pdf-input.cpp:433 +#: ../src/extension/internal/pdfinput/pdf-input.cpp:439 msgctxt "PDF input precision" msgid "fine" msgstr "" -#: ../src/extension/internal/pdfinput/pdf-input.cpp:434 +#: ../src/extension/internal/pdfinput/pdf-input.cpp:440 msgctxt "PDF input precision" msgid "very fine" msgstr "" -#: ../src/extension/internal/pdfinput/pdf-input.cpp:901 +#: ../src/extension/internal/pdfinput/pdf-input.cpp:936 msgid "PDF Input" msgstr "" -#: ../src/extension/internal/pdfinput/pdf-input.cpp:906 +#: ../src/extension/internal/pdfinput/pdf-input.cpp:941 msgid "Adobe PDF (*.pdf)" msgstr "" -#: ../src/extension/internal/pdfinput/pdf-input.cpp:907 +#: ../src/extension/internal/pdfinput/pdf-input.cpp:942 msgid "Adobe Portable Document Format" msgstr "" -#: ../src/extension/internal/pdfinput/pdf-input.cpp:914 +#: ../src/extension/internal/pdfinput/pdf-input.cpp:949 msgid "AI Input" msgstr "" -#: ../src/extension/internal/pdfinput/pdf-input.cpp:919 +#: ../src/extension/internal/pdfinput/pdf-input.cpp:954 msgid "Adobe Illustrator 9.0 and above (*.ai)" msgstr "" -#: ../src/extension/internal/pdfinput/pdf-input.cpp:920 +#: ../src/extension/internal/pdfinput/pdf-input.cpp:955 msgid "Open files saved in Adobe Illustrator 9.0 and newer versions" msgstr "" -#: ../src/extension/internal/pov-out.cpp:715 +#: ../src/extension/internal/pov-out.cpp:714 msgid "PovRay Output" msgstr "" -#: ../src/extension/internal/pov-out.cpp:720 +#: ../src/extension/internal/pov-out.cpp:719 msgid "PovRay (*.pov) (paths and shapes only)" msgstr "" -#: ../src/extension/internal/pov-out.cpp:721 +#: ../src/extension/internal/pov-out.cpp:720 msgid "PovRay Raytracer File" msgstr "" @@ -8114,33 +8129,33 @@ msgstr "" msgid "Microsoft Visio 2013 drawing (*.vsdx)" msgstr "" -#: ../src/extension/internal/wmf-inout.cpp:3158 +#: ../src/extension/internal/wmf-inout.cpp:3160 msgid "WMF Input" msgstr "" -#: ../src/extension/internal/wmf-inout.cpp:3163 +#: ../src/extension/internal/wmf-inout.cpp:3165 msgid "Windows Metafiles (*.wmf)" msgstr "" -#: ../src/extension/internal/wmf-inout.cpp:3164 +#: ../src/extension/internal/wmf-inout.cpp:3166 msgid "Windows Metafiles" msgstr "" -#: ../src/extension/internal/wmf-inout.cpp:3172 +#: ../src/extension/internal/wmf-inout.cpp:3174 msgid "WMF Output" msgstr "" -#: ../src/extension/internal/wmf-inout.cpp:3182 +#: ../src/extension/internal/wmf-inout.cpp:3184 msgid "Map all fill patterns to standard WMF hatches" msgstr "" -#: ../src/extension/internal/wmf-inout.cpp:3186 +#: ../src/extension/internal/wmf-inout.cpp:3188 #: ../share/extensions/wmf_input.inx.h:2 #: ../share/extensions/wmf_output.inx.h:2 msgid "Windows Metafile (*.wmf)" msgstr "" -#: ../src/extension/internal/wmf-inout.cpp:3187 +#: ../src/extension/internal/wmf-inout.cpp:3189 msgid "Windows Metafile" msgstr "" @@ -8366,11 +8381,11 @@ msgstr "" msgid "Soft Light" msgstr "" -#: ../src/filter-enums.cpp:63 ../src/splivarot.cpp:88 ../src/splivarot.cpp:94 +#: ../src/filter-enums.cpp:63 ../src/splivarot.cpp:89 ../src/splivarot.cpp:95 msgid "Difference" msgstr "" -#: ../src/filter-enums.cpp:64 ../src/splivarot.cpp:100 +#: ../src/filter-enums.cpp:64 ../src/splivarot.cpp:101 msgid "Exclusion" msgstr "" @@ -8447,6 +8462,7 @@ msgid "Arithmetic" msgstr "" #: ../src/filter-enums.cpp:120 ../src/selection-chemistry.cpp:545 +#: ../src/ui/dialog/objects.cpp:1889 msgid "Duplicate" msgstr "" @@ -9050,227 +9066,222 @@ msgstr "" msgid "Dock #%d" msgstr "" -#: ../src/libnrtype/FontFactory.cpp:618 +#: ../src/libnrtype/FontFactory.cpp:636 msgid "Ignoring font without family that will crash Pango" msgstr "" -#: ../src/live_effects/effect.cpp:99 +#: ../src/live_effects/effect.cpp:98 msgid "doEffect stack test" msgstr "" -#: ../src/live_effects/effect.cpp:100 +#: ../src/live_effects/effect.cpp:99 msgid "Angle bisector" msgstr "" -#. TRANSLATORS: boolean operations -#: ../src/live_effects/effect.cpp:102 -msgid "Boolops" -msgstr "" - -#: ../src/live_effects/effect.cpp:103 +#: ../src/live_effects/effect.cpp:100 msgid "Circle (by center and radius)" msgstr "" -#: ../src/live_effects/effect.cpp:104 +#: ../src/live_effects/effect.cpp:101 msgid "Circle by 3 points" msgstr "" -#: ../src/live_effects/effect.cpp:105 +#: ../src/live_effects/effect.cpp:102 msgid "Dynamic stroke" msgstr "" -#: ../src/live_effects/effect.cpp:106 ../share/extensions/extrude.inx.h:1 +#: ../src/live_effects/effect.cpp:103 ../share/extensions/extrude.inx.h:1 msgid "Extrude" msgstr "" -#: ../src/live_effects/effect.cpp:107 +#: ../src/live_effects/effect.cpp:104 msgid "Lattice Deformation" msgstr "" -#: ../src/live_effects/effect.cpp:108 +#: ../src/live_effects/effect.cpp:105 msgid "Line Segment" msgstr "" -#: ../src/live_effects/effect.cpp:109 +#: ../src/live_effects/effect.cpp:106 msgid "Mirror symmetry" msgstr "" -#: ../src/live_effects/effect.cpp:111 +#: ../src/live_effects/effect.cpp:108 msgid "Parallel" msgstr "" -#: ../src/live_effects/effect.cpp:112 +#: ../src/live_effects/effect.cpp:109 msgid "Path length" msgstr "" -#: ../src/live_effects/effect.cpp:113 +#: ../src/live_effects/effect.cpp:110 msgid "Perpendicular bisector" msgstr "" -#: ../src/live_effects/effect.cpp:114 +#: ../src/live_effects/effect.cpp:111 msgid "Perspective path" msgstr "" -#: ../src/live_effects/effect.cpp:115 +#: ../src/live_effects/effect.cpp:112 msgid "Rotate copies" msgstr "" -#: ../src/live_effects/effect.cpp:116 +#: ../src/live_effects/effect.cpp:113 msgid "Recursive skeleton" msgstr "" -#: ../src/live_effects/effect.cpp:117 +#: ../src/live_effects/effect.cpp:114 msgid "Tangent to curve" msgstr "" -#: ../src/live_effects/effect.cpp:118 +#: ../src/live_effects/effect.cpp:115 msgid "Text label" msgstr "" #. 0.46 -#: ../src/live_effects/effect.cpp:121 +#: ../src/live_effects/effect.cpp:118 msgid "Bend" msgstr "" -#: ../src/live_effects/effect.cpp:122 +#: ../src/live_effects/effect.cpp:119 msgid "Gears" msgstr "" -#: ../src/live_effects/effect.cpp:123 +#: ../src/live_effects/effect.cpp:120 msgid "Pattern Along Path" msgstr "" #. for historic reasons, this effect is called skeletal(strokes) in Inkscape:SVG -#: ../src/live_effects/effect.cpp:124 +#: ../src/live_effects/effect.cpp:121 msgid "Stitch Sub-Paths" msgstr "" #. 0.47 -#: ../src/live_effects/effect.cpp:126 +#: ../src/live_effects/effect.cpp:123 msgid "VonKoch" msgstr "" -#: ../src/live_effects/effect.cpp:127 +#: ../src/live_effects/effect.cpp:124 msgid "Knot" msgstr "" -#: ../src/live_effects/effect.cpp:128 +#: ../src/live_effects/effect.cpp:125 msgid "Construct grid" msgstr "" -#: ../src/live_effects/effect.cpp:129 +#: ../src/live_effects/effect.cpp:126 msgid "Spiro spline" msgstr "" -#: ../src/live_effects/effect.cpp:130 +#: ../src/live_effects/effect.cpp:127 msgid "Envelope Deformation" msgstr "" -#: ../src/live_effects/effect.cpp:131 +#: ../src/live_effects/effect.cpp:128 msgid "Interpolate Sub-Paths" msgstr "" -#: ../src/live_effects/effect.cpp:132 +#: ../src/live_effects/effect.cpp:129 msgid "Hatches (rough)" msgstr "" -#: ../src/live_effects/effect.cpp:133 +#: ../src/live_effects/effect.cpp:130 msgid "Sketch" msgstr "" -#: ../src/live_effects/effect.cpp:134 +#: ../src/live_effects/effect.cpp:131 msgid "Ruler" msgstr "" #. 0.91 -#: ../src/live_effects/effect.cpp:136 +#: ../src/live_effects/effect.cpp:133 msgid "Power stroke" msgstr "" -#: ../src/live_effects/effect.cpp:137 +#: ../src/live_effects/effect.cpp:134 msgid "Clone original path" msgstr "" #. EXPERIMENTAL -#: ../src/live_effects/effect.cpp:139 +#: ../src/live_effects/effect.cpp:136 #: ../src/live_effects/lpe-show_handles.cpp:26 msgid "Show handles" msgstr "" -#: ../src/live_effects/effect.cpp:141 ../src/widgets/pencil-toolbar.cpp:109 +#: ../src/live_effects/effect.cpp:138 ../src/widgets/pencil-toolbar.cpp:109 msgid "BSpline" msgstr "" -#: ../src/live_effects/effect.cpp:142 +#: ../src/live_effects/effect.cpp:139 msgid "Join type" msgstr "" -#: ../src/live_effects/effect.cpp:143 +#: ../src/live_effects/effect.cpp:140 msgid "Taper stroke" msgstr "" #. Ponyscape -#: ../src/live_effects/effect.cpp:145 +#: ../src/live_effects/effect.cpp:142 msgid "Attach path" msgstr "" -#: ../src/live_effects/effect.cpp:146 +#: ../src/live_effects/effect.cpp:143 msgid "Fill between strokes" msgstr "" -#: ../src/live_effects/effect.cpp:147 ../src/selection-chemistry.cpp:2871 +#: ../src/live_effects/effect.cpp:144 ../src/selection-chemistry.cpp:2871 msgid "Fill between many" msgstr "" -#: ../src/live_effects/effect.cpp:148 +#: ../src/live_effects/effect.cpp:145 msgid "Ellipse by 5 points" msgstr "" -#: ../src/live_effects/effect.cpp:149 +#: ../src/live_effects/effect.cpp:146 msgid "Bounding Box" msgstr "" -#: ../src/live_effects/effect.cpp:152 +#: ../src/live_effects/effect.cpp:149 msgid "Lattice Deformation 2" msgstr "" -#: ../src/live_effects/effect.cpp:153 +#: ../src/live_effects/effect.cpp:150 msgid "Perspective/Envelope" msgstr "" -#: ../src/live_effects/effect.cpp:154 +#: ../src/live_effects/effect.cpp:151 msgid "Fillet/Chamfer" msgstr "" -#: ../src/live_effects/effect.cpp:155 +#: ../src/live_effects/effect.cpp:152 msgid "Interpolate points" msgstr "" -#: ../src/live_effects/effect.cpp:362 +#: ../src/live_effects/effect.cpp:356 msgid "Is visible?" msgstr "" -#: ../src/live_effects/effect.cpp:362 +#: ../src/live_effects/effect.cpp:356 msgid "" "If unchecked, the effect remains applied to the object but is temporarily " "disabled on canvas" msgstr "" -#: ../src/live_effects/effect.cpp:387 +#: ../src/live_effects/effect.cpp:381 msgid "No effect" msgstr "" -#: ../src/live_effects/effect.cpp:495 +#: ../src/live_effects/effect.cpp:489 #, c-format msgid "Please specify a parameter path for the LPE '%s' with %d mouse clicks" msgstr "" -#: ../src/live_effects/effect.cpp:762 +#: ../src/live_effects/effect.cpp:756 #, c-format msgid "Editing parameter %s." msgstr "" -#: ../src/live_effects/effect.cpp:767 +#: ../src/live_effects/effect.cpp:761 msgid "None of the applied path effect's parameters can be edited on-canvas." msgstr "" @@ -9391,50 +9402,50 @@ msgstr "" msgid "Uses the visual bounding box" msgstr "" -#: ../src/live_effects/lpe-bspline.cpp:25 +#: ../src/live_effects/lpe-bspline.cpp:26 msgid "Steps with CTRL:" msgstr "" -#: ../src/live_effects/lpe-bspline.cpp:25 +#: ../src/live_effects/lpe-bspline.cpp:26 msgid "Change number of steps with CTRL pressed" msgstr "" -#: ../src/live_effects/lpe-bspline.cpp:26 +#: ../src/live_effects/lpe-bspline.cpp:27 #: ../src/live_effects/lpe-simplify.cpp:33 msgid "Helper size:" msgstr "" -#: ../src/live_effects/lpe-bspline.cpp:26 +#: ../src/live_effects/lpe-bspline.cpp:27 #: ../src/live_effects/lpe-simplify.cpp:33 msgid "Helper size" msgstr "" -#: ../src/live_effects/lpe-bspline.cpp:27 +#: ../src/live_effects/lpe-bspline.cpp:28 msgid "Ignore cusp nodes" msgstr "" -#: ../src/live_effects/lpe-bspline.cpp:27 +#: ../src/live_effects/lpe-bspline.cpp:28 msgid "Change ignoring cusp nodes" msgstr "" -#: ../src/live_effects/lpe-bspline.cpp:28 +#: ../src/live_effects/lpe-bspline.cpp:29 #: ../src/live_effects/lpe-fillet-chamfer.cpp:56 msgid "Change only selected nodes" msgstr "" -#: ../src/live_effects/lpe-bspline.cpp:29 +#: ../src/live_effects/lpe-bspline.cpp:30 msgid "Change weight:" msgstr "" -#: ../src/live_effects/lpe-bspline.cpp:29 +#: ../src/live_effects/lpe-bspline.cpp:30 msgid "Change weight of the effect" msgstr "" -#: ../src/live_effects/lpe-bspline.cpp:260 +#: ../src/live_effects/lpe-bspline.cpp:263 msgid "Default weight" msgstr "" -#: ../src/live_effects/lpe-bspline.cpp:265 +#: ../src/live_effects/lpe-bspline.cpp:268 msgid "Make cusp" msgstr "" @@ -9676,22 +9687,22 @@ msgid "Helper size with direction" msgstr "" #: ../src/live_effects/lpe-fillet-chamfer.cpp:154 -#: ../src/ui/dialog/lpe-fillet-chamfer-properties.cpp:71 +#: ../src/ui/dialog/lpe-fillet-chamfer-properties.cpp:72 msgid "Fillet" msgstr "" #: ../src/live_effects/lpe-fillet-chamfer.cpp:158 -#: ../src/ui/dialog/lpe-fillet-chamfer-properties.cpp:73 +#: ../src/ui/dialog/lpe-fillet-chamfer-properties.cpp:74 msgid "Inverse fillet" msgstr "" #: ../src/live_effects/lpe-fillet-chamfer.cpp:163 -#: ../src/ui/dialog/lpe-fillet-chamfer-properties.cpp:75 +#: ../src/ui/dialog/lpe-fillet-chamfer-properties.cpp:76 msgid "Chamfer" msgstr "" #: ../src/live_effects/lpe-fillet-chamfer.cpp:167 -#: ../src/ui/dialog/lpe-fillet-chamfer-properties.cpp:77 +#: ../src/ui/dialog/lpe-fillet-chamfer-properties.cpp:78 msgid "Inverse chamfer" msgstr "" @@ -9741,54 +9752,54 @@ msgid "" msgstr "" #: ../src/live_effects/lpe-interpolate_points.cpp:26 -#: ../src/live_effects/lpe-powerstroke.cpp:195 +#: ../src/live_effects/lpe-powerstroke.cpp:131 msgid "CubicBezierFit" msgstr "" #: ../src/live_effects/lpe-interpolate_points.cpp:27 -#: ../src/live_effects/lpe-powerstroke.cpp:196 +#: ../src/live_effects/lpe-powerstroke.cpp:132 msgid "CubicBezierJohan" msgstr "" #: ../src/live_effects/lpe-interpolate_points.cpp:28 -#: ../src/live_effects/lpe-powerstroke.cpp:197 +#: ../src/live_effects/lpe-powerstroke.cpp:133 msgid "SpiroInterpolator" msgstr "" #: ../src/live_effects/lpe-interpolate_points.cpp:29 -#: ../src/live_effects/lpe-powerstroke.cpp:198 +#: ../src/live_effects/lpe-powerstroke.cpp:134 msgid "Centripetal Catmull-Rom" msgstr "" #: ../src/live_effects/lpe-interpolate_points.cpp:37 -#: ../src/live_effects/lpe-powerstroke.cpp:240 +#: ../src/live_effects/lpe-powerstroke.cpp:176 msgid "Interpolator type:" msgstr "" #: ../src/live_effects/lpe-interpolate_points.cpp:38 -#: ../src/live_effects/lpe-powerstroke.cpp:240 +#: ../src/live_effects/lpe-powerstroke.cpp:176 msgid "" "Determines which kind of interpolator will be used to interpolate between " "stroke width along the path" msgstr "" #: ../src/live_effects/lpe-jointype.cpp:31 -#: ../src/live_effects/lpe-powerstroke.cpp:227 -#: ../src/live_effects/lpe-taperstroke.cpp:64 +#: ../src/live_effects/lpe-powerstroke.cpp:163 +#: ../src/live_effects/lpe-taperstroke.cpp:63 msgid "Beveled" msgstr "" #: ../src/live_effects/lpe-jointype.cpp:32 #: ../src/live_effects/lpe-jointype.cpp:40 -#: ../src/live_effects/lpe-powerstroke.cpp:228 -#: ../src/live_effects/lpe-taperstroke.cpp:65 +#: ../src/live_effects/lpe-powerstroke.cpp:164 +#: ../src/live_effects/lpe-taperstroke.cpp:64 #: ../src/widgets/star-toolbar.cpp:534 msgid "Rounded" msgstr "" #: ../src/live_effects/lpe-jointype.cpp:33 -#: ../src/live_effects/lpe-powerstroke.cpp:231 -#: ../src/live_effects/lpe-taperstroke.cpp:66 +#: ../src/live_effects/lpe-powerstroke.cpp:167 +#: ../src/live_effects/lpe-taperstroke.cpp:65 msgid "Miter" msgstr "" @@ -9798,22 +9809,22 @@ msgstr "" #. {LINEJOIN_EXTRP_MITER, N_("Extrapolated"), "extrapolated"}, // disabled because doesn't work well #: ../src/live_effects/lpe-jointype.cpp:35 -#: ../src/live_effects/lpe-powerstroke.cpp:230 +#: ../src/live_effects/lpe-powerstroke.cpp:166 msgid "Extrapolated arc" msgstr "" #: ../src/live_effects/lpe-jointype.cpp:39 -#: ../src/live_effects/lpe-powerstroke.cpp:210 +#: ../src/live_effects/lpe-powerstroke.cpp:146 msgid "Butt" msgstr "" #: ../src/live_effects/lpe-jointype.cpp:41 -#: ../src/live_effects/lpe-powerstroke.cpp:211 +#: ../src/live_effects/lpe-powerstroke.cpp:147 msgid "Square" msgstr "" #: ../src/live_effects/lpe-jointype.cpp:42 -#: ../src/live_effects/lpe-powerstroke.cpp:213 +#: ../src/live_effects/lpe-powerstroke.cpp:149 msgid "Peak" msgstr "" @@ -9833,21 +9844,21 @@ msgstr "" #. TRANSLATORS: The line join style specifies the shape to be used at the #. corners of paths. It can be "miter", "round" or "bevel". #: ../src/live_effects/lpe-jointype.cpp:53 -#: ../src/live_effects/lpe-powerstroke.cpp:243 +#: ../src/live_effects/lpe-powerstroke.cpp:179 #: ../src/widgets/stroke-style.cpp:227 msgid "Join:" msgstr "" #: ../src/live_effects/lpe-jointype.cpp:53 -#: ../src/live_effects/lpe-powerstroke.cpp:243 +#: ../src/live_effects/lpe-powerstroke.cpp:179 msgid "Determines the shape of the path's corners" msgstr "" #. start_lean(_("Start path lean"), _("Start path lean"), "start_lean", &wr, this, 0.), #. end_lean(_("End path lean"), _("End path lean"), "end_lean", &wr, this, 0.), #: ../src/live_effects/lpe-jointype.cpp:56 -#: ../src/live_effects/lpe-powerstroke.cpp:244 -#: ../src/live_effects/lpe-taperstroke.cpp:79 +#: ../src/live_effects/lpe-powerstroke.cpp:180 +#: ../src/live_effects/lpe-taperstroke.cpp:78 msgid "Miter limit:" msgstr "" @@ -10300,65 +10311,65 @@ msgstr "" msgid "Handles:" msgstr "" -#: ../src/live_effects/lpe-powerstroke.cpp:193 +#: ../src/live_effects/lpe-powerstroke.cpp:129 msgid "CubicBezierSmooth" msgstr "" -#: ../src/live_effects/lpe-powerstroke.cpp:212 +#: ../src/live_effects/lpe-powerstroke.cpp:148 #: ../share/extensions/gcodetools_prepare_path_for_plasma.inx.h:13 msgid "Round" msgstr "" -#: ../src/live_effects/lpe-powerstroke.cpp:214 +#: ../src/live_effects/lpe-powerstroke.cpp:150 msgid "Zero width" msgstr "" -#: ../src/live_effects/lpe-powerstroke.cpp:232 +#: ../src/live_effects/lpe-powerstroke.cpp:168 #: ../src/widgets/pencil-toolbar.cpp:103 msgid "Spiro" msgstr "" -#: ../src/live_effects/lpe-powerstroke.cpp:238 +#: ../src/live_effects/lpe-powerstroke.cpp:174 msgid "Offset points" msgstr "" -#: ../src/live_effects/lpe-powerstroke.cpp:239 +#: ../src/live_effects/lpe-powerstroke.cpp:175 msgid "Sort points" msgstr "" -#: ../src/live_effects/lpe-powerstroke.cpp:239 +#: ../src/live_effects/lpe-powerstroke.cpp:175 msgid "Sort offset points according to their time value along the curve" msgstr "" -#: ../src/live_effects/lpe-powerstroke.cpp:241 +#: ../src/live_effects/lpe-powerstroke.cpp:177 #: ../share/extensions/fractalize.inx.h:3 msgid "Smoothness:" msgstr "" -#: ../src/live_effects/lpe-powerstroke.cpp:241 +#: ../src/live_effects/lpe-powerstroke.cpp:177 msgid "" "Sets the smoothness for the CubicBezierJohan interpolator; 0 = linear " "interpolation, 1 = smooth" msgstr "" -#: ../src/live_effects/lpe-powerstroke.cpp:242 +#: ../src/live_effects/lpe-powerstroke.cpp:178 msgid "Start cap:" msgstr "" -#: ../src/live_effects/lpe-powerstroke.cpp:242 +#: ../src/live_effects/lpe-powerstroke.cpp:178 msgid "Determines the shape of the path's start" msgstr "" -#: ../src/live_effects/lpe-powerstroke.cpp:244 +#: ../src/live_effects/lpe-powerstroke.cpp:180 #: ../src/widgets/stroke-style.cpp:278 msgid "Maximum length of the miter (in units of stroke width)" msgstr "" -#: ../src/live_effects/lpe-powerstroke.cpp:245 +#: ../src/live_effects/lpe-powerstroke.cpp:181 msgid "End cap:" msgstr "" -#: ../src/live_effects/lpe-powerstroke.cpp:245 +#: ../src/live_effects/lpe-powerstroke.cpp:181 msgid "Determines the shape of the path's end" msgstr "" @@ -10711,8 +10722,8 @@ msgid "Scale nodes and handles" msgstr "" #: ../src/live_effects/lpe-show_handles.cpp:29 -#: ../src/ui/tool/multi-path-manipulator.cpp:779 -#: ../src/ui/tool/multi-path-manipulator.cpp:782 +#: ../src/ui/tool/multi-path-manipulator.cpp:787 +#: ../src/ui/tool/multi-path-manipulator.cpp:790 msgid "Rotate nodes" msgstr "" @@ -10894,52 +10905,52 @@ msgstr "" msgid "max curvature" msgstr "" -#: ../src/live_effects/lpe-taperstroke.cpp:67 +#: ../src/live_effects/lpe-taperstroke.cpp:66 msgid "Extrapolated" msgstr "" -#: ../src/live_effects/lpe-taperstroke.cpp:74 +#: ../src/live_effects/lpe-taperstroke.cpp:73 #: ../share/extensions/edge3d.inx.h:5 msgid "Stroke width:" msgstr "" -#: ../src/live_effects/lpe-taperstroke.cpp:74 +#: ../src/live_effects/lpe-taperstroke.cpp:73 msgid "The (non-tapered) width of the path" msgstr "" -#: ../src/live_effects/lpe-taperstroke.cpp:75 +#: ../src/live_effects/lpe-taperstroke.cpp:74 msgid "Start offset:" msgstr "" -#: ../src/live_effects/lpe-taperstroke.cpp:75 +#: ../src/live_effects/lpe-taperstroke.cpp:74 msgid "Taper distance from path start" msgstr "" -#: ../src/live_effects/lpe-taperstroke.cpp:76 +#: ../src/live_effects/lpe-taperstroke.cpp:75 msgid "End offset:" msgstr "" -#: ../src/live_effects/lpe-taperstroke.cpp:76 +#: ../src/live_effects/lpe-taperstroke.cpp:75 msgid "The ending position of the taper" msgstr "" -#: ../src/live_effects/lpe-taperstroke.cpp:77 +#: ../src/live_effects/lpe-taperstroke.cpp:76 msgid "Taper smoothing:" msgstr "" -#: ../src/live_effects/lpe-taperstroke.cpp:77 +#: ../src/live_effects/lpe-taperstroke.cpp:76 msgid "Amount of smoothing to apply to the tapers" msgstr "" -#: ../src/live_effects/lpe-taperstroke.cpp:78 +#: ../src/live_effects/lpe-taperstroke.cpp:77 msgid "Join type:" msgstr "" -#: ../src/live_effects/lpe-taperstroke.cpp:78 +#: ../src/live_effects/lpe-taperstroke.cpp:77 msgid "Join type for non-smooth nodes" msgstr "" -#: ../src/live_effects/lpe-taperstroke.cpp:79 +#: ../src/live_effects/lpe-taperstroke.cpp:78 msgid "Limit for miter joins" msgstr "" @@ -11013,29 +11024,29 @@ msgstr "" msgid "Change enumeration parameter" msgstr "" -#: ../src/live_effects/parameter/filletchamferpointarray.cpp:771 -#: ../src/live_effects/parameter/filletchamferpointarray.cpp:832 +#: ../src/live_effects/parameter/filletchamferpointarray.cpp:780 +#: ../src/live_effects/parameter/filletchamferpointarray.cpp:841 msgid "" "Chamfer: Ctrl+Click toggle type, Shift+Click open " "dialog, Ctrl+Alt+Click reset" msgstr "" -#: ../src/live_effects/parameter/filletchamferpointarray.cpp:775 -#: ../src/live_effects/parameter/filletchamferpointarray.cpp:836 +#: ../src/live_effects/parameter/filletchamferpointarray.cpp:784 +#: ../src/live_effects/parameter/filletchamferpointarray.cpp:845 msgid "" "Inverse Chamfer: Ctrl+Click toggle type, Shift+Click " "open dialog, Ctrl+Alt+Click reset" msgstr "" -#: ../src/live_effects/parameter/filletchamferpointarray.cpp:779 -#: ../src/live_effects/parameter/filletchamferpointarray.cpp:840 +#: ../src/live_effects/parameter/filletchamferpointarray.cpp:788 +#: ../src/live_effects/parameter/filletchamferpointarray.cpp:849 msgid "" "Inverse Fillet: Ctrl+Click toggle type, Shift+Click " "open dialog, Ctrl+Alt+Click reset" msgstr "" -#: ../src/live_effects/parameter/filletchamferpointarray.cpp:783 -#: ../src/live_effects/parameter/filletchamferpointarray.cpp:844 +#: ../src/live_effects/parameter/filletchamferpointarray.cpp:792 +#: ../src/live_effects/parameter/filletchamferpointarray.cpp:853 msgid "" "Fillet: Ctrl+Click toggle type, Shift+Click open " "dialog, Ctrl+Alt+Click reset" @@ -11066,12 +11077,12 @@ msgid "Remove Path" msgstr "" #: ../src/live_effects/parameter/originalpatharray.cpp:179 -#: ../src/ui/dialog/objects.cpp:1823 +#: ../src/ui/dialog/objects.cpp:1850 msgid "Move Down" msgstr "" #: ../src/live_effects/parameter/originalpatharray.cpp:191 -#: ../src/ui/dialog/objects.cpp:1831 +#: ../src/ui/dialog/objects.cpp:1858 msgid "Move Up" msgstr "" @@ -11087,7 +11098,7 @@ msgstr "" msgid "Remove path" msgstr "" -#: ../src/live_effects/parameter/parameter.cpp:168 +#: ../src/live_effects/parameter/parameter.cpp:161 msgid "Change scalar parameter" msgstr "" @@ -11898,6 +11909,7 @@ msgid "No groups to ungroup in the selection." msgstr "" #: ../src/selection-chemistry.cpp:869 ../src/sp-item-group.cpp:554 +#: ../src/ui/dialog/objects.cpp:1912 msgid "Ungroup" msgstr "" @@ -12214,7 +12226,7 @@ msgstr "" msgid "Select object(s) to create clippath or mask from." msgstr "" -#: ../src/selection-chemistry.cpp:3816 +#: ../src/selection-chemistry.cpp:3816 ../src/ui/dialog/objects.cpp:1918 msgid "Create Clip Group" msgstr "" @@ -12450,22 +12462,22 @@ msgstr "" msgid "without URI" msgstr "" -#: ../src/sp-ellipse.cpp:361 +#: ../src/sp-ellipse.cpp:362 msgid "Segment" msgstr "" -#: ../src/sp-ellipse.cpp:363 +#: ../src/sp-ellipse.cpp:364 msgid "Arc" msgstr "" #. Ellipse -#: ../src/sp-ellipse.cpp:366 ../src/sp-ellipse.cpp:373 +#: ../src/sp-ellipse.cpp:367 ../src/sp-ellipse.cpp:374 #: ../src/ui/dialog/inkscape-preferences.cpp:412 #: ../src/widgets/pencil-toolbar.cpp:163 msgid "Ellipse" msgstr "" -#: ../src/sp-ellipse.cpp:370 +#: ../src/sp-ellipse.cpp:371 msgid "Circle" msgstr "" @@ -12550,7 +12562,7 @@ msgstr "" msgid "%d × %d: %s" msgstr "" -#: ../src/sp-item-group.cpp:307 +#: ../src/sp-item-group.cpp:307 ../src/ui/dialog/objects.cpp:1911 msgid "Group" msgstr "" @@ -12618,26 +12630,26 @@ msgstr "" msgid "inset" msgstr "" -#: ../src/sp-path.cpp:60 +#: ../src/sp-path.cpp:59 msgid "Path" msgstr "" -#: ../src/sp-path.cpp:85 +#: ../src/sp-path.cpp:84 #, c-format msgid ", path effect: %s" msgstr "" -#: ../src/sp-path.cpp:88 +#: ../src/sp-path.cpp:87 #, c-format msgid "%i node%s" msgstr "" -#: ../src/sp-path.cpp:88 +#: ../src/sp-path.cpp:87 #, c-format msgid "%i nodes%s" msgstr "" -#: ../src/sp-polygon.cpp:173 +#: ../src/sp-polygon.cpp:172 msgid "Polygon" msgstr "" @@ -12759,110 +12771,110 @@ msgstr "" msgid "of: %s" msgstr "" -#: ../src/splivarot.cpp:70 ../src/splivarot.cpp:76 +#: ../src/splivarot.cpp:71 ../src/splivarot.cpp:77 msgid "Union" msgstr "" -#: ../src/splivarot.cpp:82 +#: ../src/splivarot.cpp:83 msgid "Intersection" msgstr "" -#: ../src/splivarot.cpp:105 +#: ../src/splivarot.cpp:106 msgid "Division" msgstr "" -#: ../src/splivarot.cpp:110 +#: ../src/splivarot.cpp:111 msgid "Cut path" msgstr "" -#: ../src/splivarot.cpp:333 +#: ../src/splivarot.cpp:335 msgid "Select at least 2 paths to perform a boolean operation." msgstr "" -#: ../src/splivarot.cpp:337 +#: ../src/splivarot.cpp:339 msgid "Select at least 1 path to perform a boolean union." msgstr "" -#: ../src/splivarot.cpp:345 +#: ../src/splivarot.cpp:347 msgid "" "Select exactly 2 paths to perform difference, division, or path cut." msgstr "" -#: ../src/splivarot.cpp:361 ../src/splivarot.cpp:376 +#: ../src/splivarot.cpp:363 ../src/splivarot.cpp:378 msgid "" "Unable to determine the z-order of the objects selected for " "difference, XOR, division, or path cut." msgstr "" -#: ../src/splivarot.cpp:406 +#: ../src/splivarot.cpp:408 msgid "" "One of the objects is not a path, cannot perform boolean operation." msgstr "" -#: ../src/splivarot.cpp:1150 +#: ../src/splivarot.cpp:1153 msgid "Select stroked path(s) to convert stroke to path." msgstr "" -#: ../src/splivarot.cpp:1506 +#: ../src/splivarot.cpp:1509 msgid "Convert stroke to path" msgstr "" #. TRANSLATORS: "to outline" means "to convert stroke to path" -#: ../src/splivarot.cpp:1509 +#: ../src/splivarot.cpp:1512 msgid "No stroked paths in the selection." msgstr "" -#: ../src/splivarot.cpp:1580 +#: ../src/splivarot.cpp:1583 msgid "Selected object is not a path, cannot inset/outset." msgstr "" -#: ../src/splivarot.cpp:1671 ../src/splivarot.cpp:1738 +#: ../src/splivarot.cpp:1674 ../src/splivarot.cpp:1741 msgid "Create linked offset" msgstr "" -#: ../src/splivarot.cpp:1672 ../src/splivarot.cpp:1739 +#: ../src/splivarot.cpp:1675 ../src/splivarot.cpp:1742 msgid "Create dynamic offset" msgstr "" -#: ../src/splivarot.cpp:1764 +#: ../src/splivarot.cpp:1767 msgid "Select path(s) to inset/outset." msgstr "" -#: ../src/splivarot.cpp:1957 +#: ../src/splivarot.cpp:1960 msgid "Outset path" msgstr "" -#: ../src/splivarot.cpp:1957 +#: ../src/splivarot.cpp:1960 msgid "Inset path" msgstr "" -#: ../src/splivarot.cpp:1959 +#: ../src/splivarot.cpp:1962 msgid "No paths to inset/outset in the selection." msgstr "" -#: ../src/splivarot.cpp:2121 +#: ../src/splivarot.cpp:2124 msgid "Simplifying paths (separately):" msgstr "" -#: ../src/splivarot.cpp:2123 +#: ../src/splivarot.cpp:2126 msgid "Simplifying paths:" msgstr "" -#: ../src/splivarot.cpp:2160 +#: ../src/splivarot.cpp:2163 #, c-format msgid "%s %d of %d paths simplified..." msgstr "" -#: ../src/splivarot.cpp:2173 +#: ../src/splivarot.cpp:2176 #, c-format msgid "%d paths simplified." msgstr "" -#: ../src/splivarot.cpp:2187 +#: ../src/splivarot.cpp:2190 msgid "Select path(s) to simplify." msgstr "" -#: ../src/splivarot.cpp:2203 +#: ../src/splivarot.cpp:2206 msgid "No paths to simplify in the selection." msgstr "" @@ -14434,6 +14446,7 @@ msgid "Embedded script files:" msgstr "" #: ../src/ui/dialog/document-properties.cpp:826 +#: ../src/ui/dialog/objects.cpp:1890 msgid "New" msgstr "" @@ -15769,6 +15782,7 @@ msgstr "" #: ../src/ui/dialog/find.cpp:110 ../share/extensions/embedimage.inx.h:3 #: ../share/extensions/extractimage.inx.h:5 +#: ../share/extensions/image_attributes.inx.h:29 msgid "Images" msgstr "" @@ -18810,7 +18824,7 @@ msgid "_Zoom in/out by:" msgstr "" #: ../src/ui/dialog/inkscape-preferences.cpp:1280 -#: ../src/ui/dialog/objects.cpp:1620 +#: ../src/ui/dialog/objects.cpp:1626 #: ../src/ui/widget/filter-effect-chooser.cpp:27 msgid "%" msgstr "" @@ -19551,7 +19565,7 @@ msgid "Move to Layer" msgstr "" #: ../src/ui/dialog/layer-properties.cpp:411 -#: ../src/ui/dialog/lpe-powerstroke-properties.cpp:116 +#: ../src/ui/dialog/lpe-powerstroke-properties.cpp:123 #: ../src/ui/dialog/transformation.cpp:108 msgid "_Move" msgstr "" @@ -19583,7 +19597,7 @@ msgid "Lock other layers" msgstr "" #: ../src/ui/dialog/layers.cpp:730 -msgid "Moved layer" +msgid "Move layer" msgstr "" #: ../src/ui/dialog/layers.cpp:892 @@ -19685,43 +19699,43 @@ msgstr "" msgid "Deactivate path effect" msgstr "" -#: ../src/ui/dialog/lpe-fillet-chamfer-properties.cpp:52 +#: ../src/ui/dialog/lpe-fillet-chamfer-properties.cpp:53 msgid "Radius (pixels):" msgstr "" -#: ../src/ui/dialog/lpe-fillet-chamfer-properties.cpp:64 +#: ../src/ui/dialog/lpe-fillet-chamfer-properties.cpp:65 msgid "Chamfer subdivisions:" msgstr "" -#: ../src/ui/dialog/lpe-fillet-chamfer-properties.cpp:135 +#: ../src/ui/dialog/lpe-fillet-chamfer-properties.cpp:136 msgid "Modify Fillet-Chamfer" msgstr "" -#: ../src/ui/dialog/lpe-fillet-chamfer-properties.cpp:136 +#: ../src/ui/dialog/lpe-fillet-chamfer-properties.cpp:137 msgid "_Modify" msgstr "" -#: ../src/ui/dialog/lpe-fillet-chamfer-properties.cpp:200 +#: ../src/ui/dialog/lpe-fillet-chamfer-properties.cpp:201 msgid "Radius" msgstr "" -#: ../src/ui/dialog/lpe-fillet-chamfer-properties.cpp:202 +#: ../src/ui/dialog/lpe-fillet-chamfer-properties.cpp:203 msgid "Radius approximated" msgstr "" -#: ../src/ui/dialog/lpe-fillet-chamfer-properties.cpp:205 +#: ../src/ui/dialog/lpe-fillet-chamfer-properties.cpp:206 msgid "Knot distance" msgstr "" -#: ../src/ui/dialog/lpe-fillet-chamfer-properties.cpp:212 +#: ../src/ui/dialog/lpe-fillet-chamfer-properties.cpp:213 msgid "Position (%):" msgstr "" -#: ../src/ui/dialog/lpe-fillet-chamfer-properties.cpp:215 +#: ../src/ui/dialog/lpe-fillet-chamfer-properties.cpp:216 msgid "%1:" msgstr "" -#: ../src/ui/dialog/lpe-powerstroke-properties.cpp:115 +#: ../src/ui/dialog/lpe-powerstroke-properties.cpp:122 msgid "Modify Node Position" msgstr "" @@ -19983,27 +19997,139 @@ msgstr "" msgid "Set object blur" msgstr "" -#: ../src/ui/dialog/objects.cpp:1800 +#: ../src/ui/dialog/objects.cpp:1617 +msgid "V" +msgstr "" + +#. TRANSLATORS: "L" here stands for Lightness +#: ../src/ui/dialog/objects.cpp:1618 ../src/widgets/tweak-toolbar.cpp:323 +msgid "L" +msgstr "" + +#: ../src/ui/dialog/objects.cpp:1619 +msgid "T" +msgstr "" + +#: ../src/ui/dialog/objects.cpp:1620 +msgid "CM" +msgstr "" + +#: ../src/ui/dialog/objects.cpp:1621 +msgid "HL" +msgstr "" + +#: ../src/ui/dialog/objects.cpp:1622 +msgid "Label" +msgstr "" + +#. In order to get tooltips on header, we must create our own label. +#: ../src/ui/dialog/objects.cpp:1664 +msgid "Toggle visibility of Layer, Group, or Object." +msgstr "" + +#: ../src/ui/dialog/objects.cpp:1677 +msgid "Toggle lock of Layer, Group, or Object." +msgstr "" + +#: ../src/ui/dialog/objects.cpp:1689 +msgid "" +"Type: Layer, Group, or Object. Clicking on Layer or Group icon, toggles " +"between the two types." +msgstr "" + +#: ../src/ui/dialog/objects.cpp:1708 +msgid "Is object clipped and/or masked?" +msgstr "" + +#: ../src/ui/dialog/objects.cpp:1719 +msgid "" +"Highlight color of outline in Node tool. Click to set. If alpha is zero, use " +"inherited color." +msgstr "" + +#: ../src/ui/dialog/objects.cpp:1730 +msgid "" +"Layer/Group/Object label (inkscape:label). Double-click to set. Default " +"value is object 'id'." +msgstr "" + +#: ../src/ui/dialog/objects.cpp:1827 msgid "Add layer..." msgstr "" -#: ../src/ui/dialog/objects.cpp:1807 +#: ../src/ui/dialog/objects.cpp:1834 msgid "Remove object" msgstr "" -#: ../src/ui/dialog/objects.cpp:1815 +#: ../src/ui/dialog/objects.cpp:1842 msgid "Move To Bottom" msgstr "" -#: ../src/ui/dialog/objects.cpp:1839 +#: ../src/ui/dialog/objects.cpp:1866 msgid "Move To Top" msgstr "" -#: ../src/ui/dialog/objects.cpp:1847 +#: ../src/ui/dialog/objects.cpp:1874 msgid "Collapse All" msgstr "" +#: ../src/ui/dialog/objects.cpp:1888 +msgid "Rename" +msgstr "" + +#: ../src/ui/dialog/objects.cpp:1894 +msgid "Solo" +msgstr "" + +#: ../src/ui/dialog/objects.cpp:1895 +msgid "Show All" +msgstr "" + +#: ../src/ui/dialog/objects.cpp:1896 +msgid "Hide All" +msgstr "" + +#: ../src/ui/dialog/objects.cpp:1900 +msgid "Lock Others" +msgstr "" + +#: ../src/ui/dialog/objects.cpp:1901 +msgid "Lock All" +msgstr "" + +#. LockAndHide +#: ../src/ui/dialog/objects.cpp:1902 ../src/verbs.cpp:2968 +msgid "Unlock All" +msgstr "" + +#: ../src/ui/dialog/objects.cpp:1906 +msgid "Up" +msgstr "" + +#: ../src/ui/dialog/objects.cpp:1907 +msgid "Down" +msgstr "" + +#: ../src/ui/dialog/objects.cpp:1916 +msgid "Set Clip" +msgstr "" + +#. will never be implemented +#. _watching.push_back( &_addPopupItem( targetDesktop, SP_VERB_OBJECT_SET_INVERSE_CLIPPATH, 0, "Set Inverse Clip", (int)BUTTON_SETINVCLIP ) ); #: ../src/ui/dialog/objects.cpp:1922 +msgid "Unset Clip" +msgstr "" + +#. Set mask +#: ../src/ui/dialog/objects.cpp:1926 ../src/ui/interface.cpp:1739 +msgid "Set Mask" +msgstr "" + +#: ../src/ui/dialog/objects.cpp:1927 +msgid "Unset Mask" +msgstr "" + +#: ../src/ui/dialog/objects.cpp:1949 msgid "Select Highlight Color" msgstr "" @@ -20639,7 +20765,7 @@ msgid "Set as _default" msgstr "" #: ../src/ui/dialog/text-edit.cpp:87 -msgid "AaBbCcIiPpQq12369$€¢?.;/()" +msgid "AaBbCcIiPpQq12369$Û’Û’?.;/()" msgstr "" #. Align buttons @@ -20679,7 +20805,7 @@ msgstr "" msgid "Text path offset" msgstr "" -#: ../src/ui/dialog/text-edit.cpp:594 ../src/ui/dialog/text-edit.cpp:668 +#: ../src/ui/dialog/text-edit.cpp:598 ../src/ui/dialog/text-edit.cpp:685 #: ../src/ui/tools/text-tool.cpp:1446 msgid "Set text style" msgstr "" @@ -21332,11 +21458,6 @@ msgstr "" msgid "Create _Link" msgstr "" -#. Set mask -#: ../src/ui/interface.cpp:1739 -msgid "Set Mask" -msgstr "" - #. Release mask #: ../src/ui/interface.cpp:1750 msgid "Release Mask" @@ -21529,32 +21650,40 @@ msgstr "" msgid "Drag to resize the flowed text frame" msgstr "" -#: ../src/ui/tool/curve-drag-point.cpp:119 +#: ../src/ui/tool/curve-drag-point.cpp:131 msgid "Drag curve" msgstr "" -#: ../src/ui/tool/curve-drag-point.cpp:176 -msgid "Add node" +#: ../src/ui/tool/curve-drag-point.cpp:192 +msgctxt "Path segment tip" +msgid "Shift: drag to open or move BSpline handles" msgstr "" -#: ../src/ui/tool/curve-drag-point.cpp:186 +#: ../src/ui/tool/curve-drag-point.cpp:196 msgctxt "Path segment tip" msgid "Shift: click to toggle segment selection" msgstr "" -#: ../src/ui/tool/curve-drag-point.cpp:190 +#: ../src/ui/tool/curve-drag-point.cpp:200 msgctxt "Path segment tip" msgid "Ctrl+Alt: click to insert a node" msgstr "" -#: ../src/ui/tool/curve-drag-point.cpp:194 +#: ../src/ui/tool/curve-drag-point.cpp:204 +msgctxt "Path segment tip" +msgid "" +"BSpline segment: drag to shape the segment, doubleclick to insert " +"node, click to select (more: Shift, Ctrl+Alt)" +msgstr "" + +#: ../src/ui/tool/curve-drag-point.cpp:209 msgctxt "Path segment tip" msgid "" "Linear segment: drag to convert to a Bezier segment, doubleclick to " "insert node, click to select (more: Shift, Ctrl+Alt)" msgstr "" -#: ../src/ui/tool/curve-drag-point.cpp:198 +#: ../src/ui/tool/curve-drag-point.cpp:213 msgctxt "Path segment tip" msgid "" "Bezier segment: drag to shape the segment, doubleclick to insert " @@ -21578,6 +21707,7 @@ msgid "Make segments curves" msgstr "" #: ../src/ui/tool/multi-path-manipulator.cpp:333 +#: ../src/ui/tool/multi-path-manipulator.cpp:347 msgid "Add nodes" msgstr "" @@ -21585,66 +21715,66 @@ msgstr "" msgid "Add extremum nodes" msgstr "" -#: ../src/ui/tool/multi-path-manipulator.cpp:346 +#: ../src/ui/tool/multi-path-manipulator.cpp:354 msgid "Duplicate nodes" msgstr "" -#: ../src/ui/tool/multi-path-manipulator.cpp:409 +#: ../src/ui/tool/multi-path-manipulator.cpp:417 #: ../src/widgets/node-toolbar.cpp:408 msgid "Join nodes" msgstr "" -#: ../src/ui/tool/multi-path-manipulator.cpp:416 +#: ../src/ui/tool/multi-path-manipulator.cpp:424 #: ../src/widgets/node-toolbar.cpp:419 msgid "Break nodes" msgstr "" -#: ../src/ui/tool/multi-path-manipulator.cpp:423 +#: ../src/ui/tool/multi-path-manipulator.cpp:431 msgid "Delete nodes" msgstr "" -#: ../src/ui/tool/multi-path-manipulator.cpp:768 +#: ../src/ui/tool/multi-path-manipulator.cpp:776 msgid "Move nodes" msgstr "" -#: ../src/ui/tool/multi-path-manipulator.cpp:771 +#: ../src/ui/tool/multi-path-manipulator.cpp:779 msgid "Move nodes horizontally" msgstr "" -#: ../src/ui/tool/multi-path-manipulator.cpp:775 +#: ../src/ui/tool/multi-path-manipulator.cpp:783 msgid "Move nodes vertically" msgstr "" -#: ../src/ui/tool/multi-path-manipulator.cpp:786 -#: ../src/ui/tool/multi-path-manipulator.cpp:792 +#: ../src/ui/tool/multi-path-manipulator.cpp:794 +#: ../src/ui/tool/multi-path-manipulator.cpp:800 msgid "Scale nodes uniformly" msgstr "" -#: ../src/ui/tool/multi-path-manipulator.cpp:789 +#: ../src/ui/tool/multi-path-manipulator.cpp:797 msgid "Scale nodes" msgstr "" -#: ../src/ui/tool/multi-path-manipulator.cpp:796 +#: ../src/ui/tool/multi-path-manipulator.cpp:804 msgid "Scale nodes horizontally" msgstr "" -#: ../src/ui/tool/multi-path-manipulator.cpp:800 +#: ../src/ui/tool/multi-path-manipulator.cpp:808 msgid "Scale nodes vertically" msgstr "" -#: ../src/ui/tool/multi-path-manipulator.cpp:804 +#: ../src/ui/tool/multi-path-manipulator.cpp:812 msgid "Skew nodes horizontally" msgstr "" -#: ../src/ui/tool/multi-path-manipulator.cpp:808 +#: ../src/ui/tool/multi-path-manipulator.cpp:816 msgid "Skew nodes vertically" msgstr "" -#: ../src/ui/tool/multi-path-manipulator.cpp:812 +#: ../src/ui/tool/multi-path-manipulator.cpp:820 msgid "Flip nodes horizontally" msgstr "" -#: ../src/ui/tool/multi-path-manipulator.cpp:815 +#: ../src/ui/tool/multi-path-manipulator.cpp:823 msgid "Flip nodes vertically" msgstr "" @@ -21778,13 +21908,13 @@ msgctxt "Path node tip" msgid "Alt: sculpt nodes" msgstr "" -#: ../src/ui/tool/node.cpp:1470 +#: ../src/ui/tool/node.cpp:1471 #, c-format msgctxt "Path node tip" msgid "%s: drag to shape the path (more: Shift, Ctrl, Alt)" msgstr "" -#: ../src/ui/tool/node.cpp:1473 +#: ../src/ui/tool/node.cpp:1474 #, c-format msgctxt "Path node tip" msgid "" @@ -21792,7 +21922,7 @@ msgid "" "power" msgstr "" -#: ../src/ui/tool/node.cpp:1476 +#: ../src/ui/tool/node.cpp:1477 #, c-format msgctxt "Path node tip" msgid "" @@ -21800,7 +21930,7 @@ msgid "" "(more: Shift, Ctrl, Alt)" msgstr "" -#: ../src/ui/tool/node.cpp:1480 +#: ../src/ui/tool/node.cpp:1481 #, c-format msgctxt "Path node tip" msgid "" @@ -21808,7 +21938,7 @@ msgid "" "Shift, Ctrl, Alt)" msgstr "" -#: ../src/ui/tool/node.cpp:1483 +#: ../src/ui/tool/node.cpp:1484 #, c-format msgctxt "Path node tip" msgid "" @@ -21816,43 +21946,47 @@ msgid "" "(more: Shift, Ctrl, Alt). %g power" msgstr "" -#: ../src/ui/tool/node.cpp:1496 +#: ../src/ui/tool/node.cpp:1497 #, c-format msgctxt "Path node tip" msgid "Move node by %s, %s" msgstr "" -#: ../src/ui/tool/node.cpp:1507 +#: ../src/ui/tool/node.cpp:1508 msgid "Symmetric node" msgstr "" -#: ../src/ui/tool/node.cpp:1508 +#: ../src/ui/tool/node.cpp:1509 msgid "Auto-smooth node" msgstr "" -#: ../src/ui/tool/path-manipulator.cpp:837 +#: ../src/ui/tool/path-manipulator.cpp:296 +msgid "Add node" +msgstr "" + +#: ../src/ui/tool/path-manipulator.cpp:859 msgid "Scale handle" msgstr "" -#: ../src/ui/tool/path-manipulator.cpp:861 +#: ../src/ui/tool/path-manipulator.cpp:883 msgid "Rotate handle" msgstr "" #. We need to call MPM's method because it could have been our last node -#: ../src/ui/tool/path-manipulator.cpp:1534 +#: ../src/ui/tool/path-manipulator.cpp:1556 #: ../src/widgets/node-toolbar.cpp:397 msgid "Delete node" msgstr "" -#: ../src/ui/tool/path-manipulator.cpp:1542 +#: ../src/ui/tool/path-manipulator.cpp:1564 msgid "Cycle node type" msgstr "" -#: ../src/ui/tool/path-manipulator.cpp:1557 +#: ../src/ui/tool/path-manipulator.cpp:1579 msgid "Drag handle" msgstr "" -#: ../src/ui/tool/path-manipulator.cpp:1566 +#: ../src/ui/tool/path-manipulator.cpp:1588 msgid "Retract handle" msgstr "" @@ -22065,7 +22199,7 @@ msgstr "" msgid "Drag to measure the dimensions of objects." msgstr "" -#: ../src/ui/tools-switch.cpp:112 ../src/ui/tools/dropper-tool.cpp:275 +#: ../src/ui/tools-switch.cpp:112 ../src/ui/tools/dropper-tool.cpp:274 msgid "" "Click to set fill, Shift+click to set stroke; drag to " "average color in area; with Alt to pick inverse color; Ctrl+C " @@ -22133,28 +22267,28 @@ msgstr "" msgid "Create 3D box" msgstr "" -#: ../src/ui/tools/calligraphic-tool.cpp:526 +#: ../src/ui/tools/calligraphic-tool.cpp:525 msgid "" "Guide path selected; start drawing along the guide with Ctrl" msgstr "" -#: ../src/ui/tools/calligraphic-tool.cpp:528 +#: ../src/ui/tools/calligraphic-tool.cpp:527 msgid "Select a guide path to track with Ctrl" msgstr "" -#: ../src/ui/tools/calligraphic-tool.cpp:663 +#: ../src/ui/tools/calligraphic-tool.cpp:662 msgid "Tracking: connection to guide path lost!" msgstr "" -#: ../src/ui/tools/calligraphic-tool.cpp:663 +#: ../src/ui/tools/calligraphic-tool.cpp:662 msgid "Tracking a guide path" msgstr "" -#: ../src/ui/tools/calligraphic-tool.cpp:666 +#: ../src/ui/tools/calligraphic-tool.cpp:665 msgid "Drawing a calligraphic stroke" msgstr "" -#: ../src/ui/tools/calligraphic-tool.cpp:967 +#: ../src/ui/tools/calligraphic-tool.cpp:966 msgid "Draw calligraphic stroke" msgstr "" @@ -22198,27 +22332,27 @@ msgstr "" #. alpha of color under cursor, to show in the statusbar #. locale-sensitive printf is OK, since this goes to the UI, not into SVG -#: ../src/ui/tools/dropper-tool.cpp:271 +#: ../src/ui/tools/dropper-tool.cpp:270 #, c-format msgid " alpha %.3g" msgstr "" #. where the color is picked, to show in the statusbar -#: ../src/ui/tools/dropper-tool.cpp:273 +#: ../src/ui/tools/dropper-tool.cpp:272 #, c-format msgid ", averaged with radius %d" msgstr "" -#: ../src/ui/tools/dropper-tool.cpp:273 +#: ../src/ui/tools/dropper-tool.cpp:272 msgid " under cursor" msgstr "" #. message, to show in the statusbar -#: ../src/ui/tools/dropper-tool.cpp:275 +#: ../src/ui/tools/dropper-tool.cpp:274 msgid "Release mouse to set color." msgstr "" -#: ../src/ui/tools/dropper-tool.cpp:323 +#: ../src/ui/tools/dropper-tool.cpp:322 msgid "Set picked color" msgstr "" @@ -22463,171 +22597,171 @@ msgstr "" msgid "FIXMEShift: draw mesh around the starting point" msgstr "" -#: ../src/ui/tools/node-tool.cpp:601 +#: ../src/ui/tools/node-tool.cpp:653 msgctxt "Node tool tip" msgid "" "Shift: drag to add nodes to the selection, click to toggle object " "selection" msgstr "" -#: ../src/ui/tools/node-tool.cpp:605 +#: ../src/ui/tools/node-tool.cpp:657 msgctxt "Node tool tip" msgid "Shift: drag to add nodes to the selection" msgstr "" -#: ../src/ui/tools/node-tool.cpp:617 +#: ../src/ui/tools/node-tool.cpp:686 #, c-format msgid "%u of %u node selected." msgid_plural "%u of %u nodes selected." msgstr[0] "" msgstr[1] "" -#: ../src/ui/tools/node-tool.cpp:623 +#: ../src/ui/tools/node-tool.cpp:693 #, c-format msgctxt "Node tool tip" msgid "%s Drag to select nodes, click to edit only this object (more: Shift)" msgstr "" -#: ../src/ui/tools/node-tool.cpp:629 +#: ../src/ui/tools/node-tool.cpp:699 #, c-format msgctxt "Node tool tip" msgid "%s Drag to select nodes, click clear the selection" msgstr "" -#: ../src/ui/tools/node-tool.cpp:638 +#: ../src/ui/tools/node-tool.cpp:708 msgctxt "Node tool tip" msgid "Drag to select nodes, click to edit only this object" msgstr "" -#: ../src/ui/tools/node-tool.cpp:641 +#: ../src/ui/tools/node-tool.cpp:711 msgctxt "Node tool tip" msgid "Drag to select nodes, click to clear the selection" msgstr "" -#: ../src/ui/tools/node-tool.cpp:646 +#: ../src/ui/tools/node-tool.cpp:716 msgctxt "Node tool tip" msgid "Drag to select objects to edit, click to edit this object (more: Shift)" msgstr "" -#: ../src/ui/tools/node-tool.cpp:649 +#: ../src/ui/tools/node-tool.cpp:719 msgctxt "Node tool tip" msgid "Drag to select objects to edit" msgstr "" -#: ../src/ui/tools/pen-tool.cpp:224 ../src/ui/tools/pencil-tool.cpp:454 +#: ../src/ui/tools/pen-tool.cpp:223 ../src/ui/tools/pencil-tool.cpp:455 msgid "Drawing cancelled" msgstr "" -#: ../src/ui/tools/pen-tool.cpp:460 ../src/ui/tools/pencil-tool.cpp:195 +#: ../src/ui/tools/pen-tool.cpp:459 ../src/ui/tools/pencil-tool.cpp:196 msgid "Continuing selected path" msgstr "" -#: ../src/ui/tools/pen-tool.cpp:470 ../src/ui/tools/pencil-tool.cpp:203 +#: ../src/ui/tools/pen-tool.cpp:469 ../src/ui/tools/pencil-tool.cpp:204 msgid "Creating new path" msgstr "" -#: ../src/ui/tools/pen-tool.cpp:472 ../src/ui/tools/pencil-tool.cpp:206 +#: ../src/ui/tools/pen-tool.cpp:471 ../src/ui/tools/pencil-tool.cpp:207 msgid "Appending to selected path" msgstr "" -#: ../src/ui/tools/pen-tool.cpp:637 +#: ../src/ui/tools/pen-tool.cpp:636 msgid "Click or click and drag to close and finish the path." msgstr "" -#: ../src/ui/tools/pen-tool.cpp:639 +#: ../src/ui/tools/pen-tool.cpp:638 msgid "" "Click or click and drag to close and finish the path. Shift" "+Click make a cusp node" msgstr "" -#: ../src/ui/tools/pen-tool.cpp:651 +#: ../src/ui/tools/pen-tool.cpp:650 msgid "" "Click or click and drag to continue the path from this point." msgstr "" -#: ../src/ui/tools/pen-tool.cpp:653 +#: ../src/ui/tools/pen-tool.cpp:652 msgid "" "Click or click and drag to continue the path from this point. " "Shift+Click make a cusp node" msgstr "" -#: ../src/ui/tools/pen-tool.cpp:2027 +#: ../src/ui/tools/pen-tool.cpp:2026 #, c-format msgid "" "Curve segment: angle %3.2f°, distance %s; with Ctrl to " "snap angle, Enter to finish the path" msgstr "" -#: ../src/ui/tools/pen-tool.cpp:2028 +#: ../src/ui/tools/pen-tool.cpp:2027 #, c-format msgid "" "Line segment: angle %3.2f°, distance %s; with Ctrl to " "snap angle, Enter to finish the path" msgstr "" -#: ../src/ui/tools/pen-tool.cpp:2031 +#: ../src/ui/tools/pen-tool.cpp:2030 #, c-format msgid "" "Curve segment: angle %3.2f°, distance %s; with Shift+Click make a cusp node, Enter to finish the path" msgstr "" -#: ../src/ui/tools/pen-tool.cpp:2032 +#: ../src/ui/tools/pen-tool.cpp:2031 #, c-format msgid "" "Line segment: angle %3.2f°, distance %s; with Shift+Click " "make a cusp node, Enter to finish the path" msgstr "" -#: ../src/ui/tools/pen-tool.cpp:2049 +#: ../src/ui/tools/pen-tool.cpp:2048 #, c-format msgid "" "Curve handle: angle %3.2f°, length %s; with Ctrl to snap " "angle" msgstr "" -#: ../src/ui/tools/pen-tool.cpp:2073 +#: ../src/ui/tools/pen-tool.cpp:2072 #, c-format msgid "" "Curve handle, symmetric: angle %3.2f°, length %s; with Ctrl to snap angle, with Shift to move this handle only" msgstr "" -#: ../src/ui/tools/pen-tool.cpp:2074 +#: ../src/ui/tools/pen-tool.cpp:2073 #, c-format msgid "" "Curve handle: angle %3.2f°, length %s; with Ctrl to snap " "angle, with Shift to move this handle only" msgstr "" -#: ../src/ui/tools/pen-tool.cpp:2208 +#: ../src/ui/tools/pen-tool.cpp:2207 msgid "Drawing finished" msgstr "" -#: ../src/ui/tools/pencil-tool.cpp:307 +#: ../src/ui/tools/pencil-tool.cpp:308 msgid "Release here to close and finish the path." msgstr "" -#: ../src/ui/tools/pencil-tool.cpp:313 +#: ../src/ui/tools/pencil-tool.cpp:314 msgid "Drawing a freehand path" msgstr "" -#: ../src/ui/tools/pencil-tool.cpp:318 +#: ../src/ui/tools/pencil-tool.cpp:319 msgid "Drag to continue the path from this point." msgstr "" #. Write curves to object -#: ../src/ui/tools/pencil-tool.cpp:401 +#: ../src/ui/tools/pencil-tool.cpp:402 msgid "Finishing freehand" msgstr "" -#: ../src/ui/tools/pencil-tool.cpp:503 +#: ../src/ui/tools/pencil-tool.cpp:504 msgid "" "Sketch mode: holding Alt interpolates between sketched paths. " "Release Alt to finalize." msgstr "" -#: ../src/ui/tools/pencil-tool.cpp:530 +#: ../src/ui/tools/pencil-tool.cpp:531 msgid "Finishing freehand sketch" msgstr "" @@ -22735,51 +22869,51 @@ msgstr "" msgid "Create spiral" msgstr "" -#: ../src/ui/tools/spray-tool.cpp:182 ../src/ui/tools/tweak-tool.cpp:157 +#: ../src/ui/tools/spray-tool.cpp:173 ../src/ui/tools/tweak-tool.cpp:157 #, c-format msgid "%i object selected" msgid_plural "%i objects selected" msgstr[0] "" msgstr[1] "" -#: ../src/ui/tools/spray-tool.cpp:184 ../src/ui/tools/tweak-tool.cpp:159 +#: ../src/ui/tools/spray-tool.cpp:175 ../src/ui/tools/tweak-tool.cpp:159 msgid "Nothing selected" msgstr "" -#: ../src/ui/tools/spray-tool.cpp:189 +#: ../src/ui/tools/spray-tool.cpp:180 #, c-format msgid "" "%s. Drag, click or click and scroll to spray copies of the initial " "selection." msgstr "" -#: ../src/ui/tools/spray-tool.cpp:192 +#: ../src/ui/tools/spray-tool.cpp:183 #, c-format msgid "" "%s. Drag, click or click and scroll to spray clones of the initial " "selection." msgstr "" -#: ../src/ui/tools/spray-tool.cpp:195 +#: ../src/ui/tools/spray-tool.cpp:186 #, c-format msgid "" "%s. Drag, click or click and scroll to spray in a single path of the " "initial selection." msgstr "" -#: ../src/ui/tools/spray-tool.cpp:648 +#: ../src/ui/tools/spray-tool.cpp:618 msgid "Nothing selected! Select objects to spray." msgstr "" -#: ../src/ui/tools/spray-tool.cpp:723 ../src/widgets/spray-toolbar.cpp:166 +#: ../src/ui/tools/spray-tool.cpp:693 ../src/widgets/spray-toolbar.cpp:166 msgid "Spray with copies" msgstr "" -#: ../src/ui/tools/spray-tool.cpp:727 ../src/widgets/spray-toolbar.cpp:173 +#: ../src/ui/tools/spray-tool.cpp:697 ../src/widgets/spray-toolbar.cpp:173 msgid "Spray with clones" msgstr "" -#: ../src/ui/tools/spray-tool.cpp:731 +#: ../src/ui/tools/spray-tool.cpp:701 msgid "Spray in single path" msgstr "" @@ -23018,62 +23152,66 @@ msgid "" "%s. Drag or click to increase blur; with Shift to decrease." msgstr "" -#: ../src/ui/tools/tweak-tool.cpp:1192 +#: ../src/ui/tools/tweak-tool.cpp:1191 msgid "Nothing selected! Select objects to tweak." msgstr "" -#: ../src/ui/tools/tweak-tool.cpp:1226 +#: ../src/ui/tools/tweak-tool.cpp:1225 msgid "Move tweak" msgstr "" -#: ../src/ui/tools/tweak-tool.cpp:1230 +#: ../src/ui/tools/tweak-tool.cpp:1229 msgid "Move in/out tweak" msgstr "" -#: ../src/ui/tools/tweak-tool.cpp:1234 +#: ../src/ui/tools/tweak-tool.cpp:1233 msgid "Move jitter tweak" msgstr "" -#: ../src/ui/tools/tweak-tool.cpp:1238 +#: ../src/ui/tools/tweak-tool.cpp:1237 msgid "Scale tweak" msgstr "" -#: ../src/ui/tools/tweak-tool.cpp:1242 +#: ../src/ui/tools/tweak-tool.cpp:1241 msgid "Rotate tweak" msgstr "" -#: ../src/ui/tools/tweak-tool.cpp:1246 +#: ../src/ui/tools/tweak-tool.cpp:1245 msgid "Duplicate/delete tweak" msgstr "" -#: ../src/ui/tools/tweak-tool.cpp:1250 +#: ../src/ui/tools/tweak-tool.cpp:1249 msgid "Push path tweak" msgstr "" -#: ../src/ui/tools/tweak-tool.cpp:1254 +#: ../src/ui/tools/tweak-tool.cpp:1253 msgid "Shrink/grow path tweak" msgstr "" -#: ../src/ui/tools/tweak-tool.cpp:1258 +#: ../src/ui/tools/tweak-tool.cpp:1257 msgid "Attract/repel path tweak" msgstr "" -#: ../src/ui/tools/tweak-tool.cpp:1262 +#: ../src/ui/tools/tweak-tool.cpp:1261 msgid "Roughen path tweak" msgstr "" -#: ../src/ui/tools/tweak-tool.cpp:1266 +#: ../src/ui/tools/tweak-tool.cpp:1265 msgid "Color paint tweak" msgstr "" -#: ../src/ui/tools/tweak-tool.cpp:1270 +#: ../src/ui/tools/tweak-tool.cpp:1269 msgid "Color jitter tweak" msgstr "" -#: ../src/ui/tools/tweak-tool.cpp:1274 +#: ../src/ui/tools/tweak-tool.cpp:1273 msgid "Blur tweak" msgstr "" +#: ../src/ui/widget/color-entry.cpp:31 +msgid "Hexadecimal RGBA value of the color" +msgstr "" + #: ../src/ui/widget/color-icc-selector.cpp:176 #: ../src/ui/widget/color-scales.cpp:378 msgid "_R:" @@ -23310,22 +23448,132 @@ msgstr "" msgid "Slashed Zero" msgstr "" -#: ../src/ui/widget/font-variants.cpp:80 +#: ../src/ui/widget/font-variants.cpp:71 +msgid "Feature Settings" +msgstr "" + +#: ../src/ui/widget/font-variants.cpp:72 +msgid "Selection has different Feature Settings!" +msgstr "" + +#: ../src/ui/widget/font-variants.cpp:85 msgid "Common ligatures. On by default. OpenType tables: 'liga', 'clig'" msgstr "" -#: ../src/ui/widget/font-variants.cpp:82 +#: ../src/ui/widget/font-variants.cpp:87 msgid "Discretionary ligatures. Off by default. OpenType table: 'dlig'" msgstr "" -#: ../src/ui/widget/font-variants.cpp:84 +#: ../src/ui/widget/font-variants.cpp:89 msgid "Historical ligatures. Off by default. OpenType table: 'hlig'" msgstr "" -#: ../src/ui/widget/font-variants.cpp:86 +#: ../src/ui/widget/font-variants.cpp:91 msgid "Contextual forms. On by default. OpenType table: 'calt'" msgstr "" +#. Position ---------------------------------- +#. Add tooltips +#: ../src/ui/widget/font-variants.cpp:112 +msgid "Normal position." +msgstr "" + +#: ../src/ui/widget/font-variants.cpp:113 +msgid "Subscript. OpenType table: 'subs'" +msgstr "" + +#: ../src/ui/widget/font-variants.cpp:114 +msgid "Superscript. OpenType table: 'sups'" +msgstr "" + +#. Caps ---------------------------------- +#. Add tooltips +#: ../src/ui/widget/font-variants.cpp:138 +msgid "Normal capitalization." +msgstr "" + +#: ../src/ui/widget/font-variants.cpp:139 +msgid "Small-caps (lowercase). OpenType table: 'smcp'" +msgstr "" + +#: ../src/ui/widget/font-variants.cpp:140 +msgid "" +"All small-caps (uppercase and lowercase). OpenType tables: 'c2sc' and 'smcp'" +msgstr "" + +#: ../src/ui/widget/font-variants.cpp:141 +msgid "Petite-caps (lowercase). OpenType table: 'pcap'" +msgstr "" + +#: ../src/ui/widget/font-variants.cpp:142 +msgid "" +"All petite-caps (uppercase and lowercase). OpenType tables: 'c2sc' and 'pcap'" +msgstr "" + +#: ../src/ui/widget/font-variants.cpp:143 +msgid "" +"Unicase (small caps for uppercase, normal for lowercase). OpenType table: " +"'unic'" +msgstr "" + +#: ../src/ui/widget/font-variants.cpp:144 +msgid "" +"Titling caps (lighter-weight uppercase for use in titles). OpenType table: " +"'titl'" +msgstr "" + +#. Numeric ------------------------------ +#. Add tooltips +#: ../src/ui/widget/font-variants.cpp:180 +msgid "Normal style." +msgstr "" + +#: ../src/ui/widget/font-variants.cpp:181 +msgid "Lining numerals. OpenType table: 'lnum'" +msgstr "" + +#: ../src/ui/widget/font-variants.cpp:182 +msgid "Old style numerals. OpenType table: 'onum'" +msgstr "" + +#: ../src/ui/widget/font-variants.cpp:183 +msgid "Normal widths." +msgstr "" + +#: ../src/ui/widget/font-variants.cpp:184 +msgid "Proportional width numerals. OpenType table: 'pnum'" +msgstr "" + +#: ../src/ui/widget/font-variants.cpp:185 +msgid "Same width numerals. OpenType table: 'tnum'" +msgstr "" + +#: ../src/ui/widget/font-variants.cpp:186 +msgid "Normal fractions." +msgstr "" + +#: ../src/ui/widget/font-variants.cpp:187 +msgid "Diagonal fractions. OpenType table: 'frac'" +msgstr "" + +#: ../src/ui/widget/font-variants.cpp:188 +msgid "Stacked fractions. OpenType table: 'afrc'" +msgstr "" + +#: ../src/ui/widget/font-variants.cpp:189 +msgid "Ordinals (raised 'th', etc.). OpenType table: 'ordn'" +msgstr "" + +#: ../src/ui/widget/font-variants.cpp:190 +msgid "Slashed zeros. OpenType table: 'zero'" +msgstr "" + +#. Feature settings --------------------- +#. Add tooltips +#: ../src/ui/widget/font-variants.cpp:240 +msgid "Feature settings in CSS form. No sanity checking is performed." +msgstr "" + #: ../src/ui/widget/layer-selector.cpp:118 msgid "Toggle current layer visibility" msgstr "" @@ -26280,11 +26528,6 @@ msgid "" "Fit the page to the current selection or the drawing if there is no selection" msgstr "" -#. LockAndHide -#: ../src/verbs.cpp:2968 -msgid "Unlock All" -msgstr "" - #: ../src/verbs.cpp:2970 msgid "Unlock All in All Layers" msgstr "" @@ -28604,15 +28847,15 @@ msgstr "" msgid "End Markers are drawn on the last node of a path or shape" msgstr "" -#: ../src/widgets/stroke-style.cpp:494 +#: ../src/widgets/stroke-style.cpp:498 msgid "Set markers" msgstr "" -#: ../src/widgets/stroke-style.cpp:1029 ../src/widgets/stroke-style.cpp:1113 +#: ../src/widgets/stroke-style.cpp:1033 ../src/widgets/stroke-style.cpp:1117 msgid "Set stroke style" msgstr "" -#: ../src/widgets/stroke-style.cpp:1202 +#: ../src/widgets/stroke-style.cpp:1206 msgid "Set marker color" msgstr "" @@ -29191,11 +29434,6 @@ msgstr "" msgid "In color mode, act on objects' lightness" msgstr "" -#. TRANSLATORS: "L" here stands for Lightness -#: ../src/widgets/tweak-toolbar.cpp:323 -msgid "L" -msgstr "" - #: ../src/widgets/tweak-toolbar.cpp:335 msgid "In color mode, act on objects' opacity" msgstr "" @@ -30114,12 +30352,16 @@ msgstr "" msgid "The directory \"%s\" does not exists." msgstr "" -#: ../share/extensions/webslicer_export.py:102 +#: ../share/extensions/webslicer_export.py:78 +msgid "No slicer layer found." +msgstr "" + +#: ../share/extensions/webslicer_export.py:108 #, python-format msgid "You have more than one element with \"%s\" html-id." msgstr "" -#: ../share/extensions/webslicer_export.py:332 +#: ../share/extensions/webslicer_export.py:338 msgid "You must install the ImageMagick to get JPG and GIF." msgstr "" @@ -32664,6 +32906,86 @@ msgstr "" msgid "Export an HP Graphics Language file" msgstr "" +#: ../share/extensions/image_attributes.inx.h:1 +msgid "Set Image Attributes" +msgstr "" + +#. render images like in 0.48 +#: ../share/extensions/image_attributes.inx.h:3 +msgid "Basic" +msgstr "" + +#. render images like in 0.48 +#: ../share/extensions/image_attributes.inx.h:5 +msgid "Support non-unifom scaling" +msgstr "" + +#. render images like in 0.48 +#: ../share/extensions/image_attributes.inx.h:7 +msgid "Render images blocky" +msgstr "" + +#. render images like in 0.48 +#: ../share/extensions/image_attributes.inx.h:9 +msgid "" +"Render all bitmap images like in older Inskcape versions. Available options:" +msgstr "" + +#. image aspect ratio +#: ../share/extensions/image_attributes.inx.h:11 +msgid "Image Aspect Ratio" +msgstr "" + +#. image aspect ratio +#: ../share/extensions/image_attributes.inx.h:13 +msgid "preserveAspectRatio attribute:" +msgstr "" + +#. image aspect ratio +#: ../share/extensions/image_attributes.inx.h:15 +msgid "meetOrSlice:" +msgstr "" + +#. image-rendering +#: ../share/extensions/image_attributes.inx.h:17 +msgid "Scope:" +msgstr "" + +#. image-rendering +#: ../share/extensions/image_attributes.inx.h:19 +msgid "Unset" +msgstr "" + +#: ../share/extensions/image_attributes.inx.h:20 +msgid "Change only selected image(s)" +msgstr "" + +#: ../share/extensions/image_attributes.inx.h:21 +msgid "Change all images in selection" +msgstr "" + +#: ../share/extensions/image_attributes.inx.h:22 +msgid "Change all images in document" +msgstr "" + +#. image-rendering +#: ../share/extensions/image_attributes.inx.h:24 +msgid "Image Rendering Quality" +msgstr "" + +#. image-rendering +#: ../share/extensions/image_attributes.inx.h:26 +msgid "Image rendering attribute:" +msgstr "" + +#: ../share/extensions/image_attributes.inx.h:27 +msgid "Apply attribute to parent group of selection" +msgstr "" + +#: ../share/extensions/image_attributes.inx.h:28 +msgid "Apply attribute to SVG root" +msgstr "" + #: ../share/extensions/ink2canvas.inx.h:1 msgid "Convert to html5 canvas" msgstr "" diff --git a/share/extensions/CMakeLists.txt b/share/extensions/CMakeLists.txt index c167a156a..280abd0a7 100644 --- a/share/extensions/CMakeLists.txt +++ b/share/extensions/CMakeLists.txt @@ -42,5 +42,8 @@ install(FILES ${_FILES} DESTINATION ${SHARE_INSTALL}/inkscape/extensions/Poly3DO file(GLOB _FILES "ink2canvas/*.py") install(FILES ${_FILES} DESTINATION ${SHARE_INSTALL}/inkscape/extensions/ink2canvas) +file(GLOB _FILES "scour/*.py") +install(FILES ${_FILES} DESTINATION ${SHARE_INSTALL}/inkscape/extensions/scour) + file(GLOB _FILES "xaml2svg/*.xsl") install(FILES ${_FILES} DESTINATION ${SHARE_INSTALL}/inkscape/extensions/xaml2svg) diff --git a/share/extensions/Makefile.am b/share/extensions/Makefile.am index 130ed6e40..765f18a02 100644 --- a/share/extensions/Makefile.am +++ b/share/extensions/Makefile.am @@ -2,9 +2,10 @@ SUBDIRS = \ alphabet_soup \ Barcode \ + ink2canvas \ Poly3DObjects \ + scour \ test \ - ink2canvas \ xaml2svg extensiondir = $(datadir)/inkscape/extensions diff --git a/share/extensions/scour.inkscape.py b/share/extensions/scour.inkscape.py deleted file mode 100755 index f161a09c2..000000000 --- a/share/extensions/scour.inkscape.py +++ /dev/null @@ -1,78 +0,0 @@ -#!/usr/bin/env python -# -*- coding: utf-8 -*- -import sys, inkex -from scour import scourString - -class ScourInkscape (inkex.Effect): - - def __init__(self): - inkex.Effect.__init__(self) - self.OptionParser.add_option("--tab", - action="store", type="string", - dest="tab") - self.OptionParser.add_option("--simplify-colors", type="inkbool", - action="store", dest="simple_colors", default=True, - help="won't convert all colors to #RRGGBB format") - self.OptionParser.add_option("--style-to-xml", type="inkbool", - action="store", dest="style_to_xml", default=True, - help="won't convert styles into XML attributes") - self.OptionParser.add_option("--group-collapsing", type="inkbool", - action="store", dest="group_collapse", default=True, - help="won't collapse elements") - self.OptionParser.add_option("--create-groups", type="inkbool", - action="store", dest="group_create", default=False, - help="create elements for runs of elements with identical attributes") - self.OptionParser.add_option("--enable-id-stripping", type="inkbool", - action="store", dest="strip_ids", default=False, - help="remove all un-referenced ID attributes") - self.OptionParser.add_option("--shorten-ids", type="inkbool", - action="store", dest="shorten_ids", default=False, - help="shorten all ID attributes to the least number of letters possible") - self.OptionParser.add_option("--embed-rasters", type="inkbool", - action="store", dest="embed_rasters", default=True, - help="won't embed rasters as base64-encoded data") - self.OptionParser.add_option("--keep-editor-data", type="inkbool", - action="store", dest="keep_editor_data", default=False, - help="won't remove Inkscape, Sodipodi or Adobe Illustrator elements and attributes") - self.OptionParser.add_option("--remove-metadata", type="inkbool", - action="store", dest="remove_metadata", default=False, - help="remove elements (which may contain license metadata etc.)") - self.OptionParser.add_option("--strip-xml-prolog", type="inkbool", - action="store", dest="strip_xml_prolog", default=False, - help="won't output the prolog") - self.OptionParser.add_option("-p", "--set-precision", - action="store", type=int, dest="digits", default=5, - help="set number of significant digits (default: %default)") - self.OptionParser.add_option("--indent", - action="store", type="string", dest="indent_type", default="space", - help="indentation of the output: none, space, tab (default: %default)") - self.OptionParser.add_option("--protect-ids-noninkscape", type="inkbool", - action="store", dest="protect_ids_noninkscape", default=False, - help="don't change IDs not ending with a digit") - self.OptionParser.add_option("--protect-ids-list", - action="store", type="string", dest="protect_ids_list", default=None, - help="don't change IDs given in a comma-separated list") - self.OptionParser.add_option("--protect-ids-prefix", - action="store", type="string", dest="protect_ids_prefix", default=None, - help="don't change IDs starting with the given prefix") - self.OptionParser.add_option("--enable-viewboxing", type="inkbool", - action="store", dest="enable_viewboxing", default=False, - help="changes document width/height to 100%/100% and creates viewbox coordinates") - self.OptionParser.add_option("--enable-comment-stripping", type="inkbool", - action="store", dest="strip_comments", default=False, - help="remove all comments") - self.OptionParser.add_option("--renderer-workaround", type="inkbool", - action="store", dest="renderer_workaround", default=False, - help="work around various renderer bugs (currently only librsvg)") - - def effect(self): - input = file(self.args[0], "r") - self.options.infilename=self.args[0] - sys.stdout.write(scourString(input.read(), self.options).encode("UTF-8")) - input.close() - sys.stdout.close() - - -if __name__ == '__main__': - e = ScourInkscape() - e.affect(output=False) diff --git a/share/extensions/scour.inx b/share/extensions/scour.inx index 45719deeb..c97eec502 100644 --- a/share/extensions/scour.inx +++ b/share/extensions/scour.inx @@ -2,9 +2,9 @@ <_name>Optimized SVG Output org.inkscape.output.scour - scour.py - svg_regex.py - yocto_css.py + scour/scour.py + scour/svg_regex.py + scour/yocto_css.py true @@ -63,6 +63,6 @@ <_filetypetooltip>Scalable Vector Graphics diff --git a/share/extensions/scour.py b/share/extensions/scour.py deleted file mode 100644 index 236529daa..000000000 --- a/share/extensions/scour.py +++ /dev/null @@ -1,3235 +0,0 @@ -#!/usr/bin/env python -# -*- coding: utf-8 -*- - -# Scour -# -# Copyright 2010 Jeff Schiller -# Copyright 2010 Louis Simard -# -# This file is part of Scour, http://www.codedread.com/scour/ -# -# This library is free software; you can redistribute it and/or modify -# it either under the terms of the Apache License, Version 2.0, or, at -# your option, under the terms and conditions of the GNU General -# Public License, Version 2 or newer as published by the Free Software -# Foundation. You may obtain a copy of these Licenses at: -# -# http://www.apache.org/licenses/LICENSE-2.0 -# https://www.gnu.org/licenses/old-licenses/gpl-2.0.html -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -# Notes: - -# rubys' path-crunching ideas here: http://intertwingly.net/code/svgtidy/spec.rb -# (and implemented here: http://intertwingly.net/code/svgtidy/svgtidy.rb ) - -# Yet more ideas here: http://wiki.inkscape.org/wiki/index.php/Save_Cleaned_SVG -# -# * Process Transformations -# * Collapse all group based transformations - -# Even more ideas here: http://esw.w3.org/topic/SvgTidy -# * analysis of path elements to see if rect can be used instead? (must also need to look -# at rounded corners) - -# Next Up: -# - why are marker-start, -end not removed from the style attribute? -# - why are only overflow style properties considered and not attributes? -# - only remove unreferenced elements if they are not children of a referenced element -# - add an option to remove ids if they match the Inkscape-style of IDs -# - investigate point-reducing algorithms -# - parse transform attribute -# - if a has only one element in it, collapse the (ensure transform, etc are carried down) - -# necessary to get true division -from __future__ import division - -import os -import sys -import xml.dom.minidom -import re -import math -from svg_regex import svg_parser -from svg_transform import svg_transform_parser -import optparse -from yocto_css import parseCssString - -# Python 2.3- did not have Decimal -try: - from decimal import * -except ImportError: - print >>sys.stderr, "Scour requires Python 2.4." - -# Import Psyco if available -try: - import psyco - psyco.full() -except ImportError: - pass - -APP = 'scour' -VER = '0.26+r220' -COPYRIGHT = 'Copyright Jeff Schiller, Louis Simard, 2012' - -NS = { 'SVG': 'http://www.w3.org/2000/svg', - 'XLINK': 'http://www.w3.org/1999/xlink', - 'SODIPODI': 'http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd', - 'INKSCAPE': 'http://www.inkscape.org/namespaces/inkscape', - 'ADOBE_ILLUSTRATOR': 'http://ns.adobe.com/AdobeIllustrator/10.0/', - 'ADOBE_GRAPHS': 'http://ns.adobe.com/Graphs/1.0/', - 'ADOBE_SVG_VIEWER': 'http://ns.adobe.com/AdobeSVGViewerExtensions/3.0/', - 'ADOBE_VARIABLES': 'http://ns.adobe.com/Variables/1.0/', - 'ADOBE_SFW': 'http://ns.adobe.com/SaveForWeb/1.0/', - 'ADOBE_EXTENSIBILITY': 'http://ns.adobe.com/Extensibility/1.0/', - 'ADOBE_FLOWS': 'http://ns.adobe.com/Flows/1.0/', - 'ADOBE_IMAGE_REPLACEMENT': 'http://ns.adobe.com/ImageReplacement/1.0/', - 'ADOBE_CUSTOM': 'http://ns.adobe.com/GenericCustomNamespace/1.0/', - 'ADOBE_XPATH': 'http://ns.adobe.com/XPath/1.0/' - } - -unwanted_ns = [ NS['SODIPODI'], NS['INKSCAPE'], NS['ADOBE_ILLUSTRATOR'], - NS['ADOBE_GRAPHS'], NS['ADOBE_SVG_VIEWER'], NS['ADOBE_VARIABLES'], - NS['ADOBE_SFW'], NS['ADOBE_EXTENSIBILITY'], NS['ADOBE_FLOWS'], - NS['ADOBE_IMAGE_REPLACEMENT'], NS['ADOBE_CUSTOM'], NS['ADOBE_XPATH'] ] - -svgAttributes = [ - 'clip-rule', - 'display', - 'fill', - 'fill-opacity', - 'fill-rule', - 'filter', - 'font-family', - 'font-size', - 'font-stretch', - 'font-style', - 'font-variant', - 'font-weight', - 'line-height', - 'marker', - 'marker-end', - 'marker-mid', - 'marker-start', - 'opacity', - 'overflow', - 'stop-color', - 'stop-opacity', - 'stroke', - 'stroke-dasharray', - 'stroke-dashoffset', - 'stroke-linecap', - 'stroke-linejoin', - 'stroke-miterlimit', - 'stroke-opacity', - 'stroke-width', - 'visibility' - ] - -colors = { - 'aliceblue': 'rgb(240, 248, 255)', - 'antiquewhite': 'rgb(250, 235, 215)', - 'aqua': 'rgb( 0, 255, 255)', - 'aquamarine': 'rgb(127, 255, 212)', - 'azure': 'rgb(240, 255, 255)', - 'beige': 'rgb(245, 245, 220)', - 'bisque': 'rgb(255, 228, 196)', - 'black': 'rgb( 0, 0, 0)', - 'blanchedalmond': 'rgb(255, 235, 205)', - 'blue': 'rgb( 0, 0, 255)', - 'blueviolet': 'rgb(138, 43, 226)', - 'brown': 'rgb(165, 42, 42)', - 'burlywood': 'rgb(222, 184, 135)', - 'cadetblue': 'rgb( 95, 158, 160)', - 'chartreuse': 'rgb(127, 255, 0)', - 'chocolate': 'rgb(210, 105, 30)', - 'coral': 'rgb(255, 127, 80)', - 'cornflowerblue': 'rgb(100, 149, 237)', - 'cornsilk': 'rgb(255, 248, 220)', - 'crimson': 'rgb(220, 20, 60)', - 'cyan': 'rgb( 0, 255, 255)', - 'darkblue': 'rgb( 0, 0, 139)', - 'darkcyan': 'rgb( 0, 139, 139)', - 'darkgoldenrod': 'rgb(184, 134, 11)', - 'darkgray': 'rgb(169, 169, 169)', - 'darkgreen': 'rgb( 0, 100, 0)', - 'darkgrey': 'rgb(169, 169, 169)', - 'darkkhaki': 'rgb(189, 183, 107)', - 'darkmagenta': 'rgb(139, 0, 139)', - 'darkolivegreen': 'rgb( 85, 107, 47)', - 'darkorange': 'rgb(255, 140, 0)', - 'darkorchid': 'rgb(153, 50, 204)', - 'darkred': 'rgb(139, 0, 0)', - 'darksalmon': 'rgb(233, 150, 122)', - 'darkseagreen': 'rgb(143, 188, 143)', - 'darkslateblue': 'rgb( 72, 61, 139)', - 'darkslategray': 'rgb( 47, 79, 79)', - 'darkslategrey': 'rgb( 47, 79, 79)', - 'darkturquoise': 'rgb( 0, 206, 209)', - 'darkviolet': 'rgb(148, 0, 211)', - 'deeppink': 'rgb(255, 20, 147)', - 'deepskyblue': 'rgb( 0, 191, 255)', - 'dimgray': 'rgb(105, 105, 105)', - 'dimgrey': 'rgb(105, 105, 105)', - 'dodgerblue': 'rgb( 30, 144, 255)', - 'firebrick': 'rgb(178, 34, 34)', - 'floralwhite': 'rgb(255, 250, 240)', - 'forestgreen': 'rgb( 34, 139, 34)', - 'fuchsia': 'rgb(255, 0, 255)', - 'gainsboro': 'rgb(220, 220, 220)', - 'ghostwhite': 'rgb(248, 248, 255)', - 'gold': 'rgb(255, 215, 0)', - 'goldenrod': 'rgb(218, 165, 32)', - 'gray': 'rgb(128, 128, 128)', - 'grey': 'rgb(128, 128, 128)', - 'green': 'rgb( 0, 128, 0)', - 'greenyellow': 'rgb(173, 255, 47)', - 'honeydew': 'rgb(240, 255, 240)', - 'hotpink': 'rgb(255, 105, 180)', - 'indianred': 'rgb(205, 92, 92)', - 'indigo': 'rgb( 75, 0, 130)', - 'ivory': 'rgb(255, 255, 240)', - 'khaki': 'rgb(240, 230, 140)', - 'lavender': 'rgb(230, 230, 250)', - 'lavenderblush': 'rgb(255, 240, 245)', - 'lawngreen': 'rgb(124, 252, 0)', - 'lemonchiffon': 'rgb(255, 250, 205)', - 'lightblue': 'rgb(173, 216, 230)', - 'lightcoral': 'rgb(240, 128, 128)', - 'lightcyan': 'rgb(224, 255, 255)', - 'lightgoldenrodyellow': 'rgb(250, 250, 210)', - 'lightgray': 'rgb(211, 211, 211)', - 'lightgreen': 'rgb(144, 238, 144)', - 'lightgrey': 'rgb(211, 211, 211)', - 'lightpink': 'rgb(255, 182, 193)', - 'lightsalmon': 'rgb(255, 160, 122)', - 'lightseagreen': 'rgb( 32, 178, 170)', - 'lightskyblue': 'rgb(135, 206, 250)', - 'lightslategray': 'rgb(119, 136, 153)', - 'lightslategrey': 'rgb(119, 136, 153)', - 'lightsteelblue': 'rgb(176, 196, 222)', - 'lightyellow': 'rgb(255, 255, 224)', - 'lime': 'rgb( 0, 255, 0)', - 'limegreen': 'rgb( 50, 205, 50)', - 'linen': 'rgb(250, 240, 230)', - 'magenta': 'rgb(255, 0, 255)', - 'maroon': 'rgb(128, 0, 0)', - 'mediumaquamarine': 'rgb(102, 205, 170)', - 'mediumblue': 'rgb( 0, 0, 205)', - 'mediumorchid': 'rgb(186, 85, 211)', - 'mediumpurple': 'rgb(147, 112, 219)', - 'mediumseagreen': 'rgb( 60, 179, 113)', - 'mediumslateblue': 'rgb(123, 104, 238)', - 'mediumspringgreen': 'rgb( 0, 250, 154)', - 'mediumturquoise': 'rgb( 72, 209, 204)', - 'mediumvioletred': 'rgb(199, 21, 133)', - 'midnightblue': 'rgb( 25, 25, 112)', - 'mintcream': 'rgb(245, 255, 250)', - 'mistyrose': 'rgb(255, 228, 225)', - 'moccasin': 'rgb(255, 228, 181)', - 'navajowhite': 'rgb(255, 222, 173)', - 'navy': 'rgb( 0, 0, 128)', - 'oldlace': 'rgb(253, 245, 230)', - 'olive': 'rgb(128, 128, 0)', - 'olivedrab': 'rgb(107, 142, 35)', - 'orange': 'rgb(255, 165, 0)', - 'orangered': 'rgb(255, 69, 0)', - 'orchid': 'rgb(218, 112, 214)', - 'palegoldenrod': 'rgb(238, 232, 170)', - 'palegreen': 'rgb(152, 251, 152)', - 'paleturquoise': 'rgb(175, 238, 238)', - 'palevioletred': 'rgb(219, 112, 147)', - 'papayawhip': 'rgb(255, 239, 213)', - 'peachpuff': 'rgb(255, 218, 185)', - 'peru': 'rgb(205, 133, 63)', - 'pink': 'rgb(255, 192, 203)', - 'plum': 'rgb(221, 160, 221)', - 'powderblue': 'rgb(176, 224, 230)', - 'purple': 'rgb(128, 0, 128)', - 'rebeccapurple': 'rgb(102, 51, 153)', - 'red': 'rgb(255, 0, 0)', - 'rosybrown': 'rgb(188, 143, 143)', - 'royalblue': 'rgb( 65, 105, 225)', - 'saddlebrown': 'rgb(139, 69, 19)', - 'salmon': 'rgb(250, 128, 114)', - 'sandybrown': 'rgb(244, 164, 96)', - 'seagreen': 'rgb( 46, 139, 87)', - 'seashell': 'rgb(255, 245, 238)', - 'sienna': 'rgb(160, 82, 45)', - 'silver': 'rgb(192, 192, 192)', - 'skyblue': 'rgb(135, 206, 235)', - 'slateblue': 'rgb(106, 90, 205)', - 'slategray': 'rgb(112, 128, 144)', - 'slategrey': 'rgb(112, 128, 144)', - 'snow': 'rgb(255, 250, 250)', - 'springgreen': 'rgb( 0, 255, 127)', - 'steelblue': 'rgb( 70, 130, 180)', - 'tan': 'rgb(210, 180, 140)', - 'teal': 'rgb( 0, 128, 128)', - 'thistle': 'rgb(216, 191, 216)', - 'tomato': 'rgb(255, 99, 71)', - 'turquoise': 'rgb( 64, 224, 208)', - 'violet': 'rgb(238, 130, 238)', - 'wheat': 'rgb(245, 222, 179)', - 'white': 'rgb(255, 255, 255)', - 'whitesmoke': 'rgb(245, 245, 245)', - 'yellow': 'rgb(255, 255, 0)', - 'yellowgreen': 'rgb(154, 205, 50)', - } - -default_attributes = { # excluded all attributes with 'auto' as default - # SVG 1.1 presentation attributes - 'baseline-shift': 'baseline', - 'clip-path': 'none', - 'clip-rule': 'nonzero', - 'color': '#000', - 'color-interpolation-filters': 'linearRGB', - 'color-interpolation': 'sRGB', - 'direction': 'ltr', - 'display': 'inline', - 'enable-background': 'accumulate', - 'fill': '#000', - 'fill-opacity': '1', - 'fill-rule': 'nonzero', - 'filter': 'none', - 'flood-color': '#000', - 'flood-opacity': '1', - 'font-size-adjust': 'none', - 'font-size': 'medium', - 'font-stretch': 'normal', - 'font-style': 'normal', - 'font-variant': 'normal', - 'font-weight': 'normal', - 'glyph-orientation-horizontal': '0deg', - 'letter-spacing': 'normal', - 'lighting-color': '#fff', - 'marker': 'none', - 'marker-start': 'none', - 'marker-mid': 'none', - 'marker-end': 'none', - 'mask': 'none', - 'opacity': '1', - 'pointer-events': 'visiblePainted', - 'stop-color': '#000', - 'stop-opacity': '1', - 'stroke': 'none', - 'stroke-dasharray': 'none', - 'stroke-dashoffset': '0', - 'stroke-linecap': 'butt', - 'stroke-linejoin': 'miter', - 'stroke-miterlimit': '4', - 'stroke-opacity': '1', - 'stroke-width': '1', - 'text-anchor': 'start', - 'text-decoration': 'none', - 'unicode-bidi': 'normal', - 'visibility': 'visible', - 'word-spacing': 'normal', - 'writing-mode': 'lr-tb', - # SVG 1.2 tiny properties - 'audio-level': '1', - 'solid-color': '#000', - 'solid-opacity': '1', - 'text-align': 'start', - 'vector-effect': 'none', - 'viewport-fill': 'none', - 'viewport-fill-opacity': '1', - } - -def isSameSign(a,b): return (a <= 0 and b <= 0) or (a >= 0 and b >= 0) - -scinumber = re.compile(r"[-+]?(\d*\.?)?\d+[eE][-+]?\d+") -number = re.compile(r"[-+]?(\d*\.?)?\d+") -sciExponent = re.compile(r"[eE]([-+]?\d+)") -unit = re.compile("(em|ex|px|pt|pc|cm|mm|in|%){1,1}$") - -class Unit(object): - # Integer constants for units. - INVALID = -1 - NONE = 0 - PCT = 1 - PX = 2 - PT = 3 - PC = 4 - EM = 5 - EX = 6 - CM = 7 - MM = 8 - IN = 9 - - # String to Unit. Basically, converts unit strings to their integer constants. - s2u = { - '': NONE, - '%': PCT, - 'px': PX, - 'pt': PT, - 'pc': PC, - 'em': EM, - 'ex': EX, - 'cm': CM, - 'mm': MM, - 'in': IN, - } - - # Unit to String. Basically, converts unit integer constants to their corresponding strings. - u2s = { - NONE: '', - PCT: '%', - PX: 'px', - PT: 'pt', - PC: 'pc', - EM: 'em', - EX: 'ex', - CM: 'cm', - MM: 'mm', - IN: 'in', - } - -# @staticmethod - def get(unitstr): - if unitstr is None: return Unit.NONE - try: - return Unit.s2u[unitstr] - except KeyError: - return Unit.INVALID - -# @staticmethod - def str(unitint): - try: - return Unit.u2s[unitint] - except KeyError: - return 'INVALID' - - get = staticmethod(get) - str = staticmethod(str) - -class SVGLength(object): - def __init__(self, str): - try: # simple unitless and no scientific notation - self.value = float(str) - if int(self.value) == self.value: - self.value = int(self.value) - self.units = Unit.NONE - except ValueError: - # we know that the length string has an exponent, a unit, both or is invalid - - # parse out number, exponent and unit - self.value = 0 - unitBegin = 0 - scinum = scinumber.match(str) - if scinum != None: - # this will always match, no need to check it - numMatch = number.match(str) - expMatch = sciExponent.search(str, numMatch.start(0)) - self.value = (float(numMatch.group(0)) * - 10 ** float(expMatch.group(1))) - unitBegin = expMatch.end(1) - else: - # unit or invalid - numMatch = number.match(str) - if numMatch != None: - self.value = float(numMatch.group(0)) - unitBegin = numMatch.end(0) - - if int(self.value) == self.value: - self.value = int(self.value) - - if unitBegin != 0 : - unitMatch = unit.search(str, unitBegin) - if unitMatch != None : - self.units = Unit.get(unitMatch.group(0)) - - # invalid - else: - # TODO: this needs to set the default for the given attribute (how?) - self.value = 0 - self.units = Unit.INVALID - -def findElementsWithId(node, elems=None): - """ - Returns all elements with id attributes - """ - if elems is None: - elems = {} - id = node.getAttribute('id') - if id != '' : - elems[id] = node - if node.hasChildNodes() : - for child in node.childNodes: - # from http://www.w3.org/TR/DOM-Level-2-Core/idl-definitions.html - # we are only really interested in nodes of type Element (1) - if child.nodeType == 1 : - findElementsWithId(child, elems) - return elems - -referencingProps = ['fill', 'stroke', 'filter', 'clip-path', 'mask', 'marker-start', - 'marker-end', 'marker-mid'] - -def findReferencedElements(node, ids=None): - """ - Returns the number of times an ID is referenced as well as all elements - that reference it. node is the node at which to start the search. The - return value is a map which has the id as key and each value is an array - where the first value is a count and the second value is a list of nodes - that referenced it. - - Currently looks at fill, stroke, clip-path, mask, marker, and - xlink:href attributes. - """ - global referencingProps - if ids is None: - ids = {} - # TODO: input argument ids is clunky here (see below how it is called) - # GZ: alternative to passing dict, use **kwargs - - # if this node is a style element, parse its text into CSS - if node.nodeName == 'style' and node.namespaceURI == NS['SVG']: - # one stretch of text, please! (we could use node.normalize(), but - # this actually modifies the node, and we don't want to keep - # whitespace around if there's any) - stylesheet = "".join([child.nodeValue for child in node.childNodes]) - if stylesheet != '': - cssRules = parseCssString(stylesheet) - for rule in cssRules: - for propname in rule['properties']: - propval = rule['properties'][propname] - findReferencingProperty(node, propname, propval, ids) - return ids - - # else if xlink:href is set, then grab the id - href = node.getAttributeNS(NS['XLINK'],'href') - if href != '' and len(href) > 1 and href[0] == '#': - # we remove the hash mark from the beginning of the id - id = href[1:] - if id in ids: - ids[id][0] += 1 - ids[id][1].append(node) - else: - ids[id] = [1,[node]] - - # now get all style properties and the fill, stroke, filter attributes - styles = node.getAttribute('style').split(';') - for attr in referencingProps: - styles.append(':'.join([attr, node.getAttribute(attr)])) - - for style in styles: - propval = style.split(':') - if len(propval) == 2 : - prop = propval[0].strip() - val = propval[1].strip() - findReferencingProperty(node, prop, val, ids) - - if node.hasChildNodes() : - for child in node.childNodes: - if child.nodeType == 1 : - findReferencedElements(child, ids) - return ids - -def findReferencingProperty(node, prop, val, ids): - global referencingProps - if prop in referencingProps and val != '' : - if len(val) >= 7 and val[0:5] == 'url(#' : - id = val[5:val.find(')')] - if ids.has_key(id) : - ids[id][0] += 1 - ids[id][1].append(node) - else: - ids[id] = [1,[node]] - # if the url has a quote in it, we need to compensate - elif len(val) >= 8 : - id = None - # double-quote - if val[0:6] == 'url("#' : - id = val[6:val.find('")')] - # single-quote - elif val[0:6] == "url('#" : - id = val[6:val.find("')")] - if id != None: - if ids.has_key(id) : - ids[id][0] += 1 - ids[id][1].append(node) - else: - ids[id] = [1,[node]] - -numIDsRemoved = 0 -numElemsRemoved = 0 -numAttrsRemoved = 0 -numRastersEmbedded = 0 -numPathSegmentsReduced = 0 -numCurvesStraightened = 0 -numBytesSavedInPathData = 0 -numBytesSavedInColors = 0 -numBytesSavedInIDs = 0 -numBytesSavedInLengths = 0 -numBytesSavedInTransforms = 0 -numPointsRemovedFromPolygon = 0 -numCommentBytes = 0 - -def flattenDefs(doc): - """ - Puts all defined elements into a newly created defs in the document. This function - handles recursive defs elements. - """ - defs = doc.documentElement.getElementsByTagName('defs') - - if defs.length > 1: - topDef = doc.createElementNS(NS['SVG'], 'defs') - - for defElem in defs: - # Remove all children of this defs and put it into the topDef. - while defElem.hasChildNodes(): - topDef.appendChild(defElem.firstChild) - defElem.parentNode.removeChild(defElem) - - if topDef.hasChildNodes(): - doc.documentElement.insertBefore(topDef, doc.documentElement.firstChild) - -def removeUnusedDefs(doc, defElem, elemsToRemove=None): - if elemsToRemove is None: - elemsToRemove = [] - - identifiedElements = findElementsWithId(doc.documentElement) - referencedIDs = findReferencedElements(doc.documentElement) - - keepTags = ['font', 'style', 'metadata', 'script', 'title', 'desc'] - for elem in defElem.childNodes: - # only look at it if an element and not referenced anywhere else - if elem.nodeType == 1 and (elem.getAttribute('id') == '' or \ - (not elem.getAttribute('id') in referencedIDs)): - - # we only inspect the children of a group in a defs if the group - # is not referenced anywhere else - if elem.nodeName == 'g' and elem.namespaceURI == NS['SVG']: - elemsToRemove = removeUnusedDefs(doc, elem, elemsToRemove) - # we only remove if it is not one of our tags we always keep (see above) - elif not elem.nodeName in keepTags: - elemsToRemove.append(elem) - return elemsToRemove - -def removeUnreferencedElements(doc): - """ - Removes all unreferenced elements except for , , , , and . - Also vacuums the defs of any non-referenced renderable elements. - - Returns the number of unreferenced elements removed from the document. - """ - global numElemsRemoved - num = 0 - - # Remove certain unreferenced elements outside of defs - removeTags = ['linearGradient', 'radialGradient', 'pattern'] - identifiedElements = findElementsWithId(doc.documentElement) - referencedIDs = findReferencedElements(doc.documentElement) - - for id in identifiedElements: - if not id in referencedIDs: - goner = identifiedElements[id] - if goner != None and goner.parentNode != None and goner.nodeName in removeTags: - goner.parentNode.removeChild(goner) - num += 1 - numElemsRemoved += 1 - - # Remove most unreferenced elements inside defs - defs = doc.documentElement.getElementsByTagName('defs') - for aDef in defs: - elemsToRemove = removeUnusedDefs(doc, aDef) - for elem in elemsToRemove: - elem.parentNode.removeChild(elem) - numElemsRemoved += 1 - num += 1 - return num - -def shortenIDs(doc, unprotectedElements=None): - """ - Shortens ID names used in the document. ID names referenced the most often are assigned the - shortest ID names. - If the list unprotectedElements is provided, only IDs from this list will be shortened. - - Returns the number of bytes saved by shortening ID names in the document. - """ - num = 0 - - identifiedElements = findElementsWithId(doc.documentElement) - if unprotectedElements is None: - unprotectedElements = identifiedElements - referencedIDs = findReferencedElements(doc.documentElement) - - # Make idList (list of idnames) sorted by reference count - # descending, so the highest reference count is first. - # First check that there's actually a defining element for the current ID name. - # (Cyn: I've seen documents with #id references but no element with that ID!) - idList = [(referencedIDs[rid][0], rid) for rid in referencedIDs - if rid in unprotectedElements] - idList.sort(reverse=True) - idList = [rid for count, rid in idList] - - curIdNum = 1 - - for rid in idList: - curId = intToID(curIdNum) - # First make sure that *this* element isn't already using - # the ID name we want to give it. - if curId != rid: - # Then, skip ahead if the new ID is already in identifiedElement. - while curId in identifiedElements: - curIdNum += 1 - curId = intToID(curIdNum) - # Then go rename it. - num += renameID(doc, rid, curId, identifiedElements, referencedIDs) - curIdNum += 1 - - return num - -def intToID(idnum): - """ - Returns the ID name for the given ID number, spreadsheet-style, i.e. from a to z, - then from aa to az, ba to bz, etc., until zz. - """ - rid = '' - - while idnum > 0: - idnum -= 1 - rid = chr((idnum % 26) + ord('a')) + rid - idnum = int(idnum / 26) - - return rid - -def renameID(doc, idFrom, idTo, identifiedElements, referencedIDs): - """ - Changes the ID name from idFrom to idTo, on the declaring element - as well as all references in the document doc. - - Updates identifiedElements and referencedIDs. - Does not handle the case where idTo is already the ID name - of another element in doc. - - Returns the number of bytes saved by this replacement. - """ - - num = 0 - - definingNode = identifiedElements[idFrom] - definingNode.setAttribute("id", idTo) - del identifiedElements[idFrom] - identifiedElements[idTo] = definingNode - - referringNodes = referencedIDs[idFrom] - - # Look for the idFrom ID name in each of the referencing elements, - # exactly like findReferencedElements would. - # Cyn: Duplicated processing! - - for node in referringNodes[1]: - # if this node is a style element, parse its text into CSS - if node.nodeName == 'style' and node.namespaceURI == NS['SVG']: - # node.firstChild will be either a CDATA or a Text node now - if node.firstChild != None: - # concatenate the value of all children, in case - # there's a CDATASection node surrounded by whitespace - # nodes - # (node.normalize() will NOT work here, it only acts on Text nodes) - oldValue = "".join([child.nodeValue for child in node.childNodes]) - # not going to reparse the whole thing - newValue = oldValue.replace('url(#' + idFrom + ')', 'url(#' + idTo + ')') - newValue = newValue.replace("url(#'" + idFrom + "')", 'url(#' + idTo + ')') - newValue = newValue.replace('url(#"' + idFrom + '")', 'url(#' + idTo + ')') - # and now replace all the children with this new stylesheet. - # again, this is in case the stylesheet was a CDATASection - node.childNodes[:] = [node.ownerDocument.createTextNode(newValue)] - num += len(oldValue) - len(newValue) - - # if xlink:href is set to #idFrom, then change the id - href = node.getAttributeNS(NS['XLINK'],'href') - if href == '#' + idFrom: - node.setAttributeNS(NS['XLINK'],'href', '#' + idTo) - num += len(idFrom) - len(idTo) - - # if the style has url(#idFrom), then change the id - styles = node.getAttribute('style') - if styles != '': - newValue = styles.replace('url(#' + idFrom + ')', 'url(#' + idTo + ')') - newValue = newValue.replace("url('#" + idFrom + "')", 'url(#' + idTo + ')') - newValue = newValue.replace('url("#' + idFrom + '")', 'url(#' + idTo + ')') - node.setAttribute('style', newValue) - num += len(styles) - len(newValue) - - # now try the fill, stroke, filter attributes - for attr in referencingProps: - oldValue = node.getAttribute(attr) - if oldValue != '': - newValue = oldValue.replace('url(#' + idFrom + ')', 'url(#' + idTo + ')') - newValue = newValue.replace("url('#" + idFrom + "')", 'url(#' + idTo + ')') - newValue = newValue.replace('url("#' + idFrom + '")', 'url(#' + idTo + ')') - node.setAttribute(attr, newValue) - num += len(oldValue) - len(newValue) - - del referencedIDs[idFrom] - referencedIDs[idTo] = referringNodes - - return num - -def unprotected_ids(doc, options): - u"""Returns a list of unprotected IDs within the document doc.""" - identifiedElements = findElementsWithId(doc.documentElement) - if not (options.protect_ids_noninkscape or - options.protect_ids_list or - options.protect_ids_prefix): - return identifiedElements - if options.protect_ids_list: - protect_ids_list = options.protect_ids_list.split(",") - if options.protect_ids_prefix: - protect_ids_prefixes = options.protect_ids_prefix.split(",") - for id in identifiedElements.keys(): - protected = False - if options.protect_ids_noninkscape and not id[-1].isdigit(): - protected = True - if options.protect_ids_list and id in protect_ids_list: - protected = True - if options.protect_ids_prefix: - for prefix in protect_ids_prefixes: - if id.startswith(prefix): - protected = True - if protected: - del identifiedElements[id] - return identifiedElements - -def removeUnreferencedIDs(referencedIDs, identifiedElements): - """ - Removes the unreferenced ID attributes. - - Returns the number of ID attributes removed - """ - global numIDsRemoved - keepTags = ['font'] - num = 0; - for id in identifiedElements.keys(): - node = identifiedElements[id] - if referencedIDs.has_key(id) == False and not node.nodeName in keepTags: - node.removeAttribute('id') - numIDsRemoved += 1 - num += 1 - return num - -def removeNamespacedAttributes(node, namespaces): - global numAttrsRemoved - num = 0 - if node.nodeType == 1 : - # remove all namespace'd attributes from this element - attrList = node.attributes - attrsToRemove = [] - for attrNum in xrange(attrList.length): - attr = attrList.item(attrNum) - if attr != None and attr.namespaceURI in namespaces: - attrsToRemove.append(attr.nodeName) - for attrName in attrsToRemove : - num += 1 - numAttrsRemoved += 1 - node.removeAttribute(attrName) - - # now recurse for children - for child in node.childNodes: - num += removeNamespacedAttributes(child, namespaces) - return num - -def removeNamespacedElements(node, namespaces): - global numElemsRemoved - num = 0 - if node.nodeType == 1 : - # remove all namespace'd child nodes from this element - childList = node.childNodes - childrenToRemove = [] - for child in childList: - if child != None and child.namespaceURI in namespaces: - childrenToRemove.append(child) - for child in childrenToRemove : - num += 1 - numElemsRemoved += 1 - node.removeChild(child) - - # now recurse for children - for child in node.childNodes: - num += removeNamespacedElements(child, namespaces) - return num - -def removeMetadataElements(doc): - global numElemsRemoved - num = 0 - # clone the list, as the tag list is live from the DOM - elementsToRemove = [element for element in doc.documentElement.getElementsByTagName('metadata')] - - for element in elementsToRemove: - element.parentNode.removeChild(element) - num += 1 - numElemsRemoved += 1 - - return num - -def removeNestedGroups(node): - """ - This walks further and further down the tree, removing groups - which do not have any attributes or a title/desc child and - promoting their children up one level - """ - global numElemsRemoved - num = 0 - - groupsToRemove = [] - # Only consider elements for promotion if this element isn't a . - # (partial fix for bug 594930, required by the SVG spec however) - if not (node.nodeType == 1 and node.nodeName == 'switch'): - for child in node.childNodes: - if child.nodeName == 'g' and child.namespaceURI == NS['SVG'] and len(child.attributes) == 0: - # only collapse group if it does not have a title or desc as a direct descendant, - for grandchild in child.childNodes: - if grandchild.nodeType == 1 and grandchild.namespaceURI == NS['SVG'] and \ - grandchild.nodeName in ['title','desc']: - break - else: - groupsToRemove.append(child) - - for g in groupsToRemove: - while g.childNodes.length > 0: - g.parentNode.insertBefore(g.firstChild, g) - g.parentNode.removeChild(g) - numElemsRemoved += 1 - num += 1 - - # now recurse for children - for child in node.childNodes: - if child.nodeType == 1: - num += removeNestedGroups(child) - return num - -def moveCommonAttributesToParentGroup(elem, referencedElements): - """ - This recursively calls this function on all children of the passed in element - and then iterates over all child elements and removes common inheritable attributes - from the children and places them in the parent group. But only if the parent contains - nothing but element children and whitespace. The attributes are only removed from the - children if the children are not referenced by other elements in the document. - """ - num = 0 - - childElements = [] - # recurse first into the children (depth-first) - for child in elem.childNodes: - if child.nodeType == 1: - # only add and recurse if the child is not referenced elsewhere - if not child.getAttribute('id') in referencedElements: - childElements.append(child) - num += moveCommonAttributesToParentGroup(child, referencedElements) - # else if the parent has non-whitespace text children, do not - # try to move common attributes - elif child.nodeType == 3 and child.nodeValue.strip(): - return num - - # only process the children if there are more than one element - if len(childElements) <= 1: return num - - commonAttrs = {} - # add all inheritable properties of the first child element - # FIXME: Note there is a chance that the first child is a set/animate in which case - # its fill attribute is not what we want to look at, we should look for the first - # non-animate/set element - attrList = childElements[0].attributes - for num in xrange(attrList.length): - attr = attrList.item(num) - # this is most of the inheritable properties from http://www.w3.org/TR/SVG11/propidx.html - # and http://www.w3.org/TR/SVGTiny12/attributeTable.html - if attr.nodeName in ['clip-rule', - 'display-align', - 'fill', 'fill-opacity', 'fill-rule', - 'font', 'font-family', 'font-size', 'font-size-adjust', 'font-stretch', - 'font-style', 'font-variant', 'font-weight', - 'letter-spacing', - 'pointer-events', 'shape-rendering', - 'stroke', 'stroke-dasharray', 'stroke-dashoffset', 'stroke-linecap', 'stroke-linejoin', - 'stroke-miterlimit', 'stroke-opacity', 'stroke-width', - 'text-anchor', 'text-decoration', 'text-rendering', 'visibility', - 'word-spacing', 'writing-mode']: - # we just add all the attributes from the first child - commonAttrs[attr.nodeName] = attr.nodeValue - - # for each subsequent child element - for childNum in xrange(len(childElements)): - # skip first child - if childNum == 0: - continue - - child = childElements[childNum] - # if we are on an animateXXX/set element, ignore it (due to the 'fill' attribute) - if child.localName in ['set', 'animate', 'animateColor', 'animateTransform', 'animateMotion']: - continue - - distinctAttrs = [] - # loop through all current 'common' attributes - for name in commonAttrs.keys(): - # if this child doesn't match that attribute, schedule it for removal - if child.getAttribute(name) != commonAttrs[name]: - distinctAttrs.append(name) - # remove those attributes which are not common - for name in distinctAttrs: - del commonAttrs[name] - - # commonAttrs now has all the inheritable attributes which are common among all child elements - for name in commonAttrs.keys(): - for child in childElements: - child.removeAttribute(name) - elem.setAttribute(name, commonAttrs[name]) - - # update our statistic (we remove N*M attributes and add back in M attributes) - num += (len(childElements)-1) * len(commonAttrs) - return num - -def createGroupsForCommonAttributes(elem): - """ - Creates elements to contain runs of 3 or more - consecutive child elements having at least one common attribute. - - Common attributes are not promoted to the by this function. - This is handled by moveCommonAttributesToParentGroup. - - If all children have a common attribute, an extra is not created. - - This function acts recursively on the given element. - """ - num = 0 - global numElemsRemoved - - # TODO perhaps all of the Presentation attributes in http://www.w3.org/TR/SVG/struct.html#GElement - # could be added here - # Cyn: These attributes are the same as in moveAttributesToParentGroup, and must always be - for curAttr in ['clip-rule', - 'display-align', - 'fill', 'fill-opacity', 'fill-rule', - 'font', 'font-family', 'font-size', 'font-size-adjust', 'font-stretch', - 'font-style', 'font-variant', 'font-weight', - 'letter-spacing', - 'pointer-events', 'shape-rendering', - 'stroke', 'stroke-dasharray', 'stroke-dashoffset', 'stroke-linecap', 'stroke-linejoin', - 'stroke-miterlimit', 'stroke-opacity', 'stroke-width', - 'text-anchor', 'text-decoration', 'text-rendering', 'visibility', - 'word-spacing', 'writing-mode']: - # Iterate through the children in reverse order, so item(i) for - # items we have yet to visit still returns the correct nodes. - curChild = elem.childNodes.length - 1 - while curChild >= 0: - childNode = elem.childNodes.item(curChild) - - if childNode.nodeType == 1 and childNode.getAttribute(curAttr) != '': - # We're in a possible run! Track the value and run length. - value = childNode.getAttribute(curAttr) - runStart, runEnd = curChild, curChild - # Run elements includes only element tags, no whitespace/comments/etc. - # Later, we calculate a run length which includes these. - runElements = 1 - - # Backtrack to get all the nodes having the same - # attribute value, preserving any nodes in-between. - while runStart > 0: - nextNode = elem.childNodes.item(runStart - 1) - if nextNode.nodeType == 1: - if nextNode.getAttribute(curAttr) != value: break - else: - runElements += 1 - runStart -= 1 - else: runStart -= 1 - - if runElements >= 3: - # Include whitespace/comment/etc. nodes in the run. - while runEnd < elem.childNodes.length - 1: - if elem.childNodes.item(runEnd + 1).nodeType == 1: break - else: runEnd += 1 - - runLength = runEnd - runStart + 1 - if runLength == elem.childNodes.length: # Every child has this - # If the current parent is a already, - if elem.nodeName == 'g' and elem.namespaceURI == NS['SVG']: - # do not act altogether on this attribute; all the - # children have it in common. - # Let moveCommonAttributesToParentGroup do it. - curChild = -1 - continue - # otherwise, it might be an element, and - # even if all children have the same attribute value, - # it's going to be worth making the since - # doesn't support attributes like 'stroke'. - # Fall through. - - # Create a element from scratch. - # We need the Document for this. - document = elem.ownerDocument - group = document.createElementNS(NS['SVG'], 'g') - # Move the run of elements to the group. - # a) ADD the nodes to the new group. - group.childNodes[:] = elem.childNodes[runStart:runEnd + 1] - for child in group.childNodes: - child.parentNode = group - # b) REMOVE the nodes from the element. - elem.childNodes[runStart:runEnd + 1] = [] - # Include the group in elem's children. - elem.childNodes.insert(runStart, group) - group.parentNode = elem - num += 1 - curChild = runStart - 1 - numElemsRemoved -= 1 - else: - curChild -= 1 - else: - curChild -= 1 - - # each child gets the same treatment, recursively - for childNode in elem.childNodes: - if childNode.nodeType == 1: - num += createGroupsForCommonAttributes(childNode) - - return num - -def removeUnusedAttributesOnParent(elem): - """ - This recursively calls this function on all children of the element passed in, - then removes any unused attributes on this elem if none of the children inherit it - """ - num = 0 - - childElements = [] - # recurse first into the children (depth-first) - for child in elem.childNodes: - if child.nodeType == 1: - childElements.append(child) - num += removeUnusedAttributesOnParent(child) - - # only process the children if there are more than one element - if len(childElements) <= 1: return num - - # get all attribute values on this parent - attrList = elem.attributes - unusedAttrs = {} - for num in xrange(attrList.length): - attr = attrList.item(num) - if attr.nodeName in ['clip-rule', - 'display-align', - 'fill', 'fill-opacity', 'fill-rule', - 'font', 'font-family', 'font-size', 'font-size-adjust', 'font-stretch', - 'font-style', 'font-variant', 'font-weight', - 'letter-spacing', - 'pointer-events', 'shape-rendering', - 'stroke', 'stroke-dasharray', 'stroke-dashoffset', 'stroke-linecap', 'stroke-linejoin', - 'stroke-miterlimit', 'stroke-opacity', 'stroke-width', - 'text-anchor', 'text-decoration', 'text-rendering', 'visibility', - 'word-spacing', 'writing-mode']: - unusedAttrs[attr.nodeName] = attr.nodeValue - - # for each child, if at least one child inherits the parent's attribute, then remove - for childNum in xrange(len(childElements)): - child = childElements[childNum] - inheritedAttrs = [] - for name in unusedAttrs.keys(): - val = child.getAttribute(name) - if val == '' or val == None or val == 'inherit': - inheritedAttrs.append(name) - for a in inheritedAttrs: - del unusedAttrs[a] - - # unusedAttrs now has all the parent attributes that are unused - for name in unusedAttrs.keys(): - elem.removeAttribute(name) - num += 1 - - return num - -def removeDuplicateGradientStops(doc): - global numElemsRemoved - num = 0 - - for gradType in ['linearGradient', 'radialGradient']: - for grad in doc.getElementsByTagName(gradType): - stops = {} - stopsToRemove = [] - for stop in grad.getElementsByTagName('stop'): - # convert percentages into a floating point number - offsetU = SVGLength(stop.getAttribute('offset')) - if offsetU.units == Unit.PCT: - offset = offsetU.value / 100.0 - elif offsetU.units == Unit.NONE: - offset = offsetU.value - else: - offset = 0 - # set the stop offset value to the integer or floating point equivalent - if int(offset) == offset: stop.setAttribute('offset', str(int(offset))) - else: stop.setAttribute('offset', str(offset)) - - color = stop.getAttribute('stop-color') - opacity = stop.getAttribute('stop-opacity') - style = stop.getAttribute('style') - if stops.has_key(offset) : - oldStop = stops[offset] - if oldStop[0] == color and oldStop[1] == opacity and oldStop[2] == style: - stopsToRemove.append(stop) - stops[offset] = [color, opacity, style] - - for stop in stopsToRemove: - stop.parentNode.removeChild(stop) - num += 1 - numElemsRemoved += 1 - - # linear gradients - return num - -def collapseSinglyReferencedGradients(doc): - global numElemsRemoved - num = 0 - - identifiedElements = findElementsWithId(doc.documentElement) - - # make sure to reset the ref'ed ids for when we are running this in testscour - for rid,nodeCount in findReferencedElements(doc.documentElement).iteritems(): - count = nodeCount[0] - nodes = nodeCount[1] - # Make sure that there's actually a defining element for the current ID name. - # (Cyn: I've seen documents with #id references but no element with that ID!) - if count == 1 and rid in identifiedElements: - elem = identifiedElements[rid] - if elem != None and elem.nodeType == 1 and elem.nodeName in ['linearGradient', 'radialGradient'] \ - and elem.namespaceURI == NS['SVG']: - # found a gradient that is referenced by only 1 other element - refElem = nodes[0] - if refElem.nodeType == 1 and refElem.nodeName in ['linearGradient', 'radialGradient'] \ - and refElem.namespaceURI == NS['SVG']: - # elem is a gradient referenced by only one other gradient (refElem) - - # add the stops to the referencing gradient (this removes them from elem) - if len(refElem.getElementsByTagName('stop')) == 0: - stopsToAdd = elem.getElementsByTagName('stop') - for stop in stopsToAdd: - refElem.appendChild(stop) - - # adopt the gradientUnits, spreadMethod, gradientTransform attributes if - # they are unspecified on refElem - for attr in ['gradientUnits','spreadMethod','gradientTransform']: - if refElem.getAttribute(attr) == '' and not elem.getAttribute(attr) == '': - refElem.setAttributeNS(None, attr, elem.getAttribute(attr)) - - # if both are radialGradients, adopt elem's fx,fy,cx,cy,r attributes if - # they are unspecified on refElem - if elem.nodeName == 'radialGradient' and refElem.nodeName == 'radialGradient': - for attr in ['fx','fy','cx','cy','r']: - if refElem.getAttribute(attr) == '' and not elem.getAttribute(attr) == '': - refElem.setAttributeNS(None, attr, elem.getAttribute(attr)) - - # if both are linearGradients, adopt elem's x1,y1,x2,y2 attributes if - # they are unspecified on refElem - if elem.nodeName == 'linearGradient' and refElem.nodeName == 'linearGradient': - for attr in ['x1','y1','x2','y2']: - if refElem.getAttribute(attr) == '' and not elem.getAttribute(attr) == '': - refElem.setAttributeNS(None, attr, elem.getAttribute(attr)) - - # now remove the xlink:href from refElem - refElem.removeAttributeNS(NS['XLINK'], 'href') - - # now delete elem - elem.parentNode.removeChild(elem) - numElemsRemoved += 1 - num += 1 - return num - -def removeDuplicateGradients(doc): - global numElemsRemoved - num = 0 - - gradientsToRemove = {} - duplicateToMaster = {} - - for gradType in ['linearGradient', 'radialGradient']: - grads = doc.getElementsByTagName(gradType) - for grad in grads: - # TODO: should slice grads from 'grad' here to optimize - for ograd in grads: - # do not compare gradient to itself - if grad == ograd: continue - - # compare grad to ograd (all properties, then all stops) - # if attributes do not match, go to next gradient - someGradAttrsDoNotMatch = False - for attr in ['gradientUnits','spreadMethod','gradientTransform','x1','y1','x2','y2','cx','cy','fx','fy','r']: - if grad.getAttribute(attr) != ograd.getAttribute(attr): - someGradAttrsDoNotMatch = True - break; - - if someGradAttrsDoNotMatch: continue - - # compare xlink:href values too - if grad.getAttributeNS(NS['XLINK'], 'href') != ograd.getAttributeNS(NS['XLINK'], 'href'): - continue - - # all gradient properties match, now time to compare stops - stops = grad.getElementsByTagName('stop') - ostops = ograd.getElementsByTagName('stop') - - if stops.length != ostops.length: continue - - # now compare stops - stopsNotEqual = False - for i in xrange(stops.length): - if stopsNotEqual: break - stop = stops.item(i) - ostop = ostops.item(i) - for attr in ['offset', 'stop-color', 'stop-opacity', 'style']: - if stop.getAttribute(attr) != ostop.getAttribute(attr): - stopsNotEqual = True - break - if stopsNotEqual: continue - - # ograd is a duplicate of grad, we schedule it to be removed UNLESS - # ograd is ALREADY considered a 'master' element - if not gradientsToRemove.has_key(ograd): - if not duplicateToMaster.has_key(ograd): - if not gradientsToRemove.has_key(grad): - gradientsToRemove[grad] = [] - gradientsToRemove[grad].append( ograd ) - duplicateToMaster[ograd] = grad - - # get a collection of all elements that are referenced and their referencing elements - referencedIDs = findReferencedElements(doc.documentElement) - for masterGrad in gradientsToRemove.keys(): - master_id = masterGrad.getAttribute('id') -# print 'master='+master_id - for dupGrad in gradientsToRemove[masterGrad]: - # if the duplicate gradient no longer has a parent that means it was - # already re-mapped to another master gradient - if not dupGrad.parentNode: continue - dup_id = dupGrad.getAttribute('id') -# print 'dup='+dup_id -# print referencedIDs[dup_id] - # for each element that referenced the gradient we are going to remove - for elem in referencedIDs[dup_id][1]: - # find out which attribute referenced the duplicate gradient - for attr in ['fill', 'stroke']: - v = elem.getAttribute(attr) - if v == 'url(#'+dup_id+')' or v == 'url("#'+dup_id+'")' or v == "url('#"+dup_id+"')": - elem.setAttribute(attr, 'url(#'+master_id+')') - if elem.getAttributeNS(NS['XLINK'], 'href') == '#'+dup_id: - elem.setAttributeNS(NS['XLINK'], 'href', '#'+master_id) - styles = _getStyle(elem) - for style in styles: - v = styles[style] - if v == 'url(#'+dup_id+')' or v == 'url("#'+dup_id+'")' or v == "url('#"+dup_id+"')": - styles[style] = 'url(#'+master_id+')' - _setStyle(elem, styles) - - # now that all referencing elements have been re-mapped to the master - # it is safe to remove this gradient from the document - dupGrad.parentNode.removeChild(dupGrad) - numElemsRemoved += 1 - num += 1 - return num - -def _getStyle(node): - u"""Returns the style attribute of a node as a dictionary.""" - if node.nodeType == 1 and len(node.getAttribute('style')) > 0 : - styleMap = { } - rawStyles = node.getAttribute('style').split(';') - for style in rawStyles: - propval = style.split(':') - if len(propval) == 2 : - styleMap[propval[0].strip()] = propval[1].strip() - return styleMap - else: - return {} - -def _setStyle(node, styleMap): - u"""Sets the style attribute of a node to the dictionary ``styleMap``.""" - fixedStyle = ';'.join([prop + ':' + styleMap[prop] for prop in styleMap.keys()]) - if fixedStyle != '' : - node.setAttribute('style', fixedStyle) - elif node.getAttribute('style'): - node.removeAttribute('style') - return node - -def repairStyle(node, options): - num = 0 - styleMap = _getStyle(node) - if styleMap: - - # I've seen this enough to know that I need to correct it: - # fill: url(#linearGradient4918) rgb(0, 0, 0); - for prop in ['fill', 'stroke'] : - if styleMap.has_key(prop) : - chunk = styleMap[prop].split(') ') - if len(chunk) == 2 and (chunk[0][:5] == 'url(#' or chunk[0][:6] == 'url("#' or chunk[0][:6] == "url('#") and chunk[1] == 'rgb(0, 0, 0)' : - styleMap[prop] = chunk[0] + ')' - num += 1 - - # Here is where we can weed out unnecessary styles like: - # opacity:1 - if styleMap.has_key('opacity') : - opacity = float(styleMap['opacity']) - # if opacity='0' then all fill and stroke properties are useless, remove them - if opacity == 0.0 : - for uselessStyle in ['fill', 'fill-opacity', 'fill-rule', 'stroke', 'stroke-linejoin', - 'stroke-opacity', 'stroke-miterlimit', 'stroke-linecap', 'stroke-dasharray', - 'stroke-dashoffset', 'stroke-opacity'] : - if styleMap.has_key(uselessStyle): - del styleMap[uselessStyle] - num += 1 - - # if stroke:none, then remove all stroke-related properties (stroke-width, etc) - # TODO: should also detect if the computed value of this element is stroke="none" - if styleMap.has_key('stroke') and styleMap['stroke'] == 'none' : - for strokestyle in [ 'stroke-width', 'stroke-linejoin', 'stroke-miterlimit', - 'stroke-linecap', 'stroke-dasharray', 'stroke-dashoffset', 'stroke-opacity'] : - if styleMap.has_key(strokestyle) : - del styleMap[strokestyle] - num += 1 - # TODO: This is actually a problem if a parent element has a specified stroke - # we need to properly calculate computed values - del styleMap['stroke'] - - # if fill:none, then remove all fill-related properties (fill-rule, etc) - if styleMap.has_key('fill') and styleMap['fill'] == 'none' : - for fillstyle in [ 'fill-rule', 'fill-opacity' ] : - if styleMap.has_key(fillstyle) : - del styleMap[fillstyle] - num += 1 - - # fill-opacity: 0 - if styleMap.has_key('fill-opacity') : - fillOpacity = float(styleMap['fill-opacity']) - if fillOpacity == 0.0 : - for uselessFillStyle in [ 'fill', 'fill-rule' ] : - if styleMap.has_key(uselessFillStyle): - del styleMap[uselessFillStyle] - num += 1 - - # stroke-opacity: 0 - if styleMap.has_key('stroke-opacity') : - strokeOpacity = float(styleMap['stroke-opacity']) - if strokeOpacity == 0.0 : - for uselessStrokeStyle in [ 'stroke', 'stroke-width', 'stroke-linejoin', 'stroke-linecap', - 'stroke-dasharray', 'stroke-dashoffset' ] : - if styleMap.has_key(uselessStrokeStyle): - del styleMap[uselessStrokeStyle] - num += 1 - - # stroke-width: 0 - if styleMap.has_key('stroke-width') : - strokeWidth = SVGLength(styleMap['stroke-width']) - if strokeWidth.value == 0.0 : - for uselessStrokeStyle in [ 'stroke', 'stroke-linejoin', 'stroke-linecap', - 'stroke-dasharray', 'stroke-dashoffset', 'stroke-opacity' ] : - if styleMap.has_key(uselessStrokeStyle): - del styleMap[uselessStrokeStyle] - num += 1 - - # remove font properties for non-text elements - # I've actually observed this in real SVG content - if not mayContainTextNodes(node): - for fontstyle in [ 'font-family', 'font-size', 'font-stretch', 'font-size-adjust', - 'font-style', 'font-variant', 'font-weight', - 'letter-spacing', 'line-height', 'kerning', - 'text-align', 'text-anchor', 'text-decoration', - 'text-rendering', 'unicode-bidi', - 'word-spacing', 'writing-mode'] : - if styleMap.has_key(fontstyle) : - del styleMap[fontstyle] - num += 1 - - # remove inkscape-specific styles - # TODO: need to get a full list of these - for inkscapeStyle in ['-inkscape-font-specification']: - if styleMap.has_key(inkscapeStyle): - del styleMap[inkscapeStyle] - num += 1 - - if styleMap.has_key('overflow') : - # overflow specified on element other than svg, marker, pattern - if not node.nodeName in ['svg','marker','pattern']: - del styleMap['overflow'] - num += 1 - # it is a marker, pattern or svg - # as long as this node is not the document , then only - # remove overflow='hidden'. See - # http://www.w3.org/TR/2010/WD-SVG11-20100622/masking.html#OverflowProperty - elif node != node.ownerDocument.documentElement: - if styleMap['overflow'] == 'hidden': - del styleMap['overflow'] - num += 1 - # else if outer svg has a overflow="visible", we can remove it - elif styleMap['overflow'] == 'visible': - del styleMap['overflow'] - num += 1 - - # now if any of the properties match known SVG attributes we prefer attributes - # over style so emit them and remove them from the style map - if options.style_to_xml: - for propName in styleMap.keys() : - if propName in svgAttributes : - node.setAttribute(propName, styleMap[propName]) - del styleMap[propName] - - _setStyle(node, styleMap) - - # recurse for our child elements - for child in node.childNodes : - num += repairStyle(child,options) - - return num - -def mayContainTextNodes(node): - """ - Returns True if the passed-in node is probably a text element, or at least - one of its descendants is probably a text element. - - If False is returned, it is guaranteed that the passed-in node has no - business having text-based attributes. - - If True is returned, the passed-in node should not have its text-based - attributes removed. - """ - # Cached result of a prior call? - try: - return node.mayContainTextNodes - except AttributeError: - pass - - result = True # Default value - # Comment, text and CDATA nodes don't have attributes and aren't containers - if node.nodeType != 1: - result = False - # Non-SVG elements? Unknown elements! - elif node.namespaceURI != NS['SVG']: - result = True - # Blacklisted elements. Those are guaranteed not to be text elements. - elif node.nodeName in ['rect', 'circle', 'ellipse', 'line', 'polygon', - 'polyline', 'path', 'image', 'stop']: - result = False - # Group elements. If we're missing any here, the default of True is used. - elif node.nodeName in ['g', 'clipPath', 'marker', 'mask', 'pattern', - 'linearGradient', 'radialGradient', 'symbol']: - result = False - for child in node.childNodes: - if mayContainTextNodes(child): - result = True - # Everything else should be considered a future SVG-version text element - # at best, or an unknown element at worst. result will stay True. - - # Cache this result before returning it. - node.mayContainTextNodes = result - return result - -def taint(taintedSet, taintedAttribute): - u"""Adds an attribute to a set of attributes. - - Related attributes are also included.""" - taintedSet.add(taintedAttribute) - if taintedAttribute == 'marker': - taintedSet |= set(['marker-start', 'marker-mid', 'marker-end']) - if taintedAttribute in ['marker-start', 'marker-mid', 'marker-end']: - taintedSet.add('marker') - return taintedSet - -def removeDefaultAttributeValues(node, options, tainted=set()): - u"""'tainted' keeps a set of attributes defined in parent nodes. - - For such attributes, we don't delete attributes with default values.""" - num = 0 - if node.nodeType != 1: return 0 - - # gradientUnits: objectBoundingBox - if node.getAttribute('gradientUnits') == 'objectBoundingBox': - node.removeAttribute('gradientUnits') - num += 1 - - # spreadMethod: pad - if node.getAttribute('spreadMethod') == 'pad': - node.removeAttribute('spreadMethod') - num += 1 - - # x1: 0% - if node.getAttribute('x1') != '': - x1 = SVGLength(node.getAttribute('x1')) - if x1.value == 0: - node.removeAttribute('x1') - num += 1 - - # y1: 0% - if node.getAttribute('y1') != '': - y1 = SVGLength(node.getAttribute('y1')) - if y1.value == 0: - node.removeAttribute('y1') - num += 1 - - # x2: 100% - if node.getAttribute('x2') != '': - x2 = SVGLength(node.getAttribute('x2')) - if (x2.value == 100 and x2.units == Unit.PCT) or (x2.value == 1 and x2.units == Unit.NONE): - node.removeAttribute('x2') - num += 1 - - # y2: 0% - if node.getAttribute('y2') != '': - y2 = SVGLength(node.getAttribute('y2')) - if y2.value == 0: - node.removeAttribute('y2') - num += 1 - - # fx: equal to rx - if node.getAttribute('fx') != '': - if node.getAttribute('fx') == node.getAttribute('cx'): - node.removeAttribute('fx') - num += 1 - - # fy: equal to ry - if node.getAttribute('fy') != '': - if node.getAttribute('fy') == node.getAttribute('cy'): - node.removeAttribute('fy') - num += 1 - - # cx: 50% - if node.getAttribute('cx') != '': - cx = SVGLength(node.getAttribute('cx')) - if (cx.value == 50 and cx.units == Unit.PCT) or (cx.value == 0.5 and cx.units == Unit.NONE): - node.removeAttribute('cx') - num += 1 - - # cy: 50% - if node.getAttribute('cy') != '': - cy = SVGLength(node.getAttribute('cy')) - if (cy.value == 50 and cy.units == Unit.PCT) or (cy.value == 0.5 and cy.units == Unit.NONE): - node.removeAttribute('cy') - num += 1 - - # r: 50% - if node.getAttribute('r') != '': - r = SVGLength(node.getAttribute('r')) - if (r.value == 50 and r.units == Unit.PCT) or (r.value == 0.5 and r.units == Unit.NONE): - node.removeAttribute('r') - num += 1 - - # Summarily get rid of some more attributes - attributes = [node.attributes.item(i).nodeName - for i in range(node.attributes.length)] - for attribute in attributes: - if attribute not in tainted: - if attribute in default_attributes.keys(): - if node.getAttribute(attribute) == default_attributes[attribute]: - node.removeAttribute(attribute) - num += 1 - else: - tainted = taint(tainted, attribute) - # These attributes might also occur as styles - styles = _getStyle(node) - for attribute in styles.keys(): - if attribute not in tainted: - if attribute in default_attributes.keys(): - if styles[attribute] == default_attributes[attribute]: - del styles[attribute] - num += 1 - else: - tainted = taint(tainted, attribute) - _setStyle(node, styles) - - # recurse for our child elements - for child in node.childNodes : - num += removeDefaultAttributeValues(child, options, tainted.copy()) - - return num - -rgb = re.compile(r"\s*rgb\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)\s*\)\s*") -rgbp = re.compile(r"\s*rgb\(\s*(\d*\.?\d+)%\s*,\s*(\d*\.?\d+)%\s*,\s*(\d*\.?\d+)%\s*\)\s*") -def convertColor(value): - """ - Converts the input color string and returns a #RRGGBB (or #RGB if possible) string - """ - s = value - - if s in colors.keys(): - s = colors[s] - - rgbpMatch = rgbp.match(s) - if rgbpMatch != None : - r = int(float(rgbpMatch.group(1)) * 255.0 / 100.0) - g = int(float(rgbpMatch.group(2)) * 255.0 / 100.0) - b = int(float(rgbpMatch.group(3)) * 255.0 / 100.0) - s = '#%02x%02x%02x' % (r, g, b) - else: - rgbMatch = rgb.match(s) - if rgbMatch != None : - r = int( rgbMatch.group(1) ) - g = int( rgbMatch.group(2) ) - b = int( rgbMatch.group(3) ) - s = '#%02x%02x%02x' % (r, g, b) - - if s[0] == '#': - s = s.lower() - if len(s)==7 and s[1]==s[2] and s[3]==s[4] and s[5]==s[6]: - s = '#'+s[1]+s[3]+s[5] - - return s - -def convertColors(element) : - """ - Recursively converts all color properties into #RRGGBB format if shorter - """ - numBytes = 0 - - if element.nodeType != 1: return 0 - - # set up list of color attributes for each element type - attrsToConvert = [] - if element.nodeName in ['rect', 'circle', 'ellipse', 'polygon', \ - 'line', 'polyline', 'path', 'g', 'a']: - attrsToConvert = ['fill', 'stroke'] - elif element.nodeName in ['stop']: - attrsToConvert = ['stop-color'] - elif element.nodeName in ['solidColor']: - attrsToConvert = ['solid-color'] - - # now convert all the color formats - styles = _getStyle(element) - for attr in attrsToConvert: - oldColorValue = element.getAttribute(attr) - if oldColorValue != '': - newColorValue = convertColor(oldColorValue) - oldBytes = len(oldColorValue) - newBytes = len(newColorValue) - if oldBytes > newBytes: - element.setAttribute(attr, newColorValue) - numBytes += (oldBytes - len(element.getAttribute(attr))) - # colors might also hide in styles - if attr in styles.keys(): - oldColorValue = styles[attr] - newColorValue = convertColor(oldColorValue) - oldBytes = len(oldColorValue) - newBytes = len(newColorValue) - if oldBytes > newBytes: - styles[attr] = newColorValue - numBytes += (oldBytes - len(element.getAttribute(attr))) - _setStyle(element, styles) - - # now recurse for our child elements - for child in element.childNodes : - numBytes += convertColors(child) - - return numBytes - -# TODO: go over what this method does and see if there is a way to optimize it -# TODO: go over the performance of this method and see if I can save memory/speed by -# reusing data structures, etc -def cleanPath(element, options) : - """ - Cleans the path string (d attribute) of the element - """ - global numBytesSavedInPathData - global numPathSegmentsReduced - global numCurvesStraightened - - # this gets the parser object from svg_regex.py - oldPathStr = element.getAttribute('d') - path = svg_parser.parse(oldPathStr) - - # This determines whether the stroke has round linecaps. If it does, - # we do not want to collapse empty segments, as they are actually rendered. - withRoundLineCaps = element.getAttribute('stroke-linecap') == 'round' - - # The first command must be a moveto, and whether it's relative (m) - # or absolute (M), the first set of coordinates *is* absolute. So - # the first iteration of the loop below will get x,y and startx,starty. - - # convert absolute coordinates into relative ones. - # Reuse the data structure 'path', since we're not adding or removing subcommands. - # Also reuse the coordinate lists since we're not adding or removing any. - for pathIndex in xrange(0, len(path)): - cmd, data = path[pathIndex] # Changes to cmd don't get through to the data structure - i = 0 - # adjust abs to rel - # only the A command has some values that we don't want to adjust (radii, rotation, flags) - if cmd == 'A': - for i in xrange(i, len(data), 7): - data[i+5] -= x - data[i+6] -= y - x += data[i+5] - y += data[i+6] - path[pathIndex] = ('a', data) - elif cmd == 'a': - x += sum(data[5::7]) - y += sum(data[6::7]) - elif cmd == 'H': - for i in xrange(i, len(data)): - data[i] -= x - x += data[i] - path[pathIndex] = ('h', data) - elif cmd == 'h': - x += sum(data) - elif cmd == 'V': - for i in xrange(i, len(data)): - data[i] -= y - y += data[i] - path[pathIndex] = ('v', data) - elif cmd == 'v': - y += sum(data) - elif cmd == 'M': - startx, starty = data[0], data[1] - # If this is a path starter, don't convert its first - # coordinate to relative; that would just make it (0, 0) - if pathIndex != 0: - data[0] -= x - data[1] -= y - - x, y = startx, starty - i = 2 - for i in xrange(i, len(data), 2): - data[i] -= x - data[i+1] -= y - x += data[i] - y += data[i+1] - path[pathIndex] = ('m', data) - elif cmd in ['L','T']: - for i in xrange(i, len(data), 2): - data[i] -= x - data[i+1] -= y - x += data[i] - y += data[i+1] - path[pathIndex] = (cmd.lower(), data) - elif cmd in ['m']: - if pathIndex == 0: - # START OF PATH - this is an absolute moveto - # followed by relative linetos - startx, starty = data[0], data[1] - x, y = startx, starty - i = 2 - else: - startx = x + data[0] - starty = y + data[1] - for i in xrange(i, len(data), 2): - x += data[i] - y += data[i+1] - elif cmd in ['l','t']: - x += sum(data[0::2]) - y += sum(data[1::2]) - elif cmd in ['S','Q']: - for i in xrange(i, len(data), 4): - data[i] -= x - data[i+1] -= y - data[i+2] -= x - data[i+3] -= y - x += data[i+2] - y += data[i+3] - path[pathIndex] = (cmd.lower(), data) - elif cmd in ['s','q']: - x += sum(data[2::4]) - y += sum(data[3::4]) - elif cmd == 'C': - for i in xrange(i, len(data), 6): - data[i] -= x - data[i+1] -= y - data[i+2] -= x - data[i+3] -= y - data[i+4] -= x - data[i+5] -= y - x += data[i+4] - y += data[i+5] - path[pathIndex] = ('c', data) - elif cmd == 'c': - x += sum(data[4::6]) - y += sum(data[5::6]) - elif cmd in ['z','Z']: - x, y = startx, starty - path[pathIndex] = ('z', data) - - # remove empty segments - # Reuse the data structure 'path' and the coordinate lists, even if we're - # deleting items, because these deletions are relatively cheap. - if not withRoundLineCaps: - for pathIndex in xrange(0, len(path)): - cmd, data = path[pathIndex] - i = 0 - if cmd in ['m','l','t']: - if cmd == 'm': - # remove m0,0 segments - if pathIndex > 0 and data[0] == data[i+1] == 0: - # 'm0,0 x,y' can be replaces with 'lx,y', - # except the first m which is a required absolute moveto - path[pathIndex] = ('l', data[2:]) - numPathSegmentsReduced += 1 - else: # else skip move coordinate - i = 2 - while i < len(data): - if data[i] == data[i+1] == 0: - del data[i:i+2] - numPathSegmentsReduced += 1 - else: - i += 2 - elif cmd == 'c': - while i < len(data): - if data[i] == data[i+1] == data[i+2] == data[i+3] == data[i+4] == data[i+5] == 0: - del data[i:i+6] - numPathSegmentsReduced += 1 - else: - i += 6 - elif cmd == 'a': - while i < len(data): - if data[i+5] == data[i+6] == 0: - del data[i:i+7] - numPathSegmentsReduced += 1 - else: - i += 7 - elif cmd == 'q': - while i < len(data): - if data[i] == data[i+1] == data[i+2] == data[i+3] == 0: - del data[i:i+4] - numPathSegmentsReduced += 1 - else: - i += 4 - elif cmd in ['h','v']: - oldLen = len(data) - path[pathIndex] = (cmd, [coord for coord in data if coord != 0]) - numPathSegmentsReduced += len(path[pathIndex][1]) - oldLen - - # fixup: Delete subcommands having no coordinates. - path = [elem for elem in path if len(elem[1]) > 0 or elem[0] == 'z'] - - # convert straight curves into lines - newPath = [path[0]] - for (cmd,data) in path[1:]: - i = 0 - newData = data - if cmd == 'c': - newData = [] - while i < len(data): - # since all commands are now relative, we can think of previous point as (0,0) - # and new point (dx,dy) is (data[i+4],data[i+5]) - # eqn of line will be y = (dy/dx)*x or if dx=0 then eqn of line is x=0 - (p1x,p1y) = (data[i],data[i+1]) - (p2x,p2y) = (data[i+2],data[i+3]) - dx = data[i+4] - dy = data[i+5] - - foundStraightCurve = False - - if dx == 0: - if p1x == 0 and p2x == 0: - foundStraightCurve = True - else: - m = dy/dx - if p1y == m*p1x and p2y == m*p2x: - foundStraightCurve = True - - if foundStraightCurve: - # flush any existing curve coords first - if newData: - newPath.append( (cmd,newData) ) - newData = [] - # now create a straight line segment - newPath.append( ('l', [dx,dy]) ) - numCurvesStraightened += 1 - else: - newData.extend(data[i:i+6]) - - i += 6 - if newData or cmd == 'z' or cmd == 'Z': - newPath.append( (cmd,newData) ) - path = newPath - - # collapse all consecutive commands of the same type into one command - prevCmd = '' - prevData = [] - newPath = [] - for (cmd,data) in path: - # flush the previous command if it is not the same type as the current command - if prevCmd != '': - if cmd != prevCmd or cmd == 'm': - newPath.append( (prevCmd, prevData) ) - prevCmd = '' - prevData = [] - - # if the previous and current commands are the same type, - # or the previous command is moveto and the current is lineto, collapse, - # but only if they are not move commands (since move can contain implicit lineto commands) - if (cmd == prevCmd or (cmd == 'l' and prevCmd == 'm')) and cmd != 'm': - prevData.extend(data) - - # save last command and data - else: - prevCmd = cmd - prevData = data - # flush last command and data - if prevCmd != '': - newPath.append( (prevCmd, prevData) ) - path = newPath - - # convert to shorthand path segments where possible - newPath = [] - for (cmd,data) in path: - # convert line segments into h,v where possible - if cmd == 'l': - i = 0 - lineTuples = [] - while i < len(data): - if data[i] == 0: - # vertical - if lineTuples: - # flush the existing line command - newPath.append( ('l', lineTuples) ) - lineTuples = [] - # append the v and then the remaining line coords - newPath.append( ('v', [data[i+1]]) ) - numPathSegmentsReduced += 1 - elif data[i+1] == 0: - if lineTuples: - # flush the line command, then append the h and then the remaining line coords - newPath.append( ('l', lineTuples) ) - lineTuples = [] - newPath.append( ('h', [data[i]]) ) - numPathSegmentsReduced += 1 - else: - lineTuples.extend(data[i:i+2]) - i += 2 - if lineTuples: - newPath.append( ('l', lineTuples) ) - # also handle implied relative linetos - elif cmd == 'm': - i = 2 - lineTuples = [data[0], data[1]] - while i < len(data): - if data[i] == 0: - # vertical - if lineTuples: - # flush the existing m/l command - newPath.append( (cmd, lineTuples) ) - lineTuples = [] - cmd = 'l' # dealing with linetos now - # append the v and then the remaining line coords - newPath.append( ('v', [data[i+1]]) ) - numPathSegmentsReduced += 1 - elif data[i+1] == 0: - if lineTuples: - # flush the m/l command, then append the h and then the remaining line coords - newPath.append( (cmd, lineTuples) ) - lineTuples = [] - cmd = 'l' # dealing with linetos now - newPath.append( ('h', [data[i]]) ) - numPathSegmentsReduced += 1 - else: - lineTuples.extend(data[i:i+2]) - i += 2 - if lineTuples: - newPath.append( (cmd, lineTuples) ) - # convert Bézier curve segments into s where possible - elif cmd == 'c': - bez_ctl_pt = (0,0) - i = 0 - curveTuples = [] - while i < len(data): - # rotate by 180deg means negate both coordinates - # if the previous control point is equal then we can substitute a - # shorthand bezier command - if bez_ctl_pt[0] == data[i] and bez_ctl_pt[1] == data[i+1]: - if curveTuples: - newPath.append( ('c', curveTuples) ) - curveTuples = [] - # append the s command - newPath.append( ('s', [data[i+2], data[i+3], data[i+4], data[i+5]]) ) - numPathSegmentsReduced += 1 - else: - j = 0 - while j <= 5: - curveTuples.append(data[i+j]) - j += 1 - - # set up control point for next curve segment - bez_ctl_pt = (data[i+4]-data[i+2], data[i+5]-data[i+3]) - i += 6 - - if curveTuples: - newPath.append( ('c', curveTuples) ) - # convert quadratic curve segments into t where possible - elif cmd == 'q': - quad_ctl_pt = (0,0) - i = 0 - curveTuples = [] - while i < len(data): - if quad_ctl_pt[0] == data[i] and quad_ctl_pt[1] == data[i+1]: - if curveTuples: - newPath.append( ('q', curveTuples) ) - curveTuples = [] - # append the t command - newPath.append( ('t', [data[i+2], data[i+3]]) ) - numPathSegmentsReduced += 1 - else: - j = 0; - while j <= 3: - curveTuples.append(data[i+j]) - j += 1 - - quad_ctl_pt = (data[i+2]-data[i], data[i+3]-data[i+1]) - i += 4 - - if curveTuples: - newPath.append( ('q', curveTuples) ) - else: - newPath.append( (cmd, data) ) - path = newPath - - # for each h or v, collapse unnecessary coordinates that run in the same direction - # i.e. "h-100-100" becomes "h-200" but "h300-100" does not change - # Reuse the data structure 'path', since we're not adding or removing subcommands. - # Also reuse the coordinate lists, even if we're deleting items, because these - # deletions are relatively cheap. - for pathIndex in xrange(1, len(path)): - cmd, data = path[pathIndex] - if cmd in ['h','v'] and len(data) > 1: - coordIndex = 1 - while coordIndex < len(data): - if isSameSign(data[coordIndex - 1], data[coordIndex]): - data[coordIndex - 1] += data[coordIndex] - del data[coordIndex] - numPathSegmentsReduced += 1 - else: - coordIndex += 1 - - # it is possible that we have consecutive h, v, c, t commands now - # so again collapse all consecutive commands of the same type into one command - prevCmd = '' - prevData = [] - newPath = [path[0]] - for (cmd,data) in path[1:]: - # flush the previous command if it is not the same type as the current command - if prevCmd != '': - if cmd != prevCmd or cmd == 'm': - newPath.append( (prevCmd, prevData) ) - prevCmd = '' - prevData = [] - - # if the previous and current commands are the same type, collapse - if cmd == prevCmd and cmd != 'm': - prevData.extend(data) - - # save last command and data - else: - prevCmd = cmd - prevData = data - # flush last command and data - if prevCmd != '': - newPath.append( (prevCmd, prevData) ) - path = newPath - - newPathStr = serializePath(path, options) - numBytesSavedInPathData += ( len(oldPathStr) - len(newPathStr) ) - element.setAttribute('d', newPathStr) - -def parseListOfPoints(s): - """ - Parse string into a list of points. - - Returns a list of containing an even number of coordinate strings - """ - i = 0 - - # (wsp)? comma-or-wsp-separated coordinate pairs (wsp)? - # coordinate-pair = coordinate comma-or-wsp coordinate - # coordinate = sign? integer - # comma-wsp: (wsp+ comma? wsp*) | (comma wsp*) - ws_nums = re.split(r"\s*,?\s*", s.strip()) - nums = [] - - # also, if 100-100 is found, split it into two also - # - for i in xrange(len(ws_nums)): - negcoords = ws_nums[i].split("-") - - # this string didn't have any negative coordinates - if len(negcoords) == 1: - nums.append(negcoords[0]) - # we got negative coords - else: - for j in xrange(len(negcoords)): - if j == 0: - # first number could be positive - if negcoords[0] != '': - nums.append(negcoords[0]) - # but it could also be negative - elif len(nums) == 0: - nums.append('-' + negcoords[j]) - # otherwise all other strings will be negative - else: - # unless we accidentally split a number that was in scientific notation - # and had a negative exponent (500.00e-1) - prev = nums[len(nums)-1] - if prev[len(prev)-1] in ['e', 'E']: - nums[len(nums)-1] = prev + '-' + negcoords[j] - else: - nums.append( '-'+negcoords[j] ) - - # if we have an odd number of points, return empty - if len(nums) % 2 != 0: return [] - - # now resolve into Decimal values - i = 0 - while i < len(nums): - try: - nums[i] = getcontext().create_decimal(nums[i]) - nums[i + 1] = getcontext().create_decimal(nums[i + 1]) - except decimal.InvalidOperation: # one of the lengths had a unit or is an invalid number - return [] - - i += 2 - - return nums - -def cleanPolygon(elem, options): - """ - Remove unnecessary closing point of polygon points attribute - """ - global numPointsRemovedFromPolygon - - pts = parseListOfPoints(elem.getAttribute('points')) - N = len(pts)/2 - if N >= 2: - (startx,starty) = pts[:2] - (endx,endy) = pts[-2:] - if startx == endx and starty == endy: - del pts[-2:] - numPointsRemovedFromPolygon += 1 - elem.setAttribute('points', scourCoordinates(pts, options, True)) - -def cleanPolyline(elem, options): - """ - Scour the polyline points attribute - """ - pts = parseListOfPoints(elem.getAttribute('points')) - elem.setAttribute('points', scourCoordinates(pts, options, True)) - -def serializePath(pathObj, options): - """ - Reserializes the path data with some cleanups. - """ - # elliptical arc commands must have comma/wsp separating the coordinates - # this fixes an issue outlined in Fix https://bugs.launchpad.net/scour/+bug/412754 - return ''.join([cmd + scourCoordinates(data, options, (cmd == 'a')) for cmd, data in pathObj]) - -def serializeTransform(transformObj): - """ - Reserializes the transform data with some cleanups. - """ - return ' '.join( - [command + '(' + ' '.join( - [scourUnitlessLength(number) for number in numbers] - ) + ')' - for command, numbers in transformObj] - ) - -def scourCoordinates(data, options, forceCommaWsp = False): - """ - Serializes coordinate data with some cleanups: - - removes all trailing zeros after the decimal - - integerize coordinates if possible - - removes extraneous whitespace - - adds spaces between values in a subcommand if required (or if forceCommaWsp is True) - """ - if data != None: - newData = [] - c = 0 - previousCoord = '' - for coord in data: - scouredCoord = scourUnitlessLength(coord, needsRendererWorkaround=options.renderer_workaround) - # only need the comma if the current number starts with a digit - # (numbers can start with - without needing a comma before) - # or if forceCommaWsp is True - # or if this number starts with a dot and the previous number - # had *no* dot or exponent (so we can go like -5.5.5 for -5.5,0.5 - # and 4e4.5 for 40000,0.5) - if c > 0 and (forceCommaWsp - or scouredCoord[0].isdigit() - or (scouredCoord[0] == '.' and not ('.' in previousCoord or 'e' in previousCoord)) - ): - newData.append( ' ' ) - - # add the scoured coordinate to the path string - newData.append( scouredCoord ) - previousCoord = scouredCoord - c += 1 - - # What we need to do to work around GNOME bugs 548494, 563933 and - # 620565, which are being fixed and unfixed in Ubuntu, is - # to make sure that a dot doesn't immediately follow a command - # (so 'h50' and 'h0.5' are allowed, but not 'h.5'). - # Then, we need to add a space character after any coordinates - # having an 'e' (scientific notation), so as to have the exponent - # separate from the next number. - if options.renderer_workaround: - if len(newData) > 0: - for i in xrange(1, len(newData)): - if newData[i][0] == '-' and 'e' in newData[i - 1]: - newData[i - 1] += ' ' - return ''.join(newData) - else: - return ''.join(newData) - - return '' - -def scourLength(length): - """ - Scours a length. Accepts units. - """ - length = SVGLength(length) - - return scourUnitlessLength(length.value) + Unit.str(length.units) - -def scourUnitlessLength(length, needsRendererWorkaround=False): # length is of a numeric type - """ - Scours the numeric part of a length only. Does not accept units. - - This is faster than scourLength on elements guaranteed not to - contain units. - """ - # reduce to the proper number of digits - if not isinstance(length, Decimal): - length = getcontext().create_decimal(str(length)) - # if the value is an integer, it may still have .0[...] attached to it for some reason - # remove those - if int(length) == length: - length = getcontext().create_decimal(int(length)) - - # gather the non-scientific notation version of the coordinate. - # this may actually be in scientific notation if the value is - # sufficiently large or small, so this is a misnomer. - nonsci = unicode(length).lower().replace("e+", "e") - if not needsRendererWorkaround: - if len(nonsci) > 2 and nonsci[:2] == '0.': - nonsci = nonsci[1:] # remove the 0, leave the dot - elif len(nonsci) > 3 and nonsci[:3] == '-0.': - nonsci = '-' + nonsci[2:] # remove the 0, leave the minus and dot - - if len(nonsci) > 3: # avoid calling normalize unless strictly necessary - # and then the scientific notation version, with E+NUMBER replaced with - # just eNUMBER, since SVG accepts this. - sci = unicode(length.normalize()).lower().replace("e+", "e") - - if len(sci) < len(nonsci): return sci - else: return nonsci - else: return nonsci - -def reducePrecision(element) : - """ - Because opacities, letter spacings, stroke widths and all that don't need - to be preserved in SVG files with 9 digits of precision. - - Takes all of these attributes, in the given element node and its children, - and reduces their precision to the current Decimal context's precision. - Also checks for the attributes actually being lengths, not 'inherit', 'none' - or anything that isn't an SVGLength. - - Returns the number of bytes saved after performing these reductions. - """ - num = 0 - - styles = _getStyle(element) - for lengthAttr in ['opacity', 'flood-opacity', 'fill-opacity', - 'stroke-opacity', 'stop-opacity', 'stroke-miterlimit', - 'stroke-dashoffset', 'letter-spacing', 'word-spacing', - 'kerning', 'font-size-adjust', 'font-size', - 'stroke-width']: - val = element.getAttribute(lengthAttr) - if val != '': - valLen = SVGLength(val) - if valLen.units != Unit.INVALID: # not an absolute/relative size or inherit, can be % though - newVal = scourLength(val) - if len(newVal) < len(val): - num += len(val) - len(newVal) - element.setAttribute(lengthAttr, newVal) - # repeat for attributes hidden in styles - if lengthAttr in styles.keys(): - val = styles[lengthAttr] - valLen = SVGLength(val) - if valLen.units != Unit.INVALID: - newVal = scourLength(val) - if len(newVal) < len(val): - num += len(val) - len(newVal) - styles[lengthAttr] = newVal - _setStyle(element, styles) - - for child in element.childNodes: - if child.nodeType == 1: - num += reducePrecision(child) - - return num - -def optimizeAngle(angle): - """ - Because any rotation can be expressed within 360 degrees - of any given number, and since negative angles sometimes - are one character longer than corresponding positive angle, - we shorten the number to one in the range to [-90, 270[. - """ - # First, we put the new angle in the range ]-360, 360[. - # The modulo operator yields results with the sign of the - # divisor, so for negative dividends, we preserve the sign - # of the angle. - if angle < 0: angle %= -360 - else: angle %= 360 - # 720 degrees is unneccessary, as 360 covers all angles. - # As "-x" is shorter than "35x" and "-xxx" one character - # longer than positive angles <= 260, we constrain angle - # range to [-90, 270[ (or, equally valid: ]-100, 260]). - if angle >= 270: angle -= 360 - elif angle < -90: angle += 360 - return angle - - -def optimizeTransform(transform): - """ - Optimises a series of transformations parsed from a single - transform="" attribute. - - The transformation list is modified in-place. - """ - # FIXME: reordering these would optimize even more cases: - # first: Fold consecutive runs of the same transformation - # extra: Attempt to cast between types to create sameness: - # "matrix(0 1 -1 0 0 0) rotate(180) scale(-1)" all - # are rotations (90, 180, 180) -- thus "rotate(90)" - # second: Simplify transforms where numbers are optional. - # third: Attempt to simplify any single remaining matrix() - # - # if there's only one transformation and it's a matrix, - # try to make it a shorter non-matrix transformation - # NOTE: as matrix(a b c d e f) in SVG means the matrix: - # |¯ a c e ¯| make constants |¯ A1 A2 A3 ¯| - # | b d f | translating them | B1 B2 B3 | - # |_ 0 0 1 _| to more readable |_ 0 0 1 _| - if len(transform) == 1 and transform[0][0] == 'matrix': - matrix = A1, B1, A2, B2, A3, B3 = transform[0][1] - # |¯ 1 0 0 ¯| - # | 0 1 0 | Identity matrix (no transformation) - # |_ 0 0 1 _| - if matrix == [1, 0, 0, 1, 0, 0]: - del transform[0] - # |¯ 1 0 X ¯| - # | 0 1 Y | Translation by (X, Y). - # |_ 0 0 1 _| - elif (A1 == 1 and A2 == 0 - and B1 == 0 and B2 == 1): - transform[0] = ('translate', [A3, B3]) - # |¯ X 0 0 ¯| - # | 0 Y 0 | Scaling by (X, Y). - # |_ 0 0 1 _| - elif ( A2 == 0 and A3 == 0 - and B1 == 0 and B3 == 0): - transform[0] = ('scale', [A1, B2]) - # |¯ cos(A) -sin(A) 0 ¯| Rotation by angle A, - # | sin(A) cos(A) 0 | clockwise, about the origin. - # |_ 0 0 1 _| A is in degrees, [-180...180]. - elif (A1 == B2 and -1 <= A1 <= 1 and A3 == 0 - and -B1 == A2 and -1 <= B1 <= 1 and B3 == 0 - # as cos² A + sin² A == 1 and as decimal trig is approximate: - # FIXME: the "epsilon" term here should really be some function - # of the precision of the (sin|cos)_A terms, not 1e-15: - and abs((B1 ** 2) + (A1 ** 2) - 1) < Decimal("1e-15")): - sin_A, cos_A = B1, A1 - # while asin(A) and acos(A) both only have an 180° range - # the sign of sin(A) and cos(A) varies across quadrants, - # letting us hone in on the angle the matrix represents: - # -- => < -90 | -+ => -90..0 | ++ => 0..90 | +- => >= 90 - # - # http://en.wikipedia.org/wiki/File:Sine_cosine_plot.svg - # shows asin has the correct angle the middle quadrants: - A = Decimal(str(math.degrees(math.asin(float(sin_A))))) - if cos_A < 0: # otherwise needs adjusting from the edges - if sin_A < 0: - A = -180 - A - else: - A = 180 - A - transform[0] = ('rotate', [A]) - - # Simplify transformations where numbers are optional. - for type, args in transform: - if type == 'translate': - # Only the X coordinate is required for translations. - # If the Y coordinate is unspecified, it's 0. - if len(args) == 2 and args[1] == 0: - del args[1] - elif type == 'rotate': - args[0] = optimizeAngle(args[0]) # angle - # Only the angle is required for rotations. - # If the coordinates are unspecified, it's the origin (0, 0). - if len(args) == 3 and args[1] == args[2] == 0: - del args[1:] - elif type == 'scale': - # Only the X scaling factor is required. - # If the Y factor is unspecified, it's the same as X. - if len(args) == 2 and args[0] == args[1]: - del args[1] - - # Attempt to coalesce runs of the same transformation. - # Translations followed immediately by other translations, - # rotations followed immediately by other rotations, - # scaling followed immediately by other scaling, - # are safe to add. - # Identity skewX/skewY are safe to remove, but how do they accrete? - # |¯ 1 0 0 ¯| - # | tan(A) 1 0 | skews X coordinates by angle A - # |_ 0 0 1 _| - # - # |¯ 1 tan(A) 0 ¯| - # | 0 1 0 | skews Y coordinates by angle A - # |_ 0 0 1 _| - # - # FIXME: A matrix followed immediately by another matrix - # would be safe to multiply together, too. - i = 1 - while i < len(transform): - currType, currArgs = transform[i] - prevType, prevArgs = transform[i - 1] - if currType == prevType == 'translate': - prevArgs[0] += currArgs[0] # x - # for y, only add if the second translation has an explicit y - if len(currArgs) == 2: - if len(prevArgs) == 2: - prevArgs[1] += currArgs[1] # y - elif len(prevArgs) == 1: - prevArgs.append(currArgs[1]) # y - del transform[i] - if prevArgs[0] == prevArgs[1] == 0: - # Identity translation! - i -= 1 - del transform[i] - elif (currType == prevType == 'rotate' - and len(prevArgs) == len(currArgs) == 1): - # Only coalesce if both rotations are from the origin. - prevArgs[0] = optimizeAngle(prevArgs[0] + currArgs[0]) - del transform[i] - elif currType == prevType == 'scale': - prevArgs[0] *= currArgs[0] # x - # handle an implicit y - if len(prevArgs) == 2 and len(currArgs) == 2: - # y1 * y2 - prevArgs[1] *= currArgs[1] - elif len(prevArgs) == 1 and len(currArgs) == 2: - # create y2 = uniformscalefactor1 * y2 - prevArgs.append(prevArgs[0] * currArgs[1]) - elif len(prevArgs) == 2 and len(currArgs) == 1: - # y1 * uniformscalefactor2 - prevArgs[1] *= currArgs[0] - del transform[i] - if prevArgs[0] == prevArgs[1] == 1: - # Identity scale! - i -= 1 - del transform[i] - else: - i += 1 - - # Some fixups are needed for single-element transformation lists, since - # the loop above was to coalesce elements with their predecessors in the - # list, and thus it required 2 elements. - i = 0 - while i < len(transform): - currType, currArgs = transform[i] - if ((currType == 'skewX' or currType == 'skewY') - and len(currArgs) == 1 and currArgs[0] == 0): - # Identity skew! - del transform[i] - elif ((currType == 'rotate') - and len(currArgs) == 1 and currArgs[0] == 0): - # Identity rotation! - del transform[i] - else: - i += 1 - -def optimizeTransforms(element, options) : - """ - Attempts to optimise transform specifications on the given node and its children. - - Returns the number of bytes saved after performing these reductions. - """ - num = 0 - - for transformAttr in ['transform', 'patternTransform', 'gradientTransform']: - val = element.getAttribute(transformAttr) - if val != '': - transform = svg_transform_parser.parse(val) - - optimizeTransform(transform) - - newVal = serializeTransform(transform) - - if len(newVal) < len(val): - if len(newVal): - element.setAttribute(transformAttr, newVal) - else: - element.removeAttribute(transformAttr) - num += len(val) - len(newVal) - - for child in element.childNodes: - if child.nodeType == 1: - num += optimizeTransforms(child, options) - - return num - -def removeComments(element) : - """ - Removes comments from the element and its children. - """ - global numCommentBytes - - if isinstance(element, xml.dom.minidom.Document): - # must process the document object separately, because its - # documentElement's nodes have None as their parentNode - # iterate in reverse order to prevent mess-ups with renumbering - for index in xrange(len(element.childNodes) - 1, -1, -1): - subelement = element.childNodes[index] - if isinstance(subelement, xml.dom.minidom.Comment): - numCommentBytes += len(subelement.data) - element.removeChild(subelement) - else: - removeComments(subelement) - elif isinstance(element, xml.dom.minidom.Comment): - numCommentBytes += len(element.data) - element.parentNode.removeChild(element) - else: - # iterate in reverse order to prevent mess-ups with renumbering - for index in xrange(len(element.childNodes) - 1, -1, -1): - subelement = element.childNodes[index] - removeComments(subelement) - -def embedRasters(element, options) : - import base64 - import urllib - """ - Converts raster references to inline images. - NOTE: there are size limits to base64-encoding handling in browsers - """ - global numRastersEmbedded - - href = element.getAttributeNS(NS['XLINK'],'href') - - # if xlink:href is set, then grab the id - if href != '' and len(href) > 1: - # find if href value has filename ext - ext = os.path.splitext(os.path.basename(href))[1].lower()[1:] - - # look for 'png', 'jpg', and 'gif' extensions - if ext == 'png' or ext == 'jpg' or ext == 'gif': - - # file:// URLs denote files on the local system too - if href[:7] == 'file://': - href = href[7:] - # does the file exist? - if os.path.isfile(href): - # if this is not an absolute path, set path relative - # to script file based on input arg - infilename = '.' - if options.infilename: infilename = options.infilename - href = os.path.join(os.path.dirname(infilename), href) - - rasterdata = '' - # test if file exists locally - if os.path.isfile(href): - # open raster file as raw binary - raster = open( href, "rb") - rasterdata = raster.read() - elif href[:7] == 'http://': - webFile = urllib.urlopen( href ) - rasterdata = webFile.read() - webFile.close() - - # ... should we remove all images which don't resolve? - if rasterdata != '' : - # base64-encode raster - b64eRaster = base64.b64encode( rasterdata ) - - # set href attribute to base64-encoded equivalent - if b64eRaster != '': - # PNG and GIF both have MIME Type 'image/[ext]', but - # JPEG has MIME Type 'image/jpeg' - if ext == 'jpg': - ext = 'jpeg' - - element.setAttributeNS(NS['XLINK'], 'href', 'data:image/' + ext + ';base64,' + b64eRaster) - numRastersEmbedded += 1 - del b64eRaster - -def properlySizeDoc(docElement, options): - # get doc width and height - w = SVGLength(docElement.getAttribute('width')) - h = SVGLength(docElement.getAttribute('height')) - - # if width/height are not unitless or px then it is not ok to rewrite them into a viewBox. - # well, it may be OK for Web browsers and vector editors, but not for librsvg. - if options.renderer_workaround: - if ((w.units != Unit.NONE and w.units != Unit.PX) or - (h.units != Unit.NONE and h.units != Unit.PX)): - return - - # else we have a statically sized image and we should try to remedy that - - # parse viewBox attribute - vbSep = re.split("\\s*\\,?\\s*", docElement.getAttribute('viewBox'), 3) - # if we have a valid viewBox we need to check it - vbWidth,vbHeight = 0,0 - if len(vbSep) == 4: - try: - # if x or y are specified and non-zero then it is not ok to overwrite it - vbX = float(vbSep[0]) - vbY = float(vbSep[1]) - if vbX != 0 or vbY != 0: - return - - # if width or height are not equal to doc width/height then it is not ok to overwrite it - vbWidth = float(vbSep[2]) - vbHeight = float(vbSep[3]) - if vbWidth != w.value or vbHeight != h.value: - return - # if the viewBox did not parse properly it is invalid and ok to overwrite it - except ValueError: - pass - - # at this point it's safe to set the viewBox and remove width/height - docElement.setAttribute('viewBox', '0 0 %s %s' % (w.value, h.value)) - docElement.removeAttribute('width') - docElement.removeAttribute('height') - -def remapNamespacePrefix(node, oldprefix, newprefix): - if node == None or node.nodeType != 1: return - - if node.prefix == oldprefix: - localName = node.localName - namespace = node.namespaceURI - doc = node.ownerDocument - parent = node.parentNode - - # create a replacement node - newNode = None - if newprefix != '': - newNode = doc.createElementNS(namespace, newprefix+":"+localName) - else: - newNode = doc.createElement(localName); - - # add all the attributes - attrList = node.attributes - for i in xrange(attrList.length): - attr = attrList.item(i) - newNode.setAttributeNS( attr.namespaceURI, attr.localName, attr.nodeValue) - - # clone and add all the child nodes - for child in node.childNodes: - newNode.appendChild(child.cloneNode(True)) - - # replace old node with new node - parent.replaceChild( newNode, node ) - # set the node to the new node in the remapped namespace prefix - node = newNode - - # now do all child nodes - for child in node.childNodes : - remapNamespacePrefix(child, oldprefix, newprefix) - -def makeWellFormed(str): - xml_ents = { '<':'<', '>':'>', '&':'&', "'":''', '"':'"'} - -# starr = [] -# for c in str: -# if c in xml_ents: -# starr.append(xml_ents[c]) -# else: -# starr.append(c) - - # this list comprehension is short-form for the above for-loop: - return ''.join([xml_ents[c] if c in xml_ents else c for c in str]) - -# hand-rolled serialization function that has the following benefits: -# - pretty printing -# - somewhat judicious use of whitespace -# - ensure id attributes are first -def serializeXML(element, options, ind = 0, preserveWhitespace = False): - outParts = [] - - indent = ind - I='' - if options.indent_type == 'tab': I='\t' - elif options.indent_type == 'space': I=' ' - - outParts.extend([(I * ind), '<', element.nodeName]) - - # always serialize the id or xml:id attributes first - if element.getAttribute('id') != '': - id = element.getAttribute('id') - quot = '"' - if id.find('"') != -1: - quot = "'" - outParts.extend([' id=', quot, id, quot]) - if element.getAttribute('xml:id') != '': - id = element.getAttribute('xml:id') - quot = '"' - if id.find('"') != -1: - quot = "'" - outParts.extend([' xml:id=', quot, id, quot]) - - # now serialize the other attributes - attrList = element.attributes - for num in xrange(attrList.length) : - attr = attrList.item(num) - if attr.nodeName == 'id' or attr.nodeName == 'xml:id': continue - # if the attribute value contains a double-quote, use single-quotes - quot = '"' - if attr.nodeValue.find('"') != -1: - quot = "'" - - attrValue = makeWellFormed( attr.nodeValue ) - - outParts.append(' ') - # preserve xmlns: if it is a namespace prefix declaration - if attr.prefix != None: - outParts.extend([attr.prefix, ':']) - elif attr.namespaceURI != None: - if attr.namespaceURI == 'http://www.w3.org/2000/xmlns/' and attr.nodeName.find('xmlns') == -1: - outParts.append('xmlns:') - elif attr.namespaceURI == 'http://www.w3.org/1999/xlink': - outParts.append('xlink:') - outParts.extend([attr.localName, '=', quot, attrValue, quot]) - - if attr.nodeName == 'xml:space': - if attrValue == 'preserve': - preserveWhitespace = True - elif attrValue == 'default': - preserveWhitespace = False - - # if no children, self-close - children = element.childNodes - if children.length > 0: - outParts.append('>') - - onNewLine = False - for child in element.childNodes: - # element node - if child.nodeType == 1: - if preserveWhitespace: - outParts.append(serializeXML(child, options, 0, preserveWhitespace)) - else: - outParts.extend(['\n', serializeXML(child, options, indent + 1, preserveWhitespace)]) - onNewLine = True - # text node - elif child.nodeType == 3: - # trim it only in the case of not being a child of an element - # where whitespace might be important - if preserveWhitespace: - outParts.append(makeWellFormed(child.nodeValue)) - else: - outParts.append(makeWellFormed(child.nodeValue.strip())) - # CDATA node - elif child.nodeType == 4: - outParts.extend(['', child.nodeValue, '']) - # Comment node - elif child.nodeType == 8: - outParts.extend(['']) - # TODO: entities, processing instructions, what else? - else: # ignore the rest - pass - - if onNewLine: outParts.append(I * ind) - outParts.extend(['']) - if indent > 0: outParts.append('\n') - else: - outParts.append('/>') - if indent > 0: outParts.append('\n') - - return "".join(outParts) - -# this is the main method -# input is a string representation of the input XML -# returns a string representation of the output XML -def scourString(in_string, options=None): - if options is None: - options = _options_parser.get_default_values() - getcontext().prec = options.digits - global numAttrsRemoved - global numStylePropsFixed - global numElemsRemoved - global numBytesSavedInColors - global numCommentsRemoved - global numBytesSavedInIDs - global numBytesSavedInLengths - global numBytesSavedInTransforms - doc = xml.dom.minidom.parseString(in_string) - - # for whatever reason this does not always remove all inkscape/sodipodi attributes/elements - # on the first pass, so we do it multiple times - # does it have to do with removal of children affecting the childlist? - if options.keep_editor_data == False: - while removeNamespacedElements( doc.documentElement, unwanted_ns ) > 0 : - pass - while removeNamespacedAttributes( doc.documentElement, unwanted_ns ) > 0 : - pass - - # remove the xmlns: declarations now - xmlnsDeclsToRemove = [] - attrList = doc.documentElement.attributes - for num in xrange(attrList.length) : - if attrList.item(num).nodeValue in unwanted_ns : - xmlnsDeclsToRemove.append(attrList.item(num).nodeName) - - for attr in xmlnsDeclsToRemove : - doc.documentElement.removeAttribute(attr) - numAttrsRemoved += 1 - - # ensure namespace for SVG is declared - # TODO: what if the default namespace is something else (i.e. some valid namespace)? - if doc.documentElement.getAttribute('xmlns') != 'http://www.w3.org/2000/svg': - doc.documentElement.setAttribute('xmlns', 'http://www.w3.org/2000/svg') - # TODO: throw error or warning? - - # check for redundant SVG namespace declaration - attrList = doc.documentElement.attributes - xmlnsDeclsToRemove = [] - redundantPrefixes = [] - for i in xrange(attrList.length): - attr = attrList.item(i) - name = attr.nodeName - val = attr.nodeValue - if name[0:6] == 'xmlns:' and val == 'http://www.w3.org/2000/svg': - redundantPrefixes.append(name[6:]) - xmlnsDeclsToRemove.append(name) - - for attrName in xmlnsDeclsToRemove: - doc.documentElement.removeAttribute(attrName) - - for prefix in redundantPrefixes: - remapNamespacePrefix(doc.documentElement, prefix, '') - - if options.strip_comments: - numCommentsRemoved = removeComments(doc) - - # repair style (remove unnecessary style properties and change them into XML attributes) - numStylePropsFixed = repairStyle(doc.documentElement, options) - - # convert colors to #RRGGBB format - if options.simple_colors: - numBytesSavedInColors = convertColors(doc.documentElement) - - # remove if the user wants to - if options.remove_metadata: - removeMetadataElements(doc) - - # flattend defs elements into just one defs element - flattenDefs(doc) - - # remove unreferenced gradients/patterns outside of defs - # and most unreferenced elements inside of defs - while removeUnreferencedElements(doc) > 0: - pass - - # remove empty defs, metadata, g - # NOTE: these elements will be removed if they just have whitespace-only text nodes - for tag in ['defs', 'metadata', 'g'] : - for elem in doc.documentElement.getElementsByTagName(tag) : - removeElem = not elem.hasChildNodes() - if removeElem == False : - for child in elem.childNodes : - if child.nodeType in [1, 4, 8]: - break - elif child.nodeType == 3 and not child.nodeValue.isspace(): - break - else: - removeElem = True - if removeElem : - elem.parentNode.removeChild(elem) - numElemsRemoved += 1 - - if options.strip_ids: - bContinueLooping = True - while bContinueLooping: - identifiedElements = unprotected_ids(doc, options) - referencedIDs = findReferencedElements(doc.documentElement) - bContinueLooping = (removeUnreferencedIDs(referencedIDs, identifiedElements) > 0) - - while removeDuplicateGradientStops(doc) > 0: - pass - - # remove gradients that are only referenced by one other gradient - while collapseSinglyReferencedGradients(doc) > 0: - pass - - # remove duplicate gradients - while removeDuplicateGradients(doc) > 0: - pass - - # create elements if there are runs of elements with the same attributes. - # this MUST be before moveCommonAttributesToParentGroup. - if options.group_create: - createGroupsForCommonAttributes(doc.documentElement) - - # move common attributes to parent group - # NOTE: the if the element's immediate children - # all have the same value for an attribute, it must not - # get moved to the element. The element - # doesn't accept fill=, stroke= etc.! - referencedIds = findReferencedElements(doc.documentElement) - for child in doc.documentElement.childNodes: - numAttrsRemoved += moveCommonAttributesToParentGroup(child, referencedIds) - - # remove unused attributes from parent - numAttrsRemoved += removeUnusedAttributesOnParent(doc.documentElement) - - # Collapse groups LAST, because we've created groups. If done before - # moveAttributesToParentGroup, empty 's may remain. - if options.group_collapse: - while removeNestedGroups(doc.documentElement) > 0: - pass - - # remove unnecessary closing point of polygons and scour points - for polygon in doc.documentElement.getElementsByTagName('polygon') : - cleanPolygon(polygon, options) - - # scour points of polyline - for polyline in doc.documentElement.getElementsByTagName('polyline') : - cleanPolyline(polyline, options) - - # clean path data - for elem in doc.documentElement.getElementsByTagName('path') : - if elem.getAttribute('d') == '': - elem.parentNode.removeChild(elem) - else: - cleanPath(elem, options) - - # shorten ID names as much as possible - if options.shorten_ids: - numBytesSavedInIDs += shortenIDs(doc, unprotected_ids(doc, options)) - - # scour lengths (including coordinates) - for type in ['svg', 'image', 'rect', 'circle', 'ellipse', 'line', 'linearGradient', 'radialGradient', 'stop', 'filter']: - for elem in doc.getElementsByTagName(type): - for attr in ['x', 'y', 'width', 'height', 'cx', 'cy', 'r', 'rx', 'ry', - 'x1', 'y1', 'x2', 'y2', 'fx', 'fy', 'offset']: - if elem.getAttribute(attr) != '': - elem.setAttribute(attr, scourLength(elem.getAttribute(attr))) - - # more length scouring in this function - numBytesSavedInLengths = reducePrecision(doc.documentElement) - - # remove default values of attributes - numAttrsRemoved += removeDefaultAttributeValues(doc.documentElement, options) - - # reduce the length of transformation attributes - numBytesSavedInTransforms = optimizeTransforms(doc.documentElement, options) - - # convert rasters references to base64-encoded strings - if options.embed_rasters: - for elem in doc.documentElement.getElementsByTagName('image') : - embedRasters(elem, options) - - # properly size the SVG document (ideally width/height should be 100% with a viewBox) - if options.enable_viewboxing: - properlySizeDoc(doc.documentElement, options) - - # output the document as a pretty string with a single space for indent - # NOTE: removed pretty printing because of this problem: - # http://ronrothman.com/public/leftbraned/xml-dom-minidom-toprettyxml-and-silly-whitespace/ - # rolled our own serialize function here to save on space, put id first, customize indentation, etc -# out_string = doc.documentElement.toprettyxml(' ') - out_string = serializeXML(doc.documentElement, options) + '\n' - - # now strip out empty lines - lines = [] - # Get rid of empty lines - for line in out_string.splitlines(True): - if line.strip(): - lines.append(line) - - # return the string with its XML prolog and surrounding comments - if options.strip_xml_prolog == False: - total_output = '\n' - else: - total_output = "" - - for child in doc.childNodes: - if child.nodeType == 1: - total_output += "".join(lines) - else: # doctypes, entities, comments - total_output += child.toxml() + '\n' - - return total_output - -# used mostly by unit tests -# input is a filename -# returns the minidom doc representation of the SVG -def scourXmlFile(filename, options=None): - in_string = open(filename).read() - out_string = scourString(in_string, options) - return xml.dom.minidom.parseString(out_string.encode('utf-8')) - -# GZ: Seems most other commandline tools don't do this, is it really wanted? -class HeaderedFormatter(optparse.IndentedHelpFormatter): - """ - Show application name, version number, and copyright statement - above usage information. - """ - def format_usage(self, usage): - return "%s %s\n%s\n%s" % (APP, VER, COPYRIGHT, - optparse.IndentedHelpFormatter.format_usage(self, usage)) - -# GZ: would prefer this to be in a function or class scope, but tests etc need -# access to the defaults anyway -_options_parser = optparse.OptionParser( - usage="%prog [-i input.svg] [-o output.svg] [OPTIONS]", - description=("If the input/output files are specified with a svgz" - " extension, then compressed SVG is assumed. If the input file is not" - " specified, stdin is used. If the output file is not specified, " - " stdout is used."), - formatter=HeaderedFormatter(max_help_position=30), - version=VER) - -_options_parser.add_option("--disable-simplify-colors", - action="store_false", dest="simple_colors", default=True, - help="won't convert all colors to #RRGGBB format") -_options_parser.add_option("--disable-style-to-xml", - action="store_false", dest="style_to_xml", default=True, - help="won't convert styles into XML attributes") -_options_parser.add_option("--disable-group-collapsing", - action="store_false", dest="group_collapse", default=True, - help="won't collapse elements") -_options_parser.add_option("--create-groups", - action="store_true", dest="group_create", default=False, - help="create elements for runs of elements with identical attributes") -_options_parser.add_option("--enable-id-stripping", - action="store_true", dest="strip_ids", default=False, - help="remove all un-referenced ID attributes") -_options_parser.add_option("--enable-comment-stripping", - action="store_true", dest="strip_comments", default=False, - help="remove all comments") -_options_parser.add_option("--shorten-ids", - action="store_true", dest="shorten_ids", default=False, - help="shorten all ID attributes to the least number of letters possible") -_options_parser.add_option("--disable-embed-rasters", - action="store_false", dest="embed_rasters", default=True, - help="won't embed rasters as base64-encoded data") -_options_parser.add_option("--keep-editor-data", - action="store_true", dest="keep_editor_data", default=False, - help="won't remove Inkscape, Sodipodi or Adobe Illustrator elements and attributes") -_options_parser.add_option("--remove-metadata", - action="store_true", dest="remove_metadata", default=False, - help="remove elements (which may contain license metadata etc.)") -_options_parser.add_option("--renderer-workaround", - action="store_true", dest="renderer_workaround", default=True, - help="work around various renderer bugs (currently only librsvg) (default)") -_options_parser.add_option("--no-renderer-workaround", - action="store_false", dest="renderer_workaround", default=True, - help="do not work around various renderer bugs (currently only librsvg)") -_options_parser.add_option("--strip-xml-prolog", - action="store_true", dest="strip_xml_prolog", default=False, - help="won't output the prolog") -_options_parser.add_option("--enable-viewboxing", - action="store_true", dest="enable_viewboxing", default=False, - help="changes document width/height to 100%/100% and creates viewbox coordinates") - -# GZ: this is confusing, most people will be thinking in terms of -# decimal places, which is not what decimal precision is doing -_options_parser.add_option("-p", "--set-precision", - action="store", type=int, dest="digits", default=5, - help="set number of significant digits (default: %default)") -_options_parser.add_option("-i", - action="store", dest="infilename", help=optparse.SUPPRESS_HELP) -_options_parser.add_option("-o", - action="store", dest="outfilename", help=optparse.SUPPRESS_HELP) -_options_parser.add_option("-q", "--quiet", - action="store_true", dest="quiet", default=False, - help="suppress non-error output") -_options_parser.add_option("--indent", - action="store", type="string", dest="indent_type", default="space", - help="indentation of the output: none, space, tab (default: %default)") -_options_parser.add_option("--protect-ids-noninkscape", - action="store_true", dest="protect_ids_noninkscape", default=False, - help="Don't change IDs not ending with a digit") -_options_parser.add_option("--protect-ids-list", - action="store", type="string", dest="protect_ids_list", default=None, - help="Don't change IDs given in a comma-separated list") -_options_parser.add_option("--protect-ids-prefix", - action="store", type="string", dest="protect_ids_prefix", default=None, - help="Don't change IDs starting with the given prefix") - -def maybe_gziped_file(filename, mode="r"): - if os.path.splitext(filename)[1].lower() in (".svgz", ".gz"): - import gzip - return gzip.GzipFile(filename, mode) - return file(filename, mode) - -def parse_args(args=None): - options, rargs = _options_parser.parse_args(args) - - if rargs: - _options_parser.error("Additional arguments not handled: %r, see --help" % rargs) - if options.digits < 0: - _options_parser.error("Can't have negative significant digits, see --help") - if not options.indent_type in ["tab", "space", "none"]: - _options_parser.error("Invalid value for --indent, see --help") - if options.infilename and options.outfilename and options.infilename == options.outfilename: - _options_parser.error("Input filename is the same as output filename") - - if options.infilename: - infile = maybe_gziped_file(options.infilename) - # GZ: could catch a raised IOError here and report - else: - # GZ: could sniff for gzip compression here - infile = sys.stdin - if options.outfilename: - outfile = maybe_gziped_file(options.outfilename, "wb") - else: - outfile = sys.stdout - - return options, [infile, outfile] - -def getReport(): - return ' Number of elements removed: ' + str(numElemsRemoved) + os.linesep + \ - ' Number of attributes removed: ' + str(numAttrsRemoved) + os.linesep + \ - ' Number of unreferenced id attributes removed: ' + str(numIDsRemoved) + os.linesep + \ - ' Number of style properties fixed: ' + str(numStylePropsFixed) + os.linesep + \ - ' Number of raster images embedded inline: ' + str(numRastersEmbedded) + os.linesep + \ - ' Number of path segments reduced/removed: ' + str(numPathSegmentsReduced) + os.linesep + \ - ' Number of bytes saved in path data: ' + str(numBytesSavedInPathData) + os.linesep + \ - ' Number of bytes saved in colors: ' + str(numBytesSavedInColors) + os.linesep + \ - ' Number of points removed from polygons: ' + str(numPointsRemovedFromPolygon) + os.linesep + \ - ' Number of bytes saved in comments: ' + str(numCommentBytes) + os.linesep + \ - ' Number of bytes saved in id attributes: ' + str(numBytesSavedInIDs) + os.linesep + \ - ' Number of bytes saved in lengths: ' + str(numBytesSavedInLengths) + os.linesep + \ - ' Number of bytes saved in transformations: ' + str(numBytesSavedInTransforms) - -if __name__ == '__main__': - if sys.platform == "win32": - from time import clock as get_tick - else: - # GZ: is this different from time.time() in any way? - def get_tick(): - return os.times()[0] - - start = get_tick() - - options, (input, output) = parse_args() - - if not options.quiet: - print >>sys.stderr, "%s %s\n%s" % (APP, VER, COPYRIGHT) - - # do the work - in_string = input.read() - out_string = scourString(in_string, options).encode("UTF-8") - output.write(out_string) - - # Close input and output files - input.close() - output.close() - - end = get_tick() - - # GZ: not using globals would be good too - if not options.quiet: - print >>sys.stderr, ' File:', input.name, \ - os.linesep + ' Time taken:', str(end-start) + 's' + os.linesep, \ - getReport() - - oldsize = len(in_string) - newsize = len(out_string) - sizediff = (newsize / oldsize) * 100 - print >>sys.stderr, ' Original file size:', oldsize, 'bytes;', \ - 'new file size:', newsize, 'bytes (' + str(sizediff)[:5] + '%)' diff --git a/share/extensions/scour/Makefile.am b/share/extensions/scour/Makefile.am new file mode 100644 index 000000000..8a6f42544 --- /dev/null +++ b/share/extensions/scour/Makefile.am @@ -0,0 +1,13 @@ + +scourdir = $(datadir)/inkscape/extensions/scour + +scour_DATA = \ + scour.py \ + scour.inkscape.py \ + svg_regex.py \ + svg_transform.py \ + yocto_css.py + +EXTRA_DIST = \ + $(scour_DATA) + diff --git a/share/extensions/scour/scour.inkscape.py b/share/extensions/scour/scour.inkscape.py new file mode 100755 index 000000000..f161a09c2 --- /dev/null +++ b/share/extensions/scour/scour.inkscape.py @@ -0,0 +1,78 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +import sys, inkex +from scour import scourString + +class ScourInkscape (inkex.Effect): + + def __init__(self): + inkex.Effect.__init__(self) + self.OptionParser.add_option("--tab", + action="store", type="string", + dest="tab") + self.OptionParser.add_option("--simplify-colors", type="inkbool", + action="store", dest="simple_colors", default=True, + help="won't convert all colors to #RRGGBB format") + self.OptionParser.add_option("--style-to-xml", type="inkbool", + action="store", dest="style_to_xml", default=True, + help="won't convert styles into XML attributes") + self.OptionParser.add_option("--group-collapsing", type="inkbool", + action="store", dest="group_collapse", default=True, + help="won't collapse elements") + self.OptionParser.add_option("--create-groups", type="inkbool", + action="store", dest="group_create", default=False, + help="create elements for runs of elements with identical attributes") + self.OptionParser.add_option("--enable-id-stripping", type="inkbool", + action="store", dest="strip_ids", default=False, + help="remove all un-referenced ID attributes") + self.OptionParser.add_option("--shorten-ids", type="inkbool", + action="store", dest="shorten_ids", default=False, + help="shorten all ID attributes to the least number of letters possible") + self.OptionParser.add_option("--embed-rasters", type="inkbool", + action="store", dest="embed_rasters", default=True, + help="won't embed rasters as base64-encoded data") + self.OptionParser.add_option("--keep-editor-data", type="inkbool", + action="store", dest="keep_editor_data", default=False, + help="won't remove Inkscape, Sodipodi or Adobe Illustrator elements and attributes") + self.OptionParser.add_option("--remove-metadata", type="inkbool", + action="store", dest="remove_metadata", default=False, + help="remove elements (which may contain license metadata etc.)") + self.OptionParser.add_option("--strip-xml-prolog", type="inkbool", + action="store", dest="strip_xml_prolog", default=False, + help="won't output the prolog") + self.OptionParser.add_option("-p", "--set-precision", + action="store", type=int, dest="digits", default=5, + help="set number of significant digits (default: %default)") + self.OptionParser.add_option("--indent", + action="store", type="string", dest="indent_type", default="space", + help="indentation of the output: none, space, tab (default: %default)") + self.OptionParser.add_option("--protect-ids-noninkscape", type="inkbool", + action="store", dest="protect_ids_noninkscape", default=False, + help="don't change IDs not ending with a digit") + self.OptionParser.add_option("--protect-ids-list", + action="store", type="string", dest="protect_ids_list", default=None, + help="don't change IDs given in a comma-separated list") + self.OptionParser.add_option("--protect-ids-prefix", + action="store", type="string", dest="protect_ids_prefix", default=None, + help="don't change IDs starting with the given prefix") + self.OptionParser.add_option("--enable-viewboxing", type="inkbool", + action="store", dest="enable_viewboxing", default=False, + help="changes document width/height to 100%/100% and creates viewbox coordinates") + self.OptionParser.add_option("--enable-comment-stripping", type="inkbool", + action="store", dest="strip_comments", default=False, + help="remove all comments") + self.OptionParser.add_option("--renderer-workaround", type="inkbool", + action="store", dest="renderer_workaround", default=False, + help="work around various renderer bugs (currently only librsvg)") + + def effect(self): + input = file(self.args[0], "r") + self.options.infilename=self.args[0] + sys.stdout.write(scourString(input.read(), self.options).encode("UTF-8")) + input.close() + sys.stdout.close() + + +if __name__ == '__main__': + e = ScourInkscape() + e.affect(output=False) diff --git a/share/extensions/scour/scour.py b/share/extensions/scour/scour.py new file mode 100644 index 000000000..236529daa --- /dev/null +++ b/share/extensions/scour/scour.py @@ -0,0 +1,3235 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +# Scour +# +# Copyright 2010 Jeff Schiller +# Copyright 2010 Louis Simard +# +# This file is part of Scour, http://www.codedread.com/scour/ +# +# This library is free software; you can redistribute it and/or modify +# it either under the terms of the Apache License, Version 2.0, or, at +# your option, under the terms and conditions of the GNU General +# Public License, Version 2 or newer as published by the Free Software +# Foundation. You may obtain a copy of these Licenses at: +# +# http://www.apache.org/licenses/LICENSE-2.0 +# https://www.gnu.org/licenses/old-licenses/gpl-2.0.html +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# Notes: + +# rubys' path-crunching ideas here: http://intertwingly.net/code/svgtidy/spec.rb +# (and implemented here: http://intertwingly.net/code/svgtidy/svgtidy.rb ) + +# Yet more ideas here: http://wiki.inkscape.org/wiki/index.php/Save_Cleaned_SVG +# +# * Process Transformations +# * Collapse all group based transformations + +# Even more ideas here: http://esw.w3.org/topic/SvgTidy +# * analysis of path elements to see if rect can be used instead? (must also need to look +# at rounded corners) + +# Next Up: +# - why are marker-start, -end not removed from the style attribute? +# - why are only overflow style properties considered and not attributes? +# - only remove unreferenced elements if they are not children of a referenced element +# - add an option to remove ids if they match the Inkscape-style of IDs +# - investigate point-reducing algorithms +# - parse transform attribute +# - if a has only one element in it, collapse the (ensure transform, etc are carried down) + +# necessary to get true division +from __future__ import division + +import os +import sys +import xml.dom.minidom +import re +import math +from svg_regex import svg_parser +from svg_transform import svg_transform_parser +import optparse +from yocto_css import parseCssString + +# Python 2.3- did not have Decimal +try: + from decimal import * +except ImportError: + print >>sys.stderr, "Scour requires Python 2.4." + +# Import Psyco if available +try: + import psyco + psyco.full() +except ImportError: + pass + +APP = 'scour' +VER = '0.26+r220' +COPYRIGHT = 'Copyright Jeff Schiller, Louis Simard, 2012' + +NS = { 'SVG': 'http://www.w3.org/2000/svg', + 'XLINK': 'http://www.w3.org/1999/xlink', + 'SODIPODI': 'http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd', + 'INKSCAPE': 'http://www.inkscape.org/namespaces/inkscape', + 'ADOBE_ILLUSTRATOR': 'http://ns.adobe.com/AdobeIllustrator/10.0/', + 'ADOBE_GRAPHS': 'http://ns.adobe.com/Graphs/1.0/', + 'ADOBE_SVG_VIEWER': 'http://ns.adobe.com/AdobeSVGViewerExtensions/3.0/', + 'ADOBE_VARIABLES': 'http://ns.adobe.com/Variables/1.0/', + 'ADOBE_SFW': 'http://ns.adobe.com/SaveForWeb/1.0/', + 'ADOBE_EXTENSIBILITY': 'http://ns.adobe.com/Extensibility/1.0/', + 'ADOBE_FLOWS': 'http://ns.adobe.com/Flows/1.0/', + 'ADOBE_IMAGE_REPLACEMENT': 'http://ns.adobe.com/ImageReplacement/1.0/', + 'ADOBE_CUSTOM': 'http://ns.adobe.com/GenericCustomNamespace/1.0/', + 'ADOBE_XPATH': 'http://ns.adobe.com/XPath/1.0/' + } + +unwanted_ns = [ NS['SODIPODI'], NS['INKSCAPE'], NS['ADOBE_ILLUSTRATOR'], + NS['ADOBE_GRAPHS'], NS['ADOBE_SVG_VIEWER'], NS['ADOBE_VARIABLES'], + NS['ADOBE_SFW'], NS['ADOBE_EXTENSIBILITY'], NS['ADOBE_FLOWS'], + NS['ADOBE_IMAGE_REPLACEMENT'], NS['ADOBE_CUSTOM'], NS['ADOBE_XPATH'] ] + +svgAttributes = [ + 'clip-rule', + 'display', + 'fill', + 'fill-opacity', + 'fill-rule', + 'filter', + 'font-family', + 'font-size', + 'font-stretch', + 'font-style', + 'font-variant', + 'font-weight', + 'line-height', + 'marker', + 'marker-end', + 'marker-mid', + 'marker-start', + 'opacity', + 'overflow', + 'stop-color', + 'stop-opacity', + 'stroke', + 'stroke-dasharray', + 'stroke-dashoffset', + 'stroke-linecap', + 'stroke-linejoin', + 'stroke-miterlimit', + 'stroke-opacity', + 'stroke-width', + 'visibility' + ] + +colors = { + 'aliceblue': 'rgb(240, 248, 255)', + 'antiquewhite': 'rgb(250, 235, 215)', + 'aqua': 'rgb( 0, 255, 255)', + 'aquamarine': 'rgb(127, 255, 212)', + 'azure': 'rgb(240, 255, 255)', + 'beige': 'rgb(245, 245, 220)', + 'bisque': 'rgb(255, 228, 196)', + 'black': 'rgb( 0, 0, 0)', + 'blanchedalmond': 'rgb(255, 235, 205)', + 'blue': 'rgb( 0, 0, 255)', + 'blueviolet': 'rgb(138, 43, 226)', + 'brown': 'rgb(165, 42, 42)', + 'burlywood': 'rgb(222, 184, 135)', + 'cadetblue': 'rgb( 95, 158, 160)', + 'chartreuse': 'rgb(127, 255, 0)', + 'chocolate': 'rgb(210, 105, 30)', + 'coral': 'rgb(255, 127, 80)', + 'cornflowerblue': 'rgb(100, 149, 237)', + 'cornsilk': 'rgb(255, 248, 220)', + 'crimson': 'rgb(220, 20, 60)', + 'cyan': 'rgb( 0, 255, 255)', + 'darkblue': 'rgb( 0, 0, 139)', + 'darkcyan': 'rgb( 0, 139, 139)', + 'darkgoldenrod': 'rgb(184, 134, 11)', + 'darkgray': 'rgb(169, 169, 169)', + 'darkgreen': 'rgb( 0, 100, 0)', + 'darkgrey': 'rgb(169, 169, 169)', + 'darkkhaki': 'rgb(189, 183, 107)', + 'darkmagenta': 'rgb(139, 0, 139)', + 'darkolivegreen': 'rgb( 85, 107, 47)', + 'darkorange': 'rgb(255, 140, 0)', + 'darkorchid': 'rgb(153, 50, 204)', + 'darkred': 'rgb(139, 0, 0)', + 'darksalmon': 'rgb(233, 150, 122)', + 'darkseagreen': 'rgb(143, 188, 143)', + 'darkslateblue': 'rgb( 72, 61, 139)', + 'darkslategray': 'rgb( 47, 79, 79)', + 'darkslategrey': 'rgb( 47, 79, 79)', + 'darkturquoise': 'rgb( 0, 206, 209)', + 'darkviolet': 'rgb(148, 0, 211)', + 'deeppink': 'rgb(255, 20, 147)', + 'deepskyblue': 'rgb( 0, 191, 255)', + 'dimgray': 'rgb(105, 105, 105)', + 'dimgrey': 'rgb(105, 105, 105)', + 'dodgerblue': 'rgb( 30, 144, 255)', + 'firebrick': 'rgb(178, 34, 34)', + 'floralwhite': 'rgb(255, 250, 240)', + 'forestgreen': 'rgb( 34, 139, 34)', + 'fuchsia': 'rgb(255, 0, 255)', + 'gainsboro': 'rgb(220, 220, 220)', + 'ghostwhite': 'rgb(248, 248, 255)', + 'gold': 'rgb(255, 215, 0)', + 'goldenrod': 'rgb(218, 165, 32)', + 'gray': 'rgb(128, 128, 128)', + 'grey': 'rgb(128, 128, 128)', + 'green': 'rgb( 0, 128, 0)', + 'greenyellow': 'rgb(173, 255, 47)', + 'honeydew': 'rgb(240, 255, 240)', + 'hotpink': 'rgb(255, 105, 180)', + 'indianred': 'rgb(205, 92, 92)', + 'indigo': 'rgb( 75, 0, 130)', + 'ivory': 'rgb(255, 255, 240)', + 'khaki': 'rgb(240, 230, 140)', + 'lavender': 'rgb(230, 230, 250)', + 'lavenderblush': 'rgb(255, 240, 245)', + 'lawngreen': 'rgb(124, 252, 0)', + 'lemonchiffon': 'rgb(255, 250, 205)', + 'lightblue': 'rgb(173, 216, 230)', + 'lightcoral': 'rgb(240, 128, 128)', + 'lightcyan': 'rgb(224, 255, 255)', + 'lightgoldenrodyellow': 'rgb(250, 250, 210)', + 'lightgray': 'rgb(211, 211, 211)', + 'lightgreen': 'rgb(144, 238, 144)', + 'lightgrey': 'rgb(211, 211, 211)', + 'lightpink': 'rgb(255, 182, 193)', + 'lightsalmon': 'rgb(255, 160, 122)', + 'lightseagreen': 'rgb( 32, 178, 170)', + 'lightskyblue': 'rgb(135, 206, 250)', + 'lightslategray': 'rgb(119, 136, 153)', + 'lightslategrey': 'rgb(119, 136, 153)', + 'lightsteelblue': 'rgb(176, 196, 222)', + 'lightyellow': 'rgb(255, 255, 224)', + 'lime': 'rgb( 0, 255, 0)', + 'limegreen': 'rgb( 50, 205, 50)', + 'linen': 'rgb(250, 240, 230)', + 'magenta': 'rgb(255, 0, 255)', + 'maroon': 'rgb(128, 0, 0)', + 'mediumaquamarine': 'rgb(102, 205, 170)', + 'mediumblue': 'rgb( 0, 0, 205)', + 'mediumorchid': 'rgb(186, 85, 211)', + 'mediumpurple': 'rgb(147, 112, 219)', + 'mediumseagreen': 'rgb( 60, 179, 113)', + 'mediumslateblue': 'rgb(123, 104, 238)', + 'mediumspringgreen': 'rgb( 0, 250, 154)', + 'mediumturquoise': 'rgb( 72, 209, 204)', + 'mediumvioletred': 'rgb(199, 21, 133)', + 'midnightblue': 'rgb( 25, 25, 112)', + 'mintcream': 'rgb(245, 255, 250)', + 'mistyrose': 'rgb(255, 228, 225)', + 'moccasin': 'rgb(255, 228, 181)', + 'navajowhite': 'rgb(255, 222, 173)', + 'navy': 'rgb( 0, 0, 128)', + 'oldlace': 'rgb(253, 245, 230)', + 'olive': 'rgb(128, 128, 0)', + 'olivedrab': 'rgb(107, 142, 35)', + 'orange': 'rgb(255, 165, 0)', + 'orangered': 'rgb(255, 69, 0)', + 'orchid': 'rgb(218, 112, 214)', + 'palegoldenrod': 'rgb(238, 232, 170)', + 'palegreen': 'rgb(152, 251, 152)', + 'paleturquoise': 'rgb(175, 238, 238)', + 'palevioletred': 'rgb(219, 112, 147)', + 'papayawhip': 'rgb(255, 239, 213)', + 'peachpuff': 'rgb(255, 218, 185)', + 'peru': 'rgb(205, 133, 63)', + 'pink': 'rgb(255, 192, 203)', + 'plum': 'rgb(221, 160, 221)', + 'powderblue': 'rgb(176, 224, 230)', + 'purple': 'rgb(128, 0, 128)', + 'rebeccapurple': 'rgb(102, 51, 153)', + 'red': 'rgb(255, 0, 0)', + 'rosybrown': 'rgb(188, 143, 143)', + 'royalblue': 'rgb( 65, 105, 225)', + 'saddlebrown': 'rgb(139, 69, 19)', + 'salmon': 'rgb(250, 128, 114)', + 'sandybrown': 'rgb(244, 164, 96)', + 'seagreen': 'rgb( 46, 139, 87)', + 'seashell': 'rgb(255, 245, 238)', + 'sienna': 'rgb(160, 82, 45)', + 'silver': 'rgb(192, 192, 192)', + 'skyblue': 'rgb(135, 206, 235)', + 'slateblue': 'rgb(106, 90, 205)', + 'slategray': 'rgb(112, 128, 144)', + 'slategrey': 'rgb(112, 128, 144)', + 'snow': 'rgb(255, 250, 250)', + 'springgreen': 'rgb( 0, 255, 127)', + 'steelblue': 'rgb( 70, 130, 180)', + 'tan': 'rgb(210, 180, 140)', + 'teal': 'rgb( 0, 128, 128)', + 'thistle': 'rgb(216, 191, 216)', + 'tomato': 'rgb(255, 99, 71)', + 'turquoise': 'rgb( 64, 224, 208)', + 'violet': 'rgb(238, 130, 238)', + 'wheat': 'rgb(245, 222, 179)', + 'white': 'rgb(255, 255, 255)', + 'whitesmoke': 'rgb(245, 245, 245)', + 'yellow': 'rgb(255, 255, 0)', + 'yellowgreen': 'rgb(154, 205, 50)', + } + +default_attributes = { # excluded all attributes with 'auto' as default + # SVG 1.1 presentation attributes + 'baseline-shift': 'baseline', + 'clip-path': 'none', + 'clip-rule': 'nonzero', + 'color': '#000', + 'color-interpolation-filters': 'linearRGB', + 'color-interpolation': 'sRGB', + 'direction': 'ltr', + 'display': 'inline', + 'enable-background': 'accumulate', + 'fill': '#000', + 'fill-opacity': '1', + 'fill-rule': 'nonzero', + 'filter': 'none', + 'flood-color': '#000', + 'flood-opacity': '1', + 'font-size-adjust': 'none', + 'font-size': 'medium', + 'font-stretch': 'normal', + 'font-style': 'normal', + 'font-variant': 'normal', + 'font-weight': 'normal', + 'glyph-orientation-horizontal': '0deg', + 'letter-spacing': 'normal', + 'lighting-color': '#fff', + 'marker': 'none', + 'marker-start': 'none', + 'marker-mid': 'none', + 'marker-end': 'none', + 'mask': 'none', + 'opacity': '1', + 'pointer-events': 'visiblePainted', + 'stop-color': '#000', + 'stop-opacity': '1', + 'stroke': 'none', + 'stroke-dasharray': 'none', + 'stroke-dashoffset': '0', + 'stroke-linecap': 'butt', + 'stroke-linejoin': 'miter', + 'stroke-miterlimit': '4', + 'stroke-opacity': '1', + 'stroke-width': '1', + 'text-anchor': 'start', + 'text-decoration': 'none', + 'unicode-bidi': 'normal', + 'visibility': 'visible', + 'word-spacing': 'normal', + 'writing-mode': 'lr-tb', + # SVG 1.2 tiny properties + 'audio-level': '1', + 'solid-color': '#000', + 'solid-opacity': '1', + 'text-align': 'start', + 'vector-effect': 'none', + 'viewport-fill': 'none', + 'viewport-fill-opacity': '1', + } + +def isSameSign(a,b): return (a <= 0 and b <= 0) or (a >= 0 and b >= 0) + +scinumber = re.compile(r"[-+]?(\d*\.?)?\d+[eE][-+]?\d+") +number = re.compile(r"[-+]?(\d*\.?)?\d+") +sciExponent = re.compile(r"[eE]([-+]?\d+)") +unit = re.compile("(em|ex|px|pt|pc|cm|mm|in|%){1,1}$") + +class Unit(object): + # Integer constants for units. + INVALID = -1 + NONE = 0 + PCT = 1 + PX = 2 + PT = 3 + PC = 4 + EM = 5 + EX = 6 + CM = 7 + MM = 8 + IN = 9 + + # String to Unit. Basically, converts unit strings to their integer constants. + s2u = { + '': NONE, + '%': PCT, + 'px': PX, + 'pt': PT, + 'pc': PC, + 'em': EM, + 'ex': EX, + 'cm': CM, + 'mm': MM, + 'in': IN, + } + + # Unit to String. Basically, converts unit integer constants to their corresponding strings. + u2s = { + NONE: '', + PCT: '%', + PX: 'px', + PT: 'pt', + PC: 'pc', + EM: 'em', + EX: 'ex', + CM: 'cm', + MM: 'mm', + IN: 'in', + } + +# @staticmethod + def get(unitstr): + if unitstr is None: return Unit.NONE + try: + return Unit.s2u[unitstr] + except KeyError: + return Unit.INVALID + +# @staticmethod + def str(unitint): + try: + return Unit.u2s[unitint] + except KeyError: + return 'INVALID' + + get = staticmethod(get) + str = staticmethod(str) + +class SVGLength(object): + def __init__(self, str): + try: # simple unitless and no scientific notation + self.value = float(str) + if int(self.value) == self.value: + self.value = int(self.value) + self.units = Unit.NONE + except ValueError: + # we know that the length string has an exponent, a unit, both or is invalid + + # parse out number, exponent and unit + self.value = 0 + unitBegin = 0 + scinum = scinumber.match(str) + if scinum != None: + # this will always match, no need to check it + numMatch = number.match(str) + expMatch = sciExponent.search(str, numMatch.start(0)) + self.value = (float(numMatch.group(0)) * + 10 ** float(expMatch.group(1))) + unitBegin = expMatch.end(1) + else: + # unit or invalid + numMatch = number.match(str) + if numMatch != None: + self.value = float(numMatch.group(0)) + unitBegin = numMatch.end(0) + + if int(self.value) == self.value: + self.value = int(self.value) + + if unitBegin != 0 : + unitMatch = unit.search(str, unitBegin) + if unitMatch != None : + self.units = Unit.get(unitMatch.group(0)) + + # invalid + else: + # TODO: this needs to set the default for the given attribute (how?) + self.value = 0 + self.units = Unit.INVALID + +def findElementsWithId(node, elems=None): + """ + Returns all elements with id attributes + """ + if elems is None: + elems = {} + id = node.getAttribute('id') + if id != '' : + elems[id] = node + if node.hasChildNodes() : + for child in node.childNodes: + # from http://www.w3.org/TR/DOM-Level-2-Core/idl-definitions.html + # we are only really interested in nodes of type Element (1) + if child.nodeType == 1 : + findElementsWithId(child, elems) + return elems + +referencingProps = ['fill', 'stroke', 'filter', 'clip-path', 'mask', 'marker-start', + 'marker-end', 'marker-mid'] + +def findReferencedElements(node, ids=None): + """ + Returns the number of times an ID is referenced as well as all elements + that reference it. node is the node at which to start the search. The + return value is a map which has the id as key and each value is an array + where the first value is a count and the second value is a list of nodes + that referenced it. + + Currently looks at fill, stroke, clip-path, mask, marker, and + xlink:href attributes. + """ + global referencingProps + if ids is None: + ids = {} + # TODO: input argument ids is clunky here (see below how it is called) + # GZ: alternative to passing dict, use **kwargs + + # if this node is a style element, parse its text into CSS + if node.nodeName == 'style' and node.namespaceURI == NS['SVG']: + # one stretch of text, please! (we could use node.normalize(), but + # this actually modifies the node, and we don't want to keep + # whitespace around if there's any) + stylesheet = "".join([child.nodeValue for child in node.childNodes]) + if stylesheet != '': + cssRules = parseCssString(stylesheet) + for rule in cssRules: + for propname in rule['properties']: + propval = rule['properties'][propname] + findReferencingProperty(node, propname, propval, ids) + return ids + + # else if xlink:href is set, then grab the id + href = node.getAttributeNS(NS['XLINK'],'href') + if href != '' and len(href) > 1 and href[0] == '#': + # we remove the hash mark from the beginning of the id + id = href[1:] + if id in ids: + ids[id][0] += 1 + ids[id][1].append(node) + else: + ids[id] = [1,[node]] + + # now get all style properties and the fill, stroke, filter attributes + styles = node.getAttribute('style').split(';') + for attr in referencingProps: + styles.append(':'.join([attr, node.getAttribute(attr)])) + + for style in styles: + propval = style.split(':') + if len(propval) == 2 : + prop = propval[0].strip() + val = propval[1].strip() + findReferencingProperty(node, prop, val, ids) + + if node.hasChildNodes() : + for child in node.childNodes: + if child.nodeType == 1 : + findReferencedElements(child, ids) + return ids + +def findReferencingProperty(node, prop, val, ids): + global referencingProps + if prop in referencingProps and val != '' : + if len(val) >= 7 and val[0:5] == 'url(#' : + id = val[5:val.find(')')] + if ids.has_key(id) : + ids[id][0] += 1 + ids[id][1].append(node) + else: + ids[id] = [1,[node]] + # if the url has a quote in it, we need to compensate + elif len(val) >= 8 : + id = None + # double-quote + if val[0:6] == 'url("#' : + id = val[6:val.find('")')] + # single-quote + elif val[0:6] == "url('#" : + id = val[6:val.find("')")] + if id != None: + if ids.has_key(id) : + ids[id][0] += 1 + ids[id][1].append(node) + else: + ids[id] = [1,[node]] + +numIDsRemoved = 0 +numElemsRemoved = 0 +numAttrsRemoved = 0 +numRastersEmbedded = 0 +numPathSegmentsReduced = 0 +numCurvesStraightened = 0 +numBytesSavedInPathData = 0 +numBytesSavedInColors = 0 +numBytesSavedInIDs = 0 +numBytesSavedInLengths = 0 +numBytesSavedInTransforms = 0 +numPointsRemovedFromPolygon = 0 +numCommentBytes = 0 + +def flattenDefs(doc): + """ + Puts all defined elements into a newly created defs in the document. This function + handles recursive defs elements. + """ + defs = doc.documentElement.getElementsByTagName('defs') + + if defs.length > 1: + topDef = doc.createElementNS(NS['SVG'], 'defs') + + for defElem in defs: + # Remove all children of this defs and put it into the topDef. + while defElem.hasChildNodes(): + topDef.appendChild(defElem.firstChild) + defElem.parentNode.removeChild(defElem) + + if topDef.hasChildNodes(): + doc.documentElement.insertBefore(topDef, doc.documentElement.firstChild) + +def removeUnusedDefs(doc, defElem, elemsToRemove=None): + if elemsToRemove is None: + elemsToRemove = [] + + identifiedElements = findElementsWithId(doc.documentElement) + referencedIDs = findReferencedElements(doc.documentElement) + + keepTags = ['font', 'style', 'metadata', 'script', 'title', 'desc'] + for elem in defElem.childNodes: + # only look at it if an element and not referenced anywhere else + if elem.nodeType == 1 and (elem.getAttribute('id') == '' or \ + (not elem.getAttribute('id') in referencedIDs)): + + # we only inspect the children of a group in a defs if the group + # is not referenced anywhere else + if elem.nodeName == 'g' and elem.namespaceURI == NS['SVG']: + elemsToRemove = removeUnusedDefs(doc, elem, elemsToRemove) + # we only remove if it is not one of our tags we always keep (see above) + elif not elem.nodeName in keepTags: + elemsToRemove.append(elem) + return elemsToRemove + +def removeUnreferencedElements(doc): + """ + Removes all unreferenced elements except for , , , , and . + Also vacuums the defs of any non-referenced renderable elements. + + Returns the number of unreferenced elements removed from the document. + """ + global numElemsRemoved + num = 0 + + # Remove certain unreferenced elements outside of defs + removeTags = ['linearGradient', 'radialGradient', 'pattern'] + identifiedElements = findElementsWithId(doc.documentElement) + referencedIDs = findReferencedElements(doc.documentElement) + + for id in identifiedElements: + if not id in referencedIDs: + goner = identifiedElements[id] + if goner != None and goner.parentNode != None and goner.nodeName in removeTags: + goner.parentNode.removeChild(goner) + num += 1 + numElemsRemoved += 1 + + # Remove most unreferenced elements inside defs + defs = doc.documentElement.getElementsByTagName('defs') + for aDef in defs: + elemsToRemove = removeUnusedDefs(doc, aDef) + for elem in elemsToRemove: + elem.parentNode.removeChild(elem) + numElemsRemoved += 1 + num += 1 + return num + +def shortenIDs(doc, unprotectedElements=None): + """ + Shortens ID names used in the document. ID names referenced the most often are assigned the + shortest ID names. + If the list unprotectedElements is provided, only IDs from this list will be shortened. + + Returns the number of bytes saved by shortening ID names in the document. + """ + num = 0 + + identifiedElements = findElementsWithId(doc.documentElement) + if unprotectedElements is None: + unprotectedElements = identifiedElements + referencedIDs = findReferencedElements(doc.documentElement) + + # Make idList (list of idnames) sorted by reference count + # descending, so the highest reference count is first. + # First check that there's actually a defining element for the current ID name. + # (Cyn: I've seen documents with #id references but no element with that ID!) + idList = [(referencedIDs[rid][0], rid) for rid in referencedIDs + if rid in unprotectedElements] + idList.sort(reverse=True) + idList = [rid for count, rid in idList] + + curIdNum = 1 + + for rid in idList: + curId = intToID(curIdNum) + # First make sure that *this* element isn't already using + # the ID name we want to give it. + if curId != rid: + # Then, skip ahead if the new ID is already in identifiedElement. + while curId in identifiedElements: + curIdNum += 1 + curId = intToID(curIdNum) + # Then go rename it. + num += renameID(doc, rid, curId, identifiedElements, referencedIDs) + curIdNum += 1 + + return num + +def intToID(idnum): + """ + Returns the ID name for the given ID number, spreadsheet-style, i.e. from a to z, + then from aa to az, ba to bz, etc., until zz. + """ + rid = '' + + while idnum > 0: + idnum -= 1 + rid = chr((idnum % 26) + ord('a')) + rid + idnum = int(idnum / 26) + + return rid + +def renameID(doc, idFrom, idTo, identifiedElements, referencedIDs): + """ + Changes the ID name from idFrom to idTo, on the declaring element + as well as all references in the document doc. + + Updates identifiedElements and referencedIDs. + Does not handle the case where idTo is already the ID name + of another element in doc. + + Returns the number of bytes saved by this replacement. + """ + + num = 0 + + definingNode = identifiedElements[idFrom] + definingNode.setAttribute("id", idTo) + del identifiedElements[idFrom] + identifiedElements[idTo] = definingNode + + referringNodes = referencedIDs[idFrom] + + # Look for the idFrom ID name in each of the referencing elements, + # exactly like findReferencedElements would. + # Cyn: Duplicated processing! + + for node in referringNodes[1]: + # if this node is a style element, parse its text into CSS + if node.nodeName == 'style' and node.namespaceURI == NS['SVG']: + # node.firstChild will be either a CDATA or a Text node now + if node.firstChild != None: + # concatenate the value of all children, in case + # there's a CDATASection node surrounded by whitespace + # nodes + # (node.normalize() will NOT work here, it only acts on Text nodes) + oldValue = "".join([child.nodeValue for child in node.childNodes]) + # not going to reparse the whole thing + newValue = oldValue.replace('url(#' + idFrom + ')', 'url(#' + idTo + ')') + newValue = newValue.replace("url(#'" + idFrom + "')", 'url(#' + idTo + ')') + newValue = newValue.replace('url(#"' + idFrom + '")', 'url(#' + idTo + ')') + # and now replace all the children with this new stylesheet. + # again, this is in case the stylesheet was a CDATASection + node.childNodes[:] = [node.ownerDocument.createTextNode(newValue)] + num += len(oldValue) - len(newValue) + + # if xlink:href is set to #idFrom, then change the id + href = node.getAttributeNS(NS['XLINK'],'href') + if href == '#' + idFrom: + node.setAttributeNS(NS['XLINK'],'href', '#' + idTo) + num += len(idFrom) - len(idTo) + + # if the style has url(#idFrom), then change the id + styles = node.getAttribute('style') + if styles != '': + newValue = styles.replace('url(#' + idFrom + ')', 'url(#' + idTo + ')') + newValue = newValue.replace("url('#" + idFrom + "')", 'url(#' + idTo + ')') + newValue = newValue.replace('url("#' + idFrom + '")', 'url(#' + idTo + ')') + node.setAttribute('style', newValue) + num += len(styles) - len(newValue) + + # now try the fill, stroke, filter attributes + for attr in referencingProps: + oldValue = node.getAttribute(attr) + if oldValue != '': + newValue = oldValue.replace('url(#' + idFrom + ')', 'url(#' + idTo + ')') + newValue = newValue.replace("url('#" + idFrom + "')", 'url(#' + idTo + ')') + newValue = newValue.replace('url("#' + idFrom + '")', 'url(#' + idTo + ')') + node.setAttribute(attr, newValue) + num += len(oldValue) - len(newValue) + + del referencedIDs[idFrom] + referencedIDs[idTo] = referringNodes + + return num + +def unprotected_ids(doc, options): + u"""Returns a list of unprotected IDs within the document doc.""" + identifiedElements = findElementsWithId(doc.documentElement) + if not (options.protect_ids_noninkscape or + options.protect_ids_list or + options.protect_ids_prefix): + return identifiedElements + if options.protect_ids_list: + protect_ids_list = options.protect_ids_list.split(",") + if options.protect_ids_prefix: + protect_ids_prefixes = options.protect_ids_prefix.split(",") + for id in identifiedElements.keys(): + protected = False + if options.protect_ids_noninkscape and not id[-1].isdigit(): + protected = True + if options.protect_ids_list and id in protect_ids_list: + protected = True + if options.protect_ids_prefix: + for prefix in protect_ids_prefixes: + if id.startswith(prefix): + protected = True + if protected: + del identifiedElements[id] + return identifiedElements + +def removeUnreferencedIDs(referencedIDs, identifiedElements): + """ + Removes the unreferenced ID attributes. + + Returns the number of ID attributes removed + """ + global numIDsRemoved + keepTags = ['font'] + num = 0; + for id in identifiedElements.keys(): + node = identifiedElements[id] + if referencedIDs.has_key(id) == False and not node.nodeName in keepTags: + node.removeAttribute('id') + numIDsRemoved += 1 + num += 1 + return num + +def removeNamespacedAttributes(node, namespaces): + global numAttrsRemoved + num = 0 + if node.nodeType == 1 : + # remove all namespace'd attributes from this element + attrList = node.attributes + attrsToRemove = [] + for attrNum in xrange(attrList.length): + attr = attrList.item(attrNum) + if attr != None and attr.namespaceURI in namespaces: + attrsToRemove.append(attr.nodeName) + for attrName in attrsToRemove : + num += 1 + numAttrsRemoved += 1 + node.removeAttribute(attrName) + + # now recurse for children + for child in node.childNodes: + num += removeNamespacedAttributes(child, namespaces) + return num + +def removeNamespacedElements(node, namespaces): + global numElemsRemoved + num = 0 + if node.nodeType == 1 : + # remove all namespace'd child nodes from this element + childList = node.childNodes + childrenToRemove = [] + for child in childList: + if child != None and child.namespaceURI in namespaces: + childrenToRemove.append(child) + for child in childrenToRemove : + num += 1 + numElemsRemoved += 1 + node.removeChild(child) + + # now recurse for children + for child in node.childNodes: + num += removeNamespacedElements(child, namespaces) + return num + +def removeMetadataElements(doc): + global numElemsRemoved + num = 0 + # clone the list, as the tag list is live from the DOM + elementsToRemove = [element for element in doc.documentElement.getElementsByTagName('metadata')] + + for element in elementsToRemove: + element.parentNode.removeChild(element) + num += 1 + numElemsRemoved += 1 + + return num + +def removeNestedGroups(node): + """ + This walks further and further down the tree, removing groups + which do not have any attributes or a title/desc child and + promoting their children up one level + """ + global numElemsRemoved + num = 0 + + groupsToRemove = [] + # Only consider elements for promotion if this element isn't a . + # (partial fix for bug 594930, required by the SVG spec however) + if not (node.nodeType == 1 and node.nodeName == 'switch'): + for child in node.childNodes: + if child.nodeName == 'g' and child.namespaceURI == NS['SVG'] and len(child.attributes) == 0: + # only collapse group if it does not have a title or desc as a direct descendant, + for grandchild in child.childNodes: + if grandchild.nodeType == 1 and grandchild.namespaceURI == NS['SVG'] and \ + grandchild.nodeName in ['title','desc']: + break + else: + groupsToRemove.append(child) + + for g in groupsToRemove: + while g.childNodes.length > 0: + g.parentNode.insertBefore(g.firstChild, g) + g.parentNode.removeChild(g) + numElemsRemoved += 1 + num += 1 + + # now recurse for children + for child in node.childNodes: + if child.nodeType == 1: + num += removeNestedGroups(child) + return num + +def moveCommonAttributesToParentGroup(elem, referencedElements): + """ + This recursively calls this function on all children of the passed in element + and then iterates over all child elements and removes common inheritable attributes + from the children and places them in the parent group. But only if the parent contains + nothing but element children and whitespace. The attributes are only removed from the + children if the children are not referenced by other elements in the document. + """ + num = 0 + + childElements = [] + # recurse first into the children (depth-first) + for child in elem.childNodes: + if child.nodeType == 1: + # only add and recurse if the child is not referenced elsewhere + if not child.getAttribute('id') in referencedElements: + childElements.append(child) + num += moveCommonAttributesToParentGroup(child, referencedElements) + # else if the parent has non-whitespace text children, do not + # try to move common attributes + elif child.nodeType == 3 and child.nodeValue.strip(): + return num + + # only process the children if there are more than one element + if len(childElements) <= 1: return num + + commonAttrs = {} + # add all inheritable properties of the first child element + # FIXME: Note there is a chance that the first child is a set/animate in which case + # its fill attribute is not what we want to look at, we should look for the first + # non-animate/set element + attrList = childElements[0].attributes + for num in xrange(attrList.length): + attr = attrList.item(num) + # this is most of the inheritable properties from http://www.w3.org/TR/SVG11/propidx.html + # and http://www.w3.org/TR/SVGTiny12/attributeTable.html + if attr.nodeName in ['clip-rule', + 'display-align', + 'fill', 'fill-opacity', 'fill-rule', + 'font', 'font-family', 'font-size', 'font-size-adjust', 'font-stretch', + 'font-style', 'font-variant', 'font-weight', + 'letter-spacing', + 'pointer-events', 'shape-rendering', + 'stroke', 'stroke-dasharray', 'stroke-dashoffset', 'stroke-linecap', 'stroke-linejoin', + 'stroke-miterlimit', 'stroke-opacity', 'stroke-width', + 'text-anchor', 'text-decoration', 'text-rendering', 'visibility', + 'word-spacing', 'writing-mode']: + # we just add all the attributes from the first child + commonAttrs[attr.nodeName] = attr.nodeValue + + # for each subsequent child element + for childNum in xrange(len(childElements)): + # skip first child + if childNum == 0: + continue + + child = childElements[childNum] + # if we are on an animateXXX/set element, ignore it (due to the 'fill' attribute) + if child.localName in ['set', 'animate', 'animateColor', 'animateTransform', 'animateMotion']: + continue + + distinctAttrs = [] + # loop through all current 'common' attributes + for name in commonAttrs.keys(): + # if this child doesn't match that attribute, schedule it for removal + if child.getAttribute(name) != commonAttrs[name]: + distinctAttrs.append(name) + # remove those attributes which are not common + for name in distinctAttrs: + del commonAttrs[name] + + # commonAttrs now has all the inheritable attributes which are common among all child elements + for name in commonAttrs.keys(): + for child in childElements: + child.removeAttribute(name) + elem.setAttribute(name, commonAttrs[name]) + + # update our statistic (we remove N*M attributes and add back in M attributes) + num += (len(childElements)-1) * len(commonAttrs) + return num + +def createGroupsForCommonAttributes(elem): + """ + Creates elements to contain runs of 3 or more + consecutive child elements having at least one common attribute. + + Common attributes are not promoted to the by this function. + This is handled by moveCommonAttributesToParentGroup. + + If all children have a common attribute, an extra is not created. + + This function acts recursively on the given element. + """ + num = 0 + global numElemsRemoved + + # TODO perhaps all of the Presentation attributes in http://www.w3.org/TR/SVG/struct.html#GElement + # could be added here + # Cyn: These attributes are the same as in moveAttributesToParentGroup, and must always be + for curAttr in ['clip-rule', + 'display-align', + 'fill', 'fill-opacity', 'fill-rule', + 'font', 'font-family', 'font-size', 'font-size-adjust', 'font-stretch', + 'font-style', 'font-variant', 'font-weight', + 'letter-spacing', + 'pointer-events', 'shape-rendering', + 'stroke', 'stroke-dasharray', 'stroke-dashoffset', 'stroke-linecap', 'stroke-linejoin', + 'stroke-miterlimit', 'stroke-opacity', 'stroke-width', + 'text-anchor', 'text-decoration', 'text-rendering', 'visibility', + 'word-spacing', 'writing-mode']: + # Iterate through the children in reverse order, so item(i) for + # items we have yet to visit still returns the correct nodes. + curChild = elem.childNodes.length - 1 + while curChild >= 0: + childNode = elem.childNodes.item(curChild) + + if childNode.nodeType == 1 and childNode.getAttribute(curAttr) != '': + # We're in a possible run! Track the value and run length. + value = childNode.getAttribute(curAttr) + runStart, runEnd = curChild, curChild + # Run elements includes only element tags, no whitespace/comments/etc. + # Later, we calculate a run length which includes these. + runElements = 1 + + # Backtrack to get all the nodes having the same + # attribute value, preserving any nodes in-between. + while runStart > 0: + nextNode = elem.childNodes.item(runStart - 1) + if nextNode.nodeType == 1: + if nextNode.getAttribute(curAttr) != value: break + else: + runElements += 1 + runStart -= 1 + else: runStart -= 1 + + if runElements >= 3: + # Include whitespace/comment/etc. nodes in the run. + while runEnd < elem.childNodes.length - 1: + if elem.childNodes.item(runEnd + 1).nodeType == 1: break + else: runEnd += 1 + + runLength = runEnd - runStart + 1 + if runLength == elem.childNodes.length: # Every child has this + # If the current parent is a already, + if elem.nodeName == 'g' and elem.namespaceURI == NS['SVG']: + # do not act altogether on this attribute; all the + # children have it in common. + # Let moveCommonAttributesToParentGroup do it. + curChild = -1 + continue + # otherwise, it might be an element, and + # even if all children have the same attribute value, + # it's going to be worth making the since + # doesn't support attributes like 'stroke'. + # Fall through. + + # Create a element from scratch. + # We need the Document for this. + document = elem.ownerDocument + group = document.createElementNS(NS['SVG'], 'g') + # Move the run of elements to the group. + # a) ADD the nodes to the new group. + group.childNodes[:] = elem.childNodes[runStart:runEnd + 1] + for child in group.childNodes: + child.parentNode = group + # b) REMOVE the nodes from the element. + elem.childNodes[runStart:runEnd + 1] = [] + # Include the group in elem's children. + elem.childNodes.insert(runStart, group) + group.parentNode = elem + num += 1 + curChild = runStart - 1 + numElemsRemoved -= 1 + else: + curChild -= 1 + else: + curChild -= 1 + + # each child gets the same treatment, recursively + for childNode in elem.childNodes: + if childNode.nodeType == 1: + num += createGroupsForCommonAttributes(childNode) + + return num + +def removeUnusedAttributesOnParent(elem): + """ + This recursively calls this function on all children of the element passed in, + then removes any unused attributes on this elem if none of the children inherit it + """ + num = 0 + + childElements = [] + # recurse first into the children (depth-first) + for child in elem.childNodes: + if child.nodeType == 1: + childElements.append(child) + num += removeUnusedAttributesOnParent(child) + + # only process the children if there are more than one element + if len(childElements) <= 1: return num + + # get all attribute values on this parent + attrList = elem.attributes + unusedAttrs = {} + for num in xrange(attrList.length): + attr = attrList.item(num) + if attr.nodeName in ['clip-rule', + 'display-align', + 'fill', 'fill-opacity', 'fill-rule', + 'font', 'font-family', 'font-size', 'font-size-adjust', 'font-stretch', + 'font-style', 'font-variant', 'font-weight', + 'letter-spacing', + 'pointer-events', 'shape-rendering', + 'stroke', 'stroke-dasharray', 'stroke-dashoffset', 'stroke-linecap', 'stroke-linejoin', + 'stroke-miterlimit', 'stroke-opacity', 'stroke-width', + 'text-anchor', 'text-decoration', 'text-rendering', 'visibility', + 'word-spacing', 'writing-mode']: + unusedAttrs[attr.nodeName] = attr.nodeValue + + # for each child, if at least one child inherits the parent's attribute, then remove + for childNum in xrange(len(childElements)): + child = childElements[childNum] + inheritedAttrs = [] + for name in unusedAttrs.keys(): + val = child.getAttribute(name) + if val == '' or val == None or val == 'inherit': + inheritedAttrs.append(name) + for a in inheritedAttrs: + del unusedAttrs[a] + + # unusedAttrs now has all the parent attributes that are unused + for name in unusedAttrs.keys(): + elem.removeAttribute(name) + num += 1 + + return num + +def removeDuplicateGradientStops(doc): + global numElemsRemoved + num = 0 + + for gradType in ['linearGradient', 'radialGradient']: + for grad in doc.getElementsByTagName(gradType): + stops = {} + stopsToRemove = [] + for stop in grad.getElementsByTagName('stop'): + # convert percentages into a floating point number + offsetU = SVGLength(stop.getAttribute('offset')) + if offsetU.units == Unit.PCT: + offset = offsetU.value / 100.0 + elif offsetU.units == Unit.NONE: + offset = offsetU.value + else: + offset = 0 + # set the stop offset value to the integer or floating point equivalent + if int(offset) == offset: stop.setAttribute('offset', str(int(offset))) + else: stop.setAttribute('offset', str(offset)) + + color = stop.getAttribute('stop-color') + opacity = stop.getAttribute('stop-opacity') + style = stop.getAttribute('style') + if stops.has_key(offset) : + oldStop = stops[offset] + if oldStop[0] == color and oldStop[1] == opacity and oldStop[2] == style: + stopsToRemove.append(stop) + stops[offset] = [color, opacity, style] + + for stop in stopsToRemove: + stop.parentNode.removeChild(stop) + num += 1 + numElemsRemoved += 1 + + # linear gradients + return num + +def collapseSinglyReferencedGradients(doc): + global numElemsRemoved + num = 0 + + identifiedElements = findElementsWithId(doc.documentElement) + + # make sure to reset the ref'ed ids for when we are running this in testscour + for rid,nodeCount in findReferencedElements(doc.documentElement).iteritems(): + count = nodeCount[0] + nodes = nodeCount[1] + # Make sure that there's actually a defining element for the current ID name. + # (Cyn: I've seen documents with #id references but no element with that ID!) + if count == 1 and rid in identifiedElements: + elem = identifiedElements[rid] + if elem != None and elem.nodeType == 1 and elem.nodeName in ['linearGradient', 'radialGradient'] \ + and elem.namespaceURI == NS['SVG']: + # found a gradient that is referenced by only 1 other element + refElem = nodes[0] + if refElem.nodeType == 1 and refElem.nodeName in ['linearGradient', 'radialGradient'] \ + and refElem.namespaceURI == NS['SVG']: + # elem is a gradient referenced by only one other gradient (refElem) + + # add the stops to the referencing gradient (this removes them from elem) + if len(refElem.getElementsByTagName('stop')) == 0: + stopsToAdd = elem.getElementsByTagName('stop') + for stop in stopsToAdd: + refElem.appendChild(stop) + + # adopt the gradientUnits, spreadMethod, gradientTransform attributes if + # they are unspecified on refElem + for attr in ['gradientUnits','spreadMethod','gradientTransform']: + if refElem.getAttribute(attr) == '' and not elem.getAttribute(attr) == '': + refElem.setAttributeNS(None, attr, elem.getAttribute(attr)) + + # if both are radialGradients, adopt elem's fx,fy,cx,cy,r attributes if + # they are unspecified on refElem + if elem.nodeName == 'radialGradient' and refElem.nodeName == 'radialGradient': + for attr in ['fx','fy','cx','cy','r']: + if refElem.getAttribute(attr) == '' and not elem.getAttribute(attr) == '': + refElem.setAttributeNS(None, attr, elem.getAttribute(attr)) + + # if both are linearGradients, adopt elem's x1,y1,x2,y2 attributes if + # they are unspecified on refElem + if elem.nodeName == 'linearGradient' and refElem.nodeName == 'linearGradient': + for attr in ['x1','y1','x2','y2']: + if refElem.getAttribute(attr) == '' and not elem.getAttribute(attr) == '': + refElem.setAttributeNS(None, attr, elem.getAttribute(attr)) + + # now remove the xlink:href from refElem + refElem.removeAttributeNS(NS['XLINK'], 'href') + + # now delete elem + elem.parentNode.removeChild(elem) + numElemsRemoved += 1 + num += 1 + return num + +def removeDuplicateGradients(doc): + global numElemsRemoved + num = 0 + + gradientsToRemove = {} + duplicateToMaster = {} + + for gradType in ['linearGradient', 'radialGradient']: + grads = doc.getElementsByTagName(gradType) + for grad in grads: + # TODO: should slice grads from 'grad' here to optimize + for ograd in grads: + # do not compare gradient to itself + if grad == ograd: continue + + # compare grad to ograd (all properties, then all stops) + # if attributes do not match, go to next gradient + someGradAttrsDoNotMatch = False + for attr in ['gradientUnits','spreadMethod','gradientTransform','x1','y1','x2','y2','cx','cy','fx','fy','r']: + if grad.getAttribute(attr) != ograd.getAttribute(attr): + someGradAttrsDoNotMatch = True + break; + + if someGradAttrsDoNotMatch: continue + + # compare xlink:href values too + if grad.getAttributeNS(NS['XLINK'], 'href') != ograd.getAttributeNS(NS['XLINK'], 'href'): + continue + + # all gradient properties match, now time to compare stops + stops = grad.getElementsByTagName('stop') + ostops = ograd.getElementsByTagName('stop') + + if stops.length != ostops.length: continue + + # now compare stops + stopsNotEqual = False + for i in xrange(stops.length): + if stopsNotEqual: break + stop = stops.item(i) + ostop = ostops.item(i) + for attr in ['offset', 'stop-color', 'stop-opacity', 'style']: + if stop.getAttribute(attr) != ostop.getAttribute(attr): + stopsNotEqual = True + break + if stopsNotEqual: continue + + # ograd is a duplicate of grad, we schedule it to be removed UNLESS + # ograd is ALREADY considered a 'master' element + if not gradientsToRemove.has_key(ograd): + if not duplicateToMaster.has_key(ograd): + if not gradientsToRemove.has_key(grad): + gradientsToRemove[grad] = [] + gradientsToRemove[grad].append( ograd ) + duplicateToMaster[ograd] = grad + + # get a collection of all elements that are referenced and their referencing elements + referencedIDs = findReferencedElements(doc.documentElement) + for masterGrad in gradientsToRemove.keys(): + master_id = masterGrad.getAttribute('id') +# print 'master='+master_id + for dupGrad in gradientsToRemove[masterGrad]: + # if the duplicate gradient no longer has a parent that means it was + # already re-mapped to another master gradient + if not dupGrad.parentNode: continue + dup_id = dupGrad.getAttribute('id') +# print 'dup='+dup_id +# print referencedIDs[dup_id] + # for each element that referenced the gradient we are going to remove + for elem in referencedIDs[dup_id][1]: + # find out which attribute referenced the duplicate gradient + for attr in ['fill', 'stroke']: + v = elem.getAttribute(attr) + if v == 'url(#'+dup_id+')' or v == 'url("#'+dup_id+'")' or v == "url('#"+dup_id+"')": + elem.setAttribute(attr, 'url(#'+master_id+')') + if elem.getAttributeNS(NS['XLINK'], 'href') == '#'+dup_id: + elem.setAttributeNS(NS['XLINK'], 'href', '#'+master_id) + styles = _getStyle(elem) + for style in styles: + v = styles[style] + if v == 'url(#'+dup_id+')' or v == 'url("#'+dup_id+'")' or v == "url('#"+dup_id+"')": + styles[style] = 'url(#'+master_id+')' + _setStyle(elem, styles) + + # now that all referencing elements have been re-mapped to the master + # it is safe to remove this gradient from the document + dupGrad.parentNode.removeChild(dupGrad) + numElemsRemoved += 1 + num += 1 + return num + +def _getStyle(node): + u"""Returns the style attribute of a node as a dictionary.""" + if node.nodeType == 1 and len(node.getAttribute('style')) > 0 : + styleMap = { } + rawStyles = node.getAttribute('style').split(';') + for style in rawStyles: + propval = style.split(':') + if len(propval) == 2 : + styleMap[propval[0].strip()] = propval[1].strip() + return styleMap + else: + return {} + +def _setStyle(node, styleMap): + u"""Sets the style attribute of a node to the dictionary ``styleMap``.""" + fixedStyle = ';'.join([prop + ':' + styleMap[prop] for prop in styleMap.keys()]) + if fixedStyle != '' : + node.setAttribute('style', fixedStyle) + elif node.getAttribute('style'): + node.removeAttribute('style') + return node + +def repairStyle(node, options): + num = 0 + styleMap = _getStyle(node) + if styleMap: + + # I've seen this enough to know that I need to correct it: + # fill: url(#linearGradient4918) rgb(0, 0, 0); + for prop in ['fill', 'stroke'] : + if styleMap.has_key(prop) : + chunk = styleMap[prop].split(') ') + if len(chunk) == 2 and (chunk[0][:5] == 'url(#' or chunk[0][:6] == 'url("#' or chunk[0][:6] == "url('#") and chunk[1] == 'rgb(0, 0, 0)' : + styleMap[prop] = chunk[0] + ')' + num += 1 + + # Here is where we can weed out unnecessary styles like: + # opacity:1 + if styleMap.has_key('opacity') : + opacity = float(styleMap['opacity']) + # if opacity='0' then all fill and stroke properties are useless, remove them + if opacity == 0.0 : + for uselessStyle in ['fill', 'fill-opacity', 'fill-rule', 'stroke', 'stroke-linejoin', + 'stroke-opacity', 'stroke-miterlimit', 'stroke-linecap', 'stroke-dasharray', + 'stroke-dashoffset', 'stroke-opacity'] : + if styleMap.has_key(uselessStyle): + del styleMap[uselessStyle] + num += 1 + + # if stroke:none, then remove all stroke-related properties (stroke-width, etc) + # TODO: should also detect if the computed value of this element is stroke="none" + if styleMap.has_key('stroke') and styleMap['stroke'] == 'none' : + for strokestyle in [ 'stroke-width', 'stroke-linejoin', 'stroke-miterlimit', + 'stroke-linecap', 'stroke-dasharray', 'stroke-dashoffset', 'stroke-opacity'] : + if styleMap.has_key(strokestyle) : + del styleMap[strokestyle] + num += 1 + # TODO: This is actually a problem if a parent element has a specified stroke + # we need to properly calculate computed values + del styleMap['stroke'] + + # if fill:none, then remove all fill-related properties (fill-rule, etc) + if styleMap.has_key('fill') and styleMap['fill'] == 'none' : + for fillstyle in [ 'fill-rule', 'fill-opacity' ] : + if styleMap.has_key(fillstyle) : + del styleMap[fillstyle] + num += 1 + + # fill-opacity: 0 + if styleMap.has_key('fill-opacity') : + fillOpacity = float(styleMap['fill-opacity']) + if fillOpacity == 0.0 : + for uselessFillStyle in [ 'fill', 'fill-rule' ] : + if styleMap.has_key(uselessFillStyle): + del styleMap[uselessFillStyle] + num += 1 + + # stroke-opacity: 0 + if styleMap.has_key('stroke-opacity') : + strokeOpacity = float(styleMap['stroke-opacity']) + if strokeOpacity == 0.0 : + for uselessStrokeStyle in [ 'stroke', 'stroke-width', 'stroke-linejoin', 'stroke-linecap', + 'stroke-dasharray', 'stroke-dashoffset' ] : + if styleMap.has_key(uselessStrokeStyle): + del styleMap[uselessStrokeStyle] + num += 1 + + # stroke-width: 0 + if styleMap.has_key('stroke-width') : + strokeWidth = SVGLength(styleMap['stroke-width']) + if strokeWidth.value == 0.0 : + for uselessStrokeStyle in [ 'stroke', 'stroke-linejoin', 'stroke-linecap', + 'stroke-dasharray', 'stroke-dashoffset', 'stroke-opacity' ] : + if styleMap.has_key(uselessStrokeStyle): + del styleMap[uselessStrokeStyle] + num += 1 + + # remove font properties for non-text elements + # I've actually observed this in real SVG content + if not mayContainTextNodes(node): + for fontstyle in [ 'font-family', 'font-size', 'font-stretch', 'font-size-adjust', + 'font-style', 'font-variant', 'font-weight', + 'letter-spacing', 'line-height', 'kerning', + 'text-align', 'text-anchor', 'text-decoration', + 'text-rendering', 'unicode-bidi', + 'word-spacing', 'writing-mode'] : + if styleMap.has_key(fontstyle) : + del styleMap[fontstyle] + num += 1 + + # remove inkscape-specific styles + # TODO: need to get a full list of these + for inkscapeStyle in ['-inkscape-font-specification']: + if styleMap.has_key(inkscapeStyle): + del styleMap[inkscapeStyle] + num += 1 + + if styleMap.has_key('overflow') : + # overflow specified on element other than svg, marker, pattern + if not node.nodeName in ['svg','marker','pattern']: + del styleMap['overflow'] + num += 1 + # it is a marker, pattern or svg + # as long as this node is not the document , then only + # remove overflow='hidden'. See + # http://www.w3.org/TR/2010/WD-SVG11-20100622/masking.html#OverflowProperty + elif node != node.ownerDocument.documentElement: + if styleMap['overflow'] == 'hidden': + del styleMap['overflow'] + num += 1 + # else if outer svg has a overflow="visible", we can remove it + elif styleMap['overflow'] == 'visible': + del styleMap['overflow'] + num += 1 + + # now if any of the properties match known SVG attributes we prefer attributes + # over style so emit them and remove them from the style map + if options.style_to_xml: + for propName in styleMap.keys() : + if propName in svgAttributes : + node.setAttribute(propName, styleMap[propName]) + del styleMap[propName] + + _setStyle(node, styleMap) + + # recurse for our child elements + for child in node.childNodes : + num += repairStyle(child,options) + + return num + +def mayContainTextNodes(node): + """ + Returns True if the passed-in node is probably a text element, or at least + one of its descendants is probably a text element. + + If False is returned, it is guaranteed that the passed-in node has no + business having text-based attributes. + + If True is returned, the passed-in node should not have its text-based + attributes removed. + """ + # Cached result of a prior call? + try: + return node.mayContainTextNodes + except AttributeError: + pass + + result = True # Default value + # Comment, text and CDATA nodes don't have attributes and aren't containers + if node.nodeType != 1: + result = False + # Non-SVG elements? Unknown elements! + elif node.namespaceURI != NS['SVG']: + result = True + # Blacklisted elements. Those are guaranteed not to be text elements. + elif node.nodeName in ['rect', 'circle', 'ellipse', 'line', 'polygon', + 'polyline', 'path', 'image', 'stop']: + result = False + # Group elements. If we're missing any here, the default of True is used. + elif node.nodeName in ['g', 'clipPath', 'marker', 'mask', 'pattern', + 'linearGradient', 'radialGradient', 'symbol']: + result = False + for child in node.childNodes: + if mayContainTextNodes(child): + result = True + # Everything else should be considered a future SVG-version text element + # at best, or an unknown element at worst. result will stay True. + + # Cache this result before returning it. + node.mayContainTextNodes = result + return result + +def taint(taintedSet, taintedAttribute): + u"""Adds an attribute to a set of attributes. + + Related attributes are also included.""" + taintedSet.add(taintedAttribute) + if taintedAttribute == 'marker': + taintedSet |= set(['marker-start', 'marker-mid', 'marker-end']) + if taintedAttribute in ['marker-start', 'marker-mid', 'marker-end']: + taintedSet.add('marker') + return taintedSet + +def removeDefaultAttributeValues(node, options, tainted=set()): + u"""'tainted' keeps a set of attributes defined in parent nodes. + + For such attributes, we don't delete attributes with default values.""" + num = 0 + if node.nodeType != 1: return 0 + + # gradientUnits: objectBoundingBox + if node.getAttribute('gradientUnits') == 'objectBoundingBox': + node.removeAttribute('gradientUnits') + num += 1 + + # spreadMethod: pad + if node.getAttribute('spreadMethod') == 'pad': + node.removeAttribute('spreadMethod') + num += 1 + + # x1: 0% + if node.getAttribute('x1') != '': + x1 = SVGLength(node.getAttribute('x1')) + if x1.value == 0: + node.removeAttribute('x1') + num += 1 + + # y1: 0% + if node.getAttribute('y1') != '': + y1 = SVGLength(node.getAttribute('y1')) + if y1.value == 0: + node.removeAttribute('y1') + num += 1 + + # x2: 100% + if node.getAttribute('x2') != '': + x2 = SVGLength(node.getAttribute('x2')) + if (x2.value == 100 and x2.units == Unit.PCT) or (x2.value == 1 and x2.units == Unit.NONE): + node.removeAttribute('x2') + num += 1 + + # y2: 0% + if node.getAttribute('y2') != '': + y2 = SVGLength(node.getAttribute('y2')) + if y2.value == 0: + node.removeAttribute('y2') + num += 1 + + # fx: equal to rx + if node.getAttribute('fx') != '': + if node.getAttribute('fx') == node.getAttribute('cx'): + node.removeAttribute('fx') + num += 1 + + # fy: equal to ry + if node.getAttribute('fy') != '': + if node.getAttribute('fy') == node.getAttribute('cy'): + node.removeAttribute('fy') + num += 1 + + # cx: 50% + if node.getAttribute('cx') != '': + cx = SVGLength(node.getAttribute('cx')) + if (cx.value == 50 and cx.units == Unit.PCT) or (cx.value == 0.5 and cx.units == Unit.NONE): + node.removeAttribute('cx') + num += 1 + + # cy: 50% + if node.getAttribute('cy') != '': + cy = SVGLength(node.getAttribute('cy')) + if (cy.value == 50 and cy.units == Unit.PCT) or (cy.value == 0.5 and cy.units == Unit.NONE): + node.removeAttribute('cy') + num += 1 + + # r: 50% + if node.getAttribute('r') != '': + r = SVGLength(node.getAttribute('r')) + if (r.value == 50 and r.units == Unit.PCT) or (r.value == 0.5 and r.units == Unit.NONE): + node.removeAttribute('r') + num += 1 + + # Summarily get rid of some more attributes + attributes = [node.attributes.item(i).nodeName + for i in range(node.attributes.length)] + for attribute in attributes: + if attribute not in tainted: + if attribute in default_attributes.keys(): + if node.getAttribute(attribute) == default_attributes[attribute]: + node.removeAttribute(attribute) + num += 1 + else: + tainted = taint(tainted, attribute) + # These attributes might also occur as styles + styles = _getStyle(node) + for attribute in styles.keys(): + if attribute not in tainted: + if attribute in default_attributes.keys(): + if styles[attribute] == default_attributes[attribute]: + del styles[attribute] + num += 1 + else: + tainted = taint(tainted, attribute) + _setStyle(node, styles) + + # recurse for our child elements + for child in node.childNodes : + num += removeDefaultAttributeValues(child, options, tainted.copy()) + + return num + +rgb = re.compile(r"\s*rgb\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)\s*\)\s*") +rgbp = re.compile(r"\s*rgb\(\s*(\d*\.?\d+)%\s*,\s*(\d*\.?\d+)%\s*,\s*(\d*\.?\d+)%\s*\)\s*") +def convertColor(value): + """ + Converts the input color string and returns a #RRGGBB (or #RGB if possible) string + """ + s = value + + if s in colors.keys(): + s = colors[s] + + rgbpMatch = rgbp.match(s) + if rgbpMatch != None : + r = int(float(rgbpMatch.group(1)) * 255.0 / 100.0) + g = int(float(rgbpMatch.group(2)) * 255.0 / 100.0) + b = int(float(rgbpMatch.group(3)) * 255.0 / 100.0) + s = '#%02x%02x%02x' % (r, g, b) + else: + rgbMatch = rgb.match(s) + if rgbMatch != None : + r = int( rgbMatch.group(1) ) + g = int( rgbMatch.group(2) ) + b = int( rgbMatch.group(3) ) + s = '#%02x%02x%02x' % (r, g, b) + + if s[0] == '#': + s = s.lower() + if len(s)==7 and s[1]==s[2] and s[3]==s[4] and s[5]==s[6]: + s = '#'+s[1]+s[3]+s[5] + + return s + +def convertColors(element) : + """ + Recursively converts all color properties into #RRGGBB format if shorter + """ + numBytes = 0 + + if element.nodeType != 1: return 0 + + # set up list of color attributes for each element type + attrsToConvert = [] + if element.nodeName in ['rect', 'circle', 'ellipse', 'polygon', \ + 'line', 'polyline', 'path', 'g', 'a']: + attrsToConvert = ['fill', 'stroke'] + elif element.nodeName in ['stop']: + attrsToConvert = ['stop-color'] + elif element.nodeName in ['solidColor']: + attrsToConvert = ['solid-color'] + + # now convert all the color formats + styles = _getStyle(element) + for attr in attrsToConvert: + oldColorValue = element.getAttribute(attr) + if oldColorValue != '': + newColorValue = convertColor(oldColorValue) + oldBytes = len(oldColorValue) + newBytes = len(newColorValue) + if oldBytes > newBytes: + element.setAttribute(attr, newColorValue) + numBytes += (oldBytes - len(element.getAttribute(attr))) + # colors might also hide in styles + if attr in styles.keys(): + oldColorValue = styles[attr] + newColorValue = convertColor(oldColorValue) + oldBytes = len(oldColorValue) + newBytes = len(newColorValue) + if oldBytes > newBytes: + styles[attr] = newColorValue + numBytes += (oldBytes - len(element.getAttribute(attr))) + _setStyle(element, styles) + + # now recurse for our child elements + for child in element.childNodes : + numBytes += convertColors(child) + + return numBytes + +# TODO: go over what this method does and see if there is a way to optimize it +# TODO: go over the performance of this method and see if I can save memory/speed by +# reusing data structures, etc +def cleanPath(element, options) : + """ + Cleans the path string (d attribute) of the element + """ + global numBytesSavedInPathData + global numPathSegmentsReduced + global numCurvesStraightened + + # this gets the parser object from svg_regex.py + oldPathStr = element.getAttribute('d') + path = svg_parser.parse(oldPathStr) + + # This determines whether the stroke has round linecaps. If it does, + # we do not want to collapse empty segments, as they are actually rendered. + withRoundLineCaps = element.getAttribute('stroke-linecap') == 'round' + + # The first command must be a moveto, and whether it's relative (m) + # or absolute (M), the first set of coordinates *is* absolute. So + # the first iteration of the loop below will get x,y and startx,starty. + + # convert absolute coordinates into relative ones. + # Reuse the data structure 'path', since we're not adding or removing subcommands. + # Also reuse the coordinate lists since we're not adding or removing any. + for pathIndex in xrange(0, len(path)): + cmd, data = path[pathIndex] # Changes to cmd don't get through to the data structure + i = 0 + # adjust abs to rel + # only the A command has some values that we don't want to adjust (radii, rotation, flags) + if cmd == 'A': + for i in xrange(i, len(data), 7): + data[i+5] -= x + data[i+6] -= y + x += data[i+5] + y += data[i+6] + path[pathIndex] = ('a', data) + elif cmd == 'a': + x += sum(data[5::7]) + y += sum(data[6::7]) + elif cmd == 'H': + for i in xrange(i, len(data)): + data[i] -= x + x += data[i] + path[pathIndex] = ('h', data) + elif cmd == 'h': + x += sum(data) + elif cmd == 'V': + for i in xrange(i, len(data)): + data[i] -= y + y += data[i] + path[pathIndex] = ('v', data) + elif cmd == 'v': + y += sum(data) + elif cmd == 'M': + startx, starty = data[0], data[1] + # If this is a path starter, don't convert its first + # coordinate to relative; that would just make it (0, 0) + if pathIndex != 0: + data[0] -= x + data[1] -= y + + x, y = startx, starty + i = 2 + for i in xrange(i, len(data), 2): + data[i] -= x + data[i+1] -= y + x += data[i] + y += data[i+1] + path[pathIndex] = ('m', data) + elif cmd in ['L','T']: + for i in xrange(i, len(data), 2): + data[i] -= x + data[i+1] -= y + x += data[i] + y += data[i+1] + path[pathIndex] = (cmd.lower(), data) + elif cmd in ['m']: + if pathIndex == 0: + # START OF PATH - this is an absolute moveto + # followed by relative linetos + startx, starty = data[0], data[1] + x, y = startx, starty + i = 2 + else: + startx = x + data[0] + starty = y + data[1] + for i in xrange(i, len(data), 2): + x += data[i] + y += data[i+1] + elif cmd in ['l','t']: + x += sum(data[0::2]) + y += sum(data[1::2]) + elif cmd in ['S','Q']: + for i in xrange(i, len(data), 4): + data[i] -= x + data[i+1] -= y + data[i+2] -= x + data[i+3] -= y + x += data[i+2] + y += data[i+3] + path[pathIndex] = (cmd.lower(), data) + elif cmd in ['s','q']: + x += sum(data[2::4]) + y += sum(data[3::4]) + elif cmd == 'C': + for i in xrange(i, len(data), 6): + data[i] -= x + data[i+1] -= y + data[i+2] -= x + data[i+3] -= y + data[i+4] -= x + data[i+5] -= y + x += data[i+4] + y += data[i+5] + path[pathIndex] = ('c', data) + elif cmd == 'c': + x += sum(data[4::6]) + y += sum(data[5::6]) + elif cmd in ['z','Z']: + x, y = startx, starty + path[pathIndex] = ('z', data) + + # remove empty segments + # Reuse the data structure 'path' and the coordinate lists, even if we're + # deleting items, because these deletions are relatively cheap. + if not withRoundLineCaps: + for pathIndex in xrange(0, len(path)): + cmd, data = path[pathIndex] + i = 0 + if cmd in ['m','l','t']: + if cmd == 'm': + # remove m0,0 segments + if pathIndex > 0 and data[0] == data[i+1] == 0: + # 'm0,0 x,y' can be replaces with 'lx,y', + # except the first m which is a required absolute moveto + path[pathIndex] = ('l', data[2:]) + numPathSegmentsReduced += 1 + else: # else skip move coordinate + i = 2 + while i < len(data): + if data[i] == data[i+1] == 0: + del data[i:i+2] + numPathSegmentsReduced += 1 + else: + i += 2 + elif cmd == 'c': + while i < len(data): + if data[i] == data[i+1] == data[i+2] == data[i+3] == data[i+4] == data[i+5] == 0: + del data[i:i+6] + numPathSegmentsReduced += 1 + else: + i += 6 + elif cmd == 'a': + while i < len(data): + if data[i+5] == data[i+6] == 0: + del data[i:i+7] + numPathSegmentsReduced += 1 + else: + i += 7 + elif cmd == 'q': + while i < len(data): + if data[i] == data[i+1] == data[i+2] == data[i+3] == 0: + del data[i:i+4] + numPathSegmentsReduced += 1 + else: + i += 4 + elif cmd in ['h','v']: + oldLen = len(data) + path[pathIndex] = (cmd, [coord for coord in data if coord != 0]) + numPathSegmentsReduced += len(path[pathIndex][1]) - oldLen + + # fixup: Delete subcommands having no coordinates. + path = [elem for elem in path if len(elem[1]) > 0 or elem[0] == 'z'] + + # convert straight curves into lines + newPath = [path[0]] + for (cmd,data) in path[1:]: + i = 0 + newData = data + if cmd == 'c': + newData = [] + while i < len(data): + # since all commands are now relative, we can think of previous point as (0,0) + # and new point (dx,dy) is (data[i+4],data[i+5]) + # eqn of line will be y = (dy/dx)*x or if dx=0 then eqn of line is x=0 + (p1x,p1y) = (data[i],data[i+1]) + (p2x,p2y) = (data[i+2],data[i+3]) + dx = data[i+4] + dy = data[i+5] + + foundStraightCurve = False + + if dx == 0: + if p1x == 0 and p2x == 0: + foundStraightCurve = True + else: + m = dy/dx + if p1y == m*p1x and p2y == m*p2x: + foundStraightCurve = True + + if foundStraightCurve: + # flush any existing curve coords first + if newData: + newPath.append( (cmd,newData) ) + newData = [] + # now create a straight line segment + newPath.append( ('l', [dx,dy]) ) + numCurvesStraightened += 1 + else: + newData.extend(data[i:i+6]) + + i += 6 + if newData or cmd == 'z' or cmd == 'Z': + newPath.append( (cmd,newData) ) + path = newPath + + # collapse all consecutive commands of the same type into one command + prevCmd = '' + prevData = [] + newPath = [] + for (cmd,data) in path: + # flush the previous command if it is not the same type as the current command + if prevCmd != '': + if cmd != prevCmd or cmd == 'm': + newPath.append( (prevCmd, prevData) ) + prevCmd = '' + prevData = [] + + # if the previous and current commands are the same type, + # or the previous command is moveto and the current is lineto, collapse, + # but only if they are not move commands (since move can contain implicit lineto commands) + if (cmd == prevCmd or (cmd == 'l' and prevCmd == 'm')) and cmd != 'm': + prevData.extend(data) + + # save last command and data + else: + prevCmd = cmd + prevData = data + # flush last command and data + if prevCmd != '': + newPath.append( (prevCmd, prevData) ) + path = newPath + + # convert to shorthand path segments where possible + newPath = [] + for (cmd,data) in path: + # convert line segments into h,v where possible + if cmd == 'l': + i = 0 + lineTuples = [] + while i < len(data): + if data[i] == 0: + # vertical + if lineTuples: + # flush the existing line command + newPath.append( ('l', lineTuples) ) + lineTuples = [] + # append the v and then the remaining line coords + newPath.append( ('v', [data[i+1]]) ) + numPathSegmentsReduced += 1 + elif data[i+1] == 0: + if lineTuples: + # flush the line command, then append the h and then the remaining line coords + newPath.append( ('l', lineTuples) ) + lineTuples = [] + newPath.append( ('h', [data[i]]) ) + numPathSegmentsReduced += 1 + else: + lineTuples.extend(data[i:i+2]) + i += 2 + if lineTuples: + newPath.append( ('l', lineTuples) ) + # also handle implied relative linetos + elif cmd == 'm': + i = 2 + lineTuples = [data[0], data[1]] + while i < len(data): + if data[i] == 0: + # vertical + if lineTuples: + # flush the existing m/l command + newPath.append( (cmd, lineTuples) ) + lineTuples = [] + cmd = 'l' # dealing with linetos now + # append the v and then the remaining line coords + newPath.append( ('v', [data[i+1]]) ) + numPathSegmentsReduced += 1 + elif data[i+1] == 0: + if lineTuples: + # flush the m/l command, then append the h and then the remaining line coords + newPath.append( (cmd, lineTuples) ) + lineTuples = [] + cmd = 'l' # dealing with linetos now + newPath.append( ('h', [data[i]]) ) + numPathSegmentsReduced += 1 + else: + lineTuples.extend(data[i:i+2]) + i += 2 + if lineTuples: + newPath.append( (cmd, lineTuples) ) + # convert Bézier curve segments into s where possible + elif cmd == 'c': + bez_ctl_pt = (0,0) + i = 0 + curveTuples = [] + while i < len(data): + # rotate by 180deg means negate both coordinates + # if the previous control point is equal then we can substitute a + # shorthand bezier command + if bez_ctl_pt[0] == data[i] and bez_ctl_pt[1] == data[i+1]: + if curveTuples: + newPath.append( ('c', curveTuples) ) + curveTuples = [] + # append the s command + newPath.append( ('s', [data[i+2], data[i+3], data[i+4], data[i+5]]) ) + numPathSegmentsReduced += 1 + else: + j = 0 + while j <= 5: + curveTuples.append(data[i+j]) + j += 1 + + # set up control point for next curve segment + bez_ctl_pt = (data[i+4]-data[i+2], data[i+5]-data[i+3]) + i += 6 + + if curveTuples: + newPath.append( ('c', curveTuples) ) + # convert quadratic curve segments into t where possible + elif cmd == 'q': + quad_ctl_pt = (0,0) + i = 0 + curveTuples = [] + while i < len(data): + if quad_ctl_pt[0] == data[i] and quad_ctl_pt[1] == data[i+1]: + if curveTuples: + newPath.append( ('q', curveTuples) ) + curveTuples = [] + # append the t command + newPath.append( ('t', [data[i+2], data[i+3]]) ) + numPathSegmentsReduced += 1 + else: + j = 0; + while j <= 3: + curveTuples.append(data[i+j]) + j += 1 + + quad_ctl_pt = (data[i+2]-data[i], data[i+3]-data[i+1]) + i += 4 + + if curveTuples: + newPath.append( ('q', curveTuples) ) + else: + newPath.append( (cmd, data) ) + path = newPath + + # for each h or v, collapse unnecessary coordinates that run in the same direction + # i.e. "h-100-100" becomes "h-200" but "h300-100" does not change + # Reuse the data structure 'path', since we're not adding or removing subcommands. + # Also reuse the coordinate lists, even if we're deleting items, because these + # deletions are relatively cheap. + for pathIndex in xrange(1, len(path)): + cmd, data = path[pathIndex] + if cmd in ['h','v'] and len(data) > 1: + coordIndex = 1 + while coordIndex < len(data): + if isSameSign(data[coordIndex - 1], data[coordIndex]): + data[coordIndex - 1] += data[coordIndex] + del data[coordIndex] + numPathSegmentsReduced += 1 + else: + coordIndex += 1 + + # it is possible that we have consecutive h, v, c, t commands now + # so again collapse all consecutive commands of the same type into one command + prevCmd = '' + prevData = [] + newPath = [path[0]] + for (cmd,data) in path[1:]: + # flush the previous command if it is not the same type as the current command + if prevCmd != '': + if cmd != prevCmd or cmd == 'm': + newPath.append( (prevCmd, prevData) ) + prevCmd = '' + prevData = [] + + # if the previous and current commands are the same type, collapse + if cmd == prevCmd and cmd != 'm': + prevData.extend(data) + + # save last command and data + else: + prevCmd = cmd + prevData = data + # flush last command and data + if prevCmd != '': + newPath.append( (prevCmd, prevData) ) + path = newPath + + newPathStr = serializePath(path, options) + numBytesSavedInPathData += ( len(oldPathStr) - len(newPathStr) ) + element.setAttribute('d', newPathStr) + +def parseListOfPoints(s): + """ + Parse string into a list of points. + + Returns a list of containing an even number of coordinate strings + """ + i = 0 + + # (wsp)? comma-or-wsp-separated coordinate pairs (wsp)? + # coordinate-pair = coordinate comma-or-wsp coordinate + # coordinate = sign? integer + # comma-wsp: (wsp+ comma? wsp*) | (comma wsp*) + ws_nums = re.split(r"\s*,?\s*", s.strip()) + nums = [] + + # also, if 100-100 is found, split it into two also + # + for i in xrange(len(ws_nums)): + negcoords = ws_nums[i].split("-") + + # this string didn't have any negative coordinates + if len(negcoords) == 1: + nums.append(negcoords[0]) + # we got negative coords + else: + for j in xrange(len(negcoords)): + if j == 0: + # first number could be positive + if negcoords[0] != '': + nums.append(negcoords[0]) + # but it could also be negative + elif len(nums) == 0: + nums.append('-' + negcoords[j]) + # otherwise all other strings will be negative + else: + # unless we accidentally split a number that was in scientific notation + # and had a negative exponent (500.00e-1) + prev = nums[len(nums)-1] + if prev[len(prev)-1] in ['e', 'E']: + nums[len(nums)-1] = prev + '-' + negcoords[j] + else: + nums.append( '-'+negcoords[j] ) + + # if we have an odd number of points, return empty + if len(nums) % 2 != 0: return [] + + # now resolve into Decimal values + i = 0 + while i < len(nums): + try: + nums[i] = getcontext().create_decimal(nums[i]) + nums[i + 1] = getcontext().create_decimal(nums[i + 1]) + except decimal.InvalidOperation: # one of the lengths had a unit or is an invalid number + return [] + + i += 2 + + return nums + +def cleanPolygon(elem, options): + """ + Remove unnecessary closing point of polygon points attribute + """ + global numPointsRemovedFromPolygon + + pts = parseListOfPoints(elem.getAttribute('points')) + N = len(pts)/2 + if N >= 2: + (startx,starty) = pts[:2] + (endx,endy) = pts[-2:] + if startx == endx and starty == endy: + del pts[-2:] + numPointsRemovedFromPolygon += 1 + elem.setAttribute('points', scourCoordinates(pts, options, True)) + +def cleanPolyline(elem, options): + """ + Scour the polyline points attribute + """ + pts = parseListOfPoints(elem.getAttribute('points')) + elem.setAttribute('points', scourCoordinates(pts, options, True)) + +def serializePath(pathObj, options): + """ + Reserializes the path data with some cleanups. + """ + # elliptical arc commands must have comma/wsp separating the coordinates + # this fixes an issue outlined in Fix https://bugs.launchpad.net/scour/+bug/412754 + return ''.join([cmd + scourCoordinates(data, options, (cmd == 'a')) for cmd, data in pathObj]) + +def serializeTransform(transformObj): + """ + Reserializes the transform data with some cleanups. + """ + return ' '.join( + [command + '(' + ' '.join( + [scourUnitlessLength(number) for number in numbers] + ) + ')' + for command, numbers in transformObj] + ) + +def scourCoordinates(data, options, forceCommaWsp = False): + """ + Serializes coordinate data with some cleanups: + - removes all trailing zeros after the decimal + - integerize coordinates if possible + - removes extraneous whitespace + - adds spaces between values in a subcommand if required (or if forceCommaWsp is True) + """ + if data != None: + newData = [] + c = 0 + previousCoord = '' + for coord in data: + scouredCoord = scourUnitlessLength(coord, needsRendererWorkaround=options.renderer_workaround) + # only need the comma if the current number starts with a digit + # (numbers can start with - without needing a comma before) + # or if forceCommaWsp is True + # or if this number starts with a dot and the previous number + # had *no* dot or exponent (so we can go like -5.5.5 for -5.5,0.5 + # and 4e4.5 for 40000,0.5) + if c > 0 and (forceCommaWsp + or scouredCoord[0].isdigit() + or (scouredCoord[0] == '.' and not ('.' in previousCoord or 'e' in previousCoord)) + ): + newData.append( ' ' ) + + # add the scoured coordinate to the path string + newData.append( scouredCoord ) + previousCoord = scouredCoord + c += 1 + + # What we need to do to work around GNOME bugs 548494, 563933 and + # 620565, which are being fixed and unfixed in Ubuntu, is + # to make sure that a dot doesn't immediately follow a command + # (so 'h50' and 'h0.5' are allowed, but not 'h.5'). + # Then, we need to add a space character after any coordinates + # having an 'e' (scientific notation), so as to have the exponent + # separate from the next number. + if options.renderer_workaround: + if len(newData) > 0: + for i in xrange(1, len(newData)): + if newData[i][0] == '-' and 'e' in newData[i - 1]: + newData[i - 1] += ' ' + return ''.join(newData) + else: + return ''.join(newData) + + return '' + +def scourLength(length): + """ + Scours a length. Accepts units. + """ + length = SVGLength(length) + + return scourUnitlessLength(length.value) + Unit.str(length.units) + +def scourUnitlessLength(length, needsRendererWorkaround=False): # length is of a numeric type + """ + Scours the numeric part of a length only. Does not accept units. + + This is faster than scourLength on elements guaranteed not to + contain units. + """ + # reduce to the proper number of digits + if not isinstance(length, Decimal): + length = getcontext().create_decimal(str(length)) + # if the value is an integer, it may still have .0[...] attached to it for some reason + # remove those + if int(length) == length: + length = getcontext().create_decimal(int(length)) + + # gather the non-scientific notation version of the coordinate. + # this may actually be in scientific notation if the value is + # sufficiently large or small, so this is a misnomer. + nonsci = unicode(length).lower().replace("e+", "e") + if not needsRendererWorkaround: + if len(nonsci) > 2 and nonsci[:2] == '0.': + nonsci = nonsci[1:] # remove the 0, leave the dot + elif len(nonsci) > 3 and nonsci[:3] == '-0.': + nonsci = '-' + nonsci[2:] # remove the 0, leave the minus and dot + + if len(nonsci) > 3: # avoid calling normalize unless strictly necessary + # and then the scientific notation version, with E+NUMBER replaced with + # just eNUMBER, since SVG accepts this. + sci = unicode(length.normalize()).lower().replace("e+", "e") + + if len(sci) < len(nonsci): return sci + else: return nonsci + else: return nonsci + +def reducePrecision(element) : + """ + Because opacities, letter spacings, stroke widths and all that don't need + to be preserved in SVG files with 9 digits of precision. + + Takes all of these attributes, in the given element node and its children, + and reduces their precision to the current Decimal context's precision. + Also checks for the attributes actually being lengths, not 'inherit', 'none' + or anything that isn't an SVGLength. + + Returns the number of bytes saved after performing these reductions. + """ + num = 0 + + styles = _getStyle(element) + for lengthAttr in ['opacity', 'flood-opacity', 'fill-opacity', + 'stroke-opacity', 'stop-opacity', 'stroke-miterlimit', + 'stroke-dashoffset', 'letter-spacing', 'word-spacing', + 'kerning', 'font-size-adjust', 'font-size', + 'stroke-width']: + val = element.getAttribute(lengthAttr) + if val != '': + valLen = SVGLength(val) + if valLen.units != Unit.INVALID: # not an absolute/relative size or inherit, can be % though + newVal = scourLength(val) + if len(newVal) < len(val): + num += len(val) - len(newVal) + element.setAttribute(lengthAttr, newVal) + # repeat for attributes hidden in styles + if lengthAttr in styles.keys(): + val = styles[lengthAttr] + valLen = SVGLength(val) + if valLen.units != Unit.INVALID: + newVal = scourLength(val) + if len(newVal) < len(val): + num += len(val) - len(newVal) + styles[lengthAttr] = newVal + _setStyle(element, styles) + + for child in element.childNodes: + if child.nodeType == 1: + num += reducePrecision(child) + + return num + +def optimizeAngle(angle): + """ + Because any rotation can be expressed within 360 degrees + of any given number, and since negative angles sometimes + are one character longer than corresponding positive angle, + we shorten the number to one in the range to [-90, 270[. + """ + # First, we put the new angle in the range ]-360, 360[. + # The modulo operator yields results with the sign of the + # divisor, so for negative dividends, we preserve the sign + # of the angle. + if angle < 0: angle %= -360 + else: angle %= 360 + # 720 degrees is unneccessary, as 360 covers all angles. + # As "-x" is shorter than "35x" and "-xxx" one character + # longer than positive angles <= 260, we constrain angle + # range to [-90, 270[ (or, equally valid: ]-100, 260]). + if angle >= 270: angle -= 360 + elif angle < -90: angle += 360 + return angle + + +def optimizeTransform(transform): + """ + Optimises a series of transformations parsed from a single + transform="" attribute. + + The transformation list is modified in-place. + """ + # FIXME: reordering these would optimize even more cases: + # first: Fold consecutive runs of the same transformation + # extra: Attempt to cast between types to create sameness: + # "matrix(0 1 -1 0 0 0) rotate(180) scale(-1)" all + # are rotations (90, 180, 180) -- thus "rotate(90)" + # second: Simplify transforms where numbers are optional. + # third: Attempt to simplify any single remaining matrix() + # + # if there's only one transformation and it's a matrix, + # try to make it a shorter non-matrix transformation + # NOTE: as matrix(a b c d e f) in SVG means the matrix: + # |¯ a c e ¯| make constants |¯ A1 A2 A3 ¯| + # | b d f | translating them | B1 B2 B3 | + # |_ 0 0 1 _| to more readable |_ 0 0 1 _| + if len(transform) == 1 and transform[0][0] == 'matrix': + matrix = A1, B1, A2, B2, A3, B3 = transform[0][1] + # |¯ 1 0 0 ¯| + # | 0 1 0 | Identity matrix (no transformation) + # |_ 0 0 1 _| + if matrix == [1, 0, 0, 1, 0, 0]: + del transform[0] + # |¯ 1 0 X ¯| + # | 0 1 Y | Translation by (X, Y). + # |_ 0 0 1 _| + elif (A1 == 1 and A2 == 0 + and B1 == 0 and B2 == 1): + transform[0] = ('translate', [A3, B3]) + # |¯ X 0 0 ¯| + # | 0 Y 0 | Scaling by (X, Y). + # |_ 0 0 1 _| + elif ( A2 == 0 and A3 == 0 + and B1 == 0 and B3 == 0): + transform[0] = ('scale', [A1, B2]) + # |¯ cos(A) -sin(A) 0 ¯| Rotation by angle A, + # | sin(A) cos(A) 0 | clockwise, about the origin. + # |_ 0 0 1 _| A is in degrees, [-180...180]. + elif (A1 == B2 and -1 <= A1 <= 1 and A3 == 0 + and -B1 == A2 and -1 <= B1 <= 1 and B3 == 0 + # as cos² A + sin² A == 1 and as decimal trig is approximate: + # FIXME: the "epsilon" term here should really be some function + # of the precision of the (sin|cos)_A terms, not 1e-15: + and abs((B1 ** 2) + (A1 ** 2) - 1) < Decimal("1e-15")): + sin_A, cos_A = B1, A1 + # while asin(A) and acos(A) both only have an 180° range + # the sign of sin(A) and cos(A) varies across quadrants, + # letting us hone in on the angle the matrix represents: + # -- => < -90 | -+ => -90..0 | ++ => 0..90 | +- => >= 90 + # + # http://en.wikipedia.org/wiki/File:Sine_cosine_plot.svg + # shows asin has the correct angle the middle quadrants: + A = Decimal(str(math.degrees(math.asin(float(sin_A))))) + if cos_A < 0: # otherwise needs adjusting from the edges + if sin_A < 0: + A = -180 - A + else: + A = 180 - A + transform[0] = ('rotate', [A]) + + # Simplify transformations where numbers are optional. + for type, args in transform: + if type == 'translate': + # Only the X coordinate is required for translations. + # If the Y coordinate is unspecified, it's 0. + if len(args) == 2 and args[1] == 0: + del args[1] + elif type == 'rotate': + args[0] = optimizeAngle(args[0]) # angle + # Only the angle is required for rotations. + # If the coordinates are unspecified, it's the origin (0, 0). + if len(args) == 3 and args[1] == args[2] == 0: + del args[1:] + elif type == 'scale': + # Only the X scaling factor is required. + # If the Y factor is unspecified, it's the same as X. + if len(args) == 2 and args[0] == args[1]: + del args[1] + + # Attempt to coalesce runs of the same transformation. + # Translations followed immediately by other translations, + # rotations followed immediately by other rotations, + # scaling followed immediately by other scaling, + # are safe to add. + # Identity skewX/skewY are safe to remove, but how do they accrete? + # |¯ 1 0 0 ¯| + # | tan(A) 1 0 | skews X coordinates by angle A + # |_ 0 0 1 _| + # + # |¯ 1 tan(A) 0 ¯| + # | 0 1 0 | skews Y coordinates by angle A + # |_ 0 0 1 _| + # + # FIXME: A matrix followed immediately by another matrix + # would be safe to multiply together, too. + i = 1 + while i < len(transform): + currType, currArgs = transform[i] + prevType, prevArgs = transform[i - 1] + if currType == prevType == 'translate': + prevArgs[0] += currArgs[0] # x + # for y, only add if the second translation has an explicit y + if len(currArgs) == 2: + if len(prevArgs) == 2: + prevArgs[1] += currArgs[1] # y + elif len(prevArgs) == 1: + prevArgs.append(currArgs[1]) # y + del transform[i] + if prevArgs[0] == prevArgs[1] == 0: + # Identity translation! + i -= 1 + del transform[i] + elif (currType == prevType == 'rotate' + and len(prevArgs) == len(currArgs) == 1): + # Only coalesce if both rotations are from the origin. + prevArgs[0] = optimizeAngle(prevArgs[0] + currArgs[0]) + del transform[i] + elif currType == prevType == 'scale': + prevArgs[0] *= currArgs[0] # x + # handle an implicit y + if len(prevArgs) == 2 and len(currArgs) == 2: + # y1 * y2 + prevArgs[1] *= currArgs[1] + elif len(prevArgs) == 1 and len(currArgs) == 2: + # create y2 = uniformscalefactor1 * y2 + prevArgs.append(prevArgs[0] * currArgs[1]) + elif len(prevArgs) == 2 and len(currArgs) == 1: + # y1 * uniformscalefactor2 + prevArgs[1] *= currArgs[0] + del transform[i] + if prevArgs[0] == prevArgs[1] == 1: + # Identity scale! + i -= 1 + del transform[i] + else: + i += 1 + + # Some fixups are needed for single-element transformation lists, since + # the loop above was to coalesce elements with their predecessors in the + # list, and thus it required 2 elements. + i = 0 + while i < len(transform): + currType, currArgs = transform[i] + if ((currType == 'skewX' or currType == 'skewY') + and len(currArgs) == 1 and currArgs[0] == 0): + # Identity skew! + del transform[i] + elif ((currType == 'rotate') + and len(currArgs) == 1 and currArgs[0] == 0): + # Identity rotation! + del transform[i] + else: + i += 1 + +def optimizeTransforms(element, options) : + """ + Attempts to optimise transform specifications on the given node and its children. + + Returns the number of bytes saved after performing these reductions. + """ + num = 0 + + for transformAttr in ['transform', 'patternTransform', 'gradientTransform']: + val = element.getAttribute(transformAttr) + if val != '': + transform = svg_transform_parser.parse(val) + + optimizeTransform(transform) + + newVal = serializeTransform(transform) + + if len(newVal) < len(val): + if len(newVal): + element.setAttribute(transformAttr, newVal) + else: + element.removeAttribute(transformAttr) + num += len(val) - len(newVal) + + for child in element.childNodes: + if child.nodeType == 1: + num += optimizeTransforms(child, options) + + return num + +def removeComments(element) : + """ + Removes comments from the element and its children. + """ + global numCommentBytes + + if isinstance(element, xml.dom.minidom.Document): + # must process the document object separately, because its + # documentElement's nodes have None as their parentNode + # iterate in reverse order to prevent mess-ups with renumbering + for index in xrange(len(element.childNodes) - 1, -1, -1): + subelement = element.childNodes[index] + if isinstance(subelement, xml.dom.minidom.Comment): + numCommentBytes += len(subelement.data) + element.removeChild(subelement) + else: + removeComments(subelement) + elif isinstance(element, xml.dom.minidom.Comment): + numCommentBytes += len(element.data) + element.parentNode.removeChild(element) + else: + # iterate in reverse order to prevent mess-ups with renumbering + for index in xrange(len(element.childNodes) - 1, -1, -1): + subelement = element.childNodes[index] + removeComments(subelement) + +def embedRasters(element, options) : + import base64 + import urllib + """ + Converts raster references to inline images. + NOTE: there are size limits to base64-encoding handling in browsers + """ + global numRastersEmbedded + + href = element.getAttributeNS(NS['XLINK'],'href') + + # if xlink:href is set, then grab the id + if href != '' and len(href) > 1: + # find if href value has filename ext + ext = os.path.splitext(os.path.basename(href))[1].lower()[1:] + + # look for 'png', 'jpg', and 'gif' extensions + if ext == 'png' or ext == 'jpg' or ext == 'gif': + + # file:// URLs denote files on the local system too + if href[:7] == 'file://': + href = href[7:] + # does the file exist? + if os.path.isfile(href): + # if this is not an absolute path, set path relative + # to script file based on input arg + infilename = '.' + if options.infilename: infilename = options.infilename + href = os.path.join(os.path.dirname(infilename), href) + + rasterdata = '' + # test if file exists locally + if os.path.isfile(href): + # open raster file as raw binary + raster = open( href, "rb") + rasterdata = raster.read() + elif href[:7] == 'http://': + webFile = urllib.urlopen( href ) + rasterdata = webFile.read() + webFile.close() + + # ... should we remove all images which don't resolve? + if rasterdata != '' : + # base64-encode raster + b64eRaster = base64.b64encode( rasterdata ) + + # set href attribute to base64-encoded equivalent + if b64eRaster != '': + # PNG and GIF both have MIME Type 'image/[ext]', but + # JPEG has MIME Type 'image/jpeg' + if ext == 'jpg': + ext = 'jpeg' + + element.setAttributeNS(NS['XLINK'], 'href', 'data:image/' + ext + ';base64,' + b64eRaster) + numRastersEmbedded += 1 + del b64eRaster + +def properlySizeDoc(docElement, options): + # get doc width and height + w = SVGLength(docElement.getAttribute('width')) + h = SVGLength(docElement.getAttribute('height')) + + # if width/height are not unitless or px then it is not ok to rewrite them into a viewBox. + # well, it may be OK for Web browsers and vector editors, but not for librsvg. + if options.renderer_workaround: + if ((w.units != Unit.NONE and w.units != Unit.PX) or + (h.units != Unit.NONE and h.units != Unit.PX)): + return + + # else we have a statically sized image and we should try to remedy that + + # parse viewBox attribute + vbSep = re.split("\\s*\\,?\\s*", docElement.getAttribute('viewBox'), 3) + # if we have a valid viewBox we need to check it + vbWidth,vbHeight = 0,0 + if len(vbSep) == 4: + try: + # if x or y are specified and non-zero then it is not ok to overwrite it + vbX = float(vbSep[0]) + vbY = float(vbSep[1]) + if vbX != 0 or vbY != 0: + return + + # if width or height are not equal to doc width/height then it is not ok to overwrite it + vbWidth = float(vbSep[2]) + vbHeight = float(vbSep[3]) + if vbWidth != w.value or vbHeight != h.value: + return + # if the viewBox did not parse properly it is invalid and ok to overwrite it + except ValueError: + pass + + # at this point it's safe to set the viewBox and remove width/height + docElement.setAttribute('viewBox', '0 0 %s %s' % (w.value, h.value)) + docElement.removeAttribute('width') + docElement.removeAttribute('height') + +def remapNamespacePrefix(node, oldprefix, newprefix): + if node == None or node.nodeType != 1: return + + if node.prefix == oldprefix: + localName = node.localName + namespace = node.namespaceURI + doc = node.ownerDocument + parent = node.parentNode + + # create a replacement node + newNode = None + if newprefix != '': + newNode = doc.createElementNS(namespace, newprefix+":"+localName) + else: + newNode = doc.createElement(localName); + + # add all the attributes + attrList = node.attributes + for i in xrange(attrList.length): + attr = attrList.item(i) + newNode.setAttributeNS( attr.namespaceURI, attr.localName, attr.nodeValue) + + # clone and add all the child nodes + for child in node.childNodes: + newNode.appendChild(child.cloneNode(True)) + + # replace old node with new node + parent.replaceChild( newNode, node ) + # set the node to the new node in the remapped namespace prefix + node = newNode + + # now do all child nodes + for child in node.childNodes : + remapNamespacePrefix(child, oldprefix, newprefix) + +def makeWellFormed(str): + xml_ents = { '<':'<', '>':'>', '&':'&', "'":''', '"':'"'} + +# starr = [] +# for c in str: +# if c in xml_ents: +# starr.append(xml_ents[c]) +# else: +# starr.append(c) + + # this list comprehension is short-form for the above for-loop: + return ''.join([xml_ents[c] if c in xml_ents else c for c in str]) + +# hand-rolled serialization function that has the following benefits: +# - pretty printing +# - somewhat judicious use of whitespace +# - ensure id attributes are first +def serializeXML(element, options, ind = 0, preserveWhitespace = False): + outParts = [] + + indent = ind + I='' + if options.indent_type == 'tab': I='\t' + elif options.indent_type == 'space': I=' ' + + outParts.extend([(I * ind), '<', element.nodeName]) + + # always serialize the id or xml:id attributes first + if element.getAttribute('id') != '': + id = element.getAttribute('id') + quot = '"' + if id.find('"') != -1: + quot = "'" + outParts.extend([' id=', quot, id, quot]) + if element.getAttribute('xml:id') != '': + id = element.getAttribute('xml:id') + quot = '"' + if id.find('"') != -1: + quot = "'" + outParts.extend([' xml:id=', quot, id, quot]) + + # now serialize the other attributes + attrList = element.attributes + for num in xrange(attrList.length) : + attr = attrList.item(num) + if attr.nodeName == 'id' or attr.nodeName == 'xml:id': continue + # if the attribute value contains a double-quote, use single-quotes + quot = '"' + if attr.nodeValue.find('"') != -1: + quot = "'" + + attrValue = makeWellFormed( attr.nodeValue ) + + outParts.append(' ') + # preserve xmlns: if it is a namespace prefix declaration + if attr.prefix != None: + outParts.extend([attr.prefix, ':']) + elif attr.namespaceURI != None: + if attr.namespaceURI == 'http://www.w3.org/2000/xmlns/' and attr.nodeName.find('xmlns') == -1: + outParts.append('xmlns:') + elif attr.namespaceURI == 'http://www.w3.org/1999/xlink': + outParts.append('xlink:') + outParts.extend([attr.localName, '=', quot, attrValue, quot]) + + if attr.nodeName == 'xml:space': + if attrValue == 'preserve': + preserveWhitespace = True + elif attrValue == 'default': + preserveWhitespace = False + + # if no children, self-close + children = element.childNodes + if children.length > 0: + outParts.append('>') + + onNewLine = False + for child in element.childNodes: + # element node + if child.nodeType == 1: + if preserveWhitespace: + outParts.append(serializeXML(child, options, 0, preserveWhitespace)) + else: + outParts.extend(['\n', serializeXML(child, options, indent + 1, preserveWhitespace)]) + onNewLine = True + # text node + elif child.nodeType == 3: + # trim it only in the case of not being a child of an element + # where whitespace might be important + if preserveWhitespace: + outParts.append(makeWellFormed(child.nodeValue)) + else: + outParts.append(makeWellFormed(child.nodeValue.strip())) + # CDATA node + elif child.nodeType == 4: + outParts.extend(['', child.nodeValue, '']) + # Comment node + elif child.nodeType == 8: + outParts.extend(['']) + # TODO: entities, processing instructions, what else? + else: # ignore the rest + pass + + if onNewLine: outParts.append(I * ind) + outParts.extend(['']) + if indent > 0: outParts.append('\n') + else: + outParts.append('/>') + if indent > 0: outParts.append('\n') + + return "".join(outParts) + +# this is the main method +# input is a string representation of the input XML +# returns a string representation of the output XML +def scourString(in_string, options=None): + if options is None: + options = _options_parser.get_default_values() + getcontext().prec = options.digits + global numAttrsRemoved + global numStylePropsFixed + global numElemsRemoved + global numBytesSavedInColors + global numCommentsRemoved + global numBytesSavedInIDs + global numBytesSavedInLengths + global numBytesSavedInTransforms + doc = xml.dom.minidom.parseString(in_string) + + # for whatever reason this does not always remove all inkscape/sodipodi attributes/elements + # on the first pass, so we do it multiple times + # does it have to do with removal of children affecting the childlist? + if options.keep_editor_data == False: + while removeNamespacedElements( doc.documentElement, unwanted_ns ) > 0 : + pass + while removeNamespacedAttributes( doc.documentElement, unwanted_ns ) > 0 : + pass + + # remove the xmlns: declarations now + xmlnsDeclsToRemove = [] + attrList = doc.documentElement.attributes + for num in xrange(attrList.length) : + if attrList.item(num).nodeValue in unwanted_ns : + xmlnsDeclsToRemove.append(attrList.item(num).nodeName) + + for attr in xmlnsDeclsToRemove : + doc.documentElement.removeAttribute(attr) + numAttrsRemoved += 1 + + # ensure namespace for SVG is declared + # TODO: what if the default namespace is something else (i.e. some valid namespace)? + if doc.documentElement.getAttribute('xmlns') != 'http://www.w3.org/2000/svg': + doc.documentElement.setAttribute('xmlns', 'http://www.w3.org/2000/svg') + # TODO: throw error or warning? + + # check for redundant SVG namespace declaration + attrList = doc.documentElement.attributes + xmlnsDeclsToRemove = [] + redundantPrefixes = [] + for i in xrange(attrList.length): + attr = attrList.item(i) + name = attr.nodeName + val = attr.nodeValue + if name[0:6] == 'xmlns:' and val == 'http://www.w3.org/2000/svg': + redundantPrefixes.append(name[6:]) + xmlnsDeclsToRemove.append(name) + + for attrName in xmlnsDeclsToRemove: + doc.documentElement.removeAttribute(attrName) + + for prefix in redundantPrefixes: + remapNamespacePrefix(doc.documentElement, prefix, '') + + if options.strip_comments: + numCommentsRemoved = removeComments(doc) + + # repair style (remove unnecessary style properties and change them into XML attributes) + numStylePropsFixed = repairStyle(doc.documentElement, options) + + # convert colors to #RRGGBB format + if options.simple_colors: + numBytesSavedInColors = convertColors(doc.documentElement) + + # remove if the user wants to + if options.remove_metadata: + removeMetadataElements(doc) + + # flattend defs elements into just one defs element + flattenDefs(doc) + + # remove unreferenced gradients/patterns outside of defs + # and most unreferenced elements inside of defs + while removeUnreferencedElements(doc) > 0: + pass + + # remove empty defs, metadata, g + # NOTE: these elements will be removed if they just have whitespace-only text nodes + for tag in ['defs', 'metadata', 'g'] : + for elem in doc.documentElement.getElementsByTagName(tag) : + removeElem = not elem.hasChildNodes() + if removeElem == False : + for child in elem.childNodes : + if child.nodeType in [1, 4, 8]: + break + elif child.nodeType == 3 and not child.nodeValue.isspace(): + break + else: + removeElem = True + if removeElem : + elem.parentNode.removeChild(elem) + numElemsRemoved += 1 + + if options.strip_ids: + bContinueLooping = True + while bContinueLooping: + identifiedElements = unprotected_ids(doc, options) + referencedIDs = findReferencedElements(doc.documentElement) + bContinueLooping = (removeUnreferencedIDs(referencedIDs, identifiedElements) > 0) + + while removeDuplicateGradientStops(doc) > 0: + pass + + # remove gradients that are only referenced by one other gradient + while collapseSinglyReferencedGradients(doc) > 0: + pass + + # remove duplicate gradients + while removeDuplicateGradients(doc) > 0: + pass + + # create elements if there are runs of elements with the same attributes. + # this MUST be before moveCommonAttributesToParentGroup. + if options.group_create: + createGroupsForCommonAttributes(doc.documentElement) + + # move common attributes to parent group + # NOTE: the if the element's immediate children + # all have the same value for an attribute, it must not + # get moved to the element. The element + # doesn't accept fill=, stroke= etc.! + referencedIds = findReferencedElements(doc.documentElement) + for child in doc.documentElement.childNodes: + numAttrsRemoved += moveCommonAttributesToParentGroup(child, referencedIds) + + # remove unused attributes from parent + numAttrsRemoved += removeUnusedAttributesOnParent(doc.documentElement) + + # Collapse groups LAST, because we've created groups. If done before + # moveAttributesToParentGroup, empty 's may remain. + if options.group_collapse: + while removeNestedGroups(doc.documentElement) > 0: + pass + + # remove unnecessary closing point of polygons and scour points + for polygon in doc.documentElement.getElementsByTagName('polygon') : + cleanPolygon(polygon, options) + + # scour points of polyline + for polyline in doc.documentElement.getElementsByTagName('polyline') : + cleanPolyline(polyline, options) + + # clean path data + for elem in doc.documentElement.getElementsByTagName('path') : + if elem.getAttribute('d') == '': + elem.parentNode.removeChild(elem) + else: + cleanPath(elem, options) + + # shorten ID names as much as possible + if options.shorten_ids: + numBytesSavedInIDs += shortenIDs(doc, unprotected_ids(doc, options)) + + # scour lengths (including coordinates) + for type in ['svg', 'image', 'rect', 'circle', 'ellipse', 'line', 'linearGradient', 'radialGradient', 'stop', 'filter']: + for elem in doc.getElementsByTagName(type): + for attr in ['x', 'y', 'width', 'height', 'cx', 'cy', 'r', 'rx', 'ry', + 'x1', 'y1', 'x2', 'y2', 'fx', 'fy', 'offset']: + if elem.getAttribute(attr) != '': + elem.setAttribute(attr, scourLength(elem.getAttribute(attr))) + + # more length scouring in this function + numBytesSavedInLengths = reducePrecision(doc.documentElement) + + # remove default values of attributes + numAttrsRemoved += removeDefaultAttributeValues(doc.documentElement, options) + + # reduce the length of transformation attributes + numBytesSavedInTransforms = optimizeTransforms(doc.documentElement, options) + + # convert rasters references to base64-encoded strings + if options.embed_rasters: + for elem in doc.documentElement.getElementsByTagName('image') : + embedRasters(elem, options) + + # properly size the SVG document (ideally width/height should be 100% with a viewBox) + if options.enable_viewboxing: + properlySizeDoc(doc.documentElement, options) + + # output the document as a pretty string with a single space for indent + # NOTE: removed pretty printing because of this problem: + # http://ronrothman.com/public/leftbraned/xml-dom-minidom-toprettyxml-and-silly-whitespace/ + # rolled our own serialize function here to save on space, put id first, customize indentation, etc +# out_string = doc.documentElement.toprettyxml(' ') + out_string = serializeXML(doc.documentElement, options) + '\n' + + # now strip out empty lines + lines = [] + # Get rid of empty lines + for line in out_string.splitlines(True): + if line.strip(): + lines.append(line) + + # return the string with its XML prolog and surrounding comments + if options.strip_xml_prolog == False: + total_output = '\n' + else: + total_output = "" + + for child in doc.childNodes: + if child.nodeType == 1: + total_output += "".join(lines) + else: # doctypes, entities, comments + total_output += child.toxml() + '\n' + + return total_output + +# used mostly by unit tests +# input is a filename +# returns the minidom doc representation of the SVG +def scourXmlFile(filename, options=None): + in_string = open(filename).read() + out_string = scourString(in_string, options) + return xml.dom.minidom.parseString(out_string.encode('utf-8')) + +# GZ: Seems most other commandline tools don't do this, is it really wanted? +class HeaderedFormatter(optparse.IndentedHelpFormatter): + """ + Show application name, version number, and copyright statement + above usage information. + """ + def format_usage(self, usage): + return "%s %s\n%s\n%s" % (APP, VER, COPYRIGHT, + optparse.IndentedHelpFormatter.format_usage(self, usage)) + +# GZ: would prefer this to be in a function or class scope, but tests etc need +# access to the defaults anyway +_options_parser = optparse.OptionParser( + usage="%prog [-i input.svg] [-o output.svg] [OPTIONS]", + description=("If the input/output files are specified with a svgz" + " extension, then compressed SVG is assumed. If the input file is not" + " specified, stdin is used. If the output file is not specified, " + " stdout is used."), + formatter=HeaderedFormatter(max_help_position=30), + version=VER) + +_options_parser.add_option("--disable-simplify-colors", + action="store_false", dest="simple_colors", default=True, + help="won't convert all colors to #RRGGBB format") +_options_parser.add_option("--disable-style-to-xml", + action="store_false", dest="style_to_xml", default=True, + help="won't convert styles into XML attributes") +_options_parser.add_option("--disable-group-collapsing", + action="store_false", dest="group_collapse", default=True, + help="won't collapse elements") +_options_parser.add_option("--create-groups", + action="store_true", dest="group_create", default=False, + help="create elements for runs of elements with identical attributes") +_options_parser.add_option("--enable-id-stripping", + action="store_true", dest="strip_ids", default=False, + help="remove all un-referenced ID attributes") +_options_parser.add_option("--enable-comment-stripping", + action="store_true", dest="strip_comments", default=False, + help="remove all comments") +_options_parser.add_option("--shorten-ids", + action="store_true", dest="shorten_ids", default=False, + help="shorten all ID attributes to the least number of letters possible") +_options_parser.add_option("--disable-embed-rasters", + action="store_false", dest="embed_rasters", default=True, + help="won't embed rasters as base64-encoded data") +_options_parser.add_option("--keep-editor-data", + action="store_true", dest="keep_editor_data", default=False, + help="won't remove Inkscape, Sodipodi or Adobe Illustrator elements and attributes") +_options_parser.add_option("--remove-metadata", + action="store_true", dest="remove_metadata", default=False, + help="remove elements (which may contain license metadata etc.)") +_options_parser.add_option("--renderer-workaround", + action="store_true", dest="renderer_workaround", default=True, + help="work around various renderer bugs (currently only librsvg) (default)") +_options_parser.add_option("--no-renderer-workaround", + action="store_false", dest="renderer_workaround", default=True, + help="do not work around various renderer bugs (currently only librsvg)") +_options_parser.add_option("--strip-xml-prolog", + action="store_true", dest="strip_xml_prolog", default=False, + help="won't output the prolog") +_options_parser.add_option("--enable-viewboxing", + action="store_true", dest="enable_viewboxing", default=False, + help="changes document width/height to 100%/100% and creates viewbox coordinates") + +# GZ: this is confusing, most people will be thinking in terms of +# decimal places, which is not what decimal precision is doing +_options_parser.add_option("-p", "--set-precision", + action="store", type=int, dest="digits", default=5, + help="set number of significant digits (default: %default)") +_options_parser.add_option("-i", + action="store", dest="infilename", help=optparse.SUPPRESS_HELP) +_options_parser.add_option("-o", + action="store", dest="outfilename", help=optparse.SUPPRESS_HELP) +_options_parser.add_option("-q", "--quiet", + action="store_true", dest="quiet", default=False, + help="suppress non-error output") +_options_parser.add_option("--indent", + action="store", type="string", dest="indent_type", default="space", + help="indentation of the output: none, space, tab (default: %default)") +_options_parser.add_option("--protect-ids-noninkscape", + action="store_true", dest="protect_ids_noninkscape", default=False, + help="Don't change IDs not ending with a digit") +_options_parser.add_option("--protect-ids-list", + action="store", type="string", dest="protect_ids_list", default=None, + help="Don't change IDs given in a comma-separated list") +_options_parser.add_option("--protect-ids-prefix", + action="store", type="string", dest="protect_ids_prefix", default=None, + help="Don't change IDs starting with the given prefix") + +def maybe_gziped_file(filename, mode="r"): + if os.path.splitext(filename)[1].lower() in (".svgz", ".gz"): + import gzip + return gzip.GzipFile(filename, mode) + return file(filename, mode) + +def parse_args(args=None): + options, rargs = _options_parser.parse_args(args) + + if rargs: + _options_parser.error("Additional arguments not handled: %r, see --help" % rargs) + if options.digits < 0: + _options_parser.error("Can't have negative significant digits, see --help") + if not options.indent_type in ["tab", "space", "none"]: + _options_parser.error("Invalid value for --indent, see --help") + if options.infilename and options.outfilename and options.infilename == options.outfilename: + _options_parser.error("Input filename is the same as output filename") + + if options.infilename: + infile = maybe_gziped_file(options.infilename) + # GZ: could catch a raised IOError here and report + else: + # GZ: could sniff for gzip compression here + infile = sys.stdin + if options.outfilename: + outfile = maybe_gziped_file(options.outfilename, "wb") + else: + outfile = sys.stdout + + return options, [infile, outfile] + +def getReport(): + return ' Number of elements removed: ' + str(numElemsRemoved) + os.linesep + \ + ' Number of attributes removed: ' + str(numAttrsRemoved) + os.linesep + \ + ' Number of unreferenced id attributes removed: ' + str(numIDsRemoved) + os.linesep + \ + ' Number of style properties fixed: ' + str(numStylePropsFixed) + os.linesep + \ + ' Number of raster images embedded inline: ' + str(numRastersEmbedded) + os.linesep + \ + ' Number of path segments reduced/removed: ' + str(numPathSegmentsReduced) + os.linesep + \ + ' Number of bytes saved in path data: ' + str(numBytesSavedInPathData) + os.linesep + \ + ' Number of bytes saved in colors: ' + str(numBytesSavedInColors) + os.linesep + \ + ' Number of points removed from polygons: ' + str(numPointsRemovedFromPolygon) + os.linesep + \ + ' Number of bytes saved in comments: ' + str(numCommentBytes) + os.linesep + \ + ' Number of bytes saved in id attributes: ' + str(numBytesSavedInIDs) + os.linesep + \ + ' Number of bytes saved in lengths: ' + str(numBytesSavedInLengths) + os.linesep + \ + ' Number of bytes saved in transformations: ' + str(numBytesSavedInTransforms) + +if __name__ == '__main__': + if sys.platform == "win32": + from time import clock as get_tick + else: + # GZ: is this different from time.time() in any way? + def get_tick(): + return os.times()[0] + + start = get_tick() + + options, (input, output) = parse_args() + + if not options.quiet: + print >>sys.stderr, "%s %s\n%s" % (APP, VER, COPYRIGHT) + + # do the work + in_string = input.read() + out_string = scourString(in_string, options).encode("UTF-8") + output.write(out_string) + + # Close input and output files + input.close() + output.close() + + end = get_tick() + + # GZ: not using globals would be good too + if not options.quiet: + print >>sys.stderr, ' File:', input.name, \ + os.linesep + ' Time taken:', str(end-start) + 's' + os.linesep, \ + getReport() + + oldsize = len(in_string) + newsize = len(out_string) + sizediff = (newsize / oldsize) * 100 + print >>sys.stderr, ' Original file size:', oldsize, 'bytes;', \ + 'new file size:', newsize, 'bytes (' + str(sizediff)[:5] + '%)' diff --git a/share/extensions/scour/svg_regex.py b/share/extensions/scour/svg_regex.py new file mode 100755 index 000000000..6321bff0e --- /dev/null +++ b/share/extensions/scour/svg_regex.py @@ -0,0 +1,286 @@ +#!/usr/bin/env python +# This software is OSI Certified Open Source Software. +# OSI Certified is a certification mark of the Open Source Initiative. +# +# Copyright (c) 2006, Enthought, Inc. +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are met: +# +# * Redistributions of source code must retain the above copyright notice, this +# list of conditions and the following disclaimer. +# * Redistributions in binary form must reproduce the above copyright notice, +# this list of conditions and the following disclaimer in the documentation +# and/or other materials provided with the distribution. +# * Neither the name of Enthought, Inc. nor the names of its contributors may +# be used to endorse or promote products derived from this software without +# specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +# ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR +# ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +# (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON +# ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +# SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +""" Small hand-written recursive descent parser for SVG data. + + +In [1]: from svg_regex import svg_parser + +In [3]: svg_parser.parse('M 10,20 30,40V50 60 70') +Out[3]: [('M', [(10.0, 20.0), (30.0, 40.0)]), ('V', [50.0, 60.0, 70.0])] + +In [4]: svg_parser.parse('M 0.6051.5') # An edge case +Out[4]: [('M', [(0.60509999999999997, 0.5)])] + +In [5]: svg_parser.parse('M 100-200') # Another edge case +Out[5]: [('M', [(100.0, -200.0)])] +""" + +import re +from decimal import * + + +# Sentinel. +class _EOF(object): + def __repr__(self): + return 'EOF' +EOF = _EOF() + +lexicon = [ + ('float', r'[-+]?(?:(?:[0-9]*\.[0-9]+)|(?:[0-9]+\.?))(?:[Ee][-+]?[0-9]+)?'), + ('int', r'[-+]?[0-9]+'), + ('command', r'[AaCcHhLlMmQqSsTtVvZz]'), +] + + +class Lexer(object): + """ Break SVG path data into tokens. + + The SVG spec requires that tokens are greedy. This lexer relies on Python's + regexes defaulting to greediness. + + This style of implementation was inspired by this article: + + http://www.gooli.org/blog/a-simple-lexer-in-python/ + """ + def __init__(self, lexicon): + self.lexicon = lexicon + parts = [] + for name, regex in lexicon: + parts.append('(?P<%s>%s)' % (name, regex)) + self.regex_string = '|'.join(parts) + self.regex = re.compile(self.regex_string) + + def lex(self, text): + """ Yield (token_type, str_data) tokens. + + The last token will be (EOF, None) where EOF is the singleton object + defined in this module. + """ + for match in self.regex.finditer(text): + for name, _ in self.lexicon: + m = match.group(name) + if m is not None: + yield (name, m) + break + yield (EOF, None) + +svg_lexer = Lexer(lexicon) + + +class SVGPathParser(object): + """ Parse SVG data into a list of commands. + + Each distinct command will take the form of a tuple (command, data). The + `command` is just the character string that starts the command group in the + data, so 'M' for absolute moveto, 'm' for relative moveto, 'Z' for + closepath, etc. The kind of data it carries with it depends on the command. + For 'Z' (closepath), it's just None. The others are lists of individual + argument groups. Multiple elements in these lists usually mean to repeat the + command. The notable exception is 'M' (moveto) where only the first element + is truly a moveto. The remainder are implicit linetos. + + See the SVG documentation for the interpretation of the individual elements + for each command. + + The main method is `parse(text)`. It can only consume actual strings, not + filelike objects or iterators. + """ + + def __init__(self, lexer=svg_lexer): + self.lexer = lexer + + self.command_dispatch = { + 'Z': self.rule_closepath, + 'z': self.rule_closepath, + 'M': self.rule_moveto_or_lineto, + 'm': self.rule_moveto_or_lineto, + 'L': self.rule_moveto_or_lineto, + 'l': self.rule_moveto_or_lineto, + 'H': self.rule_orthogonal_lineto, + 'h': self.rule_orthogonal_lineto, + 'V': self.rule_orthogonal_lineto, + 'v': self.rule_orthogonal_lineto, + 'C': self.rule_curveto3, + 'c': self.rule_curveto3, + 'S': self.rule_curveto2, + 's': self.rule_curveto2, + 'Q': self.rule_curveto2, + 'q': self.rule_curveto2, + 'T': self.rule_curveto1, + 't': self.rule_curveto1, + 'A': self.rule_elliptical_arc, + 'a': self.rule_elliptical_arc, + } + +# self.number_tokens = set(['int', 'float']) + self.number_tokens = list(['int', 'float']) + + def parse(self, text): + """ Parse a string of SVG data. + """ + next = self.lexer.lex(text).next + token = next() + return self.rule_svg_path(next, token) + + def rule_svg_path(self, next, token): + commands = [] + while token[0] is not EOF: + if token[0] != 'command': + raise SyntaxError("expecting a command; got %r" % (token,)) + rule = self.command_dispatch[token[1]] + command_group, token = rule(next, token) + commands.append(command_group) + return commands + + def rule_closepath(self, next, token): + command = token[1] + token = next() + return (command, []), token + + def rule_moveto_or_lineto(self, next, token): + command = token[1] + token = next() + coordinates = [] + while token[0] in self.number_tokens: + pair, token = self.rule_coordinate_pair(next, token) + coordinates.extend(pair) + return (command, coordinates), token + + def rule_orthogonal_lineto(self, next, token): + command = token[1] + token = next() + coordinates = [] + while token[0] in self.number_tokens: + coord, token = self.rule_coordinate(next, token) + coordinates.append(coord) + return (command, coordinates), token + + def rule_curveto3(self, next, token): + command = token[1] + token = next() + coordinates = [] + while token[0] in self.number_tokens: + pair1, token = self.rule_coordinate_pair(next, token) + pair2, token = self.rule_coordinate_pair(next, token) + pair3, token = self.rule_coordinate_pair(next, token) + coordinates.extend(pair1) + coordinates.extend(pair2) + coordinates.extend(pair3) + return (command, coordinates), token + + def rule_curveto2(self, next, token): + command = token[1] + token = next() + coordinates = [] + while token[0] in self.number_tokens: + pair1, token = self.rule_coordinate_pair(next, token) + pair2, token = self.rule_coordinate_pair(next, token) + coordinates.extend(pair1) + coordinates.extend(pair2) + return (command, coordinates), token + + def rule_curveto1(self, next, token): + command = token[1] + token = next() + coordinates = [] + while token[0] in self.number_tokens: + pair1, token = self.rule_coordinate_pair(next, token) + coordinates.extend(pair1) + return (command, coordinates), token + + def rule_elliptical_arc(self, next, token): + command = token[1] + token = next() + arguments = [] + while token[0] in self.number_tokens: + rx = Decimal(token[1]) * 1 + if rx < Decimal("0.0"): + raise SyntaxError("expecting a nonnegative number; got %r" % (token,)) + + token = next() + if token[0] not in self.number_tokens: + raise SyntaxError("expecting a number; got %r" % (token,)) + ry = Decimal(token[1]) * 1 + if ry < Decimal("0.0"): + raise SyntaxError("expecting a nonnegative number; got %r" % (token,)) + + token = next() + if token[0] not in self.number_tokens: + raise SyntaxError("expecting a number; got %r" % (token,)) + axis_rotation = Decimal(token[1]) * 1 + + token = next() + if token[1] not in ('0', '1'): + raise SyntaxError("expecting a boolean flag; got %r" % (token,)) + large_arc_flag = Decimal(token[1]) * 1 + + token = next() + if token[1] not in ('0', '1'): + raise SyntaxError("expecting a boolean flag; got %r" % (token,)) + sweep_flag = Decimal(token[1]) * 1 + + token = next() + if token[0] not in self.number_tokens: + raise SyntaxError("expecting a number; got %r" % (token,)) + x = Decimal(token[1]) * 1 + + token = next() + if token[0] not in self.number_tokens: + raise SyntaxError("expecting a number; got %r" % (token,)) + y = Decimal(token[1]) * 1 + + token = next() + arguments.extend([rx, ry, axis_rotation, large_arc_flag, sweep_flag, x, y]) + + return (command, arguments), token + + def rule_coordinate(self, next, token): + if token[0] not in self.number_tokens: + raise SyntaxError("expecting a number; got %r" % (token,)) + x = getcontext().create_decimal(token[1]) + token = next() + return x, token + + + def rule_coordinate_pair(self, next, token): + # Inline these since this rule is so common. + if token[0] not in self.number_tokens: + raise SyntaxError("expecting a number; got %r" % (token,)) + x = getcontext().create_decimal(token[1]) + token = next() + if token[0] not in self.number_tokens: + raise SyntaxError("expecting a number; got %r" % (token,)) + y = getcontext().create_decimal(token[1]) + token = next() + return [x, y], token + + +svg_parser = SVGPathParser() diff --git a/share/extensions/scour/svg_transform.py b/share/extensions/scour/svg_transform.py new file mode 100755 index 000000000..860c1df5d --- /dev/null +++ b/share/extensions/scour/svg_transform.py @@ -0,0 +1,236 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +# SVG transformation list parser +# +# Copyright 2010 Louis Simard +# +# This file is part of Scour, http://www.codedread.com/scour/ +# +# This library is free software; you can redistribute it and/or modify +# it either under the terms of the Apache License, Version 2.0, or, at +# your option, under the terms and conditions of the GNU General +# Public License, Version 2 or newer as published by the Free Software +# Foundation. You may obtain a copy of these Licenses at: +# +# http://www.apache.org/licenses/LICENSE-2.0 +# https://www.gnu.org/licenses/old-licenses/gpl-2.0.html +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +""" Small recursive descent parser for SVG transform="" data. + + +In [1]: from svg_transform import svg_transform_parser + +In [3]: svg_transform_parser.parse('translate(50, 50)') +Out[3]: [('translate', [50.0, 50.0])] + +In [4]: svg_transform_parser.parse('translate(50)') +Out[4]: [('translate', [50.0])] + +In [5]: svg_transform_parser.parse('rotate(36 50,50)') +Out[5]: [('rotate', [36.0, 50.0, 50.0])] + +In [6]: svg_transform_parser.parse('rotate(36)') +Out[6]: [('rotate', [36.0])] + +In [7]: svg_transform_parser.parse('skewX(20)') +Out[7]: [('skewX', [20.0])] + +In [8]: svg_transform_parser.parse('skewY(40)') +Out[8]: [('skewX', [20.0])] + +In [9]: svg_transform_parser.parse('scale(2 .5)') +Out[9]: [('scale', [2.0, 0.5])] + +In [10]: svg_transform_parser.parse('scale(.5)') +Out[10]: [('scale', [0.5])] + +In [11]: svg_transform_parser.parse('matrix(1 0 50 0 1 80)') +Out[11]: [('matrix', [1.0, 0.0, 50.0, 0.0, 1.0, 80.0])] + +Multiple transformations are supported: + +In [12]: svg_transform_parser.parse('translate(30 -30) rotate(36)') +Out[12]: [('translate', [30.0, -30.0]), ('rotate', [36.0])] +""" + +import re +from decimal import * + + +# Sentinel. +class _EOF(object): + def __repr__(self): + return 'EOF' +EOF = _EOF() + +lexicon = [ + ('float', r'[-+]?(?:(?:[0-9]*\.[0-9]+)|(?:[0-9]+\.?))(?:[Ee][-+]?[0-9]+)?'), + ('int', r'[-+]?[0-9]+'), + ('command', r'(?:matrix|translate|scale|rotate|skew[XY])'), + ('coordstart', r'\('), + ('coordend', r'\)'), +] + + +class Lexer(object): + """ Break SVG path data into tokens. + + The SVG spec requires that tokens are greedy. This lexer relies on Python's + regexes defaulting to greediness. + + This style of implementation was inspired by this article: + + http://www.gooli.org/blog/a-simple-lexer-in-python/ + """ + def __init__(self, lexicon): + self.lexicon = lexicon + parts = [] + for name, regex in lexicon: + parts.append('(?P<%s>%s)' % (name, regex)) + self.regex_string = '|'.join(parts) + self.regex = re.compile(self.regex_string) + + def lex(self, text): + """ Yield (token_type, str_data) tokens. + + The last token will be (EOF, None) where EOF is the singleton object + defined in this module. + """ + for match in self.regex.finditer(text): + for name, _ in self.lexicon: + m = match.group(name) + if m is not None: + yield (name, m) + break + yield (EOF, None) + +svg_lexer = Lexer(lexicon) + + +class SVGTransformationParser(object): + """ Parse SVG transform="" data into a list of commands. + + Each distinct command will take the form of a tuple (type, data). The + `type` is the character string that defines the type of transformation in the + transform data, so either of "translate", "rotate", "scale", "matrix", + "skewX" and "skewY". Data is always a list of numbers contained within the + transformation's parentheses. + + See the SVG documentation for the interpretation of the individual elements + for each transformation. + + The main method is `parse(text)`. It can only consume actual strings, not + filelike objects or iterators. + """ + + def __init__(self, lexer=svg_lexer): + self.lexer = lexer + + self.command_dispatch = { + 'translate': self.rule_1or2numbers, + 'scale': self.rule_1or2numbers, + 'skewX': self.rule_1number, + 'skewY': self.rule_1number, + 'rotate': self.rule_1or3numbers, + 'matrix': self.rule_6numbers, + } + +# self.number_tokens = set(['int', 'float']) + self.number_tokens = list(['int', 'float']) + + def parse(self, text): + """ Parse a string of SVG transform="" data. + """ + next = self.lexer.lex(text).next + commands = [] + token = next() + while token[0] is not EOF: + command, token = self.rule_svg_transform(next, token) + commands.append(command) + return commands + + def rule_svg_transform(self, next, token): + if token[0] != 'command': + raise SyntaxError("expecting a transformation type; got %r" % (token,)) + command = token[1] + rule = self.command_dispatch[command] + token = next() + if token[0] != 'coordstart': + raise SyntaxError("expecting '('; got %r" % (token,)) + numbers, token = rule(next, token) + if token[0] != 'coordend': + raise SyntaxError("expecting ')'; got %r" % (token,)) + token = next() + return (command, numbers), token + + def rule_1or2numbers(self, next, token): + numbers = [] + # 1st number is mandatory + token = next() + number, token = self.rule_number(next, token) + numbers.append(number) + # 2nd number is optional + number, token = self.rule_optional_number(next, token) + if number is not None: + numbers.append(number) + + return numbers, token + + def rule_1number(self, next, token): + # this number is mandatory + token = next() + number, token = self.rule_number(next, token) + numbers = [number] + return numbers, token + + def rule_1or3numbers(self, next, token): + numbers = [] + # 1st number is mandatory + token = next() + number, token = self.rule_number(next, token) + numbers.append(number) + # 2nd number is optional + number, token = self.rule_optional_number(next, token) + if number is not None: + # but, if the 2nd number is provided, the 3rd is mandatory. + # we can't have just 2. + numbers.append(number) + + number, token = self.rule_number(next, token) + numbers.append(number) + + return numbers, token + + def rule_6numbers(self, next, token): + numbers = [] + token = next() + # all numbers are mandatory + for i in xrange(6): + number, token = self.rule_number(next, token) + numbers.append(number) + return numbers, token + + def rule_number(self, next, token): + if token[0] not in self.number_tokens: + raise SyntaxError("expecting a number; got %r" % (token,)) + x = Decimal(token[1]) * 1 + token = next() + return x, token + + def rule_optional_number(self, next, token): + if token[0] not in self.number_tokens: + return None, token + else: + x = Decimal(token[1]) * 1 + token = next() + return x, token + + +svg_transform_parser = SVGTransformationParser() diff --git a/share/extensions/scour/yocto_css.py b/share/extensions/scour/yocto_css.py new file mode 100755 index 000000000..c6bd0c37e --- /dev/null +++ b/share/extensions/scour/yocto_css.py @@ -0,0 +1,75 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +# yocto-css, an extremely bare minimum CSS parser +# +# Copyright 2009 Jeff Schiller +# +# This file is part of Scour, http://www.codedread.com/scour/ +# +# This library is free software; you can redistribute it and/or modify +# it either under the terms of the Apache License, Version 2.0, or, at +# your option, under the terms and conditions of the GNU General +# Public License, Version 2 or newer as published by the Free Software +# Foundation. You may obtain a copy of these Licenses at: +# +# http://www.apache.org/licenses/LICENSE-2.0 +# https://www.gnu.org/licenses/old-licenses/gpl-2.0.html +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# In order to resolve Bug 368716 (https://bugs.launchpad.net/scour/+bug/368716) +# scour needed a bare-minimum CSS parser in order to determine if some elements +# were still referenced by CSS properties. + +# I looked at css-py (a CSS parser built in Python), but that library +# is about 35k of Python and requires ply to be installed. I just need +# something very basic to suit scour's needs. + +# yocto-css takes a string of CSS and tries to spit out a list of rules +# A rule is an associative array (dictionary) with the following keys: +# - selector: contains the string of the selector (see CSS grammar) +# - properties: contains an associative array of CSS properties for this rule + +# TODO: need to build up some unit tests for yocto_css + +# stylesheet : [ CDO | CDC | S | statement ]*; +# statement : ruleset | at-rule; +# at-rule : ATKEYWORD S* any* [ block | ';' S* ]; +# block : '{' S* [ any | block | ATKEYWORD S* | ';' S* ]* '}' S*; +# ruleset : selector? '{' S* declaration? [ ';' S* declaration? ]* '}' S*; +# selector : any+; +# declaration : property S* ':' S* value; +# property : IDENT; +# value : [ any | block | ATKEYWORD S* ]+; +# any : [ IDENT | NUMBER | PERCENTAGE | DIMENSION | STRING +# | DELIM | URI | HASH | UNICODE-RANGE | INCLUDES +# | DASHMATCH | FUNCTION S* any* ')' +# | '(' S* any* ')' | '[' S* any* ']' ] S*; + +def parseCssString(str): + rules = [] + # first, split on } to get the rule chunks + chunks = str.split('}') + for chunk in chunks: + # second, split on { to get the selector and the list of properties + bits = chunk.split('{') + if len(bits) != 2: continue + rule = {} + rule['selector'] = bits[0].strip() + # third, split on ; to get the property declarations + bites = bits[1].strip().split(';') + if len(bites) < 1: continue + props = {} + for bite in bites: + # fourth, split on : to get the property name and value + nibbles = bite.strip().split(':') + if len(nibbles) != 2: continue + props[nibbles[0].strip()] = nibbles[1].strip() + rule['properties'] = props + rules.append(rule) + return rules diff --git a/share/extensions/svg_regex.py b/share/extensions/svg_regex.py deleted file mode 100755 index 6321bff0e..000000000 --- a/share/extensions/svg_regex.py +++ /dev/null @@ -1,286 +0,0 @@ -#!/usr/bin/env python -# This software is OSI Certified Open Source Software. -# OSI Certified is a certification mark of the Open Source Initiative. -# -# Copyright (c) 2006, Enthought, Inc. -# All rights reserved. -# -# Redistribution and use in source and binary forms, with or without -# modification, are permitted provided that the following conditions are met: -# -# * Redistributions of source code must retain the above copyright notice, this -# list of conditions and the following disclaimer. -# * Redistributions in binary form must reproduce the above copyright notice, -# this list of conditions and the following disclaimer in the documentation -# and/or other materials provided with the distribution. -# * Neither the name of Enthought, Inc. nor the names of its contributors may -# be used to endorse or promote products derived from this software without -# specific prior written permission. -# -# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND -# ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED -# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE -# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR -# ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES -# (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; -# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON -# ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT -# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS -# SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - -""" Small hand-written recursive descent parser for SVG data. - - -In [1]: from svg_regex import svg_parser - -In [3]: svg_parser.parse('M 10,20 30,40V50 60 70') -Out[3]: [('M', [(10.0, 20.0), (30.0, 40.0)]), ('V', [50.0, 60.0, 70.0])] - -In [4]: svg_parser.parse('M 0.6051.5') # An edge case -Out[4]: [('M', [(0.60509999999999997, 0.5)])] - -In [5]: svg_parser.parse('M 100-200') # Another edge case -Out[5]: [('M', [(100.0, -200.0)])] -""" - -import re -from decimal import * - - -# Sentinel. -class _EOF(object): - def __repr__(self): - return 'EOF' -EOF = _EOF() - -lexicon = [ - ('float', r'[-+]?(?:(?:[0-9]*\.[0-9]+)|(?:[0-9]+\.?))(?:[Ee][-+]?[0-9]+)?'), - ('int', r'[-+]?[0-9]+'), - ('command', r'[AaCcHhLlMmQqSsTtVvZz]'), -] - - -class Lexer(object): - """ Break SVG path data into tokens. - - The SVG spec requires that tokens are greedy. This lexer relies on Python's - regexes defaulting to greediness. - - This style of implementation was inspired by this article: - - http://www.gooli.org/blog/a-simple-lexer-in-python/ - """ - def __init__(self, lexicon): - self.lexicon = lexicon - parts = [] - for name, regex in lexicon: - parts.append('(?P<%s>%s)' % (name, regex)) - self.regex_string = '|'.join(parts) - self.regex = re.compile(self.regex_string) - - def lex(self, text): - """ Yield (token_type, str_data) tokens. - - The last token will be (EOF, None) where EOF is the singleton object - defined in this module. - """ - for match in self.regex.finditer(text): - for name, _ in self.lexicon: - m = match.group(name) - if m is not None: - yield (name, m) - break - yield (EOF, None) - -svg_lexer = Lexer(lexicon) - - -class SVGPathParser(object): - """ Parse SVG data into a list of commands. - - Each distinct command will take the form of a tuple (command, data). The - `command` is just the character string that starts the command group in the - data, so 'M' for absolute moveto, 'm' for relative moveto, 'Z' for - closepath, etc. The kind of data it carries with it depends on the command. - For 'Z' (closepath), it's just None. The others are lists of individual - argument groups. Multiple elements in these lists usually mean to repeat the - command. The notable exception is 'M' (moveto) where only the first element - is truly a moveto. The remainder are implicit linetos. - - See the SVG documentation for the interpretation of the individual elements - for each command. - - The main method is `parse(text)`. It can only consume actual strings, not - filelike objects or iterators. - """ - - def __init__(self, lexer=svg_lexer): - self.lexer = lexer - - self.command_dispatch = { - 'Z': self.rule_closepath, - 'z': self.rule_closepath, - 'M': self.rule_moveto_or_lineto, - 'm': self.rule_moveto_or_lineto, - 'L': self.rule_moveto_or_lineto, - 'l': self.rule_moveto_or_lineto, - 'H': self.rule_orthogonal_lineto, - 'h': self.rule_orthogonal_lineto, - 'V': self.rule_orthogonal_lineto, - 'v': self.rule_orthogonal_lineto, - 'C': self.rule_curveto3, - 'c': self.rule_curveto3, - 'S': self.rule_curveto2, - 's': self.rule_curveto2, - 'Q': self.rule_curveto2, - 'q': self.rule_curveto2, - 'T': self.rule_curveto1, - 't': self.rule_curveto1, - 'A': self.rule_elliptical_arc, - 'a': self.rule_elliptical_arc, - } - -# self.number_tokens = set(['int', 'float']) - self.number_tokens = list(['int', 'float']) - - def parse(self, text): - """ Parse a string of SVG data. - """ - next = self.lexer.lex(text).next - token = next() - return self.rule_svg_path(next, token) - - def rule_svg_path(self, next, token): - commands = [] - while token[0] is not EOF: - if token[0] != 'command': - raise SyntaxError("expecting a command; got %r" % (token,)) - rule = self.command_dispatch[token[1]] - command_group, token = rule(next, token) - commands.append(command_group) - return commands - - def rule_closepath(self, next, token): - command = token[1] - token = next() - return (command, []), token - - def rule_moveto_or_lineto(self, next, token): - command = token[1] - token = next() - coordinates = [] - while token[0] in self.number_tokens: - pair, token = self.rule_coordinate_pair(next, token) - coordinates.extend(pair) - return (command, coordinates), token - - def rule_orthogonal_lineto(self, next, token): - command = token[1] - token = next() - coordinates = [] - while token[0] in self.number_tokens: - coord, token = self.rule_coordinate(next, token) - coordinates.append(coord) - return (command, coordinates), token - - def rule_curveto3(self, next, token): - command = token[1] - token = next() - coordinates = [] - while token[0] in self.number_tokens: - pair1, token = self.rule_coordinate_pair(next, token) - pair2, token = self.rule_coordinate_pair(next, token) - pair3, token = self.rule_coordinate_pair(next, token) - coordinates.extend(pair1) - coordinates.extend(pair2) - coordinates.extend(pair3) - return (command, coordinates), token - - def rule_curveto2(self, next, token): - command = token[1] - token = next() - coordinates = [] - while token[0] in self.number_tokens: - pair1, token = self.rule_coordinate_pair(next, token) - pair2, token = self.rule_coordinate_pair(next, token) - coordinates.extend(pair1) - coordinates.extend(pair2) - return (command, coordinates), token - - def rule_curveto1(self, next, token): - command = token[1] - token = next() - coordinates = [] - while token[0] in self.number_tokens: - pair1, token = self.rule_coordinate_pair(next, token) - coordinates.extend(pair1) - return (command, coordinates), token - - def rule_elliptical_arc(self, next, token): - command = token[1] - token = next() - arguments = [] - while token[0] in self.number_tokens: - rx = Decimal(token[1]) * 1 - if rx < Decimal("0.0"): - raise SyntaxError("expecting a nonnegative number; got %r" % (token,)) - - token = next() - if token[0] not in self.number_tokens: - raise SyntaxError("expecting a number; got %r" % (token,)) - ry = Decimal(token[1]) * 1 - if ry < Decimal("0.0"): - raise SyntaxError("expecting a nonnegative number; got %r" % (token,)) - - token = next() - if token[0] not in self.number_tokens: - raise SyntaxError("expecting a number; got %r" % (token,)) - axis_rotation = Decimal(token[1]) * 1 - - token = next() - if token[1] not in ('0', '1'): - raise SyntaxError("expecting a boolean flag; got %r" % (token,)) - large_arc_flag = Decimal(token[1]) * 1 - - token = next() - if token[1] not in ('0', '1'): - raise SyntaxError("expecting a boolean flag; got %r" % (token,)) - sweep_flag = Decimal(token[1]) * 1 - - token = next() - if token[0] not in self.number_tokens: - raise SyntaxError("expecting a number; got %r" % (token,)) - x = Decimal(token[1]) * 1 - - token = next() - if token[0] not in self.number_tokens: - raise SyntaxError("expecting a number; got %r" % (token,)) - y = Decimal(token[1]) * 1 - - token = next() - arguments.extend([rx, ry, axis_rotation, large_arc_flag, sweep_flag, x, y]) - - return (command, arguments), token - - def rule_coordinate(self, next, token): - if token[0] not in self.number_tokens: - raise SyntaxError("expecting a number; got %r" % (token,)) - x = getcontext().create_decimal(token[1]) - token = next() - return x, token - - - def rule_coordinate_pair(self, next, token): - # Inline these since this rule is so common. - if token[0] not in self.number_tokens: - raise SyntaxError("expecting a number; got %r" % (token,)) - x = getcontext().create_decimal(token[1]) - token = next() - if token[0] not in self.number_tokens: - raise SyntaxError("expecting a number; got %r" % (token,)) - y = getcontext().create_decimal(token[1]) - token = next() - return [x, y], token - - -svg_parser = SVGPathParser() diff --git a/share/extensions/svg_transform.py b/share/extensions/svg_transform.py deleted file mode 100755 index 860c1df5d..000000000 --- a/share/extensions/svg_transform.py +++ /dev/null @@ -1,236 +0,0 @@ -#!/usr/bin/env python -# -*- coding: utf-8 -*- - -# SVG transformation list parser -# -# Copyright 2010 Louis Simard -# -# This file is part of Scour, http://www.codedread.com/scour/ -# -# This library is free software; you can redistribute it and/or modify -# it either under the terms of the Apache License, Version 2.0, or, at -# your option, under the terms and conditions of the GNU General -# Public License, Version 2 or newer as published by the Free Software -# Foundation. You may obtain a copy of these Licenses at: -# -# http://www.apache.org/licenses/LICENSE-2.0 -# https://www.gnu.org/licenses/old-licenses/gpl-2.0.html -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -""" Small recursive descent parser for SVG transform="" data. - - -In [1]: from svg_transform import svg_transform_parser - -In [3]: svg_transform_parser.parse('translate(50, 50)') -Out[3]: [('translate', [50.0, 50.0])] - -In [4]: svg_transform_parser.parse('translate(50)') -Out[4]: [('translate', [50.0])] - -In [5]: svg_transform_parser.parse('rotate(36 50,50)') -Out[5]: [('rotate', [36.0, 50.0, 50.0])] - -In [6]: svg_transform_parser.parse('rotate(36)') -Out[6]: [('rotate', [36.0])] - -In [7]: svg_transform_parser.parse('skewX(20)') -Out[7]: [('skewX', [20.0])] - -In [8]: svg_transform_parser.parse('skewY(40)') -Out[8]: [('skewX', [20.0])] - -In [9]: svg_transform_parser.parse('scale(2 .5)') -Out[9]: [('scale', [2.0, 0.5])] - -In [10]: svg_transform_parser.parse('scale(.5)') -Out[10]: [('scale', [0.5])] - -In [11]: svg_transform_parser.parse('matrix(1 0 50 0 1 80)') -Out[11]: [('matrix', [1.0, 0.0, 50.0, 0.0, 1.0, 80.0])] - -Multiple transformations are supported: - -In [12]: svg_transform_parser.parse('translate(30 -30) rotate(36)') -Out[12]: [('translate', [30.0, -30.0]), ('rotate', [36.0])] -""" - -import re -from decimal import * - - -# Sentinel. -class _EOF(object): - def __repr__(self): - return 'EOF' -EOF = _EOF() - -lexicon = [ - ('float', r'[-+]?(?:(?:[0-9]*\.[0-9]+)|(?:[0-9]+\.?))(?:[Ee][-+]?[0-9]+)?'), - ('int', r'[-+]?[0-9]+'), - ('command', r'(?:matrix|translate|scale|rotate|skew[XY])'), - ('coordstart', r'\('), - ('coordend', r'\)'), -] - - -class Lexer(object): - """ Break SVG path data into tokens. - - The SVG spec requires that tokens are greedy. This lexer relies on Python's - regexes defaulting to greediness. - - This style of implementation was inspired by this article: - - http://www.gooli.org/blog/a-simple-lexer-in-python/ - """ - def __init__(self, lexicon): - self.lexicon = lexicon - parts = [] - for name, regex in lexicon: - parts.append('(?P<%s>%s)' % (name, regex)) - self.regex_string = '|'.join(parts) - self.regex = re.compile(self.regex_string) - - def lex(self, text): - """ Yield (token_type, str_data) tokens. - - The last token will be (EOF, None) where EOF is the singleton object - defined in this module. - """ - for match in self.regex.finditer(text): - for name, _ in self.lexicon: - m = match.group(name) - if m is not None: - yield (name, m) - break - yield (EOF, None) - -svg_lexer = Lexer(lexicon) - - -class SVGTransformationParser(object): - """ Parse SVG transform="" data into a list of commands. - - Each distinct command will take the form of a tuple (type, data). The - `type` is the character string that defines the type of transformation in the - transform data, so either of "translate", "rotate", "scale", "matrix", - "skewX" and "skewY". Data is always a list of numbers contained within the - transformation's parentheses. - - See the SVG documentation for the interpretation of the individual elements - for each transformation. - - The main method is `parse(text)`. It can only consume actual strings, not - filelike objects or iterators. - """ - - def __init__(self, lexer=svg_lexer): - self.lexer = lexer - - self.command_dispatch = { - 'translate': self.rule_1or2numbers, - 'scale': self.rule_1or2numbers, - 'skewX': self.rule_1number, - 'skewY': self.rule_1number, - 'rotate': self.rule_1or3numbers, - 'matrix': self.rule_6numbers, - } - -# self.number_tokens = set(['int', 'float']) - self.number_tokens = list(['int', 'float']) - - def parse(self, text): - """ Parse a string of SVG transform="" data. - """ - next = self.lexer.lex(text).next - commands = [] - token = next() - while token[0] is not EOF: - command, token = self.rule_svg_transform(next, token) - commands.append(command) - return commands - - def rule_svg_transform(self, next, token): - if token[0] != 'command': - raise SyntaxError("expecting a transformation type; got %r" % (token,)) - command = token[1] - rule = self.command_dispatch[command] - token = next() - if token[0] != 'coordstart': - raise SyntaxError("expecting '('; got %r" % (token,)) - numbers, token = rule(next, token) - if token[0] != 'coordend': - raise SyntaxError("expecting ')'; got %r" % (token,)) - token = next() - return (command, numbers), token - - def rule_1or2numbers(self, next, token): - numbers = [] - # 1st number is mandatory - token = next() - number, token = self.rule_number(next, token) - numbers.append(number) - # 2nd number is optional - number, token = self.rule_optional_number(next, token) - if number is not None: - numbers.append(number) - - return numbers, token - - def rule_1number(self, next, token): - # this number is mandatory - token = next() - number, token = self.rule_number(next, token) - numbers = [number] - return numbers, token - - def rule_1or3numbers(self, next, token): - numbers = [] - # 1st number is mandatory - token = next() - number, token = self.rule_number(next, token) - numbers.append(number) - # 2nd number is optional - number, token = self.rule_optional_number(next, token) - if number is not None: - # but, if the 2nd number is provided, the 3rd is mandatory. - # we can't have just 2. - numbers.append(number) - - number, token = self.rule_number(next, token) - numbers.append(number) - - return numbers, token - - def rule_6numbers(self, next, token): - numbers = [] - token = next() - # all numbers are mandatory - for i in xrange(6): - number, token = self.rule_number(next, token) - numbers.append(number) - return numbers, token - - def rule_number(self, next, token): - if token[0] not in self.number_tokens: - raise SyntaxError("expecting a number; got %r" % (token,)) - x = Decimal(token[1]) * 1 - token = next() - return x, token - - def rule_optional_number(self, next, token): - if token[0] not in self.number_tokens: - return None, token - else: - x = Decimal(token[1]) * 1 - token = next() - return x, token - - -svg_transform_parser = SVGTransformationParser() diff --git a/share/extensions/yocto_css.py b/share/extensions/yocto_css.py deleted file mode 100755 index c6bd0c37e..000000000 --- a/share/extensions/yocto_css.py +++ /dev/null @@ -1,75 +0,0 @@ -#!/usr/bin/env python -# -*- coding: utf-8 -*- - -# yocto-css, an extremely bare minimum CSS parser -# -# Copyright 2009 Jeff Schiller -# -# This file is part of Scour, http://www.codedread.com/scour/ -# -# This library is free software; you can redistribute it and/or modify -# it either under the terms of the Apache License, Version 2.0, or, at -# your option, under the terms and conditions of the GNU General -# Public License, Version 2 or newer as published by the Free Software -# Foundation. You may obtain a copy of these Licenses at: -# -# http://www.apache.org/licenses/LICENSE-2.0 -# https://www.gnu.org/licenses/old-licenses/gpl-2.0.html -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -# In order to resolve Bug 368716 (https://bugs.launchpad.net/scour/+bug/368716) -# scour needed a bare-minimum CSS parser in order to determine if some elements -# were still referenced by CSS properties. - -# I looked at css-py (a CSS parser built in Python), but that library -# is about 35k of Python and requires ply to be installed. I just need -# something very basic to suit scour's needs. - -# yocto-css takes a string of CSS and tries to spit out a list of rules -# A rule is an associative array (dictionary) with the following keys: -# - selector: contains the string of the selector (see CSS grammar) -# - properties: contains an associative array of CSS properties for this rule - -# TODO: need to build up some unit tests for yocto_css - -# stylesheet : [ CDO | CDC | S | statement ]*; -# statement : ruleset | at-rule; -# at-rule : ATKEYWORD S* any* [ block | ';' S* ]; -# block : '{' S* [ any | block | ATKEYWORD S* | ';' S* ]* '}' S*; -# ruleset : selector? '{' S* declaration? [ ';' S* declaration? ]* '}' S*; -# selector : any+; -# declaration : property S* ':' S* value; -# property : IDENT; -# value : [ any | block | ATKEYWORD S* ]+; -# any : [ IDENT | NUMBER | PERCENTAGE | DIMENSION | STRING -# | DELIM | URI | HASH | UNICODE-RANGE | INCLUDES -# | DASHMATCH | FUNCTION S* any* ')' -# | '(' S* any* ')' | '[' S* any* ']' ] S*; - -def parseCssString(str): - rules = [] - # first, split on } to get the rule chunks - chunks = str.split('}') - for chunk in chunks: - # second, split on { to get the selector and the list of properties - bits = chunk.split('{') - if len(bits) != 2: continue - rule = {} - rule['selector'] = bits[0].strip() - # third, split on ; to get the property declarations - bites = bits[1].strip().split(';') - if len(bites) < 1: continue - props = {} - for bite in bites: - # fourth, split on : to get the property name and value - nibbles = bite.strip().split(':') - if len(nibbles) != 2: continue - props[nibbles[0].strip()] = nibbles[1].strip() - rule['properties'] = props - rules.append(rule) - return rules -- cgit v1.2.3