مقاله فنی

Redaction و N-up Stitch در Delphi با HotPDF

یک درخواست روی میز شما می‌آید: یک بسته از صورت‌حساب‌های از قبل رندرشده را بگیرید، شماره‌حساب‌ها را بپوشانید، و برای صرفه‌جویی در کاغذ دو صفحه در هر برگ تحویل دهید. هر دو نیمهٔ این کار، جراحی content-stream روی یک PDF است که شما آن را نساخته‌اید، پس نه یک بوم صفحهٔ دوستانه برای نقاشی دارید و نه یک font manager که به آن تکیه کنید. شما مستقیماً در حال ویرایش گراف شیء یک سند بارگذاری‌شده هستید و operatorهای ترسیم خام را به صفحه‌ای اضافه می‌کنید که ابزار دیگری آن را چیده است. HotPDF دقیقاً دو نقطهٔ ورود برای این کار ارائه می‌دهد، و خطرناک‌ترِ آن دو همان چیزی است که بی‌ضرر به نظر می‌رسد

HotPDF یک مؤلفهٔ بومی VCL PDF برای Delphi و C++Builder است. API سند بارگذاری‌شدهٔ آن در round nine نخستین متدهایی را اضافه کرد که create محتوای تازه روی صفحه‌ای می‌سازند که از دیسک باز کرده‌اید، نه روی صفحه‌ای که از صفر ساخته‌اید. دو مورد از آن‌ها موضوع اینجا هستند: RedactLoadedRect، که یک مستطیل opaque را روی یک ناحیه می‌کشد، و StitchLoadedPage، که یک صفحه را scale می‌کند و آن را روی صفحهٔ دیگری می‌کشد. هر دو با نوشتن operatorهای content-stream ISO 32000-1 §8.5 در stream /Contents انجام می‌شوند. فهمیدن اینکه این operatorها چه می‌کنند، و به همان اندازه مهم، چه نمی‌کنند، تفاوت میان یک ابزار سالم و یک data breach است

افزودن operatorها به یک صفحهٔ بارگذاری‌شده

وقتی یک صفحه را با API معمول HotPDF می‌سازید، مؤلفه مالک content stream است و فراخوانی‌های TextOut و vector شما را برایتان serialize می‌کند. یک صفحهٔ بارگذاری‌شده متفاوت است: /Contents آن یک stream object موجود است، شاید shared، شاید بخشی از یک content array، و شما باید بدون خراب کردن آنچه از قبل آنجا هست در آن splice کنید. round nine سه helper کوچک معرفی کرد که این کار را امن می‌کنند. NewIndirectStream یک indirect THPDFStreamObject تازه با یک buffer خالی و یک /Length 0 entry allocate می‌کند؛ ResolveLoadedStream یک indirect reference را تا stream زیربنایی resolve می‌کند؛ و AppendLoadedStream bytes خام را در انتهای stream می‌نویسد و /Length را دوباره می‌نویسد تا شیء ذخیره‌شده well-formed بماند

الگویی که هر دو متد public دنبال می‌کنند یکی است. /Contents صفحه را پیدا کنید، آن را به یک stream resolve کنید، و اگر stream قابل استفاده‌ای وجود ندارد، یکی بسازید و به آن وصلش کنید. سپس operatorها را append کنید. چون bytes تازه در end stream می‌روند، مدل painter تضمین می‌کند که آن‌ها روی همهٔ چیزهایی که چیدمان اصلی کشیده بود render شوند. آن ترتیب کل مکانیزم پشت مستطیل redaction است، و همچنین دلیل این است که آن مستطیل چیزی نیست که بیشتر آدم‌ها تصور می‌کنند

HotPDF: الحاق عملگرها به صفحهٔ بارگذاری‌شده از سه تابع کمکی می‌گذرد؛ از یافتن جریان Contents صفحه تا حل یا ساختن آن و در پایان الحاق بایت‌های خام؛ مدل نقاش تضمین می‌کند این بایت‌ها روی هر آنچه چیدمان اصلی کشیده رنگ بیفکنند
NewIndirectStream و ResolveLoadedStream و AppendLoadedStream عملگرهای تازه را به انتهای یک استریم /Contents موجود پیوند می‌زنند، پس هر چه بکشند روی چیدمان اصلی صفحه فرود می‌آید بدون بازنویسی حتی یک بایت از آن

RedactLoadedRect: یک پوشش opaque، نه یک delete

RedactLoadedRect یک index صفحهٔ zero-based، چهار مختصات user-space، و سه مؤلفهٔ رنگ در بازهٔ 0–1 می‌گیرد:

var
  Pdf: THotPDF;
begin
  Pdf := THotPDF.Create(nil);
  try
    if Pdf.LoadFromFile('statement.pdf') > 0 then
    begin
      // نوار شماره‌حساب در صفحهٔ 1 را با سیاه یکدست بپوشانید.
      // مختصات همان user space خودِ PDF هستند: مبدأ پایین-چپ، واحد point.
      Pdf.RedactLoadedRect(0, 56, 690, 320, 706, 0, 0, 0);
      Pdf.SaveLoadedDocument('statement-covered.pdf');
    end;
  finally
    Pdf.Free;
  end;
end;

در پشت صحنه، این متد سه operator را وارد content stream می‌کند: یک تنظیم رنگ fill در DeviceRGB (r g b rg)، یک مسیر مستطیلی (x y w h re)، و یک fill (f). عرض و ارتفاع به‌صورت X2 - X1 و Y2 - Y1 به‌دست می‌آیند، پس شما دو گوشهٔ مخالف را می‌دهید و می‌گذارید متد extent را حساب کند. 0, 0, 0 را برای رنگ بدهید و یک نوار سیاه می‌گیرید؛ 1, 1, 1 را بدهید برای یک نوار سفید که با یک صفحهٔ سفید مطابقت دارد. مختصات، همان user space خودِ صفحهٔ بارگذاری‌شده هستند، یعنی مبدأ گوشهٔ پایین-چپ است و واحدها point هستند، و این هم یعنی برای قرار دادن هر چیزی با دقت، به /MediaBox صفحه نیاز دارید؛ GetLoadedPageBox با pbMediaBox همان را به شما می‌دهد

این را دوبار بخوانید: یک مستطیل پرشده محتوا را به‌صورت بصری می‌پوشاند، آن را حذف نمی‌کند. متن، تصویر یا اثر برداری زیر مستطیل همچنان در PDF حضور دارد، همچنان در گراف شیء هست، همچنان توسط هرکسی که صفحه را کپی کند، یک استخراج‌کنندهٔ متن اجرا کند، یا به‌سادگی مستطیل شما را از content stream حذف کند قابل‌استخراج است. این masking بصری است، نه redaction به معنای قانونی یا امنیتی. اگر داده‌ای واقعاً حساس را پنهان می‌کنید — شماره‌حساب‌ها، سوابق پزشکی، هویت‌ها، هرچیز مقرراتی — پوشاندن آن با یک جعبهٔ سیاه و ارسال فایل یک نشت داده است که منتظر کشف‌شدن است. redaction واقعی نیازمند حذف اشیای محتوایی زیربنایی است، نه رنگ‌آمیزی روی آن‌ها

HotPDF: شمارهٔ حساب پوشانده‌شده با رنگ‌کشی از حذف جان سالم می‌برد؛ نمایشگر نوار تیرهٔ توپری روی سطر حساب می‌کشد، درحالی‌که جریان محتوا همچنان رشتهٔ ACCT را فهرست و هر استخراج‌کنندهٔ متنی آن را دست‌نخورده چاپ می‌کند
عملگرهای r g b rg و re و f فقط پیکسل رویی می‌افزایند — شیء متن اصلی در گراف اشیاء زنده می‌ماند، پس یک گذر کپی-و-استخراج هر نویسه‌ای را که نوار سیاه می‌پوشاند پس می‌گیرد

نام متد «Redact» این است، و این یک هشدار مفید دربارهٔ این است که نتیجه چگونه اشتباه خوانده می‌شود، نه وعده‌ای دربارهٔ چیزی که حذف می‌کند. پیاده‌سازی در comment خودش صادق است: خودش را «visual redaction primitive» می‌نامد و یادآوری می‌کند که redaction حذف‌کنندهٔ محتوا به یک content-stream interpreter نیاز دارد که operatorهای موجود را بخواند و بازنویسی کند. مسیر سند بارگذاری‌شدهٔ HotPDF اینجا چنین کاری انجام نمی‌دهد. پس قاعدهٔ امن محدود است: از RedactLoadedRect برای masking cosmetic غیرحساس استفاده کنید - پنهان کردن یک watermark پیش‌نویس، خالی کردن یک ناحیه پیش از screenshot، پوشاندن یک logo قدیمی روی یک proof داخلی. به‌محض اینکه چیزی که زیر آن جعبه است اگر نشت کند مهم می‌شود، این متد ابزار درستی نیست، و پاسخ درست این است که سند را بدون آن داده دوباره بسازید یا از یک pipeline واقعی حذف محتوا استفاده کنید

StitchLoadedPage: scale، translate، draw

N-up imposition مسئلهٔ دوستانه‌تری است چون چیزی پنهان نمی‌شود، فقط جابه‌جا می‌شود. StitchLoadedPage یک target page index، یک source page index، یک offset X/Y، و یک scale factor می‌گیرد، و صفحهٔ منبع را در آن موقعیت و اندازه روی target رسم می‌کند:

// صفحهٔ 2 (اندیس 1) را روی صفحهٔ 1 (اندیس 0) بگذارید،
// با مقیاس 70٪ و کمی جابه‌جا به بالا-راست.
Pdf.StitchLoadedPage(0, 1, 40, 380, 0.7);

// نسخهٔ راحتی 2-up: صفحهٔ منبع در نیمهٔ راست هدف.
Pdf.StitchLoadedPageSideBySide(0, 1);

رشتهٔ operatorی که اضافه می‌کند یک sequence استاندارد transform-and-paint است: q برای ذخیرهٔ graphics state، یک cm matrix با scale روی قطر و offset در slotهای translation، /StitchSrc Do برای فراخوانی یک external object، و Q برای بازگرداندن state. q/Q pair مهم است: این transform را جدا می‌کند تا صفحهٔ stitched سیستم مختصاتش را به چیزی که بعداً اضافه می‌شود نشت ندهد. این متد همچنین خطاهای بدیهی را guard می‌کند - indexهای خارج از بازه، target برابر با source، scale غیرمثبت (که آن را به 1.0 clamp می‌کند) - و به‌جای raise کردن quietly بیرون می‌رود، پس ورودی‌های خودتان را چک کنید چون یک no-op خاموش دقیقاً شبیه success به نظر می‌رسد

StitchLoadedPageSideBySide یک thin convenience روی متد عمومی است. width media-box target را می‌خواند، آن را نصف می‌کند، و StitchLoadedPage را با آن half-width به‌عنوان X offset و یک scale ثابت از 0.5، صدا می‌زند و source را در نیمهٔ راست می‌گذارد. آن 0.5 hard-coded فرض می‌کند source و target یک width مشترک دارند؛ اگر نداشته باشند، source نیمهٔ خودش را تمیز پر نمی‌کند و شما به StitchLoadedPage عمومی با یک scale که خودتان از هر دو media box حساب می‌کنید نیاز خواهید داشت

ساده‌سازی XObject و trade-off آن با ISO

اینجاست که پیاده‌سازی یک میان‌بُر عمدی می‌زند که باید پیش از اعتماد به خروجی در سراسر viewerها آن را بدانید. یک N-up imposition درست، محتوای صفحهٔ source را در یک Form XObject می‌پیچد — یک شیء drawable خودبسنده که طبق ISO 32000-1 §8.10.1 باید /Type /XObject، /Subtype /Form، و جعبهٔ برش /BBox خودش را داشته باشد. stitch مربوط به round nine در HotPDF آن wrapper را نمی‌سازد. در عوض، خودِ page dictionary صفحهٔ source را مستقیم زیر /Resources /XObject هدف با نام StitchSrc ثبت می‌کند، سپس آن را با Do رسم می‌کند. یک page dict و یک Form XObject به اندازهٔ کافی از مدل محتوایی خود مشترک دارند — هر دو به یک content stream و یک resource dictionary ارجاع می‌دهند — که خیلی از readerها نتیجه را render می‌کنند

اما این یک Form XObject conforming نیست. marker /Subtype /Form و /BBox خودش را ندارد، که یعنی یک consumer سخت‌گیر حق دارد Do را نادیده بگیرد یا آن را متفاوت از انتظار شما clip کند. TechnicalNotes این round این را صریح می‌گوید: این روش «under most readers renders» اما «not a strictly ISO-compliant Form XObject» است، و compliance کامل نیاز دارد که یک Form XObject stream واقعی را به‌عنوان یک گام جداگانه synthesize کنید. پس خروجی stitch را مثل هر construct غیرconforming دیگری در نظر بگیرید: آن را در viewerهای مشخصی که مشتریان شما اجرا می‌کنند verify کنید، نه فقط در همان viewerی که روی ماشین خودتان دارید، و اگر PDFهای archival یا strict-validator-clean می‌خواهید، روی این مسیر حساب نکنید. همان انضباط روی هر چیزی که روی گراف شیء بارگذاری‌شده می‌سازید هم صدق می‌کند، و به همین دلیل یک گذر preflight PDF در Delphi جای خودش را در release pipeline می‌گیرد هر وقت سندها را به‌صورت برنامه‌نویسی‌شده mutate می‌کنید

HotPDF: عملیات دوخت q و ماتریس cm حامل مقیاس و آفست و فراخوان Do روی StitchSrc و Q را به صفحهٔ مقصد می‌افزاید و دیکشنری منابع، خودِ دیکشنری صفحهٔ مبدأ را می‌گیرد؛ بدون زیرنوع Form و بدون BBox
جفت q/Q یک تبدیل cm و سپس /StitchSrc Do را در بر می‌گیرد، اما چون شیء ثبت‌شده /Subtype /Form و /BBox ندارد، خروجی stitch به راستی‌آزمایی بصری در هر نمایشگری که مشتریان‌تان اجرا می‌کنند نیاز دارد

جای این‌ها و جایی که نیستند

هر دو متد ابزارهای content-stream هستند، پس مدل ذهنی همان مدلی است که برای ترسیم مستقیم به کار می‌برید. اگر pageها را از صفر با مؤلفه ساخته باشید، operatorهای vector و colour پشت این فراخوانی‌ها از HotPDF canvas drawing in Delphi آشنا به نظر می‌رسند؛ تفاوت فقط این است که اینجا دارید به streamی که شخص دیگری author کرده append می‌کنید، نه به streamی که خودتان مالک آن هستید. سه مرز را در نظر بگیرید:

  • Redaction یک عمل cosmetic است. RedactLoadedRect روی محتوا رنگ می‌زند و هرگز آن را حذف نمی‌کند. برای هرچیز حساس، منبع را دوباره بسازید یا از حذف واقعی محتوا استفاده کنید — یک جعبهٔ سیاه امنیت نیست
  • Stitch از ابتدا به‌صورت non-conforming طراحی شده است. صفحهٔ source به‌عنوان یک pseudo-XObject بدون /Subtype /Form و /BBox بخش §8.10.1 ارجاع داده می‌شود، پس رندر شدن را در viewerهای هدف خودتان تأیید کنید و هرجا اعتبارسنجی سخت‌گیرانه لازم است از آن دوری کنید
  • مختصات، user space صفحه هستند. مبدأ پایین-چپ، واحد point، بر پایهٔ media box خودِ صفحه. پیش از این‌که هر چیزی را قرار دهید، جعبه را با GetLoadedPageBox بخوانید، چون صفحه‌ای که بارگذاری کرده‌اید شاید آن اندازه‌ای نباشد که فرض کرده بودید

استفاده از این‌ها در همان محدوده‌ها یک workflow واقعی را پوشش می‌دهد: صفحات را برای چاپ rearrange کنید، ناحیه‌های غیرمحرمانه را mask کنید، و نتیجه را با SaveLoadedDocument دوباره بنویسید — همهٔ این‌ها بدون یک re-render کامل. API سند بارگذاری‌شده‌ای که این primitiveهای stitch و mask را شامل می‌شود همراه با HotPDF Delphi Component برای Delphi و C++Builder عرضه می‌شود، در کنار متدهای form-field، annotation و FDF از همان round