How Do You Specify DPI in .png Image File

S

Steve Yates

I understand .png files store resolution as dots per meter rather than
dots per inch, but how do I specify the resolution value to be stored
in the file saved with ImageIO.

It appears that no resolution is stored in the image by default, and
when I load the image into Windows Paint it assumes 96 dpi, but when I
load it into Paint Shop Pro it assumes 200 dpi. If I save the file
from Paint Shop Pro, Paint will then see it as 200 dpi.

I'd like to specify that the image is 300 dpi. It looks like
ImageWriteParam and/or IIOMetadata should be useful here but I haven't
figured it out yet.

Thanks,
Steve Yates
 
S

Steve Yates

     Take the image, H pixels across by V pixels tall.  Display
it on the screen of your iPhone; how many dots per inch?  Display
the same image on the Jumbotron at the local sports temple; how
many dots per inch?  The same image in both cases, mind you: H
across by V tall, H*V pixels in grand total -- and two vastly
different DPI's.

     The size of a dot (i.e., 1/DPI) is a property of the display
medium, not of the image.  The ratio of dot spacings (some images
sample more closely along one axis than the other; "non-square
pixels") might be thought of as a property of the image, but the
actual dot-to-dot interval depends on how the image is rendered.

That is all true, except image files such as the .png format do allow
you to store a value stating an intended dots per inch of is for the
image, or maybe more accurately, what the dots per inch was of the
orginating device.

Any decent graphics program such as Paint Shop Pro lets you set the
dots per inch value along with the number of pixels when creating an
image. I can create a new 300x300 image with a dpi of 100, and a
300x300 image with a dpi of 300. Both images will contain 90,000
total pixels and occupy basically the same amount of space on disk,
but if I print the images at 100% scaling, the first will be 3 inches
square, and the second will print in a 1 inch sqaure.

It is true that if you copy an image pixel by pixel directly to an
output device, the size of the image you see will depend upon the
resoultion of the output device. But if the graphics program takes
into account the original image resolution and the resolution of the
poutput device, it can recreate the image in the proper size.

I know the .png file format allows this to be stored as part of the
file, I just need to understand how to make Java insert this
information into the file. The IIOMetaData seems to be the key, but I
haven't figured out the exact property to set yet and was hoping
someone here might know. Since the file written by Paint Shop Pro
contains this information I will see if I can browse the metadata of
this file in Java and determine where this value is stored.

Steve Yates
 
M

markspace

Steve said:
contains this information I will see if I can browse the metadata of
this file in Java and determine where this value is stored.

That's what I would do, just dump all the metadata and see what's in
there. Note that some devices or applications assume a default value if
none is present, so if you don't see anything likely in your dump, you
might have to edit the DPI value to something else to make PSP actually
store a value.
 
K

Knute Johnson

Steve said:
I understand .png files store resolution as dots per meter rather than
dots per inch, but how do I specify the resolution value to be stored
in the file saved with ImageIO.

It appears that no resolution is stored in the image by default, and
when I load the image into Windows Paint it assumes 96 dpi, but when I
load it into Paint Shop Pro it assumes 200 dpi. If I save the file
from Paint Shop Pro, Paint will then see it as 200 dpi.

I'd like to specify that the image is 300 dpi. It looks like
ImageWriteParam and/or IIOMetadata should be useful here but I haven't
figured it out yet.

Thanks,
Steve Yates

Neither the BMPImageWriteParam class nor the JPEGImageWriteParam class
have an option to set DPI. There is no PNGImageWriteParam class
although I do not know why. Anyway, that is where I would expect to
find the option to set the DPI and since it isn't there, I think you
will either have to write your own or figure out something else.
 
S

Steve Yates

That's what I would do, just dump all the metadata and see what's in
there.  Note that some devices or applications assume a default value if
none is present, so if you don't see anything likely in your dump, you
might have to edit the DPI value to something else to make PSP actually
store a value.

This is what I came up with. It seems that there must be a more
direct way to get at some of these objects, but this does give me what
I was looking for. I am seeing an unwanted change in the metadata
though that concerns me.


static private void writePngFile(RenderedImage image, String
filename, int dotsPerInch) {

String dotsPerMeter = String.valueOf((int) (dotsPerInch / 0.0254));

// retrieve list of ImageWriters for png images (most likely only
one but who knows)
Iterator<ImageWriter> imageWriters =
ImageIO.getImageWritersByFormatName("png");

// loop through available ImageWriters until one succeeds
while (imageWriters.hasNext()) {
ImageWriter iw = imageWriters.next();

// get default metadata for png files
ImageWriteParam iwp = iw.getDefaultWriteParam();
IIOMetadata metadata = iw.getDefaultImageMetadata(new
ImageTypeSpecifier(image), iwp);

// get png specific metatdata tree
String pngFormatName = metadata.getNativeMetadataFormatName();
IIOMetadataNode pngNode =
(IIOMetadataNode) metadata.getAsTree(pngFormatName);

// find pHYs node, or create it if it doesn't exist
IIOMetadataNode physNode = null;
NodeList childNodes = pngNode.getElementsByTagName("pHYs");
if (childNodes.getLength() == 0) {
physNode = new IIOMetadataNode("pHYs");
pngNode.appendChild(physNode);
} else if (childNodes.getLength() == 1) {
physNode = (IIOMetadataNode) childNodes.item(0);
} else {
throw new IllegalStateException("Don't know what to do with
multiple pHYs nodes");
}

physNode.setAttribute("pixelsPerUnitXAxis", dotsPerMeter);
physNode.setAttribute("pixelsPerUnitYAxis", dotsPerMeter);
physNode.setAttribute("unitSpecifier", "meter");

try {
metadata.setFromTree(pngFormatName, pngNode);
IIOImage iioImage = new IIOImage(image, null, metadata);
File file = new File(filename);
ImageOutputStream ios = ImageIO.createImageOutputStream(file);
iw.setOutput(ios);
iw.write(iioImage);
ios.flush();
ios.close();
} catch (Exception e) {
e.printStackTrace();
continue;
}

break;
}

}


I copied a dump of the image metadata tree after the first line in the
try block blow. The pHYs block in the javax_imageio_png_1.0 is what
my code added, and the HorizontalPixelSize and VerticalPixelSize nodes
in the javax_imageio_1.0/Dimension block were added as a side effect.

That is fine, but the bitDepth in javax_imageio_png_1.0/IHDR changed
from 8 to 3, and the BitsPerSample in javax_imageio_1.0/Data changed
from "8 8 8" to "3 3 3". From the documentation I have seen, 3 is not
a valid value for bitDepth, so this concerns me. Everything else is
unchanged from the dump of the defaults retrieved from the writer.

When I read the file with an ImageReader and dump the metadata I see 8
for the bitDepth though, and when I open the document in Paint Shop
Pro it shows me the resolution is 300 dots per inch as I wanted. It
still shows the color depth as 24 bits/16 million colors, which is a
good sign, because I would expect only 256 colors with 3 bits. So
while I don't understand why the values are changing in the metadata
tree, they are somehow getting changed back before the image file is
actually written. So I guess this solution will work.


javax_imageio_png_1.0:|null|
IHDR:|null|
Attributes
width:|0|
height:|0|
bitDepth:|3|
colorType:|RGB|
compressionMethod:|deflate|
filterMethod:|adaptive|
interlaceMethod:|none|
pHYs:|null|
Attributes
pixelsPerUnitXAxis:|11811|
pixelsPerUnitYAxis:|11811|
unitSpecifier:|meter|

javax_imageio_1.0:|null|
Chroma:|null|
ColorSpaceType:|null|
Attributes
name:|RGB|
NumChannels:|null|
Attributes
value:|3|
BlackIsZero:|null|
Attributes
value:|true|
Compression:|null|
CompressionTypeName:|null|
Attributes
value:|deflate|
Lossless:|null|
Attributes
value:|true|
NumProgressiveScans:|null|
Attributes
value:|1|
Data:|null|
PlanarConfiguration:|null|
Attributes
value:|PixelInterleaved|
SampleFormat:|null|
Attributes
value:|UnsignedIntegral|
BitsPerSample:|null|
Attributes
value:|3 3 3|
Dimension:|null|
PixelAspectRatio:|null|
Attributes
value:|1.0|
ImageOrientation:|null|
Attributes
value:|Normal|
HorizontalPixelSize:|null|
Attributes
value:|0.08466683|
VerticalPixelSize:|null|
Attributes
value:|0.08466683|
Transparency:|null|
Alpha:|null|
Attributes
value:|none|
 
R

Roedy Green

I think you
will either have to write your own or figure out something else.

Worse comes to worse, you would have to read the finished image back
in, and patch in the value, by adding the new metadata field.

Here is a bit of code that extracts the image dimensions of a PNG file
without loading it. Your code would be along these same lines.


/*
* @(#)ImageInfo.java
*
* Summary: Rapidly determine the gif or jpg or png image width and
height without loading the image.
*
* Copyright: (c) 2003-2009 Roedy Green, Canadian Mind Products,
http://mindprod.com
*
* Licence: This software may be copied and used freely for any
purpose but military.
* http://mindprod.com/contact/nonmil.html
*
* Requires: JDK 1.1+
*
* Created with: IntelliJ IDEA IDE.
*
* Version History:
* 1.1 2006-03-04
*/
package com.mindprod.common11;

import com.mindprod.ledatastream.LEDataInputStream;

import java.io.DataInputStream;
import java.io.FileInputStream;
import java.io.IOException;

/**
* Rapidly determine the gif or jpg or png image width and height
without loading the image.
*
* @author Roedy Green, Canadian Mind Products
* @version 1.1 2006-03-04
* @since 2003-05-15
*/
public final class ImageInfo
{
// -------------------------- PUBLIC STATIC METHODS
--------------------------

/**
* Summary: Rapidly calculate image size without loading the
image.
* <p/>
* get the height and width of a gif or jpg image without having
to read the entire Image into RAM. This works only
* with local images, not ones out on the web accessible by URL.
* Works with fully qualified name, not necessarily anything to do
with the htmlmacros package.
*
* @param imageFilename filename. Must end in .jpg or .gif
*
* @return length-2 array of two numbers, width and height of the
image, or 0,0 if it could not be found. We don't
* return a Dimension object because it provides doubles,
not ints.
* @noinspection WeakerAccess
* @see com.mindprod.ledatastream.LEDataStream
*/
public static int[] getImageDimensions( String imageFilename )
{
int width = 0;
int height = 0;
LEDataInputStream inle = null;
DataInputStream inbe = null;
final int place = imageFilename.lastIndexOf( '.' );
if ( place < 0 )
{
return new int[] { 0, 0 };
}
final String ext = imageFilename.substring( place + 1
).toLowerCase();
try
{
try
{
if ( ext.equals( "gif" ) )
{
// signature GIF89a i.e. 0x474946383961
// or GIF87a
// just check first 4 chars
// width at offset 0x06 and height at 0x08 16-bit
little
// endian
inle = new LEDataInputStream( new FileInputStream(
imageFilename ) );
int signature4 = inle.readInt();
if ( signature4 != 0x38464947/* reversed */ )
{
throw new IOException( "not a valid gif" );
}
inle.skipBytes( 2 );
width = inle.readShort();
height = inle.readShort();
inle.close();
}
else if ( ext.equals( "jpg" ) || ext.equals( "jpeg" )
)
{
// ffd8
// in variable location: height, then width, big
endian.
inbe = new DataInputStream( new FileInputStream(
imageFilename ) );

if ( inbe.readUnsignedByte() != 0xff )
{
throw new IOException( "not a valid jpg" );
}
if ( inbe.readUnsignedByte() != 0xd8 )
{
throw new IOException( "not a valid jpg" );
}
while ( true )
{
int p1 = inbe.readUnsignedByte();
int p2 = inbe.readUnsignedByte();
if ( p1 == 0xff && 0xc0 <= p2 && p2 <= 0xc3 )
{
inbe.skipBytes( 3 );
height = inbe.readShort();
width = inbe.readShort();
break;
}
else
{
// bypass this marker
int length = inbe.readShort();
inbe.skipBytes( length - 2 );
}
}// end while
inbe.close();
}// end else
if ( ext.equals( "png" ) )
{
// see http://mindprod.com/jgloss/png.html
// The PNG file header looks like this:
// signature \211PNG\r\n\032\n 8-bytes
// ie. in hex 89504e470d0a1a0a
// chunksize 4 bytes 0x0000000D
// chunkid 4 bytes "IHDR" 0x49484452
// width 4 bytes big-endian binary
// height 4 bytes big-endian binary
inbe = new DataInputStream( new FileInputStream(
imageFilename ) );
long signature = inbe.readLong();
if ( signature != 0x89504e470d0a1a0aL )
{
throw new IOException( "not a valid png file"
);
}
inbe.skipBytes( 4 + 4 );

width = inbe.readInt();
height = inbe.readInt();
inbe.close();
}
// other file types will default to 0,0
}// end try
catch ( IOException e )
{
if ( inle != null )
{
inle.close();
}
if ( inbe != null )
{
inbe.close();
}
width = 0;
height = 0;
}
}
catch ( Exception e )
{
width = 0;
height = 0;
}
return new int[] { width, height };
}// end getImageDimensions

// --------------------------- main() method
---------------------------

/**
* Test driver to find size of an image mentioned on the command
line.
*
* @param args name of a *.gif or *.jpg or *.png image file to
test. Should print out its width and height.
*/
public static void main( String[] args )
{
if ( args.length != 1 )
{
System.out.println( "Need exactly one image filename on
the command line." );
}
String imageFilename = args[ 0 ];
int[] d = getImageDimensions( imageFilename );
System.out.println( imageFilename
+ " width:"
+ d[ 0 ]
+ " height:"
+ d[ 1 ] );
}
}
--
Roedy Green Canadian Mind Products
http://mindprod.com

"We must be very careful when we give advice to younger people: sometimes
they follow it!"
~ Edsger Wybe Dijkstra, born: 1930-05-11 died: 2002-08-06 at age: 72
 
D

Donkey Hottie

Roedy Green said:

The correct format for XML would be * <p />

But HTML is not XML, and propably never will. XHTML is a desperate, failing
try.

JavaDoc is HTML.
 

Ask a Question

Want to reply to this thread or ask your own question?

You'll need to choose a username for the site, which only take a couple of moments. After that, you can post your question and our members will help you out.

Ask a Question

Members online

No members online now.

Forum statistics

Threads
473,744
Messages
2,569,482
Members
44,901
Latest member
Noble71S45

Latest Threads

Top