找回密码
立即注册
搜索
发回帖 发新帖

5395

积分

0

好友

695

主题
发表于 1 小时前 | 查看: 11| 回复: 0

上周收到一份 OFD 格式的电子发票,想转成 PDF 归档。我习惯性地把文件拖进浏览器里的在线转换站,结果先弹广告、再要求上传。一张几百 KB 的发票,得从别人服务器过一遍。我直接把页面关了。

文件转换这件事存在了二十多年,桌面端的选择却一直别扭:要么装几百兆的 Office,要么把文件交到网上。Filee 想解决的,正是这一小段摩擦。

Filee 官方 banner:右侧是环形格式转换菜单

图 1:Filee 官方 banner,右侧是它标志性的环形格式菜单。图片来源:项目仓库 docs/media/banner-zh-CN.jpg。

它是什么

Filee 是一个免费、开源、完全离线的文件转换器,官网给它的定位只有一句话:一个靠拖拽完成工作的文件转换器。

交互方式很特别。在资源管理器里按住 Ctrl,把文件拖起来,光标旁边会展开一个圆环,环上全是这个文件能转成的格式;把文件丢到某一格,转换就开始,结果保存在原文件旁边。

整个过程没有主窗口,没有“打开软件”这一步。你人在哪,工具就在哪。

它覆盖 255 种格式:图片、PDF、Word、Excel、PowerPoint、HWP、电子书、压缩包、字体、CAD、3D 模型、地理矢量数据。

更关键的是,这些转换不需要装 Office,也不需要装 Hancom Office 或 LibreOffice。只有少数冷门格式才需要额外的可选引擎。

Filee 拖拽转换演示:光标旁弹出格式圆环

图 2:拖起文件后,光标处弹出格式圆环,松手即开始转换。动图来源:项目仓库 docs/media/demo-zh-CN.webp。

它不只是一个图形界面玩具。同一个安装包还带一个命令行程序 filee,能把同样的转换能力接进脚本和流水线。

我认为这才是它真正的价值所在:图形界面负责日常的小麻烦,命令行负责批量的大麻烦,两者共用一套引擎。

用什么写的

Filee 用 C# 写成,基于 .NET 10,界面框架是 Avalonia。

这个组合值得说一句。它没有走 Electron 那条路,不是把浏览器打包进桌面,而是一个原生的 .NET 桌面程序,启动快、内存占用稳,Windows、Linux、macOS 三个平台共用一套界面代码。

底层依赖基本都是各自领域里最成熟的那一批:

  • 图像处理用 Magick.NET,覆盖 JPEG XL、PSD、相机 RAW、DICOM 这些;
  • PDF 用 PDFsharp、PDFium、PdfPig,分别负责写、渲染、抽文字;
  • 矢量图用 Svg.Skia 和 SkiaSharp;
  • CAD 用 ACadSharp;
  • 邮件用 MimeKit 和 MsgReader;
  • 表格读取用 ExcelDataReader;
  • 3D 模型用 SharpGLTF;
  • Markdown 用 Markdig;
  • 数据格式用 System.Text.Json、YamlDotNet、Tomlyn。

两个外部程序是随安装包一起发布的:rhwp(Rust 写的 HWP 渲染器)和 7-Zip。它们体积小,跟着程序走,保证换一台电脑行为一致。

体积大的引擎则做成可选下载:FFmpeg 处理音视频,LibreOffice 处理 DOC、PPT 这类老格式,calibre 处理冷门电子书,Ghostscript 处理 EPS/PS,Pandoc 处理 Markdown 与各种标记语言。

这套分层让安装包保持在几十兆,同时又能覆盖 255 种格式。

架构与实现原理

翻完它的架构文档,我发现整个项目其实只围绕一个核心模型展开:把格式当节点,把转换器当带权重的边,然后做图搜索。

这条思路解释了很多看起来“凭空出现”的能力。

比如 HWPX 转 PNG。没有任何一个引擎直接支持这件事,但 Filee 会自己拼出一条两步路线:先用 rhwp 把 HWPX 渲染成 PDF,再用 PDFium 把 PDF 页转成 PNG。

同理,你只要新增一条 epub → pdf 的边,epub → png、epub → jpg 这些路线会自动解锁,不需要改别的代码。

路径规划器是这么写的:

// Dijkstra over formats. The graph is small (a few hundred nodes).
var dist = new Dictionary<string, int> { [from] = 0 };
var prev = new Dictionary<string, (string Node, RouteStep Step)>();
var hops = new Dictionary<string, int> { [from] = 0 };
var queue = new PriorityQueue<string, int>();
queue.Enqueue(from, 0);

while (queue.TryDequeue(out var node, out var cost))
{
    if (cost > dist[node])
        continue; // stale queue entry
    if (node == to)
        break;
    if (hops[node] >= MaxSteps)
        continue;

    foreach (var (converter, edge, edgeCost) in EdgesFrom(node))
    {
        if (edge.To == edge.From || edge.FrameSequence || edge.MotionPhoto || edge.LivePhoto
                || (edge.ImageSourcesOnly && !imageSource) || (edge.NotForImageSources && imageSource)
                || (edge.Unshaped && shaping)
                || (edge.DirectOnly && (node != from || edge.To != to)))
            continue;

        var next = cost + edgeCost;
        if (dist.TryGetValue(edge.To, out var known) && known <= next)
            continue;

        dist[edge.To] = next;
        hops[edge.To] = hops[node] + 1;
        prev[edge.To] = (node, new RouteStep(converter, edge.From, edge.To, Unshaped: edge.Unshaped));
        queue.Enqueue(edge.To, next);
    }
}

这段代码在 src/Filee.Core/Conversion/RoutePlanner.cs 里,是真实实现,不是示意。

几个细节很见功力。

最大跳数被硬限制在 3。 官方注释写得很直白:更长的链路几乎必然是又慢又有损。这条约束挡住了一堆“能转但结果不能看”的组合。

每条边可以带限制条件。 图片描摹(转 SVG)只允许从位图出发,标成 ImageSourcesOnly;PDF 转 PPTX 的图片版只允许作为独立路线,标成 DirectOnly,永远不能当中转站。

引擎优先级不改变跳数,只做同分裁决。 用户可以在设置里调整引擎顺序,这个顺序被换算成 0 到 9 的微小代价,加到边上。它只用来打破平局,不会让原本两步的路线变成三步。

文本整形是个显式排除项。 rhwp 从左到右逐字绘制,不会做阿拉伯文、希伯来文、印度语系的连写整形。所以当源文件含有这类文字时,任务队列会重新规划一条绕开 Unshaped 边的路线,宁可变慢,也不产出错乱的 PDF。

再看执行层。JobQueue 按每个引擎声明的 MaxParallelism 控制并发,中间文件放在临时目录,最终输出由路径分配器统一命名——源文件永远不被覆盖,重名时自动加编号。

整个程序分成三层:Filee.App 是 Avalonia 界面与拖拽交互,Filee.Core 是格式注册表、路线规划、任务队列、预设与输出规则,Filee.Engines 是各种转换器的实现。命令行是第四层 Filee.Cli,它复用同一套 Core 和同一份预设,只是没有界面。

Filee 环形工具栏配置界面

图 3:环形工具栏可以按文件类型配置,每一面放 12 格,正面是常用格式,背面是冷门格式和工具。图片来源:项目仓库 docs/media/page-toolbar-zh-CN.png。

还有一个细节我挺喜欢:“不装 Office 也能把 DOCX 转 PDF”是靠自己实现渲染链做到的。

内置的读取器把 DOCX、XLSX、PPTX 解析成文档模型,由 HWPX 写入器写成 HWPX,再交给 rhwp 渲染成 PDF。整条链路在进程内或随包程序里完成,不调用任何系统上已安装的办公软件。

可选引擎的治理也很克制。每个引擎的下载地址、SHA-256、体积都固定写在 engines.json 里,安装前校验哈希,解压到暂存目录,完整之后才整体替换。

引擎装在 %LOCALAPPDATA%\Filee\engines,不在程序目录里,所以升级程序不会丢引擎,卸载程序又会一并清掉。

Filee 转换引擎按需安装设置界面

图 4:只有音视频、老式文档、冷门电子书才需要额外引擎,安装时按需勾选。图片来源:项目仓库 docs/media/engine-setup-en.png。

怎么部署

有三种拿法:装发行版、用便携包、从源码构建。

Windows 10 / 11

从 Releases 页面下载 Filee-1.12.0-win-Setup.exe,双击安装。

安装程序是 Filee 自己写的(src/Filee.Setup),不是 Inno Setup 套壳,所以界面语言和主程序一致,也不需要预装 .NET 运行时。

需要静默安装的话,它兼容 Inno Setup 的参数:

.\Filee-1.12.0-win-Setup.exe /VERYSILENT /SUPPRESSMSGBOXES /NORESTART

不想安装就用便携版,解压后直接运行:

Expand-Archive .\Filee-1.12.0-win-Portable.zip -DestinationPath C:\Tools\Filee
C:\Tools\Filee\Filee.exe

安装时可以选择把 filee 命令加进 PATH,也可以勾选资源管理器右键菜单。

Linux(x64 / ARM64)

要求 Ubuntu 22.04 及以上,不需要 root,但需要 Python 3:

tar -xzf Filee-1.12.0-linux-x64.tar.gz
cd Filee-1.12.0-linux-x64
./install.sh

拖拽手势依赖 X11。Wayland 下环形菜单的“按住按键拖拽”用不了,但主页的拖放区和文件管理器右键菜单仍然可用。

macOS 13+

目前是预览版,包没有做公证:

tar -xzf Filee-1.12.0-osx-arm64.tar.gz
cd Filee-1.12.0-osx-arm64
./install.sh

第一次启动会被系统拦下,需要到“系统设置 → 隐私与安全性”里点“仍要打开”。

从源码构建

需要 .NET 10 SDK。仓库的构建脚本是 PowerShell 写的,Linux 和 macOS 上装个 pwsh 就能跑:

git clone https://github.com/KnifeLemon/Filee.git
cd Filee

# 拉取随包引擎,只取需要的两个,避免下载全部
pwsh build/fetch-engines.ps1 -Only rhwp,pandoc

# 启动桌面程序
dotnet run --project src/Filee.App

# 跑测试
dotnet test

fetch-engines.ps1 读取的也是 engines.json,所以开发环境和正式发行版用的是同一批引擎版本,不会出现“本地能转、装完不能转”的错位。

怎么用

下面这些命令都是官方文档里的真实用法,我把参数补齐成了可以直接跑的形式。

场景一:把一堆 HEIC 和 RAW 批量转成 JPG

iPhone 拍的照片默认是 HEIC,很多老系统打不开。单个转换很简单:

filee convert IMG_2041.heic --to jpg

整个目录连同子目录一起处理,输出到指定文件夹,并保留原始时间戳:

filee convert D:\Photos --recursive --to jpg -o D:\Photos\jpg --quality 88 --keep-dates

--quality 接受 1 到 100,只对 JPG、WebP、AVIF 这类有损格式生效。

同名文件默认不会被覆盖,Filee 会自动加编号;想强制覆盖就加 --overwrite,想跳过已存在的就加 --skip。

场景二:扫描件合成 PDF,顺手压一压

把一叠扫描图片转成 PDF,用内置预设 to-pdf:

filee convert D:\Scans --recursive --preset to-pdf -o D:\Scans\pdf

已有的多个 PDF 想合并成一个:

filee convert D:\Reports\*.pdf --preset merge-pdf -o D:\Reports\merged

PDF 太大想压缩:

filee convert D:\Reports\merged\*.pdf --preset compress-pdf -o D:\Reports\small

预设不只在图形界面里存在,filee presets 能列出全部预设的 id,命令行直接按 id 或名字调用。

场景三:处理中文办公格式

OFD 是国内电子发票和公文的常用格式,很多工具读不了。Filee 内置了 OFD 读取器,可以直接转 PDF 或 DOCX:

filee convert 发票202610.ofd --to pdf
filee convert 发票202610.ofd --to docx

韩文办公文档 HWP 也能处理,靠的是随包的 rhwp:

filee convert 보고서.hwp --to docx
filee convert 보고서.hwp --to hwpx

表格类文件同样可以进出:

filee convert 台账.xlsx --to csv
filee convert 台账.xlsx --to json

场景四:盯着一个文件夹,来了就转

这是我觉得最实用的一条命令。下载目录里不断有文件进来,让它们自动变成 PDF:

filee watch D:\Inbox --to pdf --move-originals --settle 3

它会等文件大小和修改时间稳定 3 秒、并且没有被其他程序占用之后才动手,避免转到一半的文件。

转换结果默认写进 D:\Inbox\converted,原件被移到 D:\Inbox\originals。

为了不让“输出又被当成新输入”造成死循环,它会在被监视目录里写一个 .filee-watch-<hash>.json 状态文件,记录自己生成过哪些文件。这个状态文件在重启之后依然有效。

场景五:把 CLI 接进脚本

--json 会输出一份结构化结果,适合给流水线判断。字段结构在源码里是固定的:preset、target、converted、skipped、failed,以及 files 数组,每项含 source、state、outputs,失败时多一个 error。

一段可以直接跑的 PowerShell:

$result = filee convert D:\Scans --recursive --preset to-pdf -o D:\Scans\pdf --json | ConvertFrom-Json

Write-Host ("转换完成 {0} 个,跳过 {1} 个,失败 {2} 个" -f $result.converted, $result.skipped, $result.failed)

foreach ($file in $result.files | Where-Object { $_.state -eq 'failed' }) {
    Write-Warning ("{0} 失败:{1}" -f $file.source, $file.error)
}

if ($result.failed -gt 0) { exit 1 }

退出码本身也有语义:0 全部成功,1 有文件失败,2 参数错误或输入不存在,3 没有可转换的文件。

如果要在 C# 服务里调用,用进程封装就够了,不需要任何 SDK:

using System.Diagnostics;
using System.Text.Json;

var psi = new ProcessStartInfo("filee")
{
    RedirectStandardOutput = true,
    RedirectStandardError = true,
    UseShellExecute = false,
};

foreach (var arg in new[] { "convert", @"D:\Scans", "--recursive", "--preset", "to-pdf", "--json" })
    psi.ArgumentList.Add(arg);

using var process = Process.Start(psi)!;
var stdout = await process.StandardOutput.ReadToEndAsync();
await process.WaitForExitAsync();

using var document = JsonDocument.Parse(stdout);
var root = document.RootElement;

Console.WriteLine($"converted={root.GetProperty("converted").GetInt32()}");
Console.WriteLine($"failed={root.GetProperty("failed").GetInt32()}");

foreach (var file in root.GetProperty("files").EnumerateArray())
{
    var state = file.GetProperty("state").GetString();
    if (state == "failed")
        Console.WriteLine($"{file.GetProperty("source")} -> {file.GetProperty("error")}");
}

return process.ExitCode;

如果你需要的是异步批量处理,把多个目录拆成多个进程并发跑,比单进程塞一个大任务更容易控制失败边界。

场景六:给它加一种转换

项目把扩展路径写得很清楚:一个转换器就是一个实现 IConverter 的类。

public sealed class SuperEpubConverter : IConverter
{
    public string Id => "superepub";
    public string DisplayName => "SuperEpub";
    public int MaxParallelism => 0; // 0 = 每个 CPU 核心一个

    public IReadOnlyList<ConversionEdge> Edges { get; } =
        [
            new("epub", "pdf"),
        ];

    public EngineStatus GetStatus() => EngineStatus.Available();

    public Task<IReadOnlyList<string>> ConvertAsync(
            ConversionStep step, IProgress<double>? progress, CancellationToken ct) =>
            Task.Run<IReadOnlyList<string>>(() =>
            {
                var output = step.Output.Allocate("pdf");
                if (output is null)
                    return [];

                SuperEpub.Convert(step.InputPath, output);
                progress?.Report(1);
                return [output];
            }, ct);
}

然后在 EngineRegistry.CreateAll 里加一行注册,这条边就进入了全局格式图,所有经过它的路线立刻可用。

规则有三条:不许修改输入文件;输出路径只能从 step.Output.Allocate 拿;外部程序必须走 ProcessRunner,并且只找随包或用户安装的引擎,不去翻系统 PATH。

License 与项目地址

Filee 使用 MIT 协议,版权归 Myeonggu Kim(KnifeLemon)及贡献者所有。

随包和下载的第三方组件各自保留原协议,全部列在仓库的 THIRD-PARTY-NOTICES.md 里。

我的看法

我一开始以为这只是个把转换按钮做得好看一点的桌面小工具。

看完架构文档我改了判断。它真正的设计是把“格式”抽象成图上的节点,让每新增一个引擎,自动解锁一整片转换能力。 这个模型不难,但愿意在最开始就把模型想清楚的项目不多。

另一个让我意外的地方是它对边界的诚实。文档里明确写了 rhwp 不做复杂文本整形、Wayland 下拖拽不可用、macOS 包还没公证、PDF 转 PPTX 只是把每页贴成图片。

这些限制很多项目会藏起来,它写在文档正文里。

不适合谁也要说清楚:它不做 OCR,扫描件里的文字不会自动识别成可编辑文本;它不是 SDK,不能当成库嵌进你的程序,只能通过命令行调用;它也不做云端协作,天然是单机工具。

适合谁:需要批量处理素材的设计和运营;要把文档转换接进构建流水线或数据管道的后端;经常收到 OFD、HWP、WPS 这类格式的国内办公场景;以及想读一个结构干净的 .NET 桌面项目的人。

回到开头那张发票。现在我把文件拖起来,丢到 PDF 那一格,两秒钟,结束。

没有上传,没有广告,也没有装 Office。




上一篇:开源AI剪辑工具Concat:离线字幕与抠图,免费替代剪映
下一篇:C++全局变量加载与初始化顺序:SIOF成因与规避实践
您需要登录后才可以回帖 登录 | 立即注册

手机版|小黑屋|网站地图|云栈社区 ( 苏ICP备2022046150号-2 )

GMT+8, 2026-10-23 11:56 , Processed in 0.084786 second(s), 38 queries , Gzip On.

Powered by Discuz! X3.5

© 2025-2026 云栈社区.

快速回复 返回顶部 返回列表