From 4550e8683231701443f184d072bee9e57aa9336d Mon Sep 17 00:00:00 2001 From: Tiwari Lokesh Date: Sun, 22 Mar 2026 15:45:37 +0530 Subject: [PATCH 1/9] feat: add support for multiple images to PDF conversion --- src/pages/tools/pdf/convert-to-pdf/index.tsx | 27 +++-- src/pages/tools/pdf/convert-to-pdf/service.ts | 107 +++++++++++++----- 2 files changed, 97 insertions(+), 37 deletions(-) diff --git a/src/pages/tools/pdf/convert-to-pdf/index.tsx b/src/pages/tools/pdf/convert-to-pdf/index.tsx index 750e843..c52c332 100644 --- a/src/pages/tools/pdf/convert-to-pdf/index.tsx +++ b/src/pages/tools/pdf/convert-to-pdf/index.tsx @@ -9,7 +9,9 @@ import { } from '@mui/material'; import React, { useState } from 'react'; import ToolContent from '@components/ToolContent'; -import ToolImageInput from 'components/input/ToolImageInput'; +import ToolMultipleImageInput, { + MultiImageInput +} from '@components/input/ToolMultipleImageInput'; import ToolFileResult from 'components/result/ToolFileResult'; import { ToolComponentProps } from '@tools/defineTool'; import { FormValues, Orientation, PageType, initialValues } from './types'; @@ -18,7 +20,8 @@ import { buildPdf } from './service'; const initialFormValues: FormValues = initialValues; export default function ConvertToPdf({ title }: ToolComponentProps) { - const [input, setInput] = useState(null); + const [input, setInput] = useState([]); + const [inputImages, setInputImages] = useState([]); const [result, setResult] = useState(null); const [imageSize, setImageSize] = useState<{ widthMm: number; @@ -27,10 +30,15 @@ export default function ConvertToPdf({ title }: ToolComponentProps) { heightPx: number; } | null>(null); + const handleInputChange = (files: MultiImageInput[]) => { + setInputImages(files); + setInput(files.map(({ file }) => file)); + }; + const compute = async (values: FormValues) => { - if (!input) return; + if (!input.length) return; const { pdfFile, imageSize } = await buildPdf({ - file: input, + files: input, pageType: values.pageType, orientation: values.orientation, scale: values.scale @@ -40,7 +48,7 @@ export default function ConvertToPdf({ title }: ToolComponentProps) { }; return ( - + title={title} input={input} setInput={setInput} @@ -48,9 +56,10 @@ export default function ConvertToPdf({ title }: ToolComponentProps) { compute={compute} inputComponent={ - } diff --git a/src/pages/tools/pdf/convert-to-pdf/service.ts b/src/pages/tools/pdf/convert-to-pdf/service.ts index 4a1eb22..af07aba 100644 --- a/src/pages/tools/pdf/convert-to-pdf/service.ts +++ b/src/pages/tools/pdf/convert-to-pdf/service.ts @@ -2,7 +2,7 @@ import jsPDF from 'jspdf'; import { Orientation, PageType, ImageSize } from './types'; export interface ComputeOptions { - file: File; + files: File[]; pageType: PageType; orientation: Orientation; scale: number; // 10..100 (only applied for A4) @@ -13,28 +13,54 @@ export interface ComputeResult { imageSize: ImageSize; } +interface LoadedImage { + image: HTMLImageElement; + objectUrl: string; + format: 'PNG' | 'JPEG'; +} + +function getPdfImageFormat(file: File): 'PNG' | 'JPEG' { + if (file.type === 'image/png' || file.name.toLowerCase().endsWith('.png')) { + return 'PNG'; + } + + return 'JPEG'; +} + +async function loadImage(file: File): Promise { + const image = new Image(); + const objectUrl = URL.createObjectURL(file); + image.src = objectUrl; + await image.decode(); + return { image, objectUrl, format: getPdfImageFormat(file) }; +} + export async function buildPdf({ - file, + files, pageType, orientation, scale }: ComputeOptions): Promise { - const img = new Image(); - img.src = URL.createObjectURL(file); + if (!files.length) { + throw new Error('No files selected'); + } + + let loadedImages: LoadedImage[] = []; try { - await img.decode(); - const pxToMm = (px: number) => px * 0.264583; - const imgWidthMm = pxToMm(img.width); - const imgHeightMm = pxToMm(img.height); + loadedImages = await Promise.all(files.map((file) => loadImage(file))); + const firstImage = loadedImages[0].image; + const firstImageWidthMm = pxToMm(firstImage.width); + const firstImageHeightMm = pxToMm(firstImage.height); const pdf = pageType === 'full' ? new jsPDF({ - orientation: imgWidthMm > imgHeightMm ? 'landscape' : 'portrait', + orientation: + firstImageWidthMm > firstImageHeightMm ? 'landscape' : 'portrait', unit: 'mm', - format: [imgWidthMm, imgHeightMm] + format: [firstImageWidthMm, firstImageHeightMm] }) : new jsPDF({ orientation, @@ -44,37 +70,62 @@ export async function buildPdf({ pdf.setDisplayMode('fullwidth'); - const pageWidth = pdf.internal.pageSize.getWidth(); - const pageHeight = pdf.internal.pageSize.getHeight(); + for (let index = 0; index < loadedImages.length; index += 1) { + const { image, format } = loadedImages[index]; + const imageWidthMm = pxToMm(image.width); + const imageHeightMm = pxToMm(image.height); - const widthRatio = pageWidth / img.width; - const heightRatio = pageHeight / img.height; - const fitScale = Math.min(widthRatio, heightRatio); + if (index > 0) { + if (pageType === 'full') { + pdf.addPage( + [imageWidthMm, imageHeightMm], + imageWidthMm > imageHeightMm ? 'landscape' : 'portrait' + ); + } else { + pdf.addPage('a4', orientation); + } + } - const finalWidth = - pageType === 'full' ? pageWidth : img.width * fitScale * (scale / 100); + const pageWidth = pdf.internal.pageSize.getWidth(); + const pageHeight = pdf.internal.pageSize.getHeight(); - const finalHeight = - pageType === 'full' ? pageHeight : img.height * fitScale * (scale / 100); + const widthRatio = pageWidth / image.width; + const heightRatio = pageHeight / image.height; + const fitScale = Math.min(widthRatio, heightRatio); - const x = pageType === 'full' ? 0 : (pageWidth - finalWidth) / 2; - const y = pageType === 'full' ? 0 : (pageHeight - finalHeight) / 2; + const finalWidth = + pageType === 'full' + ? pageWidth + : image.width * fitScale * (scale / 100); - pdf.addImage(img, 'JPEG', x, y, finalWidth, finalHeight); + const finalHeight = + pageType === 'full' + ? pageHeight + : image.height * fitScale * (scale / 100); + + const x = pageType === 'full' ? 0 : (pageWidth - finalWidth) / 2; + const y = pageType === 'full' ? 0 : (pageHeight - finalHeight) / 2; + + pdf.addImage(image, format, x, y, finalWidth, finalHeight); + } const blob = pdf.output('blob'); - const fileName = file.name.replace(/\.[^/.]+$/, '') + '.pdf'; + const firstFileName = files[0].name.replace(/\.[^/.]+$/, ''); + const fileName = + files.length === 1 + ? `${firstFileName}.pdf` + : `${firstFileName}-merged.pdf`; return { pdfFile: new File([blob], fileName, { type: 'application/pdf' }), imageSize: { - widthMm: imgWidthMm, - heightMm: imgHeightMm, - widthPx: img.width, - heightPx: img.height + widthMm: firstImageWidthMm, + heightMm: firstImageHeightMm, + widthPx: firstImage.width, + heightPx: firstImage.height } }; } finally { - URL.revokeObjectURL(img.src); + loadedImages.forEach(({ objectUrl }) => URL.revokeObjectURL(objectUrl)); } } From 2ec064a11cf28414211a65941c10860efd6bf9ac Mon Sep 17 00:00:00 2001 From: Chesterkxng Date: Tue, 24 Mar 2026 21:07:10 +0100 Subject: [PATCH 2/9] chore: types moved into types.ts file --- src/pages/tools/pdf/convert-to-pdf/types.ts | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/src/pages/tools/pdf/convert-to-pdf/types.ts b/src/pages/tools/pdf/convert-to-pdf/types.ts index 93b9a42..42d0567 100644 --- a/src/pages/tools/pdf/convert-to-pdf/types.ts +++ b/src/pages/tools/pdf/convert-to-pdf/types.ts @@ -8,14 +8,15 @@ export interface ImageSize { heightPx: number; } -export interface FormValues { +export interface InitialValuesType { pageType: PageType; orientation: Orientation; scale: number; } -export const initialValues: FormValues = { - pageType: 'full', - orientation: 'portrait', - scale: 100 -}; +export interface LoadedImage { + image: HTMLImageElement; + objectUrl: string; + format: 'PNG' | 'JPEG'; + filename: string; +} From 77191da9a37935de7218535df6c4682e5c84ad8e Mon Sep 17 00:00:00 2001 From: Chesterkxng Date: Tue, 24 Mar 2026 21:29:42 +0100 Subject: [PATCH 3/9] chore: - Heic/Heif conversion support - Array of sizes returned since multiple images support --- src/pages/tools/pdf/convert-to-pdf/service.ts | 117 +++++++++++------- 1 file changed, 74 insertions(+), 43 deletions(-) diff --git a/src/pages/tools/pdf/convert-to-pdf/service.ts b/src/pages/tools/pdf/convert-to-pdf/service.ts index af07aba..8c25b96 100644 --- a/src/pages/tools/pdf/convert-to-pdf/service.ts +++ b/src/pages/tools/pdf/convert-to-pdf/service.ts @@ -1,59 +1,91 @@ import jsPDF from 'jspdf'; -import { Orientation, PageType, ImageSize } from './types'; +import { ImageSize, InitialValuesType, LoadedImage } from './types'; +import { heicTo, isHeic } from 'heic-to'; -export interface ComputeOptions { - files: File[]; - pageType: PageType; - orientation: Orientation; - scale: number; // 10..100 (only applied for A4) -} +const pxToMm = (px: number) => px * 0.264583; -export interface ComputeResult { - pdfFile: File; - imageSize: ImageSize; -} +function returnImageFormat(file: File): 'PNG' | 'JPEG' { + const type = file.type.toLowerCase(); + const name = file.name.toLowerCase(); -interface LoadedImage { - image: HTMLImageElement; - objectUrl: string; - format: 'PNG' | 'JPEG'; -} + if (type.includes('png') || name.endsWith('.png')) return 'PNG'; -function getPdfImageFormat(file: File): 'PNG' | 'JPEG' { - if (file.type === 'image/png' || file.name.toLowerCase().endsWith('.png')) { - return 'PNG'; + if ( + type.includes('jpeg') || + type.includes('jpg') || + name.endsWith('.jpg') || + name.endsWith('.jpeg') + ) { + return 'JPEG'; } + if (type.includes('webp') || name.endsWith('.webp')) return 'JPEG'; + + if (type.includes('gif') || name.endsWith('.gif')) return 'JPEG'; + + console.warn(`Unsupported format: ${file.type}, defaulting to JPEG`); return 'JPEG'; } async function loadImage(file: File): Promise { + let processedFile = file; + + if (await isHeic(file)) { + const convertedBlob = await heicTo({ + blob: file, + type: 'image/jpeg' + }); + + processedFile = new File([convertedBlob], file.name + '.jpg', { + type: 'image/jpeg' + }); + } + const image = new Image(); - const objectUrl = URL.createObjectURL(file); + const objectUrl = URL.createObjectURL(processedFile); image.src = objectUrl; await image.decode(); - return { image, objectUrl, format: getPdfImageFormat(file) }; + return { + image, + objectUrl, + format: returnImageFormat(file), + filename: processedFile.name + }; } -export async function buildPdf({ - files, - pageType, - orientation, - scale -}: ComputeOptions): Promise { +export async function buildPdf( + files: File[], + options: InitialValuesType +): Promise<{ pdfFile: File; imageSizes: ImageSize[] }> { if (!files.length) { throw new Error('No files selected'); } + const { pageType, orientation, scale } = options; + let loadedImages: LoadedImage[] = []; + const imageSizes: ImageSize[] = []; + + const results = await Promise.all( + files.map((file) => + loadImage(file).catch((err) => { + console.warn(`Skipping ${file.name}:`, err); + return null; + }) + ) + ); + + loadedImages = results.filter((img): img is LoadedImage => img !== null); + + if (!loadedImages.length) { + throw new Error('No images could be loaded'); + } + + const { image: firstImage, filename: firstImageFileName } = loadedImages[0]; + const firstImageWidthMm = pxToMm(firstImage.width); + const firstImageHeightMm = pxToMm(firstImage.height); try { - const pxToMm = (px: number) => px * 0.264583; - loadedImages = await Promise.all(files.map((file) => loadImage(file))); - const firstImage = loadedImages[0].image; - const firstImageWidthMm = pxToMm(firstImage.width); - const firstImageHeightMm = pxToMm(firstImage.height); - const pdf = pageType === 'full' ? new jsPDF({ @@ -107,23 +139,22 @@ export async function buildPdf({ const y = pageType === 'full' ? 0 : (pageHeight - finalHeight) / 2; pdf.addImage(image, format, x, y, finalWidth, finalHeight); + imageSizes.push({ + widthMm: imageWidthMm, + heightMm: imageHeightMm, + widthPx: image.width, + heightPx: image.height + }); } const blob = pdf.output('blob'); - const firstFileName = files[0].name.replace(/\.[^/.]+$/, ''); + const firstFileName = firstImageFileName.replace(/\.[^/.]+$/, ''); const fileName = - files.length === 1 - ? `${firstFileName}.pdf` - : `${firstFileName}-merged.pdf`; + files.length === 1 ? `${firstFileName}.pdf` : 'merged-images.pdf'; return { pdfFile: new File([blob], fileName, { type: 'application/pdf' }), - imageSize: { - widthMm: firstImageWidthMm, - heightMm: firstImageHeightMm, - widthPx: firstImage.width, - heightPx: firstImage.height - } + imageSizes: imageSizes }; } finally { loadedImages.forEach(({ objectUrl }) => URL.revokeObjectURL(objectUrl)); From bba16c9b86922f6595d0a430516bbe55ac26598b Mon Sep 17 00:00:00 2001 From: Chesterkxng Date: Tue, 24 Mar 2026 21:30:55 +0100 Subject: [PATCH 4/9] chore: unsupported formats (keywords) removed --- src/pages/tools/pdf/convert-to-pdf/meta.ts | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/src/pages/tools/pdf/convert-to-pdf/meta.ts b/src/pages/tools/pdf/convert-to-pdf/meta.ts index 0fa3879..69310c5 100644 --- a/src/pages/tools/pdf/convert-to-pdf/meta.ts +++ b/src/pages/tools/pdf/convert-to-pdf/meta.ts @@ -19,14 +19,10 @@ export const tool = defineTool('pdf', { 'jpeg', 'png', 'gif', - 'tiff', 'webp', 'heic', - 'raw', - 'psd', - 'svg', - 'quality', - 'compression' + 'heif', + 'quality' ], component: lazy(() => import('./index')) }); From 02830c1fa9c2e39370f8f35f37770f49ed1af923 Mon Sep 17 00:00:00 2001 From: Chesterkxng Date: Tue, 24 Mar 2026 21:31:28 +0100 Subject: [PATCH 5/9] chore: tests deleted (front end focused) --- .../convert-to-pdf.service.test.tsx | 38 ------------------- 1 file changed, 38 deletions(-) delete mode 100644 src/pages/tools/pdf/convert-to-pdf/convert-to-pdf.service.test.tsx diff --git a/src/pages/tools/pdf/convert-to-pdf/convert-to-pdf.service.test.tsx b/src/pages/tools/pdf/convert-to-pdf/convert-to-pdf.service.test.tsx deleted file mode 100644 index 107039a..0000000 --- a/src/pages/tools/pdf/convert-to-pdf/convert-to-pdf.service.test.tsx +++ /dev/null @@ -1,38 +0,0 @@ -import { render, screen, fireEvent } from '@testing-library/react'; -import ConvertToPdf from './index'; -import { vi } from 'vitest'; -import '@testing-library/jest-dom'; - -describe('ConvertToPdf', () => { - it('renders with default state values (full, portrait hidden, no scale shown)', () => { - render(); - - expect(screen.getByLabelText(/Full Size \(Same as Image\)/i)).toBeChecked(); - - expect(screen.queryByLabelText(/A4 Page/i)).toBeInTheDocument(); - expect(screen.queryByLabelText(/Portrait/i)).not.toBeInTheDocument(); - expect(screen.queryByText(/Scale image:/i)).not.toBeInTheDocument(); - }); - - it('switches to A4 page type and shows orientation and scale', () => { - render(); - - const a4Option = screen.getByLabelText(/A4 Page/i); - fireEvent.click(a4Option); - expect(a4Option).toBeChecked(); - - expect(screen.getByLabelText(/Portrait/i)).toBeChecked(); - expect(screen.getByText(/Scale image:\s*100%/i)).toBeInTheDocument(); - }); - - it('updates scale when slider moves (after switching to A4)', () => { - render(); - - fireEvent.click(screen.getByLabelText(/A4 Page/i)); - - const slider = screen.getByRole('slider'); - fireEvent.change(slider, { target: { value: 80 } }); - - expect(screen.getByText(/Scale image:\s*80%/i)).toBeInTheDocument(); - }); -}); From 73d337bc2be1799fa0c11f12968e99757d0ce2d6 Mon Sep 17 00:00:00 2001 From: Chesterkxng Date: Tue, 24 Mar 2026 21:35:07 +0100 Subject: [PATCH 6/9] chore: - full i8n support - loading state added - Array of sizes displayed - Accept list updated --- src/pages/tools/pdf/convert-to-pdf/index.tsx | 120 +++++++++++-------- 1 file changed, 69 insertions(+), 51 deletions(-) diff --git a/src/pages/tools/pdf/convert-to-pdf/index.tsx b/src/pages/tools/pdf/convert-to-pdf/index.tsx index c52c332..59fe785 100644 --- a/src/pages/tools/pdf/convert-to-pdf/index.tsx +++ b/src/pages/tools/pdf/convert-to-pdf/index.tsx @@ -14,67 +14,66 @@ import ToolMultipleImageInput, { } from '@components/input/ToolMultipleImageInput'; import ToolFileResult from 'components/result/ToolFileResult'; import { ToolComponentProps } from '@tools/defineTool'; -import { FormValues, Orientation, PageType, initialValues } from './types'; +import { Orientation, PageType, InitialValuesType, ImageSize } from './types'; import { buildPdf } from './service'; +import { useTranslation } from 'react-i18next'; -const initialFormValues: FormValues = initialValues; +const initialValues: InitialValuesType = { + pageType: 'full', + orientation: 'portrait', + scale: 100 +}; export default function ConvertToPdf({ title }: ToolComponentProps) { - const [input, setInput] = useState([]); - const [inputImages, setInputImages] = useState([]); + const { t } = useTranslation('pdf'); + const [input, setInput] = useState([]); const [result, setResult] = useState(null); - const [imageSize, setImageSize] = useState<{ - widthMm: number; - heightMm: number; - widthPx: number; - heightPx: number; - } | null>(null); + const [imageSizes, setImageSizes] = useState(null); + const [isProcessing, setIsProcessing] = useState(false); - const handleInputChange = (files: MultiImageInput[]) => { - setInputImages(files); - setInput(files.map(({ file }) => file)); - }; - - const compute = async (values: FormValues) => { + const compute = async ( + optionsValues: InitialValuesType, + input: MultiImageInput[] + ) => { if (!input.length) return; - const { pdfFile, imageSize } = await buildPdf({ - files: input, - pageType: values.pageType, - orientation: values.orientation, - scale: values.scale - }); - setResult(pdfFile); - setImageSize(imageSize); + + setIsProcessing(true); + + try { + const files = input.sort((a, b) => a.order - b.order).map((i) => i.file); + + const { pdfFile, imageSizes } = await buildPdf(files, optionsValues); + setResult(pdfFile); + setImageSizes(imageSizes); + } catch (error) { + throw new Error('Error converting image(s) to PDF:' + error); + } finally { + setIsProcessing(false); + } }; return ( - + } @@ -85,7 +84,9 @@ export default function ConvertToPdf({ title }: ToolComponentProps) { component: ( - PDF Type + + {t('convertToPdf.options.type')} + } - label="Full Size (Same as Image)" + label={t('convertToPdf.options.fullsize')} /> } - label="A4 Page" + label={t('convertToPdf.options.a4')} /> - {values.pageType === 'full' && imageSize && ( - - Image size: {imageSize.widthMm.toFixed(1)} ×{' '} - {imageSize.heightMm.toFixed(1)} mm ({imageSize.widthPx} ×{' '} - {imageSize.heightPx} px) - + {values.pageType === 'full' && imageSizes && ( + + {imageSizes.map((size, index) => ( + + {t('convertToPdf.options.image')} {index + 1}:{' '} + {size.widthMm.toFixed(1)} × {size.heightMm.toFixed(1)}{' '} + mm ({size.widthPx} × {size.heightPx} px) + + ))} + )} {values.pageType === 'a4' && ( - Orientation + + {t('convertToPdf.options.orientation')} + } - label="Portrait (Vertical)" + label={t('convertToPdf.options.portrait')} /> } - label="Landscape (Horizontal)" + label={t('convertToPdf.options.landscape')} /> @@ -143,8 +154,10 @@ export default function ConvertToPdf({ title }: ToolComponentProps) { {values.pageType === 'a4' && ( - Scale - Scale image: {values.scale}% + + {t('convertToPdf.options.scale')} + + {values.scale}% updateField('scale', v as number)} @@ -161,7 +174,12 @@ export default function ConvertToPdf({ title }: ToolComponentProps) { ] as const; }} resultComponent={ - + } /> ); From e2a5a67d372e97b62ae02352686946ae8993ded4 Mon Sep 17 00:00:00 2001 From: Chesterkxng Date: Tue, 24 Mar 2026 21:35:42 +0100 Subject: [PATCH 7/9] locales: full i8n support for convert to pdf --- public/locales/en/pdf.json | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/public/locales/en/pdf.json b/public/locales/en/pdf.json index 82e5309..288e392 100644 --- a/public/locales/en/pdf.json +++ b/public/locales/en/pdf.json @@ -65,8 +65,20 @@ }, "convertToPdf": { "title": "Images to PDF", - "description": "Convert various image formats (PNG, GIF, JPG, TIF, PSD, SVG, WEBP, HEIC, RAW) to PDF, with options to scale the image and choose page orientation.", - "shortDescription": "Convert images to PDF with scale and orientation control" + "description": "Convert various image formats (PNG, GIF, JPG, WEBP, HEIC, HEIF) to PDF, with options to scale the image and choose page orientation.", + "shortDescription": "Convert images to PDF with scale and orientation control", + "inputTitle": "Input Images", + "resultTitle": "Output PDF", + "options": { + "type": "PDF Type", + "orientation": "Orientation", + "scale": "Scale", + "portrait": "Portrait (Vertical)", + "landscape": "Landscape (Horizontal)", + "fullsize": "Full Size (Image size)", + "a4": "A4 Page", + "image": "Image" + } }, "protectPdf": { "description": "Add password protection to your PDF files securely in your browser", From a87f7d4d4fce4c8afce0991aaa4f3015e5e72801 Mon Sep 17 00:00:00 2001 From: Chesterkxng Date: Tue, 24 Mar 2026 21:38:59 +0100 Subject: [PATCH 8/9] chore: support heic in MultipleImageInput preview --- .../input/ToolMultipleImageInput.tsx | 74 ++++++++++++++----- 1 file changed, 56 insertions(+), 18 deletions(-) diff --git a/src/components/input/ToolMultipleImageInput.tsx b/src/components/input/ToolMultipleImageInput.tsx index fe5e2f1..13094dc 100644 --- a/src/components/input/ToolMultipleImageInput.tsx +++ b/src/components/input/ToolMultipleImageInput.tsx @@ -5,6 +5,7 @@ import InputHeader from '../InputHeader'; import InputFooter from './InputFooter'; import ImageIcon from '@mui/icons-material/Image'; import { useTranslation } from 'react-i18next'; +import { heicTo, isHeic } from 'heic-to'; interface MultiImageInputComponentProps { accept: string[]; @@ -30,16 +31,44 @@ export default function ToolMultiImageInput({ const { t } = useTranslation(); const fileInputRef = useRef(null); - const handleFileChange = (event: React.ChangeEvent) => { + const handleFileChange = async ( + event: React.ChangeEvent + ) => { const files = event.target.files; - if (files) { - const newFiles: MultiImageInput[] = Array.from(files).map((file) => ({ - file, - order: value.length, - preview: URL.createObjectURL(file) - })); - onChange([...value, ...newFiles]); - } + if (!files) return; + + const newFiles: MultiImageInput[] = await Promise.all( + Array.from(files).map(async (file, index) => { + let processedFile = file; + + try { + if (await isHeic(file)) { + const convertedBlob = await heicTo({ + blob: file, + type: 'image/jpeg' + }); + + processedFile = new File( + [convertedBlob], + file.name.replace(/\.[^/.]+$/, '') + '.jpg', + { type: 'image/jpeg' } + ); + } + } catch (err) { + console.warn('HEIC conversion failed:', file.name, err); + } + + return { + file: processedFile, + order: value.length + index, + preview: URL.createObjectURL(processedFile) + }; + }) + ); + + onChange([...value, ...newFiles]); + + event.target.value = ''; }; const handleImportClick = () => { @@ -47,15 +76,27 @@ export default function ToolMultiImageInput({ }; function handleClear() { + value.forEach((f) => { + if (f.preview) URL.revokeObjectURL(f.preview); + }); onChange([]); } + function handleRemove(index: number) { + const fileToRemove = value[index]; + if (fileToRemove?.preview) { + URL.revokeObjectURL(fileToRemove.preview); + } + + const updatedFiles = value.filter((_, i) => i !== index); + onChange(updatedFiles); + } + function fileNameTruncate(fileName: string) { const maxLength = 10; - if (fileName.length > maxLength) { - return fileName.slice(0, maxLength) + '...'; - } - return fileName; + return fileName.length > maxLength + ? fileName.slice(0, maxLength) + '...' + : fileName; } return ( @@ -137,10 +178,7 @@ export default function ToolMultiImageInput({ { - const updatedFiles = value.filter((_, i) => i !== index); - onChange(updatedFiles); - }} + onClick={() => handleRemove(index)} > ✖ @@ -162,7 +200,7 @@ export default function ToolMultiImageInput({ type="file" accept={accept.join(',')} onChange={handleFileChange} - multiple={true} + multiple /> ); From 0ed6559ff9d536e2739c2d56a8bc98d7745d48eb Mon Sep 17 00:00:00 2001 From: Chesterkxng Date: Tue, 24 Mar 2026 21:39:46 +0100 Subject: [PATCH 9/9] chore: heic-to added to package --- package-lock.json | 7 +++++++ package.json | 1 + 2 files changed, 8 insertions(+) diff --git a/package-lock.json b/package-lock.json index 250551e..95ba9ac 100644 --- a/package-lock.json +++ b/package-lock.json @@ -34,6 +34,7 @@ "dayjs": "^1.11.13", "fast-xml-parser": "^5.2.5", "formik": "^2.4.6", + "heic-to": "^1.4.2", "heic2any": "^0.0.4", "i18next": "^25.3.2", "i18next-browser-languagedetector": "^8.2.0", @@ -7610,6 +7611,12 @@ "node": ">= 0.4" } }, + "node_modules/heic-to": { + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/heic-to/-/heic-to-1.4.2.tgz", + "integrity": "sha512-y69thwxfNcEm2Vk8lbOD/cMabnvMJyOREfJYiCHcXCDqlfcPyJoBhyRc8+iDe1B95LRfpbTOpzxzY1xbRkdwBA==", + "license": "LGPL-3.0" + }, "node_modules/heic2any": { "version": "0.0.4", "resolved": "https://registry.npmjs.org/heic2any/-/heic2any-0.0.4.tgz", diff --git a/package.json b/package.json index 8857a6e..427b58d 100644 --- a/package.json +++ b/package.json @@ -53,6 +53,7 @@ "dayjs": "^1.11.13", "fast-xml-parser": "^5.2.5", "formik": "^2.4.6", + "heic-to": "^1.4.2", "heic2any": "^0.0.4", "i18next": "^25.3.2", "i18next-browser-languagedetector": "^8.2.0",