pdf.js/src/core/glyf.js
Calixte Denizet 74f25d2755 Font renderer - get int8 instead of uint8 in composite glyphes (bug 1749563)
- it aims to fix https://bugzilla.mozilla.org/show_bug.cgi?id=1749563;
 - use some helper functions to get (u|i)int** values in buffer: it helps to have a clearer code;
 - in composite glyphes the translations values with a transformations are signed so consequently get some int8 instead of uint8;
 - add few TODOs.
2022-01-18 22:06:23 +01:00

705 lines
18 KiB
JavaScript

/* 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 };