diff --git a/CHANGELOG.md b/CHANGELOG.md index 88c32dd9..22d5fe9d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,11 @@ +## 4.3.0 - October 15, 2024 + +- Fix exceptions loading some PSD files. +- Fix trim for palette images. +- Fix ICC decompression. +- Add physical size handling to PNG. +- Fix TIFF out of bounds error. + ## 4.2.0 - May 22, 2024 - Fix decoding EXIF data from WebP. diff --git a/analysis_options.yaml b/analysis_options.yaml index 5d84e612..9cf4d540 100644 --- a/analysis_options.yaml +++ b/analysis_options.yaml @@ -1,4 +1,4 @@ -include: package:lints/recommended.yaml +include: package:lints/core.yaml analyzer: language: strict-casts: true @@ -62,7 +62,6 @@ linter: - non_constant_identifier_names - only_throw_errors - overridden_fields - - package_api_docs - package_names - package_prefixed_library_names - prefer_asserts_in_initializer_lists diff --git a/lib/image.dart b/lib/image.dart index e25995b8..e5d375ad 100644 --- a/lib/image.dart +++ b/lib/image.dart @@ -126,22 +126,22 @@ export 'src/formats/png/png_info.dart' hide InternalPngInfo; export 'src/formats/png_decoder.dart'; export 'src/formats/png_encoder.dart'; export 'src/formats/pnm_decoder.dart'; -//export 'src/formats/psd/effect/psd_bevel_effect.dart'; -//export 'src/formats/psd/effect/psd_drop_shadow_effect.dart'; -//export 'src/formats/psd/effect/psd_effect.dart'; -//export 'src/formats/psd/effect/psd_inner_glow_effect.dart'; -//export 'src/formats/psd/effect/psd_inner_shadow_effect.dart'; -//export 'src/formats/psd/effect/psd_outer_glow_effect.dart'; -//export 'src/formats/psd/effect/psd_solid_fill_effect.dart'; -//export 'src/formats/psd/layer_data/psd_layer_additional_data.dart'; -//export 'src/formats/psd/layer_data/psd_layer_section_divider.dart'; -//export 'src/formats/psd/psd_blending_ranges.dart'; -//export 'src/formats/psd/psd_channel.dart'; +export 'src/formats/psd/effect/psd_bevel_effect.dart'; +export 'src/formats/psd/effect/psd_drop_shadow_effect.dart'; +export 'src/formats/psd/effect/psd_effect.dart'; +export 'src/formats/psd/effect/psd_inner_glow_effect.dart'; +export 'src/formats/psd/effect/psd_inner_shadow_effect.dart'; +export 'src/formats/psd/effect/psd_outer_glow_effect.dart'; +export 'src/formats/psd/effect/psd_solid_fill_effect.dart'; +export 'src/formats/psd/layer_data/psd_layer_additional_data.dart'; +export 'src/formats/psd/layer_data/psd_layer_section_divider.dart'; +export 'src/formats/psd/psd_blending_ranges.dart'; +export 'src/formats/psd/psd_channel.dart'; export 'src/formats/psd/psd_image.dart'; -//export 'src/formats/psd/psd_image_resource.dart'; -//export 'src/formats/psd/psd_layer.dart'; -//export 'src/formats/psd/psd_layer_data.dart'; -//export 'src/formats/psd/psd_mask.dart'; +export 'src/formats/psd/psd_image_resource.dart'; +export 'src/formats/psd/psd_layer.dart'; +export 'src/formats/psd/psd_layer_data.dart'; +export 'src/formats/psd/psd_mask.dart'; export 'src/formats/psd_decoder.dart'; //export 'src/formats/pvr/pvr_bit_utility.dart'; //export 'src/formats/pvr/pvr_color.dart'; diff --git a/lib/src/draw/_calculate_circumference.dart b/lib/src/draw/_calculate_circumference.dart index 44ad39d9..b24c3862 100644 --- a/lib/src/draw/_calculate_circumference.dart +++ b/lib/src/draw/_calculate_circumference.dart @@ -32,7 +32,7 @@ List calculateCircumference(Image image, int x0, int y0, int radius) { for (var f = 1 - radius, ddFx = 0, ddFy = -(radius << 1), x = 0, y = radius; x < y;) { if (f >= 0) { - f += (ddFy += 2); + f += ddFy += 2; --y; } ++x; diff --git a/lib/src/exif/exif_data.dart b/lib/src/exif/exif_data.dart index 6f446102..a7f39aba 100644 --- a/lib/src/exif/exif_data.dart +++ b/lib/src/exif/exif_data.dart @@ -272,6 +272,9 @@ class ExifData extends IfdContainer { var index = 0; while (ifdOffset > 0) { block.offset = blockOffset + ifdOffset; + if (block.length < 2) { + break; + } final directory = IfdDirectory(); final numEntries = block.readUint16(); diff --git a/lib/src/formats/bmp_encoder.dart b/lib/src/formats/bmp_encoder.dart index e11e389c..5b498408 100644 --- a/lib/src/formats/bmp_encoder.dart +++ b/lib/src/formats/bmp_encoder.dart @@ -99,10 +99,12 @@ class BmpEncoder extends Encoder { final rowPadding = rowPaddingSize > 0 ? List.filled(rowPaddingSize, 0xff) : null; + final implicitPaletteSize = bpp >= 1 && bpp <= 8 ? 1 << bpp : 0; + final imageFileSize = fileStride * image.height; final headerInfoSize = bpp > 8 ? 124 : 40; final headerSize = headerInfoSize + 14; - final paletteSize = (image.palette?.numColors ?? 0) * 4; + final paletteSize = implicitPaletteSize * 4; final origImageOffset = headerSize + paletteSize; final imageOffset = origImageOffset; //final imageOffset = _roundToMultiple(origImageOffset); @@ -161,14 +163,24 @@ class BmpEncoder extends Encoder { if (bpp == 1 || bpp == 2 || bpp == 4 || bpp == 8) { if (palette != null) { //final palette = image.palette!; - final l = palette.numColors; - for (var pi = 0; pi < l; ++pi) { + final l = palette.numColors > implicitPaletteSize + ? implicitPaletteSize + : palette.numColors; + var pi = 0; + for (pi = 0; pi < l; ++pi) { out ..writeByte(palette.getBlue(pi).toInt()) ..writeByte(palette.getGreen(pi).toInt()) ..writeByte(palette.getRed(pi).toInt()) ..writeByte(0); } + for (; pi < implicitPaletteSize; ++pi) { + out + ..writeByte(0) + ..writeByte(0) + ..writeByte(0) + ..writeByte(0); + } } else { if (bpp == 1) { out diff --git a/lib/src/formats/psd/psd_channel.dart b/lib/src/formats/psd/psd_channel.dart index 9ac30357..d63620f8 100644 --- a/lib/src/formats/psd/psd_channel.dart +++ b/lib/src/formats/psd/psd_channel.dart @@ -1,3 +1,4 @@ +import 'dart:math'; import 'dart:typed_data'; import '../../util/image_exception.dart'; @@ -19,7 +20,7 @@ class PsdChannel { int id; int? dataLength; - late Uint8List data; + Uint8List? data; PsdChannel(this.id, this.dataLength); @@ -38,6 +39,9 @@ class PsdChannel { void readPlane(InputBuffer input, int width, int height, int? bitDepth, [int? compression, Uint16List? lineLengths, int planeNum = 0]) { + if (input.length < 2) { + return; + } compression ??= input.readUint16(); switch (compression) { @@ -70,7 +74,7 @@ class PsdChannel { } if (len > input.length) { data = Uint8List(len); - data.fillRange(0, len, 255); + data!.fillRange(0, len, 255); return; } @@ -88,23 +92,27 @@ class PsdChannel { var pos = 0; var lineIndex = planeNum * height; if (lineIndex >= lineLengths.length) { - data.fillRange(0, data.length, 255); + data!.fillRange(0, data!.length, 255); return; } for (var i = 0; i < height; ++i) { final len = lineLengths[lineIndex++]; final s = input.readBytes(len); - _decodeRLE(s, data, pos); + _decodeRLE(s, data!, pos); pos += width; } } void _decodeRLE(InputBuffer src, Uint8List dst, int dstIndex) { while (!src.isEOS) { - var n = src.readInt8(); + var n = 0; + n = src.readInt8(); if (n < 0) { n = 1 - n; + if (src.isEOS) { + break; + } final b = src.readByte(); if ((dstIndex + n) > dst.length) { n = dst.length - dstIndex; @@ -117,6 +125,7 @@ class PsdChannel { if ((dstIndex + n) > dst.length) { n = dst.length - dstIndex; } + n = min(n, src.length); for (var i = 0; i < n; ++i) { dst[dstIndex++] = src.readByte(); } diff --git a/lib/src/formats/psd/psd_image.dart b/lib/src/formats/psd/psd_image.dart index 4c4de514..25103188 100644 --- a/lib/src/formats/psd/psd_image.dart +++ b/lib/src/formats/psd/psd_image.dart @@ -41,6 +41,15 @@ class PsdImage implements DecodeInfo { final imageResources = {}; bool hasAlpha = false; + static const resourceBlockSignature = 0x3842494d; // '8BIM' + + late InputBuffer? _input; + + //InputBuffer? _colorData; + InputBuffer? _imageResourceData; + InputBuffer? _layerAndMaskData; + InputBuffer? _imageData; + @override Color? get backgroundColor => null; @@ -53,14 +62,13 @@ class PsdImage implements DecodeInfo { } var len = _input!.readUint32(); - /*_colorData =*/ - _input!.readBytes(len); + /*_colorData =*/ _input!.readBytes(len); len = _input!.readUint32(); - /*_imageResourceData =*/ _input!.readBytes(len); + _imageResourceData = _input!.readBytes(len); len = _input!.readUint32(); - /*_layerAndMaskData =*/ _input!.readBytes(len); + _layerAndMaskData = _input!.readBytes(len); _imageData = _input!.readBytes(_input!.length); } @@ -85,16 +93,16 @@ class PsdImage implements DecodeInfo { // Image Resource Block: // Image resources are used to store non-pixel data associated with images, // such as pen tool paths. - //_readImageResources(); + _readImageResources(); - //_readLayerAndMaskData(); + _readLayerAndMaskData(); _readMergeImageData(); _input = null; //_colorData = null; - //_imageResourceData = null; - //_layerAndMaskData = null; + _imageResourceData = null; + _layerAndMaskData = null; _imageData = null; return true; @@ -135,7 +143,7 @@ class PsdImage implements DecodeInfo { //var di = (layer.top! + y) * width * 4 + layer.left! * 4; final dy = layer.top! + y; for (int? x = 0, sx = layer.left; x! < layer.width; ++x, ++sx) { - final srcP = src.getPixel(x, y); + final srcP = src!.getPixel(x, y); final br = srcP.r.toInt(); final bg = srcP.g.toInt(); final bb = srcP.b.toInt(); @@ -408,7 +416,7 @@ class PsdImage implements DecodeInfo { // TODO support indexed and duotone images. } - /*void _readImageResources() { + void _readImageResources() { _imageResourceData!.rewind(); while (!_imageResourceData!.isEOS) { final blockSignature = _imageResourceData!.readUint32(); @@ -433,9 +441,9 @@ class PsdImage implements DecodeInfo { PsdImageResource(blockId, blockName, blockData); } } - }*/ + } - /*void _readLayerAndMaskData() { + void _readLayerAndMaskData() { _layerAndMaskData!.rewind(); var len = _layerAndMaskData!.readUint32(); if ((len & 1) != 0) { @@ -484,7 +492,7 @@ class PsdImage implements DecodeInfo { /*int kind =*/ ..readByte(); } - }*/ + } void _readMergeImageData() { _imageData!.rewind(); @@ -509,8 +517,11 @@ class PsdImage implements DecodeInfo { colorMode, depth, width, height, mergeImageChannels); } - static int _ch(List data, int si, int ns) => - ns == 1 ? data[si] : ((data[si] << 8) | data[si + 1]) >> 8; + static int _ch(List? data, int si, int ns) => data == null + ? 0 + : ns == 1 + ? data[si] + : ((data[si] << 8) | data[si + 1]) >> 8; static Image createImageFromChannels(PsdColorMode? colorMode, int? bitDepth, int width, int height, List channelList) { @@ -596,13 +607,4 @@ class PsdImage implements DecodeInfo { return output; } - - static const resourceBlockSignature = 0x3842494d; // '8BIM' - - late InputBuffer? _input; - - //InputBuffer _colorData; - //late InputBuffer? _imageResourceData; - //late InputBuffer? _layerAndMaskData; - late InputBuffer? _imageData; } diff --git a/lib/src/formats/psd/psd_layer.dart b/lib/src/formats/psd/psd_layer.dart index dbe17914..dae8a4bb 100644 --- a/lib/src/formats/psd/psd_layer.dart +++ b/lib/src/formats/psd/psd_layer.dart @@ -80,7 +80,7 @@ class PsdLayer { Map additionalData = {}; List children = []; PsdLayer? parent; - late Image layerImage; + Image? layerImage; List effects = []; static const signature = 0x3842494d; // '8BIM' diff --git a/lib/src/formats/tiff/tiff_image.dart b/lib/src/formats/tiff/tiff_image.dart index 9213aeae..fcfbf8e8 100644 --- a/lib/src/formats/tiff/tiff_image.dart +++ b/lib/src/formats/tiff/tiff_image.dart @@ -385,6 +385,9 @@ class TiffImage { for (var y = 0, py = outY; y < tileHeight; ++y, ++py) { for (var x = 0, px = outX; x < tileWidth; ++x, ++px) { + if (byteData.isEOS) { + break; + } if (samplesPerPixel == 1) { if (sampleFormat == TiffFormat.float) { num sample = 0; diff --git a/lib/src/image/image.dart b/lib/src/image/image.dart index e46be3a9..a6a5e86b 100644 --- a/lib/src/image/image.dart +++ b/lib/src/image/image.dart @@ -832,7 +832,9 @@ class Image extends Iterable { numChannels ??= this.numChannels; alpha ??= formatMaxValue[format]; - if (withPalette && + // Commented out because it causes problems converting a uint8 w/ palette + // to a uint1 w/ palette + /*if (withPalette && (numChannels >= 4 || !(format == Format.uint1 || format == Format.uint2 || @@ -841,7 +843,7 @@ class Image extends Iterable { (format.index < Format.uint8.index && this.format.index >= Format.uint8.index)) { withPalette = false; - } + }*/ if (format == this.format && numChannels == this.numChannels && diff --git a/lib/src/transform/copy_crop.dart b/lib/src/transform/copy_crop.dart index 8c4290be..b89e7961 100644 --- a/lib/src/transform/copy_crop.dart +++ b/lib/src/transform/copy_crop.dart @@ -48,7 +48,7 @@ Image copyCrop(Image src, final c4x = x1 + rad - 1; final c4y = y2 - rad + 1; - final iter = src.getRange(x1, y1, width, height); + final iter = frame.getRange(x1, y1, width, height); while (iter.moveNext()) { final p = iter.current; final px = p.x; diff --git a/lib/src/util/_internal.dart b/lib/src/util/_internal.dart index 8f85b735..f416f32c 100644 --- a/lib/src/util/_internal.dart +++ b/lib/src/util/_internal.dart @@ -1,6 +1,5 @@ /// Annotates a function or class that's for internal use within the image /// library and is not to be exported as part of the main API. - const internal = _Internal(); class _Internal { diff --git a/lib/src/util/clip_line.dart b/lib/src/util/clip_line.dart index e9658082..62371bb0 100644 --- a/lib/src/util/clip_line.dart +++ b/lib/src/util/clip_line.dart @@ -4,7 +4,6 @@ /// Results are stored in [line]. /// If [line] falls completely outside of [rect], false is returned, otherwise /// true is returned. - bool clipLine(List line, List rect) { var x0 = line[0]; var y0 = line[1]; diff --git a/lib/src/util/input_buffer.dart b/lib/src/util/input_buffer.dart index bdadde20..32a27ac4 100644 --- a/lib/src/util/input_buffer.dart +++ b/lib/src/util/input_buffer.dart @@ -1,4 +1,5 @@ import 'dart:convert'; +import 'dart:math'; import 'dart:typed_data'; import 'bit_utils.dart'; @@ -16,14 +17,16 @@ class InputBuffer { InputBuffer(this.buffer, {this.bigEndian = false, this.offset = 0, int? length}) : start = offset, - end = (length == null) ? buffer.length : offset + length; + end = min( + buffer.length, (length == null) ? buffer.length : offset + length); /// Create a copy of [other]. InputBuffer.from(InputBuffer other, {int offset = 0, int? length}) : buffer = other.buffer, offset = other.offset + offset, start = other.start, - end = (length == null) ? other.end : other.offset + offset + length, + end = min(other.buffer.length, + (length == null) ? other.end : other.offset + offset + length), bigEndian = other.bigEndian; /// The current read position relative to the start of the buffer. diff --git a/pubspec.yaml b/pubspec.yaml index 9ade18cf..393b6349 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,5 +1,5 @@ name: image -version: 4.2.1 +version: 4.3.1 description: >- Dart Image Library provides server and web apps the ability to load, manipulate, and save images with various image file formats. diff --git a/test/_data/gif/homer.gif b/test/_data/gif/homer.gif new file mode 100644 index 00000000..693c2c09 Binary files /dev/null and b/test/_data/gif/homer.gif differ diff --git a/test/_data/psd/rectangles.psd b/test/_data/psd/rectangles.psd new file mode 100644 index 00000000..c0340326 Binary files /dev/null and b/test/_data/psd/rectangles.psd differ diff --git a/test/formats/bmp_test.dart b/test/formats/bmp_test.dart index 52cb6f72..ed9881e3 100644 --- a/test/formats/bmp_test.dart +++ b/test/formats/bmp_test.dart @@ -32,6 +32,24 @@ void main() { }); } + test('uint1', () async { + await (Command() + ..createImage(width: 256, height: 256) + ..filter((image) { + for (final p in image) { + p + ..r = p.x % 255 + ..g = p.y % 255; + } + return image; + }) + ..grayscale() + ..quantize(numberOfColors: 2, dither: DitherKernel.floydSteinberg) + ..convert(format: Format.uint1, withPalette: true) + ..writeToFile('$testOutputPath/bmp/bmp_1.bmp')) + .execute(); + }); + test('uint4', () async { await (Command() ..createImage(width: 256, height: 256, format: Format.uint4) diff --git a/test/formats/psd_test.dart b/test/formats/psd_test.dart index 951076e2..b8ec1774 100644 --- a/test/formats/psd_test.dart +++ b/test/formats/psd_test.dart @@ -11,18 +11,30 @@ void main() { final files = dir.listSync(); group('psd', () { - for (var f in files.whereType()) { + for (final f in files.whereType()) { if (!f.path.endsWith('.psd')) { continue; } final name = f.uri.pathSegments.last; test(name, () { - final psd = PsdDecoder().decode(f.readAsBytesSync()); + final decoder = PsdDecoder(); + final psd = decoder.decode(f.readAsBytesSync()); expect(psd, isNotNull); File('$testOutputPath/psd/$name.png') ..createSync(recursive: true) ..writeAsBytesSync(encodePng(psd!)); + + var li = 0; + for (final layer in decoder.info!.layers) { + final layerImg = layer.layerImage; + if (layerImg != null) { + File('$testOutputPath/psd/${name}_${li}_${layer.name}.png') + ..createSync(recursive: true) + ..writeAsBytesSync(encodePng(layerImg)); + } + ++li; + } }); } }); diff --git a/test/transform/copy_crop_test.dart b/test/transform/copy_crop_test.dart index 2553a5c1..7a2c2db7 100644 --- a/test/transform/copy_crop_test.dart +++ b/test/transform/copy_crop_test.dart @@ -6,6 +6,20 @@ import '../_test_util.dart'; void main() { group('Transform', () { + test('crop with radius', () async { + final g1 = await decodeGifFile('test/_data/gif/homer.gif'); + final g2 = copyCrop( + g1!, + x: 0, + y: 0, + width: 500, // this is just the width of the original animation + height: 375, // this is just the height of the original animation + radius: 100, + ); + await encodeGifFile('$testOutputPath/transform/copyCrop_radius.gif', g2); + await encodePngFile('$testOutputPath/transform/copyCrop_radius.png', g2); + }); + test('copyCrop', () { final bytes = File('test/_data/png/buck_24.png').readAsBytesSync(); final i0 = PngDecoder().decode(bytes)!;