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.
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.
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)
- Route:
GET /admin/jobs/:id/ticket_background - Controller:
Admin::JobsController#ticket_background— justrender 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
barbygem (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 viaBarby::Code39— used separately from the ticket flow
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 heightflatten: { 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.
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.
# 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
endTicketImpositionPlan 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).
-
to_htmlstrategy — Renderticket_background.erbto a string viaApplicationController.renderer. No iframes. Works in controllers, background jobs, and console without a live request. -
Press proofs — TODO.
TicketImpositionPlanalready supports this viainclude_press_proofparameter. Addto_proof_pdf_url/to_proof_bitmap_urlwhen press proof printing is needed. -
Error handling — Raise on iw5 failure. The old app was fire-and-forget; the new app will raise so callers know when rendering failed.
-
hostdefault —ENV.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
- production:
-
Print queue (IwPrintQueue / IwPrintRequest) — Deferred. The old app enqueues tickets in Redis so a physical ticket printer can poll. Out of scope for
TicketRendererbut will be needed for the ticket station feature.
None of these files exist yet in catprint_on_rails. Port them in the order listed.
| 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 |
| 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/. |
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.
| 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 |
| 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/. |
| 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 |
| 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 |
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.
- Add
Catprint::IW_LOCATIONtoconfig/application.rb - Add
httpclientandbarbyto Gemfile - Add
Imposition::BLEED_WIDTHconstant (simple concern) - Add
imposed_width,imposed_height,imposed_pagesto Job (passthrough versions first) - Port
JobPagePlan - Port
PackingSlipsmodule (stub any missing order/user/production_center methods) - Port
TicketImpositionPlan - Port
ticket_background.erbview + route + controller action - Implement
TicketRenderer - Write specs mirroring
spec/models/imposition/ticket_imposition_spec.rbfrom old app - Manually verify with
job.ticket_renderer.to_bitmap_urlin console against iw5
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