moztw.org

瀏覽器的預設 File Input 不好用!我想自訂美觀實用的照片上傳元件怎麼辦?

接續前一篇介紹 Canvas 刮刮樂的文章,

本篇介紹的是去年九月份 Firefox OS 讓你盡情享受每一刻 活動的照片上傳作法。

有做過上傳表單的人一定對瀏覽器提供的預設上傳元件有一些意見,

它的外觀光用 CSS 來設定樣式也救不了,而且上傳圖片沒有前端預覽很不直覺。

幸好可以搭配<label>元素改變表單元素外觀,以及新的 FileReader API 以在上傳前預覽圖片。

實際的範例請先看底下的 fiddle:

想知道這是怎麼做的嗎?不囉嗦,以下馬上開始介紹……

如何不用 Javascript 自訂上傳元件外觀

會用 Javascript 的人大多會先想到,只要把原本的上傳元件藏起來,

再從自訂上傳鈕的
onclick 內呼叫上傳元件的 
click 即可。

但這招在 IE10 實測時發現,選完圖後居然點下表單的送出鈕沒反應!

(送出鈕同樣也是在自訂按鈕的 
onclick  去呼叫 
submit  )

仔細追蹤後發現此行為被當作不安全的動作而被擋下來了!

因為 IE10 可能認為透過程式(而非人為的
click )而編輯過的表單資料不安全。

這部分的行為在網路上沒有找到正式說明文件,但根據 stackoverflow 上神人的實測結果

上傳元件的
click 以及表單的
submit 兩者都是透過程式去驅動時就會被 IE 擋下來,

只有透過程式驅動其中一種,或是使用原生表單元件功能的話就不會有此問題。

哇~!那這下不剉賽了?難道真的要換回原生醜醜的表單元件嗎(掩面)。

還好 W3C 標準中有一個很貼心的小設計……那就是 標籤

它的作用除了說明表單元件的用途以外,更可增加元件的點擊範圍!

最常見的地方是搭配小不拉幾的 
checkbox  或 
radio  來使用,

讓使用者在選取選項時不必艱難地對準小小的按鈕,可以直接點對應的 
label  文字來選取。

更妙的是 
label  搭配 
file input  也有同樣的效果!

簡單來說只要把原本的 
file input  藏起來,

再透過 CSS 把對應它的 
label  自訂成想要的按鈕外觀,

一個自訂外觀的上傳元件就完成了!完全不需用到 Javascript!

這是 HTML 的部分:

<label class="choose-file" id="upload" for="id_image_large"></label>
<input type="file" id="id_image_large" name="image_large">

12

<label class="choose-file" id="upload" for="id_image_large"></label><input type="file" id="id_image_large" name="image_large">

label  的 
for  屬性用來指定對應的表單元件
id ,如果不用 
for  的話就必須把表單元件包在 
label  標籤之內。

以下是對應的 CSS:

/ Hide file input /
#id_image_large {
display: none;
}
/ Custom upload button appearance/
#upload {
position: absolute;
cursor: pointer;
z-index: 1;
left: 84px;
top: 201px;
display: block;
width: 138px;
height: 138px;
background: url(https://firefox.club.tw/static/img/event/every-moment/upload/upload-pic.png);
}

12345678910111213141516

/ Hide file input /#id_image_large { display: none;}/ Custom upload button appearance/#upload { position: absolute; cursor: pointer; z-index: 1; left: 84px; top: 201px; display: block; width: 138px; height: 138px; background: url(https://firefox.club.tw/static/img/event/every-moment/upload/upload-pic.png);}

很簡單對吧?

上傳前的照片預覽

照片上傳之前有一個重要的功能-預覽 ,

使用者透過預覽可以確認自己選的圖看起來如何,

甚至做一些簡單的調整。

這個功能如果可以在瀏覽器本機端就完成是最好不過了,

省去上傳等待半天又來回修改換圖的麻煩。

透過 HTML5 的 
FileReader  API (註 1)要做照片預覽現在非常容易,

只要使用 
readDataAsURL  將檔案載入即可:

function loadImage(e) {
    var image = new Image();
image.src = e.target.result;
}
function previewImage() {
var reader = new FileReader();
var file = document.getElementById("id_image_large").files[0];
reader.readAsDataURL(file);
reader.onload = loadImage;
}
$('#id_image_large').change(previewImage);

1234567891011

function loadImage(e) {    var image = new Image(); image.src = e.target.result;}function previewImage() { var reader = new FileReader(); var file = document.getElementById("id_image_large").files[0]; reader.readAsDataURL(file); reader.onload = loadImage;}$('#id_image_large').change(previewImage);

readDataAsURL  的作用是將本機的圖片檔案讀進瀏覽器中,

並將圖片資料轉換為 Base64 編碼的 Data URL(註 2)。

只要在 
file input  的 
change  事件中讀進檔案轉換為 URL,

並且在 reader 的 
onload  事件中將轉換完成的圖片 URL(
e.target.result )

塞給目標圖片的(
img  標籤) 
src  屬性即可。

拖曳改變圖片裁切位置

為了統一上傳圖片的大小,我想在上傳前就把預先裁切至適當比例,

最簡單的作法可以直接把裁切範圍鎖定在中間,

但考慮不同構圖方式只切中間可能會把重點切掉影響呈現效果,

決定還是讓使用者在上傳前可以橫向拖曳圖片,

以調整至最適合的裁切位置。

不想把功能和程式做得太複雜,所以經過討論這次只支援橫向拖曳,

利用 JQuery UI Draggable(註 3) 的限制移動功能,可以防止圖片被拖到外太空去,

這個功能有兩個屬性可以設定:

  • axis 可以把拖曳限制在 x 軸或 y 軸上
  • containment 可以把拖曳限制在指定元素的範圍之內

所以我們只需要將圖片依比例縮至符合外框大小,再依圖片和外框比例判斷要限制在哪一軸,

並且做出一個大小剛好不會讓圖片被拖出界的元素即可,如下圖的淺藍色範圍:

draggable-container

簡單來說,假設圖片的寬高比例比外框寬,就會限制成橫向拖曳,

因此我們就需要一個高度和圖片一樣,寬度比圖片長一些的 containment 元素。

要如何算出它的寬度呢?如果上傳的照片寬度為 w,而外框寬度為 m,

整張圖片的可拖曳寬度就是
2w - m ,以此作為 containment 元素的寬度,

就可以將拖曳範圍恰恰限縮在不會超界的範圍。

$('#dragger').draggable({
containment: "#container",
scroll: false,
axis: horizontal ? 'x' : 'y',
stop: function() {
// convert and save current position
}
});

12345678

$('#dragger').draggable({ containment: "#container", scroll: false, axis: horizontal ? 'x' : 'y', stop: function() { // convert and save current position }});

上面這段是用來啟動 Draggable 的程式,

想知道如何把圖片縮放至適當比例請參考 
resizeImage  函式,

詳細計算元素寬高、比例、初始位置的邏輯詳見 
initImagePosition  函式,

本文就不多加詳述。

處理照片旋轉

最麻煩的問題來了,現在的智慧型手機大多有陀螺儀,

大家可以用任何角度,愛怎麼拍就怎麼拍,

轉置角度的資訊會存在照片裡。

秀圖軟體就會依照轉置資訊把圖轉正。

聽起來好像很簡單,

但目前要在前端去處理轉置資訊需要透過第三方函式庫解析圖片裡的 EXIF 資料,

再判斷八種不同的轉置角度來決定轉正的方式。

在這個範例中引用的是 exif.js作者 Nihilogic 的網站有簡單的介紹和展示,

運用這個函式庫就可以讀進圖片並解析其 EXIF 中的 Orientation 資訊:

image.onload = function (e) {
    var bin = atob(e.target.src.split(',')[1]);
    var exif = EXIF.readFromBinaryFile(new BinaryFile(bin));
    var rotated = false;
    switch (exif.Orientation) {
        case 8:
            $(this).removeClass().addClass('rotate-90');
            rotated = true;
            break;
        case 3:
            $(this).removeClass().addClass('rotate180');
            break;
        case 6:
            $(this).removeClass().addClass('rotate90');
            rotated = true;
            break;
        default:
            break;
    }
}

1234567891011121314151617181920

image.onload = function (e) {    var bin = atob(e.target.src.split(',')[1]);    var exif = EXIF.readFromBinaryFile(new BinaryFile(bin));    var rotated = false;    switch (exif.Orientation) {        case 8:            $(this).removeClass().addClass('rotate-90');            rotated = true;            break;        case 3:            $(this).removeClass().addClass('rotate180');            break;        case 6:            $(this).removeClass().addClass('rotate90');            rotated = true;            break;        default:            break;    }}

再依照不同的 Orientation 值做對應的圖片旋轉,

Orientation 的值共有八種,這在 JPEGclub 上有詳細的解說,

八種值其中四種包含了鏡像翻轉,在大部分的情況下不需考慮鏡像翻轉,

沒有旋轉的情況也不需要處理,所以只要判斷 8、3、6三個值即可。

期待簡易的新標準

目前針對 HTML5 標準的草案中,

有一個針對圖片旋轉作處理的 CSS 特性
image-orientation 。

我在寫這篇文章的時候只有 Firefox 支援這個實驗性的特性,

它的作用除了自行指定圖片的旋轉角度、翻轉方式以外,

image-orientation: from-image;

1

image-orientation: from-image;

這行 CSS 設定即可讓圖片依據 EXIF 中的資訊進行旋轉及翻轉。

期待它能成為新的 HTML5 標準吧!

One more thing…再加個假掰光暈動畫

最後教大家用 CSS 的影格動畫(keyframe animation)做一個超搶眼的光暈閃爍動畫,

把它放在要強調的按鈕後面,讓網站的使用者想不看到你的按鈕都沒辦法:

@keyframes spot {
0% {
opacity: 1;
}
50% {
opacity: 0;
}
100% {
opacity: 1;
}
}

.spotlight {
background: radial-gradient(ellipse at center, #ffffff 0%, rgba(255, 255, 255, 0) 70%);
animation: spot 1.5s linear infinite;
}

12345678910111213141516

@keyframes spot { 0% { opacity: 1; } 50% { opacity: 0; } 100% { opacity: 1; }} .spotlight { background: radial-gradient(ellipse at center, #ffffff 0%, rgba(255, 255, 255, 0) 70%); animation: spot 1.5s linear infinite;}

如果你對這段 CSS 的長相和它的作用感到疑惑,底下就稍微解釋一下運作原理:

首先在 
@keyframe  區塊中定義動畫進度百分之幾時需要漸變的屬性目標值,

瀏覽器會根據時間動畫長度、時間函式(timing function)、

以及目前動畫播放的進度,算出一個介於中間的值。

以閃爍的動畫來說,只需要讓 
opacity  屬性從 1 漸變成 0,再從 0 漸變成 1 即可,

在範例中
.spotlight  選擇器中的 
animation  屬性指定
spot  作為動畫的影格設定,

假設 20 秒作為動畫長度,時間函式設為線性(Linear),

那麼在剛開始播放第 0 秒的時候,等於是 
keyframe  的 0%,

光暈的 
opacity  會是 1,也就是完全不透明,

第 10 秒時,播放進度到達 50%,
opacity  漸變為 0,完全變成透明了,

那麼在第 15 秒的時候呢?15 秒對 20 秒來說是 75%,等於 50% 到 100% 的半路上,

所以 
opacity  的值是 0.5,呈現出半透明的效果,以此類推。

以上就是這個動畫運算的基本原理。

附帶一提最後的 
infinte  設定可以讓動畫無限循環播放。

關於 CSS 動畫更多詳細的說明、範例和各瀏覽器支援度請參考 MDN 的 CSS 動畫文件。

至於光暈的背景如果不想用圖片的話,可以參考本範例利用 CSS 的 radial - gradient  功能。

很驚訝原來這些功能都能在網頁上運作嗎?你也試著來做做看吧!

參考資料

註 1:FileReader API 是 HTML5 的新 API,讓瀏覽器得以讀取客戶端電腦的檔案。

註 2:Data URL也是 URL 的一種標準規範,可將完整檔案資料直接嵌入URL中。

註 3:JQuery UI DraggableJQuery UI套件中用來處理元素拖放的擴充套件。