View Issue Details
ID | Project | Category | View Status | Date Submitted | Last Update |
---|---|---|---|---|---|
0007910 | Scribus | Fonts | public | 2009-03-30 18:24 | 2013-04-11 13:58 |
Reporter | Mike | Assigned To | |||
Priority | normal | Severity | minor | Reproducibility | always |
Status | closed | Resolution | fixed | ||
Platform | Linux | OS | Ubuntu | OS Version | 8.10 |
Product Version | 1.3.5svn | ||||
Target Version | 1.5.0 | Fixed in Version | 1.5.0svn | ||
Summary | 0007910: add support for OpenType kerning tables | ||||
Description | add support for OpenType kerning tables | ||||
Steps To Reproduce | Download e.g. pagella font, write AV and To and use OpenType and Type 1. Type 1 has nice kerning whereas OpenType doesn't any. Pagella font: http://www.gust.org.pl/projects/e-foundry/tex-gyre/pagella | ||||
Additional Information | - TrueType works ok - Type 1 works ok I also tried some other OpenType fonts, but with the same result. | ||||
Tags | No tags attached. | ||||
Patch | |||||
2009-03-30 18:26
|
|
|
Pierre has a patch for this, however as it changes layout, it will go into 1.3.6. |
|
Just an acknowledge! Now I'm working on a GPL font. I tried to create kerning by classes. (Similar gliphs in a group.) I mentioned that no program can handle this feature except InDesign. Only "Individual Kerning Pair" is the winner in TTF and OTF, too. Take a look at the blog entry how each programs handle kerning in " AVAY" and "kö". http://my.opera.com/brtkr/blog/kern-class-error-in-linux Thanks. |
|
|
|
The problem results from the use of the FreeType2-library in Scribus. See the FreeType2 tutorial (http://freetype.sourceforge.net/freetype2/docs/tutorial/step2.html): "The API can only access kerning via the ‘kern’ table; OpenType kerning via the ‘GPOS’ table is not supported. You need a higher-level library like Pango or ICU to handle that." So, this obviously can't be fixed unless one of these libraries is being used in Scribus. (This would probably be a major change, but the problem is increasingly urgent since many contemporary fonts aren't available anymore unless in OpenType format. Would it be possible to change the "severity" of this issue to "major"??) |
|
For a long time now we have code that builds a pseudo kern table by analysing the content of the kern feature in OpenType fonts. So yes we use FT, but lack of kerning was critical enough that we had to find a workaround. |
|
Dear Pierre, thank you, but: I've tried on Windows (!) with Scribus 1.3.6, 1.3.7 and 1.3.8 – kerning definitely does not work there when using OpenType-fonts while it works well with TrueType- ans PS-fonts. So, sorry if I ask again: The "issue details" only talk about "Platform: Linux" and 1.5.0 as targeted version (while cbradney says there's a patch in version 1.3.6). => Is there a solution for the Windows-version, too? |
2011-02-16 09:41 viewer |
sc135-otf-kerning.diff (14,990 bytes)
Index: scribus/fonts/scface_ttf.cpp =================================================================== --- scribus/fonts/scface_ttf.cpp (revision 16352) +++ scribus/fonts/scface_ttf.cpp (working copy) @@ -8,6 +8,7 @@ #include <QFile> #include <QString> #include <QObject> +#include <QDebug> #include <sys/types.h> @@ -16,8 +17,365 @@ #include "util.h" #include "scconfig.h" +KernFeature::KernFeature ( FT_Face face ) + :m_valid ( true ) +{ + FontName = QString ( face->family_name ) + " " + QString ( face->style_name ) ; +// qDebug() <<"KF"<<FontName; +// QTime t; +// t.start(); + FT_ULong length = 0; + if ( !FT_Load_Sfnt_Table ( face, TTAG_GPOS , 0, NULL, &length ) ) + { +// qDebug() <<"\t"<<"GPOS table len"<<length; + if ( length > 32 ) + { + GPOSTableRaw.resize ( length ); + FT_Load_Sfnt_Table ( face, TTAG_GPOS, 0, reinterpret_cast<FT_Byte*> ( GPOSTableRaw.data() ), &length ); + makeCoverage(); + } + else + m_valid = false; + GPOSTableRaw.clear(); + coverages.clear(); + } + else + m_valid = false; + + if ( !m_valid ) + pairs.clear(); +// qDebug() <<"\t"<<m_valid; +// qDebug() <<"\t"<<t.elapsed(); +} + +KernFeature::KernFeature ( const KernFeature & kf ) +{ + m_valid = kf.m_valid; + if ( m_valid ) + pairs = kf.pairs; + +} + + +KernFeature::~ KernFeature() +{ +} + +double KernFeature::getPairValue ( unsigned int glyph1, unsigned int glyph2 ) const +{ + if ( m_valid ) + { + if ( pairs.contains ( glyph1 ) ) + { + if ( pairs[glyph1].contains ( glyph2 ) ) + { + return pairs[glyph1][glyph2]; + } + } + } + return 0.0; +} + +void KernFeature::makeCoverage() +{ + if ( GPOSTableRaw.isEmpty() ) + return; + + quint16 FeatureList_Offset= toUint16 ( 6 ); + quint16 LookupList_Offset = toUint16 ( 8 ); + + // Find the offsets of the kern feature tables + quint16 FeatureCount = toUint16 ( FeatureList_Offset );; + QList<quint16> FeatureKern_Offset; + for ( quint16 FeatureRecord ( 0 ); FeatureRecord < FeatureCount; ++ FeatureRecord ) + { + int rawIdx ( FeatureList_Offset + 2 + ( 6 * FeatureRecord ) ); + quint32 tag ( FT_MAKE_TAG ( GPOSTableRaw.at ( rawIdx ), + GPOSTableRaw.at ( rawIdx + 1 ), + GPOSTableRaw.at ( rawIdx + 2 ), + GPOSTableRaw.at ( rawIdx + 3 ) ) ); + if ( tag == TTAG_kern ) + { + FeatureKern_Offset << ( toUint16 ( rawIdx + 4 ) + FeatureList_Offset ); + + } + } + + // Extract indices of lookups for feture kern + QList<quint16> LookupListIndex; + foreach ( quint16 kern, FeatureKern_Offset ) + { + quint16 LookupCount ( toUint16 ( kern + 2 ) ); + for ( int llio ( 0 ) ; llio < LookupCount; ++llio ) + { + quint16 Idx ( toUint16 ( kern + 4 + ( llio * 2 ) ) ); + if ( !LookupListIndex.contains ( Idx ) ) + { + LookupListIndex <<Idx ; + } + } + } + + + // Extract offsets of lookup tables for feature kern + QList<quint16> LookupTables; + QList<quint16> PairAdjustmentSubTables; + for ( int i ( 0 ); i < LookupListIndex.count(); ++i ) + { + int rawIdx ( LookupList_Offset + 2 + ( LookupListIndex[i] * 2 ) ); + quint16 Lookup ( toUint16 ( rawIdx ) + LookupList_Offset ); + quint16 SubTableCount ( toUint16 ( Lookup + 4 ) ); + for ( int stIdx ( 0 ); stIdx < SubTableCount; ++ stIdx ) + { + quint16 SubTable ( toUint16 ( Lookup + 6 + ( 2 * stIdx ) ) + Lookup ); + +// quint16 PosFormat ( toUint16 ( SubTable ) ); + quint16 Coverage_Offset ( toUint16 ( SubTable + 2 ) + SubTable ); + quint16 CoverageFormat ( toUint16 ( Coverage_Offset ) ); + + if ( 1 == CoverageFormat ) // glyph indices based + { + quint16 GlyphCount ( toUint16 ( Coverage_Offset + 2 ) ); + quint16 GlyphID ( Coverage_Offset + 4 ); + + for ( unsigned int gl ( 0 ); gl < GlyphCount; ++gl ) + { + coverages[SubTable] << toUint16 ( GlyphID + ( gl * 2 ) ); + } + } + else if ( 2 == CoverageFormat ) // Coverage Format2 => ranges based + { + quint16 RangeCount ( toUint16 ( Coverage_Offset + 2 ) ); + +// int gl_base ( 0 ); + for ( int r ( 0 ); r < RangeCount; ++r ) + { + quint16 rBase ( Coverage_Offset + 4 + ( r * 6 ) ); + quint16 Start ( toUint16 ( rBase ) ); + quint16 End ( toUint16 ( rBase + 2 ) ); +// quint16 StartCoverageIndex ( toUint16 ( rBase + 4 ) ); + for ( unsigned int gl ( Start ); gl <= End; ++gl ) + coverages[SubTable] << gl; + } + } + else + { +// qDebug() <<"Unknow Coverage Format:"<<CoverageFormat; + continue; + } + + makePairs ( SubTable ); + } + + } + + +} + + +void KernFeature::makePairs ( quint16 subtableOffset ) +{ + /* + Lookup Type 2: + Pair Adjustment Positioning Subtable + */ + + quint16 PosFormat ( toUint16 ( subtableOffset ) ); + + if ( PosFormat == 1 ) + { + quint16 ValueFormat1 ( toUint16 ( subtableOffset +4 ) ); + quint16 ValueFormat2 ( toUint16 ( subtableOffset +6 ) ); + quint16 PairSetCount ( toUint16 ( subtableOffset +8 ) ); + if ( ValueFormat1 && ValueFormat2 ) + { + for ( int psIdx ( 0 ); psIdx < PairSetCount; ++ psIdx ) + { + unsigned int FirstGlyph ( coverages[subtableOffset][psIdx] ); + quint16 PairSetOffset ( toUint16 ( subtableOffset +10 + ( 2 * psIdx ) ) + subtableOffset ); + quint16 PairValueCount ( toUint16 ( PairSetOffset ) ); + quint16 PairValueRecord ( PairSetOffset + 2 ); + for ( int pvIdx ( 0 );pvIdx < PairValueCount; ++pvIdx ) + { + quint16 recordBase ( PairValueRecord + ( ( 2 + 2 + 2 ) * pvIdx ) ); + quint16 SecondGlyph ( toUint16 ( recordBase ) ); + qint16 Value1 ( toInt16 ( recordBase + 2 ) ); + pairs[FirstGlyph][SecondGlyph] = double ( Value1 ); + + } + + } + } + else if ( ValueFormat1 && ( !ValueFormat2 ) ) + { + for ( int psIdx ( 0 ); psIdx < PairSetCount; ++ psIdx ) + { + unsigned int FirstGlyph ( coverages[subtableOffset][psIdx] ); + quint16 PairSetOffset ( toUint16 ( subtableOffset +10 + ( 2 * psIdx ) ) + subtableOffset ); + quint16 PairValueCount ( toUint16 ( PairSetOffset ) ); + quint16 PairValueRecord ( PairSetOffset + 2 ); + for ( int pvIdx ( 0 );pvIdx < PairValueCount; ++pvIdx ) + { + quint16 recordBase ( PairValueRecord + ( ( 2 + 2 ) * pvIdx ) ); + quint16 SecondGlyph ( toUint16 ( recordBase ) ); + qint16 Value1 ( toInt16 ( recordBase + 2 ) ); + pairs[FirstGlyph][SecondGlyph] = double ( Value1 ); + + } + + } + } + else + { +// qDebug() <<"ValueFormat1 is null or both ValueFormat1 and ValueFormat2 are null"; + } + + } + else if ( PosFormat == 2 ) + { + quint16 ValueFormat1 ( toUint16 ( subtableOffset +4 ) ); + quint16 ValueFormat2 ( toUint16 ( subtableOffset +6 ) ); + quint16 ClassDef1 ( toUint16 ( subtableOffset +8 ) + subtableOffset ); + quint16 ClassDef2 ( toUint16 ( subtableOffset +10 ) + subtableOffset ); + quint16 Class1Count ( toUint16 ( subtableOffset +12 ) ); + quint16 Class2Count ( toUint16 ( subtableOffset +14 ) ); + quint16 Class1Record ( subtableOffset +16 ); + + // first extract classses + ClassDefTable Class1Data ( getClass ( ClassDef1 , subtableOffset ) ); + ClassDefTable Class2Data ( getClass ( ClassDef2 , subtableOffset ) ); + + if ( ValueFormat1 && ValueFormat2 ) + { + for ( quint16 C1 ( 0 );C1 < Class1Count; ++C1 ) + { + QList<quint16> Class1 ( Class1Data[C1] ); + quint16 Class2Record ( Class1Record + ( C1 * ( 2 * 2 * Class2Count ) ) ); + for ( quint16 C2 ( 0 );C2 < Class2Count; ++C2 ) + { + qint16 Value1 ( toInt16 ( Class2Record + ( C2 * ( 2 * 2 ) ) ) ); + QList<quint16> Class2 ( Class2Data[C2] ); + // keep it barbarian :D + foreach ( quint16 FirstGlyph, Class1 ) + { + foreach ( quint16 SecondGlyph, Class2 ) + { + if ( Value1 ) + pairs[FirstGlyph][SecondGlyph] = double ( Value1 ); + } + } + } + } + } + else if ( ValueFormat1 && ( !ValueFormat2 ) ) + { + for ( quint16 C1 ( 0 );C1 < Class1Count; ++C1 ) + { + QString cdbg ( QString::number ( C1 ).rightJustified ( 5,QChar ( 32 ) ) ); + QList<quint16> Class1 ( Class1Data[C1] ); + quint16 Class2Record ( Class1Record + ( C1 * ( 2 * Class2Count ) ) ); + for ( quint16 C2 ( 0 );C2 < Class2Count; ++C2 ) + { + qint16 Value1 ( toInt16 ( Class2Record + ( C2 * 2 ) ) ); + QList<quint16> Class2 ( Class2Data[C2] ); + + foreach ( quint16 FirstGlyph, Class1 ) + { + foreach ( quint16 SecondGlyph, Class2 ) + { + if ( Value1 ) + pairs[FirstGlyph][SecondGlyph] = double ( Value1 ); + } + } + } + } + } + else + { +// qDebug() <<"ValueFormat1 is null or both ValueFormat1 and ValueFormat2 are null"; + } + + } + else + qDebug() <<"unknown PosFormat"<<PosFormat; +} + +KernFeature::ClassDefTable KernFeature::getClass ( quint16 classDefOffset, quint16 coverageId ) +{ + ClassDefTable ret; + ret[0] = coverages[coverageId]; + quint16 ClassFormat ( toUint16 ( classDefOffset ) ); + if ( ClassFormat == 1 ) + { + quint16 StartGlyph ( toUint16 ( classDefOffset +2 ) ); + quint16 GlyphCount ( toUint16 ( classDefOffset +4 ) ); + quint16 ClassValueArray ( classDefOffset + 6 ); + + for ( quint16 CV ( 0 );CV < GlyphCount; ++CV ) + { + ret[0].removeAll ( StartGlyph + CV ); + ret[ toUint16 ( ClassValueArray + ( CV * 2 ) ) ] << StartGlyph + CV; + } + } + else if ( ClassFormat == 2 ) + { + quint16 ClassRangeCount ( toUint16 ( classDefOffset + 2 ) ); + quint16 ClassRangeRecord ( classDefOffset + 4 ); + for ( int CRR ( 0 ); CRR < ClassRangeCount; ++CRR ) + { + quint16 Start ( toUint16 ( ClassRangeRecord + ( CRR * 6 ) ) ); + quint16 End ( toUint16 ( ClassRangeRecord + ( CRR * 6 ) + 2 ) ); + quint16 Class ( toUint16 ( ClassRangeRecord + ( CRR * 6 ) + 4 ) ); + + for ( quint16 gl ( Start ); gl <= End; ++gl ) + { + ret[0].removeAll ( gl ); + ret[Class] << gl; + } + } + } + else + qDebug() <<"Unknown Class Table type"; + + return ret; +} + +quint16 KernFeature::toUint16 ( quint16 index ) +{ + if ( ( index + 2 ) > GPOSTableRaw.count() ) + { +// qDebug() << "HORROR!" << index << GPOSTableRaw.count() << FontName ; + // Rather no kerning at all than random kerning +// m_valid = false; + return 0; + } + // FIXME I just do not know how it has to be done *properly* + quint16 c1 ( GPOSTableRaw.at ( index ) ); + quint16 c2 ( GPOSTableRaw.at ( index + 1 ) ); + c1 &= 0xFF; + c2 &= 0xFF; + quint16 ret ( ( c1 << 8 ) | c2 ); + return ret; +} + +qint16 KernFeature::toInt16 ( quint16 index ) +{ + if ( ( index + 2 ) > GPOSTableRaw.count() ) + { + return 0; + } + // FIXME I just do not know how it has to be done *properly* + quint16 c1 ( GPOSTableRaw.at ( index ) ); + quint16 c2 ( GPOSTableRaw.at ( index + 1 ) ); + c1 &= 0xFF; + c2 &= 0xFF; + qint16 ret ( ( c1 << 8 ) | c2 ); + return ret; +} + + namespace { uint word(QByteArray const & bb, uint pos) { @@ -54,7 +412,47 @@ } } //namespace +ScFace_ttf::ScFace_ttf ( QString fam, QString sty, QString alt, QString scname, QString psname, QString path, int face ) + : FtFace ( fam, sty, alt, scname, psname, path, face ) +{ + formatCode = ScFace::SFNT; + kernFeature = 0; +} +ScFace_ttf::~ ScFace_ttf() +{ + if ( kernFeature ) + delete kernFeature; +} + + +void ScFace_ttf::load() const +{ + if ( !kernFeature ) + kernFeature = new KernFeature ( ftFace() ); + FtFace::load(); +} + +void ScFace_ttf::unload() const +{ + if ( kernFeature ) + delete kernFeature; + kernFeature = 0; + FtFace::unload(); +} + +qreal ScFace_ttf::glyphKerning ( uint gl1, uint gl2, qreal sz ) const +{ + if ( kernFeature->isValid() ) + { + return kernFeature->getPairValue ( gl1,gl2 ) / m_uniEM * sz; + + } + else + return FtFace::glyphKerning ( gl1, gl2, sz ); +} + + void ScFace_ttf::RawData(QByteArray & bb) const { if (formatCode == ScFace::TTCF) { QByteArray coll; Index: scribus/fonts/scface_ttf.h =================================================================== --- scribus/fonts/scface_ttf.h (revision 16352) +++ scribus/fonts/scface_ttf.h (working copy) @@ -10,6 +10,70 @@ #include "scribusapi.h" #include "fonts/ftface.h" + +#include FT_TRUETYPE_TABLES_H +#include FT_TRUETYPE_TAGS_H + + +/** + An object holding a table of kerning pairs extracted from + a kern feature such as found in a GPOS table + */ +class SCRIBUS_API KernFeature +{ + public: + /** + * Build a ready-to-use kerning pairs table + * @param face a valid FT_Face, It won’t be store by KernFeature + */ + KernFeature ( FT_Face face ); + KernFeature ( const KernFeature& kf ); + ~KernFeature(); + + /** + * Get the kerning value for a pair of glyph indexes. + * @param glyph1 Index of the left glyph in logical order + * @param glyph2 Index of the right glyph in logical order + * @return the unscaled delta to apply to xadvance of the first glyph + */ + double getPairValue ( unsigned int glyph1, unsigned int glyph2 ) const; + + /** + * The table can have been invalidated if something went wrong at any moment. + * @return True if valid, False otherwise. + */ + bool isValid() const {return m_valid;} + + private: + bool m_valid; + QByteArray GPOSTableRaw; + QMap<quint16,QList<quint16> > coverages; + QMap<quint16, QMap<quint16, double> > pairs; + + void makeCoverage(); + void makePairs ( quint16 subtableOffset ); + + typedef QMap<quint16, QList<quint16> > ClassDefTable; // <Class , list<GLyph> > + ClassDefTable getClass ( quint16 classDefOffset, quint16 coverageId ); + inline quint16 toUint16 ( quint16 index ); + inline qint16 toInt16 ( quint16 index ); + + enum ValueFormat + { + XPlacement = 0x0001, + YPlacement = 0x0002, + XAdvance = 0x0004, + YAdvance = 0x0008, + XPlaDevice =0x0010, + YPlaDevice =0x0020, + XAdvDevice =0x0040, + YAdvDevice =0x0080 + }; + QString FontName;// for debugging purpose +}; + + + /* Class ScFace_ttf Subclass of ScFace, specifically for TrueType fonts. @@ -19,13 +83,19 @@ class SCRIBUS_API ScFace_ttf : public FtFace { public: - ScFace_ttf(QString fam, QString sty, QString alt, QString scname, QString psname, QString path, int face) - : FtFace(fam, sty, alt, scname, psname, path, face ) - { - formatCode = ScFace::SFNT; - } + ScFace_ttf ( QString fam, QString sty, QString alt, QString scname, QString psname, QString path, int face ); + ~ScFace_ttf(); + + void load () const; + void unload () const; + bool EmbedFont(QString &str) const; void RawData(QByteArray & bb) const; + + qreal glyphKerning ( uint gl1, uint gl2, qreal sz ) const; + +private: + mutable KernFeature * kernFeature; }; #endif Index: scribus/fonts/ftface.h =================================================================== --- scribus/fonts/ftface.h (revision 16352) +++ scribus/fonts/ftface.h (working copy) @@ -81,7 +81,7 @@ uint char2CMap(QChar ch) const; - qreal glyphKerning (uint gl1, uint gl2, qreal sz) const; + virtual qreal glyphKerning (uint gl1, uint gl2, qreal sz) const; // GlyphMetrics glyphBBox (uint gl, qreal sz) const; void RawData (QByteArray & bb) const; |
|
R16365 has the patch for 1.4.0.svn |
|
Resolving and closing this issue. OTF kerning support has been working for quite some time in trunk. |
Date Modified | Username | Field | Change |
---|---|---|---|
2009-03-30 18:24 | Mike | New Issue | |
2009-03-30 18:26 | Mike | File Added: OpenType_kerning.png | |
2009-03-30 20:25 | cbradney | Status | new => assigned |
2009-03-30 20:25 | cbradney | Assigned To | => pierremarchand |
2009-03-30 20:25 | cbradney | Note Added: 0021445 | |
2009-03-30 20:26 | cbradney | Target Version | => 1.3.6 |
2010-04-15 20:16 | timpul | Note Added: 0023702 | |
2010-07-08 16:37 | pierremarchand | File Added: kern150.png | |
2010-09-29 13:34 | ale | Relationship added | has duplicate 0009383 |
2010-12-04 09:43 |
|
Note Added: 0024978 | |
2010-12-04 09:51 |
|
Note Edited: 0024978 | |
2010-12-04 10:40 | pierremarchand | Note Added: 0024979 | |
2010-12-04 13:18 |
|
Note Added: 0024980 | |
2011-02-16 09:41 |
|
File Added: sc135-otf-kerning.diff | |
2011-02-16 09:53 |
|
Note Added: 0025603 | |
2013-04-11 13:58 | jghali | Note Added: 0030071 | |
2013-04-11 13:58 | jghali | Status | assigned => resolved |
2013-04-11 13:58 | jghali | Fixed in Version | => 1.5.0svn |
2013-04-11 13:58 | jghali | Resolution | open => fixed |
2013-04-11 13:58 | jghali | Status | resolved => closed |
2013-04-11 13:58 | jghali | Assigned To | pierremarchand => |