Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 27 additions & 1 deletion .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -80,9 +80,35 @@ jobs:
(rf'"softwareVersion":\s*"{ANY}"', f'"softwareVersion": "{new}"'),
(rf'v{old_re}', f'v{new}'),
], True),
# changelog.html carries the *whole* release history: every past
# release has a ``<span class="version-tag">v...</span>`` heading
# that must NEVER change. A global ``v{old}`` rewrite clobbered the
# topmost historical heading every release (this is why v1.13.16
# "disappeared" in 72f25e8: its heading was renamed to v1.13.17).
# Anchor the bump to the footer changelog link ONLY. New release
# headings (incl. moving the "Latest" badge) are an editorial step
# done by hand in the same PR that ships the feature work.
("docs/changelog.html", [
(rf'(<a href="changelog\.html">)v{old_re}(</a>)', rf'\g<1>v{new}\g<2>'),
], True),
# Every remaining marketing page carries the current version only,
# in an eyebrow badge and/or the footer changelog link (no historical
# version strings — verified by grep). The global ``v{old}`` -> ``v{new}``
# rewrite is therefore safe and keeps all pages in lock-step. required=True
# so the release fails loudly if a page ever drifts off the old tag
# (instead of silently masking the drift).
("docs/download.html", [
(rf'v{old_re}', f'v{new}'),
], True),
("docs/features.html", [
(rf'v{old_re}', f'v{new}'),
], True),
("docs/privacy.html", [
(rf'v{old_re}', f'v{new}'),
], True),
("docs/docs.html", [
(rf'v{old_re}', f'v{new}'),
], False),
], True),
("snap/snapcraft.yaml", [
(rf"version:\s*'{ANY}'", f"version: '{new}'"),
], True),
Expand Down
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,8 @@ test_*.pdf
test_*.xlsx
*.tmp
*.bak
# Debug repro artifacts for the thumbnails sidebar
thumb_repro*.png

# Build logs locais
pyinstaller.log
Expand Down
7 changes: 6 additions & 1 deletion app/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -382,7 +382,12 @@ def _open_fitz(self, path: str):
import fitz
doc = fitz.open(path)
if doc.needs_pass and self._pdf_password:
doc.authenticate(self._nfc(self._pdf_password))
# PyMuPDF's authenticate() returns a falsy value (0) on a
# wrong password and leaves the document locked — mirror
# _open_reader and raise instead of handing back a Document
# whose pages can't be read.
if not doc.authenticate(self._nfc(self._pdf_password)):
raise ValueError(t("tool.err.wrong_password"))
return doc

def _clear_pdf_password(self) -> None:
Expand Down
58 changes: 55 additions & 3 deletions app/i18n.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
import locale
import logging
import os
import re
import shutil
import sys
import threading
Expand Down Expand Up @@ -208,13 +209,54 @@ def _save_config_language(lang: str):
# `check_for_store_update` can suppress further nags until a NEWER
# version is published.

# A dismissed version must be a plain dotted-numeric string (1 to 4
# components, e.g. "1", "1.13", "1.13.17", "1.13.17.0"). Anything else —
# a bare integer serialized without dots (e.g. "281530812399616") or
# junk — is treated as corrupt and ignored. Without this guard a bogus
# huge value like "281530812399616" parses (via _parse_version) to a
# tuple that dwarfs every real Store version, so `check_for_store_update`
# would suppress ALL future update notifications forever.
#
# The format regex alone is NOT enough: a single gigantic component like
# "281530812399616" is a valid dotted-numeric string (0 dots), so it
# passes the regex and would still poison the comparison. MSIX / Windows
# package versions are Major.Minor.Build.Revision where every component
# is a UInt16 (0-65535), so we additionally reject any component that
# exceeds that ceiling. This rejects "281530812399616" while keeping
# real versions like "1.13.17", "1.13.17.0" and small bare ints like
# "12345".
_STORE_VERSION_RE = re.compile(r"^\d+(\.\d+){0,3}$")
_STORE_VERSION_COMPONENT_MAX = 65535 # UInt16 ceiling per MSIX spec


def _is_valid_store_version(value) -> bool:
"""True if ``value`` is a well-formed dotted-numeric version string.

Beyond matching the dotted-numeric *format*, each integer component
must fit in a UInt16 (<= 65535) — the MSIX/Windows package-version
range. This rejects an oversized bare integer (e.g.
``"281530812399616"``) that would otherwise pass the format check
and permanently suppress update notifications.
"""
if not isinstance(value, str) or not _STORE_VERSION_RE.match(value):
return False
# Regex guarantees every split part is a non-empty run of digits, so
# int() cannot raise here; the only remaining check is magnitude.
return all(int(part) <= _STORE_VERSION_COMPONENT_MAX
for part in value.split("."))


def get_dismissed_store_version() -> str | None:
"""Return the Store version the user last dismissed, or ``None``.

A returned value means: "the user already saw and dismissed this
version; do not nag again unless the Store publishes something
newer". Stored under the ``dismissed_store_version`` key in
config.json.

If the persisted value is malformed (not a dotted-numeric version),
it is treated as "nothing dismissed" AND scrubbed from config so the
corrupt key can't keep suppressing notifications on every launch.
"""
try:
with open(_CONFIG_PATH, "r", encoding="utf-8") as f:
Expand All @@ -224,8 +266,13 @@ def get_dismissed_store_version() -> str | None:
if not isinstance(cfg, dict):
return None
val = cfg.get("dismissed_store_version")
if isinstance(val, str) and val:
if val is None:
return None
if _is_valid_store_version(val):
return val
# Corrupt value: drop it so it stops masking real updates.
with contextlib.suppress(Exception):
_update_config(lambda c: c.pop("dismissed_store_version", None))
return None


Expand All @@ -235,12 +282,17 @@ def set_dismissed_store_version(version: str | None) -> None:
Pass a version string (e.g. ``"1.13.17"``) to record the dismiss.
Pass ``None`` to clear the flag (so the next available-update
notification is shown unconditionally).

Malformed version strings are rejected (never persisted) to avoid
writing a value that would later suppress all notifications.
"""
def _mutate(cfg: dict) -> None:
if version:
if version and _is_valid_store_version(version):
cfg["dismissed_store_version"] = version
else:
elif not version:
cfg.pop("dismissed_store_version", None)
# An invalid non-empty version is silently ignored: neither
# written nor used to clear an existing valid value.

_update_config(_mutate)

Expand Down
1 change: 0 additions & 1 deletion app/tools/_pdf_extract.py
Original file line number Diff line number Diff line change
Expand Up @@ -1180,7 +1180,6 @@ def _split_component_on_y_gaps(
drawing_idx_list: list[int] = []
any_text = False
consumed_text: list[int] = []
seen_text: set[int] = set()
# Precompute cell bboxes so we can pre-assign each text block
# to whichever cell contains its centroid (handles the common
# PyMuPDF case where adjacent columns of one table row land in
Expand Down
24 changes: 24 additions & 0 deletions app/translations.json
Original file line number Diff line number Diff line change
Expand Up @@ -416,6 +416,9 @@
"viewer.presentation": "Presentation",
"viewer.fullscreen": "Fullscreen",
"viewer.toc": "Bookmarks",
"viewer.sidebar.contents": "Contents",
"viewer.sidebar.pages": "Pages",
"viewer.thumbnails.loading": "Rendering thumbnails...",
"viewer.toc_empty": "No bookmarks in this PDF",
"viewer.night_mode": "Night reading mode (invert colors)",
"update.changes": "What's new in this version:",
Expand Down Expand Up @@ -1028,6 +1031,9 @@
"viewer.presentation": "Apresentação",
"viewer.fullscreen": "Ecrã inteiro",
"viewer.toc": "Marcadores",
"viewer.sidebar.contents": "Índice",
"viewer.sidebar.pages": "Páginas",
"viewer.thumbnails.loading": "A renderizar miniaturas...",
"viewer.toc_empty": "Este PDF não tem marcadores",
"viewer.night_mode": "Modo noturno (inverter cores)",
"update.changes": "Novidades nesta versão:",
Expand Down Expand Up @@ -1640,6 +1646,9 @@
"viewer.presentation": "Presentación",
"viewer.fullscreen": "Pantalla completa",
"viewer.toc": "Marcadores",
"viewer.sidebar.contents": "Índice",
"viewer.sidebar.pages": "Páginas",
"viewer.thumbnails.loading": "Renderizando miniaturas...",
"viewer.toc_empty": "Este PDF no tiene marcadores",
"viewer.night_mode": "Modo nocturno (invertir colores)",
"update.changes": "Novedades en esta versión:",
Expand Down Expand Up @@ -2252,6 +2261,9 @@
"viewer.presentation": "Présentation",
"viewer.fullscreen": "Plein écran",
"viewer.toc": "Signets",
"viewer.sidebar.contents": "Sommaire",
"viewer.sidebar.pages": "Pages",
"viewer.thumbnails.loading": "Génération des miniatures...",
"viewer.toc_empty": "Ce PDF n'a pas de signets",
"viewer.night_mode": "Mode nuit (inverser les couleurs)",
"update.changes": "Nouveautés de cette version :",
Expand Down Expand Up @@ -2864,6 +2876,9 @@
"viewer.presentation": "Präsentation",
"viewer.fullscreen": "Vollbild",
"viewer.toc": "Lesezeichen",
"viewer.sidebar.contents": "Inhalt",
"viewer.sidebar.pages": "Seiten",
"viewer.thumbnails.loading": "Miniaturansichten werden erstellt...",
"viewer.toc_empty": "Dieses PDF hat keine Lesezeichen",
"viewer.night_mode": "Nachtmodus (Farben invertieren)",
"update.changes": "Neuigkeiten in dieser Version:",
Expand Down Expand Up @@ -3476,6 +3491,9 @@
"viewer.presentation": "演示",
"viewer.fullscreen": "全屏",
"viewer.toc": "书签",
"viewer.sidebar.contents": "目录",
"viewer.sidebar.pages": "页面",
"viewer.thumbnails.loading": "生成缩略图...",
"viewer.toc_empty": "此PDF没有书签",
"viewer.night_mode": "夜间阅读模式(反转颜色)",
"update.changes": "此版本的新功能:",
Expand Down Expand Up @@ -4088,6 +4106,9 @@
"viewer.presentation": "Presentazione",
"viewer.fullscreen": "Schermo intero",
"viewer.toc": "Segnalibri",
"viewer.sidebar.contents": "Sommario",
"viewer.sidebar.pages": "Pagine",
"viewer.thumbnails.loading": "Generazione miniature...",
"viewer.toc_empty": "Questo PDF non ha segnalibri",
"viewer.night_mode": "Modalità notturna (inverti colori)",
"update.changes": "Novità in questa versione:",
Expand Down Expand Up @@ -4700,6 +4721,9 @@
"viewer.presentation": "Presentatie",
"viewer.fullscreen": "Volledig scherm",
"viewer.toc": "Bladwijzers",
"viewer.sidebar.contents": "Inhoud",
"viewer.sidebar.pages": "Pagina's",
"viewer.thumbnails.loading": "Miniaturen genereren...",
"viewer.toc_empty": "Deze PDF heeft geen bladwijzers",
"viewer.night_mode": "Nachtmodus (kleuren inverteren)",
"update.changes": "Wat is er nieuw in deze versie:",
Expand Down
Loading