pdfpicmangler.PDPng.java Source code

Java tutorial

Introduction

Here is the source code for pdfpicmangler.PDPng.java

Source

/*  This file is part of PDFPicMangler, an image resampling tool for pdf documents. 
 *  Copyright (C) 2017  Ingo Kresse
 *
 *  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 3 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 pdfpicmangler;

import java.awt.image.BufferedImage;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.Arrays;
import java.util.zip.CRC32;

import org.apache.pdfbox.cos.COSArray;
import org.apache.pdfbox.cos.COSDictionary;
import org.apache.pdfbox.cos.COSInteger;
import org.apache.pdfbox.cos.COSName;
import org.apache.pdfbox.cos.COSString;
import org.apache.pdfbox.pdmodel.PDDocument;
import org.apache.pdfbox.pdmodel.common.PDStream;
import org.apache.pdfbox.pdmodel.graphics.color.PDDeviceGray;
import org.apache.pdfbox.pdmodel.graphics.color.PDDeviceRGB;
import org.apache.pdfbox.pdmodel.graphics.color.PDIndexed;
import org.apache.pdfbox.pdmodel.graphics.xobject.PDXObjectImage;

//TODO: create file from given data
//TODO: parse and translate transparency chuck

public class PDPng extends PDXObjectImage {
    private static final byte[] PNG_MAGIC = { (byte) 137, (byte) 80, (byte) 78, (byte) 71, (byte) 13, (byte) 10,
            (byte) 26, (byte) 10 };

    private static final int PNG_TYPE_PALETTE = 1;
    private static final int PNG_TYPE_COLOR = 2;
    private static final int PNG_TYPE_ALPHA_CHANNEL = 4;

    private CRC32 checksum;

    //private PDIndexed palette;
    private COSArray paldata;

    private OutputStream data;

    private int dataLen;

    public PDPng(PDStream pngStream) {
        super(pngStream, "png");
    }

    /**
     * Construct from a stream.
     * 
     * @param doc The document to create the image as part of.
     * @param is The stream that contains the png data.
     * @throws IOException If there is an error reading the png data.
     */
    public PDPng(PDDocument doc, InputStream is) throws IOException {
        super(doc, "png");

        System.out.println("reading in png");

        COSDictionary dic = getCOSStream();

        dic.setItem(COSName.SUBTYPE, COSName.IMAGE);
        //dic.setItem(COSName.TYPE, COSName.XOBJECT);

        data = getCOSStream().createFilteredStream();

        readPng(is);

        setWidth(imageWidth);
        setHeight(imageHeight);
        dic.setInt(COSName.BITS_PER_COMPONENT, bitDepth);

        if ((colorType & PNG_TYPE_PALETTE) != 0) {
            getCOSStream().setItem(COSName.COLORSPACE, paldata);
        } else if ((colorType & PNG_TYPE_COLOR) != 0) {
            setColorSpace(PDDeviceRGB.INSTANCE);
        } else {
            setColorSpace(new PDDeviceGray());
        }

        COSDictionary filterParams = new COSDictionary();
        filterParams.setInt(COSName.PREDICTOR, 15); // png adaptive predictor
        filterParams.setInt(COSName.COLORS,
                ((colorType & PNG_TYPE_COLOR) == 0 || (colorType & PNG_TYPE_PALETTE) != 0) ? 1 : 3);
        filterParams.setInt(COSName.BITS_PER_COMPONENT, bitDepth);
        filterParams.setInt(COSName.COLUMNS, imageWidth);
        filterParams.setDirect(true);

        dic.setItem(COSName.DECODE_PARMS, filterParams);
        dic.setItem(COSName.FILTER, COSName.FLATE_DECODE);

        dic.setInt(COSName.LENGTH, dataLen);
        dic.getDictionaryObject(COSName.LENGTH).setDirect(true);
    }

    private boolean readPng(InputStream is) {
        checksum = new CRC32();
        dataLen = 0;

        try {
            boolean ok = readMagic(is);

            if (!ok) {
                throw new IOException("png magic not found.");
            }

            while (true) {
                readChunkHeader(is);

                System.out.println(chunkType + " chunk, " + chunkLen + " bytes");

                if ("IHDR".equals(chunkType)) {
                    readHeader(is);
                    readChunkChecksum(is);
                    continue;
                }
                if ("PLTE".equals(chunkType)) {
                    readPalette(is);
                    readChunkChecksum(is);
                    continue;
                }
                if ("IDAT".equals(chunkType)) {
                    readData(is);
                    readChunkChecksum(is);
                    dataLen += chunkLen;
                    continue;
                }
                if ("IEND".equals(chunkType)) {
                    readEnd(is);
                    readChunkChecksum(is);
                    break;
                }

                // unknown chunk type, eat the data
                readBytes(is, chunkLen + 4);
            }

        } catch (IOException e) {
            e.printStackTrace();
            return false;
        }

        return true;
    }

    private void readEnd(InputStream is) throws IOException {
        System.out.println("got end");
        data.close();
    }

    private void readData(InputStream is) throws IOException {
        // read chunkLen bytes into data stream
        byte[] buffer = new byte[1024];

        int bytesLeft = chunkLen;
        int len = (bytesLeft > buffer.length) ? buffer.length : bytesLeft;
        int amountRead = -1;
        while ((amountRead = is.read(buffer, 0, len)) != -1 && bytesLeft > 0) {
            data.write(buffer, 0, amountRead);
            bytesLeft -= amountRead;
            len = (bytesLeft > buffer.length) ? buffer.length : bytesLeft;

            checksum.update(buffer, 0, amountRead);
        }
        System.out.println("got " + chunkLen + " bytes of data");
    }

    private void readPalette(InputStream is) throws IOException {
        if (chunkLen > 256 * 3 || (chunkLen % 3) != 0) {
            throw new IOException("unexpected palette length " + chunkLen);
        }

        COSString data = new COSString(readBytes(is, chunkLen));

        // for some reason, readers expect an extra entry...
        byte[] extra = { 0, 0, 0 };
        data.append(extra);

        int numEntries = chunkLen / 3;

        paldata = new COSArray();
        paldata.add(COSName.getPDFName(PDIndexed.NAME));
        paldata.add(COSName.getPDFName(PDDeviceRGB.NAME));
        paldata.add(COSInteger.get(numEntries));
        paldata.add(data);

        System.out.println("got palette with " + numEntries + " entries.");
    }

    private String chunkType;
    private int chunkLen;

    private int imageHeight;
    private int imageWidth;
    private int bitDepth;

    private int colorType;

    private void readChunkHeader(InputStream is) throws IOException {
        chunkLen = readInt32(is);
        checksum.reset();
        chunkType = readType(is);
    }

    private void readChunkChecksum(InputStream is) throws IOException {
        int chkComputed = (int) checksum.getValue();
        int chkFile = readInt32(is);

        if (chkFile != chkComputed) {
            System.out.println("crc error: computed \n  0x" + Integer.toHexString(chkComputed) + " but found\n  0x"
                    + Integer.toHexString(chkFile));
            //throw...
        } else {
            System.out.println("crc ok: 0x" + Integer.toHexString(chkFile));
        }
    }

    private String readType(InputStream is) throws IOException {
        return new String(readBytes(is, 4));
    }

    private byte[] readBytes(InputStream is, int len) throws IOException {
        byte[] buffer = new byte[len];
        int amountRead = is.read(buffer);
        if (amountRead != len) {
            throw new IOException();
        }

        checksum.update(buffer);

        return buffer;
    }

    private int readInt32(InputStream is) throws IOException {
        byte[] buffer = readBytes(is, 4);
        return ((0xff & buffer[0]) << 24) | ((0xff & buffer[1]) << 16) | ((0xff & buffer[2]) << 8)
                | (0xff & buffer[3]);
    }

    private int readInt8(InputStream is) throws IOException {
        byte[] buffer = readBytes(is, 1);
        return buffer[0];
    }

    private boolean readMagic(InputStream is) throws IOException {
        byte[] buffer = readBytes(is, 8);
        return Arrays.equals(buffer, PNG_MAGIC);
    }

    private void readHeader(InputStream is) throws IOException {
        if (chunkLen != 13) {
            throw new IOException("unexpected length for IHDR chunk!");
        }

        imageWidth = readInt32(is);
        imageHeight = readInt32(is);
        bitDepth = readInt8(is);
        colorType = readInt8(is);

        int compressionMethod = readInt8(is);
        int filterMethod = readInt8(is);
        int interlaceMethod = readInt8(is);

        if (filterMethod != 0) {
            throw new IOException("unexpected filter method " + filterMethod);
        }

        if (compressionMethod != 0) {
            throw new IOException("unexpected compression method " + compressionMethod);
        }

        if (interlaceMethod != 0) {
            throw new IOException("only non-interlaced PNGs are supported in pdf. Needs recompression...");
        }

        if ((colorType & PNG_TYPE_ALPHA_CHANNEL) != 0) {
            throw new IOException("pdf does not support 'embedded' alpha channel. Needs recompression...");
        }

        System.out.println("got header");
    }

    @Override
    public BufferedImage getRGBImage() throws IOException {
        // TODO Auto-generated method stub
        return null;
    }

    @Override
    public void write2OutputStream(OutputStream out) throws IOException {
        // TODO Auto-generated method stub

    }

}