Browse Source
ADDED: Plot uses the KiCad font, embedded in PDF
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 accessiblemaster
17 changed files with 3156 additions and 99 deletions
-
2common/CMakeLists.txt
-
25common/advanced_config.cpp
-
26common/font/stroke_font.cpp
-
833common/plotters/PDF_plotter.cpp
-
589common/plotters/pdf_outline_font.cpp
-
502common/plotters/pdf_stroke_font.cpp
-
1common/trace_helpers.cpp
-
48include/advanced_config.h
-
4include/font/outline_font.h
-
6include/font/stroke_font.h
-
199include/plotters/pdf_outline_font.h
-
172include/plotters/pdf_stroke_font.h
-
56include/plotters/plotters_pslike.h
-
7include/trace_helpers.h
-
BINqa/resources/fonts/NotoSans-Regular.ttf
-
2qa/tests/common/CMakeLists.txt
-
783qa/tests/common/test_pdf_unicode_plot.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; |
|||
} |
|||
@ -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; |
|||
} |
|||
|
|||
@ -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; |
|||
}; |
|||
@ -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 |
|||
}; |
|||
|
|||
@ -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() |
|||
Write
Preview
Loading…
Cancel
Save
Reference in new issue