diff --git a/fonts/FiraSansOT-Medium_gpos.otf b/fonts/FiraSansOT-Medium_gpos.otf new file mode 100644 index 00000000..0a158367 Binary files /dev/null and b/fonts/FiraSansOT-Medium_gpos.otf differ diff --git a/src/opentype.js b/src/opentype.js index a364e871..993335af 100644 --- a/src/opentype.js +++ b/src/opentype.js @@ -179,7 +179,7 @@ function parseBuffer(buffer) { } if (gposOffset) { - gpos.parse(data, gposOffset, font); + font.tables.gpos = gpos.parse(data, gposOffset, font); } return font; diff --git a/src/parse.js b/src/parse.js index 7550be50..6997506f 100644 --- a/src/parse.js +++ b/src/parse.js @@ -104,6 +104,33 @@ Parser.prototype.parseByte = function() { return v; }; +Parser.prototype.hexdump = function(length, width) { + var length = length || 32; + var width = width || 8; + + function paddedHex(v, n){ + return (v + (1<<(4*n))).toString(16).substr(-n).toUpperCase(); + } + + var result = ""; + var values = []; + for (var i=0; i < length; i++){ + var v = this.data.getUint8(this.offset + this.relativeOffset + i); + values.push(paddedHex(v,2)); + if (i % width === width-1){ + var address = width * Math.floor(i/width); + result += paddedHex(address,4) + ":\t"; + result += values.join(' ') + "\n"; + values = [] + } + } + if (values.length){ + result += width * Math.floor(i/width) + ":\t"; + result += values.join('\t') + "\n"; + } + return result; +}; + Parser.prototype.parseChar = function() { var v = this.data.getInt8(this.offset + this.relativeOffset); this.relativeOffset += 1; diff --git a/src/table.js b/src/table.js index d47fea98..1712ef8a 100644 --- a/src/table.js +++ b/src/table.js @@ -31,11 +31,15 @@ Table.prototype.sizeOf = function() { var v = 0; for (var i = 0; i < this.fields.length; i += 1) { var field = this.fields[i]; + check.assert(field.name !== undefined, 'Field at index i='+i+' has undefined name and its value is "' + field.value + '". The field object is:'+ field + ' this.tableName='+this.tableName); + var value = this[field.name]; if (value === undefined) { value = field.value; } + check.assert(value !== undefined, 'field.name: "' + field.name + '" has undefined value!'); + if (typeof value.sizeOf === 'function') { v += value.sizeOf(); } else { diff --git a/src/tables/gpos.js b/src/tables/gpos.js index 61dddb84..38a11715 100644 --- a/src/tables/gpos.js +++ b/src/tables/gpos.js @@ -5,6 +5,7 @@ var check = require('../check'); var parse = require('../parse'); +var table = require('../table'); // Parse ScriptList and FeatureList tables of GPOS, GSUB, GDEF, BASE, JSTF tables. // These lists are unused by now, this function is just the basis for a real parsing. @@ -22,21 +23,29 @@ function parseTaggedListTable(data, start) { // Parse a coverage table in a GSUB, GPOS or GDEF table. // Format 1 is a simple list of glyph ids, // Format 2 is a list of ranges. It is expanded in a list of glyphs, maybe not the best idea. -function parseCoverageTable(data, start) { +function parseCoverageTable(data, start, subtable) { var p = new parse.Parser(data, start); - var format = p.parseUShort(); - var count = p.parseUShort(); - if (format === 1) { - return p.parseUShortList(count); + subtable.coveragetable = []; + subtable.coveragetable.format = p.parseUShort(); + subtable.coveragetable.count = p.parseUShort(); + if (subtable.coveragetable.format === 1) { + return p.parseUShortList(subtable.coveragetable.count); } - else if (format === 2) { + else if (subtable.coveragetable.format === 2) { + subtable.ranges = []; var coverage = []; + var count = subtable.coveragetable.count; for (; count--;) { - var begin = p.parseUShort(); - var end = p.parseUShort(); - var index = p.parseUShort(); - for (var i = begin; i <= end; i++) { - coverage[index++] = i; + var range = { + 'begin': p.parseUShort(), + 'end': p.parseUShort(), + 'index': p.parseUShort() + }; + subtable.ranges.push(range); + + var index = range.index; + for (var j = range.begin; j <= range.end; j++) { + coverage[index++] = j; } } @@ -92,54 +101,109 @@ function parseClassDefTable(data, start) { } // Parse a pair adjustment positioning subtable, format 1 or format 2 -// The subtable is returned in the form of a lookup function. function parsePairPosSubTable(data, start) { var p = new parse.Parser(data, start); + console.error("parsePairPosSubTable(data, start="+start+"): \n" + p.hexdump()); + + var subTable = {}; + var coverageOffset; + var value1; + var value2; + // This part is common to format 1 and format 2 subtables - var format = p.parseUShort(); - var coverageOffset = p.parseUShort(); - var coverage = parseCoverageTable(data, start + coverageOffset); + subTable.format = p.parseUShort(); + coverageOffset = p.parseUShort(); + subTable.valueFormat1 = p.parseUShort(); + subTable.valueFormat2 = p.parseUShort(); + + console.error("subTable.format: " + subTable.format); + console.error("coverageOffset: " + coverageOffset); + console.error("valueFormat1: " + subTable.valueFormat1); + console.error("valueFormat2: " + subTable.valueFormat2); + // valueFormat 4: XAdvance only, 1: XPlacement only, 0: no ValueRecord for second glyph // Only valueFormat1=4 and valueFormat2=0 is supported. - var valueFormat1 = p.parseUShort(); - var valueFormat2 = p.parseUShort(); - var value1; - var value2; - if (valueFormat1 !== 4 || valueFormat2 !== 0) return; - var sharedPairSets = {}; - if (format === 1) { + check.argument(subTable.valueFormat1 == 4 && subTable.valueFormat2 == 0, + 'GPOS table: Only valueFormat1 = 4 and valueFormat2 = 0 is supported.'); + + subTable.coverage = parseCoverageTable(data, start + coverageOffset, subTable); + + if (subTable.format == 1) { // Pair Positioning Adjustment: Format 1 + var pairSetCount = p.parseUShort(); - var pairSet = []; - // Array of offsets to PairSet tables-from beginning of PairPos subtable-ordered by Coverage Index - var pairSetOffsets = p.parseOffset16List(pairSetCount); + var pairSetOffsets = p.parseOffset16List(pairSetCount); // Array of offsets to PairSet tables-from beginning of PairPos subtable-ordered by Coverage Index + console.error("FORMAT==1: pairSetCount: " + pairSetCount); + + for (var i=0; i> hexdump:\n" + p.hexdump()); + + var lookupType = p.parseUShort() + , lookupFlag = p.parseUShort() + , useMarkFilteringSet = lookupFlag & 0x10 + , subTableCount = p.parseUShort() + , subTableOffsets = p.parseOffset16List(subTableCount) + , table = { + lookupType: lookupType, + lookupFlag: lookupFlag, + subtables: [], + markFilteringSet: useMarkFilteringSet ? p.parseUShort() : -1 } - // Return a function which finds the kerning values in the subtables. - table.getKerningValue = function(leftGlyph, rightGlyph) { - for (var i = subtables.length; i--;) { - var value = subtables[i](leftGlyph, rightGlyph); - if (value !== undefined) return value; + ; + + console.error("lookupType: ", lookupType); + console.error("lookupFlag: ", lookupFlag); + console.error("subTableCount: ", subTableCount); + + switch (lookupType){ + case LType.SINGLE_ADJUSTMENT: + console.error("FIX-ME: lookup type SINGLE_ADJUSTMENT is not implemented!"); + break; + + case LType.PAIR_ADJUSTMENT: + console.error("PAIR_ADJUSTMENT start:", start); + for (var i = 0; i < subTableCount; i++) { + console.error("parsed subTableOffsets[i]:", subTableOffsets[i]); + table.subtables.push(parsePairPosSubTable(data, start + subTableOffsets[i])); } + break; - return 0; - }; + case LType.CURSIVE_ADJUSTMENT: + console.error("FIX-ME: lookup type CURSIVE_ADJUSTMENT is not implemented!"); + break; + + case LType.MARK_TO_BASE_ATTACHMENT: + console.error("FIX-ME: lookup type MARK_TO_BASE_ATTACHMENT is not implemented!"); + break; + + case LType.MARK_TO_LIGATURE_ATTACHMENT: + console.error("FIX-ME: lookup type MARK_TO_LIGATURE_ATTACHMENT is not implemented!"); + break; + + case LType.MARK_TO_MARK_ATTACHMENT: + console.error("FIX-ME: lookup type MARK_TO_MARK_ATTACHMENT is not implemented!"); + break; + + case LType.CONTEXTUAL_POSITIONING: + console.error("FIX-ME: lookup type CONTEXTUAL_POSITIONING is not implemented!"); + break; + + case LType.CHAINED_CONTEXTUAL_POSITIONING: + console.error("FIX-ME: lookup type CHAINED_CONTEXTUAL_POSITIONING is not implemented!"); + break; + + case LType.EXTENSION_POSITIONING: + console.error("FIX-ME: lookup type EXTENSION_POSITIONING is not implemented!"); + break; + default: + console.error("Invalid Lookup Type!"); } + // Provide a function which finds the kerning values in the subtables. + table.getKerningValue = function(leftGlyph, rightGlyph) { + for (var i = table.subtables.length; i--;) { + var value = table.subtables[i].getValue(leftGlyph, rightGlyph); + if (value !== undefined) return value; + } + + return 0; + }; + return table; } @@ -216,24 +342,251 @@ function parseLookupTable(data, start) { // https://www.microsoft.com/typography/OTSPEC/gpos.htm function parseGposTable(data, start, font) { var p = new parse.Parser(data, start); - var tableVersion = p.parseFixed(); - check.argument(tableVersion === 1, 'Unsupported GPOS table version.'); + + console.error("**********************************************************\nparseGposTable"); +// console.error("hexdump:\n" + p.hexdump()); + + var gpos = {}; + gpos.tableVersion = p.parseFixed(); + check.argument(gpos.tableVersion === 1, 'Unsupported GPOS table version ('+gpos.tableVersion+').'); // ScriptList and FeatureList - ignored for now - parseTaggedListTable(data, start + p.parseUShort()); // 'kern' is the feature we are looking for. - parseTaggedListTable(data, start + p.parseUShort()); + gpos.scriptList = parseTaggedListTable(data, start + p.parseUShort()); + gpos.featureList = parseTaggedListTable(data, start + p.parseUShort()); // LookupList + gpos.lookupList = Array(); var lookupListOffset = p.parseUShort(); p.relativeOffset = lookupListOffset; var lookupCount = p.parseUShort(); + + console.error("lookupCount:", lookupCount); + var lookupTableOffsets = p.parseOffset16List(lookupCount); var lookupListAbsoluteOffset = start + lookupListOffset; + console.error("lookupListAbsoluteOffset: " + lookupListAbsoluteOffset); for (var i = 0; i < lookupCount; i++) { + console.error("lookupTableOffsets["+i+"]: " + lookupTableOffsets[i]); var table = parseLookupTable(data, lookupListAbsoluteOffset + lookupTableOffsets[i]); if (table.lookupType === 2 && !font.getGposKerningValue) font.getGposKerningValue = table.getKerningValue; + gpos.lookupList.push(table); + } + return gpos; +} + +function encodeCoverageTable(t, subtable, PREFIX){ + var size = 0; + t.fields.push({name: PREFIX+'_format', type: 'USHORT', value: subtable.coveragetable.format}); + t.fields.push({name: PREFIX+'_count', type: 'USHORT', value: subtable.coveragetable.count}); + size += 4; + + switch (subtable.format){ + case 1: + for (var i=0; i < subtable.coveragetable.count; i++){ + t.fields.push({name: PREFIX+"_coverage_"+i, type: 'USHORT', value: subtable.coverage[i]}); + size += 2; + } + break; + case 2: + check.argument(subtable.ranges.length === subtable.coveragetable.count, 'subtable.ranges.length=' + subtable.ranges.length + ' subtable.count=' + subtable.coveragetable.count); + var coverage = []; + for (var i=0; i < subtable.coveragetable.count; i++) { + var range = subtable.ranges[i]; + t.fields.push({name: PREFIX+"_rage_begin_"+i, type: 'USHORT', value: range.begin}); + t.fields.push({name: PREFIX+"_rage_end_"+i, type: 'USHORT', value: range.end}); + t.fields.push({name: PREFIX+"_rage_index_"+i, type: 'USHORT', value: range.index}); + size += 6; + } + break; + default: + console.error("wrong coverage subtable format (" + subtable.format + ")"); + } + + return size; +} + +function encodePairSet(t, subtable, i){ + var size = 0; + var pairset = subtable.pairsets[subtable.coverage[i]]; + if (!pairset) console.error("encodePairSet: pairset is undefined! i="+i); + var pairValueCount = pairset.valueRecords.length; + + t.push({name: "PairValueCount", type: 'USHORT', value: pairValueCount}); + size += 2; + for (var firstGlyph = 0; firstGlyph < pairValueCount; firstGlyph++) { + var value = pairset.valueRecords[firstGlyph]; + t.push({name: "value_secondGlyph", type: 'USHORT', value: value.secondGlyph}); + size += 2; + + if (subtable.valueFormat1) { t.push({name: "value_v1", type: 'USHORT', value: value.v1}); size += 2; } + if (subtable.valueFormat2) { t.push({name: "value_v2", type: 'USHORT', value: value.v2}); size += 2; } + } + + return size; +} + +function encodePairPosSubTable(t, subtable, i, prefix){ + var size = 0; + var start = t.sizeOf(); + var PREFIX = prefix + '_' + i; + + t.fields.push({name: PREFIX+'_format', type: 'USHORT', value: subtable.format}); + t.fields.push({name: PREFIX+'_coverageOffset', type: 'USHORT', value: 0}); + t.fields.push({name: PREFIX+'_valueFormat1', type: 'USHORT', value: subtable.valueFormat1}); + t.fields.push({name: PREFIX+'_valueFormat2', type: 'USHORT', value: subtable.valueFormat2}); + + switch (subtable.format){ + case 1: + console.error("subtable.pairsets.length: " + subtable.pairsets.length); + t.fields.push({name: PREFIX+'_pairSetCount', type: 'USHORT', value: subtable.pairsets.length}); + + var pairSetOffsets = []; + var pairSets = []; + + var offset = t.sizeOf(); + console.error("encodePairPosSubTable >>>>>>>>>>>>>>>>> subtable.coverage.length: " + subtable.coverage.length); + for (var j=0; j < subtable.coverage.length; j++){ + pairSetOffsets.push({name: PREFIX+'_offset_'+j, type: 'USHORT', value: offset}); + offset += encodePairSet(pairSets, subtable, j); + } + + t.fields = t.fields.concat(pairSetOffsets); + t.fields = t.fields.concat(pairSets); + break; + case 2: + console.error("Not yet implemented: encodePairPosSubTable format=2"); + break; + default: + console.error("Invalid subtable format: encodePairPosSubTable format=" + subtable.format); + } + + var ct = new table.Table("Coverage", []); + encodeCoverageTable(ct, subtable, PREFIX); + t[PREFIX+'_coverageOffset'] = t.sizeOf() - start; + t.fields = t.fields.concat(ct.fields); + + return t.sizeOf(); +} + +function encodeLookupEntry(t, gpos, i){ + console.error("== Encode Lookup Entry =="); + var start = t.sizeOf(); + var ltable = gpos.lookupList[i]; + var PREFIX = 'lookup_' + i; + var lookupEntry = new table.Table('LookupEntry', [ + {name: PREFIX+'_type', type: 'USHORT', value: ltable.lookupType} + , {name: PREFIX+'_flag', type: 'USHORT', value: ltable.lookupFlag} + ]); + var size = lookupEntry.sizeOf(); + + console.error("ltable.lookupType: " + ltable.lookupType); + console.error("ltable.lookupFlag: " + ltable.lookupFlag); + + var offsets = []; + var subtable_data = new table.Table('DATA', []); + + if (table.lookupFlag & 0x10){ + lookupEntry.push({name: PREFIX+'_markFilteringSet', type: 'USHORT', value: ltable.markFilteringSet}); + size += 2; + } + + switch (ltable.lookupType){ + case LType.SINGLE_ADJUSTMENT: + console.error("ERROR: Unimplemented Lookup Type: " + ltable.lookupType); + return 0; + + case LType.PAIR_ADJUSTMENT: //Pair adjustment + for (var j = 0; j < ltable.subtables.length; j++) { + offsets.push(8+size); //why 8 ?! This probably should be calculated from the previous table header size... + size += encodePairPosSubTable(subtable_data, ltable.subtables[j], j, PREFIX); + } + break; + + case LType.CURSIVE_ADJUSTMENT: + case LType.MARK_TO_BASE_ATTACHMENT: + case LType.MARK_TO_LIGATURE_ATTACHMENT: + case LType.MARK_TO_MARK_ATTACHMENT: + case LType.CONTEXTUAL_POSITIONING: + case LType.CHAINED_CONTEXTUAL_POSITIONING: + case LType.EXTENSION_POSITIONING: + console.error("ERROR: Unimplemented Lookup Type: " + ltable.lookupType); + return 0; + default: + console.error("Invalid Lookup Type: " + ltable.lookupType); + return 0; + } + + t.fields = t.fields.concat(lookupEntry.fields); + t.fields.push({name: PREFIX+'_subtable_count', type: 'USHORT', value: offsets.length}) + console.error("encoded subtable_count:" + offsets.length); + + for (var i=0; i