KiCad PCB EDA Suite
Loading...
Searching...
No Matches
test_renderer_output.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 3
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, you may find one here:
18 * http://www.gnu.org/licenses/gpl-3.0.html
19 * or you may search the http://www.gnu.org website for the version 3 license,
20 * or you may write to the Free Software Foundation, Inc.,
21 * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA
22 */
23
24#include <boost/test/unit_test.hpp>
25
28
29#include <base_units.h>
30
31#include <wx/file.h>
32#include <wx/filename.h>
33#include <wx/stdpaths.h>
34#include <wx/utils.h>
35
36#include <regex>
37#include <string>
38
39
40using namespace KICAD_DIFF;
41
42
43namespace
44{
45
46// Tiny RAII helper: makes a unique tmp path with the requested extension,
47// deletes the file on destruction so tests don't leak artifacts.
48struct ScopedTmpFile
49{
50 explicit ScopedTmpFile( const wxString& aExt )
51 {
52 wxString tmp = wxStandardPaths::Get().GetTempDir();
53 static int counter = 0;
54 wxString name = wxString::Format( wxS( "kicad_render_%d_%d.%s" ), static_cast<int>( wxGetProcessId() ),
55 ++counter, aExt );
56 m_path = tmp + wxFILE_SEP_PATH + name;
57 }
58
59 ~ScopedTmpFile()
60 {
61 if( !m_path.IsEmpty() && wxFileExists( m_path ) )
62 wxRemoveFile( m_path );
63 }
64
65 const wxString& Path() const { return m_path; }
66
67 wxString m_path;
68};
69
70
71// Read the first @p aBytes bytes of a file into a std::string so test can
72// pin header bytes for format-sniffing.
73std::string readHeaderBytes( const wxString& aPath, size_t aBytes )
74{
75 wxFile f( aPath );
76
77 if( !f.IsOpened() )
78 return {};
79
80 std::string buf( aBytes, '\0' );
81 ssize_t got = f.Read( buf.data(), aBytes );
82
83 if( got <= 0 )
84 return {};
85
86 buf.resize( static_cast<size_t>( got ) );
87 return buf;
88}
89
90
91// A DIFF_SCENE with no shapes and a zero-extent documentBBox -- both
92// renderers should emit a tiny empty placeholder PNG/SVG rather than
93// failing.
94DIFF_SCENE makeEmptyScene()
95{
96 DIFF_SCENE scene;
97 return scene;
98}
99
100
101// A DIFF_SCENE with one shape per category so the renderer walks all four
102// PAINT_ORDER entries.
103DIFF_SCENE makePopulatedScene()
104{
105 DIFF_SCENE scene;
106 scene.documentBBox = BOX2I( VECTOR2I( 0, 0 ), VECTOR2I( 1000000, 1000000 ) );
107
108 auto add = [&]( std::vector<SCENE_SHAPE>& aBucket, const BOX2I& bb,
109 const KIGFX::COLOR4D& col )
110 {
111 SCENE_SHAPE s;
112 s.bbox = bb;
113 s.color = col;
114 aBucket.push_back( s );
115 };
116
117 add( scene.modifiedShapes, BOX2I( VECTOR2I( 100000, 100000 ),
118 VECTOR2I( 100000, 100000 ) ),
119 KIGFX::COLOR4D( 1.0, 1.0, 0.0, 1.0 ) );
120 add( scene.addedShapes, BOX2I( VECTOR2I( 200000, 100000 ),
121 VECTOR2I( 100000, 100000 ) ),
122 KIGFX::COLOR4D( 0.0, 1.0, 0.0, 1.0 ) );
123 add( scene.removedShapes, BOX2I( VECTOR2I( 100000, 200000 ),
124 VECTOR2I( 100000, 100000 ) ),
125 KIGFX::COLOR4D( 1.0, 0.0, 0.0, 1.0 ) );
126 add( scene.conflictShapes, BOX2I( VECTOR2I( 200000, 200000 ),
127 VECTOR2I( 100000, 100000 ) ),
128 KIGFX::COLOR4D( 1.0, 0.0, 1.0, 1.0 ) );
129
130 return scene;
131}
132
133// A DIFF_SCENE carrying one square added-shape of a known internal-unit
134// extent, tagged with the supplied document kind. Used to observe how the
135// plotter renderer maps internal units to device (mm) coordinates per kind.
136DIFF_SCENE makeSingleSquareScene( KICAD_DIFF::DOC_KIND aKind, int aSideIU )
137{
138 DIFF_SCENE scene;
139 scene.docKind = aKind;
140 scene.documentBBox = BOX2I( VECTOR2I( 0, 0 ), VECTOR2I( aSideIU, aSideIU ) );
141
142 SCENE_SHAPE s;
143 s.bbox = BOX2I( VECTOR2I( 0, 0 ), VECTOR2I( aSideIU, aSideIU ) );
144 s.color = KIGFX::COLOR4D( 0.0, 1.0, 0.0, 1.0 );
145 scene.addedShapes.push_back( s );
146
147 return scene;
148}
149
150
151// Read an entire text file into a std::string.
152std::string readWholeFile( const wxString& aPath )
153{
154 wxFile f( aPath );
155
156 if( !f.IsOpened() )
157 return {};
158
159 wxString content;
160
161 if( !f.ReadAll( &content ) )
162 return {};
163
164 return std::string( content.utf8_str() );
165}
166
167
168// Largest width attribute (in mm) among the <rect> elements in an SVG body.
169// The diff renderer emits each scene shape as a <rect>; the biggest one is the
170// document bbox shape, whose mm width is iuWidth * iuScale-derived factor.
171double largestSvgRectWidthMM( const std::string& aSvg )
172{
173 std::regex rectWidth( "<rect[^>]*width=\"([0-9.]+)\"" );
174 double best = 0.0;
175 auto begin = std::sregex_iterator( aSvg.begin(), aSvg.end(), rectWidth );
176 auto end = std::sregex_iterator();
177
178 for( auto it = begin; it != end; ++it )
179 best = std::max( best, std::stod( ( *it )[1].str() ) );
180
181 return best;
182}
183
184} // namespace
185
186
187BOOST_AUTO_TEST_SUITE( RendererOutput )
188
189
190// A schematic-kind scene must be sized with the schematic internal-unit scale,
191// not the PCB one. The two scales differ by SCH_IU_PER_MM vs PCB_IU_PER_MM
192// (1e4 vs 1e6), so a fixed internal-unit square renders 100x larger in device
193// (mm) coordinates under the correct schematic scale. This pins the bug where
194// computeViewport hard-coded the PCB scale for every document type.
195BOOST_AUTO_TEST_CASE( SvgSchematicSceneUsesSchematicScale )
196{
197 constexpr int oneMillimetreSchIU = 10000; // SCH_IU_PER_MM
198
199 ScopedTmpFile schOut( wxS( "svg" ) );
201
202 DIFF_SCENE schScene = makeSingleSquareScene( KICAD_DIFF::DOC_KIND::SCH, oneMillimetreSchIU );
203 BOOST_REQUIRE( RenderSceneToSvg( schScene, schOut.Path(), opts ) );
204
205 double schWidthMM = largestSvgRectWidthMM( readWholeFile( schOut.Path() ) );
206
207 // 10000 schematic IU is exactly 1 mm. Allow a small tolerance for the 5%
208 // viewport margin and SVG coordinate rounding.
209 BOOST_CHECK_GT( schWidthMM, 0.5 );
210
211 // The same internal-unit square interpreted with the PCB scale would render
212 // ~100x smaller (10000 PCB IU = 0.01 mm). Confirm the schematic render is
213 // nowhere near that erroneous size.
214 ScopedTmpFile pcbOut( wxS( "svg" ) );
215 DIFF_SCENE pcbScene = makeSingleSquareScene( KICAD_DIFF::DOC_KIND::PCB, oneMillimetreSchIU );
216 BOOST_REQUIRE( RenderSceneToSvg( pcbScene, pcbOut.Path(), opts ) );
217
218 double pcbWidthMM = largestSvgRectWidthMM( readWholeFile( pcbOut.Path() ) );
219
220 BOOST_CHECK_GT( pcbWidthMM, 0.0 );
221
222 // A fixed internal-unit box renders mm-wide in inverse proportion to the
223 // scale's IU_PER_MM, so the schematic render must be (PCB_IU_PER_MM /
224 // SCH_IU_PER_MM) = 100x the PCB render of the same box.
225 const double ratio = schWidthMM / pcbWidthMM;
226 BOOST_CHECK_CLOSE( ratio, pcbIUScale.IU_PER_MM / schIUScale.IU_PER_MM, 5.0 );
227}
228
229
230// Empty scene must still produce a valid PNG file -- the renderer falls back
231// to a minimal placeholder so the CLI exit code (success) matches "no changes".
232BOOST_AUTO_TEST_CASE( PngEmptyScenePlaceholder )
233{
234 ScopedTmpFile out( wxS( "png" ) );
236 opts.dpi = 96;
237
238 DIFF_SCENE scene = makeEmptyScene();
239 BOOST_REQUIRE( RenderSceneToPng( scene, out.Path(), opts ) );
240 BOOST_REQUIRE( wxFileExists( out.Path() ) );
241
242 std::string header = readHeaderBytes( out.Path(), 8 );
243 BOOST_REQUIRE_GE( header.size(), 8u );
244
245 // PNG magic: 89 50 4E 47 0D 0A 1A 0A
246 BOOST_CHECK_EQUAL( static_cast<unsigned char>( header[0] ), 0x89u );
247 BOOST_CHECK_EQUAL( static_cast<unsigned char>( header[1] ), 0x50u );
248 BOOST_CHECK_EQUAL( static_cast<unsigned char>( header[2] ), 0x4Eu );
249 BOOST_CHECK_EQUAL( static_cast<unsigned char>( header[3] ), 0x47u );
250}
251
252
253// Populated scene produces a PNG larger than the empty-scene placeholder.
254BOOST_AUTO_TEST_CASE( PngPopulatedSceneIsLargerThanPlaceholder )
255{
256 ScopedTmpFile out( wxS( "png" ) );
258 opts.pixelWidth = 64;
259 opts.pixelHeight = 64;
260 opts.dpi = 96;
261
262 DIFF_SCENE scene = makePopulatedScene();
263 BOOST_REQUIRE( RenderSceneToPng( scene, out.Path(), opts ) );
264 BOOST_REQUIRE( wxFileExists( out.Path() ) );
265
266 wxFileName fn( out.Path() );
267 wxULongLong size = fn.GetSize();
268
269 // Empty-scene placeholder is 67 bytes (see writeEmptyPng in
270 // diff_renderer_plotter.cpp). A populated render must be bigger.
271 BOOST_CHECK_GT( size.GetValue(), 67u );
272
273 // Verify PNG header bytes too.
274 std::string header = readHeaderBytes( out.Path(), 8 );
275 BOOST_CHECK_EQUAL( static_cast<unsigned char>( header[0] ), 0x89u );
276 BOOST_CHECK_EQUAL( static_cast<unsigned char>( header[1] ), 0x50u );
277}
278
279
280BOOST_AUTO_TEST_CASE( SvgEmptyScenePlaceholder )
281{
282 ScopedTmpFile out( wxS( "svg" ) );
284
285 DIFF_SCENE scene = makeEmptyScene();
286 BOOST_REQUIRE( RenderSceneToSvg( scene, out.Path(), opts ) );
287 BOOST_REQUIRE( wxFileExists( out.Path() ) );
288
289 std::string content = readHeaderBytes( out.Path(), 256 );
290 BOOST_REQUIRE( !content.empty() );
291
292 // The empty SVG placeholder starts with an XML declaration.
293 BOOST_CHECK( content.find( "<?xml" ) == 0 );
294 BOOST_CHECK( content.find( "<svg" ) != std::string::npos );
295}
296
297
298BOOST_AUTO_TEST_CASE( SvgPopulatedSceneEmitsSvgRoot )
299{
300 ScopedTmpFile out( wxS( "svg" ) );
302 opts.pixelWidth = 64;
303 opts.pixelHeight = 64;
304
305 DIFF_SCENE scene = makePopulatedScene();
306 BOOST_REQUIRE( RenderSceneToSvg( scene, out.Path(), opts ) );
307 BOOST_REQUIRE( wxFileExists( out.Path() ) );
308
309 // Verify the output is structurally an SVG document by looking for the
310 // <svg ...> root element. Read up to 4 KiB so headers + opening root
311 // tag fit comfortably.
312 std::string content = readHeaderBytes( out.Path(), 4096 );
313 BOOST_REQUIRE( !content.empty() );
314 BOOST_CHECK( content.find( "<svg" ) != std::string::npos );
315
316 // Populated SVG must be larger than the empty placeholder (which is
317 // ~80 bytes).
318 wxFileName fn( out.Path() );
319 BOOST_CHECK_GT( fn.GetSize().GetValue(), 80u );
320}
321
322
const char * name
constexpr EDA_IU_SCALE schIUScale
Definition base_units.h:123
constexpr EDA_IU_SCALE pcbIUScale
Definition base_units.h:121
BOX2< VECTOR2I > BOX2I
Definition box2.h:918
A color representation with 4 components: red, green, blue, alpha.
Definition color4d.h:101
bool RenderSceneToSvg(const DIFF_SCENE &aScene, const wxString &aOutputPath, const PLOTTER_RENDER_OPTIONS &aOptions)
Render a DIFF_SCENE to an SVG file.
bool RenderSceneToPng(const DIFF_SCENE &aScene, const wxString &aOutputPath, const PLOTTER_RENDER_OPTIONS &aOptions)
Render a DIFF_SCENE to a PNG file.
DOC_KIND
Document type a diff/merge entry point should route to, derived from a file path's extension.
std::vector< SCENE_SHAPE > modifiedShapes
Definition diff_scene.h:232
std::vector< SCENE_SHAPE > conflictShapes
Definition diff_scene.h:233
std::vector< SCENE_SHAPE > addedShapes
Definition diff_scene.h:230
DOC_KIND docKind
Source document type.
Definition diff_scene.h:227
std::vector< SCENE_SHAPE > removedShapes
Definition diff_scene.h:231
Options controlling the headless PNG/SVG renderer.
Shared rendering model consumed by both the GAL renderer (interactive widget) and the plotter rendere...
Definition diff_scene.h:90
KIGFX::COLOR4D color
Definition diff_scene.h:92
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::vector< std::string > header
BOOST_AUTO_TEST_CASE(SvgSchematicSceneUsesSchematicScale)
VECTOR2I end
BOOST_CHECK_EQUAL(result, "25.4")
VECTOR2< int32_t > VECTOR2I
Definition vector2d.h:683