Repository navigation
Processing and mutations
Task: change an image (crop, rotate, flip, blur, adjust colors, convert the format) and know exactly which calls change the image you hold and which give you a new one.
Conditions: YuvFfi.initialize() has completed in the isolate that runs the calls; see
Platforms and troubleshooting. On native
platforms constructors, plane access, copy, applyPlanes, applyPatch and the byte getters work without it; every
other processing call throws UnsupportedError until it has finished. On Web every YuvImage operation needs it.
| Family | Calls | What happens |
|---|---|---|
In place (apply*) |
applyRgbaBytes, applyGrayscale, applyBlackWhite, applyNegate, applyGaussianBlur, applyMeanBlur, applyBoxBlur, applyCrop, applyFlipHorizontal, applyFlipVertical, applyRotation, applyFormat, applyChromaSwap, applyPlanes, applyPatch
|
The receiver changes and the same object is returned, so calls chain with ... |
| New image |
copy, cropped, rotated, toI420, toNv12, toBgra, YuvImage.decode, YuvImage.fromRgbaBytes
|
The receiver is never changed. The result is independent; it never shares bytes with the source, even for a same-format toBgra() or rotated(YuvImageRotation.rotation0), which return copies. |
| Bytes only |
toBytes, toBgraBytes, toImage
|
Return all plane storage including padding (toBytes), tight width * height * 4 BGRA pixels (toBgraBytes) or a Flutter ui.Image (toImage); the image is not changed. |
A successful apply* call advances image.revision exactly once. A rejected call changes nothing: not the bytes, the
format, the size or the revision. A call that genuinely changes nothing (for example applyRotation(rotation0), a crop
that selects no pixels, a blur with radius 0, or applyFormat to the format the image already has) leaves the revision
alone as well.
revision is how widgets know an image changed. YuvImageProvider (used by YuvImageWidget) builds its cache key from
the image identity and its revision. Two consequences:
- Mutate through the API and the revision moves by itself.
- Write into
plane.bytes(or callplane.setPixelorplane.assignFrom) and the image cannot notice; callimage.markDirty()afterwards so the next widget build decodes the new content.
This program applies one of each kind of call and prints what happened to the original.
import 'dart:ui';
import 'package:flutter/foundation.dart';
import 'package:yuv_ffi/yuv_ffi.dart';
Future<void> main() async {
await YuvFfi.initialize();
final original = YuvImage.allocate(YuvPixelFormat.i420, 320, 180);
final start = original.revision;
final rotated = original.rotated(YuvImageRotation.rotation90); // a new image
debugPrint('rotated ${rotated.width}x${rotated.height}; original ${original.width}x${original.height}, revision +${original.revision - start}');
final blurred = original.copy().applyGaussianBlur(radius: 2, sigma: 2); // changes only the copy
debugPrint('blurred copy revision ${blurred.revision}; original revision +${original.revision - start}');
final same = original.applyCrop(const Rect.fromLTWH(10, 10, 100, 50)); // changes the original
debugPrint('cropped: ${original.width}x${original.height}, same object ${identical(same, original)}, revision +${original.revision - start}');
original.applyCrop(Rect.zero); // selects no pixels
debugPrint('empty crop: revision +${original.revision - start}');
}Expected output:
rotated 180x320; original 320x180, revision +0
blurred copy revision 1; original revision +0
cropped: 100x50, same object true, revision +1
empty crop: revision +1
Use copy() whenever more than one place holds the same image. Frames that a camera or another producer hands to you
usually must not be changed; see the yuv_camera wiki.
applyCrop(region) and cropped(region) take a Rect in image pixels. Its left and top edges are rounded down, its
right and bottom edges are rounded up, and the result is clamped to the image. A region that selects no pixels is a
no-op for applyCrop, and cropped returns an independent copy of the whole image in that case. In I420 and NV12
an odd crop size or origin recomputes chroma from 2 by 2 pixel blocks, so the colors of the result can differ from the
source pixels.
applyRotation(YuvImageRotation.rotation90) and rotated(...) rotate clockwise by 0, 90, 180 or 270 degrees. A
rotation by 90 or 270 swaps width and height. applyFlipHorizontal and applyFlipVertical mirror in place and keep the
size.
| Call | Parameters |
|---|---|
applyGaussianBlur(radius:, sigma:) |
sigma must be finite and positive when radius is not 0. |
applyMeanBlur(radius:, region:) |
region (optional) is in image pixels, rounded outward and clamped; null blurs the whole image. |
applyBoxBlur(radius:, region:) |
Same arguments as the mean blur, with a normalized box kernel. |
A radius of 0 is a no-op; a negative radius or one above 256 throws ArgumentError. A region that selects no pixels
blurs nothing; it does not fall back to the whole image.
applyGrayscale, applyBlackWhite (a black/white threshold effect) and applyNegate change the colors in place in any
of the three formats.
| Call | Result |
|---|---|
applyFormat(format) |
Converts this image to format in place. Converting to the current format is a no-op. |
toI420(), toNv12(), toBgra()
|
A new image in that format. If the image already has it, you get an independent copy. |
applyChromaSwap() |
Swaps the U and V sample values of an NV12 image and keeps it labeled NV12. Throws UnsupportedError for I420 and BGRA, without changing the image. |
Conversions between YUV and BGRA use the BT.601 matrix in limited (video) range; the package does not detect full-range data or another color matrix and has no option to select one.
All nine format pairs (I420, NV12 and BGRA, each to each) are supported on native and Web in the cases the Web parity evidence records.
applyPatch(fragment, x:, y:) copies an opaque image of the same format into this one without scaling. It never touches
the destination's padding bytes. On native platforms it works without initialize() because it is plain Dart, so
the program below does not call it; on Web await YuvFfi.initialize() first, as for every YuvImage operation.
import 'package:flutter/foundation.dart';
import 'package:yuv_ffi/yuv_ffi.dart';
void main() {
final destination = YuvImage.allocate(YuvPixelFormat.i420, 8, 8);
final fragment = YuvImage.allocate(YuvPixelFormat.i420, 4, 4);
fragment.yPlane.bytes.fillRange(0, fragment.yPlane.bytes.length, 255);
final start = destination.revision;
destination.applyPatch(fragment, x: 2, y: 2);
debugPrint(
'patched: revision +${destination.revision - start}, top-left Y ${destination.yPlane.getPixel(0, 0)}, patch Y ${destination.yPlane.getPixel(2, 2)}',
);
try {
destination.applyPatch(fragment, x: 3, y: 2);
} on ArgumentError catch (error) {
debugPrint('rejected: ${error.message}; revision +${destination.revision - start}');
}
}Expected output:
patched: revision +1, top-left Y 0, patch Y 255
rejected: YUV patch coordinates must follow chroma 2x2 alignment; revision +1
The rules: both images have the same format; the fragment must fit completely and be a different image that shares no
bytes with the destination; for I420 and NV12 x and y must be even, and an odd fragment width or height is accepted
only when it reaches the right or bottom edge. A rejected patch leaves the destination unchanged.
| Symptom | Check and action |
|---|---|
UnsupportedError saying YuvFfi.initialize() must complete
|
The call is capability-gated and ran before initialization finished in this isolate. Await YuvFfi.initialize() there. |
UnsupportedError for applyChromaSwap
|
The image is not NV12. Convert first (image.applyFormat(YuvPixelFormat.nv12).applyChromaSwap()) or skip the swap. |
UnsupportedError naming an operation and formats |
The loaded backend does not export that operation. Query capabilities.supports(...) after initialize() and choose another path. |
ArgumentError about radius
|
The radius is negative or above 256. Use 0 to skip, or 1 to 256. |
YuvNativeException |
The native side reported an overflow, an allocation failure or an internal error. It carries statusCode and the operation; the image keeps its previous bytes, size and revision. Reduce the size or retry; report it with the status code if it repeats. |
| The widget shows the old picture after a change | You wrote into plane.bytes without markDirty(), or you did not rebuild the widget after an apply* call. See Displaying images. |
| A patch is rejected with an alignment message |
x, y or an odd fragment extent breaks 2 by 2 chroma alignment. Use even coordinates, or let an odd extent end at the image edge. |
- Images and layout: formats, planes and padding.
- Displaying images: show the result.
- Geometry and overlays: crop and orient exactly what is visible.
-
API reference: every parameter of
YuvImage.