490 lines
13 KiB
C++
490 lines
13 KiB
C++
/******************************************************************************
|
||
* 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: [A–Z] [a–z] [0–9] '.' '_'
|
||
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();
|
||
}
|