英文:
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.
通过集体智慧和协作来改善编程学习和解决问题的方式。致力于成为全球开发者共同参与的知识库,让每个人都能够通过互相帮助和分享经验来进步。
评论