All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.verapdf.model.impl.pb.external.PBoxJPEG2000 Maven / Gradle / Ivy

Go to download

Java PDF Box implementation for the veraPDF Validation Java API, generated from an Xtext model.

There is a newer version: 1.26.2
Show newest version
/**
 * This file is part of veraPDF PDF Box PDF/A Validation Model Implementation, a module of the veraPDF project.
 * Copyright (c) 2015, veraPDF Consortium 
 * All rights reserved.
 *
 * veraPDF PDF Box PDF/A Validation Model Implementation is free software: you can redistribute it and/or modify
 * it under the terms of either:
 *
 * The GNU General public license GPLv3+.
 * You should have received a copy of the GNU General Public License
 * along with veraPDF PDF Box PDF/A Validation Model Implementation as the LICENSE.GPL file in the root of the source
 * tree.  If not, see http://www.gnu.org/licenses/ or
 * https://www.gnu.org/licenses/gpl-3.0.en.html.
 *
 * The Mozilla Public License MPLv2+.
 * You should have received a copy of the Mozilla Public License along with
 * veraPDF PDF Box PDF/A Validation Model Implementation as the LICENSE.MPL file in the root of the source tree.
 * If a copy of the MPL was not distributed with this file, you can obtain one at
 * http://mozilla.org/MPL/2.0/.
 */
package org.verapdf.model.impl.pb.external;

import java.util.logging.Logger;
import org.apache.pdfbox.cos.COSArray;
import org.apache.pdfbox.cos.COSName;
import org.apache.pdfbox.pdmodel.PDDocument;
import org.apache.pdfbox.pdmodel.common.PDStream;
import org.apache.pdfbox.pdmodel.graphics.color.PDDeviceCMYK;
import org.apache.pdfbox.pdmodel.graphics.color.PDICCBased;
import org.apache.pdfbox.pdmodel.graphics.color.PDLab;
import org.verapdf.model.external.JPEG2000;
import org.verapdf.model.factory.colors.ColorSpaceFactory;
import org.verapdf.model.pdlayer.PDColorSpace;
import org.verapdf.pdfa.flavours.PDFAFlavour;

import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;

/**
 * @author Maksim Bezrukov
 */
public class PBoxJPEG2000 extends PBoxExternal implements JPEG2000 {

	private static final Logger LOGGER = Logger.getLogger(PBoxJPEG2000.class.getCanonicalName());

	public static final String JPEG_2000_TYPE = "JPEG2000";

	protected static final Long DEFAULT_NR_COLOR_CHANNELS = Long.valueOf(0);
	protected static final Long DEFAULT_NR_COLOR_SPACE_SPECS = Long.valueOf(0);
	protected static final Long DEFAULT_NR_COLOR_SPACES_WITH_APPROX_FIELD = Long.valueOf(0);
	protected static final Long DEFAULT_COLR_METHOD = Long.valueOf(0);
	protected static final Long DEFAULT_COLR_ENUM_CS = null;
	protected static final Long DEFAULT_BIT_DEPTH = Long.valueOf(0);
	protected static final Boolean DEFAULT_BPCC_BOX_PRESENT = Boolean.FALSE;
	protected static final PDColorSpace DEFAULT_COLOR_SPACE = null;
	private static final byte[] sign = { 0x00, 0x00, 0x00, 0x0C, 0x6A, 0x50, 0x20, 0x20, 0x0D, 0x0A, -0x79, 0x0A };

	private static final byte[] header = { 0x6A, 0x70, 0x32, 0x68 };
	private static final byte[] ihdr = { 0x69, 0x68, 0x64, 0x72 };
	private static final byte[] bpcc = { 0x62, 0x70, 0x63, 0x63 };
	private static final byte[] colr = { 0x63, 0x6F, 0x6C, 0x72 };

	private final Long nrColorChannels;
	private final Long nrColorSpaceSpecs;
	private final Long nrColorSpacesWithApproxField;
	private final Long colrMethod;
	private final Long colrEnumCS;
	private final Long bitDepth;
	private final Boolean bpccBoxPresent;
	private final PDColorSpace colorSpace;

	private PBoxJPEG2000(Long nrColorChannels, Long nrColorSpaceSpecs, Long nrColorSpacesWithApproxField,
			Long colrMethod, Long colrEnumCS, Long bitDepth, Boolean bpccBoxPresent, PDColorSpace colorSpace) {
		super(JPEG_2000_TYPE);
		this.nrColorChannels = nrColorChannels;
		this.nrColorSpaceSpecs = nrColorSpaceSpecs;
		this.nrColorSpacesWithApproxField = nrColorSpacesWithApproxField;
		this.colrMethod = colrMethod;
		this.colrEnumCS = colrEnumCS;
		this.bitDepth = bitDepth;
		this.bpccBoxPresent = bpccBoxPresent;
		this.colorSpace = colorSpace;
	}

	/**
	 * Creates new PBoxJPEG2000 object that implements JPEG2000 object from the
	 * model from the given jp2 image stream
	 *
	 * @param stream
	 *            image stream to parse
	 * @return created PBoxJPEG2000 object
	 */
	public static PBoxJPEG2000 fromStream(InputStream stream, PDDocument document, PDFAFlavour flavour) {
		Builder builder = new Builder();

		byte[] signArray = new byte[12];
		try {
			// Check if the stream starts with valid jp2 signature
			if (stream.read(signArray) != 12 || !isValidSignature(signArray)) {
				LOGGER.log(java.util.logging.Level.INFO, "File contains wrong signature");
				return builder.build();
			}
			// Finding the beginning of the header box content
			long headerLeft = findHeader(stream);

			if (headerLeft >= 0) {
				parseHeader(stream, headerLeft, builder, document, flavour);
			}

		} catch (IOException e) {
			LOGGER.log(java.util.logging.Level.INFO, e.getMessage());
		}
		return builder.build();
	}

	private static void parseHeader(final InputStream stream, final long headerLeft, final Builder builder,
			PDDocument document, PDFAFlavour flavour) throws IOException {
		long leftInHeader = headerLeft;
		boolean isHeaderReachEnd = leftInHeader == 0;
		Long nrColorSpaceSpecs = null;
		Long nrColorSpacesWithApproxField = null;
		Long firstColrMethod = null;
		Long firstColrEnumCS = null;
		Long colrMethod = null;
		Long colrEnumCS = null;
		Boolean doesFirstContainsColorSpace = null;
		org.apache.pdfbox.pdmodel.graphics.color.PDColorSpace firstColorSpace = null;
		org.apache.pdfbox.pdmodel.graphics.color.PDColorSpace colorSpace = null;

		while (true) {
			byte[] lbox = new byte[4];
			byte[] tbox = new byte[4];
			if (stream.read(lbox) != 4 || stream.read(tbox) != 4) {
				break;
			}
			int skipped = 8;
			long length = convertArrayToLong(lbox);
			if (length == 1) {
				byte[] xlbox = new byte[8];
				if (stream.read(xlbox) != 8) {
					break;
				}
				length = convertArrayToLong(xlbox);
				skipped = 16;
			}
			if (length < 0 || (!isHeaderReachEnd && (length == 0 || length > leftInHeader))) {
				break;
			}
			long leftInBox = length - skipped;

			if (matches(tbox, ihdr)) {
				if (leftInBox != 14 && length != 0) {
					LOGGER.log(java.util.logging.Level.INFO, "Image header content does not contain 14 bytes");
					break;
				}
				skipBytes(stream, 8);
				byte[] nc = new byte[2];
				if (stream.read(nc) != 2) {
					LOGGER.log(java.util.logging.Level.INFO, "Can not read number of components");
					break;
				}
				long ncColorChannels = convertArrayToLong(nc);
				builder.setNrColorChannels(Long.valueOf(ncColorChannels));
				byte[] bpc = new byte[1];
				if (stream.read(bpc) != 1) {
					LOGGER.log(java.util.logging.Level.INFO, "Can not read bitDepth");
					break;
				}
				long bitDepth = bpc[0] + 1;
				builder.setBitDepth(Long.valueOf(bitDepth));
				skipBytes(stream, 3);
			} else if (matches(tbox, bpcc)) {
				builder.setBpccBoxPresent(Boolean.TRUE);
				skipBytes(stream, leftInBox);
			} else if (matches(tbox, colr)) {
				if (leftInBox < 3) {
					LOGGER.log(java.util.logging.Level.INFO, "Founded 'colr' box with length less than 3");
					break;
				}
				if (nrColorSpaceSpecs == null) {
					nrColorSpaceSpecs = Long.valueOf(1L);
				} else {
					++nrColorSpaceSpecs;
				}
				byte[] meth = new byte[1];
				if (stream.read(meth) != 1) {
					LOGGER.log(java.util.logging.Level.INFO, "Can not read METH");
					break;
				}
				long methValue = convertArrayToLong(meth);
				if (firstColrMethod == null) {
					firstColrMethod = Long.valueOf(methValue);
				}
				skipBytes(stream, 1);
				byte[] approx = new byte[1];
				if (stream.read(approx) != 1) {
					LOGGER.log(java.util.logging.Level.INFO, "Can not read APPROX");
					break;
				}
				long approxValue = convertArrayToLong(approx);
				if (approxValue == 1) {
					if (nrColorSpacesWithApproxField == null) {
						nrColorSpacesWithApproxField = Long.valueOf(1L);
					} else {
						++nrColorSpacesWithApproxField;
					}
					if (colrMethod == null) {
						colrMethod = Long.valueOf(methValue);
					}
				}
				long read = 3;
				if (methValue == 1) {
					if (leftInBox < 7) {
						LOGGER.log(java.util.logging.Level.INFO, "Founded 'colr' box with meth value 1 and length less than 7");
						break;
					}
					byte[] enumCS = new byte[4];
					if (stream.read(enumCS) != 4) {
						LOGGER.log(java.util.logging.Level.INFO, "Can not read EnumCS");
						break;
					}
					read += 4;
					long enumCSValue = convertArrayToLong(enumCS);
					if (firstColrEnumCS == null) {
						firstColrEnumCS = Long.valueOf(enumCSValue);
						firstColorSpace = createColorSpaceFromEnumValue(firstColrEnumCS.longValue(), document);
						doesFirstContainsColorSpace = Boolean.valueOf(firstColorSpace != null);
					}
					if (approxValue == 1 && colrEnumCS == null) {
						colrEnumCS = Long.valueOf(enumCSValue);
						colorSpace = createColorSpaceFromEnumValue(colrEnumCS.longValue(), document);
					}
				} else if (methValue == 2) {
					int profileLength = (int) (leftInBox - read);
					byte[] profile = new byte[profileLength];
					if (stream.read(profile) != profileLength) {
						LOGGER.log(java.util.logging.Level.INFO, "Can not read Profile");
						break;
					}
					read += profileLength;
					if (doesFirstContainsColorSpace == null) {
						firstColorSpace = createColorSpaceFromProfile(profile, document);
						doesFirstContainsColorSpace = Boolean.valueOf(firstColorSpace != null);
					}
					if (approxValue == 1 && colorSpace == null) {
						colorSpace = createColorSpaceFromProfile(profile, document);
					}
				}
				skipBytes(stream, leftInBox - read);
			} else {
				skipBytes(stream, leftInBox);
			}

			leftInHeader -= length;
			if ((isHeaderReachEnd && length == 0) || (!isHeaderReachEnd && leftInHeader == 0)) {
				break;
			}
		}

		if (nrColorSpaceSpecs != null) {
			builder.setNrColorSpaceSpecs(nrColorSpaceSpecs);
		}
		if (nrColorSpacesWithApproxField != null) {
			builder.setNrColorSpacesWithApproxField(nrColorSpacesWithApproxField);
		}

		if (nrColorSpacesWithApproxField != null) {
			if (colrMethod != null) {
				builder.setColrMethod(colrMethod);
			}
			if (colrEnumCS != null) {
				builder.setColrEnumCS(colrEnumCS);
			}
			if (colorSpace != null) {
				builder.setColorSpace(ColorSpaceFactory.getColorSpace(colorSpace, document, flavour));
			}
		} else if (Long.valueOf(1L).equals(nrColorSpaceSpecs)) {
			if (firstColrMethod != null) {
				builder.setColrMethod(firstColrMethod);
			}
			if (firstColrEnumCS != null) {
				builder.setColrEnumCS(firstColrEnumCS);
			}
			if (firstColorSpace != null) {
				builder.setColorSpace(ColorSpaceFactory.getColorSpace(firstColorSpace, document, flavour));
			}
		}
	}

	private static org.apache.pdfbox.pdmodel.graphics.color.PDColorSpace createColorSpaceFromEnumValue(long enumCS,
			PDDocument document) {
		if (enumCS > Integer.MAX_VALUE) {
			return null;
		}

		switch ((int) enumCS) {
		case 12:
			return PDDeviceCMYK.INSTANCE;
		case 14:
			return new PDLab();
		case 17:
			PDICCBased pdiccBased = new PDICCBased(document);
			pdiccBased.setNumberOfComponents(1);
			return pdiccBased;
		case 16:
		case 18:
		case 20:
		case 21:
		case 24:
			PDICCBased pdicc = new PDICCBased(document);
			pdicc.setNumberOfComponents(3);
			return pdicc;
		default:
			return null;
		}
	}

	private static PDICCBased createColorSpaceFromProfile(byte[] profile, PDDocument document) throws IOException {
		if (profile.length < 20) {
			return null;
		}

		String type = new String(profile, 16, 4);
		COSArray array = new COSArray();
		array.add(COSName.ICCBASED);
		PDStream stream = new PDStream(document, new ByteArrayInputStream(profile));
		int nrOfComp;
		switch (type) {
		case "GRAY":
			nrOfComp = 1;
			break;
		case "2CLR":
			nrOfComp = 2;
			break;
		case "XYZ ":
		case "Lab ":
		case "Luv ":
		case "YCbr":
		case "Yxy ":
		case "RGB ":
		case "HSV ":
		case "HLS ":
		case "CMY ":
		case "3CLR":
			nrOfComp = 3;
			break;
		case "CMYK":
		case "4CLR":
			nrOfComp = 4;
			break;
		case "5CLR":
			nrOfComp = 5;
			break;
		case "6CLR":
			nrOfComp = 6;
			break;
		case "7CLR":
			nrOfComp = 7;
			break;
		case "8CLR":
			nrOfComp = 8;
			break;
		case "9CLR":
			nrOfComp = 9;
			break;
		case "ACLR":
			nrOfComp = 10;
			break;
		case "BCLR":
			nrOfComp = 11;
			break;
		case "CCLR":
			nrOfComp = 12;
			break;
		case "DCLR":
			nrOfComp = 13;
			break;
		case "ECLR":
			nrOfComp = 14;
			break;
		case "FCLR":
			nrOfComp = 15;
			break;
		default:
			LOGGER.log(java.util.logging.Level.INFO, "Unknown color space signature in ICC Profile of image. Current signature: " + type);
			return null;
		}
		stream.getStream().setInt(COSName.N, nrOfComp);
		array.add(stream);
		return new PDICCBased(array);
	}

	/**
	 * Finds the beginning of the header box content and returns its left length
	 *
	 * @param stream
	 *            image stream
	 * @return left length of the header box or -1 if it has not been found and
	 *         0 if it ends at the end of the stream
	 * @throws IOException
	 */
	private static long findHeader(InputStream stream) throws IOException {
		while (true) {
			byte[] lbox = new byte[4];
			byte[] tbox = new byte[4];
			if (stream.read(lbox) != 4 || stream.read(tbox) != 4) {
				return -1L;
			}
			int skipped = 8;
			long length = convertArrayToLong(lbox);
			if (length == 1) {
				byte[] xlbox = new byte[8];
				if (stream.read(xlbox) != 8) {
					return -1L;
				}
				length = convertArrayToLong(xlbox);
				skipped = 16;
			}
			long left = length - skipped;
			// Check is current box a header
			if (matches(tbox, header)) {
				if (length == 0) {
					return 0;
				}
				return left <= 0 ? -1L : left;
			} else if (length == 0 || left < 0) {
				return -1L;
			} else {
				skipBytes(stream, left);
			}
		}
	}

	private static void skipBytes(InputStream stream, long skipNumber) throws IOException {
		long skippedBytes = stream.skip(skipNumber);
		if (skippedBytes != skipNumber && stream.available() != 0) {
			throw new IllegalStateException("Skipped less bytes that needed.");
		}
	}

	private static long convertArrayToLong(byte[] toConvert) {
		if (toConvert.length < 1 || toConvert.length > 8) {
			throw new IllegalArgumentException("Length of the converting byte array can not be greater than 8");
		}
		long res = 0;
		for (byte aToConvert : toConvert) {
			res <<= 8;
			res += aToConvert & 0xff;
		}
		return res;
	}

	private static boolean isValidSignature(byte[] signature) {
		return matches(signature, sign);
	}

	private static boolean matches(byte[] source, byte[] match) {
		if (match.length != source.length) {
			return false;
		}
		for (int i = 0; i < match.length; ++i) {
			if (source[i] != match[i]) {
				return false;
			}
		}
		return true;
	}

	public PDColorSpace getImageColorSpace() {
		return this.colorSpace;
	}

	@Override
	public Long getnrColorChannels() {
		return this.nrColorChannels;
	}

	@Override
	public Long getnrColorSpaceSpecs() {
		return this.nrColorSpaceSpecs;
	}

	@Override
	public Long getnrColorSpacesWithApproxField() {
		return this.nrColorSpacesWithApproxField;
	}

	@Override
	public Long getcolrMethod() {
		return this.colrMethod;
	}

	@Override
	public Long getcolrEnumCS() {
		return this.colrEnumCS;
	}

	@Override
	public Long getbitDepth() {
		return this.bitDepth;
	}

	@Override
	public Boolean getbpccBoxPresent() {
		return this.bpccBoxPresent;
	}

	@Override
	public Boolean gethasColorSpace() {
		return false;
	}

	private static class Builder {
		private Long nrColorChannels = DEFAULT_NR_COLOR_CHANNELS;
		private Long nrColorSpaceSpecs = DEFAULT_NR_COLOR_SPACE_SPECS;
		private Long nrColorSpacesWithApproxField = DEFAULT_NR_COLOR_SPACES_WITH_APPROX_FIELD;
		private Long colrMethod = DEFAULT_COLR_METHOD;
		private Long colrEnumCS = DEFAULT_COLR_ENUM_CS;
		private Long bitDepth = DEFAULT_BIT_DEPTH;
		private Boolean bpccBoxPresent = DEFAULT_BPCC_BOX_PRESENT;
		private PDColorSpace colorSpace = DEFAULT_COLOR_SPACE;

		public PBoxJPEG2000 build() {
			return new PBoxJPEG2000(this.nrColorChannels, this.nrColorSpaceSpecs, this.nrColorSpacesWithApproxField,
					this.colrMethod, this.colrEnumCS, this.bitDepth, this.bpccBoxPresent, this.colorSpace);
		}

		public Builder setNrColorChannels(Long nrColorChannels) {
			this.nrColorChannels = nrColorChannels;
			return this;
		}

		public Builder setNrColorSpaceSpecs(Long nrColorSpaceSpecs) {
			this.nrColorSpaceSpecs = nrColorSpaceSpecs;
			return this;
		}

		public Builder setNrColorSpacesWithApproxField(Long nrColorSpacesWithApproxField) {
			this.nrColorSpacesWithApproxField = nrColorSpacesWithApproxField;
			return this;
		}

		public Builder setColrMethod(Long colrMethod) {
			this.colrMethod = colrMethod;
			return this;
		}

		public Builder setColrEnumCS(Long colrEnumCS) {
			this.colrEnumCS = colrEnumCS;
			return this;
		}

		public Builder setBitDepth(Long bitDepth) {
			this.bitDepth = bitDepth;
			return this;
		}

		public Builder setBpccBoxPresent(Boolean bpccBoxPresent) {
			this.bpccBoxPresent = bpccBoxPresent;
			return this;
		}

		public Builder setColorSpace(PDColorSpace colorSpace) {
			this.colorSpace = colorSpace;
			return this;
		}

	}
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy