Merge pull request #343 from TiwariLokesh/feature/multiple-images-to-pdf
feat: support multiple images to PDF conversion
This commit is contained in:
Generated
+7
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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<HTMLInputElement>(null);
|
||||
|
||||
const handleFileChange = (event: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const handleFileChange = async (
|
||||
event: React.ChangeEvent<HTMLInputElement>
|
||||
) => {
|
||||
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({
|
||||
</Typography>
|
||||
<Box
|
||||
sx={{ cursor: 'pointer' }}
|
||||
onClick={() => {
|
||||
const updatedFiles = value.filter((_, i) => i !== index);
|
||||
onChange(updatedFiles);
|
||||
}}
|
||||
onClick={() => handleRemove(index)}
|
||||
>
|
||||
✖
|
||||
</Box>
|
||||
@@ -162,7 +200,7 @@ export default function ToolMultiImageInput({
|
||||
type="file"
|
||||
accept={accept.join(',')}
|
||||
onChange={handleFileChange}
|
||||
multiple={true}
|
||||
multiple
|
||||
/>
|
||||
</Box>
|
||||
);
|
||||
|
||||
@@ -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(<ConvertToPdf title="Test PDF" />);
|
||||
|
||||
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(<ConvertToPdf title="Test PDF" />);
|
||||
|
||||
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(<ConvertToPdf title="Test PDF" />);
|
||||
|
||||
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();
|
||||
});
|
||||
});
|
||||
@@ -9,63 +9,71 @@ 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';
|
||||
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<File | null>(null);
|
||||
const { t } = useTranslation('pdf');
|
||||
const [input, setInput] = useState<MultiImageInput[]>([]);
|
||||
const [result, setResult] = useState<File | null>(null);
|
||||
const [imageSize, setImageSize] = useState<{
|
||||
widthMm: number;
|
||||
heightMm: number;
|
||||
widthPx: number;
|
||||
heightPx: number;
|
||||
} | null>(null);
|
||||
const [imageSizes, setImageSizes] = useState<ImageSize[] | null>(null);
|
||||
const [isProcessing, setIsProcessing] = useState<boolean>(false);
|
||||
|
||||
const compute = async (values: FormValues) => {
|
||||
if (!input) return;
|
||||
const { pdfFile, imageSize } = await buildPdf({
|
||||
file: input,
|
||||
pageType: values.pageType,
|
||||
orientation: values.orientation,
|
||||
scale: values.scale
|
||||
});
|
||||
setResult(pdfFile);
|
||||
setImageSize(imageSize);
|
||||
const compute = async (
|
||||
optionsValues: InitialValuesType,
|
||||
input: MultiImageInput[]
|
||||
) => {
|
||||
if (!input.length) return;
|
||||
|
||||
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 (
|
||||
<ToolContent<FormValues, File | null>
|
||||
<ToolContent
|
||||
title={title}
|
||||
input={input}
|
||||
setInput={setInput}
|
||||
initialValues={initialFormValues}
|
||||
initialValues={initialValues}
|
||||
compute={compute}
|
||||
inputComponent={
|
||||
<Box>
|
||||
<ToolImageInput
|
||||
<ToolMultipleImageInput
|
||||
type="image"
|
||||
value={input}
|
||||
onChange={setInput}
|
||||
accept={[
|
||||
'image/png',
|
||||
'image/jpeg',
|
||||
'image/webp',
|
||||
'image/tiff',
|
||||
'image/gif',
|
||||
'image/heic',
|
||||
'image/heif',
|
||||
'image/x-adobe-dng',
|
||||
'image/x-canon-cr2',
|
||||
'image/x-nikon-nef',
|
||||
'image/x-sony-arw',
|
||||
'image/vnd.adobe.photoshop'
|
||||
'image/heif'
|
||||
]}
|
||||
title="Input Image"
|
||||
title={t('convertToPdf.inputTitle')}
|
||||
/>
|
||||
</Box>
|
||||
}
|
||||
@@ -76,7 +84,9 @@ export default function ConvertToPdf({ title }: ToolComponentProps) {
|
||||
component: (
|
||||
<Stack spacing={4}>
|
||||
<Box>
|
||||
<Typography variant="h6">PDF Type</Typography>
|
||||
<Typography variant="h6">
|
||||
{t('convertToPdf.options.type')}
|
||||
</Typography>
|
||||
<RadioGroup
|
||||
row
|
||||
value={values.pageType}
|
||||
@@ -87,27 +97,37 @@ export default function ConvertToPdf({ title }: ToolComponentProps) {
|
||||
<FormControlLabel
|
||||
value="full"
|
||||
control={<Radio />}
|
||||
label="Full Size (Same as Image)"
|
||||
label={t('convertToPdf.options.fullsize')}
|
||||
/>
|
||||
<FormControlLabel
|
||||
value="a4"
|
||||
control={<Radio />}
|
||||
label="A4 Page"
|
||||
label={t('convertToPdf.options.a4')}
|
||||
/>
|
||||
</RadioGroup>
|
||||
|
||||
{values.pageType === 'full' && imageSize && (
|
||||
<Typography variant="body2" color="text.secondary">
|
||||
Image size: {imageSize.widthMm.toFixed(1)} ×{' '}
|
||||
{imageSize.heightMm.toFixed(1)} mm ({imageSize.widthPx} ×{' '}
|
||||
{imageSize.heightPx} px)
|
||||
</Typography>
|
||||
{values.pageType === 'full' && imageSizes && (
|
||||
<Stack spacing={1} mt={1}>
|
||||
{imageSizes.map((size, index) => (
|
||||
<Typography
|
||||
key={index}
|
||||
variant="body2"
|
||||
color="text.secondary"
|
||||
>
|
||||
{t('convertToPdf.options.image')} {index + 1}:{' '}
|
||||
{size.widthMm.toFixed(1)} × {size.heightMm.toFixed(1)}{' '}
|
||||
mm ({size.widthPx} × {size.heightPx} px)
|
||||
</Typography>
|
||||
))}
|
||||
</Stack>
|
||||
)}
|
||||
</Box>
|
||||
|
||||
{values.pageType === 'a4' && (
|
||||
<Box>
|
||||
<Typography variant="h6">Orientation</Typography>
|
||||
<Typography variant="h6">
|
||||
{t('convertToPdf.options.orientation')}
|
||||
</Typography>
|
||||
<RadioGroup
|
||||
row
|
||||
value={values.orientation}
|
||||
@@ -121,12 +141,12 @@ export default function ConvertToPdf({ title }: ToolComponentProps) {
|
||||
<FormControlLabel
|
||||
value="portrait"
|
||||
control={<Radio />}
|
||||
label="Portrait (Vertical)"
|
||||
label={t('convertToPdf.options.portrait')}
|
||||
/>
|
||||
<FormControlLabel
|
||||
value="landscape"
|
||||
control={<Radio />}
|
||||
label="Landscape (Horizontal)"
|
||||
label={t('convertToPdf.options.landscape')}
|
||||
/>
|
||||
</RadioGroup>
|
||||
</Box>
|
||||
@@ -134,8 +154,10 @@ export default function ConvertToPdf({ title }: ToolComponentProps) {
|
||||
|
||||
{values.pageType === 'a4' && (
|
||||
<Box>
|
||||
<Typography variant="h6">Scale</Typography>
|
||||
<Typography>Scale image: {values.scale}%</Typography>
|
||||
<Typography variant="h6">
|
||||
{t('convertToPdf.options.scale')}
|
||||
</Typography>
|
||||
<Typography>{values.scale}%</Typography>
|
||||
<Slider
|
||||
value={values.scale}
|
||||
onChange={(_, v) => updateField('scale', v as number)}
|
||||
@@ -152,7 +174,12 @@ export default function ConvertToPdf({ title }: ToolComponentProps) {
|
||||
] as const;
|
||||
}}
|
||||
resultComponent={
|
||||
<ToolFileResult title="Output PDF" value={result} extension="pdf" />
|
||||
<ToolFileResult
|
||||
title={t('convertToPdf.resultTitle')}
|
||||
value={result}
|
||||
extension="pdf"
|
||||
loading={isProcessing}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
);
|
||||
|
||||
@@ -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'))
|
||||
});
|
||||
|
||||
@@ -1,40 +1,98 @@
|
||||
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 {
|
||||
file: File;
|
||||
pageType: PageType;
|
||||
orientation: Orientation;
|
||||
scale: number; // 10..100 (only applied for A4)
|
||||
const pxToMm = (px: number) => px * 0.264583;
|
||||
|
||||
function returnImageFormat(file: File): 'PNG' | 'JPEG' {
|
||||
const type = file.type.toLowerCase();
|
||||
const name = file.name.toLowerCase();
|
||||
|
||||
if (type.includes('png') || name.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';
|
||||
}
|
||||
|
||||
export interface ComputeResult {
|
||||
pdfFile: File;
|
||||
imageSize: ImageSize;
|
||||
async function loadImage(file: File): Promise<LoadedImage> {
|
||||
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(processedFile);
|
||||
image.src = objectUrl;
|
||||
await image.decode();
|
||||
return {
|
||||
image,
|
||||
objectUrl,
|
||||
format: returnImageFormat(file),
|
||||
filename: processedFile.name
|
||||
};
|
||||
}
|
||||
|
||||
export async function buildPdf({
|
||||
file,
|
||||
pageType,
|
||||
orientation,
|
||||
scale
|
||||
}: ComputeOptions): Promise<ComputeResult> {
|
||||
const img = new Image();
|
||||
img.src = URL.createObjectURL(file);
|
||||
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 {
|
||||
await img.decode();
|
||||
|
||||
const pxToMm = (px: number) => px * 0.264583;
|
||||
const imgWidthMm = pxToMm(img.width);
|
||||
const imgHeightMm = pxToMm(img.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 +102,61 @@ 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);
|
||||
imageSizes.push({
|
||||
widthMm: imageWidthMm,
|
||||
heightMm: imageHeightMm,
|
||||
widthPx: image.width,
|
||||
heightPx: image.height
|
||||
});
|
||||
}
|
||||
|
||||
const blob = pdf.output('blob');
|
||||
const fileName = file.name.replace(/\.[^/.]+$/, '') + '.pdf';
|
||||
const firstFileName = firstImageFileName.replace(/\.[^/.]+$/, '');
|
||||
const fileName =
|
||||
files.length === 1 ? `${firstFileName}.pdf` : 'merged-images.pdf';
|
||||
|
||||
return {
|
||||
pdfFile: new File([blob], fileName, { type: 'application/pdf' }),
|
||||
imageSize: {
|
||||
widthMm: imgWidthMm,
|
||||
heightMm: imgHeightMm,
|
||||
widthPx: img.width,
|
||||
heightPx: img.height
|
||||
}
|
||||
imageSizes: imageSizes
|
||||
};
|
||||
} finally {
|
||||
URL.revokeObjectURL(img.src);
|
||||
loadedImages.forEach(({ objectUrl }) => URL.revokeObjectURL(objectUrl));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user