fix(editor): image size and xywh when converting attachment to image (#10200)

In Edgeless, the image size should be read when converting attachment to image:

* fix `size`
* fix `xywh`
This commit is contained in:
fundon
2025-02-20 05:16:21 +00:00
parent ff0ce1a962
commit adc003862b
10 changed files with 87 additions and 41 deletions

View File

@@ -1,20 +1,25 @@
import type { import {
AttachmentBlockModel, type AttachmentBlockModel,
ImageBlockProps, type ImageBlockProps,
MAX_IMAGE_WIDTH,
} from '@blocksuite/affine-model'; } from '@blocksuite/affine-model';
import { FileSizeLimitService } from '@blocksuite/affine-shared/services'; import { FileSizeLimitService } from '@blocksuite/affine-shared/services';
import { import {
readImageSize,
transformModel, transformModel,
withTempBlobData, withTempBlobData,
} from '@blocksuite/affine-shared/utils'; } from '@blocksuite/affine-shared/utils';
import { type BlockStdScope, StdIdentifier } from '@blocksuite/block-std'; import { type BlockStdScope, StdIdentifier } from '@blocksuite/block-std';
import type { Container } from '@blocksuite/global/di'; import type { Container } from '@blocksuite/global/di';
import { createIdentifier } from '@blocksuite/global/di'; import { createIdentifier } from '@blocksuite/global/di';
import { Bound } from '@blocksuite/global/utils';
import type { ExtensionType } from '@blocksuite/store'; import type { ExtensionType } from '@blocksuite/store';
import { Extension } from '@blocksuite/store'; import { Extension } from '@blocksuite/store';
import type { TemplateResult } from 'lit'; import type { TemplateResult } from 'lit';
import { html } from 'lit'; import { html } from 'lit';
import { getAttachmentBlob } from './utils';
export type AttachmentEmbedConfig = { export type AttachmentEmbedConfig = {
name: string; name: string;
/** /**
@@ -24,7 +29,10 @@ export type AttachmentEmbedConfig = {
/** /**
* The action will be executed when the 「Turn into embed view」 button is clicked. * The action will be executed when the 「Turn into embed view」 button is clicked.
*/ */
action?: (model: AttachmentBlockModel) => Promise<void> | void; action?: (
model: AttachmentBlockModel,
std: BlockStdScope
) => Promise<void> | void;
/** /**
* The template will be used to render the embed view. * The template will be used to render the embed view.
*/ */
@@ -89,11 +97,11 @@ export class AttachmentEmbedService extends Extension {
// Converts to embed view. // Converts to embed view.
convertTo(model: AttachmentBlockModel, maxFileSize = this._maxFileSize) { convertTo(model: AttachmentBlockModel, maxFileSize = this._maxFileSize) {
const config = this.values.find(config => config.check(model, maxFileSize)); const config = this.values.find(config => config.check(model, maxFileSize));
if (!config || !config.action) { if (!config?.action) {
model.doc.updateBlock(model, { embed: true }); model.doc.updateBlock(model, { embed: true });
return; return;
} }
config.action(model)?.catch(console.error); config.action(model, this.std)?.catch(console.error);
} }
embedded(model: AttachmentBlockModel, maxFileSize = this._maxFileSize) { embedded(model: AttachmentBlockModel, maxFileSize = this._maxFileSize) {
@@ -123,7 +131,12 @@ const embedConfig: AttachmentEmbedConfig[] = [
check: model => check: model =>
model.doc.schema.flavourSchemaMap.has('affine:image') && model.doc.schema.flavourSchemaMap.has('affine:image') &&
model.type.startsWith('image/'), model.type.startsWith('image/'),
action: model => turnIntoImageBlock(model), async action(model, std) {
const component = std.view.getBlock(model.id);
if (!component) return;
await turnIntoImageBlock(model);
},
}, },
{ {
name: 'pdf', name: 'pdf',
@@ -171,7 +184,7 @@ const embedConfig: AttachmentEmbedConfig[] = [
/** /**
* Turn the attachment block into an image block. * Turn the attachment block into an image block.
*/ */
export function turnIntoImageBlock(model: AttachmentBlockModel) { export async function turnIntoImageBlock(model: AttachmentBlockModel) {
if (!model.doc.schema.flavourSchemaMap.has('affine:image')) { if (!model.doc.schema.flavourSchemaMap.has('affine:image')) {
console.error('The image flavour is not supported!'); console.error('The image flavour is not supported!');
return; return;
@@ -183,15 +196,37 @@ export function turnIntoImageBlock(model: AttachmentBlockModel) {
const { saveAttachmentData, getImageData } = withTempBlobData(); const { saveAttachmentData, getImageData } = withTempBlobData();
saveAttachmentData(sourceId, { name: model.name }); saveAttachmentData(sourceId, { name: model.name });
const imageConvertData = model.sourceId let imageSize = model.sourceId ? getImageData(model.sourceId) : undefined;
? getImageData(model.sourceId)
const bounds = model.xywh
? Bound.fromXYWH(model.deserializedXYWH)
: undefined; : undefined;
if (bounds) {
if (!imageSize?.width || !imageSize?.height) {
const blob = await getAttachmentBlob(model);
if (blob) {
imageSize = await readImageSize(blob);
}
}
if (imageSize?.width && imageSize?.height) {
const p = imageSize.height / imageSize.width;
imageSize.width = Math.min(imageSize.width, MAX_IMAGE_WIDTH);
imageSize.height = imageSize.width * p;
bounds.w = imageSize.width;
bounds.h = imageSize.height;
}
}
const others = bounds ? { xywh: bounds.serialize() } : undefined;
const imageProp: Partial<ImageBlockProps> = { const imageProp: Partial<ImageBlockProps> = {
sourceId, sourceId,
caption: model.caption, caption: model.caption,
size: model.size, size: model.size,
...imageConvertData, ...imageSize,
...others,
}; };
transformModel(model, 'affine:image', imageProp); transformModel(model, 'affine:image', imageProp);
} }

View File

@@ -97,7 +97,7 @@ export async function uploadAttachmentBlob(
} }
} }
async function getAttachmentBlob(model: AttachmentBlockModel) { export async function getAttachmentBlob(model: AttachmentBlockModel) {
const sourceId = model.sourceId; const sourceId = model.sourceId;
if (!sourceId) { if (!sourceId) {
return null; return null;

View File

@@ -12,6 +12,7 @@ import {
import { import {
downloadBlob, downloadBlob,
humanFileSize, humanFileSize,
readImageSize,
transformModel, transformModel,
withTempBlobData, withTempBlobData,
} from '@blocksuite/affine-shared/utils'; } from '@blocksuite/affine-shared/utils';
@@ -425,27 +426,6 @@ export async function turnImageIntoCardView(
transformModel(model, 'affine:attachment', attachmentProp); transformModel(model, 'affine:attachment', attachmentProp);
} }
export function readImageSize(file: File | Blob) {
return new Promise<{ width: number; height: number }>(resolve => {
const size = { width: 0, height: 0 };
const img = new Image();
img.onload = () => {
size.width = img.width;
size.height = img.height;
URL.revokeObjectURL(img.src);
resolve(size);
};
img.onerror = () => {
URL.revokeObjectURL(img.src);
resolve(size);
};
img.src = URL.createObjectURL(file);
});
}
export async function addImages( export async function addImages(
std: BlockStdScope, std: BlockStdScope,
files: File[], files: File[],

View File

@@ -25,6 +25,7 @@
"@toeverything/theme": "^1.1.11", "@toeverything/theme": "^1.1.11",
"@types/hast": "^3.0.4", "@types/hast": "^3.0.4",
"@types/mdast": "^4.0.4", "@types/mdast": "^4.0.4",
"dompurify": "^3.2.4",
"fractional-indexing": "^3.2.0", "fractional-indexing": "^3.2.0",
"lit": "^3.2.0", "lit": "^3.2.0",
"lodash.clonedeep": "^4.5.0", "lodash.clonedeep": "^4.5.0",

View File

@@ -0,0 +1,28 @@
import DOMPurify from 'dompurify';
export function readImageSize(file: File | Blob) {
return new Promise<{ width: number; height: number }>(resolve => {
const size = { width: 0, height: 0 };
if (!file.type.startsWith('image/')) {
resolve(size);
return;
}
const img = new Image();
img.onload = () => {
size.width = img.width;
size.height = img.height;
URL.revokeObjectURL(img.src);
resolve(size);
};
img.onerror = () => {
URL.revokeObjectURL(img.src);
resolve(size);
};
const sanitizedURL = DOMPurify.sanitize(URL.createObjectURL(file));
img.src = sanitizedURL;
});
}

View File

@@ -8,6 +8,7 @@ export * from './edgeless';
export * from './event'; export * from './event';
export * from './file'; export * from './file';
export * from './fractional-indexing'; export * from './fractional-indexing';
export * from './image';
export * from './insert'; export * from './insert';
export * from './is-abort-error'; export * from './is-abort-error';
export * from './math'; export * from './math';

View File

@@ -51,7 +51,7 @@
"@toeverything/theme": "^1.1.11", "@toeverything/theme": "^1.1.11",
"@vanilla-extract/css": "^1.17.0", "@vanilla-extract/css": "^1.17.0",
"date-fns": "^4.0.0", "date-fns": "^4.0.0",
"dompurify": "^3.1.6", "dompurify": "^3.2.4",
"fflate": "^0.8.2", "fflate": "^0.8.2",
"file-type": "^20.0.0", "file-type": "^20.0.0",
"fractional-indexing": "^3.2.0", "fractional-indexing": "^3.2.0",

View File

@@ -23,7 +23,7 @@
"@lit/context": "^1.1.2", "@lit/context": "^1.1.2",
"@preact/signals-core": "^1.8.0", "@preact/signals-core": "^1.8.0",
"@types/hast": "^3.0.4", "@types/hast": "^3.0.4",
"dompurify": "^3.1.6", "dompurify": "^3.2.4",
"fractional-indexing": "^3.2.0", "fractional-indexing": "^3.2.0",
"lib0": "^0.2.97", "lib0": "^0.2.97",
"lit": "^3.2.0", "lit": "^3.2.0",

View File

@@ -42,10 +42,10 @@
"version": 1, "version": 1,
"props": { "props": {
"sourceId": "ejImogf-Tb7AuKY-v94uz1zuOJbClqK-tWBxVr_ksGA=", "sourceId": "ejImogf-Tb7AuKY-v94uz1zuOJbClqK-tWBxVr_ksGA=",
"width": 0, "width": 460,
"height": 0, "height": 345,
"index": "a0", "index": "a0",
"xywh": "[0,0,0,0]", "xywh": "[0,0,460,345]",
"lockedBySelf": false, "lockedBySelf": false,
"rotate": 0, "rotate": 0,
"size": 45801, "size": 45801,

View File

@@ -3749,6 +3749,7 @@ __metadata:
"@types/lodash.clonedeep": "npm:^4.5.9" "@types/lodash.clonedeep": "npm:^4.5.9"
"@types/lodash.mergewith": "npm:^4" "@types/lodash.mergewith": "npm:^4"
"@types/mdast": "npm:^4.0.4" "@types/mdast": "npm:^4.0.4"
dompurify: "npm:^3.2.4"
fractional-indexing: "npm:^3.2.0" fractional-indexing: "npm:^3.2.0"
lit: "npm:^3.2.0" lit: "npm:^3.2.0"
lodash.clonedeep: "npm:^4.5.0" lodash.clonedeep: "npm:^4.5.0"
@@ -3896,7 +3897,7 @@ __metadata:
"@lit/context": "npm:^1.1.2" "@lit/context": "npm:^1.1.2"
"@preact/signals-core": "npm:^1.8.0" "@preact/signals-core": "npm:^1.8.0"
"@types/hast": "npm:^3.0.4" "@types/hast": "npm:^3.0.4"
dompurify: "npm:^3.1.6" dompurify: "npm:^3.2.4"
fractional-indexing: "npm:^3.2.0" fractional-indexing: "npm:^3.2.0"
lib0: "npm:^0.2.97" lib0: "npm:^0.2.97"
lit: "npm:^3.2.0" lit: "npm:^3.2.0"
@@ -3954,7 +3955,7 @@ __metadata:
"@vanilla-extract/css": "npm:^1.17.0" "@vanilla-extract/css": "npm:^1.17.0"
"@vanilla-extract/vite-plugin": "npm:^5.0.0" "@vanilla-extract/vite-plugin": "npm:^5.0.0"
date-fns: "npm:^4.0.0" date-fns: "npm:^4.0.0"
dompurify: "npm:^3.1.6" dompurify: "npm:^3.2.4"
fflate: "npm:^0.8.2" fflate: "npm:^0.8.2"
file-type: "npm:^20.0.0" file-type: "npm:^20.0.0"
fractional-indexing: "npm:^3.2.0" fractional-indexing: "npm:^3.2.0"
@@ -20043,7 +20044,7 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"dompurify@npm:^3.1.6": "dompurify@npm:^3.2.4":
version: 3.2.4 version: 3.2.4
resolution: "dompurify@npm:3.2.4" resolution: "dompurify@npm:3.2.4"
dependencies: dependencies: