KiCad PCB EDA Suite
Loading...
Searching...
No Matches
gerber_to_png.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 (C) 2025 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 along
17 * with this program. If not, see <http://www.gnu.org/licenses/>.
18 */
19
20#include "gerber_to_png.h"
21#include "gerber_file_image.h"
22#include "gerber_draw_item.h"
23#include "dcode.h"
24#include "excellon_image.h"
25#include "excellon_defaults.h"
30#include <base_units.h>
31#include <trigo.h>
32#include <cmath>
33#include <wx/filename.h>
34
35
36bool IsExcellonFile( const wxString& aPath )
37{
38 wxFileName fn( aPath );
39 wxString ext = fn.GetExt().Lower();
40 return ext == wxS( "drl" ) || ext == wxS( "xln" ) || ext == wxS( "exc" ) || ext == wxS( "ncd" );
41}
42
43
45{
46 BOX2I bbox;
47 bool first = true;
48
49 for( GERBER_DRAW_ITEM* item : aImage->GetItems() )
50 {
51 BOX2I itemBox = item->GetBoundingBox();
52
53 if( first )
54 {
55 bbox = itemBox;
56 first = false;
57 }
58 else
59 {
60 bbox.Merge( itemBox );
61 }
62 }
63
64 return bbox;
65}
66
67
68std::unique_ptr<GERBER_FILE_IMAGE> LoadGerberOrExcellon( const wxString& aPath, wxString* aErrorMsg,
69 wxArrayString* aMessages )
70{
71 std::unique_ptr<GERBER_FILE_IMAGE> image;
72
73 if( IsExcellonFile( aPath ) )
74 {
75 auto excellon = std::make_unique<EXCELLON_IMAGE>( 0 );
76 EXCELLON_DEFAULTS defaults;
77
78 if( !excellon->LoadFile( aPath, &defaults ) )
79 {
80 if( aErrorMsg )
81 *aErrorMsg = wxString::Format( wxS( "Failed to load Excellon file: %s" ), aPath );
82
83 return nullptr;
84 }
85
86 image = std::move( excellon );
87 }
88 else
89 {
90 image = std::make_unique<GERBER_FILE_IMAGE>( 0 );
91
92 if( !image->LoadGerberFile( aPath ) )
93 {
94 if( aErrorMsg )
95 *aErrorMsg = wxString::Format( wxS( "Failed to load Gerber file: %s" ), aPath );
96
97 return nullptr;
98 }
99 }
100
101 if( aMessages )
102 *aMessages = image->GetMessages();
103
104 return image;
105}
106
107
108namespace
109{
110
114void RenderItem( GERBER_DRAW_ITEM* aItem, PNG_PLOTTER& aPlotter, const KIGFX::COLOR4D& aColor )
115{
116 SHAPE_POLY_SET itemPoly;
117 bool needsFlashOffset = false;
118
119 if( aItem->m_ShapeAsPolygon.OutlineCount() > 0 )
120 {
121 itemPoly = aItem->m_ShapeAsPolygon;
122 }
123 else if( aItem->m_ShapeType == GBR_SEGMENT )
124 {
125 D_CODE* dcode = aItem->GetDcodeDescr();
126
127 if( dcode && dcode->m_ApertType != APT_RECT )
128 {
129 int arcError = static_cast<int>( gerbIUScale.IU_PER_MM * ARC_LOW_DEF_MM );
130 TransformOvalToPolygon( itemPoly, aItem->m_Start, aItem->m_End,
131 aItem->m_Size.x, arcError, ERROR_INSIDE );
132 }
133 else
134 {
135 aItem->ConvertSegmentToPolygon( &itemPoly );
136 }
137 }
138 else if( aItem->m_ShapeType == GBR_ARC )
139 {
140 const int arcError = gerbIUScale.mmToIU( 0.005 );
141
142 if( aItem->m_Start == aItem->m_End )
143 {
144 int radius = KiROUND( aItem->m_Start.Distance( aItem->m_ArcCentre ) );
145 TransformRingToPolygon( itemPoly, aItem->m_ArcCentre, radius, aItem->m_Size.x,
146 arcError, ERROR_INSIDE );
147 }
148 else
149 {
150 double startAngle = atan2( static_cast<double>( aItem->m_Start.y - aItem->m_ArcCentre.y ),
151 static_cast<double>( aItem->m_Start.x - aItem->m_ArcCentre.x ) );
152 double endAngle = atan2( static_cast<double>( aItem->m_End.y - aItem->m_ArcCentre.y ),
153 static_cast<double>( aItem->m_End.x - aItem->m_ArcCentre.x ) );
154
155 if( startAngle > endAngle )
156 endAngle += 2.0 * M_PI;
157
158 VECTOR2I mid = GetRotated( aItem->m_Start, aItem->m_ArcCentre,
159 -EDA_ANGLE( ( endAngle - startAngle ) / 2.0, RADIANS_T ) );
160
161 TransformArcToPolygon( itemPoly, aItem->m_Start, mid, aItem->m_End, aItem->m_Size.x,
162 arcError, ERROR_INSIDE );
163 }
164 }
165 else if( aItem->m_Flashed )
166 {
167 D_CODE* dcode = aItem->GetDcodeDescr();
168
169 if( dcode )
170 {
171 dcode->ConvertShapeToPolygon( aItem );
172 itemPoly = dcode->m_Polygon;
173 needsFlashOffset = true;
174 }
175 }
176
177 if( itemPoly.OutlineCount() == 0 )
178 return;
179
180 // Flashed shapes from ConvertShapeToPolygon are centered at (0,0).
181 // Offset by the item's position before applying the AB transform.
182 VECTOR2I offset = needsFlashOffset ? VECTOR2I( aItem->m_Start ) : VECTOR2I( 0, 0 );
183
184 aPlotter.SetColor( aColor );
185
186 for( int i = 0; i < itemPoly.OutlineCount(); i++ )
187 {
188 const SHAPE_LINE_CHAIN& outline = itemPoly.COutline( i );
189 std::vector<VECTOR2I> pts;
190 pts.reserve( outline.PointCount() );
191
192 for( int j = 0; j < outline.PointCount(); j++ )
193 pts.push_back( aItem->GetABPosition( outline.CPoint( j ) + offset ) );
194
195 if( pts.size() >= 3 )
196 aPlotter.PlotPoly( pts, FILL_T::FILLED_SHAPE, 0 );
197 }
198}
199
200} // anonymous namespace
201
202
203GERBER_PLOTTER_VIEWPORT CalculatePlotterViewport( const BOX2I& aBBox, int aDpi, int aWidth, int aHeight )
204{
206 vp.width = aWidth;
207 vp.height = aHeight;
208
209 if( vp.width == 0 && vp.height == 0 )
210 {
211 double iuPerInch = gerbIUScale.IU_PER_MM * 25.4;
212 double widthInches = static_cast<double>( aBBox.GetWidth() ) / iuPerInch;
213 double heightInches = static_cast<double>( aBBox.GetHeight() ) / iuPerInch;
214
215 vp.width = static_cast<int>( std::ceil( widthInches * aDpi ) );
216 vp.height = static_cast<int>( std::ceil( heightInches * aDpi ) );
217
218 if( vp.width < MIN_PIXEL_SIZE )
220
221 if( vp.height < MIN_PIXEL_SIZE )
223 }
224 else if( vp.width == 0 )
225 {
226 double aspect = static_cast<double>( aBBox.GetWidth() ) / aBBox.GetHeight();
227 vp.width = static_cast<int>( vp.height * aspect );
228 }
229 else if( vp.height == 0 )
230 {
231 double aspect = static_cast<double>( aBBox.GetHeight() ) / aBBox.GetWidth();
232 vp.height = static_cast<int>( vp.width * aspect );
233 }
234
235 vp.iuPerDecimil = gerbIUScale.IU_PER_MILS / 10.0;
236
237 double scaleX = static_cast<double>( vp.width ) * vp.iuPerDecimil * 10000.0 / ( aBBox.GetWidth() * aDpi );
238 double scaleY = static_cast<double>( vp.height ) * vp.iuPerDecimil * 10000.0 / ( aBBox.GetHeight() * aDpi );
239 vp.plotScale = std::min( scaleX, scaleY );
240 vp.offset = aBBox.GetOrigin();
241
242 return vp;
243}
244
245
246bool RenderGerberToPng( const wxString& aInputPath, const wxString& aOutputPath, const GERBER_RENDER_OPTIONS& aOptions,
247 wxString* aErrorMsg, wxArrayString* aMessages )
248{
249 auto image = LoadGerberOrExcellon( aInputPath, aErrorMsg, aMessages );
250
251 if( !image )
252 return false;
253
254 if( image->GetItemsCount() == 0 )
255 {
256 if( aErrorMsg )
257 *aErrorMsg = wxS( "Gerber file contains no draw items" );
258
259 return false;
260 }
261
262 BOX2I bbox;
263
264 if( aOptions.HasViewportOverride() )
265 {
266 // Viewport is specified in Gerber-native coordinates (Y increases upward).
267 // KiCad stores gerber items with Y negated (Y increases downward), so
268 // negate the origin Y and flip the window vertically.
269 double iuPerMm = gerbIUScale.IU_PER_MM;
270 int ox = static_cast<int>( std::round( aOptions.originXMm * iuPerMm ) );
271 int oy = static_cast<int>( std::round(
272 -( aOptions.originYMm + aOptions.windowHeightMm ) * iuPerMm ) );
273 int w = static_cast<int>( std::round( aOptions.windowWidthMm * iuPerMm ) );
274 int h = static_cast<int>( std::round( aOptions.windowHeightMm * iuPerMm ) );
275
276 bbox = BOX2I( VECTOR2I( ox, oy ), VECTOR2I( w, h ) );
277 }
278 else
279 {
280 bbox = CalculateGerberBoundingBox( image.get() );
281 }
282
283 if( bbox.GetWidth() == 0 || bbox.GetHeight() == 0 )
284 {
285 if( aErrorMsg )
286 *aErrorMsg = wxS( "Gerber file has zero-size bounding box" );
287
288 return false;
289 }
290
291 // When using a viewport override with DPI, calculate pixel dimensions from the window
292 int reqWidth = aOptions.width;
293 int reqHeight = aOptions.height;
294
295 if( aOptions.HasViewportOverride() && reqWidth == 0 && reqHeight == 0 )
296 {
297 double mmPerInch = 25.4;
298 reqWidth = static_cast<int>( std::ceil( aOptions.windowWidthMm / mmPerInch * aOptions.dpi ) );
299 reqHeight = static_cast<int>( std::ceil( aOptions.windowHeightMm / mmPerInch * aOptions.dpi ) );
300 }
301
302 GERBER_PLOTTER_VIEWPORT vp = CalculatePlotterViewport( bbox, aOptions.dpi, reqWidth, reqHeight );
303
304 PNG_PLOTTER plotter;
305 plotter.SetColorMode( true );
306 plotter.SetPixelSize( vp.width, vp.height );
307 plotter.SetResolution( aOptions.dpi );
308 plotter.SetAntialias( aOptions.antialias );
309 plotter.SetBackgroundColor( aOptions.backgroundColor );
310 plotter.SetViewport( vp.offset, vp.iuPerDecimil, vp.plotScale, false );
311 plotter.OpenFile( aOutputPath );
312
313 if( !plotter.StartPlot( wxEmptyString ) )
314 {
315 if( aErrorMsg )
316 *aErrorMsg = wxS( "Failed to start PNG plotter" );
317
318 return false;
319 }
320
321 // Render all items with proper polarity handling.
322 // Negative (clear) polarity items erase copper by drawing with the background color.
323 // On transparent exports the background is alpha=0, so source-over is a no-op.
324 // Switch to CAIRO_OPERATOR_CLEAR so those regions become fully transparent.
325 bool transparentBg = ( aOptions.backgroundColor.a == 0 );
326
327 for( GERBER_DRAW_ITEM* item : image->GetItems() )
328 {
329 if( item->GetLayerPolarity() )
330 {
331 plotter.SetClearCompositing( transparentBg );
332 RenderItem( item, plotter, aOptions.backgroundColor );
333 plotter.SetClearCompositing( false );
334 }
335 else
336 {
337 RenderItem( item, plotter, aOptions.foregroundColor );
338 }
339 }
340
341 if( !plotter.EndPlot() )
342 {
343 if( aErrorMsg )
344 *aErrorMsg = wxString::Format( wxS( "Failed to save PNG file: %s" ), aOutputPath );
345
346 return false;
347 }
348
349 return true;
350}
351
352
353bool RenderGerberToPng( const wxString& aInputPath, const wxString& aOutputPath, const JOB_GERBER_EXPORT_PNG& aJob,
354 wxString* aErrorMsg, wxArrayString* aMessages )
355{
356 GERBER_RENDER_OPTIONS options;
357 options.dpi = aJob.m_dpi;
358 options.width = aJob.m_width;
359 options.height = aJob.m_height;
360 options.antialias = aJob.m_antialias;
361 options.backgroundColor =
362 aJob.m_transparentBackground ? KIGFX::COLOR4D( 1.0, 1.0, 1.0, 0.0 ) : KIGFX::COLOR4D::WHITE;
363
364 if( !aJob.m_foregroundColor.IsEmpty() )
366
367 if( !aJob.m_backgroundColor.IsEmpty() )
369
370 double toMm = 1.0;
371
372 switch( aJob.m_units )
373 {
374 case JOB_GERBER_EXPORT_PNG::UNITS::INCH: toMm = 25.4; break;
375 case JOB_GERBER_EXPORT_PNG::UNITS::MILS: toMm = 0.0254; break;
376 case JOB_GERBER_EXPORT_PNG::UNITS::MM: toMm = 1.0; break;
377 }
378
379 options.originXMm = aJob.m_originX * toMm;
380 options.originYMm = aJob.m_originY * toMm;
381 options.windowWidthMm = aJob.m_windowWidth * toMm;
382 options.windowHeightMm = aJob.m_windowHeight * toMm;
383
384 return RenderGerberToPng( aInputPath, aOutputPath, options, aErrorMsg, aMessages );
385}
@ ERROR_INSIDE
constexpr EDA_IU_SCALE gerbIUScale
Definition base_units.h:124
constexpr double ARC_LOW_DEF_MM
Definition base_units.h:131
BOX2< VECTOR2I > BOX2I
Definition box2.h:922
constexpr BOX2I KiROUND(const BOX2D &aBoxD)
Definition box2.h:990
constexpr size_type GetWidth() const
Definition box2.h:214
constexpr BOX2< Vec > & Merge(const BOX2< Vec > &aRect)
Modify the position and size of the rectangle in order to contain aRect.
Definition box2.h:658
constexpr size_type GetHeight() const
Definition box2.h:215
constexpr const Vec & GetOrigin() const
Definition box2.h:210
A gerber DCODE (also called Aperture) definition.
Definition dcode.h:80
APERTURE_T m_ApertType
Aperture type ( Line, rectangle, circle, oval poly, macro )
Definition dcode.h:202
SHAPE_POLY_SET m_Polygon
Definition dcode.h:217
void ConvertShapeToPolygon(const GERBER_DRAW_ITEM *aParent)
Convert a shape to an equivalent polygon.
Definition dcode.cpp:297
D_CODE * GetDcodeDescr() const
Return the GetDcodeDescr of this object, or NULL.
VECTOR2I GetABPosition(const VECTOR2I &aXYPosition) const
Return the image position of aPosition for this object.
SHAPE_POLY_SET m_ShapeAsPolygon
void ConvertSegmentToPolygon()
Convert a line to an equivalent polygon.
GBR_BASIC_SHAPE_TYPE m_ShapeType
Hold the image data and parameters for one gerber file and layer parameters.
GERBER_DRAW_ITEMS & GetItems()
Job to convert Gerber/Excellon files to PNG images.
A color representation with 4 components: red, green, blue, alpha.
Definition color4d.h:105
static const COLOR4D WHITE
Definition color4d.h:405
double a
Alpha component.
Definition color4d.h:396
virtual void SetColorMode(bool aColorMode)
Plot in B/W or color.
Definition plotter.h:164
PNG rasterization plotter using Cairo graphics library.
Definition plotter_png.h:40
void SetPixelSize(int aWidth, int aHeight)
Set the output image dimensions in pixels.
Definition plotter_png.h:64
void SetBackgroundColor(const COLOR4D &aColor)
Set the background color for the image.
Definition plotter_png.h:77
void SetResolution(int aDPI)
Set the output resolution in dots per inch.
Definition plotter_png.h:56
virtual bool EndPlot() override
void SetAntialias(bool aEnable)
Enable or disable anti-aliasing.
Definition plotter_png.h:84
virtual void SetColor(const COLOR4D &aColor) override
virtual bool OpenFile(const wxString &aFullFilename) override
Open or create the plot file aFullFilename.
virtual void PlotPoly(const std::vector< VECTOR2I > &aCornerList, FILL_T aFill, int aWidth, void *aData=nullptr) override
Draw a polygon ( filled or not ).
void SetClearCompositing(bool aClear)
Switch the Cairo compositing operator between CLEAR and OVER.
virtual void SetViewport(const VECTOR2I &aOffset, double aIusPerDecimil, double aScale, bool aMirror) override
Set the plot offset and scaling for the current plot.
virtual bool StartPlot(const wxString &aPageNumber) override
Represent a polyline containing arcs as well as line segments: A chain of connected line and/or arc s...
int PointCount() const
Return the number of points (vertices) in this line chain.
const VECTOR2I & CPoint(int aIndex) const
Return a reference to a given point in the line chain.
Represent a set of closed polygons.
int OutlineCount() const
Return the number of outlines in the set.
const SHAPE_LINE_CHAIN & COutline(int aIndex) const
double Distance(const VECTOR2< extended_type > &aVector) const
Compute the distance between two vectors.
Definition vector2d.h:553
void TransformRingToPolygon(SHAPE_POLY_SET &aBuffer, const VECTOR2I &aCentre, int aRadius, int aWidth, int aError, ERROR_LOC aErrorLoc)
Convert arcs to multiple straight segments.
void TransformArcToPolygon(SHAPE_POLY_SET &aBuffer, const VECTOR2I &aStart, const VECTOR2I &aMid, const VECTOR2I &aEnd, int aWidth, int aError, ERROR_LOC aErrorLoc)
Convert arc to multiple straight segments.
void TransformOvalToPolygon(SHAPE_POLY_SET &aBuffer, const VECTOR2I &aStart, const VECTOR2I &aEnd, int aWidth, int aError, ERROR_LOC aErrorLoc, int aMinSegCount=0)
Convert a oblong shape to a polygon, using multiple segments.
@ APT_RECT
Definition dcode.h:50
@ RADIANS_T
Definition eda_angle.h:32
@ FILLED_SHAPE
Fill with object color.
Definition eda_shape.h:61
@ GBR_SEGMENT
@ GBR_ARC
GERBER_PLOTTER_VIEWPORT CalculatePlotterViewport(const BOX2I &aBBox, int aDpi, int aWidth, int aHeight)
Compute pixel dimensions and plotter scale from a bounding box and DPI/size settings.
bool RenderGerberToPng(const wxString &aInputPath, const wxString &aOutputPath, const GERBER_RENDER_OPTIONS &aOptions, wxString *aErrorMsg, wxArrayString *aMessages)
Render a Gerber or Excellon file to PNG.
bool IsExcellonFile(const wxString &aPath)
Determine if a file is an Excellon drill file based on extension.
BOX2I CalculateGerberBoundingBox(GERBER_FILE_IMAGE *aImage)
Calculate bounding box for all draw items in a gerber image.
std::unique_ptr< GERBER_FILE_IMAGE > LoadGerberOrExcellon(const wxString &aPath, wxString *aErrorMsg, wxArrayString *aMessages)
Load a Gerber or Excellon file, auto-detecting by extension.
static constexpr int MIN_PIXEL_SIZE
management of default values used to read a Excellon (.nc) drill file Some important parameters are n...
Computed plotter viewport parameters from a bounding box and render settings.
Render options for Gerber to PNG conversion.
double windowWidthMm
Viewport width in mm (> 0 enables viewport mode)
KIGFX::COLOR4D foregroundColor
KIGFX::COLOR4D backgroundColor
Transparent white.
int height
0 = calculate from DPI
int width
0 = calculate from DPI
double originYMm
Viewport origin Y in mm.
bool HasViewportOverride() const
double windowHeightMm
Viewport height in mm (> 0 enables viewport mode)
double originXMm
Viewport origin X in mm.
int radius
#define M_PI
VECTOR2I GetRotated(const VECTOR2I &aVector, const EDA_ANGLE &aAngle)
Return a new VECTOR2I that is the result of rotating aVector by aAngle.
Definition trigo.h:77
VECTOR2< int32_t > VECTOR2I
Definition vector2d.h:687