repdf 0.1.1__tar.gz → 0.1.2__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.
- {repdf-0.1.1 → repdf-0.1.2}/PKG-INFO +18 -3
- {repdf-0.1.1 → repdf-0.1.2}/README.md +17 -2
- {repdf-0.1.1 → repdf-0.1.2}/pyproject.toml +1 -1
- {repdf-0.1.1 → repdf-0.1.2}/pyproject.toml.orig +1 -1
- {repdf-0.1.1 → repdf-0.1.2}/src/repdf/app.py +11 -0
- {repdf-0.1.1 → repdf-0.1.2}/src/repdf/icons.py +7 -1
- repdf-0.1.2/src/repdf/navigation.py +397 -0
- {repdf-0.1.1 → repdf-0.1.2}/src/repdf/packaging_smoke.py +20 -0
- {repdf-0.1.1 → repdf-0.1.2}/src/repdf/page_view.py +14 -0
- repdf-0.1.2/src/repdf/pdf_annotations.py +92 -0
- {repdf-0.1.1 → repdf-0.1.2}/src/repdf/pdf_navigation.py +8 -1
- {repdf-0.1.1 → repdf-0.1.2}/src/repdf/preview.py +4 -0
- {repdf-0.1.1 → repdf-0.1.2}/src/repdf/render_worker.py +20 -4
- {repdf-0.1.1 → repdf-0.1.2}/src/repdf/renderer.py +1 -1
- {repdf-0.1.1 → repdf-0.1.2}/src/repdf/strings.py +29 -0
- {repdf-0.1.1 → repdf-0.1.2}/src/repdf/widgets.py +26 -2
- repdf-0.1.1/src/repdf/navigation.py +0 -235
- {repdf-0.1.1 → repdf-0.1.2}/src/repdf/__init__.py +0 -0
- {repdf-0.1.1 → repdf-0.1.2}/src/repdf/__main__.py +0 -0
- {repdf-0.1.1 → repdf-0.1.2}/src/repdf/assets/check-dark.svg +0 -0
- {repdf-0.1.1 → repdf-0.1.2}/src/repdf/assets/check.svg +0 -0
- {repdf-0.1.1 → repdf-0.1.2}/src/repdf/assets/partial-dark.svg +0 -0
- {repdf-0.1.1 → repdf-0.1.2}/src/repdf/assets/partial.svg +0 -0
- {repdf-0.1.1 → repdf-0.1.2}/src/repdf/assets/repdf.ico +0 -0
- {repdf-0.1.1 → repdf-0.1.2}/src/repdf/assets/repdf.png +0 -0
- {repdf-0.1.1 → repdf-0.1.2}/src/repdf/core.py +0 -0
- {repdf-0.1.1 → repdf-0.1.2}/src/repdf/entrypoint.py +0 -0
- {repdf-0.1.1 → repdf-0.1.2}/src/repdf/images.py +0 -0
- {repdf-0.1.1 → repdf-0.1.2}/src/repdf/ocr.py +0 -0
- {repdf-0.1.1 → repdf-0.1.2}/src/repdf/ocr_languages.py +0 -0
- {repdf-0.1.1 → repdf-0.1.2}/src/repdf/ocr_models.py +0 -0
- {repdf-0.1.1 → repdf-0.1.2}/src/repdf/preferences.py +0 -0
- {repdf-0.1.1 → repdf-0.1.2}/src/repdf/print_preview.py +0 -0
- {repdf-0.1.1 → repdf-0.1.2}/src/repdf/printing.py +0 -0
- {repdf-0.1.1 → repdf-0.1.2}/src/repdf/search.py +0 -0
- {repdf-0.1.1 → repdf-0.1.2}/src/repdf/search_panel.py +0 -0
- {repdf-0.1.1 → repdf-0.1.2}/src/repdf/theme.py +0 -0
- {repdf-0.1.1 → repdf-0.1.2}/src/repdf/toolbar.py +0 -0
- {repdf-0.1.1 → repdf-0.1.2}/src/repdf/workers.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.3
|
|
2
2
|
Name: repdf
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.2
|
|
4
4
|
Summary: RePDF (Read the PDF): a desktop PDF reader with printing, search, bookmarks, OCR and page rearrangement.
|
|
5
5
|
Requires-Dist: onnxruntime>=1.31.0,<2
|
|
6
6
|
Requires-Dist: pillow>=11.0,<13
|
|
@@ -19,7 +19,7 @@ Description-Content-Type: text/markdown
|
|
|
19
19
|
|
|
20
20
|
[繁體中文](README.md) | [English](README.en.md)
|
|
21
21
|
|
|
22
|
-
RePDF 代表 **Read the PDF**,命令與 Python 套件名稱為 `repdf`。這是一套桌面 PDF
|
|
22
|
+
RePDF 代表 **Read the PDF**,命令與 Python 套件名稱為 `repdf`。這是一套桌面 PDF 閱讀器,支援多標籤閱讀、列印、書籤與目錄跳頁、註解清單、文字搜尋、選字複製、OCR 及圖片擷取,也可勾選頁面,依標籤與頁面排列順序重組或拆分輸出。重組與拆分保留原始頁面品質。
|
|
23
23
|
|
|
24
24
|
## 重點特色
|
|
25
25
|
|
|
@@ -68,6 +68,7 @@ repdf
|
|
|
68
68
|
| <picture><source media="(prefers-color-scheme: dark)" srcset="docs/images/toolbar-ocr-region-dark.png"><img src="docs/images/toolbar-ocr-region.png" width="24" alt="區域 OCR 圖示"></picture> | 區域 OCR | 拖曳矩形框選頁面或圖片,放開滑鼠後辨識並複製文字。 | — |
|
|
69
69
|
| <picture><source media="(prefers-color-scheme: dark)" srcset="docs/images/toolbar-extract-image-dark.png"><img src="docs/images/toolbar-extract-image.png" width="24" alt="擷取圖片圖示"></picture> | 擷取圖片 | 啟用後點選內嵌點陣圖片,再選擇位置另存為 PNG。 | — |
|
|
70
70
|
| <picture><source media="(prefers-color-scheme: dark)" srcset="docs/images/toolbar-bookmarks-dark.png"><img src="docs/images/toolbar-bookmarks.png" width="24" alt="書籤圖示"></picture> | 書籤 | 在左側章節樹展開或收合書籤,點選章節即可跳頁。 | — |
|
|
71
|
+
| <picture><source media="(prefers-color-scheme: dark)" srcset="docs/images/toolbar-annotations-dark.png"><img src="docs/images/toolbar-annotations.png" width="24" alt="註解圖示"></picture> | 註解 | 在左側列出註解,點選即可跳至對應頁面。 | — |
|
|
71
72
|
| <picture><source media="(prefers-color-scheme: dark)" srcset="docs/images/toolbar-search-dark.png"><img src="docs/images/toolbar-search.png" width="24" alt="搜尋文字圖示"></picture> | 搜尋文字 | 在右側窗格搜尋文字,上方選檔案、下方選命中段落,單擊即可跳頁並高亮。 | Windows/Linux:<kbd>Ctrl</kbd>+<kbd>F</kbd>;macOS:<kbd>⌘</kbd>+<kbd>F</kbd> |
|
|
72
73
|
| <picture><source media="(prefers-color-scheme: dark)" srcset="docs/images/toolbar-zoom-dark.png"><img src="docs/images/toolbar-zoom.png" width="160" alt="顯示比例與縮放滑桿"></picture> | 顯示比例 | 滑桿調整 25%–200%;點擊比例回到 100%。鍵盤縮放每次調整 25 個百分點。 | 放大:<kbd>Ctrl</kbd>+<kbd>+</kbd>/<kbd>Ctrl</kbd>+<kbd>=</kbd>;縮小:<kbd>Ctrl</kbd>+<kbd>-</kbd>;100%:<kbd>Ctrl</kbd>+<kbd>0</kbd> |
|
|
73
74
|
| <picture><source media="(prefers-color-scheme: dark)" srcset="docs/images/toolbar-fit-dark.png"><img src="docs/images/toolbar-fit.png" width="24" alt="Fit 圖示"></picture> | Fit | 將目前單頁等比例縮放至預覽區;調整視窗大小時自動適應。拖動滑桿或使用縮放快捷鍵會回到手動比例。 | — |
|
|
@@ -153,11 +154,25 @@ repdf
|
|
|
153
154
|
- 書籤跟隨目前標籤頁切換;沒有內建書籤的文件會顯示提示。
|
|
154
155
|
- 一般預覽與全螢幕都可直接點選 PDF 頁內的目錄連結;滑鼠移到可跳頁區域時會顯示手指游標與目的頁碼。
|
|
155
156
|
- 跳頁保留目前縮放/Fit 模式。頁面已重新排序時,仍會找到目的章節所屬的原始頁面。
|
|
156
|
-
- 視窗縮窄時,書籤與 PDF 以單一窗格切換;點選章節後返回 PDF
|
|
157
|
+
- 視窗縮窄時,書籤與 PDF 以單一窗格切換;點選章節後返回 PDF,再點「書籤」可返回目錄。書籤、註解與搜尋窗格可透過各自的工具按鈕切換。
|
|
157
158
|
- 啟用複製文字、區域 OCR 或圖片擷取模式時,頁內連結暫停點選;停用後即可跳頁,拖曳頁面仍可排序。
|
|
158
159
|
- 一般預覽與全螢幕可點選 PDF 內建的 HTTP/HTTPS 超連結,以系統預設瀏覽器開啟;滑鼠停留可查看網址。
|
|
159
160
|
- 此功能使用 PDF 已有的書籤與連結;純文字或掃描圖片形式的目錄、網址不會自動變成連結。電子郵件、外部文件與其他動作不在支援範圍。
|
|
160
161
|
|
|
162
|
+
### 註解清單
|
|
163
|
+
|
|
164
|
+
- 點工具列的「註解」對話框圖示,開啟目前標籤頁的左側註解清單。
|
|
165
|
+
- 清單依 PDF 原始頁序分組,每頁顯示註解數量,預設收合;展開後查看個別註解的類型與摘要。切換標籤、書籤或介面語言後會保留展開狀態。滑鼠停留可查看完整內容、作者、主旨及修改時間(若文件有提供)。
|
|
166
|
+
- 點選頁碼群組即可跳到該頁清單中的第一個註解,並標示其範圍;點選個別註解會跳到對應位置,並以半透明底色與外框標示註解範圍。保留目前縮放/Fit 模式,頁面重新排列後仍會找到原始目標頁。
|
|
167
|
+
- 選取含文字的註解時,會自動將註解文字複製到剪貼簿;頁碼群組會複製第一個註解的文字。無文字的註解不會覆蓋剪貼簿內容。
|
|
168
|
+
- 關閉註解窗格或切換至其他側欄時,會取消框選;重新開啟清單不會自動複製文字或恢復框選。
|
|
169
|
+
- 框選會跟隨縮放與全螢幕顯示;點選另一註解或頁碼群組會更新標示,點選書籤或搜尋結果則清除標示。文件未提供有效註解範圍時,仍可跳頁。
|
|
170
|
+
- 支援備註、文字方塊、螢光標示、底線、刪除線、手繪及形狀等註解。文字標示若沒有附註內容,會嘗試顯示標示的原文。
|
|
171
|
+
- 在一般閱讀模式可直接點選頁面上的註解,框選其範圍並同步選取左側清單、展開對應頁面群組;全螢幕也可使用。選字、區域 OCR 與圖片擷取模式保留各自操作。窄視窗點選後維持 PDF 顯示,再按「註解」即可查看清單中的選取項目。
|
|
172
|
+
- 註解跟隨標籤頁切換;沒有註解時顯示提示。書籤、註解與搜尋窗格透過各自的按鈕切換。
|
|
173
|
+
- 視窗較窄時,點選後返回 PDF;再點「註解」可返回清單。
|
|
174
|
+
- 完整註解清單只在開啟窗格時於背景讀取;頁面點選範圍則隨目前需要顯示的頁面讀取,不會在開檔時掃描整份文件;此功能提供檢視與跳頁,不編輯註解或執行附件內容。
|
|
175
|
+
|
|
161
176
|
### 搜尋文字
|
|
162
177
|
|
|
163
178
|
1. 點「搜尋文字」或按 <kbd>Ctrl</kbd>+<kbd>F</kbd>(macOS:<kbd>⌘</kbd>+<kbd>F</kbd>),開啟右側窗格。
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
[繁體中文](README.md) | [English](README.en.md)
|
|
4
4
|
|
|
5
|
-
RePDF 代表 **Read the PDF**,命令與 Python 套件名稱為 `repdf`。這是一套桌面 PDF
|
|
5
|
+
RePDF 代表 **Read the PDF**,命令與 Python 套件名稱為 `repdf`。這是一套桌面 PDF 閱讀器,支援多標籤閱讀、列印、書籤與目錄跳頁、註解清單、文字搜尋、選字複製、OCR 及圖片擷取,也可勾選頁面,依標籤與頁面排列順序重組或拆分輸出。重組與拆分保留原始頁面品質。
|
|
6
6
|
|
|
7
7
|
## 重點特色
|
|
8
8
|
|
|
@@ -51,6 +51,7 @@ repdf
|
|
|
51
51
|
| <picture><source media="(prefers-color-scheme: dark)" srcset="docs/images/toolbar-ocr-region-dark.png"><img src="docs/images/toolbar-ocr-region.png" width="24" alt="區域 OCR 圖示"></picture> | 區域 OCR | 拖曳矩形框選頁面或圖片,放開滑鼠後辨識並複製文字。 | — |
|
|
52
52
|
| <picture><source media="(prefers-color-scheme: dark)" srcset="docs/images/toolbar-extract-image-dark.png"><img src="docs/images/toolbar-extract-image.png" width="24" alt="擷取圖片圖示"></picture> | 擷取圖片 | 啟用後點選內嵌點陣圖片,再選擇位置另存為 PNG。 | — |
|
|
53
53
|
| <picture><source media="(prefers-color-scheme: dark)" srcset="docs/images/toolbar-bookmarks-dark.png"><img src="docs/images/toolbar-bookmarks.png" width="24" alt="書籤圖示"></picture> | 書籤 | 在左側章節樹展開或收合書籤,點選章節即可跳頁。 | — |
|
|
54
|
+
| <picture><source media="(prefers-color-scheme: dark)" srcset="docs/images/toolbar-annotations-dark.png"><img src="docs/images/toolbar-annotations.png" width="24" alt="註解圖示"></picture> | 註解 | 在左側列出註解,點選即可跳至對應頁面。 | — |
|
|
54
55
|
| <picture><source media="(prefers-color-scheme: dark)" srcset="docs/images/toolbar-search-dark.png"><img src="docs/images/toolbar-search.png" width="24" alt="搜尋文字圖示"></picture> | 搜尋文字 | 在右側窗格搜尋文字,上方選檔案、下方選命中段落,單擊即可跳頁並高亮。 | Windows/Linux:<kbd>Ctrl</kbd>+<kbd>F</kbd>;macOS:<kbd>⌘</kbd>+<kbd>F</kbd> |
|
|
55
56
|
| <picture><source media="(prefers-color-scheme: dark)" srcset="docs/images/toolbar-zoom-dark.png"><img src="docs/images/toolbar-zoom.png" width="160" alt="顯示比例與縮放滑桿"></picture> | 顯示比例 | 滑桿調整 25%–200%;點擊比例回到 100%。鍵盤縮放每次調整 25 個百分點。 | 放大:<kbd>Ctrl</kbd>+<kbd>+</kbd>/<kbd>Ctrl</kbd>+<kbd>=</kbd>;縮小:<kbd>Ctrl</kbd>+<kbd>-</kbd>;100%:<kbd>Ctrl</kbd>+<kbd>0</kbd> |
|
|
56
57
|
| <picture><source media="(prefers-color-scheme: dark)" srcset="docs/images/toolbar-fit-dark.png"><img src="docs/images/toolbar-fit.png" width="24" alt="Fit 圖示"></picture> | Fit | 將目前單頁等比例縮放至預覽區;調整視窗大小時自動適應。拖動滑桿或使用縮放快捷鍵會回到手動比例。 | — |
|
|
@@ -136,11 +137,25 @@ repdf
|
|
|
136
137
|
- 書籤跟隨目前標籤頁切換;沒有內建書籤的文件會顯示提示。
|
|
137
138
|
- 一般預覽與全螢幕都可直接點選 PDF 頁內的目錄連結;滑鼠移到可跳頁區域時會顯示手指游標與目的頁碼。
|
|
138
139
|
- 跳頁保留目前縮放/Fit 模式。頁面已重新排序時,仍會找到目的章節所屬的原始頁面。
|
|
139
|
-
- 視窗縮窄時,書籤與 PDF 以單一窗格切換;點選章節後返回 PDF
|
|
140
|
+
- 視窗縮窄時,書籤與 PDF 以單一窗格切換;點選章節後返回 PDF,再點「書籤」可返回目錄。書籤、註解與搜尋窗格可透過各自的工具按鈕切換。
|
|
140
141
|
- 啟用複製文字、區域 OCR 或圖片擷取模式時,頁內連結暫停點選;停用後即可跳頁,拖曳頁面仍可排序。
|
|
141
142
|
- 一般預覽與全螢幕可點選 PDF 內建的 HTTP/HTTPS 超連結,以系統預設瀏覽器開啟;滑鼠停留可查看網址。
|
|
142
143
|
- 此功能使用 PDF 已有的書籤與連結;純文字或掃描圖片形式的目錄、網址不會自動變成連結。電子郵件、外部文件與其他動作不在支援範圍。
|
|
143
144
|
|
|
145
|
+
### 註解清單
|
|
146
|
+
|
|
147
|
+
- 點工具列的「註解」對話框圖示,開啟目前標籤頁的左側註解清單。
|
|
148
|
+
- 清單依 PDF 原始頁序分組,每頁顯示註解數量,預設收合;展開後查看個別註解的類型與摘要。切換標籤、書籤或介面語言後會保留展開狀態。滑鼠停留可查看完整內容、作者、主旨及修改時間(若文件有提供)。
|
|
149
|
+
- 點選頁碼群組即可跳到該頁清單中的第一個註解,並標示其範圍;點選個別註解會跳到對應位置,並以半透明底色與外框標示註解範圍。保留目前縮放/Fit 模式,頁面重新排列後仍會找到原始目標頁。
|
|
150
|
+
- 選取含文字的註解時,會自動將註解文字複製到剪貼簿;頁碼群組會複製第一個註解的文字。無文字的註解不會覆蓋剪貼簿內容。
|
|
151
|
+
- 關閉註解窗格或切換至其他側欄時,會取消框選;重新開啟清單不會自動複製文字或恢復框選。
|
|
152
|
+
- 框選會跟隨縮放與全螢幕顯示;點選另一註解或頁碼群組會更新標示,點選書籤或搜尋結果則清除標示。文件未提供有效註解範圍時,仍可跳頁。
|
|
153
|
+
- 支援備註、文字方塊、螢光標示、底線、刪除線、手繪及形狀等註解。文字標示若沒有附註內容,會嘗試顯示標示的原文。
|
|
154
|
+
- 在一般閱讀模式可直接點選頁面上的註解,框選其範圍並同步選取左側清單、展開對應頁面群組;全螢幕也可使用。選字、區域 OCR 與圖片擷取模式保留各自操作。窄視窗點選後維持 PDF 顯示,再按「註解」即可查看清單中的選取項目。
|
|
155
|
+
- 註解跟隨標籤頁切換;沒有註解時顯示提示。書籤、註解與搜尋窗格透過各自的按鈕切換。
|
|
156
|
+
- 視窗較窄時,點選後返回 PDF;再點「註解」可返回清單。
|
|
157
|
+
- 完整註解清單只在開啟窗格時於背景讀取;頁面點選範圍則隨目前需要顯示的頁面讀取,不會在開檔時掃描整份文件;此功能提供檢視與跳頁,不編輯註解或執行附件內容。
|
|
158
|
+
|
|
144
159
|
### 搜尋文字
|
|
145
160
|
|
|
146
161
|
1. 點「搜尋文字」或按 <kbd>Ctrl</kbd>+<kbd>F</kbd>(macOS:<kbd>⌘</kbd>+<kbd>F</kbd>),開啟右側窗格。
|
|
@@ -90,8 +90,10 @@ class DocumentView(QWidget):
|
|
|
90
90
|
self.focused_page = None
|
|
91
91
|
self.page_sizes = {}
|
|
92
92
|
self.page_links = {}
|
|
93
|
+
self.page_annotations = {}
|
|
93
94
|
self.page_labels = {}
|
|
94
95
|
self.bookmarks = None
|
|
96
|
+
self.annotations = None
|
|
95
97
|
self.ready = False
|
|
96
98
|
self._search_navigation = None
|
|
97
99
|
self.password: str | None = None
|
|
@@ -178,6 +180,7 @@ class DocumentView(QWidget):
|
|
|
178
180
|
self.owner._copy_text_selection(self, index, preview, start, end, copy=False))
|
|
179
181
|
self.pages.prepare_card(card)
|
|
180
182
|
card.preview.set_links(self.page_links.get(page_index, []))
|
|
183
|
+
card.preview.set_annotations(self.page_annotations.get(page_index, []))
|
|
181
184
|
card.preview.link_activated.connect(lambda destination: self.owner.navigation.activate(self, destination))
|
|
182
185
|
self.pages.chrome = max(1, card.sizeHint().height() - card.preview.height())
|
|
183
186
|
return card
|
|
@@ -734,6 +737,12 @@ class MainWindow(QMainWindow):
|
|
|
734
737
|
self.bookmark_action.triggered.connect(lambda: self.navigation.toggle())
|
|
735
738
|
self.toolbar.addAction(self.bookmark_action)
|
|
736
739
|
self.toolbar.widgetForAction(self.bookmark_action).setObjectName('bookmarksTool')
|
|
740
|
+
self.annotation_action = QAction(tr('註解'), self)
|
|
741
|
+
self.annotation_action.setCheckable(True)
|
|
742
|
+
self.annotation_action.setToolTip(tr('註解:開啟註解清單,點選即可跳至對應頁面'))
|
|
743
|
+
self.annotation_action.triggered.connect(lambda: self.navigation.toggle('annotations'))
|
|
744
|
+
self.toolbar.addAction(self.annotation_action)
|
|
745
|
+
self.toolbar.widgetForAction(self.annotation_action).setObjectName('annotationsTool')
|
|
737
746
|
self.search_action = QAction(tr('搜尋文字'), self)
|
|
738
747
|
self.search_action.setShortcuts(QKeySequence.keyBindings(QKeySequence.StandardKey.Find))
|
|
739
748
|
self.search_action.setToolTip(tr('搜尋文字({p0}):在右側窗格搜尋目前、勾選或所有標籤頁',
|
|
@@ -822,6 +831,7 @@ class MainWindow(QMainWindow):
|
|
|
822
831
|
(self.search_action, "search"),
|
|
823
832
|
(self.print_action, "print"),
|
|
824
833
|
(self.bookmark_action, "bookmarks"),
|
|
834
|
+
(self.annotation_action, "annotations"),
|
|
825
835
|
(self.fit_action, "fit"),
|
|
826
836
|
(self.fullscreen_action, "fullscreen"),
|
|
827
837
|
(self.spread_action, "fullscreen_spread"),
|
|
@@ -1503,6 +1513,7 @@ class MainWindow(QMainWindow):
|
|
|
1503
1513
|
|
|
1504
1514
|
def closeEvent(self, event: QCloseEvent) -> None:
|
|
1505
1515
|
self._closing = True
|
|
1516
|
+
self.navigation.shutdown()
|
|
1506
1517
|
self.ocr_languages.close()
|
|
1507
1518
|
self.print_controller.close()
|
|
1508
1519
|
self.search_controller.stop()
|
|
@@ -20,7 +20,13 @@ def toolbar_icon(kind: str) -> QIcon:
|
|
|
20
20
|
painter.setPen(QPen(QApplication.palette().color(group, role),
|
|
21
21
|
1.0, Qt.PenStyle.SolidLine, Qt.PenCapStyle.RoundCap,
|
|
22
22
|
Qt.PenJoinStyle.RoundJoin))
|
|
23
|
-
if kind == '
|
|
23
|
+
if kind == 'annotations':
|
|
24
|
+
painter.drawPolyline(QPolygonF([QPointF(4, 3), QPointF(21, 3), QPointF(21, 17),
|
|
25
|
+
QPointF(11, 17), QPointF(6, 21), QPointF(6, 17),
|
|
26
|
+
QPointF(4, 17), QPointF(4, 3)]))
|
|
27
|
+
painter.drawLine(8, 8, 17, 8)
|
|
28
|
+
painter.drawLine(8, 12, 15, 12)
|
|
29
|
+
elif kind == 'chevron_down':
|
|
24
30
|
pen = painter.pen()
|
|
25
31
|
pen.setWidthF(2)
|
|
26
32
|
painter.setPen(pen)
|
|
@@ -0,0 +1,397 @@
|
|
|
1
|
+
"""Bookmark/annotation sidebar, internal navigation and web links."""
|
|
2
|
+
|
|
3
|
+
from html import escape
|
|
4
|
+
|
|
5
|
+
from PySide6.QtCore import QEvent, QObject, QSize, Qt, QUrl
|
|
6
|
+
from PySide6.QtGui import QDesktopServices
|
|
7
|
+
from PySide6.QtWidgets import (QApplication, QHBoxLayout, QLabel, QLayout, QSplitter, QToolButton,
|
|
8
|
+
QTreeWidget, QTreeWidgetItem, QVBoxLayout, QWidget)
|
|
9
|
+
|
|
10
|
+
from repdf.icons import toolbar_icon
|
|
11
|
+
from repdf.strings import tr
|
|
12
|
+
from repdf.renderer import RenderService, start_render_backend
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class BookmarkPanel(QWidget):
|
|
16
|
+
def __init__(self):
|
|
17
|
+
super().__init__()
|
|
18
|
+
self.setMinimumSize(0, 0)
|
|
19
|
+
layout = QVBoxLayout(self)
|
|
20
|
+
layout.setContentsMargins(8, 8, 8, 8)
|
|
21
|
+
layout.setSpacing(6)
|
|
22
|
+
layout.setSizeConstraint(QLayout.SizeConstraint.SetNoConstraint)
|
|
23
|
+
row = QHBoxLayout()
|
|
24
|
+
self.title = QLabel(tr('書籤'))
|
|
25
|
+
row.addWidget(self.title, 1)
|
|
26
|
+
self.close_button = QToolButton()
|
|
27
|
+
self.close_button.setText(tr('關閉書籤窗格'))
|
|
28
|
+
self.close_button.setAccessibleName(tr('關閉書籤窗格'))
|
|
29
|
+
self.close_button.setToolTip(tr('關閉書籤窗格'))
|
|
30
|
+
self.close_button.setFixedSize(34, 34)
|
|
31
|
+
self.close_button.setIconSize(QSize(20, 20))
|
|
32
|
+
self.close_button.setToolButtonStyle(Qt.ToolButtonStyle.ToolButtonIconOnly)
|
|
33
|
+
self.close_button.setIcon(toolbar_icon('close'))
|
|
34
|
+
row.addWidget(self.close_button)
|
|
35
|
+
layout.addLayout(row)
|
|
36
|
+
self.status = QLabel()
|
|
37
|
+
self.status.setWordWrap(True)
|
|
38
|
+
self.status.setTextFormat(Qt.TextFormat.PlainText)
|
|
39
|
+
layout.addWidget(self.status)
|
|
40
|
+
self.tree = QTreeWidget()
|
|
41
|
+
self.tree.setHeaderHidden(True)
|
|
42
|
+
self.tree.setAccessibleName(tr('PDF 書籤'))
|
|
43
|
+
self.tree.setMinimumSize(0, 0)
|
|
44
|
+
self.tree.setIndentation(16)
|
|
45
|
+
self.tree.setTextElideMode(Qt.TextElideMode.ElideRight)
|
|
46
|
+
self.tree.setHorizontalScrollBarPolicy(Qt.ScrollBarPolicy.ScrollBarAlwaysOff)
|
|
47
|
+
layout.addWidget(self.tree, 1)
|
|
48
|
+
|
|
49
|
+
def changeEvent(self, event):
|
|
50
|
+
super().changeEvent(event)
|
|
51
|
+
if event.type() == QEvent.Type.PaletteChange and hasattr(self, 'close_button'):
|
|
52
|
+
self.close_button.setIcon(toolbar_icon('close'))
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
class NavigationController(QObject):
|
|
56
|
+
def __init__(self, owner, content):
|
|
57
|
+
super().__init__(owner)
|
|
58
|
+
self.owner, self.content = owner, content
|
|
59
|
+
self.panel = BookmarkPanel()
|
|
60
|
+
self.workspace = QSplitter(Qt.Orientation.Horizontal)
|
|
61
|
+
self.workspace.setChildrenCollapsible(False)
|
|
62
|
+
self.workspace.addWidget(self.panel)
|
|
63
|
+
self.workspace.addWidget(content)
|
|
64
|
+
self.workspace.setStretchFactor(0, 0)
|
|
65
|
+
self.workspace.setStretchFactor(1, 1)
|
|
66
|
+
self.panel.hide()
|
|
67
|
+
self.opened = False
|
|
68
|
+
self.narrow_results = True
|
|
69
|
+
self.displayed_id = None
|
|
70
|
+
self.kind = 'bookmarks'
|
|
71
|
+
self.displayed_kind = None
|
|
72
|
+
self.annotation_service = None
|
|
73
|
+
self.items = []
|
|
74
|
+
self.annotation_groups = {}
|
|
75
|
+
self.states = {}
|
|
76
|
+
self.requests = {}
|
|
77
|
+
self.latest_jump = None
|
|
78
|
+
self.annotation_selection = None
|
|
79
|
+
self.pending_annotation_copy = None
|
|
80
|
+
self._layout_state = None
|
|
81
|
+
self.panel.close_button.clicked.connect(self.close)
|
|
82
|
+
self.panel.tree.itemClicked.connect(self._clicked)
|
|
83
|
+
self.panel.tree.itemActivated.connect(self._clicked)
|
|
84
|
+
|
|
85
|
+
def toggle(self, kind='bookmarks'):
|
|
86
|
+
if self.panel.isVisible() and self.kind == kind:
|
|
87
|
+
self.close()
|
|
88
|
+
else:
|
|
89
|
+
self.open(kind)
|
|
90
|
+
|
|
91
|
+
def open(self, kind='bookmarks', *, keep_fullscreen=False):
|
|
92
|
+
if kind != self.kind:
|
|
93
|
+
if self.kind == 'annotations':
|
|
94
|
+
self._clear_annotation_selection()
|
|
95
|
+
self._cancel_annotations()
|
|
96
|
+
self.kind = kind
|
|
97
|
+
if self.owner._fullscreen_preview is not None and not keep_fullscreen:
|
|
98
|
+
self.owner._fullscreen_preview.close()
|
|
99
|
+
self.owner.search_controller.close()
|
|
100
|
+
self.opened = True
|
|
101
|
+
self.narrow_results = True
|
|
102
|
+
self.layout()
|
|
103
|
+
self.show_document()
|
|
104
|
+
self.panel.tree.setFocus()
|
|
105
|
+
|
|
106
|
+
def close(self):
|
|
107
|
+
if self.kind == 'annotations':
|
|
108
|
+
self._clear_annotation_selection()
|
|
109
|
+
self._cancel_annotations()
|
|
110
|
+
self.opened = False
|
|
111
|
+
self.layout()
|
|
112
|
+
|
|
113
|
+
def _clear_annotation_selection(self):
|
|
114
|
+
self.annotation_selection = None
|
|
115
|
+
self.pending_annotation_copy = None
|
|
116
|
+
self.latest_jump = None
|
|
117
|
+
for doc in self.owner.documents.values():
|
|
118
|
+
doc.pages.set_annotation_highlight(None, None)
|
|
119
|
+
|
|
120
|
+
def layout(self):
|
|
121
|
+
narrow = self.owner.width() < 640
|
|
122
|
+
state = narrow, self.opened, self.narrow_results, self.kind
|
|
123
|
+
if state == self._layout_state:
|
|
124
|
+
return
|
|
125
|
+
self._layout_state = state
|
|
126
|
+
visible = self.opened and (not narrow or self.narrow_results)
|
|
127
|
+
self.content.setVisible(not (narrow and visible))
|
|
128
|
+
self.panel.setVisible(visible)
|
|
129
|
+
self.owner.bookmark_action.setChecked(visible and self.kind == 'bookmarks')
|
|
130
|
+
self.owner.annotation_action.setChecked(visible and self.kind == 'annotations')
|
|
131
|
+
if visible and not narrow:
|
|
132
|
+
self.workspace.setSizes([260, max(1, self.owner.width() - 260)])
|
|
133
|
+
|
|
134
|
+
def show_document(self):
|
|
135
|
+
doc = self.owner.current_document()
|
|
136
|
+
document_id = doc.document_id if doc is not None else None
|
|
137
|
+
if self.pending_annotation_copy is not None and self.pending_annotation_copy[0] != document_id:
|
|
138
|
+
self.pending_annotation_copy = None
|
|
139
|
+
if (self.displayed_id, self.displayed_kind) != (document_id, self.kind):
|
|
140
|
+
self._cancel_annotations()
|
|
141
|
+
self._remember_state()
|
|
142
|
+
self.latest_jump = None
|
|
143
|
+
self.displayed_id = document_id
|
|
144
|
+
self.displayed_kind = self.kind
|
|
145
|
+
self.panel.tree.clear()
|
|
146
|
+
self.items = []
|
|
147
|
+
self.annotation_groups = {}
|
|
148
|
+
annotations = self.kind == 'annotations'
|
|
149
|
+
self.panel.title.setText(tr('註解' if annotations else '書籤'))
|
|
150
|
+
close_label = tr('關閉註解窗格' if annotations else '關閉書籤窗格')
|
|
151
|
+
self.panel.close_button.setText(close_label)
|
|
152
|
+
self.panel.close_button.setToolTip(close_label)
|
|
153
|
+
self.panel.close_button.setAccessibleName(close_label)
|
|
154
|
+
self.panel.tree.setAccessibleName(tr('PDF 註解' if annotations else 'PDF 書籤'))
|
|
155
|
+
self.panel.tree.setRootIsDecorated(True)
|
|
156
|
+
self.panel.tree.setIndentation(16)
|
|
157
|
+
if not self.opened:
|
|
158
|
+
return
|
|
159
|
+
if doc is None or not doc.ready:
|
|
160
|
+
self.panel.status.setText(tr('請先開啟 PDF 檔案'))
|
|
161
|
+
self.panel.status.show()
|
|
162
|
+
return
|
|
163
|
+
if getattr(doc, self.kind) is not None:
|
|
164
|
+
self.populate(doc)
|
|
165
|
+
return
|
|
166
|
+
self.panel.status.setText(tr('正在載入註解…' if annotations else '正在載入書籤…'))
|
|
167
|
+
self.panel.status.show()
|
|
168
|
+
if not any(value == (doc.document_id, self.kind, False) for value in self.requests.values()):
|
|
169
|
+
self._request(doc, self.kind)
|
|
170
|
+
|
|
171
|
+
def _request(self, doc, kind, destination=None, fullscreen=False):
|
|
172
|
+
self.owner._next_generation += 1
|
|
173
|
+
generation = self.owner._next_generation
|
|
174
|
+
self.requests = {key: value for key, value in self.requests.items()
|
|
175
|
+
if value[:2] != (doc.document_id, kind)}
|
|
176
|
+
self.requests[generation] = doc.document_id, kind, fullscreen
|
|
177
|
+
if kind == 'navigate':
|
|
178
|
+
self.latest_jump = generation
|
|
179
|
+
service = self.owner.render_service
|
|
180
|
+
if kind == 'annotations':
|
|
181
|
+
try:
|
|
182
|
+
if self.annotation_service is None:
|
|
183
|
+
self.annotation_service = RenderService(start_render_backend(), self)
|
|
184
|
+
self.annotation_service.message.connect(self.receive, Qt.ConnectionType.QueuedConnection)
|
|
185
|
+
service = self.annotation_service
|
|
186
|
+
except (OSError, RuntimeError) as exc:
|
|
187
|
+
self.receive({'type': 'error', 'generation': generation, 'message': str(exc)})
|
|
188
|
+
return generation
|
|
189
|
+
service.request_navigation(doc.source_path, doc.document_id, generation, kind,
|
|
190
|
+
destination=destination, password=doc.password)
|
|
191
|
+
return generation
|
|
192
|
+
|
|
193
|
+
def populate(self, doc):
|
|
194
|
+
if self.items:
|
|
195
|
+
return
|
|
196
|
+
self.panel.tree.clear()
|
|
197
|
+
if self.kind == 'annotations':
|
|
198
|
+
self.populate_annotations(doc)
|
|
199
|
+
return
|
|
200
|
+
parents = []
|
|
201
|
+
remembered = self.states.get((doc.document_id, self.kind))
|
|
202
|
+
for index, bookmark in enumerate(doc.bookmarks):
|
|
203
|
+
level = min(bookmark['level'], len(parents))
|
|
204
|
+
parents = parents[:level]
|
|
205
|
+
parent = parents[-1] if parents else self.panel.tree
|
|
206
|
+
title = bookmark['title'] or tr('未命名書籤')
|
|
207
|
+
item = QTreeWidgetItem(parent, [title])
|
|
208
|
+
item.setData(0, Qt.ItemDataRole.UserRole, bookmark['destination'])
|
|
209
|
+
item.setToolTip(0, title)
|
|
210
|
+
self.items.append(item)
|
|
211
|
+
parents.append(item)
|
|
212
|
+
if remembered and 0 <= remembered[1] < len(self.items):
|
|
213
|
+
self.panel.tree.setCurrentItem(self.items[remembered[1]])
|
|
214
|
+
for index, (item, bookmark) in enumerate(zip(self.items, doc.bookmarks)):
|
|
215
|
+
item.setExpanded(index in remembered[0] if remembered else bookmark['expanded'])
|
|
216
|
+
self.panel.status.setText(tr('此 PDF 沒有內建書籤'))
|
|
217
|
+
self.panel.status.setVisible(not bool(doc.bookmarks))
|
|
218
|
+
|
|
219
|
+
def populate_annotations(self, doc):
|
|
220
|
+
counts = {}
|
|
221
|
+
first_destinations = {}
|
|
222
|
+
for annotation in doc.annotations:
|
|
223
|
+
page = annotation['page']
|
|
224
|
+
counts[page] = counts.get(page, 0) + 1
|
|
225
|
+
first_destinations.setdefault(page, annotation['destination'])
|
|
226
|
+
for page, count in counts.items():
|
|
227
|
+
title = tr('第 {page} 頁({count} 則註解)', page=page + 1, count=count)
|
|
228
|
+
group = QTreeWidgetItem(self.panel.tree, [title])
|
|
229
|
+
group.setToolTip(0, title)
|
|
230
|
+
group.setData(0, Qt.ItemDataRole.UserRole, first_destinations[page])
|
|
231
|
+
self.annotation_groups[page] = group
|
|
232
|
+
for annotation in doc.annotations:
|
|
233
|
+
summary = ' '.join((annotation['contents'] or annotation['subject'] or tr('沒有文字內容')).split())
|
|
234
|
+
title = tr(annotation['kind']) + ' · ' + summary
|
|
235
|
+
item = QTreeWidgetItem(self.annotation_groups[annotation['page']], [title])
|
|
236
|
+
item.setData(0, Qt.ItemDataRole.UserRole, annotation['destination'])
|
|
237
|
+
details = [tr(annotation['kind']), annotation['contents'] or tr('沒有文字內容')]
|
|
238
|
+
for key, label in [('author', '作者:{value}'), ('subject', '主旨:{value}'), ('modified', '修改時間:{value}')]:
|
|
239
|
+
if annotation.get(key):
|
|
240
|
+
details.append(tr(label, value=annotation[key]))
|
|
241
|
+
item.setToolTip(0, '<qt>' + '<br>'.join(escape(line).replace('\n', '<br>') for line in details) + '</qt>')
|
|
242
|
+
self.items.append(item)
|
|
243
|
+
remembered = self.states.get((doc.document_id, self.kind))
|
|
244
|
+
if remembered and 0 <= remembered[1] < len(self.items):
|
|
245
|
+
self.panel.tree.setCurrentItem(self.items[remembered[1]])
|
|
246
|
+
for page, group in self.annotation_groups.items():
|
|
247
|
+
group.setExpanded(remembered is not None and page in remembered[0])
|
|
248
|
+
self._select_annotation_item(doc)
|
|
249
|
+
self.panel.status.setText(tr('共 {count} 則註解', count=len(doc.annotations)) if doc.annotations else tr('此 PDF 沒有註解'))
|
|
250
|
+
self.panel.status.show()
|
|
251
|
+
|
|
252
|
+
def _remember_state(self):
|
|
253
|
+
selected = self.panel.tree.currentItem()
|
|
254
|
+
expanded = ([page for page, group in self.annotation_groups.items() if group.isExpanded()]
|
|
255
|
+
if self.displayed_kind == 'annotations'
|
|
256
|
+
else [i for i, item in enumerate(self.items) if item.isExpanded()])
|
|
257
|
+
self.states[self.displayed_id, self.displayed_kind] = (
|
|
258
|
+
expanded, self.items.index(selected) if selected in self.items else -1)
|
|
259
|
+
|
|
260
|
+
def _clicked(self, item, _column=0):
|
|
261
|
+
self.annotation_selection = None
|
|
262
|
+
destination = item.data(0, Qt.ItemDataRole.UserRole)
|
|
263
|
+
doc = self.owner.current_document()
|
|
264
|
+
if destination is not None and doc is not None:
|
|
265
|
+
self.activate(doc, destination)
|
|
266
|
+
if self.kind == 'annotations':
|
|
267
|
+
target = item if item in self.items else item.child(0)
|
|
268
|
+
if target in self.items:
|
|
269
|
+
annotation = doc.annotations[self.items.index(target)]
|
|
270
|
+
self.pending_annotation_copy = doc.document_id, annotation['page'], annotation['index']
|
|
271
|
+
|
|
272
|
+
def activate(self, doc, destination, fullscreen=False):
|
|
273
|
+
if doc.ready and doc is self.owner.current_document():
|
|
274
|
+
self.pending_annotation_copy = None
|
|
275
|
+
if 'annotation_index' in destination:
|
|
276
|
+
self.latest_jump = None
|
|
277
|
+
doc._search_navigation = None
|
|
278
|
+
self.annotation_selection = doc.document_id, destination['page'], destination['annotation_index']
|
|
279
|
+
self.pending_annotation_copy = self.annotation_selection
|
|
280
|
+
self.open('annotations', keep_fullscreen=fullscreen)
|
|
281
|
+
self.narrow_results = False
|
|
282
|
+
self.layout()
|
|
283
|
+
self._select_annotation_item(doc)
|
|
284
|
+
self.owner.search_controller.clear_highlights()
|
|
285
|
+
doc.pages.set_annotation_highlight(destination['page'], destination.get('annotation_rect'))
|
|
286
|
+
doc._focus_page(destination['page'])
|
|
287
|
+
return
|
|
288
|
+
if 'url' in destination:
|
|
289
|
+
url = QUrl(destination['url'], QUrl.ParsingMode.StrictMode)
|
|
290
|
+
if (url.isValid() and url.scheme().lower() in ('http', 'https') and url.host()
|
|
291
|
+
and not any(ord(c) < 32 or ord(c) == 127 for c in destination['url'])):
|
|
292
|
+
if not QDesktopServices.openUrl(url):
|
|
293
|
+
self.owner.statusBar().showMessage(tr('無法開啟連結:{p0}', p0=destination['url']))
|
|
294
|
+
return
|
|
295
|
+
self._request(doc, 'navigate', destination, fullscreen)
|
|
296
|
+
|
|
297
|
+
def _select_annotation_item(self, doc):
|
|
298
|
+
if (self.annotation_selection is None or self.kind != 'annotations'
|
|
299
|
+
or self.displayed_id != doc.document_id or doc.annotations is None):
|
|
300
|
+
return
|
|
301
|
+
for annotation, item in zip(doc.annotations, self.items):
|
|
302
|
+
if self.annotation_selection == (doc.document_id, annotation['page'], annotation['index']):
|
|
303
|
+
item.parent().setExpanded(True)
|
|
304
|
+
self.panel.tree.setCurrentItem(item)
|
|
305
|
+
self.panel.tree.scrollToItem(item)
|
|
306
|
+
self._copy_pending_annotation(doc)
|
|
307
|
+
break
|
|
308
|
+
|
|
309
|
+
def _copy_pending_annotation(self, doc):
|
|
310
|
+
if doc.annotations is None or doc is not self.owner.current_document():
|
|
311
|
+
return
|
|
312
|
+
for annotation in doc.annotations:
|
|
313
|
+
if self.pending_annotation_copy == (doc.document_id, annotation['page'], annotation['index']):
|
|
314
|
+
self.pending_annotation_copy = None
|
|
315
|
+
text = annotation['contents']
|
|
316
|
+
if text.strip():
|
|
317
|
+
QApplication.clipboard().setText(text)
|
|
318
|
+
self.owner.statusBar().showMessage(tr('已複製 {p0} 個字元到剪貼簿', p0=len(text)))
|
|
319
|
+
break
|
|
320
|
+
|
|
321
|
+
def receive(self, payload):
|
|
322
|
+
generation = payload.get('generation')
|
|
323
|
+
request = self.requests.get(generation)
|
|
324
|
+
if request is None:
|
|
325
|
+
return False
|
|
326
|
+
document_id, kind, fullscreen = request
|
|
327
|
+
doc = self.owner.documents.get(document_id)
|
|
328
|
+
message_type = payload.get('type')
|
|
329
|
+
if message_type in ('done', 'error'):
|
|
330
|
+
self.requests.pop(generation, None)
|
|
331
|
+
if doc is None or self.owner._closing:
|
|
332
|
+
return True
|
|
333
|
+
if message_type in ('bookmarks', 'annotations'):
|
|
334
|
+
setattr(doc, message_type, payload[message_type])
|
|
335
|
+
if doc.document_id == self.displayed_id and self.kind == message_type:
|
|
336
|
+
self.populate(doc)
|
|
337
|
+
elif message_type == 'navigate' and generation == self.latest_jump and doc is self.owner.current_document():
|
|
338
|
+
page = payload['page']
|
|
339
|
+
doc.page_labels[page] = payload.get('page_label')
|
|
340
|
+
doc.page_sizes[page] = payload['width_points'], payload['height_points']
|
|
341
|
+
doc._reflow_grid()
|
|
342
|
+
self.owner.search_controller.clear_highlights()
|
|
343
|
+
doc.pages.set_annotation_highlight(page, payload.get('annotation_rect'))
|
|
344
|
+
self._copy_pending_annotation(doc)
|
|
345
|
+
viewer = self.owner._fullscreen_preview
|
|
346
|
+
if fullscreen and viewer is not None and viewer.document is doc:
|
|
347
|
+
viewer.show_page(page)
|
|
348
|
+
else:
|
|
349
|
+
self.narrow_results = False
|
|
350
|
+
self.layout()
|
|
351
|
+
doc.jump_to_page(page, payload['point'])
|
|
352
|
+
elif message_type == 'error':
|
|
353
|
+
message = tr('無法載入註解:{p0}' if kind == 'annotations' else '無法載入 PDF 導航:{p0}',
|
|
354
|
+
p0=payload.get('message', ''))
|
|
355
|
+
if doc.document_id == self.displayed_id and kind == self.kind:
|
|
356
|
+
self.panel.status.setText(message)
|
|
357
|
+
self.panel.status.show()
|
|
358
|
+
self.owner.statusBar().showMessage(message)
|
|
359
|
+
return True
|
|
360
|
+
|
|
361
|
+
def remove_document(self, doc):
|
|
362
|
+
if self.annotation_selection is not None and self.annotation_selection[0] == doc.document_id:
|
|
363
|
+
self.annotation_selection = None
|
|
364
|
+
if self.pending_annotation_copy is not None and self.pending_annotation_copy[0] == doc.document_id:
|
|
365
|
+
self.pending_annotation_copy = None
|
|
366
|
+
if self.displayed_id == doc.document_id or any(
|
|
367
|
+
value[:2] == (doc.document_id, 'annotations') for value in self.requests.values()):
|
|
368
|
+
self._cancel_annotations()
|
|
369
|
+
for kind in ('bookmarks', 'annotations'):
|
|
370
|
+
self.states.pop((doc.document_id, kind), None)
|
|
371
|
+
self.requests = {key: value for key, value in self.requests.items() if value[0] != doc.document_id}
|
|
372
|
+
if self.displayed_id == doc.document_id:
|
|
373
|
+
self.displayed_id = None
|
|
374
|
+
self.items = []
|
|
375
|
+
self.annotation_groups = {}
|
|
376
|
+
self.panel.tree.clear()
|
|
377
|
+
for prefix in (-3_000_000, -4_000_000):
|
|
378
|
+
self.owner.render_service.cancel_pending(prefix - doc.document_id)
|
|
379
|
+
|
|
380
|
+
def retranslate(self):
|
|
381
|
+
if self.kind == 'annotations':
|
|
382
|
+
self._remember_state()
|
|
383
|
+
self.panel.tree.clear()
|
|
384
|
+
self.items = []
|
|
385
|
+
self.annotation_groups = {}
|
|
386
|
+
if self.opened:
|
|
387
|
+
self.show_document()
|
|
388
|
+
|
|
389
|
+
def _cancel_annotations(self):
|
|
390
|
+
self.requests = {key: value for key, value in self.requests.items() if value[1] != 'annotations'}
|
|
391
|
+
service, self.annotation_service = self.annotation_service, None
|
|
392
|
+
if service is not None:
|
|
393
|
+
service.close(terminate_tree=True)
|
|
394
|
+
service.deleteLater()
|
|
395
|
+
|
|
396
|
+
def shutdown(self):
|
|
397
|
+
self._cancel_annotations()
|
|
@@ -90,6 +90,8 @@ def main(report_path: str) -> int:
|
|
|
90
90
|
writer = PdfWriter()
|
|
91
91
|
for width in (200, 300):
|
|
92
92
|
writer.add_blank_page(width=width, height=400)
|
|
93
|
+
from pypdf.annotations import Text
|
|
94
|
+
writer.add_annotation(1, Text(rect=(30, 200, 100, 250), text='Smoke annotation 註解'))
|
|
93
95
|
with source.open("wb") as stream:
|
|
94
96
|
writer.write(stream)
|
|
95
97
|
process = start_render_backend()
|
|
@@ -107,11 +109,29 @@ def main(report_path: str) -> int:
|
|
|
107
109
|
if message["type"] == "page":
|
|
108
110
|
if QImage(message["path"]).isNull():
|
|
109
111
|
raise RuntimeError("Invalid rendered page")
|
|
112
|
+
hits = message.get('annotation_hits', [])
|
|
113
|
+
if message['page'] == 1 and (len(hits) != 1 or
|
|
114
|
+
hits[0]['destination'].get('annotation_index') != 0):
|
|
115
|
+
raise RuntimeError(f'Invalid annotation hit targets: {hits}')
|
|
110
116
|
pages.append(message["page"])
|
|
111
117
|
if message["type"] == "done":
|
|
112
118
|
break
|
|
113
119
|
if pages != [0, 1]:
|
|
114
120
|
raise RuntimeError(f"Unexpected rendered pages: {pages}")
|
|
121
|
+
process.stdin.write(json.dumps({'type': 'annotations', 'source': str(source), 'generation': 2}) + '\n')
|
|
122
|
+
process.stdin.flush()
|
|
123
|
+
annotations = None
|
|
124
|
+
for line in process.stdout:
|
|
125
|
+
message = json.loads(line)
|
|
126
|
+
if message['type'] == 'error':
|
|
127
|
+
raise RuntimeError(message['message'])
|
|
128
|
+
if message['type'] == 'annotations':
|
|
129
|
+
annotations = message['annotations']
|
|
130
|
+
if message['type'] == 'done':
|
|
131
|
+
break
|
|
132
|
+
if (not annotations or len(annotations) != 1 or annotations[0]['page'] != 1
|
|
133
|
+
or annotations[0]['contents'] != 'Smoke annotation 註解'):
|
|
134
|
+
raise RuntimeError(f'Packaged annotation extraction failed: {annotations}')
|
|
115
135
|
from PySide6.QtCore import QEventLoop, QTimer
|
|
116
136
|
from PySide6.QtPrintSupport import QPrinter
|
|
117
137
|
from repdf.printing import PrintJob, PrintPage
|