diff --git a/package-lock.json b/package-lock.json index 66e96fc..6e59fa2 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,6 +10,7 @@ "dependencies": { "@types/qrcode": "^1.5.6", "framer-motion": "^11.2.0", + "heic-convert": "^2.1.0", "lucide-react": "^0.400.0", "next": "^16.1.6", "qrcode": "^1.5.4", @@ -2334,6 +2335,41 @@ "node": ">= 0.4" } }, + "node_modules/heic-convert": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/heic-convert/-/heic-convert-2.1.0.tgz", + "integrity": "sha512-1qDuRvEHifTVAj3pFIgkqGgJIr0M3X7cxEPjEp0oG4mo8GFjq99DpCo8Eg3kg17Cy0MTjxpFdoBHOatj7ZVKtg==", + "license": "ISC", + "dependencies": { + "heic-decode": "^2.0.0", + "jpeg-js": "^0.4.4", + "pngjs": "^6.0.0" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/heic-convert/node_modules/pngjs": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/pngjs/-/pngjs-6.0.0.tgz", + "integrity": "sha512-TRzzuFRRmEoSW/p1KVAmiOgPco2Irlah+bGFCeNfJXxxYGwSw7YwAOAcd7X28K/m5bjBWKsC29KyoMfHbypayg==", + "license": "MIT", + "engines": { + "node": ">=12.13.0" + } + }, + "node_modules/heic-decode": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/heic-decode/-/heic-decode-2.1.0.tgz", + "integrity": "sha512-0fB3O3WMk38+PScbHLVp66jcNhsZ/ErtQ6u2lMYu/YxXgbBtl+oKOhGQHa4RpvE68k8IzbWkABzHnyAIjR758A==", + "license": "ISC", + "dependencies": { + "libheif-js": "^1.19.8" + }, + "engines": { + "node": ">=8.0.0" + } + }, "node_modules/is-binary-path": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", @@ -2415,12 +2451,27 @@ "jiti": "bin/jiti.js" } }, + "node_modules/jpeg-js": { + "version": "0.4.4", + "resolved": "https://registry.npmjs.org/jpeg-js/-/jpeg-js-0.4.4.tgz", + "integrity": "sha512-WZzeDOEtTOBK4Mdsar0IqEU5sMr3vSV2RqkAIzUEV2BHnUfKGyswWFPFwK5EeDo93K3FohSHbLAjj0s1Wzd+dg==", + "license": "BSD-3-Clause" + }, "node_modules/js-tokens": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", "license": "MIT" }, + "node_modules/libheif-js": { + "version": "1.19.8", + "resolved": "https://registry.npmjs.org/libheif-js/-/libheif-js-1.19.8.tgz", + "integrity": "sha512-vQJWusIxO7wavpON1dusciL8Go9jsIQ+EUrckauFYAiSTjcmLAsuJh3SszLpvkwPci3JcL41ek2n+LUZGFpPIQ==", + "license": "LGPL-3.0", + "engines": { + "node": ">=8.0.0" + } + }, "node_modules/lilconfig": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-3.1.3.tgz", diff --git a/package.json b/package.json index 57b30c6..22f38fe 100644 --- a/package.json +++ b/package.json @@ -12,6 +12,7 @@ "dependencies": { "@types/qrcode": "^1.5.6", "framer-motion": "^11.2.0", + "heic-convert": "^2.1.0", "lucide-react": "^0.400.0", "next": "^16.1.6", "qrcode": "^1.5.4", diff --git a/src/app/api/family-upload/route.ts b/src/app/api/family-upload/route.ts index 23b418c..63f07e5 100644 --- a/src/app/api/family-upload/route.ts +++ b/src/app/api/family-upload/route.ts @@ -3,7 +3,7 @@ import { writeFile, mkdir } from 'fs/promises' import path from 'path' import { randomUUID } from 'crypto' import { getDb } from '@/lib/db' -import sharp from 'sharp' +import { convertHeicToJpeg } from '@/lib/heic' export const runtime = 'nodejs' export const maxDuration = 60 @@ -77,7 +77,7 @@ export async function POST(req: NextRequest) { let finalExt = ext if (mimeType === 'image/heic' || mimeType === 'image/heif') { try { - finalBuffer = Buffer.from(await sharp(buffer).jpeg({ quality: 90 }).toBuffer()) + finalBuffer = await convertHeicToJpeg(buffer) finalExt = '.jpg' } catch { // Conversion failed — keep original diff --git a/src/app/api/files/[...path]/route.ts b/src/app/api/files/[...path]/route.ts index 5f0d641..cf34ecc 100644 --- a/src/app/api/files/[...path]/route.ts +++ b/src/app/api/files/[...path]/route.ts @@ -2,6 +2,7 @@ import { NextRequest, NextResponse } from 'next/server' import fs from 'fs' import path from 'path' import sharp from 'sharp' +import { convertHeicToJpeg } from '@/lib/heic' export const runtime = 'nodejs' @@ -55,7 +56,7 @@ export async function GET( contentType = 'image/jpeg' } else { try { - const converted = await sharp(fs.readFileSync(filePath), { failOn: 'none' }).jpeg({ quality: 90 }).toBuffer() + const converted = await convertHeicToJpeg(fs.readFileSync(filePath)) fs.writeFileSync(jpegPath, converted) fileToSendPath = jpegPath ext = '.jpg' diff --git a/src/app/api/upload/route.ts b/src/app/api/upload/route.ts index 3b7272f..4cb1529 100644 --- a/src/app/api/upload/route.ts +++ b/src/app/api/upload/route.ts @@ -4,7 +4,7 @@ import path from 'path' import { cookies } from 'next/headers' import { createHash, randomUUID } from 'crypto' import { getDb } from '@/lib/db' -import sharp from 'sharp' +import { convertHeicToJpeg } from '@/lib/heic' export const runtime = 'nodejs' export const maxDuration = 60 @@ -112,8 +112,8 @@ export async function POST(req: NextRequest) { // Convert HEIC/HEIF to JPEG so all browsers can display it if (mimeType === 'image/heic' || mimeType === 'image/heif') { try { - const converted = await sharp(buffer).jpeg({ quality: 90 }).toBuffer() - buffer = converted as unknown as Buffer + const converted = await convertHeicToJpeg(buffer) + buffer = converted ext = '.jpg' mimeType = 'image/jpeg' } catch { diff --git a/src/lib/heic.ts b/src/lib/heic.ts new file mode 100644 index 0000000..595a4c3 --- /dev/null +++ b/src/lib/heic.ts @@ -0,0 +1,15 @@ +// @ts-expect-error no type declarations available +import heicConvert from 'heic-convert' + +/** + * Convert HEIC/HEIF buffer to JPEG. + * Uses heic-convert (pure JS) since sharp's prebuilt libvips lacks HEIF support. + */ +export async function convertHeicToJpeg(buffer: Buffer): Promise { + const result = await heicConvert({ + buffer, + format: 'JPEG', + quality: 0.9, + }) + return Buffer.from(result) +}