Skip to content

Commit 340d769

Browse files
committed
Improve input processing for ImageMagick and Ghostscript
DEVSIX-5638
1 parent e3a9502 commit 340d769

File tree

9 files changed

+839
-60
lines changed

9 files changed

+839
-60
lines changed

io/src/main/java/com/itextpdf/io/util/FileUtil.java

Lines changed: 85 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -43,18 +43,14 @@ This file is part of the iText (R) project.
4343
*/
4444
package com.itextpdf.io.util;
4545

46-
import java.io.InputStream;
47-
import java.nio.file.Files;
48-
import java.nio.file.Path;
49-
import org.slf4j.LoggerFactory;
50-
5146
import java.io.BufferedOutputStream;
5247
import java.io.ByteArrayOutputStream;
5348
import java.io.File;
5449
import java.io.FileFilter;
5550
import java.io.FileNotFoundException;
5651
import java.io.FileOutputStream;
5752
import java.io.IOException;
53+
import java.io.InputStream;
5854
import java.io.OutputStream;
5955
import java.io.OutputStreamWriter;
6056
import java.io.PrintWriter;
@@ -63,11 +59,15 @@ This file is part of the iText (R) project.
6359
import java.net.MalformedURLException;
6460
import java.net.URISyntaxException;
6561
import java.net.URL;
62+
import java.nio.file.Files;
63+
import java.nio.file.Path;
6664
import java.nio.file.Paths;
65+
import java.nio.file.StandardCopyOption;
6766
import java.util.ArrayList;
6867
import java.util.Arrays;
6968
import java.util.Comparator;
7069
import java.util.List;
70+
import org.slf4j.LoggerFactory;
7171

7272
/**
7373
* This file is a helper class for internal usage only.
@@ -223,6 +223,86 @@ public static String parentDirectory(URL url) throws URISyntaxException {
223223
return url.toURI().resolve(".").toString();
224224
}
225225

226+
/**
227+
* Creates a temporary file.
228+
*
229+
* @param tempFilePrefix the prefix of the copied file's name
230+
* @param tempFilePostfix the postfix of the copied file's name
231+
*
232+
* @return the path to the copied file
233+
*/
234+
public static File createTempFile(String tempFilePrefix, String tempFilePostfix) throws IOException {
235+
return File.createTempFile(tempFilePrefix, tempFilePostfix);
236+
}
237+
238+
/**
239+
* Creates a temporary copy of a file.
240+
*
241+
* @param file the path to the file to be copied
242+
* @param tempFilePrefix the prefix of the copied file's name
243+
* @param tempFilePostfix the postfix of the copied file's name
244+
*
245+
* @return the path to the copied file
246+
*/
247+
public static String createTempCopy(String file, String tempFilePrefix, String tempFilePostfix)
248+
throws IOException {
249+
Path replacementFilePath = null;
250+
try {
251+
replacementFilePath = Files.createTempFile(tempFilePrefix, tempFilePostfix);
252+
Path pathToPassedFile = Paths.get(file);
253+
Files.copy(pathToPassedFile, replacementFilePath, StandardCopyOption.REPLACE_EXISTING);
254+
} catch (IOException e) {
255+
if (null != replacementFilePath) {
256+
FileUtil.removeFiles(new String[] {replacementFilePath.toString()});
257+
}
258+
throw e;
259+
}
260+
return replacementFilePath.toString();
261+
}
262+
263+
/**
264+
* Creates a copy of a file.
265+
*
266+
* @param inputFile the path to the file to be copied
267+
* @param outputFile the path, to which the passed file should be copied
268+
*/
269+
public static void copy(String inputFile, String outputFile)
270+
throws IOException {
271+
Files.copy(Paths.get(inputFile), Paths.get(outputFile), StandardCopyOption.REPLACE_EXISTING);
272+
}
273+
274+
/**
275+
* Creates a temporary directory.
276+
*
277+
* @param tempFilePrefix the prefix of the temporary directory's name
278+
* @return the path to the temporary directory
279+
*/
280+
public static String createTempDirectory(String tempFilePrefix)
281+
throws IOException {
282+
return Files.createTempDirectory(tempFilePrefix).toString();
283+
}
284+
285+
/**
286+
* Removes all of the passed files.
287+
*
288+
* @param paths paths to files, which should be removed
289+
*
290+
* @return true if all the files have been successfully removed, false otherwise
291+
*/
292+
public static boolean removeFiles(String[] paths) {
293+
boolean allFilesAreRemoved = true;
294+
for (String path : paths) {
295+
try {
296+
if (null != path) {
297+
Files.delete(Paths.get(path));
298+
}
299+
} catch (Exception e) {
300+
allFilesAreRemoved = false;
301+
}
302+
}
303+
return allFilesAreRemoved;
304+
}
305+
226306
private static class CaseSensitiveFileComparator implements Comparator<File> {
227307
@Override
228308
public int compare(File f1, File f2) {

io/src/main/java/com/itextpdf/io/util/GhostscriptHelper.java

Lines changed: 95 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,8 @@ This file is part of the iText (R) project.
4646
import com.itextpdf.io.IoExceptionMessage;
4747

4848
import java.io.IOException;
49+
import java.nio.file.Paths;
50+
import java.util.regex.Pattern;
4951

5052
/**
5153
* A utility class that is used as an interface to run 3rd-party tool Ghostscript.
@@ -67,8 +69,14 @@ public class GhostscriptHelper {
6769
static final String GHOSTSCRIPT_ENVIRONMENT_VARIABLE_LEGACY = "gsExec";
6870

6971
static final String GHOSTSCRIPT_KEYWORD = "GPL Ghostscript";
72+
private static final String TEMP_FILE_PREFIX = "itext_gs_io_temp";
7073

71-
private static final String GHOSTSCRIPT_PARAMS = " -dSAFER -dNOPAUSE -dBATCH -sDEVICE=png16m -r150 {0} -sOutputFile=\"{1}\" \"{2}\"";
74+
private static final String RENDERED_IMAGE_EXTENSION = "png";
75+
private static final String GHOSTSCRIPT_PARAMS = " -dSAFER -dNOPAUSE -dBATCH -sDEVICE="
76+
+ RENDERED_IMAGE_EXTENSION + "16m -r150 {0} -sOutputFile=\"{1}\" \"{2}\"";
77+
private static final String PAGE_NUMBER_PATTERN = "%03d";
78+
79+
private static final Pattern PAGE_LIST_REGEX = Pattern.compile("^(\\d+,)*\\d+$");
7280

7381
private String gsExec;
7482

@@ -112,11 +120,17 @@ public String getCliExecutionCommand() {
112120
}
113121

114122
/**
115-
* Runs ghostscript to create images of pdfs.
123+
* Runs Ghostscript to render the PDF's pages as PNG images.
116124
*
117-
* @param pdf Path to the pdf file.
118-
* @param outDir Path to the output directory
119-
* @param image Path to the generated image
125+
* @param pdf Path to the PDF file to be rendered
126+
* @param outDir Path to the output directory, in which the rendered pages will be stored
127+
* @param image String which defines the name of the resultant images. This string will be
128+
* concatenated with the number of the rendered page from the start of the
129+
* PDF in "-%03d" format, e.g. "-011" for the eleventh rendered page and so on.
130+
* This number may not correspond to the actual page number: for example,
131+
* if the passed pageList equals to "5,3", then images with postfixes "-001.png"
132+
* and "-002.png" will be created: the former for the third page, the latter
133+
* for the fifth page. "%" sign in the passed name is prohibited.
120134
*
121135
* @throws IOException if there are file's reading/writing issues
122136
* @throws InterruptedException if there is thread interruption while executing GhostScript.
@@ -127,14 +141,20 @@ public void runGhostScriptImageGeneration(String pdf, String outDir, String imag
127141
}
128142

129143
/**
130-
* Runs ghostscript to create images of specified pages of pdfs.
144+
* Runs Ghostscript to render the PDF's pages as PNG images.
131145
*
132-
* @param pdf Path to the pdf file.
133-
* @param outDir Path to the output directory
134-
* @param image Path to the generated image
135-
* @param pageList String with numbers of the required pages to extract as image. Should be formatted as string with
136-
* numbers, separated by commas, without whitespaces. Can be null, if it is required to extract
137-
* all pages as images.
146+
* @param pdf Path to the PDF file to be rendered
147+
* @param outDir Path to the output directory, in which the rendered pages will be stored
148+
* @param image String which defines the name of the resultant images. This string will be
149+
* concatenated with the number of the rendered page from the start of the
150+
* PDF in "-%03d" format, e.g. "-011" for the eleventh rendered page and so on.
151+
* This number may not correspond to the actual page number: for example,
152+
* if the passed pageList equals to "5,3", then images with postfixes "-001.png"
153+
* and "-002.png" will be created: the former for the third page, the latter
154+
* for the fifth page. "%" sign in the passed name is prohibited.
155+
* @param pageList String with numbers of the required pages to be rendered as images.
156+
* This string should be formatted as a string with numbers, separated by commas,
157+
* without whitespaces. Can be null, if it is required to render all the PDF's pages.
138158
*
139159
* @throws IOException if there are file's reading/writing issues
140160
* @throws InterruptedException if there is thread interruption while executing GhostScript.
@@ -145,12 +165,48 @@ public void runGhostScriptImageGeneration(String pdf, String outDir, String imag
145165
throw new IllegalArgumentException(
146166
IoExceptionMessage.CANNOT_OPEN_OUTPUT_DIRECTORY.replace("<filename>", pdf));
147167
}
168+
if (!validateImageFilePattern(image)) {
169+
throw new IllegalArgumentException("Invalid output image pattern: " + image);
170+
}
171+
if (!validatePageList(pageList)) {
172+
throw new IllegalArgumentException("Invalid page list: " + pageList);
173+
}
174+
String formattedPageList = (pageList == null) ? "" : "-sPageList=<pagelist>".replace("<pagelist>", pageList);
175+
176+
String replacementPdf = null;
177+
String replacementImagesDirectory = null;
178+
String[] temporaryOutputImages = null;
179+
try {
180+
replacementPdf = FileUtil.createTempCopy(pdf, TEMP_FILE_PREFIX, null);
181+
replacementImagesDirectory = FileUtil.createTempDirectory(TEMP_FILE_PREFIX);
182+
String currGsParams = MessageFormatUtil.format(GHOSTSCRIPT_PARAMS, formattedPageList,
183+
Paths.get(replacementImagesDirectory,
184+
TEMP_FILE_PREFIX + PAGE_NUMBER_PATTERN + "." + RENDERED_IMAGE_EXTENSION).toString(),
185+
replacementPdf);
186+
187+
if (!SystemUtil.runProcessAndWait(gsExec, currGsParams)) {
188+
temporaryOutputImages = FileUtil
189+
.listFilesInDirectory(replacementImagesDirectory, false);
190+
throw new GhostscriptExecutionException(
191+
IoExceptionMessage.GHOSTSCRIPT_FAILED.replace("<filename>", pdf));
192+
}
148193

149-
pageList = (pageList == null) ? "" : "-sPageList=<pagelist>".replace("<pagelist>", pageList);
150-
151-
String currGsParams = MessageFormatUtil.format(GHOSTSCRIPT_PARAMS, pageList, outDir + image, pdf);
152-
if (!SystemUtil.runProcessAndWait(gsExec, currGsParams)) {
153-
throw new GhostscriptExecutionException(IoExceptionMessage.GHOSTSCRIPT_FAILED.replace("<filename>", pdf));
194+
temporaryOutputImages = FileUtil
195+
.listFilesInDirectory(replacementImagesDirectory, false);
196+
if (null != temporaryOutputImages) {
197+
for (int i = 0; i < temporaryOutputImages.length; i++) {
198+
FileUtil.copy(temporaryOutputImages[i],
199+
Paths.get(
200+
outDir,
201+
image + "-" + formatImageNumber(i + 1) + "." + RENDERED_IMAGE_EXTENSION
202+
).toString());
203+
}
204+
}
205+
} finally {
206+
if (null != temporaryOutputImages) {
207+
FileUtil.removeFiles(temporaryOutputImages);
208+
}
209+
FileUtil.removeFiles(new String[] {replacementImagesDirectory, replacementPdf});
154210
}
155211
}
156212

@@ -168,4 +224,26 @@ public GhostscriptExecutionException(String msg) {
168224
super(msg);
169225
}
170226
}
227+
228+
static boolean validatePageList(String pageList) {
229+
return null == pageList
230+
|| PAGE_LIST_REGEX.matcher(pageList).matches();
231+
}
232+
233+
static boolean validateImageFilePattern(String imageFilePattern) {
234+
return null != imageFilePattern
235+
&& !imageFilePattern.trim().isEmpty()
236+
&& !imageFilePattern.contains("%");
237+
}
238+
239+
static String formatImageNumber(int pageNumber) {
240+
StringBuilder stringBuilder = new StringBuilder();
241+
int zeroFiller = pageNumber;
242+
while (0 == zeroFiller / 100) {
243+
stringBuilder.append('0');
244+
zeroFiller *= 10;
245+
}
246+
stringBuilder.append(pageNumber);
247+
return stringBuilder.toString();
248+
}
171249
}

io/src/main/java/com/itextpdf/io/util/ImageMagickHelper.java

Lines changed: 41 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@ This file is part of the iText (R) project.
4545

4646
import com.itextpdf.io.IoExceptionMessage;
4747

48+
import java.io.File;
4849
import java.io.IOException;
4950

5051
/**
@@ -67,6 +68,8 @@ public class ImageMagickHelper {
6768

6869
static final String MAGICK_COMPARE_KEYWORD = "ImageMagick Studio LLC";
6970

71+
private static final String TEMP_FILE_PREFIX = "itext_im_io_temp";
72+
7073
private String compareExec;
7174

7275
/**
@@ -113,9 +116,7 @@ public String getCliExecutionCommand() {
113116
* @param outImageFilePath Path to the output image file
114117
* @param cmpImageFilePath Path to the cmp image file
115118
* @param diffImageName Path to the difference output image file
116-
*
117119
* @return boolean result of comparing: true - images are visually equal
118-
*
119120
* @throws IOException if there are file's reading/writing issues
120121
* @throws InterruptedException if there is thread interruption while executing ImageMagick.
121122
*/
@@ -132,22 +133,51 @@ public boolean runImageMagickImageCompare(String outImageFilePath, String cmpIma
132133
* @param diffImageName Path to the difference output image file
133134
* @param fuzzValue String fuzziness value to compare images. Should be formatted as string with integer
134135
* or decimal number. Can be null, if it is not required to use fuzziness
135-
*
136136
* @return boolean result of comparing: true - images are visually equal
137-
*
138137
* @throws IOException if there are file's reading/writing issues
139138
* @throws InterruptedException if there is thread interruption while executing ImageMagick.
140139
*/
141140
public boolean runImageMagickImageCompare(String outImageFilePath, String cmpImageFilePath,
142141
String diffImageName, String fuzzValue) throws IOException, InterruptedException {
142+
if (!validateFuzziness(fuzzValue)) {
143+
throw new IllegalArgumentException("Invalid fuzziness value: " + fuzzValue);
144+
}
143145
fuzzValue = (fuzzValue == null) ? "" : " -metric AE -fuzz <fuzzValue>%".replace("<fuzzValue>", fuzzValue);
144146

145-
StringBuilder currCompareParams = new StringBuilder();
146-
currCompareParams
147-
.append(fuzzValue).append(" '")
148-
.append(outImageFilePath).append("' '")
149-
.append(cmpImageFilePath).append("' '")
150-
.append(diffImageName).append("'");
151-
return SystemUtil.runProcessAndWait(compareExec, currCompareParams.toString());
147+
String replacementOutFile = null;
148+
String replacementCmpFile = null;
149+
String replacementDiff = null;
150+
try {
151+
replacementOutFile = FileUtil.createTempCopy(outImageFilePath, TEMP_FILE_PREFIX, null);
152+
replacementCmpFile = FileUtil.createTempCopy(cmpImageFilePath, TEMP_FILE_PREFIX, null);
153+
154+
replacementDiff = FileUtil.createTempFile(TEMP_FILE_PREFIX, null).toString();
155+
String currCompareParams = fuzzValue + " '"
156+
+ replacementOutFile + "' '"
157+
+ replacementCmpFile + "' '"
158+
+ replacementDiff + "'";
159+
boolean result = SystemUtil.runProcessAndWait(compareExec, currCompareParams);
160+
161+
if (FileUtil.fileExists(replacementDiff)) {
162+
FileUtil.copy(replacementDiff, diffImageName);
163+
}
164+
return result;
165+
} finally {
166+
FileUtil.removeFiles(new String[] {replacementOutFile, replacementCmpFile, replacementDiff});
167+
}
168+
}
169+
170+
static boolean validateFuzziness(String fuzziness) {
171+
if (null == fuzziness) {
172+
return true;
173+
} else {
174+
try {
175+
return Double.parseDouble(fuzziness) >= 0;
176+
} catch (NumberFormatException e) {
177+
// In case of an exception the string could not be parsed to double,
178+
// therefore it is considered to be invalid.
179+
return false;
180+
}
181+
}
152182
}
153183
}

0 commit comments

Comments
 (0)