Skip to content

Commit

Permalink
Fix some bugs, add more coverage.
Browse files Browse the repository at this point in the history
  • Loading branch information
matanlurey committed Aug 25, 2024
1 parent bbc3ee4 commit de593fd
Show file tree
Hide file tree
Showing 35 changed files with 1,163 additions and 297 deletions.
1 change: 1 addition & 0 deletions .github/workflows/check.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ jobs:
- uses: dart-lang/[email protected]
- uses: actions/setup-node@v4
- uses: browser-actions/setup-chrome@v1
- uses: mfinelli/setup-imagemagick@v5
- run: dart pub get
- run: dart format --output none --set-exit-if-changed .
- run: dart analyze
Expand Down
8 changes: 4 additions & 4 deletions doc/buffers.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,12 +23,12 @@ final class MyBuffer extends Buffer<int> {
In most cases, a concrete [Pixels][] instance will be used to represent pixel
data, which is a buffer that can be read from _and written to_ and guarantees
fast access to linearly stored pixel data. For example, the [IntPixels][] class
stores pixel data as a list of integers, and [FloatPixels][] stores pixel data
stores pixel data as a list of integers, and [Float32x4Pixels][] stores pixel data
as a 32x4 matrix of floating-point values.

[Pixels]: ../pxl/Pixels-class.html
[IntPixels]: ../pxl/IntPixels-class.html
[FloatPixels]: ../pxl/FloatPixels-class.html
[Float32x4Pixels]: ../pxl/Float32x4Pixels-class.html

```dart
// Creating a 320x240 pixel buffer with the default `abgr8888` format.
Expand Down Expand Up @@ -87,11 +87,11 @@ final abgr8888Pixels = IntPixels(320, 240);
final rgba8888Buffer = abgr8888Pixels.mapConvert(rgba8888);
```

To copy the actual data, use [IntPixels.from][] or [FloatPixels.from][]:
To copy the actual data, use [IntPixels.from][] or [Float32x4Pixels.from][]:

```dart
final rgba8888Pixels = IntPixels.from(abgr8888Pixels);
```

[IntPixels.from]: ../pxl/IntPixels/IntPixels.from.html
[FloatPixels.from]: ../pxl/FloatPixels/FloatPixels.from.html
[Float32x4Pixels.from]: ../pxl/Float32x4Pixels/Float32x4Pixels.from.html
2 changes: 1 addition & 1 deletion lib/pxl.dart
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
/// Fixed-size buffer of [Pixels], with customizable [PixelFormat]s,
/// [BlendMode]s, and more.
///
/// - Create and manipulate in-memory [IntPixels] or [FloatPixels] buffers
/// - Create and manipulate in-memory [IntPixels] or [Float32x4Pixels] buffers
/// - Define and convert between [PixelFormat]s.
/// - Palette-based indexed pixel formats with [IndexedFormat].
/// - Buffer-to-buffer blitting with automatic format conversion and
Expand Down
30 changes: 14 additions & 16 deletions lib/src/buffer.dart
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import 'package:pxl/src/geometry.dart';
import 'package:pxl/src/internal.dart';

part 'buffer/clipped.dart';
part 'buffer/compare.dart';
part 'buffer/indexed.dart';
part 'buffer/map.dart';
part 'buffer/pixels_float.dart';
Expand All @@ -23,9 +24,6 @@ part 'buffer/pixels.dart';
///
/// {@category Buffers}
abstract base mixin class Buffer<T> {
/// @nodoc
const Buffer();

/// Format of the pixel data in the buffer.
PixelFormat<T, void> get format;

Expand All @@ -41,12 +39,6 @@ abstract base mixin class Buffer<T> {
/// Length of the buffer in pixels.
int get length => width * height;

/// Returns whether the buffer is empty.
bool get isEmpty => length == 0;

/// Returns whether the buffer is not empty.
bool get isNotEmpty => length != 0;

/// Returns whether the given position is within the bounds of the buffer.
bool contains(Pos pos) => bounds.contains(pos);

Expand All @@ -66,6 +58,11 @@ abstract base mixin class Buffer<T> {
/// If outside the bounds of the buffer, the behavior is undefined.
T getUnsafe(Pos pos);

/// Compares the buffer to another buffer and returns the result.
ComparisonResult<T> compare(Buffer<T> other, {double epsilon = 1e-10}) {
return ComparisonResult._compare(this, other, epsilon: epsilon);
}

/// Returns a lazy buffer buffer that converts pixels with the given function.
///
/// It is expected that the function does not change the representation of the
Expand Down Expand Up @@ -128,7 +125,7 @@ abstract base mixin class Buffer<T> {
/// ## Example
///
/// ```dart
/// final buffer = FloatPixels(1, 3, data: Float32x4List.fromList([
/// final buffer = Float32x4Pixels(1, 3, data: Float32x4List.fromList([
/// floatRgba.red,
/// floatRgba.green,
/// floatRgba.blue,
Expand All @@ -151,8 +148,8 @@ abstract base mixin class Buffer<T> {
/// Returns a lazy buffer that clips the buffer to the given [bounds].
///
/// The returned buffer will have the same dimensions as the bounds, and will
/// only contain pixels that are within the bounds of the original buffer; if
/// the bounded rectangle is empty, the returned buffer will be empty.
/// only contain pixels that are within the bounds of the original buffer; the
/// resulting buffer must not be empty.
///
/// ## Example
///
Expand All @@ -167,7 +164,11 @@ abstract base mixin class Buffer<T> {
/// print(clipped.data); // [0xFF00FF00, 0xFF0000FF]
/// ```
Buffer<T> mapRect(Rect bounds) {
return _ClippedBuffer(this, bounds.intersect(this.bounds));
final result = bounds.intersect(this.bounds);
if (result.isEmpty) {
throw ArgumentError.value(bounds, 'bounds', 'region must be non-empty');
}
return _ClippedBuffer(this, result);
}

/// Returns a lazy iterable of pixels in the buffer from [start] to [end].
Expand Down Expand Up @@ -239,7 +240,4 @@ abstract final class _Buffer<T> with Buffer<T> {

@override
int get height => _source.height;

@override
T getUnsafe(Pos pos) => _source.getUnsafe(pos);
}
50 changes: 50 additions & 0 deletions lib/src/buffer/compare.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
part of '../buffer.dart';

/// The result of a pixel comparison test.
final class ComparisonResult<T> {
/// Compares two pixel buffers [a] and [b] and returns the result.
factory ComparisonResult._compare(
Buffer<T> a,
Buffer<T> b, {
double epsilon = 1e-10,
}) {
// If the dimension are different, the buffers are considered different.
if (a.width != b.width || a.height != b.height) {
return ComparisonResult._(1.0);
}

// Ensure the pixel formats are the same.
b = b.mapConvert(a.format);

// Calculate the difference between the two buffers.
final aIterator = a.data.iterator;
final bIterator = b.data.iterator;
var pixelDiffCount = 0;
while (aIterator.moveNext() && bIterator.moveNext()) {
final diffPixel = a.format.compare(aIterator.current, bIterator.current);
if (diffPixel > epsilon) {
pixelDiffCount += 1;
}
}

return ComparisonResult._(pixelDiffCount / a.length);
}

const ComparisonResult._(this.difference);

/// The calculated percentage of pixel difference between two pixel buffers.
final double difference;

/// Whether the two pixel buffers were considered identical.
///
/// Equivalent to `difference == 0.0`.
bool get isIdentical => difference == 0.0;

/// Whether the two pixel buffers were considered different.
///
/// Equivalent to `difference != 0.0`.
bool get isDifferent => difference != 0.0;

@override
String toString() => 'ComparisonResult <difference: $difference>';
}
61 changes: 10 additions & 51 deletions lib/src/buffer/pixels.dart
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ part of '../buffer.dart';
/// lightweight wrapper around the raw bytes represented by a [TypedDataList];
/// but cannot be extended or implemented (similar to [TypedDataList]).
///
/// In most cases either [IntPixels] or [FloatPixels] will be used directly.
/// In most cases either [IntPixels] or [Float32x4Pixels] will be used directly.
///
/// {@category Buffers}
/// {@category Blending}
Expand Down Expand Up @@ -249,10 +249,15 @@ abstract final class Pixels<T> with Buffer<T> {

@override
Iterable<T> getRectUnsafe(Rect rect) {
if (rect.width == width) {
return getRangeUnsafe(rect.topLeft, rect.bottomRight);
}
return _PixelsRectIterable(data, rect);
// TODO: Consider a custom Iterable.
return Iterable.generate(
rect.height,
(y) {
final start = _indexAtUnsafe(Pos(rect.left, rect.top + y));
final end = start + rect.width;
return data.getRange(start, end);
},
).expand((e) => e);
}

/// Copies the pixel data from a source buffer to `this` buffer.
Expand Down Expand Up @@ -555,49 +560,3 @@ abstract final class Pixels<T> with Buffer<T> {
}
}
}

final class _PixelsRectIterable<T> extends Iterable<T> {
const _PixelsRectIterable(this._data, this._bounds);
final TypedDataList<T> _data;
final Rect _bounds;

@override
int get length => _bounds.area;

@override
Iterator<T> get iterator {
final startIdx = _bounds.top * _bounds.width + _bounds.left;
final endIdx = (_bounds.bottom - 1) * _bounds.width + _bounds.right - 1;
return _PixelsRectIterator(_data, _bounds, startIdx - 1, endIdx);
}
}

final class _PixelsRectIterator<T> implements Iterator<T> {
_PixelsRectIterator(this._data, this._bounds, this._start, this._end);
final TypedDataList<T> _data;
final Rect _bounds;
final int _end;

int _start;

@override
@unsafeNoBoundsChecks
T get current => _data[_start];

@override
bool moveNext() {
// Imagine we are at B, in the rectangle {B, C, F, G}
// B -> C -> F -> G
//
// A B C D
// E F G H
// I J K L
//
// If we are at the end of the row, move to the next row
_start++;
if (_start % _bounds.width == _bounds.right) {
_start += _bounds.width - _bounds.width;
}
return _start <= _end;
}
}
18 changes: 9 additions & 9 deletions lib/src/buffer/pixels_float.dart
Original file line number Diff line number Diff line change
Expand Up @@ -11,15 +11,15 @@ part of '../buffer.dart';
/// The default [format] is [floatRgba].
///
/// {@category Buffers}
final class FloatPixels extends Pixels<Float32x4> {
final class Float32x4Pixels extends Pixels<Float32x4> {
/// Creates a new buffer of multi-channel floating point pixel data.
///
/// Both [width] and [height] must be greater than zero.
///
/// The [format] defaults to [floatRgba], and if [data] is provided it's
/// contents must are assumed to be in the same format, and `data.length` must
/// be equal to `width * height`.
factory FloatPixels(
factory Float32x4Pixels(
int width,
int height, {
PixelFormat<Float32x4, void> format = floatRgba,
Expand All @@ -33,7 +33,7 @@ final class FloatPixels extends Pixels<Float32x4> {
}
if (data == null) {
data = Float32x4List(width * height);
if (format.zero.equal(Float32x4.zero()).signMask != 0) {
if (format.zero.equal(Float32x4.zero()).signMask == 0) {
data.fillRange(0, data.length, format.zero);
}
} else if (data.length != width * height) {
Expand All @@ -43,7 +43,7 @@ final class FloatPixels extends Pixels<Float32x4> {
'Must be equal to width * height.',
);
}
return FloatPixels._(
return Float32x4Pixels._(
data,
width: width,
height: height,
Expand All @@ -59,25 +59,25 @@ final class FloatPixels extends Pixels<Float32x4> {
/// ## Example
///
/// ```dart
/// final original = FloatPixels(3, 3);
/// final original = Float32x4Pixels(3, 3);
/// final clipped = original.getRegion(Rect.fromLTWH(1, 1, 2, 2));
///
/// final copy = FloatPixels.from(clipped);
/// final copy = Float32x4Pixels.from(clipped);
/// print(copy.width); // 2
/// print(copy.height); // 2
/// ```
factory FloatPixels.from(Buffer<Float32x4> buffer) {
factory Float32x4Pixels.from(Buffer<Float32x4> buffer) {
final data = Float32x4List(buffer.length);
data.setAll(0, buffer.data);
return FloatPixels(
return Float32x4Pixels(
buffer.width,
buffer.height,
data: data,
format: buffer.format,
);
}

const FloatPixels._(
const Float32x4Pixels._(
this.data, {
required super.width,
required super.height,
Expand Down
Loading

0 comments on commit de593fd

Please sign in to comment.