Skip to content

Commit

Permalink
Add a packed binary format, tweaks, fix blitting. (#9)
Browse files Browse the repository at this point in the history
Closes #3.
  • Loading branch information
matanlurey authored Aug 31, 2024
1 parent af664e0 commit ba4ea33
Show file tree
Hide file tree
Showing 17 changed files with 447 additions and 21 deletions.
2 changes: 2 additions & 0 deletions lib/src/buffer/compare.dart
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
part of '../buffer.dart';

/// The result of a pixel comparison test.
///
/// {@category Output and Comparison}
final class ComparisonResult<T> {
/// Compares two pixel buffers [a] and [b] and returns the result.
factory ComparisonResult._compare(
Expand Down
7 changes: 4 additions & 3 deletions lib/src/buffer/pixels.dart
Original file line number Diff line number Diff line change
Expand Up @@ -311,8 +311,8 @@ abstract final class Pixels<T> with Buffer<T> {
return;
}
source = Rect.fromLTWH(
source.top,
source.left,
source.top,
clipped.width,
clipped.height,
);
Expand Down Expand Up @@ -457,8 +457,8 @@ abstract final class Pixels<T> with Buffer<T> {
return;
}
source = Rect.fromLTWH(
source.top,
source.left,
source.top,
clipped.width,
clipped.height,
);
Expand Down Expand Up @@ -508,8 +508,8 @@ abstract final class Pixels<T> with Buffer<T> {
return;
}
source = Rect.fromLTWH(
sRect.top,
sRect.left,
sRect.top,
tRect.width,
tRect.height,
);
Expand Down Expand Up @@ -557,6 +557,7 @@ abstract final class Pixels<T> with Buffer<T> {
dstIdx++;
}
dstIdx += width - source.width;
srcIdx += from.width - source.width;
}
}
}
1 change: 1 addition & 0 deletions lib/src/codec.dart
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
export 'codec/netpbm.dart';
export 'codec/packedbm.dart';
export 'codec/unpng.dart';
142 changes: 135 additions & 7 deletions lib/src/codec/netpbm.dart
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,114 @@ import 'dart:typed_data';
import 'package:meta/meta.dart';
import 'package:pxl/pxl.dart';

/// A codec that encodes and decodes pixel data as a portable pixmap (Netpbm)
/// image format.
///
/// {@category Output and Comparison}
const netpbmAsciiCodec = NetpbmAsciiCodec._();

/// A codec that encodes and decodes pixel data as a portable pixmap (Netpbm)
/// image format.
///
/// {@category Output and Comparison}
const netpbmBinaryCodec = NetpbmBinaryCodec._();

/// A codec that encodes and decodes pixel data as a portable pixmap (Netpbm)
/// image format.
///
/// A singleton instance of this class is available as [netpbmAsciiCodec].
///
/// {@category Output and Comparison}
final class NetpbmAsciiCodec extends Codec<Buffer<int>, String> {
const NetpbmAsciiCodec._();

@override
NetpbmAsciiEncoder get encoder => netpbmAsciiEncoder;

/// Encodes pixel data as an ASCII Netpbm image.
///
/// The [comments] are added to the image.
///
/// If the [format] is omitted, defaults to:
/// - [NetpbmFormat.graymap] for grayscale pixel data.
/// - [NetpbmFormat.pixmap] for RGB pixel data.
/// - [NetpbmFormat.bitmap] otherwise.
@override
String encode(
Buffer<int> input, {
Iterable<String> comments = const [],
NetpbmFormat? format,
}) {
return netpbmAsciiEncoder.convert(
input,
comments: comments,
format: format,
);
}

@override
NetpbmAsciiDecoder get decoder => netpbmAsciiDecoder;

/// Decodes an ASCII Netpbm image to pixel data.
///
/// If the input is invalid, a [FormatException] is thrown.
///
/// The [format] is used to convert the pixel data to the desired format;
/// if omitted defaults to [abgr8888].
@override
Buffer<int> decode(String input, {PixelFormat<int, void>? format}) {
return netpbmAsciiDecoder.convert(input, format: format);
}
}

/// A codec that encodes and decodes pixel data as a portable pixmap (Netpbm)
/// image format.
///
/// A singleton instance of this class is available as [netpbmBinaryCodec].
///
/// {@category Output and Comparison}
final class NetpbmBinaryCodec extends Codec<Buffer<int>, Uint8List> {
const NetpbmBinaryCodec._();

@override
NetpbmBinaryEncoder get encoder => netpbmBinaryEncoder;

/// Encodes pixel data as a binary Netpbm image.
///
/// The [comments] are added to the image.
///
/// If the [format] is omitted, defaults to:
/// - [NetpbmFormat.graymap] for grayscale pixel data.
/// - [NetpbmFormat.pixmap] for RGB pixel data.
/// - [NetpbmFormat.bitmap] otherwise.
@override
Uint8List encode(
Buffer<int> input, {
Iterable<String> comments = const [],
NetpbmFormat? format,
}) {
return netpbmBinaryEncoder.convert(
input,
comments: comments,
format: format,
);
}

@override
NetpbmBinaryDecoder get decoder => netpbmBinaryDecoder;

/// Decodes a binary Netpbm image to pixel data.
///
/// If the input is invalid, a [FormatException] is thrown.
///
/// The [format] is used to convert the pixel data to the desired format;
/// if omitted defaults to [abgr8888].
@override
Buffer<int> decode(Uint8List input, {PixelFormat<int, void>? format}) {
return netpbmBinaryDecoder.convert(input, format: format);
}
}

/// Encodes pixel data as a portable pixmap (Netpbm) image format.
///
/// {@macro pxl.netpbm_encoder.format}
Expand Down Expand Up @@ -84,9 +192,19 @@ abstract final class NetpbmEncoder<T> extends Converter<Buffer<int>, T> {
/// {@endtemplate}
final NetpbmFormat? format;

/// Converts the pixel data to a Netpbm image.
///
/// The [comments] are added to the image.
///
/// If the [format] is omitted, and [NetpbmEncoder.format] is not set, the
/// format is inferred from the pixel data.
@override
T convert(Buffer<int> input, {Iterable<String> comments = const []}) {
final format = _getOrInferFormat(input);
T convert(
Buffer<int> input, {
Iterable<String> comments = const [],
NetpbmFormat? format,
}) {
format ??= _getOrInferFormat(input);
final header = NetpbmHeader(
width: input.width,
height: input.height,
Expand Down Expand Up @@ -391,13 +509,21 @@ abstract final class NetpbmDecoder<T> extends Converter<T, Buffer<int>> {
@protected
List<int> _data(T input, int offset, {required bool bitmap});

/// Converts the input to integer-based pixel data.
///
/// If the input is invalid, a [FormatException] is thrown.
///
/// The [format] is used to convert the pixel data to the desired format;
/// if omitted, the decoder's [NetpbmDecoder.format] is used, which defaults
/// to [abgr8888].
@override
@nonVirtual
Buffer<int> convert(T input) {
Buffer<int> convert(T input, {PixelFormat<int, void>? format}) {
final (header, error, offset) = _parseHeader(input);
if (header == null) {
throw FormatException(error, input);
}
final fmt = format ?? this.format;
final data = _data(
input,
offset,
Expand All @@ -407,12 +533,12 @@ abstract final class NetpbmDecoder<T> extends Converter<T, Buffer<int>> {
switch (header.format) {
case NetpbmFormat.bitmap:
pixels = data.map((value) {
return value == 0x0 ? format.zero : format.max;
return value == 0x0 ? fmt.zero : fmt.max;
});
case NetpbmFormat.graymap:
pixels = data.map((value) {
final gray = gray8.create(gray: value);
return format.convert(gray, from: gray8);
return fmt.convert(gray, from: gray8);
});
case NetpbmFormat.pixmap:
if (data.length % 3 != 0) {
Expand All @@ -423,11 +549,11 @@ abstract final class NetpbmDecoder<T> extends Converter<T, Buffer<int>> {
final g = data[i * 3 + 1];
final b = data[i * 3 + 2];
final rgb = rgb888.create(red: r, green: g, blue: b);
return format.convert(rgb, from: rgb888);
return fmt.convert(rgb, from: rgb888);
});
}

final buffer = IntPixels(header.width, header.height, format: format);
final buffer = IntPixels(header.width, header.height, format: fmt);
buffer.data.setAll(0, pixels);
return buffer;
}
Expand Down Expand Up @@ -519,6 +645,8 @@ final class NetpbmBinaryDecoder extends NetpbmDecoder<Uint8List> {
}

/// Parsed header information from a Netpbm image.
///
/// {@category Output and Comparison}
@immutable
final class NetpbmHeader {
/// Creates a new Netpbm header with the given values.
Expand Down
133 changes: 133 additions & 0 deletions lib/src/codec/packedbm.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
import 'dart:convert';
import 'dart:typed_data';

import 'package:pxl/pxl.dart';

/// Codec for encoding and decoding packed bitmaps.
///
/// A packed bitmap is a compact binary representation of a bitmap, where each
/// pixel is encoded as a single bit. This can be useful for storing large
/// bitmaps in a compact format, at the cost of some overhead to pack and
/// unpack the data.
const packedBitmapCodec = PackedBitmapCodec._();

/// Codec for encoding and decoding packed bitmaps.
final class PackedBitmapCodec extends Codec<Buffer<int>, List<int>> {
const PackedBitmapCodec._();

@override
Converter<Buffer<int>, Uint32List> get encoder => packedBitmapEncoder;

@override
Converter<List<int>, Buffer<int>> get decoder => packedBitmapDecoder;

@override
Buffer<int> decode(
List<int> encoded, {
PixelFormat<int, void> format = abgr8888,
}) {
return packedBitmapDecoder.convert(encoded, format: format);
}
}

/// Encodes a bitmap to a compact (packed) binary format.
///
/// See [PackedBitmapEncoder] for details.
const packedBitmapEncoder = PackedBitmapEncoder._();

/// Encodes a bitmap to a compact (packed) binary format.
///
/// For a given buffer, [PixelFormat.zero] is encoded as `0`, and any other
/// value is encoded as `1`. 32 values are packed into a single byte, with the
/// least significant bit representing the first value. The first two 32-bit
/// words of the output are the width and height of the bitmap, respectively.
///
/// For example, a 128x128 bitmap, which normally would require 2048 bytes,
/// can be encoded into 256 bytes (128 * 128 / 32), or a 8x reduction in size,
/// at the cost of some overhead to pack and unpack the data.
///
/// A singleton instance of this class is available as [packedBitmapEncoder].
final class PackedBitmapEncoder extends Converter<Buffer<int>, Uint32List> {
const PackedBitmapEncoder._();

@override
Uint32List convert(Buffer<int> input) {
final width = input.width;
final height = input.height;
final data = input.data;

// Convert every 32 pixels into 32-bits.
// That is, a 128x128 bitmap is represented as 4x4 32-bit words.
final output = Uint32List(2 + (data.length ~/ 32));
output[0] = width;
output[1] = height;

var word = 0;
var bit = 0;
var offset = 2;
for (final pixel in input.data) {
if (pixel != input.format.zero) {
word |= 1 << bit;
}
bit++;
if (bit == 32) {
output[offset] = word;
word = 0;
bit = 0;
offset++;
}
}

return output;
}
}

/// Decodes a bitmap from a compact (packed) binary format.
///
/// See [PackedBitmapDecoder] for details.
const packedBitmapDecoder = PackedBitmapDecoder._();

/// Decodes a bitmap from a compact (packed) binary format.
///
/// A `0` bit represents [PixelFormat.zero], and a `1` bit is [PixelFormat.max];
/// 32 values are packed into a single byte, with the least significant bit
/// representing the first value. The first two 32-bit words of the input are
/// the width and height of the bitmap, respectively.
final class PackedBitmapDecoder extends Converter<List<int>, Buffer<int>> {
const PackedBitmapDecoder._();

@override
Buffer<int> convert(
List<int> input, {
PixelFormat<int, void> format = abgr8888,
}) {
final Uint32List view;
if (input is TypedDataList<int>) {
view = input.buffer.asUint32List();
} else {
view = Uint32List.fromList(input);
}
final width = view[0];
final height = view[1];
final output = IntPixels(width, height, format: format);

// Reverse of the above encoding.
var word = 0;
var bit = 0;
var offset = 2;
for (var i = 0; i < output.data.length; i++) {
if (bit == 0) {
word = view[offset];
offset++;
}
output.data[i] = (word & 1) == 0 ? format.zero : format.max;
word >>= 1;
bit++;
if (bit == 32) {
bit = 0;
}
}

return output;
}
}
2 changes: 2 additions & 0 deletions lib/src/format/grayscale.dart
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
part of '../format.dart';

/// A mixin for pixel formats that represent _graysacle_ pixels.
///
/// {@category Pixel Formats}
base mixin Grayscale<P, C> implements PixelFormat<P, C> {
/// Creates a new pixel with the given channel values.
///
Expand Down
1 change: 1 addition & 0 deletions pubspec.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ dependencies:

dev_dependencies:
checks: ^0.3.0
image: ^4.2.0
oath: ^0.2.1
path: ^1.9.0
test: ^1.25.8
Expand Down
Loading

0 comments on commit ba4ea33

Please sign in to comment.