Skip to content

Release v2.2.0: PDF reconstruction, Italian PII recognition, single build artifact - #111

Merged
overwrite00 merged 22 commits into
mainfrom
develop
Sep 18, 2026
Merged

overwrite00 merged 22 commits into
mainfrom
develop

Conversation

@overwrite00

@overwrite00 overwrite00 commented Sep 18, 2026 •

Copy link
Copy Markdown
Owner

Summary

  • Promotes develop to stable v2.2.0, validated through betas 1-4.
  • New: "Ricostruisci PDF" reconstructs a pseudonymized PDF from its encrypted restore map back to the original values.
  • New: Italian-specific PII recognition (Codice Fiscale, Partita IVA, patente, carta d'identità, passaporto, numeri di telefono IT), blocklist/allowlist confirmation prompts, scanned-PDF warning popup.
  • Fixed: manual redaction crash (QPointF/mapToScene), blank scanned-page export, and three separate reconstruction bugs (placeholder text wrapped mid-token on narrow boxes, values silently dropped when they didn't fit their box, repeated values on other pages never restored) — see CHANGELOG for full detail.
  • Breaking: removed the Lite/Full build split; every release artifact now bundles OCR data and drops the _Lite/_Full filename suffix.
  • Security: cryptography 50.0.1, closing Dependabot alert Fix: stable cross-platform build and versioning #6 (CVE-2026-69247) once merged, since GitHub only scans the default branch.
  • CHANGELOG.md [Unreleased] has been moved under ## [2.2.0] - 2026-09-18 per the release process.

Test plan

  • pytest tests/ -v — 35/35 passing (both PyMuPDF 1.27.2.3 and the pinned 1.28.2)
  • mypy NullifyPDF.py privacy_core.py --ignore-missing-imports — clean
  • Validated across 4 beta prereleases (v2.2.0-beta.1..v2.2.0-beta.4), including a real scanner-produced PDF for the reconstruction fixes
  • Review + approve (required by branch protection) so this can be merged with a merge commit

overwrite00 and others added 22 commits August 3, 2026 14:21
…, main guard)

- Fix issue templates: default labels pointed at "bug"/"enhancement",
  which don't exist in the repo, so issues filed via template got no
  label at all. Now use the real type:bug/type:feature labels.
- project-automation.yml: label PRs by conventional-commit title prefix
  (feat/fix/docs/chore/ci/...) and by changed files (new .github/labeler.yml),
  so non-dependabot PRs get repo labels too, not just assignment.
- release.yml: verify CHANGELOG.md has a dated heading for the tagged
  version and an empty [Unreleased] section before promoting a beta to
  stable, enforcing the develop-Unreleased -> main-version changelog flow.
- New guard-main-source.yml: reject PRs into main whose head branch is
  not develop, since main must only ever be updated via develop.
Verified in an isolated Python 3.13 environment combined with PR #104's bumps: 27/27 tests pass (including the Fernet encrypt/decrypt round-trip in privacy_core.py, which is unaffected by the PKCS7/FFDH changes in 50.0.0), mypy unchanged (18 pre-existing errors), and both Lite/Full PyInstaller builds succeed. This resolves the same open Dependabot security alert (CVE-2026-69247) as PR #99, which targeted main directly via GitHub's security-update path and will be closed as a duplicate once this lands.
Verified: v7's config schema (changed-files / any-glob-to-any-file) matches our current .github/labeler.yml exactly (confirmed against the v7 README). The v5->v7 jump is internal only (ESM migration in v7, minor bug fixes in v6.x), no breaking config changes. Already rebased cleanly onto develop including the guard-main-source/labeler.yml commit (7117bdd).
…with 4 updates (#104)

Verified in an isolated Python 3.13 environment combined with PR #101's cryptography bump: 27/27 tests pass, mypy unchanged (18 pre-existing errors), and both Lite and Full PyInstaller builds succeed (confirmed PyMuPDF 1.28.2 still ships cp310-abi3 wheels, so the Python 3.13 compatibility premise holds). Dropped a requirements.txt comment that no longer matched the pinned spacy version.
Reflects the cryptography 50.0.0 security fix, the PySide6/PyMuPDF/
spaCy/pyinstaller minor-and-patch bumps, and the actions/labeler v7
CI update merged into develop.
…with 2 updates (#105)

Verified in an isolated Python 3.13 environment: 27/27 tests pass, mypy unchanged (18 pre-existing errors), Lite PyInstaller build succeeds with pyinstaller 6.22.3, and the Fernet encrypt/decrypt round-trip works correctly with cryptography 50.0.1. Both updates are patch-level bugfixes only (cryptography 50.0.1 rebuilds wheels against OpenSSL 4.0.2; pyinstaller 6.22.3 fixes Windows bootloader edge cases), no API/behavior changes relevant to this codebase.
Scope Qt6/PySide6 enum aliases (Qt.LeftButton -> Qt.MouseButton.LeftButton
and similar), rename render() -> render_page() to remove an accidental
signature clash with QWidget.render(), use getattr(sys, "_MEIPASS") for
the PyInstaller-injected attribute, and add a DetectionDict TypedDict for
the OCR-assisted AI detection dict. No behavior change; verified with
mypy (0 errors), pytest (27/27), an offscreen GUI construction smoke
test, and a Lite PyInstaller build.
… batch (#108)

The concurrent multi-file upload in softprops/action-gh-release@v3 was
timing out on the large binaries (100-300MB each) with an opaque
"Error saving asset" and no per-file retry, leaving the v2.1.1-beta.1
release with a partial (and then, after a retry, empty) set of assets.
Replaced with a sequential gh release upload loop with per-file retry.
…otion too (#109)

The v2.1.1-beta.1 release had an empty title because gh release create
was called without --title. Fixed in beta-release.yml, and applied the
same fix plus the sequential-upload-with-retry pattern (from the beta
workflow's fix for large-asset upload timeouts) to release.yml's stable
promotion step, which had the identical softprops/action-gh-release
batch-upload of the same large binaries and would hit the same failure
mode on the next promotion.
…list-change confirmations (#110)

* feat: fix manual redaction selection, extend AI PII recognition, add list-change confirmations

- Fixed a crash in PDFView.mousePressEvent/mouseMoveEvent: event.position()
  returns a QPointF, but this PySide6 version's QGraphicsView.mapToScene()
  only accepts QPoint, so drawing a manual redaction raised an uncaught
  TypeError on every mouse press. Only AI-assisted detection actually
  redacted anything until now.
- Deferred point_clicked/rect_drawn emission from mousePressEvent to
  mouseReleaseEvent, gated by the final swept distance, so a drag that
  starts on top of an existing redaction can no longer pop a modal
  mid-gesture or fire both signals for one gesture.
- Extended the Presidio AI engine with Italian-specific recognizers
  (IT_FISCAL_CODE, IT_VAT_CODE, IT_DRIVER_LICENSE, IT_IDENTITY_CARD,
  IT_PASSPORT) and added "IT" to the phone-number recognizer's supported
  regions (Italian numbers without a "+39" prefix were never matched).
  _infer_entity_type() gained matching regexes for manually-selected text.
- Manually drawing a redaction now asks whether to add the word to the
  permanent blocklist instead of silently persisting it. Removing an
  existing redaction now offers a 3-way choice: exclude only from this
  document, add to the permanent whitelist, or cancel.
- Opening a PDF with no extractable text now shows a popup (in addition
  to the existing log message) confirming it looks scanned and pointing
  at the OCR toggle; manual selection already works on such pages via
  the existing image-redaction fallback.
- Renamed the "Esporta Privacy" button to "Esporta PDF".
- Bumped to 2.2.0-beta.1: the published v2.1.1-beta.1 predates all of
  the above (new features, not a patch), so that line is being retired
  in favor of a new beta cycle.

* docs: update remove-redaction and detected-entity docs for the new UX

TROUBLESHOOTING.md's 'Wrong Redactions Applied' section still described
the old single-click-silently-allowlists behavior; USER_GUIDE.md's AI
scan entity list didn't mention the new Italian national-ID formats.
apply_redactions() was called with images=PDF_REDACT_IMAGE_REMOVE, which
deletes an image entirely if it intersects any redaction rectangle. On
a scanned PDF (the whole page is one image), redacting even a single
word wiped out the entire page, leaving only the black bar/placeholder
on an otherwise blank sheet. Switched to PDF_REDACT_IMAGE_PIXELS, which
blanks only the pixels under each redaction rect.

Verified with an end-to-end offscreen test (real cmd_export call, not
mocked): after the fix, text outside the redacted region survives
export in both anonymized and pseudonymized mode, and the embedded
page image is no longer deleted. Also verified PDF_REDACT_IMAGE_PIXELS
does not leave a visible fringe for the existing whole-image redaction
feature (Oscura Immagini), including under page rotation.
…re map

Every build/release artifact now bundles OCR data unconditionally instead
of only "Full" builds, dropping --lite/--full and the _Lite/_Full naming
from build_local.py, the release workflows, and docs.

Adds a "Ricostruisci PDF" action that decrypts a .nullifypdf-map restore
map and rewrites a previously pseudonymized PDF with the original values
back in place of their placeholders. Refuses to proceed if the PDF's
SHA-256 doesn't match the output_sha256 recorded in the map.
Reconstruction located placeholders by searching the exported PDF for
their text. On a real scanner PDF that search silently finds nothing:
apply_redactions() lays its replacement text out as a text box, so a
narrow-and-tall redaction box (the usual shape when drawing over a short
field on a scan) makes MuPDF wrap the placeholder and break it mid-token,
"DATA_" + "001". The page still reads DATA_001 and the export looked
correct, but search_for("DATA_001") returned zero hits, so every
placeholder was skipped, nothing was raised, and the reconstructed output
was identical to the input.

Restore maps are now version 2 and record each placeholder's page and
redaction box (PlaceholderOccurrence), not just the page of its first
occurrence. reconstruct_pdf addresses those boxes directly; version 1
maps still work via a character-level fallback that also finds
placeholders wrapped across lines. fit_redaction_fontsize() picks a size
that never breaks a token, used on both the export and reconstruct sides.

A second defect rode along: the restored count was incremented when a
redaction was requested, but apply_redactions() drops its replacement
text entirely when it can't be laid out in the box (e.g. long unbreakable
values like fiscal codes or IBANs) -- so a dropped value used to be
reported as restored. The count is now read back off the saved file, and
cmd_reconstruct warns when it falls short of what the map expects instead
of reporting success. The completeness check counts occurrences, not
entries, so a repeated value only partially restored across its pages no
longer passes as complete, and a same-page duplicate-box guard was
narrowed from any-intersection to containment so two adjacent but
distinct fields can't shadow each other.

Reproduced end to end against a real NAPS2 scanner PDF before fixing.
Regression tests cover the narrow-box wrap, the version 1 fallback, the
dropped-value undercount, adjacent touching boxes, and a repeated value
restored from its own per-page box.
@overwrite00 overwrite00 self-assigned this Sep 18, 2026
@github-actions github-actions Bot added type:docs Improvements or additions to documentation type:ci CI/CD, GitHub Actions scope:deps Dependency updates lang:python Python-related scope:core Core library/framework changes labels Sep 18, 2026
@overwrite00
overwrite00 merged commit e748c65 into main Sep 18, 2026
10 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

lang:python Python-related scope:core Core library/framework changes scope:deps Dependency updates type:ci CI/CD, GitHub Actions type:docs Improvements or additions to documentation

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant