Compare commits

..

No commits in common. "22657e2b6e9fe294f710d4649b2d2dc92cfa4175" and "85e6f3c92b0d26b1c0ca740855a008ac32dd7978" have entirely different histories.

14 changed files with 215 additions and 146 deletions

View File

@ -127,7 +127,6 @@ export default [
"perfectionist/sort-named-exports": "error", "perfectionist/sort-named-exports": "error",
"unicorn/no-abusive-eslint-disable": "error", "unicorn/no-abusive-eslint-disable": "error",
"unicorn/no-array-push-push": "error", "unicorn/no-array-push-push": "error",
"unicorn/no-array-reduce": ["error", { allowSimpleOperations: true }],
"unicorn/no-console-spaces": "error", "unicorn/no-console-spaces": "error",
"unicorn/no-instanceof-builtins": "error", "unicorn/no-instanceof-builtins": "error",
"unicorn/no-invalid-remove-event-listener": "error", "unicorn/no-invalid-remove-event-listener": "error",

View File

@ -25,6 +25,7 @@ import {
BASELINE_FACTOR, BASELINE_FACTOR,
FeatureTest, FeatureTest,
getModificationDate, getModificationDate,
IDENTITY_MATRIX,
info, info,
isArrayEqual, isArrayEqual,
LINE_DESCENT_FACTOR, LINE_DESCENT_FACTOR,
@ -43,7 +44,6 @@ import {
getInheritableProperty, getInheritableProperty,
getParentToUpdate, getParentToUpdate,
getRotationMatrix, getRotationMatrix,
IDENTITY_MATRIX,
isNumberArray, isNumberArray,
lookupMatrix, lookupMatrix,
lookupNormalRect, lookupNormalRect,

View File

@ -30,8 +30,6 @@ const PDF_VERSION_REGEXP = /^[1-9]\.\d$/;
const MAX_INT_32 = 2 ** 31 - 1; const MAX_INT_32 = 2 ** 31 - 1;
const MIN_INT_32 = -(2 ** 31); const MIN_INT_32 = -(2 ** 31);
const IDENTITY_MATRIX = [1, 0, 0, 1, 0, 0];
function getLookupTableFactory(initializer) { function getLookupTableFactory(initializer) {
let lookup; let lookup;
return function () { return function () {
@ -724,7 +722,6 @@ export {
getParentToUpdate, getParentToUpdate,
getRotationMatrix, getRotationMatrix,
getSizeInBytes, getSizeInBytes,
IDENTITY_MATRIX,
isAscii, isAscii,
isBooleanArray, isBooleanArray,
isNumberArray, isNumberArray,

View File

@ -19,6 +19,7 @@ import {
DrawOPS, DrawOPS,
FONT_IDENTITY_MATRIX, FONT_IDENTITY_MATRIX,
FormatError, FormatError,
IDENTITY_MATRIX,
info, info,
isArrayEqual, isArrayEqual,
normalizeUnicode, normalizeUnicode,
@ -35,7 +36,6 @@ import { compileType3Glyph, FontFlags } from "./fonts_utils.js";
import { ErrorFont, Font } from "./fonts.js"; import { ErrorFont, Font } from "./fonts.js";
import { import {
fetchBinaryData, fetchBinaryData,
IDENTITY_MATRIX,
isNumberArray, isNumberArray,
lookupMatrix, lookupMatrix,
lookupNormalRect, lookupNormalRect,
@ -72,6 +72,7 @@ import { BaseStream } from "./base_stream.js";
import { bidi } from "./bidi.js"; import { bidi } from "./bidi.js";
import { ColorSpace } from "./colorspace.js"; import { ColorSpace } from "./colorspace.js";
import { ColorSpaceUtils } from "./colorspace_utils.js"; import { ColorSpaceUtils } from "./colorspace_utils.js";
import { DecodeStream } from "./decode_stream.js";
import { getFontSubstitution } from "./font_substitutions.js"; import { getFontSubstitution } from "./font_substitutions.js";
import { getGlyphsUnicode } from "./glyphlist.js"; import { getGlyphsUnicode } from "./glyphlist.js";
import { getMetrics } from "./metrics.js"; import { getMetrics } from "./metrics.js";
@ -570,10 +571,7 @@ class PartialEvaluator {
localImageCache, localImageCache,
localColorSpaceCache, localColorSpaceCache,
}) { }) {
const { maxImageSize, ignoreErrors, isOffscreenCanvasSupported } = const dict = image.dict;
this.options;
const { dict } = image;
const imageRef = dict.objId; const imageRef = dict.objId;
const w = dict.get("W", "Width"); const w = dict.get("W", "Width");
const h = dict.get("H", "Height"); const h = dict.get("H", "Height");
@ -582,14 +580,15 @@ class PartialEvaluator {
warn("Image dimensions are missing, or not numbers."); warn("Image dimensions are missing, or not numbers.");
return; return;
} }
const maxImageSize = this.options.maxImageSize;
if (maxImageSize !== -1 && w * h > maxImageSize) { if (maxImageSize !== -1 && w * h > maxImageSize) {
const msg = "Image exceeded maximum allowed size and was removed."; const msg = "Image exceeded maximum allowed size and was removed.";
if (!ignoreErrors) { if (this.options.ignoreErrors) {
throw new Error(msg); warn(msg);
return;
} }
warn(msg); throw new Error(msg);
return;
} }
let optionalContent; let optionalContent;
@ -608,10 +607,52 @@ class PartialEvaluator {
// data can't be done here. Instead of creating a // data can't be done here. Instead of creating a
// complete PDFImage, only read the information needed // complete PDFImage, only read the information needed
// for later. // for later.
const interpolate = dict.get("I", "Interpolate");
const bitStrideLength = (w + 7) >> 3;
const imgArray = image.getBytes(bitStrideLength * h);
const decode = dict.getArray("D", "Decode");
if (this.parsingType3Font) {
// NOTE: Compared to other image resources we don't bother caching
// Type3-glyph image masks, since we've not come across any cases
// where that actually helps.
// In Type3-glyphs image masks are "always" inline resources,
// they're usually fairly small and aren't being re-used either.
imgData = PDFImage.createRawMask({
imgArray,
width: w,
height: h,
imageIsFromDecodeStream: image instanceof DecodeStream,
inverseDecode: decode?.[0] > 0,
interpolate,
});
args = compileType3Glyph(imgData);
if (args) {
operatorList.addImageOps(OPS.constructPath, args, optionalContent);
return;
}
warn("Cannot compile Type3 glyph.");
// If compilation failed, or was disabled, fallback to using an inline
// image mask; this case should be extremely rare.
operatorList.addImageOps(
OPS.paintImageMaskXObject,
[imgData],
optionalContent
);
return;
}
imgData = await PDFImage.createMask({ imgData = await PDFImage.createMask({
image, imgArray,
isOffscreenCanvasSupported: width: w,
isOffscreenCanvasSupported && !this.parsingType3Font, height: h,
imageIsFromDecodeStream: image instanceof DecodeStream,
inverseDecode: decode?.[0] > 0,
interpolate,
isOffscreenCanvasSupported: this.options.isOffscreenCanvasSupported,
}); });
if (imgData.isSingleOpaquePixel) { if (imgData.isSingleOpaquePixel) {
@ -636,36 +677,6 @@ class PartialEvaluator {
return; return;
} }
if (this.parsingType3Font) {
// NOTE: Compared to other image resources we don't bother caching
// Type3-glyph image masks, since we've not come across any cases
// where that actually helps.
// In Type3-glyphs image masks are "always" inline resources,
// they're usually fairly small and aren't being re-used either.
if (typeof PDFJSDev === "undefined" || PDFJSDev.test("TESTING")) {
assert(
imgData.data instanceof Uint8Array,
"Type3 glyph image mask must be a TypedArray."
);
}
args = compileType3Glyph(imgData);
if (args) {
operatorList.addImageOps(OPS.constructPath, args, optionalContent);
return;
}
warn("Cannot compile Type3 glyph.");
// If compilation failed, or was disabled, fallback to using an inline
// image mask; this case should be extremely rare.
operatorList.addImageOps(
OPS.paintImageMaskXObject,
[imgData],
optionalContent
);
return;
}
const objId = `mask_${this.idFactory.createObjId()}`; const objId = `mask_${this.idFactory.createObjId()}`;
operatorList.addDependency(objId); operatorList.addDependency(objId);
@ -725,7 +736,7 @@ class PartialEvaluator {
} catch (reason) { } catch (reason) {
const msg = `Unable to decode inline image: "${reason}".`; const msg = `Unable to decode inline image: "${reason}".`;
if (!ignoreErrors) { if (!this.options.ignoreErrors) {
throw new Error(msg); throw new Error(msg);
} }
warn(msg); warn(msg);
@ -808,7 +819,8 @@ class PartialEvaluator {
.then(async imageObj => { .then(async imageObj => {
imgData = await imageObj.createImageData( imgData = await imageObj.createImageData(
/* forceRGBA = */ false, /* forceRGBA = */ false,
isOffscreenCanvasSupported /* isOffscreenCanvasSupported = */ this.options
.isOffscreenCanvasSupported
); );
imgData.dataLen = imgData.bitmap imgData.dataLen = imgData.bitmap
? imgData.width * imgData.height * 4 ? imgData.width * imgData.height * 4

View File

@ -348,18 +348,58 @@ class PDFImage {
}); });
} }
static async createMask({ image, isOffscreenCanvasSupported = false }) { static createRawMask({
const { dict } = image; imgArray,
const width = dict.get("W", "Width"); width,
const height = dict.get("H", "Height"); height,
imageIsFromDecodeStream,
const interpolate = dict.get("I", "Interpolate"); inverseDecode,
const decode = dict.getArray("D", "Decode"); interpolate,
const inverseDecode = decode?.[0] > 0; }) {
// |imgArray| might not contain full data for every pixel of the mask, so
// we need to distinguish between |computedLength| and |actualLength|.
// In particular, if inverseDecode is true, then the array we return must
// have a length of |computedLength|.
const computedLength = ((width + 7) >> 3) * height; const computedLength = ((width + 7) >> 3) * height;
const imgArray = image.getBytes(computedLength); const actualLength = imgArray.byteLength;
const haveFullData = computedLength === actualLength;
let data, i;
if (imageIsFromDecodeStream && (!inverseDecode || haveFullData)) {
// imgArray came from a DecodeStream and its data is in an appropriate
// form, so we can just transfer it.
data = imgArray;
} else if (!inverseDecode) {
data = new Uint8Array(imgArray);
} else {
data = new Uint8Array(computedLength);
data.set(imgArray);
data.fill(0xff, actualLength);
}
// If necessary, invert the original mask data (but not any extra we might
// have added above). It's safe to modify the array -- whether it's the
// original or a copy, we're about to transfer it anyway, so nothing else
// in this thread can be relying on its contents.
if (inverseDecode) {
for (i = 0; i < actualLength; i++) {
data[i] ^= 0xff;
}
}
return { data, width, height, interpolate };
}
static async createMask({
imgArray,
width,
height,
imageIsFromDecodeStream,
inverseDecode,
interpolate,
isOffscreenCanvasSupported = false,
}) {
const isSingleOpaquePixel = const isSingleOpaquePixel =
width === 1 && width === 1 &&
height === 1 && height === 1 &&
@ -412,40 +452,17 @@ class PDFImage {
bitmap, bitmap,
}; };
} }
// Fallback to get the data almost as they're and they'll be decoded
// Get the data almost as they're and they'll be decoded
// just before being drawn. // just before being drawn.
return this.createRawMask({
// |imgArray| might not contain full data for every pixel of the mask, so imgArray,
// we need to distinguish between |computedLength| and |actualLength|. width,
// In particular, if inverseDecode is true, then the array we return must height,
// have a length of |computedLength|. inverseDecode,
const actualLength = imgArray.byteLength; imageIsFromDecodeStream,
const haveFullData = computedLength === actualLength; interpolate,
let data; });
if (image instanceof DecodeStream && (!inverseDecode || haveFullData)) {
// imgArray came from a DecodeStream and its data is in an appropriate
// form, so we can just transfer it.
data = imgArray;
} else if (!inverseDecode) {
data = new Uint8Array(imgArray);
} else {
data = new Uint8Array(computedLength);
data.set(imgArray);
data.fill(0xff, actualLength);
}
// If necessary, invert the original mask data (but not any extra we might
// have added above). It's safe to modify the array -- whether it's the
// original or a copy, we're about to transfer it anyway, so nothing else
// in this thread can be relying on its contents.
if (inverseDecode) {
for (let i = 0; i < actualLength; i++) {
data[i] ^= 0xff;
}
}
return { data, width, height, interpolate };
} }
get drawWidth() { get drawWidth() {

View File

@ -16,6 +16,7 @@
import { import {
assert, assert,
FormatError, FormatError,
IDENTITY_MATRIX,
info, info,
MathClamp, MathClamp,
unreachable, unreachable,
@ -23,7 +24,6 @@ import {
warn, warn,
} from "../shared/util.js"; } from "../shared/util.js";
import { import {
IDENTITY_MATRIX,
isBooleanArray, isBooleanArray,
isNumberArray, isNumberArray,
lookupMatrix, lookupMatrix,

View File

@ -13,7 +13,7 @@
* limitations under the License. * limitations under the License.
*/ */
import { shadow, unreachable } from "../shared/util.js"; import { objectFromMap, shadow, unreachable } from "../shared/util.js";
import { AnnotationEditor } from "./editor/editor.js"; import { AnnotationEditor } from "./editor/editor.js";
import { MurmurHash3_64 } from "../shared/murmurhash3.js"; import { MurmurHash3_64 } from "../shared/murmurhash3.js";
@ -41,17 +41,6 @@ class AnnotationStorage {
this.onSetModified = null; this.onSetModified = null;
this.onResetModified = null; this.onResetModified = null;
this.onAnnotationEditor = null; this.onAnnotationEditor = null;
if (typeof PDFJSDev === "undefined" || PDFJSDev.test("TESTING")) {
// For testing purposes.
Object.defineProperty(this, "_setValues", {
value: obj => {
for (const [key, val] of Object.entries(obj)) {
this.setValue(key, val);
}
},
});
}
} }
/** /**
@ -139,6 +128,22 @@ class AnnotationStorage {
return this.#storage.has(key); return this.#storage.has(key);
} }
/**
* @returns {Object | null}
*/
getAll() {
return this.#storage.size > 0 ? objectFromMap(this.#storage) : null;
}
/**
* @param {Object} obj
*/
setAll(obj) {
for (const [key, val] of Object.entries(obj)) {
this.setValue(key, val);
}
}
get size() { get size() {
return this.#storage.size; return this.#storage.size;
} }
@ -273,10 +278,6 @@ class AnnotationStorage {
hash: ids.join(","), hash: ids.join(","),
}); });
} }
[Symbol.iterator]() {
return this.#storage.entries();
}
} }
/** /**

View File

@ -13,13 +13,15 @@
* limitations under the License. * limitations under the License.
*/ */
import { objectFromMap } from "../shared/util.js";
class Metadata { class Metadata {
#map; #metadataMap;
#data; #data;
constructor({ parsedData, rawData }) { constructor({ parsedData, rawData }) {
this.#map = parsedData; this.#metadataMap = parsedData;
this.#data = rawData; this.#data = rawData;
} }
@ -28,11 +30,15 @@ class Metadata {
} }
get(name) { get(name) {
return this.#map.get(name) ?? null; return this.#metadataMap.get(name) ?? null;
} }
[Symbol.iterator]() { getAll() {
return this.#map.entries(); return objectFromMap(this.#metadataMap);
}
has(name) {
return this.#metadataMap.has(name);
} }
} }

View File

@ -15,6 +15,7 @@
import { import {
info, info,
objectFromMap,
RenderingIntentFlag, RenderingIntentFlag,
unreachable, unreachable,
warn, warn,
@ -300,6 +301,10 @@ class OptionalContentConfig {
return [...this.#groups.keys()]; return [...this.#groups.keys()];
} }
getGroups() {
return this.#groups.size > 0 ? objectFromMap(this.#groups) : null;
}
getGroup(id) { getGroup(id) {
return this.#groups.get(id) || null; return this.#groups.get(id) || null;
} }
@ -315,10 +320,6 @@ class OptionalContentConfig {
} }
return (this.#cachedGetHash = hash.hexdigest()); return (this.#cachedGetHash = hash.hexdigest());
} }
[Symbol.iterator]() {
return this.#groups.entries();
}
} }
export { OptionalContentConfig }; export { OptionalContentConfig };

View File

@ -25,6 +25,7 @@ const isNodeJS =
!process.versions.nw && !process.versions.nw &&
!(process.versions.electron && process.type && process.type !== "browser"); !(process.versions.electron && process.type && process.type !== "browser");
const IDENTITY_MATRIX = [1, 0, 0, 1, 0, 0];
const FONT_IDENTITY_MATRIX = [0.001, 0, 0, 0.001, 0, 0]; const FONT_IDENTITY_MATRIX = [0.001, 0, 0, 0.001, 0, 0];
// Represent the percentage of the height of a single-line field over // Represent the percentage of the height of a single-line field over
@ -577,6 +578,16 @@ function objectSize(obj) {
return Object.keys(obj).length; return Object.keys(obj).length;
} }
// Ensure that the returned Object has a `null` prototype; hence why
// `Object.fromEntries(...)` is not used.
function objectFromMap(map) {
const obj = Object.create(null);
for (const [key, value] of map) {
obj[key] = value;
}
return obj;
}
// 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);
@ -1292,6 +1303,7 @@ export {
getUuid, getUuid,
getVerbosityLevel, getVerbosityLevel,
hexNumbers, hexNumbers,
IDENTITY_MATRIX,
ImageKind, ImageKind,
info, info,
InvalidPDFException, InvalidPDFException,
@ -1301,6 +1313,7 @@ export {
LINE_FACTOR, LINE_FACTOR,
MathClamp, MathClamp,
normalizeUnicode, normalizeUnicode,
objectFromMap,
objectSize, objectSize,
OPS, OPS,
PageActionEventType, PageActionEventType,

View File

@ -738,7 +738,7 @@ class Driver {
await page.getAnnotations({ intent: "display" }); await page.getAnnotations({ intent: "display" });
} }
} }
doc.annotationStorage._setValues(task.annotationStorage); doc.annotationStorage.setAll(task.annotationStorage);
const data = await doc.saveDocument(); const data = await doc.saveDocument();
await loadingTask.destroy(); await loadingTask.destroy();
@ -919,7 +919,7 @@ class Driver {
pageColors = null; pageColors = null;
if (task.annotationStorage) { if (task.annotationStorage) {
task.pdfDoc.annotationStorage._setValues(task.annotationStorage); task.pdfDoc.annotationStorage.setAll(task.annotationStorage);
} }
let textLayerCanvas, annotationLayerCanvas, annotationLayerContext; let textLayerCanvas, annotationLayerCanvas, annotationLayerContext;

View File

@ -76,7 +76,10 @@ function pad(s, length, dir /* default: 'right' */) {
} }
function mean(array) { function mean(array) {
return array.reduce((a, b) => a + b, 0) / array.length; function add(a, b) {
return a + b;
}
return array.reduce(add, 0) / array.length;
} }
/* Comparator for row key sorting. */ /* Comparator for row key sorting. */

View File

@ -59,7 +59,7 @@ describe("AnnotationStorage", function () {
it("should set a new value in the annotation storage", function () { it("should set a new value in the annotation storage", function () {
const annotationStorage = new AnnotationStorage(); const annotationStorage = new AnnotationStorage();
annotationStorage.setValue("123A", { value: "an other string" }); annotationStorage.setValue("123A", { value: "an other string" });
const { value } = annotationStorage.getRawValue("123A"); const value = annotationStorage.getAll()["123A"].value;
expect(value).toEqual("an other string"); expect(value).toEqual("an other string");
}); });

View File

@ -16,6 +16,8 @@
import { Metadata } from "../../src/display/metadata.js"; import { Metadata } from "../../src/display/metadata.js";
import { MetadataParser } from "../../src/core/metadata_parser.js"; import { MetadataParser } from "../../src/core/metadata_parser.js";
const emptyObj = Object.create(null);
function createMetadata(data) { function createMetadata(data) {
const metadataParser = new MetadataParser(data); const metadataParser = new MetadataParser(data);
return new Metadata(metadataParser.serializable); return new Metadata(metadataParser.serializable);
@ -31,10 +33,13 @@ describe("metadata", function () {
"</rdf:Alt></dc:title></rdf:Description></rdf:RDF></x:xmpmeta>"; "</rdf:Alt></dc:title></rdf:Description></rdf:RDF></x:xmpmeta>";
const metadata = createMetadata(data); const metadata = createMetadata(data);
expect(metadata.has("dc:title")).toBeTruthy();
expect(metadata.has("dc:qux")).toBeFalsy();
expect(metadata.get("dc:title")).toEqual("Foo bar baz"); expect(metadata.get("dc:title")).toEqual("Foo bar baz");
expect(metadata.get("dc:qux")).toEqual(null); expect(metadata.get("dc:qux")).toEqual(null);
expect([...metadata]).toEqual([["dc:title", "Foo bar baz"]]); expect(metadata.getAll()).toEqual({ "dc:title": "Foo bar baz" });
}); });
it("should repair and handle invalid metadata", function () { it("should repair and handle invalid metadata", function () {
@ -46,10 +51,13 @@ describe("metadata", function () {
"</rdf:Description></rdf:RDF></x:xmpmeta>"; "</rdf:Description></rdf:RDF></x:xmpmeta>";
const metadata = createMetadata(data); const metadata = createMetadata(data);
expect(metadata.has("dc:title")).toBeTruthy();
expect(metadata.has("dc:qux")).toBeFalsy();
expect(metadata.get("dc:title")).toEqual("PDF&"); expect(metadata.get("dc:title")).toEqual("PDF&");
expect(metadata.get("dc:qux")).toEqual(null); expect(metadata.get("dc:qux")).toEqual(null);
expect([...metadata]).toEqual([["dc:title", "PDF&"]]); expect(metadata.getAll()).toEqual({ "dc:title": "PDF&" });
}); });
it("should repair and handle invalid metadata (bug 1424938)", function () { it("should repair and handle invalid metadata (bug 1424938)", function () {
@ -86,16 +94,19 @@ describe("metadata", function () {
"</x:xmpmeta>"; "</x:xmpmeta>";
const metadata = createMetadata(data); const metadata = createMetadata(data);
expect(metadata.has("dc:title")).toBeTruthy();
expect(metadata.has("dc:qux")).toBeFalsy();
expect(metadata.get("dc:title")).toEqual( expect(metadata.get("dc:title")).toEqual(
"L'Odissee thématique logo Odisséé - décembre 2008.pub" "L'Odissee thématique logo Odisséé - décembre 2008.pub"
); );
expect(metadata.get("dc:qux")).toEqual(null); expect(metadata.get("dc:qux")).toEqual(null);
expect([...metadata].sort()).toEqual([ expect(metadata.getAll()).toEqual({
["dc:creator", ["ODIS"]], "dc:creator": ["ODIS"],
["dc:title", "L'Odissee thématique logo Odisséé - décembre 2008.pub"], "dc:title": "L'Odissee thématique logo Odisséé - décembre 2008.pub",
["xap:creatortool", "PDFCreator Version 0.9.6"], "xap:creatortool": "PDFCreator Version 0.9.6",
]); });
}); });
it("should gracefully handle incomplete tags (issue 8884)", function () { it("should gracefully handle incomplete tags (issue 8884)", function () {
@ -126,7 +137,7 @@ describe("metadata", function () {
'<?xpacket end="w"?>'; '<?xpacket end="w"?>';
const metadata = createMetadata(data); const metadata = createMetadata(data);
expect([...metadata]).toEqual([]); expect(metadata.getAll()).toEqual(emptyObj);
}); });
it('should gracefully handle "junk" before the actual metadata (issue 10395)', function () { it('should gracefully handle "junk" before the actual metadata (issue 10395)', function () {
@ -157,23 +168,26 @@ describe("metadata", function () {
'</rdf:Description></rdf:RDF></x:xmpmeta><?xpacket end="w"?>'; '</rdf:Description></rdf:RDF></x:xmpmeta><?xpacket end="w"?>';
const metadata = createMetadata(data); const metadata = createMetadata(data);
expect(metadata.has("dc:title")).toBeTruthy();
expect(metadata.has("dc:qux")).toBeFalsy();
expect(metadata.get("dc:title")).toEqual(""); expect(metadata.get("dc:title")).toEqual("");
expect(metadata.get("dc:qux")).toEqual(null); expect(metadata.get("dc:qux")).toEqual(null);
expect([...metadata].sort()).toEqual([ expect(metadata.getAll()).toEqual({
["dc:creator", [""]], "dc:creator": [""],
["dc:description", ""], "dc:description": "",
["dc:format", "application/pdf"], "dc:format": "application/pdf",
["dc:subject", []], "dc:subject": [],
["dc:title", ""], "dc:title": "",
["pdf:keywords", ""], "pdf:keywords": "",
["pdf:pdfversion", "1.7"], "pdf:pdfversion": "1.7",
["pdf:producer", "PDFKit.NET 4.0.102.0"], "pdf:producer": "PDFKit.NET 4.0.102.0",
["xap:createdate", "2018-12-27T13:50:36-08:00"], "xap:createdate": "2018-12-27T13:50:36-08:00",
["xap:creatortool", ""], "xap:creatortool": "",
["xap:metadatadate", "2018-12-27T13:50:38-08:00"], "xap:metadatadate": "2018-12-27T13:50:38-08:00",
["xap:modifydate", "2018-12-27T13:50:38-08:00"], "xap:modifydate": "2018-12-27T13:50:38-08:00",
]); });
}); });
it('should correctly handle metadata containing "&apos" (issue 10407)', function () { it('should correctly handle metadata containing "&apos" (issue 10407)', function () {
@ -186,10 +200,13 @@ describe("metadata", function () {
"</rdf:Alt></dc:title></rdf:Description></rdf:RDF></x:xmpmeta>"; "</rdf:Alt></dc:title></rdf:Description></rdf:RDF></x:xmpmeta>";
const metadata = createMetadata(data); const metadata = createMetadata(data);
expect(metadata.has("dc:title")).toBeTruthy();
expect(metadata.has("dc:qux")).toBeFalsy();
expect(metadata.get("dc:title")).toEqual("'Foo bar baz'"); expect(metadata.get("dc:title")).toEqual("'Foo bar baz'");
expect(metadata.get("dc:qux")).toEqual(null); expect(metadata.get("dc:qux")).toEqual(null);
expect([...metadata]).toEqual([["dc:title", "'Foo bar baz'"]]); expect(metadata.getAll()).toEqual({ "dc:title": "'Foo bar baz'" });
}); });
it("should gracefully handle unbalanced end tags (issue 10410)", function () { it("should gracefully handle unbalanced end tags (issue 10410)", function () {
@ -212,7 +229,7 @@ describe("metadata", function () {
'</rdf:RDF></x:xmpmeta><?xpacket end="w"?>'; '</rdf:RDF></x:xmpmeta><?xpacket end="w"?>';
const metadata = createMetadata(data); const metadata = createMetadata(data);
expect([...metadata]).toEqual([]); expect(metadata.getAll()).toEqual(emptyObj);
}); });
it("should not be vulnerable to the billion laughs attack", function () { it("should not be vulnerable to the billion laughs attack", function () {
@ -241,9 +258,12 @@ describe("metadata", function () {
"</rdf:RDF>"; "</rdf:RDF>";
const metadata = createMetadata(data); const metadata = createMetadata(data);
expect(metadata.has("dc:title")).toBeTruthy();
expect(metadata.has("dc:qux")).toBeFalsy();
expect(metadata.get("dc:title")).toEqual("a&lol9;b"); expect(metadata.get("dc:title")).toEqual("a&lol9;b");
expect(metadata.get("dc:qux")).toEqual(null); expect(metadata.get("dc:qux")).toEqual(null);
expect([...metadata]).toEqual([["dc:title", "a&lol9;b"]]); expect(metadata.getAll()).toEqual({ "dc:title": "a&lol9;b" });
}); });
}); });