@papyrus-sdk/engine-native 0.2.9 → 0.2.11

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,240 @@
1
+ #include "papyrus_outline_loader.h"
2
+
3
+ #include <cstdlib>
4
+ #include <cstring>
5
+ #include <iostream>
6
+ #include <string>
7
+ #include <unordered_map>
8
+
9
+ namespace {
10
+
11
+ using PdfiumSymbol = OutlineLoaderDeps::PdfiumSymbol;
12
+
13
+ struct FakeLoaderEnv {
14
+ void *library_handle = reinterpret_cast<void *>(0x1);
15
+ int dlopen_calls = 0;
16
+ int dlsym_calls = 0;
17
+ int dlclose_calls = 0;
18
+ std::unordered_map<std::string, PdfiumSymbol> symbols;
19
+ };
20
+
21
+ FPDF_DOCUMENT FakeLoadDocument(const char *, const char *) {
22
+ return nullptr;
23
+ }
24
+
25
+ void FakeCloseDocument(FPDF_DOCUMENT) {}
26
+
27
+ FPDF_BOOKMARK FakeBookmarkGetFirstChild(FPDF_DOCUMENT, FPDF_BOOKMARK) {
28
+ return nullptr;
29
+ }
30
+
31
+ FPDF_BOOKMARK FakeBookmarkGetNextSibling(FPDF_DOCUMENT, FPDF_BOOKMARK) {
32
+ return nullptr;
33
+ }
34
+
35
+ unsigned long FakeBookmarkGetTitle(FPDF_BOOKMARK, void *, unsigned long) {
36
+ return 0;
37
+ }
38
+
39
+ FPDF_DEST FakeBookmarkGetDest(FPDF_DOCUMENT, FPDF_BOOKMARK) {
40
+ return nullptr;
41
+ }
42
+
43
+ int FakeDestGetPageIndex(FPDF_DOCUMENT, FPDF_DEST) {
44
+ return -1;
45
+ }
46
+
47
+ template <typename FnType>
48
+ PdfiumSymbol ToPdfiumSymbol(FnType fn) {
49
+ PdfiumSymbol symbol = nullptr;
50
+ static_assert(sizeof(symbol) == sizeof(fn),
51
+ "Expected function pointers to share the same size");
52
+ std::memcpy(&symbol, &fn, sizeof(symbol));
53
+ return symbol;
54
+ }
55
+
56
+ PdfiumOutlineFns MakeCompleteOutlineFns() {
57
+ PdfiumOutlineFns fns = {};
58
+ fns.loadDocument = &FakeLoadDocument;
59
+ fns.closeDocument = &FakeCloseDocument;
60
+ fns.bookmarkGetFirstChild = &FakeBookmarkGetFirstChild;
61
+ fns.bookmarkGetNextSibling = &FakeBookmarkGetNextSibling;
62
+ fns.bookmarkGetTitle = &FakeBookmarkGetTitle;
63
+ fns.bookmarkGetDest = &FakeBookmarkGetDest;
64
+ fns.destGetPageIndex = &FakeDestGetPageIndex;
65
+ return fns;
66
+ }
67
+
68
+ FakeLoaderEnv *GetEnv(void *user_data) {
69
+ return static_cast<FakeLoaderEnv *>(user_data);
70
+ }
71
+
72
+ void *FakeDlopen(void *user_data, const char *, int) {
73
+ FakeLoaderEnv *env = GetEnv(user_data);
74
+ env->dlopen_calls += 1;
75
+ return env->library_handle;
76
+ }
77
+
78
+ PdfiumSymbol FakeDlsym(void *user_data, void *, const char *symbol_name) {
79
+ FakeLoaderEnv *env = GetEnv(user_data);
80
+ env->dlsym_calls += 1;
81
+
82
+ const auto it = env->symbols.find(symbol_name);
83
+ if (it == env->symbols.end()) {
84
+ return nullptr;
85
+ }
86
+
87
+ return it->second;
88
+ }
89
+
90
+ int FakeDlclose(void *user_data, void *) {
91
+ FakeLoaderEnv *env = GetEnv(user_data);
92
+ env->dlclose_calls += 1;
93
+ return 0;
94
+ }
95
+
96
+ OutlineLoaderDeps MakeDeps(FakeLoaderEnv *env) {
97
+ OutlineLoaderDeps deps = {};
98
+ deps.user_data = env;
99
+ deps.dlopen_fn = &FakeDlopen;
100
+ deps.dlsym_fn = &FakeDlsym;
101
+ deps.dlclose_fn = &FakeDlclose;
102
+ deps.library_name = "libmodpdfium.so";
103
+ deps.dlopen_flags = 0;
104
+ return deps;
105
+ }
106
+
107
+ void InstallSymbols(FakeLoaderEnv *env, const PdfiumOutlineFns &fns) {
108
+ env->symbols["FPDF_LoadDocument"] = ToPdfiumSymbol(fns.loadDocument);
109
+ env->symbols["FPDF_CloseDocument"] = ToPdfiumSymbol(fns.closeDocument);
110
+ env->symbols["FPDFBookmark_GetFirstChild"] = ToPdfiumSymbol(fns.bookmarkGetFirstChild);
111
+ env->symbols["FPDFBookmark_GetNextSibling"] = ToPdfiumSymbol(fns.bookmarkGetNextSibling);
112
+ env->symbols["FPDFBookmark_GetTitle"] = ToPdfiumSymbol(fns.bookmarkGetTitle);
113
+ env->symbols["FPDFBookmark_GetDest"] = ToPdfiumSymbol(fns.bookmarkGetDest);
114
+ env->symbols["FPDFDest_GetPageIndex"] = ToPdfiumSymbol(fns.destGetPageIndex);
115
+ }
116
+
117
+ [[noreturn]] void Fail(const char *expression, const char *file, int line) {
118
+ std::cerr << file << ":" << line << ": assertion failed: " << expression << std::endl;
119
+ std::exit(1);
120
+ }
121
+
122
+ #define EXPECT_TRUE(condition) \
123
+ do { \
124
+ if (!(condition)) { \
125
+ Fail(#condition, __FILE__, __LINE__); \
126
+ } \
127
+ } while (false)
128
+
129
+ #define EXPECT_FALSE(condition) EXPECT_TRUE(!(condition))
130
+
131
+ #define EXPECT_EQ(expected, actual) \
132
+ do { \
133
+ const auto expected_value = (expected); \
134
+ const auto actual_value = (actual); \
135
+ if (!(expected_value == actual_value)) { \
136
+ std::cerr << __FILE__ << ":" << __LINE__ \
137
+ << ": expected equality for " << #expected << " and " << #actual << std::endl; \
138
+ std::exit(1); \
139
+ } \
140
+ } while (false)
141
+
142
+ void TestFullSymbolTableReturnsSupported() {
143
+ FakeLoaderEnv env = {};
144
+ const PdfiumOutlineFns complete_fns = MakeCompleteOutlineFns();
145
+ InstallSymbols(&env, complete_fns);
146
+
147
+ OutlineLoaderState state = {};
148
+ const OutlineLoaderDeps deps = MakeDeps(&env);
149
+
150
+ const OutlineLoadAttemptResult result = EnsureOutlinePdfiumLoaded(&state, &deps);
151
+
152
+ EXPECT_TRUE(result.loaded);
153
+ EXPECT_FALSE(result.transitioned_to_unsupported);
154
+ EXPECT_EQ(OutlineLoadState::kSupported, state.load_state);
155
+ EXPECT_TRUE(state.handle != nullptr);
156
+ EXPECT_TRUE(HasCompleteOutlineFns(state.fns));
157
+ EXPECT_EQ(1, env.dlopen_calls);
158
+ EXPECT_EQ(7, env.dlsym_calls);
159
+ EXPECT_EQ(0, env.dlclose_calls);
160
+ }
161
+
162
+ void TestMissingLibraryReturnsUnsupported() {
163
+ FakeLoaderEnv env = {};
164
+ env.library_handle = nullptr;
165
+
166
+ OutlineLoaderState state = {};
167
+ const OutlineLoaderDeps deps = MakeDeps(&env);
168
+
169
+ const OutlineLoadAttemptResult result = EnsureOutlinePdfiumLoaded(&state, &deps);
170
+
171
+ EXPECT_FALSE(result.loaded);
172
+ EXPECT_TRUE(result.transitioned_to_unsupported);
173
+ EXPECT_EQ(OutlineLoadState::kUnsupported, state.load_state);
174
+ EXPECT_TRUE(state.handle == nullptr);
175
+ EXPECT_FALSE(HasCompleteOutlineFns(state.fns));
176
+ EXPECT_EQ(1, env.dlopen_calls);
177
+ EXPECT_EQ(0, env.dlsym_calls);
178
+ EXPECT_EQ(0, env.dlclose_calls);
179
+ }
180
+
181
+ void TestMissingSymbolReturnsUnsupportedAndClosesHandle() {
182
+ FakeLoaderEnv env = {};
183
+ const PdfiumOutlineFns partial_fns = MakeCompleteOutlineFns();
184
+ InstallSymbols(&env, partial_fns);
185
+ env.symbols.erase("FPDFBookmark_GetDest");
186
+
187
+ OutlineLoaderState state = {};
188
+ const OutlineLoaderDeps deps = MakeDeps(&env);
189
+
190
+ const OutlineLoadAttemptResult result = EnsureOutlinePdfiumLoaded(&state, &deps);
191
+
192
+ EXPECT_FALSE(result.loaded);
193
+ EXPECT_TRUE(result.transitioned_to_unsupported);
194
+ EXPECT_EQ(OutlineLoadState::kUnsupported, state.load_state);
195
+ EXPECT_TRUE(state.handle == nullptr);
196
+ EXPECT_FALSE(HasCompleteOutlineFns(state.fns));
197
+ EXPECT_EQ(1, env.dlopen_calls);
198
+ EXPECT_EQ(7, env.dlsym_calls);
199
+ EXPECT_EQ(1, env.dlclose_calls);
200
+ }
201
+
202
+ void TestRepeatedCallsAfterFailureStayUnsupported() {
203
+ FakeLoaderEnv env = {};
204
+ const PdfiumOutlineFns partial_fns = MakeCompleteOutlineFns();
205
+ InstallSymbols(&env, partial_fns);
206
+ env.symbols.erase("FPDFDest_GetPageIndex");
207
+
208
+ OutlineLoaderState state = {};
209
+ const OutlineLoaderDeps deps = MakeDeps(&env);
210
+
211
+ const OutlineLoadAttemptResult first = EnsureOutlinePdfiumLoaded(&state, &deps);
212
+ EXPECT_FALSE(first.loaded);
213
+ EXPECT_TRUE(first.transitioned_to_unsupported);
214
+ EXPECT_EQ(OutlineLoadState::kUnsupported, state.load_state);
215
+ EXPECT_TRUE(state.handle == nullptr);
216
+ EXPECT_FALSE(HasCompleteOutlineFns(state.fns));
217
+ EXPECT_EQ(1, env.dlopen_calls);
218
+ EXPECT_EQ(7, env.dlsym_calls);
219
+ EXPECT_EQ(1, env.dlclose_calls);
220
+
221
+ const OutlineLoadAttemptResult second = EnsureOutlinePdfiumLoaded(&state, &deps);
222
+ EXPECT_FALSE(second.loaded);
223
+ EXPECT_FALSE(second.transitioned_to_unsupported);
224
+ EXPECT_EQ(OutlineLoadState::kUnsupported, state.load_state);
225
+ EXPECT_TRUE(state.handle == nullptr);
226
+ EXPECT_FALSE(HasCompleteOutlineFns(state.fns));
227
+ EXPECT_EQ(1, env.dlopen_calls);
228
+ EXPECT_EQ(7, env.dlsym_calls);
229
+ EXPECT_EQ(1, env.dlclose_calls);
230
+ }
231
+
232
+ } // namespace
233
+
234
+ int main() {
235
+ TestFullSymbolTableReturnsSupported();
236
+ TestMissingLibraryReturnsUnsupported();
237
+ TestMissingSymbolReturnsUnsupportedAndClosesHandle();
238
+ TestRepeatedCallsAfterFailureStayUnsupported();
239
+ return 0;
240
+ }
@@ -1,19 +1,33 @@
1
1
  package com.papyrus.engine;
2
2
 
3
3
  final class PapyrusOutline {
4
+ interface LibraryLoader {
5
+ void load() throws Throwable;
6
+ }
7
+
8
+ interface OutlineSupportProbe {
9
+ boolean isSupported() throws Throwable;
10
+ }
11
+
4
12
  static final boolean AVAILABLE;
5
13
 
6
14
  static {
7
- boolean available = false;
15
+ AVAILABLE = computeAvailability(
16
+ () -> System.loadLibrary("papyrus_text"),
17
+ PapyrusOutline::nativeIsOutlineSupported);
18
+ }
19
+
20
+ static boolean computeAvailability(LibraryLoader loader, OutlineSupportProbe probe) {
8
21
  try {
9
- System.loadLibrary("papyrus_text");
10
- available = true;
22
+ loader.load();
23
+ return probe.isSupported();
11
24
  } catch (Throwable ignored) {
12
- available = false;
25
+ return false;
13
26
  }
14
- AVAILABLE = available;
15
27
  }
16
28
 
29
+ static native boolean nativeIsOutlineSupported();
30
+
17
31
  static native PapyrusOutlineItem[] nativeGetOutline(long docPtr);
18
32
 
19
33
  static native PapyrusOutlineItem[] nativeGetOutlineFile(String filePath);
@@ -6,6 +6,7 @@ import android.graphics.Canvas;
6
6
  import android.graphics.Paint;
7
7
  import android.graphics.Rect;
8
8
  import android.util.AttributeSet;
9
+ import android.util.Log;
9
10
  import android.view.View;
10
11
 
11
12
  import com.shockwave.pdfium.PdfDocument;
@@ -14,6 +15,9 @@ import java.util.concurrent.ExecutorService;
14
15
  import java.util.concurrent.Executors;
15
16
 
16
17
  public class PapyrusPageView extends View {
18
+ static final long MAX_RENDER_PIXELS = 8L * 1024L * 1024L;
19
+ static final int MAX_RENDER_EDGE = 4096;
20
+ private static final String TAG = "PapyrusPageView";
17
21
  private static final ExecutorService RENDER_EXECUTOR = Executors.newSingleThreadExecutor();
18
22
 
19
23
  private final Paint paint = new Paint(Paint.ANTI_ALIAS_FLAG);
@@ -43,8 +47,12 @@ public class PapyrusPageView extends View {
43
47
  final int viewHeight = getHeight();
44
48
  final float clampedZoom = Math.max(0.1f, Math.min(5.0f, zoom));
45
49
  final float targetScale = Math.max(0.1f, scale) * clampedZoom;
46
- final int renderWidth = Math.max(1, (int) (viewWidth * targetScale));
47
- final int renderHeight = Math.max(1, (int) (viewHeight * targetScale));
50
+ final int[] renderSize = constrainRenderSize(
51
+ Math.max(1, (int) (viewWidth * targetScale)),
52
+ Math.max(1, (int) (viewHeight * targetScale))
53
+ );
54
+ final int renderWidth = renderSize[0];
55
+ final int renderHeight = renderSize[1];
48
56
 
49
57
  RENDER_EXECUTOR.execute(() -> {
50
58
  PdfDocument doc = state.document;
@@ -81,6 +89,40 @@ public class PapyrusPageView extends View {
81
89
  super.onDraw(canvas);
82
90
  if (bitmap == null) return;
83
91
  Rect dest = new Rect(0, 0, getWidth(), getHeight());
84
- canvas.drawBitmap(bitmap, null, dest, paint);
92
+ try {
93
+ canvas.drawBitmap(bitmap, null, dest, paint);
94
+ } catch (RuntimeException error) {
95
+ Log.w(TAG, "Failed to draw rendered page bitmap safely", error);
96
+ if (bitmap != null && !bitmap.isRecycled()) {
97
+ bitmap.recycle();
98
+ }
99
+ bitmap = null;
100
+ }
101
+ }
102
+
103
+ static int[] constrainRenderSize(int requestedWidth, int requestedHeight) {
104
+ int width = Math.max(1, requestedWidth);
105
+ int height = Math.max(1, requestedHeight);
106
+ double scale = 1.0d;
107
+
108
+ if (width > MAX_RENDER_EDGE || height > MAX_RENDER_EDGE) {
109
+ scale = Math.min(
110
+ scale,
111
+ Math.min((double) MAX_RENDER_EDGE / width, (double) MAX_RENDER_EDGE / height)
112
+ );
113
+ }
114
+
115
+ long pixels = (long) width * (long) height;
116
+ if (pixels > MAX_RENDER_PIXELS) {
117
+ scale = Math.min(scale, Math.sqrt((double) MAX_RENDER_PIXELS / (double) pixels));
118
+ }
119
+
120
+ if (scale >= 1.0d) {
121
+ return new int[] { width, height };
122
+ }
123
+
124
+ int safeWidth = Math.max(1, (int) Math.floor(width * scale));
125
+ int safeHeight = Math.max(1, (int) Math.floor(height * scale));
126
+ return new int[] { safeWidth, safeHeight };
85
127
  }
86
128
  }
@@ -0,0 +1,40 @@
1
+ package com.papyrus.engine;
2
+
3
+ import static org.junit.Assert.assertFalse;
4
+ import static org.junit.Assert.assertTrue;
5
+
6
+ import org.junit.Test;
7
+
8
+ public class PapyrusOutlineTest {
9
+ @Test
10
+ public void computeAvailabilityReturnsTrueWhenLoaderAndProbeSucceed() {
11
+ boolean available = PapyrusOutline.computeAvailability(() -> {}, () -> true);
12
+
13
+ assertTrue(available);
14
+ }
15
+
16
+ @Test
17
+ public void computeAvailabilityReturnsFalseWhenProbeReturnsFalse() {
18
+ boolean available = PapyrusOutline.computeAvailability(() -> {}, () -> false);
19
+
20
+ assertFalse(available);
21
+ }
22
+
23
+ @Test
24
+ public void computeAvailabilityReturnsFalseWhenProbeThrows() {
25
+ boolean available = PapyrusOutline.computeAvailability(() -> {}, () -> {
26
+ throw new UnsatisfiedLinkError("missing probe");
27
+ });
28
+
29
+ assertFalse(available);
30
+ }
31
+
32
+ @Test
33
+ public void computeAvailabilityReturnsFalseWhenLibraryLoadThrows() {
34
+ boolean available = PapyrusOutline.computeAvailability(() -> {
35
+ throw new UnsatisfiedLinkError("missing lib");
36
+ }, () -> true);
37
+
38
+ assertFalse(available);
39
+ }
40
+ }
@@ -0,0 +1,30 @@
1
+ package com.papyrus.engine;
2
+
3
+ import static org.junit.Assert.assertArrayEquals;
4
+ import static org.junit.Assert.assertTrue;
5
+
6
+ import org.junit.Test;
7
+
8
+ public class PapyrusPageViewTest {
9
+ @Test
10
+ public void constrainRenderSizeKeepsSafeSizesUntouched() {
11
+ int[] size = PapyrusPageView.constrainRenderSize(1200, 1600);
12
+
13
+ assertArrayEquals(new int[] {1200, 1600}, size);
14
+ }
15
+
16
+ @Test
17
+ public void constrainRenderSizeCapsOversizedBitmapsPreservingAspectRatio() {
18
+ int[] size = PapyrusPageView.constrainRenderSize(6000, 8000);
19
+
20
+ assertTrue(size[0] > 0);
21
+ assertTrue(size[1] > 0);
22
+ assertTrue(size[0] <= PapyrusPageView.MAX_RENDER_EDGE);
23
+ assertTrue(size[1] <= PapyrusPageView.MAX_RENDER_EDGE);
24
+ assertTrue(
25
+ ((long) size[0] * (long) size[1]) <= PapyrusPageView.MAX_RENDER_PIXELS
26
+ );
27
+ assertTrue(size[0] < 6000);
28
+ assertTrue(size[1] < 8000);
29
+ }
30
+ }