Merge remote-tracking branch 'origin/refactor'
This commit is contained in:
commit
6aa9c28c80
9
.travis.yml
Normal file
9
.travis.yml
Normal file
@ -0,0 +1,9 @@
|
||||
language: python
|
||||
python:
|
||||
- "2.7"
|
||||
# command to install dependencies
|
||||
install:
|
||||
- pip install -U setuptools
|
||||
- pip install .
|
||||
# command to run tests
|
||||
script: nosetests
|
||||
@ -1,7 +1,3 @@
|
||||
include dominion_cards.txt
|
||||
include dominion_card_extras.txt
|
||||
include *.png
|
||||
include card_db/*/*.json
|
||||
include images/*.png
|
||||
exclude card_images/*
|
||||
exclude old_images/*
|
||||
|
||||
|
||||
@ -1,3 +1,3 @@
|
||||
# main package
|
||||
|
||||
__version__ = '1.9.1'
|
||||
__version__ = '2.0'
|
||||
|
||||
@ -1,515 +0,0 @@
|
||||
#!python
|
||||
"""Bootstrap distribute installation
|
||||
|
||||
If you want to use setuptools in your package's setup.py, just include this
|
||||
file in the same directory with it, and add this to the top of your setup.py::
|
||||
|
||||
from distribute_setup import use_setuptools
|
||||
use_setuptools()
|
||||
|
||||
If you want to require a specific version of setuptools, set a download
|
||||
mirror, or use an alternate download directory, you can do so by supplying
|
||||
the appropriate options to ``use_setuptools()``.
|
||||
|
||||
This file can also be run as a script to install or upgrade setuptools.
|
||||
"""
|
||||
import os
|
||||
import sys
|
||||
import time
|
||||
import fnmatch
|
||||
import tempfile
|
||||
import tarfile
|
||||
from distutils import log
|
||||
|
||||
try:
|
||||
from site import USER_SITE
|
||||
except ImportError:
|
||||
USER_SITE = None
|
||||
|
||||
try:
|
||||
import subprocess
|
||||
|
||||
def _python_cmd(*args):
|
||||
args = (sys.executable,) + args
|
||||
return subprocess.call(args) == 0
|
||||
|
||||
except ImportError:
|
||||
# will be used for python 2.3
|
||||
def _python_cmd(*args):
|
||||
args = (sys.executable,) + args
|
||||
# quoting arguments if windows
|
||||
if sys.platform == 'win32':
|
||||
def quote(arg):
|
||||
if ' ' in arg:
|
||||
return '"%s"' % arg
|
||||
return arg
|
||||
args = [quote(arg) for arg in args]
|
||||
return os.spawnl(os.P_WAIT, sys.executable, *args) == 0
|
||||
|
||||
DEFAULT_VERSION = "0.6.28"
|
||||
DEFAULT_URL = "http://pypi.python.org/packages/source/d/distribute/"
|
||||
SETUPTOOLS_FAKED_VERSION = "0.6c11"
|
||||
|
||||
SETUPTOOLS_PKG_INFO = """\
|
||||
Metadata-Version: 1.0
|
||||
Name: setuptools
|
||||
Version: %s
|
||||
Summary: xxxx
|
||||
Home-page: xxx
|
||||
Author: xxx
|
||||
Author-email: xxx
|
||||
License: xxx
|
||||
Description: xxx
|
||||
""" % SETUPTOOLS_FAKED_VERSION
|
||||
|
||||
|
||||
def _install(tarball, install_args=()):
|
||||
# extracting the tarball
|
||||
tmpdir = tempfile.mkdtemp()
|
||||
log.warn('Extracting in %s', tmpdir)
|
||||
old_wd = os.getcwd()
|
||||
try:
|
||||
os.chdir(tmpdir)
|
||||
tar = tarfile.open(tarball)
|
||||
_extractall(tar)
|
||||
tar.close()
|
||||
|
||||
# going in the directory
|
||||
subdir = os.path.join(tmpdir, os.listdir(tmpdir)[0])
|
||||
os.chdir(subdir)
|
||||
log.warn('Now working in %s', subdir)
|
||||
|
||||
# installing
|
||||
log.warn('Installing Distribute')
|
||||
if not _python_cmd('setup.py', 'install', *install_args):
|
||||
log.warn('Something went wrong during the installation.')
|
||||
log.warn('See the error message above.')
|
||||
finally:
|
||||
os.chdir(old_wd)
|
||||
|
||||
|
||||
def _build_egg(egg, tarball, to_dir):
|
||||
# extracting the tarball
|
||||
tmpdir = tempfile.mkdtemp()
|
||||
log.warn('Extracting in %s', tmpdir)
|
||||
old_wd = os.getcwd()
|
||||
try:
|
||||
os.chdir(tmpdir)
|
||||
tar = tarfile.open(tarball)
|
||||
_extractall(tar)
|
||||
tar.close()
|
||||
|
||||
# going in the directory
|
||||
subdir = os.path.join(tmpdir, os.listdir(tmpdir)[0])
|
||||
os.chdir(subdir)
|
||||
log.warn('Now working in %s', subdir)
|
||||
|
||||
# building an egg
|
||||
log.warn('Building a Distribute egg in %s', to_dir)
|
||||
_python_cmd('setup.py', '-q', 'bdist_egg', '--dist-dir', to_dir)
|
||||
|
||||
finally:
|
||||
os.chdir(old_wd)
|
||||
# returning the result
|
||||
log.warn(egg)
|
||||
if not os.path.exists(egg):
|
||||
raise IOError('Could not build the egg.')
|
||||
|
||||
|
||||
def _do_download(version, download_base, to_dir, download_delay):
|
||||
egg = os.path.join(to_dir, 'distribute-%s-py%d.%d.egg'
|
||||
% (version, sys.version_info[0], sys.version_info[1]))
|
||||
if not os.path.exists(egg):
|
||||
tarball = download_setuptools(version, download_base,
|
||||
to_dir, download_delay)
|
||||
_build_egg(egg, tarball, to_dir)
|
||||
sys.path.insert(0, egg)
|
||||
import setuptools
|
||||
setuptools.bootstrap_install_from = egg
|
||||
|
||||
|
||||
def use_setuptools(version=DEFAULT_VERSION, download_base=DEFAULT_URL,
|
||||
to_dir=os.curdir, download_delay=15, no_fake=True):
|
||||
# making sure we use the absolute path
|
||||
to_dir = os.path.abspath(to_dir)
|
||||
was_imported = 'pkg_resources' in sys.modules or \
|
||||
'setuptools' in sys.modules
|
||||
try:
|
||||
try:
|
||||
import pkg_resources
|
||||
if not hasattr(pkg_resources, '_distribute'):
|
||||
if not no_fake:
|
||||
_fake_setuptools()
|
||||
raise ImportError
|
||||
except ImportError:
|
||||
return _do_download(version, download_base, to_dir, download_delay)
|
||||
try:
|
||||
pkg_resources.require("distribute>=" + version)
|
||||
return
|
||||
except pkg_resources.VersionConflict:
|
||||
e = sys.exc_info()[1]
|
||||
if was_imported:
|
||||
sys.stderr.write(
|
||||
"The required version of distribute (>=%s) is not available,\n"
|
||||
"and can't be installed while this script is running. Please\n"
|
||||
"install a more recent version first, using\n"
|
||||
"'easy_install -U distribute'."
|
||||
"\n\n(Currently using %r)\n" % (version, e.args[0]))
|
||||
sys.exit(2)
|
||||
else:
|
||||
del pkg_resources, sys.modules['pkg_resources'] # reload ok
|
||||
return _do_download(version, download_base, to_dir,
|
||||
download_delay)
|
||||
except pkg_resources.DistributionNotFound:
|
||||
return _do_download(version, download_base, to_dir,
|
||||
download_delay)
|
||||
finally:
|
||||
if not no_fake:
|
||||
_create_fake_setuptools_pkg_info(to_dir)
|
||||
|
||||
|
||||
def download_setuptools(version=DEFAULT_VERSION, download_base=DEFAULT_URL,
|
||||
to_dir=os.curdir, delay=15):
|
||||
"""Download distribute from a specified location and return its filename
|
||||
|
||||
`version` should be a valid distribute version number that is available
|
||||
as an egg for download under the `download_base` URL (which should end
|
||||
with a '/'). `to_dir` is the directory where the egg will be downloaded.
|
||||
`delay` is the number of seconds to pause before an actual download
|
||||
attempt.
|
||||
"""
|
||||
# making sure we use the absolute path
|
||||
to_dir = os.path.abspath(to_dir)
|
||||
try:
|
||||
from urllib.request import urlopen
|
||||
except ImportError:
|
||||
from urllib2 import urlopen
|
||||
tgz_name = "distribute-%s.tar.gz" % version
|
||||
url = download_base + tgz_name
|
||||
saveto = os.path.join(to_dir, tgz_name)
|
||||
src = dst = None
|
||||
if not os.path.exists(saveto): # Avoid repeated downloads
|
||||
try:
|
||||
log.warn("Downloading %s", url)
|
||||
src = urlopen(url)
|
||||
# Read/write all in one block, so we don't create a corrupt file
|
||||
# if the download is interrupted.
|
||||
data = src.read()
|
||||
dst = open(saveto, "wb")
|
||||
dst.write(data)
|
||||
finally:
|
||||
if src:
|
||||
src.close()
|
||||
if dst:
|
||||
dst.close()
|
||||
return os.path.realpath(saveto)
|
||||
|
||||
|
||||
def _no_sandbox(function):
|
||||
def __no_sandbox(*args, **kw):
|
||||
try:
|
||||
from setuptools.sandbox import DirectorySandbox
|
||||
if not hasattr(DirectorySandbox, '_old'):
|
||||
def violation(*args):
|
||||
pass
|
||||
DirectorySandbox._old = DirectorySandbox._violation
|
||||
DirectorySandbox._violation = violation
|
||||
patched = True
|
||||
else:
|
||||
patched = False
|
||||
except ImportError:
|
||||
patched = False
|
||||
|
||||
try:
|
||||
return function(*args, **kw)
|
||||
finally:
|
||||
if patched:
|
||||
DirectorySandbox._violation = DirectorySandbox._old
|
||||
del DirectorySandbox._old
|
||||
|
||||
return __no_sandbox
|
||||
|
||||
|
||||
def _patch_file(path, content):
|
||||
"""Will backup the file then patch it"""
|
||||
existing_content = open(path).read()
|
||||
if existing_content == content:
|
||||
# already patched
|
||||
log.warn('Already patched.')
|
||||
return False
|
||||
log.warn('Patching...')
|
||||
_rename_path(path)
|
||||
f = open(path, 'w')
|
||||
try:
|
||||
f.write(content)
|
||||
finally:
|
||||
f.close()
|
||||
return True
|
||||
|
||||
_patch_file = _no_sandbox(_patch_file)
|
||||
|
||||
|
||||
def _same_content(path, content):
|
||||
return open(path).read() == content
|
||||
|
||||
|
||||
def _rename_path(path):
|
||||
new_name = path + '.OLD.%s' % time.time()
|
||||
log.warn('Renaming %s into %s', path, new_name)
|
||||
os.rename(path, new_name)
|
||||
return new_name
|
||||
|
||||
|
||||
def _remove_flat_installation(placeholder):
|
||||
if not os.path.isdir(placeholder):
|
||||
log.warn('Unkown installation at %s', placeholder)
|
||||
return False
|
||||
found = False
|
||||
for file in os.listdir(placeholder):
|
||||
if fnmatch.fnmatch(file, 'setuptools*.egg-info'):
|
||||
found = True
|
||||
break
|
||||
if not found:
|
||||
log.warn('Could not locate setuptools*.egg-info')
|
||||
return
|
||||
|
||||
log.warn('Removing elements out of the way...')
|
||||
pkg_info = os.path.join(placeholder, file)
|
||||
if os.path.isdir(pkg_info):
|
||||
patched = _patch_egg_dir(pkg_info)
|
||||
else:
|
||||
patched = _patch_file(pkg_info, SETUPTOOLS_PKG_INFO)
|
||||
|
||||
if not patched:
|
||||
log.warn('%s already patched.', pkg_info)
|
||||
return False
|
||||
# now let's move the files out of the way
|
||||
for element in ('setuptools', 'pkg_resources.py', 'site.py'):
|
||||
element = os.path.join(placeholder, element)
|
||||
if os.path.exists(element):
|
||||
_rename_path(element)
|
||||
else:
|
||||
log.warn('Could not find the %s element of the '
|
||||
'Setuptools distribution', element)
|
||||
return True
|
||||
|
||||
_remove_flat_installation = _no_sandbox(_remove_flat_installation)
|
||||
|
||||
|
||||
def _after_install(dist):
|
||||
log.warn('After install bootstrap.')
|
||||
placeholder = dist.get_command_obj('install').install_purelib
|
||||
_create_fake_setuptools_pkg_info(placeholder)
|
||||
|
||||
|
||||
def _create_fake_setuptools_pkg_info(placeholder):
|
||||
if not placeholder or not os.path.exists(placeholder):
|
||||
log.warn('Could not find the install location')
|
||||
return
|
||||
pyver = '%s.%s' % (sys.version_info[0], sys.version_info[1])
|
||||
setuptools_file = 'setuptools-%s-py%s.egg-info' % \
|
||||
(SETUPTOOLS_FAKED_VERSION, pyver)
|
||||
pkg_info = os.path.join(placeholder, setuptools_file)
|
||||
if os.path.exists(pkg_info):
|
||||
log.warn('%s already exists', pkg_info)
|
||||
return
|
||||
|
||||
if not os.access(pkg_info, os.W_OK):
|
||||
log.warn("Don't have permissions to write %s, skipping", pkg_info)
|
||||
|
||||
log.warn('Creating %s', pkg_info)
|
||||
f = open(pkg_info, 'w')
|
||||
try:
|
||||
f.write(SETUPTOOLS_PKG_INFO)
|
||||
finally:
|
||||
f.close()
|
||||
|
||||
pth_file = os.path.join(placeholder, 'setuptools.pth')
|
||||
log.warn('Creating %s', pth_file)
|
||||
f = open(pth_file, 'w')
|
||||
try:
|
||||
f.write(os.path.join(os.curdir, setuptools_file))
|
||||
finally:
|
||||
f.close()
|
||||
|
||||
_create_fake_setuptools_pkg_info = _no_sandbox(
|
||||
_create_fake_setuptools_pkg_info
|
||||
)
|
||||
|
||||
|
||||
def _patch_egg_dir(path):
|
||||
# let's check if it's already patched
|
||||
pkg_info = os.path.join(path, 'EGG-INFO', 'PKG-INFO')
|
||||
if os.path.exists(pkg_info):
|
||||
if _same_content(pkg_info, SETUPTOOLS_PKG_INFO):
|
||||
log.warn('%s already patched.', pkg_info)
|
||||
return False
|
||||
_rename_path(path)
|
||||
os.mkdir(path)
|
||||
os.mkdir(os.path.join(path, 'EGG-INFO'))
|
||||
pkg_info = os.path.join(path, 'EGG-INFO', 'PKG-INFO')
|
||||
f = open(pkg_info, 'w')
|
||||
try:
|
||||
f.write(SETUPTOOLS_PKG_INFO)
|
||||
finally:
|
||||
f.close()
|
||||
return True
|
||||
|
||||
_patch_egg_dir = _no_sandbox(_patch_egg_dir)
|
||||
|
||||
|
||||
def _before_install():
|
||||
log.warn('Before install bootstrap.')
|
||||
_fake_setuptools()
|
||||
|
||||
|
||||
def _under_prefix(location):
|
||||
if 'install' not in sys.argv:
|
||||
return True
|
||||
args = sys.argv[sys.argv.index('install') + 1:]
|
||||
for index, arg in enumerate(args):
|
||||
for option in ('--root', '--prefix'):
|
||||
if arg.startswith('%s=' % option):
|
||||
top_dir = arg.split('root=')[-1]
|
||||
return location.startswith(top_dir)
|
||||
elif arg == option:
|
||||
if len(args) > index:
|
||||
top_dir = args[index + 1]
|
||||
return location.startswith(top_dir)
|
||||
if arg == '--user' and USER_SITE is not None:
|
||||
return location.startswith(USER_SITE)
|
||||
return True
|
||||
|
||||
|
||||
def _fake_setuptools():
|
||||
log.warn('Scanning installed packages')
|
||||
try:
|
||||
import pkg_resources
|
||||
except ImportError:
|
||||
# we're cool
|
||||
log.warn('Setuptools or Distribute does not seem to be installed.')
|
||||
return
|
||||
ws = pkg_resources.working_set
|
||||
try:
|
||||
setuptools_dist = ws.find(
|
||||
pkg_resources.Requirement.parse('setuptools', replacement=False)
|
||||
)
|
||||
except TypeError:
|
||||
# old distribute API
|
||||
setuptools_dist = ws.find(
|
||||
pkg_resources.Requirement.parse('setuptools')
|
||||
)
|
||||
|
||||
if setuptools_dist is None:
|
||||
log.warn('No setuptools distribution found')
|
||||
return
|
||||
# detecting if it was already faked
|
||||
setuptools_location = setuptools_dist.location
|
||||
log.warn('Setuptools installation detected at %s', setuptools_location)
|
||||
|
||||
# if --root or --preix was provided, and if
|
||||
# setuptools is not located in them, we don't patch it
|
||||
if not _under_prefix(setuptools_location):
|
||||
log.warn('Not patching, --root or --prefix is installing Distribute'
|
||||
' in another location')
|
||||
return
|
||||
|
||||
# let's see if its an egg
|
||||
if not setuptools_location.endswith('.egg'):
|
||||
log.warn('Non-egg installation')
|
||||
res = _remove_flat_installation(setuptools_location)
|
||||
if not res:
|
||||
return
|
||||
else:
|
||||
log.warn('Egg installation')
|
||||
pkg_info = os.path.join(setuptools_location, 'EGG-INFO', 'PKG-INFO')
|
||||
if (os.path.exists(pkg_info) and
|
||||
_same_content(pkg_info, SETUPTOOLS_PKG_INFO)):
|
||||
log.warn('Already patched.')
|
||||
return
|
||||
log.warn('Patching...')
|
||||
# let's create a fake egg replacing setuptools one
|
||||
res = _patch_egg_dir(setuptools_location)
|
||||
if not res:
|
||||
return
|
||||
log.warn('Patched done.')
|
||||
_relaunch()
|
||||
|
||||
|
||||
def _relaunch():
|
||||
log.warn('Relaunching...')
|
||||
# we have to relaunch the process
|
||||
# pip marker to avoid a relaunch bug
|
||||
_cmd = ['-c', 'install', '--single-version-externally-managed']
|
||||
if sys.argv[:3] == _cmd:
|
||||
sys.argv[0] = 'setup.py'
|
||||
args = [sys.executable] + sys.argv
|
||||
sys.exit(subprocess.call(args))
|
||||
|
||||
|
||||
def _extractall(self, path=".", members=None):
|
||||
"""Extract all members from the archive to the current working
|
||||
directory and set owner, modification time and permissions on
|
||||
directories afterwards. `path' specifies a different directory
|
||||
to extract to. `members' is optional and must be a subset of the
|
||||
list returned by getmembers().
|
||||
"""
|
||||
import copy
|
||||
import operator
|
||||
from tarfile import ExtractError
|
||||
directories = []
|
||||
|
||||
if members is None:
|
||||
members = self
|
||||
|
||||
for tarinfo in members:
|
||||
if tarinfo.isdir():
|
||||
# Extract directories with a safe mode.
|
||||
directories.append(tarinfo)
|
||||
tarinfo = copy.copy(tarinfo)
|
||||
tarinfo.mode = 448 # decimal for oct 0700
|
||||
self.extract(tarinfo, path)
|
||||
|
||||
# Reverse sort directories.
|
||||
if sys.version_info < (2, 4):
|
||||
def sorter(dir1, dir2):
|
||||
return cmp(dir1.name, dir2.name)
|
||||
directories.sort(sorter)
|
||||
directories.reverse()
|
||||
else:
|
||||
directories.sort(key=operator.attrgetter('name'), reverse=True)
|
||||
|
||||
# Set correct owner, mtime and filemode on directories.
|
||||
for tarinfo in directories:
|
||||
dirpath = os.path.join(path, tarinfo.name)
|
||||
try:
|
||||
self.chown(tarinfo, dirpath)
|
||||
self.utime(tarinfo, dirpath)
|
||||
self.chmod(tarinfo, dirpath)
|
||||
except ExtractError:
|
||||
e = sys.exc_info()[1]
|
||||
if self.errorlevel > 1:
|
||||
raise
|
||||
else:
|
||||
self._dbg(1, "tarfile: %s" % e)
|
||||
|
||||
|
||||
def _build_install_args(argv):
|
||||
install_args = []
|
||||
user_install = '--user' in argv
|
||||
if user_install and sys.version_info < (2, 6):
|
||||
log.warn("--user requires Python 2.6 or later")
|
||||
raise SystemExit(1)
|
||||
if user_install:
|
||||
install_args.append('--user')
|
||||
return install_args
|
||||
|
||||
|
||||
def main(argv, version=DEFAULT_VERSION):
|
||||
"""Install or upgrade setuptools and EasyInstall"""
|
||||
tarball = download_setuptools()
|
||||
_install(tarball, _build_install_args(argv))
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main(sys.argv[1:])
|
||||
433
domdiv/__init__.py
Normal file
433
domdiv/__init__.py
Normal file
@ -0,0 +1,433 @@
|
||||
from optparse import OptionParser
|
||||
import os
|
||||
import codecs
|
||||
import json
|
||||
import sys
|
||||
|
||||
import reportlab.lib.pagesizes as pagesizes
|
||||
from reportlab.lib.units import cm
|
||||
|
||||
from cards import Card
|
||||
from draw import DividerDrawer
|
||||
|
||||
LOCATION_CHOICES = ["tab", "body-top", "hide"]
|
||||
NAME_ALIGN_CHOICES = ["left", "right", "centre", "edge"]
|
||||
TAB_SIDE_CHOICES = ["left", "right", "left-alternate", "right-alternate", "full"]
|
||||
|
||||
|
||||
def add_opt(options, option, value):
|
||||
assert not hasattr(options, option)
|
||||
setattr(options, option, value)
|
||||
|
||||
|
||||
def parse_opts(argstring):
|
||||
parser = OptionParser()
|
||||
parser.add_option("--back_offset", type="float", dest="back_offset", default=0,
|
||||
help="Points to offset the back page to the right; needed for some printers")
|
||||
parser.add_option("--back_offset_height", type="float", dest="back_offset_height", default=0,
|
||||
help="Points to offset the back page upward; needed for some printers")
|
||||
parser.add_option("--orientation", type="choice", choices=["horizontal", "vertical"],
|
||||
dest="orientation", default="horizontal",
|
||||
help="horizontal or vertical, default:horizontal")
|
||||
parser.add_option("--sleeved", action="store_true",
|
||||
dest="sleeved", help="use --size=sleeved instead")
|
||||
parser.add_option("--size", type="string", dest="size", default='normal',
|
||||
help="'<%f>x<%f>' (size in cm), or 'normal' = '9.1x5.9', or 'sleeved' = '9.4x6.15'")
|
||||
parser.add_option("--minmargin", type="string", dest="minmargin", default="1x1",
|
||||
help="'<%f>x<%f>' (size in cm, left/right, top/bottom), default: 1x1")
|
||||
parser.add_option("--papersize", type="string", dest="papersize", default=None,
|
||||
help="'<%f>x<%f>' (size in cm), or 'A4', or 'LETTER'")
|
||||
parser.add_option("--tab_name_align", type="choice", choices=NAME_ALIGN_CHOICES + ["center"],
|
||||
dest="tab_name_align", default="left",
|
||||
help="Alignment of text on the tab. choices: left, right, centre (or center), edge."
|
||||
" The edge option will align the card name to the outside edge of the"
|
||||
" tab, so that when using tabs on alternating sides,"
|
||||
" the name is less likely to be hidden by the tab in front"
|
||||
" (edge will revert to left when tab_side is full since there is no edge in that case);"
|
||||
" default:left")
|
||||
parser.add_option("--tab_side", type="choice", choices=TAB_SIDE_CHOICES,
|
||||
dest="tab_side", default="right-alternate",
|
||||
help="Alignment of tab. choices: left, right, left-alternate, right-alternate, full;"
|
||||
" left/right forces all tabs to left/right side;"
|
||||
" left-alternate will start on the left and then toggle between left and right for the tabs;"
|
||||
" right-alternate will start on the right and then toggle between right and left for the tabs;" # noqa
|
||||
" full will force all label tabs to be full width of the divider"
|
||||
" default:right-alternate")
|
||||
parser.add_option("--tabwidth", type="float", default=4,
|
||||
help="width in cm of stick-up tab (ignored if tab_side is full or tabs-only is used)")
|
||||
parser.add_option("--cost", action="append", type="choice",
|
||||
choices=LOCATION_CHOICES, default=[],
|
||||
help="where to display the card cost; may be set to"
|
||||
" 'hide' to indicate it should not be displayed, or"
|
||||
" given multiple times to show it in multiple"
|
||||
" places; valid values are: %s; defaults to 'tab'"
|
||||
% ", ".join("'%s'" % x for x in LOCATION_CHOICES))
|
||||
parser.add_option("--set_icon", action="append", type="choice",
|
||||
choices=LOCATION_CHOICES, default=[],
|
||||
help="where to display the set icon; may be set to"
|
||||
" 'hide' to indicate it should not be displayed, or"
|
||||
" given multiple times to show it in multiple"
|
||||
" places; valid values are: %s; defaults to 'tab'"
|
||||
% ", ".join("'%s'" % x for x in LOCATION_CHOICES))
|
||||
parser.add_option("--expansions", action="append", type="string",
|
||||
help="subset of dominion expansions to produce tabs for")
|
||||
parser.add_option("--cropmarks", action="store_true", dest="cropmarks",
|
||||
help="print crop marks on both sides, rather than tab outlines on one")
|
||||
parser.add_option("--linewidth", type="float", default=.1,
|
||||
help="width of lines for card outlines/crop marks")
|
||||
parser.add_option("--write_json", action="store_true", dest="write_json",
|
||||
help="write json version of card definitions and extras")
|
||||
parser.add_option("--tabs-only", action="store_true", dest="tabs_only",
|
||||
help="draw only tabs to be printed on labels, no divider outlines")
|
||||
parser.add_option("--order", type="choice", choices=["expansion", "global"], dest="order",
|
||||
help="sort order for the cards, whether by expansion or globally alphabetical")
|
||||
parser.add_option("--expansion_dividers", action="store_true", dest="expansion_dividers",
|
||||
help="add dividers describing each expansion set")
|
||||
parser.add_option("--base_cards_with_expansion", action="store_true",
|
||||
help='print the base cards as part of the expansion; ie, a divider for "Silver"'
|
||||
' will be printed as both a "Dominion" card and as an "Intrigue" card; if this'
|
||||
' option is not given, all base cards are placed in their own "Base" expansion')
|
||||
parser.add_option("--centre_expansion_dividers", action="store_true", dest="centre_expansion_dividers",
|
||||
help='centre the tabs on expansion dividers')
|
||||
parser.add_option("--num_pages", type="int", default=-1,
|
||||
help="stop generating after this many pages, -1 for all")
|
||||
parser.add_option("--language", default='en_us', help="language of card texts")
|
||||
parser.add_option("--include_blanks", action="store_true",
|
||||
help="include a few dividers with extra text")
|
||||
parser.add_option("--exclude_events", action="store_true",
|
||||
default=False, help="exclude individual dividers for events")
|
||||
parser.add_option("--special_card_groups", action="store_true",
|
||||
default=False, help="group some cards under special dividers (e.g. Shelters, Prizes)")
|
||||
parser.add_option("--exclude_prizes", action="store_true",
|
||||
default=False, help="exclude individual dividers for prizes (cornucopia)")
|
||||
parser.add_option("--cardlist", type="string", dest="cardlist", default=None,
|
||||
help="Path to file that enumerates each card to be printed on its own line.")
|
||||
parser.add_option("--no-tab-artwork", action="store_true", dest="no_tab_artwork",
|
||||
help="don't show background artwork on tabs")
|
||||
parser.add_option("--no-card-rules", action="store_true", dest="no_card_rules",
|
||||
help="don't print the card's rules on the tab body")
|
||||
parser.add_option("--use-text-set-icon", action="store_true", dest="use_text_set_icon",
|
||||
help="use text/letters to represent a card's set instead of the set icon")
|
||||
parser.add_option("--no-page-footer", action="store_true", dest="no_page_footer",
|
||||
help="don't print the set name at the bottom of the page.")
|
||||
parser.add_option("--no-card-backs", action="store_true", dest="no_card_backs",
|
||||
help="don't print the back page of the card sheets.")
|
||||
|
||||
options, args = parser.parse_args(argstring)
|
||||
if not options.cost:
|
||||
options.cost = ['tab']
|
||||
if not options.set_icon:
|
||||
options.set_icon = ['tab']
|
||||
return options, args
|
||||
|
||||
|
||||
def parseDimensions(dimensionsStr):
|
||||
x, y = dimensionsStr.upper().split('X', 1)
|
||||
return (float(x) * cm, float(y) * cm)
|
||||
|
||||
|
||||
def generate_sample(options):
|
||||
import cStringIO
|
||||
from wand.image import Image
|
||||
buf = cStringIO.StringIO()
|
||||
options.num_pages = 1
|
||||
generate(options, '.', buf)
|
||||
with Image(blob=buf.getvalue()) as sample:
|
||||
sample.format = 'png'
|
||||
sample.save(filename='sample.png')
|
||||
|
||||
|
||||
def parse_papersize(spec):
|
||||
papersize = None
|
||||
if not spec:
|
||||
if os.path.exists("/etc/papersize"):
|
||||
papersize = open("/etc/papersize").readline().upper()
|
||||
else:
|
||||
papersize = 'LETTER'
|
||||
else:
|
||||
papersize = spec.upper()
|
||||
|
||||
try:
|
||||
paperwidth, paperheight = getattr(pagesizes, papersize)
|
||||
except AttributeError:
|
||||
try:
|
||||
paperwidth, paperheight = parseDimensions(papersize)
|
||||
print 'Using custom paper size, %.2fcm x %.2fcm' % (paperwidth / cm, paperheight / cm)
|
||||
except ValueError:
|
||||
paperwidth, paperheight = pagesizes.LETTER
|
||||
return paperwidth, paperheight
|
||||
|
||||
|
||||
def parse_cardsize(spec, sleeved):
|
||||
spec = spec.upper()
|
||||
if spec == 'SLEEVED' or sleeved:
|
||||
dominionCardWidth, dominionCardHeight = (9.4 * cm, 6.15 * cm)
|
||||
print 'Using sleeved card size, %.2fcm x %.2fcm' % (dominionCardWidth / cm,
|
||||
dominionCardHeight / cm)
|
||||
elif spec in ['NORMAL', 'UNSLEEVED']:
|
||||
dominionCardWidth, dominionCardHeight = (9.1 * cm, 5.9 * cm)
|
||||
print 'Using normal card size, %.2fcm x%.2fcm' % (dominionCardWidth / cm,
|
||||
dominionCardHeight / cm)
|
||||
else:
|
||||
dominionCardWidth, dominionCardHeight = parseDimensions(spec)
|
||||
print 'Using custom card size, %.2fcm x %.2fcm' % (dominionCardWidth / cm,
|
||||
dominionCardHeight / cm)
|
||||
return dominionCardWidth, dominionCardHeight
|
||||
|
||||
|
||||
def read_write_card_data(options):
|
||||
data_dir = os.path.join(options.data_path, "card_db", options.language)
|
||||
card_db_filepath = os.path.join(data_dir, "cards.json")
|
||||
with codecs.open(card_db_filepath, "r", "utf-8") as cardfile:
|
||||
cards = json.load(cardfile, object_hook=Card.decode_json)
|
||||
|
||||
language_mapping_filepath = os.path.join(data_dir, "mapping.json")
|
||||
with codecs.open(language_mapping_filepath, 'r', 'utf-8') as mapping_file:
|
||||
Card.language_mapping = json.load(mapping_file)
|
||||
|
||||
if options.write_json:
|
||||
fpath = "cards.json"
|
||||
with codecs.open(fpath, 'w', encoding='utf-8') as ofile:
|
||||
json.dump(cards,
|
||||
ofile,
|
||||
cls=Card.CardJSONEncoder,
|
||||
ensure_ascii=False,
|
||||
indent=True,
|
||||
sort_keys=True)
|
||||
return cards
|
||||
|
||||
|
||||
class CardSorter(object):
|
||||
|
||||
def __init__(self, order, baseCards):
|
||||
self.order = order
|
||||
if order == "global":
|
||||
self.sort_key = self.global_sort_key
|
||||
else:
|
||||
self.sort_key = self.by_expansion_sort_key
|
||||
|
||||
self.baseCards = baseCards
|
||||
|
||||
# When sorting cards, want to always put "base" cards after all
|
||||
# kingdom cards, and order the base cards in a set order - the
|
||||
# order they are listed in the database (ie, all normal treasures
|
||||
# by worth, then potion, then all normal VP cards by worth, then
|
||||
# trash)
|
||||
def baseIndex(self, name):
|
||||
try:
|
||||
return self.baseCards.index(name)
|
||||
except Exception:
|
||||
return -1
|
||||
|
||||
def isBaseExpansionCard(self, card):
|
||||
return card.cardset.lower() != 'base' and card.name in self.baseCards
|
||||
|
||||
def global_sort_key(self, card):
|
||||
return int(card.isExpansion()), self.baseIndex(card.name), card.name
|
||||
|
||||
def by_expansion_sort_key(self, card):
|
||||
return card.cardset, int(card.isExpansion()), self.baseIndex(card.name), card.name
|
||||
|
||||
def __call__(self, card):
|
||||
return self.sort_key(card)
|
||||
|
||||
|
||||
def filter_sort_cards(cards, options):
|
||||
|
||||
cardSorter = CardSorter(options.order,
|
||||
[card.name for card in cards if card.cardset.lower() == 'base'])
|
||||
if options.base_cards_with_expansion:
|
||||
cards = [card for card in cards if card.cardset.lower() != 'base']
|
||||
else:
|
||||
cards = [card for card in cards if not cardSorter.isBaseExpansionCard(card)]
|
||||
|
||||
if options.special_card_groups:
|
||||
# Load the card groups file
|
||||
card_groups_file = os.path.join(options.data_dir, "card_groups.json")
|
||||
with codecs.open(card_groups_file, 'r', 'utf-8') as cardgroup_file:
|
||||
card_groups = json.load(cardgroup_file)
|
||||
# pull out any cards which are a subcard, and rename the master card
|
||||
new_cards = []
|
||||
all_subcards = []
|
||||
for subs in [card_groups[x]["subcards"] for x in card_groups]:
|
||||
all_subcards += subs
|
||||
for card in cards:
|
||||
if card.name in card_groups.keys():
|
||||
card.name = card_groups[card.name]["new_name"]
|
||||
elif card.name in all_subcards:
|
||||
continue
|
||||
new_cards.append(card)
|
||||
cards = new_cards
|
||||
|
||||
if options.expansions:
|
||||
options.expansions = [o.lower()
|
||||
for o in options.expansions]
|
||||
reverseMapping = {
|
||||
v: k for k, v in Card.language_mapping.iteritems()}
|
||||
options.expansions = [
|
||||
reverseMapping.get(e, e) for e in options.expansions]
|
||||
filteredCards = []
|
||||
knownExpansions = set()
|
||||
for c in cards:
|
||||
knownExpansions.add(c.cardset)
|
||||
if next((e for e in options.expansions if c.cardset.startswith(e)), None):
|
||||
filteredCards.append(c)
|
||||
unknownExpansions = set(options.expansions) - knownExpansions
|
||||
if unknownExpansions:
|
||||
print "Error - unknown expansion(s): %s" % ", ".join(unknownExpansions)
|
||||
return
|
||||
|
||||
cards = filteredCards
|
||||
|
||||
if options.exclude_events:
|
||||
cards = [card for card in cards if not card.isEvent() or card.name == 'Events']
|
||||
|
||||
if options.exclude_prizes:
|
||||
cards = [card for card in cards if not card.isPrize()]
|
||||
|
||||
if options.cardlist:
|
||||
cardlist = set()
|
||||
with open(options.cardlist) as cardfile:
|
||||
for line in cardfile:
|
||||
cardlist.add(line.strip())
|
||||
if cardlist:
|
||||
cards = [card for card in cards if card.name in cardlist]
|
||||
|
||||
if options.expansion_dividers:
|
||||
cardnamesByExpansion = {}
|
||||
for c in cards:
|
||||
if cardSorter.isBaseExpansionCard(c):
|
||||
continue
|
||||
cardnamesByExpansion.setdefault(
|
||||
c.cardset, []).append(c.name.strip())
|
||||
for exp, names in cardnamesByExpansion.iteritems():
|
||||
c = Card(
|
||||
exp, exp, ("Expansion",), None, ' | '.join(sorted(names)))
|
||||
cards.append(c)
|
||||
|
||||
cards.sort(key=cardSorter)
|
||||
|
||||
return cards
|
||||
|
||||
|
||||
def calculate_layout(options):
|
||||
|
||||
dominionCardWidth, dominionCardHeight = parse_cardsize(options.size, options.sleeved)
|
||||
paperwidth, paperheight = parse_papersize(options.papersize)
|
||||
|
||||
if options.orientation == "vertical":
|
||||
dividerWidth, dividerBaseHeight = dominionCardHeight, dominionCardWidth
|
||||
else:
|
||||
dividerWidth, dividerBaseHeight = dominionCardWidth, dominionCardHeight
|
||||
|
||||
if options.tab_name_align == "center":
|
||||
options.tab_name_align = "centre"
|
||||
|
||||
if options.tab_side == "full" and options.tab_name_align == "edge":
|
||||
# This case does not make sense since there are two tab edges in this case. So picking left edge.
|
||||
print >>sys.stderr, "** Warning: Aligning card name as 'left' for 'full' tabs **"
|
||||
options.tab_name_align == "left"
|
||||
|
||||
fixedMargins = False
|
||||
if options.tabs_only:
|
||||
# fixed for Avery 8867 for now
|
||||
minmarginwidth = 0.86 * cm # was 0.76
|
||||
minmarginheight = 1.37 * cm # was 1.27
|
||||
labelHeight = 1.07 * cm # was 1.27
|
||||
labelWidth = 4.24 * cm # was 4.44
|
||||
horizontalBorderSpace = 0.96 * cm # was 0.76
|
||||
verticalBorderSpace = 0.20 * cm # was 0.01
|
||||
dividerBaseHeight = 0
|
||||
dividerWidth = labelWidth
|
||||
fixedMargins = True
|
||||
else:
|
||||
minmarginwidth, minmarginheight = parseDimensions(
|
||||
options.minmargin)
|
||||
if options.tab_side == "full":
|
||||
labelWidth = dividerWidth
|
||||
else:
|
||||
labelWidth = options.tabwidth * cm
|
||||
labelHeight = .9 * cm
|
||||
horizontalBorderSpace = 0 * cm
|
||||
verticalBorderSpace = 0 * cm
|
||||
|
||||
dividerHeight = dividerBaseHeight + labelHeight
|
||||
|
||||
add_opt(options, 'dividerWidth', dividerWidth)
|
||||
add_opt(options, 'dividerHeight', dividerHeight)
|
||||
add_opt(options, 'dividerWidthReserved', dividerWidth + horizontalBorderSpace)
|
||||
add_opt(options, 'dividerHeightReserved', dividerHeight + verticalBorderSpace)
|
||||
add_opt(options, 'labelWidth', labelWidth)
|
||||
add_opt(options, 'labelHeight', labelHeight)
|
||||
|
||||
# as we don't draw anything in the final border, it shouldn't count towards how many tabs we can fit
|
||||
# so it gets added back in to the page size here
|
||||
numDividersVerticalP = int(
|
||||
(paperheight - 2 * minmarginheight + verticalBorderSpace) / options.dividerHeightReserved)
|
||||
numDividersHorizontalP = int(
|
||||
(paperwidth - 2 * minmarginwidth + horizontalBorderSpace) / options.dividerWidthReserved)
|
||||
numDividersVerticalL = int(
|
||||
(paperwidth - 2 * minmarginwidth + verticalBorderSpace) / options.dividerHeightReserved)
|
||||
numDividersHorizontalL = int(
|
||||
(paperheight - 2 * minmarginheight + horizontalBorderSpace) / options.dividerWidthReserved)
|
||||
|
||||
if ((numDividersVerticalL * numDividersHorizontalL >
|
||||
numDividersVerticalP * numDividersHorizontalP) and not fixedMargins):
|
||||
add_opt(options, 'numDividersVertical', numDividersVerticalL)
|
||||
add_opt(options, 'numDividersHorizontal', numDividersHorizontalL)
|
||||
add_opt(options, 'paperheight', paperwidth)
|
||||
add_opt(options, 'paperwidth', paperheight)
|
||||
add_opt(options, 'minHorizontalMargin', minmarginheight)
|
||||
add_opt(options, 'minVerticalMargin', minmarginwidth)
|
||||
else:
|
||||
add_opt(options, 'numDividersVertical', numDividersVerticalP)
|
||||
add_opt(options, 'numDividersHorizontal', numDividersHorizontalP)
|
||||
add_opt(options, 'paperheight', paperheight)
|
||||
add_opt(options, 'paperwidth', paperwidth)
|
||||
add_opt(options, 'minHorizontalMargin', minmarginheight)
|
||||
add_opt(options, 'minVerticalMargin', minmarginwidth)
|
||||
|
||||
if not fixedMargins:
|
||||
# dynamically max margins
|
||||
add_opt(options, 'horizontalMargin',
|
||||
(options.paperwidth -
|
||||
options.numDividersHorizontal * options.dividerWidthReserved) / 2)
|
||||
add_opt(options, 'verticalMargin',
|
||||
(options.paperheight -
|
||||
options.numDividersVertical * options.dividerHeightReserved) / 2)
|
||||
else:
|
||||
add_opt(options, 'horizontalMargin', minmarginwidth)
|
||||
add_opt(options, 'verticalMargin', minmarginheight)
|
||||
|
||||
|
||||
def generate(options, data_path, f):
|
||||
|
||||
add_opt(options, 'data_path', data_path)
|
||||
|
||||
calculate_layout(options)
|
||||
|
||||
print "Paper dimensions: {:.2f}cm (w) x {:.2f}cm (h)".format(options.paperwidth / cm,
|
||||
options.paperheight / cm)
|
||||
print "Tab dimensions: {:.2f}cm (w) x {:.2f}cm (h)".format(options.dividerWidthReserved / cm,
|
||||
options.dividerHeightReserved / cm)
|
||||
print '{} dividers horizontally, {} vertically'.format(options.numDividersHorizontal,
|
||||
options.numDividersVertical)
|
||||
print "Margins: {:.2f}cm h, {:.2f}cm v\n".format(options.horizontalMargin / cm,
|
||||
options.verticalMargin / cm)
|
||||
|
||||
cards = read_write_card_data(options)
|
||||
cards = filter_sort_cards(cards, options)
|
||||
|
||||
if not f:
|
||||
f = "dominion_dividers.pdf"
|
||||
|
||||
dd = DividerDrawer()
|
||||
dd.draw(f, cards, options)
|
||||
|
||||
|
||||
def main(argstring, data_path):
|
||||
options, args = parse_opts(argstring)
|
||||
fname = None
|
||||
if args:
|
||||
fname = args[0]
|
||||
return generate(options, data_path, fname)
|
||||
219
domdiv/cards.py
Normal file
219
domdiv/cards.py
Normal file
@ -0,0 +1,219 @@
|
||||
import json
|
||||
import os
|
||||
|
||||
|
||||
def getType(typespec):
|
||||
return cardTypes[tuple(typespec)]
|
||||
|
||||
setImages = {
|
||||
'dominion': 'base_set.png',
|
||||
'intrigue': 'intrigue_set.png',
|
||||
'seaside': 'seaside_set.png',
|
||||
'prosperity': 'prosperity_set.png',
|
||||
'alchemy': 'alchemy_set.png',
|
||||
'cornucopia': 'cornucopia_set.png',
|
||||
'cornucopia extras': 'cornucopia_set.png',
|
||||
'hinterlands': 'hinterlands_set.png',
|
||||
'dark ages': 'dark_ages_set.png',
|
||||
'dark ages extras': 'dark_ages_set.png',
|
||||
'guilds': 'guilds_set.png',
|
||||
'adventures': 'adventures_set.png',
|
||||
'adventures extras': 'adventures_set.png'
|
||||
}
|
||||
promoImages = {
|
||||
'walled village': 'walled_village_set.png',
|
||||
'stash': 'stash_set.png',
|
||||
'governor': 'governor_set.png',
|
||||
'black market': 'black_market_set.png',
|
||||
'envoy': 'envoy_set.png',
|
||||
'prince': 'prince_set.png'
|
||||
}
|
||||
|
||||
setTextIcons = {
|
||||
'dominion': 'D',
|
||||
'intrigue': 'I',
|
||||
'seaside': 'S',
|
||||
'prosperity': 'P',
|
||||
'alchemy': 'A',
|
||||
'cornucopia': 'C',
|
||||
'cornucopia extras': 'C',
|
||||
'hinterlands': 'H',
|
||||
'dark ages': 'DA',
|
||||
'dark ages extras': 'DA',
|
||||
'guilds': 'G',
|
||||
'adventures': 'Ad',
|
||||
'adventures extras': 'Ad'
|
||||
}
|
||||
|
||||
promoTextIcons = {
|
||||
'walled village': '',
|
||||
'stash': '',
|
||||
'governor': '',
|
||||
'black market': '',
|
||||
'envoy': '',
|
||||
'prince': ''
|
||||
}
|
||||
|
||||
language_mapping = None
|
||||
|
||||
|
||||
class Card(object):
|
||||
|
||||
language_mapping = None
|
||||
|
||||
class CardJSONEncoder(json.JSONEncoder):
|
||||
|
||||
def default(self, obj):
|
||||
if isinstance(obj, Card):
|
||||
return obj.__dict__
|
||||
return json.JSONEncoder.default(self, obj)
|
||||
|
||||
@staticmethod
|
||||
def decode_json(obj):
|
||||
return Card(**obj)
|
||||
|
||||
@classmethod
|
||||
def getSetImage(cls, setName, cardName):
|
||||
if setName in setImages:
|
||||
return setImages[setName]
|
||||
if cardName.lower() in promoImages:
|
||||
return promoImages[cardName.lower()]
|
||||
if setName in cls.language_mapping:
|
||||
trans = cls.language_mapping[setName]
|
||||
if trans in setImages:
|
||||
return setImages[trans]
|
||||
if cardName in cls.language_mapping:
|
||||
trans = cls.language_mapping[cardName]
|
||||
if trans.lower() in promoImages:
|
||||
return promoImages[trans.lower()]
|
||||
return None
|
||||
|
||||
@classmethod
|
||||
def getSetText(cls, setName, cardName):
|
||||
if setName in cls.setTextIcons:
|
||||
return cls.setTextIcons[setName]
|
||||
if cardName.lower() in cls.promoTextIcons:
|
||||
return cls.promoTextIcons[cardName.lower()]
|
||||
return None
|
||||
|
||||
def __init__(self, name, cardset, types, cost, description='', potcost=0, extra=''):
|
||||
self.name = name.strip()
|
||||
self.cardset = cardset.strip()
|
||||
self.types = types
|
||||
self.cost = cost
|
||||
self.potcost = potcost
|
||||
self.description = description
|
||||
self.extra = extra
|
||||
|
||||
def getType(self):
|
||||
return getType(self.types)
|
||||
|
||||
def __repr__(self):
|
||||
return '"' + self.name + '"'
|
||||
|
||||
def toString(self):
|
||||
return self.name + ' ' + self.cardset + ' ' + '-'.join(self.types)\
|
||||
+ ' ' + self.cost + ' ' + self.description + ' ' + self.extra
|
||||
|
||||
def isExpansion(self):
|
||||
return self.getType().getTypeNames() == ('Expansion',)
|
||||
|
||||
def isEvent(self):
|
||||
return self.getType().getTypeNames() == ('Event',)
|
||||
|
||||
def isPrize(self):
|
||||
return 'Prize' in self.getType().getTypeNames()
|
||||
|
||||
def setImage(self):
|
||||
setImage = Card.getSetImage(self.cardset, self.name)
|
||||
if setImage is None and self.cardset not in ['base', 'extra'] and not self.isExpansion():
|
||||
print 'warning, no set image for set "%s" card "%s"' % (self.cardset, self.name)
|
||||
setImages[self.cardset] = 0
|
||||
promoImages[self.name.lower()] = 0
|
||||
return setImage
|
||||
|
||||
def setTextIcon(self):
|
||||
setTextIcon = getSetText(self.cardset, self.name)
|
||||
if setTextIcon is None and self.cardset not in ['base', 'extra'] and not self.isExpansion():
|
||||
print 'warning, no set text for set "%s" card "%s"' % (self.cardset, self.name)
|
||||
setTextIcons[self.cardset] = 0
|
||||
promoTextIcons[self.name.lower()] = 0
|
||||
return setTextIcon
|
||||
|
||||
def isBlank(self):
|
||||
return False
|
||||
|
||||
|
||||
class BlankCard(Card):
|
||||
|
||||
def __init__(self, num):
|
||||
Card.__init__(self, str(num), 'extra', ('Blank',), 0)
|
||||
|
||||
def isBlank(self):
|
||||
return True
|
||||
|
||||
|
||||
class CardType(object):
|
||||
|
||||
def __init__(self, typeNames, tabImageFile, tabTextHeightOffset=0, tabCostHeightOffset=-1):
|
||||
self.typeNames = typeNames
|
||||
self.tabImageFile = tabImageFile
|
||||
self.tabTextHeightOffset = tabTextHeightOffset
|
||||
self.tabCostHeightOffset = tabCostHeightOffset
|
||||
|
||||
def getTypeNames(self):
|
||||
return self.typeNames
|
||||
|
||||
def getTabImageFile(self):
|
||||
if not self.tabImageFile:
|
||||
return None
|
||||
return self.tabImageFile
|
||||
|
||||
def getNoCoinTabImageFile(self):
|
||||
if not self.tabImageFile:
|
||||
return None
|
||||
return ''.join(os.path.splitext(self.tabImageFile)[0] + '_nc' + os.path.splitext(self.tabImageFile)[1])
|
||||
|
||||
def getTabTextHeightOffset(self):
|
||||
return self.tabTextHeightOffset
|
||||
|
||||
def getTabCostHeightOffset(self):
|
||||
return self.tabCostHeightOffset
|
||||
|
||||
cardTypes = [
|
||||
CardType(('Action',), 'action.png'),
|
||||
CardType(('Action', 'Attack'), 'action.png'),
|
||||
CardType(('Action', 'Attack', 'Prize'), 'action.png'),
|
||||
CardType(('Action', 'Reaction'), 'reaction.png'),
|
||||
CardType(('Action', 'Victory'), 'action-victory.png'),
|
||||
CardType(('Action', 'Duration'), 'duration.png'),
|
||||
CardType(('Action', 'Duration', 'Reaction'), 'duration-reaction.png'),
|
||||
CardType(('Action', 'Attack', 'Duration'), 'duration.png'),
|
||||
CardType(('Action', 'Looter'), 'action.png'),
|
||||
CardType(('Action', 'Prize'), 'action.png'),
|
||||
CardType(('Action', 'Ruins'), 'ruins.png', 0, 1),
|
||||
CardType(('Action', 'Shelter'), 'action-shelter.png'),
|
||||
CardType(('Action', 'Attack', 'Looter'), 'action.png'),
|
||||
CardType(('Action', 'Attack', 'Traveller'), 'action.png'),
|
||||
CardType(('Action', 'Reserve'), 'reserve.png'),
|
||||
CardType(('Action', 'Reserve', 'Victory'), 'reserve-victory.png'),
|
||||
CardType(('Action', 'Traveller'), 'action.png'),
|
||||
CardType(('Prize',), 'action.png'),
|
||||
CardType(('Event',), 'event.png'),
|
||||
CardType(('Reaction',), 'reaction.png'),
|
||||
CardType(('Reaction', 'Shelter'), 'reaction-shelter.png'),
|
||||
CardType(('Treasure',), 'treasure.png', 3, 0),
|
||||
CardType(('Treasure', 'Attack'), 'treasure.png'),
|
||||
CardType(('Treasure', 'Victory'), 'treasure-victory.png'),
|
||||
CardType(('Treasure', 'Prize'), 'treasure.png', 3, 0),
|
||||
CardType(('Treasure', 'Reaction'), 'treasure-reaction.png', 0, 1),
|
||||
CardType(('Treasure', 'Reserve'), 'reserve-treasure.png'),
|
||||
CardType(('Victory',), 'victory.png'),
|
||||
CardType(('Victory', 'Reaction'), 'victory-reaction.png', 0, 1),
|
||||
CardType(('Victory', 'Shelter'), 'victory-shelter.png'),
|
||||
CardType(('Curse',), 'curse.png', 3),
|
||||
CardType(('Expansion',), 'expansion.png', 4),
|
||||
CardType(('Blank',), '')
|
||||
]
|
||||
|
||||
cardTypes = dict(((c.getTypeNames(), c) for c in cardTypes))
|
||||
531
domdiv/draw.py
Normal file
531
domdiv/draw.py
Normal file
@ -0,0 +1,531 @@
|
||||
import os
|
||||
import re
|
||||
import sys
|
||||
|
||||
from reportlab.lib.units import cm
|
||||
from reportlab.pdfbase import pdfmetrics
|
||||
from reportlab.lib.styles import getSampleStyleSheet
|
||||
from reportlab.platypus import Paragraph
|
||||
from reportlab.lib.enums import TA_JUSTIFY
|
||||
from reportlab.pdfgen import canvas
|
||||
from reportlab.pdfbase.ttfonts import TTFont
|
||||
|
||||
|
||||
def split(l, n):
|
||||
i = 0
|
||||
while i < len(l) - n:
|
||||
yield l[i:i + n]
|
||||
i += n
|
||||
yield l[i:]
|
||||
|
||||
|
||||
class DividerDrawer(object):
|
||||
|
||||
def __init__(self):
|
||||
self.odd = True
|
||||
self.canvas = None
|
||||
|
||||
def registerFonts(self):
|
||||
try:
|
||||
dirn = os.path.join(self.options.data_path, 'fonts')
|
||||
self.fontNameRegular = 'MinionPro-Regular'
|
||||
pdfmetrics.registerFont(
|
||||
TTFont(self.fontNameRegular, os.path.join(dirn, 'MinionPro-Regular.ttf')))
|
||||
self.fontNameBold = 'MinionPro-Bold'
|
||||
pdfmetrics.registerFont(
|
||||
TTFont(self.fontNameBold, os.path.join(dirn, 'MinionPro-Bold.ttf')))
|
||||
self.fontNameOblique = 'MinionPro-Oblique'
|
||||
pdfmetrics.registerFont(
|
||||
TTFont(self.fontNameOblique, os.path.join(dirn, 'MinionPro-It.ttf')))
|
||||
except:
|
||||
print >>sys.stderr, "Warning, Minion Pro Font ttf file not found! Falling back on Times"
|
||||
self.fontNameRegular = 'Times-Roman'
|
||||
self.fontNameBold = 'Times-Bold'
|
||||
self.fontNameOblique = 'Times-Oblique'
|
||||
|
||||
def draw(self, fname, cards, options):
|
||||
self.options = options
|
||||
|
||||
self.registerFonts()
|
||||
|
||||
dividerWidth = options.dividerWidth
|
||||
dividerHeight = options.dividerHeight
|
||||
tabLabelWidth = options.labelWidth
|
||||
|
||||
self.tabOutline = [(0, 0, dividerWidth, 0),
|
||||
(dividerWidth, 0, dividerWidth, dividerHeight),
|
||||
(dividerWidth, dividerHeight,
|
||||
dividerWidth - tabLabelWidth, dividerHeight),
|
||||
(dividerWidth - tabLabelWidth,
|
||||
dividerHeight, dividerWidth - tabLabelWidth,
|
||||
dividerHeight),
|
||||
(dividerWidth - tabLabelWidth,
|
||||
dividerHeight, 0, dividerHeight),
|
||||
(0, dividerHeight, 0, 0)]
|
||||
|
||||
self.expansionTabOutline = [(0, 0, dividerWidth, 0),
|
||||
(dividerWidth, 0, dividerWidth,
|
||||
dividerHeight),
|
||||
(dividerWidth, dividerHeight,
|
||||
dividerWidth / 2 + tabLabelWidth / 2, dividerHeight),
|
||||
(dividerWidth / 2 + tabLabelWidth / 2, dividerHeight,
|
||||
dividerWidth / 2 + tabLabelWidth / 2, dividerHeight),
|
||||
(dividerWidth / 2 + tabLabelWidth / 2, dividerHeight,
|
||||
dividerWidth / 2 - tabLabelWidth / 2, dividerHeight),
|
||||
(dividerWidth / 2 - tabLabelWidth / 2, dividerHeight,
|
||||
dividerWidth / 2 - tabLabelWidth / 2, dividerHeight),
|
||||
(dividerWidth / 2 - tabLabelWidth / 2, dividerHeight,
|
||||
0, dividerHeight),
|
||||
(0, dividerHeight, 0, 0)]
|
||||
|
||||
self.canvas = canvas.Canvas(
|
||||
fname, pagesize=(options.paperwidth, options.paperheight))
|
||||
self.drawDividers(cards)
|
||||
self.canvas.save()
|
||||
|
||||
def add_inline_images(self, text, fontsize):
|
||||
path = os.path.join(self.options.data_path, 'images')
|
||||
replace = '<img src='"'%s/coin_small_\\1.png'"' width=%d height='"'100%%'"' valign='"'middle'"'/>' % (
|
||||
path, fontsize * 1.2)
|
||||
text = re.sub('(\d)\s(c|C)oin(s)?', replace, text)
|
||||
replace = '<img src='"'%s/coin_small_question.png'"' width=%d height='"'100%%'"' valign='"'middle'"'/>' % (
|
||||
path, fontsize * 1.2)
|
||||
text = re.sub('\?\s(c|C)oin(s)?', replace, text)
|
||||
replace = '<img src='"'%s/victory_emblem.png'"' width=%d height='"'120%%'"' valign='"'middle'"'/>' % (
|
||||
path, fontsize * 1.5)
|
||||
text = re.sub('\<VP\>', replace, text)
|
||||
return text
|
||||
|
||||
def drawOutline(self, x, y, rightSide, isBack=False, isExpansionDivider=False):
|
||||
# draw outline or cropmarks
|
||||
self.canvas.saveState()
|
||||
self.canvas.setLineWidth(self.options.linewidth)
|
||||
cropmarksright = (x == self.options.numDividersHorizontal - 1)
|
||||
cropmarksleft = (x == 0)
|
||||
if rightSide:
|
||||
self.canvas.translate(self.options.dividerWidth, 0)
|
||||
self.canvas.scale(-1, 1)
|
||||
if not self.options.cropmarks and not isBack:
|
||||
# don't draw outline on back, in case lines don't line up with
|
||||
# front
|
||||
if isExpansionDivider and self.options.centre_expansion_dividers:
|
||||
outline = self.expansionTabOutline
|
||||
else:
|
||||
outline = self.tabOutline
|
||||
self.canvas.lines(outline)
|
||||
elif self.options.cropmarks:
|
||||
cmw = 0.5 * cm
|
||||
|
||||
# Horizontal-line cropmarks
|
||||
mirror = cropmarksright and not rightSide or cropmarksleft and rightSide
|
||||
if mirror:
|
||||
self.canvas.saveState()
|
||||
self.canvas.translate(self.options.dividerWidth, 0)
|
||||
self.canvas.scale(-1, 1)
|
||||
if cropmarksleft or cropmarksright:
|
||||
self.canvas.line(-2 * cmw, 0, -cmw, 0)
|
||||
self.canvas.line(-2 * cmw,
|
||||
self.dividerHeight, -cmw, self.dividerHeight)
|
||||
if y > 0:
|
||||
self.canvas.line(-2 * cmw,
|
||||
self.options.dividerHeight, -cmw, self.options.dividerHeight)
|
||||
if mirror:
|
||||
self.canvas.restoreState()
|
||||
|
||||
# Vertical-line cropmarks
|
||||
|
||||
# want to always draw the right-edge and middle-label-edge lines..
|
||||
# ...and draw the left-edge if this is the first card on the left
|
||||
|
||||
# ...but we need to take mirroring into account, to know "where"
|
||||
# to draw the left / right lines...
|
||||
if rightSide:
|
||||
leftLine = self.options.dividerWidth
|
||||
rightLine = 0
|
||||
else:
|
||||
leftLine = 0
|
||||
rightLine = self.options.dividerWidth
|
||||
middleLine = self.options.dividerWidth - self.options.labelWidth
|
||||
|
||||
if y == 0:
|
||||
self.canvas.line(rightLine, -2 * cmw, rightLine, -cmw)
|
||||
self.canvas.line(middleLine, -2 * cmw, middleLine, -cmw)
|
||||
if cropmarksleft:
|
||||
self.canvas.line(leftLine, -2 * cmw, leftLine, -cmw)
|
||||
if y == self.options.numDividersVertical - 1:
|
||||
self.canvas.line(rightLine, self.options.dividerHeight + cmw,
|
||||
rightLine, self.options.dividerHeight + 2 * cmw)
|
||||
self.canvas.line(middleLine, self.options.dividerHeight + cmw,
|
||||
middleLine, self.options.dividerHeight + 2 * cmw)
|
||||
if cropmarksleft:
|
||||
self.canvas.line(leftLine, self.options.dividerHeight + cmw,
|
||||
leftLine, self.options.dividerHeight + 2 * cmw)
|
||||
|
||||
self.canvas.restoreState()
|
||||
|
||||
def drawCost(self, card, x, y, costOffset=-1):
|
||||
# base width is 16 (for image) + 2 (1 pt border on each side)
|
||||
width = 18
|
||||
|
||||
costHeight = y + costOffset
|
||||
coinHeight = costHeight - 5
|
||||
potHeight = y - 3
|
||||
potSize = 11
|
||||
|
||||
self.canvas.drawImage(os.path.join(self.options.data_path, 'images', 'coin_small.png'),
|
||||
x, coinHeight, 16, 16, preserveAspectRatio=True, mask='auto')
|
||||
if card.potcost:
|
||||
self.canvas.drawImage(os.path.join(self.options.data_path, 'images', 'potion.png'), x + 17,
|
||||
potHeight, potSize, potSize, preserveAspectRatio=True,
|
||||
mask=[255, 255, 255, 255, 255, 255])
|
||||
width += potSize
|
||||
|
||||
self.canvas.setFont(self.fontNameBold, 12)
|
||||
cost = str(card.cost)
|
||||
self.canvas.drawCentredString(x + 8, costHeight, cost)
|
||||
return width
|
||||
|
||||
def drawSetIcon(self, setImage, x, y):
|
||||
# set image
|
||||
self.canvas.drawImage(
|
||||
os.path.join(self.options.data_path, 'images', setImage), x, y, 14, 12, mask='auto')
|
||||
|
||||
def nameWidth(self, name, fontSize):
|
||||
w = 0
|
||||
name_parts = name.split()
|
||||
for i, part in enumerate(name_parts):
|
||||
if i != 0:
|
||||
w += pdfmetrics.stringWidth(' ', self.fontNameRegular, fontSize)
|
||||
w += pdfmetrics.stringWidth(part[0], self.fontNameRegular, fontSize)
|
||||
w += pdfmetrics.stringWidth(part[1:],
|
||||
self.fontNameRegular, fontSize - 2)
|
||||
return w
|
||||
|
||||
def drawTab(self, card, rightSide):
|
||||
# draw tab flap
|
||||
self.canvas.saveState()
|
||||
if card.isExpansion() and self.options.centre_expansion_dividers:
|
||||
self.canvas.translate(self.options.dividerWidth / 2 - self.options.labelWidth / 2,
|
||||
self.options.dividerHeight - self.options.labelHeight)
|
||||
elif not rightSide:
|
||||
self.canvas.translate(self.options.dividerWidth - self.options.labelWidth,
|
||||
self.options.dividerHeight - self.options.labelHeight)
|
||||
else:
|
||||
self.canvas.translate(0, self.options.dividerHeight - self.options.labelHeight)
|
||||
|
||||
# allow for 3 pt border on each side
|
||||
textWidth = self.options.labelWidth - 6
|
||||
textHeight = 7
|
||||
if self.options.no_tab_artwork:
|
||||
textHeight = 4
|
||||
textHeight = self.options.labelHeight / 2 - textHeight + \
|
||||
card.getType().getTabTextHeightOffset()
|
||||
|
||||
# draw banner
|
||||
img = card.getType().getNoCoinTabImageFile()
|
||||
if not self.options.no_tab_artwork and img:
|
||||
self.canvas.drawImage(os.path.join(self.options.data_path, 'images', img), 1, 0,
|
||||
self.options.labelWidth -
|
||||
2, self.options.labelHeight - 1,
|
||||
preserveAspectRatio=False, anchor='n', mask='auto')
|
||||
|
||||
# draw cost
|
||||
if not card.isExpansion() and not card.isBlank():
|
||||
if 'tab' in self.options.cost:
|
||||
textInset = 4
|
||||
textInset += self.drawCost(card, textInset, textHeight,
|
||||
card.getType().getTabCostHeightOffset())
|
||||
else:
|
||||
textInset = 6
|
||||
else:
|
||||
textInset = 13
|
||||
|
||||
# draw set image
|
||||
# always need to offset from right edge, to make sure it stays on
|
||||
# banner
|
||||
textInsetRight = 6
|
||||
if self.options.use_text_set_icon:
|
||||
setImageHeight = card.getType().getTabTextHeightOffset()
|
||||
setText = card.setTextIcon()
|
||||
self.canvas.setFont(self.fontNameOblique, 8)
|
||||
if setText is None:
|
||||
setText = ""
|
||||
|
||||
self.canvas.drawCentredString(self.options.labelWidth - 10, textHeight + 2, setText)
|
||||
textInsetRight = 15
|
||||
else:
|
||||
setImage = card.setImage()
|
||||
if setImage and 'tab' in self.options.set_icon:
|
||||
setImageHeight = 3 + card.getType().getTabTextHeightOffset()
|
||||
|
||||
self.drawSetIcon(setImage, self.options.labelWidth - 20,
|
||||
setImageHeight)
|
||||
|
||||
textInsetRight = 20
|
||||
|
||||
# draw name
|
||||
fontSize = 12
|
||||
name = card.name.upper()
|
||||
|
||||
textWidth -= textInset
|
||||
textWidth -= textInsetRight
|
||||
|
||||
width = self.nameWidth(name, fontSize)
|
||||
while width > textWidth and fontSize > 8:
|
||||
fontSize -= .01
|
||||
# print 'decreasing font size for tab of',name,'now',fontSize
|
||||
width = self.nameWidth(name, fontSize)
|
||||
tooLong = width > textWidth
|
||||
if tooLong:
|
||||
name_lines = name.partition(' / ')
|
||||
if name_lines[1]:
|
||||
name_lines = (name_lines[0] + ' /', name_lines[2])
|
||||
else:
|
||||
name_lines = name.split(None, 1)
|
||||
else:
|
||||
name_lines = [name]
|
||||
# if tooLong:
|
||||
# print name
|
||||
|
||||
for linenum, line in enumerate(name_lines):
|
||||
h = textHeight
|
||||
if tooLong and len(name_lines) > 1:
|
||||
if linenum == 0:
|
||||
h += h / 2
|
||||
else:
|
||||
h -= h / 2
|
||||
|
||||
words = line.split()
|
||||
if (not self.options.tab_name_align == "right") and (self.options.tab_name_align == "centre" or
|
||||
rightSide or
|
||||
not self.options.tab_name_align == "edge"):
|
||||
if self.options.tab_name_align == "centre":
|
||||
w = self.options.labelWidth / 2 - self.nameWidth(line, fontSize) / 2
|
||||
else:
|
||||
w = textInset
|
||||
|
||||
def drawWordPiece(text, fontSize):
|
||||
self.canvas.setFont(self.fontNameRegular, fontSize)
|
||||
if text != ' ':
|
||||
self.canvas.drawString(w, h, text)
|
||||
return pdfmetrics.stringWidth(text, self.fontNameRegular, fontSize)
|
||||
for i, word in enumerate(words):
|
||||
if i != 0:
|
||||
w += drawWordPiece(' ', fontSize)
|
||||
w += drawWordPiece(word[0], fontSize)
|
||||
w += drawWordPiece(word[1:], fontSize - 2)
|
||||
else:
|
||||
# align text to the right if tab is on right side, to make
|
||||
# tabs easier to read when grouped together extra 3pt is for
|
||||
# space between text + set symbol
|
||||
|
||||
w = self.options.labelWidth - textInsetRight - 3
|
||||
words.reverse()
|
||||
|
||||
def drawWordPiece(text, fontSize):
|
||||
self.canvas.setFont(self.fontNameRegular, fontSize)
|
||||
if text != ' ':
|
||||
self.canvas.drawRightString(w, h, text)
|
||||
return -pdfmetrics.stringWidth(text, self.fontNameRegular, fontSize)
|
||||
for i, word in enumerate(words):
|
||||
w += drawWordPiece(word[1:], fontSize - 2)
|
||||
w += drawWordPiece(word[0], fontSize)
|
||||
if i != len(words) - 1:
|
||||
w += drawWordPiece(' ', fontSize)
|
||||
self.canvas.restoreState()
|
||||
|
||||
def drawText(self, card, useExtra=False):
|
||||
usedHeight = 0
|
||||
totalHeight = self.options.dividerHeight - self.options.labelHeight
|
||||
|
||||
drewTopIcon = False
|
||||
if 'body-top' in self.options.cost and not card.isExpansion():
|
||||
self.drawCost(card, cm / 4.0, totalHeight - 0.5 * cm)
|
||||
drewTopIcon = True
|
||||
|
||||
if 'body-top' in self.options.set_icon and not card.isExpansion():
|
||||
setImage = card.setImage()
|
||||
if setImage:
|
||||
self.drawSetIcon(setImage, self.options.dividerWidth - 16,
|
||||
totalHeight - 0.5 * cm - 3)
|
||||
drewTopIcon = True
|
||||
if drewTopIcon:
|
||||
usedHeight += 15
|
||||
|
||||
if self.options.no_card_rules:
|
||||
return
|
||||
|
||||
# draw text
|
||||
if useExtra and card.extra:
|
||||
descriptions = (card.extra,)
|
||||
else:
|
||||
descriptions = re.split("\n", card.description)
|
||||
|
||||
s = getSampleStyleSheet()['BodyText']
|
||||
s.fontName = "Times-Roman"
|
||||
s.alignment = TA_JUSTIFY
|
||||
|
||||
textHorizontalMargin = .5 * cm
|
||||
textVerticalMargin = .3 * cm
|
||||
textBoxWidth = self.options.dividerWidth - 2 * textHorizontalMargin
|
||||
textBoxHeight = totalHeight - usedHeight - 2 * textVerticalMargin
|
||||
spacerHeight = 0.2 * cm
|
||||
minSpacerHeight = 0.05 * cm
|
||||
|
||||
while True:
|
||||
paragraphs = []
|
||||
# this accounts for the spacers we insert between paragraphs
|
||||
h = (len(descriptions) - 1) * spacerHeight
|
||||
for d in descriptions:
|
||||
dmod = self.add_inline_images(d, s.fontSize)
|
||||
p = Paragraph(dmod, s)
|
||||
h += p.wrap(textBoxWidth, textBoxHeight)[1]
|
||||
paragraphs.append(p)
|
||||
|
||||
if h <= textBoxHeight or s.fontSize <= 1 or s.leading <= 1:
|
||||
break
|
||||
else:
|
||||
s.fontSize -= 1
|
||||
s.leading -= 1
|
||||
spacerHeight = max(spacerHeight - 1, minSpacerHeight)
|
||||
|
||||
h = totalHeight - usedHeight - textVerticalMargin
|
||||
for p in paragraphs:
|
||||
h -= p.height
|
||||
p.drawOn(self.canvas, textHorizontalMargin, h)
|
||||
h -= spacerHeight
|
||||
|
||||
def drawDivider(self, card, x, y, useExtra=False):
|
||||
# figure out whether the tab should go on the right side or not
|
||||
if self.options.tab_side == "right":
|
||||
rightSide = useExtra
|
||||
elif self.options.tab_side in ["left", "full"]:
|
||||
rightSide = not useExtra
|
||||
else:
|
||||
# alternate the cards
|
||||
if not useExtra:
|
||||
rightSide = not self.odd
|
||||
else:
|
||||
rightSide = self.odd
|
||||
|
||||
# apply the transforms to get us to the corner of the current card
|
||||
self.canvas.resetTransforms()
|
||||
self.canvas.translate(self.options.horizontalMargin, self.options.verticalMargin)
|
||||
if useExtra:
|
||||
self.canvas.translate(self.options.back_offset, self.options.back_offset_height)
|
||||
self.canvas.translate(x * self.options.dividerWidthReserved,
|
||||
y * self.options.dividerHeightReserved)
|
||||
|
||||
# actual drawing
|
||||
if not self.options.tabs_only:
|
||||
self.drawOutline(
|
||||
x, y, rightSide, useExtra, card.getType().getTypeNames() == ('Expansion',))
|
||||
self.drawTab(card, rightSide)
|
||||
if not self.options.tabs_only:
|
||||
self.drawText(card, useExtra)
|
||||
|
||||
def drawSetNames(self, pageCards):
|
||||
# print sets for this page
|
||||
self.canvas.saveState()
|
||||
|
||||
try:
|
||||
# calculate the text height, font size, and orientation
|
||||
maxFontsize = 12
|
||||
minFontsize = 6
|
||||
fontname = self.fontNameRegular
|
||||
font = pdfmetrics.getFont(fontname)
|
||||
fontHeightRelative = (font.face.ascent + abs(font.face.descent)) / 1000.0
|
||||
|
||||
canFit = False
|
||||
|
||||
layouts = [{'rotation': 0,
|
||||
'minMarginHeight': self.options.minVerticalMargin,
|
||||
'totalMarginHeight': self.options.verticalMargin,
|
||||
'width': self.options.paperwidth},
|
||||
{'rotation': 90,
|
||||
'minMarginHeight': self.options.minHorizontalMargin,
|
||||
'totalMarginHeight': self.options.horizontalMargin,
|
||||
'width': self.options.paperheight}]
|
||||
|
||||
for layout in layouts:
|
||||
availableMargin = layout[
|
||||
'totalMarginHeight'] - layout['minMarginHeight']
|
||||
fontsize = availableMargin / fontHeightRelative
|
||||
fontsize = min(maxFontsize, fontsize)
|
||||
if fontsize >= minFontsize:
|
||||
canFit = True
|
||||
break
|
||||
|
||||
if not canFit:
|
||||
import warnings
|
||||
warnings.warn("Not enough space to display set names")
|
||||
return
|
||||
|
||||
self.canvas.setFont(fontname, fontsize)
|
||||
|
||||
sets = []
|
||||
for c in pageCards:
|
||||
setTitle = c.cardset.title()
|
||||
if setTitle not in sets:
|
||||
sets.append(setTitle)
|
||||
|
||||
# Centered on page
|
||||
xPos = layout['width'] / 2
|
||||
# Place at the very edge of the margin
|
||||
yPos = layout['minMarginHeight']
|
||||
|
||||
if layout['rotation']:
|
||||
self.canvas.rotate(layout['rotation'])
|
||||
yPos = -yPos
|
||||
|
||||
self.canvas.drawCentredString(xPos, yPos, '/'.join(sets))
|
||||
finally:
|
||||
self.canvas.restoreState()
|
||||
|
||||
def drawDividers(self, cards):
|
||||
# split into pages
|
||||
cards = split(cards, self.options.numDividersVertical * self.options.numDividersHorizontal)
|
||||
|
||||
# Starting with tabs on the left or the right?
|
||||
if self.options.tab_side in ["right-alternate", "right"]:
|
||||
self.odd = True
|
||||
else:
|
||||
# left-alternate, left, full
|
||||
self.odd = False
|
||||
|
||||
for pageNum, pageCards in enumerate(cards):
|
||||
# remember whether we start with odd or even divider for tab
|
||||
# location
|
||||
pageStartOdd = self.odd
|
||||
if not self.options.no_page_footer and (not self.options.tabs_only and self.options.order != "global"):
|
||||
self.drawSetNames(pageCards)
|
||||
for i, card in enumerate(pageCards):
|
||||
# print card
|
||||
x = i % self.options.numDividersHorizontal
|
||||
y = i / self.options.numDividersHorizontal
|
||||
self.canvas.saveState()
|
||||
self.drawDivider(card, x, self.options.numDividersVertical - 1 - y)
|
||||
self.canvas.restoreState()
|
||||
self.odd = not self.odd
|
||||
self.canvas.showPage()
|
||||
if pageNum + 1 == self.options.num_pages:
|
||||
break
|
||||
if self.options.tabs_only or self.options.no_card_backs:
|
||||
# no set names or card backs for label-only sheets
|
||||
continue
|
||||
if not self.options.no_page_footer and self.options.order != "global":
|
||||
self.drawSetNames(pageCards)
|
||||
# start at same oddness
|
||||
self.odd = pageStartOdd
|
||||
for i, card in enumerate(pageCards):
|
||||
# print card
|
||||
x = (self.options.numDividersHorizontal - 1 - i) % self.options.numDividersHorizontal
|
||||
y = i / self.options.numDividersHorizontal
|
||||
self.canvas.saveState()
|
||||
self.drawDivider(
|
||||
card, x, self.options.numDividersVertical - 1 - y, useExtra=True)
|
||||
self.canvas.restoreState()
|
||||
self.odd = not self.odd
|
||||
self.canvas.showPage()
|
||||
if pageNum + 1 == self.options.num_pages:
|
||||
break
|
||||
6
dominion_dividers.py
Normal file
6
dominion_dividers.py
Normal file
@ -0,0 +1,6 @@
|
||||
import domdiv
|
||||
import os
|
||||
import sys
|
||||
|
||||
if __name__ == '__main__':
|
||||
domdiv.main(sys.argv[1:], os.path.dirname(__file__))
|
||||
1101
dominion_tabs.py
1101
dominion_tabs.py
File diff suppressed because it is too large
Load Diff
391
ez_setup.py
Normal file
391
ez_setup.py
Normal file
@ -0,0 +1,391 @@
|
||||
#!/usr/bin/env python
|
||||
|
||||
"""
|
||||
Setuptools bootstrapping installer.
|
||||
|
||||
Run this script to install or upgrade setuptools.
|
||||
"""
|
||||
|
||||
import os
|
||||
import shutil
|
||||
import sys
|
||||
import tempfile
|
||||
import zipfile
|
||||
import optparse
|
||||
import subprocess
|
||||
import platform
|
||||
import textwrap
|
||||
import contextlib
|
||||
import warnings
|
||||
|
||||
from distutils import log
|
||||
|
||||
try:
|
||||
from urllib.request import urlopen
|
||||
except ImportError:
|
||||
from urllib2 import urlopen
|
||||
|
||||
try:
|
||||
from site import USER_SITE
|
||||
except ImportError:
|
||||
USER_SITE = None
|
||||
|
||||
DEFAULT_VERSION = "18.5"
|
||||
DEFAULT_URL = "https://pypi.python.org/packages/source/s/setuptools/"
|
||||
DEFAULT_SAVE_DIR = os.curdir
|
||||
|
||||
|
||||
def _python_cmd(*args):
|
||||
"""
|
||||
Execute a command.
|
||||
|
||||
Return True if the command succeeded.
|
||||
"""
|
||||
args = (sys.executable,) + args
|
||||
return subprocess.call(args) == 0
|
||||
|
||||
|
||||
def _install(archive_filename, install_args=()):
|
||||
"""Install Setuptools."""
|
||||
with archive_context(archive_filename):
|
||||
# installing
|
||||
log.warn('Installing Setuptools')
|
||||
if not _python_cmd('setup.py', 'install', *install_args):
|
||||
log.warn('Something went wrong during the installation.')
|
||||
log.warn('See the error message above.')
|
||||
# exitcode will be 2
|
||||
return 2
|
||||
|
||||
|
||||
def _build_egg(egg, archive_filename, to_dir):
|
||||
"""Build Setuptools egg."""
|
||||
with archive_context(archive_filename):
|
||||
# building an egg
|
||||
log.warn('Building a Setuptools egg in %s', to_dir)
|
||||
_python_cmd('setup.py', '-q', 'bdist_egg', '--dist-dir', to_dir)
|
||||
# returning the result
|
||||
log.warn(egg)
|
||||
if not os.path.exists(egg):
|
||||
raise IOError('Could not build the egg.')
|
||||
|
||||
|
||||
class ContextualZipFile(zipfile.ZipFile):
|
||||
|
||||
"""Supplement ZipFile class to support context manager for Python 2.6."""
|
||||
|
||||
def __enter__(self):
|
||||
return self
|
||||
|
||||
def __exit__(self, type, value, traceback):
|
||||
self.close()
|
||||
|
||||
def __new__(cls, *args, **kwargs):
|
||||
"""Construct a ZipFile or ContextualZipFile as appropriate."""
|
||||
if hasattr(zipfile.ZipFile, '__exit__'):
|
||||
return zipfile.ZipFile(*args, **kwargs)
|
||||
return super(ContextualZipFile, cls).__new__(cls)
|
||||
|
||||
|
||||
@contextlib.contextmanager
|
||||
def archive_context(filename):
|
||||
"""
|
||||
Unzip filename to a temporary directory, set to the cwd.
|
||||
|
||||
The unzipped target is cleaned up after.
|
||||
"""
|
||||
tmpdir = tempfile.mkdtemp()
|
||||
log.warn('Extracting in %s', tmpdir)
|
||||
old_wd = os.getcwd()
|
||||
try:
|
||||
os.chdir(tmpdir)
|
||||
with ContextualZipFile(filename) as archive:
|
||||
archive.extractall()
|
||||
|
||||
# going in the directory
|
||||
subdir = os.path.join(tmpdir, os.listdir(tmpdir)[0])
|
||||
os.chdir(subdir)
|
||||
log.warn('Now working in %s', subdir)
|
||||
yield
|
||||
|
||||
finally:
|
||||
os.chdir(old_wd)
|
||||
shutil.rmtree(tmpdir)
|
||||
|
||||
|
||||
def _do_download(version, download_base, to_dir, download_delay):
|
||||
"""Download Setuptools."""
|
||||
egg = os.path.join(to_dir, 'setuptools-%s-py%d.%d.egg'
|
||||
% (version, sys.version_info[0], sys.version_info[1]))
|
||||
if not os.path.exists(egg):
|
||||
archive = download_setuptools(version, download_base,
|
||||
to_dir, download_delay)
|
||||
_build_egg(egg, archive, to_dir)
|
||||
sys.path.insert(0, egg)
|
||||
|
||||
# Remove previously-imported pkg_resources if present (see
|
||||
# https://bitbucket.org/pypa/setuptools/pull-request/7/ for details).
|
||||
if 'pkg_resources' in sys.modules:
|
||||
del sys.modules['pkg_resources']
|
||||
|
||||
import setuptools
|
||||
setuptools.bootstrap_install_from = egg
|
||||
|
||||
|
||||
def use_setuptools(
|
||||
version=DEFAULT_VERSION, download_base=DEFAULT_URL,
|
||||
to_dir=DEFAULT_SAVE_DIR, download_delay=15):
|
||||
"""
|
||||
Ensure that a setuptools version is installed.
|
||||
|
||||
Return None. Raise SystemExit if the requested version
|
||||
or later cannot be installed.
|
||||
"""
|
||||
to_dir = os.path.abspath(to_dir)
|
||||
|
||||
# prior to importing, capture the module state for
|
||||
# representative modules.
|
||||
rep_modules = 'pkg_resources', 'setuptools'
|
||||
imported = set(sys.modules).intersection(rep_modules)
|
||||
|
||||
try:
|
||||
import pkg_resources
|
||||
pkg_resources.require("setuptools>=" + version)
|
||||
# a suitable version is already installed
|
||||
return
|
||||
except ImportError:
|
||||
# pkg_resources not available; setuptools is not installed; download
|
||||
pass
|
||||
except pkg_resources.DistributionNotFound:
|
||||
# no version of setuptools was found; allow download
|
||||
pass
|
||||
except pkg_resources.VersionConflict as VC_err:
|
||||
if imported:
|
||||
_conflict_bail(VC_err, version)
|
||||
|
||||
# otherwise, unload pkg_resources to allow the downloaded version to
|
||||
# take precedence.
|
||||
del pkg_resources
|
||||
_unload_pkg_resources()
|
||||
|
||||
return _do_download(version, download_base, to_dir, download_delay)
|
||||
|
||||
|
||||
def _conflict_bail(VC_err, version):
|
||||
"""
|
||||
Setuptools was imported prior to invocation, so it is
|
||||
unsafe to unload it. Bail out.
|
||||
"""
|
||||
conflict_tmpl = textwrap.dedent("""
|
||||
The required version of setuptools (>={version}) is not available,
|
||||
and can't be installed while this script is running. Please
|
||||
install a more recent version first, using
|
||||
'easy_install -U setuptools'.
|
||||
|
||||
(Currently using {VC_err.args[0]!r})
|
||||
""")
|
||||
msg = conflict_tmpl.format(**locals())
|
||||
sys.stderr.write(msg)
|
||||
sys.exit(2)
|
||||
|
||||
|
||||
def _unload_pkg_resources():
|
||||
del_modules = [
|
||||
name for name in sys.modules
|
||||
if name.startswith('pkg_resources')
|
||||
]
|
||||
for mod_name in del_modules:
|
||||
del sys.modules[mod_name]
|
||||
|
||||
|
||||
def _clean_check(cmd, target):
|
||||
"""
|
||||
Run the command to download target.
|
||||
|
||||
If the command fails, clean up before re-raising the error.
|
||||
"""
|
||||
try:
|
||||
subprocess.check_call(cmd)
|
||||
except subprocess.CalledProcessError:
|
||||
if os.access(target, os.F_OK):
|
||||
os.unlink(target)
|
||||
raise
|
||||
|
||||
|
||||
def download_file_powershell(url, target):
|
||||
"""
|
||||
Download the file at url to target using Powershell.
|
||||
|
||||
Powershell will validate trust.
|
||||
Raise an exception if the command cannot complete.
|
||||
"""
|
||||
target = os.path.abspath(target)
|
||||
ps_cmd = (
|
||||
"[System.Net.WebRequest]::DefaultWebProxy.Credentials = "
|
||||
"[System.Net.CredentialCache]::DefaultCredentials; "
|
||||
"(new-object System.Net.WebClient).DownloadFile(%(url)r, %(target)r)"
|
||||
% vars()
|
||||
)
|
||||
cmd = [
|
||||
'powershell',
|
||||
'-Command',
|
||||
ps_cmd,
|
||||
]
|
||||
_clean_check(cmd, target)
|
||||
|
||||
|
||||
def has_powershell():
|
||||
"""Determine if Powershell is available."""
|
||||
if platform.system() != 'Windows':
|
||||
return False
|
||||
cmd = ['powershell', '-Command', 'echo test']
|
||||
with open(os.path.devnull, 'wb') as devnull:
|
||||
try:
|
||||
subprocess.check_call(cmd, stdout=devnull, stderr=devnull)
|
||||
except Exception:
|
||||
return False
|
||||
return True
|
||||
download_file_powershell.viable = has_powershell
|
||||
|
||||
|
||||
def download_file_curl(url, target):
|
||||
cmd = ['curl', url, '--silent', '--output', target]
|
||||
_clean_check(cmd, target)
|
||||
|
||||
|
||||
def has_curl():
|
||||
cmd = ['curl', '--version']
|
||||
with open(os.path.devnull, 'wb') as devnull:
|
||||
try:
|
||||
subprocess.check_call(cmd, stdout=devnull, stderr=devnull)
|
||||
except Exception:
|
||||
return False
|
||||
return True
|
||||
download_file_curl.viable = has_curl
|
||||
|
||||
|
||||
def download_file_wget(url, target):
|
||||
cmd = ['wget', url, '--quiet', '--output-document', target]
|
||||
_clean_check(cmd, target)
|
||||
|
||||
|
||||
def has_wget():
|
||||
cmd = ['wget', '--version']
|
||||
with open(os.path.devnull, 'wb') as devnull:
|
||||
try:
|
||||
subprocess.check_call(cmd, stdout=devnull, stderr=devnull)
|
||||
except Exception:
|
||||
return False
|
||||
return True
|
||||
download_file_wget.viable = has_wget
|
||||
|
||||
|
||||
def download_file_insecure(url, target):
|
||||
"""Use Python to download the file, without connection authentication."""
|
||||
src = urlopen(url)
|
||||
try:
|
||||
# Read all the data in one block.
|
||||
data = src.read()
|
||||
finally:
|
||||
src.close()
|
||||
|
||||
# Write all the data in one block to avoid creating a partial file.
|
||||
with open(target, "wb") as dst:
|
||||
dst.write(data)
|
||||
download_file_insecure.viable = lambda: True
|
||||
|
||||
|
||||
def get_best_downloader():
|
||||
downloaders = (
|
||||
download_file_powershell,
|
||||
download_file_curl,
|
||||
download_file_wget,
|
||||
download_file_insecure,
|
||||
)
|
||||
viable_downloaders = (dl for dl in downloaders if dl.viable())
|
||||
return next(viable_downloaders, None)
|
||||
|
||||
|
||||
def download_setuptools(
|
||||
version=DEFAULT_VERSION, download_base=DEFAULT_URL,
|
||||
to_dir=DEFAULT_SAVE_DIR, delay=15,
|
||||
downloader_factory=get_best_downloader):
|
||||
"""
|
||||
Download setuptools from a specified location and return its filename.
|
||||
|
||||
`version` should be a valid setuptools version number that is available
|
||||
as an sdist for download under the `download_base` URL (which should end
|
||||
with a '/'). `to_dir` is the directory where the egg will be downloaded.
|
||||
`delay` is the number of seconds to pause before an actual download
|
||||
attempt.
|
||||
|
||||
``downloader_factory`` should be a function taking no arguments and
|
||||
returning a function for downloading a URL to a target.
|
||||
"""
|
||||
# making sure we use the absolute path
|
||||
to_dir = os.path.abspath(to_dir)
|
||||
zip_name = "setuptools-%s.zip" % version
|
||||
url = download_base + zip_name
|
||||
saveto = os.path.join(to_dir, zip_name)
|
||||
if not os.path.exists(saveto): # Avoid repeated downloads
|
||||
log.warn("Downloading %s", url)
|
||||
downloader = downloader_factory()
|
||||
downloader(url, saveto)
|
||||
return os.path.realpath(saveto)
|
||||
|
||||
|
||||
def _build_install_args(options):
|
||||
"""
|
||||
Build the arguments to 'python setup.py install' on the setuptools package.
|
||||
|
||||
Returns list of command line arguments.
|
||||
"""
|
||||
return ['--user'] if options.user_install else []
|
||||
|
||||
|
||||
def _parse_args():
|
||||
"""Parse the command line for options."""
|
||||
parser = optparse.OptionParser()
|
||||
parser.add_option(
|
||||
'--user', dest='user_install', action='store_true', default=False,
|
||||
help='install in user site package (requires Python 2.6 or later)')
|
||||
parser.add_option(
|
||||
'--download-base', dest='download_base', metavar="URL",
|
||||
default=DEFAULT_URL,
|
||||
help='alternative URL from where to download the setuptools package')
|
||||
parser.add_option(
|
||||
'--insecure', dest='downloader_factory', action='store_const',
|
||||
const=lambda: download_file_insecure, default=get_best_downloader,
|
||||
help='Use internal, non-validating downloader'
|
||||
)
|
||||
parser.add_option(
|
||||
'--version', help="Specify which version to download",
|
||||
default=DEFAULT_VERSION,
|
||||
)
|
||||
parser.add_option(
|
||||
'--to-dir',
|
||||
help="Directory to save (and re-use) package",
|
||||
default=DEFAULT_SAVE_DIR,
|
||||
)
|
||||
options, args = parser.parse_args()
|
||||
# positional arguments are ignored
|
||||
return options
|
||||
|
||||
|
||||
def _download_args(options):
|
||||
"""Return args for download_setuptools function from cmdline args."""
|
||||
return dict(
|
||||
version=options.version,
|
||||
download_base=options.download_base,
|
||||
downloader_factory=options.downloader_factory,
|
||||
to_dir=options.to_dir,
|
||||
)
|
||||
|
||||
|
||||
def main():
|
||||
"""Install or upgrade setuptools and EasyInstall."""
|
||||
options = _parse_args()
|
||||
archive = download_setuptools(**_download_args(options))
|
||||
return _install(archive, _build_install_args(options))
|
||||
|
||||
if __name__ == '__main__':
|
||||
sys.exit(main())
|
||||
14
setup.py
14
setup.py
@ -1,5 +1,5 @@
|
||||
from __init__ import __version__
|
||||
from distribute_setup import use_setuptools
|
||||
from ez_setup import use_setuptools
|
||||
from setuptools import setup, find_packages
|
||||
|
||||
use_setuptools()
|
||||
@ -8,15 +8,19 @@ use_setuptools()
|
||||
setup(
|
||||
name="dominiontabs",
|
||||
version=__version__,
|
||||
scripts=["dominion_tabs.py"],
|
||||
packages=find_packages(),
|
||||
entry_points={
|
||||
'console_scripts': [
|
||||
"dominion_dividers = domdiv.main:main"
|
||||
],
|
||||
},
|
||||
packages=find_packages(exclude=['tests']),
|
||||
install_requires=["reportlab>=2.5",
|
||||
"Pillow>=2.1.0"],
|
||||
package_data={
|
||||
'': ['*.txt', '*.png']
|
||||
'domdiv': ['images/*.png', 'card_db/*/*.json']
|
||||
},
|
||||
author="Sumpfork",
|
||||
author_email="sumpfork@mailmight.net",
|
||||
description="Tab Divider Generation for the Dominion Card Game"
|
||||
description="Divider Generation for the Dominion Card Game"
|
||||
)
|
||||
|
||||
|
||||
0
tests/__init__.py
Normal file
0
tests/__init__.py
Normal file
47
tests/carddb_tests.py
Normal file
47
tests/carddb_tests.py
Normal file
@ -0,0 +1,47 @@
|
||||
import unittest
|
||||
from .. import domdiv
|
||||
from ..domdiv import cards as domdiv_cards
|
||||
|
||||
|
||||
class TestCardDB(unittest.TestCase):
|
||||
|
||||
def test_cardread(self):
|
||||
options, args = domdiv.parse_opts(['commandname'])
|
||||
options.data_path = '.'
|
||||
cards = domdiv.read_write_card_data(options)
|
||||
self.assertEquals(len(cards), 312)
|
||||
print set(c.cardset for c in cards)
|
||||
valid_cardsets = {
|
||||
u'prosperity',
|
||||
u'cornucopia extras',
|
||||
u'cornucopia',
|
||||
u'promo',
|
||||
u'adventures extras',
|
||||
u'seaside',
|
||||
u'adventures',
|
||||
u'dark ages',
|
||||
u'hinterlands',
|
||||
u'dark ages extras',
|
||||
u'alchemy',
|
||||
u'base',
|
||||
u'dominion',
|
||||
u'guilds',
|
||||
u'intrigue'
|
||||
}
|
||||
for c in cards:
|
||||
self.assertIsInstance(c, domdiv_cards.Card)
|
||||
self.assertIn(c.cardset, valid_cardsets)
|
||||
|
||||
def test_languages(self):
|
||||
# for now, just test that they load
|
||||
options, args = domdiv.parse_opts(['commandname', '--language', 'it'])
|
||||
options.data_path = '.'
|
||||
cards = domdiv.read_write_card_data(options)
|
||||
self.assertTrue(cards, 'Italians cards did not read properly')
|
||||
self.assertIn("Maledizione", [card.name for card in cards])
|
||||
|
||||
options, args = domdiv.parse_opts(['commandname', '--language', 'de'])
|
||||
options.data_path = '.'
|
||||
cards = domdiv.read_write_card_data(options)
|
||||
self.assertTrue(cards, 'German cards did not read properly')
|
||||
self.assertIn("Fluch", [card.name for card in cards])
|
||||
35
tests/layout_tests.py
Normal file
35
tests/layout_tests.py
Normal file
@ -0,0 +1,35 @@
|
||||
import unittest
|
||||
from .. import domdiv
|
||||
from reportlab.lib.units import cm
|
||||
|
||||
|
||||
class TestLayout(unittest.TestCase):
|
||||
|
||||
def test_horizontal(self):
|
||||
# should be the default
|
||||
options, args = domdiv.parse_opts(['commandname'])
|
||||
self.assertEquals(options.orientation, 'horizontal')
|
||||
domdiv.calculate_layout(options)
|
||||
self.assertEquals(options.numDividersHorizontal, 2)
|
||||
self.assertEquals(options.numDividersVertical, 3)
|
||||
self.assertEquals(options.dividerWidth, 9.1 * cm)
|
||||
self.assertEquals(options.labelHeight, 0.9 * cm)
|
||||
self.assertEquals(options.dividerHeight, 5.9 * cm + options.labelHeight)
|
||||
|
||||
def test_vertical(self):
|
||||
options, args = domdiv.parse_opts(['commandname', '--orientation', 'vertical'])
|
||||
self.assertEquals(options.orientation, 'vertical')
|
||||
domdiv.calculate_layout(options)
|
||||
self.assertEquals(options.numDividersHorizontal, 3)
|
||||
self.assertEquals(options.numDividersVertical, 2)
|
||||
self.assertEquals(options.dividerWidth, 5.9 * cm)
|
||||
self.assertEquals(options.labelHeight, 0.9 * cm)
|
||||
self.assertEquals(options.dividerHeight, 9.1 * cm + options.labelHeight)
|
||||
|
||||
def test_sleeved(self):
|
||||
options, args = domdiv.parse_opts(['commandname', '--size', 'sleeved'])
|
||||
domdiv.calculate_layout(options)
|
||||
self.assertEquals(options.dividerWidth, 9.4 * cm)
|
||||
self.assertEquals(options.labelHeight, 0.9 * cm)
|
||||
self.assertEquals(options.dividerHeight, 6.15 * cm + options.labelHeight)
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user