Skip to content

Commit 9dc0bf9

Browse files
authored
Merge pull request #514 from CodeChefVIT/feat/compression-on-upload
Feat/compression on upload
2 parents 34bf8b9 + 1f96df2 commit 9dc0bf9

3 files changed

Lines changed: 78 additions & 34 deletions

File tree

src/app/api/upload/route.ts

Lines changed: 27 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,14 @@
11
import { connectToDatabase } from "@/lib/database/mongoose";
22
import { PaperAdmin } from "@/db/papers";
3-
import { createPDFfromImages } from "@/lib/storage/pdf";
3+
import { createPDFfromImages, compressPDF } from "@/lib/storage/pdf";
44
import { uploadPDF, uploadThumbnail } from "@/lib/storage/gcp";
55
import { success, failure } from "@/lib/utils/response";
66

77
export const runtime = "nodejs";
88

9+
const MAX_COMPRESSED_PDF_SIZE = 5 * 1024 * 1024; // 5MB compressed
10+
const COMPRESS_THRESHOLD = 5 * 1024 * 1024; // 5MB
11+
912
export async function POST(req: Request) {
1013
try {
1114
await connectToDatabase();
@@ -23,9 +26,31 @@ export async function POST(req: Request) {
2326
if (!files[0]) {
2427
return failure("No PDF file provided.", 400);
2528
}
26-
pdfBytes = new Uint8Array(await files[0].arrayBuffer());
29+
30+
const rawPdfBytes = new Uint8Array(await files[0].arrayBuffer());
31+
if (rawPdfBytes.length > COMPRESS_THRESHOLD) {
32+
const compressedPdfBytes = await compressPDF(rawPdfBytes);
33+
pdfBytes = compressedPdfBytes.length <= rawPdfBytes.length
34+
? compressedPdfBytes
35+
: rawPdfBytes;
36+
37+
if (pdfBytes.length > MAX_COMPRESSED_PDF_SIZE) {
38+
return failure(
39+
"PDF is too large after compression. The compressed file must be under 5MB.",
40+
413,
41+
);
42+
}
43+
} else {
44+
pdfBytes = rawPdfBytes;
45+
}
2746
} else {
2847
pdfBytes = await createPDFfromImages(files);
48+
if (pdfBytes.length > MAX_COMPRESSED_PDF_SIZE) {
49+
return failure(
50+
"Generated PDF is too large after compression. Please upload fewer or smaller images.",
51+
413,
52+
);
53+
}
2954
}
3055

3156
const buffer = Buffer.from(pdfBytes);

src/app/upload/page.tsx

Lines changed: 15 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ import { CSS } from "@dnd-kit/utilities";
2525
import Dropzone from "react-dropzone";
2626
import { Upload, XIcon } from "lucide-react";
2727
import { GlobalWorkerOptions } from "pdfjs-dist";
28-
import type { ApiResponse } from "@/interface"
28+
import type { ApiResponse } from "@/interface";
2929

3030
GlobalWorkerOptions.workerSrc =
3131
"https://cdnjs.cloudflare.com/ajax/libs/pdf.js/4.8.69/pdf.worker.min.mjs";
@@ -79,7 +79,6 @@ export default function Page() {
7979

8080
const fileCheckAndSelect = useCallback(
8181
(acceptedFiles: File[]) => {
82-
const maxFileSize = 5 * 1024 * 1024;
8382
const allowedFileTypes = [
8483
"application/pdf",
8584
"image/jpeg",
@@ -137,28 +136,18 @@ export default function Page() {
137136
}
138137

139138
const allFiles = [...files, ...acceptedFiles];
140-
if (allFiles.length > 5) {
141-
toast.error("You can upload up to 5 files only", { id: toastId });
142-
return;
143-
}
144-
145-
const totalSize = allFiles.reduce(
146-
(sum, f) => sum + f.size,
147-
0,
148-
);
149-
if (totalSize > maxFileSize){
150-
toast.error("The total upload size exceeds 5MB.", { id: toastId });
139+
if (allFiles.length > 10) {
140+
toast.error("You can upload up to 10 files only", { id: toastId });
151141
return;
152142
}
153143

154144
const invalidFiles = acceptedFiles.filter(
155-
(file) =>
156-
file.size > maxFileSize || !allowedFileTypes.includes(file.type),
145+
(file) => !allowedFileTypes.includes(file.type),
157146
);
158147

159148
if (invalidFiles.length > 0) {
160149
toast.error(
161-
"Some files are invalid. Make sure the total size is below 5MB and files are of allowed types (PDF, JPEG, PNG, GIF).",
150+
"Some files are invalid. Make sure they are PDFs, JPEGs, PNGs, or GIFs.",
162151
{ id: toastId },
163152
);
164153
return;
@@ -279,7 +268,10 @@ export default function Page() {
279268
await toast.promise(
280269
async () => {
281270
try {
282-
await axios.post<ApiResponse<uploadResponse>>("/api/upload", formData);
271+
await axios.post<ApiResponse<uploadResponse>>(
272+
"/api/upload",
273+
formData,
274+
);
283275
return { message: "Papers uploaded successfully!" };
284276
} catch (error) {
285277
if (error instanceof AxiosError) {
@@ -447,12 +439,14 @@ export default function Page() {
447439
);
448440

449441
return (
450-
<section
442+
<section
451443
{...getRootProps()}
452-
className="mt-6 flex w-full flex-col items-center">
444+
className="mt-6 flex w-full flex-col items-center"
445+
>
453446
<input {...getInputProps()} />
454447
<div className="flex w-max gap-4">
455-
<div className="scrollbar-hide flex w-[80vw] max-w-4xl flex-col justify-between overflow-x-auto overflow-y-hidden rounded-[40px] border-[6px] border-[#A78BFA] bg-indigo-900/10 p-4 dark:border-indigo-900 sm:p-6 md:w-max md:p-8">j
448+
<div className="scrollbar-hide flex w-[80vw] max-w-4xl flex-col justify-between overflow-x-auto overflow-y-hidden rounded-[40px] border-[6px] border-[#A78BFA] bg-indigo-900/10 p-4 dark:border-indigo-900 sm:p-6 md:w-max md:p-8">
449+
j
456450
<DndContext
457451
sensors={sensors}
458452
collisionDetection={closestCenter}
@@ -528,10 +522,9 @@ export default function Page() {
528522
</div>
529523
)}
530524
</section>
531-
)
525+
);
532526
}}
533527
</Dropzone>
534-
535528
)}
536529

537530
<Button
@@ -543,7 +536,6 @@ export default function Page() {
543536
</Button>
544537
</div>
545538
</div>
546-
547539

548540
{zoomIndex !== null && (
549541
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black bg-opacity-70">

src/lib/storage/pdf.ts

Lines changed: 36 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,47 @@
11
import { PDFDocument } from "pdf-lib";
2+
import { createCanvas, loadImage } from "canvas";
3+
4+
const DEFAULT_PDF_SAVE_OPTIONS = {
5+
useObjectStreams: true,
6+
compress: true,
7+
};
8+
const MAX_IMAGE_DIMENSION = 1600;
9+
const JPEG_QUALITY = 0.72;
10+
11+
async function normalizeImage(file: File) {
12+
const rawBytes = Buffer.from(await file.arrayBuffer());
13+
const image = await loadImage(rawBytes);
14+
15+
const originalWidth = image.width;
16+
const originalHeight = image.height;
17+
const scale = Math.min(1, MAX_IMAGE_DIMENSION / Math.max(originalWidth, originalHeight));
18+
const width = Math.max(1, Math.round(originalWidth * scale));
19+
const height = Math.max(1, Math.round(originalHeight * scale));
20+
21+
const canvas = createCanvas(width, height);
22+
const ctx = canvas.getContext("2d");
23+
ctx.drawImage(image, 0, 0, width, height);
24+
25+
return canvas.toBuffer("image/jpeg", {
26+
quality: JPEG_QUALITY,
27+
chromaSubsampling: true,
28+
});
29+
}
230

331
export async function createPDFfromImages(files: File[]): Promise<Uint8Array> {
432
const pdfDoc = await PDFDocument.create();
533

634
for (const file of files) {
7-
const imgBytes = Buffer.from(await file.arrayBuffer());
8-
let img;
9-
if (file.type === "image/png") {
10-
img = await pdfDoc.embedPng(imgBytes);
11-
} else if (file.type === "image/jpeg" || file.type === "image/jpg") {
12-
img = await pdfDoc.embedJpg(imgBytes);
13-
} else continue;
14-
35+
const normalizedBytes = await normalizeImage(file);
36+
const img = await pdfDoc.embedJpg(normalizedBytes);
1537
const page = pdfDoc.addPage([img.width, img.height]);
1638
page.drawImage(img, { x: 0, y: 0, width: img.width, height: img.height });
1739
}
1840

19-
return pdfDoc.save();
41+
return pdfDoc.save(DEFAULT_PDF_SAVE_OPTIONS);
42+
}
43+
44+
export async function compressPDF(pdfBytes: Uint8Array): Promise<Uint8Array> {
45+
const pdfDoc = await PDFDocument.load(pdfBytes, { ignoreEncryption: true });
46+
return pdfDoc.save(DEFAULT_PDF_SAVE_OPTIONS);
2047
}

0 commit comments

Comments
 (0)