Compare commits

...

2 Commits

Author SHA1 Message Date
20b6e32907 Mostly working
Some checks are pending
CI / Test (20) (push) Waiting to run
CI / Test (22) (push) Waiting to run
CI / Test (23) (push) Waiting to run
CodeQL / Analyze (javascript) (push) Waiting to run
Lint / Lint (lts/*) (push) Waiting to run
Types tests / Test (lts/*) (push) Waiting to run
* ranges passed through
* updates possible
2025-03-26 17:24:25 +01:00
Kilian Schuettler
1dc874260b broken? 2025-03-17 00:21:40 +01:00
8 changed files with 263 additions and 113 deletions

View File

@ -11,7 +11,7 @@ const STANDARD_FONT_DATA_URL =
// Loading file from file system into typed array. // Loading file from file system into typed array.
const pdfPath = const pdfPath =
process.argv[2] || "../../web/compressed.tracemonkey-pldi-09.pdf"; process.argv[2] || "C:\\Users\\kj131\\pdf-forge\\test_pdfs\\ISO_32000-2_2020(en).pdf";
const data = new Uint8Array(fs.readFileSync(pdfPath)); const data = new Uint8Array(fs.readFileSync(pdfPath));
// Load the PDF file. // Load the PDF file.
@ -21,12 +21,46 @@ const loadingTask = getDocument({
cMapPacked: CMAP_PACKED, cMapPacked: CMAP_PACKED,
standardFontDataUrl: STANDARD_FONT_DATA_URL, standardFontDataUrl: STANDARD_FONT_DATA_URL,
}); });
test(loadingTask);
async function test(loading) {
try { try {
const pdfDocument = await loadingTask.promise; const pdfDocument = await loading.promise;
console.log("# PDF document loaded."); console.log("# PDF document loaded.");
const page = await pdfDocument.getPage(1); const page = await pdfDocument.getPage(4);
printOpList(page);
console.time("contents");
const contents = await page.getContents();
console.timeEnd("contents");
console.time("oplist");
const opList = await page.getOperatorList(); const opList = await page.getOperatorList();
console.log(opList); console.timeEnd("oplist");
// console.log(opList);
let newContents = "";
for (let i = 0; i < 5; i++) {
const range = opList.rangeArray[i];
if (range) {
newContents += contents.slice(range[0], range[1]);
newContents += "\n";
}
}
console.log(newContents);
await page.updateContents(newContents);
await printOpList(page);
} catch (e) { } catch (e) {
console.error(e); console.error(e);
} }
}
async function printOpList(page) {
const contents = await page.getContents();
const opList = await page.getOperatorList();
// console.log(opList);
const ops = [];
for (let i = 0; i < opList.rangeArray.length; i++) {
const range = opList.rangeArray[i];
if (range) {
ops.push(contents.slice(range[0], range[1]));
}
}
console.log(ops.slice(0, 100));
}

View File

@ -45,7 +45,7 @@ function bytesToString(bytes) {
} }
async function parse(doc) { async function parse(doc) {
const path = "/Page2/Contents/2"; const path = "/Page2/Contents/1";
let [stream] = await getPrimitive(path, doc); let [stream] = await getPrimitive(path, doc);
const lexer = new Lexer(stream); const lexer = new Lexer(stream);
const parser = new Parser({ lexer, xref: doc.xref, trackRanges: true }); const parser = new Parser({ lexer, xref: doc.xref, trackRanges: true });
@ -63,12 +63,12 @@ async function parse(doc) {
const bytes = stream.getBytes(); const bytes = stream.getBytes();
const classes = new Set(); const classes = new Set();
for (const o of objs) { for (const o of objs) {
console.log(o[0].constructor.name); // console.log(o[0].constructor.name);
classes.add(o[0].constructor.name); classes.add(o[0].constructor.name);
const lexemmeBytes = bytes.slice(o[1], o[2]); const lexemmeBytes = bytes.slice(o[1], o[2]);
console.log(bytesToString(lexemmeBytes)); // console.log(bytesToString(lexemmeBytes));
} }
console.log("unique classes", classes); // console.log("unique classes", classes);
[stream] = await getPrimitive(path, doc); [stream] = await getPrimitive(path, doc);
const preprocessor = new EvaluatorPreprocessor(stream, doc.xref); const preprocessor = new EvaluatorPreprocessor(stream, doc.xref);
const operation = {}; const operation = {};
@ -78,10 +78,10 @@ async function parse(doc) {
const fn = operation.fn; const fn = operation.fn;
const range = operation.range; const range = operation.range;
const op = bytesToString(bytes.slice(range[0], range[1])); const op = bytesToString(bytes.slice(range[0], range[1]));
console.log(args, fn); // console.log(args, fn);
console.log(`${range[0]}------------------------------------`); // console.log(`----------------- ${range} -------------------`);
console.log(op); console.log(`${fn}: ${op}`);
console.log(`${range[1]}------------------------------------`); // console.log(`---------------------------------------------`);
} }
// console.time("xref"); // console.time("xref");
// let table = await retrieveXref(doc); // let table = await retrieveXref(doc);

View File

@ -65,7 +65,7 @@ import { Catalog } from "./catalog.js";
import { clearGlobalCaches } from "./cleanup_helper.js"; import { clearGlobalCaches } from "./cleanup_helper.js";
import { DatasetReader } from "./dataset_reader.js"; import { DatasetReader } from "./dataset_reader.js";
import { Linearization } from "./parser.js"; import { Linearization } from "./parser.js";
import { NullStream } from "./stream.js"; import { NullStream, StringStream } from "./stream.js";
import { ObjectLoader } from "./object_loader.js"; import { ObjectLoader } from "./object_loader.js";
import { OperatorList } from "./operator_list.js"; import { OperatorList } from "./operator_list.js";
import { PartialEvaluator } from "./evaluator.js"; import { PartialEvaluator } from "./evaluator.js";
@ -107,6 +107,8 @@ class Page {
this.resourcesPromise = null; this.resourcesPromise = null;
this.xfaFactory = xfaFactory; this.xfaFactory = xfaFactory;
this.updatedContents = null;
const idCounters = { const idCounters = {
obj: 0, obj: 0,
}; };
@ -246,10 +248,19 @@ class Page {
throw reason; throw reason;
} }
setContents(newContents) {
this.updatedContents = newContents;
}
/** /**
* @returns {Promise<BaseStream>} * @returns {Promise<BaseStream>}
*/ */
getContentStream() { getContentStream() {
if (this.updatedContents !== null) {
return new Promise(resolve => {
resolve(new StringStream(this.updatedContents));
});
}
return this.pdfManager.ensure(this, "content").then(content => { return this.pdfManager.ensure(this, "content").then(content => {
if (content instanceof BaseStream) { if (content instanceof BaseStream) {
return content; return content;
@ -426,8 +437,11 @@ class Page {
cacheKey, cacheKey,
annotationStorage = null, annotationStorage = null,
modifiedIds = null, modifiedIds = null,
contentOverride = null,
}) { }) {
const contentStreamPromise = this.getContentStream(); const contentStreamPromise = contentOverride
? new StringStream(contentOverride)
: this.getContentStream();
const resourcesPromise = this.loadResources([ const resourcesPromise = this.loadResources([
"ColorSpace", "ColorSpace",
"ExtGState", "ExtGState",

View File

@ -802,6 +802,7 @@ class PartialEvaluator {
operatorList.addImageOps( operatorList.addImageOps(
OPS.paintImageXObject, OPS.paintImageXObject,
args, args,
range,
optionalContent, optionalContent,
hasMask hasMask
); );
@ -1445,15 +1446,20 @@ class PartialEvaluator {
minMax = [Infinity, Infinity, -Infinity, -Infinity]; minMax = [Infinity, Infinity, -Infinity, -Infinity];
break; break;
} }
operatorList.addOp(OPS.constructPath, [[fn], args, minMax], range); operatorList.addOp(
OPS.constructPath,
[[fn], args, minMax, [range[0], range[1]]],
range
);
if (parsingText) { if (parsingText) {
operatorList.addOp(OPS.restore, null, range); operatorList.addOp(OPS.restore, null);
} }
} else { } else {
const opArgs = operatorList.argsArray[lastIndex]; const opArgs = operatorList.argsArray[lastIndex];
opArgs[0].push(fn); opArgs[0].push(fn);
opArgs[1].push(...args); opArgs[1].push(...args);
opArgs[3].push(...range);
const minMax = opArgs[2]; const minMax = opArgs[2];
const opRange = operatorList.rangeArray[lastIndex]; const opRange = operatorList.rangeArray[lastIndex];
@ -5215,7 +5221,7 @@ class EvaluatorPreprocessor {
operation.fn = fn; operation.fn = fn;
operation.args = args; operation.args = args;
const end = this.parser.getPosition(); const end = this.parser.getEnd();
operation.range = [start, end]; operation.range = [start, end];
return true; return true;
} }

View File

@ -59,54 +59,39 @@ function getInlineImageCacheKey(bytes) {
} }
class Parser { class Parser {
constructor({ constructor({ lexer, xref, allowStreams = false, recoveryMode = false }) {
lexer,
xref,
allowStreams = false,
recoveryMode = false,
trackRanges = true,
}) {
this.lexer = lexer; this.lexer = lexer;
this.xref = xref; this.xref = xref;
this.allowStreams = allowStreams; this.allowStreams = allowStreams;
this.recoveryMode = recoveryMode; this.recoveryMode = recoveryMode;
this.withRange = trackRanges;
this.imageCache = Object.create(null); this.imageCache = Object.create(null);
this._imageId = 0; this._imageId = 0;
this.refill(); this.refill();
} }
refill() { refill() {
if (this.withRange) {
const [buf1, start1, end1] = this.lexer.getObjWithRange(); const [buf1, start1, end1] = this.lexer.getObjWithRange();
const [buf2, start2, end2] = this.lexer.getObjWithRange(); const [buf2, start2, end2] = this.lexer.getObjWithRange();
this.buf1 = buf1; this.buf1 = buf1;
this.range1 = [start1, end1]; this.range1 = [start1, end1];
this.buf2 = buf2; this.buf2 = buf2;
this.range2 = [start2, end2]; this.range2 = [start2, end2];
} else {
this.buf1 = this.lexer.getObj();
this.buf2 = this.lexer.getObj();
}
} }
shift() { shift() {
if (this.buf2 instanceof Cmd && this.buf2.cmd === "ID") { if (this.buf2 instanceof Cmd && this.buf2.cmd === "ID") {
this.buf1 = this.buf2; this.buf1 = this.buf2;
this.buf2 = null; this.buf2 = null;
if (this.withRange) { this.lastEnd = this.range1[1];
this.range1 = this.range2; this.range1 = this.range2;
this.range2 = null; this.range2 = null;
} } else {
} else if (this.withRange) {
this.buf1 = this.buf2; this.buf1 = this.buf2;
this.lastEnd = this.range1[1];
this.range1 = this.range2; this.range1 = this.range2;
const [buf2, start2, end2] = this.lexer.getObjWithRange(); const [buf2, start2, end2] = this.lexer.getObjWithRange();
this.buf2 = buf2; this.buf2 = buf2;
this.range2 = [start2, end2]; this.range2 = [start2, end2];
} else {
this.buf1 = this.buf2;
this.buf2 = this.lexer.getObj();
} }
} }
@ -128,6 +113,10 @@ class Parser {
return this.range1 ? this.range1[0] : 0; return this.range1 ? this.range1[0] : 0;
} }
getEnd() {
return this.lastEnd ?? 0;
}
getObjWithRange(cipherTransform = null) { getObjWithRange(cipherTransform = null) {
const start = this.range1[0]; const start = this.range1[0];
const obj = this.getObj(cipherTransform); const obj = this.getObj(cipherTransform);
@ -1241,14 +1230,18 @@ class Lexer {
} }
getObjWithRange() { getObjWithRange() {
// at the start of getObj() the stream has stepped beyond currentChar by one const ch = this._skipWhitespaceAndComments();
const start = this.stream.pos - 1; if (ch === EOF) {
return [ch, -1, -1];
}
// currentChar is always at pos - 1
const start = Math.max(this.stream.pos - 1, 0);
const obj = this.getObj(); const obj = this.getObj();
const end = this.stream.pos; const end = this.stream.pos - 1;
return [obj, start, end]; return [obj, start, end];
} }
getObj() { _skipWhitespaceAndComments() {
// Skip whitespace and comments. // Skip whitespace and comments.
let comment = false; let comment = false;
let ch = this.currentChar; let ch = this.currentChar;
@ -1267,7 +1260,14 @@ class Lexer {
} }
ch = this.nextChar(); ch = this.nextChar();
} }
return ch;
}
getObj() {
let ch = this._skipWhitespaceAndComments();
if (ch === EOF) {
return ch;
}
// Start reading a token. // Start reading a token.
switch (ch | 0) { switch (ch | 0) {
case 0x30: // '0' case 0x30: // '0'

View File

@ -748,6 +748,31 @@ class WorkerMessageHandler {
} }
); );
handler.on("StreamContents", function(data, sink) {
const pageIndex = data.pageIndex;
pdfManager.getPage(pageIndex).then(function(page) {
page.getContentStream().then(stream => {
let byte;
let string = "";
while ((byte = stream.getByte()) !== -1) {
string += String.fromCharCode(byte);
}
sink.enqueue(string, string.length);
sink.close();
});
});
});
handler.on("UpdateContents", function (data) {
return new Promise(resolve => {
const pageIndex = data.pageIndex;
pdfManager.getPage(pageIndex).then(function (page) {
page.setContents(data.value);
resolve();
});
});
});
handler.on("GetOperatorList", function (data, sink) { handler.on("GetOperatorList", function (data, sink) {
const pageIndex = data.pageIndex; const pageIndex = data.pageIndex;
pdfManager.getPage(pageIndex).then(function (page) { pdfManager.getPage(pageIndex).then(function (page) {
@ -767,6 +792,7 @@ class WorkerMessageHandler {
cacheKey: data.cacheKey, cacheKey: data.cacheKey,
annotationStorage: data.annotationStorage, annotationStorage: data.annotationStorage,
modifiedIds: data.modifiedIds, modifiedIds: data.modifiedIds,
contentOverride: data.contentOverride,
}) })
.then( .then(
function (operatorListInfo) { function (operatorListInfo) {

View File

@ -35,6 +35,7 @@ import {
AbortException, AbortException,
AnnotationMode, AnnotationMode,
assert, assert,
djb2Hash,
FeatureTest, FeatureTest,
getVerbosityLevel, getVerbosityLevel,
info, info,
@ -1660,6 +1661,37 @@ class PDFPageProxy {
return renderTask; return renderTask;
} }
updateContents(newContents) {
if (!newContents) {
throw new Error("Contents may not be null or undefined");
}
this._intentStates.clear();
return this._transport.updateContents(newContents, this._pageIndex);
}
getContents() {
const readableStream = this._transport.streamContents(this._pageIndex);
return new Promise(function (resolve, reject) {
function pump() {
reader.read().then(function ({ value, done }) {
if (done) {
resolve(textContent.text);
return;
}
textContent.text += value;
pump();
}, reject);
}
const reader = readableStream.getReader();
const textContent = {
text: "",
};
pump();
});
}
/** /**
* @param {GetOperatorListParameters} params - Page getOperatorList * @param {GetOperatorListParameters} params - Page getOperatorList
* parameters. * parameters.
@ -1671,6 +1703,7 @@ class PDFPageProxy {
annotationMode = AnnotationMode.ENABLE, annotationMode = AnnotationMode.ENABLE,
printAnnotationStorage = null, printAnnotationStorage = null,
isEditing = false, isEditing = false,
contentOverride = null,
} = {}) { } = {}) {
if (typeof PDFJSDev !== "undefined" && !PDFJSDev.test("GENERIC")) { if (typeof PDFJSDev !== "undefined" && !PDFJSDev.test("GENERIC")) {
throw new Error("Not implemented: getOperatorList"); throw new Error("Not implemented: getOperatorList");
@ -1688,7 +1721,8 @@ class PDFPageProxy {
annotationMode, annotationMode,
printAnnotationStorage, printAnnotationStorage,
isEditing, isEditing,
/* isOpList = */ true /* isOpList = */ true,
contentOverride
); );
let intentState = this._intentStates.get(intentArgs.cacheKey); let intentState = this._intentStates.get(intentArgs.cacheKey);
if (!intentState) { if (!intentState) {
@ -1910,6 +1944,7 @@ class PDFPageProxy {
cacheKey, cacheKey,
annotationStorageSerializable, annotationStorageSerializable,
modifiedIds, modifiedIds,
contentOverride,
}) { }) {
if (typeof PDFJSDev === "undefined" || PDFJSDev.test("TESTING")) { if (typeof PDFJSDev === "undefined" || PDFJSDev.test("TESTING")) {
assert( assert(
@ -1927,6 +1962,7 @@ class PDFPageProxy {
cacheKey, cacheKey,
annotationStorage: map, annotationStorage: map,
modifiedIds, modifiedIds,
contentOverride,
}, },
transfer transfer
); );
@ -2528,7 +2564,8 @@ class WorkerTransport {
annotationMode = AnnotationMode.ENABLE, annotationMode = AnnotationMode.ENABLE,
printAnnotationStorage = null, printAnnotationStorage = null,
isEditing = false, isEditing = false,
isOpList = false isOpList = false,
contentOverride = null
) { ) {
let renderingIntent = RenderingIntentFlag.DISPLAY; // Default value. let renderingIntent = RenderingIntentFlag.DISPLAY; // Default value.
let annotationStorageSerializable = SerializableEmpty; let annotationStorageSerializable = SerializableEmpty;
@ -2586,11 +2623,16 @@ class WorkerTransport {
modifiedIdsHash, modifiedIdsHash,
]; ];
if (contentOverride) {
cacheKeyBuf.push(djb2Hash(contentOverride));
}
return { return {
renderingIntent, renderingIntent,
cacheKey: cacheKeyBuf.join("_"), cacheKey: cacheKeyBuf.join("_"),
annotationStorageSerializable, annotationStorageSerializable,
modifiedIds, modifiedIds,
contentOverride,
}; };
} }
@ -2974,6 +3016,23 @@ class WorkerTransport {
return this.messageHandler.sendWithPromise("GetXRefEntries", null); return this.messageHandler.sendWithPromise("GetXRefEntries", null);
} }
streamContents(pageIndex) {
return this.messageHandler.sendWithStream("StreamContents", {
pageIndex,
});
}
/**
* @returns {Promise<void>} A promise that is resolved once the contents
* are updated.
*/
updateContents(newContents, pageIndex) {
return this.messageHandler.sendWithPromise("UpdateContents", {
value: newContents,
pageIndex,
});
}
saveDocument() { saveDocument() {
if (this.annotationStorage.size <= 0) { if (this.annotationStorage.size <= 0) {
warn( warn(

View File

@ -578,6 +578,16 @@ function objectFromMap(map) {
return obj; return obj;
} }
// fast and easy hash
function djb2Hash(str) {
let hash = 5381;
for (let i = 0; i < str.length; i++) {
hash = (hash << 5) + hash + str.charCodeAt(i);
hash &= hash; // Convert to 32-bit integer
}
return hash >>> 0; // Convert to unsigned
}
// Checks the endianness of the platform. // Checks the endianness of the platform.
function isLittleEndian() { function isLittleEndian() {
const buffer8 = new Uint8Array(4); const buffer8 = new Uint8Array(4);
@ -1136,6 +1146,7 @@ export {
BASELINE_FACTOR, BASELINE_FACTOR,
bytesToString, bytesToString,
createValidAbsoluteUrl, createValidAbsoluteUrl,
djb2Hash,
DocumentActionEventType, DocumentActionEventType,
FeatureTest, FeatureTest,
FONT_IDENTITY_MATRIX, FONT_IDENTITY_MATRIX,