/* * Interactively orient and delete JPEG files in the specified directory. * The orientation is performed by changing the EXIF header; the image data * is not modified. * * Copyright (c) 2009, Diomidis Spinellis * All rights reserved. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * 1. Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * 2. Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * THIS SOFTWARE IS PROVIDED BY AUTHOR AND CONTRIBUTORS ``AS IS'' AND * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL AUTHOR OR CONTRIBUTORS BE LIABLE * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF * SUCH DAMAGE. * * $Id: jeo.pde,v 1.7 2009/06/15 03:13:19 dds Exp $ * */ import javax.swing.JFileChooser; import javax.swing.UIManager; import java.io.*; import java.util.Arrays; File[] files; int fileIndex = 0; int moveDirection = 1; JpegFile jpegFile; class JpegFileFormatException extends Exception { public JpegFileFormatException(String s) { super(s); } } /** * A Class that can read binary data in native (big or little-endian) format. * By default the native format is big-endian. * @see RandomAccessFile */ class NativeRandomAccessFile extends RandomAccessFile { private boolean bigEndian = true; /** Create a new object for the specified file and access mode. */ NativeRandomAccessFile(File file, String mode) throws FileNotFoundException { super(file, mode); } /** Create a new object for the specified file and access mode. */ NativeRandomAccessFile(File file, String mode, boolean isBigEndian) throws FileNotFoundException { super(file, mode); bigEndian = isBigEndian; } /** Set native format to little-endian. */ void setLittleEndian() { bigEndian = false; } /** Read size bytes in little-endian ordering */ private long readLittleEndian(int size) throws IOException { // LSB stored first byte[] buff = new byte[size]; read(buff); long val = 0; for (int i = size - 1; i >= 0; i--) { val <<= 8; val |= buff[i]; } return val; } /** Read an int in native byte order. */ int readNativeInt() throws IOException { if (bigEndian) return readInt(); else { return (int)readLittleEndian(4); } } /** Read a short in native byte order. */ short readNativeShort() throws IOException { if (bigEndian) return readShort(); else { return (short)readLittleEndian(2); } } /** Write a short in native byte order. */ void writeNativeShort(short s) throws IOException { if (bigEndian) writeShort(s); else { writeByte(s & 0xff); writeByte(s >>> 8); } } } /** * A Jpeg image file with an Exif orientation. * See http://park2.wakwak.com/~tsuruzoh/Computer/Digicams/exif-e.html * http://gvsoft.homedns.org/exif/Exif-explanation.html */ class JpegFile { /** The underlying image file. */ private File imageFile; /** Offset of the EXIF orientation data. */ private long orientationOffset; /** True if file is little-endian. */ private boolean isBigEndian; /** Orientation of the image stored in the file. */ private int initialOrientation; /** Current orientation. */ private int currentOrientation; /** The image in the file. */ PImage img; // Return the EXIF offset of the selected file JpegFile(File file) throws IOException, JpegFileFormatException { imageFile = file; NativeRandomAccessFile f = new NativeRandomAccessFile(file, "r"); try { // JPEG header (SOI) byte jpegSoi[] = {(byte)0xFF, (byte)0xD8}; byte[] buff = new byte[jpegSoi.length]; f.read(buff); if (!Arrays.equals(jpegSoi, buff)) throw new JpegFileFormatException("Not a JPEG file"); // Locate APP1 header indicating Exif int appSize; for (;;) { byte exif[] = {(byte)0xFF, (byte)0xE1}; byte jpegEoi[] = {(byte)0xFF, (byte)0xD9}; byte[] marker = new byte[exif.length]; f.read(marker); appSize = (f.readShort() & 0xffff); println("APP size = " + appSize); if (Arrays.equals(marker, exif)) break; if (Arrays.equals(marker, jpegEoi)) throw new JpegFileFormatException("No APP1 block found"); f.skipBytes(appSize - 2); } // APP1 data size int remainingBytes = appSize - 2; // Exif header byte exif[] = {'E', 'x', 'i', 'f', 0, 0}; if (remainingBytes < exif.length) throw new JpegFileFormatException("Missing Exif header"); buff = new byte[exif.length]; f.read(buff); if (!Arrays.equals(buff, exif)) throw new JpegFileFormatException("Not an Exif header"); remainingBytes -= exif.length; // TIFF header final int TiffHeaderSize = 2 + 2 + 4; if (remainingBytes < TiffHeaderSize) throw new JpegFileFormatException("Missing TIFF header"); short endian = f.readShort(); switch (endian) { case 0x4d4d: // MM: Motorolla isBigEndian = true; break; case 0x4949: // II: Intel isBigEndian = false; f.setLittleEndian(); break; default: throw new JpegFileFormatException("Invalid byte order tag"); } short tag = f.readNativeShort(); if (tag != 0x2a) throw new JpegFileFormatException("Invalid TIFF 0x2a tag: " + tag); int IfdOffset = f.readNativeInt(); if (IfdOffset > remainingBytes) throw new JpegFileFormatException("IFD offset past end of APP1 segment: " + IfdOffset); remainingBytes -= TiffHeaderSize; // IFD: Image File Directory f.skipBytes(IfdOffset - TiffHeaderSize); short ifdEntries = f.readNativeShort(); println("IFD entries = " + ifdEntries); remainingBytes -= 2; /* * IFD entries. Each entry is 12 bytes * short entryType * short dataFormat * int components * int data (or offset if components * sizeof(data) > 4) */ if (remainingBytes < ifdEntries * 12) throw new JpegFileFormatException("IFD entries extend past APP1 area"); for (int i = 0; i < ifdEntries; i++) { if (f.readNativeShort() == 0x0112) { // Orientation tag short dataType = f.readNativeShort(); if (dataType != 3) throw new JpegFileFormatException("Invalid orientation type: " + dataType); int nElements = f.readNativeInt(); if (nElements != 1) throw new JpegFileFormatException("Multiple orientation elements: " + nElements); orientationOffset = f.getFilePointer(); initialOrientation = currentOrientation = f.readNativeShort(); switch (initialOrientation) { case 1: case 8: case 3: case 6: break; default: throw new JpegFileFormatException("Unsupported orientation: " + initialOrientation); } println("Orientation: " + initialOrientation); img = loadImage(file.getAbsolutePath(), "jpg"); if (img == null || img.width == -1) throw new JpegFileFormatException("Unable to load image"); return; } f.skipBytes(10); } throw new JpegFileFormatException("No orientation tag found"); } finally { f.close(); } } /** Draw the specified image without distorting it. */ private void draw(PImage img) { double sx = (double)width / img.width; double sy = (double)height / img.height; float scale = (float)Math.min(sx, sy); println("Scale = " + scale); background(0); image(img, (width - img.width * scale) / 2, (height - img.height * scale) / 2, img.width * scale, img.height * scale); } /* * Orientation values; these refer to which part of a scene is stored in (0, 0). * * Value | 0th Row | 0th Column * ------+-------------+----------- * 1 | top | left side * 2 | top | rigth side * 3 | bottom | rigth side * 4 | bottom | left side * 5 | left side | top * 6 | right side | top * 7 | right side | bottom * 8 | left side | bottom * * 1 2 3 4 5 6 7 8 * * 888888 888888 88 88 8888888888 88 88 8888888888 * 88 88 88 88 88 88 88 88 88 88 88 88 * 8888 8888 8888 8888 88 8888888888 8888888888 88 * 88 88 88 88 * 88 88 888888 888888 * */ /** Draw the image in its current orientation. */ public void draw() { println("Draw with orientation " + currentOrientation); if (currentOrientation == 1) { draw(img); return; } PImage img2; switch (currentOrientation) { case 3: img2 = createImage(img.width, img.height, RGB); break; case 6: case 8: img2 = createImage(img.height, img.width, RGB); break; default: throw new IllegalStateException(); } img.loadPixels(); img2.loadPixels(); // Let's hope the compiler or JVM unroll this monstrocity. int x2, y2; for (int x = 0; x < img.width; x++) for (int y = 0; y < img.height; y++) { switch (currentOrientation) { case 8: x2 = y; y2 = img2.height - x - 1; break; case 3: x2 = img2.width - x - 1; y2 = img2.height - y - 1; break; case 6: x2 = img2.width - y - 1; y2 = x; break; default: throw new IllegalStateException(); } img2.pixels[y2 * img2.width + x2] = img.pixels[y * img.width + x]; } img2.updatePixels(); draw(img2); } /** Change the image's orientation by the specified amount. * +ve amount is cw */ public void orient(int i) { while (i < 0) i += 4; for (; i > 0; i--) switch (currentOrientation) { case 1: currentOrientation = 6; break; case 8: currentOrientation = 1; break; case 3: currentOrientation = 8; break; case 6: currentOrientation = 3; break; } } /** Commit to the file the orientation changes made. */ public void commitChanges() { if (currentOrientation == initialOrientation) return; NativeRandomAccessFile f = null; try { long mtime = imageFile.lastModified(); f = new NativeRandomAccessFile(imageFile, "rw", isBigEndian); f.seek(orientationOffset); f.writeNativeShort((short)currentOrientation); f.close(); f = null; imageFile.setLastModified(mtime); } catch (Exception e) { println("Unable to set orientation: " + e); } finally { try { if (f != null) f.close(); } catch (IOException e) { println("Error closing file: " + e); } } } /** Delete the underlying image file. */ public void delete() { try { imageFile.delete(); } catch (Exception e) { println("Unable to delete the file: " + e); } } } void setup() { // Set native L&F try { UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName()); } catch (Exception e) { System.err.println("Error loading L&F: " + e); } // Choose directory JFileChooser chooser = new JFileChooser(); chooser.setApproveButtonText("Select Directory"); chooser.setFileSelectionMode(JFileChooser.DIRECTORIES_ONLY); chooser.showOpenDialog(null); File dir = chooser.getSelectedFile(); if (dir == null) System.exit(0); files = dir.listFiles(); if (files.length == 0) System.exit(1); size(640, 480); getValidImage(); noLoop(); } // Move to the next image the user specified void nextImage() { fileIndex = fileIndex + moveDirection; if (fileIndex == -1) fileIndex = files.length - 1; else if (fileIndex == files.length) fileIndex = 0; } // Set file to the first available valid image void getValidImage() { int start = fileIndex; for (;;) { println("fileIndex: " + fileIndex + "; drawing: " + files[fileIndex].getAbsolutePath()); try { jpegFile = new JpegFile(files[fileIndex]); frame.setTitle("jeo - " + files[fileIndex].getName()); break; } catch (Exception e) { println("Error processing file " + files[fileIndex] + ": " + e); nextImage(); if (fileIndex == start) { println("No images to display"); System.exit(1); } } } } void draw() { jpegFile.draw(); } void keyPressed() { println(keyCode + " pressed"); switch (key) { case ESC: jpegFile.commitChanges(); System.exit(0); case DELETE: jpegFile.delete(); moveDirection = 1; nextImage(); getValidImage(); break; case CODED: switch (keyCode) { case UP: jpegFile.commitChanges(); moveDirection = -1; nextImage(); getValidImage(); break; case DOWN: jpegFile.commitChanges(); moveDirection = 1; nextImage(); getValidImage(); break; case LEFT: jpegFile.orient(-1); break; case RIGHT: jpegFile.orient(1); break; } break; } redraw(); }