require 'pdf-reader' class PdfProcessorService DEFAULT_OPTIONS = { quality: 40, resize_percentage: 50, format: 'jpg' }.freeze def initialize(resource, options = {}) @resource = resource @options = DEFAULT_OPTIONS.merge(options) end def process_pdfs @resource.pdfs.each do |pdf| pdf_path = get_blob_path(pdf) process_single_pdf(pdf_path) end end private def get_blob_path(pdf) ActiveStorage::Blob.service.send(:path_for, pdf.key) end def process_single_pdf(pdf_path) page_count = get_page_count(pdf_path) (0...page_count).each do |index| Rails.logger.info "Processing page #{index + 1} of #{page_count} from #{pdf_path}" process_page(pdf_path, index) rescue StandardError => e Rails.logger.error "Failed to process page #{index + 1}: #{e.message}" raise end end def get_page_count(pdf_path) Rails.logger.debug "Checking PDF: #{pdf_path}" reader = PDF::Reader.new(pdf_path) count = reader.page_count Rails.logger.debug "PDF page count: #{count}" if count <= 0 || count > 1000 # Reasonable maximum page limit raise "Invalid page count: #{count}" end count rescue StandardError => e Rails.logger.error "Failed to get page count: #{e.message}" raise end def process_page(pdf_path, page_index) temp_files = nil temp_files = create_page_images(pdf_path, page_index) # Generate temp files attach_processed_image(temp_files[:reduced], page_index) # Attach image ensure cleanup_temp_files(temp_files) # Cleanup, even if an error occurs end def create_page_images(pdf_path, index) original_path = "page-#{index + 1}.#{@options[:format]}" reduced_path = "reduced-page-#{index + 1}.#{@options[:format]}" create_original_image(pdf_path, index, original_path) create_reduced_image(original_path, reduced_path) { original: original_path, reduced: reduced_path } end def create_original_image(pdf_path, index, output_path) MiniMagick::Tool::Convert.new do |convert| convert << "#{pdf_path}[#{index}]" convert << output_path end rescue StandardError => e Rails.logger.error "Failed to create original image for page #{index + 1} from #{pdf_path}: #{e.message}" raise end def create_reduced_image(input_path, output_path) MiniMagick::Tool::Convert.new do |convert| convert << input_path convert << "-quality" convert << @options[:quality].to_s convert.strip convert << "-resize" convert << "#{@options[:resize_percentage]}%" convert << output_path end rescue StandardError => e Rails.logger.error "Failed to create reduced image from #{input_path}: #{e.message}" raise end def attach_processed_image(image_path, index) return unless File.exist?(image_path) filename = generate_filename(index) image_blob = create_blob(image_path, filename) @resource.pdf_snapshots.attach(image_blob) end def generate_filename(index) base_name = @resource.name.strip.downcase.gsub(/\s/, "_") "#{base_name}_page-#{index + 1}.#{@options[:format]}" end def create_blob(path, filename) ActiveStorage::Blob.create_and_upload!( io: File.open(path), filename: filename, content_type: "image/#{@options[:format]}" ) end def cleanup_temp_files(temp_files) return if temp_files.nil? # Safeguard in case temp_files is nil temp_files.values.each do |path| File.delete(path) if File.exist?(path) # Safely delete files end end end