Create attractive VASL scenarios, with loads of useful information embedded to assist with game play. https://vasl-templates.org
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 
 
vasl-templates/vasl_templates/webapp/utils.py

267 lines
8.5 KiB

""" Miscellaneous utilities. """
import os
import shutil
import io
import tempfile
import pathlib
import math
from collections import defaultdict
from flask import request, Response, send_file
from PIL import Image, ImageChops
# ---------------------------------------------------------------------
class MsgStore:
"""Store different types of messages."""
def __init__( self ):
self._msgs = None
self.reset()
def reset( self ):
"""Reset the MsgStore."""
self._msgs = defaultdict( list )
def info( self, msg, *args, **kwargs ):
"""Add an informational message."""
self._add_msg( "info", msg, *args, **kwargs )
def warning( self, msg, *args, **kwargs ):
"""Add a warning message."""
self._add_msg( "warning", msg, *args, **kwargs )
def error( self, msg, *args, **kwargs ):
"""Add an error message."""
self._add_msg( "error", msg, *args, **kwargs )
def get_msgs( self, msg_type ):
"""Get stored messages."""
return self._msgs[ msg_type ]
def _add_msg( self, msg_type, msg, *args, **kwargs ):
"""Add a message to the store."""
logger = kwargs.pop( "logger", None )
if args or kwargs:
# NOTE: We only format the message if there are any parameters, to handle the case
# where the caller passed us a single string that happens to contain a {.
msg = msg.format( *args, **kwargs )
self._msgs[ msg_type ].append( msg )
if logger:
func = getattr( logger, "warn" if msg_type == "warning" else msg_type )
func( msg )
# ---------------------------------------------------------------------
class TempFile:
"""Manage a temp file that can be closed while it's still being used."""
def __init__( self, mode="wb", extn=None, encoding=None ):
self.mode = mode
self.extn = extn
self.encoding = encoding
self.temp_file = None
self.name = None
def open( self ):
"""Allocate a temp file."""
if self.encoding:
encoding = self.encoding
else:
encoding = "utf-8" if "b" not in self.mode else None
assert self.temp_file is None
self.temp_file = tempfile.NamedTemporaryFile(
mode = self.mode,
encoding = encoding,
suffix = self.extn,
delete = False
)
self.name = self.temp_file.name
def close( self, delete ):
"""Close the temp file."""
self.temp_file.close()
if delete:
os.unlink( self.temp_file.name )
def write( self, data ):
"""Write data to the temp file."""
self.temp_file.write( data )
def save_copy( self, fname, logger, caption ):
"""Make a copy of the temp file (for debugging porpoises)."""
if not fname:
return
logger.debug( "Saving a copy of the %s: %s", caption, fname )
shutil.copyfile( self.temp_file.name, fname )
def __enter__( self ):
"""Enter the context manager."""
self.open()
return self
def __exit__( self, exc_type, exc_val, exc_tb ):
"""Exit the context manager."""
self.close( delete=True )
# ---------------------------------------------------------------------
def resize_image_response( resp, default_width=None, default_height=None, default_scaling=None ):
"""Resize an image that will be returned as a Flask response."""
assert isinstance( resp, Response )
def get_image():
"""Get the the image from the Flask response that was passed in."""
resp.direct_passthrough = False
buf = io.BytesIO()
buf.write( resp.get_data() )
buf.seek( 0 )
return Image.open( buf )
# check if the caller specified a width and/or height
width = request.args.get( "width", default_width )
height = request.args.get( "height", default_height )
if width and height:
# width and height were specified, just use them as-is
img = get_image()
width = int( width )
height = int( height )
elif width and not height:
# width only, calculate the height
img = get_image()
aspect_ratio = float(img.size[0]) / float(img.size[1])
height = int(width) / aspect_ratio
elif not width and height:
# height only, calculate the width
img = get_image()
aspect_ratio = float(img.size[0]) / float(img.size[1])
width = int(height) * aspect_ratio
elif not width and not height:
# check if the caller specified a scaling factor
scaling = request.args.get( "scaling", default_scaling )
if scaling and scaling != 100:
img = get_image()
width = img.size[0] * float(scaling)/100
height = img.size[1] * float(scaling)/100
# check if we need to resize the image
if width or height:
assert width and height
# yup - make it so
img = img.resize( (int(width),int(height)), Image.ANTIALIAS )
buf = io.BytesIO()
img.save( buf, format="PNG" )
buf.seek( 0 )
return send_file( buf, mimetype="image/png" )
else:
# nope - return the image as-is
return resp
# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
def trim_image( img ):
"""Trim whitespace from an image."""
if isinstance( img, str ):
img = Image.open( img )
# trim the screenshot (nb: we assume a white background)
bgd = Image.new( img.mode, img.size, (255,255,255,255) )
diff = ImageChops.difference( img, bgd )
bbox = diff.getbbox()
return img.crop( bbox )
def get_image_data( img, **kwargs ):
"""Get the data from a Pillow image."""
buf = io.BytesIO()
img.save( buf, format=kwargs.pop("format","PNG"), **kwargs )
buf.seek( 0 )
return buf.read()
def remove_alpha_from_image( img ):
"""Remove the alpha channel from an image."""
img2 = Image.new( "RGB", img.size, "WHITE" )
img2.paste( img, (0,0), img )
return img2
# ---------------------------------------------------------------------
def change_extn( fname, extn ):
"""Change a filename's extension."""
return pathlib.Path( fname ).with_suffix( extn )
def is_image_file( fname ):
"""Check if a file is an image."""
if fname.startswith( "." ):
extn = fname
else:
extn = os.path.splitext( fname )[1]
return extn.lower() in (".png",".jpg",".jpeg",".gif")
def is_empty_file( fname ):
"""Check if a file is empty."""
return os.stat( fname ).st_size == 0
def parse_int( val, default=None ):
"""Parse an integer."""
try:
return int( val )
except (ValueError, TypeError):
return default
# ---------------------------------------------------------------------
def friendly_fractions( val, postfix=None, postfix2=None ):
"""Convert decimal values to more friendly fractions."""
if val is None:
return None
frac, val = math.modf( float( val ) )
if frac >= 0.875:
val = str( int(val) + 1 )
else:
val = str( int( val ) )
if frac >= 0.625:
val = val + "¾"
elif frac >= 0.375:
val = val + "½"
elif frac >= 0.125:
val = val + "¼"
if postfix:
if val == "0":
return "0 " + postfix2
elif val.startswith( "0&" ):
return val[1:] + " " + postfix
elif val == "1":
return "1 " + postfix
val = "{} {}".format( val, postfix2 )
return val[1:] if val.startswith( "0&" ) else val
# ---------------------------------------------------------------------
_MONTH_NAMES = [ # nb: we assume English :-/
"January", "February", "March", "April", "May", "June",
"July", "August", "September", "October", "November", "December"
]
_DAY_OF_MONTH_POSTFIXES = { # nb: we assume English :-/
0: "th",
1: "st", 2: "nd", 3: "rd", 4: "th", 5: "th", 6: "th", 7: "th", 8: "th", 9: "th", 10: "th",
11: "th", 12: "th", 13: "th"
}
def get_month_name( month ):
"""Return a month name."""
return _MONTH_NAMES[ month-1 ]
def make_formatted_day_of_month( dom ):
"""Generate a formatted day of the month."""
if dom in _DAY_OF_MONTH_POSTFIXES:
return str(dom) + _DAY_OF_MONTH_POSTFIXES[ dom ]
else:
return str(dom) + _DAY_OF_MONTH_POSTFIXES[ dom % 10 ]
# ---------------------------------------------------------------------
class SimpleError( Exception ):
"""Represents a simple error that doesn't require a stack trace (e.g. bad configuration)."""
pass