qskinny/src/graphic/QskGlyphTable.cpp

490 lines
13 KiB
C++
Raw Normal View History

2025-02-06 12:21:48 +00:00
/******************************************************************************
* QSkinny - Copyright (C) The authors
* SPDX-License-Identifier: BSD-3-Clause
*****************************************************************************/
#include "QskGlyphTable.h"
#include "QskGraphic.h"
#include "QskInternalMacros.h"
#include <qrawfont.h>
#include <qpainter.h>
#include <qpainterpath.h>
#include <qendian.h>
QSK_QT_PRIVATE_BEGIN
#include <private/qrawfont_p.h>
QSK_QT_PRIVATE_END
typedef QHash< QString, uint > GlyphNameTable;
/*
The "parsers" below do no validation checks as the font has
already been validated by QRawFont
Hope QRawFont will extend its API some day ( the underlying
font libraries do support glyph names ) and we can remove the nasty
code below. see https://bugreports.qt.io/browse/QTBUG-132629
*/
namespace PostTableParser
{
// https://learn.microsoft.com/en-us/typography/opentype/spec/post
static inline QString toString( const uint8_t* s )
{
// Pascal string. Valid characters are: [AZ] [az] [09] '.' '_'
return QString::fromUtf8(
reinterpret_cast< const char* > ( s + 1 ), *s );
}
static GlyphNameTable glyphNames( const QByteArray& blob )
{
GlyphNameTable names;
const auto* glyphData = reinterpret_cast< const uint16_t* >( blob.constData() + 32 );
if ( const auto nglyphs = qFromBigEndian( *glyphData++ ) )
{
QVarLengthArray< const uint8_t* > strings;
strings.reserve( nglyphs );
const auto from = reinterpret_cast< const uint8_t* >( glyphData + nglyphs );
const auto to = reinterpret_cast< const uint8_t* >( blob.data() + blob.size() );
for ( auto s = from; s < to; s += *s + 1 )
strings += s;
for ( int i = 0; i < nglyphs; i++ )
{
const int idx = qFromBigEndian( glyphData[i] ) - 258;
if ( idx >= 0 )
names.insert( toString( strings[idx] ), i );
}
}
return names;
}
}
namespace CFFTableParser
{
// https://adobe-type-tools.github.io/font-tech-notes/pdfs/5176.CFF.pdf
static uint32_t offsetAt( const uint8_t* d, int size )
{
switch (size)
{
case 0:
return 0;
case 1:
return d[0];
case 2:
return ( d[0] << 8 ) | d[1];
case 3:
return ( d[0] << 16 ) | ( d[1] << 8 ) | d[2];
default:
return ( d[0] << 24 ) | ( d[1] << 16 ) | ( d[2] << 8 ) | d[3];
}
}
static int indexDataSize( const uint8_t* d )
{
const int nitems = ( d[0] << 8 ) | d[1];
if ( nitems == 0 )
return 2;
const int size = d[2];
const uint8_t* lx = d + 3 + nitems * size;
return lx + size - 1 + offsetAt( lx, size ) - d;
}
static const uint8_t* indexData( const uint8_t* d )
{
const int nitems = ( d[0] << 8 ) | d[1];
d += 2;
if ( nitems == 0 )
return d;
const int size = d[0];
const uint8_t* _contents = d + ( nitems + 1 ) * size;
return _contents + offsetAt(d + 1, size);
}
static const uint8_t* skipHeader( const uint8_t* d )
{
return d + d[2];
}
static const uint8_t* skipIndex( const uint8_t* d )
{
return d + indexDataSize( d );
}
static QVector< int > stringIds( const uint8_t* data, int nglyphs )
{
const int format = data[0];
QVector< int > _sids;
_sids += 0;
const uint8_t* p = data + 1;
switch( format )
{
case 0:
{
for (; _sids.size() < nglyphs; p += 2)
{
int sid = ( p[0] << 8 ) | p[1];
_sids += sid;
}
break;
}
case 1:
{
for (; _sids.size() < nglyphs; p += 3)
{
const int sid = ( p[0] << 8 ) | p[1];
const int n = p[2];
for ( int i = 0; i <= n; i++ )
_sids += sid + i;
}
break;
}
case 2:
{
for (; _sids.size() < nglyphs; p += 4)
{
const int sid = ( p[0] << 8 ) | p[1];
const int n = ( p[2] << 8 ) | p[3];
for ( int i = 0; i <= n; i++ )
_sids += sid + i;
}
break;
}
}
return _sids;
}
static int dictValue( const uint8_t* d, int op )
{
/*
see https://adobe-type-tools.github.io/font-tech-notes/pdfs/5176.CFF.pdf
We are intersted in the offset ( operator 15 ).
*/
const uint8_t* data = indexData( d );
int value = 0;
Q_FOREVER
{
// operand
const auto valueType = data[0];
if ( valueType == 28 )
{
value = ( data[1] << 8 ) | data[2];
data += 3;
}
else if ( valueType == 29 )
{
value = ( data[1] << 24 ) | ( data[2] << 16 )
| ( data[3] << 8 ) | data[4];
data += 5;
}
else if ( valueType == 30 )
{
/*
Assuming, that an offset is never a double
we skip the operand without reading it
*/
while( ++data )
{
const int b = *data;
if ( ( b & 0x0f ) == 0x0f || ( b & 0xf0 ) == 0xf0 )
break; // 0xf nibble indicating the end
}
data++;
}
else if ( valueType >= 31 && valueType <= 246 )
{
value = data[0] - 139;
data++;
}
else if ( valueType >= 247 && valueType <= 250 )
{
value = ( ( data[0] - 247 ) << 8 ) + data[1] + 108;
data += 2;
}
else if ( valueType >= 251 && valueType <= 254 )
{
value = -( ( data[0] - 251 ) << 8 ) - data[1] - 108;
data += 2;
}
// operator
if ( op == data[0] )
return value;
data += ( data[0] == 12 ) ? 2 : 1;
}
return 0; // default value
}
class StringTable
{
public:
StringTable( const uint8_t* data )
{
const int nitems = ( data[0] << 8 ) | data[1];
if ( nitems == 0 )
{
_contents = data + 2;
_offset = nullptr;
_offsize = 0;
}
else
{
_offsize = data[2];
_offset = data + 3;
_contents = _offset + nitems * _offsize + _offsize - 1;
}
}
inline QString operator[]( int which ) const
{
const auto x = _offset + which * _offsize;
const auto d1 = _contents + offsetAt(x, _offsize);
const auto d2 = _contents + offsetAt(x + _offsize, _offsize);
return QString::fromUtf8(
reinterpret_cast< const char* >( d1 ), d2 - d1 );
}
private:
const uint8_t* _contents = nullptr;
const uint8_t* _offset = nullptr;
int _offsize = -1;
};
static GlyphNameTable glyphNames( const QByteArray& blob, int glyphCount )
{
auto data = reinterpret_cast< const uint8_t* >( blob.constData() );
auto nameIndex = skipHeader( data );
auto dictIndex = skipIndex( nameIndex );
auto stringIndex = skipIndex( dictIndex );
auto charset = data + dictValue( dictIndex, 15 ); // 15: charset offset
const QVector< int > sids = stringIds( charset, glyphCount );
const StringTable table( stringIndex );
GlyphNameTable names;
for ( int i = 0; i < glyphCount; i++ )
{
/*
The first 391 SIDs are reserved for standard strings
( Appendix A ) that are not from the charset table.
*/
const auto idx = sids[i] - 391;
if ( idx >= 0 )
names.insert( table[idx], i );
}
return names;
}
}
static uint qskGlyphCount( const QRawFont& font )
{
/*
we could also read the count from the "maxp" table:
https://learn.microsoft.com/en-us/typography/opentype/spec/maxp
*/
const auto fontEngine = QRawFontPrivate::get( font )->fontEngine;
return fontEngine ? fontEngine->glyphCount() : 0;
}
static GlyphNameTable qskGlyphNameTable( const QRawFont& font )
{
const auto count = qskGlyphCount( font );
if ( count > 0 )
{
/*
The Compact Font Format ( CFF ) table has been introduced with
the OpenType specification. For not complying fonts the names
might be found in the Post table
*/
auto blob = font.fontTable( "CFF ");
if ( !blob.isEmpty() )
return CFFTableParser::glyphNames( blob, count );
blob = font.fontTable( "post" );
if ( !blob.isEmpty() )
return PostTableParser::glyphNames( blob );
}
return GlyphNameTable();
}
static inline quint32 qskGlyphIndex( const QRawFont& font, char32_t ucs4 )
{
if ( qskGlyphCount( font ) == 0 )
return 0;
const auto idxs = font.glyphIndexesForString(
QString::fromUcs4( &ucs4, 1 ) );
// fingers crossed: icon fonts will map code points to single glyphs only
Q_ASSERT( idxs.size() == 1 );
return idxs.size() == 1 ? idxs[0] : 0;
}
class QskGlyphTable::PrivateData
{
public:
inline const GlyphNameTable& glyphNameTable() const
{
if ( !validNames )
{
auto that = const_cast< PrivateData* >( this );
that->nameTable = qskGlyphNameTable( font );
that->validNames = true;
}
return nameTable;
}
QRawFont font;
GlyphNameTable nameTable;
bool validNames = false;
};
QskGlyphTable::QskGlyphTable()
: m_data( new PrivateData )
{
}
QskGlyphTable::QskGlyphTable( const QRawFont& font )
: QskGlyphTable()
{
m_data->font = font;
}
QskGlyphTable::QskGlyphTable( const QskGlyphTable& other )
: QskGlyphTable()
{
*m_data = *other.m_data;
}
QskGlyphTable::~QskGlyphTable()
{
}
QskGlyphTable& QskGlyphTable::operator=( const QskGlyphTable& other )
{
if ( m_data != other.m_data )
*m_data = *other.m_data;
return *this;
}
void QskGlyphTable::setIconFont( const QRawFont& font )
{
if ( font != m_data->font )
{
m_data->font = font;
m_data->nameTable.clear();
m_data->validNames = false;
}
}
QRawFont QskGlyphTable::iconFont() const
{
return m_data->font;
}
QPainterPath QskGlyphTable::glyphPath( uint glyphIndex ) const
{
QPainterPath path;
/*
Unfortunately QRawFont::pathForGlyph runs into failing checks
when being called from a different thread - f.e the scene graph thread.
So we need to bypass QRawFont and retrieve from its fontEngine.
*/
if ( auto fontEngine = QRawFontPrivate::get( m_data->font )->fontEngine )
{
QFixedPoint position;
quint32 idx = glyphIndex;
fontEngine->addGlyphsToPath( &idx, &position, 1, &path, {} );
}
return path;
}
QskGraphic QskGlyphTable::glyphGraphic( uint glyphIndex ) const
{
QskGraphic graphic;
if ( glyphIndex > 0 && qskGlyphCount( m_data->font ) > 0 )
{
const auto path = glyphPath( glyphIndex );
if ( !path.isEmpty() )
{
QPainter painter( &graphic );
painter.setRenderHint( QPainter::Antialiasing, true );
painter.fillPath( path, Qt::black );
}
}
return graphic;
}
uint QskGlyphTable::glyphCount() const
{
return qskGlyphCount( m_data->font );
}
uint QskGlyphTable::codeToIndex( char32_t ucs4 ) const
{
return qskGlyphIndex( m_data->font, ucs4 );
}
uint QskGlyphTable::nameToIndex( const QString& name ) const
{
/*
Names/Codes that do not correspond to any glyph in should
be mapped to glyph index 0.
see https://learn.microsoft.com/en-us/typography/opentype/spec/cmap
*/
return m_data->glyphNameTable().value( name.toLatin1(), 0 );
}
QHash< QString, uint > QskGlyphTable::nameTable() const
{
return m_data->glyphNameTable();
}