fix: Size of inserted retina images (#6350)

* Fix pasted size of retina images

* lint

* lint
This commit is contained in:
Tom Moor
2024-01-05 19:17:39 -08:00
committed by GitHub
parent 47c13c9916
commit 89d905ebb7
10 changed files with 127 additions and 22 deletions

View File

@@ -176,7 +176,7 @@ function Editor(props: Props, ref: React.RefObject<SharedEditor> | null) {
(file) => !AttachmentValidation.imageContentTypes.includes(file.type)
);
insertFiles(view, event, pos, files, {
return insertFiles(view, event, pos, files, {
uploadFile: handleUploadFile,
onFileUploadStart: props.onFileUploadStart,
onFileUploadStop: props.onFileUploadStop,

View File

@@ -341,7 +341,9 @@ function SuggestionsMenu<T extends MenuItem>(props: Props<T>) {
setInsertItem(item);
};
const handleFilesPicked = (event: React.ChangeEvent<HTMLInputElement>) => {
const handleFilesPicked = async (
event: React.ChangeEvent<HTMLInputElement>
) => {
const { uploadFile, onFileUploadStart, onFileUploadStop } = props;
const files = getEventFiles(event);
const parent = findParentNode((node) => !!node)(view.state.selection);
@@ -353,7 +355,7 @@ function SuggestionsMenu<T extends MenuItem>(props: Props<T>) {
}
if (parent) {
insertFiles(view, event, parent.pos, files, {
await insertFiles(view, event, parent.pos, files, {
uploadFile,
onFileUploadStart,
onFileUploadStop,

View File

@@ -206,7 +206,8 @@ function CommentForm({
if (!files.length) {
return;
}
editorRef.current?.insertFiles(event, files);
return editorRef.current?.insertFiles(event, files);
};
const handleImageUpload = (event: React.MouseEvent<HTMLButtonElement>) => {

View File

@@ -158,6 +158,7 @@
"pg": "^8.11.1",
"pg-tsquery": "^8.4.1",
"pluralize": "^8.0.0",
"png-chunks-extract": "^1.0.0",
"polished": "^4.2.2",
"prosemirror-codemark": "^0.4.2",
"prosemirror-commands": "^1.5.2",
@@ -279,6 +280,7 @@
"@types/nodemailer": "^6.4.14",
"@types/passport-oauth2": "^1.4.15",
"@types/pluralize": "^0.0.33",
"@types/png-chunks-extract": "^1.0.2",
"@types/quoted-printable": "^1.0.2",
"@types/randomstring": "^1.1.11",
"@types/react": "^17.0.34",

View File

@@ -29,7 +29,7 @@ export type Options = {
};
};
const insertFiles = function (
const insertFiles = async function (
view: EditorView,
event:
| Event
@@ -38,7 +38,7 @@ const insertFiles = function (
pos: number,
files: File[],
options: Options
): void {
) {
const { dictionary, uploadFile, onFileUploadStart, onFileUploadStop } =
options;
@@ -54,14 +54,31 @@ const insertFiles = function (
// we'll use this to track of how many files have succeeded or failed
let complete = 0;
const filesToUpload = files.map((file) => ({
id: `upload-${uuidv4()}`,
isImage:
FileHelper.isImage(file) && !options.isAttachment && !!schema.nodes.image,
isVideo:
FileHelper.isVideo(file) && !options.isAttachment && !!schema.nodes.video,
file,
}));
const filesToUpload = await Promise.all(
files.map(async (file) => {
const isImage =
FileHelper.isImage(file) &&
!options.isAttachment &&
!!schema.nodes.image;
const isVideo =
FileHelper.isVideo(file) &&
!options.isAttachment &&
!!schema.nodes.video;
const getDimensions = isImage
? FileHelper.getImageDimensions
: isVideo
? FileHelper.getVideoDimensions
: undefined;
return {
id: `upload-${uuidv4()}`,
dimensions: await getDimensions?.(file),
isImage,
isVideo,
file,
};
})
);
// the user might have dropped multiple files at once, we need to loop
for (const upload of filesToUpload) {
@@ -86,11 +103,12 @@ const insertFiles = function (
}
if (upload.isImage) {
const newImg = new Image();
newImg.onload = () => {
newImg.onload = async () => {
const result = findPlaceholder(view.state, upload.id);
if (result === null) {
return;
}
if (view.isDestroyed) {
return;
}
@@ -101,7 +119,11 @@ const insertFiles = function (
.replaceWith(
from,
to || from,
schema.nodes.image.create({ src, ...options.attrs })
schema.nodes.image.create({
src,
...(upload.dimensions ?? {}),
...options.attrs,
})
)
.setMeta(uploadPlaceholderPlugin, { remove: { id: upload.id } })
);
@@ -119,7 +141,6 @@ const insertFiles = function (
}
const [from, to] = result;
const dimensions = await FileHelper.getVideoDimensions(upload.file);
if (view.isDestroyed) {
return;
@@ -133,8 +154,7 @@ const insertFiles = function (
schema.nodes.video.create({
src,
title: upload.file.name ?? dictionary.untitled,
width: dimensions.width,
height: dimensions.height,
...upload.dimensions,
...options.attrs,
})
)

View File

@@ -1,3 +1,5 @@
import extract from "png-chunks-extract";
export default class FileHelper {
/**
* Checks if a file is an image.
@@ -31,6 +33,7 @@ export default class FileHelper {
return new Promise((resolve, reject) => {
const video = document.createElement("video");
video.preload = "metadata";
video.crossOrigin = "anonymous";
video.onloadedmetadata = () => {
window.URL.revokeObjectURL(video.src);
resolve({ width: video.videoWidth, height: video.videoHeight });
@@ -39,4 +42,60 @@ export default class FileHelper {
video.src = URL.createObjectURL(file);
});
}
/**
* Loads the dimensions of an image file currently only PNGs are supported but we mainly use
* this to get the "real" dimensions of a retina image.
*
* @param file The file to load the dimensions for
* @returns The dimensions of the image, if known.
*/
static async getImageDimensions(
file: File
): Promise<{ width: number; height: number } | undefined> {
if (file.type !== "image/png") {
return;
}
function parsePhys(view: DataView) {
return {
ppux: view.getUint32(0),
ppuy: view.getUint32(4),
unit: view.getUint8(4),
};
}
function parseIHDR(view: DataView) {
return {
width: view.getUint32(0),
height: view.getUint32(4),
};
}
try {
const buffer = await file.arrayBuffer();
const chunks = extract(new Uint8Array(buffer));
const pHYsChunk = chunks.find((chunk) => chunk.name === "pHYs");
const iHDRChunk = chunks.find((chunk) => chunk.name === "IHDR");
if (!pHYsChunk || !iHDRChunk) {
return;
}
const idhrData = parseIHDR(new DataView(iHDRChunk.data.buffer));
const physData = parsePhys(new DataView(pHYsChunk.data.buffer));
if (physData.unit === 0 && physData.ppux === physData.ppuy) {
const pixelRatio = Math.round(physData.ppux / 2834.5);
return {
width: idhrData.width / pixelRatio,
height: idhrData.height / pixelRatio,
};
}
} catch {
return undefined;
}
return undefined;
}
}

View File

@@ -61,6 +61,8 @@ const uploadPlaceholder = new Plugin({
const img = document.createElement("img");
img.src = URL.createObjectURL(action.add.file);
img.width = action.add.dimensions?.width;
img.height = action.add.dimensions?.height;
element.appendChild(img);
@@ -77,6 +79,8 @@ const uploadPlaceholder = new Plugin({
video.src = URL.createObjectURL(action.add.file);
video.autoplay = false;
video.controls = false;
video.width = action.add.dimensions?.width;
video.height = action.add.dimensions?.height;
element.appendChild(video);

View File

@@ -47,7 +47,7 @@ const uploadPlugin = (options: Options) =>
}
const pos = tr.selection.from;
insertFiles(view, event, pos, files, options);
void insertFiles(view, event, pos, files, options);
return true;
},
drop(view, event: DragEvent): boolean {
@@ -71,7 +71,7 @@ const uploadPlugin = (options: Options) =>
});
if (result) {
insertFiles(view, event, result.pos, files, options);
void insertFiles(view, event, result.pos, files, options);
return true;
}

View File

@@ -142,7 +142,7 @@ export default class SimpleImage extends Node {
inputElement.accept = AttachmentValidation.imageContentTypes.join(", ");
inputElement.onchange = (event) => {
const files = getEventFiles(event);
insertFiles(view, event, state.selection.from, files, {
void insertFiles(view, event, state.selection.from, files, {
uploadFile,
onFileUploadStart,
onFileUploadStop,

View File

@@ -3266,6 +3266,11 @@
resolved "https://registry.yarnpkg.com/@types/pluralize/-/pluralize-0.0.33.tgz#8ad9018368c584d268667dd9acd5b3b806e8c82a"
integrity sha512-JOqsl+ZoCpP4e8TDke9W79FDcSgPAR0l6pixx2JHkhnRjvShyYiAYw2LVsnA7K08Y6DeOnaU6ujmENO4os/cYg==
"@types/png-chunks-extract@^1.0.2":
version "1.0.2"
resolved "https://registry.yarnpkg.com/@types/png-chunks-extract/-/png-chunks-extract-1.0.2.tgz#31dd8d74d6ba879ace317c1e042dcdabc6300d6e"
integrity sha512-z6djfFIbrrddtunoMJBOPlyZrnmeuG1kkvHUNi2QfpOb+JMMLuLliHHTmMyRi7k7LiTAut0HbdGCF6ibDtQAHQ==
"@types/prismjs@*":
version "1.26.0"
resolved "https://registry.yarnpkg.com/@types/prismjs/-/prismjs-1.26.0.tgz#a1c3809b0ad61c62cac6d4e0c56d610c910b7654"
@@ -5105,6 +5110,11 @@ cosmiconfig@8.3.6:
parse-json "^5.2.0"
path-type "^4.0.0"
crc-32@^0.3.0:
version "0.3.0"
resolved "https://registry.yarnpkg.com/crc-32/-/crc-32-0.3.0.tgz#6a3d3687f5baec41f7e9b99fe1953a2e5d19775e"
integrity sha512-kucVIjOmMc1f0tv53BJ/5WIX+MGLcKuoBhnGqQrgKJNqLByb/sVMWfW/Aw6hw0jgcqjJ2pi9E5y32zOIpaUlsA==
create-jest@^29.7.0:
version "29.7.0"
resolved "https://registry.yarnpkg.com/create-jest/-/create-jest-29.7.0.tgz#a355c5b3cb1e1af02ba177fe7afd7feee49a5320"
@@ -10484,6 +10494,13 @@ pluralize@^8.0.0:
resolved "https://registry.yarnpkg.com/pluralize/-/pluralize-8.0.0.tgz#1a6fa16a38d12a1901e0320fa017051c539ce3b1"
integrity sha512-Nc3IT5yHzflTfbjgqWcCPpo7DaKy4FnpB0l/zCAW0Tc7jxAiuqSxHasntB3D7887LSrA93kDJ9IXovxJYxyLCA==
png-chunks-extract@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/png-chunks-extract/-/png-chunks-extract-1.0.0.tgz#fad4a905e66652197351c65e35b92c64311e472d"
integrity sha512-ZiVwF5EJ0DNZyzAqld8BP1qyJBaGOFaq9zl579qfbkcmOwWLLO4I9L8i2O4j3HkI6/35i0nKG2n+dZplxiT89Q==
dependencies:
crc-32 "^0.3.0"
polished@^4.2.2:
version "4.2.2"
resolved "https://registry.yarnpkg.com/polished/-/polished-4.2.2.tgz#2529bb7c3198945373c52e34618c8fe7b1aa84d1"