Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
16 commits
Select commit Hold shift + click to select a range
5b99173
Bump actions/create-github-app-token from 3.1.1 to 3.2.0
dependabot[bot] May 19, 2026
16a9f1c
Bump github/codeql-action from 4.35.3 to 4.35.4
dependabot[bot] May 19, 2026
abe8b56
Don't use "internal" `EventBus` methods in the integration-tests
Snuffleupagus May 20, 2026
429b469
Add basic integration-tests for the `PdfTextExtractor` class
Snuffleupagus May 20, 2026
d79043b
Allow merging a PDF by dropping it onto the thumbnail viewer
calixteman May 20, 2026
7447165
Extend unit-test coverage for the `getPdfFilenameFromUrl` helper func…
Snuffleupagus May 21, 2026
0f90987
Fix 'Select all' after #20981
calixteman May 20, 2026
78cc2e3
Merge pull request #21309 from calixteman/issue21307
calixteman May 21, 2026
42db304
Merge pull request #21305 from Snuffleupagus/integration-test-EventBu…
timvandermeij May 21, 2026
2231706
Merge pull request #21301 from mozilla/dependabot/github_actions/acti…
timvandermeij May 21, 2026
83c3735
Merge pull request #21302 from mozilla/dependabot/github_actions/gith…
timvandermeij May 21, 2026
93f01aa
Merge pull request #21311 from Snuffleupagus/getPdfFilenameFromUrl-te…
timvandermeij May 21, 2026
9b5cd3d
Merge pull request #21304 from Snuffleupagus/PdfTextExtractor-tests
timvandermeij May 21, 2026
52d574c
Merge pull request #21306 from calixteman/dnd_pdf_merging
timvandermeij May 21, 2026
d6a2b91
Sanitize glyf composite cycles, OS/2 length and maxp version mismatches
calixteman May 19, 2026
98e3a85
Merge pull request #21300 from calixteman/issue21298
calixteman May 21, 2026
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
6 changes: 3 additions & 3 deletions .github/workflows/codeql.yml
Original file line number Diff line number Diff line change
Expand Up @@ -24,13 +24,13 @@ jobs:
persist-credentials: false

- name: Initialize CodeQL
uses: github/codeql-action/init@e46ed2cbd01164d986452f91f178727624ae40d7 # v4.35.3
uses: github/codeql-action/init@68bde559dea0fdcac2102bfdf6230c5f70eb485e # v4.35.4
with:
languages: ${{ matrix.language }}
queries: security-and-quality

- name: Autobuild CodeQL
uses: github/codeql-action/autobuild@e46ed2cbd01164d986452f91f178727624ae40d7 # v4.35.3
uses: github/codeql-action/autobuild@68bde559dea0fdcac2102bfdf6230c5f70eb485e # v4.35.4

- name: Perform CodeQL analysis
uses: github/codeql-action/analyze@e46ed2cbd01164d986452f91f178727624ae40d7 # v4.35.3
uses: github/codeql-action/analyze@68bde559dea0fdcac2102bfdf6230c5f70eb485e # v4.35.4
2 changes: 1 addition & 1 deletion .github/workflows/notify-pdf-sync.yml
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ jobs:
- name: Generate app token
if: steps.check.outputs.has_added == 'true'
id: app-token
uses: actions/create-github-app-token@1b10c78c7865c340bc4f6099eb2f838309f1e8c3 # v3.1.1
uses: actions/create-github-app-token@bcd2ba49218906704ab6c1aa796996da409d3eb1 # v3.2.0
with:
client-id: ${{ secrets.CLIENT_ID }}
private-key: ${{ secrets.APP_PRIVATE_KEY }}
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/update_locales.yml
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ jobs:
steps:
- name: Generate app token
id: app-token
uses: actions/create-github-app-token@1b10c78c7865c340bc4f6099eb2f838309f1e8c3 # v3.1.1
uses: actions/create-github-app-token@bcd2ba49218906704ab6c1aa796996da409d3eb1 # v3.2.0
with:
client-id: ${{ secrets.CLIENT_ID }}
private-key: ${{ secrets.APP_PRIVATE_KEY }}
Expand Down
3 changes: 3 additions & 0 deletions src/core/font_renderer.js
Original file line number Diff line number Diff line change
Expand Up @@ -163,6 +163,9 @@ function lookupCmap(ranges, unicode) {
}

function compileGlyf(code, cmds, font, visitedGlyphs = new Set()) {
if (!code?.length) {
return;
}
if (visitedGlyphs.has(code)) {
warn("compileGlyf: skipping recursive composite glyph reference.");
return;
Expand Down
43 changes: 34 additions & 9 deletions src/core/fonts.js
Original file line number Diff line number Diff line change
Expand Up @@ -55,13 +55,13 @@ import {
getSupplementalGlyphMapForArialBlack,
getSupplementalGlyphMapForCalibri,
} from "./standard_fonts.js";
import { GlyfTable, pruneCompositeGlyphCycles } from "./glyf.js";
import { IdentityToUnicodeMap, ToUnicodeMap } from "./to_unicode_map.js";
import { CFFFont } from "./cff_font.js";
import { compileFontInfo } from "./obj_bin_transform_core.js";
import { DataBuilder } from "./data_builder.js";
import { FontRendererFactory } from "./font_renderer.js";
import { getFontBasicMetrics } from "./metrics.js";
import { GlyfTable } from "./glyf.js";
import { OpenTypeFileBuilder } from "./opentype_file_builder.js";
import { Stream } from "./stream.js";
import { Type1Font } from "./type1_font.js";
Expand Down Expand Up @@ -720,6 +720,11 @@ function createCmapTable(glyphs, toUnicodeExtraMap, numGlyphs) {
function validateOS2Table(os2, file) {
file.pos = (file.start || 0) + os2.offset;
const version = file.getUint16();
// https://learn.microsoft.com/en-us/typography/opentype/spec/os2
const minLength = [78, 86, 96, 96, 96, 100][version];
if (minLength === undefined || os2.length < minLength) {
return false;
}
// TODO verify all OS/2 tables fields, but currently we validate only those
// that give us issues
file.skip(60); // skipping type, misc sizes, panose, unicode ranges
Expand Down Expand Up @@ -2195,18 +2200,25 @@ class Font {
last.endOffset = oldGlyfDataLength;
}

const droppedGlyphs = pruneCompositeGlyphCycles(
oldGlyfData,
locaEntries,
numGlyphs
);
const missingGlyphs = Object.create(null);
let writeOffset = 0;
itemEncode(locaData, 0, writeOffset);
for (i = 0, j = itemSize; i < numGlyphs; i++, j += itemSize) {
const glyphProfile = sanitizeGlyph(
oldGlyfData,
locaEntries[i].offset,
locaEntries[i].endOffset,
newGlyfData,
writeOffset,
hintsValid
);
const glyphProfile = droppedGlyphs.has(i)
? { length: 0, sizeOfInstructions: 0 }
: sanitizeGlyph(
oldGlyfData,
locaEntries[i].offset,
locaEntries[i].endOffset,
newGlyfData,
writeOffset,
hintsValid
);
const newLength = glyphProfile.length;
if (newLength === 0) {
missingGlyphs[i] = true;
Expand Down Expand Up @@ -2837,6 +2849,19 @@ class Font {
maxFunctionDefs = font.getUint16();
font.pos += 4;
maxSizeOfInstructions = font.getUint16();
} else if (isTrueType && version === 0x00005000) {
const newMaxp = new Uint8Array(32);
writeUint32(newMaxp, 0, 0x00010000);
newMaxp[4] = (numGlyphs >> 8) & 0xff;
newMaxp[5] = numGlyphs & 0xff;
newMaxp.fill(0xff, 6, 14);
newMaxp[15] = 2;
newMaxp[28] = 0xff;
newMaxp[29] = 0xff;
newMaxp[31] = 0x10;
tables.maxp.data = newMaxp;
tables.maxp.length = 32;
version = 0x00010000;
}

tables.maxp.data[4] = numGlyphsOut >> 8;
Expand Down
122 changes: 118 additions & 4 deletions src/core/glyf.js
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,8 @@ const WE_HAVE_INSTRUCTIONS = 1 << 8;
// const SCALED_COMPONENT_OFFSET = 1 << 11;
// const UNSCALED_COMPONENT_OFFSET = 1 << 12;

const GLYPH_HEADER_SIZE = 10;

/**
* GlyfTable object represents a glyf table containing glyph information:
* - glyph header (xMin, yMin, xMax, yMax);
Expand Down Expand Up @@ -218,7 +220,7 @@ class GlyphHeader {

static parse(pos, glyf) {
return [
10,
GLYPH_HEADER_SIZE,
new GlyphHeader({
numberOfContours: glyf.getInt16(pos),
xMin: glyf.getInt16(pos + 2),
Expand All @@ -230,7 +232,7 @@ class GlyphHeader {
}

getSize() {
return 10;
return GLYPH_HEADER_SIZE;
}

write(pos, buf) {
Expand All @@ -240,7 +242,7 @@ class GlyphHeader {
buf.setInt16(pos + 6, this.xMax);
buf.setInt16(pos + 8, this.yMax);

return 10;
return GLYPH_HEADER_SIZE;
}

scale(x, factor) {
Expand Down Expand Up @@ -696,4 +698,116 @@ class CompositeGlyph {
scale(x, factor) {}
}

export { GlyfTable };
function pruneCompositeGlyphCycles(glyfTable, locaEntries, numGlyphs) {
const glyf = new DataView(
glyfTable.buffer,
glyfTable.byteOffset,
glyfTable.byteLength
);
const components = new Array(numGlyphs);
for (let i = 0; i < numGlyphs; i++) {
const offset = locaEntries[i].offset;
const endOffset = Math.min(locaEntries[i].endOffset, glyf.byteLength);
if (endOffset - offset <= GLYPH_HEADER_SIZE || glyf.getInt16(offset) >= 0) {
continue;
}
const comps = [];
let p = offset + GLYPH_HEADER_SIZE;
while (p + 4 <= endOffset) {
const flags = glyf.getUint16(p);
const gid = glyf.getUint16(p + 2);
let size = 4 + (flags & ARG_1_AND_2_ARE_WORDS ? 4 : 2);
if (flags & WE_HAVE_A_SCALE) {
size += 2;
} else if (flags & WE_HAVE_AN_X_AND_Y_SCALE) {
size += 4;
} else if (flags & WE_HAVE_A_TWO_BY_TWO) {
size += 8;
}
comps.push({ gid, offset: p, size, flags });
p += size;
if (!(flags & MORE_COMPONENTS)) {
break;
}
}
if (comps.length) {
components[i] = comps;
}
}

const WHITE = 0,
GRAY = 1,
BLACK = 2;
const state = new Uint8Array(numGlyphs);
const backEdges = new Map();
for (let start = 0; start < numGlyphs; start++) {
if (state[start] !== WHITE || !components[start]) {
continue;
}
const stack = [{ node: start, idx: 0 }];
state[start] = GRAY;
while (stack.length > 0) {
const top = stack.at(-1);
const comps = components[top.node];
if (!comps || top.idx >= comps.length) {
state[top.node] = BLACK;
stack.pop();
continue;
}
const compIdx = top.idx++;
const next = comps[compIdx].gid;
if (next >= numGlyphs || state[next] === BLACK) {
continue;
}
if (state[next] === WHITE) {
state[next] = GRAY;
stack.push({ node: next, idx: 0 });
continue;
}

let removeSet = backEdges.get(top.node);
if (!removeSet) {
removeSet = new Set();
backEdges.set(top.node, removeSet);
}
removeSet.add(compIdx);
}
}

const droppedGlyphs = new Set();
for (const [gIdx, removeSet] of backEdges) {
const comps = components[gIdx];
const remaining = [];
for (let ci = 0; ci < comps.length; ci++) {
if (!removeSet.has(ci)) {
remaining.push(comps[ci]);
}
}
if (remaining.length === 0) {
droppedGlyphs.add(gIdx);
continue;
}
const start = locaEntries[gIdx].offset;
const endOffset = Math.min(locaEntries[gIdx].endOffset, glyf.byteLength);
let writePos = start + GLYPH_HEADER_SIZE;
for (let ci = 0; ci < remaining.length; ci++) {
const c = remaining[ci];
const isLast = ci === remaining.length - 1;
let newFlags = c.flags & ~WE_HAVE_INSTRUCTIONS;
newFlags = isLast
? newFlags & ~MORE_COMPONENTS
: newFlags | MORE_COMPONENTS;
if (writePos !== c.offset) {
glyfTable.copyWithin(writePos, c.offset, c.offset + c.size);
}
glyf.setUint16(writePos, newFlags);
writePos += c.size;
}
if (writePos < endOffset) {
glyfTable.fill(0, writePos, endOffset);
}
}
return droppedGlyphs;
}

export { GlyfTable, pruneCompositeGlyphCycles };
Loading
Loading