Squash @felixleong's pdf-template feature.
I will be making revisions based on this, but this is all of his hard work. Thank you!!! Conflicts: squib.gemspecdev
parent
c6aa3983c4
commit
25bf0d7a67
|
|
@ -6,9 +6,12 @@ Squib follows [semantic versioning](http://semver.org).
|
|||
Features:
|
||||
* `circle` method now supports various `arc` options, so you can draw partial circles (#211) by @sparr
|
||||
* `save_sheet` method now supports `rtl` or "right-to-left", for easier duplex printing of backs (#204, #208) by @sparr
|
||||
<<<<<<< HEAD
|
||||
* `yaml` method for reading in data, much like `csv` and `xlsx` by @blinks
|
||||
* `save_pdf/save_sheet` method now supports `template_file`, which allows you to define
|
||||
template layouts and position your cards freely (#217) by @felixleong
|
||||
|
||||
Special thanks to @sparr and @blinks for all of their work!!
|
||||
Special thanks to @sparr, @blinks and @felixleong for all of their work!!
|
||||
|
||||
## v0.13.4 / 2017-07-17
|
||||
|
||||
|
|
|
|||
|
|
@ -16,4 +16,13 @@ Mercenary.program(:squib) do |p|
|
|||
end
|
||||
end
|
||||
|
||||
p.command(:make_template) do |c|
|
||||
c.syntax 'make_template'
|
||||
c.description 'Creates a template definition file to generate a templated PDF.'
|
||||
|
||||
c.action do |args, options|
|
||||
Squib::Commands::MakeTemplate.new.process(args)
|
||||
end
|
||||
end
|
||||
|
||||
end
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ require 'pango'
|
|||
require 'rsvg2'
|
||||
require_relative 'squib/version'
|
||||
require_relative 'squib/commands/new'
|
||||
require_relative 'squib/commands/make_template'
|
||||
require_relative 'squib/deck'
|
||||
require_relative 'squib/card'
|
||||
|
||||
|
|
|
|||
|
|
@ -1,9 +1,13 @@
|
|||
require_relative '../template'
|
||||
require_relative '../args/card_range'
|
||||
require_relative '../args/hand_special'
|
||||
require_relative '../args/save_batch'
|
||||
require_relative '../args/sheet'
|
||||
require_relative '../args/template_file'
|
||||
require_relative '../args/output_file'
|
||||
require_relative '../args/showcase_special'
|
||||
require_relative '../graphics/save_pdf'
|
||||
require_relative '../graphics/save_templated_sheet'
|
||||
|
||||
module Squib
|
||||
class Deck
|
||||
|
|
@ -19,7 +23,16 @@ module Squib
|
|||
def save_pdf(opts = {})
|
||||
range = Args::CardRange.new(opts[:range], deck_size: size)
|
||||
sheet = Args::Sheet.new(custom_colors, { file: 'output.pdf' }).load!(opts, expand_by: size, layout: layout, dpi: dpi)
|
||||
Graphics::SavePDF.new(self).render_pdf(range, sheet)
|
||||
tmpl_file = Args::TemplateFile.new.load!(opts, expand_by: size)
|
||||
|
||||
if tmpl_file.template_file.nil?
|
||||
Graphics::SavePDF.new(self).render_pdf(range, sheet)
|
||||
else
|
||||
tmpl = Template.load tmpl_file.template_file, dpi
|
||||
Graphics::SaveTemplatedSheetPDF.new(self, tmpl, sheet).render_sheet(
|
||||
range
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
# DSL method. See http://squib.readthedocs.io
|
||||
|
|
@ -39,7 +52,16 @@ module Squib
|
|||
range = Args::CardRange.new(opts[:range], deck_size: size)
|
||||
batch = Args::SaveBatch.new.load!(opts, expand_by: size, layout: layout, dpi: dpi)
|
||||
sheet = Args::Sheet.new(custom_colors, { margin: 0 }, size).load!(opts, expand_by: size, layout: layout, dpi: dpi)
|
||||
render_sheet(range, batch, sheet)
|
||||
tmpl_file = Args::TemplateFile.new.load!(opts, expand_by: size)
|
||||
|
||||
if tmpl_file.template_file.nil?
|
||||
render_sheet(range, batch, sheet)
|
||||
else
|
||||
tmpl = Template.load tmpl_file.template_file, dpi
|
||||
Graphics::SaveTemplatedSheetPNG.new(self, tmpl, batch).render_sheet(
|
||||
range
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
# DSL method. See http://squib.readthedocs.io
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -50,6 +50,10 @@ def yaml_file(file)
|
|||
"#{File.expand_path(File.dirname(__FILE__))}/data/yaml/#{file}"
|
||||
end
|
||||
|
||||
def template_file(file)
|
||||
"#{File.expand_path(File.dirname(__FILE__))}/data/templates/#{file}"
|
||||
end
|
||||
|
||||
def project_template(file)
|
||||
"#{File.expand_path(File.dirname(__FILE__))}/../lib/squib/project_template/#{file}"
|
||||
end
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -38,6 +38,8 @@ Gem::Specification.new do |spec|
|
|||
spec.add_runtime_dependency 'rsvg2', '3.1.8'
|
||||
spec.add_runtime_dependency 'roo', '2.7.1'
|
||||
spec.add_runtime_dependency 'ruby-progressbar', '1.8.1'
|
||||
spec.add_runtime_dependency 'highline', '1.7.8'
|
||||
spec.add_runtime_dependency 'classy_hash', '0.1.5'
|
||||
|
||||
spec.add_development_dependency 'bundler'
|
||||
spec.add_development_dependency 'rake'
|
||||
|
|
|
|||
Loading…
Reference in New Issue