How to write PNG image to string with the PIL?
You can use the BytesIO
class to get a wrapper around strings that behaves like a file. The BytesIO
object provides the same interface as a file, but saves the contents just in memory:
import io
with io.BytesIO() as output:
image.save(output, format="GIF")
contents = output.getvalue()
You have to explicitly specify the output format with the format
parameter, otherwise PIL will raise an error when trying to automatically detect it.
If you loaded the image from a file it has a format
property that contains the original file format, so in this case you can use format=image.format
.
In old Python 2 versions before introduction of the io
module you would have used the StringIO
module instead.
Weird interaction with Python PIL image.save quality parameter
Quick takeaways from the following explanations...
- The
quality
parameter forPIL.Image.save
isn't used when saving PNGs. - JPEG is generationally-lossy so as you keep re-saving images, they will likely degrade in quality because the algorithm will introduce more artifacting (among other things)
- PNG is lossless and the file size differences you're seeing are due to
PIL
stripping metadata when you re-save your image.
Let's look at your PNG file first. PNG is a lossless format - the image data you give it will not suffer generational loss if you were to open it and re-save it as PNG over and over again.
The quality
parameter isn't even recognized by the PNG plugin to PIL - if you look at the PngImagePlugin.py/PngStream._save
method it is never referenced in there.
What's happening with your specific sample image is that Pillow is dropping some metadata when you re-save it in your code.
On my test system, I have your PNG saved as sample.png
, and I did a simple load-and-save with the following code and save it as output.png
(inside ipython
)
In [1]: from PIL import Image
In [2]: img = Image.open("sample.png")
In [3]: img.save("output.png")
Now let's look at the differences between their metadata with ImageMagick:
#> diff <(magick identify -verbose output.png) <(magick identify -verbose sample.png)
7c7,9
< Units: Undefined
---
> Resolution: 94.48x94.48
> Print size: 10.8383x10.8383
> Units: PixelsPerCentimeter
74c76,78
< Orientation: Undefined
---
> Orientation: TopLeft
> Profiles:
> Profile-exif: 5218 bytes
76,77c80,81
< date:create: 2022-08-12T21:27:13+00:00
< date:modify: 2022-08-12T21:27:13+00:00
---
> date:create: 2022-08-12T21:23:42+00:00
> date:modify: 2022-08-12T21:23:31+00:00
78a83,85
> exif:ImageDescription: IMGP5493_seamless_2.jpg
> exif:ImageLength: 1024
> exif:ImageWidth: 1024
84a92
> png:pHYs: x_res=9448, y_res=9448, units=1
85a94,95
> png:text: 1 tEXt/zTXt/iTXt chunks were found
> png:text-encoded profiles: 1 were found
86a97
> unknown: nomacs - Image Lounge 3.14
90c101
< Filesize: 933730B
---
> Filesize: 939469B
93c104
< Pixels per second: 42.9936MP
---
> Pixels per second: 43.7861MP
You can see there are metadata differences - PIL didn't retain some of the information when re-saving the image, especially some exif
properties (you can see this PNG was actually converted from a JPG and the EXIF metadata was preserved in the conversion).
However, if you re-save the image with original image's info
data...
In [1]: from PIL import Image
In [2]: img = Image.open("sample.png")
In [3]: img.save("output-with-info.png", info=img.info)
You'll see that the two files are exactly the same again:
❯ sha256sum output.png output-with-info.png
37ad78a7b7000c9430f40d63aa2f0afd2b59ffeeb93285b12bbba9c7c3dec4a2 output.png
37ad78a7b7000c9430f40d63aa2f0afd2b59ffeeb93285b12bbba9c7c3dec4a2 output-with-info.png
Maybe Reducing PNG File Size
While lossless, the PNG format does allow for reducing the size of the image by specifying how aggressive the compression is (there are also more advanced things you could do like specifying a compression dictionary).
PIL exposes these options as optimize
and compress_level
under PNG options.
optimize
If present and true, instructs the PNG writer to make the
output file as small as possible. This includes extra
processing in order to find optimal encoder settings.
compress_level
ZLIB compression level, a number between 0 and 9: 1 gives
best speed, 9 gives best compression, 0 gives no
compression at all. Default is 6. When optimize option is
True compress_level has no effect (it is set to 9 regardless
of a value passed).
And seeing it in action...
from PIL import Image
img = Image.open("sample.png")
img.save("optimized.png", optimize=True)
The resulting image I get is about 60K smaller than the original.
❯ ls -lh optimized.png sample.png
-rw-r--r-- 1 wkl staff 843K Aug 12 18:10 optimized.png
-rw-r--r-- 1 wkl staff 918K Aug 12 17:23 sample.png
JPEG File
Now, JPEG is a generationally-lossy image format - as you save it over and over, you will keep losing quality - it doesn't matter if your subsequent generations save it at even higher qualities than the previous ones, you've lost data already from the previous saves.
Note that the likely reason why you saw file sizes balloon if you used quality=100
is because libjpeg
/libjpeg-turbo
(which are the underlying libraries used by PIL for JPEG) do not do certain things when the quality is set that high, I think it doesn't do quantization which is an important step in determining how many bits are needed to compress.
Python: How to turn an IMAGE into a STRING and back?
You can convert to a string like this:
import base64
with open("image.png", "rb") as image:
b64string = base64.b64encode(image.read())
That should give you the same results as if you run this in Terminal:
base64 < image.png
And you can convert that string back to a PIL Image like this:
from PIL import Image
import io
f = io.BytesIO(base64.b64decode(b64string))
pilimage = Image.open(f)
That should be equivalent to the following in Terminal:
base64 -D < "STRING" > recoveredimage.png
Note that if you are sending this over LoRa, you are better off sending the PNG-encoded version of the file like I am here as it is compressed and will take less time. You could, alternatively, send the expanded out in-memory version of the file but that would be nearly 50% larger. The PNG file is 13kB. The expanded out in-memory version will be 100*60*3, or 18kB.
How do I save custom information to a PNG Image file in Python?
You can store metadata in Pillow using PngImagePlugin.PngInfo
like this:
from PIL import Image
from PIL.PngImagePlugin import PngInfo
targetImage = Image.open("pathToImage.png")
metadata = PngInfo()
metadata.add_text("MyNewString", "A string")
metadata.add_text("MyNewInt", str(1234))
targetImage.save("NewPath.png", pnginfo=metadata)
targetImage = Image.open("NewPath.png")
print(targetImage.text)
>>> {'MyNewString': 'A string', 'MyNewInt': '1234'}
In this example I use tEXt, but you can also save it as iTXt using add_itxt
.
Python PIL reading PNG from STDIN
If the png
variable contains the binary data from a PNG file, you can't read it using frombuffer
; that's used for reading raw pixel data. Instead, use io.StringIO
and Image.open
, i.e.:
import io
from PIL import Image
img = Image.open(io.StringIO(png))
Draw circle with PIL (Old ones doesn't works)
You can use either ImageDraw.arc()
or ImageDraw.ellipse
.
from PIL import Image, ImageDraw
# Image size
W, H = 100, 100
# Bounding box points
X0 = int(W / 4)
X1 = int(X0 * 3)
Y0 = int(H / 4)
Y1 = int(X0 * 3)
# Bounding box
bbox = [X0, Y0, X1, Y1]
# Set up
im = Image.new("RGB", (W, H))
draw = ImageDraw.Draw(im)
# Draw a circle
draw.arc(bbox, 0, 360)
# Show the image
im.show()
Or:
# Draw a circle
draw.ellipse(bbox)
Related Topics
Not Letting the Character Move Out of the Window
How to Print to Stderr in Python
Traverse a List in Reverse Order in Python
How to Check If Type of a Variable Is String
Selecting a Row of Pandas Series/Dataframe by Integer Index
How to Put Individual Tags for a Matplotlib Scatter Plot
Why Do I Need 'B' to Encode a String with Base64
Differencebetween Drawing Plots Using Plot, Axes or Figure in Matplotlib
How to Get Rid of "Unnamed: 0" Column in a Pandas Dataframe Read in from CSV File
How to Remove Nan Values from a Numpy Array
How to See If There's an Available and Active Network Connection in Python
Read File from Line 2 or Skip Header Row
Pandas Dataframe Stored List as String: How to Convert Back to List
How to Create a Set of Sets in Python
Generating an Md5 Checksum of a File
How to Transform an Xml File Using Xslt in Python