Skip to content

Commit

Permalink
Web Share Target graphics are resized using ImageBitmap & OffscreenCa…
Browse files Browse the repository at this point in the history
…nvas
  • Loading branch information
DougReeder committed Feb 4, 2024
1 parent 52b12a6 commit 8a7aa4f
Show file tree
Hide file tree
Showing 4 changed files with 75 additions and 25 deletions.
6 changes: 5 additions & 1 deletion src/util.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
// util.js — various utilty funtions for Notes Together
// Copyright © 2021–2024 Doug Reeder

/* eslint-env browser, worker */

// ASCII, Unicode, no-break & soft hyphens
// ASCII apostrophe, right-single-quote, modifier-letter-apostrophe
Expand Down Expand Up @@ -100,7 +104,7 @@ function visualViewportMatters() {

// const urlRunningTextRE = /(([^:/?#]+):)?(\/\/([^/?#]*))?([^?#]*)(\?([^#]*))?(#(.*))?/g
// Numeric IP addresses are not allowed — local network addresses are unstable and others are a security problem
const urlRunningTextRE = /(\b(?:([A-Za-z][A-Za-z+-]{2,25}:(?:\/\/)?)(?:[-;:&=+$,\w]+@)?|www\.)([A-Za-z0-9-]{1,63}\.)+[A-Za-z]{2,6}(?::\d{1,5})?)(\/[+~%/.\w_-]*)?(\?[-+=&;%@.,!\w_]*)?(#[.!/\\\w=,*-]*)?/g
const urlRunningTextRE = /(\b(?:([A-Za-z][A-Za-z+-]{2,25}:(?:\/\/)?)(?:[\w;:&=+$,-]+@)?|www\.)([A-Za-z0-9-]{1,63}\.)+[A-Za-z]{2,6}(?::\d{1,5})?)(\/[\w+~%/.)(-]*)?(\?[-+=&;%@.,!\w_]*)?(#[.!/\\\w=,*-]*)?/g

const BLOCKED_SCHEMES = ['javascript:', 'file:', 'data:'];

Expand Down
8 changes: 8 additions & 0 deletions src/util.test.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
// util.js — tests for various utilty funtions for Notes Together
// Copyright © 2021–2024 Doug Reeder

import {adHocTextReplacements, isLikelyMarkdown, normalizeUrl, urlRunningTextRE, visualViewportMatters} from "./util";

describe("isLikelyMarkdown", () => {
Expand Down Expand Up @@ -176,6 +179,11 @@ describe("urlRunningTextRE followed by normalizeUrl", () => {
expect(normalizeUrl(urlRunningTextRE.exec('www.sun.com')?.[0] || '')).toEqual('https://www.sun.com/');
});

it("should allow parentheses in path", () => {
urlRunningTextRE.lastIndex = 0;
expect(normalizeUrl(urlRunningTextRE.exec('https://en.wikipedia.org/wiki/Hose_(clothing)')?.[0] || '')).toEqual('https://en.wikipedia.org/wiki/Hose_(clothing)');
});

it("should prepend https:// if needed", () => {
urlRunningTextRE.lastIndex = 0;
expect(normalizeUrl(urlRunningTextRE.exec('www.example.com/quux')?.[0] || ''))
Expand Down
66 changes: 52 additions & 14 deletions src/util/imageFileToDataUrl.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
// imageFileToDataUrl.js - downscales an image & converts to data URL
// Copyright © 2017-2024 Doug Reeder

/* eslint-env browser, worker */

const MAX_SIZE = 200_000; // max content size / 3

Expand All @@ -16,20 +16,25 @@ async function imageFileToDataUrl(file) {
// }
}

let dataUrl;
// avoids converting vector to raster if reasonably possible
// URL.createObjectURL is not available in a Service Worker
if ((file.size < (MAX_SIZE * 1.4) && file.type === 'image/svg+xml') || 'function' !== typeof URL.createObjectURL) {
const dataUrl = await fileToDataUrl(file);
return {dataUrl, alt: texts.join('\n')};
if ((file.size < (MAX_SIZE * 1.4) && file.type === 'image/svg+xml')) {
dataUrl = await fileToDataUrl(file);
} else if ('function' === typeof URL.createObjectURL) { // not available in a Service Worker
const objectUrl = URL.createObjectURL(file);
dataUrl = await evaluateImage(file, objectUrl);
URL.revokeObjectURL(objectUrl);
} else if ('function' === typeof createImageBitmap) { // not available in Node (for testing)
dataUrl = await evaluateImageBitmap(file);
} else {
dataUrl = await fileToDataUrl(file);
}

const objectUrl = URL.createObjectURL(file);
const dataUrl = await evaluateImage(file, objectUrl);
URL.revokeObjectURL(objectUrl);

return {dataUrl, alt: texts.join('\n')};
}

const NOT_CROSS_BROWSER = ['image/tiff', 'image/jp2', 'image/jxl', 'image/avci', 'image/heif', 'image/heic'];

function evaluateImage(blob, objectURL) {
return new Promise((resolve, reject) => {
const img = new Image();
Expand All @@ -39,8 +44,7 @@ function evaluateImage(blob, objectURL) {
// Modern browsers respect the orientation data in EXIF.
// console.log("img onload size:", this.width, this.height);

if (this.width > 1280 || this.height > 1280 || blob.size > MAX_SIZE ||
['image/tiff', 'image/jp2', 'image/jxl', 'image/avci', 'image/heif', 'image/heic'].includes(blob.type)) {
if (this.width > 1280 || this.height > 1280 || blob.size > MAX_SIZE || NOT_CROSS_BROWSER.includes(blob.type)) {
resolve(resize(img, blob.type));
} else {
resolve(await fileToDataUrl(blob));
Expand Down Expand Up @@ -76,12 +80,12 @@ function resize(img, fileType) {
const canvas = document.createElement('canvas');
if (img.width >= img.height) { // constrain width
canvas.width = Math.min(1280, img.width); // logical width
canvas.height = (img.height / img.width) * canvas.width;
canvas.height = Math.round((img.height / img.width) * canvas.width);
} else { // constrain height
canvas.height = Math.min(1280, img.height); // logical height
canvas.width = (img.width / img.height) * canvas.height;
canvas.width = Math.round((img.width / img.height) * canvas.height);
}
console.info(`resizing to ${canvas.width}×${canvas.height}`);
console.info(`resizing to ${canvas.width}×${canvas.height} using Canvas`);

const context = canvas.getContext('2d');
if (!(['image/jpeg', 'image/bmp'].includes(fileType))) { // might have transparent background
Expand All @@ -97,4 +101,38 @@ function resize(img, fileType) {
return dataUrl;
}

async function evaluateImageBitmap(file) {
let imageBitmap = await createImageBitmap(file);
if ((imageBitmap.width > 1280 || imageBitmap.height > 1280 || file.size > MAX_SIZE ||
NOT_CROSS_BROWSER.includes(file.type)) &&
!('image/jpeg' === file.type && imageBitmap.height > imageBitmap.width && file.size <= MAX_SIZE)) {
// portrait JPEGs don't always come through with the correct orientation
let canvasWidth, canvasHeight;
if (imageBitmap.width > imageBitmap.height) {
canvasWidth = Math.min(imageBitmap.width, 1280);
canvasHeight = Math.round(imageBitmap.height * canvasWidth / imageBitmap.width);
} else {
canvasHeight = Math.min(imageBitmap.height, 1280);
canvasWidth = Math.round(imageBitmap.width * canvasHeight / imageBitmap.height);
}
if (canvasWidth !== imageBitmap.width || canvasHeight !== imageBitmap.height) {
imageBitmap.close();
imageBitmap = await createImageBitmap(file, {resizeWidth: canvasWidth, resizeHeight: canvasHeight});
}
const canvas = new OffscreenCanvas(canvasWidth, canvasHeight);
console.info(`resizing “${file.name}” to ${canvas.width}×${canvas.height} using OffscreenCanvas`);
const context = canvas.getContext("bitmaprenderer");
context.transferFromImageBitmap(imageBitmap); // consumes bitmap
let blob = await canvas.convertToBlob({type: 'image/webp', quality: 0.4});
if ('image/png' === blob.type) {
blob = await canvas.convertToBlob({type: 'image/jpeg', quality: 0.4});
}
return await fileToDataUrl(blob);
} else {
console.info(`importing “${file.name}” at original resolution of ${imageBitmap.width}×${imageBitmap.height}`);
imageBitmap.close();
return await fileToDataUrl(file);
}
}

export {imageFileToDataUrl, fileToDataUrl, evaluateImage};
20 changes: 10 additions & 10 deletions src/util/imageFileToDataUrl.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,14 +16,14 @@ describe("imageFileToDataUrl", () => {
expect(alt).toEqual("picture.icon.svg");
});

// it("should return small JPEG unchanged", async () => {
// const jpegDataUri = 'data:image/jpeg;base64,/9j/4AAQSkZJRgABAQEASABIAAD/2wBDAP//////////////////////////////////////////////////////////////////////////////////////2wBDAf//////////////////////////////////////////////////////////////////////////////////////wAARCAADAAcDAREAAhEBAxEB/8QAFAABAAAAAAAAAAAAAAAAAAAAAv/EABcQAAMBAAAAAAAAAAAAAAAAAAABITH/xAAUAQEAAAAAAAAAAAAAAAAAAAAA/8QAFBEBAAAAAAAAAAAAAAAAAAAAAP/aAAwDAQACEQMRAD8ATmAf/9k=';
// const jpegFile = dataURItoFile(jpegDataUri, "small.jpeg");
//
//
// const {dataUrl, alt} = await imageFileToDataUrl(jpegFile);
//
// expect(dataUrl).toEqual(jpegDataUri);
// expect(alt).toEqual("small");
// });
it("should return small JPEG unchanged", async () => {
const jpegDataUri = 'data:image/jpeg;base64,/9j/4AAQSkZJRgABAQEASABIAAD/2wBDAP//////////////////////////////////////////////////////////////////////////////////////2wBDAf//////////////////////////////////////////////////////////////////////////////////////wAARCAADAAcDAREAAhEBAxEB/8QAFAABAAAAAAAAAAAAAAAAAAAAAv/EABcQAAMBAAAAAAAAAAAAAAAAAAABITH/xAAUAQEAAAAAAAAAAAAAAAAAAAAA/8QAFBEBAAAAAAAAAAAAAAAAAAAAAP/aAAwDAQACEQMRAD8ATmAf/9k=';
const jpegFile = dataURItoFile(jpegDataUri, "small.jpeg");


const {dataUrl, alt} = await imageFileToDataUrl(jpegFile);

expect(dataUrl).toEqual(jpegDataUri);
expect(alt).toEqual("small.jpeg");
});
});

0 comments on commit 8a7aa4f

Please sign in to comment.