+++ /dev/null
-# MeWiz - Media Wizard
-
-A Java/Maven project - a Swiss army knife of various tools for audio, video, and photo editing/conversion.
-
-## Project Structure
-
-```
-src/main/java/eu/svjatoslav/meviz/
-├── Main.java # Entry point, module dispatcher
-├── Module.java # Interface for all modules
-├── encoder/ # Media encoding/conversion module
-├── htmlindexer/ # HTML gallery generator module
-├── renamer/ # Batch file rename module
-├── replace/ # Text replacement module
-└── bomremove/ # BOM stripping module
-```
-
-## Build Commands
-
-```bash
-# Clean build
-mvn clean package --settings .mvn/custom-settings.xml
-
-# Build without custom settings (may fail for some dependencies)
-mvn clean package
-
-# Run tests (if any)
-mvn test
-```
-
-## Running
-
-```bash
-# After building, run the JAR directly
-java -cp target/meviz-*-jar-with-dependencies.jar eu.svjatoslav.meviz.Main
-
-# Or use the convenience script (requires installation)
-meviz <command>
-
-# Available commands:
-# - index : Generate/update HTML image gallery
-# - rename : Batch file rename
-# - replace : Text replacement
-# - encode : Media encoding/conversion
-# - bomremove : Strip BOM from files
-# - help : Show help
-```
-
-## Dependencies
-
-- Java 8+
-- Maven
-- External tools (for full functionality):
- - `lame` - MP3 encoding
- - `vorbis-tools` - OGG audio
- - `imagemagick` - Image processing
- - `ffmpeg` - Video/audio conversion
-
-## Code Style
-
-- Standard Java conventions
-- Package prefix: `eu.svjatoslav.meviz`
-- Each module implements the `Module` interface with methods:
- - `getModuleCommand()` - CLI command name
- - `getDescription()` - Brief description
- - `run(String[] args)` - Execute module
- - `showCommandlineHelp()` - Display help
-
-## Installation
-
-See `install` script for system-wide installation (requires sudo).
--- /dev/null
+:PROPERTIES:
+:ID: 17c10717-ba3c-4f13-a18b-6b134a0efa86
+:END:
+
+#+TITLE: AGENTS — MeWiz (Media Wizard)
+#+SUBTITLE: Guidelines for AI coding agents working in this repository
+#+LANGUAGE: en
+
+* Overview
+:PROPERTIES:
+:ID: 932415dc-bfcc-4bc7-8d1e-4b85916f35dd
+:END:
+
+A Java/Maven project — a Swiss army knife of various tools for audio,
+video, and photo editing/conversion. License: CC0 (see ~COPYING~).
+
+* Project Structure
+:PROPERTIES:
+:ID: 3b8b4982-c11d-464b-8a90-551e5982c9e8
+:END:
+
+#+begin_example
+src/main/java/eu/svjatoslav/meviz/
+├── Main.java # Entry point, module dispatcher
+├── Module.java # Interface for all modules
+├── encoder/ # Media encoding/conversion module
+├── htmlindexer/ # HTML gallery generator module
+├── renamer/ # Batch file rename module
+├── replace/ # Text replacement module
+└── bomremove/ # BOM stripping module
+#+end_example
+
+* Build Commands
+:PROPERTIES:
+:ID: 55eee295-0bfb-492f-8f80-0d6837c7b461
+:END:
+
+#+begin_src bash
+# Clean build
+mvn clean package --settings .mvn/custom-settings.xml
+
+# Build without custom settings (may fail for some dependencies)
+mvn clean package
+
+# Run tests (if any)
+mvn test
+#+end_src
+
+* Running
+:PROPERTIES:
+:ID: 05296108-b9b8-4310-8cca-439ef4589a90
+:END:
+
+#+begin_src bash
+# After building, run the JAR directly
+java -cp target/meviz-*-jar-with-dependencies.jar eu.svjatoslav.meviz.Main
+
+# Or use the convenience script (requires installation)
+meviz <command>
+
+# Available commands:
+# - index : Generate/update HTML image gallery
+# - rename : Batch file rename
+# - replace : Text replacement
+# - encode : Media encoding/conversion
+# - bomremove : Strip BOM from files
+# - help : Show help
+#+end_src
+
+* Dependencies
+:PROPERTIES:
+:ID: 69373a95-4125-4d54-a6b5-4e4c370a8ad1
+:END:
+
+- Java 8+
+- Maven
+- External tools (for full functionality):
+ - ~lame~ — MP3 encoding
+ - ~vorbis-tools~ — OGG audio
+ - ~imagemagick~ — Image processing
+ - ~ffmpeg~ — Video/audio conversion
+
+* Code Style
+:PROPERTIES:
+:ID: 04108d3c-059f-4280-998e-28e470979ca1
+:END:
+
+- Standard Java conventions
+- Package prefix: ~eu.svjatoslav.meviz~
+- Each module implements the ~Module~ interface with methods:
+ - ~getModuleCommand()~ — CLI command name
+ - ~getDescription()~ — Brief description
+ - ~run(String[] args)~ — Execute module
+ - ~showCommandlineHelp()~ — Display help
+
+* Installation
+:PROPERTIES:
+:ID: 174b241c-1054-450a-91f0-1b18eae83266
+:END:
+
+See the ~install~ script for system-wide installation (requires sudo).
import javax.imageio.ImageReader;
import javax.imageio.metadata.IIOMetadata;
import javax.imageio.metadata.IIOMetadataNode;
+import javax.imageio.stream.ImageInputStream;
import java.awt.*;
+import java.awt.geom.AffineTransform;
import java.awt.image.BufferedImage;
import java.io.BufferedReader;
import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.HashSet;
+import java.util.Iterator;
import java.util.zip.CRC32;
import static org.openimaj.image.ImageUtilities.createBufferedImage;
}
}
+ /**
+ * Read EXIF orientation tag (0x0112) from a JPEG file using the JDK's
+ * built-in JPEG ImageIO metadata. Returns 1 when no usable orientation
+ * information is present.
+ *
+ * EXIF orientation values:
+ * 1 = normal, 2 = flip horizontal,
+ * 3 = 180°, 4 = flip vertical,
+ * 5 = transpose, 6 = 90° CW, 7 = transverse, 8 = 270° CW
+ */
+ public static int getExifOrientation(final File file) {
+ try (ImageInputStream stream = ImageIO.createImageInputStream(file)) {
+ if (stream == null)
+ return 1;
+
+ final Iterator<ImageReader> readers = ImageIO.getImageReaders(stream);
+ if (!readers.hasNext())
+ return 1;
+
+ final ImageReader reader = readers.next();
+ try {
+ reader.setInput(stream);
+ final IIOMetadata metadata = reader.getImageMetadata(0);
+ if (metadata == null)
+ return 1;
+
+ final String format = metadata.getNativeMetadataFormatName();
+ if (format == null || !format.toLowerCase().contains("jpeg"))
+ return 1;
+
+ final IIOMetadataNode root = (IIOMetadataNode) metadata
+ .getAsTree(format);
+ // scan all marker segments for the EXIF one
+ final NodeList allMarkers = root
+ .getElementsByTagName("markerSequence");
+ if (allMarkers.getLength() == 0)
+ return 1;
+
+ final NodeList segments = ((IIOMetadataNode) allMarkers
+ .item(0)).getElementsByTagName("unknown");
+ for (int i = 0; i < segments.getLength(); i++) {
+ final IIOMetadataNode segment = (IIOMetadataNode) segments
+ .item(i);
+ final String markerTag = segment.getAttribute("MarkerTag");
+ // EXIF is APP1 (marker tag 225 = 0xE1)
+ if (!"225".equals(markerTag))
+ continue;
+
+ final Object userObject = segment.getUserObject();
+ if (!(userObject instanceof byte[]))
+ continue;
+
+ final byte[] exif = (byte[]) userObject;
+ final Integer orientation = parseExifOrientation(exif);
+ if (orientation != null)
+ return orientation;
+ }
+ } finally {
+ reader.dispose();
+ }
+ } catch (final Exception e) {
+ System.out.println("Could not read EXIF orientation from "
+ + file + ": " + e.getMessage());
+ }
+ return 1;
+ }
+
+ /**
+ * Parse a raw EXIF APP1 segment and extract the orientation tag (0x0112).
+ * Handles both little-endian ("II") and big-endian ("MM") TIFF headers.
+ */
+ private static Integer parseExifOrientation(final byte[] exif) {
+ // Minimum: "Exif\0\0" (6) + byte order (2) + 42 (2) + IFD offset (4)
+ if (exif == null || exif.length < 14)
+ return null;
+
+ // Must start with "Exif\0\0"
+ if (exif[0] != 'E' || exif[1] != 'x' || exif[2] != 'i'
+ || exif[3] != 'f' || exif[4] != 0 || exif[5] != 0)
+ return null;
+
+ final int tiffStart = 6;
+ final boolean littleEndian;
+ if (exif[tiffStart] == 'I' && exif[tiffStart + 1] == 'I')
+ littleEndian = true;
+ else if (exif[tiffStart] == 'M' && exif[tiffStart + 1] == 'M')
+ littleEndian = false;
+ else
+ return null;
+
+ final int ifdOffset = readInt32(exif, tiffStart + 4, littleEndian);
+ final int ifdPos = tiffStart + ifdOffset;
+ if (ifdPos < 0 || ifdPos + 2 > exif.length)
+ return null;
+
+ final int entryCount = readUint16(exif, ifdPos, littleEndian);
+ for (int i = 0; i < entryCount; i++) {
+ final int entryPos = ifdPos + 2 + i * 12;
+ if (entryPos + 12 > exif.length)
+ break;
+
+ final int tag = readUint16(exif, entryPos, littleEndian);
+ if (tag == 0x0112)
+ return readUint16(exif, entryPos + 8, littleEndian);
+ }
+ return null;
+ }
+
+ private static int readUint16(final byte[] data, final int offset,
+ final boolean littleEndian) {
+ if (littleEndian)
+ return (data[offset] & 0xFF) | ((data[offset + 1] & 0xFF) << 8);
+ return ((data[offset] & 0xFF) << 8) | (data[offset + 1] & 0xFF);
+ }
+
+ private static int readInt32(final byte[] data, final int offset,
+ final boolean littleEndian) {
+ if (littleEndian)
+ return (data[offset] & 0xFF) | ((data[offset + 1] & 0xFF) << 8)
+ | ((data[offset + 2] & 0xFF) << 16)
+ | ((data[offset + 3] & 0xFF) << 24);
+ return ((data[offset] & 0xFF) << 24)
+ | ((data[offset + 1] & 0xFF) << 16)
+ | ((data[offset + 2] & 0xFF) << 8) | (data[offset + 3] & 0xFF);
+ }
+
+ /**
+ * Rotate and/or flip the image in memory according to EXIF orientation.
+ * Returns the original image unchanged for orientation 1 (or unknown).
+ * Never touches the file on disk.
+ */
+ public static BufferedImage applyExifOrientation(
+ final BufferedImage image, final int orientation) {
+ if (orientation < 2 || orientation > 8)
+ return image;
+
+ final int w = image.getWidth();
+ final int h = image.getHeight();
+ // orientations 5-8 swap width and height
+ final boolean swapDimensions = orientation >= 5;
+ final int newWidth = swapDimensions ? h : w;
+ final int newHeight = swapDimensions ? w : h;
+
+ final AffineTransform transform = new AffineTransform();
+ switch (orientation) {
+ case 2: // flip horizontal
+ transform.scale(-1, 1);
+ transform.translate(-w, 0);
+ break;
+ case 3: // 180°
+ transform.translate(w, h);
+ transform.rotate(Math.PI);
+ break;
+ case 4: // flip vertical
+ transform.scale(1, -1);
+ transform.translate(0, -h);
+ break;
+ case 5: // transpose (flip along top-left to bottom-right axis)
+ transform.rotate(Math.PI / 2);
+ transform.scale(1, -1);
+ break;
+ case 6: // 90° clockwise
+ transform.translate(h, 0);
+ transform.rotate(Math.PI / 2);
+ break;
+ case 7: // transverse (flip along top-right to bottom-left axis)
+ transform.translate(h, w);
+ transform.rotate(Math.PI / 2);
+ transform.scale(-1, 1);
+ break;
+ case 8: // 270° clockwise (90° counter-clockwise)
+ transform.translate(0, w);
+ transform.rotate(-Math.PI / 2);
+ break;
+ default:
+ return image;
+ }
+
+ final BufferedImage oriented = new BufferedImage(newWidth, newHeight,
+ image.getType() == 0 ? BufferedImage.TYPE_INT_RGB
+ : image.getType());
+ final Graphics2D g = oriented.createGraphics();
+ g.setRenderingHint(RenderingHints.KEY_INTERPOLATION,
+ RenderingHints.VALUE_INTERPOLATION_BILINEAR);
+ g.drawImage(image, transform, null);
+ g.dispose();
+ return oriented;
+ }
+
public static File getLayoutIndexFile(final Layout layout,
final File directoryToIndex) {
}
}
+ /**
+ * Load image with EXIF orientation applied in memory. The file on disk is
+ * never modified. GIFs skip rotation (handled separately, frames are
+ * rotated during thumbnail generation).
+ */
+ private static BufferedImage getOrientedBufferedImage(final File file)
+ throws ImageFormatError, IOException {
+ BufferedImage image = getBufferedImage(file);
+ if (!"gif".equalsIgnoreCase(
+ FilePathParser.getFileExtension(file.getName()))) {
+ final int orientation = Utils.getExifOrientation(file);
+ image = Utils.applyExifOrientation(image, orientation);
+ }
+ return image;
+ }
+
private static void makeJpegThumbnail(File inputFile, File outputFile, java.awt.Dimension preferredTargetDimensions)
throws IOException, ImageFormatError {
- final BufferedImage inputImage = getBufferedImage(inputFile);
+ final BufferedImage inputImage = getOrientedBufferedImage(inputFile);
final java.awt.Dimension sourceImageDimension = new java.awt.Dimension(
inputImage.getWidth(), inputImage.getHeight());
private static void makeGifThumbnail(
File inputFile, File outputFile, java.awt.Dimension preferredTargetDimensions) throws IOException {
+
+ final int orientation = Utils.getExifOrientation(inputFile);
+
ImageIcon imageIcon = new ImageIcon(IOHelper.getFileContents(inputFile));
final java.awt.Dimension sourceImageDimension = new java.awt.Dimension(
Utils.ImageFrame[] frames = Utils.readGIF(reader);
for (Utils.ImageFrame frame : frames) {
- Image scaleImage = scaleImage(frame.image, targetDimensions.width, targetDimensions.height);
+ BufferedImage orientedImage = Utils.applyExifOrientation(frame.image, orientation);
+ Image scaleImage = scaleImage(orientedImage, targetDimensions.width, targetDimensions.height);
BufferedImage bImage = new BufferedImage(
targetDimensions.width, targetDimensions.height, BufferedImage.TYPE_INT_ARGB);
Graphics2D bGr = bImage.createGraphics();
@Override
public void updateFileMetadata(final File parentDirectory) throws Exception {
- final BufferedImage image = getBufferedImage(getFile(parentDirectory));
+ final BufferedImage image = getOrientedBufferedImage(getFile(parentDirectory));
dimensions = new Dimension(image.getWidth(), image.getHeight());
}