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