مقاله فنی

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 است، و همچنین دلیل این است که آن مستطیل چیزی نیست که بیشتر آدم‌ها تصور می‌کنند

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
      // Cover the account-number band on page 1 with solid black.
      // Coordinates are PDF user space: origin bottom-left, points.
      Pdf.RedactLoadedRect(0, 56, 690, 320, 706, 0, 0, 0);
      Pdf.SaveLoadedDocument('statement-covered.pdf');
    end;
  finally
    Pdf.Free;
  end;
end;

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

آن را به شما می‌دهد

نام متد «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 رسم می‌کند:

// Overlay page 2 (index 1) onto page 1 (index 0),
// scaled to 70% and nudged up-right.
Pdf.StitchLoadedPage(0, 1, 40, 380, 0.7);

// Convenience 2-up: source page on the right half of the target.
Pdf.StitchLoadedPageSideBySide(0, 1);

رشتهٔ operatorی که اضافه می‌کند یک sequence استاندارد transform-and-paint است: qq برای ذخیرهٔ 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 جعبهٔ برش خودش را داشته باشد. خودِ page dictionary را مستقیم زیر /Resources /XObject /Resources /XObjectStitchSrc با نام DoDo

. یک page dict و یک Form XObject به اندازهٔ کافی از مدل content خود مشترک دارند - هر دو به یک content stream و یک resource dictionary ارجاع می‌دهند - که خیلی از readerها نتیجه را render می‌کنند./Subtype /Formاما این یک Form XObject conforming نیست. marker /BBox /Subtype /FormDo و BBox خود را ندارد، که یعنی یک consumer سخت‌گیر حق دارد را نادیده بگیرد یا آن را متفاوت از انتظار شما 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 می‌خواهید، روی این path حساب نکنید. همان انضباط روی هر چیزی که روی object graph بارگذاری‌شده می‌سازید هم صدق می‌کند، و به همین دلیل یک جای خودش را در release pipeline می‌گیرد هر وقت سندها را programmatically mutate می‌کنید

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

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

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

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