Browse Source
I will be making revisions based on this, but this is all of his hard work. Thank you!!! Conflicts: squib.gemspecdev
30 changed files with 1606 additions and 3 deletions
@ -0,0 +1,40 @@ |
|||||||
|
require_relative 'arg_loader' |
||||||
|
require_relative 'dir_validator' |
||||||
|
|
||||||
|
module Squib |
||||||
|
# @api private |
||||||
|
module Args |
||||||
|
class OutputFile |
||||||
|
include ArgLoader |
||||||
|
include DirValidator |
||||||
|
|
||||||
|
def initialize(dpi = 300) |
||||||
|
@dpi = 300 |
||||||
|
end |
||||||
|
|
||||||
|
def self.parameters |
||||||
|
{ |
||||||
|
dir: '_output', |
||||||
|
file: 'output.pdf' |
||||||
|
} |
||||||
|
end |
||||||
|
|
||||||
|
def self.expanding_parameters |
||||||
|
[] # none of them |
||||||
|
end |
||||||
|
|
||||||
|
def self.params_with_units |
||||||
|
[] |
||||||
|
end |
||||||
|
|
||||||
|
def validate_dir(arg) |
||||||
|
ensure_dir_created(arg) |
||||||
|
end |
||||||
|
|
||||||
|
def full_filename |
||||||
|
"#{dir}/#{file}" |
||||||
|
end |
||||||
|
|
||||||
|
end |
||||||
|
end |
||||||
|
end |
||||||
@ -0,0 +1,45 @@ |
|||||||
|
require_relative 'arg_loader' |
||||||
|
|
||||||
|
module Squib |
||||||
|
# @api private |
||||||
|
module Args |
||||||
|
# Template file argument loader |
||||||
|
class TemplateFile |
||||||
|
include ArgLoader |
||||||
|
|
||||||
|
def initialize(dsl_method_default = {}) |
||||||
|
@dsl_method_default = dsl_method_default |
||||||
|
end |
||||||
|
|
||||||
|
def self.parameters |
||||||
|
{ |
||||||
|
template_file: nil |
||||||
|
} |
||||||
|
end |
||||||
|
|
||||||
|
def self.expanding_parameters |
||||||
|
[] |
||||||
|
end |
||||||
|
|
||||||
|
def self.params_with_units |
||||||
|
[] # none of them |
||||||
|
end |
||||||
|
|
||||||
|
def validate_template_file(arg) |
||||||
|
return nil if arg.nil? |
||||||
|
|
||||||
|
thefile = File.exist?(arg) ? arg : builtin(arg) |
||||||
|
raise "File #{File.expand_path(arg)} does not exist!" unless |
||||||
|
File.exist? thefile |
||||||
|
|
||||||
|
File.expand_path(thefile) |
||||||
|
end |
||||||
|
|
||||||
|
private |
||||||
|
|
||||||
|
def builtin(file) |
||||||
|
"#{File.dirname(__FILE__)}/../sheet_templates/#{file}" |
||||||
|
end |
||||||
|
end |
||||||
|
end |
||||||
|
end |
||||||
@ -0,0 +1,109 @@ |
|||||||
|
module Squib |
||||||
|
class Margin |
||||||
|
attr_reader :top |
||||||
|
attr_reader :right |
||||||
|
attr_reader :bottom |
||||||
|
attr_reader :left |
||||||
|
|
||||||
|
## |
||||||
|
# Create a new margin definition. |
||||||
|
# |
||||||
|
# Takes +definition+ which can either be a space-separated +String+ or an |
||||||
|
# +Array+ of +Float+ and will translate it to the top, right, bottom and |
||||||
|
# left members. |
||||||
|
# |
||||||
|
# The syntax follows how CSS parses margin shorthand strings. |
||||||
|
def initialize(definition) |
||||||
|
if definition.instance_of? String |
||||||
|
@top, @right, @bottom, @left = expand_shorthand( |
||||||
|
definition.split(/\s+/).map!(&:to_f)) |
||||||
|
elsif definition.is_a? Numeric |
||||||
|
@top, @right, @bottom, @left = expand_shorthand [definition] |
||||||
|
elsif definition.instance_of? Array |
||||||
|
@top, @right, @bottom, @left = expand_shorthand definition |
||||||
|
else |
||||||
|
raise ArgumentError, 'Invalid value, must be either string or array' |
||||||
|
end |
||||||
|
end |
||||||
|
|
||||||
|
## |
||||||
|
# Map out the margin array. |
||||||
|
# |
||||||
|
# Takes +margin_arr+ and attempt to expand it to a strict [top, right, |
||||||
|
# bottom, left] array. |
||||||
|
private def expand_shorthand(margin_arr) |
||||||
|
if margin_arr.size == 1 |
||||||
|
all = margin_arr[0] |
||||||
|
[all, all, all, all] |
||||||
|
elsif margin_arr.size == 2 |
||||||
|
margin_arr + margin_arr |
||||||
|
elsif margin_arr.size == 3 |
||||||
|
margin_arr + [margin_arr[1]] |
||||||
|
elsif margin_arr.size >= 4 |
||||||
|
margin_arr[0..3] |
||||||
|
else |
||||||
|
raise ArgumentError, 'Invalid array' |
||||||
|
end |
||||||
|
end |
||||||
|
end |
||||||
|
|
||||||
|
|
||||||
|
class Gap |
||||||
|
attr_reader :horizontal |
||||||
|
attr_reader :vertical |
||||||
|
|
||||||
|
def initialize(definition) |
||||||
|
if definition.instance_of? String |
||||||
|
@horizontal, @vertical = expand_shorthand( |
||||||
|
definition.split(/\s+/).map!(&:to_f)) |
||||||
|
elsif definition.instance_of? Array |
||||||
|
@horizontal, @vertical = expand_shorthand definition |
||||||
|
elsif definition.is_a? Numeric |
||||||
|
@horizontal, @vertical = definition, definition |
||||||
|
else |
||||||
|
raise ArgumentError, 'Invalid value, must be either string or array' |
||||||
|
end |
||||||
|
end |
||||||
|
|
||||||
|
private def expand_shorthand(gap_arr) |
||||||
|
if gap_arr.size >= 2 |
||||||
|
gap_arr[0..1] |
||||||
|
elsif gap_arr.size == 1 |
||||||
|
gap_arr + gap_arr |
||||||
|
else |
||||||
|
raise ArgumentError, 'Invalid array' |
||||||
|
end |
||||||
|
end |
||||||
|
end |
||||||
|
|
||||||
|
|
||||||
|
class TemplateOption |
||||||
|
attr_accessor :unit |
||||||
|
attr_accessor :sheet_width |
||||||
|
attr_accessor :sheet_height |
||||||
|
attr_writer :sheet_margin |
||||||
|
attr_accessor :sheet_align |
||||||
|
attr_accessor :card_width |
||||||
|
attr_accessor :card_height |
||||||
|
attr_writer :card_gap |
||||||
|
attr_accessor :card_ordering |
||||||
|
attr_accessor :output_file |
||||||
|
attr_accessor :crop_lines |
||||||
|
|
||||||
|
def sheet_margin |
||||||
|
if not @sheet_margin.instance_of? Margin |
||||||
|
@sheet_margin = Margin.new @sheet_margin |
||||||
|
else |
||||||
|
@sheet_margin |
||||||
|
end |
||||||
|
end |
||||||
|
|
||||||
|
def card_gap |
||||||
|
if not @card_gap.instance_of? Gap |
||||||
|
@card_gap = Gap.new @card_gap |
||||||
|
else |
||||||
|
@card_gap |
||||||
|
end |
||||||
|
end |
||||||
|
end |
||||||
|
end |
||||||
@ -0,0 +1,275 @@ |
|||||||
|
require 'fileutils' |
||||||
|
require 'pathname' |
||||||
|
require 'highline' |
||||||
|
require 'bigdecimal' |
||||||
|
require 'yaml' |
||||||
|
require_relative 'data/template_option' |
||||||
|
|
||||||
|
module Squib |
||||||
|
# Squib's command-line option |
||||||
|
module Commands |
||||||
|
# Generate a template definition file that can be used for |
||||||
|
# +save_templated_sheet+ |
||||||
|
# |
||||||
|
# @api public |
||||||
|
class MakeTemplate |
||||||
|
# :nodoc: |
||||||
|
# @api private |
||||||
|
def process(args) |
||||||
|
# Get definitions from the user |
||||||
|
@option = prompt |
||||||
|
|
||||||
|
@printable_edge_right = ( |
||||||
|
@option.sheet_width - @option.sheet_margin.right) |
||||||
|
@printable_edge_bottom = ( |
||||||
|
@option.sheet_height - @option.sheet_margin.bottom) |
||||||
|
@card_iter_x = @option.card_width + @option.card_gap.horizontal |
||||||
|
@card_iter_y = @option.card_height + @option.card_gap.vertical |
||||||
|
|
||||||
|
# Recalculate the sheet margin if the sheet alignment is in the center |
||||||
|
if @option.sheet_align == :center |
||||||
|
@option.sheet_margin = recalculate_center_align_sheet |
||||||
|
end |
||||||
|
|
||||||
|
# We would now have to output the file |
||||||
|
YAML.dump generate_template, File.new(@option.output_file, 'w') |
||||||
|
end |
||||||
|
|
||||||
|
private |
||||||
|
|
||||||
|
# Accept user input that defines the template. |
||||||
|
def prompt |
||||||
|
option = TemplateOption.new |
||||||
|
cli = HighLine.new |
||||||
|
|
||||||
|
option.unit = cli.choose do |menu| |
||||||
|
menu.prompt = 'What measure unit should we use? ' |
||||||
|
menu.choice(:in) |
||||||
|
menu.choice(:cm) |
||||||
|
menu.choice(:mm) |
||||||
|
end |
||||||
|
|
||||||
|
cli.choose do |menu| |
||||||
|
menu.prompt = 'What paper size you are using? ' |
||||||
|
menu.choice('A4, portrait') do |
||||||
|
option.sheet_width = convert_measurement_value( |
||||||
|
210, :mm, option.unit |
||||||
|
) |
||||||
|
option.sheet_height = convert_measurement_value( |
||||||
|
297, :mm, option.unit |
||||||
|
) |
||||||
|
end |
||||||
|
menu.choice('A4, landscape') do |
||||||
|
option.sheet_width = convert_measurement_value( |
||||||
|
297, :mm, option.unit |
||||||
|
) |
||||||
|
option.sheet_height = convert_measurement_value( |
||||||
|
210, :mm, option.unit |
||||||
|
) |
||||||
|
end |
||||||
|
menu.choice('US letter, portrait') do |
||||||
|
option.sheet_width = convert_measurement_value( |
||||||
|
8.5, :in, option.unit |
||||||
|
) |
||||||
|
option.sheet_height = convert_measurement_value( |
||||||
|
11, :in, option.unit |
||||||
|
) |
||||||
|
end |
||||||
|
menu.choice('US letter, landscape') do |
||||||
|
option.sheet_width = convert_measurement_value( |
||||||
|
11, :in, option.unit |
||||||
|
) |
||||||
|
option.sheet_height = convert_measurement_value( |
||||||
|
8.5, :in, option.unit |
||||||
|
) |
||||||
|
end |
||||||
|
menu.choice('Custom size') do |
||||||
|
option.sheet_width = cli.ask( |
||||||
|
"Custom paper width? (#{option.unit}) ", Float |
||||||
|
) |
||||||
|
option.sheet_height = cli.ask( |
||||||
|
"Custom paper height? (#{option.unit}) ", Float |
||||||
|
) |
||||||
|
end |
||||||
|
end |
||||||
|
|
||||||
|
option.sheet_margin = cli.ask( |
||||||
|
"Sheet margins? (#{option.unit}) " |
||||||
|
) do |q| |
||||||
|
q.validate = /^((\d+\.\d+|\d+) ){0,3}(\d+\.\d+|\d+)/ |
||||||
|
end |
||||||
|
option.sheet_align = cli.choose do |menu| |
||||||
|
menu.prompt = 'How to align cards on sheet? [left] ' |
||||||
|
menu.choice(:left) |
||||||
|
menu.choice(:right) |
||||||
|
menu.choice(:center) |
||||||
|
menu.default = :left |
||||||
|
end |
||||||
|
|
||||||
|
option.card_width = cli.ask( |
||||||
|
"Card width? (#{option.unit}) ", Float |
||||||
|
) { |q| q.above = 0 } |
||||||
|
option.card_height = cli.ask( |
||||||
|
"Card height? (#{option.unit}) ", Float |
||||||
|
) { |q| q.above = 0 } |
||||||
|
option.card_gap = cli.ask( |
||||||
|
"Gap between cards? (#{option.unit}) " |
||||||
|
) { |q| q.validate = /^((\d+\.\d+|\d+))(\s+(\d+\.\d+|\d+))?/ } |
||||||
|
|
||||||
|
option.card_ordering = cli.choose do |menu| |
||||||
|
menu.prompt = 'How to layout your cards? [rows]' |
||||||
|
menu.choice(:rows, text: 'In rows') |
||||||
|
menu.choice(:columns, text: 'In columns') |
||||||
|
menu.default = :rows |
||||||
|
end |
||||||
|
|
||||||
|
option.crop_lines = cli.choose do |menu| |
||||||
|
menu.prompt = 'Generate crop lines? [true]' |
||||||
|
menu.choice(:true) |
||||||
|
menu.choice(:false) |
||||||
|
menu.default = :true |
||||||
|
end |
||||||
|
|
||||||
|
option.output_file = cli.ask('Output to? ') do |q| |
||||||
|
q.validate = lambda do |path_str| |
||||||
|
path = Pathname.new path_str |
||||||
|
if path.exist? |
||||||
|
path.writable? && !path.directory? |
||||||
|
else |
||||||
|
path.dirname.writable? |
||||||
|
end |
||||||
|
end |
||||||
|
|
||||||
|
q.responses[:not_valid] = ( |
||||||
|
'The filename specified is not a writable file or is a directory.' |
||||||
|
) |
||||||
|
q.default = 'template.yml' |
||||||
|
end |
||||||
|
|
||||||
|
option |
||||||
|
end |
||||||
|
|
||||||
|
def convert_measurement_value(val, from_unit, to_unit) |
||||||
|
return val if from_unit == to_unit |
||||||
|
|
||||||
|
if from_unit == :in |
||||||
|
val_mm = val * 25.4 |
||||||
|
elsif from_unit == :cm |
||||||
|
val_mm = val * 10.0 |
||||||
|
end |
||||||
|
|
||||||
|
if to_unit == :in |
||||||
|
val_mm / 25.4 |
||||||
|
elsif to_unit == :cm |
||||||
|
val_mm / 10.0 |
||||||
|
else |
||||||
|
val_mm |
||||||
|
end |
||||||
|
end |
||||||
|
|
||||||
|
def generate_template |
||||||
|
x = @option.sheet_margin.left |
||||||
|
y = @option.sheet_margin.top |
||||||
|
cards = [] |
||||||
|
horizontal_crop_lines = Set.new |
||||||
|
vertical_crop_lines = Set.new |
||||||
|
|
||||||
|
while ( |
||||||
|
x + @card_iter_x < @printable_edge_right && |
||||||
|
y + @card_iter_y < @printable_edge_bottom) |
||||||
|
xpos = x + @option.card_gap.horizontal |
||||||
|
ypos = y + @option.card_gap.vertical |
||||||
|
cards.push( |
||||||
|
'x' => "#{xpos}#{@option.unit}", |
||||||
|
'y' => "#{ypos}#{@option.unit}" |
||||||
|
) |
||||||
|
|
||||||
|
# Append the crop lines |
||||||
|
vertical_crop_lines.add xpos |
||||||
|
vertical_crop_lines.add xpos + @option.card_width |
||||||
|
horizontal_crop_lines.add ypos |
||||||
|
horizontal_crop_lines.add ypos + @option.card_height |
||||||
|
|
||||||
|
# Calculate the next iterator |
||||||
|
if @option.card_ordering == :rows |
||||||
|
x, y = next_card_pos_row(x, y) |
||||||
|
elsif @option.card_ordering == :columns |
||||||
|
x, y = next_card_pos_col(x, y) |
||||||
|
else |
||||||
|
raise RunTimeException, 'Invalid card ordering value received' |
||||||
|
end |
||||||
|
end |
||||||
|
|
||||||
|
output = { |
||||||
|
'sheet_width' => "#{@option.sheet_width}#{@option.unit}", |
||||||
|
'sheet_height' => "#{@option.sheet_height}#{@option.unit}", |
||||||
|
'card_width' => "#{@option.card_width}#{@option.unit}", |
||||||
|
'card_height' => "#{@option.card_height}#{@option.unit}", |
||||||
|
'cards' => cards |
||||||
|
} |
||||||
|
|
||||||
|
if @option.crop_lines == :true |
||||||
|
lines = [] |
||||||
|
vertical_crop_lines.each do |val| |
||||||
|
lines.push( |
||||||
|
'type' => :vertical, 'position' => "#{val}#{@option.unit}" |
||||||
|
) |
||||||
|
end |
||||||
|
horizontal_crop_lines.each do |val| |
||||||
|
lines.push( |
||||||
|
'type' => :horizontal, 'position' => "#{val}#{@option.unit}" |
||||||
|
) |
||||||
|
end |
||||||
|
output['crop_line'] = { 'lines' => lines } |
||||||
|
end |
||||||
|
|
||||||
|
# Return the output data |
||||||
|
output |
||||||
|
end |
||||||
|
|
||||||
|
def recalculate_center_align_sheet |
||||||
|
# We will still respect the user specified margins |
||||||
|
printable_width = ( |
||||||
|
@option.sheet_width - @option.sheet_margin.left - |
||||||
|
@option.sheet_margin.right) |
||||||
|
num_of_cols, remainder = printable_width.divmod(@card_iter_x) |
||||||
|
if ( |
||||||
|
@option.card_gap.horizontal > 0 && |
||||||
|
remainder < @option.card_gap.horizontal) |
||||||
|
num_of_cols -= 1 |
||||||
|
end |
||||||
|
|
||||||
|
new_hor_margin = ( |
||||||
|
(@option.sheet_width - num_of_cols * @card_iter_x - |
||||||
|
@option.card_gap.horizontal) / 2) |
||||||
|
Margin.new [ |
||||||
|
@option.sheet_margin.top, |
||||||
|
new_hor_margin, |
||||||
|
@option.sheet_margin.bottom |
||||||
|
] |
||||||
|
end |
||||||
|
|
||||||
|
def next_card_pos_row(x, y) |
||||||
|
x += @card_iter_x |
||||||
|
|
||||||
|
if (x + @card_iter_x) > @printable_edge_right |
||||||
|
x = @option.sheet_margin.left |
||||||
|
y += @card_iter_y |
||||||
|
end |
||||||
|
|
||||||
|
[x, y] |
||||||
|
end |
||||||
|
|
||||||
|
def next_card_pos_col(x, y) |
||||||
|
y += @card_iter_y |
||||||
|
|
||||||
|
if (y + @card_iter_y) > @printable_edge_bottom |
||||||
|
x += @card_iter_x |
||||||
|
y = @option.sheet_margin.top |
||||||
|
end |
||||||
|
|
||||||
|
[x, y] |
||||||
|
end |
||||||
|
end |
||||||
|
end |
||||||
|
end |
||||||
@ -0,0 +1,229 @@ |
|||||||
|
module Squib |
||||||
|
module Graphics |
||||||
|
# Helper class to generate templated sheet. |
||||||
|
class SaveTemplatedSheet |
||||||
|
def initialize(deck, tmpl, outfile) |
||||||
|
@deck = deck |
||||||
|
@tmpl = tmpl |
||||||
|
@page_number = 1 |
||||||
|
@outfile = outfile |
||||||
|
@rotated_delta = (@tmpl.card_width - @deck.width).abs / 2 |
||||||
|
@overlay_lines = @tmpl.crop_lines.select do |line| |
||||||
|
line['overlay_on_cards'] |
||||||
|
end |
||||||
|
end |
||||||
|
|
||||||
|
def render_sheet(range) |
||||||
|
cc = init_cc |
||||||
|
card_set = @tmpl.cards |
||||||
|
per_sheet = card_set.size |
||||||
|
default_angle = @tmpl.card_default_rotation |
||||||
|
if default_angle.zero? |
||||||
|
default_angle = check_card_orientation |
||||||
|
end |
||||||
|
|
||||||
|
draw_overlay_below_cards cc if range.size |
||||||
|
|
||||||
|
track_progress(range) do |bar| |
||||||
|
range.each do |i| |
||||||
|
next_page_if_needed(cc, i, per_sheet) |
||||||
|
|
||||||
|
card = @deck.cards[i] |
||||||
|
slot = card_set[i % per_sheet] |
||||||
|
x = slot['x'] |
||||||
|
y = slot['y'] |
||||||
|
angle = slot['rotate'] != 0 ? slot['rotate'] : default_angle |
||||||
|
|
||||||
|
if angle != 0 |
||||||
|
draw_rotated_card cc, card, x, y, angle |
||||||
|
else |
||||||
|
cc.set_source card.cairo_surface, x, y |
||||||
|
end |
||||||
|
cc.paint |
||||||
|
|
||||||
|
bar.increment |
||||||
|
end |
||||||
|
|
||||||
|
draw_overlay_above_cards cc |
||||||
|
draw_page cc |
||||||
|
cc.target.finish |
||||||
|
end |
||||||
|
end |
||||||
|
|
||||||
|
protected |
||||||
|
|
||||||
|
# Initialize the Cairo Context |
||||||
|
def init_cc |
||||||
|
raise NotImplementedError |
||||||
|
end |
||||||
|
|
||||||
|
def draw_page(cc) |
||||||
|
raise NotImplementedError |
||||||
|
end |
||||||
|
|
||||||
|
def full_filename |
||||||
|
raise NotImplementedError |
||||||
|
end |
||||||
|
|
||||||
|
private |
||||||
|
|
||||||
|
def next_page_if_needed(cc, i, per_sheet) |
||||||
|
return unless (i != 0) && (i % per_sheet).zero? |
||||||
|
|
||||||
|
draw_overlay_above_cards cc |
||||||
|
cc = draw_page cc |
||||||
|
draw_overlay_below_cards cc |
||||||
|
@page_number += 1 |
||||||
|
end |
||||||
|
|
||||||
|
def track_progress(range) |
||||||
|
msg = "Saving templated sheet to #{full_filename}" |
||||||
|
@deck.progress_bar.start(msg, range.size) { |bar| yield(bar) } |
||||||
|
end |
||||||
|
|
||||||
|
def draw_overlay_below_cards(cc) |
||||||
|
if @tmpl.crop_line_overlay == :on_margin |
||||||
|
add_margin_overlay_clip_mask cc |
||||||
|
cc.clip |
||||||
|
draw_crop_line cc, @tmpl.crop_lines |
||||||
|
cc.reset_clip |
||||||
|
elsif @tmpl.crop_line_overlay == :beneath_cards |
||||||
|
draw_crop_line cc, @tmpl.crop_lines |
||||||
|
end |
||||||
|
end |
||||||
|
|
||||||
|
def draw_overlay_above_cards(cc) |
||||||
|
if @tmpl.crop_line_overlay == :overlay_on_cards |
||||||
|
draw_crop_line cc, @tmpl.crop_lines |
||||||
|
else |
||||||
|
draw_crop_line cc, @overlay_lines |
||||||
|
end |
||||||
|
end |
||||||
|
|
||||||
|
def add_margin_overlay_clip_mask(cc) |
||||||
|
margin = @tmpl.margin |
||||||
|
cc.new_path |
||||||
|
cc.rectangle( |
||||||
|
margin[:left], margin[:top], |
||||||
|
margin[:right] - margin[:left], |
||||||
|
margin[:bottom] - margin[:top] |
||||||
|
) |
||||||
|
cc.new_sub_path |
||||||
|
cc.move_to @tmpl.sheet_width, 0 |
||||||
|
cc.line_to 0, 0 |
||||||
|
cc.line_to 0, @tmpl.sheet_height |
||||||
|
cc.line_to @tmpl.sheet_width, @tmpl.sheet_height |
||||||
|
cc.close_path |
||||||
|
end |
||||||
|
|
||||||
|
def draw_crop_line(cc, crop_lines) |
||||||
|
crop_lines.each do |line| |
||||||
|
cc.move_to line['line'].x1, line['line'].y1 |
||||||
|
cc.line_to line['line'].x2, line['line'].y2 |
||||||
|
cc.set_source_color line['color'] |
||||||
|
cc.set_line_width line['width'] |
||||||
|
cc.set_dash(line['style'].pattern) if line['style'].pattern |
||||||
|
cc.stroke |
||||||
|
end |
||||||
|
end |
||||||
|
|
||||||
|
def check_card_orientation |
||||||
|
clockwise = 1.5 * Math::PI |
||||||
|
# Simple detection |
||||||
|
if @deck.width == @tmpl.card_width && @deck.height == @tmpl.card_height |
||||||
|
return 0 |
||||||
|
elsif ( |
||||||
|
@deck.width == @tmpl.card_height && |
||||||
|
@deck.height == @tmpl.card_width) |
||||||
|
Squib.logger.warn { |
||||||
|
'Rotating cards to match card orientation in template.' |
||||||
|
} |
||||||
|
return clockwise |
||||||
|
end |
||||||
|
|
||||||
|
# If the card dimensions doesn't match, warns the user... |
||||||
|
Squib.logger.warn { |
||||||
|
'Card size does not match the template\'s expected card size. '\ |
||||||
|
'Cards may overlap.' |
||||||
|
} |
||||||
|
|
||||||
|
# ... but still try to auto-orient the cards anyway |
||||||
|
is_tmpl_card_landscape = @tmpl.card_width > @tmpl.card_height |
||||||
|
is_deck_card_landscape = @deck.width > @deck.height |
||||||
|
if is_tmpl_card_landscape == is_deck_card_landscape |
||||||
|
clockwise |
||||||
|
else |
||||||
|
0 |
||||||
|
end |
||||||
|
end |
||||||
|
|
||||||
|
def draw_rotated_card(cc, card, x, y, angle) |
||||||
|
# Normalize the angles first |
||||||
|
angle = angle % (2 * Math::PI) |
||||||
|
angle = 2 * Math::PI - angle if angle < 0 |
||||||
|
|
||||||
|
# Determine what's the delta we need to translate our cards |
||||||
|
delta_shift = @deck.width < @deck.height ? 1 : -1 |
||||||
|
if angle.zero? || angle == Math::PI |
||||||
|
delta = 0 |
||||||
|
elsif angle < Math::PI |
||||||
|
delta = -delta_shift * @rotated_delta |
||||||
|
else |
||||||
|
delta = delta_shift * @rotated_delta |
||||||
|
end |
||||||
|
|
||||||
|
# Perform the actual rotation and drawing |
||||||
|
mat = cc.matrix # Save the transformation matrix to revert later |
||||||
|
cc.translate x, y |
||||||
|
cc.translate @deck.width / 2, @deck.height / 2 |
||||||
|
cc.rotate angle |
||||||
|
cc.translate(-@deck.width / 2 + delta, -@deck.height / 2 + delta) |
||||||
|
cc.set_source card.cairo_surface, 0, 0 |
||||||
|
cc.matrix = mat |
||||||
|
end |
||||||
|
end |
||||||
|
|
||||||
|
# Templated sheet renderer in PDF format. |
||||||
|
class SaveTemplatedSheetPDF < SaveTemplatedSheet |
||||||
|
def init_cc |
||||||
|
ratio = 72.0 / @deck.dpi |
||||||
|
|
||||||
|
surface = Cairo::PDFSurface.new( |
||||||
|
full_filename, |
||||||
|
@tmpl.sheet_width * ratio, |
||||||
|
@tmpl.sheet_height * ratio |
||||||
|
) |
||||||
|
|
||||||
|
cc = Cairo::Context.new(surface) |
||||||
|
cc.scale(72.0 / @deck.dpi, 72.0 / @deck.dpi) # make it like pixels |
||||||
|
cc |
||||||
|
end |
||||||
|
|
||||||
|
def draw_page(cc) |
||||||
|
cc.show_page |
||||||
|
cc |
||||||
|
end |
||||||
|
|
||||||
|
def full_filename |
||||||
|
@outfile.full_filename |
||||||
|
end |
||||||
|
end |
||||||
|
|
||||||
|
# Templated sheet renderer in PDF format. |
||||||
|
class SaveTemplatedSheetPNG < SaveTemplatedSheet |
||||||
|
def init_cc |
||||||
|
surface = Cairo::ImageSurface.new @tmpl.sheet_width, @tmpl.sheet_height |
||||||
|
Cairo::Context.new(surface) |
||||||
|
end |
||||||
|
|
||||||
|
def draw_page(cc) |
||||||
|
cc.target.write_to_png(full_filename) |
||||||
|
init_cc |
||||||
|
end |
||||||
|
|
||||||
|
def full_filename |
||||||
|
@outfile.full_filename @page_number |
||||||
|
end |
||||||
|
end |
||||||
|
end |
||||||
|
end |
||||||
@ -0,0 +1,42 @@ |
|||||||
|
--- |
||||||
|
sheet_width: 210mm |
||||||
|
sheet_height: 297mm |
||||||
|
card_width: 59.0mm |
||||||
|
card_height: 92.0mm |
||||||
|
cards: |
||||||
|
- x: 16.5mm |
||||||
|
y: 10.0mm |
||||||
|
- x: 75.5mm |
||||||
|
y: 10.0mm |
||||||
|
- x: 134.5mm |
||||||
|
y: 10.0mm |
||||||
|
- x: 16.5mm |
||||||
|
y: 102.0mm |
||||||
|
- x: 75.5mm |
||||||
|
y: 102.0mm |
||||||
|
- x: 134.5mm |
||||||
|
y: 102.0mm |
||||||
|
- x: 16.5mm |
||||||
|
y: 194.0mm |
||||||
|
- x: 75.5mm |
||||||
|
y: 194.0mm |
||||||
|
- x: 134.5mm |
||||||
|
y: 194.0mm |
||||||
|
crop_line: |
||||||
|
lines: |
||||||
|
- type: :vertical |
||||||
|
position: 16.5mm |
||||||
|
- type: :vertical |
||||||
|
position: 75.5mm |
||||||
|
- type: :vertical |
||||||
|
position: 134.5mm |
||||||
|
- type: :vertical |
||||||
|
position: 193.5mm |
||||||
|
- type: :horizontal |
||||||
|
position: 10.0mm |
||||||
|
- type: :horizontal |
||||||
|
position: 102.0mm |
||||||
|
- type: :horizontal |
||||||
|
position: 194.0mm |
||||||
|
- type: :horizontal |
||||||
|
position: 286.0mm |
||||||
@ -0,0 +1,40 @@ |
|||||||
|
--- |
||||||
|
sheet_width: 297mm |
||||||
|
sheet_height: 210mm |
||||||
|
card_width: 63.0mm |
||||||
|
card_height: 88.0mm |
||||||
|
cards: |
||||||
|
- x: 22.5mm |
||||||
|
y: 10.0mm |
||||||
|
- x: 85.5mm |
||||||
|
y: 10.0mm |
||||||
|
- x: 148.5mm |
||||||
|
y: 10.0mm |
||||||
|
- x: 211.5mm |
||||||
|
y: 10.0mm |
||||||
|
- x: 22.5mm |
||||||
|
y: 98.0mm |
||||||
|
- x: 85.5mm |
||||||
|
y: 98.0mm |
||||||
|
- x: 148.5mm |
||||||
|
y: 98.0mm |
||||||
|
- x: 211.5mm |
||||||
|
y: 98.0mm |
||||||
|
crop_line: |
||||||
|
lines: |
||||||
|
- type: :vertical |
||||||
|
position: 22.5mm |
||||||
|
- type: :vertical |
||||||
|
position: 85.5mm |
||||||
|
- type: :vertical |
||||||
|
position: 148.5mm |
||||||
|
- type: :vertical |
||||||
|
position: 211.5mm |
||||||
|
- type: :vertical |
||||||
|
position: 274.5mm |
||||||
|
- type: :horizontal |
||||||
|
position: 10.0mm |
||||||
|
- type: :horizontal |
||||||
|
position: 98.0mm |
||||||
|
- type: :horizontal |
||||||
|
position: 186.0mm |
||||||
@ -0,0 +1,42 @@ |
|||||||
|
--- |
||||||
|
sheet_width: 210mm |
||||||
|
sheet_height: 297mm |
||||||
|
card_width: 63.0mm |
||||||
|
card_height: 88.0mm |
||||||
|
cards: |
||||||
|
- x: 10.5mm |
||||||
|
y: 10.0mm |
||||||
|
- x: 73.5mm |
||||||
|
y: 10.0mm |
||||||
|
- x: 136.5mm |
||||||
|
y: 10.0mm |
||||||
|
- x: 10.5mm |
||||||
|
y: 98.0mm |
||||||
|
- x: 73.5mm |
||||||
|
y: 98.0mm |
||||||
|
- x: 136.5mm |
||||||
|
y: 98.0mm |
||||||
|
- x: 10.5mm |
||||||
|
y: 186.0mm |
||||||
|
- x: 73.5mm |
||||||
|
y: 186.0mm |
||||||
|
- x: 136.5mm |
||||||
|
y: 186.0mm |
||||||
|
crop_line: |
||||||
|
lines: |
||||||
|
- type: :vertical |
||||||
|
position: 10.5mm |
||||||
|
- type: :vertical |
||||||
|
position: 73.5mm |
||||||
|
- type: :vertical |
||||||
|
position: 136.5mm |
||||||
|
- type: :vertical |
||||||
|
position: 199.5mm |
||||||
|
- type: :horizontal |
||||||
|
position: 10.0mm |
||||||
|
- type: :horizontal |
||||||
|
position: 98.0mm |
||||||
|
- type: :horizontal |
||||||
|
position: 186.0mm |
||||||
|
- type: :horizontal |
||||||
|
position: 274.0mm |
||||||
@ -0,0 +1,42 @@ |
|||||||
|
--- |
||||||
|
sheet_width: 210mm |
||||||
|
sheet_height: 297mm |
||||||
|
card_width: 56.0mm |
||||||
|
card_height: 87.0mm |
||||||
|
cards: |
||||||
|
- x: 21.0mm |
||||||
|
y: 10.0mm |
||||||
|
- x: 77.0mm |
||||||
|
y: 10.0mm |
||||||
|
- x: 133.0mm |
||||||
|
y: 10.0mm |
||||||
|
- x: 21.0mm |
||||||
|
y: 97.0mm |
||||||
|
- x: 77.0mm |
||||||
|
y: 97.0mm |
||||||
|
- x: 133.0mm |
||||||
|
y: 97.0mm |
||||||
|
- x: 21.0mm |
||||||
|
y: 184.0mm |
||||||
|
- x: 77.0mm |
||||||
|
y: 184.0mm |
||||||
|
- x: 133.0mm |
||||||
|
y: 184.0mm |
||||||
|
crop_line: |
||||||
|
lines: |
||||||
|
- type: :vertical |
||||||
|
position: 21.0mm |
||||||
|
- type: :vertical |
||||||
|
position: 77.0mm |
||||||
|
- type: :vertical |
||||||
|
position: 133.0mm |
||||||
|
- type: :vertical |
||||||
|
position: 189.0mm |
||||||
|
- type: :horizontal |
||||||
|
position: 10.0mm |
||||||
|
- type: :horizontal |
||||||
|
position: 97.0mm |
||||||
|
- type: :horizontal |
||||||
|
position: 184.0mm |
||||||
|
- type: :horizontal |
||||||
|
position: 271.0mm |
||||||
@ -0,0 +1,300 @@ |
|||||||
|
require 'yaml' |
||||||
|
require 'classy_hash' |
||||||
|
require_relative 'args/color_validator' |
||||||
|
require_relative 'args/unit_conversion' |
||||||
|
|
||||||
|
module Squib |
||||||
|
# Crop line dash definition |
||||||
|
class CropLineDash |
||||||
|
VALIDATION_REGEX = /%r{ |
||||||
|
^(\d*[.])?\d+(in|cm|mm) |
||||||
|
\s+ |
||||||
|
(\d*[.])?\d+(in|cm|mm)$ |
||||||
|
}x/ |
||||||
|
|
||||||
|
attr_reader :pattern |
||||||
|
|
||||||
|
def initialize(value, dpi) |
||||||
|
if value == :solid |
||||||
|
@pattern = nil |
||||||
|
elsif value == :dotted |
||||||
|
@pattern = [ |
||||||
|
Args::UnitConversion.parse('0.2mm', dpi), |
||||||
|
Args::UnitConversion.parse('0.5mm', dpi) |
||||||
|
] |
||||||
|
elsif value == :dashed |
||||||
|
@pattern = [ |
||||||
|
Args::UnitConversion.parse('2mm', dpi), |
||||||
|
Args::UnitConversion.parse('2mm', dpi) |
||||||
|
] |
||||||
|
elsif value.is_a? String |
||||||
|
@pattern = value.split(' ').map do |val| |
||||||
|
Args::UnitConversion.parse val, dpi |
||||||
|
end |
||||||
|
else |
||||||
|
raise ArgumentError, 'Unsupported dash style' |
||||||
|
end |
||||||
|
end |
||||||
|
end |
||||||
|
|
||||||
|
# Template file |
||||||
|
class Template |
||||||
|
include Args::ColorValidator |
||||||
|
|
||||||
|
# Defaults are set for poker sized deck on a A4 sheet, with no cards |
||||||
|
DEFAULTS = { |
||||||
|
'sheet_width' => nil, |
||||||
|
'sheet_height' => nil, |
||||||
|
'card_width' => nil, |
||||||
|
'card_height' => nil, |
||||||
|
'dpi' => 300, |
||||||
|
'position_reference' => :topleft, |
||||||
|
'rotate' => 0.0, |
||||||
|
'crop_line' => { |
||||||
|
'style' => :solid, |
||||||
|
'width' => '0.02mm', |
||||||
|
'color' => :black, |
||||||
|
'overlay' => :on_margin, |
||||||
|
'lines' => [] |
||||||
|
}, |
||||||
|
'cards' => [] |
||||||
|
}.freeze |
||||||
|
|
||||||
|
attr_reader :dpi |
||||||
|
|
||||||
|
def initialize(template_hash, dpi) |
||||||
|
ClassyHash.validate(template_hash, SCHEMA) |
||||||
|
@template_hash = template_hash |
||||||
|
@dpi = dpi |
||||||
|
@crop_line_default = @template_hash['crop_line'].select do |k, _| |
||||||
|
%w[style width color].include? k |
||||||
|
end |
||||||
|
end |
||||||
|
|
||||||
|
# Load the template definition file |
||||||
|
def self.load(file, dpi) |
||||||
|
yaml = {} |
||||||
|
thefile = File.exist?(file) ? file : builtin(file) |
||||||
|
yaml = YAML.load_file(thefile) || {} if File.exist? thefile |
||||||
|
|
||||||
|
# Bake the default values into our template |
||||||
|
new_hash = DEFAULTS.merge(yaml) |
||||||
|
new_hash['crop_line'] = DEFAULTS['crop_line'].merge( |
||||||
|
new_hash['crop_line'] |
||||||
|
) |
||||||
|
|
||||||
|
# Create a new template file |
||||||
|
warn_unrecognized(yaml) |
||||||
|
Template.new new_hash, dpi |
||||||
|
end |
||||||
|
|
||||||
|
def sheet_width |
||||||
|
Args::UnitConversion.parse @template_hash['sheet_width'], @dpi |
||||||
|
end |
||||||
|
|
||||||
|
def sheet_height |
||||||
|
Args::UnitConversion.parse @template_hash['sheet_height'], @dpi |
||||||
|
end |
||||||
|
|
||||||
|
def card_width |
||||||
|
Args::UnitConversion.parse @template_hash['card_width'], @dpi |
||||||
|
end |
||||||
|
|
||||||
|
def card_height |
||||||
|
Args::UnitConversion.parse @template_hash['card_height'], @dpi |
||||||
|
end |
||||||
|
|
||||||
|
def card_default_rotation |
||||||
|
parse_rotate_param @template_hash['rotate'] |
||||||
|
end |
||||||
|
|
||||||
|
def crop_line_overlay |
||||||
|
@template_hash['crop_line']['overlay'] |
||||||
|
end |
||||||
|
|
||||||
|
def crop_lines |
||||||
|
lines = @template_hash['crop_line']['lines'].map( |
||||||
|
&method(:parse_crop_line) |
||||||
|
) |
||||||
|
if block_given? |
||||||
|
lines.each { |v| yield v } |
||||||
|
else |
||||||
|
lines |
||||||
|
end |
||||||
|
end |
||||||
|
|
||||||
|
def cards |
||||||
|
parsed_cards = @template_hash['cards'].map(&method(:parse_card)) |
||||||
|
if block_given? |
||||||
|
parsed_cards.each { |v| yield v } |
||||||
|
else |
||||||
|
parsed_cards |
||||||
|
end |
||||||
|
end |
||||||
|
|
||||||
|
def margin |
||||||
|
# NOTE: There's a baseline of 0.25mm that we can 100% make sure that we |
||||||
|
# can overlap really thin lines on the PDF |
||||||
|
crop_line_width = [ |
||||||
|
Args::UnitConversion.parse(@template_hash['crop_line']['width'], @dpi), |
||||||
|
Args::UnitConversion.parse('0.25mm', @dpi) |
||||||
|
].max |
||||||
|
|
||||||
|
parsed_cards = cards |
||||||
|
left, right = parsed_cards.minmax { |a, b| a['x'] <=> b['x'] } |
||||||
|
top, bottom = parsed_cards.minmax { |a, b| a['y'] <=> b['y'] } |
||||||
|
|
||||||
|
{ |
||||||
|
left: left['x'] - crop_line_width, |
||||||
|
right: right['x'] + card_width + crop_line_width, |
||||||
|
top: top['y'] - crop_line_width, |
||||||
|
bottom: bottom['y'] + card_height + crop_line_width |
||||||
|
} |
||||||
|
end |
||||||
|
|
||||||
|
# Warn unrecognized options in the template sheet |
||||||
|
def self.warn_unrecognized(yaml) |
||||||
|
unrec = yaml.keys - DEFAULTS.keys |
||||||
|
return unless unrec.any? |
||||||
|
|
||||||
|
Squib.logger.warn( |
||||||
|
"Unrecognized configuration option(s): #{unrec.join(',')}" |
||||||
|
) |
||||||
|
end |
||||||
|
|
||||||
|
private |
||||||
|
|
||||||
|
# Template file schema |
||||||
|
UNIT_REGEX = /^(\d*[.])?\d+(in|cm|mm)$/ |
||||||
|
ROTATE_REGEX = /^(\d*[.])?\d+(deg|rad)?$/ |
||||||
|
SCHEMA = { |
||||||
|
'sheet_width' => UNIT_REGEX, |
||||||
|
'sheet_height' => UNIT_REGEX, |
||||||
|
'card_width' => UNIT_REGEX, |
||||||
|
'card_height' => UNIT_REGEX, |
||||||
|
'position_reference' => ClassyHash::G.enum(:topleft, :center), |
||||||
|
'rotate' => [ |
||||||
|
:optional, Numeric, |
||||||
|
ClassyHash::G.enum(:clockwise, :counterclockwise, :turnaround), |
||||||
|
ROTATE_REGEX |
||||||
|
], |
||||||
|
'crop_line' => { |
||||||
|
'style' => [ |
||||||
|
ClassyHash::G.enum(:solid, :dotted, :dashed), |
||||||
|
CropLineDash::VALIDATION_REGEX |
||||||
|
], |
||||||
|
'width' => UNIT_REGEX, |
||||||
|
'color' => [String, Symbol], |
||||||
|
'overlay' => ClassyHash::G.enum( |
||||||
|
:on_margin, :overlay_on_cards, :beneath_cards |
||||||
|
), |
||||||
|
'lines' => [[{ |
||||||
|
'type' => ClassyHash::G.enum(:horizontal, :vertical), |
||||||
|
'position' => UNIT_REGEX, |
||||||
|
'style' => [ |
||||||
|
:optional, ClassyHash::G.enum(:solid, :dotted, :dashed) |
||||||
|
], |
||||||
|
'width' => [:optional, UNIT_REGEX], |
||||||
|
'color' => [:optional, String, Symbol], |
||||||
|
'overlay_on_cards' => [:optional, TrueClass] |
||||||
|
}]] |
||||||
|
}, |
||||||
|
'cards' => [[{ |
||||||
|
'x' => UNIT_REGEX, |
||||||
|
'y' => UNIT_REGEX, |
||||||
|
# NOTE: Don't think that we should specify rotation on a per card |
||||||
|
# basis, but just included here for now |
||||||
|
'rotate' => [ |
||||||
|
:optional, Numeric, |
||||||
|
ClassyHash::G.enum(:clockwise, :counterclockwise, :turnaround), |
||||||
|
ROTATE_REGEX |
||||||
|
] |
||||||
|
}]] |
||||||
|
}.freeze |
||||||
|
|
||||||
|
# Return path for built-in sheet templates |
||||||
|
def self.builtin(file) |
||||||
|
"#{File.dirname(__FILE__)}/sheet_templates/#{file}" |
||||||
|
end |
||||||
|
|
||||||
|
# Parse crop line definitions from template. |
||||||
|
def parse_crop_line(line) |
||||||
|
new_line = @crop_line_default.merge line |
||||||
|
new_line['width'] = Args::UnitConversion.parse(new_line['width'], @dpi) |
||||||
|
new_line['color'] = colorify new_line['color'] |
||||||
|
new_line['style_desc'] = new_line['style'] |
||||||
|
new_line['style'] = CropLineDash.new(new_line['style'], @dpi) |
||||||
|
new_line['line'] = CropLine.new( |
||||||
|
new_line['type'], new_line['position'], sheet_width, sheet_height, @dpi |
||||||
|
) |
||||||
|
new_line |
||||||
|
end |
||||||
|
|
||||||
|
# Parse card definitions from template. |
||||||
|
def parse_card(card) |
||||||
|
new_card = card.clone |
||||||
|
|
||||||
|
x = Args::UnitConversion.parse(card['x'], @dpi) |
||||||
|
y = Args::UnitConversion.parse(card['y'], @dpi) |
||||||
|
if @template_hash['position_reference'] == :center |
||||||
|
# Normalize it to a top-left positional reference |
||||||
|
x -= card_width / 2 |
||||||
|
y -= card_height / 2 |
||||||
|
end |
||||||
|
|
||||||
|
new_card['x'] = x |
||||||
|
new_card['y'] = y |
||||||
|
new_card['rotate'] = parse_rotate_param( |
||||||
|
card['rotate'] ? card['rotate'] : @template_hash['rotate']) |
||||||
|
new_card |
||||||
|
end |
||||||
|
|
||||||
|
def parse_rotate_param(val) |
||||||
|
if val == :clockwise |
||||||
|
0.5 * Math::PI |
||||||
|
elsif val == :counterclockwise |
||||||
|
1.5 * Math::PI |
||||||
|
elsif val == :turnaround |
||||||
|
Math::PI |
||||||
|
elsif val.is_a? String |
||||||
|
if val.end_with? 'deg' |
||||||
|
val.gsub(/deg$/, '').to_f / 180 * Math::PI |
||||||
|
elsif val.end_with? 'rad' |
||||||
|
val.gsub(/rad$/, '').to_f |
||||||
|
else |
||||||
|
val.to_f |
||||||
|
end |
||||||
|
elsif val.nil? |
||||||
|
0.0 |
||||||
|
else |
||||||
|
val.to_f |
||||||
|
end |
||||||
|
end |
||||||
|
end |
||||||
|
|
||||||
|
# Crop line definition |
||||||
|
class CropLine |
||||||
|
attr_reader :x1, :y1, :x2, :y2 |
||||||
|
|
||||||
|
def initialize(type, position, sheet_width, sheet_height, dpi) |
||||||
|
method = "parse_#{type}" |
||||||
|
send method, position, sheet_width, sheet_height, dpi |
||||||
|
end |
||||||
|
|
||||||
|
def parse_horizontal(position, sheet_width, _, dpi) |
||||||
|
position = Args::UnitConversion.parse(position, dpi) |
||||||
|
@x1 = 0 |
||||||
|
@y1 = position |
||||||
|
@x2 = sheet_width |
||||||
|
@y2 = position |
||||||
|
end |
||||||
|
|
||||||
|
def parse_vertical(position, _, sheet_height, dpi) |
||||||
|
position = Args::UnitConversion.parse(position, dpi) |
||||||
|
@x1 = position |
||||||
|
@y1 = 0 |
||||||
|
@x2 = position |
||||||
|
@y2 = sheet_height |
||||||
|
end |
||||||
|
end |
||||||
|
end |
||||||
@ -0,0 +1,10 @@ |
|||||||
|
require 'squib' |
||||||
|
|
||||||
|
Squib::Deck.new(width: '63mm', height: '88mm', cards: 8) do |
||||||
|
rect fill_color: :gray |
||||||
|
text( |
||||||
|
str: %w[Front_1 Front_2 Front_3 Front_4 Back_1 Back_2 Back_3 Back_4], |
||||||
|
x: '3mm', y: '3mm' |
||||||
|
) |
||||||
|
save_pdf file: 'fold_sheet.pdf', template_file: 'templates/fold_sheet.yml' |
||||||
|
end |
||||||
@ -0,0 +1,14 @@ |
|||||||
|
require 'squib' |
||||||
|
|
||||||
|
Squib::Deck.new(width: '65.8mm', height: '76mm', cards: 9) do |
||||||
|
polygon( |
||||||
|
x: '32.9mm', y: '38mm', n: 6, radius: '38mm', angle: 1.571, |
||||||
|
stroke_color: :black, stroke_width: '0.014in', fill_color: :pink |
||||||
|
) |
||||||
|
text( |
||||||
|
str: %w[One Two Three Four Five Six Seven Eight Nine], |
||||||
|
x: '27mm', y: '35mm', width: '11.8mm', height: '6mm', |
||||||
|
align: :center, valign: :middle |
||||||
|
) |
||||||
|
save_pdf file: 'hex_tiles.pdf', template_file: 'templates/hex_tiles.yml' |
||||||
|
end |
||||||
@ -0,0 +1,57 @@ |
|||||||
|
--- |
||||||
|
sheet_width: 297mm |
||||||
|
sheet_height: 210mm |
||||||
|
card_width: 63.0mm |
||||||
|
card_height: 88.0mm |
||||||
|
cards: |
||||||
|
- x: 16.5mm |
||||||
|
y: 13.0mm |
||||||
|
- x: 83.5mm |
||||||
|
y: 13.0mm |
||||||
|
- x: 150.5mm |
||||||
|
y: 13.0mm |
||||||
|
- x: 217.5mm |
||||||
|
y: 13.0mm |
||||||
|
- x: 16.5mm |
||||||
|
y: 109.0mm |
||||||
|
rotate: :turnaround |
||||||
|
- x: 83.5mm |
||||||
|
y: 109.0mm |
||||||
|
rotate: :turnaround |
||||||
|
- x: 150.5mm |
||||||
|
y: 109.0mm |
||||||
|
rotate: :turnaround |
||||||
|
- x: 217.5mm |
||||||
|
y: 109.0mm |
||||||
|
rotate: :turnaround |
||||||
|
crop_line: |
||||||
|
lines: |
||||||
|
- type: :vertical |
||||||
|
position: 16.5mm |
||||||
|
- type: :vertical |
||||||
|
position: 79.5mm |
||||||
|
- type: :vertical |
||||||
|
position: 83.5mm |
||||||
|
- type: :vertical |
||||||
|
position: 146.5mm |
||||||
|
- type: :vertical |
||||||
|
position: 150.5mm |
||||||
|
- type: :vertical |
||||||
|
position: 213.5mm |
||||||
|
- type: :vertical |
||||||
|
position: 217.5mm |
||||||
|
- type: :vertical |
||||||
|
position: 280.5mm |
||||||
|
- type: :horizontal |
||||||
|
position: 13.0mm |
||||||
|
- type: :horizontal |
||||||
|
position: 101.0mm |
||||||
|
- type: :horizontal |
||||||
|
position: 109.0mm |
||||||
|
- type: :horizontal |
||||||
|
position: 197.0mm |
||||||
|
- type: :horizontal |
||||||
|
position: 105.0mm |
||||||
|
style: :dashed |
||||||
|
color: :red |
||||||
|
overlay_on_cards: true |
||||||
@ -0,0 +1,24 @@ |
|||||||
|
--- |
||||||
|
sheet_width: 210mm |
||||||
|
sheet_height: 297.000000mm |
||||||
|
card_width: 65.800000mm |
||||||
|
card_height: 76.000000mm |
||||||
|
cards: |
||||||
|
- x: 6.300000mm |
||||||
|
y: 5.000000mm |
||||||
|
- x: 6.300000mm |
||||||
|
y: 81.000000mm |
||||||
|
- x: 6.300000mm |
||||||
|
y: 157.000000mm |
||||||
|
- x: 72.100000mm |
||||||
|
y: 43.000000mm |
||||||
|
- x: 72.100000mm |
||||||
|
y: 119.000000mm |
||||||
|
- x: 72.100000mm |
||||||
|
y: 195.000000mm |
||||||
|
- x: 137.900000mm |
||||||
|
y: 5.000000mm |
||||||
|
- x: 137.900000mm |
||||||
|
y: 81.000000mm |
||||||
|
- x: 137.900000mm |
||||||
|
y: 157.000000mm |
||||||
@ -0,0 +1,8 @@ |
|||||||
|
require 'squib' |
||||||
|
|
||||||
|
Squib::Deck.new(width: '63mm', height: '88mm', cards: 9) do |
||||||
|
text( |
||||||
|
str: %w[One Two Three Four Five Six Seven Eight Nine], x: '3mm', y: '3mm' |
||||||
|
) |
||||||
|
save_pdf file: 'use_package_tmpl.pdf', template_file: 'a4_poker_card_9up.yml' |
||||||
|
end |
||||||
@ -0,0 +1,7 @@ |
|||||||
|
sheet_width: 8.5in |
||||||
|
sheet_height: 11in |
||||||
|
card_width: 2.5in |
||||||
|
card_height: 3.5in |
||||||
|
cards: |
||||||
|
- x: 0.5in |
||||||
|
y: 1in |
||||||
@ -0,0 +1,22 @@ |
|||||||
|
sheet_width: 8.5in |
||||||
|
sheet_height: 11in |
||||||
|
card_width: 2in |
||||||
|
card_height: 3in |
||||||
|
position_reference: :center |
||||||
|
cards: |
||||||
|
- x: 1.25in |
||||||
|
y: 2.5in |
||||||
|
- x: 3.25in |
||||||
|
y: 2.5in |
||||||
|
- x: 5.25in |
||||||
|
y: 2.5in |
||||||
|
- x: 7.25in |
||||||
|
y: 2.5in |
||||||
|
- x: 1.25in |
||||||
|
y: 5.5in |
||||||
|
- x: 3.25in |
||||||
|
y: 5.5in |
||||||
|
- x: 5.25in |
||||||
|
y: 5.5in |
||||||
|
- x: 7.25in |
||||||
|
y: 5.5in |
||||||
@ -0,0 +1,23 @@ |
|||||||
|
sheet_width: 8.5in |
||||||
|
sheet_height: 11in |
||||||
|
card_width: 2.5in |
||||||
|
card_height: 3.5in |
||||||
|
rotate: :clockwise |
||||||
|
cards: |
||||||
|
- x: 0.5in |
||||||
|
y: 1in |
||||||
|
- x: 3.0in |
||||||
|
y: 1in |
||||||
|
rotate: :counterclockwise |
||||||
|
- x: 3.5in |
||||||
|
y: 1in |
||||||
|
rotate: :turnaround |
||||||
|
- x: 0.5in |
||||||
|
y: 4.5in |
||||||
|
rotate: 1 |
||||||
|
- x: 3.0in |
||||||
|
y: 4.5in |
||||||
|
rotate: 60deg |
||||||
|
- x: 3.5in |
||||||
|
y: 4.5in |
||||||
|
rotate: 1.25rad |
||||||
@ -0,0 +1,28 @@ |
|||||||
|
sheet_width: 8.5in |
||||||
|
sheet_height: 11in |
||||||
|
card_width: 2in |
||||||
|
card_height: 3in |
||||||
|
crop_line: |
||||||
|
style: :dashed |
||||||
|
width: 0.1in |
||||||
|
color: :pink |
||||||
|
overlay: :overlay_on_cards |
||||||
|
lines: |
||||||
|
- type: :horizontal |
||||||
|
position: 1in |
||||||
|
- type: :horizontal |
||||||
|
position: 2in |
||||||
|
width: 0.2in |
||||||
|
- type: :horizontal |
||||||
|
position: 3in |
||||||
|
style: :dotted |
||||||
|
- type: :horizontal |
||||||
|
position: 4in |
||||||
|
color: "#ff0000" |
||||||
|
- type: :horizontal |
||||||
|
position: 5in |
||||||
|
width: 0.3in |
||||||
|
style: :solid |
||||||
|
color: :blue |
||||||
|
- type: :vertical |
||||||
|
position: 6in |
||||||
@ -0,0 +1,6 @@ |
|||||||
|
sheet_width: 8.5in |
||||||
|
sheet_height: 11in |
||||||
|
card_width: 2.5in |
||||||
|
cards: |
||||||
|
- x: 0.5in |
||||||
|
y: 1in |
||||||
@ -0,0 +1,6 @@ |
|||||||
|
sheet_width: 8.5in |
||||||
|
sheet_height: 11in |
||||||
|
card_height: 3.5in |
||||||
|
cards: |
||||||
|
- x: 0.5in |
||||||
|
y: 1in |
||||||
@ -0,0 +1,6 @@ |
|||||||
|
sheet_width: 8.5in |
||||||
|
card_width: 2.5in |
||||||
|
card_height: 3.5in |
||||||
|
cards: |
||||||
|
- x: 0.5in |
||||||
|
y: 1in |
||||||
@ -0,0 +1,6 @@ |
|||||||
|
sheet_height: 11in |
||||||
|
card_width: 2.5in |
||||||
|
card_height: 3.5in |
||||||
|
cards: |
||||||
|
- x: 0.5in |
||||||
|
y: 1in |
||||||
@ -0,0 +1,181 @@ |
|||||||
|
require 'spec_helper' |
||||||
|
|
||||||
|
describe Squib::Template do |
||||||
|
it 'loads a template' do |
||||||
|
tmpl = Squib::Template.load(template_file('basic.yml'), 100) |
||||||
|
expect(tmpl.sheet_width).to eq(850) |
||||||
|
expect(tmpl.sheet_height).to eq(1100) |
||||||
|
expect(tmpl.card_width).to eq(250) |
||||||
|
expect(tmpl.card_height).to eq(350) |
||||||
|
expect(tmpl.dpi).to eq(100) |
||||||
|
expect(tmpl.crop_line_overlay).to eq( |
||||||
|
Squib::Template::DEFAULTS['crop_line']['overlay'] |
||||||
|
) |
||||||
|
expect(tmpl.crop_lines).to eq([]) |
||||||
|
expect(tmpl.cards).to eq([{ 'x' => 50, 'y' => 100, 'rotate' => 0 }]) |
||||||
|
end |
||||||
|
|
||||||
|
it 'loads from the default templates if none exists' do |
||||||
|
tmpl = Squib::Template.load('a4_poker_card_9up.yml', 100) |
||||||
|
expect(tmpl.sheet_width).to eq(826.7716527) |
||||||
|
expect(tmpl.sheet_height).to eq(1169.2913373899999) |
||||||
|
expect(tmpl.card_width).to eq(248.03149580999997) |
||||||
|
expect(tmpl.card_height).to eq(346.45669256) |
||||||
|
expect(tmpl.dpi).to eq(100) |
||||||
|
|
||||||
|
expect(tmpl.crop_lines.length).to eq(8) |
||||||
|
expect(tmpl.crop_lines.map { |line| line['type'] }).to eq( |
||||||
|
%i[ |
||||||
|
vertical vertical vertical vertical |
||||||
|
horizontal horizontal horizontal horizontal |
||||||
|
] |
||||||
|
) |
||||||
|
expect(tmpl.crop_lines.map { |line| line['line'].x1 }).to eq( |
||||||
|
[ |
||||||
|
41.338582635, 289.370078445, 537.401574255, 785.433070065, |
||||||
|
0, 0, 0, 0 |
||||||
|
] |
||||||
|
) |
||||||
|
expect(tmpl.crop_lines.map { |line| line['line'].x2 }).to eq( |
||||||
|
[ |
||||||
|
41.338582635, 289.370078445, 537.401574255, 785.433070065, |
||||||
|
826.7716527, 826.7716527, 826.7716527, 826.7716527 |
||||||
|
] |
||||||
|
) |
||||||
|
expect(tmpl.crop_lines.map { |line| line['line'].y1 }).to eq( |
||||||
|
[ |
||||||
|
0, 0, 0, 0, |
||||||
|
39.3700787, 385.82677126, 732.28346382, 1078.74015638 |
||||||
|
] |
||||||
|
) |
||||||
|
expect(tmpl.crop_lines.map { |line| line['line'].y2 }).to eq( |
||||||
|
[ |
||||||
|
1169.2913373899999, 1169.2913373899999, 1169.2913373899999, |
||||||
|
1169.2913373899999, |
||||||
|
39.3700787, 385.82677126, 732.28346382, 1078.74015638 |
||||||
|
] |
||||||
|
) |
||||||
|
|
||||||
|
expect(tmpl.cards.length).to eq(9) |
||||||
|
expect(tmpl.cards.map { |card| card['x'] }).to eq( |
||||||
|
[41.338582635, 289.370078445, 537.401574255] * 3 |
||||||
|
) |
||||||
|
expect(tmpl.cards.map { |card| card['y'] }).to eq( |
||||||
|
[ |
||||||
|
39.3700787, 39.3700787, 39.3700787, |
||||||
|
385.82677126, 385.82677126, 385.82677126, |
||||||
|
732.28346382, 732.28346382, 732.28346382 |
||||||
|
] |
||||||
|
) |
||||||
|
|
||||||
|
expect(tmpl.margin).to eq( |
||||||
|
left: 40.3543306675, |
||||||
|
right: 786.4173220325, |
||||||
|
top: 38.3858267325, |
||||||
|
bottom: 1079.7244083475 |
||||||
|
) |
||||||
|
end |
||||||
|
|
||||||
|
it 'loads a template with the coordinates specifying the middle of cards' do |
||||||
|
tmpl = Squib::Template.load(template_file('card_center_coord.yml'), 100) |
||||||
|
expect(tmpl.sheet_width).to eq(850) |
||||||
|
expect(tmpl.sheet_height).to eq(1100) |
||||||
|
expect(tmpl.card_width).to eq(200) |
||||||
|
expect(tmpl.card_height).to eq(300) |
||||||
|
expect(tmpl.dpi).to eq(100) |
||||||
|
|
||||||
|
expect(tmpl.cards.length).to eq(8) |
||||||
|
expect(tmpl.cards.map { |card| card['x'] }).to eq( |
||||||
|
[25.0, 225.0, 425.0, 625.0] * 2 |
||||||
|
) |
||||||
|
expect(tmpl.cards.map { |card| card['y'] }).to eq( |
||||||
|
[100.0, 100.0, 100.0, 100.0, 400.0, 400.0, 400.0, 400.0] |
||||||
|
) |
||||||
|
end |
||||||
|
|
||||||
|
it 'loads a template with customized crop lines' do |
||||||
|
tmpl = Squib::Template.load(template_file('custom_crop_lines.yml'), 100) |
||||||
|
expect(tmpl.sheet_width).to eq(850) |
||||||
|
expect(tmpl.sheet_height).to eq(1100) |
||||||
|
expect(tmpl.card_width).to eq(200) |
||||||
|
expect(tmpl.card_height).to eq(300) |
||||||
|
expect(tmpl.dpi).to eq(100) |
||||||
|
expect(tmpl.crop_line_overlay).to eq(:overlay_on_cards) |
||||||
|
|
||||||
|
expect(tmpl.crop_lines.length).to eq(6) |
||||||
|
expect(tmpl.crop_lines.map { |line| line['type'] }).to eq( |
||||||
|
%i[horizontal horizontal horizontal horizontal horizontal vertical] |
||||||
|
) |
||||||
|
expect(tmpl.crop_lines.map { |line| line['line'].x1 }).to eq( |
||||||
|
[0, 0, 0, 0, 0, 600] |
||||||
|
) |
||||||
|
expect(tmpl.crop_lines.map { |line| line['line'].x2 }).to eq( |
||||||
|
[850, 850, 850, 850, 850, 600] |
||||||
|
) |
||||||
|
expect(tmpl.crop_lines.map { |line| line['line'].y1 }).to eq( |
||||||
|
[100, 200, 300, 400, 500, 0] |
||||||
|
) |
||||||
|
expect(tmpl.crop_lines.map { |line| line['line'].y2 }).to eq( |
||||||
|
[100, 200, 300, 400, 500, 1100] |
||||||
|
) |
||||||
|
expect(tmpl.crop_lines.map { |line| line['style_desc'] }).to eq( |
||||||
|
%i[dashed dashed dotted dashed solid dashed] |
||||||
|
) |
||||||
|
expect(tmpl.crop_lines.map { |line| line['width'] }).to eq( |
||||||
|
[10, 20, 10, 10, 30, 10] |
||||||
|
) |
||||||
|
expect(tmpl.crop_lines.map { |line| line['color'] }).to eq( |
||||||
|
['pink', 'pink', 'pink', '#ff0000', 'blue', 'pink'] |
||||||
|
) |
||||||
|
end |
||||||
|
|
||||||
|
it 'loads a template with rotated cards' do |
||||||
|
tmpl = Squib::Template.load(template_file('card_rotation.yml'), 100) |
||||||
|
expect(tmpl.sheet_width).to eq(850) |
||||||
|
expect(tmpl.sheet_height).to eq(1100) |
||||||
|
expect(tmpl.card_width).to eq(250) |
||||||
|
expect(tmpl.card_height).to eq(350) |
||||||
|
expect(tmpl.dpi).to eq(100) |
||||||
|
|
||||||
|
expect(tmpl.cards.length).to eq(6) |
||||||
|
expect(tmpl.cards.map { |card| card['rotate'] }).to eq( |
||||||
|
[0.5 * Math::PI, 1.5 * Math::PI, Math::PI, 1, Math::PI / 3, 1.25] |
||||||
|
) |
||||||
|
end |
||||||
|
|
||||||
|
it 'fails when sheet_width is not defined' do |
||||||
|
expect do |
||||||
|
Squib::Template.load(template_file('fail_no_sheet_width.yml'), 100) |
||||||
|
end.to raise_error( |
||||||
|
RuntimeError, |
||||||
|
'"sheet_width" is not a String matching /^(\d*[.])?\d+(in|cm|mm)$/' |
||||||
|
) |
||||||
|
end |
||||||
|
|
||||||
|
it 'fails when sheet_height is not defined' do |
||||||
|
expect do |
||||||
|
Squib::Template.load(template_file('fail_no_sheet_height.yml'), 100) |
||||||
|
end.to raise_error( |
||||||
|
RuntimeError, |
||||||
|
'"sheet_height" is not a String matching /^(\d*[.])?\d+(in|cm|mm)$/' |
||||||
|
) |
||||||
|
end |
||||||
|
|
||||||
|
it 'fails when card_width is not defined' do |
||||||
|
expect do |
||||||
|
Squib::Template.load(template_file('fail_no_card_width.yml'), 100) |
||||||
|
end.to raise_error( |
||||||
|
RuntimeError, |
||||||
|
'"card_width" is not a String matching /^(\d*[.])?\d+(in|cm|mm)$/' |
||||||
|
) |
||||||
|
end |
||||||
|
|
||||||
|
it 'fails when card_height is not defined' do |
||||||
|
expect do |
||||||
|
Squib::Template.load(template_file('fail_no_card_height.yml'), 100) |
||||||
|
end.to raise_error( |
||||||
|
RuntimeError, |
||||||
|
'"card_height" is not a String matching /^(\d*[.])?\d+(in|cm|mm)$/' |
||||||
|
) |
||||||
|
end |
||||||
|
end |
||||||
Loading…
Reference in new issue