KiCad PCB EDA Suite
Loading...
Searching...
No Matches
test_bitmap_tracing.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
22#include <bitmap_base.h>
23#include <bitmap_tracing.h>
24#include <eda_item.h>
25#include <geometry/eda_angle.h>
28#include <reporter.h>
29
30#include <wx/imagjpeg.h>
31#include <wx/mstream.h>
32
33BOOST_AUTO_TEST_SUITE( BitmapTracing )
34
35
36static wxImage makeImage( int aWidth, int aHeight, unsigned char aFill )
37{
38 wxImage image( aWidth, aHeight );
39 image.SetRGB( wxRect( 0, 0, aWidth, aHeight ), aFill, aFill, aFill );
40 return image;
41}
42
43
44static void fillRect( wxImage& aImage, const wxRect& aRect, unsigned char aValue )
45{
46 aImage.SetRGB( aRect, aValue, aValue, aValue );
47}
48
49
50BOOST_AUTO_TEST_CASE( ThresholdSplitsLightFromDark )
51{
52 wxImage image = makeImage( 16, 16, 200 );
53 fillRect( image, wxRect( 8, 0, 8, 16 ), 20 );
54
55 wxImage bilevel = ConvertImageToBilevel( image, 0.5, false );
56
57 BOOST_CHECK_EQUAL( (int) bilevel.GetGreen( 2, 8 ), 255 );
58 BOOST_CHECK_EQUAL( (int) bilevel.GetGreen( 12, 8 ), 0 );
59}
60
61
62BOOST_AUTO_TEST_CASE( InvertSwapsBlackAndWhite )
63{
64 wxImage image = makeImage( 16, 16, 200 );
65 fillRect( image, wxRect( 8, 0, 8, 16 ), 20 );
66
67 wxImage bilevel = ConvertImageToBilevel( image, 0.5, true );
68
69 BOOST_CHECK_EQUAL( (int) bilevel.GetGreen( 2, 8 ), 0 );
70 BOOST_CHECK_EQUAL( (int) bilevel.GetGreen( 12, 8 ), 255 );
71}
72
73
74BOOST_AUTO_TEST_CASE( TransparentPixelsDoNotBecomeBlack )
75{
76 wxImage image = makeImage( 16, 16, 0 );
77 image.InitAlpha();
78
79 for( int y = 0; y < 16; y++ )
80 {
81 for( int x = 0; x < 16; x++ )
82 image.SetAlpha( x, y, x < 8 ? wxIMAGE_ALPHA_TRANSPARENT : wxIMAGE_ALPHA_OPAQUE );
83 }
84
85 wxImage bilevel = ConvertImageToBilevel( image, 0.5, false );
86
87 BOOST_CHECK_MESSAGE( bilevel.GetGreen( 2, 8 ) == 255, "a transparent dark pixel must not be traced" );
88 BOOST_CHECK_MESSAGE( bilevel.GetGreen( 12, 8 ) == 0, "an opaque dark pixel must be traced" );
89}
90
91
92BOOST_AUTO_TEST_CASE( RingTracesToOneOutlineWithItsHoleRemoved )
93{
94 wxImage image = makeImage( 64, 64, 255 );
95 fillRect( image, wxRect( 12, 12, 40, 40 ), 0 );
96 fillRect( image, wxRect( 22, 22, 20, 20 ), 255 );
97
98 wxImage bilevel = ConvertImageToBilevel( image, 0.5, false );
99
101 SHAPE_POLY_SET traced = TraceBilevelImage( bilevel, 1.0, 1.0, reporter );
102
103 BOOST_REQUIRE( !traced.IsEmpty() );
104
105 // Fracturing turns the hole into a slit, so the ring is a single outline.
106 BOOST_CHECK_EQUAL( traced.OutlineCount(), 1 );
107
108 const double expectedArea = ( 40.0 * 40.0 ) - ( 20.0 * 20.0 );
109 BOOST_CHECK_CLOSE( std::abs( traced.Area() ), expectedArea, 5.0 );
110
111 BOX2I bbox = traced.BBox();
112 BOOST_CHECK_LE( std::abs( bbox.GetWidth() - 40 ), 2 );
113 BOOST_CHECK_LE( std::abs( bbox.GetHeight() - 40 ), 2 );
114}
115
116
118{
120 {
121 if( !wxImage::FindHandler( wxBITMAP_TYPE_PNG ) )
122 wxImage::AddHandler( new wxPNGHandler );
123 }
124
125 wxMemoryBuffer EncodeAsPng( const wxImage& aImage )
126 {
127 wxMemoryOutputStream stream;
128
129 BOOST_REQUIRE( aImage.SaveFile( stream, wxBITMAP_TYPE_PNG ) );
130
131 size_t length = stream.GetLength();
132 wxMemoryBuffer buffer( length );
133
134 stream.CopyTo( buffer.GetWriteBuf( length ), length );
135 buffer.SetDataLen( length );
136
137 return buffer;
138 }
139};
140
141
142BOOST_FIXTURE_TEST_CASE( PluginReportsSizeInMillimetres, PNG_FIXTURE )
143{
145
146 BOOST_REQUIRE( plugin.LoadFromMemory( EncodeAsPng( makeImage( 300, 150, 255 ) ) ) );
147
148 // An image with no resolution is read at 300 DPI, so 300 pixels is one inch.
149 BOOST_CHECK_CLOSE( plugin.GetImageWidth(), 25.4, 0.01 );
150 BOOST_CHECK_CLOSE( plugin.GetImageHeight(), 12.7, 0.01 );
151}
152
153
154BOOST_FIXTURE_TEST_CASE( PluginReportsFilledPolygons, PNG_FIXTURE )
155{
156 wxImage image = makeImage( 300, 300, 255 );
157 fillRect( image, wxRect( 60, 60, 60, 60 ), 0 );
158 fillRect( image, wxRect( 180, 180, 60, 60 ), 0 );
159
162
163 plugin.SetImporter( &importer );
164
165 BOOST_REQUIRE( plugin.LoadFromMemory( EncodeAsPng( image ) ) );
166 BOOST_REQUIRE( plugin.Import() );
167
168 BOOST_CHECK_EQUAL( importer.GetShapes().size(), 2 );
169
170 for( const std::unique_ptr<IMPORTED_SHAPE>& shape : importer.GetShapes() )
171 {
172 const IMPORTED_POLYGON* polygon = dynamic_cast<const IMPORTED_POLYGON*>( shape.get() );
173
174 BOOST_REQUIRE_MESSAGE( polygon, "a traced area must arrive as a polygon" );
175 BOOST_CHECK_MESSAGE( polygon->IsFilled(), "a traced area must arrive filled" );
176
177 // Width 0 would be read as "use the default line width" and outline the fill.
178 BOOST_CHECK_MESSAGE( polygon->GetStroke().GetWidth() == -1, "a traced area must arrive with no outline" );
179 }
180}
181
182
183BOOST_FIXTURE_TEST_CASE( PluginKeepsDetailFinerThanAMillimetre, PNG_FIXTURE )
184{
185 wxImage image = makeImage( 300, 300, 255 );
186
187 for( int bar = 0; bar < 3; bar++ )
188 fillRect( image, wxRect( 100 + bar * 6, 100, 4, 100 ), 0 );
189
192
193 plugin.SetImporter( &importer );
194
195 BOOST_REQUIRE( plugin.LoadFromMemory( EncodeAsPng( image ) ) );
196 BOOST_REQUIRE( plugin.Import() );
197
198 BOOST_CHECK_EQUAL( importer.GetShapes().size(), 3 );
199
200 for( const std::unique_ptr<IMPORTED_SHAPE>& shape : importer.GetShapes() )
201 {
202 BOX2D bbox = shape->GetBoundingBox();
203
204 BOOST_CHECK_CLOSE( bbox.GetWidth(), 4.0 * 25.4 / 300.0, 15.0 );
205 BOOST_CHECK_CLOSE( bbox.GetHeight(), 100.0 * 25.4 / 300.0, 5.0 );
206 }
207}
208
209
211static void loadAsJpeg( BITMAP_BASE& aBitmap, const wxImage& aImage )
212{
213 if( !wxImage::FindHandler( wxBITMAP_TYPE_JPEG ) )
214 wxImage::AddHandler( new wxJPEGHandler );
215
216 wxMemoryOutputStream jpeg;
217 BOOST_REQUIRE( aImage.SaveFile( jpeg, wxBITMAP_TYPE_JPEG ) );
218
219 wxMemoryInputStream stream( jpeg );
220 BOOST_REQUIRE( aBitmap.ReadImageFile( stream ) );
221}
222
223
225static std::string storedImageData( const BITMAP_BASE& aBitmap )
226{
227 wxMemoryOutputStream out;
228 BOOST_REQUIRE( aBitmap.SaveImageData( out ) );
229
230 std::string bytes( out.GetLength(), '\0' );
231 out.CopyTo( bytes.data(), bytes.size() );
232
233 return bytes;
234}
235
236
237BOOST_AUTO_TEST_CASE( APreviewPutsTheRealImageBack )
238{
239 wxImage source = makeImage( 40, 20, 255 );
240 fillRect( source, wxRect( 0, 0, 10, 20 ), 0 );
241
242 BITMAP_BASE bitmap;
243 loadAsJpeg( bitmap, source );
244
245 const KIID id = bitmap.GetImageID();
246 const std::string stored = storedImageData( bitmap );
247 const wxImage shown = *bitmap.GetImageData();
248
249 bitmap.BeginPreviewImage( ConvertImageToBilevel( shown, 0.5, true ) );
250
251 // Or a renderer caching by id would hand back the image the preview replaced.
252 BOOST_CHECK_MESSAGE( bitmap.GetImageID() != id, "a preview must not keep the image id" );
253
254 // A document can be written while a preview is up, by an autosave for one, and what it
255 // saves has to be the picture rather than the stand-in shown over it.
256 BOOST_CHECK_MESSAGE( storedImageData( bitmap ) == stored, "a preview must not change the stored data" );
257
258 bitmap.EndPreviewImage();
259
260 BOOST_CHECK_MESSAGE( bitmap.GetImageID() == id, "ending a preview must restore the image id" );
261 BOOST_CHECK_EQUAL( (int) bitmap.GetImageData()->GetGreen( 2, 10 ), (int) shown.GetGreen( 2, 10 ) );
262
263 // A preview the user cancelled must leave nothing to save, or the picture is encoded
264 // again and a JPEG loses quality every time.
265 BOOST_CHECK_MESSAGE( storedImageData( bitmap ) == stored, "ending a preview must restore the stored data" );
266}
267
268
269BOOST_AUTO_TEST_CASE( APreviewedTurnIsNotAppliedTwice )
270{
271 BITMAP_BASE bitmap;
272 BOOST_REQUIRE( bitmap.SetImage( makeImage( 40, 20, 255 ) ) );
273
274 bitmap.Rotate( true );
275
276 BOOST_REQUIRE_EQUAL( bitmap.GetSizePixels().x, 20 );
277 BOOST_REQUIRE_EQUAL( bitmap.GetOriginalImageData()->GetWidth(), 40 );
278
279 const wxImage rotated = *bitmap.GetImageData();
280
281 bitmap.BeginPreviewImage( rotated );
282
283 BOOST_CHECK_MESSAGE( bitmap.Rotation() == ANGLE_0,
284 "a preview already holding the rotation must not be turned again" );
285
286 bitmap.EndPreviewImage();
287
288 BOOST_CHECK_MESSAGE( bitmap.Rotation() != ANGLE_0, "ending a preview must restore the rotation" );
289 BOOST_CHECK_EQUAL( bitmap.GetSizePixels().x, 20 );
290 BOOST_CHECK_EQUAL( bitmap.GetOriginalImageData()->GetWidth(), 40 );
291}
292
293
294BOOST_AUTO_TEST_CASE( AFailedRegionDoesNotSwallowTheRest )
295{
296 wxImage image = makeImage( 200, 200, 255 );
297
298 // A hairline first, which cannot be normalised at this coarse scale, then a solid block.
299 fillRect( image, wxRect( 20, 20, 1, 160 ), 0 );
300 fillRect( image, wxRect( 120, 20, 60, 60 ), 0 );
301
302 wxImage bilevel = ConvertImageToBilevel( image, 0.5, false );
303
305 SHAPE_POLY_SET traced = TraceBilevelImage( bilevel, 1.0, 1.0, reporter );
306
307 bool foundBlock = false;
308
309 for( int ii = 0; ii < traced.OutlineCount(); ii++ )
310 {
311 BOX2I bbox = traced.COutline( ii ).BBox();
312
313 if( bbox.GetWidth() > 50 && bbox.GetHeight() > 50 )
314 foundBlock = true;
315 }
316
317 BOOST_CHECK_MESSAGE( foundBlock, "a region traced after a failing one must survive" );
318
319 // Whatever the tracer makes of the hairline, a region it drops has to be reported.
320 BOOST_CHECK_MESSAGE( traced.OutlineCount() == 2 || reporter.HasMessage(),
321 "a region that could not be traced must be reported" );
322}
323
324
wxImage ConvertImageToBilevel(const wxImage &aImage, double aThreshold, bool aInvert)
Reduce an image to pure black and white.
SHAPE_POLY_SET TraceBilevelImage(const wxImage &aBilevelImage, double aScaleX, double aScaleY, REPORTER &aReporter)
Trace the black regions of a black and white image into filled polygons.
BOX2< VECTOR2I > BOX2I
Definition box2.h:914
BOX2< VECTOR2D > BOX2D
Definition box2.h:915
This class handle bitmap images in KiCad.
Definition bitmap_base.h:47
void Rotate(bool aRotateCCW)
Rotate image CW or CCW.
void BeginPreviewImage(const wxImage &aImage)
Show finished pixels in place of the image, without disturbing the scale or the resolution.
const wxImage * GetOriginalImageData() const
Definition bitmap_base.h:69
VECTOR2I GetSizePixels() const
EDA_ANGLE Rotation() const
void EndPreviewImage()
Put back the image a preview stood in for, and do nothing if none is showing.
bool SaveImageData(wxOutputStream &aOutStream) const
Write the bitmap data to aOutStream.
bool SetImage(const wxImage &aImage)
Set the image from an existing wxImage.
bool ReadImageFile(const wxString &aFullFilename)
Reads and stores in memory an image file.
KIID GetImageID() const
Definition bitmap_base.h:74
wxImage * GetImageData()
Definition bitmap_base.h:66
Import a raster image by tracing its dark areas into filled polygons.
double GetImageWidth() const override
Return image width from original imported file.
bool LoadFromMemory(const wxMemoryBuffer &aMemBuffer) override
Set memory buffer with content for import.
bool Import() override
Actually imports the file.
double GetImageHeight() const override
Return image height from original imported file.
constexpr size_type GetWidth() const
Definition box2.h:211
constexpr size_type GetHeight() const
Definition box2.h:212
std::list< std::unique_ptr< IMPORTED_SHAPE > > & GetShapes()
virtual void SetImporter(GRAPHICS_IMPORTER *aImporter)
Set the receiver of the imported shapes.
const IMPORTED_STROKE & GetStroke() const
double GetWidth() const
Definition kiid.h:46
A singleton reporter that reports to nowhere.
Definition reporter.h:267
const BOX2I BBox(int aClearance=0) const override
Compute a bounding box of the shape, with a margin of aClearance a collision.
Represent a set of closed polygons.
double Area()
Return the area of this poly set.
bool IsEmpty() const
Return true if the set is empty (no polygons at all)
int OutlineCount() const
Return the number of outlines in the set.
const SHAPE_LINE_CHAIN & COutline(int aIndex) const
const BOX2I BBox(int aClearance=0) const override
Compute a bounding box of the shape, with a margin of aClearance a collision.
A wrapper for reporting to a wxString object.
Definition reporter.h:242
static constexpr EDA_ANGLE ANGLE_0
Definition eda_angle.h:448
std::string source
EDA_ANGLE abs(const EDA_ANGLE &aAngle)
Definition eda_angle.h:437
wxMemoryBuffer EncodeAsPng(const wxImage &aImage)
static std::string storedImageData(const BITMAP_BASE &aBitmap)
The bytes the image would be written to a board file with.
static void fillRect(wxImage &aImage, const wxRect &aRect, unsigned char aValue)
static void loadAsJpeg(BITMAP_BASE &aBitmap, const wxImage &aImage)
Load an image the way a board does, through encoded file data.
static wxImage makeImage(int aWidth, int aHeight, unsigned char aFill)
BOOST_FIXTURE_TEST_CASE(PluginReportsSizeInMillimetres, PNG_FIXTURE)
BOOST_AUTO_TEST_CASE(ThresholdSplitsLightFromDark)
BOOST_AUTO_TEST_SUITE(CadstarPartParser)
BOOST_REQUIRE(intersection.has_value()==c.ExpectedIntersection.has_value())
BOOST_AUTO_TEST_SUITE_END()
IbisParser parser & reporter
BOOST_CHECK_EQUAL(result, "25.4")