From 436991e14261a069acce72abdefa83d092d7ea96 Mon Sep 17 00:00:00 2001 From: cre8ive-marketing <168519979+cre8ive-marketing@users.noreply.github.com> Date: Thu, 22 Oct 2026 12:57:35 +0530 Subject: [PATCH 01/11] Recognize text in scans with Tesseract (#288), work in progress Engine package, language downloads, OCR routes, PDF reader OCR layer, tests. Still to do: preset editor UI, translations, Searchable PDF preset, docs. --- build/fetch-engines.ps1 | 16 + src/Filee.App/Services/PresetAvailability.cs | 7 +- src/Filee.Core/Conversion/IConverter.cs | 14 +- src/Filee.Core/Conversion/JobQueue.cs | 12 +- src/Filee.Core/Conversion/RoutePlanner.cs | 52 ++- src/Filee.Core/Presets/Preset.cs | 22 ++ src/Filee.Engines/EngineRegistry.cs | 2 + src/Filee.Engines/Filee.Engines.csproj | 2 + src/Filee.Engines/Hwp/Hwpx/DocumentReaders.cs | 6 +- src/Filee.Engines/Hwp/Hwpx/HwpxConverter.cs | 3 +- .../Infrastructure/EngineDownloads.cs | 12 +- .../Infrastructure/EngineEnvironment.cs | 6 +- .../Infrastructure/EngineInstaller.cs | 37 +- src/Filee.Engines/Infrastructure/engines.json | 7 + .../Infrastructure/ocr-languages.json | 351 ++++++++++++++++++ src/Filee.Engines/Ocr/OcrLanguages.cs | 110 ++++++ src/Filee.Engines/Ocr/TesseractConverter.cs | 219 +++++++++++ .../Office/DocxWriterConverter.cs | 3 +- src/Filee.Engines/Pdf/PdfDocumentReader.cs | 27 +- src/Filee.Engines/Pdf/PdfTextConverter.cs | 6 +- tests/Filee.Core.Tests/OcrRouteTests.cs | 100 +++++ .../EngineInstallerTests.cs | 31 ++ tests/Filee.Engines.Tests/OcrTests.cs | 167 +++++++++ 23 files changed, 1179 insertions(+), 33 deletions(-) create mode 100644 src/Filee.Engines/Infrastructure/ocr-languages.json create mode 100644 src/Filee.Engines/Ocr/OcrLanguages.cs create mode 100644 src/Filee.Engines/Ocr/TesseractConverter.cs create mode 100644 tests/Filee.Core.Tests/OcrRouteTests.cs create mode 100644 tests/Filee.Engines.Tests/OcrTests.cs diff --git a/build/fetch-engines.ps1 b/build/fetch-engines.ps1 index a8383acf2..e09267842 100644 --- a/build/fetch-engines.ps1 +++ b/build/fetch-engines.ps1 @@ -18,6 +18,8 @@ ffmpeg/ FFmpeg (GPL-3.0 build, separate program), video and audio; bin/ffmpeg.exe + bin/ffprobe.exe calibre/ calibre (GPL-3.0, separate program), extracted from the official MSI via an administrative install: ebook-convert for LIT, LRF, PDB, ... and MOBI / AZW3 output + tesseract/ Tesseract OCR (Apache-2.0, separate program), UB Mannheim's Windows build unpacked from its + installer with 7-Zip (never run); the language data is downloaded by the app when needed Every download is pinned to a version and verified with SHA-256 (src/Filee.Engines/Infrastructure/engines.json, shared with the app). Downloads are cached in build/.cache. @@ -253,6 +255,20 @@ if ('ghostscript' -in $selected) { Copy-VcRuntime $target } +if ('tesseract' -in $selected) { + # Trimmed like EngineInstaller.TrimTesseract: no training programs, manuals, installer files or language data. + $setup = Get-Engine 'tesseract' + $sevenZip = Join-Path $Destination '7zip\7z.exe' + if (-not (Test-Path $sevenZip)) { throw 'Tesseract is unpacked with engines/7zip: use -Only 7zip,tesseract.' } + $target = Join-Path $Destination 'tesseract' + Reset-Folder $target + & $sevenZip x -y "-o$target" $setup | Out-Null + if ($LASTEXITCODE -ne 0) { throw "7-Zip could not unpack the Tesseract installer (exit code $LASTEXITCODE)." } + foreach ($folder in '$PLUGINSDIR', 'tessdata') { Remove-Item -LiteralPath (Join-Path $target $folder) -Recurse -Force -ErrorAction SilentlyContinue } + Get-ChildItem $target -File | Where-Object { $_.Extension -in '.exe', '.html' -and $_.Name -ne 'tesseract.exe' } | Remove-Item -Force + if (-not (Test-Path (Join-Path $target 'tesseract.exe'))) { throw 'tesseract.exe not found in the installer.' } +} + if ('calibre' -in $selected) { $msi = Get-Engine 'calibre' $tmp = Join-Path $cache 'calibre-admin' diff --git a/src/Filee.App/Services/PresetAvailability.cs b/src/Filee.App/Services/PresetAvailability.cs index 55ee80f69..7ea02e061 100644 --- a/src/Filee.App/Services/PresetAvailability.cs +++ b/src/Filee.App/Services/PresetAvailability.cs @@ -113,15 +113,16 @@ private PresetJudgement Evaluate(Preset preset, IReadOnlyList format bool Possible(RoutePlanner planner) => formats.Any(f => { var target = TargetFor(f); - if (merge && f.Id == "pdf" && !turns) + if (merge && f.Id == "pdf" && !turns && !preset.RecognizesTextOf(f.Id)) return true; // merged as it is var frames = preset.SavesFramesOf(f.Id); var motion = preset.ExtractsMotionOf(f.Id); var live = preset.MakesLivePhotoOf(f.Id); + var ocr = preset.RecognizesTextOf(f.Id); if (!turns) - return planner.CanConvert(f.Id, target, frames, motionPhoto: motion, livePhoto: live); + return planner.CanConvert(f.Id, target, frames, motionPhoto: motion, livePhoto: live, ocr: ocr); // Rotate / flip: a route only counts when an engine on it turns images (not a PDF or a video copied). - if (planner.Plan(f.Id, target, frames, motionPhoto: motion, livePhoto: live) is not { } route) + if (planner.Plan(f.Id, target, frames, motionPhoto: motion, livePhoto: live, ocr: ocr) is not { } route) return false; if (route.FirstTransformStep() >= 0) return true; diff --git a/src/Filee.Core/Conversion/IConverter.cs b/src/Filee.Core/Conversion/IConverter.cs index 3a84b9d86..4212cae56 100644 --- a/src/Filee.Core/Conversion/IConverter.cs +++ b/src/Filee.Core/Conversion/IConverter.cs @@ -30,7 +30,7 @@ namespace Filee.Core.Conversion; /// /// The edge writes every frame of a video as numbered pictures into a folder (). /// It is never part of an ordinary route (a video → JPG stays one still frame), and with -/// asking for frames it is the whole route. +/// asking for frames it is the whole route. /// /// /// The engine draws text one character after the other, left to right, without complex-script shaping (no joined @@ -41,16 +41,22 @@ namespace Filee.Core.Conversion; /// /// The edge takes the video out of a Live / Motion Photo (). It is /// never part of an ordinary route (a photo → MP4 is otherwise a video of the still picture), and with -/// asking for it it is the whole route. +/// asking for it it is the whole route. /// /// /// The edge makes a Live / Motion Photo of a short clip of a video (). It is /// never part of an ordinary route (a video → JPG is otherwise one still frame), and with -/// asking for it it is the whole route. +/// asking for it it is the whole route. +/// +/// +/// The edge recognizes the text of a scan or a picture and writes a searchable PDF (). +/// It is never part of an ordinary route, and with +/// asking for it the route starts with it. /// public readonly record struct ConversionEdge( string From, string To, int Cost = 10, bool ImageSourcesOnly = false, bool NotForImageSources = false, - bool DirectOnly = false, bool FrameSequence = false, bool Unshaped = false, bool MotionPhoto = false, bool LivePhoto = false); + bool DirectOnly = false, bool FrameSequence = false, bool Unshaped = false, bool MotionPhoto = false, bool LivePhoto = false, + bool Ocr = false); /// /// A conversion an engine refuses with a reason the user can act on, shown in their language (the file's diff --git a/src/Filee.Core/Conversion/JobQueue.cs b/src/Filee.Core/Conversion/JobQueue.cs index 9cf06228e..11f180d58 100644 --- a/src/Filee.Core/Conversion/JobQueue.cs +++ b/src/Filee.Core/Conversion/JobQueue.cs @@ -236,8 +236,9 @@ await Parallel.ForEachAsync(Enumerable.Range(0, job.Files.Count), var file = job.Files[i]; if (Stopped(file)) return; - // An empty PDF goes the long way, where it fails as empty instead of breaking the whole merge. - if (FormatRegistry.Detect(file.SourcePath)?.Id == "pdf" && !IsEmptyFile(file.SourcePath)) + // An empty PDF goes the long way, where it fails as empty instead of breaking the whole merge; so does + // one whose text is recognized first. + if (FormatRegistry.Detect(file.SourcePath)?.Id == "pdf" && !IsEmptyFile(file.SourcePath) && !job.Preset.RecognizesTextOf("pdf")) { // Already a PDF, merged as it is: its pages can't be turned (only images going into it can). if (job.Preset.Image.TurnsOrMirrors) @@ -497,7 +498,10 @@ private async Task> RunFileAsync( var frames = finalOutput && job.Preset.SavesFramesOf(source.Id); var motionPhoto = finalOutput && job.Preset.ExtractsMotionOf(source.Id); var livePhoto = finalOutput && job.Preset.MakesLivePhotoOf(source.Id); - var route = planner.Plan(source.Id, target, frames, motionPhoto: motionPhoto, livePhoto: livePhoto); + // "Recognize text": Tesseract writes a searchable PDF first, read on into DOCX, TXT, … from there. Also for the + // PDFs that are merged into one. + var ocr = (finalOutput || target == "pdf") && job.Preset.RecognizesTextOf(source.Id); + var route = planner.Plan(source.Id, target, frames, motionPhoto: motionPhoto, livePhoto: livePhoto, ocr: ocr); if (route is null) return Fail(file, "error.no_route", $"{source.Id} → {target}"); @@ -508,7 +512,7 @@ private async Task> RunFileAsync( { _log.LogInformation("{File} has {Script} text, which {Engine} can't draw: planning a route without it", file.SourcePath, script, route.Steps.First(s => s.Unshaped).Converter.DisplayName); - route = planner.Plan(source.Id, target, frames, shaping: true); + route = planner.Plan(source.Id, target, frames, shaping: true, ocr: ocr); if (route is null) return Fail(file, "error.needs_shaping", script); } diff --git a/src/Filee.Core/Conversion/RoutePlanner.cs b/src/Filee.Core/Conversion/RoutePlanner.cs index b19047eb6..4c89f86f0 100644 --- a/src/Filee.Core/Conversion/RoutePlanner.cs +++ b/src/Filee.Core/Conversion/RoutePlanner.cs @@ -72,11 +72,20 @@ public RoutePlanner(IEnumerable converters, Func? p /// Make a Live / Motion Photo of a video (): the route is one /// edge, or none. Without it those edges are never taken. /// - public Route? Plan(string from, string to, bool frames = false, bool shaping = false, bool motionPhoto = false, bool livePhoto = false) + /// + /// Recognize the text first (): an + /// edge to PDF, from the file itself or from a PNG of a picture Tesseract can't read, then the cheapest route from + /// that PDF on. Without it those edges are never taken. + /// + public Route? Plan(string from, string to, bool frames = false, bool shaping = false, bool motionPhoto = false, bool livePhoto = false, + bool ocr = false) { from = from.ToLowerInvariant(); to = to.ToLowerInvariant(); + if (ocr) + return PlanWithOcr(from, to, shaping); + if (frames || motionPhoto || livePhoto) { // One of them: frames, then the video of a photo, then a photo of a video. @@ -96,7 +105,7 @@ public RoutePlanner(IEnumerable converters, Func? p if (from == to) { var best = EdgesFrom(from) - .Where(e => e.Edge.To == to && !e.Edge.FrameSequence && !e.Edge.MotionPhoto && !e.Edge.LivePhoto) + .Where(e => e.Edge.To == to && !e.Edge.FrameSequence && !e.Edge.MotionPhoto && !e.Edge.LivePhoto && !e.Edge.Ocr) .OrderBy(e => e.Cost) .FirstOrDefault(); return best.Converter is null @@ -125,7 +134,7 @@ public RoutePlanner(IEnumerable converters, Func? p foreach (var (converter, edge, edgeCost) in EdgesFrom(node)) { - if (edge.To == edge.From || edge.FrameSequence || edge.MotionPhoto || edge.LivePhoto + if (edge.To == edge.From || edge.FrameSequence || edge.MotionPhoto || edge.LivePhoto || edge.Ocr || (edge.ImageSourcesOnly && !imageSource) || (edge.NotForImageSources && imageSource) || (edge.Unshaped && shaping) || (edge.DirectOnly && (node != from || edge.To != to))) @@ -155,8 +164,41 @@ public RoutePlanner(IEnumerable converters, Func? p /// Every frame of a video as pictures (see ). /// The video of a Live / Motion Photo (see ). /// A Live / Motion Photo of a video (see ). - public bool CanConvert(string from, string to, bool frames = false, bool motionPhoto = false, bool livePhoto = false) => - Plan(from, to, frames, motionPhoto: motionPhoto, livePhoto: livePhoto) is not null; + /// Recognize the text first (see ). + public bool CanConvert(string from, string to, bool frames = false, bool motionPhoto = false, bool livePhoto = false, bool ocr = false) => + Plan(from, to, frames, motionPhoto: motionPhoto, livePhoto: livePhoto, ocr: ocr) is not null; + + private Route? PlanWithOcr(string from, string to, bool shaping) + { + var steps = new List(); + var cost = 0; + var source = from; + if (OcrEdge(from).Converter is null) + { + // A picture Tesseract can't open (HEIC, PSD, …) becomes a PNG first. + if (FormatRegistry.FindById(from)?.Category != FormatCategory.Image || Plan(from, "png", shaping: shaping) is not { } toPng) + return null; + steps.AddRange(toPng.Steps); + cost += toPng.Cost; + source = "png"; + } + var ocr = OcrEdge(source); + if (ocr.Converter is null) + return null; + steps.Add(new RouteStep(ocr.Converter, source, "pdf")); + cost += ocr.Cost; + if (to != "pdf") + { + if (Plan("pdf", to, shaping: shaping) is not { } rest) + return null; + steps.AddRange(rest.Steps); + cost += rest.Cost; + } + return new Route(steps, cost); + } + + private (IConverter Converter, ConversionEdge Edge, int Cost) OcrEdge(string from) => + EdgesFrom(from).Where(e => e.Edge.Ocr && e.Edge.To == "pdf").OrderBy(e => e.Cost).FirstOrDefault(); private IReadOnlyList<(IConverter Converter, ConversionEdge Edge, int Cost)> EdgesFrom(string format) => _edgesFrom.TryGetValue(format, out var edges) ? edges : []; diff --git a/src/Filee.Core/Presets/Preset.cs b/src/Filee.Core/Presets/Preset.cs index ed6315102..29f2d593a 100644 --- a/src/Filee.Core/Presets/Preset.cs +++ b/src/Filee.Core/Presets/Preset.cs @@ -164,6 +164,15 @@ public bool MakesLivePhotoOf(string sourceFormat) => MediaOptions.LivePhotoTargets(Media.LivePhoto).Contains(TargetFormat) && Formats.FormatRegistry.FindById(sourceFormat)?.Category == Formats.FormatCategory.Video; + /// + /// True if converting a file recognizes its text first (): + /// the file is a PDF or a picture and the target one of . + /// + public bool RecognizesTextOf(string sourceFormat) => + Pdf.Ocr + && PdfOptions.OcrTargets.Contains(TargetFormat) + && (sourceFormat == "pdf" || Formats.FormatRegistry.FindById(sourceFormat)?.Category == Formats.FormatCategory.Image); + /// True for a . [JsonIgnore] public bool IsTool => Kind == PresetKind.Tool; @@ -409,6 +418,19 @@ public sealed class PdfOptions /// public bool CompressWithGhostscript { get; set; } + /// Formats text read from scans can go into (): those Filee writes from a PDF's text itself. + public static readonly IReadOnlyList OcrTargets = ["pdf", "docx", "txt", "md", "hwpx"]; + + /// + /// Recognize the text of scanned pages and pictures with Tesseract (): a PDF + /// gets a searchable text layer, a DOCX, TXT, Markdown or HWPX gets the text. Pages that already have text stay as + /// they are. + /// + public bool Ocr { get; set; } + + /// With : the language of the text, as a Tesseract code (eng, deu, chi_sim). + public string OcrLanguage { get; set; } = "eng"; + public PdfOptions Clone() => (PdfOptions)MemberwiseClone(); } diff --git a/src/Filee.Engines/EngineRegistry.cs b/src/Filee.Engines/EngineRegistry.cs index b9c0f0102..da4daf957 100644 --- a/src/Filee.Engines/EngineRegistry.cs +++ b/src/Filee.Engines/EngineRegistry.cs @@ -15,6 +15,7 @@ using Filee.Engines.Magick; using Filee.Engines.Media; using Filee.Engines.Models3D; +using Filee.Engines.Ocr; using Filee.Engines.Office; using Filee.Engines.Pdf; using Filee.Engines.Subtitles; @@ -66,6 +67,7 @@ public static IReadOnlyList CreateAll(EngineEnvironment env, ILogger new EbookConverter(), new PandocConverter(), new GhostscriptConverter(), + new TesseractConverter(), new CalibreConverter(), new LibreOfficeConverter(env, loggers?.CreateLogger()), ]; diff --git a/src/Filee.Engines/Filee.Engines.csproj b/src/Filee.Engines/Filee.Engines.csproj index a0219929c..61787e438 100644 --- a/src/Filee.Engines/Filee.Engines.csproj +++ b/src/Filee.Engines/Filee.Engines.csproj @@ -45,5 +45,7 @@ + + diff --git a/src/Filee.Engines/Hwp/Hwpx/DocumentReaders.cs b/src/Filee.Engines/Hwp/Hwpx/DocumentReaders.cs index b98e78d0d..a1a26f80a 100644 --- a/src/Filee.Engines/Hwp/Hwpx/DocumentReaders.cs +++ b/src/Filee.Engines/Hwp/Hwpx/DocumentReaders.cs @@ -105,9 +105,13 @@ internal static class DocumentReaders /// Reads (of format ) into the document model. /// Scratch folder of the job: extracted pictures go into a new folder inside it. - public static async Task ReadAsync(string path, string format, string workDirectory, CancellationToken cancellationToken) + /// A PDF's OCR layer is its text (). + public static async Task ReadAsync(string path, string format, string workDirectory, CancellationToken cancellationToken, + bool ocrLayer = false) { var media = Path.Combine(workDirectory, $"media-{Guid.NewGuid():N}"); + if (ocrLayer && format == "pdf") + return await Task.Run(() => PdfDocumentReader.ReadWithOcrLayer(path, media), cancellationToken); if (BuiltIn.TryGetValue(format, out var reader)) return await Task.Run(() => reader(path, media), cancellationToken); if (ThroughPandoc.TryGetValue(format, out var pandocReader)) diff --git a/src/Filee.Engines/Hwp/Hwpx/HwpxConverter.cs b/src/Filee.Engines/Hwp/Hwpx/HwpxConverter.cs index bf6f570eb..e3ff067d9 100644 --- a/src/Filee.Engines/Hwp/Hwpx/HwpxConverter.cs +++ b/src/Filee.Engines/Hwp/Hwpx/HwpxConverter.cs @@ -45,7 +45,8 @@ public EngineStatus GetStatus() public async Task> ConvertAsync(ConversionStep step, IProgress? progress, CancellationToken cancellationToken) { progress?.Report(0.1); - var document = await DocumentReaders.ReadAsync(step.InputPath, step.From, step.WorkDirectory, cancellationToken); + var document = await DocumentReaders.ReadAsync(step.InputPath, step.From, step.WorkDirectory, cancellationToken, + ocrLayer: step.Preset.Pdf.Ocr); progress?.Report(0.6); var output = step.Output.Allocate("hwpx"); diff --git a/src/Filee.Engines/Infrastructure/EngineDownloads.cs b/src/Filee.Engines/Infrastructure/EngineDownloads.cs index 7ae3e3022..d1f334b1d 100644 --- a/src/Filee.Engines/Infrastructure/EngineDownloads.cs +++ b/src/Filee.Engines/Infrastructure/EngineDownloads.cs @@ -8,7 +8,7 @@ namespace Filee.Engines.Infrastructure; /// One download from engines.json. /// -/// Archive format: zip, tar.gz, tar.xz, dmg, msi, oxt or conda. +/// Archive format: zip, tar.gz, tar.xz, dmg, msi, oxt, conda or nsis. /// /// Download size in bytes. public sealed record EngineComponent(string Id, string Version, string Url, string Sha256, long Size, string Kind); @@ -44,6 +44,7 @@ public static class EngineDownloads new("ghostscript", OperatingSystem.IsWindows() ? ["ghostscript", "vcruntime"] : ["ghostscript"], 31_000_000, ["ghostscript"]), new("ffmpeg", ["ffmpeg"], 272_000_000, ["ffmpeg"]), new("calibre", ["calibre"], 663_000_000, ["calibre"]), + new("tesseract", ["tesseract"], 168_000_000, ["tesseract"]), ]; /// Total download size of a package in bytes. @@ -60,6 +61,7 @@ public static class EngineDownloads { "ffmpeg" => "Install FFmpeg with Homebrew (brew install ffmpeg), then check again or select its bin folder.", "ghostscript" => "Install Ghostscript with Homebrew (brew install ghostscript), then check again or select its bin folder.", + "tesseract" => "Install Tesseract with Homebrew (brew install tesseract), then check again or select its bin folder.", "calibre" => "The calibre Filee downloads needs macOS 14 or later. Install calibre 7.26 from calibre-ebook.com and select its .app folder.", _ => $"No Filee download is available for {RuntimeIdentifier}. Install {package.Id} for this Mac and select its program or .app folder.", }; @@ -68,6 +70,7 @@ public static class EngineDownloads { "libreoffice" => "Install LibreOffice using your distribution's package manager (Ubuntu/Debian: sudo apt install libreoffice), then check again. HWP import through LibreOffice also needs Java and the H2Orestart extension.", "ghostscript" => "Install Ghostscript using your distribution's package manager (Ubuntu/Debian: sudo apt install ghostscript), then check again.", + "tesseract" => "Install Tesseract using your distribution's package manager (Ubuntu/Debian: sudo apt install tesseract-ocr), then check again.", _ => $"No Filee download is available for {RuntimeIdentifier}. Install {package.Id} using your distribution's package manager and select its program folder.", }; return $"No Filee download is available for {RuntimeIdentifier}. Select a compatible installed copy."; @@ -79,10 +82,11 @@ public static class EngineDownloads /// /// Ticked by default in the installer and in the first-run choice. Large downloads are offered, not suggested: /// LibreOffice is only needed for older formats (DOC, XLS, PPT, OpenDocument), FFmpeg (~100 MB) only for video and - /// audio, calibre (~230 MB) only for rare e-book formats and Kindle output. Ghostscript is small but only needed for - /// EPS / PostScript. + /// audio, calibre (~230 MB) only for rare e-book formats and Kindle output. Ghostscript and Tesseract are small but + /// only needed for EPS / PostScript and for scans. /// - public static bool IsSuggested(EnginePackage package) => CanDownload(package) && DownloadSize(package) < SuggestLimit && package.Id != "ghostscript"; + public static bool IsSuggested(EnginePackage package) => + CanDownload(package) && DownloadSize(package) < SuggestLimit && package.Id is not ("ghostscript" or "tesseract"); /// The running macOS; downloads whose minimumMacOS is newer are left out. Null keeps all. public static IReadOnlyDictionary ComponentsForRuntime(string runtimeIdentifier, Version? macOSVersion = null) diff --git a/src/Filee.Engines/Infrastructure/EngineEnvironment.cs b/src/Filee.Engines/Infrastructure/EngineEnvironment.cs index a94979e60..abc9c0d82 100644 --- a/src/Filee.Engines/Infrastructure/EngineEnvironment.cs +++ b/src/Filee.Engines/Infrastructure/EngineEnvironment.cs @@ -1,6 +1,6 @@ // Shared context for engines: where Filee's own engine copies live and where engines keep their state. // Built-in engines never use software installed on the system (Microsoft Office, Hancom Office). The optional -// engines (FFmpeg, calibre, Pandoc, Ghostscript, LibreOffice) are looked up in this order: a copy the user chose in +// engines (FFmpeg, calibre, Pandoc, Ghostscript, LibreOffice, Tesseract) are looked up in this order: a copy the user chose in // Settings → Engines (OwnCopies), Filee's own copy (installer or download, the tested version), then one already on // the PC (on the PATH or where its installer puts it), so nobody has to download a second FFmpeg they already have. @@ -84,6 +84,7 @@ dataDirectoryOverride is not null ["calibre"] = [windows ? "ebook-convert.exe" : "ebook-convert"], ["rhwp"] = [windows ? "rhwp.exe" : "rhwp"], ["7zip"] = [windows ? "7z.exe" : "7zz"], + ["tesseract"] = [windows ? "tesseract.exe" : "tesseract"], }; public static string ProgramName(string name) => OperatingSystem.IsWindows() ? name @@ -231,6 +232,9 @@ private static IEnumerable SystemFolders(string id) case "calibre": folders.Add(Path.Combine(root, "Calibre2")); break; + case "tesseract": + folders.Add(Path.Combine(root, "Tesseract-OCR")); + break; case "ghostscript": // C:\Program Files\gs\gs10.04.0\bin, newest first try diff --git a/src/Filee.Engines/Infrastructure/EngineInstaller.cs b/src/Filee.Engines/Infrastructure/EngineInstaller.cs index 12fe5fac3..7b7b50f2a 100644 --- a/src/Filee.Engines/Infrastructure/EngineInstaller.cs +++ b/src/Filee.Engines/Infrastructure/EngineInstaller.cs @@ -7,6 +7,7 @@ // entries, no admin rights. Parts headless LibreOffice never uses (help, gallery, most dictionaries) are removed. // * conda-forge packages (Ghostscript, the Microsoft C++ runtime) are zip files holding zstd-compressed tarballs; // only their Windows binaries (Library/bin) are unpacked, with a managed decompressor (no 7-Zip, no installer). +// * NSIS installers (Tesseract) are unpacked with Filee's own 7-Zip, never run: files only, no registry entries. // * A marker file with the component's hash records what is installed, so an engines.json update is detected. using System.Diagnostics; @@ -205,6 +206,20 @@ private async Task UnpackAsync(EngineComponent component, string file, Cancellat UnpackConda(file, staging, cancellationToken); Replace(component.Id, staging); break; + case "traineddata": + // One Tesseract language (OcrLanguages): the file as it is, in a folder of its own. + Directory.CreateDirectory(staging); + File.Copy(file, Path.Combine(staging, component.Id + ".traineddata")); + Replace(component.Id, staging); + break; + case "nsis": + var sevenZip = Archives.SevenZip.Locate() + ?? throw new InvalidOperationException($"{component.Id} needs Filee's 7-Zip, which is missing."); + Directory.CreateDirectory(staging); + await sevenZip.ExtractAsync(file, staging, [], _root, cancellationToken); + TrimTesseract(staging); + Replace(component.Id, staging); + break; case "oxt": // LibreOffice registers bundled extensions (plain folders under share/extensions) on start-up. var extensions = Path.Combine(FolderOf("libreoffice"), "share", "extensions"); @@ -381,6 +396,26 @@ internal static void TrimLibreOffice(string root) } } + /// + /// Removes what OCR never uses (~80 MB): the training programs and their manuals, the installer's own files and + /// the bundled language data (Filee downloads its own, see OcrLanguages). tesseract.exe, its DLLs and the + /// licenses in doc/ stay. + /// + internal static void TrimTesseract(string root) + { + foreach (var folder in new[] { "$PLUGINSDIR", "tessdata" }) + { + var path = Path.Combine(root, folder); + if (Directory.Exists(path)) + Directory.Delete(path, recursive: true); + } + foreach (var file in Directory.EnumerateFiles(root, "*.exe").Concat(Directory.EnumerateFiles(root, "*.html")).ToList()) + { + if (!string.Equals(Path.GetFileName(file), "tesseract.exe", StringComparison.OrdinalIgnoreCase)) + File.Delete(file); + } + } + /// /// Unpacks the Windows binaries of a conda package (.conda = zip with pkg-*.tar.zst): the files under Library/bin /// land directly in . Resources such as Ghostscript's fonts are compiled into its DLL. @@ -443,7 +478,7 @@ private static string SingleRoot(string folder) /// Makes the engine folder, replacing a previous version. private void Replace(string id, string source) { - RequiredProgramFolder(id, source, _components[id].Kind is "msi" or "conda" || OperatingSystem.IsWindows()); + RequiredProgramFolder(id, source, _components[id].Kind is "msi" or "conda" or "nsis" || OperatingSystem.IsWindows()); var target = FolderOf(id); if (Directory.Exists(target)) Directory.Delete(target, recursive: true); diff --git a/src/Filee.Engines/Infrastructure/engines.json b/src/Filee.Engines/Infrastructure/engines.json index 6be3c151b..d2158b025 100644 --- a/src/Filee.Engines/Infrastructure/engines.json +++ b/src/Filee.Engines/Infrastructure/engines.json @@ -70,6 +70,13 @@ "sha256": "0F96AE06165C2419607C1C66726C091A78E89E08EA69F077CF3E2C884E800853", "size": 226549760, "kind": "msi" + }, + "tesseract": { + "version": "5.4.0.20240606", + "url": "https://github.com/UB-Mannheim/tesseract/releases/download/v5.4.0.20240606/tesseract-ocr-w64-setup-5.4.0.20240606.exe", + "sha256": "C885FFF6998E0608BA4BB8AB51436E1C6775C2BAFC2559A19B423E18678B60C9", + "size": 50175248, + "kind": "nsis" } }, "platforms": { diff --git a/src/Filee.Engines/Infrastructure/ocr-languages.json b/src/Filee.Engines/Infrastructure/ocr-languages.json new file mode 100644 index 000000000..9bdfd216b --- /dev/null +++ b/src/Filee.Engines/Infrastructure/ocr-languages.json @@ -0,0 +1,351 @@ +{ + "_comment": "Tesseract language data (tessdata_fast 4.1.0, Apache-2.0), downloaded on demand when an OCR preset needs a language: pinned URL + SHA-256 per file. culture names the language in the UI.", + "url": "https://raw.githubusercontent.com/tesseract-ocr/tessdata_fast/4.1.0/{0}.traineddata", + "languages": { + "eng": { + "culture": "en", + "sha256": "7D4322BD2A7749724879683FC3912CB542F19906C83BCC1A52132556427170B2", + "size": 4113088 + }, + "deu": { + "culture": "de", + "sha256": "19D219BBB6672C869D20A9636C6816A81EB9A71796CB93EBE0CB1530E2CDB22D", + "size": 1525436 + }, + "fra": { + "culture": "fr", + "sha256": "CED037562E8C80C13122DECE28DD477D399AF80911A28791A66A63AC1E3445CA", + "size": 1130365 + }, + "spa": { + "culture": "es", + "sha256": "6F2E04D02774A18F01BED44B1111F2CD7F3BA7AC9DC4373CD3F898A40EA6B464", + "size": 2294433 + }, + "por": { + "culture": "pt", + "sha256": "C4932B937207A9514B7514D518B931A99938C02A28A5A5A553F8599ED58B7DEB", + "size": 1982756 + }, + "ita": { + "culture": "it", + "sha256": "B8F89E1E785118DAC4D51AE042C029A64EDB5C3EE42EF73027A6D412748D8827", + "size": 2701314 + }, + "nld": { + "culture": "nl", + "sha256": "CED0E5E046A84C908A6AA7ACCBEF9A232C4A5D9A8276691B81C6EE64D02963F6", + "size": 6050296 + }, + "pol": { + "culture": "pl", + "sha256": "C4476CDBC0E33D898D32345122B7BE1CBF85ACE15F920F06C7714756E1EF79B2", + "size": 4765518 + }, + "ces": { + "culture": "cs", + "sha256": "934BCAF97EF3348413263331131C9FA7F55F30DB333C711929C124FB635F7E1B", + "size": 3795684 + }, + "slk": { + "culture": "sk", + "sha256": "FBCC400A9C74C6A13D922FCB1211B655D1B165387B675ED75CD2DBD756B974A5", + "size": 4427661 + }, + "slv": { + "culture": "sl", + "sha256": "B937632C17CE5FDF20535D25FEEC044DA10A0C4C8234F4302D5606FD671CF60D", + "size": 3003829 + }, + "hrv": { + "culture": "hr", + "sha256": "9E515D9832CE259DBAB550B1CC6B998F8B929FAF2EDACAACA981B05ADB130571", + "size": 4103348 + }, + "srp": { + "culture": "sr-Cyrl", + "sha256": "AA41AE3D9CC705E60D398AB38A5C3CC8B772C0D420C7D4F0859BEB13D0E321B6", + "size": 2149931 + }, + "bos": { + "culture": "bs", + "sha256": "6CC8CC87CF1AFBFA6A41FEBB725DBADB14BED96A46685906440A6EB8A7892F04", + "size": 2498242 + }, + "ron": { + "culture": "ro", + "sha256": "9ADFDE6B51BA4B97EFD10EA37C3070FD3FC2BAD7815E81F5C3C198CD96216CC9", + "size": 2376323 + }, + "hun": { + "culture": "hu", + "sha256": "35067E7CFE102DCDC953F9A758FDFAA6296B17A1EE6D874EE780FA306430B9FB", + "size": 5296273 + }, + "bul": { + "culture": "bg", + "sha256": "AEBC9B0FCC8CFAF8A9F38A02BB7B85052BD850744696A2C11CF0081820E5B21E", + "size": 1675212 + }, + "ell": { + "culture": "el", + "sha256": "4FBA8A0B461038D51F1C20D043D4F2AC38C4E778F1B90830847F7BD8FA3BA726", + "size": 1419514 + }, + "rus": { + "culture": "ru", + "sha256": "E16E5E036CCE1D9EC2B00063CF8B54472625B9E14D893A169E2B0DEDEB4DF225", + "size": 3861738 + }, + "ukr": { + "culture": "uk", + "sha256": "D59E53E2BDED32F4445F124B4B00240FCAC7E8044C003AB822CCB94F0B3DB59B", + "size": 3825102 + }, + "bel": { + "culture": "be", + "sha256": "9C6668A0B202F3DCFE074B64620D108E1902CA7498A40B5A11B4A3DA6112D58F", + "size": 3692098 + }, + "tur": { + "culture": "tr", + "sha256": "7393381111E1152420FC4092CB44EEF4237580D21B92BF30D7D221AAD192C6B7", + "size": 4550554 + }, + "swe": { + "culture": "sv", + "sha256": "F7304988D41F833EFEBCC2D529DF54B1903ECEBBC3DA1FAABD19A0FDDD4FE586", + "size": 4167034 + }, + "dan": { + "culture": "da", + "sha256": "ACB1FD074487A31D1294FCDFD7D7C673467FFD8AEACB2CCD61EBCBF04EB4E2FA", + "size": 2580059 + }, + "nor": { + "culture": "nb", + "sha256": "0451EB4F8049AE78196806BF878A389A2F40F1386FE038568CF4441226BA6EF2", + "size": 3610079 + }, + "fin": { + "culture": "fi", + "sha256": "61A04CD62B507C3D9AE0E1CDA399E6715EBF49DEA9DF47897C8ACDCD3BD3E13C", + "size": 7865732 + }, + "isl": { + "culture": "is", + "sha256": "5EC828C363F3F0062C3CAF08216677CF5F3C0C981B05FAB445123A16B98F10A9", + "size": 2278973 + }, + "est": { + "culture": "et", + "sha256": "515D4A773682B286369511E83FE412BCFF16A92A886F99C761C1D760A7E30456", + "size": 4458101 + }, + "lav": { + "culture": "lv", + "sha256": "7D9EAF22254B381F18B806CD6CB647862A397A7221ADBCE3E22CC421793A8847", + "size": 2717247 + }, + "lit": { + "culture": "lt", + "sha256": "1E383DF5B055583BC01CB5764ECDF74C540753F2CB3F8205E7105361DA4BC989", + "size": 3154896 + }, + "cat": { + "culture": "ca", + "sha256": "250DB73CD5B380D2798581295DC12F20D0828CDB335A65D833D12DFDBF57117D", + "size": 1146012 + }, + "eus": { + "culture": "eu", + "sha256": "40E7418296C355D9FD9CA843D115C51E740089E576E887443E61704823CD6624", + "size": 5175921 + }, + "glg": { + "culture": "gl", + "sha256": "7947619C5544D86849F563BD737AD90DBEA9F5319FBD838C4747A7A1CD40B260", + "size": 2554555 + }, + "ara": { + "culture": "ar", + "sha256": "E3206D3DC87FD50C24A0FB9F01838615911D25168F4E64415244B67D2BB3E729", + "size": 1432056 + }, + "fas": { + "culture": "fa", + "sha256": "DB1C0A91208AFF00D3CF1ED2C1D23F76419AFD5F024688B4F71ADC3F2CE4A505", + "size": 431500 + }, + "urd": { + "culture": "ur", + "sha256": "62E8250CE2A994106E313A82E26A516A39E2CF159D0CE3C5B5008387FD0D555F", + "size": 1398718 + }, + "heb": { + "culture": "he", + "sha256": "11F9E43AB227F786352A50F75C94C2E9906F1BABA86D93276DA19DA7CE0904DB", + "size": 961404 + }, + "hin": { + "culture": "hi", + "sha256": "4C73FFC59D497C186B19D1E90F5D721D678EA6B2E277B719BEE4E2AF12271825", + "size": 1122751 + }, + "ben": { + "culture": "bn", + "sha256": "31163084C279AAEBD376216F0C3D5C17AD4B5FEE8DB49DAE79C20000B5DE5964", + "size": 855841 + }, + "mar": { + "culture": "mr", + "sha256": "0BA3F2D116972E72FE9E176BC84C38E81DFB6670F4ED1F7F6C8E16A27DA7CB61", + "size": 2118233 + }, + "guj": { + "culture": "gu", + "sha256": "FA69658614B4946A9AFAE8853D67E0689838803DFA3D12C2E35EC53EE6F8DF34", + "size": 1418394 + }, + "pan": { + "culture": "pa", + "sha256": "1EC0907FC3534065EA9AE190C6BB7EC9E5C74FD9D2FA996AAEC7407F11AD8131", + "size": 497721 + }, + "tam": { + "culture": "ta", + "sha256": "D02FBEC24BE4B07E32E80D0CCFC3B6B67A3C5D61C9D0A7C8532677990912C6EC", + "size": 3237963 + }, + "tel": { + "culture": "te", + "sha256": "D10691FDDD5B67802E1C12800EBB321D3B8BCD8D24A2AC3FF206F93188C04AB5", + "size": 2769654 + }, + "kan": { + "culture": "kn", + "sha256": "BD31E6B6AE93271E3BCF5383D306D8EEFBB91542937CD6D735A5930C970E61D8", + "size": 3608331 + }, + "mal": { + "culture": "ml", + "sha256": "BD05CBF1B197E7810D2903419AEDB06F9EF77BFEDF50B358673C1D18D707CDB4", + "size": 5275996 + }, + "sin": { + "culture": "si", + "sha256": "BC75C6DF2375A30C9F7E759FA7D4B58AE3ECF9CE72668A702D01ACB13E551422", + "size": 1727413 + }, + "nep": { + "culture": "ne", + "sha256": "280BA9450B4F21AFBF5985E0DE87857B75A972577C50EF0603C141DDDE4F1CB8", + "size": 1002911 + }, + "tha": { + "culture": "th", + "sha256": "294227CC2D1292B0ACB28D61D4115C88252B96D466CA90B417CF4CF0C67BF07C", + "size": 1072600 + }, + "vie": { + "culture": "vi", + "sha256": "79DF64CAF7BCFB2A27DF5042ECB6121E196EADA34DA774956995747636D5BFA1", + "size": 531275 + }, + "ind": { + "culture": "id", + "sha256": "69786901DA87AB8766C1EA7FBB10B28F2110C14DA3F6C8F2735DF131FBA95D88", + "size": 1122661 + }, + "msa": { + "culture": "ms", + "sha256": "E41A3E5FEBFEC50C90371EB1CBB17A48B10CAD387900E3420B1F134C1B766CBA", + "size": 1747801 + }, + "fil": { + "culture": "fil", + "sha256": "2B17F68014AB44B88B5E2334EBCD0CE0090D9AF7C6FBEF6FAB22E0BBD9A934DD", + "size": 1841715 + }, + "khm": { + "culture": "km", + "sha256": "47F110575341B322052F3BECBEFEE61A3ECF1EF549352B5D4D33D28AFE30D099", + "size": 1446926 + }, + "lao": { + "culture": "lo", + "sha256": "20124962E93E68121E02C49A949D1F9DF5DB87DD62E5E4AA578362CA532444F8", + "size": 6386744 + }, + "mya": { + "culture": "my", + "sha256": "02AA6C25CFE9E583FA7B5D4131EAC948F962308983F0F397DF077DEA58212B03", + "size": 4640561 + }, + "kor": { + "culture": "ko", + "sha256": "6B85E11D9BBF07863B97B3523B1B112844C43E713DF8B66418A081FD1060B3B2", + "size": 1677415 + }, + "jpn": { + "culture": "ja", + "sha256": "1F5DE9236D2E85F5FDF4B3C500F2D4926F8D9449F28F5394472D9E8D83B91B4D", + "size": 2471260 + }, + "chi_sim": { + "culture": "zh-Hans", + "sha256": "A5FCB6F0DB1E1D6D8522F39DB4E848F05984669172E584E8D76B6B3141E1F730", + "size": 2469156 + }, + "chi_tra": { + "culture": "zh-Hant", + "sha256": "529C5B5797D64B126065CD55F2BB4C7FD7B15790798091B1FF259941A829330B", + "size": 2366642 + }, + "kat": { + "culture": "ka", + "sha256": "557ABB6F1C68BC1B286F1BDD00BB6B82F85A427A91899807DAB6C2F6C7986731", + "size": 2524769 + }, + "hye": { + "culture": "hy", + "sha256": "B701D0D95799A716143DEDB0197504E56F27A1BB133D6607FF5778C6988CB67C", + "size": 3463717 + }, + "aze": { + "culture": "az", + "sha256": "A365310848AECB739F19369CB3831D4660FCD9345D798E91A3042455F9CCC9F0", + "size": 3524799 + }, + "kaz": { + "culture": "kk", + "sha256": "FCC01EED3815A42B9C6321C4C9D3606F39B166CBF95ADE98B7D8D12063EAE53D", + "size": 4734644 + }, + "uzb": { + "culture": "uz", + "sha256": "0E39021EEFE692906B50D9BD22E580FA57FE43FA35DB6846AD8B6369DDC21EA8", + "size": 6470703 + }, + "mon": { + "culture": "mn", + "sha256": "A151A3806D61AC43619CD383896D551BA5C3B07388FFEC6FC83C8C604D677570", + "size": 2137042 + }, + "amh": { + "culture": "am", + "sha256": "3EC3311833A108E07D58A1152B00C0CF1848752E4F85769D46E8CA2B718A2CCC", + "size": 5470094 + }, + "swa": { + "culture": "sw", + "sha256": "395439D1EC308535066CBAEA9B15E0E4CC81F8609170AF76AB7E7E8D3EC42F3E", + "size": 2167651 + }, + "afr": { + "culture": "af", + "sha256": "126D480BFAE95BE2A911ED4916465E27BDE75FEA2DA631E21B96762E5F239646", + "size": 2652786 + } + } +} diff --git a/src/Filee.Engines/Ocr/OcrLanguages.cs b/src/Filee.Engines/Ocr/OcrLanguages.cs new file mode 100644 index 000000000..3c3991c47 --- /dev/null +++ b/src/Filee.Engines/Ocr/OcrLanguages.cs @@ -0,0 +1,110 @@ +// The languages Tesseract can read text in (tessdata_fast 4.1.0, Apache-2.0), pinned in ocr-languages.json like the +// engines in engines.json. A language is downloaded the first time a preset needs it, checked against its SHA-256 and +// kept in engines/ocr-languages//.traineddata by EngineInstaller, so a damaged or partial download never +// counts as installed. + +using System.Globalization; +using System.Text.Json; +using Filee.Engines.Infrastructure; + +namespace Filee.Engines.Ocr; + +/// One language Tesseract reads, by its Tesseract code (eng, chi_sim). +/// The language as a culture name, for its name in the UI. +public sealed record OcrLanguage(string Code, string Culture, long Size) +{ + /// The language's own name ("Deutsch", "日本語"), the same in every UI language. + public string NativeName + { + get + { + try + { + var name = CultureInfo.GetCultureInfo(Culture).NativeName; + return name.Length > 0 ? char.ToUpper(name[0], CultureInfo.GetCultureInfo(Culture)) + name[1..] : Code; + } + catch (CultureNotFoundException) + { + return Code; + } + } + } +} + +public static class OcrLanguages +{ + /// Read when a preset doesn't say (and the first language offered). + public const string Default = "eng"; + + private const string Version = "4.1.0"; + + private static readonly (IReadOnlyList All, IReadOnlyDictionary Components) List = Load(); + + /// Every language offered, English first. + public static IReadOnlyList All => List.All; + + /// The downloads, for (one component per language). + internal static IReadOnlyDictionary Components => List.Components; + + /// Folder that receives one sub folder per language. + public static string Root => Path.Combine(EngineEnvironment.DownloadRoot, "ocr-languages"); + + public static OcrLanguage? Find(string code) => All.FirstOrDefault(l => l.Code == code); + + /// The language data, or null while it isn't downloaded. + public static string? FileOf(string code) => + EngineEnvironment.FirstExisting(Path.Combine(Root, code, code + ".traineddata")) is { } file + && new EngineInstaller(Http.Value, Root, Components).IsInstalled(PackageOf(code)) + ? file : null; + + public static bool IsInstalled(string code) => FileOf(code) is not null; + + /// Downloads if it isn't there yet; one download at a time. + public static async Task EnsureAsync(string code, CancellationToken cancellationToken) + { + if (!Components.ContainsKey(code)) + throw new ArgumentException($"Tesseract has no language '{code}' in Filee's list.", nameof(code)); + await OneAtATime.WaitAsync(cancellationToken); + try + { + if (FileOf(code) is { } file) + return file; + await new EngineInstaller(Http.Value, Root, Components).InstallAsync(PackageOf(code), progress: null, cancellationToken); + return FileOf(code) ?? throw new InvalidDataException($"The {code} language data is missing after its download."); + } + finally + { + OneAtATime.Release(); + } + } + + private static readonly SemaphoreSlim OneAtATime = new(1, 1); + + /// Redirects are followed by the installer, which checks every file against its SHA-256. + private static readonly Lazy Http = new(() => + { + var http = new HttpClient(new SocketsHttpHandler { AllowAutoRedirect = false }) { Timeout = TimeSpan.FromMinutes(10) }; + http.DefaultRequestHeaders.UserAgent.ParseAdd($"Filee/{typeof(OcrLanguages).Assembly.GetName().Version}"); + return http; + }); + + private static EnginePackage PackageOf(string code) => new("ocr-" + code, [code], Components[code].Size, ["tesseract"]); + + private static (IReadOnlyList, IReadOnlyDictionary) Load() + { + using var stream = typeof(OcrLanguages).Assembly.GetManifestResourceStream("Filee.Engines.ocr-languages.json") + ?? throw new InvalidOperationException("Embedded ocr-languages.json is missing."); + using var json = JsonDocument.Parse(stream); + var url = json.RootElement.GetProperty("url").GetString()!; + var languages = new List(); + var components = new Dictionary(); + foreach (var entry in json.RootElement.GetProperty("languages").EnumerateObject()) + { + var size = entry.Value.GetProperty("size").GetInt64(); + languages.Add(new OcrLanguage(entry.Name, entry.Value.GetProperty("culture").GetString()!, size)); + components[entry.Name] = new EngineComponent(entry.Name, Version, string.Format(CultureInfo.InvariantCulture, url, entry.Name), + entry.Value.GetProperty("sha256").GetString()!, size, "traineddata"); + } + return (languages, components); + } +} diff --git a/src/Filee.Engines/Ocr/TesseractConverter.cs b/src/Filee.Engines/Ocr/TesseractConverter.cs new file mode 100644 index 000000000..98581a914 --- /dev/null +++ b/src/Filee.Engines/Ocr/TesseractConverter.cs @@ -0,0 +1,219 @@ +// Text recognition (OCR) with Tesseract (Apache-2.0, a separate program downloaded on demand into engines/tesseract; +// on Linux and macOS the one installed with the system's packages). "Recognize text" (PdfOptions.Ocr) turns a scan or +// a picture into a searchable PDF: the page as it looks, with the recognized words as an invisible text layer on top. +// The route planner puts this step first and goes on from the PDF, so DOCX, TXT, Markdown and HWPX get the text +// (PdfDocumentReader reads the layer when the preset asks for OCR). +// +// A PDF keeps its pages that already have text (born-digital pages are never turned into pictures); only pages +// without any are rendered at 300 dpi and recognized, several at a time. The language data comes from OcrLanguages, +// downloaded the first time a language is used, and is copied into the job's folder so Tesseract reads only it. + +using Filee.Core.Conversion; +using Filee.Core.Presets; +using Filee.Engines.Infrastructure; +using Filee.Engines.Pdf; +using ImageMagick; +using PDFtoImage; +using PdfSharp.Pdf; +using PdfSharp.Pdf.IO; +using SkiaSharp; +using UglyToad.PdfPig.Exceptions; + +// PDFtoImage marks its API for the platforms PDFium ships for; Filee only targets desktop platforms, all covered. +#pragma warning disable CA1416 + +namespace Filee.Engines.Ocr; + +/// Scans and pictures → searchable PDF through Tesseract. +public sealed class TesseractConverter : IConverter +{ + private static readonly TimeSpan Timeout = TimeSpan.FromMinutes(10); + + /// Scans are rendered at this resolution, which Tesseract reads best (and the page keeps its size). + internal const int Dpi = 300; + + /// Pictures Tesseract opens itself (Leptonica); others become a PNG first (RoutePlanner). + internal static readonly IReadOnlyList ImageSources = ["png", "jpg", "tiff", "bmp", "webp", "gif"]; + + /// + /// One thread per Tesseract: Filee runs several pages at a time, and Tesseract's own threads would only compete + /// (its documentation recommends this). TESSDATA_PREFIX of the system is not used, the job's folder is. + /// + private static readonly Dictionary Environment = new() + { + ["OMP_THREAD_LIMIT"] = "1", + ["TESSDATA_PREFIX"] = "", + }; + + private string? _executable; + + public string Id => "tesseract"; + public string DisplayName => "Tesseract OCR"; + + /// One file at a time; its pages are recognized in parallel. + public int MaxParallelism => 1; + + public IReadOnlyList Edges { get; } = + [.. ImageSources.Select(f => new ConversionEdge(f, "pdf", Ocr: true)), new("pdf", "pdf", Ocr: true)]; + + public EngineStatus GetStatus() + { + _executable = Locate(); + return _executable is null ? EngineStatus.Unavailable("engine.reason.not_installed") + : EngineStatus.Available(_executable, EngineEnvironment.UsesFileesCopy("tesseract") ? EngineVersions.Component("tesseract") : null); + } + + /// The user's chosen copy, Filee's download, then one installed on the system. + internal static string? Locate() => + EngineEnvironment.OwnProgram("tesseract", "tesseract.exe") ?? EngineEnvironment.BundledProgram("tesseract", "tesseract.exe") + ?? EngineEnvironment.SystemProgram("tesseract", "tesseract.exe"); + + public async Task> ConvertAsync(ConversionStep step, IProgress? progress, CancellationToken cancellationToken) + { + var tesseract = _executable ?? Locate() ?? throw new InvalidOperationException("Tesseract is not installed."); + var language = OcrLanguages.Find(step.Preset.Pdf.OcrLanguage) ?? OcrLanguages.Find(OcrLanguages.Default)!; + string data; + try + { + data = await OcrLanguages.EnsureAsync(language.Code, cancellationToken); + } + catch (HttpRequestException ex) + { + throw new ConversionRefusedException("error.ocr_language_download", $"{language.NativeName}: {ex.Message}"); + } + var scratch = Path.Combine(step.WorkDirectory, "ocr-" + Guid.NewGuid().ToString("N")[..8]); + var tessdata = Path.Combine(scratch, "tessdata"); + Directory.CreateDirectory(tessdata); + File.Copy(data, Path.Combine(tessdata, Path.GetFileName(data))); + var ocr = new Run(tesseract, tessdata, language.Code); + progress?.Report(0.05); + + var output = step.Output.Allocate("pdf"); + if (output is null) + return []; + if (step.From == "pdf") + { + await RecognizePdfAsync(ocr, step, scratch, output, progress, cancellationToken); + } + else + { + var target = Path.Combine(scratch, "page"); + await ocr.ToPdfAsync(step.InputPath, target, HasResolution(step.InputPath) ? null : Dpi, cancellationToken); + File.Move(target + ".pdf", output, overwrite: true); + } + progress?.Report(1); + return [output]; + } + + /// + /// The pages without text become recognized scans, the others are copied. A PDF whose pages all have text is + /// copied as it is, with a note. + /// + private static async Task RecognizePdfAsync(Run ocr, ConversionStep step, string scratch, string output, + IProgress? progress, CancellationToken cancellationToken) + { + var bytes = await File.ReadAllBytesAsync(step.InputPath, cancellationToken); + var hasText = PagesWithText(step.InputPath); + var pages = PageRange.Parse(step.Preset.Pdf.PageRange, hasText.Count); + var scans = pages.Where(p => !hasText[p]).ToList(); + if (scans.Count == 0 && pages.Count == hasText.Count) + { + step.Notice?.Invoke("notice.ocr_has_text"); + File.Copy(step.InputPath, output, overwrite: true); + return; + } + + // PDFium draws one page at a time; Tesseract then reads them side by side. + var options = new RenderOptions(Dpi: Dpi, WithAnnotations: true, WithFormFill: true, BackgroundColor: SKColors.White); + for (var i = 0; i < scans.Count; i++) + { + cancellationToken.ThrowIfCancellationRequested(); + using var bitmap = Conversion.ToImage(bytes, scans[i], null, options); + await using var file = File.Create(Path.Combine(scratch, $"p{scans[i]}.png")); + bitmap.Encode(file, SKEncodedImageFormat.Png, 100); + progress?.Report(0.05 + 0.25 * (i + 1) / scans.Count); + } + var done = 0; + await Parallel.ForEachAsync(scans, new ParallelOptions { MaxDegreeOfParallelism = System.Environment.ProcessorCount, CancellationToken = cancellationToken }, + async (page, ct) => + { + await ocr.ToPdfAsync(Path.Combine(scratch, $"p{page}.png"), Path.Combine(scratch, $"p{page}"), Dpi, ct); + progress?.Report(0.3 + 0.65 * Interlocked.Increment(ref done) / scans.Count); + }); + + // A new document: the pages and the document information come along, bookmarks and form fields don't. + using var source = PdfSharpConverter.Open(step.InputPath, PdfDocumentOpenMode.Import); + using var result = new PdfDocument(); + result.Info.Title = source.Info.Title; + result.Info.Author = source.Info.Author; + result.Info.Subject = source.Info.Subject; + result.Info.Keywords = source.Info.Keywords; + var recognized = new List(); + try + { + foreach (var page in pages) + { + if (hasText[page]) + { + result.AddPage(source.Pages[page]); + continue; + } + var scan = PdfReader.Open(Path.Combine(scratch, $"p{page}.pdf"), PdfDocumentOpenMode.Import); + recognized.Add(scan); + result.AddPage(scan.Pages[0]); + } + result.Save(output); + } + finally + { + recognized.ForEach(d => d.Dispose()); + } + } + + /// For each page: true when it has any text, visible or not (an OCR layer counts). + internal static List PagesWithText(string path) + { + try + { + using var pdf = UglyToad.PdfPig.PdfDocument.Open(path, new UglyToad.PdfPig.ParsingOptions { UseLenientParsing = true, SkipMissingFonts = true }); + return [.. pdf.GetPages().Select(page => page.Letters.Any(l => !string.IsNullOrWhiteSpace(l.Value)))]; + } + catch (PdfDocumentEncryptedException) + { + throw PdfDocumentReader.PasswordProtected(); + } + } + + /// + /// True when the picture says how large it is meant to be. Without that Tesseract guesses 70 dpi (a page of a + /// metre and more); 300 dpi is what scanners write. + /// + private static bool HasResolution(string path) + { + try + { + var info = new MagickImageInfo(path); + return info.Density is { X: > 1 }; + } + catch (MagickException) + { + return false; + } + } + + /// One language, one Tesseract. + private sealed record Run(string Executable, string Tessdata, string Language) + { + /// Writes {outputBase}.pdf. + public async Task ToPdfAsync(string input, string outputBase, int? dpi, CancellationToken cancellationToken) + { + // tessedit_create_pdf instead of the "pdf" config file, which lives in Tesseract's own tessdata folder. + List args = [input, outputBase, "--tessdata-dir", Tessdata, "-l", Language, "-c", "tessedit_create_pdf=1"]; + if (dpi is { } value) + args.AddRange(["--dpi", value.ToString(System.Globalization.CultureInfo.InvariantCulture)]); + var result = await ProcessRunner.RunAsync(Executable, args, Timeout, cancellationToken, environment: Environment); + if (result.ExitCode != 0 || !File.Exists(outputBase + ".pdf")) + throw new InvalidOperationException($"Tesseract failed (exit {result.ExitCode}). {result.StandardError.Trim()}"); + } + } +} diff --git a/src/Filee.Engines/Office/DocxWriterConverter.cs b/src/Filee.Engines/Office/DocxWriterConverter.cs index 0b2deb234..cacf3bb20 100644 --- a/src/Filee.Engines/Office/DocxWriterConverter.cs +++ b/src/Filee.Engines/Office/DocxWriterConverter.cs @@ -32,7 +32,8 @@ public EngineStatus GetStatus() => public async Task> ConvertAsync(ConversionStep step, IProgress? progress, CancellationToken cancellationToken) { progress?.Report(0.1); - var document = await DocumentReaders.ReadAsync(step.InputPath, step.From, step.WorkDirectory, cancellationToken); + var document = await DocumentReaders.ReadAsync(step.InputPath, step.From, step.WorkDirectory, cancellationToken, + ocrLayer: step.Preset.Pdf.Ocr); progress?.Report(0.6); var output = step.Output.Allocate("docx"); diff --git a/src/Filee.Engines/Pdf/PdfDocumentReader.cs b/src/Filee.Engines/Pdf/PdfDocumentReader.cs index cb4a90583..e3fbe1d0c 100644 --- a/src/Filee.Engines/Pdf/PdfDocumentReader.cs +++ b/src/Filee.Engines/Pdf/PdfDocumentReader.cs @@ -1,11 +1,12 @@ -// PDF → HDocument with PdfPig (Apache-2.0), so PDF → DOCX / HWPX / TXT needs no Word, LibreOffice or OCR engine. +// PDF → HDocument with PdfPig (Apache-2.0), so PDF → DOCX / HWPX / TXT needs no Word or LibreOffice. // // Every page's words (with their positions, fonts, sizes and colours) become lines, lines become paragraphs, and the // paragraphs and pictures are put in reading order, also on two-column pages (PdfDocumentReader.Layout). Headings // come from font sizes, bold / italic from the fonts, page size and margins from the page. Running headers and // footers (also those that change with the chapter), page numbers and a logo repeated at the top or bottom of the // pages are left out. A page without text (a scan) becomes the rendered page as a picture (PDFium via PDFtoImage), so -// the result is complete; there is no OCR. +// the result is complete. With "Recognize text" (PdfOptions.Ocr) Tesseract reads the scans first, and their invisible +// OCR layer is the page's text (ReadWithOcrLayer). // // Rows laid out with tab stops (a number, a label and a date far to the right) stay one line with tabs at the PDF's // positions, and a line that ends early although the next line's first word would have fitted (an address) keeps its @@ -102,19 +103,28 @@ internal sealed partial class PdfDocumentReader private readonly string _path; private readonly string _mediaFolder; private readonly bool _textOnly; + private readonly bool _ocrLayer; private byte[]? _pdfBytes; private int _pictures; - private PdfDocumentReader(string path, string mediaFolder, bool textOnly) + private PdfDocumentReader(string path, string mediaFolder, bool textOnly, bool ocrLayer = false) { _path = path; _mediaFolder = mediaFolder; _textOnly = textOnly; + _ocrLayer = ocrLayer; } /// Reads with pictures (extracted into ) and scans. public static HDocument Read(string path, string mediaFolder) => new PdfDocumentReader(path, mediaFolder, textOnly: false).ReadDocument(); + /// + /// Like , but the invisible text of a scan (its OCR layer, PdfOptions.Ocr) is the page's text: + /// the words become paragraphs and the scan under them is left out. + /// + public static HDocument ReadWithOcrLayer(string path, string mediaFolder) => + new PdfDocumentReader(path, mediaFolder, textOnly: false, ocrLayer: true).ReadDocument(); + /// /// Reads only the text, including invisible text (the OCR layer of scanned PDFs); pictures and scans are left out. /// @@ -296,16 +306,21 @@ private PageContent ReadPage(Page page) var letters = word.Letters.Where(l => !string.IsNullOrWhiteSpace(l.Value)).ToList(); if (letters.Count == 0) continue; - // Invisible text is the OCR layer of a scan: part of the text, not of the page's look. - if (!_textOnly && letters.All(l => l.RenderingMode == TextRenderingMode.Neither)) + // Invisible text is the OCR layer of a scan: part of the text, not of the page's look (unless OCR was asked for). + var invisible = letters.All(l => l.RenderingMode == TextRenderingMode.Neither); + if (!_textOnly && !_ocrLayer && invisible) continue; var lead = string.IsNullOrWhiteSpace(word.Lead.Value) ? letters[0] : word.Lead; var box = Shift(word.Box, crop); var size = Median(letters.Select(LetterSize)); var center = new PdfPoint((box.Left + box.Right) / 2, (box.Top + box.Bottom) / 2); var link = links.FirstOrDefault(l => Inside(l.Bounds, center)).Uri; + var format = Format(lead, size); + // An OCR layer's font only places the words (Tesseract's GlyphLessFont): the document's own font is used. + if (invisible) + format = format with { Font = null, Bold = false, Italic = false }; words.Add(new TextWord(CleanText(word.Text), box.Left, box.Right, box.Top, box.Bottom, - lead.StartBaseLine.Y - crop.Bottom, Math.Max(1, size), Format(lead, size), link)); + lead.StartBaseLine.Y - crop.Bottom, Math.Max(1, size), format, link)); } if (words.Count == 0 || (rotated && !_textOnly)) diff --git a/src/Filee.Engines/Pdf/PdfTextConverter.cs b/src/Filee.Engines/Pdf/PdfTextConverter.cs index 86859ffb4..d339f54c5 100644 --- a/src/Filee.Engines/Pdf/PdfTextConverter.cs +++ b/src/Filee.Engines/Pdf/PdfTextConverter.cs @@ -2,7 +2,7 @@ // two-column pages), without running headers, footers and page numbers. // // TXT has one paragraph per line and an empty line between paragraphs. The invisible text layer of OCR-processed -// scans is included; scans without one have no text to extract (Filee does no OCR). +// scans is included; scans without one have no text to extract unless the preset recognizes it first (PdfOptions.Ocr). // // Markdown keeps headings, bold / italic and links (MarkdownWriter, as for e-books). The pictures go into a // "_files" folder next to the Markdown file and are linked relatively (MarkdownPictures); a page without text @@ -60,7 +60,9 @@ private static IReadOnlyList Markdown(ConversionStep step, IProgress string.Join(", ", route!.Steps.Select(s => $"{s.Converter.Id}:{s.From}→{s.To}")); + + private static RoutePlanner Planner() => new([ + new FakeConverter("tesseract", OcrEdges), + new FakeConverter("magick", new ConversionEdge("heic", "png"), new ConversionEdge("png", "pdf"), new ConversionEdge("jpg", "pdf")), + new FakeConverter("docx", new ConversionEdge("pdf", "docx")), + new FakeConverter("text", new ConversionEdge("pdf", "txt")), + ]); + + [Fact] + public void Ocr_edges_are_never_part_of_an_ordinary_route() + { + var planner = Planner(); + + Assert.Equal("magick:png→pdf", Steps(planner.Plan("png", "pdf"))); + Assert.Null(planner.Plan("pdf", "pdf")); // no ordinary PDF → PDF edge here + } + + [Fact] + public void Asking_for_ocr_starts_with_tesseract_and_goes_on_from_its_pdf() + { + var planner = Planner(); + + Assert.Equal("tesseract:jpg→pdf", Steps(planner.Plan("jpg", "pdf", ocr: true))); + Assert.Equal("tesseract:pdf→pdf, docx:pdf→docx", Steps(planner.Plan("pdf", "docx", ocr: true))); + Assert.Equal("tesseract:png→pdf, text:pdf→txt", Steps(planner.Plan("png", "txt", ocr: true))); + // A picture Tesseract can't open becomes a PNG first. + Assert.Equal("magick:heic→png, tesseract:png→pdf, docx:pdf→docx", Steps(planner.Plan("heic", "docx", ocr: true))); + } + + [Fact] + public void Without_tesseract_there_is_no_ocr_route() + { + var planner = new RoutePlanner([new FakeConverter("magick", new ConversionEdge("png", "pdf"))]); + + Assert.Null(planner.Plan("png", "pdf", ocr: true)); + Assert.False(planner.CanConvert("png", "pdf", ocr: true)); + } + + [Fact] + public void Text_is_recognized_in_pdfs_and_pictures_for_document_targets() + { + var preset = new Preset { TargetFormat = "docx", Pdf = { Ocr = true } }; + + Assert.True(preset.RecognizesTextOf("pdf")); + Assert.True(preset.RecognizesTextOf("jpg")); + Assert.False(preset.RecognizesTextOf("docx")); // already text + Assert.False(new Preset { TargetFormat = "png", Pdf = { Ocr = true } }.RecognizesTextOf("pdf")); + Assert.False(new Preset { TargetFormat = "docx" }.RecognizesTextOf("pdf")); + } + + [Fact] + public async Task A_job_with_ocr_runs_tesseract_before_the_document_writer() + { + using var dir = new TempDir(); + var scan = dir.File("scan.jpg"); + var tesseract = new FakeRecorder("tesseract", OcrEdges); + var writer = new FakeRecorder("docx", new ConversionEdge("pdf", "docx")); + var magick = new FakeRecorder("magick", new ConversionEdge("jpg", "pdf")); + await using var queue = new JobQueue(new ConverterCatalog([tesseract, writer, magick])); + + var job = new ConversionJob([scan], new Preset { TargetFormat = "docx", Pdf = { Ocr = true } }, "Test"); + await queue.RunAsync(job); + + Assert.Equal(JobState.Completed, job.State); + Assert.Equal([("jpg", "pdf", false)], tesseract.Steps); + Assert.Equal([("pdf", "docx", false)], writer.Steps); + Assert.Empty(magick.Steps); + } + + [Fact] + public async Task Pdfs_merged_into_one_are_recognized_first() + { + using var dir = new TempDir(); + var a = dir.File("a.pdf"); + var b = dir.File("b.jpg"); + var tesseract = new FakeRecorder("tesseract", OcrEdges); + var merger = new FakeMerger(); + await using var queue = new JobQueue(new ConverterCatalog([tesseract]), merger); + + var job = new ConversionJob([a, b], new Preset { TargetFormat = "pdf", Pdf = { Ocr = true, MergeIntoSingle = true } }, "Test"); + await queue.RunAsync(job); + + Assert.Equal(JobState.Completed, job.State); + Assert.Equal([("jpg", "pdf", false), ("pdf", "pdf", false)], tesseract.Steps.OrderBy(s => s.From)); + } +} diff --git a/tests/Filee.Engines.Tests/EngineInstallerTests.cs b/tests/Filee.Engines.Tests/EngineInstallerTests.cs index 8faf7ddf4..a81f6bcab 100644 --- a/tests/Filee.Engines.Tests/EngineInstallerTests.cs +++ b/tests/Filee.Engines.Tests/EngineInstallerTests.cs @@ -318,6 +318,37 @@ public void Every_package_component_is_pinned_in_the_manifest() } /// IProgress that reports synchronously (Progress<T> would post to the thread pool). + [Fact] + public async Task A_tesseract_language_is_its_file_in_a_folder_of_its_own() + { + var data = Encoding.UTF8.GetBytes("lstm"); + var german = Component("deu", "traineddata", data); + var installer = Installer(new FakeServer(new() { [german.Url] = data }), german); + var package = new EnginePackage("ocr-deu", ["deu"], data.Length, ["tesseract"]); + + await installer.InstallAsync(package, null, TestContext.Current.CancellationToken); + + Assert.Equal("lstm", File.ReadAllText(Path.Combine(_root, "deu", "deu.traineddata"))); + Assert.True(installer.IsUpToDate(package)); + } + + [Fact] + public void Tesseract_keeps_only_what_ocr_needs() + { + var root = Path.Combine(_root, "tesseract"); + foreach (var file in new[] { "tesseract.exe", "libtesseract-5.dll", "text2image.exe", "tesseract.1.html", "tesseract-uninstall.exe", + "doc/LICENSE", "tessdata/eng.traineddata", "$PLUGINSDIR/System.dll" }) + { + Directory.CreateDirectory(Path.GetDirectoryName(Path.Combine(root, file))!); + File.WriteAllText(Path.Combine(root, file), "x"); + } + + EngineInstaller.TrimTesseract(root); + + Assert.Equal(["doc/LICENSE", "libtesseract-5.dll", "tesseract.exe"], + Directory.GetFiles(root, "*", SearchOption.AllDirectories).Select(f => Path.GetRelativePath(root, f).Replace('\\', '/')).Order(StringComparer.Ordinal)); + } + private sealed class SyncProgress(Action report) : IProgress { public void Report(EngineInstallProgress value) => report(value); diff --git a/tests/Filee.Engines.Tests/OcrTests.cs b/tests/Filee.Engines.Tests/OcrTests.cs new file mode 100644 index 000000000..7003773da --- /dev/null +++ b/tests/Filee.Engines.Tests/OcrTests.cs @@ -0,0 +1,167 @@ +// Text recognition with Tesseract (issue #288): scans and pictures become searchable PDFs, and their text goes on into +// DOCX and TXT. The real tests need Tesseract (pwsh build/fetch-engines.ps1 -Only 7zip,tesseract) and download the +// language data they read the first time (English, German). + +using System.IO.Compression; +using Filee.Core.Conversion; +using Filee.Core.Presets; +using Filee.Engines.Infrastructure; +using Filee.Engines.Ocr; +using ImageMagick; +using ImageMagick.Drawing; +using PdfSharp.Pdf; +using PdfSharp.Pdf.IO; + +namespace Filee.Engines.Tests; + +public class OcrLanguageListTests +{ + [Fact] + public void Every_language_is_pinned_and_named() + { + Assert.Equal("eng", OcrLanguages.All[0].Code); + Assert.Equal(69, OcrLanguages.All.Count); + Assert.All(OcrLanguages.All, language => + { + Assert.NotEqual(language.Code, language.NativeName); // its culture is known + var component = OcrLanguages.Components[language.Code]; + Assert.Matches("^[0-9A-F]{64}$", component.Sha256); + Assert.True(component.Size > 0); + Assert.Equal($"https://raw.githubusercontent.com/tesseract-ocr/tessdata_fast/4.1.0/{language.Code}.traineddata", component.Url); + }); + Assert.Equal("Deutsch", OcrLanguages.Find("deu")!.NativeName); + Assert.Null(OcrLanguages.Find("klingon")); + } + + [Fact] + public void Tesseract_is_offered_but_not_suggested() + { + var package = EngineDownloads.Packages.Single(p => p.Id == "tesseract"); + + Assert.False(EngineDownloads.IsSuggested(package)); + Assert.Equal(OperatingSystem.IsWindows() ? "tesseract.exe" : "tesseract", Assert.Single(EngineEnvironment.OwnCopyPrograms["tesseract"])); + } +} + +public class OcrTests(EngineFixture fx) : IClassFixture +{ + private static readonly bool Installed = TesseractConverter.Locate() is not null; + + private const string Skip = "Tesseract is not installed (pwsh build/fetch-engines.ps1 -Only 7zip,tesseract)"; + + private static readonly string? Arial = EngineEnvironment.FirstExisting(@"C:\Windows\Fonts\arial.ttf", "/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf", + "/System/Library/Fonts/Supplemental/Arial.ttf"); + + /// A page as a scanner writes it: black text on white at 300 dpi, no text layer. + private static string Scan(string folder, string name, params string[] lines) + { + using var image = new MagickImage(MagickColors.White, 2480, 800) { Density = new Density(300) }; + var drawables = new Drawables().FontPointSize(28).FillColor(MagickColors.Black); + if (Arial is not null) + drawables.Font(Arial); + for (var i = 0; i < lines.Length; i++) + drawables.Text(150, 220 + i * 180, lines[i]); + drawables.Draw(image); + var path = Path.Combine(folder, name); + image.Write(path); + return path; + } + + private static string PageText(string pdf, int page) + { + using var document = UglyToad.PdfPig.PdfDocument.Open(pdf); + return string.Join(" ", document.GetPage(page).GetWords().Select(w => w.Text)); + } + + private static Preset Ocr(string target, string language = "eng") => new() { TargetFormat = target, Pdf = { Ocr = true, OcrLanguage = language } }; + + private static void AssertDone(ConversionJob job) => + Assert.True(job.State == JobState.Completed, string.Join("; ", job.Files.Select(f => $"{f.ErrorKey} {f.ErrorDetail}"))); + + [Fact] + public async Task A_picture_becomes_a_searchable_pdf() + { + Assert.SkipUnless(Installed, Skip); + var scan = Scan(fx.NewFolder(), "letter.png", "Filee reads scanned pages", "into searchable documents"); + + var job = await fx.ConvertAsync([scan], Ocr("pdf")); + + AssertDone(job); + var text = PageText(job.Outputs.Single(), 1); + Assert.Contains("scanned", text); + Assert.Contains("searchable", text); + } + + [Fact] + public async Task A_scanned_pdf_becomes_a_docx_with_its_text_and_without_the_scan() + { + Assert.SkipUnless(Installed, Skip); + var folder = fx.NewFolder(); + var scanPdf = (await fx.ConvertAsync([Scan(folder, "scan.png", "The quarterly report is ready")], new Preset { TargetFormat = "pdf" })).Outputs.Single(); + Assert.Equal("", PageText(scanPdf, 1)); // only a picture + + var job = await fx.ConvertAsync([scanPdf], Ocr("docx")); + + AssertDone(job); + using var docx = ZipFile.OpenRead(job.Outputs.Single()); + using var reader = new StreamReader(docx.GetEntry("word/document.xml")!.Open()); + var xml = await reader.ReadToEndAsync(TestContext.Current.CancellationToken); + Assert.Contains("quarterly", xml); + Assert.DoesNotContain(" Date: Thu, 22 Oct 2026 15:09:39 +0530 Subject: [PATCH 02/11] Recognize text: preset option, languages, Searchable PDF preset, docs The preset editor gets Recognize text (OCR) with a language list for PDF, DOCX, TXT, Markdown and HWPX targets, in all 12 UI languages. Settings schema 33 adds the Searchable PDF preset after Compress PDF. ENGINES.md, THIRD-PARTY-NOTICES.md and the README privacy policy mention Tesseract and its language downloads. --- README.md | 3 +- THIRD-PARTY-NOTICES.md | 2 ++ docs/ENGINES.md | 36 +++++++++++++++++-- src/Filee.App/Assets/i18n/ar.json | 12 +++++++ src/Filee.App/Assets/i18n/en.json | 12 +++++++ src/Filee.App/Assets/i18n/es.json | 12 +++++++ src/Filee.App/Assets/i18n/fr.json | 12 +++++++ src/Filee.App/Assets/i18n/hi.json | 12 +++++++ src/Filee.App/Assets/i18n/ja.json | 12 +++++++ src/Filee.App/Assets/i18n/ko.json | 12 +++++++ src/Filee.App/Assets/i18n/pl.json | 12 +++++++ src/Filee.App/Assets/i18n/pt-BR.json | 12 +++++++ src/Filee.App/Assets/i18n/ru.json | 12 +++++++ src/Filee.App/Assets/i18n/zh-CN.json | 12 +++++++ src/Filee.App/Assets/i18n/zh-TW.json | 12 +++++++ .../ViewModels/PresetEditorViewModel.cs | 23 +++++++++++- src/Filee.App/Views/PresetEditorView.axaml | 21 +++++++++++ src/Filee.Core/Presets/BuiltInData.cs | 14 ++++++++ src/Filee.Core/Settings/AppSettings.cs | 2 +- src/Filee.Core/Settings/UserDataStore.cs | 7 ++++ tests/Filee.Core.Tests/OcrRouteTests.cs | 16 +++++++++ 21 files changed, 262 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index c3ec701c8..3dd033831 100644 --- a/README.md +++ b/README.md @@ -167,7 +167,8 @@ It connects to the internet only: - to ask GitHub (`api.github.com`) whether a newer release exists, shortly after start and then every few hours. Turn this off in Settings → About ("Check for updates automatically"). - to download the conversion engines you choose to install, from the official locations pinned in - [`engines.json`](src/Filee.Engines/Infrastructure/engines.json). + [`engines.json`](src/Filee.Engines/Infrastructure/engines.json), and the text recognition (OCR) languages a preset + uses, pinned in [`ocr-languages.json`](src/Filee.Engines/Infrastructure/ocr-languages.json). - when you click a link, which opens your web browser. ## Sponsor diff --git a/THIRD-PARTY-NOTICES.md b/THIRD-PARTY-NOTICES.md index 2e63361cb..ba86cab1e 100644 --- a/THIRD-PARTY-NOTICES.md +++ b/THIRD-PARTY-NOTICES.md @@ -78,6 +78,8 @@ Downloaded from their official release pages when the user chooses to install th | Microsoft Visual C++ Redistributable (for Ghostscript, conda-forge `vc14_runtime`) | Microsoft Visual C++ Redistributable license | https://github.com/conda-forge/vc-feedstock | | FFmpeg 9.0.2 (gyan.dev "full_build-shared" Windows build; ffmpeg.exe, ffprobe.exe and their libraries, incl. x264, x265, libvpx, LAME, Opus, Vorbis, Theora, OpenCORE AMR) | GPL-3.0-or-later (the build is configured with `--enable-gpl --enable-version3`); FFmpeg itself LGPL-2.1-or-later | https://ffmpeg.org, build: https://www.gyan.dev/ffmpeg/builds/ (source: https://github.com/GyanD/codexffmpeg) | | calibre (ebook-convert) | GPL-3.0 | https://calibre-ebook.com | +| Tesseract OCR 5.4 (UB Mannheim Windows build, with Leptonica and the MinGW libraries it ships; their notices are in its `doc` folder) | Apache-2.0 (Tesseract); Leptonica BSD-2-Clause; others as listed in the build | https://github.com/tesseract-ocr/tesseract, build: https://github.com/UB-Mannheim/tesseract | +| Tesseract language data (tessdata_fast 4.1.0), downloaded per language when used | Apache-2.0 | https://github.com/tesseract-ocr/tessdata_fast | These programs run as separate processes. Their source code is available from the linked projects. diff --git a/docs/ENGINES.md b/docs/ENGINES.md index 3ba038b0c..45e9f6f2f 100644 --- a/docs/ENGINES.md +++ b/docs/ENGINES.md @@ -32,6 +32,7 @@ Filee chooses engines automatically. Settings → *Engines* shows their status, | 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 | +| 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) | | 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 | | 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) | @@ -62,10 +63,11 @@ 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 and Ghostscript installers put it. Settings → *Engines* shows it as "Found on this PC", with a note when + 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 and Ghostscript on macOS, LibreOffice and Ghostscript on Linux), +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. @@ -90,6 +92,11 @@ they can be installed or removed later in Settings → *Engines*. A conversion t 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. ### Microsoft C++ runtime (Windows) @@ -104,7 +111,7 @@ the runtime next to every program that needs it and does not ship it (`Infrastru | Ghostscript (download) | `vcruntime` component copied next to `gswin64c.exe` (`EngineInstaller`, `fetch-engines.ps1`) | | LibreOffice (download) | its own copy, which the administrative install leaves in `System64`, copied into `program/` (`EngineInstaller`, `fetch-engines.ps1`; `LibreOfficeConverter` also repairs a copy installed by an earlier Filee) | | calibre, Java (downloads) | ship their own next to their programs (`app/bin`, `bin`) | -| 7-Zip, FFmpeg (GyanD), Pandoc, PDFium, SkiaSharp / HarfBuzz, Magick.Native, the Explorer menu DLL, .NET | import neither (static runtime or MinGW / GHC builds) | +| 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. @@ -1450,3 +1457,26 @@ format, and FFmpeg reads what the file holds, so a `.dat` with no video in it fa `nclx` sRGB, BT.601 full range when there is none) and Exif (orientation reset to 1, thumbnail removed) as a `cdsc` item. Checked with libheif (Magick.NET), FFmpeg's HEIF demuxer and Windows' HEIF/HEVC extensions; a smooth 2000 × 1500 picture is a quarter the size of a JPG at the same PSNR. + +## Text recognition (OCR) + +"Recognize text" (`PdfOptions.Ocr`, issue #288) in a preset for PDF, DOCX, TXT, Markdown or HWPX reads the text of +scans and pictures with Tesseract (`Ocr/TesseractConverter.cs`), in the language the preset names +(`PdfOptions.OcrLanguage`, a Tesseract code; English by default). The built-in "Searchable PDF" preset turns it on. + +- The route planner puts an OCR edge first (`ConversionEdge.Ocr`, `RoutePlanner.Plan(ocr: true)`): picture or PDF → + searchable PDF, then the cheapest ordinary route from that PDF. A picture Tesseract can't open (HEIC, PSD, …) + becomes a PNG first. OCR edges are never part of an ordinary route. +- A searchable PDF keeps the page as it looks and puts the recognized words on it as invisible text. PDF pages that + already have text (visible or an OCR layer) are copied as they are; only the others are rendered at 300 dpi + (PDFium) and recognized, several pages at a time with one Tesseract thread each (`OMP_THREAD_LIMIT=1`). A PDF whose + pages all have text is copied with a note. The new document keeps the pages and the document information, not + bookmarks or form fields. +- DOCX, Markdown and HWPX read the OCR layer as the page's text (`PdfDocumentReader.ReadWithOcrLayer`): the words + become paragraphs, the scan behind them is left out, and Tesseract's placeholder font is dropped. Without OCR an + OCR layer stays invisible there and the page stays a picture, as before. TXT always reads it. +- PDFs merged into one ("Merge PDF" with OCR) are recognized before they are merged. +- Pictures without a resolution are read at 300 dpi; Tesseract would otherwise assume 70 dpi and make a huge page. +- The language data is copied into the job's folder and passed with `--tessdata-dir`, so a system Tesseract reads + only Filee's pinned files; `-c tessedit_create_pdf=1` replaces the `pdf` config file that lives in Tesseract's own + tessdata folder. diff --git a/src/Filee.App/Assets/i18n/ar.json b/src/Filee.App/Assets/i18n/ar.json index 7ee866f47..25605b8d4 100644 --- a/src/Filee.App/Assets/i18n/ar.json +++ b/src/Filee.App/Assets/i18n/ar.json @@ -75,6 +75,7 @@ "format_request.notice_text": "أخبرنا بالصيغة التي تحتاجها.\nتساعد الطلبات في تحديد الصيغ التي سيدعمها Filee لاحقًا.\nلا يمكن تحويل الملفات المشفّرة أو المحمية من النسخ (DRM)، مهما كانت صيغتها.", "error.no_route": "لا يوجد محرك يستطيع إجراء هذا التحويل", "error.cannot_turn": "التدوير والانعكاس متاحان للصور فقط", + "error.ocr_language_download": "تعذّر تنزيل لغة التعرّف الضوئي OCR", "error.too_many_frames": "عدد الإطارات كبير جدًا لحفظها كصور. زِد قيمة «كل إطار رقم N»", "error.not_enough_space": "لا توجد مساحة كافية على القرص لحفظ الصور", "error.no_motion_photo": "لا تحتوي هذه الصورة على فيديو يستطيع Filee قراءته (صورة Motion Photo من Android، أو Live Photo من iPhone مع ملف MOV بجانبها)", @@ -99,6 +100,7 @@ "error.geo_unsupported_shape": "هذا النوع من الأشكال غير مدعوم", "error.geo_too_large": "سيتجاوز ملف Shapefile حد الصيغة البالغ 2 غيغابايت", "notice.pdf_not_smaller": "تعذّر تقليل الحجم، لذا نُسخ الملف كما هو", + "notice.ocr_has_text": "كل الصفحات تحتوي على نص بالفعل، لذا نُسخ الملف كما هو", "notice.live_photo_pair": "حُفظ كصورة وملف MOV بالاسم نفسه. استوردهما معًا (صور iCloud أو AirDrop أو تطبيق الصور على Mac) لتحصل على Live Photo", "notice.geo_datum_assumed": "المرجع الجيوديسي ليس WGS 84؛ حُوّلت المواقع على افتراض أنه كذلك، وقد تنحرف بمقدار يصل إلى مئات الأمتار", "notice.geo_gpx_lossy": "تحتفظ GPX بالنقاط والمسارات والطرق مع حقول محدودة فقط: حُذفت السمات الأخرى وتحولت المضلعات إلى مسارات", @@ -138,6 +140,7 @@ "engine.ffmpeg.description": "الفيديو والصوت: MP4 وMOV وMKV وWEBM وMP3 وM4A وWAV وFLAC وغيرها.\nواستخراج الصوت والترجمة، وإنشاء رسوم GIF وAPNG المتحركة، وحفظ الصور بصيغة HEIC.", "engine.pandoc.description": "التحويل بين Markdown وHTML وبين ODT وRTF، ومن DOCX إلى Markdown\nالتحويل بين reStructuredText وLaTeX وOrg وAsciiDoc وTypst وMediaWiki وDocBook وJupyter وغيرها وبين Markdown وHTML وDOCX وODT\nتحويل Markdown وOrg إلى شرائح PPTX", "engine.ghostscript.description": "تحويل EPS وPostScript وملفات Illustrator القديمة إلى PDF وصور؛ وتحويل PDF إلى EPS وPS.", + "engine.tesseract.description": "يتعرّف على نص الصور والمستندات الممسوحة (OCR): PDF قابل للبحث وDOCX وTXT.", "engine.calibre.description": "صيغ الكتب الإلكترونية النادرة (LIT وLRF وCHM وPDB وRB…) وإخراج Kindle ‏(MOBI وAZW3).\nعند وضعه فوق محرك كتابة HWPX، يُنشئ أيضًا PDF من الكتب الإلكترونية (بسرعة أكبر بكثير للكتب الطويلة).", "engine.libreoffice.description": "قراءة الصيغ القديمة والأقل شيوعًا: DOC وPPT وPages وKeynote وNumbers وWordPerfect وClarisWorks وPublisher وVisio…\nوالحفظ بصيغ DOC وXLS وPPT وODT وODP وقوالب OpenDocument وEMF.", "profile.images": "الصور", @@ -200,6 +203,7 @@ "merge.order_hint": "حدّد ملفًا وانقله إلى أعلى أو أسفل. تُدمج الملفات بالترتيب من الأعلى إلى الأسفل. (‎Alt+↑ / Alt+↓‎)", "preset.split_pdf": "تقسيم PDF", "preset.compress_pdf": "ضغط PDF", + "preset.searchable_pdf": "PDF قابل للبحث", "preset.long_image": "صورة طويلة", "preset.pdf_slides": "PPTX (شرائح)", "preset.extract": "استخراج", @@ -508,6 +512,10 @@ "presets.pdf_compression_hint": "تُصغّر الصور وتُحفَظ بصيغة JPEG.\nتبقى النصوص والخطوط والرسومات كما هي.\nإذا لم تكن النتيجة أصغر، يُحتفَظ بالأصل.", "presets.pdf_ghostscript": "تجربة Ghostscript أيضًا (عند تثبيته)", "presets.pdf_ghostscript_hint": "غالبًا ما يُنتج حجمًا أصغر، لكنه يعيد كتابة الملف بالكامل.\nتُحفَظ النتيجة الأصغر.", + "presets.ocr": "التعرّف على النص (OCR)", + "presets.ocr_hint": "يصبح نص المستندات الممسوحة والصور قابلًا للبحث والتحرير. يتطلب Tesseract (الإعدادات ← المحركات).\nتبقى الصفحات التي تحتوي على نص كما هي.", + "presets.ocr_language": "لغة النص", + "presets.ocr_language_size": "تنزيل {0}", "presets.pdf_keep_metadata": "الاحتفاظ بالبيانات الوصفية للصفحات والصور (XMP)", "presets.combine_pages_hint": "يضع كل الصفحات تحت بعضها في صورة واحدة.\nتُصغّر النتائج الطويلة جدًا إلى حد الصيغة (JPG ‏65,500 بكسل، WebP ‏16,383 بكسل).", "presets.combine_pages": "تجميع الصفحات في صورة طويلة واحدة (عند تحويل PDF)", @@ -638,6 +646,8 @@ "engines.package.pandoc.description": "يضيف التحويل بين Markdown وHTML وبين ODT وRTF، ومن DOCX إلى Markdown، ويدعم LaTeX وOrg وAsciiDoc وTypst وJupyter وغيرها من لغات الترميز.\nوينشئ شرائح PPTX من Markdown.\nيعمل تحويل Markdown وHTML إلى PDF وDOCX وHWPX بدونه.", "engines.package.ghostscript.name": "Ghostscript (EPS وPostScript)", "engines.package.ghostscript.description": "تحويل EPS وPS وملفات Illustrator القديمة إلى PDF وصور، وتحويل PDF أو SVG إلى EPS وPS.\nتُحوَّل ملفات SVG وAI المتوافقة مع PDF بدونه.", + "engines.package.tesseract.name": "Tesseract (التعرّف على النص، OCR)", + "engines.package.tesseract.description": "يقرأ نص ملفات PDF الممسوحة والصور: PDF قابل للبحث وDOCX وTXT.\nتُنزَّل كل لغة عند استخدامها أول مرة.", "engines.package.ffmpeg.name": "FFmpeg (الفيديو والصوت)", "engines.package.ffmpeg.description": "يضيف تحويل الفيديو والصوت، وتحويل الفيديو إلى GIF أو صورة ثابتة، واستخراج الصوت من الفيديو، وتحويل GIF إلى MP4.\nمطلوب للفيديو والصوت فقط.", "engines.package.calibre.name": "Calibre (Kindle وصيغ الكتب النادرة)", @@ -647,10 +657,12 @@ "engines.guidance.macos": "ثبّت {0} لهذا الـ Mac، ثم اختر «استخدام نسختي…» وحدّد البرنامج أو مجلد ‎.app الخاص به.", "engines.guidance.macos.ffmpeg": "ثبّته باستخدام Homebrew ‏(brew install ffmpeg)، ثم اختر «التحقق مجددًا».\nأو اختر «استخدام نسختي…» وحدّد مجلد bin الخاص به.", "engines.guidance.macos.ghostscript": "ثبّته باستخدام Homebrew ‏(brew install ghostscript)، ثم اختر «التحقق مجددًا».\nأو اختر «استخدام نسختي…» وحدّد مجلد bin الخاص به.", + "engines.guidance.macos.tesseract": "ثبّته باستخدام Homebrew ‏(brew install tesseract)، ثم اختر «التحقق مجددًا».\nأو اختر «استخدام نسختي…» وحدّد مجلد bin الخاص به.", "engines.guidance.macos.calibre": "يتطلب calibre الذي ينزّله Filee نظام macOS 14 أو أحدث. ثبّت calibre 7.26، آخر إصدار لهذا النظام، من calibre-ebook.com، ثم اختر «استخدام نسختي…» وحدّد مجلد ‎.app الخاص به.", "engines.guidance.linux": "ثبّت {0} باستخدام مدير الحزم في توزيعتك، ثم اختر «استخدام نسختي…» وحدّد مجلد البرنامج.", "engines.guidance.linux.libreoffice": "ثبّته باستخدام مدير الحزم (على Ubuntu/Debian: sudo apt install libreoffice)، ثم اختر «التحقق مجددًا».\nيتطلب فتح HWP أيضًا Java وإضافة H2Orestart.", "engines.guidance.linux.ghostscript": "ثبّته باستخدام مدير الحزم (على Ubuntu/Debian: sudo apt install ghostscript)، ثم اختر «التحقق مجددًا».", + "engines.guidance.linux.tesseract": "ثبّته باستخدام مدير الحزم (على Ubuntu/Debian: sudo apt install tesseract-ocr)، ثم اختر «التحقق مجددًا».", "engines.guidance.windows": "لا يوفر Filee تنزيلًا لـ {0} لهذا الحاسوب.\nاختر «استخدام نسختي…» وحدّد نسخة مثبّتة.", "engines.use_own_app_folder": "اختر المجلد الذي يحتوي على {0}.app", "engines.progress_detail": "{0} من {1} · {2}/ث · {3}", diff --git a/src/Filee.App/Assets/i18n/en.json b/src/Filee.App/Assets/i18n/en.json index d6024c303..67aca553a 100644 --- a/src/Filee.App/Assets/i18n/en.json +++ b/src/Filee.App/Assets/i18n/en.json @@ -81,6 +81,7 @@ "format_request.notice_text": "Tell us which format you need.\nRequests decide what Filee supports next.\nEncrypted or copy-protected (DRM) files can't be converted, whatever the format.", "error.no_route": "No engine can do this conversion", "error.cannot_turn": "Rotate and flip work on images only", + "error.ocr_language_download": "Couldn't download the OCR language", "error.too_many_frames": "Too many frames to save as pictures, raise \"Every Nth frame\"", "error.not_enough_space": "Not enough free disk space for the pictures", "error.no_motion_photo": "This photo has no video Filee can read (an Android Motion Photo, or an iPhone Live Photo with its MOV next to it)", @@ -105,6 +106,7 @@ "error.geo_unsupported_shape": "This kind of shape isn't supported", "error.geo_too_large": "The Shapefile would pass the format's 2 GB limit", "notice.pdf_not_smaller": "Couldn't make it smaller, so it was copied as it is", + "notice.ocr_has_text": "Every page already has text, so it was copied as it is", "notice.live_photo_pair": "Saved as a photo and a MOV with the same name. Import both together (iCloud Photos, AirDrop or Photos on a Mac) to get a Live Photo", "notice.geo_datum_assumed": "The datum isn't WGS 84; positions were converted as if it were and can be off by up to hundreds of metres", "notice.geo_gpx_lossy": "GPX holds only points, routes and tracks with a few fields: other attributes were left out and polygons became tracks", @@ -145,6 +147,7 @@ "engine.ffmpeg.description": "Video and audio: MP4, MOV, MKV, WEBM, MP3, M4A, WAV, FLAC and more.\nAlso extracts sound and subtitles, makes GIF and APNG animations and saves images as HEIC.", "engine.pandoc.description": "Markdown and HTML ↔ ODT and RTF, DOCX → Markdown\nreStructuredText, LaTeX, Org, AsciiDoc, Typst, MediaWiki, DocBook, Jupyter and more ↔ Markdown, HTML, DOCX and ODT\nMarkdown and Org → PPTX slides", "engine.ghostscript.description": "EPS, PostScript and older Illustrator files to PDF and images; PDF to EPS and PS.", + "engine.tesseract.description": "Recognizes the text of scans and pictures (OCR): searchable PDF, DOCX, TXT.", "engine.calibre.description": "Rare e-book formats (LIT, LRF, CHM, PDB, RB…) and Kindle output (MOBI, AZW3).\nAbove the HWPX writer, it also makes the PDFs of e-books (much faster for long books).", "engine.libreoffice.description": "Reads older and less common formats: DOC, PPT, Pages, Keynote, Numbers, WordPerfect, ClarisWorks, Publisher, Visio…\nAlso saves as DOC, XLS, PPT, ODT, ODP, OpenDocument templates and EMF.", @@ -210,6 +213,7 @@ "merge.order_hint": "Select a file and move it up or down. Files are merged from top to bottom. (Alt+↑ / Alt+↓)", "preset.split_pdf": "Split PDF", "preset.compress_pdf": "Compress PDF", + "preset.searchable_pdf": "Searchable PDF", "preset.long_image": "Long image", "preset.pdf_slides": "PPTX (slides)", "preset.extract": "Extract", @@ -523,6 +527,10 @@ "presets.pdf_compression_hint": "Pictures are scaled down and saved as JPEG.\nText, fonts and drawings stay as they are.\nIf the result isn't smaller, the original is kept.", "presets.pdf_ghostscript": "Also try Ghostscript (when installed)", "presets.pdf_ghostscript_hint": "Often smaller, but it rewrites the whole file.\nThe smallest result is kept.", + "presets.ocr": "Recognize text (OCR)", + "presets.ocr_hint": "Scans and pictures get text you can search and edit. Needs Tesseract (Settings → Engines).\nPages that already have text stay as they are.", + "presets.ocr_language": "Language of the text", + "presets.ocr_language_size": "{0} download", "presets.pdf_keep_metadata": "Keep page and picture metadata (XMP)", "presets.combine_pages_hint": "Puts all pages one under another in a single image.\nVery long results are scaled to the format's limit (JPG 65,500 px, WebP 16,383 px).", "presets.combine_pages": "Stack the pages into one tall image (when a PDF is converted)", @@ -655,6 +663,8 @@ "engines.package.pandoc.description": "Adds Markdown and HTML ↔ ODT and RTF, DOCX → Markdown, and LaTeX, Org, AsciiDoc, Typst, Jupyter and other markup.\nAlso makes PPTX slides from Markdown.\nMarkdown and HTML → PDF, DOCX and HWPX work without it.", "engines.package.ghostscript.name": "Ghostscript (EPS and PostScript)", "engines.package.ghostscript.description": "EPS, PS and older Illustrator files → PDF and images, PDF or SVG → EPS/PS.\nSVG and PDF-compatible AI files convert without it.", + "engines.package.tesseract.name": "Tesseract (text recognition, OCR)", + "engines.package.tesseract.description": "Reads the text of scanned PDFs and pictures: searchable PDF, DOCX, TXT.\nEach language downloads the first time it is used.", "engines.package.ffmpeg.name": "FFmpeg (video and audio)", "engines.package.ffmpeg.description": "Adds video and audio conversion, video → GIF or still image, sound from videos and GIF → MP4.\nOnly needed for video and audio.", "engines.package.calibre.name": "Calibre (Kindle and rare e-book formats)", @@ -664,10 +674,12 @@ "engines.guidance.macos": "Install {0} for this Mac, then choose “Use my copy…” and select its program or .app folder.", "engines.guidance.macos.ffmpeg": "Install it with Homebrew (brew install ffmpeg), then choose “Check again”.\nOr choose “Use my copy…” and select its bin folder.", "engines.guidance.macos.ghostscript": "Install it with Homebrew (brew install ghostscript), then choose “Check again”.\nOr choose “Use my copy…” and select its bin folder.", + "engines.guidance.macos.tesseract": "Install it with Homebrew (brew install tesseract), then choose “Check again”.\nOr choose “Use my copy…” and select its bin folder.", "engines.guidance.macos.calibre": "The calibre Filee downloads needs macOS 14 or later. Install calibre 7.26, the last version for this macOS, from calibre-ebook.com, then choose “Use my copy…” and select its .app folder.", "engines.guidance.linux": "Install {0} with your distribution’s package manager, then choose “Use my copy…” and select its program folder.", "engines.guidance.linux.libreoffice": "Install it with your package manager (Ubuntu/Debian: sudo apt install libreoffice), then choose “Check again”.\nOpening HWP also needs Java and the H2Orestart extension.", "engines.guidance.linux.ghostscript": "Install it with your package manager (Ubuntu/Debian: sudo apt install ghostscript), then choose “Check again”.", + "engines.guidance.linux.tesseract": "Install it with your package manager (Ubuntu/Debian: sudo apt install tesseract-ocr), then choose “Check again”.", "engines.guidance.windows": "Filee has no download of {0} for this PC.\nChoose “Use my copy…” and select an installed copy.", "engines.use_own_app_folder": "Choose the folder containing {0}.app", "engines.progress_detail": "{0} of {1} · {2}/s · {3}", diff --git a/src/Filee.App/Assets/i18n/es.json b/src/Filee.App/Assets/i18n/es.json index 89007a3ee..9e1deb1af 100644 --- a/src/Filee.App/Assets/i18n/es.json +++ b/src/Filee.App/Assets/i18n/es.json @@ -75,6 +75,7 @@ "format_request.notice_text": "Cuéntanos qué formato necesitas.\nLas solicitudes determinan qué formatos admitirá Filee a continuación.\nLos archivos cifrados o con protección de copia (DRM) no se pueden convertir, sea cual sea su formato.", "error.no_route": "Ningún motor puede hacer esta conversión", "error.cannot_turn": "Rotar y voltear solo funcionan con imágenes", + "error.ocr_language_download": "No se pudo descargar el idioma de OCR", "error.too_many_frames": "Demasiados fotogramas para guardar como imágenes; aumenta «Cada N fotogramas»", "error.not_enough_space": "Espacio libre en disco insuficiente para las imágenes", "error.no_motion_photo": "Esta foto no tiene un vídeo que Filee pueda leer (una Motion Photo de Android o una Live Photo de iPhone con su MOV al lado)", @@ -99,6 +100,7 @@ "error.geo_unsupported_shape": "Este tipo de forma no es compatible", "error.geo_too_large": "El Shapefile superaría el límite de 2 GB del formato", "notice.pdf_not_smaller": "No se pudo reducir su tamaño, por lo que se copió tal cual", + "notice.ocr_has_text": "Todas las páginas ya tienen texto, así que se copió tal cual", "notice.live_photo_pair": "Guardado como una foto y un MOV con el mismo nombre. Impórtalos juntos (Fotos de iCloud, AirDrop o Fotos en un Mac) para obtener una Live Photo", "notice.geo_datum_assumed": "El datum no es WGS 84; las posiciones se convirtieron como si lo fuera y pueden desviarse hasta cientos de metros", "notice.geo_gpx_lossy": "GPX solo guarda puntos, rutas y tracks con pocos campos: se omitieron los demás atributos y los polígonos pasaron a ser tracks", @@ -138,6 +140,7 @@ "engine.ffmpeg.description": "Vídeo y audio: MP4, MOV, MKV, WEBM, MP3, M4A, WAV, FLAC y más.\nTambién extrae sonido y subtítulos, crea animaciones GIF y APNG y guarda imágenes como HEIC.", "engine.pandoc.description": "Markdown y HTML ↔ ODT y RTF, DOCX → Markdown\nreStructuredText, LaTeX, Org, AsciiDoc, Typst, MediaWiki, DocBook, Jupyter y más ↔ Markdown, HTML, DOCX y ODT\nMarkdown y Org → diapositivas PPTX", "engine.ghostscript.description": "EPS, PostScript y archivos antiguos de Illustrator a PDF e imágenes; PDF a EPS y PS.", + "engine.tesseract.description": "Reconoce el texto de escaneos e imágenes (OCR): PDF con búsqueda, DOCX, TXT.", "engine.calibre.description": "Formatos raros de libros electrónicos (LIT, LRF, CHM, PDB, RB…) y salida Kindle (MOBI, AZW3).\nPor encima del escritor HWPX, también crea los PDF de libros electrónicos (mucho más rápido para libros largos).", "engine.libreoffice.description": "Lee formatos antiguos y poco habituales: DOC, PPT, Pages, Keynote, Numbers, WordPerfect, ClarisWorks, Publisher, Visio…\nTambién guarda como DOC, XLS, PPT, ODT, ODP, plantillas OpenDocument y EMF.", "profile.images": "Imágenes", @@ -200,6 +203,7 @@ "merge.order_hint": "Selecciona un archivo y muévelo arriba o abajo. Los archivos se combinan de arriba abajo. (Alt+↑ / Alt+↓)", "preset.split_pdf": "Dividir PDF", "preset.compress_pdf": "Comprimir PDF", + "preset.searchable_pdf": "PDF con búsqueda", "preset.long_image": "Imagen larga", "preset.pdf_slides": "PPTX (diapositivas)", "preset.extract": "Extraer", @@ -508,6 +512,10 @@ "presets.pdf_compression_hint": "Las imágenes se reducen y se guardan como JPEG.\nEl texto, las fuentes y los dibujos permanecen iguales.\nSi el resultado no es más pequeño, se conserva el original.", "presets.pdf_ghostscript": "Probar también con Ghostscript (si está instalado)", "presets.pdf_ghostscript_hint": "A menudo es más pequeño, pero reescribe todo el archivo.\nSe conserva el resultado más pequeño.", + "presets.ocr": "Reconocer texto (OCR)", + "presets.ocr_hint": "Los escaneos e imágenes tendrán texto que se puede buscar y editar. Requiere Tesseract (Ajustes → Motores).\nLas páginas que ya tienen texto no cambian.", + "presets.ocr_language": "Idioma del texto", + "presets.ocr_language_size": "Descarga de {0}", "presets.pdf_keep_metadata": "Conservar metadatos de página e imagen (XMP)", "presets.combine_pages_hint": "Pone todas las páginas una debajo de otra en una sola imagen.\nLos resultados muy largos se escalan al límite del formato (JPG 65 500 px, WebP 16 383 px).", "presets.combine_pages": "Apilar las páginas en una imagen alta (cuando se convierte un PDF)", @@ -638,6 +646,8 @@ "engines.package.pandoc.description": "Añade Markdown y HTML ↔ ODT y RTF, DOCX → Markdown, y LaTeX, Org, AsciiDoc, Typst, Jupyter y otros lenguajes de marcado.\nTambién crea diapositivas PPTX desde Markdown.\nMarkdown y HTML → PDF, DOCX y HWPX funcionan sin él.", "engines.package.ghostscript.name": "Ghostscript (EPS y PostScript)", "engines.package.ghostscript.description": "EPS, PS y archivos antiguos de Illustrator → PDF e imágenes, PDF o SVG → EPS/PS.\nSVG y AI compatibles con PDF se convierten sin él.", + "engines.package.tesseract.name": "Tesseract (reconocimiento de texto, OCR)", + "engines.package.tesseract.description": "Lee el texto de PDF escaneados e imágenes: PDF con búsqueda, DOCX, TXT.\nCada idioma se descarga la primera vez que se usa.", "engines.package.ffmpeg.name": "FFmpeg (vídeo y audio)", "engines.package.ffmpeg.description": "Añade conversión de vídeo y audio, vídeo → GIF o imagen fija, sonido de vídeos y GIF → MP4.\nSolo es necesario para vídeo y audio.", "engines.package.calibre.name": "Calibre (Kindle y libros electrónicos poco habituales)", @@ -647,10 +657,12 @@ "engines.guidance.macos": "Instala {0} para este Mac y luego elige «Usar mi copia…» y selecciona su programa o su carpeta .app.", "engines.guidance.macos.ffmpeg": "Instálalo con Homebrew (brew install ffmpeg) y luego elige «Comprobar otra vez».\nO bien elige «Usar mi copia…» y selecciona su carpeta bin.", "engines.guidance.macos.ghostscript": "Instálalo con Homebrew (brew install ghostscript) y luego elige «Comprobar otra vez».\nO bien elige «Usar mi copia…» y selecciona su carpeta bin.", + "engines.guidance.macos.tesseract": "Instálalo con Homebrew (brew install tesseract) y luego elige «Comprobar otra vez».\nO bien elige «Usar mi copia…» y selecciona su carpeta bin.", "engines.guidance.macos.calibre": "El calibre que descarga Filee necesita macOS 14 o posterior. Instala calibre 7.26, la última versión para este macOS, desde calibre-ebook.com y luego elige «Usar mi copia…» y selecciona su carpeta .app.", "engines.guidance.linux": "Instala {0} con el gestor de paquetes de tu distribución y luego elige «Usar mi copia…» y selecciona su carpeta de programa.", "engines.guidance.linux.libreoffice": "Instálalo con tu gestor de paquetes (Ubuntu/Debian: sudo apt install libreoffice) y luego elige «Comprobar otra vez».\nAbrir HWP necesita además Java y la extensión H2Orestart.", "engines.guidance.linux.ghostscript": "Instálalo con tu gestor de paquetes (Ubuntu/Debian: sudo apt install ghostscript) y luego elige «Comprobar otra vez».", + "engines.guidance.linux.tesseract": "Instálalo con tu gestor de paquetes (Ubuntu/Debian: sudo apt install tesseract-ocr) y luego elige «Comprobar otra vez».", "engines.guidance.windows": "Filee no tiene una descarga de {0} para este PC.\nElige «Usar mi copia…» y selecciona una copia instalada.", "engines.use_own_app_folder": "Elige la carpeta que contiene {0}.app", "engines.progress_detail": "{0} de {1} · {2}/s · {3}", diff --git a/src/Filee.App/Assets/i18n/fr.json b/src/Filee.App/Assets/i18n/fr.json index 321978517..3b27cd878 100644 --- a/src/Filee.App/Assets/i18n/fr.json +++ b/src/Filee.App/Assets/i18n/fr.json @@ -81,6 +81,7 @@ "format_request.notice_text": "Dites-nous de quel format vous avez besoin.\nLes demandes décident de ce que Filee prendra en charge ensuite.\nLes fichiers chiffrés ou protégés contre la copie (DRM) ne peuvent pas être convertis, quel que soit le format.", "error.no_route": "Aucun moteur ne peut effectuer cette conversion", "error.cannot_turn": "La rotation et le retournement ne s'appliquent qu'aux images", + "error.ocr_language_download": "Impossible de télécharger la langue de l'OCR", "error.too_many_frames": "Trop d'images à enregistrer, augmentez « Une image sur N »", "error.not_enough_space": "Pas assez d'espace disque libre pour les images", "error.no_motion_photo": "Cette photo ne contient aucune vidéo lisible par Filee (une Motion Photo Android, ou une Live Photo d'iPhone avec son MOV à côté)", @@ -105,6 +106,7 @@ "error.geo_unsupported_shape": "Ce type de forme n'est pas pris en charge", "error.geo_too_large": "Le Shapefile dépasserait la limite de 2 Go du format", "notice.pdf_not_smaller": "Impossible de réduire sa taille : le fichier a été copié tel quel", + "notice.ocr_has_text": "Toutes les pages ont déjà du texte : copié tel quel", "notice.live_photo_pair": "Enregistré en une photo et un MOV de même nom. Importez-les ensemble (Photos iCloud, AirDrop ou Photos sur Mac) pour obtenir une Live Photo", "notice.geo_datum_assumed": "Le datum n'est pas WGS 84 ; les positions ont été converties comme s'il l'était et peuvent être décalées de plusieurs centaines de mètres", "notice.geo_gpx_lossy": "GPX ne contient que des points, des routes et des traces avec quelques champs : les autres attributs ont été omis et les polygones sont devenus des traces", @@ -145,6 +147,7 @@ "engine.ffmpeg.description": "Vidéo et audio : MP4, MOV, MKV, WEBM, MP3, M4A, WAV, FLAC et plus.\nExtrait aussi le son et les sous-titres, crée des animations GIF et APNG et enregistre des images en HEIC.", "engine.pandoc.description": "Markdown et HTML ↔ ODT et RTF, DOCX → Markdown\nreStructuredText, LaTeX, Org, AsciiDoc, Typst, MediaWiki, DocBook, Jupyter et plus ↔ Markdown, HTML, DOCX et ODT\nMarkdown et Org → diapositives PPTX", "engine.ghostscript.description": "EPS, PostScript et anciens fichiers Illustrator en PDF et images ; PDF en EPS et PS.", + "engine.tesseract.description": "Reconnaît le texte des numérisations et des images (OCR) : PDF consultable, DOCX, TXT.", "engine.calibre.description": "Formats de livres numériques rares (LIT, LRF, CHM, PDB, RB…) et sortie Kindle (MOBI, AZW3).\nPlacé au-dessus du rédacteur HWPX, il produit aussi les PDF des livres numériques (bien plus vite pour les longs livres).", "engine.libreoffice.description": "Lit les formats anciens et moins courants : DOC, PPT, Pages, Keynote, Numbers, WordPerfect, ClarisWorks, Publisher, Visio…\nEnregistre aussi en DOC, XLS, PPT, ODT, ODP, modèles OpenDocument et EMF.", @@ -210,6 +213,7 @@ "merge.order_hint": "Sélectionnez un fichier et déplacez-le vers le haut ou le bas. Les fichiers sont fusionnés de haut en bas. (Alt+↑ / Alt+↓)", "preset.split_pdf": "Diviser le PDF", "preset.compress_pdf": "Compresser le PDF", + "preset.searchable_pdf": "PDF consultable", "preset.long_image": "Image longue", "preset.pdf_slides": "PPTX (diapos)", "preset.extract": "Extraire", @@ -523,6 +527,10 @@ "presets.pdf_compression_hint": "Les images sont réduites et enregistrées en JPEG.\nLe texte, les polices et les dessins restent intacts.\nSi le résultat n'est pas plus léger, l'original est conservé.", "presets.pdf_ghostscript": "Essayer aussi Ghostscript (s'il est installé)", "presets.pdf_ghostscript_hint": "Souvent plus léger, mais il réécrit tout le fichier.\nLe résultat le plus léger est conservé.", + "presets.ocr": "Reconnaître le texte (OCR)", + "presets.ocr_hint": "Le texte des numérisations et des images devient consultable et modifiable. Nécessite Tesseract (Paramètres → Moteurs).\nLes pages qui ont déjà du texte restent telles quelles.", + "presets.ocr_language": "Langue du texte", + "presets.ocr_language_size": "Téléchargement de {0}", "presets.pdf_keep_metadata": "Conserver les métadonnées des pages et des images (XMP)", "presets.combine_pages_hint": "Place toutes les pages les unes sous les autres dans une seule image.\nLes résultats très longs sont réduits à la limite du format (JPG 65 500 px, WebP 16 383 px).", "presets.combine_pages": "Empiler les pages en une seule image haute (lors de la conversion d'un PDF)", @@ -655,6 +663,8 @@ "engines.package.pandoc.description": "Ajoute Markdown et HTML ↔ ODT et RTF, DOCX → Markdown, ainsi que LaTeX, Org, AsciiDoc, Typst, Jupyter et d'autres langages de balisage.\nCrée aussi des diapositives PPTX à partir de Markdown.\nMarkdown et HTML → PDF, DOCX et HWPX fonctionnent sans lui.", "engines.package.ghostscript.name": "Ghostscript (EPS et PostScript)", "engines.package.ghostscript.description": "Fichiers EPS, PS et anciens Illustrator → PDF et images, PDF ou SVG → EPS/PS.\nLes fichiers SVG et AI compatibles PDF se convertissent sans lui.", + "engines.package.tesseract.name": "Tesseract (reconnaissance de texte, OCR)", + "engines.package.tesseract.description": "Lit le texte des PDF numérisés et des images : PDF consultable, DOCX, TXT.\nChaque langue est téléchargée à sa première utilisation.", "engines.package.ffmpeg.name": "FFmpeg (vidéo et audio)", "engines.package.ffmpeg.description": "Ajoute la conversion vidéo et audio, vidéo → GIF ou image fixe, le son des vidéos et GIF → MP4.\nNécessaire uniquement pour la vidéo et l'audio.", "engines.package.calibre.name": "Calibre (Kindle et formats de livres numériques rares)", @@ -664,10 +674,12 @@ "engines.guidance.macos": "Installez {0} pour ce Mac, puis choisissez « Utiliser ma copie… » et sélectionnez son programme ou son dossier .app.", "engines.guidance.macos.ffmpeg": "Installez-le avec Homebrew (brew install ffmpeg), puis choisissez « Revérifier ».\nOu choisissez « Utiliser ma copie… » et sélectionnez son dossier bin.", "engines.guidance.macos.ghostscript": "Installez-le avec Homebrew (brew install ghostscript), puis choisissez « Revérifier ».\nOu choisissez « Utiliser ma copie… » et sélectionnez son dossier bin.", + "engines.guidance.macos.tesseract": "Installez-le avec Homebrew (brew install tesseract), puis choisissez « Revérifier ».\nOu choisissez « Utiliser ma copie… » et sélectionnez son dossier bin.", "engines.guidance.macos.calibre": "Le calibre téléchargé par Filee nécessite macOS 14 ou ultérieur. Installez calibre 7.26, dernière version pour ce macOS, depuis calibre-ebook.com, puis choisissez « Utiliser ma copie… » et sélectionnez son dossier .app.", "engines.guidance.linux": "Installez {0} avec le gestionnaire de paquets de votre distribution, puis choisissez « Utiliser ma copie… » et sélectionnez le dossier du programme.", "engines.guidance.linux.libreoffice": "Installez-le avec votre gestionnaire de paquets (Ubuntu/Debian : sudo apt install libreoffice), puis choisissez « Revérifier ».\nL'ouverture des HWP nécessite aussi Java et l'extension H2Orestart.", "engines.guidance.linux.ghostscript": "Installez-le avec votre gestionnaire de paquets (Ubuntu/Debian : sudo apt install ghostscript), puis choisissez « Revérifier ».", + "engines.guidance.linux.tesseract": "Installez-le avec votre gestionnaire de paquets (Ubuntu/Debian : sudo apt install tesseract-ocr), puis choisissez « Revérifier ».", "engines.guidance.windows": "Filee ne propose pas de téléchargement de {0} pour ce PC.\nChoisissez « Utiliser ma copie… » et sélectionnez une copie installée.", "engines.use_own_app_folder": "Choisissez le dossier contenant {0}.app", "engines.progress_detail": "{0} sur {1} · {2}/s · {3}", diff --git a/src/Filee.App/Assets/i18n/hi.json b/src/Filee.App/Assets/i18n/hi.json index dde786072..80decab9f 100644 --- a/src/Filee.App/Assets/i18n/hi.json +++ b/src/Filee.App/Assets/i18n/hi.json @@ -75,6 +75,7 @@ "format_request.notice_text": "हमें बताएँ कि आपको कौन-सा फ़ॉर्मैट चाहिए।\nअनुरोधों से तय होता है कि Filee आगे क्या समर्थन करेगा।\nएन्क्रिप्टेड या कॉपी-सुरक्षित (DRM) फ़ाइलें किसी भी फ़ॉर्मैट में कन्वर्ट नहीं की जा सकतीं।", "error.no_route": "कोई भी इंजन यह कन्वर्ज़न नहीं कर सकता", "error.cannot_turn": "घुमाना और पलटना केवल इमेज पर काम करता है", + "error.ocr_language_download": "OCR भाषा डाउनलोड नहीं हो सकी", "error.too_many_frames": "तस्वीरों के रूप में सहेजने के लिए बहुत ज़्यादा फ़्रेम हैं, \"हर Nवाँ फ़्रेम\" बढ़ाएँ", "error.not_enough_space": "तस्वीरों के लिए डिस्क पर पर्याप्त खाली जगह नहीं है", "error.no_motion_photo": "इस फ़ोटो में ऐसा कोई वीडियो नहीं है जिसे Filee पढ़ सके (Android मोशन फ़ोटो, या पास में अपनी MOV फ़ाइल वाली iPhone लाइव फ़ोटो)", @@ -99,6 +100,7 @@ "error.geo_unsupported_shape": "इस तरह का आकार समर्थित नहीं है", "error.geo_too_large": "Shapefile फ़ॉर्मैट की 2 GB सीमा से बड़ी हो जाएगी", "notice.pdf_not_smaller": "इसे छोटा नहीं किया जा सका, इसलिए जैसा है वैसा ही कॉपी कर दिया गया", + "notice.ocr_has_text": "हर पेज में पहले से टेक्स्ट है, इसलिए फ़ाइल जैसी थी वैसी कॉपी की गई", "notice.live_photo_pair": "एक ही नाम की फ़ोटो और MOV के रूप में सहेजा गया। लाइव फ़ोटो पाने के लिए दोनों को साथ में इम्पोर्ट करें (iCloud Photos, AirDrop या Mac पर Photos)", "notice.geo_datum_assumed": "डेटम WGS 84 नहीं है। स्थितियाँ ऐसे कन्वर्ट की गईं मानो वह WGS 84 हो, इसलिए वे कुछ सौ मीटर तक खिसक सकती हैं", "notice.geo_gpx_lossy": "GPX में केवल पॉइंट, रूट और ट्रैक कुछ फ़ील्ड के साथ रहते हैं: बाकी एट्रिब्यूट छोड़ दिए गए और पॉलीगॉन ट्रैक बन गए", @@ -138,6 +140,7 @@ "engine.ffmpeg.description": "वीडियो और ऑडियो: MP4, MOV, MKV, WEBM, MP3, M4A, WAV, FLAC और अन्य।\nआवाज़ और सबटाइटल निकालता है, GIF और APNG एनिमेशन बनाता है और इमेज को HEIC में सहेजता है।", "engine.pandoc.description": "Markdown और HTML ↔ ODT और RTF, DOCX → Markdown\nreStructuredText, LaTeX, Org, AsciiDoc, Typst, MediaWiki, DocBook, Jupyter और अन्य ↔ Markdown, HTML, DOCX और ODT\nMarkdown और Org → PPTX स्लाइड", "engine.ghostscript.description": "EPS, PostScript और पुरानी Illustrator फ़ाइलें PDF और इमेज में; PDF से EPS और PS।", + "engine.tesseract.description": "स्कैन और तस्वीरों का टेक्स्ट पहचानता है (OCR): खोजने योग्य PDF, DOCX, TXT।", "engine.calibre.description": "दुर्लभ ई-बुक फ़ॉर्मैट (LIT, LRF, CHM, PDB, RB…) और Kindle आउटपुट (MOBI, AZW3)।\nHWPX writer के ऊपर होने पर यह ई-बुक के PDF भी बनाता है (लंबी किताबों के लिए बहुत तेज़)।", "engine.libreoffice.description": "पुराने और कम प्रचलित फ़ॉर्मैट पढ़ता है: DOC, PPT, Pages, Keynote, Numbers, WordPerfect, ClarisWorks, Publisher, Visio…\nDOC, XLS, PPT, ODT, ODP, OpenDocument टेम्पलेट और EMF के रूप में सहेजता भी है।", "profile.images": "इमेज", @@ -200,6 +203,7 @@ "merge.order_hint": "फ़ाइल चुनकर उसे ऊपर या नीचे ले जाएँ। फ़ाइलें ऊपर से नीचे के क्रम में मर्ज होंगी। (Alt+↑ / Alt+↓)", "preset.split_pdf": "PDF बाँटें", "preset.compress_pdf": "PDF कंप्रेस करें", + "preset.searchable_pdf": "खोजने योग्य PDF", "preset.long_image": "लंबी इमेज", "preset.pdf_slides": "PPTX (स्लाइड)", "preset.extract": "निकालें", @@ -508,6 +512,10 @@ "presets.pdf_compression_hint": "तस्वीरों को छोटा करके JPEG के रूप में सहेजा जाता है।\nटेक्स्ट, फ़ॉन्ट और ड्रॉइंग जैसे हैं वैसे ही रहते हैं।\nअगर नतीजा छोटा नहीं होता, तो मूल फ़ाइल रखी जाती है।", "presets.pdf_ghostscript": "Ghostscript भी आज़माएँ (इंस्टॉल होने पर)", "presets.pdf_ghostscript_hint": "अक्सर और छोटा होता है, लेकिन पूरी फ़ाइल को दोबारा लिखता है।\nसबसे छोटा नतीजा रखा जाता है।", + "presets.ocr": "टेक्स्ट पहचानें (OCR)", + "presets.ocr_hint": "स्कैन और तस्वीरों का टेक्स्ट खोजने और बदलने योग्य बनता है। Tesseract चाहिए (सेटिंग्स → इंजन)।\nजिन पेजों में पहले से टेक्स्ट है वे वैसे ही रहते हैं।", + "presets.ocr_language": "टेक्स्ट की भाषा", + "presets.ocr_language_size": "{0} डाउनलोड", "presets.pdf_keep_metadata": "पेज और तस्वीर का मेटाडेटा (XMP) रखें", "presets.combine_pages_hint": "सभी पेजों को एक के नीचे एक रखकर एक इमेज बनाता है।\nबहुत लंबे नतीजे फ़ॉर्मैट की सीमा तक स्केल किए जाते हैं (JPG 65,500 px, WebP 16,383 px)।", "presets.combine_pages": "पेजों को एक लंबी इमेज में जोड़ें (जब PDF कन्वर्ट हो)", @@ -638,6 +646,8 @@ "engines.package.pandoc.description": "Markdown और HTML ↔ ODT और RTF, DOCX → Markdown, और LaTeX, Org, AsciiDoc, Typst, Jupyter व अन्य मार्कअप जोड़ता है।\nMarkdown से PPTX स्लाइड भी बनाता है।\nMarkdown और HTML → PDF, DOCX और HWPX इसके बिना भी काम करते हैं।", "engines.package.ghostscript.name": "Ghostscript (EPS और PostScript)", "engines.package.ghostscript.description": "EPS, PS और पुरानी Illustrator फ़ाइलें → PDF और इमेज, PDF या SVG → EPS/PS।\nSVG और PDF-संगत AI फ़ाइलें इसके बिना भी कन्वर्ट होती हैं।", + "engines.package.tesseract.name": "Tesseract (टेक्स्ट पहचान, OCR)", + "engines.package.tesseract.description": "स्कैन किए गए PDF और तस्वीरों का टेक्स्ट पढ़ता है: खोजने योग्य PDF, DOCX, TXT।\nहर भाषा पहली बार इस्तेमाल पर डाउनलोड होती है।", "engines.package.ffmpeg.name": "FFmpeg (वीडियो और ऑडियो)", "engines.package.ffmpeg.description": "वीडियो और ऑडियो कन्वर्ज़न, वीडियो → GIF या स्टिल इमेज, वीडियो से आवाज़ और GIF → MP4 जोड़ता है।\nकेवल वीडियो और ऑडियो के लिए ज़रूरी।", "engines.package.calibre.name": "Calibre (Kindle और दुर्लभ ई-बुक फ़ॉर्मैट)", @@ -647,10 +657,12 @@ "engines.guidance.macos": "इस Mac के लिए {0} इंस्टॉल करें, फिर “मेरी कॉपी इस्तेमाल करें…” चुनें और उसका प्रोग्राम या .app फ़ोल्डर चुनें।", "engines.guidance.macos.ffmpeg": "इसे Homebrew से इंस्टॉल करें (brew install ffmpeg), फिर “फिर से जाँचें” चुनें।\nया “मेरी कॉपी इस्तेमाल करें…” चुनें और उसका bin फ़ोल्डर चुनें।", "engines.guidance.macos.ghostscript": "इसे Homebrew से इंस्टॉल करें (brew install ghostscript), फिर “फिर से जाँचें” चुनें।\nया “मेरी कॉपी इस्तेमाल करें…” चुनें और उसका bin फ़ोल्डर चुनें।", + "engines.guidance.macos.tesseract": "इसे Homebrew से इंस्टॉल करें (brew install tesseract), फिर “फिर से जाँचें” चुनें।\nया “मेरी कॉपी इस्तेमाल करें…” चुनें और उसका bin फ़ोल्डर चुनें।", "engines.guidance.macos.calibre": "Filee जो calibre डाउनलोड करता है, उसके लिए macOS 14 या नया चाहिए। calibre-ebook.com से calibre 7.26 इंस्टॉल करें (इस macOS के लिए आख़िरी वर्ज़न), फिर “मेरी कॉपी इस्तेमाल करें…” चुनें और उसका .app फ़ोल्डर चुनें।", "engines.guidance.linux": "अपने डिस्ट्रीब्यूशन के पैकेज मैनेजर से {0} इंस्टॉल करें, फिर “मेरी कॉपी इस्तेमाल करें…” चुनें और उसका प्रोग्राम फ़ोल्डर चुनें।", "engines.guidance.linux.libreoffice": "इसे अपने पैकेज मैनेजर से इंस्टॉल करें (Ubuntu/Debian: sudo apt install libreoffice), फिर “फिर से जाँचें” चुनें।\nHWP खोलने के लिए Java और H2Orestart एक्सटेंशन भी चाहिए।", "engines.guidance.linux.ghostscript": "इसे अपने पैकेज मैनेजर से इंस्टॉल करें (Ubuntu/Debian: sudo apt install ghostscript), फिर “फिर से जाँचें” चुनें।", + "engines.guidance.linux.tesseract": "इसे अपने पैकेज मैनेजर से इंस्टॉल करें (Ubuntu/Debian: sudo apt install tesseract-ocr), फिर “फिर से जाँचें” चुनें।", "engines.guidance.windows": "इस PC के लिए Filee के पास {0} का डाउनलोड नहीं है।\n“मेरी कॉपी इस्तेमाल करें…” चुनें और इंस्टॉल की गई कॉपी चुनें।", "engines.use_own_app_folder": "वह फ़ोल्डर चुनें जिसमें {0}.app है", "engines.progress_detail": "{1} में से {0} · {2}/s · {3}", diff --git a/src/Filee.App/Assets/i18n/ja.json b/src/Filee.App/Assets/i18n/ja.json index afc5f5d33..8bb389b37 100644 --- a/src/Filee.App/Assets/i18n/ja.json +++ b/src/Filee.App/Assets/i18n/ja.json @@ -75,6 +75,7 @@ "format_request.notice_text": "必要な形式を教えてください。\nリクエストをもとに、今後Fileeが対応する形式を決めます。\n暗号化またはコピー保護(DRM)されたファイルは、形式にかかわらず変換できません。", "error.no_route": "この変換に対応するエンジンがありません", "error.cannot_turn": "回転と反転は画像でのみ使えます", + "error.ocr_language_download": "OCR の言語データをダウンロードできませんでした", "error.too_many_frames": "画像として保存するフレーム数が多すぎます。「Nフレームごと」の値を大きくしてください", "error.not_enough_space": "画像を保存するための空き容量が不足しています", "error.no_motion_photo": "この写真には、Fileeが読み取れる動画がありません(Androidのモーションフォト、または隣にMOVがあるiPhoneのLive Photo)", @@ -99,6 +100,7 @@ "error.geo_unsupported_shape": "この種類の図形には対応していません", "error.geo_too_large": "Shapefile形式の上限(2 GB)を超えてしまいます", "notice.pdf_not_smaller": "サイズを小さくできなかったため、元のままコピーしました", + "notice.ocr_has_text": "すべてのページにテキストがあるため、元のままコピーしました", "notice.live_photo_pair": "同じ名前の写真とMOVとして保存しました。両方を一緒に取り込むと(iCloud写真、AirDrop、Macの写真アプリ)Live Photoになります", "notice.geo_datum_assumed": "測地系がWGS 84ではありません。WGS 84とみなして変換したため、位置が最大で数百メートルずれることがあります", "notice.geo_gpx_lossy": "GPXに保存できるのはポイント、ルート、トラックと一部の項目だけです。そのほかの属性は省き、ポリゴンはトラックにしました", @@ -138,6 +140,7 @@ "engine.ffmpeg.description": "動画と音声(MP4、MOV、MKV、WEBM、MP3、M4A、WAV、FLACなど)を変換します。\n音声や字幕の抽出、GIFやAPNGアニメーションの作成、画像のHEIC保存にも対応します。", "engine.pandoc.description": "Markdown、HTMLとODT、RTFを相互変換し、DOCXをMarkdownに変換します。\nreStructuredText、LaTeX、Org、AsciiDoc、Typst、MediaWiki、DocBook、JupyterなどをMarkdown、HTML、DOCX、ODTと相互変換します。\nMarkdown、OrgからPPTXスライドを作成します。", "engine.ghostscript.description": "EPS、PostScript、古いIllustratorファイルをPDFや画像に変換し、PDFをEPSやPSに変換します。", + "engine.tesseract.description": "スキャンや画像の文字を認識します(OCR):検索可能な PDF、DOCX、TXT。", "engine.calibre.description": "一般的でない電子書籍形式(LIT、LRF、CHM、PDB、RBなど)を変換し、Kindle用のMOBI、AZW3形式で保存できます。\n「HWPX writer」より上に置くと、電子書籍のPDF作成もCalibreが担当します(長い本では大幅に速くなります)。", "engine.libreoffice.description": "古い形式や一般的でない形式(DOC、PPT、Pages、Keynote、Numbers、WordPerfect、ClarisWorks、Publisher、Visioなど)を読み込みます。\nDOC、XLS、PPT、ODT、ODP、OpenDocumentテンプレート、EMF形式での保存にも対応します。", "profile.images": "画像", @@ -200,6 +203,7 @@ "merge.order_hint": "ファイルを選択して上下に移動します。上から順に結合されます。(Alt+↑ / Alt+↓)", "preset.split_pdf": "分割", "preset.compress_pdf": "圧縮", + "preset.searchable_pdf": "検索可能なPDF", "preset.long_image": "縦連結", "preset.pdf_slides": "PPTX (画像)", "preset.extract": "展開", @@ -508,6 +512,10 @@ "presets.pdf_compression_hint": "画像を縮小し、JPEGで保存します。\nテキスト、フォント、図形はそのまま保持されます。\n圧縮後のサイズが小さくならない場合は、元のファイルを保持します。", "presets.pdf_ghostscript": "Ghostscriptも試す(インストール済みの場合)", "presets.pdf_ghostscript_hint": "さらに小さくできることがありますが、ファイル全体を書き換えます。\n最も小さい結果を保持します。", + "presets.ocr": "文字を認識する(OCR)", + "presets.ocr_hint": "スキャンや画像の文字を検索・編集できるようにします。Tesseract が必要です(「設定」→「変換エンジン」)。\nすでにテキストがあるページはそのままです。", + "presets.ocr_language": "テキストの言語", + "presets.ocr_language_size": "{0} をダウンロード", "presets.pdf_keep_metadata": "ページと画像のメタデータ(XMP)を保持", "presets.combine_pages_hint": "すべてのページを縦に並べて1枚の画像にします。\n長すぎる場合は形式の上限に合わせて縮小されます(JPG:65,500 px、WebP:16,383 px)。", "presets.combine_pages": "ページを縦に並べて1枚の画像にする(PDFを変換する場合)", @@ -638,6 +646,8 @@ "engines.package.pandoc.description": "Markdown・HTMLとODT・RTFの相互変換、DOCX → Markdown、LaTeX、Org、AsciiDoc、Typst、Jupyterなどのマークアップ形式に対応します。\nMarkdownからPPTXスライドを作成できます。\nMarkdown・HTML → PDF、DOCX、HWPXの変換には必要ありません。", "engines.package.ghostscript.name": "Ghostscript(EPS、PostScript)", "engines.package.ghostscript.description": "EPS、PS、古いIllustratorファイル → PDF・画像、PDF・SVG → EPS・PSの変換に使います。\nSVGやPDF互換のAIファイルは、インストールしなくても変換できます。", + "engines.package.tesseract.name": "Tesseract(文字認識、OCR)", + "engines.package.tesseract.description": "スキャンした PDF や画像の文字を読み取ります:検索可能な PDF、DOCX、TXT。\n言語は初めて使うときにダウンロードされます。", "engines.package.ffmpeg.name": "FFmpeg(動画・音声)", "engines.package.ffmpeg.description": "動画・音声の変換、動画 → GIF・静止画、動画からの音声抽出、GIF → MP4に対応します。\n動画と音声の変換にのみ必要です。", "engines.package.calibre.name": "Calibre(Kindle形式・一般的でない電子書籍形式)", @@ -647,10 +657,12 @@ "engines.guidance.macos": "このMacに{0}をインストールし、「手元のプログラムを使う…」を選んで、プログラムまたは.appフォルダーを指定してください。", "engines.guidance.macos.ffmpeg": "Homebrewでffmpegをインストール(brew install ffmpeg)してから、「再確認」を選んでください。\nまたは「手元のプログラムを使う…」を選び、binフォルダーを指定してください。", "engines.guidance.macos.ghostscript": "Homebrewでghostscriptをインストール(brew install ghostscript)してから、「再確認」を選んでください。\nまたは「手元のプログラムを使う…」を選び、binフォルダーを指定してください。", + "engines.guidance.macos.tesseract": "HomebrewでTesseractをインストール(brew install tesseract)してから、「再確認」を選んでください。\nまたは「手元のプログラムを使う…」を選び、binフォルダーを指定してください。", "engines.guidance.macos.calibre": "FileeがダウンロードするcalibreにはmacOS 14以降が必要です。calibre-ebook.comからこのmacOS用の最終版calibre 7.26をインストールし、「手元のプログラムを使う…」を選んで.appフォルダーを指定してください。", "engines.guidance.linux": "ディストリビューションのパッケージマネージャーで{0}をインストールしてから、「手元のプログラムを使う…」を選び、プログラムのフォルダーを指定してください。", "engines.guidance.linux.libreoffice": "パッケージマネージャーでインストール(Ubuntu/Debian:sudo apt install libreoffice)してから、「再確認」を選んでください。\nHWPを開くには、JavaとH2Orestart拡張機能も必要です。", "engines.guidance.linux.ghostscript": "パッケージマネージャーでインストール(Ubuntu/Debian:sudo apt install ghostscript)してから、「再確認」を選んでください。", + "engines.guidance.linux.tesseract": "パッケージマネージャーでインストール(Ubuntu/Debian:sudo apt install tesseract-ocr)してから、「再確認」を選んでください。", "engines.guidance.windows": "このPC向けの{0}はFileeからダウンロードできません。\n「手元のプログラムを使う…」を選び、インストール済みのものを指定してください。", "engines.use_own_app_folder": "{0}.appを含むフォルダーを選択してください", "engines.progress_detail": "{0}/{1} · {2}/秒 · {3}", diff --git a/src/Filee.App/Assets/i18n/ko.json b/src/Filee.App/Assets/i18n/ko.json index 115bfc7e1..df8171065 100644 --- a/src/Filee.App/Assets/i18n/ko.json +++ b/src/Filee.App/Assets/i18n/ko.json @@ -81,6 +81,7 @@ "format_request.notice_text": "필요한 형식을 알려주세요.\n요청을 보고 다음에 지원할 형식을 정해요.\n암호화되거나 DRM이 걸린 파일은 변환할 수 없어요.", "error.no_route": "이 변환을 할 수 있는 엔진이 없어요", "error.cannot_turn": "회전·반전은 이미지에만 할 수 있어요", + "error.ocr_language_download": "OCR 언어 데이터를 내려받지 못했어요", "error.too_many_frames": "프레임이 너무 많아요. 'N번째 프레임마다' 값을 늘려 주세요", "error.not_enough_space": "이미지를 저장할 디스크 공간이 부족해요", "error.no_motion_photo": "이 사진에서 읽을 수 있는 동영상을 찾지 못했어요 (안드로이드 모션 포토, 또는 옆에 MOV가 있는 아이폰 라이브 포토)", @@ -105,6 +106,7 @@ "error.geo_unsupported_shape": "지원하지 않는 도형 종류예요", "error.geo_too_large": "Shapefile의 한계인 2GB를 넘어요", "notice.pdf_not_smaller": "더 작게 만들 수 없어서 그대로 복사했어요", + "notice.ocr_has_text": "모든 페이지에 이미 텍스트가 있어서 그대로 복사했어요", "notice.live_photo_pair": "같은 이름의 사진과 MOV로 저장했어요. 두 파일을 함께 가져오면(iCloud 사진, AirDrop, Mac의 사진 앱) 라이브 포토가 돼요", "notice.geo_datum_assumed": "측지계가 WGS 84가 아니라서 같은 것으로 보고 변환했어요. 위치가 수백 m까지 어긋날 수 있어요", "notice.geo_gpx_lossy": "GPX는 점·경로·트랙과 몇 가지 항목만 담아요. 다른 속성은 빠졌고 면은 트랙으로 바뀌었어요", @@ -145,6 +147,7 @@ "engine.ffmpeg.description": "MP4, MOV, MKV, WEBM, MP3, M4A, WAV, FLAC 등 동영상·오디오를 변환해요.\n소리·자막 추출, GIF·APNG 만들기, 이미지 → HEIC도 해요.", "engine.pandoc.description": "마크다운·HTML ↔ ODT·RTF, DOCX → 마크다운\nreStructuredText·LaTeX·Org·AsciiDoc·Typst·MediaWiki·DocBook·Jupyter 등 ↔ 마크다운·HTML·DOCX·ODT\n마크다운·Org → PPTX 슬라이드", "engine.ghostscript.description": "EPS, 포스트스크립트, 예전 일러스트레이터 파일을 PDF와 이미지로, PDF를 EPS·PS로.", + "engine.tesseract.description": "스캔본과 사진의 글자를 인식합니다(OCR): 검색 가능한 PDF, DOCX, TXT.", "engine.calibre.description": "드문 전자책 형식(LIT, LRF, CHM, PDB, RB…)과 킨들용 출력(MOBI, AZW3).\nHWPX writer보다 위에 두면 전자책 PDF도 만들어요(긴 책은 훨씬 빨라요).", "engine.libreoffice.description": "DOC, PPT, Pages, Keynote, Numbers, 워드퍼펙트, ClarisWorks, Publisher, Visio 같은 오래되었거나 드문 형식을 읽어요.\nDOC·XLS·PPT·ODT·ODP, 오픈도큐먼트 서식 파일, EMF로 저장도 해요.", @@ -210,6 +213,7 @@ "merge.order_hint": "파일을 골라 위아래로 옮기세요. 위에서부터 순서대로 병합돼요. (Alt+↑ / Alt+↓)", "preset.split_pdf": "PDF 분할", "preset.compress_pdf": "PDF 압축", + "preset.searchable_pdf": "검색 가능한 PDF", "preset.long_image": "긴 이미지", "preset.pdf_slides": "PPTX (슬라이드)", "preset.extract": "압축 풀기", @@ -523,6 +527,10 @@ "presets.pdf_compression_hint": "이미지를 줄이고 JPEG로 저장해요.\n글자, 글꼴, 도형은 그대로예요.\n결과가 더 작지 않으면 원본을 그대로 둬요.", "presets.pdf_ghostscript": "Ghostscript로도 해 보기 (설치된 경우)", "presets.pdf_ghostscript_hint": "더 작아질 때가 많지만 파일 전체를 다시 써요.\n가장 작은 결과를 남겨요.", + "presets.ocr": "텍스트 인식 (OCR)", + "presets.ocr_hint": "스캔본과 사진의 글자를 검색하고 편집할 수 있게 됩니다. Tesseract가 필요해요(설정 → 변환 엔진).\n이미 텍스트가 있는 페이지는 그대로 둡니다.", + "presets.ocr_language": "텍스트 언어", + "presets.ocr_language_size": "{0} 내려받기", "presets.pdf_keep_metadata": "페이지·이미지 메타데이터(XMP) 유지", "presets.combine_pages_hint": "모든 페이지를 위아래로 이어 이미지 한 장으로 만들어요.\n너무 길면 형식의 최대 높이에 맞게 줄여요(JPG 65,500px, WebP 16,383px).", "presets.combine_pages": "모든 페이지를 세로로 이어 한 장으로 (PDF를 변환할 때)", @@ -655,6 +663,8 @@ "engines.package.pandoc.description": "마크다운·HTML ↔ ODT·RTF, DOCX → 마크다운, LaTeX·Org·AsciiDoc·Typst·Jupyter 같은 마크업 변환을 추가해요.\n마크다운으로 PPTX 슬라이드도 만들어요.\n마크다운·HTML → PDF·DOCX·HWPX는 없어도 돼요.", "engines.package.ghostscript.name": "Ghostscript (EPS·PostScript)", "engines.package.ghostscript.description": "EPS·PS와 예전 일러스트레이터 파일을 PDF·이미지로, PDF·SVG를 EPS·PS로 바꿔요.\nSVG와 PDF 호환 AI 파일은 없어도 돼요.", + "engines.package.tesseract.name": "Tesseract (텍스트 인식, OCR)", + "engines.package.tesseract.description": "스캔한 PDF와 사진의 글자를 읽습니다: 검색 가능한 PDF, DOCX, TXT.\n언어는 처음 쓸 때 내려받습니다.", "engines.package.ffmpeg.name": "FFmpeg (동영상·오디오)", "engines.package.ffmpeg.description": "동영상·오디오 변환, 동영상 → GIF·정지 이미지, 소리 추출, GIF → MP4를 추가해요.\n동영상과 오디오에만 필요해요.", "engines.package.calibre.name": "Calibre (킨들과 드문 전자책 형식)", @@ -664,10 +674,12 @@ "engines.guidance.macos": "이 Mac에 {0}을(를) 설치한 뒤 “내 프로그램 쓰기…”로 프로그램이나 .app 폴더를 선택해 주세요.", "engines.guidance.macos.ffmpeg": "Homebrew로 설치(brew install ffmpeg)한 뒤 “다시 확인”을 누르세요.\n또는 “내 프로그램 쓰기…”로 bin 폴더를 고르세요.", "engines.guidance.macos.ghostscript": "Homebrew로 설치(brew install ghostscript)한 뒤 “다시 확인”을 누르세요.\n또는 “내 프로그램 쓰기…”로 bin 폴더를 고르세요.", + "engines.guidance.macos.tesseract": "Homebrew로 설치(brew install tesseract)한 뒤 “다시 확인”을 누르세요.\n또는 “내 프로그램 쓰기…”로 bin 폴더를 고르세요.", "engines.guidance.macos.calibre": "Filee가 받는 calibre는 macOS 14 이상이 필요해요. calibre-ebook.com에서 이 macOS용 마지막 버전인 calibre 7.26을 설치한 뒤 “내 프로그램 쓰기…”로 .app 폴더를 선택해 주세요.", "engines.guidance.linux": "배포판의 패키지 관리자로 {0}을(를) 설치한 뒤 “내 프로그램 쓰기…”로 프로그램 폴더를 선택해 주세요.", "engines.guidance.linux.libreoffice": "패키지 관리자로 설치(Ubuntu/Debian: sudo apt install libreoffice)한 뒤 “다시 확인”을 누르세요.\nHWP를 열려면 Java와 H2Orestart 확장도 필요해요.", "engines.guidance.linux.ghostscript": "패키지 관리자로 설치(Ubuntu/Debian: sudo apt install ghostscript)한 뒤 “다시 확인”을 누르세요.", + "engines.guidance.linux.tesseract": "패키지 관리자로 설치(Ubuntu/Debian: sudo apt install tesseract-ocr)한 뒤 “다시 확인”을 누르세요.", "engines.guidance.windows": "이 PC용 {0} 다운로드는 없어요.\n“내 프로그램 쓰기…”로 설치된 프로그램을 선택해 주세요.", "engines.use_own_app_folder": "{0}.app이 들어 있는 폴더 선택", "engines.progress_detail": "{0} / {1} · {2}/s · {3}", diff --git a/src/Filee.App/Assets/i18n/pl.json b/src/Filee.App/Assets/i18n/pl.json index 2e2da36b6..aeee94d3e 100644 --- a/src/Filee.App/Assets/i18n/pl.json +++ b/src/Filee.App/Assets/i18n/pl.json @@ -81,6 +81,7 @@ "format_request.notice_text": "Napisz, jakiego formatu potrzebujesz.\nNa podstawie próśb wybieramy kolejne obsługiwane formaty.\nPlików zaszyfrowanych lub chronionych przed kopiowaniem (DRM) nie da się konwertować, niezależnie od formatu.", "error.no_route": "Żaden silnik nie wykona tej konwersji", "error.cannot_turn": "Obracanie i odbicie działają tylko na obrazach", + "error.ocr_language_download": "Nie udało się pobrać języka OCR", "error.too_many_frames": "Za dużo klatek do zapisania jako obrazy, zwiększ „Co N-ta klatka”", "error.not_enough_space": "Za mało wolnego miejsca na dysku na obrazy", "error.no_motion_photo": "To zdjęcie nie ma wideo, które Filee może odczytać (Motion Photo z Androida lub Live Photo z iPhone'a z plikiem MOV obok)", @@ -105,6 +106,7 @@ "error.geo_unsupported_shape": "Ten rodzaj kształtu nie jest obsługiwany", "error.geo_too_large": "Shapefile przekroczyłby limit 2 GB tego formatu", "notice.pdf_not_smaller": "Nie udało się zmniejszyć pliku, więc skopiowano go bez zmian", + "notice.ocr_has_text": "Każda strona ma już tekst, więc skopiowano bez zmian", "notice.live_photo_pair": "Zapisano jako zdjęcie i plik MOV o tej samej nazwie. Zaimportuj je razem (Zdjęcia iCloud, AirDrop lub Zdjęcia na Macu), aby otrzymać Live Photo", "notice.geo_datum_assumed": "Datum nie jest WGS 84; pozycje przeliczono tak, jakby było, więc mogą się różnić nawet o setki metrów", "notice.geo_gpx_lossy": "GPX przechowuje tylko punkty, trasy i ślady z kilkoma polami: pozostałe atrybuty pominięto, a poligony stały się śladami", @@ -145,6 +147,7 @@ "engine.ffmpeg.description": "Wideo i audio: MP4, MOV, MKV, WEBM, MP3, M4A, WAV, FLAC i inne.\nWyodrębnia też dźwięk i napisy, tworzy animacje GIF i APNG oraz zapisuje obrazy jako HEIC.", "engine.pandoc.description": "Markdown i HTML ↔ ODT i RTF, DOCX → Markdown\nreStructuredText, LaTeX, Org, AsciiDoc, Typst, MediaWiki, DocBook, Jupyter i inne ↔ Markdown, HTML, DOCX i ODT\nMarkdown i Org → slajdy PPTX", "engine.ghostscript.description": "EPS, PostScript i starsze pliki Illustratora do PDF i obrazów; PDF do EPS i PS.", + "engine.tesseract.description": "Rozpoznaje tekst skanów i obrazów (OCR): przeszukiwalny PDF, DOCX, TXT.", "engine.calibre.description": "Rzadkie formaty e-booków (LIT, LRF, CHM, PDB, RB…) i zapis dla Kindle (MOBI, AZW3).\nUmieszczony nad HWPX writer tworzy też PDF-y e-booków (znacznie szybciej przy długich książkach).", "engine.libreoffice.description": "Odczytuje starsze i rzadsze formaty: DOC, PPT, Pages, Keynote, Numbers, WordPerfect, ClarisWorks, Publisher, Visio…\nZapisuje też jako DOC, XLS, PPT, ODT, ODP, szablony OpenDocument i EMF.", @@ -210,6 +213,7 @@ "merge.order_hint": "Wybierz plik i przesuń go w górę lub w dół. Pliki są scalane od góry do dołu. (Alt+↑ / Alt+↓)", "preset.split_pdf": "Podziel PDF", "preset.compress_pdf": "Kompresuj PDF", + "preset.searchable_pdf": "Przeszukiwalny PDF", "preset.long_image": "Długi obraz", "preset.pdf_slides": "PPTX (slajdy)", "preset.extract": "Rozpakuj", @@ -523,6 +527,10 @@ "presets.pdf_compression_hint": "Obrazy są pomniejszane i zapisywane jako JPEG.\nTekst, czcionki i rysunki pozostają bez zmian.\nJeśli wynik nie jest mniejszy, zostaje oryginał.", "presets.pdf_ghostscript": "Spróbuj też Ghostscript (jeśli jest zainstalowany)", "presets.pdf_ghostscript_hint": "Często daje mniejszy plik, ale zapisuje cały plik od nowa.\nZostaje najmniejszy wynik.", + "presets.ocr": "Rozpoznaj tekst (OCR)", + "presets.ocr_hint": "Tekst skanów i obrazów da się przeszukiwać i edytować. Wymaga Tesseract (Ustawienia → Silniki).\nStrony, które mają już tekst, pozostają bez zmian.", + "presets.ocr_language": "Język tekstu", + "presets.ocr_language_size": "Pobieranie {0}", "presets.pdf_keep_metadata": "Zachowaj metadane stron i obrazów (XMP)", "presets.combine_pages_hint": "Układa wszystkie strony jedna pod drugą w jednym obrazie.\nBardzo długie wyniki są skalowane do limitu formatu (JPG 65 500 px, WebP 16 383 px).", "presets.combine_pages": "Złóż strony w jeden wysoki obraz (przy konwersji PDF)", @@ -655,6 +663,8 @@ "engines.package.pandoc.description": "Dodaje Markdown i HTML ↔ ODT i RTF, DOCX → Markdown oraz LaTeX, Org, AsciiDoc, Typst, Jupyter i inne języki znaczników.\nTworzy też slajdy PPTX z Markdown.\nMarkdown i HTML → PDF, DOCX i HWPX działają bez niego.", "engines.package.ghostscript.name": "Ghostscript (EPS i PostScript)", "engines.package.ghostscript.description": "EPS, PS i starsze pliki Illustratora → PDF i obrazy, PDF lub SVG → EPS/PS.\nSVG i pliki AI zgodne z PDF konwertują się bez niego.", + "engines.package.tesseract.name": "Tesseract (rozpoznawanie tekstu, OCR)", + "engines.package.tesseract.description": "Odczytuje tekst zeskanowanych PDF i obrazów: przeszukiwalny PDF, DOCX, TXT.\nKażdy język jest pobierany przy pierwszym użyciu.", "engines.package.ffmpeg.name": "FFmpeg (wideo i audio)", "engines.package.ffmpeg.description": "Dodaje konwersję wideo i audio, wideo → GIF lub klatka, dźwięk z wideo i GIF → MP4.\nPotrzebny tylko do wideo i audio.", "engines.package.calibre.name": "Calibre (Kindle i rzadkie formaty e-booków)", @@ -664,10 +674,12 @@ "engines.guidance.macos": "Zainstaluj {0} dla tego Maca, potem wybierz „Użyj mojej kopii…” i wskaż jego program lub folder .app.", "engines.guidance.macos.ffmpeg": "Zainstaluj przez Homebrew (brew install ffmpeg), potem wybierz „Sprawdź ponownie”.\nAlbo wybierz „Użyj mojej kopii…” i wskaż folder bin.", "engines.guidance.macos.ghostscript": "Zainstaluj przez Homebrew (brew install ghostscript), potem wybierz „Sprawdź ponownie”.\nAlbo wybierz „Użyj mojej kopii…” i wskaż folder bin.", + "engines.guidance.macos.tesseract": "Zainstaluj przez Homebrew (brew install tesseract), potem wybierz „Sprawdź ponownie”.\nAlbo wybierz „Użyj mojej kopii…” i wskaż folder bin.", "engines.guidance.macos.calibre": "calibre pobierane przez Filee wymaga macOS 14 lub nowszego. Zainstaluj calibre 7.26, ostatnią wersję dla tego macOS, ze strony calibre-ebook.com, potem wybierz „Użyj mojej kopii…” i wskaż jego folder .app.", "engines.guidance.linux": "Zainstaluj {0} menedżerem pakietów swojej dystrybucji, potem wybierz „Użyj mojej kopii…” i wskaż folder programu.", "engines.guidance.linux.libreoffice": "Zainstaluj menedżerem pakietów (Ubuntu/Debian: sudo apt install libreoffice), potem wybierz „Sprawdź ponownie”.\nOtwieranie HWP wymaga też Javy i rozszerzenia H2Orestart.", "engines.guidance.linux.ghostscript": "Zainstaluj menedżerem pakietów (Ubuntu/Debian: sudo apt install ghostscript), potem wybierz „Sprawdź ponownie”.", + "engines.guidance.linux.tesseract": "Zainstaluj menedżerem pakietów (Ubuntu/Debian: sudo apt install tesseract-ocr), potem wybierz „Sprawdź ponownie”.", "engines.guidance.windows": "Filee nie ma do pobrania {0} dla tego komputera.\nWybierz „Użyj mojej kopii…” i wskaż zainstalowaną kopię.", "engines.use_own_app_folder": "Wybierz folder zawierający {0}.app", "engines.progress_detail": "{0} z {1} · {2}/s · {3}", diff --git a/src/Filee.App/Assets/i18n/pt-BR.json b/src/Filee.App/Assets/i18n/pt-BR.json index 0b7cd7cd0..7dd2720b2 100644 --- a/src/Filee.App/Assets/i18n/pt-BR.json +++ b/src/Filee.App/Assets/i18n/pt-BR.json @@ -81,6 +81,7 @@ "format_request.notice_text": "Diga qual formato você precisa.\nOs pedidos decidem o que o Filee passa a aceitar.\nArquivos criptografados ou protegidos contra cópia (DRM) não podem ser convertidos, seja qual for o formato.", "error.no_route": "Nenhum motor faz esta conversão", "error.cannot_turn": "Girar e espelhar só funcionam em imagens", + "error.ocr_language_download": "Não foi possível baixar o idioma do OCR", "error.too_many_frames": "Quadros demais para salvar como imagens. Aumente “A cada N quadros”", "error.not_enough_space": "Não há espaço livre suficiente em disco para as imagens", "error.no_motion_photo": "Esta foto não tem um vídeo que o Filee consiga ler (uma Motion Photo do Android, ou uma Live Photo do iPhone com o MOV ao lado)", @@ -105,6 +106,7 @@ "error.geo_unsupported_shape": "Este tipo de forma não é compatível", "error.geo_too_large": "O Shapefile passaria do limite de 2 GB do formato", "notice.pdf_not_smaller": "Não deu para deixar menor, então foi copiado como está", + "notice.ocr_has_text": "Todas as páginas já têm texto, então foi copiado como está", "notice.live_photo_pair": "Salvo como uma foto e um MOV com o mesmo nome. Importe os dois juntos (Fotos do iCloud, AirDrop ou Fotos no Mac) para obter uma Live Photo", "notice.geo_datum_assumed": "O datum não é WGS 84; as posições foram convertidas como se fosse, e podem errar em até centenas de metros", "notice.geo_gpx_lossy": "O GPX guarda só pontos, rotas e trilhas com poucos campos: outros atributos ficaram de fora e os polígonos viraram trilhas", @@ -145,6 +147,7 @@ "engine.ffmpeg.description": "Vídeo e áudio: MP4, MOV, MKV, WEBM, MP3, M4A, WAV, FLAC e mais.\nTambém extrai som e legendas, faz animações GIF e APNG e salva imagens como HEIC.", "engine.pandoc.description": "Markdown e HTML ↔ ODT e RTF, DOCX → Markdown\nreStructuredText, LaTeX, Org, AsciiDoc, Typst, MediaWiki, DocBook, Jupyter e mais ↔ Markdown, HTML, DOCX e ODT\nMarkdown e Org → slides PPTX", "engine.ghostscript.description": "EPS, PostScript e arquivos antigos do Illustrator para PDF e imagens; PDF para EPS e PS.", + "engine.tesseract.description": "Reconhece o texto de digitalizações e imagens (OCR): PDF pesquisável, DOCX, TXT.", "engine.calibre.description": "Formatos raros de e-book (LIT, LRF, CHM, PDB, RB…) e saída Kindle (MOBI, AZW3).\nAcima do gravador de HWPX, também gera os PDFs de e-books (bem mais rápido em livros longos).", "engine.libreoffice.description": "Lê formatos antigos e menos comuns: DOC, PPT, Pages, Keynote, Numbers, WordPerfect, ClarisWorks, Publisher, Visio…\nTambém salva como DOC, XLS, PPT, ODT, ODP, modelos OpenDocument e EMF.", @@ -210,6 +213,7 @@ "merge.order_hint": "Selecione um arquivo e mova para cima ou para baixo. Os arquivos são mesclados de cima para baixo. (Alt+↑ / Alt+↓)", "preset.split_pdf": "Dividir PDF", "preset.compress_pdf": "Comprimir PDF", + "preset.searchable_pdf": "PDF pesquisável", "preset.long_image": "Imagem longa", "preset.pdf_slides": "PPTX (slides)", "preset.extract": "Extrair", @@ -523,6 +527,10 @@ "presets.pdf_compression_hint": "As imagens são reduzidas e salvas como JPEG.\nTexto, fontes e desenhos permanecem como estão.\nSe o resultado não ficar menor, o original é mantido.", "presets.pdf_ghostscript": "Tentar também o Ghostscript (quando instalado)", "presets.pdf_ghostscript_hint": "Muitas vezes menor, mas reescreve o arquivo inteiro.\nO menor resultado é mantido.", + "presets.ocr": "Reconhecer texto (OCR)", + "presets.ocr_hint": "Digitalizações e imagens ganham texto que pode ser pesquisado e editado. Requer o Tesseract (Configurações → Motores).\nPáginas que já têm texto ficam como estão.", + "presets.ocr_language": "Idioma do texto", + "presets.ocr_language_size": "Download de {0}", "presets.pdf_keep_metadata": "Manter metadados de página e imagem (XMP)", "presets.combine_pages_hint": "Coloca todas as páginas uma embaixo da outra numa única imagem.\nResultados muito longos são reduzidos ao limite do formato (JPG 65.500 px, WebP 16.383 px).", "presets.combine_pages": "Empilhar as páginas numa imagem alta (quando um PDF é convertido)", @@ -655,6 +663,8 @@ "engines.package.pandoc.description": "Adiciona Markdown e HTML ↔ ODT e RTF, DOCX → Markdown, e LaTeX, Org, AsciiDoc, Typst, Jupyter e outras marcações.\nTambém faz slides PPTX a partir de Markdown.\nMarkdown e HTML → PDF, DOCX e HWPX funcionam sem ele.", "engines.package.ghostscript.name": "Ghostscript (EPS e PostScript)", "engines.package.ghostscript.description": "EPS, PS e arquivos antigos do Illustrator → PDF e imagens, PDF ou SVG → EPS/PS.\nSVG e arquivos AI compatíveis com PDF convertem sem ele.", + "engines.package.tesseract.name": "Tesseract (reconhecimento de texto, OCR)", + "engines.package.tesseract.description": "Lê o texto de PDFs digitalizados e imagens: PDF pesquisável, DOCX, TXT.\nCada idioma é baixado na primeira vez que é usado.", "engines.package.ffmpeg.name": "FFmpeg (vídeo e áudio)", "engines.package.ffmpeg.description": "Adiciona conversão de vídeo e áudio, vídeo → GIF ou imagem parada, som de vídeos e GIF → MP4.\nSó é necessário para vídeo e áudio.", "engines.package.calibre.name": "Calibre (Kindle e formatos raros de e-book)", @@ -664,10 +674,12 @@ "engines.guidance.macos": "Instale {0} para este Mac, depois escolha “Usar minha cópia…” e selecione o programa ou a pasta .app.", "engines.guidance.macos.ffmpeg": "Instale com o Homebrew (brew install ffmpeg) e escolha “Verificar de novo”.\nOu escolha “Usar minha cópia…” e selecione a pasta bin.", "engines.guidance.macos.ghostscript": "Instale com o Homebrew (brew install ghostscript) e escolha “Verificar de novo”.\nOu escolha “Usar minha cópia…” e selecione a pasta bin.", + "engines.guidance.macos.tesseract": "Instale com o Homebrew (brew install tesseract) e escolha “Verificar de novo”.\nOu escolha “Usar minha cópia…” e selecione a pasta bin.", "engines.guidance.macos.calibre": "O calibre que o Filee baixa precisa do macOS 14 ou posterior. Instale o calibre 7.26, a última versão para este macOS, em calibre-ebook.com, depois escolha “Usar minha cópia…” e selecione a pasta .app.", "engines.guidance.linux": "Instale {0} com o gerenciador de pacotes da sua distribuição, depois escolha “Usar minha cópia…” e selecione a pasta do programa.", "engines.guidance.linux.libreoffice": "Instale com o gerenciador de pacotes (Ubuntu/Debian: sudo apt install libreoffice) e escolha “Verificar de novo”.\nAbrir HWP também precisa de Java e da extensão H2Orestart.", "engines.guidance.linux.ghostscript": "Instale com o gerenciador de pacotes (Ubuntu/Debian: sudo apt install ghostscript) e escolha “Verificar de novo”.", + "engines.guidance.linux.tesseract": "Instale com o gerenciador de pacotes (Ubuntu/Debian: sudo apt install tesseract-ocr) e escolha “Verificar de novo”.", "engines.guidance.windows": "O Filee não tem download de {0} para este PC.\nEscolha “Usar minha cópia…” e selecione uma cópia instalada.", "engines.use_own_app_folder": "Escolha a pasta que contém {0}.app", "engines.progress_detail": "{0} de {1} · {2}/s · {3}", diff --git a/src/Filee.App/Assets/i18n/ru.json b/src/Filee.App/Assets/i18n/ru.json index 3eb526c74..598ffb77f 100644 --- a/src/Filee.App/Assets/i18n/ru.json +++ b/src/Filee.App/Assets/i18n/ru.json @@ -81,6 +81,7 @@ "format_request.notice_text": "Напишите, какой формат вам нужен.\nОт запросов зависит, что Filee будет поддерживать дальше.\nЗашифрованные и защищённые от копирования (DRM) файлы сконвертировать нельзя, в каком бы формате они ни были.", "error.no_route": "Нет движка для такой конвертации", "error.cannot_turn": "Поворот и отражение работают только с изображениями", + "error.ocr_language_download": "Не удалось скачать язык для OCR", "error.too_many_frames": "Слишком много кадров для сохранения картинками — увеличьте «Каждый N-й кадр»", "error.not_enough_space": "Не хватает места на диске для картинок", "error.no_motion_photo": "В этом фото нет видео, которое Filee может прочитать (Motion Photo с Android или Live Photo с iPhone с файлом MOV рядом)", @@ -105,6 +106,7 @@ "error.geo_unsupported_shape": "Такой тип фигуры не поддерживается", "error.geo_too_large": "Shapefile превысил бы предел формата в 2 ГБ", "notice.pdf_not_smaller": "Уменьшить не получилось, файл скопирован как есть", + "notice.ocr_has_text": "На всех страницах уже есть текст, поэтому файл скопирован как есть", "notice.live_photo_pair": "Сохранено как фото и MOV с одинаковым именем. Импортируйте их вместе (Фото iCloud, AirDrop или «Фото» на Mac), чтобы получить Live Photo", "notice.geo_datum_assumed": "Датум отличается от WGS 84; координаты пересчитаны как для WGS 84 и могут сместиться на сотни метров", "notice.geo_gpx_lossy": "GPX хранит только точки, маршруты и треки с небольшим набором полей: остальные атрибуты не вошли, а полигоны стали треками", @@ -145,6 +147,7 @@ "engine.ffmpeg.description": "Видео и аудио: MP4, MOV, MKV, WEBM, MP3, M4A, WAV, FLAC и другие.\nА ещё достаёт звук и субтитры, делает анимации GIF и APNG и сохраняет картинки в HEIC.", "engine.pandoc.description": "Markdown и HTML ↔ ODT и RTF, DOCX → Markdown\nreStructuredText, LaTeX, Org, AsciiDoc, Typst, MediaWiki, DocBook, Jupyter и другие ↔ Markdown, HTML, DOCX и ODT\nMarkdown и Org → слайды PPTX", "engine.ghostscript.description": "EPS, PostScript и старые файлы Illustrator — в PDF и картинки; PDF — в EPS и PS.", + "engine.tesseract.description": "Распознаёт текст сканов и изображений (OCR): PDF с поиском, DOCX, TXT.", "engine.calibre.description": "Редкие форматы электронных книг (LIT, LRF, CHM, PDB, RB…) и вывод для Kindle (MOBI, AZW3).\nЕсли стоит выше HWPX writer, он же делает PDF из электронных книг (для длинных книг намного быстрее).", "engine.libreoffice.description": "Читает старые и редкие форматы: DOC, PPT, Pages, Keynote, Numbers, WordPerfect, ClarisWorks, Publisher, Visio…\nА ещё сохраняет в DOC, XLS, PPT, ODT, ODP, шаблоны OpenDocument и EMF.", @@ -210,6 +213,7 @@ "merge.order_hint": "Выберите файл и переместите его вверх или вниз. Файлы объединяются сверху вниз. (Alt+↑ / Alt+↓)", "preset.split_pdf": "Разделить PDF", "preset.compress_pdf": "Сжать PDF", + "preset.searchable_pdf": "PDF с поиском", "preset.long_image": "Длинная картинка", "preset.pdf_slides": "PPTX (слайды)", "preset.extract": "Распаковать", @@ -523,6 +527,10 @@ "presets.pdf_compression_hint": "Картинки уменьшаются и сохраняются в JPEG.\nТекст, шрифты и рисунки остаются как есть.\nЕсли результат не меньше, остаётся оригинал.", "presets.pdf_ghostscript": "Попробовать также Ghostscript (если установлен)", "presets.pdf_ghostscript_hint": "Часто получается меньше, но он перезаписывает весь файл.\nОстаётся самый маленький результат.", + "presets.ocr": "Распознать текст (OCR)", + "presets.ocr_hint": "Текст сканов и изображений можно будет искать и редактировать. Нужен Tesseract (Настройки → Движки).\nСтраницы, где уже есть текст, не меняются.", + "presets.ocr_language": "Язык текста", + "presets.ocr_language_size": "Загрузка {0}", "presets.pdf_keep_metadata": "Сохранять метаданные страниц и картинок (XMP)", "presets.combine_pages_hint": "Ставит все страницы одну под другой в одной картинке.\nОчень длинный результат уменьшается до предела формата (JPG 65 500 px, WebP 16 383 px).", "presets.combine_pages": "Сложить страницы в одну длинную картинку (при конвертации PDF)", @@ -655,6 +663,8 @@ "engines.package.pandoc.description": "Добавляет Markdown и HTML ↔ ODT и RTF, DOCX → Markdown, а также LaTeX, Org, AsciiDoc, Typst, Jupyter и другую разметку.\nА ещё делает слайды PPTX из Markdown.\nMarkdown и HTML → PDF, DOCX и HWPX работают и без него.", "engines.package.ghostscript.name": "Ghostscript (EPS и PostScript)", "engines.package.ghostscript.description": "EPS, PS и старые файлы Illustrator → PDF и картинки, PDF или SVG → EPS/PS.\nSVG и файлы AI, совместимые с PDF, конвертируются и без него.", + "engines.package.tesseract.name": "Tesseract (распознавание текста, OCR)", + "engines.package.tesseract.description": "Читает текст отсканированных PDF и изображений: PDF с поиском, DOCX, TXT.\nКаждый язык скачивается при первом использовании.", "engines.package.ffmpeg.name": "FFmpeg (видео и аудио)", "engines.package.ffmpeg.description": "Добавляет конвертацию видео и аудио, видео → GIF или стоп-кадр, звук из видео и GIF → MP4.\nНужен только для видео и аудио.", "engines.package.calibre.name": "Calibre (Kindle и редкие форматы книг)", @@ -664,10 +674,12 @@ "engines.guidance.macos": "Установите {0} для этого Mac, затем выберите «Использовать свою копию…» и укажите программу или папку .app.", "engines.guidance.macos.ffmpeg": "Установите через Homebrew (brew install ffmpeg), затем выберите «Проверить снова».\nИли выберите «Использовать свою копию…» и укажите папку bin.", "engines.guidance.macos.ghostscript": "Установите через Homebrew (brew install ghostscript), затем выберите «Проверить снова».\nИли выберите «Использовать свою копию…» и укажите папку bin.", + "engines.guidance.macos.tesseract": "Установите через Homebrew (brew install tesseract), затем выберите «Проверить снова».\nИли выберите «Использовать свою копию…» и укажите папку bin.", "engines.guidance.macos.calibre": "Для calibre, которую скачивает Filee, нужна macOS 14 или новее. Установите calibre 7.26, последнюю версию для этой macOS, с calibre-ebook.com, затем выберите «Использовать свою копию…» и укажите её папку .app.", "engines.guidance.linux": "Установите {0} менеджером пакетов вашего дистрибутива, затем выберите «Использовать свою копию…» и укажите папку программы.", "engines.guidance.linux.libreoffice": "Установите менеджером пакетов (Ubuntu/Debian: sudo apt install libreoffice), затем выберите «Проверить снова».\nДля открытия HWP также нужны Java и расширение H2Orestart.", "engines.guidance.linux.ghostscript": "Установите менеджером пакетов (Ubuntu/Debian: sudo apt install ghostscript), затем выберите «Проверить снова».", + "engines.guidance.linux.tesseract": "Установите менеджером пакетов (Ubuntu/Debian: sudo apt install tesseract-ocr), затем выберите «Проверить снова».", "engines.guidance.windows": "Filee не может скачать {0} для этого компьютера.\nВыберите «Использовать свою копию…» и укажите установленную копию.", "engines.use_own_app_folder": "Выберите папку, в которой лежит {0}.app", "engines.progress_detail": "{0} из {1} · {2}/с · {3}", diff --git a/src/Filee.App/Assets/i18n/zh-CN.json b/src/Filee.App/Assets/i18n/zh-CN.json index 6247a2b0a..775978755 100644 --- a/src/Filee.App/Assets/i18n/zh-CN.json +++ b/src/Filee.App/Assets/i18n/zh-CN.json @@ -81,6 +81,7 @@ "format_request.notice_text": "告诉我们你需要的格式。\n我们会根据请求决定接下来支持哪些格式。\n加密或有复制保护 (DRM) 的文件,无论什么格式都无法转换。", "error.no_route": "没有引擎可以完成此转换", "error.cannot_turn": "只能旋转和翻转图片", + "error.ocr_language_download": "无法下载 OCR 语言数据", "error.too_many_frames": "帧数太多,请调大“每 N 帧”", "error.not_enough_space": "磁盘空间不足,无法保存这些图片", "error.no_motion_photo": "这张照片中没有可读取的视频(安卓动态照片,或旁边有 MOV 的 iPhone 实况照片)", @@ -105,6 +106,7 @@ "error.geo_unsupported_shape": "不支持此类形状", "error.geo_too_large": "将超过 Shapefile 2 GB 的上限", "notice.pdf_not_smaller": "无法再缩小,已原样复制", + "notice.ocr_has_text": "每页都已有文字,已原样复制", "notice.live_photo_pair": "已保存为同名的照片和 MOV。将两者一起导入(iCloud 照片、隔空投送或 Mac 上的“照片”)即可得到实况照片", "notice.geo_datum_assumed": "基准面不是 WGS 84,已按相同处理,位置可能偏差数百米", "notice.geo_gpx_lossy": "GPX 只能保存点、路线和轨迹及少数字段,其他属性已省略,面变成了轨迹", @@ -145,6 +147,7 @@ "engine.ffmpeg.description": "MP4、MOV、MKV、WEBM、MP3、M4A、WAV、FLAC 等视频和音频。\n还能提取声音和字幕、制作 GIF 和 APNG、把图片保存为 HEIC。", "engine.pandoc.description": "Markdown 和 HTML ↔ ODT、RTF,DOCX → Markdown\nreStructuredText、LaTeX、Org、AsciiDoc、Typst、MediaWiki、DocBook、Jupyter 等 ↔ Markdown、HTML、DOCX、ODT\nMarkdown 和 Org → PPTX 幻灯片", "engine.ghostscript.description": "EPS、PostScript 和旧版 Illustrator 文件转为 PDF 和图片;PDF 转为 EPS 和 PS。", + "engine.tesseract.description": "识别扫描件和图片中的文字(OCR):可搜索的 PDF、DOCX、TXT。", "engine.calibre.description": "少见的电子书格式(LIT、LRF、CHM、PDB、RB…)以及 Kindle 输出(MOBI、AZW3)。\n排在 HWPX writer 之上时,也由它把电子书转为 PDF(长书快得多)。", "engine.libreoffice.description": "读取较旧或少见的格式,如 DOC、PPT、Pages、Keynote、Numbers、WordPerfect、ClarisWorks、Publisher、Visio。\n也能保存为 DOC、XLS、PPT、ODT、ODP、OpenDocument 模板或 EMF。", @@ -210,6 +213,7 @@ "merge.order_hint": "选择文件并上下移动。文件将按从上到下的顺序合并。(Alt+↑ / Alt+↓)", "preset.split_pdf": "拆分 PDF", "preset.compress_pdf": "压缩 PDF", + "preset.searchable_pdf": "可搜索的 PDF", "preset.long_image": "长图", "preset.pdf_slides": "PPTX(幻灯片)", "preset.extract": "解压", @@ -523,6 +527,10 @@ "presets.pdf_compression_hint": "图片会缩小并存为 JPEG。\n文字、字体和图形保持不变。\n结果没有变小时保留原文件。", "presets.pdf_ghostscript": "也试试 Ghostscript(已安装时)", "presets.pdf_ghostscript_hint": "通常更小,但会重写整个文件。\n保留最小的结果。", + "presets.ocr": "识别文字(OCR)", + "presets.ocr_hint": "让扫描件和图片中的文字可以搜索和编辑。需要 Tesseract(设置 → 转换引擎)。\n已有文字的页面保持不变。", + "presets.ocr_language": "文字的语言", + "presets.ocr_language_size": "下载 {0}", "presets.pdf_keep_metadata": "保留页面和图片元数据 (XMP)", "presets.combine_pages_hint": "把所有页面上下相接,生成一张图片。\n过长时会缩小到该格式的最大高度(JPG 65,500 像素,WebP 16,383 像素)。", "presets.combine_pages": "把所有页面上下拼成一张长图(转换 PDF 时)", @@ -655,6 +663,8 @@ "engines.package.pandoc.description": "增加 Markdown 和 HTML ↔ ODT、RTF,DOCX → Markdown,以及 LaTeX、Org、AsciiDoc、Typst、Jupyter 等标记语言转换。\n也能用 Markdown 制作 PPTX 幻灯片。\nMarkdown 和 HTML → PDF、DOCX、HWPX 无需它。", "engines.package.ghostscript.name": "Ghostscript(EPS 与 PostScript)", "engines.package.ghostscript.description": "EPS、PS 和旧版 Illustrator 文件 → PDF 和图片,PDF 或 SVG → EPS/PS。\nSVG 和兼容 PDF 的 AI 文件无需它即可转换。", + "engines.package.tesseract.name": "Tesseract(文字识别,OCR)", + "engines.package.tesseract.description": "读取扫描 PDF 和图片中的文字:可搜索的 PDF、DOCX、TXT。\n每种语言在首次使用时下载。", "engines.package.ffmpeg.name": "FFmpeg(视频和音频)", "engines.package.ffmpeg.description": "增加视频和音频转换、视频 → GIF 或静态图片、提取声音以及 GIF → MP4。\n仅视频和音频转换需要它。", "engines.package.calibre.name": "Calibre(Kindle 与少见电子书格式)", @@ -664,10 +674,12 @@ "engines.guidance.macos": "先在这台 Mac 上安装 {0},再点“使用我的程序…”选择它的程序或 .app 文件夹。", "engines.guidance.macos.ffmpeg": "用 Homebrew 安装(brew install ffmpeg)后点“重新检查”。\n或点“使用我的程序…”选择它的 bin 文件夹。", "engines.guidance.macos.ghostscript": "用 Homebrew 安装(brew install ghostscript)后点“重新检查”。\n或点“使用我的程序…”选择它的 bin 文件夹。", + "engines.guidance.macos.tesseract": "用 Homebrew 安装(brew install tesseract)后点“重新检查”。\n或点“使用我的程序…”选择它的 bin 文件夹。", "engines.guidance.macos.calibre": "Filee 下载的 calibre 需要 macOS 14 或更高版本。请从 calibre-ebook.com 安装适用于此 macOS 的最后一个版本 calibre 7.26,再点“使用我的程序…”选择它的 .app 文件夹。", "engines.guidance.linux": "用发行版的软件包管理器安装 {0},再点“使用我的程序…”选择它的程序文件夹。", "engines.guidance.linux.libreoffice": "用软件包管理器安装(Ubuntu/Debian 下运行 sudo apt install libreoffice)后点“重新检查”。\n打开 HWP 还需要 Java 和 H2Orestart 扩展。", "engines.guidance.linux.ghostscript": "用软件包管理器安装(Ubuntu/Debian 下运行 sudo apt install ghostscript)后点“重新检查”。", + "engines.guidance.linux.tesseract": "用软件包管理器安装(Ubuntu/Debian 下运行 sudo apt install tesseract-ocr)后点“重新检查”。", "engines.guidance.windows": "没有适用于这台电脑的 {0} 下载。\n点“使用我的程序…”选择已安装的程序。", "engines.use_own_app_folder": "选择包含 {0}.app 的文件夹", "engines.progress_detail": "{0} / {1} · {2}/s · {3}", diff --git a/src/Filee.App/Assets/i18n/zh-TW.json b/src/Filee.App/Assets/i18n/zh-TW.json index d1a197a11..68b5d688b 100644 --- a/src/Filee.App/Assets/i18n/zh-TW.json +++ b/src/Filee.App/Assets/i18n/zh-TW.json @@ -81,6 +81,7 @@ "format_request.notice_text": "告訴我們你需要的格式。\n我們會根據請求決定接下來支援哪些格式。\n加密或有複製保護 (DRM) 的檔案,無論什麼格式都無法轉換。", "error.no_route": "沒有引擎可以完成此轉換", "error.cannot_turn": "只能旋轉和翻轉圖片", + "error.ocr_language_download": "無法下載 OCR 語言資料", "error.too_many_frames": "影格數太多,請調大「每 N 個影格」", "error.not_enough_space": "磁碟空間不足,無法儲存這些圖片", "error.no_motion_photo": "這張照片中沒有可讀取的影片(Android 動態相片,或旁邊有 MOV 的 iPhone 原況照片)", @@ -105,6 +106,7 @@ "error.geo_unsupported_shape": "不支援此類形狀", "error.geo_too_large": "將超過 Shapefile 2 GB 的上限", "notice.pdf_not_smaller": "無法再縮小,已原樣複製", + "notice.ocr_has_text": "每頁都已有文字,已原樣複製", "notice.live_photo_pair": "已儲存為同名的照片和 MOV。將兩者一起匯入(iCloud 照片、隔空投送或 Mac 上的「照片」)即可得到原況照片", "notice.geo_datum_assumed": "基準面不是 WGS 84,已按相同處理,位置可能偏差數百公尺", "notice.geo_gpx_lossy": "GPX 只能儲存點、路線和軌跡及少數字段,其他屬性已省略,面變成了軌跡", @@ -145,6 +147,7 @@ "engine.ffmpeg.description": "MP4、MOV、MKV、WEBM、MP3、M4A、WAV、FLAC 等影片和音訊。\n還能擷取聲音和字幕、製作 GIF 和 APNG、把圖片儲存為 HEIC。", "engine.pandoc.description": "Markdown 和 HTML ↔ ODT、RTF,DOCX → Markdown\nreStructuredText、LaTeX、Org、AsciiDoc、Typst、MediaWiki、DocBook、Jupyter 等 ↔ Markdown、HTML、DOCX、ODT\nMarkdown 和 Org → PPTX 投影片", "engine.ghostscript.description": "EPS、PostScript 和舊版 Illustrator 檔案轉為 PDF 和圖片;PDF 轉為 EPS 和 PS。", + "engine.tesseract.description": "辨識掃描檔和圖片中的文字(OCR):可搜尋的 PDF、DOCX、TXT。", "engine.calibre.description": "少見的電子書格式(LIT、LRF、CHM、PDB、RB…)以及 Kindle 輸出(MOBI、AZW3)。\n排在 HWPX writer 之上時,也由它把電子書轉為 PDF(長書快得多)。", "engine.libreoffice.description": "讀取較舊或少見的格式,如 DOC、PPT、Pages、Keynote、Numbers、WordPerfect、ClarisWorks、Publisher、Visio。\n也能儲存為 DOC、XLS、PPT、ODT、ODP、OpenDocument 範本或 EMF。", @@ -210,6 +213,7 @@ "merge.order_hint": "選擇檔案並上下移動。檔案將按從上到下的順序合併。(Alt+↑ / Alt+↓)", "preset.split_pdf": "拆分 PDF", "preset.compress_pdf": "壓縮 PDF", + "preset.searchable_pdf": "可搜尋的 PDF", "preset.long_image": "長圖", "preset.pdf_slides": "PPTX(投影片)", "preset.extract": "解壓縮", @@ -523,6 +527,10 @@ "presets.pdf_compression_hint": "圖片會縮小並存為 JPEG。\n文字、字型和圖形保持不變。\n結果沒有變小時保留原始檔案。", "presets.pdf_ghostscript": "也試試 Ghostscript(已安裝時)", "presets.pdf_ghostscript_hint": "通常更小,但會重寫整個檔案。\n保留最小的結果。", + "presets.ocr": "辨識文字(OCR)", + "presets.ocr_hint": "讓掃描檔和圖片中的文字可以搜尋和編輯。需要 Tesseract(設定 → 轉換引擎)。\n已有文字的頁面保持不變。", + "presets.ocr_language": "文字的語言", + "presets.ocr_language_size": "下載 {0}", "presets.pdf_keep_metadata": "保留頁面和圖片中繼資料 (XMP)", "presets.combine_pages_hint": "把所有頁面上下相接,產生一張圖片。\n過長時會縮小到該格式的最大高度(JPG 65,500 像素,WebP 16,383 像素)。", "presets.combine_pages": "把所有頁面上下拼成一張長圖(轉換 PDF 時)", @@ -655,6 +663,8 @@ "engines.package.pandoc.description": "增加 Markdown 和 HTML ↔ ODT、RTF,DOCX → Markdown,以及 LaTeX、Org、AsciiDoc、Typst、Jupyter 等標記語言轉換。\n也能用 Markdown 製作 PPTX 投影片。\nMarkdown 和 HTML → PDF、DOCX、HWPX 無需它。", "engines.package.ghostscript.name": "Ghostscript(EPS 與 PostScript)", "engines.package.ghostscript.description": "EPS、PS 和舊版 Illustrator 檔案 → PDF 和圖片,PDF 或 SVG → EPS/PS。\nSVG 和相容 PDF 的 AI 檔案無需它即可轉換。", + "engines.package.tesseract.name": "Tesseract(文字辨識,OCR)", + "engines.package.tesseract.description": "讀取掃描 PDF 和圖片中的文字:可搜尋的 PDF、DOCX、TXT。\n每種語言在第一次使用時下載。", "engines.package.ffmpeg.name": "FFmpeg(影片和音訊)", "engines.package.ffmpeg.description": "增加影片和音訊轉換、影片 → GIF 或靜態圖片、擷取聲音以及 GIF → MP4。\n僅影片和音訊轉換需要它。", "engines.package.calibre.name": "Calibre(Kindle 與少見電子書格式)", @@ -664,10 +674,12 @@ "engines.guidance.macos": "先在這台 Mac 上安裝 {0},再點「使用我的程式…」選擇它的程式或 .app 資料夾。", "engines.guidance.macos.ffmpeg": "用 Homebrew 安裝(brew install ffmpeg)後點「重新檢查」。\n或點「使用我的程式…」選擇它的 bin 資料夾。", "engines.guidance.macos.ghostscript": "用 Homebrew 安裝(brew install ghostscript)後點「重新檢查」。\n或點「使用我的程式…」選擇它的 bin 資料夾。", + "engines.guidance.macos.tesseract": "用 Homebrew 安裝(brew install tesseract)後點「重新檢查」。\n或點「使用我的程式…」選擇它的 bin 資料夾。", "engines.guidance.macos.calibre": "Filee 下載的 calibre 需要 macOS 14 或更高版本。請從 calibre-ebook.com 安裝適用於此 macOS 的最後一個版本 calibre 7.26,再點「使用我的程式…」選擇它的 .app 資料夾。", "engines.guidance.linux": "用發行版的套件管理員安裝 {0},再點「使用我的程式…」選擇它的程式資料夾。", "engines.guidance.linux.libreoffice": "用套件管理員安裝(Ubuntu/Debian 下執行 sudo apt install libreoffice)後點「重新檢查」。\n開啟 HWP 還需要 Java 和 H2Orestart 擴充功能。", "engines.guidance.linux.ghostscript": "用套件管理員安裝(Ubuntu/Debian 下執行 sudo apt install ghostscript)後點「重新檢查」。", + "engines.guidance.linux.tesseract": "用套件管理員安裝(Ubuntu/Debian 下執行 sudo apt install tesseract-ocr)後點「重新檢查」。", "engines.guidance.windows": "沒有適用於這台電腦的 {0} 下載。\n點「使用我的程式…」選擇已安裝的程式。", "engines.use_own_app_folder": "選擇包含 {0}.app 的資料夾", "engines.progress_detail": "{0} / {1} · {2}/s · {3}", diff --git a/src/Filee.App/ViewModels/PresetEditorViewModel.cs b/src/Filee.App/ViewModels/PresetEditorViewModel.cs index d76f638b3..bf6ce0f62 100644 --- a/src/Filee.App/ViewModels/PresetEditorViewModel.cs +++ b/src/Filee.App/ViewModels/PresetEditorViewModel.cs @@ -7,6 +7,7 @@ using Filee.Core.Formats; using Filee.Core.Localization; using Filee.Core.Presets; +using Filee.Engines.Ocr; namespace Filee.App.ViewModels; @@ -81,6 +82,11 @@ .. FormatRegistry.Known Flips = Enum.GetValues().Select(v => new Choice(v, loc[$"presets.flip.{v}"])).ToList(); PageSizes = Enum.GetValues().Select(v => new Choice(v, loc[$"presets.page_size.{v}"])).ToList(); CompressionLevels = Enum.GetValues().Select(v => new Choice(v, loc[$"presets.pdf_compression.{v}"])).ToList(); + // Each language under its own name; one not downloaded yet shows its size (it downloads with the first file). + OcrLanguageChoices = [.. OcrLanguages.All + .OrderBy(l => l.Code != OcrLanguages.Default).ThenBy(l => l.NativeName, StringComparer.CurrentCulture) + .Select(l => new Choice(l.Code, l.NativeName, + OcrLanguages.IsInstalled(l.Code) ? null : loc.Format("presets.ocr_language_size", $"{l.Size / 1_000_000.0:0.#} MB")))]; Locations = Enum.GetValues().Select(v => new Choice(v, loc[$"presets.location.{v}"])).ToList(); Conflicts = Enum.GetValues().Select(v => new Choice(v, loc[$"presets.conflict.{v}"])).ToList(); TiffCompressions = Enum.GetValues().Select(v => new Choice(v, v.ToString().ToUpperInvariant())).ToList(); @@ -105,6 +111,8 @@ .. FormatRegistry.Known _pageSize = PageSizes.First(p => p.Value == _edit.Pdf.PageSize); _compressionLevel = CompressionLevels.FirstOrDefault(c => c.Value == _edit.Pdf.Compression) ?? CompressionLevels[0]; _compressWithGhostscript = _edit.Pdf.CompressWithGhostscript; + _ocr = _edit.Pdf.Ocr; + _ocrLanguage = OcrLanguageChoices.FirstOrDefault(l => l.Value == _edit.Pdf.OcrLanguage) ?? OcrLanguageChoices[0]; _location = Locations.First(l => l.Value == _edit.Output.Location); _conflict = Conflicts.First(c => c.Value == _edit.Output.Conflict); _tiffCompression = TiffCompressions.First(t => t.Value == _edit.Image.TiffCompression); @@ -177,6 +185,9 @@ .. FormatRegistry.Known public IReadOnlyList> Flips { get; } public IReadOnlyList> PageSizes { get; } + /// Languages Tesseract reads (PdfOptions.OcrLanguage), English first. + public IReadOnlyList> OcrLanguageChoices { get; } + /// PDF → PDF: don't compress, or the level (pictures at 72, 150 or 300 dpi). public IReadOnlyList> CompressionLevels { get; } public IReadOnlyList> Locations { get; } @@ -200,7 +211,7 @@ .. FormatRegistry.Known [ObservableProperty] [NotifyPropertyChangedFor(nameof(ShowImage), nameof(ShowTurnForPdf), nameof(ShowQuality), nameof(ShowTiff), nameof(ShowMultiPageTiff), nameof(ShowWebp), nameof(ShowIco), nameof(ShowZipResult), nameof(ShowPdf), nameof(ShowPdfBuild), nameof(ShowDocument), nameof(ShowLeaveOutPictures), nameof(ShowPdfSplit), nameof(ShowPdfCombine), - nameof(ShowPdfCompression), nameof(ShowPdfCompressionOptions), + nameof(ShowPdfCompression), nameof(ShowPdfCompressionOptions), nameof(ShowOcr), nameof(ShowMedia), nameof(ShowMediaQuality), nameof(ShowMaxHeight), nameof(ShowAudioBitrate), nameof(ShowRemoveAudio), nameof(ShowAudioChannels), nameof(ShowWav), nameof(VideoCodecs), nameof(VideoCodecChoice), nameof(ShowVideoCodec), nameof(ShowHardwareEncoding), nameof(AudioCodecs), nameof(AudioCodecChoice), nameof(ShowAudioCodec), @@ -385,6 +396,10 @@ public bool ColorProfileChecked [ObservableProperty] private bool _compressWithGhostscript; + /// Recognize the text of scans and pictures (PdfOptions.Ocr), in . + [ObservableProperty] private bool _ocr; + [ObservableProperty] private Choice _ocrLanguage; + /// PDF → image: one tall image of all pages. [ObservableProperty] private bool _combinePages; [ObservableProperty] private bool _trimPageMargins; @@ -491,6 +506,9 @@ partial void OnAnimationSpeedChanged(Choice? oldValue, Choice ne public bool ShowPdfCompression => TargetId == "pdf"; public bool ShowPdfCompressionOptions => ShowPdfCompression && CompressionLevel.Value != PdfCompression.None; + /// Text from scans goes into the formats Filee writes from a PDF's text itself (PdfOptions.OcrTargets). + public bool ShowOcr => !IsTool && PdfOptions.OcrTargets.Contains(TargetId); + /// PDF → a single-image format: the pages can be stacked into one tall image (TIFF, GIF, APNG and MNG keep pages). public bool ShowPdfCombine => TargetCategory == FormatCategory.Image && TargetId is not ("tiff" or "gif" or "apng" or "mng" or "ico" or "cur" or "icns" or "svg" or "pdf"); @@ -595,6 +613,9 @@ public bool Apply() pdf.SplitTallImage = SplitTallImage; pdf.Compression = CompressionLevel.Value; pdf.CompressWithGhostscript = CompressWithGhostscript; + // Only where the option is shown, as with SaveEveryFrame below. + pdf.Ocr = ShowOcr && Ocr; + pdf.OcrLanguage = OcrLanguage.Value; _original.Document.PdfA = PdfA; // Only where the option is shown, as with SaveEveryFrame below. _original.Document.LeaveOutPictures = ShowLeaveOutPictures && LeaveOutPictures; diff --git a/src/Filee.App/Views/PresetEditorView.axaml b/src/Filee.App/Views/PresetEditorView.axaml index 8c81df875..69ccbe6d5 100644 --- a/src/Filee.App/Views/PresetEditorView.axaml +++ b/src/Filee.App/Views/PresetEditorView.axaml @@ -317,6 +317,27 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/src/Filee.Core/Presets/BuiltInData.cs b/src/Filee.Core/Presets/BuiltInData.cs index 4a3c96c03..2e8a6769e 100644 --- a/src/Filee.Core/Presets/BuiltInData.cs +++ b/src/Filee.Core/Presets/BuiltInData.cs @@ -71,6 +71,7 @@ .. ImageToolPresets(), Pdf = { SplitPages = true }, Output = { Location = OutputLocation.Subfolder, SubfolderName = "{name}" }, }, .. PdfCompressionPresets(), + .. OcrPresets(), .. PdfSlidesPresets(), .. FramePresets(), .. MotionPhotoPresets(), @@ -124,6 +125,19 @@ public static List PdfCompressionPresets() => }, ]; + /// + /// "Searchable PDF": scans and pictures with their text recognized by Tesseract (added in settings schema 33). On no + /// donut, like "Compress PDF". + /// + public static List OcrPresets() => + [ + new() + { + Id = "searchable-pdf", NameKey = "preset.searchable_pdf", TargetFormat = "pdf", + Pdf = { Ocr = true }, Output = { FileNamePattern = "{name}_searchable" }, + }, + ]; + /// /// PDF → PowerPoint with a picture of each page on its own slide (added in settings schema 18). JPEG at 90 like the /// JPG preset; the render DPI is the PDF default (150), which the converter never goes below. diff --git a/src/Filee.Core/Settings/AppSettings.cs b/src/Filee.Core/Settings/AppSettings.cs index 4f488dcba..3df95d287 100644 --- a/src/Filee.Core/Settings/AppSettings.cs +++ b/src/Filee.Core/Settings/AppSettings.cs @@ -112,7 +112,7 @@ public static bool IsFlipKeyAllowed(string key, IEnumerable gest public sealed class AppSettings { /// Current schema version. Increase when the format changes and add a migration. - public const int CurrentSchemaVersion = 32; + public const int CurrentSchemaVersion = 33; public int SchemaVersion { get; set; } = CurrentSchemaVersion; diff --git a/src/Filee.Core/Settings/UserDataStore.cs b/src/Filee.Core/Settings/UserDataStore.cs index 7aff0c842..1aef7e316 100644 --- a/src/Filee.Core/Settings/UserDataStore.cs +++ b/src/Filee.Core/Settings/UserDataStore.cs @@ -750,6 +750,13 @@ public static bool ApplyToLibrary(int from, List presets, List p.Id == "searchable-pdf")) + { + var at = presets.FindIndex(p => p.Id == "compress-pdf"); + presets.Insert(at < 0 ? presets.Count : at + 1, BuiltInData.OcrPresets().Single()); + changed = true; + } return changed; } diff --git a/tests/Filee.Core.Tests/OcrRouteTests.cs b/tests/Filee.Core.Tests/OcrRouteTests.cs index 44dc7abd2..3d5712c19 100644 --- a/tests/Filee.Core.Tests/OcrRouteTests.cs +++ b/tests/Filee.Core.Tests/OcrRouteTests.cs @@ -3,6 +3,7 @@ using Filee.Core.Conversion; using Filee.Core.Presets; +using Filee.Core.Settings; namespace Filee.Core.Tests; @@ -97,4 +98,19 @@ public async Task Pdfs_merged_into_one_are_recognized_first() Assert.Equal(JobState.Completed, job.State); Assert.Equal([("jpg", "pdf", false), ("pdf", "pdf", false)], tesseract.Steps.OrderBy(s => s.From)); } + + [Fact] + public void A_version_32_library_gets_searchable_pdf_after_compress_pdf() + { + var presets = BuiltInData.CreatePresets(); + presets.RemoveAll(p => p.Id == "searchable-pdf"); + var profiles = BuiltInData.CreateProfiles(); + + Assert.True(SettingsMigrations.ApplyToLibrary(32, presets, profiles)); + + var ids = presets.Select(p => p.Id).ToList(); + Assert.Equal(ids.IndexOf("compress-pdf") + 1, ids.IndexOf("searchable-pdf")); + Assert.True(presets.Single(p => p.Id == "searchable-pdf").Pdf.Ocr); + Assert.False(SettingsMigrations.ApplyToLibrary(33, presets, profiles)); + } } From ce22a8ee450700102c591668077fbbc6c1fcff09 Mon Sep 17 00:00:00 2001 From: cre8ive-marketing <168519979+cre8ive-marketing@users.noreply.github.com> Date: Thu, 22 Oct 2026 15:11:34 +0530 Subject: [PATCH 03/11] Order imports --- src/Filee.Engines/Ocr/TesseractConverter.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Filee.Engines/Ocr/TesseractConverter.cs b/src/Filee.Engines/Ocr/TesseractConverter.cs index 98581a914..a4433eed6 100644 --- a/src/Filee.Engines/Ocr/TesseractConverter.cs +++ b/src/Filee.Engines/Ocr/TesseractConverter.cs @@ -13,9 +13,9 @@ using Filee.Engines.Infrastructure; using Filee.Engines.Pdf; using ImageMagick; -using PDFtoImage; using PdfSharp.Pdf; using PdfSharp.Pdf.IO; +using PDFtoImage; using SkiaSharp; using UglyToad.PdfPig.Exceptions; From cca4e39e7caab829e3f1dfed2658497b74fdac84 Mon Sep 17 00:00:00 2001 From: cre8ive-marketing <168519979+cre8ive-marketing@users.noreply.github.com> Date: Thu, 22 Oct 2026 15:24:45 +0530 Subject: [PATCH 04/11] Test the OCR options in the preset editor --- tests/Filee.App.Tests/RenderTests.cs | 36 ++++++++++++++++++++++++++++ 1 file changed, 36 insertions(+) diff --git a/tests/Filee.App.Tests/RenderTests.cs b/tests/Filee.App.Tests/RenderTests.cs index a5f09e05f..07a337157 100644 --- a/tests/Filee.App.Tests/RenderTests.cs +++ b/tests/Filee.App.Tests/RenderTests.cs @@ -1116,6 +1116,42 @@ public void Preset_editor_offers_the_subfolder_only_for_several_files_with_a_sub Assert.False(preset.Output.SubfolderOnlyForSeveralFiles); } + [AvaloniaTheory] + [InlineData("en")] + [InlineData("ko")] + public void Preset_editor_shows_and_saves_text_recognition(string language) + { + TestServices.EnsureInitialized(language); + var loc = AppHost.Get(); + var preset = Filee.Core.Presets.BuiltInData.CreatePresets().Single(p => p.Id == "searchable-pdf"); + var vm = new PresetEditorViewModel(preset, loc); + Assert.True(vm.ShowOcr); + Assert.True(vm.Ocr); + Assert.Equal("eng", vm.OcrLanguage.Value); + Assert.Equal("eng", vm.OcrLanguageChoices[0].Value); // English first, the others by their own names + + var window = new PresetEditorWindow { DataContext = vm, Height = 1400 }; + window.Show(); + Pump(); + Save(window, $"preset-ocr-{language}.png"); + window.Close(); + + // Scanned letters into Word, in German. + vm.Target = vm.Targets.Single(t => t.Value == "docx"); + vm.OcrLanguage = vm.OcrLanguageChoices.Single(l => l.Value == "deu"); + Assert.Equal("Deutsch", vm.OcrLanguage.Label); + Assert.True(vm.Apply()); + Assert.True(preset.Pdf.Ocr); + Assert.Equal("deu", preset.Pdf.OcrLanguage); + + // Not for pictures: the box is hidden there, and not kept on behind it. + vm = new PresetEditorViewModel(preset, loc); + vm.Target = vm.Targets.Single(t => t.Value == "png"); + Assert.False(vm.ShowOcr); + Assert.True(vm.Apply()); + Assert.False(preset.Pdf.Ocr); + } + [AvaloniaFact] public void Preset_editor_keeps_rotation_visible_for_pdf_and_drops_it_for_targets_that_cannot_turn() { From 53f7384740aec7d027af8b78260b0bf39ac5f736 Mon Sep 17 00:00:00 2001 From: cre8ive-marketing <168519979+cre8ive-marketing@users.noreply.github.com> Date: Thu, 22 Oct 2026 17:12:08 +0530 Subject: [PATCH 05/11] OCR page by page, at most four Tesseract runs (#429 review) A page is rendered only when one of at most four runs (fewer on PCs with fewer cores) is free, and its picture is deleted once its PDF exists, so a long scan never has more than four 300 dpi pictures in the work folder. --- docs/ENGINES.md | 4 +- src/Filee.Engines/Ocr/TesseractConverter.cs | 61 ++++++++++++++++----- tests/Filee.Engines.Tests/OcrTests.cs | 18 ++++++ 3 files changed, 67 insertions(+), 16 deletions(-) diff --git a/docs/ENGINES.md b/docs/ENGINES.md index 45e9f6f2f..9f8a2a9c0 100644 --- a/docs/ENGINES.md +++ b/docs/ENGINES.md @@ -1469,7 +1469,9 @@ scans and pictures with Tesseract (`Ocr/TesseractConverter.cs`), in the language becomes a PNG first. OCR edges are never part of an ordinary route. - A searchable PDF keeps the page as it looks and puts the recognized words on it as invisible text. PDF pages that already have text (visible or an OCR layer) are copied as they are; only the others are rendered at 300 dpi - (PDFium) and recognized, several pages at a time with one Tesseract thread each (`OMP_THREAD_LIMIT=1`). A PDF whose + (PDFium) and recognized page by page: a page is rendered when one of at most four Tesseract runs (fewer on PCs with + fewer cores) is free, each with one thread (`OMP_THREAD_LIMIT=1`), and its picture is deleted once its PDF exists, + so a long scan never has more than four pictures in the work folder. A PDF whose pages all have text is copied with a note. The new document keeps the pages and the document information, not bookmarks or form fields. - DOCX, Markdown and HWPX read the OCR layer as the page's text (`PdfDocumentReader.ReadWithOcrLayer`): the words diff --git a/src/Filee.Engines/Ocr/TesseractConverter.cs b/src/Filee.Engines/Ocr/TesseractConverter.cs index a4433eed6..242fee1df 100644 --- a/src/Filee.Engines/Ocr/TesseractConverter.cs +++ b/src/Filee.Engines/Ocr/TesseractConverter.cs @@ -5,7 +5,7 @@ // (PdfDocumentReader reads the layer when the preset asks for OCR). // // A PDF keeps its pages that already have text (born-digital pages are never turned into pictures); only pages -// without any are rendered at 300 dpi and recognized, several at a time. The language data comes from OcrLanguages, +// without any are rendered at 300 dpi and recognized, page by page, at most four at a time. The language data comes from OcrLanguages, // downloaded the first time a language is used, and is copied into the job's folder so Tesseract reads only it. using Filee.Core.Conversion; @@ -32,6 +32,12 @@ public sealed class TesseractConverter : IConverter /// Scans are rendered at this resolution, which Tesseract reads best (and the page keeps its size). internal const int Dpi = 300; + /// + /// Tesseract runs side by side at most: enough to keep a desktop busy, few enough for memory and disk on a long scan + /// (each run holds one 300 dpi page). A PC with fewer cores runs fewer. + /// + internal static int Runs => Math.Clamp(System.Environment.ProcessorCount, 1, 4); + /// Pictures Tesseract opens itself (Leptonica); others become a PNG first (RoutePlanner). internal static readonly IReadOnlyList ImageSources = ["png", "jpg", "tiff", "bmp", "webp", "gif"]; @@ -123,23 +129,48 @@ private static async Task RecognizePdfAsync(Run ocr, ConversionStep step, string return; } - // PDFium draws one page at a time; Tesseract then reads them side by side. + // Page by page: PDFium draws a page only when one of the Tesseract runs is free, and a page's picture is deleted + // as soon as its PDF exists, so a 300-page scan never has more than Runs pictures in the work folder. var options = new RenderOptions(Dpi: Dpi, WithAnnotations: true, WithFormFill: true, BackgroundColor: SKColors.White); - for (var i = 0; i < scans.Count; i++) - { - cancellationToken.ThrowIfCancellationRequested(); - using var bitmap = Conversion.ToImage(bytes, scans[i], null, options); - await using var file = File.Create(Path.Combine(scratch, $"p{scans[i]}.png")); - bitmap.Encode(file, SKEncodedImageFormat.Png, 100); - progress?.Report(0.05 + 0.25 * (i + 1) / scans.Count); - } + using var free = new SemaphoreSlim(Runs); + var runs = new List(); var done = 0; - await Parallel.ForEachAsync(scans, new ParallelOptions { MaxDegreeOfParallelism = System.Environment.ProcessorCount, CancellationToken = cancellationToken }, - async (page, ct) => + foreach (var page in scans) + { + await free.WaitAsync(cancellationToken); + if (runs.Any(r => r.IsFaulted)) + { + free.Release(); + break; // a page failed: stop rendering, the error comes out below + } + var picture = Path.Combine(scratch, $"p{page}.png"); + try { - await ocr.ToPdfAsync(Path.Combine(scratch, $"p{page}.png"), Path.Combine(scratch, $"p{page}"), Dpi, ct); - progress?.Report(0.3 + 0.65 * Interlocked.Increment(ref done) / scans.Count); - }); + using var bitmap = Conversion.ToImage(bytes, page, null, options); + await using var file = File.Create(picture); + bitmap.Encode(file, SKEncodedImageFormat.Png, 100); + } + catch + { + free.Release(); + await Task.WhenAll(runs).ContinueWith(_ => { }, TaskScheduler.Default); // let the running pages end first + throw; + } + runs.Add(Task.Run(async () => + { + try + { + await ocr.ToPdfAsync(picture, Path.Combine(scratch, $"p{page}"), Dpi, cancellationToken); + File.Delete(picture); + progress?.Report(0.05 + 0.9 * Interlocked.Increment(ref done) / scans.Count); + } + finally + { + free.Release(); + } + }, cancellationToken)); + } + await Task.WhenAll(runs); // A new document: the pages and the document information come along, bookmarks and form fields don't. using var source = PdfSharpConverter.Open(step.InputPath, PdfDocumentOpenMode.Import); diff --git a/tests/Filee.Engines.Tests/OcrTests.cs b/tests/Filee.Engines.Tests/OcrTests.cs index 7003773da..66aeadb52 100644 --- a/tests/Filee.Engines.Tests/OcrTests.cs +++ b/tests/Filee.Engines.Tests/OcrTests.cs @@ -136,6 +136,24 @@ public async Task Pages_with_text_stay_as_they_are_and_only_scans_are_recognized Assert.Contains("picture", PageText(output, 2)); } + [Fact] + public async Task A_long_scan_is_recognized_page_by_page_in_order() + { + Assert.SkipUnless(Installed, Skip); + Assert.InRange(TesseractConverter.Runs, 1, 4); + var folder = fx.NewFolder(); + var words = new[] { "alpha", "bravo", "charlie", "delta", "echo", "foxtrot", "golf" }; + var scans = words.Select((w, i) => Scan(folder, $"p{i}.png", $"Page {w}")).ToList(); + var pdf = (await fx.ConvertAsync(scans, new Preset { TargetFormat = "pdf", Pdf = { MergeIntoSingle = true } })).Outputs.Single(); + + var job = await fx.ConvertAsync([pdf], Ocr("pdf")); + + AssertDone(job); + var output = job.Outputs.Single(); + for (var i = 0; i < words.Length; i++) + Assert.Contains(words[i], PageText(output, i + 1)); + } + [Fact] public async Task A_pdf_that_has_text_everywhere_is_copied_with_a_note() { From a552a1422efed6b4fd69a6bee04cd1a1fde23d7e Mon Sep 17 00:00:00 2001 From: cre8ive-marketing <168519979+cre8ive-marketing@users.noreply.github.com> Date: Fri, 23 Oct 2026 10:32:58 +0530 Subject: [PATCH 06/11] Stress test: compact at least once, also on a busy runner On the macOS arm64 CI runner the compactor task could start only after the workers had finished, so compactions stayed 0 and the test failed although nothing crashed. The loop now runs at least once. --- tests/Filee.Engines.Tests/NativeMemoryStressTests.cs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/tests/Filee.Engines.Tests/NativeMemoryStressTests.cs b/tests/Filee.Engines.Tests/NativeMemoryStressTests.cs index 092e59126..fc910af2d 100644 --- a/tests/Filee.Engines.Tests/NativeMemoryStressTests.cs +++ b/tests/Filee.Engines.Tests/NativeMemoryStressTests.cs @@ -47,13 +47,15 @@ public async Task Native_conversions_survive_a_compacting_collector_and_cache_re var compactions = 0; var compactor = Task.Run(() => { - while (!Volatile.Read(ref done)) + // At least once: on a busy CI runner this task can start only after the workers have finished. + do { GCSettings.LargeObjectHeapCompactionMode = GCLargeObjectHeapCompactionMode.CompactOnce; GC.Collect(GC.MaxGeneration, GCCollectionMode.Forced, blocking: true, compacting: true); compactions++; Thread.Sleep(20); } + while (!Volatile.Read(ref done)); }, TestContext.Current.CancellationToken); var releaser = Task.Run(() => { From e77c45706c5ce50e118bdd75fe9568281d109cf0 Mon Sep 17 00:00:00 2001 From: cre8ive-marketing <168519979+cre8ive-marketing@users.noreply.github.com> Date: Fri, 23 Oct 2026 13:15:06 +0530 Subject: [PATCH 07/11] OCR: a scan with a stamp is still recognized Review point 2 on #429. A scanned page whose only text is a little visible text the scanner software added (a page number, a Bates number) counted as a page with text and was skipped. Now a page counts as a scan when a picture covers at least half of it and its visible text covers under 1% of it. An OCR layer (invisible text) always counts as text, so a page recognized before is never recognized again, and a page drawn with little text and no picture under it (a cover, a short note) is left alone. --- src/Filee.Engines/Ocr/TesseractConverter.cs | 26 +++++++++- tests/Filee.Engines.Tests/OcrTests.cs | 57 +++++++++++++++++++++ 2 files changed, 81 insertions(+), 2 deletions(-) diff --git a/src/Filee.Engines/Ocr/TesseractConverter.cs b/src/Filee.Engines/Ocr/TesseractConverter.cs index 242fee1df..a5c8f9f58 100644 --- a/src/Filee.Engines/Ocr/TesseractConverter.cs +++ b/src/Filee.Engines/Ocr/TesseractConverter.cs @@ -17,6 +17,7 @@ using PdfSharp.Pdf.IO; using PDFtoImage; using SkiaSharp; +using UglyToad.PdfPig.Core; using UglyToad.PdfPig.Exceptions; // PDFtoImage marks its API for the platforms PDFium ships for; Filee only targets desktop platforms, all covered. @@ -201,13 +202,17 @@ private static async Task RecognizePdfAsync(Run ocr, ConversionStep step, string } } - /// For each page: true when it has any text, visible or not (an OCR layer counts). + /// + /// For each page: true when it has text of its own. An OCR layer (invisible text) always counts. A scan with a + /// stamp does not: a picture over most of the page whose only text is a little visible text (a page number or a + /// Bates number the scanner software added, under of the page). + /// internal static List PagesWithText(string path) { try { using var pdf = UglyToad.PdfPig.PdfDocument.Open(path, new UglyToad.PdfPig.ParsingOptions { UseLenientParsing = true, SkipMissingFonts = true }); - return [.. pdf.GetPages().Select(page => page.Letters.Any(l => !string.IsNullOrWhiteSpace(l.Value)))]; + return [.. pdf.GetPages().Select(HasText)]; } catch (PdfDocumentEncryptedException) { @@ -215,6 +220,23 @@ internal static List PagesWithText(string path) } } + /// The share of a page that visible text may cover on a scan and still be only a stamp. + internal const double StampCoverage = 0.01; + + private static bool HasText(UglyToad.PdfPig.Content.Page page) + { + var letters = page.Letters.Where(l => !string.IsNullOrWhiteSpace(l.Value)).ToList(); + if (letters.Count == 0) + return false; + if (letters.Any(l => l.RenderingMode is TextRenderingMode.Neither or TextRenderingMode.NeitherClip)) + return true; + var area = page.Width * page.Height; + if (area <= 0 || letters.Sum(l => l.GlyphRectangle.Area) >= StampCoverage * area) + return true; + // A page drawn with little text and no picture under it (a cover, a short note) is not a scan. + return !page.GetImages().Any(image => image.Bounds.Area >= area / 2); + } + /// /// True when the picture says how large it is meant to be. Without that Tesseract guesses 70 dpi (a page of a /// metre and more); 300 dpi is what scanners write. diff --git a/tests/Filee.Engines.Tests/OcrTests.cs b/tests/Filee.Engines.Tests/OcrTests.cs index 66aeadb52..b8582e339 100644 --- a/tests/Filee.Engines.Tests/OcrTests.cs +++ b/tests/Filee.Engines.Tests/OcrTests.cs @@ -3,6 +3,7 @@ // language data they read the first time (English, German). using System.IO.Compression; +using System.Text; using Filee.Core.Conversion; using Filee.Core.Presets; using Filee.Engines.Infrastructure; @@ -43,6 +44,62 @@ public void Tesseract_is_offered_but_not_suggested() } } +/// Which pages count as having text, so OCR leaves them alone (no Tesseract needed). +public class OcrPagesWithTextTests(EngineFixture fx) : IClassFixture +{ + /// + /// A one-page A4 PDF written by hand: optionally a picture over the whole page (a scan), then lines of Helvetica + /// text, visible or invisible (render mode 3, as an OCR layer is written). + /// + private string Pdf(bool scan, int lines, string text = "ABC-000123", bool invisible = false) + { + var content = new StringBuilder(); + if (scan) + content.Append("q 595 0 0 842 0 0 cm /Im1 Do Q\n"); + for (var i = 0; i < lines; i++) + content.Append($"BT /F1 10 Tf {(invisible ? "3" : "0")} Tr 40 {800 - i * 12} Td ({text}) Tj ET\n"); + string[] objects = + [ + "<< /Type /Catalog /Pages 2 0 R >>", + "<< /Type /Pages /Kids [3 0 R] /Count 1 >>", + "<< /Type /Page /Parent 2 0 R /MediaBox [0 0 595 842] /Contents 4 0 R /Resources << /Font << /F1 5 0 R >> /XObject << /Im1 6 0 R >> >> >>", + $"<< /Length {content.Length} >>\nstream\n{content}endstream", + "<< /Type /Font /Subtype /Type1 /BaseFont /Helvetica >>", + "<< /Type /XObject /Subtype /Image /Width 1 /Height 1 /ColorSpace /DeviceGray /BitsPerComponent 8 /Length 1 >>\nstream\n\u00c8\nendstream", + ]; + var pdf = new StringBuilder("%PDF-1.4\n"); + var offsets = new List(); + for (var i = 0; i < objects.Length; i++) + { + offsets.Add(pdf.Length); + pdf.Append($"{i + 1} 0 obj\n{objects[i]}\nendobj\n"); + } + var xref = pdf.Length; + pdf.Append($"xref\n0 {objects.Length + 1}\n0000000000 65535 f \n"); + foreach (var offset in offsets) + pdf.Append($"{offset:D10} 00000 n \n"); + pdf.Append($"trailer\n<< /Size {objects.Length + 1} /Root 1 0 R >>\nstartxref\n{xref}\n%%EOF\n"); + var path = Path.Combine(fx.NewFolder(), "page.pdf"); + File.WriteAllBytes(path, Encoding.Latin1.GetBytes(pdf.ToString())); + return path; + } + + [Fact] + public void A_scan_with_only_a_stamp_is_recognized() + { + Assert.Equal([false], TesseractConverter.PagesWithText(Pdf(scan: true, lines: 1))); + Assert.Equal([false], TesseractConverter.PagesWithText(Pdf(scan: true, lines: 0))); + } + + [Fact] + public void An_ocr_layer_a_page_of_text_and_a_drawn_note_are_left_alone() + { + Assert.Equal([true], TesseractConverter.PagesWithText(Pdf(scan: true, lines: 1, invisible: true))); + Assert.Equal([true], TesseractConverter.PagesWithText(Pdf(scan: true, lines: 40, text: "The quick brown fox jumps over the lazy dog again and again"))); + Assert.Equal([true], TesseractConverter.PagesWithText(Pdf(scan: false, lines: 1))); + } +} + public class OcrTests(EngineFixture fx) : IClassFixture { private static readonly bool Installed = TesseractConverter.Locate() is not null; From c7e40712c7c78de2250b3b8d1ce0cc7fee39cea4 Mon Sep 17 00:00:00 2001 From: cre8ive-marketing <168519979+cre8ive-marketing@users.noreply.github.com> Date: Fri, 23 Oct 2026 13:16:06 +0530 Subject: [PATCH 08/11] ENGINES.md: when a scan with a stamp is still recognized --- docs/ENGINES.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/docs/ENGINES.md b/docs/ENGINES.md index da4e9487b..efe3d375b 100644 --- a/docs/ENGINES.md +++ b/docs/ENGINES.md @@ -1537,7 +1537,9 @@ scans and pictures with Tesseract (`Ocr/TesseractConverter.cs`), in the language searchable PDF, then the cheapest ordinary route from that PDF. A picture Tesseract can't open (HEIC, PSD, …) becomes a PNG first. OCR edges are never part of an ordinary route. - A searchable PDF keeps the page as it looks and puts the recognized words on it as invisible text. PDF pages that - already have text (visible or an OCR layer) are copied as they are; only the others are rendered at 300 dpi + already have text (visible or an OCR layer) are copied as they are. A scan with a stamp still counts as a scan: a + picture over at least half the page whose only text is visible and covers under 1% of it (a page number or Bates + number the scanner software added). Only the others are rendered at 300 dpi (PDFium) and recognized page by page: a page is rendered when one of at most four Tesseract runs (fewer on PCs with fewer cores) is free, each with one thread (`OMP_THREAD_LIMIT=1`), and its picture is deleted once its PDF exists, so a long scan never has more than four pictures in the work folder. A PDF whose From 327224b4e5943053bbcc583c0d548ad8a1166ada Mon Sep 17 00:00:00 2001 From: cre8ive-marketing <168519979+cre8ive-marketing@users.noreply.github.com> Date: Fri, 23 Oct 2026 15:06:21 +0530 Subject: [PATCH 09/11] OCR: other PDF options, non-ASCII paths, cancel (review of #429) 1. With "Recognize text" the editor hides one file per page, compression (with removing metadata), page size and margin, and clears them in Apply: Tesseract makes the PDF, so none of them would happen. A scanned PDF with "Remove metadata" no longer gets the source's title, author, subject and keywords. 2. Tesseract runs in the job's scratch folder and sees only plain relative names there (the picture is copied in as picture., the language in tessdata), so a user name or folder in Korean or Cyrillic can't trip a build that opens files through the ANSI code page. - A cancelled or failed PDF waits for the running pages before the work folder can be removed. - The OcrPresets() comment names schema 40. --- .../ViewModels/PresetEditorViewModel.cs | 30 +++++++++-- src/Filee.App/Views/PresetEditorView.axaml | 2 +- src/Filee.Core/Presets/BuiltInData.cs | 2 +- src/Filee.Engines/Ocr/TesseractConverter.cs | 51 ++++++++++++------- tests/Filee.App.Tests/RenderTests.cs | 30 +++++++++++ tests/Filee.Engines.Tests/OcrTests.cs | 45 ++++++++++++++++ 6 files changed, 136 insertions(+), 24 deletions(-) diff --git a/src/Filee.App/ViewModels/PresetEditorViewModel.cs b/src/Filee.App/ViewModels/PresetEditorViewModel.cs index 4cdddb971..1fee4e9bf 100644 --- a/src/Filee.App/ViewModels/PresetEditorViewModel.cs +++ b/src/Filee.App/ViewModels/PresetEditorViewModel.cs @@ -214,7 +214,7 @@ .. FormatRegistry.Known [ObservableProperty] [NotifyPropertyChangedFor(nameof(ShowImage), nameof(ShowTurnForPdf), nameof(ShowQuality), nameof(ShowTiff), nameof(ShowMultiPageTiff), nameof(ShowWebp), nameof(ShowIco), nameof(ShowZipResult), nameof(ShowPdf), nameof(ShowPdfBuild), nameof(ShowDocument), nameof(ShowLeaveOutPictures), nameof(ShowPdfSplit), nameof(ShowPdfCombine), - nameof(ShowPdfCompression), nameof(ShowPdfCompressionOptions), nameof(ShowOcr), + nameof(ShowPdfCompression), nameof(ShowPdfCompressionOptions), nameof(ShowOcr), nameof(ShowPageSize), nameof(ShowMedia), nameof(ShowMediaQuality), nameof(ShowMaxHeight), nameof(ShowAudioBitrate), nameof(ShowRemoveAudio), nameof(ShowAudioChannels), nameof(ShowWav), nameof(VideoCodecs), nameof(VideoCodecChoice), nameof(ShowVideoCodec), nameof(ShowHardwareEncoding), nameof(AudioCodecs), nameof(AudioCodecChoice), nameof(ShowAudioCodec), @@ -400,7 +400,15 @@ public bool ColorProfileChecked [ObservableProperty] private bool _compressWithGhostscript; /// Recognize the text of scans and pictures (PdfOptions.Ocr), in . - [ObservableProperty] private bool _ocr; + [ObservableProperty] + [NotifyPropertyChangedFor(nameof(ShowPdfSplit), nameof(ShowPdfCompression), nameof(ShowPdfCompressionOptions), nameof(ShowPageSize))] + private bool _ocr; + + /// + /// Text recognition makes the PDF itself (Tesseract): one file with each page the size of its scan or picture, so one + /// file per page, compression (and with it removing metadata), page size and margin don't apply and are hidden. + /// + private bool Recognizes => ShowOcr && Ocr; [ObservableProperty] private Choice _ocrLanguage; /// PDF → image: one tall image of all pages. @@ -508,10 +516,13 @@ partial void OnAnimationSpeedChanged(Choice? oldValue, Choice ne /// "Save as ZIP" is offered for geospatial targets, whose results are several files (a Shapefile) or travel zipped. public bool ShowZipResult => TargetCategory == FormatCategory.Geo; public bool ShowPdfBuild => TargetId == "pdf"; - public bool ShowPdfSplit => TargetId == "pdf"; + + /// Page size and margin of a PDF built from pictures; not with text recognition (). + public bool ShowPageSize => ShowPdfBuild && !Recognizes; + public bool ShowPdfSplit => TargetId == "pdf" && !Recognizes; /// Compression is for PDF → PDF; Ghostscript and metadata only matter while it is on. - public bool ShowPdfCompression => TargetId == "pdf"; + public bool ShowPdfCompression => TargetId == "pdf" && !Recognizes; public bool ShowPdfCompressionOptions => ShowPdfCompression && CompressionLevel.Value != PdfCompression.None; /// Text from scans goes into the formats Filee writes from a PDF's text itself (PdfOptions.OcrTargets). @@ -624,8 +635,17 @@ public bool Apply() pdf.Compression = CompressionLevel.Value; pdf.CompressWithGhostscript = CompressWithGhostscript; // Only where the option is shown, as with SaveEveryFrame below. - pdf.Ocr = ShowOcr && Ocr; + pdf.Ocr = Recognizes; pdf.OcrLanguage = OcrLanguage.Value; + if (pdf.Ocr) + { + // Hidden while text is recognized, so nothing is kept behind it that the PDF wouldn't do (Recognizes). + pdf.SplitPages = false; + pdf.Compression = PdfCompression.None; + pdf.CompressWithGhostscript = false; + pdf.PageSize = PdfPageSize.FitImage; + pdf.MarginMm = 0; + } _original.Document.PdfA = PdfA; // Only where the option is shown, as with SaveEveryFrame below. _original.Document.LeaveOutPictures = ShowLeaveOutPictures && LeaveOutPictures; diff --git a/src/Filee.App/Views/PresetEditorView.axaml b/src/Filee.App/Views/PresetEditorView.axaml index 8e105efdc..81a1db4db 100644 --- a/src/Filee.App/Views/PresetEditorView.axaml +++ b/src/Filee.App/Views/PresetEditorView.axaml @@ -355,7 +355,7 @@ - + diff --git a/src/Filee.Core/Presets/BuiltInData.cs b/src/Filee.Core/Presets/BuiltInData.cs index af5cd208b..e6fe44ac7 100644 --- a/src/Filee.Core/Presets/BuiltInData.cs +++ b/src/Filee.Core/Presets/BuiltInData.cs @@ -197,7 +197,7 @@ public static List PdfCompressionPresets() => ]; /// - /// "Searchable PDF": scans and pictures with their text recognized by Tesseract (added in settings schema 33). On no + /// "Searchable PDF": scans and pictures with their text recognized by Tesseract (added in settings schema 40). On no /// donut, like "Compress PDF". /// public static List OcrPresets() => diff --git a/src/Filee.Engines/Ocr/TesseractConverter.cs b/src/Filee.Engines/Ocr/TesseractConverter.cs index a5c8f9f58..89c5a09fc 100644 --- a/src/Filee.Engines/Ocr/TesseractConverter.cs +++ b/src/Filee.Engines/Ocr/TesseractConverter.cs @@ -92,7 +92,7 @@ public async Task> ConvertAsync(ConversionStep step, IProg var tessdata = Path.Combine(scratch, "tessdata"); Directory.CreateDirectory(tessdata); File.Copy(data, Path.Combine(tessdata, Path.GetFileName(data))); - var ocr = new Run(tesseract, tessdata, language.Code); + var ocr = new Run(tesseract, scratch, language.Code); progress?.Report(0.05); var output = step.Output.Allocate("pdf"); @@ -104,9 +104,11 @@ public async Task> ConvertAsync(ConversionStep step, IProg } else { - var target = Path.Combine(scratch, "page"); - await ocr.ToPdfAsync(step.InputPath, target, HasResolution(step.InputPath) ? null : Dpi, cancellationToken); - File.Move(target + ".pdf", output, overwrite: true); + // A copy with a plain name: the picture's own folder and name may not be readable for Tesseract (Run). + var picture = "picture" + Path.GetExtension(step.InputPath).ToLowerInvariant(); + File.Copy(step.InputPath, Path.Combine(scratch, picture)); + await ocr.ToPdfAsync(picture, "page", HasResolution(step.InputPath) ? null : Dpi, cancellationToken); + File.Move(Path.Combine(scratch, "page.pdf"), output, overwrite: true); } progress?.Report(1); return [output]; @@ -136,6 +138,8 @@ private static async Task RecognizePdfAsync(Run ocr, ConversionStep step, string using var free = new SemaphoreSlim(Runs); var runs = new List(); var done = 0; + try + { foreach (var page in scans) { await free.WaitAsync(cancellationToken); @@ -154,14 +158,13 @@ private static async Task RecognizePdfAsync(Run ocr, ConversionStep step, string catch { free.Release(); - await Task.WhenAll(runs).ContinueWith(_ => { }, TaskScheduler.Default); // let the running pages end first throw; } runs.Add(Task.Run(async () => { try { - await ocr.ToPdfAsync(picture, Path.Combine(scratch, $"p{page}"), Dpi, cancellationToken); + await ocr.ToPdfAsync($"p{page}.png", $"p{page}", Dpi, cancellationToken); File.Delete(picture); progress?.Report(0.05 + 0.9 * Interlocked.Increment(ref done) / scans.Count); } @@ -171,15 +174,25 @@ private static async Task RecognizePdfAsync(Run ocr, ConversionStep step, string } }, cancellationToken)); } + } + finally + { + // Rendering stopped (a failed page, a cancel): the running pages end before the work folder can go. + await Task.WhenAll(runs).ContinueWith(_ => { }, TaskScheduler.Default); + } await Task.WhenAll(runs); - // A new document: the pages and the document information come along, bookmarks and form fields don't. + // A new document: the pages and the document information come along (not when the preset removes metadata), + // bookmarks and form fields don't. using var source = PdfSharpConverter.Open(step.InputPath, PdfDocumentOpenMode.Import); using var result = new PdfDocument(); - result.Info.Title = source.Info.Title; - result.Info.Author = source.Info.Author; - result.Info.Subject = source.Info.Subject; - result.Info.Keywords = source.Info.Keywords; + if (!step.Preset.Pdf.RemoveMetadata) + { + result.Info.Title = source.Info.Title; + result.Info.Author = source.Info.Author; + result.Info.Subject = source.Info.Subject; + result.Info.Keywords = source.Info.Keywords; + } var recognized = new List(); try { @@ -254,18 +267,22 @@ private static bool HasResolution(string path) } } - /// One language, one Tesseract. - private sealed record Run(string Executable, string Tessdata, string Language) + /// + /// One language, one Tesseract, working in (the job's scratch folder, with the language in + /// its tessdata). Tesseract only ever sees plain relative names there: some Windows builds of Tesseract and + /// Leptonica open files through the ANSI code page and fail on a user name or folder in Korean or Cyrillic. + /// + private sealed record Run(string Executable, string Folder, string Language) { - /// Writes {outputBase}.pdf. + /// Reads and writes {outputBase}.pdf, both names in . public async Task ToPdfAsync(string input, string outputBase, int? dpi, CancellationToken cancellationToken) { // tessedit_create_pdf instead of the "pdf" config file, which lives in Tesseract's own tessdata folder. - List args = [input, outputBase, "--tessdata-dir", Tessdata, "-l", Language, "-c", "tessedit_create_pdf=1"]; + List args = [input, outputBase, "--tessdata-dir", "tessdata", "-l", Language, "-c", "tessedit_create_pdf=1"]; if (dpi is { } value) args.AddRange(["--dpi", value.ToString(System.Globalization.CultureInfo.InvariantCulture)]); - var result = await ProcessRunner.RunAsync(Executable, args, Timeout, cancellationToken, environment: Environment); - if (result.ExitCode != 0 || !File.Exists(outputBase + ".pdf")) + var result = await ProcessRunner.RunAsync(Executable, args, Timeout, cancellationToken, workingDirectory: Folder, environment: Environment); + if (result.ExitCode != 0 || !File.Exists(Path.Combine(Folder, outputBase + ".pdf"))) throw new InvalidOperationException($"Tesseract failed (exit {result.ExitCode}). {result.StandardError.Trim()}"); } } diff --git a/tests/Filee.App.Tests/RenderTests.cs b/tests/Filee.App.Tests/RenderTests.cs index cffc0059f..eb6346d9f 100644 --- a/tests/Filee.App.Tests/RenderTests.cs +++ b/tests/Filee.App.Tests/RenderTests.cs @@ -1194,6 +1194,36 @@ public void Preset_editor_shows_and_saves_text_recognition(string language) Assert.False(preset.Pdf.Ocr); } + [AvaloniaFact] + public void Preset_editor_hides_and_clears_what_text_recognition_does_not_do() + { + TestServices.EnsureInitialized("en"); + var loc = AppHost.Get(); + var preset = new Filee.Core.Presets.Preset + { + TargetFormat = "pdf", Name = "Scans", + Pdf = { SplitPages = true, Compression = Filee.Core.Presets.PdfCompression.Screen, PageSize = Filee.Core.Presets.PdfPageSize.A4, MarginMm = 10 }, + }; + var vm = new PresetEditorViewModel(preset, loc); + Assert.True(vm.ShowPdfSplit && vm.ShowPdfCompression && vm.ShowPageSize); + + vm.Ocr = true; + + Assert.False(vm.ShowPdfSplit); + Assert.False(vm.ShowPdfCompression); + Assert.False(vm.ShowPdfCompressionOptions); + Assert.False(vm.ShowPageSize); + Assert.True(vm.Apply()); + var pdf = preset.Pdf; + Assert.True(pdf.Ocr); + Assert.Equal((false, Filee.Core.Presets.PdfCompression.None, Filee.Core.Presets.PdfPageSize.FitImage, 0.0), + (pdf.SplitPages, pdf.Compression, pdf.PageSize, pdf.MarginMm)); + + // Without it they are back. + vm.Ocr = false; + Assert.True(vm.ShowPdfSplit && vm.ShowPdfCompression && vm.ShowPageSize); + } + [AvaloniaFact] public void Preset_editor_keeps_rotation_visible_for_pdf_and_drops_it_for_targets_that_cannot_turn() { diff --git a/tests/Filee.Engines.Tests/OcrTests.cs b/tests/Filee.Engines.Tests/OcrTests.cs index b8582e339..ea0d514b6 100644 --- a/tests/Filee.Engines.Tests/OcrTests.cs +++ b/tests/Filee.Engines.Tests/OcrTests.cs @@ -149,6 +149,51 @@ public async Task A_picture_becomes_a_searchable_pdf() Assert.Contains("searchable", text); } + [Fact] + public async Task Pictures_and_scans_with_korean_and_cyrillic_names_are_recognized() + { + Assert.SkipUnless(Installed, Skip); + var folder = Path.Combine(fx.NewFolder(), "김민수 Документы"); + Directory.CreateDirectory(folder); + var picture = Scan(folder, "스캔.png", "Filee reads scanned pages"); + var scanPdf = (await fx.ConvertAsync([Scan(folder, "Скан.png", "The quarterly report is ready")], new Preset { TargetFormat = "pdf" })).Outputs.Single(); + + var job = await fx.ConvertAsync([picture, scanPdf], Ocr("pdf")); + + AssertDone(job); + Assert.Contains("scanned", PageText(job.Outputs.Single(o => Path.GetFileName(o).StartsWith("스캔", StringComparison.Ordinal)), 1)); + Assert.Contains("quarterly", PageText(job.Outputs.Single(o => Path.GetFileName(o).StartsWith("Скан", StringComparison.Ordinal)), 1)); + } + + [Fact] + public async Task Removing_metadata_leaves_the_document_information_behind() + { + Assert.SkipUnless(Installed, Skip); + var folder = fx.NewFolder(); + var scanPdf = (await fx.ConvertAsync([Scan(folder, "scan.png", "Private letter")], new Preset { TargetFormat = "pdf" })).Outputs.Single(); + using (var document = PdfReader.Open(scanPdf, PdfDocumentOpenMode.Modify)) + { + document.Info.Title = "Secret title"; + document.Info.Author = "Someone"; + document.Save(scanPdf); + } + var keep = Ocr("pdf"); + var remove = Ocr("pdf"); + remove.Pdf.RemoveMetadata = true; + + var kept = (await fx.ConvertAsync([scanPdf], keep)).Outputs.Single(); + var removed = (await fx.ConvertAsync([scanPdf], remove)).Outputs.Single(); + + using (var document = PdfReader.Open(kept, PdfDocumentOpenMode.Import)) + Assert.Equal("Secret title", document.Info.Title); + using (var document = PdfReader.Open(removed, PdfDocumentOpenMode.Import)) + { + Assert.Equal("", document.Info.Title); + Assert.Equal("", document.Info.Author); + } + Assert.Contains("Private", PageText(removed, 1)); + } + [Fact] public async Task A_scanned_pdf_becomes_a_docx_with_its_text_and_without_the_scan() { From 6dab0c0526d9b41aca8e00bcffef9db0adfe4a2a Mon Sep 17 00:00:00 2001 From: cre8ive-marketing <168519979+cre8ive-marketing@users.noreply.github.com> Date: Fri, 23 Oct 2026 15:09:10 +0530 Subject: [PATCH 10/11] Format the OCR review changes --- src/Filee.Engines/Ocr/TesseractConverter.cs | 54 ++++++++++----------- tests/Filee.App.Tests/RenderTests.cs | 3 +- 2 files changed, 29 insertions(+), 28 deletions(-) diff --git a/src/Filee.Engines/Ocr/TesseractConverter.cs b/src/Filee.Engines/Ocr/TesseractConverter.cs index 89c5a09fc..f0f370130 100644 --- a/src/Filee.Engines/Ocr/TesseractConverter.cs +++ b/src/Filee.Engines/Ocr/TesseractConverter.cs @@ -140,40 +140,40 @@ private static async Task RecognizePdfAsync(Run ocr, ConversionStep step, string var done = 0; try { - foreach (var page in scans) - { - await free.WaitAsync(cancellationToken); - if (runs.Any(r => r.IsFaulted)) - { - free.Release(); - break; // a page failed: stop rendering, the error comes out below - } - var picture = Path.Combine(scratch, $"p{page}.png"); - try - { - using var bitmap = Conversion.ToImage(bytes, page, null, options); - await using var file = File.Create(picture); - bitmap.Encode(file, SKEncodedImageFormat.Png, 100); - } - catch - { - free.Release(); - throw; - } - runs.Add(Task.Run(async () => + foreach (var page in scans) { + await free.WaitAsync(cancellationToken); + if (runs.Any(r => r.IsFaulted)) + { + free.Release(); + break; // a page failed: stop rendering, the error comes out below + } + var picture = Path.Combine(scratch, $"p{page}.png"); try { - await ocr.ToPdfAsync($"p{page}.png", $"p{page}", Dpi, cancellationToken); - File.Delete(picture); - progress?.Report(0.05 + 0.9 * Interlocked.Increment(ref done) / scans.Count); + using var bitmap = Conversion.ToImage(bytes, page, null, options); + await using var file = File.Create(picture); + bitmap.Encode(file, SKEncodedImageFormat.Png, 100); } - finally + catch { free.Release(); + throw; } - }, cancellationToken)); - } + runs.Add(Task.Run(async () => + { + try + { + await ocr.ToPdfAsync($"p{page}.png", $"p{page}", Dpi, cancellationToken); + File.Delete(picture); + progress?.Report(0.05 + 0.9 * Interlocked.Increment(ref done) / scans.Count); + } + finally + { + free.Release(); + } + }, cancellationToken)); + } } finally { diff --git a/tests/Filee.App.Tests/RenderTests.cs b/tests/Filee.App.Tests/RenderTests.cs index eb6346d9f..587b7fdf7 100644 --- a/tests/Filee.App.Tests/RenderTests.cs +++ b/tests/Filee.App.Tests/RenderTests.cs @@ -1201,7 +1201,8 @@ public void Preset_editor_hides_and_clears_what_text_recognition_does_not_do() var loc = AppHost.Get(); var preset = new Filee.Core.Presets.Preset { - TargetFormat = "pdf", Name = "Scans", + TargetFormat = "pdf", + Name = "Scans", Pdf = { SplitPages = true, Compression = Filee.Core.Presets.PdfCompression.Screen, PageSize = Filee.Core.Presets.PdfPageSize.A4, MarginMm = 10 }, }; var vm = new PresetEditorViewModel(preset, loc); From fc189f81497258616ac5c214dea723b8798aaf96 Mon Sep 17 00:00:00 2001 From: KnifeLemon Date: Fri, 23 Oct 2026 19:13:34 +0900 Subject: [PATCH 11/11] OCR: polite Korean strings, OCR languages in the translated privacy notes - ko: engine.tesseract.description, presets.ocr_hint and engines.package.tesseract.description in the polite style of the rest of ko.json. - README.fr, README.pt-BR and README.th (the translations with the privacy section): the OCR languages pinned in ocr-languages.json, as README.md has it. Co-Authored-By: Claude Opus 5.5 --- README.fr.md | 2 +- README.pt-BR.md | 2 +- README.th.md | 2 +- src/Filee.App/Assets/i18n/ko.json | 6 +++--- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/README.fr.md b/README.fr.md index 4618deb7c..3279d4195 100644 --- a/README.fr.md +++ b/README.fr.md @@ -216,7 +216,7 @@ Il ne se connecte à Internet que : - pour demander à GitHub (`api.github.com`) si une nouvelle version existe, peu après le démarrage puis toutes les quelques heures. Désactivez-le dans Paramètres → À propos (« Rechercher automatiquement les mises à jour »). -- pour télécharger les moteurs de conversion que vous choisissez d'installer, depuis les emplacements officiels fixés dans [`engines.json`](src/Filee.Engines/Infrastructure/engines.json). +- pour télécharger les moteurs de conversion que vous choisissez d'installer, depuis les emplacements officiels fixés dans [`engines.json`](src/Filee.Engines/Infrastructure/engines.json), et les langues de reconnaissance de texte (OCR) qu'un préréglage utilise, fixées dans [`ocr-languages.json`](src/Filee.Engines/Infrastructure/ocr-languages.json). - quand vous cliquez sur un lien, ce qui ouvre votre navigateur web. ## Soutenir Filee diff --git a/README.pt-BR.md b/README.pt-BR.md index 280cf4b55..4ec6584ff 100644 --- a/README.pt-BR.md +++ b/README.pt-BR.md @@ -216,7 +216,7 @@ Ele se conecta à internet só: - para perguntar ao GitHub (`api.github.com`) se existe uma versão mais nova, pouco depois de iniciar e depois a cada algumas horas. Desligue isso em Configurações → Sobre (“Procurar atualizações automaticamente”). -- para baixar os motores de conversão que você escolher instalar, dos locais oficiais fixados em [`engines.json`](src/Filee.Engines/Infrastructure/engines.json). +- para baixar os motores de conversão que você escolher instalar, dos locais oficiais fixados em [`engines.json`](src/Filee.Engines/Infrastructure/engines.json), e os idiomas de reconhecimento de texto (OCR) que uma predefinição usa, fixados em [`ocr-languages.json`](src/Filee.Engines/Infrastructure/ocr-languages.json). - quando você clica num link, o que abre o navegador. ## Patrocínio diff --git a/README.th.md b/README.th.md index 4708a7079..2dc273973 100644 --- a/README.th.md +++ b/README.th.md @@ -215,7 +215,7 @@ Filee ทำการแปลงไฟล์ภายในเครื่อ การเชื่อมต่ออินเทอร์เน็ตจะเกิดขึ้นเฉพาะในกรณีต่อไปนี้: - ตรวจสอบเวอร์ชันใหม่กับ GitHub (`api.github.com`) หลังเปิดโปรแกรมและทุกๆ สองสามชั่วโมง (ปิดได้ที่ การตั้งค่า → เกี่ยวกับ "ตรวจสอบการอัปเดตอัตโนมัติ") -- ดาวน์โหลดเครื่องมือแปลงไฟล์ที่คุณเลือกติดตั้ง จากแหล่งทางการที่ระบุไว้ใน [`engines.json`](src/Filee.Engines/Infrastructure/engines.json) +- ดาวน์โหลดเครื่องมือแปลงไฟล์ที่คุณเลือกติดตั้ง จากแหล่งทางการที่ระบุไว้ใน [`engines.json`](src/Filee.Engines/Infrastructure/engines.json) และภาษาสำหรับการรู้จำข้อความ (OCR) ที่พรีเซ็ตใช้ ซึ่งระบุไว้ใน [`ocr-languages.json`](src/Filee.Engines/Infrastructure/ocr-languages.json) - เมื่อคุณคลิกลิงก์ ซึ่งจะเปิดหน้าเว็บในเบราว์เซอร์ของคุณ ## ผู้สนับสนุน (Sponsor) diff --git a/src/Filee.App/Assets/i18n/ko.json b/src/Filee.App/Assets/i18n/ko.json index a35c2928c..a851b05e6 100644 --- a/src/Filee.App/Assets/i18n/ko.json +++ b/src/Filee.App/Assets/i18n/ko.json @@ -153,7 +153,7 @@ "engine.ffmpeg.description": "MP4, MOV, MKV, WEBM, MP3, M4A, WAV, FLAC 등 동영상·오디오를 변환해요.\n소리·자막 추출, GIF·APNG 만들기, 이미지 → HEIC도 해요.", "engine.pandoc.description": "마크다운·HTML ↔ ODT·RTF, DOCX → 마크다운\nreStructuredText·LaTeX·Org·AsciiDoc·Typst·MediaWiki·DocBook·Jupyter 등 ↔ 마크다운·HTML·DOCX·ODT\n마크다운·Org → PPTX 슬라이드", "engine.ghostscript.description": "EPS, 포스트스크립트, 예전 일러스트레이터 파일을 PDF와 이미지로, PDF를 EPS·PS로.", - "engine.tesseract.description": "스캔본과 사진의 글자를 인식합니다(OCR): 검색 가능한 PDF, DOCX, TXT.", + "engine.tesseract.description": "스캔본과 사진의 글자를 인식해요(OCR): 검색 가능한 PDF, DOCX, TXT.", "engine.calibre.description": "드문 전자책 형식(LIT, LRF, CHM, PDB, RB…)과 킨들용 출력(MOBI, AZW3).\nHWPX writer보다 위에 두면 전자책 PDF도 만들어요(긴 책은 훨씬 빨라요).", "engine.libreoffice.description": "DOC, PPT, Pages, Keynote, Numbers, 워드퍼펙트, ClarisWorks, Publisher, Visio 같은 오래되었거나 드문 형식을 읽어요.\nDOC·XLS·PPT·ODT·ODP, 오픈도큐먼트 서식 파일, EMF로 저장도 해요.", @@ -546,7 +546,7 @@ "presets.pdf_ghostscript": "Ghostscript로도 해 보기 (설치된 경우)", "presets.pdf_ghostscript_hint": "더 작아질 때가 많지만 파일 전체를 다시 써요.\n가장 작은 결과를 남겨요.", "presets.ocr": "텍스트 인식 (OCR)", - "presets.ocr_hint": "스캔본과 사진의 글자를 검색하고 편집할 수 있게 됩니다. Tesseract가 필요해요(설정 → 변환 엔진).\n이미 텍스트가 있는 페이지는 그대로 둡니다.", + "presets.ocr_hint": "스캔본과 사진의 글자를 검색하고 편집할 수 있게 돼요. Tesseract가 필요해요(설정 → 변환 엔진).\n이미 텍스트가 있는 페이지는 그대로 둬요.", "presets.ocr_language": "텍스트 언어", "presets.ocr_language_size": "{0} 내려받기", "presets.pdf_keep_metadata": "페이지·이미지 메타데이터(XMP) 유지", @@ -682,7 +682,7 @@ "engines.package.ghostscript.name": "Ghostscript (EPS·PostScript)", "engines.package.ghostscript.description": "EPS·PS와 예전 일러스트레이터 파일을 PDF·이미지로, PDF·SVG를 EPS·PS로 바꿔요.\nSVG와 PDF 호환 AI 파일은 없어도 돼요.", "engines.package.tesseract.name": "Tesseract (텍스트 인식, OCR)", - "engines.package.tesseract.description": "스캔한 PDF와 사진의 글자를 읽습니다: 검색 가능한 PDF, DOCX, TXT.\n언어는 처음 쓸 때 내려받습니다.", + "engines.package.tesseract.description": "스캔한 PDF와 사진의 글자를 읽어요: 검색 가능한 PDF, DOCX, TXT.\n언어는 처음 쓸 때 내려받아요.", "engines.package.ffmpeg.name": "FFmpeg (동영상·오디오)", "engines.package.ffmpeg.description": "동영상·오디오 변환, 동영상 → GIF·정지 이미지, 소리 추출, GIF → MP4를 추가해요.\n동영상과 오디오에만 필요해요.", "engines.package.calibre.name": "Calibre (킨들과 드문 전자책 형식)",