vendor: import KillerPDF 1.7.5 source (GPL-3.0) as the base for MMD PDF
This commit is contained in:
@@ -0,0 +1,143 @@
|
||||
using System.IO;
|
||||
using System.Reflection;
|
||||
using System.Runtime.InteropServices;
|
||||
|
||||
namespace KillerPDF.Services
|
||||
{
|
||||
/// <summary>
|
||||
/// Keeps the single-exe build self-sufficient for OCR. The native Tesseract DLLs (x64) and the bundled
|
||||
/// language data are embedded as resources and self-extracted on first use, the same pattern Costura
|
||||
/// uses for the managed assemblies. Native libs go in a per-version cache (they must match the app);
|
||||
/// language data goes in a STABLE folder so user-downloaded packs survive app updates. Thread-safe.
|
||||
/// </summary>
|
||||
internal static class OcrNativeBootstrap
|
||||
{
|
||||
private const string NativePrefix = "KillerPDF.OcrNative.";
|
||||
private const string TessDataPrefix = "KillerPDF.OcrTessData.";
|
||||
|
||||
private static readonly object _gate = new();
|
||||
private static bool _langReady;
|
||||
private static bool _nativeReady;
|
||||
|
||||
[DllImport("kernel32", SetLastError = true, CharSet = CharSet.Unicode)]
|
||||
private static extern bool SetDllDirectory(string lpPathName);
|
||||
|
||||
[DllImport("kernel32", SetLastError = true, CharSet = CharSet.Unicode)]
|
||||
private static extern IntPtr LoadLibrary(string lpFileName);
|
||||
|
||||
/// <summary>
|
||||
/// Version-independent tessdata folder. The bundled English is extracted here on first use, and
|
||||
/// user-downloaded language packs are written here too, so they persist across app updates.
|
||||
/// </summary>
|
||||
public static string TessDataDir { get; } = Path.Combine(
|
||||
Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData),
|
||||
"KillerPDF", "tessdata");
|
||||
|
||||
/// <summary>
|
||||
/// Ensures the bundled language data (English) is present in <see cref="TessDataDir"/> and returns
|
||||
/// that folder. Light - does not touch the native libraries, so it is safe to call just to inspect
|
||||
/// or list installed languages (e.g. when building the language menu).
|
||||
/// </summary>
|
||||
public static string EnsureLanguageData()
|
||||
{
|
||||
if (_langReady) return TessDataDir;
|
||||
lock (_gate)
|
||||
{
|
||||
if (_langReady) return TessDataDir;
|
||||
Directory.CreateDirectory(TessDataDir);
|
||||
|
||||
var asm = typeof(OcrNativeBootstrap).Assembly;
|
||||
foreach (string res in asm.GetManifestResourceNames())
|
||||
{
|
||||
if (res.StartsWith(TessDataPrefix, StringComparison.Ordinal))
|
||||
{
|
||||
string file = res[TessDataPrefix.Length..];
|
||||
ExtractResource(asm, res, Path.Combine(TessDataDir, file), onlyIfMissing: true);
|
||||
}
|
||||
}
|
||||
|
||||
_langReady = true;
|
||||
return TessDataDir;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Extracts the native libs to a per-version cache, ensures language data, configures Tesseract's
|
||||
/// native loader, and returns the tessdata folder for OcrService. Call before constructing OcrService.
|
||||
/// </summary>
|
||||
public static string EnsureReady()
|
||||
{
|
||||
EnsureLanguageData();
|
||||
if (_nativeReady) return TessDataDir;
|
||||
lock (_gate)
|
||||
{
|
||||
if (_nativeReady) return TessDataDir;
|
||||
|
||||
var asm = typeof(OcrNativeBootstrap).Assembly;
|
||||
string version = asm.GetName().Version?.ToString() ?? "0";
|
||||
string baseDir = Path.Combine(
|
||||
Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData),
|
||||
"KillerPDF", "ocr", version);
|
||||
string nativeDir = Path.Combine(baseDir, "x64");
|
||||
Directory.CreateDirectory(nativeDir);
|
||||
|
||||
foreach (string res in asm.GetManifestResourceNames())
|
||||
{
|
||||
if (res.StartsWith(NativePrefix, StringComparison.Ordinal))
|
||||
{
|
||||
string file = res[NativePrefix.Length..];
|
||||
// Tesseract's loader looks in the x64 subfolder; the flat copy covers any loader
|
||||
// path that does not append the platform name.
|
||||
ExtractResource(asm, res, Path.Combine(nativeDir, file), onlyIfMissing: false);
|
||||
ExtractResource(asm, res, Path.Combine(baseDir, file), onlyIfMissing: false);
|
||||
}
|
||||
}
|
||||
|
||||
// Point Tesseract's native loader at the cache. Reflection avoids a compile-time bind in
|
||||
// case the loader type's visibility differs across package versions; the preload below is
|
||||
// the hard guarantee regardless.
|
||||
try
|
||||
{
|
||||
var loaderType = Type.GetType("InteropDotNet.LibraryLoader, Tesseract");
|
||||
object? instance = loaderType?
|
||||
.GetProperty("Instance", BindingFlags.Public | BindingFlags.Static)?
|
||||
.GetValue(null);
|
||||
loaderType?.GetProperty("CustomSearchPath")?.SetValue(instance, baseDir);
|
||||
}
|
||||
catch { /* fall through to the preload */ }
|
||||
|
||||
// Belt and suspenders: add the native dir to the DLL search path and preload the libs.
|
||||
// leptonica must load before tesseract50, which depends on it.
|
||||
try
|
||||
{
|
||||
SetDllDirectory(nativeDir);
|
||||
foreach (string dll in Directory.GetFiles(nativeDir, "leptonica*.dll")) LoadLibrary(dll);
|
||||
foreach (string dll in Directory.GetFiles(nativeDir, "tesseract*.dll")) LoadLibrary(dll);
|
||||
}
|
||||
catch { /* loader search paths above still apply */ }
|
||||
|
||||
_nativeReady = true;
|
||||
return TessDataDir;
|
||||
}
|
||||
}
|
||||
|
||||
private static void ExtractResource(Assembly asm, string resourceName, string targetPath, bool onlyIfMissing)
|
||||
{
|
||||
// Language data is extracted only-if-missing: a user-downloaded pack (e.g. a high-quality model,
|
||||
// or an HQ English) must never be clobbered by the bundled copy on the next launch. Native libs
|
||||
// keep the length check so a version change refreshes them.
|
||||
if (onlyIfMissing && File.Exists(targetPath)) return;
|
||||
|
||||
using var src = asm.GetManifestResourceStream(resourceName);
|
||||
if (src == null) return;
|
||||
|
||||
if (!onlyIfMissing && File.Exists(targetPath) && new FileInfo(targetPath).Length == src.Length) return;
|
||||
|
||||
string tmp = targetPath + ".tmp";
|
||||
using (var dst = File.Create(tmp))
|
||||
src.CopyTo(dst);
|
||||
if (File.Exists(targetPath)) File.Delete(targetPath);
|
||||
File.Move(tmp, targetPath);
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user