/* Copyright 2021 Mozilla Foundation * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ const ON_CURVE_POINT = 1 << 0; const X_SHORT_VECTOR = 1 << 1; const Y_SHORT_VECTOR = 1 << 2; const REPEAT_FLAG = 1 << 3; const X_IS_SAME_OR_POSITIVE_X_SHORT_VECTOR = 1 << 4; const Y_IS_SAME_OR_POSITIVE_Y_SHORT_VECTOR = 1 << 5; const OVERLAP_SIMPLE = 1 << 6; const ARG_1_AND_2_ARE_WORDS = 1 << 0; const ARGS_ARE_XY_VALUES = 1 << 1; // const ROUND_XY_TO_GRID = 1 << 2; const WE_HAVE_A_SCALE = 1 << 3; const MORE_COMPONENTS = 1 << 5; const WE_HAVE_AN_X_AND_Y_SCALE = 1 << 6; const WE_HAVE_A_TWO_BY_TWO = 1 << 7; const WE_HAVE_INSTRUCTIONS = 1 << 8; // const USE_MY_METRICS = 1 << 9; // const OVERLAP_COMPOUND = 1 << 10; // const SCALED_COMPONENT_OFFSET = 1 << 11; // const UNSCALED_COMPONENT_OFFSET = 1 << 12; /** * GlyfTable object represents a glyf table containing glyph information: * - glyph header (xMin, yMin, xMax, yMax); * - contours if any; * - components if the glyph is a composite. * * It's possible to re-scale each glyph in order to have a new font which * exactly fits an other one: the goal is to be able to build some substitution * font for well-known fonts (Myriad, Arial, ...). * * A full description of glyf table can be found here * https://developer.apple.com/fonts/TrueType-Reference-Manual/RM06/Chap6glyf.html */ class GlyfTable { constructor({ glyfTable, isGlyphLocationsLong, locaTable, numGlyphs }) { this.glyphs = []; const loca = new DataView( locaTable.buffer, locaTable.byteOffset, locaTable.byteLength ); const glyf = new DataView( glyfTable.buffer, glyfTable.byteOffset, glyfTable.byteLength ); const offsetSize = isGlyphLocationsLong ? 4 : 2; let prev = isGlyphLocationsLong ? loca.getUint32(0) : 2 * loca.getUint16(0); let pos = 0; for (let i = 0; i < numGlyphs; i++) { pos += offsetSize; const next = isGlyphLocationsLong ? loca.getUint32(pos) : 2 * loca.getUint16(pos); if (next === prev) { this.glyphs.push(new Glyph({})); continue; } const glyph = Glyph.parse(prev, glyf); this.glyphs.push(glyph); prev = next; } } getSize() { return this.glyphs.reduce((a, g) => { const size = g.getSize(); // Round to next multiple of 4 if needed. return a + ((size + 3) & ~3); }, 0); } write() { const totalSize = this.getSize(); const glyfTable = new DataView(new ArrayBuffer(totalSize)); const isLocationLong = totalSize > /* 0xffff * 2 */ 0x1fffe; const offsetSize = isLocationLong ? 4 : 2; const locaTable = new DataView( new ArrayBuffer((this.glyphs.length + 1) * offsetSize) ); if (isLocationLong) { locaTable.setUint32(0, 0); } else { locaTable.setUint16(0, 0); } let pos = 0; let locaIndex = 0; for (const glyph of this.glyphs) { pos += glyph.write(pos, glyfTable); // Round to next multiple of 4 if needed. pos = (pos + 3) & ~3; locaIndex += offsetSize; if (isLocationLong) { locaTable.setUint32(locaIndex, pos); } else { locaTable.setUint16(locaIndex, pos >> 1); } } return { isLocationLong, loca: new Uint8Array(locaTable.buffer), glyf: new Uint8Array(glyfTable.buffer), }; } scale(factors) { for (let i = 0, ii = this.glyphs.length; i < ii; i++) { this.glyphs[i].scale(factors[i]); } } } class Glyph { constructor({ header = null, simple = null, composites = null }) { this.header = header; this.simple = simple; this.composites = composites; } static parse(pos, glyf) { const [read, header] = GlyphHeader.parse(pos, glyf); pos += read; if (header.numberOfContours < 0) { // Composite glyph. const composites = []; while (true) { const [n, composite] = CompositeGlyph.parse(pos, glyf); pos += n; composites.push(composite); if (!(composite.flags & MORE_COMPONENTS)) { break; } } return new Glyph({ header, composites }); } const simple = SimpleGlyph.parse(pos, glyf, header.numberOfContours); return new Glyph({ header, simple }); } getSize() { if (!this.header) { return 0; } const size = this.simple ? this.simple.getSize() : this.composites.reduce((a, c) => a + c.getSize(), 0); return this.header.getSize() + size; } write(pos, buf) { if (!this.header) { return 0; } const spos = pos; pos += this.header.write(pos, buf); if (this.simple) { pos += this.simple.write(pos, buf); } else { for (const composite of this.composites) { pos += composite.write(pos, buf); } } return pos - spos; } scale(factor) { if (!this.header) { return; } const xMiddle = (this.header.xMin + this.header.xMax) / 2; this.header.scale(xMiddle, factor); if (this.simple) { this.simple.scale(xMiddle, factor); } else { for (const composite of this.composites) { composite.scale(xMiddle, factor); } } } } class GlyphHeader { constructor({ numberOfContours, xMin, yMin, xMax, yMax }) { this.numberOfContours = numberOfContours; this.xMin = xMin; this.yMin = yMin; this.xMax = xMax; this.yMax = yMax; } static parse(pos, glyf) { return [ 10, new GlyphHeader({ numberOfContours: glyf.getInt16(pos), xMin: glyf.getInt16(pos + 2), yMin: glyf.getInt16(pos + 4), xMax: glyf.getInt16(pos + 6), yMax: glyf.getInt16(pos + 8), }), ]; } getSize() { return 10; } write(pos, buf) { buf.setInt16(pos, this.numberOfContours); buf.setInt16(pos + 2, this.xMin); buf.setInt16(pos + 4, this.yMin); buf.setInt16(pos + 6, this.xMax); buf.setInt16(pos + 8, this.yMax); return 10; } scale(x, factor) { this.xMin = Math.round(x + (this.xMin - x) * factor); this.xMax = Math.round(x + (this.xMax - x) * factor); } } class Contour { constructor({ flags, xCoordinates, yCoordinates }) { this.xCoordinates = xCoordinates; this.yCoordinates = yCoordinates; this.flags = flags; } } class SimpleGlyph { constructor({ contours, instructions }) { this.contours = contours; this.instructions = instructions; } static parse(pos, glyf, numberOfContours) { const endPtsOfContours = []; for (let i = 0; i < numberOfContours; i++) { const endPt = glyf.getUint16(pos); pos += 2; endPtsOfContours.push(endPt); } const numberOfPt = endPtsOfContours[numberOfContours - 1] + 1; const instructionLength = glyf.getUint16(pos); pos += 2; const instructions = new Uint8Array(glyf).slice( pos, pos + instructionLength ); pos += instructionLength; const flags = []; for (let i = 0; i < numberOfPt; pos++, i++) { let flag = glyf.getUint8(pos); flags.push(flag); if (flag & REPEAT_FLAG) { const count = glyf.getUint8(++pos); flag ^= REPEAT_FLAG; for (let m = 0; m < count; m++) { flags.push(flag); } i += count; } } const allXCoordinates = []; let xCoordinates = []; let yCoordinates = []; let pointFlags = []; const contours = []; let endPtsOfContoursIndex = 0; let lastCoordinate = 0; // Get x coordinates. for (let i = 0; i < numberOfPt; i++) { const flag = flags[i]; if (flag & X_SHORT_VECTOR) { // 8-bits unsigned value. const x = glyf.getUint8(pos++); lastCoordinate += flag & X_IS_SAME_OR_POSITIVE_X_SHORT_VECTOR ? x : -x; xCoordinates.push(lastCoordinate); } else if (flag & X_IS_SAME_OR_POSITIVE_X_SHORT_VECTOR) { // IS_SAME. xCoordinates.push(lastCoordinate); } else { lastCoordinate += glyf.getInt16(pos); pos += 2; xCoordinates.push(lastCoordinate); } if (endPtsOfContours[endPtsOfContoursIndex] === i) { // Next entry is the first one of a new contour. endPtsOfContoursIndex++; allXCoordinates.push(xCoordinates); xCoordinates = []; } } lastCoordinate = 0; endPtsOfContoursIndex = 0; for (let i = 0; i < numberOfPt; i++) { const flag = flags[i]; if (flag & Y_SHORT_VECTOR) { // 8-bits unsigned value. const y = glyf.getUint8(pos++); lastCoordinate += flag & Y_IS_SAME_OR_POSITIVE_Y_SHORT_VECTOR ? y : -y; yCoordinates.push(lastCoordinate); } else if (flag & Y_IS_SAME_OR_POSITIVE_Y_SHORT_VECTOR) { // IS_SAME. yCoordinates.push(lastCoordinate); } else { lastCoordinate += glyf.getInt16(pos); pos += 2; yCoordinates.push(lastCoordinate); } pointFlags.push((flag & ON_CURVE_POINT) | (flag & OVERLAP_SIMPLE)); if (endPtsOfContours[endPtsOfContoursIndex] === i) { // Next entry is the first one of a new contour. xCoordinates = allXCoordinates[endPtsOfContoursIndex]; endPtsOfContoursIndex++; contours.push( new Contour({ flags: pointFlags, xCoordinates, yCoordinates, }) ); yCoordinates = []; pointFlags = []; } } return new SimpleGlyph({ contours, instructions, }); } getSize() { let size = this.contours.length * 2 + 2 + this.instructions.length; let lastX = 0; let lastY = 0; for (const contour of this.contours) { size += contour.flags.length; for (let i = 0, ii = contour.xCoordinates.length; i < ii; i++) { const x = contour.xCoordinates[i]; const y = contour.yCoordinates[i]; let abs = Math.abs(x - lastX); if (abs > 255) { size += 2; } else if (abs > 0) { size += 1; } lastX = x; abs = Math.abs(y - lastY); if (abs > 255) { size += 2; } else if (abs > 0) { size += 1; } lastY = y; } } return size; } write(pos, buf) { const spos = pos; const xCoordinates = []; const yCoordinates = []; const flags = []; let lastX = 0; let lastY = 0; for (const contour of this.contours) { for (let i = 0, ii = contour.xCoordinates.length; i < ii; i++) { let flag = contour.flags[i]; const x = contour.xCoordinates[i]; let delta = x - lastX; if (delta === 0) { flag |= X_IS_SAME_OR_POSITIVE_X_SHORT_VECTOR; xCoordinates.push(0); } else { const abs = Math.abs(delta); if (abs <= 255) { flag |= delta >= 0 ? X_SHORT_VECTOR | X_IS_SAME_OR_POSITIVE_X_SHORT_VECTOR : X_SHORT_VECTOR; xCoordinates.push(abs); } else { xCoordinates.push(delta); } } lastX = x; const y = contour.yCoordinates[i]; delta = y - lastY; if (delta === 0) { flag |= Y_IS_SAME_OR_POSITIVE_Y_SHORT_VECTOR; yCoordinates.push(0); } else { const abs = Math.abs(delta); if (abs <= 255) { flag |= delta >= 0 ? Y_SHORT_VECTOR | Y_IS_SAME_OR_POSITIVE_Y_SHORT_VECTOR : Y_SHORT_VECTOR; yCoordinates.push(abs); } else { yCoordinates.push(delta); } } lastY = y; flags.push(flag); } // Write endPtsOfContours entry. buf.setUint16(pos, xCoordinates.length - 1); pos += 2; } // Write instructionLength. buf.setUint16(pos, this.instructions.length); pos += 2; if (this.instructions.length) { // Write instructions. new Uint8Array(buf.buffer, 0, buf.buffer.byteLength).set( this.instructions, pos ); pos += this.instructions.length; } // Write flags. for (const flag of flags) { buf.setUint8(pos++, flag); } // Write xCoordinates. for (let i = 0, ii = xCoordinates.length; i < ii; i++) { const x = xCoordinates[i]; const flag = flags[i]; if (flag & X_SHORT_VECTOR) { buf.setUint8(pos++, x); } else if (!(flag & X_IS_SAME_OR_POSITIVE_X_SHORT_VECTOR)) { buf.setInt16(pos, x); pos += 2; } } // Write yCoordinates. for (let i = 0, ii = yCoordinates.length; i < ii; i++) { const y = yCoordinates[i]; const flag = flags[i]; if (flag & Y_SHORT_VECTOR) { buf.setUint8(pos++, y); } else if (!(flag & Y_IS_SAME_OR_POSITIVE_Y_SHORT_VECTOR)) { buf.setInt16(pos, y); pos += 2; } } return pos - spos; } scale(x, factor) { for (const contour of this.contours) { if (contour.xCoordinates.length === 0) { continue; } for (let i = 0, ii = contour.xCoordinates.length; i < ii; i++) { contour.xCoordinates[i] = Math.round( x + (contour.xCoordinates[i] - x) * factor ); } } } } class CompositeGlyph { constructor({ flags, glyphIndex, argument1, argument2, transf, instructions, }) { this.flags = flags; this.glyphIndex = glyphIndex; this.argument1 = argument1; this.argument2 = argument2; this.transf = transf; this.instructions = instructions; } static parse(pos, glyf) { const spos = pos; const transf = []; let flags = glyf.getUint16(pos); const glyphIndex = glyf.getUint16(pos + 2); pos += 4; let argument1, argument2; if (flags & ARG_1_AND_2_ARE_WORDS) { if (flags & ARGS_ARE_XY_VALUES) { argument1 = glyf.getInt16(pos); argument2 = glyf.getInt16(pos + 2); } else { argument1 = glyf.getUint16(pos); argument2 = glyf.getUint16(pos + 2); } pos += 4; flags ^= ARG_1_AND_2_ARE_WORDS; } else { if (flags & ARGS_ARE_XY_VALUES) { argument1 = glyf.getInt8(pos); argument2 = glyf.getInt8(pos + 1); } else { argument1 = glyf.getUint8(pos); argument2 = glyf.getUint8(pos + 1); } pos += 2; } if (flags & WE_HAVE_A_SCALE) { // Single F2.14. transf.push(glyf.getUint16(pos)); pos += 2; } else if (flags & WE_HAVE_AN_X_AND_Y_SCALE) { // Two F2.14. transf.push(glyf.getUint16(pos), glyf.getUint16(pos + 2)); pos += 4; } else if (flags & WE_HAVE_A_TWO_BY_TWO) { // Four F2.14. transf.push( glyf.getUint16(pos), glyf.getUint16(pos + 2), glyf.getUint16(pos + 4), glyf.getUint16(pos + 6) ); pos += 8; } let instructions = null; if (flags & WE_HAVE_INSTRUCTIONS) { const instructionLength = glyf.getUint16(pos); pos += 2; instructions = new Uint8Array(glyf).slice(pos, pos + instructionLength); pos += instructionLength; } return [ pos - spos, new CompositeGlyph({ flags, glyphIndex, argument1, argument2, transf, instructions, }), ]; } getSize() { let size = 2 + 2 + this.transf.length * 2; if (this.flags & WE_HAVE_INSTRUCTIONS) { size += 2 + this.instructions.length; } size += 2; if (this.flags & 2) { // Arguments are signed. if ( !( this.argument1 >= -128 && this.argument1 <= 127 && this.argument2 >= -128 && this.argument2 <= 127 ) ) { size += 2; } } else { if ( !( this.argument1 >= 0 && this.argument1 <= 255 && this.argument2 >= 0 && this.argument2 <= 255 ) ) { size += 2; } } return size; } write(pos, buf) { const spos = pos; if (this.flags & ARGS_ARE_XY_VALUES) { // Arguments are signed. if ( !( this.argument1 >= -128 && this.argument1 <= 127 && this.argument2 >= -128 && this.argument2 <= 127 ) ) { this.flags |= ARG_1_AND_2_ARE_WORDS; } } else { if ( !( this.argument1 >= 0 && this.argument1 <= 255 && this.argument2 >= 0 && this.argument2 <= 255 ) ) { this.flags |= ARG_1_AND_2_ARE_WORDS; } } buf.setUint16(pos, this.flags); buf.setUint16(pos + 2, this.glyphIndex); pos += 4; if (this.flags & ARG_1_AND_2_ARE_WORDS) { if (this.flags & ARGS_ARE_XY_VALUES) { buf.setInt16(pos, this.argument1); buf.setInt16(pos + 2, this.argument2); } else { buf.setUint16(pos, this.argument1); buf.setUint16(pos + 2, this.argument2); } pos += 4; } else { buf.setUint8(pos, this.argument1); buf.setUint8(pos + 1, this.argument2); pos += 2; } if (this.flags & WE_HAVE_INSTRUCTIONS) { buf.setUint16(pos, this.instructions.length); pos += 2; // Write instructions. if (this.instructions.length) { new Uint8Array(buf.buffer, 0, buf.buffer.byteLength).set( this.instructions, pos ); pos += this.instructions.length; } } return pos - spos; } scale(x, factor) {} } export { GlyfTable };