20#include <boost/test/unit_test.hpp>
22#include <wx/filename.h>
52 const std::string sampleUtf8 =
"ABCDEF Привет 日本語 漢字";
53 wxString sample = wxString::FromUTF8( sampleUtf8.c_str() );
65 auto emitStyle = [&](
bool bold,
bool italic,
int yoff )
72 strokeFont.get(), metrics );
75 emitStyle(
false,
false, 0 );
76 emitStyle(
true,
false, 8000 );
77 emitStyle(
false,
true, 16000 );
78 emitStyle(
true,
true, 24000 );
85 BOOST_CHECK( buffer.rfind(
"%PDF", 0 ) == 0 );
90 BOOST_CHECK_MESSAGE( cmapCount >= 4,
"Expected at least 4 CMaps (got " << cmapCount <<
")" );
92 auto requireAll = [&](
const char* codeHex,
const char* label ) {
94 BOOST_CHECK_MESSAGE( occurrences >= 4,
"Codepoint " << label <<
" (" << codeHex
95 <<
") expected in all 4 styles; found " << occurrences );
98 requireAll(
"041F",
"Cyrillic PE" );
99 requireAll(
"65E5",
"Kanji 日" );
100 requireAll(
"672C",
"Kanji 本" );
106 const std::string sampleUtf8 =
"ABCDEF Привет 日本語 漢字";
107 wxString sample = wxString::FromUTF8( sampleUtf8.c_str() );
136 strokeFont.get(), metrics );
143 auto contains = [&](
const char* needle ) {
return PdfContains( buffer2, needle ); };
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 / 漢)" );
158 BOOST_CHECK_MESSAGE( darkPixels > 200,
159 "Rasterized PDF appears blank or too sparse (" << darkPixels
160 <<
" dark pixels)" );
171 const std::string sampleUtf8 =
"ABCDEF Привет 日本語 漢字";
172 wxString sample = wxString::FromUTF8( sampleUtf8.c_str() );
188 strokeFont.get(), metrics );
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)" );
198 const std::string sampleUtf8 =
"ABCDEF Привет 日本語 漢字";
199 wxString sample = wxString::FromUTF8( sampleUtf8.c_str() );
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)" );
223 const std::string sampleUtf8 =
"ABCDEF Привет 日本語 漢字";
224 wxString sample = wxString::FromUTF8( sampleUtf8.c_str() );
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)" );
253 const std::string sampleUtf8 =
"MW";
254 wxString sample = wxString::FromUTF8( sampleUtf8.c_str() );
271 strokeFont.get(), metrics );
282 double unitsPerEm = 1000.0;
284 double widthFactor = 300.0 / 3000.0;
285 double expectedHalfStroke = unitsPerEm * widthFactor / 2.0;
288 std::string::size_type pos = 0;
289 int checkedGlyphs = 0;
291 while( ( pos = buffer.find(
" d1 ", pos ) ) != std::string::npos )
294 std::string::size_type lineStart = buffer.rfind(
'\n', pos );
296 if( lineStart == std::string::npos )
301 std::string d1Line = buffer.substr( lineStart, pos + 3 - lineStart );
303 double width, wy, minX, minY, maxX, maxY;
305 if( sscanf( d1Line.c_str(),
"%lf %lf %lf %lf %lf %lf", &width, &wy, &minX, &minY,
306 &maxX, &maxY ) == 6 )
309 if( width == 0.0 && maxX == 0.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" );
323 BOOST_CHECK_MESSAGE( maxX > width,
324 "Glyph bbox maxX (" << maxX <<
") must exceed advance width ("
325 << width <<
") to prevent clipping" );
333 BOOST_CHECK_MESSAGE( checkedGlyphs >= 2,
334 "Expected at least 2 non-notdef glyphs, found " << checkedGlyphs );
345 const std::string sampleUtf8 =
"Yg Test ñ";
346 wxString sample = wxString::FromUTF8( sampleUtf8.c_str() );
370 BOOST_CHECK( buffer6.rfind(
"%PDF", 0 ) == 0 );
374 BOOST_CHECK_MESSAGE( buffer6.find(
"d1" ) != std::string::npos,
375 "PDF should contain d1 operators for glyph bounding boxes" );
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();
403 std::vector<wxString> embeddedFonts;
404 embeddedFonts.push_back( fontPath );
409 wxString sample = wxString::FromUTF8(
"Outline café" );
412 outlineFont, metrics );
416 wxFFile file( pdfPath,
"rb" );
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 );
423 appendDecompressed();
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" );
440 wxString rasterBase = rasterDir.
ChildPathStr(
"raster" );
441 wxString cmd = wxString::Format( wxT(
"pdftoppm -r 72 -singlefile -png \"%s\" \"%s\""),
442 pdfPath, rasterBase );
444 int ret = wxExecute( cmd, wxEXEC_SYNC );
448 wxString pngPath = rasterBase + wxT(
".png");
450 if( wxFileExists( pngPath ) )
453 if( !wxImage::FindHandler( wxBITMAP_TYPE_PNG ) )
454 wxImage::AddHandler(
new wxPNGHandler );
456 wxImage img( pngPath );
457 BOOST_REQUIRE_MESSAGE( img.IsOk(),
"Failed to load rasterized PDF image" );
460 int w = img.GetWidth();
461 int h = img.GetHeight();
463 for(
int y = 0; y < h; ++y )
465 for(
int x = 0; x < w; ++x )
467 unsigned char r = img.GetRed( x, y );
468 unsigned char g = img.GetGreen( x, y );
469 unsigned char b = img.GetBlue( x, y );
472 if( r < 240 || g < 240 || b < 240 )
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." );
485 BOOST_TEST_MESSAGE(
"pdftoppm succeeded but PNG output missing; skipping raster validation" );
500 std::vector<double> origins;
503 while( ( pos = aBuffer.find(
"cm BT", pos ) ) != std::string::npos )
505 std::string head = aBuffer.substr( 0, pos );
506 std::vector<double> nums;
508 for(
size_t end = head.size();
end > 0 && nums.size() < 6; )
510 size_t start = head.find_last_not_of(
" \n\r\t",
end - 1 );
512 if( start == std::string::npos )
515 size_t tokStart = head.find_last_of(
" \n\r\t", start );
516 tokStart = ( tokStart == std::string::npos ) ? 0 : tokStart + 1;
520 nums.push_back( std::stod( head.substr( tokStart, start - tokStart + 1 ) ) );
530 if( nums.size() == 6 )
531 origins.push_back( nums[1] );
560 const int sizeIu = 1270000;
567 strokeFont.get(), metrics );
569 strokeFont.get(), metrics );
573 attrs, strokeFont.get(), metrics );
579 BOOST_CHECK( buffer.rfind(
"%PDF", 0 ) == 0 );
584 BOOST_REQUIRE_EQUAL( origins.size(), 4u );
586 const double scale = ( origins[1] - origins[0] ) / ( 80000000.0 - 40000000.0 );
590 const double measuredOffsetIu = ( origins[3] - origins[2] ) /
scale;
592 const VECTOR2I size( sizeIu, sizeIu );
595 const double expectedOffsetIu =
596 strokeFont->GetTextAsGlyphs(
nullptr,
nullptr, wxT(
"v07:\t" ), size,
VECTOR2I(),
601 const double widthV07 =
602 strokeFont->GetTextAsGlyphs(
nullptr,
nullptr, wxT(
"v07:" ), size,
VECTOR2I(),
ANGLE_0,
605 const int oldTabWidth =
KiROUND( sizeIu * 4 * 0.6 );
606 const double oldOffsetIu = widthV07 + ( oldTabWidth -
KiROUND( widthV07 ) % oldTabWidth );
608 const double tolerance = sizeIu * 0.02;
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)" );
637 const int anchorY = 60000;
638 const int sizeIU = 3000;
639 const int strokeW = 300;
648 strokeFont.get(), metrics );
662 struct MATRIX_SAMPLE {
double a, b, c, d, e, f; };
663 std::vector<MATRIX_SAMPLE> matrices;
664 std::string::size_type pos = 0;
666 while( ( pos = buffer.find(
"cm BT", pos ) ) != std::string::npos )
668 std::string::size_type lineStart = buffer.rfind(
'\n', pos );
669 lineStart = ( lineStart == std::string::npos ) ? 0 : lineStart + 1;
671 std::string cmLine = buffer.substr( lineStart, pos - lineStart );
674 if( sscanf( cmLine.c_str(),
"q %lf %lf %lf %lf %lf %lf", &m.a, &m.b, &m.c, &m.d, &m.e,
677 matrices.push_back( m );
683 BOOST_REQUIRE_EQUAL( matrices.size(), 5u );
685 const double fontSize = (double) sizeIU;
686 const double thickness = (double) strokeW;
695 const double expected_delta_top_center = ( 1.000 - 0.415 ) * fontSize;
696 const double expected_delta_center_bot = ( 0.415 - ( -0.17 ) ) * fontSize;
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;
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 );
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 );
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." );
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 <<
")" );
724 const double italic_delta_e = matrices[4].e - matrices[3].e;
725 const double nonitalic_delta_e = matrices[1].e - matrices[0].e;
727 BOOST_CHECK_SMALL( nonitalic_delta_e, 1.0 );
728 BOOST_CHECK_SMALL( italic_delta_e, 1.0 );
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 );
761 const int sizeIU = 1900;
762 const int strokeW = 380;
771 wxT(
"TEST text Silkscreen" ), attrs, strokeFont.get(), metrics );
779 std::vector<double> wordOriginX;
780 std::string::size_type pos = 0;
782 while( ( pos = buffer.find(
"cm BT", pos ) ) != std::string::npos )
784 std::string::size_type lineStart = buffer.rfind(
'\n', pos );
785 lineStart = ( lineStart == std::string::npos ) ? 0 : lineStart + 1;
787 std::string cmLine = buffer.substr( lineStart, pos - lineStart );
788 double a, b, c, d, e, f;
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 );
797 BOOST_REQUIRE_EQUAL( wordOriginX.size(), 3u );
803 auto strokeAdvanceIU = [&](
const wxString& aWord )
806 ->GetTextAsGlyphs(
nullptr,
nullptr, aWord,
VECTOR2I( sizeIU, sizeIU ),
811 const double expectedTestAdvanceIU = (double) strokeAdvanceIU( wxT(
"TEST " ) );
812 const double expectedTextAdvanceIU = (double) strokeAdvanceIU( wxT(
"text " ) );
814 const double observedTestAdvanceDev = wordOriginX[1] - wordOriginX[0];
815 const double observedTextAdvanceDev = wordOriginX[2] - wordOriginX[1];
818 const double scale = observedTestAdvanceDev / expectedTestAdvanceIU;
823 const double expectedTextAdvanceDev =
scale * expectedTextAdvanceIU;
824 BOOST_CHECK_CLOSE( observedTextAdvanceDev, expectedTextAdvanceDev, 0.5 );
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." );
875 BOOST_CHECK( buffer.rfind(
"%PDF", 0 ) == 0 );
879 for(
const char* illegalPattern : {
"[0 0] 0 d",
"[0 0 0 0] 0 d",
"[0 0 0 0 0 0] 0 d" } )
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" );
887 BOOST_CHECK_MESSAGE( buffer.find(
"[] 0 d" ) != std::string::npos,
888 "Zero-width dashed style should fall back to a solid dash array" );
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" );
926 size_t dashPos = buffer.find(
"] 0 d" );
927 BOOST_REQUIRE_MESSAGE( dashPos != std::string::npos,
"No dash array was emitted" );
929 size_t openPos = buffer.rfind(
'[', dashPos );
930 BOOST_REQUIRE( openPos != std::string::npos && openPos < dashPos );
932 std::string dashArray = buffer.substr( openPos, dashPos - openPos + 1 );
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 "
937 BOOST_CHECK_MESSAGE( dashArray !=
"[]",
938 "A non-zero pen width must not fall back to the solid line" );
constexpr BOX2I KiROUND(const BOX2D &aBoxD)
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.
static FONT * GetFont(const wxString &aFontName=wxEmptyString, bool aBold=false, bool aItalic=false, const std::vector< wxString > *aEmbeddedFiles=nullptr, bool aForDrawingSheet=false)
A color representation with 4 components: red, green, blue, alpha.
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)
void MoveTo(const VECTOR2I &pos)
void FinishTo(const VECTOR2I &pos)
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
@ FILLED_SHAPE
Fill with object color.
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)
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)
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)
BOOST_TEST_MESSAGE("Polyline has "<< chain.PointCount()<< " points")
GR_TEXT_V_ALIGN_T
This is API surface mapped to common.types.VertialAlignment.
VECTOR2< int32_t > VECTOR2I