From 34b4199d0a47828825cc19ab8521f4a96c199985 Mon Sep 17 00:00:00 2001 From: silverwind Date: Sat, 10 Feb 2024 04:21:52 +0100 Subject: [PATCH 01/23] Downscale pasted images based on png metadata Some images like MacOS screenshots contain pHYs data which we can use to downscale uploaded images so they render in the same dppx ratio in which they were taken. --- package-lock.json | 17 +++++++++++++ package.json | 1 + web_src/js/features/comp/ImagePaste.js | 18 ++++++++++++-- web_src/js/utils/image.js | 34 ++++++++++++++++++++++++++ 4 files changed, 68 insertions(+), 2 deletions(-) create mode 100644 web_src/js/utils/image.js diff --git a/package-lock.json b/package-lock.json index 62bf36e7b73f1..ed9042510bd45 100644 --- a/package-lock.json +++ b/package-lock.json @@ -36,6 +36,7 @@ "monaco-editor": "0.46.0", "monaco-editor-webpack-plugin": "7.1.0", "pdfobject": "2.2.12", + "png-chunks-extract": "1.0.0", "pretty-ms": "9.0.0", "sortablejs": "1.15.2", "swagger-ui-dist": "5.11.3", @@ -3574,6 +3575,14 @@ } } }, + "node_modules/crc-32": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/crc-32/-/crc-32-0.3.0.tgz", + "integrity": "sha512-kucVIjOmMc1f0tv53BJ/5WIX+MGLcKuoBhnGqQrgKJNqLByb/sVMWfW/Aw6hw0jgcqjJ2pi9E5y32zOIpaUlsA==", + "engines": { + "node": ">=0.8" + } + }, "node_modules/cross-spawn": { "version": "7.0.3", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", @@ -8504,6 +8513,14 @@ "node": ">=4" } }, + "node_modules/png-chunks-extract": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/png-chunks-extract/-/png-chunks-extract-1.0.0.tgz", + "integrity": "sha512-ZiVwF5EJ0DNZyzAqld8BP1qyJBaGOFaq9zl579qfbkcmOwWLLO4I9L8i2O4j3HkI6/35i0nKG2n+dZplxiT89Q==", + "dependencies": { + "crc-32": "^0.3.0" + } + }, "node_modules/pony-cause": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/pony-cause/-/pony-cause-1.1.1.tgz", diff --git a/package.json b/package.json index 46dfdd1055418..54ad3cd8255f0 100644 --- a/package.json +++ b/package.json @@ -35,6 +35,7 @@ "monaco-editor": "0.46.0", "monaco-editor-webpack-plugin": "7.1.0", "pdfobject": "2.2.12", + "png-chunks-extract": "1.0.0", "pretty-ms": "9.0.0", "sortablejs": "1.15.2", "swagger-ui-dist": "5.11.3", diff --git a/web_src/js/features/comp/ImagePaste.js b/web_src/js/features/comp/ImagePaste.js index 27abcfe56f374..968edd1089a73 100644 --- a/web_src/js/features/comp/ImagePaste.js +++ b/web_src/js/features/comp/ImagePaste.js @@ -1,5 +1,6 @@ import $ from 'jquery'; import {POST} from '../../modules/fetch.js'; +import {imageInfo} from '../../utils/image.js'; async function uploadFile(file, uploadUrl) { const formData = new FormData(); @@ -109,8 +110,21 @@ const uploadClipboardImage = async (editor, dropzone, e) => { const placeholder = `![${name}](uploading ...)`; editor.insertPlaceholder(placeholder); - const data = await uploadFile(img, uploadUrl); - editor.replacePlaceholder(placeholder, `![${name}](/attachments/${data.uuid})`); + + const [data, {width, dppx}] = await Promise.all([ + uploadFile(img, uploadUrl), + imageInfo(img), + ]); + + const url = `/attachments/${data.uuid}`; + let text; + if (width > 0 && dppx > 1) { + text = `image`; + } else { + text = `![${name}](${url})`; + } + + editor.replacePlaceholder(placeholder, text); const $input = $(``).attr('id', data.uuid).val(data.uuid); $files.append($input); diff --git a/web_src/js/utils/image.js b/web_src/js/utils/image.js new file mode 100644 index 0000000000000..5c68d3726d4d0 --- /dev/null +++ b/web_src/js/utils/image.js @@ -0,0 +1,34 @@ +import pngChunksExtract from 'png-chunks-extract'; + +export async function imageInfo(file) { + let width = 0; + let dppx = 1; + + try { + if (file.type === 'image/png') { + const buffer = await file.arrayBuffer(); + const chunks = pngChunksExtract(new Uint8Array(buffer)); + + // extract width from mandatory IHDR chunk + const ihdr = chunks.find((chunk) => chunk.name === 'IHDR'); + if (ihdr?.data?.length) { + const View = new DataView(ihdr.data.buffer, 0); + width = View.getUint32(0); + } + + // extract dppx from optional pHYs chunk, assuming unit is meter and pixels are square + const phys = chunks.find((chunk) => chunk.name === 'pHYs'); + if (phys?.data?.length) { + const view = new DataView(phys.data.buffer, 0); + const dpi = Math.round(view.getUint32(0) / 39.3701); + const unit = view.getUint8(8); + if (unit !== 1) return 1; + dppx = dpi / 72; + } else { + dppx = 1; + } + } + } catch {} + + return {width, dppx}; +} From bd88a3a7fc82007b6191acde869136446d1250c2 Mon Sep 17 00:00:00 2001 From: silverwind Date: Sat, 10 Feb 2024 15:45:10 +0100 Subject: [PATCH 02/23] review and tweaks --- web_src/js/features/comp/ImagePaste.js | 14 ++++----- web_src/js/utils/image.js | 43 +++++++++++++------------- 2 files changed, 28 insertions(+), 29 deletions(-) diff --git a/web_src/js/features/comp/ImagePaste.js b/web_src/js/features/comp/ImagePaste.js index 968edd1089a73..0fb3e48b822ce 100644 --- a/web_src/js/features/comp/ImagePaste.js +++ b/web_src/js/features/comp/ImagePaste.js @@ -1,6 +1,6 @@ import $ from 'jquery'; import {POST} from '../../modules/fetch.js'; -import {imageInfo} from '../../utils/image.js'; +import {pngInfo} from '../../utils/image.js'; async function uploadFile(file, uploadUrl) { const formData = new FormData(); @@ -111,14 +111,12 @@ const uploadClipboardImage = async (editor, dropzone, e) => { const placeholder = `![${name}](uploading ...)`; editor.insertPlaceholder(placeholder); - const [data, {width, dppx}] = await Promise.all([ - uploadFile(img, uploadUrl), - imageInfo(img), - ]); + const {uuid} = await uploadFile(img, uploadUrl); + const {width, dppx} = await pngInfo(img); - const url = `/attachments/${data.uuid}`; + const url = `/attachments/${uuid}`; let text; - if (width > 0 && dppx > 1) { + if (width > 0 && dppx > 1) { // Scale down images from HiDPI monitors text = `image`; } else { text = `![${name}](${url})`; @@ -126,7 +124,7 @@ const uploadClipboardImage = async (editor, dropzone, e) => { editor.replacePlaceholder(placeholder, text); - const $input = $(``).attr('id', data.uuid).val(data.uuid); + const $input = $(``).attr('id', uuid).val(uuid); $files.append($input); } }; diff --git a/web_src/js/utils/image.js b/web_src/js/utils/image.js index 5c68d3726d4d0..dd86803de547d 100644 --- a/web_src/js/utils/image.js +++ b/web_src/js/utils/image.js @@ -1,32 +1,33 @@ import pngChunksExtract from 'png-chunks-extract'; -export async function imageInfo(file) { +export async function pngInfo(blob) { let width = 0; let dppx = 1; + // only png is supported currently + if (blob.type !== 'image/png') return {width, dppx}; + try { - if (file.type === 'image/png') { - const buffer = await file.arrayBuffer(); - const chunks = pngChunksExtract(new Uint8Array(buffer)); + const buffer = await blob.arrayBuffer(); + const chunks = pngChunksExtract(new Uint8Array(buffer)); - // extract width from mandatory IHDR chunk - const ihdr = chunks.find((chunk) => chunk.name === 'IHDR'); - if (ihdr?.data?.length) { - const View = new DataView(ihdr.data.buffer, 0); - width = View.getUint32(0); - } + // extract width from mandatory IHDR chunk + const ihdr = chunks.find((chunk) => chunk.name === 'IHDR'); + if (ihdr?.data?.length) { + const View = new DataView(ihdr.data.buffer, 0); + width = View.getUint32(0); + } - // extract dppx from optional pHYs chunk, assuming unit is meter and pixels are square - const phys = chunks.find((chunk) => chunk.name === 'pHYs'); - if (phys?.data?.length) { - const view = new DataView(phys.data.buffer, 0); - const dpi = Math.round(view.getUint32(0) / 39.3701); - const unit = view.getUint8(8); - if (unit !== 1) return 1; - dppx = dpi / 72; - } else { - dppx = 1; - } + // extract dppx from optional pHYs chunk, assuming pixels are square + const phys = chunks.find((chunk) => chunk.name === 'pHYs'); + if (phys?.data?.length) { + const view = new DataView(phys.data.buffer, 0); + const dpi = Math.round(view.getUint32(0) / 39.3701); + const unit = view.getUint8(8); + if (unit !== 1) return 1; + dppx = dpi / 72; + } else { + dppx = 1; } } catch {} From e84c41adc7c7c15f110852e618d4f212ea13baa0 Mon Sep 17 00:00:00 2001 From: silverwind Date: Sat, 10 Feb 2024 16:49:48 +0100 Subject: [PATCH 03/23] add pngChunks function, remove dependency --- package-lock.json | 17 ----------------- package.json | 1 - web_src/js/features/comp/ImagePaste.js | 1 - web_src/js/utils/image.js | 26 +++++++++++++++++++++----- web_src/js/utils/image.test.js | 10 ++++++++++ 5 files changed, 31 insertions(+), 24 deletions(-) create mode 100644 web_src/js/utils/image.test.js diff --git a/package-lock.json b/package-lock.json index ed9042510bd45..62bf36e7b73f1 100644 --- a/package-lock.json +++ b/package-lock.json @@ -36,7 +36,6 @@ "monaco-editor": "0.46.0", "monaco-editor-webpack-plugin": "7.1.0", "pdfobject": "2.2.12", - "png-chunks-extract": "1.0.0", "pretty-ms": "9.0.0", "sortablejs": "1.15.2", "swagger-ui-dist": "5.11.3", @@ -3575,14 +3574,6 @@ } } }, - "node_modules/crc-32": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/crc-32/-/crc-32-0.3.0.tgz", - "integrity": "sha512-kucVIjOmMc1f0tv53BJ/5WIX+MGLcKuoBhnGqQrgKJNqLByb/sVMWfW/Aw6hw0jgcqjJ2pi9E5y32zOIpaUlsA==", - "engines": { - "node": ">=0.8" - } - }, "node_modules/cross-spawn": { "version": "7.0.3", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", @@ -8513,14 +8504,6 @@ "node": ">=4" } }, - "node_modules/png-chunks-extract": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/png-chunks-extract/-/png-chunks-extract-1.0.0.tgz", - "integrity": "sha512-ZiVwF5EJ0DNZyzAqld8BP1qyJBaGOFaq9zl579qfbkcmOwWLLO4I9L8i2O4j3HkI6/35i0nKG2n+dZplxiT89Q==", - "dependencies": { - "crc-32": "^0.3.0" - } - }, "node_modules/pony-cause": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/pony-cause/-/pony-cause-1.1.1.tgz", diff --git a/package.json b/package.json index 54ad3cd8255f0..46dfdd1055418 100644 --- a/package.json +++ b/package.json @@ -35,7 +35,6 @@ "monaco-editor": "0.46.0", "monaco-editor-webpack-plugin": "7.1.0", "pdfobject": "2.2.12", - "png-chunks-extract": "1.0.0", "pretty-ms": "9.0.0", "sortablejs": "1.15.2", "swagger-ui-dist": "5.11.3", diff --git a/web_src/js/features/comp/ImagePaste.js b/web_src/js/features/comp/ImagePaste.js index 0fb3e48b822ce..7cabc0187cc08 100644 --- a/web_src/js/features/comp/ImagePaste.js +++ b/web_src/js/features/comp/ImagePaste.js @@ -121,7 +121,6 @@ const uploadClipboardImage = async (editor, dropzone, e) => { } else { text = `![${name}](${url})`; } - editor.replacePlaceholder(placeholder, text); const $input = $(``).attr('id', uuid).val(uuid); diff --git a/web_src/js/utils/image.js b/web_src/js/utils/image.js index dd86803de547d..616a3b5e7c9cb 100644 --- a/web_src/js/utils/image.js +++ b/web_src/js/utils/image.js @@ -1,4 +1,21 @@ -import pngChunksExtract from 'png-chunks-extract'; +export function pngChunks(data) { + const view = new DataView(data.buffer, 0); + if (view.getBigUint64(0) !== 9894494448401390090n) throw new Error(`Invalid png header`); + + const decoder = new TextDecoder(); + const chunks = []; + let index = 8; + while (index < data.length) { + const len = view.getUint32(index); + chunks.push({ + name: decoder.decode(data.slice(index + 4, index + 8)), + data: data.slice(index + 8, index + 8 + len), + }); + index += len + 12; + } + + return chunks; +} export async function pngInfo(blob) { let width = 0; @@ -8,14 +25,13 @@ export async function pngInfo(blob) { if (blob.type !== 'image/png') return {width, dppx}; try { - const buffer = await blob.arrayBuffer(); - const chunks = pngChunksExtract(new Uint8Array(buffer)); + const chunks = pngChunks(new Uint8Array(await blob.arrayBuffer())); // extract width from mandatory IHDR chunk const ihdr = chunks.find((chunk) => chunk.name === 'IHDR'); if (ihdr?.data?.length) { - const View = new DataView(ihdr.data.buffer, 0); - width = View.getUint32(0); + const view = new DataView(ihdr.data.buffer, 0); + width = view.getUint32(0); } // extract dppx from optional pHYs chunk, assuming pixels are square diff --git a/web_src/js/utils/image.test.js b/web_src/js/utils/image.test.js new file mode 100644 index 0000000000000..d960f135e2548 --- /dev/null +++ b/web_src/js/utils/image.test.js @@ -0,0 +1,10 @@ +import {pngChunks} from './image.js'; + +test('pngChunks', async () => { + const blob = await (await globalThis.fetch('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAAAAAA6fptVAAAADUlEQVQIHQECAP3/AAAAAgABzePRKwAAAABJRU5ErkJggg==')).blob(); + expect(pngChunks(new Uint8Array(await blob.arrayBuffer()))).toEqual([ + {name: "IHDR", data: new Uint8Array([0, 0, 0, 1, 0, 0, 0, 1, 8, 0, 0, 0, 0])}, + {name: "IDAT", data: new Uint8Array([8, 29, 1, 2, 0, 253, 255, 0, 0, 0, 2, 0, 1])}, + {name: "IEND", data: new Uint8Array([])}, + ]); +}); From dac316dad3415f766df8a3f23e7b34260b68e509 Mon Sep 17 00:00:00 2001 From: silverwind Date: Sat, 10 Feb 2024 16:58:03 +0100 Subject: [PATCH 04/23] use filename and escape --- web_src/js/features/comp/ImagePaste.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/web_src/js/features/comp/ImagePaste.js b/web_src/js/features/comp/ImagePaste.js index 7cabc0187cc08..9111b3317f831 100644 --- a/web_src/js/features/comp/ImagePaste.js +++ b/web_src/js/features/comp/ImagePaste.js @@ -1,4 +1,5 @@ import $ from 'jquery'; +import {htmlEscape} from 'escape-goat'; import {POST} from '../../modules/fetch.js'; import {pngInfo} from '../../utils/image.js'; @@ -117,7 +118,7 @@ const uploadClipboardImage = async (editor, dropzone, e) => { const url = `/attachments/${uuid}`; let text; if (width > 0 && dppx > 1) { // Scale down images from HiDPI monitors - text = `image`; + text = `${htmlEscape(name)}`; } else { text = `![${name}](${url})`; } From f544683e20f9a4fb51bb7c0245730b3af86ee9ef Mon Sep 17 00:00:00 2001 From: silverwind Date: Sat, 10 Feb 2024 16:59:58 +0100 Subject: [PATCH 05/23] check unit before using it --- web_src/js/utils/image.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/web_src/js/utils/image.js b/web_src/js/utils/image.js index 616a3b5e7c9cb..02a8850b04866 100644 --- a/web_src/js/utils/image.js +++ b/web_src/js/utils/image.js @@ -38,9 +38,9 @@ export async function pngInfo(blob) { const phys = chunks.find((chunk) => chunk.name === 'pHYs'); if (phys?.data?.length) { const view = new DataView(phys.data.buffer, 0); - const dpi = Math.round(view.getUint32(0) / 39.3701); const unit = view.getUint8(8); - if (unit !== 1) return 1; + if (unit !== 1) return 1; // not meter` + const dpi = Math.round(view.getUint32(0) / 39.3701); dppx = dpi / 72; } else { dppx = 1; From 132ea7ec9c411a8915b7bc68703547017bd4467d Mon Sep 17 00:00:00 2001 From: silverwind Date: Sat, 10 Feb 2024 17:00:45 +0100 Subject: [PATCH 06/23] typo --- web_src/js/utils/image.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/web_src/js/utils/image.js b/web_src/js/utils/image.js index 02a8850b04866..d074b2e62d196 100644 --- a/web_src/js/utils/image.js +++ b/web_src/js/utils/image.js @@ -39,7 +39,7 @@ export async function pngInfo(blob) { if (phys?.data?.length) { const view = new DataView(phys.data.buffer, 0); const unit = view.getUint8(8); - if (unit !== 1) return 1; // not meter` + if (unit !== 1) return 1; // not meter const dpi = Math.round(view.getUint32(0) / 39.3701); dppx = dpi / 72; } else { From eeac4080a5a8afe5a448c38031e8065f0aea5830 Mon Sep 17 00:00:00 2001 From: silverwind Date: Sat, 10 Feb 2024 17:07:18 +0100 Subject: [PATCH 07/23] fix lint --- web_src/js/utils/image.test.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/web_src/js/utils/image.test.js b/web_src/js/utils/image.test.js index d960f135e2548..938a0c77d983c 100644 --- a/web_src/js/utils/image.test.js +++ b/web_src/js/utils/image.test.js @@ -3,8 +3,8 @@ import {pngChunks} from './image.js'; test('pngChunks', async () => { const blob = await (await globalThis.fetch('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAAAAAA6fptVAAAADUlEQVQIHQECAP3/AAAAAgABzePRKwAAAABJRU5ErkJggg==')).blob(); expect(pngChunks(new Uint8Array(await blob.arrayBuffer()))).toEqual([ - {name: "IHDR", data: new Uint8Array([0, 0, 0, 1, 0, 0, 0, 1, 8, 0, 0, 0, 0])}, - {name: "IDAT", data: new Uint8Array([8, 29, 1, 2, 0, 253, 255, 0, 0, 0, 2, 0, 1])}, - {name: "IEND", data: new Uint8Array([])}, + {name: 'IHDR', data: new Uint8Array([0, 0, 0, 1, 0, 0, 0, 1, 8, 0, 0, 0, 0])}, + {name: 'IDAT', data: new Uint8Array([8, 29, 1, 2, 0, 253, 255, 0, 0, 0, 2, 0, 1])}, + {name: 'IEND', data: new Uint8Array([])}, ]); }); From c4a1e6abdb00a666651c2da15cd516dad205f6b5 Mon Sep 17 00:00:00 2001 From: silverwind Date: Sat, 10 Feb 2024 17:59:04 +0100 Subject: [PATCH 08/23] add test for pngInfo --- web_src/js/utils/image.js | 2 +- web_src/js/utils/image.test.js | 7 ++++++- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/web_src/js/utils/image.js b/web_src/js/utils/image.js index d074b2e62d196..ac507667cf849 100644 --- a/web_src/js/utils/image.js +++ b/web_src/js/utils/image.js @@ -1,6 +1,6 @@ export function pngChunks(data) { const view = new DataView(data.buffer, 0); - if (view.getBigUint64(0) !== 9894494448401390090n) throw new Error(`Invalid png header`); + if (view.getBigUint64(0) !== 9894494448401390090n) throw new Error('Invalid png header'); const decoder = new TextDecoder(); const chunks = []; diff --git a/web_src/js/utils/image.test.js b/web_src/js/utils/image.test.js index 938a0c77d983c..b22765fce4b21 100644 --- a/web_src/js/utils/image.test.js +++ b/web_src/js/utils/image.test.js @@ -1,4 +1,4 @@ -import {pngChunks} from './image.js'; +import {pngChunks, pngInfo} from './image.js'; test('pngChunks', async () => { const blob = await (await globalThis.fetch('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAAAAAA6fptVAAAADUlEQVQIHQECAP3/AAAAAgABzePRKwAAAABJRU5ErkJggg==')).blob(); @@ -8,3 +8,8 @@ test('pngChunks', async () => { {name: 'IEND', data: new Uint8Array([])}, ]); }); + +test('pngInfo', async () => { + const blob = await (await globalThis.fetch('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAIAAAACCAIAAAD91JpzAAAACXBIWXMAABYlAAAWJQFJUiTwAAAAEElEQVQI12OQNZcAIgYIBQAL8gGxdzzM0A==')).blob(); + expect(await pngInfo(blob)).toEqual({dppx: 2, width: 2}); +}); From 1fdf77f8dcc7611fc3e2e907a5919c7aad8475ff Mon Sep 17 00:00:00 2001 From: silverwind Date: Sat, 10 Feb 2024 18:00:13 +0100 Subject: [PATCH 09/23] swap --- web_src/js/utils/image.test.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/web_src/js/utils/image.test.js b/web_src/js/utils/image.test.js index b22765fce4b21..9d5d6a0a203c1 100644 --- a/web_src/js/utils/image.test.js +++ b/web_src/js/utils/image.test.js @@ -11,5 +11,5 @@ test('pngChunks', async () => { test('pngInfo', async () => { const blob = await (await globalThis.fetch('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAIAAAACCAIAAAD91JpzAAAACXBIWXMAABYlAAAWJQFJUiTwAAAAEElEQVQI12OQNZcAIgYIBQAL8gGxdzzM0A==')).blob(); - expect(await pngInfo(blob)).toEqual({dppx: 2, width: 2}); + expect(await pngInfo(blob)).toEqual({width: 2, dppx: 2}); }); From a417737b33772274e787b9c7a53b4689709229c1 Mon Sep 17 00:00:00 2001 From: silverwind Date: Sun, 11 Feb 2024 13:56:37 +0100 Subject: [PATCH 10/23] fix non-meter logic --- web_src/js/utils/image.js | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/web_src/js/utils/image.js b/web_src/js/utils/image.js index ac507667cf849..8c6a6cde50474 100644 --- a/web_src/js/utils/image.js +++ b/web_src/js/utils/image.js @@ -20,8 +20,6 @@ export function pngChunks(data) { export async function pngInfo(blob) { let width = 0; let dppx = 1; - - // only png is supported currently if (blob.type !== 'image/png') return {width, dppx}; try { @@ -39,9 +37,11 @@ export async function pngInfo(blob) { if (phys?.data?.length) { const view = new DataView(phys.data.buffer, 0); const unit = view.getUint8(8); - if (unit !== 1) return 1; // not meter - const dpi = Math.round(view.getUint32(0) / 39.3701); - dppx = dpi / 72; + if (unit !== 1) { + dppx = 1; // not meter + } else { + dppx = Math.round(view.getUint32(0) / 39.3701) / 72; // meter to inch to pixels + } } else { dppx = 1; } From 84325b8128c88c839d599b68adf499574bcb5dde Mon Sep 17 00:00:00 2001 From: silverwind Date: Sun, 11 Feb 2024 14:16:44 +0100 Subject: [PATCH 11/23] make both functions accept blobs and add tests --- web_src/js/utils/image.js | 13 +++++++------ web_src/js/utils/image.test.js | 19 +++++++++++++++---- 2 files changed, 22 insertions(+), 10 deletions(-) diff --git a/web_src/js/utils/image.js b/web_src/js/utils/image.js index 8c6a6cde50474..9bf33f1ba28ed 100644 --- a/web_src/js/utils/image.js +++ b/web_src/js/utils/image.js @@ -1,15 +1,16 @@ -export function pngChunks(data) { - const view = new DataView(data.buffer, 0); +export async function pngChunks(blob) { + const uint8arr = new Uint8Array(await blob.arrayBuffer()); + const view = new DataView(uint8arr.buffer, 0); if (view.getBigUint64(0) !== 9894494448401390090n) throw new Error('Invalid png header'); const decoder = new TextDecoder(); const chunks = []; let index = 8; - while (index < data.length) { + while (index < uint8arr.length) { const len = view.getUint32(index); chunks.push({ - name: decoder.decode(data.slice(index + 4, index + 8)), - data: data.slice(index + 8, index + 8 + len), + name: decoder.decode(uint8arr.slice(index + 4, index + 8)), + data: uint8arr.slice(index + 8, index + 8 + len), }); index += len + 12; } @@ -23,7 +24,7 @@ export async function pngInfo(blob) { if (blob.type !== 'image/png') return {width, dppx}; try { - const chunks = pngChunks(new Uint8Array(await blob.arrayBuffer())); + const chunks = await pngChunks(blob); // extract width from mandatory IHDR chunk const ihdr = chunks.find((chunk) => chunk.name === 'IHDR'); diff --git a/web_src/js/utils/image.test.js b/web_src/js/utils/image.test.js index 9d5d6a0a203c1..8ed81b8640795 100644 --- a/web_src/js/utils/image.test.js +++ b/web_src/js/utils/image.test.js @@ -1,15 +1,26 @@ import {pngChunks, pngInfo} from './image.js'; +const pngNoPhys = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAAAAAA6fptVAAAADUlEQVQIHQECAP3/AAAAAgABzePRKwAAAABJRU5ErkJggg=='; +const pngPhys = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAIAAAACCAIAAAD91JpzAAAACXBIWXMAABYlAAAWJQFJUiTwAAAAEElEQVQI12OQNZcAIgYIBQAL8gGxdzzM0A=='; + +async function toBlob(datauri) { + return await (await globalThis.fetch(datauri)).blob(); +} + test('pngChunks', async () => { - const blob = await (await globalThis.fetch('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAAAAAA6fptVAAAADUlEQVQIHQECAP3/AAAAAgABzePRKwAAAABJRU5ErkJggg==')).blob(); - expect(pngChunks(new Uint8Array(await blob.arrayBuffer()))).toEqual([ + expect(await pngChunks(await toBlob(pngNoPhys))).toEqual([ {name: 'IHDR', data: new Uint8Array([0, 0, 0, 1, 0, 0, 0, 1, 8, 0, 0, 0, 0])}, {name: 'IDAT', data: new Uint8Array([8, 29, 1, 2, 0, 253, 255, 0, 0, 0, 2, 0, 1])}, {name: 'IEND', data: new Uint8Array([])}, ]); + expect(await pngChunks(await toBlob(pngPhys))).toEqual([ + {name: 'IHDR', data: new Uint8Array([0, 0, 0, 2, 0, 0, 0, 2, 8, 2, 0, 0, 0])}, + {name: 'pHYs', data: new Uint8Array([0, 0, 22, 37, 0, 0, 22, 37, 1])}, + {name: 'IDAT', data: new Uint8Array([8, 215, 99, 144, 53, 151, 0, 34, 6, 8, 5, 0, 11, 242, 1, 177])}, + ]); }); test('pngInfo', async () => { - const blob = await (await globalThis.fetch('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAIAAAACCAIAAAD91JpzAAAACXBIWXMAABYlAAAWJQFJUiTwAAAAEElEQVQI12OQNZcAIgYIBQAL8gGxdzzM0A==')).blob(); - expect(await pngInfo(blob)).toEqual({width: 2, dppx: 2}); + expect(await pngInfo(await toBlob(pngNoPhys))).toEqual({width: 1, dppx: 1}); + expect(await pngInfo(await toBlob(pngPhys))).toEqual({width: 2, dppx: 2}); }); From c8683c57f0018b521781c1d36131f0d227d0f0df Mon Sep 17 00:00:00 2001 From: silverwind Date: Sun, 11 Feb 2024 14:17:17 +0100 Subject: [PATCH 12/23] rename function --- web_src/js/utils/image.test.js | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/web_src/js/utils/image.test.js b/web_src/js/utils/image.test.js index 8ed81b8640795..eb4ecbbc160c9 100644 --- a/web_src/js/utils/image.test.js +++ b/web_src/js/utils/image.test.js @@ -3,17 +3,17 @@ import {pngChunks, pngInfo} from './image.js'; const pngNoPhys = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAAAAAA6fptVAAAADUlEQVQIHQECAP3/AAAAAgABzePRKwAAAABJRU5ErkJggg=='; const pngPhys = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAIAAAACCAIAAAD91JpzAAAACXBIWXMAABYlAAAWJQFJUiTwAAAAEElEQVQI12OQNZcAIgYIBQAL8gGxdzzM0A=='; -async function toBlob(datauri) { +async function dataUriToBlob(datauri) { return await (await globalThis.fetch(datauri)).blob(); } test('pngChunks', async () => { - expect(await pngChunks(await toBlob(pngNoPhys))).toEqual([ + expect(await pngChunks(await dataUriToBlob(pngNoPhys))).toEqual([ {name: 'IHDR', data: new Uint8Array([0, 0, 0, 1, 0, 0, 0, 1, 8, 0, 0, 0, 0])}, {name: 'IDAT', data: new Uint8Array([8, 29, 1, 2, 0, 253, 255, 0, 0, 0, 2, 0, 1])}, {name: 'IEND', data: new Uint8Array([])}, ]); - expect(await pngChunks(await toBlob(pngPhys))).toEqual([ + expect(await pngChunks(await dataUriToBlob(pngPhys))).toEqual([ {name: 'IHDR', data: new Uint8Array([0, 0, 0, 2, 0, 0, 0, 2, 8, 2, 0, 0, 0])}, {name: 'pHYs', data: new Uint8Array([0, 0, 22, 37, 0, 0, 22, 37, 1])}, {name: 'IDAT', data: new Uint8Array([8, 215, 99, 144, 53, 151, 0, 34, 6, 8, 5, 0, 11, 242, 1, 177])}, @@ -21,6 +21,6 @@ test('pngChunks', async () => { }); test('pngInfo', async () => { - expect(await pngInfo(await toBlob(pngNoPhys))).toEqual({width: 1, dppx: 1}); - expect(await pngInfo(await toBlob(pngPhys))).toEqual({width: 2, dppx: 2}); + expect(await pngInfo(await dataUriToBlob(pngNoPhys))).toEqual({width: 1, dppx: 1}); + expect(await pngInfo(await dataUriToBlob(pngPhys))).toEqual({width: 2, dppx: 2}); }); From d0ff9e303f09a03bd9ff932117e4e54289c77e4c Mon Sep 17 00:00:00 2001 From: silverwind Date: Sun, 11 Feb 2024 15:21:05 +0100 Subject: [PATCH 13/23] enhance comment --- web_src/js/features/comp/ImagePaste.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/web_src/js/features/comp/ImagePaste.js b/web_src/js/features/comp/ImagePaste.js index 9111b3317f831..1ab41e82674e1 100644 --- a/web_src/js/features/comp/ImagePaste.js +++ b/web_src/js/features/comp/ImagePaste.js @@ -117,7 +117,9 @@ const uploadClipboardImage = async (editor, dropzone, e) => { const url = `/attachments/${uuid}`; let text; - if (width > 0 && dppx > 1) { // Scale down images from HiDPI monitors + if (width > 0 && dppx > 1) { + // Scale down images from HiDPI monitors. This uses the tag because it's the only + // method to change image size in Markdown that is supported by all implementations. text = `${htmlEscape(name)}`; } else { text = `![${name}](${url})`; From c9cd1a2fb4a5889aa5eb906e0337698859ce6232 Mon Sep 17 00:00:00 2001 From: silverwind Date: Sun, 11 Feb 2024 15:39:17 +0100 Subject: [PATCH 14/23] move comment --- web_src/js/utils/image.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/web_src/js/utils/image.js b/web_src/js/utils/image.js index 9bf33f1ba28ed..7a3b30b32cd68 100644 --- a/web_src/js/utils/image.js +++ b/web_src/js/utils/image.js @@ -38,8 +38,8 @@ export async function pngInfo(blob) { if (phys?.data?.length) { const view = new DataView(phys.data.buffer, 0); const unit = view.getUint8(8); - if (unit !== 1) { - dppx = 1; // not meter + if (unit !== 1) { // not meter + dppx = 1; } else { dppx = Math.round(view.getUint32(0) / 39.3701) / 72; // meter to inch to pixels } From 93468a0eb618c7fbe5f7c944590398e87e65956d Mon Sep 17 00:00:00 2001 From: silverwind Date: Sun, 11 Feb 2024 15:40:50 +0100 Subject: [PATCH 15/23] fix comment --- web_src/js/utils/image.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/web_src/js/utils/image.js b/web_src/js/utils/image.js index 7a3b30b32cd68..b4967d590a22d 100644 --- a/web_src/js/utils/image.js +++ b/web_src/js/utils/image.js @@ -41,7 +41,7 @@ export async function pngInfo(blob) { if (unit !== 1) { // not meter dppx = 1; } else { - dppx = Math.round(view.getUint32(0) / 39.3701) / 72; // meter to inch to pixels + dppx = Math.round(view.getUint32(0) / 39.3701) / 72; // meter to inch to dppx } } else { dppx = 1; From d5ce26fc2c4ba14af20a171e35adaa208ccd33de Mon Sep 17 00:00:00 2001 From: silverwind Date: Sun, 11 Feb 2024 16:45:14 +0100 Subject: [PATCH 16/23] handle empty png --- web_src/js/utils/image.js | 5 +++-- web_src/js/utils/image.test.js | 3 +++ 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/web_src/js/utils/image.js b/web_src/js/utils/image.js index b4967d590a22d..dcf3e5920c267 100644 --- a/web_src/js/utils/image.js +++ b/web_src/js/utils/image.js @@ -1,10 +1,11 @@ export async function pngChunks(blob) { const uint8arr = new Uint8Array(await blob.arrayBuffer()); + const chunks = []; + if (uint8arr.length < 8) return chunks; const view = new DataView(uint8arr.buffer, 0); - if (view.getBigUint64(0) !== 9894494448401390090n) throw new Error('Invalid png header'); + if (view.getBigUint64(0) !== 9894494448401390090n) return chunks; const decoder = new TextDecoder(); - const chunks = []; let index = 8; while (index < uint8arr.length) { const len = view.getUint32(index); diff --git a/web_src/js/utils/image.test.js b/web_src/js/utils/image.test.js index eb4ecbbc160c9..bb116ac400e04 100644 --- a/web_src/js/utils/image.test.js +++ b/web_src/js/utils/image.test.js @@ -2,6 +2,7 @@ import {pngChunks, pngInfo} from './image.js'; const pngNoPhys = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAAAAAA6fptVAAAADUlEQVQIHQECAP3/AAAAAgABzePRKwAAAABJRU5ErkJggg=='; const pngPhys = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAIAAAACCAIAAAD91JpzAAAACXBIWXMAABYlAAAWJQFJUiTwAAAAEElEQVQI12OQNZcAIgYIBQAL8gGxdzzM0A=='; +const pngEmpty = 'data:image/png;base64,'; async function dataUriToBlob(datauri) { return await (await globalThis.fetch(datauri)).blob(); @@ -18,9 +19,11 @@ test('pngChunks', async () => { {name: 'pHYs', data: new Uint8Array([0, 0, 22, 37, 0, 0, 22, 37, 1])}, {name: 'IDAT', data: new Uint8Array([8, 215, 99, 144, 53, 151, 0, 34, 6, 8, 5, 0, 11, 242, 1, 177])}, ]); + expect(await pngChunks(await dataUriToBlob(pngEmpty))).toEqual([]); }); test('pngInfo', async () => { expect(await pngInfo(await dataUriToBlob(pngNoPhys))).toEqual({width: 1, dppx: 1}); expect(await pngInfo(await dataUriToBlob(pngPhys))).toEqual({width: 2, dppx: 2}); + expect(await pngInfo(await dataUriToBlob(pngEmpty))).toEqual({width: 0, dppx: 1}); }); From 5880a2c6e748a39c6530a7d3c8e307831a3b9a37 Mon Sep 17 00:00:00 2001 From: silverwind Date: Sun, 11 Feb 2024 16:48:33 +0100 Subject: [PATCH 17/23] shorten code --- web_src/js/utils/image.js | 31 +++++++++++-------------------- 1 file changed, 11 insertions(+), 20 deletions(-) diff --git a/web_src/js/utils/image.js b/web_src/js/utils/image.js index dcf3e5920c267..6d24e7216e9d9 100644 --- a/web_src/js/utils/image.js +++ b/web_src/js/utils/image.js @@ -25,27 +25,18 @@ export async function pngInfo(blob) { if (blob.type !== 'image/png') return {width, dppx}; try { - const chunks = await pngChunks(blob); - - // extract width from mandatory IHDR chunk - const ihdr = chunks.find((chunk) => chunk.name === 'IHDR'); - if (ihdr?.data?.length) { - const view = new DataView(ihdr.data.buffer, 0); - width = view.getUint32(0); - } - - // extract dppx from optional pHYs chunk, assuming pixels are square - const phys = chunks.find((chunk) => chunk.name === 'pHYs'); - if (phys?.data?.length) { - const view = new DataView(phys.data.buffer, 0); - const unit = view.getUint8(8); - if (unit !== 1) { // not meter - dppx = 1; - } else { - dppx = Math.round(view.getUint32(0) / 39.3701) / 72; // meter to inch to dppx + for (const {name, data} of await pngChunks(blob)) { + // extract width from mandatory IHDR chunk + if (name === 'IHDR' && data?.length) { + const view = new DataView(data.buffer, 0); + width = view.getUint32(0); + } else if (name === 'pHYs' && data?.length) { + const view = new DataView(data.buffer, 0); + const unit = view.getUint8(8); + if (unit === 1) { + dppx = Math.round(view.getUint32(0) / 39.3701) / 72; // meter to inch to dppx + } } - } else { - dppx = 1; } } catch {} From 4bc121d78bde003b7205a35dea5d52e17f26c307 Mon Sep 17 00:00:00 2001 From: silverwind Date: Sun, 11 Feb 2024 16:49:58 +0100 Subject: [PATCH 18/23] tweak comments --- web_src/js/utils/image.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/web_src/js/utils/image.js b/web_src/js/utils/image.js index 6d24e7216e9d9..4d7e5f48839f2 100644 --- a/web_src/js/utils/image.js +++ b/web_src/js/utils/image.js @@ -26,11 +26,12 @@ export async function pngInfo(blob) { try { for (const {name, data} of await pngChunks(blob)) { - // extract width from mandatory IHDR chunk if (name === 'IHDR' && data?.length) { + // extract width from mandatory IHDR chunk const view = new DataView(data.buffer, 0); width = view.getUint32(0); } else if (name === 'pHYs' && data?.length) { + // extract dppx from optional pHYs chunk, assuming pixels are square const view = new DataView(data.buffer, 0); const unit = view.getUint8(8); if (unit === 1) { From 86de298c83af2153193722b33af4b3cd47b5aef3 Mon Sep 17 00:00:00 2001 From: silverwind Date: Sun, 11 Feb 2024 16:50:26 +0100 Subject: [PATCH 19/23] create DataView only once --- web_src/js/utils/image.js | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/web_src/js/utils/image.js b/web_src/js/utils/image.js index 4d7e5f48839f2..d7bce66cb3b40 100644 --- a/web_src/js/utils/image.js +++ b/web_src/js/utils/image.js @@ -26,13 +26,12 @@ export async function pngInfo(blob) { try { for (const {name, data} of await pngChunks(blob)) { + const view = new DataView(data.buffer, 0); if (name === 'IHDR' && data?.length) { // extract width from mandatory IHDR chunk - const view = new DataView(data.buffer, 0); width = view.getUint32(0); } else if (name === 'pHYs' && data?.length) { // extract dppx from optional pHYs chunk, assuming pixels are square - const view = new DataView(data.buffer, 0); const unit = view.getUint8(8); if (unit === 1) { dppx = Math.round(view.getUint32(0) / 39.3701) / 72; // meter to inch to dppx From fcbdbfb3f9d12ad416068319fa7e774f83adb5e2 Mon Sep 17 00:00:00 2001 From: silverwind Date: Sun, 11 Feb 2024 16:53:02 +0100 Subject: [PATCH 20/23] use DataView default offset --- web_src/js/utils/image.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/web_src/js/utils/image.js b/web_src/js/utils/image.js index d7bce66cb3b40..3b59d5c380597 100644 --- a/web_src/js/utils/image.js +++ b/web_src/js/utils/image.js @@ -2,7 +2,7 @@ export async function pngChunks(blob) { const uint8arr = new Uint8Array(await blob.arrayBuffer()); const chunks = []; if (uint8arr.length < 8) return chunks; - const view = new DataView(uint8arr.buffer, 0); + const view = new DataView(uint8arr.buffer); if (view.getBigUint64(0) !== 9894494448401390090n) return chunks; const decoder = new TextDecoder(); @@ -26,7 +26,7 @@ export async function pngInfo(blob) { try { for (const {name, data} of await pngChunks(blob)) { - const view = new DataView(data.buffer, 0); + const view = new DataView(data.buffer); if (name === 'IHDR' && data?.length) { // extract width from mandatory IHDR chunk width = view.getUint32(0); From cd5d8d13c16f148e5d223ada5f21541cb7e2ea99 Mon Sep 17 00:00:00 2001 From: silverwind Date: Sun, 11 Feb 2024 17:05:44 +0100 Subject: [PATCH 21/23] minimal valid png length is 12 bytes --- web_src/js/utils/image.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/web_src/js/utils/image.js b/web_src/js/utils/image.js index 3b59d5c380597..d8709f7b65c8e 100644 --- a/web_src/js/utils/image.js +++ b/web_src/js/utils/image.js @@ -1,7 +1,7 @@ export async function pngChunks(blob) { const uint8arr = new Uint8Array(await blob.arrayBuffer()); const chunks = []; - if (uint8arr.length < 8) return chunks; + if (uint8arr.length < 12) return chunks; const view = new DataView(uint8arr.buffer); if (view.getBigUint64(0) !== 9894494448401390090n) return chunks; From 6f947b7cf86faeffbd816dac6b216fed8db2d599 Mon Sep 17 00:00:00 2001 From: silverwind Date: Thu, 15 Feb 2024 22:46:29 +0100 Subject: [PATCH 22/23] rename back to imageInfo --- web_src/js/features/comp/ImagePaste.js | 4 +-- web_src/js/utils/image.js | 37 ++++++++++++++------------ web_src/js/utils/image.test.js | 10 +++---- 3 files changed, 27 insertions(+), 24 deletions(-) diff --git a/web_src/js/features/comp/ImagePaste.js b/web_src/js/features/comp/ImagePaste.js index 1ab41e82674e1..444ab89150ca7 100644 --- a/web_src/js/features/comp/ImagePaste.js +++ b/web_src/js/features/comp/ImagePaste.js @@ -1,7 +1,7 @@ import $ from 'jquery'; import {htmlEscape} from 'escape-goat'; import {POST} from '../../modules/fetch.js'; -import {pngInfo} from '../../utils/image.js'; +import {imageInfo} from '../../utils/image.js'; async function uploadFile(file, uploadUrl) { const formData = new FormData(); @@ -113,7 +113,7 @@ const uploadClipboardImage = async (editor, dropzone, e) => { editor.insertPlaceholder(placeholder); const {uuid} = await uploadFile(img, uploadUrl); - const {width, dppx} = await pngInfo(img); + const {width, dppx} = await imageInfo(img); const url = `/attachments/${uuid}`; let text; diff --git a/web_src/js/utils/image.js b/web_src/js/utils/image.js index d8709f7b65c8e..388a9c9e7c3cd 100644 --- a/web_src/js/utils/image.js +++ b/web_src/js/utils/image.js @@ -19,26 +19,29 @@ export async function pngChunks(blob) { return chunks; } -export async function pngInfo(blob) { - let width = 0; - let dppx = 1; - if (blob.type !== 'image/png') return {width, dppx}; +// decode a image and try to obtain width and dppx. If will never throw but instead +// return default values. +export async function imageInfo(blob) { + let width = 0; // 0 means no width could be determined + let dppx = 1; // default to 1:1 pixel ratio - try { - for (const {name, data} of await pngChunks(blob)) { - const view = new DataView(data.buffer); - if (name === 'IHDR' && data?.length) { - // extract width from mandatory IHDR chunk - width = view.getUint32(0); - } else if (name === 'pHYs' && data?.length) { - // extract dppx from optional pHYs chunk, assuming pixels are square - const unit = view.getUint8(8); - if (unit === 1) { - dppx = Math.round(view.getUint32(0) / 39.3701) / 72; // meter to inch to dppx + if (blob.type === 'image/png') { // only png is supported currently + try { + for (const {name, data} of await pngChunks(blob)) { + const view = new DataView(data.buffer); + if (name === 'IHDR' && data?.length) { + // extract width from mandatory IHDR chunk + width = view.getUint32(0); + } else if (name === 'pHYs' && data?.length) { + // extract dppx from optional pHYs chunk, assuming pixels are square + const unit = view.getUint8(8); + if (unit === 1) { + dppx = Math.round(view.getUint32(0) / 39.3701) / 72; // meter to inch to dppx + } } } - } - } catch {} + } catch {} + } return {width, dppx}; } diff --git a/web_src/js/utils/image.test.js b/web_src/js/utils/image.test.js index bb116ac400e04..ba4758250c7f6 100644 --- a/web_src/js/utils/image.test.js +++ b/web_src/js/utils/image.test.js @@ -1,4 +1,4 @@ -import {pngChunks, pngInfo} from './image.js'; +import {pngChunks, imageInfo} from './image.js'; const pngNoPhys = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAAAAAA6fptVAAAADUlEQVQIHQECAP3/AAAAAgABzePRKwAAAABJRU5ErkJggg=='; const pngPhys = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAIAAAACCAIAAAD91JpzAAAACXBIWXMAABYlAAAWJQFJUiTwAAAAEElEQVQI12OQNZcAIgYIBQAL8gGxdzzM0A=='; @@ -22,8 +22,8 @@ test('pngChunks', async () => { expect(await pngChunks(await dataUriToBlob(pngEmpty))).toEqual([]); }); -test('pngInfo', async () => { - expect(await pngInfo(await dataUriToBlob(pngNoPhys))).toEqual({width: 1, dppx: 1}); - expect(await pngInfo(await dataUriToBlob(pngPhys))).toEqual({width: 2, dppx: 2}); - expect(await pngInfo(await dataUriToBlob(pngEmpty))).toEqual({width: 0, dppx: 1}); +test('imageInfo', async () => { + expect(await imageInfo(await dataUriToBlob(pngNoPhys))).toEqual({width: 1, dppx: 1}); + expect(await imageInfo(await dataUriToBlob(pngPhys))).toEqual({width: 2, dppx: 2}); + expect(await imageInfo(await dataUriToBlob(pngEmpty))).toEqual({width: 0, dppx: 1}); }); From bd3accbfd1d71392b0925b778acc14ece8022f99 Mon Sep 17 00:00:00 2001 From: silverwind Date: Fri, 16 Feb 2024 03:17:13 +0100 Subject: [PATCH 23/23] tweak comment --- web_src/js/utils/image.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/web_src/js/utils/image.js b/web_src/js/utils/image.js index 388a9c9e7c3cd..ed5d98e35ad0b 100644 --- a/web_src/js/utils/image.js +++ b/web_src/js/utils/image.js @@ -23,7 +23,7 @@ export async function pngChunks(blob) { // return default values. export async function imageInfo(blob) { let width = 0; // 0 means no width could be determined - let dppx = 1; // default to 1:1 pixel ratio + let dppx = 1; // 1 dot per pixel for non-HiDPI screens if (blob.type === 'image/png') { // only png is supported currently try {