Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Editor] Support svg images in the stamp annotation #16650

Merged
merged 1 commit into from
Jul 7, 2023
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
13 changes: 11 additions & 2 deletions src/display/annotation_storage.js
Original file line number Diff line number Diff line change
Expand Up @@ -183,6 +183,7 @@ class AnnotationStorage {
hash = new MurmurHash3_64(),
transfers = [];
const context = Object.create(null);
let hasBitmap = false;

for (const [key, val] of this.#storage) {
const serialized =
Expand All @@ -193,12 +194,20 @@ class AnnotationStorage {
map.set(key, serialized);

hash.update(`${key}:${JSON.stringify(serialized)}`);
hasBitmap ||= !!serialized.bitmap;
}
}

if (serialized.bitmap) {
transfers.push(serialized.bitmap);
if (hasBitmap) {
// We must transfer the bitmap data separately, since it can be changed
// during serialization with SVG images.
for (const value of map.values()) {
if (value.bitmap) {
transfers.push(value.bitmap);
}
}
}

return map.size > 0
? { map, hash: hash.hexdigest(), transfers }
: SerializableEmpty;
Expand Down
90 changes: 82 additions & 8 deletions src/display/editor/stamp.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@

import { AnnotationEditor } from "./editor.js";
import { AnnotationEditorType } from "../../shared/util.js";
import { PixelsPerInch } from "../display_utils.js";
import { StampAnnotationElement } from "../annotation_layer.js";

/**
Expand All @@ -35,6 +36,8 @@ class StampEditor extends AnnotationEditor {

#resizeTimeoutId = null;

#isSvg = false;

static _type = "stamp";

constructor(params) {
Expand Down Expand Up @@ -66,13 +69,22 @@ class StampEditor extends AnnotationEditor {
this.remove();
return;
}
({ bitmap: this.#bitmap, id: this.#bitmapId } = data);
({
bitmap: this.#bitmap,
id: this.#bitmapId,
isSvg: this.#isSvg,
} = data);
this.#createCanvas();
});
return;
}

const input = document.createElement("input");
if (typeof PDFJSDev !== "undefined" && PDFJSDev.test("TESTING")) {
input.hidden = true;
input.id = "stampEditorFileInput";
document.body.append(input);
}
input.type = "file";
input.accept = "image/*";
this.#bitmapPromise = new Promise(resolve => {
Expand All @@ -88,9 +100,16 @@ class StampEditor extends AnnotationEditor {
this.remove();
return;
}
({ bitmap: this.#bitmap, id: this.#bitmapId } = data);
({
bitmap: this.#bitmap,
id: this.#bitmapId,
isSvg: this.#isSvg,
} = data);
this.#createCanvas();
}
if (typeof PDFJSDev !== "undefined" && PDFJSDev.test("TESTING")) {
input.remove();
}
resolve();
});
input.addEventListener("cancel", () => {
Expand All @@ -99,7 +118,9 @@ class StampEditor extends AnnotationEditor {
resolve();
});
});
input.click();
if (typeof PDFJSDev === "undefined" || !PDFJSDev.test("TESTING")) {
input.click();
}
}

/** @inheritdoc */
Expand Down Expand Up @@ -142,7 +163,11 @@ class StampEditor extends AnnotationEditor {

/** @inheritdoc */
isEmpty() {
return this.#bitmapPromise === null && this.#bitmap === null;
return (
this.#bitmapPromise === null &&
this.#bitmap === null &&
this.#bitmapUrl === null
);
}

/** @inheritdoc */
Expand Down Expand Up @@ -302,7 +327,9 @@ class StampEditor extends AnnotationEditor {
}
canvas.width = width;
canvas.height = height;
const bitmap = this.#scaleBitmap(width, height);
const bitmap = this.#isSvg
? this.#bitmap
: this.#scaleBitmap(width, height);
const ctx = canvas.getContext("2d");
ctx.filter = this._uiManager.hcmFilter;
ctx.drawImage(
Expand All @@ -320,6 +347,12 @@ class StampEditor extends AnnotationEditor {

#serializeBitmap(toUrl) {
if (toUrl) {
if (this.#isSvg) {
const url = this._uiManager.imageManager.getSvgUrl(this.#bitmapId);
if (url) {
return url;
}
}
// We convert to a data url because it's sync and the url can live in the
// clipboard.
const canvas = document.createElement("canvas");
Expand All @@ -330,6 +363,32 @@ class StampEditor extends AnnotationEditor {
return canvas.toDataURL();
}

if (this.#isSvg) {
const [pageWidth, pageHeight] = this.pageDimensions;
// Multiply by PixelsPerInch.PDF_TO_CSS_UNITS in order to increase the
// image resolution when rasterizing it.
const width = Math.round(
this.width * pageWidth * PixelsPerInch.PDF_TO_CSS_UNITS
);
const height = Math.round(
this.height * pageHeight * PixelsPerInch.PDF_TO_CSS_UNITS
);
const offscreen = new OffscreenCanvas(width, height);
const ctx = offscreen.getContext("2d");
ctx.drawImage(
this.#bitmap,
0,
0,
this.#bitmap.width,
this.#bitmap.height,
0,
0,
width,
height
);
return offscreen.transferToImageBitmap();
}

return structuredClone(this.#bitmap);
}

Expand All @@ -352,12 +411,13 @@ class StampEditor extends AnnotationEditor {
return null;
}
const editor = super.deserialize(data, parent, uiManager);
const { rect, bitmapUrl, bitmapId } = data;
const { rect, bitmapUrl, bitmapId, isSvg } = data;
if (bitmapId && uiManager.imageManager.isValidId(bitmapId)) {
editor.#bitmapId = bitmapId;
} else {
editor.#bitmapUrl = bitmapUrl;
}
editor.#isSvg = isSvg;

const [parentWidth, parentHeight] = editor.pageDimensions;
editor.width = (rect[2] - rect[0]) / parentWidth;
Expand All @@ -378,6 +438,7 @@ class StampEditor extends AnnotationEditor {
pageIndex: this.pageIndex,
rect: this.getRect(0, 0),
rotation: this.rotation,
isSvg: this.#isSvg,
};

if (isForCopying) {
Expand All @@ -392,12 +453,25 @@ class StampEditor extends AnnotationEditor {
return serialized;
}

context.stamps ||= new Set();
context.stamps ||= new Map();
const area = this.#isSvg
? (serialized.rect[2] - serialized.rect[0]) *
(serialized.rect[3] - serialized.rect[1])
: null;
if (!context.stamps.has(this.#bitmapId)) {
// We don't want to have multiple copies of the same bitmap in the
// annotationMap, hence we only add the bitmap the first time we meet it.
context.stamps.add(this.#bitmapId);
context.stamps.set(this.#bitmapId, { area, serialized });
serialized.bitmap = this.#serializeBitmap(/* toUrl = */ false);
} else if (this.#isSvg) {
// If we have multiple copies of the same svg but with different sizes,
// then we want to keep the biggest one.
const prevData = context.stamps.get(this.#bitmapId);
if (area > prevData.area) {
prevData.area = area;
prevData.serialized.bitmap.close();
prevData.serialized.bitmap = this.#serializeBitmap(/* toUrl = */ false);
}
}
return serialized;
}
Expand Down
39 changes: 36 additions & 3 deletions src/display/editor/tools.js
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,7 @@ class ImageManager {
bitmap: null,
id: `image_${this.#baseId}_${this.#id++}`,
refCounter: 0,
isSvg: false,
};
let image;
if (typeof rawData === "string") {
Expand All @@ -102,11 +103,35 @@ class ImageManager {
}
image = await response.blob();
} else {
data.file = rawData;
image = data.file = rawData;
}

calixteman marked this conversation as resolved.
Show resolved Hide resolved
image = rawData;
if (image.type === "image/svg+xml") {
// Unfortunately, createImageBitmap doesn't work with SVG images.
// (see https://bugzilla.mozilla.org/1841972).
const fileReader = new FileReader();
const dataUrlPromise = new Promise(resolve => {
fileReader.onload = () => {
data.svgUrl = fileReader.result;
resolve();
};
});
fileReader.readAsDataURL(image);
const url = URL.createObjectURL(image);
image = new Image();
const imagePromise = new Promise(resolve => {
image.onload = () => {
URL.revokeObjectURL(url);
data.bitmap = image;
data.isSvg = true;
resolve();
};
});
image.src = url;
await Promise.all([imagePromise, dataUrlPromise]);
} else {
data.bitmap = await createImageBitmap(image);
}
data.bitmap = await createImageBitmap(image);
data.refCounter = 1;
} catch (e) {
console.error(e);
Expand Down Expand Up @@ -145,6 +170,14 @@ class ImageManager {
return this.getFromUrl(data.url);
}

getSvgUrl(id) {
const data = this.#cache.get(id);
if (!data?.isSvg) {
return null;
}
return data.svgUrl;
}

deleteId(id) {
this.#cache ||= new Map();
const data = this.#cache.get(id);
Expand Down
1 change: 1 addition & 0 deletions test/images/firefox_logo.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
1 change: 1 addition & 0 deletions test/integration-boot.js
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ async function runTests(results) {
"freetext_editor_spec.js",
"ink_editor_spec.js",
"scripting_spec.js",
"stamp_editor_spec.js",
],
});

Expand Down
Loading