@צול-גאה מצ"ב מקלוד:
Spoiler
PDFConvert — מסמך לוגיקה
כלי לכיווץ ספרי PDF. מורכב משני רכיבים:
מארח (wvhost.ps1' → PDFConvert.exe'): ממשק WebView2/HTML + תזמורת.
מנוע (engine.py' → pdfengine.exe'): מבצע את הכיווץ בפועל, תהליך נפרד לכל קובץ.
המארח לא מכווץ בעצמו — הוא רק מנהל תהליכי-מנוע ומדווח התקדמות ל-HTML.
1. ארכיטקטורה ותקשורת
ui.html ──postMessage──► wvhost.ps1 ──Start-Process──► pdfengine.exe (×N)
▲ │ (קובץ-פלט stdout) │
└────PostWebMessage────────┘◄──────────קריאת קובץ בטיימר──────┘
HTML→מארח: הודעות JSON (action, dpi, wk, sizemode, colorpol...) ב-Handle-Message.
מארח→HTML: Push שולח JSON (type: log/progress/est/conflict/colorask/running...).
מנוע→מארח: כל תהליך-מנוע כותב ל-stdout שמנותב לקובץ זמני. פרוטוקול TAB:
P<tab>frac<tab>stage — התקדמות (0..1)
R<tab>in<tab>out<tab>pages — הצלחה (גדלים בבייטים)
S<tab>reason — דילוג, E<tab>msg — שגיאה
מצבי-עזר: C (count), B/BD (probe), K/KD (classify)
מקביליות אמיתית מושגת ע"י תהליכים נפרדים (אין GIL), לא threads. הטיימר (200ms) של המארח הוא הלולאה הראשית: מפעיל קבצים חדשים, קורא פלט, מדווח.
2. זרימת המארח
בחירת קלט והערכה (חי, לפני המרה)
גרירה/בחירה מוסיפה לרשימת Inputs (קבצים/תיקיות).
ספירה off-thread: pdfengine --count (os.scandir מהיר) — תיקיית-ענק (עשרות-אלפי קבצים) לא מקפיאה את ה-UI. נשלף בטיימר נפרד.
הערכת גודל (--probe לכל קובץ מסומן טקסט/סריקה ומחושב גודל-צפוי.
קובץ-טקסט: est = גודל אחרי דחיסה-ללא-אובדן (מדויק).
קובץ-סריקה: est לפי נוסחה יחס ≈ 0.40·(dpi/150)².
תקרות-ביצועים: מעל MaxProbe=400 אין probe (נוסחה בלבד); מעל BuildCap=5000 אין אובייקט פר-קובץ (אומדן מהספירה בלבד).
DPI אפקטיבי ('Get-EffDpi'): במצב dpi = הערך שנבחר; במצב pct/size נגזר DPI שמשוער להגיע ליעד. ההערכה וההרצה משתמשות באותו DPI כדי שיתאמו.
מצבי גודל (SizeMode)
dpi — איכות לפי DPI נבחר.
pct — אחוז-יעד מהמקור. size — גודל-יעד ב-MB.
orig — "שמור DPI מקורי": ללא הקטנת רזולוציה, המרת-צבע בלבד (--keep-dpi).
מדיניות צבע
AllMode: auto (לפי סוג קובץ) / bw (הכל ש"ל) / gray (הכל אפור).
ColorPolicy (קובץ-צבע): keep/gray/bw/ask. GrayPolicy (קובץ-אפור): keep/bw/ask.
אם המדיניות לא מובילה הכל ל-bw → מריצים סיווג (--classify) ברקע לפני ההמרה; התוצאה (color/gray/bw) נשמרת במטמון Cat. ask → מודאל ב-HTML לכל קובץ.
כל קובץ מתורגם לפעולה: --op bw|gray|keep, ועם --force-bw להמרה כפויה לשחור-לבן.
התחלת המרה (Start-Convert)
בונה רשימת Pdfs (כל אחד: מקור, יעד, מצב, קובץ-פלט-stdout).
מחלק ליבות: concurrent = min(Workers, #קבצים), Jobs = ceil(usable/concurrent) — כך ספר בודד גדול מנצל את כל הליבות, ובלי oversubscription בריבוי קבצים. MaxPerf משאיר/מנצל ליבה.
הטיימר מפעיל עד Workers תהליכים במקביל, קורא P/R/S/E, מצרף התקדמות לפי בייטים, מחשב ETA, מעדכן הערכה לפי יחס בפועל.
עצירה/השהיה/התנגשות
ביטול: Proc.Kill() לכל הרצים.
השהיה/חידוש: NtSuspendProcess/NtResumeProcess מקפיא את כל החוטים של התהליך → המשך מדויק מאותה נקודה.
התנגשות-שם ביעד (כולל יעד=מקור): מודאל rename/overwrite (אפשר "החל על הכל"). דריסה-במקום כותבת לקובץ זמני ואז Move-Item על המקור.
גרור-ושחרר מעל WebView2 דורש IDropTarget ידני על חלונות-בן של Chromium (ראה הערות ב-wvhost.ps1).
3. לוגיקת המנוע (engine.py)
עיקרון-על
לכל קובץ: זיהוי סוג → ניתוב למסלול מתאים → לעולם לא דורסים את המקור ולעולם לא מגדילים (אם הפלט ≥ המקור — משחזרים את המקור, _never_grow).
זיהוי סוג קובץ (is_text_pdf)
דוגם עד 24 עמודים. אם ברוב העמודים יש תמונה גדולה (≥0.5MP) → סריקה. אחרת → טקסט (נולד-דיגיטלי). קריטי: למסלול הסריקה (רסטור-עמוד) אסור לגעת בקובץ-טקסט — זה צורב טקסט חד לרסטר מפוקסל ואף מגדיל.
סיווג צבע (classify_pdf)
שלב-מטא מהיר (pikepdf): בודק ColorSpace/BitsPerComponent של תמונות כל עמוד → bw(1-bit) / gray(אפור 8-bit) / מועמד-צבע (RGB/CMYK/Indexed). רק מועמדי-הצבע מרונדרים ב-DPI נמוך (42) ומאומתים ברוויה. כלל: עמוד-צבע אחד ⇒ "צבע"; עמוד-אפור אחד ⇒ "אפור"; אחרת "שחור-לבן". כשל → bw (בטוח).
מסלולי המרה
סוג קובץ
op=bw
op=gray
op=keep
סריקה
compress_pdf (jbig2)
optimize_text_pdf(gray)
optimize_text_pdf
טקסט
bw_text_pdf
gray_text_pdf
optimize_text_pdf
מסלול סריקה לשחור-לבן — compress_pdf (הליבה)
שני שלבים, שניהם מקביליים על כל הליבות:
רינדור מקבילי ('_render_parallel'): מפצל את ה-PDF ל-njobs מקטעי-עמודים (כל מקטע מתחיל מעמוד 1 → אין קנס FirstPage של Ghostscript), מרנדר ב-GS לגווני-אפור (PGM) במקביל. מאיץ ~×4.
סף + jbig2 ('_compress_page', CHUNK_SIZE=1) : כל עמוד —
אם יש תוכן גווני-ביניים משמעותי (_is_grayscale_page > 4%) → נשמר כאפור-8bit ללא-אובדן.
אחרת → סף גלובלי ל-1-bit (PBM) → jbig2 -s (לוסי סימבולי). אצווה=1 כי jbig2 -s הוא ~O(אצווה²) — קטסטרופלי על סריקות רועשות (פי ~28 מאצווה=6, בעוד הפלט גדל ב-~1.3% בלבד).
כשל jbig2 בעמוד → אותו עמוד נשמר אפור (לא מפיל את הקובץ).
הזרקה: מחליפים את ה-XObject של כל עמוד במקום (/JBIG2Decode) — שומר את שכבת הטקסט, הגופנים, ה-MediaBox והמיקום. הטקסט נשאר ניתן-לחיפוש ומיושר.
--force-bw על עמוד שאינו תמונה-בודדת → _rebuild_page_single_image (כל הדף הופך לתמונת-בילבל אחת).
מסלול טקסט לשחור-לבן — bw_text_pdf
לא מרסטרים את העמוד! מאתרים כל תמונה מוטמעת (כולל מקוננות ב-Form-XObject), מרנדרים כל אחת לבדה 1:1, סף+jbig2, ומחליפים XObject במקום — הטקסט הווקטורי נשאר חד. רכיבים וקטוריים צבעוניים → אפור במעבר שני.
המרה לאפור — optimize_text_pdf(gray) / gray_text_pdf
סריקה: GS pdfwrite עם ColorConversionStrategy=Gray, תקרת JPEG (GRAY_JPEGQ=70) למניעת ניפוח.
טקסט: שלב מהיר ממיר רק תמונות-צבע לאפור (במקום), ושלב-שני (_vector_color_pass) ממיר רכיבים וקטוריים צבעוניים שנותרו — רק על העמודים הצבעוניים (_color_pages), כדי לא לנפח גופנים בכל המסמך.
"השאר כך" / כיווץ-טקסט — optimize_text_pdf
GS 'pdfwrite' (/ebook דחיסת זרמים, subset גופנים, הקטנת תמונות ל-DPI הנבחר — תוך שמירת צבע/אפור. בנוסף מחושב גם נתיב דחיסה-ללא-אובדן (pikepdf) ונבחר הקטן מביניהם.
"שמור DPI מקורי" (--keep-dpi)
bw: רינדור ב-native_dpi המקורי (רצפה BW_MIN_RENDER_DPI=300 כדי שהסף לא ייצר טקסט משונן), ללא הקטנה.
gray/keep: GS עם Downsample*Images=false — ההמרה היא רק שינוי-צבע/כיווץ-זרמים.
טיפול בנתיבים (חשוב ל-Windows/עברית)
GS נשבר על נתיבי-קלט עם רווח/עברית → המנוע מעתיק תמיד לנתיב ASCII זמני לפני GS.
המארח עוטף בגרשיים כל ארגומנט עם רווח ל-Start-Process (PS 5.1).
רשימות-נתיבים ארוכות מועברות דרך @file (לא שורת-פקודה) לעקיפת מגבלת ~32KB.
4. CLI של המנוע
pdfengine <in> <out> [--dpi N] [--jobs N] [--op bw|gray|keep] [--force-bw] [--keep-dpi]
pdfengine --count @list.txt → C<tab>count<tab>bytes
pdfengine --probe @list.txt → B<tab>is_text<tab>src<tab>est<tab>path ... BD
pdfengine --classify @list.txt → K<tab>color|gray|bw<tab>path ... KD
5. קבצי המקור
קובץ
תפקיד
wvhost.ps1
המארח (מקור ל-PDFConvert.exe)
ui.html
הממשק (RTL, WebView2)
engine.py
המנוע (מקור ל-pdfengine.exe)
build_sfx.ps1
אריזת SFX
_app/
בונדל מופץ: exe-ים, Ghostscript, jbig2
PDFConvert.ps1 / gui.py
גרסאות WinForms ישנות (לא המופץ)