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
index 82ace2d..107039a 100644
--- 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
@@ -3,68 +3,36 @@ import ConvertToPdf from './index';
import { vi } from 'vitest';
import '@testing-library/jest-dom';
-it('should render with default state values', () => {
- render();
- expect(screen.getByLabelText(/A4 Page/i)).toBeChecked();
- expect(screen.getByLabelText(/Portrait/i)).toBeChecked();
- expect(screen.getByText(/Scale image: 100%/i)).toBeInTheDocument();
-});
-
-it('should switch to full page type when selected', () => {
- render();
- const fullOption = screen.getByLabelText(/Full Size/i);
- fireEvent.click(fullOption);
- expect(fullOption).toBeChecked();
-});
-
-it('should update scale when slider moves', () => {
- render();
- const slider = screen.getByRole('slider');
- fireEvent.change(slider, { target: { value: 80 } });
- expect(screen.getByText(/Scale image: 80%/i)).toBeInTheDocument();
-});
-
-it('should change orientation to landscape', () => {
- render();
- const landscapeRadio = screen.getByLabelText(/Landscape/i);
- fireEvent.click(landscapeRadio);
- expect(landscapeRadio).toBeChecked();
-});
-
-vi.mock('jspdf', () => {
- return {
- default: vi.fn().mockImplementation(() => ({
- setDisplayMode: vi.fn(),
- internal: { pageSize: { getWidth: () => 210, getHeight: () => 297 } },
- addImage: vi.fn(),
- output: vi.fn().mockReturnValue(new Blob())
- }))
- };
-});
-
-it('should call jsPDF and addImage when compute is triggered', async () => {
- const createObjectURLStub = vi
- .spyOn(global.URL, 'createObjectURL')
- .mockReturnValue('blob:url');
-
- vi.mock('components/input/ToolImageInput', () => ({
- default: ({ onChange }: any) => (
- onChange(e.target.files[0])}
- />
- )
- }));
-
- const mockFile = new File(['dummy'], 'test.jpg', { type: 'image/jpeg' });
- render();
-
- const fileInput = screen.getByTitle(/Input Image/i);
- fireEvent.change(fileInput, { target: { files: [mockFile] } });
-
- const jsPDF = (await import('jspdf')).default;
- expect(jsPDF).toHaveBeenCalled();
-
- createObjectURLStub.mockRestore();
+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();
+ });
});
diff --git a/src/pages/tools/pdf/convert-to-pdf/index.tsx b/src/pages/tools/pdf/convert-to-pdf/index.tsx
index 1e35a6c..750e843 100644
--- a/src/pages/tools/pdf/convert-to-pdf/index.tsx
+++ b/src/pages/tools/pdf/convert-to-pdf/index.tsx
@@ -4,23 +4,22 @@ import {
Typography,
RadioGroup,
FormControlLabel,
- Radio
+ Radio,
+ Stack
} from '@mui/material';
+import React, { useState } from 'react';
+import ToolContent from '@components/ToolContent';
import ToolImageInput from 'components/input/ToolImageInput';
import ToolFileResult from 'components/result/ToolFileResult';
-import React, { useState, useEffect } from 'react';
-import ToolContent from '@components/ToolContent';
import { ToolComponentProps } from '@tools/defineTool';
-import jsPDF from 'jspdf';
+import { FormValues, Orientation, PageType, initialValues } from './types';
+import { buildPdf } from './service';
+
+const initialFormValues: FormValues = initialValues;
export default function ConvertToPdf({ title }: ToolComponentProps) {
const [input, setInput] = useState(null);
const [result, setResult] = useState(null);
- const [scale, setScale] = useState(100);
- const [orientation, setOrientation] = useState<'portrait' | 'landscape'>(
- 'portrait'
- );
- const [pageType, setPageType] = useState<'a4' | 'full'>('a4');
const [imageSize, setImageSize] = useState<{
widthMm: number;
heightMm: number;
@@ -28,81 +27,27 @@ export default function ConvertToPdf({ title }: ToolComponentProps) {
heightPx: number;
} | null>(null);
- const compute = async (file: File | null, currentScale: number) => {
- if (!file) return;
-
- const img = new Image();
- img.src = URL.createObjectURL(file);
-
- try {
- await img.decode();
-
- const pxToMm = (px: number) => px * 0.264583;
- const imgWidthMm = pxToMm(img.width);
- const imgHeightMm = pxToMm(img.height);
- setImageSize({
- widthMm: imgWidthMm,
- heightMm: imgHeightMm,
- widthPx: img.width,
- heightPx: img.height
- });
-
- const pdf =
- pageType === 'full'
- ? new jsPDF({
- orientation: imgWidthMm > imgHeightMm ? 'landscape' : 'portrait',
- unit: 'mm',
- format: [imgWidthMm, imgHeightMm]
- })
- : new jsPDF({
- orientation,
- unit: 'mm',
- format: 'a4'
- });
-
- pdf.setDisplayMode('fullwidth');
-
- const pageWidth = pdf.internal.pageSize.getWidth();
- const pageHeight = pdf.internal.pageSize.getHeight();
-
- const widthRatio = pageWidth / img.width;
- const heightRatio = pageHeight / img.height;
- const fitScale = Math.min(widthRatio, heightRatio);
-
- const finalWidth =
- pageType === 'full'
- ? pageWidth
- : img.width * fitScale * (currentScale / 100);
- const finalHeight =
- pageType === 'full'
- ? pageHeight
- : img.height * fitScale * (currentScale / 100);
-
- const x = pageType === 'full' ? 0 : (pageWidth - finalWidth) / 2;
- const y = pageType === 'full' ? 0 : (pageHeight - finalHeight) / 2;
-
- pdf.addImage(img, 'JPEG', x, y, finalWidth, finalHeight);
-
- const blob = pdf.output('blob');
- const fileName = file.name.replace(/\.[^/.]+$/, '') + '.pdf';
- setResult(new File([blob], fileName, { type: 'application/pdf' }));
- } catch (e) {
- console.error(e);
- } finally {
- URL.revokeObjectURL(img.src);
- }
+ 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);
};
- useEffect(() => {
- compute(input, scale);
- }, [input, orientation, pageType]);
-
return (
-
title={title}
input={input}
+ setInput={setInput}
+ initialValues={initialFormValues}
+ compute={compute}
inputComponent={
-
+
-
-
- PDF Type
- setPageType(e.target.value as 'a4' | 'full')}
- >
- }
- label="A4 Page"
- />
- }
- label="Full Size (Same as Image)"
- />
-
-
- {pageType === 'full' && imageSize && (
-
- Image size: {imageSize.widthMm.toFixed(1)} ×{' '}
- {imageSize.heightMm.toFixed(1)} mm ({imageSize.widthPx} ×{' '}
- {imageSize.heightPx} px)
-
- )}
-
-
- {pageType === 'a4' && (
- <>
-
- Orientation
-
- setOrientation(e.target.value as 'portrait' | 'landscape')
- }
- >
- }
- label="Portrait (Vertical)"
- />
- }
- label="Landscape (Horizontal)"
- />
-
-
-
-
- Scale image: {scale}%
- setScale(v as number)}
- onChangeCommitted={(_, v) => compute(input, v as number)}
- min={10}
- max={100}
- step={1}
- valueLabelDisplay="auto"
- />
-
- >
- )}
}
+ getGroups={({ values, updateField }) => {
+ return [
+ {
+ title: '',
+ component: (
+
+
+ PDF Type
+
+ updateField('pageType', e.target.value as PageType)
+ }
+ >
+ }
+ label="Full Size (Same as Image)"
+ />
+ }
+ label="A4 Page"
+ />
+
+
+ {values.pageType === 'full' && imageSize && (
+
+ Image size: {imageSize.widthMm.toFixed(1)} ×{' '}
+ {imageSize.heightMm.toFixed(1)} mm ({imageSize.widthPx} ×{' '}
+ {imageSize.heightPx} px)
+
+ )}
+
+
+ {values.pageType === 'a4' && (
+
+ Orientation
+
+ updateField(
+ 'orientation',
+ e.target.value as Orientation
+ )
+ }
+ >
+ }
+ label="Portrait (Vertical)"
+ />
+ }
+ label="Landscape (Horizontal)"
+ />
+
+
+ )}
+
+ {values.pageType === 'a4' && (
+
+ Scale
+ Scale image: {values.scale}%
+ updateField('scale', v as number)}
+ min={10}
+ max={100}
+ step={1}
+ valueLabelDisplay="auto"
+ />
+
+ )}
+
+ )
+ }
+ ] as const;
+ }}
resultComponent={
-
+
}
- compute={() => compute(input, scale)}
- setInput={setInput}
/>
);
}
diff --git a/src/pages/tools/pdf/convert-to-pdf/service.ts b/src/pages/tools/pdf/convert-to-pdf/service.ts
new file mode 100644
index 0000000..4a1eb22
--- /dev/null
+++ b/src/pages/tools/pdf/convert-to-pdf/service.ts
@@ -0,0 +1,80 @@
+import jsPDF from 'jspdf';
+import { Orientation, PageType, ImageSize } from './types';
+
+export interface ComputeOptions {
+ file: File;
+ pageType: PageType;
+ orientation: Orientation;
+ scale: number; // 10..100 (only applied for A4)
+}
+
+export interface ComputeResult {
+ pdfFile: File;
+ imageSize: ImageSize;
+}
+
+export async function buildPdf({
+ file,
+ pageType,
+ orientation,
+ scale
+}: ComputeOptions): Promise {
+ const img = new Image();
+ img.src = URL.createObjectURL(file);
+
+ 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',
+ unit: 'mm',
+ format: [imgWidthMm, imgHeightMm]
+ })
+ : new jsPDF({
+ orientation,
+ unit: 'mm',
+ format: 'a4'
+ });
+
+ pdf.setDisplayMode('fullwidth');
+
+ const pageWidth = pdf.internal.pageSize.getWidth();
+ const pageHeight = pdf.internal.pageSize.getHeight();
+
+ const widthRatio = pageWidth / img.width;
+ const heightRatio = pageHeight / img.height;
+ const fitScale = Math.min(widthRatio, heightRatio);
+
+ const finalWidth =
+ pageType === 'full' ? pageWidth : img.width * fitScale * (scale / 100);
+
+ const finalHeight =
+ pageType === 'full' ? pageHeight : img.height * fitScale * (scale / 100);
+
+ const x = pageType === 'full' ? 0 : (pageWidth - finalWidth) / 2;
+ const y = pageType === 'full' ? 0 : (pageHeight - finalHeight) / 2;
+
+ pdf.addImage(img, 'JPEG', x, y, finalWidth, finalHeight);
+
+ const blob = pdf.output('blob');
+ const fileName = file.name.replace(/\.[^/.]+$/, '') + '.pdf';
+
+ return {
+ pdfFile: new File([blob], fileName, { type: 'application/pdf' }),
+ imageSize: {
+ widthMm: imgWidthMm,
+ heightMm: imgHeightMm,
+ widthPx: img.width,
+ heightPx: img.height
+ }
+ };
+ } finally {
+ URL.revokeObjectURL(img.src);
+ }
+}
diff --git a/src/pages/tools/pdf/convert-to-pdf/types.ts b/src/pages/tools/pdf/convert-to-pdf/types.ts
new file mode 100644
index 0000000..93b9a42
--- /dev/null
+++ b/src/pages/tools/pdf/convert-to-pdf/types.ts
@@ -0,0 +1,21 @@
+export type Orientation = 'portrait' | 'landscape';
+export type PageType = 'a4' | 'full';
+
+export interface ImageSize {
+ widthMm: number;
+ heightMm: number;
+ widthPx: number;
+ heightPx: number;
+}
+
+export interface FormValues {
+ pageType: PageType;
+ orientation: Orientation;
+ scale: number;
+}
+
+export const initialValues: FormValues = {
+ pageType: 'full',
+ orientation: 'portrait',
+ scale: 100
+};