Files
server/usr/share/psa-pear/pear/php/Horde/Image/Exif.php
2026-01-07 20:52:11 +01:00

618 lines
26 KiB
PHP

<?php
/**
* Copyright 2003-2017 Horde LLC (http://www.horde.org/)
*
* See the enclosed file LICENSE for license information (LGPL). If you
* did not receive this file, see http://www.horde.org/licenses/lgpl21.
*
* @author Chuck Hagenbuch <chuck@horde.org>
* @author Michael J. Rubinsky <mrubinsk@horde.org>
* @category Horde
* @license http://www.horde.org/licenses/lgpl21 LGPL-2.1
* @package Image
*/
/**
* General class for fetching and parsing EXIF information from images.
*
* Works equally well with either the built in php exif functions (if PHP
* compiled with exif support), the Exiftool package (more complete but
* slower), or the bundled exif library.
*
* @author Chuck Hagenbuch <chuck@horde.org>
* @author Michael J. Rubinsky <mrubinsk@horde.org>
* @category Horde
* @copyright 2003-2017 Horde LLC
* @license http://www.horde.org/licenses/lgpl21 LGPL-2.1
* @package Image
*/
class Horde_Image_Exif
{
protected static $_titleFields = array(
'IPTC' => array('ObjectName'),
'XMP' => array('Title'),
'EXIF' => array(),
'COMPOSITE' => array()
);
protected static $_descriptionFields = array(
'IPTC' => array('Caption-Abstract'),
'XMP' => array('Description'),
'EXIF' => array('ImageDescription'),
'COMPOSITE' => array()
);
/**
* Factory method for instantiating a Horde_Image_Exif object.
*
* @param string $driver
* @param array $params
*
* @return Horde_Image_Exif
*/
public static function factory($driver = null, $params = array())
{
if (empty($driver) && function_exists('exif_read_data')) {
$driver = 'Php';
} elseif (empty($driver)) {
$driver = 'Bundled';
} else {
$driver = basename($driver);
}
$class = 'Horde_Image_Exif_' . $driver;
return new $class($params);
}
/**
* Converts from Intel to Motorola endien.
*
* Just reverses the bytes (assumes hex is passed in)
*
* @param string $intel
*
* @return string
*/
public static function intel2Moto($intel)
{
$len = strlen($intel);
$moto = '';
for ($i = 0; $i <= $len; $i += 2) {
$moto .= substr($intel, $len - $i, 2);
}
return $moto;
}
/**
* Obtains an array of supported meta data fields.
*
* @TODO: This should probably be extended by the subclass?
*
* @return array
*/
public static function getCategories()
{
return array(
'IPTC' => array(
'Keywords' => array('description' => Horde_Image_Translation::t("Image keywords"), 'type' => 'array'),
'ObjectName' => array('description' => Horde_Image_Translation::t("Image Title"), 'type' => 'text'),
'By-line' => array('description' => Horde_Image_Translation::t("By"), 'type' => 'text'),
'CopyrightNotice' => array('description' => Horde_Image_Translation::t("Copyright"), 'type' => 'text'),
'Caption-Abstract' => array('description' => Horde_Image_Translation::t("Caption"), 'type' => 'text'),
),
'XMP' => array(
'Creator' => array('description' => Horde_Image_Translation::t("Image Creator"), 'type' => 'text'),
'Rights' => array('description' => Horde_Image_Translation::t("Rights"), 'type' => 'text'),
'UsageTerms' => array('description' => Horde_Image_Translation::t("Usage Terms"), 'type' => 'text'),
'Title' => array('description' => Horde_Image_Translation::t("Title"), 'type' => 'text'),
'Description' => array('description' => Horde_Image_Translation::t("Description"), 'type' => 'text'),
),
'EXIF' => array(
'DateTime' => array('description' => Horde_Image_Translation::t("Date Photo Modified"), 'type' => 'date'),
'DateTimeOriginal' => array('description' => Horde_Image_Translation::t("Date Photo Taken"), 'type' => 'date'),
'DateTimeDigitized' => array('description' => Horde_Image_Translation::t("Date Photo Digitized"), 'type' => 'date'),
'GPSLatitude' => array('description' => Horde_Image_Translation::t("Latitude"), 'type' => 'gps'),
'GPSLongitude' => array('description' => Horde_Image_Translation::t("Longitude"), 'type' => 'gps'),
'Make' => array('description' => Horde_Image_Translation::t("Camera Make"), 'type' => 'text'),
'Model' => array('description' => Horde_Image_Translation::t("Camera Model"), 'type' => 'text'),
'Software' => array('description' => Horde_Image_Translation::t("Software Version"), 'type' => 'text'),
'ImageType' => array('description' => Horde_Image_Translation::t("Photo Type"), 'type' => 'text'),
'ImageDescription' => array('description' => Horde_Image_Translation::t("Photo Description"), 'type' => 'text'),
'FileSize' => array('description' => Horde_Image_Translation::t("File Size"), 'type' => 'number'),
'ExifImageWidth' => array('description' => Horde_Image_Translation::t("Width"), 'type' => 'number'),
'ExifImageLength' => array('description' => Horde_Image_Translation::t("Height"), 'type' => 'number'),
'XResolution' => array('description' => Horde_Image_Translation::t("X Resolution"), 'type' => 'number'),
'YResolution' => array('description' => Horde_Image_Translation::t("Y Resolution"), 'type' => 'number'),
'ResolutionUnit' => array('description' => Horde_Image_Translation::t("Resolution Unit"), 'type' => 'text'),
'ShutterSpeedValue' => array('description' => Horde_Image_Translation::t("Shutter Speed"), 'type' => 'number'),
'ExposureTime' => array('description' => Horde_Image_Translation::t("Exposure"), 'type' => 'number'),
'FocalLength' => array('description' => Horde_Image_Translation::t("Focal Length"), 'type' => 'number'),
'FocalLengthIn35mmFilm' => array('description' => Horde_Image_Translation::t("Focal Length (35mm equiv)"), 'type' => 'number'),
'ApertureValue' => array('description' => Horde_Image_Translation::t("Aperture"), 'type' => 'number'),
'FNumber' => array('description' => Horde_Image_Translation::t("F-Number"), 'type' => 'number'),
'ISOSpeedRatings' => array('description' => Horde_Image_Translation::t("ISO Setting"), 'type' => 'number'),
'ExposureBiasValue' => array('description' => Horde_Image_Translation::t("Exposure Bias"), 'type' => 'number'),
'ExposureMode' => array('description' => Horde_Image_Translation::t("Exposure Mode"), 'type' => 'number'),
'ExposureProgram' => array('description' => Horde_Image_Translation::t("Exposure Program"), 'type' => 'number'),
'MeteringMode' => array('description' => Horde_Image_Translation::t("Metering Mode"), 'type' => 'number'),
'Flash' => array('description' => Horde_Image_Translation::t("Flash Setting"), 'type' => 'number'),
'UserComment' => array('description' => Horde_Image_Translation::t("User Comment"), 'type' => 'text'),
'ColorSpace' => array('description' => Horde_Image_Translation::t("Color Space"), 'type' => 'number'),
'SensingMethod' => array('description' => Horde_Image_Translation::t("Sensing Method"), 'type' => 'number'),
'WhiteBalance' => array('description' => Horde_Image_Translation::t("White Balance"), 'type' => 'number'),
'Orientation' => array('description' => Horde_Image_Translation::t("Camera Orientation"), 'type' => 'number'),
'Copyright' => array('description' => Horde_Image_Translation::t("Copyright"), 'type' => 'text'),
'Artist' => array('description' => Horde_Image_Translation::t("Artist"), 'type' => 'text'),
'LightSource' => array('description' => Horde_Image_Translation::t("Light source"), 'type' => 'number'),
'ImageStabalization' => array('description' => Horde_Image_Translation::t("Image Stabilization"), 'type' => 'text'),
'SceneCaptureType' => array('description' => Horde_Image_Translation::t("Scene Type"), 'type' => 'number'),
),
'COMPOSITE' => array(
'LensID' => array('description' => Horde_Image_Translation::t("Lens Id"), 'type' => 'text'),
'Lens' => array('description' => 'Lens', 'type' => 'text'),
'Aperture' => array('description' => Horde_Image_Translation::t("Aperture"), 'type' => 'text'),
'DOF' => array('description' => Horde_Image_Translation::t("Depth of Field"), 'type' => 'text'),
'FOV' => array('description' => Horde_Image_Translation::t("Field of View"), 'type' => 'text')
)
);
}
/**
* Returns a list of metadata fields that can by used for image titles.
*
* @param mixed $driver A Horde_Image_Exif_Base instance or a string
* specifying the driver in use.
*
* @return array An array of metadata field name hashes.
* @since 2.1.0
*/
public static function getTitleFields($driver = null)
{
if (!is_null($driver) && is_array($driver)) {
$driver = self::factory($driver[0], $driver[1]);
}
if ($driver instanceof Horde_Image_Exif_Base) {
$supported = $driver->supportedCategories();
} else {
$supported = array('XMP', 'IPTC', 'EXIF');
}
$fields = array();
foreach ($supported as $category) {
$fields = array_merge($fields, self::$_titleFields[$category]);
}
$return = array();
$all = self::getFields($driver, true);
foreach ($fields as $field) {
$return[$field] = $all[$field];
}
return $return;
}
/**
* Returns a list of metadata fields that can by used for image
* descriptions.
*
* @param mixed $driver A Horde_Image_Exif_Base instance or a string
* specifying the driver in use.
*
* @return array An array of metadata field hashes.
* @since 2.1.0
*/
public static function getDescriptionFields($driver = null)
{
$map = self::getCategories();
if (!is_null($driver) && is_array($driver)) {
$driver = self::factory($driver[0], $driver[1]);
}
if ($driver instanceof Horde_Image_Exif_Base) {
$supported = $driver->supportedCategories();
} else {
$supported = array('XMP', 'IPTC', 'EXIF');
}
$fields = array();
foreach ($supported as $category) {
$fields = array_merge($fields, self::$_descriptionFields[$category]);
}
$return = array();
$all = self::getFields($driver, true);
foreach ($fields as $field) {
$return[$field] = $all[$field];
}
return $return;
}
/**
* Returns a flattened array of supported metadata fields.
*
* @param mixed $driver A Horde_Image_Exif_Base instance or a
* string specifying the driver in use.
* @param boolean $description_only Only return the field descriptions.
*
* @return array
*/
public static function getFields($driver = null, $description_only = false)
{
if (!is_null($driver) && is_array($driver)) {
$driver = self::factory($driver[0], $driver[1]);
}
if ($driver instanceof Horde_Image_Exif_Base) {
$supported = $driver->supportedCategories();
} else {
$supported = array('XMP', 'IPTC', 'EXIF' );
}
$categories = self::getCategories();
$flattened = array();
foreach ($supported as $category) {
$flattened = array_merge($flattened, $categories[$category]);
}
if ($description_only) {
foreach ($flattened as $key => $data) {
$return[$key] = $data['description'];
}
return $return;
}
return $flattened;
}
/**
* More human friendly exposure formatting.
*/
protected static function _formatExposure($data)
{
if ($data > 0) {
if ($data > 1) {
return sprintf(
Horde_Image_Translation::t("%d sec"),
round($data, 2)
);
} else {
$n = $d = 0;
self::_convertToFraction($data, $n, $d);
if ($n <> 1) {
return sprintf(
Horde_Image_Translation::t("%4f sec"), $n / $d
);
}
return sprintf(
Horde_Image_Translation::t("%s / %s sec"), $n, $d
);
}
} else {
return Horde_Image_Translation::t("Bulb");
}
}
/**
* Converts a floating point number into a fraction.
*
* Many thanks to Matthieu Froment for this code.
*
* (Ported from the Exifer library).
*/
protected static function _convertToFraction($v, &$n, &$d)
{
$MaxTerms = 15; // Limit to prevent infinite loop
$MinDivisor = 0.000001; // Limit to prevent divide by zero
$MaxError = 0.00000001; // How close is enough
// Initialize fraction being converted
$f = $v;
// Initialize fractions with 1/0, 0/1
$n_un = 1;
$d_un = 0;
$n_deux = 0;
$d_deux = 1;
for ($i = 0; $i < $MaxTerms; $i++) {
$a = floor($f); // Get next term
$f = $f - $a; // Get new divisor
$n = $n_un * $a + $n_deux; // Calculate new fraction
$d = $d_un * $a + $d_deux;
$n_deux = $n_un; // Save last two fractions
$d_deux = $d_un;
$n_un = $n;
$d_un = $d;
// Quit if dividing by zero
if ($f < $MinDivisor) {
break;
}
if (abs($v - $n / $d) < $MaxError) {
break;
}
// reciprocal
$f = 1 / $f;
}
}
/**
* Converts an exif field into human-readable form.
*
* Some of these cases are ported from the Exifer library, others were
* changed from their implementation where the EXIF standard dictated
* different behaviour.
*
* @param string $field The name of the field to translate.
* @param string $data The data value to translate.
*
* @return string The converted data.
*/
public static function getHumanReadable($field, $data)
{
switch ($field) {
case 'ExposureMode':
switch ($data) {
case 0: return Horde_Image_Translation::t("Auto exposure");
case 1: return Horde_Image_Translation::t("Manual exposure");
case 2: return Horde_Image_Translation::t("Auto bracket");
default: return Horde_Image_Translation::t("Unknown");
}
case 'ExposureProgram':
switch ($data) {
case 1: return Horde_Image_Translation::t("Manual");
case 2: return Horde_Image_Translation::t("Normal Program");
case 3: return Horde_Image_Translation::t("Aperture Priority");
case 4: return Horde_Image_Translation::t("Shutter Priority");
case 5: return Horde_Image_Translation::t("Creative");
case 6: return Horde_Image_Translation::t("Action");
case 7: return Horde_Image_Translation::t("Portrait");
case 8: return Horde_Image_Translation::t("Landscape");
default: return Horde_Image_Translation::t("Unknown");
}
case 'XResolution':
case 'YResolution':
if (strpos($data, '/') !== false) {
list($n, $d) = explode('/', $data, 2);
return sprintf(Horde_Image_Translation::t("%d dots per unit"), $n);
}
return sprintf(Horde_Image_Translation::t("%d per unit"), $data);
case 'ResolutionUnit':
switch ($data) {
case 1: return Horde_Image_Translation::t("Pixels");
case 2: return Horde_Image_Translation::t("Inch");
case 3: return Horde_Image_Translation::t("Centimeter");
default: return Horde_Image_Translation::t("Unknown");
}
case 'ExifImageWidth':
case 'ExifImageLength':
return sprintf(Horde_Image_Translation::t("%d pixels"), $data);
case 'Orientation':
switch ($data) {
case 1:
return sprintf(Horde_Image_Translation::t("Normal (O deg)"));
case 2:
return sprintf(Horde_Image_Translation::t("Mirrored"));
case 3:
return sprintf(Horde_Image_Translation::t("Upsidedown"));
case 4:
return sprintf(Horde_Image_Translation::t("Upsidedown Mirrored"));
case 5:
return sprintf(Horde_Image_Translation::t("90 deg CW Mirrored"));
case 6:
return sprintf(Horde_Image_Translation::t("90 deg CCW"));
case 7:
return sprintf(Horde_Image_Translation::t("90 deg CCW Mirrored"));
case 8:
return sprintf(Horde_Image_Translation::t("90 deg CW"));
}
break;
case 'ExposureTime':
if (strpos($data, '/') !== false) {
list($n, $d) = explode('/', $data, 2);
if ($d == 0) {
return;
}
$data = $n / $d;
}
return self::_formatExposure($data);
case 'ShutterSpeedValue':
if (strpos($data, '/') !== false) {
list($n, $d) = explode('/', $data, 2);
if ($d == 0) {
return;
}
$data = $n / $d;
}
$data = exp($data * log(2));
if ($data > 0) {
$data = 1 / $data;
}
return self::_formatExposure($data);
case 'ApertureValue':
case 'MaxApertureValue':
if (strpos($data, '/') !== false) {
list($n, $d) = explode('/', $data, 2);
if ($d == 0) {
return;
}
$data = $n / $d;
$data = exp(($data * log(2)) / 2);
// Precision is 1 digit.
$data = round($data, 1);
}
return 'f/' . $data;
case 'FocalLength':
if (strpos($data, '/') !== false) {
list($n, $d) = explode('/', $data, 2);
if ($d == 0) {
return;
}
return sprintf(Horde_Image_Translation::t("%d mm"), round($n / $d));
}
return sprintf(Horde_Image_Translation::t("%d mm"), $data);
case 'FNumber':
if (strpos($data, '/') !== false) {
list($n, $d) = explode('/', $data, 2);
if ($d != 0) {
return 'f/' . round($n / $d, 1);
}
}
return 'f/' . $data;
case 'ExposureBiasValue':
if (strpos($data, '/') !== false) {
list($n, $d) = explode('/', $data, 2);
if ($n == 0) {
return '0 EV';
}
}
return $data . ' EV';
case 'MeteringMode':
switch ($data) {
case 0: return Horde_Image_Translation::t("Unknown");
case 1: return Horde_Image_Translation::t("Average");
case 2: return Horde_Image_Translation::t("Center Weighted Average");
case 3: return Horde_Image_Translation::t("Spot");
case 4: return Horde_Image_Translation::t("Multi-Spot");
case 5: return Horde_Image_Translation::t("Multi-Segment");
case 6: return Horde_Image_Translation::t("Partial");
case 255: return Horde_Image_Translation::t("Other");
default: return sprintf(Horde_Image_Translation::t("Unknown: %s"), $data);
}
break;
case 'LightSource':
switch ($data) {
case 1: return Horde_Image_Translation::t("Daylight");
case 2: return Horde_Image_Translation::t("Fluorescent");
case 3: return Horde_Image_Translation::t("Tungsten");
case 4: return Horde_Image_Translation::t("Flash");
case 9: return Horde_Image_Translation::t("Fine weather");
case 10: return Horde_Image_Translation::t("Cloudy weather");
case 11: return Horde_Image_Translation::t("Shade");
case 12: return Horde_Image_Translation::t("Daylight fluorescent");
case 13: return Horde_Image_Translation::t("Day white fluorescent");
case 14: return Horde_Image_Translation::t("Cool white fluorescent");
case 15: return Horde_Image_Translation::t("White fluorescent");
case 17: return Horde_Image_Translation::t("Standard light A");
case 18: return Horde_Image_Translation::t("Standard light B");
case 19: return Horde_Image_Translation::t("Standard light C");
case 20: return 'D55';
case 21: return 'D65';
case 22: return 'D75';
case 23: return 'D50';
case 24: return Horde_Image_Translation::t("ISO studio tungsten");
case 255: return Horde_Image_Translation::t("other light source");
default: return Horde_Image_Translation::t("Unknown");
}
case 'WhiteBalance':
switch ($data) {
case 0: return Horde_Image_Translation::t("Auto");
case 1: return Horde_Image_Translation::t("Manual");
default: Horde_Image_Translation::t("Unknown");
}
break;
case 'FocalLengthIn35mmFilm':
return $data . ' mm';
case 'Flash':
switch ($data) {
case 0: return Horde_Image_Translation::t("No Flash");
case 1: return Horde_Image_Translation::t("Flash");
case 5: return Horde_Image_Translation::t("Flash, strobe return light not detected");
case 7: return Horde_Image_Translation::t("Flash, strobe return light detected");
case 9: return Horde_Image_Translation::t("Compulsory Flash");
case 13: return Horde_Image_Translation::t("Compulsory Flash, Return light not detected");
case 15: return Horde_Image_Translation::t("Compulsory Flash, Return light detected");
case 16: return Horde_Image_Translation::t("No Flash");
case 24: return Horde_Image_Translation::t("No Flash");
case 25: return Horde_Image_Translation::t("Flash, Auto-Mode");
case 29: return Horde_Image_Translation::t("Flash, Auto-Mode, Return light not detected");
case 31: return Horde_Image_Translation::t("Flash, Auto-Mode, Return light detected");
case 32: return Horde_Image_Translation::t("No Flash");
case 65: return Horde_Image_Translation::t("Red Eye");
case 69: return Horde_Image_Translation::t("Red Eye, Return light not detected");
case 71: return Horde_Image_Translation::t("Red Eye, Return light detected");
case 73: return Horde_Image_Translation::t("Red Eye, Compulsory Flash");
case 77: return Horde_Image_Translation::t("Red Eye, Compulsory Flash, Return light not detected");
case 79: return Horde_Image_Translation::t("Red Eye, Compulsory Flash, Return light detected");
case 89: return Horde_Image_Translation::t("Red Eye, Auto-Mode");
case 93: return Horde_Image_Translation::t("Red Eye, Auto-Mode, Return light not detected");
case 95: return Horde_Image_Translation::t("Red Eye, Auto-Mode, Return light detected");
}
break;
case 'FileSize':
if ($data <= 0) {
return '0 Bytes';
}
$s = array('B', 'kB', 'MB', 'GB');
$e = floor(log($data, 1024));
return round($data/pow(1024, $e), 2) . ' ' . $s[$e];
case 'SensingMethod':
switch ($data) {
case 1: return Horde_Image_Translation::t("Not defined");
case 2: return Horde_Image_Translation::t("One Chip Color Area Sensor");
case 3: return Horde_Image_Translation::t("Two Chip Color Area Sensor");
case 4: return Horde_Image_Translation::t("Three Chip Color Area Sensor");
case 5: return Horde_Image_Translation::t("Color Sequential Area Sensor");
case 7: return Horde_Image_Translation::t("Trilinear Sensor");
case 8: return Horde_Image_Translation::t("Color Sequential Linear Sensor");
default: return Horde_Image_Translation::t("Unknown");
}
case 'ColorSpace':
switch ($data) {
case 1: return Horde_Image_Translation::t("sRGB");
default: return Horde_Image_Translation::t("Uncalibrated");
}
case 'SceneCaptureType':
switch ($data) {
case 0: return Horde_Image_Translation::t("Standard");
case 1: return Horde_Image_Translation::t("Landscape");
case 2: return Horde_Image_Translation::t("Portrait");
case 3: return Horde_Image_Translation::t("Night Scene");
default: return Horde_Image_Translation::t("Unknown");
}
case 'DateTime':
case 'DateTimeOriginal':
case 'DateTimeDigitized':
return date('m/d/Y H:i:s O', $data);
case 'UserComment':
//@TODO: the first 8 bytes of this field contain the charset used
// to encode the comment. Either ASCII, JIS, UNICODE, or
// UNDEFINED. Should probably either convert to a known charset
// here and let the calling code deal with it, or allow this
// method to take an optional charset to convert to (would
// introduce a dependency on Horde_String to do the conversion).
$data = trim(substr($data, 7)) ;
default:
return !empty($data) ? $data : '---';
}
}
}