From cf5f6d94586ca049f59214281d051dc814a288aa Mon Sep 17 00:00:00 2001 From: brendan-duncan Date: Wed, 15 Jan 2025 22:37:37 -0700 Subject: [PATCH] Add resize function for in-place resizing --- lib/image.dart | 1 + lib/src/image/image_data.dart | 4 +- lib/src/transform/resize.dart | 190 ++++++++++++++++++++++++++++++++ test/transform/resize_test.dart | 177 +++++++++++++++++++++++++++++ 4 files changed, 370 insertions(+), 2 deletions(-) create mode 100644 lib/src/transform/resize.dart create mode 100644 test/transform/resize_test.dart diff --git a/lib/image.dart b/lib/image.dart index 71174dad..48193083 100644 --- a/lib/image.dart +++ b/lib/image.dart @@ -227,6 +227,7 @@ export 'src/transform/copy_resize.dart'; export 'src/transform/copy_resize_crop_square.dart'; export 'src/transform/copy_rotate.dart'; export 'src/transform/flip.dart'; +export 'src/transform/resize.dart'; export 'src/transform/trim.dart'; export 'src/util/binary_quantizer.dart'; export 'src/util/clip_line.dart'; diff --git a/lib/src/image/image_data.dart b/lib/src/image/image_data.dart index 723792c3..e122cf81 100644 --- a/lib/src/image/image_data.dart +++ b/lib/src/image/image_data.dart @@ -7,8 +7,8 @@ import 'palette.dart'; import 'pixel.dart'; abstract class ImageData extends Iterable { - final int width; - final int height; + int width; + int height; final int numChannels; ImageData(this.width, this.height, this.numChannels); diff --git a/lib/src/transform/resize.dart b/lib/src/transform/resize.dart new file mode 100644 index 00000000..130b61a9 --- /dev/null +++ b/lib/src/transform/resize.dart @@ -0,0 +1,190 @@ +import 'dart:typed_data'; + +import '../color/color.dart'; +import '../image/image.dart'; +import '../image/interpolation.dart'; +import '../util/image_exception.dart'; +import 'bake_orientation.dart'; +import 'copy_resize.dart'; + +Image resize(Image src, + {int? width, + int? height, + bool? maintainAspect, + Color? backgroundColor, + Interpolation interpolation = Interpolation.nearest}) { + if (width == null && height == null) { + throw ImageException('Invalid size'); + } + + // You can't interpolate index pixels + if (src.hasPalette) { + interpolation = Interpolation.nearest; + } + + if (src.exif.imageIfd.hasOrientation && src.exif.imageIfd.orientation != 1) { + src = bakeOrientation(src); + } + + var x1 = 0; + var y1 = 0; + var x2 = 0; + var y2 = 0; + + // this block sets [width] and [height] if null or negative. + if (width != null && height != null && maintainAspect == true) { + x1 = 0; + x2 = width; + final srcAspect = src.height / src.width; + final h = (width * srcAspect).toInt(); + final dy = (height - h) ~/ 2; + y1 = dy; + y2 = y1 + h; + if (y1 < 0 || y2 > height) { + y1 = 0; + y2 = height; + final srcAspect = src.width / src.height; + final w = (height * srcAspect).toInt(); + final dx = (width - w) ~/ 2; + x1 = dx; + x2 = x1 + w; + } + } else { + maintainAspect = false; + } + + if (height == null || height <= 0) { + height = (width! * (src.height / src.width)).round(); + } + if (width == null || width <= 0) { + width = (height * (src.width / src.height)).round(); + } + + final w = maintainAspect! ? x2 - x1 : width; + final h = maintainAspect ? y2 - y1 : height; + + if (!maintainAspect) { + x1 = 0; + x2 = width; + y1 = 0; + y2 = height; + } + + if (width == src.width && height == src.height) { + return src; + } + + if ((width * height) > (src.width * src.height)) { + return copyResize(src, width: width, height: height, + maintainAspect: maintainAspect, backgroundColor: backgroundColor, + interpolation: interpolation); + } + + final scaleX = Int32List(w); + final dx = src.width / w; + for (var x = 0; x < w; ++x) { + scaleX[x] = (x * dx).toInt(); + } + + final origWidth = src.width; + final origHeight = src.height; + + final numFrames = src.numFrames; + for (var i = 0; i < numFrames; ++i) { + final frame = src.frames[i]; + final dst = frame; + + final dy = frame.height / h; + final dx = frame.width / w; + + if (maintainAspect && backgroundColor != null) { + dst.clear(backgroundColor); + } + + if (interpolation == Interpolation.average) { + for (var y = 0; y < h; ++y) { + final ay1 = (y * dy).toInt(); + var ay2 = ((y + 1) * dy).toInt(); + if (ay2 == ay1) { + ay2++; + } + + for (var x = 0; x < w; ++x) { + final ax1 = (x * dx).toInt(); + var ax2 = ((x + 1) * dx).toInt(); + if (ax2 == ax1) { + ax2++; + } + + num r = 0; + num g = 0; + num b = 0; + num a = 0; + var np = 0; + for (var sy = ay1; sy < ay2; ++sy) { + for (var sx = ax1; sx < ax2; ++sx, ++np) { + final s = frame.getPixel(sx, sy); + r += s.r; + g += s.g; + b += s.b; + a += s.a; + } + } + final c = dst.getColor(r / np, g / np, b / np, a / np); + + dst.data!.width = width; + dst.data!.height = height; + dst.setPixel(x1 + x, y1 + y, c); + dst.data!.width = origWidth; + dst.data!.height = origHeight; + } + } + } else if (interpolation == Interpolation.nearest) { + if (frame.hasPalette) { + for (var y = 0; y < h; ++y) { + final y2 = (y * dy).toInt(); + for (var x = 0; x < w; ++x) { + final p = frame.getPixelIndex(scaleX[x], y2); + dst.data!.width = width; + dst.data!.height = height; + dst.setPixelIndex(x1 + x, y1 + y, p); + dst.data!.width = origWidth; + dst.data!.height = origHeight; + } + } + } else { + for (var y = 0; y < h; ++y) { + final y2 = (y * dy).toInt(); + for (var x = 0; x < w; ++x) { + final p = frame.getPixel(scaleX[x], y2); + dst.data!.width = width; + dst.data!.height = height; + dst.setPixel(x1 + x, y1 + y, p); + dst.data!.width = origWidth; + dst.data!.height = origHeight; + } + } + } + } else { + // Copy the pixels from this image to the new image. + for (var y = 0; y < h; ++y) { + final sy2 = y * dy; + for (var x = 0; x < w; ++x) { + final sx2 = x * dx; + final p = frame.getPixelInterpolate(x1 + sx2, y1 + sy2, + interpolation: interpolation); + dst.data!.width = width; + dst.data!.height = height; + dst.setPixel(x, y, p); + dst.data!.width = origWidth; + dst.data!.height = origHeight; + } + } + } + + dst.data!.width = width; + dst.data!.height = height; + } + + return src; +} diff --git a/test/transform/resize_test.dart b/test/transform/resize_test.dart new file mode 100644 index 00000000..acaa6104 --- /dev/null +++ b/test/transform/resize_test.dart @@ -0,0 +1,177 @@ +import 'dart:io'; +import 'package:image/image.dart'; +import 'package:test/test.dart'; + +import '../_test_util.dart'; + +void main() { + group('Transform', () { + test('resize nearest', () { + final img = + decodePng(File('test/_data/png/buck_24.png').readAsBytesSync())!; + final i0 = resize(img, width: 64); + expect(i0.width, equals(64)); + expect(i0.height, equals(40)); + File('$testOutputPath/transform/resize.png') + ..createSync(recursive: true) + ..writeAsBytesSync(encodePng(i0)); + }); + + test('resize average', () { + final img = + decodePng(File('test/_data/png/buck_24.png').readAsBytesSync())!; + final i0 = + resize(img, width: 64, interpolation: Interpolation.average); + expect(i0.width, equals(64)); + expect(i0.height, equals(40)); + File('$testOutputPath/transform/resize_average.png') + ..createSync(recursive: true) + ..writeAsBytesSync(encodePng(i0)); + }); + + test('resize linear', () { + final img = + decodePng(File('test/_data/png/buck_24.png').readAsBytesSync())!; + final i0 = + resize(img, width: 64, interpolation: Interpolation.linear); + expect(i0.width, equals(64)); + expect(i0.height, equals(40)); + File('$testOutputPath/transform/resize_linear.png') + ..createSync(recursive: true) + ..writeAsBytesSync(encodePng(i0)); + }); + + test('resize cubic', () { + final img = + decodePng(File('test/_data/png/buck_24.png').readAsBytesSync())!; + final i0 = resize(img, width: 64, interpolation: Interpolation.cubic); + expect(i0.width, equals(64)); + expect(i0.height, equals(40)); + File('$testOutputPath/transform/resize_cubic.png') + ..createSync(recursive: true) + ..writeAsBytesSync(encodePng(i0)); + }); + + test('resize maintainAspect', () { + final img = + decodePng(File('test/_data/png/buck_24.png').readAsBytesSync())!; + final i0 = resize(img, + width: 640, + height: 640, + maintainAspect: true, + backgroundColor: ColorRgb8(0, 0, 255)); + expect(i0.width, equals(640)); + expect(i0.height, equals(640)); + File('$testOutputPath/transform/resize_maintainAspect.png') + ..createSync(recursive: true) + ..writeAsBytesSync(encodePng(i0)); + }); + + test('resize maintainAspect palette', () { + final img = + decodePng(File('test/_data/png/buck_8.png').readAsBytesSync())!; + final i0 = resize(img, + width: 640, + height: 640, + maintainAspect: true, + backgroundColor: ColorRgb8(0, 0, 255)); + expect(i0.width, equals(640)); + expect(i0.height, equals(640)); + File('$testOutputPath/transform/resize_maintainAspect_palette.png') + ..createSync(recursive: true) + ..writeAsBytesSync(encodePng(i0)); + }); + + test('resize maintainAspect 2', () { + final i0 = Image(width: 100, height: 50)..clear(ColorRgb8(255, 0, 0)); + final i1 = resize(i0, + width: 200, + height: 200, + maintainAspect: true, + backgroundColor: ColorRgb8(0, 0, 255)); + expect(i1.width, equals(200)); + expect(i1.height, equals(200)); + File('$testOutputPath/transform/resize_maintainAspect_2.png') + ..createSync(recursive: true) + ..writeAsBytesSync(encodePng(i1)); + }); + + test('resize maintainAspect 3', () { + final i0 = Image(width: 50, height: 100)..clear(ColorRgb8(0, 255, 0)); + final i1 = resize(i0, + width: 200, + height: 200, + maintainAspect: true, + backgroundColor: ColorRgb8(0, 0, 255)); + expect(i1.width, equals(200)); + expect(i1.height, equals(200)); + File('$testOutputPath/transform/resize_maintainAspect_3.png') + ..createSync(recursive: true) + ..writeAsBytesSync(encodePng(i1)); + }); + + test('resize maintainAspect 4', () { + final i0 = Image(width: 100, height: 50)..clear(ColorRgb8(255, 0, 0)); + final i1 = resize(i0, + width: 50, + height: 100, + maintainAspect: true, + backgroundColor: ColorRgb8(0, 0, 255)); + expect(i1.width, equals(50)); + expect(i1.height, equals(100)); + File('$testOutputPath/transform/resize_maintainAspect_4.png') + ..createSync(recursive: true) + ..writeAsBytesSync(encodePng(i1)); + }); + + test('resize maintainAspect 5', () { + final i0 = Image(width: 50, height: 100)..clear(ColorRgb8(0, 255, 0)); + final i1 = resize(i0, + width: 100, + height: 50, + maintainAspect: true, + backgroundColor: ColorRgb8(0, 0, 255)); + expect(i1.width, equals(100)); + expect(i1.height, equals(50)); + File('$testOutputPath/transform/resize_maintainAspect_5.png') + ..createSync(recursive: true) + ..writeAsBytesSync(encodePng(i1)); + }); + + test('resize maintainAspect 5', () { + final i0 = Image(width: 50, height: 100)..clear(ColorRgb8(0, 255, 0)); + final i1 = resize(i0, + width: 100, + height: 500, + maintainAspect: true, + backgroundColor: ColorRgb8(0, 0, 255)); + expect(i1.width, equals(100)); + expect(i1.height, equals(500)); + File('$testOutputPath/transform/resize_maintainAspect_5.png') + ..createSync(recursive: true) + ..writeAsBytesSync(encodePng(i1)); + }); + + test('resize maintainAspect 6', () { + final i0 = Image(width: 100, height: 50)..clear(ColorRgb8(0, 255, 0)); + final i1 = resize(i0, + width: 500, + height: 100, + maintainAspect: true, + backgroundColor: ColorRgb8(0, 0, 255)); + expect(i1.width, equals(500)); + expect(i1.height, equals(100)); + File('$testOutputPath/transform/resize_maintainAspect_6.png') + ..createSync(recursive: true) + ..writeAsBytesSync(encodePng(i1)); + }); + + test('resize palette', () async { + final img = await decodePngFile('test/_data/png/test.png'); + final i0 = + resize(img!, width: 64, interpolation: Interpolation.cubic); + await encodePngFile( + '$testOutputPath/transform/resize_palette.png', i0); + }); + }); +}