/* * Copyright (c) 2006 Novell Inc. * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License as * published by the Free Software Foundation; either version 2 of the * License, or (at your option) any later version. * * This program 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 * General Public License for more details. * * You should have received a copy of the GNU General Public * License along with this program; if not, write to the * Free Software Foundation, Inc., 59 Temple Place - Suite 330, * Boston, MA 02111-1307, USA. */ using System; using System.IO; using System.Collections; using FSpot.Xmp; using FSpot; #if ENABLE_NUNIT using NUnit.Framework; #endif public class JpegHeader : SemWeb.StatementSource { public enum JpegMarker { Tem = 0x01, Rst0 = 0xd0, // RstN used for resync, ignore Rst1 = 0xd1, Rst2 = 0xd2, Rst3 = 0xd3, Rst4 = 0xd4, Rst5 = 0xd5, Rst6 = 0xd6, Rst7 = 0xd7, Sof0 = 0xc0, // SOFn Start of frame 0-1 common Sof1 = 0xc1, Sof2 = 0xc2, Sof3 = 0xc3, Dht = 0xc4, // Define Huffman Table Sof5 = 0xc5, Sof6 = 0xc6, Sof7 = 0xc7, Jpg = 0xc8, // reserved Sof9 = 0xc9, Sof10 = 0xca, Sof11 = 0xcb, Sof12 = 0xcc, Sof13 = 0xcd, Sof14 = 0xce, Sof15 = 0xcf, // These tags all consist of a marker and then a length. // These are the major structure tags. Soi = 0xd8, // Start of Image Eoi = 0xd9, // End of Image Sos = 0xda, // Start of Scan Dnl = 0xdc, Dri = 0xdd, // Define restart interval Dhp = 0xde, Exp = 0xdf, Dqt = 0xdb, // Define Quantization Table // These are the app marker tags that contain the application metadata // in its various forms. App0 = 0xe0, // AppN Markers for application data App1 = 0xe1, App2 = 0xe2, App3 = 0xe3, App4 = 0xe4, App5 = 0xe5, App6 = 0xe6, App7 = 0xe7, App8 = 0xe8, App9 = 0xe9, App10 = 0xea, App11 = 0xeb, App12 = 0xec, App13 = 0xed, App14 = 0xee, App15 = 0xef, Jpg0 = 0xf0, Jpg1 = 0xf1, Jpg2 = 0xf2, Jpg3 = 0xf3, Jpg4 = 0xf4, Jpg5 = 0xf5, Jpg6 = 0xf6, Jpg7 = 0xf7, Jpg8 = 0xf8, Jpg9 = 0xf9, Jpg10 = 0xfa, Jpg11 = 0xfb, Jpg12 = 0xfc, Jpg13 = 0xfd, Com = 0xfe // Comment } private System.Collections.ArrayList marker_list = new System.Collections.ArrayList (); private byte [] image_data; // False seems a safe default public bool Distinct { get { return false; } } public class Marker { public JpegMarker Type; public byte [] Data; public Marker (JpegMarker type, byte [] data) { this.Type = type; this.Data = data; } public bool IsApp { get { return (this.Type >= JpegMarker.App0 && this.Type <= JpegMarker.App15); } } public bool Matches (Signature sig) { if (Type == sig.Id) { if (sig.Name == null) return true; byte [] name = System.Text.Encoding.ASCII.GetBytes (sig.Name); for (int i = 0; i < name.Length; i++) if (Data [i] != name [i]) return false; return true; } return false; } public string GetName () { if (!this.IsApp) return null; int j; for (j = 0; j < this.Data.Length; j++) { if (this.Data [j] == 0x00) break; } if (j > 0) return System.Text.Encoding.ASCII.GetString (this.Data, 0, j); else return null; } private static int Read (Stream stream, byte [] dest, int start, int len) { int pos = 0; while (pos < len) { int read = stream.Read (dest, pos + start, len - pos); if (read <= 0) break; pos += read; } return pos; } public static Marker Load (Stream stream) { byte [] raw = new byte [2]; ushort length; if (stream.Length - stream.Position < 2) return null; // FIXME there is a potential loop here. int read = Read (stream, raw, 0, 2); if (read < 2 || raw [0] != 0xff) throw new System.Exception (System.String.Format ("Invalid marker found {0}", raw [0])); JpegMarker id = (JpegMarker) raw [1]; switch (id) { case JpegMarker.Soi: case JpegMarker.Eoi: case JpegMarker.Rst0: case JpegMarker.Rst1: case JpegMarker.Rst2: case JpegMarker.Rst3: case JpegMarker.Rst4: case JpegMarker.Rst5: case JpegMarker.Rst6: case JpegMarker.Rst7: case JpegMarker.Tem: case (JpegMarker) 0: return new Marker (id, null); default: Read (stream, raw, 0, 2); length = FSpot.BitConverter.ToUInt16 (raw, 0, false); byte [] data = new byte [length - 2]; Read (stream, data, 0, data.Length); return new Marker (id, data); } } public void Save (System.IO.Stream stream) { /* * It is possible we should just base this choice off the existance * of this.Data, but I'm not sure so I'll do it this way for now */ switch (this.Type) { case JpegMarker.Soi: case JpegMarker.Eoi: stream.WriteByte (0xff); stream.WriteByte ((byte)this.Type); break; default: stream.WriteByte (0xff); stream.WriteByte ((byte)this.Type); ushort length = (ushort)(this.Data.Length + 2); byte [] len = FSpot.BitConverter.GetBytes (length, false); stream.Write (len, 0, len.Length); stream.Write (this.Data, 0, this.Data.Length); break; } } } public static Signature JfifSignature = new Signature (JpegMarker.App0, "JFIF\0"); public static Signature JfxxSignature = new Signature (JpegMarker.App0, "JFXX\0"); public static Signature XmpSignature = new Signature (JpegMarker.App1, "http://ns.adobe.com/xap/1.0/\0"); public static Signature ExifSignature = new Signature (JpegMarker.App1, "Exif\0\0"); public static Signature IccProfileSignature = new Signature (JpegMarker.App2, "ICC_PROFILE\0"); public static Signature PhotoshopSignature = new Signature (JpegMarker.App13, "Photoshop 3.0\0"); public static Signature QuantizationSignature = new Signature (JpegMarker.Dqt, null); public class Signature { public JpegMarker Id; public string Name; public Signature (JpegMarker marker, string name) { Id = marker; Name = name; } public int WriteName (Stream stream) { byte [] sig = System.Text.Encoding.ASCII.GetBytes (Name); stream.Write (sig, 0, sig.Length); return sig.Length; } } public Marker FindMarker (Signature sig) { foreach (Marker m in Markers) { if (m.Matches (sig)) return m; } return null; } public Marker FindMarker (JpegMarker id, string name) { return FindMarker (new Signature (id, name)); } public Cms.Profile GetProfile () { Marker m = FindMarker (IccProfileSignature); string name = IccProfileSignature.Name; try { if (m != null) return new Cms.Profile (m.Data, name.Length, m.Data.Length - name.Length); } catch (System.Exception e) { System.Console.WriteLine (e); } FSpot.Tiff.Header exif = GetExifHeader (); if (exif != null) return exif.Directory.GetProfile (); return null; } public FSpot.Tiff.Header GetExifHeader () { string name = ExifSignature.Name; Marker marker = FindMarker (ExifSignature); if (marker == null) return null; using (System.IO.Stream exifstream = new System.IO.MemoryStream (marker.Data, name.Length, marker.Data.Length - name.Length, false)) { FSpot.Tiff.Header exif = new FSpot.Tiff.Header (exifstream); return exif; } } public XmpFile GetXmp () { string name = XmpSignature.Name; Marker marker = FindMarker (XmpSignature); if (marker != null) { int len = name.Length; //System.Console.WriteLine (System.Text.Encoding.ASCII.GetString (marker.Data, len, // marker.Data.Length - len)); using (System.IO.Stream xmpstream = new System.IO.MemoryStream (marker.Data, len, marker.Data.Length - len, false)) { XmpFile xmp = new XmpFile (xmpstream); return xmp; } } return null; } public void Select (SemWeb.StatementSink sink) { FSpot.Tiff.Header exif = GetExifHeader (); if (exif != null) exif.Select (sink); XmpFile xmp = GetXmp (); if (xmp != null) xmp.Select (sink); string name = PhotoshopSignature.Name; JpegHeader.Marker marker = FindMarker (PhotoshopSignature); if (marker != null) { int len = name.Length; using (System.IO.Stream bimstream = new System.IO.MemoryStream (marker.Data, len, marker.Data.Length - len, false)) { FSpot.Bim.BimFile bim = new FSpot.Bim.BimFile (bimstream); bim.Select (sink); } } } public Exif.ExifData Exif { get { Marker m = FindMarker (ExifSignature); if (m == null) return null; return new Exif.ExifData (m.Data); } } public void Replace (Signature sig, Marker data) { bool added = false; for (int i = 1; i < Markers.Count; i++) { Marker m = (Marker) Markers [i]; if (m.Matches (sig)) { if (!added) { Markers [i] = data; added = true; } else Markers.RemoveAt (i--); } else if (!m.IsApp || m.Type > sig.Id) { if (!added) { Markers.Insert (i, data); added = true; } } } if (!added) throw new System.Exception (String.Format ("unable to replace {0} marker", sig.Name)); } public void SetExif (Exif.ExifData value) { // Console.WriteLine ("before save"); byte [] raw_data = value.Save (); // Console.WriteLine ("saved"); Marker exif = new Marker (ExifSignature.Id, raw_data); // Console.WriteLine ("new"); Replace (ExifSignature, exif); // Console.WriteLine ("replaced"); } public void SetXmp (XmpFile xmp) { using (MemoryStream stream = new MemoryStream ()) { XmpSignature.WriteName (stream); xmp.Save (stream); Marker xmp_marker = new Marker (XmpSignature.Id, stream.ToArray ()); Replace (XmpSignature, xmp_marker); } } public System.Collections.ArrayList Markers { get { return marker_list; } } public void Save (System.IO.Stream stream) { foreach (Marker marker in marker_list) { // System.Console.WriteLine ("saving marker {0} {1}", marker.Type, // (marker.Data != null) ? marker.Data.Length .ToString (): "(null)"); marker.Save (stream); if (marker.Type == JpegMarker.Sos) stream.Write (ImageData, 0, ImageData.Length); } } public JpegHeader (System.IO.Stream stream) { Load (stream, false); } public JpegHeader (System.IO.Stream stream, bool metadata_only) { try { Load (stream, metadata_only); } catch (System.Exception e) { Console.WriteLine ("Exeption while reading jpeg headers"); Console.WriteLine(e); } } private void Load (System.IO.Stream stream, bool metadata_only) { marker_list.Clear (); image_data = null; bool at_image = false; Marker marker = Marker.Load (stream); if (marker.Type != JpegMarker.Soi) throw new System.Exception ("This doesn't appear to be a jpeg stream"); this.Markers.Add (marker); while (!at_image) { marker = Marker.Load (stream); if (marker == null) break; // System.Console.WriteLine ("loaded marker {0} length {1}", marker.Type, marker.Data.Length); this.Markers.Add (marker); if (marker.Type == JpegMarker.Sos) { at_image = true; if (metadata_only) { // System.Console.WriteLine ("read = {0}", stream.Position); return; } } } long image_data_length = stream.Length - stream.Position; this.image_data = new byte [image_data_length]; if (stream.Read (image_data, 0, (int)image_data_length) != image_data_length) throw new System.Exception ("truncated image data or something"); } static int [] StandardLuminanceQuantization = new int [] { 16, 11, 12, 14, 12, 10, 16, 14, 13, 14, 18, 17, 16, 19, 24, 40, 26, 24, 22, 22, 24, 49, 35, 37, 29, 40, 58, 51, 61, 60, 57, 51, 56, 55, 64, 72, 92, 78, 64, 68, 87, 69, 55, 56, 80, 109, 81, 87, 95, 98, 103, 104, 103, 62, 77, 113, 121, 112, 100, 120, 92, 101, 103, 99 }; static int [] StandardChrominanceQuantization = new int [] { 17, 18, 18, 24, 21, 24, 47, 26, 26, 47, 99, 66, 56, 66, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99 }; /* * GuessQuality is taken from the jpegdump utility * Copyright (c) 1992 Handmade Software, Inc. * by Allan N. Hessenflow licenced as GPL with the authors * permission. Many Thanks. */ public int GuessQuality () { Marker dqt = FindMarker (QuantizationSignature); int quality = 0; int position = 0; while (position < dqt.Data.Length) { int tableindex; int [] table = null; double cumsf = 0.0; double cumsf2 = 0.0; bool allones = true; int row, col; tableindex = dqt.Data [position ++]; switch (tableindex & 0x0f) { case 0: table = StandardLuminanceQuantization; break; case 1: table = StandardChrominanceQuantization; break; default: table = null; break; } for (row=0; row<8; row++) { for (col=0; col<8; col++) { uint val; if ((tableindex >> 4) > 0) { val = FSpot.BitConverter.ToUInt16 (dqt.Data, position, false); position += 2; } else val = (uint) dqt.Data [position ++]; if (table != null) { double x; /* scaling factor in percent */ x = 100.0 * (double)val / (double)table [row*8+col]; cumsf += x; cumsf2 += x * x; /* separate check for all-ones table (Q 100) */ if (val != 1) allones = false; } } } if (table != null) { double local_quality; cumsf /= 64.0; /* mean scale factor */ cumsf2 /= 64.0; //double variance; //variance = cumsf2 - (cumsf * cumsf); if (allones) /* special case for all-ones table */ local_quality = 100.0; else if (cumsf <= 100.0) local_quality = (200.0 - cumsf) / 2.0; else local_quality = 5000.0 / cumsf; quality = Math.Max (quality, (int)local_quality); } } return quality; } public byte [] ImageData { get { return image_data; } } #if ENABLE_NUNIT [TestFixture] public class Tests { int quality = 75; public string CreateFile () { Gdk.Pixbuf test = new Gdk.Pixbuf (null, "f-spot-32.png"); string path = FSpot.ImageFile.TempPath ("joe.jpg"); string desc = "\x00a9 Novell Inc."; PixbufOrientation orient = PixbufOrientation.TopRight; PixbufUtils.SaveJpeg (test, path, quality, new Exif.ExifData ()); FSpot.JpegFile jimg = new FSpot.JpegFile (path); jimg.SetDescription (desc); jimg.SetOrientation (orient); jimg.SaveMetaData (path); return path; } [Test] public void Load () { string path = CreateFile (); using (Stream stream = File.OpenRead (path)) { JpegHeader jhead = new JpegHeader (stream); Assert.AreEqual (((Marker)jhead.Markers [0]).Type, JpegMarker.Soi); Assert.AreEqual (((Marker)jhead.Markers [1]).GetName (), "JFIF"); Assert.AreEqual (((Marker)jhead.Markers [1]).Type, JpegMarker.App0); Assert.AreEqual (((Marker)jhead.Markers [2]).GetName (), "Exif"); Assert.AreEqual (((Marker)jhead.Markers [2]).Type, JpegMarker.App1); // NOTE the currently we don't store the Eoi as the last marker Assert.AreEqual (((Marker)jhead.Markers [jhead.Markers.Count -1]).Type, JpegMarker.Sos); // NOTE this is kind of sill but it might help Assert.IsTrue (Math.Abs (jhead.GuessQuality () - quality) <= 1); Assert.IsNotNull (jhead.GetExifHeader ()); } File.Delete (path); } [Test] public void Save () { string in_path = CreateFile (); string out_path = ImageFile.TempPath ("output.jpg"); JpegHeader source; JpegHeader dest; using (Stream orig = File.OpenRead (in_path)) { source = new JpegHeader (orig); using (Stream output = File.OpenWrite (out_path)) { source.Save (output); } using (Stream result = File.OpenRead (out_path)) { dest = new JpegHeader (result); Assert.AreEqual (source.Markers.Count, dest.Markers.Count); Assert.AreEqual (source.GuessQuality (), dest.GuessQuality ()); Assert.AreEqual (orig.Length, result.Length); for (int i = 0; i < source.Markers.Count; i++) { Marker d = (Marker) dest.Markers [i]; Marker s = (Marker) source.Markers [i]; Assert.AreEqual (d.Type, s.Type); Assert.AreEqual (d.GetName (), s.GetName ()); if (d.Data != null) { Assert.AreEqual (d.Data.Length, s.Data.Length); for (int j = 0; j < d.Data.Length; j++) { Assert.AreEqual (d.Data [j], s.Data [j]); } } else { Assert.AreEqual (d.Data, s.Data); } } } } File.Delete (in_path); File.Delete (out_path); } } #endif #if false public static int Main (string [] args) { JpegHeader data = new JpegHeader (args [0]); byte [] value = data.GetRawXmp (); if (value != null) { string xml = System.Text.Encoding.UTF8.GetString (value, 29, value.Length - 29); System.Console.WriteLine (xml); } value = data.GetRaw ("ICC_PROFILE"); if (value != null) { System.IO.FileStream stream = new System.IO.FileStream ("profile.icc", System.IO.FileMode.Create); stream.Write (value, 12, value.Length - 12); stream.Close (); } value = data.GetRawExif (); //System.IO.Stream ostream = System.IO.File.Open ("/home/lewing/test.jpg", System.IO.FileMode.OpenOrCreate); //data.Save (ostream); //ostream.Position = 0; //data = new JpegHeader (ostream); return 0; } #endif }