prawnpdf/prawn

View on GitHub
lib/prawn/images/jpg.rb

Summary

Maintainability
A
0 mins
Test Coverage
# encoding: ASCII-8BIT
# frozen_string_literal: true

require 'stringio'

module Prawn
  module Images # rubocop: disable Style/Documentation
    # A convenience class that wraps the logic for extracting the parts of a JPG
    # image that we need to embed them in a PDF.
    class JPG < Image
      # Signals an issue with the image format. The image is probably corrupted
      # if you're getting this.
      class FormatError < StandardError; end

      # @group Extension API

      # Image width in pixels.
      # @return [Integer]
      attr_reader :width

      # Image height in pixels.
      # @return [Integer]
      attr_reader :height

      # Sample Precision in bits.
      # @return [Integer]
      attr_reader :bits

      # Number of image components (channels).
      # @return [Integer]
      attr_reader :channels

      # Scaled width of the image in PDF points.
      # @return [Number]
      attr_accessor :scaled_width

      # Scaled height of the image in PDF points.
      # @return [Number]
      attr_accessor :scaled_height

      # @private
      JPEG_SOF_BLOCKS = [
        0xC0, 0xC1, 0xC2, 0xC3, 0xC5, 0xC6, 0xC7, 0xC9, 0xCA, 0xCB, 0xCD, 0xCE,
        0xCF,
      ].freeze

      # Can this image handler process this image?
      #
      # @param image_blob [String]
      # @return [Boolean]
      def self.can_render?(image_blob)
        image_blob[0, 3].unpack('C*') == [255, 216, 255]
      end

      # Process a new JPG image.
      #
      # @param data [String] A binary string of JPEG data.
      def initialize(data)
        super()
        @data = data
        d = StringIO.new(@data)
        d.binmode

        c_marker = 0xff # Section marker.
        d.seek(2) # Skip the first two bytes of JPEG identifier.
        loop do
          marker, code, length = d.read(4).unpack('CCn')
          raise FormatError, 'JPEG marker not found!' if marker != c_marker

          if JPEG_SOF_BLOCKS.include?(code)
            @bits, @height, @width, @channels = d.read(6).unpack('CnnC')
            break
          end

          d.seek(length - 2, IO::SEEK_CUR)
        end
      end

      # Build a PDF object representing this image in `document`, and return
      # a Reference to it.
      #
      # @param document [Prawn::Document]
      # @return [PDF::Core::Reference]
      def build_pdf_object(document)
        color_space =
          case channels
          when 1
            :DeviceGray
          when 3
            :DeviceRGB
          when 4
            :DeviceCMYK
          else
            raise ArgumentError, 'JPG uses an unsupported number of channels'
          end

        obj = document.ref!(
          Type: :XObject,
          Subtype: :Image,
          ColorSpace: color_space,
          BitsPerComponent: bits,
          Width: width,
          Height: height,
        )

        # add extra decode params for CMYK images. By swapping the
        # min and max values from the default, we invert the colours. See
        # section 4.8.4 of the spec.
        if color_space == :DeviceCMYK
          obj.data[:Decode] = [1.0, 0.0, 1.0, 0.0, 1.0, 0.0, 1.0, 0.0]
        end

        obj.stream << @data
        obj.stream.filters << :DCTDecode
        obj
      end
    end

    Prawn.image_handler.register(Prawn::Images::JPG)
  end
end