ATS-996: TIFF to PDF - invalid output format

This commit is contained in:
Krystian Dabrowski 2023-03-27 16:30:32 +02:00
parent 34fa617d0b
commit b84a5613f3
4 changed files with 155 additions and 48 deletions

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.END_PAGE;
import static org.alfresco.transform.common.RequestParamMap.PDF_FORMAT; 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.alfresco.transform.common.RequestParamMap.START_PAGE;
import javax.imageio.ImageIO; 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. * 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. * 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. * 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: * Transformer accepts bellow optional transform parameters:
* - startPage - page number of image (for multipage images) from which transformer should start conversion. Default: first page of the image. * - 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 multipage images) up to which transformation should be performed. Default: last 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: A0, A1, A2, A3, A4, A5, A6, LETTER, LEGAL. Default: A4. * - 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 @Component
public class ImageToPdfTransformer implements CustomTransformerFileAdaptor 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 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_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 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 String DEFAULT_PDF_FORMAT_STRING = "DEFAULT";
private static final PDRectangle DEFAULT_PDF_FORMAT = PDRectangle.A4; private static final String DEFAULT_PDF_ORIENTATION_STRING = "DEFAULT";
@Override @Override
public String getTransformerName() 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 startPage = parseOptionIfPresent(transformOptions, START_PAGE, Integer.class).orElse(null);
final Integer endPage = parseOptionIfPresent(transformOptions, END_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 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); verifyOptions(startPage, endPage);
final ImageReader imageReader = findImageReader(imageInputStream, imageFile.getName(), sourceMimetype); final ImageReader imageReader = findImageReader(imageInputStream, imageFile.getName(), sourceMimetype);
@ -108,7 +111,7 @@ public class ImageToPdfTransformer implements CustomTransformerFileAdaptor
break; break;
} }
scaleAndDrawImage(pdfDocument, imageReader.read(i), pdfFormat); scaleAndDrawImage(pdfDocument, imageReader.read(i), pdfFormat, pdfOrientation);
} }
pdfDocument.save(pdfFile); pdfDocument.save(pdfFile);
@ -128,11 +131,12 @@ public class ImageToPdfTransformer implements CustomTransformerFileAdaptor
return imageReader; 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 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)) try (PDPageContentStream pdfPageContent = new PDPageContentStream(pdfDocument, pdfPage))
{ {
final PDRectangle pageSize = pdfPage.getMediaBox(); final PDRectangle pageSize = pdfPage.getMediaBox();
@ -146,31 +150,67 @@ public class ImageToPdfTransformer implements CustomTransformerFileAdaptor
} }
} }
private PDRectangle resolvePdfFormat(final String pdfFormat) private PDRectangle resolvePdfFormat(final String pdfFormat, final String pdfOrientation, final int actualWidth, final int actualHeight)
{ {
PDRectangle pdRectangle;
switch (pdfFormat.toUpperCase()) { switch (pdfFormat.toUpperCase()) {
case "DEFAULT":
pdRectangle = new PDRectangle(actualWidth, actualHeight);
break;
case "A4": case "A4":
return DEFAULT_PDF_FORMAT; pdRectangle = PDRectangle.A4;
break;
case "LETTER": case "LETTER":
return PDRectangle.LETTER; pdRectangle = PDRectangle.LETTER;
break;
case "A0": case "A0":
return PDRectangle.A0; pdRectangle = PDRectangle.A0;
break;
case "A1": case "A1":
return PDRectangle.A1; pdRectangle = PDRectangle.A1;
break;
case "A2": case "A2":
return PDRectangle.A2; pdRectangle = PDRectangle.A2;
break;
case "A3": case "A3":
return PDRectangle.A3; pdRectangle = PDRectangle.A3;
break;
case "A5": case "A5":
return PDRectangle.A5; pdRectangle = PDRectangle.A5;
break;
case "A6": case "A6":
return PDRectangle.A6; pdRectangle = PDRectangle.A6;
break;
case "LEGAL": case "LEGAL":
return PDRectangle.LEGAL; pdRectangle = PDRectangle.LEGAL;
break;
default: default:
log.info("PDF format: '{}' not supported. Using default: '{}'", pdfFormat, DEFAULT_PDF_FORMAT_STRING); log.warn("PDF format: '{}' not supported. Maintaining the default one.", pdfFormat);
return DEFAULT_PDF_FORMAT; pdRectangle = new PDRectangle(actualWidth, actualHeight);
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) 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": [ "imageToPdfOptions": [
{"value": {"name": "startPage"}}, {"value": {"name": "startPage"}},
{"value": {"name": "endPage"}}, {"value": {"name": "endPage"}},
{"value": {"name": "pdfFormat"}} {"value": {"name": "pdfFormat"}},
{"value": {"name": "pdfOrientation"}}
] ]
}, },
"transformers": [ "transformers": [

View File

@ -33,12 +33,15 @@ 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.Mimetype.MIMETYPE_PDF;
import static org.alfresco.transform.common.RequestParamMap.END_PAGE; 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_FORMAT;
import static org.alfresco.transform.common.RequestParamMap.PDF_ORIENTATION;
import static org.alfresco.transform.common.RequestParamMap.START_PAGE; import static org.alfresco.transform.common.RequestParamMap.START_PAGE;
import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.mockito.BDDMockito.then; import static org.mockito.BDDMockito.then;
import javax.imageio.ImageIO;
import java.awt.image.BufferedImage;
import java.io.File; import java.io.File;
import java.io.IOException; import java.io.IOException;
import java.util.HashMap; import java.util.HashMap;
@ -108,16 +111,17 @@ class ImageToPdfTransformerTest
TransformOptions.of(null, 0), // expected 1 page in target file TransformOptions.of(null, 0), // expected 1 page in target file
TransformOptions.of(null, 1), // expected 2 pages 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(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 TransformOptions.none() // expected all pages in target file
); );
} }
static Stream<Arguments> transformSourcesAndOptions() static Stream<Arguments> transformSourcesAndOptions()
{ {
ImageFile tiffImage = ImageFile.of("sample.tiff", MIMETYPE_IMAGE_TIFF, 6);
return Stream.of( return Stream.of(
ArgumentsCartesianProduct.of(imageFiles(), defaultTransformOptions()), ArgumentsCartesianProduct.of(imageFiles(), defaultTransformOptions()),
ArgumentsCartesianProduct.of(ImageFile.of("sample.tiff", MIMETYPE_IMAGE_TIFF, 6), tiffTransformOptions()) ArgumentsCartesianProduct.of(tiffImage, tiffTransformOptions())
).flatMap(Function.identity()); ).flatMap(Function.identity());
} }
@ -186,23 +190,27 @@ class ImageToPdfTransformerTest
transformer.transform(MIMETYPE_IMAGE_TIFF, MIMETYPE_PDF, transformOptions, sourceFile, targetFile, transformManager)); transformer.transform(MIMETYPE_IMAGE_TIFF, MIMETYPE_PDF, transformOptions, sourceFile, targetFile, transformManager));
} }
static Stream<String> validPdfFormats() static Stream<Arguments> validPdfFormatsAndOrientations()
{ {
return Stream.of("A0", "a0", "A1", "A2", "A3", "A4", "A5", "A6", "a6", "LETTER", "letter", "LEGAL", "legal"); return ArgumentsCartesianProduct.of(
Stream.of("default", "DEFAULT", "A0", "a0", "A1", "A2", "A3", "A4", "A5", "A6", "a6", "LETTER", "letter", "LEGAL", "legal"),
Stream.of("default", "DEFAULT", "portrait", "PORTRAIT", "landscape", "LANDSCAPE")
);
} }
@ParameterizedTest @ParameterizedTest
@MethodSource("validPdfFormats") @MethodSource("validPdfFormatsAndOrientations")
void testTransformImageToPDF_withVariousPdfFormats(String pdfFormat) throws Exception void testTransformImageToPDF_withVariousPdfFormatsAndOrientations(String pdfFormat, String pdfOrientation) throws Exception
{ {
TransformOptions transformOptions = TransformOptions.of(pdfFormat); TransformOptions transformOptions = TransformOptions.of(pdfFormat, pdfOrientation);
// when // when
transformer.transform(MIMETYPE_IMAGE_TIFF, MIMETYPE_PDF, transformOptions.toMap(), sourceFile, targetFile, transformManager); transformer.transform(MIMETYPE_IMAGE_TIFF, MIMETYPE_PDF, transformOptions.toMap(), sourceFile, targetFile, transformManager);
try (PDDocument actualPdfDocument = PDDocument.load(targetFile)) try (PDDocument actualPdfDocument = PDDocument.load(targetFile))
{ {
PDRectangle expectedPdfFormat = resolveExpectedPdfFormat(pdfFormat); BufferedImage actualImage = ImageIO.read(sourceFile);
PDRectangle expectedPdfFormat = resolveExpectedPdfFormat(pdfFormat, pdfOrientation, actualImage.getWidth(), actualImage.getHeight());
assertNotNull(actualPdfDocument); assertNotNull(actualPdfDocument);
assertEquals(expectedPdfFormat.getWidth(), actualPdfDocument.getPage(0).getMediaBox().getWidth()); assertEquals(expectedPdfFormat.getWidth(), actualPdfDocument.getPage(0).getMediaBox().getWidth());
assertEquals(expectedPdfFormat.getHeight(), actualPdfDocument.getPage(0).getMediaBox().getHeight()); assertEquals(expectedPdfFormat.getHeight(), actualPdfDocument.getPage(0).getMediaBox().getHeight());
@ -219,37 +227,83 @@ class ImageToPdfTransformerTest
try (PDDocument actualPdfDocument = PDDocument.load(targetFile)) try (PDDocument actualPdfDocument = PDDocument.load(targetFile))
{ {
BufferedImage actualImage = ImageIO.read(sourceFile);
assertNotNull(actualPdfDocument); assertNotNull(actualPdfDocument);
assertEquals(PDRectangle.A4.getWidth(), actualPdfDocument.getPage(0).getMediaBox().getWidth()); assertEquals(actualImage.getWidth(), actualPdfDocument.getPage(0).getMediaBox().getWidth());
assertEquals(PDRectangle.A4.getHeight(), actualPdfDocument.getPage(0).getMediaBox().getHeight()); 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 ----------------------------------------------- //----------------------------------------------- Helper methods and classes -----------------------------------------------
private static PDRectangle resolveExpectedPdfFormat(String pdfFormat) private static PDRectangle resolveExpectedPdfFormat(String pdfFormat, String pdfOrientation, int defaultWidth, int defaultHeight)
{ {
PDRectangle pdRectangle;
switch (pdfFormat.toUpperCase()) { switch (pdfFormat.toUpperCase()) {
case "LETTER": case "LETTER":
return PDRectangle.LETTER; pdRectangle = PDRectangle.LETTER;
break;
case "LEGAL": case "LEGAL":
return PDRectangle.LEGAL; pdRectangle = PDRectangle.LEGAL;
break;
case "A0": case "A0":
return PDRectangle.A0; pdRectangle = PDRectangle.A0;
break;
case "A1": case "A1":
return PDRectangle.A1; pdRectangle = PDRectangle.A1;
break;
case "A2": case "A2":
return PDRectangle.A2; pdRectangle = PDRectangle.A2;
break;
case "A3": case "A3":
return PDRectangle.A3; pdRectangle = PDRectangle.A3;
case "A5": break;
return PDRectangle.A5;
case "A6":
return PDRectangle.A6;
case "A4": case "A4":
pdRectangle = PDRectangle.A4;
break;
case "A5":
pdRectangle = PDRectangle.A5;
break;
case "A6":
pdRectangle = PDRectangle.A6;
break;
default: default:
return PDRectangle.A4; pdRectangle = new PDRectangle(defaultWidth, defaultHeight);
} }
switch (pdfOrientation.toUpperCase()) {
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;
}
return pdRectangle;
} }
private static File loadFile(String fileName) private static File loadFile(String fileName)
@ -302,12 +356,14 @@ class ImageToPdfTransformerTest
Integer startPage; Integer startPage;
Integer endPage; Integer endPage;
String pdfFormat; 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.startPage = startPage;
this.endPage = endPage; this.endPage = endPage;
this.pdfFormat = pdfFormat; this.pdfFormat = pdfFormat;
this.pdfOrientation = pdfOrientation;
} }
public Map<String, String> toMap() public Map<String, String> toMap()
@ -325,17 +381,26 @@ class ImageToPdfTransformerTest
{ {
transformOptions.put(PDF_FORMAT, pdfFormat); transformOptions.put(PDF_FORMAT, pdfFormat);
} }
if (pdfOrientation != null)
{
transformOptions.put(PDF_ORIENTATION, pdfOrientation);
}
return transformOptions; return transformOptions;
} }
public static TransformOptions of(Integer startPage, Integer endPage) 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) 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() public static TransformOptions none()

View File

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