blob: c8817d4f6811052c75532b78242e03e2b8c34e29 [file] [log] [blame]
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.apache.clerezza.utils.imagemagick;
import java.awt.Color;
import java.awt.image.BufferedImage;
import java.io.BufferedReader;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.UnsupportedEncodingException;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import javax.imageio.ImageIO;
import org.apache.clerezza.rdf.core.TripleCollection;
import org.apache.clerezza.rdf.core.serializedform.Parser;
import org.apache.clerezza.rdf.core.serializedform.Serializer;
import org.apache.clerezza.utils.imageprocessing.ImageProcessor;
import org.apache.clerezza.utils.imageprocessing.ImageReaderService;
import org.apache.clerezza.utils.imageprocessing.metadataprocessing.ExifTagDataSet;
import org.apache.clerezza.utils.imageprocessing.metadataprocessing.IptcDataSet;
import org.apache.clerezza.utils.imageprocessing.metadataprocessing.MetaData;
import org.apache.clerezza.utils.imageprocessing.metadataprocessing.MetaDataProcessor;
import org.apache.felix.scr.annotations.Component;
import org.apache.felix.scr.annotations.Properties;
import org.apache.felix.scr.annotations.Property;
import org.apache.felix.scr.annotations.Reference;
import org.apache.felix.scr.annotations.Service;
import org.osgi.service.component.ComponentContext;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* This class implements interfaces that execute system calls to imageMagick.
*
* <p>
* Note: ImageMagick must be installed in the machine this service is running
* on. ImageMagick is free open-source software to edit bitmap images on most
* platforms. More information and binaries as well as source code can be found
* at: <a href='http://www.imagemagick.org/'>http://www.imagemagick.org/</a>.
* </p>
*
* @author tio, hasan, daniel
*/
@Component(metatype=true)
@Properties({
@Property(name="convert", value="convert", description="Specifies the ImageMagick convert command."),
@Property(name="identify", value="identify", description="Specifies the ImageMagick identify command."),
@Property(name="release_number", intValue=6, description="Specifies ImageMagick release number (Syntax: release.version.majorRevision-minorRevision)."),
@Property(name="version_number", intValue=5, description="Specifies ImageMagick version number (Syntax: release.version.majorRevision-minorRevision)."),
@Property(name="major_release_number", intValue=2, description="Specifies ImageMagick major revision number (Syntax: release.version.majorRevision-minorRevision)."),
@Property(name="minor_release_number", intValue=10, description="Specifies ImageMagick minor revision number (Syntax: release.version.majorRevision-minorRevision)."),
@Property(name="service.ranking", value="100")
})
@Service({
ImageProcessor.class,
MetaDataProcessor.class
})
public class ImageMagickProvider extends ImageProcessor implements MetaDataProcessor {
private String convert = "convert";
private String identify = "identify";
private int imagemagickRelease = 6;
private int imagemagickVersion = 5;
private int imagemagickRevisionMajorNumber = 2;
private int imagemagickRevisionMinorNumber = 10;
@Reference
private Serializer serializer;
@Reference
private ImageReaderService imageReaderService;
private final Logger logger = LoggerFactory.getLogger(getClass());
protected void activate(ComponentContext cCtx) {
if (cCtx != null) {
convert = (String) cCtx.getProperties().get("convert");
identify = (String) cCtx.getProperties().get("identify");
imagemagickRelease = (Integer) cCtx.getProperties().
get("release_number");
imagemagickVersion = (Integer) cCtx.getProperties().
get("version_number");
imagemagickRevisionMajorNumber = (Integer) cCtx.getProperties().
get("major_release_number");
imagemagickRevisionMinorNumber = (Integer) cCtx.getProperties().
get("minor_release_number");
}
checkImageMagickInstallation();
logger.info("ImageMagickProvider activated");
}
/**
* Default Constructor
*/
public ImageMagickProvider() {
this.serializer = Serializer.getInstance();
}
/**
* This method checks if ImageMagick is correctly installed.
* You can configure the required version of ImageMagick
* via service properties.
*
* @throws RuntimeException when no ImageMagick installation
* is found or the version is too
* low.
*/
protected void checkImageMagickInstallation() throws RuntimeException {
boolean ok = true;
try {
List<String> command = new ArrayList<String>();
command.add(identify);
command.add("--version");
Process proc = execCommand(command);
BufferedReader br = new BufferedReader(
new InputStreamReader(proc.getInputStream()));
String output = br.readLine();
br.close();
ok = checkImageMagickVersion(output, imagemagickRelease,
imagemagickVersion,
imagemagickRevisionMajorNumber,
imagemagickRevisionMinorNumber);
command.clear();
command.add(convert);
command.add("--version");
proc = execCommand(command);
br = new BufferedReader(new InputStreamReader(proc.getInputStream()));
output = br.readLine();
br.close();
if(output!=null && !output.contains("Version: ImageMagick")) {
ok = false;
}
} catch (InterruptedException ex) {
Thread.currentThread().interrupt();
logger.warn("ImageMagick version check has been interrupted. " +
"Assuming correct version.");
} catch (IOException ex) {
//this occurs when the commands are miising
ok = false;
} catch (NullPointerException ex) {
//can occur when output is empty (e.g. imagemagick prints
//only error messages which go to stderror)
ok = false;
}
if(!ok) {
logger.error("ImageMagick version can not be verified. " +
"Please make sure you have ImageMagick (>=" +
imagemagickRelease + "." + imagemagickVersion + "." +
imagemagickRevisionMajorNumber + "-" +
imagemagickRevisionMinorNumber +
") installed correctly");
throw new RuntimeException("ImageMagick not installed correctly.");
}
}
private boolean checkImageMagickVersion(String str, int release, int version,
int revision_major_number, int revision_minor_number) {
Pattern pattern = Pattern.compile("(\\d+\\.){2}\\d+-\\d+");
Matcher matcher = pattern.matcher(str);
boolean error = false;
if (matcher.find()) {
String versionString = matcher.group();
String[] versionParts = versionString.split("\\.");
if (Integer.parseInt(versionParts[0]) < release) {
error = true;
} else if (Integer.parseInt(versionParts[0]) == release) {
if (Integer.parseInt(versionParts[1]) < version) {
error = true;
} else if (Integer.parseInt(versionParts[1]) == version) {
String[] revisionParts = versionParts[2].split("-");
if (Integer.parseInt(revisionParts[0]) < revision_major_number) {
error = true;
} else if (Integer.parseInt(revisionParts[0]) == revision_major_number) {
if (Integer.parseInt(revisionParts[1]) < revision_minor_number) {
error = true;
}
}
}
}
return !error;
} else {
return false;
}
}
@Override
public BufferedImage makeImageTranslucent(BufferedImage image,
float translucency) {
throw new UnsupportedOperationException("Not supported yet.");
}
@Override
public BufferedImage makeColorTransparent(BufferedImage image, Color color) {
throw new UnsupportedOperationException("Not supported yet.");
}
@Override
public BufferedImage flip(BufferedImage image, int direction) {
List<String> command = new ArrayList<String>(10);
command.add(convert);
if (direction == 0) {
command.add("-flop");
} else {
command.add("-flip");
}
return processImage(command, 100, image);
}
@Override
public BufferedImage rotate(BufferedImage image, int angle) {
List<String> command = new ArrayList<String>(10);
command.add(convert);
command.add("-rotate");
command.add("" + angle);
return processImage(command, 100, image);
}
@Override
public BufferedImage resize(BufferedImage image, int newWidth, int newHeight) {
List<String> command = new ArrayList<String>(10);
command.add(convert);
command.add("-geometry");
command.add(newWidth + "x" + newHeight + "!");
return processImage(command, 100, image);
}
@Override
public BufferedImage resizeProportional(BufferedImage image, int newWidth,
int newHeight) {
List<String> command = new ArrayList<String>(10);
command.add(convert);
command.add("-geometry");
if (newWidth != 0) {
command.add("" + newWidth);
} else {
if (newHeight != 0) {
command.add("x" + newHeight);
} else {
return image;
}
}
return processImage(command, 100, image);
}
@Override
public BufferedImage resizeRelative(BufferedImage image,
float resizeFactorWidth, float resizeFactorHeight) {
List<String> command = new ArrayList<String>(10);
command.add(convert);
command.add("-geometry");
command.add((100 * resizeFactorWidth) + "%x"
+ (100 * resizeFactorHeight) + "%");
return processImage(command, 100, image);
}
@Override
public BufferedImage resizeRelativeProportional(BufferedImage image,
float resizeFactor) {
List<String> command = new ArrayList<String>(10);
command.add(convert);
command.add("-geometry");
command.add(100 * resizeFactor + "%");
return processImage(command, 100, image);
}
private BufferedImage crop(BufferedImage image, int newWidth, int newHeight) {
List<String> command = new ArrayList<String>(10);
command.add(convert);
command.add("-crop");
command.add(newWidth + "x" + newHeight);
return processImage(command, 100, image);
}
@Override
public MetaData<IptcDataSet> extractIPTC(byte[] mediaFile) {
List<String> command = new ArrayList<String>(3);
command.add(convert);
command.add("-");
command.add("IPTCTEXT:-");
try {
List<String> resultLines = inputStreamToStringList(execCommand(
command, mediaFile).getInputStream());
MetaData<IptcDataSet> metaData = new MetaData<IptcDataSet>();
for (String line : resultLines) {
// ImageMagick specific output processing
// output has the form of:
// recordNumber#dataSetNumber#recordName="value"
// recordNumber is always 2 as imagemagick only reads the record 2
// values.
try {
int pos;
int dataSetNumber;
boolean hasPropertyName = true;
if ((pos = line.indexOf('#', 2)) > -1 && pos < 6) {
//output contains a property name (normal situation)
dataSetNumber = Integer.parseInt(line.substring(2, pos));
} else {
// output doesn't contains a property name (e.g. record
// version: 2#0="value")
pos = line.indexOf('=');
hasPropertyName = false;
dataSetNumber = Integer.parseInt(line.substring(2, pos));
}
if (hasPropertyName) {
//jump to the value part if data set contains recordName.
pos = line.indexOf('=');
}
metaData.add(new IptcDataSet(2, dataSetNumber,
line.substring(pos + 2, line.length() - 1)));
} catch (NumberFormatException ex) {
logger.info(
"Could not parse IPTC record number for DataSet: {}",
line);
// format of the line is corrupt. nothing can be done about it,
// we try the next line
continue;
}
}
return metaData;
} catch (IOException ex) {
logger.warn("IOException while trying to execute {}", command);
} catch (InterruptedException ex) {
logger.warn("ImageMagick has been interrupted");
Thread.currentThread().interrupt();
}
return null;
}
@Override
public MetaData<ExifTagDataSet> extractEXIF(byte[] mediaFile) {
List<String> command = new ArrayList<String>(4);
command.add(identify);
command.add("-format");
command.add("%[exif:*]");
command.add("-");
try {
List<String> resultLines = inputStreamToStringList(execCommand(
command, mediaFile).getInputStream());
MetaData<ExifTagDataSet> metaData = new MetaData<ExifTagDataSet>();
for (String line : resultLines) {
// ImageMagick specific output processing
// output has the form of: exif:tagName=value
line = line.trim();
if (line.length() > 5) {
// line not empty (contains more than "exif:")
// we don't need the "exif:" part
String[] sa = line.substring(5).split("=");
// sa[0] contains the tagName, sa[1] contains the value
try {
try {
// special handling of some wrongly named exif tags
if (sa[0].equals("ExifImageLength")) {
sa[0] = "ImageLength";
} else if (sa[0].equals("ExifImageWidth")) {
sa[0] = "ImageWidth";
}
metaData.add(new ExifTagDataSet(sa[0], sa[1]));
} catch (ArrayIndexOutOfBoundsException ex) {
// the data set has no value or contains no equal (=)
// sign.
if (sa.length == 1) {
// assume empty value
metaData.add(new ExifTagDataSet(sa[0], ""));
} else {
logger.info("Could not identify EXIF tag in: {}",
line);
continue;
}
}
} catch (NoSuchFieldException ex) {
logger.info("Could not identify EXIF tagName in: {}", line);
continue;
}
}
}
return metaData;
} catch (IOException ex) {
logger.warn("IOException while trying to execute {}", command);
} catch (InterruptedException ex) {
logger.warn("ImageMagick has been interrupted");
Thread.currentThread().interrupt();
}
return null;
}
@Override
public TripleCollection extractXMP(byte[] mediaFile) {
List<String> command = new ArrayList<String>(3);
command.add(convert);
command.add("-");
command.add("XMP:-");
try {
Iterator<String> it = inputStreamToStringList(
execCommand(command, mediaFile).getInputStream()).iterator();
StringBuilder sb = new StringBuilder();
while (it.hasNext()) {
//ImageMagick specific output processing
String line = it.next().trim();
if(line.equals("")) {
continue;
}
if(line.startsWith("<?") || line.startsWith("<x:") ||
line.startsWith("</x:")) {
continue;
}
sb.append(" " + line);
}
ByteArrayInputStream bais = new ByteArrayInputStream(sb.toString()
.getBytes());
return Parser.getInstance().parse(bais, "application/rdf+xml");
} catch (IOException ex) {
logger.warn("IOException while trying to execute {}", command);
} catch (InterruptedException ex) {
logger.warn("ImageMagick has been interrupted");
Thread.currentThread().interrupt();
}
return null;
}
@Override
public byte[] writeXMP(byte[] mediaFile,
TripleCollection metaData) {
synchronized(this) {
//I use files because i couldn't find a way to supply
//3 streams as arguments to imagemagick
//using files requires this block to be synchronized
//possibly this can be solved by using one of the java APIs
//for imagemagick
File profile = new File("tmpFile.rdf");
File inFile = new File("tmpFile2.jpg");
File outFile = new File("tmpFile3.jpg");
List<String> command = new ArrayList<String>(5);
command.add(convert);
command.add("-profile");
command.add("XMP:" + profile.getName());
command.add(inFile.getName());
command.add(outFile.getName());
try {
FileOutputStream fos = new FileOutputStream(profile);
//write XMP header
fos.write("<?xpacket begin=\"\uFEFF\" id=\"W5M0MpCehiHzreSzNTczkc9d\"?>\n".getBytes());
fos.write("<x:xmpmeta xmlns:x=\"adobe:ns:meta/\">\n".getBytes());
serializer.serialize(fos, metaData,"application/rdf+xml");
fos.write("\n</x:xmpmeta>".getBytes());
fos.write("\n<?xpacket end=\"w\"?>".getBytes());
fos.close();
FileOutputStream fos2 = new FileOutputStream(inFile);
fos2.write(mediaFile);
fos2.close();
execCommand(command);
FileInputStream fis = new FileInputStream(outFile);
ByteArrayOutputStream baos = new ByteArrayOutputStream();
int ch;
while((ch = fis.read()) != -1) {
baos.write(ch);
}
fis.close();
return baos.toByteArray();
} catch (IOException ex) {
logger.warn("IOException while trying to execute {}", command);
} catch (InterruptedException ex) {
logger.warn("ImageMagick has been interrupted");
Thread.currentThread().interrupt();
} finally {
profile.delete();
inFile.delete();
outFile.delete();
}
}
return null;
}
private BufferedImage processImage(List<String> command, int quality,
BufferedImage image) {
command.add("-quality");
command.add(String.valueOf(quality));
command.add("-");
command.add("-");
ByteArrayOutputStream baos = new ByteArrayOutputStream();
try {
if (ImageIO.write(image, "png", baos) == false) {
logger.warn("Cannot write image to output stream");
return null;
}
return imageReaderService.getBufferedImage(execCommand(
command, baos.toByteArray()).getInputStream());
} catch (InterruptedException ex) {
logger.warn("ImageMagick has been interrupted");
Thread.currentThread().interrupt();
return null;
} catch (IOException ex) {
logger.warn("IOException while trying to execute {}", command);
return null;
}
}
private Process execCommand(List<String> command,
byte[]... inputData) throws IOException,
InterruptedException {
logger.info("Trying to execute command {}", command);
Process proc = new ProcessBuilder(command).start();
for(byte[] bytes : inputData) {
proc.getOutputStream().write(bytes);
}
proc.getOutputStream().close();
if (proc.waitFor() > 0) {
//an error occurred
StringBuilder sb = new StringBuilder();
Iterator<String> it;
try {
it = inputStreamToStringList(
proc.getErrorStream()).iterator();
while (it.hasNext()) {
sb.append(it.next());
sb.append("\n");
}
} catch (IOException ex) {
throw new RuntimeException(ex);
} finally {
logger.warn("Error in ImageMagick while trying to execute {}. Error: {} ",
command, sb.toString());
}
}
return proc;
}
private List<String> inputStreamToStringList(InputStream is)
throws UnsupportedEncodingException, IOException {
BufferedReader br = new BufferedReader(new InputStreamReader(is,
"utf-8"));
List<String> sl = new ArrayList<String>();
String line;
while ((line = br.readLine()) != null) {
sl.add(line);
}
br.close();
return sl;
}
}