Some files, and things.
This commit is contained in:
+127
-46
@@ -1,6 +1,7 @@
|
||||
import sys
|
||||
import os
|
||||
import time
|
||||
import json # Added for schema output
|
||||
import tkinter as tk
|
||||
from threading import Thread
|
||||
from queue import Queue, Empty
|
||||
@@ -35,9 +36,56 @@ if not os.path.exists(OUTPUT_DIR):
|
||||
|
||||
# --- Processing Logic ---
|
||||
|
||||
def distribute_pages(total_pages, max_per_file=5):
|
||||
"""
|
||||
Calculates how to split pages into chunks <= max_per_file,
|
||||
balancing the number of columns per file.
|
||||
Example: 12 pages, max 5 -> [4, 4, 4]
|
||||
"""
|
||||
if total_pages == 0:
|
||||
return []
|
||||
|
||||
# Calculate minimum number of files needed
|
||||
num_files = (total_pages + max_per_file - 1) // max_per_file
|
||||
|
||||
# Calculate base size and remainder
|
||||
base_count = total_pages // num_files
|
||||
remainder = total_pages % num_files
|
||||
|
||||
distribution = []
|
||||
for i in range(num_files):
|
||||
# Distribute remainder to the first few files
|
||||
count = base_count + (1 if i < remainder else 0)
|
||||
distribution.append(count)
|
||||
|
||||
return distribution
|
||||
|
||||
def stitch_images(image_list):
|
||||
"""Helper to stitch a list of images horizontally with delimiters."""
|
||||
if not image_list:
|
||||
return None
|
||||
|
||||
num_images = len(image_list)
|
||||
total_width = sum(img.width for img in image_list) + (num_images - 1) * DELIMITER_WIDTH
|
||||
max_height = max(img.height for img in image_list)
|
||||
|
||||
combined = Image.new('RGB', (total_width, max_height), color=(255, 255, 255))
|
||||
|
||||
x_offset = 0
|
||||
for idx, img in enumerate(image_list):
|
||||
combined.paste(img, (x_offset, 0))
|
||||
x_offset += img.width
|
||||
if idx < num_images - 1:
|
||||
delimiter = Image.new('RGB', (DELIMITER_WIDTH, max_height), color=DELIMITER_COLOR)
|
||||
combined.paste(delimiter, (x_offset, 0))
|
||||
x_offset += DELIMITER_WIDTH
|
||||
|
||||
return combined
|
||||
|
||||
def process_single_pdf(filename, shift_offset=0):
|
||||
"""
|
||||
Converts PDF to stitched JPG image (PIL object).
|
||||
Converts PDF to stitched images.
|
||||
Returns a tuple: (preview_image_resized, list_of_split_images, schema_dict)
|
||||
"""
|
||||
pdf_path = os.path.join(INPUT_DIR, filename)
|
||||
try:
|
||||
@@ -61,35 +109,57 @@ def process_single_pdf(filename, shift_offset=0):
|
||||
if not cropped_images:
|
||||
return None
|
||||
|
||||
# Combine
|
||||
num_images = len(cropped_images)
|
||||
total_width = sum(img.width for img in cropped_images) + (num_images - 1) * DELIMITER_WIDTH
|
||||
max_height = max(img.height for img in cropped_images)
|
||||
# 1. Generate Schema / Distribution
|
||||
col_distribution = distribute_pages(len(cropped_images), max_per_file=5)
|
||||
|
||||
combined = Image.new('RGB', (total_width, max_height), color=(255, 255, 255))
|
||||
# 2. Generate Split Images (Full Resolution)
|
||||
split_images = []
|
||||
current_idx = 0
|
||||
for count in col_distribution:
|
||||
chunk = cropped_images[current_idx : current_idx + count]
|
||||
stitched_chunk = stitch_images(chunk)
|
||||
split_images.append(stitched_chunk)
|
||||
current_idx += count
|
||||
|
||||
x_offset = 0
|
||||
for idx, img in enumerate(cropped_images):
|
||||
combined.paste(img, (x_offset, 0))
|
||||
x_offset += img.width
|
||||
if idx < num_images - 1:
|
||||
delimiter = Image.new('RGB', (DELIMITER_WIDTH, max_height), color=DELIMITER_COLOR)
|
||||
combined.paste(delimiter, (x_offset, 0))
|
||||
x_offset += DELIMITER_WIDTH
|
||||
# 3. Generate Preview (All stitched together, Resized)
|
||||
full_stitch = stitch_images(cropped_images)
|
||||
preview_resized = full_stitch.resize(OUTPUT_SIZE, Image.LANCZOS)
|
||||
|
||||
# Resize
|
||||
resized = combined.resize(OUTPUT_SIZE, Image.LANCZOS)
|
||||
return resized
|
||||
schema = {
|
||||
"original_filename": filename,
|
||||
"total_pages": len(cropped_images),
|
||||
"number_of_files": len(split_images),
|
||||
"columns_per_file": col_distribution
|
||||
}
|
||||
|
||||
return (preview_resized, split_images, schema)
|
||||
|
||||
except Exception as e:
|
||||
print(f"Error processing {filename}: {e}")
|
||||
return None
|
||||
|
||||
def save_image(pil_img, filename):
|
||||
output_filename = os.path.splitext(filename)[0] + ".jpg"
|
||||
output_path = os.path.join(OUTPUT_DIR, output_filename)
|
||||
pil_img.save(output_path, "JPEG", quality=95)
|
||||
print(f"Saved: {output_filename}")
|
||||
def save_results(result_tuple, filename):
|
||||
"""
|
||||
Saves the split images and the schema JSON.
|
||||
"""
|
||||
_, splits, schema = result_tuple
|
||||
base_name = os.path.splitext(filename)[0]
|
||||
|
||||
# Save Images
|
||||
for i, img in enumerate(splits):
|
||||
# Suffix _01, _02, etc.
|
||||
suffix = f"_{i+1:02d}"
|
||||
output_filename = f"{base_name}{suffix}.jpg"
|
||||
output_path = os.path.join(OUTPUT_DIR, output_filename)
|
||||
img.save(output_path, "JPEG", quality=95)
|
||||
print(f"Saved: {output_filename}")
|
||||
|
||||
# Save Schema
|
||||
json_filename = f"{base_name}_schema.json"
|
||||
json_path = os.path.join(OUTPUT_DIR, json_filename)
|
||||
with open(json_path, 'w') as f:
|
||||
json.dump(schema, f, indent=4)
|
||||
print(f"Saved schema: {json_filename}")
|
||||
|
||||
# --- GUI Application ---
|
||||
|
||||
@@ -98,10 +168,10 @@ class ImageReviewer:
|
||||
self.files = file_list
|
||||
self.index = 0
|
||||
self.current_shift = 0
|
||||
self.current_pil = None
|
||||
self.current_preview = None # Only stores the resized preview for GUI
|
||||
self.is_processing = False
|
||||
|
||||
# Queue for pre-fetched images (index, image)
|
||||
# Queue for pre-fetched results (index, (preview, splits, schema))
|
||||
self.prefetch_queue = Queue(maxsize=1)
|
||||
# Queue for manual re-processing results
|
||||
self.manual_queue = Queue()
|
||||
@@ -142,12 +212,11 @@ class ImageReviewer:
|
||||
if target < len(self.files):
|
||||
if idx_to_process != target:
|
||||
fname = self.files[target]
|
||||
img = process_single_pdf(fname, shift_offset=0)
|
||||
if img:
|
||||
self.prefetch_queue.put((target, img)) # Blocks if full
|
||||
result = process_single_pdf(fname, shift_offset=0)
|
||||
if result:
|
||||
self.prefetch_queue.put((target, result)) # Blocks if full
|
||||
idx_to_process = target
|
||||
|
||||
# Crucial fix: Sleep briefly to release CPU
|
||||
time.sleep(0.1)
|
||||
|
||||
def load_current_image(self, use_prefetch=False):
|
||||
@@ -159,19 +228,17 @@ class ImageReviewer:
|
||||
filename = self.files[self.index]
|
||||
self.is_processing = False
|
||||
|
||||
img_found = None
|
||||
result_found = None
|
||||
|
||||
if use_prefetch and not self.prefetch_queue.empty():
|
||||
q_idx, q_img = self.prefetch_queue.queue[0]
|
||||
q_idx, q_result = self.prefetch_queue.queue[0]
|
||||
if q_idx == self.index:
|
||||
_, img_found = self.prefetch_queue.get()
|
||||
_, result_found = self.prefetch_queue.get()
|
||||
self.current_shift = 0
|
||||
print(f"Loaded {filename} from prefetch.")
|
||||
|
||||
if img_found:
|
||||
self.current_pil = img_found
|
||||
save_image(self.current_pil, filename)
|
||||
self.update_display(filename)
|
||||
if result_found:
|
||||
self.handle_processing_result(result_found, filename)
|
||||
else:
|
||||
# Not in queue (first load or queue mismatch), process manually
|
||||
self.trigger_processing(filename, self.current_shift)
|
||||
@@ -182,8 +249,8 @@ class ImageReviewer:
|
||||
self.label_info.configure(text=f"Processing {filename} (Shift {shift})... Please wait.", fg="red")
|
||||
|
||||
def worker():
|
||||
img = process_single_pdf(filename, shift)
|
||||
self.manual_queue.put(img)
|
||||
res = process_single_pdf(filename, shift)
|
||||
self.manual_queue.put(res)
|
||||
|
||||
Thread(target=worker, daemon=True).start()
|
||||
self.check_manual_queue(filename)
|
||||
@@ -191,11 +258,9 @@ class ImageReviewer:
|
||||
def check_manual_queue(self, filename):
|
||||
"""Polls the manual queue for result."""
|
||||
try:
|
||||
img = self.manual_queue.get_nowait()
|
||||
self.current_pil = img
|
||||
if self.current_pil:
|
||||
save_image(self.current_pil, filename)
|
||||
self.update_display(filename)
|
||||
result = self.manual_queue.get_nowait()
|
||||
if result:
|
||||
self.handle_processing_result(result, filename)
|
||||
else:
|
||||
print(f"Failed to process {filename}, skipping.")
|
||||
self.index += 1
|
||||
@@ -205,13 +270,29 @@ class ImageReviewer:
|
||||
# Check again in 100ms
|
||||
self.root.after(100, lambda: self.check_manual_queue(filename))
|
||||
|
||||
def update_display(self, filename):
|
||||
if self.current_pil:
|
||||
tk_image = ImageTk.PhotoImage(self.current_pil)
|
||||
def handle_processing_result(self, result, filename):
|
||||
"""Unpacks result, saves files, and updates display."""
|
||||
preview, splits, schema = result
|
||||
self.current_preview = preview
|
||||
|
||||
# Save immediately upon loading/calculating
|
||||
save_results(result, filename)
|
||||
self.update_display(filename, schema)
|
||||
|
||||
def update_display(self, filename, schema=None):
|
||||
if self.current_preview:
|
||||
tk_image = ImageTk.PhotoImage(self.current_preview)
|
||||
self.label_img.configure(image=tk_image)
|
||||
self.label_img.image = tk_image
|
||||
|
||||
schema_info = ""
|
||||
if schema:
|
||||
cols = str(schema['columns_per_file'])
|
||||
schema_info = f"\nFiles: {schema['number_of_files']} | Cols: {cols}"
|
||||
|
||||
self.label_info.configure(
|
||||
text=f"[{self.index+1}/{len(self.files)}] {filename} | Shift: {self.current_shift}px\n"
|
||||
text=f"[{self.index+1}/{len(self.files)}] {filename} | Shift: {self.current_shift}px"
|
||||
f"{schema_info}\n"
|
||||
f"Enter: Next | n: +50 | N: +100 | t: -50",
|
||||
fg="black"
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user