Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion Readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ This project is a comprehensive full‑stack web app for doing simple, local fil
- Reorder PDF pages (files over 50 pages are rejected to avoid truncated exports)
- Convert PDF to DOCX
- Convert DOCX to PDF
- Rotate or flip PDF pages while preserving all source pages
- Rotate or flip PDF pages (including mirrored export for horizontal/vertical flips) while preserving all source pages
- Add watermarks to PDFs
- Sign PDFs

Expand Down
2 changes: 1 addition & 1 deletion backend/blueprints/dpi_converter.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import os
from flask import Blueprint, request, jsonify
from flask import Blueprint, request
from PIL import Image, ImageFile
from werkzeug.utils import secure_filename
from utils.helpers import error, send_file_and_cleanup, success
Expand Down
18 changes: 9 additions & 9 deletions backend/blueprints/image.py
Original file line number Diff line number Diff line change
Expand Up @@ -250,6 +250,13 @@ def compress_image():
if upload_error:
return upload_error

requested_format = request.form.get("format", "original").lower()
if requested_format != "original" and requested_format not in COMPRESSION_FORMATS:
return error(
"Invalid format. Please choose one of: original, jpeg, webp, png.",
400,
)

img, file_bytes, image_error = validate_image_file(file)

if image_error:
Expand All @@ -259,8 +266,6 @@ def compress_image():

quality = max(1, min(100, quality))

requested_format = request.form.get("format", "original").lower()

if requested_format == "original":
# Keep the uploaded format, falling back to JPEG for anything the
# compressor cannot re-encode.
Expand All @@ -269,13 +274,8 @@ def compress_image():
if img.format in COMPRESSION_EXTENSIONS
else "JPEG"
)
elif requested_format in COMPRESSION_FORMATS:
img_format = COMPRESSION_FORMATS[requested_format]
else:
return error(
"Invalid format. Please choose one of: original, jpeg, webp, png.",
400,
)
img_format = COMPRESSION_FORMATS[requested_format]

# JPEG has no alpha channel, so transparency is flattened onto white
# instead of turning black. WebP only accepts RGB/RGBA input.
Expand Down Expand Up @@ -393,4 +393,4 @@ def resize_image(img, filename, file_bytes):
try:
resized_img.close()
except Exception:
pass
pass
2 changes: 0 additions & 2 deletions backend/blueprints/pdf.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,8 +36,6 @@ def convert_pdf_to_png():
# Read PDF into memory and open from bytes
pdf_bytes = pdf_file.read()

target_lang = request.form.get("language", "eng")

# Extract DPI/resolution settings. Default to 72 DPI (standard screen resolution).
# DPI determines the scaling: zoom = requested_dpi / 72
# For 150 DPI: zoom = 150/72 = 2.08
Expand Down
16 changes: 9 additions & 7 deletions backend/blueprints/pdf_to_docx.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
from flask import Blueprint, request

from utils.helpers import error, send_file_and_cleanup
from utils.validators import validate_pdf_file, validate_uploaded_file

pdf_docx_bp = Blueprint("pdf_docx", __name__)

Expand All @@ -14,14 +15,15 @@
def convert_pdf_to_docx():
doc = None
try:
if "file" not in request.files:
return error("No file provided")
pdf_file, filename, upload_error = validate_uploaded_file(request, "file")
if upload_error:
return upload_error

pdf_file = request.files["file"]

if pdf_file.filename == "":
return error("No file selected")
pdf_error = validate_pdf_file(pdf_file, filename)
if pdf_error:
return pdf_error

pdf_bytes = pdf_file.read()
try:
doc = fitz.open(stream=pdf_bytes, filetype="pdf")
except Exception:
Expand Down Expand Up @@ -64,4 +66,4 @@ def convert_pdf_to_docx():
"Failed to convert the PDF to DOCX. The file may be corrupted "
"or unsupported.",
500,
)
)
2 changes: 1 addition & 1 deletion backend/blueprints/removebg.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
import threading

import numpy as np
from flask import Blueprint, jsonify, request, Response
from flask import Blueprint, jsonify, Response
from PIL import Image, ImageFilter
from rembg import remove
from skimage import morphology
Expand Down
3 changes: 1 addition & 2 deletions backend/blueprints/watermark.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@
from PIL import Image, ImageDraw, ImageFont
from utils.helpers import error
import io
import os

watermark_bp = Blueprint('watermark', __name__)

Expand Down Expand Up @@ -96,7 +95,7 @@ def create_text_watermark(text, font_size, color, opacity):

try:
color_rgb = tuple(int(color.lstrip('#')[i:i+2], 16) for i in (0, 2, 4))
except:
except (TypeError, ValueError):
color_rgb = (255, 255, 255)

alpha = int(255 * opacity / 100)
Expand Down
5 changes: 5 additions & 0 deletions backend/pyproject.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
[tool.ruff.lint]
# Keep CI on Ruff's established correctness-focused default rules. Ruff is
# installed unpinned by the workflow, so pinning the selection prevents future
# releases from unexpectedly enabling repository-wide style rule families.
select = ["E4", "E7", "E9", "F"]
1 change: 0 additions & 1 deletion backend/test_app.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import os
import io
import pytest
from flask import Flask
Expand Down
2 changes: 0 additions & 2 deletions backend/tests/test_path_traversal.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,6 @@ def test_path_traversal_filename(client):

assert "../" not in header
assert "..\\" not in header
import io

def test_nested_path_traversal(client):
response = client.post(
Expand All @@ -28,7 +27,6 @@ def test_nested_path_traversal(client):
header = response.headers.get("Content-Disposition", "")

assert "../" not in header
import io

def test_windows_path_traversal(client):
response = client.post(
Expand Down
3 changes: 0 additions & 3 deletions backend/tests/test_send_file_helper.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@ def test_normal_filename(app):
)

assert "hello.txt" in response.headers["Content-Disposition"]
import io

def test_path_traversal_removed(app):
with app.test_request_context():
Expand All @@ -23,7 +22,6 @@ def test_path_traversal_removed(app):
header = response.headers["Content-Disposition"]

assert "../" not in header
import io

def test_windows_traversal_removed(app):
with app.test_request_context():
Expand All @@ -37,7 +35,6 @@ def test_windows_traversal_removed(app):

assert "..\\" not in header

import io

def test_absolute_path_removed(app):
with app.test_request_context():
Expand Down
3 changes: 0 additions & 3 deletions backend/tests/test_upload_security_headers.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,6 @@ def test_removebg_path_traversal_filename(client):

assert "../" not in header
assert "..\\" not in header
import io

def test_removebg_nested_path(client):
response = client.post(
Expand All @@ -31,7 +30,6 @@ def test_removebg_nested_path(client):
header = response.headers.get("Content-Disposition", "")

assert "../" not in header
import io

def test_removebg_windows_path(client):
response = client.post(
Expand All @@ -48,7 +46,6 @@ def test_removebg_windows_path(client):
header = response.headers.get("Content-Disposition", "")

assert "..\\" not in header
import io

def test_removebg_absolute_path(client):
response = client.post(
Expand Down
41 changes: 10 additions & 31 deletions frontend/src/pages/PdfRotateFlip.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import { useState, useRef, useEffect } from "react";
import * as pdfjsLib from "pdfjs-dist/legacy/build/pdf";
import pdfWorker from "pdfjs-dist/legacy/build/pdf.worker.min.mjs?url";
import { PDFDocument, degrees } from "pdf-lib";
import { buildExportPages } from "../utils/pdfPageSelection";
import { Toaster, toast } from "sonner";
import { buildExportPages } from "../utils/pdfPageSelection";
import { buildTransformedPdf } from "../utils/pdfPageTransforms";
import { motion } from "framer-motion";
import {
RotateCcw,
Expand Down Expand Up @@ -199,40 +199,19 @@ const transformAndDownload = async () => {
try {
const arrayBuffer = await file.arrayBuffer();

const originalPdfDoc = await PDFDocument.load(arrayBuffer);
const newPdfDoc = await PDFDocument.create();

const exportPages = buildExportPages(
previews,
originalPdfDoc.getPageCount(),
totalPages ?? previews.length,
removedPageNumbers,
);
const total = exportPages.length;

for (let i = 0; i < exportPages.length; i++) {
const previewItem = exportPages[i];

const originalIndex = previewItem.originalPageNum - 1;

const [copiedPage] = await newPdfDoc.copyPages(
originalPdfDoc,
[originalIndex]
);

const shouldTransform =
scope === "all" ||
selectedPreviewPages.includes(previewItem.originalPageNum);

if (shouldTransform) {
copiedPage.setRotation(degrees(previewItem.currentRotation));
}

newPdfDoc.addPage(copiedPage);

setProgress(Math.round(((i + 1) / total) * 100));
}

const pdfBytes = await newPdfDoc.save();
const pdfBytes = await buildTransformedPdf(
arrayBuffer,
exportPages,
(originalPageNum) =>
scope === "all" || selectedPreviewPages.includes(originalPageNum),
setProgress,
);

const blob = new Blob([new Uint8Array(pdfBytes)], {
type: "application/pdf",
Expand Down
Loading
Loading