1

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!

sf001
  • 46
  • 4
  • 1
    Please have a look at related answers [here](https://stackoverflow.com/a/71324775/17865804), as well as [here](https://stackoverflow.com/a/73264904/17865804), [here](https://stackoverflow.com/a/73754985/17865804) and [here](https://stackoverflow.com/a/71643439/17865804) – Chris Mar 20 '23 at 16:32
  • @Chris thanks for the links, appreciate it – sf001 Mar 20 '23 at 16:35

1 Answers1

1

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.

sf001
  • 46
  • 4