Browse Source

ADDED: Plot uses the KiCad font, embedded in PDF

Creates a Type-3 representation of our stroke font, embeds it in the PDF
and then causes text to be represented in that font.

This prevents the need for over-stroking and keeps our PDFs searchable
and accessible
master
Seth Hillbrand 2 months ago
parent
commit
753afc7f37
  1. 2
      common/CMakeLists.txt
  2. 25
      common/advanced_config.cpp
  3. 26
      common/font/stroke_font.cpp
  4. 833
      common/plotters/PDF_plotter.cpp
  5. 589
      common/plotters/pdf_outline_font.cpp
  6. 502
      common/plotters/pdf_stroke_font.cpp
  7. 1
      common/trace_helpers.cpp
  8. 48
      include/advanced_config.h
  9. 4
      include/font/outline_font.h
  10. 6
      include/font/stroke_font.h
  11. 199
      include/plotters/pdf_outline_font.h
  12. 172
      include/plotters/pdf_stroke_font.h
  13. 56
      include/plotters/plotters_pslike.h
  14. 7
      include/trace_helpers.h
  15. BIN
      qa/resources/fonts/NotoSans-Regular.ttf
  16. 2
      qa/tests/common/CMakeLists.txt
  17. 783
      qa/tests/common/test_pdf_unicode_plot.cpp

2
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

25
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<PARAM_CFG_BOOL>( true, AC_KEYS::DebugPDFWriter,
&m_DebugPDFWriter, m_DebugPDFWriter ) );
m_entries.push_back( std::make_unique<PARAM_CFG_DOUBLE>( true, AC_KEYS::PDFStrokeFontWidthFactor,
&m_PDFStrokeFontWidthFactor, m_PDFStrokeFontWidthFactor ) );
m_entries.push_back( std::make_unique<PARAM_CFG_DOUBLE>( true, AC_KEYS::PDFStrokeFontXOffset,
&m_PDFStrokeFontXOffset, m_PDFStrokeFontXOffset ) );
m_entries.push_back( std::make_unique<PARAM_CFG_DOUBLE>( true, AC_KEYS::PDFStrokeFontYOffset,
&m_PDFStrokeFontYOffset, m_PDFStrokeFontYOffset ) );
m_entries.push_back( std::make_unique<PARAM_CFG_DOUBLE>( true, AC_KEYS::PDFStrokeFontBoldMultiplier,
&m_PDFStrokeFontBoldMultiplier, m_PDFStrokeFontBoldMultiplier ) );
m_entries.push_back( std::make_unique<PARAM_CFG_DOUBLE>( true, AC_KEYS::PDFStrokeFontKerningFactor,
&m_PDFStrokeFontKerningFactor, m_PDFStrokeFontKerningFactor ) );
m_entries.push_back( std::make_unique<PARAM_CFG_BOOL>( true, AC_KEYS::UsePdfPrint,
&m_UsePdfPrint, m_UsePdfPrint ) );

26
common/font/stroke_font.cpp

@ -289,3 +289,29 @@ VECTOR2I STROKE_FONT::GetTextAsGlyphs( BOX2I* aBBox, std::vector<std::unique_ptr
return VECTOR2I( cursor.x, aPosition.y );
}
unsigned STROKE_FONT::GetGlyphCount() const
{
return m_glyphs ? m_glyphs->size() : 0;
}
const STROKE_GLYPH* STROKE_FONT::GetGlyph( unsigned aIndex ) const
{
if( !m_glyphs || aIndex >= m_glyphs->size() )
return nullptr;
return static_cast<const STROKE_GLYPH*>( 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 );
}

833
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 <algorithm>
#include <iterator>
#include <cstdio> // snprintf
#include <stack>
@ -44,14 +40,22 @@
#include <font/font.h>
#include <core/ignore.h>
#include <macros.h>
#include <trace_helpers.h>
#include <trigo.h>
#include <string_utils.h>
#include <markup_parser.h>
#include <fmt/format.h>
#include <fmt/chrono.h>
#include <fmt/ranges.h>
#include <plotters/pdf_stroke_font.h>
#include <plotters/pdf_outline_font.h>
#include <plotters/plotters_pslike.h>
#include <geometry/shape_rect.h>
#include <text_eval/text_eval_wrapper.h>
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<char>( byte ) );
}
else if( byte < 32 || byte > 126 )
{
fmt::format_to( std::back_inserter( result ), "\\{:03o}", byte );
}
else
{
result.push_back( static_cast<char>( 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<OUTLINE_NODE>();
if( !m_strokeFontManager )
m_strokeFontManager = std::make_unique<PDF_STROKE_FONT_MANAGER>();
else
m_strokeFontManager->Reset();
if( !m_outlineFontManager )
m_outlineFontManager = std::make_unique<PDF_OUTLINE_FONT_MANAGER>();
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<uint8_t>& 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<MARKUP::NODE> 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<OverbarInfo> 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<KIFONT::OUTLINE_FONT*>( aFont )->GetFace() ? static_cast<KIFONT::OUTLINE_FONT*>( 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<int>( 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<int>( 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<PDF_OUTLINE_FONT_RUN> outlineRuns;
if( m_outlineFontManager )
{
m_outlineFontManager->EncodeString( aWord, static_cast<KIFONT::OUTLINE_FONT*>( 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<KIFONT::OUTLINE_FONT*>( aFont )->IsFakeItalic();
bool fontIsFakeBold = aFont && aFont->IsOutline() && static_cast<KIFONT::OUTLINE_FONT*>( 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=<float degrees or tangent?>: 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<unsigned char>( ( glyph.cid >> 8 ) & 0xFF ),
static_cast<unsigned char>( 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<PDF_STROKE_FONT_RUN> 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<OverbarInfo>& 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<MARKUP::NODE>& 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<OverbarInfo>& 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();
}
}

589
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 <plotters/pdf_outline_font.h>
#include <trace_helpers.h>
#include <wx/log.h>
#include <cstdlib>
#include <algorithm>
#include <cctype>
#include <limits>
#include <cmath>
#include <fmt/format.h>
#include <harfbuzz/hb-ft.h>
#include <harfbuzz/hb.h>
#include <wx/ffile.h>
#include <core/utf8.h>
#include <ft2build.h>
#include FT_FREETYPE_H
#include <font/font.h>
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<uint32_t>( *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<double>( face->units_per_EM );
m_ascent = unitsToPdf( static_cast<double>( face->ascender ), m_unitsPerEm );
m_descent = unitsToPdf( static_cast<double>( face->descender ), m_unitsPerEm );
m_capHeight = unitsToPdf( static_cast<double>( face->bbox.yMax ), m_unitsPerEm );
m_bboxMinX = unitsToPdf( static_cast<double>( face->bbox.xMin ), m_unitsPerEm );
m_bboxMinY = unitsToPdf( static_cast<double>( face->bbox.yMin ), m_unitsPerEm );
m_bboxMaxX = unitsToPdf( static_cast<double>( face->bbox.xMax ), m_unitsPerEm );
m_bboxMaxY = unitsToPdf( static_cast<double>( 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<double>( 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<uint16_t>( aGlyphIndex );
m_cidToUnicode[cid] = aUnicode;
m_glyphMap.emplace( key, cid );
return cid;
}
const std::vector<uint8_t>& 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<size_t>( 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> <FFFF>\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<size_t>( 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<char>( ( gid >> 8 ) & 0xFF );
data[ cid * 2 + 1 ] = static_cast<char>( 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<char>( 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<PDF_OUTLINE_FONT_SUBSET>( 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<KIFONT::OUTLINE_FONT*>( aFont )->IsFakeItalic();
bool fakeBold = static_cast<KIFONT::OUTLINE_FONT*>( 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<PDF_OUTLINE_FONT_RUN>* 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<int>( textUtf8.size() ), 0,
static_cast<int>( 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<char>( ( cid >> 8 ) & 0xFF ) );
run.m_bytes.push_back( static_cast<char>( 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_SUBSET*> PDF_OUTLINE_FONT_MANAGER::AllSubsets() const
{
std::vector<PDF_OUTLINE_FONT_SUBSET*> result;
result.reserve( m_subsets.size() );
for( const auto& [ font, subset ] : m_subsets )
{
if( subset )
result.push_back( subset.get() );
}
return result;
}

502
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 <plotters/pdf_stroke_font.h>
#include <algorithm>
#include <cstdint>
#include <limits>
#include <fmt/format.h>
#include <font/glyph.h>
#include <advanced_config.h>
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<VECTOR2D>& 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<double>::max() ),
m_bboxMinY( std::numeric_limits<double>::max() ),
m_bboxMaxX( std::numeric_limits<double>::lowest() ),
m_bboxMaxY( std::numeric_limits<double>::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<int>( 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> <FF>\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<uint32_t>( 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<int>( aCode );
if( value < ' ' )
return static_cast<int>( '?' ) - ' ';
int index = value - ' ';
int count = static_cast<int>( m_font ? m_font->GetGlyphCount() : 0 );
if( index < 0 || index >= count )
return static_cast<int>( '?' ) - ' ';
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<PDF_STROKE_FONT_RUN>* 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<char>( 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<PDF_STROKE_FONT_SUBSET>& subset : group.subsets )
{
if( subset->Contains( aCode ) )
return subset.get();
}
for( const std::unique_ptr<PDF_STROKE_FONT_SUBSET>& subset : group.subsets )
{
if( subset->IsFull() )
continue;
if( subset->EnsureGlyph( aCode ) >= 0 )
return subset.get();
}
unsigned subsetIndex = m_nextSubsetIndex++;
auto newSubset = std::make_unique<PDF_STROKE_FONT_SUBSET>( 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_SUBSET*> PDF_STROKE_FONT_MANAGER::AllSubsets() const
{
std::vector<PDF_STROKE_FONT_SUBSET*> out;
for( const auto& [key, group] : m_styleGroups )
{
(void) key;
for( const auto& up : group.subsets )
out.push_back( up.get() );
}
return out;
}

1
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 )

48
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.
*

4
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;

6
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.

199
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 <cstdint>
#include <map>
#include <memory>
#include <string>
#include <vector>
#include <wx/string.h>
#include <font/outline_font.h>
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<PDF_OUTLINE_FONT_GLYPH> 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<double>& Widths() const { return m_widths; }
const std::vector<uint16_t>& CIDToGID() const { return m_cidToGid; }
const std::vector<std::u32string>& 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<uint8_t>& 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<double> m_widths;
std::vector<uint16_t> m_cidToGid;
std::vector<std::u32string> m_cidToUnicode;
std::map<GLYPH_KEY, uint16_t> 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<uint8_t> 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<PDF_OUTLINE_FONT_RUN>* aRuns );
std::vector<PDF_OUTLINE_FONT_SUBSET*> 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<SUBSET_KEY, std::unique_ptr<PDF_OUTLINE_FONT_SUBSET>> m_subsets;
unsigned m_nextSubsetIndex;
};

172
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 <map>
#include <memory>
#include <string>
#include <vector>
#include <wx/string.h>
#include <font/stroke_font.h>
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<GLYPH>& Glyphs() { return m_glyphs; }
const std::vector<GLYPH>& Glyphs() const { return m_glyphs; }
const std::vector<double>& 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<wxUniChar, int> m_unicodeToCode;
std::vector<GLYPH> m_glyphs;
std::vector<double> 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<PDF_STROKE_FONT_RUN>* 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<PDF_STROKE_FONT_SUBSET*> 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<std::unique_ptr<PDF_STROKE_FONT_SUBSET>> subsets; // may overflow 256 glyph limit
};
STYLE_GROUP& groupFor( bool aBold, bool aItalic );
private:
std::unique_ptr<KIFONT::STROKE_FONT> m_font;
double m_unitsPerEm;
unsigned m_nextSubsetIndex; // global counter for unique resource names
std::map<unsigned, STYLE_GROUP> m_styleGroups; // all style groups including default
};

56
include/plotters/plotters_pslike.h

@ -26,7 +26,11 @@
#pragma once
#include "plotter.h"
#include <memory>
#include <plotters/pdf_stroke_font.h>
#include <plotters/pdf_outline_font.h>
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<OverbarInfo>& aOverbars );
/**
* Draw overbar lines above text
*/
void drawOverbars( const std::vector<OverbarInfo>& 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<PDF_STROKE_FONT_MANAGER> m_strokeFontManager;
std::unique_ptr<PDF_OUTLINE_FONT_MANAGER> m_outlineFontManager;
};

7
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;
///@}
/**

BIN
qa/resources/fonts/NotoSans-Regular.ttf

2
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(

783
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 <boost/test/unit_test.hpp>
#include <wx/filename.h>
#include <wx/filefn.h>
#include <wx/ffile.h>
#include <wx/utils.h>
#include <wx/image.h>
#include <zlib.h>
#include <plotters/plotters_pslike.h>
#include <advanced_config.h>
#include <render_settings.h>
#include <trigo.h>
#include <font/font.h>
#include <font/stroke_font.h>
#include <qa_utils/wx_utils/unit_test_utils.h>
/* 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<const unsigned char*>(buffer.data()+ds); z_stream zs{}; zs.next_in=const_cast<Bytef*>(data); zs.avail_in=(uInt)dl; if(inflateInit(&zs)==Z_OK){ std::string out; out.resize(dl*4+64); zs.next_out=reinterpret_cast<Bytef*>(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<const unsigned char*>( buffer.data() + dataStart );
size_t dataLen = endPos - dataStart;
// Try zlib decompression
z_stream zs{};
zs.next_in = const_cast<Bytef*>( data );
zs.avail_in = static_cast<uInt>( dataLen );
if( inflateInit( &zs ) == Z_OK )
{
std::string out;
out.resize( dataLen * 4 + 64 );
zs.next_out = reinterpret_cast<Bytef*>( out.data() );
zs.avail_out = static_cast<uInt>( 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<const unsigned char*>( buffer.data() + dataStart );
z_stream zs{}; zs.next_in = const_cast<Bytef*>( data ); zs.avail_in = (uInt) dataLen;
if( inflateInit( &zs ) == Z_OK )
{
std::string out; out.resize( dataLen * 4 + 64 );
zs.next_out = reinterpret_cast<Bytef*>( 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<const unsigned char*>(buffer.data()+ds); z_stream zs{}; zs.next_in=const_cast<Bytef*>(data); zs.avail_in=(uInt)dl; if(inflateInit(&zs)==Z_OK){ std::string out; out.resize(dl*4+64); zs.next_out=reinterpret_cast<Bytef*>(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<const unsigned char*>( buffer.data() + ds );
z_stream zs{};
zs.next_in = const_cast<Bytef*>( data );
zs.avail_in = (uInt) dl;
if( inflateInit( &zs ) == Z_OK )
{
std::string out;
out.resize( dl * 4 + 64 );
zs.next_out = reinterpret_cast<Bytef*>( 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&>( 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<const unsigned char*>(buffer.data()+ds); z_stream zs{}; zs.next_in=const_cast<Bytef*>(data); zs.avail_in=(uInt)dl; if(inflateInit(&zs)==Z_OK){ std::string out; out.resize(dl*4+64); zs.next_out=reinterpret_cast<Bytef*>(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<wxString> 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<const unsigned char*>(buffer.data()+ds); z_stream zs{}; zs.next_in=const_cast<Bytef*>(data); zs.avail_in=(uInt)dl; if(inflateInit(&zs)==Z_OK){ std::string out; out.resize(dl*4+64); zs.next_out=reinterpret_cast<Bytef*>(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()
Loading…
Cancel
Save