Skip to content

Commit

Permalink
Merge pull request #675 from tdenniston/png-phys
Browse files Browse the repository at this point in the history
Add physical pixel size handling for PNG.
  • Loading branch information
brendan-duncan authored Aug 27, 2024
2 parents 2c3df91 + 88a2d9f commit a7b051e
Show file tree
Hide file tree
Showing 4 changed files with 82 additions and 1 deletion.
46 changes: 46 additions & 0 deletions lib/src/formats/png/png_info.dart
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,51 @@ class PngColorType {

enum PngFilterType { none, sub, up, average, paeth }

/// The intended physical pixel size of the image.
/// See <https://www.w3.org/TR/png-3/#11pHYs>.
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;
Expand All @@ -44,6 +89,7 @@ class PngInfo implements DecodeInfo {
int iccpCompression = 0;
Uint8List? iccpData;
Map<String, String> textData = {};
PngPhysicalPixelDimensions? pixelDimensions;

// APNG extensions
@override
Expand Down
9 changes: 9 additions & 0 deletions lib/src/formats/png_decoder.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down
11 changes: 10 additions & 1 deletion lib/src/formats/png_encoder.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -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++;
Expand Down Expand Up @@ -410,4 +418,5 @@ class PngEncoder extends Encoder {
bool isAnimated = false;
OutputBuffer? output;
Map<String, String>? textData;
PngPhysicalPixelDimensions? pixelDimensions;
}
17 changes: 17 additions & 0 deletions test/formats/png_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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)!;
Expand Down

0 comments on commit a7b051e

Please sign in to comment.