From abb98d96666004eb53447e4d3de4e43bd65a0379 Mon Sep 17 00:00:00 2001 From: Weiyi Li <1093656961@qq.com> Date: Mon, 21 Sep 2026 14:31:26 +0000 Subject: [PATCH] Select a single value from XMP language alternatives Fixes #20801. `dc:title`, `dc:description`, `dc:rights` and `xmpRights:UsageTerms` are language alternatives, so pick the `x-default` entry (compared case-insensitively, as the XMP specification requires), or the first one when there is no default, instead of concatenating all of them. Continues the approach proposed by @nyxsky404 in https://github.com/mozilla/pdf.js/pull/20874. --- src/core/metadata_parser.js | 30 ++++++++++++++++++- test/unit/metadata_spec.js | 58 +++++++++++++++++++++++++++++++++++++ 2 files changed, 87 insertions(+), 1 deletion(-) diff --git a/src/core/metadata_parser.js b/src/core/metadata_parser.js index f50927e43db57..a4cbfa12d59c0 100644 --- a/src/core/metadata_parser.js +++ b/src/core/metadata_parser.js @@ -21,7 +21,10 @@ class MetadataParser { data = this._repair(data); // Convert the string to an XML document. - const parser = new SimpleXMLParser({ lowerCaseName: true }); + const parser = new SimpleXMLParser({ + lowerCaseName: true, + hasAttributes: true, + }); const xmlDocument = parser.parseFromString(data); this._metadataMap = new Map(); @@ -99,6 +102,25 @@ class MetadataParser { ); } + _parseLangAlt(entry) { + const alt = entry.children.find(node => node.nodeName === "rdf:alt"); + const alternatives = alt ? this._getSequence(alt) : []; + // Language alternatives represent the same value in different languages. + // Prefer the default value, without changing the string-valued API. + const selected = + alternatives.find(node => + node.attributes.some( + ({ name, value }) => + name === "xml:lang" && value.toLowerCase() === "x-default" + ) + ) || alternatives[0]; + + this._metadataMap.set( + entry.nodeName, + (selected || entry).textContent.trim() + ); + } + _parse(xmlDocument) { let rdf = xmlDocument.documentElement; @@ -128,6 +150,12 @@ class MetadataParser { case "dc:subject": this._parseArray(entry); continue; + case "dc:title": + case "dc:description": + case "dc:rights": + case "xmprights:usageterms": + this._parseLangAlt(entry); + continue; } this._metadataMap.set(name, entry.textContent.trim()); } diff --git a/test/unit/metadata_spec.js b/test/unit/metadata_spec.js index 4bb5afde7cb03..618113907361e 100644 --- a/test/unit/metadata_spec.js +++ b/test/unit/metadata_spec.js @@ -37,6 +37,64 @@ describe("metadata", function () { expect([...metadata]).toEqual([["dc:title", "Foo bar baz"]]); }); + it("should select the default language alternative (issue 20801)", function () { + for (const name of [ + "dc:title", + "dc:description", + "dc:rights", + "xmpRights:UsageTerms", + ]) { + for (const alternatives of [ + 'Hello World' + + 'Hello World', + 'Bonjour le monde' + + 'Hello World', + 'Bonjour le monde' + + 'Hello World', + ]) { + const data = + "" + + `<${name}>${alternatives}` + + ""; + + expect(createMetadata(data).get(name.toLowerCase())) + .withContext(`${name}: ${alternatives}`) + .toEqual("Hello World"); + } + } + }); + + it("should use the first language alternative if there is no default", function () { + for (const name of [ + "dc:title", + "dc:description", + "dc:rights", + "xmpRights:UsageTerms", + ]) { + const data = + "" + + `<${name}>` + + 'Hello World' + + 'Bonjour le monde' + + `` + + ""; + + expect(createMetadata(data).get(name.toLowerCase())) + .withContext(name) + .toEqual("Hello World"); + } + }); + + it("should handle whitespace before the language alternatives", function () { + const data = + "\n \n" + + ' English title\n' + + ' Default title \n' + + "\n"; + + expect(createMetadata(data).get("dc:title")).toEqual("Default title"); + }); + it("should repair and handle invalid metadata", function () { const data = "" +