Merge pull request #768 from Alfresco/feature/ATS-996-TIFF_to_PDF_-_invalid_output_format

ATS-996: TIFF to PDF - invalid output format
This commit is contained in:
Krystian Dabrowski
2023-03-28 17:42:53 +02:00
committed by GitHub
6 changed files with 204 additions and 64 deletions

View File

@@ -64,6 +64,7 @@ public class AIOTikaTest extends TikaTest
"page",
"pageLimit",
"pdfFormat",
"pdfOrientation",
"resizeHeight",
"resizePercentage",
"resizeWidth",

View File

@@ -28,6 +28,7 @@ package org.alfresco.transform.misc.transformers;
import static org.alfresco.transform.common.RequestParamMap.END_PAGE;
import static org.alfresco.transform.common.RequestParamMap.PDF_FORMAT;
import static org.alfresco.transform.common.RequestParamMap.PDF_ORIENTATION;
import static org.alfresco.transform.common.RequestParamMap.START_PAGE;
import javax.imageio.ImageIO;
@@ -58,10 +59,11 @@ import org.springframework.stereotype.Component;
* Converts image files into PDF files. Transformer uses PDF Box to perform conversions.
* During conversion image might be scaled down (keeping proportions) to match width or height of the PDF document.
* If the image is smaller than PDF page size, the image will be placed in the top left-hand side of the PDF document page.
* Transformer takes 3 optional transform options:
* - startPage - page number of image (for multipage images) from which transformer should start conversion. Default: first page of the image.
* - endPage - page number of image (for multipage images) up to which transformation should be performed. Default: last page of the image.
* - pdfFormat - output PDF file format. Available formats: A0, A1, A2, A3, A4, A5, A6, LETTER, LEGAL. Default: A4.
* Transformer accepts bellow optional transform parameters:
* - startPage - page number of image (for multi-page images) from which transformer should start conversion. Default: first page of the image.
* - endPage - page number of image (for multi-page images) up to which transformation should be performed. Default: last page of the image.
* - pdfFormat - output PDF file format. Available formats: DEFAULT, A0, A1, A2, A3, A4, A5, A6, LETTER, LEGAL. Default: original image size.
* - pdfOrientation - output PDF file orientation. Available options: DEFAULT, PORTRAIT, LANDSCAPE. Default: original image orientation.
*/
@Component
public class ImageToPdfTransformer implements CustomTransformerFileAdaptor
@@ -73,8 +75,8 @@ public class ImageToPdfTransformer implements CustomTransformerFileAdaptor
private static final String START_PAGE_GREATER_THAN_END_PAGE_ERROR_MESSAGE = "Start page number cannot be greater than end page.";
private static final String INVALID_OPTION_ERROR_MESSAGE = "Parameter '%s' is invalid: \"%s\" - it must be an integer.";
private static final String INVALID_IMAGE_ERROR_MESSAGE = "Image file (%s) format (%s) not supported by ImageIO.";
private static final String DEFAULT_PDF_FORMAT_STRING = "A4";
private static final PDRectangle DEFAULT_PDF_FORMAT = PDRectangle.A4;
private static final String DEFAULT_PDF_FORMAT_STRING = "DEFAULT";
private static final String DEFAULT_PDF_ORIENTATION_STRING = "DEFAULT";
@Override
public String getTransformerName()
@@ -94,6 +96,7 @@ public class ImageToPdfTransformer implements CustomTransformerFileAdaptor
final Integer startPage = parseOptionIfPresent(transformOptions, START_PAGE, Integer.class).orElse(null);
final Integer endPage = parseOptionIfPresent(transformOptions, END_PAGE, Integer.class).orElse(null);
final String pdfFormat = parseOptionIfPresent(transformOptions, PDF_FORMAT, String.class).orElse(DEFAULT_PDF_FORMAT_STRING);
final String pdfOrientation = parseOptionIfPresent(transformOptions, PDF_ORIENTATION, String.class).orElse(DEFAULT_PDF_ORIENTATION_STRING);
verifyOptions(startPage, endPage);
final ImageReader imageReader = findImageReader(imageInputStream, imageFile.getName(), sourceMimetype);
@@ -108,7 +111,7 @@ public class ImageToPdfTransformer implements CustomTransformerFileAdaptor
break;
}
scaleAndDrawImage(pdfDocument, imageReader.read(i), pdfFormat);
scaleAndDrawImage(pdfDocument, imageReader.read(i), pdfFormat, pdfOrientation);
}
pdfDocument.save(pdfFile);
@@ -128,11 +131,12 @@ public class ImageToPdfTransformer implements CustomTransformerFileAdaptor
return imageReader;
}
private void scaleAndDrawImage(final PDDocument pdfDocument, final BufferedImage bufferedImage, final String pdfFormat) throws IOException
private void scaleAndDrawImage(final PDDocument pdfDocument, final BufferedImage bufferedImage, final String pdfFormat, final String pdfOrientation)
throws IOException
{
final PDPage pdfPage = new PDPage(resolvePdfFormat(pdfFormat));
pdfDocument.addPage(pdfPage);
final PDImageXObject image = LosslessFactory.createFromImage(pdfDocument, bufferedImage);
final PDPage pdfPage = new PDPage(resolvePdfFormat(pdfFormat, pdfOrientation, image.getWidth(), image.getHeight()));
pdfDocument.addPage(pdfPage);
try (PDPageContentStream pdfPageContent = new PDPageContentStream(pdfDocument, pdfPage))
{
final PDRectangle pageSize = pdfPage.getMediaBox();
@@ -146,31 +150,69 @@ public class ImageToPdfTransformer implements CustomTransformerFileAdaptor
}
}
private PDRectangle resolvePdfFormat(final String pdfFormat)
private PDRectangle resolvePdfFormat(final String pdfFormat, final String pdfOrientation, final int defaultWidth, final int defaultHeight)
{
switch (pdfFormat.toUpperCase()) {
PDRectangle pdRectangle;
switch (pdfFormat.toUpperCase())
{
case "DEFAULT":
pdRectangle = new PDRectangle(defaultWidth, defaultHeight);
break;
case "A4":
return DEFAULT_PDF_FORMAT;
pdRectangle = PDRectangle.A4;
break;
case "LETTER":
return PDRectangle.LETTER;
pdRectangle = PDRectangle.LETTER;
break;
case "A0":
return PDRectangle.A0;
pdRectangle = PDRectangle.A0;
break;
case "A1":
return PDRectangle.A1;
pdRectangle = PDRectangle.A1;
break;
case "A2":
return PDRectangle.A2;
pdRectangle = PDRectangle.A2;
break;
case "A3":
return PDRectangle.A3;
pdRectangle = PDRectangle.A3;
break;
case "A5":
return PDRectangle.A5;
pdRectangle = PDRectangle.A5;
break;
case "A6":
return PDRectangle.A6;
pdRectangle = PDRectangle.A6;
break;
case "LEGAL":
return PDRectangle.LEGAL;
pdRectangle = PDRectangle.LEGAL;
break;
default:
log.info("PDF format: '{}' not supported. Using default: '{}'", pdfFormat, DEFAULT_PDF_FORMAT_STRING);
return DEFAULT_PDF_FORMAT;
log.warn("PDF format: '{}' not supported. Maintaining the default one.", pdfFormat);
pdRectangle = new PDRectangle(defaultWidth, defaultHeight);
break;
}
switch (pdfOrientation.toUpperCase())
{
case "DEFAULT":
break;
case "PORTRAIT":
if (pdRectangle.getWidth() > pdRectangle.getHeight())
{
pdRectangle = new PDRectangle(pdRectangle.getHeight(), pdRectangle.getWidth());
}
break;
case "LANDSCAPE":
if (pdRectangle.getHeight() > pdRectangle.getWidth())
{
pdRectangle = new PDRectangle(pdRectangle.getHeight(), pdRectangle.getWidth());
}
break;
default:
log.warn("PDF orientation: '{}' not supported. Maintaining the default one.", pdfOrientation);
break;
}
return pdRectangle;
}
private static <T> Optional<T> parseOptionIfPresent(final Map<String, String> transformOptions, final String parameter, final Class<T> targetType)

View File

@@ -12,7 +12,8 @@
"imageToPdfOptions": [
{"value": {"name": "startPage"}},
{"value": {"name": "endPage"}},
{"value": {"name": "pdfFormat"}}
{"value": {"name": "pdfFormat"}},
{"value": {"name": "pdfOrientation"}}
]
},
"transformers": [

View File

@@ -33,18 +33,23 @@ import static org.alfresco.transform.common.Mimetype.MIMETYPE_IMAGE_TIFF;
import static org.alfresco.transform.common.Mimetype.MIMETYPE_PDF;
import static org.alfresco.transform.common.RequestParamMap.END_PAGE;
import static org.alfresco.transform.common.RequestParamMap.PDF_FORMAT;
import static org.alfresco.transform.common.RequestParamMap.PDF_ORIENTATION;
import static org.alfresco.transform.common.RequestParamMap.START_PAGE;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.mockito.BDDMockito.then;
import javax.imageio.ImageIO;
import java.awt.image.BufferedImage;
import java.io.File;
import java.io.IOException;
import java.util.HashMap;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.function.BiFunction;
import java.util.function.BiPredicate;
import java.util.function.Function;
import java.util.stream.Stream;
@@ -64,6 +69,8 @@ import org.mockito.MockitoAnnotations;
class ImageToPdfTransformerTest
{
private static final File sourceFile = loadFile("sample.gif");
private static final int sourceFileWidth;
private static final int sourceFileHeight;
@Mock
private TransformManager transformManager;
@@ -108,16 +115,17 @@ class ImageToPdfTransformerTest
TransformOptions.of(null, 0), // expected 1 page in target file
TransformOptions.of(null, 1), // expected 2 pages in target file
TransformOptions.of(0, null), // expected all pages in target file
TransformOptions.of(1, null), // expected 1 page in target file
TransformOptions.of(1, null), // expected all except first page in target file
TransformOptions.none() // expected all pages in target file
);
}
static Stream<Arguments> transformSourcesAndOptions()
{
ImageFile tiffImage = ImageFile.of("sample.tiff", MIMETYPE_IMAGE_TIFF, 6);
return Stream.of(
ArgumentsCartesianProduct.of(imageFiles(), defaultTransformOptions()),
ArgumentsCartesianProduct.of(ImageFile.of("sample.tiff", MIMETYPE_IMAGE_TIFF, 6), tiffTransformOptions())
ArgumentsCartesianProduct.of(tiffImage, tiffTransformOptions())
).flatMap(Function.identity());
}
@@ -186,26 +194,65 @@ class ImageToPdfTransformerTest
transformer.transform(MIMETYPE_IMAGE_TIFF, MIMETYPE_PDF, transformOptions, sourceFile, targetFile, transformManager));
}
static Stream<String> validPdfFormats()
/** Option and expected dimensions. */
static Stream<Arguments> validPdfFormats()
{
return Stream.of("A0", "a0", "A1", "A2", "A3", "A4", "A5", "A6", "a6", "LETTER", "letter", "LEGAL", "legal");
return Stream.of(
Arguments.of("DEFAULT", new PDRectangle(sourceFileWidth, sourceFileHeight)),
Arguments.of("default", new PDRectangle(sourceFileWidth, sourceFileHeight)),
Arguments.of("A0", PDRectangle.A0),
Arguments.of("a0", PDRectangle.A0),
Arguments.of("A1", PDRectangle.A1),
Arguments.of("A2", PDRectangle.A2),
Arguments.of("A3", PDRectangle.A3),
Arguments.of("A4", PDRectangle.A4),
Arguments.of("A5", PDRectangle.A5),
Arguments.of("A6", PDRectangle.A6),
Arguments.of("A6", PDRectangle.A6),
Arguments.of("LETTER", PDRectangle.LETTER),
Arguments.of("letter", PDRectangle.LETTER),
Arguments.of("LEGAL", PDRectangle.LEGAL),
Arguments.of("legal", PDRectangle.LEGAL)
);
}
/** Option and expected orientation. */
static Stream<Arguments> validPdfOrientations()
{
return Stream.of(
Arguments.of("DEFAULT", unchangedRectangle()),
Arguments.of("default", unchangedRectangle()),
Arguments.of("PORTRAIT", rectangleRotatedIf((width, height) -> width > height)),
Arguments.of("portrait", rectangleRotatedIf((width, height) -> width > height)),
Arguments.of("LANDSCAPE", rectangleRotatedIf((width, height) -> height > width)),
Arguments.of("landscape", rectangleRotatedIf((width, height) -> height > width))
);
}
static Stream<Arguments> validPdfFormatsAndOrientations()
{
return ArgumentsCartesianProduct.ofArguments(
validPdfFormats(),
validPdfOrientations()
);
}
@ParameterizedTest
@MethodSource("validPdfFormats")
void testTransformImageToPDF_withVariousPdfFormats(String pdfFormat) throws Exception
@MethodSource("validPdfFormatsAndOrientations")
void testTransformImageToPDF_withVariousPdfFormatsAndOrientations(String pdfFormat, PDRectangle expectedPdfFormat,
String pdfOrientation, BiFunction<Float, Float, PDRectangle> expectedPdfFormatRotator) throws Exception
{
TransformOptions transformOptions = TransformOptions.of(pdfFormat);
TransformOptions transformOptions = TransformOptions.of(pdfFormat, pdfOrientation);
// when
transformer.transform(MIMETYPE_IMAGE_TIFF, MIMETYPE_PDF, transformOptions.toMap(), sourceFile, targetFile, transformManager);
try (PDDocument actualPdfDocument = PDDocument.load(targetFile))
{
PDRectangle expectedPdfFormat = resolveExpectedPdfFormat(pdfFormat);
PDRectangle finalExpectedPdfFormat = expectedPdfFormatRotator.apply(expectedPdfFormat.getWidth(), expectedPdfFormat.getHeight());
assertNotNull(actualPdfDocument);
assertEquals(expectedPdfFormat.getWidth(), actualPdfDocument.getPage(0).getMediaBox().getWidth());
assertEquals(expectedPdfFormat.getHeight(), actualPdfDocument.getPage(0).getMediaBox().getHeight());
assertEquals(finalExpectedPdfFormat.getWidth(), actualPdfDocument.getPage(0).getMediaBox().getWidth());
assertEquals(finalExpectedPdfFormat.getHeight(), actualPdfDocument.getPage(0).getMediaBox().getHeight());
}
}
@@ -219,37 +266,45 @@ class ImageToPdfTransformerTest
try (PDDocument actualPdfDocument = PDDocument.load(targetFile))
{
BufferedImage actualImage = ImageIO.read(sourceFile);
assertNotNull(actualPdfDocument);
assertEquals(PDRectangle.A4.getWidth(), actualPdfDocument.getPage(0).getMediaBox().getWidth());
assertEquals(PDRectangle.A4.getHeight(), actualPdfDocument.getPage(0).getMediaBox().getHeight());
assertEquals(actualImage.getWidth(), actualPdfDocument.getPage(0).getMediaBox().getWidth());
assertEquals(actualImage.getHeight(), actualPdfDocument.getPage(0).getMediaBox().getHeight());
}
}
@Test
void testTransformImageToPDF_withInvalidPdfOrientationAndUsingDefaultOne() throws Exception
{
TransformOptions transformOptions = TransformOptions.of(null, "INVALID");
// when
transformer.transform(MIMETYPE_IMAGE_TIFF, MIMETYPE_PDF, transformOptions.toMap(), sourceFile, targetFile, transformManager);
try (PDDocument actualPdfDocument = PDDocument.load(targetFile))
{
BufferedImage actualImage = ImageIO.read(sourceFile);
assertNotNull(actualPdfDocument);
assertEquals(actualImage.getWidth(), actualPdfDocument.getPage(0).getMediaBox().getWidth());
assertEquals(actualImage.getHeight(), actualPdfDocument.getPage(0).getMediaBox().getHeight());
}
}
//----------------------------------------------- Helper methods and classes -----------------------------------------------
private static PDRectangle resolveExpectedPdfFormat(String pdfFormat)
private static BiFunction<Float, Float, PDRectangle> unchangedRectangle()
{
switch (pdfFormat.toUpperCase()) {
case "LETTER":
return PDRectangle.LETTER;
case "LEGAL":
return PDRectangle.LEGAL;
case "A0":
return PDRectangle.A0;
case "A1":
return PDRectangle.A1;
case "A2":
return PDRectangle.A2;
case "A3":
return PDRectangle.A3;
case "A5":
return PDRectangle.A5;
case "A6":
return PDRectangle.A6;
case "A4":
default:
return PDRectangle.A4;
return rectangleRotatedIf(null);
}
private static BiFunction<Float, Float, PDRectangle> rectangleRotatedIf(BiPredicate<Float, Float> predicate)
{
if (predicate == null)
{
return PDRectangle::new;
}
return (width, height) -> predicate.test(width, height)? new PDRectangle(height, width) : new PDRectangle(width, height);
}
private static File loadFile(String fileName)
@@ -302,12 +357,14 @@ class ImageToPdfTransformerTest
Integer startPage;
Integer endPage;
String pdfFormat;
String pdfOrientation;
private TransformOptions(Integer startPage, Integer endPage, String pdfFormat)
private TransformOptions(Integer startPage, Integer endPage, String pdfFormat, String pdfOrientation)
{
this.startPage = startPage;
this.endPage = endPage;
this.pdfFormat = pdfFormat;
this.pdfOrientation = pdfOrientation;
}
public Map<String, String> toMap()
@@ -325,17 +382,26 @@ class ImageToPdfTransformerTest
{
transformOptions.put(PDF_FORMAT, pdfFormat);
}
if (pdfOrientation != null)
{
transformOptions.put(PDF_ORIENTATION, pdfOrientation);
}
return transformOptions;
}
public static TransformOptions of(Integer startPage, Integer endPage)
{
return new TransformOptions(startPage, endPage, null);
return new TransformOptions(startPage, endPage, null, null);
}
public static TransformOptions of(String pdfFormat)
{
return new TransformOptions(null, null, pdfFormat);
return new TransformOptions(null, null, pdfFormat, null);
}
public static TransformOptions of(String pdfFormat, String pdfOrientation)
{
return new TransformOptions(null, null, pdfFormat, pdfOrientation);
}
public static TransformOptions none()
@@ -349,4 +415,17 @@ class ImageToPdfTransformerTest
return "TransformOption{" + "startPage=" + startPage + ", endPage=" + endPage + '}';
}
}
static {
try
{
BufferedImage image = ImageIO.read(sourceFile);
sourceFileWidth = image.getWidth();
sourceFileHeight = image.getHeight();
}
catch (IOException e)
{
throw new RuntimeException(e);
}
}
}

View File

@@ -27,6 +27,7 @@
package org.alfresco.transform.misc.util;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
@@ -42,7 +43,7 @@ import org.junit.jupiter.params.provider.Arguments;
public class ArgumentsCartesianProduct
{
/**
* Creates cartesian product of fixed argument and a stream of arguments.
* Creates arguments cartesian product of fixed object and a stream of objects.
* Example: a ✕ {x,y,z} = {a,x}, {a,y}, {a,z}
*/
public static Stream<Arguments> of(final Object fixedFirstArgument, final Stream<?> secondArguments)
@@ -51,7 +52,7 @@ public class ArgumentsCartesianProduct
}
/**
* Creates cartesian product of a stream of arguments and fixed arguments.
* Creates arguments cartesian product of a stream of objects and fixed object.
* Example: {a,b,c} ✕ y ✕ z = {a,y,z}, {b,y,z}, {c,y,z}
*/
public static Stream<Arguments> of(final Stream<?> firstArguments, final Object... otherFixedArguments)
@@ -60,7 +61,7 @@ public class ArgumentsCartesianProduct
}
/**
* Creates cartesian product of two streams of arguments.
* Creates arguments cartesian product of two streams of objects.
* Example: {a,b} ✕ {y,z} = {a,y}, {a,z}, {b,y}, {b,z}
*/
public static Stream<Arguments> of(final Stream<?> firstArguments, final Stream<?> secondArguments)
@@ -69,7 +70,7 @@ public class ArgumentsCartesianProduct
}
/**
* Creates cartesian product of multiple streams of arguments.
* Creates arguments cartesian product of multiple streams of objects.
* Example: {a,b} ✕ {k,l,m} ✕ ... ✕ {y,z} = {a,k,...,y}, {a,k,...,z}, {a,l,...,y}, ..., {b,m,...,z}
*/
public static Stream<Arguments> of(final Stream<?>... argumentsStreams)
@@ -77,6 +78,21 @@ public class ArgumentsCartesianProduct
return cartesianProductOf(argumentsStreams).map(arguments -> Arguments.of(arguments.toArray()));
}
/**
* Creates arguments cartesian product of multiple streams of arguments.
* Example: {a,b} ✕ {k,l,m} ✕ ... ✕ {y,z} = {a,k,...,y}, {a,k,...,z}, {a,l,...,y}, ..., {b,m,...,z}
*/
@SafeVarargs
@SuppressWarnings("unchecked")
public static Stream<Arguments> ofArguments(final Stream<Arguments>... argumentsStreams)
{
return cartesianProductOf(argumentsStreams)
.map(argumentsStream -> (Stream<Arguments>) argumentsStream)
.map(argumentsStream -> Arguments.of(argumentsStream
.flatMap(arguments -> Arrays.stream(arguments.get()))
.toArray()));
}
private static Stream<Stream<?>> cartesianProductOf(final Stream<?>... streams)
{
if (streams == null)

View File

@@ -65,6 +65,7 @@ public interface RequestParamMap
String NOT_EXTRACT_BOOKMARKS_TEXT = "notExtractBookmarksText";
String PAGE_LIMIT = "pageLimit";
String PDF_FORMAT = "pdfFormat";
String PDF_ORIENTATION = "pdfOrientation";
// Parameters interpreted by the TransformController
String DIRECT_ACCESS_URL = "directAccessUrl";