feat: bound decoded screenshot pixels (Closes #837)
CI / lint (pull_request) Successful in 1m46s
CI / build-release (pull_request) Successful in 6s
CI / release-candidate (pull_request) Has been skipped

This commit is contained in:
timmy committed 2026-08-14 16:35:21 +00:00
1 parent 5a579362ff
commit a476170fca
3 files changed
+102 -16

No files matched your search

+6 -3
View File
@@ -8,6 +8,7 @@
'use strict';
const MAX_BYTES = 2 * 1024 * 1024;
const MAX_PIXELS = issueEvidenceReview.MAX_PIXELS;
const MAX_FILES = 5;
const MAX_FILES_MESSAGE = 'Up to 5 screenshots. Remove one before adding another.';
const IMAGE_TYPES = new Set(['image/png', 'image/jpeg', 'image/webp']);
@@ -38,6 +39,7 @@
function create(options) {
const upload = options.upload;
const maxFiles = options.maxFiles === MAX_FILES ? MAX_FILES : 1;
const inspectPixels = options.inspectPixels === true;
const optimizeSelectedImage = options.optimizeImage || optimizeImage;
const createOperationId = options.createOperationId || (() => {
if (typeof globalThis !== 'undefined' && globalThis.crypto?.randomUUID) {
@@ -72,7 +74,7 @@
if (!Number.isFinite(file.size) || file.size <= 0) {
throw new Error('Choose a screenshot that is 2 MB or smaller.');
}
if (file.size > MAX_BYTES) {
if (file.size > MAX_BYTES || inspectPixels) {
return Promise.resolve(optimizeSelectedImage(file)).then(optimized => {
if (generation !== selectionGeneration) return state();
return commitSelection(optimized);
@@ -219,7 +221,8 @@
function mount(options) {
const controller = create({
...options, maxFiles:options.maxFiles || (options.input?.multiple ? MAX_FILES : 1),
...options, inspectPixels:true,
maxFiles:options.maxFiles || (options.input?.multiple ? MAX_FILES : 1),
});
const clearSelection = controller.clear;
const restoreSelection = controller.restore;
@@ -376,5 +379,5 @@
return Object.assign(controller, { clear: clearPreview, restore: restorePreview, setBusy });
}
return { create, mount, multipart, optimizeImage, MAX_BYTES };
return { create, mount, multipart, optimizeImage, MAX_BYTES, MAX_PIXELS };
});
+17 -7
View File
@@ -6,6 +6,8 @@
'use strict';
const MAX_BYTES = 2 * 1024 * 1024;
const MAX_PIXELS = 12 * 1024 * 1024;
const MAX_DIMENSION = 8192;
function namedBlob(blob, name) {
if (typeof File === 'function') return new File([blob], name, { type: blob.type });
@@ -14,20 +16,28 @@
}
async function optimizeImage(file, environment = {}) {
if (file.size <= MAX_BYTES) return file;
const decode = environment.createImageBitmap || globalThis.createImageBitmap;
const makeCanvas = environment.createCanvas || (() => document.createElement('canvas'));
if (typeof decode !== 'function') throw new Error('This browser cannot optimize the screenshot. Try cropping it and choose it again.');
if (typeof decode !== 'function') throw new Error('This browser cannot safely inspect the screenshot. Try cropping it and choose it again.');
let bitmap;
try {
bitmap = await decode(file);
const width = Number(bitmap.width);
const height = Number(bitmap.height);
if (!Number.isSafeInteger(width) || !Number.isSafeInteger(height) || width <= 0 || height <= 0) {
throw new Error('decode');
}
const pixelScale = Math.min(1, Math.sqrt(MAX_PIXELS / (width * height)));
const dimensionScale = Math.min(1, MAX_DIMENSION / Math.max(width, height));
const byteScale = file.size > MAX_BYTES ? Math.sqrt(MAX_BYTES / file.size) * 0.92 : 1;
let scale = Math.min(pixelScale, dimensionScale, byteScale);
if (scale === 1 && file.size <= MAX_BYTES) return file;
const canvas = makeCanvas();
const context = canvas && canvas.getContext && canvas.getContext('2d');
if (!context || !bitmap.width || !bitmap.height) throw new Error('decode');
let scale = Math.min(1, Math.sqrt(MAX_BYTES / file.size) * 0.92);
if (!context) throw new Error('decode');
for (let attempt = 0; attempt < 10; attempt += 1) {
canvas.width = Math.max(1, Math.round(bitmap.width * scale));
canvas.height = Math.max(1, Math.round(bitmap.height * scale));
canvas.width = Math.max(1, Math.floor(width * scale));
canvas.height = Math.max(1, Math.floor(height * scale));
context.drawImage(bitmap, 0, 0, canvas.width, canvas.height);
const blob = await new Promise(resolve => canvas.toBlob(resolve, file.type, file.type === 'image/png' ? undefined : 0.86));
if (!blob) throw new Error('encode');
@@ -172,5 +182,5 @@
return { activeIndex: () => activeIndex, afterRemoval, clear, render, setBusy };
}
return { create, optimizeImage };
return { create, optimizeImage, MAX_PIXELS };
});