#!/usr/bin/python
# make_web_animation.py
# --copyright-- Copyright 2013 (C) Tranzoa, Co. All rights reserved. Warranty: You're free and on your own here. This code is not necessarily up-to-date or of public quality.
# --url-- http://www.tranzoa.net/tzpython/
# --email-- pycode is the name to send to. tranzoa.com is the place to send to.
# --bodstamps--
# February 25, 2014 bar
# February 26, 2014 bar clean up and add some IDs
# April 30, 2014 bar --controls works
# --of
# June 11, 2014 bar be less confusing on the cmd line
# change default duration from zero to 0.1 seconds
# --title
# step forw/back and zap buttons
# June 13, 2014 bar starting_num
# October 9, 2014 bar body attributes and head css
# October 12, 2014 bar keys
# October 15, 2014 bar flip play button
# mouse wheel - works on FF, modern chrome, IE 11, chromium-browser and webkit - not sure about safari and mobile browsers
# October 17, 2014 bar allow chrome and firefox users to click images to download them to their file names
# October 29, 2014 bar allow us to be told the image data in an_image()
# don't crash if the image has a url rather than a file name
# January 26, 2015 bar scale image to window
# January 27, 2015 bar call resize inside the a_player creator
# February 4, 2015 bar give a little extra width room in resize
# February 13, 2015 bar finally fix the problem with resize starting out - by explicitly setting the image w/h values
# March 8, 2015 bar jsl applied
# March 24, 2015 bar more autoscale extra hite
# step left rite changes play direction
# make the header font size 150% rather than its default 200%
# March 25, 2015 bar make the play/pause button the same size as the others
# flip the pause button when going backward
# March 28, 2015 bar remove the me undefined from chromium browser
# strip the javascript
# March 29, 2015 bar space before the style:none for the images
# March 30, 2015 bar show_image() and hide_image()
# March 31, 2015 bar safety those rtns
# April 1, 2015 bar page visibility stop/starter
# April 2, 2015 bar start_timeout
# don't allow scaling by browser window to such and extent as before (which was .1 and 2.0)
# set_javascript()
# April 3, 2015 bar !==, not !=
# April 4, 2015 bar remove IE fuss
# April 29, 2015 bar note the PIL image for images - make it at __init__ time?
# August 4, 2015 bar use tzlib to sense PIL images
# March 4, 2023 bar python3
# --eodstamps--
## \file
# \namespace tzpython.make_web_animation
#
#
"""
Make an animation from a bunch of image files and timings for each.
"""
from __future__ import print_function
import base64
import functools
from io import BytesIO
import mimetypes
import os
import random
import re
import replace_file
import sys
from PIL import Image
import output_files
import strip_files
import tzlib
PLAYER_EXTRA_WIDTH = 20 #: extra pixels to take horizontally when auto-sizing
PLAYER_EXTRA_HITE = 50 #: extra pixels to take vertically when auto-sizing
HTML_BODY_HDR = """
%s
"""
HTML_HDR = """
""" + HTML_BODY_HDR
html_body_trl = """
"""
HTML_TRL = html_body_trl + """
"""
JAVASCRIPT_SETTINGS = """
A_PLAYER_EXTRA_WIDTH = %u;
A_PLAYER_EXTRA_HITE = %u;
A_PLAYER_MIN_RESIZE = 0.25; /* how much smaller window resizing can scale the image */
A_PLAYER_MAX_RESIZE = 1.25; /* how much bigger window resizing can scale the image */
"""
JAVASCRIPT_CODE = """
function a_page_visibler(page_invisible_rtn, page_visible_rtn, thang)
{
var me = this;
me.gone_rtn = page_invisible_rtn || function(t){};
me.visi_rtn = page_visible_rtn || function(t){};
me.thang = thang;
/*** Code from http://www.html5rocks.com/en/tutorials/pagevisibility/intro/ ***/
me.getHiddenProp = function()
{
var prefixes = ['webkit','moz','ms','o'];
/*** if 'hidden' is natively supported just return it ***/
if ('hidden' in document)
{
return('hidden');
}
/*** otherwise loop over all the known prefixes until we find one ***/
for (var i = 0; i < prefixes.length; i++)
{
if ((prefixes[i] + 'Hidden') in document)
{
return(prefixes[i] + 'Hidden');
}
}
/*** otherwise it's not supported ***/
return(null);
};
me.is_hidden = function()
{
var prop = getHiddenProp();
if (!prop)
{
return(false);
}
return(document[prop]);
};
me.on_vis_change = function()
{
if (me.is_hidden())
{
me.gone_rtn(me.thang);
}
else
{
me.visi_rtn(me.thang);
}
};
/*** use the property name to generate the prefixed event name ***/
me.visProp = getHiddenProp();
if (me.visProp)
{
document.addEventListener(me.visProp.replace(/[H|h]idden/,'') + 'visibilitychange', on_vis_change);
}
return(me);
}
function a_player(img_ids, ptyp)
{
var me = this;
me.ptyp = ptyp;
me.pi = -1;
me.pd = 1;
me.ia = [];
me._stp = false;
me.body = document.getElementById('page_body');
me.div = document.getElementById('tzanimation_body');
me.imgs = document.getElementById('tzanimation_images');
me.pbtn = document.getElementById('tzanimation_play_btn');
me.sbtn = document.getElementById('tzanimation_stop_btn');
me.inel = document.getElementById('tzanimation_image_num');
me.ctls = document.getElementById('tzanimation_controls');
me.inst = 0;
me.inum = 0;
me._rsz = false;
if (me.inel)
{
me.inum = me.inel.innerHTML;
}
for (var i in img_ids)
{
me.ia[i] = document.getElementById(img_ids[i]);
me.ia[i].org_w = me.ia[i].width;
me.ia[i].org_h = me.ia[i].height;
}
me.start_timeout = null;
me.clear_start_timeout = function()
{
if (me.start_timeout)
{
var tmout = me.start_timeout;
me.start_timeout = null;
window.clearTimeout(tmout);
}
};
me.resize = function()
{
if (me.body)
{
var w = me.body.clientWidth - A_PLAYER_EXTRA_WIDTH;
var h = me.body.clientHeight - me.imgs.offsetTop - (me.inel ? me.inel.offsetHeight : 0) - (me.ctls ? me.ctls.clientHeight : 0) - A_PLAYER_EXTRA_HITE;
for (var i in me.ia)
{
var sf = Math.max(A_PLAYER_MIN_RESIZE, Math.min(A_PLAYER_MAX_RESIZE, w / (0.0 + me.ia[i].org_w), h / (0.0 + me.ia[i].org_h)));
me.ia[i].style.width = sf * me.ia[i].org_w;
me.ia[i].style.height = sf * me.ia[i].org_h;
}
}
};
me.set_image_num = function()
{
if (me.inel)
{
me.inel.innerHTML = "" + (+me.inum + me.pi);
}
};
me.timer = null;
me.clear_timeout = function()
{
if (me.timer)
{
var timer = me.timer;
me.timer = null;
window.clearTimeout(timer);
}
};
me.set_play_button = function(pd)
{
me.pd = pd;
if (me.pbtn)
{
if (me.pd < 0)
{
if (me.pbtn.className.indexOf(" flip_horizontal") < 0)
{
me.pbtn.className += " flip_horizontal";
}
if (me.sbtn.className.indexOf(" flip_horizontal") < 0)
{
me.sbtn.className += " flip_horizontal";
}
}
else
{
me.pbtn.className = me.pbtn.className.replace(" flip_horizontal", "");
me.sbtn.className = me.pbtn.className.replace(" flip_horizontal", "");
}
}
};
me.maybe_force_play_button_direction = function()
{
if (me.ptyp === 'ping_pong')
{
if (me.pi <= 0)
{
me.set_play_button( 1);
}
else if (me.pi >= me.ia.length - 1)
{
me.set_play_button(-1);
}
}
};
me.hide_image = function()
{
if (me.pi >= 0)
{
me.ia[me.pi].style.display = 'none';
}
};
me.show_image = function()
{
if (me.pi >= 0)
{
me.ia[me.pi].style.display = '';
}
};
me.show_next = function()
{
me.clear_timeout();
if ((me.pi >= 0) && (me.pi < me.ia.length))
{
me.hide_image();
}
me.pi += me.pd;
if (me.pi < 0)
{
me.pi = 1; /* must be ping pong */
me.set_play_button(1);
}
if (me.pi >= me.ia.length)
{
if (me.ptyp === 'once')
{
me.stop();
}
else if (me.ptyp === 'ping_pong')
{
me.pi = me.ia.length - 2;
me.set_play_button(-1);
}
else
{
me.pi = 0; /* loop */
}
}
me.pi = Math.max(0, Math.min(me.ia.length - 1, me.pi));
me.maybe_force_play_button_direction();
if (me.pi < me.ia.length)
{
me.show_image();
me.set_image_num();
}
if (!me._rsz)
{
me._rsz = true;
me.resize();
}
if (!me._stp)
{
me.timer = window.setTimeout(me.show_next, Math.max(20, me.ia[me.pi].getAttribute('duration') * 1000));
}
};
me.play = function()
{
me.clear_start_timeout();
if (me.pbtn) { me.pbtn.style.display = 'none'; }
if (me.sbtn) { me.sbtn.style.display = ''; }
me._stp = false;
me.show_next();
};
me.stop = function()
{
if (me.sbtn) { me.sbtn.style.display = 'none'; }
if (me.pbtn) { me.pbtn.style.display = ''; }
me._stp = true;
me.clear_timeout();
me.maybe_force_play_button_direction();
};
me.do_goto = function(pi)
{
me.clear_start_timeout();
me.stop();
if ((me.pi >= 0) && (me.pi < me.ia.length))
{
me.hide_image();
}
me.pi = (pi + me.ia.length) % me.ia.length;
me.set_image_num();
me.show_image();
me.maybe_force_play_button_direction();
};
me.step = function(pd)
{
me.set_play_button(pd);
me.do_goto(me.pi + pd);
};
me.key_down = function(evt)
{
evt = evt || window.event;
var kc = (typeof evt.which == "number") ? evt.which : evt.keyCode;
switch (kc)
{
case 32 : /* space */
if (me._stp)
{
me.play();
}
else
{
me.stop();
}
break;
case 36 : /* home */
case 38 : /* up arrow */
me.do_goto(0);
break;
case 27 : /* esc */
me.do_goto(Math.floor(me.ia.length / 2));
break;
case 35 : /* end */
case 40 : /* down arrow */
me.do_goto(me.ia.length - 1);
break;
case 33 : /* page up */
case 37 : /* left arrow */
me.step(-1);
break;
case 34 : /* page down */
case 39 : /* rite arrow */
me.step(1);
break;
default :
/* alert("kc " + kc); */
return(true);
}
return(false);
};
if (me.body)
{
me.body.addEventListener("keydown", me.key_down);
}
else if (me.div)
{
me.div.addEventListener( "keydown", me.key_down);
me.div.focus(); /* note: this isn't needed, probably, and futhermore, it's an open question how to keep FF from dot-outlining the page when the image buttons are hit */
}
me.wheeling = function(evt)
{
evt = evt || window.event;
me.stop();
me.set_play_button(1);
var d = evt.deltaY ? evt.deltaY : (evt.wheelDelta ? -evt.wheelDelta : 0);
if (d < 0)
{
if (me.pi)
{
me.do_goto(me.pi - 1);
}
}
else if (d > 0)
{
if (me.pi < me.ia.length - 1)
{
me.do_goto(me.pi + 1);
}
}
me.set_play_button(1);
me.maybe_force_play_button_direction();
return(false);
};
if (me.imgs)
{
if ("onwheel" in me.imgs)
{
me.imgs.addEventListener("wheel", me.wheeling); /* normal */
}
else if ("onmousewheel" in me.imgs)
{
me.imgs.addEventListener("mousewheel", me.wheeling); /* webkit and old FF and definately IE 11 and chromium-browser */
}
else
{
me.imgs.addEventListener("wheel", me.wheeling); /* bail */
me.imgs.addEventListener("mousewheel", me.wheeling);
}
}
me.save_image_as = function(id) /* This function allows the user to click on the images to download them with the appropriate file name. They can still saveAs, but without a suggested file name. */
{
me.stop();
var e = document.getElementById(id);
var a = document.createElement('a');
document.body.appendChild(a); /* needed for firefox */
a.download = e.getAttribute('download');
a.href = e.src;
a.click();
document.body.removeChild(a);
return(true);
};
me.resize();
me.auto_paused = false;
me.do_auto_pause = function(t)
{
me.auto_paused = !me._stp;
me.stop();
};
me.do_auto_resume = function(t)
{
if (me.auto_paused)
{
me.play();
}
};
me.visier = a_page_visibler(me.do_auto_pause, me.do_auto_resume, me);
return(me);
}
"""
JAVASCRIPT = ""
STRIPPED_JAVASCRIPT = ""
JAVASCRIPT_HTML = ""
def set_javascript() :
global JAVASCRIPT, STRIPPED_JAVASCRIPT, JAVASCRIPT_HTML
JAVASCRIPT = (JAVASCRIPT_SETTINGS % ( PLAYER_EXTRA_WIDTH, PLAYER_EXTRA_HITE, )) + JAVASCRIPT_CODE
STRIPPED_JAVASCRIPT = strip_files.strip_curly_string(JAVASCRIPT)[0]
JAVASCRIPT_HTML = """
""" % STRIPPED_JAVASCRIPT
pass
set_javascript()
IMAGE_NUM = """Image number %i
\n""" # current image number to show we're showing
CONTROLS = """
"""
class an_image(object) :
""" Class for images. File name or URL and durations. """
def __init__(me, fn_url, id = None, x = 0, y = 0, z = 0, alpha = 255, duration = None, img = None) :
""" Make an object. """
me.fn_url = fn_url
me.id = (((not id) or (not tzlib.is_stringish(id))) and ("tzanimation_%08u" % (((id is None) and random.randint(1, 99999998)) or abs(id) or 0))) or id
me.x = x or 0
me.y = y or 0
me.z = z or 0
me.alpha = ((alpha is None) and 255) or max(0, min(255, alpha)) #: unused
me.duration = duration or 0
me.img = img #: cStringIO or PIL Image or string with the image data such as would have been read from a file
me.pil_img = None #: PIL image if known
def set_duration(me, duration = None) :
""" Set/get the duration. """
ov = me.duration
if duration != None :
me.duration = duration
return(ov)
def set_xyz(me, x = None, y = None, z = None) :
""" Set/get the unused XYZ location of the image. """
ov = [ me.x, me.y, me.z ]
if x != None : me.x = x
if y != None : me.y = y
if z != None : me.z = z
return(ov)
def to_img_htm(me) :
""" Return an HTML string for this image as an IMG element. """
fd = me.fn_url
dlhtm = ""
if not re.search(r"^[a-z]{2,}://", me.fn_url) :
fd = None
if me.img :
if tzlib.is_stringish(me.img) : # already in string form, read from an image file?
fd = me.img
elif tzlib.is_pil_image(me.img) : # PIL image?
fd = me.img
elif hasattr(me.img, 'getvalue') : # stringio?
fd = me.img.getvalue()
elif hasattr(me.img, 'read') : # file-like object? After some thought, I figured it's best to not seek back to where we are in the file now.
fd = me.img.read()
pass
if not fd :
fd = tzlib.safe_read_whole_binary_file(me.fn_url)
if not fd :
raise ValueError("No file or content in %s" % me.fn_url)
t, e = mimetypes.guess_type(me.fn_url)
if t is None :
raise ValueError("No MIME type for %s" % me.fn_url)
ec = ((e != None) and (";" + e)) or ""
if tzlib.is_pil_image(fd) :
img = fd # fd is me.img - use it directly, picking up the file name from the fn_url to get the mimetype and encoding
else :
img = Image.open(BytesIO(fd))
me.pil_img = img # save it for handy, dandy use by others (March 5, 2023 Could not find any others)
fd = base64.b64encode(fd)
fd = fd.decode('latin1')
fd = "data:%s%s;base64,%s" % ( t, ec, fd, )
dlhtm = """ download='%s' onclick='me.save_image_as("%s");' """ % ( os.path.basename(me.fn_url), me.id, )
s = "" % ( me.id, me.duration, img.size[0], img.size[1], dlhtm, fd or "", )
return(s)
# an_image
def image_html(images) :
""" Return the HTML for the given images. """
html = "\n"
for img in images :
html += img.to_img_htm() + "\n"
html += "
\n"
return(html)
def runner_html(images, play_type = None) :
""" Return HTML for the executive control, running logic. """
images = images or []
ids = [ img.id for img in images ]
html = """
""" % ( repr(ids), play_type or 'once', )
return(html)
def get_title_htm(title) :
""" Return a element if wanted. """
return((title and re.sub(r'\s+', ' ', ("%s" % tzlib.de_html_str(title)))) or "")
def get_h1_htm(h1) :
""" Return a element if wanted. """
return((h1 and ("%s
" % h1)) or "")
def to_html(images, play_type = 'once', image_num_htm = None, controls_htm = None, title_htm = None, head_css = None, body_attributes = None) :
"""
Make an HTML file in string form from the given an_images.
'play_type' can be 'once', 'loop', or 'ping_pong'.
The 'controls' can be CONTROLS to show a play/pause/step/zap buttons.
"""
html = HTML_HDR % ( head_css or "", body_attributes or "", get_title_htm(title_htm) + get_h1_htm(title_htm, ))
html += JAVASCRIPT_HTML
html += image_html(images)
html += (image_num_htm or "")
html += (controls_htm or "")
html += runner_html(images, play_type)
html += HTML_TRL
if tzlib.is_unicode(html) :
html = html.encode('utf8')
return(html)
help_str = """
%s images_and_options...
Make the equivalent of an animated GIF, allowing duped images.
Options:
--title HTML_text Make the given text the web page title and H1.
Implies --test Output stand-alone web page.
--duration seconds How long to show the latest image given in the command line. (default: %.1f)
--loop Loop the animation.
--ping_pong Ping pong the animation.
--image_number number Include an image number starting with the given number.
--controls Include a play/stop button.
--output file_name Output to the given HTML file rather than to stdout.
--of before(,after) ambiguous_file_name
Find the latest command line image file (or next one, if none)
in the files matching the 'ambiguous_file_name'.
Include in the animation 'before' files before that image and 'after' files after.
If 'after' not given, use the 'before' value.
--body_attributes html Set any attributes needed.
--head_css css Insert the given css in .
--test Output stand-alone web page.
--verbose Increase the verbosity.
This program prints or creates a browser-based, JavaScript-controlled, animated image.
"""
if __name__ == '__main__' :
import glob
import TZCommandLineAtFile
program_name = sys.argv.pop(0)
TZCommandLineAtFile.expand_at_sign_command_line_files(sys.argv)
images = []
ptyp = 'once'
ofile_name = None
duration = 0.1
test = 0
of_files = None
title = None
controls = ""
image_num = ""
body_attributes = None
head_css = None
verbose = 0
class an_of(object) :
def __init__(me, before, after, amb_fn, files) :
me.before = before
me.after = after
me.amb_fn = amb_fn
me.files = files
def replace_image(me, images) :
ffn = images.pop().fn_url
fnd = False
for i, fn in enumerate(me.files) :
if tzlib.same_file(fn, ffn) :
for i in range(max(0, i - me.before), i + me.after + 1) :
images.append(an_image(me.files[i], duration = duration))
fnd = True
break
pass
if not fnd :
print("File [%s] not found in [%s]!" % ( ffn, me.of_files, ), file = sys.stderr)
sys.exit(103)
pass
# an_of
while len(sys.argv) :
a = sys.argv.pop(0)
if tzlib.array_find([ "--help", "-h", "-?", "/h", "/H", "/?" ], a) >= 0 :
print(help_str % ( os.path.basename(program_name), duration, ))
sys.exit(254)
if False :
pass
elif tzlib.array_find([ "--verbose", "-v", ], a) >= 0 :
verbose += 1
elif tzlib.array_find([ "--test", ], a) >= 0 :
test += 1
elif tzlib.array_find([ "--duration", "-d", ], a) >= 0 :
duration = float(sys.argv.pop(0))
elif tzlib.array_find([ "--loop", "-l", ], a) >= 0 :
ptyp = 'loop'
elif tzlib.array_find([ "--ping_pong", "--ping-pong", "--pingpong", "--pp", ], a) >= 0 :
ptyp = 'ping_pong'
elif tzlib.array_find([ "--image_num", "--image-num", "--imagenum", "--num", ], a) >= 0 :
image_num = IMAGE_NUM % int(sys.argv.pop(0))
elif tzlib.array_find([ "--controls", "-c", ], a) >= 0 :
controls = CONTROLS
elif tzlib.array_find([ "--body_attributes", "--body-attributes", "--bodyattributes", "--body_attrib", "--body-attrib", "--bodyattrib", "--body_attr", "--body-attr", "--bodyattr", "--ba", ], a) >= 0 :
body_attributes = sys.argv.pop(0)
elif tzlib.array_find([ "--head_css", "--head-css", "--headcess", "--css", ], a) >= 0 :
head_css = sys.argv.pop(0)
elif tzlib.array_find([ "--output", "-o", ], a) >= 0 :
if ofile_name :
print("Only 1 output file per customer [%s]!" % a, file = sys.stderr)
sys.exit(101)
ofile_name = sys.argv.pop(0)
elif tzlib.array_find([ "--title", ], a) >= 0 :
title = sys.argv.pop(0)
test += 1
elif os.path.splitext(a)[1].lower() in [ '.htm', '.html', ] :
if ofile_name :
print("Only 1 output file per customer [%s]!" % a, file = sys.stderr)
sys.exit(101)
ofile_name = a
elif tzlib.array_find([ "--of", ], a) >= 0 :
nn = sys.argv.pop(0)
ft = [ int(n.strip()) for n in nn.split(',') ]
if len(ft) == 1 :
ft.append(ft[0])
afn = sys.argv.pop(0)
fns = glob.glob(tzlib.expand_user_vars(afn))
if not len(fns) :
print("No files found for ambiguous name [%s]!" % afn, file = sys.stderr)
sys.exit(102)
fns.sort(key = functools.cmp_to_key(tzlib.cmp_lower_str_with_ints))
if of_files :
print("Two --of images for one image file [%s]!" % afn, file = sys.stderr)
sys.exit(102)
of_files = an_of(ft[0], ft[1], afn, fns)
if len(images) :
of_files.replace_image(images)
of_files = None
pass
else :
fns = tzlib.ambiguous_file_list(a)
fns.sort(key = functools.cmp_to_key(tzlib.cmp_lower_str_with_ints))
for fn in fns :
images.append(an_image(fn, duration = duration))
if of_files :
of_files.replace_image(images)
of_files = None
pass
pass
pass
if not len(images) :
print("No images to put in the animation!", file = sys.stderr)
sys.exit(104)
tfn = None
fo = sys.stdout
if ofile_name :
if test :
fo = output_files.a_file(ofile_name)
fo.write(HTML_BODY_HDR % ( head_css or "", body_attributes or "", get_title_htm(title) + get_h1_htm(title), ))
else :
tfn = ofile_name + ".tmp"
fo = open(tfn, "wt")
pass
elif test :
fo.write(HTML_HDR % ( head_css or "", body_attributes or "", get_title_htm(title) + get_h1_htm(title), ))
if test :
fo.write(JAVASCRIPT_HTML)
fo.write(image_html(images))
fo.write(image_num or "")
fo.write(controls or "")
fo.write(runner_html(images, ptyp))
if test and (not ofile_name) :
fo.write(HTML_TRL)
if ofile_name :
if test :
fo.write(html_body_trl)
fo.close()
if tfn :
replace_file.replace_file(ofile_name, tfn, ofile_name + ".bak")
pass
pass
#
#
# eof