From 5bc20d9c01bb81bda96c87316cf33cc728ac854c Mon Sep 17 00:00:00 2001 From: WEI ZHANG Date: Wed, 7 Jan 2026 17:19:01 +0800 Subject: [PATCH] feat(sheets-drawing-ui): add API to register custom URL image downloader (#6430) Co-authored-by: weird94 --- .../sheets-drawing-ui/src/facade/f-univer.ts | 27 ++++++++++++-- .../src/services/batch-save-images.service.ts | 36 +++++++++++++++++-- 2 files changed, 59 insertions(+), 4 deletions(-) diff --git a/packages/sheets-drawing-ui/src/facade/f-univer.ts b/packages/sheets-drawing-ui/src/facade/f-univer.ts index 16207e384b..c0c5d438aa 100644 --- a/packages/sheets-drawing-ui/src/facade/f-univer.ts +++ b/packages/sheets-drawing-ui/src/facade/f-univer.ts @@ -14,7 +14,7 @@ * limitations under the License. */ -import type { IDrawingSearch, Injector } from '@univerjs/core'; +import type { IDisposable, IDrawingSearch, Injector } from '@univerjs/core'; import type { IEventBase } from '@univerjs/core/facade'; import type { ISheetFloatDom, ISheetImage } from '@univerjs/sheets-drawing'; import type { IDeleteDrawingCommandParams, IInsertDrawingCommandParams, ISetDrawingCommandParams } from '@univerjs/sheets-drawing-ui'; @@ -28,7 +28,7 @@ import type { import { CanceledError, DrawingTypeEnum, ICommandService } from '@univerjs/core'; import { FUniver } from '@univerjs/core/facade'; import { IDrawingManagerService, SetDrawingSelectedOperation } from '@univerjs/drawing'; -import { InsertSheetDrawingCommand, RemoveSheetDrawingCommand, SetSheetDrawingCommand } from '@univerjs/sheets-drawing-ui'; +import { IBatchSaveImagesService, InsertSheetDrawingCommand, RemoveSheetDrawingCommand, SetSheetDrawingCommand } from '@univerjs/sheets-drawing-ui'; import { FOverGridImage } from './f-over-grid-image'; interface IBeforeOverGridImageInsertParam extends IEventBase { @@ -476,6 +476,29 @@ export class FUniverDrawingMixin extends FUniver { return this._injector.createInstance(FOverGridImage, drawing); }); } + + /** + * Register a custom image downloader for URL images + * @param downloader The downloader function that takes a URL and returns a base64 string + * @returns A disposable object to unregister the downloader + * @example + * ```ts + * const disposable = univerAPI.registerURLImageDownloader(async (url) => { + * const response = await fetch(url); + * const blob = await response.blob(); + * const base64 = await new Promise((resolve) => { + * const reader = new FileReader(); + * reader.onloadend = () => resolve(reader.result as string); + * reader.readAsDataURL(blob); + * }); + * return base64; + * }); + * ``` + */ + registerURLImageDownloader(downloader: (url: string) => Promise): IDisposable { + const batchSaveImagesService = this._injector.get(IBatchSaveImagesService); + return batchSaveImagesService.registerURLImageDownloader(downloader); + } } FUniver.extend(FUniverDrawingMixin); diff --git a/packages/sheets-drawing-ui/src/services/batch-save-images.service.ts b/packages/sheets-drawing-ui/src/services/batch-save-images.service.ts index 2d1a5df937..41a44a4cc6 100644 --- a/packages/sheets-drawing-ui/src/services/batch-save-images.service.ts +++ b/packages/sheets-drawing-ui/src/services/batch-save-images.service.ts @@ -14,9 +14,9 @@ * limitations under the License. */ -import type { ICellData, IRange, Nullable, Workbook } from '@univerjs/core'; +import type { ICellData, IDisposable, IRange, Nullable, Workbook } from '@univerjs/core'; import type { IImageData } from '@univerjs/drawing'; -import { createIdentifier, Disposable, IImageIoService, ImageSourceType, Inject, IUniverInstanceService, UniverInstanceType } from '@univerjs/core'; +import { createIdentifier, Disposable, IImageIoService, ImageSourceType, Inject, IUniverInstanceService, toDisposable, UniverInstanceType } from '@univerjs/core'; import { SheetsSelectionsService } from '@univerjs/sheets'; declare global { @@ -136,10 +136,20 @@ export interface IBatchSaveImagesService { */ getSelectionRowRange(): { startRow: number; endRow: number } | null; + /** + * Get all column indices that are within the current selection + */ /** * Get all column indices that are within the current selection */ getSelectionColumnIndices(): Set; + + /** + * Register a custom image downloader for URL images + * @param downloader The downloader function that takes a URL and returns a base64 string + * @returns A disposable object to unregister the downloader + */ + registerURLImageDownloader(downloader: (url: string) => Promise): IDisposable; } export const IBatchSaveImagesService = createIdentifier('sheets-drawing-ui.batch-save-images.service'); @@ -242,6 +252,8 @@ async function imageSourceToBlob(source: string, imageSourceType: ImageSourceTyp } export class BatchSaveImagesService extends Disposable implements IBatchSaveImagesService { + private _urlImageDownloader: Nullable<(url: string) => Promise> = null; + constructor( @IUniverInstanceService private readonly _univerInstanceService: IUniverInstanceService, @Inject(SheetsSelectionsService) private readonly _selectionService: SheetsSelectionsService, @@ -250,6 +262,15 @@ export class BatchSaveImagesService extends Disposable implements IBatchSaveImag super(); } + registerURLImageDownloader(downloader: (url: string) => Promise): IDisposable { + this._urlImageDownloader = downloader; + return toDisposable(() => { + if (this._urlImageDownloader === downloader) { + this._urlImageDownloader = null; + } + }); + } + getCellImagesInSelection(): ICellImageInfo[] { const workbook = this._univerInstanceService.getCurrentUnitForType(UniverInstanceType.UNIVER_SHEET); if (!workbook) return []; @@ -666,6 +687,17 @@ export class BatchSaveImagesService extends Disposable implements IBatchSaveImag const imageUrl = await this._imageIoService.getImage(imageInfo.source); return imageSourceToBlob(imageUrl, ImageSourceType.URL); } + + if (imageInfo.imageSourceType === ImageSourceType.URL && this._urlImageDownloader) { + try { + const base64 = await this._urlImageDownloader(imageInfo.source); + return imageSourceToBlob(base64, ImageSourceType.BASE64); + } catch (error) { + console.error(`Custom downloader failed for ${imageInfo.source}, falling back to default fetch:`, error); + // Fallback to default fetch if custom downloader fails + } + } + return imageSourceToBlob(imageInfo.source, imageInfo.imageSourceType); } }