Looking closer to fonts, I found some oddity in CMUSerif-Roman which I couldn't explain. Some curves were cut. Apparently they were only cut at the left and not the right.
The problem was specific to the roman style, the italic style was fine.
Run
postScriptEditor(`
10 dict begin /height 200 def /canvas 1 def /raw 0 def currentdict setpagedevice end
/inch { 72 mul } def
/CMUSerif-Italic findfont 2 inch scalefont setfont
0 inch 1 inch moveto
(oOcCdgp) show
showpage
`);
I extracted the path from the o letter. On the left, the filled circle is the outer path of the o, the line is connecting all points of the TrueType file. The line seems correct. What is strange is that the circle is tangential to the line except in one point, the starting point, which creates the oddity.
Run
postScriptEditor(`
10 dict begin /height 200 def /canvas 1 def /raw 0 def currentdict setpagedevice end
/CMUSerif-Roman findfont 412 scalefont setfont
300 10 moveto
(o) charpath fill
0.2 0.2 scale 0 50 translate
0 setgray
57 244 moveto 57 633 lineto 189.5 775.5 lineto 322 918 lineto 512 918 lineto 698 918 lineto 831.5 776.5 lineto 965 635 lineto 965 438 lineto 965 245 lineto 830.5 111 lineto 696 -23 lineto 510 -23 lineto
328 -23 lineto 57 244 lineto
closepath stroke
0.7 setgray
57 244 moveto 57 633 189.5 775.5 qcurveto 322 918 512 918 qcurveto 698 918 831.5 776.5 qcurveto 965 635 965 438 qcurveto 965 245 830.5 111 qcurveto 696 -23 510 -23 qcurveto 328 -23 57 244 qcurveto closepath fill
`);
So we need to rewrite rpnTTF and try again. Refresh the page, run first this new code for rpnTTF and then run the PostScript codes.
Run
rpnTTF = class {
constructor(path) {
this.path = path;
this.error = "";
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 (let 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()
};
if (this.head.magicNumber != 0x5F0F3CF5) {
this.error = "invalid truetype magic";
return;
}
if (!this.tables.maxp) {
this.error = "truetype error maxp missing";
return;
}
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) {
this.error = "truetype error camp version not 0";
return;
}
for (let 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 (let 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 ) {
this.error = "truetype unsupported encoding";
return;
}
const formatstart = this.tables.cmap.offset + encodingsOffset;
this.reader.setPosition(formatstart);
const format = this.reader.getUint16();
if (format != 4 ) {
this.error = "truetype unsupported format";
return;
}
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 (let i = 0; i < segCount; i++) {
this.cmap.format.endCode.push(this.reader.getUint16());
}
this.reader.getUint16(); // Reserved pad.
for (let i = 0; i < segCount; i++) {
this.cmap.format.startCode.push(this.reader.getUint16());
}
for (let i = 0; i < segCount; i++) {
this.cmap.format.idDelta.push(this.reader.getInt16());
}
for (let 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 (let 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 (let i = 0; i < this.hhea.numOfLongHorMetrics; i++) {
hMetrics.push({
advanceWidth: this.reader.getUint16(),
leftSideBearing: this.reader.getInt16()
});
}
const leftSideBearing = [];
for (let i = 0; i < this.maxp.numGlyphs - this.hhea.numOfLongHorMetrics; i++) {
leftSideBearing.push(this.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 = [];
function readCoords(glyph, name, byteFlag, deltaFlag, numPoints, flags, reader) {
var value = 0;
for (let 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
}
}
for (let i = 0; i < loca.length - 1; i++) {
const length = loca[i+1] - loca[i];
if (!length) {
this.glyphs.push(null);
continue;
}
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 (let j = 0; j < glyph.numberOfContours; j++) {
glyph.endPtsOfContours.push(this.reader.getUint16());
}
glyph.instructionLength = this.reader.getUint16();
glyph.instructions = [];
for (let j = 0; j < glyph.instructionLength; j++) {
glyph.instructions.push(this.reader.getUint8());
}
const numPoints = Math.max(...glyph.endPtsOfContours)+1;
const flags = [];
glyph.points = [];
for (let 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; WTF
readCoords(glyph, "x", 2, 16, numPoints, flags, this.reader);
readCoords(glyph, "y", 4, 32, numPoints, flags, 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); /* while loop ends with ; */
}
}
glyphIndex(unicode) {
for (let 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) {
if (gi < this.hmtx.hMetrics.length) {
return this.hmtx.hMetrics[gi].advanceWidth;
} else {
return this.hmtx.hMetrics[0].advanceWidth; // monospaced font
}
}
glyphPath(gi) {
const glyph = this.glyphs[gi];
if (!glyph) return "";
var ps = "";
var points = [];
var endPtsOfContours = [];
if (glyph.points.length) {
points = glyph.points;
endPtsOfContours = glyph.endPtsOfContours;
} else if (glyph.components.length) {
for (let 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) {
/* Where quadratic points occur next to each other, an on-curve control point is interpolated between them. And there is another convention, that if a closed path starts with a quadratic point, the last point of the path is examined, and if it is quadratic, an on-curve point is interpolated between them, and the path is taken to start with that on-curve point; if the last point is not a quadratic control point, it is itself used for the start point.
https://stackoverflow.com/questions/3465809/how-to-interpret-a-freetype-glyph-outline-when-the-first-point-on-the-contour-is
*/
var p;
var bezier = [];
var start = true;
var last = 0;
var lastp;
var p0x;
var p0y;
for (let j = 0; j < points.length; j++) {
p = points[j];
if (start)
{
if (p.onCurve) {
p0x = p.x;
p0y = p.y;
} else {
// find last point
for (let k = 0; k < endPtsOfContours.length; k++) {
if (endPtsOfContours[k] > j) {
last = k;
break;
}
// should not happen
}
lastp = points[endPtsOfContours[last]];
if (lastp.onCurve) {
p0x = lastp.x;
p0x = lastp.y;
} else {
p0x = (p.x + lastp.x) / 2;
p0y = (p.y + lastp.y) / 2;
}
}
ps += p0x + " " + p0y + " moveto ";
start = false;
}
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)) {
if (bezier.length) {
ps += (bezier[0]) + " " + (bezier[1]) + " " + p0x + " " + p0y + " qcurveto ";
}
ps += " closepath " ;
start = true;
bezier = [];
}
}
}
return ps;
}
};