Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add physical pixel size handling for PNG. #675

Merged
merged 1 commit into from
Aug 27, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading