org.sejda.impl.sambox.component.PageImageWriter Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of sejda-sambox Show documentation
Show all versions of sejda-sambox Show documentation
Package containing tasks implemented using sambox.
/*
* Copyright 2016 by Eduard Weissmann ([email protected]).
*
* This file is part of the Sejda source code
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero 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 Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see .
*/
package org.sejda.impl.sambox.component;
import java.awt.color.ColorSpace;
import java.awt.color.ICC_ColorSpace;
import java.awt.color.ICC_ProfileGray;
import java.awt.geom.AffineTransform;
import java.awt.geom.Point2D;
import java.awt.image.BufferedImage;
import java.io.File;
import java.io.IOException;
import java.util.Iterator;
import java.util.Optional;
import javax.imageio.IIOException;
import javax.imageio.IIOImage;
import javax.imageio.ImageIO;
import javax.imageio.ImageReader;
import javax.imageio.ImageTypeSpecifier;
import javax.imageio.ImageWriteParam;
import javax.imageio.ImageWriter;
import javax.imageio.stream.FileImageOutputStream;
import javax.imageio.stream.ImageInputStream;
import javax.imageio.stream.ImageOutputStream;
import org.sejda.model.util.IOUtils;
import org.sejda.io.SeekableSource;
import org.sejda.io.SeekableSources;
import org.sejda.model.exception.TaskIOException;
import org.sejda.model.input.Source;
import org.sejda.sambox.pdmodel.PDDocument;
import org.sejda.sambox.pdmodel.PDPage;
import org.sejda.sambox.pdmodel.PDPageContentStream;
import org.sejda.sambox.pdmodel.graphics.PDXObject;
import org.sejda.sambox.pdmodel.graphics.form.PDFormXObject;
import org.sejda.sambox.pdmodel.graphics.image.PDImageXObject;
import org.sejda.sambox.pdmodel.graphics.image.UnsupportedImageFormatException;
import org.sejda.sambox.pdmodel.graphics.image.UnsupportedTiffImageException;
import org.sejda.sambox.pdmodel.graphics.state.PDExtendedGraphicsState;
import org.sejda.sambox.util.Matrix;
import org.sejda.sambox.util.filetypedetector.FileType;
import org.sejda.sambox.util.filetypedetector.FileTypeDetector;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import net.coobird.thumbnailator.Thumbnails;
public class PageImageWriter {
private static final Logger LOG = LoggerFactory.getLogger(PageImageWriter.class);
private PDDocument document;
public PageImageWriter(PDDocument document) {
this.document = document;
}
public void append(PDPage page, PDImageXObject image, Point2D position, float width, float height,
PDExtendedGraphicsState gs, int rotation) throws TaskIOException {
write(page, image, position, width, height, PDPageContentStream.AppendMode.APPEND, gs, true, rotation);
}
public void append(PDPage page, PDFormXObject image, Point2D position, float width, float height,
PDExtendedGraphicsState gs, int rotation) throws TaskIOException {
write(page, image, position, width, height, PDPageContentStream.AppendMode.APPEND, gs, true, rotation);
}
public void prepend(PDPage page, PDImageXObject image, Point2D position, float width, float height,
PDExtendedGraphicsState gs, int rotation) throws TaskIOException {
write(page, image, position, width, height, PDPageContentStream.AppendMode.PREPEND, gs, false, rotation);
}
public void prepend(PDPage page, PDFormXObject image, Point2D position, float width, float height,
PDExtendedGraphicsState gs, int rotation) throws TaskIOException {
write(page, image, position, width, height, PDPageContentStream.AppendMode.PREPEND, gs, false, rotation);
}
private void write(PDPage page, PDXObject image, Point2D position, float width, float height,
PDPageContentStream.AppendMode mode, PDExtendedGraphicsState gs, boolean resetContext, int rotation)
throws TaskIOException {
try (PDPageContentStream contentStream = new PDPageContentStream(document, page, mode, true, resetContext)) {
AffineTransform at = new AffineTransform(width, 0, 0, height, (float) position.getX(),
(float) position.getY());
if (rotation != 0) {
at.rotate(Math.toRadians(rotation));
}
if (image instanceof PDFormXObject) {
contentStream.drawImage((PDFormXObject) image, new Matrix(at), gs);
} else {
contentStream.drawImage((PDImageXObject) image, new Matrix(at), gs);
}
} catch (IOException e) {
throw new TaskIOException("An error occurred writing image to the page.", e);
}
}
public static PDImageXObject toPDXImageObject(Source> source) throws TaskIOException {
return toPDXImageObject(source, 0);
}
public static PDImageXObject toPDXImageObject(Source> source, int number) throws TaskIOException {
try {
return createFromSeekableSource(source.getSeekableSource(), source.getName(), number);
} catch (Exception e) {
String message = "An error occurred creating PDImageXObject from file source: " + source.getName();
if(number > 0) {
message += " and number: " + number;
}
throw new TaskIOException(message, e);
}
}
public static PDImageXObject createFromSeekableSource(SeekableSource original, String name)
throws TaskIOException, IOException {
return createFromSeekableSource(original, name, 0);
}
public static PDImageXObject createFromSeekableSource(SeekableSource original, String name, int number)
throws TaskIOException, IOException {
SeekableSource source = original;
if(number == 0) {
Optional maybeConvertedFile = convertExifRotatedIf(source, name);
if (maybeConvertedFile.isPresent()) {
source = maybeConvertedFile.get();
}
maybeConvertedFile = convertCMYKJpegIf(source, name);
if (maybeConvertedFile.isPresent()) {
source = maybeConvertedFile.get();
}
maybeConvertedFile = convertICCGrayPngIf(source, name);
if (maybeConvertedFile.isPresent()) {
source = maybeConvertedFile.get();
}
}
try {
return PDImageXObject.createFromSeekableSource(source, name, number);
} catch (UnsupportedTiffImageException e) {
LOG.warn("Found unsupported TIFF compression, converting TIFF to JPEG: " + e.getMessage());
try {
return PDImageXObject.createFromSeekableSource(convertTiffToJpg(source, number), name);
} catch (UnsupportedOperationException ex) {
if (ex.getMessage().contains("alpha channel")) {
LOG.warn("Found alpha channel image, JPEG compression failed, converting TIFF to PNG");
return PDImageXObject.createFromSeekableSource(convertTiffToPng(source, number), name);
}
throw ex;
}
}
}
public static SeekableSource convertTiffToJpg(SeekableSource source, int number) throws IOException, TaskIOException {
return convertImageTo(source, number, "jpeg");
}
public static SeekableSource convertTiffToPng(SeekableSource source, int number) throws IOException, TaskIOException {
return convertImageTo(source, number, "png");
}
private static FileType getFileType(SeekableSource source) {
try {
return FileTypeDetector.detectFileType(source);
} catch (IOException e) {
return null;
}
}
// same as ImageIO.read() but allows reading a specific image from a multi-page image (eg: tiff)
public static BufferedImage read(SeekableSource source, int number) throws IOException {
BufferedImage image = null;
ImageInputStream is = ImageIO.createImageInputStream(source.asNewInputStream());
Iterator readers = ImageIO.getImageReaders(is);
if (readers.hasNext()) {
ImageReader reader = readers.next();
reader.setInput(is);
try {
image = reader.read(number);
} finally {
reader.dispose();
is.close();
}
}
return image;
}
public static SeekableSource convertImageTo(SeekableSource source, int number, String format)
throws IOException, TaskIOException {
BufferedImage image = read(source, number);
File tmpFile = IOUtils.createTemporaryBuffer("." + format);
ImageOutputStream outputStream = new FileImageOutputStream(tmpFile);
try {
ImageWriter writer = ImageIO.getImageWritersByFormatName(format).next();
writer.setOutput(outputStream);
ImageWriteParam param = writer.getDefaultWriteParam();
if (format.equals("jpeg")) {
param.setCompressionMode(ImageWriteParam.MODE_EXPLICIT);
param.setCompressionQuality(1.0F);
}
writer.write(null, new IIOImage(image, null, null), param);
} finally {
org.sejda.commons.util.IOUtils.closeQuietly(outputStream);
}
return SeekableSources.seekableSourceFrom(tmpFile);
}
/**
* Checks if the input file has exit rotation If that's the case, converts to rotated image without exif rotation
*/
private static Optional convertExifRotatedIf(SeekableSource source, String name)
throws IOException, TaskIOException {
int degrees = ExifHelper.getRotationBasedOnExifOrientation(source.asNewInputStream());
if (degrees > 0) {
BufferedImage orig = ImageIO.read(source.asNewInputStream());
if(orig == null) {
FileType type = FileTypeDetector.detectFileType(source);
throw new UnsupportedImageFormatException(type, name, null);
}
BufferedImage result = Thumbnails.of(orig).scale(1).rotate(degrees).asBufferedImage();
File tmpFile = IOUtils.createTemporaryBufferWithName(name);
ImageIO.write(result, getImageIOSaveFormat(source), tmpFile);
return Optional.of(SeekableSources.seekableSourceFrom(tmpFile));
}
return Optional.empty();
}
private static String getImageIOSaveFormat(SeekableSource source) {
FileType fileType = getFileType(source);
if (fileType == FileType.JPEG) {
return "jpg";
}
return "png";
}
/**
* Checks if the input file is a JPEG using CMYK If that's the case, converts to RGB and returns the file path
*/
private static Optional convertCMYKJpegIf(SeekableSource source, String name)
throws IOException, TaskIOException {
try {
if (FileType.JPEG.equals(getFileType(source))) {
try (ImageInputStream iis = ImageIO.createImageInputStream(source.asNewInputStream())) {
ImageReader reader = ImageIO.getImageReadersByFormatName("jpg").next();
boolean isCmyk = false;
try {
ImageIO.setUseCache(false);
reader.setInput(iis);
for (Iterator it = reader.getImageTypes(0); it.hasNext();) {
ImageTypeSpecifier typeSpecifier = it.next();
if (typeSpecifier.getColorModel().getColorSpace().getType() == ColorSpace.TYPE_CMYK) {
isCmyk = true;
}
}
if (isCmyk) {
LOG.debug("Detected a CMYK JPEG image, will convert to RGB and save to a new file");
// convert to rgb
// twelvemonkeys JPEG plugin already converts it to rgb when reading the image
// just write it out
BufferedImage image = reader.read(0);
File tmpFile = IOUtils.createTemporaryBufferWithName(name);
ImageIO.write(image, "jpg", tmpFile);
return Optional.of(SeekableSources.seekableSourceFrom(tmpFile));
}
} finally {
reader.dispose();
}
}
}
} catch (IIOException e) {
if (e.getMessage().startsWith("Not a JPEG stream")) {
// this was a different image format with a JPEG extension
} else {
throw e;
}
}
return Optional.empty();
}
/**
* Checks if the input file is a PNG using ICC Gray color model If that's the case, converts to RGB and returns the file path
*/
private static Optional convertICCGrayPngIf(SeekableSource source, String name)
throws IOException, TaskIOException {
try {
if (FileType.PNG.equals(getFileType(source))) {
try (ImageInputStream iis = ImageIO.createImageInputStream(source.asNewInputStream())) {
ImageReader reader = ImageIO.getImageReadersByFormatName("png").next();
boolean isICCGray = false;
try {
ImageIO.setUseCache(false);
reader.setInput(iis);
for (Iterator it = reader.getImageTypes(0); it.hasNext();) {
ImageTypeSpecifier typeSpecifier = it.next();
ColorSpace colorSpace = typeSpecifier.getColorModel().getColorSpace();
if (colorSpace instanceof ICC_ColorSpace
&& ((ICC_ColorSpace) colorSpace).getProfile() instanceof ICC_ProfileGray) {
isICCGray = true;
break;
}
}
if (isICCGray) {
LOG.debug("Detected a Gray PNG image, will convert to RGB and save to a new file");
// convert to rgb
BufferedImage original = reader.read(0);
BufferedImage rgb = toARGB(original);
File tmpFile = IOUtils.createTemporaryBufferWithName(name);
ImageIO.write(rgb, "png", tmpFile);
return Optional.of(SeekableSources.seekableSourceFrom(tmpFile));
}
} finally {
reader.dispose();
}
}
}
} catch (IIOException e) {
LOG.debug("Failed convertICCGrayPngIf()", e);
}
return Optional.empty();
}
private static BufferedImage toARGB(BufferedImage i) {
BufferedImage rgb = new BufferedImage(i.getWidth(null), i.getHeight(null), BufferedImage.TYPE_INT_ARGB);
rgb.createGraphics().drawImage(i, 0, 0, null);
return rgb;
}
}