2014-09-23 22:50:59 +00:00
|
|
|
#!/usr/bin/env python2.7
|
|
|
|
# -*- coding: utf-8 -*-
|
|
|
|
|
|
|
|
# callirhoe - high quality calendar rendering
|
|
|
|
# Copyright (C) 2012-2014 George M. Tzoumas
|
|
|
|
|
|
|
|
# This program is free software: you can redistribute it and/or modify
|
|
|
|
# it under the terms of the GNU General Public License as published by
|
|
|
|
# the Free Software Foundation, either version 3 of the License, or
|
|
|
|
# (at your option) any later version.
|
|
|
|
#
|
|
|
|
# This program is distributed in the hope that it will be useful,
|
|
|
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
|
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
|
|
# GNU General Public License for more details.
|
|
|
|
#
|
|
|
|
# You should have received a copy of the GNU General Public License
|
|
|
|
# along with this program. If not, see http://www.gnu.org/licenses/
|
|
|
|
|
|
|
|
# *****************************************************************
|
|
|
|
# #
|
|
|
|
""" high quality photo calendar composition using Imagemagick """
|
|
|
|
# #
|
|
|
|
# *****************************************************************
|
|
|
|
|
|
|
|
import sys
|
|
|
|
import subprocess
|
|
|
|
import os.path
|
2014-09-25 23:32:39 +00:00
|
|
|
import os
|
|
|
|
import tempfile
|
|
|
|
import glob
|
|
|
|
import random
|
2014-09-27 16:55:34 +00:00
|
|
|
import optparse
|
2014-09-23 22:50:59 +00:00
|
|
|
|
2014-09-25 10:37:09 +00:00
|
|
|
from callirhoe import extract_parser_args, parse_month_range, parse_year
|
2014-09-27 16:55:34 +00:00
|
|
|
from lib.geom import rect_rel_scale
|
2014-09-23 22:50:59 +00:00
|
|
|
|
2014-09-26 12:09:55 +00:00
|
|
|
# TODO:
|
|
|
|
# cache stuff when --sample is used, move --sample to filedef, like '*.jpg:0'
|
2014-09-27 16:55:34 +00:00
|
|
|
# fork processes (independent stuff...)
|
2014-09-26 12:09:55 +00:00
|
|
|
|
2014-09-23 22:50:59 +00:00
|
|
|
def run_callirhoe(style, w, h, args, outfile):
|
2014-09-25 10:37:09 +00:00
|
|
|
if subprocess.call(['callirhoe', '-s', style, '--paper=-%d:-%d' % (w,h)] + args + [outfile]):
|
|
|
|
sys.exit("calmagick: calendar creation failed")
|
2014-09-23 22:50:59 +00:00
|
|
|
|
|
|
|
class PNMImage(object):
|
|
|
|
def __init__(self, strlist):
|
|
|
|
self.data = [];
|
|
|
|
state = 0;
|
|
|
|
for i in range(len(strlist)):
|
|
|
|
# skip comments
|
|
|
|
if strlist[i].startswith('#'): continue
|
|
|
|
# parse header
|
|
|
|
if state == 0:
|
|
|
|
if not strlist[i].startswith('P2'):
|
2014-09-24 12:34:28 +00:00
|
|
|
raise RuntimeError('invalid PNM image format: %s' % strlist[i])
|
2014-09-23 22:50:59 +00:00
|
|
|
state += 1
|
|
|
|
# parse size
|
|
|
|
elif state == 1:
|
|
|
|
w,h = map(int,strlist[i].split())
|
|
|
|
if w != h:
|
|
|
|
raise RuntimeError('non-square PNM image')
|
|
|
|
self.size = (w,h)
|
|
|
|
state += 1
|
|
|
|
# parse max value
|
|
|
|
elif state == 2:
|
|
|
|
self.maxval = int(strlist[i])
|
|
|
|
state += 1
|
|
|
|
# bitmap
|
|
|
|
else:
|
|
|
|
data = ' '.join(filter(lambda s: not s.startswith('#'), strlist[i:]))
|
|
|
|
intlist = map(int,data.split())
|
|
|
|
self.data = [intlist[x:x+w] for x in range(0, len(intlist), w)]
|
|
|
|
break
|
|
|
|
|
|
|
|
self.xsum = [map(lambda x: sum(self.data[y][0:x]), range(w+1)) for y in range(0,h)]
|
|
|
|
|
|
|
|
def block_avg(self, x, y, sz):
|
|
|
|
return float(sum([(self.xsum[y][x+sz] - self.xsum[y][x]) for y in range(y,y+sz)]))/(sz*sz)
|
|
|
|
|
2014-09-24 18:20:40 +00:00
|
|
|
def lowest_block_avg(self, sz, at_least = 0):
|
|
|
|
best = (self.maxval,1,0,0,sz) # avg, sz_ratio, x, y, sz
|
2014-09-23 22:50:59 +00:00
|
|
|
w,h = self.size
|
2014-09-24 18:20:40 +00:00
|
|
|
for y in range(0,h-sz+1):
|
|
|
|
for x in range(0,w-sz+1):
|
|
|
|
cur = (self.block_avg(x,y,sz), float(sz)/w, x, y, sz)
|
|
|
|
if cur[0] < best[0]:
|
|
|
|
best = cur
|
|
|
|
if best[0] <= at_least: return best
|
2014-09-23 22:50:59 +00:00
|
|
|
return best
|
|
|
|
|
2014-09-24 18:20:40 +00:00
|
|
|
def fit_rect(self, size_range = (0.333, 0.8), at_least = 7, relax = 0.2):
|
|
|
|
w,h = self.size
|
|
|
|
sz_range = (int(w*size_range[0]+0.5), int(w*size_range[1]+0.5))
|
2014-09-25 23:32:39 +00:00
|
|
|
best = self.lowest_block_avg(sz_range[0])
|
|
|
|
# we do not use at_least because non-global minimum, when relaxed, may jump well above threshold
|
|
|
|
entropy_thres = max(at_least, best[0]*(1+relax))
|
2014-09-24 18:20:40 +00:00
|
|
|
for sz in range(sz_range[1],sz_range[0],-1):
|
2014-09-25 23:32:39 +00:00
|
|
|
# we do not use at_least because we want the best possible option, for bigger sizes
|
|
|
|
cur = self.lowest_block_avg(sz)
|
|
|
|
if cur[0] <= entropy_thres: return cur + (best[0],)
|
2014-09-24 18:20:40 +00:00
|
|
|
return best + (best[0],) # avg, sz_ratio, x, y, sz, best_avg
|
|
|
|
|
2014-09-23 22:50:59 +00:00
|
|
|
_version = "0.1.0"
|
|
|
|
|
|
|
|
def get_parser():
|
|
|
|
"""get the argument parser object"""
|
2014-09-24 21:07:47 +00:00
|
|
|
parser = optparse.OptionParser(usage="usage: %prog IMAGE [options] [callirhoe-options] [--pre-magick ...] [--in-magick ...] [--post-magick ...]",
|
2014-09-26 12:09:55 +00:00
|
|
|
description="""High quality photo calendar composition with automatic minimal-entropy placement.
|
2014-09-27 16:55:34 +00:00
|
|
|
If IMAGE is a single file, then a calendar of the current month is overlayed. If IMAGE contains wildcards,
|
|
|
|
then every month is generated according to the --range option, advancing one month for every photo file.
|
2014-09-26 12:09:55 +00:00
|
|
|
Photos will be reused in a round-robin fashion if more calendar
|
|
|
|
months are requested.""", version="calmagick " + _version)
|
2014-09-25 23:32:39 +00:00
|
|
|
parser.add_option("--outdir", default=".",
|
|
|
|
help="set directory for the output image(s); directory will be created if it does not already exist [%default]")
|
2014-09-28 14:38:30 +00:00
|
|
|
parser.add_option("--outfile", default=None,
|
|
|
|
help="set output filename when no --range is requested; by default will use the same name, unless it is going to "
|
|
|
|
"overwrite the input image, in which case suffix '_calmagick' will be added; this option will override --outdir and --format options")
|
|
|
|
parser.add_option("--prefix", type="choice", choices=['no','auto','yes'], default='auto',
|
|
|
|
help="set output filename prefix for multiple image output (with --range); 'no' means no prefix will be added, thus the output "
|
|
|
|
"filename order may not be the same, if the input photos are randomized (--shuffle or --sample); "
|
|
|
|
"'auto' adds YEAR_MONTH_ prefix only when input photos are randomized; 'yes' will always add prefix [%default]")
|
2014-09-23 22:50:59 +00:00
|
|
|
parser.add_option("--quantum", type="int", default=60,
|
|
|
|
help="choose quantization level for entropy computation [%default]")
|
2014-09-26 12:09:55 +00:00
|
|
|
parser.add_option("--placement", type="choice", choices="min max N S W E NW NE SW SE center random".split(),
|
|
|
|
default="min", help="choose placement algorithm among {min, max, "
|
|
|
|
"N, S, W, E, NW, NE, SW, SE, center, random} [%default]")
|
2014-09-27 16:55:34 +00:00
|
|
|
parser.add_option("--min-size", type="float", default=None,
|
|
|
|
help="for min/max/random placement: set minimum calendar/photo size ratio [0.333]; for "
|
|
|
|
"N,S,W,E,NW,NE,SW,SE placement: set margin/opposite-margin size ratio [0.05]; for "
|
|
|
|
"center placement it has no effect")
|
2014-09-23 22:50:59 +00:00
|
|
|
parser.add_option("--max-size", type="float", default=0.8,
|
|
|
|
help="set maximum calendar/photo size ratio [%default]")
|
2014-09-24 18:20:40 +00:00
|
|
|
parser.add_option("--low-entropy", type="float", default=7,
|
|
|
|
help="set minimum entropy threshold (0-255) for early termination (0=global minimum) [%default]")
|
|
|
|
parser.add_option("--relax", type="float", default=0.2,
|
|
|
|
help="relax minimum entropy multiplying by 1+RELAX, to allow for bigger sizes [%default]")
|
2014-09-24 10:33:33 +00:00
|
|
|
parser.add_option("--negative", type="float", default=100,
|
|
|
|
help="average luminosity (0-255) threshold of the overlaid area, below which a negative "
|
|
|
|
"overlay is chosen [%default]")
|
2014-09-26 12:40:55 +00:00
|
|
|
parser.add_option("--test", type="choice", choices="none area quant print crop".split(), default='none',
|
|
|
|
help="test entropy minimization algorithm, without creating any calendar, TEST should be among "
|
|
|
|
"{none, area, quant, print, crop}: none=test disabled; "
|
2014-09-27 16:55:34 +00:00
|
|
|
"area=show area in original image; quant=show area in quantizer; print=print minimum entropy area in STDOUT as W H X Y, "
|
2014-09-26 12:40:55 +00:00
|
|
|
"without generating any files at all; crop=crop selected area [%default]")
|
2014-09-24 10:33:33 +00:00
|
|
|
parser.add_option("-v", "--verbose", action="store_true", default=False,
|
|
|
|
help="print progress messages")
|
|
|
|
|
2014-09-25 10:37:09 +00:00
|
|
|
cal = optparse.OptionGroup(parser, "Calendar Options", "These options determine how callirhoe is invoked.")
|
|
|
|
cal.add_option("-s", "--style", default="transparent",
|
|
|
|
help="calendar default style [%default]")
|
|
|
|
cal.add_option("--range", default=None,
|
2014-09-26 12:09:55 +00:00
|
|
|
help="""set month range for calendar. Format is MONTH/YEAR or MONTH1-MONTH2/YEAR or
|
|
|
|
MONTH:SPAN/YEAR. If set, these arguments will be expanded (as positional arguments for callirhoe)
|
|
|
|
and a calendar will be created for
|
|
|
|
each month separately, for each input photo. Photo files will be globbed by the script
|
|
|
|
and used in a round-robin fashion if more months are requested. Globbing means that you should
|
|
|
|
normally enclose the file name in single quotes like '*.jpg' in order to avoid shell expansion.
|
|
|
|
If less months are requested, then the calendar
|
2014-09-28 14:38:30 +00:00
|
|
|
making process will terminate without having used all available photos. SPAN=0 will match the number of input
|
|
|
|
photos.""")
|
2014-09-25 23:32:39 +00:00
|
|
|
cal.add_option("--sample", type="int", default=None,
|
|
|
|
help="choose SAMPLE random images from the input and use in round-robin fashion (see --range option); if "
|
|
|
|
"SAMPLE=0 then the sample size is chosen to be equal to the month span defined with --range")
|
2014-09-28 14:38:30 +00:00
|
|
|
cal.add_option("--shuffle", action="store_true", default=False,
|
|
|
|
help="shuffle input images and to use in round-robin fashion (see --range option); "
|
|
|
|
"the sample size is chosen to be equal to the month span defined with --range; this "
|
|
|
|
"is equivalent to specifying --sample=0")
|
2014-09-25 10:37:09 +00:00
|
|
|
cal.add_option("--vanilla", action="store_true", default=False,
|
|
|
|
help="suppress default options --no-footer --border=0")
|
|
|
|
parser.add_option_group(cal)
|
|
|
|
|
2014-09-24 10:33:33 +00:00
|
|
|
im = optparse.OptionGroup(parser, "ImageMagick Options", "These options determine how ImageMagick is used.")
|
2014-09-25 23:32:39 +00:00
|
|
|
im.add_option("--format", default="",
|
|
|
|
help="determines the file extension (without dot!) of the output image files; "
|
|
|
|
"use this option to generate files in a different format than the input, for example "
|
|
|
|
"to preserve quality by generating PNG from JPEG, thus not recompressing")
|
2014-09-24 10:33:33 +00:00
|
|
|
im.add_option("--brightness", type="int", default=10,
|
|
|
|
help="increase/decrease brightness by this (percent) value; "
|
2014-09-23 22:50:59 +00:00
|
|
|
"brightness is decreased on negative overlays [%default]")
|
2014-09-24 10:33:33 +00:00
|
|
|
im.add_option("--saturation", type="int", default=100,
|
|
|
|
help="set saturation of the overlaid area "
|
2014-09-23 22:50:59 +00:00
|
|
|
"to this value (percent) [%default]")
|
2014-09-26 12:09:55 +00:00
|
|
|
# im.add_option("--radius", type="float", default=2,
|
|
|
|
# help="radius for the entropy computation algorithm [%default]")
|
2014-09-24 10:33:33 +00:00
|
|
|
im.add_option("--pre-magick", action="store_true", default=False,
|
2014-09-24 21:07:47 +00:00
|
|
|
help="pass all subsequent arguments to ImageMagick, before entropy computation; should precede --in-magick and --post-magick")
|
2014-09-24 10:33:33 +00:00
|
|
|
im.add_option("--in-magick", action="store_true", default=False,
|
2014-09-24 21:07:47 +00:00
|
|
|
help="pass all subsequent arguments to ImageMagick, to be applied on the minimal-entropy area; should precede --post-magick")
|
2014-09-24 10:33:33 +00:00
|
|
|
im.add_option("--post-magick", action="store_true", default=False,
|
|
|
|
help="pass all subsequent arguments to ImageMagick, to be applied on the final output")
|
|
|
|
parser.add_option_group(im)
|
2014-09-23 22:50:59 +00:00
|
|
|
return parser
|
|
|
|
|
2014-09-28 14:38:30 +00:00
|
|
|
def check_parsed_options(options):
|
|
|
|
if options.min_size is None:
|
|
|
|
options.min_size = 0.333 if options.placement in ['min','max','random'] else 0.05
|
|
|
|
if options.sample is not None and not options.range:
|
|
|
|
sys.exit("calmagick: --sample requested without --range")
|
|
|
|
if options.outfile is not None and options.range:
|
|
|
|
sys.exit("calmagick: you cannot specify both --outfile and --range options")
|
|
|
|
if options.sample is not None and options.shuffle:
|
|
|
|
sys.exit("calmagick: you cannot specify both --shuffle and --sample options")
|
|
|
|
if options.shuffle:
|
|
|
|
options.sample = 0
|
|
|
|
if options.sample is None:
|
|
|
|
if options.prefix == 'auto': options.prefix = 'no'
|
|
|
|
else:
|
|
|
|
if options.prefix == 'auto': options.prefix = 'yes'
|
|
|
|
|
2014-09-24 21:07:47 +00:00
|
|
|
def parse_magick_args():
|
|
|
|
magickargs = [[],[],[]]
|
2014-09-24 10:33:33 +00:00
|
|
|
try:
|
|
|
|
m = sys.argv.index('--post-magick')
|
2014-09-24 21:07:47 +00:00
|
|
|
magickargs[2] = sys.argv[m+1:]
|
|
|
|
del sys.argv[m:]
|
|
|
|
except:
|
|
|
|
pass
|
|
|
|
try:
|
|
|
|
m = sys.argv.index('--in-magick')
|
|
|
|
magickargs[1] = sys.argv[m+1:]
|
|
|
|
del sys.argv[m:]
|
|
|
|
except:
|
|
|
|
pass
|
|
|
|
try:
|
|
|
|
m = sys.argv.index('--pre-magick')
|
|
|
|
magickargs[0] = sys.argv[m+1:]
|
2014-09-24 10:33:33 +00:00
|
|
|
del sys.argv[m:]
|
|
|
|
except:
|
|
|
|
pass
|
2014-09-24 21:07:47 +00:00
|
|
|
if ('--post-magick' in magickargs[2] or '--in-magick' in magickargs[2] or
|
|
|
|
'--pre-magick' in magickargs[2] or '--in-magick' in magickargs[1] or
|
|
|
|
'--pre-magick' in magickargs[1] or '--pre-magick' in magickargs[0]):
|
|
|
|
parser.print_help()
|
|
|
|
sys.exit(0)
|
|
|
|
return magickargs
|
|
|
|
|
2014-09-25 23:32:39 +00:00
|
|
|
def mktemp(ext=''):
|
|
|
|
f = tempfile.NamedTemporaryFile(suffix=ext, delete=False)
|
|
|
|
f.close()
|
|
|
|
return f.name
|
2014-09-24 10:33:33 +00:00
|
|
|
|
2014-09-28 14:38:30 +00:00
|
|
|
def get_outfile(infile, outdir, base_prefix, format, hint=None):
|
|
|
|
if hint:
|
|
|
|
outfile = hint
|
|
|
|
else:
|
|
|
|
head,tail = os.path.split(infile)
|
|
|
|
base,ext = os.path.splitext(tail)
|
|
|
|
if format: ext = '.' + format
|
|
|
|
outfile = os.path.join(outdir,base_prefix+base+ext)
|
2014-09-25 23:32:39 +00:00
|
|
|
if os.path.exists(outfile) and os.path.samefile(infile, outfile):
|
2014-09-28 14:38:30 +00:00
|
|
|
if hint: sys.exit("calmagick: --outfile same as input, aborting")
|
2014-09-25 23:32:39 +00:00
|
|
|
outfile = os.path.join(outdir,base_prefix+base+'_calmagick'+ext)
|
|
|
|
return outfile
|
2014-09-23 22:50:59 +00:00
|
|
|
|
2014-09-27 16:55:34 +00:00
|
|
|
def _get_image_size(img, args):
|
|
|
|
info = subprocess.check_output(['convert', img] + args + ['-format', '%w %h', 'info:']).split()
|
|
|
|
return tuple(map(int, info))
|
2014-09-25 23:32:39 +00:00
|
|
|
|
2014-09-27 16:55:34 +00:00
|
|
|
def _get_image_luminance(img, args, geometry = None):
|
|
|
|
return 255.0*float(subprocess.check_output(['convert', img] + args +
|
|
|
|
(['-crop', '%dx%d+%d+%d' % geometry] if geometry else []) +
|
|
|
|
['-colorspace', 'Gray', '-format', '%[fx:mean]', 'info:']))
|
|
|
|
|
|
|
|
def _entropy_placement(img, size, args, options):
|
2014-09-24 10:33:33 +00:00
|
|
|
if options.verbose:
|
2014-09-23 22:50:59 +00:00
|
|
|
print "Calculating image entropy..."
|
2014-09-27 16:55:34 +00:00
|
|
|
qresize = '%dx%d!' % ((options.quantum,)*2)
|
|
|
|
pnm_entropy = PNMImage(subprocess.check_output(['convert', img] + args +
|
2014-09-26 12:09:55 +00:00
|
|
|
"-scale 512> -define convolve:scale=! -define morphology:compose=Lighten -morphology Convolve Sobel:> -colorspace Gray -normalize -unsharp 0x5 -scale".split() +
|
2014-09-27 16:55:34 +00:00
|
|
|
[qresize] + (['-negate'] if options.placement == 'max' else []) + ['-compress', 'None', 'pnm:-']).splitlines())
|
2014-09-26 12:09:55 +00:00
|
|
|
|
|
|
|
#-scale 512> ( -clone -blur 0x2 ) -compose minus -composite -colorspace Gray -normalize -unsharp 0x5 -scale 60x60! -normalize
|
|
|
|
#-scale 512> -define convolve:scale=! -define morphology:compose=Lighten -morphology Convolve Sobel:> -colorspace Gray -normalize -unsharp 0x5 -scale 60x60!
|
2014-09-24 18:20:40 +00:00
|
|
|
|
2014-09-25 23:32:39 +00:00
|
|
|
# find optimal fit
|
2014-09-24 10:33:33 +00:00
|
|
|
if options.verbose: print "Fitting... ",
|
2014-09-24 18:20:40 +00:00
|
|
|
best = pnm_entropy.fit_rect((options.min_size,options.max_size), options.low_entropy, options.relax)
|
2014-09-24 10:33:33 +00:00
|
|
|
if options.verbose: print best
|
2014-09-23 22:50:59 +00:00
|
|
|
|
2014-09-27 16:55:34 +00:00
|
|
|
# (W,H,X,Y)
|
|
|
|
w,h = size
|
|
|
|
geometry = tuple(map(int, (w*best[1], h*best[1],
|
|
|
|
float(w*best[2])/pnm_entropy.size[0],
|
|
|
|
float(h*best[3])/pnm_entropy.size[1])))
|
|
|
|
return geometry
|
|
|
|
|
|
|
|
def _manual_placement(size, options):
|
|
|
|
r = (0, 0, size[0], size[1])
|
|
|
|
if options.placement == 'random':
|
|
|
|
f = random.uniform(options.min_size, options.max_size)
|
|
|
|
r2 = rect_rel_scale(r, f, f, random.uniform(-1,1), random.uniform(-1,1))
|
|
|
|
else:
|
|
|
|
ax = ay = 0
|
|
|
|
if 'W' in options.placement: ax = -1 + 2.0*options.min_size
|
|
|
|
if 'E' in options.placement: ax = 1 - 2.0*options.min_size
|
|
|
|
if 'N' in options.placement: ay = -1 + 2.0*options.min_size
|
|
|
|
if 'S' in options.placement: ay = 1 - 2.0*options.min_size
|
|
|
|
r2 = rect_rel_scale(r, options.max_size, options.max_size, ax, ay)
|
|
|
|
return tuple(map(int,[r2[2], r2[3], r2[0], r2[1]]))
|
|
|
|
|
|
|
|
def compose_calendar(img, outimg, options, callirhoe_args, magick_args):
|
|
|
|
# get image info (dimensions)
|
|
|
|
if options.verbose: print "Extracting image info..."
|
|
|
|
w,h = _get_image_size(img, magick_args[0])
|
|
|
|
qresize = '%dx%d!' % ((options.quantum,)*2)
|
|
|
|
if options.verbose:
|
|
|
|
print "%s %dx%d %dmp" % (img, w, h, int(w*h/1000000.0+0.5))
|
|
|
|
|
|
|
|
if options.placement == 'min' or options.placement == 'max':
|
|
|
|
geometry = _entropy_placement(img, (w,h), magick_args[0], options)
|
|
|
|
else:
|
|
|
|
geometry = _manual_placement((w,h), options)
|
2014-09-23 22:50:59 +00:00
|
|
|
|
2014-09-26 12:40:55 +00:00
|
|
|
if options.test != 'none':
|
|
|
|
if options.test == 'area':
|
2014-09-27 16:55:34 +00:00
|
|
|
subprocess.call(['convert', img] + magick_args[0] + ['-region', '%dx%d+%d+%d' % geometry,
|
2014-09-25 23:32:39 +00:00
|
|
|
'-negate', outimg])
|
2014-09-26 12:40:55 +00:00
|
|
|
elif options.test == 'quant':
|
2014-09-26 12:09:55 +00:00
|
|
|
subprocess.call(['convert', img] + magick_args[0] +
|
|
|
|
"-scale 512> -define convolve:scale=! -define morphology:compose=Lighten -morphology Convolve Sobel:> -colorspace Gray -normalize -unsharp 0x5 -scale".split() +
|
2014-09-27 16:55:34 +00:00
|
|
|
[qresize, '-scale', '%dx%d!' % (w,h), '-region', '%dx%d+%d+%d' % geometry,
|
2014-09-25 23:32:39 +00:00
|
|
|
'-negate', outimg])
|
2014-09-26 12:40:55 +00:00
|
|
|
elif options.test == 'print':
|
2014-09-27 16:55:34 +00:00
|
|
|
print ' '.join(map(str,geometry))
|
2014-09-26 12:40:55 +00:00
|
|
|
elif options.test == 'crop':
|
2014-09-27 16:55:34 +00:00
|
|
|
subprocess.call(['convert', img] + magick_args[0] + ['-crop', '%dx%d+%d+%d' % geometry,
|
2014-09-26 12:40:55 +00:00
|
|
|
outimg])
|
2014-09-25 23:32:39 +00:00
|
|
|
return
|
2014-09-23 22:50:59 +00:00
|
|
|
|
2014-09-25 23:32:39 +00:00
|
|
|
# measure luminance
|
2014-09-27 16:55:34 +00:00
|
|
|
if options.verbose: print "Measuring luminance...",
|
2014-09-26 12:09:55 +00:00
|
|
|
if options.negative > 0 and options.negative < 255:
|
2014-09-27 16:55:34 +00:00
|
|
|
luma = _get_image_luminance(img, magick_args[0], geometry)
|
|
|
|
if options.verbose: print "(%s)" % luma,
|
2014-09-26 12:09:55 +00:00
|
|
|
else:
|
|
|
|
luma = 255 - options.negative
|
|
|
|
dark = luma < options.negative
|
|
|
|
if options.verbose: print "DARK" if dark else "LIGHT"
|
2014-09-23 22:50:59 +00:00
|
|
|
|
2014-09-25 23:32:39 +00:00
|
|
|
# generate callirhoe calendar
|
|
|
|
if options.verbose: print "Generating calendar image (%s)..." % options.style
|
|
|
|
if not options.vanilla: callirhoe_args = callirhoe_args + ['--no-footer', '--border=0']
|
|
|
|
calimg = mktemp('.png')
|
|
|
|
try:
|
2014-09-27 16:55:34 +00:00
|
|
|
run_callirhoe(options.style, geometry[0], geometry[1], callirhoe_args, calimg);
|
2014-09-23 22:50:59 +00:00
|
|
|
|
2014-09-25 23:32:39 +00:00
|
|
|
# perform final composition
|
|
|
|
if options.verbose: print "Composing overlay (%s)..." % outimg
|
2014-09-26 12:09:55 +00:00
|
|
|
overlay = ['(', '-negate', calimg, ')'] if dark else [calimg]
|
2014-09-27 16:55:34 +00:00
|
|
|
subprocess.call(['convert', img] + magick_args[0] + ['-region', '%dx%d+%d+%d' % geometry] +
|
2014-09-26 12:09:55 +00:00
|
|
|
([] if options.brightness == 0 else ['-brightness-contrast', '%d' % (-options.brightness if dark else options.brightness)]) +
|
2014-09-25 23:32:39 +00:00
|
|
|
([] if options.saturation == 100 else ['-modulate', '100,%d' % options.saturation]) + magick_args[1] +
|
2014-09-27 16:55:34 +00:00
|
|
|
['-compose', 'over'] + overlay + ['-geometry', '+%d+%d' % geometry[2:], '-composite'] +
|
2014-09-25 23:32:39 +00:00
|
|
|
magick_args[2] + [outimg])
|
|
|
|
finally:
|
|
|
|
os.remove(calimg)
|
2014-09-23 22:50:59 +00:00
|
|
|
|
2014-09-28 14:38:30 +00:00
|
|
|
def parse_range(s,hint=None):
|
2014-09-25 23:32:39 +00:00
|
|
|
if '/' in s:
|
|
|
|
t = s.split('/')
|
|
|
|
month,span = parse_month_range(t[0])
|
2014-09-28 14:38:30 +00:00
|
|
|
if hint and span == 0: span = hint
|
2014-09-25 23:32:39 +00:00
|
|
|
year = parse_year(t[1])
|
|
|
|
margs = []
|
|
|
|
for m in xrange(span):
|
|
|
|
margs += [(month,year)]
|
|
|
|
month += 1
|
|
|
|
if month > 12: month = 1; year += 1
|
|
|
|
return margs
|
|
|
|
else:
|
2014-09-28 14:38:30 +00:00
|
|
|
sys.exit("calmagick: invalid range format '%s'." % options.range)
|
2014-09-25 23:32:39 +00:00
|
|
|
|
|
|
|
if __name__ == '__main__':
|
|
|
|
parser = get_parser()
|
|
|
|
|
|
|
|
magick_args = parse_magick_args()
|
|
|
|
sys.argv,argv2 = extract_parser_args(sys.argv,parser,2)
|
|
|
|
(options,args) = parser.parse_args()
|
2014-09-28 14:38:30 +00:00
|
|
|
check_parsed_options(options)
|
2014-09-25 23:32:39 +00:00
|
|
|
|
|
|
|
if len(args) < 1:
|
|
|
|
parser.print_help()
|
|
|
|
sys.exit(0)
|
|
|
|
|
|
|
|
if not os.path.isdir(options.outdir):
|
|
|
|
# this way we get an exception if outdir exists and is a normal file
|
|
|
|
os.mkdir(options.outdir)
|
|
|
|
|
|
|
|
if options.range:
|
|
|
|
flist = glob.glob(args[0])
|
2014-09-28 14:38:30 +00:00
|
|
|
mrange = parse_range(options.range,hint=len(flist))
|
|
|
|
if options.verbose: print "Composing %d photos..." % len(mrange)
|
2014-09-25 23:32:39 +00:00
|
|
|
if options.sample is not None:
|
|
|
|
flist = random.sample(flist, options.sample if options.sample else len(mrange))
|
|
|
|
nf = len(flist)
|
|
|
|
if nf > 0:
|
|
|
|
for i in range(len(mrange)):
|
|
|
|
img = flist[i % nf]
|
|
|
|
m,y = mrange[i]
|
2014-09-28 14:38:30 +00:00
|
|
|
prefix = '' if options.prefix == 'no' else '%04d-%02d_' % (y,m)
|
|
|
|
outimg = get_outfile(img,options.outdir,prefix,options.format)
|
2014-09-25 23:32:39 +00:00
|
|
|
compose_calendar(img, outimg, options, [str(m), str(y)] + argv2, magick_args)
|
|
|
|
else:
|
|
|
|
img = args[0]
|
|
|
|
if not os.path.isfile(img):
|
2014-09-28 14:38:30 +00:00
|
|
|
sys.exit("calmagick: input image '%s' does not exist" % img)
|
|
|
|
outimg = get_outfile(img,options.outdir,'',options.format,options.outfile)
|
2014-09-25 23:32:39 +00:00
|
|
|
compose_calendar(img, outimg, options, argv2, magick_args)
|