如何在React和FastAPI/MongoDB中显示这个以二进制数据存储的图像?

huangapple go评论74阅读模式
英文:

How can I display this image (stored as binary data) with React and FastAPI/MongoDB

问题

I understand that you want assistance with the issues you're facing. However, your request is quite extensive, and there are several components involved. To pinpoint the exact issue, it would be best to start by examining the error message you provided:

File "pydantic\json.py", line 45, in pydantic.json.lambda
UnicodeDecodeError: 'utf-8' codec can't decode byte 0x89 in position 0: invalid start byte

This error suggests that there is an issue with the encoding when trying to decode the image data. The 0x89 byte suggests it might be a PNG image.

To help you further, I would need to know if this error occurs when you try to display a product image, a user image, or both, as well as more information about the front-end React components where you call the ProductImage component.

If you could provide more specific details about where the error occurs and any additional error messages or logs, I can provide more targeted assistance.

英文:

I am trying to display an image which is stored as binary data in my MongoDB database. I have this functionality working well for User images, but cannot get it to work for products. I'm using FastAPI, React, and MongoDB as my stack.

I have a list of products being stored in my MongoDB products collection, which has the following structure:

class Products(BaseModel):
    _id: str = Field(alias="_id")
    productName: str
    productID: str
    photographer: str
    productEvent: str
    productTags: Optional[List[str]] = None
    productPrice: float
    productCurrency: str
    productDate: Optional[datetime.date] = None
    productImage: Optional[bytes] = None
    productImageExtension: Optional[str] = None

The 'productImage' field is a binary field in the mongoDB collection. I can populate this with data using the following form perfectly:

import React, {useState, useEffect} from 'react';
import axios from 'axios';

function ProductUpload(){
  const [productName, setProductName] = useState('');
  const [productID, setProductID] = useState('');
  const [photographer, setPhotographer] = useState('');
  const [productEvent, setProductEvent] = useState('');
  const [productTags, setProductTags] = useState([]);
  const [productPrice, setProductPrice] = useState(0);
  const [productCurrency, setProductCurrency] = useState('');
  const [productDate, setProductDate] = useState('');
  const [productImage, setProductImage] = useState(null);
  const [error, setError] = useState('');

  const handleSubmit = async (event) => {
    event.preventDefault();
  
    const formData = new FormData();
    formData.append('productName', productName);
    formData.append('productID', productID);
    formData.append('photographer', photographer);
    formData.append('productEvent', productEvent);
    formData.append('productTags', JSON.stringify(productTags));
    formData.append('productPrice', productPrice);
    formData.append('productCurrency', productCurrency);
    formData.append('productDate', productDate);
    formData.append('productImage', productImage);
  
    try {
      const response = await axios.post('http://localhost:8000/api/products', formData);
      console.log(response.data);
    } catch (error) {
      console.log(error.response.data);
      setError(error.response.data.detail);
    }
  };

  return(
    <form onSubmit={handleSubmit} style={{alignContents: "center", justifyContent: "center", display: "flex", flexDirection: "column", margin: "auto", width: "100%"}}>
      <label>
        Product Name:
        <input type="text" value={productName} onChange={(event) => setProductName(event.target.value)} />
      </label>
      <label>
        Product ID:
        <input type="text" value={productID} onChange={(event) => setProductID(event.target.value)} />
      </label>
      <label>
        Photographer:
        <input type="text" value={photographer} onChange={(event) => setPhotographer(event.target.value)} />
      </label>
      <label>
        Product Event:
        <input type="text" value={productEvent} onChange={(event) => setProductEvent(event.target.value)} />
      </label>
      <label>
        Product Tags:
        <input type="text" value={productTags} onChange={(event) => setProductTags(event.target.value.split(','))} />
      </label>
      <label>
        Product Price:
        <input type="number" value={productPrice} onChange={(event) => setProductPrice(event.target.value)} />
      </label>
      <label>
        Product Currency:
        <input type="text" value={productCurrency} onChange={(event) => setProductCurrency(event.target.value)} />
      </label>
      <label>
        Product Date:
        <input type="date" value={productDate} onChange={(event) => setProductDate(event.target.value)} />
      </label>
      <label>
        Product Image:
        <input type="file" onChange={(event) => setProductImage(event.target.files[0])} />
      </label>
      <button type="submit">Submit product</button>
      </form>


  )
}



export default ProductUpload;

Here are my backend functions relating to Products:

main.py:

@app.get("/api/products")
async def get_products():
    response = await fetch_all_products()
    return response

@app.get("/api/products{productName}", response_model=Products)
async def get_product_by_name(productName):
    response = await fetch_one_product(productName)
    if response:
        return response
    raise HTTPException(404, f"There is no Product item with this title: {productName}")

@app.get("/api/products/image/{productID}")
async def get_product_img(productID: str):
    image_data, image_extension = await fetch_product_image(productID)
    if image_data:
        return StreamingResponse(io.BytesIO(image_data), media_type=f"image/{image_extension}")
    raise HTTPException(404, f"There is no Product with this ID: {productID}")

@app.post("/api/products")
async def post_product(
    productName: str = Form(...),
    productID: str = Form(...),
    photographer: str = Form(...),
    productEvent: str = Form(...),
    productTags: str = Form(...),
    productPrice: float = Form(...),
    productCurrency: str = Form(...),
    productDate: str = Form(...),
    productImage: UploadFile = File(...)
):
    productTagsList = [tag.strip() for tag in productTags.split(',')]
    productDateObj = datetime.datetime.strptime(productDate, "%Y-%m-%d").date()

    image_data = await productImage.read()
    img = Image.open(io.BytesIO(image_data))
    original_format = img.format
    

    productImageExtension = original_format.lower()
    binary_image_data = Binary(image_data)

    product = Products(
        productName=productName,
        productID=productID,
        photographer=photographer,
        productEvent=productEvent,
        productTags=productTagsList,
        productPrice=productPrice,
        productCurrency=productCurrency,
        productDate=productDateObj,
        productImage=binary_image_data,
        productImageExtension=productImageExtension
    )
    response = await create_product(product.dict())
    if response:
        return response
    raise HTTPException(400, "Something went wrong / Bad Request")

@app.put("/api/products{productName}/", response_model=Products)
async def put_product(productName:str, productID:str, photographer:str, productEvent:str, productTags:List[str], productPrice:float,
                    productCurrency: str, productDate: datetime.date, productImage: bytes):
        response = await update_product(productName, productID, photographer, productEvent, productTags,
                                      productPrice, productCurrency, productDate, productImage)
        if response:
            return response
        raise HTTPException(400, "Something went wrong / Bad Request")

@app.delete("/api/products{productName}")
async def delete_product(productName):
    response = await remove_product(productName)
    if response:
        return "Successfully deleted Product Item"
    raise HTTPException(400, "Something went wrong / Bad Request")

database.py

async def fetch_one_product(name):
    document = await database.products.find_one({"productName":name})
    return document

async def fetch_all_products():
    productsL = []
    cursor = database.products.find({})
    async for document in cursor:
        productsL.append(Products(**document))
    return productsL

async def create_product(product: dict):
    product['productDate'] = datetime.combine(product['productDate'], datetime.min.time())
    document = product
    result = await database.products.insert_one(document)
    document["_id"] = result.inserted_id

    product_instance = Products(**document)
    return product_instance.to_dict()

async def update_product(productName, productID, photographer, productEvent, productTags, productPrice, productCurrency, productDate, productImage):
    await database.products.update_one({"name":productName},{"$set":{"productName": productName, "productID": productID, "photographer": photographer,
                                                                   "productEvent": productEvent, "productTags": productTags, "productPrice": productPrice,
                                                                   "productCurrency": productCurrency, "productDate": productDate, "productImage": productImage}})
    document = await database.products.find_one({"name":productName})
    return document

async def remove_product(name):
    await database.products.delete_one({"name":name})
    return True

async def fetch_one_product_byID(productID):
    document = await database.products.find_one({"productID": productID})
    return document

async def fetch_product_image(productID: str):
    product = await database.products.find_one({"productID": productID}, {"productImage": 1, "productImageExtension": 1})
    if product:
        image_data = bytes(product["productImage"])
        image_extension = product["productImageExtension"]
        return image_data, image_extension
    return None, None

I am trying to display the productImage using a ProductImage React component, which is as follows:

import React, { useState, useEffect } from "react";
import axios from "axios";

const ProductImage = ({ productID, size = "normal" }) => {
  const [image, setImage] = useState(null);

  let imageSize = "250px";

  if(size === "small"){
    imageSize = "20px";
  }

  useEffect(() => {
    const fetchImage = async () => {
      try {
        const response = await axios.get(`http://localhost:8000/api/products/image/${productID}`, {
          responseType: "blob",
        });
        setImage(URL.createObjectURL(response.data));
      } catch (error) {
        console.error(error);
      }
    };

    fetchImage();
  }, [productID]);

  return <img src={image} alt={productID} style={{borderRadius: "50%", width: imageSize, height: imageSize}}/>;
};

export default ProductImage;

It seems to return errors relating to the image (I think), here's an error which appears when I try to access the page where the images should be rendering:

 File "pydantic\json.py", line 45, in pydantic.json.lambda
UnicodeDecodeError: 'utf-8' codec can't decode byte 0x89 in position 0: invalid start byte

I just can't seem to pinpoint where the issue is, but I'm fairly sure it is to do with the image and maybe how its being stored or processed. I know it's not the best practice to store images like this, but it's just a personal project to get an idea. I have the code working for displaying user images, here's a short sample of how that looks:

Backend:

@app.post("/api/User/image")
async def upload_user_img(email: str = Form(...), image: UploadFile = File(...)):
    res = await user_image_upload(email, image)
    return res

@app.get("/api/User/image/{email}")
async def get_user_img(email: str):
    image_data = await fetch_user_image(email)
    if image_data:
        return StreamingResponse(io.BytesIO(image_data), media_type="image/png")
    raise HTTPException(404, f"There is no User with this email: {email}")

async def user_image_upload(email: str, image: UploadFile = File(...)):
    user_email = email
    user = database.User.find_one({"email": user_email})
    if user:
        image_data = await image.read()
        binary_image_data = Binary(image_data)
        database.User.update_one({"email": user_email}, {"$set": {"image": binary_image_data}})
        return {"message": "Image uploaded successfully"}
    else:
        return {"message": "User not found"}
    
async def fetch_user_image(email):
    user = await database.User.find_one({"email": email}, {"image": 1})
    return user["image"]

Frontend:

import React, { useState, useEffect } from "react";
import axios from "axios";

const UserImage = ({ email, size = "normal" }) => {
  const [image, setImage] = useState(null);

  let imageSize = "250px";

  if(size === "small"){
    imageSize = "20px";
  }

  useEffect(() => {
    const fetchImage = async () => {
      try {
        const response = await axios.get(`http://localhost:8000/api/User/image/${email}`, {
          responseType: "blob",
        });
        setImage(URL.createObjectURL(response.data));
      } catch (error) {
        console.error(error);
      }
    };

    fetchImage();
  }, [email]);

  return <img src={image} alt={email} style={{borderRadius: "50%", width: imageSize, height: imageSize}}/>;
};

export default UserImage;

Data model for User:

class User(BaseModel):
    _id: str
    name: Optional[str] = None
    email: str
    phone: Optional[str] = None
    password: str
    active: Optional[bool] = None
    image: Optional[bytes] = None
    dob: Optional[datetime.datetime] = None
    weight: Optional[float] = None
    club: Optional[str] = None
    category: Optional[str] = None
    location: Optional[str] = None
    bank_details: Optional[bankDetails] = None
    isAdmin: Optional[bool] = None
    type: Optional[str] = None

It's worth noting that I call this UserImage react component from various places and it works well, and calling the ProductImage component is done in the same way, so I'm not sure what is going on.

Apologies for the long question, any advice helpful!

答案1

得分: 1

以下是翻译好的部分:

首先,我通过将后端函数fetch_all_products()中的productImage转换为Base64编码的字符串来进行了一些更改:

async def fetch_all_products():
    productsL = []
    cursor = database.products.find({})
    async for document in cursor:
        if document.get("productImage"):
            document["productImage"] = base64.b64encode(document["productImage"])
        productsL.append(Products(**document))
    return productsL

然后,我更新了ProductImage.js中的useEffect()以显示Base64编码的图像:

useEffect(() => {
  const fetchImage = async () => {
    try {
      const response = await axios.get(`http://localhost:8000/api/products/image/${productID}`);
      setImage(`data:image/${response.data.productImageExtension};base64,${response.data.productImage}`);
    } catch (error) {
      console.error(error);
    }
  };

  fetchImage();
}, [productID]);

接下来,我更改了get_product_img()路由以返回Base64编码的图像和扩展名:

@app.get("/api/products/image/{productID}")
async def get_product_img(productID: str):
    image_data, image_extension = await fetch_product_image(productID)
    if image_data:
        base64_encoded_image = base64.b64encode(image_data).decode("utf-8")
        return {"productImage": base64_encoded_image, "productImageExtension": image_extension}
    raise HTTPException(404, f"There is no Product with this ID: {productID}")

这允许我正确显示图像。我尝试上传更多图像以进行测试,并遇到另一个错误,但通过简单地更改了产品数据模式/模型,特别是def to_dict()函数,以使其能够正确处理二进制数据来解决了这个问题:

def to_dict(self):
    product_dict = self.dict(by_alias=True, exclude={'productImage'})
    if "_id" in product_dict:
        product_dict["_id"] = str(product_dict["_id"])
    if self.productImage:
        product_dict["productImage"] = base64.b64encode(self.productImage).decode('utf-8')
    return product_dict

最后,我将*post_product()*中的binary_image_data = Binary(image_data)行更改为binary_image_data = image_data。总的来说,这只是我在处理二进制数据时的疏忽。希望这个答案对某人有所帮助。

英文:

Ended up solving it with some changes as follows:

First I changed the backend function fetch_all_products() by converting productImage to a base64 encoded string as follows:

async def fetch_all_products():
    productsL = []
    cursor = database.products.find({})
    async for document in cursor:
        if document.get("productImage"):
            document["productImage"] = base64.b64encode(document["productImage"])
        productsL.append(Products(**document))
    return productsL

Then I updated the useEffect() in my ProductImage.js react component to display a base64 encoded image.

useEffect(() => {
  const fetchImage = async () => {
    try {
      const response = await axios.get(`http://localhost:8000/api/products/image/${productID}`);
      setImage(`data:image/${response.data.productImageExtension};base64,${response.data.productImage}`);
    } catch (error) {
      console.error(error);
    }
  };

  fetchImage();
}, [productID]);

Then I changed the get_product_img() route to return a base64 encoded image and the extension:

@app.get("/api/products/image/{productID}")
async def get_product_img(productID: str):
    image_data, image_extension = await fetch_product_image(productID)
    if image_data:
        base64_encoded_image = base64.b64encode(image_data).decode("utf-8")
        return {"productImage": base64_encoded_image, "productImageExtension": image_extension}
    raise HTTPException(404, f"There is no Product with this ID: {productID}")

And that allowed me to display images correctly. I tried uploading more images to test, and got another error, but solved it by simply altering the Products data schema/model, specifically the def to_dict() function, allowing it to properly handle the binary data:

def to_dict(self):
        product_dict = self.dict(by_alias=True, exclude={'productImage'})
        if "_id" in product_dict:
            product_dict["_id"] = str(product_dict["_id"])
        if self.productImage:
            product_dict["productImage"] = base64.b64encode(self.productImage).decode('utf-8')
        return product_dict

And finally, I changed the binary_image_data = Binary(image_data) line in post_product() to binary_image_data = image_data.

Overall, it was just my oversight on handling binary data. Hope the answer helps somebody.

huangapple
  • 本文由 发表于 2023年3月20日 23:33:33
  • 转载请务必保留本文链接:https://go.coder-hub.com/75792285.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定