Logo Search packages:      
Sourcecode: f-spot version File versions  Download package

File.cs

//
// File.cs: Provides tagging for Jpeg files
//
// Author:
//   Ruben Vermeersch (ruben@savanne.be)
//   Mike Gemuende (mike@gemuende.de)
//   Stephane Delcroix (stephane@delcroix.org)
//
// Copyright (C) 2009 Ruben Vermeersch
// Copyright (C) 2009 Mike Gemuende
// Copyright (c) 2009 Stephane Delcroix
//
// This library is free software; you can redistribute it and/or modify
// it  under the terms of the GNU Lesser General Public License version
// 2.1 as published by the Free Software Foundation.
//
// This library is distributed in the hope that it will be useful, but
// WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
// Lesser General Public License for more details.
//
// You should have received a copy of the GNU Lesser General Public
// License along with this library; if not, write to the Free Software
// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307
// USA
//

using System;
using System.Collections.Generic;
using System.IO;

using TagLib.Image;
using TagLib.IFD;
using TagLib.IFD.Entries;
using TagLib.Xmp;

namespace TagLib.Jpeg
{

      /// <summary>
      ///    This class extends <see cref="TagLib.Image.ImageBlockFile" /> to provide tagging
      ///    and properties support for Jpeg files.
      /// </summary>
      [SupportedMimeType("taglib/jpg", "jpg")]
      [SupportedMimeType("taglib/jpeg", "jpeg")]
      [SupportedMimeType("taglib/jpe", "jpe")]
      [SupportedMimeType("taglib/jif", "jif")]
      [SupportedMimeType("taglib/jfif", "jfif")]
      [SupportedMimeType("taglib/jfi", "jfi")]
      [SupportedMimeType("image/jpeg")]
00051       public class File : TagLib.Image.ImageBlockFile
      {

            /// <summary>
            ///    The magic bits used to recognize an Exif segment
            /// </summary>
00057             private static readonly string EXIF_IDENTIFIER = "Exif\0\0";

            /// <summary>
            ///    Standard (empty) JFIF header to add, if no one is contained
            /// </summary>
00062             private static readonly byte [] BASIC_JFIF_HEADER = new byte [] {
                  // segment maker
                  0xFF, (byte) Marker.APP0,

                  // segment size
                  0x00, 0x10,

                  // segment data
                  0x4A, 0x46, 0x49, 0x46, 0x00, 0x01, 0x01,
                  0x01, 0x00, 0x48, 0x00, 0x48, 0x00, 0x00
            };


#region Private Fields

            /// <summary>
            ///    Contains the media properties.
            /// </summary>
00080             private Properties properties;

            /// <summary>
            ///    For now, we do not allow to change the jfif header. As long as this is
            ///    the case, the header is kept as it is.
            /// </summary>
00086             private ByteVector jfif_header = null;

            /// <summary>
            ///    The image width, as parsed from the Frame
            /// </summary>
00091             ushort width;

            /// <summary>
            ///    The image height, as parsed from the Frame
            /// </summary>
00096             ushort height;

            /// <summary>
            ///    Quality of the image, stored as we parse the file
            /// </summary>
00101             int quality;

#endregion

#region Constructors

            /// <summary>
            ///    Constructs and initializes a new instance of <see
            ///    cref="File" /> for a specified path in the local file
            ///    system and specified read style.
            /// </summary>
            /// <param name="path">
            ///    A <see cref="string" /> object containing the path of the
            ///    file to use in the new instance.
            /// </param>
            /// <param name="propertiesStyle">
            ///    A <see cref="ReadStyle" /> value specifying at what level
            ///    of accuracy to read the media properties, or <see
            ///    cref="ReadStyle.None" /> to ignore the properties.
            /// </param>
            /// <exception cref="ArgumentNullException">
            ///    <paramref name="path" /> is <see langword="null" />.
            /// </exception>
00124             public File (string path, ReadStyle propertiesStyle)
                  : this (new File.LocalFileAbstraction (path),
                        propertiesStyle)
            {
            }

            /// <summary>
            ///    Constructs and initializes a new instance of <see
            ///    cref="File" /> for a specified path in the local file
            ///    system.
            /// </summary>
            /// <param name="path">
            ///    A <see cref="string" /> object containing the path of the
            ///    file to use in the new instance.
            /// </param>
            /// <exception cref="ArgumentNullException">
            ///    <paramref name="path" /> is <see langword="null" />.
            /// </exception>
00142             public File (string path) : this (path, ReadStyle.Average)
            {
            }

            /// <summary>
            ///    Constructs and initializes a new instance of <see
            ///    cref="File" /> for a specified file abstraction and
            ///    specified read style.
            /// </summary>
            /// <param name="abstraction">
            ///    A <see cref="IFileAbstraction" /> object to use when
            ///    reading from and writing to the file.
            /// </param>
            /// <param name="propertiesStyle">
            ///    A <see cref="ReadStyle" /> value specifying at what level
            ///    of accuracy to read the media properties, or <see
            ///    cref="ReadStyle.None" /> to ignore the properties.
            /// </param>
            /// <exception cref="ArgumentNullException">
            ///    <paramref name="abstraction" /> is <see langword="null"
            ///    />.
            /// </exception>
00164             public File (File.IFileAbstraction abstraction,
                         ReadStyle propertiesStyle) : base (abstraction)
            {
                  Read (propertiesStyle);
            }

            /// <summary>
            ///    Constructs and initializes a new instance of <see
            ///    cref="File" /> for a specified file abstraction.
            /// </summary>
            /// <param name="abstraction">
            ///    A <see cref="IFileAbstraction" /> object to use when
            ///    reading from and writing to the file.
            /// </param>
            /// <exception cref="ArgumentNullException">
            ///    <paramref name="abstraction" /> is <see langword="null"
            ///    />.
            /// </exception>
00182             protected File (IFileAbstraction abstraction)
                  : this (abstraction, ReadStyle.Average)
            {
            }

#endregion

#region Public Properties

            /// <summary>
            ///    Gets the media properties of the file represented by the
            ///    current instance.
            /// </summary>
            /// <value>
            ///    A <see cref="TagLib.Properties" /> object containing the
            ///    media properties of the file represented by the current
            ///    instance.
            /// </value>
00200             public override TagLib.Properties Properties {
                  get { return properties; }
            }

#endregion

#region Public Methods

            /// <summary>
            ///    Saves the changes made in the current instance to the
            ///    file it represents.
            /// </summary>
00212             public override void Save ()
            {
                  if (!Writeable || PossiblyCorrupt)
                        throw new InvalidOperationException ("File not writeable. Corrupt metadata?");

                  Mode = AccessMode.Write;
                  try {
                        WriteMetadata ();

                        TagTypesOnDisk = TagTypes;
                  } finally {
                        Mode = AccessMode.Closed;
                  }
            }

#endregion

#region Private Methods

            /// <summary>
            ///    Reads the information from file with a specified read style.
            /// </summary>
            /// <param name="propertiesStyle">
            ///    A <see cref="ReadStyle" /> value specifying at what level
            ///    of accuracy to read the media properties, or <see
            ///    cref="ReadStyle.None" /> to ignore the properties.
            /// </param>
00239             private void Read (ReadStyle propertiesStyle)
            {
                  Mode = AccessMode.Read;
                  try {
                        ImageTag = new CombinedImageTag (TagTypes.XMP | TagTypes.TiffIFD | TagTypes.JpegComment);

                        ValidateHeader ();
                        ReadMetadata ();

                        TagTypesOnDisk = TagTypes;

                        if (propertiesStyle != ReadStyle.None)
                              properties = ExtractProperties ();

                  } finally {
                        Mode = AccessMode.Closed;
                  }
            }

            /// <summary>
            ///    Attempts to extract the media properties of the main
            ///    photo.
            /// </summary>
            /// <returns>
            ///    A <see cref="Properties" /> object with a best effort guess
            ///    at the right values. When no guess at all can be made,
            ///    <see langword="null" /> is returned.
            /// </returns>
00267             private Properties ExtractProperties ()
            {
                  if (width > 0 && height > 0)
                        return new Properties (TimeSpan.Zero, new Codec (width, height, quality));

                  return null;

            }

            /// <summary>
            ///    Validates if the opened file is actually a JPEG.
            /// </summary>
00279             private void ValidateHeader ()
            {
                  ByteVector segment = ReadBlock (2);
                  if (segment.ToUShort () != 0xFFD8)
                        throw new CorruptFileException ("Expected SOI marker at the start of the file.");
            }


            /// <summary>
            ///    Reads a segment marker for a segment starting at current position.
            ///    The second byte of the marker is returned, since the first is equal
            ///    to 0xFF in every case.
            /// </summary>
            /// <returns>
            ///    A <see cref="TagLib.Jpeg.Marker"/> with the second byte of the segment marker.
            /// </returns>
00295             private Marker ReadSegmentMarker ()
            {
                  ByteVector segment_header = ReadBlock (2);

                  if (segment_header.Count != 2)
                        throw new CorruptFileException ("Could not read enough bytes for segment maker");

                  if (segment_header[0] != 0xFF)
                        throw new CorruptFileException ("Start of Segment expected at " + (Tell - 2));

                  return (Marker)segment_header[1];
            }


            /// <summary>
            ///    Reads the size of a segment at the current position.
            /// </summary>
            /// <returns>
            ///    A <see cref="System.UInt16"/> with the size of the current segment.
            /// </returns>
00315             private ushort ReadSegmentSize ()
            {
                  long position = Tell;

                  ByteVector segment_size_bytes = ReadBlock (2);

                  if (segment_size_bytes.Count != 2)
                        throw new CorruptFileException ("Could not read enough bytes to determine segment size");

                  ushort segment_size = segment_size_bytes.ToUShort ();

                  // the size itself must be contained in the segment size
                  // so the smallest (theoretically) possible number of bytes if 2
                  if (segment_size < 2)
                        throw new CorruptFileException (String.Format ("Invalid segment size ({0} bytes)", segment_size));

                  long length = 0;
                  try {
                        length = Length;
                  } catch (Exception) {
                        // Probably not supported by stream.
                  }

                  if (length > 0 && position + segment_size >= length)
                        throw new CorruptFileException ("Segment size exceeds file size");

                  return segment_size;
            }


            /// <summary>
            ///    Extracts the metadata from the current file by reading every segment in file.
            ///    Method should be called with read position at first segment marker.
            /// </summary>
00349             private void ReadMetadata ()
            {
                  // loop while marker is not EOI and not the data segment
                  while (true) {
                        Marker marker = ReadSegmentMarker ();

                        // we stop parsing when the end of file (EOI) or the begin of the
                        // data segment is reached (SOS)
                        // the second case is a trade-off between tolerant and fast parsing
                        if (marker == Marker.EOI || marker == Marker.SOS)
                              break;

                        long position = Tell;
                        ushort segment_size = ReadSegmentSize ();

                        // segment size contains 2 bytes of the size itself, so the
                        // pure data size is this (and the cast is save)
                        ushort data_size = (ushort) (segment_size - 2);

                        switch (marker) {
                        case Marker.APP0: // possibly JFIF header
                              ReadJFIFHeader (data_size);
                              break;

                        case Marker.APP1: // possibly Exif or Xmp data found
                              ReadAPP1Segment (data_size);
                              break;

                        case Marker.COM:  // Comment segment found
                              ReadCOMSegment (data_size);
                              break;

                        case Marker.SOF0:
                        case Marker.SOF1:
                        case Marker.SOF2:
                        case Marker.SOF3:
                        case Marker.SOF9:
                        case Marker.SOF10:
                        case Marker.SOF11:
                              ReadSOFSegment (data_size, marker);
                              break;

                        case Marker.DQT:  // Quantization table(s), use it to guess quality
                              ReadDQTSegment (data_size);
                              break;
                        }

                        // set position to next segment and start with next segment marker
                        Seek (position + segment_size, SeekOrigin.Begin);
                  }
            }

            /// <summary>
            ///    Reads a JFIF header at current position
            /// </summary>
00404             private void ReadJFIFHeader (ushort length)
            {
                  // JFIF header should be contained as first segment
                  // SOI marker + APP0 Marker + segment size = 6 bytes
                  if (Tell != 6)
                        return;

                  if (ReadBlock (5).ToString ().Equals ("JFIF\0")) {

                        // store the JFIF header as it is
                        Seek (2, SeekOrigin.Begin);
                        jfif_header = ReadBlock (length + 2 + 2);

                        AddMetadataBlock (2, length + 2 + 2);
                  }

            }

            /// <summary>
            ///    Reads an APP1 segment to find EXIF or XMP metadata.
            /// </summary>
            /// <param name="length">
            ///    The length of the segment that will be read.
            /// </param>
00428             private void ReadAPP1Segment (ushort length)
            {
                  long position = Tell;
                  ByteVector data = null;

                  // for an Exif segment, the data block consists of 14 bytes of:
                  //    * 6 bytes Exif identifier string
                  //    * 2 bytes bigendian indication MM (or II)
                  //    * 2 bytes Tiff magic number (42)
                  //    * 4 bytes offset of the first IFD in this segment
                  //
                  //    the last two points are alreay encoded according to
                  //    big- or littleendian
                  int exif_header_length = 14;

                  // could be an Exif segment
                  if ((ImageTag.TagTypes & TagLib.TagTypes.TiffIFD) == 0x00 && length >= exif_header_length) {

                        data = ReadBlock (exif_header_length);

                        if (data.Count == exif_header_length
                            && data.Mid (0, 6).ToString ().Equals (EXIF_IDENTIFIER)) {

                              bool is_bigendian = data.Mid (6, 2).ToString ().Equals ("MM");

                              ushort magic = data.Mid (8, 2).ToUShort (is_bigendian);
                              if (magic != 42)
                                    throw new Exception (String.Format ("Invalid TIFF magic: {0}", magic));

                              uint ifd_offset = data.Mid (10, 4).ToUInt (is_bigendian);

                              var exif = new IFDTag ();
                              var reader = new IFDReader (this, is_bigendian, exif.Structure, position + 6, ifd_offset, (uint) (length - 6));
                              reader.Read ();
                              ImageTag.AddTag (exif);

                              AddMetadataBlock (position - 4, length + 4);

                              return;
                        }
                  }

                  int xmp_header_length = XmpTag.XAP_NS.Length + 1;

                  // could be an Xmp segment
                  if ((ImageTag.TagTypes & TagLib.TagTypes.XMP) == 0x00 && length >= xmp_header_length) {

                        // if already data is read for determining the Exif segment,
                        // just read the remaining bytes.
                        // NOTE: that (exif_header_length < xmp_header_length) holds
                        if (data == null)
                              data = ReadBlock (xmp_header_length);
                        else
                              data.Add (ReadBlock (xmp_header_length - exif_header_length));

                        if (data.ToString ().Equals (XmpTag.XAP_NS + "\0")) {
                              ByteVector xmp_data = ReadBlock (length - xmp_header_length);

                              ImageTag.AddTag (new XmpTag (xmp_data.ToString ()));

                              AddMetadataBlock (position - 4, length + 4);
                        }
                  }
            }


            /// <summary>
            ///    Writes the metadata back to file. All metadata is stored in the first segments
            ///    of the file.
            /// </summary>
00498             private void WriteMetadata ()
            {
                  // first render all metadata segments to a ByteVector before the
                  // file is touched ...
                  ByteVector data = new ByteVector ();

                  // existing jfif header is retained, otherwise a standard one
                  // is created
                  if (jfif_header != null)
                        data.Add (jfif_header);
                  else
                        data.Add (BASIC_JFIF_HEADER);

                  data.Add (RenderExifSegment ());
                  data.Add (RenderXMPSegment ());
                  data.Add (RenderCOMSegment ());

                  SaveMetadata (data, 2);
            }

            /// <summary>
            ///    Creates a <see cref="ByteVector"/> for the Exif segment of this file
            /// </summary>
            /// <returns>
            ///    A <see cref="ByteVector"/> with the whole Exif segment, if exif tags
            ///    exists, otherwise null.
            /// </returns>
00525             private ByteVector RenderExifSegment ()
            {
                  // Check, if IFD0 is contained
                  IFDTag exif = ImageTag.Exif;
                  if (exif == null)
                        return null;

                  // first IFD starts at 8
                  uint first_ifd_offset = 8;

                  // Render IFD0
                  // FIXME: store endianess and use it here
                  var renderer = new IFDRenderer (true, exif.Structure, first_ifd_offset);
                  ByteVector exif_data = renderer.Render ();

                  uint segment_size = (uint) (first_ifd_offset + exif_data.Count + 2 + 6);

                  // do not render data segments, which cannot fit into the possible segment size
                  if (segment_size > ushort.MaxValue)
                        throw new Exception ("Exif Segment is too big to render");

                  // Create whole segment
                  ByteVector data = new ByteVector (new byte [] { 0xFF, (byte) Marker.APP1 });
                  data.Add (ByteVector.FromUShort ((ushort) segment_size));
                  data.Add ("Exif\0\0");
                  data.Add (ByteVector.FromString ("MM", StringType.Latin1));
                  data.Add (ByteVector.FromUShort (42));
                  data.Add (ByteVector.FromUInt (first_ifd_offset));

                  // Add ifd data itself
                  data.Add (exif_data);

                  return data;
            }


            /// <summary>
            ///    Creates a <see cref="ByteVector"/> for the Xmp segment of this file
            /// </summary>
            /// <returns>
            ///    A <see cref="ByteVector"/> with the whole Xmp segment, if xmp tags
            ///    exists, otherwise null.
            /// </returns>
00568             private ByteVector RenderXMPSegment ()
            {
                  // Check, if XmpTag is contained
                  XmpTag xmp = ImageTag.Xmp;
                  if (xmp == null)
                        return null;

                  ByteVector xmp_data = XmpTag.XAP_NS + "\0";
                  xmp_data.Add (xmp.Render ());

                  uint segment_size = (uint) (2 + xmp_data.Count);

                  // do not render data segments, which cannot fit into the possible segment size
                  if (segment_size > ushort.MaxValue)
                        throw new Exception ("XMP Segment is too big to render");

                  // Create whole segment
                  ByteVector data = new ByteVector (new byte [] { 0xFF, (byte) Marker.APP1 });
                  data.Add (ByteVector.FromUShort ((ushort) segment_size));
                  data.Add (xmp_data);

                  return data;
            }


            /// <summary>
            ///    Reads a COM segment to find the JPEG comment.
            /// </summary>
            /// <param name="length">
            ///    The length of the segment that will be read.
            /// </param>
00599             private void ReadCOMSegment (int length)
            {
                  if ((ImageTag.TagTypes & TagLib.TagTypes.JpegComment) != 0x00)
                        return;

                  long position = Tell;

                  JpegCommentTag com_tag;

                  if (length == 0) {
                         com_tag = new JpegCommentTag ();
                  } else {
                        ByteVector data = ReadBlock (length);

                        int terminator = data.Find ("\0", 0);

                        if (terminator < 0)
                              com_tag = new JpegCommentTag (data.ToString ());
                        else
                              com_tag = new JpegCommentTag (data.Mid (0, terminator).ToString ());
                  }

                  ImageTag.AddTag (com_tag);
                  AddMetadataBlock (position - 4, length + 4);
            }

            /// <summary>
            ///    Creates a <see cref="ByteVector"/> for the comment segment of this file
            /// </summary>
            /// <returns>
            ///    A <see cref="ByteVector"/> with the whole comment segment, if a comment tag
            ///    exists, otherwise null.
            /// </returns>
00632             private ByteVector RenderCOMSegment ()
            {
                  // check, if Comment is contained
                  JpegCommentTag com_tag = GetTag (TagTypes.JpegComment) as JpegCommentTag;
                  if (com_tag == null)
                        return null;

                  // create comment data
                  ByteVector com_data =
                        ByteVector.FromString (com_tag.Value + "\0", StringType.Latin1);

                  uint segment_size = (uint) (2 + com_data.Count);

                  // do not render data segments, which cannot fit into the possible segment size
                  if (segment_size > ushort.MaxValue)
                        throw new Exception ("Comment Segment is too big to render");

                  // create segment
                  ByteVector data = new ByteVector (new byte [] { 0xFF, (byte) Marker.COM });
                  data.Add (ByteVector.FromUShort ((ushort) segment_size));

                  data.Add (com_data);

                  return data;
            }

            /// <summary>
            ///    Reads and parse a SOF segment
            /// </summary>
            /// <param name="length">
            ///    The length of the segment that will be read.
            /// </param>
            /// <param name="marker">
            ///    The SOFx marker.
            /// </param>
00667             void ReadSOFSegment (int length, Marker marker)
            {
#pragma warning disable 219 // Assigned, never read
                  byte p = ReadBlock (1)[0];    //precision
#pragma warning restore 219

                  //FIXME: according to specs, height could be 0 here, and should be retrieved from the DNL marker
                  height = ReadBlock (2).ToUShort ();
                  width = ReadBlock (2).ToUShort ();
            }

            /// <summary>
            ///    Reads the DQT Segment, and Guesstimate the image quality from it
            /// </summary>
            /// <param name="length">
            ///    The length of the segment that will be read
            /// </param>
00684             void ReadDQTSegment (int length)
            {
                  // See CCITT Rec. T.81 (1992 E), B.2.4.1 (p39) for DQT syntax
                  while (length > 0) {

                        byte pqtq = ReadBlock (1)[0]; length --;
                        byte pq = (byte)(pqtq >> 4);  //0 indicates 8-bit Qk, 1 indicates 16-bit Qk
                        byte tq = (byte)(pqtq & 0x0f);      //table index;
                        int [] table = null;
                        switch (tq) {
                        case 0:
                              table = Table.StandardLuminanceQuantization;
                              break;
                        case 1:
                              table = Table.StandardChrominanceQuantization;
                              break;
                        }

                        bool allones = true; //check for all-ones tables (q=100)
                        double cumsf = 0.0;
                        //double cumsf2 = 0.0;
                        for (int row = 0; row < 8; row ++) {
                              for (int col = 0; col < 8; col++) {
                                    ushort val = ReadBlock (pq == 1 ? 2 : 1).ToUShort (); length -= (pq + 1);
                                    if (table != null) {
                                          double x = 100.0 * (double)val / (double)table [row*8+col]; //Scaling factor in percent
                                          cumsf += x;
                                          //cumsf2 += x*x;
                                          allones = allones && (val == 1);
                                    }
                              }
                        }

                        if (table != null) {
                              double local_q;
                              cumsf /= 64.0;          // mean scale factor
                              //cumfs2 /= 64.0;
                              //double variance = cumsf2 - (cumsf * cumsf);

                              if (allones)
                                    local_q = 100.0;
                              else if (cumsf <= 100.0)
                                    local_q = (200.0 - cumsf) / 2.0;
                              else
                                    local_q = 5000.0 / cumsf;
                              quality = Math.Max (quality, (int)local_q);
                        }
                  }
            }

#endregion
      }
}

Generated by  Doxygen 1.6.0   Back to index