$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 ?>
Diese Demo-Datei verarbeitet MPO-Dateien und erstellt ein zusammengesetztes Bild mit:
<?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);
}
?>
mpo_demo.php?file=pfad/zur/datei.mpompo_demo.php?file=https://example.com/bild.mpo
Am Anfang der Datei (Zeile 11-20) können folgende Einstellungen angepasst werden:
MPO_STEREO_IMAGE (true/false) - Side-by-Side Stereo-Ansicht anzeigenMPO_STEREO_DOTS (true/false) - Weiße Ausrichtungspunkte anzeigenMPO_STEREO_MAX_WIDTH (Pixel) - Max. Breite der Stereo-BilderMPO_STEREO_MAX_HEIGHT (Pixel) - Max. Höhe der Stereo-BilderMPO_FULL_IMAGE (true/false) - Großes Bild anzeigenMPO_FULL_ANAGLYPH (true/false) - Als Anaglyph (Rot-Cyan 3D)MPO_FULL_MAX_WIDTH (Pixel) - Max. Breite des großen BildesMPO_FULL_MAX_HEIGHT (Pixel) - Max. Höhe des großen BildesMPO_SPACING (Pixel) - Abstand zwischen ElementenDiese Demo unterstützt verschiedene MPO-Varianten:
Hauptfunktionen:
mpo_read_image($file) - Hauptfunktion zum Laden und Verarbeitenmpo_find_images($mpo) - Findet JPEG-Positionen in der MPO-Dateimpo_create_stereo_image($mpo, $offsets) - Erstellt Stereo-Ausgabebildmpo_create_anaglyph($left, $right, $w, $h) - Erstellt Anaglyph-Bildmpo_aspect_resize($w, $h, $max_w, $max_h, $enlarge) - Größenberechnung