# Conversion engines Filee chooses engines automatically. Settings → *Engines* shows their status, lets you change the priority (higher wins when two engines can do the same conversion), and install or remove the optional engines. | Engine | How it gets there | Handles | |---|---|---| | ImageMagick (Magick.NET) | built in (library) | JPG, PNG, WEBP, TIFF, BMP, GIF, ICO, AVIF, JPEG XL, JPEG 2000, PSD/PSB, TGA, PPM, MNG, QOI, DDS, PCX, CUR, XPM, XBM, WBMP, PAM, PFM, SGI, Sun Raster, MIFF ↔ each other, and **APNG** (Filee's own reader and writer); HEIC (written with FFmpeg), XCF, camera RAW (LibRaw), DICOM (uncompressed ones by Filee's own reader), **FITS** (Filee's own reader; TIFF at the file's own depth by Filee's own writer), Photo CD, OpenEXR, Radiance HDR, EMF/WMF (Windows), the photo of an iPhone LIVP, the merged picture of a Krita KRA, a **Paint.NET PDN** (Filee's own reader and writer: layers merged as Paint.NET shows them, or kept between PDN and PSD/PSB), the icon of a Windows program or library (EXE, DLL) and a Windows **animated cursor** (ANI, Filee's own reader) read | | Windows Imaging Component | part of Windows (Windows only) | **JPEG XR** (JXR, WDP, HDP; HDR screenshots of Windows) → the images ImageMagick writes, and PDF, ICNS, HEIC and SVG through PNG; HDR tone-mapped to SDR | | Vector graphics | built in (Svg.Skia, SkiaSharp, ACadSharp) | **SVG/SVGZ → PDF (vector) and images**, SVG ↔ SVGZ, **SVG → DXF** (→ DWG with the CAD engine), **raster images → SVG (tracing)**, PDF-compatible AI → PDF, **HP-GL / HP-GL/2 plot files (PLT) → SVG**, PDF and images (pen moves, packed PE polylines, arcs and filled polygons; text labels are not drawn), **Tajima DST embroidery → SVG** (stitches as lines, a palette colour per thread; → PNG, JPG, PDF from there) | | ICNS | built in | images → ICNS (16–1024 px), ICNS → images | | PDFsharp | built in (library) | images → PDF (CMYK pictures stay CMYK), merge, split, page ranges, page organising, lossless rotate and crop-box edits, pages per sheet (N-up), spreadsheet print-studio assembly, **compression** (smaller PDFs, see below), PDFs without their metadata | | PDFium | built in (library) | PDF → images (multi-page TIFF/GIF supported); CMYK pages → CMYK JPG/TIFF through Ghostscript when it is installed | | Unhwp | built in (library) | HWP/HWPX → TXT, Markdown, HTML (pictures in a `_files` folder next to the Markdown, embedded in the HTML) | | Text & Markdown (Markdig) | built in (library) | **Markdown → HTML, TXT**; **TXT, LOG, PY, PAC, BAT, CMD, COMMAND, LUA, CSS, PS1, SQL, AHK, INI, RSC, C, H, CPP, CC, CXX, HPP, HH, HXX, CS, DART, HQL, DBML, JAVA, INO, PDE → UTF-8 TXT**, preserving source text without running scripts (and → PDF / DOCX through the HWPX writer, rhwp or LibreOffice) | | Spreadsheets (ExcelDataReader for XLS) | built in (library) | **XLSX, XLS, ODS, CSV, TSV → XLSX, ODS, CSV, TSV** (one CSV / TSV per sheet) | | Office Open XML | built in | **DOCM / DOTX / DOTM ↔ DOCX, XLSM / XLTX ↔ XLSX, PPTM / POTX / POTM / PPSX ↔ PPTX** (macros removed for macro-free types) | | Fonts | built in | **TTF, OTF, WOFF, WOFF2, EOT ↔ each other**; **FON (NE monochrome FNT v2/v3, supported single-byte charsets) → TTF, OTF, WOFF, WOFF2, EOT only**; CFF (PostScript) outlines become TrueType for TTF and EOT | | **HWPX writer** | built in | **DOCX (+ DOCM/DOTX/DOTM), XLSX (+ XLSM/XLTX), XLS, ODS, CSV, TSV, PPTX (+ PPTM/POTX/POTM/PPSX), PDF, OFD, HTML, MHTML, EPUB, MOBI/AZW3, FB2, HWPX, TXT, Markdown → HWPX** (and with rhwp → PDF and images); ODT, RTF, reStructuredText, LaTeX → HWPX with Pandoc | | **DOCX writer** | built in | **HWPX (and HWP through rhwp), PDF, OFD, HTML, MHTML, EPUB, MOBI/AZW3, FB2, Markdown, TXT, XLSX, XLS, ODS, CSV, PPTX (+ variants) → DOCX** | | PDF text | built in (PdfPig) | **PDF → TXT, Markdown** (reading order, also two columns; Markdown keeps headings, emphasis, links and the pictures; no OCR) | | PDF slides | built in (PDFium, PdfPig) | **PDF → PPTX**: a picture of each page on its own slide (looks like the PDF, not editable), the page's text in the speaker notes | | PDF tables | built in (PdfPig) | **PDF → XLSX, ODS, CSV** (#267): each table drawn with lines on its own sheet, numbers and dates as real values, merged cells and column widths kept | | Document tables | built in | **DOCX (+ DOCM/DOTX/DOTM), HTML, MHTML → XLSX, ODS, CSV** (#1016): each table on its own sheet, numbers and dates as real values, merged cells, borders and column widths kept | | E-mail | built in (MimeKit, MsgReader) | **EML, Outlook MSG → HTML** (headers + body, inline pictures), **TXT**, **ZIP** (the attachments); **MSG → EML**; **MHTML → HTML** (the saved page in one file, pictures inlined; on to PDF, DOCX, HWPX, TXT, Markdown, EPUB from there) | | Signed files | built in (System.Security.Cryptography.Pkcs) | **P7M → the signed file inside** (CMS / PKCS #7, CAdES: Italian e-invoices `*.xml.p7m`, signed PDFs `*.pdf.p7m`), then on from its format (P7M → PDF → JPG); the signature isn't checked | | Data | built in (System.Text.Json, YamlDotNet, Tomlyn, System.Xml) | **JSON, JSON Lines, YAML, TOML, XML, CSV ↔ each other** (CSV for tables); **vCard, iCalendar ↔ CSV**; **property lists (.plist, XML and binary) → each of them and TXT**; FreeMASTER oscilloscope TXT → CSV; spreadsheets, contacts and events reach JSON / YAML / TOML / XML through CSV | | **E-books** | built in | **EPUB, MOBI/AZW/AZW3/PRC, FB2, HTMLZ, TXTZ → EPUB, HWPX (→ PDF), TXT, HTML, Markdown, FB2, HTMLZ, TXTZ**; HTML, Markdown, TXT, DOCX → EPUB/FB2/HTMLZ/TXTZ; HTML → TXT; comics CBZ/CBR/CB7/CBT/CBC → PDF, EPUB, CBZ; PDF → CBZ; AZW4 → PDF; **EPUB ↔ KEPUB** (Kobo) | | CAD (ACadSharp + built-in renderer) | built in (library) | **DWG ↔ DXF**; DWG/DXF → **PDF** (vector), **SVG**, PNG, JPG, WEBP, TIFF, BMP, GIF, ICO, AVIF | | **Subtitles** | built in | **SRT, ASS/SSA, WebVTT, SAMI (SMI), LRC, TTML, MicroDVD, SCC → SRT, ASS, WebVTT, SAMI, LRC, TTML, MicroDVD, TXT**, always UTF-8 (Korean CP949 / EUC-KR files read correctly) | | 3D models (SharpGLTF for glTF) | built in (library) | **GLB, glTF, STL, OBJ (+ MTL), 3MF, PLY ↔ each other**; FBX → each of them; → PNG, JPG, … (an isometric preview) | | Pictures to 3D relief | built in | **PNG, JPG, WEBP, … (every picture ImageMagick reads) → GLB, glTF, STL, OBJ, 3MF, PLY**: a relief, brighter areas higher | | **Geospatial vector data** | built in | **GeoJSON, KML, KMZ, GPX, Shapefile ↔ each other and CSV** (longitude / latitude or WKT); Garmin FIT (activities) and OpenStreetMap XML (`.osm`), both read only, → each of them and CSV; projected Shapefiles (UTM, Web Mercator, Lambert, Albers) are converted to WGS 84; through CSV to XLSX, JSON, … (see below) | | rhwp | bundled with the installer (`engines/rhwp`, with the Microsoft C++ runtime, see below) | HWP/HWPX → PDF, **HWP → HWPX, HWPX → HWP** | | Archives (7-Zip) | bundled with the installer (`engines/7zip`, ~2.5 MB) + built-in readers | ZIP (including ANSYS Workbench WBPZ), 7Z, RAR, TAR (+ GZ/BZ2/XZ/Z/7Z/LZ), CAB, ISO, DMG, split (.001), Minecraft and Modrinth packs, … **→ folder, ZIP, 7Z, TAR, TAR.GZ/BZ2/XZ, the packs**; ALZ, EGG, lzip read in-process; "Compress into one archive" for any files | | LibreOffice + H2Orestart + Java | **optional download** (~420 MB, 1.3 GB on disk) | older and rare formats only: DOC, XLS, PPT, RTF and OpenDocument → PDF and to each other, output as DOCX/ODT/ODS/ODP; HWP/HWPX → DOCX/ODT; read-only formats (see below) → PDF and their kind's editable formats; FODT, OpenDocument templates (OTT, OTS, OTP) and XLT out; SVG → EMF/WMF/ODG, a PDF page → SVG | | Pandoc | **optional download** (~42 MB, 240 MB on disk) | Markdown ↔ DOCX/ODT/RTF, DOCX/ODT/HTML/RTF → Markdown, HTML ↔ DOCX/ODT, markup languages (reStructuredText, LaTeX, Org, AsciiDoc, Typst, MediaWiki, DocBook, Jupyter, Textile, man, OPML) ↔ Markdown/HTML/DOCX/ODT and each other (and → RTF, EPUB, TXT), Markdown and markup → PPTX slides | | Ghostscript | **optional download** (~20 MB, 31 MB on disk) | EPS/PS → PDF, PDF → EPS/PS, PostScript-based AI → PDF (images through PDF and PDFium); smaller PDFs when a preset asks for it; PDF/A-2b for PDF/A presets; renders CMYK PDF/AI pages for CMYK JPG/TIFF | | Tesseract | **optional download** (~50 MB, 160 MB on disk; each language 0.4–10 MB when first used) | **text recognition (OCR)**: scans and pictures → searchable PDF, and their text into DOCX, TXT, Markdown and HWPX ("Recognize text" in the preset, see below) | | 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) | | 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 | DOCX, XLSX, XLS, ODS and PPTX → PDF need neither Microsoft Office nor LibreOffice: they are read in-process, written as HWPX and rendered by rhwp. Anything the built-in readers understand (including PDF) is written as DOCX by the DOCX writer. LibreOffice's and Pandoc's costs are set so the built-in route always wins where one exists. Except for text that needs shaping. rhwp draws one glyph per character from left to right without OpenType shaping, so Arabic and Hebrew come out unjoined and in the wrong order, and Indic vowel signs and conjuncts as separate glyphs with dotted circles (#185 Gujarati, #202 Arabic). Its PDF edges are marked `Unshaped`, and rhwp tells (`ITextShapingCheck`) whether a source has such text: `Infrastructure/ComplexScripts.cs` scans the text parts of the file without reading it into the document model (DOCX `word/document.xml`, headers, footers and notes, PPTX slides, XLSX shared strings and sheets, HWPX sections, OFD pages and templates, EPUB chapters, or a text file with its HTML / 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 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 combining mark is laid out at a fixed half-em. Such a document takes a route without unshaped edges when one exists and is never refused when none does: Filee then gives those letters a letter spacing that takes rhwp's half-em to the real width in the font rhwp draws them with (LATIN for Latin Extended, HANGUL for other scripts; issue #1262), so they don't overlap. Combining marks keep the half-em. Only letters inside words count (an extended Latin letter next to any letter, a letter of another script next to one of its own script), so a symbol or unit on its own (α, Ω, μm in a Korean report) keeps rhwp, and an HWPX stays with rhwp for this (only text that needs shaping sends it elsewhere). Documents in French, German or Spanish (Latin-1 letters only), Chinese, Japanese and Korean keep the built-in route; a binary HWP is not scanned and stays with rhwp. All built-in document readers are registered in one place, `Hwp/Hwpx/DocumentReaders.cs` (format id → reader, and which formats need Pandoc). The HWPX writer and the DOCX writer take their input formats from it: adding a reader there is one line and makes both writers (and every route through them) accept the format. Every engine is built in, bundled with the installer or downloaded into Filee's own folder, so a conversion behaves the same on every PC. An external engine is looked for in this order: 1. a copy the user picked in Settings → *Engines*; 2. Filee's own copy (bundled or downloaded); 3. a copy already on the computer: on `PATH`, in Homebrew's and the usual Unix folders, or where the LibreOffice, calibre, Ghostscript and Tesseract installers put it. Settings → *Engines* shows it as "Found on this PC", with a note when its version is older than the one Filee downloads. Where Filee has no download of an engine (FFmpeg, Ghostscript and Tesseract on macOS, LibreOffice, Ghostscript and Tesseract on Linux), step 3 is how it is found. Microsoft Office and Hancom Office are never used, even when installed. Running from source, the repository's `engines/` folder (filled by `build/fetch-engines.ps1`) is used. ## Optional engines The installer contains only the small engines. On first start Filee asks which of the large ones to download; they can be installed or removed later in Settings → *Engines*. A conversion that needs a missing engine shows "Needs LibreOffice …" on its donut slice. - Downloads are pinned in `src/Filee.Engines/Infrastructure/engines.json` (URL, SHA-256, size). The same file is read by `build/fetch-engines.ps1`, so development and the app always use the same versions. A macOS download can name the oldest macOS it runs on (`minimumMacOS`); an older Mac isn't offered it. - `EngineInstaller` verifies the SHA-256 before unpacking and unpacks into a staging folder that replaces the engine folder only when complete. Redirects to mirrors (even plain HTTP) are followed, because the hash decides. - Without internet (#447) the same downloads can come from a folder: the card's *Offline* menu saves them on a computer with internet (*Save the files for another computer…*, `filee engines download -o `), copies their links, and installs them (*Install from downloaded files…*, `filee engines install --from `). A file counts as a component's when it has the exact size engines.json pins (its download's name breaks a tie), and only the top level of a folder is looked at. Every file is copied into `.downloads` and checked against its SHA-256 before the first one is unpacked, so a missing or wrong file leaves the package as it was. Only the downloads are taken, never an installed engine folder: an unpacked and trimmed folder has nothing pinned to check it against. The other computer needs the same Filee version (the same engines.json). - Downloads (engines, OCR languages, update checks, links dragged from a browser) go through the proxy in Settings → *General* → *Network* when one is set (`AppSettings.Proxy`, `ProxySetting`): `http`, `https`, `socks4`, `socks4a` or `socks5` with a host and port (`http://127.0.0.1:7890`), optionally with `user:password@`. It is for Filee alone (#879), so the system can stay without a proxy, and the CLI reads the same setting. Empty: the system proxy and `HTTPS_PROXY` / `NO_PROXY`, as before. Addresses on this computer and the local network (Ollama, an intranet) never go through it, and logs show only its scheme, host and port. - Engines go to `%LOCALAPPDATA%\Filee\engines`: outside the app folder, so updates keep them, and inside Filee's install root, so uninstalling removes them. - LibreOffice and calibre come as MSIs and are unpacked with an administrative install (`msiexec /a`): files only, no registry entries, no admin rights. LibreOffice's help, gallery and most dictionaries are removed afterwards (~500 MB). msiexec gets the real paths of the package, the target and its log: the Windows Installer service won't follow a junction a user made (error 1620 or 1622, #527), as when `AppData\Local` was moved to another drive. - Ghostscript comes from conda-forge (Artifex publishes only an NSIS installer): a `.conda` package is a zip with a zstd tarball, of which only `Library/bin` is unpacked (SharpCompress; `fetch-engines.ps1` uses Windows' `tar.exe`). Its fonts and resources are compiled into `gsdll64.dll`. The Microsoft C++ runtime it was built against (`vcruntime` component) is copied next to `gswin64c.exe`, so it runs on PCs without the VC++ Redistributable. - Tesseract is UB Mannheim's Windows build (the one Tesseract's documentation points to), published only as an NSIS installer. It is never run: Filee's own 7-Zip unpacks it (`nsis` kind), and the training programs, manuals, installer files and bundled language data are removed (`TrimTesseract`, ~80 MB). The languages come from `tessdata_fast` 4.1.0, pinned like the engines in `Infrastructure/ocr-languages.json` (`traineddata` kind, one folder per language under `engines/ocr-languages`), and are downloaded the first time a preset uses one. - ExifTool is Phil Harvey's Windows build (a Perl bundled with it), whose `exiftool(-k).exe` is renamed `exiftool.exe`: the "-k" makes it wait for a key before it exits. On macOS and Linux it is the Perl distribution (`Image-ExifTool-*.tar.gz`), run by the Perl both come with. ### Microsoft C++ runtime (Windows) Programs built with Visual C++ import `MSVCP140.dll` / `VCRUNTIME140.dll`, which a fresh Windows does not have (they come with the Visual C++ Redistributable). Windows looks for a program's DLLs in its own folder first, so Filee puts the runtime next to every program that needs it and does not ship it (`Infrastructure/AppLocalRuntime.cs`): | Binary | Runtime | |---|---| | `engines/rhwp/rhwp.exe` (bundled) | `vcruntime` component copied next to it by `fetch-engines.ps1` (#248) | | `unhwp.dll` (NuGet, loaded into Filee.exe and filee-cli.exe) | `vcruntime` component copied next to `Filee.exe` (`build-installer.ps1` → `fetch-engines.ps1 -RuntimeTo`) | | 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`) | | 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 Program Files cannot repair its own bundled files, so an older installation gets them by installing the new version. When a DLL is still missing, Windows does not start the program and reports `0xC0000135` (STATUS_DLL_NOT_FOUND; `0xC0000139` for an outdated one) as its exit code: `ProcessRunner` and `FfmpegRunner` turn that into `error.runtime_missing` ("A Microsoft Visual C++ runtime file this engine needs is missing or outdated …", with the program and the code in brackets) instead of the raw exit code, and `UnhwpConverter` does the same when `unhwp.dll` cannot be loaded. To check binaries after an engine update, list the DLLs each `.exe` / `.dll` imports (its PE import table, e.g. `dumpbin /dependents`) and look for `MSVCP140*.dll`, `VCRUNTIME140*.dll` or `CONCRT140.dll`. To bump a version, change `url`, `sha256` and `size` in engines.json together, run `pwsh build/fetch-engines.ps1`, test, and update `THIRD-PARTY-NOTICES.md`. For rhwp, also update `RHWP_VERSION`, `RHWP_COMMIT` and `RUST_TOOLCHAIN` in `build/build-native.py` (see below); CI fails until they match. The Windows ARM64 builds of rhwp and Unhwp need a new build and pin too (see [Windows on ARM64](#windows-on-arm64)). When the LibreOffice team retires a version from `download.documentfoundation.org/libreoffice/stable/`, its URL stops working: keep the pinned version current. ## Linux builds for older glibc Linux packages run on Ubuntu 22.04 (glibc 2.35). rhwp's Linux release is linked on Ubuntu 24.04 (needs glibc 2.39). The `linux-native` job of `.github/workflows/portable.yml` builds it from its pinned, unmodified source on Ubuntu 22.04 (x64 and arm64) with `build/build-native.py`: - `rhwp`: `cargo build --release --locked --bin rhwp` at the tag's commit with its pinned Rust toolchain, in the layout of rhwp's release archive. - `unhwp` (arm64 only): Unhwp's NuGet package ships no linux-arm64 library, so `libunhwp.so` is built from the commit the package's nuspec names (`cargo build --release --locked -p unhwp --lib --features ffi`, as upstream builds it). - `check` (also run on its own): fails when engines.json pins another rhwp version, or Directory.Packages.props another Unhwp version, than the script builds. The Linux package jobs pass the result to `build/build-portable.ps1 -RhwpOverride ` (arm64 also `-UnhwpOverride `, which the arm64 tests are built with too: `-p:UnhwpNativeLibrary`). Local builds without it use the downloaded rhwp (fine on the machine that builds it). SharpHook's native libraries are packaged as they come from NuGet: SharpHook 8.0.0 built its X11 back ends against glibc 2.38, 8.0.1 needs glibc 2.34 at most again. `build/check-elf-glibc.py` lists the newest GLIBC version every ELF file in the package needs and fails above 2.35, and the packages are unpacked and started on Ubuntu 22.04 (`build/smoke-portable.ps1`: CLI, rhwp conversions, loading the X11 input hook back end; `build/smoke-launch-app.py`: the app under Xvfb). If a SharpHook or engine update fails these checks, keep the previous version until upstream builds for an older glibc again, or build the binary from its pinned source in `build/build-native.py` like rhwp (PR #113 did this for SharpHook 8.0.0's libuiohook back ends). ## Windows on ARM64 `build/build-installer.ps1 -Runtime win-arm64` builds `Filee--win-arm64-Setup.exe` (and `-Portable.zip`): the app, the command line and Setup for win-arm64, the Explorer menu DLL for ARM64 (`build-explorer-menu.ps1 -Architecture arm64`, packaged as `arm64`), and ARM64 builds of the bundled engines (`fetch-engines.ps1 -Runtime win-arm64`). The engines the app downloads stay x64: on Windows 11 on ARM, which runs them under emulation, the app offers the win-x64 downloads (`EngineDownloads.DownloadRuntime`); Windows 10 on ARM can't run them and gets none. Setup's engine list is made when it is packed (`build/tools/make-setup.cs`), so the ARM64 Setup offers the x64 one. - **7-Zip**: the official ARM64 installer of the version engines.json pins, a 7z self-extracting archive unpacked by the x64 7-Zip (Windows' `tar.exe` can't undo its ARM64 executable filter). - **rhwp** and **Unhwp's library** have no ARM64 release. `.github/workflows/windows-arm64-native.yml` (run it from the Actions tab once per version) builds them on a Windows ARM64 runner with `build/build-native.py`, from the same pinned commits as above, with the C runtime linked statically (`+crt-static`): they import no Visual C++ runtime DLL, so the ARM64 build ships none. Its artifact is hosted on dl.filee.sh and pinned by SHA-256 in `$arm64Pins` of `build/fetch-engines.ps1`; until a pin is filled in, a win-arm64 build stops and says so. `unhwp.dll` goes next to `Filee.exe` through the build (`-p:UnhwpNativeLibrary`). `UnhwpConverter` reports Unhwp as available where its library loads, so a build without it shows the engine as not available on this platform. CI's `build (win-arm64)` job builds and tests on Windows 11 ARM64; once both pins are filled in it also builds the package (artifact `windows-arm64-package`). ## Images and vector graphics - **Trim transparent borders** (preset *Trim transparent borders*, `ImageOptions.TrimTransparent`, #332) crops a PNG, WebP, … to the box around its pixels that aren't fully transparent (`ImageEncoder.TrimTransparentBorders`) and saves `name_trimmed` in the same format; the source is never overwritten. Only alpha counts: ImageMagick's own trim takes the corners' colour, so content touching a corner would lose a solid border. Pictures without transparent borders, fully transparent ones and multi-frame animations (each frame alone would shift) come out as they are. Formats that can't be transparent (JPG, HEIC, …) have nothing to trim: the preset is greyed out for them, and a JPG in a mixed selection fails with that reason instead of being re-encoded. - **Animations.** An animated GIF, WebP, APNG, MNG, JPEG XL or AVIF keeps every frame and its timing when converted to one of these (animated GIF ↔ animated WebP ↔ APNG ↔ MNG ↔ JPEG XL ↔ AVIF, WebP lossy or lossless as the preset says, #988) and becomes a multi-page TIFF; other targets get the first frame. Multi-page TIFFs and PDFs stay one image per page in WebP and AVIF. - **Animated AVIF** (#1200) needs no FFmpeg, but goes much faster with it. ImageMagick reads the frames and applies the preset either way. With FFmpeg and its libaom-av1 encoder (Filee's FFmpeg download has it), `Media/AvifSequenceWriter.cs` packs the frames into an APNG in the work folder (`Magick/Apng.cs`, exact delays) and FFmpeg's avif muxer writes the image sequence (`avis`): FFmpeg encodes 60 busy 480 × 270 frames in about 3 s, where ImageMagick's writer takes 50 s or more. Cancelling stops it, and the loop count stays (the muxer's `-loop`, the edit list's repeat). Only libaom-av1 is used: the avif muxer takes the alpha track as one grey plane, which SVT-AV1 doesn't encode, and FFmpeg's SVT-AV1 wrapper passes no packet durations, so the last frame's length would be a guess (a GIF's long pause at the end cut short). Colour is full-range BT.709 4:2:0 tagged as sRGB, the preset's quality the quantizer libheif would pick. An FFmpeg without libaom-av1, or one whose run fails (an old avif muxer), leaves the file to ImageMagick, which writes the sequence through libheif. Its writer stores no loop count (an animation that plays a set number of times loops for ever) and can't be interrupted. Both ways, the first frame is also stored as the still picture for viewers that don't play sequences, and ImageMagick reads such a file back frame by frame, also one FFmpeg wrote. Transparency stays (one alpha track, so `ImageEncoder.MatchAlpha` gives every frame an alpha channel when any frame is transparent, and none when all are opaque, which leaves out the alpha track). An AVIF source is an animation only when it is a sequence (`Magick/AvifSequence.cs`: major brand `avis` and a `moov` box). The items of any other AVIF (a burst, a visible gain map, pictures of their own sizes) stay separate pictures, one `_pN` file each. A still picture takes the same one-picture path as ever. On the way to video an AVIF counts as a photo (`ImageEncoder.Edge`): nearly all of them are, so an animated one becomes a video of its first frame rather than every AVIF going through a 256-colour GIF. - **Playback speed** (preset option *Playback speed*, `ImageOptions.AnimationSpeed`: 0.25×, 0.5×, 0.75×, 1×, 1.5×, 2×, 4×; shown for GIF, WebP, APNG, MNG, JPEG XL, AVIF and "same as source") rewrites the frame delays of an animation that stays animated (`Magick/AnimationTiming.cs`). Delays are divided by the speed in the target's own units (hundredths of a second for GIF, MNG and JPEG XL, milliseconds for WebP, APNG and AVIF) and rounded on the running total, so the whole animation keeps its new length. Players show a frame of 10 ms or less for 100 ms, so no delay goes below 20 ms (50 frames a second): a frame that would start sooner than that after the one before is dropped and its time goes to that one, which leaves frames out evenly. Source delays of 10 ms or less count as the 100 ms players show. Loop count and disposal stay; 1× (also every preset saved before the option) leaves the timing untouched. Video → GIF / APNG (FFmpeg) puts `setpts=PTS/` before the `fps` filter, so the frame rate limits stay. The speed changes only in the step that writes the output (`JobQueue`), never twice on a longer route. - **APNG** (`Magick/Apng.cs`) is Filee's own code, written from the APNG specification: ImageMagick reads only an APNG's default image and writes APNG through an external FFmpeg. Every frame is an ordinary PNG decoded or encoded by ImageMagick; Filee handles the chunks around them. - Reading composes each frame onto the canvas as a player shows it: `fcTL` regions and offsets, blend `SOURCE` / `OVER`, dispose `NONE` / `BACKGROUND` / `PREVIOUS`, delays (a denominator of 0 means hundredths) and the loop count. A default image that comes before the first `fcTL` is not part of the animation and is skipped. Animated PNGs saved as `.png` are recognised by their `acTL` chunk and stay animated as GIF, WebP or APNG; other targets get the first frame. - Writing gives every frame the same pixel format (8-bit RGB, or RGBA when any frame is transparent or smaller than the canvas): the first frame is the default image (`IDAT`, so viewers without APNG support show it), the others are `fdAT` chunks holding only the rectangle that changed (dispose `NONE`, blend `SOURCE`); a frame identical to the one before lengthens its delay instead. Checked with FFmpeg's APNG demuxer. - APNG ↔ video needs FFmpeg: video → APNG uses its `apng` encoder and muxer (24-bit RGB, looping, the GIF's frame rate and width limits), APNG → MP4/WEBM/MOV its APNG demuxer. Animated WebP and GIF still reach video through GIF, a still picture through PNG. - **More raster formats** through ImageMagick's own coders, each checked against `MagickNET.SupportedFormats` and converted there and back in `ImageFormatTests`: QOI, MNG (animated), DDS (DXT block compression, which is lossy; of a texture with mipmaps the largest image is read; uncompressed 32-bit pixels stored R, G, B, A, which ImageMagick reads with red and blue swapped, are swapped back when converting and in the crop tool), PCX, CUR (at most 256 × 256, larger pictures are scaled down; ImageMagick writes it as an icon, type 1, whose directory holds colour planes and bits per pixel where Windows reads the hotspot, so `CursorFile` makes every .cur a real cursor, type 2, clicking at the top left corner, or, from a CUR or ANI, where the source's hotspot is once the picture is resized, turned or mirrored), XPM, XBM and WBMP (1-bit), PAM, PFM, SGI (`.sgi`, `.rgb`, `.rgba`, `.bw`, always read as SGI; ImageMagick would take `.rgb` for headerless pixels), Sun Raster (`.ras`, `.sun`) and MIFF. XBM, WBMP, PFM, PCX and Sun Raster are written without transparency (flattened onto the preset's background): PFM has none, and the alpha planes ImageMagick can put into PCX and Sun Raster are read by hardly any other program. Not offered: PICT, because ImageMagick can't read back direct-colour PICTs wider than about 130 pixels that it wrote itself. - **Read only**: Photo CD (the 768 × 512 base image), OpenEXR and Radiance HDR. The bundled Magick.NET is the 8-bit (Q8) build without HDRI, so high-dynamic-range values are clipped to the displayable range and stored with 8 bits per channel; there is no tone mapping. Writing EXR or HDR from 8-bit pictures would only make larger files, so they are not targets. - **JPEG XR** (`.jxr`, `.wdp`, `.hdp`, read only, Windows only; `Wic/`). ImageMagick has no JPEG XR coder in any build, so the picture is decoded by Windows' own codec (WIC's JPEG XR decoder, in every Windows 10 and 11) and handed to ImageMagick for the preset's options and the output format. Elsewhere the engine reports "not available on this platform". 8- and 16-bit pictures are converted to 8-bit sRGB by WIC. Fixed-point and float ones are linear scRGB (1.0 = SDR white at 80 nits), like the 64-bit half-float screenshots Windows saves on an HDR display (Xbox Game Bar, Win + Alt + PrtScn, Snipping Tool), and are tone-mapped (`Wic/HdrToneMap.cs`) instead of clipped: - the reference white is the luminance 99 % of the pixels stay below, between 1.0 and 4.0 (80–320 nits). Windows draws SDR content at the display's "SDR content brightness", so the white of a desktop screenshot lands there and its SDR parts come out as they were drawn; small highlights don't darken the picture; - values up to 80 % of that white are kept; above, an extended Reinhard shoulder takes everything up to the brightest pixel into the last 20 %, on the largest of R, G and B so colours keep their hue. A picture with nothing brighter than white is left as it is; - colours outside the sRGB gamut are clipped, then the sRGB transfer curve and 8 bits. An alpha channel that is opaque everywhere (HDR screenshots have one) is dropped. - **DICOM** (`.dcm`, `.dicom`, read only). The Q8 build reads only 8-bit DICOM, while CT, MR and X-ray scanners store 12 or 16 bits, so `Magick/Dicom.cs` (written from the DICOM standard, PS3.5 and PS3.3 C.7.6.3 / C.11.2) reads uncompressed files itself: implicit and explicit VR little endian, explicit VR big endian and deflated files, with or without the 128-byte preamble; 8 or 16 bits allocated, signed or unsigned; MONOCHROME1 (inverted) and MONOCHROME2, RGB and YBR_FULL (interleaved or planar); every frame of a multi-frame file. Grey values go through the rescale slope / intercept and the file's first window (centre / width) like a viewer's default view, or are stretched over the file's range when there is no window, to 8 bits. Compressed pixel data (JPEG, JPEG-LS, JPEG 2000, RLE) is left to ImageMagick, which manages only 8-bit images; others fail with a message asking for an uncompressed export. - **FITS** (`.fits`, `.fts`, read only; `.fit` is left to Garmin's FIT). Astronomy pictures from telescope cameras, capture and stacking programs. `Magick/Fits.cs` (written from the FITS 4.0 standard) reads the first HDU with a picture, the primary one or the first IMAGE extension: 2880-byte blocks of 80-character cards, big-endian data, BITPIX 8, 16, 32, 64, -32 and -64, physical values BZERO + BSCALE × stored (BLANK integers become undefined), a cube of three planes as red, green and blue (other cubes give their first plane). The Bayer pattern comes from `BAYERPAT` or `COLORTYP` (RGGB, BGGR, GRBG, GBRG) at the first stored pixel, shifted by `XBAYROFF` / `YBAYROFF`. `ROWORDER = 'BOTTOM-UP'` turns the picture upright; without it rows are taken top-down, as capture programs write them. Tile-compressed files (fpack, a BINTABLE with `ZIMAGE = T`) fail with a message asking to `funpack` them. - **PNG, JPG and every other target** (`FitsRendering`): stretched automatically like a capture program's preview. The values are scaled to 0–1 over their range; per channel, the shadows are clipped 2.8 normalized MADs (1.4826 × the median absolute deviation) below the median and a midtones transfer function takes the median to 0.2. A colour camera's mosaic is debayered first (bilinear: each missing colour is the average of the neighbours of that colour) unless the preset's *Debayer FITS from a colour camera* is off (`ImageOptions.FitsDebayer`); its channels are stretched one by one, so the sky comes out neutral. 8 bits per channel, then ImageMagick as usual. - **TIFF** (`TiffWriter`, as the Q8 build stores 8 bits): *Linear* (the data as it is, the default) or *Stretched* (`ImageOptions.FitsTiff`), at the file's own depth: 8-bit and 16-bit data stay so when they are whole numbers in range (unsigned 16-bit through BZERO 32768 included); 32-bit integers, floating point and signed or scaled values that don't fit become 32-bit floating point. Never debayered, so another program can still debayer the mosaic. Uncompressed, LZW or Deflate as the preset's TIFF compression says (integers with the horizontal predictor); turning and mirroring apply, resizing and the other picture options don't. FITS → TIFF costs a little more than other edges, so other formats are reached through a stretched picture, never through the linear TIFF. - **Fujifilm HEIF** (`.hif`) is read as HEIC. - **Rotate and flip** happen after the EXIF orientation is applied, in the one step of a route that turns images (`IImageTransformer`). There is no lossless JPEG rotation: a turned JPEG is re-encoded, at its own estimated quality when that is lower than the preset's, so a phone photo saved at 75 doesn't come back bigger at 95. - **Layered files.** PSD/PSB convert from Photoshop's composite (ImageMagick merges the layers when a file was saved without "maximize compatibility"). GIMP XCF layers are composited at their offsets with their blend modes; hidden layers are left out. ImageMagick reads 8-bit XCF without zlib tile compression (GIMP 2.10+ "better but slower compression" files give a clear error). - **LIVP** (an iPhone Live Photo as iCloud downloads it: a ZIP of the photo, HEIC or JPEG, and its MOV) is read as its photo, taken out of the package in memory (`LivePhotoSource.ReadStill`, at most 64 MB); its video is FFmpeg's (see Live / Motion Photos there). - **CMYK pictures → PDF** (#1027, #1055): print work keeps its colours. PDFsharp's image import takes grey and RGB only, so `Pdf/CmykPictures.cs` writes the image itself: CMYK, with the picture's own ICC profile as an ICCBased colour space (else DeviceCMYK); a JPEG source as JPEG data (ImageMagick writes CMYK JPEGs inverted with Adobe's APP14 marker, undone with a Decode array), others as their samples compressed losslessly; transparency as a soft mask. Grey and RGB pictures go in through PDFsharp as before. - **KRA** (Krita's own file, a ZIP of the layers) is read as the merged picture Krita stores at its top for other programs (`mergedimage.png`, `Krita.ReadMerged`, at most 256 MB, in memory), so it looks as it does in Krita. A file without one gives a clear error. Krita's backup of the previous save, `name.kra~`, is the same file and is read the same way; its output is named `name.png`. Watch folders skip it (and any name ending in `~`), as it is written next to the drawing on every save. - **PDN** (Paint.NET's own file) is read by Filee itself (`PaintNet`): an XML header, the document in .NET binary serialization, read as plain data with System.Formats.Nrbf (no type it names is ever created), then each layer's BGRA pixels in gzip-compressed chunks. For PNG, JPG and every other target the visible layers are merged with Paint.NET's own blend formulas (all 14 modes, layer opacity), so the picture looks as it does in Paint.NET. To **PSD / PSB** the layers are kept, for Photoshop, GIMP and Krita: names (also as Unicode, `luni`), opacity, visibility and the blend modes Photoshop shares (Multiply, Additive as Linear Dodge, Color Burn, Color Dodge, Overlay, Difference, Lighten, Darken, Screen; Reflect, Glow, Negation and Xor become Normal), with the merged picture first. ImageMagick's PSD writer stores a layer's visibility in the wrong bit and locks its transparency, so `PaintNet.SetLayerFlags` sets the flags afterwards. A preset that changes the picture (resize, rotate, …) gives one merged picture instead. Filee also **writes** PDN (`PaintNetWriter`), in the shape Paint.NET 4.2 saves and every later version opens (the same records, class metadata and 256 KB gzip chunks, and a thumbnail in the header): any picture becomes one layer named Background, and a **PSD / PSB** keeps its pixel layers, each at its place on the canvas, with its Unicode name, opacity, visibility and the blend modes Paint.NET shares. PDN → PSD → PDN gives back the same layers, pixel for pixel. A PDN is a target only where the Design & print field is on. - **EXE / DLL** (a Windows program or library, #1188) is read as its icon, the one Explorer shows: the first icon group of its resources, put back together as an `.ico` in memory (`ProgramIcon`, Filee's own reader for 32 and 64-bit PE files; only the headers and the icon are read, at most 64 MB, and the file is never loaded or run). To ICO it is that icon as it is, every size the program has, unless the preset changes the picture or asks for other icon sizes; then, as for every other target, the largest size is converted. A file without an icon of its own, or a 16-bit or damaged one, gives a clear error. Programs are converted only when dropped or named: watch folders and folders given to the command line skip them, and Recent conversions never opens one. - **ANI** (a Windows animated cursor, #1365) is read by Filee itself (`Ani`): a RIFF file whose frames are whole .cur or .ico files, each decoded by ImageMagick at its largest size. To GIF, APNG, WebP and the other animated targets every step plays in the order of its `seq ` chunk (frames can come back), for the time its `rate` chunk or the header gives (jiffies of 1/60 s, rounded to hundredths), looping forever; other targets get the first step. To **CUR** the first frame is written as stored, every size and its hotspot; a preset that changes the picture converts it, and the hotspot follows it when it is resized, turned or mirrored (trimmed or set on a background, it goes to the top left corner; see CUR above). Files are read in memory with their sizes checked (at most 64 MB, 1,024 frames, 4,096 steps, 32 sizes a frame, 1,024 px a side, and a pixel budget for all sizes and steps together, checked before ImageMagick decodes anything); frames stored as raw bitmaps (no `AF_ICON` flag, which Windows' own loaders hardly use) and damaged files give a clear error. - **Live / Motion Photos from a video** (JPG and HEIC targets) are FFmpeg's: the clip and the photo's frame come from it, ImageMagick or the HEIC writer only encode that frame (see *Making Live / Motion Photos* there). - **Camera RAW** (DNG, CR2, CR3, NEF, ARW, …, and `.raw`) is decoded by LibRaw with the camera's white balance, sRGB output and AHD demosaicing, then turned upright from the EXIF orientation. - **EMF/WMF** are rendered through GDI+ (Windows only) at the preset's render DPI (at least 150), keeping their physical size in PDF output. - **SVG → PDF** replays the drawing into Skia's PDF backend: paths, gradients and clips stay vector. Text is written as glyph outlines (shaped with HarfBuzz), because SkiaSharp's PDF backend has no font subsetter and would embed every font whole (13 MB for one line of Korean). SVG → images is rendered at its final size: the preset's DPI, at least 1024 px on the long edge, or exactly the preset's resize target. External files next to the SVG are loaded; nothing is fetched from the network and scripts don't run. - **ICNS** files are written with every size iconutil makes (16–1024 px, PNG), the picture centred on a transparent square; reading takes the largest image (PNG, JPEG 2000 or the old RLE + mask entries). - **Illustrator** files saved with "Create PDF compatible file" (the default since Illustrator 9) are PDFs and need nothing extra; PostScript-based ones need Ghostscript. - **CMYK pages** (#1027). PDFium renders RGB only, so a print file (a CMYK PDF, or an Illustrator file through its PDF) → JPG or TIFF is rendered by Ghostscript instead when it is installed: `tiff32nc` (8-bit CMYK) at the preset's render DPI, cut to the crop box, anti-aliased, then written like PDFium's pages (quality, resize, `_p{n}` names, multi-page TIFF, long images, size limits). `Pdf/PdfCmykContent.cs` decides per document with PdfPig, from the first 20 selected pages: DeviceCMYK colours (`k` / `K`, also inside forms), four-channel ICC colour spaces, Separation / DeviceN (spot colours), CMYK pictures and gradients. DeviceCMYK values come out as the document gives them (Ghostscript's default CMYK profile is both their source and the output). When the PDF has an output intent with a CMYK profile (PDF/X), Ghostscript renders into that profile and the picture carries it; otherwise the picture is untagged CMYK, which keeps the numbers and leaves the profile to the print shop (Ghostscript's own default profile is removed). RGB documents, a grayscale preset, other targets (PNG, WebP, …), a PDF PdfPig can't read and PCs without Ghostscript keep PDFium's RGB rendering. When a CMYK file comes out in RGB only because Ghostscript is missing or failed on it, the result says so (`notice.cmyk_needs_ghostscript`, `notice.cmyk_render_failed`): print work shouldn't change colour unseen. - EPS and PS → PNG go straight through Ghostscript's `pngalpha` device, so the background stays transparent; JPG, WebP, … and AI reach images through PDF (Ghostscript → PDFium), and SVG reaches EPS through PDF. ## Tracing (raster images → SVG) Built in (`Vector/Tracing`), no Potrace or other program: Filee's own code, written from published descriptions of the steps (no code of Potrace, AutoTrace or VTracer is used). Every raster format ImageMagick reads (PNG, JPG, BMP, GIF, WEBP, TIFF, HEIC, PSD, RAW, DICOM, …) is traced, and other raster images through PNG (ICNS, APNG). Only raster images are traced: PDF, EPS, PS, AI, EMF/WMF, CDR, documents and videos are never redrawn from a rendering, so without a real vector route they have no SVG target (PDF → SVG, for example, needs LibreOffice). The first frame or page is used, turned upright, in sRGB. The SVG has the picture's pixel size (`width`/`height`/`viewBox` in pixels), so it lines up with the original. - **Black and white** (*Line art to SVG*: logos, signatures, scans): pixels darker than Otsu's threshold are ink (transparent pixels count as white paper). On a transparent background the opaque pixels are the ink, or only their darker part when the opaque area itself is light and dark. One black `` on a transparent background. - **Colour** (*Trace to SVG*: illustrations, icons): k-means reduces the picture to the preset's number of colours (2–64; run on the mean colours of 32 768 colour boxes weighted by their pixel counts, so it sees every pixel). One-pixel bands of in-between colours along edges (anti-aliasing) join the colour on either side; regions smaller than a few pixels (more on large pictures) join the neighbour they share most border with. Colours are stacked largest first, and each colour's shape is the union of itself and every colour above it: lower shapes run under upper ones, so no background shows through between neighbouring shapes. Transparent areas stay transparent. One `` per colour. - **Outlines**: each shape's boundary is followed along the pixel edges (inside always on the right, so holes come out reversed and fill correctly with the non-zero rule; pixels touching only at a corner stay one shape). Outlines smaller than the speck size are dropped (and small holes filled). - **Curves**: the staircase is replaced by points on the edge it approximates (midpoints of the steps); corners are kept where two long pixel runs meet, at the ends of thin lines, where the direction turns sharply within two pixels, or where a gentler turn happens all at once (unlike a small circle). Points between corners are smoothed; stretches that stay within half a pixel of a line without bending become lines; the rest becomes cubic Béziers fitted by least squares (Philip J. Schneider, "An Algorithm for Automatically Fitting Digitized Curves", Graphics Gems, 1990), meeting lines and each other with matching tangents. - **Options** (Settings → Presets): mode, colours (colour mode) and *Detail* 0–100. Low detail drops larger specks, smooths more and allows the curves 1.15 px of deviation; high detail keeps every pixel's shape (0.25 px). The default 50 drops specks under 6 px (black and white) or merges regions under 17 px (colour), at 0.7 px; speck and region sizes grow with pictures larger than 1000 px (up to four times). - **Size and speed**: pictures over 6 megapixels or 4096 px on a side are reduced before tracing; the SVG keeps the original size and says so in a comment. A 2000 × 2000 illustration traces in 1–3 s at 16 colours and well under a second in black and white; a noisy 2000 × 2000 photo at 16 colours takes about 4 s (64 colours about 5 s, and a 20 MB SVG). Photos are not what tracing is for: they come out as posterized patches. - **Routes**: tracing is the only raster → SVG route and costs 40 (`VectorConverter.TraceCost`), more than any route from an image to a format SVG leads to, so it is never a step on the way to PDF, PNG, EPS, …; a test checks every pair of formats. Its edges are marked `ImageSourcesOnly`: the planner uses them only when the file being converted is a raster image (`FormatCategory.Image`), so a vector file rendered to PNG can't be traced; a test checks this for every pair of formats too. Through SVG, pictures reach DXF and DWG. ## SVG → DXF / DWG `Vector/SvgDxfWriter.cs` writes the drawing's outlines with ACadSharp (DXF in the preset's version like the CAD engine; DWG is DXF → DWG by the CAD engine). The SVG is loaded with Svg.Skia as for PDF output, so groups, transforms, ``, nested ``, CSS and text are resolved the same way. - Circles and ellipses become CIRCLE and ELLIPSE (also rotated, scaled or skewed: the transformed axes come from the singular values of the transform). Paths, lines, polylines, polygons and rectangles (rounded too) become LWPOLYLINEs, closed where the shape is closed, or a LINE for one straight segment. Bézier curves and arcs are divided into straight pieces within 0.01 mm of the curve: every CAD and CAM program reads polylines, while SPLINE support varies. Text becomes its glyph outlines. - Units: SVG user units are CSS pixels, 96 per inch, so 1 px = 25.4 / 96 mm and the DXF is in millimetres (`$INSUNITS` 4, metric): a drawing 100 mm wide in the SVG is 100 mm wide in CAD. The y axis is flipped (SVG points down, DXF up) with the viewport's bottom-left corner at (0, 0). - Colours are kept as true colours (the stroke's, else the fill's; black for gradients). Fills, stroke widths, clipping, masks, images and filters are not represented; a shape both filled and stroked is written once. An SVG with nothing to outline fails with a message. ## HWPX writer Filee writes OWPML (HWPX) itself, so saving as HWPX needs no Hancom Office and shows no approval dialog. Readers turn the source into a small document model (`Hwp/Hwpx/HwpxModel.cs`) and `HwpxWriter` writes the package: - **DOCX** is read directly (`Hwp/Hwpx/Docx/DocxReader*.cs`) so the layout survives: - page size, orientation and margins per section; columns, also when they change mid-page; - headers and footers (odd/even pages, hidden on the first page) with page numbers and page counts; - paragraph formatting (alignment, indents, spacing, line spacing, tab stops, keep with next); - character formatting (fonts incl. theme fonts, size, colour, highlight and shading, bold/italic/underline/strike, super/subscript, spacing); - lists with Word's numbering formats (1., 가., ①, i., bullets); - symbols of Wingdings, Wingdings 2, Wingdings 3, Webdings and Symbol (Insert → Symbol, bullets, text typed in those fonts) as the Unicode characters they show: check boxes ☐ ☑ ☒, check marks and crosses, arrows, stars, hands, phone and mail signs, Greek letters and math signs (`Hwp/Hwpx/SymbolFonts.cs`, #350; PPTX bullets and text too). Codes without a table entry stay as before (an unknown bullet is "●"); - tables with merged cells, column widths, borders and shading from the table style and cells, repeated header rows; - pictures inline and floating, text boxes, rectangles/ellipses/lines with solid or gradient fills, groups; - footnotes, endnotes, bookmarks, hyperlinks (web and within the document, e.g. a table of contents); - equations (Word's equation editor, Office Math) as 한글 equations, see *Equations* below; - Word's document grid ("lines"), which 한글 does not have, as an "at least" line spacing. - **XLSX, XLS, ODS, CSV and TSV** (`Office/Sheets`, see [Spreadsheets](#spreadsheets)): every visible sheet becomes a section with one table. Cells show what Excel shows (number formats via ExcelNumberFormat, dates, percentages, cached formula results), with fonts, fills, borders, alignment and merged cells; an XLSX's rows to repeat at the top (Print Titles, #407), else frozen top rows, repeat on every page, and rows above the print titles come once before them. The page is A4 with narrow margins, landscape when the columns are much wider than portrait, and the table is scaled to the page width like Excel's "Fit all columns on one page". CSV is read with its delimiter (comma, semicolon, tab) and encoding (UTF-8 / UTF-16 with BOM; else CP949 when it looks Korean or on Korean Windows, as Korean Excel saves it; else the PC's code page). Not converted: charts, pictures, conditional formatting, print areas and page breaks. - **PPTX** (`Hwp/Hwpx/Pptx`): one page per slide, sized like the slide, with every object floating at its place: placeholders inherit position and text style from the layout and master, text with bullets and numbering, shapes (rectangles, rounded rectangles, ellipses, lines) with solid or gradient fills and outlines, pictures, tables with PowerPoint's default table style, groups, master decorations and the slide background. Not converted: charts, SmartArt, rotation, effects (shadows, glow), picture cropping, animations, notes and hidden slides. - **TXT** needs no reader (one paragraph per line; UTF-8, UTF-16, CP949 when the bytes look Korean or, on Korean Windows, are valid CP949, else the PC's code page: GBK on Chinese Windows, Shift-JIS on Japanese, Windows-1252 on Western, as subtitles are read; a `charset=` in the text isn't followed). - **Markdown** is parsed with Markdig (`MarkdownReader.cs`): headings, emphasis, code, lists (1. / a. / i. with start numbers), task lists, quotes, tables with spans, links, images, footnotes and math. No Pandoc needed. - **PDF** is read with PdfPig (see *PDF reader* below); DOCM / DOTX / DOTM, XLSM / XLTX and PPTM / POTX / POTM / PPSX are read like DOCX, XLSX and PPTX. - **HTML** is parsed with AngleSharp (`HtmlReader.cs`, `HtmlCss.cs`): headings, paragraphs, bold / italic / underline / strike / sub / sup / code / mark, the basic inline CSS (colour, background, font weight, style and size, text-align, text-indent, page breaks) and simple `tag` / `.class` rules of style sheets, nested lists with start numbers and types, tables with spans / header rows / borders, pictures (local files and data: URIs; remote pictures are skipped), links and anchors, quotes, `pre`, `hr` and figures. Scripts, styles, forms and `nav` are skipped. The encoding comes from the BOM, the XML declaration or ``, else UTF-8 or the system code page. No Pandoc needed. - **MHTML** (`.mhtml`, `.mht`) is turned into its standalone page first (see *E-mail* below) and that page read like HTML; it refers to nothing but data: URIs, so no file outside the job's scratch folder is looked at. - **ODT, RTF** are parsed by Pandoc into its JSON AST (`PandocAstReader.cs`, following [pypandoc-hwpx](https://github.com/msjang/pypandoc-hwpx)), in Pandoc's `--sandbox`: Pandoc reads nothing but the file, so include directives stay unread and pictures the document links to are found by Filee. - Markdown, HTML and Pandoc keep structure only, so page setup comes from the built-in template (A4). - Pictures (and HTML style sheets) a Markdown, HTML or Pandoc document links to are read only from the document's own folder and its subfolders (`DocumentFiles.cs`, also for e-books, MHTML and translation): `../`, absolute paths and `file:` URLs outside are left out like missing pictures. A path or `file:` URL naming another computer (`\\host\share`, `file://host/`, `//host/`) is refused by its text alone, so no network connection is made. - **Limits** (`Hwp/Hwpx/Nesting.cs`, `Infrastructure/BoundedXml.cs`): the XML parts of DOCX, PPTX, XLSX, HWPX and ODS packages (and FB2 and OPF files) are read without DTDs, each up to 256 MB unpacked, 1 GB per package, with elements nested at most 1,000 levels; larger or deeper files are refused (`error.document_too_large`). Tables in table cells, text boxes, notes and links nested more than 32 deep become plain paragraphs of their text, in the DOCX and HWPX readers and again in every writer (whatever reader made the model); an equation deeper than 200 levels becomes its text, notes inside notes are not followed, and PPTX groups are opened without recursion. The readers run on a thread with a 16 MB stack. The scan for text rhwp can't draw (`ComplexScripts.cs`) stops at the same sizes: it runs before any reader, finds nothing in a file over them, and the reader then refuses that file. Element order and attribute values follow files saved by 한글 where the schema and 한글 disagree, for example: - `hp:default` repeats paragraph margins and fixed line spacing doubled (`hp:case` holds the real values); - page width/height are always those of the portrait sheet, `landscape="NARROWLY"` turns it; - 한글 keeps a hanging paragraph's first line at `left` and indents the other lines; - object offsets are unsigned: objects reaching into the margin are anchored to the paper instead; - layout caches (table row heights, inline tab widths) are estimated, because simpler readers use them as-is. Every generated file is checked in the tests with three independent readers: rhwp (renders it), Unhwp (reads the text) and LibreOffice + H2Orestart (opens it), see `tests/Filee.Engines.Tests/DocxToHwpxTests.cs` and `OfficeTests.cs`. Not converted: Word charts, SmartArt, free-form shapes, tracked changes and comments. ### Equations Word equations (`m:oMath`, `m:oMathPara`) become 한글 equation objects (`hp:equation`) with an equation script (`Hwp/Hwpx/Docx/OmmlReader.cs` reads them into a small math tree, `Hwp/Hwpx/EquationScript.cs` writes the script), so DOCX → HWPX and HWP keep editable equations and DOCX → PDF draws them with rhwp (#143): - fractions (`{a} over {b}`, also without bar or written linear `a/b`), sub- and superscripts and scripts before the base, square roots and roots with an index, sums, products, unions and integrals with their limits, brackets that grow (`LEFT ( … RIGHT )`), matrices (`pmatrix`, `bmatrix`, `dmatrix`), cases, equation arrays (`pile`), function names and `lim` with what goes under it, accents (hat, bar, vec, dot, tilde …), over- and underlines; - Greek letters, operators and arrows as 한글's commands (`omega`, `TIMES`, `LEQ`, `INF`, `rarrow` …), upright text (`m:sty="p"`, normal text) in `rm`, bold in `bold`; a display equation alone in its paragraph is centred; - variables are written letter by letter when they read like a command (`x _{i n}`, not `in` = ∈); - boxes, phantoms and group characters (braces above or below) keep only their content; other characters are written as they are. The box an equation is stored with (`hp:sz`) is measured like rhwp lays the script out (estimated character widths, script scale 0.7, fraction and root spacing, from rhwp's `equation/layout.rs`): rhwp stretches an equation to its stored width, so a wrong box would distort it. 한글 lays the script out again when the equation is edited. The model's other writers (HTML, EPUB, Markdown, TXT …) write a linear form instead, `(a+b)/(2c)`, `x_i^2`, `√(x)`, which is also the equation's alternative text in HWPX. ## Spreadsheets `SpreadsheetConverter` (engine id `spreadsheet`) converts between XLSX, XLS (and XLT templates, read only), ODS, CSV and TSV (also `.tab`) in-process; every format goes through one in-memory workbook (`Office/Sheets/Workbook.cs`) whose cells keep the text the spreadsheet shows, the value behind it and its Excel number format. | Format | Read | Written | |---|---|---| | XLSX | own reader: values, number formats, fonts, fills, borders, alignment, merges, widths, heights, hidden rows / columns / sheets, frozen rows, print titles (rows) | every sheet, numbers and dates as numbers with their formats, booleans, styles, merges, widths, heights, hidden rows / columns, frozen rows | | XLS (Excel 97–2003, also Excel 5/95) | ExcelDataReader (MIT): values, number formats, merges, horizontal / vertical alignment; widths, heights and hidden rows / columns from the BIFF records (`XlsLayout.cs`) | no (LibreOffice) | | ODS | own reader (`content.xml`, `styles.xml`, `settings.xml`): repeated rows / columns (the empty repeats up to the end of the sheet are skipped), spans, every value type, the text Calc shows (`text:p`), data styles as Excel codes, cell styles, widths, heights, hidden rows / columns / sheets, frozen and header rows | every sheet, typed cells (float, percentage, currency, date, time, boolean) with data styles and the shown text, styles, merges, widths, heights, hidden rows / columns, frozen rows | | CSV / TSV | delimiter (CSV: comma, semicolon or tab; TSV: tab) and encoding (UTF-8 / UTF-16 with BOM, CP949 when it looks Korean or on Korean Windows, else the PC's code page) | UTF-8 with BOM, CRLF, one file per sheet | - Number formats: Excel codes and OpenDocument data styles are converted both ways (`OdsNumberStyles.cs`) for digits, decimals, grouping, thousands scaling, percent, scientific, currency symbols, literal text, colours, positive / negative / zero sections, dates, times and durations. Codes with conditions (`[>100]`), fractions or text between digits are written as plain numbers (General). - Column widths use Excel's unit (a digit of Calibri 11 = 7 px); in ODS a character is 5.25 pt. LibreOffice measures with its own fonts, so a width can differ by up to a fifth after LibreOffice opens the file. - XLS cells keep the default font, fill and borders (ExcelDataReader does not expose them); XLS output still needs LibreOffice. Password-protected XLS and ODS files stop with a clear message. - Not converted: formulas (their cached results are kept), charts, pictures, comments, conditional formatting, data validation, print areas and page breaks. The XLSX donut also offers **PDF + Edit** (`tool-sheet-studio`). Unlike an ordinary routed XLSX → PDF conversion, this interactive tool always requires LibreOffice regardless of engine priority: it preserves the original workbook, makes one disposable copy per selected sheet region, applies that region's print settings, and asks LibreOffice to render it. PDFsharp combines the resulting PDFs. The live preview follows this exact studio export path and uses PDFium/PDFtoImage for lazy thumbnails and the selected large page; the ordinary donut's PDF can differ because its built-in XLSX route reads the workbook into Filee's document model. ## Fonts Fonts are converted in-process (`Filee.Engines/Fonts`); zlib and Brotli come with .NET. The input format is recognised by content, so a mislabelled file still converts. FON is the exception: it is chosen by its `.fon` extension, and the file must then have an NE header with FNT resources. - **WOFF 1.0**: every table zlib-compressed (stored as-is when that isn't smaller). Lossless both ways. - **WOFF2**: one Brotli stream at the highest quality with the glyf/loca transform and, where it applies, the hmtx transform, so files come out the size of Google's reference encoder's. Reading handles both transforms, the overlap bitmap, untransformed tables and the first font of a WOFF2 collection. Like other encoders, Filee drops the DSIG signature and sets bit 11 of head.flags. Brotli is slow on big fonts (about 10 s per MB, so about a minute for a 4.6 MB CJK font); the job shows progress and can be cancelled. - **EOT**: written as version 0x00020001 (names and OS/2 fields in the header, no MicroType Express compression, no XOR obfuscation, no URL restriction). Uncompressed EOT files are read (XOR-obfuscated ones too); MTX-compressed ones fail with a clear message. - **TTF → OTF** keeps the TrueType outlines: TrueType-flavoured OpenType is valid OpenType and what systems and browsers expect; cubic outlines would gain nothing. - **OTF with CFF outlines → TTF** (and → EOT, because Internet Explorer renders only TrueType outlines in EOT) converts the glyphs: Type 2 charstrings with subroutines, flex, seac accents and CID-keyed fonts are drawn, curves are approximated with quadratic splines within 1/1000 em (the approach of fontTools' cu2qu) and contours are reversed to TrueType's direction. glyf, loca, maxp 1.0 and post (format 2 with the CFF glyph names, format 3 for CID fonts) are built; head, hhea, hmtx and, from VORG, vmtx are updated; CFF, VORG and DSIG are dropped; every other table (cmap, name, OS/2, GSUB, GPOS, kern, ...) is kept. PostScript hints are not converted: the font gets a gasp table (smoothing at every size) and a prep that switches on dropout control instead. - **FON → TTF, OTF, WOFF, WOFF2, EOT** (input only): Filee reads the monochrome FNT v2/v3 bitmap resources of an NE executable and turns each lit pixel into a straight, grid-aligned TrueType outline. The result retains the original pixel shape, per-glyph advance, ascent and baseline; there is no smoothing or curve tracing. A FON with a single font resource produces `Name.ttf` (or the corresponding OTF/WOFF/WOFF2/EOT extension). Several resources produce one file per strike, such as `Name-13px.ttf`, `Name-13px-Bold.ttf`, `Name-13px-2.ttf` and `Name-14px.ttf`; the font family names distinguish heights, group different styles at the same height, and number only repeated styles at the same height. Existing output files follow the usual Skip, Rename and Overwrite policies independently for each strike. Invalid resources fail the whole input before output files are allocated. Character mapping uses the Windows single-byte code page indicated by the resource charset, not the computer's locale. Undefined bytes (including those that Windows decodes to Private Use code points in the Greek, Hebrew, Baltic and Thai code pages), non-round-tripping bytes and Unicode control characters retain their glyphs and spacing but receive no text mapping; charset hints cannot recover the meaning of custom glyphs. PE resources, vector or externally stored bitmaps, color FNT, standalone `.fnt` files and double-byte or unrecognized charsets are not supported. FON cannot be an output format. - Not supported: CFF2 (variable) outlines to TTF or EOT (to WOFF, WOFF2 and OTF they convert), writing font collections, and WOFF/WOFF2 extended metadata (dropped). `tests/Filee.Engines.Tests/FontTests.cs` checks every conversion with small OFL fonts in `tests/Filee.Engines.Tests/Fonts` (see `OFL.txt` there): round trips keep every table byte for byte, a WOFF2 made by Google's encoder decodes to the source font, and converted CFF glyphs are compared with the originals in Skia. `tests/Filee.Engines.Tests/FonTests.cs` builds synthetic FON samples in code (`FonBuilder`) and compares every rendered glyph with its source bitmap. ## DOCX writer The counterpart of the HWPX writer: `Hwp/Hwpx/Docx/DocxWriter*.cs` writes WordprocessingML by hand from the same document model, so every reader in `DocumentReaders` can produce an editable Word file without Word, LibreOffice or Pandoc. It writes: - sections with page size, orientation, margins (header and footer distances), columns, page number restarts; columns that change mid-page become continuous section breaks, as Word does it; - headers and footers (odd/even pages, "different first page") with PAGE / NUMPAGES fields; a section without its own header gets an empty one, since Word would otherwise repeat the previous section's; - paragraphs: alignment, indents (first line / hanging), spacing, line spacing (the model's percent of the font size is converted back to Word's multiple, the inverse of DocxReader), keep with next / keep lines / widow control, tab stops with leaders, page and column breaks; - runs: Latin and East Asian fonts, size, bold, italic, underline, strike, colour, highlight (Word's named colours) or shading, super/subscript, letter spacing; - headings as Word's built-in "heading 1".."heading 9" styles (outline levels, so the navigation pane and tables of contents work), lists as real numbering definitions (1., a., 가., ①, bullets, start numbers, nine levels); - tables with column widths, horizontal and vertical merges, borders, fills, cell alignment, row heights and repeated header rows; - pictures inline and floating (anchored to page, margin, column or paragraph, with wrapping), text boxes and rectangles / rounded rectangles / ellipses / lines as Word 2010 shapes (wps) with solid or gradient fills and outlines, hyperlinks (web and to bookmarks), bookmarks, footnotes and endnotes. Parts, relationships and content types are written per part (pictures and links in headers and notes are related from their own part), children in schema order. The tests validate every file with the Open XML SDK schema validator, read it back with DocxReader (DOCX → model → DOCX → model keeps the layout) and convert it with LibreOffice as an independent reader, see `tests/Filee.Engines.Tests/DocxWriterTests.cs`. Its edges cost 8, less than Pandoc's and LibreOffice's direct conversions, so Markdown → DOCX, PDF → DOCX, ... use it. DOCX → DOCX is no edge; the Word variants are the Office Open XML engine's job. ## PDF reader `Pdf/PdfDocumentReader*.cs` reads PDFs with PdfPig (Apache-2.0) into the document model, for PDF → DOCX, HWPX and (with `PdfTextConverter`) TXT and Markdown: - words with their positions, fonts, sizes and colours → lines → paragraphs: a line continues the paragraph above when it follows at about one line pitch, unless it starts with a bullet or number, is indented as a first line, or the previous line ended although the next word would have fitted; hyphenated line ends are joined, and no space is added between Chinese or Japanese lines; - tab stops: a line split at wide gaps ("1. RFP Published 08/14/2026") stays one paragraph with tabs and left tab stops where its parts start, unless a column gutter lies between them; a gap counts as a tab after a list number, when it is wider than 6 em, or when a row right above or below has a tab at the same place (a form's value column); - line breaks: in a paragraph narrower than its column (an address block), a line that ended although the next line's first word would have fitted keeps its line break; - reading order: gutters between columns are found where no narrow paragraph crosses; paragraphs that span the columns (titles, figures, closing text) split the page into bands, read column by column; - text that needs shaping (`PdfDocumentReader.ComplexText.cs`): an `/ActualText` of marked content replaces the glyphs it covers on every page (LibreOffice writes one for each cluster whose glyphs don't map to its characters one to one: conjuncts, vowel signs drawn before their consonant, ligatures). On a page with Arabic, Hebrew, Indic, Thai, ... text, words are built from the glyphs in content-stream order instead of by position, because marks have no width and overlap their letter: a mark joins the letter before it, a vowel sign drawn before its consonant (Word's PDFs keep that visual order) joins the consonant after it, and the dotted circles of PDFs made by rhwp (mapped to U+0002 or to a vowel sign, wider than any mark) are left out. Right-to-left words are read from the right, with numbers and Latin words in them left to right, a line's words likewise; a paragraph of mostly right-to-left lines is marked right to left (`HParaFormat.RightToLeft`), its alignment and indents measured from the right, and DOCX gets `w:bidi` on it and `w:rtl` on its Arabic or Hebrew runs (HWPX has no right-to-left paragraphs). Fonts keep their family names (`ArialUnicodeMS` → Arial Unicode MS, `NirmalaUI` → Nirmala UI, `NotoSansGujarati` → Noto Sans Gujarati). Not recovered: a cluster drawn as one glyph whose ToUnicode entry is wrong, in a PDF without ActualText (some Word PDFs), and the word spacing of rhwp's broken layouts; - headings from font sizes (short paragraphs clearly larger than the body text, the largest size is level 1), bold and italic from the fonts, colours, links from link annotations, alignment, indents, spacing and line pitch; - pictures: JPEG streams are kept as they are, others are decoded to PNG, and what PdfPig cannot decode is cut out of the rendered page; a picture covering the page behind text (the scan under an OCR layer) is skipped; - ruled tables (`PdfDocumentReader.Tables.cs`): tables drawn with lines become real tables in their place in the reading order. The rules come from the page's vector paths: stroked straight lines and rectangle edges, and thin filled rectangles (up to 4 pt, the way Word and many generators draw borders). Rules on one line (within 1.5 pt) that overlap or leave a gap of at most 2.5 pt are joined; horizontal and vertical rules that meet or cross form a grid, and grid lines closer than 3 pt are one edge. The edges give the columns and rows (the column widths and minimum row heights of the result); where a rule covers less than half of the edge between two grid units, the units are one merged cell (column and row spans). A table without side or bottom rules still reaches as far as most of its rules do. Words go into the cell under their centre and are read like the rest of the page (paragraphs, line breaks, alignment within the cell, fonts, sizes, bold / italic, colours); each cell gets the borders that are drawn around it (weight, colour, dashed or solid), shading from a filled rectangle under it, and a vertical alignment from where its text sits. Only clear tables are rebuilt: at least 2 rows and 2 columns, every merged cell a rectangle, no rule through the middle of a word, and text in at least two cells. A box around a note, underlines, separators and empty grids (a chart's frame) stay as they were. Not rebuilt: tables without rules (aligned by white space only, or with only horizontal rules), nested tables (their text stays in the outer cell), rotated pages; a table running over several pages becomes one table per page. PDF → TXT keeps the cell text in reading order; - page size per page and margins shared by the document; what sits at the top or bottom edge only because it is a page is left out: running headers and footers that repeat on most pages (digits ignored, so "Page 3" matches "Page 4"), running heads that change with the chapter (the same text at the same height on three pages or more, no larger than the body text, so a heading that starts several pages stays), bare page numbers ("12", "- 12 -", "Page 3 of 10", "第 3 页", roman "iv") and the same picture (a logo) at the same place on most pages; - a page without text (a scan, a page of drawings, a turned page) becomes the rendered page (PDFium, 150 dpi JPEG) as a picture behind the text of an empty page, so nothing is lost; there is no OCR. PDF → TXT includes the invisible OCR text layer of scanned PDFs that have one. Password-protected PDFs fail with a clear message. Not converted: tables without rules (their cells come out as paragraphs in reading order), other vector drawings on text pages, form fields and annotations other than links. **PDF → Markdown** is one step (`PdfTextConverter`, a `DirectOnly` edge, so PDF → HTML, EPUB, ... never start going through a Markdown file): the document is written by `MarkdownWriter` as for e-books, with headings (`#`, not bold inside), bold / italic and links. Indents are the PDF's layout, not block quotes, and the PDF's information title (often a file name) gives no front matter. A Markdown file can't hold pictures, so `Text/MarkdownPictures.cs` writes them into a folder next to it, reserved through the output allocator like any output (`report.md` → `report_files/img0001.jpg`, `report_files (2)` when that folder is taken), and links them relatively, so editors show them and the pair can be moved together; JPEGs are the PDF's own, a scanned page is the picture of the page. The preset option *Leave out pictures* (`DocumentOptions.LeaveOutPictures`, Documents card, shown for Markdown) writes the text only, read like PDF → TXT (the invisible OCR layer of scans included) and without a folder. The same option applies to every Markdown writer: e-books (`EbookConverter`), HWP / HWPX (`UnhwpConverter`) and Pandoc (a Lua filter drops the pictures, figures keep their captions). ## OFD reader OFD (GB/T 33190-2016) is China's fixed-layout format: fully digitalized e-invoices (数电票), contracts and official documents. `Ofd/OfdReader*.cs` reads it into the document model with no external engine, so OFD → DOCX and HWPX are one step, → PDF two (rhwp), and TXT, Markdown, HTML, EPUB follow from there. OFD is read only (`Writable: false`) and sits on the office donut (settings schema 27 teaches existing donuts `.ofd`). - **Package** (`Ofd/OfdPackage.cs`): OFD.xml → the first document (Document.xml) → its pages, template pages and resource files (PublicRes, DocumentRes, a page's PageRes, and the unnamed `Res.xml` WPS writes next to them). Parts are looked up by name inside the ZIP and never written out under their own names; references are resolved against the referring part (or the package root for "/…"), "." and ".." applied, and anything that leaves the package, a URL or a drive path finds nothing. XML is read without DTDs; a part may unpack to 64 MB, a picture to 32 MB (larger ones are left out), the whole package to 1 GB; at most 100,000 parts, 10,000 pages, 2,000,000 drawing objects, page blocks nested 32 deep. DeltaX / DeltaY "g n d" repeats never yield more values than the text has characters. - **Encryption**: a package with an `Encryptions.xml` (GM/T 0099) or encryption entries in OFD.xml is refused with a clear message; nothing is decrypted. Parts that are not XML (encrypted or damaged) fail the same way. - **Text**: TextObjects with their Boundary, CTM (scale, also of the font size), Size in millimetres, TextCode X / Y (missing ones continue where the previous code ended) and DeltaX advances; DeltaY alone moving the characters is vertical text. A DeltaX list shorter than the text goes on with its last value while the characters are as wide (a title with one advance for all its characters); a step back with DeltaX or down with DeltaY inside one TextCode starts a new line, kept with a line break (the four lines of a VAT invoice's 密码区 cipher text are one TextCode). Fonts from the resource files, with common Chinese names written in English (宋体 → SimSun, 黑体 → SimHei, 楷体 → KaiTi, 仿宋 → FangSong, 微软雅黑 → Microsoft YaHei, …) so Word and LibreOffice find them on any system; bold from `Weight` ≥ 600 or the font, italic, colour from the object (FillColor element or attribute; RGB, grey, CMYK, decimal or #hex channels), its DrawParam or the layer's DrawParam (tax invoices colour the template that way), following `Relative` chains. Invisible objects (`Visible="false"`) are left out. - **Templates**: the template pages a page names are read first, so an invoice's labels (发票号码:, 名称:) and its values come out together. In template content, single characters stacked in one column (购买方信息, 备注) or a pair spread over a line (合 计) become one word, once the page's own text is there: a pair with a value between (共 4 页 of a sales list) stays apart. - **Annotations** (Annots): the text and pictures of each annotation's appearance, placed from its boundary (a stamp drawn as a picture, a note). Hidden annotations and their paths (highlights, underlines) are left out. - **Seals** (`OfdReader.Seals.cs`): an e-invoice's or a contract's seal is stored in its signature (`Signs/Sign_n/SignedValue.dat`, GM/T 0031 SES_Signature). The seal picture in it is shown where the signature's StampAnnot puts it, floating in front of the text. PNG, JPEG, GIF and BMP are kept; a seal that is a small OFD package of its own (the red oval of 全国统一发票监制章 with its characters set round the edge) is drawn with SkiaSharp at 12 pixels per mm on a transparent PNG: text per character with its CTM rotation, stroked and filled paths (lines, Béziers, arcs) and pictures. The DER is walked with every length checked against what is left, nesting and the element count capped; a SignedValue is read up to 4 MB, a seal package unpacks to 16 MB, at most 64 signatures with 16 stamps each. The signature is not verified: what shows is the picture the file carries. - **Layout** (`OfdReader.Layout.cs`): fragments on one baseline form a line, in reading order top to bottom and left to right; a line that ran to the right edge from at least half the measure continues in the next one (justified, first-line indent kept, no space between Chinese lines). Wide gaps (1.5 em) become tabs with left tab stops where each part starts, so an invoice's item rows keep their columns; a line under such a row whose parts start in its columns and hold no amounts is the wrapped end of its cells and follows after a line break in the same paragraph. Centred and right-aligned lines, left indents and vertical distances (space before) follow the page; page size per page (a new section when it changes), margins from what is drawn. Line distances are exact where the largest text fits ("at least" grows each line to the font's own height in Word and LibreOffice, ~1.4 em for KaiTi, and a full 210 × 140 mm invoice ran onto a second page); a one-line paragraph closer to the one above than 1.2 sizes gets the distance between them. A block that starts above the end of the one before (the invoice number beside the title) is pushed down, and the next gaps take that back, so the frame stays at its place. A table gets its distance from the block above through a 1 pt paragraph, and is `Tight` in HWPX: no 5 mm under it and no empty line in the paragraph that holds it. The first block's distance from the top margin is an empty line of that height (rhwp draws no space before at the top of a page). - **Ruled tables** (`OfdReader.Tables.cs`): the approach of the PDF reader in millimetres. PathObjects (AbbreviatedData M/S/L/Q/B/A/C, with Boundary, CTM, line width and stroke colour from the object or its DrawParams) give stroked straight rules and thin filled rectangles; joined rules that meet form grids, and a grid of at least 2 × 2 units with rectangular merged cells and text in two cells becomes a table with the page's column widths and row heights, borders in the drawn colour and shading. Text goes into the cell of its first character (widths without DeltaX are estimated) and is laid out like the page, with exact line spacing so the rows keep their heights. The frame of an e-invoice becomes one table: buyer and seller blocks, the item list (one cell with tabbed rows: the invoice draws no rules between item columns), totals and remarks. A cell with one paragraph about as far from its top as from its bottom is centred; any other keeps its text's distance from the top as the cell's top padding (space before a cell's first paragraph is not drawn by every renderer), and only lines at about single spacing (1.6 sizes) are one paragraph. So the columns of a sales list (销货清单: rules only between the columns, each column one cell with all its rows) keep their rows level, the 小计 at the bottom of 序号 beside its amount. - **Pictures**: ImageObjects (MultiMedia of type Image) with the box from their CTM. PNG, JPEG, GIF and BMP are kept as they are (the format comes from the bytes, not the name), TIFF and JPEG 2000 are converted to PNG (ImageMagick, at most 100 megapixels), anything else (JBIG2) is left out. A picture alone in its band of the page becomes a paragraph; one beside or over text (a QR code next to the title, a seal on a table) floats at its place on the paper, in front of the text. Not converted: signature checks (a seal shows whether or not its signature is valid), seal pictures in SVG, embedded fonts (the font name is kept), composite objects and other vector drawings (the ⊗ before an invoice's amount in words), attachments, more than the first document of a package, tables without rules (their text stays in tabbed rows), clipping. Two text blocks side by side without rules share their lines. Renderers place text differently inside an exact line (Word and LibreOffice at its foot, 한글 and rhwp at its head), so in the rhwp PDF text sits up to 0.2 sizes higher than on the page. LibreOffice adds cell borders to row heights: the DOCX of a full 140 mm invoice opened there puts its last line on a second page (rhwp's PDF keeps it on one). ## PDF → XLSX `Pdf/PdfTablesConverter.cs` puts the tables of a PDF into a workbook (#267). The tables are the ruled ones PDF → DOCX finds (`PdfDocumentReader.Tables.cs`, above): drawn with lines, with their merged cells, column widths and row heights. Text outside the tables is left out, as the XLSX preset's hint says; tables aligned by white space only are not found. - **One sheet per table** ("Table 1", "Table 2" …). A table that goes on at the top of the next page with the same columns (widths within 5 %), right after one that ended its page, with no text between them, joins it on one sheet; a header row repeated on the new page is left out. - **Numbers and dates** become real values with a number format that shows them as the PDF did: `1,234.50` (`#,##0.00`), `(120)` (negative), `$9.99` / `€1,000.00` / `₩` `£` `¥` `₹` (the symbol in the format), `12.5%` (`0.0%`), and dates with the year first, `2026-10-23`, `2026/1/5`, `2026. 10. 23.` (Korean style). Everything else stays text: a leading zero (codes, phone numbers), more than 15 digits, a decimal comma (`1.234,50`, which can't be told from thousands), day- or month-first dates. - Every cell gets thin borders (the table was drawn with lines), its shading, vertical alignment and bold when all its text is bold; text wraps. Merged areas keep their borders on every covered cell. - A PDF with no such table, or a scan (no text), fails with `error.pdf_no_tables` instead of writing an empty workbook. The same tables can be written as ODS, or as CSV (one file per sheet). These edges are direct only: an image made into a PDF has no ruled tables, so no picture reaches a spreadsheet through PDF. ## DOCX / HTML → XLSX `Office/DocumentTablesConverter.cs` puts the tables of a Word document (DOCX and its DOCM / DOTX / DOTM variants), a web page (HTML) or a saved web page (MHTML) into a workbook (#1016). The tables are the ones the DOCX and HTML readers give the HWPX and DOCX writers, with their merged cells (`gridSpan` / `vMerge`, `colspan` / `rowspan`); text outside the tables is left out. The cells are written as in PDF → XLSX (`Office/Sheets/TableSheets.cs`, shared by both). - **One sheet per table** ("Table 1", "Table 2" …) in reading order, a table inside a cell too (its text isn't repeated in the outer cell). Nothing is joined: a Word table running over a page break is one table already. A table without any text, such as a web page's layout table around the real one, gets no sheet. - **Numbers and dates** become real values by the same rules as PDF → XLSX (above). - Cells keep their own borders (a Word table style's lines, the HTML grid unless `border="0"`), shading, vertical alignment and bold. Column widths come from the Word grid or the HTML `` / first-row widths (shares of an A4 text width); without either, a column is as wide as its longest line, up to 60 characters. Rows fit their text: a Word row height is only a minimum. Leading header rows (Word's "repeat as header row", HTML `` or rows of ``) are frozen. - A file without tables fails with `error.document_no_tables`. The edges are direct only, like PDF → XLSX: a PDF, a picture or an e-book that became a DOCX on the way doesn't reach spreadsheets this way, and a document's tables don't go on to JSON or XML. For the same reason DOC (which LibreOffice reads into a DOCX first) isn't an input: the XLSX preset's note says to convert it to DOCX first. ## PDF → PPTX `Pdf/PdfSlidesConverter.cs` turns a PDF into a PowerPoint deck of pictures: every page is rendered (PDFium, as for PDF → images) and becomes one picture on its own slide. The slides look exactly like the PDF but are **not editable**: there are no text boxes to change, the PDF's layout is not rebuilt. PDF → DOCX is the editable route. - **Slide size** is the first page's own size (an A4 portrait PDF gives A4 portrait slides, a PDF exported from a 16:9 deck gives 16:9 slides), within PowerPoint's limits of 1 to 56 inches a side. Every page, also one of another size or orientation (pages turned in the PDF are shown turned), is fitted onto the slide without cropping and centred on white. - **Resolution** is the preset's render DPI, at least 150 and at most 600; a page larger than 25 megapixels at that resolution (a poster) is rendered at a lower one. The preset's page range picks the pages. - **Pictures** are JPEG at the preset's quality (90 by default, as the "PPTX (slides)" preset sets it). Pages are rendered on white, as PDF viewers show them, so they have no transparency that would need PNG, and JPEG keeps a long deck at a size that is easy to send (an A4 text page at 150 dpi is a few hundred KB). - **Speaker notes** hold the page's text, read like PDF → TXT (reading order, the invisible OCR layer of scans included), so it can still be searched and copied. Pages without text get no notes; when PdfPig cannot read a PDF that PDFium renders, the deck is written without notes. - The package (`Pdf/PictureDeckWriter.cs`) is written by hand: presentation, one slide master with a blank layout, a theme, the slides with their pictures, a notes master and notes slides, and the document properties. The tests validate it with the Open XML SDK and read it back with Filee's own PPTX reader. - The edge is `DirectOnly`: the route planner uses it only for a PDF converted to PPTX. A Word document or an image is never turned into picture slides through PDF, and a picture deck is never a step towards ODP, PPSX or another format (a test checks that every other pair of formats keeps its route). Password-protected PDFs fail with the same message as PDF → DOCX. ## PDF compression "Compress PDF" (and any PDF → PDF preset with *Compress* set, also with a page range or split into files) makes a PDF smaller while it stays a PDF, for e-mail attachments and upload limits. `Pdf/PdfCompressor.cs` does it with PDFsharp and Magick.NET; nothing has to be installed. | Level | Pictures scaled down to | JPEG quality | |---|---|---| | Smallest (`Screen`) | 72 dpi | 55 | | Balanced (`Ebook`, the preset's) | 150 dpi | 65 | | High quality (`Print`) | 300 dpi | 85 | - **Pictures**: `Pdf/PdfPictureUses.cs` follows the content streams of the pages, of the forms they draw and of annotation appearances (`q`/`Q`/`cm`/`Do`) to find the largest size each picture is drawn at. A picture drawn at more than 1.5 × the level's resolution is scaled down to it (as Ghostscript does); a picture whose drawing can't be followed is taken as large as the page holds it, so it is never scaled down too far. Photos are saved as JPEG at the level's quality, pictures with up to 256 colours (charts, screenshots, logos) losslessly (Flate with PNG prediction). A JPEG that keeps its size is encoded again only when that saves at least 10 %. A picture is replaced only when the result is smaller. - **Which pictures**: 8 bits per component, gray, RGB (also ICC-based and calibrated) and CMYK, stored as JPEG, Flate (with or without PNG prediction), RunLength or uncompressed. CMYK JPEGs, 1-bit pictures and masks, indexed (palette) pictures, spot colours, JPEG 2000, JBIG2, CCITT and pictures with a colour-key mask stay as they are. - **Transparency**: a soft mask is scaled with its picture and kept lossless, so transparent parts stay transparent. - **Duplicates**: a picture stored more than once with the same data (a logo embedded again on every page) is kept once. - **Everything else**: every stream is deflated at the highest level when that's smaller; page thumbnails and other applications' private data (`PieceInfo`, such as Illustrator's editing data) are removed, as are unused objects. When the preset doesn't keep metadata, the XMP metadata of pages and pictures goes too. The document's own XMP is written afresh from its document information by PDFsharp, as for every PDF it saves. - **Unchanged**: text, fonts, vector drawings, links, bookmarks and form fields. The whole file is compressed as it is; a page range or single pages are first copied into new PDFs, as without compression (which leaves bookmarks and form fields behind), and so is a PDF locked only against changes. - **Never bigger**: when nothing gets smaller (a PDF of text only, one compressed before), the PDF is written as it was and the toast (and `filee convert`) says that it couldn't be made smaller. Password-protected PDFs fail with the same message as the other PDF conversions. - **Ghostscript** (optional, *Also try Ghostscript* in the preset): when it is installed, the PDF is also run through `pdfwrite` with `-dPDFSETTINGS=/screen`, `/ebook` or `/printer` (the same 72, 150 and 300 dpi) and `-dCompatibilityLevel=1.6`, and the smallest of the two results is kept; when Ghostscript fails, the built-in result stands. It is off by default: Ghostscript rewrites the whole file (fonts are subset again, structure and form fields may change), so it is the user's choice. It also writes object streams, which PDFsharp can't, and its JPEGs are somewhat smaller at the Balanced level. - **Not done**: object streams (PDFsharp writes none), font subsetting or removal of unused glyphs, converting colour to gray, flattening transparency or form fields, recompressing JBIG2/CCITT scans. ## Media tools Everyday presets, on the back faces of the images, PDF, video and audio donuts where they fit ("Video from images" is in the preset library only). Video and audio need FFmpeg. - **Compress image / video / audio** re-encode smaller (quality 75, never above a JPEG's own and PNG at zlib level 9; H.264 at the Small quality, at most 1080p; AAC at the Small quality). A copy in the same format that isn't smaller is replaced by the source, with the same notice as Compress PDF (`OutputRule.OnlyIfSmaller`); under "Save as ZIP" the source goes into the ZIP instead. - **Extract audio** copies AAC or ALAC into M4A without an encode (`MediaOptions.CopyAudioWhenPossible`); other sound is encoded to AAC. - **Merge videos** copies the clips with the concat demuxer only when all share the codec setup (ffprobe's extradata hash), size, pixel format, frame rate, turn and kind of sound; otherwise every clip is fitted into the first clip's frame and encoded, a clip without sound silent for its length. Sound tracks FFmpeg can't decode are skipped: each clip gives its first track FFmpeg can decode, and the clips are copied only when that track has the same number in every clip. **Merge audio** always decodes and joins the sound into one M4A, so no encoder padding or gap is left between files. Both are in `Media/MediaJoining.cs`. As a special case, a pair of self-contained `.m4s` files, one with only video and one with only audio, is muxed together into one MP4 without re-encoding (in either order). Both codecs and the preset's settings must allow copying. Other files without pictures dropped with the videos (#1402) are music: in their order, one after another, cut where the videos end (fading out over 1 s when still playing), at 60 % under the videos' own sound or as recorded when they have none. The pictures are still copied when they can be; only the sound is encoded. With "Loop music" (`MediaOptions.LoopMusic`, chosen in the merge order window) songs shorter than the videos are first decoded into one FLAC in the work folder, which then repeats with `-stream_loop -1`. - **Video from images**: each picture is fitted into 1920 × 1080 as a JPEG, then shown for the preset's seconds with crossfades (`xfade`) of 1 s, at most half a picture's time, or cuts. At most `MediaOptions.MaxSlides` (300) pictures, not counting downloads that failed; the queue refuses more before any is converted. A filter graph too long for a command line goes into a `-/filter_complex` file. - **Remove metadata** keeps the format. Images lose EXIF, XMP, IPTC, comments and the ICC profile, and a JPEG keeps its own quality. Video and audio are copied without global and per-stream tags, chapters and data tracks; an MKV's attachments (subtitle fonts) stay, with their file name and type. PDFs lose their document information and every XMP packet (`PdfSharpConverter.StripMetadata`), and PDFsharp writes a new one holding only its own name and the save date; pages, links, bookmarks and form fields stay. ## Pages per sheet (N-up) "Pages per sheet" (#737, `pdf-nup` group in the palette, `PdfOptions.PagesPerSheet` 2, 4, 6, 8 or 9; `Pdf/PdfSheets.cs`, PDFsharp only) puts several pages of a PDF on each sheet of a new PDF. - Each page goes in as a vector form (`XPdfForm`): text stays text, nothing is rasterised. It is placed by its crop box and its `/Rotate`, as a viewer shows it, clipped to the crop box, as large as fits in its cell and centred; a page that fits larger turned (a landscape page in a portrait cell) is turned a quarter to the left. - The sheet: A4, Letter, A3 or the size of the first page; portrait, landscape or (auto) whichever fits the first page larger. The long side of the sheet takes more pages (4-up 2 × 2, 6-up 2 × 3, 8-up 2 × 4, 9-up 3 × 3). - Options: order (across then down, or down then across), margin and gap in mm (cut down when too large for the sheet), a thin grey border around each page. The page range applies. - Only the page content comes along (as in any N-up): the values of filled form fields, link and comment annotations and bookmarks of the source are not on the sheets. ## PDF watermark, page numbers and password A preset that writes PDF can finish every PDF it makes (`PdfOptions.Watermark`, `PageNumbers`, `Password`; `Pdf/PdfFinishing.cs`, PDFsharp only). `JobQueue` runs it (`IPdfFinisher`) on each PDF a file became, whichever engine wrote it (PDFsharp, rhwp, LibreOffice, Ghostscript …), and once on a merged PDF, so its pages are numbered through. - **Watermark**: the text in light grey (22 % opacity), bold, along the page's diagonal over the content, at 70 % of the diagonal and at most a quarter of the short side high. - **Page numbers**: "3 / 10", 9 pt dark grey, centred 10 mm above the bottom edge. - Both go on the page as it is shown: centred in its crop box (the media box when it has none), the right way up for its `/Rotate`. The page is drawn on unturned and its `/Rotate` put back. - Text is drawn as glyph outlines (`Vector/GlyphOutlines.cs`, HarfBuzz shaping, shared with SVG text in PDFs). Any script works with a font the PC has for it (Skia's font fallback, else Arial or the default face), and no font is embedded, so the PDF grows by a few kilobytes. The text can't be selected or searched. - **Password**: opens the PDF (user and owner password are the same), AES-256 (`SetEncryptionToV5`). It is saved with the preset in the settings file, but left out of a preset export and of the copy history keeps to convert failed files again: a retry takes it from the preset again, and fails (`error.pdf_password_gone`) when the preset no longer has one. The editor takes at most 127 UTF-8 bytes, all that AES-256 readers use, and turns the box off while PDF/A is ticked, since PDF/A forbids encryption. ## PDF/A A preset with *PDF/A (archival)* ticked (`DocumentOptions.PdfA`; the built-in "PDF/A" preset, #1367) makes every PDF it writes a PDF/A-2b, PDF → PDF too. `JobQueue` runs `IPdfArchiver` (Ghostscript) after the finishing, on each PDF a file became and once on a merged PDF, so the box means the same whichever engine wrote the PDF. - **Already PDF/A**: LibreOffice (`SelectPdfVersion` 2) and SVG → PDF (Skia) write PDF/A themselves. A PDF whose catalog XMP has `pdfaid:part` is kept as it is, unless a watermark or page numbers were added since: then it is rewritten like any other. - **Ghostscript** (`GhostscriptConverter.ConvertToPdfAAsync`): `pdfwrite` with `-dPDFA=2 -dPDFACompatibilityPolicy=1 -sColorConversionStrategy=RGB`, after a small PostScript prelude (as Ghostscript's `PDFA_def.ps`) that adds the sRGB output intent; the profile is Magick.NET's sRGB, written into the prelude as a hex string so safe mode needn't read a file. Colours become RGB, fonts are embedded, what PDF/A forbids is left out (policy 1), and a tagged PDF loses its tags (part B doesn't need them). - **Without Ghostscript**: a preset whose files are all PDFs is greyed out with the usual engine reason. Other files are converted and the PDF is left as it is, with `notice.pdfa_needs_ghostscript` in the result, unless LibreOffice made it PDF/A. ## Office Open XML variants `Office/OoxmlConverter.cs` converts between the variants of a family (Word, Excel, PowerPoint) by copying the package with the main part's content type rewritten. Going to a macro-free type also removes the VBA project, its signatures and data, Word's key map customisations and Excel 4.0 macro sheets (with their sheet entries), and every relationship and content type entry pointing to them. The built-in readers take the variants directly (DOCM → HWPX, XLSM → CSV, PPSX → DOCX ...). ## E-mail `Email/EmlConverter.cs` reads .eml files with MimeKitLite (MIT), which decodes quoted-printable and base64, encoded headers (RFC 2047 / 2231) and charsets including EUC-KR / CP949 and ISO-2022-KR; raw 8-bit header text that is not UTF-8 is read as CP949, as older Korean mail programs wrote it. Outlook .msg files are read with MsgReader (MIT, `Email/OutlookMessage.cs`) and rebuilt as a MIME message with MimeKit: sender, To / Cc / Bcc, subject, sent date, Message-ID, the text and HTML bodies (MsgReader takes the HTML out of RTF-only messages), attachments, pictures the HTML shows (`cid:`, kept with the body) and attached Outlook items (as attached .eml messages). That message then goes the EML way below, so MSG → HTML / TXT / ZIP are direct edges (cost 10; through EML would be 20 and parse twice) and **MSG → EML** writes the rebuilt message. Attachments that only link to a file elsewhere (`ATTACH_BY_REFERENCE`) are not followed. MsgReader brings OpenMcdf (MPL-2.0, the compound file reader), RtfPipe (MIT) and UTF.Unknown (MPL-1.1, charset detection for old ANSI messages). - **EML → HTML**: a standalone page with From / To / Cc / Date / Subject / Attachments and the HTML body (its style sheets kept, scripts removed) or the text body; pictures sent inline (`cid:`) become data URIs. - **EML → TXT**: the same headers and the text body (or the HTML body as text). - **EML → ZIP**: the attachments (attached messages as .eml) with safe, unique file names. ### MHTML web archives A web page saved as "Webpage, Single File" (`.mhtml` from Chrome and Edge, `.mht` from Word and Internet Explorer) is a MIME message like an e-mail: the page and its pictures, style sheets and frames as parts of a multipart/related body (RFC 2557). `Email/MhtmlArchive.cs` reads it with the same MimeKit parser and writes the page as **one HTML file** (`MHTML → HTML`, engine `email`); the HWPX and DOCX writers read archives directly through the same code (`DocumentReaders`), so MHTML → DOCX / HWPX is one step and → PDF two (rhwp), and TXT, Markdown, EPUB and the other HTML targets go on from the page. - **The page** is the root of multipart/related (its `start` parameter, else the first part), else the first text/html part. Its charset parameter decodes it; without one (Chrome writes none) or with a wrong "us-ascii" over 8-bit text, the BOM, `` or UTF-8 decide, as for HTML files. Quoted-printable and base64 are undone by MimeKit. - **References** are matched against the parts' Content-Location, as written or resolved against the page's own location or its `` (Chrome: `https://…`; Word: `file:///C:/…/page_files/image001.png` named relatively from the page), and `cid:` against Content-ID (Chrome names its style sheets `cid:css-…@mhtml.blink`). These are compared as text only: nothing is downloaded, and no Content-Location is ever opened or written as a file. - **Pictures** of the archive (`img src`, `srcset`, `` sources, the `background` attribute and CSS `url()`) become data URIs; raster only (PNG, JPEG, GIF, WebP, BMP, AVIF, also when saved as application/octet-stream), not SVG. Linked style sheets the archive holds are inlined as `