以下提供一個 「可在 Google Apps Script 中實際進行 OCR 及圖片分析」 的範例,使用 Google Cloud Vision API 完成「文字辨識 (OCR)」和「影像標籤分析」的功能。請注意,若要在 Apps Script 中使用此功能,需要:
- 在 Google Cloud Console 中啟用「Cloud Vision API」並建立 API Key。
- 於 Apps Script 專案中啟用「UrlFetchApp」的權限(預設大多已開啟)。
- 有計費專案,因為 Cloud Vision API 屬於付費方案(有免費額度)。
步驟總覽
- 建立或開啟一個 Google Apps Script 專案。
- 建立一個檔案(例如
Code.gs)貼上伺服器端程式碼。 - 建立另一個檔案(例如
Index.html)貼上網頁前端程式碼。 - 在
Code.gs中將YOUR_API_KEY替換成你在 Google Cloud Console 申請的實際 API Key。 - 部署為新 Web App(Deploy → New deployment → Select type: Web App),設定好權限後,即可使用。
1. 伺服器端:Code.gs
/**
* 主入口:GET 請求時回傳 HTML 頁面
*/
function doGet() {
return HtmlService.createTemplateFromFile('Index')
.evaluate()
.setTitle("File Upload");
}
/**
* 分析影像:呼叫 Cloud Vision API 的 LABEL_DETECTION
* @param {string} imageDataUrl - Base64 的 Data URL
* @return {string} 分析結果文字
*/
function analyzeImageServer(imageDataUrl) {
// 從 Data URL 中取出 base64 編碼
var base64Data = imageDataUrl.split(',')[1];
// 你的 Cloud Vision API Key
var apiKey = "YOUR_API_KEY"; // <--- 請替換成實際的 API Key
// Vision API 的端點
var endpoint = "https://vision.googleapis.com/v1/images:annotate?key=" + apiKey;
// 建構請求 JSON
var requestBody = {
"requests": [
{
"image": {
"content": base64Data
},
"features": [
{
"type": "LABEL_DETECTION",
"maxResults": 5
}
]
}
]
};
// 透過 UrlFetchApp 呼叫 Vision API
var response = UrlFetchApp.fetch(endpoint, {
method: "post",
contentType: "application/json",
payload: JSON.stringify(requestBody),
muteHttpExceptions: true
});
var json = JSON.parse(response.getContentText());
// 若有錯誤,則顯示錯誤訊息
if (json.error) {
throw new Error(json.error.message);
}
// 從回傳結果中取出 labelAnnotations
var labels = json.responses[0].labelAnnotations || [];
if (labels.length === 0) {
return "未偵測到明顯的標籤。";
}
// 將每個標籤與信心分數做簡易串接
var result = labels.map(function(label) {
var desc = label.description || "未知";
var score = label.score ? (label.score * 100).toFixed(1) + "%" : "無";
return desc + " (信心: " + score + ")";
});
return "圖像標籤分析結果:\n" + result.join("\n");
}
/**
* 文字辨識:呼叫 Cloud Vision API 的 TEXT_DETECTION
* @param {string} imageDataUrl - Base64 的 Data URL
* @return {string} OCR 文字辨識結果
*/
function recognizeTextServer(imageDataUrl) {
// 從 Data URL 中取出 base64 編碼
var base64Data = imageDataUrl.split(',')[1];
// 你的 Cloud Vision API Key
var apiKey = "YOUR_API_KEY"; // <--- 請替換成實際的 API Key
// Vision API 的端點
var endpoint = "https://vision.googleapis.com/v1/images:annotate?key=" + apiKey;
// 建構請求 JSON
var requestBody = {
"requests": [
{
"image": {
"content": base64Data
},
"features": [
{
"type": "TEXT_DETECTION"
}
]
}
]
};
// 透過 UrlFetchApp 呼叫 Vision API
var response = UrlFetchApp.fetch(endpoint, {
method: "post",
contentType: "application/json",
payload: JSON.stringify(requestBody),
muteHttpExceptions: true
});
var json = JSON.parse(response.getContentText());
// 若有錯誤,則顯示錯誤訊息
if (json.error) {
throw new Error(json.error.message);
}
// 取得偵測到的文字
var textAnnotation = json.responses[0].fullTextAnnotation;
if (!textAnnotation || !textAnnotation.text) {
return "未偵測到文字。";
}
return textAnnotation.text;
}
提醒:請務必將
YOUR_API_KEY替換成你在 Google Cloud Console 上為 Cloud Vision API 申請的 實際 API Key,並確認已啟用 Cloud Vision API、綁定計費專案。
2. 客戶端:Index.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>File Upload</title>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
font-family: Arial, sans-serif;
}
body {
background-color: #f5f5f5;
display: flex;
justify-content: center;
align-items: center;
min-height: 100vh;
padding: 20px;
}
.container {
background-color: white;
border-radius: 10px;
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
padding: 30px;
width: 100%;
max-width: 500px;
}
h1 {
text-align: center;
margin-bottom: 20px;
color: #333;
}
.upload-area {
border: 2px dashed #3498db;
border-radius: 5px;
padding: 30px;
text-align: center;
margin-bottom: 20px;
transition: background-color 0.3s, border-color 0.3s;
cursor: pointer;
}
.upload-area:hover, .upload-area.dragover {
background-color: rgba(52, 152, 219, 0.1);
border-color: #2980b9;
}
.upload-icon {
color: #3498db;
margin-bottom: 15px;
}
.browse-link {
color: #3498db;
cursor: pointer;
text-decoration: underline;
}
.file-list {
margin-bottom: 20px;
}
.file-item {
display: flex;
flex-direction: column;
padding: 12px;
border: 1px solid #eee;
border-radius: 5px;
margin-bottom: 12px;
box-shadow: 0 1px 3px rgba(0,0,0,0.05);
transition: box-shadow 0.2s;
}
.file-item:hover {
box-shadow: 0 2px 5px rgba(0,0,0,0.1);
}
.file-item-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 5px;
width: 100%;
}
.file-info {
display: flex;
align-items: center;
width: 80%;
}
.file-icon {
margin-right: 10px;
color: #555;
min-width: 24px;
}
.file-preview {
width: 40px;
height: 40px;
margin-right: 10px;
object-fit: cover;
border-radius: 3px;
}
.file-name {
margin-right: 10px;
word-break: break-all;
}
.file-size {
color: #777;
font-size: 0.8rem;
}
.file-buttons {
display: flex;
align-items: center;
}
.preview-file, .remove-file, .analyze-file, .text-recognize-file {
cursor: pointer;
background: none;
border: none;
display: flex;
align-items: center;
justify-content: center;
margin-left: 5px;
padding: 5px;
border-radius: 3px;
transition: background-color 0.2s;
}
.preview-file {
color: #3498db;
}
.preview-file:hover {
background-color: rgba(52, 152, 219, 0.1);
}
.remove-file {
color: #e74c3c;
font-size: 1.2rem;
}
.remove-file:hover {
background-color: rgba(231, 76, 60, 0.1);
}
.analyze-file {
color: #9b59b6;
}
.analyze-file:hover {
background-color: rgba(155, 89, 182, 0.1);
}
.text-recognize-file {
color: #27ae60;
}
.text-recognize-file:hover {
background-color: rgba(39, 174, 96, 0.1);
}
.upload-button {
background-color: #3498db;
color: white;
border: none;
border-radius: 5px;
padding: 10px 20px;
width: 100%;
cursor: pointer;
font-size: 1rem;
transition: background-color 0.3s;
}
.upload-button:hover:not(:disabled) {
background-color: #2980b9;
}
.upload-button:disabled {
background-color: #bdc3c7;
cursor: not-allowed;
}
.progress-bar {
height: 5px;
background-color: #eee;
border-radius: 5px;
margin-top: 5px;
overflow: hidden;
}
.progress {
height: 100%;
background-color: #2ecc71;
width: 0;
transition: width 0.3s;
}
.analyzing .progress {
transition: width 0.5s;
}
.analysis-result, .text-recognition-result {
width: 100%;
margin-top: 10px;
padding: 12px;
border-radius: 6px;
background-color: #f8f9fa;
font-size: 0.9rem;
border: 1px solid #e9ecef;
box-shadow: 0 1px 3px rgba(0,0,0,0.05);
overflow-wrap: break-word;
white-space: pre-line;
}
.analysis-loading, .text-loading {
color: #3498db;
font-style: italic;
padding: 5px 0;
}
.analysis-content, .text-content {
color: #333;
line-height: 1.6;
max-height: 300px;
overflow-y: auto;
padding-right: 5px;
}
.analysis-error, .text-error {
color: #e74c3c;
font-style: italic;
padding: 5px 0;
}
.analysis-content::-webkit-scrollbar, .text-content::-webkit-scrollbar {
width: 6px;
}
.analysis-content::-webkit-scrollbar-track, .text-content::-webkit-scrollbar-track {
background: #f1f1f1;
border-radius: 3px;
}
.analysis-content::-webkit-scrollbar-thumb, .text-content::-webkit-scrollbar-thumb {
background: #c1c1c1;
border-radius: 3px;
}
.analysis-content::-webkit-scrollbar-thumb:hover, .text-content::-webkit-scrollbar-thumb:hover {
background: #a8a8a8;
}
.preview-modal {
display: none;
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: rgba(0, 0, 0, 0.7);
z-index: 1000;
justify-content: center;
align-items: center;
backdrop-filter: blur(3px);
}
.preview-container {
background-color: white;
border-radius: 8px;
padding: 20px;
max-width: 90%;
max-height: 90%;
overflow: hidden;
position: relative;
box-shadow: 0 5px 15px rgba(0, 0, 0, 0.3);
}
.preview-close {
position: absolute;
top: 10px;
right: 10px;
background: rgba(0, 0, 0, 0.2);
border: none;
color: white;
border-radius: 50%;
width: 30px;
height: 30px;
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
font-size: 20px;
transition: background-color 0.2s;
}
.preview-close:hover {
background: rgba(0, 0, 0, 0.4);
}
.preview-content {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
max-height: calc(90vh - 40px);
overflow: auto;
}
.preview-image, .preview-video {
max-width: 100%;
max-height: 70vh;
object-fit: contain;
}
.preview-audio {
width: 100%;
max-width: 400px;
}
.preview-icon {
font-size: 64px;
color: #555;
margin: 20px 0;
}
.preview-name {
font-weight: bold;
margin: 10px 0;
word-break: break-all;
text-align: center;
}
.preview-info {
color: #777;
text-align: center;
}
</style>
</head>
<body>
<div class="container">
<h1>File Upload</h1>
<div class="upload-area" id="uploadArea">
<div class="upload-icon">
<svg width="64" height="64" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"></path>
<polyline points="17 8 12 3 7 8"></polyline>
<line x1="12" y1="3" x2="12" y2="15"></line>
</svg>
</div>
<p>拖曳檔案至此或 <label for="fileInput" class="browse-link">瀏覽</label></p>
<input type="file" id="fileInput" multiple hidden>
</div>
<div class="file-list" id="fileList"></div>
<button id="uploadButton" class="upload-button" disabled>上傳檔案</button>
</div>
<!-- Preview Modal -->
<div class="preview-modal" id="previewModal">
<div class="preview-container">
<button id="closeModal" class="preview-close">×</button>
<div class="preview-content" id="previewContent"></div>
</div>
</div>
<script>
document.addEventListener('DOMContentLoaded', function() {
const uploadArea = document.getElementById('uploadArea');
const fileInput = document.getElementById('fileInput');
const fileList = document.getElementById('fileList');
const uploadButton = document.getElementById('uploadButton');
const previewModal = document.getElementById('previewModal');
const previewContent = document.getElementById('previewContent');
const closeModal = document.getElementById('closeModal');
let files = [];
// 避免預設拖曳事件
['dragenter', 'dragover', 'dragleave', 'drop'].forEach(eventName => {
uploadArea.addEventListener(eventName, preventDefaults, false);
});
function preventDefaults(e) {
e.preventDefault();
e.stopPropagation();
}
['dragenter', 'dragover'].forEach(eventName => {
uploadArea.addEventListener(eventName, highlight, false);
});
['dragleave', 'drop'].forEach(eventName => {
uploadArea.addEventListener(eventName, unhighlight, false);
});
function highlight() {
uploadArea.classList.add('dragover');
}
function unhighlight() {
uploadArea.classList.remove('dragover');
}
uploadArea.addEventListener('drop', handleDrop, false);
function handleDrop(e) {
const dt = e.dataTransfer;
const newFiles = dt.files;
handleFiles(newFiles);
}
uploadArea.addEventListener('click', () => {
fileInput.click();
});
fileInput.addEventListener('change', () => {
handleFiles(fileInput.files);
});
function handleFiles(newFiles) {
if (newFiles.length === 0) return;
const filesArray = Array.from(newFiles);
const processedFiles = filesArray.map(file => {
if (file.type.startsWith('image/')) {
file.previewUrl = URL.createObjectURL(file);
}
return file;
});
files = [...files, ...processedFiles];
updateFileList();
fileInput.value = '';
}
function updateFileList() {
fileList.innerHTML = '';
files.forEach((file, index) => {
const fileItem = document.createElement('div');
fileItem.className = 'file-item';
const fileItemHeader = document.createElement('div');
fileItemHeader.className = 'file-item-header';
const fileInfo = document.createElement('div');
fileInfo.className = 'file-info';
if (file.type.startsWith('image/') && file.previewUrl) {
const preview = document.createElement('img');
preview.className = 'file-preview';
preview.src = file.previewUrl;
preview.alt = file.name;
fileInfo.appendChild(preview);
} else {
const fileIcon = document.createElement('div');
fileIcon.className = 'file-icon';
fileIcon.innerHTML = getFileIcon(file);
fileInfo.appendChild(fileIcon);
}
const fileName = document.createElement('div');
fileName.className = 'file-name';
fileName.textContent = file.name;
const fileSize = document.createElement('div');
fileSize.className = 'file-size';
fileSize.textContent = formatFileSize(file.size);
const buttonsContainer = document.createElement('div');
buttonsContainer.className = 'file-buttons';
const previewButton = document.createElement('button');
previewButton.className = 'preview-file';
previewButton.innerHTML = '<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z"></path><circle cx="12" cy="12" r="3"></circle></svg>';
previewButton.title = "預覽";
previewButton.addEventListener('click', (e) => {
e.stopPropagation();
previewFile(file);
});
const analyzeButton = document.createElement('button');
analyzeButton.className = 'analyze-file';
analyzeButton.innerHTML = '<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"></circle><path d="M12 16v-4"></path><path d="M12 8h.01"></path></svg>';
analyzeButton.title = "影像標籤分析";
analyzeButton.addEventListener('click', (e) => {
e.stopPropagation();
if (file.type.startsWith('image/')) {
analyzeImage(file, index);
} else {
alert('目前只支援圖片檔案的分析功能');
}
});
const textRecognizeButton = document.createElement('button');
textRecognizeButton.className = 'text-recognize-file';
textRecognizeButton.innerHTML = '<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M4 7V4h16v3"></path><path d="M9 20h6"></path><path d="M12 4v16"></path></svg>';
textRecognizeButton.title = "文字辨識";
textRecognizeButton.addEventListener('click', (e) => {
e.stopPropagation();
if (file.type.startsWith('image/')) {
recognizeText(file, index);
} else {
alert('目前只支援圖片檔案的文字辨識功能');
}
});
const removeButton = document.createElement('button');
removeButton.className = 'remove-file';
removeButton.innerHTML = '×';
removeButton.title = "移除";
removeButton.addEventListener('click', (e) => {
e.stopPropagation();
removeFile(index);
});
buttonsContainer.appendChild(previewButton);
buttonsContainer.appendChild(analyzeButton);
buttonsContainer.appendChild(textRecognizeButton);
buttonsContainer.appendChild(removeButton);
fileInfo.appendChild(fileName);
fileInfo.appendChild(fileSize);
fileItemHeader.appendChild(fileInfo);
fileItemHeader.appendChild(buttonsContainer);
const progressBarContainer = document.createElement('div');
progressBarContainer.className = 'progress-bar';
const progressBar = document.createElement('div');
progressBar.className = 'progress';
progressBarContainer.appendChild(progressBar);
fileItem.appendChild(fileItemHeader);
fileItem.appendChild(progressBarContainer);
fileList.appendChild(fileItem);
// 若已經有分析或辨識結果(理論上初次不會有),可在此顯示
if (file.analysisResult) {
const analysisContainer = document.createElement('div');
analysisContainer.className = 'analysis-result';
analysisContainer.innerHTML = `<div class="analysis-content">${file.analysisResult}</div>`;
fileItem.appendChild(analysisContainer);
file.analysisContainer = analysisContainer;
}
if (file.textRecognitionResult) {
const textContainer = document.createElement('div');
textContainer.className = 'text-recognition-result';
textContainer.innerHTML = `<div class="text-content">${file.textRecognitionResult}</div>`;
fileItem.appendChild(textContainer);
file.textContainer = textContainer;
}
});
uploadButton.disabled = files.length === 0;
}
function removeFile(index) {
if (files[index].previewUrl) {
URL.revokeObjectURL(files[index].previewUrl);
}
files.splice(index, 1);
updateFileList();
}
function getFileIcon(file) {
const fileType = file.type.split('/')[0];
let icon = '';
switch (fileType) {
case 'image':
icon = '<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="3" width="18" height="18" rx="2" ry="2"></rect><circle cx="8.5" cy="8.5" r="1.5"></circle><polyline points="21 15 16 10 5 21"></polyline></svg>';
break;
case 'video':
icon = '<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="2" y="2" width="20" height="20" rx="2.18" ry="2.18"></rect><line x1="7" y1="2" x2="7" y2="22"></line><line x1="17" y1="2" x2="17" y2="22"></line><line x1="2" y1="12" x2="22" y2="12"></line><line x1="2" y1="7" x2="7" y2="7"></line><line x1="2" y1="17" x2="7" y2="17"></line><line x1="17" y1="17" x2="22" y2="17"></line><line x1="17" y1="7" x2="22" y2="7"></line></svg>';
break;
case 'audio':
icon = '<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M3 18v-6a9 9 0 0 1 18 0v6"></path><path d="M21 19a2 2 0 0 1-2 2h-1a2 2 0 0 1-2-2v-3a2 2 0 0 1 2-2h3zM3 19a2 2 0 0 0 2 2h1a2 2 0 0 0 2-2v-3a2 2 0 0 0-2-2H3z"></path></svg>';
break;
default:
icon = '<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"></path><polyline points="14 2 14 8 20 8"></polyline><line x1="16" y1="13" x2="8" y2="13"></line><line x1="16" y1="17" x2="8" y2="17"></line><polyline points="10 9 9 9 8 9"></polyline></svg>';
}
return icon;
}
function formatFileSize(bytes) {
const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB'];
if (bytes === 0) return '0 Byte';
const i = parseInt(Math.floor(Math.log(bytes) / Math.log(1024)));
return Math.round(bytes / Math.pow(1024, i), 2) + ' ' + sizes[i];
}
function previewFile(file) {
previewContent.innerHTML = '';
if (file.type.startsWith('image/')) {
const img = document.createElement('img');
img.src = file.previewUrl;
img.alt = file.name;
img.className = 'preview-image';
previewContent.appendChild(img);
} else if (file.type.startsWith('video/')) {
const video = document.createElement('video');
video.src = URL.createObjectURL(file);
video.controls = true;
video.autoplay = false;
video.className = 'preview-video';
previewContent.appendChild(video);
} else if (file.type.startsWith('audio/')) {
const audio = document.createElement('audio');
audio.src = URL.createObjectURL(file);
audio.controls = true;
audio.className = 'preview-audio';
previewContent.appendChild(audio);
} else {
const icon = document.createElement('div');
icon.className = 'preview-icon';
icon.innerHTML = getFileIcon(file);
const name = document.createElement('p');
name.textContent = file.name;
name.className = 'preview-name';
const info = document.createElement('p');
info.textContent = '此檔案類型無法預覽';
info.className = 'preview-info';
previewContent.appendChild(icon);
previewContent.appendChild(name);
previewContent.appendChild(info);
}
previewModal.style.display = 'flex';
}
closeModal.addEventListener('click', () => {
previewModal.style.display = 'none';
const videoOrAudio = previewContent.querySelector('video, audio');
if (videoOrAudio && videoOrAudio.src) {
URL.revokeObjectURL(videoOrAudio.src);
}
previewContent.innerHTML = '';
});
previewModal.addEventListener('click', (e) => {
if (e.target === previewModal) {
closeModal.click();
}
});
uploadButton.addEventListener('click', () => {
const progressBars = document.querySelectorAll('.progress');
progressBars.forEach((progressBar, index) => {
let progress = 0;
const interval = setInterval(() => {
progress += Math.random() * 10;
if (progress >= 100) {
progress = 100;
clearInterval(interval);
if (index === files.length - 1) {
setTimeout(() => {
alert('所有檔案上傳完成!(示範用:實際未上傳至伺服器)');
files.forEach(file => {
if (file.previewUrl) {
URL.revokeObjectURL(file.previewUrl);
}
});
files = [];
updateFileList();
}, 500);
}
}
progressBar.style.width = progress + '%';
}, 200);
});
});
function analyzeImage(file, index) {
const fileItems = document.querySelectorAll('.file-item');
const fileItem = fileItems[index];
fileItem.classList.add('analyzing');
const progressBar = fileItem.querySelector('.progress');
progressBar.style.width = '100%';
progressBar.style.backgroundColor = '#3498db';
readFileAsDataURL(file).then(imageDataUrl => {
if (!file.analysisContainer) {
const analysisContainer = document.createElement('div');
analysisContainer.className = 'analysis-result';
fileItem.appendChild(analysisContainer);
file.analysisContainer = analysisContainer;
}
file.analysisContainer.innerHTML = '<div class="analysis-loading">影像標籤分析中...</div>';
// 呼叫後端 Google Apps Script 函式
google.script.run
.withSuccessHandler(function(result) {
file.analysisResult = result;
file.analysisContainer.innerHTML = `<div class="analysis-content">${file.analysisResult}</div>`;
fileItem.classList.remove('analyzing');
progressBar.style.width = '0%';
})
.withFailureHandler(function(error) {
console.error('Image analysis error:', error);
file.analysisContainer.innerHTML = `<div class="analysis-error">辨識失敗: ${error.message}</div>`;
fileItem.classList.remove('analyzing');
progressBar.style.width = '0%';
})
.analyzeImageServer(imageDataUrl);
}).catch(error => {
console.error('File read error:', error);
fileItem.classList.remove('analyzing');
progressBar.style.width = '0%';
});
}
function recognizeText(file, index) {
const fileItems = document.querySelectorAll('.file-item');
const fileItem = fileItems[index];
fileItem.classList.add('analyzing');
const progressBar = fileItem.querySelector('.progress');
progressBar.style.width = '100%';
progressBar.style.backgroundColor = '#9b59b6';
readFileAsDataURL(file).then(imageDataUrl => {
if (!file.textContainer) {
const textContainer = document.createElement('div');
textContainer.className = 'text-recognition-result';
fileItem.appendChild(textContainer);
file.textContainer = textContainer;
}
file.textContainer.innerHTML = '<div class="text-loading">文字辨識中...</div>';
// 呼叫後端 Google Apps Script 函式
google.script.run
.withSuccessHandler(function(result) {
file.textRecognitionResult = result;
file.textContainer.innerHTML = `<div class="text-content">${file.textRecognitionResult}</div>`;
fileItem.classList.remove('analyzing');
progressBar.style.width = '0%';
})
.withFailureHandler(function(error) {
console.error('Text recognition error:', error);
file.textContainer.innerHTML = `<div class="text-error">文字辨識失敗: ${error.message}</div>`;
fileItem.classList.remove('analyzing');
progressBar.style.width = '0%';
})
.recognizeTextServer(imageDataUrl);
}).catch(error => {
console.error('File read error:', error);
fileItem.classList.remove('analyzing');
progressBar.style.width = '0%';
});
}
function readFileAsDataURL(file) {
return new Promise((resolve, reject) => {
const reader = new FileReader();
reader.onload = () => resolve(reader.result);
reader.onerror = () => reject(new Error('檔案讀取失敗'));
reader.readAsDataURL(file);
});
}
});
</script>
</body>
</html>
重要注意事項
-
啟用 Cloud Vision API
- 前往 Google Cloud Console,建立或選擇已有的專案。
- 在 API & Services 裡面啟用 Cloud Vision API,並建立 API Key。
- 需綁定計費專案才能使用文字辨識等功能(有免費額度)。
-
設定 API Key
- 在
Code.gs的analyzeImageServer與recognizeTextServer兩處,將YOUR_API_KEY替換為實際的 API Key。 - 或考量安全性,將 API Key 存於 Script Properties 讀取。
- 在
-
部署為 Web App
- 進入 Apps Script 編輯器後,選擇 Deploy → New deployment → Web app。
- 設定好專案名稱、執行方式(Me)與存取層級(Anyone)。
- 點選 Deploy 完成。
-
權限與測試
- 首次部署可能需要授權
UrlFetchApp等。 - 部署成功後,複製該 Web App URL,在瀏覽器打開即可使用。
- 首次部署可能需要授權
完成上述步驟後,你就能在 Web App 介面上上傳圖片,並實際呼叫 Google Cloud Vision API 進行文字辨識 (OCR) 與影像標籤 (Label Detection) 分析。