276 lines · 8.8 KB
Raw Download
1
import tkinter as tk
2
from tkinter import messagebox
3
from PIL import Image, ImageTk
4
import requests
5
import pdf2image
6
import io
7
import csv
8
import threading
9
import queue
10
import time
11
import os
12
13
# --- Configuration ---
14
INPUT_FILE = "pdf-urls.txt"
15
OUTPUT_CSV = "pdf-urls-results.csv"
16
MAX_DIMENSION = 500
17
ANIMATION_DURATION_MS = 2000  # 2 seconds total loop
18
MAX_PAGES = 10
19
PRELOAD_BUFFER = 10  # How many PDFs to process in advance
20
21
22
class PDFProcessor(threading.Thread):
23
    """Background thread to download and process PDFs."""
24
25
    def __init__(self, url_queue, result_queue):
26
        super().__init__()
27
        self.url_queue = url_queue
28
        self.result_queue = result_queue
29
        self.daemon = True  # Kill thread when main app closes
30
31
    def run(self):
32
        while True:
33
            try:
34
                url = self.url_queue.get()
35
                if url is None:
36
                    break  # Sentinel to stop
37
38
                print(f"Processing: {url}")
39
40
                # 1. Download PDF
41
                try:
42
                    response = requests.get(url, timeout=10)
43
                    response.raise_for_status()
44
                    pdf_bytes = response.content
45
                except Exception as e:
46
                    print(f"Error downloading {url}: {e}")
47
                    self.result_queue.put(("error", url, str(e)))
48
                    continue
49
50
                # 2. Convert to Images (First 10 pages)
51
                try:
52
                    # poppler_path=r'C:\Program Files\poppler-xx\bin' # UNCOMMENT AND SET IF WINDOWS PATH ISSUES
53
                    images = pdf2image.convert_from_bytes(
54
                        pdf_bytes, first_page=1, last_page=MAX_PAGES, fmt="jpeg"
55
                    )
56
                except Exception as e:
57
                    print(f"Error converting PDF {url}: {e}")
58
                    self.result_queue.put(("error", url, "PDF Conversion Failed"))
59
                    continue
60
61
                # 3. Resize Images (Thumbnailing)
62
                processed_frames = []
63
                for img in images:
64
                    img.thumbnail((MAX_DIMENSION, MAX_DIMENSION))
65
                    processed_frames.append(img)
66
67
                # 4. Put ready data into queue
68
                self.result_queue.put(("success", url, processed_frames))
69
70
            except Exception as e:
71
                print(f"Unexpected error: {e}")
72
73
74
class TriageApp:
75
    def __init__(self, root):
76
        self.root = root
77
        self.root.title("PDF Triage Tool")
78
        self.root.geometry("600x700")
79
80
        # Data Structures
81
        self.url_queue = queue.Queue()
82
        self.ready_queue = queue.Queue(maxsize=PRELOAD_BUFFER)
83
84
        self.current_url = None
85
        self.current_frames = []
86
        self.animation_running = False
87
        self.frame_index = 0
88
89
        # Load URLs
90
        self.load_urls()
91
92
        # GUI Components
93
        self.setup_ui()
94
95
        # Start Worker Thread
96
        self.worker = PDFProcessor(self.url_queue, self.ready_queue)
97
        self.worker.start()
98
99
        # Start Polling for Content
100
        self.check_queue()
101
102
    def load_urls(self):
103
        try:
104
            with open(INPUT_FILE, "r") as f:
105
                urls = [line.strip() for line in f if line.strip()]
106
107
            # Check which are already done
108
            done_urls = set()
109
            if os.path.exists(OUTPUT_CSV):
110
                with open(OUTPUT_CSV, "r") as f:
111
                    reader = csv.reader(f)
112
                    for row in reader:
113
                        if row:
114
                            done_urls.add(row[0])
115
116
            count = 0
117
            for u in urls:
118
                if u not in done_urls:
119
                    self.url_queue.put(u)
120
                    count += 1
121
122
            print(f"Queued {count} URLs for processing.")
123
124
        except FileNotFoundError:
125
            messagebox.showerror("Error", f"Could not find {INPUT_FILE}")
126
127
    def setup_ui(self):
128
        # 1. Info Frame
129
        self.info_frame = tk.Frame(self.root, pady=10)
130
        self.info_frame.pack(fill=tk.X)
131
132
        self.lbl_url = tk.Label(
133
            self.info_frame,
134
            text="Waiting for worker...",
135
            wraplength=550,
136
            font=("Arial", 10),
137
        )
138
        self.lbl_url.pack()
139
140
        # 2. Image Display Area
141
        self.img_container = tk.Frame(self.root, width=500, height=500, bg="#e0e0e0")
142
        self.img_container.pack(pady=10)
143
        self.img_container.pack_propagate(False)  # Don't shrink
144
145
        self.lbl_image = tk.Label(self.img_container, bg="#e0e0e0")
146
        self.lbl_image.pack(expand=True)
147
148
        # 3. Controls Frame
149
        self.btn_frame = tk.Frame(self.root, pady=20)
150
        self.btn_frame.pack(fill=tk.X)
151
        self.btn_frame.columnconfigure(0, weight=1)
152
        self.btn_frame.columnconfigure(1, weight=1)
153
        self.btn_frame.columnconfigure(2, weight=1)
154
155
        # Buttons
156
        self.btn_del = tk.Button(
157
            self.btn_frame,
158
            text="Delete (D)",
159
            bg="#ffcccc",
160
            fg="red",
161
            command=lambda: self.vote("Delete"),
162
            height=2,
163
        )
164
        self.btn_del.grid(row=0, column=0, sticky="ew", padx=5)
165
166
        self.btn_web = tk.Button(
167
            self.btn_frame,
168
            text="Webize (J)",
169
            bg="#ccffcc",
170
            fg="green",
171
            command=lambda: self.vote("Webize"),
172
            height=2,
173
        )
174
        self.btn_web.grid(row=0, column=1, sticky="ew", padx=5)
175
176
        self.btn_fix = tk.Button(
177
            self.btn_frame,
178
            text="Fix (F)",
179
            bg="#ffeebb",
180
            fg="#cc6600",
181
            command=lambda: self.vote("Fix"),
182
            height=2,
183
        )
184
        self.btn_fix.grid(row=0, column=2, sticky="ew", padx=5)
185
186
        # Keyboard Shortcuts
187
        self.root.bind("<d>", lambda e: self.vote("Delete"))
188
        self.root.bind("<j>", lambda e: self.vote("Webize"))
189
        self.root.bind("<f>", lambda e: self.vote("Fix"))
190
191
    def check_queue(self):
192
        """Polls the ready queue to see if the next PDF is ready."""
193
        if self.current_url is None:
194
            try:
195
                # Non-blocking get
196
                status, url, data = self.ready_queue.get_nowait()
197
198
                if status == "error":
199
                    # Skip errors, log them, maybe save to CSV as 'Error', and recurse
200
                    self.log_to_csv(url, f"Error: {data}")
201
                    self.check_queue()
202
                else:
203
                    self.load_new_content(url, data)
204
            except queue.Empty:
205
                if self.url_queue.empty() and self.ready_queue.empty():
206
                    self.lbl_url.config(text="All Done! No more URLs.")
207
                    self.lbl_image.config(image="", text="Done")
208
                else:
209
                    self.lbl_url.config(text="Loading next PDF...")
210
                    self.root.after(500, self.check_queue)
211
        else:
212
            # We have content, no need to check queue
213
            pass
214
215
    def load_new_content(self, url, frames):
216
        self.current_url = url
217
        self.current_frames = frames
218
        self.lbl_url.config(text=url)
219
        self.frame_index = 0
220
221
        if frames:
222
            # Calculate speed: 2000ms / number of frames
223
            self.delay = int(ANIMATION_DURATION_MS / len(frames))
224
            self.animation_running = True
225
            self.animate()
226
        else:
227
            self.lbl_image.config(text="Empty PDF or No Images")
228
229
    def animate(self):
230
        if not self.animation_running or not self.current_frames:
231
            return
232
233
        # Prepare image for Tkinter
234
        pil_img = self.current_frames[self.frame_index]
235
        tk_img = ImageTk.PhotoImage(pil_img)
236
237
        # Update Label
238
        self.lbl_image.configure(image=tk_img)
239
        self.lbl_image.image = tk_img  # Keep reference to prevent GC
240
241
        # Increment index
242
        self.frame_index = (self.frame_index + 1) % len(self.current_frames)
243
244
        # Schedule next frame
245
        self.root.after(self.delay, self.animate)
246
247
    def vote(self, choice):
248
        if not self.current_url:
249
            return
250
251
        # Save Result
252
        self.log_to_csv(self.current_url, choice)
253
254
        # Reset State
255
        self.animation_running = False
256
        self.current_url = None
257
        self.current_frames = []
258
        self.lbl_image.config(image="")
259
260
        # Get next
261
        self.check_queue()
262
263
    def log_to_csv(self, url, choice):
264
        try:
265
            with open(OUTPUT_CSV, "a", newline="") as f:
266
                writer = csv.writer(f)
267
                writer.writerow([url, choice])
268
        except Exception as e:
269
            messagebox.showerror("Error", f"Could not save to CSV: {e}")
270
271
272
if __name__ == "__main__":
273
    root = tk.Tk()
274
    app = TriageApp(root)
275
    root.mainloop()
276