Skip to content

Commit

Permalink
feat: added image to pdf tool
Browse files Browse the repository at this point in the history
feat: added image to pdf tool
  • Loading branch information
a0v0 authored Apr 15, 2024
2 parents 19057e0 + facda3b commit 6fff1b0
Show file tree
Hide file tree
Showing 27 changed files with 693 additions and 169 deletions.
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -36,4 +36,5 @@ yarn-error.log*
*.tsbuildinfo
next-env.d.ts

.contentlayer
.contentlayer
search-meta.json
6 changes: 6 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,3 +27,9 @@ task dev
```

For more commands, see [Taskfile](./Taskfile.yml)

## Gallery

| | |
| ----------------------------- | ----------------------------- |
| ![1](./archive/gallery/1.png) | ![2](./archive/gallery/2.png) |
2 changes: 2 additions & 0 deletions TODO.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
<!-- TODO: create release only when a new tag is released -->
<!-- TODO: add seo -->
<!-- wasm notes -->

```
Expand Down
9 changes: 9 additions & 0 deletions Taskfile.yml
Original file line number Diff line number Diff line change
Expand Up @@ -19,3 +19,12 @@ tasks:
desc: Disable nextjs telemetry
cmds:
- pnpm exec next telemetry disable

# Sometimes git is not tracking the files that are in .gitignore
# This task will fix that
# See: https://stackoverflow.com/questions/25436312/gitignore-not-working
fix_gitignore:
desc: Fix gitignore
cmds:
- git rm -rf --cached .
- git add .
125 changes: 125 additions & 0 deletions app/tools/image-to-pdf/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
"use client";

import FileUploader from "@/components/file-uploader/file-uploader";
import {useFileUploaderStore} from "@/components/file-uploader/store";
import {subtitle, title} from "@/components/primitives";
import {getToolByHref} from "@/config/tools";
import {MimeType} from "@/libs/mime";
import {downloadFile} from "@/utils/download";
import {
Button,
Modal,
ModalBody,
ModalContent,
ModalFooter,
ModalHeader,
Spacer,
useDisclosure,
} from "@nextui-org/react";
import {usePathname} from "next/navigation";
import {useEffect, useState} from "react";
import {WorkerInput, WorkerOutput} from "./worker";

const allowedFileTypes: MimeType[] = [
"image/jpeg",
"image/webp",
"image/png",
// TODO: add support for these too
// "image/svg+xml",
// "image/bmp",
// "image/tiff",
// "image/gif",
// "image/heif",
// "image/heic",
];

export default function page() {
const {files, reset, error} = useFileUploaderStore();
const path = usePathname();
const tool = getToolByHref(path);
const [isLoading, setIsLoading] = useState(false);
const {isOpen, onOpen, onOpenChange} = useDisclosure();

function _startProcess() {
setIsLoading(true);

const worker = new Worker(new URL("./worker.ts", import.meta.url));
worker.onmessage = (event: MessageEvent<WorkerOutput>) => {
setIsLoading(false);
const {blob, error} = event.data;
if (error?.length == 0) {
downloadFile(blob, files[0].name.split(".")[0] + "-merged.pdf");
} else {
onOpen();
reset();
}
};

const workerInput: WorkerInput = {
files: files,
};
worker.postMessage(workerInput);
}

useEffect(() => {
if (error.length > 0) {
onOpen();
reset();
}
}, [error]);

return (
<>
<center>
<Spacer y={3} />
<h1 className={title({color: "green"})}>{tool?.title}</h1>
<h2
className={subtitle({
fullWidth: true,
})}
>
{tool?.description}
</h2>
<Spacer y={6} />
<FileUploader primaryColor="#18c964" acceptedFileTypes={allowedFileTypes} />
<Spacer y={6} />
{files.length > 0 ? (
<div className="grid grid-cols-2 gap-2">
<Button color="danger" variant="bordered" onPress={reset}>
Reset
</Button>
<Button
color="success"
variant="bordered"
isLoading={isLoading}
onPress={_startProcess}
>
Convert to PDF
</Button>
</div>
) : null}
</center>
<Modal backdrop="blur" isOpen={isOpen} onOpenChange={onOpenChange}>
<ModalContent>
{(onClose) => (
<>
<ModalHeader className="flex flex-col gap-1">Invalid File</ModalHeader>
<ModalBody>
<p>
One or more of the files you have selected are not supported, invalid, or
corrupted.
</p>
<p>Please ensure that the file is valid and not corrupted.</p>
</ModalBody>
<ModalFooter>
<Button color="danger" variant="flat" onPress={onClose}>
OK
</Button>
</ModalFooter>
</>
)}
</ModalContent>
</Modal>
</>
);
}
48 changes: 48 additions & 0 deletions app/tools/image-to-pdf/worker.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import {jsPDF} from "jspdf";

export interface WorkerInput {
files: File[];
}

export interface WorkerOutput {
blob: Blob;
error: string[];
}

const onmessage = (input: MessageEvent<WorkerInput>) => {
const {files} = input.data;

async function _start() {
try {
const pdfDoc = new jsPDF();
pdfDoc.deletePage(1);
for (const file of files) {
var img = URL.createObjectURL(file);
var imgProps = pdfDoc.getImageProperties(img);
var page = pdfDoc.addPage(
[imgProps.height, imgProps.width],
imgProps.height > imgProps.width ? "portrait" : "landscape",
);
page.addImage(img, imgProps.fileType, 0, 0, imgProps.width, imgProps.height);
URL.revokeObjectURL(img);
}

var workerOutput: WorkerOutput = {
blob: pdfDoc.output("blob"),
error: [],
};
postMessage(workerOutput);
} catch (error) {
var workerOutput: WorkerOutput = {
blob: new Blob(),
error: ["An error occurred while processing the images. Please try again."],
};
postMessage(workerOutput);
console.error("🍎 Error: ", error);
}
}

_start();
};

addEventListener("message", onmessage);
2 changes: 1 addition & 1 deletion app/tools/merge-pdf/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
import FileUploader from "@/components/file-uploader/file-uploader";
import {useFileUploaderStore} from "@/components/file-uploader/store";
import {subtitle, title} from "@/components/primitives";
import {getToolByHref} from "@/config/config";
import {getToolByHref} from "@/config/tools";
import {downloadFile} from "@/utils/download";
import {
Button,
Expand Down
Binary file added archive/gallery/1.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added archive/gallery/2.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified archive/icons.psd
Binary file not shown.
37 changes: 15 additions & 22 deletions components/file-uploader/file-uploader.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import {MimeType} from "@/libs/mime";
import {MimeType, mimeToExtension} from "@/libs/mime";
import {getRandomId} from "@/utils/random";
import type {
DragEndEvent,
DragStartEvent,
Expand All @@ -8,12 +9,10 @@ import type {
import {
DndContext,
DragOverlay,
DropAnimation,
KeyboardSensor,
MeasuringStrategy,
PointerSensor,
closestCenter,
defaultDropAnimationSideEffects,
useDndContext,
useSensor,
useSensors,
Expand All @@ -29,6 +28,8 @@ import {
Button,
Card,
CardBody,
Chip,
Divider,
Link,
Modal,
ModalBody,
Expand Down Expand Up @@ -78,24 +79,6 @@ const FileUploader: React.FC<FileUploaderProps> = ({primaryColor, acceptedFileTy
strategy: MeasuringStrategy.Always,
},
};
const dropAnimation: DropAnimation = {
keyframes({transform}) {
return [
{transform: CSS.Transform.toString(transform.initial)},
{
transform: CSS.Transform.toString({
scaleX: 0.98,
scaleY: 0.98,
x: transform.final.x - 10,
y: transform.final.y - 10,
}),
},
];
},
sideEffects: defaultDropAnimationSideEffects({
className: {},
}),
};

useEffect(() => {
if (acceptedFiles) {
Expand Down Expand Up @@ -247,7 +230,7 @@ const FileUploader: React.FC<FileUploaderProps> = ({primaryColor, acceptedFileTy
<CardBody className="items-center justify-center">
<input {...getInputProps()} />

<Card onPress={open} className="w-72 " isPressable>
<Card onPress={open} className="w-72" isPressable>
<CardBody className="text-center ">
<h1 className={subtitle({fullWidth: true, size: "sm"})}>+ Select Files</h1>
</CardBody>
Expand All @@ -264,6 +247,16 @@ const FileUploader: React.FC<FileUploaderProps> = ({primaryColor, acceptedFileTy
>
drop your files here...
</h2>
<Divider className="my-2" />
<div className="max-w-96 gap-2 text-center">
{acceptedFileTypes.map((fileType) => (
<Chip key={getRandomId()} className="m-[2px]" color="success" variant="flat">
{mimeToExtension(fileType) != undefined
? mimeToExtension(fileType)?.toUpperCase()
: fileType}
</Chip>
))}
</div>
</CardBody>
</Card>
)}
Expand Down
2 changes: 2 additions & 0 deletions components/file-uploader/preview/Page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,8 @@ export const Page = forwardRef<HTMLLIElement, Props>(function Page(
const existingPreview = previews.find((preview) => preview.file === file);
if (existingPreview) {
} else {
// TODO: file is an image convert to png

setPreview(file, URL.createObjectURL(file));
}
} else {
Expand Down
62 changes: 32 additions & 30 deletions components/navbar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -130,36 +130,38 @@ export const Navbar: FC<NavbarProps> = ({children, routes, mobileRoutes = [], sl
</NavbarBrand>
</NavbarContent>
<NavbarContent className="hidden sm:flex gap-4" justify="start">
{manifest.routes.map((category, index) => (
<Dropdown key={index}>
<NavbarItem>
<DropdownTrigger>
<Button
className="p-0 bg-transparent data-[hover=true]:bg-transparent"
endContent={<ChevronDown fill="currentColor" size={16} />}
radius="sm"
variant="light"
>
{category.title}
</Button>
</DropdownTrigger>
</NavbarItem>
<DropdownMenu
key={index}
aria-label={category.title}
className="w-[340px]"
itemClasses={{
base: "gap-4",
}}
>
{category.routes.map((tool, index) => (
<DropdownItem key={index} href={tool.href} startContent={tool.icon}>
{tool.title}
</DropdownItem>
))}
</DropdownMenu>
</Dropdown>
))}
{manifest.routes.map((category, index) =>
category.routes.length > 0 ? (
<Dropdown key={index}>
<NavbarItem>
<DropdownTrigger>
<Button
className="p-0 bg-transparent data-[hover=true]:bg-transparent"
endContent={<ChevronDown fill="currentColor" size={16} />}
radius="sm"
variant="light"
>
{category.title}
</Button>
</DropdownTrigger>
</NavbarItem>
<DropdownMenu
key={index}
aria-label={category.title}
className="w-[340px]"
itemClasses={{
base: "gap-4",
}}
>
{category.routes.map((tool, index) => (
<DropdownItem key={index} href={tool.href} startContent={tool.icon}>
{tool.title}
</DropdownItem>
))}
</DropdownMenu>
</Dropdown>
) : null,
)}
</NavbarContent>
<NavbarContent className="flex w-full gap-2 sm:hidden" justify="end">
<NavbarItem className="flex h-full items-center">
Expand Down
Loading

0 comments on commit 6fff1b0

Please sign in to comment.