We rendered all Ascii characters.
Run
postScriptEditor(`
10 dict begin /rawurl 1 def /oversampling 2 def currentdict setpagedevice end
0 270 moveto (ABCDEFGHIJKLMN) showttf
0 220 moveto (OPQRSTUVWXYZ) showttf
0 170 moveto (abcdefghijklmn) showttf
0 120 moveto (opqrstuvwxyz) showttf
0 70 moveto (0123456789) showttf
0 20 moveto (.,;?!-+/()%) showttf
showpage
`);
But how did it render accented characters?
Run
postScriptEditor(`
10 dict begin /rawurl 1 def /oversampling 2 def currentdict setpagedevice end
0 270 moveto (Zürich) showttf
0 220 moveto (Genève) showttf
0 170 moveto (Noël) showttf
0 120 moveto (Città) showttf
0 70 moveto (Hétérogénéité) showttf
0 20 moveto (Bullerbü) showttf
showpage
`);
It seems to work because we used Computer Modern Unicode (CMU) fonts, we mapped to Latin-1 characterset. We use the file CMUSerif-Italic,ttf .
But how about the original Computer Modern fonts cmti10.ttf ?
With the following code snippets, you can switch between the two fonts
Run
font = new rpnTTF("site/files/CMUSerif-Italic.ttf")
Run
font = new rpnTTF("site/files/cmti10.ttf")
Switch the fonts and render again above.
Wow, the mapping is completely different. It does not even render the ascii characters right. We need to inspect the cmap.
Run
font = new rpnTTF("site/files/CMUSerif-Italic.ttf")
console.log(JSON.stringify(font.cmap))
font = new rpnTTF("site/files/cmmi10.ttf")
console.log(JSON.stringify(font.cmap))
We see that the old font does not use idDelta but only idRangeOffset. Did we fail to implement the offsets properly
unicodeOffset(unicode) { for(var i = 0; i < this.cmap.format.segCountX2 / 2 ; i++) { if (unicode >= this.cmap.format.startCode[i] && unicode <= this.cmap.format.endCode[i] ) { return unicode + this.cmap.format.idDelta[i]; } } return 0; //missing glyph }
Ha. We do not use idRangeOffset at all. It has to be applied together with idDelta.
We rewrite rpnTTF
Run
rpnTTF = class {
constructor(path) {
this.path = path;
const file = readSyncURL(path);
this.reader = new rpnBinary(file);
this.reader.getUint32() // scalarType
const numTables = this.reader.getUint16()
this.reader.getUint16() // searchRange
this.reader.getUint16() // entrySelector
this.reader.getUint16() // rangeShift
this.tables = {};
for (var i = 0; i < numTables; i++) {
const tag = this.reader.getString(4)
this.tables[tag] = {
checksum: this.reader.getUint32(),
offset: this.reader.getUint32(),
length: this.reader.getUint32(),
}
}
this.reader.setPosition(this.tables.head.offset);
this.head = {
majorVersion: this.reader.getUint16(),
minorVersion: this.reader.getUint16(),
fontRevision: this.reader.getFixed(),
checksumAdjustment: this.reader.getUint32(),
magicNumber: this.reader.getUint32(),
flags: this.reader.getUint16(),
unitsPerEm: this.reader.getUint16(),
created: this.reader.getDate(),
modified: this.reader.getDate(),
xMin: this.reader.getFWord(),
yMin: this.reader.getFWord(),
xMax: this.reader.getFWord(),
yMax: this.reader.getFWord(),
macStyle: this.reader.getUint16(),
lowestRecPPEM: this.reader.getUint16(),
fontDirectionHint: this.reader.getInt16(),
indexToLocFormat: this.reader.getInt16(),
glyphDataFormat: this.reader.getInt16()
};
this.reader.setPosition(this.tables.maxp.offset);
this.maxp = {
version: this.reader.getFixed(),
numGlyphs: this.reader.getUint16(),
maxPoints: this.reader.getUint16(),
maxContours: this.reader.getUint16(),
maxCompositePoints: this.reader.getUint16(),
maxCompositeContours: this.reader.getUint16(),
maxZones: this.reader.getUint16(),
maxTwilightPoints: this.reader.getUint16(),
maxStorage: this.reader.getUint16(),
maxFunctionDefs: this.reader.getUint16(),
maxInstructionDefs: this.reader.getUint16(),
maxStackElements: this.reader.getUint16(),
maxSizeOfInstructions: this.reader.getUint16(),
maxComponentElements: this.reader.getUint16(),
maxComponentDepth: this.reader.getUint16()
}
this.reader.setPosition(this.tables.cmap.offset);
this.cmap = {
version: this.reader.getUint16(),
numTables: this.reader.getUint16(),
encodingRecords: [],
glyphIndexMap: {},
}
if (this.cmap.version) throw "truetype error camp version not 0";
for (var i = 0; i < this.cmap.numTables; i++) {
this.cmap.encodingRecords.push({
platformID: this.reader.getUint16(),
encodingID: this.reader.getUint16(),
offset: this.reader.getOffset32(),
})
}
// we only support platform 0 and and encoding 3
var encodingsOffset = -1;
for (var i = 0; i < this.cmap.encodingRecords.length; i++) {
const { platformID, encodingID, offset } = this.cmap. encodingRecords[i];
if ( platformID == 0 && encodingID == 3 ) encodingsOffset = offset; // unicode
if ( platformID == 3 && encodingID == 1 ) encodingsOffset = offset; // windows
if (encodingsOffset > -1 ) break;
}
if (encodingsOffset == -1 ) throw "truetype unsupported encoding";
const formatstart = this.tables.cmap.offset + encodingsOffset
this.reader.setPosition(formatstart);
const format = this.reader.getUint16();
if (!(format == 4) ) throw "truetype unsupported format";
this.cmap.format = {
format: 4,
length: this.reader.getUint16(),
language: this.reader.getUint16(),
segCountX2: this.reader.getUint16(),
searchRange: this.reader.getUint16(),
entrySelector: this.reader.getUint16(),
rangeShift: this.reader.getUint16(),
endCode: [],
startCode: [],
idDelta: [],
idRangeOffset: [],
glyphIndexMap: {}, // This one is my addition, contains final unicode->index mapping
}
const segCount = this.cmap.format.segCountX2 / 2;
for (var i = 0; i < segCount; i++) {
this.cmap.format.endCode.push(this.reader.getUint16())
}
this.reader.getUint16() // Reserved pad.
for (var i = 0; i < segCount; i++) {
this.cmap.format.startCode.push(this.reader.getUint16())
}
for (var i = 0; i < segCount; i++) {
this.cmap.format.idDelta.push(this.reader.getInt16())
}
for (var i = 0; i < segCount; i++) {
this.cmap.format.idRangeOffset.push(this.reader.getUint16())
}
const remaining_bytes = formatstart + this.cmap.format.length - this.reader.getPosition();
this.cmap.format.glyphIdArray = [];
for (var i = 0; i < remaining_bytes / 2; i++) {
this.cmap.format.glyphIdArray.push(this.reader.getUint16());
}
this.reader.setPosition(this.tables.hhea.offset);
this.hhea = {
version: this.reader.getFixed(),
ascent: this.reader.getFWord(),
descent: this.reader.getFWord(),
lineGap: this.reader.getFWord(),
advanceWidthMax: this.reader.getUFWord(),
minLeftSideBearing: this.reader.getFWord(),
minRightSideBearing: this.reader.getFWord(),
xMaxExtent: this.reader.getFWord(),
caretSlopeRise: this.reader.getInt16(),
caretSlopeRun: this.reader.getInt16(),
caretOffset: this.reader.getFWord(),
reserved0: this.reader.getInt16(),
reserved1: this.reader.getInt16(),
reserved2: this.reader.getInt16(),
reserved3: this.reader.getInt16(),
metricDataFormat: this.reader.getInt16(),
numOfLongHorMetrics: this.reader.getInt16()
}
this.reader.setPosition(this.tables.hmtx.offset);
const hMetrics = [];
for (var i = 0; i < this.hhea.numOfLongHorMetrics; i++) {
hMetrics.push({
advanceWidth: this.reader.getUint16(),
leftSideBearing: this.reader.getInt16()
})
}
const leftSideBearing = [];
for (var i = 0; i < this.maxp.numGlyphs - this.hhea.numOfLongHorMetrics; i++) {
leftSideBearing.push(reader.getFWord())
}
this.hmtx = {
hMetrics,
leftSideBearing
}
this.reader.setPosition(this.tables.loca.offset);
const loca = [];
for (let i = 0; i < this.maxp.numGlyphs + 1; i++) {
if (this.head.indexToLocFormat)
loca.push(this.reader.getOffset32());
else
loca.push(this.reader.getOffset16());
}
this.glyphs = [];
for (var i = 0; i < loca.length - 1; i++) {
const multiplier = this.head.indexToLocFormat === 0 ? 2 : 1
const locaOffset = loca[i] * multiplier;
this.reader.setPosition(this.tables.glyf.offset + locaOffset)
const glyph = {
numberOfContours: this.reader.getInt16(),
xMin: this.reader.getInt16(),
yMin: this.reader.getInt16(),
xMax: this.reader.getInt16(),
yMax: this.reader.getInt16()
};
glyph.endPtsOfContours = []
for (var j = 0; j < glyph.numberOfContours; j++) {
glyph.endPtsOfContours.push(this.reader.getUint16());
}
glyph.instructionLength = this.reader.getUint16();
glyph.instructions = [];
for (var j = 0; j < glyph.instructionLength; j++) {
glyph.instructions.push(this.reader.getUint8());
}
const numPoints = Math.max(...glyph.endPtsOfContours)+1;
const flags = [];
glyph.points = [];
for (var j = 0; j < numPoints; j++) {
const flag = this.reader.getUint8();
flags.push(flag);
glyph.points.push( { x: 0, y: 0, onCurve: flag & 1 });
if (flag & 8) {
var repeatCount = this.reader.getUint8();
j += repeatCount;
while (repeatCount--) {
flags.push(flag);
glyph.points.push( { x: 0, y: 0, onCurve: flag & 1 });
}
}
}
if (glyph.points.length) glyph.points[0]["onCurve"] = 1;
function readCoords(name, byteFlag, deltaFlag, reader) {
var value = 0;
for (var i = 0; i < numPoints; i++) {
var flag = flags[i];
if (flag & byteFlag) {
if (flag & deltaFlag) {
value += reader.getUint8();
} else {
value -= reader.getUint8();
}
} else if (~flag & deltaFlag) {
value += reader.getInt16();
} else {
// value is unchanged.
}
glyph.points[i][name] = value;
}
}
readCoords("x", 2, 16, this.reader);
readCoords("y", 4, 32, this.reader);
this.glyphs.push(glyph);
}
}
unicodeOffset(unicode) {
for(var i = 0; i < this.cmap.format.segCountX2 / 2 ; i++) {
if (unicode >= this.cmap.format.startCode[i] && unicode <= this.cmap.format.endCode[i] ) {
if (this.cmap.format.idRangeOffset[i]) {
const idx = this.cmap.format.idRangeOffset[i]/2 + unicode - this.cmap.format.startCode[i] - this.cmap.format.idRangeOffset.length; // offset calculation starts at beginning of idRangeOffset, not glyphIdArray, so we must subtract 2 bytes per value
return this.cmap.format.glyphIdArray[idx] + this.cmap.format.idDelta[i] ;
} else {
return unicode + this.cmap.format.idDelta[i] ;
}
}
}
return 0; //missing glyph
}
glyphMetrics(g) {
}
}
font = new rpnTTF("site/files/cmti10.ttf")
We get the correct encoding values, but the accents are not rendered.
Run
postScriptEditor(`
10 dict begin /rawurl 1 def /oversampling 2 def currentdict setpagedevice end
0 270 moveto (Zürich) showttf
0 220 moveto (Genève) showttf
0 170 moveto (Noël) showttf
0 120 moveto (Città) showttf
0 70 moveto (Hétérogénéité) showttf
0 20 moveto (Bullerbü) showttf
showpage
`);
This is because the accents are composite glyphs. We did not handle them yet. We look again the documentationhttps://learn.microsoft.com/en-us/typography/opentype/spec/glyf
If the number of contours is greater than or equal to zero, this is a simple glyph. If negative, this is a composite glyph — the value -1 should be used for composite glyphs.
The composite glyph record has 4 components
flags uint16
glyphIndex uint16
argument1 (x-offset) uint8, int8, uint16 or int16
argument2 (y-yoffset) uint8, int8, uint16 or int16
transformdata optional
How long 3-5 are and if that is the last record, depends on the flags
bit 0 ARG_1_AND_2_ARE_WORDS: arguments are 2 bytes, else 1 byte
bit 1 ARGS_ARE_XY_VALUES: arguments are signed, else unsigned
bit 2 ROUND_XY_TO_GRID: rounding
bit 3 WE_HAVE_A_SCALE: scale present, else scale = 1.0
bit 4 not used
bit 5 MORE_COMPONENTS: this is not the last glyph
bit 6 WE_HAVE_AN_X_AND_Y_SCALE: xscale and yscale are different
bit 7 WE_HAVE_A_TWO_BY_TWO There is a 2 by 2 transformation that will be used to scale the component
bit 8 WE_HAVE_INSTRUCTIONS: there are instructions after the last component
bit 9 USE_MY_METRICS: use metric of this component (kerning)
bit 10 OVERLAP_COMPOUND: components overlap
bit 11 SCALED_COMPONENT_OFFSET: The composite is designed to have the component offset scaled. Ignored if ARGS_ARE_XY_VALUES is not set.
bit 12 UNSCALED_COMPONENT_OFFSET: The composite is designed not to have the component offset scaled. Ignored if ARGS_ARE_XY_VALUES is not set.
Sounds intimidating. Let's look what happens if we ignore the position and use only bit 5 and grab all components.
Run
rpnTTF = class {
constructor(path) {
this.path = path;
const file = readSyncURL(path);
this.reader = new rpnBinary(file);
this.reader.getUint32() // scalarType
const numTables = this.reader.getUint16()
this.reader.getUint16() // searchRange
this.reader.getUint16() // entrySelector
this.reader.getUint16() // rangeShift
this.tables = {};
for (var i = 0; i < numTables; i++) {
const tag = this.reader.getString(4)
this.tables[tag] = {
checksum: this.reader.getUint32(),
offset: this.reader.getUint32(),
length: this.reader.getUint32(),
}
}
this.reader.setPosition(this.tables.head.offset);
this.head = {
majorVersion: this.reader.getUint16(),
minorVersion: this.reader.getUint16(),
fontRevision: this.reader.getFixed(),
checksumAdjustment: this.reader.getUint32(),
magicNumber: this.reader.getUint32(),
flags: this.reader.getUint16(),
unitsPerEm: this.reader.getUint16(),
created: this.reader.getDate(),
modified: this.reader.getDate(),
xMin: this.reader.getFWord(),
yMin: this.reader.getFWord(),
xMax: this.reader.getFWord(),
yMax: this.reader.getFWord(),
macStyle: this.reader.getUint16(),
lowestRecPPEM: this.reader.getUint16(),
fontDirectionHint: this.reader.getInt16(),
indexToLocFormat: this.reader.getInt16(),
glyphDataFormat: this.reader.getInt16()
};
this.reader.setPosition(this.tables.maxp.offset);
this.maxp = {
version: this.reader.getFixed(),
numGlyphs: this.reader.getUint16(),
maxPoints: this.reader.getUint16(),
maxContours: this.reader.getUint16(),
maxCompositePoints: this.reader.getUint16(),
maxCompositeContours: this.reader.getUint16(),
maxZones: this.reader.getUint16(),
maxTwilightPoints: this.reader.getUint16(),
maxStorage: this.reader.getUint16(),
maxFunctionDefs: this.reader.getUint16(),
maxInstructionDefs: this.reader.getUint16(),
maxStackElements: this.reader.getUint16(),
maxSizeOfInstructions: this.reader.getUint16(),
maxComponentElements: this.reader.getUint16(),
maxComponentDepth: this.reader.getUint16()
}
this.reader.setPosition(this.tables.cmap.offset);
this.cmap = {
version: this.reader.getUint16(),
numTables: this.reader.getUint16(),
encodingRecords: [],
glyphIndexMap: {},
}
if (this.cmap.version) throw "truetype error camp version not 0";
for (var i = 0; i < this.cmap.numTables; i++) {
this.cmap.encodingRecords.push({
platformID: this.reader.getUint16(),
encodingID: this.reader.getUint16(),
offset: this.reader.getOffset32(),
})
}
// we only support platform 0 and and encoding 3
var encodingsOffset = -1;
for (var i = 0; i < this.cmap.encodingRecords.length; i++) {
const { platformID, encodingID, offset } = this.cmap. encodingRecords[i];
if ( platformID == 0 && encodingID == 3 ) encodingsOffset = offset; // unicode
if ( platformID == 3 && encodingID == 1 ) encodingsOffset = offset; // windows
if (encodingsOffset > -1 ) break;
}
if (encodingsOffset == -1 ) throw "truetype unsupported encoding";
const formatstart = this.tables.cmap.offset + encodingsOffset
this.reader.setPosition(formatstart);
const format = this.reader.getUint16();
if (!(format == 4) ) throw "truetype unsupported format";
this.cmap.format = {
format: 4,
length: this.reader.getUint16(),
language: this.reader.getUint16(),
segCountX2: this.reader.getUint16(),
searchRange: this.reader.getUint16(),
entrySelector: this.reader.getUint16(),
rangeShift: this.reader.getUint16(),
endCode: [],
startCode: [],
idDelta: [],
idRangeOffset: [],
glyphIndexMap: {}, // This one is my addition, contains final unicode->index mapping
}
const segCount = this.cmap.format.segCountX2 / 2;
for (var i = 0; i < segCount; i++) {
this.cmap.format.endCode.push(this.reader.getUint16())
}
this.reader.getUint16() // Reserved pad.
for (var i = 0; i < segCount; i++) {
this.cmap.format.startCode.push(this.reader.getUint16())
}
for (var i = 0; i < segCount; i++) {
this.cmap.format.idDelta.push(this.reader.getInt16())
}
for (var i = 0; i < segCount; i++) {
this.cmap.format.idRangeOffset.push(this.reader.getUint16())
}
const remaining_bytes = formatstart + this.cmap.format.length - this.reader.getPosition();
this.cmap.format.glyphIdArray = [];
for (var i = 0; i < remaining_bytes / 2; i++) {
this.cmap.format.glyphIdArray.push(this.reader.getUint16());
}
this.reader.setPosition(this.tables.hhea.offset);
this.hhea = {
version: this.reader.getFixed(),
ascent: this.reader.getFWord(),
descent: this.reader.getFWord(),
lineGap: this.reader.getFWord(),
advanceWidthMax: this.reader.getUFWord(),
minLeftSideBearing: this.reader.getFWord(),
minRightSideBearing: this.reader.getFWord(),
xMaxExtent: this.reader.getFWord(),
caretSlopeRise: this.reader.getInt16(),
caretSlopeRun: this.reader.getInt16(),
caretOffset: this.reader.getFWord(),
reserved0: this.reader.getInt16(),
reserved1: this.reader.getInt16(),
reserved2: this.reader.getInt16(),
reserved3: this.reader.getInt16(),
metricDataFormat: this.reader.getInt16(),
numOfLongHorMetrics: this.reader.getInt16()
}
this.reader.setPosition(this.tables.hmtx.offset);
const hMetrics = [];
for (var i = 0; i < this.hhea.numOfLongHorMetrics; i++) {
hMetrics.push({
advanceWidth: this.reader.getUint16(),
leftSideBearing: this.reader.getInt16()
})
}
const leftSideBearing = [];
for (var i = 0; i < this.maxp.numGlyphs - this.hhea.numOfLongHorMetrics; i++) {
leftSideBearing.push(reader.getFWord())
}
this.hmtx = {
hMetrics,
leftSideBearing
}
this.reader.setPosition(this.tables.loca.offset);
const loca = [];
for (let i = 0; i < this.maxp.numGlyphs + 1; i++) {
if (this.head.indexToLocFormat)
loca.push(this.reader.getOffset32());
else
loca.push(this.reader.getOffset16());
}
this.glyphs = [];
for (var i = 0; i < loca.length - 1; i++) {
const multiplier = this.head.indexToLocFormat === 0 ? 2 : 1
const locaOffset = loca[i] * multiplier;
this.reader.setPosition(this.tables.glyf.offset + locaOffset)
const glyph = {
numberOfContours: this.reader.getInt16(),
xMin: this.reader.getInt16(),
yMin: this.reader.getInt16(),
xMax: this.reader.getInt16(),
yMax: this.reader.getInt16()
};
if (glyph.numberOfContours >= 0) {
glyph.endPtsOfContours = []
for (var j = 0; j < glyph.numberOfContours; j++) {
glyph.endPtsOfContours.push(this.reader.getUint16());
}
glyph.instructionLength = this.reader.getUint16();
glyph.instructions = [];
for (var j = 0; j < glyph.instructionLength; j++) {
glyph.instructions.push(this.reader.getUint8());
}
const numPoints = Math.max(...glyph.endPtsOfContours)+1;
const flags = [];
glyph.points = [];
for (var j = 0; j < numPoints; j++) {
const flag = this.reader.getUint8();
flags.push(flag);
glyph.points.push( { x: 0, y: 0, onCurve: flag & 1 });
if (flag & 8) {
var repeatCount = this.reader.getUint8();
j += repeatCount;
while (repeatCount--) {
flags.push(flag);
glyph.points.push( { x: 0, y: 0, onCurve: flag & 1 });
}
}
}
if (glyph.points.length) glyph.points[0]["onCurve"] = 1;
function readCoords(name, byteFlag, deltaFlag, reader) {
var value = 0;
for (var i = 0; i < numPoints; i++) {
var flag = flags[i];
if (flag & byteFlag) {
if (flag & deltaFlag) {
value += reader.getUint8();
} else {
value -= reader.getUint8();
}
} else if (~flag & deltaFlag) {
value += reader.getInt16();
} else {
// value is unchanged.
}
glyph.points[i][name] = value;
}
}
readCoords("x", 2, 16, this.reader);
readCoords("y", 4, 32, this.reader);
} else {
glyph.components = [];
// read composite glyph
do {
const flag = reader.getUint16();
const index = reader.getUint16();
glyph.components.push(index);
const argument1 = (flags & 0x0001) ? reader.getUint16() : reader.getUint8();
const argument2 = (flags & 0x0001) ? reader.getUint16() : reader.getUint8();
var scale = 1.0;
var xscale = 1.0;
var yscale = 1.0;
if (flag & 0x0008) {
scale = reader.getF2Dot14();
} else if (flag & 0x0040) {
xscale = reader.getF2Dot14();
yscale = reader.getF2Dot14();
} else if (flag & 0x0080) {
xscale = reader.getF2Dot14();
reader.getF2Dot14();
reader.getF2Dot14();
yscale = reader.getF2Dot14();
}
} while (flags & 0x0020)
}
this.glyphs.push(glyph);
}
}
glyphIndex(unicode) {
for(var i = 0; i < this.cmap.format.segCountX2 / 2 ; i++) {
if (unicode >= this.cmap.format.startCode[i] && unicode <= this.cmap.format.endCode[i] ) {
if (this.cmap.format.idRangeOffset[i]) {
const idx = this.cmap.format.idRangeOffset[i]/2 + unicode - this.cmap.format.startCode[i] - this.cmap.format.idRangeOffset.length; // offset calculation starts at beginning of idRangeOffset, not glyphIdArray, so we must subtract 2 bytes per value
return this.cmap.format.glyphIdArray[idx] + this.cmap.format.idDelta[i] ;
} else {
return unicode + this.cmap.format.idDelta[i] ;
}
}
}
return 0; //missing glyph
}
glyphWidth(gi) {
return this.hmtx.hMetrics[gi].advanceWidth;
}
glyphPath(gi) {
const glyph = this.glyphs[gi];
var ps = "";
var points = [];
var endPtsOfContours = [];
if (glyph.points.length) {
points = glyph.points;
endPtsOfContours = glyph.endPtsOfContours;
} else if (glyph.components.length) {
for (var j = 0; j < glyph.components.length; j++) {
const cglyph = this.glyphs[glyph.components[j]];
points = points.concat(cglyph.points);
endPtsOfContours = endPtsOfContours.concat(cglyph.points);
}
}
if (points.length) {
var p = points[0];
ps += p.x + " " + p.y + " moveto ";
var bezier = [];
for (var j = 1; j < points.length; j++) {
p = points[j];
if (p.onCurve) {
if (bezier.length) {
ps += bezier[0] + " " + bezier[1] + " " + p.x + " " + p.y + " qcurveto ";
bezier = [];
} else
ps += p.x + " " + p.y + " lineto ";
} else {
if (bezier.length) {
ps += bezier[0] + " " + bezier[1] + " " + (bezier[0] + p.x)/2 + " " + (bezier[1] + p.y)/2 + " qcurveto ";
}
bezier = [p.x, p.y];
}
if (endPtsOfContours.includes(j)) {
ps += " closepath " ;
if (j < points.length-1) {
j++;
p = points[j];
ps += p.x + " " + p.y + " moveto ";
bezier = [];
}
}
}
}
return ps;
}
}
operators.showttf = function(context) {
const [s] = context.pop("string");
if (!context.graphics.current.length) {
context.stack.push(new rpnError("nocurrentpoint"));
return context;
}
if (!s) return context;
const scale = 60 / font.head.unitsPerEm;
var ps = " currentpoint currentpoint translate gsave " + scale + " " + scale + " scale ";
for(var i = 0; i < s.value.length; i++) {
const c = s.value.charCodeAt(i);
const gi = font.glyphIndex(c);
ps += font.glyphPath(gi);
ps += " fill ";
ps += font.glyphWidth(gi) + " 0 translate ";
}
ps += " grestore neg exch neg exch translate ";
context = rpn(ps, context);
return context;
}
font = new rpnTTF("site/files/cmti10.ttf")
Unfortunately it still does not work.
Run
postScriptEditor(`
10 dict begin /rawurl 1 def /oversampling 2 def currentdict setpagedevice end
0 270 moveto (Zürich) showttf
0 220 moveto (Genève) showttf
0 170 moveto (Noël) showttf
0 120 moveto (Città) showttf
0 70 moveto (Hétérogénéité) showttf
0 20 moveto (Bullerbü) showttf
showpage
`);
The original Computer Modern TrueType font does not have composing glyphs. There are glyphs for accents, but the font does not compose it. Unicode knows the concept of combining letters, but the accents are at position 0x300 ff, not where they are in the Computer Modern font. I am afraid we will not use the original fonts, but create light variations of the CMU fonts containing only Latin characters.
My Journey to PostScript