Add symbol editor clipboard handling

Mirrors handling for schematic editor.  Outputs SVG+PNG+Kicad text to
the clipboard, applications choose which to paste
This commit is contained in:
Seth Hillbrand
2025-12-31 14:22:31 -08:00
parent 743adf20f3
commit a4dd0174b9
7 changed files with 2454 additions and 1 deletions
+23
View File
@@ -343,6 +343,28 @@ void SVG_PLOTTER::EndBlock( void* aData )
}
void SVG_PLOTTER::StartLayer( const wxString& aLayerName )
{
// Close any pending graphics context group
if( m_graphics_changed )
setSVGPlotStyle( GetCurrentLineWidth() );
// Start a new named layer group with inkscape-compatible layer attributes
fmt::print( m_outputFile, "<g id=\"{}\" inkscape:label=\"{}\" inkscape:groupmode=\"layer\">\n",
TO_UTF8( aLayerName ), TO_UTF8( aLayerName ) );
}
void SVG_PLOTTER::EndLayer()
{
// Close any pending graphics context group first
fmt::print( m_outputFile, "</g>\n" );
// Then close the layer group
fmt::print( m_outputFile, "</g>\n" );
m_graphics_changed = true; // Force new graphics context on next draw
}
void SVG_PLOTTER::emitSetRGBColor( double r, double g, double b, double a )
{
uint32_t red = (uint32_t) ( 255.0 * r );
@@ -764,6 +786,7 @@ bool SVG_PLOTTER::StartPlot( const wxString& aPageNumber )
" xmlns:svg=\"http://www.w3.org/2000/svg\"\n"
" xmlns=\"http://www.w3.org/2000/svg\"\n"
" xmlns:xlink=\"http://www.w3.org/1999/xlink\"\n"
" xmlns:inkscape=\"http://www.inkscape.org/namespaces/inkscape\"\n"
" version=\"1.1\"\n";
// Write header.
+361 -1
View File
@@ -44,6 +44,7 @@
#include <dialogs/dialog_lib_edit_pin_table.h>
#include <dialogs/dialog_update_symbol_fields.h>
#include <view/view_controls.h>
#include <view/view.h>
#include <richio.h>
#include <sch_io/kicad_sexpr/sch_io_kicad_sexpr.h>
#include <sch_textbox.h>
@@ -52,6 +53,297 @@
#include <math/util.h> // for KiROUND
#include <io/kicad/kicad_io_utils.h>
#include <trace_helpers.h>
#include <plotters/plotters_pslike.h>
#include <sch_painter.h>
#include <sch_plotter.h>
#include <locale_io.h>
#include <gal/gal_print.h>
#include <gal/graphics_abstraction_layer.h>
#include <zoom_defines.h>
#include <wx/ffile.h>
#include <wx/mstream.h>
namespace
{
constexpr double clipboardPpi = 96.0;
constexpr int clipboardMaxBitmapSize = 4096;
constexpr double clipboardBboxInflation = 0.02;
void appendMimeData( std::vector<CLIPBOARD_MIME_DATA>& aMimeData, const wxString& aMimeType,
const wxMemoryBuffer& aBuffer )
{
if( aBuffer.GetDataLen() == 0 )
return;
CLIPBOARD_MIME_DATA entry;
entry.m_mimeType = aMimeType;
entry.m_data = aBuffer;
aMimeData.push_back( entry );
}
bool loadFileToBuffer( const wxString& aFileName, wxMemoryBuffer& aBuffer )
{
wxFFile file( aFileName, wxS( "rb" ) );
if( !file.IsOpened() )
return false;
wxFileOffset size = file.Length();
if( size <= 0 )
return false;
void* data = aBuffer.GetWriteBuf( size );
if( file.Read( data, size ) != static_cast<size_t>( size ) )
{
aBuffer.UngetWriteBuf( 0 );
return false;
}
aBuffer.UngetWriteBuf( size );
return true;
}
bool plotSymbolToSvg( SYMBOL_EDIT_FRAME* aFrame, LIB_SYMBOL* aSymbol, const BOX2I& aBBox,
int aUnit, int aBodyStyle, wxMemoryBuffer& aBuffer )
{
if( !aSymbol )
return false;
SCH_RENDER_SETTINGS renderSettings;
renderSettings.LoadColors( aFrame->GetColorSettings() );
renderSettings.SetDefaultPenWidth( aFrame->GetRenderSettings()->GetDefaultPenWidth() );
std::unique_ptr<SVG_PLOTTER> plotter = std::make_unique<SVG_PLOTTER>();
plotter->SetRenderSettings( &renderSettings );
PAGE_INFO pageInfo = aFrame->GetScreen()->GetPageSettings();
pageInfo.SetWidthMils( schIUScale.IUToMils( aBBox.GetWidth() ) );
pageInfo.SetHeightMils( schIUScale.IUToMils( aBBox.GetHeight() ) );
plotter->SetPageSettings( pageInfo );
plotter->SetColorMode( true );
VECTOR2I plot_offset = aBBox.GetOrigin();
plotter->SetViewport( plot_offset, schIUScale.IU_PER_MILS / 10, 1.0, false );
plotter->SetCreator( wxT( "Eeschema-SVG" ) );
wxFileName tempFile( wxFileName::CreateTempFileName( wxS( "kicad_symbol_svg" ) ) );
if( !plotter->OpenFile( tempFile.GetFullPath() ) )
{
wxRemoveFile( tempFile.GetFullPath() );
return false;
}
LOCALE_IO toggle;
SCH_PLOT_OPTS plotOpts;
plotter->StartPlot( wxT( "1" ) );
constexpr bool background = true;
aSymbol->Plot( plotter.get(), background, plotOpts, aUnit, aBodyStyle, VECTOR2I( 0, 0 ), false );
aSymbol->Plot( plotter.get(), !background, plotOpts, aUnit, aBodyStyle, VECTOR2I( 0, 0 ), false );
aSymbol->PlotFields( plotter.get(), !background, plotOpts, aUnit, aBodyStyle, VECTOR2I( 0, 0 ), false );
plotter->EndPlot();
plotter.reset();
bool ok = loadFileToBuffer( tempFile.GetFullPath(), aBuffer );
wxRemoveFile( tempFile.GetFullPath() );
return ok;
}
wxImage renderSymbolToBitmap( SYMBOL_EDIT_FRAME* aFrame, LIB_SYMBOL* aSymbol, const BOX2I& aBBox,
int aUnit, int aBodyStyle, int aWidth, int aHeight,
double aViewScale, const wxColour& aBgColor )
{
if( !aSymbol )
return wxImage();
wxBitmap bitmap( aWidth, aHeight, 24 );
wxMemoryDC dc;
dc.SelectObject( bitmap );
dc.SetBackground( wxBrush( aBgColor ) );
dc.Clear();
KIGFX::GAL_DISPLAY_OPTIONS options;
options.antialiasing_mode = KIGFX::GAL_ANTIALIASING_MODE::AA_HIGHQUALITY;
std::unique_ptr<KIGFX::GAL_PRINT> galPrint = KIGFX::GAL_PRINT::Create( options, &dc );
if( !galPrint )
return wxImage();
KIGFX::GAL* gal = galPrint->GetGAL();
KIGFX::PRINT_CONTEXT* printCtx = galPrint->GetPrintCtx();
std::unique_ptr<KIGFX::SCH_PAINTER> painter = std::make_unique<KIGFX::SCH_PAINTER>( gal );
std::unique_ptr<KIGFX::VIEW> view = std::make_unique<KIGFX::VIEW>();
// For symbol editor, we don't have a full schematic context
// but SCH_PAINTER can still work for rendering individual items
view->SetGAL( gal );
view->SetPainter( painter.get() );
view->SetScaleLimits( ZOOM_MAX_LIMIT_EESCHEMA, ZOOM_MIN_LIMIT_EESCHEMA );
view->SetScale( 1.0 );
gal->SetWorldUnitLength( SCH_WORLD_UNIT );
// Clone items and add to view
std::vector<std::unique_ptr<SCH_ITEM>> clonedItems;
for( SCH_ITEM& item : aSymbol->GetDrawItems() )
{
if( aUnit && item.GetUnit() && item.GetUnit() != aUnit )
continue;
if( aBodyStyle && item.GetBodyStyle() && item.GetBodyStyle() != aBodyStyle )
continue;
SCH_ITEM* clone = static_cast<SCH_ITEM*>( item.Clone() );
clonedItems.emplace_back( clone );
view->Add( clone );
}
SCH_RENDER_SETTINGS* dstSettings = painter->GetSettings();
dstSettings->LoadColors( aFrame->GetColorSettings() );
dstSettings->SetDefaultPenWidth( aFrame->GetRenderSettings()->GetDefaultPenWidth() );
dstSettings->SetIsPrinting( true );
COLOR4D bgColor4D( aBgColor.Red() / 255.0, aBgColor.Green() / 255.0,
aBgColor.Blue() / 255.0, 1.0 );
dstSettings->SetBackgroundColor( bgColor4D );
for( int i = 0; i < KIGFX::VIEW::VIEW_MAX_LAYERS; ++i )
{
view->SetLayerVisible( i, true );
view->SetLayerTarget( i, KIGFX::TARGET_NONCACHED );
}
view->SetLayerVisible( LAYER_DRAWINGSHEET, false );
double ppi = clipboardPpi;
double inch2Iu = 1000.0 * schIUScale.IU_PER_MILS;
VECTOR2D pageSizeIn( (double) aWidth / ppi, (double) aHeight / ppi );
galPrint->SetSheetSize( pageSizeIn );
galPrint->SetNativePaperSize( pageSizeIn, printCtx->HasNativeLandscapeRotation() );
double zoomFactor = aViewScale * inch2Iu / ppi;
gal->SetLookAtPoint( aBBox.Centre() );
gal->SetZoomFactor( zoomFactor );
gal->SetClearColor( bgColor4D );
gal->ClearScreen();
view->UseDrawPriority( true );
{
KIGFX::GAL_DRAWING_CONTEXT ctx( gal );
view->Redraw();
}
dc.SelectObject( wxNullBitmap );
return bitmap.ConvertToImage();
}
bool plotSymbolToPng( SYMBOL_EDIT_FRAME* aFrame, LIB_SYMBOL* aSymbol, const BOX2I& aBBox,
int aUnit, int aBodyStyle, wxMemoryBuffer& aBuffer )
{
if( !aSymbol )
return false;
VECTOR2I size = aBBox.GetSize();
if( size.x <= 0 || size.y <= 0 )
return false;
// Use the current view scale to match what the user sees on screen
double viewScale = aFrame->GetCanvas()->GetView()->GetScale();
int bitmapWidth = KiROUND( size.x * viewScale );
int bitmapHeight = KiROUND( size.y * viewScale );
// Clamp to maximum size while preserving aspect ratio
if( bitmapWidth > clipboardMaxBitmapSize || bitmapHeight > clipboardMaxBitmapSize )
{
double scaleDown = (double) clipboardMaxBitmapSize / std::max( bitmapWidth, bitmapHeight );
bitmapWidth = KiROUND( bitmapWidth * scaleDown );
bitmapHeight = KiROUND( bitmapHeight * scaleDown );
viewScale *= scaleDown;
}
if( bitmapWidth <= 0 || bitmapHeight <= 0 )
return false;
// Render twice with different backgrounds for alpha computation
wxImage imageOnWhite = renderSymbolToBitmap( aFrame, aSymbol, aBBox, aUnit, aBodyStyle,
bitmapWidth, bitmapHeight, viewScale, *wxWHITE );
wxImage imageOnBlack = renderSymbolToBitmap( aFrame, aSymbol, aBBox, aUnit, aBodyStyle,
bitmapWidth, bitmapHeight, viewScale, *wxBLACK );
if( !imageOnWhite.IsOk() || !imageOnBlack.IsOk() )
return false;
// Create output image with alpha channel
wxImage result( bitmapWidth, bitmapHeight );
result.InitAlpha();
unsigned char* rgbWhite = imageOnWhite.GetData();
unsigned char* rgbBlack = imageOnBlack.GetData();
unsigned char* rgbResult = result.GetData();
unsigned char* alphaResult = result.GetAlpha();
int pixelCount = bitmapWidth * bitmapHeight;
for( int i = 0; i < pixelCount; ++i )
{
int idx = i * 3;
int rW = rgbWhite[idx], gW = rgbWhite[idx + 1], bW = rgbWhite[idx + 2];
int rB = rgbBlack[idx], gB = rgbBlack[idx + 1], bB = rgbBlack[idx + 2];
// Alpha computation: α = 1 - (white - black) / 255
int diffR = rW - rB;
int diffG = gW - gB;
int diffB = bW - bB;
int avgDiff = ( diffR + diffG + diffB ) / 3;
int alpha = 255 - avgDiff;
alpha = std::max( 0, std::min( 255, alpha ) );
alphaResult[i] = static_cast<unsigned char>( alpha );
if( alpha > 0 )
{
rgbResult[idx] = static_cast<unsigned char>( std::min( 255, rB * 255 / alpha ) );
rgbResult[idx + 1] = static_cast<unsigned char>( std::min( 255, gB * 255 / alpha ) );
rgbResult[idx + 2] = static_cast<unsigned char>( std::min( 255, bB * 255 / alpha ) );
}
else
{
rgbResult[idx] = 0;
rgbResult[idx + 1] = 0;
rgbResult[idx + 2] = 0;
}
}
wxMemoryOutputStream stream;
if( !result.SaveFile( stream, wxBITMAP_TYPE_PNG ) )
return false;
size_t dataSize = stream.GetOutputStreamBuffer()->GetBufferSize();
aBuffer.AppendData( stream.GetOutputStreamBuffer()->GetBufferStart(), dataSize );
return true;
}
} // namespace
SYMBOL_EDITOR_EDIT_TOOL::SYMBOL_EDITOR_EDIT_TOOL() :
SCH_TOOL_BASE( "eeschema.SymbolEditTool" )
@@ -1259,7 +1551,75 @@ int SYMBOL_EDITOR_EDIT_TOOL::Copy( const TOOL_EVENT& aEvent )
std::string prettyData = formatter.GetString();
KICAD_FORMAT::Prettify( prettyData, KICAD_FORMAT::FORMAT_MODE::COMPACT_TEXT_PROPERTIES );
if( SaveClipboard( prettyData ) )
// Generate SVG and PNG for multi-format clipboard
std::vector<CLIPBOARD_MIME_DATA> mimeData;
// Get the bounding box for just the selected items
BOX2I bbox;
for( EDA_ITEM* item : selection )
{
SCH_ITEM* schItem = static_cast<SCH_ITEM*>( item );
if( bbox.GetWidth() == 0 && bbox.GetHeight() == 0 )
bbox = schItem->GetBoundingBox();
else
bbox.Merge( schItem->GetBoundingBox() );
}
if( bbox.GetWidth() > 0 && bbox.GetHeight() > 0 )
{
bbox.Inflate( bbox.GetWidth() * clipboardBboxInflation,
bbox.GetHeight() * clipboardBboxInflation );
// Create a temporary symbol with just the selected items for plotting
LIB_SYMBOL* plotSymbol = new LIB_SYMBOL( *symbol );
// Mark unselected items as deleted in the plot copy
for( SCH_ITEM& item : plotSymbol->GetDrawItems() )
{
if( item.Type() == SCH_FIELD_T )
continue;
// Find matching item in selection by position/type
bool found = false;
for( EDA_ITEM* selItem : selection )
{
SCH_ITEM* selSchItem = static_cast<SCH_ITEM*>( selItem );
if( selSchItem->Type() == item.Type()
&& selSchItem->GetPosition() == item.GetPosition() )
{
found = true;
break;
}
}
if( !found )
item.SetFlags( STRUCT_DELETED );
}
// Now copy only the non-deleted items to a clean symbol for plotting
LIB_SYMBOL* cleanSymbol = new LIB_SYMBOL( *plotSymbol );
delete plotSymbol;
int unit = m_frame->GetUnit();
int bodyStyle = m_frame->GetBodyStyle();
wxMemoryBuffer svgBuffer;
if( plotSymbolToSvg( m_frame, cleanSymbol, bbox, unit, bodyStyle, svgBuffer ) )
appendMimeData( mimeData, wxS( "image/svg+xml" ), svgBuffer );
wxMemoryBuffer pngBuffer;
if( plotSymbolToPng( m_frame, cleanSymbol, bbox, unit, bodyStyle, pngBuffer ) )
appendMimeData( mimeData, wxS( "image/png" ), pngBuffer );
delete cleanSymbol;
}
if( SaveClipboard( prettyData, mimeData ) )
return 0;
else
return -1;
+11
View File
@@ -699,6 +699,17 @@ public:
*/
virtual void EndBlock( void* aData ) override;
/**
* Start a new named layer group in the SVG output.
* @param aLayerName The name/id for the layer group
*/
void StartLayer( const wxString& aLayerName );
/**
* End the current layer group in the SVG output.
*/
void EndLayer();
virtual void Text( const VECTOR2I& aPos,
const COLOR4D& aColor,
const wxString& aText,
+2
View File
@@ -112,6 +112,8 @@ set( QA_EESCHEMA_SRCS
test_update_items_connectivity.cpp
test_symbol_library.cpp
test_symbol_embedded_files.cpp
test_symbol_clipboard_export.cpp
test_schematic_clipboard_export.cpp
)
if( WIN32 )
@@ -0,0 +1,432 @@
/*
* This program source code file is part of KiCad, a free EDA CAD application.
*
* Copyright The 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 as published by the
* Free Software Foundation, either version 3 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, see <http://www.gnu.org/licenses/>.
*/
/**
* @file test_schematic_clipboard_export.cpp
* Tests for multi-format clipboard export functionality for schematic editor.
*
* These tests verify:
* 1. SVG export produces valid output for schematic items
* 2. Various schematic element types can be created for export testing
* 3. Bounding box calculation for different element types
*/
#include <boost/test/unit_test.hpp>
#include <sch_symbol.h>
#include <sch_line.h>
#include <sch_junction.h>
#include <sch_bus_entry.h>
#include <sch_no_connect.h>
#include <sch_text.h>
#include <sch_label.h>
#include <sch_shape.h>
#include <sch_sheet.h>
#include <lib_symbol.h>
#include <plotters/plotters_pslike.h>
#include <sch_painter.h>
#include <sch_plotter.h>
#include <locale_io.h>
#include <settings/color_settings.h>
#include <wx/ffile.h>
#include <wx/mstream.h>
class SCHEMATIC_CLIPBOARD_FIXTURE
{
public:
SCHEMATIC_CLIPBOARD_FIXTURE() = default;
~SCHEMATIC_CLIPBOARD_FIXTURE() = default;
std::unique_ptr<SCH_LINE> CreateWire( int x1, int y1, int x2, int y2 )
{
auto wire = std::make_unique<SCH_LINE>(
VECTOR2I( schIUScale.MilsToIU( x1 ), schIUScale.MilsToIU( y1 ) ),
LAYER_WIRE );
wire->SetEndPoint( VECTOR2I( schIUScale.MilsToIU( x2 ), schIUScale.MilsToIU( y2 ) ) );
m_items.push_back( wire.get() );
return wire;
}
std::unique_ptr<SCH_LINE> CreateBus( int x1, int y1, int x2, int y2 )
{
auto bus = std::make_unique<SCH_LINE>(
VECTOR2I( schIUScale.MilsToIU( x1 ), schIUScale.MilsToIU( y1 ) ),
LAYER_BUS );
bus->SetEndPoint( VECTOR2I( schIUScale.MilsToIU( x2 ), schIUScale.MilsToIU( y2 ) ) );
m_items.push_back( bus.get() );
return bus;
}
std::unique_ptr<SCH_JUNCTION> CreateJunction( int x, int y )
{
auto junction = std::make_unique<SCH_JUNCTION>(
VECTOR2I( schIUScale.MilsToIU( x ), schIUScale.MilsToIU( y ) ) );
m_items.push_back( junction.get() );
return junction;
}
std::unique_ptr<SCH_NO_CONNECT> CreateNoConnect( int x, int y )
{
auto noConnect = std::make_unique<SCH_NO_CONNECT>(
VECTOR2I( schIUScale.MilsToIU( x ), schIUScale.MilsToIU( y ) ) );
m_items.push_back( noConnect.get() );
return noConnect;
}
std::unique_ptr<SCH_BUS_WIRE_ENTRY> CreateBusEntry( int x, int y )
{
auto entry = std::make_unique<SCH_BUS_WIRE_ENTRY>(
VECTOR2I( schIUScale.MilsToIU( x ), schIUScale.MilsToIU( y ) ) );
m_items.push_back( entry.get() );
return entry;
}
std::unique_ptr<SCH_TEXT> CreateText( int x, int y, const wxString& text )
{
auto schText = std::make_unique<SCH_TEXT>(
VECTOR2I( schIUScale.MilsToIU( x ), schIUScale.MilsToIU( y ) ), text );
schText->SetTextSize( VECTOR2I( schIUScale.MilsToIU( 50 ), schIUScale.MilsToIU( 50 ) ) );
m_items.push_back( schText.get() );
return schText;
}
std::unique_ptr<SCH_LABEL> CreateLabel( int x, int y, const wxString& text )
{
auto label = std::make_unique<SCH_LABEL>(
VECTOR2I( schIUScale.MilsToIU( x ), schIUScale.MilsToIU( y ) ), text );
m_items.push_back( label.get() );
return label;
}
std::unique_ptr<SCH_GLOBALLABEL> CreateGlobalLabel( int x, int y, const wxString& text )
{
auto label = std::make_unique<SCH_GLOBALLABEL>(
VECTOR2I( schIUScale.MilsToIU( x ), schIUScale.MilsToIU( y ) ), text );
m_items.push_back( label.get() );
return label;
}
std::unique_ptr<SCH_HIERLABEL> CreateHierLabel( int x, int y, const wxString& text )
{
auto label = std::make_unique<SCH_HIERLABEL>(
VECTOR2I( schIUScale.MilsToIU( x ), schIUScale.MilsToIU( y ) ), text );
m_items.push_back( label.get() );
return label;
}
std::unique_ptr<SCH_SHAPE> CreateRectangle( int x1, int y1, int x2, int y2 )
{
auto rect = std::make_unique<SCH_SHAPE>( SHAPE_T::RECTANGLE );
rect->SetPosition( VECTOR2I( schIUScale.MilsToIU( x1 ), schIUScale.MilsToIU( y1 ) ) );
rect->SetEnd( VECTOR2I( schIUScale.MilsToIU( x2 ), schIUScale.MilsToIU( y2 ) ) );
rect->SetStroke( STROKE_PARAMS( schIUScale.MilsToIU( 10 ), LINE_STYLE::SOLID ) );
m_items.push_back( rect.get() );
return rect;
}
std::unique_ptr<SCH_SHAPE> CreateCircle( int cx, int cy, int radius )
{
auto circle = std::make_unique<SCH_SHAPE>( SHAPE_T::CIRCLE );
circle->SetPosition( VECTOR2I( schIUScale.MilsToIU( cx ), schIUScale.MilsToIU( cy ) ) );
circle->SetEnd( VECTOR2I( schIUScale.MilsToIU( cx + radius ), schIUScale.MilsToIU( cy ) ) );
circle->SetStroke( STROKE_PARAMS( schIUScale.MilsToIU( 10 ), LINE_STYLE::SOLID ) );
m_items.push_back( circle.get() );
return circle;
}
std::unique_ptr<SCH_SHAPE> CreatePolyline( const std::vector<std::pair<int, int>>& points )
{
auto poly = std::make_unique<SCH_SHAPE>( SHAPE_T::POLY );
poly->SetStroke( STROKE_PARAMS( schIUScale.MilsToIU( 10 ), LINE_STYLE::SOLID ) );
for( const auto& pt : points )
poly->AddPoint( VECTOR2I( schIUScale.MilsToIU( pt.first ), schIUScale.MilsToIU( pt.second ) ) );
m_items.push_back( poly.get() );
return poly;
}
void ClearItems()
{
m_items.clear();
}
std::vector<SCH_ITEM*> m_items;
};
BOOST_FIXTURE_TEST_SUITE( SchematicClipboardExport, SCHEMATIC_CLIPBOARD_FIXTURE )
/**
* Test that wires can be created for export testing.
*/
BOOST_AUTO_TEST_CASE( ElementCreation_Wires )
{
auto wire = CreateWire( 0, 0, 100, 0 );
BOOST_CHECK( wire != nullptr );
BOOST_CHECK( wire->IsWire() );
BOOST_CHECK_EQUAL( wire->GetStartPoint().x, schIUScale.MilsToIU( 0 ) );
BOOST_CHECK_EQUAL( wire->GetEndPoint().x, schIUScale.MilsToIU( 100 ) );
}
/**
* Test that buses can be created for export testing.
*/
BOOST_AUTO_TEST_CASE( ElementCreation_Buses )
{
auto bus = CreateBus( 0, 0, 0, 100 );
BOOST_CHECK( bus != nullptr );
BOOST_CHECK( bus->IsBus() );
}
/**
* Test that junctions can be created for export testing.
*/
BOOST_AUTO_TEST_CASE( ElementCreation_Junctions )
{
auto junction = CreateJunction( 50, 50 );
BOOST_CHECK( junction != nullptr );
BOOST_CHECK_EQUAL( junction->GetPosition().x, schIUScale.MilsToIU( 50 ) );
BOOST_CHECK_EQUAL( junction->GetPosition().y, schIUScale.MilsToIU( 50 ) );
}
/**
* Test that no-connect markers can be created for export testing.
*/
BOOST_AUTO_TEST_CASE( ElementCreation_NoConnect )
{
auto noConnect = CreateNoConnect( 100, 100 );
BOOST_CHECK( noConnect != nullptr );
BOOST_CHECK( noConnect->Type() == SCH_NO_CONNECT_T );
}
/**
* Test that bus entries can be created for export testing.
*/
BOOST_AUTO_TEST_CASE( ElementCreation_BusEntry )
{
auto entry = CreateBusEntry( 150, 150 );
BOOST_CHECK( entry != nullptr );
BOOST_CHECK( entry->Type() == SCH_BUS_WIRE_ENTRY_T );
}
/**
* Test that text items can be created for export testing.
*/
BOOST_AUTO_TEST_CASE( ElementCreation_Text )
{
auto text = CreateText( 200, 200, wxT( "Test Text" ) );
BOOST_CHECK( text != nullptr );
BOOST_CHECK( text->GetText() == wxT( "Test Text" ) );
}
/**
* Test that labels can be created for export testing.
*/
BOOST_AUTO_TEST_CASE( ElementCreation_Labels )
{
auto label = CreateLabel( 0, 0, wxT( "NET1" ) );
auto globalLabel = CreateGlobalLabel( 100, 0, wxT( "VCC" ) );
auto hierLabel = CreateHierLabel( 200, 0, wxT( "DATA_IN" ) );
BOOST_CHECK( label != nullptr );
BOOST_CHECK( globalLabel != nullptr );
BOOST_CHECK( hierLabel != nullptr );
BOOST_CHECK( label->Type() == SCH_LABEL_T );
BOOST_CHECK( globalLabel->Type() == SCH_GLOBAL_LABEL_T );
BOOST_CHECK( hierLabel->Type() == SCH_HIER_LABEL_T );
}
/**
* Test that shapes can be created for export testing.
*/
BOOST_AUTO_TEST_CASE( ElementCreation_Shapes )
{
auto rect = CreateRectangle( 0, 0, 100, 100 );
auto circle = CreateCircle( 200, 50, 50 );
auto poly = CreatePolyline( { { 300, 0 }, { 350, 50 }, { 300, 100 } } );
BOOST_CHECK( rect != nullptr );
BOOST_CHECK( circle != nullptr );
BOOST_CHECK( poly != nullptr );
BOOST_CHECK( rect->GetShape() == SHAPE_T::RECTANGLE );
BOOST_CHECK( circle->GetShape() == SHAPE_T::CIRCLE );
BOOST_CHECK( poly->GetShape() == SHAPE_T::POLY );
}
/**
* Test wire endpoint calculation.
*/
BOOST_AUTO_TEST_CASE( Wire_Endpoints )
{
auto wire1 = CreateWire( 0, 0, 100, 0 );
auto wire2 = CreateWire( 0, 0, 0, 100 );
// Verify wire endpoints
BOOST_CHECK_EQUAL( wire1->GetStartPoint().x, schIUScale.MilsToIU( 0 ) );
BOOST_CHECK_EQUAL( wire1->GetEndPoint().x, schIUScale.MilsToIU( 100 ) );
BOOST_CHECK_EQUAL( wire2->GetStartPoint().y, schIUScale.MilsToIU( 0 ) );
BOOST_CHECK_EQUAL( wire2->GetEndPoint().y, schIUScale.MilsToIU( 100 ) );
}
/**
* Test mixed element positions.
*/
BOOST_AUTO_TEST_CASE( MixedElements_Positions )
{
auto wire = CreateWire( 0, 0, 100, 0 );
auto junction = CreateJunction( 100, 0 );
auto text = CreateText( 150, 0, wxT( "Label" ) );
// Verify positions
BOOST_CHECK_EQUAL( wire->GetEndPoint().x, schIUScale.MilsToIU( 100 ) );
BOOST_CHECK_EQUAL( junction->GetPosition().x, schIUScale.MilsToIU( 100 ) );
BOOST_CHECK_EQUAL( text->GetPosition().x, schIUScale.MilsToIU( 150 ) );
}
/**
* Test rectangle dimensions.
*/
BOOST_AUTO_TEST_CASE( Rectangle_Dimensions )
{
auto rect = CreateRectangle( -50, -50, 50, 50 );
// Verify the rectangle was created with correct points
BOOST_CHECK_EQUAL( rect->GetStart().x, schIUScale.MilsToIU( -50 ) );
BOOST_CHECK_EQUAL( rect->GetStart().y, schIUScale.MilsToIU( -50 ) );
BOOST_CHECK_EQUAL( rect->GetEnd().x, schIUScale.MilsToIU( 50 ) );
BOOST_CHECK_EQUAL( rect->GetEnd().y, schIUScale.MilsToIU( 50 ) );
}
/**
* Test circle creation.
*/
BOOST_AUTO_TEST_CASE( Circle_Creation )
{
int radius = 100;
auto circle = CreateCircle( 0, 0, radius );
// Verify circle center and end point (which determines radius)
BOOST_CHECK_EQUAL( circle->GetStart().x, schIUScale.MilsToIU( 0 ) );
BOOST_CHECK_EQUAL( circle->GetStart().y, schIUScale.MilsToIU( 0 ) );
BOOST_CHECK_EQUAL( circle->GetEnd().x, schIUScale.MilsToIU( radius ) );
}
/**
* Test PNG alpha computation formula used in schematic export.
*/
BOOST_AUTO_TEST_CASE( PngExport_AlphaComputation_Opaque )
{
// Opaque pixel: same on white and black
int rW = 128, gW = 128, bW = 128;
int rB = 128, gB = 128, bB = 128;
int diffR = rW - rB;
int diffG = gW - gB;
int diffB = bW - bB;
int avgDiff = ( diffR + diffG + diffB ) / 3;
int alpha = 255 - avgDiff;
BOOST_CHECK_EQUAL( alpha, 255 );
}
/**
* Test PNG alpha computation for transparent pixel.
*/
BOOST_AUTO_TEST_CASE( PngExport_AlphaComputation_Transparent )
{
// Transparent pixel: shows background
int rW = 255, gW = 255, bW = 255;
int rB = 0, gB = 0, bB = 0;
int diffR = rW - rB;
int diffG = gW - gB;
int diffB = bW - bB;
int avgDiff = ( diffR + diffG + diffB ) / 3;
int alpha = 255 - avgDiff;
BOOST_CHECK_EQUAL( alpha, 0 );
}
/**
* Test that a complex schematic selection can be created.
*/
BOOST_AUTO_TEST_CASE( ComplexSchematic_MultipleLayers )
{
// Create a simple schematic structure
// Wires
auto wire1 = CreateWire( 0, 0, 200, 0 );
auto wire2 = CreateWire( 200, 0, 200, 100 );
// Junction at intersection
auto junction = CreateJunction( 200, 0 );
// Bus and bus entry
auto bus = CreateBus( 0, 200, 300, 200 );
auto busEntry = CreateBusEntry( 150, 200 );
// Labels
auto label = CreateLabel( 50, -20, wxT( "NET_A" ) );
auto globalLabel = CreateGlobalLabel( 250, 50, wxT( "VCC" ) );
// Text
auto text = CreateText( 100, 300, wxT( "Note: Power section" ) );
// Shapes
auto rect = CreateRectangle( -50, -50, 350, 350 );
// Verify all items were created with correct types
BOOST_CHECK_EQUAL( m_items.size(), 9 );
BOOST_CHECK( wire1->IsWire() );
BOOST_CHECK( wire2->IsWire() );
BOOST_CHECK( bus->IsBus() );
BOOST_CHECK( junction->Type() == SCH_JUNCTION_T );
BOOST_CHECK( busEntry->Type() == SCH_BUS_WIRE_ENTRY_T );
BOOST_CHECK( label->Type() == SCH_LABEL_T );
BOOST_CHECK( globalLabel->Type() == SCH_GLOBAL_LABEL_T );
BOOST_CHECK( text->Type() == SCH_TEXT_T );
BOOST_CHECK( rect->GetShape() == SHAPE_T::RECTANGLE );
}
BOOST_AUTO_TEST_SUITE_END()
@@ -0,0 +1,457 @@
/*
* This program source code file is part of KiCad, a free EDA CAD application.
*
* Copyright The 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 as published by the
* Free Software Foundation, either version 3 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, see <http://www.gnu.org/licenses/>.
*/
/**
* @file test_symbol_clipboard_export.cpp
* Tests for multi-format clipboard export functionality for symbol editor.
*
* These tests verify:
* 1. SVG export produces valid output for symbols
* 2. Symbol bounding box calculation is correct for different element types
* 3. PNG alpha computation using dual-buffer technique
*/
#include <boost/test/unit_test.hpp>
#include <lib_symbol.h>
#include <sch_pin.h>
#include <sch_shape.h>
#include <sch_text.h>
#include <sch_textbox.h>
#include <plotters/plotters_pslike.h>
#include <sch_painter.h>
#include <sch_plotter.h>
#include <locale_io.h>
#include <settings/color_settings.h>
#include <wx/ffile.h>
#include <wx/mstream.h>
class SYMBOL_CLIPBOARD_FIXTURE
{
public:
SYMBOL_CLIPBOARD_FIXTURE()
{
m_symbol = std::make_unique<LIB_SYMBOL>( wxT( "TestSymbol" ), nullptr );
}
~SYMBOL_CLIPBOARD_FIXTURE() = default;
void AddPin( int x, int y, const wxString& name, const wxString& number )
{
std::unique_ptr<SCH_PIN> pin = std::make_unique<SCH_PIN>( m_symbol.get() );
pin->SetPosition( VECTOR2I( schIUScale.MilsToIU( x ), schIUScale.MilsToIU( y ) ) );
pin->SetName( name );
pin->SetNumber( number );
pin->SetLength( schIUScale.MilsToIU( 100 ) );
m_symbol->AddDrawItem( pin.release() );
}
void AddRectangle( int x1, int y1, int x2, int y2 )
{
std::unique_ptr<SCH_SHAPE> rect = std::make_unique<SCH_SHAPE>( SHAPE_T::RECTANGLE, LAYER_DEVICE );
rect->SetPosition( VECTOR2I( schIUScale.MilsToIU( x1 ), schIUScale.MilsToIU( y1 ) ) );
rect->SetEnd( VECTOR2I( schIUScale.MilsToIU( x2 ), schIUScale.MilsToIU( y2 ) ) );
rect->SetStroke( STROKE_PARAMS( schIUScale.MilsToIU( 10 ), LINE_STYLE::SOLID ) );
m_symbol->AddDrawItem( rect.release() );
}
void AddCircle( int cx, int cy, int radius )
{
std::unique_ptr<SCH_SHAPE> circle = std::make_unique<SCH_SHAPE>( SHAPE_T::CIRCLE, LAYER_DEVICE );
circle->SetPosition( VECTOR2I( schIUScale.MilsToIU( cx ), schIUScale.MilsToIU( cy ) ) );
circle->SetEnd( VECTOR2I( schIUScale.MilsToIU( cx + radius ), schIUScale.MilsToIU( cy ) ) );
circle->SetStroke( STROKE_PARAMS( schIUScale.MilsToIU( 10 ), LINE_STYLE::SOLID ) );
m_symbol->AddDrawItem( circle.release() );
}
void AddPolyline( const std::vector<std::pair<int, int>>& points )
{
std::unique_ptr<SCH_SHAPE> poly = std::make_unique<SCH_SHAPE>( SHAPE_T::POLY, LAYER_DEVICE );
poly->SetStroke( STROKE_PARAMS( schIUScale.MilsToIU( 10 ), LINE_STYLE::SOLID ) );
for( const auto& pt : points )
poly->AddPoint( VECTOR2I( schIUScale.MilsToIU( pt.first ), schIUScale.MilsToIU( pt.second ) ) );
m_symbol->AddDrawItem( poly.release() );
}
void AddArc( int cx, int cy, int radius, EDA_ANGLE startAngle, EDA_ANGLE endAngle )
{
std::unique_ptr<SCH_SHAPE> arc = std::make_unique<SCH_SHAPE>( SHAPE_T::ARC, LAYER_DEVICE );
arc->SetCenter( VECTOR2I( schIUScale.MilsToIU( cx ), schIUScale.MilsToIU( cy ) ) );
arc->SetRadius( schIUScale.MilsToIU( radius ) );
arc->SetArcAngleAndEnd( endAngle - startAngle );
arc->SetStroke( STROKE_PARAMS( schIUScale.MilsToIU( 10 ), LINE_STYLE::SOLID ) );
m_symbol->AddDrawItem( arc.release() );
}
void AddText( int x, int y, const wxString& text )
{
std::unique_ptr<SCH_TEXT> txt = std::make_unique<SCH_TEXT>( VECTOR2I( schIUScale.MilsToIU( x ),
schIUScale.MilsToIU( y ) ),
text, LAYER_DEVICE );
txt->SetTextSize( VECTOR2I( schIUScale.MilsToIU( 50 ), schIUScale.MilsToIU( 50 ) ) );
m_symbol->AddDrawItem( txt.release() );
}
BOX2I GetSymbolBoundingBox( int aUnit = 0, int aBodyStyle = 0 )
{
BOX2I bbox;
for( SCH_ITEM& item : m_symbol->GetDrawItems() )
{
if( item.Type() == SCH_FIELD_T )
continue;
if( aUnit && item.GetUnit() && item.GetUnit() != aUnit )
continue;
if( aBodyStyle && item.GetBodyStyle() && item.GetBodyStyle() != aBodyStyle )
continue;
if( bbox.GetWidth() == 0 && bbox.GetHeight() == 0 )
bbox = item.GetBoundingBox();
else
bbox.Merge( item.GetBoundingBox() );
}
return bbox;
}
wxString PlotToSvgString( int aUnit = 0, int aBodyStyle = 0 )
{
BOX2I bbox = GetSymbolBoundingBox( aUnit, aBodyStyle );
if( bbox.GetWidth() <= 0 || bbox.GetHeight() <= 0 )
return wxEmptyString;
bbox.Inflate( bbox.GetWidth() * 0.02, bbox.GetHeight() * 0.02 );
SCH_RENDER_SETTINGS renderSettings;
COLOR_SETTINGS colorSettings;
renderSettings.LoadColors( &colorSettings );
renderSettings.SetDefaultPenWidth( schIUScale.MilsToIU( 6 ) );
std::unique_ptr<SVG_PLOTTER> plotter = std::make_unique<SVG_PLOTTER>();
plotter->SetRenderSettings( &renderSettings );
PAGE_INFO pageInfo;
pageInfo.SetWidthMils( schIUScale.IUToMils( bbox.GetWidth() ) );
pageInfo.SetHeightMils( schIUScale.IUToMils( bbox.GetHeight() ) );
plotter->SetPageSettings( pageInfo );
plotter->SetColorMode( true );
VECTOR2I plot_offset = bbox.GetOrigin();
plotter->SetViewport( plot_offset, schIUScale.IU_PER_MILS / 10, 1.0, false );
plotter->SetCreator( wxT( "Eeschema-SVG-Test" ) );
wxFileName tempFile( wxFileName::CreateTempFileName( wxT( "kicad_test_svg" ) ) );
if( !plotter->OpenFile( tempFile.GetFullPath() ) )
{
wxRemoveFile( tempFile.GetFullPath() );
return wxEmptyString;
}
LOCALE_IO toggle;
SCH_PLOT_OPTS plotOpts;
plotter->StartPlot( wxT( "1" ) );
constexpr bool background = true;
m_symbol->Plot( plotter.get(), background, plotOpts, aUnit, aBodyStyle, VECTOR2I( 0, 0 ), false );
m_symbol->Plot( plotter.get(), !background, plotOpts, aUnit, aBodyStyle, VECTOR2I( 0, 0 ), false );
m_symbol->PlotFields( plotter.get(), !background, plotOpts, aUnit, aBodyStyle, VECTOR2I( 0, 0 ), false );
plotter->EndPlot();
plotter.reset();
wxFFile file( tempFile.GetFullPath(), wxT( "rb" ) );
wxString content;
if( file.IsOpened() )
file.ReadAll( &content );
wxRemoveFile( tempFile.GetFullPath() );
return content;
}
std::unique_ptr<LIB_SYMBOL> m_symbol;
};
BOOST_FIXTURE_TEST_SUITE( SymbolClipboardExport, SYMBOL_CLIPBOARD_FIXTURE )
/**
* Test that a symbol with pins produces SVG output
*/
BOOST_AUTO_TEST_CASE( SvgExport_ContainsPins )
{
AddPin( 0, 0, wxT( "VCC" ), wxT( "1" ) );
AddPin( 0, 100, wxT( "GND" ), wxT( "2" ) );
AddPin( 0, 200, wxT( "OUT" ), wxT( "3" ) );
wxString svg = PlotToSvgString();
BOOST_CHECK( !svg.IsEmpty() );
BOOST_CHECK( svg.Contains( wxT( "<svg" ) ) );
BOOST_CHECK( svg.Contains( wxT( "</svg>" ) ) );
// SVG should contain paths or lines for pin elements
BOOST_CHECK( svg.Contains( wxT( "<path" ) ) || svg.Contains( wxT( "<line" ) ) );
}
/**
* Test that a symbol with a rectangle produces SVG output
*/
BOOST_AUTO_TEST_CASE( SvgExport_ContainsRectangle )
{
AddRectangle( -100, -100, 100, 100 );
wxString svg = PlotToSvgString();
BOOST_CHECK( !svg.IsEmpty() );
BOOST_CHECK( svg.Contains( wxT( "<svg" ) ) );
// Rectangle should produce a path or rect element
BOOST_CHECK( svg.Contains( wxT( "<path" ) ) || svg.Contains( wxT( "<rect" ) ) );
}
/**
* Test that a symbol with a circle produces SVG output
*/
BOOST_AUTO_TEST_CASE( SvgExport_ContainsCircle )
{
AddCircle( 0, 0, 50 );
wxString svg = PlotToSvgString();
BOOST_CHECK( !svg.IsEmpty() );
BOOST_CHECK( svg.Contains( wxT( "<svg" ) ) );
// Circle should produce a circle or ellipse element
BOOST_CHECK( svg.Contains( wxT( "<circle" ) ) || svg.Contains( wxT( "<ellipse" ) )
|| svg.Contains( wxT( "<path" ) ) );
}
/**
* Test that a symbol with a polyline produces SVG output
*/
BOOST_AUTO_TEST_CASE( SvgExport_ContainsPolyline )
{
AddPolyline( { { 0, 0 }, { 50, 50 }, { 100, 0 }, { 100, 100 } } );
wxString svg = PlotToSvgString();
BOOST_CHECK( !svg.IsEmpty() );
BOOST_CHECK( svg.Contains( wxT( "<svg" ) ) );
// Polyline should produce a path or polyline element
BOOST_CHECK( svg.Contains( wxT( "<path" ) ) || svg.Contains( wxT( "<polyline" ) ) );
}
/**
* Test that a symbol with text produces SVG output
*/
BOOST_AUTO_TEST_CASE( SvgExport_ContainsText )
{
AddText( 0, 0, wxT( "Test Label" ) );
wxString svg = PlotToSvgString();
BOOST_CHECK( !svg.IsEmpty() );
BOOST_CHECK( svg.Contains( wxT( "<svg" ) ) );
// Text should produce a text element or paths for glyphs
BOOST_CHECK( svg.Contains( wxT( "<text" ) ) || svg.Contains( wxT( "<path" ) ) );
}
/**
* Test that bounding box is calculated correctly for pins
*/
BOOST_AUTO_TEST_CASE( BoundingBox_Pins )
{
AddPin( 0, 0, wxT( "PIN1" ), wxT( "1" ) );
AddPin( 200, 0, wxT( "PIN2" ), wxT( "2" ) );
BOX2I bbox = GetSymbolBoundingBox();
// Bounding box should span from first pin to second pin (plus pin length and decoration)
BOOST_CHECK( bbox.GetWidth() > 0 );
BOOST_CHECK( bbox.GetHeight() > 0 );
}
/**
* Test that bounding box is calculated correctly for rectangle
*/
BOOST_AUTO_TEST_CASE( BoundingBox_Rectangle )
{
AddRectangle( -50, -50, 50, 50 );
BOX2I bbox = GetSymbolBoundingBox();
// Bounding box should approximately match the rectangle size
int expectedWidth = schIUScale.MilsToIU( 100 ); // 50 - (-50) = 100 mils
int expectedHeight = schIUScale.MilsToIU( 100 );
BOOST_CHECK( std::abs( bbox.GetWidth() - expectedWidth ) < schIUScale.MilsToIU( 20 ) );
BOOST_CHECK( std::abs( bbox.GetHeight() - expectedHeight ) < schIUScale.MilsToIU( 20 ) );
}
/**
* Test that bounding box is calculated correctly for circle
*/
BOOST_AUTO_TEST_CASE( BoundingBox_Circle )
{
int radius = 100;
AddCircle( 0, 0, radius );
BOX2I bbox = GetSymbolBoundingBox();
// Bounding box should be approximately 2*radius in each dimension
int expectedSize = schIUScale.MilsToIU( 2 * radius );
BOOST_CHECK( std::abs( bbox.GetWidth() - expectedSize ) < schIUScale.MilsToIU( 20 ) );
BOOST_CHECK( std::abs( bbox.GetHeight() - expectedSize ) < schIUScale.MilsToIU( 20 ) );
}
/**
* Test that a complex symbol with multiple elements produces valid SVG
*/
BOOST_AUTO_TEST_CASE( SvgExport_ComplexSymbol )
{
// Create a simple IC-like symbol
AddRectangle( -100, -150, 100, 150 );
AddPin( -200, -100, wxT( "A" ), wxT( "1" ) );
AddPin( -200, 0, wxT( "B" ), wxT( "2" ) );
AddPin( -200, 100, wxT( "C" ), wxT( "3" ) );
AddPin( 200, -100, wxT( "Y" ), wxT( "4" ) );
AddPin( 200, 0, wxT( "Z" ), wxT( "5" ) );
AddPin( 200, 100, wxT( "W" ), wxT( "6" ) );
AddText( 0, 0, wxT( "IC" ) );
wxString svg = PlotToSvgString();
BOOST_CHECK( !svg.IsEmpty() );
BOOST_CHECK( svg.Contains( wxT( "<svg" ) ) );
BOOST_CHECK( svg.Contains( wxT( "</svg>" ) ) );
// Should have multiple path elements for all the components
int pathCount = 0;
size_t pos = 0;
while( ( pos = svg.find( wxT( "<path" ), pos ) ) != wxString::npos )
{
pathCount++;
pos++;
}
// A complex symbol should have multiple paths
BOOST_CHECK( pathCount >= 1 );
}
/**
* Test PNG alpha computation formula:
* Given pixels on white background (W) and black background (B),
* alpha = 255 - (W - B), and color = B * 255 / alpha
*/
BOOST_AUTO_TEST_CASE( PngExport_AlphaComputation_OpaquePixel )
{
// An opaque red pixel: on white = red, on black = red
int rW = 255, gW = 0, bW = 0; // red on white
int rB = 255, gB = 0, bB = 0; // red on black
int diffR = rW - rB;
int diffG = gW - gB;
int diffB = bW - bB;
int avgDiff = ( diffR + diffG + diffB ) / 3;
int alpha = 255 - avgDiff;
BOOST_CHECK_EQUAL( alpha, 255 ); // Fully opaque
}
/**
* Test PNG alpha computation for transparent pixel
*/
BOOST_AUTO_TEST_CASE( PngExport_AlphaComputation_TransparentPixel )
{
// A transparent pixel: on white = white, on black = black
int rW = 255, gW = 255, bW = 255; // white on white
int rB = 0, gB = 0, bB = 0; // black on black
int diffR = rW - rB;
int diffG = gW - gB;
int diffB = bW - bB;
int avgDiff = ( diffR + diffG + diffB ) / 3;
int alpha = 255 - avgDiff;
BOOST_CHECK_EQUAL( alpha, 0 ); // Fully transparent
}
/**
* Test PNG alpha computation for semi-transparent pixel
*/
BOOST_AUTO_TEST_CASE( PngExport_AlphaComputation_SemiTransparentPixel )
{
// A 50% transparent red pixel
// On white: blends to (255, 128, 128) approximately
// On black: blends to (128, 0, 0) approximately
int rW = 255, gW = 128, bW = 128;
int rB = 128, gB = 0, bB = 0;
int diffR = rW - rB; // 127
int diffG = gW - gB; // 128
int diffB = bW - bB; // 128
int avgDiff = ( diffR + diffG + diffB ) / 3; // approximately 127-128
int alpha = 255 - avgDiff;
// Alpha should be around 127-128 (50%)
BOOST_CHECK( alpha > 120 && alpha < 140 );
}
/**
* Test that an empty symbol produces empty SVG content
*/
BOOST_AUTO_TEST_CASE( SvgExport_EmptySymbol )
{
// Don't add any items - the symbol only has default fields
// After removing fields from bounding box calculation, should be empty
BOX2I bbox = GetSymbolBoundingBox();
// An empty symbol (no non-field items) should have zero-size bounding box
BOOST_CHECK( bbox.GetWidth() == 0 || bbox.GetHeight() == 0 );
}
BOOST_AUTO_TEST_SUITE_END()
File diff suppressed because it is too large Load Diff