How to save an .EPS file to PNG with transparency in Python - python

I'm building a Paint-like app Since I want the freedom to reposition and modify the shape properties later, I am using Tkinter to draw shapes on Canvas instead of PIL Draw or anything else. From other answers, I found how to save a canvas as PNG by 1st creating a postscript file and then converting it to PNG using PIL.
Now the problem is the EPS file has transparent spaces but the PNG file fills those voids with a White background color. I'm not sure where I am going wrong.
Below is the function I used.
def saveImg(event):
global canvas
canvas.postscript(file="my_drawing.eps", colormode='color')
imgNew = Image.open("my_drawing.eps")
imgNew.convert("RGBA")
imgNew.thumbnail((2000,2000), Image.ANTIALIAS)
imgNew.save('testImg.png', quality=90)

Looks like transparency is not supported. From the docs:
The EPS driver can read EPS images in L, LAB, RGB and CMYK mode, but Ghostscript may convert the images to RGB mode rather than leaving them in the original color space.
When you load in RGB (instead of RGBA) the alpha channel information is discarded and converting it to RGBA later will not recover it.
Your best shot is porting it to more recent toolkits like cairo or QT or converting the file using GhostScript directly as suggested by PM2Ring.
For the GS approach in order to set the width and height of the output file you must use the -rN switch where N is the resolution in PPI (pixels per inch). You must do the math in order to get target resolution from the EPS bounding box and the desired output size.
Or you can render to a fixed resolution first, lets say, 100 PPI, see the width you got and do the math in order to get the correct resolution. For example, if rendering with -r100 gives you a file 500 pixels wide but you want it to be 1024:
desired_resolution = initial_resolution * desired_width // initial_width
In order to get a file 1024 pixels wide:
>>> 100 * 1024 // 500
204
So you must render the EPS again using -r204.
Edit 1:
I got the solution from this Question
We can set custom width and height using -gNNNNxMMMM
but the dpi value crops only a small area. I tried with the usual 72dpi and I got a decent output(I'm not sure if it's perfect or not). Now I need to find how to execute this command every time when I run the program and provide the custom image size value. :\

Related

In python convert SVG to PNG while resizing and increasing quality

First I note that there are many related questions, but after a day of trying pyvip and cairo and the rest none of them work for me, even after installing other software that they seem to depend on. The exception is svglib with reportlab, it comes close but doesn't quite get there! This is the best post I found and may help some.
I have all my source images in SVG files. Most app stores require you to provide a set of PNGs with specific sizes and qualities. So I need to take an SVG and produce a PNG with width w and height h and specific dpi. I want to do this programmatically in python.
I have written a function that almost works, but scaling and dpi interact with each other in weird ways. I use svglib to convert the SVG to a ReportLab drawing then use reportlab to manipulate the drawing. The install went smoothly on Windows unlike some of the other options.
pip install svglib
pip install reportlab
The code is as follows. I inspected the above libraries to get the arguments, but added stuff to get specific size.
from svglib.svglib import svg2rlg
from reportlab.graphics import renderPM
def svg_to_png(in_path,out_path,fmt="PNG",
scale=None,size=None,scale_x=1,size_x=None,scale_y=1,size_y=None,
dpi=72, bg=0xffffff):
# Convert SVG to ReportLab drawing.
drawing = svg2rlg(in_path)
# Work out scale factors
# Scale over-rides scale_x|y, ditto size
scale_x = scale if scale else scale_x
scale_y = scale if scale else scale_y
size_x = size if size else size_x
size_y = size if size else size_y
# Size over-rides scale
scaling_x = size_x/drawing.width if size_x else scale_x
scaling_y = size_y/drawing.height if size_y else scale_y
# Scale the drawing
drawing.width = drawing.minWidth() * scaling_x
drawing.height = drawing.height * scaling_y
drawing.scale(scaling_x, scaling_y)
# Render ReportLab drawing as a PNG
renderPM.drawToFile(drawing, out_path, fmt=fmt, dpi=dpi, bg=bg)
if __name__ == '__main__':
breakpoint()
in_path = 'C:\\Users\\...\\A.svg'
out_path = 'C:\\Users\\...\\A.png'
svg_to_png(in_path,out_path,scale=2,dpi=72)
The function works for resizing so long as I leave the dpi alone. The dpi=72 seems to be a universal default in reportlab and the source of the issues. I hoped that increasing the dpi would impove the quality of the PNG (reduce pixilation), but all it seems to do is expand the canvas.
The input SVG is of course pixel perfect. Here are four PNGs from the above function.
Scale 1 dpi 72 (dim 115x124 size 8.13kb), conversion with all defaults.
Scale 1 dpi 144 (dim 230x249 size 9.06kb), canvas doubled, pic in bottom-left quadrant, ignore black line bug.
Scale 2 dpi 72 (dim 230x249 size 17.5kb), scaled properly but pixelated (look at the eye)
Scale 2 dpi 144 (dim 461x497 size 19.8kb), canvas quadrupled? picture doubled.
Can someone please advise how to use reportlab properly to resize the picture to given scale or size and within that fixed size, increase the quality via the dpi.
Answering my own question, after lots of code inspections within svglib it appears it is impossible to do resize and increase dpi to predefined values. This is because svglib renders an SVG to a ReportLab drawing without any ability to tell it to render the SVG at the resolution required, which is a shame given the whole point of vector is arbitrary resolution. Once its in ReportLab you are stuck with the resolution of the drawing.
I switched to pyvips which makes use of libvips.
Its a bit fiddly to install, you can't just pip install pyvips. You have to download the libvips package and unzip it on disk somewhere you keep programs. You can then pip install pyvips in the usual way. In your python code you then add the libvips /bin to your path, assuming you don't want to do this permanently from the operating system.
The following function can then be used to convert an SVG to a PNG and set the resolution of the PNG and either scale it or set its horizontal width.
def svg_to_png(svg_path,png_path,dpi=72,scale=1,size=None):
# Documentation
# Ref: https://libvips.github.io/libvips/API/current/
# Ref: https://libvips.github.io/pyvips/
# Initialise
debug=False
import os
os.environ['path'] += r';C:\programs\vips\vips-dev-8.11\bin'
import pyvips
# Get the image
if size:
image = pyvips.Image.new_from_file(svg_path,dpi=dpi,scale=1)
if debug: print({field:image.get(field) for field in image.get_fields()})
scale = size/image.get('width')
image = image.resize(scale)
else:
image = pyvips.Image.new_from_file(svg_path,dpi=dpi,scale=scale)
# Write the image
if debug: print({field:image.get(field) for field in image.get_fields()})
image.write_to_file(png_path)
The function works properly when scaling. When setting a fixed output size it works well but a little bit of fiddling with the input dpi may be required to get the exact right output dpi.
Here's my solution which I also posted to a relevant Github Issue. This uses pymupdf to convert the intermediary PDF generated with svglib and reportlab to an SVG.
The advantage of this solution is that it doesn't need any fiddling with external dependencies like poppler, cairo or libvips, as pymupdf has prebuild wheels for Linux, Windows and MacOS.
Another advantage is the support for a transparent background.
import fitz
from svglib import svglib
from reportlab.graphics import renderPDF
# Convert svg to pdf in memory with svglib+reportlab
# directly rendering to png does not support transparency nor scaling
drawing = svglib.svg2rlg(path="input.svg")
pdf = renderPDF.drawToString(drawing)
# Open pdf with fitz (pyMuPdf) to convert to PNG
doc = fitz.Document(stream=pdf)
pix = doc.load_page(0).get_pixmap(alpha=True, dpi=300)
pix.save("output.png")
Cheers!

How to adjust Pillow EPS to JPG quality

I'm trying to convert EPS images to JPEG using Pillow. But the results are of low quality. I'm trying to use resize method, but it gets completely ignored. I set up the size of JPEG image as (3600, 4700), but the resulted image has (360, 470) size. My code is:
eps_image = Image.open('img.eps')
height = eps_image.height * 10
width = eps_image.width * 10
new_size = (height, width)
print(new_size) # prints (3600, 4700)
eps_image.resize(new_size, Image.ANTIALIAS)
eps_image.save(
'img.jpeg',
format='JPEG'
dpi=(9000, 9000),
quality=95)
UPD. Vasu Deo.S noticed one my error, and thanks to him the JPG image has become bigger, but quality is still low. I've tried different DPI, sizes, resample values for resize function, but the result does not change much. How can i make it better?
The problem is that PIL is a raster image processor, as opposed to a vector image processor. It "rasterises" vector images (such as your EPS file and SVG files) onto a grid when it opens them because it can only deal with rasters.
If that grid doesn't have enough resolution, you can never regain it. Normally, it rasterises at 100dpi, so if you want to make bigger images, you need to rasterise onto a larger grid before you even get started.
Compare:
from PIL import Image
eps_image = Image.open('image.eps')
eps_image.save('a.jpg')
The result is 540x720:
And this:
from PIL import Image
eps_image = Image.open('image.eps')
# Rasterise onto 4x higher resolution grid
eps_image.load(scale=4)
eps_image.save('a.jpg')
The result is 2160x2880:
You now have enough quality to resize however you like.
Note that you don't need to write any Python to do this at all - ImageMagick will do it all for you. It is included in most Linux distros and is available for macOS and Windows and you just use it in Terminal. The equivalent command is like this:
magick -density 400 input.eps -resize 800x600 -quality 95 output.jpg
It's because eps_image.resize(new_size, Image.ANTIALIAS) returns an resized copy of an image. Therefore you have to store it in a separate variable. Just change:-
eps_image.resize(new_size, Image.ANTIALIAS)
to
eps_image = eps_image.resize(new_size, Image.ANTIALIAS)
UPDATE:-
These may not solve the problem completely, but still would help.
You are trying to save your output image as a .jpeg, which is a
lossy compression format, therefore information is lost during the
compression/transformation (for the most part). Change the output
file extension to a lossless compression format like .png so that
data would not be compromised during compression. Also change
quality=95 to quality=100 in Image.save()
You are using Image.ANTIALIAS for resampling the image, which is
not that good when upscaling the image (it has been replaced by
Image.LANCZOS in newer version, the clause still exists for
backward compatibility). Try using Image.BICUBIC, which produces
quite favorable results (for the most part) when upscaling the image.

Writing text on 8-bit bitmap with PyQt

I've got an indexed colour (up to 6 colours) 8 bit bitmap file.
I need to write some text on it and save without changing the format nor the colour table.
Unfortunately via docs:
http://doc.qt.io/qt-5/qimage.html
Note: Drawing into a QImage with QImage::Format_Indexed8 is not supported.
My attempt was to open the file as QImage, convert to a format I can write on with QPainter (using only colours which are already in the colour table) and then convert it back to an 8-bit bitmap.
Something like this:
image = image.convertToFormat(QImage.Format_RGB16)
painter = QPainter()
painter.begin(image)
[....]
painter.end()
image = image.convertToFormat(QImage.Format_Indexed8, Qt.ThresholdDither)
But no matter what I do I always end up with a somehow corrupted image (it has 4 bit colour depth, it's damaged with kind of a noise in it, or it has its colours changed).
I've tried using different flags during conversion (usually messes the colour depth).
I also tried to copy the colorTable from the original image and set it after the conversion (before save) but this produces a negative/black&white image.
I would be very grateful for any help with this issue.

Python Image Library: clean Downsampling

I've been having trouble trying to get PIL to nicely downsample images. The goal, in this case, is for my website to automagically downsample->cache the original image file whenever a different size is required, thus removing the pain of maintaining multiple versions of the same image. However, I have not had any luck. I've tried:
image.thumbnail((width, height), Image.ANTIALIAS)
image.save(newSource)
and
image.resize((width, height), Image.ANTIALIAS).save(newSource)
and
ImageOps.fit(image, (width, height), Image.ANTIALIAS, (0, 0)).save(newSource)
and all of them seem to perform a nearest-neighbout downsample, rather than averaging over the pixels as it should Hence it turns images like
http://www.techcreation.sg/media/projects//software/Java%20Games/images/Tanks3D%20Full.png
to
http://www.techcreation.sg/media/temp/0x5780b20fe2fd0ed/Tanks3D.png
which isn't very nice. Has anyone else bumped into this issue?
That image is an indexed-color (palette or P mode) image. There are a very limited number of colors to work with and there's not much chance that a pixel from the resized image will be in the palette, since it will need a lot of in-between colors. So it always uses nearest-neighbor mode when resizing; it's really the only way to keep the same palette.
This behavior is the same as in Adobe Photoshop.
You want to convert to RGB mode first and resize it, then go back to palette mode before saving, if desired. (Actually I would just save it in RGB mode, and then turn PNGCrush loose on the folder of resized images.)
This is over a year old, but in case anyone is still looking:
Here is a sample of code that will see if an image is in a palette mode, and make adjustments
import Image # or from PIL import Image
img = Image.open(sourceFile)
if 'P' in img.mode: # check if image is a palette type
img = img.convert("RGB") # convert it to RGB
img = img.resize((w,h),Image.ANTIALIAS) # resize it
img = img.convert("P",dither=Image.NONE, palette=Image.ADAPTIVE)
#convert back to palette
else:
img = img.resize((w,h),Image.ANTIALIAS) # regular resize
img.save(newSourceFile) # save the image to the new source
#img.save(newSourceFile, quality = 95, dpi=(72,72), optimize = True)
# set quality, dpi , and shrink size
By converting the paletted version to RGB, we can resize it with the anti alias. If you want to reconvert it back, then you have to set dithering to NONE, and use an ADAPTIVE palette. If there options aren't included your result (if reconverted to palette) will be grainy. Also you can use the quality option, in the save function, on some image formats to improve the quality even more.

Python PIL: best scaling method that preserves lines

I have a 2D drawing with a black background and white lines (exported from Autocad) and I want to create a thumbnail preserving lines, using Python PIL library.
But what I obtain using the 'thumbnail' method is just a black picture scattered with white dots.
Note that if I put the image into an IMG tag with fixed width, I obtain exactly what I want (but the image is entirely loaded).
After your comments, here is my sample code:
from PIL import Image
fn = 'filename.gif'
im = Image(fn)
im.convert('RGB')
im.thumbnail((300, 300), Image.ANTIALIAS)
im.save('newfilename.png', 'PNG')
How can I do?
The default resizing method used by thumbnail is NEAREST, which is a really bad choice. If you're resizing to 1/5 of the original size for example, it will output one pixel and throw out the next 4 - a one-pixel wide line has only a 1 out of 5 chance of showing up at all in the result!
The surprising thing is that BILINEAR and BICUBIC aren't much better. They take a formula and apply it to the 2 or 3 closest pixels to the source point, but there's still lots of pixels they don't look at, and the formula will deemphasize the line anyway.
The best choice is ANTIALIAS, which appears to take all of the original image into consideration without throwing away any pixels. The lines will become dimmer but they won't disappear entirely; you can do an extra step to improve the contrast if necessary.
Note that all of these methods will fall back to NEAREST if you're working with a paletted image, i.e. im.mode == 'P'. You must always convert to 'RGB'.
from PIL import Image
im = Image.open(fn)
im = im.convert('RGB')
im.thumbnail(size, Image.ANTIALIAS)
Here's an example taken from the electronics.stackexchange site https://electronics.stackexchange.com/questions/5412/easiest-and-best-poe-ethernet-chip-micro-design-for-diy-interface-with-custom-ard/5418#5418
Using the default NEAREST algorithm, which I assume is similar to the results you had:
Using the ANTIALIAS algorithm:
By default, im.resize uses the NEAREST filter, which is going to do what you're seeing -- lose information unless it happens to fall on an appropriately moduloed pixel.
Instead call
im.resize(size, Image.BILINEAR)
This should preserve your lines. If not, try Image.BICUBIC or Image.ANTIALIAS. Any of those should work better than NEAREST.

Categories

Resources