feat: Add detailed logging for user profile components and image metadata extraction; enhance color handling utilities
This commit is contained in:
@@ -170,6 +170,8 @@ export function detectImageFormat(data: ArrayBuffer | Uint8Array): ImageFormat {
|
||||
export function extractColorFromImage(imageData: ArrayBuffer | Uint8Array): string | undefined {
|
||||
const format = detectImageFormat(imageData);
|
||||
|
||||
console.log('[extractColorFromImage] Detected format:', format);
|
||||
|
||||
switch (format) {
|
||||
case 'png':
|
||||
return extractColorFromPNG(imageData);
|
||||
@@ -211,14 +213,19 @@ export function extractBannerFromImage(imageData: ArrayBuffer | Uint8Array): str
|
||||
export function extractMetadataFromImage(imageData: ArrayBuffer | Uint8Array): ImageMetadata {
|
||||
const format = detectImageFormat(imageData);
|
||||
|
||||
console.log('[extractMetadataFromImage] Detected format:', format);
|
||||
|
||||
switch (format) {
|
||||
case 'png':
|
||||
return extractMetadataFromPNG(imageData);
|
||||
const pngMetadata = extractMetadataFromPNG(imageData);
|
||||
console.log('[extractMetadataFromImage] PNG metadata:', pngMetadata);
|
||||
return pngMetadata;
|
||||
// For other formats, extract color only for now
|
||||
case 'webp':
|
||||
case 'jpeg':
|
||||
case 'gif': {
|
||||
const color = extractColorFromImage(imageData);
|
||||
console.log('[extractMetadataFromImage] Non-PNG color:', color);
|
||||
return color ? { color } : {};
|
||||
}
|
||||
default:
|
||||
@@ -344,6 +351,10 @@ export function getExtension(format: ImageFormat): string {
|
||||
const avatarMetadataCache = new LRUCache<string, { metadata: ImageMetadata; timestamp: number }>(100);
|
||||
const METADATA_CACHE_TTL_MS = 30 * 60 * 1000; // 30 minutes
|
||||
|
||||
// In-flight fetch tracking to prevent duplicate parallel requests
|
||||
const inflightColorFetches = new Map<string, Promise<string | undefined>>();
|
||||
const inflightMetadataFetches = new Map<string, Promise<ImageMetadata>>();
|
||||
|
||||
/**
|
||||
* Fetch and extract color from an image URL (supports all formats)
|
||||
* @param url - HTTP URL to fetch the image from
|
||||
@@ -354,29 +365,51 @@ export async function fetchAndExtractColor(url: string, accessToken?: string | n
|
||||
// Check if full metadata is cached
|
||||
const cached = avatarMetadataCache.get(url);
|
||||
if (cached && Date.now() - cached.timestamp < METADATA_CACHE_TTL_MS) {
|
||||
console.log('[fetchAndExtractColor] Using cached color for', url, ':', cached.metadata.color);
|
||||
return cached.metadata.color;
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await fetch(url, {
|
||||
method: 'GET',
|
||||
headers: accessToken ? { Authorization: `Bearer ${accessToken}` } : undefined,
|
||||
});
|
||||
if (!response.ok) return undefined;
|
||||
|
||||
const data = await response.arrayBuffer();
|
||||
const color = extractColorFromImage(data);
|
||||
|
||||
// Cache the color result as part of metadata
|
||||
avatarMetadataCache.set(url, {
|
||||
metadata: { color },
|
||||
timestamp: Date.now()
|
||||
});
|
||||
|
||||
return color;
|
||||
} catch {
|
||||
return undefined;
|
||||
// Check if fetch already in progress
|
||||
const inflightKey = `${url}:${accessToken || ''}`;
|
||||
const existingFetch = inflightColorFetches.get(inflightKey);
|
||||
if (existingFetch) {
|
||||
console.log('[fetchAndExtractColor] Reusing in-flight fetch for', url);
|
||||
return existingFetch;
|
||||
}
|
||||
|
||||
console.log('[fetchAndExtractColor] Fetching and extracting color from', url);
|
||||
|
||||
const fetchPromise = (async () => {
|
||||
try {
|
||||
const response = await fetch(url, {
|
||||
method: 'GET',
|
||||
headers: accessToken ? { Authorization: `Bearer ${accessToken}` } : undefined,
|
||||
});
|
||||
|
||||
console.log('[fetchAndExtractColor] Fetch response status:', response.ok, response.status);
|
||||
|
||||
if (!response.ok) return undefined;
|
||||
|
||||
const data = await response.arrayBuffer();
|
||||
console.log('[fetchAndExtractColor] Downloaded', data.byteLength, 'bytes');
|
||||
|
||||
const color = extractColorFromImage(data);
|
||||
console.log('[fetchAndExtractColor] Extracted color:', color);
|
||||
|
||||
// Don't cache here - fetchAndExtractMetadata will cache full metadata
|
||||
// This avoids cache collision where color-only fetch overwrites full metadata
|
||||
|
||||
return color;
|
||||
} catch (error) {
|
||||
console.error('[fetchAndExtractColor] Error fetching/extracting color:', error);
|
||||
return undefined;
|
||||
} finally {
|
||||
inflightColorFetches.delete(inflightKey);
|
||||
}
|
||||
})();
|
||||
|
||||
inflightColorFetches.set(inflightKey, fetchPromise);
|
||||
return fetchPromise;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -389,27 +422,51 @@ export async function fetchAndExtractMetadata(url: string, accessToken?: string
|
||||
// Check cache first
|
||||
const cached = avatarMetadataCache.get(url);
|
||||
if (cached && Date.now() - cached.timestamp < METADATA_CACHE_TTL_MS) {
|
||||
console.log('[fetchAndExtractMetadata] Using cached metadata for', url, ':', cached.metadata);
|
||||
return cached.metadata;
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await fetch(url, {
|
||||
method: 'GET',
|
||||
headers: accessToken ? { Authorization: `Bearer ${accessToken}` } : undefined,
|
||||
});
|
||||
if (!response.ok) return {};
|
||||
|
||||
const data = await response.arrayBuffer();
|
||||
const metadata = extractMetadataFromImage(data);
|
||||
|
||||
// Cache the result
|
||||
avatarMetadataCache.set(url, {
|
||||
metadata,
|
||||
timestamp: Date.now()
|
||||
});
|
||||
|
||||
return metadata;
|
||||
} catch {
|
||||
return {};
|
||||
// Check if fetch already in progress
|
||||
const inflightKey = `${url}:${accessToken || ''}`;
|
||||
const existingFetch = inflightMetadataFetches.get(inflightKey);
|
||||
if (existingFetch) {
|
||||
console.log('[fetchAndExtractMetadata] Reusing in-flight fetch for', url);
|
||||
return existingFetch;
|
||||
}
|
||||
|
||||
console.log('[fetchAndExtractMetadata] Fetching metadata from', url);
|
||||
|
||||
const fetchPromise = (async () => {
|
||||
try {
|
||||
const response = await fetch(url, {
|
||||
method: 'GET',
|
||||
headers: accessToken ? { Authorization: `Bearer ${accessToken}` } : undefined,
|
||||
});
|
||||
|
||||
console.log('[fetchAndExtractMetadata] Fetch response status:', response.ok, response.status);
|
||||
|
||||
if (!response.ok) return {};
|
||||
|
||||
const data = await response.arrayBuffer();
|
||||
console.log('[fetchAndExtractMetadata] Downloaded', data.byteLength, 'bytes');
|
||||
|
||||
const metadata = extractMetadataFromImage(data);
|
||||
console.log('[fetchAndExtractMetadata] Extracted metadata:', metadata);
|
||||
|
||||
// Cache the result
|
||||
avatarMetadataCache.set(url, {
|
||||
metadata,
|
||||
timestamp: Date.now()
|
||||
});
|
||||
|
||||
return metadata;
|
||||
} catch {
|
||||
return {};
|
||||
} finally {
|
||||
inflightMetadataFetches.delete(inflightKey);
|
||||
}
|
||||
})();
|
||||
|
||||
inflightMetadataFetches.set(inflightKey, fetchPromise);
|
||||
return fetchPromise;
|
||||
}
|
||||
|
||||
@@ -153,21 +153,30 @@ export function extractColorFromPNG(imageData: ArrayBuffer | Uint8Array): string
|
||||
// Verify PNG signature
|
||||
for (let i = 0; i < 8; i++) {
|
||||
if (data[i] !== PNG_SIGNATURE[i]) {
|
||||
console.warn('[extractColorFromPNG] Not a valid PNG');
|
||||
return undefined; // Not a PNG
|
||||
}
|
||||
}
|
||||
|
||||
const chunks = parseChunks(data);
|
||||
console.log('[extractColorFromPNG] Found', chunks.length, 'chunks');
|
||||
|
||||
let textChunksFound = 0;
|
||||
for (const chunk of chunks) {
|
||||
if (chunk.type === 'tEXt') {
|
||||
textChunksFound++;
|
||||
const text = parseTextChunk(chunk.data);
|
||||
if (text && text.key === PAARROT_COLOR_KEY) {
|
||||
return text.value;
|
||||
if (text) {
|
||||
console.log('[extractColorFromPNG] Found tEXt chunk:', text.key, '=', text.value);
|
||||
if (text.key === PAARROT_COLOR_KEY) {
|
||||
console.log('[extractColorFromPNG] Found paarrot:color =', text.value);
|
||||
return text.value;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
console.log('[extractColorFromPNG] No paarrot:color found (found', textChunksFound, 'tEXt chunks)');
|
||||
return undefined;
|
||||
}
|
||||
|
||||
@@ -214,16 +223,21 @@ export function extractMetadataFromPNG(imageData: ArrayBuffer | Uint8Array): Ima
|
||||
// Verify PNG signature
|
||||
for (let i = 0; i < 8; i++) {
|
||||
if (data[i] !== PNG_SIGNATURE[i]) {
|
||||
console.warn('[extractMetadataFromPNG] Not a valid PNG');
|
||||
return metadata; // Not a PNG
|
||||
}
|
||||
}
|
||||
|
||||
const chunks = parseChunks(data);
|
||||
console.log('[extractMetadataFromPNG] Found', chunks.length, 'chunks');
|
||||
|
||||
let textChunksFound = 0;
|
||||
for (const chunk of chunks) {
|
||||
if (chunk.type === 'tEXt') {
|
||||
textChunksFound++;
|
||||
const text = parseTextChunk(chunk.data);
|
||||
if (text) {
|
||||
console.log('[extractMetadataFromPNG] Found tEXt chunk:', text.key, '=', text.value);
|
||||
if (text.key === PAARROT_COLOR_KEY) {
|
||||
metadata.color = text.value;
|
||||
} else if (text.key === PAARROT_BANNER_KEY) {
|
||||
@@ -241,6 +255,8 @@ export function extractMetadataFromPNG(imageData: ArrayBuffer | Uint8Array): Ima
|
||||
}
|
||||
}
|
||||
|
||||
console.log('[extractMetadataFromPNG] Final metadata extracted:', metadata, 'found', textChunksFound, 'tEXt chunks');
|
||||
|
||||
return metadata;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user