diff --git a/README.md b/README.md index 37f0cd9..2e780e4 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # pxl -A cross-platform pixel buffer and foundation for pixel-based graphics. +A tiny cross-platform pixel buffer and foundation for pixel-based graphics. ## Usage @@ -10,7 +10,13 @@ import 'package:pxl/pxl.dart'; ## Features -TODO: Document what the package does, include screenshots, etc. +![Example](https://github.com/user-attachments/assets/5d8a97c5-d9d8-4c60-852c-bfd043f1634b) + +- Create and manipulate in-memory integer or floating-point pixel buffers. +- Define and convert between pixel formats. +- Palette-based indexed pixel formats. +- Buffer-to-buffer blitting with automatic format conversion and blend modes. +- Region-based pixel manipulation, replacement, and copying. ## Contributing @@ -32,8 +38,9 @@ To preview `dartdoc` output locally, run: ./chore dartodc ``` -### Resources +### Inspiration and Sources - [`MTLPixelFormat`](https://developer.apple.com/documentation/metal/mtlpixelformat) - [`@thi.ng/pixel`](https://github.com/thi-ng/umbrella/tree/main/packages/pixel) - [`embedded-graphics`](https://crates.io/crates/embedded-graphics) +- diff --git a/dartdoc_options.yaml b/dartdoc_options.yaml new file mode 100644 index 0000000..80b9407 --- /dev/null +++ b/dartdoc_options.yaml @@ -0,0 +1,18 @@ +# Format: https://github.com/dart-lang/dartdoc#dartdoc_optionsyaml. + +dartdoc: + categories: + "Buffers": + markdown: doc/buffers.md + "Pixel Formats": + markdown: doc/formats.md + "Blending": + markdown: doc/blending.md + "Output and Comparison": + markdown: doc/output.md + + categoryOrder: + - "Buffers" + - "Pixel Formats" + - "Blending" + - "Output and Comparison" diff --git a/doc/blending.md b/doc/blending.md new file mode 100644 index 0000000..5fe36b4 --- /dev/null +++ b/doc/blending.md @@ -0,0 +1,47 @@ +The [BlendMode] interface generates a function that blends two pixel values +together: + +```dart +// Combine non-overlapping regions of colors. +final xor = BlendMode.xor.getBlend(abgr8888, abgr8888); +print(xor(0x00000000, 0xFFFFFFFF)); // 0xFFFFFFFF +``` + +[BlendMode]: ../pxl/BlendMode-class.html + +Default blend modes are provided using the [PorterDuff] constructor: + +```dart +const BlendMode srcIn = PorterDuff( + PorterDuff.dst, + PorterDuff.zero, +); +``` + +Blend Mode | Description +---------- | ----------- +[clear][] | Sets pixels to `zero`. +[src][] | Copies the source pixel. +[dst][] | Copies the destination pixel. +[srcIn][] | The source that overlaps the destination replaces the destination. +[dstIn][] | The destination that overlaps the source replaces the source. +[srcOut][] | The source that does not overlap the destination replaces the destination. +[dstOut][] | The destination that does not overlap the source replaces the source. +[srcAtop][]| The source that overlaps the destination is blended with the destination. +[dstAtop][]| The destination that overlaps the source is blended with the source. +[xor][] | The source and destination are combined where they do not overlap. +[plus][] | The source and destination are added together. + +[PorterDuff]: ../pxl/PorterDuff-class.html + +[clear]: ../pxl/BlendMode/clear-constant.html +[src]: ../pxl/BlendMode/src-constant.html +[dst]: ../pxl/BlendMode/dst-constant.html +[srcIn]: ../pxl/BlendMode/srcIn-constant.html +[dstIn]: ../pxl/BlendMode/dstIn-constant.html +[srcOut]: ../pxl/BlendMode/srcOut-constant.html +[dstOut]: ../pxl/BlendMode/dstOut-constant.html +[srcAtop]: ../pxl/BlendMode/srcAtop-constant.html +[dstAtop]: ../pxl/BlendMode/dstAtop-constant.html +[xor]: ../pxl/BlendMode/xor-constant.html +[plus]: ../pxl/BlendMode/plus-constant.html diff --git a/doc/buffers.md b/doc/buffers.md new file mode 100644 index 0000000..9eb2a1c --- /dev/null +++ b/doc/buffers.md @@ -0,0 +1,97 @@ +2-dimensional views of pixel data are provided by the [Buffer][] type, which +provides a read-only view, with similar functionality to an `Iterable`: it can +be extended or mixed-in as a base class provided the necessary methods are +implemented. + +```dart +final class MyBuffer extends Buffer { + @override + PixelFormat get format => myFormat; + + @override + int get width => 640; + + @override + int get height => 480; + + /* ... rest of the class implementation ... */ +} +``` + +[Buffer]: ../pxl/Buffer-class.html + +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 +as a 32x4 matrix of floating-point values. + +[Pixels]: ../pxl/Pixels-class.html +[IntPixels]: ../pxl/IntPixels-class.html +[FloatPixels]: ../pxl/FloatPixels-class.html + +```dart +// Creating a 320x240 pixel buffer with the default `abgr8888` format. +final pixels = new IntPixels(320, 240); + +// Setting the pixel at (10, 20) to red. +pixels.set(Pos(10, 20), abgr8888.red); +``` + +## Writing data + +The `set` method is not the only way to write data to a buffer: + +- [`clear`][]: Clears a region to the `zero` value. +- [`fill`][]: Fills a region with a pixel value. +- [`copyFrom`][]: Copies pixel data from another buffer. + +[`clear`]: ../pxl/Pixels/clear.html +[`fill`]: ../pxl/Pixels/fill.html +[`copyFrom`]: ../pxl/Pixels/copyFrom.html + +For example, to fill the entire buffer with red: + +```dart +pixels.fill(abgr8888.red); +``` + +Pixels also support _alpha blending_; see [blending](./Blending-topic.html) +for more information. + +## Reading data + +Reading data from a buffer is done using the `get` method: + +```dart +final pixel = pixels.get(Pos(10, 20)); +``` + +The `get` method is not the only way to read data from a buffer: + +- [`getRange`][]: Reads a range of pixels lazily. +- [`getRect`][]: Reads a rectangular region of pixels lazily. + +[`getRange`]: ../pxl/Buffer/getRange.html +[`getRect`]: ../pxl/Buffer/getRect.html + +## Converting data + +Many operations in Pxl can automatically convert between different pixel formats +as needed. + +To lazily convert buffers, use the `mapConvert` method: + +```dart +final abgr8888Pixels = IntPixels(320, 240); +final rgba8888Buffer = abgr8888Pixels.mapConvert(rgba8888); +``` + +To copy the actual data, use [IntPixels.from][] or [FloatPixels.from][]: + +```dart +final rgba8888Pixels = IntPixels.from(abgr8888Pixels); +``` + +[IntPixels.from]: ../pxl/IntPixels/IntPixels.from.html +[FloatPixels.from]: ../pxl/FloatPixels/FloatPixels.from.html diff --git a/doc/formats.md b/doc/formats.md new file mode 100644 index 0000000..cf04825 --- /dev/null +++ b/doc/formats.md @@ -0,0 +1,82 @@ +A [PixelFormat][] is a description of how pixel data is stored in memory. It +includes a set of standard properties and rules for how to interpret the data, +such as how many bytes are used to store each pixel, canonical (`zero` or `max`) +values, and conversion rules to and from other formats. + +Pxl ships with a number of built-in pixel formats, and can be extended with +additional formats: + +```dart +const myFormat = const MyFormat._(); + +// Pixel data type Channel data type +// v v +final class MyFormat extends PixelFormat { + const MyFormat._(); + + @override + int get bytesPerPixel => 4; + + @override + int get zero => 0; + + // ... rest of the class implementation +} +``` + +[PixelFormat]: ../pxl/PixelFormat-class.html + +## Integer pixel formats + +All integer formats use the ABGR 32-bit format as a common intermediate for +conversions; channels that are larger or smaller than 8 bits are scaled to fit +within the 8-bit range. For example a 4-bit channel value of `0x0F` would be +scaled to `0xFF` when converting to 8-bit. + +Name | Bits per pixel | Description +------------ | -------------- | ------------------------------------------------ +[abgr8888][] | 32 | 4 channels @ 8 bits each +[argb8888][] | 32 | 4 channels @ 8 bits each +[rgba8888][] | 32 | 4 channels @ 8 bits each + +[abgr8888]: ../pxl/abgr8888-constant.html +[argb8888]: ../pxl/argb8888-constant.html +[rgba8888]: ../pxl/rgba8888-constant.html + +## Floating-point pixel formats + +All floating-point formats use the RGBA 128-bit format as a common intermediate +for conversions; channels that are larger or smaller than 32 bits are scaled to +fit within the 32-bit range. When converting to packed integer formats, data is +normalized to the range `[0.0, 1.0]` and then scaled to fit within the integer +range. + +Name | Bits per pixel | Description +------------- | -------------- | ----------------------------------------------- +[floatRgba][] | 128 | Red, Green, Blue, Alpha + +[floatRgba]: ../pxl/floatRgba-constant.html + +## Indexed pixel formats + +[IndexedFormat][]s use a palette to map pixel values to colors. + +The palette is stored as a separate array of colors, and the pixel data is +stored as indices into the palette. + +```dart +// Example of a 1-bit indexed format. +final mono1 = IndexedPixelFormat.bits8( + const [0xFF000000, 0xFFFFFFFF], + format: abgr8888, +); +``` + +Name | Bits per pixel | Description +------------- | -------------- | ----------------------------------------------- +[system8][] | 8 | 8 colors. +[system256][] | 8 | 256 colors (216 RGB + 40 grayscale). + +[IndexedFormat]: ../pxl/IndexedFormat-class.html +[system8]: ../pxl/system8.html +[system256]: ../pxl/system256.html diff --git a/doc/output.md b/doc/output.md new file mode 100644 index 0000000..cabe7d4 --- /dev/null +++ b/doc/output.md @@ -0,0 +1,54 @@ +Pxl is a headless pixel manipulation library, and provides limited support for +rendering, expecting the user to provide their own rendering solution (any +`Canvas`-like API will do, or something that supports pixel buffers in one of +the many formats Pxl provides). + +To aid with testing and debugging, Pxl provides a few useful utilities. + +## Comparisons + +Two buffers can be compared for differences using the `compare` method: + +```dart +final diff = pixels1.compare(pixels2); +print(diff); +``` + +The `compare` method returns a `Comparison` object, which can be used to +summarize, iterate, or visualize the differences between two buffers. Pxl itself +uses this method in its test suite to compare expected and actual results for +pixel operations. + +```dart +final diff = pixels1.compare(pixels2); +if (diff.difference < 0.01) { + print('The buffers are nearly identical.'); +} else { + print('The buffers differ by ${diff.difference}.'); +} +``` + +## Codecs + +Converting pixel buffers to popular formats is best done with a full-featured +library like `image` or `dart:ui`, but Pxl provides a few simple codecs for +converting pixel buffers to pixel-based formats like [PFM][], [PBM][], and can +even output as an uncompressed PNG with [uncompressedPngEncoder][]: + +```dart +import 'dart:io'; + +import 'package:pxl/pxl.dart'; + +void main() { + final image = IntPixels(8, 8); + image.fill(abgr8888.red); + + final bytes = uncompressedPngEncoder.convert(image); + File('example.png').writeAsBytesSync(bytes); +} +``` + +[uncompressedPngEncoder]: ../pxl/uncompressedPngEncoder-constant.html +[PFM]: http://netpbm.sourceforge.net/doc/pfm.html +[PBM]: http://netpbm.sourceforge.net/doc/pbm.html diff --git a/example.png b/example.png new file mode 100644 index 0000000..fe1872f Binary files /dev/null and b/example.png differ diff --git a/example/example.dart b/example/example.dart new file mode 100755 index 0000000..13d9360 --- /dev/null +++ b/example/example.dart @@ -0,0 +1,62 @@ +#!/usr/bin/env dart + +import 'dart:io'; +import 'dart:math' as math; + +import 'package:pxl/pxl.dart'; + +void main() { + const imageWidth = 256; + const imageHeight = 256; + final image = IntPixels(imageWidth, imageHeight); + + // Fill background with green + image.fill(abgr8888.green); + + // Draw a gradient sky + for (var y = 0; y < imageHeight / 2; y++) { + final blue = (y / (imageHeight / 2) * 255).toInt(); + image.fill( + abgr8888.create(blue: blue), + target: Rect.fromLTWH(0, y, imageWidth, 1), + ); + } + + // Draw a pixelated yellow sun + final sunCenter = Pos.floor(imageWidth / 2, imageHeight / 4); + const sunRadius = 32; + for (var y = sunCenter.y - sunRadius; y <= sunCenter.y + sunRadius; y++) { + for (var x = sunCenter.x - sunRadius; x <= sunCenter.x + sunRadius; x++) { + final distance = sunCenter.distanceTo(Pos(x, y)); + if (distance <= sunRadius) { + final intensity = 255 - (distance / sunRadius * 255).toInt(); + image.set( + Pos(x, y), + abgr8888.create(red: 255 - intensity, green: 255 - intensity), + ); + } + } + } + + // Draw some pixelated clouds + final rng = math.Random(); + for (var i = 0; i < 32; i++) { + final cloudX = rng.nextInt(imageWidth - 10); + final cloudY = rng.nextInt(imageHeight ~/ 2 - 5); + final cloudWidth = rng.nextInt(10) + 5; + final cloudHeight = rng.nextInt(5) + 3; + + for (var y = cloudY; y < cloudY + cloudHeight; y++) { + for (var x = cloudX; x < cloudX + cloudWidth; x++) { + if (rng.nextDouble() < 0.7) { + // Some randomness for cloud shape + image.set(Pos(x, y), abgr8888.white); + } + } + } + } + + // Save the image + final bytes = uncompressedPngEncoder.convert(image); + File('example.png').writeAsBytesSync(bytes); +} diff --git a/lib/pxl.dart b/lib/pxl.dart index 08f0071..6d3c04a 100644 --- a/lib/pxl.dart +++ b/lib/pxl.dart @@ -1,4 +1,20 @@ +/// Fixed-size buffer of [Pixels], with customizable [PixelFormat]s, +/// [BlendMode]s, and more. +/// +/// - Create and manipulate in-memory [IntPixels] or [FloatPixels] buffers +/// - Define and convert between [PixelFormat]s. +/// - Palette-based indexed pixel formats with [IndexedFormat]. +/// - Buffer-to-buffer blitting with automatic format conversion and +/// [BlendMode]s. +/// - Region-based pixel manipulation, replacement, and copying. +/// +/// +library; + export 'package:pxl/src/blend.dart'; export 'package:pxl/src/buffer.dart'; +export 'package:pxl/src/codec.dart'; export 'package:pxl/src/format.dart'; export 'package:pxl/src/geometry.dart'; diff --git a/lib/src/blend.dart b/lib/src/blend.dart index 12a884e..114f73f 100644 --- a/lib/src/blend.dart +++ b/lib/src/blend.dart @@ -2,7 +2,6 @@ import 'dart:typed_data'; import 'package:meta/meta.dart'; import 'package:pxl/src/format.dart'; -import 'package:pxl/src/internal.dart'; part 'blend/porter_duff.dart'; @@ -18,24 +17,9 @@ part 'blend/porter_duff.dart'; /// on the canvas. The algorithm then returns a new color that is the result of /// blending the two colors. /// -/// ## SIMD -/// -/// Some blend modes can optionally use [SIMD optimizations][] by setting the -/// `pxl.SIMD` Dart compilation environment variable: -/// -/// [SIMD optimizations]: https://en.wikipedia.org/wiki/SIMD -/// -/// ```sh -/// dart compile -Dpxl.SIMD=true -/// ``` -/// -/// This will use the [Float32x4] class to perform the blending calculations, -/// which can be faster than using scalar but should be tested for performance -/// and correctness. -/// -/// See for more details. +/// {@category Blending} @immutable -abstract mixin class BlendMode { +abstract interface class BlendMode { /// Destination pixels covered by the source are cleared to 0. static const BlendMode clear = PorterDuff( PorterDuff.zero, diff --git a/lib/src/blend/porter_duff.dart b/lib/src/blend/porter_duff.dart index e3228f8..7ec6b12 100644 --- a/lib/src/blend/porter_duff.dart +++ b/lib/src/blend/porter_duff.dart @@ -11,7 +11,9 @@ part of '../blend.dart'; /// // Creates a custom blend mode that always returns zero. /// final customBlendMode = PorterDuff(PorterDuff.zero, PorterDuff.one); /// ``` -final class PorterDuff with BlendMode { +/// +/// {@category Blending} +final class PorterDuff implements BlendMode { /// Always returns zero (`0.0`). static double zero(double srcAlpha, double dstAlpha) => 0; @@ -52,6 +54,9 @@ final class PorterDuff with BlendMode { PixelFormat srcFormat, PixelFormat dstFormat, ) { + if (identical(srcFormat, floatRgba) && identical(dstFormat, floatRgba)) { + return _blendFloatRgba as T Function(S src, T dst); + } return (src, dst) { final srcRgba = srcFormat.toFloatRgba(src); final dstRgba = dstFormat.toFloatRgba(dst); @@ -61,33 +66,6 @@ final class PorterDuff with BlendMode { } Float32x4 _blendFloatRgba(Float32x4 src, Float32x4 dst) { - return useSimd - ? _blendFloat32x4SIMD(src, dst) - : _blendFloat32x4Scalar(src, dst); - } - - Float32x4 _blendFloat32x4Scalar(Float32x4 src, Float32x4 dst) { - final Float32x4( - x: sr, - y: sg, - z: sb, - w: sa, - ) = src; - final Float32x4( - x: dr, - y: dg, - z: db, - w: da, - ) = dst; - - final r = _src(sa, da) * sr + _dst(sa, da) * dr; - final g = _src(sa, da) * sg + _dst(sa, da) * dg; - final b = _src(sa, da) * sb + _dst(sa, da) * db; - final a = _src(sa, da) * sa + _dst(sa, da) * da; - return Float32x4(r, g, b, a); - } - - Float32x4 _blendFloat32x4SIMD(Float32x4 src, Float32x4 dst) { final srcA = Float32x4.splat(_src(src.w, dst.w)); final dstA = Float32x4.splat(_dst(src.w, dst.w)); return src * srcA + dst * dstA; diff --git a/lib/src/buffer.dart b/lib/src/buffer.dart index e5fc1a4..3ef480d 100644 --- a/lib/src/buffer.dart +++ b/lib/src/buffer.dart @@ -20,7 +20,9 @@ part 'buffer/pixels.dart'; /// /// In most cases buffers will be used ephemerally; [Pixels] is an actual /// representation of pixel data. -abstract mixin class Buffer { +/// +/// {@category Buffers} +abstract base mixin class Buffer { /// @nodoc const Buffer(); @@ -121,6 +123,8 @@ abstract mixin class Buffer { /// Returns a lazy buffer that converts pixels to the given [format]. /// + /// If `this.format == format`, the buffer is returned as-is. + /// /// ## Example /// /// ```dart @@ -159,10 +163,10 @@ abstract mixin class Buffer { /// abgr8888.red, abgr8888.green, abgr8888.blue, /// ])); /// - /// final clipped = buffer.mapClipped(Rect.fromLTWH(1, 1, 2, 2)); + /// final clipped = buffer.mapRect(Rect.fromLTWH(1, 1, 2, 2)); /// print(clipped.data); // [0xFF00FF00, 0xFF0000FF] /// ``` - Buffer mapClipped(Rect bounds) { + Buffer mapRect(Rect bounds) { return _ClippedBuffer(this, bounds.intersect(this.bounds)); } diff --git a/lib/src/buffer/pixels.dart b/lib/src/buffer/pixels.dart index 7b5c119..c6ab809 100644 --- a/lib/src/buffer/pixels.dart +++ b/lib/src/buffer/pixels.dart @@ -7,6 +7,9 @@ part of '../buffer.dart'; /// but cannot be extended or implemented (similar to [TypedDataList]). /// /// In most cases either [IntPixels] or [FloatPixels] will be used directly. +/// +/// {@category Buffers} +/// {@category Blending} abstract final class Pixels with Buffer { /// @nodoc const Pixels({ @@ -264,7 +267,8 @@ abstract final class Pixels with Buffer { /// corner of the `this` buffer. If there is not sufficient space in the /// target buffer, the source rectangle will be clipped to fit `this`. /// - /// The pixels are copied as-is, without any conversion or blending. + /// The pixels are copied as-is, **without any conversion or blending**; see + /// [blit] for converting and blending pixel data. /// /// ## Example /// @@ -320,7 +324,8 @@ abstract final class Pixels with Buffer { /// rectangle will be copied starting at that position. If there is not /// sufficient space in the target buffer, the behavior is undefined. /// - /// The pixels are copied as-is, without any conversion or blending. + /// The pixels are copied as-is, **without any conversion or blending**; see + /// [blit] for converting and blending pixel data. /// /// ## Example /// diff --git a/lib/src/buffer/pixels_float.dart b/lib/src/buffer/pixels_float.dart index f49fa98..aa23005 100644 --- a/lib/src/buffer/pixels_float.dart +++ b/lib/src/buffer/pixels_float.dart @@ -9,6 +9,8 @@ part of '../buffer.dart'; /// formats. /// /// The default [format] is [floatRgba]. +/// +/// {@category Buffers} final class FloatPixels extends Pixels { /// Creates a new buffer of multi-channel floating point pixel data. /// @@ -31,6 +33,9 @@ final class FloatPixels extends Pixels { } if (data == null) { data = Float32x4List(width * height); + if (format.zero.equal(Float32x4.zero()).signMask != 0) { + data.fillRange(0, data.length, format.zero); + } } else if (data.length != width * height) { throw RangeError.value( data.length, diff --git a/lib/src/buffer/pixels_int.dart b/lib/src/buffer/pixels_int.dart index ac8190f..c988c06 100644 --- a/lib/src/buffer/pixels_int.dart +++ b/lib/src/buffer/pixels_int.dart @@ -6,6 +6,8 @@ part of '../buffer.dart'; /// between 8 and 32 bits of data. /// /// The default [format] is [abgr8888]. +/// +/// {@category Buffers} final class IntPixels extends Pixels { /// Creates a new buffer of integer-based pixel data. /// @@ -28,6 +30,9 @@ final class IntPixels extends Pixels { } if (data == null) { data = newIntBuffer(bytes: format.bytesPerPixel, length: width * height); + if (format.zero != 0) { + data.fillRange(0, data.length, format.zero); + } } else if (data.length != width * height) { throw RangeError.value( data.length, diff --git a/lib/src/codec.dart b/lib/src/codec.dart new file mode 100644 index 0000000..8a33066 --- /dev/null +++ b/lib/src/codec.dart @@ -0,0 +1 @@ +export 'codec/unpng.dart'; diff --git a/lib/src/codec/unpng.dart b/lib/src/codec/unpng.dart new file mode 100644 index 0000000..c69fc95 --- /dev/null +++ b/lib/src/codec/unpng.dart @@ -0,0 +1,232 @@ +/// Inspired from . +/// +/// See also: +/// - +library; + +import 'dart:convert'; +import 'dart:typed_data'; + +import 'package:pxl/pxl.dart'; + +/// Encodes a buffer of integer pixel data as an uncompressed RGBA PNG image. +/// +/// This encoder is intentionally minimal and does not support all features of +/// the PNG format. It's primary purpose is to provide a zero-dependency way to +/// visualize and persist pixel data in a standard format, i.e. for debugging +/// purposes, and it's recommended to use a dedicated library for more advanced +/// use cases. +/// +/// ## Limitations +/// +/// While the produced image is a valid PNG file, consider the following: +/// +/// - The maximum resolution supported is 8192x8192. +/// - No compression is performed. +/// - Interlacing is not supported. +/// - Only a single `IDAT` chunk is written. +/// - Only 8-bit color depth is supported. +/// - Pixel formats are converted to [rgba8888] before encoding. +/// - No additional metadata or chunks are supported. +/// +/// ## Alternatives +/// +/// - In Flutter, prefer [`instantiateImageCodec`][1]. +/// - In the browser, prefer [`HTMLCanvasElement toBlob`][2]. +/// - In the standalone Dart VM, consider using [`package:image`][3]. +/// +/// [1]: https://api.flutter.dev/flutter/dart-ui/instantiateImageCodec.html +/// [2]: https://developer.mozilla.org/en-US/docs/Web/API/HTMLCanvasElement/toBlob +/// [3]: https://pub.dev/documentation/image/latest/image/PngEncoder-class.html +const uncompressedPngEncoder = UncompressedPngEncoder._(); + +/// Encodes a buffer of pixel data as an uncompressed RGBA PNG image with 8-bit +/// color depth. +/// +/// A singleton instance of this class is available as [uncompressedPngEncoder]. +final class UncompressedPngEncoder extends Converter, List> { + const UncompressedPngEncoder._(); + + /// The maximum resolution we support is 8192x8192. + static const _maxResolution = 0x2000; + + @override + Uint8List convert(Buffer input) { + // Verify the resolution + RangeError.checkValueInInterval( + input.width, + 1, + _maxResolution, + 'input.width', + ); + RangeError.checkValueInInterval( + input.height, + 1, + _maxResolution, + 'input.height', + ); + + return _encodeUncompressedPng(input.mapConvert(rgba8888)); + } +} + +/// . +final _pngSignature = Uint8List(8) + ..[0] = 0x89 + ..[1] = 0x50 + ..[2] = 0x4E + ..[3] = 0x47 + ..[4] = 0x0D + ..[5] = 0x0A + ..[6] = 0x1A + ..[7] = 0x0A; + +Uint8List _encodeUncompressedPng(Buffer pixels) { + assert( + pixels.format == rgba8888, + 'Unsupported pixel format: ${pixels.format}', + ); + final output = BytesBuilder(copy: false); + + // Write the PNG signature (https://www.w3.org/TR/png-3/#3PNGsignature). + output.add(_pngSignature); + + // Writes a chunk to the PNG buffer. + void writeChunk(String type, Uint8List data) { + // Write the length of the data. + output.addWord(data.length); + + // Store the current offset for the checksum. + final offset = output.length; + + // Write the chunk type. + output.add(utf8.encode(type)); + + // Write the data. + output.add(data); + + // Compute and write a CRC32 checksum excluding the length. + final view = Uint8List.view(output.toBytes().buffer, offset); + output.addWord(_crc32(view)); + } + + // Write the IHDR chunk (https://www.w3.org/TR/png-3/#11IHDR). + final ihdr = Uint8List(13) + ..buffer.asByteData().setUint32(0, pixels.width) // .3: Width + ..buffer.asByteData().setUint32(4, pixels.height) // .7: Height + ..[8] = 8 // 08: Bit depth + ..[9] = 6 // 09: Color type (RGBA) + ..[10] = 0 // 10: Compression method + ..[11] = 0 // 11: Filter method + ..[12] = 0; // 12: Interlace method + writeChunk('IHDR', ihdr); + + // Write the IDAT chunk (https://www.w3.org/TR/png-3/#11IDAT). + final idat = BytesBuilder(copy: false) + ..addByte(0x78) // CMF (0x78 means 32K window size) + ..addByte(0x01); // FLG (0x01 means no preset dictionary) + + // Hard-coded for 8-bit RGBA pixels. + final baseStride = pixels.width * 4; + final rowMagicLength = baseStride + 1; + + final rowMagicHeader = Uint8List(8) + ..[0] = 0x02 + ..[1] = 0x08 + ..[2] = 0x00 + ..buffer.asByteData().setUint16(3, rowMagicLength, Endian.little) + ..buffer.asByteData().setUint16(5, rowMagicLength ^ 0xFFFF, Endian.little) + ..[7] = 0x00; + + var adlerSum = 1; + for (var y = 0; y < pixels.height; y++) { + // Write the row data. + final row = Uint8List(baseStride); + + // Copy the pixel data into the row buffer. It's already in RGBA format. + final dat = row.buffer.asUint32List(); + dat.setRange(0, pixels.width, pixels.data, y * pixels.width); + + // Update the Adler-32 checksum for the filter type and row data. + adlerSum = _adler32(const [0], adlerSum); + adlerSum = _adler32(row, adlerSum); + + // Write the header and row data. + idat.add(rowMagicHeader); + idat.add(row); + } + + // Add the final deflate block of zero length, plus adler32 checksum. + idat.addByte(0x02); + idat.addByte(0x08); + idat.addByte(0x30); + idat.addByte(0x00); + idat.addWord(adlerSum); + writeChunk('IDAT', idat.toBytes()); + + // Write the IEND chunk (https://www.w3.org/TR/png-3/#11IEND). + writeChunk('IEND', Uint8List(0)); + + return output.toBytes(); +} + +extension on BytesBuilder { + /// Adds a 32-bit word to the buffer in the given [endian] order. + void addWord(int word, [Endian endian = Endian.big]) { + if (endian == Endian.big) { + _addWordBigEndian(word); + } else { + _addWordLittleEndian(word); + } + } + + void _addWordBigEndian(int word) { + addByte(word >> 24 & 0xFF); + addByte(word >> 16 & 0xFF); + addByte(word >> 8 & 0xFF); + addByte(word & 0xFF); + } + + void _addWordLittleEndian(int word) { + addByte(word & 0xFF); + addByte(word >> 8 & 0xFF); + addByte(word >> 16 & 0xFF); + addByte(word >> 24 & 0xFF); + } +} + +/// A table of lazily computed CRC-32 values for all 8-bit unsigned integers. +final _crc32Table = () { + final table = Uint32List(256); + for (var i = 0; i < 256; i++) { + var c = i; + for (var k = 0; k < 8; k++) { + if ((c & 1) == 1) { + c = 0xedb88320 ^ (c >> 1); + } else { + c = c >> 1; + } + } + table[i] = c; + } + return table; +}(); + +/// Computes the CRC32 checksum of a list of bytes. +int _crc32(Uint8List bytes) { + var crc = 0xFFFFFFFF; + for (final byte in bytes) { + crc = _crc32Table[(crc ^ byte) & 0xFF] ^ (crc >> 8); + } + return crc ^ 0xFFFFFFFF; +} + +int _adler32(List bytes, int seed) { + var a = seed & 0xFFFF; + var b = (seed >> 16) & 0xFFFF; + for (final y in bytes) { + a = (a + y) % 65521; + b = (b + a) % 65521; + } + return (b << 16) | a; +} diff --git a/lib/src/format.dart b/lib/src/format.dart index 62daace..dba7faa 100644 --- a/lib/src/format.dart +++ b/lib/src/format.dart @@ -1,10 +1,17 @@ import 'dart:typed_data'; import 'package:meta/meta.dart'; +import 'package:pxl/src/internal.dart'; part 'format/abgr8888.dart'; +part 'format/argb8888.dart'; part 'format/float_rgba.dart'; +part 'format/indexed.dart'; +part 'format/rgb.dart'; part 'format/rgba.dart'; +part 'format/rgba8888.dart'; +part 'format/system8.dart'; +part 'format/system256.dart'; /// Describes the organization and characteristics of pixel data [P] in memory. /// @@ -13,10 +20,9 @@ part 'format/rgba.dart'; /// of pixels. Several built-in pixel formats are provided, such as [abgr8888] /// and [floatRgba]. /// -/// > [!TIP] -/// > While the API evolves, it is not possible to create custom pixel formats. +/// /// {@category Pixel Formats} @immutable -abstract final class PixelFormat { +abstract base mixin class PixelFormat { /// @nodoc const PixelFormat(); @@ -36,6 +42,13 @@ abstract final class PixelFormat { /// Number of bytes required to store a single pixel in memory. int get bytesPerPixel; + /// Returns the distance between two pixels in the pixel format. + /// + /// What constitutes distance is not defined by this method, and is up to the + /// pixel format to define. For example, both [abgr8888] and [floatRgba] use + /// cartesian distance to compare pixels. + double distance(P a, P b); + /// The zero, or minimum, value for the pixel format. /// /// This value typically represents transparent and/or empty (black) pixels. diff --git a/lib/src/format/abgr8888.dart b/lib/src/format/abgr8888.dart index a296f4c..1f718a3 100644 --- a/lib/src/format/abgr8888.dart +++ b/lib/src/format/abgr8888.dart @@ -3,99 +3,34 @@ part of '../format.dart'; /// 32-bit ABGR pixel format with four 8-bit channels. /// /// This is the package's canonical integer-based pixel format. +/// +/// Colors in this format are represented as follows: +/// +/// Color | Value +/// -------------|------ +/// [Rgba.red] | `0xFFFF0000` +/// [Rgba.green] | `0xFF00FF00` +/// [Rgba.blue] | `0xFF0000FF` +/// [Rgba.white] | `0xFFFFFFFF` +/// [Rgba.black] | `0xFF000000` +/// +/// {@category Pixel Formats} const abgr8888 = Abgr8888._(); /// 32-bit ABGR pixel format with four 8-bit channels. /// /// For a singleton instance of this class, and further details, see [abgr8888]. -final class Abgr8888 extends Rgba { - const Abgr8888._(); +/// +/// {@category Pixel Formats} +final class Abgr8888 extends _Rgba8x4Int { + const Abgr8888._() : super.fromRgbaOffsets(16, 8, 0, 24); @override String get name => 'ABGR8888'; - @override - int get bytesPerPixel => Uint32List.bytesPerElement; - - @override - int get zero => 0x00000000; - - @override - int get max => 0xFFFFFFFF; - - @override - int clamp(int pixel) => pixel & max; - - @override - int copyWith( - int pixel, { - int? red, - int? green, - int? blue, - int? alpha, - }) { - var output = pixel; - if (red != null) { - output = (output & 0x00FFFFFF) | (red & 0xFF) << 24; - } - if (green != null) { - output = (output & 0xFF00FFFF) | (green & 0xFF) << 16; - } - if (blue != null) { - output = (output & 0xFFFF00FF) | (blue & 0xFF) << 8; - } - if (alpha != null) { - output = (output & 0xFFFFFF00) | (alpha & 0xFF); - } - return output; - } - - @override - int copyWithNormalized( - int pixel, { - double? red, - double? green, - double? blue, - double? alpha, - }) { - var output = pixel; - if (red != null) { - output &= 0x00FFFFFF; - output |= (red.clamp(0.0, 1.0) * 0xFF).floor() << 24; - } - if (green != null) { - output &= 0xFF00FFFF; - output |= (green.clamp(0.0, 1.0) * 0xFF).floor() << 16; - } - if (blue != null) { - output &= 0xFFFF00FF; - output |= (blue.clamp(0.0, 1.0) * 0xFF).floor() << 8; - } - if (alpha != null) { - output &= 0xFFFFFF00; - output |= (alpha.clamp(0.0, 1.0) * 0xFF).floor(); - } - return output; - } - - @override - int get maxRed => 0xFF; - - @override - int get maxGreen => 0xFF; - - @override - int get maxBlue => 0xFF; - - @override - int get maxAlpha => 0xFF; - @override int fromAbgr8888(int pixel) => pixel; @override int toAbgr8888(int pixel) => pixel; - - @override - Float32x4 toFloatRgba(int pixel) => floatRgba.fromAbgr8888(pixel); } diff --git a/lib/src/format/argb8888.dart b/lib/src/format/argb8888.dart new file mode 100644 index 0000000..0d980e8 --- /dev/null +++ b/lib/src/format/argb8888.dart @@ -0,0 +1,36 @@ +part of '../format.dart'; + +/// 32-bit ARGB pixel format with four 8-bit channels. +/// +/// This is the most common pixel format for images with an alpha channel. +/// +/// Colors in this format are represented as follows: +/// +/// Color | Value +/// -------------|------ +/// [Rgba.red] | `0xFF0000FF` +/// [Rgba.green] | `0xFF00FF00` +/// [Rgba.blue] | `0xFFFF0000` +/// [Rgba.white] | `0xFFFFFFFF` +/// [Rgba.black] | `0xFF000000` +/// +/// {@category Pixel Formats} +const argb8888 = Argb8888._(); + +/// 32-bit ARGB pixel format with four 8-bit channels. +/// +/// For a singleton instance of this class, and further details, see [argb8888]. +/// +/// {@category Pixel Formats} +final class Argb8888 extends _Rgba8x4Int { + const Argb8888._() : super.fromRgbaOffsets(0, 8, 16, 24); + + @override + String get name => 'ARGB8888'; + + @override + int fromAbgr8888(int pixel) => swapLane13(pixel); + + @override + int toAbgr8888(int pixel) => swapLane13(pixel); +} diff --git a/lib/src/format/float_rgba.dart b/lib/src/format/float_rgba.dart index 9dfa7f1..b41cff5 100644 --- a/lib/src/format/float_rgba.dart +++ b/lib/src/format/float_rgba.dart @@ -3,12 +3,16 @@ part of '../format.dart'; /// A 128-bit floating-point RGBA pixel format with four 32-bit channels. /// /// This is the package's canonical floating-point pixel format. +/// +/// {@category Pixel Formats} const floatRgba = FloatRgba._(); /// A 128-bit floating-point RGBA pixel format with four 32-bit channels. /// /// For a singleton instance of this class, and further details, see /// [floatRgba]. +/// +/// {@category Pixel Formats} final class FloatRgba extends Rgba { const FloatRgba._(); @@ -27,6 +31,13 @@ final class FloatRgba extends Rgba { @override Float32x4 clamp(Float32x4 pixel) => pixel.clamp(zero, max); + @override + double distance(Float32x4 a, Float32x4 b) { + var d = a - b; + d *= d; + return d.x + d.y + d.z + d.w; + } + @override Float32x4 copyWith( Float32x4 pixel, { @@ -36,10 +47,10 @@ final class FloatRgba extends Rgba { double? alpha, }) { final output = Float32x4( - red ?? pixel.x, - green ?? pixel.y, - blue ?? pixel.z, - alpha ?? pixel.w, + red ?? getRed(pixel), + green ?? getGreen(pixel), + blue ?? getBlue(pixel), + alpha ?? getAlpha(pixel), ); return output.clamp(zero, max); } @@ -55,6 +66,18 @@ final class FloatRgba extends Rgba { return copyWith(pixel, red: red, green: green, blue: blue, alpha: alpha); } + @override + double get minRed => 0.0; + + @override + double get minGreen => 0.0; + + @override + double get minBlue => 0.0; + + @override + double get minAlpha => 0.0; + @override double get maxRed => 1.0; @@ -68,13 +91,19 @@ final class FloatRgba extends Rgba { double get maxAlpha => 1.0; @override - Float32x4 fromAbgr8888(int pixel) { - final a = (pixel & 0xFF) / 0xFF; - final b = ((pixel >> 8) & 0xFF) / 0xFF; - final g = ((pixel >> 16) & 0xFF) / 0xFF; - final r = ((pixel >> 24) & 0xFF) / 0xFF; - return Float32x4(r, g, b, a); - } + double getRed(Float32x4 pixel) => pixel.x; + + @override + double getGreen(Float32x4 pixel) => pixel.y; + + @override + double getBlue(Float32x4 pixel) => pixel.z; + + @override + double getAlpha(Float32x4 pixel) => pixel.w; + + @override + Float32x4 fromAbgr8888(int pixel) => abgr8888.toFloatRgba(pixel); @override int toAbgr8888(Float32x4 pixel) { diff --git a/lib/src/format/indexed.dart b/lib/src/format/indexed.dart new file mode 100644 index 0000000..bb1ad09 --- /dev/null +++ b/lib/src/format/indexed.dart @@ -0,0 +1,236 @@ +part of '../format.dart'; + +/// A pixel format that uses a predefined _palette_ to index colors. +/// +/// Given a linear list of colors, an indexed pixel format uses a single integer +/// value to represent a color in the palette. This is useful for reducing the +/// memory footprint of images, especially when the number of unique colors is +/// small. +/// +/// ## Example +/// +/// ```dart +/// // Creating a simple system palette with 8 colors. +/// final palette = [ +/// abgr8888.create(red: 0xFF, green: 0x00, blue: 0x00), +/// abgr8888.create(red: 0x00, green: 0xFF, blue: 0x00), +/// abgr8888.create(red: 0x00, green: 0x00, blue: 0xFF), +/// abgr8888.create(red: 0xFF, green: 0xFF, blue: 0x00), +/// abgr8888.create(red: 0xFF, green: 0x00, blue: 0xFF), +/// abgr8888.create(red: 0x00, green: 0xFF, blue: 0xFF), +/// abgr8888.create(red: 0xFF, green: 0xFF, blue: 0xFF), +/// abgr8888.create(red: 0x00, green: 0x00, blue: 0x00), +/// ]; +/// +/// // Creating an 8-bit indexed pixel format with the palette. +/// final indexed = IndexedFormat.bits8(palette, format: abgr8888); +/// +/// // Converting a color to an index. +/// final index = indexed.fromAbgr8888(abgr8888.create(red: 0xFF, green: 0x00, blue: 0x00)); +/// +/// // Converting an index to a color. +/// final color = indexed.toAbgr8888(index); +/// ``` +/// +/// See [system8] for a predefined palette of common RGB colors. +/// +/// {@category Pixel Formats} +final class IndexedFormat

extends PixelFormat { + /// Creates an 8-bit indexed pixel format with the given [palette]. + /// + /// The [format] is used to convert between the palette colors and the + /// indexed values. + /// + /// The [zero] and [max] values are used to clamp the indexed values to the + /// valid range of the palette; if omitted, they default to `0` and + /// `palette.length - 1` respectively. + factory IndexedFormat.bits8( + Iterable

palette, { + required PixelFormat format, + String? name, + int? zero, + int? max, + }) { + final list = List.of(palette); + if (list.length > 256) { + throw ArgumentError.value( + list.length, + 'palette.length', + 'Must be less than or equal to 256 for 8-bit indexed format.', + ); + } + return IndexedFormat._( + List.of(palette), + format, + zero: zero ?? 0, + max: max ?? palette.length - 1, + bytesPerPixel: 1, + name: name ?? 'INDEXED_${format.name}_${palette.length}', + ); + } + + /// Creates an 16-bit indexed pixel format with the given [palette]. + /// + /// The [format] is used to convert between the palette colors and the + /// indexed values. + /// + /// The [zero] and [max] values are used to clamp the indexed values to the + /// valid range of the palette; if omitted, they default to `0` and + /// `palette.length - 1` respectively. + factory IndexedFormat.bits16( + Iterable

palette, { + required PixelFormat format, + String? name, + int? zero, + int? max, + }) { + final list = List.of(palette); + if (list.length > 65536) { + throw ArgumentError.value( + list.length, + 'palette.length', + 'Must be less than or equal to 65536 for 16-bit indexed format.', + ); + } + return IndexedFormat._( + List.of(palette), + format, + zero: zero ?? 0, + max: max ?? palette.length - 1, + bytesPerPixel: 2, + name: name ?? 'INDEXED_${format.name}_${palette.length}', + ); + } + + /// Creates an 32-bit indexed pixel format with the given [palette]. + /// + /// The [format] is used to convert between the palette colors and the + /// indexed values. + /// + /// The [zero] and [max] values are used to clamp the indexed values to the + /// valid range of the palette; if omitted, they default to `0` and + /// `palette.length - 1` respectively. + factory IndexedFormat.bits32( + Iterable

palette, { + required PixelFormat format, + String? name, + int? zero, + int? max, + }) { + final list = List.of(palette); + if (list.length > 4294967296) { + throw ArgumentError.value( + list.length, + 'palette.length', + 'Must be less than or equal to 4294967296 for 32-bit indexed format.', + ); + } + return IndexedFormat._( + List.of(palette), + format, + zero: zero ?? 0, + max: max ?? palette.length - 1, + bytesPerPixel: 4, + name: name ?? 'INDEXED_${format.name}_${palette.length}', + ); + } + + const IndexedFormat._( + this._palette, + this._format, { + required this.zero, + required this.max, + required this.bytesPerPixel, + required this.name, + }); + + final List

_palette; + final PixelFormat _format; + + @override + final String name; + + @override + final int bytesPerPixel; + + /// Length of the palette. + int get length => _palette.length; + + /// The zero, or minimum, value for the pixel format. + /// + /// This value typically represents transparent and/or empty (black) pixels, + /// but for an [IndexedFormat], it represents the index of the arbitrary color + /// in the palette which **may not be transparent black**. + @override + final int zero; + + /// The maximum value for the pixel format. + /// + /// This value typically represents opaque and/or full (white) pixels, but for + /// an [IndexedFormat], it represents the index of an arbitrary color in the + /// palette which **may not be opaque white**. + @override + final int max; + + /// Clamps a [pixel] to the valid range of values for the pixel format. + @override + int clamp(int pixel) => pixel.clamp(0, length - 1); + + @override + int copyWith(int pixel) => pixel; + + @override + int copyWithNormalized(int pixel) => pixel; + + @override + double distance(int a, int b) => (a - b).abs().toDouble(); + + /// Converts a pixel in the [abgr8888] pixel format to `this` pixel format. + /// + /// The method returns the closest color in the palette to the given [pixel]. + @override + int fromAbgr8888(int pixel) { + return _findClosestIndex(_format.fromAbgr8888(pixel)); + } + + /// Converts a pixel in the [floatRgba] pixel format to `this` pixel format. + /// + /// The method returns the closest color in the palette to the given [pixel]. + @override + int fromFloatRgba(Float32x4 pixel) { + return _findClosestIndex(_format.fromFloatRgba(pixel)); + } + + /// Returns the color in the palette at the given [index]. + P operator [](int index) => _lookupByIndex(index); + + int _findClosestIndex(P color) { + var closestIndex = 0; + var closestDistance = double.infinity; + for (var i = 0; i < length; i++) { + final distance = _format.distance(_palette[i], color); + if (distance < closestDistance) { + closestIndex = i; + closestDistance = distance; + } + } + return closestIndex; + } + + P _lookupByIndex(int index) { + if (index < 0 || index >= length) { + return _format.zero; + } + return _palette[index]; + } + + @override + int toAbgr8888(int pixel) { + return _format.toAbgr8888(_lookupByIndex(pixel)); + } + + @override + Float32x4 toFloatRgba(int pixel) { + return _format.toFloatRgba(_lookupByIndex(pixel)); + } +} diff --git a/lib/src/format/rgb.dart b/lib/src/format/rgb.dart new file mode 100644 index 0000000..06887f4 --- /dev/null +++ b/lib/src/format/rgb.dart @@ -0,0 +1,157 @@ +part of '../format.dart'; + +/// Base API for pixel formats that have red, green, and blue channels. +/// +/// Additional RGBA-specific methods are provided: +/// - [copyWith] and [copyWithNormalized] for replacing channel values; +/// - [maxRed], [maxGreen], and [maxBlue] for the maximum channel values; +/// - [black], [white], [red], [green], [blue], [yellow], [cyan], and [magenta] +/// for common colors. +/// +/// In addition, [fromFloatRgba] is implemented using [copyWithNormalized]. +/// +/// {@category Pixel Formats} +abstract final class Rgb extends PixelFormat { + /// @nodoc + const Rgb(); + + /// Creates a new pixel with the given channel values. + /// + /// The [red], [green], and [blue] values are optional. + /// + /// If omitted, the corresponding channel value is set to the minimum value. + /// + /// ## Example + /// + /// ```dart + /// // Creating a full red pixel. + /// final pixel = rgb.create(red: 0xFF); + /// ``` + P create({ + C? red, + C? green, + C? blue, + }) { + return copyWith( + zero, + red: red ?? minRed, + green: green ?? minGreen, + blue: blue ?? minBlue, + ); + } + + @override + P copyWith( + P pixel, { + C? red, + C? green, + C? blue, + }); + + /// Creates a new pixel with the given channel values normalized to the range + /// `[0.0, 1.0]`. + /// + /// The [red], [green], and [blue] values are optional. + /// + /// If omitted, the corresponding channel value is set to `0.0`. + /// + /// ## Example + /// + /// ```dart + /// // Creating a full red pixel. + /// final pixel = rgb.createNormalized(red: 1.0); + /// ``` + P createNormalized({ + double red = 0.0, + double green = 0.0, + double blue = 0.0, + }) { + return copyWithNormalized( + zero, + red: red, + green: green, + blue: blue, + ); + } + + @override + P copyWithNormalized( + P pixel, { + double? red, + double? green, + double? blue, + }); + + /// The minimum value for the red channel. + C get minRed; + + /// The minimum value for the green channel. + C get minGreen; + + /// The minimum value for the blue channel. + C get minBlue; + + /// The maximum value for the red channel. + C get maxRed; + + /// The maximum value for the green channel. + C get maxGreen; + + /// The maximum value for the blue channel. + C get maxBlue; + + /// Black color with full transparency. + P get black => zero; + + /// White color with full transparency. + P get white => max; + + /// Red color with full transparency. + P get red { + return create(red: maxRed); + } + + /// Green color with full transparency. + P get green { + return create(green: maxGreen); + } + + /// Blue color with full transparency. + P get blue { + return create(blue: maxBlue); + } + + /// Yellow color with full transparency. + P get yellow { + return create(red: maxRed, green: maxGreen); + } + + /// Cyan color with full transparency. + P get cyan { + return create(green: maxGreen, blue: maxBlue); + } + + /// Magenta color with full transparency. + P get magenta { + return create(red: maxRed, blue: maxBlue); + } + + /// Returns the red channel value of the [pixel]. + C getRed(P pixel); + + /// Returns the green channel value of the [pixel]. + C getGreen(P pixel); + + /// 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, + ); + } +} diff --git a/lib/src/format/rgba.dart b/lib/src/format/rgba.dart index a3c90e5..ab80b52 100644 --- a/lib/src/format/rgba.dart +++ b/lib/src/format/rgba.dart @@ -10,20 +10,32 @@ part of '../format.dart'; /// for common colors. /// /// In addition, [fromFloatRgba] is implemented using [copyWithNormalized]. -abstract final class Rgba extends PixelFormat { +/// +/// {@category Pixel Formats} +abstract final class Rgba extends Rgb { /// @nodoc const Rgba(); + @override + P get black => create(); + /// Creates a new pixel with the given channel values. /// - /// The [red], [green], [blue], and [alpha] values are optional and default to - /// [zero] if not provided + /// The [red], [green], [blue], and [alpha] values are optional. + /// + /// If omitted, the corresponding channel value is set to the minimum value, + /// except for the alpha channel which is set to the maximum value by default. /// /// ## Example /// /// ```dart + /// // Creating a fully opaque red pixel. + /// final pixel = abgr8888.create(red: 0xFF); + /// + /// // Creating a semi-transparent red pixel. /// final pixel = abgr8888.create(red: 0xFF, alpha: 0x80); /// ``` + @override P create({ C? red, C? green, @@ -31,6 +43,22 @@ abstract final class Rgba extends PixelFormat { C? alpha, }) { return copyWith( + zero, + red: red ?? minRed, + green: green ?? minGreen, + blue: blue ?? minBlue, + alpha: alpha ?? maxAlpha, + ); + } + + @override + P createNormalized({ + double red = 0.0, + double green = 0.0, + double blue = 0.0, + double alpha = 1.0, + }) { + return copyWithNormalized( zero, red: red, green: green, @@ -39,6 +67,12 @@ abstract final class Rgba extends PixelFormat { ); } + /// The minimum value for the alpha channel. + C get minAlpha; + + /// The maximum value for the alpha channel. + C get maxAlpha; + @override P copyWith( P pixel, { @@ -57,72 +91,173 @@ abstract final class Rgba extends PixelFormat { double? alpha, }); - /// The maximum value for the red channel. - C get maxRed; + /// Returns the alpha channel value of the [pixel]. + C getAlpha(P pixel); + + @override + P fromFloatRgba(Float32x4 pixel) { + return copyWithNormalized( + zero, + red: pixel.x, + green: pixel.y, + blue: pixel.z, + alpha: pixel.w, + ); + } +} + +abstract final class _Rgba8x4Int extends Rgba { + const _Rgba8x4Int.fromRgbaOffsets( + int r, + int g, + int b, + int a, + ) : _maskAlpha = 0xFF << a, + _maskRed = 0xFF << r, + _maskGreen = 0xFF << g, + _maskBlue = 0xFF << b, + _offsetAlpha = a, + _offsetRed = r, + _offsetGreen = g, + _offsetBlue = b; - /// The maximum value for the green channel. - C get maxGreen; + final int _maskAlpha; + final int _maskRed; + final int _maskGreen; + final int _maskBlue; + final int _offsetAlpha; + final int _offsetRed; + final int _offsetGreen; + final int _offsetBlue; - /// The maximum value for the blue channel. - C get maxBlue; + @override + @nonVirtual + int get bytesPerPixel => Uint32List.bytesPerElement; - /// The maximum value for the alpha channel. - C get maxAlpha; + @override + @nonVirtual + int get zero => 0x00000000; + + @override + @nonVirtual + int get max => 0xFFFFFFFF; + + @override + @nonVirtual + int clamp(int pixel) => pixel & max; - /// Black color with full transparency. - P get black { - return copyWith(zero, alpha: maxAlpha); + @override + @nonVirtual + double distance(int a, int b) { + final dr = getRed(a) - getRed(b); + final dg = getGreen(a) - getGreen(b); + final db = getBlue(a) - getBlue(b); + final da = getAlpha(a) - getAlpha(b); + return (dr * dr + dg * dg + db * db + da * da).toDouble(); } - /// White color with full transparency. - P get white { + @override + @nonVirtual + int copyWithNormalized( + int pixel, { + double? red, + double? green, + double? blue, + double? alpha, + }) { return copyWith( - max, - red: maxRed, - green: maxGreen, - blue: maxBlue, - alpha: maxAlpha, + pixel, + red: red != null ? (red.clamp(0.0, 1.0) * 0xFF).floor() : null, + green: green != null ? (green.clamp(0.0, 1.0) * 0xFF).floor() : null, + blue: blue != null ? (blue.clamp(0.0, 1.0) * 0xFF).floor() : null, + alpha: alpha != null ? (alpha.clamp(0.0, 1.0) * 0xFF).floor() : null, ); } - /// Red color with full transparency. - P get red { - return copyWith(zero, red: maxRed, alpha: maxAlpha); - } + @override + @nonVirtual + int get minRed => 0x00; - /// Green color with full transparency. - P get green { - return copyWith(zero, green: maxGreen, alpha: maxAlpha); - } + @override + @nonVirtual + int get minGreen => 0x00; - /// Blue color with full transparency. - P get blue { - return copyWith(zero, blue: maxBlue, alpha: maxAlpha); - } + @override + @nonVirtual + int get minBlue => 0x00; - /// Yellow color with full transparency. - P get yellow { - return copyWith(zero, red: maxRed, green: maxGreen, alpha: maxAlpha); - } + @override + @nonVirtual + int get minAlpha => 0x00; - /// Cyan color with full transparency. - P get cyan { - return copyWith(zero, green: maxGreen, blue: maxBlue, alpha: maxAlpha); - } + @override + @nonVirtual + int get maxRed => 0xFF; - /// Magenta color with full transparency. - P get magenta { - return copyWith(zero, red: maxRed, blue: maxBlue, alpha: maxAlpha); + @override + @nonVirtual + int get maxGreen => 0xFF; + + @override + @nonVirtual + int get maxBlue => 0xFF; + + @override + @nonVirtual + int get maxAlpha => 0xFF; + + @override + @nonVirtual + Float32x4 toFloatRgba(int pixel) { + final r = getRed(pixel) / 255.0; + final g = getGreen(pixel) / 255.0; + final b = getBlue(pixel) / 255.0; + final a = getAlpha(pixel) / 255.0; + return Float32x4(r, g, b, a); } @override - P fromFloatRgba(Float32x4 pixel) { - return copyWithNormalized( - zero, - red: pixel.x, - green: pixel.y, - blue: pixel.z, - alpha: pixel.w, - ); + @nonVirtual + int copyWith( + int pixel, { + int? red, + int? green, + int? blue, + int? alpha, + }) { + var output = pixel; + if (red != null) { + output &= ~_maskRed; + output |= (red & 0xFF) << _offsetRed; + } + if (green != null) { + output &= ~_maskGreen; + output |= (green & 0xFF) << _offsetGreen; + } + if (blue != null) { + output &= ~_maskBlue; + output |= (blue & 0xFF) << _offsetBlue; + } + if (alpha != null) { + output &= ~_maskAlpha; + output |= (alpha & 0xFF) << _offsetAlpha; + } + return output; } + + @override + @nonVirtual + int getRed(int pixel) => (pixel >> _offsetRed) & 0xFF; + + @override + @nonVirtual + int getGreen(int pixel) => (pixel >> _offsetGreen) & 0xFF; + + @override + @nonVirtual + int getBlue(int pixel) => (pixel >> _offsetBlue) & 0xFF; + + @override + @nonVirtual + int getAlpha(int pixel) => (pixel >> _offsetAlpha) & 0xFF; } diff --git a/lib/src/format/rgba8888.dart b/lib/src/format/rgba8888.dart new file mode 100644 index 0000000..01a1af3 --- /dev/null +++ b/lib/src/format/rgba8888.dart @@ -0,0 +1,34 @@ +part of '../format.dart'; + +/// 32-bit RGBA pixel format with four 8-bit channels. +/// +/// Colors in this format are represented as follows: +/// +/// Color | Value +/// -------------|------ +/// [Rgba.red] | `0xFF0000FF` +/// [Rgba.green] | `0x00FF00FF` +/// [Rgba.blue] | `0x0000FFFF` +/// [Rgba.white] | `0xFFFFFFFF` +/// [Rgba.black] | `0x000000FF` +/// +/// {@category Pixel Formats} +const rgba8888 = Argb8888._(); + +/// 32-bit RGBA pixel format with four 8-bit channels. +/// +/// For a singleton instance of this class, and further details, see [rgba8888]. +/// +/// {@category Pixel Formats} +final class Rgba8888 extends _Rgba8x4Int { + const Rgba8888._() : super.fromRgbaOffsets(24, 16, 8, 0); + + @override + String get name => 'RGBA8888'; + + @override + int fromAbgr8888(int pixel) => swapLane13(pixel); + + @override + int toAbgr8888(int pixel) => swapLane13(pixel); +} diff --git a/lib/src/format/system256.dart b/lib/src/format/system256.dart new file mode 100644 index 0000000..21474b3 --- /dev/null +++ b/lib/src/format/system256.dart @@ -0,0 +1,26 @@ +part of '../format.dart'; + +/// A simple 256-color system palette of common RGB colors. +/// +/// The palette is generated automatically by combining 6 levels of red, green, +/// and blue channels to create 216 colors, followed by 40 shades of gray, for +/// a total of 256 unique colors. +/// +/// {@category Pixel Formats} +final system256 = IndexedFormat.bits8( + Iterable.generate(256, (i) { + final r = ((i ~/ 36) % 6) * 51; + final g = ((i ~/ 6) % 6) * 51; + final b = (i % 6) * 51; + + // Ensure we generate 256 unique colors + if (i >= 216) { + final gray = (i - 216) * (255 / 40).floor(); + return abgr8888.create(red: gray, green: gray, blue: gray); + } else { + return abgr8888.create(red: r, green: g, blue: b); + } + }), + format: abgr8888, + name: 'SYSTEM_256', +); diff --git a/lib/src/format/system8.dart b/lib/src/format/system8.dart new file mode 100644 index 0000000..19f7ed5 --- /dev/null +++ b/lib/src/format/system8.dart @@ -0,0 +1,32 @@ +part of '../format.dart'; + +/// A simple 8-color system palette of common RGB colors. +/// +/// The palette contains the following colors in the order they are listed: +/// +/// Index | Color +/// ------|------ +/// 0 | Black +/// 1 | Red +/// 2 | Green +/// 3 | Blue +/// 4 | Yellow +/// 5 | Cyan +/// 6 | Magenta +/// 7 | White +/// +/// {@category Pixel Formats} +final system8 = IndexedFormat.bits8( + [ + abgr8888.black, + abgr8888.red, + abgr8888.green, + abgr8888.blue, + abgr8888.yellow, + abgr8888.cyan, + abgr8888.magenta, + abgr8888.white, + ], + format: abgr8888, + name: 'SYSTEM_8', +); diff --git a/lib/src/geometry.dart b/lib/src/geometry.dart index 8410c1d..630940b 100644 --- a/lib/src/geometry.dart +++ b/lib/src/geometry.dart @@ -1 +1,19 @@ -export 'package:lodim/lodim.dart' show Pos, Rect; +import 'package:lodim/lodim.dart' as lodim; + +/// An immutable 2D fixed-point vector. +/// +/// This type is a re-export of [lodim.Pos]. +/// +/// See for more details. +/// +/// {@category Buffers} +typedef Pos = lodim.Pos; + +/// An immutable 2D fixed-point rectangle. +/// +/// This type is a re-export of [lodim.Rect]. +/// +/// See for more details. +/// +/// {@category Buffers} +typedef Rect = lodim.Rect; diff --git a/lib/src/internal.dart b/lib/src/internal.dart index a9dd5a5..b0d12d0 100644 --- a/lib/src/internal.dart +++ b/lib/src/internal.dart @@ -1,15 +1,8 @@ -// Allow Dart defines for this file. -// See for details. -// ignore_for_file: do_not_use_environment - import 'dart:typed_data'; /// Whether the current runtime is JavaScript. const isJsRuntime = identical(1, 1.0); -/// Whether SIMD optimizations should be used where possible. -final useSimd = const bool.fromEnvironment('pxl.SIMD'); - /// Disables bounds checking for the given function. const unsafeNoBoundsChecks = isJsRuntime ? pragma('dart2js:index-bounds:trust') @@ -27,3 +20,8 @@ TypedDataList newIntBuffer({required int bytes, required int length}) { _ => throw StateError('Unsupported integer size: $bytes'), }; } + +/// Swaps bytes lane 1 & 3 (i.e. bits 16-23 with bits 0-7). +int swapLane13(int x) { + return ((x & 0xff) << 16) | ((x >> 16) & 0xff) | (x & 0xff00ff00); +} diff --git a/test/argb_8888_test.dart b/test/argb_8888_test.dart index 37a9f48..c07494b 100644 --- a/test/argb_8888_test.dart +++ b/test/argb_8888_test.dart @@ -17,25 +17,47 @@ void main() { check(abgr8888).has((a) => a.max, 'max').equalsHex(0xFFFFFFFF); }); + group('smoke tests', () { + test('0xff000000', () { + check(abgr8888.fromAbgr8888(0xff000000)).equalsHex(abgr8888.black); + check(abgr8888.create()).equalsHex(abgr8888.black); + }); + + test('0xffff0000', () { + check(abgr8888.fromAbgr8888(0xffff0000)).equalsHex(abgr8888.red); + check(abgr8888.create(red: 0xff)).equalsHex(abgr8888.red); + }); + + test('0xff00ff00', () { + check(abgr8888.fromAbgr8888(0xff00ff00)).equalsHex(abgr8888.green); + check(abgr8888.create(green: 0xff)).equalsHex(abgr8888.green); + }); + + test('0xff0000ff', () { + check(abgr8888.fromAbgr8888(0xff0000ff)).equalsHex(abgr8888.blue); + check(abgr8888.create(blue: 0xff)).equalsHex(abgr8888.blue); + }); + }); + group('copyWith', () { test('makes no changes if no parameters are passed', () { check(abgr8888.copyWith(0x12345678)).equalsHex(0x12345678); }); test('changes alpha channel', () { - check(abgr8888.copyWith(0x12345678, alpha: 0x9A)).equalsHex(0x1234569A); + check(abgr8888.copyWith(0x12345678, alpha: 0x9A)).equalsHex(0x9a345678); }); test('changes blue channel', () { - check(abgr8888.copyWith(0x12345678, blue: 0x9A)).equalsHex(0x12349A78); + check(abgr8888.copyWith(0x12345678, blue: 0x9A)).equalsHex(0x1234569a); }); test('changes green channel', () { - check(abgr8888.copyWith(0x12345678, green: 0x9A)).equalsHex(0x129A5678); + check(abgr8888.copyWith(0x12345678, green: 0x9A)).equalsHex(0x12349a78); }); test('changes red channel', () { - check(abgr8888.copyWith(0x12345678, red: 0x9A)).equalsHex(0x9A345678); + check(abgr8888.copyWith(0x12345678, red: 0x9A)).equalsHex(0x129a5678); }); }); @@ -47,25 +69,25 @@ void main() { test('changes alpha channel', () { check( abgr8888.copyWithNormalized(0x12345678, alpha: 0.5), - ).equalsHex(0x1234567F); + ).equalsHex(0x7f345678); }); test('changes blue channel', () { check( abgr8888.copyWithNormalized(0x12345678, blue: 0.5), - ).equalsHex(0x12347F78); + ).equalsHex(0x1234567f); }); test('changes green channel', () { check( abgr8888.copyWithNormalized(0x12345678, green: 0.5), - ).equalsHex(0x127F5678); + ).equalsHex(0x12347f78); }); test('changes red channel', () { check( abgr8888.copyWithNormalized(0x12345678, red: 0.5), - ).equalsHex(0x7F345678); + ).equalsHex(0x127f5678); }); }); diff --git a/test/blend_test.dart b/test/blend_test.dart index b4f421c..0a218fa 100644 --- a/test/blend_test.dart +++ b/test/blend_test.dart @@ -7,90 +7,90 @@ void main() { final src = abgr8888.red; final dst = abgr8888.green; final blend = BlendMode.clear.getBlend(abgr8888, abgr8888); - check(blend(src, dst)).equals(abgr8888.zero); + check(blend(src, dst)).equalsHex(abgr8888.zero); }); test('BlendMode.src', () { final src = abgr8888.red; final dst = abgr8888.green; final blend = BlendMode.src.getBlend(abgr8888, abgr8888); - check(blend(src, dst)).equals(src); + check(blend(src, dst)).equalsHex(src); }); test('BlendMode.dst', () { final src = abgr8888.red; final dst = abgr8888.green; final blend = BlendMode.dst.getBlend(abgr8888, abgr8888); - check(blend(src, dst)).equals(dst); + check(blend(src, dst)).equalsHex(dst); }); test('BlendMode.srcOver', () { final src = abgr8888.create(red: 0x80, alpha: 0x80); final dst = abgr8888.create(green: 0x80, alpha: 0x80); final blend = BlendMode.srcOver.getBlend(abgr8888, abgr8888); - check(blend(src, dst)).equals(0x803f00bf); + check(blend(src, dst)).equalsHex(0xbf803f00); }); test('BlendMode.dstOver', () { final dst = abgr8888.create(red: 0x80, alpha: 0x80); final src = abgr8888.create(green: 0x80, alpha: 0x80); final blend = BlendMode.dstOver.getBlend(abgr8888, abgr8888); - check(blend(src, dst)).equals(0x803f00bf); + check(blend(src, dst)).equalsHex(0xbf803f00); }); test('BlendMode.srcIn', () { final src = abgr8888.create(red: 0x80, alpha: 0x80); final dst = abgr8888.create(green: 0x80, alpha: 0x80); final blend = BlendMode.srcIn.getBlend(abgr8888, abgr8888); - check(blend(src, dst)).equals(0x40000040); + check(blend(src, dst)).equalsHex(0x40400000); }); test('BlendMode.dstIn', () { final dst = abgr8888.create(red: 0x80, alpha: 0x80); final src = abgr8888.create(green: 0x80, alpha: 0x80); final blend = BlendMode.dstIn.getBlend(abgr8888, abgr8888); - check(blend(src, dst)).equals(0x40000040); + check(blend(src, dst)).equalsHex(0x40400000); }); test('BlendMode.srcOut', () { final src = abgr8888.create(red: 0x80, alpha: 0x80); final dst = abgr8888.create(green: 0x80, alpha: 0x80); final blend = BlendMode.srcOut.getBlend(abgr8888, abgr8888); - check(blend(src, dst)).equals(0x3f00003f); + check(blend(src, dst)).equalsHex(0x3f3f0000); }); test('BlendMode.dstOut', () { final dst = abgr8888.create(red: 0x80, alpha: 0x80); final src = abgr8888.create(green: 0x80, alpha: 0x80); final blend = BlendMode.dstOut.getBlend(abgr8888, abgr8888); - check(blend(src, dst)).equals(0x3f00003f); + check(blend(src, dst)).equalsHex(0x3f3f0000); }); test('BlendMode.srcAtop', () { final src = abgr8888.create(red: 0x80, alpha: 0x80); final dst = abgr8888.create(green: 0x80, alpha: 0x80); final blend = BlendMode.srcAtop.getBlend(abgr8888, abgr8888); - check(blend(src, dst)).equals(0x403f0080); + check(blend(src, dst)).equalsHex(0x80403f00); }); test('BlendMode.dstAtop', () { final dst = abgr8888.create(red: 0x80, alpha: 0x80); final src = abgr8888.create(green: 0x80, alpha: 0x80); final blend = BlendMode.dstAtop.getBlend(abgr8888, abgr8888); - check(blend(src, dst)).equals(0x403f0080); + check(blend(src, dst)).equalsHex(0x80403f00); }); test('BlendMode.xor', () { final src = abgr8888.create(red: 0x80, alpha: 0x80); final dst = abgr8888.create(green: 0x80, alpha: 0x80); final blend = BlendMode.xor.getBlend(abgr8888, abgr8888); - check(blend(src, dst)).equals(0x3f3f007f); + check(blend(src, dst)).equalsHex(0x7f3f3f00); }); test('BlendMode.plus', () { final src = abgr8888.create(red: 0x80, alpha: 0x80); final dst = abgr8888.create(green: 0x80, alpha: 0x80); final blend = BlendMode.plus.getBlend(abgr8888, abgr8888); - check(blend(src, dst)).equals(0x808000ff); + check(blend(src, dst)).equalsHex(0xff808000); }); } diff --git a/test/buffer_test.dart b/test/buffer_test.dart index 68e2bf1..87740d7 100644 --- a/test/buffer_test.dart +++ b/test/buffer_test.dart @@ -16,9 +16,9 @@ void main() { ]), ); final converted = buffer.map((pixel) => pixel ^ 0xFFFFFFFF); - check(converted.data.elementAt(0)).equalsHex(0x00FFFF00); - check(converted.data.elementAt(1)).equalsHex(0xFF00FF00); - check(converted.data.elementAt(2)).equalsHex(0xFFFF0000); + check(converted.data.elementAt(0)).equalsHex(0x0000ffff); + check(converted.data.elementAt(1)).equalsHex(0x00ff00ff); + check(converted.data.elementAt(2)).equalsHex(0x00ffff00); }); test('mapConvert', () { @@ -75,7 +75,7 @@ void main() { abgr8888.cyan, ]), ); - final clipped = buffer.mapClipped( + final clipped = buffer.mapRect( Rect.fromLTWH(0, 0, 1, 1), ); check(clipped.length).equals(1); diff --git a/test/indexed_test.dart b/test/indexed_test.dart new file mode 100644 index 0000000..615a3ed --- /dev/null +++ b/test/indexed_test.dart @@ -0,0 +1,79 @@ +import 'package:pxl/pxl.dart'; + +import 'src/prelude.dart'; + +void main() { + group('system8', () { + test('length is 8', () { + check(system8).has((a) => a.length, 'length').equals(8); + }); + + test('bytesPerPixel is 1', () { + check(system8).has((a) => a.bytesPerPixel, 'bytesPerPixel').equals(1); + }); + + test('[0] = black (#000000 in RGB)', () { + check(system8[0]).equalsHex(abgr8888.black); + }); + + test('[1] = red (#FF0000 in RGB)', () { + check(system8[1]).equalsHex(abgr8888.red); + }); + + test('[2] = green (#00FF00 in RGB)', () { + check(system8[2]).equalsHex(abgr8888.green); + }); + + test('[3] = blue (#0000FF in RGB)', () { + check(system8[3]).equalsHex(abgr8888.blue); + }); + + test('[4] = yellow (#FFFF00 in RGB)', () { + check(system8[4]).equalsHex(abgr8888.yellow); + }); + + test('[5] = cyan (#00FFFF in RGB)', () { + check(system8[5]).equalsHex(abgr8888.cyan); + }); + + test('[6] = magenta (#FF00FF in RGB)', () { + check(system8[6]).equalsHex(abgr8888.magenta); + }); + + test('[7] = white (#FFFFFF in RGB)', () { + check(system8[7]).equalsHex(abgr8888.white); + }); + + test('zero is 0', () { + check(system8).has((a) => a.zero, 'zero').equals(0); + }); + + test('max is 7', () { + check(system8).has((a) => a.max, 'max').equals(7); + }); + + test('clamp(0) is 0', () { + check(system8.clamp(0)).equals(0); + }); + + test('clamp(7) is 7', () { + check(system8.clamp(7)).equals(7); + }); + + test('clamp(-1) is 0', () { + check(system8.clamp(-1)).equals(0); + }); + + test('clamp(8) is 7', () { + check(system8.clamp(8)).equals(7); + }); + + test('fromAbgr8888(red) == red (1)', () { + check(system8.fromAbgr8888(abgr8888.red)).equals(1); + }); + + test('convert() == red (1)', () { + check(system8.convert(0xFFFF4500, from: abgr8888)).equals(1); + }); + }); +} diff --git a/test/pixels_test.dart b/test/pixels_test.dart index 6ced836..470db16 100644 --- a/test/pixels_test.dart +++ b/test/pixels_test.dart @@ -449,10 +449,10 @@ void main() { dst.blit(src.map((p) => p)); check(dst.data).deepEquals([ - 0xff00007f, - 0x00ff007f, - 0x0000ff7f, - 0x00ffff7f, + 0x7fff0000, + 0x7f00ff00, + 0x7f0000ff, + 0x7f00ffff, ]); }); @@ -471,10 +471,10 @@ void main() { dst.blit(src); check(dst.data).deepEquals([ - 0xff00007f, - 0x00ff007f, - 0x0000ff7f, - 0x00ffff7f, + 0x7fff0000, + 0x7f00ff00, + 0x7f0000ff, + 0x7f00ffff, ]); }); } diff --git a/test/src/unpng_1x1_red_pixel.png b/test/src/unpng_1x1_red_pixel.png new file mode 100644 index 0000000..7e1325f Binary files /dev/null and b/test/src/unpng_1x1_red_pixel.png differ diff --git a/test/unpng_test.dart b/test/unpng_test.dart new file mode 100644 index 0000000..ab73b3a --- /dev/null +++ b/test/unpng_test.dart @@ -0,0 +1 @@ +void main() {}