這篇記錄的前端使用 HTML、CSS、JavaScript,上傳圖片使用的是 python 製作的 API,整體資料夾結構使用的是 Flask。
除了「上傳圖片到後端」這件事情交給 python 的 API 來處理,其餘都可以用 JavaScript 做掉。
目錄
上傳圖片 / 圖片網址轉圖片的格式轉換
上傳圖片其實只需要呼叫 API 就可以達到,但網路圖片連結想要轉換成圖片再顯示,就需要先把圖片位址路徑轉換成 base64 格式,再轉換成 blob 物件,再轉換成 File 資料格式。
因為太過麻煩,決定把程式碼記錄起來,避免之後要用到全部要重寫。
最終實現成果
最終 HTML 會是這樣:
到 Unsplash 圖庫網站,可以直接複製圖片位址:
貼上路徑,就能完成圖片上傳到後端並預覽的效果囉!
後端上傳後,也能看到圖片囉!
程式碼範例
HTML:
HTML 用來顯示上傳圖片的按鈕、輸入圖片網址的按鈕,以及預覽圖片:
放在 HTML <head> 使用到的 CDN:
<!-- bootstrap - JavaScript Bundle with Popper -->
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.2.2/dist/js/bootstrap.bundle.min.js"
integrity="sha384-OERcA2EqjJCMA+/3y+gxIOqMEjwtxJY7qPCqsdltbNJuaOe923+mo//f6V8Qbsw3"
crossorigin="anonymous"></script>
<!-- axios cdn -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/axios/1.1.3/axios.min.js"
integrity="sha512-0qU9M9jfqPw6FKkPafM3gy2CBAvUWnYVOfNPDYKVuRTel1PrciTj+a9P3loJB+j0QmN2Y0JYQmkBBS8W+mbezg=="
crossorigin="anonymous" referrerpolicy="no-referrer"></script>
HTML <body> 區域:
<div>
<div>
<label for="formFile" class="btn btn-primary form-label mb-0">
<span><i class="fa-solid fa-arrow-up-from-bracket"></i>
選擇圖片</span>
<input class="form-control d-none" type="file" id="formFile" name="sendfile" />
</label>
</div>
<!-- 輸入圖片網址 -->
<div class="input-group flex-grow-1" style="width: auto">
<input type="text" id="uploadUrl" class="form-control" placeholder="請輸入圖片網址" aria-label="upload-url"
aria-describedby="upload-url" />
<button class="btn btn-primary" type="button" id="uploadUrlBtn">
預覽圖片
</button>
</div>
<!-- 預覽圖片 -->
<img id="display-src-from-thumbnail" style="width:500px" />
</div>
<!-- JS -->
<script type="module" src="../static/js/main.js"></script>
JavaScript:
為了方便拆分功能,這是 JS 檔案結構:
順便附上 Flask 完整結構,必須要有 uploads 資料夾,上傳的圖片會放在這裡:
【進入點:main.js】
/* *********
主檔案
********* */
import { setFileUploader } from "./imgUpload.js";
import { setUrlUpload } from "./urlUpload.js";
function init() {
// 【上傳圖片】 功能 onchange
setFileUploader();
// 設定【網路抓圖預覽】功能
setUrlUpload();
}
init();
【上傳圖片至後端 API:uploadToBackend.js】
/* ******************
元件功能:把圖片上傳到後端 uploads 資料夾中
****************** */
export async function uploadToBackend(getFile) {
// 回傳內容暫存在 rtn 變數
let rtn;
let sendfile = getFile;
let data = new FormData();
data.append("sendfile", sendfile);
let config = {
method: "post",
url: "./uploadcustomfile",
headers: {
Authorization: "Bearer 8aa7c158-1fc1-4021-ade4-f48ffd14068a",
"Content-Type": "multipart/form-data",
},
data: data,
};
await axios(config)
.then(async function (res) {
console.log(res);
rtn = res;
})
.catch(function (err) {
alert("不支援此格式,僅限上傳副檔名為 png、jpg、gif 格式的圖片");
console.log(err.response);
});
return rtn;
}
【後端 API 內容:app.py】
from flask import Flask, render_template,request
import json
import requests
import json
# 上傳檔案
import os
from flaskapp import app
from api import callapi,callxcreen,getimage
import urllib.request
from queue import Queue
import time
from werkzeug.utils import secure_filename
from flask import Flask, flash, request, redirect, url_for, render_template
import threading
import json
import pathlib
ALLOWED_EXTENSIONS = set(['png', 'jpg', 'jpeg'])
@app.route("/")
@app.route("/index")
def index():
return render_template("index.html")
# 上傳檔案、辨識
@app.route('/uploadcustomfile', methods=['POST'])
def upload_image():
# 取得目前檔案所在的資料夾
SRC_PATH = pathlib.Path(__file__).parent.absolute() # SRC_PATH 值為 '/app'
UPLOAD_FOLDER = os.path.join(SRC_PATH, 'static', 'uploads') # UPLOAD_FOLDER 值為 '/app/static/uploads'
try:
file = request.files['sendfile'] # 取得 AJAX 傳來的整張圖片
file_names = []
file_names.append(file.filename)
if file.filename != '': # 如果取得到圖片的檔名
# 驗證檔案型態
isAllowedExtensions = allowed_file(file.filename)
# 驗證成功
if isAllowedExtensions:
file.save(os.path.join(UPLOAD_FOLDER, file.filename)) # 圖片儲存路徑
return {'states': "success",'msg':"上傳成功","data":file_names},200
else:
return {'states': "error",'msg':"不支援上傳此副檔名"},400
except BaseException as e:
return {'states': 'error','msg':"{}".format(e)},400
def allowed_file(filename):
return '.' in filename and filename.rsplit('.', 1)[1].lower() in ALLOWED_EXTENSIONS
【將 Base64 格式圖檔,轉 Blob 再轉回圖片 File 檔:resizeToFileType.js】
/* ******************
元件功能:將 Base64 格式圖檔,轉 Blob 再轉回圖片 File 檔
****************** */
export function resizeToFileType(imgbase64, getFile, forUrlUpload) {
// 讀取檔案
// 把 base64 流轉換成 blob 物件
let binary = atob(imgbase64.split(",")[1]);
let b_array = [];
for (let i = 0; i < binary.length; i++) {
b_array.push(binary.charCodeAt(i));
}
// 這裡就要先定義 type 為圖片檔格式,否則後續會給不到
let newPicBlob = new Blob([new Uint8Array(b_array)], { type: "image/jpg" });
// 檔名前面加上時間戳記
let currentDateTime = new Date().getTime(); // 時間戳記
let cropFileName;
// forUrlUpload 變數如果是 true,表示目前使用者是用網路圖片位址或圖片路徑方式
if (forUrlUpload == true) {
cropFileName = currentDateTime + getFile + ".jpg"; // 只用連結的預覽方式,後面有多副檔名
} else {
cropFileName = currentDateTime + getFile.name; // 裁切後新檔名
}
console.log("目前裁切的檔案名稱:", cropFileName);
// 先轉換成 Blob 格式,參數帶入 圖片、自訂裁切後的圖片檔名
// 切記:不可直接轉 File,因為 iOS 系統會不支援
let toBlobFunc = function (newBlob, fileName) {
newBlob.lastModifiedDate = new Date();
newBlob.name = fileName;
return newBlob;
};
// 執行 toBlobFunc 函式
let blobTypeFile = toBlobFunc(newPicBlob, cropFileName);
// 轉換 Blob to File 型態
function blobToFileType(blobTypeFile, fileName) {
return new File([blobTypeFile], fileName, {
lastModified: new Date().getTime(),
type: blobTypeFile.type,
});
}
// 執行 blobToFileType 函式
let finCropFileReturn = blobToFileType(blobTypeFile, cropFileName);
return finCropFileReturn;
}
【將網址轉成 Base64 格式:webUrlToFile.js】
/* ******************
元件功能:把網址或圖檔路徑,經 resizeToFileType.js 轉換成 File 資料格式
****************** */
import { resizeToFileType } from "./resizeToFileType.js";
// Base64 資料格式轉成檔案格式
export async function readyToFile(myBase64) {
return new Promise(async function (resolve, reject) {
// Base64 轉 Blob 轉 File,第二個參數給固定檔名,進入函式會再加上時間戳記
let sameAsUploadFile = resizeToFileType(myBase64, "urlToFileName", true);
resolve(sameAsUploadFile);
reject("圖片格式轉換失敗!");
});
}
// 網址或路徑轉成 Base64 資料格式
export function webUrlToDataUrl(url, callback) {
var xhr = new XMLHttpRequest();
xhr.onload = function () {
var reader = new FileReader();
reader.onloadend = function () {
callback(reader.result);
};
reader.readAsDataURL(xhr.response);
};
xhr.open("GET", url);
xhr.responseType = "blob";
xhr.send();
}
【上傳圖片:imgUpload.js】
/* ******************
【上傳圖片】功能
****************** */
import {
calcUploadImgSize,
alreadyUploadToDisplay,
setOriginalImg,
} from "./displayImg.js";
import { uploadToBackend } from "./component/uploadToBackend.js";
/**** 開始上傳 onchange ****/
// 上傳功能
const fileUploader = document.querySelector("#formFile");
export function setFileUploader() {
fileUploader.addEventListener("change", function (e) {
// 異動時是有上傳圖片
if (e.target.files.length != 0) {
// 判斷是否為可接受的副檔名
let isPicExtension = isPicExtensionFunc(e.target.files[0].name);
if (isPicExtension) {
fileUploaderOnchange(e);
} else {
alert("不支援上傳此副檔名!");
}
}
});
}
// 是否為圖片副檔名
function isPicExtensionFunc(fileName) {
let strTmp = fileName.split(".");
console.log(strTmp[strTmp.length - 1]);
if (
strTmp[strTmp.length - 1] == "png" ||
strTmp[strTmp.length - 1] == "jpg" ||
strTmp[strTmp.length - 1] == "jpeg"
) {
return true;
} else {
return false;
}
}
async function fileUploaderOnchange(e) {
// 使用同步技術,計算原始圖片寬高
// 上傳圖片原始寬、高:originalImgArr[0], originalImgArr[1]
let originalImgArr = await calcUploadImgSize(e.target.files[0]);
// 設定圖片寬高到 originalImgW 和 originalImgH 變數
setOriginalImg(originalImgArr);
// 如果有上傳內容
// e.target.files[0] 是 File 格式原始圖片
if (e.target.files[0]) {
// 上傳後端,回傳 finUploadToBackend 只有圖片檔名
let finUploadToBackend = await uploadToBackend(e.target.files[0]);
// 把回傳給前端的圖片檔名,組成路徑放回前端顯示
alreadyUploadToDisplay(finUploadToBackend);
}
}
【將上傳的圖片顯示:displayImg.js】
/* ******************
【顯示上傳的圖片到前端】
****************** */
// 原始圖片大小
export let originalImgW;
export let originalImgH;
const displaySrcFromThumbnail = document.querySelector(
"#display-src-from-thumbnail"
);
/**** 計算上傳圖片的原始寬高 ****/
// 計算上傳圖片的原始寬高,用陣列儲存
let uploadOriginSizeArr = [];
export async function calcUploadImgSize(fileUpload) {
let returnOriginSize = await calcOriginalSizeFunc(fileUpload)
.then((res) => {
console.log(res);
return res;
})
.catch((err) => {
console.log(err);
});
// 原始圖片寬、高
return returnOriginSize;
}
// 因為圖片載入 image.onload 需要時間,所以用 Promise 包起,才能用 async/await 處理
async function calcOriginalSizeFunc(fileUpload) {
// 初始化 FileReader
let reader = new FileReader();
// 讀取圖片文件
// 如果是上傳網路圖或路徑方式時,只會傳入圖檔
reader.readAsDataURL(fileUpload);
return new Promise((resolve, reject) => {
reader.onload = function (e) {
// 初始化
let image = new Image();
// FileReader 取得 Base64 字符串
image.src = e.target.result;
image.onload = function () {
// 取得圖片寬高
let height = this.height;
let width = this.width;
// 放入陣列
uploadOriginSizeArr = [];
uploadOriginSizeArr.push(width);
uploadOriginSizeArr.push(height);
// 成功狀態要執行回傳的函式,括號中帶入的內容,即為回傳的 res
resolve(uploadOriginSizeArr);
reject("error");
};
};
});
}
// 上傳圖片原始寬、高
export function setOriginalImg(originalImgArr) {
originalImgW = originalImgArr[0];
originalImgH = originalImgArr[1];
console.log(originalImgW, originalImgH);
}
// 把回傳給前端的圖片檔名,組成路徑放回前端顯示
export function alreadyUploadToDisplay(finUploadToBackend) {
console.log(finUploadToBackend.data.data[0]); // 取得圖片檔名(含副檔名)
// 組成路徑放回前端顯示
let resPicUrl = `../static/uploads/${finUploadToBackend.data.data[0]}`;
displaySrcFromThumbnail.setAttribute("src", resPicUrl);
}
【圖片網址上傳:urlUpload.js】
/* ******************
【網路圖上傳 + 前端顯示】
****************** */
import {
calcUploadImgSize,
alreadyUploadToDisplay,
setOriginalImg,
} from "./displayImg.js";
import { uploadToBackend } from "./component/uploadToBackend.js";
import { readyToFile, webUrlToDataUrl } from "./component/webUrlToFile.js";
// 網路抓圖預覽功能
const uploadUrl = document.querySelector("#uploadUrl");
const uploadUrlBtn = document.querySelector("#uploadUrlBtn");
// 點擊「預覽圖片」按鈕
export function setUrlUpload() {
uploadUrlBtn.addEventListener("click", async function (e) {
// 判斷預覽圖片輸入框是否為空白
let isInputCorrectUrl = await isInputCorrectUrlFunc();
// 如果有輸入圖片網址
if (isInputCorrectUrl) {
// 網址或路徑轉成 base64 格式
webUrlToDataUrl(uploadUrl.value, async function (myBase64) {
console.log(myBase64);
// 再轉換格式成 File
let isFile = await readyToFile(myBase64);
// 把檔案上傳到後端
let finUploadToBackend = await uploadToBackend(isFile);
// 計算圖片原始寬高
// 使用同步技術,計算原始圖片寬高
let originalImgArr = await calcUploadImgSize(isFile);
// console.log("上傳圖片原始寬、高:", originalImgArr[0], originalImgArr[1]);
setOriginalImg(originalImgArr);
// 把回傳給前端的圖片檔名,組成路徑放回前端顯示
alreadyUploadToDisplay(finUploadToBackend);
console.warn("2.完成完整圖片上傳到後端的函式");
});
} else {
alert("您輸入的不是正確的圖片格式!");
}
});
}
// 判斷預覽圖片輸入框是否為空白
async function isInputCorrectUrlFunc() {
// 有輸入字
if (uploadUrl.value) {
let imageSrc = uploadUrl.value;
let isInputCorrectUrlFuncRtn = await checkImageSync(imageSrc);
return isInputCorrectUrlFuncRtn;
} else {
// 輸入框空白
return false;
}
}
// 判斷圖片是否存在
async function checkImageSync(imageSrc) {
let rtn = await checkImage(imageSrc)
.then((res) => {
console.log("Image exists.");
return res;
})
.catch((err) => {
console.log("Image does not exist.");
return err;
});
return rtn;
}
// 判斷圖片是否存在 Promise
async function checkImage(imageSrc) {
return new Promise((resolve, reject) => {
var img = new Image();
img.onload = function () {
resolve(true);
};
img.onerror = function () {
reject(false);
};
img.src = imageSrc;
});
}
總結
以上就是上傳圖片和網址路徑轉換的程式碼範例。
延伸閱讀: