mirror of
https://github.com/Frooodle/Stirling-PDF.git
synced 2026-02-17 13:52:14 +01:00
fix: correct paths for python scripts and implement classpath extraction (#3984)
# Description of Changes - **What was changed** - Relocated `png_to_webp.py` and `split_photos.py` from `scripts/` to `app/core/src/main/resources/static/python/`. - Updated `.github/labeler-config-srvaroa.yml` and `.pre-commit-config.yaml` to include the new script directory in their file-matching patterns. - Added `GeneralUtils.extractScript(String scriptName)` to load Python scripts from the classpath (`static/python/`), extract them into a temporary directory at runtime, and return the filesystem path. - **Why the change was made** - To fix the Internal Server Error caused by missing script files at their old locations. - Ensure the Python helper scripts are packaged inside the JAR/WAR and reliably accessible when the application runs. - Only local installations were affected --- ## Checklist ### General - [x] I have read the [Contribution Guidelines](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/CONTRIBUTING.md) - [x] I have read the [Stirling-PDF Developer Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/DeveloperGuide.md) (if applicable) - [ ] I have read the [How to add new languages to Stirling-PDF](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md) (if applicable) - [x] I have performed a self-review of my own code - [x] My changes generate no new warnings ### Documentation - [ ] I have updated relevant docs on [Stirling-PDF's doc repo](https://github.com/Stirling-Tools/Stirling-Tools.github.io/blob/main/docs/) (if functionality has heavily changed) - [ ] I have read the section [Add New Translation Tags](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md#add-new-translation-tags) (for new translation tags only) ### UI Changes (if applicable) - [ ] Screenshots or videos demonstrating the UI changes are attached (e.g., as comments or direct attachments in the PR) ### Testing (if applicable) - [ ] I have tested my changes locally. Refer to the [Testing Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/DeveloperGuide.md#6-testing) for more details. --------- Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
This commit is contained in:
2
app/core/.gitignore
vendored
2
app/core/.gitignore
vendored
@@ -194,3 +194,5 @@ id_ed25519.pub
|
||||
|
||||
# node_modules
|
||||
node_modules/
|
||||
|
||||
scripts/**/*
|
||||
|
||||
@@ -56,8 +56,8 @@ public class ConvertImgPDFController {
|
||||
summary = "Convert PDF to image(s)",
|
||||
description =
|
||||
"This endpoint converts a PDF file to image(s) with the specified image format,"
|
||||
+ " color type, and DPI. Users can choose to get a single image or multiple"
|
||||
+ " images. Input:PDF Output:Image Type:SI-Conditional")
|
||||
+ " color type, and DPI. Users can choose to get a single image or multiple"
|
||||
+ " images. Input:PDF Output:Image Type:SI-Conditional")
|
||||
public ResponseEntity<byte[]> convertToImage(@ModelAttribute ConvertToImageRequest request)
|
||||
throws Exception {
|
||||
MultipartFile file = request.getFileInput();
|
||||
@@ -117,10 +117,14 @@ public class ConvertImgPDFController {
|
||||
}
|
||||
|
||||
String pythonVersion = CheckProgramInstall.getAvailablePythonCommand();
|
||||
Path pngToWebpScript = GeneralUtils.extractScript("png_to_webp.py");
|
||||
|
||||
List<String> command = new ArrayList<>();
|
||||
command.add(pythonVersion);
|
||||
command.add("./scripts/png_to_webp.py"); // Python script to handle the conversion
|
||||
command.add(
|
||||
pngToWebpScript
|
||||
.toAbsolutePath()
|
||||
.toString()); // Python script to handle the conversion
|
||||
|
||||
// Create a temporary directory for the output WebP files
|
||||
tempOutputDir = Files.createTempDirectory("webp_output");
|
||||
@@ -232,7 +236,8 @@ public class ConvertImgPDFController {
|
||||
PdfUtils.imageToPdf(file, fitOption, autoRotate, colorType, pdfDocumentFactory);
|
||||
return WebResponseUtils.bytesToWebResponse(
|
||||
bytes,
|
||||
new File(file[0].getOriginalFilename()).getName().replaceFirst("[.][^.]+$", "") + "_converted.pdf");
|
||||
new File(file[0].getOriginalFilename()).getName().replaceFirst("[.][^.]+$", "")
|
||||
+ "_converted.pdf");
|
||||
}
|
||||
|
||||
private String getMediaType(String imageFormat) {
|
||||
|
||||
@@ -34,6 +34,7 @@ import stirling.software.SPDF.model.api.misc.ExtractImageScansRequest;
|
||||
import stirling.software.common.service.CustomPDFDocumentFactory;
|
||||
import stirling.software.common.util.CheckProgramInstall;
|
||||
import stirling.software.common.util.ExceptionUtils;
|
||||
import stirling.software.common.util.GeneralUtils;
|
||||
import stirling.software.common.util.ProcessExecutor;
|
||||
import stirling.software.common.util.ProcessExecutor.ProcessExecutorResult;
|
||||
import stirling.software.common.util.WebResponseUtils;
|
||||
@@ -54,9 +55,9 @@ public class ExtractImageScansController {
|
||||
summary = "Extract image scans from an input file",
|
||||
description =
|
||||
"This endpoint extracts image scans from a given file based on certain"
|
||||
+ " parameters. Users can specify angle threshold, tolerance, minimum area,"
|
||||
+ " minimum contour area, and border size. Input:PDF Output:IMAGE/ZIP"
|
||||
+ " Type:SIMO")
|
||||
+ " parameters. Users can specify angle threshold, tolerance, minimum area,"
|
||||
+ " minimum contour area, and border size. Input:PDF Output:IMAGE/ZIP"
|
||||
+ " Type:SIMO")
|
||||
public ResponseEntity<byte[]> extractImageScans(
|
||||
@ModelAttribute ExtractImageScansRequest request)
|
||||
throws IOException, InterruptedException {
|
||||
@@ -78,6 +79,7 @@ public class ExtractImageScansController {
|
||||
}
|
||||
|
||||
String pythonVersion = CheckProgramInstall.getAvailablePythonCommand();
|
||||
Path splitPhotosScript = GeneralUtils.extractScript("split_photos.py");
|
||||
try {
|
||||
// Check if input file is a PDF
|
||||
if ("pdf".equalsIgnoreCase(extension)) {
|
||||
@@ -120,7 +122,7 @@ public class ExtractImageScansController {
|
||||
new ArrayList<>(
|
||||
Arrays.asList(
|
||||
pythonVersion,
|
||||
"./scripts/split_photos.py",
|
||||
splitPhotosScript.toAbsolutePath().toString(),
|
||||
images.get(i),
|
||||
tempDir.toString(),
|
||||
"--angle_threshold",
|
||||
|
||||
174
app/core/src/main/resources/static/python/png_to_webp.py
Normal file
174
app/core/src/main/resources/static/python/png_to_webp.py
Normal file
@@ -0,0 +1,174 @@
|
||||
"""
|
||||
Author: Ludy87
|
||||
Description: This script converts a PDF file to WebP images. It includes functionality to resize images if they exceed specified dimensions and handle conversion of PDF pages to WebP format.
|
||||
|
||||
Example
|
||||
-------
|
||||
To convert a PDF file to WebP images with each page as a separate WebP file:
|
||||
python script.py input.pdf output_directory
|
||||
|
||||
To convert a PDF file to a single WebP image:
|
||||
python script.py input.pdf output_directory --single
|
||||
|
||||
To adjust the DPI resolution for rendering PDF pages:
|
||||
python script.py input.pdf output_directory --dpi 150
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import os
|
||||
from pdf2image import convert_from_path
|
||||
from PIL import Image
|
||||
|
||||
|
||||
def resize_image(input_image_path, output_image_path, max_size=(16383, 16383)):
|
||||
"""
|
||||
Resize the image if its dimensions exceed the maximum allowed size and save it as WebP.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
input_image_path : str
|
||||
Path to the input image file.
|
||||
output_image_path : str
|
||||
Path where the output WebP image will be saved.
|
||||
max_size : tuple of int, optional
|
||||
Maximum allowed dimensions for the image (width, height). Default is (16383, 16383).
|
||||
|
||||
Returns
|
||||
-------
|
||||
None
|
||||
"""
|
||||
try:
|
||||
# Open the image
|
||||
image = Image.open(input_image_path)
|
||||
width, height = image.size
|
||||
max_width, max_height = max_size
|
||||
|
||||
# Check if the image dimensions exceed the maximum allowed dimensions
|
||||
if width > max_width or height > max_height:
|
||||
# Calculate the scaling ratio
|
||||
ratio = min(max_width / width, max_height / height)
|
||||
new_width = int(width * ratio)
|
||||
new_height = int(height * ratio)
|
||||
|
||||
# Resize the image
|
||||
resized_image = image.resize((new_width, new_height), Image.LANCZOS)
|
||||
resized_image.save(output_image_path, format="WEBP", quality=100)
|
||||
print(
|
||||
f"The image was successfully resized to ({new_width}, {new_height}) and saved as WebP: {output_image_path}"
|
||||
)
|
||||
else:
|
||||
# If dimensions are within the allowed limits, save the image directly
|
||||
image.save(output_image_path, format="WEBP", quality=100)
|
||||
print(f"The image was successfully saved as WebP: {output_image_path}")
|
||||
except Exception as e:
|
||||
print(f"An error occurred: {e}")
|
||||
|
||||
|
||||
def convert_image_to_webp(input_image, output_file):
|
||||
"""
|
||||
Convert an image to WebP format, resizing it if it exceeds the maximum dimensions.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
input_image : str
|
||||
Path to the input image file.
|
||||
output_file : str
|
||||
Path where the output WebP image will be saved.
|
||||
|
||||
Returns
|
||||
-------
|
||||
None
|
||||
"""
|
||||
# Resize the image if it exceeds the maximum dimensions
|
||||
resize_image(input_image, output_file, max_size=(16383, 16383))
|
||||
|
||||
|
||||
def pdf_to_webp(pdf_path, output_dir, dpi=300):
|
||||
"""
|
||||
Convert each page of a PDF file to WebP images.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
pdf_path : str
|
||||
Path to the input PDF file.
|
||||
output_dir : str
|
||||
Directory where the WebP images will be saved.
|
||||
dpi : int, optional
|
||||
DPI resolution for rendering PDF pages. Default is 300.
|
||||
|
||||
Returns
|
||||
-------
|
||||
None
|
||||
"""
|
||||
# Convert the PDF to a list of images
|
||||
images = convert_from_path(pdf_path, dpi=dpi)
|
||||
|
||||
for page_number, image in enumerate(images):
|
||||
# Define temporary PNG path
|
||||
temp_png_path = os.path.join(output_dir, f"temp_page_{page_number + 1}.png")
|
||||
image.save(temp_png_path, format="PNG")
|
||||
|
||||
# Define the output path for WebP
|
||||
output_path = os.path.join(output_dir, f"page_{page_number + 1}.webp")
|
||||
|
||||
# Convert PNG to WebP
|
||||
convert_image_to_webp(temp_png_path, output_path)
|
||||
|
||||
# Delete the temporary PNG file
|
||||
os.remove(temp_png_path)
|
||||
|
||||
|
||||
def main(pdf_image_path, output_dir, dpi=300, single_images_flag=False):
|
||||
"""
|
||||
Main function to handle conversion from PDF to WebP images.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
pdf_image_path : str
|
||||
Path to the input PDF file or image.
|
||||
output_dir : str
|
||||
Directory where the WebP images will be saved.
|
||||
dpi : int, optional
|
||||
DPI resolution for rendering PDF pages. Default is 300.
|
||||
single_images_flag : bool, optional
|
||||
If True, combine all pages into a single WebP image. Default is False.
|
||||
|
||||
Returns
|
||||
-------
|
||||
None
|
||||
"""
|
||||
if single_images_flag:
|
||||
# Combine all pages into a single WebP image
|
||||
output_path = os.path.join(output_dir, "combined_image.webp")
|
||||
convert_image_to_webp(pdf_image_path, output_path)
|
||||
else:
|
||||
# Convert each PDF page to a separate WebP image
|
||||
pdf_to_webp(pdf_image_path, output_dir, dpi)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
parser = argparse.ArgumentParser(description="Convert a PDF file to WebP images.")
|
||||
parser.add_argument("pdf_path", help="The path to the input PDF file.")
|
||||
parser.add_argument(
|
||||
"output_dir", help="The directory where the WebP images should be saved."
|
||||
)
|
||||
parser.add_argument(
|
||||
"--dpi",
|
||||
type=int,
|
||||
default=300,
|
||||
help="The DPI resolution for rendering the PDF pages (default: 300).",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--single",
|
||||
action="store_true",
|
||||
help="Combine all pages into a single WebP image.",
|
||||
)
|
||||
args = parser.parse_args()
|
||||
|
||||
os.makedirs(args.output_dir, exist_ok=True)
|
||||
main(
|
||||
args.pdf_path,
|
||||
args.output_dir,
|
||||
dpi=args.dpi,
|
||||
single_images_flag=args.single,
|
||||
)
|
||||
122
app/core/src/main/resources/static/python/split_photos.py
Normal file
122
app/core/src/main/resources/static/python/split_photos.py
Normal file
@@ -0,0 +1,122 @@
|
||||
import argparse
|
||||
import sys
|
||||
import cv2
|
||||
import numpy as np
|
||||
import os
|
||||
|
||||
def find_photo_boundaries(image, background_color, tolerance=30, min_area=10000, min_contour_area=500):
|
||||
mask = cv2.inRange(image, background_color - tolerance, background_color + tolerance)
|
||||
mask = cv2.bitwise_not(mask)
|
||||
kernel = np.ones((5,5),np.uint8)
|
||||
mask = cv2.dilate(mask, kernel, iterations=2)
|
||||
contours, _ = cv2.findContours(mask, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
|
||||
|
||||
photo_boundaries = []
|
||||
for contour in contours:
|
||||
x, y, w, h = cv2.boundingRect(contour)
|
||||
area = w * h
|
||||
contour_area = cv2.contourArea(contour)
|
||||
if area >= min_area and contour_area >= min_contour_area:
|
||||
photo_boundaries.append((x, y, w, h))
|
||||
|
||||
return photo_boundaries
|
||||
|
||||
def estimate_background_color(image, sample_points=5):
|
||||
h, w, _ = image.shape
|
||||
points = [
|
||||
(0, 0),
|
||||
(w - 1, 0),
|
||||
(w - 1, h - 1),
|
||||
(0, h - 1),
|
||||
(w // 2, h // 2),
|
||||
]
|
||||
|
||||
colors = []
|
||||
for x, y in points:
|
||||
colors.append(image[y, x])
|
||||
|
||||
return np.median(colors, axis=0)
|
||||
|
||||
def auto_rotate(image, angle_threshold=1):
|
||||
gray = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)
|
||||
edges = cv2.Canny(gray, 50, 150, apertureSize=3)
|
||||
lines = cv2.HoughLines(edges, 1, np.pi / 180, 200)
|
||||
|
||||
if lines is None:
|
||||
return image
|
||||
|
||||
# compute the median angle of the lines
|
||||
angles = []
|
||||
for rho, theta in lines[:, 0]:
|
||||
angles.append((theta * 180) / np.pi - 90)
|
||||
|
||||
angle = np.median(angles)
|
||||
|
||||
if abs(angle) < angle_threshold:
|
||||
return image
|
||||
|
||||
(h, w) = image.shape[:2]
|
||||
center = (w // 2, h // 2)
|
||||
M = cv2.getRotationMatrix2D(center, angle, 1.0)
|
||||
return cv2.warpAffine(image, M, (w, h), flags=cv2.INTER_CUBIC, borderMode=cv2.BORDER_REPLICATE)
|
||||
|
||||
|
||||
|
||||
|
||||
def crop_borders(image, border_color, tolerance=30):
|
||||
mask = cv2.inRange(image, border_color - tolerance, border_color + tolerance)
|
||||
|
||||
contours, _ = cv2.findContours(mask, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
|
||||
if len(contours) == 0:
|
||||
return image
|
||||
|
||||
largest_contour = max(contours, key=cv2.contourArea)
|
||||
x, y, w, h = cv2.boundingRect(largest_contour)
|
||||
|
||||
return image[y:y+h, x:x+w]
|
||||
|
||||
def split_photos(input_file, output_directory, tolerance=30, min_area=10000, min_contour_area=500, angle_threshold=10, border_size=0):
|
||||
image = cv2.imread(input_file)
|
||||
background_color = estimate_background_color(image)
|
||||
|
||||
# Add a constant border around the image
|
||||
image = cv2.copyMakeBorder(image, border_size, border_size, border_size, border_size, cv2.BORDER_CONSTANT, value=background_color)
|
||||
|
||||
photo_boundaries = find_photo_boundaries(image, background_color, tolerance)
|
||||
|
||||
if not os.path.exists(output_directory):
|
||||
os.makedirs(output_directory)
|
||||
|
||||
# Get the input file's base name without the extension
|
||||
input_file_basename = os.path.splitext(os.path.basename(input_file))[0]
|
||||
|
||||
for idx, (x, y, w, h) in enumerate(photo_boundaries):
|
||||
cropped_image = image[y:y+h, x:x+w]
|
||||
cropped_image = auto_rotate(cropped_image, angle_threshold)
|
||||
|
||||
# Remove the added border, but ensure we don't create an empty image
|
||||
if border_size > 0 and cropped_image.shape[0] > 2 * border_size and cropped_image.shape[1] > 2 * border_size:
|
||||
cropped_image = cropped_image[border_size:-border_size, border_size:-border_size]
|
||||
|
||||
# Check if the cropped image is valid before saving
|
||||
if cropped_image.size == 0 or cropped_image.shape[0] == 0 or cropped_image.shape[1] == 0:
|
||||
print(f"Warning: Skipping empty image for region {idx+1}")
|
||||
continue
|
||||
|
||||
output_path = os.path.join(output_directory, f"{input_file_basename}_{idx+1}.png")
|
||||
cv2.imwrite(output_path, cropped_image)
|
||||
print(f"Saved {output_path}")
|
||||
|
||||
if __name__ == "__main__":
|
||||
parser = argparse.ArgumentParser(description="Split photos in an image")
|
||||
parser.add_argument("input_file", help="The input scanned image containing multiple photos.")
|
||||
parser.add_argument("output_directory", help="The directory where the result images should be placed.")
|
||||
parser.add_argument("--tolerance", type=int, default=30, help="Determines the range of color variation around the estimated background color (default: 30).")
|
||||
parser.add_argument("--min_area", type=int, default=10000, help="Sets the minimum area threshold for a photo (default: 10000).")
|
||||
parser.add_argument("--min_contour_area", type=int, default=500, help="Sets the minimum contour area threshold for a photo (default: 500).")
|
||||
parser.add_argument("--angle_threshold", type=int, default=10, help="Sets the minimum absolute angle required for the image to be rotated (default: 10).")
|
||||
parser.add_argument("--border_size", type=int, default=0, help="Sets the size of the border added and removed to prevent white borders in the output (default: 0).")
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
split_photos(args.input_file, args.output_directory, tolerance=args.tolerance, min_area=args.min_area, min_contour_area=args.min_contour_area, angle_threshold=args.angle_threshold, border_size=args.border_size)
|
||||
Reference in New Issue
Block a user