Last active
December 18, 2015 10:49
-
-
Save malleusinferni/5770905 to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/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