pdflinkcheck 1.1.7__py3-none-any.whl → 1.1.72__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- pdflinkcheck/__init__.py +69 -0
- pdflinkcheck/analyze_pymupdf.py +338 -0
- pdflinkcheck/analyze_pypdf.py +184 -0
- pdflinkcheck/analyze_pypdf_v2.py +218 -0
- pdflinkcheck/cli.py +303 -27
- pdflinkcheck/data/LICENSE +661 -0
- pdflinkcheck/data/README.md +278 -0
- pdflinkcheck/data/pyproject.toml +98 -0
- pdflinkcheck/datacopy.py +60 -0
- pdflinkcheck/dev.py +109 -0
- pdflinkcheck/gui.py +477 -52
- pdflinkcheck/io.py +213 -0
- pdflinkcheck/report.py +280 -0
- pdflinkcheck/stdlib_server.py +176 -0
- pdflinkcheck/validate.py +380 -0
- pdflinkcheck/version_info.py +83 -0
- pdflinkcheck-1.1.72.dist-info/METADATA +322 -0
- pdflinkcheck-1.1.72.dist-info/RECORD +21 -0
- pdflinkcheck-1.1.72.dist-info/WHEEL +4 -0
- {pdflinkcheck-1.1.7.dist-info → pdflinkcheck-1.1.72.dist-info}/entry_points.txt +1 -1
- pdflinkcheck-1.1.72.dist-info/licenses/LICENSE +661 -0
- pdflinkcheck/analyze.py +0 -330
- pdflinkcheck/remnants.py +0 -142
- pdflinkcheck-1.1.7.dist-info/METADATA +0 -109
- pdflinkcheck-1.1.7.dist-info/RECORD +0 -10
- pdflinkcheck-1.1.7.dist-info/WHEEL +0 -5
- pdflinkcheck-1.1.7.dist-info/top_level.txt +0 -1
pdflinkcheck/gui.py
CHANGED
|
@@ -1,11 +1,26 @@
|
|
|
1
1
|
# src/pdflinkcheck/gui.py
|
|
2
2
|
import tkinter as tk
|
|
3
|
-
from tkinter import filedialog, ttk
|
|
3
|
+
from tkinter import filedialog, ttk, messagebox # Added messagebox
|
|
4
4
|
import sys
|
|
5
5
|
from pathlib import Path
|
|
6
|
-
|
|
6
|
+
from typing import Optional # Added Optional
|
|
7
|
+
import unicodedata
|
|
8
|
+
from importlib.resources import files
|
|
9
|
+
import pyhabitat
|
|
10
|
+
"""
|
|
11
|
+
try:
|
|
12
|
+
import sv_ttk
|
|
13
|
+
# Apply Sun Valley Tk theme
|
|
14
|
+
sv_ttk.set_theme("light")
|
|
15
|
+
except Exception:
|
|
16
|
+
# Theme not available in bundle — use default
|
|
17
|
+
pass
|
|
18
|
+
"""
|
|
7
19
|
# Import the core analysis function
|
|
8
|
-
from pdflinkcheck.
|
|
20
|
+
from pdflinkcheck.report import run_report
|
|
21
|
+
from pdflinkcheck.validate import run_validation
|
|
22
|
+
from pdflinkcheck.version_info import get_version_from_pyproject
|
|
23
|
+
from pdflinkcheck.io import get_first_pdf_in_cwd, get_friendly_path, PDFLINKCHECK_HOME
|
|
9
24
|
|
|
10
25
|
class RedirectText:
|
|
11
26
|
"""A class to redirect sys.stdout messages to a Tkinter Text widget."""
|
|
@@ -16,85 +31,327 @@ class RedirectText:
|
|
|
16
31
|
"""Insert the incoming string into the Text widget."""
|
|
17
32
|
self.text_widget.insert(tk.END, string)
|
|
18
33
|
self.text_widget.see(tk.END) # Scroll to the end
|
|
19
|
-
self.text_widget.update_idletasks() # Refresh GUI
|
|
34
|
+
self.text_widget.update_idletasks() # Refresh GUI to allow real timie updates << If suppress: The mainloop will handle updates efficiently without forcing them, , but info appears outdated when a new file is analyzed. Immediate feedback is better.
|
|
20
35
|
|
|
21
|
-
def flush(self):
|
|
36
|
+
def flush(self, *args):
|
|
22
37
|
"""Required for file-like objects, but does nothing here."""
|
|
23
38
|
pass
|
|
24
39
|
|
|
25
40
|
class PDFLinkCheckerApp(tk.Tk):
|
|
26
41
|
def __init__(self):
|
|
27
42
|
super().__init__()
|
|
28
|
-
self.title("PDF Link
|
|
43
|
+
self.title(f"PDF Link Check v{get_version_from_pyproject()}")
|
|
29
44
|
self.geometry("800x600")
|
|
30
45
|
|
|
31
46
|
# Style for the application
|
|
32
47
|
style = ttk.Style(self)
|
|
33
48
|
style.theme_use('clam')
|
|
34
49
|
|
|
50
|
+
# --- 1. Initialize Variables ---
|
|
35
51
|
self.pdf_path = tk.StringVar(value="")
|
|
36
|
-
self.
|
|
52
|
+
self.pdf_library_var = tk.StringVar(value="PyMuPDF")
|
|
53
|
+
#self.pdf_library_var.set("PyMuPDF")
|
|
37
54
|
self.max_links_var = tk.StringVar(value="50")
|
|
38
|
-
self.show_all_links_var = tk.BooleanVar(value=
|
|
55
|
+
self.show_all_links_var = tk.BooleanVar(value=True)
|
|
56
|
+
self.do_export_report_json_var = tk.BooleanVar(value=True)
|
|
57
|
+
self.do_export_report_txt_var = tk.BooleanVar(value=False)
|
|
58
|
+
self.current_report_text = None
|
|
59
|
+
self.current_report_data = None
|
|
60
|
+
|
|
61
|
+
self.supported_export_formats = ["JSON", "MD", "TXT"]
|
|
62
|
+
self.supported_export_formats = ["JSON"]
|
|
39
63
|
|
|
64
|
+
|
|
65
|
+
# --- 2. Create Widgets ---
|
|
40
66
|
self._create_widgets()
|
|
67
|
+
|
|
68
|
+
# --- 3. Set Initial Dependent Widget States ---
|
|
69
|
+
self._toggle_max_links_entry()
|
|
70
|
+
self._toggle_json_export()
|
|
71
|
+
self._toggle_txt_export()
|
|
72
|
+
|
|
73
|
+
# In class PDFLinkCheckerApp:
|
|
74
|
+
|
|
75
|
+
def _copy_pdf_path(self):
|
|
76
|
+
"""Copies the current PDF path from the Entry widget to the system clipboard."""
|
|
77
|
+
path_to_copy = self.pdf_path.get()
|
|
78
|
+
|
|
79
|
+
if path_to_copy:
|
|
80
|
+
try:
|
|
81
|
+
# Clear the clipboard
|
|
82
|
+
self.clipboard_clear()
|
|
83
|
+
# Append the path string to the clipboard
|
|
84
|
+
self.clipboard_append(path_to_copy)
|
|
85
|
+
# Notify the user (optional, but good UX)
|
|
86
|
+
messagebox.showinfo("Copied", "PDF Path copied to clipboard.")
|
|
87
|
+
except tk.TclError as e:
|
|
88
|
+
# Handle cases where clipboard access might be blocked
|
|
89
|
+
messagebox.showerror("Copy Error", f"Failed to access the system clipboard: {e}")
|
|
90
|
+
else:
|
|
91
|
+
messagebox.showwarning("Copy Failed", "The PDF Path field is empty.")
|
|
92
|
+
|
|
93
|
+
def _scroll_to_top(self):
|
|
94
|
+
"""Scrolls the output text widget to the top."""
|
|
95
|
+
self.output_text.see('1.0') # '1.0' is the index for the very first character
|
|
96
|
+
|
|
97
|
+
def _scroll_to_bottom(self):
|
|
98
|
+
"""Scrolls the output text widget to the bottom."""
|
|
99
|
+
self.output_text.see(tk.END) # tk.END is the index for the position just after the last character
|
|
100
|
+
|
|
101
|
+
def _show_license(self):
|
|
102
|
+
"""
|
|
103
|
+
Reads the embedded LICENSE file (AGPLv3) and displays its content in a new modal window.
|
|
104
|
+
"""
|
|
105
|
+
try:
|
|
106
|
+
# CORRECT WAY: Use the Traversable object's read_text() method.
|
|
107
|
+
# This handles files located inside zip archives (.pyz, pipx venvs) correctly.
|
|
108
|
+
license_path_traversable = files("pdflinkcheck.data") / "LICENSE"
|
|
109
|
+
license_content = license_path_traversable.read_text(encoding="utf-8")
|
|
110
|
+
|
|
111
|
+
except FileNotFoundError:
|
|
112
|
+
messagebox.showerror(
|
|
113
|
+
"License Error",
|
|
114
|
+
"LICENSE file not found within the installation package (pdflinkcheck.data/LICENSE). Check build process."
|
|
115
|
+
)
|
|
116
|
+
return
|
|
117
|
+
except Exception as e:
|
|
118
|
+
messagebox.showerror("Read Error", f"Failed to read embedded LICENSE file: {e}")
|
|
119
|
+
return
|
|
120
|
+
|
|
121
|
+
# --- Display in a New Toplevel Window ---
|
|
122
|
+
license_window = tk.Toplevel(self)
|
|
123
|
+
license_window.title("Software License")
|
|
124
|
+
license_window.geometry("600x400")
|
|
125
|
+
|
|
126
|
+
# Text widget for content
|
|
127
|
+
text_widget = tk.Text(license_window, wrap=tk.WORD, font=('Monospace', 10), padx=10, pady=10)
|
|
128
|
+
text_widget.insert(tk.END, license_content)
|
|
129
|
+
text_widget.config(state=tk.DISABLED)
|
|
130
|
+
|
|
131
|
+
# Scrollbar
|
|
132
|
+
scrollbar = ttk.Scrollbar(license_window, command=text_widget.yview)
|
|
133
|
+
text_widget['yscrollcommand'] = scrollbar.set
|
|
134
|
+
|
|
135
|
+
# Layout
|
|
136
|
+
scrollbar.pack(side=tk.RIGHT, fill=tk.Y)
|
|
137
|
+
text_widget.pack(fill='both', expand=True)
|
|
138
|
+
|
|
139
|
+
# Make the window modal (optional, but good practice for notices)
|
|
140
|
+
license_window.transient(self)
|
|
141
|
+
license_window.grab_set()
|
|
142
|
+
self.wait_window(license_window)
|
|
143
|
+
|
|
144
|
+
def _show_readme(self):
|
|
145
|
+
"""
|
|
146
|
+
Reads the embedded README.md file and displays its content in a new modal window.
|
|
147
|
+
"""
|
|
148
|
+
try:
|
|
149
|
+
# CORRECT WAY: Use the Traversable object's read_text() method.
|
|
150
|
+
# This handles files located inside zip archives (.pyz, pipx venvs) correctly.
|
|
151
|
+
readme_path_traversable = files("pdflinkcheck.data") / "README.md"
|
|
152
|
+
readme_content = readme_path_traversable.read_text(encoding="utf-8")
|
|
153
|
+
readme_content = sanitize_glyphs_for_tkinter(readme_content)
|
|
154
|
+
|
|
155
|
+
except FileNotFoundError:
|
|
156
|
+
messagebox.showerror(
|
|
157
|
+
"Readme Error",
|
|
158
|
+
"README.md file not found within the installation package (pdflinkcheck.data/README.md). Check build process."
|
|
159
|
+
)
|
|
160
|
+
return
|
|
161
|
+
except Exception as e:
|
|
162
|
+
messagebox.showerror("Read Error", f"Failed to read embedded README.md file: {e}")
|
|
163
|
+
return
|
|
164
|
+
|
|
165
|
+
# --- Display in a New Toplevel Window ---
|
|
166
|
+
readme_window = tk.Toplevel(self)
|
|
167
|
+
readme_window.title("pdflinkcheck README.md")
|
|
168
|
+
readme_window.geometry("600x400")
|
|
169
|
+
|
|
170
|
+
# Text widget for content
|
|
171
|
+
text_widget = tk.Text(readme_window, wrap=tk.WORD, font=('Monospace', 10), padx=10, pady=10)
|
|
172
|
+
text_widget.insert(tk.END, readme_content)
|
|
173
|
+
text_widget.config(state=tk.DISABLED)
|
|
174
|
+
|
|
175
|
+
# Scrollbar
|
|
176
|
+
scrollbar = ttk.Scrollbar(readme_window, command=text_widget.yview)
|
|
177
|
+
text_widget['yscrollcommand'] = scrollbar.set
|
|
178
|
+
|
|
179
|
+
# Layout
|
|
180
|
+
scrollbar.pack(side=tk.RIGHT, fill=tk.Y)
|
|
181
|
+
text_widget.pack(fill='both', expand=True)
|
|
182
|
+
|
|
183
|
+
# Make the window modal (optional, but good practice for notices)
|
|
184
|
+
readme_window.transient(self)
|
|
185
|
+
readme_window.grab_set()
|
|
186
|
+
self.wait_window(readme_window)
|
|
41
187
|
|
|
42
188
|
def _create_widgets(self):
|
|
43
189
|
# --- Control Frame (Top) ---
|
|
44
190
|
control_frame = ttk.Frame(self, padding="10")
|
|
45
191
|
control_frame.pack(fill='x')
|
|
46
192
|
|
|
47
|
-
# File Selection
|
|
48
|
-
ttk.Label(control_frame, text="PDF Path:").grid(row=0, column=0, padx=5, pady=5, sticky='w')
|
|
49
|
-
ttk.Entry(control_frame, textvariable=self.pdf_path, width=60).grid(row=0, column=1, padx=5, pady=5, sticky='ew')
|
|
50
|
-
ttk.Button(control_frame, text="Browse...", command=self._select_pdf).grid(row=0, column=2, padx=5, pady=5)
|
|
193
|
+
# Row 0: File Selection
|
|
51
194
|
|
|
52
|
-
#
|
|
53
|
-
ttk.
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
195
|
+
# === File Selection Frame (Row 0) ===
|
|
196
|
+
file_selection_frame = ttk.Frame(control_frame)
|
|
197
|
+
file_selection_frame.grid(row=0, column=0, columnspan=3, padx=0, pady=5, sticky='ew')
|
|
198
|
+
|
|
199
|
+
# Elements are now packed/gridded within file_selection_frame
|
|
200
|
+
|
|
201
|
+
# Label
|
|
202
|
+
ttk.Label(file_selection_frame, text="PDF Path:").pack(side=tk.LEFT, padx=(0, 5))
|
|
203
|
+
|
|
204
|
+
# Entry (Path Display)
|
|
205
|
+
ttk.Entry(file_selection_frame, textvariable=self.pdf_path, width=50).pack(side=tk.LEFT, fill='x', expand=True, padx=5)
|
|
206
|
+
# The Entry field (column 1) must expand horizontally within its frame
|
|
207
|
+
# Since we are using PACK for this frame, we use fill='x', expand=True on the Entry.
|
|
208
|
+
|
|
209
|
+
# Browse Button
|
|
210
|
+
ttk.Button(file_selection_frame, text="Browse...", command=self._select_pdf).pack(side=tk.LEFT, padx=(5, 5))
|
|
58
211
|
|
|
212
|
+
# Copy Button
|
|
213
|
+
# NOTE: Removed leading spaces from " Copy Path"
|
|
214
|
+
ttk.Button(file_selection_frame, text="Copy Path", command=self._copy_pdf_path).pack(side=tk.LEFT, padx=(0, 0))
|
|
215
|
+
|
|
216
|
+
# === END: File Selection Frame ===
|
|
217
|
+
|
|
218
|
+
# --- Report brevity options ----
|
|
219
|
+
report_brevity_frame = ttk.LabelFrame(control_frame, text="Report Brevity Options:")
|
|
220
|
+
#report_brevity_frame.grid(row=1, column=0, columnspan=2, padx=5, pady=1, sticky='nsew')
|
|
221
|
+
report_brevity_frame.grid(row=1, column=0, padx=5, pady=5, sticky='nsew')
|
|
222
|
+
#
|
|
59
223
|
ttk.Checkbutton(
|
|
60
|
-
|
|
61
|
-
text="Show All Links
|
|
224
|
+
report_brevity_frame,
|
|
225
|
+
text="Show All Links.",
|
|
62
226
|
variable=self.show_all_links_var,
|
|
63
|
-
# Optional: Disable max_links entry when this is checked
|
|
64
227
|
command=self._toggle_max_links_entry
|
|
65
|
-
).
|
|
228
|
+
).pack(side='left', padx=5, pady=1)
|
|
229
|
+
|
|
230
|
+
ttk.Label(report_brevity_frame, text="Max Links to Display:").pack(side='left', padx=5, pady=1)
|
|
231
|
+
self.max_links_entry = ttk.Entry(report_brevity_frame, textvariable=self.max_links_var, width=4)
|
|
232
|
+
self.max_links_entry.pack(side='left', padx=5, pady=5)
|
|
66
233
|
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
234
|
+
# --- PDF Library Selection ---
|
|
235
|
+
# Create a labeled group for the PDF options
|
|
236
|
+
pdf_library_frame = ttk.LabelFrame(control_frame, text="Select PDF Library:")
|
|
237
|
+
pdf_library_frame.grid(row=1, column=1, padx=5, pady=5, sticky='nsew')
|
|
238
|
+
|
|
239
|
+
# Radio options inside the frame
|
|
240
|
+
ttk.Radiobutton(
|
|
241
|
+
pdf_library_frame,
|
|
242
|
+
text="PyMuPDF",
|
|
243
|
+
variable=self.pdf_library_var,
|
|
244
|
+
value="PyMuPDF",
|
|
245
|
+
|
|
246
|
+
).pack(side='left', padx=5, pady=1)
|
|
247
|
+
|
|
248
|
+
ttk.Radiobutton(
|
|
249
|
+
pdf_library_frame,
|
|
250
|
+
text="pypdf",
|
|
251
|
+
variable=self.pdf_library_var,
|
|
252
|
+
value="pypdf",
|
|
253
|
+
).pack(side='left', padx=5, pady=1)
|
|
254
|
+
|
|
255
|
+
export_group_frame = ttk.LabelFrame(control_frame, text="Export Format:")
|
|
256
|
+
#export_group_frame = ttk.LabelFrame(control_frame, text = "Export Filetype Selection:")
|
|
257
|
+
export_group_frame.grid(row=1, column=2, padx=5, pady=5, sticky='nseew') # Placed in the original Checkbutton's column
|
|
70
258
|
|
|
71
|
-
|
|
72
|
-
|
|
259
|
+
ttk.Checkbutton(
|
|
260
|
+
export_group_frame,
|
|
261
|
+
#text="Export Report",
|
|
262
|
+
text = "JSON" ,
|
|
263
|
+
variable=self.do_export_report_json_var
|
|
264
|
+
).pack(side=tk.LEFT, padx=(0, 5)) # Pack Checkbutton to the left with small internal padding
|
|
265
|
+
ttk.Checkbutton(
|
|
266
|
+
export_group_frame,
|
|
267
|
+
text = "TXT" ,
|
|
268
|
+
#state=tk.DISABLED,
|
|
269
|
+
variable=self.do_export_report_txt_var,
|
|
270
|
+
).pack(side=tk.LEFT, padx=(0, 5)) # Pack Checkbutton to the left with small internal padding
|
|
73
271
|
|
|
272
|
+
# Row 3: Run Button, Export Filetype selection, License Button, and readme button
|
|
273
|
+
# 1. Run Button (Spans columns 0 and 1)
|
|
274
|
+
run_analysis_btn = ttk.Button(control_frame, text="▶ Run Analysis", command=self._run_report_gui, style='Accent.TButton')
|
|
275
|
+
run_analysis_btn.grid(row=3, column=0, columnspan=2, pady=10, sticky='ew', padx=(0, 5))
|
|
276
|
+
|
|
277
|
+
run_validation_btn = ttk.Button(control_frame, text="▶ Run Validation", command=self._run_validation_gui, style='Accent.TButton')
|
|
278
|
+
run_validation_btn.grid(row=4, column=0, columnspan=2, pady=10, sticky='ew', padx=(0, 5))
|
|
279
|
+
# Ensure the run button frame expands to fill its column
|
|
280
|
+
#run_analysis_btn.grid_columnconfigure(0, weight=1)
|
|
281
|
+
|
|
282
|
+
# 2. Create a Frame to hold the two file link buttons (This frame goes into column 2)
|
|
283
|
+
info_btn_frame = ttk.Frame(control_frame)
|
|
284
|
+
info_btn_frame.grid(row=3, column=2, columnspan=1, pady=10, sticky='ew', padx=(5, 0))
|
|
285
|
+
# Ensure the info button frame expands to fill its column
|
|
286
|
+
info_btn_frame.grid_columnconfigure(0, weight=1)
|
|
287
|
+
info_btn_frame.grid_columnconfigure(1, weight=1)
|
|
288
|
+
|
|
289
|
+
# 3. Place License and Readme buttons inside the new frame
|
|
290
|
+
license_btn = ttk.Button(info_btn_frame, text="License", command=self._show_license)
|
|
291
|
+
# Use PACK or a 2-column GRID inside the info_btn_frame. GRID is cleaner here.
|
|
292
|
+
license_btn.grid(row=0, column=0, sticky='ew', padx=(0, 2)) # Left side of the frame
|
|
293
|
+
|
|
294
|
+
readme_btn = ttk.Button(info_btn_frame, text="Readme", command=self._show_readme)
|
|
295
|
+
readme_btn.grid(row=0, column=1, sticky='ew', padx=(2, 0)) # Right side of the frame
|
|
296
|
+
|
|
297
|
+
# Force the columns to distribute space evenly
|
|
298
|
+
control_frame.grid_columnconfigure(0, weight=2)
|
|
74
299
|
control_frame.grid_columnconfigure(1, weight=1)
|
|
300
|
+
control_frame.grid_columnconfigure(2, weight=1)
|
|
75
301
|
|
|
76
302
|
# --- Output Frame (Bottom) ---
|
|
77
|
-
output_frame = ttk.Frame(self, padding=
|
|
303
|
+
output_frame = ttk.Frame(self, padding=(10, 2, 10, 10)) # Left, Top, Right, Bottom
|
|
78
304
|
output_frame.pack(fill='both', expand=True)
|
|
79
305
|
|
|
80
|
-
|
|
306
|
+
output_header_frame = ttk.Frame(output_frame)
|
|
307
|
+
output_header_frame.pack(fill='x', pady=(0, 5))
|
|
308
|
+
|
|
309
|
+
# Label
|
|
310
|
+
ttk.Label(output_header_frame, text="Analysis Report Output:").pack(side=tk.LEFT, fill='x', expand=True)
|
|
311
|
+
|
|
312
|
+
# Scroll to Bottom Button # put this first so that it on the right when the Top button is added on the left.
|
|
313
|
+
bottom_btn = ttk.Button(output_header_frame, text="▼ Bottom", command=self._scroll_to_bottom, width=8)
|
|
314
|
+
bottom_btn.pack(side=tk.RIGHT, padx=(0, 5))
|
|
315
|
+
|
|
316
|
+
# Scroll to Top Button
|
|
317
|
+
top_btn = ttk.Button(output_header_frame, text="▲ Top", command=self._scroll_to_top, width=6)
|
|
318
|
+
top_btn.pack(side=tk.RIGHT, padx=(5, 5))
|
|
319
|
+
|
|
320
|
+
# Open Report Button
|
|
321
|
+
self.open_report_btn = ttk.Button(output_header_frame, text="Open Report", command=self._open_report_text)
|
|
322
|
+
self.open_report_btn.pack(side=tk.RIGHT, padx=(5, 5))
|
|
323
|
+
|
|
324
|
+
|
|
325
|
+
# ----------------------------------------------------
|
|
326
|
+
|
|
81
327
|
|
|
82
328
|
# Scrollable Text Widget for output
|
|
83
|
-
|
|
84
|
-
|
|
329
|
+
# Use an internal frame for text and scrollbar to ensure correct packing
|
|
330
|
+
text_scroll_frame = ttk.Frame(output_frame)
|
|
331
|
+
text_scroll_frame.pack(fill='both', expand=True, padx=5, pady=5)
|
|
85
332
|
|
|
86
|
-
#
|
|
87
|
-
|
|
88
|
-
|
|
333
|
+
self.output_text = tk.Text(text_scroll_frame, wrap=tk.WORD, state=tk.DISABLED, bg='#333333', fg='white', font=('Monospace', 10))
|
|
334
|
+
self.output_text.pack(side=tk.LEFT, fill='both', expand=True) # Text fills and expands
|
|
335
|
+
|
|
336
|
+
# Scrollbar (Scrollbar must be packed AFTER the text widget)
|
|
337
|
+
scrollbar = ttk.Scrollbar(text_scroll_frame, command=self.output_text.yview)
|
|
89
338
|
scrollbar.pack(side=tk.RIGHT, fill=tk.Y)
|
|
339
|
+
self.output_text['yscrollcommand'] = scrollbar.set # Link text widget back to scrollbar
|
|
90
340
|
|
|
91
341
|
def _select_pdf(self):
|
|
342
|
+
if self.pdf_path.get():
|
|
343
|
+
initialdir = str(Path(self.pdf_path.get()).parent)
|
|
344
|
+
else:
|
|
345
|
+
initialdir = str(Path.cwd())
|
|
346
|
+
|
|
92
347
|
file_path = filedialog.askopenfilename(
|
|
348
|
+
initialdir=initialdir,
|
|
93
349
|
defaultextension=".pdf",
|
|
94
350
|
filetypes=[("PDF files", "*.pdf"), ("All files", "*.*")]
|
|
95
351
|
)
|
|
96
352
|
if file_path:
|
|
97
|
-
self.pdf_path.set(file_path)
|
|
353
|
+
self.pdf_path.set(get_friendly_path(file_path))
|
|
354
|
+
|
|
98
355
|
|
|
99
356
|
def _toggle_max_links_entry(self):
|
|
100
357
|
"""Disables/enables the max_links entry based on show_all_links_var."""
|
|
@@ -102,26 +359,63 @@ class PDFLinkCheckerApp(tk.Tk):
|
|
|
102
359
|
self.max_links_entry.config(state=tk.DISABLED)
|
|
103
360
|
else:
|
|
104
361
|
self.max_links_entry.config(state=tk.NORMAL)
|
|
362
|
+
|
|
363
|
+
def _toggle_json_export(self):
|
|
364
|
+
"""Checkbox toggle for json filetype report."""
|
|
365
|
+
if self.do_export_report_json_var.get():
|
|
366
|
+
pass # placeholder # no side effects
|
|
105
367
|
|
|
106
|
-
def
|
|
107
|
-
|
|
108
|
-
if
|
|
109
|
-
|
|
368
|
+
def _toggle_txt_export(self):
|
|
369
|
+
"""Checkbox toggle for TXT filetype report."""
|
|
370
|
+
if self.do_export_report_txt_var.get():
|
|
371
|
+
pass # placeholder # no side effects
|
|
372
|
+
|
|
373
|
+
def _assess_pdf_path_str(self):
|
|
374
|
+
pdf_path_str = self.pdf_path.get().strip()
|
|
375
|
+
if not pdf_path_str:
|
|
376
|
+
pdf_path_str = get_first_pdf_in_cwd()
|
|
377
|
+
if not pdf_path_str:
|
|
378
|
+
self._display_error("Error: No PDF found in current directory.")
|
|
379
|
+
return
|
|
380
|
+
|
|
381
|
+
p = Path(pdf_path_str).expanduser().resolve()
|
|
382
|
+
|
|
383
|
+
if not p.exists():
|
|
384
|
+
self._display_error(f"Error: PDF file not found at: {p}")
|
|
110
385
|
return
|
|
386
|
+
|
|
387
|
+
# Use the resolved string version for the rest of the function
|
|
388
|
+
pdf_path_str_assessed = str(p)
|
|
389
|
+
return pdf_path_str_assessed
|
|
390
|
+
|
|
391
|
+
def _run_report_gui(self):
|
|
111
392
|
|
|
393
|
+
pdf_path_str = self._assess_pdf_path_str()
|
|
394
|
+
if not pdf_path_str:
|
|
395
|
+
return
|
|
396
|
+
|
|
112
397
|
if self.show_all_links_var.get():
|
|
113
|
-
# Pass 0 to the backend, which analyze.py interprets as "Show All"
|
|
114
398
|
max_links_to_pass = 0
|
|
115
399
|
else:
|
|
116
400
|
try:
|
|
117
401
|
max_links_to_pass = int(self.max_links_var.get())
|
|
118
|
-
if max_links_to_pass
|
|
402
|
+
if max_links_to_pass < 1:
|
|
119
403
|
self._display_error("Error: Max Links must be a positive number (or use 'Show All').")
|
|
120
404
|
return
|
|
121
405
|
except ValueError:
|
|
122
406
|
self._display_error("Error: Max Links must be an integer.")
|
|
123
407
|
return
|
|
124
408
|
|
|
409
|
+
export_format = None # default value, if selection is not made (if selection is not active)
|
|
410
|
+
export_format = ""
|
|
411
|
+
if self.do_export_report_json_var.get():
|
|
412
|
+
export_format = export_format + "JSON"
|
|
413
|
+
if self.do_export_report_txt_var.get():
|
|
414
|
+
export_format = export_format + "TXT"
|
|
415
|
+
|
|
416
|
+
pdf_library = self._discern_pdf_library()
|
|
417
|
+
|
|
418
|
+
|
|
125
419
|
# 1. Clear previous output and enable editing
|
|
126
420
|
self.output_text.config(state=tk.NORMAL)
|
|
127
421
|
self.output_text.delete('1.0', tk.END)
|
|
@@ -132,34 +426,165 @@ class PDFLinkCheckerApp(tk.Tk):
|
|
|
132
426
|
|
|
133
427
|
try:
|
|
134
428
|
# 3. Call the core logic function
|
|
135
|
-
self.output_text.insert(tk.END, "--- Starting Analysis ---\n")
|
|
136
|
-
|
|
429
|
+
#self.output_text.insert(tk.END, "--- Starting Analysis ---\n")
|
|
430
|
+
report_results = run_report(
|
|
137
431
|
pdf_path=pdf_path_str,
|
|
138
|
-
|
|
139
|
-
|
|
432
|
+
max_links=max_links_to_pass,
|
|
433
|
+
export_format=export_format,
|
|
434
|
+
pdf_library = pdf_library,
|
|
140
435
|
)
|
|
141
|
-
self.
|
|
436
|
+
self.current_report_text = report_results.get("text", "")
|
|
437
|
+
self.current_report_data = report_results.get("data", {})
|
|
438
|
+
|
|
439
|
+
#self.output_text.insert(tk.END, "\n--- Analysis Complete ---\n")
|
|
142
440
|
|
|
143
441
|
except Exception as e:
|
|
442
|
+
# Inform the user in the GUI with a clean message
|
|
144
443
|
self._display_error(f"An unexpected error occurred during analysis: {e}")
|
|
145
444
|
|
|
146
445
|
finally:
|
|
147
446
|
# 4. Restore standard output and disable editing
|
|
148
447
|
sys.stdout = original_stdout
|
|
149
448
|
self.output_text.config(state=tk.DISABLED)
|
|
449
|
+
|
|
450
|
+
def _run_validation_gui(self):
|
|
451
|
+
|
|
452
|
+
pdf_path_str = self._assess_pdf_path_str()
|
|
453
|
+
if not pdf_path_str:
|
|
454
|
+
return
|
|
150
455
|
|
|
151
|
-
|
|
456
|
+
pdf_library = self._discern_pdf_library()
|
|
457
|
+
|
|
458
|
+
# 1. Clear previous output and enable editing
|
|
152
459
|
self.output_text.config(state=tk.NORMAL)
|
|
153
460
|
self.output_text.delete('1.0', tk.END)
|
|
461
|
+
|
|
462
|
+
# 2. Redirect standard output to the Text widget
|
|
463
|
+
original_stdout = sys.stdout
|
|
464
|
+
sys.stdout = RedirectText(self.output_text)
|
|
465
|
+
|
|
466
|
+
if not self.current_report_data:
|
|
467
|
+
self._run_report_gui()
|
|
468
|
+
report_results = self.current_report_data
|
|
469
|
+
|
|
470
|
+
try:
|
|
471
|
+
# 3. Call the core logic function
|
|
472
|
+
#self.output_text.insert(tk.END, "--- Starting Analysis ---\n")
|
|
473
|
+
validation_results = run_validation(
|
|
474
|
+
report_results=report_results,
|
|
475
|
+
pdf_path=pdf_path_str,
|
|
476
|
+
pdf_library=pdf_library,
|
|
477
|
+
export_json=True,
|
|
478
|
+
print_bool=True
|
|
479
|
+
)
|
|
480
|
+
self.current_report_text = report_results.get("text", "")
|
|
481
|
+
self.current_report_data = report_results.get("data", {})
|
|
482
|
+
|
|
483
|
+
#self.output_text.insert(tk.END, "\n--- Analysis Complete ---\n")
|
|
484
|
+
|
|
485
|
+
except Exception as e:
|
|
486
|
+
# Inform the user in the GUI with a clean message
|
|
487
|
+
self._display_error(f"An unexpected error occurred during analysis: {e}")
|
|
488
|
+
|
|
489
|
+
finally:
|
|
490
|
+
# 4. Restore standard output and disable editing
|
|
491
|
+
sys.stdout = original_stdout
|
|
492
|
+
self.output_text.config(state=tk.DISABLED)
|
|
493
|
+
|
|
494
|
+
|
|
495
|
+
def _discern_pdf_library(self):
|
|
496
|
+
selected_lib = self.pdf_library_var.get().lower()
|
|
497
|
+
|
|
498
|
+
if selected_lib == "pymupdf":
|
|
499
|
+
print("Using high-speed PyMuPDF engine.")
|
|
500
|
+
elif selected_lib == "pypdf":
|
|
501
|
+
print("Using pure-python pypdf engine.")
|
|
502
|
+
return selected_lib
|
|
503
|
+
|
|
504
|
+
def _display_error(self, message):
|
|
505
|
+
# Ensure output is in normal state to write
|
|
506
|
+
original_state = self.output_text.cget('state')
|
|
507
|
+
if original_state == tk.DISABLED:
|
|
508
|
+
self.output_text.config(state=tk.NORMAL)
|
|
509
|
+
|
|
510
|
+
#self.output_text.delete('1.0', tk.END)
|
|
154
511
|
self.output_text.insert(tk.END, f"[ERROR] {message}\n", 'error')
|
|
155
512
|
self.output_text.tag_config('error', foreground='red')
|
|
513
|
+
self.output_text.see(tk.END)
|
|
514
|
+
|
|
515
|
+
# Restore state
|
|
156
516
|
self.output_text.config(state=tk.DISABLED)
|
|
157
517
|
|
|
518
|
+
def _open_report_text(self):
|
|
519
|
+
"""Opens the LATEST analysis text in an editor, regardless of export settings."""
|
|
520
|
+
# 1. Check our internal buffer, not the window or the disk
|
|
521
|
+
if not self.current_report_text:
|
|
522
|
+
messagebox.showwarning("Open Failed", "No analysis data available. Please run an analysis first.")
|
|
523
|
+
return
|
|
524
|
+
|
|
525
|
+
try:
|
|
526
|
+
# 2. Always create a 'viewing' file in a temp directory or .tmp folder
|
|
527
|
+
# This prevents clobbering an actual user-saved report.
|
|
528
|
+
pdf_name = Path(self.pdf_path.get()).stem if self.pdf_path.get() else "report"
|
|
529
|
+
view_path = PDFLINKCHECK_HOME / f"LAST_REPORT_{pdf_name}.txt"
|
|
530
|
+
|
|
531
|
+
# 3. Write our buffer to this 'View' file
|
|
532
|
+
view_path.write_text(self.current_report_text, encoding="utf-8")
|
|
533
|
+
|
|
534
|
+
# 4. Open with pyhabitat
|
|
535
|
+
pyhabitat.edit_textfile(view_path)
|
|
536
|
+
|
|
537
|
+
except Exception as e:
|
|
538
|
+
messagebox.showerror("View Error", f"Could not launch editor: {e}")
|
|
539
|
+
|
|
540
|
+
"""
|
|
541
|
+
def toggle_theme():
|
|
542
|
+
try:
|
|
543
|
+
current = sv_ttk.get_theme()
|
|
544
|
+
sv_ttk.set_theme("dark" if current == "light" else "light")
|
|
545
|
+
except Exception:
|
|
546
|
+
pass
|
|
547
|
+
"""
|
|
548
|
+
def sanitize_glyphs_for_tkinter(text: str) -> str:
|
|
549
|
+
"""
|
|
550
|
+
Converts complex Unicode characters (like emojis and symbols)
|
|
551
|
+
into their closest ASCII representation, ignoring those that
|
|
552
|
+
cannot be mapped. This prevents the 'empty square' issue in Tkinter.
|
|
553
|
+
"""
|
|
554
|
+
# 1. Normalize the text (NFKD converts composite characters to their base parts)
|
|
555
|
+
normalized = unicodedata.normalize('NFKD', text)
|
|
556
|
+
|
|
557
|
+
# 2. Encode to ASCII and decode back.
|
|
558
|
+
# The 'ignore' flag is crucial: it removes any characters
|
|
559
|
+
# that don't have an ASCII representation.
|
|
560
|
+
sanitized = normalized.encode('ascii', 'ignore').decode('utf-8')
|
|
561
|
+
|
|
562
|
+
# 3. Clean up any resulting double spaces or artifacts
|
|
563
|
+
sanitized = sanitized.replace(' ', ' ')
|
|
564
|
+
return sanitized
|
|
565
|
+
|
|
566
|
+
def auto_close_window(root, delay_ms:int = 0):
|
|
567
|
+
"""
|
|
568
|
+
Schedules the Tkinter window to be destroyed after a specified delay.
|
|
569
|
+
"""
|
|
570
|
+
if delay_ms > 0:
|
|
571
|
+
print(f"Window is set to automatically close in {delay_ms/1000} seconds.")
|
|
572
|
+
root.after(delay_ms, root.destroy)
|
|
573
|
+
else:
|
|
574
|
+
return
|
|
575
|
+
|
|
576
|
+
|
|
577
|
+
def start_gui(time_auto_close:int=0):
|
|
578
|
+
"""
|
|
579
|
+
Entry point function to launch the application.
|
|
580
|
+
"""
|
|
581
|
+
print("pdflinkcheck: start_gui ...")
|
|
582
|
+
tk_app = PDFLinkCheckerApp()
|
|
583
|
+
|
|
584
|
+
auto_close_window(tk_app, time_auto_close)
|
|
158
585
|
|
|
159
|
-
|
|
160
|
-
"
|
|
161
|
-
app = PDFLinkCheckerApp()
|
|
162
|
-
app.mainloop()
|
|
586
|
+
tk_app.mainloop()
|
|
587
|
+
print("pdflinkcheck: gui closed.")
|
|
163
588
|
|
|
164
589
|
if __name__ == "__main__":
|
|
165
|
-
start_gui()
|
|
590
|
+
start_gui()
|