lib/rqrcode/export/png.rb



# frozen_string_literal: true

require "chunky_png"

# This class creates PNG files.
module RQRCode
  module Export
    module PNG
      # Render the PNG from the QR Code.
      #
      # Options:
      # fill  - Background ChunkyPNG::Color, defaults to 'white'.
      # color - Foreground ChunkyPNG::Color, defaults to 'black'.
      #
      # When option :file is supplied you can use the following ChunkyPNG constraints
      # color_mode  - The color mode to use. Use one of the ChunkyPNG::COLOR_* constants.
      #               (defaults to 'ChunkyPNG::COLOR_GRAYSCALE')
      # bit_depth   - The bit depth to use. This option is only used for indexed images.
      #               (defaults to 1 bit)
      # interlace   - Whether to use interlacing (true or false).
      #               (defaults to ChunkyPNG default)
      # compression - The compression level for Zlib. This can be a value between 0 and 9, or a
      #               Zlib constant like Zlib::BEST_COMPRESSION
      #               (defaults to ChunkyPNG default)
      #
      # There are two sizing algorithms.
      #
      # - Original that can result in blurry and hard to scan images
      # - Google's Chart API inspired sizing that resizes the module size to fit within the given image size.
      #
      # The Googleis one will be used when no options are given or when the new size option is used.
      #
      # *Google*
      # size            - Total size of PNG in pixels. The module size is calculated so it fits.
      #                   (defaults to 120)
      # border_modules  - Width of white border around in modules.
      #                   (defaults to 4).
      #
      #  -- DONT USE border_modules OPTION UNLESS YOU KNOW ABOUT THE QUIET ZONE NEEDS OF QR CODES --
      #
      # *Original*
      # module_px_size  - Image size, in pixels.
      # border          - Border thickness, in pixels
      #
      # It first creates an image where 1px = 1 module, then resizes.
      # Defaults to 120x120 pixels, customizable by option.
      #
      def as_png(options = {})
        default_img_options = {
          bit_depth: 1,
          border_modules: 4,
          color_mode: ChunkyPNG::COLOR_GRAYSCALE,
          color: "black",
          file: false,
          fill: "white",
          module_px_size: 6,
          resize_exactly_to: false,
          resize_gte_to: false,
          size: 120
        }

        googleis = options.length == 0 || !options[:size].nil?
        options = default_img_options.merge(options) # reverse_merge
        fill = ChunkyPNG::Color(*(options[:fill].is_a?(Array) ? options[:fill] : [options[:fill]]))
        color = ChunkyPNG::Color(*(options[:color].is_a?(Array) ? options[:color] : [options[:color]]))
        output_file = options[:file]
        module_px_size = nil
        border_px = nil
        png = nil

        if googleis
          total_image_size = options[:size]
          border_modules = options[:border_modules]

          module_px_size = (total_image_size.to_f / (@qrcode.module_count + 2 * border_modules).to_f).floor.to_i

          img_size = module_px_size * @qrcode.module_count

          remaining = total_image_size - img_size
          border_px = (remaining / 2.0).floor.to_i

          png = ChunkyPNG::Image.new(total_image_size, total_image_size, fill)
        else
          border = options[:border_modules]
          total_border = border * 2
          module_px_size = if options[:resize_gte_to]
            (options[:resize_gte_to].to_f / (@qrcode.module_count + total_border).to_f).ceil.to_i
          else
            options[:module_px_size]
          end
          border_px = border * module_px_size
          total_border_px = border_px * 2
          resize_to = options[:resize_exactly_to]

          img_size = module_px_size * @qrcode.module_count
          total_img_size = img_size + total_border_px

          png = ChunkyPNG::Image.new(total_img_size, total_img_size, fill)
        end

        @qrcode.modules.each_index do |x|
          @qrcode.modules.each_index do |y|
            if @qrcode.checked?(x, y)
              (0...module_px_size).each do |i|
                (0...module_px_size).each do |j|
                  png[(y * module_px_size) + border_px + j, (x * module_px_size) + border_px + i] = color
                end
              end
            end
          end
        end

        if !googleis && resize_to
          png = png.resize(resize_to, resize_to)
        end

        if output_file
          constraints = {
            color_mode: options[:color_mode],
            bit_depth: options[:bit_depth]
          }
          constraints[:interlace] = options[:interlace] if options.has_key?(:interlace)
          constraints[:compression] = options[:compression] if options.has_key?(:compression)
          png.save(output_file, constraints)
        end

        png
      end
    end
  end
end

RQRCode::QRCode.send :include, RQRCode::Export::PNG