12. وصفات عملية
حالات كاملة تُنقل كما هي. كل واحدة تحلّ مشكلة حقيقية من الميدان.
12.1 مطبعة: تدقيق ما قبل الطبع لمجلة أوفست
المشكلة: يسلّم العميل ملفه، وقبل حرق الألواح يجب التحقق من الأحبار والخطوط والصور والفيض. والخطأ الذي يُكتشف لاحقًا يضيّع الطبعة كلها.
profiles/offset.pdfl:
profile "offset-magazine" {
const TAC_LIMIT = 300% // حدّ الحبر على الورق المطلي
const BLEED = 3mm // مقتضى التركيب
const MIN_DPI = 300
check "Ink coverage" tags: ["prepress"] {
// الـ TAC الدقيق يقرأ الألوان المعلَنة في الملف؛ أما التقدير
// بالتصيير فيقلّل من الأسود الغني ويفوّت التجاوزات
doc.pages.each { |page|
tac = prepress::calculate_exact_tac(page.number)
assert tac <= TAC_LIMIT,
"page #{page.number}: #{tac}% ink (limit #{TAC_LIMIT}%)"
}
}
check "Colors" tags: ["prepress"] {
assert prepress::detect_color_mode() != "RGB",
"document is in RGB — convert to CMYK"
spots = prepress::detect_spot_colors()
assert spots.length == 0, "unquoted special ink: #{spots.join(", ")}"
assert !prepress::detect_rich_black(),
"rich black detected — use 0/0/0/100 for text"
}
check "Fonts" tags: ["fonts"] {
loose = prepress::detect_text_substitution()
assert loose.length == 0,
"fonts not embedded (text will change at the RIP): #{loose.join(", ")}"
assert prepress::validate_font_size(6),
"there is text below 6 pt — illegible once printed"
}
check "Strokes" tags: ["prepress"] {
assert !prepress::detect_hairlines(0.25),
"strokes below 0.25 pt disappear in print"
assert !prepress::detect_hairlines_exact(),
"there is a stroke with 0 width — set a real thickness"
}
check "Images" tags: ["images"] {
doc.images.each { |img|
assert img.dpi >= MIN_DPI,
"image on page #{img.page_number}: #{round(img.dpi)} DPI (minimum #{MIN_DPI})"
assert img.color_space != "DeviceRGB",
"RGB image on page #{img.page_number}"
}
}
check "Geometry" tags: ["prepress"] {
assert prepress::validate_trim_box(),
"no TrimBox — imposition cannot know where to trim"
assert prepress::validate_bleed_box(), "no BleedBox — no bleed is defined"
assert prepress::check_page_geometry(BLEED),
"bleed smaller than 3 mm on some page"
}
}
عند الاستقبال:
# تقرير HTML يُعاد إلى العميل
pdfl run profiles/offset.pdfl client.pdf --output html --output-file report.html
كمجلد مراقَب: يضع المشغّل الملف فيظهر التقرير بجواره.
pdfl watch inbox/ --script profiles/offset.pdfl \
--output-dir reports/ --report html
12.2 دار نشر قانونية: تدقيق عقد قبل النشر
المشكلة: يجب أن تحمل العقود والوثائق البنود الإلزامية، وألا يبقى فيها نص مسودّة، وألا تكشف بيانات شخصية، وأن يظل نصها قابلًا للبحث.
profiles/legal.pdfl:
profile "standard-contract" {
check "Mandatory clauses" tags: ["legal"] {
// مسرد تديره الإدارة القانونية
missing = data::validate_against_reference("terms/clauses.txt")
assert missing.length == 0, "missing clauses: #{missing.join("; ")}"
}
check "No drafts" tags: ["legal"] {
assert text::forbid_text("DRAFT"), "document marked as draft"
assert text::forbid_text("lorem ipsum"), "placeholder text present"
assert text::forbid_match("X{3,}"), "unfilled fields (XXX)"
}
check "Privacy" tags: ["compliance"] {
// أرقام الضريبة لا تُحتسب إلا بخانة تحقق صحيحة،
// فأرقام الأمثلة لا تثير إنذارًا كاذبًا
found = text::detect_personal_data()
assert found.length == 0, "personal data in the document: #{found.join("; ")}"
}
check "Numbering and initials" tags: ["legal"] {
doc.pages.each { |page|
footer = region(0, 0, page.width, 60, "footer")
content = text::extract_from_region(page.number, footer).trim()
assert content != "",
"page #{page.number} has no numbering/initials in the footer"
}
}
check "Searchable text" tags: ["accessibility"] {
assert !text::detect_rasterized_text(),
"there are scanned pages — text cannot be searched or read by screen readers"
}
}
12.3 مختبر أدوية: نشرة برمز الدفعة
المشكلة: يجب أن تحمل النشرة النصوص التي تطلبها الجهة التنظيمية، وأن يشير الباركود إلى المنتج الصحيح. وتبديل الرموز بين المنتجات أغلى أخطاء هذا القطاع.
profiles/insert.pdfl:
profile "regulated-insert" {
check "Mandatory texts" tags: ["regulatory"] {
missing = data::validate_against_reference("databases/regulatory_texts.txt")
assert missing.length == 0, "mandatory texts missing: #{missing.join("; ")}"
}
check "Legibility" tags: ["regulatory"] {
assert prepress::validate_font_size(6), "there is text below 6 pt"
}
check "Barcode" tags: ["codes", "critical"] {
assert codes::detect_barcodes(), "insert has no barcode"
code = codes::decode_barcode(1)
assert codes::validate_barcode_checksum(1), "invalid check digit: #{code}"
// هذا الفحص يلتقط أغلى خطأ:
// رمز منتج مع نص منتج آخر
assert codes::compare_barcode_with_text(),
"the code number does not appear in the insert text"
}
check "Approved product" tags: ["data", "critical"] {
code = codes::decode_barcode(1)
product = data::query_gtin(code)
assert product, "GTIN #{code} is not in the product database"
name = product.get(2)
assert text::require_text(name),
"the name '#{name}' does not appear on the insert"
print("product verified:", name)
}
check "Code position" tags: ["layout"] {
area = region(400, 20, 180, 90, "barcode area")
assert codes::validate_barcode_position(area),
"code outside the reserved area — risk of being trimmed off"
}
}
PDFL_DATA_DIR=./databases pdfl run profiles/insert.pdfl insert_v3.pdf
12.4 الموافقة: المقارنة بالنسخة المعتمدة
المشكلة: اعتمد العميل النسخة الأولى. ثم تصل الثانية بقول «غيّرنا كلمة واحدة». وتصديق ذلك مكلف.
# HTML يُظهر ما تغيّر فعلًا
pdfl compare approved/catalogue_v1.pdf received/catalogue_v2.pdf \
--normalize --ignore-dates \
--output html --output-file differences.html
echo "exit: $?" # 0 متطابقان · 1 بيانات وصفية فقط · 2 تغيّر المحتوى
وللتحقق من الشكل أيضًا لا من النص وحده:
// profiles/fidelity.pdfl
profile "visual-fidelity" {
const APPROVED = "approved/catalogue_v1.pdf"
check "Pages visually identical" tags: ["approval"] {
doc.pages.each { |page|
ssim = visual::measure_ssim(page.number, APPROVED)
assert ssim > 0.99,
"page #{page.number} changed visually (SSIM #{ssim}, #{visual::pixel_diff(page.number, APPROVED)}% of pixels)"
}
}
check "No image replaced" tags: ["approval"] {
doc.pages.each { |page|
assert !visual::detect_image_replacement(page.number, APPROVED),
"page #{page.number}: image swapped compared to the approved version"
}
}
}
12.5 CI/CD: التدقيق بالجملة
المشكلة: كل ملف يدخل المستودع يجب أن يجتاز تدقيق ما قبل الطبع، دون أن يشغّله أحد يدويًا.
.github/workflows/preflight.yml:
name: PDF preflight
on: [push, pull_request]
jobs:
validate:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Install pdfl
env:
GH_TOKEN: ${{ secrets.PDFLANG_TOKEN }} # صلاحية قراءة على مستودع pdflang الخاص
run: |
gh release download --repo k074r0k1kuch1/pdflang \
--pattern 'pdfl-linux-x64.tar.gz'
tar xzf pdfl-linux-x64.tar.gz
echo "$PWD/pdfl" >> $GITHUB_PATH
- name: Check the scripts themselves
run: |
for f in profiles/*.pdfl; do
pdfl lint "$f"
pdfl fmt "$f" --check
done
- name: Preflight every PDF
env:
PDFL_LICENSE: ${{ secrets.PDFL_LICENSE }} # ترخيص seatless: جهاز التكامل المستمر ليس ثابتًا
run: |
pdfl watch files/ --script profiles/offset.pdfl \
--output-dir reports/ --once
- name: Publish the reports
if: always()
uses: actions/upload-artifact@v4
with:
name: reports
path: reports/
لا تحتاج خطوتا
lintوfmtإلى ترخيص — فهما لا تقرآن إلا النصوص البرمجية. أما خطوة الفحص المسبق فتحتاجه: إذ يعالجpdfl watchملفات PDF. ويحملPDFL_LICENSEترخيصseatless، أي الذي لا ينظر إلى الجهاز، لأن مشغّل التكامل المستمر يُدمَّر في كل تشغيل وتتغير بصمته في كل مرة.
pdflangمستودع خاص، فتنزيل الملف التنفيذي يقتضي المصادقة. وPDFLANG_TOKENرمز وصول شخصي له صلاحية قراءة عليه، محفوظ سرًّا في المستودع الذي يشغّل سير العمل هذا. أماGITHUB_TOKENالافتراضي فلا يكفي: إذ لا يمنح إلا الوصول إلى المستودع الذي يعمل فيه.
12.6 تهيئة ملف ناشر للمطبعة
// profiles/prepare.pdfl
check "Preconditions" {
require doc.page_count > 0
assert !struct::check_encryption(),
"file is encrypted — ask for the open version"
}
// أُطر الإنتاج التي لم يضبطها الناشر
fix::set_trim_box(8.5, 8.5, 586.5, 833.5)
fix::set_bleed_box(0, 0, 595, 842)
// التنظيف
fix::remove_annotations() // تعليقات المراجعة
fix::remove_attachments() // مرفقات لا تزيد الملف إلا ثقلًا
fix::flatten_layers() // يمنع تشغيل طبقة عن طريق الخطأ
fix::remove_unused_resources()
pdfl fix publisher.pdf profiles/prepare.pdfl --output print.pdf --dry-run # للتحقق
pdfl fix publisher.pdf profiles/prepare.pdfl --output print.pdf # للتطبيق
pdfl run profiles/offset.pdfl print.pdf # للتدقيق
12.7 توزيع ملف تعريفي على الفريق
المشكلة: خمسة أجهزة يجب أن تستعمل الملف التعريفي نفسه والبيانات نفسها تمامًا، دون أن يعبث بها أحد.
# على الجهاز الذي يصون الملف التعريفي
pdfl pack profiles/ --name print-profile --version 1.2.0
# على أجهزة الإنتاج
pdfl add print-profile.pdflpkg
# يثبّت في ./pdfl_profiles/print-profile@1.2.0/ مع التحقق من كل بصمة
pdfl run pdfl_profiles/print-profile@1.2.0/offset.pdfl file.pdf
وإن عُدِّلت الحزمة في الطريق رفض add التثبيت.
12.8 استقصاء ملف فيه مشكلة
خطوات عملية حين لا تعرف من أين أتت المشكلة:
# 1. صورة عامة في ثوانٍ
pdfl inspect suspect.pdf
# 2. نص برمجي للاستقصاء، بـ print() فقط
cat > investigate.pdfl <<'EOF'
check "X-ray" {
print("exact TAC:", prepress::calculate_exact_tac(), "%")
print("estimated TAC:", prepress::calculate_tac(), "%")
print("spots:", prepress::detect_spot_colors().join(", "))
print("rich black?", prepress::detect_rich_black())
print("overprint ok?", prepress::validate_overprint_settings())
print("loose fonts:", prepress::detect_text_substitution().join(", "))
doc.images.each { |img|
print("image page", img.page_number, ":", img.width, "x", img.height,
"@", round(img.dpi), "DPI", img.color_space)
}
}
EOF
pdfl run investigate.pdfl suspect.pdf > /dev/null
# print() يكتب على مخرج الأخطاء، فيمكن رمي التقرير
# والاكتفاء بنتائج الاستقصاء