This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<?php | |
$exif = new Exif("test.jpg"); | |
$dt = "2011:08:10 12:00:00"; | |
$exif->add_entry("DateTime", TagEntry::FORMAT_ASCII_STRINGS, 19, $dt); | |
$exif->add_entry("DateTimeOriginal", TagEntry::FORMAT_ASCII_STRINGS, 19, $dt); | |
$exif->add_entry("DateTimeDigitized", TagEntry::FORMAT_ASCII_STRINGS, 19, $dt); | |
file_put_contents("test2.jpg", $exif->to_binary()); | |
class TagEntry { | |
const BYTE_SIZE = 12; | |
const FORMAT_UNSIGNED_BYTE = 1; | |
const FORMAT_ASCII_STRINGS = 2; | |
const FORMAT_UNSIGNED_SHORT = 3; | |
const FORMAT_UNSIGNED_LONG = 4; | |
const FORMAT_UNSIGNED_RATIONAL = 5; | |
const FORMAT_SIGNED_BYTE = 6; | |
const FORMAT_UNDEFINED = 7; | |
const FORMAT_SIGNED_SHORT = 8; | |
const FORMAT_SIGNED_LONG = 9; | |
const FORMAT_SIGNED_RATIONAL = 10; | |
const FORMAT_SIGNED_FLOAT = 11; | |
const FORMAT_DOUBLE_FLOAT = 12; | |
var $_data_size = array( | |
self::FORMAT_UNSIGNED_BYTE => 1, | |
self::FORMAT_ASCII_STRINGS => 1, | |
self::FORMAT_UNSIGNED_SHORT => 2, | |
self::FORMAT_UNSIGNED_LONG => 4, | |
self::FORMAT_UNSIGNED_RATIONAL => 8, | |
self::FORMAT_SIGNED_BYTE => 1, | |
self::FORMAT_UNDEFINED => 1, | |
self::FORMAT_SIGNED_SHORT => 2, | |
self::FORMAT_SIGNED_LONG => 4, | |
self::FORMAT_SIGNED_RATIONAL => 8, | |
self::FORMAT_SIGNED_FLOAT => 4, | |
self::FORMAT_DOUBLE_FLOAT => 8, | |
); | |
var $_tag; // 0xXXXX | |
var $_format; | |
var $_num; | |
var $_val; | |
var $_offset = 0; | |
function __construct($tag, $format, $num, $val){ | |
$this->_tag = $tag; | |
$this->_format = $format; | |
$this->_num = $num; | |
$this->_val = $val; | |
} | |
function is_over_size(){ | |
$size = $this->size(); | |
return ($size > 4); | |
} | |
function size(){ | |
return $this->_data_size[$this->_format] * $this->_num; | |
} | |
function to_binary(){ | |
$data = ""; | |
$data .= pack("v", $this->_tag); | |
$data .= pack("v", $this->_format); | |
$data .= pack("V", $this->_num); | |
if($this->_offset){ | |
$data .= pack("V", $this->_offset); | |
} | |
else{ | |
$data .= $this->_val; | |
} | |
return $data; | |
} | |
} | |
class IFD { | |
var $_enabled_tags; | |
var $_entries; | |
function __construct(){ | |
$this->_enabled_tags = array(); | |
$this->_entries = array(); | |
} | |
function is_enabled_tag($tag){ | |
return ( ! empty($this->_enabled_tags[$tag])); | |
} | |
function add_entry($tag, $format, $num, $val){ | |
if( ! $this->is_enabled_tag($tag)){ | |
return FALSE; | |
} | |
$this->_entries[$tag] = new TagEntry($this->_enabled_tags[$tag], $format, $num, $val); | |
} | |
function has_entry(){ | |
return (count(array_values($this->_entries)) > 0); | |
} | |
function to_binary($base, $has_next_ifd = FALSE){ | |
if( ! $this->has_entry()){ | |
return array(0, "", "", ""); | |
} | |
$entries = array_values($this->_entries); | |
$entries_count = count($entries); | |
// pointer of base + byte of entry_count_area + byte of entries + byte of next_link_area | |
$data_section_base = $base + 2 + TagEntry::BYTE_SIZE * count($entries) + 4; | |
$dir_section = ""; | |
$next_link = ""; | |
$data_section = ""; | |
// 2byte - number of entry | |
$dir_section .= pack("v", $entries_count); | |
// 12byte * n - entries | |
foreach($entries as $entry){ | |
if($entry->is_over_size()){ | |
$size = $entry->size(); | |
$val = $entry->_val; | |
$entry->_offset = $data_section_base; | |
$data_section_base += $size; | |
$data_section .= $val; | |
} | |
$dir_section .= $entry->to_binary(); | |
} | |
// 4byte - offset to next ifd | |
if($has_next_ifd){ | |
$size = strlen($dir_section) + 4 + strlen($data_section); | |
$next_link .= pack("V", $base + $size); | |
} | |
else{ | |
$next_link .= pack("V", 0); | |
} | |
return array(strlen($dir_section . $next_link . $data_section), $dir_section, $next_link, $data_section); | |
} | |
} | |
class IFD0 extends IFD { | |
function __construct(){ | |
parent::__construct(); | |
$this->_enabled_tags = array( | |
"ImageDescription" => 0x010e, | |
"Make" => 0x010f, | |
"Model" => 0x0110, | |
"Orientation" => 0x0112, | |
"XResolution" => 0x011a, | |
"YResolution" => 0x011b, | |
"ResolutionUnit" => 0x0128, | |
"Software" => 0x0131, | |
"DateTime" => 0x0132, | |
"WhitePoint" => 0x013e, | |
"PrimaryChromaticities" => 0x013f, | |
"YCbCrCoefficients" => 0x0211, | |
"YCbCrPositioning" => 0x0213, | |
"ReferenceBlackWhite" => 0x0214, | |
"Copyright" => 0x8298, | |
"ExifIFDPointer" => 0x8769, | |
); | |
} | |
function to_binary($base, $has_next_ifd = FALSE){ | |
if( ! $this->has_entry()){ | |
return array(0, "", "", ""); | |
} | |
// dummy entry | |
$this->add_entry("ExifIFDPointer", TagEntry::FORMAT_UNSIGNED_LONG, 1, pack("V", 0)); | |
list($size, $dir, $link, $data) = parent::to_binary($base, $has_next_ifd); | |
$exif_ifd_base = $base + $size; | |
// set real entry | |
$this->add_entry("ExifIFDPointer", TagEntry::FORMAT_UNSIGNED_LONG, 1, pack("V", $exif_ifd_base)); | |
// recalc | |
return parent::to_binary($base, $has_next_ifd); | |
} | |
} | |
class IFDExif extends IFD { | |
function __construct(){ | |
parent::__construct(); | |
$this->_enabled_tags = array( | |
"ExposureTime" => 0x829a, | |
"FNumber" => 0x829d, | |
"ExposureProgram" => 0x8822, | |
"ISOSpeedRatings" => 0x8827, | |
"ExifVersion" => 0x9000, | |
"DateTimeOriginal" => 0x9003, | |
"DateTimeDigitized" => 0x9004, | |
"ComponentsConfiguration" => 0x9101, | |
"CompressedBitsPerPixel" => 0x9102, | |
"ShutterSpeedValue" => 0x9201, | |
"ApertureValue" => 0x9202, | |
"BrightnessValue" => 0x9203, | |
"ExposureBiasValue" => 0x9204, | |
"MaxApertureValue" => 0x9205, | |
"SubjectDistance" => 0x9206, | |
"MeteringMode" => 0x9207, | |
"LightSource" => 0x9208, | |
"Flash" => 0x9209, | |
"FocalLength" => 0x920a, | |
"MakerNote" => 0x927c, | |
"UserComment" => 0x9286, | |
"SubsecTime" => 0x9290, | |
"SubsecTimeOriginal" => 0x9291, | |
"SubsecTimeDigitized" => 0x9292, | |
"FlashPixVersion" => 0xa000, | |
"ColorSpace" => 0xa001, | |
"ExifImageWidth" => 0xa002, | |
"ExifImageHeight" => 0xa003, | |
"RelatedSoundFile" => 0xa004, | |
"InteroperabilityIFDPointer" => 0xa005, | |
"FocalPlaneXResolution" => 0xa20e, | |
"FocalPlaneYResolution" => 0xa20f, | |
"FocalPlaneResolutionUnit" => 0xa210, | |
"ExposureIndex" => 0xa215, | |
"SensingMethod" => 0xa217, | |
"FileSource" => 0xa300, | |
"SceneType" => 0xa301, | |
"CFAPattern" => 0xa302, | |
); | |
} | |
} | |
class IFD1 extends IFD { | |
function __construct(){ | |
parent::__construct(); | |
$this->_enabled_tags = array( | |
"ImageWidth" => 0x0100, | |
"ImageLength" => 0x0101, | |
"BitsPerSample" => 0x0102, | |
"Compression" => 0x0103, | |
"PhotometricInterpretation" => 0x0106, | |
"StripOffsets" => 0x0111, | |
"Orientation" => 0x0112, | |
"SamplesPerPixel" => 0x0115, | |
"RowsPerStrip" => 0x0116, | |
"StripByteConunts" => 0x0117, | |
"XResolution" => 0x011a, | |
"YResolution" => 0x011b, | |
"PlanarConfiguration" => 0x011c, | |
"ResolutionUnit" => 0x0128, | |
"JpegInterchangeFormat" => 0x0201, | |
"JpegInterchangeFormatLength" => 0x0202, | |
"YCbCrCoefficients" => 0x0211, | |
"YCbCrSubSampling" => 0x0212, | |
"YCbCrPositioning" => 0x0213, | |
"ReferenceBlackWhite" => 0x0214, | |
); | |
} | |
} | |
class Exif { | |
var $_ifd0; | |
var $_ifdexif; | |
var $_ifd1; | |
var $_path; | |
function __construct($path){ | |
$this->_ifd0 = new IFD0(); | |
$this->_ifdexif = new IFDExif(); | |
$this->_ifd1 = new IFD1(); | |
$this->_path = $path; | |
$this->_ifd0->add_entry("Make", TagEntry::FORMAT_ASCII_STRINGS, 4, str_pad("", 4)); | |
$this->_ifd0->add_entry("Model", TagEntry::FORMAT_ASCII_STRINGS, 4, str_pad("", 4)); | |
$this->_ifd0->add_entry("DateTime", TagEntry::FORMAT_ASCII_STRINGS, 19, str_pad("", 19)); | |
$this->_ifdexif->add_entry("DateTimeOriginal", TagEntry::FORMAT_ASCII_STRINGS, 19, str_pad("", 19)); | |
$this->_ifdexif->add_entry("DateTimeDigitized", TagEntry::FORMAT_ASCII_STRINGS, 19, str_pad("", 19)); | |
} | |
function add_entry($tag, $format, $num, $val){ | |
if($this->_ifd0->is_enabled_tag($tag)){ | |
$this->_ifd0->add_entry($tag, $format, $num, $val); | |
} | |
else if($this->_ifdexif->is_enabled_tag($tag)){ | |
$this->_ifdexif->add_entry($tag, $format, $num, $val); | |
} | |
else if($this->_ifd1->is_enabled_tag($tag)){ | |
$this->_ifd1->add_entry($tag, $format, $num, $val); | |
} | |
else{ | |
return FALSE; | |
} | |
} | |
function to_binary(){ | |
$reader = new Reader($this->_path); | |
$data = ""; | |
// SOI(2byte) | |
$data .= $reader->read(2); | |
// APP0 | |
$marker = $reader->read_unsigned_short(); | |
if($marker == 0xFFE0){ | |
$size = $reader->read_unsigned_short(); | |
$data .= pack("n", $marker); | |
$data .= pack("n", $size); | |
$data .= $reader->read($size - 2); // exclude size area | |
} | |
else{ | |
// back | |
$reader->seek(-2); | |
} | |
// APP1 | |
$app1 = ""; | |
// Exif header | |
$app1 .= "Exif" . pack("n", 0x0000); | |
// Tiff header - intel format | |
$base = 8; | |
$app1 .= "II" . pack("v", 0x002A) . pack("V", $base); | |
$current_base = $base; | |
// IFD0 | |
list($ifd0_size, $ifd0_dir, $ifd0_link, $ifd0_data) = | |
$this->_ifd0->to_binary($current_base, $this->_ifd1->has_entry()); | |
$current_base += $ifd0_size; | |
// ExifIFD | |
list($ifdexif_size, $ifdexif_dir, $ifdexif_link, $ifdexif_data) = $this->_ifdexif->to_binary($current_base); | |
$current_base += $ifdexif_size; | |
// IFD1 | |
list($ifd1_size, $ifd1_dir, $ifd1_link, $ifd1_data) = $this->_ifd1->to_binary($current_base); | |
$current_base += $ifd1_size; | |
$app1 .= $ifd0_dir; | |
if($this->_ifd1->has_entry()){ | |
$app1 .= pack("V", $base + $ifd0_size + $ifdexif_size); | |
} | |
else{ | |
$app1 .= $ifd0_link; | |
} | |
$app1 .= $ifd0_data; | |
$app1 .= $ifdexif_dir . $ifdexif_link . $ifdexif_data; | |
$app1 .= $ifd1_dir . $ifd1_link . $ifd1_data; | |
if($ifd0_size + $ifdexif_size + $ifd1_size > 0){ | |
// Marker | |
$data .= pack("n", 0xFFE1); | |
// App1 size | |
$data .= pack("n", 2 + strlen($app1)); | |
// App1 data | |
$data .= $app1; | |
} | |
// remain data | |
$data .= $reader->read(); | |
$reader->close(); | |
return $data; | |
} | |
} | |
class Reader { | |
const BYTE_ORDER_BE = 1; | |
const BYTE_ORDER_LE = 2; | |
function __construct($path = ""){ | |
$this->_byte_order = self::BYTE_ORDER_BE; | |
if($path){ | |
$this->open($path); | |
} | |
} | |
function open($path){ | |
$this->_fp = fopen($path, "r"); | |
} | |
function close(){ | |
fclose($this->_fp); | |
} | |
function set_byte_order($order){ | |
$this->_byte_order = $order; | |
} | |
function get_byte_order($order){ | |
return $this->_byte_order; | |
} | |
function is_be(){ | |
return ($this->_byte_order == self::BYTE_ORDER_BE); | |
} | |
function is_le(){ | |
return ( ! $this->is_be()); | |
} | |
function seek($offset){ | |
fseek($this->_fp, $offset, SEEK_CUR); | |
} | |
function read($length = 0){ | |
if(feof($this->_fp)){ | |
return NULL; | |
} | |
if($length){ | |
return fread($this->_fp, $length); | |
} | |
else{ | |
$data = ''; | |
while( ! feof($this->_fp)){ | |
$data .= fread($this->_fp, 4096); | |
} | |
return $data; | |
} | |
} | |
function read_hex_string($length){ | |
$data = $this->read($length); | |
$format = "H*"; | |
return array_shift(unpack($format, $data)); | |
} | |
function read_byte(){ | |
$data = $this->read(1); | |
$format = "c"; | |
return array_shift(unpack($format, $data)); | |
} | |
function read_unsigned_byte(){ | |
$data = $this->read(1); | |
$format = "C"; | |
return array_shift(unpack($format, $data)); | |
} | |
function read_unsigned_short(){ | |
$data = $this->read(2); | |
$format = ($this->is_be()) ? "n" : "v"; | |
return array_shift(unpack($format, $data)); | |
} | |
function read_unsigned_long(){ | |
$data = $this->read(4); | |
$format = ($this->is_be()) ? "N" : "V"; | |
return array_shift(unpack($format, $data)); | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment