Open Matteljay opened 6 years ago
+1 here. PIL increased my build size from somewhere around 23MB to around 150MB. That's clearly nonsense when no dynamic image creation is necessary.
I actually forked pystray and cut out PIL.
Currently, I’m building the system-specific image objects in my own code like this and hand them over to Icon()
:
if MAC:
image = NSImage.alloc().initWithContentsOfFile_(NSBundle.mainBundle().pathForResource_ofType_('MacSystemTrayIcon', 'pdf'))
image.setTemplate_(True)
if WIN:
image = win32.LoadImage(
None,
os.path.join(os.path.dirname(__file__), 'icon', 'TaskbarIcon.ico'),
win32.IMAGE_ICON,
0,
0,
win32.LR_DEFAULTSIZE | win32.LR_LOADFROMFILE)
icon = pystray.Icon("Type.World", image, "Type.World", menu)
Doing this in user code has some advantages as the image.setTemplate_(True)
shows. Achieving the same functionality in pystray further complicates its code.
Otherwise I'm all for simplicity of use, of course.
I don't want to contribute my code because I've really messed with pystray here. But it works for now. And I've only implemented this for Windows and Mac, so my code is of no much use here.
Thank you for your comments and request!
I understand your request to get rid of the PIL dependency---125 MB of seemingly unnecessary dependencies is somewhat unreasonable.
The reason for the dependency is that PIL appears to be the de-facto image library for Python, and a design goal of pystray is to abstract away the need for code like the example above with several if statements in every application using the library. As you note, the code above does not support Linux, in which case you would still need PIL.
Nonetheless, I think it would be a good idea to make the dependency on PIL optional for platforms not needing it, and add support for initialising the icon with a native handle.
As a hack, If you only need to support windows and linux, then all the image
needs to do is quack like one method of PIL.Image
. On windows and linux the only method called is PIL.Image.save()
so I made a pure python version of the PIL.Image
class that only supports loading 256x256 alpha transparent PNGs and then saves to PNG / ICO by using the pure python code from https://github.com/flexxui/webruntime/blob/e88f7abb28fa0ea02c3f5cb4fb6b5fe258d7ed9c/webruntime/util/icon.py
A better API would be to actually just define the interface that pystray needs from the Image-like object, and call that.
The original reason for the dependency on PIL is actually the X.Org backend; it has a direct dependency on PIL in the form of put_pil_image.
Now that I actually looked at the source, it does not seem very difficult to implement, but still: given the multitude of platforms supported by this library, the interface to image-like objects needed would need consideration, and the amount of code that needed to be written would be non-trivial.
I'm on Windows, and I found it quite stupid to absolutely require PIL
dependency. I get that it's required for a different backend, but I'm not aiming at deploying there, yet the Icon
signature explicitly asks for a PIL.Image
instance. I dug into the code and found out that all it does is call the save
method into a temporary file, so I wrote this "wrapper" that simulates it:
class ICOImage:
def __init__(self, path: str):
with open(path, 'rb') as file:
self._data = file.read()
def save(self, file, format):
file.write(self._data)
Now instead of Image.open("app.ico")
in the signature, I can use ICOImage("app.ico")
and it still works properly. Why one can't just pass in the icon file as bytes or a path to read directly from instead? It'd simplify things enormously.
My app package has grown from 10.2 MB to 48 MB after adding a tray icon with a PIL
import, replacing the import with the wrapper above has shrunk it back down to 10.6 MB. I really think this part could be improved somewhat.
I'm on Windows, and I found it quite stupid to absolutely require
PIL
dependency. I get that it's required for a different backend, but I'm not aiming at deploying there, yet theIcon
signature explicitly asks for aPIL.Image
instance. I dug into the code and found out that all it does is call thesave
method into a temporary file, so I wrote this "wrapper" that simulates it:class ICOImage: def __init__(self, path: str): with open(path, 'rb') as file: self._data = file.read() def save(self, file, format): file.write(self._data)
Now instead of
Image.open("app.ico")
in the signature, I can useICOImage("app.ico")
and it still works properly. Why one can't just pass in the icon file as bytes or a path to read directly from instead? It'd simplify things enormously.My app package has grown from 10.2 MB to 48 MB after adding a tray icon with a
PIL
import, replacing the import with the wrapper above has shrunk it back down to 10.6 MB. I really think this part could be improved somewhat.
Would this work on mac and linux as well?
Would this work on mac and linux as well?
No, this is a Windows-only solution. The reason why an open PIL image is needed is because different operating systems require different solutions when it comes to the icon, most notably: Windows uses an ICO file, Linux and Mac (appear to) use PNG, Darwin uses what the code refers to as "NSImage", and so on. The PIL image is used to dynamically create all those formats on an as-needed basis depending on where the code is ran, hence why it doesn't really matter what format is the picture you're initially opening to serve as the icon. For my "workaround", it needs to explicitly be an ICO file already, and it'll work only for Windows, since that's the only place where ICO files are used. Still, if Windows is the only target platform, this removes the PIL dependency from the equation.
It turns out that I needed to use more PIL in my application anyway, so I reverted back to the documented way of providing the image.
Would this work on mac and linux as well?
No, this is a Windows-only solution. The reason why an open PIL image is needed is because different operating systems require different solutions when it comes to the icon, most notably: Windows uses an ICO file, Linux and Mac (appear to) use PNG, Darwin uses what the code refers to as "NSImage", and so on. The PIL image is used to dynamically create all those formats on an as-needed basis depending on where the code is ran, hence why it doesn't really matter what format is the picture you're initially opening to serve as the icon. For my "workaround", it needs to explicitly be an ICO file already, and it'll work only for Windows, since that's the only place where ICO files are used. Still, if Windows is the only target platform, this removes the PIL dependency from the equation.
It turns out that I needed to use more PIL in my application anyway, so I reverted back to the documented way of providing the image.
Interesting. There should probably be an option to provide the exact image required instead of requiring Pillow. I understand that Pillow would be useful when there is a need to dynamically create an icon, but I would expect that the majority of use cases are just using an application logo/icon.
For those interested, this is the code I use to remove the PIL dependency on windows/mac/linux
note: in my app I use opencv, so it makes sense to me to use that for the basic uses here, instead of PIL. your tradeoffs may be different
# Little endian int encoding (for bmp/icon writing)
w1 = lambda x: struct.pack('<B', x)
w2 = lambda x: struct.pack('<H', x)
w4 = lambda x: struct.pack('<I', x)
# quack like a PIL.Image without depending on PIL
class _Img(object):
def __init__(self, path):
self._arr = cv2.imread(path, cv2.IMREAD_UNCHANGED)
if self._arr.shape[2] != 4:
raise ValueError('must be PNG with alpha channel')
@property
def img(self):
return self._arr[:, :, :3]
def save(self, fd, format):
if format == 'ICO':
self._save_ico(fd)
elif format in ('PNG', 'JPG'):
_, arr = cv2.imencode('.%s' % format.lower(), self._arr)
arr.tofile(fd)
else:
raise NotImplementedError
@staticmethod
def _to_bmp(im, file_header=False):
_h, width = im.shape[:2]
if _h != width:
raise ValueError('must be square')
height = reported_height = width
if not file_header:
reported_height *= 2 # This is soo weird, but it needs to be so
# Flip vertically
im = cv2.flip(im, 0)
# DIB header
bb = b''
bb += w4(40) # header size
bb += w4(width)
bb += w4(reported_height)
bb += w2(1) # 1 color plane
bb += w2(32)
bb += w4(0) # no compression
bb += w4(len(im))
bb += w4(2835) + w4(2835) # 2835 pixels/meter, ~ 72 dpi
bb += w4(0) # number of colors in palette
bb += w4(0) # number of important colors (0->all)
# File header (not when bm is in-memory)
header = b''
if file_header:
header += b'BM'
header += w4(14 + 40 + len(im)) # file size
header += b'\x00\x00\x00\x00'
header += w4(14 + 40) # pixel data offset
# Add pixels
# No padding, because we assume power of 2 image sizes
return header + bb + im.tobytes()
def _save_ico(self, fd):
sizes = [(16, 16), (24, 24), (32, 32), (48, 48), (64, 64), (128, 128), (256, 256)]
bb = b''
imdatas = []
# Header
bb += w2(0)
bb += w2(1) # 1:ICO, 2:CUR
bb += w2(len(sizes))
# Put offset right after the last directory entry
offset = len(bb) + 16 * len(sizes)
# Directory (header for each image)
for sw, sh in sizes:
# icons are square
size = sw
im = cv2.resize(self._arr, (sw, sh))
if size > 256:
continue
elif size >= 64:
_, arr = cv2.imencode('.png', self._arr)
imdata = arr.tobytes()
else:
imdata = self._to_bmp(im)
imdatas.append(imdata)
# Prepare dimensions
w = h = 0 if size == 256 else size
# Write directory entry
bb += w1(w)
bb += w1(h)
bb += w1(0) # number of colors in palette, assume no palette (0)
bb += w1(0) # reserved (must be 0)
bb += w2(0) # color planes
bb += w2(32) # bits per pixel
bb += w4(len(imdata)) # size of image data
bb += w4(offset)
# Set offset pointer
offset += len(imdata)
fd.write(b''.join([bb] + imdatas))
and then I call it, more or less, like the following pseudocode
image = _Img('logo.png'))
menu = [pystray.MenuItem('Open XXX', self._on_open, default=True)]
icon = pystray.Icon("MyApp", image, "MyApp", menu)
BTW, in case it wasn't clear from my code above, if you want to remove the opencv dependency then all you need is to ship PNG files of the correct size (which removes the need to resize them), and use the pure python PNG read/write routines from the file linked in https://github.com/moses-palmer/pystray/issues/26#issuecomment-468704074
As I said, I already depened on opencv so I didnt need them
Has there been any movement on making PIL an optional dependency? Right now I also will have to fork this package in order to remove the PIL dependency. Easy, but not ideal.
Hi great project i really appreciate any modules that add basic functions to Python while keeping it cross-platform. I'd like to "feature" request that you remove the dependency on PIL and allow for usage of an icon.png file. Hope i'm not alone in this desire, that it sounds reasonible and hope that it is easy to implement.