From 2ff74cb3fc7be819c5035cb372037163adf6dfe2 Mon Sep 17 00:00:00 2001 From: jean-pierre charras Date: Fri, 16 Mar 2018 19:32:49 +0100 Subject: [PATCH] Add support for .gbrjob new file format (JSON format) in Gerbview Add experimental code to generate .gbrjob files in the new JSON file format --- common/plotters/GERBER_plotter.cpp | 89 +- gerbview/CMakeLists.txt | 1 + gerbview/job_file_reader.cpp | 89 +- gerbview/json11.cpp | 1153 ++++++++++++++++++++ gerbview/json11.hpp | 251 +++++ pcbnew/exporters/gerber_jobfile_writer.cpp | 621 ++++++++++- pcbnew/exporters/gerber_jobfile_writer.h | 106 +- 7 files changed, 2240 insertions(+), 70 deletions(-) create mode 100644 gerbview/json11.cpp create mode 100644 gerbview/json11.hpp diff --git a/common/plotters/GERBER_plotter.cpp b/common/plotters/GERBER_plotter.cpp index ddad547b64..8371a97376 100644 --- a/common/plotters/GERBER_plotter.cpp +++ b/common/plotters/GERBER_plotter.cpp @@ -649,7 +649,6 @@ void GERBER_PLOTTER::FlashPadOval( const wxPoint& pos, const wxSize& aSize, doub EDA_DRAW_MODE_T trace_mode, void* aData ) { wxASSERT( outputFile ); - int x0, y0, x1, y1, delta; wxSize size( aSize ); GBR_METADATA* gbr_metadata = static_cast( aData ); @@ -684,18 +683,17 @@ void GERBER_PLOTTER::FlashPadOval( const wxPoint& pos, const wxSize& aSize, doub if( trace_mode == FILLED ) { // TODO: use an aperture macro to declare the rotated pad - // // Flash a pad anchor, if a netlist attribute is set if( aData ) FlashPadCircle( pos, size.x, trace_mode, aData ); // The pad is reduced to an segment with dy > dx - delta = size.y - size.x; - x0 = 0; - y0 = -delta / 2; - x1 = 0; - y1 = delta / 2; + int delta = size.y - size.x; + int x0 = 0; + int y0 = -delta / 2; + int x1 = 0; + int y1 = delta / 2; RotatePoint( &x0, &y0, orient ); RotatePoint( &x1, &y1, orient ); GBR_METADATA metadata; @@ -740,8 +738,7 @@ void GERBER_PLOTTER::FlashPadRect( const wxPoint& pos, const wxSize& aSize, case 900: case 2700: // rotation of 90 degrees or 270 swaps sizes std::swap( size.x, size.y ); - - // Pass through + // Pass through case 0: case 1800: if( trace_mode == SKETCH ) @@ -755,7 +752,7 @@ void GERBER_PLOTTER::FlashPadRect( const wxPoint& pos, const wxSize& aSize, pos.y - (size.y - currentPenWidth) / 2 ), wxPoint( pos.x + (size.x - currentPenWidth) / 2, pos.y + (size.y - currentPenWidth) / 2 ), - NO_FILL ); + NO_FILL, GetCurrentLineWidth() ); } else { @@ -800,24 +797,6 @@ void GERBER_PLOTTER::FlashPadRoundRect( const wxPoint& aPadPos, const wxSize& aS EDA_DRAW_MODE_T aTraceMode, void* aData ) { - // Currently, a Pad RoundRect is plotted as polygon. - // TODO: use Aperture macro and flash it - SHAPE_POLY_SET outline; - const int segmentToCircleCount = 64; - TransformRoundRectToPolygon( outline, aPadPos, aSize, aOrient, - aCornerRadius, segmentToCircleCount ); - - std::vector< wxPoint > cornerList; - cornerList.reserve( segmentToCircleCount + 5 ); - // TransformRoundRectToPolygon creates only one convex polygon - SHAPE_LINE_CHAIN& poly = outline.Outline( 0 ); - - for( int ii = 0; ii < poly.PointCount(); ++ii ) - cornerList.push_back( wxPoint( poly.Point( ii ).x, poly.Point( ii ).y ) ); - - // Close polygon - cornerList.push_back( cornerList[0] ); - GBR_METADATA gbr_metadata; if( aData ) @@ -832,7 +811,32 @@ void GERBER_PLOTTER::FlashPadRoundRect( const wxPoint& aPadPos, const wxSize& aS gbr_metadata.m_NetlistMetadata.ClearAttribute( &attrname ); // not allowed on inner layers } - PlotPoly( cornerList, ( aTraceMode == FILLED ) ? FILLED_SHAPE : NO_FILL, USE_DEFAULT_LINE_WIDTH, &gbr_metadata ); + if( aTraceMode != FILLED ) + SetCurrentLineWidth( USE_DEFAULT_LINE_WIDTH, &gbr_metadata ); + + // Currently, a Pad RoundRect is plotted as polygon. + // TODO: use Aperture macro and flash it + SHAPE_POLY_SET outline; + const int segmentToCircleCount = 64; + TransformRoundRectToPolygon( outline, aPadPos, aSize, aOrient, + aCornerRadius, segmentToCircleCount ); + + if( aTraceMode != FILLED ) + outline.Inflate( -GetCurrentLineWidth()/2, 16 ); + + std::vector< wxPoint > cornerList; + // TransformRoundRectToPolygon creates only one convex polygon + SHAPE_LINE_CHAIN& poly = outline.Outline( 0 ); + cornerList.reserve( poly.PointCount() + 1 ); + + for( int ii = 0; ii < poly.PointCount(); ++ii ) + cornerList.push_back( wxPoint( poly.Point( ii ).x, poly.Point( ii ).y ) ); + + // Close polygon + cornerList.push_back( cornerList[0] ); + + PlotPoly( cornerList, aTraceMode == FILLED ? FILLED_SHAPE : NO_FILL, + aTraceMode == FILLED ? 0 : GetCurrentLineWidth(), &gbr_metadata ); // Now, flash a pad anchor, if a netlist attribute is set // (remove me when a Aperture macro will be used) @@ -854,7 +858,9 @@ void GERBER_PLOTTER::FlashPadCustom( const wxPoint& aPadPos, const wxSize& aSize // However, because the anchor pad can be circle or rect, we use only // a circle not bigger than the rect. // the main purpose is to print a flashed DCode as pad anchor - FlashPadCircle( aPadPos, std::min( aSize.x, aSize.y ), aTraceMode, aData ); + if( aTraceMode == FILLED ) + FlashPadCircle( aPadPos, std::min( aSize.x, aSize.y ), aTraceMode, aData ); + GBR_METADATA gbr_metadata; if( aData ) @@ -869,11 +875,20 @@ void GERBER_PLOTTER::FlashPadCustom( const wxPoint& aPadPos, const wxSize& aSize gbr_metadata.m_NetlistMetadata.ClearAttribute( &attrname ); // not allowed on inner layers } + SHAPE_POLY_SET polyshape = *aPolygons; + + if( aTraceMode != FILLED ) + { + SetCurrentLineWidth( USE_DEFAULT_LINE_WIDTH, &gbr_metadata ); + polyshape.Inflate( -GetCurrentLineWidth()/2, 16 ); + } + std::vector< wxPoint > cornerList; - for( int cnt = 0; cnt < aPolygons->OutlineCount(); ++cnt ) + for( int cnt = 0; cnt < polyshape.OutlineCount(); ++cnt ) { - SHAPE_LINE_CHAIN& poly = aPolygons->Outline( cnt ); + SHAPE_LINE_CHAIN& poly = polyshape.Outline( cnt ); + cornerList.clear(); for( int ii = 0; ii < poly.PointCount(); ++ii ) @@ -882,7 +897,9 @@ void GERBER_PLOTTER::FlashPadCustom( const wxPoint& aPadPos, const wxSize& aSize // Close polygon cornerList.push_back( cornerList[0] ); - PlotPoly( cornerList, ( aTraceMode == FILLED ) ? FILLED_SHAPE : NO_FILL, USE_DEFAULT_LINE_WIDTH, &gbr_metadata ); + PlotPoly( cornerList, + aTraceMode == FILLED ? FILLED_SHAPE : NO_FILL, + aTraceMode == FILLED ? 0 : GetCurrentLineWidth(), &gbr_metadata ); } } @@ -902,7 +919,7 @@ void GERBER_PLOTTER::FlashPadTrapez( const wxPoint& aPadPos, const wxPoint* aCo // Now, flash a pad anchor, if a netlist attribute is set // (remove me when a Aperture macro will be used) - if( aData && (aTrace_Mode==FILLED) ) + if( aData && ( aTrace_Mode == FILLED ) ) { // Calculate the radius of the circle inside the shape // It is the smaller dist from shape pos to edges @@ -945,7 +962,9 @@ void GERBER_PLOTTER::FlashPadTrapez( const wxPoint& aPadPos, const wxPoint* aCo } SetCurrentLineWidth( USE_DEFAULT_LINE_WIDTH, &metadata ); - PlotPoly( cornerList, aTrace_Mode==FILLED ? FILLED_SHAPE : NO_FILL, USE_DEFAULT_LINE_WIDTH, &metadata ); + PlotPoly( cornerList, aTrace_Mode == FILLED ? FILLED_SHAPE : NO_FILL, + aTrace_Mode == FILLED ? 0 : GetCurrentLineWidth(), + &metadata ); } diff --git a/gerbview/CMakeLists.txt b/gerbview/CMakeLists.txt index 61045ea64e..ad8b1771d2 100644 --- a/gerbview/CMakeLists.txt +++ b/gerbview/CMakeLists.txt @@ -51,6 +51,7 @@ set( GERBVIEW_SRCS gerbview_config.cpp gerbview_frame.cpp hotkeys.cpp + json11.cpp job_file_reader.cpp locate.cpp menubar.cpp diff --git a/gerbview/job_file_reader.cpp b/gerbview/job_file_reader.cpp index f0f381e245..4fc55cdb11 100644 --- a/gerbview/job_file_reader.cpp +++ b/gerbview/job_file_reader.cpp @@ -41,19 +41,39 @@ #include #include +#include "json11.hpp" // A light JSON parser /** * this class read and parse a Gerber job file to extract useful info * for GerbView * - * In a gerber job file, data lines start by + * In a gerber job file, old (deprecated) format, data lines start by * %TF. (usual Gerber X2 info) * %TJ.B. (board info) * %TJ.D. (design info) * %TJ.L. (layers info) * some others are not yet handled by Kicad * M02* is the last line + + * In a gerber job file, JSON format, first lines are + * { + * "Header": + * and the block ( a JSON array) containing the filename of files to load is + * "FilesAttributes": + * [ + * { + * "Path": "interf_u-Composant.gbr", + * "FileFunction": "Copper,L1,Top", + * "FilePolarity": "Positive" + * }, + * { + * "Path": "interf_u-In1.Cu.gbr", + * "FileFunction": "Copper,L2,Inr", + * "FilePolarity": "Positive" + * }, + * ], */ + class GERBER_JOBFILE_READER { public: @@ -96,28 +116,65 @@ bool GERBER_JOBFILE_READER::ReadGerberJobFile() wxString msg; wxString data; - while( true ) + // detect the file format: old gerber format of new JSON format + bool json_format = false; + + char* line = jobfileReader.ReadLine(); + + if( !line ) // end of file + return false; + + data = line; + + if( data.Contains("{" ) ) + json_format = true; + + if( json_format ) { - char* line = jobfileReader.ReadLine(); + while( ( line = jobfileReader.ReadLine() ) ) + data << '\n' << line; - if( !line ) // end of file - break; + std::string err; + json11::Json json_parser = json11::Json::parse( TO_UTF8( data ), err ); - wxString text( line ); - text.Trim( true ); - text.Trim( false ); + if( !err.empty() ) + return false; - // Search for lines starting by '%', others are not usefull - if( text.StartsWith( "%TJ.L.", &data ) // First job file syntax - || text.StartsWith( "%TJ.L_", &data ) // current job file syntax - ) + for( auto& entry : json_parser["FilesAttributes"].array_items() ) { - parseTJLayerString( data ); - continue; + //wxLogMessage( entry.dump().c_str() ); + std::string name = entry["Path"].string_value(); + //wxLogMessage( name.c_str() ); + m_GerberFiles.Add( FormatStringFromGerber( name ) ); } + } + else + { + jobfileReader.Rewind(); - if( text.StartsWith( "M02" ) ) // End of file - break; + while( true ) + { + line = jobfileReader.ReadLine(); + + if( !line ) // end of file + break; + + wxString text( line ); + text.Trim( true ); + text.Trim( false ); + + // Search for lines starting by '%', others are not usefull + if( text.StartsWith( "%TJ.L.", &data ) // First job file syntax + || text.StartsWith( "%TJ.L_", &data ) // current job file syntax + ) + { + parseTJLayerString( data ); + continue; + } + + if( text.StartsWith( "M02" ) ) // End of file + break; + } } return true; diff --git a/gerbview/json11.cpp b/gerbview/json11.cpp new file mode 100644 index 0000000000..17bbee7f56 --- /dev/null +++ b/gerbview/json11.cpp @@ -0,0 +1,1153 @@ +/* Copyright (c) 2013 Dropbox, Inc. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +#include "json11.hpp" +#include +#include +#include +#include +#include + +namespace json11 { +static const int max_depth = 200; + +using std::string; +using std::vector; +using std::map; +using std::make_shared; +using std::initializer_list; +using std::move; + +/* Helper for representing null - just a do-nothing struct, plus comparison + * operators so the helpers in JsonValue work. We can't use nullptr_t because + * it may not be orderable. + */ +struct NullStruct +{ + bool operator==( NullStruct ) const { return true; } + bool operator<( NullStruct ) const { return false; } +}; + +/* * * * * * * * * * * * * * * * * * * * + * Serialization + */ + +static void dump( NullStruct, string& out ) +{ + out += "null"; +} + + +static void dump( double value, string& out ) +{ + if( std::isfinite( value ) ) + { + char buf[32]; + snprintf( buf, sizeof buf, "%.17g", value ); + out += buf; + } + else + { + out += "null"; + } +} + + +static void dump( int value, string& out ) +{ + char buf[32]; + + snprintf( buf, sizeof buf, "%d", value ); + out += buf; +} + + +static void dump( bool value, string& out ) +{ + out += value ? "true" : "false"; +} + + +static void dump( const string& value, string& out ) +{ + out += '"'; + + for( size_t i = 0; i < value.length(); i++ ) + { + const char ch = value[i]; + + if( ch == '\\' ) + { + out += "\\\\"; + } + else if( ch == '"' ) + { + out += "\\\""; + } + else if( ch == '\b' ) + { + out += "\\b"; + } + else if( ch == '\f' ) + { + out += "\\f"; + } + else if( ch == '\n' ) + { + out += "\\n"; + } + else if( ch == '\r' ) + { + out += "\\r"; + } + else if( ch == '\t' ) + { + out += "\\t"; + } + else if( static_cast(ch) <= 0x1f ) + { + char buf[8]; + snprintf( buf, sizeof buf, "\\u%04x", ch ); + out += buf; + } + else if( static_cast(ch) == 0xe2 && static_cast(value[i + 1]) == 0x80 + && static_cast(value[i + 2]) == 0xa8 ) + { + out += "\\u2028"; + i += 2; + } + else if( static_cast(ch) == 0xe2 && static_cast(value[i + 1]) == 0x80 + && static_cast(value[i + 2]) == 0xa9 ) + { + out += "\\u2029"; + i += 2; + } + else + { + out += ch; + } + } + + out += '"'; +} + + +static void dump( const Json::array& values, string& out ) +{ + bool first = true; + + out += "["; + + for( const auto& value : values ) + { + if( !first ) + out += ", "; + + value.dump( out ); + first = false; + } + + out += "]"; +} + + +static void dump( const Json::object& values, string& out ) +{ + bool first = true; + + out += "{"; + + for( const auto& kv : values ) + { + if( !first ) + out += ", "; + + dump( kv.first, out ); + out += ": "; + kv.second.dump( out ); + first = false; + } + + out += "}"; +} + + +void Json::dump( string& out ) const +{ + m_ptr->dump( out ); +} + + +/* * * * * * * * * * * * * * * * * * * * + * Value wrappers + */ + +template +class Value : public JsonValue +{ +protected: + + // Constructors + explicit Value( const T& value ) : m_value( value ) {} + explicit Value( T&& value ) : m_value( move( value ) ) {} + + // Get type tag + Json::Type type() const override + { + return tag; + } + + // Comparisons + bool equals( const JsonValue* other ) const override + { + return m_value == static_cast*>(other)->m_value; + } + + bool less( const JsonValue* other ) const override + { + return m_value < static_cast*>(other)->m_value; + } + + const T m_value; + void dump( string& out ) const override { json11::dump( m_value, out ); } +}; + +class JsonDouble final : public Value +{ + double number_value() const override { return m_value; } + int int_value() const override { return static_cast(m_value); } + bool equals( const JsonValue* other ) const override + { + return m_value == other->number_value(); + } + bool less( const JsonValue* other ) const override + { + return m_value < other->number_value(); + } + +public: + explicit JsonDouble( double value ) : Value( value ) {} +}; + +class JsonInt final : public Value +{ + double number_value() const override { return m_value; } + int int_value() const override { return m_value; } + bool equals( const JsonValue* other ) const override + { + return m_value == other->number_value(); + } + bool less( const JsonValue* other ) const override + { + return m_value < other->number_value(); + } + +public: + explicit JsonInt( int value ) : Value( value ) {} +}; + +class JsonBoolean final : public Value +{ + bool bool_value() const override { return m_value; } + +public: + explicit JsonBoolean( bool value ) : Value( value ) {} +}; + +class JsonString final : public Value +{ + const string& string_value() const override { return m_value; } + +public: + explicit JsonString( const string& value ) : Value( value ) {} + explicit JsonString( string&& value ) : Value( move( value ) ) {} +}; + +class JsonArray final : public Value +{ + const Json::array& array_items() const override { return m_value; } + const Json& operator[]( size_t i ) const override; + +public: + explicit JsonArray( const Json::array& value ) : Value( value ) {} + explicit JsonArray( Json::array&& value ) : Value( move( value ) ) {} +}; + +class JsonObject final : public Value +{ + const Json::object& object_items() const override { return m_value; } + const Json& operator[]( const string& key ) const override; + +public: + explicit JsonObject( const Json::object& value ) : Value( value ) {} + explicit JsonObject( Json::object&& value ) : Value( move( value ) ) {} +}; + +class JsonNull final : public Value +{ +public: + JsonNull() : Value( {} ) {} +}; + +/* * * * * * * * * * * * * * * * * * * * + * Static globals - static-init-safe + */ +struct Statics +{ + const std::shared_ptr null = make_shared(); + const std::shared_ptr t = make_shared( true ); + const std::shared_ptr f = make_shared( false ); + const string empty_string; + const vector empty_vector; + const map empty_map; + Statics() {} +}; + +static const Statics& statics() +{ + static const Statics s {}; + + return s; +} + + +static const Json& static_null() +{ + // This has to be separate, not in Statics, because Json() accesses statics().null. + static const Json json_null; + + return json_null; +} + + +/* * * * * * * * * * * * * * * * * * * * + * Constructors + */ + +Json::Json() noexcept : m_ptr( statics().null ) +{ +} + + +Json::Json( std::nullptr_t ) noexcept : m_ptr( statics().null ) +{ +} + + +Json::Json( double value ) : m_ptr( make_shared( value ) ) +{ +} + + +Json::Json( int value ) : m_ptr( make_shared( value ) ) +{ +} + + +Json::Json( bool value ) : m_ptr( value ? statics().t : statics().f ) +{ +} + + +Json::Json( const string& value ) : m_ptr( make_shared( value ) ) +{ +} + + +Json::Json( string&& value ) : m_ptr( make_shared( move( value ) ) ) +{ +} + + +Json::Json( const char* value ) : m_ptr( make_shared( value ) ) +{ +} + + +Json::Json( const Json::array& values ) : m_ptr( make_shared( values ) ) +{ +} + + +Json::Json( Json::array&& values ) : m_ptr( make_shared( move( values ) ) ) +{ +} + + +Json::Json( const Json::object& values ) : m_ptr( make_shared( values ) ) +{ +} + + +Json::Json( Json::object&& values ) : m_ptr( make_shared( move( values ) ) ) +{ +} + + +/* * * * * * * * * * * * * * * * * * * * + * Accessors + */ + +Json::Type Json::type() const +{ + return m_ptr->type(); +} + + +double Json::number_value() const +{ + return m_ptr->number_value(); +} + + +int Json::int_value() const +{ + return m_ptr->int_value(); +} + + +bool Json::bool_value() const +{ + return m_ptr->bool_value(); +} + + +const string& Json::string_value() const +{ + return m_ptr->string_value(); +} + + +const vector& Json::array_items() const +{ + return m_ptr->array_items(); +} + + +const map& Json::object_items() const +{ + return m_ptr->object_items(); +} + + +const Json& Json::operator[]( size_t i ) const +{ + return (*m_ptr)[i]; +} + + +const Json& Json::operator[]( const string& key ) const +{ + return (*m_ptr)[key]; +} + + +double JsonValue::number_value() const +{ + return 0; +} + + +int JsonValue::int_value() const +{ + return 0; +} + + +bool JsonValue::bool_value() const +{ + return false; +} + + +const string& JsonValue::string_value() const +{ + return statics().empty_string; +} + + +const vector& JsonValue::array_items() const +{ + return statics().empty_vector; +} + + +const map& JsonValue::object_items() const +{ + return statics().empty_map; +} + + +const Json& JsonValue::operator[]( size_t ) const +{ + return static_null(); +} + + +const Json& JsonValue::operator[]( const string& ) const +{ + return static_null(); +} + + +const Json& JsonObject::operator[]( const string& key ) const +{ + auto iter = m_value.find( key ); + + return ( iter == m_value.end() ) ? static_null() : iter->second; +} + + +const Json& JsonArray::operator[]( size_t i ) const +{ + if( i >= m_value.size() ) + return static_null(); + else + return m_value[i]; +} + + +/* * * * * * * * * * * * * * * * * * * * + * Comparison + */ + +bool Json::operator==( const Json& other ) const +{ + if( m_ptr == other.m_ptr ) + return true; + + if( m_ptr->type() != other.m_ptr->type() ) + return false; + + return m_ptr->equals( other.m_ptr.get() ); +} + + +bool Json::operator<( const Json& other ) const +{ + if( m_ptr == other.m_ptr ) + return false; + + if( m_ptr->type() != other.m_ptr->type() ) + return m_ptr->type() < other.m_ptr->type(); + + return m_ptr->less( other.m_ptr.get() ); +} + + +/* * * * * * * * * * * * * * * * * * * * + * Parsing + */ + +/* esc(c) + * + * Format char c suitable for printing in an error message. + */ +static inline string esc( char c ) +{ + char buf[12]; + + if( static_cast(c) >= 0x20 && static_cast(c) <= 0x7f ) + { + snprintf( buf, sizeof buf, "'%c' (%d)", c, c ); + } + else + { + snprintf( buf, sizeof buf, "(%d)", c ); + } + + return string( buf ); +} + + +static inline bool in_range( long x, long lower, long upper ) +{ + return x >= lower && x <= upper; +} + + +namespace { +/* JsonParser + * + * Object that tracks all state of an in-progress parse. + */ +struct JsonParser final +{ + /* State + */ + const string& str; + size_t i; + string& err; + bool failed; + const JsonParse strategy; + + /* fail(msg, err_ret = Json()) + * + * Mark this parse as failed. + */ + Json fail( string&& msg ) + { + return fail( move( msg ), Json() ); + } + + template + T fail( string&& msg, const T err_ret ) + { + if( !failed ) + err = std::move( msg ); + + failed = true; + return err_ret; + } + + /* consume_whitespace() + * + * Advance until the current character is non-whitespace. + */ + void consume_whitespace() + { + while( str[i] == ' ' || str[i] == '\r' || str[i] == '\n' || str[i] == '\t' ) + i++; + } + + /* consume_comment() + * + * Advance comments (c-style inline and multiline). + */ + bool consume_comment() + { + bool comment_found = false; + + if( str[i] == '/' ) + { + i++; + + if( i == str.size() ) + return fail( "unexpected end of input after start of comment", false ); + + if( str[i] == '/' ) // inline comment + { + i++; + + // advance until next line, or end of input + while( i < str.size() && str[i] != '\n' ) + { + i++; + } + + comment_found = true; + } + else if( str[i] == '*' ) // multiline comment + { + i++; + + if( i > str.size() - 2 ) + return fail( "unexpected end of input inside multi-line comment", false ); + + // advance until closing tokens + while( !(str[i] == '*' && str[i + 1] == '/') ) + { + i++; + + if( i > str.size() - 2 ) + return fail( + "unexpected end of input inside multi-line comment", false ); + } + + i += 2; + comment_found = true; + } + else + return fail( "malformed comment", false ); + } + + return comment_found; + } + + /* consume_garbage() + * + * Advance until the current character is non-whitespace and non-comment. + */ + void consume_garbage() + { + consume_whitespace(); + + if( strategy == JsonParse::COMMENTS ) + { + bool comment_found = false; + + do { + comment_found = consume_comment(); + + if( failed ) return; + + consume_whitespace(); + } while( comment_found ); + } + } + + /* get_next_token() + * + * Return the next non-whitespace character. If the end of the input is reached, + * flag an error and return 0. + */ + char get_next_token() + { + consume_garbage(); + + if( failed ) return (char) 0; + + if( i == str.size() ) + return fail( "unexpected end of input", (char) 0 ); + + return str[i++]; + } + + /* encode_utf8(pt, out) + * + * Encode pt as UTF-8 and add it to out. + */ + void encode_utf8( long pt, string& out ) + { + if( pt < 0 ) + return; + + if( pt < 0x80 ) + { + out += static_cast(pt); + } + else if( pt < 0x800 ) + { + out += static_cast( (pt >> 6) | 0xC0 ); + out += static_cast( (pt & 0x3F) | 0x80 ); + } + else if( pt < 0x10000 ) + { + out += static_cast( (pt >> 12) | 0xE0 ); + out += static_cast( ( (pt >> 6) & 0x3F ) | 0x80 ); + out += static_cast( (pt & 0x3F) | 0x80 ); + } + else + { + out += static_cast( (pt >> 18) | 0xF0 ); + out += static_cast( ( (pt >> 12) & 0x3F ) | 0x80 ); + out += static_cast( ( (pt >> 6) & 0x3F ) | 0x80 ); + out += static_cast( (pt & 0x3F) | 0x80 ); + } + } + + /* parse_string() + * + * Parse a string, starting at the current position. + */ + string parse_string() + { + string out; + long last_escaped_codepoint = -1; + + while( true ) + { + if( i == str.size() ) + return fail( "unexpected end of input in string", "" ); + + char ch = str[i++]; + + if( ch == '"' ) + { + encode_utf8( last_escaped_codepoint, out ); + return out; + } + + if( in_range( ch, 0, 0x1f ) ) + return fail( "unescaped " + esc( ch ) + " in string", "" ); + + // The usual case: non-escaped characters + if( ch != '\\' ) + { + encode_utf8( last_escaped_codepoint, out ); + last_escaped_codepoint = -1; + out += ch; + continue; + } + + // Handle escapes + if( i == str.size() ) + return fail( "unexpected end of input in string", "" ); + + ch = str[i++]; + + if( ch == 'u' ) + { + // Extract 4-byte escape sequence + string esc = str.substr( i, 4 ); + + // Explicitly check length of the substring. The following loop + // relies on std::string returning the terminating NUL when + // accessing str[length]. Checking here reduces brittleness. + if( esc.length() < 4 ) + { + return fail( "bad \\u escape: " + esc, "" ); + } + + for( size_t j = 0; j < 4; j++ ) + { + if( !in_range( esc[j], 'a', 'f' ) && !in_range( esc[j], 'A', 'F' ) + && !in_range( esc[j], '0', '9' ) ) + return fail( "bad \\u escape: " + esc, "" ); + } + + long codepoint = strtol( esc.data(), nullptr, 16 ); + + // JSON specifies that characters outside the BMP shall be encoded as a pair + // of 4-hex-digit \u escapes encoding their surrogate pair components. Check + // whether we're in the middle of such a beast: the previous codepoint was an + // escaped lead (high) surrogate, and this is a trail (low) surrogate. + if( in_range( last_escaped_codepoint, 0xD800, 0xDBFF ) + && in_range( codepoint, 0xDC00, 0xDFFF ) ) + { + // Reassemble the two surrogate pairs into one astral-plane character, per + // the UTF-16 algorithm. + encode_utf8( ( ( (last_escaped_codepoint - 0xD800) << 10 ) + | (codepoint - 0xDC00) ) + 0x10000, out ); + last_escaped_codepoint = -1; + } + else + { + encode_utf8( last_escaped_codepoint, out ); + last_escaped_codepoint = codepoint; + } + + i += 4; + continue; + } + + encode_utf8( last_escaped_codepoint, out ); + last_escaped_codepoint = -1; + + if( ch == 'b' ) + { + out += '\b'; + } + else if( ch == 'f' ) + { + out += '\f'; + } + else if( ch == 'n' ) + { + out += '\n'; + } + else if( ch == 'r' ) + { + out += '\r'; + } + else if( ch == 't' ) + { + out += '\t'; + } + else if( ch == '"' || ch == '\\' || ch == '/' ) + { + out += ch; + } + else + { + return fail( "invalid escape character " + esc( ch ), "" ); + } + } + } + + /* parse_number() + * + * Parse a double. + */ + Json parse_number() + { + size_t start_pos = i; + + if( str[i] == '-' ) + i++; + + // Integer part + if( str[i] == '0' ) + { + i++; + + if( in_range( str[i], '0', '9' ) ) + return fail( "leading 0s not permitted in numbers" ); + } + else if( in_range( str[i], '1', '9' ) ) + { + i++; + + while( in_range( str[i], '0', '9' ) ) + i++; + } + else + { + return fail( "invalid " + esc( str[i] ) + " in number" ); + } + + if( str[i] != '.' && str[i] != 'e' && str[i] != 'E' + && (i - start_pos) <= static_cast(std::numeric_limits::digits10) ) + { + return std::atoi( str.c_str() + start_pos ); + } + + // Decimal part + if( str[i] == '.' ) + { + i++; + + if( !in_range( str[i], '0', '9' ) ) + return fail( "at least one digit required in fractional part" ); + + while( in_range( str[i], '0', '9' ) ) + i++; + } + + // Exponent part + if( str[i] == 'e' || str[i] == 'E' ) + { + i++; + + if( str[i] == '+' || str[i] == '-' ) + i++; + + if( !in_range( str[i], '0', '9' ) ) + return fail( "at least one digit required in exponent" ); + + while( in_range( str[i], '0', '9' ) ) + i++; + } + + return std::strtod( str.c_str() + start_pos, nullptr ); + } + + /* expect(str, res) + * + * Expect that 'str' starts at the character that was just read. If it does, advance + * the input and return res. If not, flag an error. + */ + Json expect( const string& expected, Json res ) + { + assert( i != 0 ); + i--; + + if( str.compare( i, expected.length(), expected ) == 0 ) + { + i += expected.length(); + return res; + } + else + { + return fail( "parse error: expected " + expected + ", got " + + str.substr( i, expected.length() ) ); + } + } + + /* parse_json() + * + * Parse a JSON object. + */ + Json parse_json( int depth ) + { + if( depth > max_depth ) + { + return fail( "exceeded maximum nesting depth" ); + } + + char ch = get_next_token(); + + if( failed ) + return Json(); + + if( ch == '-' || (ch >= '0' && ch <= '9') ) + { + i--; + return parse_number(); + } + + if( ch == 't' ) + return expect( "true", true ); + + if( ch == 'f' ) + return expect( "false", false ); + + if( ch == 'n' ) + return expect( "null", Json() ); + + if( ch == '"' ) + return parse_string(); + + if( ch == '{' ) + { + map data; + ch = get_next_token(); + + if( ch == '}' ) + return data; + + while( 1 ) + { + if( ch != '"' ) + return fail( "expected '\"' in object, got " + esc( ch ) ); + + string key = parse_string(); + + if( failed ) + return Json(); + + ch = get_next_token(); + + if( ch != ':' ) + return fail( "expected ':' in object, got " + esc( ch ) ); + + data[std::move( key )] = parse_json( depth + 1 ); + + if( failed ) + return Json(); + + ch = get_next_token(); + + if( ch == '}' ) + break; + + if( ch != ',' ) + return fail( "expected ',' in object, got " + esc( ch ) ); + + ch = get_next_token(); + } + + return data; + } + + if( ch == '[' ) + { + vector data; + ch = get_next_token(); + + if( ch == ']' ) + return data; + + while( 1 ) + { + i--; + data.push_back( parse_json( depth + 1 ) ); + + if( failed ) + return Json(); + + ch = get_next_token(); + + if( ch == ']' ) + break; + + if( ch != ',' ) + return fail( "expected ',' in list, got " + esc( ch ) ); + + ch = get_next_token(); + (void) ch; + } + + return data; + } + + return fail( "expected value, got " + esc( ch ) ); + } +}; +} // namespace { + +Json Json::parse( const string& in, string& err, JsonParse strategy ) +{ + JsonParser parser { + in, 0, err, false, strategy + }; + Json result = parser.parse_json( 0 ); + + // Check for any trailing garbage + parser.consume_garbage(); + + if( parser.failed ) + return Json(); + + if( parser.i != in.size() ) + return parser.fail( "unexpected trailing " + esc( in[parser.i] ) ); + + return result; +} + + +// Documented in json11.hpp +vector Json::parse_multi( const string& in, + std::string::size_type& parser_stop_pos, + string& err, + JsonParse strategy ) +{ + JsonParser parser { + in, 0, err, false, strategy + }; + + parser_stop_pos = 0; + vector json_vec; + + while( parser.i != in.size() && !parser.failed ) + { + json_vec.push_back( parser.parse_json( 0 ) ); + + if( parser.failed ) + break; + + // Check for another object + parser.consume_garbage(); + + if( parser.failed ) + break; + + parser_stop_pos = parser.i; + } + + return json_vec; +} + + +/* * * * * * * * * * * * * * * * * * * * + * Shape-checking + */ + +bool Json::has_shape( const shape& types, string& err ) const +{ + if( !is_object() ) + { + err = "expected JSON object, got " + dump(); + return false; + } + + for( auto& item : types ) + { + if( (*this)[item.first].type() != item.second ) + { + err = "bad type for " + item.first + " in " + dump(); + return false; + } + } + + return true; +} +} // namespace json11 diff --git a/gerbview/json11.hpp b/gerbview/json11.hpp new file mode 100644 index 0000000000..78c4b109be --- /dev/null +++ b/gerbview/json11.hpp @@ -0,0 +1,251 @@ +/* json11 + * + * json11 is a tiny JSON library for C++11, providing JSON parsing and serialization. + * + * The core object provided by the library is json11::Json. A Json object represents any JSON + * value: null, bool, number (int or double), string (std::string), array (std::vector), or + * object (std::map). + * + * Json objects act like values: they can be assigned, copied, moved, compared for equality or + * order, etc. There are also helper methods Json::dump, to serialize a Json to a string, and + * Json::parse (static) to parse a std::string as a Json object. + * + * Internally, the various types of Json object are represented by the JsonValue class + * hierarchy. + * + * A note on numbers - JSON specifies the syntax of number formatting but not its semantics, + * so some JSON implementations distinguish between integers and floating-point numbers, while + * some don't. In json11, we choose the latter. Because some JSON implementations (namely + * Javascript itself) treat all numbers as the same type, distinguishing the two leads + * to JSON that will be *silently* changed by a round-trip through those implementations. + * Dangerous! To avoid that risk, json11 stores all numbers as double internally, but also + * provides integer helpers. + * + * Fortunately, double-precision IEEE754 ('double') can precisely store any integer in the + * range +/-2^53, which includes every 'int' on most systems. (Timestamps often use int64 + * or long long to avoid the Y2038K problem; a double storing microseconds since some epoch + * will be exact for +/- 275 years.) + */ + +/* Copyright (c) 2013 Dropbox, Inc. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +#pragma once + +#include +#include +#include +#include +#include + +#ifdef _MSC_VER + #if _MSC_VER <= 1800 // VS 2013 + #ifndef noexcept + #define noexcept throw() + #endif + + #ifndef snprintf + #define snprintf _snprintf_s + #endif + #endif +#endif + +namespace json11 { +enum JsonParse +{ + STANDARD, COMMENTS +}; + +class JsonValue; + +class Json final +{ +public: + // Types + enum Type + { + NUL, NUMBER, BOOL, STRING, ARRAY, OBJECT + }; + + // Array and object typedefs + typedef std::vector array; + typedef std::map object; + + // Constructors for the various types of JSON value. + Json() noexcept; // NUL + Json( std::nullptr_t ) noexcept; // NUL + Json( double value ); // NUMBER + Json( int value ); // NUMBER + Json( bool value ); // BOOL + Json( const std::string& value ); // STRING + Json( std::string&& value ); // STRING + Json( const char* value ); // STRING + Json( const array& values ); // ARRAY + Json( array&& values ); // ARRAY + Json( const object& values ); // OBJECT + Json( object&& values ); // OBJECT + + // Implicit constructor: anything with a to_json() function. + template + Json( const T& t ) : Json( t.to_json() ) {} + + // Implicit constructor: map-like objects (std::map, std::unordered_map, etc) + template ().begin()->first)>::value + && std::is_constructible().begin()->second)>::value, + int>::type = 0> + Json( const M& m ) : Json( object( m.begin(), m.end() ) ) {} + + // Implicit constructor: vector-like objects (std::list, std::vector, std::set, etc) + template ().begin() )>::value, + int>::type = 0> + Json( const V& v ) : Json( array( v.begin(), v.end() ) ) {} + + // This prevents Json(some_pointer) from accidentally producing a bool. Use + // Json(bool(some_pointer)) if that behavior is desired. + Json( void* ) = delete; + + // Accessors + Type type() const; + + bool is_null() const { return type() == NUL; } + bool is_number() const { return type() == NUMBER; } + bool is_bool() const { return type() == BOOL; } + bool is_string() const { return type() == STRING; } + bool is_array() const { return type() == ARRAY; } + bool is_object() const { return type() == OBJECT; } + + // Return the enclosed value if this is a number, 0 otherwise. Note that json11 does not + // distinguish between integer and non-integer numbers - number_value() and int_value() + // can both be applied to a NUMBER-typed object. + double number_value() const; + int int_value() const; + + // Return the enclosed value if this is a boolean, false otherwise. + bool bool_value() const; + + // Return the enclosed string if this is a string, "" otherwise. + const std::string& string_value() const; + + // Return the enclosed std::vector if this is an array, or an empty vector otherwise. + const array& array_items() const; + + // Return the enclosed std::map if this is an object, or an empty map otherwise. + const object& object_items() const; + + // Return a reference to arr[i] if this is an array, Json() otherwise. + const Json& operator[]( size_t i ) const; + + // Return a reference to obj[key] if this is an object, Json() otherwise. + const Json& operator[]( const std::string& key ) const; + + // Serialize. + void dump( std::string& out ) const; + + std::string dump() const + { + std::string out; + + dump( out ); + return out; + } + + // Parse. If parse fails, return Json() and assign an error message to err. + static Json parse( const std::string& in, + std::string& err, + JsonParse strategy = JsonParse::STANDARD ); + + static Json parse( const char* in, + std::string& err, + JsonParse strategy = JsonParse::STANDARD ) + { + if( in ) + { + return parse( std::string( in ), err, strategy ); + } + else + { + err = "null input"; + return nullptr; + } + } + + // Parse multiple objects, concatenated or separated by whitespace + static std::vector parse_multi( const std::string& in, + std::string::size_type& parser_stop_pos, + std::string& err, + JsonParse strategy = JsonParse::STANDARD ); + + static inline std::vector parse_multi( const std::string& in, + std::string& err, + JsonParse strategy = JsonParse::STANDARD ) + { + std::string::size_type parser_stop_pos; + + return parse_multi( in, parser_stop_pos, err, strategy ); + } + + bool operator==( const Json& rhs ) const; + bool operator<( const Json& rhs ) const; + + bool operator!=( const Json& rhs ) const { return !(*this == rhs); } + bool operator<=( const Json& rhs ) const { return !(rhs < *this); } + bool operator>( const Json& rhs ) const { return rhs < *this; } + bool operator>=( const Json& rhs ) const { return !(*this < rhs); } + + /* has_shape(types, err) + * + * Return true if this is a JSON object and, for each item in types, has a field of + * the given type. If not, return false and set err to a descriptive message. + */ + typedef std::initializer_list > shape; + bool has_shape( const shape& types, std::string& err ) const; + +private: + std::shared_ptr m_ptr; +}; + +// Internal class hierarchy - JsonValue objects are not exposed to users of this API. +class JsonValue +{ +protected: + friend class Json; + friend class JsonInt; + friend class JsonDouble; + virtual Json::Type type() const = 0; + virtual bool equals( const JsonValue* other ) const = 0; + virtual bool less( const JsonValue* other ) const = 0; + virtual void dump( std::string& out ) const = 0; + virtual double number_value() const; + virtual int int_value() const; + virtual bool bool_value() const; + virtual const std::string& string_value() const; + virtual const Json::array& array_items() const; + virtual const Json& operator[]( size_t i ) const; + virtual const Json::object& object_items() const; + virtual const Json& operator[]( const std::string& key ) const; + + virtual ~JsonValue() {} +}; +} // namespace json11 diff --git a/pcbnew/exporters/gerber_jobfile_writer.cpp b/pcbnew/exporters/gerber_jobfile_writer.cpp index 78247d2ad0..c4856df9fc 100644 --- a/pcbnew/exporters/gerber_jobfile_writer.cpp +++ b/pcbnew/exporters/gerber_jobfile_writer.cpp @@ -1,8 +1,8 @@ /* * This program source code file is part of KiCad, a free EDA CAD application. * - * Copyright (C) 2017 Jean_Pierre Charras - * Copyright (C) 1992-2017 KiCad Developers, see AUTHORS.txt for contributors. + * Copyright (C) 2018 Jean_Pierre Charras + * Copyright (C) 1992-2018 KiCad Developers, see AUTHORS.txt for contributors. * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License @@ -24,7 +24,7 @@ /** * @file gendrill_gerber_writer.cpp - * @brief Functions to create drill files in gerber X2 format. + * @brief Functions to create the Gerber job file in JSON format. */ #include @@ -52,6 +52,8 @@ GERBER_JOBFILE_WRITER::GERBER_JOBFILE_WRITER( BOARD* aPcb, REPORTER* aReporter ) m_pcb = aPcb; m_reporter = aReporter; m_conversionUnits = 1.0 / IU_PER_MM; // Gerber units = mm + m_useJSONformat = true; + m_indent = 0; } enum ONSIDE GERBER_JOBFILE_WRITER::hasSilkLayers() @@ -113,10 +115,38 @@ const char* GERBER_JOBFILE_WRITER::sideKeyValue( enum ONSIDE aValue ) } +bool GERBER_JOBFILE_WRITER::CreateJobFile( const wxString& aFullFilename ) +{ + bool success; + wxString msg; + + if( m_useJSONformat ) + success = CreateJSONJobFile( aFullFilename ); + else + success = CreateGbrJobFile( aFullFilename ); + + if( !success ) + { + if( m_reporter ) + { + msg.Printf( _( "Unable to create job file \"%s\"" ), aFullFilename ); + m_reporter->Report( msg, REPORTER::RPT_ERROR ); + } + } + else if( m_reporter ) + { + msg.Printf( _( "Create Gerber job file \"%s\"" ), aFullFilename ); + m_reporter->Report( msg, REPORTER::RPT_ACTION ); + } + + return success; +} + + extern void BuildGerberX2Header( const BOARD *aBoard, wxArrayString& aHeader ); -bool GERBER_JOBFILE_WRITER::CreateJobFile( const wxString& aFullFilename ) +bool GERBER_JOBFILE_WRITER::CreateGbrJobFile( const wxString& aFullFilename ) { // Note: in Gerber job file, dimensions are in mm, and are floating numbers FILE* jobFile = wxFopen( aFullFilename, "wt" ); @@ -124,14 +154,7 @@ bool GERBER_JOBFILE_WRITER::CreateJobFile( const wxString& aFullFilename ) wxString msg; if( jobFile == nullptr ) - { - if( m_reporter ) - { - msg.Printf( _( "Unable to create job file \"%s\"" ), aFullFilename ); - m_reporter->Report( msg, REPORTER::RPT_ERROR ); - } return false; - } LOCALE_IO dummy; @@ -378,11 +401,581 @@ bool GERBER_JOBFILE_WRITER::CreateJobFile( const wxString& aFullFilename ) fclose( jobFile ); - if( m_reporter ) + return true; +} + + + +void GERBER_JOBFILE_WRITER::addJSONHeader() +{ + wxString text; + openBlock(); + addJSONObject( "\"Header\":\n" ); + openBlock(); + + // Creates the GenerationSoftware + addJSONObject( "\"GenerationSoftware\":\n" ); + openBlock(); + addJSONObject( "\"Vendor\": \"KiCad\",\n" ); + addJSONObject( "\"Application\": \"Pcbnew\",\n" ); + text.Printf( "\"Version\": \"%s\"\n", GetBuildVersion() ); + addJSONObject( text ); + closeBlockWithSep(); + + // creates the TF.CreationDate ext: + // The attribute value must conform to the full version of the ISO 8601 + // date and time format, including time and time zone. Note that this is + // the date the Gerber file was effectively created, + // not the time the project of PCB was started + wxDateTime date( wxDateTime::GetTimeNow() ); + // Date format: see http://www.cplusplus.com/reference/ctime/strftime + wxString msg = date.Format( wxT( "%z" ) ); // Extract the time zone offset + // The time zone offset format is + (or -) mm or hhmm (mm = number of minutes, hh = number of hours) + // we want +(or -) hh:mm + if( msg.Len() > 3 ) + msg.insert( 3, ":", 1 ), + text.Printf( wxT( "\"CreationDate\": \"%s%s\"\n" ),date.FormatISOCombined(), msg ); + addJSONObject( text ); + + closeBlockWithSep(); +} + + +void GERBER_JOBFILE_WRITER::removeJSONSepararator() +{ + if( m_JSONbuffer.Last() == ',' ) { - msg.Printf( _( "Create Gerber job file \"%s\"" ), aFullFilename ); - m_reporter->Report( msg, REPORTER::RPT_ACTION ); + m_JSONbuffer.RemoveLast(); + return; } + if( m_JSONbuffer.Last() == '\n' ) + { + m_JSONbuffer.RemoveLast(); + + if( m_JSONbuffer.Last() == ',' ) + m_JSONbuffer.RemoveLast(); + + m_JSONbuffer.Append( '\n' ); + } +} + +bool GERBER_JOBFILE_WRITER::CreateJSONJobFile( const wxString& aFullFilename ) +{ + // Note: in Gerber job file, dimensions are in mm, and are floating numbers + FILE* jobFile = wxFopen( aFullFilename, "wt" ); + + m_JSONbuffer.Empty(); + m_indent = 0; + + if( jobFile == nullptr ) + return false; + + LOCALE_IO dummy; + + // output the job file header + addJSONHeader(); + + // Add the General Specs + addJSONGeneralSpecs(); + + // Job file support a few design rules: + addJSONDesignRules(); + + // output the gerber file list: + addJSONFilesAttributes(); + + // output the board stackup: + addJSONMaterialStackup(); + + // Close job file full block data + removeJSONSepararator(); // remove the last separator + closeBlock(); + + fputs( TO_UTF8( m_JSONbuffer ), jobFile ); + + fclose( jobFile ); + return true; } + + +void GERBER_JOBFILE_WRITER::addJSONGeneralSpecs() +{ + addJSONObject( "\"GeneralSpecs\":\n" ); + openBlock(); + + addJSONObject( "\"ProjectId\":\n" ); + openBlock(); + + // Creates the ProjectId. Format is (from Gerber file format doc): + // ProjectId,,,*% + // is the name of the project, restricted to basic ASCII symbols only, + // and comma not accepted + // All illegal chars will be replaced by underscore + // is a 32 hexadecimal digits string which is an unique id of a project. + // This is a random 128-bit number expressed in 32 hexadecimal digits. + // See en.wikipedia.org/wiki/GUID for more information + // However Kicad does not handle such a project GUID, so it is built from the board name + // Rem: accepts only ASCII 7 code (only basic ASCII codes are allowed in gerber files). + wxFileName fn = m_pcb->GetFileName(); + wxString msg = fn.GetFullName(); + wxString guid; + + // Build a 32 digits GUID from the board name: + for( unsigned ii = 0; ii < msg.Len(); ii++ ) + { + int cc1 = int( msg[ii] ) & 0x0F; + int cc2 = ( int( msg[ii] ) >> 4) & 0x0F; + guid << wxString::Format( wxT( "%X%X" ), cc2, cc1 ); + + if( guid.Len() >= 32 ) + break; + } + + // guid has 32 digits, so add missing digits + int cnt = 32 - guid.Len(); + + if( cnt > 0 ) + guid.Append( '0', cnt ); + + // build the string: this is the board short filename (without ext) + // and all non ASCII chars are replaced by '_' + msg = fn.GetName(); + + // build the string. All non ASCII chars and comma are replaced by '_' + wxString rev = m_pcb->GetTitleBlock().GetRevision(); + + if( rev.IsEmpty() ) + rev = wxT( "rev?" ); + + addJSONObject( wxString::Format( "\"Name\": \"%s\",\n", msg.ToAscii() ) ); + addJSONObject( wxString::Format( "\"GUID\": \"%s\",\n", guid ) ); + addJSONObject( wxString::Format( "\"Revision\": \"%s\"\n", rev.ToAscii() ) ); + + closeBlockWithSep(); + + // output the bord size in mm: + EDA_RECT brect = m_pcb->GetBoardEdgesBoundingBox(); + addJSONObject( "\"Size\":\n" ); + openBlock(); + + addJSONObject( wxString::Format( "\"X\": %.3f,\n", brect.GetWidth()*m_conversionUnits ) ); + addJSONObject( wxString::Format( "\"Y\": %.3f\n", brect.GetHeight()*m_conversionUnits ) ); + closeBlockWithSep(); + + // Add some data to the JSON header, GeneralSpecs: + // number of copper layers + addJSONObject( wxString::Format( "\"LayerNumber\": %d,\n", m_pcb->GetCopperLayerCount() ) ); + + // Board thickness + addJSONObject( wxString::Format( "\"BoardThickness\": %.3f,\n", + m_pcb->GetDesignSettings().GetBoardThickness()*m_conversionUnits ) ); + +#if 0 // Not yet in use + /* The board type according to IPC-2221. There are six primary board types: + - Type 1 - Single-sided + - Type 2 - Double-sided + - Type 3 – Multilayer, TH components only + - Type 4 – Multilayer, with TH, blind and/or buried vias. + - Type 5 - Multilayer metal-core board, TH components only + - Type 6 - Multilayer metal-core + */ + addJSONObject( wxString::Format( "\"IPC-2221-Type\": \"%d\",\n", 4 ) ); + + /* Via protection: key words: + Ia Tented - Single-sided + Ib Tented - Double-sided + IIa Tented and Covered – Single-sided + IIb Tented and Covered – Double-sided + IIIa Plugged – Single-sided + IIIb…….Plugged – Double-sided + IVa…….Plugged and Covered – Single-sided + IVb…….Plugged and Covered – Double-sided + V Filled (fully plugged) + VI Filled and Covered + VIII Filled and Capped + None…...No protection + */ + addJSONObject( wxString::Format( "\"ViaProtection\": \"%s\",\n", "Ib" ) ); +#endif + removeJSONSepararator(); + closeBlockWithSep(); +} + + +void GERBER_JOBFILE_WRITER::addJSONFilesAttributes() +{ + // Add the Files Attributes section in JSON format to m_JSONbuffer + addJSONObject( "\"FilesAttributes\":\n" ); + openArrayBlock(); + + for( unsigned ii = 0; ii < m_params.m_GerberFileList.GetCount(); ii ++ ) + { + wxString& name = m_params.m_GerberFileList[ii]; + PCB_LAYER_ID layer = m_params.m_LayerId[ii]; + wxString gbr_layer_id; + bool skip_file = false; // true to skip files which should not be in job file + const char* polarity = "Positive"; + + if( layer <= B_Cu ) + { + gbr_layer_id = "Copper,L"; + + if( layer == B_Cu ) + gbr_layer_id << m_pcb->GetCopperLayerCount(); + else + gbr_layer_id << layer+1; + + gbr_layer_id << ","; + + if( layer == B_Cu ) + gbr_layer_id << "Bot"; + else if( layer == F_Cu ) + gbr_layer_id << "Top"; + else + gbr_layer_id << "Inr"; + } + + else + { + switch( layer ) + { + case B_Adhes: + gbr_layer_id = "Glue,Bot"; break; + case F_Adhes: + gbr_layer_id = "Glue,Top"; break; + + case B_Paste: + gbr_layer_id = "SolderPaste,Bot"; break; + case F_Paste: + gbr_layer_id = "SolderPaste,Top"; break; + + case B_SilkS: + gbr_layer_id = "Legend,Bot"; break; + case F_SilkS: + gbr_layer_id = "Legend,Top"; break; + + case B_Mask: + gbr_layer_id = "SolderMask,Bot"; polarity = "Negative"; break; + case F_Mask: + gbr_layer_id = "SolderMask,Top"; polarity = "Negative"; break; + + case Edge_Cuts: + gbr_layer_id = "Profile"; break; + + case B_Fab: + gbr_layer_id = "AssemblyDrawing,Bot"; break; + case F_Fab: + gbr_layer_id = "AssemblyDrawing,Top"; break; + + case Dwgs_User: + case Cmts_User: + case Eco1_User: + case Eco2_User: + case Margin: + case B_CrtYd: + case F_CrtYd: + skip_file = true; break; + + default: + skip_file = true; + m_reporter->Report( "Unexpected layer id in job file", + REPORTER::RPT_ERROR ); + break; + } + } + + if( !skip_file ) + { + // name can contain non ASCII7 chars. + // Only ASCII7 chars are accepted in gerber files. others must be converted to + // a gerber hexa sequence. + std::string strname = formatStringToGerber( name ); + + openBlock(); + addJSONObject( wxString::Format( "\"Path\": \"%s\",\n", strname.c_str() ) ); + addJSONObject( wxString::Format( "\"FileFunction\": \"%s\",\n", gbr_layer_id ) ), + addJSONObject( wxString::Format( "\"FilePolarity\": \"%s\"\n", polarity ) ); + closeBlockWithSep(); + } + } + // Close the file list: + removeJSONSepararator(); // remove the last separator + closeArrayBlockWithSep(); +} + + +void GERBER_JOBFILE_WRITER::addJSONDesignRules() +{ + // Add the Design Rules section in JSON format to m_JSONbuffer + // Job file support a few design rules: + const BOARD_DESIGN_SETTINGS& dsnSettings = m_pcb->GetDesignSettings(); + NETCLASS defaultNC = *dsnSettings.GetDefault(); + int minclearanceOuter = defaultNC.GetClearance(); + bool hasInnerLayers = m_pcb->GetCopperLayerCount() > 2; + + // Search a smaller clearance in other net classes, if any. + for( NETCLASSES::const_iterator it = dsnSettings.m_NetClasses.begin(); + it != dsnSettings.m_NetClasses.end(); + ++it ) + { + NETCLASS netclass = *it->second; + minclearanceOuter = std::min( minclearanceOuter, netclass.GetClearance() ); + } + + // job file knows different clearance types. + // Kicad knows only one clearance for pads and tracks + int minclearance_track2track = minclearanceOuter; + + // However, pads can have a specific clearance defined for a pad or a footprint, + // and min clearance can be dependent on layers. + // Search for a minimal pad clearance: + int minPadClearanceOuter = defaultNC.GetClearance(); + int minPadClearanceInner = defaultNC.GetClearance(); + + for( MODULE* module : m_pcb->Modules() ) + { + for( auto& pad : module->Pads() ) + { + if( ( pad->GetLayerSet() & LSET::InternalCuMask() ).any() ) + minPadClearanceInner = std::min( minPadClearanceInner, pad->GetClearance() ); + + if( ( pad->GetLayerSet() & LSET::ExternalCuMask() ).any() ) + minPadClearanceOuter = std::min( minPadClearanceOuter, pad->GetClearance() ); + } + } + + + addJSONObject( "\"DesignRules\":\n" ); + openArrayBlock(); + + openBlock(); + addJSONObject( "\"Layers\": \"Outer\",\n" ); + + addJSONObject( "\"Values\":\n" ); + + openBlock(); + addJSONObject( wxString::Format( "\"PadToPad\": %.3f,\n", minPadClearanceOuter*m_conversionUnits ) ); + addJSONObject( wxString::Format( "\"PadToTrack\": %.3f,\n", minPadClearanceOuter*m_conversionUnits ) ); + addJSONObject( wxString::Format( "\"TrackToTrack\": %.3f,\n", minclearance_track2track*m_conversionUnits ) ); + + // Until this is changed in Kicad, use the same value for internal tracks + int minclearanceInner = minclearanceOuter; + + // Output the minimal track width + int mintrackWidthOuter = INT_MAX; + int mintrackWidthInner = INT_MAX; + + for( TRACK* track : m_pcb->Tracks() ) + { + if( track->Type() == PCB_VIA_T ) + continue; + + if( track->GetLayer() == B_Cu || track->GetLayer() == F_Cu ) + mintrackWidthOuter = std::min( mintrackWidthOuter, track->GetWidth() ); + else + mintrackWidthInner = std::min( mintrackWidthInner, track->GetWidth() ); + } + + if( mintrackWidthOuter != INT_MAX ) + addJSONObject( wxString::Format( "\"MinLineWidth\": %.3f,\n", + mintrackWidthOuter*m_conversionUnits ) ); + + // Output the minimal zone to xx clearance + // Note: zones can have a zone clearance set to 0 + // if happens, the actual zone clearance is the clearance of its class + minclearanceOuter = INT_MAX; + minclearanceInner = INT_MAX; + + for( int ii = 0; ii < m_pcb->GetAreaCount(); ii++ ) + { + ZONE_CONTAINER* zone = m_pcb->GetArea( ii ); + + if( zone->GetIsKeepout() || !zone->IsOnCopperLayer() ) + continue; + + int zclerance = zone->GetClearance(); + + if( zone->GetLayer() == B_Cu || zone->GetLayer() == F_Cu ) + minclearanceOuter = std::min( minclearanceOuter, zclerance ); + else + minclearanceInner = std::min( minclearanceInner, zclerance ); + } + + if( minclearanceOuter != INT_MAX ) + addJSONObject( wxString::Format( "\"TrackToRegion\": %.3f,\n", + minclearanceOuter*m_conversionUnits ) ); + + if( minclearanceOuter != INT_MAX ) + addJSONObject( wxString::Format( "\"RegionToRegion\": %.3f,\n", + minclearanceOuter*m_conversionUnits ) ); + + removeJSONSepararator(); // remove the last separator + closeBlock(); + + + if( hasInnerLayers ) + { + closeBlockWithSep(); + + openBlock(); + addJSONObject( "\"Layers\": \"Inner\",\n" ); + + addJSONObject( "\"Values\":\n" ); + openBlock(); + + addJSONObject( wxString::Format( "\"PadToPad\": %.3f,\n", minPadClearanceInner*m_conversionUnits ) ); + addJSONObject( wxString::Format( "\"PadToTrack\": %.3f,\n", minPadClearanceInner*m_conversionUnits ) ); + addJSONObject( wxString::Format( "\"TrackToTrack\": %.3f,\n", minclearance_track2track*m_conversionUnits ) ); + + if( mintrackWidthInner != INT_MAX ) + addJSONObject( wxString::Format( "\"MinLineWidth\": %.3f,\n", mintrackWidthInner*m_conversionUnits ) ); + + if( minclearanceInner != INT_MAX ) + addJSONObject( wxString::Format( "\"TrackToRegion\": %.3f,\n", minclearanceInner*m_conversionUnits ) ); + + if( minclearanceInner != INT_MAX ) + addJSONObject( wxString::Format( "\"RegionToRegion\": %.3f,\n", minclearanceInner*m_conversionUnits ) ); + + removeJSONSepararator(); // remove the last separator + closeBlock(); + } + + // Close DesignRules + closeBlock(); + closeArrayBlockWithSep(); +} + + +void GERBER_JOBFILE_WRITER::addJSONMaterialStackup() +{ + // Add the Material Stackup section in JSON format to m_JSONbuffer + addJSONObject( "\"MaterialStackup\":\n" ); + openArrayBlock(); + + // Build the candidates: only layers on a board are candidates: + LSET maskLayer; + + for( unsigned ii = 0; ii < m_params.m_GerberFileList.GetCount(); ii ++ ) + { + PCB_LAYER_ID layer = m_params.m_LayerId[ii]; + + if( layer <= B_Cu ) + maskLayer.set( layer ); + else + { + switch( layer ) + { + case B_Paste: + case F_Paste: + case B_SilkS: + case F_SilkS: + case B_Mask: + case F_Mask: + maskLayer.set( layer ); + break; + + case Edge_Cuts: + case B_Adhes: + case F_Adhes: + case B_Fab: + case F_Fab: + case Dwgs_User: + case Cmts_User: + case Eco1_User: + case Eco2_User: + case Margin: + case B_CrtYd: + case F_CrtYd: + break; + + default: + m_reporter->Report( + wxString::Format( "Unexpected layer id %d in job file", layer ), + REPORTER::RPT_ERROR ); + break; + } + } + } + + // build a candidate list (in reverse order: bottom to top): + LSEQ list = maskLayer.SeqStackupBottom2Top(); + // Generate the list (top to bottom): + for( int ii = list.size()-1; ii >= 0; --ii ) + { + PCB_LAYER_ID layer = list[ii]; + wxString layer_type; + wxString color; + wxString dielectric; + double thickness = 0.0; // layer thickness in mm + + if( layer <= B_Cu ) + { + layer_type = "Copper"; + //thickness = 0.035; + } + else + { + switch( layer ) + { + case B_Paste: + case F_Paste: + layer_type = "SolderPaste"; + break; + + case B_SilkS: + case F_SilkS: + //color = "White"; + layer_type = "Legend"; + break; + + case B_Mask: + case F_Mask: + //color = "Green"; + //thickness = 0.025; + layer_type = "SolderMask"; + break; + + default: + break; + } + } + + openBlock(); + addJSONObject( wxString::Format( "\"Type\": \"%s\",\n", layer_type ) ); + + if( !color.IsEmpty() ) + addJSONObject( wxString::Format( "\"Color\": \"%s\",\n", color ) ); + + if( thickness > 0.0 ) + addJSONObject( wxString::Format( "\"Thickness\": %f,\n", thickness ) ); + + std::string strname = formatStringToGerber( m_pcb->GetLayerName( layer ) ); + addJSONObject( wxString::Format( "\"S_Notes\": \"Layer %s\",\n", strname.c_str() ) ); + removeJSONSepararator(); + closeBlockWithSep(); + + if( layer < B_Cu ) // Add dielectric between copper layers + { + dielectric = "FR4"; // Temporary + + openBlock(); + addJSONObject( wxString::Format( "\"Type\": \"%s\",\n", "Dielectric" ) ); + + if( thickness > 0.0 ) + addJSONObject( wxString::Format( "\"Thickness\": %f,\n", color ) ); + + if( !dielectric.IsEmpty() ) + addJSONObject( wxString::Format( "\"Material\": \"%s\",\n", dielectric ) ); + + addJSONObject( wxString::Format( "\"S_Notes\": \"Layers L%d/L%d\",\n", + layer+1, layer+2 ) ); + removeJSONSepararator(); + closeBlockWithSep(); + } + } + + removeJSONSepararator(); + closeArrayBlockWithSep(); +} diff --git a/pcbnew/exporters/gerber_jobfile_writer.h b/pcbnew/exporters/gerber_jobfile_writer.h index f5f24fa9fb..489220398e 100644 --- a/pcbnew/exporters/gerber_jobfile_writer.h +++ b/pcbnew/exporters/gerber_jobfile_writer.h @@ -1,13 +1,13 @@ /** * @file gerber_jobfile_writer.h - * @brief Classes used in drill files, map files and report files generation. + * @brief Classes used to generate a Gerber job file in JSON */ /* * This program source code file is part of KiCad, a free EDA CAD application. * - * Copyright (C) 1992-2017 Jean_Pierre Charras - * Copyright (C) 1992-2017 KiCad Developers, see change_log.txt for contributors. + * Copyright (C) 1992-2018 Jean_Pierre Charras + * Copyright (C) 1992-2018 KiCad Developers, see AUTHORS.txt for contributors. * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License @@ -87,12 +87,26 @@ public: } /** - * Creates an Excellon drill file + * Creates a Gerber job file + * @param aFullFilename = the full filename + * @return true, or false if the file cannot be created + */ + bool CreateJobFile( const wxString& aFullFilename ); + + /** + * Creates a Gerber job file in old gbr format + * @param aFullFilename = the full filename + * @return true, or false if the file cannot be created + */ + bool CreateGbrJobFile( const wxString& aFullFilename ); + + /** + * Creates an Gerber job file in JSON format * @param aFullFilename = the full filename * @param aParams = true for a NPTH file, false for a PTH file * @return true, or false if the file cannot be created */ - bool CreateJobFile( const wxString& aFullFilename ); + bool CreateJSONJobFile( const wxString& aFullFilename ); private: /** @return SIDE_NONE if no silk screen layer is in list @@ -114,12 +128,94 @@ private: */ const char* sideKeyValue( enum ONSIDE aValue ); + /** + * Add the job file header in JSON format to m_JSONbuffer + */ + void addJSONHeader(); + + /** + * Add the General Specs in JSON format to m_JSONbuffer + */ + void addJSONGeneralSpecs(); + + /** + * Add the Files Attributes section in JSON format to m_JSONbuffer + */ + void addJSONFilesAttributes(); + + /** + * Add the Material Stackup section in JSON format to m_JSONbuffer + * This is the ordered list of stackup layers (mask, paste, silk, copper, dielectric) + * used to make the physical board. Therefore not all layers are listed here + */ + void addJSONMaterialStackup(); + + /** + * Add the Design Rules section in JSON format to m_JSONbuffer + */ + void addJSONDesignRules(); + + /** + * Remove the comma if it is the last char in m_JSONbuffer, + * or the previous char if the last char is a \n + */ + void removeJSONSepararator(); + + /** + * add m_indent spaces in m_JSONbuffer + */ + void addIndent() { m_JSONbuffer.Append( ' ', m_indent ); } + + /** + * open a JSON block: add '{' and increment indentation + */ + void openBlock() { addIndent(); m_JSONbuffer << "{\n"; m_indent += 2; } + + /** + * open a JSON array block: add '[' and increment indentation + */ + void openArrayBlock() { addIndent(); m_JSONbuffer << "[\n"; m_indent += 2; } + + /** + * close a JSON block: decrement indentation and add '}' + */ + void closeBlock() { m_indent -= 2; addIndent(); m_JSONbuffer << "}\n"; } + + /** + * close a JSON block: decrement indentation and add '}' and ',' + */ + void closeBlockWithSep() { m_indent -= 2; addIndent(); m_JSONbuffer << "},\n"; } + + /** + * close a JSON array block: decrement indentation and add ']' + */ + void closeArrayBlock() { m_indent -= 2; addIndent(); m_JSONbuffer << "]\n"; } + + /** + * close a JSON array block: decrement indentation and add ']' and ',' + */ + void closeArrayBlockWithSep() { m_indent -= 2; addIndent(); m_JSONbuffer << "],\n"; } + + /** + * Add aParam to m_JSONbuffer, with suitable indentation + */ + void addJSONObject( const wxString& aParam ) + { + addIndent(); m_JSONbuffer << aParam; + } + void addJSONObject( const char* aParam ) + { + addIndent(); m_JSONbuffer << aParam; + } private: BOARD* m_pcb; // The board REPORTER* m_reporter; // a reporter for messages (can be null) JOBFILE_PARAMS m_params; // the list of various prms and data to write in a job file double m_conversionUnits; // scaling factor to convert brd units to gerber units (mm) + bool m_useJSONformat; // temporary option + wxString m_JSONbuffer; // a buffer to build the JSON data + int m_indent; // helper for JSON format: the current indentation value }; #endif // #ifndef GERBER_JOBFILE_WRITER_H