Next step
1) Make a Demo video
2) Deploy
This commit is contained in:
Aryan Singh committed 2026-02-25 23:23:23 -06:00
1 parent 41186abc57
commit 6db1dc32ec
13 files changed
+566 -80

No files matched your search

@@ -0,0 +1,93 @@
import { NextRequest, NextResponse } from "next/server";
import { getServerSession } from "next-auth";
import { authOptions } from "@/app/api/auth/[...nextauth]/route";
import { prisma } from "@/lib/prisma";
import { readdir, readFile } from "fs/promises";
import path from "path";
import { PDFDocument } from "pdf-lib";
type ExportType = "original" | "pixel";
function parsePageNumber(fileName: string): number {
const match = fileName.match(/^page_(\d+)\.png$/);
return match ? Number(match[1]) : Number.MAX_SAFE_INTEGER;
}
export async function GET(
req: NextRequest,
{ params }: { params: Promise<{ id: string }> }
) {
const session = await getServerSession(authOptions);
if (!session?.user?.email) {
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
}
const { id } = await params;
const typeParam = req.nextUrl.searchParams.get("type");
const type: ExportType = typeParam === "pixel" ? "pixel" : "original";
const doc = await prisma.pdfDocument.findUnique({ where: { id } });
if (!doc) {
return NextResponse.json({ error: "Document not found" }, { status: 404 });
}
const user = await prisma.user.findUnique({
where: { email: session.user.email },
});
if (!user || doc.userId !== user.id) {
return NextResponse.json({ error: "Forbidden" }, { status: 403 });
}
try {
const resultsDir = path.join(
process.cwd(),
"public",
"results",
user.id,
id,
type
);
const files = (await readdir(resultsDir))
.filter((fileName) => /^page_\d+\.png$/.test(fileName))
.sort((a, b) => parsePageNumber(a) - parsePageNumber(b));
if (files.length === 0) {
return NextResponse.json(
{ error: `No ${type} pages found. Render/process the document first.` },
{ status: 409 }
);
}
const pdfDoc = await PDFDocument.create();
for (const fileName of files) {
const imagePath = path.join(resultsDir, fileName);
const imageBytes = await readFile(imagePath);
const pngImage = await pdfDoc.embedPng(imageBytes);
const page = pdfDoc.addPage([pngImage.width, pngImage.height]);
page.drawImage(pngImage, {
x: 0,
y: 0,
width: pngImage.width,
height: pngImage.height,
});
}
const pdfBytes = await pdfDoc.save();
const safeBaseName = doc.originalName.replace(/\.pdf$/i, "").replace(/\s+/g, "_");
const outputName = `${safeBaseName}_${type}.pdf`;
return new NextResponse(Buffer.from(pdfBytes), {
status: 200,
headers: {
"Content-Type": "application/pdf",
"Content-Disposition": `attachment; filename=\"${outputName}\"`,
"Cache-Control": "no-store",
},
});
} catch (error) {
console.error("PDF export error:", error);
return NextResponse.json({ error: "Failed to export PDF" }, { status: 500 });
}
}
@@ -80,7 +80,7 @@ export async function POST(
const stream = new ReadableStream({
async start(controller) {
try {
const { createCanvas } = await import("canvas");
const { createCanvas } = await import("@napi-rs/canvas");
const Tesseract = await import("tesseract.js");
const allPages: { page: number; path: string }[] = [];
@@ -106,6 +106,10 @@ export async function POST(
const canvas = createCanvas(width, height);
const ctx = canvas.getContext("2d");
// Fill white background for accurate OCR
ctx.fillStyle = "#ffffff";
ctx.fillRect(0, 0, width, height);
await page.render({
// eslint-disable-next-line @typescript-eslint/no-explicit-any
canvasContext: ctx as any,
@@ -34,14 +34,7 @@ export async function POST(
return NextResponse.json({ error: "Forbidden" }, { status: 403 });
}
// If already processed, return existing results
if (doc.pixelResultDir && doc.pixelPageCount > 0) {
const pages = Array.from({ length: doc.pixelPageCount }, (_, i) => ({
page: i + 1,
path: `${doc.pixelResultDir}/page_${i + 1}.png`,
}));
return NextResponse.json({ pages, pageCount: doc.pixelPageCount });
}
// Always regenerate so stale cached outputs don't persist.
try {
const pdfPath = path.join(process.cwd(), "public", doc.filePath);
@@ -67,7 +60,7 @@ export async function POST(
const publicDir = `/results/${user.id}/${id}/pixel`;
const pages: { page: number; path: string }[] = [];
const { createCanvas } = await import("canvas");
const { createCanvas } = await import("@napi-rs/canvas");
for (let pageNum = 1; pageNum <= numPages; pageNum++) {
const page = await pdfDoc.getPage(pageNum);
@@ -79,6 +72,11 @@ export async function POST(
const canvas = createCanvas(width, height);
const ctx = canvas.getContext("2d");
// Fill with white background first — node-canvas starts transparent
// which causes the image to appear all-black after processing
ctx.fillStyle = "#ffffff";
ctx.fillRect(0, 0, width, height);
await page.render({
// eslint-disable-next-line @typescript-eslint/no-explicit-any
canvasContext: ctx as any,
@@ -92,10 +90,17 @@ export async function POST(
const resultFileName = `page_${pageNum}.png`;
const resultPath = path.join(resultsDir, resultFileName);
// Pixel analysis pipeline:
// 1. flatten alpha to white (ensures no transparent areas)
// 2. grayscale for clarity
// 3. normalize stretches histogram to full 0-255 range,
// revealing subtle near-white hidden text
// 4. sharpen edges to make text crisp
await sharp(imageBuffer)
.negate({ alpha: false })
.flatten({ background: { r: 255, g: 255, b: 255 } })
.grayscale()
.normalize()
.sharpen()
.sharpen({ sigma: 3 })
.toFile(resultPath);
pages.push({
@@ -0,0 +1,109 @@
import { NextRequest, NextResponse } from "next/server";
import { getServerSession } from "next-auth";
import { authOptions } from "@/app/api/auth/[...nextauth]/route";
import { prisma } from "@/lib/prisma";
import { readFile, mkdir } from "fs/promises";
import path from "path";
/**
* Server-side PDF page renderer: renders EVERY page of the PDF as a
* clean PNG image (no processing). Used by the "Original" panel so
* all three panels display consistently via server-rendered images.
*/
export async function POST(
req: NextRequest,
{ params }: { params: Promise<{ id: string }> }
) {
const session = await getServerSession(authOptions);
if (!session?.user?.email) {
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
}
const { id } = await params;
const doc = await prisma.pdfDocument.findUnique({ where: { id } });
if (!doc) {
return NextResponse.json({ error: "Document not found" }, { status: 404 });
}
const user = await prisma.user.findUnique({
where: { email: session.user.email },
});
if (!user || doc.userId !== user.id) {
return NextResponse.json({ error: "Forbidden" }, { status: 403 });
}
const publicDir = `/results/${user.id}/${id}/original`;
const resultsDir = path.join(process.cwd(), "public", publicDir);
// Always regenerate so stale cached renders don't persist.
try {
const pdfPath = path.join(process.cwd(), "public", doc.filePath);
const pdfBuffer = await readFile(pdfPath);
const pdfjsLib = await import("pdfjs-dist/legacy/build/pdf.mjs");
const loadingTask = pdfjsLib.getDocument({
data: new Uint8Array(pdfBuffer),
});
const pdfDoc = await loadingTask.promise;
const numPages = pdfDoc.numPages;
await mkdir(resultsDir, { recursive: true });
const pages: { page: number; path: string }[] = [];
const { createCanvas } = await import("@napi-rs/canvas");
for (let pageNum = 1; pageNum <= numPages; pageNum++) {
const page = await pdfDoc.getPage(pageNum);
const scale = 2.0;
const viewport = page.getViewport({ scale });
const width = Math.floor(viewport.width);
const height = Math.floor(viewport.height);
const canvas = createCanvas(width, height);
const ctx = canvas.getContext("2d");
// Fill white background — node-canvas defaults to transparent
ctx.fillStyle = "#ffffff";
ctx.fillRect(0, 0, width, height);
await page.render({
// eslint-disable-next-line @typescript-eslint/no-explicit-any
canvasContext: ctx as any,
// eslint-disable-next-line @typescript-eslint/no-explicit-any
canvas: canvas as any,
viewport,
}).promise;
const resultFileName = `page_${pageNum}.png`;
const resultPath = path.join(resultsDir, resultFileName);
// Write the raw rendered PNG — no sharp processing
const buffer = canvas.toBuffer("image/png");
const { writeFile } = await import("fs/promises");
await writeFile(resultPath, buffer);
pages.push({
page: pageNum,
path: `${publicDir}/${resultFileName}`,
});
}
// Update pageCount if not set
if (doc.pageCount === 0) {
await prisma.pdfDocument.update({
where: { id },
data: { pageCount: numPages },
});
}
return NextResponse.json({ pages, pageCount: numPages });
} catch (e) {
console.error("Render error:", e);
return NextResponse.json(
{ error: "Page rendering failed" },
{ status: 500 }
);
}
}