کسی روی یک نام یک مستطیل سیاه میکشد، هیچ چیزی را تخت نمیکند، فایل را ارسال میکند و بازبینیکننده مستطیل را انتخاب و نام را داخل یک ایمیل paste میکند. PDFiumPas جواب این را با حذف متنی در سطح عملگر میدهد: SaveAsRedacted فقط اسکالرهای Unicode را حذف میکند که جعبه کاراکترشان با یک مستطیل حذف تماس دارد، بازماندهها را از فونت و اندازه و ماتریس و مود رندر و رنگ اصلی بازمیسازد و بهجای انداختن کامل، مسیرها و تصاویر همراستا با محورها را crop میکند
چرا یک مستطیل کشیدهشده حذف متنی نیست
یک عملیات ترسیمی که روی یک استریم محتوا اضافه میشود هیچ چیزی را پنهان نمیکند، چون عملگرهای نمایش متن زیر آن هنوز در استریم هستند و هنوز به code point نگاشت میشوند. ISO 32000-1 §9.4 یک شیء متن را دنبالهای از عملگرهای موقعیتدهی و نمایش داخل BT و ET تعریف میکند؛ یک مستطیل توپر که بعداً کشیده شود صرفاً عملگر دیگری در همان استریم است. استخراج روی عملگرها راه میرود، نه روی پیکسلها، پس رشته پوشاندهشده دستنخورده برمیگردد. حذف متنی واقعی باید عملوند را حذف کند، نه اینکه خروجی را مبهم کند
پیادهسازی امن بدیهی بیرحمانه است: هر شیء صفحهای را که جعبه محصورش با یک مستطیل حذف تقاطع دارد پیدا کن و کل شیء را حذف کن. این کاری است که نسخههای قبلی PDFiumPas میکردند و درست است اما پرهزینه است. یک Tj تنها میتواند یک سطر کامل جدول را حمل کند، پس سیاه کردن یک شماره حساب تاریخ و توضیح و مبلغ را هم با خودش برد. یک fill مستطیلی که اتفاقاً یک نوار جدول تمامعرض بود کل صفحه ناپدید شد. لوگوی یک فاکتور غیبش زد چون حذف فقط یک گوشهاش را میبرید. نسخه 3.101.0 تصمیم را یک سطح پایین میبرد، از شیء صفحه به عملوند
حذف متنی در سطح عملگر دقیقاً چه چیزی را حذف میکند؟
PDFiumPas اسکالرهای Unicode را حذف میکند، نه اشیاء متن. در طول SaveAsRedacted کامپوننت یک نگاشت کاراکتر به شیء صفحه از صفحه متنی لودشده میسازد، بعد برای هر کاراکتری که متعلق به شیء تحت آزمون است جعبه کاراکتر را میخواند و آن جعبه را با هر مستطیل حذف تقاطع میدهد. کاراکترهایی که با یک مستطیل تماس دارند برای حذف علامت میخورند؛ بقیه بهعنوان بازمانده علامت میخورند. اگر هیچ تقاطعی نباشد، شیء کاملاً به حال خود رها میشود. اگر همه کاراکترها تقاطع داشته باشند، شیء درست مثل قبل یکجا حذف میشود. فقط حالت مختلط split را راه میاندازد
هر بازمانده بعد بهشکل شیء متن خودش دوباره صادر میشود که از هندل فونت اصلی، اندازه فونت اصلی، ماتریس متن به ازای هر کاراکتر، مود رندر متن اصلی و وضعیت fill و stroke شیء والد شامل ضخامت stroke و line join و line cap و dash array ساخته میشود. استفاده مجدد از هندل فونت بهجای حل کردن یک هندل جدید همان چیزی است که گلیفها را از نظر متریک یکسان نگه میدارد، و استفاده مجدد از ماتریس به ازای هر کاراکتر همان چیزی است که kerning و فاصله کلمه را بدون اجرای مجدد layout سر جای خودش نگه میدارد. هزینه آن تعداد اشیاء است: یک کاراکتر نگهداشتهشده یک شیء متن میشود، به همین دلیل TPdfRedactionOptions.MaxSplitObjects بهعنوان سقف سخت روی قطعات تولیدشده وجود دارد
procedure RedactDocument(const SourcePdf, TargetPdf: string);
var
Pdf: TPdf;
Options: TPdfRedactionOptions;
Report: TPdfRedactionReport;
begin
Pdf := TPdf.Create(nil);
try
Pdf.FileName := SourcePdf; // فایل از قبل حاشیهنویسیهای /Redact را حمل میکند
Pdf.Active := True;
Options := TPdfRedactionOptions.Default;
Options.PreservePartialObjects := True; // split در سطح عملگر (پیشفرض)
Options.RemoveIntersectingAnnotations := True;
Options.MaxSplitObjects := 20000; // سقف روی قطعات تولیدشده
if not Pdf.SaveAsRedacted(TargetPdf, Options, Report) then
raise Exception.Create(Report.ErrorMessage); // fail closed، ارسال نکن
finally
Pdf.Free;
end;
end;
مستطیلها crop میشوند، هندسه چرخیده نه
مسیرها فقط وقتی split میشوند که PDFiumPas بتواند ثابت کند مسیر یک مستطیل همراستا با محورها است. اثبات عمداً باریک است: ماتریس شیء باید هر دو جمله shear زیر 0.0001 داشته باشد، مسیر باید از چهار تا شش سگمنت تشکیل شده باشد که با MOVETO شروع و فقط با LINETO ادامه پیدا میکنند، و نقاط تبدیلشده باید درون تحمل 0.01 روی هر چهار گوشه کرانهای شیء فرود بیایند. مسیری که از این بررسی رد شود با تفریق متوالی مستطیلها کاهش مییابد، هر مستطیل حذف مجموعه بازمانده را به نوارهای چپ و راست و زیر و رو میتراشد، و هر نوار حاصل با مود fill و فلگ stroke و وضعیت paint اصلی دوباره ساخته میشود. منحنیها و مثلثها و شکلهای کلیپشده و هر چیزی که چرخیده باشد از بررسی رد میشوند و کل شیء حذف میشود
تصاویر از ISO 32000-1 §8.9 پیروی میکنند، جایی که نمونههای تصویر مربع واحدی را اشغال میکنند که از طریق ماتریس تبدیل جاری نگاشت شده است. PDFiumPas آن نگاشت را معکوس میکند تا هر قطعه بازمانده در فضای صفحه را به مختصات تصویر نرمالشده برگرداند، آنها را به بازه واحد گیره میکند و بعد با گرد کردن به داخل به ایندکس پیکسل تبدیل میکند: لبههای چپ و بالا از Ceil میروند، راست و پایین از Floor. جهتش مهم است. گرد کردن به بیرون اجازه میداد یک ستون ناقص از پیکسلهای مبدأ سمت حذفشده در لبه قطعه زنده بماند. کرانهای پیکسلی صحیح بعد به مختصات نرمالشده تبدیل میشوند و برای استخراج ماتریس قطعه استفاده میشوند، پس bitmap بریدهشده دقیقاً روی مرز پیکسلی که بریده شده فرود میآید. خود crop یک کپی سطری آگاه از stride در قالبهای Gray و BGR و BGRx و BGRA است. مثل مسیرها، تصویر چرخیده یا مورب، یا تصویری که ماتریسش جمله scale منحط دارد، یکجا حذف میشود
// بعد از یک فراخوانی موفق SaveAsRedacted
Writeln(Format('applied %d redaction(s) on %d page(s)',
[Report.RedactionCount, Report.RedactedPageCount]));
Writeln(Format('scanned %d object(s), removed %d',
[Report.ScannedObjectCount, Report.RemovedObjectCount]));
Writeln(Format('split text/path/image: %d / %d / %d',
[Report.SplitTextObjectCount, Report.SplitPathObjectCount,
Report.SplitImageObjectCount]));
Writeln(Format('preserved %d fragment(s)', [Report.PreservedFragmentCount]));
Writeln(Format('pruned %d resource name(s), swept %d object(s)',
[Report.ResourcePruneReport.RemovedNameCount,
Report.ResourcePruneReport.RemovedObjectCount]));
if Report.PreservedFragmentCount = 0 then
// هیچ چیزی قابل split نبود: هر شیء تقاطعدار یکجا انداخته شد
LogWholeObjectFallback(SourcePdf);
چرا PDFiumPas روی کاراکترهای نگاشتنشده fail closed میکند؟
چون گلیفی که اسکالر Unicode قابلبازتولید ندارد نمیتواند بهطور صادقانه بازسازی شود. بازسازی یک بازمانده یعنی صدا زدن API تنظیم متن با یک رشته، و آن برای هر کاراکتر نگهداشتهشده یک code point پایدار میخواهد. فونتهای زیرمجموعه سمبولیک با داده ToUnicode خراب یا غایب میتوانند نگاشت خالی بدهند، و بازانکود کردن با حدس خروجیای تولید میکند که روی صفحه درست به نظر میرسد در حالی که زیرش کاراکتر متفاوتی حمل میکند. PDFiumPas امتناع میکند: بررسی کاراکتر نگهداشتهشده raise میکند، استثنا داخل SaveAsRedacted گرفته میشود، TPdfRedactionReport.Succeeded با پیام در ErrorMessage برمیگردد False و تابع False برمیگرداند. همان قانون برای بودجه split هم صدق میکند، که بهجای کوتاه کردن بیسروصدای مجموعه قطعات raise میکند. وقتی سندی فونتهایی دارد که به آنها اعتماد ندارید و رفتار قطعی قدیمی را میخواهید، Options.PreservePartialObjects := False ست کنید و هر شیء تقاطعدار یکجا میرود
هرس منابع در اسکوپهای مشترک
split کردن اشیاء یتیمهایی جا میگذارد، و هرس کردن آنها بهسادگی diff کردن دیکشنری /Resources سطح صفحه نیست. ISO 32000-1 §7.8.3 اجازه میدهد همان دیکشنری منابع همزمان توسط چند صفحه، توسط Form XObjectها، توسط الگوها و توسط استریمهای ظاهر حاشیهنویسی ارجاع شود. حذف یک نام فونت چون یک صفحه استفادهاش را قطع کرده، صفحه دیگری را که هنوز استفاده میکند میشکند. PruneUnusedPdfResources پس per scope کار میکند: /Contents را حل میکند چه آرایه مستقیم باشد، چه ارجاع غیرمستقیم به آرایه، چه یک استریم تنها، بعد استفاده از منابع را از عملگرهایی که واقعاً منابع را نام میبرند جمع میکند — Tf برای فونتها، Do برای XObjectها، gs برای وضعیت گرافیکی، CS و cs و SCN و scn برای فضاهای رنگی و الگوها، sh برای سایهزنیها، BDC و DP برای خصوصیات marked-content، بهعلاوه مدخل /CS تصاویر inline. وقتی یک دیکشنری توسط چند اسکوپ مشترک شده باشد، مجموعههای نام استفادهشده پیش از حذف هر چیزی per category یکپارچه میشوند
فقط نامهایی انداخته میشوند که در همه اسکوپهایی که به دیکشنری اشاره میکنند بیارجاع بودنشان تأیید شده باشد. اسکوپی که با اطمینان parse نمیشود دستنخورده میماند، که جهت محافظهکارانه است: فایل هرسنشده صرفاً بزرگتر است، فایل بدهرسشده خراب است. دیکشنریهای بازمانده بهشکل یک بهروزرسانی افزایشی پراکنده با شمارههای نسل دقیق برگردانده میشوند، و بازنویسی دسترسپذیری بعد اشیائی را که با ناپدید شدن نامها دستنیافتنی شدند جارو میکند. TPdfResourcePruneReport گزارش میدهد ScannedScopeCount و UpdatedScopeCount و RemovedNameCount و RemovedObjectCount و تعداد بایتها و یک فلگ Succeeded. SaveAsRedacted این مرحله را خودکار روی خروجی پاکسازیشده اجرا میکند، پس مسیر حذف متنی از قبل شاملش میشود، اما تابع در سطح استریم برای pipelineهایی که خودش را میخواهند export شده است
uses
FPdfCompress;
procedure PruneResourceNames(const SourcePdf, TargetPdf: string);
var
Source, Dest: TFileStream;
Report: TPdfResourcePruneReport;
begin
Source := TFileStream.Create(SourcePdf, fmOpenRead or fmShareDenyWrite);
try
Dest := TFileStream.Create(TargetPdf, fmCreate);
try
// AllowSignedDocument همان False میماند: یک بازنویسی افزایشی
// بایت رنجهایی را که امضا پوشش میدهد نامعتبر میکند
PruneUnusedPdfResources(Source, Dest, Report);
if not Report.Succeeded then
raise Exception.Create(Report.ErrorMessage);
Writeln(Format('%d name(s) removed from %d scope(s), %d -> %d bytes',
[Report.RemovedNameCount, Report.UpdatedScopeCount,
Report.SourceByteCount, Report.OutputByteCount]));
finally
Dest.Free;
end;
finally
Source.Free;
end;
end;
وصل کردن آن به یک pipeline سند
مسیر حذف متنی هرگز سندی که لود کردید را جهش نمیدهد. SaveAsRedacted یک snapshot مجزا میگیرد، حاشیهنویسیهای /Redact را آنجا اعمال میکند، پیوستها را میچیند، گذر پاکسازی را اجرا میکند که open action و اکشنهای کاتالوگ و درختهای نام و فایلهای مرتبط و AcroForm و متادیتا را حذف میکند، منابع را هرس میکند و فقط بعد استریم خروجی را مینویسد. باز کردن دوباره آن خروجی بهشکل یک سند مستقل و استخراج مجدد متن، مرحله راستیآزمایی است که ارزش نگه داشتن در test suite خودتان را دارد، چون تنها بررسیای است که به پرسش اصلی جواب میدهد — آیا خواننده هنوز میتواند رشته را بگیرد. یک پیامد که باید برایش برنامه بریزید: split اشیاء صفحه را جایگزین میکند، پس هر هندل FPDF_PAGEOBJECT که در دست داشتید بعدش مُرده است، همان تله عمر که در هندلهای منقضی شیء صفحه بعد از یک transform توضیح داده شده
دو قطعه همسایه این workflow را کامل میکنند. تصمیم اینکه کجا مستطیلهای حذف میروند معمولاً از هندسه استخراجشده شروع میشود، و مدل بلوک و ترتیب خواندن در بلوکهای متن ساختاریافته و ترتیب خواندن منبع بهتری برای جعبههای نامزد نسبت به رانهای خام کاراکتر است. سرو کردن نتیجه به بازبینیکننده مربوط به قواعد سختسازی در ساختن یک پیشنمایش امن PDF است، جایی که پر کردن فرم و JavaScript بهصورت پیشفرض خاموش میمانند. با هم حلقهای را پوشش میدهند که بیشتر workflowهای انطباق لازم دارند: پیدا کردن، حذف متنی در سطح عملگر، راستیآزمایی با باز کردن دوباره، پیشنمایش امن. سطح کامل API و دانلود آزمایشی و شرایط لایسنس کامپوننت روی صفحه محصول PDFium Delphi Component است