/* * Copyright 2002-2012 Drew Noakes * * Licensed 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. * * More information about this project is available at: * * http://drewnoakes.com/code/exif/ * http://code.google.com/p/metadata-extractor/ */ package com.almalence.util.exifreader.imaging; import com.almalence.util.exifreader.imaging.jpeg.JpegMetadataReader; import com.almalence.util.exifreader.lang.annotations.NotNull; import com.almalence.util.exifreader.lang.annotations.Nullable; import com.almalence.util.exifreader.metadata.Directory; import com.almalence.util.exifreader.metadata.Metadata; import com.almalence.util.exifreader.metadata.MetadataException; import com.almalence.util.exifreader.metadata.Tag; import com.almalence.util.exifreader.metadata.exif.ExifIFD0Directory; import java.io.BufferedInputStream; import java.io.File; import java.io.FileInputStream; import java.io.IOException; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; /** * Obtains metadata from all supported file formats, including JPEG, RAW (NEF/CRw/CR2) and TIFF. * If the caller knows their file to be of a particular type, they may prefer to use the dedicated MetadataReader * directly (<code>JpegMetadataReader</code> for Jpeg files, or <code>TiffMetadataReader</code> for TIFF and RAW files). * The dedicated readers offer a very slight performance improvement, though for most scenarios it is simpler, * more convenient and more robust to use this class. * * @author Drew Noakes http://drewnoakes.com */ public class ImageMetadataReader { private static final int JPEG_FILE_MAGIC_NUMBER = 0xFFD8; private static final int MOTOROLA_TIFF_MAGIC_NUMBER = 0x4D4D; // "MM" private static final int INTEL_TIFF_MAGIC_NUMBER = 0x4949; // "II" private static final int PSD_MAGIC_NUMBER = 0x3842; // "8B" note that the full magic number is 8BPS /** * Reads metadata from an input stream. The file inputStream examined to determine its type and consequently the * appropriate method to extract the data, though this inputStream transparent to the caller. * * @param inputStream a stream from which the image data may be read. The stream must be positioned at the * beginning of the image data. * @return a populated Metadata object containing directories of tags with values and any processing errors. * @throws ImageProcessingException for general processing errors. */ @NotNull public static Metadata readMetadata(@NotNull BufferedInputStream inputStream, boolean waitForBytes) throws ImageProcessingException, IOException { int magicNumber = readMagicNumber(inputStream); return readMetadata(inputStream, null, magicNumber, waitForBytes); } /** * Reads metadata from a file. The file is examined to determine its type and consequently the appropriate * method to extract the data, though this is transparent to the caller. * * @param file a file from which the image data may be read. * @return a populated Metadata object containing directories of tags with values and any processing errors. * @throws ImageProcessingException for general processing errors. */ @NotNull public static Metadata readMetadata(@NotNull File file) throws ImageProcessingException, IOException { BufferedInputStream inputStream = new BufferedInputStream(new FileInputStream(file)); int magicNumber; try { magicNumber = readMagicNumber(inputStream); } finally { inputStream.close(); } return readMetadata(null, file, magicNumber, false); } @NotNull private static Metadata readMetadata(@Nullable BufferedInputStream inputStream, @Nullable File file, int magicNumber, boolean waitForBytes) throws ImageProcessingException, IOException { assert(file!=null ^ inputStream!=null); // This covers all JPEG files if ((magicNumber & JPEG_FILE_MAGIC_NUMBER) == JPEG_FILE_MAGIC_NUMBER) { if (inputStream != null) return JpegMetadataReader.readMetadata(inputStream, waitForBytes); else return JpegMetadataReader.readMetadata(file); } throw new ImageProcessingException("File format is not supported"); } private static int readMagicNumber(@NotNull BufferedInputStream inputStream) throws IOException { inputStream.mark(2); int magicNumber = inputStream.read() << 8 | inputStream.read(); inputStream.reset(); return magicNumber; } private ImageMetadataReader() throws Exception { throw new Exception("Not intended for instantiation"); } /** * An application entry point. Takes the name of one or more files as arguments and prints the contents of all * metadata directories to System.out. If <code>/thumb</code> is passed, then any thumbnail data will be * written to a file with name of the input file having '.thumb.jpg' appended. * * @param args the command line arguments */ public static void main(@NotNull String[] args) throws MetadataException, IOException { Collection<String> argList = new ArrayList<String>(Arrays.asList(args)); boolean thumbRequested = argList.remove("/thumb"); boolean wikiFormat = argList.remove("/wiki"); if (argList.size() < 1) { System.out.println("Usage: java -jar metadata-extractor-a.b.c.jar <filename> [<filename>] [/thumb] [/wiki]"); System.exit(1); } for (String filePath : argList) { long startTime = System.nanoTime(); File file = new File(filePath); if (!wikiFormat && argList.size()>1) System.out.println("***** PROCESSING: " + filePath); Metadata metadata = null; try { metadata = ImageMetadataReader.readMetadata(file); } catch (Exception e) { e.printStackTrace(System.err); System.exit(1); } long took = System.nanoTime() - startTime; if (!wikiFormat) System.out.println("Processed " + (file.length()/(1024d*1024)) + "MB file in " + (took / 1000000d) + "ms"); if (wikiFormat) { String fileName = file.getName(); String urlName = fileName.replace(" ", "%20"); // How to do this using framework? ExifIFD0Directory exifIFD0Directory = metadata.getOrCreateDirectory(ExifIFD0Directory.class); String make = escapeForWiki(exifIFD0Directory.getString(ExifIFD0Directory.TAG_MAKE)); String model = escapeForWiki(exifIFD0Directory.getString(ExifIFD0Directory.TAG_MODEL)); System.out.println(); System.out.println("-----"); System.out.println(); System.out.printf("= %s - %s =%n", make, model); System.out.println(); System.out.printf("<a href=\"http://metadata-extractor.googlecode.com/svn/sample-images/%s\">%n", urlName); System.out.printf("<img src=\"http://metadata-extractor.googlecode.com/svn/sample-images/%s\" width=\"300\"/><br/>%n", urlName); System.out.println(fileName); System.out.println("</a>"); System.out.println(); System.out.println("|| *Directory* || *Tag Id* || *Tag Name* || *Tag Description* ||"); } // iterate over the metadata and print to System.out for (Directory directory : metadata.getDirectories()) { for (Tag tag : directory.getTags()) { String tagName = tag.getTagName(); String directoryName = directory.getName(); String description = tag.getDescription(); // truncate the description if it's too long if (description != null && description.length() > 1024) { description = description.substring(0, 1024) + "..."; } if (wikiFormat) { System.out.printf("||%s||0x%s||%s||%s||%n", escapeForWiki(directoryName), Integer.toHexString(tag.getTagType()), escapeForWiki(tagName), escapeForWiki(description)); } else { System.out.printf("[%s] %s = %s%n", directoryName, tagName, description); } } // print out any errors for (String error : directory.getErrors()) System.err.println("ERROR: " + error); } } } @Nullable private static String escapeForWiki(@Nullable String text) { if (text==null) return null; text = text.replaceAll("(\\W|^)(([A-Z][a-z0-9]+){2,})", "$1!$2"); if (text!=null && text.length() > 120) text = text.substring(0, 120) + "..."; if (text != null) text = text.replace("[", "`[`").replace("]", "`]`").replace("<", "`<`").replace(">", "`>`"); return text; } }