تجزئ PDFium Component الخطوط المغروسة أصلاً في ملف PDF قائم عبر SubsetPdfDocumentFonts في وحدة FPdfDocumentFontSubset. تستبدل الدالة برنامج /FontFile2 من TrueType مغروساً بآخر أصغر فقط عندما تثبت موارد الخط نفسها كل محرف يمكن للمستند الإشارة إليه؛ والخط الذي لا يمكن إثبات مجال محارفه يُترك كما هو بعينه
السبب المعتاد للرغبة في هذا أرشيف لم تنتجه أنت. الفواتير وكشوف الحسابات والتقارير من مولدات أقدم تحمل كثيراً Arial كاملاً أو واجهة CJK كاملة في كل ملف، وبضع مئات الكيلوبايتات من الخط لكل مستند من صفحتين تتراكم سريعاً عبر بضعة ملايين من المستندات. إعادة العرض من المصدر خيار نادر المنال. ما لديك هو الـ PDF، والسؤال الآمن الوحيد هو كم من كل خط مغروس يمكن أن يذهب دون أن تفقد صفحة واحدة محرفاً
لماذا يخاطر تجزئة ملف PDF قائم بفقد محارف؟
يخاطر تجزئة ملف PDF قائم بفقد محارف لأن المعالج اللاحق لا يعرف أي محارف استخدمها الكاتب الأصلي. المنتج الذي غرس الخط عرف نصه الخاص؛ والأداة التي تجري بعده عليها أن تعيد بناء تلك المعرفة، وأي محرف تفوته يتحول صندوقاً فارغاً أو شكل .notdef في صفحة لا ينظر إليها أحد حتى ينظر إليها عميل
المقاربة البديهية أن تحلل كل تدفق محتوى، وكل Form XObject، وكل مظهر تعليقة، وكل مظهر افتراضي حقل، وتجمع الأكواد المعروضة بكل خط، وتحفظ تلك المحارف. تعمل حتى تصطدم بمورد خط مشترك عبر صفحات ونماذج، أو حقل نموذج يجدد عارضٌ مظهره من /DA، أو تدفق محتوى بتسلسل معاملات لا يمثله الماسح. لا تمسح SubsetPdfDocumentFonts تدفقات المحتوى عمداً. تشتق مجال المحارف من البيانات الوصفية التي لا بد أن يحملها الـ PDF عن كل خط على أي حال: خريطة /CIDToGIDMap، ومصفوفتي العرض /W و /W2، وصورة /CIDSet النقطية، و CMap /ToUnicode، ومصفوفة /Widths للخط البسيط، و /Encoding. وحين لا تحدد تلك البيانات المجال يبقى الخط لم يُمس
حفظ معرفات المحارف الأصلية كي تبقى تدفقات المحتوى لم تمس
خلفية التجزئة خلف SubsetPdfDocumentFonts، أي TPdfTrueTypeSubsetBackend، تحفظ كل خانة معرف محرف أصلية، فلا تحتاج أي تدفق محتوى أو CID أو /CIDToGIDMap إلى إعادة كتابة قط. تعيد بناء جدولي glyf و loca، وتمنح كل محرف غير مستخدم مدى loca بطول صفري وتترك maxp.numGlyphs كما هو. المحرف 4711 ما يزال المحرف 4711 بعد التجزئة؛ ليست له مخططات تفصيلية فقط إلا إذا استطاع المستند الوصول إليه
يزيل ذلك الاختيار أخطر جزء في تجزئة مستند منته. إعادة ترقيم المحارف ستعني إعادة كتابة كل سلسلة في كل تدفق محتوى يستخدم الخط، مع تدفق المطابقة، ومع أي شيء يخزن معرفات المحارف مؤقتاً. تكلف المعرفات المحفوظة قليلاً من الحجم، لأن loca تحفظ مدخلاً واحداً لكل محرف أصلي، لكن جل برنامج TrueType بيانات مخططات، وهذا هو ما يذهب. وتتبع المحارف المركبة مكوناتها، فحرف منقّوط يحفظ الحرف الأساس والنقطة التي بُني منها؛ وتُخفق مراجع المركبات الدائرية أو العميقة فوق الحد الخط بدل إنتاج غلق مكسور
يتبع الخط المعاد تسميته أيضاً ISO 32000-1 §9.6.4، التي تشترط أن يبدأ اسم خط مجزأ بستة حروف كبيرة وعلامة زائد. تشتق المكوّن الحروف الستة من بصمة SHA-256 لبرنامج الخط الجديد، ويقتطع أي بادئة ABCDEF+ قائمة أولاً، ويحل التصادمات بين الخطوط في المستند نفسه، ويكتب الاسم الجديد في /FontName للوصف و /BaseFont لقاموس الخط معاً، شاملاً الأب Type0 لـ CIDFont
uses
System.SysUtils, System.Classes, FPdfDocumentFontSubset;
procedure SubsetEmbeddedFonts(const SourceName, TargetName: string);
var
Source, Target: TFileStream;
Options: TPdfDocumentFontSubsetOptions;
Report: TPdfDocumentFontSubsetReport;
Output: TBytes;
begin
Options := TPdfDocumentFontSubsetOptions.Default;
Source := TFileStream.Create(SourceName, fmOpenRead or fmShareDenyWrite);
try
if not SubsetPdfDocumentFonts(Source, Options, Output, Report) then
raise Exception.Create('Font subsetting failed: ' + Report.ErrorMessage);
finally
Source.Free;
end;
// المخرج دوماً PDF تام: الملف المعاد بناؤه عندما تكون Report.Applied
// True، وإلا نسخة مطابقة بايتياً من المصدر
Target := TFileStream.Create(TargetName, fmCreate);
try
if Length(Output) > 0 then
Target.WriteBuffer(Output[0], Length(Output));
finally
Target.Free;
end;
end;
كيف يثبت SubsetPdfDocumentFonts مجال محارف CIDFont؟
يثبت SubsetPdfDocumentFonts مجال محارف خط CIDFontType2 من مطابقته من CID إلى محرف، والمطابقة تقرر كم دليل إضافي يلزم. تدفق /CIDToGIDMap صريح (ISO 32000-1 §9.7.4.2) هو أصلاً قائمة بكل محرف يستطيع الخط مخاطبته، فكل GID في التدفق ينضم إلى المجال. وتدفق مطابقة أكبر من 65536 مدخلاً، أو ذو عدد بايتات فردي، يرسب الملف التعريفي المحدود ويُحفظ الخط
/CIDToGIDMap /Identity هي الحالة الصعبة، وهي أيضاً الأكثر شيوعاً في الملفات الحديثة. تقول Identity إن CID يساوي GID، وهو ما لا يخبرك شيئاً عن أي CIDs يستخدمها المستند. لذلك يشترط المكوّن دليلاً أولياً: مصفوفة عرض /W أو /W2 صريحة (§9.7.4.3)، تشير مدخلات CID فيها إلى محارف أعلن المنتج مقاييس لها، أو تدفق /CIDSet في وصف الخط (§9.8.3)، صورة نقطية ببت واحد لكل CID، البت الأعلى أولاً. ويُحفظ خط Identity مجرد بلا واحدة منهما بالسبب Identity CIDToGIDMap requires an explicit W array or CIDSet stream
فخ استخدام /W دليلاً هو /DW. يحق للمنتج أن يترك خارجاً كل CID يساوي عرضه العرض الافتراضي، والكاتب المحسّن جيداً يفعل ذلك بعينه لعرضات المحارف الأكثر شيوعاً. لو جاء المجال من /W وحدها اختفت تلك المحارف بهدوء من التجزئة. ولكي يُسد تلك الفجوة يمشي المكوّن كل خط Type0 تشير /DescendantFonts فيه إلى الـ CIDFont، ويشترط أن تكون /Encoding له /Identity-H أو /Identity-V فيكون الكود ذو البايتين هو الـ CID، ويدمج أكواد المصدر لكل مدخل beginbfchar و beginbfrange من CMap /ToUnicode له (§9.10.3) في المجال. وأي ترميز آخر، أو تدفق /ToUnicode لا يمكن فك ترميزه ضمن الميزانية، يحفظ الخط
خطوط TrueType البسيطة: من Widths إلى معرفات المحارف
خطوط /Subtype /TrueType أحادية البايت تثبت عبر مدى أكوادها وترميزها، ثم تُحل إلى معرفات محارف عبر cmap لبرنامج الخط نفسه. يشترط المكوّن 0 <= FirstChar <= LastChar <= 255 ومصفوفة /Widths طولها بالضبط LastChar - FirstChar + 1؛ والمصفوفة القصيرة أو الطويلة أو غير العددية تعني أن مجال الكود غير مثبت، فيبقى الخط كما هو
كل كود في ذلك المدى، مع أي كود مسمى في /Differences أو مغطى بـ /ToUnicode، يُربط بعد ذلك بالطريقة التي يصفها ISO 32000-1 §9.6.6. الأكواد تحت /WinAnsiEncoding أو /MacRomanEncoding أو /StandardEncoding تمر بالجدول المطابق إلى Unicode. وتُحل أسماء محارف /Differences عبر قائمة Adobe Glyph List، وأسماء uniXXXX و uXXXX، وأسماء المحارف الماكينية القياسية البالغة 258، وجدول أسماء post بصيغة 2.0 الخاص بالخط. وتُبحث القيم الناتجة في جداول cmap الفرعية بصيغ 0 و4 و6 و12، شاملة Unicode (3,1) و (3,10) و (0,*)، و Mac Roman (1,0)، و Windows Symbol (3,0) عند F000 مضافاً إليه الكود
عندما لا يكون للخط ترميز أساس إطلاقاً يشير المكوّن إلى اتحاد كل تفسير معقول بدل تخمين واحد. يحفظ ذلك بضعة محارف على الأرجح غير مستخدمة، وهو الاتجاه الصواب في الخطأ. والحالة المعاكسة صارمة: اسم /Differences لا يُربط بمعرف محرف مثبت يرفض الخط كله، لأن اسم غير محلول واحد يعني أن المجال غير تام
procedure ReportFontSubset(Source: TStream; Log: TStrings);
var
Options: TPdfDocumentFontSubsetOptions;
Report: TPdfDocumentFontSubsetReport;
Output: TBytes;
I: Integer;
begin
Options := TPdfDocumentFontSubsetOptions.Default;
Options.SubsetOptions.MinimumSavingPercent := 10; // لكل خط
Options.MinimumDocumentSavingPercent := 5; // الملف كله
if not SubsetPdfDocumentFonts(Source, Options, Output, Report) then
begin
Log.Add('Rejected: ' + Report.ErrorMessage);
Exit;
end;
Log.Add(Format('Fonts %d, subset %d, preserved %d, glyphs %d',
[Report.FontCount, Report.SubsetFontCount, Report.PreservedFontCount,
Report.GlyphCount]));
Log.Add(Format('Font bytes %d -> %d, file bytes %d -> %d',
[Report.OriginalFontBytes, Report.SubsetFontBytes,
Report.SourceByteCount, Report.OutputByteCount]));
for I := 0 to High(Report.Issues) do
Log.Add(Format(' object %d kept: %s',
[Report.Issues[I].ObjectNumber, Report.Issues[I].Reason]));
end;
ماذا يترك SubsetPdfDocumentFonts لم يمس؟
يترك SubsetPdfDocumentFonts كل خط لا يستطيع إثباته، ويبلّغ عن كل واحد في Report.Issues برقم الكائن وسبب بدل إخفاق المستند. وتكون Report.Complete مساوية True فقط عندما لم يُحفظ أي خط. حالات الحفظ تستحق المعرفة قبل أن تعد بتقليص حجم:
- برامج
/FontFileمن النوع 1 و CFF أو OpenType /FontFile3، بُلِّغ عنها بالرسالةOnly TrueType FontFile2 programs are supported - CIDFonts من نوع Identity بلا
/Wأو/W2أو/CIDSet، و CIDFonts بلا تدفق مطابقة ولا/Identity - برامج خطوط يُشار إليها من غير وصف خطوطها، وأوصاف مشتركة مع مستهلك ليس
CIDFontType2أوTrueTypeبسيطاً - خطوط تفوّت تجزئتها
SubsetOptions.MinimumSavingPercent، وكل الخطوط عندما يفوّت الملف المعاد بناؤهMinimumDocumentSavingPercent؛ وفي تلك الحالة تكونReport.AppliedمساويةFalseوالمخرج البايتات الأصلية
مدخلان يفشلان جملة بدل حفظهما. المستندات الموقعة والمستندات ذات حقول التواقيع تُرفض، لأن استبدال كائنات الخطوط سيُبطل مدى بايتات كل توقيع، والملفات المشفرة تحتاج مصدراً مفكوك التشفير أولاً. تكتب التشغيلة الناجحة كائنات الاستبدال تحديثاً متفرقاً ثم تعيد بناء الملف عبر ممر الوصولية نفسه الموصوف في دليل بتر الكائنات اليتيمة وإزالة تكرار الموارد، فلا تنجو برامج الخطوط القديمة كائنات لا سبيل إليها وتكون النتيجة مراجعة نظيفة واحدة
تجزئة النص الجديد وقت الحفظ بواسطة saSubsetNewFonts
الخطوط التي يغرسها PDFium Component خلال الجلسة الحالية حالة منفصلة، يعالجها خيار الحفظ saSubsetNewFonts لا SubsetPdfDocumentFonts. يقابل saSubsetNewFonts علم PDFium FPDF_SUBSET_NEW_FONTS، الذي يجري مجزئ HarfBuzz الأصلي فوق الخطوط التي حمّلتها استدعاءات أصلية مثل TPdf.AddText من النظام، شاملة واجهات OpenType CFF. ما تزال PDFium الأم تسمي ذلك العلم تجريبياً. ولا يمس الخطوط التي كانت في الملف أصلاً
var
Pdf: TPdf;
begin
Pdf := TPdf.Create(nil);
try
Pdf.CreateDocument;
Pdf.AddPage(0, 595, 842);
Pdf.AddText('Quarterly statement', 'Arial', 14, 72, 770, clBlack, $FF, 0.0);
if not Pdf.SaveAs('statement.pdf', saSubsetNewFonts) then
raise Exception.Create('Save failed');
finally
Pdf.Free;
end;
end;
في مجس المكوّن هبط مستند بنص Arial من 590 KB إلى 22 KB تحت saSubsetNewFonts، وما يزال استخلاص النص من الملف المحفوظ يطابق تماماً. خط ألوان COLR v0، وهو Segoe UI Emoji بحجم 7.0 MB، سلك المسار نفسه إلى 10.5 KB، معروضاً بمخططاته الأساسية لأن الـ PDF لا يملك تصوراً لطبقات COLR. والخطوط المتغيرة هي الاستثناء، والسد يقف أمام المجزئ: أعادت GDI GetFontData تدفق بديل من 18.3 MB لملف Segoe UI Variable ذي 1.85 MB، وفشل إنشاء كائن النص قبل أن تبدأ أي تجزئة. وتغطي جولة غرس خطوط CID Type 2 من Delphi مسار الغرس الذي تسلكه تلك الخطوط أولاً
أين يتوقف الإثبات
الإثبات داخل SubsetPdfDocumentFonts قوي بقدر بيانات الخط الوصفية التي كتبها المنتج بالضبط. لدى CIDFont من نوع Identity دليلها /W وحدها نقطة عمياء واحدة: محرف يساوي عرضه /DW، وليس له مدخل /ToUnicode، ولا تسرده الوصف في /CIDSet. محرف كهذا غير قابل للإثبات من الموارد، ولا يستطيع المكوّن معرفة وجوده دون مسح تدفقات المحتوى الذي يجتنبه عمداً. المنتجون الذين يتركون /ToUnicode و /CIDSet معاً وهم يضغطون /W نادرون، لكنهم موجودون، فلأرشيف من مولد مجهول اعرض عينة قبل وبعد وقارن البكسلات والنص المستخلص
قبل تشغيل دفعة ينفع أيضاً أن تعرف بما تتعامل. تخبرك مقاربة قراءة خصائص خطوط PDF بواسطة PDFium Component أي الخطوط مغراسة أصلاً، ويخبرك Report.Issues من تشغيل تجريبي أي منها سيُحفظ ولماذا. وتحدد الخيارات الافتراضية العمل أيضاً: 1024 خطاً، ونحو مليون محرف إجمالاً، و128 MB من المصدر وميزانية عمل ثابتة، مع CancellationToken اختيارية للوظائف الدفعية الطويلة
FAQ
هل يغير SubsetPdfDocumentFonts النص أو تخطيط الـ PDF؟
لا. تُحفظ معرفات المحارف الأصلية و CIDs والعروض وتدفقات المحتوى، فيستخدم استخلاص النص والعرض المحارف نفسها كما قبل؛ ولا يزال إلا المخططات التي لا سبيل إليها وتنال أسماء الخطوط بادئة تجزئة جديدة
لماذا لم تُجزَّأ خط مغراس؟
اقرأ Report.Issues. يحمل كل خط محفوظ سبباً مثل /W أو /CIDSet غائبة على CIDFont من نوع Identity، أو برنامج غير TrueType، أو مصفوفة /Widths مشوهة، أو توفير دون العتبة
هل saSubsetNewFonts هي نفسها SubsetPdfDocumentFonts؟
لا. تجزئ saSubsetNewFonts الخطوط المغروسة في الجلسة الحالية عند الحفظ، بينما يعمل SubsetPdfDocumentFonts على تدفق PDF قائم ويستبدل الخطوط التي غرسها منتج آخر أصلاً
تجزئة خطوط المستندات وتجزئة الخطوط وقت الحفظ وممر بتر اليتامى خلفهما كلها تُشحن مع PDFium Component لـ Delphi و C++Builder، الذي يغلّف PDFium لمشاريع Delphi و C++Builder و Lazarus