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, see <https://www.gnu.org/licenses/>.
18 */
19
20#include <boost/test/unit_test.hpp>
21
22#include <wx/filename.h>
23#include <wx/filefn.h>
24#include <wx/ffile.h>
25#include <wx/utils.h>
26#include <wx/image.h>
27#include <zlib.h>
28
30#include <advanced_config.h>
31#include <render_settings.h>
32#include <trigo.h>
33#include <math/util.h>
34#include <font/font.h>
35#include <font/stroke_font.h>
36#include <qa_utils/file_utils.h>
39
40/* Test objective:
41 * Ensure PDF_PLOTTER can emit glyphs for ASCII plus some Cyrillic, Japanese and Chinese
42 * characters using stroke font fallback. We verify by checking resulting PDF file contains
43 * expected glyph names or UTF-16 hex sequences for those code points.
44 */
45
46BOOST_AUTO_TEST_SUITE( PDFUnicodePlot )
47
48// Comprehensive mapping test: emit all four style variants in a single PDF and verify that
49// every style's ToUnicode CMap contains expected codepoints (Cyrillic 041F, Japanese 65E5, Chinese 672C).
50BOOST_AUTO_TEST_CASE( PlotMultilingualAllStylesMappings )
51{
52 const std::string sampleUtf8 = "ABCDEF Привет 日本語 漢字";
53 wxString sample = wxString::FromUTF8( sampleUtf8.c_str() );
54 KI_TEST::SCOPED_TEMP_DIR tempDir( "kicad_pdf_unicode_allstyles" );
55 const wxString pdfPath = tempDir.CreateChildFileStr( "output.pdf" );
56
57 PDF_PLOTTER plotter;
58 SIMPLE_RENDER_SETTINGS renderSettings;
59
60 plotter.SetRenderSettings( &renderSettings );
61 BOOST_REQUIRE( plotter.OpenFile( pdfPath ) );
62 plotter.SetViewport( VECTOR2I(0,0), 1.0, 1.0, false );
63 BOOST_REQUIRE( plotter.StartPlot( wxT("1"), wxT("TestPage") ) );
64
65 auto emitStyle = [&]( bool bold, bool italic, int yoff )
66 {
67 TEXT_ATTRIBUTES attrs = BuildTextAttributes( 3000, 300, bold, italic );
68 auto strokeFont = LoadStrokeFontUnique();
69 KIFONT::METRICS metrics;
70
71 plotter.PlotText( VECTOR2I( 50000, 60000 - yoff ), COLOR4D( 0, 0, 0, 1 ), sample, attrs,
72 strokeFont.get(), metrics );
73 };
74
75 emitStyle( false, false, 0 ); // normal
76 emitStyle( true, false, 8000 ); // bold
77 emitStyle( false, true, 16000 ); // italic
78 emitStyle( true, true, 24000 ); // bold-italic
79
80 plotter.EndPlot();
81
82 // Read entire PDF (may have compression). We'll search each Type3 font object's preceding
83 // name to separate CMaps logically.
84 std::string buffer; BOOST_REQUIRE( ReadPdfWithDecompressedStreams( pdfPath, buffer ) );
85 BOOST_CHECK( buffer.rfind( "%PDF", 0 ) == 0 );
86
87 // Count how many distinct KiCadStrokeCMap names present; expect at least 4 (one per style).
88 int cmapCount = CountOccurrences( buffer, "/CMapName /KiCadStrokeCMap" );
89
90 BOOST_CHECK_MESSAGE( cmapCount >= 4, "Expected at least 4 CMaps (got " << cmapCount << ")" );
91
92 auto requireAll = [&]( const char* codeHex, const char* label ) {
93 int occurrences = CountOccurrences( buffer, codeHex );
94 BOOST_CHECK_MESSAGE( occurrences >= 4, "Codepoint " << label << " (" << codeHex
95 << ") expected in all 4 styles; found " << occurrences );
96 };
97
98 requireAll( "041F", "Cyrillic PE" );
99 requireAll( "65E5", "Kanji 日" );
100 requireAll( "672C", "Kanji 本" );
101}
102
103BOOST_AUTO_TEST_CASE( PlotMultilingualText )
104{
105 // UTF-8 sample with Latin, Cyrillic, Japanese, Chinese (shared Han) characters.
106 const std::string sampleUtf8 = "ABCDEF Привет 日本語 漢字";
107 wxString sample = wxString::FromUTF8( sampleUtf8.c_str() );
108
109 KI_TEST::SCOPED_TEMP_DIR tempDir( "kicad_pdf_unicode" );
110 const wxString pdfPath = tempDir.CreateChildFileStr( "output.pdf" );
111
112 PDF_PLOTTER plotter;
113
114 // Force uncompressed PDF streams via environment so we can directly search for
115 // unicode hex strings in the output (otherwise they are Flate compressed).
116 // Do not force debug writer; allow normal compression so page content is valid.
117 // The plotter expects non-null render settings for default pen width and font queries.
118 // Provide a minimal concrete RENDER_SETTINGS implementation for the plotter.
119 SIMPLE_RENDER_SETTINGS renderSettings; plotter.SetRenderSettings( &renderSettings );
120
121 // Minimal viewport and plot setup. Use 1 IU per decimil so internal coordinates are small
122 // and resulting translation keeps text inside the page for rasterization.
123 BOOST_REQUIRE( plotter.OpenFile( pdfPath ) );
124 plotter.SetViewport( VECTOR2I(0,0), 1.0, 1.0, false );
125 // StartPlot opens first page stream internally; use simple page number
126 BOOST_REQUIRE( plotter.StartPlot( wxT("1"), wxT("TestPage") ) );
127
128 TEXT_ATTRIBUTES attrs = BuildTextAttributes( 3000, 300, false, false );
129 auto strokeFont = LoadStrokeFontUnique();
130 KIFONT::METRICS metrics; // not used for stroke fallback
131
132 // Plot near lower-left inside the page.
133 // Place text near the top of the page in internal units so after the 0.0072 scale it
134 // appears well within the MediaBox. Empirically m_paperSize.y ~ 116k internal units.
135 plotter.PlotText( VECTOR2I( 50000, 60000 ), COLOR4D( 0.0, 0.0, 0.0, 1.0 ), sample, attrs,
136 strokeFont.get(), metrics );
137
138 plotter.EndPlot();
139
140 // Read file back and check for expected UTF-16 hex encodings for some code points
141 // We expect CMap to contain mappings. E.g. '041F' (Cyrillic capital Pe), '65E5'(日), '672C'(本).
142 std::string buffer2; BOOST_REQUIRE( ReadPdfWithDecompressedStreams( pdfPath, buffer2 ) );
143 auto contains = [&]( const char* needle ) { return PdfContains( buffer2, needle ); };
144
145 BOOST_CHECK_MESSAGE( contains( "041F" ), "Missing Cyrillic glyph mapping (041F)" );
146 BOOST_CHECK_MESSAGE( contains( "0420" ) || contains( "0440" ), "Missing Cyrillic glyph mapping (0420/0440)" );
147 BOOST_CHECK_MESSAGE( contains( "65E5" ), "Missing Japanese Kanji glyph mapping (65E5)" );
148 BOOST_CHECK_MESSAGE( contains( "672C" ), "Missing Japanese Kanji glyph mapping (672C)" );
149 BOOST_CHECK_MESSAGE( contains( "6F22" ) || contains( "6漢" ), "Expect Chinese Han character mapping (6F22 / 漢)" );
150
151 // Cleanup temp file (unless debugging requested)
152 // Optional: rasterize PDF to image (requires poppler 'pdftoppm').
153 // We treat absence of the tool as a skipped sub-check rather than a failure.
154 {
155 long darkPixels = 0;
156 if( RasterizePdfCountDark( pdfPath, 72, 240, darkPixels ) )
157 {
158 BOOST_CHECK_MESSAGE( darkPixels > 200,
159 "Rasterized PDF appears blank or too sparse (" << darkPixels
160 << " dark pixels)" );
161 }
162 else
163 {
164 BOOST_TEST_MESSAGE( "pdftoppm not available or failed; skipping raster validation" );
165 }
166 }
167}
168
169BOOST_AUTO_TEST_CASE( PlotMultilingualTextBold )
170{
171 const std::string sampleUtf8 = "ABCDEF Привет 日本語 漢字";
172 wxString sample = wxString::FromUTF8( sampleUtf8.c_str() );
173
174 KI_TEST::SCOPED_TEMP_DIR tempDir( "kicad_pdf_unicode_bold" );
175 const wxString pdfPath = tempDir.CreateChildFileStr( "output.pdf" );
176
177 PDF_PLOTTER plotter;
178 SIMPLE_RENDER_SETTINGS renderSettings;
179 plotter.SetRenderSettings( &renderSettings );
180 BOOST_REQUIRE( plotter.OpenFile( pdfPath ) );
181 plotter.SetViewport( VECTOR2I(0,0), 1.0, 1.0, false );
182 BOOST_REQUIRE( plotter.StartPlot( wxT("1"), wxT("TestPage") ) );
183
184 TEXT_ATTRIBUTES attrs = BuildTextAttributes( 3000, 300, true, false );
185 auto strokeFont = LoadStrokeFontUnique();
186 KIFONT::METRICS metrics;
187 plotter.PlotText( VECTOR2I( 50000, 60000 ), COLOR4D( 0.0, 0.0, 0.0, 1.0 ), sample, attrs,
188 strokeFont.get(), metrics );
189 plotter.EndPlot();
190 std::string buffer3; BOOST_REQUIRE( ReadPdfWithDecompressedStreams( pdfPath, buffer3 ) );
191 auto contains = [&]( const char* needle ) { return PdfContains( buffer3, needle ); };
192 BOOST_CHECK_MESSAGE( contains( "041F" ), "Missing Cyrillic glyph mapping (bold 041F)" );
193 BOOST_CHECK_MESSAGE( contains( "65E5" ), "Missing Japanese glyph mapping (bold 65E5)" );
194}
195
196BOOST_AUTO_TEST_CASE( PlotMultilingualTextItalic )
197{
198 const std::string sampleUtf8 = "ABCDEF Привет 日本語 漢字";
199 wxString sample = wxString::FromUTF8( sampleUtf8.c_str() );
200 KI_TEST::SCOPED_TEMP_DIR tempDir( "kicad_pdf_unicode_italic" );
201 const wxString pdfPath = tempDir.CreateChildFileStr( "output.pdf" );
202 PDF_PLOTTER plotter;
203 SIMPLE_RENDER_SETTINGS renderSettings;
204
205 plotter.SetRenderSettings( &renderSettings );
206 BOOST_REQUIRE( plotter.OpenFile( pdfPath ) );
207 plotter.SetViewport( VECTOR2I(0,0), 1.0, 1.0, false );
208 BOOST_REQUIRE( plotter.StartPlot( wxT( "1" ), wxT( "TestPage" ) ) );
209 TEXT_ATTRIBUTES attrs = BuildTextAttributes( 3000, 300, false, true );
210 auto strokeFont = LoadStrokeFontUnique();
211 KIFONT::METRICS metrics;
212 plotter.PlotText( VECTOR2I( 50000, 60000 ), COLOR4D( 0, 0, 0, 1 ), sample, attrs, strokeFont.get(), metrics );
213 plotter.EndPlot();
214
215 std::string buffer4; BOOST_REQUIRE( ReadPdfWithDecompressedStreams( pdfPath, buffer4 ) );
216 auto contains = [&]( const char* n ) { return PdfContains( buffer4, n ); };
217 BOOST_CHECK_MESSAGE( contains( "041F" ), "Missing Cyrillic glyph mapping (italic 041F)" );
218 BOOST_CHECK_MESSAGE( contains( "65E5" ), "Missing Japanese glyph mapping (italic 65E5)" );
219}
220
221BOOST_AUTO_TEST_CASE( PlotMultilingualTextBoldItalic )
222{
223 const std::string sampleUtf8 = "ABCDEF Привет 日本語 漢字";
224 wxString sample = wxString::FromUTF8( sampleUtf8.c_str() );
225 KI_TEST::SCOPED_TEMP_DIR tempDir( "kicad_pdf_unicode_bolditalic" );
226 const wxString pdfPath = tempDir.CreateChildFileStr( "output.pdf" );
227 PDF_PLOTTER plotter;
228 SIMPLE_RENDER_SETTINGS renderSettings;
229
230 plotter.SetRenderSettings( &renderSettings );
231 BOOST_REQUIRE( plotter.OpenFile( pdfPath ) );
232
233 plotter.SetViewport( VECTOR2I( 0, 0 ), 1.0, 1.0, false );
234 BOOST_REQUIRE( plotter.StartPlot( wxT( "1" ), wxT( "TestPage" ) ) );
235
236 TEXT_ATTRIBUTES attrs = BuildTextAttributes( 3000, 300, true, true );
237 auto strokeFont = LoadStrokeFontUnique();
238 KIFONT::METRICS metrics;
239
240 plotter.PlotText( VECTOR2I( 50000, 60000 ), COLOR4D( 0, 0, 0, 1 ), sample, attrs, strokeFont.get(), metrics );
241 plotter.EndPlot();
242
243 std::string buffer5; BOOST_REQUIRE( ReadPdfWithDecompressedStreams( pdfPath, buffer5 ) );
244 auto contains = [&]( const char* n ) { return PdfContains( buffer5, n ); };
245 BOOST_CHECK_MESSAGE( contains( "041F" ), "Missing Cyrillic glyph mapping (bold-italic 041F)" );
246 BOOST_CHECK_MESSAGE( contains( "65E5" ), "Missing Japanese glyph mapping (bold-italic 65E5)" );
247}
248
249// Verify that d1 bounding boxes account for X offset and stroke width so PDF viewers don't
250// clip the rendered glyphs. Regression test for gitlab.com/kicad/code/kicad/-/issues/23621.
251BOOST_AUTO_TEST_CASE( GlyphBBoxIncludesOffsetAndStrokeWidth )
252{
253 const std::string sampleUtf8 = "MW";
254 wxString sample = wxString::FromUTF8( sampleUtf8.c_str() );
255 KI_TEST::SCOPED_TEMP_DIR tempDir( "kicad_pdf_bbox_check" );
256 const wxString pdfPath = tempDir.CreateChildFileStr( "output.pdf" );
257
258 PDF_PLOTTER plotter;
259 SIMPLE_RENDER_SETTINGS renderSettings;
260
261 plotter.SetRenderSettings( &renderSettings );
262 BOOST_REQUIRE( plotter.OpenFile( pdfPath ) );
263 plotter.SetViewport( VECTOR2I( 0, 0 ), 1.0, 1.0, false );
264 BOOST_REQUIRE( plotter.StartPlot( wxT( "1" ), wxT( "BBoxTest" ) ) );
265
266 TEXT_ATTRIBUTES attrs = BuildTextAttributes( 3000, 300, false, false );
267 auto strokeFont = LoadStrokeFontUnique();
268 KIFONT::METRICS metrics;
269
270 plotter.PlotText( VECTOR2I( 50000, 60000 ), COLOR4D( 0, 0, 0, 1 ), sample, attrs,
271 strokeFont.get(), metrics );
272 plotter.EndPlot();
273
274 std::string buffer;
275 BOOST_REQUIRE( ReadPdfWithDecompressedStreams( pdfPath, buffer ) );
276
277 // Parse d1 operators: format is "width 0 minX minY maxX maxY d1"
278 // Verify that no glyph has a minX of exactly 0 (would mean X offset was not applied) and
279 // that the bbox extends beyond the stroke center coordinates by at least half the stroke
280 // width.
281 const ADVANCED_CFG& cfg = ADVANCED_CFG::GetCfg();
282 double unitsPerEm = 1000.0;
283 double expectedXOffset = cfg.m_PDFStrokeFontXOffset * unitsPerEm;
284 double widthFactor = 300.0 / 3000.0;
285 double expectedHalfStroke = unitsPerEm * widthFactor / 2.0;
286
287 // Find all d1 operators (skip .notdef which has all-zero bbox)
288 std::string::size_type pos = 0;
289 int checkedGlyphs = 0;
290
291 while( ( pos = buffer.find( " d1 ", pos ) ) != std::string::npos )
292 {
293 // Walk backwards to find the start of the d1 line
294 std::string::size_type lineStart = buffer.rfind( '\n', pos );
295
296 if( lineStart == std::string::npos )
297 lineStart = 0;
298 else
299 lineStart++;
300
301 std::string d1Line = buffer.substr( lineStart, pos + 3 - lineStart );
302
303 double width, wy, minX, minY, maxX, maxY;
304
305 if( sscanf( d1Line.c_str(), "%lf %lf %lf %lf %lf %lf", &width, &wy, &minX, &minY,
306 &maxX, &maxY ) == 6 )
307 {
308 // Skip .notdef (all zeros)
309 if( width == 0.0 && maxX == 0.0 )
310 {
311 pos += 4;
312 continue;
313 }
314
315 // The bbox minX should be shifted by the X offset minus half stroke width.
316 // With default offset 0.1 and stroke width 300 at size 3000, minX should be around
317 // 0.1*1000 - 0.1*1000/2 = 100 - 50 = 50, not 0.
318 BOOST_CHECK_MESSAGE( minX < -expectedHalfStroke + 1.0 || minX > 0.1,
319 "Glyph bbox minX (" << minX
320 << ") suggests X offset or stroke padding is missing" );
321
322 // maxX must exceed the advance width to account for offset + stroke padding
323 BOOST_CHECK_MESSAGE( maxX > width,
324 "Glyph bbox maxX (" << maxX << ") must exceed advance width ("
325 << width << ") to prevent clipping" );
326
327 checkedGlyphs++;
328 }
329
330 pos += 4;
331 }
332
333 BOOST_CHECK_MESSAGE( checkedGlyphs >= 2,
334 "Expected at least 2 non-notdef glyphs, found " << checkedGlyphs );
335}
336
337// Test Y offset bounding box fix: ensure characters are not clipped when Y offset is applied
338BOOST_AUTO_TEST_CASE( PlotMultilingualTextWithYOffset )
339{
340 // Temporarily modify the Y offset configuration
341 ADVANCED_CFG& cfg = const_cast<ADVANCED_CFG&>( ADVANCED_CFG::GetCfg() );
342 double originalOffset = cfg.m_PDFStrokeFontYOffset;
343 cfg.m_PDFStrokeFontYOffset = 0.2; // 20% of EM unit offset upward
344
345 const std::string sampleUtf8 = "Yg Test ñ"; // characters with ascenders and descenders
346 wxString sample = wxString::FromUTF8( sampleUtf8.c_str() );
347 KI_TEST::SCOPED_TEMP_DIR tempDir( "kicad_pdf_unicode_yoffset" );
348 const wxString pdfPath = tempDir.CreateChildFileStr( "output.pdf" );
349
350 PDF_PLOTTER plotter;
351 SIMPLE_RENDER_SETTINGS renderSettings;
352
353 plotter.SetRenderSettings( &renderSettings );
354 BOOST_REQUIRE( plotter.OpenFile( pdfPath ) );
355 plotter.SetViewport( VECTOR2I( 0, 0 ), 1.0, 1.0, false );
356 BOOST_REQUIRE( plotter.StartPlot( wxT( "1" ), wxT( "TestPage" ) ) );
357
358 TEXT_ATTRIBUTES attrs = BuildTextAttributes( 4000, 400, false, false );
359 auto strokeFont = LoadStrokeFontUnique();
360 KIFONT::METRICS metrics;
361 plotter.PlotText( VECTOR2I( 50000, 60000 ), COLOR4D( 0, 0, 0, 1 ), sample, attrs, strokeFont.get(), metrics );
362 plotter.EndPlot();
363
364
365 // Restore original Y offset
366 cfg.m_PDFStrokeFontYOffset = originalOffset;
367
368 // Basic PDF validation and decompression
369 std::string buffer6; BOOST_REQUIRE( ReadPdfWithDecompressedStreams( pdfPath, buffer6 ) );
370 BOOST_CHECK( buffer6.rfind( "%PDF", 0 ) == 0 );
371
372 // Check that bounding boxes exist and are reasonable (not clipped)
373 // Look for d1 operators which specify character bounding boxes
374 BOOST_CHECK_MESSAGE( buffer6.find( "d1" ) != std::string::npos,
375 "PDF should contain d1 operators for glyph bounding boxes" );
376}
377
378BOOST_AUTO_TEST_CASE( PlotOutlineFontEmbedding )
379{
380 KI_TEST::SCOPED_TEMP_DIR tempDir( "kicad_pdf_outline_font" );
381 const wxString pdfPath = tempDir.CreateChildFileStr( "output.pdf" );
382
383 // Locate test font file (Noto Sans) in test resources
384 wxFileName fontFile( KI_TEST::GetTestDataRootDir() );
385 fontFile.RemoveLastDir();
386 fontFile.AppendDir( wxT( "resources" ) );
387 fontFile.AppendDir( wxT( "fonts" ) );
388 fontFile.SetFullName( wxT( "NotoSans-Regular.ttf" ) );
389 wxString fontPath = fontFile.GetFullPath();
390
391 BOOST_REQUIRE( wxFileExists( fontPath ) );
392
393 PDF_PLOTTER plotter;
394 SIMPLE_RENDER_SETTINGS renderSettings;
395
396 plotter.SetRenderSettings( &renderSettings );
397 BOOST_REQUIRE( plotter.OpenFile( pdfPath ) );
398 plotter.SetViewport( VECTOR2I(0,0), 1.0, 1.0, false );
399 BOOST_REQUIRE( plotter.StartPlot( wxT("1"), wxT("OutlineFont") ) );
400
401 TEXT_ATTRIBUTES attrs = BuildTextAttributes( 4000, 0, false, false );
402
403 std::vector<wxString> embeddedFonts;
404 embeddedFonts.push_back( fontPath );
405
406 KIFONT::FONT* outlineFont = KIFONT::FONT::GetFont( wxT( "Noto Sans" ), false, false, &embeddedFonts );
407 KIFONT::METRICS metrics;
408
409 wxString sample = wxString::FromUTF8( "Outline café" );
410
411 plotter.PlotText( VECTOR2I( 42000, 52000 ), COLOR4D( 0, 0, 0, 1 ), sample, attrs,
412 outlineFont, metrics );
413
414 plotter.EndPlot();
415
416 wxFFile file( pdfPath, "rb" );
417 BOOST_REQUIRE( file.IsOpened() );
418 wxFileOffset len = file.Length();
419 std::string buffer; buffer.resize( (size_t) len ); file.Read( buffer.data(), len );
420 BOOST_CHECK( buffer.rfind( "%PDF", 0 ) == 0 );
421
422 auto appendDecompressed = [&]() { std::string tmp; ReadPdfWithDecompressedStreams( pdfPath, tmp ); buffer.swap( tmp ); };
423 appendDecompressed();
424
425 BOOST_CHECK_MESSAGE( buffer.find( "/CIDFontType2" ) != std::string::npos,
426 "Expected CIDFontType2 descendant font" );
427 BOOST_CHECK_MESSAGE( buffer.find( "/FontFile2" ) != std::string::npos,
428 "Embedded outline font should include FontFile2 stream" );
429 BOOST_CHECK_MESSAGE( buffer.find( "AAAAAA+Noto-Sans" ) != std::string::npos,
430 "BaseFont should reference Noto Sans subset" );
431 BOOST_CHECK_MESSAGE( buffer.find( "00E9" ) != std::string::npos,
432 "ToUnicode map should include Latin character with accent" );
433 BOOST_CHECK_MESSAGE( buffer.find( "/KiCadOutline" ) != std::string::npos,
434 "Outline font resource entry missing" );
435
436 // Optional: rasterize PDF to image (requires poppler 'pdftoppm').
437 // We treat absence of the tool as a skipped sub-check rather than a failure.
438 {
439 KI_TEST::SCOPED_TEMP_DIR rasterDir( "kicad_pdf_raster" );
440 wxString rasterBase = rasterDir.ChildPathStr( "raster" );
441 wxString cmd = wxString::Format( wxT("pdftoppm -r 72 -singlefile -png \"%s\" \"%s\""),
442 pdfPath, rasterBase );
443
444 int ret = wxExecute( cmd, wxEXEC_SYNC );
445
446 if( ret == 0 )
447 {
448 wxString pngPath = rasterBase + wxT(".png");
449
450 if( wxFileExists( pngPath ) )
451 {
452 // Ensure PNG handler is available
453 if( !wxImage::FindHandler( wxBITMAP_TYPE_PNG ) )
454 wxImage::AddHandler( new wxPNGHandler );
455
456 wxImage img( pngPath );
457 BOOST_REQUIRE_MESSAGE( img.IsOk(), "Failed to load rasterized PDF image" );
458
459 long darkPixels = 0;
460 int w = img.GetWidth();
461 int h = img.GetHeight();
462
463 for( int y = 0; y < h; ++y )
464 {
465 for( int x = 0; x < w; ++x )
466 {
467 unsigned char r = img.GetRed( x, y );
468 unsigned char g = img.GetGreen( x, y );
469 unsigned char b = img.GetBlue( x, y );
470
471 // Count any non-near-white pixel as drawn content
472 if( r < 240 || g < 240 || b < 240 )
473 ++darkPixels;
474 }
475 }
476
477 // Demand at least 100 non-white pixels to consider outline font rendered correctly.
478 // This threshold is lower than stroke font since outline fonts may render differently.
479 BOOST_CHECK_MESSAGE( darkPixels > 100,
480 "Rasterized PDF appears blank or too sparse (" << darkPixels
481 << " dark pixels). Outline font may not be rendering correctly." );
482 }
483 else
484 {
485 BOOST_TEST_MESSAGE( "pdftoppm succeeded but PNG output missing; skipping raster validation" );
486 }
487 }
488 else
489 {
490 BOOST_TEST_MESSAGE( "pdftoppm not available or failed; skipping raster validation" );
491 }
492 }
493}
494
495// Extract the device-space X translation of every stroke-font text block ("... cm BT") in a
496// decompressed PDF content stream, in stream order. The stroke plotter emits each rendered word
497// as "q a b c d e f cm BT ...", where the 5th number (e) is the text-matrix X origin.
498static std::vector<double> ExtractStrokeTextOriginsX( const std::string& aBuffer )
499{
500 std::vector<double> origins;
501 size_t pos = 0;
502
503 while( ( pos = aBuffer.find( "cm BT", pos ) ) != std::string::npos )
504 {
505 std::string head = aBuffer.substr( 0, pos );
506 std::vector<double> nums;
507
508 for( size_t end = head.size(); end > 0 && nums.size() < 6; )
509 {
510 size_t start = head.find_last_not_of( " \n\r\t", end - 1 );
511
512 if( start == std::string::npos )
513 break;
514
515 size_t tokStart = head.find_last_of( " \n\r\t", start );
516 tokStart = ( tokStart == std::string::npos ) ? 0 : tokStart + 1;
517
518 try
519 {
520 nums.push_back( std::stod( head.substr( tokStart, start - tokStart + 1 ) ) );
521 }
522 catch( ... )
523 {
524 break;
525 }
526
527 end = tokStart;
528 }
529
530 if( nums.size() == 6 )
531 origins.push_back( nums[1] ); // reversed order: e is second-from-last collected
532
533 pos += 5;
534 }
535
536 return origins;
537}
538
539
540// Issue #22606: PDF plotting of stroke-font text containing tab characters placed the following
541// text at the wrong tab stop. The plotter hardcoded the outline font's tab rule (2.4 * size),
542// but the default schematic font is the stroke font, whose tab stops are column based and land
543// much further right. The result was tabs that came out too short in the PDF versus on screen.
544//
545// This drives the plotter with the reporter's own text ("v07:\terstversion") and checks that the
546// text after the tab is placed exactly where the stroke font's own glyph model puts it.
547BOOST_AUTO_TEST_CASE( PlotStrokeTextTabStopMatchesFont )
548{
549 KI_TEST::SCOPED_TEMP_DIR tempDir( "kicad_pdf_tabs" );
550 const wxString pdfPath = tempDir.CreateChildFileStr( "output.pdf" );
551
552 PDF_PLOTTER plotter;
553 SIMPLE_RENDER_SETTINGS renderSettings;
554
555 plotter.SetRenderSettings( &renderSettings );
556 BOOST_REQUIRE( plotter.OpenFile( pdfPath ) );
557 plotter.SetViewport( VECTOR2I( 0, 0 ), 1.0, 1.0, false );
558 BOOST_REQUIRE( plotter.StartPlot( wxT( "1" ), wxT( "TabTest" ) ) );
559
560 const int sizeIu = 1270000; // 1.27 mm, the reporter's text size
561 TEXT_ATTRIBUTES attrs = BuildTextAttributes( sizeIu, sizeIu / 10, false, false );
562 auto strokeFont = LoadStrokeFontUnique();
563 KIFONT::METRICS metrics;
564
565 // Two reference words establish the linear user->device X scale of the viewport.
566 plotter.PlotText( VECTOR2I( 40000000, 60000000 ), COLOR4D( 0, 0, 0, 1 ), wxT( "M" ), attrs,
567 strokeFont.get(), metrics );
568 plotter.PlotText( VECTOR2I( 80000000, 60000000 ), COLOR4D( 0, 0, 0, 1 ), wxT( "M" ), attrs,
569 strokeFont.get(), metrics );
570
571 // The reporter's tabbed line: "v07:" then a tab then the rest of the line.
572 plotter.PlotText( VECTOR2I( 40000000, 40000000 ), COLOR4D( 0, 0, 0, 1 ), wxT( "v07:\terstversion" ),
573 attrs, strokeFont.get(), metrics );
574
575 plotter.EndPlot();
576
577 std::string buffer;
578 BOOST_REQUIRE( ReadPdfWithDecompressedStreams( pdfPath, buffer ) );
579 BOOST_CHECK( buffer.rfind( "%PDF", 0 ) == 0 );
580
581 std::vector<double> origins = ExtractStrokeTextOriginsX( buffer );
582
583 // Two reference "M" blocks plus the two segments ("v07:" and "erstversion") of the tabbed line.
584 BOOST_REQUIRE_EQUAL( origins.size(), 4u );
585
586 const double scale = ( origins[1] - origins[0] ) / ( 80000000.0 - 40000000.0 );
587 BOOST_REQUIRE( std::abs( scale ) > 0.0 );
588
589 // Distance in user units from the "v07:" origin to the post-tab "erstversion" origin.
590 const double measuredOffsetIu = ( origins[3] - origins[2] ) / scale;
591
592 const VECTOR2I size( sizeIu, sizeIu );
593
594 // The stroke font's own glyph placement is the ground truth for the on-screen position.
595 const double expectedOffsetIu =
596 strokeFont->GetTextAsGlyphs( nullptr, nullptr, wxT( "v07:\t" ), size, VECTOR2I(),
597 ANGLE_0, false, VECTOR2I(), 0 )
598 .x;
599
600 // The old (buggy) hardcoded rule would have placed it here; kept only to document the gap.
601 const double widthV07 =
602 strokeFont->GetTextAsGlyphs( nullptr, nullptr, wxT( "v07:" ), size, VECTOR2I(), ANGLE_0,
603 false, VECTOR2I(), 0 )
604 .x;
605 const int oldTabWidth = KiROUND( sizeIu * 4 * 0.6 );
606 const double oldOffsetIu = widthV07 + ( oldTabWidth - KiROUND( widthV07 ) % oldTabWidth );
607
608 const double tolerance = sizeIu * 0.02;
609
610 BOOST_CHECK_MESSAGE( std::abs( measuredOffsetIu - expectedOffsetIu ) < tolerance,
611 "Post-tab text at " << measuredOffsetIu << " IU, font model expects "
612 << expectedOffsetIu << " IU (old buggy rule: "
613 << oldOffsetIu << " IU)" );
614}
615
616// Regression test for GitLab issue 23740: stroke-font Type3 glyphs in the PDF output were
617// plotted above and to the left of where they should have been. The old code derived the
618// vertical anchor from StringBoundaryLimits (which inflates by 3*thickness) and used
619// thickness-dependent CTM hacks. Alignment now goes through FONT::GetAlignedDrawPosition
620// (GAL getLinePositions), with a constant cancel of the baked Type3 glyph Y offset.
621//
622// The test plots the same character with V_TOP, V_CENTER and V_BOTTOM alignments at the same
623// anchor, parses the text-matrix ctm_f value out of the content stream, then verifies that
624// ctm_f deltas match FONT::getLinePositions (the constant YOffset cancel drops out of deltas).
625BOOST_AUTO_TEST_CASE( StrokeFontVerticalAlignmentMatchesScreen )
626{
627 KI_TEST::SCOPED_TEMP_DIR tempDir( "kicad_pdf_valign" );
628 const wxString pdfPath = tempDir.CreateChildFileStr( "output.pdf" );
629
630 PDF_PLOTTER plotter;
631 SIMPLE_RENDER_SETTINGS renderSettings;
632 plotter.SetRenderSettings( &renderSettings );
633 BOOST_REQUIRE( plotter.OpenFile( pdfPath ) );
634 plotter.SetViewport( VECTOR2I( 0, 0 ), 1.0, 1.0, false );
635 BOOST_REQUIRE( plotter.StartPlot( wxT( "1" ), wxT( "VAlignTest" ) ) );
636
637 const int anchorY = 60000;
638 const int sizeIU = 3000;
639 const int strokeW = 300;
640
641 auto plotAt = [&]( GR_TEXT_V_ALIGN_T aVJustify, bool aItalic )
642 {
643 TEXT_ATTRIBUTES attrs = BuildTextAttributes( sizeIU, strokeW, false, aItalic );
644 attrs.m_Valign = aVJustify;
645 auto strokeFont = LoadStrokeFontUnique();
646 KIFONT::METRICS metrics;
647 plotter.PlotText( VECTOR2I( 50000, anchorY ), COLOR4D( 0, 0, 0, 1 ), wxT( "T" ), attrs,
648 strokeFont.get(), metrics );
649 };
650
651 plotAt( GR_TEXT_V_ALIGN_TOP, false );
652 plotAt( GR_TEXT_V_ALIGN_CENTER, false );
653 plotAt( GR_TEXT_V_ALIGN_BOTTOM, false );
654 plotAt( GR_TEXT_V_ALIGN_TOP, true );
655 plotAt( GR_TEXT_V_ALIGN_CENTER, true );
656 plotter.EndPlot();
657
658 std::string buffer;
659 BOOST_REQUIRE( ReadPdfWithDecompressedStreams( pdfPath, buffer ) );
660
661 // Capture the full text matrix for every `q ... cm BT ... Tj ET Q` block.
662 struct MATRIX_SAMPLE { double a, b, c, d, e, f; };
663 std::vector<MATRIX_SAMPLE> matrices;
664 std::string::size_type pos = 0;
665
666 while( ( pos = buffer.find( "cm BT", pos ) ) != std::string::npos )
667 {
668 std::string::size_type lineStart = buffer.rfind( '\n', pos );
669 lineStart = ( lineStart == std::string::npos ) ? 0 : lineStart + 1;
670
671 std::string cmLine = buffer.substr( lineStart, pos - lineStart );
672 MATRIX_SAMPLE m{};
673
674 if( sscanf( cmLine.c_str(), "q %lf %lf %lf %lf %lf %lf", &m.a, &m.b, &m.c, &m.d, &m.e,
675 &m.f ) == 6 )
676 {
677 matrices.push_back( m );
678 }
679
680 pos += 5;
681 }
682
683 BOOST_REQUIRE_EQUAL( matrices.size(), 5u );
684
685 const double fontSize = (double) sizeIU;
686 const double thickness = (double) strokeW;
687
688 // GAL cursor Y offsets from FONT::getLinePositions (single line, height = 1.17 * size),
689 // ignoring the strokeWidth*0.052 fudge which is the same for every V-align and cancels
690 // in the deltas:
691 // V_TOP = 1.000 * size
692 // V_CENTER = 0.415 * size
693 // V_BOTTOM = -0.17 * size
694 // PDF device Y decreases as IU Y increases, so ctm_f_TOP < ctm_f_CENTER < ctm_f_BOTTOM.
695 const double expected_delta_top_center = ( 1.000 - 0.415 ) * fontSize;
696 const double expected_delta_center_bot = ( 0.415 - ( -0.17 ) ) * fontSize;
697
698 const double observed_delta_top_center = matrices[1].f - matrices[0].f;
699 const double observed_delta_center_bot = matrices[2].f - matrices[1].f;
700
701 BOOST_CHECK_CLOSE( observed_delta_top_center, expected_delta_top_center, 1.0 );
702 BOOST_CHECK_CLOSE( observed_delta_center_bot, expected_delta_center_bot, 1.0 );
703
704 BOOST_CHECK_MESSAGE( matrices[0].f < matrices[1].f && matrices[1].f < matrices[2].f,
705 "Expected ctm_f_TOP < ctm_f_CENTER < ctm_f_BOTTOM, got "
706 << matrices[0].f << ", " << matrices[1].f << ", " << matrices[2].f );
707
708 // The uncorrected StringBoundaryLimits formula (pre-fix) would give
709 // 0.5 * (size + 3*thickness) for the V_TOP to V_CENTER delta.
710 const double uncorrectedDelta = 0.5 * ( fontSize + 3.0 * thickness );
711 BOOST_CHECK_MESSAGE( std::abs( observed_delta_top_center - uncorrectedDelta ) > 1.0,
712 "ctm_f delta for V_TOP vs V_CENTER matches the uncorrected formula; "
713 "the stroke-font alignment fix does not appear to be in effect." );
714
715 // Italic shear must still be present on the text matrix.
716 BOOST_CHECK_MESSAGE( std::abs( matrices[3].c ) > 0.01,
717 "Italic text matrix does not show the expected shear (adj_c == "
718 << matrices[3].c << ")" );
719
720 // Glyph YOffset cancel is constant across V-alignments. With GAL-based start positions,
721 // only IU Y differs between V_TOP and V_CENTER for horizontal text, so ctm_e must not
722 // change (old per-V-align deltaDev hacks incorrectly introduced a shear * Y correction
723 // into e). Same for italic and non-italic.
724 const double italic_delta_e = matrices[4].e - matrices[3].e;
725 const double nonitalic_delta_e = matrices[1].e - matrices[0].e;
726
727 BOOST_CHECK_SMALL( nonitalic_delta_e, 1.0 );
728 BOOST_CHECK_SMALL( italic_delta_e, 1.0 );
729
730 // V-alignment deltas on f remain the same under italic shear (Y cancel is constant).
731 const double italic_delta_top_center = matrices[4].f - matrices[3].f;
732 BOOST_CHECK_CLOSE( italic_delta_top_center, expected_delta_top_center, 1.0 );
733}
734
735
736// Regression test for https://gitlab.com/kicad/code/kicad/-/issues/24419
737// PDF_PLOTTER::renderWord used to advance the per-word cursor by
738// FONT::StringBoundaryLimits, which for stroke fonts inflates the returned
739// bounding box by 3*thickness. The Tj operator that actually renders the word
740// advances by the sum of glyph widths (no inflation), so successive words were
741// pushed too far apart in the rendered PDF. The bug was visible on silkscreen
742// text whose width exceeded the board edge in the exported PDF but fit on the
743// PCB editor canvas.
744//
745// The fix is to derive the cursor advance from FONT::GetTextAsGlyphs, which
746// matches exactly what Tj produces. This test plots a multi-word string and
747// verifies that the gap between word origins corresponds to the actual
748// glyph-width sum rather than the inflated bbox.
749BOOST_AUTO_TEST_CASE( StrokeFontWordSpacingMatchesGlyphAdvance )
750{
751 KI_TEST::SCOPED_TEMP_DIR tempDir( "kicad_pdf_wordspacing" );
752 const wxString pdfPath = tempDir.CreateChildFileStr( "output.pdf" );
753
754 PDF_PLOTTER plotter;
755 SIMPLE_RENDER_SETTINGS renderSettings;
756 plotter.SetRenderSettings( &renderSettings );
757 BOOST_REQUIRE( plotter.OpenFile( pdfPath ) );
758 plotter.SetViewport( VECTOR2I( 0, 0 ), 1.0, 1.0, false );
759 BOOST_REQUIRE( plotter.StartPlot( wxT( "1" ), wxT( "WordSpacingTest" ) ) );
760
761 const int sizeIU = 1900; // mimics the 1.9mm silk text from issue #24419
762 const int strokeW = 380; // bold thickness as in the issue file
763 TEXT_ATTRIBUTES attrs = BuildTextAttributes( sizeIU, strokeW, true, false );
766
767 auto strokeFont = LoadStrokeFontUnique();
768 const KIFONT::METRICS metrics;
769
770 plotter.PlotText( VECTOR2I( 50000, 60000 ), COLOR4D( 0, 0, 0, 1 ),
771 wxT( "TEST text Silkscreen" ), attrs, strokeFont.get(), metrics );
772 plotter.EndPlot();
773
774 std::string buffer;
775 BOOST_REQUIRE( ReadPdfWithDecompressedStreams( pdfPath, buffer ) );
776
777 // Each word becomes one `q ... cm BT ... Tj ET Q` block. Capture the X translation (ctm_e)
778 // of each block so we can verify the gap between words.
779 std::vector<double> wordOriginX;
780 std::string::size_type pos = 0;
781
782 while( ( pos = buffer.find( "cm BT", pos ) ) != std::string::npos )
783 {
784 std::string::size_type lineStart = buffer.rfind( '\n', pos );
785 lineStart = ( lineStart == std::string::npos ) ? 0 : lineStart + 1;
786
787 std::string cmLine = buffer.substr( lineStart, pos - lineStart );
788 double a, b, c, d, e, f;
789
790 if( sscanf( cmLine.c_str(), "q %lf %lf %lf %lf %lf %lf", &a, &b, &c, &d, &e, &f ) == 6 )
791 wordOriginX.push_back( e );
792
793 pos += 5;
794 }
795
796 // Expect one block per word: "TEST", "text", "Silkscreen".
797 BOOST_REQUIRE_EQUAL( wordOriginX.size(), 3u );
798
799 // Compute the expected stroke-font cursor advance for "TEST " (word + trailing space) and
800 // "text " in IU. This is what the Tj operator will move the text cursor by and what the
801 // renderWord cursor should match. Compare ratios so we don't have to convert to device
802 // units (userToDeviceSize is protected).
803 auto strokeAdvanceIU = [&]( const wxString& aWord )
804 {
805 return strokeFont
806 ->GetTextAsGlyphs( nullptr, nullptr, aWord, VECTOR2I( sizeIU, sizeIU ),
808 .x;
809 };
810
811 const double expectedTestAdvanceIU = (double) strokeAdvanceIU( wxT( "TEST " ) );
812 const double expectedTextAdvanceIU = (double) strokeAdvanceIU( wxT( "text " ) );
813
814 const double observedTestAdvanceDev = wordOriginX[1] - wordOriginX[0];
815 const double observedTextAdvanceDev = wordOriginX[2] - wordOriginX[1];
816
817 // device-per-IU scale factor derived from the first observed gap.
818 const double scale = observedTestAdvanceDev / expectedTestAdvanceIU;
819
820 // The TEST -> text advance and the text -> Silkscreen advance must both follow the
821 // same IU-to-device scale: if the spacing matches glyph metrics, the second observed
822 // gap equals scale * expectedTextAdvanceIU.
823 const double expectedTextAdvanceDev = scale * expectedTextAdvanceIU;
824 BOOST_CHECK_CLOSE( observedTextAdvanceDev, expectedTextAdvanceDev, 0.5 );
825
826 // Sanity guard against regression to the inflated-bbox formula, which adds 3*thickness IU
827 // of slop per word. Use the second word so this check is independent of the scale derivation
828 // above.
829 const double inflatedTextAdvanceDev = scale * ( expectedTextAdvanceIU + 3.0 * strokeW );
830 BOOST_CHECK_MESSAGE( std::abs( observedTextAdvanceDev - inflatedTextAdvanceDev )
831 > 0.5 * ( inflatedTextAdvanceDev - expectedTextAdvanceDev ),
832 "Word spacing matches the buggy inflated-bbox formula ("
833 << observedTextAdvanceDev << " ~= " << inflatedTextAdvanceDev
834 << "); the renderWord cursor fix appears inactive." );
835}
836
837// Regression test for https://gitlab.com/kicad/code/kicad/-/issues/23843
838// A border-less filled shape with a dashed/dotted stroke is plotted with a zero pen width.
839// PDF_PLOTTER::SetDash then computed every dash element from that zero width, emitting an
840// all-zero dash array "[0 0] 0 d". Such an array is illegal in PDF and makes strict viewers
841// (Adobe Acrobat, Evince) abort rendering of the rest of the page, dropping the shape fill,
842// the page background and any junction dots that follow. SetDash must fall back to a solid
843// line "[] 0 d" when the requested pattern degenerates to all zeros.
844BOOST_AUTO_TEST_CASE( SetDashZeroWidthNoIllegalDashArray )
845{
846 KI_TEST::SCOPED_TEMP_DIR tempDir( "kicad_pdf_zero_dash" );
847 const wxString pdfPath = tempDir.CreateChildFileStr( "output.pdf" );
848
849 PDF_PLOTTER plotter;
850 SIMPLE_RENDER_SETTINGS renderSettings;
851
852 plotter.SetRenderSettings( &renderSettings );
853 BOOST_REQUIRE( plotter.OpenFile( pdfPath ) );
854 plotter.SetViewport( VECTOR2I( 0, 0 ), 1.0, 1.0, false );
855 BOOST_REQUIRE( plotter.StartPlot( wxT( "1" ), wxT( "DashTest" ) ) );
856
857 // Reproduce the border-less dotted filled rectangle from the issue: a dotted dash style
858 // with a zero pen width, followed by the filled (no-outline) rectangle itself. Exercise
859 // every dashed style so a regression in any of them is caught.
860 plotter.SetColor( COLOR4D( 1.0, 0.0, 0.0, 1.0 ) );
861
864 {
865 plotter.SetDash( 0, style );
866 }
867
868 plotter.SetDash( 0, LINE_STYLE::DOT );
869 plotter.Rect( VECTOR2I( 50000, 50000 ), VECTOR2I( 70000, 60000 ), FILL_T::FILLED_SHAPE, 0 );
870
871 plotter.EndPlot();
872
873 std::string buffer;
874 BOOST_REQUIRE( ReadPdfWithDecompressedStreams( pdfPath, buffer ) );
875 BOOST_CHECK( buffer.rfind( "%PDF", 0 ) == 0 );
876
877 // No illegal all-zero dash array may be present anywhere in the content stream, for any
878 // of the dashed styles.
879 for( const char* illegalPattern : { "[0 0] 0 d", "[0 0 0 0] 0 d", "[0 0 0 0 0 0] 0 d" } )
880 {
881 BOOST_CHECK_MESSAGE( buffer.find( illegalPattern ) == std::string::npos,
882 "PDF contains an illegal all-zero dash array (" << illegalPattern
883 << "), which breaks strict viewers" );
884 }
885
886 // The zero-width dashed styles must have degenerated to a solid line.
887 BOOST_CHECK_MESSAGE( buffer.find( "[] 0 d" ) != std::string::npos,
888 "Zero-width dashed style should fall back to a solid dash array" );
889
890 // The fill operator for the rectangle must still be emitted.
891 BOOST_CHECK_MESSAGE( buffer.find( " re f" ) != std::string::npos
892 || buffer.find( "re\nf" ) != std::string::npos,
893 "Filled rectangle should still be plotted" );
894}
895
896
897// Companion to SetDashZeroWidthNoIllegalDashArray: the solid-line fallback must apply only to the
898// degenerate all-zero case. A normal non-zero pen width must still emit a real dashed array, so a
899// fix that collapsed every dashed style to solid would regress here.
900BOOST_AUTO_TEST_CASE( SetDashNonZeroWidthKeepsDashArray )
901{
902 KI_TEST::SCOPED_TEMP_DIR tempDir( "kicad_pdf_nonzero_dash" );
903 const wxString pdfPath = tempDir.CreateChildFileStr( "output.pdf" );
904
905 PDF_PLOTTER plotter;
906 SIMPLE_RENDER_SETTINGS renderSettings;
907
908 plotter.SetRenderSettings( &renderSettings );
909 BOOST_REQUIRE( plotter.OpenFile( pdfPath ) );
910 plotter.SetViewport( VECTOR2I( 0, 0 ), 1.0, 1.0, false );
911 BOOST_REQUIRE( plotter.StartPlot( wxT( "1" ), wxT( "DashTest" ) ) );
912
913 plotter.SetColor( COLOR4D( 1.0, 0.0, 0.0, 1.0 ) );
914 plotter.SetDash( 10000, LINE_STYLE::DASH );
915 plotter.MoveTo( VECTOR2I( 50000, 50000 ) );
916 plotter.FinishTo( VECTOR2I( 70000, 50000 ) );
917
918 plotter.EndPlot();
919
920 std::string buffer;
921 BOOST_REQUIRE( ReadPdfWithDecompressedStreams( pdfPath, buffer ) );
922
923 // The dash array elements scale with the pen width, so a non-zero width yields a real,
924 // non-zero pattern. Find the emitted array and confirm it is neither the solid fallback
925 // "[]" nor the all-zero degenerate form.
926 size_t dashPos = buffer.find( "] 0 d" );
927 BOOST_REQUIRE_MESSAGE( dashPos != std::string::npos, "No dash array was emitted" );
928
929 size_t openPos = buffer.rfind( '[', dashPos );
930 BOOST_REQUIRE( openPos != std::string::npos && openPos < dashPos );
931
932 std::string dashArray = buffer.substr( openPos, dashPos - openPos + 1 );
933
934 BOOST_CHECK_MESSAGE( dashArray.find_first_of( "123456789" ) != std::string::npos,
935 "Non-zero dashed style should emit a real, non-zero dash array, got "
936 << dashArray );
937 BOOST_CHECK_MESSAGE( dashArray != "[]",
938 "A non-zero pen width must not fall back to the solid line" );
939}
940
constexpr BOX2I KiROUND(const BOX2D &aBoxD)
Definition box2.h:995
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:94
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:143
A color representation with 4 components: red, green, blue, alpha.
Definition color4d.h:101
wxString CreateChildFileStr(const wxString &aName) const
Create and return the path to a direct child file as a wxString.
wxString ChildPathStr(const wxString &aName) const
Get the path to a direct child of the temporary directory as a wxString, without creating the child.
virtual bool EndPlot() override
virtual void SetDash(int aLineWidth, LINE_STYLE aLineStyle) override
PDF supports dashed lines.
virtual bool OpenFile(const wxString &aFullFilename) override
Open or create the plot file aFullFilename.
virtual void Rect(const VECTOR2I &p1, const VECTOR2I &p2, FILL_T fill, int width, int aCornerRadius=0) override
Rectangles in PDF.
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
void MoveTo(const VECTOR2I &pos)
Definition plotter.h:308
void FinishTo(const VECTOR2I &pos)
Definition plotter.h:318
virtual void SetColor(const COLOR4D &color) override
The SetColor implementation is split with the subclasses: the PSLIKE computes the rgb values,...
Minimal concrete render settings suitable for plotters in tests.
GR_TEXT_H_ALIGN_T m_Halign
GR_TEXT_V_ALIGN_T m_Valign
static constexpr EDA_ANGLE ANGLE_0
Definition eda_angle.h:422
@ FILLED_SHAPE
Fill with object color.
Definition eda_fill.h:31
@ BOLD
Definition font.h:43
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...
std::string GetTestDataRootDir()
EDA_ANGLE abs(const EDA_ANGLE &aAngle)
Definition eda_angle.h:411
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.
Plotting engines similar to ps (PostScript, Gerber, svg)
const int scale
LINE_STYLE
Dashed line types.
BOOST_AUTO_TEST_CASE(HorizontalAlignment)
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 std::vector< double > ExtractStrokeTextOriginsX(const std::string &aBuffer)
VECTOR2I end
BOOST_TEST_MESSAGE("Polyline has "<< chain.PointCount()<< " points")
@ GR_TEXT_H_ALIGN_LEFT
GR_TEXT_V_ALIGN_T
This is API surface mapped to common.types.VertialAlignment.
@ GR_TEXT_V_ALIGN_BOTTOM
@ GR_TEXT_V_ALIGN_CENTER
@ GR_TEXT_V_ALIGN_TOP
VECTOR2< int32_t > VECTOR2I
Definition vector2d.h:683