Skip to content

Commit f63dcc5

Browse files
committed
Translate the 6 technical docs pages to English
Replaces the placeholder stubs (docs/en/*.md) with full translations of the French originals — architecture, contributing guide, operations reference, build guide, and the security/CISO analysis. The docs viewer JS was already wired correctly; only the content was pending.
1 parent 41355a6 commit f63dcc5

6 files changed

Lines changed: 1294 additions & 24 deletions

File tree

docs/en/1.INDEX.md

Lines changed: 36 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,39 @@
1-
# PDF-EquilibristDocumentation
1+
# DocumentationPDF Equilibrist
22

3-
> **English translation in progress.** The French version is available at [/docs/](/docs/).
3+
> Table of contents for the project's technical documentation.
44
5-
## Contents
5+
---
66

7-
1. [Index](1.INDEX.md) — this page
8-
2. [Architecture](2.ARCHITECTURE.md) — source structure and data flow
9-
3. [Contributing](3.CONTRIBUTING.md) — how to contribute
10-
4. [Operations](4.OPERATIONS.md) — PDF operations reference
11-
5. [Build](5.BUILD.md) — build and packaging
12-
6. [Security](6.SECURITE.md) — security policy
7+
## Documents
8+
9+
| # | Document | Content |
10+
|---|---|---|
11+
| 1 | [INDEX](1.INDEX.md) | This table of contents |
12+
| 2 | [ARCHITECTURE](2.ARCHITECTURE.md) | Architecture, data flow, multi-document handling, frameless window |
13+
| 3 | [CONTRIBUTING](3.CONTRIBUTING.md) | Dev setup, conventions, adding a tab or an operation |
14+
| 4 | [OPERATIONS](4.OPERATIONS.md) | Full reference for every `operations/` function |
15+
| 5 | [BUILD](5.BUILD.md) | PyInstaller build, spec file, Windows registration, release checklist |
16+
| 6 | [SECURITY](6.SECURITE.md) | Risk analysis, dependencies, CVEs, CISO/InfoSec recommendations |
17+
18+
---
19+
20+
## Generating the PDFs
21+
22+
```powershell
23+
python tools/build_docs_pdf.py
24+
# → docs/pdf/1.INDEX.pdf (with clickable links between documents)
25+
# → docs/pdf/2.ARCHITECTURE.pdf
26+
# → docs/pdf/3.CONTRIBUTING.pdf
27+
# → docs/pdf/4.OPERATIONS.pdf
28+
# → docs/pdf/5.BUILD.pdf
29+
```
30+
31+
---
32+
33+
## Quick links
34+
35+
- **Getting started** → read [CONTRIBUTING](3.CONTRIBUTING.md) for the dev setup
36+
- **Understanding the architecture**[ARCHITECTURE](2.ARCHITECTURE.md)
37+
- **Adding a feature**[OPERATIONS](4.OPERATIONS.md) + [CONTRIBUTING](3.CONTRIBUTING.md)
38+
- **Shipping a release**[BUILD](5.BUILD.md)
39+
- **Security / CISO assessment**[SECURITY](6.SECURITE.md)

docs/en/2.ARCHITECTURE.md

Lines changed: 325 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,327 @@
1-
# Architecture
1+
# Architecture — PDF Equilibrist
22

3-
> **English translation in progress.** The French version is available at [/docs/](/docs/).
3+
> [Index](1.INDEX.md) · [Contributing](3.CONTRIBUTING.md) · [Operations](4.OPERATIONS.md) · [Build](5.BUILD.md)
44
5-
Please refer to the French documentation while the translation is being prepared.
5+
---
6+
7+
## Overview
8+
9+
PDF Equilibrist is a desktop PDF editor built on **PyQt6** (graphical interface)
10+
and **PyMuPDF / fitz** (PDF engine). The architecture follows a strict separation
11+
between business logic and the user interface.
12+
13+
```
14+
src/pdf_equilibrist/
15+
├── main.py # Entry point
16+
├── app.py # QApplication configuration + theme
17+
├── utils.py # resource_path() — dev/exe paths
18+
├── registration.py # Windows "Open with..." registration
19+
├── core/
20+
│ └── document.py # Document model — sole owner of the fitz.Document
21+
├── ui/ # Presentation layer (PyQt6)
22+
│ ├── main_window.py
23+
│ ├── title_bar.py
24+
│ ├── viewer.py
25+
│ ├── thumbnail_panel.py
26+
│ ├── floating_item.py
27+
│ ├── page_edit_widget.py
28+
│ ├── splash_screen.py
29+
│ ├── signature_dialog.py
30+
│ ├── batch_dialog.py
31+
│ ├── widgets.py
32+
│ ├── dialogs.py
33+
│ └── tabs/
34+
│ ├── tab_afficher.py
35+
│ ├── tab_modifier.py
36+
│ ├── tab_convertir.py
37+
│ ├── tab_annoter.py
38+
│ ├── tab_page.py
39+
│ └── tab_proteger.py
40+
└── operations/ # Pure PDF logic — no UI, testable
41+
├── edit.py
42+
├── pages.py
43+
├── annotate.py
44+
├── convert.py
45+
└── protect.py
46+
```
47+
48+
---
49+
50+
## Core principle: the `changed` signal
51+
52+
`Document` inherits from `QObject` and emits `changed` after every modification.
53+
All UI components subscribe to it to refresh themselves automatically.
54+
55+
```
56+
Any PDF operation
57+
└── document.changed.emit()
58+
├── PdfViewer._on_doc_changed() → re-renders the pages
59+
├── ThumbnailPanel.rebuild() → re-renders the thumbnails
60+
└── MainWindow._on_doc_changed() → status bar, tab title
61+
```
62+
63+
**Rule**: functions in `operations/` modify the `fitz.Document` in place.
64+
The calling tab is responsible for emitting `document.changed` after the call.
65+
66+
---
67+
68+
## Typical data flow
69+
70+
```
71+
User clicks "Watermark" in tab_modifier
72+
73+
74+
tab_modifier._add_watermark()
75+
│ ask_watermark_text() → "CONFIDENTIAL"
76+
77+
operations.edit.add_watermark(document.fitz_doc, "CONFIDENTIAL")
78+
│ modifies the fitz.Document in place (insert_text on every page)
79+
80+
document.changed.emit()
81+
├── viewer.refresh() → re-renders the QLabel for each page
82+
└── thumbs.rebuild() → re-renders the thumbnails
83+
```
84+
85+
---
86+
87+
## Multi-document handling
88+
89+
`MainWindow` maintains a list of `Document` instances, one per open tab.
90+
91+
```python
92+
_documents : list[Document] # one Document per tab
93+
_active_idx : int # index of the displayed document
94+
_current_doc : Document # direct reference (rebound by _rebind_doc)
95+
```
96+
97+
### `_rebind_doc(doc)` — the central method
98+
99+
Called on every tab switch. It:
100+
101+
1. Disconnects **all** signals from the previous document (`old.changed.disconnect()`)
102+
2. Reconnects the new one to the 3 slots: `_on_doc_changed`, `viewer._on_doc_changed`, `thumbs.rebuild`
103+
3. Updates the `document` reference in every ribbon tab
104+
4. Forces an immediate refresh of the viewer and thumbnails
105+
106+
The viewer and thumbnail panel are **shared** across all tabs and rebound on
107+
every switch. They are never recreated.
108+
109+
---
110+
111+
## Frameless window
112+
113+
`MainWindow` inherits from `QWidget` (not `QMainWindow`) with `FramelessWindowHint`.
114+
115+
```
116+
MainWindow (QWidget, FramelessWindowHint)
117+
├── TitleBar ← drag via startSystemMove()
118+
├── QMenuBar ← File menu
119+
├── Ribbon (QTabBar + QStackedWidget)
120+
├── QSplitter
121+
│ ├── ThumbnailPanel ← fixed, adaptive width
122+
│ └── PdfViewer ← stretch=1, takes the remaining space
123+
└── Status bar
124+
```
125+
126+
**Resizing**: `mousePressEvent` on the edges calls
127+
`windowHandle().startSystemResize(edge)` — native Qt6 API.
128+
129+
**Moving**: `TitleBar.mousePressEvent``startSystemMove()` — native.
130+
131+
**PowerToys FancyZones / Windows Snap**: `showEvent` calls `_apply_win32_styles()`,
132+
which adds `WS_THICKFRAME | WS_MAXIMIZEBOX | WS_MINIMIZEBOX` via `SetWindowLong`.
133+
This makes the window recognizable by PowerToys and enables Windows Snap (Win+arrow).
134+
135+
**Windows 11 Snap Layouts**: `nativeEvent` intercepts `WM_NCHITTEST` and returns
136+
`HTMAXBUTTON` when the cursor hovers over the maximize button → Windows shows
137+
the Snap layout popup. `WM_SYSCOMMAND` is also intercepted for
138+
SC_MAXIMIZE / SC_RESTORE / SC_MINIMIZE sent by Snap and PowerToys.
139+
140+
> Memory safety: `ctypes.string_at` + `from_buffer_copy` copies the MSG struct's
141+
> bytes into a Python buffer before reading, avoiding the segfault that
142+
> `from_address` can cause by reading in place from a potentially invalid address.
143+
144+
---
145+
146+
## Text editing mode
147+
148+
```
149+
1. extract_text_blocks(doc, page_i)
150+
└─ get_text("dict") → list of TextBlock
151+
(rect, baseline origin, fontname, fontsize, color, font_buffer)
152+
153+
2. viewer.enter_edit_mode(blocks_by_page)
154+
└─ replaces the QLabel with a PageEditWidget per page
155+
156+
3. Click on a block → _BlockEditor (inline QTextEdit)
157+
└─ positioned pixel-perfectly on span["origin"] (baseline point)
158+
159+
4. Enter (confirm) → apply_text_edits()
160+
├─ add_redact_annot(rect, fill=None) ← removes without a white rectangle
161+
├─ apply_redactions()
162+
└─ insert_text(origin, new_text, ...) ← reinserts at the exact baseline
163+
164+
5. viewer.exit_edit_mode() → refresh()
165+
```
166+
167+
**Key point**: `fill=None` in the redaction avoids the visible white rectangle
168+
on pages with a colored background or a background image.
169+
170+
---
171+
172+
## Floating placement (text / image / stamp)
173+
174+
```
175+
viewer.show_floating_item(data, page_index, on_commit, on_cancel)
176+
└─ FloatingItem(data, zoom, page_widget)
177+
├─ drag → move (manual startSystemMove-like behavior)
178+
├─ corners → resize
179+
├─ green handle → rotate (atan2 around the center)
180+
└─ "✔ Confirm"
181+
└─ _commit()
182+
├─ pdf_rect = screen_coords / zoom
183+
└─ committed.emit({...pdf_rect, angle})
184+
└─ tab._on_placement_commit()
185+
└─ page.insert_text/insert_image/draw_rect
186+
└─ document.changed.emit()
187+
```
188+
189+
---
190+
191+
## Thumbnail panel — drag & drop
192+
193+
Drag & drop is handled by `ThumbnailPanel` (the panel itself, not the Qt list).
194+
195+
- **Internal drag**: an event filter on the viewport → `drag_started(row)`
196+
`QDrag` with mime type `application/x-pdf-equilibrist-row``doc.move_page(src, to)`
197+
- **External PDF drop**: `dragEnterEvent` on the panel accepts `.pdf` URLs →
198+
`doc.insert_pdf(src_doc, start_at=insert_before)`
199+
- **Insertion indicator**: a transparent `_DropLine` widget, overlaid on the
200+
viewport, draws the white line at the computed insertion position
201+
202+
---
203+
204+
## Printing (Ctrl+P)
205+
206+
```
207+
Ctrl+P / Print button / File → Print
208+
└── MainWindow._print()
209+
└── ui/print_dialog.print_document(document, parent)
210+
├── QPrintDialog ← Windows dialog box
211+
└── _win32_print() ← direct Win32 GDI printing
212+
├── StartDoc / StartPage
213+
├── fitz.get_pixmap(dpi/UPSAMPLE) ← render at half resolution
214+
├── StretchDIBits HALFTONE ×2 ← upscale → fine lines stay visible
215+
└── _draw_vector_strokes() ← vector GDI overlay
216+
├── get_drawings(extended=True) ← all PDF paths + XObjects
217+
├── Polyline GDI (chains) ← clean joins, no blobs
218+
└── hatching filters ← clip / multi-segment / image
219+
```
220+
221+
### Hairline strategy (AutoCAD drawings)
222+
223+
Thin lines (0–0.25 pt) rasterized at full printer resolution become invisible.
224+
Two-part solution:
225+
226+
1. **UPSAMPLE=2**: PyMuPDF renders at `dpi/2`, then `StretchDIBits HALFTONE` upscales ×2.
227+
Each fitz pixel becomes a 2×2 px block on paper → thin lines stay visible.
228+
2. **GDI overlay**: `_draw_vector_strokes()` re-traces stroke-only paths in GDI
229+
on top of the raster for clean joins and normalized widths.
230+
231+
### `_draw_vector_strokes()` — filters applied
232+
233+
| Filter | Condition | Target |
234+
|---|---|---|
235+
| Clippath | `clip is not None` | AutoCAD hatching with a clippath |
236+
| Multi-line | `len(items) > 30`, all `l` | Hatching in a single path |
237+
| No stroke | `color` absent | Solid fills, wide polylines |
238+
| Large image | image > 50% of the page | Aerial photo, scanned background |
239+
| < 500 drawings | total drawings < 500 | Excel, Word, simple PDFs |
240+
241+
`l` (line), `re` (rect), `qu` (quad), and `c` (cubic bézier) paths are all
242+
handled. Béziers are approximated with 8 line segments.
243+
244+
---
245+
246+
## PyInstaller compatibility
247+
248+
`resource_path(relative)` resolves asset paths:
249+
250+
```python
251+
# Dev : src/pdf_equilibrist/utils.py → go up 3 levels → project root
252+
# Exe : sys._MEIPASS → PyInstaller's temporary extraction folder
253+
```
254+
255+
All asset access (logo, buttons, splash screen) goes through `resource_path()`.
256+
257+
---
258+
259+
## Security — CVE checker (`cve_checker.py`)
260+
261+
Accessible from the **Help → Check for CVE vulnerabilities** menu.
262+
263+
```
264+
CVEDialog
265+
└── _ScanThread (QThread)
266+
├── cve_checker.get_installed_packages()
267+
│ └── importlib.metadata.distributions()
268+
│ └── {name: version} for each installed package
269+
│ (works in a PyInstaller exe without requirements.txt)
270+
├── cve_checker.scan_dependencies(pkgs)
271+
│ └── ThreadPoolExecutor(max_workers=8)
272+
│ └── query_package_vulnerabilities(name, version)
273+
│ └── POST https://api.osv.dev/v1/query
274+
│ {package: {name, ecosystem:"PyPI"}, version: "x.y.z"}
275+
└── progress.emit(done, total) → progress bar X/N packages
276+
```
277+
278+
**Design note**: sending the installed version to OSV.dev restricts results to
279+
CVEs that precisely affect that version, eliminating false positives for
280+
already-patched versions. The 8 parallel HTTP requests reduce scan time from
281+
roughly N×t down to ~t.
282+
283+
---
284+
285+
## Updates and statistics (`update.py` + `ui/update_dialog.py`)
286+
287+
Accessible from **Help → Check for updates**.
288+
289+
```
290+
UpdateDialog
291+
├── _CheckThread (QThread) ← starts in parallel with _StatsThread
292+
│ └── updater.get_latest_release_info(__version__)
293+
│ └── GET https://api.github.com/repos/{owner}/{repo}/releases/latest
294+
│ └── compares tag_name with __version__ (tuple)
295+
296+
└── _StatsThread (QThread)
297+
└── updater.get_download_stats(repo, __version__)
298+
└── GET https://api.github.com/repos/{owner}/{repo}/releases?per_page=100
299+
└── aggregates asset.download_count:
300+
"current": release whose tag == v{__version__}
301+
"total" : across all releases
302+
"releases": number of releases
303+
```
304+
305+
The UI shows two live counters:
306+
- **This version** (green `#6BBF4E`) — downloads for the current version
307+
- **Total** (white) — cumulative downloads across all releases
308+
309+
Both threads start simultaneously in `__init__`; the counters are shown as
310+
soon as `_StatsThread` returns, independently of `_CheckThread`'s result.
311+
312+
---
313+
314+
## Technologies
315+
316+
| Component | Library | Role |
317+
|---|---|---|
318+
| Interface | PyQt6 6.11 | Window, widgets, signals |
319+
| PDF engine | PyMuPDF 1.27 | Rendering, editing, annotations |
320+
| PDF → Word | pdf2docx 0.5 | Structural conversion |
321+
| PDF → Excel | pdfplumber + openpyxl | Table extraction |
322+
| PDF → PPT | python-pptx | Image slides |
323+
| Office → PDF | docx2pdf / LibreOffice | COM automation or headless |
324+
| Images | Pillow 12 | Image manipulation |
325+
| Build | PyInstaller | Windows onefile exe |
326+
| CVE scan | importlib.metadata + OSV.dev | Dependency vulnerabilities |
327+
| Updates | GitHub Releases API | Version check + download stats |

0 commit comments

Comments
 (0)