diff --git a/lib/src/formats/png/png_info.dart b/lib/src/formats/png/png_info.dart index 1cc6469f..fda70fc1 100644 --- a/lib/src/formats/png/png_info.dart +++ b/lib/src/formats/png/png_info.dart @@ -25,6 +25,51 @@ class PngColorType { enum PngFilterType { none, sub, up, average, paeth } +/// The intended physical pixel size of the image. +/// See . +class PngPhysicalPixelDimensions { + static const double _inchesPerM = 39.3701; + + /// Unit is unknown. + static const int unitUnknown = 0; + + /// Unit is the meter. + static const int unitMeter = 1; + + /// Pixels per unit on the X axis. + final int xPxPerUnit; + + /// Pixels per unit on the Y axis. + final int yPxPerUnit; + + /// Unit specifier, either [unitUnknown] or [unitMeter]. + final int unitSpecifier; + + /// Constructs a dimension descriptor with the given values. + const PngPhysicalPixelDimensions( + {required this.xPxPerUnit, + required this.yPxPerUnit, + required this.unitSpecifier}); + + /// Constructs a dimension descriptor specifying x and y resolution in dots + /// per inch (DPI). If [yDpi] is unspecified, [xDpi] is used for both x and y + /// axes. + PngPhysicalPixelDimensions.dpi(int xDpi, [int? yDpi]) + : xPxPerUnit = (xDpi * _inchesPerM).round(), + yPxPerUnit = ((yDpi ?? xDpi) * _inchesPerM).round(), + unitSpecifier = unitMeter; + + @override + int get hashCode => Object.hash(xPxPerUnit, yPxPerUnit, unitSpecifier); + + @override + bool operator ==(Object other) => + other is PngPhysicalPixelDimensions && + other.xPxPerUnit == xPxPerUnit && + other.yPxPerUnit == yPxPerUnit && + other.unitSpecifier == unitSpecifier; +} + class PngInfo implements DecodeInfo { @override int width = 0; @@ -44,6 +89,7 @@ class PngInfo implements DecodeInfo { int iccpCompression = 0; Uint8List? iccpData; Map textData = {}; + PngPhysicalPixelDimensions? pixelDimensions; // APNG extensions @override diff --git a/lib/src/formats/png_decoder.dart b/lib/src/formats/png_decoder.dart index 5f901ce9..85613c9e 100644 --- a/lib/src/formats/png_decoder.dart +++ b/lib/src/formats/png_decoder.dart @@ -75,6 +75,15 @@ class PngDecoder extends Decoder { } _input.skip(4); //crc break; + case 'pHYs': + final physData = InputBuffer.from(_input.readBytes(chunkSize)); + final x = physData.readUint32(); + final y = physData.readUint32(); + final unit = physData.readByte(); + _info.pixelDimensions = PngPhysicalPixelDimensions( + xPxPerUnit: x, yPxPerUnit: y, unitSpecifier: unit); + _input.skip(4); // CRC + break; case 'IHDR': final hdr = InputBuffer.from(_input.readBytes(chunkSize)); final Uint8List hdrBytes = hdr.toUint8List(); diff --git a/lib/src/formats/png_encoder.dart b/lib/src/formats/png_encoder.dart index 0d0fe095..e9ca2a5f 100644 --- a/lib/src/formats/png_encoder.dart +++ b/lib/src/formats/png_encoder.dart @@ -19,7 +19,7 @@ enum PngFilter { none, sub, up, average, paeth } class PngEncoder extends Encoder { Quantizer? _globalQuantizer; - PngEncoder({this.filter = PngFilter.paeth, this.level}); + PngEncoder({this.filter = PngFilter.paeth, this.level, this.pixelDimensions}); int _numChannels(Image image) => image.hasPalette ? 1 : image.numChannels; @@ -74,6 +74,14 @@ class PngEncoder extends Encoder { } } + if (pixelDimensions != null) { + final phys = OutputBuffer(bigEndian: true) + ..writeUint32(pixelDimensions!.xPxPerUnit) + ..writeUint32(pixelDimensions!.yPxPerUnit) + ..writeByte(pixelDimensions!.unitSpecifier); + _writeChunk(output!, 'pHYs', phys.getBytes()); + } + if (isAnimated) { _writeFrameControlChunk(image); sequenceNumber++; @@ -410,4 +418,5 @@ class PngEncoder extends Encoder { bool isAnimated = false; OutputBuffer? output; Map? textData; + PngPhysicalPixelDimensions? pixelDimensions; } diff --git a/test/formats/png_test.dart b/test/formats/png_test.dart index bbc47ecc..193e0e25 100644 --- a/test/formats/png_test.dart +++ b/test/formats/png_test.dart @@ -439,6 +439,23 @@ void main() { expect(img2?.textData?["foo"], equals("bar")); }); + test('pHYs', () { + final img = Image(width: 16, height: 16); + const phys1 = PngPhysicalPixelDimensions( + xPxPerUnit: 1000, + yPxPerUnit: 1000, + unitSpecifier: PngPhysicalPixelDimensions.unitMeter); + final png1 = PngEncoder(pixelDimensions: phys1).encode(img); + final dec1 = PngDecoder()..decode(png1); + expect(dec1.info.pixelDimensions, phys1); + + final phys2 = PngPhysicalPixelDimensions.dpi(144, 288); + final png2 = PngEncoder(pixelDimensions: phys2).encode(img); + final dec2 = PngDecoder()..decode(png2); + expect(dec2.info.pixelDimensions, isNot(phys1)); + expect(dec2.info.pixelDimensions, phys2); + }); + test('iCCP', () { final bytes = File('test/_data/png/iCCP.png').readAsBytesSync(); final image = PngDecoder().decode(bytes)!;