···1import type { Item, WebsiteData } from './types';
2import { COLUMNS, margin, mobileMargin } from '$lib';
3import { CardDefinitionsByType } from './cards';
4-import { deleteRecord, putRecord } from '$lib/atproto';
5import { toast } from '@foxui/core';
6import * as TID from '@atcute/tid';
7···337 }
338}
339340-export function compressImage(file: File, maxSize: number = 900 * 1024): Promise<Blob> {
341 return new Promise((resolve, reject) => {
342 const img = new Image();
343 const reader = new FileReader();
···353 reader.readAsDataURL(file);
354355 img.onload = () => {
00000000356 let width = img.width;
357 let height = img.height;
358- const maxDimension = 2048;
359360 if (width > maxDimension || height > maxDimension) {
361 if (width > height) {
···375 if (!ctx) return reject(new Error('Failed to get canvas context.'));
376 ctx.drawImage(img, 0, 0, width, height);
377378- // Function to try compressing at a given quality
379- let quality = 0.8;
0380 function attemptCompression() {
381 canvas.toBlob(
382 (blob) => {
383 if (!blob) {
384 return reject(new Error('Compression failed.'));
385 }
386- // If the blob is under our size limit, or quality is too low, resolve it
387 if (blob.size <= maxSize || quality < 0.3) {
388- console.log('Compression successful. Blob size:', blob.size);
389- console.log('Quality:', quality);
390 resolve(blob);
391 } else {
392- // Otherwise, reduce the quality and try again
393 quality -= 0.1;
394 attemptCompression();
395 }
396 },
397- 'image/jpeg',
398 quality
399 );
400 }
···536 window.scrollTo({ top: offset + cellSize * (currentY - 1), behavior: 'smooth' });
537 }
538}
0000000000000000000000000000000000000000000000
···1import type { Item, WebsiteData } from './types';
2import { COLUMNS, margin, mobileMargin } from '$lib';
3import { CardDefinitionsByType } from './cards';
4+import { deleteRecord, getImageBlobUrl, putRecord, uploadBlob } from '$lib/atproto';
5import { toast } from '@foxui/core';
6import * as TID from '@atcute/tid';
7···337 }
338}
339340+export function compressImage(file: File | Blob, maxSize: number = 900 * 1024): Promise<Blob> {
341 return new Promise((resolve, reject) => {
342 const img = new Image();
343 const reader = new FileReader();
···353 reader.readAsDataURL(file);
354355 img.onload = () => {
356+ const maxDimension = 2048;
357+358+ // If image is already small enough, return original
359+ if (file.size <= maxSize) {
360+ console.log('skipping compression+resizing, already small enough');
361+ return resolve(file);
362+ }
363+364 let width = img.width;
365 let height = img.height;
0366367 if (width > maxDimension || height > maxDimension) {
368 if (width > height) {
···382 if (!ctx) return reject(new Error('Failed to get canvas context.'));
383 ctx.drawImage(img, 0, 0, width, height);
384385+ // Use WebP for both compression and transparency support
386+ let quality = 0.9;
387+388 function attemptCompression() {
389 canvas.toBlob(
390 (blob) => {
391 if (!blob) {
392 return reject(new Error('Compression failed.'));
393 }
0394 if (blob.size <= maxSize || quality < 0.3) {
00395 resolve(blob);
396 } else {
0397 quality -= 0.1;
398 attemptCompression();
399 }
400 },
401+ 'image/webp',
402 quality
403 );
404 }
···540 window.scrollTo({ top: offset + cellSize * (currentY - 1), behavior: 'smooth' });
541 }
542}
543+544+export async function checkAndUploadImage(
545+ objectWithImage: Record<string, any>,
546+ key: string = 'image'
547+) {
548+ if (!objectWithImage[key]) return;
549+550+ // Already uploaded as blob
551+ if (typeof objectWithImage[key] === 'object' && objectWithImage[key].$type === 'blob') {
552+ return;
553+ }
554+555+ if (typeof objectWithImage[key] === 'string') {
556+ // Download image from URL via proxy (to avoid CORS) and upload as blob
557+ try {
558+ const proxyUrl = `/api/image-proxy?url=${encodeURIComponent(objectWithImage[key])}`;
559+ const response = await fetch(proxyUrl);
560+ if (!response.ok) {
561+ console.error('Failed to fetch image:', objectWithImage[key]);
562+ return;
563+ }
564+ const blob = await response.blob();
565+ const compressedBlob = await compressImage(blob);
566+ objectWithImage[key] = await uploadBlob({ blob: compressedBlob });
567+ } catch (error) {
568+ console.error('Failed to download and upload image:', error);
569+ }
570+ return;
571+ }
572+573+ if (objectWithImage[key]?.blob) {
574+ const compressedBlob = await compressImage(objectWithImage[key].blob);
575+ objectWithImage[key] = await uploadBlob({ blob: compressedBlob });
576+ }
577+}
578+579+export function getImage(objectWithImage: Record<string, any>, did: string, key: string = 'image') {
580+ if (!objectWithImage[key]) return;
581+582+ if (objectWithImage[key].objectUrl) return objectWithImage[key].objectUrl;
583+584+ if (typeof objectWithImage[key] === 'object' && objectWithImage[key].$type === 'blob') {
585+ return getImageBlobUrl({ did, blob: objectWithImage[key] });
586+ }
587+ return objectWithImage[key];
588+}
+3-2
src/lib/website/EditableWebsite.svelte
···320321 item.cardType = isGif ? 'gif' : 'image';
322 item.cardData = {
323- blob: processedFile,
324- objectUrl
325 };
326327 // If grid position is provided
···492 // Reset the input so the same file can be selected again
493 target.value = '';
494 }
00495</script>
496497<svelte:body
···320321 item.cardType = isGif ? 'gif' : 'image';
322 item.cardData = {
323+ image: { blob: processedFile, objectUrl }
0324 };
325326 // If grid position is provided
···491 // Reset the input so the same file can be selected again
492 target.value = '';
493 }
494+495+ $inspect(items);
496</script>
497498<svelte:body
+44
src/routes/api/image-proxy/+server.ts
···00000000000000000000000000000000000000000000
···1+import { error } from '@sveltejs/kit';
2+3+export async function GET({ url }) {
4+ const imageUrl = url.searchParams.get('url');
5+ if (!imageUrl) {
6+ throw error(400, 'No URL provided');
7+ }
8+9+ try {
10+ new URL(imageUrl);
11+ } catch {
12+ throw error(400, 'Invalid URL');
13+ }
14+15+ try {
16+ const response = await fetch(imageUrl);
17+18+ if (!response.ok) {
19+ throw error(response.status, 'Failed to fetch image');
20+ }
21+22+ const contentType = response.headers.get('content-type');
23+24+ // Only allow image content types
25+ if (!contentType?.startsWith('image/')) {
26+ throw error(400, 'URL does not point to an image');
27+ }
28+29+ const blob = await response.blob();
30+31+ return new Response(blob, {
32+ headers: {
33+ 'Content-Type': contentType,
34+ 'Cache-Control': 'public, max-age=86400'
35+ }
36+ });
37+ } catch (err) {
38+ if (err && typeof err === 'object' && 'status' in err) {
39+ throw err;
40+ }
41+ console.error('Error proxying image:', err);
42+ throw error(500, 'Failed to proxy image');
43+ }
44+}