Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add a grayscale format and tweak. #2

Merged
merged 1 commit into from
Aug 26, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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