2
* Copyright (c) 2006 Novell Inc.
5
* Permission is hereby granted, free of charge, to any person obtaining a
6
* copy of this software and associated documentation files (the "Software"),
7
* to deal in the Software without restriction, including without limitation
8
* the rights to use, copy, modify, merge, publish, distribute, sublicense,
9
* and/or sell copies of the Software, and to permit persons to whom the
10
* Software is furnished to do so, subject to the following conditions:
12
* The above copyright notice and this permission notice shall be included in
13
* all copies or substantial portions of the Software.
15
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
20
* FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
21
* DEALINGS IN THE SOFTWARE.
27
using System.Collections;
32
using NUnit.Framework;
35
public class JpegHeader : SemWeb.StatementSource {
36
public enum JpegMarker {
38
Rst0 = 0xd0, // RstN used for resync, ignore
47
Sof0 = 0xc0, // SOFn Start of frame 0-1 common
52
Dht = 0xc4, // Define Huffman Table
58
Jpg = 0xc8, // reserved
68
// These tags all consist of a marker and then a length.
70
// These are the major structure tags.
71
Soi = 0xd8, // Start of Image
72
Eoi = 0xd9, // End of Image
73
Sos = 0xda, // Start of Scan
76
Dri = 0xdd, // Define restart interval
80
Dqt = 0xdb, // Define Quantization Table
82
// These are the app marker tags that contain the application metadata
83
// in its various forms.
84
App0 = 0xe0, // AppN Markers for application data
116
Com = 0xfe // Comment
119
private System.Collections.ArrayList marker_list = new System.Collections.ArrayList ();
120
private byte [] image_data;
122
// False seems a safe default
123
public bool Distinct {
124
get { return false; }
128
public class Marker {
129
public JpegMarker Type;
132
public Marker (JpegMarker type, byte [] data)
140
return (this.Type >= JpegMarker.App0 && this.Type <= JpegMarker.App15);
144
public bool Matches (Signature sig)
146
if (Type == sig.Id) {
147
if (sig.Name == null)
150
byte [] name = System.Text.Encoding.ASCII.GetBytes (sig.Name);
152
for (int i = 0; i < name.Length; i++)
153
if (Data [i] != name [i])
161
public string GetName ()
167
for (j = 0; j < this.Data.Length; j++) {
168
if (this.Data [j] == 0x00)
174
return System.Text.Encoding.ASCII.GetString (this.Data, 0, j);
179
private static int Read (Stream stream, byte [] dest, int start, int len)
184
int read = stream.Read (dest, pos + start, len - pos);
193
public static Marker Load (Stream stream) {
194
byte [] raw = new byte [2];
197
if (stream.Length - stream.Position < 2)
200
// FIXME there is a potential loop here.
202
int read = Read (stream, raw, 0, 2);
203
if (read < 2 || raw [0] != 0xff)
204
throw new System.Exception (System.String.Format ("Invalid marker found {0}", raw [0]));
206
JpegMarker id = (JpegMarker) raw [1];
210
case JpegMarker.Rst0:
211
case JpegMarker.Rst1:
212
case JpegMarker.Rst2:
213
case JpegMarker.Rst3:
214
case JpegMarker.Rst4:
215
case JpegMarker.Rst5:
216
case JpegMarker.Rst6:
217
case JpegMarker.Rst7:
220
return new Marker (id, null);
222
Read (stream, raw, 0, 2);
223
length = FSpot.BitConverter.ToUInt16 (raw, 0, false);
225
byte [] data = new byte [length - 2];
226
Read (stream, data, 0, data.Length);
227
return new Marker (id, data);
232
public void Save (System.IO.Stream stream) {
234
* It is possible we should just base this choice off the existance
235
* of this.Data, but I'm not sure so I'll do it this way for now
241
stream.WriteByte (0xff);
242
stream.WriteByte ((byte)this.Type);
245
stream.WriteByte (0xff);
246
stream.WriteByte ((byte)this.Type);
247
ushort length = (ushort)(this.Data.Length + 2);
249
byte [] len = FSpot.BitConverter.GetBytes (length, false);
250
stream.Write (len, 0, len.Length);
252
//workaround for mono bug: http://bugzilla.ximian.com/show_bug.cgi?id=82836
253
if (this.Data.Length > 0)
254
stream.Write (this.Data, 0, this.Data.Length);
260
public static Signature JfifSignature = new Signature (JpegMarker.App0, "JFIF\0");
261
public static Signature ComSignature = new Signature (JpegMarker.Com, null);
262
public static Signature JfxxSignature = new Signature (JpegMarker.App0, "JFXX\0");
263
public static Signature XmpSignature = new Signature (JpegMarker.App1, "http://ns.adobe.com/xap/1.0/\0");
264
public static Signature ExifSignature = new Signature (JpegMarker.App1, "Exif\0\0");
265
public static Signature IccProfileSignature = new Signature (JpegMarker.App2, "ICC_PROFILE\0");
266
public static Signature PhotoshopSignature = new Signature (JpegMarker.App13, "Photoshop 3.0\0");
267
public static Signature QuantizationSignature = new Signature (JpegMarker.Dqt, null);
269
public class Signature {
270
public JpegMarker Id;
273
public Signature (JpegMarker marker, string name)
279
public int WriteName (Stream stream)
281
byte [] sig = System.Text.Encoding.ASCII.GetBytes (Name);
282
stream.Write (sig, 0, sig.Length);
287
public Marker FindMarker (Signature sig)
289
foreach (Marker m in Markers) {
298
public Marker FindMarker (JpegMarker id, string name)
300
return FindMarker (new Signature (id, name));
304
public Cms.Profile GetProfile ()
306
Marker m = FindMarker (IccProfileSignature);
307
string name = IccProfileSignature.Name;
310
return new Cms.Profile (m.Data, name.Length, m.Data.Length - name.Length);
311
} catch (System.Exception e) {
312
//System.Console.WriteLine (e);
315
FSpot.Tiff.Header exif = GetExifHeader ();
317
return exif.Directory.GetProfile ();
322
public FSpot.Tiff.Header GetExifHeader ()
324
string name = ExifSignature.Name;
325
Marker marker = FindMarker (ExifSignature);
330
using (System.IO.Stream exifstream = new System.IO.MemoryStream (marker.Data, name.Length, marker.Data.Length - name.Length, false)) {
331
FSpot.Tiff.Header exif = new FSpot.Tiff.Header (exifstream);
336
public string GetJFIFComment ()
338
string name = ComSignature.Name;
339
Marker marker = FindMarker (ComSignature);
344
if (marker.Data != null && marker.Data.Length != 0)
345
return System.Text.Encoding.Default.GetString (marker.Data, 0, marker.Data.Length);
350
public XmpFile GetXmp ()
352
string name = XmpSignature.Name;
353
Marker marker = FindMarker (XmpSignature);
354
if (marker != null) {
355
int len = name.Length;
356
//System.Console.WriteLine (System.Text.Encoding.ASCII.GetString (marker.Data, len,
357
// marker.Data.Length - len));
358
using (System.IO.Stream xmpstream = new System.IO.MemoryStream (marker.Data, len,
359
marker.Data.Length - len, false)) {
361
XmpFile xmp = new XmpFile (xmpstream);
368
public void Select (SemWeb.StatementSink sink)
370
FSpot.Tiff.Header exif = GetExifHeader ();
374
XmpFile xmp = GetXmp ();
378
string name = PhotoshopSignature.Name;
379
JpegHeader.Marker marker = FindMarker (PhotoshopSignature);
380
if (marker != null) {
381
int len = name.Length;
382
using (System.IO.Stream bimstream = new System.IO.MemoryStream (marker.Data, len, marker.Data.Length - len, false)) {
383
FSpot.Bim.BimFile bim = new FSpot.Bim.BimFile (bimstream);
389
public FSpot.Iptc.IptcFile GetIptc ()
391
string name = PhotoshopSignature.Name;
392
JpegHeader.Marker marker = FindMarker (PhotoshopSignature);
393
if (marker != null) {
394
int len = name.Length;
395
using (System.IO.Stream bimstream = new System.IO.MemoryStream (marker.Data, len,
396
marker.Data.Length - len, false)) {
398
FSpot.Bim.BimFile bim;
400
bim = new FSpot.Bim.BimFile (bimstream);
402
// Bim entry with marker "PHUT" is not handled by Bim.cs
406
// FIXME: What about EntryType.XMP ?
407
FSpot.Bim.Entry iptc_entry = bim.FindEntry (FSpot.Bim.EntryType.IPTCNAA);
408
if (iptc_entry == null)
411
using (System.IO.Stream iptcstream = new System.IO.MemoryStream (iptc_entry.Data)) {
412
FSpot.Iptc.IptcFile iptc = new FSpot.Iptc.IptcFile (iptcstream);
420
public Exif.ExifData Exif {
422
Marker m = FindMarker (ExifSignature);
427
return new Exif.ExifData (m.Data);
431
public void Replace (Signature sig, Marker data)
434
for (int i = 1; i < Markers.Count; i++) {
435
Marker m = (Marker) Markers [i];
437
if (m.Matches (sig)) {
443
Markers.RemoveAt (i--);
445
} else if (!m.IsApp || m.Type > sig.Id) {
447
Markers.Insert (i, data);
454
throw new System.Exception (String.Format ("unable to replace {0} marker", sig.Name));
457
public void SetExif (Exif.ExifData value)
459
// Console.WriteLine ("before save");
460
byte [] raw_data = value.Save ();
461
// Console.WriteLine ("saved");
462
Marker exif = new Marker (ExifSignature.Id, raw_data);
463
// Console.WriteLine ("new");
464
Replace (ExifSignature, exif);
465
// Console.WriteLine ("replaced");
468
public void SetXmp (XmpFile xmp)
470
using (MemoryStream stream = new MemoryStream ()) {
472
XmpSignature.WriteName (stream);
475
Marker xmp_marker = new Marker (XmpSignature.Id, stream.ToArray ());
476
Replace (XmpSignature, xmp_marker);
480
public System.Collections.ArrayList Markers {
486
public void Save (System.IO.Stream stream)
488
foreach (Marker marker in marker_list) {
489
// System.Console.WriteLine ("saving marker {0} {1}", marker.Type,
490
// (marker.Data != null) ? marker.Data.Length .ToString (): "(null)");
491
marker.Save (stream);
492
if (marker.Type == JpegMarker.Sos)
493
stream.Write (ImageData, 0, ImageData.Length);
497
public JpegHeader (System.IO.Stream stream)
499
Load (stream, false);
502
public JpegHeader (System.IO.Stream stream, bool metadata_only)
505
Load (stream, metadata_only);
506
} catch (System.Exception e) {
507
Console.WriteLine ("Exeption while reading jpeg headers");
508
Console.WriteLine(e);
512
private void Load (System.IO.Stream stream, bool metadata_only)
514
marker_list.Clear ();
516
bool at_image = false;
518
Marker marker = Marker.Load (stream);
519
if (marker == null || marker.Type != JpegMarker.Soi)
520
throw new System.Exception ("This doesn't appear to be a jpeg stream");
522
this.Markers.Add (marker);
524
marker = Marker.Load (stream);
529
// System.Console.WriteLine ("loaded marker {0} length {1}", marker.Type, marker.Data.Length);
531
this.Markers.Add (marker);
533
if (marker.Type == JpegMarker.Sos) {
537
// System.Console.WriteLine ("read = {0}", stream.Position);
543
long image_data_length = stream.Length - stream.Position;
544
this.image_data = new byte [image_data_length];
546
if (stream.Read (image_data, 0, (int)image_data_length) != image_data_length)
547
throw new System.Exception ("truncated image data or something");
550
static int [] StandardLuminanceQuantization = new int [] {
551
16, 11, 12, 14, 12, 10, 16, 14,
552
13, 14, 18, 17, 16, 19, 24, 40,
553
26, 24, 22, 22, 24, 49, 35, 37,
554
29, 40, 58, 51, 61, 60, 57, 51,
555
56, 55, 64, 72, 92, 78, 64, 68,
556
87, 69, 55, 56, 80, 109, 81, 87,
557
95, 98, 103, 104, 103, 62, 77, 113,
558
121, 112, 100, 120, 92, 101, 103, 99
561
static int [] StandardChrominanceQuantization = new int [] {
562
17, 18, 18, 24, 21, 24, 47, 26,
563
26, 47, 99, 66, 56, 66, 99, 99,
564
99, 99, 99, 99, 99, 99, 99, 99,
565
99, 99, 99, 99, 99, 99, 99, 99,
566
99, 99, 99, 99, 99, 99, 99, 99,
567
99, 99, 99, 99, 99, 99, 99, 99,
568
99, 99, 99, 99, 99, 99, 99, 99,
569
99, 99, 99, 99, 99, 99, 99, 99
573
* GuessQuality is taken from the jpegdump utility
574
* Copyright (c) 1992 Handmade Software, Inc.
575
* by Allan N. Hessenflow licenced as GPL with the authors
576
* permission. Many Thanks.
578
public int GuessQuality ()
580
Marker dqt = FindMarker (QuantizationSignature);
584
while (position < dqt.Data.Length) {
592
tableindex = dqt.Data [position ++];
594
switch (tableindex & 0x0f) {
596
table = StandardLuminanceQuantization;
599
table = StandardChrominanceQuantization;
606
for (row=0; row<8; row++) {
607
for (col=0; col<8; col++) {
610
if ((tableindex >> 4) > 0) {
611
val = FSpot.BitConverter.ToUInt16 (dqt.Data, position, false);
614
val = (uint) dqt.Data [position ++];
619
/* scaling factor in percent */
620
x = 100.0 * (double)val / (double)table [row*8+col];
624
/* separate check for all-ones table (Q 100) */
632
double local_quality;
634
cumsf /= 64.0; /* mean scale factor */
638
//variance = cumsf2 - (cumsf * cumsf);
640
if (allones) /* special case for all-ones table */
641
local_quality = 100.0;
642
else if (cumsf <= 100.0)
643
local_quality = (200.0 - cumsf) / 2.0;
645
local_quality = 5000.0 / cumsf;
647
quality = Math.Max (quality, (int)local_quality);
653
public byte [] ImageData {
664
public string CreateFile ()
666
Gdk.Pixbuf test = new Gdk.Pixbuf (null, "f-spot-32.png");
667
string path = FSpot.ImageFile.TempPath ("joe.jpg");
668
string desc = "\x00a9 Novell Inc.";
669
PixbufOrientation orient = PixbufOrientation.TopRight;
671
PixbufUtils.SaveJpeg (test, path, quality, new Exif.ExifData ());
672
FSpot.JpegFile jimg = new FSpot.JpegFile (path);
673
jimg.SetDescription (desc);
674
jimg.SetOrientation (orient);
675
jimg.SaveMetaData (path);
683
string path = CreateFile ();
685
using (Stream stream = File.OpenRead (path)) {
686
JpegHeader jhead = new JpegHeader (stream);
688
Assert.AreEqual (((Marker)jhead.Markers [0]).Type, JpegMarker.Soi);
689
Assert.AreEqual (((Marker)jhead.Markers [1]).GetName (), "JFIF");
690
Assert.AreEqual (((Marker)jhead.Markers [1]).Type, JpegMarker.App0);
691
Assert.AreEqual (((Marker)jhead.Markers [2]).GetName (), "Exif");
692
Assert.AreEqual (((Marker)jhead.Markers [2]).Type, JpegMarker.App1);
694
// NOTE the currently we don't store the Eoi as the last marker
695
Assert.AreEqual (((Marker)jhead.Markers [jhead.Markers.Count -1]).Type, JpegMarker.Sos);
697
// NOTE this is kind of sill but it might help
698
Assert.IsTrue (Math.Abs (jhead.GuessQuality () - quality) <= 1);
700
Assert.IsNotNull (jhead.GetExifHeader ());
709
string in_path = CreateFile ();
710
string out_path = ImageFile.TempPath ("output.jpg");
714
using (Stream orig = File.OpenRead (in_path)) {
715
source = new JpegHeader (orig);
717
using (Stream output = File.OpenWrite (out_path)) {
718
source.Save (output);
721
using (Stream result = File.OpenRead (out_path)) {
722
dest = new JpegHeader (result);
724
Assert.AreEqual (source.Markers.Count, dest.Markers.Count);
725
Assert.AreEqual (source.GuessQuality (), dest.GuessQuality ());
726
Assert.AreEqual (orig.Length, result.Length);
727
for (int i = 0; i < source.Markers.Count; i++) {
728
Marker d = (Marker) dest.Markers [i];
729
Marker s = (Marker) source.Markers [i];
731
Assert.AreEqual (d.Type, s.Type);
732
Assert.AreEqual (d.GetName (), s.GetName ());
734
if (d.Data != null) {
735
Assert.AreEqual (d.Data.Length, s.Data.Length);
737
for (int j = 0; j < d.Data.Length; j++) {
738
Assert.AreEqual (d.Data [j], s.Data [j]);
741
Assert.AreEqual (d.Data, s.Data);
747
File.Delete (in_path);
748
File.Delete (out_path);
754
public static int Main (string [] args)
756
JpegHeader data = new JpegHeader (args [0]);
757
byte [] value = data.GetRawXmp ();
760
string xml = System.Text.Encoding.UTF8.GetString (value, 29, value.Length - 29);
761
//System.Console.WriteLine (xml);
764
value = data.GetRaw ("ICC_PROFILE");
766
System.IO.FileStream stream = new System.IO.FileStream ("profile.icc", System.IO.FileMode.Create);
767
stream.Write (value, 12, value.Length - 12);
771
value = data.GetRawExif ();
774
//System.IO.Stream ostream = System.IO.File.Open ("/home/lewing/test.jpg", System.IO.FileMode.OpenOrCreate);
775
//data.Save (ostream);
776
//ostream.Position = 0;
777
//data = new JpegHeader (ostream);