-
-
Save ecarnevale/63892 to your computer and use it in GitHub Desktop.
This file contains hidden or 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
# Generates a series of substitution cipher puzzles. The messages to | |
# "encrypt" are take from a text file passed on the command-line, | |
# where each message is on one line. | |
# | |
# The output is a PDF, "codes.pdf". | |
# | |
# Forked from Jamis Buck's http://gist.github.com/58141 | |
# changed to show code as numbers instead of letters and a hint of solution instead of the key. | |
$LOAD_PATH.unshift(File.join(File.dirname(__FILE__), 'prawnlink', 'lib')) | |
require 'prawn' | |
ALPHABET = ('A'..'Z').to_a.freeze | |
CHARTEST = /[A-Z1234567890]/ | |
# Generate the encrypted version of the given message. Returns a | |
# Hash object including the original message, the encoded message, | |
# and the key (another Hash describing the cipher used). | |
def code_for(message, hintPos=0) | |
#randomized = ALPHABET.sort_by { rand } | |
randomized = (1..ALPHABET.length).sort_by { rand } | |
map = Hash[*ALPHABET.zip(randomized).to_a.flatten] | |
rmap = Hash[*randomized.zip(ALPHABET).to_a.flatten] | |
messageUp = message.strip.upcase | |
if hintPos > 0 then | |
hintStart = 1 + messageUp.split(" ")[0,hintPos-1].join(" ").length | |
hintStop = messageUp.split(" ")[hintPos-1].length + hintStart | |
else | |
hintStart = 0 | |
hintStop = 0 | |
end | |
encoded = messageUp.split(//).map { |c| map[c] || c } | |
{ :message => messageUp.split(//), :encoded => encoded, :key => rmap, :hintStart => hintStart, :hintStop => hintStop } | |
end | |
# Writes the "key" to the PDF in the form of a red line of letters | |
# and a corresponding green line of letters, showing how to decipher | |
# the code. | |
def write_key(doc, result) | |
from = result[:key].keys.sort | |
to = from.map { |char| result[:key][char] } | |
doc.font "Helvetica", :size => 8 | |
y = doc.y - doc.font.ascender - doc.bounds.absolute_bottom | |
margin = 30 | |
doc.fill_color "ff0000" | |
from.each_with_index do |char, index| | |
char = char.to_s | |
width = doc.width_of(char) | |
doc.text(char, :at => [margin + (index+1) * doc.font_size * 1.5 - width/2, y]) | |
end | |
y -= doc.font.height | |
doc.fill_color "008000" | |
to.each_with_index do |char, index| | |
width = doc.width_of(char) | |
doc.text(char, :at => [margin + (index+1) * doc.font_size * 1.5 - width/2, y]) | |
end | |
doc.y = y + doc.bounds.absolute_bottom - doc.font.height | |
end | |
# Writes the given encrypted message (index +number+ into the | |
# +result+ array of ciphers) to the PDF (+doc+). If +divider+ | |
# is true, a horizontal rule is rendered above the puzzle. | |
# | |
# The message is word-wrapped, and the height calculated, so that | |
# a puzzle never spans two pages. (This means the messages need | |
# to be short, or the results are undefined.) | |
def write_puzzle(number, doc, result, divider) | |
doc.font_size = 10 | |
# Calculate where the lines should be broken to wrap the puzzle | |
# onto multiple lines. | |
breaks = [result[:encoded].length] | |
character_width = doc.font_size * 2.5 | |
line_width = doc.bounds.width - doc.font_size * 3 | |
position = 0 | |
length = 0 | |
while position < result[:encoded].length | |
char = result[:encoded][position] | |
breaks[-1] = position if char == ' ' | |
if char.to_s =~ CHARTEST | |
length += character_width | |
else | |
length += doc.font_size | |
end | |
position += 1 | |
if length > line_width | |
position = breaks.last + 1 | |
breaks << result[:encoded].length | |
length = 0 | |
end | |
end | |
y = doc.y - doc.bounds.absolute_bottom | |
# The line-break at the end of the message can be discarded | |
breaks.pop | |
# If the cipher is longer than the remaining space on the page, | |
# start a new page. | |
if y - (breaks.length+1) * doc.font.height * 3 < doc.bounds.absolute_bottom | |
doc.start_new_page | |
y = doc.y - doc.bounds.absolute_bottom | |
divider = false | |
end | |
# Draw a horizontal rule if a divider line is needed | |
if divider | |
doc.move_to 0, y | |
doc.line_to bounds.width, y | |
doc.move_to 0, y - 2 | |
doc.line_to bounds.width, y - 2 | |
doc.stroke | |
y -= doc.font.height * 2 | |
end | |
height = (breaks.length + 1) * doc.font.height * 3 | |
margin = doc.font_size * 2 | |
line = 0 | |
x = doc.font_size * 3 | |
# Draw the puzzle number as an outlined glyph (render mode 1) | |
doc.stroke_color "000000" | |
doc.fill_color "000000" | |
doc.font "Helvetica", :style => :bold, :size => 32 | |
doc.add_content "1 Tr" | |
doc.text(number.to_s, :at => [0,y - doc.font.height/2]) | |
doc.font "Helvetica", :size => 10 | |
doc.add_content "0 Tr" | |
y -= doc.font.height | |
# Draw each line of the encrypted message as blank lines | |
# that can be filled in by pencil. | |
puts result[:hintStart] | |
puts result[:hintStop] | |
result[:encoded].each_with_index do |char, index| | |
# If we're at a line break position, start a new line | |
if index == breaks[line] | |
line += 1 | |
x = doc.font_size * 3 | |
y -= doc.font.height * 3 | |
# If the puzzle is a letter at this position, draw a | |
# blank line with the encrypted letter below it. | |
elsif char.to_s =~ CHARTEST | |
length = character_width - doc.font_size / 2 | |
width = doc.width_of(char.to_s) | |
if ((result[:hintStart]..result[:hintStop]).include?(index+1)) then | |
doc.text(result[:message][index], :at => [x + length/2 - width/2, y]) | |
else | |
doc.move_to x, y | |
doc.line_to x + length, y | |
doc.stroke | |
doc.text(char, :at => [x + length/2 - width/2, y - doc.font.height]) | |
end | |
x += character_width | |
# Other characters we draw directly (punctuation, spaces, etc.) | |
else | |
doc.text(char, :at => [x,y]) | |
x += doc.font_size | |
end | |
end | |
doc.y = y - doc.font.height * 2 + doc.bounds.absolute_bottom | |
end | |
abort "Please specify a text file containing the messages to encrypt" unless ARGV.any? | |
Prawn::Document.generate("codes.pdf", :compress => true) do | |
messages = File.readlines(ARGV.first) | |
messages.each_with_index do |message, index| | |
result = code_for(message, 5) | |
write_puzzle(index+1, self, result, index > 0) | |
#write_key(self, result) | |
self.y -= font.height | |
end | |
end |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment