Montag, 16. April 2018

Wie verwirrend! How confusing! Defaults in TIFF

Hint: english version below :)

Erste Überlegung: Hä?

Ernsthaft? Was soll denn an den Defaults von TIFF so problematisch sein? Steht doch alles in der Spezifikation. Es gilt:
  1. Enthält ein TIFF ein Tag nicht, für das ein Default definiert ist, gilt der Default.
  2. Wenn ein TIFF ein Tag enthält, gilt der Wert des Tags.
  3. Sonst gilt, der Wert ist nicht definiert und demnach nicht vorhanden.

Der zweite Blick

Leider ist es in der Praxis komplizierter. Ich bekam die Frage, wenn jhove bei der Prüfung der von checkit_tiff mitgelieferten Beispiel-TIFFs für das Thresholding-Tag 263 den Wert "1" ausgibt:
$> jhove tiffs_should_pass/minimal_valid_baseline.tiff
Jhove (Rel. 1.6, 2011-01-04)
 Date: 2018-04-16 12:41:25 MESZ
 RepresentationInformation: tiffs_should_pass/minimal_valid_baseline.tiff
  ReportingModule: TIFF-hul, Rel. 1.5 (2007-10-02)
  LastModified: 2017-07-14 11:28:57 MESZ
  Size: 323
  Format: TIFF
  Version: 5.0
  Status: Well-Formed and valid
  SignatureMatches:
   TIFF-hul
  MIMEtype: image/tiff
  Profile: Baseline bilevel (Class B), TIFF/IT-BP (ISO 12639:1998), TIFF/IT-BP/P1 (ISO 12639:1998), TIFF/IT-BP/P2 (ISO 12639:1998), TIFF/IT-MP (ISO 12639:1998)
  TIFFMetadata:
   ByteOrder: little-endian
   IFDs:
    Number: 1
    IFD:
     Offset: 38
     Type: TIFF
     Entries:
      NisoImageMetadata:
       ByteOrder: little_endian
       CompressionScheme: uncompressed
       ImageWidth: 20
       ImageHeight: 10
       ColorSpace: white is zero
       Orientation: normal
       SamplingFrequencyUnit: inch
       XSamplingFrequency: 376,193
       YSamplingFrequency: 376,193
       BitsPerSample: 1
       BitsPerSampleUnit: integer
       SamplesPerPixel: 1
      NewSubfileType: 0
      SampleFormat: 1
      MinSampleValue: 0
      MaxSampleValue: 1
      Threshholding: 1
      TIFFITProperties:
       BackgroundColorIndicator: background not defined
       ImageColorIndicator: image not defined
       TransparencyIndicator: no transparency
       PixelIntensityRange: 0, 1
       RasterPadding: 1 byte
       BitsPerRunLength: 8
       BitsPerExtendedRunLength: 16
aber checkit_tiff mit dem beigefügten Beispiel keinen Fehler wirft, obwohl doch keine Positiv-Regel in der Konfigurationsdatei hinterlegt ist:
$> checkit_tiff example_configs/cit_tiff6_baseline_SLUB.cfg tiffs_should_pass/minimal_valid_baseline.tiff
'./build/checkit_tiff' version: development_v0.4.0
    revision: 408
licensed under conditions of libtiff (see http://libtiff.maptools.org/misc.html)
cfg_file=example_configs/cit_tiff6_baseline_SLUB.cfg
tiff file/dir=tiffs_should_pass/minimal_valid_baseline.tiff
file: tiffs_should_pass/minimal_valid_baseline.tiff
(./)    general    --> TIFF should have just one IFD, (lineno: 12)
(./)    general    --> All tag offsets should be word aligned, (lineno: 14)
(./)    general    --> All offsets may only be used once, (lineno: 14)
(./)    general    --> All tag offsets should be greater than zero, (lineno: 14)
(./)    general    --> All IFDs should be word aligned, (lineno: 15)
(./)    general    --> Tags should be sorted in ascending order, (lineno: 15)
(./)    tag 256 (ImageWidth)    --> Tag should have a value in a range of (lineno: 23)
(./)    tag 257 (ImageLength)    --> Tag should have a value in a range of (lineno: 25)
(./)    tag 258 (BitsPerSample)    --> One or more conditions needs to be combined in a logical_or operation (open) (lineno: 30)
(./)    tag 259 (Compression)    --> Tag should have one exact value. (lineno: 36)
(./)    tag 262 (Photometric)    --> Tag should have a value in a range of (lineno: 40)
(./)    tag 273 (StripOffsets)    --> TIFF should contain this tag. (lineno: 45)
(./)    tag 277 (SamplesPerPixel)    --> Tag should have one exact value. (lineno: 52)
(./)    tag 278 (RowsPerStrip)    --> Tag should have a value in a range of (lineno: 55)
(./)    tag 279 (StripByteCounts)    --> TIFF should contain this tag. (lineno: 60)
(./)    tag 282 (XResolution)    --> Tag should have a value in a range of (lineno: 63)
(./)    tag 283 (YResolution)    --> Tag should have a value in a range of (lineno: 66)
(./)    tag 296 (ResolutionUnit)    --> Tag should have one exact value. (lineno: 69)
(./)    tag 254 (SubFileType)    --> One or more conditions needs to be combined in a logical_or operation (open) (lineno: 77)
(./)    tag 274 (Orientation)    --> Tag should have one exact value. (lineno: 113)
(./)    tag 284 (PlanarConfig)    --> Tag should have one exact value. (lineno: 122)
(./)
(./)Yes, the given tif is valid :)
Zuerst war ich etwas erschrocken, war ich mir doch sicher, dass checkit_tiff funktioniert und ich alles sorgfältig geprüft hatte. Zur Sicherheit habe ich die Ausgabe mit tiffdump der libtiff geprüft:
$> tiffdump tiffs_should_pass/minimal_valid_baseline.tifftiffs_should_pass/minimal_valid_baseline.tiff:
Magic: 0x4949 <little-endian> Version: 0x2a <ClassicTIFF>
Directory 0: offset 38 (0x26) next 0 (0)
SubFileType (254) LONG (4) 1<0>
ImageWidth (256) SHORT (3) 1<20>
ImageLength (257) SHORT (3) 1<10>
BitsPerSample (258) SHORT (3) 1<1>
Compression (259) SHORT (3) 1<1>
Photometric (262) SHORT (3) 1<0>
StripOffsets (273) LONG (4) 1<8>
Orientation (274) SHORT (3) 1<1>
SamplesPerPixel (277) SHORT (3) 1<1>
RowsPerStrip (278) SHORT (3) 1<64>
StripByteCounts (279) LONG (4) 1<30>
XResolution (282) RATIONAL (5) 1<376.193>
YResolution (283) RATIONAL (5) 1<376.193>
PlanarConfig (284) SHORT (3) 1<1>
ResolutionUnit (296) SHORT (3) 1<2>
Gut, tiffdump war auf meiner Seite. Was ist also der Grund für diese Diskrepanz? Schauen wir zuerst in die TIFF-6.0 Spezifikation, dort steht auf Seite 41:
For black and white TIFF files that represent shades of gray, the technique used to
convert from gray to black and white pixels.
Tag = 263 (107.H)
Type = SHORT
N = 1
1 = No dithering or halftoning has been applied to the image data.
2 = An ordered dither or halftone technique has been applied to the image data.
3 = A randomized process such as error diffusion has been applied to the image data.
Default is Threshholding = 1. See also CellWidth, CellLength.
Okay. Für das oben benutzte TIFF trifft zu, dass es schwarz-weiß ist und kein Tag 263 enthält. Daher wird der Default = 1 angenommen.

Jhove präsentiert die Metadaten der TIFF-Dateien also so, wie ein TIFF-Reader sie interpretieren würde. Die Tools checkit_tiff und tiffdump zeigen dagegen, welche TIFF-Tags mit welchen Werten tatsächlich in den TIFF-Dateien explizit kodiert sind.

Fazit

Kenne Deine Tools! Statt Default-Werte zu interpretieren, sollten solche Annahmen explizit gekennzeichnet werden. Für den Durchschnittsanwender ist sonst nicht ersichtlich, wie die Ergebnisse zustande kommen. Als Lektion für checkit_tiff nehme ich diese Frage mit in die FAQ auf.






First thought: WTF?

Seriously? What's supposed to be so problematic about TIFF's defaults? After all, the Spezifikation says it all. The rules are:
  1. If a TIFF does not contain a tag that has a well-defined default value, then that default value is used.
  2. If a TIFF does contain a tag, then that tag's value is used.
  3. In all other cases, the value is undefined and hence nonexistent.

Der zweite Blick

Unfortunately, the real world is a little more complicated. I was asked why jhove would give a value of "1" for the Thresholding tag 263 when validating TIFF-examples that are delivered with checkit_tiff as shown below:
$> jhove tiffs_should_pass/minimal_valid_baseline.tiff
Jhove (Rel. 1.6, 2011-01-04)
 Date: 2018-04-16 12:41:25 MESZ
 RepresentationInformation: tiffs_should_pass/minimal_valid_baseline.tiff
  ReportingModule: TIFF-hul, Rel. 1.5 (2007-10-02)
  LastModified: 2017-07-14 11:28:57 MESZ
  Size: 323
  Format: TIFF
  Version: 5.0
  Status: Well-Formed and valid
  SignatureMatches:
   TIFF-hul
  MIMEtype: image/tiff
  Profile: Baseline bilevel (Class B), TIFF/IT-BP (ISO 12639:1998), TIFF/IT-BP/P1 (ISO 12639:1998), TIFF/IT-BP/P2 (ISO 12639:1998), TIFF/IT-MP (ISO 12639:1998)
  TIFFMetadata:
   ByteOrder: little-endian
   IFDs:
    Number: 1
    IFD:
     Offset: 38
     Type: TIFF
     Entries:
      NisoImageMetadata:
       ByteOrder: little_endian
       CompressionScheme: uncompressed
       ImageWidth: 20
       ImageHeight: 10
       ColorSpace: white is zero
       Orientation: normal
       SamplingFrequencyUnit: inch
       XSamplingFrequency: 376,193
       YSamplingFrequency: 376,193
       BitsPerSample: 1
       BitsPerSampleUnit: integer
       SamplesPerPixel: 1
      NewSubfileType: 0
      SampleFormat: 1
      MinSampleValue: 0
      MaxSampleValue: 1
      Threshholding: 1
      TIFFITProperties:
       BackgroundColorIndicator: background not defined
       ImageColorIndicator: image not defined
       TransparencyIndicator: no transparency
       PixelIntensityRange: 0, 1
       RasterPadding: 1 byte
       BitsPerRunLength: 8
       BitsPerExtendedRunLength: 16
However, checkit_tiff does not throw an error while validating the same sample file, even though there's no whitelist rule for that tag in the config file:
$> checkit_tiff example_configs/cit_tiff6_baseline_SLUB.cfg tiffs_should_pass/minimal_valid_baseline.tiff
'./build/checkit_tiff' version: development_v0.4.0
    revision: 408
licensed under conditions of libtiff (see http://libtiff.maptools.org/misc.html)
cfg_file=example_configs/cit_tiff6_baseline_SLUB.cfg
tiff file/dir=tiffs_should_pass/minimal_valid_baseline.tiff
file: tiffs_should_pass/minimal_valid_baseline.tiff
(./)    general    --> TIFF should have just one IFD, (lineno: 12)
(./)    general    --> All tag offsets should be word aligned, (lineno: 14)
(./)    general    --> All offsets may only be used once, (lineno: 14)
(./)    general    --> All tag offsets should be greater than zero, (lineno: 14)
(./)    general    --> All IFDs should be word aligned, (lineno: 15)
(./)    general    --> Tags should be sorted in ascending order, (lineno: 15)
(./)    tag 256 (ImageWidth)    --> Tag should have a value in a range of (lineno: 23)
(./)    tag 257 (ImageLength)    --> Tag should have a value in a range of (lineno: 25)
(./)    tag 258 (BitsPerSample)    --> One or more conditions needs to be combined in a logical_or operation (open) (lineno: 30)
(./)    tag 259 (Compression)    --> Tag should have one exact value. (lineno: 36)
(./)    tag 262 (Photometric)    --> Tag should have a value in a range of (lineno: 40)
(./)    tag 273 (StripOffsets)    --> TIFF should contain this tag. (lineno: 45)
(./)    tag 277 (SamplesPerPixel)    --> Tag should have one exact value. (lineno: 52)
(./)    tag 278 (RowsPerStrip)    --> Tag should have a value in a range of (lineno: 55)
(./)    tag 279 (StripByteCounts)    --> TIFF should contain this tag. (lineno: 60)
(./)    tag 282 (XResolution)    --> Tag should have a value in a range of (lineno: 63)
(./)    tag 283 (YResolution)    --> Tag should have a value in a range of (lineno: 66)
(./)    tag 296 (ResolutionUnit)    --> Tag should have one exact value. (lineno: 69)
(./)    tag 254 (SubFileType)    --> One or more conditions needs to be combined in a logical_or operation (open) (lineno: 77)
(./)    tag 274 (Orientation)    --> Tag should have one exact value. (lineno: 113)
(./)    tag 284 (PlanarConfig)    --> Tag should have one exact value. (lineno: 122)
(./)
(./)Yes, the given tif is valid :)
Being sure that checkit_tiff works as expected and that I had checked everything, I was shocked at first. To err on the side of safety, I ran a crosscheck of checkit_tiff's output with the output of the tiffdump tool from the libtiff:
$> tiffdump tiffs_should_pass/minimal_valid_baseline.tifftiffs_should_pass/minimal_valid_baseline.tiff:
Magic: 0x4949 <little-endian> Version: 0x2a <ClassicTIFF>
Directory 0: offset 38 (0x26) next 0 (0)
SubFileType (254) LONG (4) 1<0>
ImageWidth (256) SHORT (3) 1<20>
ImageLength (257) SHORT (3) 1<10>
BitsPerSample (258) SHORT (3) 1<1>
Compression (259) SHORT (3) 1<1>
Photometric (262) SHORT (3) 1<0>
StripOffsets (273) LONG (4) 1<8>
Orientation (274) SHORT (3) 1<1>
SamplesPerPixel (277) SHORT (3) 1<1>
RowsPerStrip (278) SHORT (3) 1<64>
StripByteCounts (279) LONG (4) 1<30>
XResolution (282) RATIONAL (5) 1<376.193>
YResolution (283) RATIONAL (5) 1<376.193>
PlanarConfig (284) SHORT (3) 1<1>
ResolutionUnit (296) SHORT (3) 1<2>
Well, tiffdump was in my team there. So, what's the reason for that discrepancy? First, let's have a loot at the TIFF-6.0 Spezifikation. On page 41, the specification states:
For black and white TIFF files that represent shades of gray, the technique used to
convert from gray to black and white pixels.
Tag = 263 (107.H)
Type = SHORT
N = 1
1 = No dithering or halftoning has been applied to the image data.
2 = An ordered dither or halftone technique has been applied to the image data.
3 = A randomized process such as error diffusion has been applied to the image data.
Default is Threshholding = 1. See also CellWidth, CellLength.
Okay. Looking at the sample TIFF we used above, it's true that it's a black-and-white image and does not contain tag 263. Hence, a default = 1 is assumed.

Apparently, Jhove will present the metadata in the TIF files in a way that a TIF reader would interpret them. The tools checkit_tiff and tiffdump however show which TIF tags are actually explicitely encoded in the TIFFs and what values they have.

Wrap-up

Know your tools!Instead of interpreting default values, these kinds of exceptions need to be cleary marked. Otherwise, the genesis of these results might not be apparent to the average user.
I have learned learned my lesson and will include this question into the checkit_tiff FAQ.

Montag, 26. Februar 2018

Valid TIFFs need love, too.

(english version below)

Über einen Kollegen haben wir ein interessantes TIFF erhalten. Es hatte alle Validierungen bestanden und zeigte keine strukturellen Fehler in tiffinfo/tiffdump, ließ sich aber trotzdem im Vorschaubetrachter des Workflowtools nicht anzeigen. Außerdem war es ca. dreimal so groß wie alle anderen Scans aus dem gleichen Vorgang. Er bat uns, das TIFF zu untersuchen.

Im Gegensatz zu ihm habe ich keine Probleme damit gehabt, das TIFF überhaupt zu öffnen; der Windows-Bildbetrachter, IrfanView, MS Paint, Paint.NET und XnViewMP stellten alle das Bild dar. Allerdings war es in der Horizontalen stark gestreckt, d.h. deutlich breiter als erwartet. Große Teile des Bildinhaltes (eine gescannte Zeitschriftenseite) fehlten, und der rechte Rand war nicht sichtbar. 

kaputte Anzeige des TIFFs

In tiffinfo sahen wir, dass das TIFF ein Grayscale-Image ist:
Bits/Sample: 8
Samples/Pixel: 1

Auffällig war, dass die Listeneinträge für StripByteCounts genau um Faktor 3 größer als die ImageWidth waren (4302 * 3 = 12906); das erklärte die Streckung des Bildes in X-Richtung. Man sah außerdem, dass die StripOffsets in Schritten von 12906 Bytes anwuchsen; vermutlich war der Viewer deswegen überhaupt in der Lage, irgendein Bild anzuzeigen. Die ImageLength stimmte mit der Anzahl der Einträge in StripByteCount überein (6020), deshalb gab es hier keine Verzerrung.
Image Width: 4302
Image Length: 6020
StripByteCounts (279) LONG (4) 6020<12906 12906 12906 12906 12906 12906 12906 12906 12906 12906 12906 12906 12906 12906 12906 12906 12906 12906 12906 12906 12906 12906 12906 12906 ...> StripOffsets (273) LONG (4) 6020<8 12914 25820 38726 ...>

In Okteta konnten wir sehen, dass die Bilddaten für jedes Pixel dreimal identisch gespeichert waren. Das deckt sich der Aussage des Kollegen, dass das Bild ca. dreimal größer war als alle anderen Scans im gleichen Vorgang. Außerdem haben wir gesehen, dass das IFD0 am Dateiende stand und Hinweise auf Bearbeitungen mit IrfanView enthielt.

normales RGB-TIFF

defektes TIFF mit zwei Bytes redundanten Grayscale-Daten je Pixel


Nachdem wir das Problem verstanden hatten, haben wir Reparaturmöglichkeiten diskutiert:
- Man könnte die Redundanz der Pixel entfernen und die StripOffsets (und wahrscheinlich noch andere Offsets) anpassen. Das wäre wahrscheinlich die sauberere Lösung, müsste aber definitiv mit Softwareunterstützung getan werden.
- Man könnte die SamplesPerPixel auf "3" setzen, um die drei duplizierten Bytes je Pixel als RGB-Kanäle zu interpretieren und damit drei Bytes zu einem Pixel im Bild zusammenzufassen. Das haben wir getan, und es hat funktioniert; zumindest war das Bild anzeigbar, nicht gestaucht und nicht in ausgefallene Farben getaucht.

Zur Ursache des Fehlers gab es nun zwei Theorien:
- Es könnte einen Bitflip gegeben haben, bei dem SamplesPerPixel beschädigt wurde: der Weg von "00 11"B ("0 3" D) zu "00 01"B ("0 1" D) ist nicht weit und würde das Fehlerbild erklären.
- Es könnte einen Fehler bei der Konvertierung eines RGB-Scans von einer Grayscale-Vorlage gegeben haben, bei dem die überzähligen Bytes pro Pixel nicht entfernt wurden. Das SamplesPerPixel Tag wäre dabei korrekt und absichtlich gesetzt worden.

Als erstes haben wir nun also SamplesPerPixel im Hex-Editor auf "3" gesetzt, um den TIFF-Viewer anzuweisen, die Bilddaten als RGB-Bild zu interpretieren. Schon diese kleine Änderung bewirkte, dass sich das Bild fehlerfrei anzeigen ließ. Der Umstand, dass das Bild ungewöhnlich groß war (wir hatten erwartet, dass es ähnlich groß wäre wie die anderen Scans aus der gleichen Zeitschrift), blieb aber vorerst ungeklärt.

defektes Grayscale-TIFF, als RGB interpretiert


korrekte Anzeige des TIFFs

Wir erwägen, eine Plausibilitätsprüfung für diesen Fehlertyp in checkit_tiff zu implementieren, sofern man davon ausgeht, dass innerhalb eines Bildes alle Strips gleich lang sind. Dazu verwendet man die Formel: "StripByteCounts / SamplesPerPixel / RowsPerStrip = ImageWidth". Am einfachsten funktioniert das mit TIFFs, bei denen RowsPerStrip = 1 ist; andernfalls müssen zusätzlich komplexere Prüfungen durchgeführt werden, weil bei mehrzeiligen Strips, deren Bytelänge nicht ohne Rest ganzzahlig durch die Zeilenanzahl teilbar ist, kein Padding angefügt wird. Dadurch können Rows entstehen, die kürzer sind als die vorderen Rows eines Strips. 

Zusätzlich denkbare Plausibilitätsprüfungen wären:
- Die Höhe des Bildes ist genau so lang wie das Produkt aus RowsPerStrip und Anzahl der Strips: ImageLength = RowsPerStrip * StripOffsets.Count
- Jeder StripByteCount muss so groß sein wie die Differenz der dazugehörigen StripByteOffsets: StripByteCounts[0] = StripOffsets[1] - StripOffsets[0] (bzw. allgemeiner StripByteCounts[n] = StripOffsets[n+1] - StripOffsets[n])
- Jeder Strip muss gleich lang sein: StripByteCounts[0] = StripByteCounts[1] = StripByteCounts[2] = ... = StripByteCounts[n]

Diese Möglichkeiten haben wir im größeren Kreis diskutiert, was Andreas neugierig gemacht hat. Er hat also sein neues Tool zum Finden möglicher ehemaliger IFDs in TIFFs um einige weiche Suchkritierien erweitert und es genutzt, um IFDs aus früheren Dateiversionen zu finden. Außerdem hat er ein ganz neues Tool geschrieben, das eine TIFF-Datei und eine Adresse in Hex-Notation einliest und den Inhalt an dieser Adresse so interpretiert, als wäre dort ein IFD gespeichert. Auf diese Weise konnten wir insgesamt sechs frühere IFDs ermitteln, die auf ältere Versionen der Datei hinweisen, und den Inhalt dieser IFDs in Augenschein nehmen. Die Tools sind unter https://github.com/SLUB-digitalpreservation/fixit_tiff/tree/master/src/archeological_tools im Quellcode verfügbar; sie sind Teil des bekannten Tools fixit_tiff.

Pointer zum ursprünglichen IFD0, wie er in der ersten Version der Datei stand

Die Ausgabe möglicher IFD-Adressen sieht so aus:
# adress,weight,is_sorted,has_required_baseline
0x4a184b0,2,y,y
0x4a241aa,2,y,y
0x4a2fea4,2,y,y
0x4a3bbb0,2,y,y
0x4a478d0,2,y,y
0x4a535ea,2,y,y

Diese Adressen der IFDs haben wir mittels Hex-Editor als IFD0-Offset in die TIFF-Datei eingetragen und so in einer Art TIFF-Archäologie schrittweise die alten Versionen der Datei wieder hergestellt. Dabei bestätigte sich die Annahme, dass der Scan ursprünglich in RGB abgespeichert worden war. Danach wurde wohl eine fehlerhafte Grayscale-Konvertierung durchgeführt, bei der nur die Tags PhotometricInterpretation (min-is-black) und BitsPerSample (1) verändert wurden. Ob dabei auch die Bilddaten selbst verändert wurden, lässt sich nicht mehr genau rekonstruieren.

In der vermutlich ersten Version des IFD0 sieht man mit tiffinfo noch die Angaben zum RGB-Bild:
Photometric Interpretation: RGB color
Samples/Pixel: 3

Die späteren Fassungen dagegen enthalten die Werte:
Photometric Interpretation: min-is-black
Samples/Pixel: 1

Außerdem wurden noch einige weitere Versionen des TIFFs erzeugt, bei denen einige andere Tags verändert, hinzugefügt oder entfernt wurden (Make, Model und Software).

Der Fehler war überhaupt nur aufgefallen, weil es eine intellektuelle Prüfung gab und der Bearbeiterin der Anzeigefehler auffiel (und sie ihn dann auch gemeldet hat!). Weil außerdem die MD5-Summen erst am Ende der Bearbeitung generiert werden und damit zum Fehlerzeitpunkt noch keine Prüfsumme existierte, wäre der Fehler nicht durch einen Fixity-Mismatch aufgefallen. Die einzig saubere Lösung wird nun wohl sein, die Seite neu zu scannen. Trotzdem ist es aber sehr eindrucksvoll zu sehen, welche Möglichkeiten das TIF Format bietet, kaputte Dateien wiederherzustellen.

frühere Artikel zu diesem Thema (also available in English):



-------------------------------------------------------------------------------------------------------------------

english version

A few days ago, a colleague gave us an interesting TIFF. It had successfully completed all validation attempts and didn't show any signs of structural issues in tiffinfo/tiffdump. However, it was not possible to display the image in the preview of the workflow tool used. Also, it was about three times the size of the other scans in the same intellectual entity. Our colleague asked us to have a closer look at that TIFF, so we went at it.

In contrast to our colleague, I didn't have any problem in displaying the TIFF altogether; the  Windows Image Viewer, IrfanView, MS Paint, Paint.NET und XnViewMP all displayed the image correctly. However, it was significantly stretched horizontally, which means that it was a lot wider than expected. Large parts of the scanned newspaper page were missing, and the rightmost part of the image was not visible.

broken display of the TIFF

In tiffinfo, we saw that the TIFF is a grayscale image:
Bits/Sample: 8
Samples/Pixel: 1

Particularly striking was the fact that the list entries for StripByteCounts was exactly by faktor 3 larger than the ImageWidth (4302 * 3 = 12906), which explained the stretch we saw in the image. Also, you could see that the StripOffsets grew in steps of 12906 Bytes; presumeably that's why the viewer was able to display a picture in the first place, regardless of the final quality. The ImageLength matched up with the number of entries in StripByteCount (6020), which is why there was no stretch in vertical direction.
Image Width: 4302
Image Length: 6020
StripByteCounts (279) LONG (4) 6020<12906 12906 12906 12906 12906 12906 12906 12906 12906 12906 12906 12906 12906 12906 12906 12906 12906 12906 12906 12906 12906 12906 12906 12906 ...> StripOffsets (273) LONG (4) 6020<8 12914 25820 38726 ...>

We could see in Okteta that the image data for each pixel were saved identically three times in a row. That explains our colleagues information about the filesize being three times larger than the other files in that IE. Also, we noticed that the IFD0 was written to the end of the file and contained information about an editing step in IrfanView.

normal RGB-TIFF

defective TIFF with two Bytes of redundant grayscale data per pixel

After having understood the problem, we discussed possible ways to repair the file:
- We could remove the redundant pixels and adapt the StripOffsets (and quite possibly all other ofsets in that file). While this is the more proper solution, software support for this kind of work would be imperative.
- We could set SamplesPerPixel to"3" to interpret the three duplicate pixels each as three RGB channels, thus summarizing three Bytes into one pixel. We actually did that, and it worked like a charm; at least we could display the image without getting any stretching or funky colors.

Now we had two theories about the origin of this error:
- There might have been a bit flip that damaged SamplesPerPixel. It's not a long way to go from "00 11"B ("0 3" D) to "00 01"B ("0 1" D), and it would explain the error we're seing.
- There could have been an error during a conversion of an RGB scan that was made from an analog grayscale template, during which the unnecessary pixels have not been removed. During this conversion, the SamplesPerPixel tag would have been rightfully set to a new value.

In a first test we set SamplesPerPixel to "3" using a Hex editor in order to command the TIFF viewer to interpret the image data in an RGB fashion. This little change alone caused the image to be displayed without any errors. The puzzle, however, that the image was uncommonly large (we expected it to about ad big as the other scans from the same newspaper) remained unsolved.


defective grayscale TIFF, interpreted as RGB

TIFF displayed correctly

We contemplated implementing plausibility checks for this type of error in checkit_tiff, which would be easily feasible assuming that all Strips in an image are of the same length. The following formula could be used: "StripByteCounts / SamplesPerPixel / RowsPerStrip = ImageWidth". This works best for TIFFs with RowsPerStrip = 1 set; other TIFFs would have to undergo more complex checks, because multiline Strips with byte counts that cannot be divided by the row number without modulo may not contain any padding. Due to this, there may be Rows that are shorter that the previous Rows in the same Strip. 

Other possible plausibility checks include:
- The image height is exactly as large as the multiplication product of RowsPerStrip and number of Strips: ImageLength = RowsPerStrip * StripOffsets.Count
- Each StripByteCount must be equally large as the difference of the neighboring StripByteOffsets: StripByteCounts[0] = StripOffsets[1] - StripOffsets[0] (or more general StripByteCounts[n] = StripOffsets[n+1] - StripOffsets[n])
- Each Strip needs to be equally long: StripByteCounts[0] = StripByteCounts[1] = StripByteCounts[2] = ... = StripByteCounts[n]

We discussed these possibilities in a larger group, which made Andreas curious, so he sat down to enhance his tool for finding candidates for former IFDs in TIFFs by some soft search criteria. Furthermore, he created an entirely new tool reads a TIFF and interprets the contents at a given address in a way that ressembles the IFD structure. This way, we were able to identify six former IFDs that hint to older versions of this file and inspect these IFDs a little further. The tools are available at https://github.com/SLUB-digitalpreservation/fixit_tiff/tree/master/src/archeological_tools in source code, they are part of the established tool fixit_tiff.

Pointer to the original IFD0, just like it was stored in the 1st file version

The list of possible IFD addresses as given by our tools looks like this:
# adress,weight,is_sorted,has_required_baseline
0x4a184b0,2,y,y
0x4a241aa,2,y,y
0x4a2fea4,2,y,y
0x4a3bbb0,2,y,y
0x4a478d0,2,y,y
0x4a535ea,2,y,y

We inserted these IFD addresses into the file's IFD0 offset pointer using a Hex Editor. Step by step, using this method, we were able to recreate older versions of the file in an archaeology style of work. In the course of the work we could confirm that the scan was originally saved in RGB. Later, there must have been an error in a grayscale conversion where only the tags PhotometricInterpretation (min-is-black) and BitsPerSample (1) were changed. We were not able to find out if the image data had been altered as well.

Ttiffinfo shows these information from the preusmeable 1st IFD0 version of the RGB image:
Photometric Interpretation: RGB color
Samples/Pixel: 3

Later versions, however, contain the values:
Photometric Interpretation: min-is-black
Samples/Pixel: 1

Also, there have been later files versions where some other tags have been added, altered or deleted (Make, Model and Software).

The error was only even discovered because intellectual checks were in place and the human operator noticed the error in displaying the TIFF (and because she decided to inform our colleague of this oddity!). Also, because checksums are only generated after the processing workflow is completed, we wouldn't have noticed the error by a fixity mismatch. We simply didn't have any checksums yet to compare the image against. In the end, the only proper solution will be a rescan of that newspaper page. However, it's still impressive to see the possibilities that TIF offers to repair seemingly broken images.

former articles on this subject (also available in English):

Freitag, 2. Februar 2018

Restaurierung von kaputten TIFF-Dateien

(English version below)

Kaputtes TIFF, erste Analyse


Ein Kollege schickte uns dieser Tage eine TIFF-Datei, die sich nicht öffnen liess. ImageMagick meldete:

display-im6.q16: Can not read TIFF directory count. `TIFFFetchDirectory' @ error/tiff.c/TIFFErrors/564.
display-im6.q16: Failed to read directory at offset 27934990. `TIFFReadDirectory' @ error/tiff.c/TIFFErrors/564.

Das Tool tiffinfo gab diese Fehlermeldung zurück:

TIFFFetchDirectory: Can not read TIFF directory count.
TIFFReadDirectory: Failed to read directory at offset 27934990.

Ein Blick mit dem Hexeditor Okteta und aktiviertem TIFF-Profil (welches im Übrigen unter https://github.com/art1pirat/okteta_tiff zu finden ist) zeigt, dass das der Offset-Zeiger, der auf das erste ImageFileDirectory (IFD) verweisen sollte, eine Adresse außerhalb der Datei enthält:

Screenshot Okteta, TIFF mit defektem Verweis auf erstes IFD
Faktisch ist das TIFF damit kaputt. Doch bestimmte Eigenschaften dieses Dateiformates erlauben es, eine Restaurierung zu versuchen.

Nebeneinschub

Für eine gut lesbare Einführung in den Aufbau von TIFF-Dateien sei auf den Blogeintrag "baseline TIFF" verwiesen. In "baseline TIFF - Versuch einer Rekonstruktion" wird auf einige manuelle Plausibilitätsprüfungen eingegangen.

Einen kurzen Überblick liefert auch "nestor Thema: Das Dateiformat TIFF" (zu finden auf http://www.langzeitarchivierung.de/Subsites/nestor/DE/Publikationen/Thema/thema.html)

Finden von IFDs


TIFF bringt ein paar Eigenschaften mit, die den Versuch einer Restaurierung erleichtern. So müssen laut Spezifikation Offsets immer auf gerade Adressen verweisen. Damit halbiert sich schon einmal der Suchraum.

Desweiteren können wir annehmen, dass ein IFD mindestens 4 Tags (oft deutlich mehr) enthält, in der Regel Subfiletype (0x00fe), ImageWidth (0x0100), ImageLength (0x0101) und BitsPerSample (0x0102).

Da ein IFD nach den Tags als letzten Eintrag ein NextIFD Feld enthält, welches entweder auf 0 gesetzt ist oder auf ein weiteres IFD verweist, haben wir bereits einiges an wertvollen Hinweisen zusammen.

Auch die Tageinträge innerhalb des IFD selber folgen einer Struktur. Jeder Eintrag besteht aus 2 Bytes TagId, 2 Bytes FieldType, sowie 4 Bytes Count und 4 Bytes ValueOrOffset (sh. Tag-Aufbau, Artikel "baseline TIFF" auf http://art1pirat.blogspot.de).

In der TIFF-Spezifikation sind für FieldType 12 mögliche Werte definiert, die libtiff kennt 18 Werte. Wir können also für jedes angenommene Tag prüfen, ob die Werte im Bereich 1-18 liegen.

Neben diesen harten Kriterien könnten wir, falls die Notwendigkeit besteht, noch weitere hinzuziehen, zum Beispiel:

  • Prüfe, ob bestimmte Pflicht-Tags vorhanden sind
  • Prüfe, ob alle Tags, wie von der Spezifikation gefordert, aufsteigend sortiert sind und keine Dubletten enthalten
  • Prüfe, ob ValueOrOffset ein Offset sein könnte und damit auf eine gerade Adresse verweist

Sicherlich ließen sich noch weitere Kriterien finden, doch in der Praxis zeigt sich, dass die og. harten Kriterien in der Regel schon ausreichen.

Um die Suche nach diesen nicht händisch vornehmen zu müssen, besitzt das Tool fixit_tiff seit kurzem das Programm "find_potential_IFD_offsets".

Wenn man es mit:

$> ./find_potential_IFD_offsets test.tiff test.out.txt

aufruft, spuckt es in der Datei "test.out.txt" eine Liste von Adressen aus, die potentiell ein IFD sein könnten. Für unsere Datei lieferte es den Wert "0x0008", sprich: das IFD müsste an Adresse 8 anfangen.

Mit Okteta die Datei geladen und geändert, voila!, es sieht gut aus:

Screenshot Okteta, TIFF mit repariertem Verweis auf erstes IFD





Auch tiffinfo ist jetzt etwas glücklicher:


TIFFReadDirectory: Warning, Bogus "StripByteCounts" field, ignoring and calculating from imagelength.
TIFF Directory at offset 0x8 (8)
  Subfile Type: (0 = 0x0)
  Image Width: 4506 Image Length: 6101
  Resolution: 300, 300 pixels/inch
  Bits/Sample: 8
  Compression Scheme: None
  Photometric Interpretation: min-is-black
  FillOrder: msb-to-lsb
  Orientation: row 0 top, col 0 lhs
  Samples/Pixel: 1
  Rows/Strip: 6101
  Planar Configuration: single image plane
  Color Map: (present)
  Software: Quantum Process V 1.04.73


Und ImageMagick zeigt sich nun gnädiger:

Ansicht des TIFFs mit repariertem Offset auf IFD





Wie man sieht, ist noch nicht alles repariert, schliesslich meldet auch ImageMagick noch Probleme:

display-im6.q16: Bogus "StripByteCounts" field, ignoring and calculating from imagelength. `TIFFReadDirectory' @ warning/tiff.c/TIFFWarnings/912.
display-im6.q16: Read error on strip 4075; got 2706 bytes, expected 4506. `TIFFFillStrip' @ error/tiff.c/TIFFErrors/564.

Doch sollte vorliegend gezeigt werden, dass eine Restaurierung von kaputten TIFF-Dateien durchaus möglich ist.

---------------------------------------------------------------------

Broken TIFF, a first analysis


A colleague recently sent us a TIFF file that he couldn't open. ImageMagick reported:

display-im6.q16: Can not read TIFF directory count. `TIFFFetchDirectory' @ error/tiff.c/TIFFErrors/564.
display-im6.q16: Failed to read directory at offset 27934990. `TIFFReadDirectory' @ error/tiff.c/TIFFErrors/564.

The tool tiffinfo returned the following error:

TIFFFetchDirectory: Can not read TIFF directory count.
TIFFReadDirectory: Failed to read directory at offset 27934990.

A quick investigation in the Hex editor Okteta with the TIFF profile activated (to be found at https://github.com/art1pirat/okteta_tiff) revealed that the offset pointer, which should be pointing to the first ImageFileDirectory (IFD), points to an address that is beyond the end of the file:

screenshot Okteta, TIFF with defective pointer to the 1st IFD
Given that, the TIFF is de facto broken. However, we can leverage certain properties of this file format to try a restoration.

Side note

For a well-readable introduction into the structure of TIFF files, pleases refer to the blog post "baseline TIFF". The article "baseline TIFF - Versuch einer Rekonstruktion" describes some manual plausibility checks.

Another short overview is provided by "nestor Thema: Das Dateiformat TIFF" (to be found at http://www.langzeitarchivierung.de/Subsites/nestor/DE/Publikationen/Thema/thema.html)

Finding IFDs


TIFF comes with a few properties that facilitate restoration attempts. According to the specification, offsets must point to even addresses, which already cuts the search space in half.

Also, we can assume that an IFD contains at least four tags (often significantly more), usually Subfiletype (0x00fe), ImageWidth (0x0100), ImageLength (0x0101) and BitsPerSample (0x0102).

As an IFD's last entry after all the tags is a pointer to the NextIFD, which is either set to 0 or points to another IFD, we already have some useful hints to work with.

The tag entries  inside of the IFD follow a strict structure as well. Each entry consists of 2 Bytes TagId, 2 Bytes FieldType, 4 Bytes Count and 4 Bytes ValueOrOffset (also see Tag-Aufbau, Artikel "baseline TIFF" auf http://art1pirat.blogspot.de).

The TIFF specification defines 12 possible values for the FieldType, libtiff knows 18 values. Following that, we can check for each chunk of Bytes that might be a tag if the value is between 1 and 18.

Additionally, we could add some soft criteria to these hard criteria that we already have:

  • check if certain mandatory tags can be found
  • check if all tags are sorted in an ascending order and don't contain any duplicates as required by the specification
  • check is ValueOrOffset can be an actual offset by checking if it points to an even offset

We could think up even more criteria, but practical experience shows that the hard criteria are already sufficient for most of the cases.

In order to avoid having to search for potential IFDs in the files manually, the tool fixit_tiff now comes with the program "find_potential_IFD_offsets".

If it is invoked like:

$> ./find_potential_IFD_offsets test.tiff test.out.txt

it will spew out a list of addresses to the file "test.out.txt" that might potentially mark the beginning of an IFD. For the file from our colleague, it gave us only one value, which was "0x0008". In other words, the IFD should start at address 8.

Now load up the file in Okteta change the pointer to the first IFD right after the TIFF header to the correct address, et voila!, it looks good:

screenshot Okteta, TIFF with repaired pointer to 1st IFD





tiffinfo is now a little happier as well:


TIFFReadDirectory: Warning, Bogus "StripByteCounts" field, ignoring and calculating from imagelength.
TIFF Directory at offset 0x8 (8)
  Subfile Type: (0 = 0x0)
  Image Width: 4506 Image Length: 6101
  Resolution: 300, 300 pixels/inch
  Bits/Sample: 8
  Compression Scheme: None
  Photometric Interpretation: min-is-black
  FillOrder: msb-to-lsb
  Orientation: row 0 top, col 0 lhs
  Samples/Pixel: 1
  Rows/Strip: 6101
  Planar Configuration: single image plane
  Color Map: (present)
  Software: Quantum Process V 1.04.73


And even ImageMagick is now a little more gracious:

Ansicht des TIFFs mit repariertem Offset auf IFD





As you can see, not everything has been repaired yet, and ImageMagick is still reporting some problems:

display-im6.q16: Bogus "StripByteCounts" field, ignoring and calculating from imagelength. `TIFFReadDirectory' @ warning/tiff.c/TIFFWarnings/912.
display-im6.q16: Read error on strip 4075; got 2706 bytes, expected 4506. `TIFFFillStrip' @ error/tiff.c/TIFFErrors/564.

However, we were able to show that a restoration of broken TIFFs is indeed feasible, and even though some of the data is lost, we still can see a part of what has been a magazine scan.

Mittwoch, 6. September 2017

Hinweis auf interessantes Interview zu FFV1

Ein äußerst interessantes Interview von Jürgen Keiper mit Peter Bubestinger zur Entstehung und Motivation von Matroska/FFV1 als langzeitarchivfähiges Datenformat für audiovisuelle Medien.

Es ist besonders interessant für Archivare, die wissen wollen, warum FFV1/Matroska ihre Probleme lösen kann. Peter schafft es Sachverhalte einfach und anschaulich zu erklären und kommt (fast) ohne technisches Vokabular aus.

Prädikat: Sehenswert!

Hier der Link zum Video:

https://www.memento-movie.de/2017/08/die-geschichte-eines-codecs-ffv1-in-der-archivwelt/

Dienstag, 30. Mai 2017

Bibtag - und 'ne Kleinigkeit gelernt

Heute hatte ich einen Abstecher zum Bibliothekartag 2017 nach Frankfurt am Main gemacht. Zum einen, um etliche Ex-Kommilitonen zu treffen, zum anderen war ich am Workshop von Yvonne Tunnat von der ZBW zur Formatidentifikation interessiert.

Yvonne hat eine wunderbare, pragmatische Art komplizierte Sachverhalte zu erklären. Wer sie kennenlernen möchte, der nestor-Praktikertag 2017 zur Formatvalidierung hat noch Plätze frei.

Zwei Dinge, die ich mitnehme. Zum einen kannte ich das Werkzeug peepdf noch nicht. Es handelt sich um ein CLI-Programm um eine PDF-Datei zu sezieren und kommt ursprünglich aus der Forensik-Ecke.

Zum anderen gibt es mit Bad Peggy ein Validierungstool um JPEGs zu analysieren.

Eine Diskussion, die immer wieder auftaucht ist die, wie man mit unbekannten Dateiformaten umgeht. IMHO sind diese nicht archivfähig, und wie Binärmüll zu betrachten. Dazu bedarf es aber mal eines längeren Beitrags und einer genaueren Analyse, ob und unter welchen Bedingungen solche Dateien vernachlässigbar sind, oder der long-tail zuschlägt.

BTW., wer am Mittwoch noch auf dem Bibtag ist, schaue mal beim Vortrag unserer Kollegin Sabine zu den Ergebnissen der PDF/A Validierung vorbei.

Dienstag, 16. Mai 2017

Über die Idee, ein Langzeitarchiv vermessen zu wollen

OpenClipart von yves_guillou, sh. Link
OpenClipart von yves_guillou, sh. Link im Bild
Irgendwann gerät man in einer Organisation an den Punkt, an dem man auf Menschen trifft, die sich den Zahlen verschrieben haben. Menschen, die als Mathematiker, als Finanzbuchhalter oder als Controller arbeiten. Das ist okay, denn Rechnungen wollen bezahlt, Ressourcen geplant und Mittel bereitgestellt werden.

Omnimetrie


Problematisch wird das Zusammentreffen mit Zahlenmenschen dann, wenn diese die Steuerung der Organisation bestimmen. Wenn es nur noch um Kennzahlen geht, um Durchsatz, um messbare Leistung, um Omnimetrie.

Schon Gunter Dueck schrieb in Wild Duck¹: "In unserer Wissens- und Servicegesellschaft gibt es immer mehr Tätigkeiten, die man bisher nicht nach Metern, Kilogramm oder Megabytes messen kann, weil sie quasi einen 'höheren', im weitesten Sinn einen künstlerischen Touch haben. Die Arbeitswelt versagt bisher bei der Normierung höherer Prinzipien."
  

Zahlen lügen nicht


Schauen wir uns konkret ein digitales Langzeitarchiv an. Mit Forderungen nach der Erhebung von Kennzahlen, wie:
  • Anzahl der Dateien, die pro Monat in das Archiv wandern, 
  • oder Zahl der Submission Information Packages (SIPs), die aus bestimmten Workflows stammen, 
demotiviert man ein engagiertes Archivteam. 

Denn diese Zahlen sagen nichts aus. Digitale Langzeitarchive stehen auch bei automatisierten Workflows am Ende der Verwertungskette. Es wäre in etwa so als würde man den Verkauf von Würstchen an der Zahl der Besucher der Kundentoilette messen wollen.

In der Praxis ist es so, dass Intellektuelle Einheiten (IE), die langzeitarchiviert werden sollen, nach dem Grad ihrer Archivfähigkeit und Übereinstimmung mit den archiveigenen Format-Policies sortiert werden.

Diejenigen  IEs, die als valide angesehen werden, wandern in
Archivinformationspaketen (AIP) eingepackt in den Langzeitspeicher. Die IEs, die nicht archivfähig sind, landen in der Quarantäne und ein Technical Analyst (TA) kümmert sich um eine Lösung oder weist die Transferpakete (SIP) mit diesen IEs zurück.

Wenn wir einen weitgehend homogenen Workflow, wie die Langzeitarchivierung von Retrodigitalisaten, betrachten, so sollte der größte Bestandteil der IEs ohne Probleme im Langzeitspeicher landen können. In dem Fall kann man leicht auf die Idee kommen, einfach die Anzahl der IEs und Anzahl und Größe der zugehörigen Dateien zu messen, um eine Aussage über den Durchsatz des Langzeitarchivs und die Leistung des LZA-Teams zu bekommen.

Ausnahme Standardfall


Doch diese Betrachtung negiert, dass nicht der Standardfall, wo IEs homogenisiert und automatisiert in das Archivsystem wandern, zeitaufwändig ist, sondern der Einzelfall, in dem sich der TA mit der Frage auseinander setzen muss, warum das IE anders aufgebaut ist und wie man eine dazu passende Lösung findet.

Formatwissen


Was die einfache Durchsatzbetrachtung ebenfalls negiert, ist, dass das Archivteam Formatwissen für bisher nicht oder nur allgemein bekannte Daten- und Metadatenformate aufbauen muss. Dieser Lernprozess ist hochgradig davon abhängig, wie gut die Formate bereits dokumentiert und wie komplex deren inneren Strukturen sind.

Organisatorischer Prozess


Ein dritter Punkt, den ein Management nach der Methode Omnimetrie negiert, ist die bereits im Nestor-Handbuch² formulierte Erkenntnis, dass digitale Langzeitarchivierung ein organisatorischer Prozess sein muss.

Wenn, wie in vielen Gedächtnisorganisationen, die Retrodigitalisate produzieren, auf Halde digitalisiert wurde, und das Langzeitarchivteam erst ein bis zwei Jahre später die entstandenen digitalen Bilder erhält, so kann von diesem im Fehlerfall kaum noch auf den Produzenten der Digitalisate zurückgewirkt werden. Die oft projektweise Abarbeitung von Digitalisierungsaufgaben durch externe Dienstleister verschärft das Problem zusätzlich. Was man in dem Falle messen würde, wäre in Wahrheit keine Minderleistung des LZA-Teams, sondern ein Ausdruck des organisatorischen Versagens, die digitale Langzeitverfügbarkeit der Digitalisate von Anfang an mitzudenken.

Natürlich ist es sinnvoll, die Entwicklung des Archivs auch mit Kennzahlen zu begleiten. Speicher muss rechtzeitig beschafft, Bandbreite bereitgestellt werden. Auch hier gilt, Augenmaß und Vernunft.

¹ Gunter Dueck, Wild Duck -- Empirische Philosophie der Mensch-Computer-Vernetzung, Springer-Verlag Berlin-Heidelberg,  (c)2008, 4. Auflage., S. 71
² Nestor Handbuch -- Eine kleine Enzyklopädie der digitalen Langzeitarchivierung, Dr. Heike Neuroth u.a., Kapitel 8 Vertrauenswürdigkeit von digitalen Langzeitarchiven, von Susanne Dobratz und Astrid Schoger, http://nestor.sub.uni-goettingen.de/handbuch/artikel/text_84.pdf, S.3