/* * This program source code file is part of KICAD, a free EDA CAD application. * * Copyright (C) 2016 CERN * Copyright (C) 1992-2023 KiCad Developers, see AUTHORS.txt for contributors. * @author Janito V. Ferreira Filho * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License * as published by the Free Software Foundation; either version 2 * of the License, or (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, you may find one here: * http://www.gnu.org/licenses/old-licenses/gpl-2.0.html * or you may search the http://www.gnu.org website for the version 2 license, * or you may write to the Free Software Foundation, Inc., * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA */ #include "svg_import_plugin.h" #include #include #include #include #include #include "graphics_importer.h" static const int SVG_DPI = 96; static VECTOR2D calculateBezierBoundingBoxExtremity( const float* aCurvePoints, std::function< const float&( const float&, const float& ) > comparator ); static float calculateBezierSegmentationThreshold( const float* aCurvePoints ); static void segmentBezierCurve( const VECTOR2D& aStart, const VECTOR2D& aEnd, float aOffset, float aStep, const float* aCurvePoints, float aSegmentationThreshold, std::vector< VECTOR2D >& aGeneratedPoints ); static void createNewBezierCurveSegments( const VECTOR2D& aStart, const VECTOR2D& aMiddle, const VECTOR2D& aEnd, float aOffset, float aStep, const float* aCurvePoints, float aSegmentationThreshold, std::vector< VECTOR2D >& aGeneratedPoints ); static VECTOR2D getBezierPoint( const float* aCurvePoints, float aStep ); static VECTOR2D getPoint( const float* aPointCoordinates ); static VECTOR2D getPointInLine( const VECTOR2D& aLineStart, const VECTOR2D& aLineEnd, float aDistance ); static float distanceFromPointToLine( const VECTOR2D& aPoint, const VECTOR2D& aLineStart, const VECTOR2D& aLineEnd ); bool SVG_IMPORT_PLUGIN::Load( const wxString& aFileName ) { wxCHECK( m_importer, false ); LOCALE_IO toggle; // switch on/off the locale "C" notation // 1- wxFopen takes care of unicode filenames across platforms // 2 - nanosvg (exactly nsvgParseFromFile) expects a binary file (exactly the CRLF eof must // not be replaced by LF and changes the byte count) in one validity test, // so open it in binary mode. FILE* fp = wxFopen( aFileName, wxT( "rb" ) ); if( fp == nullptr ) return false; // nsvgParseFromFile will close the file after reading m_parsedImage = nsvgParseFromFile( fp, "mm", SVG_DPI ); wxCHECK( m_parsedImage, false ); return true; } bool SVG_IMPORT_PLUGIN::LoadFromMemory( const wxMemoryBuffer& aMemBuffer ) { wxCHECK( m_importer, false ); LOCALE_IO toggle; // switch on/off the locale "C" notation std::string str( reinterpret_cast( aMemBuffer.GetData() ), aMemBuffer.GetDataLen() ); wxCHECK( str.data()[aMemBuffer.GetDataLen()] == '\0', false ); // nsvgParse will modify the string data m_parsedImage = nsvgParse( str.data(), "mm", SVG_DPI ); wxCHECK( m_parsedImage, false ); return true; } bool SVG_IMPORT_PLUGIN::Import() { auto alpha = []( unsigned int color ) { return color >> 24; }; for( NSVGshape* shape = m_parsedImage->shapes; shape != nullptr; shape = shape->next ) { if( !( shape->flags & NSVG_FLAGS_VISIBLE ) ) continue; if( shape->stroke.type == NSVG_PAINT_NONE && shape->fill.type == NSVG_PAINT_NONE ) continue; double lineWidth = shape->stroke.type != NSVG_PAINT_NONE ? shape->strokeWidth : -1; bool filled = shape->fill.type != NSVG_PAINT_NONE && alpha( shape->fill.color ) > 0; COLOR4D fillColor = COLOR4D::UNSPECIFIED; if( shape->fill.type == NSVG_PAINT_COLOR ) { unsigned int icolor = shape->fill.color; fillColor.r = std::clamp( ( icolor >> 0 ) & 0xFF, 0u, 255u ) / 255.0; fillColor.g = std::clamp( ( icolor >> 8 ) & 0xFF, 0u, 255u ) / 255.0; fillColor.b = std::clamp( ( icolor >> 16 ) & 0xFF, 0u, 255u ) / 255.0; fillColor.a = std::clamp( ( icolor >> 24 ) & 0xFF, 0u, 255u ) / 255.0; // nanosvg probably didn't read it properly, use default if( fillColor == COLOR4D::BLACK ) fillColor = COLOR4D::UNSPECIFIED; } COLOR4D strokeColor = COLOR4D::UNSPECIFIED; if( shape->stroke.type == NSVG_PAINT_COLOR ) { unsigned int icolor = shape->stroke.color; strokeColor.r = std::clamp( ( icolor >> 0 ) & 0xFF, 0u, 255u ) / 255.0; strokeColor.g = std::clamp( ( icolor >> 8 ) & 0xFF, 0u, 255u ) / 255.0; strokeColor.b = std::clamp( ( icolor >> 16 ) & 0xFF, 0u, 255u ) / 255.0; strokeColor.a = std::clamp( ( icolor >> 24 ) & 0xFF, 0u, 255u ) / 255.0; // nanosvg probably didn't read it properly, use default if( strokeColor == COLOR4D::BLACK ) strokeColor = COLOR4D::UNSPECIFIED; } LINE_STYLE dashType = LINE_STYLE::SOLID; if( shape->strokeDashCount > 0 ) { float* dashArray = shape->strokeDashArray; int dotCount = 0; int dashCount = 0; const float dashThreshold = shape->strokeWidth * 1.9f; for( int i = 0; i < shape->strokeDashCount; i += 2 ) { if( dashArray[i] < dashThreshold ) dotCount++; else dashCount++; } if( dotCount > 0 && dashCount == 0 ) dashType = LINE_STYLE::DOT; else if( dotCount == 0 && dashCount > 0 ) dashType = LINE_STYLE::DASH; else if( dotCount == 1 && dashCount == 1 ) dashType = LINE_STYLE::DASHDOT; else if( dotCount == 2 && dashCount == 1 ) dashType = LINE_STYLE::DASHDOTDOT; } IMPORTED_STROKE stroke( lineWidth, dashType, strokeColor ); GRAPHICS_IMPORTER::POLY_FILL_RULE rule = GRAPHICS_IMPORTER::PF_NONZERO; switch( shape->fillRule ) { case NSVG_FILLRULE_NONZERO: rule = GRAPHICS_IMPORTER::PF_NONZERO; break; case NSVG_FILLRULE_EVENODD: rule = GRAPHICS_IMPORTER::PF_EVEN_ODD; break; default: break; } m_internalImporter.NewShape( rule ); for( NSVGpath* path = shape->paths; path != nullptr; path = path->next ) { bool closed = path->closed || filled || rule == GRAPHICS_IMPORTER::PF_EVEN_ODD; DrawPath( path->pts, path->npts, closed, stroke, filled, fillColor ); } } m_internalImporter.PostprocessNestedPolygons(); wxCHECK( m_importer, false ); m_internalImporter.ImportTo( *m_importer ); return true; } double SVG_IMPORT_PLUGIN::GetImageHeight() const { if( !m_parsedImage ) { wxASSERT_MSG( false, wxT( "Image must have been loaded before checking height" ) ); return 0.0; } return m_parsedImage->height / SVG_DPI * inches2mm; } double SVG_IMPORT_PLUGIN::GetImageWidth() const { if( !m_parsedImage ) { wxASSERT_MSG( false, wxT( "Image must have been loaded before checking width" ) ); return 0.0; } return m_parsedImage->width / SVG_DPI * inches2mm; } BOX2D SVG_IMPORT_PLUGIN::GetImageBBox() const { BOX2D bbox; if( !m_parsedImage || !m_parsedImage->shapes ) { wxASSERT_MSG( false, wxT( "Image must have been loaded before getting bbox" ) ); return bbox; } for( NSVGshape* shape = m_parsedImage->shapes; shape != nullptr; shape = shape->next ) { BOX2D shapeBbox; float( &bounds )[4] = shape->bounds; shapeBbox.SetOrigin( bounds[0], bounds[1] ); shapeBbox.SetEnd( bounds[2], bounds[3] ); bbox.Merge( shapeBbox ); } return bbox; } void SVG_IMPORT_PLUGIN::DrawPath( const float* aPoints, int aNumPoints, bool aClosedPath, const IMPORTED_STROKE& aStroke, bool aFilled, const COLOR4D& aFillColor ) { std::vector collectedPathPoints; if( aNumPoints > 0 ) DrawCubicBezierPath( aPoints, aNumPoints, collectedPathPoints ); if( aClosedPath && collectedPathPoints.size() > 2 ) DrawPolygon( collectedPathPoints, aStroke, aFilled, aFillColor ); else DrawLineSegments( collectedPathPoints, aStroke ); } void SVG_IMPORT_PLUGIN::DrawCubicBezierPath( const float* aPoints, int aNumPoints, std::vector& aGeneratedPoints ) { const int pointsPerSegment = 4; const int curveSpecificPointsPerSegment = 3; const int curveSpecificCoordinatesPerSegment = 2 * curveSpecificPointsPerSegment; const float* currentPoints = aPoints; int remainingPoints = aNumPoints; while( remainingPoints >= pointsPerSegment ) { DrawCubicBezierCurve( currentPoints, aGeneratedPoints ); currentPoints += curveSpecificCoordinatesPerSegment; remainingPoints -= curveSpecificPointsPerSegment; } } void SVG_IMPORT_PLUGIN::DrawCubicBezierCurve( const float* aPoints, std::vector& aGeneratedPoints ) { auto start = getBezierPoint( aPoints, 0.0f ); auto end = getBezierPoint( aPoints, 1.0f ); auto segmentationThreshold = calculateBezierSegmentationThreshold( aPoints ); aGeneratedPoints.push_back( start ); segmentBezierCurve( start, end, 0.0f, 0.5f, aPoints, segmentationThreshold, aGeneratedPoints ); aGeneratedPoints.push_back( end ); } void SVG_IMPORT_PLUGIN::DrawPolygon( const std::vector& aPoints, const IMPORTED_STROKE& aStroke, bool aFilled, const COLOR4D& aFillColor ) { m_internalImporter.AddPolygon( aPoints, aStroke, aFilled, aFillColor ); } void SVG_IMPORT_PLUGIN::DrawLineSegments( const std::vector& aPoints, const IMPORTED_STROKE& aStroke ) { unsigned int numLineStartPoints = aPoints.size() - 1; for( unsigned int pointIndex = 0; pointIndex < numLineStartPoints; ++pointIndex ) m_internalImporter.AddLine( aPoints[pointIndex], aPoints[pointIndex + 1], aStroke ); } static VECTOR2D getPoint( const float* aPointCoordinates ) { return VECTOR2D( aPointCoordinates[0], aPointCoordinates[1] ); } static VECTOR2D getBezierPoint( const float* aPoints, float aStep ) { const int coordinatesPerPoint = 2; auto firstCubicPoint = getPoint( aPoints ); auto secondCubicPoint = getPoint( aPoints + 1 * coordinatesPerPoint ); auto thirdCubicPoint = getPoint( aPoints + 2 * coordinatesPerPoint ); auto fourthCubicPoint = getPoint( aPoints + 3 * coordinatesPerPoint ); auto firstQuadraticPoint = getPointInLine( firstCubicPoint, secondCubicPoint, aStep ); auto secondQuadraticPoint = getPointInLine( secondCubicPoint, thirdCubicPoint, aStep ); auto thirdQuadraticPoint = getPointInLine( thirdCubicPoint, fourthCubicPoint, aStep ); auto firstLinearPoint = getPointInLine( firstQuadraticPoint, secondQuadraticPoint, aStep ); auto secondLinearPoint = getPointInLine( secondQuadraticPoint, thirdQuadraticPoint, aStep ); return getPointInLine( firstLinearPoint, secondLinearPoint, aStep ); } static VECTOR2D getPointInLine( const VECTOR2D& aLineStart, const VECTOR2D& aLineEnd, float aDistance ) { return aLineStart + ( aLineEnd - aLineStart ) * aDistance; } static float calculateBezierSegmentationThreshold( const float* aCurvePoints ) { using comparatorFunction = const float&(*)( const float&, const float& ); auto minimumComparator = static_cast< comparatorFunction >( &std::min ); auto maximumComparator = static_cast< comparatorFunction >( &std::max ); VECTOR2D minimum = calculateBezierBoundingBoxExtremity( aCurvePoints, minimumComparator ); VECTOR2D maximum = calculateBezierBoundingBoxExtremity( aCurvePoints, maximumComparator ); VECTOR2D boundingBoxDimensions = maximum - minimum; return 0.001 * std::max( boundingBoxDimensions.x, boundingBoxDimensions.y ); } static VECTOR2D calculateBezierBoundingBoxExtremity( const float* aCurvePoints, std::function< const float&( const float&, const float& ) > comparator ) { float x = aCurvePoints[0]; float y = aCurvePoints[1]; for( int pointIndex = 1; pointIndex < 3; ++pointIndex ) { x = comparator( x, aCurvePoints[ 2 * pointIndex ] ); y = comparator( y, aCurvePoints[ 2 * pointIndex + 1 ] ); } return VECTOR2D( x, y ); } static void segmentBezierCurve( const VECTOR2D& aStart, const VECTOR2D& aEnd, float aOffset, float aStep, const float* aCurvePoints, float aSegmentationThreshold, std::vector< VECTOR2D >& aGeneratedPoints ) { VECTOR2D middle = getBezierPoint( aCurvePoints, aOffset + aStep ); float distanceToPreviousSegment = distanceFromPointToLine( middle, aStart, aEnd ); if( distanceToPreviousSegment > aSegmentationThreshold ) { createNewBezierCurveSegments( aStart, middle, aEnd, aOffset, aStep, aCurvePoints, aSegmentationThreshold, aGeneratedPoints ); } } static void createNewBezierCurveSegments( const VECTOR2D& aStart, const VECTOR2D& aMiddle, const VECTOR2D& aEnd, float aOffset, float aStep, const float* aCurvePoints, float aSegmentationThreshold, std::vector< VECTOR2D >& aGeneratedPoints ) { float newStep = aStep / 2.f; float offsetAfterMiddle = aOffset + aStep; segmentBezierCurve( aStart, aMiddle, aOffset, newStep, aCurvePoints, aSegmentationThreshold, aGeneratedPoints ); aGeneratedPoints.push_back( aMiddle ); segmentBezierCurve( aMiddle, aEnd, offsetAfterMiddle, newStep, aCurvePoints, aSegmentationThreshold, aGeneratedPoints ); } static float distanceFromPointToLine( const VECTOR2D& aPoint, const VECTOR2D& aLineStart, const VECTOR2D& aLineEnd ) { auto lineDirection = aLineEnd - aLineStart; auto lineNormal = lineDirection.Perpendicular().Resize( 1.f ); auto lineStartToPoint = aPoint - aLineStart; auto distance = lineNormal.Dot( lineStartToPoint ); return fabs( distance ); }