Skip to content

Commit

Permalink
[Editor] Support svg images in the stamp annotation
Browse files Browse the repository at this point in the history
createImageBitmap doesn't work with svg files (see bug 1841972), so we need to workaround
this in using an Image.
When printing/saving we must rasterize the image, hence we get the biggest bitmap as image
reference to avoid duplications or poor quality on rendering.
  • Loading branch information
calixteman committed Jul 6, 2023
1 parent 8281bb8 commit eaf9ea2
Show file tree
Hide file tree
Showing 3 changed files with 111 additions and 12 deletions.
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
71 changes: 64 additions & 7 deletions src/display/editor/stamp.js
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,8 @@ class StampEditor extends AnnotationEditor {

#resizeTimeoutId = null;

#isSvg = false;

static _type = "stamp";

constructor(params) {
Expand Down Expand Up @@ -66,7 +68,11 @@ 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;
Expand All @@ -88,7 +94,11 @@ 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();
}
resolve();
Expand Down Expand Up @@ -142,7 +152,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 +316,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 +336,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 +352,26 @@ class StampEditor extends AnnotationEditor {
return canvas.toDataURL();
}

if (this.#isSvg) {
const [pageWidth, pageHeight] = this.pageDimensions;
const width = Math.round(this.width * pageWidth);
const height = Math.round(this.height * pageHeight);
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 +394,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 +421,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 +436,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;
}

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

0 comments on commit eaf9ea2

Please sign in to comment.