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

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('-------&gt; 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))

问题分析与修复方案

核心问题

  1. Flutter端请求格式错误:混合使用http.MultipartFile和普通JSON数据,Dio无法正确序列化这种混合格式,导致PDF字节流被错误处理,传递到后端的不是有效PDF。
  2. 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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.26 12:17:04