View Issue Details

IDProjectCategoryView StatusLast Update
0007910ScribusFontspublic2013-04-11 13:58
ReporterMike Assigned To 
PrioritynormalSeverityminorReproducibilityalways
Status closedResolutionfixed 
PlatformLinuxOSUbuntuOS Version8.10
Product Version1.3.5svn 
Target Version1.5.0Fixed in Version1.5.0svn 
Summary0007910: add support for OpenType kerning tables
Descriptionadd support for OpenType kerning tables
Steps To ReproduceDownload 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.
TagsNo tags attached.
Patch

Relationships

has duplicate 0009383 closedale GPOS kern table not used in Scribus (kerning pairs for Latin+diacritics) 

Activities

2009-03-30 18:26

 

OpenType_kerning.png (29,560 bytes)   
OpenType_kerning.png (29,560 bytes)   

cbradney

2009-03-30 20:25

administrator   ~0021445

Pierre has a patch for this, however as it changes layout, it will go into 1.3.6.

timpul

2010-04-15 20:16

reporter   ~0023702

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.

pierremarchand

2010-07-08 16:37

reporter  

kern150.png (57,278 bytes)   
kern150.png (57,278 bytes)   

user3518

2010-12-04 09:43

  ~0024978

Last edited: 2010-12-04 09:51

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"??)

pierremarchand

2010-12-04 10:40

reporter   ~0024979

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.

user3518

2010-12-04 13:18

  ~0024980

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?

plinnell

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;
sc135-otf-kerning.diff (14,990 bytes)   

plinnell

2011-02-16 09:53

viewer   ~0025603

R16365 has the patch for 1.4.0.svn

jghali

2013-04-11 13:58

administrator   ~0030071

Resolving and closing this issue. OTF kerning support has been working for quite some time in trunk.

Issue History

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 user3518 Note Added: 0024978
2010-12-04 09:51 user3518 Note Edited: 0024978
2010-12-04 10:40 pierremarchand Note Added: 0024979
2010-12-04 13:18 user3518 Note Added: 0024980
2011-02-16 09:41 plinnell File Added: sc135-otf-kerning.diff
2011-02-16 09:53 plinnell 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 =>