diff --git a/Gemfile b/Gemfile index 4fa8924..ef450d6 100644 --- a/Gemfile +++ b/Gemfile @@ -169,3 +169,5 @@ gem "literal", "~> 1.7" gem "phlex-rails", "~> 2.2" gem "xsv", "~> 1.3" + +gem "phlex-pdf", "~> 0.1.2" diff --git a/Gemfile.lock b/Gemfile.lock index d9532f2..a14314c 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -372,6 +372,9 @@ GEM pg (1.5.9) phlex (2.2.1) zeitwerk (~> 2.7) + phlex-pdf (0.1.2) + matrix (~> 0.4) + prawn (~> 2.0) phlex-rails (2.2.0) phlex (~> 2.2.1) railties (>= 7.1, < 9) @@ -667,6 +670,7 @@ DEPENDENCIES oauth2 (~> 2.0) parallel (~> 1.26) pg (~> 1.1) + phlex-pdf (~> 0.1.2) phlex-rails (~> 2.2) prawn (~> 2.5) propshaft diff --git a/app/controllers/letters_controller.rb b/app/controllers/letters_controller.rb index 95ed41a..d0b40e1 100644 --- a/app/controllers/letters_controller.rb +++ b/app/controllers/letters_controller.rb @@ -19,7 +19,7 @@ class LettersController < ApplicationController # GET /letters/1 def show authorize @letter - @available_templates = SnailMail::Service.available_templates + @available_templates = SnailMail::PhlexService.available_templates end # GET /letters/new @@ -114,7 +114,7 @@ class LettersController < ApplicationController authorize @letter, :preview_template? template = params["template"] include_qr_code = params["qr"].present? - send_data SnailMail::Service.generate_label(@letter, { template:, include_qr_code: }).render, type: "application/pdf", disposition: "inline" + send_data SnailMail::PhlexService.generate_label(@letter, { template:, include_qr_code: }).render, type: "application/pdf", disposition: "inline" end # POST /letters/1/mark_printed diff --git a/app/lib/snail_mail/base_template.rb b/app/lib/snail_mail/base_template.rb deleted file mode 100644 index 344f7ed..0000000 --- a/app/lib/snail_mail/base_template.rb +++ /dev/null @@ -1,228 +0,0 @@ -module SnailMail - class BaseTemplate - include SnailMail::Helpers - - # Template sizes in points [width, height] - SIZES = { - standard: [6 * 72, 4 * 72], # 4x6 inches (432 x 288 points) - envelope: [9.5 * 72, 4.125 * 72], # #10 envelope (684 x 297 points) - half_letter: [8 * 72, 5 * 72], # half-letter (576 x 360 points) - }.freeze - - # Template metadata - override in subclasses - def self.template_name - name.demodulize.underscore.sub(/_template$/, "") - end - - def self.template_size - :standard # default to 4x6 standard - end - - def self.show_on_single? - false - end - - def self.template_description - "A label template" - end - - # Instance methods - attr_reader :options - - def initialize(options = {}) - @options = options - end - - # Size in points [width, height] - def size - SIZES[self.class.template_size] || SIZES[:standard] - end - - # Main render method, to be implemented by subclasses - def render(pdf, letter) - raise NotImplementedError, "Subclasses must implement the render method" - end - - protected - - # Helper methods for templates - - # Render return address - def render_return_address(pdf, letter, x, y, width, height, options = {}) - default_options = { - font: "arial", - size: 11, - align: :left, - valign: :top, - overflow: :shrink_to_fit, - min_font_size: 6, - } - - options = default_options.merge(options) - font_name = options.delete(:font) - - pdf.font(font_name) do - pdf.text_box( - format_return_address(letter, options[:no_name_line]), - at: [x, y], - width: width, - height: height, - **options, - ) - end - end - - # Render destination address - def render_destination_address(pdf, letter, x, y, width, height, options = {}) - default_options = { - font: "f25", - size: 11, - align: :left, - valign: :center, - overflow: :shrink_to_fit, - min_font_size: 6, - disable_wrap_by_char: true, - } - - options = default_options.merge(options) - font_name = options.delete(:font) - stroke = options.delete(:dbg_stroke) - - pdf.font(font_name) do - pdf.text_box( - letter.address.snailify(letter.return_address.country), - at: [x, y], - width: width, - height: height, - **options, - ) - end - if stroke - pdf.stroke { pdf.rectangle([x, y], width, height) } - end - end - - # Render Intelligent Mail barcode - def render_imb(pdf, letter, x, y, width, options = {}) - - # we want an IMb if: - # - the letter is US-to-US (end-to-end IV) - # - the letter is US-to-non-US (IV until it's out of the US) - # - the letter is non-US-to-US (IV after it enters the US) - # but not if - # - the letter is non-US-to-non-US (that'd be pretty stupid) - - return unless letter.address.us? || letter.return_address.us? - - default_options = { - font: "imb", - size: 24, - align: :center, - overflow: :shrink_to_fit, - } - - options = default_options.merge(options) - font_name = options.delete(:font) - - pdf.font(font_name) do - pdf.text_box( - generate_imb(letter), - at: [x, y], - width: width, - disable_wrap_by_char: true, - **options, - ) - end - end - - # Render QR code - def render_qr_code(pdf, letter, x, y, size = 70) - return unless options[:include_qr_code] - SnailMail::QRCodeGenerator.generate_qr_code(pdf, "https://hack.club/#{letter.public_id}", x, y, size) - pdf.font("f25") do - pdf.text_box("scan this so we know you got it!", at: [x + 3, y + 22], width: 54, size: 6.4) - end - end - - def render_letter_id(pdf, letter, x, y, size, opts = {}) - return if options[:include_qr_code] - pdf.font(opts.delete(:font) || "f25") { pdf.text_box(letter.public_id, at: [x, y], size:, overflow: :shrink_to_fit, valign: :top, **opts) } - end - - private - - # Format destination address - def format_destination_address(letter) - letter.address.snailify(letter.return_address.country) - end - - # Generate IMb barcode - def generate_imb(letter) - # Use the IMb module to generate the barcode - IMb.new(letter).generate - end - - def render_postage(pdf, letter, x = pdf.bounds.right - 138) - if letter.postage_type == "indicia" - IMI.render_indicium(pdf, letter, letter.usps_indicium, x) - FIM.render_fim_d(pdf, x - 62) - elsif letter.postage_type == "stamps" - postage_amount = letter.postage - stamps = USPS::McNuggetEngine.find_stamp_combination(postage_amount) - - requested_stamps = if stamps.size == 1 - stamp = stamps.first - "#{stamp[:count]} #{stamp[:name]}" - elsif stamps.size == 2 - "#{stamps[0][:count]} #{stamps[0][:name]} and #{stamps[1][:count]} #{stamps[1][:name]}" - else - stamps.map.with_index do |stamp, index| - if index == stamps.size - 1 - "and #{stamp[:count]} #{stamp[:name]}" - else - "#{stamp[:count]} #{stamp[:name]}" - end - end.join(", ") - end - - postage_info = <<~EOT - i take #{ActiveSupport::NumberHelper.number_to_currency(postage_amount)} in postage, so #{requested_stamps} - EOT - - pdf.bounding_box([pdf.bounds.right - 55, pdf.bounds.top - 5], width: 50, height: 50) do - pdf.font("f25") do - pdf.text_box( - postage_info, - at: [1, 48], - width: 48, - height: 45, - size: 8, - align: :center, - min_font_size: 4, - overflow: :shrink_to_fit, - ) - end - end - else - pdf.bounding_box([pdf.bounds.right - 55, pdf.bounds.top - 5], width: 52, height: 50) do - pdf.font("f25") do - pdf.text_box("please affix however much postage your post would like", at: [1, 48], width: 50, height: 45, size: 8, align: :center, min_font_size: 4, overflow: :shrink_to_fit) - end - end - end - end - - def format_return_address(letter, no_name_line = false) - return_address = letter.return_address - return "No return address" unless return_address - - <<~EOA - #{letter.return_address_name_line unless no_name_line} - #{[return_address.line_1, return_address.line_2].compact_blank.join("\n")} - #{return_address.city}, #{return_address.state} #{return_address.postal_code} - #{return_address.country if return_address.country != letter.address.country} - EOA - .strip - end - end -end diff --git a/app/lib/snail_mail/components.rb b/app/lib/snail_mail/components.rb new file mode 100644 index 0000000..7226249 --- /dev/null +++ b/app/lib/snail_mail/components.rb @@ -0,0 +1,21 @@ +require_relative "components/base_component" +require_relative "components/page_component" +require_relative "components/template_base" +require_relative "components/half_letter_component" + +# Individual render components +require_relative "components/return_address_component" +require_relative "components/destination_address_component" +require_relative "components/imb_component" +require_relative "components/qr_code_component" +require_relative "components/letter_id_component" +require_relative "components/postage_component" +require_relative "components/speech_bubble_component" + +require_relative "components/registry" + +module SnailMail + module Components + # This module serves as the entry point for all Phlex::PDF components + end +end diff --git a/app/lib/snail_mail/components/base_component.rb b/app/lib/snail_mail/components/base_component.rb new file mode 100644 index 0000000..71f90b3 --- /dev/null +++ b/app/lib/snail_mail/components/base_component.rb @@ -0,0 +1,80 @@ +module SnailMail + module Components + class BaseComponent < Phlex::PDF + include SnailMail::Helpers + + # Template sizes in points [width, height] + SIZES = { + standard: [6 * 72, 4 * 72], # 4x6 inches (432 x 288 points) + envelope: [9.5 * 72, 4.125 * 72], # #10 envelope (684 x 297 points) + half_letter: [8 * 72, 5 * 72], # half-letter (576 x 360 points) + }.freeze + + # Template configuration methods - can be overridden in subclasses + def self.template_name + name.demodulize.underscore.sub(/_component$/, "") + end + + def self.template_size + :standard # default to 4x6 standard + end + + def self.show_on_single? + false + end + + def self.template_description + "A label template" + end + + attr_reader :letter, :options + + def initialize(letter:, **options) + @letter = letter + @options = options + super() + end + + # Size in points [width, height] + def size + SIZES[self.class.template_size] || SIZES[:standard] + end + + # Override in subclasses to define the template + def view_template + raise NotImplementedError, "Subclasses must implement view_template" + end + + protected + + # Helper methods to render components + def render_return_address(x, y, width, height, **options) + render ReturnAddressComponent.new(letter: letter, x: x, y: y, width: width, height: height, **options) + end + + def render_destination_address(x, y, width, height, **options) + render DestinationAddressComponent.new(letter: letter, x: x, y: y, width: width, height: height, **options) + end + + def render_imb(x, y, width, **options) + render ImbComponent.new(letter: letter, x: x, y: y, width: width, **options) + end + + def render_qr_code(x, y, size = 70) + render QrCodeComponent.new(letter: letter, x: x, y: y, size: size, **options) + end + + def render_letter_id(x, y, size, **opts) + render LetterIdComponent.new(letter: letter, x: x, y: y, size: size, **opts.merge(options)) + end + + def render_postage(x = nil) + render PostageComponent.new(letter: letter, x: x, **options) + end + + def render_speech_bubble(**opts) + render SpeechBubbleComponent.new(letter: letter, **opts.merge(options)) + end + end + end +end diff --git a/app/lib/snail_mail/components/destination_address_component.rb b/app/lib/snail_mail/components/destination_address_component.rb new file mode 100644 index 0000000..b203994 --- /dev/null +++ b/app/lib/snail_mail/components/destination_address_component.rb @@ -0,0 +1,43 @@ +module SnailMail + module Components + class DestinationAddressComponent < BaseComponent + def initialize(letter:, x:, y:, width:, height:, **options) + @x = x + @y = y + @width = width + @height = height + super(letter: letter, **options) + end + + def view_template + default_options = { + font: "f25", + size: 11, + align: :left, + valign: :center, + overflow: :shrink_to_fit, + min_font_size: 6, + disable_wrap_by_char: true, + } + + opts = default_options.merge(options) + font_name = opts.delete(:font) + stroke_box = opts.delete(:dbg_stroke) + + font(font_name) do + text_box( + letter.address.snailify(letter.return_address.country), + at: [@x, @y], + width: @width, + height: @height, + **opts, + ) + end + + if stroke_box + stroke { rectangle([@x, @y], @width, @height) } + end + end + end + end +end diff --git a/app/lib/snail_mail/components/half_letter_component.rb b/app/lib/snail_mail/components/half_letter_component.rb new file mode 100644 index 0000000..8065206 --- /dev/null +++ b/app/lib/snail_mail/components/half_letter_component.rb @@ -0,0 +1,22 @@ +module SnailMail + module Components + class HalfLetterComponent < TemplateBase + def self.abstract? + true + end + + def self.template_size + :half_letter + end + + def view_template + render_front + end + + # Override in subclasses to define the front content + def render_front + raise NotImplementedError, "Subclasses must implement render_front" + end + end + end +end diff --git a/app/lib/snail_mail/components/imb_component.rb b/app/lib/snail_mail/components/imb_component.rb new file mode 100644 index 0000000..d40ca16 --- /dev/null +++ b/app/lib/snail_mail/components/imb_component.rb @@ -0,0 +1,43 @@ +module SnailMail + module Components + class ImbComponent < BaseComponent + def initialize(letter:, x:, y:, width:, **options) + @x = x + @y = y + @width = width + super(letter: letter, **options) + end + + def view_template + # Only render IMB for appropriate US mail scenarios + return unless letter.address.us? || letter.return_address.us? + + default_options = { + font: "imb", + size: 24, + align: :center, + overflow: :shrink_to_fit, + } + + opts = default_options.merge(options) + font_name = opts.delete(:font) + + font(font_name) do + text_box( + generate_imb(letter), + at: [@x, @y], + width: @width, + disable_wrap_by_char: true, + **opts, + ) + end + end + + private + + def generate_imb(letter) + IMb.new(letter).generate + end + end + end +end diff --git a/app/lib/snail_mail/components/letter_id_component.rb b/app/lib/snail_mail/components/letter_id_component.rb new file mode 100644 index 0000000..92cb655 --- /dev/null +++ b/app/lib/snail_mail/components/letter_id_component.rb @@ -0,0 +1,27 @@ +module SnailMail + module Components + class LetterIdComponent < BaseComponent + def initialize(letter:, x:, y:, size:, **options) + @x = x + @y = y + @size = size + super(letter: letter, **options) + end + + def view_template + return if options[:include_qr_code] + + font(options[:font] || "f25") do + text_box( + letter.public_id, + at: [@x, @y], + size: @size, + overflow: :shrink_to_fit, + valign: :top, + **options.except(:font) + ) + end + end + end + end +end diff --git a/app/lib/snail_mail/components/page_component.rb b/app/lib/snail_mail/components/page_component.rb new file mode 100644 index 0000000..cbc0ed0 --- /dev/null +++ b/app/lib/snail_mail/components/page_component.rb @@ -0,0 +1,28 @@ +module SnailMail + module Components + class PageComponent < BaseComponent + def before_template + start_new_page unless page_number > 0 + register_fonts + fallback_fonts(["arial", "noto"]) + end + + private + + def register_fonts + font_families.update( + "comic" => { normal: font_path("comic sans.ttf") }, + "arial" => { normal: font_path("arial.otf") }, + "f25" => { normal: font_path("f25.ttf") }, + "imb" => { normal: font_path("imb.ttf") }, + "gohu" => { normal: font_path("gohu.ttf") }, + "noto" => { normal: font_path("noto sans regular.ttf") }, + ) + end + + def font_path(font_name) + File.join(Rails.root, "app", "lib", "snail_mail", "assets", "fonts", font_name) + end + end + end +end diff --git a/app/lib/snail_mail/components/postage_component.rb b/app/lib/snail_mail/components/postage_component.rb new file mode 100644 index 0000000..2b8b06a --- /dev/null +++ b/app/lib/snail_mail/components/postage_component.rb @@ -0,0 +1,82 @@ +module SnailMail + module Components + class PostageComponent < BaseComponent + def initialize(letter:, x: nil, **options) + @x = x + super(letter: letter, **options) + end + + def view_template + x_position = @x || (bounds.right - 138) + + if letter.postage_type == "indicia" + IMI.render_indicium(self, letter, letter.usps_indicium, x_position) + FIM.render_fim_d(self, x_position - 62) + elsif letter.postage_type == "stamps" + render_stamps_postage(x_position) + else + render_generic_postage + end + end + + private + + def render_stamps_postage(x_position) + postage_amount = letter.postage + stamps = USPS::McNuggetEngine.find_stamp_combination(postage_amount) + + requested_stamps = format_stamps_text(stamps) + postage_info = "i take #{ActiveSupport::NumberHelper.number_to_currency(postage_amount)} in postage, so #{requested_stamps}" + + bounding_box([bounds.right - 55, bounds.top - 5], width: 50, height: 50) do + font("f25") do + text_box( + postage_info, + at: [1, 48], + width: 48, + height: 45, + size: 8, + align: :center, + min_font_size: 4, + overflow: :shrink_to_fit, + ) + end + end + end + + def render_generic_postage + bounding_box([bounds.right - 55, bounds.top - 5], width: 52, height: 50) do + font("f25") do + text_box( + "please affix however much postage your post would like", + at: [1, 48], + width: 50, + height: 45, + size: 8, + align: :center, + min_font_size: 4, + overflow: :shrink_to_fit + ) + end + end + end + + def format_stamps_text(stamps) + if stamps.size == 1 + stamp = stamps.first + "#{stamp[:count]} #{stamp[:name]}" + elsif stamps.size == 2 + "#{stamps[0][:count]} #{stamps[0][:name]} and #{stamps[1][:count]} #{stamps[1][:name]}" + else + stamps.map.with_index do |stamp, index| + if index == stamps.size - 1 + "and #{stamp[:count]} #{stamp[:name]}" + else + "#{stamp[:count]} #{stamp[:name]}" + end + end.join(", ") + end + end + end + end +end diff --git a/app/lib/snail_mail/components/qr_code_component.rb b/app/lib/snail_mail/components/qr_code_component.rb new file mode 100644 index 0000000..156802c --- /dev/null +++ b/app/lib/snail_mail/components/qr_code_component.rb @@ -0,0 +1,22 @@ +module SnailMail + module Components + class QrCodeComponent < BaseComponent + def initialize(letter:, x:, y:, size: 70, **options) + @x = x + @y = y + @size = size + super(letter: letter, **options) + end + + def view_template + return unless options[:include_qr_code] + + SnailMail::QRCodeGenerator.generate_qr_code(self, "https://hack.club/#{letter.public_id}", @x, @y, @size) + + font("f25") do + text_box("scan this so we know you got it!", at: [@x + 3, @y + 22], width: 54, size: 6.4) + end + end + end + end +end diff --git a/app/lib/snail_mail/components/registry.rb b/app/lib/snail_mail/components/registry.rb new file mode 100644 index 0000000..d8df3e2 --- /dev/null +++ b/app/lib/snail_mail/components/registry.rb @@ -0,0 +1,104 @@ +module SnailMail + module Components + class Registry + class ComponentNotFoundError < StandardError; end + + # Default component to use when none is specified + DEFAULT_TEMPLATE_NAME = "kestrel's heidi template!" + + class << self + # Get all template classes using descendants, excluding abstract ones + def all + TemplateBase.descendants.reject(&:abstract?) + end + + # Get a component class by name + def get_component_class(name) + component_name = name.to_sym + component_class = all.find { |c| c.template_name.to_sym == component_name } + component_class || raise(ComponentNotFoundError, "Component not found: #{name}") + end + + # Get a component instance for a letter + def component_for(letter, options = {}) + # Check if component name is specified in options + component_name = options[:template]&.to_sym + + component_class = if component_name + # Find component by name + all.find { |c| c.template_name.to_sym == component_name } + else + # Use default + default_component_class + end + + # Create a new instance of the component + component_class ||= default_component_class + component_class.new(letter: letter, **options) + end + + # Get components by size + def components_by_size(size) + size_sym = size.to_sym + all.select { |c| c.template_size == size_sym } + end + + # List all available component names + def available_templates + all.map { |c| c.template_name.to_sym } + end + + def available_single_templates + all.select { |c| c.show_on_single? }.map { |c| c.template_name.to_sym } + end + + # Check if a component exists + def template_exists?(name) + all.any? { |c| c.template_name.to_sym == name.to_sym } + end + + # Get the default template name + def default_template + DEFAULT_TEMPLATE_NAME + end + + # Get the default component class + def default_component_class + all.find { |c| c.template_name == DEFAULT_TEMPLATE_NAME } + end + + # Get template info for all components + def template_info + all.map do |component_class| + { + name: component_class.template_name.to_sym, + size: component_class.template_size, + description: component_class.template_description, + is_default: component_class.template_name == DEFAULT_TEMPLATE_NAME, + } + end + end + + # Get templates for a specific size + def templates_for_size(size) + components = components_by_size(size) + Rails.logger.info "Components for size #{size}: Found #{components.count} components" + + template_names = components.map do |component_class| + begin + name = component_class.template_name.to_s + Rails.logger.info " - Component: #{name}, Size: #{component_class.template_size}" + name + rescue => e + Rails.logger.error "Error getting component name: #{e.message}" + nil + end + end.compact + + Rails.logger.info "Final template names for size #{size}: #{template_names.inspect}" + template_names + end + end + end + end +end diff --git a/app/lib/snail_mail/components/return_address_component.rb b/app/lib/snail_mail/components/return_address_component.rb new file mode 100644 index 0000000..b46c8b1 --- /dev/null +++ b/app/lib/snail_mail/components/return_address_component.rb @@ -0,0 +1,52 @@ +module SnailMail + module Components + class ReturnAddressComponent < BaseComponent + def initialize(letter:, x:, y:, width:, height:, **options) + @x = x + @y = y + @width = width + @height = height + super(letter: letter, **options) + end + + def view_template + default_options = { + font: "arial", + size: 11, + align: :left, + valign: :top, + overflow: :shrink_to_fit, + min_font_size: 6, + } + + opts = default_options.merge(options) + font_name = opts.delete(:font) + + font(font_name) do + text_box( + format_return_address(letter, opts[:no_name_line]), + at: [@x, @y], + width: @width, + height: @height, + **opts, + ) + end + end + + private + + def format_return_address(letter, no_name_line = false) + return_address = letter.return_address + return "No return address" unless return_address + + <<~EOA + #{letter.return_address_name_line unless no_name_line} + #{[return_address.line_1, return_address.line_2].compact_blank.join("\n")} + #{return_address.city}, #{return_address.state} #{return_address.postal_code} + #{return_address.country if return_address.country != letter.address.country} + EOA + .strip + end + end + end +end diff --git a/app/lib/snail_mail/components/speech_bubble_component.rb b/app/lib/snail_mail/components/speech_bubble_component.rb new file mode 100644 index 0000000..2fb0fa0 --- /dev/null +++ b/app/lib/snail_mail/components/speech_bubble_component.rb @@ -0,0 +1,32 @@ +module SnailMail + module Components + class SpeechBubbleComponent < BaseComponent + def initialize(letter:, bubble_position:, bubble_width:, bubble_height:, bubble_radius: 10, tail_x:, tail_y:, tail_width:, line_width: 2.5, **options) + @bubble_position = bubble_position + @bubble_width = bubble_width + @bubble_height = bubble_height + @bubble_radius = bubble_radius + @tail_x = tail_x + @tail_y = tail_y + @tail_width = tail_width + @line_width = line_width + super(letter: letter, **options) + end + + def view_template + # Draw the rounded rectangle bubble + self.line_width = @line_width + stroke do + rounded_rectangle(@bubble_position, @bubble_width, @bubble_height, @bubble_radius) + end + + # Draw the speech tail + image( + image_path("speech-tail.png"), + at: [@tail_x, @tail_y], + width: @tail_width + ) + end + end + end +end diff --git a/app/lib/snail_mail/components/template_base.rb b/app/lib/snail_mail/components/template_base.rb new file mode 100644 index 0000000..a235aee --- /dev/null +++ b/app/lib/snail_mail/components/template_base.rb @@ -0,0 +1,16 @@ +module SnailMail + module Components + class TemplateBase < PageComponent + # Base class for all mail templates + # This allows us to use descendants to automatically discover templates + + def self.abstract? + false + end + + def self.template_description + "A mail template" + end + end + end +end diff --git a/app/lib/snail_mail/components/templates/athena_stickers_template.rb b/app/lib/snail_mail/components/templates/athena_stickers_template.rb new file mode 100644 index 0000000..5d11b0f --- /dev/null +++ b/app/lib/snail_mail/components/templates/athena_stickers_template.rb @@ -0,0 +1,57 @@ +# frozen_string_literal: true + +module SnailMail + module Components + module Templates + class AthenaStickersTemplate < TemplateBase + def self.template_name + "Athena stickers" + end + + def self.show_on_single? + true + end + + def view_template + render_return_address(5, bounds.top - 45, 190, 90, size: 8, font: "f25") + + image( + image_path("athena/logo-stars.png"), + at: [5, bounds.top - 5], + width: 80, + ) + + render_destination_address( + 104, + 196, + 256, + 107, + size: 18, valign: :center, align: :left + ) + + render_speech_bubble( + bubble_position: [72, 202], + bubble_width: 306, + bubble_height: 122, + bubble_radius: 10, + tail_x: 96, + tail_y: 83, + tail_width: 32.2, + line_width: 2.5 + ) + + image( + image_path("athena/nyc-orphy.png"), + at: [13, 98], + height: 97, + ) + + render_imb(230, 25, 190) + render_letter_id(3, 15, 8, rotate: 90) + render_qr_code(7, 160, 50) + render_postage + end + end + end + end +end diff --git a/app/lib/snail_mail/components/templates/dino_waving_template.rb b/app/lib/snail_mail/components/templates/dino_waving_template.rb new file mode 100644 index 0000000..0a82c04 --- /dev/null +++ b/app/lib/snail_mail/components/templates/dino_waving_template.rb @@ -0,0 +1,43 @@ +# frozen_string_literal: true + +module SnailMail + module Components + module Templates + class DinoWavingTemplate < TemplateBase + def self.template_name + "Dino Waving" + end + + def self.show_on_single? + true + end + + def view_template + image( + image_path("dino-waving.png"), + at: [333, 163], + width: 87, + ) + + # Render return address + render_return_address(10, 278, 260, 70, size: 10) + + # Render destination address in speech bubble + render_destination_address( + 88, + 166, + 236, + 71, + size: 16, valign: :bottom, align: :left + ) + + # Render IMb barcode + render_imb(240, 24, 183) + render_qr_code(5, 65, 60) + render_letter_id(10, 19, 10) + render_postage + end + end + end + end +end diff --git a/app/lib/snail_mail/components/templates/good_job_template.rb b/app/lib/snail_mail/components/templates/good_job_template.rb new file mode 100644 index 0000000..bce95ae --- /dev/null +++ b/app/lib/snail_mail/components/templates/good_job_template.rb @@ -0,0 +1,27 @@ +# frozen_string_literal: true + +module SnailMail + module Components + module Templates + class GoodJobTemplate < HalfLetterComponent + ADDRESS_FONT = "arial" + + def self.template_name + "good job" + end + + def self.template_size + :half_letter + end + + def render_front + font "arial" do + text_box "good job", size: 99, at: [0, bounds.top], valign: :center, align: :center + end + + text_box "from: @#{letter.metadata["gj_from"]}\n#{letter.metadata["gj_reason"]}", size: 18, at: [100, 100], align: :left + end + end + end + end +end diff --git a/app/lib/snail_mail/components/templates/hackatime_otp_template.rb b/app/lib/snail_mail/components/templates/hackatime_otp_template.rb new file mode 100644 index 0000000..4b09c0b --- /dev/null +++ b/app/lib/snail_mail/components/templates/hackatime_otp_template.rb @@ -0,0 +1,36 @@ +# frozen_string_literal: true + +module SnailMail + module Components + module Templates + class HackatimeOTPTemplate < HalfLetterComponent + ADDRESS_FONT = "comic" + + def self.template_name + "hackatime OTP" + end + + def self.template_size + :half_letter + end + + def render_front + move_down 100 + text("Your Hackatime sign-in code is:", style: :bold, size: 30, align: :center) + move_down 10 + text(letter.rubber_stamps || "mrrrrp :3", style: :bold, size: 80, align: :center) + text("This code will expire in 1 year.", size: 10, align: :center) + text("(that's #{1.year.from_now.in_time_zone("America/New_York").strftime("%-I:%M %p EST on %B %d")})", size: 9, align: :center, style: :italic) + + stroke_rectangle([2, 55], bounds.width - 5, 52) + + bounding_box([5, 50], width: bounds.width - 15, height: 49) do + self.line_width = 3 + text("CONFIDENTIALITY NOTICE:", style: :bold, size: 8) + text("The information contained in this letter is intended only for the use of the individual named on the address side. It may contain information that is privileged, confidential, and exempt from disclosure under applicable law. If you are not the intended recipient, you are hereby notified that any disclosure, copying, or distribution of this information is prohibited. If you believe you have received this letter in error, please notify us immediately by return postcard and securely destroy the original letter.", size: 8) + end + end + end + end + end +end diff --git a/app/lib/snail_mail/components/templates/hackatime_template.rb b/app/lib/snail_mail/components/templates/hackatime_template.rb new file mode 100644 index 0000000..d85cc7f --- /dev/null +++ b/app/lib/snail_mail/components/templates/hackatime_template.rb @@ -0,0 +1,41 @@ +# frozen_string_literal: true + +module SnailMail + module Components + module Templates + class HackatimeTemplate < TemplateBase + def self.template_name + "Hackatime (new)" + end + + def view_template + image( + image_path("hackatime/its_about_time.png"), + at: [13, 219], + width: 409, + ) + + # Render return address + render_return_address(10, 278, 146, 70, font: "f25") + + # Render destination address in speech bubble + render_destination_address( + 80, + 134, + 290, + 86, + size: 19, valign: :top, align: :left + ) + + # Render IMb barcode + render_imb(216, 25, 207) + + render_letter_id(10, 19, 10) + render_qr_code(5, 55, 50) + + render_postage + end + end + end + end +end diff --git a/app/lib/snail_mail/components/templates/hcb_stickers_template.rb b/app/lib/snail_mail/components/templates/hcb_stickers_template.rb new file mode 100644 index 0000000..0d30754 --- /dev/null +++ b/app/lib/snail_mail/components/templates/hcb_stickers_template.rb @@ -0,0 +1,48 @@ +module SnailMail + module Components + module Templates + class HCBStickersTemplate < TemplateBase + def self.template_name + "HCB Stickers" + end + + def view_template + image( + image_path("lilia-hcb-stickers-bg.png"), + at: [0, 288], + width: 432, + ) + + # Render speech bubble + # image( + # image_path(speech_bubble_image), + # at: [speech_position[:x], speech_position[:y]], + # width: speech_position[:width] + # ) + + # Render return address + render_return_address(10, 278, 146, 70) + + # Render destination address in speech bubble + render_destination_address( + 192, + 149, + 226, + 57, + size: 16, + valign: :bottom, + align: :left + ) + + # Render IMb barcode + render_imb(216, 25, 207) + + render_letter_id(10, 12, 10) + render_qr_code(5, 196, 50) + + render_postage + end + end + end + end +end diff --git a/app/lib/snail_mail/components/templates/hcb_welcome_postcard_template.rb b/app/lib/snail_mail/components/templates/hcb_welcome_postcard_template.rb new file mode 100644 index 0000000..f1669f4 --- /dev/null +++ b/app/lib/snail_mail/components/templates/hcb_welcome_postcard_template.rb @@ -0,0 +1,39 @@ +# frozen_string_literal: true + +module SnailMail + module Components + module Templates + class HCBWelcomePostcardTemplate < HalfLetterComponent + ADDRESS_FONT = "arial" + + def self.template_name + "hcb welcome postcard" + end + + SAMPLE_WELCOME_TEXT = "Hey! + + I'm super excited to work with your org because I think whatever you do is a really important cause and it aligns perfectly with our mission to support things that we believe are good. + + At HCB, we're all about empowering organizations like yours to make a real difference in the world. We believe in the power of community, innovation, and collaboration to create positive change. Your work resonates deeply with our values, and we can't wait to see the amazing things we'll accomplish together. + + We're here to support you every step of the way. Whether you need technical assistance, community resources, or just someone to bounce ideas off of, our team is ready to help. We're not just a service provider – we're your partner in making the world a better place. + + Let's build something incredible together! + + Warm regards, + The HCB Team" + + def render_front + bounding_box([10, bounds.top - 10], width: bounds.width - 20, height: bounds.height - 20) do + image(image_path("hcb/hcb-icon.png"), width: 60) + text_box("Welcome to HCB!", size: 30, at: [70, bounds.top - 18]) + end + + bounding_box([20, bounds.top - 90], width: bounds.width - 40, height: bounds.height - 100) do + text(letter.rubber_stamps || "", size: 15, align: :justify, overflow: :shrink_to_fit) + end + end + end + end + end +end diff --git a/app/lib/snail_mail/components/templates/hcpcxc_template.rb b/app/lib/snail_mail/components/templates/hcpcxc_template.rb new file mode 100644 index 0000000..a3fecf7 --- /dev/null +++ b/app/lib/snail_mail/components/templates/hcpcxc_template.rb @@ -0,0 +1,58 @@ +# frozen_string_literal: true + +module SnailMail + module Components + module Templates + class HcpcxcTemplate < TemplateBase + def self.template_name + "hcpcxc" + end + + def view_template + image( + image_path("dino-waving.png"), + at: [ 333, 163 ], + width: 87 + ) + + image( + image_path("hcpcxc_ra.png"), + at: [ 5, 288-5 ], + width: 175 + ) + + render_destination_address( + 88, + 166, + 236, + 71, + size: 16, valign: :bottom, align: :left + ) + + # Render IMb barcode + render_imb(240, 24, 183) + + render_qr_code(5, 65, 60) + + render_letter_id(10, 19, 10) + + if letter.rubber_stamps.present? + font("arial") do + text_box( + letter.rubber_stamps, + at: [ 294, 220 ], + width: 255, + height: 21, + overflow: :shrink_to_fit, + disable_wrap_by_char: true, + min_size: 1 + ) + end + end + + render_postage + end + end + end + end +end diff --git a/app/lib/snail_mail/components/templates/heidi_readme_template.rb b/app/lib/snail_mail/components/templates/heidi_readme_template.rb new file mode 100644 index 0000000..77a1ed0 --- /dev/null +++ b/app/lib/snail_mail/components/templates/heidi_readme_template.rb @@ -0,0 +1,51 @@ +# frozen_string_literal: true + +module SnailMail + module Components + module Templates + class HeidiReadmeTemplate < TemplateBase + def self.template_name + "Heidi Can't Readme" + end + + def self.show_on_single? + true + end + + def view_template + render_return_address(10, 278, 190, 90, size: 12, font: "f25") + + render_destination_address( + 133, + 176, + 256, + 107, + size: 18, valign: :center, align: :left + ) + + render_speech_bubble( + bubble_position: [90 + 20, 189 - 5], + bubble_width: 306, + bubble_height: 122, + bubble_radius: 10, + tail_x: 114 + 20, + tail_y: 70 - 5, + tail_width: 32.2, + line_width: 2.5 + ) + + image( + image_path("msw-heidi-cant-readme.png"), + at: [6 + 20, 75], + width: 111, + ) + + render_imb(230, 25, 190) + render_letter_id(3, 15, 8, rotate: 90) + render_qr_code(7, 72 + 7 + 50 + 10 + 12, 60) + render_postage + end + end + end + end +end diff --git a/app/lib/snail_mail/components/templates/joyous_cat_template.rb b/app/lib/snail_mail/components/templates/joyous_cat_template.rb new file mode 100644 index 0000000..1c4e7d4 --- /dev/null +++ b/app/lib/snail_mail/components/templates/joyous_cat_template.rb @@ -0,0 +1,50 @@ +module SnailMail + module Components + module Templates + class JoyousCatTemplate < TemplateBase + def self.template_name + "Joyous Cat :3" + end + + def self.show_on_single? + true + end + + def view_template + render_speech_bubble( + bubble_position: [111, 189], + bubble_width: 306, + bubble_height: 122, + bubble_radius: 10, + tail_x: 208, + tail_y: 74, + tail_width: 106.4, + line_width: 3 + ) + + image( + image_path("acon-joyous-cat.png"), + at: [208, 74], + width: 106.4, + ) + + render_return_address(10, 270, 130, 70) + + render_destination_address( + 134, + 173, + 266, + 67, + size: 16, + valign: :center, + align: :left + ) + + render_imb(131, 100, 266) + render_qr_code(7, 72 + 7, 72) + render_postage + end + end + end + end +end diff --git a/app/lib/snail_mail/components/templates/kestrel_heidi_template.rb b/app/lib/snail_mail/components/templates/kestrel_heidi_template.rb new file mode 100644 index 0000000..e4a8f98 --- /dev/null +++ b/app/lib/snail_mail/components/templates/kestrel_heidi_template.rb @@ -0,0 +1,39 @@ +module SnailMail + module Components + module Templates + class KestrelHeidiTemplate < TemplateBase + def self.template_name + "kestrel's heidi template!" + end + + def self.show_on_single? + true + end + + def view_template + image( + image_path("kestrel-mail-heidi.png"), + at: [107, 216], + width: 305, + ) + + render_return_address(10, 278, 190, 90, size: 14) + + render_destination_address( + 126, + 201, + 266, + 67, + size: 16, + valign: :center, + align: :left + ) + + render_imb(124, 120, 200) + render_qr_code(7, 72 + 7, 72) + render_postage + end + end + end + end +end diff --git a/app/lib/snail_mail/components/templates/mail_orpheus_template.rb b/app/lib/snail_mail/components/templates/mail_orpheus_template.rb new file mode 100644 index 0000000..8445b67 --- /dev/null +++ b/app/lib/snail_mail/components/templates/mail_orpheus_template.rb @@ -0,0 +1,49 @@ +module SnailMail + module Components + module Templates + class MailOrpheusTemplate < TemplateBase + def self.template_name + "Mail Orpheus!" + end + + def self.show_on_single? + true + end + + def view_template + image( + image_path("eleeza-mail-orpheus.png"), + at: [320, 113], + width: 106.4, + ) + + # Render speech bubble + # image( + # image_path(speech_bubble_image), + # at: [speech_position[:x], speech_position[:y]], + # width: speech_position[:width] + # ) + + # Render return address + render_return_address(10, 270, 130, 70) + + # Render destination address in speech bubble + render_destination_address( + 79.5, + 202, + 237, + 100, + size: 16, valign: :bottom, align: :left + ) + + # Render IMb barcode + render_imb(78, 102, 237) + + # Render QR code for tracking + render_qr_code(7, 67, 60) + render_postage + end + end + end + end +end diff --git a/app/lib/snail_mail/components/templates/summer_of_making_free_stickers_template.rb b/app/lib/snail_mail/components/templates/summer_of_making_free_stickers_template.rb new file mode 100644 index 0000000..fa4711f --- /dev/null +++ b/app/lib/snail_mail/components/templates/summer_of_making_free_stickers_template.rb @@ -0,0 +1,40 @@ +# frozen_string_literal: true + +module SnailMail + module Components + module Templates + class SummerOfMakingFreeStickersTemplate < TemplateBase + def self.template_name + "SoM Free Stickers" + end + + def self.show_on_single? + true + end + + def view_template + render_return_address(5, bounds.top - 5, 190, 90, size: 8, font: "f25") + + render_destination_address( + 120, + 115, + 270, + 81, + size: 18, valign: :center, align: :left + ) + + image( + image_path("som/banner.png"), + at: [-5, 288 - 56], + width: 445, + ) + + render_imb(245, 20, 170) + render_letter_id(3, 15, 8, rotate: 90) + render_qr_code(2, 52, 50) + render_postage + end + end + end + end +end diff --git a/app/lib/snail_mail/components/templates/tarot_template.rb b/app/lib/snail_mail/components/templates/tarot_template.rb new file mode 100644 index 0000000..bdf1a4b --- /dev/null +++ b/app/lib/snail_mail/components/templates/tarot_template.rb @@ -0,0 +1,66 @@ +# frozen_string_literal: true + +module SnailMail + module Components + module Templates + class TarotTemplate < TemplateBase + def self.template_name + "Tarot" + end + + def view_template + render_return_address(10, 278, 190, 90, size: 12, font: 'comic') + + if letter.rubber_stamps.present? + font("gohu") do + text_box( + "\"#{letter.rubber_stamps}\"", + at: [ 137, 183 ], + width: 255, + height: 21, + overflow: :shrink_to_fit, + disable_wrap_by_char: true, + min_size: 1 + ) + end + end + + render_destination_address( + 137, + 160, + 255, + 90, + size: 16, valign: :center, align: :left + ) + + stroke do + self.line_width = 1 + line([ 137 - 25, 167 ], [ 392 + 25, 167 ]) + end + + render_speech_bubble( + bubble_position: [111, 189], + bubble_width: 306, + bubble_height: 122, + bubble_radius: 10, + tail_x: 118, + tail_y: 70, + tail_width: 32.2, + line_width: 2.5 + ) + + image( + image_path("tarot/msw-joker.png"), + at: [ 6, 104 ], + width: 111 + ) + + render_imb(216, 25, 207) + render_letter_id(3, 15, 8, rotate: 90) + render_qr_code(7, 72 + 7, 72) + render_postage + end + end + end + end +end diff --git a/app/lib/snail_mail/label_generator.rb b/app/lib/snail_mail/label_generator.rb deleted file mode 100644 index 2806b3a..0000000 --- a/app/lib/snail_mail/label_generator.rb +++ /dev/null @@ -1,105 +0,0 @@ -require "prawn" -require "securerandom" -require_relative "templates" - -module SnailMail - class LabelGenerator - class Error < StandardError; end - - attr_reader :options - - def initialize(options = {}) - @options = { - margin: 0, - }.merge(options) - end - - # Generate labels for a collection of letters - # template_names: array of template names to cycle through - # Returns the PDF object directly (doesn't write to disk unless output_path provided) - def generate(letters, output_path = nil, template_names) - raise Error, "No letters provided" if letters.empty? - raise Error, "No template names provided" if template_names.empty? - - begin - # Get template classes upfront to avoid repeated lookups - template_classes = template_names.map do |name| - Templates.get_template_class(name) - end - - # Ensure all template sizes are the same - sizes = template_classes.map(&:template_size).uniq - if sizes.length > 1 - raise Error, "Mixed template sizes in batch (#{sizes.join(", ")}). All templates must have the same size." - end - - # Create template lookup for faster access - template_lookup = {} - template_names.each_with_index do |name, i| - template_lookup[name] = template_classes[i] - end - - # All templates have the same size, create one PDF - pdf = create_document(sizes.first) - - letters.each_with_index do |letter, index| - template_name = template_names[index % template_names.length] - render_letter(pdf, letter, template: template_name, template_class: template_lookup[template_name]) - pdf.start_new_page unless index == letters.length - 1 - end - - # Write to disk only if output_path is provided - pdf.render_file(output_path) if output_path - - # Return the PDF object - pdf - rescue => e - Rails.logger.error("Failed to generate labels: #{e.message}") - raise - end - end - - private - - def create_document(page_size_name) - page_size = BaseTemplate::SIZES[page_size_name] || BaseTemplate::SIZES[:standard] - - pdf = Prawn::Document.new( - page_size: page_size, - margin: @options[:margin], - ) - - register_fonts(pdf) - pdf.fallback_fonts(["arial", "noto"]) - pdf - end - - def register_fonts(pdf) - pdf.font_families.update( - "comic" => { normal: font_path("comic sans.ttf") }, - "arial" => { normal: font_path("arial.otf") }, - "f25" => { normal: font_path("f25.ttf") }, - "imb" => { normal: font_path("imb.ttf") }, - "gohu" => { normal: font_path("gohu.ttf") }, - "noto" => { normal: font_path("noto sans regular.ttf") }, - ) - end - - def font_path(font_name) - File.join(Rails.root, "app", "lib", "snail_mail", "assets", "fonts", font_name) - end - - def render_letter(pdf, letter, letter_options = {}) - template_options = @options.merge(letter_options) - - # Use pre-fetched template class if provided, otherwise look it up - if template_class = letter_options[:template_class] - template = template_class.new(template_options) - else - template = Templates.template_for(letter, template_options) - end - - template.render(pdf, letter) - end - end -end diff --git a/app/lib/snail_mail/phlex_service.rb b/app/lib/snail_mail/phlex_service.rb new file mode 100644 index 0000000..f9d2134 --- /dev/null +++ b/app/lib/snail_mail/phlex_service.rb @@ -0,0 +1,165 @@ +require_relative "components" + +module SnailMail + class PhlexService + class Error < StandardError; end + + # Generate a label for a single letter using Phlex::PDF + def self.generate_label(letter, options = {}) + validate_letter(letter) + template_name = options.delete(:template) || default_template + + # Get page size from component class + component_class = Components::Registry.get_component_class(template_name) + page_size = Components::BaseComponent::SIZES[component_class.template_size] || Components::BaseComponent::SIZES[:standard] + + # Create component + component = Components::Registry.component_for(letter, options.merge(template: template_name)) + + # Use a simple wrapper approach - create the PDF and delegate methods to the component + class << component + attr_accessor :document + + # Override method_missing to delegate to the document when needed + def method_missing(method_name, *args, **kwargs, &block) + if document && document.respond_to?(method_name) + document.send(method_name, *args, **kwargs, &block) + else + super + end + end + + def respond_to_missing?(method_name, include_private = false) + (document && document.respond_to?(method_name, include_private)) || super + end + end + + # Create Prawn document and set it on the component + component.document = Prawn::Document.new( + page_size: page_size, + margin: options[:margin] || 0, + ) + + # Call the component's template methods + component.before_template if component.respond_to?(:before_template) + component.view_template + component.after_template if component.respond_to?(:after_template) + + component.document + end + + # Generate labels for a batch of letters using Phlex::PDF + def self.generate_batch_labels(letters, options = {}) + validate_batch(letters) + + template_cycle = options[:template_cycle] + validate_template_cycle(template_cycle) if template_cycle + + # If no template cycle is provided, use the default template + template_cycle ||= [default_template] + + # Get component classes once, avoid repeated lookups + component_classes = template_cycle.map do |name| + Components::Registry.get_component_class(name) + end + + # Ensure all templates in the cycle are of the same size + template_sizes = component_classes.map(&:template_size).uniq + if template_sizes.length > 1 + raise Error, "All templates in cycle must have the same size. Found: #{template_sizes.join(", ")}" + end + + # Create combined document with proper page size + page_size = Components::BaseComponent::SIZES[template_sizes.first] || Components::BaseComponent::SIZES[:standard] + combined_pdf = Prawn::Document.new( + page_size: page_size, + margin: options[:margin] || 0, + ) + + letters.each_with_index do |letter, index| + template_name = template_cycle[index % template_cycle.length] + component = Components::Registry.component_for(letter, options.merge(template: template_name)) + + # Use the same method delegation approach as single label generation + class << component + attr_accessor :document + + def method_missing(method_name, *args, **kwargs, &block) + if document && document.respond_to?(method_name) + document.send(method_name, *args, **kwargs, &block) + else + super + end + end + + def respond_to_missing?(method_name, include_private = false) + (document && document.respond_to?(method_name, include_private)) || super + end + end + + # Start new page for subsequent letters + if index > 0 + combined_pdf.start_new_page + end + + # Set the document context and render + component.document = combined_pdf + component.before_template if component.respond_to?(:before_template) + component.view_template + component.after_template if component.respond_to?(:after_template) + end + + combined_pdf + end + + # List available templates + def self.available_templates + Components::Registry.available_templates.uniq + end + + # Get a list of all templates with their metadata + def self.template_info + Components::Registry.template_info + end + + # Get templates for a specific size + def self.templates_for_size(size) + Components::Registry.templates_for_size(size) + end + + # Get the default template + def self.default_template + Components::Registry.default_template + end + + # Check if templates exist + def self.templates_exist?(template_names) + Array(template_names).all? do |name| + Components::Registry.template_exists?(name) + end + end + + private + + def self.validate_letter(letter) + raise Error, "Letter cannot be nil" unless letter + raise Error, "Letter must have an address" unless letter.respond_to?(:address) && letter.address + end + + def self.validate_batch(letters) + raise Error, "Letters cannot be nil" unless letters + raise Error, "Letters must be a collection" unless letters.respond_to?(:each) + raise Error, "Letters collection cannot be empty" if letters.empty? + end + + def self.validate_template_cycle(template_cycle) + raise Error, "Template cycle must be an array" unless template_cycle.is_a?(Array) + raise Error, "Template cycle cannot be empty" if template_cycle.empty? + + invalid_templates = template_cycle.reject { |name| templates_exist?([name]) } + if invalid_templates.any? + raise Error, "Invalid templates in cycle: #{invalid_templates.join(", ")}" + end + end + end +end diff --git a/app/lib/snail_mail/preview.rb b/app/lib/snail_mail/preview.rb index 57f4ec5..8103ca8 100644 --- a/app/lib/snail_mail/preview.rb +++ b/app/lib/snail_mail/preview.rb @@ -55,8 +55,8 @@ module SnailMail usps_mailer_id = OpenStruct.new(mid: "111111") - Templates.available_templates.each do |name| - template = Templates.get_template_class(name) + SnailMail::Components::Registry.available_templates.each do |name| + template = SnailMail::Components::Registry.get_component_class(name) sender, recipient = names.sample(2) mock_letter = OpenStruct.new( @@ -81,7 +81,7 @@ module SnailMail ) Rails.logger.info("generating preview for #{name}...") - pdf = SnailMail::Service.generate_label(mock_letter, template: name) + pdf = SnailMail::PhlexService.generate_label(mock_letter, template: name) png_path = OUTPUT_DIR.join("#{template.name.split("::").last.underscore}.png") diff --git a/app/lib/snail_mail/service.rb b/app/lib/snail_mail/service.rb deleted file mode 100644 index da30341..0000000 --- a/app/lib/snail_mail/service.rb +++ /dev/null @@ -1,121 +0,0 @@ -require_relative "label_generator" -require_relative "templates" - -module SnailMail - class Service - class Error < StandardError; end - - # Generate a label for a single letter and attach it to the letter - def self.generate_label(letter, options = {}) - validate_letter(letter) - template_name = options.delete(:template) || default_template - - generator = LabelGenerator.new(options) - - # Generate PDF without writing to disk - pdf = generator.generate([letter], nil, [template_name]) - - pdf - end - - # Generate labels for a batch of letters and attach to batch - def self.generate_batch_labels(letters, options = {}) - validate_batch(letters) - - template_cycle = options[:template_cycle] - validate_template_cycle(template_cycle) if template_cycle - - # If no template cycle is provided, use the default template - template_cycle ||= [default_template] - - # Get template classes once, avoid repeated lookups - template_classes = template_cycle.map do |name| - Templates.get_template_class(name) - end - - # Ensure all templates in the cycle are of the same size - template_sizes = template_classes.map(&:template_size).uniq - if template_sizes.length > 1 - raise Error, "All templates in cycle must have the same size. Found: #{template_sizes.join(", ")}" - end - - # Generate labels with template cycling without writing to disk - generator = LabelGenerator.new(options) - pdf = generator.generate(letters, nil, template_cycle) - - pdf - end - - # List available templates - def self.available_templates - Templates.available_templates.uniq - end - - # Get a list of all templates with their metadata - def self.template_info - Templates.all.map do |template_class| - { - name: template_class.template_name.to_sym, - size: template_class.template_size, - description: template_class.template_description, - is_default: template_class == Templates::DEFAULT_TEMPLATE, - } - end - end - - # Get templates for a specific size - def self.templates_for_size(size) - templates = Templates.templates_by_size(size) - Rails.logger.info "Templates for size #{size}: Found #{templates.count} templates" - - template_names = templates.map do |template_class| - begin - name = template_class.template_name.to_s - Rails.logger.info " - Template: #{name}, Size: #{template_class.template_size}" - name - rescue => e - Rails.logger.error "Error getting template name: #{e.message}" - nil - end - end.compact - - Rails.logger.info "Final template names for size #{size}: #{template_names.inspect}" - template_names - end - - # Get the default template - def self.default_template - Templates::DEFAULT_TEMPLATE.template_name - end - - # Check if templates exist - def self.templates_exist?(template_names) - Array(template_names).all? do |name| - Templates.template_exists?(name) - end - end - - private - - def self.validate_letter(letter) - raise Error, "Letter cannot be nil" unless letter - raise Error, "Letter must have an address" unless letter.respond_to?(:address) && letter.address - end - - def self.validate_batch(letters) - raise Error, "Letters cannot be nil" unless letters - raise Error, "Letters must be a collection" unless letters.respond_to?(:each) - raise Error, "Letters collection cannot be empty" if letters.empty? - end - - def self.validate_template_cycle(template_cycle) - raise Error, "Template cycle must be an array" unless template_cycle.is_a?(Array) - raise Error, "Template cycle cannot be empty" if template_cycle.empty? - - invalid_templates = template_cycle.reject { |name| templates_exist?([name]) } - if invalid_templates.any? - raise Error, "Invalid templates in cycle: #{invalid_templates.join(", ")}" - end - end - end -end diff --git a/app/lib/snail_mail/templates.rb b/app/lib/snail_mail/templates.rb deleted file mode 100644 index b967b5d..0000000 --- a/app/lib/snail_mail/templates.rb +++ /dev/null @@ -1,86 +0,0 @@ -module SnailMail - module Templates - class TemplateNotFoundError < StandardError; end - - # All available templates hardcoded in a single array - TEMPLATES = [ - JoyousCatTemplate, - MailOrpheusTemplate, - HCBStickersTemplate, - KestrelHeidiTemplate, - # HackatimeStickersTemplate, - TarotTemplate, - DinoWavingTemplate, - HcpcxcTemplate, - HackatimeTemplate, - HeidiReadmeTemplate, - GoodJobTemplate, - HackatimeOTPTemplate, - AthenaStickersTemplate, - HCBWelcomePostcardTemplate, - SummerOfMakingFreeStickersTemplate - ].freeze - - # Default template to use when none is specified - DEFAULT_TEMPLATE = KestrelHeidiTemplate - - class << self - # Get all template classes - def all - TEMPLATES - end - - # Get a template class by name - def get_template_class(name) - template_name = name.to_sym - template_class = TEMPLATES.find { |t| t.template_name.to_sym == template_name } - template_class || raise(TemplateNotFoundError, "Template not found: #{name}") - end - - # Get a template instance for a letter - # Options: - # template: Specifies the template to use, overriding any template in letter.rubber_stamps - # template_class: Pre-fetched template class to use (fastest option) - def template_for(letter, options = {}) - # First check if template_class is provided (fastest path) - if template_class = options[:template_class] - return template_class.new(options) - end - - # Next check if template name is specified in options - template_name = options[:template]&.to_sym - - template_class = if template_name - # Find template by name - TEMPLATES.find { |t| t.template_name.to_sym == template_name } - else - # Use default - DEFAULT_TEMPLATE - end - - # Create a new instance of the template - template_class ? template_class.new(options) : DEFAULT_TEMPLATE.new(options) - end - - # Get templates by size - def templates_by_size(size) - size_sym = size.to_sym - TEMPLATES.select { |t| t.template_size == size_sym } - end - - # List all available template names - def available_templates - TEMPLATES.map { |t| t.template_name.to_sym } - end - - def available_single_templates - TEMPLATES.select { |t| t.show_on_single? }.map { |t| t.template_name.to_sym } - end - - # Check if a template exists - def template_exists?(name) - TEMPLATES.any? { |t| t.template_name.to_sym == name.to_sym } - end - end - end -end diff --git a/app/lib/snail_mail/templates/athena_stickers_template.rb b/app/lib/snail_mail/templates/athena_stickers_template.rb deleted file mode 100644 index c1a85b2..0000000 --- a/app/lib/snail_mail/templates/athena_stickers_template.rb +++ /dev/null @@ -1,53 +0,0 @@ -module SnailMail - module Templates - class AthenaStickersTemplate < BaseTemplate - def self.template_name - "Athena stickers" - end - - def self.show_on_single? - true - end - - def render(pdf, letter) - render_return_address(pdf, letter, 5, pdf.bounds.top - 45, 190, 90, size: 8, font: "f25") - pdf.image( - image_path("athena/logo-stars.png"), - at: [5, pdf.bounds.top - 5], - width: 80, - ) - render_destination_address( - pdf, - letter, - 104, - 196, - 256, - 107, - { size: 18, valign: :center, align: :left } - ) - - pdf.stroke do - pdf.line_width = 2.5 - pdf.rounded_rectangle([72, 202], 306, 122, 10) - end - - pdf.image( - image_path("athena/nyc-orphy.png"), - at: [13, 98], - height: 97, - ) - - pdf.image( - image_path("speech-tail.png"), - at: [96, 83], - width: 32.2, - ) - - render_imb(pdf, letter, 230, 25, 190) - render_letter_id(pdf, letter, 3, 15, 8, rotate: 90) - render_qr_code(pdf, letter, 7, 160, 50) - render_postage(pdf, letter) - end - end - end -end diff --git a/app/lib/snail_mail/templates/character_template.rb b/app/lib/snail_mail/templates/character_template.rb deleted file mode 100644 index 686e9cf..0000000 --- a/app/lib/snail_mail/templates/character_template.rb +++ /dev/null @@ -1,64 +0,0 @@ -require_relative "../base_template" - -module SnailMail - module Templates - class CharacterTemplate < BaseTemplate - # Abstract base class for character templates - - def self.template_name - "character" # This template isn't meant to be used directly - end - - def self.template_description - "Base class for character templates (not for direct use)" - end - - attr_reader :character_image, :speech_bubble_image, :character_position, :speech_position - - def initialize(options = {}) - super - @character_image = options[:character_image] - @speech_bubble_image = options[:speech_bubble_image] || "speech_bubble.png" - @character_position = options[:character_position] || { x: 10, y: 100, width: 120 } - @speech_position = options[:speech_position] || { x: 100, y: 250, width: 300, height: 100 } - end - - def render(pdf, letter) - # Render character - pdf.image( - image_path(character_image), - at: [ character_position[:x], character_position[:y] ], - width: character_position[:width] - ) - - # Render speech bubble - pdf.image( - image_path(speech_bubble_image), - at: [ speech_position[:x], speech_position[:y] ], - width: speech_position[:width] - ) - - # Render return address - render_return_address(pdf, letter, 10, 270, 130, 70) - - # Render destination address in speech bubble - render_destination_address( - pdf, - letter, - speech_position[:x] + 20, - speech_position[:y] - 10, - speech_position[:width] - 40, - speech_position[:height] - 20, - { size: 12, valign: :center } - ) - - # Render IMb barcode - render_imb(pdf, letter, 100, 90, 280, 30) - - # Render QR code for tracking - render_qr_code(pdf, letter, 5, 65, 60) - render_postage(pdf, letter) - end - end - end -end diff --git a/app/lib/snail_mail/templates/corporate_envelope_template.rb b/app/lib/snail_mail/templates/corporate_envelope_template.rb deleted file mode 100644 index 8563557..0000000 --- a/app/lib/snail_mail/templates/corporate_envelope_template.rb +++ /dev/null @@ -1,59 +0,0 @@ -require_relative "../base_template" - -module SnailMail - module Templates - class CorporateEnvelopeTemplate < BaseTemplate - def self.template_name - "corporate_envelope" - end - - def self.template_size - :envelope # Use the envelope size from BaseTemplate::SIZES - end - - def self.template_description - "Professional business envelope template" - end - - def render(pdf, letter) - # Draw a subtle border - pdf.stroke do - pdf.rectangle [ 15, 4 * 72 - 15 ], 9.5 * 72 - 30, 4.125 * 72 - 30 - end - - # Render return address in top left - pdf.font("Helvetica") do - pdf.text_box( - format_return_address(letter), - at: [ 30, 4 * 72 - 30 ], - width: 250, - height: 60, - overflow: :shrink_to_fit, - min_font_size: 8, - style: :bold - ) - end - - # Render destination address - pdf.font("Helvetica") do - pdf.text_box( - format_destination_address(letter), - at: [ 4.5 * 72 - 200, 2.5 * 72 + 50 ], - width: 400, - height: 120, - overflow: :shrink_to_fit, - min_font_size: 10, - leading: 2 - ) - end - - # Render IMb barcode at bottom - render_imb(pdf, letter, 72, 30, 7 * 72, 30) - - # Render QR code in bottom left with smaller size - render_qr_code(pdf, letter, 25, 70, 50) - render_postage(pdf, letter) - end - end - end -end diff --git a/app/lib/snail_mail/templates/dino_waving_template.rb b/app/lib/snail_mail/templates/dino_waving_template.rb deleted file mode 100644 index ddfa10f..0000000 --- a/app/lib/snail_mail/templates/dino_waving_template.rb +++ /dev/null @@ -1,41 +0,0 @@ -module SnailMail - module Templates - class DinoWavingTemplate < BaseTemplate - def self.template_name - "Dino Waving" - end - - def self.show_on_single? - true - end - - def render(pdf, letter) - pdf.image( - image_path("dino-waving.png"), - at: [333, 163], - width: 87, - ) - - # Render return address - render_return_address(pdf, letter, 10, 278, 260, 70, size: 10) - - # Render destination address in speech bubble - render_destination_address( - pdf, - letter, - 88, - 166, - 236, - 71, - { size: 16, valign: :bottom, align: :left } - ) - - # Render IMb barcode - render_imb(pdf, letter, 240, 24, 183) - render_qr_code(pdf, letter, 5, 65, 60) - render_letter_id(pdf, letter, 10, 19, 10) - render_postage(pdf, letter) - end - end - end -end diff --git a/app/lib/snail_mail/templates/envelope_template.rb b/app/lib/snail_mail/templates/envelope_template.rb deleted file mode 100644 index ec63826..0000000 --- a/app/lib/snail_mail/templates/envelope_template.rb +++ /dev/null @@ -1,46 +0,0 @@ -require_relative "../base_template" - -module SnailMail - module Templates - class EnvelopeTemplate < BaseTemplate - def self.template_name - "envelope" - end - - def self.template_size - :envelope # Use the envelope size from BaseTemplate::SIZES - end - - def self.template_description - "Standard #10 business envelope template" - end - - def render(pdf, letter) - # Render return address in top left - render_return_address(pdf, letter, 15, 4 * 72 - 30, 250, 60) - - # Render destination address centered - render_destination_address( - pdf, - letter, - 4.5 * 72 - 150, # Centered horizontally - 2.5 * 72, # Centered vertically - 300, # Width - 120, # Height - { - size: 12, - valign: :center, - align: :center - } - ) - - # Render IMb barcode at bottom - render_imb(pdf, letter, 72, 30, 7 * 72, 30) - - # Render QR code in bottom left - render_qr_code(pdf, letter, 15, 70, 60) - render_postage(pdf, letter) - end - end - end -end diff --git a/app/lib/snail_mail/templates/good_job_template.rb b/app/lib/snail_mail/templates/good_job_template.rb deleted file mode 100644 index 8258d72..0000000 --- a/app/lib/snail_mail/templates/good_job_template.rb +++ /dev/null @@ -1,23 +0,0 @@ -# frozen_string_literal: true -module SnailMail - module Templates - class GoodJobTemplate < HalfLetterTemplate - ADDRESS_FONT = "arial" - def self.template_name - "good job" - end - - def self.template_size - :half_letter - end - - def render_front(pdf, letter) - pdf.font "arial" do - pdf.text_box "good job", size: 99, at: [0, pdf.bounds.top], valign: :center, align: :center - end - - pdf.text_box "from: @#{letter.metadata["gj_from"]}\n#{letter.metadata["gj_reason"]}", size: 18, at: [100, 100], align: :left - end - end - end -end diff --git a/app/lib/snail_mail/templates/hackatime_otp_template.rb b/app/lib/snail_mail/templates/hackatime_otp_template.rb deleted file mode 100644 index fc54f40..0000000 --- a/app/lib/snail_mail/templates/hackatime_otp_template.rb +++ /dev/null @@ -1,34 +0,0 @@ -# frozen_string_literal: true -module SnailMail - module Templates - class HackatimeOTPTemplate < HalfLetterTemplate - ADDRESS_FONT = "comic" - - def self.template_name - "hackatime OTP" - end - - def self.template_size - :half_letter - end - - def render_front(pdf, letter) - pdf.move_down 100 - pdf.text("Your Hackatime sign-in code is:", style: :bold, size: 30, align: :center) - pdf.move_down 10 - pdf.text(letter.rubber_stamps || "mrrrrp :3", style: :bold, size: 80, align: :center) - # pdf.move_up 30 - pdf.text("This code will expire in 1 year.", size: 10, align: :center) - pdf.text("(that's #{1.year.from_now.in_time_zone("America/New_York").strftime("%-I:%M %p EST on %B %d")})", size: 9, align: :center, style: :italic) - - pdf.stroke_rectangle([2, 55], pdf.bounds.width - 5, 52) - - pdf.bounding_box([5, 50], width: pdf.bounds.width - 15, height: 49) do - pdf.line_width 3 - pdf.text("CONFIDENTIALITY NOTICE:", style: :bold, size: 8) - pdf.text("The information contained in this letter is intended only for the use of the individual named on the address side. It may contain information that is privileged, confidential, and exempt from disclosure under applicable law. If you are not the intended recipient, you are hereby notified that any disclosure, copying, or distribution of this information is prohibited. If you believe you have received this letter in error, please notify us immediately by return postcard and securely destroy the original letter.", size: 8) - end - end - end - end -end diff --git a/app/lib/snail_mail/templates/hackatime_stickers_template.rb b/app/lib/snail_mail/templates/hackatime_stickers_template.rb deleted file mode 100644 index 2c1c5ba..0000000 --- a/app/lib/snail_mail/templates/hackatime_stickers_template.rb +++ /dev/null @@ -1,26 +0,0 @@ -module SnailMail - module Templates - class HackatimeStickersTemplate < KestrelHeidiTemplate - MSG = <<~EOT - you're getting this because you coded for - ≥15 minutes during Scrapyard and tracked - it w/ Hackatime V2! ^_^ - - zach sent you an email about it... - EOT - - def self.template_name - "Hackatime Stickers" - end - - def render(pdf, letter) - super - render_letter_id(pdf, letter, 360, 13, 12) - pdf.image(image_path("hackatime/badge.png"), at: [ 10, 92 ], width: 117) - pdf.font("gohu") do - pdf.text_box(MSG, at: [ 162, 278 ], size: 8) - end - end - end - end -end diff --git a/app/lib/snail_mail/templates/hackatime_template.rb b/app/lib/snail_mail/templates/hackatime_template.rb deleted file mode 100644 index 772a5d8..0000000 --- a/app/lib/snail_mail/templates/hackatime_template.rb +++ /dev/null @@ -1,46 +0,0 @@ -module SnailMail - module Templates - class HackatimeTemplate < BaseTemplate - def self.template_name - "Hackatime (new)" - end - - def render(pdf, letter) - pdf.image( - image_path("hackatime/its_about_time.png"), - at: [13, 219], - width: 409, - ) - - # Render speech bubble - # pdf.image( - # image_path(speech_bubble_image), - # at: [speech_position[:x], speech_position[:y]], - # width: speech_position[:width] - # ) - - # Render return address - render_return_address(pdf, letter, 10, 278, 146, 70, font: "f25") - - # Render destination address in speech bubble - render_destination_address( - pdf, - letter, - 80, - 134, - 290, - 86, - { size: 19, valign: :top, align: :left } - ) - - # Render IMb barcode - render_imb(pdf, letter, 216, 25, 207) - - render_letter_id(pdf, letter, 10, 19, 10) - render_qr_code(pdf, letter, 5, 55, 50) - - render_postage(pdf, letter) - end - end - end -end diff --git a/app/lib/snail_mail/templates/half_letter_template.rb b/app/lib/snail_mail/templates/half_letter_template.rb deleted file mode 100644 index 876eee2..0000000 --- a/app/lib/snail_mail/templates/half_letter_template.rb +++ /dev/null @@ -1,41 +0,0 @@ -# frozen_string_literal: true -module SnailMail - module Templates - class HalfLetterTemplate < BaseTemplate - ADDRESS_FONT = "f25" - - def self.template_name - raise NotImplementedError, "Subclass must implement template_name" - end - - def self.template_size - :half_letter - end - - def render_front(pdf, letter) - raise NotImplementedError, "Subclass must implement render_front" - end - - def render(pdf, letter) - render_front(pdf, letter) - - pdf.start_new_page - - render_postage(pdf, letter) - - render_return_address(pdf, letter, 10, pdf.bounds.top - 10, 146, 70) - render_imb(pdf, letter, pdf.bounds.right - 200, pdf.bounds.bottom + 17, 180) - - render_destination_address( - pdf, - letter, - 150, - pdf.bounds.bottom + 210, - 300, - 100, - { size: 23, valign: :bottom, align: :left, font: self.class::ADDRESS_FONT } - ) - end - end - end -end diff --git a/app/lib/snail_mail/templates/hcb_stickers_template.rb b/app/lib/snail_mail/templates/hcb_stickers_template.rb deleted file mode 100644 index 6f38f39..0000000 --- a/app/lib/snail_mail/templates/hcb_stickers_template.rb +++ /dev/null @@ -1,46 +0,0 @@ -module SnailMail - module Templates - class HCBStickersTemplate < BaseTemplate - def self.template_name - "HCB Stickers" - end - - def render(pdf, letter) - pdf.image( - image_path("lilia-hcb-stickers-bg.png"), - at: [0, 288], - width: 432, - ) - - # Render speech bubble - # pdf.image( - # image_path(speech_bubble_image), - # at: [speech_position[:x], speech_position[:y]], - # width: speech_position[:width] - # ) - - # Render return address - render_return_address(pdf, letter, 10, 278, 146, 70) - - # Render destination address in speech bubble - render_destination_address( - pdf, - letter, - 192, - 149, - 226, - 57, - { size: 16, valign: :bottom, align: :left } - ) - - # Render IMb barcode - render_imb(pdf, letter, 216, 25, 207) - - render_letter_id(pdf, letter, 10, 12, 10) - render_qr_code(pdf, letter, 5, 196, 50) - - render_postage(pdf, letter) - end - end - end -end diff --git a/app/lib/snail_mail/templates/hcb_welcome_postcard_template.rb b/app/lib/snail_mail/templates/hcb_welcome_postcard_template.rb deleted file mode 100644 index 0780d2c..0000000 --- a/app/lib/snail_mail/templates/hcb_welcome_postcard_template.rb +++ /dev/null @@ -1,35 +0,0 @@ -module SnailMail - module Templates - class HCBWelcomePostcardTemplate < HalfLetterTemplate - ADDRESS_FONT = "arial" - - def self.template_name - "hcb welcome postcard" - end - - SAMPLE_WELCOME_TEXT = "Hey! - - I'm super excited to work with your org because I think whatever you do is a really important cause and it aligns perfectly with our mission to support things that we believe are good. - - At HCB, we're all about empowering organizations like yours to make a real difference in the world. We believe in the power of community, innovation, and collaboration to create positive change. Your work resonates deeply with our values, and we can't wait to see the amazing things we'll accomplish together. - - We're here to support you every step of the way. Whether you need technical assistance, community resources, or just someone to bounce ideas off of, our team is ready to help. We're not just a service provider – we're your partner in making the world a better place. - - Let's build something incredible together! - - Warm regards, - The HCB Team" - - def render_front(pdf, letter) - pdf.bounding_box([10, pdf.bounds.top - 10], width: pdf.bounds.width - 20, height: pdf.bounds.height - 20) do - pdf.image(image_path("hcb/hcb-icon.png"), width: 60) - pdf.text_box("Welcome to HCB!", size: 30, at: [70, pdf.bounds.top - 18]) - end - - pdf.bounding_box([20, pdf.bounds.top - 90], width: pdf.bounds.width - 40, height: pdf.bounds.height - 100) do - pdf.text(letter.rubber_stamps || "", size: 15, align: :justify, overflow: :shrink_to_fit) - end - end - end - end -end diff --git a/app/lib/snail_mail/templates/hcpcxc_template.rb b/app/lib/snail_mail/templates/hcpcxc_template.rb deleted file mode 100644 index 0f5b6a5..0000000 --- a/app/lib/snail_mail/templates/hcpcxc_template.rb +++ /dev/null @@ -1,55 +0,0 @@ -module SnailMail - module Templates - class HcpcxcTemplate < BaseTemplate - def self.template_name - "hcpcxc" - end - - - def render(pdf, letter) - pdf.image( - image_path("dino-waving.png"), - at: [ 333, 163 ], - width: 87 - ) - - pdf.image( - image_path("hcpcxc_ra.png"), - at: [ 5, 288-5 ], - width: 175 - ) - - render_destination_address( - pdf, - letter, - 88, - 166, - 236, - 71, - { size: 16, valign: :bottom, align: :left } - ) - - # Render IMb barcode - render_imb(pdf, letter, 240, 24, 183) - - render_qr_code(pdf, letter, 5, 65, 60) - - render_letter_id(pdf, letter, 10, 19, 10) - if letter.rubber_stamps.present? - pdf.font("arial") do - pdf.text_box( - letter.rubber_stamps, - at: [ 294, 220 ], - width: 255, - height: 21, - overflow: :shrink_to_fit, - disable_wrap_by_char: true, - min_size: 1 - ) - end - end - render_postage(pdf, letter) - end - end - end -end diff --git a/app/lib/snail_mail/templates/heidi_readme_template.rb b/app/lib/snail_mail/templates/heidi_readme_template.rb deleted file mode 100644 index d89f162..0000000 --- a/app/lib/snail_mail/templates/heidi_readme_template.rb +++ /dev/null @@ -1,49 +0,0 @@ -module SnailMail - module Templates - class HeidiReadmeTemplate < BaseTemplate - def self.template_name - "Heidi Can't Readme" - end - - def self.show_on_single? - true - end - - def render(pdf, letter) - render_return_address(pdf, letter, 10, 278, 190, 90, size: 12, font: "f25") - - render_destination_address( - pdf, - letter, - 133, - 176, - 256, - 107, - { size: 18, valign: :center, align: :left } - ) - - pdf.stroke do - pdf.line_width = 2.5 - pdf.rounded_rectangle([90 + 20, 189 - 5], 306, 122, 10) - end - - pdf.image( - image_path("msw-heidi-cant-readme.png"), - at: [6 + 20, 75], - width: 111, - ) - - pdf.image( - image_path("speech-tail.png"), - at: [114 + 20, 70 - 5], - width: 32.2, - ) - - render_imb(pdf, letter, 230, 25, 190) - render_letter_id(pdf, letter, 3, 15, 8, rotate: 90) - render_qr_code(pdf, letter, 7, 72 + 7 + 50 + 10 + 12, 60) - render_postage(pdf, letter) - end - end - end -end diff --git a/app/lib/snail_mail/templates/joyous_cat_template.rb b/app/lib/snail_mail/templates/joyous_cat_template.rb deleted file mode 100644 index bc0f43b..0000000 --- a/app/lib/snail_mail/templates/joyous_cat_template.rb +++ /dev/null @@ -1,43 +0,0 @@ -module SnailMail - module Templates - class JoyousCatTemplate < BaseTemplate - def self.template_name - "Joyous Cat :3" - end - - def self.show_on_single? - true - end - - def render(pdf, letter) - pdf.line_width = 3 - pdf.stroke do - pdf.rounded_rectangle([111, 189], 306, 122, 10) - end - - pdf.image( - image_path("acon-joyous-cat.png"), - at: [208, 74], - width: 106.4, - ) - - render_return_address(pdf, letter, 10, 270, 130, 70) - - render_destination_address( - pdf, - letter, - 134, - 173, - 266, - 67, - { size: 16, valign: :center, align: :left } - ) - - render_imb(pdf, letter, 131, 100, 266) - - render_qr_code(pdf, letter, 7, 72 + 7, 72) - render_postage(pdf, letter) - end - end - end -end diff --git a/app/lib/snail_mail/templates/kestrel_heidi_template.rb b/app/lib/snail_mail/templates/kestrel_heidi_template.rb deleted file mode 100644 index 147430d..0000000 --- a/app/lib/snail_mail/templates/kestrel_heidi_template.rb +++ /dev/null @@ -1,39 +0,0 @@ -module SnailMail - module Templates - class KestrelHeidiTemplate < BaseTemplate - def self.template_name - "kestrel's heidi template!" - end - - def self.show_on_single? - true - end - - def render(pdf, letter) - pdf.image( - image_path("kestrel-mail-heidi.png"), - at: [107, 216], - width: 305, - ) - - render_return_address(pdf, letter, 10, 278, 190, 90, size: 14) - - render_destination_address( - pdf, - letter, - 126, - 201, - 266, - 67, - { size: 16, valign: :center, align: :left } - ) - - render_imb(pdf, letter, 124, 120, 200) - - render_qr_code(pdf, letter, 7, 72 + 7, 72) - - render_postage(pdf, letter) - end - end - end -end diff --git a/app/lib/snail_mail/templates/mail_orpheus_template.rb b/app/lib/snail_mail/templates/mail_orpheus_template.rb deleted file mode 100644 index 289bb46..0000000 --- a/app/lib/snail_mail/templates/mail_orpheus_template.rb +++ /dev/null @@ -1,49 +0,0 @@ -module SnailMail - module Templates - class MailOrpheusTemplate < BaseTemplate - def self.template_name - "Mail Orpheus!" - end - - def self.show_on_single? - true - end - - def render(pdf, letter) - pdf.image( - image_path("eleeza-mail-orpheus.png"), - at: [320, 113], - width: 106.4, - ) - - # Render speech bubble - # pdf.image( - # image_path(speech_bubble_image), - # at: [speech_position[:x], speech_position[:y]], - # width: speech_position[:width] - # ) - - # Render return address - render_return_address(pdf, letter, 10, 270, 130, 70) - - # Render destination address in speech bubble - render_destination_address( - pdf, - letter, - 79.5, - 202, - 237, - 100, - { size: 16, valign: :bottom, align: :left } - ) - - # Render IMb barcode - render_imb(pdf, letter, 78, 102, 237) - - # Render QR code for tracking - render_qr_code(pdf, letter, 7, 67, 60) - render_postage(pdf, letter) - end - end - end -end diff --git a/app/lib/snail_mail/templates/orpheus_template.rb b/app/lib/snail_mail/templates/orpheus_template.rb deleted file mode 100644 index a4ff105..0000000 --- a/app/lib/snail_mail/templates/orpheus_template.rb +++ /dev/null @@ -1,15 +0,0 @@ -require_relative "character_template" - -module SnailMail - module Templates - class OrpheusTemplate < CharacterTemplate - def initialize(options = {}) - super(options.merge( - character_image: "eleeza-mail-orpheus.png", - character_position: { x: 10, y: 85, width: 130 }, - speech_position: { x: 95, y: 240, width: 290, height: 90 } - )) - end - end - end -end diff --git a/app/lib/snail_mail/templates/summer_of_making_free_stickers_template.rb b/app/lib/snail_mail/templates/summer_of_making_free_stickers_template.rb deleted file mode 100644 index 313ab34..0000000 --- a/app/lib/snail_mail/templates/summer_of_making_free_stickers_template.rb +++ /dev/null @@ -1,38 +0,0 @@ -module SnailMail - module Templates - class SummerOfMakingFreeStickersTemplate < BaseTemplate - def self.template_name - "SoM Free Stickers" - end - - def self.show_on_single? - true - end - - def render(pdf, letter) - render_return_address(pdf, letter, 5, pdf.bounds.top - 5, 190, 90, size: 8, font: "f25") - - render_destination_address( - pdf, - letter, - 120, - 115, - 270, - 81, - { size: 18, valign: :center, align: :left } - ) - - pdf.image( - image_path("som/banner.png"), - at: [-5, 288 - 56], - width: 445, - ) - - render_imb(pdf, letter, 245, 20, 170) - render_letter_id(pdf, letter, 3, 15, 8, rotate: 90) - render_qr_code(pdf, letter, 2, 52, 50) - render_postage(pdf, letter) - end - end - end -end diff --git a/app/lib/snail_mail/templates/tarot_template.rb b/app/lib/snail_mail/templates/tarot_template.rb deleted file mode 100644 index 4ef835a..0000000 --- a/app/lib/snail_mail/templates/tarot_template.rb +++ /dev/null @@ -1,63 +0,0 @@ -module SnailMail - module Templates - class TarotTemplate < BaseTemplate - def self.template_name - "Tarot" - end - - def render(pdf, letter) - render_return_address(pdf, letter, 10, 278, 190, 90, size: 12, font: 'comic') - - if letter.rubber_stamps.present? - pdf.font("gohu") do - pdf.text_box( - "\"#{letter.rubber_stamps}\"", - at: [ 137, 183 ], - width: 255, - height: 21, - overflow: :shrink_to_fit, - disable_wrap_by_char: true, - min_size: 1 - ) - end - end - - render_destination_address( - pdf, - letter, - 137, - 160, - 255, - 90, - { size: 16, valign: :center, align: :left } - ) - pdf.stroke do - pdf.line_width = 1 - pdf.line([ 137 - 25, 167 ], [ 392 + 25, 167 ]) - end - - pdf.stroke do - pdf.line_width = 2.5 - pdf.rounded_rectangle([ 111, 189 ], 306, 122, 10) - end - - pdf.image( - image_path("tarot/msw-joker.png"), - at: [ 6, 104 ], - width: 111 - ) - - pdf.image( - image_path("speech-tail.png"), - at: [ 118, 70 ], - width: 32.2 - ) - - render_imb(pdf, letter, 216, 25, 207) - render_letter_id(pdf, letter, 3, 15, 8, rotate: 90) - render_qr_code(pdf, letter, 7, 72 + 7, 72) - render_postage(pdf, letter) - end - end - end -end diff --git a/app/models/letter.rb b/app/models/letter.rb index 3397207..c23c6a6 100644 --- a/app/models/letter.rb +++ b/app/models/letter.rb @@ -116,7 +116,7 @@ class Letter < ApplicationRecord # Generate a label for this letter def generate_label(options = {}) - pdf = SnailMail::Service.generate_label(self, options) + pdf = SnailMail::PhlexService.generate_label(self, options) # Directly attach the PDF to this letter attach_pdf(pdf.render) diff --git a/app/models/letter/batch.rb b/app/models/letter/batch.rb index ff15678..95e2092 100644 --- a/app/models/letter/batch.rb +++ b/app/models/letter/batch.rb @@ -348,7 +348,7 @@ class Letter::Batch < Batch end # Use the SnailMail service to generate labels - pdf = SnailMail::Service.generate_batch_labels( + pdf = SnailMail::PhlexService.generate_batch_labels( preloaded_letters, label_options.merge(options) ) diff --git a/app/views/batches/process_letter.html.erb b/app/views/batches/process_letter.html.erb index ae92aa6..da8cffb 100644 --- a/app/views/batches/process_letter.html.erb +++ b/app/views/batches/process_letter.html.erb @@ -13,7 +13,7 @@ <%= form.label :template_cycle, "Label Templates" %>