Skip to content

Commit b330957

Browse files
esmcelroyCopilot
andcommitted
feat: add output format, resize cap, gradient fill, blur fill, and custom ratio
Phase 1 features: - Output format selector (PNG/JPEG/WebP) with quality slider - Max dimension cap for controlling output file size - Gradient fill with direction and color controls - Blur fill using source photo (Instagram-style) - Custom aspect ratio W:H input Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent 78f3c19 commit b330957

7 files changed

Lines changed: 355 additions & 65 deletions

File tree

e2e/settings.spec.ts

Lines changed: 13 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -8,25 +8,25 @@ test.beforeEach(async ({ page }) => {
88
await page.reload()
99
})
1010

11-
test('can switch between Solid Color and Background Image fill types', async ({
11+
test('can switch between Color and Image fill types', async ({
1212
page,
1313
}) => {
14-
const solidBtn = page.getByRole('button', { name: 'Solid Color' })
15-
const bgImgBtn = page.getByRole('button', { name: 'Background Image' })
14+
const colorBtn = page.getByRole('button', { name: 'Color', exact: true })
15+
const imgBtn = page.getByRole('button', { name: 'Image', exact: true })
1616

17-
// Solid Color is default (active)
18-
await expect(solidBtn).toBeVisible()
19-
await expect(bgImgBtn).toBeVisible()
17+
// Color is default (active)
18+
await expect(colorBtn).toBeVisible()
19+
await expect(imgBtn).toBeVisible()
2020

21-
// Switch to Background Image
22-
await bgImgBtn.click()
21+
// Switch to Image
22+
await imgBtn.click()
2323
// "Upload image" button should appear (background image UI)
2424
await expect(
2525
page.getByRole('button', { name: 'Upload image' }),
2626
).toBeVisible()
2727

28-
// Switch back to Solid Color
29-
await solidBtn.click()
28+
// Switch back to Color
29+
await colorBtn.click()
3030
// Color hex input should appear
3131
await expect(page.locator('input[placeholder="#ffffff"]')).toBeVisible()
3232
})
@@ -40,11 +40,11 @@ test('can change the fill color using the hex input', async ({ page }) => {
4040
await expect(hexInput).toHaveValue('#ff0000')
4141
})
4242

43-
test('style buttons appear when Background Image is selected', async ({
43+
test('style buttons appear when Image fill is selected', async ({
4444
page,
4545
}) => {
46-
// Switch to Background Image
47-
await page.getByRole('button', { name: 'Background Image' }).click()
46+
// Switch to Image fill
47+
await page.getByRole('button', { name: 'Image', exact: true }).click()
4848

4949
// Style buttons should be visible
5050
await expect(page.getByRole('button', { name: 'cover' })).toBeVisible()

e2e/smoke.spec.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,6 @@ test('upload zone is visible', async ({ page }) => {
1414
test('padding settings panel is visible', async ({ page }) => {
1515
await page.goto('/')
1616
await expect(page.getByText('Padding Settings')).toBeVisible()
17-
await expect(page.getByText('Solid Color')).toBeVisible()
18-
await expect(page.getByText('Background Image')).toBeVisible()
17+
await expect(page.getByText('Gradient')).toBeVisible()
18+
await expect(page.getByText('Blur')).toBeVisible()
1919
})

src/App.tsx

Lines changed: 18 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,16 @@ const DEFAULT_SETTINGS: PaddingSettings = {
1616
fillImageDataUrl: null,
1717
fillImageStyle: 'cover',
1818
aspectRatio: 'auto',
19+
customRatioWidth: 4,
20+
customRatioHeight: 3,
1921
borderPadding: 0,
22+
outputFormat: 'png',
23+
outputQuality: 0.92,
24+
maxDimension: 0,
25+
gradientDirection: 'vertical',
26+
gradientColorStart: '#ffffff',
27+
gradientColorEnd: '#000000',
28+
blurAmount: 40,
2029
};
2130

2231
function readFileAsDataUrl(file: File): Promise<string> {
@@ -78,8 +87,13 @@ export default function App() {
7887
setProgress(0);
7988

8089
// Determine target aspect ratio
81-
const preset = ASPECT_RATIO_PRESETS.find(p => p.value === settings.aspectRatio);
82-
const target = preset?.ratio ?? findMaxAspectRatio(photos);
90+
let target: number;
91+
if (settings.aspectRatio === 'custom') {
92+
target = settings.customRatioWidth / settings.customRatioHeight;
93+
} else {
94+
const preset = ASPECT_RATIO_PRESETS.find(p => p.value === settings.aspectRatio);
95+
target = preset?.ratio ?? findMaxAspectRatio(photos);
96+
}
8397

8498
const processed: UploadedPhoto[] = [];
8599
for (let i = 0; i < photos.length; i++) {
@@ -96,10 +110,11 @@ export default function App() {
96110
const processedPhotos = photos.filter(p => p.paddedDataUrl);
97111
if (processedPhotos.length === 0) return;
98112

113+
const ext = settings.outputFormat === 'jpeg' ? 'jpg' : settings.outputFormat;
99114
const zip = new JSZip();
100115
processedPhotos.forEach((photo, idx) => {
101116
const base64 = photo.paddedDataUrl!.split(',')[1];
102-
zip.file(`squarify-${String(idx + 1).padStart(2, '0')}.png`, base64, { base64: true });
117+
zip.file(`squarify-${String(idx + 1).padStart(2, '0')}.${ext}`, base64, { base64: true });
103118
});
104119

105120
const blob = await zip.generateAsync({ type: 'blob' });

src/__tests__/PaddingSettingsPanel.test.tsx

Lines changed: 46 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,16 @@ const defaultSettings: PaddingSettings = {
1010
fillImageDataUrl: null,
1111
fillImageStyle: 'cover',
1212
aspectRatio: 'auto',
13+
customRatioWidth: 4,
14+
customRatioHeight: 3,
1315
borderPadding: 0,
16+
outputFormat: 'png',
17+
outputQuality: 0.92,
18+
maxDimension: 0,
19+
gradientDirection: 'vertical',
20+
gradientColorStart: '#ffffff',
21+
gradientColorEnd: '#000000',
22+
blurAmount: 40,
1423
};
1524

1625
function renderPanel(overrides: Partial<Parameters<typeof PaddingSettingsPanel>[0]> = {}) {
@@ -27,34 +36,39 @@ function renderPanel(overrides: Partial<Parameters<typeof PaddingSettingsPanel>[
2736
}
2837

2938
describe('PaddingSettingsPanel', () => {
30-
it('renders with default settings (Solid Color selected)', () => {
39+
it('renders with default settings (Color fill selected)', () => {
3140
renderPanel();
32-
expect(screen.getByText('Solid Color')).toBeInTheDocument();
33-
expect(screen.getByText('Background Image')).toBeInTheDocument();
34-
expect(screen.getByText('Color')).toBeInTheDocument();
41+
expect(screen.getByText('Padding Settings')).toBeInTheDocument();
42+
// Fill type buttons
43+
const colorBtn = screen.getAllByText('Color')[0];
44+
expect(colorBtn).toBeInTheDocument();
45+
expect(screen.getByText('Gradient')).toBeInTheDocument();
46+
expect(screen.getByText('Blur')).toBeInTheDocument();
3547
});
3648

37-
it('switches to Background Image fill type', async () => {
49+
it('switches to Image fill type', async () => {
3850
const user = userEvent.setup();
3951
const { onChange } = renderPanel();
4052

41-
await user.click(screen.getByText('Background Image'));
53+
// The fill type buttons are in the grid - "Image" is the second one
54+
const fillButtons = screen.getAllByText('Image');
55+
await user.click(fillButtons[0]);
4256
expect(onChange).toHaveBeenCalledWith(expect.objectContaining({ fillType: 'image' }));
4357
});
4458

45-
it('switches to Solid Color fill type', async () => {
59+
it('switches to Color fill type from image', async () => {
4660
const user = userEvent.setup();
4761
const { onChange } = renderPanel({
4862
settings: { ...defaultSettings, fillType: 'image' },
4963
});
5064

51-
await user.click(screen.getByText('Solid Color'));
65+
const colorButtons = screen.getAllByText('Color');
66+
await user.click(colorButtons[0]);
5267
expect(onChange).toHaveBeenCalledWith(expect.objectContaining({ fillType: 'color' }));
5368
});
5469

5570
it('shows color picker when fillType is color', () => {
5671
renderPanel();
57-
expect(screen.getByText('Color')).toBeInTheDocument();
5872
const colorInput = document.querySelector('input[type="color"]');
5973
expect(colorInput).toBeInTheDocument();
6074
});
@@ -64,7 +78,29 @@ describe('PaddingSettingsPanel', () => {
6478
settings: { ...defaultSettings, fillType: 'image' },
6579
});
6680
expect(screen.getByText('Upload image')).toBeInTheDocument();
67-
expect(screen.getByText('Image')).toBeInTheDocument();
81+
});
82+
83+
it('shows gradient controls when fillType is gradient', () => {
84+
renderPanel({
85+
settings: { ...defaultSettings, fillType: 'gradient' },
86+
});
87+
expect(screen.getByText('Direction')).toBeInTheDocument();
88+
expect(screen.getByText('Start')).toBeInTheDocument();
89+
expect(screen.getByText('End')).toBeInTheDocument();
90+
});
91+
92+
it('shows blur controls when fillType is blur', () => {
93+
renderPanel({
94+
settings: { ...defaultSettings, fillType: 'blur' },
95+
});
96+
expect(screen.getByText('Blur Amount')).toBeInTheDocument();
97+
});
98+
99+
it('shows output format selector', () => {
100+
renderPanel();
101+
expect(screen.getByText('png')).toBeInTheDocument();
102+
expect(screen.getByText('jpeg')).toBeInTheDocument();
103+
expect(screen.getByText('webp')).toBeInTheDocument();
68104
});
69105

70106
it('process button is disabled when hasPhotos is false', () => {

0 commit comments

Comments
 (0)