باگ: تا وقتی صفحه نچرخد، کار می‌کند

جای‌گذاری امضا باید ساده باشد — کاربر روی پیش‌نمایش یک کادر می‌کشد، شما تصویر امضا را داخل همان کادر روی PDF واقعی می‌کشید. برای صفحه‌های عمودیِ راست کار می‌کرد. بعد کسی یک قرارداد اسکن‌شده را امضا کرد که دستگاه فتوکپی مهربانانه با پرچم /Rotate 90 ذخیره‌اش کرده بود، و امضا در حاشیه، به‌پهلو نشست.

علت اصلی: pdf.js (چیزی که پیش‌نمایش را در مرورگر رندر می‌کند) مقدار /Rotate صفحه را خودکار اعمال می‌کند وقتی نمایشش می‌دهد، پس کاربر دارد به صفحه‌ی بصری نگاه می‌کند و رویش کلیک می‌کند. فضای مختصات داخلی خود PDF، که هر کتابخانه‌ی امضازنی رویش می‌نویسد، صفحه‌ی نچرخیده است. اگر کدتان موقعیت کلیک را طوری رفتار کند که انگار از قبل در فضای PDF است، روی هر صفحه‌ای که راست نباشد ۹۰ درجه اشتباه می‌کند — و به‌طور گسترده‌تر، روی هر صفحه‌ای که کادر برشش با کادر رسانه‌اش یکی نباشد.

راه‌حل: مالکیت صریح ترجمه‌ی مختصات

بسته‌ی امضازنی WKFGo (به زبان Go، با استفاده از pdfcpu به‌جای خط‌لوله‌ی قدیمی پایتون که هر وقت PyPDF2 یا reportlab نصب نبود خراب می‌شد) قرارداد مختصات را در کد صریح می‌کند، نه در یک قرارداد ضمنی:

فراخوان همیشه کادر امضا را در مختصات بصری نرمال‌شده می‌دهد — بین صفر تا یک، مبدأ در گوشه‌ی بالا-چپ، دقیقاً همان چیزی که کاربر در مرورگر دیده و کلیک کرده. بسته کادر واقعیِ نمایشی و چرخش مؤثر صفحه را می‌خواند، نرمال‌شده به زاویه‌ی راست:

visible := inhAttrs.MediaBox
if inhAttrs.CropBox != nil {
    visible = inhAttrs.CropBox // the region pdf.js actually displays
}
rot := ((inhAttrs.Rotate % 360) + 360) % 360
rot = (rot / 90) * 90

اگر صفحه ۹۰ یا ۲۷۰ درجه چرخیده باشد، عرض و ارتفاع برای ابعاد بصری جابه‌جا می‌شوند — چون چیزی که روی صفحه شبیه عرض صفحه به نظر می‌رسد، در فضای نچرخیده‌ی PDF همان ارتفاعش است:

if rot == 90 || rot == 270 {
    g.VisualW, g.VisualH = visible.Height(), visible.Width()
}

هر چیزی پایین‌دست‌تر — موقعیت کادر، مقیاس تصویر — در همین فضای بصری محاسبه می‌شود، بعد به جای‌گذاری واترمارک pdfcpu سپرده می‌شود، که خودش مستقل /Rotate را جبران می‌کند. این دو جبران در وسط به هم می‌رسند و خنثی می‌شوند، به‌جای اینکه یکی‌شان بی‌صدا اتفاق نیفتد.

باگ دومی که در باگ اول پنهان بود: نسبت تصویر

حتی با درستیِ فضای مختصات، کشیدن تصویر امضا تا دقیقاً پر کردن یک کادر پهن و کوتاه، بد به نظر می‌رسد — امضاها له می‌شوند. راه‌حل رفتار CSS object-fit: contain را تکرار می‌کند: تصویر را به بزرگ‌ترین اندازه‌ای که داخل کادر جا می‌شود مقیاس کن، با حفظ نسبت ابعاد، بعد وسط‌چین کن:

scale := boxW / float64(imgW)
if s := boxH / float64(imgH); s < scale {
    scale = s
}
fitW, fitH := float64(imgW)*scale, float64(imgH)*scale
fitX := boxX + (boxW-fitW)/2
fitY := boxY + (boxH-fitH)/2

این دقیقاً همان رفتاری است که پیش‌نمایش مرورگر برای نشان‌دادن کادر قبل از امضا استفاده می‌کند، پس چیزی که امضاکننده تأیید کرده و چیزی که روی PDF می‌نشیند، به یک شکل محاسبه می‌شوند.

جزئیات سوم: پس‌زمینه‌ی سفید به‌طور پیش‌فرض شفاف نیست

امضایی که روی بوم کشیده می‌شود، یک مستطیل سفید با خط‌های تیره است، نه یک PNG شفاف. اگر همان‌طور مهر شود، یک جعبه‌ی سفید متن زیرش را می‌پوشاند. ProcessSignatureImage هر پیکسل نزدیک‌به‌سفید (R، G و B همه بالای ۲۱۰) را قبل از مهرزدن کاملاً شفاف می‌کند، پس امضا بدون پوشاندن سند تمیز رویش می‌نشیند.

اشتباهات رایج

اعتماد به فیلدهای «اندازه» روی صفحه‌ی چرخانده‌شده. عرض/ارتفاع MediaBox همیشه در جهت اصلی PDF است — روی صفحه‌ی ۹۰ درجه چرخانده‌شده خامش را بخوانید و هر محاسبه‌ای جابه‌جا می‌شود.

تست فقط با PDF‌های عمودیِ راست. این باگ تا وقتی کسی یک سند اسکن‌شده، چرخانده‌شده یا افقی امضا نکند دیده نمی‌شود — که دقیقاً همان نوع سندی است که معمولاً بیشتر به امضا نیاز دارد (قراردادی که از فکس اسکن شده، فرمی که به‌پهلو عکس گرفته شده).

قفل‌کردن آستانه‌ی سفید روی دقیقاً ۲۵۵. تصاویر امضای فشرده یا اسکن‌شده به‌ندرت پس‌زمینه‌ی کاملاً سفید دارند — آنتی‌الیاسینگ و آرتیفکت‌های JPEG پیکسل‌های نزدیک‌به‌سفید باقی می‌گذارند که چک سخت‌گیرانه‌ی == 255 مات باقی‌شان می‌گذاشت.

سؤالات متداول

آیا این PDF‌های افقی که چرخانده نشده‌اند (فقط صفحه‌ی پهن) را هم مدیریت می‌کند؟

بله — جهت از /Rotate و شکل صفحه (پهن در برابر بلند) مستقل هستند؛ محاسبات فضای بصری هر دو را بدون ویژه‌سازی جداگانه مدیریت می‌کند.

امضا با چه فرمت‌های تصویری آپلود می‌شود؟

PNG، JPEG و GIF به‌طور بومی رمزگشایی می‌شوند؛ خروجی همیشه یک PNG پردازش‌شده برای شفافیت است، صرف‌نظر از فرمت ورودی.

آیا این روی حالت‌های واقعیِ چرخش تست شده، نه فقط مسیر خوش‌بینانه؟

بله — مجموعه‌ی تست روی هر چهار مقدار چرخش، ترکیب‌شده با هندسه‌ی صفحه‌ی عمودی و افقی، مهر می‌زند و چک می‌کند که مهر در همان موقعیت بصری‌ای بنشیند که یک انسان انتظار دارد، صرف‌نظر از اینکه PDF زیرین جهت را چطور رمزگذاری کرده.

جمع‌بندی

«امضا جای اشتباه است» به‌ندرت یک باگ کشیدن است — تقریباً همیشه یک باگ فضای مختصات است، جایی که کد بی‌صدا فرض می‌کند فضایی که کاربر می‌بیند و فضایی که فایل ذخیره می‌کند یکی است. نیستند، همان لحظه‌ای که چرخش وارد می‌شود، و راه‌حل این است که ترجمه‌ی بین این دو را یک قدم صریح و تست‌شده کنید، نه یک فرض.