Merge pull request #21566 from calixteman/fix/pdf-editor-unicode-name-trees

Preserve Unicode destination and attachment names
This commit is contained in:
calixteman 2026-07-13 21:46:35 +02:00 committed by GitHub
commit b19cc7c746
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 88 additions and 39 deletions

View File

@ -2652,7 +2652,15 @@ class PDFEditor {
#makeNameNumTree(map, areNames) {
const allEntries = map.sort(
areNames
? ([keyA], [keyB]) => keyA.localeCompare(keyB)
? ([keyA], [keyB]) => {
if (keyA < keyB) {
return -1;
}
if (keyA > keyB) {
return 1;
}
return 0;
}
: ([keyA], [keyB]) => keyA - keyB
);
const maxLeaves =
@ -2729,7 +2737,7 @@ class PDFEditor {
/* keepEscapeSequence = */ true
);
for (let i = 1; ; i++) {
const deduped = `${displayName}_${i}`;
const deduped = stringToAsciiOrUTF16BE(`${displayName}_${i}`);
if (!embeddedFiles.has(deduped)) {
name = deduped;
break;
@ -2775,7 +2783,10 @@ class PDFEditor {
this.namesDict.set(
"Dests",
this.#makeNameNumTree(
Array.from(namedDestinations.entries()),
Array.from(namedDestinations, ([name, dest]) => [
stringToAsciiOrUTF16BE(name),
dest,
]),
/* areNames = */ true
)
);

View File

@ -103,25 +103,7 @@ describe("api", function () {
return count;
}
function buildSharedImageResourcePdf() {
const streamObject = (num, dict, data) =>
`${num} 0 obj\n<< ${dict} /Length ${data.length} >>\n` +
`stream\n${data}\nendstream\nendobj\n`;
const objects = [
"1 0 obj\n<< /Type /Catalog /Pages 2 0 R >>\nendobj\n",
"2 0 obj\n<< /Type /Pages /Kids [3 0 R] /Count 1 >>\nendobj\n",
"3 0 obj\n<< /Type /Page /Parent 2 0 R /MediaBox [0 0 10 10] " +
"/Resources << /XObject << /Im0 4 0 R /Im1 4 0 R >> >> " +
"/Contents 5 0 R >>\nendobj\n",
streamObject(
4,
"/Type /XObject /Subtype /Image /Width 1 /Height 1 " +
"/ColorSpace /DeviceRGB /BitsPerComponent 8 /Filter /ASCIIHexDecode",
"FF0000>"
),
streamObject(5, "", "q 10 0 0 10 0 0 cm /Im0 Do Q"),
];
function assemblePdf(objects) {
let pdf = "%PDF-1.7\n";
const offsets = [];
for (const obj of objects) {
@ -140,6 +122,26 @@ describe("api", function () {
return stringToBytes(pdf);
}
function buildSharedImageResourcePdf() {
const streamObject = (num, dict, data) =>
`${num} 0 obj\n<< ${dict} /Length ${data.length} >>\n` +
`stream\n${data}\nendstream\nendobj\n`;
return assemblePdf([
"1 0 obj\n<< /Type /Catalog /Pages 2 0 R >>\nendobj\n",
"2 0 obj\n<< /Type /Pages /Kids [3 0 R] /Count 1 >>\nendobj\n",
"3 0 obj\n<< /Type /Page /Parent 2 0 R /MediaBox [0 0 10 10] " +
"/Resources << /XObject << /Im0 4 0 R /Im1 4 0 R >> >> " +
"/Contents 5 0 R >>\nendobj\n",
streamObject(
4,
"/Type /XObject /Subtype /Image /Width 1 /Height 1 " +
"/ColorSpace /DeviceRGB /BitsPerComponent 8 /Filter /ASCIIHexDecode",
"FF0000>"
),
streamObject(5, "", "q 10 0 0 10 0 0 cm /Im0 Do Q"),
]);
}
function getNamedNodeInXML(node, path) {
for (const component of path.split(".")) {
if (!node.childNodes) {
@ -6404,23 +6406,7 @@ small scripts as well as for`);
"5 0 obj\n<< /Type /Annot /Subtype /Link /Rect [10 0 20 10] " +
"/A << /S /GoToR /F (other.pdf) /D (target) >> >>\nendobj\n",
];
let pdfData = "%PDF-1.7\n";
const offsets = [];
for (const object of objects) {
offsets.push(pdfData.length);
pdfData += object;
}
const xrefOffset = pdfData.length;
pdfData += `xref\n0 ${objects.length + 1}\n`;
pdfData += "0000000000 65535 f \n";
for (const offset of offsets) {
pdfData += `${offset.toString().padStart(10, "0")} 00000 n \n`;
}
pdfData +=
`trailer\n<< /Size ${objects.length + 1} /Root 1 0 R >>\n` +
`startxref\n${xrefOffset}\n%%EOF\n`;
let loadingTask = getDocument({ data: stringToBytes(pdfData) });
let loadingTask = getDocument({ data: assemblePdf(objects) });
let pdfDoc = await loadingTask.promise;
const data = await pdfDoc.extractPages([{ document: null }]);
await loadingTask.destroy();
@ -6435,6 +6421,33 @@ small scripts as well as for`);
await loadingTask.destroy();
});
it("preserves Unicode destination names", async function () {
const objects = [
"1 0 obj\n<< /Type /Catalog /Pages 2 0 R " +
"/Names << /Dests 4 0 R >> >>\nendobj\n",
"2 0 obj\n<< /Type /Pages /Kids [3 0 R] /Count 1 >>\nendobj\n",
"3 0 obj\n<< /Type /Page /Parent 2 0 R " +
"/MediaBox [0 0 100 100] /Annots [5 0 R] >>\nendobj\n",
"4 0 obj\n<< /Names [<FEFF540D> [3 0 R /Fit]] >>\nendobj\n",
"5 0 obj\n<< /Type /Annot /Subtype /Link /Rect [0 0 10 10] " +
"/Dest <FEFF540D> >>\nendobj\n",
];
let loadingTask = getDocument({ data: assemblePdf(objects) });
let pdfDoc = await loadingTask.promise;
expect(Object.keys(await pdfDoc.getDestinations()))
.withContext("before extraction")
.toEqual(["名"]);
const data = await pdfDoc.extractPages([{ document: null }]);
await loadingTask.destroy();
loadingTask = getDocument({ data });
pdfDoc = await loadingTask.promise;
expect(Object.keys(await pdfDoc.getDestinations()))
.withContext("after extraction")
.toEqual(["名"]);
await loadingTask.destroy();
});
it("keeps colliding deduplicated destination names unique", async function () {
let loadingTask = getDocument(
buildGetDocumentParams("named_dest_collision_for_editor.pdf")
@ -7248,6 +7261,31 @@ small scripts as well as for`);
await loadingTask.destroy();
});
it("preserves Unicode EmbeddedFiles (attachments) names when deduplicating", async function () {
const objects = [
"1 0 obj\n<< /Type /Catalog /Pages 2 0 R " +
"/Names << /EmbeddedFiles 4 0 R >> >>\nendobj\n",
"2 0 obj\n<< /Type /Pages /Kids [3 0 R] /Count 1 >>\nendobj\n",
"3 0 obj\n<< /Type /Page /Parent 2 0 R " +
"/MediaBox [0 0 100 100] >>\nendobj\n",
"4 0 obj\n<< /Names [<FEFF540D> 5 0 R] >>\nendobj\n",
"5 0 obj\n<< /Type /Filespec /F (file) /UF <FEFF540D> >>\nendobj\n",
];
let loadingTask = getDocument({ data: assemblePdf(objects) });
let pdfDoc = await loadingTask.promise;
const data = await pdfDoc.extractPages([
{ document: null },
{ document: null },
]);
await loadingTask.destroy();
loadingTask = getDocument({ data });
pdfDoc = await loadingTask.promise;
const attachments = await pdfDoc.getAttachments();
expect(Array.from(attachments.keys()).sort()).toEqual(["名", "名_1"]);
await loadingTask.destroy();
});
});
describe("AcroForm", function () {