Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 23 additions & 4 deletions src/glyphset.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -134,10 +134,29 @@ function ttfGlyphLoader(font, index, parseGlyph, data, position, buildPath) {
const glyph = new Glyph({index: index, font: font});

glyph.path = function() {
parseGlyph(glyph, data, position);
const path = buildPath(font.glyphs, glyph);
path.unitsPerEm = font.unitsPerEm;
return path;
const pathLoadStack = font._glyphPathLoadStack || (font._glyphPathLoadStack = []);
if (glyph._isLoadingPath) {
const cycleStart = pathLoadStack.indexOf(index);
const cycle = cycleStart === -1 ?
[index, index] :
pathLoadStack.slice(cycleStart).concat(index);
throw new Error(`Circular component reference detected: ${cycle.join(' -> ')}`);
}

glyph._isLoadingPath = true;
pathLoadStack.push(index);
try {
parseGlyph(glyph, data, position);
const path = buildPath(font.glyphs, glyph);
path.unitsPerEm = font.unitsPerEm;
return path;
} finally {
pathLoadStack.pop();
glyph._isLoadingPath = false;
if (pathLoadStack.length === 0) {
delete font._glyphPathLoadStack;
}
}
};

defineDependentProperty(glyph, 'numberOfContours', '_numberOfContours');
Expand Down
Binary file added test/fonts/CompositeCycle.ttf
Binary file not shown.
159 changes: 159 additions & 0 deletions test/fonts/generate-composite-cycle-font.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,159 @@
import { writeFileSync } from 'fs';
import { fileURLToPath } from 'url';
import sfnt from '../../src/tables/sfnt.mjs';
import table from '../../src/table.mjs';
import cmap from '../../src/tables/cmap.mjs';
import head from '../../src/tables/head.mjs';
import hhea from '../../src/tables/hhea.mjs';
import maxp from '../../src/tables/maxp.mjs';
import name from '../../src/tables/name.mjs';
import post from '../../src/tables/post.mjs';

const TRUE_TYPE_SIGNATURE = String.fromCharCode(0, 1, 0, 0);
const COMPOSITE_FLAGS = {
ARG_1_AND_2_ARE_WORDS: 0x0001,
ARGS_ARE_XY_VALUES: 0x0002,
MORE_COMPONENTS: 0x0020
};

function makeByteTable(tableName, bytes) {
return new table.Table(tableName, bytes.map((value, index) => ({
name: `byte_${index}`,
type: 'BYTE',
value
})));
}

function makeUShortTable(tableName, values) {
return new table.Table(tableName, values.map((value, index) => ({
name: `ushort_${index}`,
type: 'USHORT',
value
})));
}

function makeHmtxTable(advanceWidths, leftSideBearings) {
const fields = [];
for (let i = 0; i < advanceWidths.length; i += 1) {
fields.push({name: `advanceWidth_${i}`, type: 'USHORT', value: advanceWidths[i]});
fields.push({name: `leftSideBearing_${i}`, type: 'SHORT', value: leftSideBearings[i]});
}
return new table.Table('hmtx', fields);
}

function createNames() {
const unicode = {
copyright: {en: ' '},
fontFamily: {en: 'Composite Cycle Test'},
fontSubfamily: {en: 'Regular'},
uniqueID: {en: 'Composite Cycle Test Regular'},
fullName: {en: 'Composite Cycle Test Regular'},
version: {en: 'Version 1.0'},
postScriptName: {en: 'CompositeCycleTest-Regular'},
trademark: {en: ' '}
};

return {
unicode,
windows: unicode,
macintosh: unicode
};
}

function createGlyphMetadata() {
const glyphs = [
{name: '.notdef', unicodes: []},
{name: 'cycleA', unicode: 65, unicodes: [65]},
{name: 'cycleB', unicode: 66, unicodes: [66]}
];

return {
length: glyphs.length,
get(index) {
return glyphs[index];
}
};
}

function createGlyfBytes() {
return [
// glyph 0: empty .notdef
0x00, 0x00,
0x00, 0x00,
0x00, 0x00,
0x00, 0x00,
0x00, 0x00,

// glyph 1: composite referencing glyph 2
0xFF, 0xFF,
0x00, 0x00,
0x00, 0x00,
0x00, 0x00,
0x00, 0x00,
0x00, 0x23,
0x00, 0x02,
0x00, 0x00,
0x00, 0x00,

// glyph 2: composite referencing glyph 1
0xFF, 0xFF,
0x00, 0x00,
0x00, 0x00,
0x00, 0x00,
0x00, 0x00,
0x00, 0x03,
0x00, 0x01,
0x00, 0x00,
0x00, 0x00
];
}

export function makeCompositeCycleFontBuffer() {
const glyphs = createGlyphMetadata();
const tables = [
cmap.make(glyphs),
head.make({
unitsPerEm: 1000,
indexToLocFormat: 0,
lowestRecPPEM: 3
}),
hhea.make({
ascender: 800,
descender: -200,
advanceWidthMax: 500,
numberOfHMetrics: glyphs.length
}),
maxp.make(glyphs.length),
makeByteTable('glyf', createGlyfBytes()),
makeUShortTable('loca', [0, 5, 14, 23]),
makeHmtxTable([500, 500, 500], [0, 0, 0]),
name.make(createNames(), []),
post.make({tables: {}})
];

const sfntTable = sfnt.make(tables);
sfntTable.version = TRUE_TYPE_SIGNATURE;

const bytes = sfntTable.encode();
const checkSum = sfnt.computeCheckSum(bytes);
for (let i = 0; i < sfntTable.fields.length; i += 1) {
if (sfntTable.fields[i].name === 'head table') {
sfntTable.fields[i].value.checkSumAdjustment = 0xB1B0AFBA - checkSum;
break;
}
}

return new Uint8Array(sfntTable.encode()).buffer;
}

const outputPath = fileURLToPath(new URL('./CompositeCycle.ttf', import.meta.url));
if (process.argv[1] === fileURLToPath(import.meta.url)) {
const buffer = new Uint8Array(makeCompositeCycleFontBuffer());
writeFileSync(outputPath, buffer);
const aComponentFlags = COMPOSITE_FLAGS.ARG_1_AND_2_ARE_WORDS |
COMPOSITE_FLAGS.ARGS_ARE_XY_VALUES |
COMPOSITE_FLAGS.MORE_COMPONENTS;
const bComponentFlags = COMPOSITE_FLAGS.ARG_1_AND_2_ARE_WORDS |
COMPOSITE_FLAGS.ARGS_ARE_XY_VALUES;
console.log(`Wrote ${outputPath} (${buffer.byteLength} bytes, flags ${aComponentFlags}/${bComponentFlags}).`);
}
20 changes: 20 additions & 0 deletions test/glyph.spec.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -173,6 +173,26 @@ describe('glyph.mjs', function() {
});
});

describe('composite glyph cycle detection', function() {
function loadCompositeCycleFont(opt) {
return loadSync('./test/fonts/CompositeCycle.ttf', opt);
}

it('throws a controlled error for circular composite references', function() {
const font = loadCompositeCycleFont();
assert.throws(function() {
font.glyphs.get(1).getPath();
}, /Circular component reference/);
});

it('throws a controlled error for circular composite references in low memory mode', function() {
const font = loadCompositeCycleFont({lowMemory: true});
assert.throws(function() {
font.glyphs.get(1).getPath();
}, /Circular component reference/);
});
});

describe('color glyph drawing/rendering', function() {
it('draws and renders layers correctly', function() {
let contextLogs = [];
Expand Down
Loading