rgpdf 0.0.1__tar.gz

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.
rgpdf-0.0.1/PKG-INFO ADDED
@@ -0,0 +1,75 @@
1
+ Metadata-Version: 2.3
2
+ Name: rgpdf
3
+ Version: 0.0.1
4
+ Summary: Cross-platform PDF text and regular-expression search desktop app
5
+ Requires-Dist: pymupdf>=1.25
6
+ Requires-Dist: pyside6>=6.8
7
+ Requires-Dist: regex>=2024.11.6
8
+ Requires-Python: >=3.12, <3.14
9
+ Project-URL: Homepage, https://github.com/codemee/rgpdf
10
+ Project-URL: Issues, https://github.com/codemee/rgpdf/issues
11
+ Project-URL: Repository, https://github.com/codemee/rgpdf
12
+ Description-Content-Type: text/markdown
13
+
14
+ # rgpdf
15
+
16
+ [繁體中文](README.zh-TW.md) · English
17
+
18
+ rgpdf is a cross-platform desktop application for searching text-layer PDFs with plain text or regular expressions. Matches appear as they are found and can be opened in an asynchronous page preview with precise, configurable highlighting.
19
+
20
+ ## Features
21
+
22
+ - Search multiple PDFs in a folder, optionally including subfolders.
23
+ - Plain-text and regular-expression modes with optional case sensitivity.
24
+ - Matches across lines and page boundaries, including line-end dehyphenation.
25
+ - Incremental per-match results without blocking the interface.
26
+ - Three coordinated views for files, matching passages, and PDF page preview.
27
+ - Per-character highlight geometry and configurable highlight colors.
28
+ - English and Traditional Chinese interfaces, plus system/light/dark themes.
29
+ - Persistent search and appearance preferences.
30
+
31
+ rgpdf searches existing PDF text layers. It does not perform OCR or modify source files.
32
+
33
+ ## Install the latest release
34
+
35
+ Install [uv](https://docs.astral.sh/uv/), then install the latest release from PyPI:
36
+
37
+ ```shell
38
+ uv tool install rgpdf
39
+ ```
40
+
41
+ Run the application:
42
+
43
+ ```shell
44
+ rgpdf
45
+ ```
46
+
47
+ To update an existing installation:
48
+
49
+ ```shell
50
+ uv tool upgrade rgpdf
51
+ ```
52
+
53
+ Windows and macOS are supported; uv manages the required Python environment.
54
+
55
+ To install the latest source release directly from GitHub instead, use the moving `latest` tag:
56
+
57
+ ```shell
58
+ uv tool install "git+https://github.com/codemee/rgpdf.git@latest"
59
+ ```
60
+
61
+ ## Development
62
+
63
+ ```shell
64
+ git clone https://github.com/codemee/rgpdf.git
65
+ cd rgpdf
66
+ uv sync
67
+ uv run pytest
68
+ uv run rgpdf
69
+ ```
70
+
71
+ See [Technical details](docs/TECHNICAL.md) for architecture, matching semantics, concurrency, and release engineering. Version history is in the [changelog](CHANGELOG.md).
72
+
73
+ ## Releases
74
+
75
+ Immutable releases use version tags such as `v0.0.1`. The movable `latest` Git tag always points to the newest published source release. PyPI provides the canonical packaged release.
rgpdf-0.0.1/README.md ADDED
@@ -0,0 +1,62 @@
1
+ # rgpdf
2
+
3
+ [繁體中文](README.zh-TW.md) · English
4
+
5
+ rgpdf is a cross-platform desktop application for searching text-layer PDFs with plain text or regular expressions. Matches appear as they are found and can be opened in an asynchronous page preview with precise, configurable highlighting.
6
+
7
+ ## Features
8
+
9
+ - Search multiple PDFs in a folder, optionally including subfolders.
10
+ - Plain-text and regular-expression modes with optional case sensitivity.
11
+ - Matches across lines and page boundaries, including line-end dehyphenation.
12
+ - Incremental per-match results without blocking the interface.
13
+ - Three coordinated views for files, matching passages, and PDF page preview.
14
+ - Per-character highlight geometry and configurable highlight colors.
15
+ - English and Traditional Chinese interfaces, plus system/light/dark themes.
16
+ - Persistent search and appearance preferences.
17
+
18
+ rgpdf searches existing PDF text layers. It does not perform OCR or modify source files.
19
+
20
+ ## Install the latest release
21
+
22
+ Install [uv](https://docs.astral.sh/uv/), then install the latest release from PyPI:
23
+
24
+ ```shell
25
+ uv tool install rgpdf
26
+ ```
27
+
28
+ Run the application:
29
+
30
+ ```shell
31
+ rgpdf
32
+ ```
33
+
34
+ To update an existing installation:
35
+
36
+ ```shell
37
+ uv tool upgrade rgpdf
38
+ ```
39
+
40
+ Windows and macOS are supported; uv manages the required Python environment.
41
+
42
+ To install the latest source release directly from GitHub instead, use the moving `latest` tag:
43
+
44
+ ```shell
45
+ uv tool install "git+https://github.com/codemee/rgpdf.git@latest"
46
+ ```
47
+
48
+ ## Development
49
+
50
+ ```shell
51
+ git clone https://github.com/codemee/rgpdf.git
52
+ cd rgpdf
53
+ uv sync
54
+ uv run pytest
55
+ uv run rgpdf
56
+ ```
57
+
58
+ See [Technical details](docs/TECHNICAL.md) for architecture, matching semantics, concurrency, and release engineering. Version history is in the [changelog](CHANGELOG.md).
59
+
60
+ ## Releases
61
+
62
+ Immutable releases use version tags such as `v0.0.1`. The movable `latest` Git tag always points to the newest published source release. PyPI provides the canonical packaged release.
@@ -0,0 +1,33 @@
1
+ [project]
2
+ name = "rgpdf"
3
+ version = "0.0.1"
4
+ description = "Cross-platform PDF text and regular-expression search desktop app"
5
+ readme = "README.md"
6
+ requires-python = ">=3.12,<3.14"
7
+ dependencies = [
8
+ "pymupdf>=1.25",
9
+ "pyside6>=6.8",
10
+ "regex>=2024.11.6",
11
+ ]
12
+
13
+ [project.urls]
14
+ Homepage = "https://github.com/codemee/rgpdf"
15
+ Repository = "https://github.com/codemee/rgpdf"
16
+ Issues = "https://github.com/codemee/rgpdf/issues"
17
+
18
+ [project.scripts]
19
+ rgpdf = "rgpdf.app:main"
20
+
21
+ [dependency-groups]
22
+ dev = [
23
+ "pytest>=8.3",
24
+ "pytest-qt>=4.4",
25
+ ]
26
+
27
+ [build-system]
28
+ requires = ["uv_build>=0.8,<0.9"]
29
+ build-backend = "uv_build"
30
+
31
+ [tool.pytest.ini_options]
32
+ testpaths = ["tests"]
33
+ addopts = "-q"
@@ -0,0 +1,3 @@
1
+ """rgpdf package."""
2
+
3
+ __version__ = "0.0.1"
@@ -0,0 +1,6 @@
1
+ from rgpdf.app import main
2
+
3
+
4
+ if __name__ == "__main__":
5
+ raise SystemExit(main())
6
+
@@ -0,0 +1,163 @@
1
+ from __future__ import annotations
2
+
3
+ import sys
4
+
5
+ from PySide6.QtCore import QSettings, Qt
6
+ from PySide6.QtGui import QColor, QPalette
7
+ from PySide6.QtWidgets import QApplication
8
+
9
+ from rgpdf.main_window import MainWindow
10
+ from rgpdf.resources import application_icon
11
+
12
+
13
+ BASE_STYLE = """
14
+ QWidget { font-size: 13px; color: palette(window-text); }
15
+ QMainWindow, QWidget#centralWidget, QMenuBar, QMenu { background: palette(window); }
16
+ QToolBar#settingsToolbar {
17
+ background: palette(alternate-base);
18
+ border: 0;
19
+ border-bottom: 1px solid palette(mid);
20
+ padding: 2px 7px;
21
+ spacing: 1px;
22
+ }
23
+ QToolBar#settingsToolbar::separator {
24
+ background: palette(mid);
25
+ width: 1px;
26
+ margin: 7px 6px;
27
+ }
28
+ QLineEdit, QTreeWidget, QScrollArea {
29
+ border: 1px solid palette(mid);
30
+ border-radius: 5px;
31
+ padding: 5px;
32
+ background: palette(base);
33
+ }
34
+ QToolButton {
35
+ border: 0;
36
+ border-radius: 8px;
37
+ background: transparent;
38
+ }
39
+ QToolButton:hover { background: palette(midlight); }
40
+ QToolButton:pressed { background: palette(mid); }
41
+ QToolButton:checked {
42
+ background: palette(midlight);
43
+ border: 1px solid palette(mid);
44
+ }
45
+ QPushButton {
46
+ border: 0;
47
+ border-radius: 7px;
48
+ padding: 6px 12px;
49
+ background: palette(button);
50
+ }
51
+ QPushButton:hover { background: palette(midlight); }
52
+ QPushButton:pressed { background: palette(midlight); }
53
+ QPushButton:disabled { color: palette(mid); }
54
+ QGroupBox {
55
+ border: 1px solid palette(mid);
56
+ border-radius: 7px;
57
+ margin-top: 10px;
58
+ padding-top: 7px;
59
+ font-weight: 600;
60
+ }
61
+ QGroupBox::title { subcontrol-origin: margin; left: 9px; padding: 0 4px; }
62
+ QFrame#highlightPanel {
63
+ background: palette(alternate-base);
64
+ border: 1px solid palette(mid);
65
+ border-radius: 6px;
66
+ }
67
+ QFrame#searchArea {
68
+ background: palette(alternate-base);
69
+ border: 1px solid palette(mid);
70
+ border-radius: 9px;
71
+ }
72
+ QHeaderView::section {
73
+ background: palette(alternate-base);
74
+ color: palette(text);
75
+ border: 0;
76
+ border-bottom: 1px solid palette(mid);
77
+ padding: 5px 7px;
78
+ font-weight: 600;
79
+ }
80
+ QProgressBar { border: 0; border-radius: 3px; text-align: center; min-width: 180px; }
81
+ QProgressBar::chunk { background: palette(highlight); border-radius: 3px; }
82
+ QSplitter::handle { background: transparent; }
83
+ """
84
+
85
+
86
+ def _light_palette(app: QApplication) -> QPalette:
87
+ palette = app.style().standardPalette()
88
+ palette.setColor(QPalette.ColorRole.Window, QColor("#f5f6f8"))
89
+ palette.setColor(QPalette.ColorRole.Base, QColor("#ffffff"))
90
+ palette.setColor(QPalette.ColorRole.AlternateBase, QColor("#eef0f3"))
91
+ palette.setColor(QPalette.ColorRole.Button, QColor("#ffffff"))
92
+ palette.setColor(QPalette.ColorRole.ButtonText, QColor("#475569"))
93
+ palette.setColor(QPalette.ColorRole.Mid, QColor("#c8ced8"))
94
+ palette.setColor(QPalette.ColorRole.Midlight, QColor("#e2e6ed"))
95
+ palette.setColor(QPalette.ColorRole.PlaceholderText, QColor("#718096"))
96
+ palette.setColor(QPalette.ColorRole.Highlight, QColor("#3578e5"))
97
+ palette.setColor(QPalette.ColorRole.HighlightedText, QColor("#ffffff"))
98
+ return palette
99
+
100
+
101
+ def _dark_palette() -> QPalette:
102
+ palette = QPalette()
103
+ palette.setColor(QPalette.ColorRole.Window, QColor("#202124"))
104
+ palette.setColor(QPalette.ColorRole.WindowText, QColor("#eceff4"))
105
+ palette.setColor(QPalette.ColorRole.Base, QColor("#292b2f"))
106
+ palette.setColor(QPalette.ColorRole.AlternateBase, QColor("#32353a"))
107
+ palette.setColor(QPalette.ColorRole.ToolTipBase, QColor("#292b2f"))
108
+ palette.setColor(QPalette.ColorRole.ToolTipText, QColor("#eceff4"))
109
+ palette.setColor(QPalette.ColorRole.Text, QColor("#eceff4"))
110
+ palette.setColor(QPalette.ColorRole.Button, QColor("#303236"))
111
+ palette.setColor(QPalette.ColorRole.ButtonText, QColor("#eceff4"))
112
+ palette.setColor(QPalette.ColorRole.BrightText, QColor("#ffffff"))
113
+ palette.setColor(QPalette.ColorRole.Link, QColor("#78a9ff"))
114
+ palette.setColor(QPalette.ColorRole.Highlight, QColor("#4f8ef7"))
115
+ palette.setColor(QPalette.ColorRole.HighlightedText, QColor("#ffffff"))
116
+ palette.setColor(QPalette.ColorRole.Mid, QColor("#55585e"))
117
+ palette.setColor(QPalette.ColorRole.Midlight, QColor("#45484d"))
118
+ palette.setColor(QPalette.ColorRole.PlaceholderText, QColor("#aeb6c4"))
119
+ palette.setColor(QPalette.ColorGroup.Disabled, QPalette.ColorRole.WindowText, QColor("#777b82"))
120
+ palette.setColor(QPalette.ColorGroup.Disabled, QPalette.ColorRole.Text, QColor("#777b82"))
121
+ palette.setColor(QPalette.ColorGroup.Disabled, QPalette.ColorRole.ButtonText, QColor("#777b82"))
122
+ return palette
123
+
124
+
125
+ def apply_application_theme(app: QApplication, theme: str) -> None:
126
+ selected = theme
127
+ if theme == "system":
128
+ scheme = app.styleHints().colorScheme()
129
+ selected = "dark" if scheme == Qt.ColorScheme.Dark else "light"
130
+ app.setPalette(_dark_palette() if selected == "dark" else _light_palette(app))
131
+ # Qt caches palette() values referenced by stylesheets, so force a full repolish.
132
+ app.setStyleSheet("")
133
+ app.setStyleSheet(BASE_STYLE)
134
+ for widget in app.topLevelWidgets():
135
+ widget.style().unpolish(widget)
136
+ widget.style().polish(widget)
137
+ widget.update()
138
+
139
+
140
+ def main() -> int:
141
+ QApplication.setHighDpiScaleFactorRoundingPolicy(
142
+ Qt.HighDpiScaleFactorRoundingPolicy.PassThrough
143
+ )
144
+ app = QApplication(sys.argv)
145
+ app.setApplicationName("rgpdf")
146
+ app.setOrganizationName("rgpdf")
147
+ app.setWindowIcon(application_icon())
148
+ app.setStyle("Fusion")
149
+ app.setStyleSheet(BASE_STYLE)
150
+
151
+ def apply_theme(theme: str) -> None:
152
+ apply_application_theme(app, theme)
153
+
154
+ settings = QSettings("rgpdf", "rgpdf")
155
+ apply_theme(settings.value("appearance/theme", "system", type=str))
156
+ window = MainWindow(apply_theme)
157
+ app.styleHints().colorSchemeChanged.connect(
158
+ lambda _scheme: apply_theme("system")
159
+ if settings.value("appearance/theme", "system", type=str) == "system"
160
+ else None
161
+ )
162
+ window.show()
163
+ return app.exec()
@@ -0,0 +1,2 @@
1
+ """Application artwork."""
2
+
@@ -0,0 +1,11 @@
1
+ <?xml version="1.0" encoding="UTF-8"?>
2
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512" role="img" aria-label="Regular expression document search">
3
+ <path d="M120 36h190l92 92v318c0 20-16 36-36 36H120c-20 0-36-16-36-36V72c0-20 16-36 36-36z" fill="#315EFB"/>
4
+ <path d="M310 36v92h92z" fill="#BFD0FF"/>
5
+ <circle cx="180" cy="268" r="20" fill="#FFFFFF"/>
6
+ <g stroke="#FFFFFF" stroke-width="22" stroke-linecap="round">
7
+ <path d="M286 210v116"/>
8
+ <path d="M236 239l100 58"/>
9
+ <path d="M236 297l100-58"/>
10
+ </g>
11
+ </svg>
@@ -0,0 +1,114 @@
1
+ from __future__ import annotations
2
+
3
+
4
+ TRANSLATIONS: dict[str, dict[str, str]] = {
5
+ "zh_TW": {
6
+ "app_title": "rgpdf — PDF 搜尋",
7
+ "folder": "PDF 資料夾",
8
+ "browse": "瀏覽…",
9
+ "pattern": "搜尋樣式",
10
+ "pattern_hint": "輸入文字或正規表示式",
11
+ "plain": "純文字",
12
+ "regex": "規則表達式",
13
+ "case": "區分大小寫",
14
+ "recursive": "包含子資料夾",
15
+ "highlight_settings": "螢光筆顏色",
16
+ "common_colors": "常用顏色",
17
+ "custom_color": "自訂顏色…",
18
+ "choose_highlight_color": "選擇螢光筆顏色",
19
+ "settings": "設定",
20
+ "enabled": "啟用",
21
+ "disabled": "停用",
22
+ "search": "搜尋",
23
+ "cancel": "取消",
24
+ "files": "檔案",
25
+ "matches": "命中項目",
26
+ "filename": "檔名",
27
+ "count": "筆數",
28
+ "page_number": "頁碼",
29
+ "paragraph": "段落",
30
+ "preview": "頁面預覽",
31
+ "ready": "選取資料夾並輸入搜尋樣式",
32
+ "searching": "正在搜尋 {current}/{total}:{name}",
33
+ "done": "完成:搜尋 {scanned} 份 PDF,{files} 份有結果,共 {matches} 筆命中",
34
+ "cancelled": "搜尋已取消:已處理 {scanned} 份 PDF",
35
+ "no_results": "找不到符合項目",
36
+ "choose_folder": "選取 PDF 資料夾",
37
+ "invalid": "無法搜尋",
38
+ "warnings": "部分檔案無法搜尋",
39
+ "warning_body": "略過 {count} 份檔案:\n\n{details}",
40
+ "language": "語言",
41
+ "theme": "主題",
42
+ "system": "跟隨系統",
43
+ "light": "淺色",
44
+ "dark": "深色",
45
+ "traditional_chinese": "繁體中文",
46
+ "english": "English",
47
+ "page": "第 {page} 頁,共 {total} 頁",
48
+ "previous_page": "上一個命中頁",
49
+ "next_page": "下一個命中頁",
50
+ "zoom_out": "縮小",
51
+ "zoom_in": "放大",
52
+ "fit": "符合寬度",
53
+ "page_single": "第 {start} 頁",
54
+ "page_range": "第 {start}–{end} 頁",
55
+ "match_count": "{count} 筆",
56
+ },
57
+ "en": {
58
+ "app_title": "rgpdf — PDF Search",
59
+ "folder": "PDF folder",
60
+ "browse": "Browse…",
61
+ "pattern": "Search pattern",
62
+ "pattern_hint": "Enter text or a regular expression",
63
+ "plain": "Plain text",
64
+ "regex": "Regular Expression",
65
+ "case": "Case sensitive",
66
+ "recursive": "Include subfolders",
67
+ "highlight_settings": "Highlight color",
68
+ "common_colors": "Common colors",
69
+ "custom_color": "Custom color…",
70
+ "choose_highlight_color": "Choose highlight color",
71
+ "settings": "Settings",
72
+ "enabled": "On",
73
+ "disabled": "Off",
74
+ "search": "Search",
75
+ "cancel": "Cancel",
76
+ "files": "Files",
77
+ "matches": "Matches",
78
+ "filename": "File name",
79
+ "count": "Count",
80
+ "page_number": "Page",
81
+ "paragraph": "Paragraph",
82
+ "preview": "Page preview",
83
+ "ready": "Choose a folder and enter a search pattern",
84
+ "searching": "Searching {current}/{total}: {name}",
85
+ "done": "Done: searched {scanned} PDFs, {files} files matched, {matches} matches",
86
+ "cancelled": "Search cancelled after {scanned} PDFs",
87
+ "no_results": "No matches found",
88
+ "choose_folder": "Choose PDF folder",
89
+ "invalid": "Cannot search",
90
+ "warnings": "Some files could not be searched",
91
+ "warning_body": "Skipped {count} files:\n\n{details}",
92
+ "language": "Language",
93
+ "theme": "Theme",
94
+ "system": "Follow system",
95
+ "light": "Light",
96
+ "dark": "Dark",
97
+ "traditional_chinese": "繁體中文",
98
+ "english": "English",
99
+ "page": "Page {page} of {total}",
100
+ "previous_page": "Previous matched page",
101
+ "next_page": "Next matched page",
102
+ "zoom_out": "Zoom out",
103
+ "zoom_in": "Zoom in",
104
+ "fit": "Fit width",
105
+ "page_single": "Page {start}",
106
+ "page_range": "Pages {start}–{end}",
107
+ "match_count": "{count} matches",
108
+ },
109
+ }
110
+
111
+
112
+ def translate(language: str, key: str, **values: object) -> str:
113
+ catalog = TRANSLATIONS.get(language, TRANSLATIONS["en"])
114
+ return catalog.get(key, TRANSLATIONS["en"].get(key, key)).format(**values)