Set DPI on a PNG when saving a PGraphics?

edited April 2018 in How To...

Does anyone know of a method to set the Metadata of a PNG to a specific value (such as 300 DPI) when saving from a PGraphics? I'm hoping it will be possible.

Answers

  • edited March 2018 Answer ✓

    Hmm. Well, PNG format supports physical size in the phys chunk -- it isn't quite DPI, because it is in meters, but close.

    I don't know if you can generate metadata like that through saveImage. If not, perhaps you might call an external tool to add the metadata after the image is saved into a file ?

    https://stackoverflow.com/a/27336389

  • edited March 2018

    Okay, I slapped this together and, well, it works:

    (and by slapped together, I mean I stole bits of code and duct taped them together)

    import java.awt.image.BufferedImage;
    import java.util.Iterator;
    import javax.imageio.*;
    import javax.imageio.metadata.*;
    import javax.imageio.stream.*;
    import java.awt.Graphics2D;
    
    
    BufferedImage gridImage;
    PImage p;
    
    int DPI = 400;
    float  INCH_2_CM = 2.54;
    File loadThis;
    
    void setup() {
      size(300, 300);
      p = loadImage("test.png");
      gridImage = toBufferedImage(p);
      loadThis = new File(sketchPath("test.png"));
    
      try {
        saveGridImage(loadThis);
      }
      catch(IOException e) {
        println(e.getMessage());
      }
      exit();
    }
    
    void draw() {
      background(255);
    }
    
    void saveGridImage(File output) throws IOException {
      output.delete();
    
      final String formatName = "png";
    
      for (Iterator<ImageWriter> iw = ImageIO.getImageWritersByFormatName(formatName); iw.hasNext(); ) {
        ImageWriter writer = iw.next();
        ImageWriteParam writeParam = writer.getDefaultWriteParam();
        ImageTypeSpecifier typeSpecifier = ImageTypeSpecifier.createFromBufferedImageType(BufferedImage.TYPE_INT_RGB);
        IIOMetadata metadata = writer.getDefaultImageMetadata(typeSpecifier, writeParam);
        if (metadata.isReadOnly() || !metadata.isStandardMetadataFormatSupported()) {
          continue;
        }
    
        setDPI(metadata);
    
        final ImageOutputStream stream = ImageIO.createImageOutputStream(output);
    
        try {
          writer.setOutput(stream);
          writer.write(metadata, new IIOImage(gridImage, null, metadata), writeParam);
        } 
        finally {
          writer.dispose();
          stream.flush();
          stream.close();
        }
        break;
      }
    }
    
    private void setDPI(IIOMetadata metadata) throws IIOInvalidTreeException {
    
      // for PMG, it's dots per millimeter
      double dotsPerMilli = 1.0 * DPI / 10 / INCH_2_CM;
    
      IIOMetadataNode horiz = new IIOMetadataNode("HorizontalPixelSize");
      horiz.setAttribute("value", Double.toString(dotsPerMilli));
    
      IIOMetadataNode vert = new IIOMetadataNode("VerticalPixelSize");
      vert.setAttribute("value", Double.toString(dotsPerMilli));
    
      IIOMetadataNode dim = new IIOMetadataNode("Dimension");
      dim.appendChild(horiz);
      dim.appendChild(vert);
    
      IIOMetadataNode root = new IIOMetadataNode("javax_imageio_1.0");
      root.appendChild(dim);
    
      metadata.mergeTree("javax_imageio_1.0", root);
    }
    
    BufferedImage toBufferedImage(PImage img) {
      // Create a buffered image with transparency
      BufferedImage bimage = new BufferedImage(img.width, img.height, BufferedImage.TYPE_INT_ARGB);
    
      // Draw the image on to the buffered image
      Graphics2D bGr = bimage.createGraphics();
      bGr.drawImage(img.getImage(), 0, 0, null);
      bGr.dispose();
    
      // Return the buffered image
      return bimage;
    }
    
  • edited April 2018 Answer ✓

    Thanks for sharing this solution!

    I haven't gone through the toBufferedImage and saveGridImage code in detail, but some quick feedback:

    1. pass your arguments as arguments -- the whole point of your code is to set DPI, so make your DPI-setting functions accept a DPI argument, don't use a global directly.
    2. check your logic. for example, you have an if that contains a continue and has no else -- this does nothing unless there are side-effects to the test, so you can delete it. Also, you have a for loop that ends in a break -- this will never loop, so just drop the for and execute the loop setup as normal normal lines of code.
    3. make your demo sketch work for others without requiring an included test.png -- I pointed it at a common Processing resource.
    4. watch your variable names. You named your destination (output) file handle "loadThis" -- which makes it sound like that is where your image data comes from; it isn't.
    5. if your sketch runs setup() and then exits, no need to define a draw()

    So:

    // by Bird 2018-03-27 Processing 3.3.6 -- lightly revised JD
    // downloads an image, sets PNG DPI to 400, saves to disk
    // forum.processing.org/two/discussion/27406/set-dpi-on-a-png-when-saving-a-pgraphics
    
    import java.awt.image.BufferedImage;
    import java.util.Iterator;
    import javax.imageio.*;
    import javax.imageio.metadata.*;
    import javax.imageio.stream.*;
    import java.awt.Graphics2D;
    
    int DPI = 400;
    
    void setup() {
      size(300, 300);
      PImage imgSource = loadImage("https:" + "//processing.org/img/" + "processing3-logo.png");
      BufferedImage gridImage = toBufferedImage(imgSource);
      File output = new File(sketchPath("processing3-logo.png"));
      try {
        saveGridImage(gridImage, output, DPI);
      }
      catch(IOException e) {
        println(e.getMessage());
      }
      exit();
    }
    
    void saveGridImage(BufferedImage gridImage, File output, int dpi) throws IOException {
      String formatName = "png";
      output.delete();
      Iterator<ImageWriter> iw = ImageIO.getImageWritersByFormatName(formatName);
      iw.hasNext();
      ImageWriter writer = iw.next();
      ImageWriteParam writeParam = writer.getDefaultWriteParam();
      ImageTypeSpecifier typeSpecifier = ImageTypeSpecifier.createFromBufferedImageType(BufferedImage.TYPE_INT_RGB);
      IIOMetadata metadata = writer.getDefaultImageMetadata(typeSpecifier, writeParam);
      setDPI(metadata, dpi);
      final ImageOutputStream stream = ImageIO.createImageOutputStream(output);
      try {
        writer.setOutput(stream);
        writer.write(metadata, new IIOImage(gridImage, null, metadata), writeParam);
      } 
      finally {
        writer.dispose();
        stream.flush();
        stream.close();
      }
    }
    
    private void setDPI(IIOMetadata metadata, int dpi) throws IIOInvalidTreeException {
      // for PNG, it's dots per millimeter
      float  inch_2_cm = 2.54;
      double dotsPerMilli = 1.0 * dpi / 10 / inch_2_cm;
      IIOMetadataNode horiz = new IIOMetadataNode("HorizontalPixelSize");
      horiz.setAttribute("value", Double.toString(dotsPerMilli));
      IIOMetadataNode vert = new IIOMetadataNode("VerticalPixelSize");
      vert.setAttribute("value", Double.toString(dotsPerMilli));
      IIOMetadataNode dim = new IIOMetadataNode("Dimension");
      dim.appendChild(horiz);
      dim.appendChild(vert);
      IIOMetadataNode root = new IIOMetadataNode("javax_imageio_1.0");
      root.appendChild(dim);
      metadata.mergeTree("javax_imageio_1.0", root);
    }
    
    BufferedImage toBufferedImage(PImage img) {
      // Create a buffered image with transparency
      BufferedImage bimage = new BufferedImage(img.width, img.height, BufferedImage.TYPE_INT_ARGB);
      // Draw the image on to the buffered image
      Graphics2D bGr = bimage.createGraphics();
      bGr.drawImage(img.getImage(), 0, 0, null);
      bGr.dispose();
      // Return the buffered image
      return bimage;
    }
    
Sign In or Register to comment.