diff --git a/common/CMakeLists.txt b/common/CMakeLists.txt index 7faab9f847..f98bf83483 100644 --- a/common/CMakeLists.txt +++ b/common/CMakeLists.txt @@ -593,6 +593,8 @@ set( PLOTTERS_CONTROL_SRCS plotters/DXF_plotter.cpp plotters/GERBER_plotter.cpp plotters/PDF_plotter.cpp + plotters/pdf_stroke_font.cpp + plotters/pdf_outline_font.cpp plotters/PS_plotter.cpp plotters/SVG_plotter.cpp plotters/common_plot_functions.cpp diff --git a/common/advanced_config.cpp b/common/advanced_config.cpp index 299c18dc8b..84ef2f20f5 100644 --- a/common/advanced_config.cpp +++ b/common/advanced_config.cpp @@ -84,6 +84,11 @@ static const wxChar ExtraZoneDisplayModes[] = wxT( "ExtraZoneDisplayModes" ); static const wxChar MinPlotPenWidth[] = wxT( "MinPlotPenWidth" ); static const wxChar DebugZoneFiller[] = wxT( "DebugZoneFiller" ); static const wxChar DebugPDFWriter[] = wxT( "DebugPDFWriter" ); +static const wxChar PDFStrokeFontWidthFactor[] = wxT( "PDFStrokeFontWidthFactor" ); +static const wxChar PDFStrokeFontXOffset[] = wxT( "PDFStrokeFontXOffset" ); +static const wxChar PDFStrokeFontYOffset[] = wxT( "PDFStrokeFontYOffset" ); +static const wxChar PDFStrokeFontBoldMultiplier[] = wxT( "PDFStrokeFontBoldMultiplier" ); +static const wxChar PDFStrokeFontKerningFactor[] = wxT( "PDFStrokeFontKerningFactor" ); static const wxChar UsePdfPrint[] = wxT( "UsePdfPrint" ); static const wxChar SmallDrillMarkSize[] = wxT( "SmallDrillMarkSize" ); static const wxChar HotkeysDumper[] = wxT( "HotkeysDumper" ); @@ -248,6 +253,11 @@ ADVANCED_CFG::ADVANCED_CFG() m_DebugZoneFiller = false; m_DebugPDFWriter = false; + m_PDFStrokeFontWidthFactor = .12; // default 12% of EM + m_PDFStrokeFontXOffset = 0.1; + m_PDFStrokeFontYOffset = 0.35; + m_PDFStrokeFontBoldMultiplier = 1.8; + m_PDFStrokeFontKerningFactor = 1.0; m_UsePdfPrint = false; m_SmallDrillMarkSize = 0.35; m_HotkeysDumper = false; @@ -463,6 +473,21 @@ void ADVANCED_CFG::loadSettings( wxConfigBase& aCfg ) m_entries.push_back( std::make_unique( true, AC_KEYS::DebugPDFWriter, &m_DebugPDFWriter, m_DebugPDFWriter ) ); + m_entries.push_back( std::make_unique( true, AC_KEYS::PDFStrokeFontWidthFactor, + &m_PDFStrokeFontWidthFactor, m_PDFStrokeFontWidthFactor ) ); + + m_entries.push_back( std::make_unique( true, AC_KEYS::PDFStrokeFontXOffset, + &m_PDFStrokeFontXOffset, m_PDFStrokeFontXOffset ) ); + + m_entries.push_back( std::make_unique( true, AC_KEYS::PDFStrokeFontYOffset, + &m_PDFStrokeFontYOffset, m_PDFStrokeFontYOffset ) ); + + m_entries.push_back( std::make_unique( true, AC_KEYS::PDFStrokeFontBoldMultiplier, + &m_PDFStrokeFontBoldMultiplier, m_PDFStrokeFontBoldMultiplier ) ); + + m_entries.push_back( std::make_unique( true, AC_KEYS::PDFStrokeFontKerningFactor, + &m_PDFStrokeFontKerningFactor, m_PDFStrokeFontKerningFactor ) ); + m_entries.push_back( std::make_unique( true, AC_KEYS::UsePdfPrint, &m_UsePdfPrint, m_UsePdfPrint ) ); diff --git a/common/font/stroke_font.cpp b/common/font/stroke_font.cpp index d7eb108366..ba50b77a70 100644 --- a/common/font/stroke_font.cpp +++ b/common/font/stroke_font.cpp @@ -289,3 +289,29 @@ VECTOR2I STROKE_FONT::GetTextAsGlyphs( BOX2I* aBBox, std::vectorsize() : 0; +} + + +const STROKE_GLYPH* STROKE_FONT::GetGlyph( unsigned aIndex ) const +{ + if( !m_glyphs || aIndex >= m_glyphs->size() ) + return nullptr; + + return static_cast( m_glyphs->at( aIndex ).get() ); +} + + +const BOX2D& STROKE_FONT::GetGlyphBoundingBox( unsigned aIndex ) const +{ + static const BOX2D empty; + + if( !m_glyphBoundingBoxes || aIndex >= m_glyphBoundingBoxes->size() ) + return empty; + + return m_glyphBoundingBoxes->at( aIndex ); +} diff --git a/common/plotters/PDF_plotter.cpp b/common/plotters/PDF_plotter.cpp index 4ac04f8534..3cd3ad8a40 100644 --- a/common/plotters/PDF_plotter.cpp +++ b/common/plotters/PDF_plotter.cpp @@ -1,8 +1,3 @@ -/** - * @file PDF_plotter.cpp - * @brief KiCad: specialized plotter for PDF files format - */ - /* * This program source code file is part of KiCad, a free EDA CAD application. * @@ -28,6 +23,7 @@ */ #include +#include #include // snprintf #include @@ -44,14 +40,22 @@ #include #include #include +#include #include #include +#include #include #include #include +#include +#include #include #include +#include + + +PDF_PLOTTER::~PDF_PLOTTER() = default; std::string PDF_PLOTTER::encodeStringForPlotter( const wxString& aText ) @@ -116,6 +120,34 @@ std::string PDF_PLOTTER::encodeStringForPlotter( const wxString& aText ) } +std::string PDF_PLOTTER::encodeByteString( const std::string& aBytes ) +{ + std::string result; + result.reserve( aBytes.size() * 4 + 2 ); + result.push_back( '(' ); + + for( unsigned char byte : aBytes ) + { + if( byte == '(' || byte == ')' || byte == '\\' ) + { + result.push_back( '\\' ); + result.push_back( static_cast( byte ) ); + } + else if( byte < 32 || byte > 126 ) + { + fmt::format_to( std::back_inserter( result ), "\\{:03o}", byte ); + } + else + { + result.push_back( static_cast( byte ) ); + } + } + + result.push_back( ')' ); + return result; +} + + bool PDF_PLOTTER::OpenFile( const wxString& aFullFilename ) { m_filename = aFullFilename; @@ -610,17 +642,15 @@ int PDF_PLOTTER::startPdfStream( int aHandle ) if( ADVANCED_CFG::GetCfg().m_DebugPDFWriter ) { - fmt::println( m_outputFile, - "<< /Length {} 0 R >>\n" - "stream", - handle + 1 ); + fmt::print( m_outputFile, + "<< /Length {} 0 R >>\nstream\n", + m_streamLengthHandle ); } else { - fmt::println( m_outputFile, - "<< /Length {} 0 R /Filter /FlateDecode >>\n" - "stream", - handle + 1 ); + fmt::print( m_outputFile, + "<< /Length {} 0 R /Filter /FlateDecode >>\nstream\n", + m_streamLengthHandle ); } // Open a temporary file to accumulate the stream @@ -1059,6 +1089,16 @@ bool PDF_PLOTTER::StartPlot( const wxString& aPageNumber, const wxString& aPageN m_outlineRoot = std::make_unique(); + if( !m_strokeFontManager ) + m_strokeFontManager = std::make_unique(); + else + m_strokeFontManager->Reset(); + + if( !m_outlineFontManager ) + m_outlineFontManager = std::make_unique(); + else + m_outlineFontManager->Reset(); + /* The header (that's easy!). The second line is binary junk required to make the file binary from the beginning (the important thing is that they must have the bit 7 set) */ @@ -1225,46 +1265,204 @@ int PDF_PLOTTER::emitOutline() return -1; } -void PDF_PLOTTER::endPlotEmitResources() +void PDF_PLOTTER::emitStrokeFonts() { - /* We need to declare the resources we're using (fonts in particular) - The useful standard one is the Helvetica family. Adding external fonts - is *very* involved! */ - struct { - const char *psname; - const char *rsname; - int font_handle; - } fontdefs[4] = { - { "/Helvetica", "/KicadFont", 0 }, - { "/Helvetica-Oblique", "/KicadFontI", 0 }, - { "/Helvetica-Bold", "/KicadFontB", 0 }, - { "/Helvetica-BoldOblique", "/KicadFontBI", 0 } - }; + if( !m_strokeFontManager ) + return; - /* Declare the font resources. Since they're builtin fonts, no descriptors (yay!) - We'll need metrics anyway to do any alignment (these are in the shared with - the postscript engine) */ - for( int i = 0; i < 4; i++ ) + for( PDF_STROKE_FONT_SUBSET* subsetPtr : m_strokeFontManager->AllSubsets() ) { - fontdefs[i].font_handle = startPdfObject(); - fmt::println( m_outputFile, - "<< /BaseFont {}\n" - " /Type /Font\n" - " /Subtype /Type1\n" - " /Encoding /WinAnsiEncoding\n" - ">>", - fontdefs[i].psname ); + PDF_STROKE_FONT_SUBSET& subset = *subsetPtr; + + if( subset.GlyphCount() <= 1 ) + { + subset.SetCharProcsHandle( -1 ); + subset.SetFontHandle( -1 ); + subset.SetToUnicodeHandle( -1 ); + continue; + } + + for( PDF_STROKE_FONT_SUBSET::GLYPH& glyph : subset.Glyphs() ) + { + int charProcHandle = startPdfStream(); + + if( !glyph.m_stream.empty() ) + fmt::print( m_workFile, "{}\n", glyph.m_stream ); + + closePdfStream(); + glyph.m_charProcHandle = charProcHandle; + } + + int charProcDictHandle = startPdfObject(); + fmt::println( m_outputFile, "<<" ); + + for( const PDF_STROKE_FONT_SUBSET::GLYPH& glyph : subset.Glyphs() ) + fmt::println( m_outputFile, " /{} {} 0 R", glyph.m_name, glyph.m_charProcHandle ); + + fmt::println( m_outputFile, ">>" ); closePdfObject(); + subset.SetCharProcsHandle( charProcDictHandle ); + + int toUnicodeHandle = startPdfStream(); + std::string cmap = subset.BuildToUnicodeCMap(); + + if( !cmap.empty() ) + fmt::print( m_workFile, "{}", cmap ); + + closePdfStream(); + subset.SetToUnicodeHandle( toUnicodeHandle ); + + double fontMatrixScale = 1.0 / subset.UnitsPerEm(); + double minX = subset.FontBBoxMinX(); + double minY = subset.FontBBoxMinY(); + double maxX = subset.FontBBoxMaxX(); + double maxY = subset.FontBBoxMaxY(); + + int fontHandle = startPdfObject(); + fmt::print( m_outputFile, + "<<\n/Type /Font\n/Subtype /Type3\n/Name {}\n/FontBBox [ {:g} {:g} {:g} {:g} ]\n", + subset.ResourceName(), + minX, + minY, + maxX, + maxY ); + fmt::print( m_outputFile, + "/FontMatrix [ {:g} 0 0 {:g} 0 0 ]\n/CharProcs {} 0 R\n", + fontMatrixScale, + fontMatrixScale, + subset.CharProcsHandle() ); + fmt::print( m_outputFile, + "/Encoding << /Type /Encoding /Differences {} >>\n", + subset.BuildDifferencesArray() ); + fmt::print( m_outputFile, + "/FirstChar {}\n/LastChar {}\n/Widths {}\n", + subset.FirstChar(), + subset.LastChar(), + subset.BuildWidthsArray() ); + fmt::print( m_outputFile, + "/ToUnicode {} 0 R\n/Resources << /ProcSet [/PDF /Text] >>\n>>\n", + subset.ToUnicodeHandle() ); + closePdfObject(); + subset.SetFontHandle( fontHandle ); } +} + + +void PDF_PLOTTER::emitOutlineFonts() +{ + if( !m_outlineFontManager ) + return; + + for( PDF_OUTLINE_FONT_SUBSET* subsetPtr : m_outlineFontManager->AllSubsets() ) + { + if( !subsetPtr || !subsetPtr->HasGlyphs() ) + continue; + + const std::vector& fontData = subsetPtr->FontFileData(); + + if( fontData.empty() ) + continue; + + int fontFileHandle = startPdfStream(); + subsetPtr->SetFontFileHandle( fontFileHandle ); + + if( !fontData.empty() ) + fwrite( fontData.data(), fontData.size(), 1, m_workFile ); + + closePdfStream(); + + std::string cidMap = subsetPtr->BuildCIDToGIDStream(); + int cidMapHandle = startPdfStream(); + subsetPtr->SetCIDMapHandle( cidMapHandle ); + + if( !cidMap.empty() ) + fwrite( cidMap.data(), cidMap.size(), 1, m_workFile ); + + closePdfStream(); + + std::string toUnicode = subsetPtr->BuildToUnicodeCMap(); + int toUnicodeHandle = startPdfStream(); + subsetPtr->SetToUnicodeHandle( toUnicodeHandle ); + + if( !toUnicode.empty() ) + fmt::print( m_workFile, "{}", toUnicode ); + + closePdfStream(); + + int descriptorHandle = startPdfObject(); + subsetPtr->SetFontDescriptorHandle( descriptorHandle ); + + fmt::print( m_outputFile, + "<<\n/Type /FontDescriptor\n/FontName /{}\n/Flags {}\n/ItalicAngle {:g}\n/Ascent {:g}\n/Descent {:g}\n" + "/CapHeight {:g}\n/StemV {:g}\n/FontBBox [ {:g} {:g} {:g} {:g} ]\n/FontFile2 {} 0 R\n>>\n", + subsetPtr->BaseFontName(), + subsetPtr->Flags(), + subsetPtr->ItalicAngle(), + subsetPtr->Ascent(), + subsetPtr->Descent(), + subsetPtr->CapHeight(), + subsetPtr->StemV(), + subsetPtr->BBoxMinX(), + subsetPtr->BBoxMinY(), + subsetPtr->BBoxMaxX(), + subsetPtr->BBoxMaxY(), + subsetPtr->FontFileHandle() ); + closePdfObject(); + + int cidFontHandle = startPdfObject(); + subsetPtr->SetCIDFontHandle( cidFontHandle ); + + std::string widths = subsetPtr->BuildWidthsArray(); + + fmt::print( m_outputFile, + "<<\n/Type /Font\n/Subtype /CIDFontType2\n/BaseFont /{}\n" + "/CIDSystemInfo << /Registry (Adobe) /Ordering (Identity) /Supplement 0 >>\n" + "/FontDescriptor {} 0 R\n/W {}\n/CIDToGIDMap {} 0 R\n>>\n", + subsetPtr->BaseFontName(), + subsetPtr->FontDescriptorHandle(), + widths, + subsetPtr->CIDMapHandle() ); + closePdfObject(); + + int fontHandle = startPdfObject(); + subsetPtr->SetFontHandle( fontHandle ); + + fmt::print( m_outputFile, + "<<\n/Type /Font\n/Subtype /Type0\n/BaseFont /{}\n/Encoding /Identity-H\n" + "/DescendantFonts [ {} 0 R ]\n/ToUnicode {} 0 R\n>>\n", + subsetPtr->BaseFontName(), + subsetPtr->CIDFontHandle(), + subsetPtr->ToUnicodeHandle() ); + closePdfObject(); + } +} + + +void PDF_PLOTTER::endPlotEmitResources() +{ + emitOutlineFonts(); + emitStrokeFonts(); - // Named font dictionary (was allocated, now we emit it) startPdfObject( m_fontResDictHandle ); fmt::println( m_outputFile, "<<" ); - for( int i = 0; i < 4; i++ ) + if( m_outlineFontManager ) { - fmt::println( m_outputFile, " {} {} 0 R", - fontdefs[i].rsname, fontdefs[i].font_handle ); + for( PDF_OUTLINE_FONT_SUBSET* subsetPtr : m_outlineFontManager->AllSubsets() ) + { + if( subsetPtr && subsetPtr->FontHandle() >= 0 ) + fmt::println( m_outputFile, " {} {} 0 R", subsetPtr->ResourceName(), subsetPtr->FontHandle() ); + } + } + + if( m_strokeFontManager ) + { + for( PDF_STROKE_FONT_SUBSET* subsetPtr : m_strokeFontManager->AllSubsets() ) + { + const PDF_STROKE_FONT_SUBSET& subset = *subsetPtr; + if( subset.FontHandle() >= 0 ) + fmt::println( m_outputFile, " {} {} 0 R", subset.ResourceName(), subset.FontHandle() ); + } } fmt::println( m_outputFile, ">>" ); @@ -1714,6 +1912,14 @@ bool PDF_PLOTTER::EndPlot() } +struct OverbarInfo +{ + VECTOR2I startPos; + VECTOR2I endPos; + VECTOR2I fontSize; +}; + + void PDF_PLOTTER::Text( const VECTOR2I& aPos, const COLOR4D& aColor, const wxString& aText, @@ -1733,34 +1939,45 @@ void PDF_PLOTTER::Text( const VECTOR2I& aPos, if( aSize.x == 0 || aSize.y == 0 ) return; - // Render phantom text (which will be searchable) behind the stroke font. This won't - // be pixel-accurate, but it doesn't matter for searching. - int render_mode = 3; // invisible + wxString text( aText ); - VECTOR2I pos( aPos ); - const char *fontname = aItalic ? ( aBold ? "/KicadFontBI" : "/KicadFontI" ) - : ( aBold ? "/KicadFontB" : "/KicadFont" ); + if( text.Contains( wxS( "@{" ) ) ) + { + EXPRESSION_EVALUATOR evaluator; + text = evaluator.Evaluate( text ); + } - // Compute the copious transformation parameters of the Current Transform Matrix - double ctm_a, ctm_b, ctm_c, ctm_d, ctm_e, ctm_f; - double wideningFactor, heightFactor; + SetColor( aColor ); + SetCurrentLineWidth( aWidth, aData ); + + if( !aFont ) + aFont = KIFONT::FONT::GetFont( m_renderSettings->GetDefaultFont() ); VECTOR2I t_size( std::abs( aSize.x ), std::abs( aSize.y ) ); bool textMirrored = aSize.x < 0; - computeTextParameters( aPos, aText, aOrient, t_size, textMirrored, aH_justify, aV_justify, - aWidth, aItalic, aBold, &wideningFactor, &ctm_a, &ctm_b, &ctm_c, &ctm_d, - &ctm_e, &ctm_f, &heightFactor ); - - SetColor( aColor ); - SetCurrentLineWidth( aWidth, aData ); + // Parse the text for markup + MARKUP::MARKUP_PARSER markupParser( text.ToStdString() ); + std::unique_ptr markupTree( markupParser.Parse() ); - wxStringTokenizer str_tok( aText, " ", wxTOKEN_RET_DELIMS ); + if( !markupTree ) + { + wxLogTrace( tracePdfPlotter, "PDF_PLOTTER::Text: Markup parsing failed, falling back to plain text." ); + // Fallback to simple text rendering if parsing fails + wxStringTokenizer str_tok( text, " ", wxTOKEN_RET_DELIMS ); + VECTOR2I pos = aPos; - if( !aFont ) - aFont = KIFONT::FONT::GetFont( m_renderSettings->GetDefaultFont() ); + while( str_tok.HasMoreTokens() ) + { + wxString word = str_tok.GetNextToken(); + pos = renderWord( word, pos, t_size, aOrient, textMirrored, aWidth, aBold, aItalic, + aFont, aFontMetrics, aV_justify, 0 ); + } + return; + } - VECTOR2I full_box( aFont->StringBoundaryLimits( aText, t_size, aWidth, aBold, aItalic, + // Calculate the full text bounding box for alignment + VECTOR2I full_box( aFont->StringBoundaryLimits( text, t_size, aWidth, aBold, aItalic, aFontMetrics ) ); if( textMirrored ) @@ -1772,6 +1989,7 @@ void PDF_PLOTTER::Text( const VECTOR2I& aPos, RotatePoint( box_x, aOrient ); RotatePoint( box_y, aOrient ); + VECTOR2I pos( aPos ); if( aH_justify == GR_TEXT_H_ALIGN_CENTER ) pos -= box_x / 2; else if( aH_justify == GR_TEXT_H_ALIGN_RIGHT ) @@ -1782,49 +2000,468 @@ void PDF_PLOTTER::Text( const VECTOR2I& aPos, else if( aV_justify == GR_TEXT_V_ALIGN_TOP ) pos += box_y; - while( str_tok.HasMoreTokens() ) + // Render markup tree + std::vector overbars; + renderMarkupNode( markupTree.get(), pos, t_size, aOrient, textMirrored, aWidth, + aBold, aItalic, aFont, aFontMetrics, aV_justify, 0, overbars ); + + // Draw any overbars that were accumulated + drawOverbars( overbars, aOrient, aFontMetrics ); +} + + +VECTOR2I PDF_PLOTTER::renderWord( const wxString& aWord, const VECTOR2I& aPosition, + const VECTOR2I& aSize, const EDA_ANGLE& aOrient, + bool aTextMirrored, int aWidth, bool aBold, bool aItalic, + KIFONT::FONT* aFont, const KIFONT::METRICS& aFontMetrics, + enum GR_TEXT_V_ALIGN_T aV_justify, TEXT_STYLE_FLAGS aTextStyle ) +{ + if( wxGetEnv( "KICAD_DEBUG_SYN_STYLE", nullptr ) ) { - wxString word = str_tok.GetNextToken(); + int styleFlags = 0; + if( aFont && aFont->IsOutline() ) + styleFlags = static_cast( aFont )->GetFace() ? static_cast( aFont )->GetFace()->style_flags : 0; + wxLogTrace( tracePdfPlotter, "renderWord enter word='%s' bold=%d italic=%d textStyle=%u styleFlags=%d", TO_UTF8( aWord ), (int) aBold, (int) aItalic, (unsigned) aTextStyle, styleFlags ); + } - computeTextParameters( pos, word, aOrient, t_size, textMirrored, GR_TEXT_H_ALIGN_LEFT, - GR_TEXT_V_ALIGN_BOTTOM, aWidth, aItalic, aBold, &wideningFactor, - &ctm_a, &ctm_b, &ctm_c, &ctm_d, &ctm_e, &ctm_f, &heightFactor ); + // Don't try to output a blank string, but handle space characters for word separation + if( aWord.empty() ) + return aPosition; - // Extract the changed width and rotate by the orientation to get the offset for the - // next word - VECTOR2I bbox( aFont->StringBoundaryLimits( word, t_size, aWidth, - aBold, aItalic, aFontMetrics ).x, 0 ); + // If the word is just a space character, advance position by space width and continue + if( aWord == wxT(" ") ) + { + // Calculate space width and advance position + VECTOR2I spaceBox( aFont->StringBoundaryLimits( wxT("n"), aSize, aWidth, + aBold, aItalic, aFontMetrics ).x / 2, 0 ); - if( textMirrored ) - bbox.x *= -1; + if( aTextMirrored ) + spaceBox.x *= -1; - RotatePoint( bbox, aOrient ); - pos += bbox; + VECTOR2I rotatedSpaceBox = spaceBox; + RotatePoint( rotatedSpaceBox, aOrient ); + return aPosition + rotatedSpaceBox; + } - // Don't try to output a blank string - if( word.Trim( false ).Trim( true ).empty() ) - continue; + // Compute transformation parameters for this word + double ctm_a, ctm_b, ctm_c, ctm_d, ctm_e, ctm_f; + double wideningFactor, heightFactor; + + computeTextParameters( aPosition, aWord, aOrient, aSize, aTextMirrored, + GR_TEXT_H_ALIGN_LEFT, GR_TEXT_V_ALIGN_BOTTOM, aWidth, + aItalic, aBold, &wideningFactor, &ctm_a, &ctm_b, &ctm_c, + &ctm_d, &ctm_e, &ctm_f, &heightFactor ); + + // Calculate next position for word spacing + VECTOR2I bbox( aFont->StringBoundaryLimits( aWord, aSize, aWidth, aBold, aItalic, aFontMetrics ).x, 0 ); + if( aTextMirrored ) + bbox.x *= -1; + RotatePoint( bbox, aOrient ); + VECTOR2I nextPos = aPosition + bbox; + + // Apply vertical offset for subscript/superscript + // Stroke font positioning (baseline) already correct per user feedback. + // Outline fonts need: superscript +1 full font height higher; subscript +1 full font height higher + if( aTextStyle & TEXT_STYLE::SUPERSCRIPT ) + { + double factor = ( aFont && aFont->IsOutline() ) ? 0.050 : 0.030; // stroke original ~0.40, outline needs +1.0 + VECTOR2I offset( 0, static_cast( std::lround( aSize.y * factor ) ) ); + RotatePoint( offset, aOrient ); + ctm_e -= offset.x; + ctm_f += offset.y; // Note: PDF Y increases upward + } + else if( aTextStyle & TEXT_STYLE::SUBSCRIPT ) + { + // For outline fonts raise by one font height versus stroke (which shifts downward slightly) + VECTOR2I offset( 0, 0 ); + + if( !aFont || aFont->IsStroke() ) + offset.y = static_cast( std::lround( aSize.y * 0.01 ) ); + + RotatePoint( offset, aOrient ); + ctm_e += offset.x; + ctm_f -= offset.y; // Note: PDF Y increases upward + } + + // Render the word using existing outline font logic + bool useOutlineFont = aFont && aFont->IsOutline(); + + if( useOutlineFont ) + { + std::vector outlineRuns; + + if( m_outlineFontManager ) + { + m_outlineFontManager->EncodeString( aWord, static_cast( aFont ), + ( aItalic || ( aTextStyle & TEXT_STYLE::ITALIC ) ), + ( aBold || ( aTextStyle & TEXT_STYLE::BOLD ) ), + &outlineRuns ); + } + + if( !outlineRuns.empty() ) + { + // Apply baseline adjustment (keeping existing logic) + double baseline_factor = 0.17; + double alignment_multiplier = 1.0; + if( aV_justify == GR_TEXT_V_ALIGN_CENTER ) + alignment_multiplier = 2.0; + else if( aV_justify == GR_TEXT_V_ALIGN_TOP ) + alignment_multiplier = 4.0; + + VECTOR2D font_size_dev = userToDeviceSize( aSize ); + double baseline_adjustment = font_size_dev.y * baseline_factor * alignment_multiplier; + + double adjusted_ctm_e = ctm_e; + double adjusted_ctm_f = ctm_f; + + double angle_rad = aOrient.AsRadians(); + double cos_angle = cos( angle_rad ); + double sin_angle = sin( angle_rad ); + + adjusted_ctm_e = ctm_e - baseline_adjustment * sin_angle; + adjusted_ctm_f = ctm_f + baseline_adjustment * cos_angle; + + double adj_c = ctm_c; + double adj_d = ctm_d; + + if( aItalic && ( !aFont || !aFont->IsOutline() ) ) + { + double tilt = -ITALIC_TILT; + if( wideningFactor < 0 ) + tilt = -tilt; + + adj_c -= ctm_a * tilt; + adj_d -= ctm_b * tilt; + } + + // Synthetic italic (shear) for outline font if requested but font not intrinsically italic + bool syntheticItalicApplied = false; + double appliedTilt = 0.0; + double syn_c = adj_c; + double syn_d = adj_d; + double syn_a = ctm_a; + double syn_b = ctm_b; + bool wantItalic = ( aItalic || ( aTextStyle & TEXT_STYLE::ITALIC ) ); + if( std::getenv( "KICAD_FORCE_SYN_ITALIC" ) ) + wantItalic = true; // debug: ensure path triggers when forcing synthetic italic + bool wantBold = ( aBold || ( aTextStyle & TEXT_STYLE::BOLD ) ); + bool fontIsItalic = aFont && aFont->IsOutline() && aFont->IsItalic(); + bool fontIsBold = aFont && aFont->IsOutline() && aFont->IsBold(); + bool fontIsFakeItalic = aFont && aFont->IsOutline() && static_cast( aFont )->IsFakeItalic(); + bool fontIsFakeBold = aFont && aFont->IsOutline() && static_cast( aFont )->IsFakeBold(); + + // Environment overrides for testing synthetic italics: + // KICAD_FORCE_SYN_ITALIC=1 forces synthetic shear even if font has italic face + // KICAD_SYN_ITALIC_TILT=: if value contains 'deg' treat as degrees, + // otherwise treat as raw tilt factor (x += tilt*y) + bool forceSynItalic = false; + double overrideTilt = 0.0; + if( const char* envForce = std::getenv( "KICAD_FORCE_SYN_ITALIC" ) ) + { + if( *envForce != '\0' && *envForce != '0' ) + forceSynItalic = true; + } + + if( const char* envTilt = std::getenv( "KICAD_SYN_ITALIC_TILT" ) ) + { + std::string tiltStr( envTilt ); + try + { + if( tiltStr.find( "deg" ) != std::string::npos ) + { + double deg = std::stod( tiltStr ); + overrideTilt = tan( deg * M_PI / 180.0 ); + } + else + { + overrideTilt = std::stod( tiltStr ); + } + } + catch( ... ) + { + overrideTilt = 0.0; // ignore malformed + } + } + + // Trace after we know style flags + wxLogTrace( tracePdfPlotter, + "Outline path word='%s' runs=%zu wantItalic=%d fontIsItalic=%d fontIsFakeItalic=%d wantBold=%d fontIsBold=%d fontIsFakeBold=%d forceSyn=%d", + TO_UTF8( aWord ), outlineRuns.size(), (int) wantItalic, (int) fontIsItalic, + (int) fontIsFakeItalic, (int) wantBold, (int) fontIsBold, (int) fontIsFakeBold, + (int) forceSynItalic ); + + // Apply synthetic italic if: + // - Italic requested AND outline font + // - And either forceSynItalic env var set OR there is no REAL italic face. + // (A fake italic flag from fontconfig substitution should NOT block synthetic shear.) + bool realItalicFace = aFont && aFont->IsOutline() && aFont->IsItalic() && !fontIsFakeItalic; + if( wantItalic && aFont && aFont->IsOutline() && ( forceSynItalic || !realItalicFace ) ) + { + // We want to apply a horizontal shear so that x' = x + tilt * y in the glyph's + // local coordinate system BEFORE rotation. The existing text matrix columns are: + // first column = (a, b)^T -> x-axis direction & scale + // second column = (c, d)^T -> y-axis direction & scale + // Prepending a shear matrix S = [[1 tilt][0 1]] (i.e. T' = T * S is WRONG here). + // We need to LEFT-multiply: T' = R * S where R is the original rotation/scale. + // Left multiplication keeps first column unchanged and adds (tilt * firstColumn) + // to the second column: (c', d') = (c + tilt * a, d + tilt * b). + // This produces a right-leaning italic for positive tilt. + double tilt = ( overrideTilt != 0.0 ) ? overrideTilt : ITALIC_TILT; + if( wideningFactor < 0 ) // mirrored text should mirror the shear + tilt = -tilt; + + syn_c = adj_c + tilt * syn_a; + syn_d = adj_d + tilt * syn_b; + appliedTilt = tilt; + syntheticItalicApplied = true; + + wxLogTrace( tracePdfPlotter, + "Synthetic italic shear applied: tilt=%f a=%f b=%f c->%f d->%f", + tilt, syn_a, syn_b, syn_c, syn_d ); + } + + if( wantBold && aFont && aFont->IsOutline() && !fontIsBold ) + { + // Slight horizontal widening to simulate bold (~3%) + syn_a *= 1.03; + syn_b *= 1.03; + } + + if( syntheticItalicApplied ) + { + // PDF comment to allow manual inspection in the output stream + fmt::print( m_workFile, "% syn-italic tilt={} a={} b={} c={} d={}\n", appliedTilt, syn_a, syn_b, syn_c, syn_d ); + } + + fmt::print( m_workFile, "q {:f} {:f} {:f} {:f} {:f} {:f} cm BT {} Tr {:g} Tz ", + syn_a, syn_b, syn_c, syn_d, adjusted_ctm_e, adjusted_ctm_f, + 0, // render_mode + wideningFactor * 100 ); + + for( const PDF_OUTLINE_FONT_RUN& run : outlineRuns ) + { + fmt::print( m_workFile, "{} {:g} Tf <", run.m_subset->ResourceName(), heightFactor ); + + for( const PDF_OUTLINE_FONT_GLYPH& glyph : run.m_glyphs ) + { + fmt::print( m_workFile, "{:02X}{:02X}", + static_cast( ( glyph.cid >> 8 ) & 0xFF ), + static_cast( glyph.cid & 0xFF ) ); + } + + fmt::print( m_workFile, "> Tj " ); + } + + fmt::println( m_workFile, "ET" ); + fmt::println( m_workFile, "Q" ); + } + } + else + { + // Handle stroke fonts + if( !m_strokeFontManager ) + return nextPos; + + wxLogTrace( tracePdfPlotter, "Stroke path word='%s' wantItalic=%d aItalic=%d aBold=%d", + TO_UTF8( aWord ), (int) ( aItalic || ( aTextStyle & TEXT_STYLE::ITALIC ) ), (int) aItalic, (int) aBold ); + + std::vector runs; + m_strokeFontManager->EncodeString( aWord, &runs, aBold, aItalic ); + + if( !runs.empty() ) + { + VECTOR2D dev_size = userToDeviceSize( aSize ); + double fontSize = dev_size.y; - /* We use the full CTM instead of the text matrix because the same - coordinate system will be used for the overlining. Also the %f - for the trig part of the matrix to avoid %g going in exponential - format (which is not supported) */ - fmt::print( m_workFile, "q {:f} {:f} {:f} {:f} {:f} {:f} cm BT {} {:g} Tf {} Tr {:g} Tz ", - ctm_a, ctm_b, ctm_c, ctm_d, ctm_e, ctm_f, - fontname, - heightFactor, - render_mode, - wideningFactor * 100 ); - - std::string txt_pdf = encodeStringForPlotter( word ); - fmt::println( m_workFile, "{} Tj ET", txt_pdf ); - // Restore the CTM - fmt::println( m_workFile, "Q" ); - } - - // Plot the stroked text (if requested) - PLOTTER::Text( aPos, aColor, aText, aOrient, aSize, aH_justify, aV_justify, aWidth, aItalic, - aBold, aMultilineAllowed, aFont, aFontMetrics ); + double adj_c = ctm_c; + double adj_d = ctm_d; + + if( aItalic && ( !aFont || !aFont->IsOutline() ) ) + { + double tilt = -ITALIC_TILT; + if( wideningFactor < 0 ) + tilt = -tilt; + + adj_c -= ctm_a * tilt; + adj_d -= ctm_b * tilt; + } + + fmt::print( m_workFile, "q {:f} {:f} {:f} {:f} {:f} {:f} cm BT {} Tr {:g} Tz ", + ctm_a, ctm_b, adj_c, adj_d, ctm_e, ctm_f, + 0, // render_mode + wideningFactor * 100 ); + + for( const PDF_STROKE_FONT_RUN& run : runs ) + { + fmt::print( m_workFile, "{} {:g} Tf {} Tj ", + run.m_subset->ResourceName(), + fontSize, + encodeByteString( run.m_bytes ) ); + } + + fmt::println( m_workFile, "ET" ); + fmt::println( m_workFile, "Q" ); + } + } + + return nextPos; +} + + +VECTOR2I PDF_PLOTTER::renderMarkupNode( const MARKUP::NODE* aNode, const VECTOR2I& aPosition, + const VECTOR2I& aBaseSize, const EDA_ANGLE& aOrient, + bool aTextMirrored, int aWidth, bool aBaseBold, + bool aBaseItalic, KIFONT::FONT* aFont, + const KIFONT::METRICS& aFontMetrics, + enum GR_TEXT_V_ALIGN_T aV_justify, + TEXT_STYLE_FLAGS aTextStyle, + std::vector& aOverbars ) +{ + VECTOR2I nextPosition = aPosition; + + if( !aNode ) + return nextPosition; + + TEXT_STYLE_FLAGS currentStyle = aTextStyle; + VECTOR2I currentSize = aBaseSize; + bool drawOverbar = false; + + // Handle markup node types + if( !aNode->is_root() ) + { + if( aNode->isSubscript() ) + { + currentStyle |= TEXT_STYLE::SUBSCRIPT; + // Subscript: smaller size and lower position + currentSize = VECTOR2I( aBaseSize.x * 0.5, aBaseSize.y * 0.6 ); + } + else if( aNode->isSuperscript() ) + { + currentStyle |= TEXT_STYLE::SUPERSCRIPT; + // Superscript: smaller size and higher position + currentSize = VECTOR2I( aBaseSize.x * 0.5, aBaseSize.y * 0.6 ); + } + + if( aNode->isOverbar() ) + { + drawOverbar = true; + // Overbar doesn't change font size, just adds decoration + } + + // Render content of this node if it has text + if( aNode->has_content() ) + { + wxString nodeText = aNode->asWxString(); + + // Process text content (simplified version of the main text processing) + wxStringTokenizer str_tok( nodeText, " ", wxTOKEN_RET_DELIMS ); + + while( str_tok.HasMoreTokens() ) + { + wxString word = str_tok.GetNextToken(); + nextPosition = renderWord( word, nextPosition, currentSize, aOrient, aTextMirrored, + aWidth, aBaseBold || (currentStyle & TEXT_STYLE::BOLD), + aBaseItalic || (currentStyle & TEXT_STYLE::ITALIC), + aFont, aFontMetrics, aV_justify, currentStyle ); + } + } + } + + // Process child nodes recursively + for( const std::unique_ptr& child : aNode->children ) + { + VECTOR2I startPos = nextPosition; + + nextPosition = renderMarkupNode( child.get(), nextPosition, currentSize, aOrient, + aTextMirrored, aWidth, aBaseBold, aBaseItalic, + aFont, aFontMetrics, aV_justify, currentStyle, aOverbars ); + + // Store overbar info for later rendering + if( drawOverbar ) + { + VECTOR2I endPos = nextPosition; + aOverbars.push_back( { startPos, endPos, currentSize, aFont && aFont->IsOutline(), aV_justify } ); + } + } + + return nextPosition; +} + + +void PDF_PLOTTER::drawOverbars( const std::vector& aOverbars, + const EDA_ANGLE& aOrient, const KIFONT::METRICS& aFontMetrics ) +{ + for( const OverbarInfo& overbar : aOverbars ) + { + // Baseline direction (vector from start to end). If zero length, derive from orientation. + VECTOR2D dir( overbar.endPos.x - overbar.startPos.x, + overbar.endPos.y - overbar.startPos.y ); + + double len = hypot( dir.x, dir.y ); + if( len <= 1e-6 ) + { + // Fallback: derive direction from orientation angle + double ang = aOrient.AsRadians(); + dir.x = cos( ang ); + dir.y = sin( ang ); + len = 1.0; + } + + dir.x /= len; + dir.y /= len; + + // Perpendicular (rotate dir 90° CCW). Upward in text space so overbar sits above baseline. + VECTOR2D nrm( -dir.y, dir.x ); + + // Base vertical offset distance in device units (baseline -> default overbar position) + double barOffset = aFontMetrics.GetOverbarVerticalPosition( overbar.fontSize.y ); + + // Adjust further for outline fonts + if( overbar.isOutline ) + barOffset += overbar.fontSize.y * 0.25; // extra raise for outline font + + // Mirror the text vertical alignment adjustments used for baseline shifting. + // Earlier logic scales baseline adjustment: CENTER ~2x, TOP ~4x. We apply proportional + // extra raise so that overbars track visually with perceived baseline shift. + double alignMult = 1.0; + + switch( overbar.vAlign ) + { + case GR_TEXT_V_ALIGN_CENTER: alignMult = overbar.isOutline ? 2.0 : 1.0; break; + case GR_TEXT_V_ALIGN_TOP: alignMult = overbar.isOutline ? 4.0 : 1.0; break; + default: alignMult = 1.0; break; // bottom + } + + if( alignMult > 1.0 ) + { + // Scale only the baseline component (approx 17% of height, matching earlier baseline_factor) + double baseline_factor = 0.17; + barOffset += ( alignMult - 1.0 ) * ( baseline_factor * overbar.fontSize.y ); + } + + // Trim to avoid rounded cap extension (assumes stroke caps); proportion of font width. + double barTrim = overbar.fontSize.x * 0.1; + + // Apply trim along baseline direction and offset along normal + VECTOR2D startPt( overbar.startPos.x, overbar.startPos.y ); + VECTOR2D endPt( overbar.endPos.x, overbar.endPos.y ); + + // Both endpoints should share identical vertical (normal) offset above baseline. + // Use a single offset vector offVec = -barOffset * nrm (negative because nrm points 'up'). + VECTOR2D offVec( -barOffset * nrm.x, -barOffset * nrm.y ); + + startPt.x += dir.x * barTrim + offVec.x; + startPt.y += dir.y * barTrim + offVec.y; + endPt.x -= dir.x * barTrim - offVec.x; // subtract trim, then apply same vertical offset + endPt.y -= dir.y * barTrim - offVec.y; + + VECTOR2I iStart( KiROUND( startPt.x ), KiROUND( startPt.y ) ); + VECTOR2I iEnd( KiROUND( endPt.x ), KiROUND( endPt.y ) ); + + MoveTo( iStart ); + LineTo( iEnd ); + PenFinish(); + } } diff --git a/common/plotters/pdf_outline_font.cpp b/common/plotters/pdf_outline_font.cpp new file mode 100644 index 0000000000..58048ccb2f --- /dev/null +++ b/common/plotters/pdf_outline_font.cpp @@ -0,0 +1,589 @@ +/* + * 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, you may find one here: + * http://www.gnu.org/licenses/gpl-3.0.html + * or you may write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA + */ + +#include +#include +#include +#include + +#include +#include +#include +#include + +#include +#include +#include +#include + +#include + +#include +#include FT_FREETYPE_H + +#include + +namespace +{ +std::string formatUnicodeHex( uint32_t aCodepoint ) +{ + if( aCodepoint <= 0xFFFF ) + return fmt::format( "{:04X}", aCodepoint ); + + if( aCodepoint <= 0x10FFFF ) + { + uint32_t value = aCodepoint - 0x10000; + uint16_t high = 0xD800 + ( value >> 10 ); + uint16_t low = 0xDC00 + ( value & 0x3FF ); + return fmt::format( "{:04X}{:04X}", high, low ); + } + + return std::string( "003F" ); +} + +std::u32string utf8ToU32( const std::string& aUtf8 ) +{ + std::u32string result; + UTF8 utf8( aUtf8.c_str() ); + + for( auto it = utf8.ubegin(); it < utf8.uend(); ++it ) + result.push_back( static_cast( *it ) ); + + return result; +} + +std::string generateSubsetPrefix( unsigned aSubsetIndex ) +{ + static constexpr char letters[] = "ABCDEFGHIJKLMNOPQRSTUVWXYZ"; + std::string prefix( 6, 'A' ); + + for( int ii = 5; ii >= 0; --ii ) + { + prefix[ii] = letters[ aSubsetIndex % 26 ]; + aSubsetIndex /= 26; + } + + return prefix; +} + +double unitsToPdf( double aValue, double aUnitsPerEm ) +{ + if( aUnitsPerEm == 0.0 ) + return 0.0; + + return aValue * 1000.0 / aUnitsPerEm; +} +} + +bool PDF_OUTLINE_FONT_SUBSET::GLYPH_KEY::operator<( const GLYPH_KEY& aOther ) const +{ + if( m_glyphIndex != aOther.m_glyphIndex ) + return m_glyphIndex < aOther.m_glyphIndex; + + return m_unicode < aOther.m_unicode; +} + +PDF_OUTLINE_FONT_SUBSET::PDF_OUTLINE_FONT_SUBSET( KIFONT::OUTLINE_FONT* aFont, unsigned aSubsetIndex ) : + m_font( aFont ), + m_resourceName( makeResourceName( aSubsetIndex ) ), + m_baseFontName( makeSubsetName( aFont, aSubsetIndex ) ), + m_unitsPerEm( 1000.0 ), + m_ascent( 0.0 ), + m_descent( 0.0 ), + m_capHeight( 0.0 ), + m_italicAngle( 0.0 ), + m_stemV( 80.0 ), + m_bboxMinX( 0.0 ), + m_bboxMinY( 0.0 ), + m_bboxMaxX( 0.0 ), + m_bboxMaxY( 0.0 ), + m_flags( 32 ), + m_fontDataLoaded( false ), + m_nextCID( 1 ), + m_fontFileHandle( -1 ), + m_fontDescriptorHandle( -1 ), + m_cidFontHandle( -1 ), + m_cidMapHandle( -1 ), + m_toUnicodeHandle( -1 ), + m_fontHandle( -1 ) +{ + FT_Face face = aFont ? aFont->GetFace() : nullptr; + + if( face ) + { + if( face->units_per_EM > 0 ) + m_unitsPerEm = static_cast( face->units_per_EM ); + + m_ascent = unitsToPdf( static_cast( face->ascender ), m_unitsPerEm ); + m_descent = unitsToPdf( static_cast( face->descender ), m_unitsPerEm ); + m_capHeight = unitsToPdf( static_cast( face->bbox.yMax ), m_unitsPerEm ); + m_bboxMinX = unitsToPdf( static_cast( face->bbox.xMin ), m_unitsPerEm ); + m_bboxMinY = unitsToPdf( static_cast( face->bbox.yMin ), m_unitsPerEm ); + m_bboxMaxX = unitsToPdf( static_cast( face->bbox.xMax ), m_unitsPerEm ); + m_bboxMaxY = unitsToPdf( static_cast( face->bbox.yMax ), m_unitsPerEm ); + + if( face->style_flags & FT_STYLE_FLAG_ITALIC ) + m_italicAngle = -12.0; + else if( aFont->IsItalic() ) + m_italicAngle = -12.0; + + if( aFont->IsBold() ) + m_stemV = 140.0; + + if( face->face_flags & FT_FACE_FLAG_FIXED_WIDTH ) + m_flags |= 1; + + if( aFont->IsItalic() ) + m_flags |= 64; + } + + m_widths.resize( 1, 0.0 ); + m_cidToGid.resize( 1, 0 ); + m_cidToUnicode.resize( 1 ); +} + +bool PDF_OUTLINE_FONT_SUBSET::HasGlyphs() const +{ + return m_nextCID > 1; +} + +void PDF_OUTLINE_FONT_SUBSET::ensureNotdef() +{ + if( m_widths.empty() ) + { + m_widths.resize( 1, 0.0 ); + m_cidToGid.resize( 1, 0 ); + m_cidToUnicode.resize( 1 ); + } +} + +uint16_t PDF_OUTLINE_FONT_SUBSET::EnsureGlyph( uint32_t aGlyphIndex, const std::u32string& aUnicode ) +{ + if( aGlyphIndex == 0 ) + return 0; + + GLYPH_KEY key{ aGlyphIndex, aUnicode }; + + auto it = m_glyphMap.find( key ); + + if( it != m_glyphMap.end() ) + return it->second; + + ensureNotdef(); + + FT_Face face = m_font ? m_font->GetFace() : nullptr; + + if( !face ) + return 0; + + if( FT_Load_Glyph( face, aGlyphIndex, FT_LOAD_NO_SCALE | FT_LOAD_NO_HINTING ) ) + return 0; + + // For FT_LOAD_NO_SCALE the advance.x should be in font units (26.6) unless metrics differ. + // We divide by 64.0 to get raw font units; convert to our internal PDF user units via unitsToPdf. + double rawAdvance266 = static_cast( face->glyph->advance.x ); + double rawAdvanceFontUnits = rawAdvance266 / 64.0; + double advance = unitsToPdf( rawAdvanceFontUnits, m_unitsPerEm ); + + uint16_t cid = m_nextCID++; + + if( m_widths.size() <= cid ) + m_widths.resize( cid + 1, 0.0 ); + + if( m_cidToGid.size() <= cid ) + m_cidToGid.resize( cid + 1, 0 ); + + if( m_cidToUnicode.size() <= cid ) + m_cidToUnicode.resize( cid + 1 ); + + m_widths[cid] = advance; + + if( std::getenv( "KICAD_DEBUG_FONT_ADV" ) ) + { + wxLogTrace( tracePdfPlotter, + "EnsureGlyph font='%s' gid=%u cid=%u rawAdvance26.6=%f rawAdvanceUnits=%f storedAdvancePdfUnits=%f unitsPerEm=%f", \ + m_font ? m_font->GetName().ToUTF8().data() : "(null)", (unsigned) aGlyphIndex, (unsigned) cid, \ + rawAdvance266, rawAdvanceFontUnits, advance, m_unitsPerEm ); + } + m_cidToGid[cid] = static_cast( aGlyphIndex ); + m_cidToUnicode[cid] = aUnicode; + + m_glyphMap.emplace( key, cid ); + + return cid; +} + +const std::vector& PDF_OUTLINE_FONT_SUBSET::FontFileData() +{ + if( !m_fontDataLoaded ) + { + m_fontDataLoaded = true; + + if( !m_font ) + return m_fontData; + + wxString fontFile = m_font->GetFileName(); + + if( fontFile.IsEmpty() ) + return m_fontData; + + wxFFile file( fontFile, wxT( "rb" ) ); + + if( !file.IsOpened() ) + return m_fontData; + + wxFileOffset length = file.Length(); + + if( length > 0 ) + { + m_fontData.resize( static_cast( length ) ); + file.Read( m_fontData.data(), length ); + } + } + + return m_fontData; +} + +std::string PDF_OUTLINE_FONT_SUBSET::BuildWidthsArray() const +{ + // Return empty if there are no glyphs beyond .notdef + if( m_nextCID <= 1 ) + return std::string( "[]" ); + + // PDF expects widths in 1000/em units for CIDFontType2 /W array entries. + // m_widths currently stores advance in PDF user units produced by unitsToPdf(). + // This is a bit of a fudge factor to reconstruct the output width + double designScale = 0.0072 * 2.25; + int logCount = 0; + + fmt::memory_buffer buffer; + fmt::format_to( std::back_inserter( buffer ), "[ 1 [" ); + + for( uint16_t cid = 1; cid < m_nextCID; ++cid ) + { + double adv = m_widths[cid]; + long width1000 = 0; + + if( designScale != 0.0 ) + width1000 = lrint( adv / designScale ); + + fmt::format_to( std::back_inserter( buffer ), " {}", width1000 ); + + if( std::getenv( "KICAD_DEBUG_FONT_ADV" ) && logCount < 16 ) + { + wxLogTrace( tracePdfPlotter, "BuildWidthsArray FIXED cid=%u advPdfUnits=%f width1000=%ld", (unsigned) cid, adv, width1000 ); + ++logCount; + } + } + + fmt::format_to( std::back_inserter( buffer ), " ] ]" ); + return std::string( buffer.data(), buffer.size() ); +} + +std::string PDF_OUTLINE_FONT_SUBSET::BuildToUnicodeCMap() const +{ + if( m_nextCID <= 1 ) + return std::string(); + + fmt::memory_buffer buffer; + + std::string cmapName = m_baseFontName + "_ToUnicode"; + + fmt::format_to( std::back_inserter( buffer ), "/CIDInit /ProcSet findresource begin\n" ); + fmt::format_to( std::back_inserter( buffer ), "12 dict begin\n" ); + fmt::format_to( std::back_inserter( buffer ), "begincmap\n" ); + fmt::format_to( std::back_inserter( buffer ), "/CIDSystemInfo << /Registry (Adobe) /Ordering (Identity) /Supplement 0 >> def\n" ); + fmt::format_to( std::back_inserter( buffer ), "/CMapName /{} def\n", cmapName ); + fmt::format_to( std::back_inserter( buffer ), "/CMapType 2 def\n" ); + fmt::format_to( std::back_inserter( buffer ), "1 begincodespacerange\n" ); + fmt::format_to( std::back_inserter( buffer ), "<0000> \n" ); + fmt::format_to( std::back_inserter( buffer ), "endcodespacerange\n" ); + + size_t mappingCount = 0; + + for( uint16_t cid = 1; cid < m_nextCID; ++cid ) + { + if( !m_cidToUnicode[cid].empty() ) + ++mappingCount; + } + + fmt::format_to( std::back_inserter( buffer ), "{} beginbfchar\n", mappingCount ); + + for( uint16_t cid = 1; cid < m_nextCID; ++cid ) + { + const std::u32string& unicode = m_cidToUnicode[cid]; + + if( unicode.empty() ) + continue; + + fmt::format_to( std::back_inserter( buffer ), "<{:04X}> <", cid ); + + for( uint32_t codepoint : unicode ) + fmt::format_to( std::back_inserter( buffer ), "{}", formatUnicodeHex( codepoint ) ); + + fmt::format_to( std::back_inserter( buffer ), ">\n" ); + } + + fmt::format_to( std::back_inserter( buffer ), "endbfchar\n" ); + fmt::format_to( std::back_inserter( buffer ), "endcmap\n" ); + fmt::format_to( std::back_inserter( buffer ), "CMapName currentdict /CMap defineresource pop\n" ); + fmt::format_to( std::back_inserter( buffer ), "end\n" ); + fmt::format_to( std::back_inserter( buffer ), "end\n" ); + + return std::string( buffer.data(), buffer.size() ); +} + +std::string PDF_OUTLINE_FONT_SUBSET::BuildCIDToGIDStream() const +{ + std::string data; + + if( m_nextCID == 0 ) + return data; + + data.resize( static_cast( m_nextCID ) * 2, 0 ); + + for( uint16_t cid = 0; cid < m_nextCID; ++cid ) + { + uint16_t gid = cid < m_cidToGid.size() ? m_cidToGid[cid] : 0; + data[ cid * 2 ] = static_cast( ( gid >> 8 ) & 0xFF ); + data[ cid * 2 + 1 ] = static_cast( gid & 0xFF ); + } + + return data; +} + +std::string PDF_OUTLINE_FONT_SUBSET::makeResourceName( unsigned aSubsetIndex ) +{ + return fmt::format( "/KiCadOutline{}", aSubsetIndex ); +} + +std::string PDF_OUTLINE_FONT_SUBSET::sanitizeFontName( const wxString& aName ) +{ + std::string utf8 = UTF8( aName ).substr(); + std::string sanitized; + sanitized.reserve( utf8.size() ); + + for( unsigned char ch : utf8 ) + { + if( std::isalnum( ch ) ) + sanitized.push_back( static_cast( ch ) ); + else if( ch == '-' ) + sanitized.push_back( '-' ); + else + sanitized.push_back( '-' ); + } + + if( sanitized.empty() ) + sanitized = "Font"; + + return sanitized; +} + +std::string PDF_OUTLINE_FONT_SUBSET::makeSubsetName( KIFONT::OUTLINE_FONT* aFont, unsigned aSubsetIndex ) +{ + std::string prefix = generateSubsetPrefix( aSubsetIndex ); + std::string name = aFont ? sanitizeFontName( aFont->GetName() ) : std::string( "Font" ); + return fmt::format( "{}+{}", prefix, name ); +} + +PDF_OUTLINE_FONT_MANAGER::PDF_OUTLINE_FONT_MANAGER() : + m_nextSubsetIndex( 0 ) +{ +} + +void PDF_OUTLINE_FONT_MANAGER::Reset() +{ + m_subsets.clear(); + m_nextSubsetIndex = 0; +} + +PDF_OUTLINE_FONT_SUBSET* PDF_OUTLINE_FONT_MANAGER::ensureSubset( KIFONT::OUTLINE_FONT* aFont, bool aItalic, bool aBold ) +{ + if( !aFont ) + return nullptr; + SUBSET_KEY key{ aFont, aItalic, aBold }; + auto it = m_subsets.find( key ); + if( it != m_subsets.end() ) + return it->second.get(); + auto subset = std::make_unique( aFont, m_nextSubsetIndex++ ); + PDF_OUTLINE_FONT_SUBSET* subsetPtr = subset.get(); + + // Synthetic style application: if requested styles not actually present in font face flags. + // Distinguish real face style from fake style flags so that a fake italic does not block + // PDF shear application. We consider the face to have a real italic only if FT_STYLE_FLAG_ITALIC + // is set. (m_fakeItal only indicates substitution missing an italic variant.) + bool faceHasRealItalic = false; + bool faceHasRealBold = false; + if( const FT_Face& face = aFont->GetFace() ) + { + faceHasRealItalic = ( face->style_flags & FT_STYLE_FLAG_ITALIC ) != 0; + faceHasRealBold = ( face->style_flags & FT_STYLE_FLAG_BOLD ) != 0; + } + + bool needSyntheticItalic = aItalic && !faceHasRealItalic; // ignore fake italic + bool needSyntheticBold = aBold && !faceHasRealBold; // ignore fake bold + + if( std::getenv( "KICAD_DEBUG_SYN_STYLE" ) ) + { + const FT_Face& face = aFont->GetFace(); + int styleFlags = face ? face->style_flags : 0; + const char* fname = aFont->GetName().ToUTF8().data(); + const char* styleName = ( face && face->style_name ) ? face->style_name : "(null)"; + bool fakeItal = static_cast( aFont )->IsFakeItalic(); + bool fakeBold = static_cast( aFont )->IsFakeBold(); + wxLogTrace( tracePdfPlotter, + "ensureSubset font='%s' styleName='%s' reqItalic=%d reqBold=%d FT_style_flags=%d " + "faceHasRealItalic=%d faceHasRealBold=%d fakeItal=%d fakeBold=%d syntheticItalic=%d " + "syntheticBold=%d subsetKey[i=%d b=%d] subsetIdx=%u", + fname ? fname : "(null)", styleName, (int) aItalic, (int) aBold, styleFlags, + (int) faceHasRealItalic, (int) faceHasRealBold, (int) fakeItal, (int) fakeBold, + (int) needSyntheticItalic, (int) needSyntheticBold, (int) aItalic, (int) aBold, + subsetPtr->Font()->GetFace() ? subsetPtr->Font()->GetFace()->face_index : 0 ); + } + + if( needSyntheticItalic || needSyntheticBold ) + { + // Approx italic angle based on shear ITALIC_TILT (radians) => degrees + double angleDeg = -std::atan( ITALIC_TILT ) * 180.0 / M_PI; // negative for conventional PDF italicAngle + subsetPtr->ForceSyntheticStyle( needSyntheticBold, needSyntheticItalic, angleDeg ); + if( std::getenv( "KICAD_DEBUG_SYN_STYLE" ) ) + { + wxLogTrace( tracePdfPlotter, "ForceSyntheticStyle applied: bold=%d italic=%d angle=%f", (int) needSyntheticBold, (int) needSyntheticItalic, angleDeg ); + } + } + + m_subsets.emplace( key, std::move( subset ) ); + return subsetPtr; +} + +void PDF_OUTLINE_FONT_MANAGER::EncodeString( const wxString& aText, KIFONT::OUTLINE_FONT* aFont, + bool aItalicRequested, bool aBoldRequested, + std::vector* aRuns ) +{ + if( !aRuns || !aFont ) + return; + + auto permission = aFont->GetEmbeddingPermission(); + + if( permission != KIFONT::OUTLINE_FONT::EMBEDDING_PERMISSION::INSTALLABLE + && permission != KIFONT::OUTLINE_FONT::EMBEDDING_PERMISSION::EDITABLE ) + { + return; + } + + // If italic requested and font has a dedicated italic variant discoverable via style linkage, + // the caller should already have selected that font (aFont). We still separate subsets by + // italic flag so synthetic slant and regular do not share widths. + PDF_OUTLINE_FONT_SUBSET* subset = ensureSubset( aFont, aItalicRequested, aBoldRequested ); + + if( !subset ) + return; + + UTF8 utf8Text( aText ); + std::string textUtf8 = utf8Text.substr(); + + hb_buffer_t* buffer = hb_buffer_create(); + hb_buffer_set_cluster_level( buffer, HB_BUFFER_CLUSTER_LEVEL_MONOTONE_GRAPHEMES ); + hb_buffer_add_utf8( buffer, textUtf8.c_str(), static_cast( textUtf8.size() ), 0, + static_cast( textUtf8.size() ) ); + hb_buffer_guess_segment_properties( buffer ); + + hb_font_t* hbFont = hb_ft_font_create_referenced( aFont->GetFace() ); + hb_ft_font_set_funcs( hbFont ); + hb_shape( hbFont, buffer, nullptr, 0 ); + + unsigned int glyphCount = 0; + hb_glyph_info_t* glyphInfo = hb_buffer_get_glyph_infos( buffer, &glyphCount ); + hb_glyph_position_t* glyphPos = hb_buffer_get_glyph_positions( buffer, &glyphCount ); + + hb_font_destroy( hbFont ); + + if( glyphCount == 0 ) + { + hb_buffer_destroy( buffer ); + return; + } + + PDF_OUTLINE_FONT_RUN run; + run.m_subset = subset; + + bool hasVisibleGlyph = false; + + for( unsigned int ii = 0; ii < glyphCount; ++ii ) + { + uint32_t glyphIndex = glyphInfo[ii].codepoint; + + size_t clusterStart = glyphInfo[ii].cluster; + size_t clusterEnd = ( ii + 1 < glyphCount ) ? glyphInfo[ii + 1].cluster : textUtf8.size(); + + if( clusterEnd < clusterStart ) + std::swap( clusterStart, clusterEnd ); + + std::string clusterUtf8 = textUtf8.substr( clusterStart, clusterEnd - clusterStart ); + std::u32string unicode = utf8ToU32( clusterUtf8 ); + + if( unicode.empty() ) + unicode.push_back( 0 ); + + uint16_t cid = subset->EnsureGlyph( glyphIndex, unicode ); + + if( cid != 0 ) + hasVisibleGlyph = true; + + run.m_bytes.push_back( static_cast( ( cid >> 8 ) & 0xFF ) ); + run.m_bytes.push_back( static_cast( cid & 0xFF ) ); + + // Capture HarfBuzz positioning information and convert to PDF units + PDF_OUTLINE_FONT_GLYPH glyph; + glyph.cid = cid; + // Convert from 26.6 fixed point to font units, then to PDF units + double xAdvanceFontUnits = glyphPos[ii].x_advance / 64.0; + double yAdvanceFontUnits = glyphPos[ii].y_advance / 64.0; + double xOffsetFontUnits = glyphPos[ii].x_offset / 64.0; + double yOffsetFontUnits = glyphPos[ii].y_offset / 64.0; + + glyph.xAdvance = unitsToPdf( xAdvanceFontUnits, subset->UnitsPerEm() ); + glyph.yAdvance = unitsToPdf( yAdvanceFontUnits, subset->UnitsPerEm() ); + glyph.xOffset = unitsToPdf( xOffsetFontUnits, subset->UnitsPerEm() ); + glyph.yOffset = unitsToPdf( yOffsetFontUnits, subset->UnitsPerEm() ); + run.m_glyphs.push_back( glyph ); + } + + hb_buffer_destroy( buffer ); + + if( hasVisibleGlyph && !run.m_bytes.empty() ) + aRuns->push_back( std::move( run ) ); +} + +std::vector PDF_OUTLINE_FONT_MANAGER::AllSubsets() const +{ + std::vector result; + result.reserve( m_subsets.size() ); + + for( const auto& [ font, subset ] : m_subsets ) + { + if( subset ) + result.push_back( subset.get() ); + } + + return result; +} diff --git a/common/plotters/pdf_stroke_font.cpp b/common/plotters/pdf_stroke_font.cpp new file mode 100644 index 0000000000..ffcfc326b7 --- /dev/null +++ b/common/plotters/pdf_stroke_font.cpp @@ -0,0 +1,502 @@ +/* + * 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 2 + * 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, you may find one here: + * http://www.gnu.org/licenses/old-licenses/gpl-2.0.html + * or you may search the http://www.gnu.org website for the version 2 license, + * or you may write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA + */ + +#include + +#include +#include +#include + +#include + +#include +#include + +namespace +{ +static constexpr int MAX_SIMPLE_FONT_CODES = 256; + +// Build the stroked path for a glyph. +// KiCad's internal stroke font glyph coordinates use an inverted Y axis relative to the +// PDF coordinate system we are targeting here. We therefore optionally flip Y so text +// renders upright. A slightly thicker default stroke width (4% of EM) is used to improve +// legibility at typical plot zoom levels. +static std::string buildGlyphStream( const KIFONT::STROKE_GLYPH* aGlyph, double aUnitsPerEm, + bool aInvertY, bool aBold ) +{ + if( !aGlyph ) + return std::string(); + + fmt::memory_buffer buffer; + double factor = ADVANCED_CFG::GetCfg().m_PDFStrokeFontWidthFactor; + + if( aBold ) + { + double boldMul = ADVANCED_CFG::GetCfg().m_PDFStrokeFontBoldMultiplier; + if( boldMul < 1.0 ) boldMul = 1.0; + factor *= boldMul; + } + + if( factor <= 0.0 ) + factor = 0.04; // fallback safety + + double lw = aUnitsPerEm * factor; + fmt::format_to( std::back_inserter( buffer ), "{:.3f} w 1 J 1 j ", lw ); + auto& cfg = ADVANCED_CFG::GetCfg(); + + for( const std::vector& stroke : *aGlyph ) + { + bool firstPoint = true; + + for( const VECTOR2D& point : stroke ) + { + double x = ( point.x + cfg.m_PDFStrokeFontXOffset ) * aUnitsPerEm; + double y = point.y * aUnitsPerEm; + + if( aInvertY ) + { + y = -y; // Mirror vertically about baseline (y=0) + y += cfg.m_PDFStrokeFontYOffset * aUnitsPerEm; + } + + if( firstPoint ) + { + fmt::format_to( std::back_inserter( buffer ), "{:.3f} {:.3f} m ", x, y ); + firstPoint = false; + } + else + { + fmt::format_to( std::back_inserter( buffer ), "{:.3f} {:.3f} l ", x, y ); + } + } + + if( !stroke.empty() ) + fmt::format_to( std::back_inserter( buffer ), "S " ); + } + + return std::string( buffer.data(), buffer.size() ); +} + +static std::string formatUnicodeHex( uint32_t aCodepoint ) +{ + if( aCodepoint <= 0xFFFF ) + return fmt::format( "{:04X}", aCodepoint ); + + if( aCodepoint <= 0x10FFFF ) + { + uint32_t value = aCodepoint - 0x10000; + uint16_t high = 0xD800 + ( value >> 10 ); + uint16_t low = 0xDC00 + ( value & 0x3FF ); + return fmt::format( "{:04X}{:04X}", high, low ); + } + + return std::string( "003F" ); +} +} // anonymous namespace + + +PDF_STROKE_FONT_SUBSET::PDF_STROKE_FONT_SUBSET( const KIFONT::STROKE_FONT* aFont, double aUnitsPerEm, + unsigned aSubsetIndex, bool aBold, bool aItalic ) : + m_font( aFont ), + m_unitsPerEm( aUnitsPerEm ), + m_resourceName( fmt::format( "/KiCadStroke{}", aSubsetIndex ) ), + m_cmapName( fmt::format( "KiCadStrokeCMap{}", aSubsetIndex ) ), + m_widths( MAX_SIMPLE_FONT_CODES, 0.0 ), + m_nextCode( 1 ), + m_lastCode( 0 ), + m_bboxMinX( std::numeric_limits::max() ), + m_bboxMinY( std::numeric_limits::max() ), + m_bboxMaxX( std::numeric_limits::lowest() ), + m_bboxMaxY( std::numeric_limits::lowest() ), + m_charProcsHandle( -1 ), + m_fontHandle( -1 ), + m_toUnicodeHandle( -1 ), + m_isBold( aBold ), + m_isItalic( aItalic ) +{ + GLYPH notdef; + notdef.m_unicode = 0; + notdef.m_code = 0; + notdef.m_glyphIndex = -1; + notdef.m_name = ".notdef"; + notdef.m_stream.clear(); + notdef.m_width = 0.0; + notdef.m_minX = 0.0; + notdef.m_minY = 0.0; + notdef.m_maxX = 0.0; + notdef.m_maxY = 0.0; + notdef.m_charProcHandle = -1; + + m_glyphs.push_back( notdef ); + m_bboxMinX = 0.0; + m_bboxMinY = 0.0; + m_bboxMaxX = 0.0; + m_bboxMaxY = 0.0; +} + + +bool PDF_STROKE_FONT_SUBSET::Contains( wxUniChar aCode ) const +{ + return m_unicodeToCode.find( aCode ) != m_unicodeToCode.end(); +} + + +int PDF_STROKE_FONT_SUBSET::EnsureGlyph( wxUniChar aCode ) +{ + auto it = m_unicodeToCode.find( aCode ); + + if( it != m_unicodeToCode.end() ) + return it->second; + + if( IsFull() ) + return -1; + + int glyphIndex = glyphIndexForUnicode( aCode ); + const KIFONT::STROKE_GLYPH* glyph = m_font ? m_font->GetGlyph( glyphIndex ) : nullptr; + + int code = m_nextCode++; + m_lastCode = std::max( m_lastCode, code ); + + GLYPH data; + data.m_unicode = aCode; + data.m_code = code; + data.m_glyphIndex = glyphIndex; + data.m_name = makeGlyphName( code ); + data.m_charProcHandle = -1; + + const BOX2D& bbox = m_font->GetGlyphBoundingBox( glyphIndex ); + VECTOR2D origin = bbox.GetOrigin(); + VECTOR2D size = bbox.GetSize(); + + data.m_width = size.x * m_unitsPerEm; + data.m_minX = origin.x * m_unitsPerEm; + data.m_minY = origin.y * m_unitsPerEm; + data.m_maxX = ( origin.x + size.x ) * m_unitsPerEm; + data.m_maxY = ( origin.y + size.y ) * m_unitsPerEm; + + // Invert Y so glyphs render upright in PDF coordinate space. + bool invertY = true; + + if( invertY ) + { + // Mirror bounding box vertically about baseline. + double newMinY = -data.m_maxY; + double newMaxY = -data.m_minY; + data.m_minY = newMinY; + data.m_maxY = newMaxY; + + // Apply Y offset to bounding box to match the offset applied to stroke coordinates + double yOffset = ADVANCED_CFG::GetCfg().m_PDFStrokeFontYOffset * m_unitsPerEm; + data.m_minY += yOffset; + data.m_maxY += yOffset; + } + + // Build charproc stream: first specify width and bbox (d1 operator) then stroke path. + double kerningFactor = ADVANCED_CFG::GetCfg().m_PDFStrokeFontKerningFactor; + + if( kerningFactor <= 0.0 ) + kerningFactor = 1.0; + + std::string strokes = buildGlyphStream( glyph, m_unitsPerEm, invertY, m_isBold ); + data.m_width = size.x * m_unitsPerEm * kerningFactor; + data.m_stream = fmt::format( "{:.3f} 0 {:.3f} {:.3f} {:.3f} {:.3f} d1 {}", + data.m_width, + data.m_minX, data.m_minY, data.m_maxX, data.m_maxY, + strokes ); + + m_widths[code] = data.m_width; + + m_bboxMinX = std::min( m_bboxMinX, data.m_minX ); + m_bboxMinY = std::min( m_bboxMinY, data.m_minY ); + m_bboxMaxX = std::max( m_bboxMaxX, data.m_maxX ); + m_bboxMaxY = std::max( m_bboxMaxY, data.m_maxY ); + + m_glyphs.push_back( data ); + m_unicodeToCode.emplace( aCode, code ); + + return code; +} + + +int PDF_STROKE_FONT_SUBSET::CodeForGlyph( wxUniChar aCode ) const +{ + auto it = m_unicodeToCode.find( aCode ); + + if( it != m_unicodeToCode.end() ) + return it->second; + + return -1; +} + + +bool PDF_STROKE_FONT_SUBSET::IsFull() const +{ + return m_nextCode >= MAX_SIMPLE_FONT_CODES; +} + + +int PDF_STROKE_FONT_SUBSET::GlyphCount() const +{ + return static_cast( m_glyphs.size() ); +} + + +int PDF_STROKE_FONT_SUBSET::FirstChar() const +{ + return 0; +} + + +int PDF_STROKE_FONT_SUBSET::LastChar() const +{ + return std::max( 0, m_lastCode ); +} + + +std::string PDF_STROKE_FONT_SUBSET::BuildDifferencesArray() const +{ + int first = FirstChar(); + int last = LastChar(); + + fmt::memory_buffer buffer; + fmt::format_to( std::back_inserter( buffer ), "[ {} ", first ); + + for( int code = first; code <= last; ++code ) + { + const GLYPH* glyph = glyphForCode( code ); + + if( glyph ) + fmt::format_to( std::back_inserter( buffer ), "/{} ", glyph->m_name ); + else + fmt::format_to( std::back_inserter( buffer ), "/.notdef " ); + } + + fmt::format_to( std::back_inserter( buffer ), "]" ); + return std::string( buffer.data(), buffer.size() ); +} + + +std::string PDF_STROKE_FONT_SUBSET::BuildWidthsArray() const +{ + int first = FirstChar(); + int last = LastChar(); + + fmt::memory_buffer buffer; + fmt::format_to( std::back_inserter( buffer ), "[" ); + + for( int code = first; code <= last; ++code ) + fmt::format_to( std::back_inserter( buffer ), " {:g}", m_widths[code] ); + + fmt::format_to( std::back_inserter( buffer ), " ]" ); + return std::string( buffer.data(), buffer.size() ); +} + + +std::string PDF_STROKE_FONT_SUBSET::BuildToUnicodeCMap() const +{ + size_t mappingCount = 0; + + for( const GLYPH& glyph : m_glyphs ) + { + if( glyph.m_code == 0 ) + continue; + + ++mappingCount; + } + + fmt::memory_buffer buffer; + + fmt::format_to( std::back_inserter( buffer ), "/CIDInit /ProcSet findresource begin\n" ); + fmt::format_to( std::back_inserter( buffer ), "12 dict begin\n" ); + fmt::format_to( std::back_inserter( buffer ), "begincmap\n" ); + fmt::format_to( std::back_inserter( buffer ), "/CIDSystemInfo << /Registry (KiCad) /Ordering (StrokeFont) /Supplement 0 >> def\n" ); + fmt::format_to( std::back_inserter( buffer ), "/CMapName /{} def\n", m_cmapName ); + fmt::format_to( std::back_inserter( buffer ), "/CMapType 2 def\n" ); + fmt::format_to( std::back_inserter( buffer ), "1 begincodespacerange\n" ); + fmt::format_to( std::back_inserter( buffer ), "<00> \n" ); + fmt::format_to( std::back_inserter( buffer ), "endcodespacerange\n" ); + + fmt::format_to( std::back_inserter( buffer ), "{} beginbfchar\n", mappingCount ); + + for( const GLYPH& glyph : m_glyphs ) + { + if( glyph.m_code == 0 ) + continue; + + fmt::format_to( std::back_inserter( buffer ), "<{:02X}> <{}>\n", glyph.m_code, + formatUnicodeHex( static_cast( glyph.m_unicode ) ) ); + } + + fmt::format_to( std::back_inserter( buffer ), "endbfchar\n" ); + fmt::format_to( std::back_inserter( buffer ), "endcmap\n" ); + fmt::format_to( std::back_inserter( buffer ), "CMapName currentdict /CMap defineresource pop\n" ); + fmt::format_to( std::back_inserter( buffer ), "end\n" ); + fmt::format_to( std::back_inserter( buffer ), "end\n" ); + + return std::string( buffer.data(), buffer.size() ); +} + + +int PDF_STROKE_FONT_SUBSET::glyphIndexForUnicode( wxUniChar aCode ) const +{ + int value = static_cast( aCode ); + + if( value < ' ' ) + return static_cast( '?' ) - ' '; + + int index = value - ' '; + int count = static_cast( m_font ? m_font->GetGlyphCount() : 0 ); + + if( index < 0 || index >= count ) + return static_cast( '?' ) - ' '; + + return index; +} + + +std::string PDF_STROKE_FONT_SUBSET::makeGlyphName( int aCode ) const +{ + return fmt::format( "g{:02X}", aCode ); +} + + +const PDF_STROKE_FONT_SUBSET::GLYPH* PDF_STROKE_FONT_SUBSET::glyphForCode( int aCode ) const +{ + for( const GLYPH& glyph : m_glyphs ) + { + if( glyph.m_code == aCode ) + return &glyph; + } + + return nullptr; +} + + +PDF_STROKE_FONT_MANAGER::PDF_STROKE_FONT_MANAGER() : + m_font( KIFONT::STROKE_FONT::LoadFont( wxEmptyString ) ), + m_unitsPerEm( 1000.0 ), + m_nextSubsetIndex( 0 ) +{ + Reset(); +} + + +void PDF_STROKE_FONT_MANAGER::Reset() +{ + m_styleGroups.clear(); + m_nextSubsetIndex = 0; + + if( !m_font ) + m_font.reset( KIFONT::STROKE_FONT::LoadFont( wxEmptyString ) ); +} + + +PDF_STROKE_FONT_MANAGER::STYLE_GROUP& PDF_STROKE_FONT_MANAGER::groupFor( bool aBold, bool aItalic ) +{ + unsigned key = styleKey( aBold, aItalic ); + return m_styleGroups[key]; +} + +void PDF_STROKE_FONT_MANAGER::EncodeString( const wxString& aText, + std::vector* aRuns, + bool aBold, bool aItalic ) +{ + if( !aRuns ) + return; + + aRuns->clear(); + + if( aText.empty() ) + return; + + PDF_STROKE_FONT_SUBSET* currentSubset = nullptr; + std::string currentBytes; + + for( wxUniChar ch : aText ) + { + PDF_STROKE_FONT_SUBSET* subset = ensureSubsetForGlyph( ch, aBold, aItalic ); + + if( !subset ) + continue; + + int code = subset->EnsureGlyph( ch ); + + if( code < 0 ) + continue; + + if( subset != currentSubset ) + { + if( !currentBytes.empty() && currentSubset ) + aRuns->push_back( { currentSubset, currentBytes, aBold, aItalic } ); + + currentSubset = subset; + currentBytes.clear(); + } + + currentBytes.push_back( static_cast( code ) ); + } + + if( !currentBytes.empty() && currentSubset ) + aRuns->push_back( { currentSubset, currentBytes, aBold, aItalic } ); +} + +PDF_STROKE_FONT_SUBSET* PDF_STROKE_FONT_MANAGER::ensureSubsetForGlyph( wxUniChar aCode, bool aBold, bool aItalic ) +{ + STYLE_GROUP& group = groupFor( aBold, aItalic ); + + for( const std::unique_ptr& subset : group.subsets ) + { + if( subset->Contains( aCode ) ) + return subset.get(); + } + + for( const std::unique_ptr& subset : group.subsets ) + { + if( subset->IsFull() ) + continue; + + if( subset->EnsureGlyph( aCode ) >= 0 ) + return subset.get(); + } + + unsigned subsetIndex = m_nextSubsetIndex++; + auto newSubset = std::make_unique( m_font.get(), m_unitsPerEm, subsetIndex, aBold, aItalic ); + PDF_STROKE_FONT_SUBSET* subsetPtr = newSubset.get(); + subsetPtr->EnsureGlyph( aCode ); + group.subsets.emplace_back( std::move( newSubset ) ); + return subsetPtr; +} + +std::vector PDF_STROKE_FONT_MANAGER::AllSubsets() const +{ + std::vector out; + + for( const auto& [key, group] : m_styleGroups ) + { + (void) key; + for( const auto& up : group.subsets ) + out.push_back( up.get() ); + } + return out; +} + diff --git a/common/trace_helpers.cpp b/common/trace_helpers.cpp index e982e2b820..ca960514ab 100644 --- a/common/trace_helpers.cpp +++ b/common/trace_helpers.cpp @@ -60,6 +60,7 @@ const wxChar* const traceGit = wxT( "KICAD_GIT" ); const wxChar* const traceEagleIo = wxT( "KICAD_EAGLE_IO" ); const wxChar* const traceDesignBlocks = wxT( "KICAD_DESIGN_BLOCK" ); const wxChar* const traceLibFieldTable = wxT( "KICAD_LIB_FIELD_TABLE" ); +const wxChar* const tracePdfPlotter = wxT( "KICAD_PDF_PLOTTER" ); wxString dump( const wxArrayString& aArray ) diff --git a/include/advanced_config.h b/include/advanced_config.h index 4d2255d7ed..30c641b7e4 100644 --- a/include/advanced_config.h +++ b/include/advanced_config.h @@ -322,6 +322,54 @@ public: */ bool m_DebugPDFWriter; + /** + * Stroke font line width factor relative to EM size for PDF stroke fonts. + * + * Setting name: "PDFStrokeFontWidthFactor" + * Valid values: 0.0 to 1.0 (practical range 0.005 - 0.1) + * Default value: 0.04 + */ + double m_PDFStrokeFontWidthFactor; + + /** + * Horizontal offset factor applied to stroke font glyph coordinates (in EM units) after + * to compensate misalignment. Positive values move glyphs right. + * + * Setting name: "PDFStrokeFontXOffset" + * Valid values: -1.0 to 1.0 + * Default value: 0.0 + */ + double m_PDFStrokeFontXOffset; + + /** + * Vertical offset factor applied to stroke font glyph coordinates (in EM units) after + * Y inversion to compensate baseline misalignment. Positive values move glyphs up. + * + * Setting name: "PDFStrokeFontYOffset" + * Valid values: -1.0 to 1.0 + * Default value: 0.0 + */ + double m_PDFStrokeFontYOffset; + + /** + * Multiplier applied to stroke width factor when rendering bold stroke font subsets. + * + * Setting name: "PDFStrokeFontBoldMultiplier" + * Valid values: 1.0 to 5.0 + * Default value: 1.6 + */ + double m_PDFStrokeFontBoldMultiplier; + + /** + * Kerning (spacing) factor applied to glyph advance (width). Values < 1 tighten spacing. + * Applied uniformly across stroke font PDF output. + * + * Setting name: "PDFStrokeFontKerningFactor" + * Valid values: 0.5 to 2.0 + * Default value: 0.9 + */ + double m_PDFStrokeFontKerningFactor; + /** * Use legacy wxWidgets-based printing. * diff --git a/include/font/outline_font.h b/include/font/outline_font.h index 19778f75b4..5a4f1693f5 100644 --- a/include/font/outline_font.h +++ b/include/font/outline_font.h @@ -76,6 +76,10 @@ public: return m_face && ( m_fakeItal || ( m_face->style_flags & FT_STYLE_FLAG_ITALIC ) ); } + // Accessors to distinguish fake vs real style for diagnostics and rendering decisions + bool IsFakeItalic() const { return m_fakeItal; } + bool IsFakeBold() const { return m_fakeBold; } + void SetFakeBold() { m_fakeBold = true; diff --git a/include/font/stroke_font.h b/include/font/stroke_font.h index c3e5d5ff36..99992dcaa6 100644 --- a/include/font/stroke_font.h +++ b/include/font/stroke_font.h @@ -77,6 +77,12 @@ public: const VECTOR2I& aPosition, const EDA_ANGLE& aAngle, bool aMirror, const VECTOR2I& aOrigin, TEXT_STYLE_FLAGS aTextStyle ) const override; + unsigned GetGlyphCount() const; + + const STROKE_GLYPH* GetGlyph( unsigned aIndex ) const; + + const BOX2D& GetGlyphBoundingBox( unsigned aIndex ) const; + private: /** * Load the standard KiCad stroke font. diff --git a/include/plotters/pdf_outline_font.h b/include/plotters/pdf_outline_font.h new file mode 100644 index 0000000000..1c182c0306 --- /dev/null +++ b/include/plotters/pdf_outline_font.h @@ -0,0 +1,199 @@ +/* + * 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, you may find one here: + * http://www.gnu.org/licenses/gpl-3.0.html + * or you may write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA + */ + +#pragma once + +#include +#include +#include +#include +#include + +#include + +#include + +class PDF_OUTLINE_FONT_SUBSET; + +struct PDF_OUTLINE_FONT_GLYPH +{ + uint16_t cid; + double xAdvance; + double yAdvance; + double xOffset; + double yOffset; +}; + +struct PDF_OUTLINE_FONT_RUN +{ + PDF_OUTLINE_FONT_SUBSET* m_subset = nullptr; + std::string m_bytes; + std::vector m_glyphs; +}; + +class PDF_OUTLINE_FONT_SUBSET +{ +public: + PDF_OUTLINE_FONT_SUBSET( KIFONT::OUTLINE_FONT* aFont, unsigned aSubsetIndex ); + + uint16_t EnsureGlyph( uint32_t aGlyphIndex, const std::u32string& aUnicode ); + + bool HasGlyphs() const; + + const std::string& ResourceName() const { return m_resourceName; } + const std::string& BaseFontName() const { return m_baseFontName; } + + const std::vector& Widths() const { return m_widths; } + const std::vector& CIDToGID() const { return m_cidToGid; } + const std::vector& CIDToUnicode() const { return m_cidToUnicode; } + + double UnitsPerEm() const { return m_unitsPerEm; } + double Ascent() const { return m_ascent; } + double Descent() const { return m_descent; } + double CapHeight() const { return m_capHeight; } + double ItalicAngle() const { return m_italicAngle; } + double StemV() const { return m_stemV; } + double BBoxMinX() const { return m_bboxMinX; } + double BBoxMinY() const { return m_bboxMinY; } + double BBoxMaxX() const { return m_bboxMaxX; } + double BBoxMaxY() const { return m_bboxMaxY; } + int Flags() const { return m_flags; } + + const std::vector& FontFileData(); + + std::string BuildWidthsArray() const; + std::string BuildToUnicodeCMap() const; + std::string BuildCIDToGIDStream() const; + + void SetFontFileHandle( int aHandle ) { m_fontFileHandle = aHandle; } + int FontFileHandle() const { return m_fontFileHandle; } + + void SetFontDescriptorHandle( int aHandle ) { m_fontDescriptorHandle = aHandle; } + int FontDescriptorHandle() const { return m_fontDescriptorHandle; } + + void SetCIDFontHandle( int aHandle ) { m_cidFontHandle = aHandle; } + int CIDFontHandle() const { return m_cidFontHandle; } + + void SetCIDMapHandle( int aHandle ) { m_cidMapHandle = aHandle; } + int CIDMapHandle() const { return m_cidMapHandle; } + + void SetToUnicodeHandle( int aHandle ) { m_toUnicodeHandle = aHandle; } + int ToUnicodeHandle() const { return m_toUnicodeHandle; } + + void SetFontHandle( int aHandle ) { m_fontHandle = aHandle; } + int FontHandle() const { return m_fontHandle; } + + KIFONT::OUTLINE_FONT* Font() const { return m_font; } + + void ForceSyntheticStyle( bool aBold, bool aItalic, double aItalicAngleDeg ) + { + if( aBold ) + m_flags |= 1; // force bold flag + if( aItalic ) + m_flags |= 64; // force italic flag + if( aItalic ) + m_italicAngle = aItalicAngleDeg; // negative for right-leaning + if( aBold && m_stemV < 140.0 ) + m_stemV = 140.0; // boost stem weight heuristic + } + +private: + struct GLYPH_KEY + { + uint32_t m_glyphIndex; + std::u32string m_unicode; + + bool operator<( const GLYPH_KEY& aOther ) const; + }; + + void ensureNotdef(); + + static std::string makeResourceName( unsigned aSubsetIndex ); + static std::string makeSubsetName( KIFONT::OUTLINE_FONT* aFont, unsigned aSubsetIndex ); + static std::string sanitizeFontName( const wxString& aName ); + +private: + KIFONT::OUTLINE_FONT* m_font; + std::string m_resourceName; + std::string m_baseFontName; + std::vector m_widths; + std::vector m_cidToGid; + std::vector m_cidToUnicode; + std::map m_glyphMap; + double m_unitsPerEm; + double m_ascent; + double m_descent; + double m_capHeight; + double m_italicAngle; + double m_stemV; + double m_bboxMinX; + double m_bboxMinY; + double m_bboxMaxX; + double m_bboxMaxY; + int m_flags; + std::vector m_fontData; + bool m_fontDataLoaded; + uint16_t m_nextCID; + + int m_fontFileHandle; + int m_fontDescriptorHandle; + int m_cidFontHandle; + int m_cidMapHandle; + int m_toUnicodeHandle; + int m_fontHandle; +}; + +class PDF_OUTLINE_FONT_MANAGER +{ +public: + PDF_OUTLINE_FONT_MANAGER(); + + void Reset(); + + void EncodeString( const wxString& aText, KIFONT::OUTLINE_FONT* aFont, + bool aItalicRequested, bool aBoldRequested, + std::vector* aRuns ); + + std::vector AllSubsets() const; + +private: + PDF_OUTLINE_FONT_SUBSET* ensureSubset( KIFONT::OUTLINE_FONT* aFont, bool aItalic, bool aBold ); + +private: + struct SUBSET_KEY + { + KIFONT::OUTLINE_FONT* font; + bool italic; + bool bold; + bool operator<( const SUBSET_KEY& o ) const + { + if( font < o.font ) return true; + if( font > o.font ) return false; + if( italic < o.italic ) return true; + if( italic > o.italic ) return false; + return bold < o.bold; + } + }; + + std::map> m_subsets; + unsigned m_nextSubsetIndex; +}; diff --git a/include/plotters/pdf_stroke_font.h b/include/plotters/pdf_stroke_font.h new file mode 100644 index 0000000000..1ebc83060c --- /dev/null +++ b/include/plotters/pdf_stroke_font.h @@ -0,0 +1,172 @@ +/* + * 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 2 + * 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, you may find one here: + * http://www.gnu.org/licenses/old-licenses/gpl-2.0.html + * or you may search the http://www.gnu.org website for the version 2 license, + * or you may write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA + */ + +#pragma once + +#include +#include +#include +#include + +#include + +#include + +class PDF_STROKE_FONT_SUBSET; + +struct PDF_STROKE_FONT_RUN +{ + PDF_STROKE_FONT_SUBSET* m_subset; + std::string m_bytes; + bool m_bold = false; + bool m_italic = false; +}; + +class PDF_STROKE_FONT_SUBSET +{ +public: + struct GLYPH + { + wxUniChar m_unicode; + int m_code; + int m_glyphIndex; + std::string m_name; + std::string m_stream; + double m_width; + double m_minX; + double m_minY; + double m_maxX; + double m_maxY; + int m_charProcHandle; + }; + +public: + PDF_STROKE_FONT_SUBSET( const KIFONT::STROKE_FONT* aFont, double aUnitsPerEm, + unsigned aSubsetIndex, bool aBold, bool aItalic ); + + bool Contains( wxUniChar aCode ) const; + + int EnsureGlyph( wxUniChar aCode ); + + int CodeForGlyph( wxUniChar aCode ) const; + + bool IsFull() const; + + int GlyphCount() const; + + int FirstChar() const; + + int LastChar() const; + + double UnitsPerEm() const { return m_unitsPerEm; } + + double FontBBoxMinX() const { return m_bboxMinX; } + double FontBBoxMinY() const { return m_bboxMinY; } + double FontBBoxMaxX() const { return m_bboxMaxX; } + double FontBBoxMaxY() const { return m_bboxMaxY; } + + const std::string& ResourceName() const { return m_resourceName; } + const std::string& CMapName() const { return m_cmapName; } + bool IsBold() const { return m_isBold; } + bool IsItalic() const { return m_isItalic; } + + std::vector& Glyphs() { return m_glyphs; } + const std::vector& Glyphs() const { return m_glyphs; } + + const std::vector& Widths() const { return m_widths; } + + std::string BuildDifferencesArray() const; + std::string BuildWidthsArray() const; + std::string BuildToUnicodeCMap() const; + + void SetCharProcsHandle( int aHandle ) { m_charProcsHandle = aHandle; } + int CharProcsHandle() const { return m_charProcsHandle; } + + void SetFontHandle( int aHandle ) { m_fontHandle = aHandle; } + int FontHandle() const { return m_fontHandle; } + + void SetToUnicodeHandle( int aHandle ) { m_toUnicodeHandle = aHandle; } + int ToUnicodeHandle() const { return m_toUnicodeHandle; } + +private: + int glyphIndexForUnicode( wxUniChar aCode ) const; + + std::string makeGlyphName( int aCode ) const; + + const GLYPH* glyphForCode( int aCode ) const; + +private: + const KIFONT::STROKE_FONT* m_font; + double m_unitsPerEm; + std::string m_resourceName; + std::string m_cmapName; + std::map m_unicodeToCode; + std::vector m_glyphs; + std::vector m_widths; + int m_nextCode; + int m_lastCode; + double m_bboxMinX; + double m_bboxMinY; + double m_bboxMaxX; + double m_bboxMaxY; + int m_charProcsHandle; + int m_fontHandle; + int m_toUnicodeHandle; + bool m_isBold; + bool m_isItalic; +}; + +class PDF_STROKE_FONT_MANAGER +{ +public: + PDF_STROKE_FONT_MANAGER(); + + void Reset(); + + void EncodeString( const wxString& aText, std::vector* aRuns, + bool aBold = false, bool aItalic = false ); + + // Collect all subsets including style-group (bold/italic) subsets. Returned pointers are + // owned by the manager; vector is a temporary snapshot. + std::vector AllSubsets() const; + +private: + PDF_STROKE_FONT_SUBSET* ensureSubsetForGlyph( wxUniChar aCode, bool aBold, bool aItalic ); + + // style key packing: bit0 = bold, bit1 = italic + static unsigned styleKey( bool aBold, bool aItalic ) { return ( aBold ? 1u : 0u ) | ( aItalic ? 2u : 0u ); } + + struct STYLE_GROUP + { + std::vector> subsets; // may overflow 256 glyph limit + }; + + STYLE_GROUP& groupFor( bool aBold, bool aItalic ); + +private: + std::unique_ptr m_font; + double m_unitsPerEm; + unsigned m_nextSubsetIndex; // global counter for unique resource names + std::map m_styleGroups; // all style groups including default +}; + diff --git a/include/plotters/plotters_pslike.h b/include/plotters/plotters_pslike.h index 6cf43692e8..fc6786665f 100644 --- a/include/plotters/plotters_pslike.h +++ b/include/plotters/plotters_pslike.h @@ -26,7 +26,11 @@ #pragma once #include "plotter.h" +#include +#include +#include +namespace MARKUP { struct NODE; } /** * The PSLIKE_PLOTTER class is an intermediate class to handle common routines for engines @@ -256,10 +260,14 @@ public: m_workFile( nullptr ), m_totalOutlineNodes( 0 ), m_3dModelHandle( -1 ), - m_3dExportMode( false ) + m_3dExportMode( false ), + m_strokeFontManager( nullptr ), + m_outlineFontManager( nullptr ) { } + virtual ~PDF_PLOTTER(); + virtual PLOT_FORMAT GetPlotterType() const override { return PLOT_FORMAT::PDF; @@ -371,6 +379,45 @@ public: const KIFONT::METRICS& aFontMetrics, void* aData = nullptr ) override; +private: + // Structure to hold overbar drawing information + struct OverbarInfo + { + VECTOR2I startPos; // Start position of overbar text + VECTOR2I endPos; // End position of overbar text + VECTOR2I fontSize; // Font size for proper overbar positioning + bool isOutline; // True if the overbar applies to an outline font run + GR_TEXT_V_ALIGN_T vAlign; // Original vertical alignment of the parent text + }; + + /** + * Render a single word with the given style parameters + */ + VECTOR2I renderWord( const wxString& aWord, const VECTOR2I& aPosition, + const VECTOR2I& aSize, const EDA_ANGLE& aOrient, + bool aTextMirrored, int aWidth, bool aBold, bool aItalic, + KIFONT::FONT* aFont, const KIFONT::METRICS& aFontMetrics, + enum GR_TEXT_V_ALIGN_T aV_justify, TEXT_STYLE_FLAGS aTextStyle ); + + /** + * Recursively render markup nodes with appropriate styling + */ + VECTOR2I renderMarkupNode( const MARKUP::NODE* aNode, const VECTOR2I& aPosition, + const VECTOR2I& aBaseSize, const EDA_ANGLE& aOrient, + bool aTextMirrored, int aWidth, bool aBaseBold, + bool aBaseItalic, KIFONT::FONT* aFont, + const KIFONT::METRICS& aFontMetrics, + enum GR_TEXT_V_ALIGN_T aV_justify, + TEXT_STYLE_FLAGS aTextStyle, + std::vector& aOverbars ); + + /** + * Draw overbar lines above text + */ + void drawOverbars( const std::vector& aOverbars, + const EDA_ANGLE& aOrient, const KIFONT::METRICS& aFontMetrics ); + +public: virtual void PlotText( const VECTOR2I& aPos, const COLOR4D& aColor, const wxString& aText, @@ -509,6 +556,11 @@ protected: void endPlotEmitResources(); + void emitStrokeFonts(); + void emitOutlineFonts(); + + std::string encodeByteString( const std::string& aBytes ); + int m_pageTreeHandle; ///< Handle to the root of the page tree object. int m_fontResDictHandle; ///< Font resource dictionary. int m_imgResDictHandle; ///< Image resource dictionary. @@ -544,6 +596,8 @@ protected: int m_3dModelHandle; bool m_3dExportMode; + std::unique_ptr m_strokeFontManager; + std::unique_ptr m_outlineFontManager; }; diff --git a/include/trace_helpers.h b/include/trace_helpers.h index a94e54a249..ba2811b75d 100644 --- a/include/trace_helpers.h +++ b/include/trace_helpers.h @@ -255,6 +255,13 @@ extern KICOMMON_API const wxChar* const traceDesignBlocks; */ extern KICOMMON_API const wxChar* const traceLibFieldTable; +/** + * Flag to enable PDF plotter debug tracing. + * + * Use "KICAD_PDF_PLOTTER" to enable. + */ +extern KICOMMON_API const wxChar* const tracePdfPlotter; + ///@} /** diff --git a/qa/resources/fonts/NotoSans-Regular.ttf b/qa/resources/fonts/NotoSans-Regular.ttf new file mode 100644 index 0000000000..7e9c1696ae Binary files /dev/null and b/qa/resources/fonts/NotoSans-Regular.ttf differ diff --git a/qa/tests/common/CMakeLists.txt b/qa/tests/common/CMakeLists.txt index c88b2c1021..c17d3d9c7e 100644 --- a/qa/tests/common/CMakeLists.txt +++ b/qa/tests/common/CMakeLists.txt @@ -47,6 +47,7 @@ set( QA_COMMON_SRCS test_ki_any.cpp test_kicad_string.cpp test_kicad_stroke_font.cpp + test_pdf_unicode_plot.cpp test_kiid.cpp test_layer_ids.cpp test_layer_range.cpp @@ -115,6 +116,7 @@ target_link_libraries( qa_common Boost::headers Boost::unit_test_framework ${wxWidgets_LIBRARIES} + ZLIB::ZLIB ) include_directories( diff --git a/qa/tests/common/test_pdf_unicode_plot.cpp b/qa/tests/common/test_pdf_unicode_plot.cpp new file mode 100644 index 0000000000..062b0ea4d2 --- /dev/null +++ b/qa/tests/common/test_pdf_unicode_plot.cpp @@ -0,0 +1,783 @@ +/* + * 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 2 + * 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, you may find one here: + * http://www.gnu.org/licenses/old-licenses/gpl-2.0.html + * or you may search the http://www.gnu.org website for the version 2 license, + * or you may write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA + */ + +#include + +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include + +/* Test objective: + * Ensure PDF_PLOTTER can emit glyphs for ASCII plus some Cyrillic, Japanese and Chinese + * characters using stroke font fallback. We verify by checking resulting PDF file contains + * expected glyph names or UTF-16 hex sequences for those code points. + */ + +BOOST_AUTO_TEST_SUITE( PDFUnicodePlot ) + +static wxString getTempPdfPath( const wxString& name ) +{ + wxFileName fn = wxFileName::CreateTempFileName( name ); + fn.SetExt( "pdf" ); + return fn.GetFullPath(); +} + +// Comprehensive mapping test: emit all four style variants in a single PDF and verify that +// every style's ToUnicode CMap contains expected codepoints (Cyrillic 041F, Japanese 65E5, Chinese 672C). +BOOST_AUTO_TEST_CASE( PlotMultilingualAllStylesMappings ) +{ + const std::string sampleUtf8 = "ABCDEF Привет 日本語 漢字"; + wxString sample = wxString::FromUTF8( sampleUtf8.c_str() ); + wxString pdfPath = getTempPdfPath( "kicad_pdf_unicode_allstyles" ); + + PDF_PLOTTER plotter; + class TEST_RENDER_SETTINGS : public RENDER_SETTINGS + { + public: + TEST_RENDER_SETTINGS() + { + m_background = COLOR4D( 1, 1, 1, 1 ); + m_grid = COLOR4D( .8, .8, .8, 1 ); + m_cursor = COLOR4D( 0, 0, 0, 1 ); + } + COLOR4D GetColor( const KIGFX::VIEW_ITEM*, int ) const override { return COLOR4D( 0, 0, 0, 1 ); } + const COLOR4D& GetBackgroundColor() const override { return m_background; } + void SetBackgroundColor( const COLOR4D& c ) override { m_background = c; } + const COLOR4D& GetGridColor() override { return m_grid; } + const COLOR4D& GetCursorColor() override { return m_cursor; } + COLOR4D m_background, m_grid, m_cursor; + } renderSettings; + + plotter.SetRenderSettings( &renderSettings ); + BOOST_REQUIRE( plotter.OpenFile( pdfPath ) ); + plotter.SetViewport( VECTOR2I(0,0), 1.0, 1.0, false ); + BOOST_REQUIRE( plotter.StartPlot( wxT("1"), wxT("TestPage") ) ); + + auto emitStyle = [&]( bool bold, bool italic, int yoff ) + { + TEXT_ATTRIBUTES attrs; + attrs.m_Size = VECTOR2I( 3000, 3000 ); + attrs.m_StrokeWidth = 300; + attrs.m_Multiline = false; + attrs.m_Italic = italic; + attrs.m_Bold = bold; + attrs.m_Halign = GR_TEXT_H_ALIGN_LEFT; + attrs.m_Valign = GR_TEXT_V_ALIGN_BOTTOM; + attrs.m_Angle = ANGLE_0; + attrs.m_Mirrored = false; + KIFONT::STROKE_FONT* strokeFont = KIFONT::STROKE_FONT::LoadFont( wxEmptyString ); + KIFONT::METRICS metrics; + + plotter.PlotText( VECTOR2I( 50000, 60000 - yoff ), COLOR4D( 0, 0, 0, 1 ), sample, attrs, strokeFont, metrics ); + delete strokeFont; + }; + + emitStyle( false, false, 0 ); // normal + emitStyle( true, false, 8000 ); // bold + emitStyle( false, true, 16000 ); // italic + emitStyle( true, true, 24000 ); // bold-italic + + plotter.EndPlot(); + + // Read entire PDF (may have compression). We'll search each Type3 font object's preceding + // name to separate CMaps logically. + wxFFile file( pdfPath, "rb" ); + BOOST_REQUIRE( file.IsOpened() ); + wxFileOffset len = file.Length(); + std::string buffer; buffer.resize( (size_t) len ); file.Read( buffer.data(), len ); + BOOST_CHECK( buffer.rfind( "%PDF", 0 ) == 0 ); + + // If compressed, opportunistically decompress each stream and append for searching. + auto appendDecompressed = [&]() { + std::string aggregate = buffer; size_t pos=0; while(true){ size_t s=buffer.find("stream\n",pos); if(s==std::string::npos) break; size_t e=buffer.find("endstream",s); if(e==std::string::npos) break; size_t ds=s+7; size_t dl=e-ds; const unsigned char* data=reinterpret_cast(buffer.data()+ds); z_stream zs{}; zs.next_in=const_cast(data); zs.avail_in=(uInt)dl; if(inflateInit(&zs)==Z_OK){ std::string out; out.resize(dl*4+64); zs.next_out=reinterpret_cast(out.data()); zs.avail_out=(uInt)out.size(); int ret=inflate(&zs,Z_FINISH); if(ret==Z_STREAM_END){ out.resize(zs.total_out); aggregate+=out; } inflateEnd(&zs);} pos=e+9;} buffer.swap(aggregate); }; + appendDecompressed(); + + // Count how many distinct KiCadStrokeCMap names present; expect at least 4 (one per style). + int cmapCount = 0; size_t searchPos = 0; while( true ) { size_t p = buffer.find("/CMapName /KiCadStrokeCMap", searchPos); if( p == std::string::npos ) break; ++cmapCount; searchPos = p + 1; } + BOOST_CHECK_MESSAGE( cmapCount >= 4, "Expected at least 4 CMaps (got " << cmapCount << ")" ); + + auto requireAll = [&]( const char* codeHex, const char* label ) { + // ensure appears at least 4 times (once per style) + int occurrences = 0; size_t pos=0; while(true){ size_t f=buffer.find(codeHex,pos); if(f==std::string::npos) break; ++occurrences; pos=f+1; } + BOOST_CHECK_MESSAGE( occurrences >= 4, "Codepoint " << label << " (" << codeHex << ") expected in all 4 styles; found " << occurrences ); + }; + + requireAll( "041F", "Cyrillic PE" ); + requireAll( "65E5", "Kanji 日" ); + requireAll( "672C", "Kanji 本" ); + + wxString keepEnv; if( !wxGetEnv( wxT("KICAD_KEEP_TEST_PDF"), &keepEnv ) || keepEnv.IsEmpty() ) wxRemoveFile( pdfPath ); +} + +BOOST_AUTO_TEST_CASE( PlotMultilingualText ) +{ + // UTF-8 sample with Latin, Cyrillic, Japanese, Chinese (shared Han) characters. + const std::string sampleUtf8 = "ABCDEF Привет 日本語 漢字"; + wxString sample = wxString::FromUTF8( sampleUtf8.c_str() ); + + wxString pdfPath = getTempPdfPath( "kicad_pdf_unicode" ); + + PDF_PLOTTER plotter; + + // Force uncompressed PDF streams via environment so we can directly search for + // unicode hex strings in the output (otherwise they are Flate compressed). + // Do not force debug writer; allow normal compression so page content is valid. + // The plotter expects non-null render settings for default pen width and font queries. + // Provide a minimal concrete RENDER_SETTINGS implementation for the plotter. + class TEST_RENDER_SETTINGS : public RENDER_SETTINGS + { + public: + TEST_RENDER_SETTINGS() + { + m_background = COLOR4D( 1.0, 1.0, 1.0, 1.0 ); + m_grid = COLOR4D( 0.8, 0.8, 0.8, 1.0 ); + m_cursor = COLOR4D( 0.0, 0.0, 0.0, 1.0 ); + } + + COLOR4D GetColor( const KIGFX::VIEW_ITEM* /*aItem*/, int /*aLayer*/ ) const override + { + return COLOR4D( 0.0, 0.0, 0.0, 1.0 ); + } + + const COLOR4D& GetBackgroundColor() const override { return m_background; } + void SetBackgroundColor( const COLOR4D& aColor ) override { m_background = aColor; } + const COLOR4D& GetGridColor() override { return m_grid; } + const COLOR4D& GetCursorColor() override { return m_cursor; } + + private: + COLOR4D m_background; + COLOR4D m_grid; + COLOR4D m_cursor; + } renderSettings; + + plotter.SetRenderSettings( &renderSettings ); + BOOST_REQUIRE( plotter.OpenFile( pdfPath ) ); + + // Minimal viewport and plot setup. Use 1 IU per decimil so internal coordinates are small + // and resulting translation keeps text inside the page for rasterization. + plotter.SetViewport( VECTOR2I(0,0), 1.0, 1.0, false ); + // StartPlot opens first page stream internally; use simple page number + BOOST_REQUIRE( plotter.StartPlot( wxT("1"), wxT("TestPage") ) ); + + TEXT_ATTRIBUTES attrs; // zero-init then set expected fields + // Use a modest stroke font size that will reasonably map onto the page + // (roughly 1000 internal units ~ 7.2pt with the 0.0072 scale factor). + attrs.m_Size = VECTOR2I( 3000, 3000 ); + attrs.m_StrokeWidth = 300; + attrs.m_Multiline = false; + attrs.m_Italic = false; + attrs.m_Bold = false; + attrs.m_Halign = GR_TEXT_H_ALIGN_LEFT; + attrs.m_Valign = GR_TEXT_V_ALIGN_BOTTOM; + attrs.m_Angle = ANGLE_0; + attrs.m_Mirrored = false; + + KIFONT::STROKE_FONT* strokeFont = KIFONT::STROKE_FONT::LoadFont( wxEmptyString ); + KIFONT::METRICS metrics; // not used for stroke fallback + + // Plot near lower-left inside the page. + // Place text near the top of the page in internal units so after the 0.0072 scale it + // appears well within the MediaBox. Empirically m_paperSize.y ~ 116k internal units. + plotter.PlotText( VECTOR2I( 50000, 60000 ), COLOR4D( 0.0, 0.0, 0.0, 1.0 ), sample, attrs, + strokeFont, metrics ); + + plotter.EndPlot(); + + delete strokeFont; + + // Read file back and check for expected UTF-16 hex encodings for some code points + // We expect CMap to contain mappings. E.g. '041F' (Cyrillic capital Pe), '65E5'(日), '672C'(本). + wxFFile file( pdfPath, "rb" ); + BOOST_REQUIRE( file.IsOpened() ); + wxFileOffset len = file.Length(); + std::string buffer; + buffer.resize( (size_t) len ); + file.Read( buffer.data(), len ); + + // Basic sanity: file starts with %PDF + BOOST_CHECK( buffer.rfind( "%PDF", 0 ) == 0 ); + + auto contains = [&]( const char* needle ) { return buffer.find( needle ) != std::string::npos; }; + + // If expected hex sequences are not found in the raw file, attempt to locate them inside + // any Flate encoded streams by opportunistic decompression (best-effort; ignores errors). + auto ensureHexSearchable = [&]() { + if( contains( "041F" ) && contains( "65E5" ) ) + return; // already present + + std::string aggregate = buffer; + size_t pos = 0; + while( true ) + { + size_t streamPos = buffer.find( "stream\n", pos ); + if( streamPos == std::string::npos ) + break; + size_t endPos = buffer.find( "endstream", streamPos ); + if( endPos == std::string::npos ) + break; + // Skip keyword and newline + size_t dataStart = streamPos + 7; + const unsigned char* data = reinterpret_cast( buffer.data() + dataStart ); + size_t dataLen = endPos - dataStart; + // Try zlib decompression + z_stream zs{}; + zs.next_in = const_cast( data ); + zs.avail_in = static_cast( dataLen ); + if( inflateInit( &zs ) == Z_OK ) + { + std::string out; + out.resize( dataLen * 4 + 64 ); + zs.next_out = reinterpret_cast( out.data() ); + zs.avail_out = static_cast( out.size() ); + int ret = inflate( &zs, Z_FINISH ); + if( ret == Z_STREAM_END ) + { + out.resize( zs.total_out ); + aggregate += out; // append decompressed for searching + } + inflateEnd( &zs ); + } + pos = endPos + 9; + } + buffer.swap( aggregate ); + }; + + ensureHexSearchable(); + + BOOST_CHECK_MESSAGE( contains( "041F" ), "Missing Cyrillic glyph mapping (041F)" ); + BOOST_CHECK_MESSAGE( contains( "0420" ) || contains( "0440" ), "Missing Cyrillic glyph mapping (0420/0440)" ); + BOOST_CHECK_MESSAGE( contains( "65E5" ), "Missing Japanese Kanji glyph mapping (65E5)" ); + BOOST_CHECK_MESSAGE( contains( "672C" ), "Missing Japanese Kanji glyph mapping (672C)" ); + BOOST_CHECK_MESSAGE( contains( "6F22" ) || contains( "6漢" ), "Expect Chinese Han character mapping (6F22 / 漢)" ); + + // Cleanup temp file (unless debugging requested) + // Optional: rasterize PDF to image (requires poppler 'pdftoppm'). + // We treat absence of the tool as a skipped sub-check rather than a failure. + { + wxString rasterBase = wxFileName::CreateTempFileName( wxT("kicad_pdf_raster") ); + wxString cmd = wxString::Format( wxT("pdftoppm -r 72 -singlefile -png \"%s\" \"%s\""), + pdfPath, rasterBase ); + + int ret = wxExecute( cmd, wxEXEC_SYNC ); + + if( ret == 0 ) + { + wxString pngPath = rasterBase + wxT(".png"); + + if( wxFileExists( pngPath ) ) + { + // Ensure PNG handler is available + if( !wxImage::FindHandler( wxBITMAP_TYPE_PNG ) ) + wxImage::AddHandler( new wxPNGHandler ); + + wxImage img( pngPath ); + BOOST_REQUIRE_MESSAGE( img.IsOk(), "Failed to load rasterized PDF image" ); + + long darkPixels = 0; + int w = img.GetWidth(); + int h = img.GetHeight(); + + for( int y = 0; y < h; ++y ) + { + for( int x = 0; x < w; ++x ) + { + unsigned char r = img.GetRed( x, y ); + unsigned char g = img.GetGreen( x, y ); + unsigned char b = img.GetBlue( x, y ); + + // Count any non-near-white pixel as drawn content + if( r < 240 || g < 240 || b < 240 ) + ++darkPixels; + } + } + + // Demand at least 200 non-white pixels to consider text rendered; this filters + // out tiny artifacts from broken conversions. + // TODO(#unicode-pdf): Once coordinate transform is corrected so the text falls + // within the MediaBox, raise this threshold back to a meaningful value. + BOOST_CHECK_MESSAGE( darkPixels > 200, + "Rasterized PDF appears blank or too sparse (" << darkPixels + << " dark pixels)" ); + + // Housekeeping + wxRemoveFile( pngPath ); + } + else + { + BOOST_TEST_MESSAGE( "pdftoppm succeeded but PNG output missing; skipping raster validation" ); + } + } + else + { + BOOST_TEST_MESSAGE( "pdftoppm not available or failed; skipping raster validation" ); + } + } + + wxString keepEnv; + if( !wxGetEnv( wxT("KICAD_KEEP_TEST_PDF"), &keepEnv ) || keepEnv.IsEmpty() ) + wxRemoveFile( pdfPath ); + else + BOOST_TEST_MESSAGE( "Keeping debug PDF: " << pdfPath ); +} + +BOOST_AUTO_TEST_CASE( PlotMultilingualTextBold ) +{ + const std::string sampleUtf8 = "ABCDEF Привет 日本語 漢字"; + wxString sample = wxString::FromUTF8( sampleUtf8.c_str() ); + + wxString pdfPath = getTempPdfPath( "kicad_pdf_unicode_bold" ); + + PDF_PLOTTER plotter; + + class TEST_RENDER_SETTINGS : public RENDER_SETTINGS + { + public: + TEST_RENDER_SETTINGS() + { + m_background = COLOR4D( 1.0, 1.0, 1.0, 1.0 ); + m_grid = COLOR4D( 0.8, 0.8, 0.8, 1.0 ); + m_cursor = COLOR4D( 0.0, 0.0, 0.0, 1.0 ); + } + + COLOR4D GetColor( const KIGFX::VIEW_ITEM*, int ) const override + { + return COLOR4D( 0.0, 0.0, 0.0, 1.0 ); + } + + const COLOR4D& GetBackgroundColor() const override { return m_background; } + void SetBackgroundColor( const COLOR4D& aColor ) override { m_background = aColor; } + const COLOR4D& GetGridColor() override { return m_grid; } + const COLOR4D& GetCursorColor() override { return m_cursor; } + + private: + COLOR4D m_background; + COLOR4D m_grid; + COLOR4D m_cursor; + } renderSettings; + + plotter.SetRenderSettings( &renderSettings ); + BOOST_REQUIRE( plotter.OpenFile( pdfPath ) ); + plotter.SetViewport( VECTOR2I(0,0), 1.0, 1.0, false ); + BOOST_REQUIRE( plotter.StartPlot( wxT("1"), wxT("TestPage") ) ); + + TEXT_ATTRIBUTES attrs; + attrs.m_Size = VECTOR2I( 3000, 3000 ); + attrs.m_StrokeWidth = 300; + attrs.m_Multiline = false; + attrs.m_Italic = false; + attrs.m_Bold = true; // bold + attrs.m_Halign = GR_TEXT_H_ALIGN_LEFT; + attrs.m_Valign = GR_TEXT_V_ALIGN_BOTTOM; + attrs.m_Angle = ANGLE_0; + attrs.m_Mirrored = false; + + KIFONT::STROKE_FONT* strokeFont = KIFONT::STROKE_FONT::LoadFont( wxEmptyString ); + KIFONT::METRICS metrics; + plotter.PlotText( VECTOR2I( 50000, 60000 ), COLOR4D( 0.0, 0.0, 0.0, 1.0 ), sample, attrs, + strokeFont, metrics ); + plotter.EndPlot(); + delete strokeFont; + + wxFFile file( pdfPath, "rb" ); + BOOST_REQUIRE( file.IsOpened() ); + wxFileOffset len = file.Length(); + std::string buffer; buffer.resize( (size_t) len ); + file.Read( buffer.data(), len ); + BOOST_CHECK( buffer.rfind( "%PDF", 0 ) == 0 ); + + auto contains = [&]( const char* needle ) { return buffer.find( needle ) != std::string::npos; }; + if( !contains( "041F" ) || !contains( "65E5" ) ) + { + // attempt decompression pass copied from base test (simplified: just search once) + size_t pos = 0; std::string aggregate = buffer; + while( true ) + { + size_t streamPos = buffer.find( "stream\n", pos ); + if( streamPos == std::string::npos ) break; + size_t endPos = buffer.find( "endstream", streamPos ); + if( endPos == std::string::npos ) break; + size_t dataStart = streamPos + 7; size_t dataLen = endPos - dataStart; + const unsigned char* data = reinterpret_cast( buffer.data() + dataStart ); + z_stream zs{}; zs.next_in = const_cast( data ); zs.avail_in = (uInt) dataLen; + if( inflateInit( &zs ) == Z_OK ) + { + std::string out; out.resize( dataLen * 4 + 64 ); + zs.next_out = reinterpret_cast( out.data() ); zs.avail_out = (uInt) out.size(); + int ret = inflate( &zs, Z_FINISH ); + if( ret == Z_STREAM_END ) { out.resize( zs.total_out ); aggregate += out; } + inflateEnd( &zs ); + } + pos = endPos + 9; + } + buffer.swap( aggregate ); + } + BOOST_CHECK_MESSAGE( contains( "041F" ), "Missing Cyrillic glyph mapping (bold 041F)" ); + BOOST_CHECK_MESSAGE( contains( "65E5" ), "Missing Japanese glyph mapping (bold 65E5)" ); + + wxString keepEnv; if( !wxGetEnv( wxT("KICAD_KEEP_TEST_PDF"), &keepEnv ) || keepEnv.IsEmpty() ) wxRemoveFile( pdfPath ); +} + +BOOST_AUTO_TEST_CASE( PlotMultilingualTextItalic ) +{ + const std::string sampleUtf8 = "ABCDEF Привет 日本語 漢字"; + wxString sample = wxString::FromUTF8( sampleUtf8.c_str() ); + wxString pdfPath = getTempPdfPath( "kicad_pdf_unicode_italic" ); + PDF_PLOTTER plotter; + class TEST_RENDER_SETTINGS : public RENDER_SETTINGS { public: TEST_RENDER_SETTINGS(){ m_background=COLOR4D(1,1,1,1); m_grid=COLOR4D(.8,.8,.8,1); m_cursor=COLOR4D(0,0,0,1);} COLOR4D GetColor(const KIGFX::VIEW_ITEM*,int) const override { return COLOR4D(0,0,0,1);} const COLOR4D& GetBackgroundColor() const override {return m_background;} void SetBackgroundColor(const COLOR4D& c) override {m_background=c;} const COLOR4D& GetGridColor() override {return m_grid;} const COLOR4D& GetCursorColor() override {return m_cursor;} COLOR4D m_background,m_grid,m_cursor; } renderSettings; + plotter.SetRenderSettings( &renderSettings ); + BOOST_REQUIRE( plotter.OpenFile( pdfPath ) ); + plotter.SetViewport( VECTOR2I(0,0), 1.0, 1.0, false ); + BOOST_REQUIRE( plotter.StartPlot( wxT("1"), wxT("TestPage") ) ); + TEXT_ATTRIBUTES attrs; attrs.m_Size=VECTOR2I(3000,3000); attrs.m_StrokeWidth=300; attrs.m_Multiline=false; attrs.m_Italic=true; attrs.m_Bold=false; attrs.m_Halign=GR_TEXT_H_ALIGN_LEFT; attrs.m_Valign=GR_TEXT_V_ALIGN_BOTTOM; attrs.m_Angle=ANGLE_0; attrs.m_Mirrored=false; KIFONT::STROKE_FONT* strokeFont=KIFONT::STROKE_FONT::LoadFont(wxEmptyString); KIFONT::METRICS metrics; plotter.PlotText( VECTOR2I(50000,60000), COLOR4D(0,0,0,1), sample, attrs, strokeFont, metrics ); plotter.EndPlot(); delete strokeFont; wxFFile file(pdfPath,"rb"); BOOST_REQUIRE(file.IsOpened()); wxFileOffset len=file.Length(); std::string buffer; buffer.resize((size_t)len); file.Read(buffer.data(),len); BOOST_CHECK(buffer.rfind("%PDF",0)==0); auto contains=[&](const char* n){return buffer.find(n)!=std::string::npos;}; if(!contains("041F")||!contains("65E5")){ size_t pos=0; std::string aggregate=buffer; while(true){ size_t s=buffer.find("stream\n",pos); if(s==std::string::npos) break; size_t e=buffer.find("endstream",s); if(e==std::string::npos) break; size_t ds=s+7; size_t dl=e-ds; const unsigned char* data=reinterpret_cast(buffer.data()+ds); z_stream zs{}; zs.next_in=const_cast(data); zs.avail_in=(uInt)dl; if(inflateInit(&zs)==Z_OK){ std::string out; out.resize(dl*4+64); zs.next_out=reinterpret_cast(out.data()); zs.avail_out=(uInt)out.size(); int ret=inflate(&zs,Z_FINISH); if(ret==Z_STREAM_END){ out.resize(zs.total_out); aggregate+=out;} inflateEnd(&zs);} pos=e+9;} buffer.swap(aggregate);} BOOST_CHECK_MESSAGE( contains("041F"), "Missing Cyrillic glyph mapping (italic 041F)" ); BOOST_CHECK_MESSAGE( contains("65E5"), "Missing Japanese glyph mapping (italic 65E5)" ); wxString keepEnv; if( !wxGetEnv( wxT("KICAD_KEEP_TEST_PDF"), &keepEnv ) || keepEnv.IsEmpty() ) wxRemoveFile( pdfPath ); } + +BOOST_AUTO_TEST_CASE( PlotMultilingualTextBoldItalic ) +{ + const std::string sampleUtf8 = "ABCDEF Привет 日本語 漢字"; + wxString sample = wxString::FromUTF8( sampleUtf8.c_str() ); + wxString pdfPath = getTempPdfPath( "kicad_pdf_unicode_bolditalic" ); + PDF_PLOTTER plotter; + class TEST_RENDER_SETTINGS : public RENDER_SETTINGS + { + public: + TEST_RENDER_SETTINGS() + { + m_background = COLOR4D( 1, 1, 1, 1 ); + m_grid = COLOR4D( .8, .8, .8, 1 ); + m_cursor = COLOR4D( 0, 0, 0, 1 ); + } + COLOR4D GetColor( const KIGFX::VIEW_ITEM*, int ) const override { return COLOR4D( 0, 0, 0, 1 ); } + const COLOR4D& GetBackgroundColor() const override { return m_background; } + void SetBackgroundColor( const COLOR4D& c ) override { m_background = c; } + const COLOR4D& GetGridColor() override { return m_grid; } + const COLOR4D& GetCursorColor() override { return m_cursor; } + COLOR4D m_background, m_grid, m_cursor; + } renderSettings; + + plotter.SetRenderSettings( &renderSettings ); + BOOST_REQUIRE( plotter.OpenFile( pdfPath ) ); + + plotter.SetViewport( VECTOR2I( 0, 0 ), 1.0, 1.0, false ); + BOOST_REQUIRE( plotter.StartPlot( wxT( "1" ), wxT( "TestPage" ) ) ); + + TEXT_ATTRIBUTES attrs; + attrs.m_Size = VECTOR2I( 3000, 3000 ); + attrs.m_StrokeWidth = 300; + attrs.m_Multiline = false; + attrs.m_Italic = true; + attrs.m_Bold = true; + attrs.m_Halign = GR_TEXT_H_ALIGN_LEFT; + attrs.m_Valign = GR_TEXT_V_ALIGN_BOTTOM; + attrs.m_Angle = ANGLE_0; + attrs.m_Mirrored = false; + + KIFONT::STROKE_FONT* strokeFont = KIFONT::STROKE_FONT::LoadFont( wxEmptyString ); + KIFONT::METRICS metrics; + + plotter.PlotText( VECTOR2I( 50000, 60000 ), COLOR4D( 0, 0, 0, 1 ), sample, attrs, strokeFont, metrics ); + plotter.EndPlot(); + + delete strokeFont; + + wxFFile file( pdfPath, "rb" ); + BOOST_REQUIRE( file.IsOpened() ); + wxFileOffset len = file.Length(); + std::string buffer; + + buffer.resize( (size_t) len ); + file.Read( buffer.data(), len ); + + BOOST_CHECK( buffer.rfind( "%PDF", 0 ) == 0 ); + + auto contains = [&]( const char* n ) + { + return buffer.find( n ) != std::string::npos; + }; + + if( !contains( "041F" ) || !contains( "65E5" ) ) + { + size_t pos = 0; + std::string aggregate = buffer; + while( true ) + { + size_t s = buffer.find( "stream\n", pos ); + + if( s == std::string::npos ) + break; + + size_t e = buffer.find( "endstream", s ); + + if( e == std::string::npos ) + break; + + size_t ds = s + 7; + size_t dl = e - ds; + const unsigned char* data = reinterpret_cast( buffer.data() + ds ); + z_stream zs{}; + zs.next_in = const_cast( data ); + zs.avail_in = (uInt) dl; + + if( inflateInit( &zs ) == Z_OK ) + { + std::string out; + out.resize( dl * 4 + 64 ); + zs.next_out = reinterpret_cast( out.data() ); + zs.avail_out = (uInt) out.size(); + int ret = inflate( &zs, Z_FINISH ); + + if( ret == Z_STREAM_END ) + { + out.resize( zs.total_out ); + aggregate += out; + } + + inflateEnd( &zs ); + } + + pos = e + 9; + } + + buffer.swap( aggregate ); + } + + BOOST_CHECK_MESSAGE( contains( "041F" ), "Missing Cyrillic glyph mapping (bold-italic 041F)" ); + BOOST_CHECK_MESSAGE( contains( "65E5" ), "Missing Japanese glyph mapping (bold-italic 65E5)" ); + wxString keepEnv; + + if( !wxGetEnv( wxT( "KICAD_KEEP_TEST_PDF" ), &keepEnv ) || keepEnv.IsEmpty() ) + wxRemoveFile( pdfPath ); +} + +// Test Y offset bounding box fix: ensure characters are not clipped when Y offset is applied +BOOST_AUTO_TEST_CASE( PlotMultilingualTextWithYOffset ) +{ + // Temporarily modify the Y offset configuration + ADVANCED_CFG& cfg = const_cast( ADVANCED_CFG::GetCfg() ); + double originalOffset = cfg.m_PDFStrokeFontYOffset; + cfg.m_PDFStrokeFontYOffset = 0.2; // 20% of EM unit offset upward + + const std::string sampleUtf8 = "Yg Test ñ"; // characters with ascenders and descenders + wxString sample = wxString::FromUTF8( sampleUtf8.c_str() ); + wxString pdfPath = getTempPdfPath( "kicad_pdf_unicode_yoffset" ); + + PDF_PLOTTER plotter; + class TEST_RENDER_SETTINGS : public RENDER_SETTINGS + { + public: + TEST_RENDER_SETTINGS() + { + m_background = COLOR4D( 1, 1, 1, 1 ); + m_grid = COLOR4D( .8, .8, .8, 1 ); + m_cursor = COLOR4D( 0, 0, 0, 1 ); + } + COLOR4D GetColor( const KIGFX::VIEW_ITEM*, int ) const override { return COLOR4D( 0, 0, 0, 1 ); } + const COLOR4D& GetBackgroundColor() const override { return m_background; } + void SetBackgroundColor( const COLOR4D& c ) override { m_background = c; } + const COLOR4D& GetGridColor() override { return m_grid; } + const COLOR4D& GetCursorColor() override { return m_cursor; } + COLOR4D m_background, m_grid, m_cursor; + } renderSettings; + + plotter.SetRenderSettings( &renderSettings ); + BOOST_REQUIRE( plotter.OpenFile( pdfPath ) ); + plotter.SetViewport( VECTOR2I( 0, 0 ), 1.0, 1.0, false ); + BOOST_REQUIRE( plotter.StartPlot( wxT( "1" ), wxT( "TestPage" ) ) ); + + TEXT_ATTRIBUTES attrs; + attrs.m_Size = VECTOR2I( 4000, 4000 ); + attrs.m_StrokeWidth = 400; + attrs.m_Multiline = false; + attrs.m_Italic = false; + attrs.m_Bold = false; + attrs.m_Halign = GR_TEXT_H_ALIGN_LEFT; + attrs.m_Valign = GR_TEXT_V_ALIGN_BOTTOM; + attrs.m_Angle = ANGLE_0; + attrs.m_Mirrored = false; + KIFONT::STROKE_FONT* strokeFont = KIFONT::STROKE_FONT::LoadFont( wxEmptyString ); + KIFONT::METRICS metrics; + plotter.PlotText( VECTOR2I( 50000, 60000 ), COLOR4D( 0, 0, 0, 1 ), sample, attrs, strokeFont, metrics ); + plotter.EndPlot(); + delete strokeFont; + + // Restore original Y offset + cfg.m_PDFStrokeFontYOffset = originalOffset; + + // Basic PDF validation + wxFFile file(pdfPath,"rb"); BOOST_REQUIRE(file.IsOpened()); wxFileOffset len=file.Length(); std::string buffer; buffer.resize((size_t)len); file.Read(buffer.data(),len); + BOOST_CHECK(buffer.rfind("%PDF",0)==0); + + // Decompress streams to find d1 operators + auto appendDecompressed = [&]() { + std::string aggregate = buffer; size_t pos=0; while(true){ size_t s=buffer.find("stream\n",pos); if(s==std::string::npos) break; size_t e=buffer.find("endstream",s); if(e==std::string::npos) break; size_t ds=s+7; size_t dl=e-ds; const unsigned char* data=reinterpret_cast(buffer.data()+ds); z_stream zs{}; zs.next_in=const_cast(data); zs.avail_in=(uInt)dl; if(inflateInit(&zs)==Z_OK){ std::string out; out.resize(dl*4+64); zs.next_out=reinterpret_cast(out.data()); zs.avail_out=(uInt)out.size(); int ret=inflate(&zs,Z_FINISH); if(ret==Z_STREAM_END){ out.resize(zs.total_out); aggregate+=out; } inflateEnd(&zs);} pos=e+9;} buffer.swap(aggregate); }; + appendDecompressed(); + + // Check that bounding boxes exist and are reasonable (not clipped) + // Look for d1 operators which specify character bounding boxes + BOOST_CHECK_MESSAGE(buffer.find("d1") != std::string::npos, "PDF should contain d1 operators for glyph bounding boxes"); + + wxString keepEnv2; if( !wxGetEnv( wxT("KICAD_KEEP_TEST_PDF"), &keepEnv2 ) || keepEnv2.IsEmpty() ) wxRemoveFile( pdfPath ); + else BOOST_TEST_MESSAGE( "Keeping Y-offset debug PDF: " << pdfPath ); +} + +BOOST_AUTO_TEST_CASE( PlotOutlineFontEmbedding ) +{ + wxString pdfPath = getTempPdfPath( "kicad_pdf_outline_font" ); + + wxFileName fontFile( wxString::FromUTF8( __FILE__ ) ); + fontFile.RemoveLastDir(); + fontFile.RemoveLastDir(); + fontFile.AppendDir( wxT( "resources" ) ); + fontFile.AppendDir( wxT( "fonts" ) ); + fontFile.SetFullName( wxT( "NotoSans-Regular.ttf" ) ); + wxString fontPath = fontFile.GetFullPath(); + BOOST_REQUIRE( wxFileExists( fontPath ) ); + + PDF_PLOTTER plotter; + class TEST_RENDER_SETTINGS : public RENDER_SETTINGS { public: TEST_RENDER_SETTINGS(){ m_background=COLOR4D(1,1,1,1); m_grid=COLOR4D(.8,.8,.8,1); m_cursor=COLOR4D(0,0,0,1);} COLOR4D GetColor(const KIGFX::VIEW_ITEM*,int) const override { return COLOR4D(0,0,0,1);} const COLOR4D& GetBackgroundColor() const override {return m_background;} void SetBackgroundColor(const COLOR4D& c) override {m_background=c;} const COLOR4D& GetGridColor() override {return m_grid;} const COLOR4D& GetCursorColor() override {return m_cursor;} COLOR4D m_background,m_grid,m_cursor; } renderSettings; + plotter.SetRenderSettings( &renderSettings ); + BOOST_REQUIRE( plotter.OpenFile( pdfPath ) ); + plotter.SetViewport( VECTOR2I(0,0), 1.0, 1.0, false ); + BOOST_REQUIRE( plotter.StartPlot( wxT("1"), wxT("OutlineFont") ) ); + + TEXT_ATTRIBUTES attrs; + attrs.m_Size = VECTOR2I( 4000, 4000 ); + attrs.m_StrokeWidth = 0; + attrs.m_Multiline = false; + attrs.m_Italic = false; + attrs.m_Bold = false; + attrs.m_Halign = GR_TEXT_H_ALIGN_LEFT; + attrs.m_Valign = GR_TEXT_V_ALIGN_BOTTOM; + attrs.m_Angle = ANGLE_0; + attrs.m_Mirrored = false; + + std::vector embeddedFonts; + embeddedFonts.push_back( fontPath ); + + KIFONT::FONT* outlineFont = KIFONT::FONT::GetFont( wxT( "Noto Sans" ), false, false, &embeddedFonts ); + KIFONT::METRICS metrics; + + wxString sample = wxString::FromUTF8( "Outline café" ); + + plotter.PlotText( VECTOR2I( 42000, 52000 ), COLOR4D( 0, 0, 0, 1 ), sample, attrs, + outlineFont, metrics ); + + plotter.EndPlot(); + + wxFFile file( pdfPath, "rb" ); + BOOST_REQUIRE( file.IsOpened() ); + wxFileOffset len = file.Length(); + std::string buffer; buffer.resize( (size_t) len ); file.Read( buffer.data(), len ); + BOOST_CHECK( buffer.rfind( "%PDF", 0 ) == 0 ); + + auto appendDecompressed = [&]() { + std::string aggregate = buffer; size_t pos=0; while(true){ size_t s=buffer.find("stream\n",pos); if(s==std::string::npos) break; size_t e=buffer.find("endstream",s); if(e==std::string::npos) break; size_t ds=s+7; size_t dl=e-ds; const unsigned char* data=reinterpret_cast(buffer.data()+ds); z_stream zs{}; zs.next_in=const_cast(data); zs.avail_in=(uInt)dl; if(inflateInit(&zs)==Z_OK){ std::string out; out.resize(dl*4+64); zs.next_out=reinterpret_cast(out.data()); zs.avail_out=(uInt)out.size(); int ret=inflate(&zs,Z_FINISH); if(ret==Z_STREAM_END){ out.resize(zs.total_out); aggregate+=out; } inflateEnd(&zs);} pos=e+9;} buffer.swap(aggregate); }; + appendDecompressed(); + + BOOST_CHECK_MESSAGE( buffer.find( "/CIDFontType2" ) != std::string::npos, + "Expected CIDFontType2 descendant font" ); + BOOST_CHECK_MESSAGE( buffer.find( "/FontFile2" ) != std::string::npos, + "Embedded outline font should include FontFile2 stream" ); + BOOST_CHECK_MESSAGE( buffer.find( "AAAAAA+Noto-Sans" ) != std::string::npos, + "BaseFont should reference Noto Sans subset" ); + BOOST_CHECK_MESSAGE( buffer.find( "00E9" ) != std::string::npos, + "ToUnicode map should include Latin character with accent" ); + BOOST_CHECK_MESSAGE( buffer.find( "/KiCadOutline" ) != std::string::npos, + "Outline font resource entry missing" ); + + // Optional: rasterize PDF to image (requires poppler 'pdftoppm'). + // We treat absence of the tool as a skipped sub-check rather than a failure. + { + wxString rasterBase = wxFileName::CreateTempFileName( wxT("kicad_pdf_raster") ); + wxString cmd = wxString::Format( wxT("pdftoppm -r 72 -singlefile -png \"%s\" \"%s\""), + pdfPath, rasterBase ); + + int ret = wxExecute( cmd, wxEXEC_SYNC ); + + if( ret == 0 ) + { + wxString pngPath = rasterBase + wxT(".png"); + + if( wxFileExists( pngPath ) ) + { + // Ensure PNG handler is available + if( !wxImage::FindHandler( wxBITMAP_TYPE_PNG ) ) + wxImage::AddHandler( new wxPNGHandler ); + + wxImage img( pngPath ); + BOOST_REQUIRE_MESSAGE( img.IsOk(), "Failed to load rasterized PDF image" ); + + long darkPixels = 0; + int w = img.GetWidth(); + int h = img.GetHeight(); + + for( int y = 0; y < h; ++y ) + { + for( int x = 0; x < w; ++x ) + { + unsigned char r = img.GetRed( x, y ); + unsigned char g = img.GetGreen( x, y ); + unsigned char b = img.GetBlue( x, y ); + + // Count any non-near-white pixel as drawn content + if( r < 240 || g < 240 || b < 240 ) + ++darkPixels; + } + } + + // Demand at least 100 non-white pixels to consider outline font rendered correctly. + // This threshold is lower than stroke font since outline fonts may render differently. + BOOST_CHECK_MESSAGE( darkPixels > 100, + "Rasterized PDF appears blank or too sparse (" << darkPixels + << " dark pixels). Outline font may not be rendering correctly." ); + + // Housekeeping + wxRemoveFile( pngPath ); + } + else + { + BOOST_TEST_MESSAGE( "pdftoppm succeeded but PNG output missing; skipping raster validation" ); + } + } + else + { + BOOST_TEST_MESSAGE( "pdftoppm not available or failed; skipping raster validation" ); + } + } + + wxString keepEnv; + + if( !wxGetEnv( wxT("KICAD_KEEP_TEST_PDF"), &keepEnv ) || keepEnv.IsEmpty() ) + wxRemoveFile( pdfPath ); +} + +BOOST_AUTO_TEST_SUITE_END()