Skip to content

Instantly share code, notes, and snippets.

@catmando
Created June 23, 2026 12:34
Show Gist options
  • Select an option

  • Save catmando/c8447156bcf1a1a9521aa28808ff580e to your computer and use it in GitHub Desktop.

Select an option

Save catmando/c8447156bcf1a1a9521aa28808ff580e to your computer and use it in GitHub Desktop.
CatPrint ticket renderer porting guide

Ticket Renderer: Architecture, Porting Guide & Open Decisions

Handoff context

This doc was written by Claude after researching both the old app (catprint/) and the new app (catprint_on_rails/) in June 2026. It is intended to be handed to a developer who will implement this feature by working with Claude in the catprint_on_rails repo. Claude will have full context if you paste this doc into the conversation at the start of the session.

The old app lives at /Users/mitchvanduyn/catprintlabs/catprint. The new app lives at /Users/mitchvanduyn/catprintlabs/catprint_on_rails.

Before starting: make sure the old catprint repo is checked out as a sibling directory of catprint_on_rails (i.e. both live under the same parent folder). Claude will need to read files from it directly during implementation — it is the authoritative reference for every file being ported.


What we are building

A TicketRenderer service object in the new Rails app that can produce a job ticket in three formats:

Method What it does Calls iw5?
to_html Returns the ticket as an HTML string No
to_pdf_url Submits to ImageWarehouse, returns PDF URL Yes
to_bitmap_url Same but rasterized at 300 DPI Yes

A "ticket" is the physical job sheet printed and attached to every production order. It is 8.5 × 14 inches and contains: barcode, job ID, job specs, a thumbnail of the customer's artwork, and a shipping insert / packing slip at the bottom.


How the old system works (full architecture)

The three-part pipeline

1. ticket_background  (HTML page rendered by Rails)
         ↓  URL is embedded in the imposition plan
2. TicketImpositionPlan  (JSON layout descriptor sent to iw5)
         ↓  POSTed to iw5.catprint.com
3. ImageWarehouse / iw5  (renders HTML → PDF/bitmap via wkhtmltopdf)

Part 1 — ticket_background HTML page

  • Route: GET /admin/jobs/:id/ticket_background
  • Controller: Admin::JobsController#ticket_background — just render layout: false
  • View: app/views/admin/jobs/ticket_background.erb
  • This is a full standalone HTML page that wkhtmltopdf loads and renders
  • It uses the barby gem (Code 39 barcode) and references background images from /public/images/ticket_backgrounds/ (reprint, special_handling, cancelled, etc.)
  • The barcode image inside the HTML is fetched live from iw5: https://iw5.catprint.com/v1/barcode.png?text=<job_number>&height=50&size=2&extended=true&margin=0
  • There is also a standalone Rails barcode action (Admin::JobsController#barcode) that renders a PNG inline via Barby::Code39 — used separately from the ticket flow

Part 2 — TicketImpositionPlan

File: catprint/app/models/ticket_imposition_plan.rb

Builds a JSON document that tells iw5 exactly how to lay out one page. For a standard ticket (no press proof) the page has three slot groups:

Slot Method Position on page
Artwork thumbnails thumbnail_slots(host) Middle band — scaled copies of each artwork page
Ticket background background_slot(host) Top 5.5″ — the ticket_background HTML rendered by wkhtmltopdf
Shipping insert shipping_insert_slot(host) Bottom 3.5″ — packing slip PDF or HTML, rotated 90°

Page size constants (PostScript points, 72 pt = 1 inch):

TICKET_WIDTH            = 612.0   # 8.5"
TICKET_HEIGHT           = 1008.0  # 14.0"
TICKET_TEXT_AREA_HEIGHT = 396.0   # 5.5"  ← background slot height
SHIPPING_INSERT_HEIGHT  = 252.0   # 3.5"  ← packing slip slot height

flatten: { density: 300 } in the plan tells iw5 to rasterize, producing ~2550 × 4200 px. When flatten is omitted the output is a vector PDF.

The host parameter is the base URL iw5 uses to fetch the HTML pages. In a controller it comes from ticket_origin_url (request.original_url.split('/admin')[0]). Outside a request (console, background jobs) pass "https://www.catprint.com" directly.

Part 3 — Submitting to ImageWarehouse

Minimal console incantation (already verified working):

job   = Job.find(job_id)
plan  = TicketImpositionPlan.new(job).to_json("https://www.catprint.com")
token = SecureRandom.hex

HTTPClient.new.post("#{Catprint::IW_LOCATION}/v1/imposings",
                    { secure_token: token, json: plan })

url = "#{Catprint::IW_LOCATION}/v1/imposings/#{token}/#{job.friendly_id}_ticket.pdf"

Catprint::IW_LOCATION defaults to https://iw5.catprint.com (set in catprint/config/application.rb line 35: ENV['IW_LOCATION'] || 'https://iw5.catprint.com').

In production the old app uses IwPrintRequest instead, which additionally enqueues via IwPrintQueue (Redis-backed) so a physical ticket printer at the station can poll for the next job. That is a separate concern from rendering and can be deferred.


Proposed TicketRenderer service object

# app/services/ticket_renderer.rb

class TicketRenderer
  def initialize(job, host: ENV.fetch("CATPRINT_HOST"))
    @job  = job
    @host = host
  end

  # Returns the ticket as an HTML string — no iw5 call.
  # Renders ticket_background.erb via ActionView so it works anywhere
  # (controllers, background jobs, console). Caller inserts it directly into a page.
  def to_html
    ApplicationController.renderer.render(
      template: "admin/jobs/ticket_background",
      layout:   false,
      assigns:  { job: @job, host: @host }
    )
  end

  # Returns a URL to a vector PDF on iw5.
  def to_pdf_url
    submit_to_iw(flatten: nil)
  end

  # Returns a URL to a rasterized bitmap PDF on iw5 (300 DPI, ~2550×4200 px).
  def to_bitmap_url
    submit_to_iw(flatten: { density: 300 })
  end

  # TODO: press proof support (to_proof_pdf_url, to_proof_bitmap_url)
  # TicketImpositionPlan already supports this via include_press_proof parameter.
  # Defer until press proof printing is needed.

  private

  def submit_to_iw(flatten:)
    plan     = TicketImpositionPlan.new(@job).to_json(@host, nil, true, flatten)
    token    = SecureRandom.hex
    response = HTTPClient.new.post("#{Catprint::IW_LOCATION}/v1/imposings",
                                   { secure_token: token, json: plan })

    raise "iw5 error (#{response.status}): #{response.body}" unless response.ok?

    "#{Catprint::IW_LOCATION}/v1/imposings/#{token}/#{@job.friendly_id}_ticket.pdf"
  end
end

TicketImpositionPlan stays as a separate value object (pure data, no I/O) — it is already well-scoped and has its own spec in the old app (spec/models/imposition/ticket_imposition_spec.rb).


Design decisions (settled)

  1. to_html strategy — Render ticket_background.erb to a string via ApplicationController.renderer. No iframes. Works in controllers, background jobs, and console without a live request.

  2. Press proofs — TODO. TicketImpositionPlan already supports this via include_press_proof parameter. Add to_proof_pdf_url / to_proof_bitmap_url when press proof printing is needed.

  3. Error handling — Raise on iw5 failure. The old app was fire-and-forget; the new app will raise so callers know when rendering failed.

  4. host defaultENV.fetch("CATPRINT_HOST") with no fallback, so each environment must configure it explicitly. Add to credentials or .env:

    • production: CATPRINT_HOST=https://www.catprint.com
    • staging: CATPRINT_HOST=https://staging.catprint.com
    • development: CATPRINT_HOST=http://localhost:3000
  5. Print queue (IwPrintQueue / IwPrintRequest) — Deferred. The old app enqueues tickets in Redis so a physical ticket printer can poll. Out of scope for TicketRenderer but will be needed for the ticket station feature.


Full dependency tree — what needs porting

None of these files exist yet in catprint_on_rails. Port them in the order listed.

Layer 1 — Infrastructure (port first)

File in old app What it is
config/application.rb lines 35-36 Add Catprint::IW_LOCATION and Catprint::IW_LOCATION_HTTP constants
Gemfile: httpclient HTTP client used to POST to iw5
Gemfile: barby Barcode generation (Code 39) — used in ticket_background.erb and the barcode action

Layer 2 — Constants

File in old app What it is
app/hyperstack/models/imposition.rb Only need Imposition::BLEED_WIDTH = 9.0. Can be a simple module in app/models/concerns/.

Layer 3 — Job methods

These methods must exist on the Job model (or a concern) before TicketImpositionPlan and JobPagePlan will work. They all live in catprint/app/hyperstack/models/job.rb.

Method What it does
imposed_width Width after folding/imposition (may differ from raw width)
imposed_height Height after folding/imposition
imposed_pages Number of imposed page slots (accounts for booklet layout)
page_plan(page_number, options) Returns a JobPagePlan for one artwork page
black_and_white?(page_number) Used in press proof slots to add bw_warning param
needs_booklet_imposition? Used inside JobPagePlan
finishing.fold_offsets Used inside JobPagePlan
creep(page) Bleed extension for booklet pages

For an initial implementation, simple passthrough versions of imposed_width/imposed_height (just returning width and height) and imposed_pages (just returning pages) may be enough to get non-booklet tickets rendering. The full booklet logic can follow.

Layer 4 — Core models

File in old app What it is Depends on
app/models/job_page_plan.rb Computes slot coordinates for one artwork page; handles bleeds, folds, booklet imposition, stamps Imposition::BLEED_WIDTH, job methods above
app/models/packing_slips.rb Module — resolves which packing slip URL (PDF or HTML) to use for the shipping insert slot. Reads from order, user, and production center. job.order, job.user, job.production_center associations
app/models/ticket_imposition_plan.rb Builds the full JSON layout plan JobPagePlan, PackingSlips, job methods above

Layer 5 — View

File in old app What it is
app/views/admin/jobs/ticket_background.erb Standalone HTML rendered by wkhtmltopdf. Uses barby, ticket background images, and many job/order methods. Port as-is and fix method references.
public/images/ticket_backgrounds/ Background images (reprint, special_handling, cancelled, production_signoff PNGs). Copy to public/.

Layer 6 — Service and controller wiring

What Notes
app/services/ticket_renderer.rb The new service object (see sketch above)
Route: GET /admin/jobs/:id/ticket_background Add to routes
Controller action: Admin::Jobs::TicketBackgroundController or action on existing jobs controller Calls render layout: false
Route + action: GET /admin/jobs/:id/barcode Optional — standalone PNG barcode

Defer (not needed for initial TicketRenderer)

File Why deferred
app/models/iw_print_request.rb Only needed for ticket station print queue
app/models/iw_print_queue.rb Redis-backed queue — ticket station feature
Press proof methods in ticket_imposition_plan.rb build_press_proof_pages, press_proof_background_slot, etc.
job.transform_color, job.paper_background_image Press proof rendering paths only

PackingSlips module — methods it calls on related models

The shipping insert slot routing in PackingSlips touches several associations. These must exist on the relevant models in the new app:

Association / method Model
order.is_drop_ship? Order
order.first_order? Order
order.gift_note Order
order.drop_ship_packing_slip_url Order
order.hard_copy_proof? Order
user.drop_ship_insert_url User
user.custom_ticket_with_number User
user.gift_note_template (ActiveStorage attachment) User
production_center.default_packing_slip? / .default_packing_slip.url ProductionCenter
production_center.gift_note_template (ActiveStorage attachment) ProductionCenter
production_center.drop_ship_gift_note_template (ActiveStorage attachment) ProductionCenter
job.all_instructions Job — combined instructions string used to detect drop-ship insert URLs
job.personalized? Job
job.proof_approved? Job

If any of these don't exist yet in the new app, the shipping insert slot will need stubbing or skipping until those models are ported.


Suggested implementation order

  1. Add Catprint::IW_LOCATION to config/application.rb
  2. Add httpclient and barby to Gemfile
  3. Add Imposition::BLEED_WIDTH constant (simple concern)
  4. Add imposed_width, imposed_height, imposed_pages to Job (passthrough versions first)
  5. Port JobPagePlan
  6. Port PackingSlips module (stub any missing order/user/production_center methods)
  7. Port TicketImpositionPlan
  8. Port ticket_background.erb view + route + controller action
  9. Implement TicketRenderer
  10. Write specs mirroring spec/models/imposition/ticket_imposition_spec.rb from old app
  11. Manually verify with job.ticket_renderer.to_bitmap_url in console against iw5

Key files to read in the old app

catprint/app/models/ticket_imposition_plan.rb      ← start here
catprint/app/models/packing_slips.rb
catprint/app/models/iw_print_request.rb
catprint/app/models/iw_print_queue.rb
catprint/app/models/job_page_plan.rb
catprint/app/hyperstack/models/job.rb              ← imposed_width/height/pages, page_plan
catprint/app/hyperstack/models/imposition.rb       ← BLEED_WIDTH
catprint/app/views/admin/jobs/ticket_background.erb
catprint/app/controllers/admin/jobs_controller.rb  ← ticket, tickets, ticket_background, barcode actions
catprint/spec/models/imposition/ticket_imposition_spec.rb
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment