Repository navigation
Can't save all extensions to BytesIO #9428
Description
Activity
In your error trace, you're showing
im.save(out, format=ext)andKeyError: '.APNG'. Theformatargument fromsave()is not intended to take an extension, but a format. In this case, it is "PNG"If I adjust your code to
im.save(out, "PNG"), it works. Pillow will automatically give you an APNG image if there is more than one frame.Why don't the plugins use Pillow to convert the mode themselves?
You may be interested in #2663, an open issue with a suggestion for how to make this easier.
I tried "APNG" before, which failed. As Pillow usually parses the extension anyway, I figured it might work as the format argument as well. Alas, even after providing file names, it appears that Pillow needs a real file:
from PIL import Image from pyfakefs.fake_filesystem_unittest import patchfs im = Image.open("eye.gif") def set_supported_files(): """Set supported files""" global SUPPORTED_EXTENSIONS, SUPPORTED_FILES_READ, SUPPORTED_FILES_WRITE exts = Image.registered_extensions() exts[".eml"] = "MHTML" exts[".mht"] = "MHTML" exts[".mhtml"] = "MHTML" exts[".svg"] = "SVG" exts[".svgz"] = "SVG" exts[".tar"] = "TAR" exts[".tar.bz2"] = "TAR" exts[".tbz2"] = "TAR" exts[".tar.gz"] = "TAR" exts[".tgz"] = "TAR" exts[".tar.xz"] = "TAR" exts[".txz"] = "TAR" exts[".tar.zst"] = "TAR" exts[".tar.zstd"] = "TAR" exts[".zip"] = "ZIP" added_exts = [ "EML", "MHT", "MHTML", "SVG", "SVGZ", "TAR", "TAR.BZ2", "TAR.GZ", "TAR.XZ", "TAR.ZST", "TAR.ZSTD", "TBZ2", "TGZ", "TXZ", "ZIP", ] SUPPORTED_EXTENSIONS = sorted( [k[1:].upper() for k, v in exts.items() if v in Image.OPEN] + added_exts ) type_exts = {} for k, v in exts.items(): type_exts.setdefault(v, []).append(k) SUPPORTED_FILES_READ = [ ( "All supported files", " ".join( sorted(list(k for k, v in exts.items() if v in Image.OPEN) + added_exts) ), ), ("All files", "*"), ( "Archives", ".eml .mht .mhtml .tar .tar.bz2 .tbz2 .tar.gz .tgz .tar.xz .txz .tar.zst .tar.zstd .zip", ), *sorted( (k, v) for k, v in type_exts.items() if k in Image.OPEN or k in added_exts ), ] SUPPORTED_FILES_WRITE = [ ( "All supported files", " ".join(sorted(list(k for k, v in exts.items() if v in Image.SAVE))), ), *sorted((k, v) for k, v in type_exts.items() if k in Image.SAVE), ] print( f"Reads {len(SUPPORTED_EXTENSIONS)} formats:\n{", ".join(SUPPORTED_EXTENSIONS)}" ) saves = sorted(k[1:].upper() for k, v in exts.items() if v in Image.SAVE) print( f"Writes {len(saves)} formats:\n{", ".join(saves)}", ) print( f"Saves all frames:\n{", ".join( sorted(k[1:].upper() for k, v in exts.items() if v in Image.SAVE_ALL) )}" ) @patchfs def test_save(fake_fs): """ >>> test_save() """ set_supported_files() print(Image.SAVE) exts = ("APNG", "BW") list(v for v in exts if v in Image.SAVE) for ext in SUPPORTED_FILES_WRITE[0][1].split(" "): fname = f"/foo/bar{ext}" print(f"Testing {fname}") fake_fs.create_file(fname) im.save(fname) print("Done.") if __name__ == "__main__": test_save() (venv) 1:26:10 d:\code\image viewer\tk_image_viewer>python test.py Reads 82 formats: APNG, AVIF, AVIFS, BLP, BMP, BUFR, BW, CUR, DCX, DDS, DIB, EMF, EML, EPS, FIT, FITS, FLC, FLI, FTC, FTU, GBR, GIF, GRIB, H5, HDF, ICB, ICNS, ICO, IIM, IM, J2C, J2K, JFIF, JP2, JPC, JPE, JPEG, JPF, JPG, JPX, MHT, MHTML, MPEG, MPG, MSP, PBM, PCD, PCX, PFM, PGM, PNG, PNM, PPM, PS, PSD, PXR, QOI, RAS, RGB, RGBA, SGI, SVG, SVGZ, TAR, TAR.BZ2, TAR.GZ, TAR.XZ, TAR.ZST, TAR.ZSTD, TBZ2, TGA, TGZ, TIF, TIFF, TXZ, VDA, VST, WEBP, WMF, XBM, XPM, ZIP Writes 53 formats: APNG, AVIF, AVIFS, BLP, BMP, BUFR, BW, DDS, DIB, EMF, EPS, GIF, GRIB, H5, HDF, ICB, ICNS, ICO, IM, J2C, J2K, JFIF, JP2, JPC, JPE, JPEG, JPF, JPG, JPX, MPO, MSP, PALM, PBM, PCX, PDF, PFM, PGM, PNG, PNM, PPM, PS, QOI, RGB, RGBA, SGI, TGA, TIF, TIFF, VDA, VST, WEBP, WMF, XBM Saves all frames: APNG, AVIF, AVIFS, GIF, MPO, PDF, PNG, TIF, TIFF, WEBP {'BMP': <function _save at 0x00000174B736E340>, 'DIB': <function _dib_save at 0x00000174B736DEE0>, 'GIF': <function _save at 0x00000174B7397B00>, 'JPEG': <function _save at 0x00000174B73A51C0>, 'PPM': <function _save at 0x00000174B73A56C0>, 'PNG': <function _save at 0x00000174B73F71A0>, 'AVIF': <function _save at 0x00000174B7424360>, 'BLP': <function _save at 0x00000174B7424AE0>, 'BUFR': <function _save at 0x00000174B7425760>, 'PCX': <function _save at 0x00000174B74262A0>, 'DDS': <function _save at 0x00000174B74267A0>, 'EPS': <function _save at 0x00000174B7426FC0>, 'GRIB': <function _save at 0x00000174B7D16660>, 'HDF5': <function _save at 0x00000174B7D16B60>, 'JPEG2000': <function _save at 0x00000174B7D34900>, 'ICNS': <function _save at 0x00000174B7D34B80>, 'ICO': <function _save at 0x00000174B7D35260>, 'IM': <function _save at 0x00000174B7D362A0>, 'TIFF': <function _save at 0x00000174B7D61EE0>, 'MPO': <function _save at 0x00000174B7D37380>, 'MSP': <function _save at 0x00000174B7D8E660>, 'PALM': <function _save at 0x00000174B7D8EAC0>, 'PDF': <function _save at 0x00000174B7E00360>, 'QOI': <function _save at 0x00000174B7E022A0>, 'SGI': <function _save at 0x00000174B7E02A20>, 'SPIDER': <function _save_spider at 0x00000174B7E03880>, 'TGA': <function _save at 0x00000174B7E03BA0>, 'WEBP': <function _save at 0x00000174B7E10A40>, 'WMF': <function _save at 0x00000174B7E10FE0>, 'XBM': <function _save at 0x00000174B7E11580>} Testing /foo/bar.apng Testing /foo/bar.avif Testing /foo/bar.avifs Testing /foo/bar.blp Testing /foo/bar.bmp Traceback (most recent call last): File "d:\code\image viewer\tk_image_viewer\test.py", line 106, in <module> test_save() ~~~~~~~~~^^ File "D:\code\image viewer\tk_image_viewer\venv\Lib\site-packages\pyfakefs\fake_filesystem_unittest.py", line 230, in wrapped return f(*args, **kwargs) File "d:\code\image viewer\tk_image_viewer\test.py", line 101, in test_save im.save(fname) ~~~~~~~^^^^^^^ File "D:\code\image viewer\tk_image_viewer\venv\Lib\site-packages\PIL\Image.py", line 2590, in save save_handler(self, fp, filename) ~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^ File "D:\code\image viewer\tk_image_viewer\venv\Lib\site-packages\PIL\BmpImagePlugin.py", line 493, in _save ImageFile._save( ~~~~~~~~~~~~~~~^ im, fp, [ImageFile._Tile("raw", (0, 0) + im.size, 0, (rawmode, stride, -1))] ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ ) ^ File "D:\code\image viewer\tk_image_viewer\venv\Lib\site-packages\PIL\ImageFile.py", line 662, in _save _encode_tile(im, fp, tile, bufsize, fh) ~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^ File "D:\code\image viewer\tk_image_viewer\venv\Lib\site-packages\PIL\ImageFile.py", line 697, in _encode_tile errcode = encoder.encode_to_file(fh, bufsize) OSError: [Errno 9] Bad file descriptor
- changed the title
[-]Can't save APNG to BytesIO[/-][+]Can't save all extensions to BytesIO[/+]on Feb 11, 2026 To be clear,
from io import BytesIO from PIL import Image im = Image.open("eye.gif") out = BytesIO() im.save(out, "PNG")
works with Pillow 12.1.0 and Python 3.13.
Your extended code example is shows bar.apng working successfully. So now you're asking about bar.bmp. Testing, I find this is no longer a problem when I remove the use of pyfakefs.
If pyfakefs isn't working where saving to a normal path does, that sounds like an issue to raise with https://github.com/pytest-dev/pyfakefs
Then why did it work on the extensions before bmp? Anyway, I just want to test all extensions without writing to disk.
This would be my suggestion for saving files in all formats.
from io import BytesIO from PIL import Image Image.init() for format in Image.SAVE: for mode in ("L", "P"): out = BytesIO() im = Image.new(mode, (1, 1)) try: im.save(out, format) except OSError as e: if str(e).endswith(" handler not installed"): print(format+": Handler not installed") break except ValueError: continue print(format+": Success") break else: print(format+": Unable to save in any mode that was tried")
If you do explicitly want to use an extension without saving to disk, this would be my suggestion.
from io import BytesIO from PIL import Image Image.init() for ext in Image.EXTENSION: if not Image.EXTENSION[ext] in Image.SAVE: continue for mode in ("L", "P", "RGB"): out = BytesIO() out.name = "out"+ext im = Image.new(mode, (1, 1)) try: im.save(out) except OSError as e: if str(e).endswith(" handler not installed"): print(ext+": Handler not installed") break except ValueError: continue print(ext+": Success") break else: print(ext+": Unable to save in any mode that was tried")
Reacted by Cees Timmermanout.name = "out"+ext
I googled that but could not find it used or documented (and I disabled member warnings for my global hacks). Thanks!
Why then the format parameter, and why does that not accept "BW" for example?
The
formatparameter has been on thesave()method since we forked from PIL, so I can't give you any kind of official explanation.My guess would be that a file extension was thought of as just as an an alias for the image format - who cares if it is
.tifor.tiff? If there's no filename, the exact extension is usually meaningless, and what really matters is what type of image it is, so let's have one single identifier for that, theformat. It's perhaps also worth noting that the SPIDER image format doesn't have any extensions, so we couldn't rely purely on extensions.".bw" is a file extension for the "SGI" format. We could make the
formatparameter more flexible, to also accept extensions, but without a concrete need for that functionality, the additional possibility seems confusing, rather than clarifying, for new users. There's already a clear mapping from file extensions to formats in the form ofImage.EXTENSION, and I can't recall this ever previously being raised.I don't think <24-bit images are as popular as they once were, and even with the BytesIO instance
out.name = "bla.bw",im.save(out)fails withValueError: Unsupported SGI image mode, so if the SGI format covers the BW extension, it should autoconvert to mode L.
It accepts and writes mode RGB but SGI extension ".bw" should be mode L. And SGI extension ".rgba" should be RGBA.I think you're making two requests.
- If a user tries to save an image with an unsupported mode, it should be converted.
Not all image conversion is lossless, and losing data without notifying the user doesn't sound ideal to me. We do so for GIF at least, but I'm not sure it should be universal. Feel free to see if others disagree with me. As I said, I think it worth looking at #2663, an open issue with a suggestion for how to make this easier.
- You'd like us to automatically convert the image based on the file extension.
We do use the filename extension for the image type when saving JPEG2000 images (not the mode), but that's the only extension-based plugin-specific data handling that I'm aware of.
I think it's a reasonably fundamental concept of computing that users can name their files whatever they like. I just opened GIMP and exported a JPEG with a '.png' extension. Granted, it made me confirm what I was doing, but it still allowed me to do so in the end.
It also seems a little odd given that you just went through effort to be able to save images without a filename. Earlier in this issue you tried to save a single frame image as an APNG. Conceptually, I'm not sold that images meaningfully have, or need, an extension if they don't have a filename.
I've upvoted #2663 but as the issue is from 2017, it might take another year or 5 to get merged, so I've already written my own conversion code that doesn't bother the user who should know that ".bw" is for black and white files (probably why the user typed it in the first place). Magic bytes require reading the file, and MIME types require a confusing and ugly separate metadata channel like MacOS's __MACOSX folder.
As the issue with testing without burdening the disk is resolved by using the undocumented name property of a BytesIO object, this issue can remain as a bug for putting RGB in a BW and similar unexpected situations.
the user who should know that ".bw" is for black and white files
Magic bytes require reading the file
While it would nice if all file extensions were correct, I think Pillow acknowledges that this may not be the case. https://pillow.readthedocs.io/en/stable/handbook/image-file-formats.html#image-file-formats
The open() function identifies files from their contents, not their names, but the save() method looks at the name to determine which format to use, unless the format is given explicitly.
We allow a JPEG to be saved with a '.png' extension.
>>> from PIL import Image >>> im = Image.new("1", (1, 1)) >>> im.save("out.png", "JPEG")
Fine as you're reading the file anyway, but
im.save("out.sgi", "BW")doesn't work.I think this has already been discussed.
why does that not accept "BW" for example?
".bw" is a file extension for the "SGI" format. We could make the
formatparameter more flexible, to also accept extensions, but without a concrete need for that functionality, the additional possibility seems confusing, rather than clarifying, for new users. There's already a clear mapping from file extensions to formats in the form ofImage.EXTENSION, and I can't recall this ever previously being raised.If you disagree with me, I can just leave this issue open for a while for others to chime in, or you can create a PR to make your proposal more concrete.
What did you do?
Try to automate testing all supported save formats.
What did you expect to happen?
Use the special needs of the specific extensions like BW of SGI only accepting mode L. (Why don't the plugins use Pillow to convert the mode themselves?)
What actually happened?
What are your OS, Python and Pillow versions?