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

+ +
+ +
+
+ + +
+ + +
+ +
+ +
+ {% for pos, label in [ + ('top-left', '↖ Top Left'), ('top-center', '↑ Top Center'), ('top-right', '↗ Top Right'), + ('bottom-left', '↙ Bottom Left'), ('bottom-center', '↓ Bottom Center'), ('bottom-right', '↘ Bottom Right') + ] %} + + {% endfor %} +
+
+ +
+ + Cancel +
+
+
+ + +
+
Position preview ({{ page_count }} page{{ 's' if page_count != 1 else '' }})
+
+
✍️
+
+ +
+
+{% endblock %} +'''