You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何通过Dash/Streamlit/Flask在客户端提取图片EXIF(不上传服务器)

解决方案:客户端提取图片EXIF并传回Python

一、修复你的Dash代码

你的Dash代码无响应主要是ID不匹配、数据处理逻辑错误、EXIF调用方式不对导致的,以下是修复后的完整代码:

import dash
from dash import dcc, html, Input, Output

app = dash.Dash(__name__, external_scripts=['https://cdn.jsdelivr.net/npm/exif-js'])
app.title = "Get EXIF Data"

app.layout = html.Div(
    [
        dcc.Upload(
            id='upload-images',
            children=html.Div([
                'Drag and Drop or ',
                html.A('Select Files')
            ]),
            style={
                'width': '100%',
                'height': '60px',
                'lineHeight': '60px',
                'borderWidth': '1px',
                'borderStyle': 'dashed',
                'borderRadius': '5px',
                'textAlign': 'center',
                'margin': '10px'
            },
            multiple=True
        ),
        html.Div(id='display-exif'),
        # 隐藏存储组件,用于将EXIF数据传回Python端
        dcc.Store(id='exif-data-store')
    ]
)

# 客户端回调:提取EXIF并返回显示内容+存储数据
app.clientside_callback(
    """
    function(contents) {
        if (!contents || contents.length === 0) {
            return ["", null];
        }
        
        // 处理第一个上传的文件(base64转Blob)
        const content = contents[0];
        const [typePart, base64Data] = content.split(',');
        const contentType = typePart.split(':')[1].split(';')[0];
        const blob = b64toBlob(base64Data, contentType);
        
        // 创建图片元素供EXIF解析
        const img = new Image();
        img.src = URL.createObjectURL(blob);
        
        return new Promise((resolve) => {
            img.onload = function() {
                EXIF.getData(img, function() {
                    const exifData = {
                        make: EXIF.getTag(this, "Make") || "Unknown",
                        model: EXIF.getTag(this, "Model") || "Unknown",
                        dateTime: EXIF.getTag(this, "DateTimeOriginal") || "Unknown",
                        gpsLat: EXIF.getTag(this, "GPSLatitude") || "Unknown"
                    };
                    
                    // 返回显示内容和存储用的JSON数据
                    resolve([
                        html.Div([
                            html.H4("EXIF信息"),
                            html.P(`相机品牌: ${exifData.make}`),
                            html.P(`相机型号: ${exifData.model}`),
                            html.P(`拍摄时间: ${exifData.dateTime}`)
                        ]),
                        exifData
                    ]);
                });
            };
        });
        
        // 辅助函数:base64转Blob
        function b64toBlob(b64Data, contentType = '', sliceSize = 512) {
            const byteCharacters = atob(b64Data);
            const byteArrays = [];
            for (let offset = 0; offset < byteCharacters.length; offset += sliceSize) {
                const slice = byteCharacters.slice(offset, offset + sliceSize);
                const byteNumbers = new Array(slice.length);
                for (let i = 0; i < slice.length; i++) {
                    byteNumbers[i] = slice.charCodeAt(i);
                }
                byteArrays.push(new Uint8Array(byteNumbers));
            }
            return new Blob(byteArrays, { type: contentType });
        }
    }
    """,
    [Output('display-exif', 'children'), Output('exif-data-store', 'data')],
    Input('upload-images', 'contents'),
    prevent_initial_call=True
)

# Python端回调:接收EXIF数据做后续处理
@app.callback(
    Output('display-exif', 'children', allow_duplicate=True),
    Input('exif-data-store', 'data'),
    prevent_initial_call=True
)
def process_exif_data(exif_data):
    # 这里可添加自定义处理逻辑,比如存入数据库、数据分析等
    processed_note = f"Python端已完成EXIF数据处理:品牌{exif_data['make']},型号{exif_data['model']}"
    return html.Div([
        html.H4("EXIF信息"),
        html.P(f"相机品牌: {exif_data['make']}"),
        html.P(f"相机型号: {exif_data['model']}"),
        html.P(f"拍摄时间: {exif_data['dateTime']}"),
        html.Hr(),
        html.P(processed_note)
    ])

if __name__ == '__main__':
    app.run_server(debug=False)

关键修复点:

  • 修正了上传组件与回调的ID匹配问题
  • 正确将dcc.Upload返回的base64内容转为Blob,再创建图片供EXIF.js解析
  • 用Promise处理异步加载逻辑,确保EXIF数据提取完成后再返回结果
  • 通过dcc.Store将EXIF数据以JSON格式传回Python端
  • 新增Python端回调,接收数据并执行后续处理

二、Streamlit实现方案

Streamlit默认会上传文件到服务器,但可通过嵌入JS实现客户端EXIF提取,再将数据传回Python:

import streamlit as st
import json

st.title("客户端提取图片EXIF")

# 嵌入JS实现客户端EXIF提取
st.components.v1.html("""
<script src="https://cdn.jsdelivr.net/npm/exif-js"></script>
<div>
    <input type="file" id="image-upload" accept="image/*">
    <div id="exif-display"></div>
</div>
<script>
    const uploadInput = document.getElementById('image-upload');
    const displayDiv = document.getElementById('exif-display');
    
    uploadInput.addEventListener('change', function(e) {
        const file = e.target.files[0];
        if (!file) return;
        
        const img = new Image();
        img.src = URL.createObjectURL(file);
        
        img.onload = function() {
            EXIF.getData(img, function() {
                const exifData = {
                    make: EXIF.getTag(this, "Make") || "Unknown",
                    model: EXIF.getTag(this, "Model") || "Unknown",
                    dateTime: EXIF.getTag(this, "DateTimeOriginal") || "Unknown"
                };
                
                // 显示客户端提取结果
                displayDiv.innerHTML = `
                    <h4>EXIF信息</h4>
                    <p>相机品牌: ${exifData.make}</p>
                    <p>相机型号: ${exifData.model}</p>
                    <p>拍摄时间: ${exifData.dateTime}</p>
                `;
                
                // 将数据传回Streamlit
                window.parent.postMessage({type: 'EXIF_DATA', data: exifData}, '*');
            });
        };
    });
</script>
""", height=300)

# 接收JS传回的EXIF数据
st.components.v1.html("""
<script>
    window.addEventListener('message', function(event) {
        if (event.data.type === 'EXIF_DATA') {
            // 创建隐藏文本框存储数据,供Python读取
            const textarea = document.createElement('textarea');
            textarea.id = 'exif-data';
            textarea.style.display = 'none';
            textarea.value = JSON.stringify(event.data.data);
            document.body.appendChild(textarea);
            // 触发Streamlit组件更新
            window.parent.document.dispatchEvent(new Event('streamlit:componentValueChanged'));
        }
    });
</script>
""", height=0)

# 读取隐藏文本框的EXIF数据
exif_raw = st.text_input("", key='exif_input', label_visibility='hidden')
if exif_raw:
    exif_data = json.loads(exif_raw)
    st.subheader("Python端处理结果")
    st.write(f"相机品牌: {exif_data['make']}")
    st.write(f"相机型号: {exif_data['model']}")
    st.write(f"拍摄时间: {exif_data['dateTime']}")
    # 此处添加自定义Python处理逻辑

说明:

  • 通过st.components.v1.html嵌入JS,在浏览器端直接完成EXIF提取
  • 用postMessage将数据传到Streamlit父页面,存入隐藏文本框供Python读取
  • 全程无需上传图片文件到服务器,完全在客户端完成解析

三、核心逻辑总结

不管用Dash还是Streamlit,实现需求的核心步骤都是:

  1. 在客户端加载EXIF.js库
  2. 将用户上传的图片转为可解析的图片元素
  3. 提取目标EXIF字段并转为JSON格式
  4. 通过组件(Dash的dcc.Store/Streamlit的隐藏输入框)将数据传回Python
  5. 在Python端接收数据并执行后续处理

内容的提问来源于stack exchange,提问作者b.dot

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.13 14:10:27