KiCad PCB EDA Suite
Loading...
Searching...
No Matches
test_svg_import.cpp
Go to the documentation of this file.
1/*
2 * This program source code file is part of KiCad, a free EDA CAD application.
3 *
4 * Copyright The KiCad Developers, see AUTHORS.TXT for contributors.
5 *
6 * This program is free software; you can redistribute it and/or
7 * modify it under the terms of the GNU General Public License
8 * as published by the Free Software Foundation; either version 2
9 * of the License, or (at your option) any later version.
10 *
11 * This program is distributed in the hope that it will be useful,
12 * but WITHOUT ANY WARRANTY; without even the implied warranty of
13 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 * GNU General Public License for more details.
15 *
16 * You should have received a copy of the GNU General Public License
17 * along with this program. If not, see <https://www.gnu.org/licenses/>.
18 */
19
21
24#include <eda_item.h>
25#include <base_units.h>
26#include <nanosvg.h>
27#include <nanosvgrast.h>
28#include <memory>
29#include <fontconfig/fontconfig.h>
30#include <paths.h>
31
32
34{
35public:
40
41 void AddLine( const VECTOR2D& aStart, const VECTOR2D& aEnd,
42 const IMPORTED_STROKE& aStroke ) override
43 {
44 m_lines.push_back( { aStart, aEnd } );
45 }
46
47 void AddCircle( const VECTOR2D& aCenter, double aRadius, const IMPORTED_STROKE& aStroke,
48 bool aFilled, const COLOR4D& aFillColor ) override
49 {
50 }
51
52 void AddArc( const VECTOR2D& aCenter, const VECTOR2D& aStart, const EDA_ANGLE& aAngle,
53 const IMPORTED_STROKE& aStroke ) override
54 {
55 }
56
57 void AddEllipse( const VECTOR2D& aCenter, double aMajorRadius, double aMinorRadius, const EDA_ANGLE& aRotation,
58 const IMPORTED_STROKE& aStroke, bool aFilled, const COLOR4D& aFillColor ) override
59 {
60 }
61
62 void AddEllipseArc( const VECTOR2D& aCenter, double aMajorRadius, double aMinorRadius, const EDA_ANGLE& aRotation,
63 const EDA_ANGLE& aStartAngle, const EDA_ANGLE& aEndAngle,
64 const IMPORTED_STROKE& aStroke ) override
65 {
66 }
67
68 void AddPolygon( const std::vector<VECTOR2D>& aVertices, const IMPORTED_STROKE& aStroke,
69 bool aFilled, const COLOR4D& aFillColor ) override
70 {
71 m_polygons.push_back( aVertices );
72 }
73
74 void AddText( const VECTOR2D& aOrigin, const wxString& aText, double aHeight, double aWidth,
75 double aThickness, double aOrientation, GR_TEXT_H_ALIGN_T aHJustify,
76 GR_TEXT_V_ALIGN_T aVJustify, const COLOR4D& aColor ) override
77 {
78 }
79
80 void AddSpline( const VECTOR2D& aStart, const VECTOR2D& aBezierControl1,
81 const VECTOR2D& aBezierControl2, const VECTOR2D& aEnd,
82 const IMPORTED_STROKE& aStroke ) override
83 {
84 m_splines.push_back( { aStart, aBezierControl1, aBezierControl2, aEnd } );
85 }
86
87 std::vector<std::pair<VECTOR2D, VECTOR2D>> m_lines;
88 std::vector<std::vector<VECTOR2D>> m_polygons;
89 std::vector<std::tuple<VECTOR2D, VECTOR2D, VECTOR2D, VECTOR2D>> m_splines;
90};
91
92
93BOOST_AUTO_TEST_SUITE( SvgImport )
94
95
96
103BOOST_AUTO_TEST_CASE( SingleNodeClosedPath )
104{
105 const char* svg =
106 "<?xml version=\"1.0\" encoding=\"UTF-8\"?>"
107 "<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 100 100\""
108 " width=\"100mm\" height=\"100mm\">"
109 "<path d=\"M 50,80 C 80,20 20,20 50,80 Z\" fill=\"black\" stroke=\"none\" />"
110 "</svg>";
111
112 wxMemoryBuffer buf;
113 buf.AppendData( svg, strlen( svg ) + 1 );
114
115 SVG_IMPORT_PLUGIN plugin;
117 plugin.SetImporter( &importer );
118
119 BOOST_REQUIRE( plugin.LoadFromMemory( buf ) );
120 BOOST_REQUIRE( plugin.Import() );
121
122 // The single-node closed path should produce at least one polygon with non-degenerate area
123 BOOST_REQUIRE_MESSAGE( !importer.m_polygons.empty(),
124 "Single-node closed SVG path should produce a polygon" );
125
126 const auto& poly = importer.m_polygons[0];
127
128 BOOST_CHECK_MESSAGE( poly.size() > 3,
129 "Polygon should have more than 3 vertices (got " << poly.size() << ")" );
130
131 // Verify the polygon has non-zero area by checking that not all vertices are identical
132 bool hasDistinctVertices = false;
133
134 for( size_t i = 1; i < poly.size(); ++i )
135 {
136 if( poly[i] != poly[0] )
137 {
138 hasDistinctVertices = true;
139 break;
140 }
141 }
142
143 BOOST_CHECK_MESSAGE( hasDistinctVertices,
144 "Polygon vertices should not all be at the same point" );
145
146 // Compute approximate area using the shoelace formula to verify non-degenerate polygon
147 double area = 0.0;
148
149 for( size_t i = 0; i < poly.size(); ++i )
150 {
151 size_t j = ( i + 1 ) % poly.size();
152 area += poly[i].x * poly[j].y;
153 area -= poly[j].x * poly[i].y;
154 }
155
156 area = std::fabs( area ) / 2.0;
157
158 BOOST_CHECK_MESSAGE( area > 0.01,
159 "Polygon should have non-zero area (got " << area << ")" );
160}
161
162
166BOOST_AUTO_TEST_CASE( MultiNodeClosedPath )
167{
168 const char* svg =
169 "<?xml version=\"1.0\" encoding=\"UTF-8\"?>"
170 "<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 100 100\""
171 " width=\"100mm\" height=\"100mm\">"
172 "<path d=\"M 10,10 C 40,0 60,0 90,10 C 100,40 100,60 90,90"
173 " C 60,100 40,100 10,90 C 0,60 0,40 10,10 Z\" fill=\"black\" stroke=\"none\" />"
174 "</svg>";
175
176 wxMemoryBuffer buf;
177 buf.AppendData( svg, strlen( svg ) + 1 );
178
179 SVG_IMPORT_PLUGIN plugin;
181 plugin.SetImporter( &importer );
182
183 BOOST_REQUIRE( plugin.LoadFromMemory( buf ) );
184 BOOST_REQUIRE( plugin.Import() );
185
186 BOOST_REQUIRE_MESSAGE( !importer.m_polygons.empty(),
187 "Multi-node closed SVG path should produce a polygon" );
188
189 BOOST_CHECK_MESSAGE( importer.m_polygons[0].size() > 4,
190 "Multi-node polygon should have many vertices" );
191}
192
193
194namespace
195{
196using SVG_IMAGE = std::unique_ptr<NSVGimage, decltype( &nsvgDelete )>;
197
198SVG_IMAGE parseSvgText( const std::string& aBody )
199{
200 using FONT_CONFIG = std::unique_ptr<FcConfig, decltype( &FcConfigDestroy )>;
201 static FONT_CONFIG fonts = []
202 {
203 FONT_CONFIG config( FcConfigCreate(), FcConfigDestroy );
205
206 for( const char* file : { "NimbusSans-Regular.t1", "NimbusSans-Bold.t1", "NimbusSans-Italic.t1" } )
207 {
208 wxCharBuffer path = ( PATHS::GetStockDataPath() + wxS( "/libwmf/fonts/" )
209 + wxString::FromUTF8( file ) ).utf8_str();
210 BOOST_REQUIRE( FcConfigAppFontAddFile( config.get(), reinterpret_cast<const FcChar8*>( path.data() ) ) );
211 }
212
213 return config;
214 }();
215
216 std::string svg = "<svg width=\"300\" height=\"150\" xmlns=\"http://www.w3.org/2000/svg\">"
217 + aBody + "</svg>";
218 return SVG_IMAGE( nsvgParseWithFontConfig( svg.data(), "px", 96, fonts.get() ), nsvgDelete );
219}
220
221std::vector<NSVGshape*> svgShapes( const SVG_IMAGE& aImage )
222{
223 std::vector<NSVGshape*> result;
224
225 for( NSVGshape* shape = aImage->shapes; shape; shape = shape->next )
226 result.push_back( shape );
227
228 return result;
229}
230}
231
232
233BOOST_AUTO_TEST_CASE( TextOutlinesRasterize )
234{
235 auto image = parseSvgText( "<text x=\"10\" y=\"60\" font-family=\"sans-serif\" font-size=\"40\">"
236 "<![CDATA[UMC]]></text>" );
238 BOOST_REQUIRE_EQUAL( svgShapes( image ).size(), 3 );
239 std::unique_ptr<NSVGrasterizer, decltype( &nsvgDeleteRasterizer )> rasterizer(
240 nsvgCreateRasterizer(), nsvgDeleteRasterizer );
241 BOOST_REQUIRE( rasterizer );
242 std::vector<unsigned char> pixels( 300 * 150 * 4 );
243 nsvgRasterize( rasterizer.get(), image.get(), 0, 0, 1, pixels.data(), 300, 150, 300 * 4 );
244 size_t ink = 0;
245
246 for( size_t i = 3; i < pixels.size(); i += 4 )
247 ink += pixels[i] != 0;
248
249 BOOST_CHECK_GT( ink, 200 );
250}
251
252
253BOOST_AUTO_TEST_CASE( TextEntitiesAndCdata )
254{
255 auto encoded = parseSvgText( "<text y=\"50\">&lt;&amp;&#x3a9;&#233;</text>" );
256 auto literal = parseSvgText( "<text y=\"50\"><![CDATA[<&Ωé]]></text>" );
257 auto unexpanded = parseSvgText( "<text y=\"50\"><![CDATA[&lt;]]></text>" );
258 BOOST_REQUIRE( encoded );
259 BOOST_REQUIRE( literal );
260 BOOST_REQUIRE( unexpanded );
261 auto a = svgShapes( encoded );
262 auto b = svgShapes( literal );
263 BOOST_REQUIRE_EQUAL( a.size(), 4 );
264 BOOST_REQUIRE_EQUAL( b.size(), a.size() );
265 BOOST_CHECK_EQUAL( svgShapes( unexpanded ).size(), 4 );
266
267 for( size_t i = 0; i < a.size(); ++i )
268 {
269 for( int axis = 0; axis < 4; ++axis )
270 BOOST_CHECK_SMALL( a[i]->bounds[axis] - b[i]->bounds[axis], 0.001f );
271 }
272}
273
274
275BOOST_AUTO_TEST_CASE( TextPositionListsAndSpans )
276{
277 auto image = parseSvgText( "<text x=\"10 50 100\" y=\"40 60 80\" font-size=\"20\">"
278 "H<tspan dx=\"3\" dy=\"4\">H</tspan>H</text>" );
280 auto shapes = svgShapes( image );
281 BOOST_REQUIRE_EQUAL( shapes.size(), 3 );
282 BOOST_CHECK_SMALL( shapes[1]->bounds[0] - shapes[0]->bounds[0] - 43, 0.001f );
283 BOOST_CHECK_SMALL( shapes[1]->bounds[1] - shapes[0]->bounds[1] - 24, 0.001f );
284 BOOST_CHECK_SMALL( shapes[2]->bounds[0] - shapes[0]->bounds[0] - 90, 0.001f );
285 BOOST_CHECK_SMALL( shapes[2]->bounds[1] - shapes[0]->bounds[1] - 40, 0.001f );
286}
287
288
289BOOST_AUTO_TEST_CASE( TextAnchorCoversSpans )
290{
291 auto start = parseSvgText( "<text x=\"150\" y=\"60\" font-size=\"30\">H<tspan>H</tspan>H</text>" );
292 auto middle = parseSvgText( "<text x=\"150\" y=\"60\" font-size=\"30\" text-anchor=\"middle\">"
293 "H<tspan>H</tspan>H</text>" );
294 auto end = parseSvgText( "<text x=\"150\" y=\"60\" font-size=\"30\" text-anchor=\"end\">"
295 "H<tspan>H</tspan>H</text>" );
296 BOOST_REQUIRE( start );
297 BOOST_REQUIRE( middle );
299 auto a = svgShapes( start );
300 auto b = svgShapes( middle );
301 auto c = svgShapes( end );
302 BOOST_REQUIRE_EQUAL( a.size(), 3 );
303 BOOST_REQUIRE_EQUAL( b.size(), 3 );
304 BOOST_REQUIRE_EQUAL( c.size(), 3 );
305 float shift = a[0]->bounds[0] - b[0]->bounds[0];
306 BOOST_CHECK_GT( shift, 20 );
307
308 for( size_t i = 0; i < a.size(); ++i )
309 {
310 BOOST_CHECK_SMALL( a[i]->bounds[0] - b[i]->bounds[0] - shift, 0.001f );
311 BOOST_CHECK_SMALL( a[i]->bounds[0] - c[i]->bounds[0] - 2 * shift, 0.001f );
312 }
313}
314
315
316BOOST_AUTO_TEST_CASE( TextTransformsAndInheritedStyle )
317{
318 auto image = parseSvgText( "<g font-family=\"sans-serif\" font-size=\"20\" fill=\"#123456\">"
319 "<text x=\"10\" y=\"40\">H</text>"
320 "<text x=\"10\" y=\"40\" transform=\"translate(100,0) rotate(90)\">H</text>"
321 "<text x=\"10\" y=\"40\" font-size=\"40\">H</text></g>" );
323 auto shapes = svgShapes( image );
324 BOOST_REQUIRE_EQUAL( shapes.size(), 3 );
325 BOOST_CHECK_EQUAL( shapes[0]->fill.color, 0xff563412u );
326 BOOST_CHECK_SMALL( shapes[1]->bounds[0] - ( 100 - shapes[0]->bounds[3] ), 0.001f );
327 BOOST_CHECK_SMALL( shapes[1]->bounds[1] - shapes[0]->bounds[0], 0.001f );
328 float height = shapes[0]->bounds[3] - shapes[0]->bounds[1];
329 BOOST_CHECK_SMALL( shapes[2]->bounds[3] - shapes[2]->bounds[1] - 2 * height, 0.05f );
330}
331
332
333BOOST_AUTO_TEST_CASE( UpdatedParserPreservesLocalDefaults )
334{
335 auto image = parseSvgText( "<style>.s { fill: #ff0000; }</style>"
336 "<path style=\"fill:#00ff00\" class=\"s\" d=\"M0 0 L10 0 L10 10 Z\"/>" );
338 BOOST_REQUIRE( image->shapes );
339 BOOST_CHECK_EQUAL( image->shapes->fill.color, 0xff00ff00u );
340 BOOST_CHECK_EQUAL( image->shapes->strokeWidth, 0 );
341 std::string svg = "<svg width=\"200\" viewBox=\"0 0 100 100\"><rect width=\"100\" height=\"100\"/></svg>";
342 SVG_IMAGE inferred( nsvgParse( svg.data(), "px", 96 ), nsvgDelete );
343 BOOST_REQUIRE( inferred );
344 BOOST_CHECK_EQUAL( inferred->width, 200 );
345 BOOST_CHECK_EQUAL( inferred->height, 100 );
346 BOOST_REQUIRE( inferred->shapes );
347 BOOST_CHECK_EQUAL( inferred->shapes->bounds[0], 50 );
348}
349
350BOOST_AUTO_TEST_CASE( TextWhitespaceAndFontVariants )
351{
352 auto regular = parseSvgText( "<text y=\"50\" font-family=\"sans-serif\" font-size=\"30\"> H H </text>" );
353 auto spans = parseSvgText( "<text y=\"50\" font-family=\"sans-serif\" font-size=\"30\">"
354 "H <tspan> H</tspan></text>" );
355 auto bold = parseSvgText( "<text y=\"50\" font-family=\"sans-serif\" font-size=\"30\" font-weight=\"700\">H</text>" );
356 auto italic = parseSvgText( "<text y=\"50\" font-family=\"sans-serif\" font-size=\"30\" font-style=\"italic\">H</text>" );
357 BOOST_REQUIRE( regular );
358 BOOST_REQUIRE( spans );
359 BOOST_REQUIRE( bold );
360 BOOST_REQUIRE( italic );
361 auto a = svgShapes( regular );
362 auto b = svgShapes( spans );
363 BOOST_REQUIRE_EQUAL( a.size(), 2 );
364 BOOST_REQUIRE_EQUAL( b.size(), 2 );
365 BOOST_REQUIRE( bold->shapes );
366 BOOST_REQUIRE( italic->shapes );
367 BOOST_CHECK_SMALL( a[1]->bounds[0] - b[1]->bounds[0], 0.001f );
368 float width = a[0]->bounds[2] - a[0]->bounds[0];
369 BOOST_CHECK_GT( bold->shapes->bounds[2] - bold->shapes->bounds[0], width );
370 BOOST_CHECK_GT( italic->shapes->bounds[2] - italic->shapes->bounds[0], width );
371}
372
373
374BOOST_AUTO_TEST_CASE( DefinitionsDoNotConsumeTextState )
375{
376 auto reference = parseSvgText( "<text x=\"150\" y=\"60\" text-anchor=\"middle\" fill=\"#123456\">HH</text>"
377 "<text x=\"20\" y=\"100\">H</text>" );
378 auto definitions = parseSvgText( "<text x=\"150\" y=\"60\" text-anchor=\"middle\" fill=\"#123456\">H"
379 "<defs><g><tspan>ignored</tspan></g><defs><tspan>nested</tspan></defs>"
380 "<tspan>also ignored</tspan></defs>H</text><text x=\"20\" y=\"100\">H</text>" );
381 BOOST_REQUIRE( reference );
382 BOOST_REQUIRE( definitions );
383 auto a = svgShapes( reference );
384 auto b = svgShapes( definitions );
385 BOOST_REQUIRE_EQUAL( a.size(), 3 );
386 BOOST_REQUIRE_EQUAL( b.size(), a.size() );
387
388 for( size_t i = 0; i < a.size(); ++i )
389 {
390 BOOST_CHECK_EQUAL( a[i]->fill.color, b[i]->fill.color );
391
392 for( int axis = 0; axis < 4; ++axis )
393 BOOST_CHECK_SMALL( a[i]->bounds[axis] - b[i]->bounds[axis], 0.001f );
394 }
395}
396
397
398BOOST_AUTO_TEST_CASE( InvalidNumericEntitiesStayLiteral )
399{
400 auto encoded = parseSvgText( "<text y=\"50\">&#zz;&#xzz;&#12z;</text>" );
401 auto literal = parseSvgText( "<text y=\"50\"><![CDATA[&#zz;&#xzz;&#12z;]]></text>" );
402 BOOST_REQUIRE( encoded );
403 BOOST_REQUIRE( literal );
404 auto a = svgShapes( encoded );
405 auto b = svgShapes( literal );
406 BOOST_REQUIRE_EQUAL( a.size(), 17 );
407 BOOST_REQUIRE_EQUAL( b.size(), a.size() );
408
409 for( size_t i = 0; i < a.size(); ++i )
410 {
411 for( int axis = 0; axis < 4; ++axis )
412 BOOST_CHECK_SMALL( a[i]->bounds[axis] - b[i]->bounds[axis], 0.001f );
413 }
414}
415
416
constexpr double PCB_IU_PER_MM
Pcbnew IU is 1 nanometer.
Definition base_units.h:68
double m_millimeterToIu
Factor to convert millimeters to Internal Units.
virtual void SetImporter(GRAPHICS_IMPORTER *aImporter)
Set the receiver of the imported shapes.
A clone of IMPORTED_STROKE, but with floating-point width.
A color representation with 4 components: red, green, blue, alpha.
Definition color4d.h:101
static wxString GetStockDataPath(bool aRespectRunFromBuildDir=true)
Gets the stock (install) data path, which is the base path for things like scripting,...
Definition paths.cpp:233
bool Import() override
Actually imports the file.
bool LoadFromMemory(const wxMemoryBuffer &aMemBuffer) override
Set memory buffer with content for import.
void AddSpline(const VECTOR2D &aStart, const VECTOR2D &aBezierControl1, const VECTOR2D &aBezierControl2, const VECTOR2D &aEnd, const IMPORTED_STROKE &aStroke) override
Create an object representing an arc.
void AddText(const VECTOR2D &aOrigin, const wxString &aText, double aHeight, double aWidth, double aThickness, double aOrientation, GR_TEXT_H_ALIGN_T aHJustify, GR_TEXT_V_ALIGN_T aVJustify, const COLOR4D &aColor) override
Create an object representing a text.
std::vector< std::vector< VECTOR2D > > m_polygons
void AddArc(const VECTOR2D &aCenter, const VECTOR2D &aStart, const EDA_ANGLE &aAngle, const IMPORTED_STROKE &aStroke) override
Create an object representing an arc.
void AddCircle(const VECTOR2D &aCenter, double aRadius, const IMPORTED_STROKE &aStroke, bool aFilled, const COLOR4D &aFillColor) override
Create an object representing a circle.
void AddEllipseArc(const VECTOR2D &aCenter, double aMajorRadius, double aMinorRadius, const EDA_ANGLE &aRotation, const EDA_ANGLE &aStartAngle, const EDA_ANGLE &aEndAngle, const IMPORTED_STROKE &aStroke) override
Create an object representing an elliptical arc.
void AddLine(const VECTOR2D &aStart, const VECTOR2D &aEnd, const IMPORTED_STROKE &aStroke) override
Create an object representing a line segment.
std::vector< std::tuple< VECTOR2D, VECTOR2D, VECTOR2D, VECTOR2D > > m_splines
void AddEllipse(const VECTOR2D &aCenter, double aMajorRadius, double aMinorRadius, const EDA_ANGLE &aRotation, const IMPORTED_STROKE &aStroke, bool aFilled, const COLOR4D &aFillColor) override
Create an object representing a closed ellipse.
std::vector< std::pair< VECTOR2D, VECTOR2D > > m_lines
void AddPolygon(const std::vector< VECTOR2D > &aVertices, const IMPORTED_STROKE &aStroke, bool aFilled, const COLOR4D &aFillColor) override
Create an object representing a polygon.
BOOST_AUTO_TEST_CASE(HorizontalAlignment)
BOOST_AUTO_TEST_SUITE(CadstarPartParser)
BOOST_REQUIRE(intersection.has_value()==c.ExpectedIntersection.has_value())
BOOST_AUTO_TEST_SUITE_END()
std::string path
VECTOR2I end
BOOST_AUTO_TEST_CASE(SingleNodeClosedPath)
Regression test for https://gitlab.com/kicad/code/kicad/-/issues/11445.
wxString result
Test unit parsing edge cases and error handling.
BOOST_CHECK_EQUAL(result, "25.4")
GR_TEXT_H_ALIGN_T
This is API surface mapped to common.types.HorizontalAlignment.
GR_TEXT_V_ALIGN_T
This is API surface mapped to common.types.VertialAlignment.
VECTOR2< double > VECTOR2D
Definition vector2d.h:682