Seth Hillbrand 77797103f7 Add ability to embed files in various elements
Schematics, symbols, boards and footprints all get the ability to store
files inside their file structures.  File lookups now have a
kicad-embed:// URI to allow various parts of KiCad to refer to files
stored in this manner.

kicad-embed://datasheet.pdf references the file named "datasheet.pdf"
embedded in the document.  Embeds are allowed in schematics, boards,
symbols and footprints.  Currently supported embeddings are Datasheets,
3D Models and drawingsheets

Fixes https://gitlab.com/kicad/code/kicad/-/issues/6918

Fixes https://gitlab.com/kicad/code/kicad/-/issues/2376

Fixes https://gitlab.com/kicad/code/kicad/-/issues/17827
2024-07-15 16:06:55 -07:00

683 lines
21 KiB
C++

/*
* This program source code file is part of KICAD, a free EDA CAD application.
*
* Copyright (C) 2021 Ola Rinta-Koski
* Copyright (C) 2021-2023 Kicad Developers, see AUTHORS.txt for contributors.
*
* Font abstract base class
*
* 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 <list>
#include <mutex>
#include <unordered_map>
#include <macros.h>
#include <string_utils.h>
#include <gal/graphics_abstraction_layer.h>
#include <font/stroke_font.h>
#include <font/outline_font.h>
#include <trigo.h>
#include <markup_parser.h>
// The "official" name of the Kicad stroke font (always existing)
#include <font/kicad_font_name.h>
#include <wx/tokenzr.h>
// markup_parser.h includes pegtl.hpp which includes windows.h... which leaks #define DrawText
#undef DrawText
using namespace KIFONT;
METRICS g_defaultMetrics;
const METRICS& METRICS::Default()
{
return g_defaultMetrics;
}
FONT* FONT::s_defaultFont = nullptr;
std::map< std::tuple<wxString, bool, bool>, FONT*> FONT::s_fontMap;
class MARKUP_CACHE
{
public:
struct ENTRY
{
std::string source;
std::unique_ptr<MARKUP::NODE> root;
};
typedef std::pair<wxString, ENTRY> CACHE_ENTRY;
MARKUP_CACHE( size_t aMaxSize ) :
m_maxSize( aMaxSize )
{
}
ENTRY& Put( const CACHE_ENTRY::first_type& aQuery, ENTRY&& aResult )
{
auto it = m_cache.find( aQuery );
m_cacheMru.emplace_front( CACHE_ENTRY( aQuery, std::move( aResult ) ) );
if( it != m_cache.end() )
{
m_cacheMru.erase( it->second );
m_cache.erase( it );
}
m_cache[aQuery] = m_cacheMru.begin();
if( m_cache.size() > m_maxSize )
{
auto last = m_cacheMru.end();
last--;
m_cache.erase( last->first );
m_cacheMru.pop_back();
}
return m_cacheMru.begin()->second;
}
ENTRY* Get( const CACHE_ENTRY::first_type& aQuery )
{
auto it = m_cache.find( aQuery );
if( it == m_cache.end() )
return nullptr;
m_cacheMru.splice( m_cacheMru.begin(), m_cacheMru, it->second );
return &m_cacheMru.begin()->second;
}
void Clear()
{
m_cacheMru.clear();
m_cache.clear();
}
private:
size_t m_maxSize;
std::list<CACHE_ENTRY> m_cacheMru;
std::unordered_map<wxString, std::list<CACHE_ENTRY>::iterator> m_cache;
};
static MARKUP_CACHE s_markupCache( 1024 );
static std::mutex s_markupCacheMutex;
FONT::FONT()
{
}
FONT* FONT::getDefaultFont()
{
if( !s_defaultFont )
s_defaultFont = STROKE_FONT::LoadFont( wxEmptyString );
return s_defaultFont;
}
FONT* FONT::GetFont( const wxString& aFontName, bool aBold, bool aItalic, const std::vector<wxString>* aEmbeddedFiles )
{
if( aFontName.empty() || aFontName.StartsWith( KICAD_FONT_NAME ) )
return getDefaultFont();
std::tuple<wxString, bool, bool> key = { aFontName, aBold, aItalic };
FONT* font = nullptr;
if( s_fontMap.find( key ) != s_fontMap.end() )
font = s_fontMap[key];
if( !font )
font = OUTLINE_FONT::LoadFont( aFontName, aBold, aItalic, aEmbeddedFiles );
if( !font )
font = getDefaultFont();
s_fontMap[key] = font;
return font;
}
bool FONT::IsStroke( const wxString& aFontName )
{
// This would need a more complex implementation if we ever support more stroke fonts
// than the KiCad Font.
return aFontName == _( "Default Font" ) || aFontName == KICAD_FONT_NAME;
}
void FONT::getLinePositions( const wxString& aText, const VECTOR2I& aPosition,
wxArrayString& aTextLines, std::vector<VECTOR2I>& aPositions,
std::vector<VECTOR2I>& aExtents, const TEXT_ATTRIBUTES& aAttrs,
const METRICS& aFontMetrics ) const
{
wxStringSplit( aText, aTextLines, '\n' );
int lineCount = aTextLines.Count();
aPositions.reserve( lineCount );
int interline = GetInterline( aAttrs.m_Size.y, aFontMetrics ) * aAttrs.m_LineSpacing;
int height = 0;
for( int i = 0; i < lineCount; i++ )
{
VECTOR2I pos( aPosition.x, aPosition.y + i * interline );
VECTOR2I end = boundingBoxSingleLine( nullptr, aTextLines[i], pos, aAttrs.m_Size,
aAttrs.m_Italic, aFontMetrics );
VECTOR2I bBox( end - pos );
aExtents.push_back( bBox );
if( i == 0 )
height += ( aAttrs.m_Size.y * 1.17 ); // 1.17 is a fudge to match 6.0 positioning
else
height += interline;
}
VECTOR2I offset( 0, 0 );
offset.y += aAttrs.m_Size.y;
if( IsStroke() )
{
// Fudge factors to match 6.0 positioning
offset.x += aAttrs.m_StrokeWidth / 1.52;
offset.y -= aAttrs.m_StrokeWidth * 0.052;
}
switch( aAttrs.m_Valign )
{
case GR_TEXT_V_ALIGN_TOP: break;
case GR_TEXT_V_ALIGN_CENTER: offset.y -= height / 2; break;
case GR_TEXT_V_ALIGN_BOTTOM: offset.y -= height; break;
case GR_TEXT_V_ALIGN_INDETERMINATE:
wxFAIL_MSG( wxT( "Indeterminate state legal only in dialogs." ) );
break;
}
for( int i = 0; i < lineCount; i++ )
{
VECTOR2I lineSize = aExtents.at( i );
VECTOR2I lineOffset( offset );
lineOffset.y += i * interline;
switch( aAttrs.m_Halign )
{
case GR_TEXT_H_ALIGN_LEFT: break;
case GR_TEXT_H_ALIGN_CENTER: lineOffset.x = -lineSize.x / 2; break;
case GR_TEXT_H_ALIGN_RIGHT: lineOffset.x = -( lineSize.x + offset.x ); break;
case GR_TEXT_H_ALIGN_INDETERMINATE:
wxFAIL_MSG( wxT( "Indeterminate state legal only in dialogs." ) );
break;
}
aPositions.push_back( aPosition + lineOffset );
}
}
/**
* Draw a string.
*
* @param aGal
* @param aText is the text to be drawn.
* @param aPosition is the text object position in world coordinates.
* @param aCursor is the current text position (for multiple text blocks within a single text
* object, such as a run of superscript characters)
* @param aAttrs are the styling attributes of the text, including its rotation
*/
void FONT::Draw( KIGFX::GAL* aGal, const wxString& aText, const VECTOR2I& aPosition,
const VECTOR2I& aCursor, const TEXT_ATTRIBUTES& aAttrs,
const METRICS& aFontMetrics ) const
{
if( !aGal || aText.empty() )
return;
VECTOR2I position( aPosition - aCursor );
// Split multiline strings into separate ones and draw them line by line
wxArrayString strings_list;
std::vector<VECTOR2I> positions;
std::vector<VECTOR2I> extents;
getLinePositions( aText, position, strings_list, positions, extents, aAttrs, aFontMetrics );
aGal->SetLineWidth( aAttrs.m_StrokeWidth );
for( size_t i = 0; i < strings_list.GetCount(); i++ )
{
drawSingleLineText( aGal, nullptr, strings_list[i], positions[i], aAttrs.m_Size,
aAttrs.m_Angle, aAttrs.m_Mirrored, aPosition, aAttrs.m_Italic,
aAttrs.m_Underlined, aFontMetrics );
}
}
/**
* @return position of cursor for drawing next substring
*/
VECTOR2I drawMarkup( BOX2I* aBoundingBox, std::vector<std::unique_ptr<GLYPH>>* aGlyphs,
const MARKUP::NODE* aNode, const VECTOR2I& aPosition,
const KIFONT::FONT* aFont, const VECTOR2I& aSize, const EDA_ANGLE& aAngle,
bool aMirror, const VECTOR2I& aOrigin, TEXT_STYLE_FLAGS aTextStyle,
const METRICS& aFontMetrics )
{
VECTOR2I nextPosition = aPosition;
bool drawUnderline = false;
bool drawOverbar = false;
if( aNode )
{
TEXT_STYLE_FLAGS textStyle = aTextStyle;
if( !aNode->is_root() )
{
if( aNode->isSubscript() )
textStyle |= TEXT_STYLE::SUBSCRIPT;
else if( aNode->isSuperscript() )
textStyle |= TEXT_STYLE::SUPERSCRIPT;
if( aNode->isOverbar() )
drawOverbar = true;
if( aNode->has_content() )
{
BOX2I bbox;
nextPosition = aFont->GetTextAsGlyphs( &bbox, aGlyphs, aNode->asWxString(), aSize,
nextPosition, aAngle, aMirror, aOrigin,
textStyle );
if( aBoundingBox )
aBoundingBox->Merge( bbox );
}
}
else if( aTextStyle & TEXT_STYLE::UNDERLINE )
{
drawUnderline = true;
}
for( const std::unique_ptr<MARKUP::NODE>& child : aNode->children )
{
nextPosition = drawMarkup( aBoundingBox, aGlyphs, child.get(), nextPosition, aFont,
aSize, aAngle, aMirror, aOrigin, textStyle, aFontMetrics );
}
}
if( drawUnderline )
{
// Shorten the bar a little so its rounded ends don't make it over-long
double barTrim = aSize.x * 0.1;
double barOffset = aFontMetrics.GetUnderlineVerticalPosition( aSize.y );
VECTOR2D barStart( aPosition.x + barTrim, aPosition.y - barOffset );
VECTOR2D barEnd( nextPosition.x - barTrim, nextPosition.y - barOffset );
if( aGlyphs )
{
STROKE_GLYPH barGlyph;
barGlyph.AddPoint( barStart );
barGlyph.AddPoint( barEnd );
barGlyph.Finalize();
aGlyphs->push_back( barGlyph.Transform( { 1.0, 1.0 }, { 0, 0 }, false, aAngle, aMirror,
aOrigin ) );
}
}
if( drawOverbar )
{
// Shorten the bar a little so its rounded ends don't make it over-long
double barTrim = aSize.x * 0.1;
double barOffset = aFontMetrics.GetOverbarVerticalPosition( aSize.y );
VECTOR2D barStart( aPosition.x + barTrim, aPosition.y - barOffset );
VECTOR2D barEnd( nextPosition.x - barTrim, nextPosition.y - barOffset );
if( aGlyphs )
{
STROKE_GLYPH barGlyph;
barGlyph.AddPoint( barStart );
barGlyph.AddPoint( barEnd );
barGlyph.Finalize();
aGlyphs->push_back( barGlyph.Transform( { 1.0, 1.0 }, { 0, 0 }, false, aAngle, aMirror,
aOrigin ) );
}
}
return nextPosition;
}
VECTOR2I FONT::drawMarkup( BOX2I* aBoundingBox, std::vector<std::unique_ptr<GLYPH>>* aGlyphs,
const wxString& aText, const VECTOR2I& aPosition, const VECTOR2I& aSize,
const EDA_ANGLE& aAngle, bool aMirror, const VECTOR2I& aOrigin,
TEXT_STYLE_FLAGS aTextStyle, const METRICS& aFontMetrics ) const
{
std::lock_guard<std::mutex> lock( s_markupCacheMutex );
MARKUP_CACHE::ENTRY* markup = s_markupCache.Get( aText );
if( !markup || !markup->root )
{
MARKUP_CACHE::ENTRY& cached = s_markupCache.Put( aText, {} );
cached.source = TO_UTF8( aText );
MARKUP::MARKUP_PARSER markupParser( &cached.source );
cached.root = markupParser.Parse();
markup = &cached;
}
wxASSERT( markup && markup->root );
return ::drawMarkup( aBoundingBox, aGlyphs, markup->root.get(), aPosition, this, aSize, aAngle,
aMirror, aOrigin, aTextStyle, aFontMetrics );
}
void FONT::drawSingleLineText( KIGFX::GAL* aGal, BOX2I* aBoundingBox, const wxString& aText,
const VECTOR2I& aPosition, const VECTOR2I& aSize,
const EDA_ANGLE& aAngle, bool aMirror, const VECTOR2I& aOrigin,
bool aItalic, bool aUnderline, const METRICS& aFontMetrics ) const
{
if( !aGal )
return;
TEXT_STYLE_FLAGS textStyle = 0;
if( aItalic )
textStyle |= TEXT_STYLE::ITALIC;
if( aUnderline )
textStyle |= TEXT_STYLE::UNDERLINE;
std::vector<std::unique_ptr<GLYPH>> glyphs;
(void) drawMarkup( aBoundingBox, &glyphs, aText, aPosition, aSize, aAngle, aMirror, aOrigin,
textStyle, aFontMetrics );
aGal->DrawGlyphs( glyphs );
}
VECTOR2I FONT::StringBoundaryLimits( const wxString& aText, const VECTOR2I& aSize, int aThickness,
bool aBold, bool aItalic, const METRICS& aFontMetrics ) const
{
// TODO do we need to parse every time - have we already parsed?
BOX2I boundingBox;
TEXT_STYLE_FLAGS textStyle = 0;
if( aBold )
textStyle |= TEXT_STYLE::BOLD;
if( aItalic )
textStyle |= TEXT_STYLE::ITALIC;
(void) drawMarkup( &boundingBox, nullptr, aText, VECTOR2I(), aSize, ANGLE_0, false, VECTOR2I(),
textStyle, aFontMetrics );
if( IsStroke() )
{
// Inflate by a bit more than thickness/2 to catch diacriticals, descenders, etc.
boundingBox.Inflate( KiROUND( aThickness * 1.5 ) );
}
else if( IsOutline() )
{
// Outline fonts have thickness built in, and *usually* stay within their ascent/descent
}
return boundingBox.GetSize();
}
VECTOR2I FONT::boundingBoxSingleLine( BOX2I* aBBox, const wxString& aText,
const VECTOR2I& aPosition, const VECTOR2I& aSize,
bool aItalic, const METRICS& aFontMetrics ) const
{
TEXT_STYLE_FLAGS textStyle = 0;
if( aItalic )
textStyle |= TEXT_STYLE::ITALIC;
VECTOR2I extents = drawMarkup( aBBox, nullptr, aText, aPosition, aSize, ANGLE_0, false,
VECTOR2I(), textStyle, aFontMetrics );
return extents;
}
/*
* Break marked-up text into "words". In this context, a "word" is EITHER a run of marked-up
* text (subscript, superscript or overbar), OR a run of non-marked-up text separated by spaces.
*/
void wordbreakMarkup( std::vector<std::pair<wxString, int>>* aWords,
const std::unique_ptr<MARKUP::NODE>& aNode, const KIFONT::FONT* aFont,
const VECTOR2I& aSize, TEXT_STYLE_FLAGS aTextStyle )
{
TEXT_STYLE_FLAGS textStyle = aTextStyle;
if( !aNode->is_root() )
{
wxChar escapeChar = 0;
if( aNode->isSubscript() )
{
escapeChar = '_';
textStyle = TEXT_STYLE::SUBSCRIPT;
}
else if( aNode->isSuperscript() )
{
escapeChar = '^';
textStyle = TEXT_STYLE::SUPERSCRIPT;
}
if( aNode->isOverbar() )
{
escapeChar = '~';
textStyle |= TEXT_STYLE::OVERBAR;
}
if( escapeChar )
{
wxString word = wxString::Format( wxT( "%c{" ), escapeChar );
int width = 0;
if( aNode->has_content() )
{
VECTOR2I next = aFont->GetTextAsGlyphs( nullptr, nullptr, aNode->asWxString(),
aSize, { 0, 0 }, ANGLE_0, false, { 0, 0 },
textStyle );
word += aNode->asWxString();
width += next.x;
}
std::vector<std::pair<wxString, int>> childWords;
for( const std::unique_ptr<MARKUP::NODE>& child : aNode->children )
wordbreakMarkup( &childWords, child, aFont, aSize, textStyle );
for( const std::pair<wxString, int>& childWord : childWords )
{
word += childWord.first;
width += childWord.second;
}
word += wxT( "}" );
aWords->emplace_back( std::make_pair( word, width ) );
return;
}
else
{
wxString textRun = aNode->asWxString();
wxStringTokenizer tokenizer( textRun, " ", wxTOKEN_RET_DELIMS );
std::vector<wxString> words;
while( tokenizer.HasMoreTokens() )
words.emplace_back( tokenizer.GetNextToken() );
for( const wxString& word : words )
{
wxString chars = word;
chars.Trim();
int w = aFont->GetTextAsGlyphs( nullptr, nullptr, chars, aSize, { 0, 0 },
ANGLE_0, false, { 0, 0 }, textStyle ).x;
aWords->emplace_back( std::make_pair( word, w ) );
}
}
}
for( const std::unique_ptr<MARKUP::NODE>& child : aNode->children )
wordbreakMarkup( aWords, child, aFont, aSize, textStyle );
}
void FONT::wordbreakMarkup( std::vector<std::pair<wxString, int>>* aWords, const wxString& aText,
const VECTOR2I& aSize, TEXT_STYLE_FLAGS aTextStyle ) const
{
MARKUP::MARKUP_PARSER markupParser( TO_UTF8( aText ) );
std::unique_ptr<MARKUP::NODE> root = markupParser.Parse();
::wordbreakMarkup( aWords, root, this, aSize, aTextStyle );
}
/*
* This is a highly simplified line-breaker. KiCad is an EDA tool, not a word processor.
*
* 1) It breaks only on spaces. If you type a word wider than the column width then you get
* overflow.
* 2) It treats runs of formatted text (superscript, subscript, overbar) as single words.
* 3) It does not perform justification.
*
* The results of the linebreaking are the addition of \n in the text. It is presumed that this
* function is called on m_shownText (or equivalent) rather than the original source text.
*/
void FONT::LinebreakText( wxString& aText, int aColumnWidth, const VECTOR2I& aSize, int aThickness,
bool aBold, bool aItalic ) const
{
TEXT_STYLE_FLAGS textStyle = 0;
if( aBold )
textStyle |= TEXT_STYLE::BOLD;
if( aItalic )
textStyle |= TEXT_STYLE::ITALIC;
int spaceWidth = GetTextAsGlyphs( nullptr, nullptr, wxS( " " ), aSize, VECTOR2I(), ANGLE_0,
false, VECTOR2I(), textStyle ).x;
wxArrayString textLines;
wxStringSplit( aText, textLines, '\n' );
aText = wxEmptyString;
for( size_t ii = 0; ii < textLines.Count(); ++ii )
{
std::vector<std::pair<wxString, int>> markup;
std::vector<std::pair<wxString, int>> words;
wordbreakMarkup( &markup, textLines[ii], aSize, textStyle );
for( const auto& [ run, runWidth ] : markup )
{
if( !words.empty() && !words.back().first.EndsWith( ' ' ) )
{
words.back().first += run;
words.back().second += runWidth;
}
else
{
words.emplace_back( std::make_pair( run, runWidth ) );
}
}
bool buryMode = false;
int lineWidth = 0;
wxString pendingSpaces;
for( const auto& [ word, wordWidth ] : words )
{
int pendingSpaceWidth = (int) pendingSpaces.Length() * spaceWidth;
bool overflow = lineWidth + pendingSpaceWidth + wordWidth > aColumnWidth - aThickness;
if( overflow && pendingSpaces.Length() > 0 )
{
aText += '\n';
lineWidth = 0;
pendingSpaces = wxEmptyString;
pendingSpaceWidth = 0;
buryMode = true;
}
if( word == wxS( " " ) )
{
pendingSpaces += word;
}
else
{
if( buryMode )
{
buryMode = false;
}
else
{
aText += pendingSpaces;
lineWidth += pendingSpaceWidth;
}
if( word.EndsWith( ' ' ) )
{
aText += word.Left( word.Length() - 1 );
pendingSpaces = wxS( " " );
}
else
{
aText += word;
pendingSpaces = wxEmptyString;
}
lineWidth += wordWidth;
}
}
// Add the newlines back onto the string
if( ii != ( textLines.Count() - 1 ) )
aText += '\n';
}
}