$aspect_y) { $new_img_width = $max_width; $new_img_height = $image_height / $aspect_x; } else { $new_img_height = $max_height; $new_img_width = $image_width / $aspect_y; } return [(int)round($new_img_width), (int)round($new_img_height)]; } /** * Liest eine MPO-Datei und erstellt ein zusammengesetztes 3D-Bild * * MPO-Dateien enthalten zwei JPEG-Bilder (für linkes und rechtes Auge). * Diese Funktion: * 1. Findet beide Bilder in der MPO-Datei * 2. Extrahiert sie als separate JPEG-Daten * 3. Erstellt ein kombiniertes Ausgabebild mit: * - Optional: Side-by-Side Stereo-Ansicht (oben) * - Optional: Anaglyph-Bild für 3D-Brillen (unten) * * @param string $file Pfad zur MPO-Datei (lokal oder URL) * @return resource|false GD-Bild-Ressource oder false bei Fehler */ function mpo_read_image($file) { // Schritt 1: Datei komplett einlesen if (!$mpo = @file_get_contents($file)) { return false; } // Schritt 2: Position der JPEG-Bilder in der MPO-Datei finden $imgOffset = mpo_find_images($mpo); // Mindestens 2 Offsets erforderlich (Start von Bild 1, Start von Bild 2/Ende) if (count($imgOffset) < 2) { return false; } // Schritt 3: Bilder verarbeiten if (count($imgOffset) > 2) { // Stereo-Bild mit linkem und rechtem Bild return mpo_create_stereo_image($mpo, $imgOffset); } else { // Nur ein Bild vorhanden (kein echtes Stereo) return mpo_create_single_image($mpo, $imgOffset); } } /** * Findet die Positionen der JPEG-Bilder in der MPO-Datei * * MPO-Dateien können verschiedene Formate haben: * - Format A: Marker FF D8 FF E1 (EXIF) zwischen Bildern * - Format B: Marker FF D9 FF E0 (JFIF nach EOI) zwischen Bildern * - Format C: MPF-Header im APP2-Segment mit Offset-Informationen * - Format D: Einfach mehrere JPEG SOI-Marker (FF D8) * * @param string $mpo Inhalt der MPO-Datei * @return array Positionen der JPEG-Starts + Dateiende */ function mpo_find_images($mpo) { $imgOffset = []; // Methode 1: Suche nach speziellen Marker-Kombinationen (Format A + B) $imgOffset = mpo_find_by_markers($mpo); // Methode 2: Suche nach MPF-Header (Format C) if (count($imgOffset) == 0) { $imgOffset = mpo_find_by_mpf_header($mpo); } // Methode 3: Suche nach allen JPEG SOI-Markern (Format D - Fallback) if (count($imgOffset) == 0) { $imgOffset = mpo_find_by_soi_markers($mpo); } // Dateiende hinzufügen (markiert Ende des letzten Bildes) $imgOffset[] = strlen($mpo); return $imgOffset; } /** * Methode 1: Suche nach Marker-Kombinationen FF D8 FF E1 oder FF D9 FF E0 */ function mpo_find_by_markers($mpo) { $imgOffset = []; $offset = 0; $marker = true; $markA = chr(0xFF).chr(0xD8).chr(0xFF).chr(0xE1); // JPEG SOI + APP1 (EXIF) $markB = chr(0xFF).chr(0xD9).chr(0xFF).chr(0xE0); // JPEG EOI + APP0 (JFIF) // Suche abwechselnd nach beiden Markern while ($marker !== false) { $marker = strpos($mpo, $markA, $offset); if ($marker === false) { $marker = strpos($mpo, $markB, $offset); } if ($marker !== false) { $imgOffset[] = $marker; $offset = $marker + 4; } } return $imgOffset; } /** * Methode 2: Suche nach MPF (Multi-Picture Format) Header im APP2-Segment */ function mpo_find_by_mpf_header($mpo) { $imgOffset = []; // APP2-Marker suchen (FF E2) $app2_marker = chr(0xFF).chr(0xE2); $app2_pos = strpos($mpo, $app2_marker); if ($app2_pos !== false) { // Prüfen ob MPF-Header vorhanden $mpf_check = substr($mpo, $app2_pos + 4, 4); if ($mpf_check === 'MPF' . chr(0x00)) { // Erstes Bild beginnt bei Position 0 $imgOffset[] = 0; // Zweites Bild suchen: nach dem ersten JPEG-Ende (EOI = FF D9) $first_eoi = strpos($mpo, chr(0xFF).chr(0xD9)); if ($first_eoi !== false) { // Nach dem EOI nach neuem JPEG-Start suchen (SOI = FF D8) $second_soi = strpos($mpo, chr(0xFF).chr(0xD8), $first_eoi + 2); if ($second_soi !== false && $second_soi > $first_eoi) { $imgOffset[] = $second_soi; } } } } return $imgOffset; } /** * Methode 3: Suche nach allen JPEG SOI-Markern (FF D8) mit Validierung */ function mpo_find_by_soi_markers($mpo) { $imgOffset = []; $jpeg_soi = chr(0xFF).chr(0xD8); // JPEG Start of Image $offset = 0; while (($pos = strpos($mpo, $jpeg_soi, $offset)) !== false) { // Validierung: Nach SOI muss ein gültiger JPEG-Marker folgen if ($pos + 3 < strlen($mpo)) { $next_byte = ord($mpo[$pos + 2]); if ($next_byte == 0xFF) // Marker beginnt mit 0xFF { $marker_type = ord($mpo[$pos + 3]); // Gültige JPEG-Marker nach SOI: // E0-EF: APPn (Application-specific) // DB: Define Quantization Table // C0, C2: Start of Frame // C4: Define Huffman Table if (($marker_type >= 0xE0 && $marker_type <= 0xEF) || $marker_type == 0xDB || $marker_type == 0xC0 || $marker_type == 0xC2 || $marker_type == 0xC4) { $imgOffset[] = $pos; } } } $offset = $pos + 2; } return $imgOffset; } /** * Erstellt ein Stereo-Bild mit linkem und rechtem Auge * * Layout des Ausgabebildes: * +----------------------------------+ * | • Dot Links Rechts • | <- Stereo-Ansicht mit Ausrichtungspunkten * +----------------------------------+ * | | * | Anaglyph oder Links | <- Großes Bild * | | * +----------------------------------+ */ function mpo_create_stereo_image($mpo, $imgOffset) { // JPEG-Bilder aus den Byte-Bereichen extrahieren $img_left = imagecreatefromstring(substr($mpo, $imgOffset[0], $imgOffset[1] - $imgOffset[0])); $img_right = imagecreatefromstring(substr($mpo, $imgOffset[1], $imgOffset[2] - $imgOffset[1])); // Größen für Stereo-Ansicht berechnen (kleine Bilder oben) list($mpo_stereo_width, $mpo_stereo_height) = mpo_aspect_resize( imagesx($img_left), imagesy($img_left), MPO_STEREO_MAX_WIDTH, MPO_STEREO_MAX_HEIGHT, true // Vergrößerung erlaubt ); // Größen für volles Bild berechnen (großes Bild unten) list($mpo_full_width, $mpo_full_height) = mpo_aspect_resize( imagesx($img_left), imagesy($img_left), MPO_FULL_MAX_WIDTH, MPO_FULL_MAX_HEIGHT, false // Keine Vergrößerung ); // Platz für Ausrichtungspunkte berechnen (weiße Dots über Stereo-Bildern) $stereo_dot_space = 0; if (MPO_STEREO_DOTS) { $dot_size = 3; $stereo_dot_space = 2 * $dot_size + 2 * MPO_SPACING; } // Layout-Berechnung: Gesamtgröße des Ausgabebildes $stereo_align = 0; // Horizontale Ausrichtung der Stereo-Bilder $new_img_width = 0; // Gesamtbreite $new_img_height = 0; // Gesamthöhe $full_offset_y = 0; // Y-Position des großen Bildes // Stereo-Bereich (oben) if (MPO_STEREO_IMAGE) { $new_img_width = $mpo_stereo_width * 2 + MPO_SPACING; $new_img_height = $stereo_dot_space + $mpo_stereo_height + (MPO_FULL_IMAGE ? MPO_SPACING : 0); $full_offset_y = $mpo_stereo_height + MPO_SPACING + $stereo_dot_space; } // X-Position des großen Bildes (zentriert) $full_offset_x = round(($new_img_width - $mpo_full_width) / 2); // Voller Bild-Bereich (unten) if (MPO_FULL_IMAGE) { // Wenn volles Bild breiter ist, Gesamtbreite anpassen if ($mpo_full_width > $new_img_width) { $new_img_width = $mpo_full_width; $stereo_align = (int)(($mpo_full_width - ($mpo_stereo_width * 2 + MPO_SPACING)) / 2); $full_offset_x = 0; } $new_img_height += $mpo_full_height; } // Ausgabebild erstellen $new_image = imagecreatetruecolor($new_img_width, $new_img_height); // Bilder auf Zielgröße für volles Bild skalieren $tmp_left = imagecreatetruecolor($mpo_full_width, $mpo_full_height); imagecopyresampled($tmp_left, $img_left, 0, 0, 0, 0, $mpo_full_width, $mpo_full_height, imagesx($img_left), imagesy($img_left)); $tmp_right = imagecreatetruecolor($mpo_full_width, $mpo_full_height); imagecopyresampled($tmp_right, $img_right, 0, 0, 0, 0, $mpo_full_width, $mpo_full_height, imagesx($img_right), imagesy($img_right)); // Volles Bild einfügen (unten) if (MPO_FULL_IMAGE) { if (MPO_FULL_ANAGLYPH) { // Anaglyph-Bild erstellen (Rot-Cyan 3D für 3D-Brillen) $anaglyph_image = mpo_create_anaglyph($tmp_left, $tmp_right, $mpo_full_width, $mpo_full_height); imagecopyresampled($new_image, $anaglyph_image, $full_offset_x, $full_offset_y, 0, 0, $mpo_full_width, $mpo_full_height, $mpo_full_width, $mpo_full_height); imagedestroy($anaglyph_image); imagedestroy($tmp_left); imagedestroy($tmp_right); } else { // Nur linkes Bild verwenden imagecopyresampled($new_image, $img_left, $full_offset_x, $full_offset_y, 0, 0, $mpo_full_width, $mpo_full_height, imagesx($img_left), imagesy($img_left)); } } // Stereo-Bilder nebeneinander einfügen (oben) if (MPO_STEREO_IMAGE) { // Linkes Bild imagecopyresampled($new_image, $img_left, $stereo_align, $stereo_dot_space, 0, 0, $mpo_stereo_width, $mpo_stereo_height, imagesx($img_left), imagesy($img_left)); imagedestroy($img_left); // Rechtes Bild imagecopyresampled($new_image, $img_right, $stereo_align + $mpo_stereo_width + MPO_SPACING, $stereo_dot_space, 0, 0, $mpo_stereo_width, $mpo_stereo_height, imagesx($img_right), imagesy($img_right)); imagedestroy($img_right); // Weiße Ausrichtungspunkte zeichnen (für Cross-View-Betrachtung) // Diese Punkte helfen beim Überkreuzen der Augen if (MPO_STEREO_DOTS) { $white = imagecolorallocate($new_image, 255, 255, 255); // Punkt über linkem Bild (Mitte) imagefilledrectangle($new_image, $stereo_align + (int)($mpo_stereo_width / 2) - 3, MPO_SPACING - 3, $stereo_align + (int)($mpo_stereo_width / 2) + 3, MPO_SPACING + 3, $white); // Punkt über rechtem Bild (Mitte) imagefilledrectangle($new_image, $stereo_align + MPO_SPACING + (int)($mpo_stereo_width * 1.5) - 3, MPO_SPACING - 3, $stereo_align + MPO_SPACING + (int)($mpo_stereo_width * 1.5) + 3, MPO_SPACING + 3, $white); } } return $new_image; } /** * Erstellt ein Anaglyph-Bild (Rot-Cyan 3D) * * Anaglyph-Methode: * - Linkes Bild -> Rot-Kanal (in Graustufen umgewandelt) * - Rechtes Bild -> Cyan-Kanäle (Grün + Blau) * - Mit Rot-Cyan-Brille betrachten für 3D-Effekt * * @param resource $img_left Linkes Bild (GD-Ressource) * @param resource $img_right Rechtes Bild (GD-Ressource) * @param int $width Breite des Ausgabebildes * @param int $height Höhe des Ausgabebildes * @return resource GD-Bild-Ressource */ function mpo_create_anaglyph($img_left, $img_right, $width, $height) { $anaglyph_image = imagecreatetruecolor($width, $height); imagealphablending($anaglyph_image, false); // Jedes Pixel einzeln verarbeiten for ($y = 0; $y < $height; $y++) { for ($x = 0; $x < $width; $x++) { // Linkes Bild: RGB in Graustufen umwandeln (Luminanz-Formel) // Dann als Rot-Kanal verwenden $left_color = imagecolorat($img_left, $x, $y); $r = (int)((($left_color >> 16) & 255) * 0.299 + // Rot-Anteil (($left_color >> 8) & 255) * 0.587 + // Grün-Anteil (($left_color) & 255) * 0.114); // Blau-Anteil $r = min($r, 255); // Auf 255 begrenzen // Rechtes Bild: Grün- und Blau-Kanäle direkt übernehmen $right_color = imagecolorat($img_right, $x, $y); $g = ($right_color >> 8) & 255; // Grün-Kanal $b = $right_color & 255; // Blau-Kanal // Kombiniertes Anaglyph-Pixel setzen imagesetpixel($anaglyph_image, $x, $y, imagecolorallocate($anaglyph_image, $r, $g, $b)); } } return $anaglyph_image; } /** * Erstellt ein einfaches Bild (wenn kein Stereo vorhanden) */ function mpo_create_single_image($mpo, $imgOffset) { // Bild extrahieren $image = imagecreatefromstring(substr($mpo, $imgOffset[0], $imgOffset[1] - $imgOffset[0])); // Größe berechnen list($mpo_width, $mpo_height) = mpo_aspect_resize( imagesx($image), imagesy($image), MPO_FULL_MAX_WIDTH, MPO_FULL_MAX_HEIGHT, false // Keine Vergrößerung ); // Ausgabebild erstellen und skalieren $new_image = imagecreatetruecolor($mpo_width, $mpo_height); imagecopyresampled($new_image, $image, 0, 0, 0, 0, $mpo_width, $mpo_height, imagesx($image), imagesy($image)); imagedestroy($image); return $new_image; } // ============================================================================ // DEMO-VERWENDUNG (Web-Interface) // ============================================================================ if (isset($_GET['file'])) { // Datei verarbeiten und ausgeben $mpo_file = $_GET['file']; // Sicherheitsprüfungen $is_url = filter_var($mpo_file, FILTER_VALIDATE_URL); $extension = strtolower(pathinfo($mpo_file, PATHINFO_EXTENSION)); if ($extension !== 'mpo') { header('HTTP/1.1 400 Bad Request'); die('Fehler: Nur .mpo Dateien werden unterstützt.'); } // Prüfen ob Datei erreichbar ist $file_accessible = false; if ($is_url) { $headers = @get_headers($mpo_file); $file_accessible = ($headers && strpos($headers[0], '200') !== false); } else { $file_accessible = file_exists($mpo_file); } if (!$file_accessible) { header('HTTP/1.1 404 Not Found'); die('Fehler: MPO-Datei nicht gefunden oder nicht erreichbar.'); } // MPO-Datei verarbeiten $image = mpo_read_image($mpo_file); if ($image !== false) { // Erfolgreich: Bild als PNG ausgeben header('Content-Type: image/png'); imagepng($image); imagedestroy($image); } else { // Fehler beim Verarbeiten header('HTTP/1.1 500 Internal Server Error'); die('Fehler: MPO-Datei konnte nicht verarbeitet werden.'); } } else { // Dokumentation anzeigen ?> MPO Image Reader Demo

MPO Image Reader Demo

Was ist MPO?
MPO (Multi Picture Object) ist ein Dateiformat für stereoskopische 3D-Bilder. Es wird verwendet von: Nintendo 3DS, Fujifilm FinePix Real 3D, und anderen Stereo-Kameras. Jede MPO-Datei enthält zwei JPEG-Bilder (für linkes und rechtes Auge).

Funktionsweise

Diese Demo-Datei verarbeitet MPO-Dateien und erstellt ein zusammengesetztes Bild mit:

Verwendung als PHP-Funktion

<?php
// MPO-Datei laden und verarbeiten
$image = mpo_read_image('pfad/zur/datei.mpo');

if ($image !== false) {
    // Als PNG ausgeben
    header('Content-Type: image/png');
    imagepng($image);
    imagedestroy($image);
}
?>

Verwendung als Web-Interface

mpo_demo.php?file=pfad/zur/datei.mpo
mpo_demo.php?file=https://example.com/bild.mpo

Konfiguration

Am Anfang der Datei (Zeile 11-20) können folgende Einstellungen angepasst werden:

Unterstützte MPO-Formate

Diese Demo unterstützt verschiedene MPO-Varianten:

3D-Betrachtung

Anaglyph (Rot-Cyan 3D-Brille)

Setzen Sie eine Rot-Cyan 3D-Brille auf (Rot = links, Cyan = rechts). Das große Bild wird dann räumlich erscheinen.

Cross-View (ohne Brille)

Schauen Sie auf die beiden kleinen Bilder oben.
Kreuzen Sie Ihre Augen, sodass die weißen Punkte sich überlagern.
Zwischen den beiden Bildern erscheint ein drittes, mittiges Bild in 3D.

Technische Details

Hauptfunktionen: