From fbe94ffa976f97bd143b8b3c2a03c007d11cbf76 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 2 Nov 2026 09:06:42 +0000 Subject: [PATCH] feat(modifier): dialogue de mise en forme pour l'ajout de texte (police, taille, gras/italique, couleur) Co-Authored-By: Claude Sonnet 5.5 Claude-Session: https://claude.ai/code/session_01Bcezot9aEcwSRbdFrScYjB --- src/pdf_equilibrist/ui/dialogs.py | 90 ++++++++++++++++++++- src/pdf_equilibrist/ui/floating_item.py | 8 +- src/pdf_equilibrist/ui/tabs/tab_modifier.py | 10 +-- tests/test_ui/test_text_format.py | 20 +++++ 4 files changed, 120 insertions(+), 8 deletions(-) create mode 100644 tests/test_ui/test_text_format.py diff --git a/src/pdf_equilibrist/ui/dialogs.py b/src/pdf_equilibrist/ui/dialogs.py index f4442c1..79cd7bb 100644 --- a/src/pdf_equilibrist/ui/dialogs.py +++ b/src/pdf_equilibrist/ui/dialogs.py @@ -18,17 +18,26 @@ - ``ask_page_index`` : numéro de page via QSpinBox - ``ask_watermark_text`` : texte du filigrane - ``ask_text_input`` : saisie de texte libre (générique) +- ``ask_text_format`` : texte + police, taille, gras/italique, couleur - ``ask_image_format`` : sélection du format image (combo png/jpg/bmp/tiff) - ``show_info`` : message d'information (non bloquant pour l'UX) - ``show_error`` : message d'erreur critique - ``ensure_suffix`` : force l'extension d'un chemin issu de getSaveFileName """ from PyQt6.QtCore import QCoreApplication +from PyQt6.QtGui import QColor from PyQt6.QtWidgets import ( QDialog, QVBoxLayout, QHBoxLayout, QLabel, QLineEdit, - QPushButton, QSpinBox, QComboBox, QMessageBox, + QPushButton, QSpinBox, QComboBox, QMessageBox, QCheckBox, QColorDialog, ) +# Polices PDF standard (base 14) : famille -> (normal, gras, italique, gras+italique) +PDF_FONTS = { + "Helvetica": ("helv", "hebo", "heit", "hebi"), + "Times": ("tiro", "tibo", "tiit", "tibi"), + "Courier": ("cour", "cobo", "coit", "cobi"), +} + DIALOG_STYLE = """ QDialog { background: #2D2D2D; color: #F0F0F0; } @@ -141,6 +150,85 @@ def ask_text_input(parent, title: str, label: str) -> str | None: return txt.text().strip() or None if dlg.exec() == QDialog.DialogCode.Accepted else None +def pdf_fontname(family: str, bold: bool, italic: bool) -> str: + """Nom de police PDF (base 14) pour une famille et un style.""" + names = PDF_FONTS.get(family, PDF_FONTS["Helvetica"]) + return names[(1 if bold else 0) + (2 if italic else 0)] + + +def ask_text_format(parent, title: str, label: str) -> dict | None: + """Saisie d'un texte avec police, taille, style et couleur. + + Retourne ``{'text', 'fontname', 'family', 'bold', 'italic', 'fontsize', + 'color'}`` (couleur RGB 0–1) ou ``None`` si annulé / texte vide. + """ + tr = QCoreApplication.translate + dlg = QDialog(parent) + dlg.setWindowTitle(title) + dlg.setStyleSheet(DIALOG_STYLE) + dlg.setMinimumWidth(380) + layout = QVBoxLayout(dlg) + layout.addWidget(QLabel(label)) + txt = QLineEdit() + layout.addWidget(txt) + + row = QHBoxLayout() + family = QComboBox() + family.addItems(list(PDF_FONTS)) + size = QSpinBox() + size.setRange(4, 200) + size.setValue(14) + size.setSuffix(" pt") + bold = QCheckBox(tr("dialogs", "Gras")) + italic = QCheckBox(tr("dialogs", "Italique")) + row.addWidget(QLabel(tr("dialogs", "Police :"))) + row.addWidget(family, 1) + row.addWidget(QLabel(tr("dialogs", "Taille :"))) + row.addWidget(size) + layout.addLayout(row) + + row2 = QHBoxLayout() + row2.addWidget(bold) + row2.addWidget(italic) + row2.addStretch() + color_btn = QPushButton(tr("dialogs", "Couleur…")) + chosen = {"c": QColor(0, 0, 0)} + + def _refresh(): + c = chosen["c"] + fg = "#000" if c.lightness() > 128 else "#FFF" + color_btn.setStyleSheet( + f"background:{c.name()}; color:{fg}; border:1px solid #4A4A4A;") + + def _pick(): + c = QColorDialog.getColor(chosen["c"], dlg, tr("dialogs", "Couleur du texte")) + if c.isValid(): + chosen["c"] = c + _refresh() + + color_btn.clicked.connect(_pick) + _refresh() + row2.addWidget(color_btn) + layout.addLayout(row2) + + _ok_cancel(dlg, layout) + if dlg.exec() != QDialog.DialogCode.Accepted: + return None + text = txt.text().strip() + if not text: + return None + c = chosen["c"] + return { + "text": text, + "family": family.currentText(), + "bold": bold.isChecked(), + "italic": italic.isChecked(), + "fontname": pdf_fontname(family.currentText(), bold.isChecked(), italic.isChecked()), + "fontsize": size.value(), + "color": (c.redF(), c.greenF(), c.blueF()), + } + + def ask_image_format(parent) -> str | None: dlg = QDialog(parent) dlg.setWindowTitle(QCoreApplication.translate("dialogs", "Format d'image")) diff --git a/src/pdf_equilibrist/ui/floating_item.py b/src/pdf_equilibrist/ui/floating_item.py index cb9684d..ed9b0c5 100644 --- a/src/pdf_equilibrist/ui/floating_item.py +++ b/src/pdf_equilibrist/ui/floating_item.py @@ -52,7 +52,8 @@ class FloatingItem(QWidget): Widget interactif superposé sur une page PDF pour le placement visuel. Supporte trois types d'éléments via le dict ``data`` : - - ``{'type': 'text', 'text': str, 'fontsize': int, 'color': tuple}`` + - ``{'type': 'text', 'text': str, 'fontsize': int, 'color': tuple, + 'fontname': str (base 14), 'family': str, 'bold': bool, 'italic': bool}`` - ``{'type': 'image', 'path': str}`` - ``{'type': 'stamp', 'text': str, 'color': tuple}`` @@ -342,7 +343,10 @@ def _draw_content(self, p: QPainter, r: QRectF): r_val, g_val, b_val = (int(c * 255) for c in color) p.setPen(QPen(QColor(r_val, g_val, b_val))) fs = max(8, int(self._data.get("fontsize", 12) * self._zoom)) - p.setFont(QFont("Segoe UI", fs)) + qf = QFont(self._data.get("family", "Segoe UI"), fs) + qf.setBold(bool(self._data.get("bold"))) + qf.setItalic(bool(self._data.get("italic"))) + p.setFont(qf) p.drawText(r.adjusted(4, 4, -4, -4), Qt.AlignmentFlag.AlignLeft | Qt.AlignmentFlag.AlignVCenter, self._data.get("text", "")) diff --git a/src/pdf_equilibrist/ui/tabs/tab_modifier.py b/src/pdf_equilibrist/ui/tabs/tab_modifier.py index 43e68fb..6a4c44d 100644 --- a/src/pdf_equilibrist/ui/tabs/tab_modifier.py +++ b/src/pdf_equilibrist/ui/tabs/tab_modifier.py @@ -29,7 +29,7 @@ from pathlib import Path from PyQt6.QtWidgets import QWidget, QHBoxLayout, QFileDialog from pdf_equilibrist.ui.widgets import RibbonButton, RibbonGroup -from pdf_equilibrist.ui.dialogs import ask_watermark_text, ask_text_input, show_info, show_error, ensure_suffix +from pdf_equilibrist.ui.dialogs import ask_watermark_text, ask_text_format, show_info, show_error, ensure_suffix from pdf_equilibrist.core.document import Document from pdf_equilibrist.operations.edit import ( compress, add_watermark, @@ -152,11 +152,10 @@ def _reset_edit_ui(self): def _add_text(self): if not self.document.is_open: return - text = ask_text_input(self, self.tr("Ajouter du texte"), self.tr("Texte à insérer :")) - if not text: + fmt = ask_text_format(self, self.tr("Ajouter du texte"), self.tr("Texte à insérer :")) + if not fmt: return - data = {"type": "text", "text": text, - "fontsize": 14, "color": (0, 0, 0)} + data = {"type": "text", **fmt} self.viewer.show_floating_item(data, self.viewer.current_page_index, self._on_placement_commit, None) @@ -208,6 +207,7 @@ def _on_placement_commit(self, result: dict, page_index: int): page.insert_text( pymupdf.Point(x0, y1), result.get("text", ""), + fontname=result.get("fontname", "helv"), fontsize=result.get("fontsize", 14), color=result.get("color", (0, 0, 0)), rotate=int(-angle), diff --git a/tests/test_ui/test_text_format.py b/tests/test_ui/test_text_format.py new file mode 100644 index 0000000..ebcd384 --- /dev/null +++ b/tests/test_ui/test_text_format.py @@ -0,0 +1,20 @@ +"""Choix de police pour l'ajout de texte.""" +import pymupdf +import pytest + +from pdf_equilibrist.ui.dialogs import PDF_FONTS, pdf_fontname + + +@pytest.mark.parametrize("family", list(PDF_FONTS)) +@pytest.mark.parametrize("bold", [False, True]) +@pytest.mark.parametrize("italic", [False, True]) +def test_pdf_fontname_is_insertable(family, bold, italic): + name = pdf_fontname(family, bold, italic) + doc = pymupdf.open() + page = doc.new_page() + page.insert_text(pymupdf.Point(50, 50), "Éàç test", fontname=name, fontsize=12) + assert "test" in page.get_text() + + +def test_pdf_fontname_unknown_family_falls_back(): + assert pdf_fontname("Inconnue", False, False) == "helv"