blob: e4c72fa0bc170cb93152853275bdeaa36948051b [file] [log] [blame]
import 'dart:math';
import 'dart:typed_data';
import 'color.dart';
import 'exif_data.dart';
import 'icc_profile_data.dart';
import 'image_exception.dart';
import 'util/interpolation.dart';
/// A 32-bit image buffer where pixels are encoded into 32-bit unsigned ints.
/// You can use the methods in color to encode/decode the RGBA channels of a
/// color for a pixel.
///
/// Pixels are stored in 32-bit unsigned integers in aabbggrr format.
/// This is to be consistent with HTML canvas data. You can use
/// [getBytes] to access the pixel at the byte (channel) level, where there
/// are four bytes per pixel in red, green, blue, and alpha order.
///
/// If this image is a frame of an animation as decoded by the [decodeFrame]
/// method of [Decoder], then the [xOffset], [yOffset], [width] and [height]
/// coordinates determine area of the canvas this image should be drawn into,
/// as some frames of an animation only modify part of the canvas (recording
/// the part of the frame that actually changes). The [decodeAnimation] method
/// will always return the fully composed animation, so these coordinate
/// properties are not used.
class Image {
/// 24-bit RGB image.
static const int RGB = 3;
/// 32-bit RGBA image.
static const int RGBA = 4;
/// When drawing this frame, the canvas should be left as it is.
static const int DISPOSE_NONE = 0;
/// When drawing this frame, the canvas should be cleared first.
static const int DISPOSE_CLEAR = 1;
/// When drawing this frame, the canvas should be reverted to how it was before drawing it.
static const int DISPOSE_PREVIOUS = 2;
/// No alpha blending should be done when drawing this frame (replace
/// pixels in canvas).
static const int BLEND_SOURCE = 0;
/// Alpha blending should be used when drawing this frame (composited over
/// the current canvas image).
static const int BLEND_OVER = 1;
/// Width of the image.
final int width;
/// Height of the image.
final int height;
/// x position at which to render the frame.
int xOffset = 0;
/// y position at which to render the frame.
int yOffset = 0;
/// How long this frame should be displayed, in milliseconds.
/// A duration of 0 indicates no delay and the next frame will be drawn
/// as quickly as it can.
int duration = 0;
/// Defines what should be done to the canvas when drawing this frame.
int disposeMethod = DISPOSE_CLEAR;
/// Defines the blending method (alpha compositing) to use when drawing this
/// frame.
int blendMethod = BLEND_OVER;
/// Pixels are encoded into 4-byte integers, where each byte is an RGBA
/// channel.
final Uint32List data;
ExifData exif;
ICCProfileData iccProfile;
/// Create an image with the given dimensions and format.
Image(int width, int height,
[this._format = RGBA, ExifData exif, ICCProfileData iccp])
: this.width = width,
this.height = height,
data = Uint32List(width * height),
exif = ExifData.from(exif),
iccProfile = iccp;
/// Create a copy of the image [other].
Image.from(Image other)
: width = other.width,
height = other.height,
xOffset = other.xOffset,
yOffset = other.yOffset,
duration = other.duration,
disposeMethod = other.disposeMethod,
blendMethod = other.blendMethod,
_format = other._format,
data = Uint32List.fromList(other.data),
exif = ExifData.from(other.exif),
iccProfile = other.iccProfile;
/// Create an image from [bytes].
///
/// [bytes] should be in RGB<A> format with a byte [0,255] for each channel.
/// The length of [bytes] should be <3|4> * (width * height).
/// [format] determines if there are 3 or 4 channels per pixel.
///
/// For example, given an Html Canvas, you could create an image:
/// var bytes = canvas.getContext('2d').getImageData(0, 0,
/// canvas.width, canvas.height).data;
/// Image image = Image.fromBytes(canvas.width, canvas.height, bytes);
Image.fromBytes(int width, int height, List<int> bytes,
[this._format = RGBA, ExifData exif, ICCProfileData iccp])
: this.width = width,
this.height = height,
// Create a uint32 view of the byte buffer.
// This assumes the system architecture is little-endian...
data = bytes is Uint8List
? new Uint32List.view(bytes.buffer)
: bytes is Uint8ClampedList
? new Uint32List.view(bytes.buffer)
: bytes is Uint32List
? new Uint32List.view(bytes.buffer)
: new Uint32List.view(new Uint8List.fromList(bytes).buffer),
exif = ExifData.from(exif),
iccProfile = iccp;
/// Clone this image.
Image clone() => new Image.from(this);
/// Get the RGBA bytes from the image. You can use this to access the
/// RGBA color channels directly, or to pass it to something like an
/// Html canvas context.
///
/// For example, given an Html Canvas, you could draw this image into the
/// canvas:
/// Html.ImageData d = context2D.createImageData(image.width, image.height);
/// d.data.setRange(0, image.length, image.getBytes());
/// context2D.putImageData(data, 0, 0);
Uint8List getBytes() => new Uint8List.view(data.buffer);
/// Get the format of the image, either [RGB] or [RGBA].
int get format => _format;
/// Set the format of the image, either [RGB] or [RGBA]. The format is used
/// for informational purposes and has no effect on the actual stored data,
/// which is always in 4-byte RGBA format.
set format(int f) {
if (f == _format) {
return;
}
if (f != RGB && f != RGBA) {
throw new ImageException('Invalid image format: $f');
}
_format = f;
}
/// How many color channels does the image have, 3 or 4?
/// Note that internally, images always have 4 8-bit channels.
int get numChannels => _format;
/// Set all of the pixels of the image to the given [color].
Image fill(int color) {
data.fillRange(0, data.length, color);
return this;
}
/// Add the colors of [other] to the pixels of this image.
Image operator +(Image other) {
int h = min(height, other.height);
int w = min(width, other.width);
for (int y = 0; y < h; ++y) {
for (int x = 0; x < w; ++x) {
int c1 = getPixel(x, y);
int r1 = getRed(c1);
int g1 = getGreen(c1);
int b1 = getBlue(c1);
int a1 = getAlpha(c1);
int c2 = other.getPixel(x, y);
int r2 = getRed(c2);
int g2 = getGreen(c2);
int b2 = getBlue(c2);
int a2 = getAlpha(c2);
setPixel(x, y, getColor(r1 + r2, g1 + g2, b1 + b2, a1 + a2));
}
}
return this;
}
/// Subtract the colors of [other] from the pixels of this image.
Image operator -(Image other) {
int h = min(height, other.height);
int w = min(width, other.width);
for (int y = 0; y < h; ++y) {
for (int x = 0; x < w; ++x) {
int c1 = getPixel(x, y);
int r1 = getRed(c1);
int g1 = getGreen(c1);
int b1 = getBlue(c1);
int a1 = getAlpha(c1);
int c2 = other.getPixel(x, y);
int r2 = getRed(c2);
int g2 = getGreen(c2);
int b2 = getBlue(c2);
int a2 = getAlpha(c2);
setPixel(x, y, getColor(r1 - r2, g1 - g2, b1 - b2, a1 - a2));
}
}
return this;
}
/// Multiply the colors of [other] with the pixels of this image.
Image operator *(Image other) {
int h = min(height, other.height);
int w = min(width, other.width);
for (int y = 0; y < h; ++y) {
for (int x = 0; x < w; ++x) {
int c1 = getPixel(x, y);
int r1 = getRed(c1);
int g1 = getGreen(c1);
int b1 = getBlue(c1);
int a1 = getAlpha(c1);
int c2 = other.getPixel(x, y);
int r2 = getRed(c2);
int g2 = getGreen(c2);
int b2 = getBlue(c2);
int a2 = getAlpha(c2);
setPixel(x, y, getColor(r1 * r2, g1 * g2, b1 * b2, a1 * a2));
}
}
return this;
}
/// OR the colors of [other] to the pixels of this image.
Image operator |(Image other) {
int h = min(height, other.height);
int w = min(width, other.width);
for (int y = 0; y < h; ++y) {
for (int x = 0; x < w; ++x) {
int c1 = getPixel(x, y);
int r1 = getRed(c1);
int g1 = getGreen(c1);
int b1 = getBlue(c1);
int a1 = getAlpha(c1);
int c2 = other.getPixel(x, y);
int r2 = getRed(c2);
int g2 = getGreen(c2);
int b2 = getBlue(c2);
int a2 = getAlpha(c2);
setPixel(x, y, getColor(r1 | r2, g1 | g2, b1 | b2, a1 | a2));
}
}
return this;
}
/// AND the colors of [other] with the pixels of this image.
Image operator &(Image other) {
int h = min(height, other.height);
int w = min(width, other.width);
for (int y = 0; y < h; ++y) {
for (int x = 0; x < w; ++x) {
int c1 = getPixel(x, y);
int r1 = getRed(c1);
int g1 = getGreen(c1);
int b1 = getBlue(c1);
int a1 = getAlpha(c1);
int c2 = other.getPixel(x, y);
int r2 = getRed(c2);
int g2 = getGreen(c2);
int b2 = getBlue(c2);
int a2 = getAlpha(c2);
setPixel(x, y, getColor(r1 & r2, g1 & g2, b1 & b2, a1 & a2));
}
}
return this;
}
/// Modula the colors of [other] with the pixels of this image.
Image operator %(Image other) {
int h = min(height, other.height);
int w = min(width, other.width);
for (int y = 0; y < h; ++y) {
for (int x = 0; x < w; ++x) {
int c1 = getPixel(x, y);
int r1 = getRed(c1);
int g1 = getGreen(c1);
int b1 = getBlue(c1);
int a1 = getAlpha(c1);
int c2 = other.getPixel(x, y);
int r2 = getRed(c2);
int g2 = getGreen(c2);
int b2 = getBlue(c2);
int a2 = getAlpha(c2);
setPixel(x, y, getColor(r1 % r2, g1 % g2, b1 % b2, a1 % a2));
}
}
return this;
}
/// The size of the image buffer.
int get length => data.length;
/// Get a pixel from the buffer.
int operator [](int index) => data[index];
/// Set a pixel in the buffer.
void operator []=(int index, int color) {
data[index] = color;
}
/// Get the buffer index for the [x], [y] pixel coordinates.
int index(int x, int y) => y * width + x;
/// Is the given pixel coordinates within the resolution of the image.
bool boundsSafe(int x, int y) => x >= 0 && x < width && y >= 0 && y < height;
/// Get the pixel from the given [x], [y] coordinate.
int getPixel(int x, int y) => boundsSafe(x, y) ? data[y * width + x] : 0;
/// Get the pixel from the given [x], [y] coordinate without check the bounds.
int getUnsafePixel(int x, int y) => data[y * width + x];
/// Get the pixel from the given [offset], index.
int getUnsafePixel_(int offset) => data[offset];
/// Get the pixel using the given [interpolation] type for non-integer pixel
/// coordinates.
int getPixelInterpolate(num fx, num fy, [int interpolation = LINEAR]) {
if (interpolation == CUBIC) {
return getPixelCubic(fx, fy);
} else if (interpolation == LINEAR) {
return getPixelLinear(fx, fy);
}
return getPixel(fx.toInt(), fy.toInt());
}
/// Get the pixel using linear interpolation for non-integer pixel
/// coordinates.
int getPixelLinear(num fx, num fy) {
int x = fx.toInt() - (fx >= 0 ? 0 : 1);
int nx = x + 1;
int y = fy.toInt() - (fy >= 0 ? 0 : 1);
int ny = y + 1;
num dx = fx - x;
num dy = fy - y;
int _linear(int Icc, int Inc, int Icn, int Inn) {
return (Icc +
dx * (Inc - Icc + dy * (Icc + Inn - Icn - Inc)) +
dy * (Icn - Icc))
.toInt();
}
int Icc = getPixel(x, y);
int Inc = getPixel(nx, y);
int Icn = getPixel(x, ny);
int Inn = getPixel(nx, ny);
return getColor(
_linear(getRed(Icc), getRed(Inc), getRed(Icn), getRed(Inn)),
_linear(getGreen(Icc), getGreen(Inc), getGreen(Icn), getGreen(Inn)),
_linear(getBlue(Icc), getBlue(Inc), getBlue(Icn), getBlue(Inn)),
_linear(getAlpha(Icc), getAlpha(Inc), getAlpha(Icn), getAlpha(Inn)));
}
/// Get the pixel using cubic interpolation for non-integer pixel
/// coordinates.
int getPixelCubic(num fx, num fy) {
int x = fx.toInt() - (fx >= 0.0 ? 0 : 1);
int px = x - 1;
int nx = x + 1;
int ax = x + 2;
int y = fy.toInt() - (fy >= 0.0 ? 0 : 1);
int py = y - 1;
int ny = y + 1;
int ay = y + 2;
var dx = fx - x;
var dy = fy - y;
num _cubic(num dx, num Ipp, num Icp, num Inp, num Iap) =>
Icp + 0.5 * (dx * (-Ipp + Inp) +
dx * dx * (2 * Ipp - 5 * Icp + 4 * Inp - Iap) +
dx * dx * dx * (-Ipp + 3 * Icp - 3 * Inp + Iap));
int Ipp = getPixel(px, py);
int Icp = getPixel(x, py);
int Inp = getPixel(nx, py);
int Iap = getPixel(ax, py);
num Ip0 = _cubic(dx, getRed(Ipp), getRed(Icp), getRed(Inp), getRed(Iap));
num Ip1 =
_cubic(dx, getGreen(Ipp), getGreen(Icp), getGreen(Inp), getGreen(Iap));
num Ip2 =
_cubic(dx, getBlue(Ipp), getBlue(Icp), getBlue(Inp), getBlue(Iap));
num Ip3 =
_cubic(dx, getAlpha(Ipp), getAlpha(Icp), getAlpha(Inp), getAlpha(Iap));
int Ipc = getPixel(px, y);
int Icc = getPixel(x, y);
int Inc = getPixel(nx, y);
int Iac = getPixel(ax, y);
num Ic0 = _cubic(dx, getRed(Ipc), getRed(Icc), getRed(Inc), getRed(Iac));
num Ic1 =
_cubic(dx, getGreen(Ipc), getGreen(Icc), getGreen(Inc), getGreen(Iac));
num Ic2 =
_cubic(dx, getBlue(Ipc), getBlue(Icc), getBlue(Inc), getBlue(Iac));
num Ic3 =
_cubic(dx, getAlpha(Ipc), getAlpha(Icc), getAlpha(Inc), getAlpha(Iac));
int Ipn = getPixel(px, ny);
int Icn = getPixel(x, ny);
int Inn = getPixel(nx, ny);
int Ian = getPixel(ax, ny);
num In0 = _cubic(dx, getRed(Ipn), getRed(Icn), getRed(Inn), getRed(Ian));
num In1 =
_cubic(dx, getGreen(Ipn), getGreen(Icn), getGreen(Inn), getGreen(Ian));
num In2 =
_cubic(dx, getBlue(Ipn), getBlue(Icn), getBlue(Inn), getBlue(Ian));
num In3 =
_cubic(dx, getAlpha(Ipn), getAlpha(Icn), getAlpha(Inn), getAlpha(Ian));
int Ipa = getPixel(px, ay);
int Ica = getPixel(x, ay);
int Ina = getPixel(nx, ay);
int Iaa = getPixel(ax, ay);
num Ia0 = _cubic(dx, getRed(Ipa), getRed(Ica), getRed(Ina), getRed(Iaa));
num Ia1 =
_cubic(dx, getGreen(Ipa), getGreen(Ica), getGreen(Ina), getGreen(Iaa));
num Ia2 =
_cubic(dx, getBlue(Ipa), getBlue(Ica), getBlue(Ina), getBlue(Iaa));
num Ia3 =
_cubic(dx, getAlpha(Ipa), getAlpha(Ica), getAlpha(Ina), getAlpha(Iaa));
num c0 = _cubic(dy, Ip0, Ic0, In0, Ia0);
num c1 = _cubic(dy, Ip1, Ic1, In1, Ia1);
num c2 = _cubic(dy, Ip2, Ic2, In2, Ia2);
num c3 = _cubic(dy, Ip3, Ic3, In3, Ia3);
return getColor(c0.toInt(), c1.toInt(), c2.toInt(), c3.toInt());
}
/// Set the pixel at the given [x], [y] coordinate to the [color].
///
/// This simply replaces the existing color, it does not do any alpha
/// blending. Use [drawPixel] for that.
void setPixel(int x, int y, int color) {
data[y * width + x] = color;
}
void setPixelSafe(int x, int y, int color) {
if (boundsSafe(x, y)) {
data[y * width + x] = color;
}
}
/// Set the pixel at the given [x], [y] coordinate to the [color] without
/// check the bounds.
///
/// This simply replaces the existing color, it does not do any alpha
/// blending. Use [drawPixel] for that.
void setUnsafePixel(int x, int y, int color) {
data[y * width + x] = color;
}
/// Set the pixel at the given [offset] index to the [color] without check
/// the bounds.
void setUnsafePixel_(int offset, int color) {
data[offset] = color;
}
/// Set the pixel at the given [x], [y] coordinate to the color
/// [r], [g], [b], [a].
///
/// This simply replaces the existing color, it does not do any alpha
/// blending. Use [drawPixel] for that.
void setPixelRGBA(int x, int y, int r, int g, int b, [int a = 0xff]) {
data[y * width + x] = getColor(r, g, b, a);
}
/// Return the average gray value of the image.
int getWhiteBalance() {
final len = data.length;
int r = 0;
int g = 0;
int b = 0;
for (int i = 0; i < len; ++i) {
r += getRed(data[i]);
g += getGreen(data[i]);
b += getBlue(data[i]);
}
r ~/= len;
g ~/= len;
b ~/= len;
return (r + g + b) ~/ 3;
}
/// Format of the image.
int _format;
}