Skip to content

Commit c96a847

Browse files
authored
Merge pull request #46 from sjvrensburg/macos-prep
macOS compatibility: native library resolution and config paths
2 parents 363c9b5 + d041eaa commit c96a847

7 files changed

Lines changed: 111 additions & 8 deletions

File tree

CLAUDE.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -138,7 +138,7 @@ Four SkSL shaders compiled at startup via `SKRuntimeEffect.CreateColorFilter()`.
138138

139139
## Configuration
140140

141-
Config: `~/.config/railreader2/config.json` (Linux) or `%APPDATA%\railreader2\config.json` (Windows). Auto-created with defaults. Editable live via Settings panel. See `Services/AppConfig.cs` for all fields and defaults.
141+
Config: `~/.config/railreader2/config.json` (Linux), `%APPDATA%\railreader2\config.json` (Windows), or `~/Library/Application Support/railreader2/config.json` (macOS). Auto-created with defaults. Editable live via Settings panel. See `Services/AppConfig.cs` for all fields and defaults.
142142

143143
Key fields: `rail_zoom_threshold`, `snap_duration_ms`, `scroll_speed_start/max`, `scroll_ramp_time`, `analysis_lookahead_pages`, `ui_font_scale`, `colour_effect/intensity`, `motion_blur/intensity`, `pixel_snapping`, `line_focus_blur/intensity`, `line_highlight_tint/opacity`, `auto_scroll_line_pause_ms/block_pause_ms`, `jump_percentage`, `dark_mode`, `navigable_classes[]`, `recent_files[]`.
144144

@@ -148,6 +148,8 @@ Key fields: `rail_zoom_threshold`, `snap_duration_ms`, `scroll_speed_start/max`,
148148

149149
- ONNX model outputs `[N, 7]` tensors with reading order as the 7th column (Global Pointer Mechanism)
150150
- `PdfOutlineExtractor` uses direct PDFium P/Invoke (`FPDF_*`/`FPDFBookmark_*`) since PDFtoImage doesn't expose bookmarks
151+
- `PdfiumResolver.Initialize()` must be called before any PDFium P/Invoke — registers a `DllImportResolver` that maps "pdfium" to the correct platform-specific native library (pdfium.dll / libpdfium.so / libpdfium.dylib). Called in all entry points (GUI, CLI, Agent)
152+
- ONNX runtime pre-loading in `LayoutAnalyzer` static constructor handles Linux (.so) and macOS (.dylib); Windows uses OnnxRuntime's own resolver
151153
- SkiaSharp 3.x explicitly overrides Avalonia 11's bundled 2.88 — required for `SKRuntimeEffect.CreateColorFilter()`
152154
- TODO.md and DISTRIBUTION.md are legacy Rust files — disregard them
153155
- Without the ONNX model, layout falls back to simple horizontal strip detection

src/RailReader.Agent/Program.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@
66
using RailReader.Core.Services;
77
using RailReader.Agent;
88

9+
PdfiumResolver.Initialize();
10+
911
// --- Check for --capture-screenshots mode ---
1012
if (args.Length >= 1 && args[0] == "--capture-screenshots")
1113
{

src/RailReader.Cli/Program.cs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,9 @@
22
using RailReader.Cli;
33
using RailReader.Cli.Commands;
44
using RailReader.Cli.Output;
5+
using RailReader.Core.Services;
6+
7+
PdfiumResolver.Initialize();
58

69
var jsonOption = new Option<bool>("--json") { Description = "Output in JSON format for machine consumption", Recursive = true };
710

src/RailReader.Core/Services/AppConfig.cs

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -48,9 +48,15 @@ public static string ConfigDir
4848
{
4949
get
5050
{
51-
var baseDir = OperatingSystem.IsWindows()
52-
? Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData)
53-
: Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), ".config");
51+
string baseDir;
52+
if (OperatingSystem.IsWindows())
53+
baseDir = Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData);
54+
else if (OperatingSystem.IsMacOS())
55+
baseDir = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile),
56+
"Library", "Application Support");
57+
else
58+
baseDir = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), ".config");
59+
5460
var dir = Path.Combine(baseDir, "railreader2");
5561
Directory.CreateDirectory(dir);
5662
return dir;

src/RailReader.Core/Services/LayoutAnalyzer.cs

Lines changed: 18 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -17,15 +17,29 @@ static LayoutAnalyzer()
1717
// P/Invoke inside OnnxRuntime finds it already loaded — no resolver conflict.
1818
// (SetDllImportResolver can only be called once per assembly and OnnxRuntime
1919
// registers its own, so we must not use it.)
20-
if (!RuntimeInformation.IsOSPlatform(OSPlatform.Linux)) return;
20+
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) return;
2121

22+
// Platform-specific library name and fallback RIDs
23+
string ext, fallbackRid;
24+
if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX))
25+
{
26+
ext = ".dylib";
27+
fallbackRid = "osx-arm64";
28+
}
29+
else
30+
{
31+
ext = ".so";
32+
fallbackRid = "linux-x64";
33+
}
34+
35+
string libName = $"libonnxruntime{ext}";
2236
string[] candidates =
2337
[
2438
Path.Combine(AppContext.BaseDirectory,
25-
"runtimes", RuntimeInformation.RuntimeIdentifier, "native", "libonnxruntime.so"),
39+
"runtimes", RuntimeInformation.RuntimeIdentifier, "native", libName),
2640
Path.Combine(AppContext.BaseDirectory,
27-
"runtimes", "linux-x64", "native", "libonnxruntime.so"),
28-
Path.Combine(AppContext.BaseDirectory, "libonnxruntime.so"),
41+
"runtimes", fallbackRid, "native", libName),
42+
Path.Combine(AppContext.BaseDirectory, libName),
2943
];
3044

3145
foreach (var path in candidates)
Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
using System.Reflection;
2+
using System.Runtime.InteropServices;
3+
4+
namespace RailReader.Core.Services;
5+
6+
/// <summary>
7+
/// Registers a DllImport resolver for the "pdfium" native library.
8+
/// On Linux/macOS the NuGet package ships as libpdfium.so/.dylib in
9+
/// runtimes/{rid}/native/. .NET's default probing normally finds it
10+
/// (tries lib prefix + platform extension), but this resolver provides
11+
/// an explicit fallback to guarantee resolution on all platforms.
12+
///
13+
/// Call <see cref="Initialize"/> once at startup, before any P/Invoke
14+
/// into PDFium (PdfOutlineExtractor, PdfTextService).
15+
/// PDFtoImage must be referenced so the native asset is deployed.
16+
/// </summary>
17+
public static class PdfiumResolver
18+
{
19+
private static bool s_initialized;
20+
21+
public static void Initialize()
22+
{
23+
if (s_initialized) return;
24+
s_initialized = true;
25+
26+
NativeLibrary.SetDllImportResolver(
27+
typeof(PdfiumResolver).Assembly,
28+
ResolvePdfium);
29+
}
30+
31+
private static IntPtr ResolvePdfium(string libraryName, Assembly assembly, DllImportSearchPath? searchPath)
32+
{
33+
if (libraryName != "pdfium")
34+
return IntPtr.Zero; // fall back to default resolution
35+
36+
// Try default probing first (handles most cases)
37+
if (NativeLibrary.TryLoad(libraryName, assembly, searchPath, out var handle))
38+
return handle;
39+
40+
// Explicit platform-specific fallback
41+
string ext, prefix;
42+
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
43+
{
44+
ext = ".dll";
45+
prefix = "";
46+
}
47+
else if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX))
48+
{
49+
ext = ".dylib";
50+
prefix = "lib";
51+
}
52+
else
53+
{
54+
ext = ".so";
55+
prefix = "lib";
56+
}
57+
58+
string libFile = $"{prefix}pdfium{ext}";
59+
string[] candidates =
60+
[
61+
Path.Combine(AppContext.BaseDirectory,
62+
"runtimes", RuntimeInformation.RuntimeIdentifier, "native", libFile),
63+
Path.Combine(AppContext.BaseDirectory, libFile),
64+
];
65+
66+
foreach (var path in candidates)
67+
{
68+
if (File.Exists(path) && NativeLibrary.TryLoad(path, out handle))
69+
return handle;
70+
}
71+
72+
return IntPtr.Zero;
73+
}
74+
}

src/RailReader2/Program.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
using Avalonia;
2+
using RailReader.Core.Services;
23

34
namespace RailReader2;
45

@@ -7,6 +8,7 @@ internal sealed class Program
78
[STAThread]
89
public static void Main(string[] args)
910
{
11+
PdfiumResolver.Initialize();
1012
BuildAvaloniaApp()
1113
.StartWithClassicDesktopLifetime(args);
1214
}

0 commit comments

Comments
 (0)