Class: GeneratedPdfGenerator
- Inherits:
-
Object
- Object
- GeneratedPdfGenerator
- Defined in:
- app/services/generated_pdf_generator.rb
Overview
Renders a PDF (via Pdf::Toolkit) and stages it as a GeneratedPdf for review
in the PDF studio — the PDF analogue of +ImageGenerationWorker+'s staging step.
Shared by the synchronous studio create action and the async
PdfGenerationWorker. PDF generation is fast (no external API), so the studio
renders inline; the worker exists for the Sunny hand-off and future async
reconstruction.
The stored +layout+ keeps stable references (an image's library id/URL, a
video URL); GeneratedPdfGenerator.render_bytes resolves those to renderable form at render time —
image references are downloaded to a temp file, and a +video+ block expands
into a QR code ("scan to watch") plus a visible link.
Usage:
result = GeneratedPdfGenerator.from_layout(layout:, title: "Spec", created_by_id: 42)
result.success? # => true
result.generated_pdf # => GeneratedPdf (status pending)
Defined Under Namespace
Classes: Result
Class Method Summary collapse
-
.from_layout(layout:, title: nil, instructions: nil, created_by_id: nil, source_publication_id: nil, source_record: nil) ⇒ Result
Render a declarative layout into a staged PDF.
-
.layout_from_text(title:, subtitle: nil, content: nil) ⇒ Hash
Build a layout Hash from a title/subtitle + a lightweight markdown-ish body: "# Heading" → heading "- item" / "* item" → bullets "
" → image (src = library Image id or image URL) "@video URL | cap" → video (renders a QR + a link) "text" → link blank line → spacer anything else → paragraph.
-
.render_bytes(layout) ⇒ Pdf::Toolkit::Result
Resolve a stored layout's media references and render it to a Toolkit result.
-
.stage(bytes:, layout: {}, kind: 'generated', title: nil, instructions: nil, created_by_id: nil, source_publication_id: nil, source_record: nil, page_count: nil, filename: nil) ⇒ Result
Stage already-rendered PDF bytes (e.g. from the Sunny pdf_edit/pdf_generate tools, or a reconstruction loop) with the layout that produced them.
-
.transcript_seed(video_ref) ⇒ String?
Pull a video's transcript text to seed a "summary sheet" generation.
Class Method Details
.from_layout(layout:, title: nil, instructions: nil, created_by_id: nil, source_publication_id: nil, source_record: nil) ⇒ Result
Render a declarative layout into a staged PDF. The original +layout+ (with
its image/video references intact) is what gets stored for iteration.
34 35 36 37 38 39 40 41 42 43 |
# File 'app/services/generated_pdf_generator.rb', line 34 def from_layout(layout:, title: nil, instructions: nil, created_by_id: nil, source_publication_id: nil, source_record: nil) rendered = render_bytes(layout) stage(bytes: rendered.bytes, layout: layout, kind: 'generated', title: title, instructions: instructions, created_by_id: created_by_id, source_publication_id: source_publication_id, source_record: source_record, page_count: rendered.[:pages]) rescue Pdf::Toolkit::Error => e Result.new(error: e.) end |
.layout_from_text(title:, subtitle: nil, content: nil) ⇒ Hash
Build a layout Hash from a title/subtitle + a lightweight markdown-ish body:
"# Heading" → heading
"- item" / "* item" → bullets
"" → image (src = library Image id or image URL)
"@video URL | cap" → video (renders a QR + a link)
"text" → link
blank line → spacer
anything else → paragraph
92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 |
# File 'app/services/generated_pdf_generator.rb', line 92 def layout_from_text(title:, subtitle: nil, content: nil) blocks = [] content.to_s.each_line do |raw| line = raw.strip if line.empty? blocks << { 'type' => 'spacer', 'size' => 6 } unless blocks.last&.dig('type') == 'spacer' elsif (m = line.match(/\A!\[(.*?)\]\((.+?)\)\z/)) blocks << { 'type' => 'image', 'source' => m[2].strip, 'caption' => m[1].strip.presence }.compact elsif (m = line.match(/\A@video\s+(\S+)(?:\s*\|\s*(.+))?\z/i)) blocks << { 'type' => 'video', 'url' => m[1].strip, 'caption' => m[2]&.strip.presence }.compact elsif (m = line.match(%r{\A\[(.+?)\]\((https?://[^)]+)\)\z})) blocks << { 'type' => 'link', 'text' => m[1].strip, 'url' => m[2].strip } elsif line.start_with?('#') blocks << { 'type' => 'heading', 'text' => line.sub(/\A\#+\s*/, '') } elsif line.start_with?('- ', '* ') item = line.sub(/\A[-*]\s+/, '') if blocks.last&.dig('type') == 'bullets' blocks.last['items'] << item else blocks << { 'type' => 'bullets', 'items' => [item] } end else blocks << { 'type' => 'paragraph', 'text' => line } end end { 'title' => title, 'subtitle' => subtitle.presence, 'blocks' => blocks }.compact end |
.render_bytes(layout) ⇒ Pdf::Toolkit::Result
Resolve a stored layout's media references and render it to a Toolkit result.
Used by both from_layout and the studio's regenerate action so iteration
re-resolves references. Temp files for downloaded images are cleaned up.
50 51 52 53 54 55 |
# File 'app/services/generated_pdf_generator.rb', line 50 def render_bytes(layout) temp_paths = [] Pdf::Toolkit.generate(layout: resolve_media(layout, temp_paths)) ensure temp_paths&.each { |p| File.delete(p) if p && File.exist?(p) } end |
.stage(bytes:, layout: {}, kind: 'generated', title: nil, instructions: nil, created_by_id: nil, source_publication_id: nil, source_record: nil, page_count: nil, filename: nil) ⇒ Result
Stage already-rendered PDF bytes (e.g. from the Sunny pdf_edit/pdf_generate
tools, or a reconstruction loop) with the layout that produced them.
60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 |
# File 'app/services/generated_pdf_generator.rb', line 60 def stage(bytes:, layout: {}, kind: 'generated', title: nil, instructions: nil, created_by_id: nil, source_publication_id: nil, source_record: nil, page_count: nil, filename: nil) gen = GeneratedPdf.new( kind: kind, layout: layout || {}, title: title, suggested_title: title, instructions: instructions, created_by_id: created_by_id, source_publication_id: source_publication_id, source_record: source_record, page_count: page_count ) gen.assign_pdf(bytes, filename: filename || default_filename(title)) gen.page_count ||= count_pages(bytes) gen.save! Result.new(generated_pdf: gen) rescue StandardError => e Rails.logger.error "[GeneratedPdfGenerator] #{e.class}: #{e.}" Result.new(error: e.) end |
.transcript_seed(video_ref) ⇒ String?
Pull a video's transcript text to seed a "summary sheet" generation.
123 124 125 126 127 128 |
# File 'app/services/generated_pdf_generator.rb', line 123 def transcript_seed(video_ref) video = video_ref.is_a?(Video) ? video_ref : Video.find_by(id: video_ref) return nil unless video.respond_to?(:transcript) video.transcript.presence end |