Files
kicad-source-mirror/qa/tests/common/test_clipboard.cpp
T
Alex Shvartzkop 71471893d2 Use opaque background when copying schematic elements for performance.
Avoids unnecessary wxBitmap -> wxImage -> wxBitmap conversions.
Avoids rendering twice.
2026-01-19 01:31:03 +03:00

907 lines
29 KiB
C++
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/*
* 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/>.
*/
#include <boost/test/unit_test.hpp>
#include <clipboard.h>
#include <wx/clipbrd.h>
#include <wx/display.h>
#include <wx/image.h>
#include <wx/string.h>
#include <wx/filename.h>
#include <wx/mstream.h>
#include <vector>
#include <cstdlib>
/**
* Check if a display is available for clipboard operations.
* On Linux/GTK, clipboard operations require a display connection.
* In headless CI environments, this is not available.
*/
static bool IsDisplayAvailable()
{
#ifdef __WXGTK__
// On GTK, check if wxWidgets can actually see displays.
// Just having DISPLAY environment variable set isn't enough
return wxDisplay::GetCount() > 0;
#endif
return true;
}
/**
* Macro to skip clipboard tests in headless environments.
* This prevents GTK assertions when no display is available.
*/
#define SKIP_IF_HEADLESS() \
do \
{ \
if( !IsDisplayAvailable() ) \
{ \
BOOST_TEST_MESSAGE( "Skipping test - no display available (headless environment)" ); \
return; \
} \
} while( 0 )
BOOST_AUTO_TEST_SUITE( ClipboardTests )
BOOST_AUTO_TEST_CASE( SaveClipboard_BasicText )
{
SKIP_IF_HEADLESS();
std::string testText = "Basic clipboard test";
bool result = SaveClipboard( testText );
if( result )
{
std::string retrieved = GetClipboardUTF8();
BOOST_CHECK_EQUAL( retrieved, testText );
}
// Note: Test may fail on headless systems where clipboard isn't available
}
BOOST_AUTO_TEST_CASE( SaveClipboard_EmptyString )
{
SKIP_IF_HEADLESS();
std::string emptyText = "";
bool result = SaveClipboard( emptyText );
if( result )
{
std::string retrieved = GetClipboardUTF8();
BOOST_CHECK_EQUAL( retrieved, emptyText );
}
}
BOOST_AUTO_TEST_CASE( SaveClipboard_UTF8Characters )
{
SKIP_IF_HEADLESS();
std::string utf8Text = "Héllo Wörld! 你好 🚀";
bool result = SaveClipboard( utf8Text );
if( result )
{
std::string retrieved = GetClipboardUTF8();
BOOST_CHECK_EQUAL( retrieved, utf8Text );
}
}
BOOST_AUTO_TEST_CASE( SaveClipboard_LargeText )
{
SKIP_IF_HEADLESS();
std::string largeText( 10000, 'A' );
largeText += "END";
bool result = SaveClipboard( largeText );
if( result )
{
std::string retrieved = GetClipboardUTF8();
BOOST_CHECK_EQUAL( retrieved, largeText );
}
}
BOOST_AUTO_TEST_CASE( SaveClipboard_SpecialCharacters )
{
SKIP_IF_HEADLESS();
std::string specialText = "Line1\nLine2\tTabbed\r\nWindows newline";
bool result = SaveClipboard( specialText );
if( result )
{
std::string retrieved = GetClipboardUTF8();
BOOST_CHECK_EQUAL( retrieved, specialText );
}
}
BOOST_AUTO_TEST_CASE( GetClipboardUTF8_EmptyClipboard )
{
SKIP_IF_HEADLESS();
// Clear clipboard first
if( wxTheClipboard->Open() )
{
wxTheClipboard->Clear();
wxTheClipboard->Close();
}
std::string result = GetClipboardUTF8();
BOOST_CHECK( result.empty() );
}
BOOST_AUTO_TEST_CASE( GetClipboardUTF8_NonTextData )
{
SKIP_IF_HEADLESS();
// This test verifies behavior when clipboard contains non-text data
// Implementation depends on system behavior - may return empty string
std::string result = GetClipboardUTF8();
// No specific assertion - just ensure it doesn't crash
BOOST_CHECK( true );
}
BOOST_AUTO_TEST_CASE( SaveTabularData_SimpleGrid )
{
SKIP_IF_HEADLESS();
std::vector<std::vector<wxString>> testData = {
{ wxS("A1"), wxS("B1"), wxS("C1") },
{ wxS("A2"), wxS("B2"), wxS("C2") },
{ wxS("A3"), wxS("B3"), wxS("C3") }
};
bool result = SaveTabularDataToClipboard( testData );
if( result )
{
std::vector<std::vector<wxString>> retrieved;
bool parseResult = GetTabularDataFromClipboard( retrieved );
if( parseResult )
{
BOOST_CHECK_EQUAL( retrieved.size(), testData.size() );
for( size_t i = 0; i < testData.size() && i < retrieved.size(); ++i )
{
BOOST_CHECK_EQUAL( retrieved[i].size(), testData[i].size() );
for( size_t j = 0; j < testData[i].size() && j < retrieved[i].size(); ++j )
{
BOOST_CHECK_EQUAL( retrieved[i][j], testData[i][j] );
}
}
}
}
}
BOOST_AUTO_TEST_CASE( SaveTabularData_EmptyGrid )
{
SKIP_IF_HEADLESS();
std::vector<std::vector<wxString>> emptyData;
bool result = SaveTabularDataToClipboard( emptyData );
if( result )
{
std::vector<std::vector<wxString>> retrieved;
bool parseResult = GetTabularDataFromClipboard( retrieved );
if( parseResult )
{
BOOST_CHECK( retrieved.empty() );
}
}
}
BOOST_AUTO_TEST_CASE( SaveTabularData_SingleCell )
{
SKIP_IF_HEADLESS();
std::vector<std::vector<wxString>> singleCell = {
{ wxS("OnlyCell") }
};
bool result = SaveTabularDataToClipboard( singleCell );
if( result )
{
std::vector<std::vector<wxString>> retrieved;
bool parseResult = GetTabularDataFromClipboard( retrieved );
if( parseResult )
{
BOOST_CHECK_EQUAL( retrieved.size(), 1 );
BOOST_CHECK_EQUAL( retrieved[0].size(), 1 );
BOOST_CHECK_EQUAL( retrieved[0][0], wxString( wxS("OnlyCell") ) );
}
}
}
BOOST_AUTO_TEST_CASE( SaveTabularData_WithCommas )
{
SKIP_IF_HEADLESS();
std::vector<std::vector<wxString>> dataWithCommas = {
{ wxS("Value, with comma"), wxS("Normal") },
{ wxS("Another, comma"), wxS("Also normal") }
};
bool result = SaveTabularDataToClipboard( dataWithCommas );
if( result )
{
std::vector<std::vector<wxString>> retrieved;
bool parseResult = GetTabularDataFromClipboard( retrieved );
if( parseResult )
{
BOOST_CHECK_EQUAL( retrieved.size(), dataWithCommas.size() );
for( size_t i = 0; i < dataWithCommas.size() && i < retrieved.size(); ++i )
{
BOOST_CHECK_EQUAL( retrieved[i].size(), dataWithCommas[i].size() );
for( size_t j = 0; j < dataWithCommas[i].size() && j < retrieved[i].size(); ++j )
{
BOOST_CHECK_EQUAL( retrieved[i][j], dataWithCommas[i][j] );
}
}
}
}
}
BOOST_AUTO_TEST_CASE( SaveTabularData_WithQuotes )
{
SKIP_IF_HEADLESS();
std::vector<std::vector<wxString>> dataWithQuotes = {
{ wxS("\"Quoted value\""), wxS("Normal") },
{ wxS("Value with \"inner\" quotes"), wxS("Plain") }
};
bool result = SaveTabularDataToClipboard( dataWithQuotes );
if( result )
{
std::vector<std::vector<wxString>> retrieved;
bool parseResult = GetTabularDataFromClipboard( retrieved );
if( parseResult )
{
BOOST_CHECK_EQUAL( retrieved.size(), dataWithQuotes.size() );
// Note: Exact quote handling depends on CSV parser implementation
}
}
}
BOOST_AUTO_TEST_CASE( SaveTabularData_WithNewlines )
{
SKIP_IF_HEADLESS();
std::vector<std::vector<wxString>> dataWithNewlines = {
{ wxS("Line1\nLine2"), wxS("Normal") },
{ wxS("Single line"), wxS("Another\nmultiline") }
};
bool result = SaveTabularDataToClipboard( dataWithNewlines );
if( result )
{
std::vector<std::vector<wxString>> retrieved;
bool parseResult = GetTabularDataFromClipboard( retrieved );
if( parseResult )
{
BOOST_CHECK_EQUAL( retrieved.size(), dataWithNewlines.size() );
// Note: Newline handling depends on CSV parser implementation
}
}
}
BOOST_AUTO_TEST_CASE( SaveTabularData_IrregularGrid )
{
SKIP_IF_HEADLESS();
std::vector<std::vector<wxString>> irregularData = {
{ wxS("A1"), wxS("B1"), wxS("C1"), wxS("D1") },
{ wxS("A2"), wxS("B2") },
{ wxS("A3"), wxS("B3"), wxS("C3") }
};
bool result = SaveTabularDataToClipboard( irregularData );
if( result )
{
std::vector<std::vector<wxString>> retrieved;
bool parseResult = GetTabularDataFromClipboard( retrieved );
if( parseResult )
{
BOOST_CHECK_EQUAL( retrieved.size(), irregularData.size() );
// Each row should maintain its individual size
for( size_t i = 0; i < irregularData.size() && i < retrieved.size(); ++i )
{
for( size_t j = 0; j < irregularData[i].size() && j < retrieved[i].size(); ++j )
{
BOOST_CHECK_EQUAL( retrieved[i][j], irregularData[i][j] );
}
}
}
}
}
BOOST_AUTO_TEST_CASE( GetTabularDataFromClipboard_InvalidData )
{
SKIP_IF_HEADLESS();
// Save non-tabular text to clipboard
std::string invalidText = "This is not tabular data\nJust some text";
SaveClipboard( invalidText );
std::vector<std::vector<wxString>> retrieved;
bool result = GetTabularDataFromClipboard( retrieved );
// Should either parse as single-column data or return appropriate result
// Exact behavior depends on AutoDecodeCSV implementation
BOOST_CHECK( true ); // Test that it doesn't crash
}
BOOST_AUTO_TEST_CASE( GetImageFromClipboard_NoImage )
{
SKIP_IF_HEADLESS();
// Clear clipboard
if( wxTheClipboard->Open() )
{
wxTheClipboard->Clear();
wxTheClipboard->Close();
}
std::unique_ptr<wxBitmap> image = GetImageFromClipboard();
BOOST_CHECK( !image || !image->IsOk() );
}
BOOST_AUTO_TEST_CASE( GetImageFromClipboard_TextInClipboard )
{
SKIP_IF_HEADLESS();
// Put text in clipboard
SaveClipboard( "This is text, not an image" );
std::unique_ptr<wxBitmap> image = GetImageFromClipboard();
BOOST_CHECK( !image || !image->IsOk() );
}
BOOST_AUTO_TEST_CASE( Clipboard_MultipleSaveOperations )
{
SKIP_IF_HEADLESS();
// Test multiple sequential save operations
std::vector<std::string> testStrings = {
"First string",
"Second string with 特殊字符",
"Third string\nwith\nnewlines",
""
};
for( const auto& testString : testStrings )
{
bool saved = SaveClipboard( testString );
if( saved )
{
std::string retrieved = GetClipboardUTF8();
BOOST_CHECK_EQUAL( retrieved, testString );
}
}
}
BOOST_AUTO_TEST_CASE( Clipboard_ConcurrentAccess )
{
SKIP_IF_HEADLESS();
// Test that clipboard operations are properly synchronized
std::string testText1 = "Concurrent test 1";
std::string testText2 = "Concurrent test 2";
bool result1 = SaveClipboard( testText1 );
bool result2 = SaveClipboard( testText2 );
if( result2 )
{
std::string retrieved = GetClipboardUTF8();
BOOST_CHECK_EQUAL( retrieved, testText2 ); // Should have the last saved value
}
}
BOOST_AUTO_TEST_CASE( Clipboard_FlushBehavior )
{
SKIP_IF_HEADLESS();
// Test that Flush() allows data to persist after the application
std::string persistentText = "This should persist after flush";
bool result = SaveClipboard( persistentText );
if( result )
{
// Data should still be available
std::string retrieved = GetClipboardUTF8();
BOOST_CHECK_EQUAL( retrieved, persistentText );
}
}
BOOST_AUTO_TEST_CASE( SaveClipboard_WithMimeData_EmptyMimeDataFallsBack )
{
SKIP_IF_HEADLESS();
// When MIME data is empty, should fall back to basic SaveClipboard
std::string testText = "Fallback test with empty MIME data";
std::vector<CLIPBOARD_MIME_DATA> emptyMimeData;
bool result = SaveClipboard( testText, emptyMimeData );
if( result )
{
std::string retrieved = GetClipboardUTF8();
BOOST_CHECK_EQUAL( retrieved, testText );
}
}
BOOST_AUTO_TEST_CASE( SaveClipboard_WithMimeData_KicadFormat )
{
SKIP_IF_HEADLESS();
// Test that application/kicad MIME type is prioritized when reading
std::string textData = "Plain text representation";
std::string kicadData = "KiCad native format data";
std::vector<CLIPBOARD_MIME_DATA> mimeData;
CLIPBOARD_MIME_DATA kicadEntry;
kicadEntry.m_mimeType = wxS( "application/kicad" );
kicadEntry.m_data.AppendData( kicadData.data(), kicadData.size() );
mimeData.push_back( kicadEntry );
bool result = SaveClipboard( textData, mimeData );
if( result )
{
// GetClipboardUTF8 should prioritize application/kicad format
std::string retrieved = GetClipboardUTF8();
BOOST_CHECK_EQUAL( retrieved, kicadData );
}
}
BOOST_AUTO_TEST_CASE( SaveClipboard_WithMimeData_MultipleMimeTypes )
{
SKIP_IF_HEADLESS();
// Test saving with multiple MIME types
std::string textData = "Text for clipboard";
std::string kicadData = "KiCad data for clipboard";
std::string svgData = "<svg></svg>";
std::string pngData = "\x89PNG\r\n"; // PNG magic bytes (truncated for test)
std::vector<CLIPBOARD_MIME_DATA> mimeData;
CLIPBOARD_MIME_DATA kicadEntry;
kicadEntry.m_mimeType = wxS( "application/kicad" );
kicadEntry.m_data.AppendData( kicadData.data(), kicadData.size() );
mimeData.push_back( kicadEntry );
CLIPBOARD_MIME_DATA svgEntry;
svgEntry.m_mimeType = wxS( "image/svg+xml" );
svgEntry.m_data.AppendData( svgData.data(), svgData.size() );
mimeData.push_back( svgEntry );
CLIPBOARD_MIME_DATA pngEntry;
pngEntry.m_mimeType = wxS( "image/png" );
pngEntry.m_data.AppendData( pngData.data(), pngData.size() );
mimeData.push_back( pngEntry );
bool result = SaveClipboard( textData, mimeData );
if( result )
{
// Verify at least the KiCad format is retrievable
std::string retrieved = GetClipboardUTF8();
BOOST_CHECK_EQUAL( retrieved, kicadData );
}
}
BOOST_AUTO_TEST_CASE( SaveClipboard_WithMimeData_NoKicadFormat )
{
SKIP_IF_HEADLESS();
// When no application/kicad format, should fall back to text
std::string textData = "Text for clipboard without kicad format";
std::string svgData = "<svg></svg>";
std::vector<CLIPBOARD_MIME_DATA> mimeData;
CLIPBOARD_MIME_DATA svgEntry;
svgEntry.m_mimeType = wxS( "image/svg+xml" );
svgEntry.m_data.AppendData( svgData.data(), svgData.size() );
mimeData.push_back( svgEntry );
bool result = SaveClipboard( textData, mimeData );
if( result )
{
// Should get the text since no application/kicad format is present
std::string retrieved = GetClipboardUTF8();
BOOST_CHECK_EQUAL( retrieved, textData );
}
}
BOOST_AUTO_TEST_CASE( SaveClipboard_WithMimeData_UTF8InKicadFormat )
{
SKIP_IF_HEADLESS();
// Test that UTF8 characters are preserved in KiCad MIME format
std::string textData = "Plain text";
std::string kicadData = "KiCad data with UTF8: 你好世界 🔧";
std::vector<CLIPBOARD_MIME_DATA> mimeData;
CLIPBOARD_MIME_DATA kicadEntry;
kicadEntry.m_mimeType = wxS( "application/kicad" );
kicadEntry.m_data.AppendData( kicadData.data(), kicadData.size() );
mimeData.push_back( kicadEntry );
bool result = SaveClipboard( textData, mimeData );
if( result )
{
std::string retrieved = GetClipboardUTF8();
BOOST_CHECK_EQUAL( retrieved, kicadData );
}
}
BOOST_AUTO_TEST_CASE( SaveClipboard_WithMimeData_SExpressionRoundTrip )
{
SKIP_IF_HEADLESS();
// Test that S-expression data (like schematic content) round-trips correctly
// This simulates the actual copy/paste flow for schematic elements
std::string sExprData =
"(kicad_sch (version 20231120) (generator \"eeschema\")\n"
" (symbol (lib_id \"Device:R\") (at 100.33 50.8 0)\n"
" (property \"Reference\" \"R1\" (at 101.6 49.53 0))\n"
" (property \"Value\" \"10kΩ\" (at 101.6 52.07 0))\n"
" )\n"
")\n";
std::vector<CLIPBOARD_MIME_DATA> mimeData;
CLIPBOARD_MIME_DATA kicadEntry;
kicadEntry.m_mimeType = wxS( "application/kicad" );
kicadEntry.m_data.AppendData( sExprData.data(), sExprData.size() );
mimeData.push_back( kicadEntry );
bool result = SaveClipboard( sExprData, mimeData );
if( result )
{
std::string retrieved = GetClipboardUTF8();
// Verify exact byte-for-byte match for S-expression parsing
BOOST_CHECK_EQUAL( retrieved.size(), sExprData.size() );
BOOST_CHECK_EQUAL( retrieved, sExprData );
}
}
BOOST_AUTO_TEST_CASE( SaveClipboard_WithMimeData_EmptyDataSkipped )
{
SKIP_IF_HEADLESS();
// Test that entries with empty data are skipped without error
std::string textData = "Text with empty MIME entries";
std::vector<CLIPBOARD_MIME_DATA> mimeData;
// Add an entry with empty data - should be skipped
CLIPBOARD_MIME_DATA emptyEntry;
emptyEntry.m_mimeType = wxS( "application/empty" );
// Note: m_data is empty by default
mimeData.push_back( emptyEntry );
// Add a valid entry
CLIPBOARD_MIME_DATA kicadEntry;
kicadEntry.m_mimeType = wxS( "application/kicad" );
kicadEntry.m_data.AppendData( textData.data(), textData.size() );
mimeData.push_back( kicadEntry );
bool result = SaveClipboard( textData, mimeData );
if( result )
{
std::string retrieved = GetClipboardUTF8();
BOOST_CHECK_EQUAL( retrieved, textData );
}
}
BOOST_AUTO_TEST_CASE( SaveClipboard_WithMimeData_PngHandledAsBitmap )
{
SKIP_IF_HEADLESS();
// Test that PNG data is handled correctly (converted to bitmap format)
// Note: This test verifies the function doesn't crash with PNG data
// Full bitmap round-trip testing requires a display connection
std::string textData = "Text with PNG data";
// Create minimal valid PNG data (1x1 transparent pixel)
// PNG header + IHDR + IDAT + IEND
static const unsigned char minimalPng[] = {
0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A, // PNG signature
0x00, 0x00, 0x00, 0x0D, 0x49, 0x48, 0x44, 0x52, // IHDR chunk
0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x01, // 1x1 size
0x08, 0x06, 0x00, 0x00, 0x00, 0x1F, 0x15, 0xC4, // 8-bit RGBA
0x89, 0x00, 0x00, 0x00, 0x0A, 0x49, 0x44, 0x41, // IDAT chunk
0x54, 0x78, 0x9C, 0x63, 0x00, 0x01, 0x00, 0x00, // compressed data
0x05, 0x00, 0x01, 0x0D, 0x0A, 0x2D, 0xB4, 0x00, // checksum
0x00, 0x00, 0x00, 0x49, 0x45, 0x4E, 0x44, 0xAE, // IEND chunk
0x42, 0x60, 0x82 // IEND checksum
};
std::vector<CLIPBOARD_MIME_DATA> mimeData;
CLIPBOARD_MIME_DATA pngEntry;
pngEntry.m_mimeType = wxS( "image/png" );
pngEntry.m_data.AppendData( minimalPng, sizeof( minimalPng ) );
mimeData.push_back( pngEntry );
CLIPBOARD_MIME_DATA kicadEntry;
kicadEntry.m_mimeType = wxS( "application/kicad" );
kicadEntry.m_data.AppendData( textData.data(), textData.size() );
mimeData.push_back( kicadEntry );
// This should not crash or assert
bool result = SaveClipboard( textData, mimeData );
if( result )
{
// Verify we can still get the KiCad data back
std::string retrieved = GetClipboardUTF8();
BOOST_CHECK_EQUAL( retrieved, textData );
}
}
/**
* Tests for the dual-buffer alpha computation algorithm.
* This algorithm computes alpha by comparing renders on white vs black backgrounds:
* - On white: result_w = α*F + (1-α)*255
* - On black: result_b = α*F
* Therefore: α = 1 - (result_w - result_b)/255
* And: F = result_b / α (when α > 0)
*/
BOOST_AUTO_TEST_CASE( DualBufferAlpha_FullyOpaque )
{
// Test fully opaque pixels (alpha = 255)
// For a red pixel (255, 0, 0) with alpha=1:
// On white: (255, 255, 255) blended with (255, 0, 0) at α=1 -> (255, 0, 0)
// On black: (0, 0, 0) blended with (255, 0, 0) at α=1 -> (255, 0, 0)
int rW = 255, gW = 0, bW = 0; // Red on white
int rB = 255, gB = 0, bB = 0; // Red on black (same because fully opaque)
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 );
// Recover color
if( alpha > 0 )
{
int recoveredR = std::min( 255, rB * 255 / alpha );
int recoveredG = std::min( 255, gB * 255 / alpha );
int recoveredB = std::min( 255, bB * 255 / alpha );
BOOST_CHECK_EQUAL( recoveredR, 255 );
BOOST_CHECK_EQUAL( recoveredG, 0 );
BOOST_CHECK_EQUAL( recoveredB, 0 );
}
}
BOOST_AUTO_TEST_CASE( DualBufferAlpha_FullyTransparent )
{
// Test fully transparent pixels (alpha = 0)
// On white: just white (255, 255, 255)
// On black: just black (0, 0, 0)
int rW = 255, gW = 255, bW = 255; // White background
int rB = 0, gB = 0, bB = 0; // Black background
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 );
}
BOOST_AUTO_TEST_CASE( DualBufferAlpha_SemiTransparent )
{
// Test semi-transparent pixel (alpha = 0.5, i.e., 128)
// Foreground color: (100, 150, 200)
// On white: 0.5*F + 0.5*255 = 0.5*(100,150,200) + (127.5,127.5,127.5) = (177, 202, 227) approx
// On black: 0.5*F + 0.5*0 = 0.5*(100,150,200) = (50, 75, 100)
int rW = 177, gW = 202, bW = 227;
int rB = 50, gB = 75, bB = 100;
int diffR = rW - rB; // 127
int diffG = gW - gB; // 127
int diffB = bW - bB; // 127
int avgDiff = (diffR + diffG + diffB) / 3; // 127
int alpha = 255 - avgDiff; // 128
BOOST_CHECK_CLOSE( (double)alpha, 128.0, 1.0 ); // Allow 1% tolerance
// Recover color: F = black_result / α * 255
if( alpha > 0 )
{
int recoveredR = std::min( 255, rB * 255 / alpha );
int recoveredG = std::min( 255, gB * 255 / alpha );
int recoveredB = std::min( 255, bB * 255 / alpha );
BOOST_CHECK_CLOSE( (double)recoveredR, 99.0, 2.0 ); // Should be ~100
BOOST_CHECK_CLOSE( (double)recoveredG, 149.0, 2.0 ); // Should be ~150
BOOST_CHECK_CLOSE( (double)recoveredB, 199.0, 2.0 ); // Should be ~200
}
}
BOOST_AUTO_TEST_CASE( DualBufferAlpha_AntiAliasedEdge )
{
// Test anti-aliased edge with varying alpha
// Simulates a gray line with alpha gradient at edges
// High alpha region (α ≈ 0.9)
int rW1 = 128, gW1 = 128, bW1 = 128; // Gray on white (dimmed by 0.9 blend)
int rB1 = 115, gB1 = 115, bB1 = 115; // Gray on black
int diff1 = (rW1 - rB1 + gW1 - gB1 + bW1 - bB1) / 3;
int alpha1 = 255 - diff1;
BOOST_CHECK( alpha1 > 200 ); // High alpha
// Low alpha region at edge (α ≈ 0.3)
int rW2 = 210, gW2 = 210, bW2 = 210; // Mostly white with some gray
int rB2 = 38, gB2 = 38, bB2 = 38; // Mostly black with some gray
int diff2 = (rW2 - rB2 + gW2 - gB2 + bW2 - bB2) / 3;
int alpha2 = 255 - diff2;
BOOST_CHECK( alpha2 < 100 ); // Low alpha
BOOST_CHECK( alpha2 > 0 ); // But not fully transparent
}
BOOST_AUTO_TEST_CASE( BitmapSizeCalculation_MatchesViewScale )
{
// Test that bitmap size is correctly calculated from bbox and view scale
// This tests the formula: bitmapSize = bbox_IU * viewScale
// Simulate a 1000x500 IU bounding box at various zoom levels
int bboxWidth = 1000;
int bboxHeight = 500;
// At viewScale = 1.0 (1:1 zoom)
double viewScale1 = 1.0;
int bitmapWidth1 = (int)(bboxWidth * viewScale1 + 0.5);
int bitmapHeight1 = (int)(bboxHeight * viewScale1 + 0.5);
BOOST_CHECK_EQUAL( bitmapWidth1, 1000 );
BOOST_CHECK_EQUAL( bitmapHeight1, 500 );
// At viewScale = 2.0 (zoomed in 2x)
double viewScale2 = 2.0;
int bitmapWidth2 = (int)(bboxWidth * viewScale2 + 0.5);
int bitmapHeight2 = (int)(bboxHeight * viewScale2 + 0.5);
BOOST_CHECK_EQUAL( bitmapWidth2, 2000 );
BOOST_CHECK_EQUAL( bitmapHeight2, 1000 );
// At viewScale = 0.5 (zoomed out)
double viewScale3 = 0.5;
int bitmapWidth3 = (int)(bboxWidth * viewScale3 + 0.5);
int bitmapHeight3 = (int)(bboxHeight * viewScale3 + 0.5);
BOOST_CHECK_EQUAL( bitmapWidth3, 500 );
BOOST_CHECK_EQUAL( bitmapHeight3, 250 );
}
BOOST_AUTO_TEST_CASE( BitmapSizeCalculation_ClampToMaxSize )
{
// Test that bitmap size is clamped while preserving aspect ratio
const int maxBitmapSize = 4096;
// Large bbox that would exceed max size
int bboxWidth = 10000;
int bboxHeight = 5000;
double viewScale = 1.0;
int bitmapWidth = (int)(bboxWidth * viewScale + 0.5);
int bitmapHeight = (int)(bboxHeight * viewScale + 0.5);
// Apply clamping as in plotSelectionToPng
if( bitmapWidth > maxBitmapSize || bitmapHeight > maxBitmapSize )
{
double scaleDown = (double)maxBitmapSize / std::max( bitmapWidth, bitmapHeight );
bitmapWidth = (int)(bitmapWidth * scaleDown + 0.5);
bitmapHeight = (int)(bitmapHeight * scaleDown + 0.5);
viewScale *= scaleDown;
}
BOOST_CHECK( bitmapWidth <= maxBitmapSize );
BOOST_CHECK( bitmapHeight <= maxBitmapSize );
// Check aspect ratio is preserved (2:1)
BOOST_CHECK_CLOSE( (double)bitmapWidth / bitmapHeight, 2.0, 0.1 );
}
BOOST_AUTO_TEST_CASE( ZoomFactorCalculation_MatchesViewScale )
{
// Test the zoom factor calculation that maps view scale to GAL print scale
// Formula: zoomFactor = viewScale * inch2Iu / ppi
// where inch2Iu = 1000 * IU_PER_MILS (typically 10000 for schematic)
const double ppi = 96.0;
const double IU_PER_MILS = 10.0; // Typical for schematic
const double inch2Iu = 1000.0 * IU_PER_MILS;
// At viewScale = 1.0
double viewScale1 = 1.0;
double zoomFactor1 = viewScale1 * inch2Iu / ppi;
// This should give a zoom that maps content at the right size
BOOST_CHECK_CLOSE( zoomFactor1, 104.166666, 0.01 );
// At viewScale = 0.1 (zoomed out)
double viewScale2 = 0.1;
double zoomFactor2 = viewScale2 * inch2Iu / ppi;
BOOST_CHECK_CLOSE( zoomFactor2, 10.416666, 0.01 );
// Zoom factors should scale linearly with view scale
BOOST_CHECK_CLOSE( zoomFactor1 / zoomFactor2, viewScale1 / viewScale2, 0.01 );
}
BOOST_AUTO_TEST_CASE( PageSizeCalculation_MatchesBitmapForCentering )
{
// Test that page size is calculated to match the bitmap dimensions at target ppi.
// This is critical for proper centering of content in the output bitmap.
// Formula: pageSizeIn = bitmapSize / ppi (in inches)
//
// When page size matches bitmap, the GAL will render content centered.
// If page size were set to bbox size instead, content would be offset.
const double ppi = 96.0;
// Bitmap dimensions (in pixels)
int bitmapWidth = 800;
int bitmapHeight = 600;
// Page size should match bitmap at target ppi
double pageSizeInX = (double) bitmapWidth / ppi; // 8.333... inches
double pageSizeInY = (double) bitmapHeight / ppi; // 6.25 inches
// Verify the math
BOOST_CHECK_CLOSE( pageSizeInX, 800.0 / 96.0, 0.01 );
BOOST_CHECK_CLOSE( pageSizeInY, 600.0 / 96.0, 0.01 );
// At ppi, this page maps back to the bitmap size
double reconstructedWidth = pageSizeInX * ppi;
double reconstructedHeight = pageSizeInY * ppi;
BOOST_CHECK_CLOSE( reconstructedWidth, (double) bitmapWidth, 0.01 );
BOOST_CHECK_CLOSE( reconstructedHeight, (double) bitmapHeight, 0.01 );
// Test with different bitmap sizes to ensure formula is consistent
int bitmapWidth2 = 1920;
int bitmapHeight2 = 1080;
double pageSizeInX2 = (double) bitmapWidth2 / ppi;
double pageSizeInY2 = (double) bitmapHeight2 / ppi;
BOOST_CHECK_CLOSE( pageSizeInX2 * ppi, (double) bitmapWidth2, 0.01 );
BOOST_CHECK_CLOSE( pageSizeInY2 * ppi, (double) bitmapHeight2, 0.01 );
}
BOOST_AUTO_TEST_SUITE_END()