Skip to content

Commit

Permalink
Added Exif CLI
Browse files Browse the repository at this point in the history
  • Loading branch information
ngrossmann committed Feb 6, 2013
1 parent b99f9cf commit fba6a5e
Show file tree
Hide file tree
Showing 9 changed files with 92 additions and 28 deletions.
29 changes: 29 additions & 0 deletions src/main/scala/net/n12n/exif/Exif.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
package net.n12n.exif
import java.io.IOException

object Exif extends App {
try {
if (args.length != 1)
usage()
val image = JpegMetaData(args(0))
for (exif <- image.exif) {
println("Exif (length: %d):".format(exif.length))
exif.ifds.foreach(printIfd)
val xres: Rational = exif.ifd0.value(TiffIfd.XResolution)
}
} catch {
case e: IOException =>
System.err.println("Faild to open %s for reading".format(args(0)))
System.exit(2)
}

private def usage() {
System.err.println("Usage: Exif <path>")
System.exit(1)
}

private def printIfd(ifd: Ifd) {
println("IFD: %s".format(ifd.name))
ifd.tags.foreach(a => println(" %s: %s".format(a.tag.name, a.value)))
}
}
17 changes: 11 additions & 6 deletions src/main/scala/net/n12n/exif/ExifIfd.scala
Original file line number Diff line number Diff line change
Expand Up @@ -25,19 +25,24 @@ package net.n12n.exif
* @param exif The Exif segment containing this IFD.
* @param offset Start of this IFD relative to the [[net.n12n.exif.ExifSegment#tiffOffset]].
*/
class ExifIfd(exif: ExifSegment, offset: Int) extends Ifd(exif, offset, ExifIfd.marker2tag)
class ExifIfd(exif: ExifSegment, offset: Int) extends Ifd(exif, offset, "Exif IFD") {
override type T = ExifTag
override val Tags = ExifIfd.Tags
override protected def createTag(marker: Int) =
ExifTag(marker, "Unknown Exif Tag %04x".format(marker))
}

case class ExifTag(marker: Int, name: String) extends Tag

object ExifIfd extends IfdObjectBase[ExifTag] {
override protected val tag = (marker: Int, name: String) => ExifTag(marker, name)
object ExifIfd {

val PixelXDimension = new ExifTag(40962, "PixelXDimension") with NumericTag
val PixelYDimension = new ExifTag(40963, "PixelYDimension") with NumericTag
val ComponentsConfiguration = new ExifTag(37121, "ComponentsConfiguration") with UndefinedTag
val CompressedBitsPerPixel = new ExifTag(37122, "CompressedBitsPerPixel") with RationalTag
val ExifVersion = new ExifTag(36864, "ExifVersion") with UndefinedTag
val FlashpixVersion = new ExifTag(40960, "FlashpixVersion") with UndefinedTag
val ColorSpace = new ExifTag(40961, "ColorSpace") with ShortTag
val MakerNote = new ExifTag(37500, "MakerNote") with UndefinedTag
val UserComment = new ExifTag(37510, "UserComment") with UndefinedTag
val RelatedSoundFile = new ExifTag(40964, "RelatedSoundFile") with AsciiTag
Expand Down Expand Up @@ -89,9 +94,9 @@ object ExifIfd extends IfdObjectBase[ExifTag] {
val DeviceSettingDescription = new ExifTag(41995, "DeviceSettingDescription") with UndefinedTag
val SubjectDistanceRange = new ExifTag(41996, "SubjectDistanceRange") with ShortTag

override val Tags = Set[ExifTag](PixelXDimension, PixelYDimension,
ComponentsConfiguration, CompressedBitsPerPixel, ExifVersion, FlashpixVersion, MakerNote,
UserComment, RelatedSoundFile, DateTimeOriginal, DateTimeDigitized, SubSecTime,
val Tags = Set[ExifTag](PixelXDimension, PixelYDimension,
ComponentsConfiguration, CompressedBitsPerPixel, ExifVersion, FlashpixVersion, ColorSpace,
MakerNote, UserComment, RelatedSoundFile, DateTimeOriginal, DateTimeDigitized, SubSecTime,
SubSecTimeOriginal, SubSecTimeDigitized, ImageUniqueID,
ExposureTime, FNumber, ExposureProgram, SpectralSensitivity, ISOSpeedRatings, OECF,
ShutterSpeedValue, ApertureValue, BrightnessValue, ExposureBiasValue, MaxApertureValue,
Expand Down
9 changes: 6 additions & 3 deletions src/main/scala/net/n12n/exif/ExifSegment.scala
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ object ExifSegment {
* @param opt Optional IFD.
* @return Attributes or an empty list.
*/
def listAttrs[T <: Tag](opt: Option[Ifd[T]]): List[IfdAttribute[T]] = opt match {
def listAttrs(opt: Option[Ifd]): List[IfdAttribute[Tag]] = opt match {
case Some(ifd) => ifd.tags.toList
case None => Nil
}
Expand All @@ -56,9 +56,9 @@ class ExifSegment(length: Int, data: ByteSeq, offset: Int = 0)
data.toSignedShort(tiffOffset + 2, byteOrder)))
private val ifdOffset = data.toSignedLong(tiffOffset + 4, byteOrder)
/** The 0th IFD, always present. */
val ifd0 = new TiffIfd(this, ifdOffset)
val ifd0: TiffIfd = new TiffIfd(this, ifdOffset, "IFD0")
/** Optional 1st IFD. */
val ifd1: Option[TiffIfd] = if (ifd0.nextIfd != 0) Some(new TiffIfd(this, ifd0.nextIfd)) else None
val ifd1: Option[TiffIfd] = if (ifd0.nextIfd != 0) Some(new TiffIfd(this, ifd0.nextIfd, "IFD1")) else None
/** Optional Exif IFD. */
val exifIfd: Option[ExifIfd] = ifd0.tags.find(_.tag == TiffIfd.ExifIfdPointer) match {
case Some(pointer: LongIFD[_]) => Some(new ExifIfd(this, pointer.value.head.toInt))
Expand All @@ -69,6 +69,9 @@ class ExifSegment(length: Int, data: ByteSeq, offset: Int = 0)
case Some(pointer: LongIFD[_]) => Some(new GpsIfd(this, pointer.value.head.toInt))
case None => None
}
/** List of all IFDs in this Exif segment. */
lazy val ifds = ifd0 :: ifd1.toList ::: exifIfd.toList ::: gpsIfd.toList

/** List of all attributes of all IFDs of this segment. */
lazy val allAttrs: List[IfdAttribute[Tag]] = ifd0.tags.toList ::: listAttrs(ifd1) :::
listAttrs(exifIfd) ::: listAttrs(gpsIfd)
Expand Down
11 changes: 7 additions & 4 deletions src/main/scala/net/n12n/exif/GpsIfd.scala
Original file line number Diff line number Diff line change
Expand Up @@ -23,12 +23,15 @@ package net.n12n.exif
* @author niklas
*
*/
class GpsIfd(exif: ExifSegment, offset: Int) extends Ifd(exif, offset, GpsIfd.marker2tag)
class GpsIfd(exif: ExifSegment, offset: Int) extends Ifd(exif, offset, "GPS IFD") {
override type T = GpsTag
override val Tags = GpsIfd.Tags
override protected def createTag(marker: Int) = GpsTag(marker, "Unknown GPS Tag")
}

case class GpsTag(val marker: Int, val name: String) extends Tag

object GpsIfd extends IfdObjectBase[GpsTag] {
override val tag = (marker: Int, name: String) => new GpsTag(marker, name)
object GpsIfd {

val GPSVersionID = new GpsTag(0, "GPSVersionID") with ByteTag
val GPSLatitudeRef = new GpsTag(1, "GPSLatitudeRef") with AsciiTag
Expand Down Expand Up @@ -61,7 +64,7 @@ object GpsIfd extends IfdObjectBase[GpsTag] {
val GPSAreaInformation = new GpsTag(28, "GPSAreaInformation") with UndefinedTag
val GPSDateStamp = new GpsTag(29, "GPSDateStamp") with AsciiTag
val GPSDifferential = new GpsTag(30, "GPSDifferential") with ShortTag
override val Tags = Set[GpsTag](GPSVersionID,
val Tags = Set[GpsTag](GPSVersionID,
GPSLatitudeRef,
GPSLatitude,
GPSLongitudeRef,
Expand Down
14 changes: 11 additions & 3 deletions src/main/scala/net/n12n/exif/Ifd.scala
Original file line number Diff line number Diff line change
Expand Up @@ -25,15 +25,18 @@ package net.n12n.exif
* @param exif The Exif segment containing this IFD.
* @param offset Start of this IFD relative to the [[net.n12n.exif.ExifSegment#tiffOffset]].
*/
class Ifd[T <: Tag](exif: ExifSegment, offset: Int, bytes2tag: (Int) => T) {
abstract class Ifd(exif: ExifSegment, offset: Int, val name: String) {
type T <: Tag
/** Set of tags. */
val Tags: Set[T]
val count = exif.data.toShort(exif.tiffOffset + offset, exif.byteOrder)
val tags: Seq[IfdAttribute[T]] = for (i <- 0 until count) yield
lazy val tags: Seq[IfdAttribute[T]] = for (i <- 0 until count) yield
IfdAttribute(exif.data, offset + 2 + i * IfdAttribute.Length, exif.tiffOffset, exif.byteOrder,
bytes2tag)
/**
* Offset to next IDF relative to the Exif's TiffHeader.
*/
val nextIfd = exif.data.toSignedLong(exif.tiffOffset + offset + 2 + count * IfdAttribute.Length,
lazy val nextIfd = exif.data.toSignedLong(exif.tiffOffset + offset + 2 + count * IfdAttribute.Length,
exif.byteOrder)

/**
Expand Down Expand Up @@ -62,6 +65,11 @@ class Ifd[T <: Tag](exif: ExifSegment, offset: Int, bytes2tag: (Int) => T) {
throw AttributeNotFoundException(tag.name))
tag.value(ifdAttr, exif.byteOrder)
}

protected def bytes2tag(id: Int): T =
Tags.find(_.marker == id).getOrElse(createTag(id))

protected def createTag(id: Int): T

override def toString() = "IFD(%x, %x)\n%s".format(count, nextIfd, tags.mkString(" \n"))
}
6 changes: 6 additions & 0 deletions src/main/scala/net/n12n/exif/IfdAttribute.scala
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,12 @@ class LongIFD[T <: Tag](tag: T, count: Int, data: ByteSeq, order: ByteOrder)
val value: Seq[Long] = for (i <- 0 until count) yield data.toLong(i * tagType.size, order)
}

class LongIFD2[T <: Tag](tag: T, count: Int, data: ByteSeq, order: ByteOrder)
extends IfdAttribute(tag, Type.Long, count, data) {
type V = Seq[Long]
val value: Seq[Long] = for (i <- 0 until count) yield data.toLong(i * tagType.size, order)
}

class RationalIFD[T <: Tag](tag: T, count: Int, data: ByteSeq, order: ByteOrder)
extends IfdAttribute(tag, Type.Rational, count, data) {
type V = Seq[Rational]
Expand Down
20 changes: 14 additions & 6 deletions src/main/scala/net/n12n/exif/JpegMetaData.scala
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,14 @@ package net.n12n.exif
import java.io.InputStream
import scala.io.BufferedSource
import java.lang.{IllegalArgumentException, IllegalStateException}

import java.io.FileInputStream
object JpegMetaData {
/**
* Read image from file.
* @param file filename.
*/
def apply(file: String): JpegMetaData = new JpegMetaData(new FileInputStream(file))
}
/**
* Read JPEG from byte stream.
*
Expand All @@ -36,16 +43,17 @@ class JpegMetaData(data: InputStream) {
if (SoiMarker != soi) throw new IllegalArgumentException("Not a JPEG image, expected " + SoiMarker +
" found " + soi)
val segments = parseSegments(in)

val comment = segments.find(_.marker == Segment.ComMarker) match {
case Some(seg: ComSegment) => seg.comment
case None => ""
}
/** List of comment segments */
val comments = segments.filter(_.marker == Segment.ComMarker)

val exif: Option[ExifSegment] = segments.find(_.isInstanceOf[ExifSegment]) match {
case Some(exif: ExifSegment) => Some(exif)
case _ => None
}
/**
* Total size of the image.
*/
lazy val size = segments.map(_.length).sum + SoiMarker.length + SosMarker.length

/**
* JPEG meta-data as string.
Expand Down
10 changes: 6 additions & 4 deletions src/main/scala/net/n12n/exif/TiffIfd.scala
Original file line number Diff line number Diff line change
Expand Up @@ -25,13 +25,15 @@ package net.n12n.exif
* @param exif The Exif segment containing this IFD.
* @param offset Start of this IFD relative to the [[net.n12n.exif.ExifSegment#tiffOffset]].
*/
class TiffIfd(exif: ExifSegment, offset: Int) extends Ifd(exif, offset, TiffIfd.marker2tag)
class TiffIfd(exif: ExifSegment, offset: Int, name: String) extends Ifd(exif, offset, name) {
override type T = TiffTag
override val Tags = TiffIfd.Tags
override protected def createTag(marker: Int) = TiffTag(marker, "Unknown Tiff Tag")
}

case class TiffTag(marker: Int, name: String) extends Tag

object TiffIfd extends IfdObjectBase[TiffTag]{
override protected val tag = (marker: Int, name: String) => TiffTag(marker, name)

object TiffIfd {
val ImageWidth = new TiffTag(256, "ImageWidth") with NumericTag
val ImageLength = new TiffTag(257, "ImageLength") with NumericTag
val BitsPerSample = new TiffTag(258, "BitsPerSample") with ShortListTag
Expand Down
4 changes: 2 additions & 2 deletions src/test/scala/net/n12n/exif/JpegMetadataTest.scala
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ class JpegMetaDataTest extends FunSuite {
val metadata = load("image.jpg")
assert(metadata.exif != None, "Exif tag found")
}

expect(8, "Orientation") {
val metadata = load("image-vertical.jpg")
metadata.exif match {
Expand All @@ -39,7 +39,7 @@ class JpegMetaDataTest extends FunSuite {
case None => -1
}
}

test("GPS Data") {
val metadata = load("image-gps.jpg")
metadata.exif match {
Expand Down

0 comments on commit fba6a5e

Please sign in to comment.