Skip to content

Instantly share code, notes, and snippets.

@malleusinferni
Last active December 18, 2015 10:49
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save malleusinferni/5770905 to your computer and use it in GitHub Desktop.
Save malleusinferni/5770905 to your computer and use it in GitHub Desktop.
#!/usr/bin/env python
from __future__ import print_function
# This program takes Homestuck pesterlog files (.page) in plain text,
# colorizes them, and renders them in other formats. By default, it
# outputs a PNG file, but BBCode and HTML output are also supported.
# Planned features:
# - HTML output with classes
# - CSS output in separate files or <style> tags
# - Detect output format from destination filename
# - Read from stdin (ie. act as a filter)
# - Multiple input files
# - Automatic chumhandle abbreviations in headers
# - Italicized text
# Things I would like to do but probably won't because they'd be
# an unbelievable pain in the ass:
# - Support for other font formats
# - Automatic font file lookup
# - Animated GIFs in input (eg. "L[O]RD ENGLISH")
# - Animated output with GIFs synced up
# - Testing any of this on Windows
# Example input:
"""
#COLORS
uu=#2ed73a
GT=JAKE
#PESTERLOG
GT: How do i explain.
GT: You know. Its a rather old fashioned term for being jolly and festive together.
GT: Like "that rollicking time we had scrumming the other eve sure was gay."
uu: I SEE.
uu: THEN YES. YOU ARE CORRECT.
uu: THIS IS GOING TO BE GAY AS HELL.
#END
"""
# Default colors. Most of these are for reference. If the colors I've
# included here are not the ones you want for those chumhandles, you
# can redefine them at the top of each file with a #COLORS directive.
# Alternatively, you can prefix individual lines with "LABEL!" to use
# the color for LABEL instead of the default for that abbreviation.
# For example, the following line (minus the "DIRK!" part) would be
# colored orange instead of purple:
#
# DIRK!TT: Said the stubborn skeptic, skeptically.
#
# Also, the list is incomplete. This is just what I already use.
fgcolors = {
# Infrastructure (change these for different themes)
'fg': '#000000',
'bg': '#eeeeee',
'border': '#ffaa33',
# B1 chumhandles
'EB': '#0715cd',
'TT': '#b536da',
'TG': '#e00707',
'GG': '#4ac925',
# Trolls
'AA': '#a10000',
'TA': '#a1a100',
'GC': '#008282',
'CG': '#696969',
'GA': '#008141',
# B2 names
'JANE': '#00d5f2',
'ROXY': '#ff2ff6',
'DIRK': '#f2a400',
'JAKE': '#1f9400',
# Cherubs
'UU': '#929292',
'uu': '#323232',
'LE': '#2ed73a',
}
def setcolor(label, color):
if label and color:
fgcolors[label] = color
def getcolor(label):
if fgcolors[label]:
return fgcolors[label]
else:
raise Exception("Can't find color for " + label)
import re
# TODO More reliable extension-swapping
def rename(oldfile, newext):
match = re.match('^(.*\.)(\w+)$', oldfile)
if match:
return match.group(1) + newext
else:
return oldfile + '.' + newext
# Parses a limited subset of the .page format for visual.rsmw.net.
# Does not handle nested directives, Tetanus markup, etc.
def parse_page(lines):
colorfmt = re.compile(r"""
^(\w+)=(
([#][0-9A-Fa-f]{6}) # CSS RGB color
| \w+ # Existing label to be aliased
)$""", re.X)
handlefmt = re.compile(r"""
^(?:
(\w+!)? # Explicit color flag
(\w*) # Abbreviated chumhandle
)
(:[ ]+
(.*) # The actual text of the line
)$""", re.X | re.U)
log = []
mode = 'pesterlog'
for line in lines.split('\n'):
if re.match(r'^#COLORS\b', line):
mode = 'colors'
elif re.match(r'^#PESTERLOG\b', line):
mode = 'pesterlog'
elif re.match('^#END$', line):
return log
elif re.match('^#', line):
raise Exception("Unrecognized directive: " + line)
elif mode == 'colors':
match = re.match(colorfmt, line)
if match:
if match.group(3):
# Six digit RGB (DS=#ffffff)
setcolor(match.group(1), match.group(3))
else:
# Alias for another label (TG=DAVE)
setcolor(match.group(1), getcolor(match.group(2)))
else:
raise Exception("Can't parse color: " + line)
elif mode == 'pesterlog':
fgcolor = getcolor('fg')
match = re.match(handlefmt, line)
if match:
# Example input: "AR!TT: Yes."
# Groups matched: "AR!", "TT", ": Yes.", "Yes."
# Note that group 3 encompasses group 4.
if match.group(1):
# Get color from explicit color flag (AR!)
fgcolor = getcolor(match.group(1).rstrip('!'))
elif match.group(2):
# Get color from abbreviated chumhandle (TT)
fgcolor = getcolor(match.group(2))
if match.group(1):
# Remove color flags
if match.group(2):
# Normal chatlog text; just remove flag
# "AR!TT" -> "TT"
line = match.group(2) + match.group(3)
else:
# Narrator-speak; remove all front matter
# "DS!: Is it, Seer?" -> "Is it, Seer?"
line = match.group(4)
# TODO Support "began pestering" messages somehow
log.append((fgcolor, line))
else:
# Dear Diary Today I Dont Even Know What Happened
raise Exception("Internal error! Unrecognized mode: " + mode)
return log
# TODO When outputting any kind of textual markup language, adjacent
# lines with the same class or color should be enclosed by the same
# tag, and blank lines should receive special treatment. This is not
# yet implemented for BBCode or HTML.
def print_bbcode(log, args):
lines = []
for fg, text in log:
lines.append('[color="' + fg + '"]' + text + '[/color]')
print('[font="Courier New"][b]' + "\n".join(lines) + '[/b][/font]')
def print_html(log, args):
import cgi
for fg, text in log:
htext = cgi.escape(text).encode('ascii', 'xmlcharrefreplace')
print('<span color="' + fg + '">' + htext + '</span>')
def print_png(log, args):
# PIL must be compiled with Freetype support for this to work.
import Image, ImageFont, ImageDraw
ttfont = ImageFont.truetype(args.font, args.pointsize)
# Calculate the maximum (pixel) width of a rendered line, then
# work backwards to find out how many characters can be on each
# line, which we need to know for text layout purposes.
textwidth = args.width - (2 * args.margin)
charwidth, charheight = ttfont.getsize('X')
from textwrap import TextWrapper
wrapper = TextWrapper(width=textwidth / charwidth)
bgcolor = getcolor('bg')
# Wrap lines of text to fit inside the image.
shortlines = []
for fg, text in log:
if text:
for line in wrapper.wrap(text):
shortlines.append((fg, line))
else:
# Hack to preserve blank lines
shortlines.append((bgcolor, u'\xa0',))
# Finish calculating the image size.
lineheight = args.leading + charheight
textheight = len(shortlines) * lineheight
imgheight = textheight + (2 * args.margin)
canvas = Image.new('RGB', (args.width, imgheight), bgcolor)
draw = ImageDraw.Draw(canvas)
# Disable antialiasing.
draw.fontmode = '1'
ceiling = args.margin
for fg, line in shortlines:
draw.text((args.margin, ceiling), line, fg, font=ttfont)
ceiling += lineheight
if args.border:
border_rect = (0, 0, args.width - 1, imgheight - 1)
draw.rectangle(border_rect, outline=getcolor('border'))
args.outfile = args.outfile or rename(args.infile, 'png')
canvas.save(args.outfile, 'PNG')
del draw
del canvas
# end of print_png
import argparse
import os
ap = argparse.ArgumentParser()
def file_exists_check(arg):
if os.path.isfile(arg):
return arg
elif os.path.exists(arg):
ap.error("%s is not a usable file" % arg)
else:
ap.error("%s does not exist" % arg)
ap.add_argument('-m', '--margin', type=int, default=10,
help="Number of blank pixels around the edge of the image")
ap.add_argument('-w', '--width', type=int, default=500,
help="Total width of the image, including margins")
ap.add_argument('--leading', type=int, default=2,
help="Extra space between lines of text")
ap.add_argument('--pointsize', type=int, default=14,
help="Point size (not pixel size!) of text")
ap.add_argument('--font', default='/Library/Fonts/Courier New Bold.ttf',
type=file_exists_check,
help="Location on disk of a Truetype font file")
ap.add_argument('--dark', action='store_true', default=False,
help="Use dark background to make white text readable")
ap.add_argument('--border', action='store_true', default=False,
help="Draw a one pixel border around the edge of the image")
ap.add_argument('--format', type=str, default='png',
help="Output format to use")
ap.add_argument('infile', type=file_exists_check)
ap.add_argument('outfile', nargs='?')
args = ap.parse_args()
# TODO Detect piped/redirected input
# from sys import stdin; if stdin.isatty()
import codecs
with codecs.open(args.infile, encoding='utf-8') as f:
log = parse_page(f.read())
if args.format == 'bbcode':
print_bbcode(log, args)
elif args.format == 'html':
print_html(log, args)
elif args.format == 'png':
print_png(log, args)
else:
print("Unsupported format: " + args.format)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment