From 05d9bb3babec76a815a650eb84c8bde2d4ae21ab Mon Sep 17 00:00:00 2001 From: Enrico Ros Date: Sun, 16 Mar 2025 07:07:04 -0700 Subject: [PATCH] Gemini: store compressed images. Save 80% at 98% quality (png -> webp) --- src/common/util/imageUtils.ts | 4 +++ src/modules/aix/client/ContentReassembler.ts | 38 ++++++++++++++++---- 2 files changed, 35 insertions(+), 7 deletions(-) diff --git a/src/common/util/imageUtils.ts b/src/common/util/imageUtils.ts index 0dcb842b2..044862751 100644 --- a/src/common/util/imageUtils.ts +++ b/src/common/util/imageUtils.ts @@ -54,6 +54,8 @@ export async function getImageDimensions(base64DataUrl: string): Promise<{ width export async function convertBase64Image(base64DataUrl: string, destMimeType: string /*= 'image/webp'*/, destQuality: number /*= 0.90*/): Promise<{ mimeType: string, base64: string, + width: number, + height: number, }> { return new Promise((resolve, reject) => { const image = new Image(); @@ -75,6 +77,8 @@ export async function convertBase64Image(base64DataUrl: string, destMimeType: st resolve({ mimeType: actualMimeType, base64: base64Data, + width: image.width, + height: image.height, }); } catch (error) { console.warn(`imageUtils: failed to convert image to ${destMimeType}.`, { error }); diff --git a/src/modules/aix/client/ContentReassembler.ts b/src/modules/aix/client/ContentReassembler.ts index 7e50a3fe1..f7bc5eb9e 100644 --- a/src/modules/aix/client/ContentReassembler.ts +++ b/src/modules/aix/client/ContentReassembler.ts @@ -1,9 +1,10 @@ import { addDBImageAsset } from '~/modules/dblobs/dblobs.images'; import type { MaybePromise } from '~/common/types/useful.types'; +import { DEFAULT_ADRAFT_IMAGE_MIMETYPE } from '~/common/attachment-drafts/attachment.pipeline'; +import { convertBase64Image, getImageDimensions } from '~/common/util/imageUtils'; import { create_CodeExecutionInvocation_ContentFragment, create_CodeExecutionResponse_ContentFragment, create_FunctionCallInvocation_ContentFragment, createAnnotationsVoidFragment, createDMessageDataRefDBlob, createDVoidWebCitation, createErrorContentFragment, createImageContentFragment, createModelAuxVoidFragment, createTextContentFragment, DVoidModelAuxPart, isContentFragment, isModelAuxPart, isTextContentFragment, isVoidAnnotationsFragment, isVoidFragment } from '~/common/stores/chat/chat.fragments'; import { ellipsizeMiddle } from '~/common/util/textUtils'; -import { getImageDimensions } from '~/common/util/imageUtils'; import { metricsFinishChatGenerateLg, metricsPendChatGenerateLg } from '~/common/stores/metrics/metrics.chatgenerate'; import { presentErrorToHumans } from '~/common/util/errorUtils'; @@ -16,6 +17,8 @@ import { AixChatGenerateContent_LL, DEBUG_PARTICLES } from './aix.client'; // configuration +const GENERATED_IMAGES_CONVERT_TO_COMPRESSED = true; // converts PNG to WebP or JPEG to save IndexedDB space +const GENERATED_IMAGES_COMPRESSION_QUALITY = 0.98; const ELLIPSIZE_DEV_ISSUE_MESSAGES = 4096; const MERGE_ISSUES_INTO_TEXT_PART_IF_OPEN = true; @@ -373,15 +376,36 @@ export class ContentReassembler { // Break text accumulation, as we have a full image part in the middle this.currentTextFragmentIndex = null; - const { mimeType, i_b64: base64Data, label } = particle; + let { mimeType, i_b64: base64Data, label, generator, prompt } = particle; const safeLabel = label || 'Generated Image'; try { + let safeWidth; + let safeHeight; + + // TODO: re-evaluate conversion-before-storage (quality is 0.98 and WebP is really optimized, but still, this is not the 'original' data) + // PNG -> conversion to WebP or JPEG to save IndexedDB space - will + if (GENERATED_IMAGES_CONVERT_TO_COMPRESSED && mimeType === 'image/png') { + const preSize = base64Data.length; + const convertedData = await convertBase64Image(`data:${mimeType};base64,${base64Data}`, DEFAULT_ADRAFT_IMAGE_MIMETYPE, GENERATED_IMAGES_COMPRESSION_QUALITY).catch(() => null); + if (convertedData) { + mimeType = convertedData.mimeType; + base64Data = convertedData.base64; + safeWidth = convertedData.width || 0; + safeHeight = convertedData.height || 0; + } + const postSize = base64Data.length; + const sizeDiffPerc = preSize ? Math.round(((postSize - preSize) / preSize) * 100) : 0; + console.warn(`[image-pipeline] stored generated PNG as ${mimeType} (quality:${GENERATED_IMAGES_COMPRESSION_QUALITY}, ${sizeDiffPerc}% reduction, ${preSize?.toLocaleString()} -> ${postSize?.toLocaleString()})`); + } + // find out the dimensions (frontend) - const dimensions = await getImageDimensions(`data:${mimeType};base64,${base64Data}`).catch(() => null); - const safeWidth = dimensions?.width || 0; - const safeHeight = dimensions?.height || 0; + if (!safeWidth || !safeHeight) { + const dimensions = await getImageDimensions(`data:${mimeType};base64,${base64Data}`).catch(() => null); + safeWidth = dimensions?.width || 0; + safeHeight = dimensions?.height || 0; + } // add the image to the DBlobs DB const dblobAssetId = await addDBImageAsset('global', 'app-chat', { @@ -393,8 +417,8 @@ export class ContentReassembler { origin: { ot: 'generated', source: 'ai-text-to-image', - generatorName: '', // ? - prompt: '', // ? + generatorName: generator ?? '', + prompt: prompt ?? '', parameters: {}, // ? generatedAt: new Date().toISOString(), },