diff --git a/modules/documents/templates/document_view.html b/modules/documents/templates/document_view.html
index ed5dcd5..2ed3013 100644
--- a/modules/documents/templates/document_view.html
+++ b/modules/documents/templates/document_view.html
@@ -113,9 +113,14 @@
-
-
+ {% if cap.has_position is defined and cap.has_position %}
+ ✍️ {{ cap.name }}
+ {% else %}
+
+ {% endif %}
{% endfor %}
{% endif %}
diff --git a/modules/pdf_signature/index.py b/modules/pdf_signature/index.py
index 427581e..cb68e18 100644
--- a/modules/pdf_signature/index.py
+++ b/modules/pdf_signature/index.py
@@ -10,7 +10,7 @@
import importlib.util
from module_manager import BaseModule
-from flask import Blueprint, request, jsonify, render_template, flash, redirect, url_for
+from flask import Blueprint, request, jsonify, render_template, render_template_string, flash, redirect, url_for
# Graceful degradation: try importing optional dependencies
try:
@@ -140,7 +140,7 @@ def _get_invoice_sig(self, invoice_id):
# --- PDF signing methods ---
- def _apply_visual_signature(self, pdf_bytes):
+ def _apply_visual_signature(self, pdf_bytes, position=None, page_num=None, **kw):
if not PILLOW_AVAILABLE:
raise RuntimeError('Pillow library is not installed — cannot apply visual signature.')
@@ -185,10 +185,19 @@ def _apply_visual_signature(self, pdf_bytes):
for page in reader.pages:
writer.add_page(page)
- # Build overlay for the last page
- last_page = writer.pages[-1]
- page_w = float(last_page.mediabox.width)
- page_h = float(last_page.mediabox.height)
+ # Build overlay for the target page (default: last)
+ total_pages = len(writer.pages)
+ target_idx = total_pages - 1 # default: last page
+ if page_num is not None:
+ if page_num == 0: # 0 = first page
+ target_idx = 0
+ elif 1 <= page_num <= total_pages:
+ target_idx = page_num - 1 # 1-based to 0-based
+ # else: keep last page
+
+ target_page = writer.pages[target_idx]
+ page_w = float(target_page.mediabox.width)
+ page_h = float(target_page.mediabox.height)
# Scale signature (keep aspect ratio)
max_w = cfg.sig_max_width or 150
@@ -196,24 +205,24 @@ def _apply_visual_signature(self, pdf_bytes):
draw_w = img_w * scale
draw_h = img_h * scale
- # Position based on config
+ # Position based on config or override
margin_x = cfg.sig_margin_x if cfg.sig_margin_x is not None else 40
margin_y = cfg.sig_margin_y if cfg.sig_margin_y is not None else 40
- position = cfg.sig_position or 'bottom-left'
+ pos = position or cfg.sig_position or 'bottom-left'
- if position == 'bottom-right':
+ if pos == 'bottom-right':
x = page_w - draw_w - margin_x
y = margin_y
- elif position == 'bottom-center':
+ elif pos == 'bottom-center':
x = (page_w - draw_w) / 2
y = margin_y
- elif position == 'top-left':
+ elif pos == 'top-left':
x = margin_x
y = page_h - draw_h - margin_y
- elif position == 'top-right':
+ elif pos == 'top-right':
x = page_w - draw_w - margin_x
y = page_h - draw_h - margin_y
- elif position == 'top-center':
+ elif pos == 'top-center':
x = (page_w - draw_w) / 2
y = page_h - draw_h - margin_y
else: # bottom-left (default)
@@ -226,10 +235,10 @@ def _apply_visual_signature(self, pdf_bytes):
c.drawImage(tmp_path, x, y, width=draw_w, height=draw_h, mask='auto')
c.save()
- # Merge overlay onto last page
+ # Merge overlay onto target page
overlay_buf.seek(0)
overlay_reader = PdfReader(overlay_buf)
- last_page.merge_page(overlay_reader.pages[0])
+ target_page.merge_page(overlay_reader.pages[0])
# Write result
out = io.BytesIO()
@@ -594,7 +603,8 @@ def get_capabilities(self):
'method': 'visual',
'name': 'Visual Signature (image overlay)',
'accepts': ['pdf'],
- 'action': lambda pdf_bytes, **kw: self._apply_visual_signature(pdf_bytes),
+ 'action': lambda pdf_bytes, **kw: self._apply_visual_signature(pdf_bytes, **kw),
+ 'has_position': True,
})
if cfg.digital_enabled and cfg.certificate_key:
caps.append({
@@ -755,4 +765,141 @@ def sign_invoice(invoice_id):
)
return redirect(url_for('view_invoice', id=invoice_id))
+ @bp.route('/sign-file//')
+ @login_required
+ def sign_file_picker(file_id, cap_index):
+ """Show position picker before signing a document file."""
+ from pypdf import PdfReader as _PdfReader
+ result = module.core.storage.get(
+ module._db.session.execute(
+ module._db.text('SELECT file_path FROM document_file WHERE id = :fid'),
+ {'fid': file_id}
+ ).fetchone()[0]
+ )
+ page_count = 1
+ if result:
+ try:
+ reader = _PdfReader(io.BytesIO(result[0]))
+ page_count = len(reader.pages)
+ except Exception:
+ pass
+ return render_template_string(SIGN_POSITION_TEMPLATE,
+ file_id=file_id, cap_index=cap_index,
+ page_count=page_count)
+
+ @bp.route('/sign-file//', methods=['POST'])
+ @login_required
+ def sign_file_apply(file_id, cap_index):
+ """Apply signature with chosen position."""
+ position = request.form.get('position', 'bottom-left')
+ page_num = int(request.form.get('page_num', 0))
+ back_url = request.form.get('back_url', '/')
+
+ signers = module.core.module_manager.find_capabilities('pdf_sign')
+ if cap_index < 0 or cap_index >= len(signers):
+ flash('Signing method not available.', 'danger')
+ return redirect(back_url)
+
+ cap = signers[cap_index]
+ row = module._db.session.execute(
+ module._db.text('SELECT file_path, document_id, original_filename FROM document_file WHERE id = :fid'),
+ {'fid': file_id}
+ ).fetchone()
+ if not row:
+ flash('File not found.', 'danger')
+ return redirect(back_url)
+
+ file_path, doc_id, orig_name = row
+ try:
+ result = module.core.storage.get(file_path)
+ if not result:
+ flash('File not found in storage.', 'danger')
+ return redirect(back_url)
+ pdf_bytes, _ = result
+ signed = cap['action'](pdf_bytes, position=position, page_num=page_num)
+ module.core.storage.save(signed, file_path)
+ flash(f'File signed with {cap.get("name", "signer")}.', 'success')
+ except Exception as e:
+ module.logger.error('Sign file error: %s', e)
+ flash('Signing failed. Check server logs.', 'danger')
+
+ return redirect(back_url)
+
app.register_blueprint(bp)
+
+
+SIGN_POSITION_TEMPLATE = '''
+{% extends "base.html" %}
+{% block title %}Sign PDF — Choose Position{% endblock %}
+{% block content %}
+✍️ Sign PDF — Choose Position
+
+
+
+
+
+
+
+
Position preview ({{ page_count }} page{{ 's' if page_count != 1 else '' }})
+
+
+
+
+{% endblock %}
+'''