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!