Skip to content

Commit

Permalink
Add a grayscale format and tweak. (#2)
Browse files Browse the repository at this point in the history
  • Loading branch information
matanlurey authored Aug 26, 2024
1 parent d9335c2 commit 543c000
Show file tree
Hide file tree
Showing 12 changed files with 292 additions and 14 deletions.
4 changes: 4 additions & 0 deletions doc/formats.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,12 +37,16 @@ Name | Bits per pixel | Description
------------ | -------------- | ------------------------------------------------
[abgr8888][] | 32 | 4 channels @ 8 bits each
[argb8888][] | 32 | 4 channels @ 8 bits each
[gray8][] | 8 | 1 channel @ 8 bits
[rgba8888][] | 32 | 4 channels @ 8 bits each

[abgr8888]: ../pxl/abgr8888-constant.html
[argb8888]: ../pxl/argb8888-constant.html
[gray8]: ../pxl/gray8-constant.html
[rgba8888]: ../pxl/rgba8888-constant.html

Grayscale formats use _luminance_ values to represent color.

## Floating-point pixel formats

All floating-point formats use the RGBA 128-bit format as a common intermediate
Expand Down
4 changes: 3 additions & 1 deletion lib/src/blend/porter_duff.dart
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,9 @@ final class PorterDuff implements BlendMode {
PixelFormat<S, void> srcFormat,
PixelFormat<T, void> dstFormat,
) {
if (identical(srcFormat, floatRgba) && identical(dstFormat, floatRgba)) {
// Intentionally ignore the type check, we're performing it implicitly.
// ignore: unrelated_type_equality_checks
if (srcFormat == floatRgba && dstFormat == floatRgba) {
return _blendFloatRgba as T Function(S src, T dst);
}
return (src, dst) {
Expand Down
2 changes: 1 addition & 1 deletion lib/src/buffer.dart
Original file line number Diff line number Diff line change
Expand Up @@ -135,7 +135,7 @@ abstract base mixin class Buffer<T> {
/// print(converted.data); // [0xFFFF0000, 0xFF00FF00, 0xFF0000FF]
/// ```
Buffer<R> mapConvert<R>(PixelFormat<R, void> format) {
if (identical(this.format, format)) {
if (format == this.format) {
return this as Buffer<R>;
}
return _MapBuffer(
Expand Down
2 changes: 2 additions & 0 deletions lib/src/codec/unpng.dart
Original file line number Diff line number Diff line change
Expand Up @@ -87,10 +87,12 @@ final _pngSignature = Uint8List(8)
..[7] = 0x0A;

Uint8List _encodeUncompressedPng(Buffer<int> pixels) {
// coverage:ignore-start
assert(
pixels.format == abgr8888,
'Unsupported pixel format: ${pixels.format}',
);
// coverage:ignore-end
final output = BytesBuilder(copy: false);

// Write the PNG signature (https://www.w3.org/TR/png-3/#3PNGsignature).
Expand Down
19 changes: 19 additions & 0 deletions lib/src/format.dart
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ import 'package:pxl/src/internal.dart';
part 'format/abgr8888.dart';
part 'format/argb8888.dart';
part 'format/float_rgba.dart';
part 'format/gray8.dart';
part 'format/grayscale.dart';
part 'format/indexed.dart';
part 'format/rgb.dart';
part 'format/rgb888.dart';
Expand Down Expand Up @@ -199,3 +201,20 @@ abstract base mixin class PixelFormat<P, C> {
@override
String toString() => name;
}

/// Converts RGB channels to a gray luminance value.
///
/// The resulting value is in the range `[0, 255]`.
int _luminanceRgb888(int r, int g, int b) {
final weightedSum = (r & 0xFF) * 76 + (g & 0xFF) * 150 + (b & 0xFF) * 29;
return weightedSum ~/ 0xFF;
}

/// Converts floating-point RGB channels to a gray luminance value.
///
/// The resulting value is in the range `[0.0, 1.0]`.
(double gray, double alpha) _luminanceFloatRgba(Float32x4 pixel) {
final product = pixel * Float32x4(76 / 0xFF, 150 / 0xFF, 29 / 0xFF, 0.0);
final weightedSum = product.x + product.y + product.z;
return (weightedSum, pixel.w);
}
81 changes: 81 additions & 0 deletions lib/src/format/gray8.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
part of '../format.dart';

/// 8-bit grayscale pixel format.
///
/// Colors in this format are represented as follows:
///
/// Color | Value
/// --------------|------
/// [Gray8.black] | `0x00`
/// [Gray8.white] | `0xFF`
///
/// {@category Pixel Formats}
const gray8 = Gray8._();

/// 8-bit grayscale pixel format.
///
/// For a singleton instance of this class, and further details, see [gray8].
///
/// {@category Pixel Formats}
final class Gray8 extends _GrayInt {
const Gray8._();

@override
String get name => 'GRAY8';

@override
int get bytesPerPixel => Uint8List.bytesPerElement;

@override
int get maxGray => 0xFF;

@override
int get max => maxGray;

@override
int copyWith(int pixel, {int? gray}) {
var output = pixel;
if (gray != null) {
output = gray & 0xFF;
}
return output;
}

@override
int copyWithNormalized(int pixel, {double? gray}) {
return copyWith(
pixel,
gray: gray != null ? (gray.clamp(0.0, 1.0) * 0xFF).floor() : null,
);
}

@override
int getGray(int pixel) => pixel & 0xFF;

@override
int fromAbgr8888(int pixel) {
return _luminanceRgb888(
abgr8888.getRed(pixel),
abgr8888.getGreen(pixel),
abgr8888.getBlue(pixel),
);
}

@override
int toAbgr8888(int pixel) {
// Isolate the least significant 8 bits.
final value = pixel & 0xFF;

// Replicate the value across all channels (R, G, B).
final asRgb = value * 0x010101;

// Set the alpha channel to 0xFF.
return asRgb | 0xFF000000;
}

@override
Float32x4 toFloatRgba(int pixel) {
final g = getGray(pixel) / 255.0;
return Float32x4(g, g, g, 1.0);
}
}
88 changes: 88 additions & 0 deletions lib/src/format/grayscale.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
part of '../format.dart';

/// A mixin for pixel formats that represent _graysacle_ pixels.
base mixin Grayscale<P, C> implements PixelFormat<P, C> {
/// Creates a new pixel with the given channel values.
///
/// The [gray] value is optional.
///
/// If omitted, the channel value is set to the minimum value.
///
/// ## Example
///
/// ```dart
/// // Creating a full gray pixel.
/// final pixel = grayscale.create(gray: 0xFF);
/// ```
P create({C? gray}) => copyWith(zero, gray: gray ?? minGray);

@override
P copyWith(P pixel, {C? gray});

/// Creates a new pixel with the given channel value normalized to the range
/// `[0.0, 1.0]`.
///
/// The [gray] value is optional.
///
/// If omitted, the channel value is set to `0.0`.
///
/// ## Example
///
/// ```dart
/// // Creating a full gray pixel.
/// final pixel = grayscale.createNormalized(gray: 1.0);
/// ```
P createNormalized({double gray = 0.0}) {
return copyWithNormalized(zero, gray: gray);
}

@override
P copyWithNormalized(P pixel, {double? gray});

/// The minimum value for the gray channel.
C get minGray;

/// The maximum value for the gray channel.
C get maxGray;

/// Black pixel.
P get black;

/// White pixel.
P get white => max;

/// Returns the gray channel value of the [pixel].
C getGray(P pixel);

@override
P fromFloatRgba(Float32x4 pixel) {
final (g, _) = _luminanceFloatRgba(pixel);
return createNormalized(gray: g);
}
}

abstract final class _GrayInt extends PixelFormat<int, int>
with Grayscale<int, int> {
const _GrayInt();

@override
double distance(int a, int b) => (a - b).abs().toDouble();

@override
double compare(int a, int b) => 1.0 - (a - b).abs() / maxGray.toDouble();

@override
@nonVirtual
int get zero => 0x0;

@override
@nonVirtual
int get minGray => 0x0;

@override
@nonVirtual
int clamp(int pixel) => pixel & max;

@override
int get black => create(gray: minGray);
}
10 changes: 0 additions & 10 deletions lib/src/format/rgb.dart
Original file line number Diff line number Diff line change
Expand Up @@ -144,16 +144,6 @@ abstract final class Rgb<P, C> extends PixelFormat<P, C> {

/// Returns the blue channel value of the [pixel].
C getBlue(P pixel);

@override
P fromFloatRgba(Float32x4 pixel) {
return copyWithNormalized(
zero,
red: pixel.x,
green: pixel.y,
blue: pixel.z,
);
}
}

base mixin _Rgb8Int on Rgb<int, int> {
Expand Down
10 changes: 10 additions & 0 deletions lib/src/format/rgb888.dart
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,16 @@ final class Rgb888 extends Rgb<int, int> with _Rgb8Int {
);
}

@override
int fromFloatRgba(Float32x4 pixel) {
return copyWithNormalized(
zero,
red: pixel.x,
green: pixel.y,
blue: pixel.z,
);
}

@override
int getRed(int pixel) => (pixel >> 16) & 0xFF;

Expand Down
2 changes: 1 addition & 1 deletion test/buffer_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -229,7 +229,7 @@ void main() {

test('buffer.format is pixels.format', () {
final buffer = IntPixels(2, 2).mapRect(Rect.fromLTWH(1, 1, 1, 1));
check(buffer.format).identicalTo(abgr8888);
check(buffer.format).equals(abgr8888);
});

test('buffer.getUnsafe maps to pixels.getUnsafe', () {
Expand Down
82 changes: 82 additions & 0 deletions test/format/gray8_test.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
import 'dart:typed_data';

import 'package:pxl/pxl.dart';

import '../src/prelude.dart';

void main() {
test('smoke test of GRAY8 <> ABGR8888', () {
check(gray8.name).equals('GRAY8');
check(gray8.bytesPerPixel).equals(1);
check(gray8.maxGray).equals(255);
check(gray8.max).equals(255);

check(gray8.getGray(0x00)).equals(0);
check(gray8.getGray(0x1d)).equals(29);
check(gray8.getGray(0x96)).equals(150);

check(gray8.fromAbgr8888(abgr8888.black)).equals(0);
check(gray8.fromAbgr8888(abgr8888.blue)).equals(29);
check(gray8.fromAbgr8888(abgr8888.green)).equals(150);
check(gray8.fromAbgr8888(abgr8888.red)).equals(76);
check(gray8.fromAbgr8888(abgr8888.white)).equals(255);

check(gray8.fromFloatRgba(floatRgba.black)).equals(0);
check(gray8.fromFloatRgba(floatRgba.blue)).equals(29);
check(gray8.fromFloatRgba(floatRgba.green)).equals(150);
check(gray8.fromFloatRgba(floatRgba.red)).equals(76);
check(gray8.fromFloatRgba(floatRgba.white)).equals(255);

check(gray8.toAbgr8888(0)).equalsHex(abgr8888.black);
check(gray8.toAbgr8888(29)).equalsHex(0xff1d1d1d);
check(gray8.toAbgr8888(150)).equalsHex(0xff969696);
check(gray8.toAbgr8888(76)).equalsHex(0xff4c4c4c);
check(gray8.toAbgr8888(255)).equalsHex(abgr8888.white);

check(gray8.toFloatRgba(0)).equals(floatRgba.black);
check(gray8.toFloatRgba(29)).equals(
Float32x4(0.113725, 0.113725, 0.113725, 1.0),
);
check(gray8.toFloatRgba(150)).equals(
Float32x4(0.588235, 0.588235, 0.588235, 1.0),
);
check(gray8.toFloatRgba(76)).equals(
Float32x4(0.298039, 0.298039, 0.298039, 1.0),
);
check(gray8.toFloatRgba(255)).equals(floatRgba.white);
});

test('create', () {
check(gray8.create(gray: 0)).equals(gray8.black);
check(gray8.create(gray: 29)).equals(29);
check(gray8.create(gray: 150)).equals(150);
check(gray8.create(gray: 76)).equals(76);
check(gray8.create(gray: 255)).equals(gray8.white);
});

test('distance', () {
check(gray8.distance(0, 0)).equals(0);
check(gray8.distance(0, 29)).equals(29);
});

test('compare', () {
check(gray8.compare(0, 0)).equals(1.0);
check(gray8.compare(0, 29)).equals(0.8862745098039215);
check(gray8.compare(29, 0)).equals(0.8862745098039215);
check(gray8.compare(29, 29)).equals(1.0);
});

test('minGray', () {
check(gray8.minGray).equals(0);
});

test('clamp', () {
check(gray8.clamp(-1)).equals(255);
check(gray8.clamp(0)).equals(0);
check(gray8.clamp(29)).equals(29);
check(gray8.clamp(150)).equals(150);
check(gray8.clamp(76)).equals(76);
check(gray8.clamp(255)).equals(255);
check(gray8.clamp(256)).equals(0);
});
}
2 changes: 1 addition & 1 deletion test/src/prelude.dart
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ extension Float32x4Checks on Subject<Float32x4> {
void equals(Float32x4 other) {
context.expect(() => prefixFirst('equals ', literal(other)), (actual) {
final result = actual.equal(other);
if (result.signMask == 0xF) return null;
if (result.signMask != 0) return null;
return Rejection(which: ['are not equal']);
});
}
Expand Down

0 comments on commit 543c000

Please sign in to comment.