KiCad PCB EDA Suite
Loading...
Searching...
No Matches
test_pdf_unicode_plot.cpp
Go to the documentation of this file.
1/*
2 * This program source code file is part of KiCad, a free EDA CAD application.
3 *
4 * Copyright The KiCad Developers, see AUTHORS.TXT for contributors.
5 *
6 * This program is free software; you can redistribute it and/or
7 * modify it under the terms of the GNU General Public License
8 * as published by the Free Software Foundation; either version 2
9 * of the License, or (at your option) any later version.
10 *
11 * This program is distributed in the hope that it will be useful,
12 * but WITHOUT ANY WARRANTY; without even the implied warranty of
13 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 * GNU General Public License for more details.
15 *
16 * You should have received a copy of the GNU General Public License
17 * along with this program; if not, you may find one here:
18 * http://www.gnu.org/licenses/old-licenses/gpl-2.0.html
19 * or you may search the http://www.gnu.org website for the version 2 license,
20 * or you may write to the Free Software Foundation, Inc.,
21 * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA
22 */
23
24#include <boost/test/unit_test.hpp>
25
26#include <wx/filename.h>
27#include <wx/filefn.h>
28#include <wx/ffile.h>
29#include <wx/utils.h>
30#include <wx/image.h>
31#include <zlib.h>
32
34#include <advanced_config.h>
35#include <render_settings.h>
36#include <trigo.h>
37#include <font/font.h>
38#include <font/stroke_font.h>
41
42/* Test objective:
43 * Ensure PDF_PLOTTER can emit glyphs for ASCII plus some Cyrillic, Japanese and Chinese
44 * characters using stroke font fallback. We verify by checking resulting PDF file contains
45 * expected glyph names or UTF-16 hex sequences for those code points.
46 */
47
48BOOST_AUTO_TEST_SUITE( PDFUnicodePlot )
49
50static wxString getTempPdfPath( const wxString& name ) { return MakeTempPdfPath( name ); }
51
52// Comprehensive mapping test: emit all four style variants in a single PDF and verify that
53// every style's ToUnicode CMap contains expected codepoints (Cyrillic 041F, Japanese 65E5, Chinese 672C).
54BOOST_AUTO_TEST_CASE( PlotMultilingualAllStylesMappings )
55{
56 const std::string sampleUtf8 = "ABCDEF Привет 日本語 漢字";
57 wxString sample = wxString::FromUTF8( sampleUtf8.c_str() );
58 wxString pdfPath = getTempPdfPath( "kicad_pdf_unicode_allstyles" );
59
60 PDF_PLOTTER plotter;
61 SIMPLE_RENDER_SETTINGS renderSettings;
62
63 plotter.SetRenderSettings( &renderSettings );
64 BOOST_REQUIRE( plotter.OpenFile( pdfPath ) );
65 plotter.SetViewport( VECTOR2I(0,0), 1.0, 1.0, false );
66 BOOST_REQUIRE( plotter.StartPlot( wxT("1"), wxT("TestPage") ) );
67
68 auto emitStyle = [&]( bool bold, bool italic, int yoff )
69 {
70 TEXT_ATTRIBUTES attrs = BuildTextAttributes( 3000, 300, bold, italic );
71 auto strokeFont = LoadStrokeFontUnique();
72 KIFONT::METRICS metrics;
73
74 plotter.PlotText( VECTOR2I( 50000, 60000 - yoff ), COLOR4D( 0, 0, 0, 1 ), sample, attrs,
75 strokeFont.get(), metrics );
76 };
77
78 emitStyle( false, false, 0 ); // normal
79 emitStyle( true, false, 8000 ); // bold
80 emitStyle( false, true, 16000 ); // italic
81 emitStyle( true, true, 24000 ); // bold-italic
82
83 plotter.EndPlot();
84
85 // Read entire PDF (may have compression). We'll search each Type3 font object's preceding
86 // name to separate CMaps logically.
87 std::string buffer; BOOST_REQUIRE( ReadPdfWithDecompressedStreams( pdfPath, buffer ) );
88 BOOST_CHECK( buffer.rfind( "%PDF", 0 ) == 0 );
89
90 // Count how many distinct KiCadStrokeCMap names present; expect at least 4 (one per style).
91 int cmapCount = CountOccurrences( buffer, "/CMapName /KiCadStrokeCMap" );
92
93 BOOST_CHECK_MESSAGE( cmapCount >= 4, "Expected at least 4 CMaps (got " << cmapCount << ")" );
94
95 auto requireAll = [&]( const char* codeHex, const char* label ) {
96 int occurrences = CountOccurrences( buffer, codeHex );
97 BOOST_CHECK_MESSAGE( occurrences >= 4, "Codepoint " << label << " (" << codeHex
98 << ") expected in all 4 styles; found " << occurrences );
99 };
100
101 requireAll( "041F", "Cyrillic PE" );
102 requireAll( "65E5", "Kanji 日" );
103 requireAll( "672C", "Kanji 本" );
104
105 MaybeRemoveFile( pdfPath );
106}
107
108BOOST_AUTO_TEST_CASE( PlotMultilingualText )
109{
110 // UTF-8 sample with Latin, Cyrillic, Japanese, Chinese (shared Han) characters.
111 const std::string sampleUtf8 = "ABCDEF Привет 日本語 漢字";
112 wxString sample = wxString::FromUTF8( sampleUtf8.c_str() );
113
114 wxString pdfPath = getTempPdfPath( "kicad_pdf_unicode" );
115
116 PDF_PLOTTER plotter;
117
118 // Force uncompressed PDF streams via environment so we can directly search for
119 // unicode hex strings in the output (otherwise they are Flate compressed).
120 // Do not force debug writer; allow normal compression so page content is valid.
121 // The plotter expects non-null render settings for default pen width and font queries.
122 // Provide a minimal concrete RENDER_SETTINGS implementation for the plotter.
123 SIMPLE_RENDER_SETTINGS renderSettings; plotter.SetRenderSettings( &renderSettings );
124
125 // Minimal viewport and plot setup. Use 1 IU per decimil so internal coordinates are small
126 // and resulting translation keeps text inside the page for rasterization.
127 BOOST_REQUIRE( plotter.OpenFile( pdfPath ) );
128 plotter.SetViewport( VECTOR2I(0,0), 1.0, 1.0, false );
129 // StartPlot opens first page stream internally; use simple page number
130 BOOST_REQUIRE( plotter.StartPlot( wxT("1"), wxT("TestPage") ) );
131
132 TEXT_ATTRIBUTES attrs = BuildTextAttributes( 3000, 300, false, false );
133 auto strokeFont = LoadStrokeFontUnique();
134 KIFONT::METRICS metrics; // not used for stroke fallback
135
136 // Plot near lower-left inside the page.
137 // Place text near the top of the page in internal units so after the 0.0072 scale it
138 // appears well within the MediaBox. Empirically m_paperSize.y ~ 116k internal units.
139 plotter.PlotText( VECTOR2I( 50000, 60000 ), COLOR4D( 0.0, 0.0, 0.0, 1.0 ), sample, attrs,
140 strokeFont.get(), metrics );
141
142 plotter.EndPlot();
143
144 // Read file back and check for expected UTF-16 hex encodings for some code points
145 // We expect CMap to contain mappings. E.g. '041F' (Cyrillic capital Pe), '65E5'(日), '672C'(本).
146 std::string buffer2; BOOST_REQUIRE( ReadPdfWithDecompressedStreams( pdfPath, buffer2 ) );
147 auto contains = [&]( const char* needle ) { return PdfContains( buffer2, needle ); };
148
149 BOOST_CHECK_MESSAGE( contains( "041F" ), "Missing Cyrillic glyph mapping (041F)" );
150 BOOST_CHECK_MESSAGE( contains( "0420" ) || contains( "0440" ), "Missing Cyrillic glyph mapping (0420/0440)" );
151 BOOST_CHECK_MESSAGE( contains( "65E5" ), "Missing Japanese Kanji glyph mapping (65E5)" );
152 BOOST_CHECK_MESSAGE( contains( "672C" ), "Missing Japanese Kanji glyph mapping (672C)" );
153 BOOST_CHECK_MESSAGE( contains( "6F22" ) || contains( "6漢" ), "Expect Chinese Han character mapping (6F22 / 漢)" );
154
155 // Cleanup temp file (unless debugging requested)
156 // Optional: rasterize PDF to image (requires poppler 'pdftoppm').
157 // We treat absence of the tool as a skipped sub-check rather than a failure.
158 {
159 long darkPixels = 0;
160 if( RasterizePdfCountDark( pdfPath, 72, 240, darkPixels ) )
161 {
162 BOOST_CHECK_MESSAGE( darkPixels > 200,
163 "Rasterized PDF appears blank or too sparse (" << darkPixels
164 << " dark pixels)" );
165 }
166 else
167 {
168 BOOST_TEST_MESSAGE( "pdftoppm not available or failed; skipping raster validation" );
169 }
170 }
171
172 MaybeRemoveFile( pdfPath );
173}
174
175BOOST_AUTO_TEST_CASE( PlotMultilingualTextBold )
176{
177 const std::string sampleUtf8 = "ABCDEF Привет 日本語 漢字";
178 wxString sample = wxString::FromUTF8( sampleUtf8.c_str() );
179
180 wxString pdfPath = getTempPdfPath( "kicad_pdf_unicode_bold" );
181
182 PDF_PLOTTER plotter;
183 SIMPLE_RENDER_SETTINGS renderSettings;
184 plotter.SetRenderSettings( &renderSettings );
185 BOOST_REQUIRE( plotter.OpenFile( pdfPath ) );
186 plotter.SetViewport( VECTOR2I(0,0), 1.0, 1.0, false );
187 BOOST_REQUIRE( plotter.StartPlot( wxT("1"), wxT("TestPage") ) );
188
189 TEXT_ATTRIBUTES attrs = BuildTextAttributes( 3000, 300, true, false );
190 auto strokeFont = LoadStrokeFontUnique();
191 KIFONT::METRICS metrics;
192 plotter.PlotText( VECTOR2I( 50000, 60000 ), COLOR4D( 0.0, 0.0, 0.0, 1.0 ), sample, attrs,
193 strokeFont.get(), metrics );
194 plotter.EndPlot();
195 std::string buffer3; BOOST_REQUIRE( ReadPdfWithDecompressedStreams( pdfPath, buffer3 ) );
196 auto contains = [&]( const char* needle ) { return PdfContains( buffer3, needle ); };
197 BOOST_CHECK_MESSAGE( contains( "041F" ), "Missing Cyrillic glyph mapping (bold 041F)" );
198 BOOST_CHECK_MESSAGE( contains( "65E5" ), "Missing Japanese glyph mapping (bold 65E5)" );
199
200 MaybeRemoveFile( pdfPath );
201}
202
203BOOST_AUTO_TEST_CASE( PlotMultilingualTextItalic )
204{
205 const std::string sampleUtf8 = "ABCDEF Привет 日本語 漢字";
206 wxString sample = wxString::FromUTF8( sampleUtf8.c_str() );
207 wxString pdfPath = getTempPdfPath( "kicad_pdf_unicode_italic" );
208 PDF_PLOTTER plotter;
209 SIMPLE_RENDER_SETTINGS renderSettings;
210
211 plotter.SetRenderSettings( &renderSettings );
212 BOOST_REQUIRE( plotter.OpenFile( pdfPath ) );
213 plotter.SetViewport( VECTOR2I(0,0), 1.0, 1.0, false );
214 BOOST_REQUIRE( plotter.StartPlot( wxT( "1" ), wxT( "TestPage" ) ) );
215 TEXT_ATTRIBUTES attrs = BuildTextAttributes( 3000, 300, false, true );
216 auto strokeFont = LoadStrokeFontUnique();
217 KIFONT::METRICS metrics;
218 plotter.PlotText( VECTOR2I( 50000, 60000 ), COLOR4D( 0, 0, 0, 1 ), sample, attrs, strokeFont.get(), metrics );
219 plotter.EndPlot();
220
221 std::string buffer4; BOOST_REQUIRE( ReadPdfWithDecompressedStreams( pdfPath, buffer4 ) );
222 auto contains = [&]( const char* n ) { return PdfContains( buffer4, n ); };
223 BOOST_CHECK_MESSAGE( contains( "041F" ), "Missing Cyrillic glyph mapping (italic 041F)" );
224 BOOST_CHECK_MESSAGE( contains( "65E5" ), "Missing Japanese glyph mapping (italic 65E5)" );
225 MaybeRemoveFile( pdfPath );
226}
227
228BOOST_AUTO_TEST_CASE( PlotMultilingualTextBoldItalic )
229{
230 const std::string sampleUtf8 = "ABCDEF Привет 日本語 漢字";
231 wxString sample = wxString::FromUTF8( sampleUtf8.c_str() );
232 wxString pdfPath = getTempPdfPath( "kicad_pdf_unicode_bolditalic" );
233 PDF_PLOTTER plotter;
234 SIMPLE_RENDER_SETTINGS renderSettings;
235
236 plotter.SetRenderSettings( &renderSettings );
237 BOOST_REQUIRE( plotter.OpenFile( pdfPath ) );
238
239 plotter.SetViewport( VECTOR2I( 0, 0 ), 1.0, 1.0, false );
240 BOOST_REQUIRE( plotter.StartPlot( wxT( "1" ), wxT( "TestPage" ) ) );
241
242 TEXT_ATTRIBUTES attrs = BuildTextAttributes( 3000, 300, true, true );
243 auto strokeFont = LoadStrokeFontUnique();
244 KIFONT::METRICS metrics;
245
246 plotter.PlotText( VECTOR2I( 50000, 60000 ), COLOR4D( 0, 0, 0, 1 ), sample, attrs, strokeFont.get(), metrics );
247 plotter.EndPlot();
248
249 std::string buffer5; BOOST_REQUIRE( ReadPdfWithDecompressedStreams( pdfPath, buffer5 ) );
250 auto contains = [&]( const char* n ) { return PdfContains( buffer5, n ); };
251 BOOST_CHECK_MESSAGE( contains( "041F" ), "Missing Cyrillic glyph mapping (bold-italic 041F)" );
252 BOOST_CHECK_MESSAGE( contains( "65E5" ), "Missing Japanese glyph mapping (bold-italic 65E5)" );
253 MaybeRemoveFile( pdfPath );
254}
255
256// Verify that d1 bounding boxes account for X offset and stroke width so PDF viewers don't
257// clip the rendered glyphs. Regression test for gitlab.com/kicad/code/kicad/-/issues/23621.
258BOOST_AUTO_TEST_CASE( GlyphBBoxIncludesOffsetAndStrokeWidth )
259{
260 const std::string sampleUtf8 = "MW";
261 wxString sample = wxString::FromUTF8( sampleUtf8.c_str() );
262 wxString pdfPath = getTempPdfPath( "kicad_pdf_bbox_check" );
263
264 PDF_PLOTTER plotter;
265 SIMPLE_RENDER_SETTINGS renderSettings;
266
267 plotter.SetRenderSettings( &renderSettings );
268 BOOST_REQUIRE( plotter.OpenFile( pdfPath ) );
269 plotter.SetViewport( VECTOR2I( 0, 0 ), 1.0, 1.0, false );
270 BOOST_REQUIRE( plotter.StartPlot( wxT( "1" ), wxT( "BBoxTest" ) ) );
271
272 TEXT_ATTRIBUTES attrs = BuildTextAttributes( 3000, 300, false, false );
273 auto strokeFont = LoadStrokeFontUnique();
274 KIFONT::METRICS metrics;
275
276 plotter.PlotText( VECTOR2I( 50000, 60000 ), COLOR4D( 0, 0, 0, 1 ), sample, attrs,
277 strokeFont.get(), metrics );
278 plotter.EndPlot();
279
280 std::string buffer;
281 BOOST_REQUIRE( ReadPdfWithDecompressedStreams( pdfPath, buffer ) );
282
283 // Parse d1 operators: format is "width 0 minX minY maxX maxY d1"
284 // Verify that no glyph has a minX of exactly 0 (would mean X offset was not applied) and
285 // that the bbox extends beyond the stroke center coordinates by at least half the stroke
286 // width.
287 const ADVANCED_CFG& cfg = ADVANCED_CFG::GetCfg();
288 double unitsPerEm = 1000.0;
289 double expectedXOffset = cfg.m_PDFStrokeFontXOffset * unitsPerEm;
290 double expectedHalfStroke = unitsPerEm * cfg.m_PDFStrokeFontWidthFactor / 2.0;
291
292 // Find all d1 operators (skip .notdef which has all-zero bbox)
293 std::string::size_type pos = 0;
294 int checkedGlyphs = 0;
295
296 while( ( pos = buffer.find( " d1 ", pos ) ) != std::string::npos )
297 {
298 // Walk backwards to find the start of the d1 line
299 std::string::size_type lineStart = buffer.rfind( '\n', pos );
300
301 if( lineStart == std::string::npos )
302 lineStart = 0;
303 else
304 lineStart++;
305
306 std::string d1Line = buffer.substr( lineStart, pos + 3 - lineStart );
307
308 double width, wy, minX, minY, maxX, maxY;
309
310 if( sscanf( d1Line.c_str(), "%lf %lf %lf %lf %lf %lf", &width, &wy, &minX, &minY,
311 &maxX, &maxY ) == 6 )
312 {
313 // Skip .notdef (all zeros)
314 if( width == 0.0 && maxX == 0.0 )
315 {
316 pos += 4;
317 continue;
318 }
319
320 // The bbox minX should be shifted by the X offset minus half stroke width.
321 // With default offset 0.1 and stroke factor 0.12, minX should be around
322 // 0.1*1000 - 0.12*1000/2 = 100 - 60 = 40, not 0.
323 BOOST_CHECK_MESSAGE( minX < -expectedHalfStroke + 1.0 || minX > 0.1,
324 "Glyph bbox minX (" << minX
325 << ") suggests X offset or stroke padding is missing" );
326
327 // maxX must exceed the advance width to account for offset + stroke padding
328 BOOST_CHECK_MESSAGE( maxX > width,
329 "Glyph bbox maxX (" << maxX << ") must exceed advance width ("
330 << width << ") to prevent clipping" );
331
332 checkedGlyphs++;
333 }
334
335 pos += 4;
336 }
337
338 BOOST_CHECK_MESSAGE( checkedGlyphs >= 2,
339 "Expected at least 2 non-notdef glyphs, found " << checkedGlyphs );
340
341 MaybeRemoveFile( pdfPath );
342}
343
344// Test Y offset bounding box fix: ensure characters are not clipped when Y offset is applied
345BOOST_AUTO_TEST_CASE( PlotMultilingualTextWithYOffset )
346{
347 // Temporarily modify the Y offset configuration
348 ADVANCED_CFG& cfg = const_cast<ADVANCED_CFG&>( ADVANCED_CFG::GetCfg() );
349 double originalOffset = cfg.m_PDFStrokeFontYOffset;
350 cfg.m_PDFStrokeFontYOffset = 0.2; // 20% of EM unit offset upward
351
352 const std::string sampleUtf8 = "Yg Test ñ"; // characters with ascenders and descenders
353 wxString sample = wxString::FromUTF8( sampleUtf8.c_str() );
354 wxString pdfPath = getTempPdfPath( "kicad_pdf_unicode_yoffset" );
355
356 PDF_PLOTTER plotter;
357 SIMPLE_RENDER_SETTINGS renderSettings;
358
359 plotter.SetRenderSettings( &renderSettings );
360 BOOST_REQUIRE( plotter.OpenFile( pdfPath ) );
361 plotter.SetViewport( VECTOR2I( 0, 0 ), 1.0, 1.0, false );
362 BOOST_REQUIRE( plotter.StartPlot( wxT( "1" ), wxT( "TestPage" ) ) );
363
364 TEXT_ATTRIBUTES attrs = BuildTextAttributes( 4000, 400, false, false );
365 auto strokeFont = LoadStrokeFontUnique();
366 KIFONT::METRICS metrics;
367 plotter.PlotText( VECTOR2I( 50000, 60000 ), COLOR4D( 0, 0, 0, 1 ), sample, attrs, strokeFont.get(), metrics );
368 plotter.EndPlot();
369
370
371 // Restore original Y offset
372 cfg.m_PDFStrokeFontYOffset = originalOffset;
373
374 // Basic PDF validation and decompression
375 std::string buffer6; BOOST_REQUIRE( ReadPdfWithDecompressedStreams( pdfPath, buffer6 ) );
376 BOOST_CHECK( buffer6.rfind( "%PDF", 0 ) == 0 );
377
378 // Check that bounding boxes exist and are reasonable (not clipped)
379 // Look for d1 operators which specify character bounding boxes
380 BOOST_CHECK_MESSAGE( buffer6.find( "d1" ) != std::string::npos,
381 "PDF should contain d1 operators for glyph bounding boxes" );
382
383 MaybeRemoveFile( pdfPath );
384}
385
386BOOST_AUTO_TEST_CASE( PlotOutlineFontEmbedding )
387{
388 wxString pdfPath = getTempPdfPath( "kicad_pdf_outline_font" );
389
390 // Locate test font file (Noto Sans) in test resources
391 wxFileName fontFile( KI_TEST::GetTestDataRootDir() );
392 fontFile.RemoveLastDir();
393 fontFile.AppendDir( wxT( "resources" ) );
394 fontFile.AppendDir( wxT( "fonts" ) );
395 fontFile.SetFullName( wxT( "NotoSans-Regular.ttf" ) );
396 wxString fontPath = fontFile.GetFullPath();
397
398 BOOST_REQUIRE( wxFileExists( fontPath ) );
399
400 PDF_PLOTTER plotter;
401 SIMPLE_RENDER_SETTINGS renderSettings;
402
403 plotter.SetRenderSettings( &renderSettings );
404 BOOST_REQUIRE( plotter.OpenFile( pdfPath ) );
405 plotter.SetViewport( VECTOR2I(0,0), 1.0, 1.0, false );
406 BOOST_REQUIRE( plotter.StartPlot( wxT("1"), wxT("OutlineFont") ) );
407
408 TEXT_ATTRIBUTES attrs = BuildTextAttributes( 4000, 0, false, false );
409
410 std::vector<wxString> embeddedFonts;
411 embeddedFonts.push_back( fontPath );
412
413 KIFONT::FONT* outlineFont = KIFONT::FONT::GetFont( wxT( "Noto Sans" ), false, false, &embeddedFonts );
414 KIFONT::METRICS metrics;
415
416 wxString sample = wxString::FromUTF8( "Outline café" );
417
418 plotter.PlotText( VECTOR2I( 42000, 52000 ), COLOR4D( 0, 0, 0, 1 ), sample, attrs,
419 outlineFont, metrics );
420
421 plotter.EndPlot();
422
423 wxFFile file( pdfPath, "rb" );
424 BOOST_REQUIRE( file.IsOpened() );
425 wxFileOffset len = file.Length();
426 std::string buffer; buffer.resize( (size_t) len ); file.Read( buffer.data(), len );
427 BOOST_CHECK( buffer.rfind( "%PDF", 0 ) == 0 );
428
429 auto appendDecompressed = [&]() { std::string tmp; ReadPdfWithDecompressedStreams( pdfPath, tmp ); buffer.swap( tmp ); };
430 appendDecompressed();
431
432 BOOST_CHECK_MESSAGE( buffer.find( "/CIDFontType2" ) != std::string::npos,
433 "Expected CIDFontType2 descendant font" );
434 BOOST_CHECK_MESSAGE( buffer.find( "/FontFile2" ) != std::string::npos,
435 "Embedded outline font should include FontFile2 stream" );
436 BOOST_CHECK_MESSAGE( buffer.find( "AAAAAA+Noto-Sans" ) != std::string::npos,
437 "BaseFont should reference Noto Sans subset" );
438 BOOST_CHECK_MESSAGE( buffer.find( "00E9" ) != std::string::npos,
439 "ToUnicode map should include Latin character with accent" );
440 BOOST_CHECK_MESSAGE( buffer.find( "/KiCadOutline" ) != std::string::npos,
441 "Outline font resource entry missing" );
442
443 // Optional: rasterize PDF to image (requires poppler 'pdftoppm').
444 // We treat absence of the tool as a skipped sub-check rather than a failure.
445 {
446 wxString rasterBase = wxFileName::CreateTempFileName( wxT("kicad_pdf_raster") );
447 wxString cmd = wxString::Format( wxT("pdftoppm -r 72 -singlefile -png \"%s\" \"%s\""),
448 pdfPath, rasterBase );
449
450 int ret = wxExecute( cmd, wxEXEC_SYNC );
451
452 if( ret == 0 )
453 {
454 wxString pngPath = rasterBase + wxT(".png");
455
456 if( wxFileExists( pngPath ) )
457 {
458 // Ensure PNG handler is available
459 if( !wxImage::FindHandler( wxBITMAP_TYPE_PNG ) )
460 wxImage::AddHandler( new wxPNGHandler );
461
462 wxImage img( pngPath );
463 BOOST_REQUIRE_MESSAGE( img.IsOk(), "Failed to load rasterized PDF image" );
464
465 long darkPixels = 0;
466 int w = img.GetWidth();
467 int h = img.GetHeight();
468
469 for( int y = 0; y < h; ++y )
470 {
471 for( int x = 0; x < w; ++x )
472 {
473 unsigned char r = img.GetRed( x, y );
474 unsigned char g = img.GetGreen( x, y );
475 unsigned char b = img.GetBlue( x, y );
476
477 // Count any non-near-white pixel as drawn content
478 if( r < 240 || g < 240 || b < 240 )
479 ++darkPixels;
480 }
481 }
482
483 // Demand at least 100 non-white pixels to consider outline font rendered correctly.
484 // This threshold is lower than stroke font since outline fonts may render differently.
485 BOOST_CHECK_MESSAGE( darkPixels > 100,
486 "Rasterized PDF appears blank or too sparse (" << darkPixels
487 << " dark pixels). Outline font may not be rendering correctly." );
488
489 // Housekeeping
490 wxRemoveFile( pngPath );
491 }
492 else
493 {
494 BOOST_TEST_MESSAGE( "pdftoppm succeeded but PNG output missing; skipping raster validation" );
495 }
496 }
497 else
498 {
499 BOOST_TEST_MESSAGE( "pdftoppm not available or failed; skipping raster validation" );
500 }
501 }
502
503 MaybeRemoveFile( pdfPath );
504}
505
506// Test tab handling in PDF text output (issue #22606).
507// When text contains tab characters, each tab should advance to the next tab stop.
508// We verify that text with tabs produces different glyph positions than without tabs.
509BOOST_AUTO_TEST_CASE( PlotTextWithTabs )
510{
511 wxString pdfPath = getTempPdfPath( "kicad_pdf_tabs" );
512
513 PDF_PLOTTER plotter;
514 SIMPLE_RENDER_SETTINGS renderSettings;
515
516 plotter.SetRenderSettings( &renderSettings );
517 BOOST_REQUIRE( plotter.OpenFile( pdfPath ) );
518 plotter.SetViewport( VECTOR2I( 0, 0 ), 1.0, 1.0, false );
519 BOOST_REQUIRE( plotter.StartPlot( wxT( "1" ), wxT( "TabTest" ) ) );
520
521 TEXT_ATTRIBUTES attrs = BuildTextAttributes( 3000, 300, false, false );
522 auto strokeFont = LoadStrokeFontUnique();
523 KIFONT::METRICS metrics;
524
525 wxString textWithTab = wxT( "Before\tAfter" );
526 wxString textWithoutTab = wxT( "BeforeAfter" );
527
528 plotter.PlotText( VECTOR2I( 50000, 60000 ), COLOR4D( 0, 0, 0, 1 ), textWithTab, attrs,
529 strokeFont.get(), metrics );
530 plotter.PlotText( VECTOR2I( 50000, 50000 ), COLOR4D( 0, 0, 0, 1 ), textWithoutTab, attrs,
531 strokeFont.get(), metrics );
532
533 plotter.EndPlot();
534
535 std::string buffer;
536 BOOST_REQUIRE( ReadPdfWithDecompressedStreams( pdfPath, buffer ) );
537 BOOST_CHECK( buffer.rfind( "%PDF", 0 ) == 0 );
538
539 // The PDF should contain text content. Tabs should not produce visible tab glyphs but should
540 // create proper spacing. We verify the PDF is valid and contains our text characters.
541 BOOST_CHECK_MESSAGE( PdfContains( buffer, "0041" ) || PdfContains( buffer, "A" ),
542 "PDF should contain 'A' from 'After'" );
543
544 MaybeRemoveFile( pdfPath );
545}
546
const char * name
static const ADVANCED_CFG & GetCfg()
Get the singleton instance's config, which is shared by all consumers.
FONT is an abstract base class for both outline and stroke fonts.
Definition font.h:98
static FONT * GetFont(const wxString &aFontName=wxEmptyString, bool aBold=false, bool aItalic=false, const std::vector< wxString > *aEmbeddedFiles=nullptr, bool aForDrawingSheet=false)
Definition font.cpp:147
A color representation with 4 components: red, green, blue, alpha.
Definition color4d.h:105
virtual bool EndPlot() override
virtual bool OpenFile(const wxString &aFullFilename) override
Open or create the plot file aFullFilename.
virtual bool StartPlot(const wxString &aPageNumber) override
The PDF engine supports multiple pages; the first one is opened 'for free' the following are to be cl...
virtual void SetViewport(const VECTOR2I &aOffset, double aIusPerDecimil, double aScale, bool aMirror) override
PDF can have multiple pages, so SetPageSettings can be called with the outputFile open (but not insid...
virtual void PlotText(const VECTOR2I &aPos, const COLOR4D &aColor, const wxString &aText, const TEXT_ATTRIBUTES &aAttributes, KIFONT::FONT *aFont, const KIFONT::METRICS &aFontMetrics, void *aData=nullptr) override
void SetRenderSettings(RENDER_SETTINGS *aSettings)
Definition plotter.h:166
Minimal concrete render settings suitable for plotters in tests.
double m_PDFStrokeFontXOffset
Horizontal offset factor applied to stroke font glyph coordinates (in EM units) after to compensate m...
double m_PDFStrokeFontYOffset
Vertical offset factor applied to stroke font glyph coordinates (in EM units) after Y inversion to co...
double m_PDFStrokeFontWidthFactor
Stroke font line width factor relative to EM size for PDF stroke fonts.
std::string GetTestDataRootDir()
int CountOccurrences(const std::string &aHaystack, const std::string &aNeedle)
Count occurrences of a substring in a string (overlapping allowed).
bool PdfContains(const std::string &aBuffer, const char *aNeedle)
Convenience contains check.
bool ReadPdfWithDecompressedStreams(const wxString &aPdfPath, std::string &aOutBuffer)
Read a PDF file and append best-effort decompressed contents of any Flate streams to the returned buf...
std::unique_ptr< KIFONT::STROKE_FONT > LoadStrokeFontUnique()
Load the default stroke font and return a unique_ptr for RAII deletion.
TEXT_ATTRIBUTES BuildTextAttributes(int aSizeIu=3000, int aStrokeWidth=300, bool aBold=false, bool aItalic=false)
Build a commonly used set of text attributes for plotting text in tests.
bool RasterizePdfCountDark(const wxString &aPdfPath, int aDpi, int aNearWhiteThresh, long &aOutDarkPixels)
Rasterize a PDF page to PNG using pdftoppm if available and count non-near-white pixels.
wxString MakeTempPdfPath(const wxString &aPrefix)
Make a temporary file path with .pdf extension using a given prefix.
void MaybeRemoveFile(const wxString &aPath, const wxString &aEnvVar=wxT("KICAD_KEEP_TEST_PDF"))
Remove a file unless the given environment variable is set (defaults to KICAD_KEEP_TEST_PDF).
Plotting engines similar to ps (PostScript, Gerber, svg)
BOOST_AUTO_TEST_SUITE(CadstarPartParser)
BOOST_REQUIRE(intersection.has_value()==c.ExpectedIntersection.has_value())
BOOST_AUTO_TEST_SUITE_END()
BOOST_AUTO_TEST_CASE(PlotMultilingualAllStylesMappings)
static wxString getTempPdfPath(const wxString &name)
BOOST_CHECK_MESSAGE(totalMismatches==0, std::to_string(totalMismatches)+" board(s) with strategy disagreements")
BOOST_TEST_MESSAGE("\n=== Real-World Polygon PIP Benchmark ===\n"<< formatTable(table))
VECTOR2< int32_t > VECTOR2I
Definition vector2d.h:687