Skip to content

Commit

Permalink
Merge branch 'dev'
Browse files Browse the repository at this point in the history
  • Loading branch information
helgeerbe committed Dec 4, 2022
2 parents 9707f40 + d5f2599 commit 3930e4e
Show file tree
Hide file tree
Showing 10 changed files with 166 additions and 88 deletions.
1 change: 1 addition & 0 deletions picframe/config/configuration_example.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ viewer:
display_y: 0 # offset from top of screen (can be negative)
display_w: null # width of display surface (null->None will use max returned by hardware)
display_h: null # height of display surface
display_power: 0 # default=0. choices={0, 1}, 0 will use legacy `vcgencmd` and 1 will use `xset` to blank the display
use_glx: False # default=False. Set to True on linux with xserver running

mat_images: 0.01 # default=0.01, True, automatically mat all images. False, don't automatically mat any images. Real value, auto-mat all images with aspect ratio difference > than value
Expand Down
163 changes: 115 additions & 48 deletions picframe/get_image_meta.py
Original file line number Diff line number Diff line change
@@ -1,25 +1,105 @@
import exifread
import logging
import os
from PIL import Image
from PIL.Image import Exif
from PIL.ExifTags import TAGS, GPSTAGS
from pi_heif import register_heif_opener
from fractions import Fraction


register_heif_opener()

class GetImageMeta:

def __init__(self, filename):
self.__logger = logging.getLogger("get_image_meta.GetImageMeta")
self.__tags = {}
self.__filename = filename # in case no exif data in which case needed for size
image = self.get_image_object(filename)
if image:
exif = image.getexif()
self.__do_image_tags(exif)
self.__do_exif_tags(exif)
self.__do_geo_tags(exif)
self.__do_iptc_keywords()
xmp = image.getxmp()
if len(xmp) > 0:
self.__do_xmp_keywords(xmp)

def __do_image_tags(self, exif):
tags = {
"Image " + TAGS.get(key, key): value
for key, value in exif.items()
}
self.__tags.update(tags)

def __do_exif_tags(self, exif):
for key, value in TAGS.items():
if value == "ExifOffset":
break
info = exif.get_ifd(key)
tags = {
"EXIF " + TAGS.get(key, key): value
for key, value in info.items()
}
self.__tags.update(tags)

def __do_geo_tags(self, exif):
for key, value in TAGS.items():
if value == "GPSInfo":
break
gps_info = exif.get_ifd(key)
tags = {
"GPS " + GPSTAGS.get(key, key): value
for key, value in gps_info.items()
}
self.__tags.update(tags)

def __find_xmp_key(self, key, dic):
for k, v in dic.items():
if key == k:
return v
elif isinstance(v, dict):
val = self.__find_xmp_key(key, v)
if val:
return val
elif isinstance(v, list):
for x in v:
if isinstance(x, dict):
val = self.__find_xmp_key(key, x)
if val:
return val
return None

def __do_xmp_keywords(self, xmp):
try:
with open(filename, 'rb') as fh:
self.__tags = exifread.process_file(fh, details=False)
except OSError as e:
self.__logger.warning("Can't open file: \"%s\"", filename)
self.__logger.warning("Cause: %s", e)
#raise # the system should be able to withstand files being moved etc without crashing
# title
val = self.__find_xmp_key('Headline', xmp)
if val and isinstance(val, str) and len(val) > 0:
self.__tags['IPTC Object Name'] = val
# caption
try:
val = self.__find_xmp_key('description', xmp)
if val:
val = val['Alt']['li']['text']
if val and isinstance(val, str) and len(val) > 0:
self.__tags['IPTC Caption/Abstract'] = val
except KeyError:
pass
# tags
try:
val = self.__find_xmp_key('subject', xmp)
if val:
val = val['Bag']['li']
if val and isinstance(val, list) and len(val) > 0:
tags = ''
for tag in val:
tags += tag + ","
self.__tags['IPTC Keywords'] = tags
except KeyError:
pass
except Exception as e:
self.__logger.warning("exifread doesn't manage well and gives AttributeError for heif files %s -> %s",
filename, e)
self.__do_iptc_keywords()
self.__logger.warning("xmp loading has failed: %s -> %s", self.__filename, e)

def __do_iptc_keywords(self):
try:
Expand Down Expand Up @@ -59,11 +139,8 @@ def __get_if_exist(self, key):
return None

def __convert_to_degrees(self, value):
(deg, min, sec) = value.values
d = float(deg.num) / float(deg.den if deg.den > 0 else 1) #TODO better catching?
m = float(min.num) / float(min.den if min.den > 0 else 1)
s = float(sec.num) / float(sec.den if sec.den > 0 else 1)
return d + (m / 60.0) + (s / 3600.0)
(deg, min, sec) = value
return deg + (min / 60.0) + (sec / 3600.0)

def get_location(self):
gps = {"latitude": None, "longitude": None}
Expand All @@ -78,12 +155,12 @@ def get_location(self):
try:
if gps_latitude and gps_latitude_ref and gps_longitude and gps_longitude_ref:
lat = self.__convert_to_degrees(gps_latitude)
if len(gps_latitude_ref.values) > 0 and gps_latitude_ref.values[0] == 'S':
if len(gps_latitude_ref) > 0 and gps_latitude_ref[0] == 'S':
# assume zero length string means N
lat = 0 - lat
gps["latitude"] = lat
lon = self.__convert_to_degrees(gps_longitude)
if len(gps_longitude_ref.values) and gps_longitude_ref.values[0] == 'W':
if len(gps_longitude_ref) and gps_longitude_ref[0] == 'W':
lon = 0 - lon
gps["longitude"] = lon
except Exception as e:
Expand All @@ -94,7 +171,7 @@ def get_orientation(self):
try:
val = self.__get_if_exist('Image Orientation')
if val is not None:
return int(val.values[0])
return val
else:
return 1
except Exception as e:
Expand All @@ -120,14 +197,14 @@ def get_exif(self, key):
elif grp == "Image":
newkey = "EXIF" + " " + tag
val = self.__get_if_exist(newkey)
if val is not None:
if key == 'EXIF FNumber':
val = round(val.values[0].num / val.values[0].den, 1)
elif key in ['IPTC Keywords', 'IPTC Caption/Abstract', 'IPTC Object Name']:
return val
else:
val = val.printable
return val
if val:
if key == "EXIF ExposureTime":
val = str(Fraction(val))
elif key == "EXIF FocalLength":
val = str(val)
elif key == "EXIF FNumber":
val = float(val)
return val
except Exception as e:
self.__logger.warning("get_exif failed on %s -> %s", self.__filename, e)
return None
Expand All @@ -141,25 +218,15 @@ def get_size(self):

@staticmethod
def get_image_object(fname):
ext = os.path.splitext(fname)[1].lower()
if ext in ('.heif','.heic'):
try:
import pyheif

heif_file = pyheif.read(fname)
image = Image.frombytes(heif_file.mode, heif_file.size, heif_file.data,
"raw", heif_file.mode, heif_file.stride)
if image.mode not in ("RGB", "RGBA"):
image = image.convert("RGB")
return image
except:
logger = logging.getLogger("get_image_meta.GetImageMeta")
logger.warning("Failed attempt to convert %s \n** Have you installed pyheif? **", fname)
else:
try:
image = Image.open(fname)
if image.mode not in ("RGB", "RGBA"): # mat system needs RGB or more
image = image.convert("RGB")
except: # for whatever reason
image = None
return image
ext = os.path.splitext(fname)[1].lower()
try:
image = Image.open(fname)
if image.mode not in ("RGB", "RGBA"): # mat system needs RGB or more
image = image.convert("RGB")
#raise # the system should be able to withstand files being moved etc without crashing
except Exception as e:
logger = logging.getLogger("get_image_meta.GetImageMeta")
logger.warning("Can't open file: \"%s\"", fname)
logger.warning("Cause: %s", e)
image = None
return image
9 changes: 4 additions & 5 deletions picframe/interface_http.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,19 +18,18 @@

def heif_to_jpg(fname):
try:
import pyheif
from PIL import Image
from pi_heif import register_heif_opener

heif_file = pyheif.read(fname)
image = Image.frombytes(heif_file.mode, heif_file.size, heif_file.data,
"raw", heif_file.mode, heif_file.stride)
register_heif_opener()
image = Image.open(fname)
if image.mode not in ("RGB", "RGBA"):
image = image.convert("RGB")
image.save("/dev/shm/temp.jpg") # default 75% quality
return "/dev/shm/temp.jpg"
except:
logger = logging.getLogger("interface_http.heif_to_jpg")
logger.warning("Failed attempt to convert %s \n** Have you installed pyheif? **", fname)
logger.warning("Failed attempt to convert %s \n** Have you installed pi_heif? **", fname)
return "" # this will not render as a page and will generate error TODO serve specific page with explicit error

class RequestHandler(BaseHTTPRequestHandler):
Expand Down
1 change: 1 addition & 0 deletions picframe/model.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@
'display_y': 0,
'display_w': None,
'display_h': None,
'display_power': 0,
'use_glx': False, # default=False. Set to True on linux with xserver running
'test_key': 'test_value',
'mat_images': True,
Expand Down
7 changes: 3 additions & 4 deletions picframe/start.py
Original file line number Diff line number Diff line change
Expand Up @@ -114,17 +114,16 @@ def main():
print("\nChecking required packages......")
required_packages=[
'PIL',
'exifread',
'pi3d',
'yaml',
'paho.mqtt',
'iptcinfo3',
'numpy',
'ninepatch'
'ninepatch',
'pi_heif',
'defusedxml'
]
check_packages(required_packages)
print("\nChecking optional packages......")
check_packages(['pyheif'])
return
elif args.configfile:
m = model.Model(args.configfile)
Expand Down
50 changes: 30 additions & 20 deletions picframe/viewer_display.py
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,7 @@ def __init__(self, config):
self.__display_y = int(config['display_y'])
self.__display_w = None if config['display_w'] is None else int(config['display_w'])
self.__display_h = None if config['display_h'] is None else int(config['display_h'])
self.__display_power = int(config['display_power'])
self.__use_glx = config['use_glx']
#self.__codepoints = config['codepoints']
self.__alpha = 0.0 # alpha - proportion front image to back
Expand Down Expand Up @@ -103,15 +104,18 @@ def __init__(self, config):

@property
def display_is_on(self):
try: # vcgencmd only applies to raspberry pi
state = str(subprocess.check_output(["vcgencmd", "display_power"]))
if (state.find("display_power=1") != -1):
return True
else:
return False
except Exception as e:
self.__logger.debug("Display ON/OFF is vcgencmd, but an error occurred")
self.__logger.debug("Cause: %s", e)
if self.__display_power == 0:
try: # vcgencmd only applies to raspberry pi
state = str(subprocess.check_output(["vcgencmd", "display_power"]))
if (state.find("display_power=1") != -1):
return True
else:
return False
except Exception as e:
self.__logger.debug("Display ON/OFF is vcgencmd, but an error occurred")
self.__logger.debug("Cause: %s", e)
return True
elif self.__display_power == 1:
try: # try xset on linux, DPMS has to be enabled
output = subprocess.check_output(["xset" , "-display", ":0", "-q"])
if output.find(b'Monitor is On') != -1:
Expand All @@ -121,19 +125,24 @@ def display_is_on(self):
except Exception as e:
self.__logger.debug("Display ON/OFF is X with dpms enabled, but an error occurred")
self.__logger.debug("Cause: %s", e)
self.__logger.warning("Display ON/OFF is not supported for this platform.")
return True
return True
else:
self.__logger.warning("Unsupported setting for display_power=%d.", self.__display_power)
return True

@display_is_on.setter
def display_is_on(self, on_off):
try: # vcgencmd only applies to raspberry pi
if on_off == True:
subprocess.call(["vcgencmd", "display_power", "1"])
else:
subprocess.call(["vcgencmd", "display_power", "0"])
except Exception as e:
self.__logger.debug("Display ON/OFF is vcgencmd, but an error occured")
self.__logger.debug("Cause: %s", e)
self.__logger.debug("Switch display (display_power=%d).", self.__display_power)
if self.__display_power == 0:
try: # vcgencmd only applies to raspberry pi
if on_off == True:
subprocess.call(["vcgencmd", "display_power", "1"])
else:
subprocess.call(["vcgencmd", "display_power", "0"])
except Exception as e:
self.__logger.debug("Display ON/OFF is vcgencmd, but an error occured")
self.__logger.debug("Cause: %s", e)
elif self.__display_power == 1:
try: # try xset on linux, DPMS has to be enabled
if on_off == True:
subprocess.call(["xset" , "-display", ":0", "dpms", "force", "on"])
Expand All @@ -142,7 +151,8 @@ def display_is_on(self, on_off):
except Exception as e:
self.__logger.debug("Display ON/OFF is xset via dpms, but an error occured")
self.__logger.debug("Cause: %s", e)
self.__logger.warning("Display ON/OFF is not supported for this platform.")
else:
self.__logger.warning("Unsupported setting for display_power=%d.", self.__display_power)

def set_show_text(self, txt_key=None, val="ON"):
if txt_key is None:
Expand Down
5 changes: 3 additions & 2 deletions setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,13 +32,14 @@
'html/*', 'html/**/*']},
install_requires=[
'Pillow>=9.0.0',
'ExifRead',
'defusedxml',
'pi3d>=2.49',
'PyYAML',
'paho-mqtt',
'IPTCInfo3',
'numpy',
'ninepatch'
'ninepatch',
'pi_heif>=0.8.0'
],
entry_points = {
'console_scripts': ['picframe=picframe.start:main']
Expand Down
Binary file added test/images/sample1.heic
Binary file not shown.
Binary file modified test/images/test3.HEIC
Binary file not shown.
Loading

0 comments on commit 3930e4e

Please sign in to comment.