[JS筆記]實作上傳圖片功能、圖片網址轉圖片格式轉換 base64 to blob

這篇記錄的前端使用 HTML、CSS、JavaScript,上傳圖片使用的是 python 製作的 API,整體資料夾結構使用的是 Flask

除了「上傳圖片到後端」這件事情交給 python 的 API 來處理,其餘都可以用 JavaScript 做掉

上傳圖片 / 圖片網址轉圖片的格式轉換

上傳圖片其實只需要呼叫 API 就可以達到,但網路圖片連結想要轉換成圖片再顯示,就需要先把圖片位址路徑轉換成 base64 格式,再轉換成 blob 物件,再轉換成 File 資料格式

因為太過麻煩,決定把程式碼記錄起來,避免之後要用到全部要重寫。

最終實現成果

最終 HTML 會是這樣:

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 檔案結構:

JS 檔案結構

順便附上 Flask 完整結構,必須要有 uploads 資料夾,上傳的圖片會放在這裡:

Flask 完整檔案結構

【進入點: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;
  });
}

總結

以上就是上傳圖片和網址路徑轉換的程式碼範例。


延伸閱讀:

分享這篇文章

發佈留言