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#ifdef QA_SRC_ROOT
207 wxString fontDir = wxString::FromUTF8( QA_SRC_ROOT ) + wxS( "/thirdparty/libwmf/fonts" );
208#else
209 wxString fontDir = PATHS::GetStockDataPath() + wxS( "/libwmf/fonts" );
210#endif
211
212 for( const char* file : { "NimbusSans-Regular.t1", "NimbusSans-Bold.t1", "NimbusSans-Italic.t1" } )
213 {
214 wxCharBuffer path = ( fontDir + wxS( "/" ) + wxString::FromUTF8( file ) ).utf8_str();
215 BOOST_REQUIRE( FcConfigAppFontAddFile( config.get(), reinterpret_cast<const FcChar8*>( path.data() ) ) );
216 }
217
218 return config;
219 }();
220
221 std::string svg = "<svg width=\"300\" height=\"150\" xmlns=\"http://www.w3.org/2000/svg\">"
222 + aBody + "</svg>";
223 return SVG_IMAGE( nsvgParseWithFontConfig( svg.data(), "px", 96, fonts.get() ), nsvgDelete );
224}
225
226std::vector<NSVGshape*> svgShapes( const SVG_IMAGE& aImage )
227{
228 std::vector<NSVGshape*> result;
229
230 for( NSVGshape* shape = aImage->shapes; shape; shape = shape->next )
231 result.push_back( shape );
232
233 return result;
234}
235}
236
237
238BOOST_AUTO_TEST_CASE( TextOutlinesRasterize )
239{
240 auto image = parseSvgText( "<text x=\"10\" y=\"60\" font-family=\"sans-serif\" font-size=\"40\">"
241 "<![CDATA[UMC]]></text>" );
243 BOOST_REQUIRE_EQUAL( svgShapes( image ).size(), 3 );
244 std::unique_ptr<NSVGrasterizer, decltype( &nsvgDeleteRasterizer )> rasterizer(
245 nsvgCreateRasterizer(), nsvgDeleteRasterizer );
246 BOOST_REQUIRE( rasterizer );
247 std::vector<unsigned char> pixels( 300 * 150 * 4 );
248 nsvgRasterize( rasterizer.get(), image.get(), 0, 0, 1, pixels.data(), 300, 150, 300 * 4 );
249 size_t ink = 0;
250
251 for( size_t i = 3; i < pixels.size(); i += 4 )
252 ink += pixels[i] != 0;
253
254 BOOST_CHECK_GT( ink, 200 );
255}
256
257
258BOOST_AUTO_TEST_CASE( TextEntitiesAndCdata )
259{
260 auto encoded = parseSvgText( "<text y=\"50\">&lt;&amp;&#x3a9;&#233;</text>" );
261 auto literal = parseSvgText( "<text y=\"50\"><![CDATA[<&Ωé]]></text>" );
262 auto unexpanded = parseSvgText( "<text y=\"50\"><![CDATA[&lt;]]></text>" );
263 BOOST_REQUIRE( encoded );
264 BOOST_REQUIRE( literal );
265 BOOST_REQUIRE( unexpanded );
266 auto a = svgShapes( encoded );
267 auto b = svgShapes( literal );
268 BOOST_REQUIRE_EQUAL( a.size(), 4 );
269 BOOST_REQUIRE_EQUAL( b.size(), a.size() );
270 BOOST_CHECK_EQUAL( svgShapes( unexpanded ).size(), 4 );
271
272 for( size_t i = 0; i < a.size(); ++i )
273 {
274 for( int axis = 0; axis < 4; ++axis )
275 BOOST_CHECK_SMALL( a[i]->bounds[axis] - b[i]->bounds[axis], 0.001f );
276 }
277}
278
279
280BOOST_AUTO_TEST_CASE( TextPositionListsAndSpans )
281{
282 auto image = parseSvgText( "<text x=\"10 50 100\" y=\"40 60 80\" font-size=\"20\">"
283 "H<tspan dx=\"3\" dy=\"4\">H</tspan>H</text>" );
285 auto shapes = svgShapes( image );
286 BOOST_REQUIRE_EQUAL( shapes.size(), 3 );
287 BOOST_CHECK_SMALL( shapes[1]->bounds[0] - shapes[0]->bounds[0] - 43, 0.001f );
288 BOOST_CHECK_SMALL( shapes[1]->bounds[1] - shapes[0]->bounds[1] - 24, 0.001f );
289 BOOST_CHECK_SMALL( shapes[2]->bounds[0] - shapes[0]->bounds[0] - 90, 0.001f );
290 BOOST_CHECK_SMALL( shapes[2]->bounds[1] - shapes[0]->bounds[1] - 40, 0.001f );
291}
292
293
294BOOST_AUTO_TEST_CASE( TextAnchorCoversSpans )
295{
296 auto start = parseSvgText( "<text x=\"150\" y=\"60\" font-size=\"30\">H<tspan>H</tspan>H</text>" );
297 auto middle = parseSvgText( "<text x=\"150\" y=\"60\" font-size=\"30\" text-anchor=\"middle\">"
298 "H<tspan>H</tspan>H</text>" );
299 auto end = parseSvgText( "<text x=\"150\" y=\"60\" font-size=\"30\" text-anchor=\"end\">"
300 "H<tspan>H</tspan>H</text>" );
301 BOOST_REQUIRE( start );
302 BOOST_REQUIRE( middle );
304 auto a = svgShapes( start );
305 auto b = svgShapes( middle );
306 auto c = svgShapes( end );
307 BOOST_REQUIRE_EQUAL( a.size(), 3 );
308 BOOST_REQUIRE_EQUAL( b.size(), 3 );
309 BOOST_REQUIRE_EQUAL( c.size(), 3 );
310 float shift = a[0]->bounds[0] - b[0]->bounds[0];
311 BOOST_CHECK_GT( shift, 20 );
312
313 for( size_t i = 0; i < a.size(); ++i )
314 {
315 BOOST_CHECK_SMALL( a[i]->bounds[0] - b[i]->bounds[0] - shift, 0.001f );
316 BOOST_CHECK_SMALL( a[i]->bounds[0] - c[i]->bounds[0] - 2 * shift, 0.001f );
317 }
318}
319
320
321BOOST_AUTO_TEST_CASE( TextTransformsAndInheritedStyle )
322{
323 auto image = parseSvgText( "<g font-family=\"sans-serif\" font-size=\"20\" fill=\"#123456\">"
324 "<text x=\"10\" y=\"40\">H</text>"
325 "<text x=\"10\" y=\"40\" transform=\"translate(100,0) rotate(90)\">H</text>"
326 "<text x=\"10\" y=\"40\" font-size=\"40\">H</text></g>" );
328 auto shapes = svgShapes( image );
329 BOOST_REQUIRE_EQUAL( shapes.size(), 3 );
330 BOOST_CHECK_EQUAL( shapes[0]->fill.color, 0xff563412u );
331 BOOST_CHECK_SMALL( shapes[1]->bounds[0] - ( 100 - shapes[0]->bounds[3] ), 0.001f );
332 BOOST_CHECK_SMALL( shapes[1]->bounds[1] - shapes[0]->bounds[0], 0.001f );
333 float height = shapes[0]->bounds[3] - shapes[0]->bounds[1];
334 BOOST_CHECK_SMALL( shapes[2]->bounds[3] - shapes[2]->bounds[1] - 2 * height, 0.05f );
335}
336
337
338BOOST_AUTO_TEST_CASE( UpdatedParserPreservesLocalDefaults )
339{
340 auto image = parseSvgText( "<style>.s { fill: #ff0000; }</style>"
341 "<path style=\"fill:#00ff00\" class=\"s\" d=\"M0 0 L10 0 L10 10 Z\"/>" );
343 BOOST_REQUIRE( image->shapes );
344 BOOST_CHECK_EQUAL( image->shapes->fill.color, 0xff00ff00u );
345 BOOST_CHECK_EQUAL( image->shapes->strokeWidth, 0 );
346 std::string svg = "<svg width=\"200\" viewBox=\"0 0 100 100\"><rect width=\"100\" height=\"100\"/></svg>";
347 SVG_IMAGE inferred( nsvgParse( svg.data(), "px", 96 ), nsvgDelete );
348 BOOST_REQUIRE( inferred );
349 BOOST_CHECK_EQUAL( inferred->width, 200 );
350 BOOST_CHECK_EQUAL( inferred->height, 100 );
351 BOOST_REQUIRE( inferred->shapes );
352 BOOST_CHECK_EQUAL( inferred->shapes->bounds[0], 50 );
353}
354
355BOOST_AUTO_TEST_CASE( TextWhitespaceAndFontVariants )
356{
357 auto regular = parseSvgText( "<text y=\"50\" font-family=\"sans-serif\" font-size=\"30\"> H H </text>" );
358 auto spans = parseSvgText( "<text y=\"50\" font-family=\"sans-serif\" font-size=\"30\">"
359 "H <tspan> H</tspan></text>" );
360 auto bold = parseSvgText( "<text y=\"50\" font-family=\"sans-serif\" font-size=\"30\" font-weight=\"700\">H</text>" );
361 auto italic = parseSvgText( "<text y=\"50\" font-family=\"sans-serif\" font-size=\"30\" font-style=\"italic\">H</text>" );
362 BOOST_REQUIRE( regular );
363 BOOST_REQUIRE( spans );
364 BOOST_REQUIRE( bold );
365 BOOST_REQUIRE( italic );
366 auto a = svgShapes( regular );
367 auto b = svgShapes( spans );
368 BOOST_REQUIRE_EQUAL( a.size(), 2 );
369 BOOST_REQUIRE_EQUAL( b.size(), 2 );
370 BOOST_REQUIRE( bold->shapes );
371 BOOST_REQUIRE( italic->shapes );
372 BOOST_CHECK_SMALL( a[1]->bounds[0] - b[1]->bounds[0], 0.001f );
373 float width = a[0]->bounds[2] - a[0]->bounds[0];
374 BOOST_CHECK_GT( bold->shapes->bounds[2] - bold->shapes->bounds[0], width );
375 BOOST_CHECK_GT( italic->shapes->bounds[2] - italic->shapes->bounds[0], width );
376}
377
378
379BOOST_AUTO_TEST_CASE( DefinitionsDoNotConsumeTextState )
380{
381 auto reference = parseSvgText( "<text x=\"150\" y=\"60\" text-anchor=\"middle\" fill=\"#123456\">HH</text>"
382 "<text x=\"20\" y=\"100\">H</text>" );
383 auto definitions = parseSvgText( "<text x=\"150\" y=\"60\" text-anchor=\"middle\" fill=\"#123456\">H"
384 "<defs><g><tspan>ignored</tspan></g><defs><tspan>nested</tspan></defs>"
385 "<tspan>also ignored</tspan></defs>H</text><text x=\"20\" y=\"100\">H</text>" );
386 BOOST_REQUIRE( reference );
387 BOOST_REQUIRE( definitions );
388 auto a = svgShapes( reference );
389 auto b = svgShapes( definitions );
390 BOOST_REQUIRE_EQUAL( a.size(), 3 );
391 BOOST_REQUIRE_EQUAL( b.size(), a.size() );
392
393 for( size_t i = 0; i < a.size(); ++i )
394 {
395 BOOST_CHECK_EQUAL( a[i]->fill.color, b[i]->fill.color );
396
397 for( int axis = 0; axis < 4; ++axis )
398 BOOST_CHECK_SMALL( a[i]->bounds[axis] - b[i]->bounds[axis], 0.001f );
399 }
400}
401
402
403BOOST_AUTO_TEST_CASE( InvalidNumericEntitiesStayLiteral )
404{
405 auto encoded = parseSvgText( "<text y=\"50\">&#zz;&#xzz;&#12z;</text>" );
406 auto literal = parseSvgText( "<text y=\"50\"><![CDATA[&#zz;&#xzz;&#12z;]]></text>" );
407 BOOST_REQUIRE( encoded );
408 BOOST_REQUIRE( literal );
409 auto a = svgShapes( encoded );
410 auto b = svgShapes( literal );
411 BOOST_REQUIRE_EQUAL( a.size(), 17 );
412 BOOST_REQUIRE_EQUAL( b.size(), a.size() );
413
414 for( size_t i = 0; i < a.size(); ++i )
415 {
416 for( int axis = 0; axis < 4; ++axis )
417 BOOST_CHECK_SMALL( a[i]->bounds[axis] - b[i]->bounds[axis], 0.001f );
418 }
419}
420
421
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:707