@skyprint/image2pdf-expo 0.1.0

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,147 @@
1
+ /* tslint:disable */
2
+ /* eslint-disable */
3
+
4
+ export function Pdf_BytesToDocument(input: string): Promise<string>;
5
+
6
+ /**
7
+ * Parses the input PDF file (as a base64 encoded string), outputs the parsed
8
+ * PDF (and any warnings) as a JSON object
9
+ *
10
+ * ```js,no_run,ignore
11
+ * let input = JSON.encode({ pdf_base64: atob(my_pdf) });
12
+ * let doc = JSON.parse(Pdf_BytesToPdfDocument(input));
13
+ * console.log(doc.pdf);
14
+ * console.log(doc.warnings);
15
+ * // {
16
+ * // status: 0,
17
+ * // data: {
18
+ * // metadata: ...,
19
+ * // resources: ...,
20
+ * // bookmarks: ...,
21
+ * // pages: [{ media_box, trim_box, crop_box, ops }]
22
+ * // }
23
+ * // }
24
+ * ```
25
+ */
26
+ export function Pdf_BytesToDocumentSync(input: string): string;
27
+
28
+ export function Pdf_DocumentToBytes(input: string): Promise<string>;
29
+
30
+ /**
31
+ * Takes a `PdfDocument` JS object and returns the base64 PDF bytes
32
+ */
33
+ export function Pdf_DocumentToBytesSync(input: string): string;
34
+
35
+ export function Pdf_HtmlToDocument(input: string): Promise<string>;
36
+
37
+ /**
38
+ * Parses the input HTML, converts it to PDF pages and outputs the generated
39
+ * PDF as a JSON object
40
+ *
41
+ * ```js,no_run,ignore
42
+ * let html = "<!doctype html><html><body><h1>Hello!</h1></body></html>";
43
+ * let input = JSON.encode({ html: html, title "My PDF!" });
44
+ * let document = JSON.parse(Pdf_HtmlToPdfDocument(input));
45
+ * console.log(document);
46
+ * // {
47
+ * // status: 0,
48
+ * // data: {
49
+ * // metadata: ...,
50
+ * // resources: ...,
51
+ * // bookmarks: ...,
52
+ * // pages: [{ media_box, trim_box, crop_box, ops }]
53
+ * // }
54
+ * // }
55
+ * ```
56
+ */
57
+ export function Pdf_HtmlToDocumentSync(input: string): string;
58
+
59
+ export function Pdf_PageToSvg(input: string): Promise<string>;
60
+
61
+ /**
62
+ * Takes a `PdfPage` JS object and outputs the SVG string for that page
63
+ */
64
+ export function Pdf_PageToSvgSync(input: string): string;
65
+
66
+ export function Pdf_ResourcesForPage(input: string): Promise<string>;
67
+
68
+ /**
69
+ * Helper function that takes a PDF page and outputs a list of all
70
+ * images IDs / fonts IDs that have to be gathered from the documents
71
+ * resources in order to render this page.
72
+ */
73
+ export function Pdf_ResourcesForPageSync(input: string): string;
74
+
75
+ /**
76
+ * Convert a list of encoded image buffers into a single PDF.
77
+ *
78
+ * # Parameters
79
+ * - `images` — a JS `Array` of `Uint8Array`, one per image. Each buffer must be
80
+ * a complete, self-contained image file (the same bytes you'd get from
81
+ * `fs.readFileSync` or `File.arrayBuffer()`).
82
+ * - `options` — a JS object matching the `ConvertOptions` shape, or `undefined`
83
+ * to use defaults (A4 page, 10 mm margin, 75 DPI).
84
+ *
85
+ * # Returns
86
+ * Raw PDF bytes as `Vec<u8>`, exposed to JS as a `Uint8Array`.
87
+ *
88
+ * # Errors
89
+ * Returns a `JsValue` carrying a readable error message on decode failure,
90
+ * unsupported format, or PDF write error.
91
+ */
92
+ export function convert(images: Array<any>, options: any): Uint8Array;
93
+
94
+ /**
95
+ * Wasm entry-point. Installs a panic hook that prints Rust panics into the
96
+ * browser console instead of failing silently with `unreachable`.
97
+ */
98
+ export function start(): void;
99
+
100
+ export type InitInput = RequestInfo | URL | Response | BufferSource | WebAssembly.Module;
101
+
102
+ export interface InitOutput {
103
+ readonly memory: WebAssembly.Memory;
104
+ readonly convert: (a: number, b: number, c: number) => void;
105
+ readonly start: () => void;
106
+ readonly Pdf_BytesToDocument: (a: number, b: number) => number;
107
+ readonly Pdf_BytesToDocumentSync: (a: number, b: number, c: number) => void;
108
+ readonly Pdf_DocumentToBytes: (a: number, b: number) => number;
109
+ readonly Pdf_DocumentToBytesSync: (a: number, b: number, c: number) => void;
110
+ readonly Pdf_HtmlToDocument: (a: number, b: number) => number;
111
+ readonly Pdf_HtmlToDocumentSync: (a: number, b: number, c: number) => void;
112
+ readonly Pdf_PageToSvg: (a: number, b: number) => number;
113
+ readonly Pdf_PageToSvgSync: (a: number, b: number, c: number) => void;
114
+ readonly Pdf_ResourcesForPage: (a: number, b: number) => number;
115
+ readonly Pdf_ResourcesForPageSync: (a: number, b: number, c: number) => void;
116
+ readonly __wasm_bindgen_func_elem_9463: (a: number, b: number, c: number, d: number) => void;
117
+ readonly __wasm_bindgen_func_elem_9473: (a: number, b: number, c: number, d: number) => void;
118
+ readonly __wbindgen_export: (a: number, b: number) => number;
119
+ readonly __wbindgen_export2: (a: number, b: number, c: number, d: number) => number;
120
+ readonly __wbindgen_export3: (a: number) => void;
121
+ readonly __wbindgen_export4: (a: number, b: number, c: number) => void;
122
+ readonly __wbindgen_export5: (a: number, b: number) => void;
123
+ readonly __wbindgen_add_to_stack_pointer: (a: number) => number;
124
+ readonly __wbindgen_start: () => void;
125
+ }
126
+
127
+ export type SyncInitInput = BufferSource | WebAssembly.Module;
128
+
129
+ /**
130
+ * Instantiates the given `module`, which can either be bytes or
131
+ * a precompiled `WebAssembly.Module`.
132
+ *
133
+ * @param {{ module: SyncInitInput }} module - Passing `SyncInitInput` directly is deprecated.
134
+ *
135
+ * @returns {InitOutput}
136
+ */
137
+ export function initSync(module: { module: SyncInitInput } | SyncInitInput): InitOutput;
138
+
139
+ /**
140
+ * If `module_or_path` is {RequestInfo} or {URL}, makes a request and
141
+ * for everything else, calls `WebAssembly.instantiate` directly.
142
+ *
143
+ * @param {{ module_or_path: InitInput | Promise<InitInput> }} module_or_path - Passing `InitInput` directly is deprecated.
144
+ *
145
+ * @returns {Promise<InitOutput>}
146
+ */
147
+ export default function __wbg_init (module_or_path?: { module_or_path: InitInput | Promise<InitInput> } | InitInput | Promise<InitInput>): Promise<InitOutput>;
Binary file
@@ -0,0 +1,24 @@
1
+ /* tslint:disable */
2
+ /* eslint-disable */
3
+ export const memory: WebAssembly.Memory;
4
+ export const convert: (a: number, b: number, c: number) => void;
5
+ export const start: () => void;
6
+ export const Pdf_BytesToDocument: (a: number, b: number) => number;
7
+ export const Pdf_BytesToDocumentSync: (a: number, b: number, c: number) => void;
8
+ export const Pdf_DocumentToBytes: (a: number, b: number) => number;
9
+ export const Pdf_DocumentToBytesSync: (a: number, b: number, c: number) => void;
10
+ export const Pdf_HtmlToDocument: (a: number, b: number) => number;
11
+ export const Pdf_HtmlToDocumentSync: (a: number, b: number, c: number) => void;
12
+ export const Pdf_PageToSvg: (a: number, b: number) => number;
13
+ export const Pdf_PageToSvgSync: (a: number, b: number, c: number) => void;
14
+ export const Pdf_ResourcesForPage: (a: number, b: number) => number;
15
+ export const Pdf_ResourcesForPageSync: (a: number, b: number, c: number) => void;
16
+ export const __wasm_bindgen_func_elem_9463: (a: number, b: number, c: number, d: number) => void;
17
+ export const __wasm_bindgen_func_elem_9473: (a: number, b: number, c: number, d: number) => void;
18
+ export const __wbindgen_export: (a: number, b: number) => number;
19
+ export const __wbindgen_export2: (a: number, b: number, c: number, d: number) => number;
20
+ export const __wbindgen_export3: (a: number) => void;
21
+ export const __wbindgen_export4: (a: number, b: number, c: number) => void;
22
+ export const __wbindgen_export5: (a: number, b: number) => void;
23
+ export const __wbindgen_add_to_stack_pointer: (a: number) => number;
24
+ export const __wbindgen_start: () => void;
package/package.json ADDED
@@ -0,0 +1,45 @@
1
+ {
2
+ "name": "@skyprint/image2pdf-expo",
3
+ "version": "0.1.0",
4
+ "description": "Expo / React Native bridge for image2pdf — converts JPG/PNG/HEIC images to a single A4 PDF entirely on-device.",
5
+ "license": "MIT",
6
+ "repository": {
7
+ "type": "git",
8
+ "url": "git@git.duguying.net:cloudprinter/image2pdf.git",
9
+ "directory": "integrations/expo"
10
+ },
11
+ "main": "src/index.ts",
12
+ "react-native": "src/index.ts",
13
+ "types": "src/index.ts",
14
+ "files": [
15
+ "src",
16
+ "assets",
17
+ "README.md"
18
+ ],
19
+ "keywords": [
20
+ "expo",
21
+ "react-native",
22
+ "pdf",
23
+ "image",
24
+ "heic",
25
+ "jpg",
26
+ "png",
27
+ "wasm"
28
+ ],
29
+ "scripts": {
30
+ "build": "node scripts/copy-wasm.mjs",
31
+ "typecheck": "tsc --noEmit"
32
+ },
33
+ "peerDependencies": {
34
+ "expo": ">=55.0.0",
35
+ "expo-asset": ">=55.0.0",
36
+ "expo-file-system": ">=55.0.0",
37
+ "react": ">=19.0.0",
38
+ "react-native": ">=0.83.0",
39
+ "react-native-webview": ">=13.16.0"
40
+ },
41
+ "devDependencies": {
42
+ "@types/react": "~19.2.10",
43
+ "typescript": "~5.9.2"
44
+ }
45
+ }
package/src/Bridge.tsx ADDED
@@ -0,0 +1,169 @@
1
+ /**
2
+ * <Image2PdfBridge /> — hidden WebView that runs the image2pdf wasm.
3
+ *
4
+ * The component renders as a 0×0 absolutely-positioned view; it does not
5
+ * take screen real estate. Consumers drive it via the imperative ref
6
+ * (returned by `useImage2Pdf`) rather than props, because the conversion
7
+ * flow is a multi-step request/response conversation, not a single
8
+ * declarative render.
9
+ *
10
+ * Why a hidden WebView and not direct WASM loading? React Native's Hermes
11
+ * engine (used in Expo Go) does not implement `WebAssembly`. The WebView
12
+ * runs WebKit/WKWebView (iOS) or Chrome/Blink (Android) which both do.
13
+ * Stashing the WASM inside a hidden WebView gives us access to it from
14
+ * the JS side via `postMessage` — the only bridge available without
15
+ * leaving Expo Go.
16
+ */
17
+
18
+ import React, { forwardRef, useCallback, useEffect, useImperativeHandle, useRef, useState } from 'react';
19
+ import { WebView, type WebViewMessageEvent } from 'react-native-webview';
20
+ import type { WebView as WebViewType } from 'react-native-webview';
21
+ import * as FileSystem from 'expo-file-system/legacy';
22
+
23
+ import { debugLog, debugWarn } from './debugLog';
24
+ import { loadBridgeBundle } from './loadBridgeBundle';
25
+ import type { RnToWv, WvToRn } from './protocol';
26
+
27
+ import { B64_CHUNK_SIZE } from './readUriBase64';
28
+
29
+ export interface Image2PdfBridgeHandle {
30
+ /** Send a message into the WebView. */
31
+ post(message: RnToWv): void;
32
+ /** True once the bundle HTML URI is ready (not wasm-ready — wait for `ready` message). */
33
+ isReady(): boolean;
34
+ }
35
+
36
+ interface Props {
37
+ /** Optional callback for `ready` / `addOk` / `addErr` / `done` / `error`. */
38
+ onMessage?(msg: WvToRn): void;
39
+ }
40
+
41
+ export const Image2PdfBridge = forwardRef<Image2PdfBridgeHandle, Props>(function Image2PdfBridge(
42
+ { onMessage },
43
+ ref,
44
+ ) {
45
+ const webRef = useRef<WebViewType>(null);
46
+ const wasmUriRef = useRef<string | null>(null);
47
+ const [source, setSource] = useState<{ uri: string; allowingReadAccessToURL: string } | null>(
48
+ null,
49
+ );
50
+ const [loadError, setLoadError] = useState<string | null>(null);
51
+
52
+ const deliverWasmBytes = useCallback(async () => {
53
+ const wasmUri = wasmUriRef.current;
54
+ const web = webRef.current;
55
+ if (!wasmUri || !web) {
56
+ debugWarn('needWasm but bridge not ready');
57
+ web?.postMessage(JSON.stringify({ type: 'wasmErr', message: 'bridge not ready' }));
58
+ return;
59
+ }
60
+
61
+ try {
62
+ debugLog('reading wasm for WebView', wasmUri);
63
+ const t0 = Date.now();
64
+ const b64 = await FileSystem.readAsStringAsync(wasmUri, {
65
+ encoding: FileSystem.EncodingType.Base64,
66
+ });
67
+ const total = Math.ceil(b64.length / B64_CHUNK_SIZE);
68
+ for (let i = 0; i < total; i++) {
69
+ const chunk: RnToWv = {
70
+ type: 'wasmChunk',
71
+ index: i,
72
+ total,
73
+ data: b64.slice(i * B64_CHUNK_SIZE, (i + 1) * B64_CHUNK_SIZE),
74
+ };
75
+ web.postMessage(JSON.stringify(chunk));
76
+ }
77
+ debugLog('wasm delivered to WebView', { chunks: total, b64Len: b64.length, ms: Date.now() - t0 });
78
+ } catch (e) {
79
+ const message = e instanceof Error ? e.message : String(e);
80
+ debugWarn('wasm delivery failed:', message);
81
+ web.postMessage(JSON.stringify({ type: 'wasmErr', message }));
82
+ }
83
+ }, []);
84
+
85
+ useEffect(() => {
86
+ let cancelled = false;
87
+ (async () => {
88
+ try {
89
+ const { htmlUri, readAccessUri, wasmUri } = await loadBridgeBundle();
90
+ if (!cancelled) {
91
+ wasmUriRef.current = wasmUri;
92
+ debugLog('bridge bundle ready', htmlUri, { readAccessUri, wasmUri });
93
+ setSource({ uri: htmlUri, allowingReadAccessToURL: readAccessUri });
94
+ }
95
+ } catch (e) {
96
+ const msg = e instanceof Error ? e.message : String(e);
97
+ debugWarn('bridge bundle load failed:', msg);
98
+ if (!cancelled) setLoadError(msg);
99
+ }
100
+ })();
101
+ return () => {
102
+ cancelled = true;
103
+ };
104
+ }, []);
105
+
106
+ useImperativeHandle(
107
+ ref,
108
+ () => ({
109
+ post(message) {
110
+ if (!webRef.current) {
111
+ debugWarn('Bridge.post before WebView mounted', message.type);
112
+ return;
113
+ }
114
+ debugLog('Bridge.post', message.type, message);
115
+ webRef.current.postMessage(JSON.stringify(message));
116
+ },
117
+ isReady() {
118
+ return source !== null;
119
+ },
120
+ }),
121
+ [source],
122
+ );
123
+
124
+ const handleMessage = (e: WebViewMessageEvent) => {
125
+ let parsed: WvToRn;
126
+ try {
127
+ parsed = JSON.parse(e.nativeEvent.data);
128
+ } catch {
129
+ if (__DEV__) console.warn('[image2pdf] ignoring non-JSON message:', e.nativeEvent.data);
130
+ return;
131
+ }
132
+ debugLog('Bridge.onMessage', parsed.type, parsed);
133
+ if (parsed.type === 'needWasm') {
134
+ void deliverWasmBytes();
135
+ return;
136
+ }
137
+ onMessage?.(parsed);
138
+ };
139
+
140
+ if (loadError) {
141
+ debugWarn('bridge not rendered:', loadError);
142
+ return null;
143
+ }
144
+
145
+ if (!source) {
146
+ return null;
147
+ }
148
+
149
+ return (
150
+ <WebView
151
+ ref={webRef}
152
+ originWhitelist={['*']}
153
+ source={source}
154
+ allowingReadAccessToURL={source.allowingReadAccessToURL}
155
+ style={{ width: 0, height: 0, position: 'absolute', top: -1, left: -1, opacity: 0 }}
156
+ allowFileAccess
157
+ allowFileAccessFromFileURLs
158
+ allowUniversalAccessFromFileURLs
159
+ mixedContentMode="always"
160
+ javaScriptEnabled
161
+ domStorageEnabled
162
+ onMessage={handleMessage}
163
+ onError={(e) => debugWarn('WebView error', e.nativeEvent)}
164
+ onHttpError={(e) => debugWarn('WebView HTTP error', e.nativeEvent)}
165
+ onLoadEnd={() => debugLog('WebView onLoadEnd', source.uri)}
166
+ containerStyle={{ backgroundColor: 'transparent' }}
167
+ />
168
+ );
169
+ });
@@ -0,0 +1,9 @@
1
+ declare module '*.bundle' {
2
+ const assetId: number;
3
+ export default assetId;
4
+ }
5
+
6
+ declare module '*.wasm' {
7
+ const assetId: number;
8
+ export default assetId;
9
+ }
@@ -0,0 +1,12 @@
1
+ /** Dev-only logging for the RN ↔ WebView bridge. Prefix grep: `[image2pdf]`. */
2
+ export function debugLog(...args: unknown[]): void {
3
+ if (typeof __DEV__ !== 'undefined' && __DEV__) {
4
+ console.log('[image2pdf]', ...args);
5
+ }
6
+ }
7
+
8
+ export function debugWarn(...args: unknown[]): void {
9
+ if (typeof __DEV__ !== 'undefined' && __DEV__) {
10
+ console.warn('[image2pdf]', ...args);
11
+ }
12
+ }
package/src/index.ts ADDED
@@ -0,0 +1,20 @@
1
+ /**
2
+ * @skyprint/image2pdf-expo — public API.
3
+ *
4
+ * Mount `<result.Bridge />` once near the top of your React tree, then call
5
+ * `result.convert(uris, options?)` to produce a PDF.
6
+ *
7
+ * const { convert, Bridge, status, error, result } = useImage2Pdf();
8
+ * return (
9
+ * <>
10
+ * <Bridge />
11
+ * <Button title="Convert" onPress={() => convert(pickedUris)} />
12
+ * </>
13
+ * );
14
+ */
15
+
16
+ export { useImage2Pdf } from './useImage2Pdf';
17
+ export { Image2PdfBridge } from './Bridge';
18
+ export type { Image2PdfBridgeHandle } from './Bridge';
19
+ export type { ConvertOptions, ConvertResult } from './types';
20
+ export type { RnToWv, WvToRn } from './protocol';
@@ -0,0 +1,79 @@
1
+ /**
2
+ * Stage bridge.html + JS + wasm into one cache directory so relative
3
+ * `import './image2pdf.js'` and wasm `import.meta.url` resolution work inside
4
+ * the hidden WebView. Expo only bundles assets that are `require()`'d from RN;
5
+ * a lone `bridge.html` asset does NOT pull siblings along automatically.
6
+ */
7
+
8
+ import { Asset } from 'expo-asset';
9
+ import * as FileSystem from 'expo-file-system/legacy';
10
+
11
+ import { debugLog, debugWarn } from './debugLog';
12
+
13
+ /** Bump when bridge/wasm assets change to invalidate the on-device cache. */
14
+ const BUNDLE_VERSION = '6';
15
+
16
+ const BUNDLE_DIR = `${FileSystem.cacheDirectory}image2pdf-bridge/`;
17
+
18
+ /** Metro asset modules — `.bundle` is in `assetExts`, not parsed as RN source. */
19
+ const BUNDLE_FILES: Record<string, number> = {
20
+ 'bridge.html': require('../assets/bridge.html'),
21
+ 'bridge.js': require('../assets/bridge.bundle'),
22
+ 'image2pdf.js': require('../assets/image2pdf.bundle'),
23
+ 'image2pdf_bg.wasm': require('../assets/image2pdf_bg.wasm'),
24
+ };
25
+
26
+ export interface BridgeBundleSource {
27
+ htmlUri: string;
28
+ /** iOS WKWebView: sibling `file://` loads need read access to this directory. */
29
+ readAccessUri: string;
30
+ /** Staged wasm on disk — RN reads this and posts bytes into the WebView. */
31
+ wasmUri: string;
32
+ }
33
+
34
+ /**
35
+ * Returns `file://` URIs for bridge.html and its directory (for iOS read access).
36
+ */
37
+ export async function loadBridgeBundle(): Promise<BridgeBundleSource> {
38
+ const stampPath = `${BUNDLE_DIR}.version`;
39
+ const stampInfo = await FileSystem.getInfoAsync(stampPath);
40
+ let cachedVersion: string | null = null;
41
+ if (stampInfo.exists) {
42
+ try {
43
+ cachedVersion = await FileSystem.readAsStringAsync(stampPath);
44
+ } catch {
45
+ cachedVersion = null;
46
+ }
47
+ }
48
+
49
+ if (cachedVersion !== BUNDLE_VERSION) {
50
+ debugLog('staging bridge bundle', { dir: BUNDLE_DIR, version: BUNDLE_VERSION });
51
+ await FileSystem.makeDirectoryAsync(BUNDLE_DIR, { intermediates: true });
52
+
53
+ for (const [destName, moduleId] of Object.entries(BUNDLE_FILES)) {
54
+ const asset = Asset.fromModule(moduleId);
55
+ await asset.downloadAsync();
56
+ if (!asset.localUri) {
57
+ throw new Error(`image2pdf bridge asset missing localUri: ${destName}`);
58
+ }
59
+ const dest = `${BUNDLE_DIR}${destName}`;
60
+ await FileSystem.copyAsync({ from: asset.localUri, to: dest });
61
+ debugLog('staged', destName, dest);
62
+ }
63
+
64
+ await FileSystem.writeAsStringAsync(stampPath, BUNDLE_VERSION);
65
+ }
66
+
67
+ const htmlUri = `${BUNDLE_DIR}bridge.html`;
68
+ const htmlInfo = await FileSystem.getInfoAsync(htmlUri);
69
+ if (!htmlInfo.exists) {
70
+ throw new Error(`bridge.html not found after staging: ${htmlUri}`);
71
+ }
72
+ return { htmlUri, readAccessUri: BUNDLE_DIR, wasmUri: `${BUNDLE_DIR}image2pdf_bg.wasm` };
73
+ }
74
+
75
+ /** @deprecated Use `loadBridgeBundle()` */
76
+ export async function loadBridgeBundleUri(): Promise<string> {
77
+ const { htmlUri } = await loadBridgeBundle();
78
+ return htmlUri;
79
+ }
@@ -0,0 +1,43 @@
1
+ /**
2
+ * postMessage protocol between the React Native side and the WebView running
3
+ * the image2pdf wasm.
4
+ *
5
+ * Wire format: each message is a single JSON object with a `type` field.
6
+ * Binary crosses the postMessage bridge as chunked base64 (RN→WV for images
7
+ * and wasm; WV→RN for the PDF). iOS WKWebView cannot `fetch()` local `file://`
8
+ * URIs, so RN reads files with expo-file-system and streams chunks.
9
+ */
10
+
11
+ import type { ConvertOptions } from './types';
12
+
13
+ export type RnToWv =
14
+ | { type: 'init'; options?: ConvertOptions }
15
+ /** @deprecated WebView cannot fetch local URIs on iOS — use `addBytes`. */
16
+ | { type: 'add'; id: string; uri: string }
17
+ /** Chunk of base64 image bytes for one `add` id. */
18
+ | { type: 'addBytes'; id: string; index: number; total: number; data: string }
19
+ /** Compose the PDF from all currently-loaded images. */
20
+ | { type: 'finalize'; requestId: string; options?: ConvertOptions }
21
+ /** Drop all loaded images (frees memory). */
22
+ | { type: 'reset' }
23
+ /** Chunk of base64 wasm bytes (RN cannot `fetch` file:// wasm on iOS). */
24
+ | { type: 'wasmChunk'; index: number; total: number; data: string }
25
+ /** RN failed to read staged wasm. */
26
+ | { type: 'wasmErr'; message: string };
27
+
28
+ export type WvToRn =
29
+ | { type: 'needWasm' }
30
+ | { type: 'ready' }
31
+ | { type: 'addOk'; id: string }
32
+ | { type: 'addErr'; id: string; message: string }
33
+ | { type: 'done'; requestId: string; pdfBase64: string; pages: number; bytes: number }
34
+ | { type: 'error'; requestId: string; message: string }
35
+ /** Dev trace from bridge.js — forwarded to Metro console. */
36
+ | { type: 'log'; message: string };
37
+
38
+ /** Map a (port, type) → handler structure. Internal — not exported from the package. */
39
+ export interface ProtocolContext {
40
+ post(message: RnToWv): void;
41
+ onMessage(handler: (msg: WvToRn) => void): () => void;
42
+ onReady(handler: () => void): () => void;
43
+ }
@@ -0,0 +1,39 @@
1
+ /**
2
+ * Read a local image URI as base64 on the RN side.
3
+ * iOS WKWebView cannot `fetch()` picker `file://` URIs — bytes must cross via postMessage.
4
+ */
5
+
6
+ import * as FileSystem from 'expo-file-system/legacy';
7
+
8
+ /** Max base64 chars per postMessage chunk (iOS WebView limit). */
9
+ export const B64_CHUNK_SIZE = 480_000;
10
+
11
+ export async function readUriAsBase64(uri: string): Promise<string> {
12
+ try {
13
+ const info = await FileSystem.getInfoAsync(uri);
14
+ if (info.exists) {
15
+ return await FileSystem.readAsStringAsync(uri, {
16
+ encoding: FileSystem.EncodingType.Base64,
17
+ });
18
+ }
19
+ } catch {
20
+ // picker URIs are sometimes readable only via fetch
21
+ }
22
+
23
+ const res = await fetch(uri);
24
+ if (!res.ok) throw new Error(`read image failed: ${res.status}`);
25
+ return arrayBufferToBase64(await res.arrayBuffer());
26
+ }
27
+
28
+ function arrayBufferToBase64(buffer: ArrayBuffer): string {
29
+ const bytes = new Uint8Array(buffer);
30
+ let binary = '';
31
+ const chunk = 0x8000;
32
+ for (let i = 0; i < bytes.length; i += chunk) {
33
+ binary += String.fromCharCode.apply(
34
+ null,
35
+ Array.from(bytes.subarray(i, Math.min(i + chunk, bytes.length))),
36
+ );
37
+ }
38
+ return btoa(binary);
39
+ }
package/src/types.ts ADDED
@@ -0,0 +1,33 @@
1
+ /**
2
+ * Public options type for the Expo integration. Mirrors the Rust `ConvertOptions`
3
+ * shape, in camelCase so JS callers can write `{ marginMm: 5, dpi: 150 }`.
4
+ *
5
+ * This file is kept separate from `index.ts` so library consumers can import
6
+ * just the type without pulling in the React Native runtime.
7
+ */
8
+
9
+ export interface ConvertOptions {
10
+ /** PDF document title. Defaults to "image2pdf". */
11
+ title?: string;
12
+ /** PDF document author metadata. */
13
+ author?: string;
14
+ /** Page margin in millimeters. Defaults to 10. */
15
+ marginMm?: number;
16
+ /**
17
+ * Assumed DPI for image-to-points conversion. Lower DPI = larger image on
18
+ * the page. Defaults to 75 (PDF default).
19
+ */
20
+ dpi?: number;
21
+ }
22
+
23
+ /** Returned by `useImage2Pdf().convert()`. */
24
+ export interface ConvertResult {
25
+ /** `file://` URI of the saved PDF (in the cache directory). */
26
+ uri: string;
27
+ /** Size in bytes. */
28
+ bytes: number;
29
+ /** Number of pages in the generated PDF. */
30
+ pages: number;
31
+ /** Wall-clock milliseconds spent in `convert()` (WASM call only). */
32
+ durationMs: number;
33
+ }