I need to store PNG files as blob/binary in the database and then be able to retrieve and show them.
Here's my model that has a binary field to store the image:
class ImageFile(models.Model):
file = models.BinaryField(editable=True)
I created a widget based on this answer:
class BinaryFileInputWidget(forms.ClearableFileInput):
def is_initial(self, value):
return bool(value)
def format_value(self, value):
if self.is_initial(value):
return f"{len(value)} bytes"
def value_from_datadict(self, data, files, name):
upload = super().value_from_datadict(data, files, name)
if upload:
return upload.read()
And I used it in admin.py
like this:
@admin.register(ImageFile)
class ImageFileAdmin(admin.ModelAdmin):
list_display = ["id"]
formfield_overrides = {
models.BinaryField: {"widget": BinaryFileInputWidget()},
}
Then I encode the file as base64
in the view:
def image_view(request: HttpRequest, id: int):
document_file = ImageFile.objects.filter(id=id).first()
data = ""
if document_file:
data = base64.b64encode(document_file.file).decode(encoding="ascii")
return render(request, "image.html", {"data": data})
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Document</title>
</head>
<body>
<pre>data:image/png;base64,{{ data }}</pre>
<img src="data:image/png;base64,{{ data }}" alt="" />
</body>
</html>
There is no error and the data is shown in the pre
element but the img
element fails to load the image. I tried using some online converters to check if the data is valid but they show errors saying the base64 string is invalid.
It loads properly if I convert the file to base64
in the widget before storing it in the database (return base64.b64encode(upload.read()).decode('ascii')
instead of return upload.read()
), and then convert it again in the view. But I cant use this method because I might need to compress the files and apparently base64
encoded data can't be compressed properly.
Sample output using my profile picture:
<!DOCTYPE html>
<html lang="en">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Document</title>
</head>
<body cz-shortcut-listen="true">
<pre>
data:image/png;base64,bx89PNGrnx1anx00x00x00rIHDRx00x00x01x00x00x00x01x00x08x03x00x00x00kxacXTx00x00x03x00PLTEx19x19x1dxffxffxffxd4xd5xe1xd3xd4xe1xd2xd3xe0x1fx11x11x15x10x10x13x18x18x1cx13x14x18x15x16x1ax16x17x1bxd5xd6xe2x15x15x19xfexfexffxdcxddxe9x13x12x16xdexdfxebxddxdexebxfdxfcxfdxd7xd8xe4xfdxfdxfex0fx0fx12xdaxdbxe7x0ex0ex12xd2xd3xdfxf9xf9xfbx1d9xf3xf3xf7x1ax1ax1exdfxe0xe9xfaxfaxfcxf1xf0xf3xdcxdcxe8xd7xd7xe3xf6xf6xf9xe0xe1xebxfcxfbxfdx174x1a7xebxecxf1xe4xe5xedxf7xf7xf9xe6xe7xeexd9xdaxe6xeexefxf4xa4xa2xadxc7xc8xd3xe8xe9xefx9bx98xa7x1ex1dxd0xd1xddxf5xf5xf8xc1xc2xcdxb8xb6xbfxb3xb1xbcxe2xe3xecxf8xf8xfbx9fx9dxa877xeexedxefllrdcixf4xf4xf8xd8xd9xe6x92x92x9cvvzxf1xf1xf5xb9xb9xc4xecxedxf3xa5xa6xaeTTX//3x8ax85x97CCFxe1xe0xe3xd8xd9xe4xdexddxe2xc3xc1xc7xc5xc4xce+a559x8bx8bx92x91x8cx9dxeaxeaxefxccxccxd8xf5xf4xf5xbcxbcxc8x86x83x92x97x94xa3xc7xc5xca9/Lxe4xe3xe5xabxa9xb2xb1xadxb6x85x80x91rrvxbfxbfxca225xcdxcbxd0x9cx95xa2xc0xbbxc3xeaxe9xebxa8xa5xb1xcexcdxcfx9dx9ax9bNOSxdexdexe8x88x82x7fxd5xd3xd9x8dx88x99xe7xe6xe9xb7xb6xb6xa1xa0xa2GZx92x8fx8ex98x95x96hhkdx8fx8fx97x88x88x8bxcaxc9xccxb4xb4xbcxadx7fx83xd1x98x93xc2x90x93IY0ATvx82x90vpx83x7fyx8cxafxadxbdxaaxa8xa9x84x7fx80x83xf0zBEx82JKPxd4xd1xd3zyxabxacxb7xe3xe3xebxaexadxaexeexacxa4xe8xa6x9fZRgxbdx8ax8axa0x93GFJxb8x86x87xfcxebxe8vx88xd9x9fx97ODe7WKl4Fxd8xd6xdbxfdx80Dxf9xbaxbaxbcxa5xa2xa2xf9xe4xe2xe9xeaxf3xf6xbbxb3xffxf6xf4x95iqxefxc3xbexe1xa8x9fA6VH/x1ex141xcfxcfxd8A8Qx93xx8dxe5xb4xc9xffxd9xe9x84lx83xdbxdaxddxd8xd8xd8xaezxa2vxcax93x91xaawxb3x82x84WWx97mtxaaxa7xb6YmernAxdbrjbvx90x81x90xffxc3xa7xa9x86x9cxfbxd6xd1xf6xd3xcfxf4xcaxc5x18x19x1dxfcxf1xf0xd6xa6xa3x9fsyxa0x80ngz4NROPxa3Y8xf5x83Oxc1h+x89N3xaex80jxfbxa9x83x91x89x97xf8x97ixd4xb3xc6xbax9bxafxf1xc3xd7xfaxcdxdfx8dsx89xe9xb6xb1xddxc4xc5x87hxffxeexecxcexbfxc3xe8xdfxe1xf7xc9xc2xb8xa4xabkx81vG1xe3x93m7Hxe0x86Zwvxfexdaxc9xe4xc9xbexfdx9fsxffxe9xdbxffxe1xd1xcaxa6xbaxd5xd6xd9xcbx9dxb3xecxb9xcexa7xa3xa0xdfxdbxd4xe3x00x00rxIDATxx9cxedx9dux1bxd9x16xc7xb93Cx1ex02x01x12x10x08txeeZx9cxb6Hxb4PJxa9Axa1xb4xddnuwxddxbaxf7uxebxdfxb7xebxbaxcbwxeexfbxe4x13DfRxb6xa4x8fx19xf6xffxe1xb3wfxfbxb9xbfx93sxcc=</pre
>
<img
src="data:image/png;base64,bx89PNGrnx1anx00x00x00rIHDRx00x00x01x00x00x00x01x00x08x03x00x00x00kxacXTx00x00x03x00PLTEx19x19x1dxffxffxffxd4xd5xe1xd3xd4xe1xd2xd3xe0x1fx11x11x15x10x10x13x18x18x1cx13x14x18x15x16x1ax16x17x1bxd5xd6xe2x15x15x19xfexfexffxdcxddxe9x13x12x16xdexdfxebxddxdexebxfdxfcxfdxd7xd8xe4xfdxfdxfex0fx0fx12xdaxdbxe7x0ex0ex12xd2xd3xdfxf9xf9xfbx1d9xf3xf3xf7x1ax1ax1exdfxe0xe9xfaxfaxfcxf1xf0xf3xdcxdcxe8xd7xd7xe3xf6xf6xf9xe0xe1xebxfcxfbxfdx174x1a7xebxecxf1xe4xe5xedxf7xf7xf9xe6xe7xeexd9xdaxe6xeexefxf4xa4xa2xadxc7xc8xd3xe8xe9xefx9bx98xa7x1ex1dxd0xd1xddxf5xf5xf8xc1xc2xcdxb8xb6xbfxb3xb1xbcxe2xe3xecxf8xf8xfbx9fx9dxa877xeexedxefllrdcixf4xf4xf8xd8xd9xe6x92x92x9cvvzxf1xf1xf5xb9xb9xc4xecxedxf3xa5xa6xaeTTX//3x8ax85x97CCFxe1xe0xe3xd8xd9xe4xdexddxe2xc3xc1xc7xc5xc4xce+a559x8bx8bx92x91x8cx9dxeaxeaxefxccxccxd8xf5xf4xf5xbcxbcxc8x86x83x92x97x94xa3xc7xc5xca9/Lxe4xe3xe5xabxa9xb2xb1xadxb6x85x80x91rrvxbfxbfxca225xcdxcbxd0x9cx95xa2xc0xbbxc3xeaxe9xebxa8xa5xb1xcexcdxcfx9dx9ax9bNOSxdexdexe8x88x82x7fxd5xd3xd9x8dx88x99xe7xe6xe9xb7xb6xb6xa1xa0xa2GZx92x8fx8ex98x95x96hhkdx8fx8fx97x88x88x8bxcaxc9xccxb4xb4xbcxadx7fx83xd1x98x93xc2x90x93IY0ATvx82x90vpx83x7fyx8cxafxadxbdxaaxa8xa9x84x7fx80x83xf0zBEx82JKPxd4xd1xd3zyxabxacxb7xe3xe3xebxaexadxaexeexacxa4xe8xa6x9fZRgxbdx8ax8axa0x93GFJxb8x86x87xfcxebxe8vx88xd9x9fx97ODe7WKl4Fxd8xd6xdbxfdx80Dxf9xbaxbaxbcxa5xa2xa2xf9xe4xe2xe9xeaxf3xf6xbbxb3xffxf6xf4x95iqxefxc3xbexe1xa8x9fA6VH/x1ex141xcfxcfxd8A8Qx93xx8dxe5xb4xc9xffxd9xe9x84lx83xdbxdaxddxd8xd8xd8xaezxa2vxcax93x91xaawxb3x82x84WWx97mtxaaxa7xb6YmernAxdbrjbvx90x81x90xffxc3xa7xa9x86x9cxfbxd6xd1xf6xd3xcfxf4xcaxc5x18x19x1dxfcxf1xf0xd6xa6xa3x9fsyxa0x80ngz4NROPxa3Y8xf5x83Oxc1h+x89N3xaex80jxfbxa9x83x91x89x97xf8x97ixd4xb3xc6xbax9bxafxf1xc3xd7xfaxcdxdfx8dsx89xe9xb6xb1xddxc4xc5x87hxffxeexecxcexbfxc3xe8xdfxe1xf7xc9xc2xb8xa4xabkx81vG1xe3x93m7Hxe0x86Zwvxfexdaxc9xe4xc9xbexfdx9fsxffxe9xdbxffxe1xd1xcaxa6xbaxd5xd6xd9xcbx9dxb3xecxb9xcexa7xa3xa0xdfxdbxd4xe3x00x00rxIDATxx9cxedx9dux1bxd9x16xc7xb93Cx1ex02x01x12x10x08txeeZx9cxb6Hxb4PJxa9Axa1xb4xddnuwxddxbaxf7uxebxdfxb7xebxbaxcbwxeexfbxe4x13DfRxb6xa4x8fx19xf6xffxe1xb3wfxfbxb9xbfx93sxcc="
alt=""
/>
</body>
</html>
I ended up creating a custom form (instead of widget) that reads the file from a FileField
and then saves it in the model.
forms.py:
class ImageFileForm(forms.ModelForm):
file_upload = forms.FileField()
def save(self, commit=True, *args, **kwargs):
instance = super(ImageFileForm, self).save(commit=False)
file: InMemoryUploadedFile | None = self.cleaned_data.get("file_upload")
if file:
with file.open() as f:
instance.file = f.read()
if commit:
instance.save()
return instance
class Meta:
model = ImageFile
fields = ["file_upload"]
admin.py:
@admin.register(ImageFile)
class ImageFileAdmin(admin.ModelAdmin):
list_display = ["id"]
form = DocumentFileForm