Merge remote-tracking branch 'qubesos/pr/25'
* qubesos/pr/25: replace tinting algorithm with one that partially preserves saturation too reimplement tint algorithm with numpy for reasonable performance use PIL image library instead of ImageMagick to load/save images when tinting add Python pillow and numpy dependencies remove unused cairo import
This commit is contained in:
commit
0a7d2c0789
@ -45,7 +45,7 @@ make -C imgconverter all
|
|||||||
}
|
}
|
||||||
|
|
||||||
package_qubes-vm-utils() {
|
package_qubes-vm-utils() {
|
||||||
depends=(qubes-libvchan imagemagick python2-cairo)
|
depends=(qubes-libvchan imagemagick python2-cairo python2-pillow python2-numpy python-pillow python-numpy)
|
||||||
install=PKGBUILD-qubes-vm-utils.install
|
install=PKGBUILD-qubes-vm-utils.install
|
||||||
|
|
||||||
# Install all for python2
|
# Install all for python2
|
||||||
|
2
debian/control
vendored
2
debian/control
vendored
@ -17,7 +17,7 @@ Vcs-Git: http://dsg.is/qubes/qubes-linux-utils.git
|
|||||||
|
|
||||||
Package: qubes-utils
|
Package: qubes-utils
|
||||||
Architecture: any
|
Architecture: any
|
||||||
Depends: libvchan-xen, lsb-base, ${shlibs:Depends}, ${misc:Depends}
|
Depends: libvchan-xen, lsb-base, python-pil, python-numpy, python3-pil, python3-numpy, ${shlibs:Depends}, ${misc:Depends}
|
||||||
Conflicts: qubes-linux-utils
|
Conflicts: qubes-linux-utils
|
||||||
Breaks: qubes-core-agent (<< 3.1.4)
|
Breaks: qubes-core-agent (<< 3.1.4)
|
||||||
Recommends: python2.7
|
Recommends: python2.7
|
||||||
|
@ -23,8 +23,6 @@ Toolkit for secure transfer and conversion of images between Qubes VMs.'''
|
|||||||
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
||||||
|
|
||||||
|
|
||||||
import colorsys
|
|
||||||
import math
|
|
||||||
import os
|
import os
|
||||||
import re
|
import re
|
||||||
try:
|
try:
|
||||||
@ -35,7 +33,8 @@ import subprocess
|
|||||||
import sys
|
import sys
|
||||||
import unittest
|
import unittest
|
||||||
|
|
||||||
import cairo
|
import PIL.Image
|
||||||
|
import numpy
|
||||||
|
|
||||||
# those are for "zOMG UlTRa HD! WalLLpapPer 8K!!1!" to work seamlessly;
|
# those are for "zOMG UlTRa HD! WalLLpapPer 8K!!1!" to work seamlessly;
|
||||||
# 8192 * 5120 * 4 B = 160 MiB, so DoS by memory exhaustion is unlikely
|
# 8192 * 5120 * 4 B = 160 MiB, so DoS by memory exhaustion is unlikely
|
||||||
@ -73,6 +72,12 @@ get_from_stream(), get_from_vm(), get_xdg_icon_from_vm(), get_through_dvm()'''
|
|||||||
if p.wait():
|
if p.wait():
|
||||||
raise Exception('Conversion failed')
|
raise Exception('Conversion failed')
|
||||||
|
|
||||||
|
def save_pil(self, dst):
|
||||||
|
'''Save image to disk using PIL.'''
|
||||||
|
|
||||||
|
img = PIL.Image.frombytes('RGBA', self._size, self._rgba)
|
||||||
|
img.save(dst)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def data(self):
|
def data(self):
|
||||||
return self._rgba
|
return self._rgba
|
||||||
@ -88,27 +93,66 @@ get_from_stream(), get_from_vm(), get_xdg_icon_from_vm(), get_through_dvm()'''
|
|||||||
def tint(self, colour):
|
def tint(self, colour):
|
||||||
'''Return new tinted image'''
|
'''Return new tinted image'''
|
||||||
|
|
||||||
r, g, b = hex_to_float(colour)
|
tr, tg, tb = hex_to_int(colour)
|
||||||
h, _, s = colorsys.rgb_to_hls(r, g, b)
|
tM = max(tr, tg, tb)
|
||||||
result = BytesIO()
|
tm = min(tr, tg, tb)
|
||||||
|
|
||||||
# duplicate the whole loop for performance reasons
|
# (trn/tdn, tgn/tdn, tbn/tdn) is the tint color with maximum saturation
|
||||||
if sys.version_info[0] < 3:
|
if tm == tM:
|
||||||
for i in range(0, self._size[0] * self._size[1] * 4, 4):
|
trn = 1
|
||||||
r, g, b, a = tuple(ord(c) / 255. for c in self._rgba[i:i+4])
|
tgn = 1
|
||||||
_, l, _ = colorsys.rgb_to_hls(r, g, b)
|
tbn = 1
|
||||||
r, g, b = colorsys.hls_to_rgb(h, l, s)
|
tdn = 2
|
||||||
|
|
||||||
result.write(b''.join(chr(int(i * 255)) for i in [r, g, b, a]))
|
|
||||||
else:
|
else:
|
||||||
for i in range(0, self._size[0] * self._size[1] * 4, 4):
|
trn = tr - tm
|
||||||
r, g, b, a = tuple(c / 255. for c in self._rgba[i:i + 4])
|
tgn = tg - tm
|
||||||
_, l, _ = colorsys.rgb_to_hls(r, g, b)
|
tbn = tb - tm
|
||||||
r, g, b = colorsys.hls_to_rgb(h, l, s)
|
tdn = tM - tm
|
||||||
|
|
||||||
result.write(bytes(int(i * 255) for i in [r, g, b, a]))
|
# use a 1D image representation since we only process a single pixel at a time
|
||||||
|
pixels = self._size[0] * self._size[1]
|
||||||
|
x = numpy.fromstring(self._rgba, 'B').reshape(pixels, 4)
|
||||||
|
r = x[:, 0]
|
||||||
|
g = x[:, 1]
|
||||||
|
b = x[:, 2]
|
||||||
|
a = x[:, 3]
|
||||||
|
M = numpy.maximum(numpy.maximum(r, g), b).astype('u4')
|
||||||
|
m = numpy.minimum(numpy.minimum(r, g), b).astype('u4')
|
||||||
|
|
||||||
return self.__class__(rgba=result.getvalue(), size=self._size)
|
# Tn/Td is how much chroma range is reserved for the tint color
|
||||||
|
# 0 -> greyscale image becomes greyscale image
|
||||||
|
# 1 -> image becomes solid tint color
|
||||||
|
Tn = 1
|
||||||
|
Td = 4
|
||||||
|
|
||||||
|
# set chroma to the original pixel chroma mapped to the Tn/Td .. 1 range
|
||||||
|
# float c2 = (Tn/Td) + (1.0 - Tn/Td) * c
|
||||||
|
|
||||||
|
# set lightness to the original pixel lightness mapped to the range for the new chroma value
|
||||||
|
# float m2 = m * (1.0 - c2) / (1.0 - c)
|
||||||
|
|
||||||
|
c = M - m
|
||||||
|
|
||||||
|
c2 = (Tn * 255) + (Td - Tn) * c
|
||||||
|
c2d = Td
|
||||||
|
|
||||||
|
m2 = ((255 * c2d) - c2) * m
|
||||||
|
# the maximum avoids division by 0 when c = 255 (m2 is 0 anyway, so m2d doesn't matter)
|
||||||
|
m2d = numpy.maximum((255 - c) * c2d, 1)
|
||||||
|
|
||||||
|
# precomputed values
|
||||||
|
c2d_tdn = tdn * c2d
|
||||||
|
m2_c2d_tdn = m2 * c2d_tdn
|
||||||
|
m2d_c2d_tdn = m2d * c2d_tdn
|
||||||
|
c2_m2d = c2 * m2d
|
||||||
|
|
||||||
|
# float vt = m2 + tvn * c2
|
||||||
|
rt = ((m2_c2d_tdn + trn * c2_m2d) // m2d_c2d_tdn).astype('B')
|
||||||
|
gt = ((m2_c2d_tdn + tgn * c2_m2d) // m2d_c2d_tdn).astype('B')
|
||||||
|
bt = ((m2_c2d_tdn + tbn * c2_m2d) // m2d_c2d_tdn).astype('B')
|
||||||
|
|
||||||
|
xt = numpy.column_stack((rt, gt, bt, a))
|
||||||
|
return self.__class__(rgba=xt.tobytes(), size=self._size)
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def load_from_file(cls, filename):
|
def load_from_file(cls, filename):
|
||||||
@ -130,6 +174,12 @@ get_from_stream(), get_from_vm(), get_xdg_icon_from_vm(), get_through_dvm()'''
|
|||||||
|
|
||||||
return cls(rgba=rgba, size=size)
|
return cls(rgba=rgba, size=size)
|
||||||
|
|
||||||
|
def load_from_file_pil(cls, filename):
|
||||||
|
'''Loads image from local file using PIL.'''
|
||||||
|
img = PIL.Image.open(filename)
|
||||||
|
img = img.convert('RGBA')
|
||||||
|
return cls(rgba=img.tobytes(), size=img.size)
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def get_from_stream(cls, stream, max_width=MAX_WIDTH, max_height=MAX_HEIGHT):
|
def get_from_stream(cls, stream, max_width=MAX_WIDTH, max_height=MAX_HEIGHT):
|
||||||
'''Carefully parse image data from stream.
|
'''Carefully parse image data from stream.
|
||||||
@ -223,26 +273,23 @@ expects header+RGBA on stdin. This method is invoked from qvm-imgconverter-clien
|
|||||||
def __ne__(self, other):
|
def __ne__(self, other):
|
||||||
return not self.__eq__(other)
|
return not self.__eq__(other)
|
||||||
|
|
||||||
def hex_to_float(colour, channels=3, depth=8):
|
def hex_to_int(colour, channels=3, depth=1):
|
||||||
'''Convert hex colour definition to tuple of floats.'''
|
'''Convert hex colour definition to tuple of ints.'''
|
||||||
|
|
||||||
if depth % 4 != 0:
|
length = channels * depth * 2
|
||||||
raise NotImplementedError('depths not divisible by 4 are unsupported')
|
step = depth * 2
|
||||||
|
|
||||||
length = channels * depth // 4
|
|
||||||
step = depth // 4
|
|
||||||
|
|
||||||
# get rid of '#' or '0x' in front of hex values
|
# get rid of '#' or '0x' in front of hex values
|
||||||
colour = colour[-length:]
|
colour = colour[-length:]
|
||||||
|
|
||||||
return tuple(int(colour[i:i+step], 0x10) / float(2**depth - 1) for i in range(0, length, step))
|
return tuple(int(colour[i:i+step], 0x10) for i in range(0, length, step))
|
||||||
|
|
||||||
def tint(src, dst, colour):
|
def tint(src, dst, colour):
|
||||||
'''Tint image to reflect vm label.
|
'''Tint image to reflect vm label.
|
||||||
|
|
||||||
src and dst may specify format, like png:aqq.gif'''
|
src and dst may NOT specify ImageMagick format'''
|
||||||
|
|
||||||
Image.load_from_file(src).tint(colour).save(dst)
|
Image.load_from_file_pil(src).tint(colour).save_pil(dst)
|
||||||
|
|
||||||
|
|
||||||
# vim: ft=python sw=4 ts=4 et
|
# vim: ft=python sw=4 ts=4 et
|
||||||
|
@ -35,6 +35,8 @@ Common Linux files for Qubes Dom0 and VM
|
|||||||
Summary: Python package qubesimgconverter
|
Summary: Python package qubesimgconverter
|
||||||
Requires: python
|
Requires: python
|
||||||
Requires: pycairo
|
Requires: pycairo
|
||||||
|
Requires: python2-pillow
|
||||||
|
Requires: python2-numpy
|
||||||
|
|
||||||
%description -n python2-qubesimgconverter
|
%description -n python2-qubesimgconverter
|
||||||
Python package qubesimgconverter
|
Python package qubesimgconverter
|
||||||
@ -48,6 +50,8 @@ Requires: pycairo
|
|||||||
Requires: python3
|
Requires: python3
|
||||||
Requires: python3-cairo
|
Requires: python3-cairo
|
||||||
%endif
|
%endif
|
||||||
|
Requires: python3-pillow
|
||||||
|
Requires: python3-numpy
|
||||||
|
|
||||||
%description -n python3-qubesimgconverter
|
%description -n python3-qubesimgconverter
|
||||||
Python package qubesimgconverter
|
Python package qubesimgconverter
|
||||||
|
Loading…
Reference in New Issue
Block a user