Media-Dim-Scan

Tutorial zur PHP-Klasse 'Media-Dim-Scan' von Daniel Schwamm (18.10.2011)

Inhalt

1. Prolog

1.1. Die verborgenen Innereien von Medien-Dateien

Media-Dim-Scan - Media-Player

1.1.1. Motivation

Für meinen eigenen Datei-Explorer ("ComCen") kam mir die Idee, neben den üblichen Angaben wie Namen, Datum, Grösse und Attribute dort auch noch die Dimension der aufgelisteten Dateien anzuzeigen, also Breite und Höhe, sofern es sich um Medien-Daten wie Bilder oder Filme handelt. "ComCen" ist in Delphi programmiert worden, und das Auslesen solcher Informationen wird von Haus aus nicht unterstützt. Also musste ich selbst ran.

1.1.2. Geheimniskrämerei

Das Vorhaben erwies sich als nicht trivial. Ich gewann bisweilen den Eindruck, die Entwickler der diversen Medien-Daten haben alles daran gesetzt, Informationen zu Höhe und Breite von Bildern und Filmen, sowie zu den Meta-Daten in Sound-Files, möglichst effektiv vor aufdringlichen Programmieren wie mir zu verbergen. Da hiess es immer wieder lange, manchmal auch zermürbende Internet-Sitzungen zu schieben, um Schritt für Schritt mehr Details gewinnen zu können.

1.1.3. Geknackte Medien-Formate

Aber es gelang. Am Schluss hatte ich eine mächtige Delphi-Funktion zur Verfügung, der ich nur noch den Dateinamen übergeben musste, und die mir daraufhin, basierend auf der Datei-Extension, die gewünschten Meta-Daten zu einer ganzen Palette von Medien-Formaten zurücklieferte. Im Einzelnen sind dies:

  • Bilder
    • BMP: Höhe und Breite plus Pixelformat
    • GIF: Höhe und Breite
    • JPG: Höhe und Breite plus Pixelformat
    • PNG: Höhe und Breite
    • TIF: Höhe und Breite plus Pixelformat
  • Sounds
    • ASX/WMA: Meta-Daten wie Entstehungsjahr und Titel
    • MP3: Meta-Daten wie Länge, Entstehungsjahr und Titel
  • Filme
    • ASF/WMV: Breite und Höhe
    • AVI: Breite und Höhe plus Frames per Second
    • FLV: Breite und Höhe plus Frames per Second und Länge
    • MOV/MP4/QT: Breite und Höhe plus Frames per Second und Länge
    • MPG: Breite und Höhe plus Frames per Second

1.1.4. Ein Beispiel für den Delphi-Scanner im Einsatz

Im oben erwähnten Datei-Manager "ComCen" sieht das Ergebnis eines solchen Medien-Scans, der übrigens für gewöhnlich mithilfe der Delphi-Media-Dim-Scan-Funktion in Sekundenbruchteil vonstattengeht, folgendermassen aus:

Media-Dim-Scan - Gescannter ComCen-Ordner

Gescannter ComCen-Ordner: Angezeigt werden zu jeder Datei im Ordner der Namen, das Datum, die Grösse, die Attribute und eventuell Breite, Höhe plus diverser Zusatz-Informationen wie Musik-Titel, FPS und Länge in Sekunden.

1.1.5. Download des Delphi-Sources

Wie bereits erwähnt wurde die Medien-Scanner-Funktionalität ursprünglich ihm Rahmen eines grösseren Delphi-Projektes programmiert. Der Source dazu kann hier eingesehen werden:

Media-Dim-Scan-Funktion in Delphi

1.2. Von Delphi zu PHP

Media-Dim-Scan - Delphi-Logo     Media-Dim-Scan - Pfeil nach rechts     Media-Dim-Scan - PHP-Logo

1.2.1. Media-Dim-Scanner in PHP

Da ich mir aber überlegte, dass ein solcher Medien-Scanner auch in PHP ganz nützlich sein könnte, beschloss ich, den Delphi-Source nach PHP zu konvertieren. Hierzu entwickelte ich die Klasse "media_scan", die wir uns im nachfolgendem Kapitel näher ansehen werden.

1.2.2. Klitzekleiner Online-Praxis-Test

Doch zunächst schauen wir uns die PHP-Klasse in einem kleinen Praxis-Test an.

1.2.2.1. Der Source

Der folgenden Source zeigt eine PHP-Seite, die den Ordner dieser Webseite nach Medien-Daten scannt und dann das Ergebnis in Tabellenform ausgibt:

00001
00002
00003
00004
00005
00006
00007
00008
00009
00010
00011
00012
00013
00014
00015
00016
00017
00018
00019
00020
00021
00022
00023
00024
00025
00026
00027
00028
00029
00030
00031
00032
00033
00034
00035
00036
00037
00038
00039
00040
00041
00042
00043
00044
00045
00046
00047
00048
00049
00050
00051
00052
00053
00054
00055
00056
00057
00058
00059
<?php
// Created 04.09.2011 12:56:19 by dirty DanPHPED vom 25.08.2011 19:54:22

error_reporting(E_ALL);

include 
"media-dim-scan-demo-class.php";

//----------------------------------------------
$media_dir=".";
$file_a=glob("$media_dir/*.*",GLOB_MARK);

$media_dim_scan=new media_dim_scan;

$table=
  
"<TABLE>\r\n".
  
"<TR>\r\n".
  
"<TH>Datei</TH>\r\n".
  
"<TH>Breite</TH>\r\n".
  
"<TH>Höhe</TH>\r\n".
  
"<TH>Zusatz-Infos</TH>\r\n".
  
"<TH>DEBUG: Gescannt</TH>\r\n".
  
"<TH>DEBUG: Vergleiche</TH>\r\n".
  
"</TR>\r\n";
for(
$r=0;$r<count($file_a);$r++)
{
  
$fn=$file_a[$r];
  
$media_dim_scan->scan($fn);
  
$fn=basename($fn);
  
$table.=
    
"<TR>".
    
"<TD>$fn</TD>".
    
"<TD class='int'>$media_dim_scan->width</TD>".
    
"<TD class='int'>$media_dim_scan->height</TD>".
    
"<TD>$media_dim_scan->infos</TD>".
    
"<TD class='int'>$media_dim_scan->debug_read_c</TD>".
    
"<TD class='int'>$media_dim_scan->debug_compare_c</TD>".
    
"</TR>\r\n";
};
$table.="</TABLE>\r\n";

$page="
<!DOCTYPE HTML PUBLIC '-//W3C//DTD HTML 4.01 Transitional//EN' 
  'http://www.w3.org/TR/html4/loose.dtd'>
<HTML>
<HEAD>
<META http-equiv='content-type' content='text/html; charset=windows-1252'>
<TITLE>Media-Dim-Scan-Demo</TITLE>
<LINK rel='stylesheet' type='text/css' href='media-dim-scan-demo-styles.css'>
</HEAD>
<BODY>
<H1>Media-Dim-Scan-Demo</H1>
$table
<H2>2011 Demo for PHP-Class 'media_dim_scan' by Daniel Schwamm</H2>
</BODY>
</HTML>
"
;

echo 
trim($page);
?>
1.2.2.2. Vorgehensweise
  1. Inkludierung des Sources der Media-Dim-Klasse "media-dim-scan-demo-class.php"
  2. Der anzuzeigende Ordner wird in "$media_dir" gesetzt
  3. Einlesen der Dateien in das Array "$file_a"
  4. Initialisierung der Media_Dim-Klasse "$media_dim_scan"
  5. Aufbau der Ausgabe-Tabelle
    1. Dateinamen aus "$file_a" holen
    2. Media-Dim-Scan durch Aufruf von "$media_dim_scan->scan($fn)"
    3. Gescannte Informationen in Tabellen-Zellen eintragen
  6. Ausgabe der Webseite
1.2.2.3. Einfach einmal ausprobieren

Media-Dim-Scan - Media-Dim-Scan-Demo

Demo zur PHP-Klasse "media_dim_scan"

2. Die Media-Dim-Klasse in PHP: Aufbau und Hilfsfunktionen

2.1. Aufbau

Die Media-Dim-Scan-Klasse "media_dim_scan" ist in der PHP-Datei "media-dim-scan-demo-class.php" gespeichert und besitzt folgenden, groben Aufbau:

00001
00002
00003
00004
00005
00006
00007
00008
00009
00010
00011
00012
00013
00014
00015
00016
00017
00018
00019
00020
<?PHP
class media_dim_scan
{
  
//Konstanten

  //Variablen

  //Service-Funktionen allgemein

  //Service-Funktion big-endian/little-endian

  //Scan-Funktionen Bilder

  //Scan-Funktionen Sound

  //Scan-Funktionen Filme

  //öffentliche Function scan
};
?>

2.2. Konstanten und Klassen-Variablen

Eine Reihe von Konstanten und Klassen-Variablen werden in der Media-Dim-Scan-Klasse definiert, die weitgehend nur zur internen Verwendung gedacht sind und nicht geändert werden sollten. Mit einer Ausnahme vielleicht.

00001
00002
00003
00004
00005
00006
00007
00008
00009
00010
00011
00012
00013
00014
00015
00016
00017
00018
00019
00020
00021
00022
00023
00024
00025
00026
00027
00028
00029
00030
00031
<?PHP
const _block_sz_max=512;       //"offizielle" Anzahl Bytes in Block
const _block_sz_overlap=100;   //zusätzliche Bytes in Block
const _asf_srch_len=15;        //minimaler Bereich bei ASF-Scans
const _flv_srch_len=15;
const 
_mpg_srch_len=10;
const 
_mov_srch_len=10;
const 
_asx_srch_len=30;
const 
_mp3_srch_len=10;

var 
$fn;                       //Dateinamen
var $ext;                      //Dateiextension
var $fh;                       //Dateihandle

var $file_sz;                  //Dateigrösse
var $file_pos;                 //aktuelle Position in der Datei

var $block;                    //Byte-Array mit Dateiinhalten
var $block_max;                //erlaubte Obergrenze für Block
var $block_read_c;             //tatsächlich eingelesen Obergrenze für Block

var $scan_a;                   //multi_scan: Such-Array
var $scan_inx;                 //multi_scan: Index-Treffer des Such-Arrays

var $debug_read_c;             //insgesamt gelesene Bytes
var $debug_compare_c;          //insgesamt vorgenommene Vergleiche

var $width;                    //Ergebnis: Breite der Mediendatei
var $height;                   //Ergebnis: Hoehe der Mediendatei
var $infos="";                 //Ergebnis: Zusatzinformationen wie FPS, Dauer
?>

2.2.1. Experimentierfeld

So wird die maximale Blockgrösse "_block_sz_max" vorgegeben, also die Anzahl Bytes, die je Lesevorgang aus der Datei in das Array "$block" kopiert werden soll. Der Wert von 512 Byte ist allerdings nur empirisch erhoben worden und muss keinesfalls das Optimum für ein bestimmtes Medien-Format abgeben. Wer will, kann hier gerne eigene Experimente anstellen. Wobei gilt: Je kleiner der Wert, desto weniger muss im Mittel vom File gelesen werden, um alle relevanten Informationen zu erhalten. Je grösser allerdings der Wert ist, um so effizienter ist der Dateizugriff, denn es dauert z.B. deutlich länger, 10 x 100 Bytes einzulesen, als 1 x 1.000 Bytes.

2.3. Service-Funktionen

Die folgenden kleinen Hilfsfunktionen sind innerhalb der Klasse gekapselt worden:

00001
00002
00003
00004
00005
00006
00007
00008
00009
00010
00011
00012
00013
00014
00015
00016
00017
00018
00019
00020
00021
00022
00023
00024
00025
00026
00027
00028
00029
00030
00031
<?PHP
//-----------------------------------------------------------
//1000er point
function format_int($value)
{
  return 
number_format($value,0,",",".");
}

//-----------------------------------------------------------
//auffuellen mit vorangehenden Nullen
function zero_fill($i,$len)
{
  
$s=strval($i);
  while(
strlen($s)<$len){$s="0$s";};
  return 
$s;
}

//-------------------------------------------------------------
//man glaubt es kaum, aber so kompliziert ist ein test auf int!
function chk_int($s)
{
  if(
$s===strval(intval($s)))return true;
  return 
false;
}

//------------------------------------------------------------
function debug($func,$name,$value)
{
  echo 
"<div class=debug>$func(): $name$value</div>";
}
?>

"format_int()" liefert Ganzzahlen als String mit 1000er-Punkten zurück. "zero_fill()" füllt Nullen vor den String eines Zahlwertes. "check_int()" prüft, ob der übergebene String einen Zahlwert enthält. Und "debug()" spuckt einen speziell formatierten String zu Debug-Zwecken aus.

2.4. Big Endian und Little Endian

Media-Dim-Scan - Motorola-Logo     Media-Dim-Scan - Intel-Logo

Zur Erläuterung der nächsten Funktionen in der Media-Dim-Scan-Klasse muss etwas ausgeholt werden ...

Um Ganzzahlen binär in einer Datei abzulegen, gibt es (unglücklicherweise) zwei grundsätzliche Möglichkeiten:

  1. Big Endian: Erst kommen die höherwertigen, dann die niederwertigen Bytes. Die Hexadezimal-Zahl 1F4 (=500) wird im Speicher mittels zweier Bytes folgendermassen abgelegt: $01 $F4. Dieses Verfahren wird von Motorola favorisiert.
  2. Little Endian: Erst kommen die niederwertigen, dann die höherwertigen Bytes. Die Hexadezimal-Zahl 1F4 (=500) wird im Speicher mittels zweier Bytes folgendermassen abgelegt: $F4 $01. Dieses Verfahren wird von Intel favorisiert.

2.4.1. Integer-Konvertierung

Um also eine Zwei-Byte-Zahl (Word) aus einem Byte-Array "$block" ab Position "$c" auszulesen, müssen wir auf zwei verschiedene Funktionen zurückgreifen können. Double-Words, also vier Byte lange Zahlen, werden in analoger Weise berechnet. Damit ergeben sich insgesamt die vier folgenden Decodierungsfunktionen:

00001
00002
00003
00004
00005
00006
00007
00008
00009
00010
00011
00012
00013
00014
00015
00016
00017
00018
00019
00020
00021
00022
00023
00024
00025
00026
00027
00028
00029
00030
00031
00032
00033
00034
<?PHP
//word big endian
private function word_be($c)
{
  return (
ord($this->block[$c])<<8) | ord($this->block[$c+1]);
}

//----------------------------------------------------------------------
//double word big endian
private function dword_be($c)
{
  return (
ord($this->block[$c  ])<<24) |
         (
ord($this->block[$c+1])<<16) |
         (
ord($this->block[$c+2])<< 8) |
         (
ord($this->block[$c+3])    );
}

//----------------------------------------------------------------------
//word little endian
private function word_le($c)
{
  return 
ord($this->block[$c]) | (ord($this->block[$c+1])<<8);
}

//----------------------------------------------------------------------
//double word little endian
private function dword_le($c)
{
  return (
ord($this->block[$c  ])    ) |
         (
ord($this->block[$c+1])<< 8) |
         (
ord($this->block[$c+2])<<16) |
         (
ord($this->block[$c+3])<<24);
}
?>

2.4.2. Big Little Endian Little Big-Mix

Zum Glück hat sich gezeigt, dass die Integer-Codierung eines Medien-Formates üblicherweise einheitlich ist. Eine Ausnahme bilden TIF-Bilder, deren erste "Magic Bytes" (dazu später mehr) aber festlegen, ob intern mit Big Endian- oder Little Endian-Integers hantiert wird; innerhalb einer Datei wird das einmal gewählte Verfahren dann meistens beibehalten (obwohl es durchaus so exotische Fälle gibt, bei denen etwa ein Big Endian-codiertes JPG intern ein Thumbnail seiner selbst kapselt, welches wiederum als Little Endian-codiertes TIF darin abgelegt wurde - aber das muss uns hier nicht weiter kümmern).

2.4.3. Sorry Motorola - Pfui Intel!

An dieser Stelle sei auch einmal eine Entschuldigung von mir an Motorola ausgesprochen. Denn jahrelang ging ich irgendwie davon aus, dass diese Firma das Little Endian-Format verbrochen hat, welches mir aufgrund der verdrehten Notation stets nur schwer in den Schädel ging. Daher wurde im Rahmen meiner Programmiertätigkeit so mancher Fluch gen Motorola ausgestossen, der eigentlich an Intel hätten gehen müssen.

Wie mich Wikipedia lehrte, kann man aber zur Ehrenrettung von Intel sagen, dass die Little Endian-Technik für die CPU vermutlich die effizientere ist, da etwa bei den häufig vorkommenden Casts von Double Word nach Word keine Byte-Grenzen verschoben werden müssen wie bei Motorola, sondern in diesem Fall einfach nur zwei statt vier Bytes im Speicher betrachtet werden.

2.4.4. Verdrehte Welt

Ach ja, da wäre noch ein Wort zu Big Endian und Little Endian: Irgendwie finde ich nämlich die Bezeichnung gerade falsch herum, denn bei Big Endian stehen doch die Bytes, die eine grosse Zahl ausmachen, am Anfang der Folge und nicht am Ende, wie die Bezeichnung eigentlich nahe läge (dieser Umstand war es dann auch, der mich jahrelang dem erwähnten Motorola-Intel-Irrtum aufsitzen liess). Ausser mir scheint sich darüber aber niemand zu wundern, also vermute ich mal irgendwo einen Denkfehler bei mir. Wäre ja nicht der Erste.

2.5. Dateizugriffe

Okay, zurück zur Media-Dim-Scan-Klasse: Um uns Datei-Inhalte anzusehen bzw. innerhalb einer Datei zu navigieren, kommen folgende Funktionen zum Einsatz:

00001
00002
00003
00004
00005
00006
00007
00008
00009
00010
00011
00012
00013
00014
00015
00016
00017
00018
00019
00020
00021
00022
00023
00024
00025
00026
00027
00028
00029
00030
00031
00032
<?PHP
//-------------------------------------------------
private function fh_seek($offset,$origin)
{
  
$rc=fseek($this->fh,$offset,$origin);
  
$this->file_pos=ftell($this->fh);
  return (
$rc!=-1);
}

//----------------------------------------------
private function fh_read($sz)
{
  
$pos=ftell($this->fh);
  
$this->block=fread($this->fh,$sz);
  
$this->block_read_c=ftell($this->fh)-$pos;
  if(
$this->block_read_c==0)return false;

  
$this->debug_read_c+=$this->block_read_c;
  if(
$this->block_read_c>self::_block_sz_max)
    
$this->block_read_c=self::_block_sz_max;

  return 
true;
}

//----------------------------------------------
private function fh_open_magic($sz)
{
  if(!
$this->fh=fopen($this->fn,"r"))return false;
  if(!
$this->fh_read($sz))return false;
  return (
$this->multi_scan(0)!=-1);
}
?>

2.5.1. Springen im Inneren einer Datei

Häufig stehen in Medien-Dateien an bestimmten Stellen Sprungadressen, die auf weitere interessante Stellen innerhalb des Files weisen. Je nach Medien-Typ sind diese Sprungadressen im Big Endian- oder Little Endian-Format codiert. Um sie auszulesen und an die errechnete Zieladresse zu springen, nutzen wir die oben gezeigten Decodierungsfunktionen, so wie die folgenden Funktionen:

00001
00002
00003
00004
00005
00006
00007
00008
00009
00010
00011
00012
00013
00014
00015
00016
00017
00018
00019
00020
00021
00022
00023
00024
<?PHP
//---------------------------------------------------
private function fh_read_word_be()
{
  if(!
$this->fh_read(2))return -1;
  return 
$this->word_be(0);
}

//---------------------------------------------------
private function fh_read_dword_be()
{
  if(!
$this->fh_read(4))return -1;
  return 
$this->dword_be(0);
}

//----------------------------------------------
private function fh_jump($start,$motorola_ok)
{
  if(
$motorola_ok){$sz=$this->word_be($start);}
              else{
$sz=$this->word_le($start);};
  
$this->file_pos=$this->file_pos+$start+$sz;
  return 
$this->fh_read(self::_block_sz_max);
}
?>

2.6. Multi-Scan - parallele Suchen nach mehreren Suchbegriffen

Media-Dim-Scan - Multi-Scan-Suche

Bei Medien-Dateien liegt häufig der Fall vor, dass innerhalb eines Byte-Arrays ("$block") nach verschiedenen Suchbegriffen gleichzeitig gesucht werden sollte, um Zeit zu sparen. In der Regel bestimmt dann der erste Treffer, wie weiter verfahren werden muss. So kann ein MPG-File z.B. mit der hexadezimalen Byte-Folge "00 00 01 B3" beginnen, wenn es sich um eine Movie ohne Sound handelt, oder aber es beginnt mit der Byte-Folge "0 00 01 BC", wenn Sound-Daten integriert sind.

Um eine solche effiziente Parallel-Suche zu realisieren, habe ich die Funktion "multi_scan" in die Media-Dim-Scan-Klasse integriert:

00001
00002
00003
00004
00005
00006
00007
00008
00009
00010
00011
00012
00013
00014
00015
00016
00017
00018
00019
00020
00021
00022
00023
00024
00025
00026
00027
00028
00029
00030
00031
00032
<?PHP
//---------------------------------------------------------
private function multi_scan($start)
{
  
$this->scan_inx=-1;
  for(
$c=$start;$c<$this->block_read_c;$c++)
  {
    for(
$r=0;$r<count($this->scan_a);$r++)
    {
      
$s=$this->scan_a[$r];
      if(
$c+strlen($s)>$this->block_read_c)break;
      
$cc=0;
      while(
$cc<strlen($s))
      {
        if(
$s[$cc]!="?")
        {
          
$this->debug_compare_c++;
          if(
ord($this->block[$c+$cc])!=ord($s[$cc]))break;
        };
        
$cc++;
      };
      
//gefunden?
      
if($cc==strlen($s))
      {
        
$this->scan_inx=$r;
        return 
$c;
      };
    };
  };
  return -
1;
}
?>

Durchsucht wird das Byte-Array "$block" ab Position "$start". Die zu suchenden Begriffe sind im Array "$scan_a" vermerkt. Das Zeichen "?" kann in Suchbegriffen als Joker verwendet werden; an dieser Stelle sind alle Zeichen erlaubt (dies ist sehr nützlich etwa bei UTF-16-codierten Tags, bei denen gewissermassen nur jedes zweite Byte interessiert).

Als Resultat wird die exakte Treffer-Position in "$block" zurückgeliefert. Zudem gibt "$scan_index" an, welcher der übergebenen Suchbegriffe zuerst gefunden wurde. Wurde dagegen kein Suchbegriff gefunden, wird -1 zurückgeliefert (und "$scan_index" wird ebenfalls auf -1 gesetzt).

2.7. Die Extension bestimmt das Sein

Bevor wir uns den eigentlichen Medien-Scans widmen, schauen wir uns zuerst noch die einzige Public-Funktion "scan()" der Media-Dim-Scan-Klasse an.

00001
00002
00003
00004
00005
00006
00007
00008
00009
00010
00011
00012
00013
00014
00015
00016
00017
00018
00019
00020
00021
00022
00023
00024
00025
00026
00027
00028
00029
00030
00031
00032
00033
00034
00035
00036
00037
00038
00039
00040
00041
00042
00043
00044
00045
00046
00047
00048
<?PHP
//----------------------------------------------
public function scan($fn)
{
  
$this->fh=0;
  
$this->debug_read_c=$this->debug_compare_c=0;
  
$this->block_read_c=$this->file_pos=0;
  
$this->file_sz=filesize($fn);
  
$this->block_max=1000;
  
$this->scan_a=array();
  
$this->debug_read_c=$this->debug_compare_c=0;
  
$this->width=$this->height=-1;$this->infos="";

  
$this->fn=$fn;
  
$ext=strtolower(pathinfo($fn,PATHINFO_EXTENSION));
  if(
$ext=="jpeg" || $ext=="jpe")$ext="jpg";
  if(
$ext=="tiff")$ext="tif";
  if(
$ext=="wmv")$ext="asf";
  if(
$ext=="mpeg" || $ext=="mpe" || $ext=="m1v")$ext="mpg";
  if(
$ext=="mp4" || $ext=="qt")$ext="mov";
  if(
$ext=="wma")$ext="asx";

  switch(
$ext)
  {
    
//pictures
    
case "bmp":$this->scan_bmp();break;
    case 
"gif":$this->scan_gif();break;
    case 
"jpg":$this->scan_jpg();break;
    case 
"png":$this->scan_png();break;
    case 
"tif":$this->scan_tif();break;

    
//sounds
    
case "asx":$this->scan_asx();break;
    case 
"mp3":$this->scan_mp3();break;

    
//movies
    
case "asf":$this->scan_asf();break;
    case 
"avi":$this->scan_avi();break;
    case 
"flv":$this->scan_flv();break;
    case 
"mov":$this->scan_mov();break;
    case 
"mpg":$this->scan_mpg();break;

  };
  if(
$this->width ==-1)$this->width ="";
  if(
$this->height==-1)$this->height="";
  if(
$this->fh)fclose($this->fh);
}
?>

Hier werden zunächst ein paar interne Variablen initialisiert.

Anschliessend wird die Dateiendung des übergebenen Files "normiert", da gleiche Medien-Typen verschiedene Extensions haben können. So sind z.B. "jpg", "jpe" und "jpeg" alles gültige Dateiendungen für ein JPG-Bild.

Anhand der normierten Extension "$ext" wird dann entschieden, welche Scan-Funktion aufgerufen wird. Die Media-Dim-Scan-Klasse analysiert also nicht selbst den Medien-Typ, sondern vertraut auf eine korrekte Datei-Endung. In der Praxis hat sich allerdings gezeigt, dass dies nicht immer gegeben sein muss. Falls der Scan eines vermeintlichen MPG-Movies einen Fehler zurückliefert, kann es u.U. helfen, der Datei z.B. eine "AVI"-Extension zu geben und dann erneut zu scannen.

Unbekannte Extensions werden ignoriert. Ansonsten liefern die internen Scan-Funktionen true oder false zurück, je nachdem, ob die Analyse erfolgreich war oder nicht. Die Public-Variablen "$width", "$height" und "$infos" werden entsprechend gesetzt.

3. Die Media-Dim-Klasse in PHP: Analyse von Bildern

Media-Dim-Scan - Analyse von Bildern

Wird anhand der Extension ein bestimmtes Bild-Format erkannt, wird die zugehörige Scan-Funktion aufgerufen. Die unterstützten Formate schauen wir uns nun genauer an.

3.1. BMP-Scan - grosse Dateien, simple Analyse

Die Analyse des Bitmap-Formates wird in der Media-Dim-Scan-Klasse mithilfe der internen Funktion "scan_bmp()" realisiert:

00001
00002
00003
00004
00005
00006
00007
00008
00009
00010
00011
00012
00013
00014
00015
00016
00017
<?PHP
//----------------------------------------------
private function scan_bmp()
{
  
$this->infos="No BMP";
  
$this->scan_a[]="BM";if(!$this->fh_open_magic(29))return false;
  
  
//old version?
  
$c=14;if(ord($this->block[$c])==12){$add=2;}else{$add=4;};
  
  
$c=14+4;    $this->width =$this->format_int($this->word_le($c));
  
$c+=$add;   $this->height=$this->format_int($this->word_le($c));
  
$c+=$add+2$this->infos =$this->zero_fill(ord($this->block[$c]),2)." bit";
  
  return 
true;
}
?>

3.1.1. Magic Bytes

Wie fast alle anderen Medien-Formate auch besitzen Bitmaps gleich zu Beginn ein paar "Magic Bytes", die sie als solche ausweisen. Fehlen diese Bytes, ist die Datei fehlerhaft aufgebaut. Oder aber die Extension "lügt" und es liegt in Wirklichkeit ein ganz anderes (Bild-)Format vor.

Bei Bitmaps sind diese Magic Bytes die Folge "BM". Diese Signatur wird als einziges Suchwort in das Array "$scan_a" eingetragen. Die Funktion "fh_open_magic(29)" sorgt dafür, dass die ersten 29 Bytes der Datei eingeladen werden und dort nach "BM" gesucht wird. Wird nichts gefunden, dann beendet sich die Funktion gleich wieder.

3.1.2. Treffer versenkt

Im Erfolgsfall macht es uns das Bitmap-Format recht einfach, die gesuchten Informationen zu finden, da diese an relativ fixen Positionen innerhalb des Files stehen. Je nach Version der Bitmap wird noch ein Offset berechnet und dann können auch schon direkt die Breite, Höhe und das Pixelformat (in "$infos") ausgelesen werden. Die Verwendung von "word_le()" zur Berechnung der Zahlenwerte demonstriert dabei übrigens, dass Bitmaps intern mit dem Intel-Format "Little Endian" arbeiten - was angesichts der engen Zusammenarbeit von Microsoft, den Entwicklern des Bitmap-Formates, und dem Prozessor-Hersteller Intel ja auch keine allzu grosse Überraschung ist.

3.2. GIF-Scan - 10 Bytes genügen

Noch einfacher als Bitmaps machen es uns GIF-Bilder: Hier reichen uns sogar nur die ersten 10 Bytes der Datei, um bereits Höhe und Breite ermitteln zu können:

00001
00002
00003
00004
00005
00006
00007
00008
00009
00010
00011
00012
<?PHP
//----------------------------------------------
private function scan_gif()
{
  
$this->infos="No GIF";
  
$this->scan_a[]="GIF";if(!$this->fh_open_magic(10))return false;
  
$this->width =$this->format_int($this->word_le(6));
  
$this->height=$this->format_int($this->word_le(8));
  
$this->infos="";
  return 
true;
}
?>

GIF-Bilder beginnen mit den stimmigen Magic Bytes "GIF". Und ab Position 6 bzw. 8 stehen Breite bzw. Höhe des Bildes, abgelegt im Intel-Format Little Endian.

3.3. JPG-Scan - geniales Format, aber komplex

Das JPG-Format ist sicher jenes, welches ich mit Abstand selbst am häufigsten verwende. Denn die Bilder können darüber mit hoher Qualität, aber auch sehr platzsparend gesichert werden. Leider haben es einem die Entwickler des Formates aber bei Weitem nicht so einfach wie bei BMP und GIF gemacht, um Höhe und Breite des enthaltenen Bildes ermitteln zu können. Doch seht selbst!

00001
00002
00003
00004
00005
00006
00007
00008
00009
00010
00011
00012
00013
00014
00015
00016
00017
00018
00019
00020
00021
00022
00023
00024
00025
00026
00027
00028
00029
00030
00031
00032
00033
00034
00035
00036
00037
00038
00039
00040
00041
00042
00043
00044
00045
00046
00047
<?PHP
//----------------------------------------------
private function scan_jpg()
{
  
$this->infos="No JPG";
  
$this->scan_a[]=chr(0xff).chr(0xd8);if(!$this->fh_open_magic(2))return false;
  
$this->infos="JPG-Error";
  while(
true)
  {
    if(!
$this->fh_read(1))return false;
    
$this->debug_compare_c++;
    if(
ord($this->block[0])==0xff)continue;

    
$this->debug_compare_c+=3;
    
$b=ord($this->block[0]);
    if(
$b==0xC0 || $b==0xC1 || $b==0xC2)
    {
      if(!
$this->fh_read(8))return false;
      
$this->height=$this->format_int($this->word_be(3));
      
$this->width=$this->format_int($this->word_be(5));
      switch(
ord($this->block[7]))
      {
        case  
4:$this->infos="32 bit";break;
        case  
3:$this->infos="24 bit";break;
        default:
$this->infos="08 bit";
      };
      return 
true;
    };

    
$this->debug_compare_c+=9;
    
$b=ord($this->block[0]);
    if(
$b!=0x01 && $b!=0xD0 && $b!=0xD1 && $b!=0xD2 && $b!=0xD3 && $b!=0xD4 && $b!=0xD5 && $b!=0xD6 && $b!=0xD7)
    {
      
$this->block_sz=$this->fh_read_word_be();
      if(
$this->block_sz<0)return false;
      if(!
$this->fh_seek($this->block_sz-2,SEEK_CUR))return false;

      
//wichtig - es muss $ff folgen, sonst ungültiges jpg!
      
if(!$this->fh_read(1))return false;
      if(
ord($this->block[0])!=0xff)return false;
      
$this->debug_compare_c++;
    };

    
$this->debug_compare_c+=3+9;
  };
}
?>

3.3.1. Magic Bytes

Die Magic Bytes eines JPGs sind die hexadezimale Folge "ff d8". Werden diese beiden Bytes am Anfang der Datei gefunden, wird die Analyse fortgesetzt, ansonsten abgebrochen.

3.3.2. Mühsam ernährt sich das Eichhörnchen

Anschliessend wird die Datei Byte für Byte ausgelesen. Wir suchen die Byte-Folge "FF C0" bzw. "FF C1" bzw. "FF C2". Finden wir diese Signatur, folgt 3 Bytes weiter die Höhe bzw. 5 Bytes weiter die Breite (kurioserweise ist bei einem JPG die Höhe tatsächlich vor der Breite abgelegt, was mir bei keinem anderen Medien-Format untergekommen ist). Ab Offset 7 finden sich dann die Informationen zum Pixelformat, also ob es sich um ein 32 Bit-Farbbild, 24 Bit-Farbbild oder 8 Bit-Graustufenbild handelt.

3.3.3. Hardcore-Grep

Meine erste JPG-Scan-Version sah so aus, dass ich einfach grosse Byte-Blöcke aus der JPG-Datei sequenziell eingelesen habe und diese nach der oben genannten Byte-Folge durchsuchte. Überraschenderweise musste ich dabei feststellen, dass oft mehr als die Hälfte des Bildes eingelesen werden musste, bis die gesuchte Stelle endlich erreicht war. Das machte den Scan-Vorgang natürlich ziemlich zeitaufwendig.

3.3.3.1. Treffer, die keine sind

Noch unangenehmer aber war, dass die erste Treffer-Stelle nicht unbedingt die gültige war. Denn häufig erwies sich die dort aufgespürte Dimensionsangabe als fehlerhaft, mit gigantischen Zufallswerten oder aber mit Null gefüllt. Zudem stiess ich auf diese Weise nicht selten nicht auf die Dimension des Hauptbildes, sondern die des enthaltenen JPG-Thumbnails (die üblicherweise 160 x 120 Pixel beträgt und dadurch relativ leicht als solche erkannt werden konnte).

3.3.3.2. Plausibilitätschecks

Spürte ich also eine Dimensionsangabe auf, dann behalf ich mir notgedrungen mit verschiedenen Plausibilitätskontrollen. Wenn Höhe und Breite im Vergleich zur Datei-Grösse unwahrscheinlich erschienen, dann ignorierte ich die Werte und suchte nach dem nächsten Signatur-Treffer im JPG, woraufhin erneut die Werte kontrolliert werden mussten usw. Das war eine ... nun ja, ziemlich ungenaue Vorgehensweise; dem ausgespuckten Ergebnis konnte man nie so recht trauen.

3.3.3.3. Love the Web!

Zum Glück gibt es aber schlauere Leute als mich. Und so fand ich im Web ein Scan-Verfahren, welches ausnutzt, dass JPGs strukturiert aufgebaut sind. Findet man dort nämlich nach einem "FF"-Byte ein definiertes anderes Byte wie "01", "D0", "D1", "D2" usw. hat man einen JPG-Block gefunden, der eine Grössenangabe besitzt. Mann kann nun diese Grössenangabe auslesen und mittels "fh_seek()" den Block in nur einem Schritt komplett überspringen. Dadurch werden auch automatisch alle potenziellen Fehlertreffer umgangen. Üblicherweise findet man so die gesuchten Informationen nach nur einigen Lesevorgängen. Und das ist natürlich eine viel effektivere Vorgehensweise als mein eigenes Gewurschtel.

3.4. PNG-Scan - ein modernes Format wie es sein sollte

Nach der Komplexität des JPG-Formates lobe ich mir doch das PNG-Format, was zurzeit ja immer grössere Verbreitung (im Web) findet. Hier müssen wir bloss auf die Magic Bytes "89 P N G" prüfen. Die gesuchten Informationen stehen dann gleich ab fixer Position 18 (Breite) und 22 (Höhe) leicht zugänglich als Big Endian Values. Das ist mit wenigen Programmzeilen zu realisieren:

00001
00002
00003
00004
00005
00006
00007
00008
00009
00010
00011
00012
<?PHP
//----------------------------------------------
private function scan_png()
{
  
$this->infos="No PNG";
  
$this->scan_a[]=chr(0x89)."PNG";if(!$this->fh_open_magic(24))return false;
  
$this->width =$this->format_int($this->word_be(18));
  
$this->height=$this->format_int($this->word_be(22));
  
$this->infos="24 bit";
  return 
true;
}
?>

3.5. TIF-Scan - die Big Endian- und Little Endian-Entscheidung

Widmen wir uns nun dem TIF-Format zu:

00001
00002
00003
00004
00005
00006
00007
00008
00009
00010
00011
00012
00013
00014
00015
00016
00017
00018
00019
00020
00021
00022
00023
00024
00025
00026
00027
<?PHP
//----------------------------------------------
private function scan_tif()
{
  
$this->infos="No TIF";
  
$this->scan_a[]="II";$this->scan_a[]="MM";if(!$this->fh_open_magic(0x37))return false;

  if(
$this->scan_inx==0)
  {
    
$this->width =$this->format_int($this->word_le(0x1e));
    
$this->height=$this->format_int($this->word_le(0x2a));
  }
  else
  {
    
$this->width =$this->format_int($this->word_be(0x1e));
    
$this->height=$this->format_int($this->word_be(0x2a));
  };

  
$b1=ord($this->block[0x32]);$b2=ord($this->block[0x36]);
  if     (
$b1==&& $b2==4){$this->infos="04 bit";}
  else if(
$b1==1          ){$this->infos="08 bit";}
  else if(
$b2==3          ){$this->infos="24 bit";}
  else                      
$this->infos="32 bit";
  
  return 
true;
}
?>

Auch hier ist der Scan relativ leicht zu realisieren, weil die Grössenangaben an eindeutig definierten Positionen im File stehen. Die Besonderheit bei TIFs ist jedoch zweifellos, dass diese intern entweder als Big Endian oder aber Little Endian codiert sein können.

Welches Codierungsverfahren konkret vorliegt, verraten uns die Magic Bytes des TIFs: Beginnen sie mit den Buchstaben "II", wobei das I für Intel steht, dann haben wir es konsequenterweise mit Little Endians zu tun. Beginnt die Datei dagegen mit "MM", wobei M folgerichtig für Motorola steht, müssen wir stattdessen von Big Endian-codierten Zahlenwerten ausgehen.

4. Die Media-Dim-Klasse in PHP: Analyse von Sounds

Media-Dim-Scan - Analyse von Sounds

Sound-Dateien besitzen zwar keine Dimension, also Höhe und Breite, jedoch häufig andere Meta-Informationen, deren Auslesung interessant sein könnte. So lassen sich manchmal Informationen zum Titel des Stückes finden, das Jahr der Publizierung und/oder Angaben zur Abspieldauer.

Wie man bei den beiden unten vorgestellten Scan-Codes feststellen kann, bin ich bei der Sound-Analyse allerdings ziemlich ... nun ja, grobschlächtig vorgegangen. Ein vollwertiger Ersatz für eine ordentliche Meta-Daten-Analyse, wie sie häufig in MP3-Playern integriert ist, stellen diese Funktionen jedenfalls nicht da.

4.1. ASX/WMA-Scan - Meta-Infos in UT-8/16?

4.1.1. ASX? Hä?

Was Advanced Stream Redirecting-Files (ASX) nun eigentlich genau sind, habe ich trotz Web-Recherche nicht so ganz kapiert. Es ist wohl eine Art Container-Format von Microsoft, welches u.a. Sound-Dateien beherbergen kann. In Anlehnung an WMV-Movies (Windows Media Movie; siehe weiter vorne) verwenden ASX-Files auch häufig die Extension "WMA" (Windows Media Audio).

4.1.2. ASX "gehext"

Dieses ominöse ASX-Sound-Format kannte ich vor der Arbeit an der Media-Dim-Scan-Klasse praktisch gar nicht. Nichtsdestotrotz fand ich einige ASX-Dateien auf meiner Festplatte vor. Also wollte ich dann doch etwas mehr darüber erfahren, als dass, was ich bei Wikipedia & Co. dazu fand. Ich griff dazu zu einer sehr bewährten Methode - und spionierte die Innereien der ASX-Files einfach mit einem Hex-Editor aus:

Media-Dim-Scan - ASX-Datei im Hex-Editor

ASX-Datei im Hex-Editor: Diese zwei Hex-Ausschnitte zeigen etwas vom inneren Aufbau der ASX-Dateien. Wir sehen vorne die Magic Bytes, gefolgt von einem Text "Dream Daniel", welches der Titel des Musikstückes sein dürfte. Im zweiten Bild erkennen wir diverse Meta-Daten, die mit "WM/" eingeleitet werden und hier z.B. Titel und Jahr ausweisen.

4.1.3. UTF-8/16-Hilfsfunktion

Wie man sieht, können ASX-Dateien offenbar Meta-Informationen zum Musikstück enthalten. Die Text-Informationen sind allerdings im UTF-8- oder UTF-16-Format abgelegt. Da mir (in Delphi) keine Konvertierung in das Standard-String-Format gelingen wollte, bastelte ich mir eine kleine Funktion, die einfach nur jedes zweite Byte der Folge betrachtet und so in der Regel dennoch einen "normalen" Text zurückliefern kann:

00001
00002
00003
00004
00005
00006
00007
00008
00009
00010
00011
00012
00013
00014
00015
00016
00017
00018
00019
00020
00021
00022
00023
00024
00025
<?PHP
//------------------------------------------------------
//4byte 0: 255 254 ? 0 ? 0 ... utf16?
private function filter_utf_pseudo($s)
{
  if(
strlen($s)>3)
  {
    if(
ord($s[3])==0)
    {
      
$s=substr($s,2,strlen($s));
      
$ss="";
      for(
$c=0;$c<strlen($s);$c++)
      {
        if((
$c%2)==0)$ss.=$s[$c];
      };
      
$s=$ss;
    };
  }
  else
  {
    
$s="";
  };
  return 
trim($s);
}
?>

4.1.4. Filter-Funktionen

Zwei weitere Hilfsfunktionen helfen uns, Texte bzw. Zahlenwerte aus den Meta-Informationen der ASX-Dateien zu extrahieren. Bei dem String wird zunächst die vorangestellte Längeninformation ausgelesenen und dann entsprechend viele Bytes in einen Buffer eingelesen. Dieser wird anschliessend an die obige "filter_utf_pseudo()"-Funktion übergeben. In ähnlicher Weiser werden Zahlwerte zunächst als UTF-8/16-Strings eingelesen, hinterher aber noch zusätzlich in einen Integer-Wert konvertiert.

00001
00002
00003
00004
00005
00006
00007
00008
00009
00010
00011
00012
00013
00014
00015
00016
00017
00018
00019
<?PHP
//----------------------------------------------
private function scan_asx_string($start)
{
  
$start+=4;$len=$this->word_le($start);if($len>100)$len=100;
  if(
$start+$len>self::_block_sz_max+self::_block_sz_overlap-1)
    
$len=self::_block_sz_max+self::_block_sz_overlap-$start-1;
  
$s="";for($c=0;$c<=$len-2;$c++)$s.=$this->block[$start+$c];
  return 
$this->filter_utf_pseudo($s);
}
 
//----------------------------------------------
private function scan_asx_int($start)
{
  
$s=$this->scan_asx_string($start);
  if(!
$this->chk_int($s))return -1;
  return 
strval($s);
}
?>

4.1.5. Haupt-Funktion

4.1.5.1. Magic Bytes

Kommen wir zur Haupt-Funktion: Hier wird zunächst auf die hexadezimalen Magic Bytes "30 26 B2 75" geprüft. Es wird ein möglichst grosser Block an Daten aus dem File gelesen. Wir schauen weiter, ob an einer bestimmten Position die Byte-Folge "33 1F" steht. Falls ja, stehen die Meta-Infos im Kopf ab Position 0x40 in diesem ersten Block. Wir lesen ab dort einfach jedes zweite Byte in "$infos" ein. Ein "trim()" am Schluss sorgt dafür, dass überflüssiger Anhang abgeschnitten wird.

4.1.5.2. W?M?/?-?T?a?g?s?

Danach suchen wir nach weiteren Meta-Infos, die zusätzlich in ASX-Files vorliegen können, die aber auf andere Art und Weise codiert sind. Dazu lesen wir die Datei blockweise aus. Jeder Block wird per Multi-Scan nach den Wörtern "W?M?/?Y?e?a?r?", ""W?M?/?T?i?t?l?e?" und "W?M?/?A?l?b?u?m?T?i?t?l?e?" durchsuchen, wobei, wie weiter oben beschrieben, die "?" als Joker fungieren, also jedes beliebige Zeichen sein dürfen (im Falle von UTF-8/16 üblicherweise 0). Wurden alle Informationen gefunden bzw. die global definierte Höchstzahl "$block_max" von Blöcken analysiert, wird abgebrochen.

00001
00002
00003
00004
00005
00006
00007
00008
00009
00010
00011
00012
00013
00014
00015
00016
00017
00018
00019
00020
00021
00022
00023
00024
00025
00026
00027
00028
00029
00030
00031
00032
00033
00034
00035
00036
00037
00038
00039
00040
00041
00042
00043
00044
00045
00046
00047
00048
00049
00050
00051
00052
00053
00054
00055
00056
00057
00058
00059
00060
<?PHP
//----------------------------------------------
private function scan_asx()
{
  
$this->infos="No ASX/WMA";
  
$this->scan_a[]=chr(0x30).chr(0x26).chr(0xb2).chr(0x75);
  if(!
$this->fh_open_magic(self::_block_sz_max+self::_block_sz_overlap))return false;

  
$this->infos="";

  
//Titel direkt nach Kopf?
  
if(ord($this->block[0x1e])==0x33 && ord($this->block[0x1f])==0x26)
  {
    
$c=0x40;$this->infos="";
    while(
strlen($this->infos)<80 && $c<=$this->block_read_c+self::_block_sz_overlap)
    {
      if(
ord($this->block[$c])==0)break;
      
$this->infos.=$this->block[$c];
      
$c+=2;
    };
    
$this->infos=trim($this->infos);
  };

  
$this->block_max=10;
  for(
$r=0;$r<=$this->block_max;$r++)
  {
    if(
$this->height==-1)
    {
      
//Jahr
      
$this->scan_a=array();$this->scan_a[]="W?M?/?Y?e?a?r?";
      
$c=$this->multi_scan(0);if($c>-1)$this->height=$this->scan_asx_int($c+strlen($this->scan_a[$this->scan_inx]));
    };

    if(
$this->infos=="")
    {
      
//Titel
      
$this->scan_a=array();$this->scan_a[]="W?M?/?T?i?t?l?e?";
      
$c=$this->multi_scan(0);if($c>-1)$this->infos=$this->scan_asx_string($c+strlen($this->scan_a[$this->scan_inx]));
      
      if(
$c==-1)
      {
        
//kein Titel? dann Album-Titel
        
$this->scan_a=array();$this->scan_a[]="W?M?/?A?l?b?u?m?T?i?t?l?e?";
        
$c=$this->multi_scan(0);if($c>-1)$this->infos=$this->scan_asx_string($c+strlen($this->scan_a[$this->scan_inx]));
      };
      
      
      
//Titel genügt
      
if($this->infos!="")break;
    };

    
//nächsten block lesen
    
$this->file_pos+=$this->block_read_c-self::_asx_srch_len;
    if(!
$this->fh_seek($this->file_pos,SEEK_SET))return false;
    if(!
$this->fh_read(self::_block_sz_max+self::_block_sz_overlap))return false;
  };

  return (
$this->infos!="");
}
?>
4.1.5.3. Überlappende Blöcke

Noch ein paar Hinweise zum Einlesen der Blöcke: Wir lesen diese nicht einfach stur sequenziell ein, sondern müssen vor jedem Block mindestens so viele Zeichen zurück-"seeken", wie der längste Suchbegriff lang sein kann (hier angeben in der Konstanten "_asx_srch_len"). Dies verhindert, dass ein Suchwort im File nicht gefunden wird, weil es genau auf der Grenze zwischen zwei Blöcken liegt.

Block 1Daten...SUCH
Block 2WORTDaten ...

Sequenzielle Blöcke: SUCHWORT wird weder in "Block 1" noch in "Block 2" gefunden.

Block 1Daten...SUCH
Block 2Daten ...SUCHWORTDaten ...

Überlappende Blöcke: SUCHWORT wird nicht in "Block 1" gefunden, aber in "Block 2", bei dem eine bestimmte Anzahl der letzten Bytes aus "Block 1" an den Anfang gestellt sind.

4.1.5.4. Überlappende Blöcke mit Reserve

Tja, um die Sache noch etwas komplizierter zu machen, gilt es auch folgenden Umstand zu beachten: Uns genügt häufig nicht nur das Finden der Suchwörter in einem bestimmten Block, sondern wir sind besonders an den Informationen interessiert, die erst hinter dem Suchwort liegen. Um hier das eventuell nötige Einladen eines weiteren Blockes zu vermeiden, habe ich die Konstante "_block_sz_overlap" eingeführt (die den empirisch ermittelten Wert von 100 hat). Diese Anzahl Bytes können beim Einladen eines Blockes nämlich noch zusätzlich angehängt werden, wobei allerdings die Multi-Scan-Suche stets nur bis maximal "_block_sz_max" Bytes in den Block vordringt. Selbst wenn also ein Treffer erst ganz am Schluss erfolgt, bleiben stets noch "_block_sz_overlap" Bytes übrig, die die eigentlich gesuchten Informationen enthalten (sollten).

_block_sz_max (512 Bytes)_block_sz_overlap (100 Bytes)
Daten    ...    SUCHWORTGesuchte Informationen

Blocks mit Reserve: Ein eingelesener Block kann einen Reserve-Bereich besitzen, der bei der Multi-Scan-Suche nicht berücksichtigt wird. So wird sichergestellt, dass die eigentlich gesuchte Information hinter dem Suchwort noch innerhalb des Treffer-Blocks sein muss.

4.2. MP3-Scan

Wesentlich populärer als das ASX-Format sind MP3-codierte Musik-Dateien. Zum Auslesen derer Meta-Informationen gibt es zahlreiche, recht umfangreiche Bibliotheken, auch für PHP. Da ich aber stets gerne ohne Fremd-Code auskomme und komplette Libraries üblicherweise weit über meine bescheidene Ziele hinausschiessen, strickte ich mir eine eigene Scan-Funktion zusammen.

4.2.1. Hilfsfunktionen

Ähnlich wie bei ASX-Dateien werden String-Information in MP3-Files im UTF-8/16-Format abgelegt. Die Längen-Angaben der Strings sind hier jedoch etwas anders organisiert - noch dazu auf zwei verschiedene Weisen -, was bei den folgenden Hilfsfunktionen über den zusätzlichen Parameter "$flag_ok" berücksichtigt wird.

00001
00002
00003
00004
00005
00006
00007
00008
00009
00010
00011
00012
00013
00014
00015
00016
00017
00018
00019
00020
00021
00022
00023
00024
00025
00026
00027
00028
00029
00030
00031
00032
00033
<?PHP
//----------------------------------------------
private function scan_mp3_string($start,$flag_ok)
{
  if(
$flag_ok){$len=ord($this->block[$start+3]);$start+=7;}
          else{
$len=ord($this->block[$start+2]);$start+=3;}
  if(
$len>100)$len=100;

  if(
$start+$len>self::_block_sz_max+self::_block_sz_overlap-1)
    
$len=self::_block_sz_max+self::_block_sz_overlap-$start-1;
  
$s="";for($c=0;$c<=$len-2;$c++)$s.=$this->block[$start+$c];
  return 
$this->filter_utf_pseudo($s);
}
 
//----------------------------------------------
private function scan_mp3_int($start,$flag_ok)
{
  
$s=$this->scan_mp3_string($start,$flag_ok);
  if(!
$this->chk_int($s))return -1;
  return 
strval($s);
}

//----------------------------------------------
private function scan_mp3_filter_string($start,$max_len)
{
  
$c=$start;$s="";
  while(
ord($this->block[$c])!=&& $c<$this->block_read_c && $c-$start<$max_len)
  {
    
$s.=$this->block[$c];
    
$c++;
  };
}
?>

4.2.2. Haupt-Funktion

Betrachten wir nun die eigentliche MP3-Scan-Funktion:

00001
00002
00003
00004
00005
00006
00007
00008
00009
00010
00011
00012
00013
00014
00015
00016
00017
00018
00019
00020
00021
00022
00023
00024
00025
00026
00027
00028
00029
00030
00031
00032
00033
00034
00035
00036
00037
00038
00039
00040
00041
00042
00043
00044
00045
00046
00047
00048
00049
00050
00051
00052
00053
00054
00055
00056
00057
00058
00059
00060
00061
00062
00063
00064
00065
00066
00067
00068
00069
00070
00071
00072
00073
00074
00075
00076
<?PHP
//----------------------------------------------
private function scan_mp3()
{
  
$this->infos="No MP3";
  
$this->scan_a[]=chr(0xff).chr(0xfa);
  
$this->scan_a[]=chr(0xff).chr(0xfb);
  
$this->scan_a[]="ID3";
  if(!
$this->fh_open_magic(self::_block_sz_max+self::_block_sz_overlap))
    return 
false;

  
$this->infos="";
  
$ok=false;
  if(
$this->scan_inx==2)
  {
    
//id3v2
    
$ok=true;
    
$this->block_max=5;
    for(
$r=0;$r<=$this->block_max;$r++)
    {
      if(
$this->width==-1)
      {
        
//Länge in Millisekunden
        
$this->scan_a=array();$this->scan_a[]="TLEN";$this->scan_a[]="TLE";
        
$c=$this->multi_scan(0);if($c>-1)$this->width=$this->scan_mp3_int($c+strlen($this->scan_a[$this->scan_inx]),true);
        if(
$this->width>-1)$this->w=floor($this->width/1000);
      };

      if(
$this->height==-1)
      {
        
//Jahr
        
$this->scan_a=array();$this->scan_a[]="TYER";$this->scan_a[]="TYE";$this->scan_a[]="TDRC";
        
$c=$this->multi_scan(0);if($c>-1)$this->height=$this->scan_mp3_int($c+strlen($this->scan_a[$this->scan_inx]),true);
      };

      if(
$this->infos=="")
      {
        
//Titel
        
$this->scan_a=array();$this->scan_a[]="TT2";$this->scan_a[]="TIT2";
        
$c=$this->multi_scan(0);
        if(
$c>-1)$this->infos=$this->scan_mp3_string($c+strlen($this->scan_a[$this->scan_inx]),($this->scan_inx==1));
        if(
$c==-1)
        {
          
//kein Titel? dann Album-Titel
          
$this->scan_a=array();$this->scan_a[]="TP1";
          
$c=$this->multi_scan(0);
          if(
$c>-1)$this->infos=$this->scan_mp3_string($c+strlen($this->scan_a[$this->scan_inx]),($this->scan_inx==1));
        };
        
//Titel genügt
        
if($this->infos!="")break;
      };
      
      
      
//if infos<>"MP3-Error" then exit;
      
$this->file_pos+=$this->block_read_c-self::_mp3_srch_len;
      if(!
$this->fh_seek($this->file_pos,SEEK_SET))return false;
      if(!
$this->fh_read(self::_block_sz_max+self::_block_sz_overlap))return false;
    };

    
$ok=($this->infos!="");
  };
  
  if(!
$ok)
  {
    
//alternative apetagex bzw. id2v1: letzte 128 byte
    
$this->file_pos=$this->file_sz-128;
    if(!
$this->fh_seek($this->file_pos,SEEK_SET))return false;
    if(!
$this->fh_read(128))return false;
    
$this->scan_a=array();$this->scan_a[]="TAG";if($this->multi_scan(0)!=0)return false;
    
$this->infos="ID3v1: ".$this->scan_mp3_filter_string(3,30);
    
$this->height=$this->scan_mp3_filter_string(93,4);
    
$ok=true;
  };
  return 
$ok;    
}
?>
4.2.2.1. Magic Bytes

Das MP3-File wird auf drei verschiedene Arten Magic Bytes geprüft: "FF FA", "FF FB" oder "ID3". In den ersten beiden Fällen sind keine Meta-Daten am Anfang des MP3s gespeichert, weshalb wir dort auch erst gar nicht danach suchen müssen. "ID3" dagegen gibt an, dass sich ID3-Tags der Version 2 (oder zukünftig auch höher) in der Datei befinden, die Meta-Informationen zum Musikstück enthalten, und zwar vor den eigentlichen Musik-Daten.

4.2.2.2. ID3v2-Scan

Wurde die "ID3"-Kennung gefunden, lesen wir die Datei Block für Block aus, wobei wieder die bei den ASX-Dateien beschriebene "Block-Überlappungstechnik mit Reserve" zum Einsatz kommt.

4.2.2.3. ID3v2-Tag: Dauer

Die Länge des Musik-Stücks wird entweder durch das Tag "TLEN" oder aber das Tag "TLE" markiert. Denn unglücklicherweise wurden die einzelnen Meta-Informationen nicht mit eindeutigen Tag-Kennungen normiert. Und mir ist ehrlich gesagt ziemlich schleierhaft, warum hier so unnötig kompliziert verfahren wurde. Nun ja, zum Glück haben wir ja eine Multi-Scan-Funktion, die dieses Problem gut abzudecken vermag. Wurde also ein passendes Tag gefunden, lesen wir die Information mithilfe unserer Filter-Funktionen aus, teilen das Ergebnis durch 1.000 und erhalten die Länge des Stückes in Sekunden. Leider ist - meiner Beobachtung nach - die Zeitdauer jedoch nur relativ selten in den ID3-Tags gesetzt.

4.2.2.4. ID3v2-Tag: Jahr

Das Jahr der Publizierung finden wir in den Tag "TYER" bzw. "TYE" bzw. "TDRC". Wie bei der Musik-Dauer ist auch dieses Tag nur selten in MP3-Files zu finden. Dies zeigt unschön auf, was leider einer der grossen Nachteile von Meta-Informationen ist: Da sie aus technischen Gründen nicht notwendig sind und Menschen gerne faul sind (trifft zumindest auf mich zu), werden sie oft einfach weggelassen.

4.2.2.5. ID3v2-Tag: Titel

Wesentlich häufiger immerhin lassen sich Informationen zum Titel des Musik-Stückes in MP3s finden. Wir scannen dazu nach den Tags "TT2" bzw. "TIT2". Sollten wir diese nicht finden, suchen wir stattdessen Informationen zum Album-Titel, und zwar im ID3-Tags "TP1" - immer noch besser, als "$info" ganz unbesetzt zu lassen.

4.2.2.6. ID3v1-Tags oder: Früher war alles besser

Haben wir nicht alle Informationen finden können oder lag kein ID3 Version 2 vor, dann prüfen wir am Schluss noch die letzten 128 Bytes der MP3-Datei. Hier befinden sich nämlich möglicherweise noch Meta-Daten, die im ID3 Version 1-Format abgespeichert wurden, leicht zu erkennen am Wörtchen "TAG" im Block. Die relevanten Daten darin liegen an exakt definierten Positionen und sind so leicht auszulesen.

Wie üblich muss man feststellen, dass die alte Version der ID3-Tags eigentlich wesentlich einfacher zu handhaben ist als ihre moderne Variante. Warum also ist man dabei nicht geblieben? Von mir aus auch etwas aufgebohrt auf 1.024 Bytes oder so. Wieso müssen Menschen mit der Zeit nur immer alles komplizierter machen statt einfacher?

5. Die Media-Dim-Klasse in PHP: Analyse von Filmen

Media-Dim-Scan - Analyse von Filmen

Nachdem wir Bilder und Sound-Files nach Meta-Informationen gescannt haben, sind nun Filme dran. Wir versuchen die Höhe und Breite von ihnen zu ermitteln, und in manchen Fällen auch Zusatzinformationen wie Dauer und/oder die Frames per Second (FPS).

5.1. ASF/WMV-Scan

Das Advanced System Format ist ein von Microsoft entwickeltes Container-Format, welches verschiedene Movie-Stream-Formate beinhalten kann, was das Scannen natürlich nicht gerade einfacher macht. Tatsächlich ist meine Funktion hierzu auch deshalb nicht gerade sonderlich ausgereift, weshalb sie relativ häufig zu keinem Ergebnis kommt.

Nun ja, schauen wir uns einmal an, was ich da genau fabriziert habe:

00001
00002
00003
00004
00005
00006
00007
00008
00009
00010
00011
00012
00013
00014
00015
00016
00017
00018
00019
00020
00021
00022
00023
00024
00025
00026
00027
00028
00029
00030
00031
00032
00033
00034
00035
00036
00037
00038
00039
00040
00041
00042
<?PHP
//----------------------------------------------
private function scan_asf()
{
  
$this->infos="No ASF/WMV";
  
$this->scan_a[]=chr(0x30).chr(0x26).chr(0xb2).chr(0x75);
  if(!
$this->fh_open_magic(self::_block_sz_max))return false;

  
$this->infos="ASF/WMV-Error";
  
$this->scan_a=array();
  
$this->scan_a[]=chr(0xcb).chr(0x96).chr(0xaa).chr(0xe8); //filler
  
$this->scan_a[]=chr(0x18).chr(0)."MP4";
  
$this->scan_a[]=chr(0x18).chr(0)."WMV";

  for(
$r=0;$r<=$this->block_max;$r++)
  {
    
$c=$this->multi_scan(0);
    if(
$this->scan_inx<1)
    {
      if(
$this->scan_inx==-1)
      {
        
$this->file_pos=$this->file_pos+$this->block_read_c-self::_asf_srch_len;
        if(!
$this->fh_seek($this->file_pos,0))return false;
        if(!
$this->fh_read(self::_block_sz_max))return false;
      }
      else
      {
        if(!
$this->fh_jump($c+4,false))return false;
      };
      continue;
    };

    
//Auflösung
    
$c-=10;$this->width =$this->format_int($this->word_le($c));
    
$c+= 4;$this->height=$this->format_int($this->word_le($c));
    
$this->infos="";
    return 
true;
  };

  return 
false;
}
?>

5.1.1. Magic Bytes und Stream-Format

ASF-Files erkennt man an der hexadezimalen Byte-Folge "30 26 B2 75". Damit weiss man aber noch nicht, welches konkrete Stream-Format sich im Inneren der Datei befindet. Deshalb starten wir einen Multi-Scan nach den Suchwörtern "CB 96 AA E8" (Filler), "18 00 M P 4" und "18 00 W M V".

5.1.2. Filler: Spring drüber!

Im ersten Fall, wenn die "Filler"-Signatur gefunden wurde, können wir die nachfolgende Längen-Information auslesen und dann innerhalb der Datei direkt zum nächsten Informations-Block weiterspringen. Dort beginnt der Multi-Scan dann von Neuem. Sollte gar kein Treffer gelandet werden, wird einfach nur der nächste Block an Daten aus der ASF-Datei geladen und ein weiterer Multi-Scan durchgeführt.

5.1.3. WMV und MP4 auslesen

Wurde hingegen eines der beiden anderen Tags gefunden, können wir direkt Höhe und Breite auslesen, die dort ab einer gewissen Offset-Position als Little Endians abgelegt wurden. Dies passt bei WMVs und MP4s glücklicherweise in der genau gleichen Weise.

MP4-Files liegen übrigens häufiger (?) als eigenständiges Format vor bzw. sind mit MOV-Dateien verbunden (siehe weiter vorne). Sie können zusätzliche Meta-Informationen beinhalten, die hier jedoch nicht weiter berücksichtigt werden.

5.1.4. Andere Stream-Formate

Auch andere Stream-Formate als WMV und MP4 werden nicht berücksichtigt, obwohl sie durchaus Inhalt einer ASF-Datei sein können. Keine Ahnung, ob nicht sogar jedes beliebige Stream-Format in ASF-Dateien gekapselt werden kann. Was dann vermutlich auch der Grund für die relativ hohe Fehleranfälligkeit meiner ASF-Scan-Funktion ist. Da ich i.d.R. aber ASF/WMV-Movies vermeide, weil sie regelmässig meinen PC in die Knie zwingen, trotzdem sie ja eigentlich von Microsoft höchstselbst speziell für Windows entwickelt wurden, war mir das nicht so wichtig.

5.2. AVI-Scan

Audio Video Interleave-Dateien sind ebenfalls ein Container-Format von Microsoft. Sie sind, wie ich finde, ziemlich strukturiert aufgebaut, und lassen sich daher wohl auch vergleichsweise einfach analysieren.

00001
00002
00003
00004
00005
00006
00007
00008
00009
00010
00011
00012
00013
00014
00015
00016
00017
00018
00019
00020
00021
<?PHP
//----------------------------------------------
private function scan_avi()
{
  
$this->infos="No AVI";
  
$this->scan_a[]="RIFF????AVI";
  if(!
$this->fh_open_magic(73))return false;

  
$this->infos="AVI-Error";
  
$this->scan_a=array();
  
$this->scan_a[]="avih";
  
$c=$this->multi_scan(0);if($c==-1)return false;
  
  
$c=$c+4+4;$d=$this->dword_le($c);if($d!=0)$d=1000000/$d;
  
$this->infos=$this->zero_fill(floor($d),2)." fps";

  
$c=$c+8*4;$this->width =$this->format_int($this->dword_le($c));
  
$c=$c+4;  $this->height=$this->format_int($this->dword_le($c));
  return 
true;
}
?>

5.2.1. Magic Bytes

Zunächst prüfen wir wieder auf die Magic Bytes. Im Falle von AVI-Movies ist dies die Byte-Folge "RIFF????AVI", wobei die "?"-Zeichen in bereits vertrauter Weise als Joker eingesetzt werden.

5.2.2. AVI-Header auslesen

Anschliessend suchen wir im geladenem, 73 Bytes grossem Block nach der "avih"-Header-Kennung. Innerhalb des Headers lassen sich dann an fixen Offset-Positionen Breite, Höhe und die FPS als Little Endians finden.

5.3. FLV-Scan

Das Flash Video-Format ist endlich einmal nicht von Microsoft, sondern von Adobe Systems entwickelt worden. Netterweise hat man dort gleich daran gedacht, auch Meta-Informationen in die Files verpacken zu können. Weniger nett ist allerdings, dass sie es einem so schwierig gemacht haben, an eben diese Informationen heranzukommen. Obwohl doch ich, also der Benutzer, der Adressat dieser Informationen bin. Oder sehe ich das falsch?

5.3.1. Wie codiere ich eine Zahl auf die denkbar komplizierteste Art und Weise?

Man schaue sich bloss einmal folgende Funktion an, die uns hilft, die in FLV-Tags abgelegten Zahlenwerte zu interpretieren. Diese Funktion kommt übrigens zum Einsatz, wenn wir die exakte Position des Zahlwertes in "$block" bereits ermittelt, den vermeintlich schwierigen Part der Analyse also bereits hinter uns gebracht haben!

00001
00002
00003
00004
00005
00006
00007
00008
00009
00010
00011
<?PHP
//----------------------------------------------
private function scan_flv_value_get($start)
{
  
$high1=ord($this->block[$start])>>4;
  
$high2=ord($this->block[$start+2])>>4;
  
$low=ord($this->block[$start]) & 0xf;
  
$mantissa=($low<<4) | $high2;
  return ((
256+$mantissa)<<$high1)>>7;
}
?>

Das glaubt man doch kaum, oder? Ich meine, alles, was wir wollen ist eine simple Zahl zurück. Den Adobe-Leuten war es aber wohl zu primitiv, diese Zahl im üblichen Big Endian oder Little Endian Format abzulegen, nein, es muss erst obiges Wahnsinnskonstrukt durchlaufen werden, um den wahren Wert der dort liegenden 3 Bytes zu ermitteln. Was zum Teufel haben die sich dabei gedacht? Wer denkt sich so etwas Krankes aus? Und warum? Hallo? Es heisst doch Meta-Informationen - warum werden die derart bösartig verschlüsselt, dass dabei jeder Kryptoalgorithmus Minderwertigkeitskomplexe entwickeln muss?

5.3.2. Meta-Tags

Nach dieser kalten Dusche widmen wir uns der Haupt-Funktion "scan_flv()". In der Hoffnung, dass es hier wieder etwas weniger komplex zu geht:

00001
00002
00003
00004
00005
00006
00007
00008
00009
00010
00011
00012
00013
00014
00015
00016
00017
00018
00019
00020
00021
00022
00023
00024
00025
00026
00027
00028
00029
00030
00031
00032
00033
00034
00035
00036
00037
00038
00039
00040
00041
00042
00043
00044
00045
00046
00047
00048
00049
00050
00051
00052
00053
00054
00055
00056
00057
00058
00059
00060
00061
00062
<?PHP
//----------------------------------------------
private function scan_flv()
{
  
$this->infos="No FLV";
  
$this->scan_a[]="FLV";
  if(!
$this->fh_open_magic(self::_block_sz_max))return false;

  
$this->infos="FLV-Error";$fps="?? fps";$duration="";
  
$this->scan_a=array();
  
$this->scan_a[]="onMetaData";
  
$this->scan_a[]="width";
  
$this->scan_a[]="height";
  
$this->scan_a[]="framerate";
  
$this->scan_a[]="duration";

  
$metadata_ok=false;$start=0;
  for(
$r=0;$r<=10;$r++)
  {
    
$c=$this->multi_scan($start);

    if(
$this->scan_inx==0)
    {
      
$metadata_ok=true;
      
$start=$c+strlen($this->scan_a[0]);
      continue;
    };

    if(
$this->scan_inx<|| !$metadata_ok)
    {
      
$this->file_pos=$this->file_pos+$this->block_read_c-self::_flv_srch_len;
      if(!
$this->fh_seek($this->file_pos,0))return false;
      if(!
$this->fh_read(self::_block_sz_max))return false;
      
$start=0;
      continue;
    };

    switch(
$this->scan_inx)
    {
      case 
1:$this->width =$this->format_int($this->scan_flv_value_get($c+strlen($this->scan_a[1])+2));break;
      case 
2:$this->height=$this->format_int($this->scan_flv_value_get($c+strlen($this->scan_a[2])+2));break;
      case 
3:$fps         =$this->zero_fill$this->scan_flv_value_get($c+strlen($this->scan_a[3])+2),2)." fps";break;
      case 
4:$duration    =$this->zero_fill$this->scan_flv_value_get($c+strlen($this->scan_a[4])+2),6)." s";break;
    };

    
//verhindere das der gleiche Treffer noch einmal gefunden wird
    
$start=$c+5;

    
//Breite, Höhe und FPS ermittelt?
    
$ok=($this->width!=-&& $this->height!=-&& $fps && $duration);
    if(
$ok)break;
  };
  
  
  
$this->infos=$fps;if($duration)$this->infos.=" - $duration";
  if(
$this->infos && $this->width ==-1)$this->width =0;
  if(
$this->infos && $this->height==-1)$this->height=0;
  
$ok=($this->width!=-&& $this->height!=-1);
  if(!
$ok)$this->infos="FLV-Error";
  return 
$ok;
}
?>

5.3.3. Magic Bytes

FLV-Dateien verraten sich netterweise durch die Magic Bytes "F L V".

5.3.4. Meta-Tags mit sprechenden Namen

Unser Suchwort-Array "$scan_a" wird diesmal reichlich gefüllt, und zwar mit all jenen Tags, die die Informationen beinhalten, die wir suchen und die im Falle von FLV-Files eindeutige Namen besitzen, als da wären "width", "height", "framerate" und "duration". Ausserdem scannen wir noch nach "onMetaData", was den Beginn der Meta-Informationen im File markiert, sofern überhaupt welche vorliegen sollten.

5.3.5. Suchbereich eingeschränkt

Üblicherweise liegen die Meta-Informationen von FLVs irgendwo am Anfang der Datei. Daher beschränken wir unsere Suche auch auf die ersten zehn "_block_sz_max"-grossen Blöcke. Wird innerhalb dieser mittels Multi-Scan nichts gefunden, brechen wir die Aktion ab. Dies verhindert, dass zu viel Scan-Zeit verbraten wird, wenn die Wahrscheinlichkeit nur noch sehr gering ist, Meta-Informationen finden zu können.

5.4. MOV/MP4-Scan

5.4.1. Unverdauliches von Apple für Microsoft

Das MOV-Movie-Format hat uns Apple geschenkt. Ich kenne es hauptsächlich von meinen diversen Kameras. Ich mochte MOVs zugegebenermassen lange nicht, weil mein eigener Video-Player, der auf dem Windows Media Player basiert, damit nicht zurecht kam. Inzwischen klappt es aber doch. Ich muss die MOVs also nicht mehr umständlich konvertieren. Ihre Anzahl auf meiner Platte nimmt seitdem stetig zu. Und so lohnte es sich für mich, eine passende Scan-Funktion zu basteln.

5.4.2. Zwei auf einen Streich

Wie sich bei der Arbeit am Media-Dim-Scanner gezeigt hat, orientiert sich das zunehmend populärer werdende MP4-Format ebenfalls am MOV-Aufbau. Wie das historisch zu begründen ist, weiss ich nicht. Jedenfalls können dadurch günstigerweise beide Formate mit nur einer Funktion nach Meta-Informationen gescannt werden.

5.4.3. Hilfsfunktionen

Zwei kleine Funktionen helfen uns bei der Analyse von MOV/MP4-Files:

00001
00002
00003
00004
00005
00006
00007
00008
00009
00010
00011
00012
00013
00014
00015
00016
00017
00018
00019
00020
00021
00022
00023
00024
00025
00026
00027
00028
00029
00030
<?PHP
//----------------------------------------------
private function scan_mov_compare_mem($srch)
{
  return
    
$this->block[4+0]==$srch[0] &&
    
$this->block[4+1]==$srch[1] &&
    
$this->block[4+2]==$srch[2] &&
    
$this->block[4+3]==$srch[3];
}

//----------------------------------------------
private function scan_mov_atom_search($srch)
{
  
$end_ok=false;$i=0;
  while(!
$end_ok && $i<100)
  {
    if(!
$this->fh_read(8))return false;
    
$this->debug_compare_c++;
    
$end_ok=$this->scan_mov_compare_mem($srch);
    if(!
$end_ok)
    {
      
$dw=$this->dword_be(0);
      if(!
$this->fh_seek($dw-8,1))return false;
      
$i++;
    };
  };
  return 
$end_ok;
}
?>

"scan_mov_compare_mem()" ermöglicht einen schnellen Vergleich eines Speicherbereichs ($block) mit einem bestimmten Suchwort ($srch), welches Struktur-bedingt beim MOV-Format stets 4 Bytes lang ist.

"scan_mov_atom_search()" hilft uns beim Durchhangeln der MOV-Struktur. MOVs sind durch eine Verkettung sogenannter "Atome" aufgebaut. Diese sind stets 4 Bytes gross, wobei die ersten 4 Bytes die Länge der nachfolgenden Meta-Informationen angeben, und die restlichen 4 Bytes eine Kennung in Klartext beinhalten, der Auskunft darüber gibt, um was für eine Meta-Information es sich konkret handelt. Die Funktion prüft also das Byte-Array "$block" auf die Kennung "$search", und falls sie nichts findet, wird die Längenangabe als Offset verwendet, um direkt zum nächsten Atom im File springen zu können, wo der Check von Neuem beginnt usw.

5.4.4. Haupt-Funktion

Die Scan-Funktion für MOV/MP4-Files fällt recht umfangreich aus:

00001
00002
00003
00004
00005
00006
00007
00008
00009
00010
00011
00012
00013
00014
00015
00016
00017
00018
00019
00020
00021
00022
00023
00024
00025
00026
00027
00028
00029
00030
00031
00032
00033
00034
00035
00036
00037
00038
00039
00040
00041
00042
00043
00044
00045
00046
00047
00048
00049
00050
00051
00052
00053
00054
00055
00056
00057
00058
00059
00060
00061
00062
00063
00064
00065
00066
00067
00068
00069
00070
00071
00072
00073
00074
00075
00076
00077
00078
00079
00080
<?PHP
//----------------------------------------------
private function scan_mov()
{
  
$this->infos="No MOV/MP4";
  if(!
$this->fh=fopen($this->fn,"r"))return false;
  if(!
$this->scan_mov_atom_search("moov"))return false;
  
$this->infos="MOV/MP4-Error";

  
$block_start=$this->file_pos;
  
$this->fh_read(8);if(!$this->scan_mov_compare_mem("mvhd"))return false;
  
$block_len=$this->dword_be(0);
  
  if(!
$this->fh_seek($block_start+20+8,SEEK_SET))return false;
  
$timescale=$this->fh_read_dword_be();
  
$duration =$this->fh_read_dword_be();
  
  
//"trak" suchen
  
if(!$this->fh_seek($block_start+$block_len+8,SEEK_SET))return false;

  
$end_ok=false;
  while(!
$end_ok)
  {
    if(!
$this->scan_mov_atom_search("trak"))return false;
    if(!
$this->scan_mov_atom_search("tkhd"))return false;

    
//lese komplettes "tkhd"-Atom
    
$this->fh_read(92);

    
//Adresse der Dimension variiert je nach MOV-Version
    
$version=ord($this->block[0]);if($version==0){$c=76;}else{$c=76+12;};
    
    
$this->width =$this->word_be($c+0);
    
$this->height=$this->word_be($c+4);

    
//manchmal ist Dimension nicht gesetzt: nächstes "trak"-Atom suchen
    
$end_ok=($this->width!=&& $this->height!=0);
  };       
  
$this->width =$this->format_int($this->width );
  
$this->height=$this->format_int($this->height);
  
  
//fps berechenbar?
  
$this->infos="?? fps";if(!$duration)return false;

  
//ab jetzt greppen
  
$this->scan_a=array();
  
$this->scan_a[]="stts";     //Atom-Block überspringen
  
$this->scan_a[]="ctts";     //Atom-Block überspringen
  
$this->scan_a[]="stsz";     //gesuchtes Atom
  
  
$this->fh_read(self::_block_sz_max);
  for(
$r=0;$r<=$this->block_max;$r++)
  {
    
$c=$this->multi_scan(0);
    if(
$this->scan_inx<2)
    {
      if(
$this->block_read_c<self::_block_sz_max)return false;
      if(
$this->scan_inx<0){$this->file_pos+=$this->block_read_c-self::_mov_srch_len;}
                       else{
$dw=$this->dword_be($c-4);$this->file_pos+=$c+$dw-8;};
      if(!
$this->fh_seek($this->file_pos,SEEK_SET))return false;
      if(!
$this->fh_read(self::_block_sz_max))return false;
      continue;
    };

    
//stsz-atom gefunden
    
$framecount=$this->dword_be($c+4+4+4);
    
$fps=round(($framecount*$timescale)/$duration);
    
$this->infos=$this->zero_fill($fps,2)." fps";

    
//playtime berechenbar?
    
if(!$timescale)return true;
    
$playtime=round($duration/$timescale);
    
$this->infos.=" - ".$this->zero_fill($playtime,6)." s";
    
    return 
true;
  };    

  return 
true;
}
?>

5.4.5. Nix ist mit Magic Bytes

MOV- bzw. MP4-Dateien verfügen lästigerweise über keine eindeutigen Magic Bytes am Anfang der Datei, woran sie leicht zu erkennen wären. Immerhin gilt aber, dass irgendwo, möglichst früh, die Atom-Kennung "moov" auftauchen sollte, sofern das Movie Meta-Informationen beherbergt.

5.4.6. Duration und Timescale

Finden wir das "moov"-Atom, suchen wir anschliessend weiter nach dem "mvhd"-Atom. An dieser Positionen plus fixem Offset lassen sich dann die Werte für die Filmdauer und die sogenannte Timescale als Big Endians finden. Beide Werte sind etwas ... nun ja, kryptisch. Wir benötigen sie jedoch später noch zur Berechnung der FPS.

5.4.7. Breite und Höhe

Im Abschluss daran durchsuchen wir die Datei erst nach dem Atom "trak" und dann nach dem Atom "tkhd". Finden wir diese, lesen wir mit "fh_read(92)" den kompletten "tkhd"-Block ein, innerhalb dem sich die Breite und die Höhe an exakt definierten Positionen befinden, wobei allerdings das Offset versionsabhängig variieren kann.

5.4.8. FPS machen einem das Leben schwer

Duration, Breite und Höhe sind ermittelt. An dieser Stelle hätte ich besser aufgehört. Aber meine Eitelkeit verlangte, dass ich auch noch die Frames Per Second aus MOVs extrahiere. Dies erwies sich jedoch als schwer verdaulicher Brocken.

Gesucht ist das Atom "stsz", wie ich nach vielen Hex-Analysen und langen Web-Recherchen ermitteln konnte. Es handelt sich dabei aber offenbar um eine Art Sub-Atom, ist also innerhalb eines grösseren Atom-Information-Blockes eingebettet. Hier können wir die "von Atom zu Atom hangeln"-Funktion ""scan_mov_atom_search()"" leider nicht verwenden, sondern müssen auf stupides "greppen" zurückgreifen, d.h., Block für Block aus der Datei auslesen und darin jeweils nach dieser Byte-Folge suchen.

Haben wir auf diese Weise "stsz" aufgespürt, holen wir uns den Wert der Anzahl Frames im Movie ("$framecount"), der dort an einer fixen Offset-Position als Big Endian zu finden ist. Die FPS lassen sich dann durch folgende Formel berechnen (die herauszubekommen mich sicher einige, vor Überanstrengung geplatzte Neuronen gekostet hat):

$fps=round(($framecount*$timescale)/$duration);

5.4.9. Duration ist nicht Spieldauer

Auch die "$duration" genannte Variable drückt noch nicht das aus, was wir eigentlich haben wollen (weshalb ich sie vorhin "kryptisch" nannte), nämlich die Spieldauer des Films. Die lässt sich aber aus den Variablen, die wir bisher ermittelt haben, schlussendlich folgendermassen berechnen:

$playtime=round($duration/$timescale);

5.5. MPG-Scan

Nun widmen wir uns MPG-Movies, die, ebenso wie JPGs bei den Bildern, den Grossteil all meiner Filme ausmachen. Aber wiederum ebenso wie bei JPGs ist die Extrahierung von Meta-Informationen wie Höhe, Breite und FPS hier leider nicht ganz einfach zu realisieren. Aber das kennen wir ja bereits ...

Okay, genug gejammert! Und hier kommt die Maus ... äh, der Source:

00001
00002
00003
00004
00005
00006
00007
00008
00009
00010
00011
00012
00013
00014
00015
00016
00017
00018
00019
00020
00021
00022
00023
00024
00025
00026
00027
00028
00029
00030
00031
00032
00033
00034
00035
00036
00037
00038
00039
00040
00041
00042
00043
00044
00045
00046
00047
00048
00049
00050
00051
00052
00053
00054
00055
00056
00057
00058
00059
00060
00061
00062
00063
00064
00065
00066
00067
00068
00069
00070
00071
00072
00073
00074
00075
00076
00077
00078
00079
00080
00081
00082
00083
00084
00085
00086
00087
00088
00089
00090
00091
00092
00093
00094
00095
00096
00097
00098
00099
<?PHP
//----------------------------------------------
private function scan_mpg()
{
  
$this->infos="No MPG";
  
$this->scan_a[]=chr(0).chr(0).chr(1).chr(0xb3); //m1v
  
$this->scan_a[]=chr(0).chr(0).chr(1).chr(0xba); //mpg
      
  
if(!$this->fh_open_magic(4))
  {
    
//xitami-special: magic erst ab position $55
    
if(!$this->fh_seek(0x55,SEEK_SET))return false;
    
$this->debug_compare_c++;
    
$dw=$this->fh_read_dword_be();if($dw!=0x000001ba)return false;
    
$this->scan_inx=1;
  };
 
  if(
$this->scan_inx==1)
  {
    
//MPG-Movie: Version bestimmen
    
if(!$this->fh_read(1+4+3+4))return false;
    
$this->infos="MPG-Version-Error";
    
$this->debug_compare_c++;

    if((
ord($this->block[0]) & 0xf0)!=0x20)return false;

    
$this->infos="MPG-Error";
    
$dw=$this->dword_be(8);

    
//das gesuchte Tag b3 liegt innerhalb von ba
    //bei e0 muss ebenso abgebrochen werden, denn b3 kann innerhalb davon liegen
    
$this->debug_compare_c+=2;
    while(
$dw!=0x000001ba && $dw!=0x000001e0)
    {
      if(
$dw==0)
      {
        while((
$dw 0xffffff00)!=0x00000100)
        {
          
$this->debug_compare_c++;
          
$dw<<=8;
          if(!
$this->fh_read(1))return false;
          
$dw|=ord($this->block[0]);
        };
      }
      else
      {
        
$dw=$this->fh_read_word_be();if($dw<0)return false;
        if(!
$this->fh_seek($dw,SEEK_CUR))return false;
        
$dw=$this->fh_read_word_be();if($dw<0)return false;
      };
      
$this->debug_compare_c+=2;
    };

    
//ab hier greppen
    
$this->fh_seek(0,SEEK_CUR);

    
$this->scan_a=array();
    
$this->scan_a[]=chr(0).chr(0).chr(1).chr(0xb3); //Halt bei Sequence
    
$c=0;$this->scan_inx=-1;
    for(
$r=0;$r<=$this->block_max;$r++)
    {
      if(!
$this->fh_read(self::_block_sz_max))return false;
      
$c=$this->multi_scan(0);if($this->scan_inx==0)break;
      
$this->file_pos=$this->file_pos+$this->block_read_c-self::_mpg_srch_len;
      if(!
$this->fh_seek($this->file_pos,SEEK_SET))return false;
    };

    
//nichts gefunden?
    
if($this->scan_inx==-1)return false;

    if(!
$this->fh_seek($this->file_pos+$c+4,SEEK_SET))return false;
  };
  
  
//dimension
  
if(!$this->fh_read(4))return false;

  
$dw=(ord($this->block[0])<<16) | (ord($this->block[1])<<8) | ord($this->block[2]);
  
$this->width=$this->format_int(($dw 0xfff000)>>12);
  
$this->height=$this->format_int($dw 0x000fff);

  
//fps
  
$b=(ord($this->block[3]) & 0x0f);
  switch(
$b)
  {
    case  
1:$this->infos="23";break;
    case  
2:$this->infos="24";break;
    case  
3:$this->infos="25";break;
    case  
4:$this->infos="29";break;
    case  
5:$this->infos="30";break;
    case  
6:$this->infos="50";break;
    case  
7:$this->infos="59";break;
    case  
8:$this->infos="60";break;
    default:
$this->infos="??";
  };
  
$this->infos.=" fps";

  return 
true;
}
?>

5.5.1. Magic Bytes - Sound oder nicht Sound, das ist hier die Frage

MPG-Movies beginnen mit der hexadezimalen Byte-Folge "00 00 01 B3" (SEQUENCE-Tag) oder "00 00 01 BA" (PACK-Tag). Im ersten Fall handelt es sich um "stumme" Filme, bei denen keine Audio-Daten integriert sind. Diese erhalten häufig die Extension ".m1v". Im zweiten Fall handelt es sich dagegen um Filme, die mit "Tonspur" unterlegt sind, und die üblicherweise mit der Extension ".mpg" versehen werden. Unabhängig von der Datei-Endung prüfen wir aber stets auf beide Tags.

5.5.2. XITAMI-Movies?

Weil eine Reihe von MPGs auf meiner Platte die genannten Magic Bytes nicht am Anfang der Datei enthielten, schaute ich sie mir mit einem Hex-Editor näher an - und fand dann dies:

Media-Dim-Scan - Hex-View eines Xitmai-MPG-Movies

XITAMI-MPG: Bei diesem MPG-Typ folgen die Magic Bytes des PACK-Tags (grün) erst ab Position 0x55. Im Kopf davor stehen offenbar ein paar Meta-Informationen, die den Film als "XITAMI_DELIVERS" ausweisen.

Nun habe ich keinen Plan, was es mit diesen XITAMI-Movies auf sich hat. Kann uns glücklicherweise aber auch egal sein. Nach dem Standard-Check nach Magic Bytes an Datei-Position 0 wiederholen wir das Ganze bei Misserfolg einfach auch noch ab Position 0x55. That's all. Der weitere Scan erfolgt dann bei allen MPGs auf die gleiche Weise.

5.5.3. Movies mit Audio-Daten

Handelt es sich bei dem MPG um einen Film mit Audio-Daten, fällt der Scan deutlich umfangreicher aus, als bei M1V-Movies. Denn in diesem Fall müssen wir erst nach der Byte-Folge "00 00 01 B3" suchen, die die sogenannten SEQUENZ-Chunks einleitet. Lästigerweise kann es jedoch eine ganze Weile dauern, bis diese Byte-Folge in einem MPG zum ersten Mal auftaucht.

5.5.4. MPG2-Movies fallen durch

Zunächst schauen wir uns aber noch an, um welche MPG-Version es sich genau handelt. Denn die Media-Dim-Scan-Klasse unterstützt nur die Analyse von MPG1-, nicht auch die von MPG2-Movies. Die sind nämlich intern etwas anders strukturiert. Und da ich selbst nur sehr wenige Filme von diesem Typ besitze, war ich schlicht zu faul, mich näher mit ihnen auseinanderzusetzen. Finden wir also ein MPG2-Movie, dann brechen wir den Scan-Vorgang kurzerhand mit einem "MPG-Version-Error" ab.

5.5.5. Wo ist das verflixte SEQUENCE-Tag?

Wie erwähnt, versuchen wir das SEQUENCE-Tag zu finden. Statt nun einfach nur stur nach dieser Byte-Folge in der Datei zu greppen, gehen wir etwas intelligenter vor. Und zwar hangeln wir uns, wie nun schon des Öfteren, von Tag zu Tag durch, wobei wir die Tag-Längenangaben nutzen, um grosse Bereiche im File ignorieren zu können. Die Tags, die wir zunächst suchen, sind das PACK-Tag (0x000001ba) oder aber das PADDING-Tag (000001be). Denn genau innerhalb dieser "Chunks" sollte sich dann unser gesuchtes SEQUENCE-Tag tummeln.

5.5.6. Ohne Grep geht's doch nicht

Haben wir mit obiger Methode das PACK- bzw. PADDING-Tag schliesslich irgendwo in den Tiefen der MPG-Datei aufgespürt, beginnen wir auf bewährte Weise damit, ab dieser Position Datenblöcke sequenziell aus der Datei zu lesen und diese mit unserer Multi-Scan-Funktion nach der Byte-Folge "00 00 01 B3" zu durchforsten.

5.5.7. M1v und MPG - Treffpunkt beim SEQUENCE-Tag

M1V-Movies werden, wie beschrieben, direkt mit dem SEQUENZ-Tag "00 00 01 B3" eingeleitet. Und eben dieses Tag haben wir gerade auch bei den MPG-Movies ermittelt. Die Ausgangspositionen ist also für beide Typen von MPG-Movies die gleiche. Und daher kann von nun an auch günstigerweise exakt der gleiche Source zur weiteren Ermittlung der Meta-Daten verwendet werden.

5.5.8. Noch einmal Byte-Akrobatik

Alle Informationen, die wir auslesen wollen, sind in den 4 Bytes versteckt, die dem SEQUENCE-Tag unmittelbar nachfolgen. Ähnlich wie beim FLV-Movie haben sich auch die MPG-Entwickler auf ein recht kryptisches Verfahren geeinigt - auf das ich hier auch gar nicht näher eingehen will -, um Breite, Höhe und FPS im MPG abzuspeichern. Die Motivation hier scheint mir aber eher der Effizienz geschuldet zu sein - und nicht purer Bosheit wie bei den FLVs. Aber da kann ich mich auch irren. In beiden Fällen.

6. Epilog

6.1. Media-Dim-Scan versus PHP-Funktion "getimagesize()"

6.1.1. Böse Welt

Ts, ts! Da hatte ich gerade mühsam eine eigene PHP-Klasse entwickelt, um ein paar Meta-Information aus Medien-Daten gewinnen zu können, nur um danach festzustellen, dass es eine solche Funktion in PHP bereits gibt - obgleich im Funktionsumfang etwas eingeschränkter und auch nur auf Bild-Formate anwendbar. Dennoch: Grrr!

Gemein(t) ist die Funktion "getimagesize()". An diese muss, ähnlich wie bei der Scan-Dim-Klasse, nur der Filename übergeben werden, und sie liefert Höhe und Breite eines Bildes zurück. Unterstützt werden dabei mindestens BMPs, GIFs, JPGs, PNGs und TIFs. Hätte ich also diese Funktion in meinem Source eingebaut, wären mir zwar ein paar Informationen zum Pixelformat verlorenen gegangen, aber ich hätte mir viel, viel Source, Zeit und Kopfzerbrechen ersparen können.

6.1.2. Licht am dunklem Horizont?

Nun ja, eine stille Hoffnung hatte ich gemeinerweise noch: Nämlich die, dass die "getimagesize()"-Funktion langsamer arbeitet als meine Scan-Dim-Klasse. Denn es wäre ja möglich, dass sie, ähnlich wie die Delphi-Image-Komponente, erst das komplette Bild einladen muss, um an Breite und Höhe zu gelangen. Meine PHP-Klasse dagegen liest üblicherweise nur einige wenige Bytes einer jeden Datei ein, um zu einem Ergebnis zu gelangen.

6.1.3. Ein Rennen

Um also die Geschwindigkeit meiner PHP-Klasse mit der "getimagesize()"-Funktion zu vergleichen, füllte ich ein Ordner mit 2.000 Bildern verschiedener Formate. Anschliessend verfütterte ich die Bilder zuerst an meine Media-Dim-Scan-Klasse, und im Anschluss an die PHP-eigene "getimagesize()"-Funktion. Und das Ergebnis fiel recht deutlich aus:

MethodeBenötigte Zeit (Erst-Scan)Benötigte Zeit (Wiederholungen)
Media-Dim-Scan-Kasse26.82 s1.16 s
getimagesize()27.20 s0.49 s

Der erste Durchlauf wurde jeweils bei einem "jungfräulichen" Computer vorgenommen, also direkt nach dem Neustart, wenn noch keine Daten bei Platten-Zugriffen gecached werden konnten. Interessanterweise liegt hier die Media-Dim-Scan-Klasse sogar knapp vor der "getimagesize()"-Funktion. Im Wiederholungsfall kann Letztere aber ihr besseres Zusammenspiel mit dem Betriebssystem voll ausnutzen und ist dadurch plötzlich mehr als doppelt so schnell wie die PHP-Klasse geworden.

6.1.4. Schlägt sich wacker, die Media-Dim-Scan-Klasse

Okay, okay, die "getimagesize()"-Funktion geht offenbar sehr clever bei ihrer Analyse der Bilder vor. Und erwies sich daher letztlich als deutlich schneller als mein eigener PHP-Source. Was ja auch kein Wunder ist, denn PHP ist und bleibt bei aller Raffinesse doch nur eine Interpreter-Sprache. Und als solche kann sie gegen direkt ausführbaren und CPU-optimierter Maschinencode einfach nicht bestehen. Dies hatte sich übrigens auch schon bei meinem Delphi-Source erwiesen: Auch der arbeitet erheblich schneller als die PHP-Version.

6.2. Tröstliches Fazit

Dennoch bin ich zufrieden mit dem Geleisteten. Denn auch wenn die Scan-Dim-Klasse langsamer als die "getimagesize()"-Funktion ist, so vermag sie eben nicht nur Bilder, sondern eben auch Sound- und Film-Dateien zu analysieren. Ausserdem kann man die ermittelte Scan-Dauer von gerade einmal 1.16 s im Wiederholungsfall für immerhin 2.000 Bilder wahrlich nicht gerade als "unperformant" bezeichnen.

So etwas verdirbt mir jedenfalls nicht den Spass an meinem Proggy. Ist halt nichts Hundertprozentiges. But who cares?