Flutter与FastAPI文件上传时的PDF格式异常问题
问题:Flutter+FastAPI上传CBC参数与PDF到数据库时PDF格式异常
在基于Flutter和FastAPI开发的应用中,调用/upload-images接口获取CBC参数和PDF内容后,携带访问令牌调用/reports接口上传数据到数据库时出现异常,已定位问题为PDF格式不正确。
返回的JSON数据示例
{"CBC_features":{"RBC":3.0,"MCV":80.0,"MCH":32.0,"MCHC":32.0,"RDWSD":50.0,"RDWCV":14.0,"PCT":0.2,"MPV":10.0,"PDW":20.0,"NEUTp":40.0,"NEUTn":4.0,"LYMp":50.0,"WBC":10.0,"MIDn":0.5,"MIDp":10.0,"PLT":150.0,"PLCR":15.0,"LYMn":0,"HGB":0,"HCT":0},"pdf_content":"JVBERi0xLjMKMyAwIG9iago8PC9UeXBlIC9QYWdlCi9QYXJlbnQgMSAwIFIKL1Jlc291cmNlcyAyIDAgUgovQ29udGVudHMgNCAwIFI+PgplbmRvYmoKNCAwIG9iago8PC9GaWx0ZXIgL0ZsYXRlRGVjb2RlIC9MZW5ndGggNTc+PgpzdHJlYW0KeJwzUvDiMtAzNVco5ypUMLU01TOyUDAAQgsTQz0LYwUDPQMDEGGskJyroO9pqOCSrxDIBQAXSAqaCmVuZHN0cmVhbQplbmRvYmoKMSAwIG9iago8PC9UeXBlIC9QYWdlcwovS2lkcyBbMyAwIFIgXQovQ29udGVudCAxCi9NZWRpYUJveCBbMCAwIDU5NS4yOCA4NDEuODldCj4+CmVuZG9iagp1IDAgb2JqCjw8L1R5cGUgL1hPYmplY3QKL1N1YnR5cGUgL0ltYWdlCi9XaWR0aCAxMjQxCi9IZWlnaHQgMTc1NQovQ29sb3JTcGFjZSAvRGV2aWNlUkdGCi9CaXRzUGVyQ29tcG9uZW50IDgKL0ZpbHRlciAvRmxhdGVEZWNvZGUKL0RlY29kZVBhcm1zIDw8L1ByZWRpY3RvciAxNS9Db2xvcnMgMy9CaXRzUGVyQ29tcG9uZW50IDgvQ29sdW1ucyAxMjQxPj4KL0xlbmd0aCAyMTc2MDg+PgpzdHJlYW0KbmljNzYxbm5iUlFWK3ZqdFV4MG5rWVlYZ29BSUFvSWdpcUtDa2FZSjw8LykK"}
Flutter端上传代码
Future<void> uploadImages() async { // Check if there are images to upload if (_images.isEmpty) { // Show a message to the user indicating that no images are selected ScaffoldMessenger.of(context).showSnackBar( const SnackBar( content: Text('No images selected to upload'), backgroundColor: Colors.red, ), ); return; } // Show a loading indicator while uploading ScaffoldMessenger.of(context).showSnackBar( SnackBar( content: const Text('Uploading images...'), backgroundColor: Colors.grey[900], duration: const Duration( days: 1), // Show indefinitely until upload is complete ), ); try { // Prepare the list of files to upload List<MultipartFile> files = []; for (final image in _images) { // Convert file to multipart file files.add(await MultipartFile.fromFile( image.path, filename: image.path.split('/').last, )); } // Create a FormData object FormData formData = FormData.fromMap({'files': files}); // Make an API request to upload images final response = await Dio().post( 'http://127.0.0.1:8000/upload-images', data: formData, options: Options( headers: { 'Accept': 'application/json', 'Content-Type': 'multipart/form-data', }, ), ); print(response); print(response.statusCode); // Check if the request was successful if (response.statusCode == 200) { // Extract CBC features and PDF content from the response final cbcFeatures = response.data['CBC_features']; final pdfContent = response.data['pdf_content']; // Convert the base64 encoded string back to bytes final Uint8List pdfBytes = base64Decode(pdfContent); // Construct the multipart file from the bytes final pdfFile = http.MultipartFile.fromBytes('pdf_file', pdfBytes, filename: 'report.pdf'); // URL encode the access token final encodedAccessToken = Uri.encodeComponent(widget.accessToken); // Construct the URL for the report endpoint final reportUrl = 'http://127.0.0.1:8000/reports?access_token=$encodedAccessToken'; // Make a request to the /report endpoint using the extracted data final reportResponse = await Dio().post( reportUrl, data: { 'cbc_features': cbcFeatures, 'pdf_file': pdfFile, }, options: Options( headers: { 'Accept': 'application/json', }, ), ); // Check if the report request was successful if (reportResponse.statusCode == 201) { print('reportResponse.statusCode == 201'); // Show a success message to the user ScaffoldMessenger.of(context).showSnackBar( const SnackBar( content: Text('Report created successfully'), backgroundColor: Colors.green, ), ); } else { print('error message 1'); // Show an error message to the user ScaffoldMessenger.of(context).showSnackBar( SnackBar( content: Text('Failed to create report: ${reportResponse.statusCode}'), backgroundColor: Colors.red, ), ); } } else { print('error message 2'); // Show an error message to the user ScaffoldMessenger.of(context).showSnackBar( SnackBar( content: Text('Failed to upload images: ${response.statusCode}'), backgroundColor: Colors.red, ), ); } } catch (error) { print('Error occurred: $error'); // Print the error message for debugging print('error message 3'); // Show an error message to the user ScaffoldMessenger.of(context).showSnackBar( SnackBar( content: Text('Error creating report: $error'), backgroundColor: Colors.red, ), ); } }
FastAPI端接口代码
@app.post("/upload-images") async def upload_images(files: List[UploadFile] = File(...)): CBC_features = [] result = {} pdf = FPDF() try: for idx, file in enumerate(files): contents = await file.read() # ------------ OCR Start ------------ print("OCR started") response = await ocr(file, contents) CBC_features.append(response) print(f"Received file: {file.filename}, OCR: {response}") # ------------ OCR End ------------ # ------------ Image2PDF Start ------------ # Convert image data to PNG format image = Image.open(io.BytesIO(contents)) page_width, page_height = 210, 297 # A4 dimensions in mm image_width, image_height = image.size scaling_factor = min(page_width / image_width, page_height / image_height) new_width = image_width * scaling_factor new_height = image_height * scaling_factor x = (page_width - new_width) / 2 y = (page_height - new_height) / 2 with io.BytesIO() as temp_buffer: image.save(temp_buffer, format="PNG") temp_buffer.seek(0) png_data = temp_buffer.read() # Write PNG data to a temporary file with tempfile.NamedTemporaryFile(delete=False, suffix='.png') as temp_file: temp_file.write(png_data) temp_file_path = temp_file.name # Add a page to the PDF pdf.add_page() # Add image to the PDF using the temporary file path pdf.image(temp_file_path, x=x, y=y, w=new_width, h=new_height) # Delete the temporary file os.unlink(temp_file_path) # ------------ Image2PDF End ------------ except Exception as e: print('error in OCR:', idx, e) print('-------> CBC_features:', CBC_features) try: # Extract the first value for each key for features in CBC_features: for key, value in features.items(): if key not in result or (key in result and value != 0): result[key] = value print('final result:', result) except Exception as e: print('error in compression:', e) pdf_path = "pdf/output.pdf" pdf.output(pdf_path) with open(pdf_path, "rb") as pdf_file: pdf_bytes = pdf_file.read() print("PDF saved successfully.") pdf_base64 = base64.b64encode(pdf_bytes).decode('utf-8') return {"CBC_features": result, "pdf_content": pdf_base64} @app.post("/reports", status_code=status.HTTP_201_CREATED) async def create_report(cbc_parameters: List[str], pdf_file: UploadFile = File(...), db: Session = Depends(get_db), current_user: models.User = Depends(get_current_user)): try: print(cbc_parameters) # print(pdf_file) print(current_user.id) pdf_content = pdf_file.file.read() # Create report instance db_report = models.Report(cbc_parameters=cbc_parameters, pdf=pdf_content, owner_id=current_user.id) # Add report to the database db.add(db_report) db.commit() db.refresh(db_report) print("db updated") print("running CBC model....") await cbc_model(username=current_user.username, user_id=current_user.id, db=db) print("model run complete!") except Exception as e: db.rollback() print(e) raise HTTPException(status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail=str(e))
问题分析与修复方案
核心问题
- Flutter端请求格式错误:混合使用
http.MultipartFile和普通JSON数据,Dio无法正确序列化这种混合格式,导致PDF字节流被错误处理,传递到后端的不是有效PDF。 - FastAPI端参数类型不匹配:
cbc_parameters定义为List[str],但前端传递的是JSON对象,类型不匹配直接引发异常。
修复步骤
1. Flutter端统一使用Dio的FormData构造请求
替换原有的PDF文件构造和请求部分,用FormData统一处理文件和参数:
import 'dart:convert'; // ... 其他代码不变 // 替换原请求构造部分 final formData = FormData.fromMap({ 'cbc_features': jsonEncode(cbcFeatures), // 将CBC参数转为JSON字符串 'pdf_file': MultipartFile.fromBytes( pdfBytes, filename: 'report.pdf', contentType: MediaType('application', 'pdf'), ), }); final reportResponse = await Dio().post( reportUrl, data: formData, options: Options( headers: { 'Accept': 'application/json', 'Content-Type': 'multipart/form-data', }, ), );
2. FastAPI端调整参数解析逻辑
将cbc_parameters改为接收JSON字符串,解析为字典后使用,同时修正文件读取方式:
import json from pydantic import BaseModel # 定义CBC参数验证模型(可选,用于格式校验) class CBCParameters(BaseModel): RBC: float MCV: float MCH: float # 补充其他参数... @app.post("/reports", status_code=status.HTTP_201_CREATED) async def create_report( cbc_parameters: str, # 接收JSON字符串 pdf_file: UploadFile = File(...), db: Session = Depends(get_db), current_user: models.User = Depends(get_current_user) ): try: # 解析JSON字符串为字典 cbc_params = json.loads(cbc_parameters) # 可选:用模型验证参数格式 validated_params = CBCParameters(**cbc_params) pdf_content = await pdf_file.read() # 异步读取文件内容 db_report = models.Report( cbc_parameters=cbc_params, # 直接存储字典(或转字符串,根据数据库字段类型调整) pdf=pdf_content, owner_id=current_user.id ) db.add(db_report) db.commit() db.refresh(db_report) await cbc_model(username=current_user.username, user_id=current_user.id, db=db) except Exception as e: db.rollback() raise HTTPException(status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail=str(e))
3. 额外优化
FastAPI的/upload-images接口可直接在内存生成PDF,避免写入本地文件:
# 替换原PDF存储和读取部分 pdf_buffer = io.BytesIO() pdf.output(pdf_buffer) pdf_buffer.seek(0) pdf_bytes = pdf_buffer.read() pdf_base64 = base64.b64encode(pdf_bytes).decode('utf-8')
内容的提问来源于stack exchange,提问作者Abhitay Shinde
相关产品推荐
相关产品推荐

