How to combine multiple PNGs into one big PNG file?
Asked Answered
A

9

38

I have approx. 6000 PNG files (256*256 pixels) and want to combine them into a big PNG holding all of them programmatically.

What's the best/fastest way to do that?

(The purpose is printing on paper, so using some web-technology is not an option and having one, single picture file will eliminate many usage errors.)

I tried fahd's suggestion but I get a NullPointerException when I try to create a BufferedImage with 24576 pixels wide and 15360 pixels high. Any ideas?

Aesir answered 13/10, 2010 at 9:25 Comment(9)
You want one massive PNG? that will be 1536000x1536000 pixels? I must say a basic image library would be a better option here.Shew
Even if there is a way around re-encoding, a re-encoded very big image could potentially compress much better, especially if the images are similar.Officialese
@kyndigs: more like 15360 x 25600 (for a 60 x 100 arrangement)Officialese
Regardless, a basic image library would be a better option :pShew
Does not seem entirely unreasonable for a big CSS sprite map.Officialese
Can your printer actually handle an image which is tens of thousands of pixels in each dimension? At 4bpp, this is 1.5GB of image data we're talking about here.Amboise
Well, there is no better way than to try it :-)Aesir
15360 x 25600 will require an A2 printer at 1200dpi.Legitimist
... and a DinA0 printer at 300dpi, which is what I want.Aesir
S
61

Create a large image which you will write to. Work out its dimensions based on how many rows and columns you want.

    BufferedImage result = new BufferedImage(
                               width, height, //work these out
                               BufferedImage.TYPE_INT_RGB);
    Graphics g = result.getGraphics();

Now loop through your images and draw them:

    for(String image : images){
        BufferedImage bi = ImageIO.read(new File(image));
        g.drawImage(bi, x, y, null);
        x += 256;
        if(x > result.getWidth()){
            x = 0;
            y += bi.getHeight();
        }
    }

Finally write it out to file:

    ImageIO.write(result,"png",new File("result.png"));
Scumble answered 13/10, 2010 at 10:6 Comment(1)
Oh man, this was soooooo much easier than trying to use JAI and Mosaic Descriptor. THANK YOU!Antofagasta
I
8

I do not see how it would be possible "without processing and re-encoding". If you insist on using Java then I just suggest you to use JAI (project page here). With that you would create one big BufferedImage, load smaller images and draw them on the bigger one.

Or just use ImageMagick montage:

montage *.png output.png

For more information about montage, see usage.

Incredible answered 13/10, 2010 at 9:53 Comment(0)
P
8

I had some similar need some time ago (huge images -and, I my case with 16 bitdepth- to have them fully in memory was not an option). And I ended coding a PNG library to do the read/write in a sequential way. In case someone find it useful, it's here.

Updated: here's a sample code:

/**
 * Takes several tiles and join them in a single image
 * 
 * @param tiles            Filenames of PNG files to tile
 * @param dest            Destination PNG filename
 * @param nTilesX            How many tiles per row?
 */
public class SampleTileImage {

        public static void doTiling(String tiles[], String dest, int nTilesX) {
                int ntiles = tiles.length;
                int nTilesY = (ntiles + nTilesX - 1) / nTilesX; // integer ceil
                ImageInfo imi1, imi2; // 1:small tile   2:big image
                PngReader pngr = new PngReader(new File(tiles[0]));
                imi1 = pngr.imgInfo;
                PngReader[] readers = new PngReader[nTilesX];
                imi2 = new ImageInfo(imi1.cols * nTilesX, imi1.rows * nTilesY, imi1.bitDepth, imi1.alpha, imi1.greyscale,
                                imi1.indexed);
                PngWriter pngw = new PngWriter(new File(dest), imi2, true);
                // copy palette and transparency if necessary (more chunks?)
                pngw.copyChunksFrom(pngr.getChunksList(), ChunkCopyBehaviour.COPY_PALETTE
                                | ChunkCopyBehaviour.COPY_TRANSPARENCY);
                pngr.readSkippingAllRows(); // reads only metadata             
                pngr.end(); // close, we'll reopen it again soon
                ImageLineInt line2 = new ImageLineInt(imi2);
                int row2 = 0;
                for (int ty = 0; ty < nTilesY; ty++) {
                        int nTilesXcur = ty < nTilesY - 1 ? nTilesX : ntiles - (nTilesY - 1) * nTilesX;
                        Arrays.fill(line2.getScanline(), 0);
                        for (int tx = 0; tx < nTilesXcur; tx++) { // open several readers
                                readers[tx] = new PngReader(new File(tiles[tx + ty * nTilesX]));
                                readers[tx].setChunkLoadBehaviour(ChunkLoadBehaviour.LOAD_CHUNK_NEVER);
                                if (!readers[tx].imgInfo.equals(imi1))
                                        throw new RuntimeException("different tile ? " + readers[tx].imgInfo);
                        }
                        for (int row1 = 0; row1 < imi1.rows; row1++, row2++) {
                                for (int tx = 0; tx < nTilesXcur; tx++) {
                                        ImageLineInt line1 = (ImageLineInt) readers[tx].readRow(row1); // read line
                                        System.arraycopy(line1.getScanline(), 0, line2.getScanline(), line1.getScanline().length * tx,
                                                        line1.getScanline().length);
                                }
                                pngw.writeRow(line2, row2); // write to full image
                        }
                        for (int tx = 0; tx < nTilesXcur; tx++)
                                readers[tx].end(); // close readers
                }
                pngw.end(); // close writer
        }

        public static void main(String[] args) {
                doTiling(new String[] { "t1.png", "t2.png", "t3.png", "t4.png", "t5.png", "t6.png" }, "tiled.png", 2);
                System.out.println("done");
        }
}
Parallelize answered 30/5, 2011 at 12:47 Comment(3)
This is interesting but the code here is out of date with the current version of the lib. Any chance you have a similar example that is current? Many thanks.Tear
@MattFriedman Have you tried the one posted in "Snippets" ? code.google.com/p/pngj/wiki/SnippetsParallelize
The code posted at the link you provided compiles. Many thanks. Perhaps you want to go ahead and swap out the code above with the new stuff. Just a thought. Thanks again.Tear
F
3

The PNG format has no support for tiling, so there is no way you can escape at least decompressing and recompressing the data stream. If the palettes of all images are identical (or all absent), this is the only thing you really need to do. (I'm also assuming the images aren't interlaced.)

You could do this in a streaming way, only having open one "row" of PNGs at a time, reading appropriately-sized chunks from their data stream and writing them to the output stream. This way you would not need to keep entire images in memory. The most efficient way would be to program this on top of libpng yourself. You may need to keep slightly more than one scanline of pixels in memory because of the pixel prediction.

But just using the command-line utilities of ImageMagick, netpbm or similar will save you a large amount of development time for what may be little gain.

Friederike answered 13/10, 2010 at 10:3 Comment(0)
O
3

As others have pointed out, using Java is not necessarily the best bet here.

If you're going to use Java, your best bet--assuming you're sufficiently short on memory so that you can't read the entire dataset into memory multiple times and then write it out again--is to implement RenderedImage with a class that will read your PNGs off the disk upon demand. If you just create your own new BufferedImage and then try to write it out, the PNG writer will create an extra copy of the data. If you create your own RenderedImage, you can pass it to ImageIO.write(myImageSet,"png",myFileName). You can copy SampleModel and ColorModel information from your first PNG--hopefully they're all the same.

If you pretend that the entire image is multiple tiles (one tile per source image), then ImageIO.write will create a WritableRaster that is the size of the entire image data set, and will call your implementation of RenderedImage.copyData to fill it with data. If you have enough memory, this is an easy way to go (because you get a huge target set of data and can just dump all your image data into it--using the setRect(dx,dy,Raster) method--and then don't have to worry about it again). I haven't tested to see whether this saves memory, but it seems to me that it should.

Alternatively, if you pretend that the whole image is a single tile, ImageIO.write will then ask, using getTile(0,0), for the raster that corresponds to that entire image. So you have to create your own Raster, which in turn makes you create your own DataBuffer. When I tried this approach, the minimum memory usage that successfully wrote a 15360x25600 RGB PNG was -Xmx1700M (in Scala, incidentally), which is just barely over 4 bytes per pixel of written image, so there's very little overhead above one full image in memory.

The PNG data format itself is not one that requires the entire image in memory--it would work okay in chunks--but, sadly, the default implementation of the PNG writer assumes it will have the entire pixel array in memory.

Oilcan answered 13/10, 2010 at 20:7 Comment(0)
A
2

You might be best off bouncing things off another (lossless) image format. PPM is dead easy to use (and to put tiles in programmatically; it's just a big array on disk, so you'll only have to store one row of tiles at most), but it's very wasteful of space (12 bytes per pixel!).

Then use a standard converter (e.g. ppm2png) that takes the intermediary format and turns it into the giant PNG.

Azores answered 13/10, 2010 at 13:36 Comment(0)
C
2

simple python script for joining tiles into one big image:

import Image

TILESIZE = 256
ZOOM = 15
def merge_images( xmin, xmax, ymin, ymax, output) :
    out = Image.new( 'RGB', ((xmax-xmin+1) * TILESIZE, (ymax-ymin+1) * TILESIZE) ) 

    imx = 0;
    for x in range(xmin, xmax+1) :
        imy = 0
        for y in range(ymin, ymax+1) :
            tile = Image.open( "%s_%s_%s.png" % (ZOOM, x, y) )
            out.paste( tile, (imx, imy) )
            imy += TILESIZE
        imx += TILESIZE

    out.save( output )

run:

merge_images(18188, 18207, 11097, 11111, "output.png")

works for files named like %ZOOM_%XCORD_%YCORD.png , for example 15_18188_11097.png

Curlpaper answered 29/11, 2010 at 12:21 Comment(0)
I
2

Combing Images

private static void combineALLImages(String screenNames, int screens) throws IOException, InterruptedException {
    System.out.println("screenNames --> D:\\screenshots\\screen   screens --> 0,1,2 to 10/..");
    int rows = screens + 1;
    int cols = 1;
    int chunks = rows * cols ; 

     File[] imgFiles = new File[chunks];
    String files = "";
    for (int i = 0; i < chunks; i++) {
        files = screenNames + i + ".jpg";
        imgFiles[i] = new File(files);          
        System.out.println(screenNames + i + ".jpg"+"\t Screens : "+screens);    

    }

    BufferedImage sample = ImageIO.read(imgFiles[0]);
    //Initializing the final image
    BufferedImage finalImg = new BufferedImage(sample.getWidth() * cols, sample.getHeight() * rows, sample.getType());

    int index = 0;
    for (int i = 0; i < rows; i++) {
        for (int j = 0; j < cols; j++) {
            BufferedImage temp = ImageIO.read(imgFiles[index]);
            finalImg.createGraphics().drawImage(temp, sample.getWidth() * j, sample.getHeight() * i, null);
            System.out.println(screenNames + index + ".jpg");
            index++;
        }
    }
    File final_Image = new File("D:\\Screenshots\\FinalImage.jpg");
    ImageIO.write(finalImg, "jpeg", final_Image);

}
Industrials answered 6/7, 2015 at 6:36 Comment(0)
T
2

I keep coming back to this question because I have a similar problem and found an acceptable solution in another thread, which I will link here for future reference.

It doesn't exactly solve OPs problem, but it does allow to stitch horizontal slices ("tile lines") together without having to load everything into memory at the same time using AWT APIs.

Merge small images into one without allocating full image in memory

The linked repository is no longer available, but there are mirrors available.

/*******************************************************************************
 * Copyright (c) MOBAC developers
 * 
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 2 of the License, or
 * (at your option) any later version.
 * 
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 * 
 * You should have received a copy of the GNU General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 ******************************************************************************/
package mobac.utilities.imageio;

/*
 * PNGWriter.java
 *
 * Copyright (c) 2007 Matthias Mann - www.matthiasmann.de
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in
 * all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
 * THE SOFTWARE.
 *
 */

import static mobac.utilities.imageio.PngConstants.*;

import java.awt.Rectangle;
import java.awt.image.BufferedImage;
import java.awt.image.ColorModel;
import java.awt.image.DataBuffer;
import java.awt.image.DataBufferByte;
import java.awt.image.DataBufferInt;
import java.awt.image.DirectColorModel;
import java.io.BufferedOutputStream;
import java.io.DataOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.util.zip.CRC32;
import java.util.zip.Deflater;
import java.util.zip.DeflaterOutputStream;

import javax.activation.UnsupportedDataTypeException;

/**
 * A PNG writer that is able to write extra large PNG images using incremental
 * writing.
 * <p>
 * The image is processed incremental in "tile lines" - e.g. an PNG image of
 * 30000 x 20000 pixels (width x height) can be written by 200 "tile lines" of
 * size 30000 x 100 pixels. Each tile line can be written via the method
 * {@link #writeTileLine(BufferedImage)}. After writing the last line you have
 * to call {@link #finish()} which will write the final PNG structure
 * information into the {@link OutputStream}.
 * </p>
 * <p>
 * Please note that this writer creates 24bit/truecolor PNGs. Transparency and
 * alpha masks are not supported.
 * </p>
 * Bases on the PNGWriter written by Matthias Mann - www.matthiasmann.de
 * 
 * @author r_x
 */
public class PngXxlWriter {

    private static final int BUFFER_SIZE = 128 * 1024;

    private int width;
    private int height;
    private DataOutputStream dos;

    ImageDataChunkWriter imageDataChunkWriter;

    /**
     * Creates an PNG writer instance for an image with the specified width and
     * height.
     * 
     * @param width
     *            width of the PNG image to be written
     * @param height
     *            height of the PNG image to be written
     * @param os
     *            destination to write the PNG image data to
     * @throws IOException
     */
    public PngXxlWriter(int width, int height, OutputStream os) throws IOException {
        this.width = width;
        this.height = height;
        this.dos = new DataOutputStream(os);

        dos.write(SIGNATURE);

        PngChunk cIHDR = new PngChunk(IHDR);
        cIHDR.writeInt(this.width);
        cIHDR.writeInt(this.height);
        cIHDR.writeByte(8); // 8 bit per component
        cIHDR.writeByte(COLOR_TRUECOLOR);
        cIHDR.writeByte(COMPRESSION_DEFLATE);
        cIHDR.writeByte(FILTER_SET_1);
        cIHDR.writeByte(INTERLACE_NONE);
        cIHDR.writeTo(dos);
        imageDataChunkWriter = new ImageDataChunkWriter(dos);
    }

    /**
     * 
     * @param tileLineImage
     * @throws IOException
     */
    public void writeTileLine(BufferedImage tileLineImage) throws IOException {

        int tileLineHeight = tileLineImage.getHeight();
        int tileLineWidth = tileLineImage.getWidth();

        if (width != tileLineWidth)
            throw new RuntimeException("Invalid width");

        ColorModel cm = tileLineImage.getColorModel();

        if (!(cm instanceof DirectColorModel))
            throw new UnsupportedDataTypeException(
                    "Image uses wrong color model. Only DirectColorModel is supported!");

        // We process the image line by line, from head to bottom
        Rectangle rect = new Rectangle(0, 0, tileLineWidth, 1);

        DataOutputStream imageDataStream = imageDataChunkWriter.getStream();

        byte[] curLine = new byte[width * 3];
        for (int line = 0; line < tileLineHeight; line++) {
            rect.y = line;
            DataBuffer db = tileLineImage.getData(rect).getDataBuffer();
            if (db.getNumBanks() > 1)
                throw new UnsupportedDataTypeException("Image data has more than one data bank");
            if (db instanceof DataBufferByte)
                curLine = ((DataBufferByte) db).getData();
            else if (db instanceof DataBufferInt) {
                int[] intLine = ((DataBufferInt) db).getData();
                int c = 0;
                for (int i = 0; i < intLine.length; i++) {
                    int pixel = intLine[i];
                    curLine[c++] = (byte) (pixel >> 16 & 0xFF);
                    curLine[c++] = (byte) (pixel >> 8 & 0xFF);
                    curLine[c++] = (byte) (pixel & 0xFF);
                }
            } else
                throw new UnsupportedDataTypeException(db.getClass().getName());

            imageDataStream.write(FILTER_TYPE_NONE);
            imageDataStream.write(curLine);
        }
    }

    public void finish() throws IOException {
        imageDataChunkWriter.finish();
        PngChunk cIEND = new PngChunk(IEND);
        cIEND.writeTo(dos);
        cIEND.close();
        dos.flush();
    }

    static class ImageDataChunkWriter extends OutputStream {

        DeflaterOutputStream dfos;
        DataOutputStream stream;
        DataOutputStream out;
        CRC32 crc = new CRC32();

        public ImageDataChunkWriter(DataOutputStream out) throws IOException {
            this.out = out;
            dfos = new DeflaterOutputStream(new BufferedOutputStream(this, BUFFER_SIZE),
                    new Deflater(Deflater.BEST_COMPRESSION));
            stream = new DataOutputStream(dfos);
        }

        public DataOutputStream getStream() {
            return stream;
        }

        public void finish() throws IOException {
            stream.flush();
            stream.close();
            dfos.finish();
            dfos = null;
        }

        @Override
        public void write(byte[] b, int off, int len) throws IOException {
            crc.reset();
            out.writeInt(len);
            out.writeInt(IDAT);
            out.write(b, off, len);
            crc.update("IDAT".getBytes());
            crc.update(b, off, len);
            out.writeInt((int) crc.getValue());
        }

        @Override
        public void write(byte[] b) throws IOException {
            write(b, 0, b.length);
        }

        @Override
        public void write(int b) throws IOException {
            throw new IOException("Simgle byte writing not supported");
        }
    }
}
Typeface answered 17/4, 2021 at 15:54 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.