解決無法正常上傳RAW照片,getimagesize讀取RAW圖檔大小的問題

最近我花了許多時間,終於把insoler社群網站從舊版的Dolphin 7.1.6,升級系統版本到目前最新的Dolphin 7.3版本。當然,在Dolphin 7.1.6經過大幅度修改,可以支援RAW上傳的Photos相簿模組,也因應Dolphin 7.3放棄Flash,改用HTML 5,所以也花了超過1個月以上的時間,在熱心網友ayaka的協助下,完成了支援HTML 5的全新相片模組。

在改寫Photos模組,開發上傳RAW程式碼、進行測試的時候,雖然有遇到過一些問題,但是並沒有遇到【只能上傳 .CR2,卻無法上傳 .NEF】的問題!原本以為可以正常運作的相簿模組,卻突然出問題,居然只能上傳Canon相機拍攝的RAW檔案,卻無法上傳Nikon相機拍攝的RAW檔案!

我追查了PHP程式碼之後,猜想這個問題應該是與Apple最近在2016/3/21釋出的安全性更新版本有關!

下載 - 安全性更新 2016-002 Yosemite

建議所有使用者安裝「安全性更新 2016-002」,它改進了 OS X 的安全性。

如需此更新項目安全性內容的相關資訊,請參訪 http://support.apple.com/kb/HT1222?viewlocale=zh_TW 網站。

投稿日: 2016/3/21

最大的問題是出在php的getimagesize這個可以同時取得圖檔大小、圖檔格式的函式!在舊版Dolphin 7.1.6的時候,是寫成這樣:

            @chmod($sTempFileName, 0644);
            if(file_exists($sTempFileName) && filesize($sTempFileName)>0) {
                $aSize = getimagesize($sTempFileName);
                if (!$aSize) {
                    @unlink($sTempFileName);
                    if ($iRAW) @unlink($sTmpFileRAW); //BNW
                    return false;
                }

                switch($aSize[2]) {
                    case IMAGETYPE_JPEG: $sExtension = '.jpg'; break;
                    case IMAGETYPE_GIF:  $sExtension = '.gif'; break;
                    case IMAGETYPE_PNG:  $sExtension = '.png'; break;
                    default:
                        @unlink($sTempFileName);
                        if ($iRAW) @unlink($sTmpFileRAW); //BNW
                        return false;
                }

                $sStatus = 'processing';
                $iImgWidth = (int)$aSize[0];
                $iImgHeight = (int)$aSize[1];
                if (!(($iImgWidth >= 1280 && $iImgHeight >= 600) || ($iImgWidth >= 600 && $iImgHeight >= 1280))) { //BNW Photo least 1280x1280
                    @unlink($sTempFileName);
                    if ($iRAW) @unlink($sTmpFileRAW); //BNW
                    return false;
                }

 

熱心網友ayaka幫我改寫成這樣:

                $aSize = getimagesize($sTempFileName);
                if (!$aSize) {
                    @unlink($sTempFileName);
                    return false;
                }

                switch($aSize[2]) {
                    case IMAGETYPE_JPEG: $sExtension = '.jpg'; break;
                    case IMAGETYPE_GIF:  $sExtension = '.gif'; break;
                    case IMAGETYPE_PNG:  $sExtension = '.png'; break;
                    default:
                        @unlink($sTempFileName);
                        return false;
                }

                $sStatus = 'processing';
                $iImgWidth = (int)$aSize[0];
                $iImgHeight = (int)$aSize[1];
                $sDimension = $iImgWidth.'x'.$iImgHeight;
                //BNW check the image size
                if (!(($iImgWidth >= 1280 && $iImgHeight >= 600) ||
                    ($iImgWidth >= 600 && $iImgHeight >= 1280)))
                {
                    @unlink($sTempFileName);
                    if ($this->sTempFileExts) {
                        @unlink($sTempRAWFileName);
                    }
                    return false;
                }

 

基本上這兩段PHP程式碼並沒有什麼太大的問題,如果有的話,在Dolphin 7.1.6就會出問題,導致無法上傳Nikon相機拍攝的 .NEF 照片!雖然getimagesize其實並不支援RAW,以前讀取的 .NEF 檔案的照片大小,以Nikon D90來說,讀取到的RAW照片大小竟然只有2144x1424!事實上這個錯誤,從一開始就一直記錄在MySQL裡面。我並沒有修正。因為連原版Dolphin在瀏覽照片(無論是只有上傳JPEG或是RAW)的時候,BOONEX他們自己也不使用bx_photos_main的Size這個資料,只是一直把Size儲存在MySQL裡面而已。

查Nikon原廠網站,就會看到Nikon D90正確的照片大小應該是4288×2848才對。但是getimagesize以前只能讀到1/2一半大小的2144x1424!

D90 - 主な仕様 | ニコンイメージング

事實上,Canon也一樣只能讀取到1944x1296只有一半大小的圖檔大小!只能猜想getimagesize不支援RAW所以會判斷錯誤。但無論如何,讀取到的圖檔size都不算太小,以前並沒有什麼太大問題。

但是在我安裝「安全性更新 2016-002,它改進了 OS X 的安全性」之後,getimagesize讀取到的 .NEF 的圖檔大小,竟然只剩160x120的縮圖大小!所以才會導致上傳 .NEF 照片檔案會失敗出錯!

Array
(
    [0] => 160
    [1] => 120
    [2] => 8
    [3] => width="160" height="120"
    [mime] => image/tiff
)

 

因為160x120的照片太小,不符合我限定的最小必須是1280 x 600(或是600x1280)以上的照片大小,所以才會導致只有Nikon相機拍攝的 .NEF上傳照片失敗!

那麼,如果上傳Canon相機的 .CR2呢?結果也與以前的1944x1296不同!為什麼會這樣?我也不知道!只能確定PHP內建函式getimagesize不能在RAW上面了!雖然用來讀取JPEG照片大小還算是正確。

Array
(
    [0] => 1936
    [1] => 1288
    [2] => 7
    [3] => width="1936" height="1288"
    [mime] => image/tiff
)

 

getimagesize不能用的話,解決辦法?

原本在正常的情況下,getimagesize應該要可以讀取JPEG、RAW照片的常、寬size才對,但因為不支援RAW,所以會讀出錯誤的大小,這個函式不能用的話,第二個選擇當然是改用PHP內建的exif_read_data函式,從EXIF裡面應該也能讀到照片大小才對!但更不幸的是,exif_read_data竟然連最基本的【照片大小】都讀不出來!可說是100%不支援RAW!竟然比getimagesize還要更差!

在PHP內建的exif_read_data、getimagesize都不能用的情況下,只好求助於ExifTool這個免費的工具程式。只要是上傳RAW的時候,就只能放棄getimagesize,改用ExifTool來讀取照片大小。雖然從PHP呼叫外部程式會花費更多時間,但是相較於上傳RAW照片本身就需要更多時間,在上傳照片完成之後,再多花一點時間呼叫ExifTool來讀出EXIF,取出照片大小,用來判別照片大小,儲存在MySQL的bx_photos_main裡面,雖然會略慢一點點,但因為上傳照片的會員不多,應該不會有什麼太大的感覺。

修正Bug之後(雖然這個Bug不是我造成的),新的程式碼改寫成這樣:

                if ($RAW != '') {
                    $pSize = $this->getPhotoSize($sTempFileName); // 讀取RAW照片大小
                    if (!$pSize) return false;
                    $iImgWidth = (int)$pSize[0];
                    $iImgHeight = (int)$pSize[1];
                }
                else {
                    $iImgWidth = (int)$aSize[0];
                    $iImgHeight = (int)$aSize[1];
                }
                if (!(($iImgWidth >= 1280 && $iImgHeight >= 600) || ($iImgWidth >= 600 && $iImgHeight >= 1280))) { //BNW 照片大小必須是1280x1280以上
                    @unlink($sTempFileName);
                    return false;
                }

 

這段PHP程式碼會先判別上傳的是不是RAW,如果不是,就還是用原本的getimagesize來讀取照片大小(同時還會讀出圖檔類型是JPEG、GIF、PNG),但如果上傳的是RAW照片的話,就會執行getPhotoSize副程式,再由getPhotoSize去呼叫外部的ExifTool,再讀出照片大小。

由於熱心網友ayaka提出【非同步處理】的寶貴意見,所以在這裡的ExifTool只會讀出照片大小,用來記錄在bx_photos_main的Size這個資料欄位。等到【非同步處理RAW或是JPEG】的時候,還需要再執行一次ExifTool讀出EXIF資訊,並且呼叫我寫的class BnwPhotoEXIF,來分析處理EXIF,把EXIF判別的結果記錄在bnw_photo_exif裡面。

🍎insoler社群網站CEO、兼站長兼顧問兼工程師

真好yell,加班完就回去上傳寫真,

我要成為幸運的一般會社員

想不到星期六還要加班啊~undecided黑心國該不會是「免費無料加班」吧?yell

非常歡迎拼命上傳寫真!不管要上傳多少張寫真都可以!covermouth你可以先試試看上傳JPEG寫真(當然要用ViewNX 2來轉換JPEG),體驗看看JPEG的上傳速度!再上傳RAW來體驗看看。covermouth當然也可以反過來,先上傳RAW,再上傳JPEG,但不要一次同時上傳JPEG+RAW。我其實一直都是上傳RAW+JPEG,因為只有Canon、Nikon自己製作的寫真處理工具,會得到最正確的色彩。laughing

阿光我是覺得比以前快很多(實際傳送速度本身還是要看網路速度)!covermouth但目前還有上傳的最後幾張寫真無法顯示的問題!這個問題,某老蘇說還要做各種實驗看看,應該還要再等很久。tongue-out

🍎たったひとつの真実見抜く、見た目は大人、頭脳は子供、その名は名馬鹿ヒカル!🍏

沒有拉,是我出包了cry

我要成為幸運的一般會社員

沒關係,不管是誰都會出錯。covermouth阿光我之前買了新的8TB外接硬碟,本來要把4TB硬碟的一堆檔案複製到新的8TB上面,在格式化硬碟的時候,不小心就選了4TB!也沒有仔細看清楚,就按下去,下場就...cry

🍎たったひとつの真実見抜く、見た目は大人、頭脳は子供、その名は名馬鹿ヒカル!🍏

我寫了一個簡單的PHP測試程式來測試執行exiftool總共需要多少時間?

        $time_start = microtime(true);
        $sExif = shell_exec("/usr/local/bin/exiftool -json $sMediaDir");
        $getEXIF = json_decode($sExif, True);
        $gExif = $getEXIF[0];
        $time_end = microtime(true);
        $time = $time_end - $time_start;

 

得到的$time的結果是0.26347589492798秒!這還只是讀取一張照片的EXIF資訊而已!如果連續處理10張照片,就要花2秒!這還只是一個人上傳10張照片!如果有10個人同時上傳10張照片的話,看似不會很久的2秒就會變成20秒這種讓人無法接受的數字了!雖然說insoler目前還不會有10個人同時上傳10張照片這種熱況。embarassed

如果把程式碼改成使用PHP內建函式exif_read_data的話...

        $time_start = microtime(true);
        $gExif = @exif_read_data($sMediaDir, 0);
        $time_end = microtime(true);
        $time = $time_end - $time_start;

 

得到的$time的結果是0.00032806396484375秒!雖然時間的計算會有一些誤差,假設是0.26 / 0.000328 = 792.68292682926829,也就是說exif_read_data的執行速度比exiftool快了792倍!shock

為了這個理由,我把前面的程式碼改成這樣:

	function CheckRAW($sFilePath)
{
$gExif = @exif_read_data($sFilePath, 0);
if (isset($gExif['MimeType'])) {
if ($gExif['MimeType'] == 'image/tiff') {
if (isset($gExif['Make']) && isset($gExif['ExposureTime']) && isset($gExif['FNumber'])) {
return true;
}
else return false;
}
}
}

                if ($RAW != '') {
                    $pSize = $this->CheckRAW($sTempFileName); // 檢查真的是RAW?
                    if (!$pSize) return false;
                    $iImgWidth = 2048;
                    $iImgHeight = 2048;
                }
                else {
                    $iImgWidth = (int)$aSize[0];
                    $iImgHeight = (int)$aSize[1];
                }
                if (!(($iImgWidth >= 1280 && $iImgHeight >= 600) || ($iImgWidth >= 600 && $iImgHeight >= 1280))) { //BNW 大小必須是1280x1280以上
                    @unlink($sTempFileName);
                    return false;
                }

 

為了避免駭客上傳「造假的RAW檔案」所以在上傳檔案之後,還是必須檢查一下上傳的到底是不是RAW檔案?這個部分其實是用高速的exif_read_data來讀取EXIF,如果可以讀出EXIF,而且也有MimeType、Make、ExposureTime、FNumber這幾個基本資料的話,應該就能確認上傳的是真實的RAW檔案。

之後的$iImgWidth = 2048;與$iImgHeight = 2048;其實沒有任何意義,主要是用來做記號而已!除了可以用來通過我設定的大小以外,主要是暫時把2048x2048儲存在MySQL裡面,等到非同步的背景程式執行的時候,再依據exiftool讀取的照片大小,修正成正確的大小。

但如果萬一非同步的背景程式,沒有正確執行的時候,就可以依據MySQL裡面儲存的2048x2048這個資料來找到出問題的照片,再手動修正,或是想辦法解決非同步執行的問題。

修正以後,就能讓上傳RAW的時候後續的處理快了792倍?當然不是!只是每張照片可以節省0.2秒!一次上傳40張照片的話,可以節省8秒。讓上傳的速度感覺上比較流暢一些。

🍎insoler社群網站CEO、兼站長兼顧問兼工程師