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
90 changes: 89 additions & 1 deletion src/pdf_equilibrist/ui/dialogs.py
Original file line number Diff line number Diff line change
Expand Up @@ -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; }
Expand Down Expand Up @@ -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"))
Expand Down
8 changes: 6 additions & 2 deletions src/pdf_equilibrist/ui/floating_item.py
Original file line number Diff line number Diff line change
Expand Up @@ -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}``

Expand Down Expand Up @@ -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", ""))
Expand Down
10 changes: 5 additions & 5 deletions src/pdf_equilibrist/ui/tabs/tab_modifier.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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)

Expand Down Expand Up @@ -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),
Expand Down
20 changes: 20 additions & 0 deletions tests/test_ui/test_text_format.py
Original file line number Diff line number Diff line change
@@ -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"
Loading