Skip to content
Merged
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
89 changes: 61 additions & 28 deletions src/core/evaluator.js
Original file line number Diff line number Diff line change
Expand Up @@ -1720,13 +1720,20 @@ class PartialEvaluator {
const stateManager = new StateManager(initialState);
const preprocessor = new EvaluatorPreprocessor(stream, xref, stateManager);
const timeSlotManager = new TimeSlotManager();
let markedContentLevel = 0;

function closePendingRestoreOPS(argument) {
for (let i = 0, ii = preprocessor.savedStatesDepth; i < ii; i++) {
operatorList.addOp(OPS.restore, []);
}
}

function closePendingMarkedContentOPS() {
for (; markedContentLevel > 0; markedContentLevel--) {
operatorList.addOp(OPS.endMarkedContent, []);
}
}

return new Promise(function promiseBody(resolve, reject) {
const next = function (promise) {
Promise.all([promise, operatorList.ready]).then(function () {
Expand All @@ -1741,7 +1748,7 @@ class PartialEvaluator {
timeSlotManager.reset();

const operation = {};
let stop, i, ii, cs, name, isValidName;
let stop, cs, name, isValidName;
while (!(stop = timeSlotManager.check())) {
// The arguments parsed by read() are used beyond this loop, so we
// cannot reuse the same array on each iteration. Therefore we pass
Expand Down Expand Up @@ -2299,6 +2306,7 @@ class PartialEvaluator {
// but doing so is meaningless without knowing the semantics.
continue;
case OPS.beginMarkedContentProps:
markedContentLevel++;
if (!(args[0] instanceof Name)) {
warn(`Expected name for beginMarkedContentProps arg0=${args[0]}`);
operatorList.addOp(OPS.beginMarkedContentProps, ["OC", null]);
Expand Down Expand Up @@ -2333,29 +2341,35 @@ class PartialEvaluator {
);
return;
}
// Other marked content types aren't supported yet.
// Preserve only the MCID from non-OC property dictionaries.
args = [
args[0].name,
args[1] instanceof Dict ? args[1].get("MCID") : null,
];

break;
case OPS.beginMarkedContent:
if (args?.some(arg => arg instanceof Dict)) {
warn(`getOperatorList - ignoring operator: ${fn}`);
continue;
}
markedContentLevel++;
break;
case OPS.endMarkedContent:
if (args?.some(arg => arg instanceof Dict)) {
warn(`getOperatorList - ignoring operator: ${fn}`);
continue;
}
if (markedContentLevel === 0) {
continue;
}
markedContentLevel--;
break;
default:
// Note: Ignore the operator if it has `Dict` arguments, since
// those are non-serializable, otherwise postMessage will throw
// "An object could not be cloned.".
if (args !== null) {
for (i = 0, ii = args.length; i < ii; i++) {
if (args[i] instanceof Dict) {
break;
}
}
if (i < ii) {
warn("getOperatorList - ignoring operator: " + fn);
continue;
}
// Avoid postMessage errors from `Dict` arguments.
if (args?.some(arg => arg instanceof Dict)) {
warn(`getOperatorList - ignoring operator: ${fn}`);
continue;
}
}
operatorList.addOp(fn, args);
Expand All @@ -2364,8 +2378,8 @@ class PartialEvaluator {
next(deferred);
return;
}
// Some PDFs don't close all restores inside object/form.
// Closing those for them.
// Close marked content and graphics states left open by this stream.
closePendingMarkedContentOPS();
closePendingRestoreOPS();
resolve();
}).catch(reason => {
Expand All @@ -2378,6 +2392,7 @@ class PartialEvaluator {
`task: "${reason}".`
);

closePendingMarkedContentOPS();
closePendingRestoreOPS();
return;
}
Expand All @@ -2395,7 +2410,6 @@ class PartialEvaluator {
seenStyles = new Set(),
viewBox,
lang = null,
markedContentData = null,
disableNormalization = false,
keepWhiteSpace = false,
prevRefs = null,
Expand Down Expand Up @@ -2425,9 +2439,8 @@ class PartialEvaluator {
resources ||= Dict.empty;
stateManager ||= new StateManager(new TextState());

if (includeMarkedContent) {
markedContentData ||= { level: 0 };
}
let markedContentLevel = 0;
let textMarkedContentLevel = null;

const textContent = {
items: [],
Expand Down Expand Up @@ -3173,6 +3186,19 @@ class PartialEvaluator {
textContentItem.str.length = 0;
}

function closePendingMarkedContentItems(level = 0) {
if (!includeMarkedContent || markedContentLevel <= level) {
return;
}
flushTextContentItem();

for (; markedContentLevel > level; markedContentLevel--) {
textContent.items.push({
type: "endMarkedContent",
});
}
}

function enqueueChunk(batch = false) {
const length = textContent.items.length;
if (length === 0) {
Expand Down Expand Up @@ -3291,6 +3317,13 @@ class PartialEvaluator {
case OPS.beginText:
textState.textMatrix = IDENTITY_MATRIX.slice();
textState.textLineMatrix = IDENTITY_MATRIX.slice();
textMarkedContentLevel = markedContentLevel;
break;
case OPS.endText:
if (textMarkedContentLevel !== null) {
closePendingMarkedContentItems(textMarkedContentLevel);
textMarkedContentLevel = null;
}
break;
case OPS.showSpacedText:
if (!stateManager.state.font) {
Expand Down Expand Up @@ -3451,7 +3484,6 @@ class PartialEvaluator {
seenStyles,
viewBox,
lang,
markedContentData,
disableNormalization,
keepWhiteSpace,
prevRefs: seenRefs,
Expand Down Expand Up @@ -3535,7 +3567,7 @@ class PartialEvaluator {
case OPS.beginMarkedContent:
flushTextContentItem();
if (includeMarkedContent) {
markedContentData.level++;
markedContentLevel++;

textContent.items.push({
type: "beginMarkedContent",
Expand All @@ -3546,7 +3578,7 @@ class PartialEvaluator {
case OPS.beginMarkedContentProps:
flushTextContentItem();
if (includeMarkedContent) {
markedContentData.level++;
markedContentLevel++;

const mcid = args[1] instanceof Dict ? args[1].get("MCID") : null;
textContent.items.push({
Expand All @@ -3561,12 +3593,11 @@ class PartialEvaluator {
case OPS.endMarkedContent:
flushTextContentItem();
if (includeMarkedContent) {
if (markedContentData.level === 0) {
// Handle unbalanced beginMarkedContent/endMarkedContent
// operators (fixes issue15629.pdf).
if (markedContentLevel === 0) {
// Ignore unmatched EMC operators (issue 15629).
break;
}
markedContentData.level--;
markedContentLevel--;

textContent.items.push({
type: "endMarkedContent",
Expand All @@ -3585,6 +3616,7 @@ class PartialEvaluator {
return;
}
flushTextContentItem();
closePendingMarkedContentItems();
enqueueChunk();
resolve();
}).catch(reason => {
Expand All @@ -3599,6 +3631,7 @@ class PartialEvaluator {
);

flushTextContentItem();
closePendingMarkedContentItems();
enqueueChunk();
return;
}
Expand Down
2 changes: 1 addition & 1 deletion src/core/jpeg_stream.js
Original file line number Diff line number Diff line change
Expand Up @@ -179,7 +179,7 @@ class JpegStream extends DecodeStream {
// the height is only known from a DNL marker or because the scan simply
// ends early (issue15492.pdf). `ImageDecoder` reports and scales the
// frame according to the SOF, so let our own decoder, which honours the
// dictionary, handle the image instead.
// actual JPEG image data, handle the image instead.
return null;
}
if (useImageDecoder.exifStart) {
Expand Down
20 changes: 12 additions & 8 deletions src/core/jpg.js
Original file line number Diff line number Diff line change
Expand Up @@ -811,11 +811,14 @@ class JpegImage {

static canUseImageDecoder(data, colorTransform = -1) {
const view = new DataView(data.buffer, data.byteOffset, data.byteLength);
let exifOffsets = null;
const info = {
width: 0,
height: 0,
exifStart: 0,
exifEnd: 0,
};
let offset = 0;
let numComponents = null;
let scanLines = 0,
samplesPerLine = 0;
let fileMarker = view.getUint16(offset);
offset += 2;
if (fileMarker !== /* SOI (Start of Image) = */ 0xffd8) {
Expand Down Expand Up @@ -845,12 +848,13 @@ class JpegImage {
appData[4] === 0 &&
appData[5] === 0
) {
if (exifOffsets) {
if (info.exifStart) {
throw new JpegError("Duplicate EXIF-blocks found.");
}
// Don't do the EXIF-block replacement here, see `JpegStream`,
// since that can modify the original PDF document.
exifOffsets = { exifStart: oldOffset + 6, exifEnd: newOffset };
info.exifStart = oldOffset + 6;
info.exifEnd = newOffset;
}
fileMarker = view.getUint16(offset);
offset += 2;
Expand All @@ -860,8 +864,8 @@ class JpegImage {
case 0xffc2: // SOF2 (Start of Frame, Progressive DCT)
// Skip marker length.
// Skip precision.
scanLines = view.getUint16(offset + (2 + 1));
samplesPerLine = view.getUint16(offset + (2 + 1 + 2));
info.height = view.getUint16(offset + (2 + 1)); // scanLines
info.width = view.getUint16(offset + (2 + 1 + 2)); // samplesPerLine
numComponents = data[offset + (2 + 1 + 2 + 2)];
break markerLoop;
case 0xffff: // Fill bytes
Expand All @@ -882,7 +886,7 @@ class JpegImage {
return null;
}
// A zero SOF height means that a later DNL marker defines it.
return { width: samplesPerLine, height: scanLines, ...exifOffsets };
return info;
}

parse(data, { dnlScanLines = null } = {}) {
Expand Down
6 changes: 5 additions & 1 deletion src/core/string_utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,7 @@ const PDFStringTranslateTable = [
0x2019, 0x201a, 0x2122, 0xfb01, 0xfb02, 0x141, 0x152, 0x160, 0x178, 0x17d,
0x131, 0x142, 0x153, 0x161, 0x17e, 0, 0x20ac,
];
const PDFStringTextDecoders = Object.create(null);

function stringToPDFString(str, keepEscapeSequence = false) {
// See section 7.9.2.2 Text String Type.
Expand All @@ -85,7 +86,10 @@ function stringToPDFString(str, keepEscapeSequence = false) {

if (encoding) {
try {
const decoder = new TextDecoder(encoding, { fatal: true });
const decoder = (PDFStringTextDecoders[encoding] ??= new TextDecoder(
encoding,
{ fatal: true }
));
const buffer = stringToBytes(str);
const decoded = decoder.decode(buffer);
if (keepEscapeSequence || !decoded.includes("\x1b")) {
Expand Down
26 changes: 26 additions & 0 deletions test/integration/text_layer_spec.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -1534,4 +1534,30 @@ describe("Text layer", () => {
expect(getPercentDiff(rect.height, 12)).toBeLessThan(0.03);
});
});

describe("marked-content nesting (bug 1898053)", () => {
let pages;

beforeAll(async () => {
pages = await loadAndWait(
"bug1898053_minimal.pdf",
".textLayer .endOfContent"
);
});
afterAll(async () => {
await closePages(pages);
});

it("must keep auto-closed sections at the text-layer root", async () => {
await Promise.all(
pages.map(async ([browserName, page]) => {
const count = await page.evaluate(
() =>
document.querySelectorAll(".textLayer > .markedContent").length
);
expect(count).toBe(6);
})
);
});
});
});
1 change: 1 addition & 0 deletions test/pdfs/.gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -951,3 +951,4 @@
!signed_verified.pdf
!function_based_shading_cmyk.pdf
!large_jpeg_downscale.pdf
!bug1898053_minimal.pdf
Binary file added test/pdfs/bug1898053_minimal.pdf
Binary file not shown.
53 changes: 53 additions & 0 deletions test/unit/api_spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -4480,6 +4480,59 @@ Caron Broadcasting, Inc., an Ohio corporation (“Lessee”).`)
await loadingTask.destroy();
});

it("auto-closes marked content opened in a text object (bug 1898053)", async function () {
const loadingTask = getDocument(
buildGetDocumentParams("bug1898053_minimal.pdf")
);
const pdfDoc = await loadingTask.promise;
const pdfPage = await pdfDoc.getPage(1);
const { items } = await pdfPage.getTextContent({
includeMarkedContent: true,
});

expect(items.map(item => item.id ?? item.type ?? item.str)).toEqual([
"Hello, world!",
"p3R_mc1",
"endMarkedContent",
"p3R_mc2",
"endMarkedContent",
"p3R_mc3",
"endMarkedContent",
"p3R_mc4",
"endMarkedContent",
"p3R_mc5",
"endMarkedContent",
"p3R_mc6",
"endMarkedContent",
]);

await loadingTask.destroy();
});

it("preserves marked content spanning text objects (bug 1823296)", async function () {
const loadingTask = getDocument(buildGetDocumentParams("bug1823296.pdf"));
const pdfDoc = await loadingTask.promise;
const pdfPage = await pdfDoc.getPage(1);
const { items } = await pdfPage.getTextContent({
includeMarkedContent: true,
disableNormalization: true,
});

const start = items.findIndex(item => item.id === "p3R_mc8");
const end = items.findIndex(
(item, index) => index > start && item.type === "endMarkedContent"
);

expect(items.slice(start + 1, end).map(item => item.str)).toEqual([
"",
"PDF/UA is not a separate file-format but simply a way to use the " +
"familiar PDF format invented by Adobe",
"Systems and now standardized as ISO 32000.[5]",
]);

await loadingTask.destroy();
});

it("gets text content with multi-byte entries, using predefined CMaps (issue 16176)", async function () {
const loadingTask = getDocument(
buildGetDocumentParams("issue16176.pdf", {
Expand Down
Loading
Loading