Skip to main content
Glama
image-processing.ts6.79 kB
import {ContentBlock, ImageMetadataBlock} from "./types"; import {CONFIG} from "./config"; import { estimateBase64Size } from "./data-uri"; import { Buffer } from "buffer"; /** * Downloads images from image_metadata blocks and applies smart size/count limiting * Prioritizes keeping the most recent images (assumes content is ordered with newest items last) * Uses intelligent size calculation accounting for text content * * @param content Array of content blocks that may contain image_metadata blocks * @param maxImages Maximum number of images to keep (defaults to CONFIG.maxImages) * @param maxSizeMB Maximum response size in MB (defaults to CONFIG.maxResponseSizeMB) * @returns Promise resolving to content array with downloaded images or placeholders */ export async function downloadImages(content: (ContentBlock | ImageMetadataBlock)[], maxImages: number = CONFIG.maxImages, maxSizeMB: number = CONFIG.maxResponseSizeMB): Promise<ContentBlock[]> { // First apply count-based limiting to image_metadata blocks const countLimitedContent = applyCountBasedLimitToImageMetadata(content, maxImages); // Calculate text size to determine available image budget const textContent = countLimitedContent.filter(block => block.type !== "image_metadata"); const textSizeBytes = JSON.stringify(textContent).length; const maxSizeBytes = maxSizeMB * 1024 * 1024; const availableImageBudget = Math.max(0, maxSizeBytes - textSizeBytes); // Calculate per-image budget const imageMetadataBlocks = countLimitedContent.filter(block => block.type === "image_metadata") as ImageMetadataBlock[]; const perImageBudget = imageMetadataBlocks.length > 0 ? availableImageBudget / imageMetadataBlocks.length : 0; // Download images in parallel and replace image_metadata blocks const downloadPromises = countLimitedContent.map(async (block) => { if (block.type === "image_metadata") { if (block.inlineData) { return convertInlineImage(block, perImageBudget); } return await downloadSingleImage(block, perImageBudget); } return block as ContentBlock; }); return Promise.all(downloadPromises); } /** * Apply count-based limiting to image_metadata blocks * Keeps the most recent image_metadata blocks (newest last) */ function applyCountBasedLimitToImageMetadata(content: (ContentBlock | ImageMetadataBlock)[], maxImages: number): (ContentBlock | ImageMetadataBlock)[] { // Find all image_metadata block indices const imageMetadataIndices: number[] = []; content.forEach((block, index) => { if (block.type === "image_metadata") { imageMetadataIndices.push(index); } }); // If we have fewer images than the limit, return the original content if (imageMetadataIndices.length <= maxImages) { return content; } // Determine which image_metadata blocks to remove (keep the most recent ones) const imagesToRemove = imageMetadataIndices.slice(0, imageMetadataIndices.length - maxImages); // Create a new content array with excess image_metadata blocks replaced by text placeholders return content.map((block, index) => { if (block.type === "image_metadata" && imagesToRemove.includes(index)) { return { type: "text" as const, text: "[Image removed due to count limitations. Only the most recent images are shown.]", }; } return block; }); } /** * Download a single image from an image_metadata block, trying different sizes if needed */ async function downloadSingleImage(imageMetadata: ImageMetadataBlock, perImageBudget: number): Promise<ContentBlock> { const fallbackText = createImageFallback(imageMetadata); // Try each URL in order (largest to smallest) for (const url of imageMetadata.urls) { const abortController = new AbortController(); try { const response = await fetch(url, { signal: abortController.signal }); if (!response.ok) { console.error(`Failed to fetch image from ${url}: ${response.status}`); continue; } // Check Content-Length header first to avoid downloading large images const contentLength = response.headers.get("Content-Length"); if (contentLength) { const imageSizeBytes = parseInt(contentLength, 10); if (imageSizeBytes > perImageBudget) { // Cancel the request to stop any ongoing download abortController.abort(); console.error(`Image from ${url} is ${imageSizeBytes} bytes (from Content-Length), exceeds budget of ${perImageBudget} bytes`); // Continue to try smaller thumbnail continue; } } const imageBuffer = await response.arrayBuffer(); const actualSizeBytes = imageBuffer.byteLength; // Double-check actual size (in case Content-Length was missing or incorrect) if (actualSizeBytes <= perImageBudget) { return { type: "image", mimeType: response.headers.get("Content-Type") || "image/png", data: Buffer.from(imageBuffer).toString("base64"), }; } else { // Cancel to clean up the connection abortController.abort(); console.error(`Image from ${url} is ${actualSizeBytes} bytes (actual size), exceeds budget of ${perImageBudget} bytes`); // Continue to try smaller thumbnail } } catch (error: any) { if (error.name === 'AbortError') { // Request was cancelled, this is expected - continue to next URL continue; } console.error(`Error fetching image from ${url}: ${error.message || "Unknown error"}`); // Continue to try next URL } } // If all URLs failed or were too large, return fallback text return fallbackText; } /** * Create a fallback text block when an image cannot be included */ function createImageFallback(imageMetadata: ImageMetadataBlock): ContentBlock { return { type: "text" as const, text: `[Image "${imageMetadata.alt}" removed due to size limitations.]`, }; } /** * Convert inline image data URIs into image blocks while respecting size budgets */ function convertInlineImage(imageMetadata: ImageMetadataBlock, perImageBudget: number): ContentBlock { const inlineData = imageMetadata.inlineData; if (!inlineData) { return createImageFallback(imageMetadata); } const estimatedSize = estimateBase64Size(inlineData.base64Data); if (perImageBudget <= 0 || estimatedSize > perImageBudget) { console.error( `Inline image for "${imageMetadata.alt}" is ${estimatedSize} bytes, exceeds budget of ${perImageBudget} bytes` ); return createImageFallback(imageMetadata); } return { type: "image", mimeType: inlineData.mimeType || "image/png", data: inlineData.base64Data, }; }

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/hauptsacheNet/clickup-mcp'

If you have feedback or need assistance with the MCP directory API, please join our Discord server