From bb3d8b4710b06c4b15734b9ce915b145079adea5 Mon Sep 17 00:00:00 2001 From: Calixte Denizet Date: Mon, 10 May 2021 11:54:38 +0200 Subject: [PATCH] Add the possibility to rescale each glyph in a font - a lot of xfa files are using Myriad pro or Arial fonts without embedding them and some containers have some dimensions based on those font metrics. So not having the exact same font leads to a wrong display. - since it's pretty hard to find a replacement font with the exact same metrics, this patch gives the possibility to read glyf table, rescale each glyph and then write a new table. - so once PR #12726 is merged we could rescale for example Helvetica to replace Myriad Pro. --- src/core/fonts.js | 46 +++ src/core/glyf.js | 708 ++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 754 insertions(+) create mode 100644 src/core/glyf.js diff --git a/src/core/fonts.js b/src/core/fonts.js index ebf84af211115..8b8ec434d3377 100644 --- a/src/core/fonts.js +++ b/src/core/fonts.js @@ -57,6 +57,7 @@ import { import { IdentityToUnicodeMap, ToUnicodeMap } from "./to_unicode_map.js"; import { CFFFont } from "./cff_font.js"; import { FontRendererFactory } from "./font_renderer.js"; +import { GlyfTable } from "./glyf.js"; import { IdentityCMap } from "./cmap.js"; import { OpenTypeFileBuilder } from "./opentype_file_builder.js"; import { readUint32 } from "./core_utils.js"; @@ -2323,6 +2324,51 @@ class Font { font.pos = (font.start || 0) + tables.maxp.offset; const version = font.getInt32(); const numGlyphs = font.getUint16(); + + if ( + properties.scaleFactors && + properties.scaleFactors.length === numGlyphs && + isTrueType + ) { + const { scaleFactors } = properties; + const isGlyphLocationsLong = int16( + tables.head.data[50], + tables.head.data[51] + ); + + const glyphs = new GlyfTable({ + glyfTable: tables.glyf.data, + isGlyphLocationsLong, + locaTable: tables.loca.data, + numGlyphs, + }); + glyphs.scale(scaleFactors); + + const { glyf, loca, isLocationLong } = glyphs.write(); + tables.glyf.data = glyf; + tables.loca.data = loca; + + if (isLocationLong !== !!isGlyphLocationsLong) { + tables.head.data[50] = 0; + tables.head.data[51] = isLocationLong ? 1 : 0; + } + + const metrics = tables.hmtx.data; + + for (let i = 0; i < numGlyphs; i++) { + const j = 4 * i; + const advanceWidth = Math.round( + scaleFactors[i] * int16(metrics[j], metrics[j + 1]) + ); + metrics[j] = (advanceWidth >> 8) & 0xff; + metrics[j + 1] = advanceWidth & 0xff; + const lsb = Math.round( + scaleFactors[i] * signedInt16(metrics[j + 2], metrics[j + 3]) + ); + writeSignedInt16(metrics, j + 2, lsb); + } + } + // Glyph 0 is duplicated and appended. let numGlyphsOut = numGlyphs + 1; let dupFirstEntry = true; diff --git a/src/core/glyf.js b/src/core/glyf.js new file mode 100644 index 0000000000000..4027214da6c58 --- /dev/null +++ b/src/core/glyf.js @@ -0,0 +1,708 @@ +/* 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 = 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 = flag | X_IS_SAME_OR_POSITIVE_X_SHORT_VECTOR; + xCoordinates.push(0); + } else { + const abs = Math.abs(delta); + if (abs <= 255) { + flag = + 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 = flag | Y_IS_SAME_OR_POSITIVE_Y_SHORT_VECTOR; + yCoordinates.push(0); + } else { + const abs = Math.abs(delta); + if (abs <= 255) { + flag = + 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 = flags ^ ARG_1_AND_2_ARE_WORDS; + } else { + argument1 = glyf.getUint8(pos); + argument2 = glyf.getUint8(pos + 1); + if (flags & ARGS_ARE_XY_VALUES) { + const abs1 = argument1 & 0x7f; + argument1 = argument1 & 0x80 ? -abs1 : abs1; + + const abs2 = argument2 & 0x7f; + argument2 = argument2 & 0x80 ? -abs2 : abs2; + } + 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 = this.flags | ARG_1_AND_2_ARE_WORDS; + } + } else { + if ( + !( + this.argument1 >= 0 && + this.argument1 <= 255 && + this.argument2 >= 0 && + this.argument2 <= 255 + ) + ) { + this.flags = 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 };