Skip to content

Commit 5fd381f

Browse files
authored
Merge pull request #690 from OskarEichler/codex/viewshot-capture-documentation
docs: correct capture examples and RAW conversion semantics
2 parents 647f80c + 1ed85ec commit 5fd381f

4 files changed

Lines changed: 123 additions & 269 deletions

File tree

README.md

Lines changed: 78 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -36,29 +36,35 @@ npx pod-install
3636
## High Level API
3737

3838
```js
39+
import React, {useCallback, useEffect, useRef} from "react";
40+
import {Image, ScrollView, Text} from "react-native";
3941
import ViewShot from "react-native-view-shot";
4042

41-
function ExampleCaptureOnMountManually {
43+
function ExampleCaptureOnMountManually() {
4244
const ref = useRef();
4345

4446
useEffect(() => {
4547
// on mount
46-
ref.current.capture().then(uri => {
47-
console.log("do something with ", uri);
48-
});
48+
ref.current
49+
.capture()
50+
.then(uri => {
51+
console.log("do something with ", uri);
52+
})
53+
.catch(console.error);
4954
}, []);
5055

5156
return (
52-
<ViewShot ref={ref} options={{ fileName: "Your-File-Name", format: "jpg", quality: 0.9 }}>
57+
<ViewShot
58+
ref={ref}
59+
options={{fileName: "Your-File-Name", format: "jpg", quality: 0.9}}
60+
>
5361
<Text>...Something to rasterize...</Text>
5462
</ViewShot>
5563
);
5664
}
5765

5866
// alternative
59-
function ExampleCaptureOnMountSimpler {
60-
const ref = useRef();
61-
67+
function ExampleCaptureOnMountSimpler() {
6268
const onCapture = useCallback(uri => {
6369
console.log("do something with ", uri);
6470
}, []);
@@ -72,28 +78,29 @@ function ExampleCaptureOnMountSimpler {
7278

7379
// waiting an image
7480

75-
function ExampleWaitingCapture {
81+
function ExampleWaitingCapture({imageSource}) {
7682
const ref = useRef();
7783

7884
const onImageLoad = useCallback(() => {
79-
ref.current.capture().then(uri => {
80-
console.log("do something with ", uri);
81-
})
85+
ref.current
86+
.capture()
87+
.then(uri => {
88+
console.log("do something with ", uri);
89+
})
90+
.catch(console.error);
8291
}, []);
8392

8493
return (
8594
<ViewShot ref={ref}>
8695
<Text>...Something to rasterize...</Text>
87-
<Image ... onLoad={onImageLoad} />
96+
<Image source={imageSource} onLoad={onImageLoad} />
8897
</ViewShot>
8998
);
9099
}
91100

92101
// capture ScrollView content
93102
// NB: you may need to go the "imperative way" to use snapshotContentContainer with the scrollview ref instead
94-
function ExampleCaptureOnMountSimpler {
95-
const ref = useRef();
96-
103+
function ExampleCaptureScrollContent() {
97104
const onCapture = useCallback(uri => {
98105
console.log("do something with ", uri);
99106
}, []);
@@ -114,7 +121,7 @@ function ExampleCaptureOnMountSimpler {
114121
- **`options`**: the same options as in `captureRef` method.
115122
- **`captureMode`** (string):
116123
- if not defined (default). the capture is not automatic and you need to use the ref and call `capture()` yourself.
117-
- `"mount"`. Capture the view once at mount. (It is important to understand image loading won't be waited, in such case you want to use `"none"` with `viewShotRef.capture()` after `Image#onLoad`.)
124+
- `"mount"`. Capture the view once at mount. Image loading is not awaited; omit `captureMode` and call `viewShotRef.current.capture()` after `Image#onLoad` when needed.
118125
- `"continuous"` EXPERIMENTAL, this will capture A LOT of images continuously. For very specific use-cases.
119126
- `"update"` EXPERIMENTAL, this will capture images each time React redraw (on did update). For very specific use-cases.
120127
- **`onCapture`**: when a `captureMode` is defined, this callback will be called with the capture result.
@@ -140,7 +147,7 @@ Returns a Promise of the image URI.
140147
- **`options`** may include:
141148
- **`fileName`** _(string)_: (Android only) the file name of the file. Must be at least 3 characters long.
142149
- **`width`** / **`height`** _(number)_: the width and height of the final image (resized from the View bound. don't provide it if you want the original pixel size).
143-
- **`format`** _(string)_: either `png` or `jpg` or `webm` (Android). Defaults to `png`.
150+
- **`format`** _(string)_: `png` or `jpg`, plus `webp` and `raw` on Android. Defaults to `png`. `webm` is a deprecated alias for `webp`; it produces WebP image data, not a video.
144151
- **`quality`** _(number)_: the quality. 0.0 - 1.0 (default). (only available on lossy formats like jpg)
145152
- **`result`** _(string)_, the method you want to use to save the snapshot, one of:
146153
- `"tmpfile"` (default): save to a temporary file _(that will only exist for as long as the app is running)_.
@@ -232,11 +239,11 @@ Introduced a new image format RAW. it correspond a ARGB array of pixels.
232239

233240
Advantages:
234241

235-
- no compression, so its supper quick. Screenshot taking is less than 16ms;
242+
- avoids PNG/JPEG compression; capture time still depends on the view and device.
236243

237244
RAW format supported for `zip-base64`, `base64` and `tmpfile` result types.
238245

239-
RAW file on disk saved in format: `${width}:${height}|${base64}` string.
246+
RAW files contain an ASCII `${width}:${height}|` header followed by binary pixel bytes. The `base64` result instead returns that header followed by base64-encoded pixels; `zip-base64` returns the header followed by base64-encoded, zlib-compressed pixels.
240247

241248
### zip-base64
242249

@@ -246,45 +253,60 @@ approach for capturing screen views and deliver them to the react side.
246253

247254
### How to work with zip-base64 and RAW format?
248255

256+
Capture in React Native, then send the result and format to a Node.js process for conversion. Node's `fs` and `zlib` modules are not built into React Native.
257+
249258
```js
250-
const fs = require("fs");
251-
const zlib = require("zlib");
252-
const PNG = require("pngjs").PNG;
253-
const Buffer = require("buffer").Buffer;
259+
import {Platform} from "react-native";
260+
import {captureRef} from "react-native-view-shot";
254261

255262
const format = Platform.OS === "android" ? "raw" : "png";
256263
const result = Platform.OS === "android" ? "zip-base64" : "base64";
257264

258-
captureRef(this.ref, {result, format}).then(data => {
259-
// expected pattern 'width:height|', example: '1080:1731|'
260-
const resolution = /^(\d+):(\d+)\|/g.exec(data);
261-
const width = (resolution || ["", 0, 0])[1];
262-
const height = (resolution || ["", 0, 0])[2];
263-
const base64 = data.substr((resolution || [""])[0].length || 0);
264-
265-
// convert from base64 to Buffer
266-
const buffer = Buffer.from(base64, "base64");
267-
// un-compress data
268-
const inflated = zlib.inflateSync(buffer);
269-
// compose PNG
270-
const png = new PNG({width, height});
271-
png.data = inflated;
272-
const pngData = PNG.sync.write(png);
273-
// save composed PNG
274-
fs.writeFileSync(output, pngData);
275-
});
265+
captureRef(viewRef, {result, format})
266+
.then(data => {
267+
// Send {data, format} to your Node.js conversion process.
268+
})
269+
.catch(console.error);
270+
```
271+
272+
In Node.js:
273+
274+
```js
275+
const fs = require("node:fs");
276+
const zlib = require("node:zlib");
277+
const {PNG} = require("pngjs");
278+
279+
function saveCapture(data, format, outputPath) {
280+
let pngData;
281+
if (format === "raw") {
282+
const resolution = /^(\d+):(\d+)\|/.exec(data);
283+
if (!resolution) throw new Error("Missing RAW dimensions");
284+
const width = Number(resolution[1]);
285+
const height = Number(resolution[2]);
286+
const compressed = Buffer.from(data.slice(resolution[0].length), "base64");
287+
const pixels = zlib.inflateSync(compressed);
288+
if (!width || !height || pixels.length !== width * height * 4) {
289+
throw new Error("Invalid RAW dimensions or pixel data");
290+
}
291+
const png = new PNG({width, height});
292+
png.data = pixels;
293+
pngData = PNG.sync.write(png);
294+
} else {
295+
// The iOS/Windows fallback is already PNG, not zlib-compressed RAW.
296+
pngData = Buffer.from(data, "base64");
297+
}
298+
fs.writeFileSync(outputPath, pngData);
299+
}
276300
```
277301

278302
Keep in mind that packaging PNG data is a CPU consuming operation as a `zlib.inflate`.
279303

280304
Hint: use `process.fork()` approach for converting raw data into PNGs.
281305

282-
> Note: code is tested in large commercial project.
283-
284-
> Note #2: Don't forget to add packages into your project:
306+
> Install the PNG encoder in your Node.js conversion project (`zlib` is built in):
285307
>
286308
> ```bash
287-
> npm install pngjs zlib
309+
> npm install pngjs
288310
> ```
289311
290312
## Troubleshooting / FAQ
@@ -328,20 +350,23 @@ This is because the snapshot image result is in real pixel size where the width/
328350
A prop may be necessary to properly capture GL Surface View in the view tree:
329351
330352
```js
331-
/**
332-
* if true and when view is a SurfaceView or have it in the view tree, view will be captured.
333-
* False by default, because it can have signoficant performance impact
334-
*/
335-
handleGLSurfaceViewOnAndroid?: boolean;
353+
// Opt in to SurfaceView capture; it can have a significant performance cost.
354+
captureRef(viewRef, {handleGLSurfaceViewOnAndroid: true});
336355
```
337356
338357
### Trying to share the capture result with `expo-sharing`?
339358
340-
`tmpfile` or the default capture result works best for this. Just be sure to prepend `file://` to result before you call `shareAsync`.
359+
`tmpfile` or the default capture result works best for this. Add `file://` only when it is absent; Android already returns a file URI.
341360
342361
```js
343362
captureRef(viewRef)
344-
.then((uri) => Sharing.shareAsync(`file://${uri}`, options)
363+
.then(uri =>
364+
Sharing.shareAsync(
365+
uri.startsWith("file://") ? uri : `file://${uri}`,
366+
options,
367+
),
368+
)
369+
.catch(console.error);
345370
```
346371
347372
---

example-web/README.md

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -96,19 +96,17 @@ See `.github/workflows/ci.yml` - the `test-web-example` job runs all Playwright
9696

9797
**CI Artifacts:**
9898

99-
- `web-snapshots-reference`: Current reference snapshots (download to update local snapshots)
100-
- `web-snapshots-diff`: Visual diffs when tests fail (actual vs expected)
99+
- `web-snapshots-actual`: Actual, expected and diff images from failed comparisons
101100
- `playwright-report`: Full HTML test report
102101
- `playwright-test-results`: Detailed test results
103102

104103
**When snapshots differ:**
105104

106105
- The CI will show a clear message in the GitHub Actions summary
107-
- Download the `web-snapshots-reference` artifact
108-
- Replace `example-web/e2e/snapshots/reference/` with the downloaded files
109-
- Commit the updated snapshots
106+
- Run `./scripts/update-snapshots-from-ci.sh <RUN_ID>` from `example-web/` to import only the actual images from `web-snapshots-actual`
107+
- Review every changed reference image and commit only intended visual changes
110108

111-
**Note:** Snapshots are platform-specific (Linux vs macOS). The CI runs on Linux, so you may need to update snapshots when running locally on macOS.
109+
**Note:** Committed reference images are generated on Linux. Local non-Linux runs use separate platform-suffixed images; do not replace the Linux references with macOS or Windows output. See [snapshot guidance](e2e/snapshots/README.md).
112110

113111
## Related
114112

0 commit comments

Comments
 (0)