Skip to content

Commit

Permalink
feat: Add export to jpeg (#67)
Browse files Browse the repository at this point in the history
  • Loading branch information
rhea-so committed Apr 9, 2024
2 parents 1939f37 + b997ceb commit d34ca64
Show file tree
Hide file tree
Showing 12 changed files with 99 additions and 8 deletions.
29 changes: 29 additions & 0 deletions web/src/core/canvas-to-jpeg.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
const canvasToJpeg = async (canvas: HTMLCanvasElement, quality?: number): Promise<ArrayBuffer> => {
const arrayBuffer = await new Promise<ArrayBuffer>((resolve) => {
canvas.toBlob(
(blob) => {
if (!blob) {
throw new Error('Failed to convert canvas to blob');
}
const reader = new FileReader();
reader.onload = () => {
if (reader.result instanceof ArrayBuffer) {
resolve(reader.result);
} else {
throw new Error('Failed to convert canvas to ArrayBuffer');
}
};
reader.readAsArrayBuffer(blob);
},
'image/jpeg',
(quality || 1) / 100
);
});
// remove the original canvas
canvas.width = 0;
canvas.height = 0;
canvas.remove();
return arrayBuffer;
};

export default canvasToJpeg;
6 changes: 4 additions & 2 deletions web/src/core/download-many-file.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
import JSZip from 'jszip';
import saveAs from 'file-saver';

const downloadManyFile = async (files: { name: string; buffer: ArrayBuffer }[]): Promise<void> => {
const downloadManyFile = async (files: { name: string; buffer: ArrayBuffer; type: 'image/jpeg' | 'image/webp' }[]): Promise<void> => {
const zip = new JSZip();
files.forEach((file) => zip.file(file.name.replace(/\.[^/.]+$/, '.webp'), file.buffer, { binary: true }));
files.forEach((file) =>
zip.file(file.name.replace(/\.[^/.]+$/, `.${file.type === 'image/jpeg' ? 'jpg' : 'webp'}`), file.buffer, { binary: true })
);
zip.generateAsync({ type: 'blob' }).then((content) => saveAs(content, 'images.zip'));
};

Expand Down
4 changes: 2 additions & 2 deletions web/src/core/download-one-file.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import saveAs from 'file-saver';

const downloadOneFile = async (file: { name: string; buffer: ArrayBuffer }): Promise<void> => {
saveAs(new Blob([file.buffer], { type: 'image/webp' }), file.name.replace(/\.[^/.]+$/, '.webp'));
const downloadOneFile = async (file: { name: string; buffer: ArrayBuffer; type: 'image/jpeg' | 'image/webp' }): Promise<void> => {
saveAs(new Blob([file.buffer], { type: file.type }), file.name.replace(/\.[^/.]+$/, `.${file.type === 'image/jpeg' ? 'jpg' : 'webp'}`));
};

export default downloadOneFile;
12 changes: 12 additions & 0 deletions web/src/icons/jpeg.icon.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import { Icon } from 'konsta/react';
import { SiJpeg } from 'react-icons/si';

interface JpegIconProps {
size?: number;
}

const JpegIcon = ({ size }: JpegIconProps) => {
return <Icon ios={<SiJpeg size={size} />} />;
};

export default JpegIcon;
1 change: 1 addition & 0 deletions web/src/locales/translations/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@

"root.settings.dark-mode": "Dark Mode",
"root.settings.language": "Language",
"root.settings.export-to-jpeg": "Export to JPEG",
"root.settings.quality": "Quality",
"root.settings.fix-image-width": "Fix Image Width",
"root.settings.image-width": "Image Width (px)",
Expand Down
3 changes: 2 additions & 1 deletion web/src/locales/translations/ja.json
Original file line number Diff line number Diff line change
Expand Up @@ -27,10 +27,11 @@

"root.settings.dark-mode": "ダークモード",
"root.settings.language": "言語",
"root.settings.export-to-jpeg": "JPEGにエクスポート",
"root.settings.quality": "画質",
"root.settings.fix-image-width": "画像幅を固定",
"root.settings.image-width": "画像幅(px)",
"root.settings.fix-watermark": "ウォーターマークを固定",
"root.settings.fix-watermark": "ウォーターマーク",
"root.settings.watermark": "ウォーターマーク",
"root.settings.show-camera-maker": "カメラメーカーを表示",
"root.settings.show-camera-model": "カメラモデルを表示",
Expand Down
1 change: 1 addition & 0 deletions web/src/locales/translations/ko.json
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@

"root.settings.dark-mode": "다크 모드",
"root.settings.language": "언어",
"root.settings.export-to-jpeg": "JPEG로 내보내기",
"root.settings.quality": "품질",
"root.settings.fix-image-width": "사진 너비 고정",
"root.settings.image-width": "사진 너비 (px)",
Expand Down
10 changes: 8 additions & 2 deletions web/src/pages/root/components/download-all-photo.button.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,12 @@ import themes from '../../../themes';
import canvasToWebp from '../../../core/canvas-to-webp';
import downloadManyFile from '../../../core/download-many-file';
import draw from '../../../themes/draw';
import canvasToJpeg from '../../../core/canvas-to-jpeg';

const DownloadAllPhotoButton = () => {
const { t } = useTranslation();
const {
exportToJpeg,
selectedThemeName,
quality,
photos,
Expand All @@ -34,7 +36,7 @@ const DownloadAllPhotoButton = () => {
onClick={async () => {
if (photos.length === 0) return;
setLoading(true);
const files: { name: string; buffer: ArrayBuffer }[] = [];
const files: { name: string; buffer: ArrayBuffer; type: 'image/jpeg' | 'image/webp' }[] = [];
await Promise.all(
photos.map(async (photo) => {
const canvas = await draw(selectedTheme.func, photo, {
Expand All @@ -47,7 +49,11 @@ const DownloadAllPhotoButton = () => {
overrideCameraModel,
overrideLensModel,
});
files.push({ name: photo.file.name, buffer: await canvasToWebp(canvas, quality) });
files.push({
name: photo.file.name,
buffer: exportToJpeg ? await canvasToJpeg(canvas, quality) : await canvasToWebp(canvas, quality),
type: exportToJpeg ? 'image/jpeg' : 'image/webp',
});
})
);
await downloadManyFile(files);
Expand Down
8 changes: 7 additions & 1 deletion web/src/pages/root/components/download-one-photo.button.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,15 @@ import themes from '../../../themes';
import canvasToWebp from '../../../core/canvas-to-webp';
import { IoDownloadOutline } from 'react-icons/io5';
import draw from '../../../themes/draw';
import canvasToJpeg from '../../../core/canvas-to-jpeg';

interface DownloadOnePhotoButtonProps {
photo: Photo;
}

const DownloadOnePhotoButton: React.FC<DownloadOnePhotoButtonProps> = ({ photo }) => {
const {
exportToJpeg,
selectedThemeName,
quality,
fixImageWidth,
Expand Down Expand Up @@ -44,7 +46,11 @@ const DownloadOnePhotoButton: React.FC<DownloadOnePhotoButtonProps> = ({ photo }
overrideCameraModel,
overrideLensModel,
});
await downloadOneFile({ name: photo.file.name, buffer: await canvasToWebp(canvas, quality) });
await downloadOneFile({
name: photo.file.name,
buffer: exportToJpeg ? await canvasToJpeg(canvas, quality) : await canvasToWebp(canvas, quality),
type: exportToJpeg ? 'image/jpeg' : 'image/webp',
});
setLoading(false);
}}
>
Expand Down
21 changes: 21 additions & 0 deletions web/src/pages/root/components/export-to-jpeg.list-item.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import { ListItem, Toggle } from 'konsta/react';
import { useStore } from '../../../store';
import { useTranslation } from 'react-i18next';
import JpegIcon from '../../../icons/jpeg.icon';

const ExportToJpegListItem = () => {
const { t } = useTranslation();
const { exportToJpeg, setExportToJpeg } = useStore();

return (
<>
<ListItem
title={t('root.settings.export-to-jpeg')}
media={<JpegIcon size={26} />}
after={<Toggle checked={exportToJpeg} onChange={() => setExportToJpeg(!exportToJpeg)} />}
/>
</>
);
};

export default ExportToJpegListItem;
2 changes: 2 additions & 0 deletions web/src/pages/root/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ import ReleasesButton from './components/releases.button';
import OverrideMetadataPopup from './components/override-metadata.popup';
import OverrideMetadataButton from './components/override-metadata.button';
import FixWatermarkListItem from './components/fix-watermark.list-item';
import ExportToJpegListItem from './components/export-to-jpeg.list-item';

const RootPage = () => {
const { t } = useTranslation();
Expand Down Expand Up @@ -74,6 +75,7 @@ const RootPage = () => {
</List>

<List strongIos inset>
<ExportToJpegListItem />
<QualityListItem />
<FixImageWidthListItem />
</List>
Expand Down
10 changes: 10 additions & 0 deletions web/src/store.ts
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,9 @@ type Store = {

watermark: string;
setWatermark: (watermark: string) => void;

exportToJpeg: boolean;
setExportToJpeg: (exportToJpeg: boolean) => void;
};

const useStore = create<Store>((set) => ({
Expand Down Expand Up @@ -181,6 +184,13 @@ const useStore = create<Store>((set) => ({
localStorage.setItem('watermark', watermark);
return { watermark };
}),

exportToJpeg: localStorage.getItem('exportToJpeg') === 'true',
setExportToJpeg: (exportToJpeg: boolean) =>
set(() => {
localStorage.setItem('exportToJpeg', exportToJpeg.toString());
return { exportToJpeg };
}),
}));

// Set the theme on page load
Expand Down

0 comments on commit d34ca64

Please sign in to comment.