KiCad PCB EDA Suite
Loading...
Searching...
No Matches
test_svg_plotter.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 modify it
7 * under the terms of the GNU General Public License as published by the
8 * Free Software Foundation, either version 3 of the License, or (at your
9 * option) any later version.
10 *
11 * This program is distributed in the hope that it will be useful, but
12 * WITHOUT ANY WARRANTY; without even the implied warranty of
13 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
14 * 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#include <qa_utils/file_utils.h>
23
24#include <wx/ffile.h>
25
26#include <page_info.h>
28
29#include <memory>
30#include <set>
31#include <string>
32#include <vector>
33
34
35namespace utf = boost::unit_test;
36
37namespace
38{
39
41wxString plotFilledShape( const COLOR4D& aColor, bool aMirrored = false, bool aFitToBoard = false )
42{
43 KI_TEST::SCOPED_TEMP_DIR tempDir( "kicad_svg_plotter" );
44 const wxString tempFile = tempDir.CreateChildFileStr( "plot.svg" );
45
46 SVG_PLOTTER plotter;
47 PAGE_INFO pageInfo;
48
49 pageInfo.SetWidthMils( 1000 );
50 pageInfo.SetHeightMils( 1000 );
51 plotter.SetPageSettings( pageInfo );
52 plotter.SetViewport( VECTOR2I( 0, 0 ), 1, 1.0, aMirrored );
53
54 // If fit to board, define the board at (20000, 20000) with size 10000x10000 IU
55 if( aFitToBoard )
56 plotter.SetPlotBBox( BOX2I( VECTOR2I( 20000, 20000 ), VECTOR2I( 10000, 10000 ) ) );
57
58 plotter.SetColorMode( true );
59
60 BOOST_REQUIRE( plotter.OpenFile( tempFile ) );
61 BOOST_REQUIRE( plotter.StartPlot( wxT( "1" ) ) );
62
63 plotter.SetColor( aColor );
64
65 VECTOR2I shapeOrigin;
66 VECTOR2I shapeSize{ 4000, 4000 };
67
68 // Draw the shape in the page somewhere.
69 if( aFitToBoard )
70 shapeOrigin = VECTOR2I( 20000, 20000 );
71 else
72 shapeOrigin = VECTOR2I( 100, 100 );
73
74 // Actually draw the shape.
75 plotter.Rect( shapeOrigin, shapeOrigin + shapeSize, FILL_T::FILLED_SHAPE, 10 );
76
77 BOOST_REQUIRE( plotter.EndPlot() );
78
79 wxFFile fileForReading( tempFile, wxT( "r" ) );
80 BOOST_REQUIRE( fileForReading.IsOpened() );
81
82 wxString content;
83 fileForReading.ReadAll( &content );
84 fileForReading.Close();
85
86 return content;
87}
88
89
91std::set<std::string> fillOpacities( const wxString& aSvg )
92{
93 const std::string doc = aSvg.ToStdString();
94 const std::string key = "fill-opacity:";
95 std::set<std::string> values;
96
97 for( size_t pos = doc.find( key ); pos != std::string::npos; pos = doc.find( key, pos + 1 ) )
98 {
99 size_t start = pos + key.size();
100 size_t end = doc.find_first_not_of( "0123456789.", start );
101
102 values.insert( doc.substr( start, end - start ) );
103 }
104
105 return values;
106}
107
108
109std::set<std::string> strokeOpacities( const wxString& aSvg )
110{
111 const std::string doc = aSvg.ToStdString();
112 const std::string key = "stroke-opacity:";
113 std::set<std::string> values;
114
115 for( size_t pos = doc.find( key ); pos != std::string::npos; pos = doc.find( key, pos + 1 ) )
116 {
117 size_t start = pos + key.size();
118 size_t end = doc.find_first_not_of( "0123456789.", start );
119
120 values.insert( doc.substr( start, end - start ) );
121 }
122
123 return values;
124}
125
126
130class SVG_PLOT
131{
132public:
133 SVG_PLOT( const wxString& aSvg )
134 {
135 tl::expected<wxXmlDocument, wxString> document = KI_TEST::LoadSvg( aSvg );
136
137 if( !document.has_value() )
138 BOOST_FAIL( document.error() );
139
140 m_document = std::make_unique<wxXmlDocument>( std::move( *document ) );
141
142 tl::expected<KI_TEST::SVG_VIEWBOX, wxString> viewBox = KI_TEST::ParseViewBox( *m_document->GetRoot() );
143
144 if( !viewBox.has_value() )
145 BOOST_FAIL( viewBox.error() );
146
147 m_viewBox = *viewBox;
148 m_rect = KI_TEST::FindFirstRect( *m_document->GetRoot() );
149 BOOST_REQUIRE( m_rect );
150 }
151
152 const KI_TEST::SVG_VIEWBOX& ViewBox() const { return m_viewBox; }
153
154 wxString RectAttribute( const wxString& aName ) const { return m_rect->GetAttribute( aName ); }
155
156private:
157 std::unique_ptr<wxXmlDocument> m_document;
158 KI_TEST::SVG_VIEWBOX m_viewBox = {};
159 const wxXmlNode* m_rect = nullptr;
160};
161
162} // anonymous namespace
163
164
165BOOST_AUTO_TEST_SUITE( SvgPlotter )
166
167
168BOOST_AUTO_TEST_CASE( FillOpacityMatchesColorAlpha )
169{
170 std::set<std::string> values = fillOpacities( plotFilledShape( COLOR4D( 1.0, 0.0, 0.0, 0.5 ) ) );
171
172 // The plotter writes m_precision (4) digits, so a half-transparent brush is exactly 0.5000
173 BOOST_CHECK_EQUAL( values.count( "0.5000" ), 1 );
174 BOOST_CHECK_EQUAL( values.count( "0.0000" ), 0 );
175}
176
177
178BOOST_AUTO_TEST_CASE( FillOpacityFullyOpaque )
179{
180 std::set<std::string> values = fillOpacities( plotFilledShape( COLOR4D( 0.0, 0.0, 1.0, 1.0 ) ) );
181
182 BOOST_REQUIRE_EQUAL( values.count( "1.0000" ), 1 );
183
184 // Nothing may be drawn semi-transparently when the brush is opaque
185 for( const std::string& value : values )
186 BOOST_CHECK_EQUAL( value, "1.0000" );
187}
188
189
190BOOST_AUTO_TEST_CASE( FillOpacityPreservedThroughSetColor )
191{
192 const std::vector<std::pair<double, std::string>> cases = {
193 { 0.0, "0.0000" }, { 0.25, "0.2500" }, { 0.5, "0.5000" }, { 0.75, "0.7500" }, { 1.0, "1.0000" }
194 };
195
196 for( const auto& [alpha, expected] : cases )
197 {
198 BOOST_TEST_CONTEXT( "alpha " << alpha )
199 {
200 std::set<std::string> values = fillOpacities( plotFilledShape( COLOR4D( 0.5, 0.5, 0.5, alpha ) ) );
201
202 BOOST_CHECK_EQUAL( values.count( expected ), 1 );
203
204 // No other requested alpha may appear, or SetColor is not reaching the output
205 for( const auto& [otherAlpha, other] : cases )
206 {
207 if( other != expected && other != "1.0000" )
208 BOOST_CHECK_EQUAL( values.count( other ), 0 );
209 }
210 }
211 }
212}
213
214
215BOOST_AUTO_TEST_CASE( StrokeOpacityMatchesColorAlpha )
216{
217 const wxString svg = plotFilledShape( COLOR4D( 1.0, 0.0, 0.0, 0.25 ) );
218 std::set<std::string> values = strokeOpacities( svg );
219
220 BOOST_CHECK_EQUAL( values.count( "0.2500" ), 1 );
221}
222
223
224static const auto kSvgValTol = utf::tolerance( 0.00001 );
225
226
227/*
228 * The default documents looks like this,
229 * with a 400-mil on the 1000x1000 mil page
230 *
231 * +---------+
232 * | +---+ |
233 * | | | |
234 * | +---+ |
235 * | |
236 * +---------+
237 */
238BOOST_AUTO_TEST_CASE( DefaultPlotViewBoxIsOrigin, *kSvgValTol )
239{
240 const bool isMirrored = false;
241 const bool isFitToBoard = false;
242
243 const wxString svg = plotFilledShape( COLOR4D( 1.0, 0.0, 0.0, 0.25 ), isMirrored, isFitToBoard );
244 const SVG_PLOT svgPlot( svg );
245 const KI_TEST::SVG_VIEWBOX& viewBox = svgPlot.ViewBox();
246
247 // The default plotter origin is at (0, 0) IU
248 // And the page is 1000 mils square, so the viewBox is 25.4 * 25.4 mm
249 BOOST_TEST( viewBox.m_X == 0.0 );
250 BOOST_TEST( viewBox.m_Y == 0.0 );
251 BOOST_TEST( viewBox.m_Width == 25.4 );
252 BOOST_TEST( viewBox.m_Height == 25.4 );
253
254 // The rectangle's left edge is 10 mils = 0.254 mm
255 BOOST_TEST( svgPlot.RectAttribute( wxT( "x" ) ) == wxT( "0.254000" ) );
256}
257
258
259BOOST_AUTO_TEST_CASE( MirroredSvgPreservesPlotOrigin, *kSvgValTol )
260{
261 const bool isMirrored = true;
262 const bool isFitToBoard = false;
263 const wxString svg = plotFilledShape( COLOR4D( 1.0, 0.0, 0.0, 0.25 ), isMirrored, isFitToBoard );
264 const SVG_PLOT svgPlot( svg );
265 const KI_TEST::SVG_VIEWBOX& viewBox = svgPlot.ViewBox();
266
267 // We didn't fit to the board, so the unmirrored page was from 0 to 25.4 mm (in x)
268 // And the mirrored page is from -25.4 to 0 mm (in x)
269 BOOST_TEST( viewBox.m_X == -25.4 );
270 BOOST_TEST( viewBox.m_Y == 0.0 );
271 BOOST_TEST( viewBox.m_Width == 25.4 );
272 BOOST_TEST( viewBox.m_Height == 25.4 );
273
274 // And the rectangle's left edge is -410 mils = -10.414 mm
275 BOOST_TEST( svgPlot.RectAttribute( wxT( "x" ) ) == wxT( "-10.414000" ) );
276}
277
278
279BOOST_AUTO_TEST_CASE( FitToBoardUsesViewBox, *kSvgValTol )
280{
281 const bool isMirrored = false;
282 const bool isFitToBoard = true;
283 const wxString svg = plotFilledShape( COLOR4D( 1.0, 0.0, 0.0, 0.25 ), isMirrored, isFitToBoard );
284 const SVG_PLOT svgPlot( svg );
285 const KI_TEST::SVG_VIEWBOX& viewBox = svgPlot.ViewBox();
286
287 // The board is 1000 mils square
288 // And the viewBox is offset by 2000 mils
289 BOOST_TEST( viewBox.m_X == 50.8 );
290 BOOST_TEST( viewBox.m_Y == 50.8 );
291 BOOST_TEST( viewBox.m_Width == 25.4 );
292 BOOST_TEST( viewBox.m_Height == 25.4 );
293
294 // The rectangle's left edge is +2000mils
295 BOOST_TEST( svgPlot.RectAttribute( wxT( "x" ) ) == wxT( "50.800000" ) );
296}
297
298
299BOOST_AUTO_TEST_CASE( MirroredFitToBoardUsesMirroredViewBox, *kSvgValTol )
300{
301 const bool isMirrored = true;
302 const bool isFitToBoard = true;
303 const wxString svg = plotFilledShape( COLOR4D( 1.0, 0.0, 0.0, 0.25 ), isMirrored, isFitToBoard );
304 const SVG_PLOT svgPlot( svg );
305
306 const KI_TEST::SVG_VIEWBOX& viewBox = svgPlot.ViewBox();
307
308 // For the mirrored case, the viewBox is flipped horizontally
309 BOOST_TEST( viewBox.m_X == -76.2 );
310 BOOST_TEST( viewBox.m_Y == 50.8 );
311 BOOST_TEST( viewBox.m_Width == 25.4 );
312 BOOST_TEST( viewBox.m_Height == 25.4 );
313
314 // The rectangle spans 20000 to 24000 IU, which mirrors to -20000 to -24000 IU.
315 BOOST_TEST( svgPlot.RectAttribute( wxT( "x" ) ) == wxT( "-60.960000" ) );
316}
317
318
BOX2< VECTOR2I > BOX2I
Definition box2.h:927
A color representation with 4 components: red, green, blue, alpha.
Definition color4d.h:101
Describe the page size and margins of a paper page on which to eventually print or plot.
Definition page_info.h:75
void SetHeightMils(double aHeightInMils)
void SetWidthMils(double aWidthInMils)
virtual bool OpenFile(const wxString &aFullFilename)
Open or create the plot file aFullFilename.
Definition plotter.cpp:75
virtual void SetPageSettings(const PAGE_INFO &aPageSettings)
Definition plotter.h:169
virtual void SetColorMode(bool aColorMode)
Plot in B/W or color.
Definition plotter.h:163
virtual void SetColor(const COLOR4D &color) override
The SetColor implementation is split with the subclasses: the PSLIKE computes the rgb values,...
virtual bool StartPlot(const wxString &aPageNumber) override
Create SVG file header.
virtual void SetPlotBBox(const BOX2I &aBBoxIU) override
Set an explicit bounding box for the plotted content (in IUs).
virtual void SetViewport(const VECTOR2I &aOffset, double aIusPerDecimil, double aScale, bool aMirror) override
Set the plot offset and scaling for the current plot.
virtual void Rect(const VECTOR2I &p1, const VECTOR2I &p2, FILL_T fill, int width, int aCornerRadius=0) override
virtual bool EndPlot() override
@ FILLED_SHAPE
Fill with object color.
Definition eda_fill.h:31
const wxXmlNode * FindFirstRect(const wxXmlNode &aNode)
Find the first <rect> element in the given XML node's descendants.
tl::expected< wxXmlDocument, wxString > LoadSvg(const wxString &aSvg)
tl::expected< SVG_VIEWBOX, wxString > ParseViewBox(const wxXmlNode &aRoot)
Parse the four numbers of the SVG viewBox attribute (min-x, min-y, width, height).
Plotting engines similar to ps (PostScript, Gerber, svg)
BOOST_AUTO_TEST_CASE(HorizontalAlignment)
BOOST_AUTO_TEST_SUITE(CadstarPartParser)
BOOST_REQUIRE(intersection.has_value()==c.ExpectedIntersection.has_value())
BOOST_AUTO_TEST_SUITE_END()
BOOST_TEST(netlist.find("R_G1 ARM_OUT1 DIE_B R='0.001 / ((SW_STATE)") !=std::string::npos)
VECTOR3I expected(15, 30, 45)
VECTOR2I end
BOOST_TEST_CONTEXT("Test Clearance")
static const auto kSvgValTol
BOOST_AUTO_TEST_CASE(FillOpacityMatchesColorAlpha)
BOOST_CHECK_EQUAL(result, "25.4")
VECTOR2< int32_t > VECTOR2I
Definition vector2d.h:683