pdflinkcheck 1.1.73__py3-none-any.whl → 1.1.94__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 +2 -5
- pdflinkcheck/analyze_pymupdf.py +12 -6
- pdflinkcheck/analyze_pypdf.py +25 -7
- pdflinkcheck/analyze_pypdf_v2.py +5 -6
- pdflinkcheck/cli.py +82 -91
- pdflinkcheck/data/I Have Questions.md +51 -0
- pdflinkcheck/data/LICENSE +17 -654
- pdflinkcheck/data/README.md +49 -49
- pdflinkcheck/data/icons/BoxArt-1080x1080.png +0 -0
- pdflinkcheck/data/icons/Logo-150x150.png +0 -0
- pdflinkcheck/data/icons/Logo-300x300.png +0 -0
- pdflinkcheck/data/icons/Logo-71x71.png +0 -0
- pdflinkcheck/data/icons/PosterArt-720x1080.png +0 -0
- pdflinkcheck/data/icons/SmallLogo-44x44.png +0 -0
- pdflinkcheck/data/icons/SplashScreen-620x300.png +0 -0
- pdflinkcheck/data/icons/StoreLogo-50x50.png +0 -0
- pdflinkcheck/data/icons/WideLogo-310x150.png +0 -0
- pdflinkcheck/data/icons/red_pdf_512px.ico +0 -0
- pdflinkcheck/data/pyproject.toml +20 -23
- pdflinkcheck/data/themes/forest/forest-dark/border-accent-hover.png +0 -0
- pdflinkcheck/data/themes/forest/forest-dark/border-accent.png +0 -0
- pdflinkcheck/data/themes/forest/forest-dark/border-basic.png +0 -0
- pdflinkcheck/data/themes/forest/forest-dark/border-hover.png +0 -0
- pdflinkcheck/data/themes/forest/forest-dark/border-invalid.png +0 -0
- pdflinkcheck/data/themes/forest/forest-dark/card.png +0 -0
- pdflinkcheck/data/themes/forest/forest-dark/check-accent.png +0 -0
- pdflinkcheck/data/themes/forest/forest-dark/check-basic.png +0 -0
- pdflinkcheck/data/themes/forest/forest-dark/check-hover.png +0 -0
- pdflinkcheck/data/themes/forest/forest-dark/check-tri-accent.png +0 -0
- pdflinkcheck/data/themes/forest/forest-dark/check-tri-basic.png +0 -0
- pdflinkcheck/data/themes/forest/forest-dark/check-tri-hover.png +0 -0
- pdflinkcheck/data/themes/forest/forest-dark/check-unsel-accent.png +0 -0
- pdflinkcheck/data/themes/forest/forest-dark/check-unsel-basic.png +0 -0
- pdflinkcheck/data/themes/forest/forest-dark/check-unsel-hover.png +0 -0
- pdflinkcheck/data/themes/forest/forest-dark/check-unsel-pressed.png +0 -0
- pdflinkcheck/data/themes/forest/forest-dark/combo-button-basic.png +0 -0
- pdflinkcheck/data/themes/forest/forest-dark/combo-button-focus.png +0 -0
- pdflinkcheck/data/themes/forest/forest-dark/combo-button-hover.png +0 -0
- pdflinkcheck/data/themes/forest/forest-dark/down.png +0 -0
- pdflinkcheck/data/themes/forest/forest-dark/empty.png +0 -0
- pdflinkcheck/data/themes/forest/forest-dark/hor-accent.png +0 -0
- pdflinkcheck/data/themes/forest/forest-dark/hor-basic.png +0 -0
- pdflinkcheck/data/themes/forest/forest-dark/hor-hover.png +0 -0
- pdflinkcheck/data/themes/forest/forest-dark/notebook.png +0 -0
- pdflinkcheck/data/themes/forest/forest-dark/off-accent.png +0 -0
- pdflinkcheck/data/themes/forest/forest-dark/off-basic.png +0 -0
- pdflinkcheck/data/themes/forest/forest-dark/off-hover.png +0 -0
- pdflinkcheck/data/themes/forest/forest-dark/on-accent.png +0 -0
- pdflinkcheck/data/themes/forest/forest-dark/on-basic.png +0 -0
- pdflinkcheck/data/themes/forest/forest-dark/on-hover.png +0 -0
- pdflinkcheck/data/themes/forest/forest-dark/radio-accent.png +0 -0
- pdflinkcheck/data/themes/forest/forest-dark/radio-basic.png +0 -0
- pdflinkcheck/data/themes/forest/forest-dark/radio-hover.png +0 -0
- pdflinkcheck/data/themes/forest/forest-dark/radio-tri-accent.png +0 -0
- pdflinkcheck/data/themes/forest/forest-dark/radio-tri-basic.png +0 -0
- pdflinkcheck/data/themes/forest/forest-dark/radio-tri-hover.png +0 -0
- pdflinkcheck/data/themes/forest/forest-dark/radio-unsel-accent.png +0 -0
- pdflinkcheck/data/themes/forest/forest-dark/radio-unsel-basic.png +0 -0
- pdflinkcheck/data/themes/forest/forest-dark/radio-unsel-hover.png +0 -0
- pdflinkcheck/data/themes/forest/forest-dark/radio-unsel-pressed.png +0 -0
- pdflinkcheck/data/themes/forest/forest-dark/rect-accent-hover.png +0 -0
- pdflinkcheck/data/themes/forest/forest-dark/rect-accent.png +0 -0
- pdflinkcheck/data/themes/forest/forest-dark/rect-basic.png +0 -0
- pdflinkcheck/data/themes/forest/forest-dark/rect-hover.png +0 -0
- pdflinkcheck/data/themes/forest/forest-dark/right.png +0 -0
- pdflinkcheck/data/themes/forest/forest-dark/scale-hor.png +0 -0
- pdflinkcheck/data/themes/forest/forest-dark/scale-vert.png +0 -0
- pdflinkcheck/data/themes/forest/forest-dark/separator.png +0 -0
- pdflinkcheck/data/themes/forest/forest-dark/sizegrip.png +0 -0
- pdflinkcheck/data/themes/forest/forest-dark/spin-button-down-basic.png +0 -0
- pdflinkcheck/data/themes/forest/forest-dark/spin-button-down-focus.png +0 -0
- pdflinkcheck/data/themes/forest/forest-dark/spin-button-up.png +0 -0
- pdflinkcheck/data/themes/forest/forest-dark/tab-accent.png +0 -0
- pdflinkcheck/data/themes/forest/forest-dark/tab-basic.png +0 -0
- pdflinkcheck/data/themes/forest/forest-dark/tab-hover.png +0 -0
- pdflinkcheck/data/themes/forest/forest-dark/thumb-hor-accent.png +0 -0
- pdflinkcheck/data/themes/forest/forest-dark/thumb-hor-basic.png +0 -0
- pdflinkcheck/data/themes/forest/forest-dark/thumb-hor-hover.png +0 -0
- pdflinkcheck/data/themes/forest/forest-dark/thumb-vert-accent.png +0 -0
- pdflinkcheck/data/themes/forest/forest-dark/thumb-vert-basic.png +0 -0
- pdflinkcheck/data/themes/forest/forest-dark/thumb-vert-hover.png +0 -0
- pdflinkcheck/data/themes/forest/forest-dark/tree-basic.png +0 -0
- pdflinkcheck/data/themes/forest/forest-dark/tree-pressed.png +0 -0
- pdflinkcheck/data/themes/forest/forest-dark/up.png +0 -0
- pdflinkcheck/data/themes/forest/forest-dark/vert-accent.png +0 -0
- pdflinkcheck/data/themes/forest/forest-dark/vert-basic.png +0 -0
- pdflinkcheck/data/themes/forest/forest-dark/vert-hover.png +0 -0
- pdflinkcheck/data/themes/forest/forest-dark.tcl +536 -0
- pdflinkcheck/data/themes/forest/forest-light/border-accent-hover.png +0 -0
- pdflinkcheck/data/themes/forest/forest-light/border-accent.png +0 -0
- pdflinkcheck/data/themes/forest/forest-light/border-basic.png +0 -0
- pdflinkcheck/data/themes/forest/forest-light/border-hover.png +0 -0
- pdflinkcheck/data/themes/forest/forest-light/border-invalid.png +0 -0
- pdflinkcheck/data/themes/forest/forest-light/card.png +0 -0
- pdflinkcheck/data/themes/forest/forest-light/check-accent.png +0 -0
- pdflinkcheck/data/themes/forest/forest-light/check-basic.png +0 -0
- pdflinkcheck/data/themes/forest/forest-light/check-hover.png +0 -0
- pdflinkcheck/data/themes/forest/forest-light/check-tri-accent.png +0 -0
- pdflinkcheck/data/themes/forest/forest-light/check-tri-basic.png +0 -0
- pdflinkcheck/data/themes/forest/forest-light/check-tri-hover.png +0 -0
- pdflinkcheck/data/themes/forest/forest-light/check-unsel-accent.png +0 -0
- pdflinkcheck/data/themes/forest/forest-light/check-unsel-basic.png +0 -0
- pdflinkcheck/data/themes/forest/forest-light/check-unsel-hover.png +0 -0
- pdflinkcheck/data/themes/forest/forest-light/check-unsel-pressed.png +0 -0
- pdflinkcheck/data/themes/forest/forest-light/combo-button-basic.png +0 -0
- pdflinkcheck/data/themes/forest/forest-light/combo-button-focus.png +0 -0
- pdflinkcheck/data/themes/forest/forest-light/combo-button-hover.png +0 -0
- pdflinkcheck/data/themes/forest/forest-light/down-focus.png +0 -0
- pdflinkcheck/data/themes/forest/forest-light/down.png +0 -0
- pdflinkcheck/data/themes/forest/forest-light/empty.png +0 -0
- pdflinkcheck/data/themes/forest/forest-light/hor-accent.png +0 -0
- pdflinkcheck/data/themes/forest/forest-light/hor-basic.png +0 -0
- pdflinkcheck/data/themes/forest/forest-light/hor-hover.png +0 -0
- pdflinkcheck/data/themes/forest/forest-light/notebook.png +0 -0
- pdflinkcheck/data/themes/forest/forest-light/off-accent.png +0 -0
- pdflinkcheck/data/themes/forest/forest-light/off-basic.png +0 -0
- pdflinkcheck/data/themes/forest/forest-light/off-hover.png +0 -0
- pdflinkcheck/data/themes/forest/forest-light/on-accent.png +0 -0
- pdflinkcheck/data/themes/forest/forest-light/on-basic.png +0 -0
- pdflinkcheck/data/themes/forest/forest-light/on-hover.png +0 -0
- pdflinkcheck/data/themes/forest/forest-light/radio-accent.png +0 -0
- pdflinkcheck/data/themes/forest/forest-light/radio-basic.png +0 -0
- pdflinkcheck/data/themes/forest/forest-light/radio-hover.png +0 -0
- pdflinkcheck/data/themes/forest/forest-light/radio-tri-accent.png +0 -0
- pdflinkcheck/data/themes/forest/forest-light/radio-tri-basic.png +0 -0
- pdflinkcheck/data/themes/forest/forest-light/radio-tri-hover.png +0 -0
- pdflinkcheck/data/themes/forest/forest-light/radio-unsel-accent.png +0 -0
- pdflinkcheck/data/themes/forest/forest-light/radio-unsel-basic.png +0 -0
- pdflinkcheck/data/themes/forest/forest-light/radio-unsel-hover.png +0 -0
- pdflinkcheck/data/themes/forest/forest-light/radio-unsel-pressed.png +0 -0
- pdflinkcheck/data/themes/forest/forest-light/rect-accent-hover.png +0 -0
- pdflinkcheck/data/themes/forest/forest-light/rect-accent.png +0 -0
- pdflinkcheck/data/themes/forest/forest-light/rect-basic.png +0 -0
- pdflinkcheck/data/themes/forest/forest-light/rect-hover.png +0 -0
- pdflinkcheck/data/themes/forest/forest-light/right-focus.png +0 -0
- pdflinkcheck/data/themes/forest/forest-light/right.png +0 -0
- pdflinkcheck/data/themes/forest/forest-light/scale-hor.png +0 -0
- pdflinkcheck/data/themes/forest/forest-light/scale-vert.png +0 -0
- pdflinkcheck/data/themes/forest/forest-light/separator.png +0 -0
- pdflinkcheck/data/themes/forest/forest-light/sizegrip.png +0 -0
- pdflinkcheck/data/themes/forest/forest-light/spin-button-down-basic.png +0 -0
- pdflinkcheck/data/themes/forest/forest-light/spin-button-down-focus.png +0 -0
- pdflinkcheck/data/themes/forest/forest-light/spin-button-up.png +0 -0
- pdflinkcheck/data/themes/forest/forest-light/tab-accent.png +0 -0
- pdflinkcheck/data/themes/forest/forest-light/tab-basic.png +0 -0
- pdflinkcheck/data/themes/forest/forest-light/tab-hover.png +0 -0
- pdflinkcheck/data/themes/forest/forest-light/thumb-hor-accent.png +0 -0
- pdflinkcheck/data/themes/forest/forest-light/thumb-hor-basic.png +0 -0
- pdflinkcheck/data/themes/forest/forest-light/thumb-hor-hover.png +0 -0
- pdflinkcheck/data/themes/forest/forest-light/thumb-vert-accent.png +0 -0
- pdflinkcheck/data/themes/forest/forest-light/thumb-vert-basic.png +0 -0
- pdflinkcheck/data/themes/forest/forest-light/thumb-vert-hover.png +0 -0
- pdflinkcheck/data/themes/forest/forest-light/tree-basic.png +0 -0
- pdflinkcheck/data/themes/forest/forest-light/tree-pressed.png +0 -0
- pdflinkcheck/data/themes/forest/forest-light/up.png +0 -0
- pdflinkcheck/data/themes/forest/forest-light/vert-accent.png +0 -0
- pdflinkcheck/data/themes/forest/forest-light/vert-basic.png +0 -0
- pdflinkcheck/data/themes/forest/forest-light/vert-hover.png +0 -0
- pdflinkcheck/data/themes/forest/forest-light.tcl +544 -0
- pdflinkcheck/datacopy.py +2 -0
- pdflinkcheck/dev.py +10 -23
- pdflinkcheck/environment.py +64 -0
- pdflinkcheck/gui.py +229 -103
- pdflinkcheck/io.py +4 -18
- pdflinkcheck/report.py +148 -78
- pdflinkcheck/stdlib_server.py +14 -6
- pdflinkcheck/update_msix_version.py +47 -0
- pdflinkcheck/validate.py +50 -73
- pdflinkcheck/version_info.py +5 -2
- {pdflinkcheck-1.1.73.dist-info → pdflinkcheck-1.1.94.dist-info}/METADATA +54 -52
- pdflinkcheck-1.1.94.dist-info/RECORD +176 -0
- pdflinkcheck-1.1.94.dist-info/licenses/LICENSE +24 -0
- pdflinkcheck-1.1.94.dist-info/licenses/LICENSE-MIT +9 -0
- pdflinkcheck-1.1.73.dist-info/RECORD +0 -21
- {pdflinkcheck-1.1.73.dist-info → pdflinkcheck-1.1.94.dist-info}/WHEEL +0 -0
- {pdflinkcheck-1.1.73.dist-info → pdflinkcheck-1.1.94.dist-info}/entry_points.txt +0 -0
- /pdflinkcheck-1.1.73.dist-info/licenses/LICENSE → /pdflinkcheck-1.1.94.dist-info/licenses/LICENSE-AGPL3 +0 -0
pdflinkcheck/gui.py
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
# SPDX-License-Identifier: MIT
|
|
1
3
|
# src/pdflinkcheck/gui.py
|
|
2
4
|
import tkinter as tk
|
|
3
5
|
from tkinter import filedialog, ttk, messagebox # Added messagebox
|
|
@@ -7,20 +9,15 @@ from typing import Optional # Added Optional
|
|
|
7
9
|
import unicodedata
|
|
8
10
|
from importlib.resources import files
|
|
9
11
|
import pyhabitat
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
sv_ttk.set_theme("light")
|
|
15
|
-
except Exception:
|
|
16
|
-
# Theme not available in bundle — use default
|
|
17
|
-
pass
|
|
18
|
-
"""
|
|
12
|
+
import ctypes
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
|
|
19
16
|
# Import the core analysis function
|
|
20
|
-
from pdflinkcheck.report import
|
|
21
|
-
from pdflinkcheck.validate import run_validation
|
|
17
|
+
from pdflinkcheck.report import run_report_and_call_exports
|
|
22
18
|
from pdflinkcheck.version_info import get_version_from_pyproject
|
|
23
19
|
from pdflinkcheck.io import get_first_pdf_in_cwd, get_friendly_path, PDFLINKCHECK_HOME
|
|
20
|
+
from pdflinkcheck.environment import pymupdf_is_available, clear_all_caches, is_in_git_repo
|
|
24
21
|
|
|
25
22
|
class RedirectText:
|
|
26
23
|
"""A class to redirect sys.stdout messages to a Tkinter Text widget."""
|
|
@@ -37,30 +34,79 @@ class RedirectText:
|
|
|
37
34
|
"""Required for file-like objects, but does nothing here."""
|
|
38
35
|
pass
|
|
39
36
|
|
|
37
|
+
|
|
40
38
|
class PDFLinkCheckerApp(tk.Tk):
|
|
39
|
+
|
|
40
|
+
def _initialize_forest_theme(self):
|
|
41
|
+
from importlib.resources import files
|
|
42
|
+
# Path to pdflinkcheck/data/themes/forest/
|
|
43
|
+
theme_dir = files("pdflinkcheck.data.themes.forest")
|
|
44
|
+
# Load the theme files
|
|
45
|
+
self.tk.call("source", str(theme_dir / f"forest-light.tcl"))
|
|
46
|
+
self.tk.call("source", str(theme_dir / f"forest-dark.tcl"))
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def _toggle_theme(self):
|
|
51
|
+
# You could instead assign the dark to light of a single theme here
|
|
52
|
+
"""
|
|
53
|
+
Calls light/dark toggle for the forest theme with self._toggle_theme_just_forest()
|
|
54
|
+
"""
|
|
55
|
+
return self._toggle_theme_forest()
|
|
56
|
+
|
|
57
|
+
def _toggle_theme_forest(self):
|
|
58
|
+
if ttk.Style().theme_use() == "forest-light":
|
|
59
|
+
ttk.Style().theme_use("forest-dark")
|
|
60
|
+
elif ttk.Style().theme_use() == "forest-dark":
|
|
61
|
+
ttk.Style().theme_use("forest-light")
|
|
62
|
+
|
|
63
|
+
def _set_icon(self):
|
|
64
|
+
from importlib.resources import files
|
|
65
|
+
# Path to pdflinkcheck/data/icons/
|
|
66
|
+
icon_dir = files("pdflinkcheck.data.icons")
|
|
67
|
+
# Convert to a real filesystem path
|
|
68
|
+
icon_path = icon_dir.joinpath("red_pdf_128px.ico")
|
|
69
|
+
icon_path = icon_dir.joinpath("red_pdf_512px.ico")
|
|
70
|
+
|
|
71
|
+
self.iconbitmap(str(icon_path))
|
|
72
|
+
|
|
41
73
|
def __init__(self):
|
|
42
74
|
super().__init__()
|
|
43
|
-
|
|
44
|
-
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
self._initialize_forest_theme() # load but do not set internally
|
|
45
78
|
|
|
46
|
-
#
|
|
47
|
-
|
|
48
|
-
|
|
79
|
+
ttk.Style().theme_use("forest-dark") # but if you use _toggle_theme_just_forest(), then you had better do this
|
|
80
|
+
|
|
81
|
+
if is_in_git_repo() and not pyhabitat.as_pyinstaller() and not pyhabitat.is_pyz():
|
|
82
|
+
# Checking for PYZ is overkill, because a PYZ s not expected to carry a .git directory, which is a check that is already completed.
|
|
83
|
+
title_suffix = ""# " [Development]"
|
|
84
|
+
else:
|
|
85
|
+
title_suffix = ""
|
|
86
|
+
|
|
87
|
+
self.title(f"PDF Link Check v{get_version_from_pyproject()}{title_suffix}")
|
|
88
|
+
self.geometry("800x600")
|
|
89
|
+
|
|
90
|
+
self._set_icon()
|
|
91
|
+
|
|
49
92
|
|
|
50
93
|
# --- 1. Initialize Variables ---
|
|
51
94
|
self.pdf_path = tk.StringVar(value="")
|
|
52
95
|
self.pdf_library_var = tk.StringVar(value="PyMuPDF")
|
|
53
|
-
#self.pdf_library_var.set("PyMuPDF")
|
|
54
96
|
self.max_links_var = tk.StringVar(value="50")
|
|
55
97
|
self.show_all_links_var = tk.BooleanVar(value=True)
|
|
56
98
|
self.do_export_report_json_var = tk.BooleanVar(value=True)
|
|
57
|
-
self.do_export_report_txt_var = tk.BooleanVar(value=
|
|
99
|
+
self.do_export_report_txt_var = tk.BooleanVar(value=True)
|
|
58
100
|
self.current_report_text = None
|
|
59
101
|
self.current_report_data = None
|
|
60
102
|
|
|
61
103
|
self.supported_export_formats = ["JSON", "MD", "TXT"]
|
|
62
104
|
self.supported_export_formats = ["JSON"]
|
|
63
|
-
|
|
105
|
+
|
|
106
|
+
if not pymupdf_is_available():
|
|
107
|
+
print(f"pymupdf_is_available: {pymupdf_is_available()}")
|
|
108
|
+
self.pdf_library_var.set("pypdf")
|
|
109
|
+
|
|
64
110
|
|
|
65
111
|
# --- 2. Create Widgets ---
|
|
66
112
|
self._create_widgets()
|
|
@@ -69,7 +115,23 @@ class PDFLinkCheckerApp(tk.Tk):
|
|
|
69
115
|
self._toggle_max_links_entry()
|
|
70
116
|
self._toggle_json_export()
|
|
71
117
|
self._toggle_txt_export()
|
|
72
|
-
|
|
118
|
+
|
|
119
|
+
# --- Menubar with dropdown ---
|
|
120
|
+
menubar = tk.Menu(self)
|
|
121
|
+
self.config(menu=menubar)
|
|
122
|
+
|
|
123
|
+
# Tools menu
|
|
124
|
+
tools_menu = tk.Menu(menubar, tearoff=0)
|
|
125
|
+
menubar.add_cascade(label="Tools", menu=tools_menu)
|
|
126
|
+
|
|
127
|
+
tools_menu.add_command(label="Toggle Theme", command=self._toggle_theme)
|
|
128
|
+
tools_menu.add_command(label="Clear Cache", command=self._clear_all_caches)
|
|
129
|
+
|
|
130
|
+
# Add existing License/Readme to tools menu
|
|
131
|
+
tools_menu.add_separator()
|
|
132
|
+
tools_menu.add_command(label="License", command=self._show_license)
|
|
133
|
+
tools_menu.add_command(label="Readme", command=self._show_readme)
|
|
134
|
+
tools_menu.add_command(label="I Have Questions", command=self._show_i_have_questions)
|
|
73
135
|
# In class PDFLinkCheckerApp:
|
|
74
136
|
|
|
75
137
|
def _copy_pdf_path(self):
|
|
@@ -98,22 +160,43 @@ class PDFLinkCheckerApp(tk.Tk):
|
|
|
98
160
|
"""Scrolls the output text widget to the bottom."""
|
|
99
161
|
self.output_text.see(tk.END) # tk.END is the index for the position just after the last character
|
|
100
162
|
|
|
163
|
+
def _clear_all_caches(self):
|
|
164
|
+
"""Clear caches and show confirmation."""
|
|
165
|
+
clear_all_caches()
|
|
166
|
+
messagebox.showinfo("Caches Cleared", f"All caches have been cleared.\nPyMuPDF available: {pymupdf_is_available()}")
|
|
167
|
+
|
|
101
168
|
def _show_license(self):
|
|
102
169
|
"""
|
|
103
170
|
Reads the embedded LICENSE file (AGPLv3) and displays its content in a new modal window.
|
|
104
171
|
"""
|
|
105
172
|
try:
|
|
106
|
-
#
|
|
173
|
+
# Use the Traversable object's read_text() method.
|
|
107
174
|
# This handles files located inside zip archives (.pyz, pipx venvs) correctly.
|
|
108
175
|
license_path_traversable = files("pdflinkcheck.data") / "LICENSE"
|
|
109
176
|
license_content = license_path_traversable.read_text(encoding="utf-8")
|
|
110
177
|
|
|
111
178
|
except FileNotFoundError:
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
179
|
+
if is_in_git_repo():
|
|
180
|
+
messagebox.showinfo(
|
|
181
|
+
"Local Development Mode",
|
|
182
|
+
"Embedded data files not found – copying from root..."
|
|
183
|
+
)
|
|
184
|
+
try:
|
|
185
|
+
from pdflinkcheck.datacopy import ensure_package_license, ensure_data_files_for_build
|
|
186
|
+
#ensure_package_license()
|
|
187
|
+
ensure_data_files_for_build()
|
|
188
|
+
# Retry display
|
|
189
|
+
self._show_license()
|
|
190
|
+
return
|
|
191
|
+
except Exception as e:
|
|
192
|
+
messagebox.showerror("Copy Failed", f"Could not copy file: {e}")
|
|
193
|
+
else:
|
|
194
|
+
messagebox.showerror(
|
|
195
|
+
"Packaging Error",
|
|
196
|
+
"Embedded file not found. This indicates a problem with the package build/installation."
|
|
197
|
+
)
|
|
116
198
|
return
|
|
199
|
+
|
|
117
200
|
except Exception as e:
|
|
118
201
|
messagebox.showerror("Read Error", f"Failed to read embedded LICENSE file: {e}")
|
|
119
202
|
return
|
|
@@ -146,17 +229,31 @@ class PDFLinkCheckerApp(tk.Tk):
|
|
|
146
229
|
Reads the embedded README.md file and displays its content in a new modal window.
|
|
147
230
|
"""
|
|
148
231
|
try:
|
|
149
|
-
#
|
|
232
|
+
# Use the Traversable object's read_text() method.
|
|
150
233
|
# This handles files located inside zip archives (.pyz, pipx venvs) correctly.
|
|
151
234
|
readme_path_traversable = files("pdflinkcheck.data") / "README.md"
|
|
152
235
|
readme_content = readme_path_traversable.read_text(encoding="utf-8")
|
|
153
236
|
readme_content = sanitize_glyphs_for_tkinter(readme_content)
|
|
154
|
-
|
|
155
237
|
except FileNotFoundError:
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
238
|
+
if is_in_git_repo():
|
|
239
|
+
messagebox.showinfo(
|
|
240
|
+
"Local Development Mode",
|
|
241
|
+
"Embedded data files not found – copying from root..."
|
|
242
|
+
)
|
|
243
|
+
try:
|
|
244
|
+
from pdflinkcheck.datacopy import ensure_package_readme, ensure_data_files_for_build
|
|
245
|
+
#ensure_package_readme()
|
|
246
|
+
ensure_data_files_for_build()
|
|
247
|
+
# Retry display
|
|
248
|
+
self._show_readme()
|
|
249
|
+
return
|
|
250
|
+
except Exception as e:
|
|
251
|
+
messagebox.showerror("Copy Failed", f"Could not copy file: {e}")
|
|
252
|
+
else:
|
|
253
|
+
messagebox.showerror(
|
|
254
|
+
"Packaging Error",
|
|
255
|
+
"Embedded file not found. This indicates a problem with the package build/installation."
|
|
256
|
+
)
|
|
160
257
|
return
|
|
161
258
|
except Exception as e:
|
|
162
259
|
messagebox.showerror("Read Error", f"Failed to read embedded README.md file: {e}")
|
|
@@ -185,6 +282,43 @@ class PDFLinkCheckerApp(tk.Tk):
|
|
|
185
282
|
readme_window.grab_set()
|
|
186
283
|
self.wait_window(readme_window)
|
|
187
284
|
|
|
285
|
+
def _show_i_have_questions(self):
|
|
286
|
+
"""
|
|
287
|
+
Reads the embedded I Have Questions.md file and displays its content in a new modal window.
|
|
288
|
+
"""
|
|
289
|
+
try:
|
|
290
|
+
# Use the Traversable object's read_text() method.
|
|
291
|
+
# This handles files located inside zip archives (.pyz, pipx venvs) correctly.
|
|
292
|
+
i_have_questions_path_traversable = files("pdflinkcheck.data") / "I Have Questions.md"
|
|
293
|
+
i_have_questions_content = i_have_questions_path_traversable.read_text(encoding="utf-8")
|
|
294
|
+
i_have_questions_content = sanitize_glyphs_for_tkinter(i_have_questions_content)
|
|
295
|
+
except FileNotFoundError:
|
|
296
|
+
messagebox.showerror("Read Error", f"Failed to read embedded 'I Have Questions.md' file.")
|
|
297
|
+
return
|
|
298
|
+
|
|
299
|
+
# --- Display in a New Toplevel Window ---
|
|
300
|
+
i_have_questions_window = tk.Toplevel(self)
|
|
301
|
+
i_have_questions_window.title("I Have Questions.md")
|
|
302
|
+
i_have_questions_window.geometry("600x400")
|
|
303
|
+
|
|
304
|
+
# Text widget for content
|
|
305
|
+
text_widget = tk.Text(i_have_questions_window, wrap=tk.WORD, font=('Monospace', 10), padx=10, pady=10)
|
|
306
|
+
text_widget.insert(tk.END, i_have_questions_content)
|
|
307
|
+
text_widget.config(state=tk.DISABLED)
|
|
308
|
+
|
|
309
|
+
# Scrollbar
|
|
310
|
+
scrollbar = ttk.Scrollbar(i_have_questions_window, command=text_widget.yview)
|
|
311
|
+
text_widget['yscrollcommand'] = scrollbar.set
|
|
312
|
+
|
|
313
|
+
# Layout
|
|
314
|
+
scrollbar.pack(side=tk.RIGHT, fill=tk.Y)
|
|
315
|
+
text_widget.pack(fill='both', expand=True)
|
|
316
|
+
|
|
317
|
+
# Make the window modal (optional, but good practice for notices)
|
|
318
|
+
i_have_questions_window.transient(self)
|
|
319
|
+
i_have_questions_window.grab_set()
|
|
320
|
+
self.wait_window(i_have_questions_window)
|
|
321
|
+
|
|
188
322
|
def _create_widgets(self):
|
|
189
323
|
# --- Control Frame (Top) ---
|
|
190
324
|
control_frame = ttk.Frame(self, padding="10")
|
|
@@ -228,7 +362,7 @@ class PDFLinkCheckerApp(tk.Tk):
|
|
|
228
362
|
).pack(side='left', padx=5, pady=1)
|
|
229
363
|
|
|
230
364
|
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=
|
|
365
|
+
self.max_links_entry = ttk.Entry(report_brevity_frame, textvariable=self.max_links_var, width=7)
|
|
232
366
|
self.max_links_entry.pack(side='left', padx=5, pady=5)
|
|
233
367
|
|
|
234
368
|
# --- PDF Library Selection ---
|
|
@@ -274,11 +408,7 @@ class PDFLinkCheckerApp(tk.Tk):
|
|
|
274
408
|
run_analysis_btn = ttk.Button(control_frame, text="▶ Run Analysis", command=self._run_report_gui, style='Accent.TButton')
|
|
275
409
|
run_analysis_btn.grid(row=3, column=0, columnspan=2, pady=10, sticky='ew', padx=(0, 5))
|
|
276
410
|
|
|
277
|
-
|
|
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
|
-
|
|
411
|
+
"""
|
|
282
412
|
# 2. Create a Frame to hold the two file link buttons (This frame goes into column 2)
|
|
283
413
|
info_btn_frame = ttk.Frame(control_frame)
|
|
284
414
|
info_btn_frame.grid(row=3, column=2, columnspan=1, pady=10, sticky='ew', padx=(5, 0))
|
|
@@ -286,13 +416,14 @@ class PDFLinkCheckerApp(tk.Tk):
|
|
|
286
416
|
info_btn_frame.grid_columnconfigure(0, weight=1)
|
|
287
417
|
info_btn_frame.grid_columnconfigure(1, weight=1)
|
|
288
418
|
|
|
289
|
-
# 3.
|
|
290
|
-
|
|
419
|
+
# 3. Placeholder buttons inside the info button frame
|
|
420
|
+
info_1_btn = ttk.Button(info_btn_frame, text="Empty1", command=self._do_stuff_1)
|
|
291
421
|
# Use PACK or a 2-column GRID inside the info_btn_frame. GRID is cleaner here.
|
|
292
|
-
|
|
422
|
+
info_1_btn.grid(row=0, column=0, sticky='ew', padx=(0, 2)) # Left side of the frame
|
|
293
423
|
|
|
294
|
-
|
|
295
|
-
|
|
424
|
+
info_2_btn = ttk.Button(info_btn_frame, text="Empty2", command=self._do_stuff_2)
|
|
425
|
+
info_2_btn.grid(row=0, column=1, sticky='ew', padx=(2, 0)) # Right side of the frame
|
|
426
|
+
"""
|
|
296
427
|
|
|
297
428
|
# Force the columns to distribute space evenly
|
|
298
429
|
control_frame.grid_columnconfigure(0, weight=2)
|
|
@@ -342,7 +473,7 @@ class PDFLinkCheckerApp(tk.Tk):
|
|
|
342
473
|
if self.pdf_path.get():
|
|
343
474
|
initialdir = str(Path(self.pdf_path.get()).parent)
|
|
344
475
|
else:
|
|
345
|
-
initialdir = str(Path.
|
|
476
|
+
initialdir = str(Path.home())
|
|
346
477
|
|
|
347
478
|
file_path = filedialog.askopenfilename(
|
|
348
479
|
initialdir=initialdir,
|
|
@@ -370,6 +501,15 @@ class PDFLinkCheckerApp(tk.Tk):
|
|
|
370
501
|
if self.do_export_report_txt_var.get():
|
|
371
502
|
pass # placeholder # no side effects
|
|
372
503
|
|
|
504
|
+
|
|
505
|
+
|
|
506
|
+
def _toggle_pdf_library(self):
|
|
507
|
+
selected_lib = self.pdf_library_var.get().lower()
|
|
508
|
+
try:
|
|
509
|
+
self.pdf_library_var.set("pypdf" if selected_lib == "pymupdf" else "pymupdf")
|
|
510
|
+
except Exception:
|
|
511
|
+
pass
|
|
512
|
+
|
|
373
513
|
def _assess_pdf_path_str(self):
|
|
374
514
|
pdf_path_str = self.pdf_path.get().strip()
|
|
375
515
|
if not pdf_path_str:
|
|
@@ -399,7 +539,7 @@ class PDFLinkCheckerApp(tk.Tk):
|
|
|
399
539
|
else:
|
|
400
540
|
try:
|
|
401
541
|
max_links_to_pass = int(self.max_links_var.get())
|
|
402
|
-
if max_links_to_pass <
|
|
542
|
+
if max_links_to_pass < 0:
|
|
403
543
|
self._display_error("Error: Max Links must be a positive number (or use 'Show All').")
|
|
404
544
|
return
|
|
405
545
|
except ValueError:
|
|
@@ -427,7 +567,7 @@ class PDFLinkCheckerApp(tk.Tk):
|
|
|
427
567
|
try:
|
|
428
568
|
# 3. Call the core logic function
|
|
429
569
|
#self.output_text.insert(tk.END, "--- Starting Analysis ---\n")
|
|
430
|
-
report_results =
|
|
570
|
+
report_results = run_report_and_call_exports(
|
|
431
571
|
pdf_path=pdf_path_str,
|
|
432
572
|
max_links=max_links_to_pass,
|
|
433
573
|
export_format=export_format,
|
|
@@ -440,67 +580,44 @@ class PDFLinkCheckerApp(tk.Tk):
|
|
|
440
580
|
|
|
441
581
|
except Exception as e:
|
|
442
582
|
# Inform the user in the GUI with a clean message
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
pdf_path_str = self._assess_pdf_path_str()
|
|
453
|
-
if not pdf_path_str:
|
|
454
|
-
return
|
|
455
|
-
|
|
456
|
-
pdf_library = self._discern_pdf_library()
|
|
457
|
-
|
|
458
|
-
# 1. Clear previous output and enable editing
|
|
459
|
-
self.output_text.config(state=tk.NORMAL)
|
|
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
|
|
583
|
+
messagebox.showinfo( # Changed from showwarning – less alarming
|
|
584
|
+
"PyMuPDF Not Available",
|
|
585
|
+
"PyMuPDF is not installed or not working.\n"
|
|
586
|
+
"Switched to pypdf engine automatically.\n\n"
|
|
587
|
+
"To use the faster PyMuPDF engine:\n"
|
|
588
|
+
"1. Install it: pip install pymupdf\n"
|
|
589
|
+
"2. Go to Tools → Clear Cache\n"
|
|
590
|
+
"3. Run analysis again"
|
|
479
591
|
)
|
|
480
|
-
self.
|
|
481
|
-
|
|
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
|
-
|
|
592
|
+
self._toggle_pdf_library()
|
|
593
|
+
|
|
594
|
+
|
|
489
595
|
finally:
|
|
490
596
|
# 4. Restore standard output and disable editing
|
|
491
597
|
sys.stdout = original_stdout
|
|
492
598
|
self.output_text.config(state=tk.DISABLED)
|
|
493
|
-
|
|
494
|
-
|
|
599
|
+
# -- This call to _toggle_pdf_library() fails currently --
|
|
600
|
+
|
|
495
601
|
def _discern_pdf_library(self):
|
|
496
602
|
selected_lib = self.pdf_library_var.get().lower()
|
|
497
603
|
|
|
498
604
|
if selected_lib == "pymupdf":
|
|
499
|
-
|
|
605
|
+
self._display_msg("Using high-speed PyMuPDF engine.")
|
|
500
606
|
elif selected_lib == "pypdf":
|
|
501
|
-
|
|
607
|
+
self._display_msg("Using pure-python pypdf engine.")
|
|
502
608
|
return selected_lib
|
|
503
609
|
|
|
610
|
+
def _display_msg(self, message):
|
|
611
|
+
# Ensure output is in normal state to write
|
|
612
|
+
original_state = self.output_text.cget('state')
|
|
613
|
+
if original_state == tk.DISABLED:
|
|
614
|
+
self.output_text.config(state=tk.NORMAL)
|
|
615
|
+
|
|
616
|
+
#self.output_text.delete('1.0', tk.END)
|
|
617
|
+
self.output_text.insert(tk.END, f"{message}\n", 'msg')
|
|
618
|
+
self.output_text.tag_config('msg')#, foreground='red')
|
|
619
|
+
self.output_text.see(tk.END)
|
|
620
|
+
|
|
504
621
|
def _display_error(self, message):
|
|
505
622
|
# Ensure output is in normal state to write
|
|
506
623
|
original_state = self.output_text.cget('state')
|
|
@@ -536,15 +653,7 @@ class PDFLinkCheckerApp(tk.Tk):
|
|
|
536
653
|
|
|
537
654
|
except Exception as e:
|
|
538
655
|
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
|
-
"""
|
|
656
|
+
|
|
548
657
|
def sanitize_glyphs_for_tkinter(text: str) -> str:
|
|
549
658
|
"""
|
|
550
659
|
Converts complex Unicode characters (like emojis and symbols)
|
|
@@ -574,13 +683,30 @@ def auto_close_window(root, delay_ms:int = 0):
|
|
|
574
683
|
return
|
|
575
684
|
|
|
576
685
|
|
|
686
|
+
|
|
577
687
|
def start_gui(time_auto_close:int=0):
|
|
578
688
|
"""
|
|
579
689
|
Entry point function to launch the application.
|
|
580
690
|
"""
|
|
581
691
|
print("pdflinkcheck: start_gui ...")
|
|
692
|
+
|
|
582
693
|
tk_app = PDFLinkCheckerApp()
|
|
583
694
|
|
|
695
|
+
# Bring window to front
|
|
696
|
+
tk_app.lift()
|
|
697
|
+
#tk_app.attributes('-topmost', True)
|
|
698
|
+
#tk_app.after(100, lambda: tk_app.attributes('-topmost', False))
|
|
699
|
+
tk_app.wm_attributes("-topmost", True)
|
|
700
|
+
tk_app.after(200, lambda: tk_app.wm_attributes("-topmost", False))
|
|
701
|
+
tk_app.deiconify()
|
|
702
|
+
tk_app.focus_force()
|
|
703
|
+
|
|
704
|
+
# Win32 nudge (optional but helpful)
|
|
705
|
+
if pyhabitat.on_windows():
|
|
706
|
+
hwnd = tk_app.winfo_id()
|
|
707
|
+
ctypes.windll.user32.SetForegroundWindow(hwnd)
|
|
708
|
+
|
|
709
|
+
# Ths is called in the CLI by the --auto-close flag value, for CI scripted testing purposes (like in .github/workflows/build.yml)
|
|
584
710
|
auto_close_window(tk_app, time_auto_close)
|
|
585
711
|
|
|
586
712
|
tk_app.mainloop()
|
pdflinkcheck/io.py
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
# SPDX-License-Identifier: MIT
|
|
1
3
|
# src/pdflinkcheck/io.py
|
|
2
4
|
import logging
|
|
3
5
|
import json
|
|
@@ -112,6 +114,7 @@ def export_report_json(
|
|
|
112
114
|
pdf_library: str
|
|
113
115
|
) -> Path:
|
|
114
116
|
"""Exports structured dictionary results to a .json file."""
|
|
117
|
+
|
|
115
118
|
base_name = Path(pdf_filename).stem
|
|
116
119
|
output_path = PDFLINKCHECK_HOME / f"{base_name}_{pdf_library}_report.json"
|
|
117
120
|
|
|
@@ -130,6 +133,7 @@ def export_report_txt(
|
|
|
130
133
|
pdf_library: str
|
|
131
134
|
) -> Path:
|
|
132
135
|
"""Exports the formatted string buffer to a .txt file."""
|
|
136
|
+
#pdf_filename = implement_non_redundant_naming(pdf_filename)
|
|
133
137
|
base_name = Path(pdf_filename).stem
|
|
134
138
|
output_path = PDFLINKCHECK_HOME / f"{base_name}_{pdf_library}_report.txt"
|
|
135
139
|
|
|
@@ -141,24 +145,6 @@ def export_report_txt(
|
|
|
141
145
|
error_logger.error(f"TXT export failed: {e}", exc_info=True)
|
|
142
146
|
raise RuntimeError(f"TXT export failed: {e}")
|
|
143
147
|
|
|
144
|
-
def export_validation_json(
|
|
145
|
-
report_data: Dict[str, Any],
|
|
146
|
-
pdf_filename: str,
|
|
147
|
-
pdf_library: str
|
|
148
|
-
) -> Path:
|
|
149
|
-
"""Exports structured dictionary validation results to a .json file."""
|
|
150
|
-
base_name = Path(pdf_filename).stem
|
|
151
|
-
output_path = PDFLINKCHECK_HOME / f"{base_name}_{pdf_library}_validation.json"
|
|
152
|
-
|
|
153
|
-
try:
|
|
154
|
-
with open(output_path, 'w', encoding='utf-8') as f:
|
|
155
|
-
json.dump(report_data, f, indent=4)
|
|
156
|
-
print(f"\nJSON validation exported: {get_friendly_path(output_path)}")
|
|
157
|
-
return output_path
|
|
158
|
-
except Exception as e:
|
|
159
|
-
error_logger.error(f"JSON validation export failed: {e}", exc_info=True)
|
|
160
|
-
raise RuntimeError(f"JSON validation export failed: {e}")
|
|
161
|
-
|
|
162
148
|
# --- helpers ---
|
|
163
149
|
def get_friendly_path(full_path: str) -> str:
|
|
164
150
|
p = Path(full_path).resolve()
|