diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 6522acb4c..ebf1b8161 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -154,11 +154,11 @@ jobs: path: build/.cache/zig-x86_64-windows-0.16.0 key: zig-0.16.0-x86_64-windows - # rhwp, Pandoc, 7-Zip and ExifTool are small; with them the HWP/HWPX, archive and metadata engine tests run too - # (LibreOffice is skipped to save time). - - name: Fetch rhwp, Pandoc, 7-Zip and ExifTool for engine tests + # rhwp, Pandoc, 7-Zip, ExifTool and WeasyPrint are small; with them the HWP/HWPX, archive, metadata and WeasyPrint + # engine tests run too, WeasyPrint's against the pinned Windows build (LibreOffice is skipped to save time). + - name: Fetch rhwp, Pandoc, 7-Zip, ExifTool and WeasyPrint for engine tests shell: pwsh - run: ./build/fetch-engines.ps1 -Only rhwp,pandoc,7zip,exiftool + run: ./build/fetch-engines.ps1 -Only rhwp,pandoc,7zip,exiftool,weasyprint # The UI tests on their own first, as the release runs them (there they crashed with 0xC0000005): a crash # names the test that was running and leaves a dump in TestResults. diff --git a/THIRD-PARTY-NOTICES.md b/THIRD-PARTY-NOTICES.md index 61d84a614..1cb11b04d 100644 --- a/THIRD-PARTY-NOTICES.md +++ b/THIRD-PARTY-NOTICES.md @@ -87,6 +87,7 @@ Downloaded from their official release pages when the user chooses to install th | Microsoft Visual C++ Redistributable (for Ghostscript and Upscayl, conda-forge `vc14_runtime` and `vcomp14`) | Microsoft Visual C++ Redistributable license | https://github.com/conda-forge/vc-feedstock | | FFmpeg 9.0.2 (gyan.dev "full_build-shared" Windows build; ffmpeg.exe, ffprobe.exe and their libraries, incl. x264, x265, libvpx, LAME, Opus, Vorbis, Theora, OpenCORE AMR) | GPL-3.0-or-later (the build is configured with `--enable-gpl --enable-version3`); FFmpeg itself LGPL-2.1-or-later | https://ffmpeg.org, build: https://www.gyan.dev/ffmpeg/builds/ (source: https://github.com/GyanD/codexffmpeg) | | calibre (ebook-convert) | GPL-3.0 | https://calibre-ebook.com | +| WeasyPrint 70.0 (Windows "onedir" build, `weasyprint-windows-onedir.zip`: `weasyprint.exe` with its `_internal` folder, built with PyInstaller on MSYS2 UCRT64; it embeds Python 3.14, Pango, HarfBuzz, FreeType, Fontconfig, FriBidi, GLib, libthai, Pillow, fontTools, Pyphen, pydyf, tinycss2, cssselect2, Brotli, Zopfli, OpenSSL, zlib, libpng, Expat and the MinGW runtime) | BSD-3-Clause (WeasyPrint); PSF-2.0 (Python); LGPL-2.1-or-later (Pango, GLib, FriBidi, libthai); MIT (HarfBuzz, Fontconfig, fontTools, Brotli, Expat); FreeType License; HPND (Pillow); GPL/LGPL/MPL tri-licence (Pyphen); BSD-3-Clause (pydyf, tinycss2, cssselect2); Apache-2.0 (Zopfli, OpenSSL); GCC Runtime Library Exception (MinGW runtime); zlib and libpng licences | https://github.com/Kozea/WeasyPrint | | Tesseract OCR 5.4 (UB Mannheim Windows build, with Leptonica and the MinGW libraries it ships; their notices are in its `doc` folder) | Apache-2.0 (Tesseract); Leptonica BSD-2-Clause; others as listed in the build | https://github.com/tesseract-ocr/tesseract, build: https://github.com/UB-Mannheim/tesseract | | Tesseract language data (tessdata_fast 4.1.0), downloaded per language when used | Apache-2.0 | https://github.com/tesseract-ocr/tessdata_fast | | whisper.cpp (whisper-cli.exe Windows build) | MIT | https://github.com/ggml-org/whisper.cpp | diff --git a/build/fetch-engines.ps1 b/build/fetch-engines.ps1 index cbe5d5bf9..f5afa0572 100644 --- a/build/fetch-engines.ps1 +++ b/build/fetch-engines.ps1 @@ -22,6 +22,8 @@ installer with 7-Zip (never run); the language data is downloaded by the app when needed exiftool/ ExifTool (Perl's licence, separate program), the Windows build: exiftool(-k).exe renamed exiftool.exe (the "-k" makes it wait for a key), with its exiftool_files/ beside it + weasyprint/ WeasyPrint (BSD-3-Clause, separate program), its Windows "onedir" build: weasyprint.exe with + its _internal/ folder (Python, Pango, HarfBuzz, ...) and LICENSE, HTML -> PDF Every download is pinned to a version and verified with SHA-256 (src/Filee.Engines/Infrastructure/engines.json, shared with the app). Downloads are cached in build/.cache. @@ -381,6 +383,23 @@ if ('exiftool' -in $selected) { if (-not (Test-Path (Join-Path $target 'exiftool.exe'))) { throw 'exiftool.exe not found in the archive.' } } +if ('weasyprint' -in $selected) { + # As EngineInstaller.WeasyPrintFolder does: the folder with weasyprint.exe (onedir/weasyprint), with the licence + # from the top of the zip moved into it. + $tmp = Join-Path $cache ('w-' + [Guid]::NewGuid().ToString('N').Substring(0, 8)) + Expand-Archive -Path (Get-Engine 'weasyprint') -DestinationPath $tmp + $exe = Get-ChildItem $tmp -Recurse -Filter 'weasyprint.exe' | Select-Object -First 1 + if (-not $exe) { throw 'weasyprint.exe not found in the archive.' } + $target = Join-Path $Destination 'weasyprint' + Reset-Folder $target + Get-ChildItem $exe.DirectoryName -Force | Move-Item -Destination $target + foreach ($name in 'LICENSE', 'README.rst') { + $file = Join-Path $tmp $name + if (Test-Path $file) { Move-Item -LiteralPath $file -Destination $target -Force } + } + Remove-Item $tmp -Recurse -Force +} + if ('calibre' -in $selected) { $msi = Get-Engine 'calibre' $tmp = Join-Path $cache 'calibre-admin' diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md index f80436ab5..e1544d61b 100644 --- a/docs/ARCHITECTURE.md +++ b/docs/ARCHITECTURE.md @@ -25,6 +25,7 @@ │ · E-mail · Signed files · Data · E-books · CAD · Fonts · Subtitles · 3D models │ │ Bundled: rhwp (HWP→PDF, HWP↔HWPX) · 7-Zip (archives) · Unhwp · Markdig │ │ Optional: FFmpeg · LibreOffice + H2Orestart · calibre · Ghostscript · Pandoc │ + │ · WeasyPrint │ └────────────────────────────────────────────────────────────────────────────────────┘ ``` diff --git a/docs/ENGINES.md b/docs/ENGINES.md index b945a9b7e..723db4248 100644 --- a/docs/ENGINES.md +++ b/docs/ENGINES.md @@ -40,6 +40,7 @@ Filee chooses engines automatically. Settings → *Engines* shows their status, | Apple Vision | part of macOS (Mac only) | **text recognition (OCR) on a Mac**, the same step as Tesseract without a download; Tesseract reads the languages Vision doesn't (see "Apple Vision on macOS") | | FFmpeg | **optional download** (~100 MB, 272 MB on disk) | **all video and audio**: video ↔ video (H.264, HEVC, AV1), video → animated GIF / APNG, a still frame or every frame, audio and subtitle extraction, audio ↔ audio (also APE, WavPack, TTA, DSD, DTS, ALAC, …), GIF, APNG and photos → MP4/WEBM/MOV, Live / Motion Photos → their video, a clip of a video → an iPhone Live Photo or Android Motion Photo (experimental), images → HEIC, clips or audio files joined into one, pictures → a slideshow MP4, a video split into parts every so often or into equal parts (copied, not encoded), copies without metadata (see "Media tools") | | Calibre | **optional download** (~216 MB, 660 MB on disk) | rare e-book formats: LIT, LRF, CHM, PDB, PML, RB, SNB, TCR, OEB → EPUB; EPUB → MOBI, AZW3, LIT, LRF, PDB, PML, RB, SNB, TCR; e-books → PDF (ranked above the HWPX writer, see below) | +| WeasyPrint | **optional download** (Windows only for now; ~32 MB, 65 MB on disk) | **HTML, Markdown, EML, MSG, MHTML → PDF** laid out as a browser prints them (headings, tables, bold, Arabic and Indic shaping), instead of the built-in route; nothing changes without it (see below) | | Whisper | **optional download** (Windows only; ~157 MB with model, ~160 MB on disk) | **Speech-to-text transcription**: video and audio → SRT subtitles or plain TXT in ~100 languages using OpenAI Whisper (whisper.cpp); runs locally and offline; needs FFmpeg | | ExifTool | **optional download** (~11 MB on Windows, ~8 MB elsewhere; 30 MB on disk) | **the Metadata tool**: reads every metadata field of pictures, PDFs, video and sound, and writes changes into a copy in the file's own format without re-encoding it (JPG, PNG, TIFF, WEBP, HEIC, AVIF, JPEG XL, GIF, PSD, camera RAW, PDF, MP4, MOV, M4A, 3GP, …; not MP3, FLAC, WAV, MKV, AVI or BMP, which it shows read-only). Without it the Metadata tool shows nothing and offers it | @@ -56,7 +57,8 @@ XLSX shared strings and sheets, HWPX sections, OFD pages and templates, EPUB cha RTF character references) for the Hebrew, Arabic, Syriac, Thaana, N'Ko, Indic, Sinhala, Thai, Lao, Tibetan, Myanmar, Khmer, Mongolian and other South / Southeast Asian blocks. For such a file `JobQueue` plans again without unshaped edges: DOCX, TXT, Markdown, HTML, XLSX, PPTX and HWPX → PDF (and → images through PDF) go through LibreOffice even -when Settings → *Engines* doesn't choose it for documents. Without LibreOffice the file fails with "This script needs +when Settings → *Engines* doesn't choose it for documents; HTML, Markdown and e-mails go through WeasyPrint instead +when it is installed (it shapes text, see *WeasyPrint*). Without LibreOffice the file fails with "This script needs LibreOffice to convert correctly" and the script's name instead of becoming a broken PDF. The same scan also finds text rhwp has no widths for: its width tables cover only ASCII and Latin-1, general punctuation, math, the CJK forms and Hangul (rhwp#8143), so a letter of any other script — Polish, Turkish, Vietnamese, Greek, Cyrillic, ... — or a @@ -139,6 +141,7 @@ the runtime next to every program that needs it and does not ship it (`Infrastru | Ghostscript (download) | `vcruntime` component copied next to `gswin64c.exe` (`EngineInstaller`, `fetch-engines.ps1`) | | LibreOffice (download) | its own copy, which the administrative install leaves in `System64`, copied into `program/` (`EngineInstaller`, `fetch-engines.ps1`; `LibreOfficeConverter` also repairs a copy installed by an earlier Filee) | | calibre, Java (downloads) | ship their own next to their programs (`app/bin`, `bin`) | +| WeasyPrint (download) | ships its own (the MinGW / UCRT runtime and `VCRUNTIME140.dll` of its Python) in `_internal` | | 7-Zip, FFmpeg (GyanD), Tesseract (UB Mannheim, MinGW), Pandoc, PDFium, SkiaSharp / HarfBuzz, Magick.Native, the Explorer menu DLL, .NET | import neither (static runtime or MinGW / GHC builds) | The installer and the portable zip are made from the same publish folder, so both have the DLLs. Filee installed in @@ -1519,6 +1522,43 @@ never switches engines by itself. (`CALIBRE_CONFIG_DIRECTORY`, ...), so a calibre the user installed is never read or changed; messages are English (`CALIBRE_OVERRIDE_LANG`) and progress comes from its "34% ..." lines. +## WeasyPrint + +WeasyPrint (BSD-3-Clause, `Text/WeasyPrintConverter.cs`) is an optional download that makes PDFs of HTML the way a +browser prints it, with Pango and HarfBuzz: headings, tables, bold, links, and Arabic, Hebrew or Indic text shaped and +in the right order. The built-in route (HWPX writer → rhwp) keeps structure only and draws unshaped text (#1403). + +- **Download**: Windows only for now, WeasyPrint's own "onedir" build (`weasyprint-windows-onedir.zip`, pinned in + `engines.json`): `weasyprint.exe` with an `_internal` folder holding Python, Pango, HarfBuzz, Fontconfig, ... The + folder with `weasyprint.exe` becomes `engines/weasyprint`, with WeasyPrint's `LICENSE` moved into it. It is found + only as Filee's download or a copy the user picked in Settings → *Engines*, never on the PATH. Linux and macOS + follow in a separate change once WeasyPrint publishes a self-contained build for them; until then they keep the + routes below. +- **Routes**: one edge, HTML → PDF, cost 1, used only in routes that start from HTML, Markdown, EML, MSG or MHTML + (`ConversionEdge.Sources`). So a page goes straight to WeasyPrint, Markdown through Markdig's page (Markdown → HTML + → PDF, 9 + 10 at the end of the engine order) and an e-mail or a saved web page through its page (19 + 10), all + cheaper than the built-in route (31) and LibreOffice (37). A Word document, an e-book or a PDF never goes through a + page to reach it. Without WeasyPrint installed the edge isn't planned and every route stays as it was. → PNG, JPG, + ... of such a file go through WeasyPrint's PDF and PDFium. A source with text that needs shaping takes it too: the + edge isn't `Unshaped`. +- **Locked down**: WeasyPrint gets a copy of the page (`WeasyPrintConverter.PreparePage`, the HTML rule of + `LinkedContent`): pictures inside the document's own folder (or the Markdown file's or e-mail's, for a page made + from it; `DocumentFiles.cs`) become `data:` URIs, SVG included; every other picture, frame, object, embed, ``, + `` style sheet, `@import` and CSS `url()` address is taken out, except references to the page's own elements + (`#id`: an inline SVG's ``, a gradient's `xlink:href`, `fill: url(#g)`), which WeasyPrint resolves + within the page. It then runs with `--allowed-protocols data`, + so whatever the copy still names (a relative or absolute path, a `file:`, `http:` or `https:` address, an + attachment link, a CSS `image-set()` string the copy didn't recognise) is refused by WeasyPrint itself: no local + file outside the document is read and no address is contacted. Removing first and refusing second means a + reference the HTML rewrite misses still loads nothing, and one WeasyPrint would refuse anyway doesn't leave an error + per picture in its output. Tests: `WeasyPrintTests` (the page prepared without WeasyPrint, and the PDF with it). +- **Options**: `--presentational-hints` (an e-mail's `width`, `bgcolor`, ...) and `--verbose`, whose "Step 5 - + Creating layout - Page 12" lines drive the progress bar and the stall watch. Page size and margins come from the + page's CSS, else A4 (WeasyPrint's default). Pictures embedded in one page are limited to 256 MB in all, like for + LibreOffice. +- **Time**: WeasyPrint may run as long as it keeps printing lines, but is stopped after 5 minutes without one + (`error.engine_stalled`) and after 30 minutes in all. Cancelling stops it at once. Two run at the same time. + ## HWP ↔ HWPX rhwp converts between the two 한글 formats without Hancom Office (`export-hwpx` and `convert`). Anything → HWP goes @@ -1542,7 +1582,8 @@ per MB of unpacked `Contents/section*.xml` (an HWP counts three times its file s 60 minutes, after which a rhwp that hangs is stopped. The progress estimate assumes 15 minutes per MB. Cancelling stops rhwp at once. A time-out fails with "rhwp couldn't finish this long document in time" (with calibre installed, it suggests ranking calibre above the HWPX writer for e-books). The other engines keep fixed limits: LibreOffice 3 -minutes, Ghostscript 5, Pandoc 2 and calibre 10 (it made the 640-page novel's PDF in a minute). +minutes, Ghostscript 5, Pandoc 2 and calibre 10 (it made the 640-page novel's PDF in a minute); WeasyPrint 5 minutes +without output, 30 in all. ## Archives diff --git a/src/Filee.App/Assets/i18n/ar.json b/src/Filee.App/Assets/i18n/ar.json index 58f678a03..de16359a1 100644 --- a/src/Filee.App/Assets/i18n/ar.json +++ b/src/Filee.App/Assets/i18n/ar.json @@ -270,6 +270,7 @@ "engine.tesseract.description": "يتعرّف على نص الصور والمستندات الممسوحة (OCR): PDF قابل للبحث وDOCX وTXT.", "engine.vision.description": "يتعرّف على نص الصور والمستندات الممسوحة بأداة التعرّف على النص المدمجة في macOS (OCR): PDF قابل للبحث وDOCX وTXT. لا حاجة إلى أي تنزيل.", "engine.calibre.description": "صيغ الكتب الإلكترونية النادرة (LIT وLRF وCHM وPDB وRB…) وإخراج Kindle ‏(MOBI وAZW3).\nعند وضعه فوق محرك كتابة HWPX، يُنشئ أيضًا PDF من الكتب الإلكترونية (بسرعة أكبر بكثير للكتب الطويلة).", + "engine.weasyprint.description": "ينشئ ملفات PDF من صفحات HTML وMarkdown والبريد الإلكتروني كما يطبعها المتصفح: تظهر العناوين والجداول والخط العريض والنص العربي أو الهندي بشكل صحيح.\nلا يحمّل سوى الصور الموجودة بجانب المستند، ولا شيء من الإنترنت.", "engine.libreoffice.description": "قراءة الصيغ القديمة والأقل شيوعًا: DOC وPPT وPages وKeynote وNumbers وWordPerfect وClarisWorks وPublisher وVisio…\nوالحفظ بصيغ DOC وXLS وPPT وODT وODP وقوالب OpenDocument وEMF.", "engine.translator.description": "يقوم بترجمة المستندات والكتب الإلكترونية محلياً باستخدام نماذج Ollama دون اتصال بالإنترنت.\nيترجم أيضاً ملفات الترجمة بصيغتي SRT وWebVTT: يُرسَل نص المقاطع فقط إلى النموذج، وتبقى أرقام المقاطع وتوقيتاتها كما هي.", "engine.exiftool.description": "يقرأ كل حقول البيانات الوصفية للصور وملفات PDF والفيديو والصوت، ويعدّلها في معظم أنواع الملفات دون المساس بمحتوى الملف.\nتستخدمه أداة البيانات الوصفية؛ ومن دونه تُعرض الحقول للقراءة فقط.", @@ -1317,6 +1318,8 @@ "engines.package.whisper.description": "تحويل الكلام في الفيديو والصوت إلى ملفات ترجمة SRT. يعمل بدون اتصال بالإنترنت.", "engines.package.calibre.name": "Calibre (Kindle وصيغ الكتب النادرة)", "engines.package.calibre.description": "يحفظ MOBI وAZW3 لأجهزة Kindle، ويحوّل صيغ الكتب الإلكترونية النادرة (LIT وLRF وPDB وCHM…).\nتُقرأ EPUB وMOBI وAZW3 وFB2 والقصص المصورة بدونه.", + "engines.package.weasyprint.name": "WeasyPrint (من HTML وMarkdown والبريد إلى PDF)", + "engines.package.weasyprint.description": "ينشئ ملفات PDF أفضل من صفحات HTML وMarkdown والبريد الإلكتروني (EML وMSG وMHTML): العناوين والجداول والخط العريض والكتابات من اليمين إلى اليسار أو الهندية كما يطبعها المتصفح.\nوبدونه تصبح أيضًا ملفات PDF عبر المسار المدمج.", "engines.package.size": "التنزيل {0} · على القرص {1}", "engines.package.system_required": "استخدم نسخة مثبّتة على هذا الحاسوب.", "engines.guidance.macos": "ثبّت {0} لهذا الـ Mac، ثم اختر «استخدام نسختي…» وحدّد البرنامج أو مجلد ‎.app الخاص به.", diff --git a/src/Filee.App/Assets/i18n/bn-BD.json b/src/Filee.App/Assets/i18n/bn-BD.json index 4edb7bb39..bc005f36c 100644 --- a/src/Filee.App/Assets/i18n/bn-BD.json +++ b/src/Filee.App/Assets/i18n/bn-BD.json @@ -284,6 +284,7 @@ "engine.tesseract.description": "স্ক্যান এবং ছবির টেক্সট শনাক্ত করে (OCR): অনুসন্ধানযোগ্য পিডিএফ, DOCX, TXT।", "engine.vision.description": "macOS-এর নিজস্ব টেক্সট শনাক্তকরণ (OCR) দিয়ে স্ক্যান এবং ছবির টেক্সট শনাক্ত করে: অনুসন্ধানযোগ্য পিডিএফ, DOCX, TXT। কিছু ডাউনলোড করার প্রয়োজন নেই।", "engine.calibre.description": "বিরল ই-বুক ফরম্যাট (LIT, LRF, CHM, PDB, RB…) এবং Kindle আউটপুট (MOBI, AZW3)।\nHWPX রাইটারের ওপরে থাকলে এটি ই-বুকের পিডিএফ-ও তৈরি করে (বড় বইয়ের জন্য অনেক দ্রুত)।", + "engine.weasyprint.description": "HTML পেজ, Markdown ও ই-মেইলের PDF ব্রাউজার যেভাবে প্রিন্ট করে সেভাবে তৈরি করে: শিরোনাম, টেবিল, বোল্ড এবং আরবি বা হিন্দি লেখা ঠিকভাবে আসে।\nএটি শুধু ডকুমেন্টের পাশের ছবিগুলো লোড করে, ইন্টারনেট থেকে কিছুই নয়।", "engine.libreoffice.description": "পুরোনো এবং কম প্রচলিত ফরম্যাট পড়ে: DOC, PPT, Pages, Keynote, Numbers, ওয়ার্ডপার্ফেক্ট, ClarisWorks, Publisher, Visio…\nDOC, XLS, PPT, ODT, ODP, OpenDocument টেমপ্লেট এবং EMF হিসেবেও সংরক্ষণ করে।", "engine.translator.description": "অফলাইন Ollama মডেল ব্যবহার করে স্থানীয়ভাবে ডকুমেন্ট এবং ই-বুক অনুবাদ করে।\nSRT এবং WebVTT সাবটাইটেলও অনুবাদ করে: কেবল কিউ টেক্সট মডেলে পাঠানো হয়, কিউ নম্বর ও টাইমস্ট্যাম্প অপরিবর্তিত থাকে।", "engine.exiftool.description": "ছবি, PDF, ভিডিও ও শব্দের প্রতিটি মেটাডেটা ফিল্ড পড়ে এবং ফাইলের বিষয়বস্তু না ছুঁয়ে বেশিরভাগ ধরনের ফাইলে সেগুলো সম্পাদনা করে।\nমেটাডেটা টুল এটি ব্যবহার করে; এটি ছাড়া ফিল্ডগুলো শুধু পড়ার জন্য দেখানো হয়।", @@ -1316,6 +1317,8 @@ "engines.package.whisper.description": "ভিডিও এবং অডিওর কণ্ঠস্বর থেকে SRT সাবটাইটেল ফাইলে রূপান্তর করে। ইন্টারনেট সংযোগ ছাড়াই অফলাইনে চলে।", "engines.package.calibre.name": "Calibre (Kindle এবং বিরল ই-বুক ফরম্যাট)", "engines.package.calibre.description": "Kindle-এর জন্য MOBI ও AZW3 সংরক্ষণ করে এবং বিরল ই-বুক ফরম্যাট (LIT, LRF, PDB, CHM…) রূপান্তর করে।\nEPUB, MOBI, AZW3, FB2 এবং কমিকস এটি ছাড়াই পড়া যায়।", + "engines.package.weasyprint.name": "WeasyPrint (HTML, Markdown ও ই-মেইল থেকে PDF)", + "engines.package.weasyprint.description": "HTML পেজ, Markdown ও ই-মেইলের (EML, MSG, MHTML) আরও ভালো PDF তৈরি করে: শিরোনাম, টেবিল, বোল্ড এবং ডান থেকে বাম বা ভারতীয় লিপি ব্রাউজারের প্রিন্টের মতো।\nএটি ছাড়াও বিল্ট-ইন পথে সেগুলো PDF হয়।", "engines.package.size": "ডাউনলোড {0} · ডিস্কে {1}", "engines.package.system_required": "এই কম্পিউটারে ইনস্টল করা একটি কপি ব্যবহার করুন।", "engines.guidance.macos": "এই Mac-এর জন্য {0} ইনস্টল করুন, তারপর “আমার কপি ব্যবহার করুন…” বেছে নিয়ে এর প্রোগ্রাম বা .app ফোল্ডারটি নির্বাচন করুন।", diff --git a/src/Filee.App/Assets/i18n/de.json b/src/Filee.App/Assets/i18n/de.json index 74f990d1c..6e55819d1 100644 --- a/src/Filee.App/Assets/i18n/de.json +++ b/src/Filee.App/Assets/i18n/de.json @@ -270,6 +270,7 @@ "engine.tesseract.description": "Erkennt Text in Scans und Bildern (OCR): durchsuchbare PDF, DOCX, TXT.", "engine.vision.description": "Erkennt Text in Scans und Bildern mit der Texterkennung von macOS (OCR): durchsuchbare PDF, DOCX, TXT. Kein Download nötig.", "engine.calibre.description": "Seltene E-Book-Formate (LIT, LRF, CHM, PDB, RB…) und Kindle-Ausgabe (MOBI, AZW3).\nOberhalb des HWPX-Writers erstellt es auch PDFs aus E-Books (bei langen Büchern deutlich schneller).", + "engine.weasyprint.description": "Erstellt PDFs von HTML-Seiten, Markdown und E-Mails so, wie ein Browser sie druckt: Überschriften, Tabellen, Fettschrift und arabischer oder Hindi-Text stimmen.\nEs lädt nur die Bilder neben dem Dokument, nichts aus dem Internet.", "engine.libreoffice.description": "Liest ältere und weniger verbreitete Formate: DOC, PPT, Pages, Keynote, Numbers, WordPerfect, ClarisWorks, Publisher, Visio…\nSpeichert auch als DOC, XLS, PPT, ODT, ODP, OpenDocument-Vorlagen und EMF.", "engine.translator.description": "Übersetzt Dokumente und E-Books lokal mit Offline-Ollama-Modellen.\nÜbersetzt auch SRT- und WebVTT-Untertitel: Nur der Text der Einträge wird an das Modell gesendet, Nummern und Zeitstempel der Einträge bleiben erhalten.", "engine.exiftool.description": "Liest alle Metadatenfelder von Bildern, PDFs, Video und Audio und bearbeitet sie in den meisten Dateiarten, ohne den Inhalt der Datei zu verändern.\nVom Metadaten-Werkzeug genutzt; ohne es sind die Felder schreibgeschützt.", @@ -1317,6 +1318,8 @@ "engines.package.whisper.description": "Transkribiert Sprache in Videos und Audio in SRT-Untertiteldateien. Funktioniert offline ohne Internetverbindung.", "engines.package.calibre.name": "Calibre (Kindle und seltene E-Book-Formate)", "engines.package.calibre.description": "Speichert MOBI und AZW3 für Kindle und konvertiert seltene E-Book-Formate (LIT, LRF, PDB, CHM…).\nEPUB, MOBI, AZW3, FB2 und Comics lassen sich auch ohne Calibre lesen.", + "engines.package.weasyprint.name": "WeasyPrint (HTML, Markdown und E-Mail zu PDF)", + "engines.package.weasyprint.description": "Erstellt bessere PDFs von HTML-Seiten, Markdown und E-Mails (EML, MSG, MHTML): Überschriften, Tabellen, Fettschrift sowie rechtsläufige und indische Schriften wie im Browserdruck.\nOhne es werden sie auch zu PDFs, über den eingebauten Weg.", "engines.package.size": "Download {0} · Platzbedarf {1}", "engines.package.system_required": "Eine auf diesem Computer installierte Version verwenden.", "engines.guidance.macos": "Installiere {0} für diesen Mac. Wähle dann „Eigene Version…“ und die zugehörige Programmdatei oder den .app-Ordner.", diff --git a/src/Filee.App/Assets/i18n/en.json b/src/Filee.App/Assets/i18n/en.json index 8c2d96d45..9e73e7e9e 100644 --- a/src/Filee.App/Assets/i18n/en.json +++ b/src/Filee.App/Assets/i18n/en.json @@ -277,6 +277,7 @@ "engine.tesseract.description": "Recognizes the text of scans and pictures (OCR): searchable PDF, DOCX, TXT.", "engine.vision.description": "Recognizes the text of scans and pictures with macOS's own text recognition (OCR): searchable PDF, DOCX, TXT. Nothing to download.", "engine.calibre.description": "Rare e-book formats (LIT, LRF, CHM, PDB, RB…) and Kindle output (MOBI, AZW3).\nAbove the HWPX writer, it also makes the PDFs of e-books (much faster for long books).", + "engine.weasyprint.description": "Makes PDFs of HTML pages, Markdown and e-mails as a browser prints them: headings, tables, bold and Arabic or Hindi text come out right.\nIt loads only the pictures beside the document, nothing from the internet.", "engine.libreoffice.description": "Reads older and less common formats: DOC, PPT, Pages, Keynote, Numbers, WordPerfect, ClarisWorks, Publisher, Visio…\nAlso saves as DOC, XLS, PPT, ODT, ODP, OpenDocument templates and EMF.", "engine.translator.description": "Translates documents and e-books locally using offline Ollama models.\nAlso translates SRT and WebVTT subtitles: only the cue text is sent to the model, cue numbers and timestamps are kept.", "engine.exiftool.description": "Reads every metadata field of pictures, PDFs, video and sound, and edits them in most kinds of file without touching the file's content.\nUsed by the Metadata tool; without it the fields are shown read-only.", @@ -1334,6 +1335,8 @@ "engines.package.whisper.description": "Transcribes speech in video and audio to SRT subtitle files. Runs offline without an internet connection.", "engines.package.calibre.name": "Calibre (Kindle and rare e-book formats)", "engines.package.calibre.description": "Saves MOBI and AZW3 for Kindle and converts rare e-book formats (LIT, LRF, PDB, CHM…).\nEPUB, MOBI, AZW3, FB2 and comics are read without it.", + "engines.package.weasyprint.name": "WeasyPrint (HTML, Markdown and e-mail to PDF)", + "engines.package.weasyprint.description": "Makes better PDFs of HTML pages, Markdown and e-mails (EML, MSG, MHTML): headings, tables, bold and right-to-left or Indic scripts as a browser prints them.\nThey become PDFs without it too, through the built-in route.", "engines.package.size": "Download {0} · {1} on disk", "engines.package.system_required": "Use a copy installed on this computer.", "engines.guidance.macos": "Install {0} for this Mac, then choose “Use my copy…” and select its program or .app folder.", diff --git a/src/Filee.App/Assets/i18n/es.json b/src/Filee.App/Assets/i18n/es.json index c0514ff66..14954c617 100644 --- a/src/Filee.App/Assets/i18n/es.json +++ b/src/Filee.App/Assets/i18n/es.json @@ -270,6 +270,7 @@ "engine.tesseract.description": "Reconoce el texto de escaneos e imágenes (OCR): PDF con búsqueda, DOCX, TXT.", "engine.vision.description": "Reconoce el texto de escaneos e imágenes con el reconocimiento de texto propio de macOS (OCR): PDF con búsqueda, DOCX, TXT. No hay nada que descargar.", "engine.calibre.description": "Formatos raros de libros electrónicos (LIT, LRF, CHM, PDB, RB…) y salida Kindle (MOBI, AZW3).\nPor encima del escritor HWPX, también crea los PDF de libros electrónicos (mucho más rápido para libros largos).", + "engine.weasyprint.description": "Crea los PDF de páginas HTML, Markdown y correos como los imprime un navegador: títulos, tablas, negrita y texto árabe o hindi salen bien.\nSolo carga las imágenes junto al documento, nada de Internet.", "engine.libreoffice.description": "Lee formatos antiguos y poco habituales: DOC, PPT, Pages, Keynote, Numbers, WordPerfect, ClarisWorks, Publisher, Visio…\nTambién guarda como DOC, XLS, PPT, ODT, ODP, plantillas OpenDocument y EMF.", "engine.translator.description": "Traduce documentos y libros electrónicos localmente usando modelos de Ollama fuera de línea.\nTambién traduce subtítulos SRT y WebVTT: solo se envía al modelo el texto de cada entrada; los números de entrada y las marcas de tiempo se conservan.", "engine.exiftool.description": "Lee todos los campos de metadatos de imágenes, PDF, vídeo y audio, y los edita en la mayoría de tipos de archivo sin tocar su contenido.\nLo usa la herramienta Metadatos; sin él, los campos se muestran de solo lectura.", @@ -1317,6 +1318,8 @@ "engines.package.whisper.description": "Transcribe la voz de vídeos y audios a archivos de subtítulos SRT. Funciona sin conexión a internet.", "engines.package.calibre.name": "Calibre (Kindle y libros electrónicos poco habituales)", "engines.package.calibre.description": "Guarda MOBI y AZW3 para Kindle y convierte formatos de libro electrónico poco habituales (LIT, LRF, PDB, CHM…).\nEPUB, MOBI, AZW3, FB2 y cómics se leen sin él.", + "engines.package.weasyprint.name": "WeasyPrint (HTML, Markdown y correo a PDF)", + "engines.package.weasyprint.description": "Crea mejores PDF de páginas HTML, Markdown y correos (EML, MSG, MHTML): títulos, tablas, negrita y escrituras de derecha a izquierda o índicas como las imprime un navegador.\nSin él también se convierten en PDF, por la ruta integrada.", "engines.package.size": "Descargar {0} · {1} en disco", "engines.package.system_required": "Usa una copia instalada en este equipo.", "engines.guidance.macos": "Instala {0} para este Mac y luego elige «Usar mi copia…» y selecciona su programa o su carpeta .app.", diff --git a/src/Filee.App/Assets/i18n/fa.json b/src/Filee.App/Assets/i18n/fa.json index 1bd8836ca..bd3ee5df5 100644 --- a/src/Filee.App/Assets/i18n/fa.json +++ b/src/Filee.App/Assets/i18n/fa.json @@ -284,6 +284,7 @@ "engine.tesseract.description": "متن را در بیش از ۱۰۰ زبان با استفاده از Tesseract OCR تشخیص می‌دهد.", "engine.vision.description": "متن اسکن‌ها و تصاویر را با تشخیص متن خود macOS (OCR) شناسایی می‌کند: PDF قابل جستجو، DOCX، TXT. بدون نیاز به دانلود چیزی.", "engine.calibre.description": "کتاب‌های الکترونیکی را تبدیل و ایجاد می‌کند، جلد و فراداده را تنظیم می‌کند و با فرمت‌های MOBI و AZW3 کار می‌نماید.", + "engine.weasyprint.description": "از صفحه‌های HTML، Markdown و ایمیل‌ها همان‌طور که مرورگر چاپ می‌کند PDF می‌سازد: عنوان‌ها، جدول‌ها، متن پررنگ و متن عربی یا هندی درست درمی‌آیند.\nفقط تصویرهای کنار سند را بارگیری می‌کند، هیچ چیز از اینترنت.", "engine.libreoffice.description": "اسناد Word، Excel، PowerPoint، Visio، OpenDocument، WordPerfect و HWP را با استفاده از LibreOffice رندر و تبدیل می‌کند.", "engine.translator.description": "اسناد، کتاب‌های الکترونیکی و زیرنویس‌ها را به زبان‌های دیگر ترجمه می‌کند. کاملاً آفلاین و روی رایانه شما اجرا می‌شود.", "engine.exiftool.description": "همه فیلدهای متادیتای تصویر، PDF، ویدیو و صدا را می‌خواند و بدون دست زدن به محتوای فایل، آن‌ها را در بیشتر انواع فایل ویرایش می‌کند.\nابزار متادیتا از آن استفاده می‌کند؛ بدون آن فیلدها فقط‌خواندنی نمایش داده می‌شوند.", @@ -1316,6 +1317,8 @@ "engines.package.whisper.description": "گفتار در فایل‌های صوتی و ویدیویی را به زیرنویس متنی تبدیل می‌کند.", "engines.package.calibre.name": "Calibre (فرمت‌های نادر کتاب الکترونیکی)", "engines.package.calibre.description": "کتاب‌ها را در فرمت‌های MOBI و AZW3 برای Kindle ذخیره می‌کند و فرمت‌های نادر (LIT، LRF، PDB، CHM…) را تبدیل می‌نماید.\nفرمت‌های EPUB، MOBI، AZW3، FB2 و کمیک‌ها بدون آن نیز خوانده می‌شوند.", + "engines.package.weasyprint.name": "WeasyPrint (از HTML، Markdown و ایمیل به PDF)", + "engines.package.weasyprint.description": "از صفحه‌های HTML، Markdown و ایمیل‌ها (EML، MSG، MHTML) PDF بهتری می‌سازد: عنوان‌ها، جدول‌ها، متن پررنگ و خط‌های راست‌به‌چپ یا هندی همان‌طور که مرورگر چاپ می‌کند.\nبدون آن هم از راه داخلی به PDF تبدیل می‌شوند.", "engines.package.size": "دانلود {0} · {1} روی دیسک", "engines.package.system_required": "از نسخه نصب‌شده روی این رایانه استفاده کنید.", "engines.guidance.macos": "{0} را برای این مک نصب کنید، سپس «استفاده از نسخه من…» را انتخاب کرده و برنامه یا پوشه .app آن را برگزینید.", diff --git a/src/Filee.App/Assets/i18n/fr.json b/src/Filee.App/Assets/i18n/fr.json index c27675e61..0b95a193d 100644 --- a/src/Filee.App/Assets/i18n/fr.json +++ b/src/Filee.App/Assets/i18n/fr.json @@ -277,6 +277,7 @@ "engine.tesseract.description": "Reconnaît le texte des numérisations et des images (OCR) : PDF consultable, DOCX, TXT.", "engine.vision.description": "Reconnaît le texte des numérisations et des images avec la reconnaissance de texte intégrée à macOS (OCR) : PDF consultable, DOCX, TXT. Rien à télécharger.", "engine.calibre.description": "Formats de livres numériques rares (LIT, LRF, CHM, PDB, RB…) et sortie Kindle (MOBI, AZW3).\nPlacé au-dessus du rédacteur HWPX, il produit aussi les PDF des livres numériques (bien plus vite pour les longs livres).", + "engine.weasyprint.description": "Crée les PDF des pages HTML, du Markdown et des e-mails comme un navigateur les imprime : titres, tableaux, gras et textes arabes ou hindis sont corrects.\nIl ne charge que les images à côté du document, rien depuis Internet.", "engine.libreoffice.description": "Lit les formats anciens et moins courants : DOC, PPT, Pages, Keynote, Numbers, WordPerfect, ClarisWorks, Publisher, Visio…\nEnregistre aussi en DOC, XLS, PPT, ODT, ODP, modèles OpenDocument et EMF.", "engine.translator.description": "Traduit les documents et livres électroniques localement à l'aide de modèles Ollama hors ligne.\nTraduit aussi les sous-titres SRT et WebVTT : seul le texte des répliques est envoyé au modèle, les numéros et les horodatages sont conservés.", "engine.exiftool.description": "Lit tous les champs de métadonnées des images, PDF, vidéos et sons, et les modifie dans la plupart des types de fichiers sans toucher au contenu.\nUtilisé par l'outil Métadonnées ; sans lui, les champs sont en lecture seule.", @@ -1334,6 +1335,8 @@ "engines.package.whisper.description": "Transcrit la parole des vidéos et audios en fichiers de sous-titres SRT. Fonctionne hors ligne sans connexion Internet.", "engines.package.calibre.name": "Calibre (Kindle et formats de livres numériques rares)", "engines.package.calibre.description": "Enregistre en MOBI et AZW3 pour Kindle et convertit les formats de livres numériques rares (LIT, LRF, PDB, CHM…).\nEPUB, MOBI, AZW3, FB2 et bandes dessinées sont lus sans lui.", + "engines.package.weasyprint.name": "WeasyPrint (HTML, Markdown et e-mails en PDF)", + "engines.package.weasyprint.description": "Crée de meilleurs PDF des pages HTML, du Markdown et des e-mails (EML, MSG, MHTML) : titres, tableaux, gras et écritures de droite à gauche ou indiennes comme un navigateur les imprime.\nSans lui, ils deviennent aussi des PDF par la route intégrée.", "engines.package.size": "Téléchargement {0} · {1} sur le disque", "engines.package.system_required": "Utiliser une copie installée sur cet ordinateur.", "engines.guidance.macos": "Installez {0} pour ce Mac, puis choisissez « Utiliser ma copie… » et sélectionnez son programme ou son dossier .app.", diff --git a/src/Filee.App/Assets/i18n/hi.json b/src/Filee.App/Assets/i18n/hi.json index 1c9e0b53c..3737f2501 100644 --- a/src/Filee.App/Assets/i18n/hi.json +++ b/src/Filee.App/Assets/i18n/hi.json @@ -270,6 +270,7 @@ "engine.tesseract.description": "स्कैन और तस्वीरों का टेक्स्ट पहचानता है (OCR): खोजने योग्य PDF, DOCX, TXT।", "engine.vision.description": "macOS की अपनी टेक्स्ट पहचान से स्कैन और तस्वीरों का टेक्स्ट पहचानता है (OCR): खोजने योग्य PDF, DOCX, TXT। कुछ डाउनलोड नहीं करना पड़ता।", "engine.calibre.description": "दुर्लभ ई-बुक फ़ॉर्मैट (LIT, LRF, CHM, PDB, RB…) और Kindle आउटपुट (MOBI, AZW3)।\nHWPX writer के ऊपर होने पर यह ई-बुक के PDF भी बनाता है (लंबी किताबों के लिए बहुत तेज़)।", + "engine.weasyprint.description": "HTML पेज, Markdown और ईमेल के PDF वैसे बनाता है जैसे ब्राउज़र उन्हें प्रिंट करता है: शीर्षक, तालिकाएँ, बोल्ड और अरबी या हिंदी पाठ सही आते हैं।\nयह केवल दस्तावेज़ के पास की तस्वीरें लोड करता है, इंटरनेट से कुछ नहीं।", "engine.libreoffice.description": "पुराने और कम प्रचलित फ़ॉर्मैट पढ़ता है: DOC, PPT, Pages, Keynote, Numbers, WordPerfect, ClarisWorks, Publisher, Visio…\nDOC, XLS, PPT, ODT, ODP, OpenDocument टेम्पलेट और EMF के रूप में सहेजता भी है।", "engine.translator.description": "ऑफ़लाइन ओलामा मॉडल का उपयोग करके दस्तावेज़ों और ई-पुस्तकों का स्थानीय अनुवाद करता है।\nSRT और WebVTT सबटाइटल का भी अनुवाद करता है: मॉडल को केवल क्यू का टेक्स्ट भेजा जाता है, क्यू नंबर और टाइमस्टैम्प वैसे ही रहते हैं।", "engine.exiftool.description": "चित्रों, PDF, वीडियो और ऑडियो के सभी मेटाडेटा फ़ील्ड पढ़ता है, और ज़्यादातर तरह की फ़ाइलों में फ़ाइल की सामग्री छुए बिना उन्हें बदलता है।\nमेटाडेटा टूल इसका उपयोग करता है; इसके बिना फ़ील्ड केवल पढ़ने के लिए दिखते हैं।", @@ -1317,6 +1318,8 @@ "engines.package.whisper.description": "वीडियो और ऑडियो के भाषण को SRT उपशीर्षक फ़ाइलों में ट्रांसक्राइब करता है। इंटरनेट कनेक्शन के बिना ऑफ़लाइन काम करता है।", "engines.package.calibre.name": "Calibre (Kindle और दुर्लभ ई-बुक फ़ॉर्मैट)", "engines.package.calibre.description": "Kindle के लिए MOBI और AZW3 सहेजता है और दुर्लभ ई-बुक फ़ॉर्मैट (LIT, LRF, PDB, CHM…) कन्वर्ट करता है।\nEPUB, MOBI, AZW3, FB2 और कॉमिक्स इसके बिना भी पढ़े जाते हैं।", + "engines.package.weasyprint.name": "WeasyPrint (HTML, Markdown और ईमेल से PDF)", + "engines.package.weasyprint.description": "HTML पेज, Markdown और ईमेल (EML, MSG, MHTML) के बेहतर PDF बनाता है: शीर्षक, तालिकाएँ, बोल्ड और दाएँ से बाएँ या भारतीय लिपियाँ वैसे ही जैसे ब्राउज़र प्रिंट करता है।\nइसके बिना भी वे अंतर्निहित तरीके से PDF बनते हैं।", "engines.package.size": "डाउनलोड {0} · डिस्क पर {1}", "engines.package.system_required": "इस कंप्यूटर पर इंस्टॉल की गई कॉपी इस्तेमाल करें।", "engines.guidance.macos": "इस Mac के लिए {0} इंस्टॉल करें, फिर “मेरी कॉपी इस्तेमाल करें…” चुनें और उसका प्रोग्राम या .app फ़ोल्डर चुनें।", diff --git a/src/Filee.App/Assets/i18n/id.json b/src/Filee.App/Assets/i18n/id.json index 4c02a912f..1e5dfa448 100644 --- a/src/Filee.App/Assets/i18n/id.json +++ b/src/Filee.App/Assets/i18n/id.json @@ -270,6 +270,7 @@ "engine.tesseract.description": "Mengenali teks dari hasil pindai dan gambar (OCR): PDF yang dapat dicari, DOCX, TXT.", "engine.vision.description": "Mengenali teks dari hasil pindai dan gambar dengan pengenal teks bawaan macOS (OCR): PDF yang dapat dicari, DOCX, TXT. Tanpa perlu mengunduh apa pun.", "engine.calibre.description": "Format e-book langka (LIT, LRF, CHM, PDB, RB…) dan output Kindle (MOBI, AZW3).\nBila diposisikan di atas pembuat HWPX, Calibre juga membuat PDF dari e-book (jauh lebih cepat untuk buku tebal).", + "engine.weasyprint.description": "Membuat PDF dari halaman HTML, Markdown, dan email seperti yang dicetak browser: judul, tabel, huruf tebal, serta teks Arab atau Hindi tampil benar.\nHanya memuat gambar di samping dokumen, tidak ada dari internet.", "engine.libreoffice.description": "Membaca format lama dan jarang digunakan: DOC, PPT, Pages, Keynote, Numbers, WordPerfect, ClarisWorks, Publisher, Visio…\nJuga menyimpan sebagai DOC, XLS, PPT, ODT, ODP, templat OpenDocument, dan EMF.", "engine.translator.description": "Menerjemahkan dokumen dan e-book secara lokal menggunakan model Ollama offline.\nJuga menerjemahkan subtitle SRT dan WebVTT: hanya teks cue yang dikirim ke model, nomor cue dan stempel waktu tetap dipertahankan.", "engine.exiftool.description": "Membaca semua bidang metadata gambar, PDF, video, dan audio, serta mengeditnya di sebagian besar jenis file tanpa menyentuh isi file.\nDigunakan oleh alat Metadata; tanpanya, bidang hanya dapat dibaca.", @@ -1317,6 +1318,8 @@ "engines.package.whisper.description": "Mentranskripsikan ucapan dalam video dan audio menjadi file subtitle SRT. Berjalan offline tanpa koneksi internet.", "engines.package.calibre.name": "Calibre (Kindle dan format e-book langka)", "engines.package.calibre.description": "Menyimpan MOBI dan AZW3 untuk Kindle serta mengonversi format e-book langka (LIT, LRF, PDB, CHM…).\nEPUB, MOBI, AZW3, FB2, dan komik dapat dibaca tanpanya.", + "engines.package.weasyprint.name": "WeasyPrint (HTML, Markdown, dan email ke PDF)", + "engines.package.weasyprint.description": "Membuat PDF yang lebih baik dari halaman HTML, Markdown, dan email (EML, MSG, MHTML): judul, tabel, huruf tebal, serta aksara kanan-ke-kiri atau India seperti cetakan browser.\nTanpanya, file tetap menjadi PDF lewat rute bawaan.", "engines.package.size": "Unduhan {0} · {1} di disk", "engines.package.system_required": "Gunakan salinan yang terpasang di komputer ini.", "engines.guidance.macos": "Pasang {0} untuk Mac ini, lalu pilih “Gunakan salinan saya…” dan pilih folder program atau folder .app miliknya.", diff --git a/src/Filee.App/Assets/i18n/it.json b/src/Filee.App/Assets/i18n/it.json index 292f914b9..04795d3d4 100644 --- a/src/Filee.App/Assets/i18n/it.json +++ b/src/Filee.App/Assets/i18n/it.json @@ -269,6 +269,7 @@ "engine.tesseract.description": "Riconosce il testo di scansioni e immagini (OCR): PDF ricercabile, DOCX, TXT.", "engine.vision.description": "Riconosce il testo di scansioni e immagini con il riconoscimento testo di macOS (OCR): PDF ricercabile, DOCX, TXT. Non c'è nulla da scaricare.", "engine.calibre.description": "Formati di e-book rari (LIT, LRF, CHM, PDB, RB…) e output per Kindle (MOBI, AZW3).\nPosto sopra il writer HWPX, crea anche i PDF degli e-book (molto più veloce per i libri lunghi).", + "engine.weasyprint.description": "Crea i PDF di pagine HTML, Markdown ed e-mail come li stampa un browser: titoli, tabelle, grassetto e testo arabo o hindi vengono corretti.\nCarica solo le immagini accanto al documento, niente da Internet.", "engine.libreoffice.description": "Legge formati vecchi e meno comuni: DOC, PPT, Pages, Keynote, Numbers, WordPerfect, ClarisWorks, Publisher, Visio…\nSalva anche come DOC, XLS, PPT, ODT, ODP, modelli OpenDocument ed EMF.", "engine.translator.description": "Traduce documenti ed e-book localmente utilizzando modelli Ollama offline.\nTraduce anche i sottotitoli SRT e WebVTT: al modello viene inviato solo il testo delle battute, numerazione e tempi restano invariati.", "engine.exiftool.description": "Legge tutti i campi dei metadati di immagini, PDF, video e audio, e li modifica nella maggior parte dei tipi di file senza toccarne il contenuto.\nUsato dallo strumento Metadati; senza, i campi sono di sola lettura.", @@ -1316,6 +1317,8 @@ "engines.package.whisper.description": "Trascrive la voce in video e audio in file di sottotitoli SRT. Funziona offline senza connessione Internet.", "engines.package.calibre.name": "Calibre (Kindle e formati di e-book rari)", "engines.package.calibre.description": "Salva MOBI e AZW3 per Kindle e converte formati di e-book rari (LIT, LRF, PDB, CHM…).\nEPUB, MOBI, AZW3, FB2 e fumetti si leggono senza.", + "engines.package.weasyprint.name": "WeasyPrint (HTML, Markdown ed e-mail in PDF)", + "engines.package.weasyprint.description": "Crea PDF migliori di pagine HTML, Markdown ed e-mail (EML, MSG, MHTML): titoli, tabelle, grassetto e scritture da destra a sinistra o indiane come le stampa un browser.\nAnche senza di esso diventano PDF, con il percorso integrato.", "engines.package.size": "Download {0} · {1} su disco", "engines.package.system_required": "Usa una copia installata su questo computer.", "engines.guidance.macos": "Installa {0} per questo Mac, poi scegli “Usa la mia copia…” e seleziona il suo programma o la cartella .app.", diff --git a/src/Filee.App/Assets/i18n/ja.json b/src/Filee.App/Assets/i18n/ja.json index a35cbc281..b0128dbc9 100644 --- a/src/Filee.App/Assets/i18n/ja.json +++ b/src/Filee.App/Assets/i18n/ja.json @@ -270,6 +270,7 @@ "engine.tesseract.description": "スキャンや画像の文字を認識します(OCR):検索可能な PDF、DOCX、TXT。", "engine.vision.description": "macOS 標準の文字認識で、スキャンや画像の文字を認識します(OCR):検索可能な PDF、DOCX、TXT。ダウンロードは不要です。", "engine.calibre.description": "一般的でない電子書籍形式(LIT、LRF、CHM、PDB、RBなど)を変換し、Kindle用のMOBI、AZW3形式で保存できます。\n「HWPX writer」より上に置くと、電子書籍のPDF作成もCalibreが担当します(長い本では大幅に速くなります)。", + "engine.weasyprint.description": "HTML ページ、Markdown、メールをブラウザーで印刷したとおりに PDF にします。見出し、表、太字、アラビア語やヒンディー語の文字も正しく出ます。\n読み込むのは文書の横にある画像だけで、インターネットからは何も読み込みません。", "engine.libreoffice.description": "古い形式や一般的でない形式(DOC、PPT、Pages、Keynote、Numbers、WordPerfect、ClarisWorks、Publisher、Visioなど)を読み込みます。\nDOC、XLS、PPT、ODT、ODP、OpenDocumentテンプレート、EMF形式での保存にも対応します。", "engine.translator.description": "オフラインのOllamaモデルを使用して、ローカルでドキュメントや電子書籍を翻訳します。\nSRT・WebVTT字幕も翻訳します。モデルに送るのは字幕のテキストだけで、字幕番号とタイムスタンプはそのまま保持されます。", "engine.exiftool.description": "画像、PDF、動画、音声のすべてのメタデータ項目を読み取り、ほとんどの種類のファイルでは内容に触れずに編集します。\nメタデータツールが使います。ない場合、項目は読み取り専用で表示されます。", @@ -1317,6 +1318,8 @@ "engines.package.whisper.description": "動画や音声の音声をSRT字幕ファイルに書き起こします。インターネット接続なしでオフラインで動作します。", "engines.package.calibre.name": "Calibre(Kindle形式・一般的でない電子書籍形式)", "engines.package.calibre.description": "Kindle用にMOBI、AZW3形式で保存できるほか、一般的でない電子書籍形式(LIT、LRF、PDB、CHMなど)を変換できます。\nEPUB、MOBI、AZW3、FB2、コミックの読み込みには、このエンジンは必要ありません。", + "engines.package.weasyprint.name": "WeasyPrint(HTML・Markdown・メールを PDF に)", + "engines.package.weasyprint.description": "HTML ページ、Markdown、メール(EML、MSG、MHTML)をよりきれいな PDF にします。見出し、表、太字、右から左に書く文字やインドの文字もブラウザーで印刷したとおりです。\nなくても、組み込みの方法で PDF になります。", "engines.package.size": "ダウンロード:{0} · ディスク使用量:{1}", "engines.package.system_required": "このコンピューターにインストールされているものを使ってください。", "engines.guidance.macos": "このMacに{0}をインストールし、「手元のプログラムを使う…」を選んで、プログラムまたは.appフォルダーを指定してください。", diff --git a/src/Filee.App/Assets/i18n/ko.json b/src/Filee.App/Assets/i18n/ko.json index e2e06bada..0befe9771 100644 --- a/src/Filee.App/Assets/i18n/ko.json +++ b/src/Filee.App/Assets/i18n/ko.json @@ -277,6 +277,7 @@ "engine.tesseract.description": "스캔본과 사진의 글자를 인식해요(OCR): 검색 가능한 PDF, DOCX, TXT.", "engine.vision.description": "macOS에 들어 있는 문자 인식으로 스캔본과 사진의 글자를 인식해요(OCR): 검색 가능한 PDF, DOCX, TXT. 따로 내려받을 게 없어요.", "engine.calibre.description": "드문 전자책 형식(LIT, LRF, CHM, PDB, RB…)과 킨들용 출력(MOBI, AZW3).\nHWPX writer보다 위에 두면 전자책 PDF도 만들어요(긴 책은 훨씬 빨라요).", + "engine.weasyprint.description": "HTML 페이지, Markdown, 이메일을 브라우저에서 인쇄한 그대로 PDF로 만들어요. 제목, 표, 굵은 글씨, 아랍어·힌디어 텍스트도 올바르게 나와요.\n문서 옆에 있는 그림만 불러오고 인터넷에서는 아무것도 불러오지 않아요.", "engine.libreoffice.description": "DOC, PPT, Pages, Keynote, Numbers, 워드퍼펙트, ClarisWorks, Publisher, Visio 같은 오래되었거나 드문 형식을 읽어요.\nDOC·XLS·PPT·ODT·ODP, 오픈도큐먼트 서식 파일, EMF로 저장도 해요.", "engine.translator.description": "오프라인 Ollama 모델을 사용해 문서와 전자책을 컴퓨터에서 번역해요.\nSRT·WebVTT 자막도 번역해요. 모델에는 대사 텍스트만 보내고, 자막 번호와 시간은 그대로 둬요.", "engine.exiftool.description": "사진, PDF, 동영상, 소리의 모든 메타데이터 항목을 읽고, 대부분의 파일 종류에서 파일 내용은 건드리지 않고 편집해요.\n메타데이터 도구가 사용해요. 없으면 항목을 읽기 전용으로 보여 줘요.", @@ -1334,6 +1335,8 @@ "engines.package.whisper.description": "동영상과 오디오의 말소리를 SRT 자막 파일로 바꿔요. 인터넷 연결 없이 작동해요.", "engines.package.calibre.name": "Calibre (킨들과 드문 전자책 형식)", "engines.package.calibre.description": "킨들용 MOBI·AZW3로 저장하고, 드문 전자책 형식(LIT, LRF, PDB, CHM 등)을 변환해요.\nEPUB·MOBI·AZW3·FB2와 만화책은 없어도 읽어요.", + "engines.package.weasyprint.name": "WeasyPrint (HTML, Markdown, 이메일 → PDF)", + "engines.package.weasyprint.description": "HTML 페이지, Markdown, 이메일(EML, MSG, MHTML)을 더 나은 PDF로 만들어요. 제목, 표, 굵은 글씨, 오른쪽에서 왼쪽으로 쓰는 문자와 인도 문자도 브라우저 인쇄처럼 나와요.\n없어도 내장 경로로 PDF가 돼요.", "engines.package.size": "다운로드 {0} · 설치 후 {1}", "engines.package.system_required": "이 컴퓨터에 설치된 엔진을 연결해 주세요.", "engines.guidance.macos": "이 Mac에 {0}을(를) 설치한 뒤 “내 프로그램 쓰기…”로 프로그램이나 .app 폴더를 선택해 주세요.", diff --git a/src/Filee.App/Assets/i18n/ne.json b/src/Filee.App/Assets/i18n/ne.json index 320f12e7a..e4311b4ca 100644 --- a/src/Filee.App/Assets/i18n/ne.json +++ b/src/Filee.App/Assets/i18n/ne.json @@ -270,6 +270,7 @@ "engine.tesseract.description": "स्क्यान र तस्बिरको पाठ पहिचान गर्छ (OCR): खोज्न मिल्ने PDF, DOCX, TXT।", "engine.vision.description": "macOS को आफ्नै पाठ पहिचानले स्क्यान र तस्बिरको पाठ पहिचान गर्छ (OCR): खोज्न मिल्ने PDF, DOCX, TXT। केही डाउनलोड गर्नु पर्दैन।", "engine.calibre.description": "दुर्लभ इ-पुस्तक ढाँचाहरू (LIT, LRF, CHM, PDB, RB…) र Kindle आउटपुट (MOBI, AZW3)।\nHWPX writer भन्दा माथि राख्दा, यसले इ-पुस्तकका PDF पनि बनाउँछ (लामो पुस्तकका लागि धेरै छिटो)।", + "engine.weasyprint.description": "HTML पृष्ठ, Markdown र इमेलका PDF ब्राउजरले छापेजस्तै बनाउँछ: शीर्षक, तालिका, बोल्ड र अरबी वा हिन्दी पाठ ठीकसँग आउँछन्।\nयसले कागजातको छेउका तस्बिरहरू मात्र लोड गर्छ, इन्टरनेटबाट केही पनि होइन।", "engine.libreoffice.description": "पुराना र कम प्रचलित ढाँचाहरू पढ्छ: DOC, PPT, Pages, Keynote, Numbers, WordPerfect, ClarisWorks, Publisher, Visio…\nसाथै DOC, XLS, PPT, ODT, ODP, OpenDocument टेम्प्लेट र EMF को रूपमा सुरक्षित गर्दछ।", "engine.translator.description": "अफलाइन ओलामा मोडेलहरू प्रयोग गरेर कागजात र ई-पुस्तकहरू स्थानीय रूपमा अनुवाद गर्दछ।\nSRT र WebVTT उपशीर्षकहरू पनि अनुवाद गर्दछ: मोडेललाई क्यूको पाठ मात्र पठाइन्छ, क्यू नम्बर र टाइमस्ट्याम्प जस्ताको तस्तै राखिन्छ।", "engine.exiftool.description": "चित्र, PDF, भिडियो र अडियोका सबै मेटाडाटा फिल्ड पढ्छ, र धेरैजसो प्रकारका फाइलमा फाइलको सामग्री नछोई तिनलाई सम्पादन गर्छ।\nमेटाडाटा उपकरणले यसलाई प्रयोग गर्छ; यसबिना फिल्डहरू पढ्न मात्र मिल्ने गरी देखिन्छन्।", @@ -1317,6 +1318,8 @@ "engines.package.whisper.description": "भिडियो र अडियोमा भएको भाषणलाई SRT उपशीर्षक फाइलहरूमा ट्रान्सक्राइब गर्दछ। इन्टरनेट जडान बिना अफलाइन काम गर्दछ।", "engines.package.calibre.name": "Calibre (Kindle र दुर्लभ इ-पुस्तक ढाँचाहरू)", "engines.package.calibre.description": "Kindle का लागि MOBI र AZW3 सुरक्षित गर्दछ र दुर्लभ इ-पुस्तक ढाँचाहरू (LIT, LRF, PDB, CHM…) रूपान्तरण गर्दछ।\nEPUB, MOBI, AZW3, FB2 र कमिक्स यस बिना पनि पढ्न सकिन्छ।", + "engines.package.weasyprint.name": "WeasyPrint (HTML, Markdown र इमेलबाट PDF)", + "engines.package.weasyprint.description": "HTML पृष्ठ, Markdown र इमेल (EML, MSG, MHTML) का राम्रा PDF बनाउँछ: शीर्षक, तालिका, बोल्ड र दायाँबाट बायाँ वा भारतीय लिपिहरू ब्राउजरले छापेजस्तै।\nयसविना पनि तिनीहरू भित्री बाटोबाट PDF बन्छन्।", "engines.package.size": "डाउनलोड {0} · डिस्कमा {1}", "engines.package.system_required": "यस कम्प्युटरमा स्थापना गरिएको प्रतिलिपि प्रयोग गर्नुहोस्।", "engines.guidance.macos": "यस Mac का लागि {0} स्थापना गर्नुहोस्, त्यसपछि “मेरो प्रतिलिपि प्रयोग गर्नुहोस्…” रोजेर यसको प्रोग्राम वा .app फोल्डर चयन गर्नुहोस्।", diff --git a/src/Filee.App/Assets/i18n/pl.json b/src/Filee.App/Assets/i18n/pl.json index 016fb9607..d5818d606 100644 --- a/src/Filee.App/Assets/i18n/pl.json +++ b/src/Filee.App/Assets/i18n/pl.json @@ -277,6 +277,7 @@ "engine.tesseract.description": "Rozpoznaje tekst skanów i obrazów (OCR): przeszukiwalny PDF, DOCX, TXT.", "engine.vision.description": "Rozpoznaje tekst skanów i obrazów wbudowanym rozpoznawaniem tekstu macOS (OCR): przeszukiwalny PDF, DOCX, TXT. Nic nie trzeba pobierać.", "engine.calibre.description": "Rzadkie formaty e-booków (LIT, LRF, CHM, PDB, RB…) i zapis dla Kindle (MOBI, AZW3).\nUmieszczony nad HWPX writer tworzy też PDF-y e-booków (znacznie szybciej przy długich książkach).", + "engine.weasyprint.description": "Tworzy pliki PDF ze stron HTML, Markdown i e-maili tak, jak drukuje je przeglądarka: nagłówki, tabele, pogrubienia oraz tekst arabski lub hindi wychodzą poprawnie.\nWczytuje tylko obrazy obok dokumentu, nic z internetu.", "engine.libreoffice.description": "Odczytuje starsze i rzadsze formaty: DOC, PPT, Pages, Keynote, Numbers, WordPerfect, ClarisWorks, Publisher, Visio…\nZapisuje też jako DOC, XLS, PPT, ODT, ODP, szablony OpenDocument i EMF.", "engine.translator.description": "Tłumaczy dokumenty i e-booki lokalnie za pomocą modeli offline Ollama.\nTłumaczy też napisy SRT i WebVTT: do modelu trafia tylko tekst napisów, a numery i znaczniki czasu pozostają bez zmian.", "engine.exiftool.description": "Odczytuje wszystkie pola metadanych obrazów, PDF-ów, wideo i dźwięku oraz edytuje je w większości rodzajów plików, nie zmieniając ich zawartości.\nUżywa go narzędzie Metadane; bez niego pola są tylko do odczytu.", @@ -1334,6 +1335,8 @@ "engines.package.whisper.description": "Transkrybuje mowę z filmów i nagrań audio do plików napisów SRT. Działa offline bez połączenia z internetem.", "engines.package.calibre.name": "Calibre (Kindle i rzadkie formaty e-booków)", "engines.package.calibre.description": "Zapisuje MOBI i AZW3 dla Kindle i konwertuje rzadkie formaty e-booków (LIT, LRF, PDB, CHM…).\nEPUB, MOBI, AZW3, FB2 i komiksy są odczytywane bez niego.", + "engines.package.weasyprint.name": "WeasyPrint (HTML, Markdown i e-mail do PDF)", + "engines.package.weasyprint.description": "Tworzy lepsze pliki PDF ze stron HTML, Markdown i e-maili (EML, MSG, MHTML): nagłówki, tabele, pogrubienia oraz pisma od prawej do lewej i indyjskie tak, jak drukuje je przeglądarka.\nBez niego też stają się PDF-ami, wbudowaną drogą.", "engines.package.size": "Pobieranie {0} · {1} na dysku", "engines.package.system_required": "Użyj kopii zainstalowanej na tym komputerze.", "engines.guidance.macos": "Zainstaluj {0} dla tego Maca, potem wybierz „Użyj mojej kopii…” i wskaż jego program lub folder .app.", diff --git a/src/Filee.App/Assets/i18n/pt-BR.json b/src/Filee.App/Assets/i18n/pt-BR.json index 044461a63..f1308cfbd 100644 --- a/src/Filee.App/Assets/i18n/pt-BR.json +++ b/src/Filee.App/Assets/i18n/pt-BR.json @@ -277,6 +277,7 @@ "engine.tesseract.description": "Reconhece o texto de digitalizações e imagens (OCR): PDF pesquisável, DOCX, TXT.", "engine.vision.description": "Reconhece o texto de digitalizações e imagens com o reconhecimento de texto do próprio macOS (OCR): PDF pesquisável, DOCX, TXT. Nada para baixar.", "engine.calibre.description": "Formatos raros de e-book (LIT, LRF, CHM, PDB, RB…) e saída Kindle (MOBI, AZW3).\nAcima do gravador de HWPX, também gera os PDFs de e-books (bem mais rápido em livros longos).", + "engine.weasyprint.description": "Cria PDFs de páginas HTML, Markdown e e-mails como um navegador os imprime: títulos, tabelas, negrito e texto árabe ou hindi saem certos.\nCarrega só as imagens ao lado do documento, nada da internet.", "engine.libreoffice.description": "Lê formatos antigos e menos comuns: DOC, PPT, Pages, Keynote, Numbers, WordPerfect, ClarisWorks, Publisher, Visio…\nTambém salva como DOC, XLS, PPT, ODT, ODP, modelos OpenDocument e EMF.", "engine.translator.description": "Traduz documentos e e-books localmente usando modelos offline do Ollama.\nTambém traduz legendas SRT e WebVTT: só o texto de cada legenda é enviado ao modelo; os números e os tempos das legendas são mantidos.", "engine.exiftool.description": "Lê todos os campos de metadados de imagens, PDFs, vídeo e áudio, e os edita na maioria dos tipos de arquivo sem mexer no conteúdo.\nUsado pela ferramenta Metadados; sem ele, os campos aparecem somente para leitura.", @@ -1334,6 +1335,8 @@ "engines.package.whisper.description": "Transcreve a fala de vídeos e áudios para arquivos de legendas SRT. Funciona offline sem conexão com a internet.", "engines.package.calibre.name": "Calibre (Kindle e formatos raros de e-book)", "engines.package.calibre.description": "Salva MOBI e AZW3 para Kindle e converte formatos raros de e-book (LIT, LRF, PDB, CHM…).\nEPUB, MOBI, AZW3, FB2 e quadrinhos são lidos sem ele.", + "engines.package.weasyprint.name": "WeasyPrint (HTML, Markdown e e-mail para PDF)", + "engines.package.weasyprint.description": "Cria PDFs melhores de páginas HTML, Markdown e e-mails (EML, MSG, MHTML): títulos, tabelas, negrito e escritas da direita para a esquerda ou indianas como um navegador os imprime.\nSem ele, também viram PDF pela rota integrada.", "engines.package.size": "Download de {0} · {1} em disco", "engines.package.system_required": "Use uma cópia instalada neste computador.", "engines.guidance.macos": "Instale {0} para este Mac, depois escolha “Usar minha cópia…” e selecione o programa ou a pasta .app.", diff --git a/src/Filee.App/Assets/i18n/pt-PT.json b/src/Filee.App/Assets/i18n/pt-PT.json index 3b598acc3..38cd4628c 100644 --- a/src/Filee.App/Assets/i18n/pt-PT.json +++ b/src/Filee.App/Assets/i18n/pt-PT.json @@ -277,6 +277,7 @@ "engine.tesseract.description": "Reconhece o texto de digitalizações e imagens (OCR): PDF pesquisável, DOCX e TXT.", "engine.vision.description": "Reconhece o texto de digitalizações e imagens com o reconhecimento de texto do macOS (OCR): PDF pesquisável, DOCX e TXT. Não é necessário descarregar nada.", "engine.calibre.description": "Formatos raros de livros digitais (LIT, LRF, CHM, PDB, RB…) e saída para Kindle (MOBI, AZW3).\nAcima do gerador HWPX, também cria PDF de livros digitais (muito mais depressa para livros longos).", + "engine.weasyprint.description": "Cria PDF de páginas HTML, Markdown e e-mails como um navegador os imprime: títulos, tabelas, negrito e texto árabe ou hindi ficam corretos.\nCarrega apenas as imagens ao lado do documento, nada da Internet.", "engine.libreoffice.description": "Lê formatos antigos e menos comuns: DOC, PPT, Pages, Keynote, Numbers, WordPerfect, ClarisWorks, Publisher, Visio…\nTambém guarda em DOC, XLS, PPT, ODT, ODP, modelos OpenDocument e EMF.", "engine.translator.description": "Traduz documentos e e-books localmente através de modelos offline do Ollama.\nTambém traduz legendas SRT e WebVTT: apenas o texto de cada legenda é enviado ao modelo; os números e os tempos das legendas mantêm-se.", "engine.exiftool.description": "Lê todos os campos de metadados de imagens, PDF, vídeo e áudio, e edita-os na maioria dos tipos de ficheiro sem mexer no conteúdo.\nUsado pela ferramenta Metadados; sem ele, os campos aparecem só para leitura.", @@ -1334,6 +1335,8 @@ "engines.package.whisper.description": "Transcreve a fala de vídeos e áudios para ficheiros de legendas SRT. Funciona offline sem ligação à Internet.", "engines.package.calibre.name": "Calibre (Kindle e formatos raros de livros digitais)", "engines.package.calibre.description": "Guarda em MOBI e AZW3 para Kindle e converte formatos raros de livros digitais (LIT, LRF, PDB, CHM…).\nEPUB, MOBI, AZW3, FB2 e banda desenhada são lidos sem ele.", + "engines.package.weasyprint.name": "WeasyPrint (HTML, Markdown e e-mail para PDF)", + "engines.package.weasyprint.description": "Cria PDF melhores de páginas HTML, Markdown e e-mails (EML, MSG, MHTML): títulos, tabelas, negrito e escritas da direita para a esquerda ou indianas como um navegador os imprime.\nSem ele, também se tornam PDF pela via integrada.", "engines.package.size": "Transferência de {0} · {1} no disco", "engines.package.system_required": "Utiliza uma cópia instalada neste computador.", "engines.guidance.macos": "Instala o {0} para este Mac, escolhe «Utilizar a minha cópia…» e seleciona o programa ou a pasta .app.", diff --git a/src/Filee.App/Assets/i18n/ru.json b/src/Filee.App/Assets/i18n/ru.json index 4ebb6fab9..a51acbf48 100644 --- a/src/Filee.App/Assets/i18n/ru.json +++ b/src/Filee.App/Assets/i18n/ru.json @@ -277,6 +277,7 @@ "engine.tesseract.description": "Распознаёт текст сканов и изображений (OCR): PDF с поиском, DOCX, TXT.", "engine.vision.description": "Распознаёт текст сканов и изображений встроенным в macOS распознаванием текста (OCR): PDF с поиском, DOCX, TXT. Ничего скачивать не нужно.", "engine.calibre.description": "Редкие форматы электронных книг (LIT, LRF, CHM, PDB, RB…) и вывод для Kindle (MOBI, AZW3).\nЕсли стоит выше HWPX writer, он же делает PDF из электронных книг (для длинных книг намного быстрее).", + "engine.weasyprint.description": "Создаёт PDF из HTML-страниц, Markdown и писем так, как их печатает браузер: заголовки, таблицы, жирный шрифт и арабский или хинди текст выглядят правильно.\nЗагружает только изображения рядом с документом, ничего из интернета.", "engine.libreoffice.description": "Читает старые и редкие форматы: DOC, PPT, Pages, Keynote, Numbers, WordPerfect, ClarisWorks, Publisher, Visio…\nА ещё сохраняет в DOC, XLS, PPT, ODT, ODP, шаблоны OpenDocument и EMF.", "engine.translator.description": "Переводит документы и электронные книги локально с помощью офлайн-моделей Ollama.\nТакже переводит субтитры SRT и WebVTT: в модель отправляется только текст реплик, номера и тайм-коды сохраняются.", "engine.exiftool.description": "Читает все поля метаданных изображений, PDF, видео и звука и изменяет их в большинстве типов файлов, не трогая содержимое.\nИспользуется инструментом «Метаданные»; без него поля доступны только для чтения.", @@ -1334,6 +1335,8 @@ "engines.package.whisper.description": "Преобразует речь из видео и аудио в файлы субтитров SRT. Работает автономно без подключения к Интернету.", "engines.package.calibre.name": "Calibre (Kindle и редкие форматы книг)", "engines.package.calibre.description": "Сохраняет MOBI и AZW3 для Kindle и конвертирует редкие форматы электронных книг (LIT, LRF, PDB, CHM…).\nEPUB, MOBI, AZW3, FB2 и комиксы читаются и без него.", + "engines.package.weasyprint.name": "WeasyPrint (HTML, Markdown и письма в PDF)", + "engines.package.weasyprint.description": "Создаёт более качественные PDF из HTML-страниц, Markdown и писем (EML, MSG, MHTML): заголовки, таблицы, жирный шрифт и письменности справа налево или индийские, как при печати из браузера.\nБез него они тоже становятся PDF встроенным способом.", "engines.package.size": "Загрузка: {0} · на диске: {1}", "engines.package.system_required": "Используйте копию, установленную на этом компьютере.", "engines.guidance.macos": "Установите {0} для этого Mac, затем выберите «Использовать свою копию…» и укажите программу или папку .app.", diff --git a/src/Filee.App/Assets/i18n/th.json b/src/Filee.App/Assets/i18n/th.json index 774623b30..d8a3acea9 100644 --- a/src/Filee.App/Assets/i18n/th.json +++ b/src/Filee.App/Assets/i18n/th.json @@ -270,6 +270,7 @@ "engine.tesseract.description": "อ่านข้อความจากภาพสแกนและรูปภาพ (OCR): PDF ที่ค้นหาได้, DOCX, TXT", "engine.vision.description": "อ่านข้อความจากภาพสแกนและรูปภาพด้วยระบบรู้จำข้อความในตัวของ macOS (OCR): PDF ที่ค้นหาได้, DOCX, TXT ไม่ต้องดาวน์โหลดอะไรเพิ่ม", "engine.calibre.description": "ฟอร์แมตอีบุ๊กที่หาได้ยาก (LIT, LRF, CHM, PDB, RB…) และไฟล์สำหรับ Kindle (MOBI, AZW3)\nเมื่อวางไว้เหนือตัวเขียน HWPX จะทำหน้าที่สร้าง PDF ของอีบุ๊กด้วย (เร็วกว่ามากสำหรับหนังสือเล่มหนา)", + "engine.weasyprint.description": "สร้าง PDF จากหน้า HTML, Markdown และอีเมลแบบที่เบราว์เซอร์พิมพ์: หัวข้อ ตาราง ตัวหนา และข้อความภาษาอาหรับหรือฮินดีออกมาถูกต้อง\nโหลดเฉพาะรูปภาพที่อยู่ข้างเอกสาร ไม่โหลดอะไรจากอินเทอร์เน็ต", "engine.libreoffice.description": "อ่านฟอร์แมตรุ่นเก่าและฟอร์แมตที่พบน้อย: DOC, PPT, Pages, Keynote, Numbers, WordPerfect, ClarisWorks, Publisher, Visio…\nและบันทึกเป็น DOC, XLS, PPT, ODT, ODP, แม่แบบ OpenDocument และ EMF", "engine.translator.description": "แปลเอกสารและอีบุ๊กแบบออฟไลน์บนเครื่องโดยใช้โมเดล Ollama\nแปลคำบรรยาย SRT และ WebVTT ได้ด้วย โดยส่งเฉพาะข้อความคำบรรยายไปยังโมเดล ส่วนหมายเลขและเวลาของแต่ละช่วงจะคงเดิม", "engine.exiftool.description": "อ่านทุกฟิลด์เมทาดาตาของรูปภาพ PDF วิดีโอ และเสียง และแก้ไขได้ในไฟล์ส่วนใหญ่โดยไม่แตะต้องเนื้อหาของไฟล์\nเครื่องมือเมทาดาตาใช้สิ่งนี้ หากไม่มี ฟิลด์จะแสดงแบบอ่านอย่างเดียว", @@ -1317,6 +1318,8 @@ "engines.package.whisper.description": "ถอดเสียงคำพูดในวิดีโอและเสียงเป็นไฟล์คำบรรยาย SRT ทำงานแบบออฟไลน์โดยไม่ต้องเชื่อมต่ออินเทอร์เน็ต", "engines.package.calibre.name": "Calibre (Kindle และอีบุ๊กหายาก)", "engines.package.calibre.description": "บันทึกเป็น MOBI และ AZW3 สำหรับ Kindle และแปลงฟอร์แมตอีบุ๊กหายาก (LIT, LRF, PDB, CHM…)\n(EPUB, MOBI, AZW3, FB2 และการ์ตูนสามารถอ่านได้โดยไม่ต้องติดตั้ง)", + "engines.package.weasyprint.name": "WeasyPrint (HTML, Markdown และอีเมลเป็น PDF)", + "engines.package.weasyprint.description": "สร้าง PDF ที่ดีกว่าจากหน้า HTML, Markdown และอีเมล (EML, MSG, MHTML): หัวข้อ ตาราง ตัวหนา และอักษรที่เขียนจากขวาไปซ้ายหรืออักษรอินเดียแบบที่เบราว์เซอร์พิมพ์\nหากไม่มีก็ยังเป็น PDF ได้ผ่านเส้นทางในตัว", "engines.package.size": "ดาวน์โหลด {0} · ขนาดบนดิสก์ {1}", "engines.package.system_required": "ใช้โปรแกรมที่ติดตั้งอยู่แล้วในคอมพิวเตอร์เครื่องนี้", "engines.guidance.macos": "ติดตั้ง {0} สำหรับ Mac เครื่องนี้ จากนั้นเลือก “ใช้โปรแกรมในเครื่อง…” แล้วเลือกโปรแกรมหรือโฟลเดอร์ .app", diff --git a/src/Filee.App/Assets/i18n/tr.json b/src/Filee.App/Assets/i18n/tr.json index e4195232f..8db57b717 100644 --- a/src/Filee.App/Assets/i18n/tr.json +++ b/src/Filee.App/Assets/i18n/tr.json @@ -270,6 +270,7 @@ "engine.tesseract.description": "Tarama ve görsellerdeki metni tanır (OCR): aranabilir PDF, DOCX, TXT.", "engine.vision.description": "Tarama ve görsellerdeki metni macOS'un kendi metin tanıma özelliğiyle tanır (OCR): aranabilir PDF, DOCX, TXT. İndirme gerekmez.", "engine.calibre.description": "Nadir e-kitap biçimleri (LIT, LRF, CHM, PDB, RB…) ve Kindle çıktısı (MOBI, AZW3).\nHWPX yazıcısının üstüne alındığında e-kitapları PDF'ye de dönüştürür (uzun kitaplarda çok daha hızlıdır).", + "engine.weasyprint.description": "HTML sayfalarının, Markdown'ın ve e-postaların PDF'lerini bir tarayıcının yazdırdığı gibi oluşturur: başlıklar, tablolar, kalın yazı ve Arapça ya da Hintçe metin doğru çıkar.\nYalnızca belgenin yanındaki resimleri yükler, internetten hiçbir şey yüklemez.", "engine.libreoffice.description": "Eski ve az kullanılan biçimleri okur: DOC, PPT, Pages, Keynote, Numbers, WordPerfect, ClarisWorks, Publisher, Visio…\nDOC, XLS, PPT, ODT, ODP, OpenDocument şablonları ve EMF olarak da kaydeder.", "engine.translator.description": "Çevrimdışı Ollama modellerini kullanarak belgeleri ve e-kitapları yerel olarak çevirir.\nSRT ve WebVTT altyazılarını da çevirir: modele yalnızca altyazı metni gönderilir, altyazı numaraları ve zaman damgaları korunur.", "engine.exiftool.description": "Resimlerin, PDF'lerin, videonun ve sesin tüm üst veri alanlarını okur ve çoğu dosya türünde dosyanın içeriğine dokunmadan düzenler.\nÜst Veri aracı kullanır; o olmadan alanlar salt okunur gösterilir.", @@ -1317,6 +1318,8 @@ "engines.package.whisper.description": "Video ve sesteki konuşmaları SRT altyazı dosyalarına dönüştürür. İnternet bağlantısı olmadan çevrimdışı çalışır.", "engines.package.calibre.name": "Calibre (Kindle ve nadir e-kitap biçimleri)", "engines.package.calibre.description": "Kindle için MOBI ve AZW3 kaydeder; nadir e-kitap biçimlerini dönüştürür (LIT, LRF, PDB, CHM…).\nEPUB, MOBI, AZW3, FB2 ve çizgi romanlar bu motor olmadan okunur.", + "engines.package.weasyprint.name": "WeasyPrint (HTML, Markdown ve e-postadan PDF'ye)", + "engines.package.weasyprint.description": "HTML sayfalarının, Markdown'ın ve e-postaların (EML, MSG, MHTML) daha iyi PDF'lerini oluşturur: başlıklar, tablolar, kalın yazı ve sağdan sola ya da Hint yazıları tarayıcı çıktısındaki gibi.\nO olmadan da yerleşik yolla PDF olurlar.", "engines.package.size": "İndirme {0} · Diskte {1}", "engines.package.system_required": "Bu bilgisayarda kurulu kopyayı kullanın.", "engines.guidance.macos": "Bu Mac için {0} kurun; ardından “Kendi kopyamı kullan…” ile programını veya .app klasörünü seçin.", diff --git a/src/Filee.App/Assets/i18n/vi.json b/src/Filee.App/Assets/i18n/vi.json index 4a4dbaa5f..f3bb5cbbc 100644 --- a/src/Filee.App/Assets/i18n/vi.json +++ b/src/Filee.App/Assets/i18n/vi.json @@ -277,6 +277,7 @@ "engine.tesseract.description": "Nhận dạng văn bản trong bản quét và ảnh (OCR): PDF có thể tìm kiếm, DOCX, TXT.", "engine.vision.description": "Nhận dạng văn bản trong bản quét và ảnh bằng tính năng nhận dạng văn bản của chính macOS (OCR): PDF có thể tìm kiếm, DOCX, TXT. Không cần tải gì.", "engine.calibre.description": "Định dạng sách điện tử hiếm (LIT, LRF, CHM, PDB, RB…) và đầu ra Kindle (MOBI, AZW3).\nKhi đặt trên trình ghi HWPX, Calibre cũng tạo PDF cho sách điện tử (nhanh hơn nhiều với sách dài).", + "engine.weasyprint.description": "Tạo PDF từ trang HTML, Markdown và email giống như trình duyệt in: tiêu đề, bảng, chữ đậm và văn bản tiếng Ả Rập hay tiếng Hindi hiển thị đúng.\nChỉ tải ảnh nằm cạnh tài liệu, không tải gì từ Internet.", "engine.libreoffice.description": "Đọc các định dạng cũ hơn và ít phổ biến: DOC, PPT, Pages, Keynote, Numbers, WordPerfect, ClarisWorks, Publisher, Visio…\nCũng lưu dưới dạng DOC, XLS, PPT, ODT, ODP, mẫu OpenDocument và EMF.", "engine.translator.description": "Dịch tài liệu và sách điện tử ngay trên máy bằng các mô hình Ollama ngoại tuyến.\nCũng dịch phụ đề SRT và WebVTT: chỉ văn bản của từng câu phụ đề được gửi tới mô hình, số thứ tự và mốc thời gian được giữ nguyên.", "engine.exiftool.description": "Đọc mọi trường siêu dữ liệu của ảnh, PDF, video và âm thanh, và sửa chúng ở hầu hết các loại tệp mà không đụng đến nội dung tệp.\nĐược công cụ Siêu dữ liệu dùng; không có nó, các trường chỉ đọc.", @@ -1334,6 +1335,8 @@ "engines.package.whisper.description": "Chuyển lời nói trong video và âm thanh thành tệp phụ đề SRT. Chạy ngoại tuyến, không cần kết nối Internet.", "engines.package.calibre.name": "Calibre (Kindle và định dạng sách điện tử hiếm)", "engines.package.calibre.description": "Lưu MOBI và AZW3 cho Kindle và chuyển đổi các định dạng sách điện tử hiếm (LIT, LRF, PDB, CHM…).\nEPUB, MOBI, AZW3, FB2 và truyện tranh được đọc mà không cần Calibre.", + "engines.package.weasyprint.name": "WeasyPrint (HTML, Markdown và email sang PDF)", + "engines.package.weasyprint.description": "Tạo PDF tốt hơn từ trang HTML, Markdown và email (EML, MSG, MHTML): tiêu đề, bảng, chữ đậm và chữ viết từ phải sang trái hay chữ Ấn Độ giống như trình duyệt in.\nKhông có nó, tệp vẫn thành PDF qua đường tích hợp.", "engines.package.size": "Tải xuống {0} · {1} trên đĩa", "engines.package.system_required": "Dùng bản đã cài trên máy tính này.", "engines.guidance.macos": "Cài {0} cho máy Mac này, rồi chọn “Dùng bản của tôi…” và chọn chương trình hoặc thư mục .app của nó.", diff --git a/src/Filee.App/Assets/i18n/zh-CN.json b/src/Filee.App/Assets/i18n/zh-CN.json index 8fefa9659..15b87512a 100644 --- a/src/Filee.App/Assets/i18n/zh-CN.json +++ b/src/Filee.App/Assets/i18n/zh-CN.json @@ -277,6 +277,7 @@ "engine.tesseract.description": "识别扫描件和图片中的文字(OCR):可搜索的 PDF、DOCX、TXT。", "engine.vision.description": "使用 macOS 自带的文字识别来识别扫描件和图片中的文字(OCR):可搜索的 PDF、DOCX、TXT。无需下载。", "engine.calibre.description": "少见的电子书格式(LIT、LRF、CHM、PDB、RB…)以及 Kindle 输出(MOBI、AZW3)。\n排在 HWPX writer 之上时,也由它把电子书转为 PDF(长书快得多)。", + "engine.weasyprint.description": "像浏览器打印那样把 HTML 页面、Markdown 和电子邮件制作成 PDF:标题、表格、粗体以及阿拉伯语或印地语文本都能正确显示。\n只加载文档旁边的图片,不从互联网加载任何内容。", "engine.libreoffice.description": "读取较旧或少见的格式,如 DOC、PPT、Pages、Keynote、Numbers、WordPerfect、ClarisWorks、Publisher、Visio。\n也能保存为 DOC、XLS、PPT、ODT、ODP、OpenDocument 模板或 EMF。", "engine.translator.description": "使用离线 Ollama 模型在本地翻译文档和电子书。\n也能翻译 SRT 和 WebVTT 字幕:只把字幕文本发送给模型,字幕序号和时间轴保持不变。", "engine.exiftool.description": "读取图片、PDF、视频和音频的所有元数据字段,并在大多数类型的文件中编辑它们,不改动文件内容。\n供“元数据”工具使用;没有它时,字段只能查看。", @@ -1334,6 +1335,8 @@ "engines.package.whisper.description": "将视频和音频中的语音转录为 SRT 字幕文件。无需互联网连接即可离线运行。", "engines.package.calibre.name": "Calibre(Kindle 与少见电子书格式)", "engines.package.calibre.description": "可保存为 Kindle 用的 MOBI 和 AZW3,并转换少见的电子书格式(LIT、LRF、PDB、CHM 等)。\nEPUB、MOBI、AZW3、FB2 和漫画无需它即可读取。", + "engines.package.weasyprint.name": "WeasyPrint(HTML、Markdown 和邮件转 PDF)", + "engines.package.weasyprint.description": "为 HTML 页面、Markdown 和电子邮件(EML、MSG、MHTML)制作更好的 PDF:标题、表格、粗体以及从右到左书写的文字或印度文字都与浏览器打印一致。\n没有它,这些文件也能通过内置方式转为 PDF。", "engines.package.size": "下载 {0} · 占用 {1}", "engines.package.system_required": "请使用此电脑上已安装的引擎。", "engines.guidance.macos": "先在这台 Mac 上安装 {0},再点“使用我的程序…”选择它的程序或 .app 文件夹。", diff --git a/src/Filee.App/Assets/i18n/zh-TW.json b/src/Filee.App/Assets/i18n/zh-TW.json index 4956c7684..35451fc6c 100644 --- a/src/Filee.App/Assets/i18n/zh-TW.json +++ b/src/Filee.App/Assets/i18n/zh-TW.json @@ -277,6 +277,7 @@ "engine.tesseract.description": "辨識掃描檔和圖片中的文字(OCR):可搜尋的 PDF、DOCX、TXT。", "engine.vision.description": "使用 macOS 內建的文字辨識來辨識掃描檔和圖片中的文字(OCR):可搜尋的 PDF、DOCX、TXT。不需要下載。", "engine.calibre.description": "少見的電子書格式(LIT、LRF、CHM、PDB、RB…)以及 Kindle 輸出(MOBI、AZW3)。\n排在 HWPX writer 之上時,也由它把電子書轉為 PDF(長書快得多)。", + "engine.weasyprint.description": "像瀏覽器列印那樣把 HTML 頁面、Markdown 和電子郵件製作成 PDF:標題、表格、粗體以及阿拉伯文或印地文文字都能正確顯示。\n只載入文件旁邊的圖片,不從網際網路載入任何內容。", "engine.libreoffice.description": "讀取較舊或少見的格式,如 DOC、PPT、Pages、Keynote、Numbers、WordPerfect、ClarisWorks、Publisher、Visio。\n也能儲存為 DOC、XLS、PPT、ODT、ODP、OpenDocument 範本或 EMF。", "engine.translator.description": "使用離線 Ollama 模型在本地翻譯文件與電子書。\n也能翻譯 SRT 和 WebVTT 字幕:只將字幕文字傳送給模型,字幕序號和時間軸保持不變。", "engine.exiftool.description": "讀取圖片、PDF、影片和音訊的所有中繼資料欄位,並在大多數類型的檔案中編輯它們,不更動檔案內容。\n供「中繼資料」工具使用;沒有它時,欄位只能檢視。", @@ -1334,6 +1335,8 @@ "engines.package.whisper.description": "將影片與音訊中的語音轉錄為 SRT 字幕檔。無需網際網路連線即可離線執行。", "engines.package.calibre.name": "Calibre(Kindle 與少見電子書格式)", "engines.package.calibre.description": "可儲存為 Kindle 用的 MOBI 和 AZW3,並轉換少見的電子書格式(LIT、LRF、PDB、CHM 等)。\nEPUB、MOBI、AZW3、FB2 和漫畫無需它即可讀取。", + "engines.package.weasyprint.name": "WeasyPrint(HTML、Markdown 和郵件轉 PDF)", + "engines.package.weasyprint.description": "為 HTML 頁面、Markdown 和電子郵件(EML、MSG、MHTML)製作更好的 PDF:標題、表格、粗體以及由右至左書寫的文字或印度文字都與瀏覽器列印一致。\n沒有它,這些檔案也能透過內建方式轉為 PDF。", "engines.package.size": "下載 {0} · 佔用 {1}", "engines.package.system_required": "請使用此電腦上已安裝的引擎。", "engines.guidance.macos": "先在這台 Mac 上安裝 {0},再點「使用我的程式…」選擇它的程式或 .app 資料夾。", diff --git a/src/Filee.Core/Conversion/IConverter.cs b/src/Filee.Core/Conversion/IConverter.cs index 11560a0bb..5254e1091 100644 --- a/src/Filee.Core/Conversion/IConverter.cs +++ b/src/Filee.Core/Conversion/IConverter.cs @@ -63,10 +63,16 @@ namespace Filee.Core.Conversion; /// left as it is (, ExifTool). It is never part of an ordinary route, and with /// asking for it it is the whole route. /// +/// +/// The edge is used only in routes that start from one of these formats (ids, lower case); null for every source. +/// WeasyPrint's HTML → PDF serves pages, Markdown and e-mails, which become HTML first, but must not become a hop for +/// a Word document or an e-book that the HTML writers could also turn into a page. +/// public readonly record struct ConversionEdge( string From, string To, int Cost = 10, bool ImageSourcesOnly = false, bool NotForImageSources = false, bool DirectOnly = false, bool FrameSequence = false, bool Unshaped = false, bool MotionPhoto = false, bool LivePhoto = false, - bool Ocr = false, bool Caption = false, bool Translate = false, bool Metadata = false, bool Upscale = false); + bool Ocr = false, bool Caption = false, bool Translate = false, bool Metadata = false, bool Upscale = false, + IReadOnlyList? Sources = null); /// /// A conversion an engine refuses with a reason the user can act on, shown in their language (the file's diff --git a/src/Filee.Core/Conversion/RoutePlanner.cs b/src/Filee.Core/Conversion/RoutePlanner.cs index ce4f6844d..36e94cd5e 100644 --- a/src/Filee.Core/Conversion/RoutePlanner.cs +++ b/src/Filee.Core/Conversion/RoutePlanner.cs @@ -182,6 +182,7 @@ public RoutePlanner(IEnumerable converters, Func? p if (edge.To == edge.From || edge.FrameSequence || edge.MotionPhoto || edge.LivePhoto || edge.Ocr || edge.Translate || edge.Caption || edge.Upscale || (edge.ImageSourcesOnly && !imageSource) || (edge.NotForImageSources && imageSource) || (edge.Unshaped && shaping) + || (edge.Sources is { } sources && !sources.Contains(from)) || (edge.DirectOnly && (node != from || edge.To != to))) continue; var next = cost + edgeCost; diff --git a/src/Filee.Engines/EngineRegistry.cs b/src/Filee.Engines/EngineRegistry.cs index dc527f2fa..4e3db5e73 100644 --- a/src/Filee.Engines/EngineRegistry.cs +++ b/src/Filee.Engines/EngineRegistry.cs @@ -81,6 +81,7 @@ public static IReadOnlyList CreateAll(EngineEnvironment env, ILogger new VisionConverter(), // before Tesseract: a Mac's OCR step (Tesseract reads the languages Vision doesn't) new TesseractConverter(), new CalibreConverter(), + new WeasyPrintConverter(), new LibreOfficeConverter(env, loggers?.CreateLogger()), ]; diff --git a/src/Filee.Engines/Infrastructure/CopyVersions.cs b/src/Filee.Engines/Infrastructure/CopyVersions.cs index 5c0fe5440..571438f6a 100644 --- a/src/Filee.Engines/Infrastructure/CopyVersions.cs +++ b/src/Filee.Engines/Infrastructure/CopyVersions.cs @@ -50,6 +50,7 @@ public static bool IsOutdated(string id, string folder) => "ffmpeg" => FfmpegVersion().Match(text), "pandoc" => PandocVersion().Match(text), "calibre" => CalibreVersion().Match(text), + "weasyprint" => WeasyPrintVersion().Match(text), "libreoffice" => LibreOfficeVersion().Match(text), _ => LeadingVersion().Match(text), }; @@ -107,6 +108,9 @@ private static string Read(string id, string program) [GeneratedRegex(@"calibre (\d+)\.(\d+)")] private static partial Regex CalibreVersion(); + [GeneratedRegex(@"WeasyPrint version (\d+)\.(\d+)")] + private static partial Regex WeasyPrintVersion(); + [GeneratedRegex(@"(?:MsiProductVersion=|LibreOffice\s+)(\d+)\.(\d+)")] private static partial Regex LibreOfficeVersion(); diff --git a/src/Filee.Engines/Infrastructure/EngineDownloads.cs b/src/Filee.Engines/Infrastructure/EngineDownloads.cs index aae632aef..2f70f0d43 100644 --- a/src/Filee.Engines/Infrastructure/EngineDownloads.cs +++ b/src/Filee.Engines/Infrastructure/EngineDownloads.cs @@ -53,6 +53,9 @@ internal static string DownloadRuntime(string runtimeIdentifier, int windowsBuil new("ghostscript", OperatingSystem.IsWindows() ? ["ghostscript", "vcruntime"] : ["ghostscript"], 31_000_000, ["ghostscript"]), new("ffmpeg", ["ffmpeg"], 272_000_000, ["ffmpeg"]), new("calibre", ["calibre"], 663_000_000, ["calibre"]), + // WeasyPrint's own Windows build (Python, Pango and HarfBuzz in one folder). Linux and macOS follow once + // WeasyPrint publishes a self-contained build for them (#1403); until then they keep their routes. + .. (OperatingSystem.IsWindows() ? [new EnginePackage("weasyprint", ["weasyprint"], 65_000_000, ["weasyprint"])] : Array.Empty()), new("tesseract", ["tesseract"], 168_000_000, ["tesseract"]), new("upscayl-bin", OperatingSystem.IsWindows() && RuntimeInformation.ProcessArchitecture != Architecture.Arm64 ? ["upscayl-bin", "vcruntime", "vcomp14"] : OperatingSystem.IsWindows() ? [] : ["upscayl-bin"], 8_600_000, ["upscaler"]), .. (OperatingSystem.IsWindows() ? [new EnginePackage("whisper", ["whisper", "whisper-model"], 156_880_105, ["whisper"])] : Array.Empty()), @@ -119,10 +122,11 @@ public static string DownloadList(EnginePackage package, IReadOnlyDictionary public static bool IsSuggested(EnginePackage package) => - CanDownload(package) && DownloadSize(package) < SuggestLimit && package.Id is not ("ghostscript" or "tesseract" or "upscayl-bin"); + CanDownload(package) && DownloadSize(package) < SuggestLimit && package.Id is not ("ghostscript" or "tesseract" or "upscayl-bin" or "weasyprint"); /// The running macOS; downloads whose minimumMacOS is newer are left out. Null keeps all. public static IReadOnlyDictionary ComponentsForRuntime(string runtimeIdentifier, Version? macOSVersion = null) diff --git a/src/Filee.Engines/Infrastructure/EngineEnvironment.cs b/src/Filee.Engines/Infrastructure/EngineEnvironment.cs index e081e1091..a40d68411 100644 --- a/src/Filee.Engines/Infrastructure/EngineEnvironment.cs +++ b/src/Filee.Engines/Infrastructure/EngineEnvironment.cs @@ -85,6 +85,7 @@ dataDirectoryOverride is not null ["ghostscript"] = [windows ? "gswin64c.exe" : "gs"], ["ffmpeg"] = windows ? ["ffmpeg.exe", "ffprobe.exe"] : ["ffmpeg", "ffprobe"], ["calibre"] = [windows ? "ebook-convert.exe" : "ebook-convert"], + ["weasyprint"] = [windows ? "weasyprint.exe" : "weasyprint"], ["rhwp"] = [windows ? "rhwp.exe" : "rhwp"], ["7zip"] = [windows ? "7z.exe" : "7zz"], ["tesseract"] = [windows ? "tesseract.exe" : "tesseract"], diff --git a/src/Filee.Engines/Infrastructure/EngineInstaller.cs b/src/Filee.Engines/Infrastructure/EngineInstaller.cs index 9dc3948eb..5a7301af0 100644 --- a/src/Filee.Engines/Infrastructure/EngineInstaller.cs +++ b/src/Filee.Engines/Infrastructure/EngineInstaller.cs @@ -573,9 +573,10 @@ private Task UnpackAsync(EngineComponent component, string file, CancellationTok { case "zip": ZipFile.ExtractToDirectory(file, staging); - RenameExifTool(component.Id, SingleRoot(staging)); - EnsureExecutablePrograms(component.Id, SingleRoot(staging)); - Replace(component.Id, SingleRoot(staging)); + var root = component.Id == "weasyprint" ? WeasyPrintFolder(staging) : SingleRoot(staging); + RenameExifTool(component.Id, root); + EnsureExecutablePrograms(component.Id, root); + Replace(component.Id, root); break; case "tar.gz": case "tar.xz": @@ -901,6 +902,26 @@ private void CopyRuntime(EnginePackage package) } } + /// + /// WeasyPrint's Windows "onedir" zip holds LICENSE and README.rst at its top and the program in + /// onedir/weasyprint (weasyprint.exe with its _internal folder of Python, Pango, HarfBuzz, ...): that folder is + /// kept, with the licence moved into it. + /// + internal static string WeasyPrintFolder(string staging) + { + var program = EngineEnvironment.ProgramName("weasyprint.exe"); + var found = Directory.EnumerateFiles(staging, program, SearchOption.AllDirectories).FirstOrDefault() + ?? throw new InvalidDataException($"{program} not found in the WeasyPrint package."); + var folder = Path.GetDirectoryName(found)!; + foreach (var name in new[] { "LICENSE", "README.rst" }) + { + var top = Path.Combine(staging, name); + if (File.Exists(top) && !File.Exists(Path.Combine(folder, name))) + File.Move(top, Path.Combine(folder, name)); + } + return folder; + } + /// An archive with a single top-level folder is unpacked without that extra level. private static string SingleRoot(string folder) { diff --git a/src/Filee.Engines/Infrastructure/engines.json b/src/Filee.Engines/Infrastructure/engines.json index 3a7e09bc5..c721198e9 100644 --- a/src/Filee.Engines/Infrastructure/engines.json +++ b/src/Filee.Engines/Infrastructure/engines.json @@ -78,6 +78,13 @@ "size": 240201728, "kind": "msi" }, + "weasyprint": { + "version": "70.0", + "url": "https://github.com/Kozea/WeasyPrint/releases/download/v70.0/weasyprint-windows-onedir.zip", + "sha256": "AB1151F210B4E6BB7AA7A79E91A67E8DDB760094C107BFDA55241B6AAEFE7D53", + "size": 32271073, + "kind": "zip" + }, "tesseract": { "version": "5.4.0.20240606", "url": "https://github.com/UB-Mannheim/tesseract/releases/download/v5.4.0.20240606/tesseract-ocr-w64-setup-5.4.0.20240606.exe", diff --git a/src/Filee.Engines/Office/LinkedContent.cs b/src/Filee.Engines/Office/LinkedContent.cs index e8cc536e2..5dccc88a2 100644 --- a/src/Filee.Engines/Office/LinkedContent.cs +++ b/src/Filee.Engines/Office/LinkedContent.cs @@ -9,7 +9,8 @@ // * Office Open XML: relationships with TargetMode="External", except hyperlinks; // * OpenDocument (packages and flat XML): xlink:href outside the package on anything but hyperlinks and event // listeners (pictures, objects, section and table sources, sounds, fill images); -// * HTML: picture, frame, object and style sheet addresses and CSS url() other than data: URIs (hyperlinks stay). +// * HTML: picture, frame, object and style sheet addresses and CSS url() other than data: URIs and references to the +// page's own elements (#id, e.g. an inline SVG's ); hyperlinks stay. // Pictures the page may use by the rule every reader follows (DocumentFiles: inside the document's own folder) are // put into the copy as data: URIs, because the profile keeps LibreOffice from loading even those; // * RTF: INCLUDEPICTURE fields (their last result stays, as for any field LibreOffice doesn't know). @@ -187,7 +188,7 @@ private static bool IsOutside(string href, IReadOnlySet? entries) var resource = key is "src" or "srcset" or "lowsrc" or "dynsrc" or "data" or "poster" or "background" or "codebase" or "xlink:href" or "longdesc" || key == "href" && name is "link" or "base" or "image" or "use"; - if (!resource || value.TrimStart().StartsWith("data:", StringComparison.OrdinalIgnoreCase)) + if (!resource || value.TrimStart().StartsWith("data:", StringComparison.OrdinalIgnoreCase) || SameDocument(value)) return attribute.Value; return name == "img" && key == "src" && picture?.Invoke(Utf8(attribute.Groups["value"].Value)) is { } uri ? $"{attribute.Groups["space"].Value}src=\"{uri}\"" @@ -205,9 +206,10 @@ private static string Utf8(string attribute) => /// /// The data: URI of a picture inside the page's folder (or the original document's, for a page made from it), by - /// . SVG stays out: a drawing can link to further files, and LibreOffice would load them. + /// . SVG stays out unless : a drawing can link to further files, and + /// LibreOffice would load them (WeasyPrint, run with only data: allowed, doesn't). /// - private static Func PagePictures(string page, string? originalSource) + internal static Func PagePictures(string page, string? originalSource, bool svg = false) { var folder = Path.GetDirectoryName(Path.GetFullPath(page))!; string[] roots = originalSource is null ? [folder] : [folder, Path.GetDirectoryName(Path.GetFullPath(originalSource))!]; @@ -216,20 +218,29 @@ private static string Utf8(string attribute) => { if (DocumentFiles.Find(address, folder, roots) is not { } file || Path.GetExtension(file).TrimStart('.').ToLowerInvariant() is var extension - && extension is not ("png" or "jpg" or "jpeg" or "gif" or "bmp" or "webp" or "tif" or "tiff")) + && extension is not ("png" or "jpg" or "jpeg" or "gif" or "bmp" or "webp" or "tif" or "tiff") + && !(svg && extension == "svg")) return null; var length = new FileInfo(file).Length; if (length > left) return null; left -= length; - return $"data:image/{extension};base64,{Convert.ToBase64String(File.ReadAllBytes(file))}"; + var type = extension switch { "svg" => "svg+xml", "jpg" => "jpeg", "tif" => "tiff", _ => extension }; + return $"data:image/{type};base64,{Convert.ToBase64String(File.ReadAllBytes(file))}"; }; } - /// CSS with url() other than data: emptied and @import rules taken out. + /// CSS with url() other than data: and same-document references (#id) emptied and @import rules taken out. private static string CssUrls(string css) => Import().Replace(CssUrl().Replace(css, url => - url.Groups["address"].Value.TrimStart().StartsWith("data:", StringComparison.OrdinalIgnoreCase) ? url.Value : "url()"), ""); + url.Groups["address"].Value.TrimStart().StartsWith("data:", StringComparison.OrdinalIgnoreCase) || SameDocument(url.Groups["address"].Value) + ? url.Value : "url()"), ""); + + /// + /// A reference to an element of the page itself (an inline SVG's <use href="#icon">, a gradient's + /// xlink:href="#base", fill:url(#g)): nothing outside the page is named, so it stays. + /// + private static bool SameDocument(string address) => address.TrimStart().StartsWith('#'); /// RTF whose INCLUDEPICTURE fields are renamed to a field LibreOffice doesn't know, or null without any. internal static byte[]? StripRtf(byte[] rtf) diff --git a/src/Filee.Engines/Text/WeasyPrintConverter.cs b/src/Filee.Engines/Text/WeasyPrintConverter.cs new file mode 100644 index 000000000..7a19254ee --- /dev/null +++ b/src/Filee.Engines/Text/WeasyPrintConverter.cs @@ -0,0 +1,141 @@ +// HTML → PDF with WeasyPrint (BSD-3-Clause, a separate program the user downloads in Settings → Engines, Windows only +// for now, #1403). WeasyPrint lays a page out with Pango and HarfBuzz like a browser prints it: headings, tables, bold +// and Arabic or Hindi shaping come out as on screen, where the built-in route (HWPX writer → rhwp) loses them. +// +// Its one edge, HTML → PDF, serves routes that start from an HTML page, Markdown (Markdig writes the page), an e-mail +// (EML, MSG) or a saved web page (MHTML), and no other (ConversionEdge.Sources): a Word document or an e-book keeps its +// route instead of going through a page. It costs so little that WeasyPrint wins wherever it is installed, even after +// the e-mail step and in last place of the engine order (1 + 9 against 31 for the built-in route); without it nothing +// changes. It is found only as Filee's download or a copy the user picked, never on the PATH. +// +// Locked down: WeasyPrint gets a copy of the page whose pictures inside the document's folder are data: URIs and that +// links to nothing else (LinkedContent: style sheets, @import, CSS url(), frames, objects, taken out), and it +// runs with --allowed-protocols data, so whatever that copy still names (a picture by a relative or absolute path, a +// file: or https: address, an attachment) is refused by WeasyPrint itself and never read or fetched. + +using System.Globalization; +using System.Text.RegularExpressions; +using Filee.Core.Conversion; +using Filee.Engines.Infrastructure; +using Filee.Engines.Office; + +namespace Filee.Engines.Text; + +/// Conversions through WeasyPrint's command line program. +public sealed partial class WeasyPrintConverter : IConverter +{ + /// Formats whose routes may end with WeasyPrint's HTML → PDF: all of them reach PDF through a page. + internal static readonly string[] Sources = ["html", "md", "eml", "msg", "mhtml"]; + + /// + /// Cost of HTML → PDF. The built-in route costs 31 in the default engine order (HWPX writer 10 + 8, rhwp 10 + 3) and + /// LibreOffice 28 + 9. WeasyPrint at the end of the order costs 1 + 9, after Markdown → HTML (4 + 5) 19, after + /// MHTML or an e-mail → HTML (10 + 9) 29: always cheaper. Its source list keeps it out of every other route. + /// + internal const int PdfCost = 1; + + /// + /// With --verbose WeasyPrint prints each step and each page it lays out ("Step 5 - Creating layout - Page 12"). Only + /// this long without a line means it is stuck. + /// + internal static readonly TimeSpan StallTimeout = TimeSpan.FromMinutes(5); + + /// The longest a page may take even while WeasyPrint keeps printing. + internal static readonly TimeSpan MaxDuration = TimeSpan.FromMinutes(30); + + private string? _executable; + + public string Id => "weasyprint"; + public string DisplayName => "WeasyPrint"; + + /// One WeasyPrint is a Python process with the whole page in memory; two at a time keep the PC responsive. + public int MaxParallelism => 2; + + public IReadOnlyList Edges { get; } = [new("html", "pdf", PdfCost, Sources: Sources)]; + + public EngineStatus GetStatus() + { + _executable = Locate(); + // Filee has no WeasyPrint download for Linux and macOS yet (#1403): "not installed" would point to nothing. + return _executable is null + ? EngineStatus.Unavailable(OperatingSystem.IsWindows() ? "engine.reason.not_installed" : "engine.reason.unsupported_platform") + : EngineStatus.Available(_executable, EngineEnvironment.UsesFileesCopy("weasyprint") ? EngineVersions.Component("weasyprint") : null); + } + + public async Task> ConvertAsync(ConversionStep step, IProgress? progress, CancellationToken cancellationToken) + { + var executable = _executable ?? Locate() ?? throw new InvalidOperationException("WeasyPrint is not installed."); + var work = Directory.CreateDirectory(Path.Combine(step.WorkDirectory, "weasyprint-" + Guid.NewGuid().ToString("N")[..8])).FullName; + var page = PreparePage(step.InputPath, work, step.OriginalSourcePath); + var temp = Path.Combine(work, "output.pdf"); + + // For a WeasyPrint installed with pip. The onedir weasyprint.exe ignores both (PyInstaller starts Python with an + // isolated configuration); its progress still arrives line by line, as WeasyPrint logs to stderr, which stays + // line-buffered. + var environment = new Dictionary + { + ["PYTHONIOENCODING"] = "utf-8", + ["PYTHONUNBUFFERED"] = "1", + }; + progress?.Report(0.02); + var result = await ProcessRunner.RunAsync(executable, Arguments(page, temp), MaxDuration, cancellationToken, + workingDirectory: work, environment: environment, onOutput: line => ReportProgress(line, progress), + stallTimeout: StallTimeout); + if (result.ExitCode != 0 || !File.Exists(temp)) + throw new InvalidOperationException($"WeasyPrint failed (exit {result.ExitCode}). {LastLines(result.StandardError.Trim().Length > 0 ? result.StandardError : result.StandardOutput)}"); + + var output = step.Output.Allocate("pdf"); + if (output is null) + return []; + File.Move(temp, output, overwrite: true); + progress?.Report(1); + return [output]; + } + + /// + /// WeasyPrint's command line: only data: URIs may be loaded, HTML attributes such as width and bgcolor count (e-mails + /// use them), and progress lines for the stall watch. + /// + internal static string[] Arguments(string page, string pdf) => + ["--allowed-protocols", "data", "--presentational-hints", "--verbose", page, pdf]; + + /// + /// The page WeasyPrint gets, in : with the pictures the document may + /// use (inside its folder, or the folder of for a page made from it) as data: URIs + /// and every other address of a picture, frame, object or style sheet taken out (). + /// + internal static string PreparePage(string input, string work, string? originalSource) + { + var page = Path.Combine(work, "page.html"); + var html = File.ReadAllBytes(input); + File.WriteAllBytes(page, LinkedContent.StripHtml(html, LinkedContent.PagePictures(input, originalSource, svg: true)) ?? html); + return page; + } + + /// WeasyPrint prints "INFO: Step 5 - Creating layout - Page 3" while it works: seven steps. + private static void ReportProgress(string line, IProgress? progress) + { + if (progress is not null && StepLine().Match(line) is { Success: true } match + && int.TryParse(match.Groups[1].Value, NumberStyles.None, CultureInfo.InvariantCulture, out var number)) + progress.Report(Math.Clamp(number, 1, 7) / 7.0 * 0.95); + } + + /// The end of WeasyPrint's output, where its error message (or Python traceback) is. + private static string LastLines(string output) + { + var lines = output.Split('\n', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries); + return string.Join(" ", lines.TakeLast(3)); + } + + [GeneratedRegex(@"Step (\d) - ")] + private static partial Regex StepLine(); + + /// + /// The user's own WeasyPrint when they chose one in Settings → Engines, else Filee's (engines/weasyprint, downloaded + /// on demand). Never one on the PATH or elsewhere on the PC (#1403). A copy the user picked that is too old to know + /// --allowed-protocols fails with a usage error instead of running unlocked. + /// + internal static string? Locate() => + EngineEnvironment.OwnProgram("weasyprint", "weasyprint.exe") + ?? EngineEnvironment.BundledProgram("weasyprint", "weasyprint.exe"); +} diff --git a/tests/Filee.Engines.Tests/HtmlReaderTests.cs b/tests/Filee.Engines.Tests/HtmlReaderTests.cs index c409c57b3..8bc785469 100644 --- a/tests/Filee.Engines.Tests/HtmlReaderTests.cs +++ b/tests/Filee.Engines.Tests/HtmlReaderTests.cs @@ -14,7 +14,7 @@ namespace Filee.Engines.Tests; public class HtmlReaderTests(EngineFixture fx) : IClassFixture { /// Only engines that ship with the app (or run in-process). - private static readonly string[] OptionalEngines = ["pandoc", "libreoffice", "calibre"]; + private static readonly string[] OptionalEngines = ["pandoc", "libreoffice", "calibre", "weasyprint"]; private HtmlContent Parse(string html, string? folder = null) { diff --git a/tests/Filee.Engines.Tests/LibreOfficeLinkedContentTests.cs b/tests/Filee.Engines.Tests/LibreOfficeLinkedContentTests.cs index b744e58cc..d04f131be 100644 --- a/tests/Filee.Engines.Tests/LibreOfficeLinkedContentTests.cs +++ b/tests/Filee.Engines.Tests/LibreOfficeLinkedContentTests.cs @@ -177,6 +177,27 @@ public void Html_keeps_only_data_pictures_and_hyperlinks() Assert.Contains("alt=\"a\"", text); } + [Fact] + public void Html_references_to_the_pages_own_elements_stay_in_the_copy() + { + var folder = fx.NewFolder(); + var html = Path.Combine(folder, "page.html"); + File.WriteAllText(html, """ + + + + """); + + var text = File.ReadAllText(LinkedContent.Strip(html, "html", folder)!); + + Assert.Contains("", text); + Assert.Contains("xlink:href=\"#base\"", text); + Assert.Contains(".a { fill: url(#g) }", text); + Assert.Contains("style=\"fill: url(#g)\"", text); + foreach (var gone in new[] { "outside", "other.svg" }) + Assert.DoesNotContain(gone, text); + } + [Fact] public void Html_pictures_inside_the_page_folder_go_into_the_copy() { diff --git a/tests/Filee.Engines.Tests/LibreOfficeRouteTests.cs b/tests/Filee.Engines.Tests/LibreOfficeRouteTests.cs index 99c4eccbc..cda63afa0 100644 --- a/tests/Filee.Engines.Tests/LibreOfficeRouteTests.cs +++ b/tests/Filee.Engines.Tests/LibreOfficeRouteTests.cs @@ -13,7 +13,8 @@ public class LibreOfficeRouteTests(EngineFixture fx) : IClassFixture c.Id != "libreoffice").Append(libreOffice).ToList(); + // WeasyPrint, which takes HTML, Markdown and e-mails to PDF when installed, has its own tests (WeasyPrintTests). + var converters = fx.Converters.Where(c => c.Id is not ("libreoffice" or "weasyprint")).Append(libreOffice).ToList(); var priority = new AppSettings().EnginePriority; return new RoutePlanner(converters, c => Math.Min(priority.IndexOf(c.Id) is >= 0 and var i ? i : priority.Count, 9)); } diff --git a/tests/Filee.Engines.Tests/MarkdownTests.cs b/tests/Filee.Engines.Tests/MarkdownTests.cs index cd9ccd9c7..d6aba1b01 100644 --- a/tests/Filee.Engines.Tests/MarkdownTests.cs +++ b/tests/Filee.Engines.Tests/MarkdownTests.cs @@ -39,7 +39,7 @@ public class MarkdownTests(EngineFixture fx) : IClassFixture """; /// Only engines that ship with the app (or run in-process): no Pandoc or LibreOffice. - private static readonly string[] OptionalEngines = ["pandoc", "libreoffice"]; + private static readonly string[] OptionalEngines = ["pandoc", "libreoffice", "weasyprint"]; private RoutePlanner BuiltInPlanner(params string[] assumeInstalled) => new ConverterCatalog(fx.Converters.Where(c => !OptionalEngines.Contains(c.Id))) { Priority = fx.Catalog.Priority } diff --git a/tests/Filee.Engines.Tests/MhtmlTests.cs b/tests/Filee.Engines.Tests/MhtmlTests.cs index d548e64bb..a5634644b 100644 --- a/tests/Filee.Engines.Tests/MhtmlTests.cs +++ b/tests/Filee.Engines.Tests/MhtmlTests.cs @@ -17,7 +17,7 @@ public class MhtmlTests(EngineFixture fx) : IClassFixture { static MhtmlTests() => Encoding.RegisterProvider(CodePagesEncodingProvider.Instance); - private static readonly string[] OptionalEngines = ["pandoc", "libreoffice", "calibre"]; + private static readonly string[] OptionalEngines = ["pandoc", "libreoffice", "calibre", "weasyprint"]; private static readonly byte[] Photo = EbookBuilders.Png(40, 30, MagickColors.Teal); private static readonly byte[] Banner = EbookBuilders.Png(50, 10, MagickColors.Orange); diff --git a/tests/Filee.Engines.Tests/OwnCopyTests.cs b/tests/Filee.Engines.Tests/OwnCopyTests.cs index 0d41ce4ea..2f8b7bf73 100644 --- a/tests/Filee.Engines.Tests/OwnCopyTests.cs +++ b/tests/Filee.Engines.Tests/OwnCopyTests.cs @@ -131,6 +131,7 @@ public void LibreOffice_is_found_in_the_folder_its_installer_recorded() [InlineData("ffmpeg", "ffmpeg version 2025-06-02-git-688f3944ce-full_build", null)] [InlineData("pandoc", "pandoc.exe 3.6.4\nFeatures: +server +lua", "3.6")] [InlineData("calibre", "ebook-convert.exe (calibre 8.5.0)\nCreated by: Kovid Goyal", "8.5")] + [InlineData("weasyprint", "WeasyPrint version 70.0", "70.0")] [InlineData("ghostscript", "10.04.0\r\n", "10.4")] [InlineData("libreoffice", "[Version]\nAllLanguages=en-US\nMsiProductVersion=25.2.4.3\n", "25.2")] [InlineData("libreoffice", "LibreOffice 26.2.6.2 98d5f8\n", "26.2")] diff --git a/tests/Filee.Engines.Tests/WeasyPrintTests.cs b/tests/Filee.Engines.Tests/WeasyPrintTests.cs new file mode 100644 index 000000000..1d9d11f88 --- /dev/null +++ b/tests/Filee.Engines.Tests/WeasyPrintTests.cs @@ -0,0 +1,446 @@ +// WeasyPrint (#1403): its routes (HTML, Markdown, e-mails → PDF only, and only when it is installed), the page Filee +// hands it (pictures beside the document as data: URIs, nothing else it could load), and, with WeasyPrint installed, +// the PDFs: a style sheet by file: URL, a picture by absolute path, one by relative path outside the folder and one +// by https: address are never loaded, neither from Filee's page nor when WeasyPrint gets the page as it is. +// +// Every page sits in "inbox" with a picture in "inbox/img" (4×4, kept); "private" next to it holds a picture (6×6) +// and two style sheets that must never be read. The https: address is a listener on this computer that counts +// connections, so nothing goes to the internet. + +using System.Net; +using System.Net.Sockets; +using System.Text; +using System.Text.RegularExpressions; +using Filee.Core.Conversion; +using Filee.Core.Formats; +using Filee.Core.Presets; +using Filee.Engines.Infrastructure; +using Filee.Engines.Text; +using ImageMagick; +using UglyToad.PdfPig; + +namespace Filee.Engines.Tests; + +public sealed partial class WeasyPrintTests(EngineFixture fx) : IClassFixture +{ + /// The engines the route tests count as installed besides WeasyPrint, so routes don't depend on this PC. + private static readonly string[] Others = ["rhwp", "pandoc", "libreoffice", "calibre"]; + + private bool HasWeasyPrint => fx.Catalog.StatusOf(fx.Converters.Single(c => c.Id == "weasyprint")).IsAvailable; + + private const string NotInstalled = "WeasyPrint not found (pwsh build/fetch-engines.ps1 -Only weasyprint)"; + + private static IEnumerable Ids(Route? route) => route?.Steps.Select(s => s.Converter.Id) ?? []; + + private RoutePlanner WithoutWeasyPrint() => + new ConverterCatalog(fx.Converters.Where(c => c.Id != "weasyprint")) { Priority = fx.Catalog.Priority }.CreatePlanner(Others); + + // ───────────────────────── Routes ───────────────────────── + + [Fact] + public void WeasyPrint_makes_the_pdfs_of_pages_markdown_and_e_mails() + { + var planner = fx.Catalog.CreatePlanner([.. Others, "weasyprint"]); + + Assert.Equal(["weasyprint"], Ids(planner.Plan("html", "pdf"))); + Assert.Equal(["markdown", "weasyprint"], Ids(planner.Plan("md", "pdf"))); + foreach (var from in new[] { "eml", "msg", "mhtml" }) + Assert.Equal(["email", "weasyprint"], Ids(planner.Plan(from, "pdf"))); + Assert.Equal(["weasyprint", "pdfium"], Ids(planner.Plan("html", "png"))); + // It shapes text, so a page in Arabic or Hindi takes it too. + Assert.Equal(["weasyprint"], Ids(planner.Plan("html", "pdf", shaping: true))); + Assert.Equal(["markdown", "weasyprint"], Ids(planner.Plan("md", "pdf", shaping: true))); + } + + [Fact] + public void WeasyPrint_wins_wherever_it_is_in_the_engine_order() + { + foreach (var index in new[] { 0, fx.Catalog.Priority.Count }) + { + var order = fx.Catalog.Priority.Where(id => id != "weasyprint").ToList(); + order.Insert(Math.Min(index, order.Count), "weasyprint"); + var planner = new ConverterCatalog(fx.Converters) { Priority = order }.CreatePlanner([.. Others, "weasyprint"]); + foreach (var from in WeasyPrintConverter.Sources) + Assert.Equal("weasyprint", Ids(planner.Plan(from, "pdf")).Last()); + } + } + + [Fact] + public void Every_other_route_is_the_same_with_or_without_WeasyPrint() + { + var with = fx.Catalog.CreatePlanner([.. Others, "weasyprint"]); + var without = WithoutWeasyPrint(); + + // A Word document, an e-book, a PDF, ... never goes through a page to reach WeasyPrint. + foreach (var from in FormatRegistry.Known.Select(f => f.Id).Where(id => !WeasyPrintConverter.Sources.Contains(id))) + foreach (var to in new[] { "pdf", "png", "jpg", "docx", "hwpx", "txt", "html" }) + Assert.True(Ids(without.Plan(from, to)).SequenceEqual(Ids(with.Plan(from, to))), $"{from} → {to}"); + // Pages, Markdown and e-mails keep their routes to everything but PDF and the pictures made from it. + foreach (var from in WeasyPrintConverter.Sources) + foreach (var to in new[] { "docx", "hwpx", "txt", "epub", "odt", "html", "md" }) + Assert.True(Ids(without.Plan(from, to)).SequenceEqual(Ids(with.Plan(from, to))), $"{from} → {to}"); + } + + [Fact] + public void Without_WeasyPrint_no_route_takes_it() + { + var planner = WithoutWeasyPrint(); + foreach (var from in WeasyPrintConverter.Sources) + Assert.DoesNotContain("weasyprint", Ids(planner.Plan(from, "pdf"))); + // The edge itself is held to its sources. + var edge = Assert.Single(new WeasyPrintConverter().Edges); + Assert.Equal(("html", "pdf"), (edge.From, edge.To)); + Assert.Equal(WeasyPrintConverter.Sources, edge.Sources); + } + + [Fact] + public void A_source_list_keeps_an_edge_out_of_routes_from_other_formats() + { + var planner = new RoutePlanner([new FakeConverter("a", [new("x", "y")]), new FakeConverter("b", [new("y", "z", Sources: ["y"])])]); + Assert.Null(planner.Plan("x", "z")); + Assert.Equal(["b"], Ids(planner.Plan("y", "z"))); + } + + private sealed class FakeConverter(string id, IReadOnlyList edges) : IConverter + { + public string Id => id; + public string DisplayName => id; + public IReadOnlyList Edges => edges; + public int MaxParallelism => 1; + public EngineStatus GetStatus() => EngineStatus.Available(); + public Task> ConvertAsync(ConversionStep step, IProgress? progress, CancellationToken cancellationToken) => + throw new NotSupportedException(); + } + + [Fact] + public void WeasyPrint_is_a_windows_download_found_only_in_fileees_engine_folders() + { + var component = EngineDownloads.ComponentsForRuntime("win-x64")["weasyprint"]; + Assert.Equal("70.0", component.Version); + Assert.EndsWith("/v70.0/weasyprint-windows-onedir.zip", component.Url, StringComparison.Ordinal); + Assert.Equal("zip", component.Kind); + foreach (var runtime in new[] { "linux-x64", "linux-arm64", "osx-x64", "osx-arm64" }) + Assert.False(EngineDownloads.ComponentsForRuntime(runtime).ContainsKey("weasyprint"), runtime); + Assert.Equal(OperatingSystem.IsWindows(), EngineDownloads.Packages.Any(p => p.Id == "weasyprint")); + // Offered, not ticked by default: it changes how pages become PDFs. + Assert.False(EngineDownloads.IsSuggested(new EnginePackage("weasyprint", ["weasyprint"], 65_000_000, ["weasyprint"]))); + Assert.Equal(["--allowed-protocols", "data"], WeasyPrintConverter.Arguments("page.html", "out.pdf")[..2]); + } + + [Fact] + public void The_onedir_zip_keeps_the_program_folder_and_its_licence() + { + var staging = fx.NewFolder(); + var folder = Directory.CreateDirectory(Path.Combine(staging, "onedir", "weasyprint")).FullName; + File.WriteAllText(Path.Combine(folder, EngineEnvironment.ProgramName("weasyprint.exe")), ""); + Directory.CreateDirectory(Path.Combine(folder, "_internal")); + File.WriteAllText(Path.Combine(staging, "LICENSE"), "BSD 3-Clause License"); + File.WriteAllText(Path.Combine(staging, "README.rst"), "WeasyPrint"); + + Assert.Equal(folder, EngineInstaller.WeasyPrintFolder(staging)); + Assert.True(File.Exists(Path.Combine(folder, "LICENSE"))); + Assert.Equal(folder, EngineEnvironment.FolderWithPrograms("weasyprint", folder)); + } + + // ───────────────────────── The page WeasyPrint gets (no WeasyPrint needed) ───────────────────────── + + /// "inbox/page.html" (or another document there) with "inbox/img/inside.png" and the files in "private". + private sealed record Folders(string Inbox, string Inside, string Secret, string Css, string ImportedCss); + + private Folders Make() + { + var root = fx.NewFolder(); + var inbox = Path.Combine(root, "inbox"); + var inside = Path.Combine(inbox, "img", "inside.png"); + var secret = Path.Combine(root, "private", "secret.png"); + Directory.CreateDirectory(Path.GetDirectoryName(inside)!); + Directory.CreateDirectory(Path.GetDirectoryName(secret)!); + File.WriteAllBytes(inside, EbookBuilders.Png(4, 4, MagickColors.Teal)); + File.WriteAllBytes(secret, EbookBuilders.Png(6, 6, MagickColors.Red)); + var css = Path.Combine(root, "private", "linked.css"); + File.WriteAllText(css, "h1::after { content: \"LINKEDSTYLE\"; }"); + var imported = Path.Combine(root, "private", "imported.css"); + File.WriteAllText(imported, "h1::before { content: \"IMPORTEDSTYLE\"; }"); + return new Folders(inbox, inside, secret, css, imported); + } + + /// + /// A page with the four references WeasyPrint must not load (a file: style sheet, a picture by absolute path, one by + /// relative path outside the folder, one by https: address), an @import and a CSS url(), the picture beside it, + /// and . + /// + private static string Page(Folders folders, int port, string extra = "") + { + var page = Path.Combine(folders.Inbox, "page.html"); + File.WriteAllText(page, $$""" + + Report + + + +

Quarterly report

+

kept

+

absolute

+

relative

+

remote

+ {{extra}} + + """, new UTF8Encoding(false)); + return page; + } + + /// The pictures of the page's data: URIs, decoded: their sizes. + private static List<(uint Width, uint Height)> DataPictures(string html) => + DataUri().Matches(html).Select(m => + { + using var image = new MagickImage(Convert.FromBase64String(m.Groups[1].Value)); + return (image.Width, image.Height); + }).ToList(); + + private static void AssertNothingLoadable(string html, Folders folders) + { + foreach (var gone in new[] { "file:", "https:", "127.0.0.1", "../", "secret.png", "linked.css", "imported.css", "@import", folders.Secret }) + Assert.DoesNotContain(gone, html, StringComparison.OrdinalIgnoreCase); + // Every address left is a data: URI or names an element of the page (a stays, without its href). + Assert.All(Address().Matches(html), + m => Assert.True(m.Groups[1].Value.StartsWith("data:", StringComparison.OrdinalIgnoreCase) || m.Groups[1].Value.StartsWith('#'), m.Value)); + } + + [Fact] + public void The_page_WeasyPrint_gets_holds_only_the_picture_beside_it_as_a_data_uri() + { + var folders = Make(); + var page = Page(folders, port: 9, extra: """

styled

"""); + + var html = File.ReadAllText(WeasyPrintConverter.PreparePage(page, fx.NewFolder(), page)); + + Assert.Equal([(4u, 4u)], DataPictures(html)); + AssertNothingLoadable(html, folders); + Assert.Contains("Quarterly report", html, StringComparison.Ordinal); + Assert.True(File.Exists(folders.Secret)); // the user's files are untouched + } + + [Fact] + public void The_page_of_a_markdown_file_holds_only_the_picture_beside_the_markdown() + { + var folders = Make(); + var markdown = Path.Combine(folders.Inbox, "notes.md"); + File.WriteAllText(markdown, $"# Notes\n\n![kept](img/inside.png)\n\n![absolute](<{folders.Secret}>)\n\n![relative](../private/secret.png)\n\n![remote](https://127.0.0.1:9/pixel.png)\n"); + // The Markdown step writes its page into the job's folder, linking the pictures by file: URL (MarkdownConverter). + var step = Path.Combine(fx.NewFolder(), "notes.html"); + File.WriteAllText(step, MarkdownConverter.ToHtml(File.ReadAllText(markdown), markdown, step)); + Assert.Contains("file:", File.ReadAllText(step), StringComparison.Ordinal); + + var html = File.ReadAllText(WeasyPrintConverter.PreparePage(step, fx.NewFolder(), markdown)); + + Assert.Equal([(4u, 4u)], DataPictures(html)); + AssertNothingLoadable(html, folders); + } + + [Fact] + public void A_picture_svg_beside_the_page_is_embedded_too() + { + var folders = Make(); + File.WriteAllText(Path.Combine(folders.Inbox, "img", "chart.svg"), """"""); + var page = Path.Combine(folders.Inbox, "chart.html"); + File.WriteAllText(page, """"""); + + var html = File.ReadAllText(WeasyPrintConverter.PreparePage(page, fx.NewFolder(), page)); + + Assert.Contains("src=\"data:image/svg+xml;base64,", html, StringComparison.Ordinal); + } + + /// An inline SVG whose parts refer to each other: a symbol used twice, a gradient built on another, url(#…) fills. + private const string InlineSvg = """ + + + + USEDSYMBOL + + + """; + + [Fact] + public void References_to_the_pages_own_elements_stay() + { + var folders = Make(); + var page = Page(folders, port: 9, extra: InlineSvg); + + var html = File.ReadAllText(WeasyPrintConverter.PreparePage(page, fx.NewFolder(), page)); + + Assert.Contains("", html, StringComparison.Ordinal); + Assert.Contains("A listener on this computer: the https: pictures point at it, and it counts who connects.
+ private sealed class Listener : IDisposable + { + private readonly TcpListener _listener = new(IPAddress.Loopback, 0); + + public Listener() => _listener.Start(); + + public int Port => ((IPEndPoint)_listener.LocalEndpoint).Port; + + /// True once anything connected. + public bool Contacted => _listener.Pending(); + + public void Dispose() => _listener.Stop(); + } + + private static List<(int Width, int Height)> PdfPictures(string pdf) + { + using var document = PdfDocument.Open(pdf); + return document.GetPages().SelectMany(p => p.GetImages()).Select(i => (i.WidthInSamples, i.HeightInSamples)).ToList(); + } + + private static string PdfText(string pdf) + { + using var document = PdfDocument.Open(pdf); + return string.Concat(document.GetPages().Select(p => p.Text)); + } + + private async Task ToPdfAsync(string input) + { + var job = await fx.ConvertAsync([input], new Preset { TargetFormat = "pdf" }); + Assert.True(job.State == JobState.Completed, string.Join("; ", job.Files.Select(f => $"{f.ErrorKey} {f.ErrorDetail}"))); + return job.Outputs.Single(); + } + + [Fact] + public async Task A_page_becomes_a_pdf_with_only_the_picture_beside_it() + { + Assert.SkipUnless(HasWeasyPrint, NotInstalled); + var folders = Make(); + using var listener = new Listener(); + var page = Page(folders, listener.Port); + Assert.Equal(["weasyprint"], Ids(fx.Catalog.CreatePlanner().Plan("html", "pdf"))); + + var pdf = await ToPdfAsync(page); + + Assert.Equal([(4, 4)], PdfPictures(pdf)); + var text = PdfText(pdf); + Assert.Contains("Quarterly report", text, StringComparison.Ordinal); + Assert.DoesNotContain("LINKEDSTYLE", text, StringComparison.Ordinal); + Assert.DoesNotContain("IMPORTEDSTYLE", text, StringComparison.Ordinal); + Assert.False(listener.Contacted); + } + + [Fact] + public async Task WeasyPrint_itself_refuses_everything_but_data_uris() + { + Assert.SkipUnless(HasWeasyPrint, NotInstalled); + var folders = Make(); + using var listener = new Listener(); + // The page as it is, without Filee's copy: the lock holds by itself. Only the 3×3 data: picture loads, not even + // the picture beside the page, which Filee would have embedded. + var dataPicture = Convert.ToBase64String(EbookBuilders.Png(3, 3, MagickColors.Navy)); + var page = Page(folders, listener.Port, $"""

"""); + var pdf = Path.Combine(fx.NewFolder(), "raw.pdf"); + + var result = await ProcessRunner.RunAsync(WeasyPrintConverter.Locate()!, WeasyPrintConverter.Arguments(page, pdf), + TimeSpan.FromMinutes(3), TestContext.Current.CancellationToken, workingDirectory: folders.Inbox); + + Assert.Equal(0, result.ExitCode); + Assert.Equal([(3, 3)], PdfPictures(pdf)); + var text = PdfText(pdf); + Assert.DoesNotContain("LINKEDSTYLE", text, StringComparison.Ordinal); + Assert.DoesNotContain("IMPORTEDSTYLE", text, StringComparison.Ordinal); + Assert.False(listener.Contacted); + Assert.Contains("disallowed protocol", result.StandardError, StringComparison.OrdinalIgnoreCase); + } + + [Fact] + public async Task An_inline_svg_draws_the_symbol_it_uses() + { + Assert.SkipUnless(HasWeasyPrint, NotInstalled); + var folders = Make(); + using var listener = new Listener(); + + var pdf = await ToPdfAsync(Page(folders, listener.Port, InlineSvg)); + + Assert.Contains("USEDSYMBOL", PdfText(pdf), StringComparison.Ordinal); + Assert.Equal([(4, 4)], PdfPictures(pdf)); + Assert.False(listener.Contacted); + } + + [Fact] + public async Task Markdown_becomes_a_pdf_with_its_table_and_only_the_picture_beside_it() + { + Assert.SkipUnless(HasWeasyPrint, NotInstalled); + var folders = Make(); + using var listener = new Listener(); + var markdown = Path.Combine(folders.Inbox, "notes.md"); + File.WriteAllText(markdown, $""" + # Release notes + + Some **bold** text. + + | Name | Value | + |------|-------| + | Alpha | 42 | + + ![kept](img/inside.png) + + ![absolute](<{folders.Secret}>) + + ![relative](../private/secret.png) + + ![remote](https://127.0.0.1:{listener.Port}/pixel.png) + + """); + Assert.Equal(["markdown", "weasyprint"], Ids(fx.Catalog.CreatePlanner().Plan("md", "pdf"))); + + var pdf = await ToPdfAsync(markdown); + + Assert.Equal([(4, 4)], PdfPictures(pdf)); + var text = PdfText(pdf); + Assert.Contains("Release notes", text, StringComparison.Ordinal); + Assert.Contains("Alpha", text, StringComparison.Ordinal); + Assert.Contains("42", text, StringComparison.Ordinal); + Assert.False(listener.Contacted); + } + + [Fact] + public async Task An_e_mail_becomes_a_pdf_with_its_inline_picture_and_nothing_it_links_to() + { + Assert.SkipUnless(HasWeasyPrint, NotInstalled); + var folders = Make(); + using var listener = new Listener(); + var inline = Convert.ToBase64String(EbookBuilders.Png(16, 16, MagickColors.Teal)); + var html = $"""

Hello from the mail

"""; + var eml = Path.Combine(folders.Inbox, "mail.eml"); + File.WriteAllText(eml, string.Join("\r\n", + "From: sender@example.com", "To: receiver@example.com", "Subject: Report", "Date: Thu, 01 Oct 2026 09:30:00 +0900", + "MIME-Version: 1.0", "Content-Type: multipart/related; boundary=\"related\"", "", + "--related", "Content-Type: text/html; charset=utf-8", "", html, + "--related", "Content-Type: image/png", "Content-Transfer-Encoding: base64", "Content-ID: ", "", inline, + "--related--", "")); + Assert.Equal(["email", "weasyprint"], Ids(fx.Catalog.CreatePlanner().Plan("eml", "pdf"))); + + var pdf = await ToPdfAsync(eml); + + Assert.Equal([(16, 16)], PdfPictures(pdf)); + Assert.Contains("Hello from the mail", PdfText(pdf), StringComparison.Ordinal); + Assert.False(listener.Contacted); + } +}