| /* |
| * 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; |
| } |
| } |