/*
* Copyright (c) 1997-2024 IDRsolutions (https://www.idrsolutions.com)
*/
package org.jpedal.render.output.io;
import com.idrsolutions.image.jpeg.JpegEncoder;
import com.idrsolutions.image.png.PngEncoder;
import com.idrsolutions.image.png.options.PngCompressionFormat;
import org.jpedal.utils.LogWriter;
import java.awt.image.BufferedImage;
import java.io.BufferedOutputStream;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.nio.charset.StandardCharsets;
import java.util.Base64;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Semaphore;
import java.util.concurrent.TimeUnit;
import java.util.zip.GZIPOutputStream;
public class DefaultIO implements CustomIO {
// Put image output inside a thread to speed up conversion
private ExecutorService executorService;
private Semaphore blocker;
private final boolean useImageOutputThread;
private final boolean useLegacyImageFileType;
private boolean compressImages;
/**
* Constructs a DefaultIO that will use a separate image output thread.
* Only safe to use if completeDocument() is called after page decodes are finished.
*/
protected DefaultIO() {
this(true, false);
}
/**
* Constructs a DefaultIO
*
* @param useImageOutputThread Whether a separate image output thread should be used. (Not for ExtractPagesAsHTML)
* @param useLegacyImageFileType Whether to use legacy image file type (png)
*/
public DefaultIO(final boolean useImageOutputThread, final boolean useLegacyImageFileType) {
this.useImageOutputThread = useImageOutputThread;
this.useLegacyImageFileType = useLegacyImageFileType;
}
@Override
public void writeFont(final String rootDir, final String path, final byte[] rawFontData) {
// Make sure dir exists
final File dir = new File(rootDir + path).getParentFile();
if (!dir.exists()) {
dir.mkdirs();
}
try (BufferedOutputStream bos = new BufferedOutputStream(new FileOutputStream(rootDir + path))) {
bos.write(rawFontData);
} catch (final IOException e) {
LogWriter.writeLog("Exception: " + e.getMessage());
}
}
@Override
public void writePlainTextFile(final String rootDir, final String path, final StringBuilder content) {
writePlainTextFile(rootDir, path, content, false, false);
}
@Override
public void writePlainTextAppendedFile(final String rootDir, final String path, final StringBuilder content, final boolean append) {
writePlainTextFile(rootDir, path, content, append, false);
}
@Override
public void writeGZIPCompressedFile(final String rootDir, final String path, final StringBuilder content) {
writePlainTextFile(rootDir, path, content, false, true);
}
private static void writePlainTextFile(final String rootDir, final String path, final StringBuilder content, final boolean append, final boolean gzipCompress) {
// Make sure dir exists
final File dir = new File(rootDir + path).getParentFile();
if (!dir.exists()) {
dir.mkdirs();
}
try (BufferedOutputStream output = getOutputStream(rootDir, path, append, gzipCompress)) {
output.write(content.toString().getBytes(StandardCharsets.UTF_8));
output.flush();
} catch (final IOException e) {
LogWriter.writeLog("Exception: " + e.getMessage());
}
}
private static BufferedOutputStream getOutputStream(final String rootDir, final String path, final boolean append, final boolean gzipCompress) throws IOException {
if (gzipCompress && append) {
throw new RuntimeException("Cannot append to a gzip compressed file.");
}
if (gzipCompress) {
return new BufferedOutputStream(new GZIPOutputStream(new FileOutputStream(rootDir + path)));
} else {
return new BufferedOutputStream(new FileOutputStream(rootDir + path, append));
}
}
@Override
public void writeFileFromStream(final String rootDir, final String path, final InputStream is) {
new File(rootDir + path).getParentFile().mkdirs();
try (OutputStream os = new FileOutputStream(rootDir + path);
InputStream iis = is) {
final byte[] buffer = new byte[1_024];
int length;
while ((length = iis.read(buffer)) > 0) {
os.write(buffer, 0, length);
}
is.close();
} catch (final IOException e) {
LogWriter.writeLog("Exception: " + e.getMessage());
}
}
@Override
public void setCompressImages(final boolean compressImages) {
this.compressImages = compressImages;
}
@Override
public void writeImage(final String rootDir, final String path, final BufferedImage image, final ImageType imageType) {
final ImageFileType imageFileType = getImageTypeUsed(imageType);
final String file = path + imageFileType.getFileExtension();
final String fullPath = rootDir + file;
// Make sure img Dir exists
final File imgDir = new File(fullPath).getParentFile();
if (!imgDir.exists()) {
imgDir.mkdirs();
}
// Put image output inside a thread to speed up conversion (reuse the same thread for all images)
final Runnable r = () -> {
try {
try (BufferedOutputStream bos = new BufferedOutputStream(new FileOutputStream(fullPath))) {
writeImageToStream(image, bos, imageFileType, compressImages);
bos.flush();
}
} catch (final IOException e) {
LogWriter.writeLog("Exception: " + e.getMessage());
} finally {
if (useImageOutputThread) {
blocker.release();
}
}
};
if (useImageOutputThread) {
if (executorService == null) {
executorService = Executors.newFixedThreadPool(1); // Single thread as ImageIO is not thread safe
blocker = new Semaphore(2); // Limit to 2 images in the queue
}
try {
blocker.acquire(); // Blocks if the queue is too long
} catch (final InterruptedException e) {
Thread.currentThread().interrupt();
LogWriter.writeLog("Exception in handling thread " + e);
}
executorService.submit(r);
} else {
r.run();
}
}
protected void writeImageToStream(final BufferedImage image, final OutputStream os, final ImageFileType imageFileType, final boolean compressImages) throws IOException {
if (imageFileType == ImageFileType.JPG) {
final JpegEncoder encoder = new JpegEncoder();
encoder.getEncoderOptions().setQuality(85);
encoder.write(image, os);
} else {
final PngEncoder encoder = new PngEncoder();
if (compressImages && image.getWidth() * image.getHeight() > 256) {
encoder.getEncoderOptions().setCompressionFormat(PngCompressionFormat.QUANTISED8BIT);
}
encoder.write(image, os);
}
}
/**
* Grab file type to use for output for specific type of image
*
* @return ImageFileType to use
*/
@Override
public ImageFileType getImageTypeUsed(final ImageType imageType) {
if (useLegacyImageFileType) {
return ImageFileType.PNG;
} else {
return switch (imageType) {
case BACKGROUND, THUMBNAIL, SVG_ALPHA_NOT_REQUIRED -> ImageFileType.JPG;
case SVG_ALPHA_REQUIRED, FORM, SHADE -> ImageFileType.PNG;
};
}
}
/**
* If using a separate image output thread, this should be called after pages have
* finished decoding to wait for image images to finish outputting.
*/
@Override
public void completeDocument() {
waitForImageThreads();
}
private void waitForImageThreads() {
if (useImageOutputThread && executorService != null) {
executorService.shutdown();
try {
executorService.awaitTermination(1, TimeUnit.DAYS);
} catch (final InterruptedException e) {
LogWriter.writeLog("Exception: " + e.getMessage());
}
executorService = null;
}
}
/**
* Creates a base64 image stream
*
* @param image :: BufferedImage to encode as base64 stream
* @return A string of the image stream
*/
@Override
public String createBase64ImageStream(final BufferedImage image, final ImageFileType imageFileType) {
String stream = "";
try {
final ByteArrayOutputStream bos = new ByteArrayOutputStream();
writeImageToStream(image, bos, imageFileType, compressImages);
bos.flush();
bos.close();
stream = "data:" + imageFileType.getMimeType() + ";base64," + Base64.getEncoder().encodeToString(bos.toByteArray());
stream = stream.replaceAll("\r\n", "");
} catch (final IOException e) {
LogWriter.writeLog("Exception: " + e.getMessage());
}
return stream;
}
@Override
public void cancelPage(final String dirToFlush) {
waitForImageThreads();
delete(new File(dirToFlush));
}
private static void delete(final File file) {
if (file.isDirectory()) {
final File[] files = file.listFiles();
if (files != null) {
for (final File f : files) {
delete(f);
}
}
}
final boolean result = file.delete();
if (!result) {
LogWriter.writeLog("Failed to delete " + file.getAbsolutePath());
}
}
}
|