Media-Dim-Scan
Tutorial zur PHP-Klasse 'Media-Dim-Scan' von Daniel Schwamm (18.10.2011)
Inhalt
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.
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.
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
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:
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.
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
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.
Doch zunächst schauen wir uns die PHP-Klasse in einem kleinen Praxis-Test an.
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);
?>
- Inkludierung des Sources der Media-Dim-Klasse "media-dim-scan-demo-class.php"
- Der anzuzeigende Ordner wird in "$media_dir" gesetzt
- Einlesen der Dateien in das Array "$file_a"
- Initialisierung der Media_Dim-Klasse "$media_dim_scan"
- Aufbau der Ausgabe-Tabelle
- Dateinamen aus "$file_a" holen
- Media-Dim-Scan durch Aufruf von "$media_dim_scan->scan($fn)"
- Gescannte Informationen in Tabellen-Zellen eintragen
- Ausgabe der Webseite
Demo zur PHP-Klasse "media_dim_scan"
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
};
?>
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
?>
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.
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.
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:
-
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.
-
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.
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);
}
?>
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).
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.
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.
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);
}
?>
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);
}
?>
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).
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.
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.
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;
}
?>
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.
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.
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.
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;
};
}
?>
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.
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.
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.
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).
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.
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.
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;
}
?>
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==1 && $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.
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.
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).
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:
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.
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);
}
?>
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);
}
?>
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.
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!="");
}
?>
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.
Sequenzielle Blöcke: SUCHWORT wird weder in "Block 1"
noch in "Block 2" gefunden.
|
Block 2 | Daten ... | SUCHWORT | Daten ... |
|
Ü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.
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 ... SUCHWORT | Gesuchte 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.
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.
Ä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])!=0 && $c<$this->block_read_c && $c-$start<$max_len)
{
$s.=$this->block[$c];
$c++;
};
}
?>
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;
}
?>
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.
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.
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.
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.
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.
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?
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).
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;
}
?>
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".
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.
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.
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.
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;
}
?>
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.
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.
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?
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?
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<0 || !$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!=-1 && $this->height!=-1 && $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!=-1 && $this->height!=-1);
if(!$ok)$this->infos="FLV-Error";
return $ok;
}
?>
FLV-Dateien verraten sich netterweise durch die Magic Bytes "F L V".
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.
Ü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.
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.
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.
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.
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!=0 && $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;
}
?>
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.
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.
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.
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);
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);
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;
}
?>
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.
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:
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.
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.
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.
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.
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.
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.
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.
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.
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.
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:
Methode | Benötigte Zeit (Erst-Scan) | Benötigte Zeit (Wiederholungen) |
Media-Dim-Scan-Kasse | 26.82 s | 1.16 s |
getimagesize() | 27.20 s | 0.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.
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.
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?