首页 > 技术文章 > 记一次用rmagick绘制图像

lwh-note 2020-08-18 17:01 原文

最近做了个需求,大概要求是将几个元素(分享人头像,小程序码,商户名称,商户头像,背景图片),根据页面配置的形式绘制出分享图片,大概效果如下:

 

 

为此引用rmagick的gem包,其中所需gem包有

gem 'rmagick'
gem 'mini_magick'

 

写了个图片拼接工具类,其中代码中用到的 ContentAgent::Content 对象是我们公司自己封装的一个内容中心服务器,其主要用于图片的上传,下载

# encoding: utf-8
# magic image 合并图片工具
class MagicImageTools
  class << self
    #
    # == 根据configs合并为一个图片
    #
    # @param [Array<Hash>] configs 配置
    #
    def composite_by_configs(configs)
      configs = configs.deep_symbolize_keys
      container = configs.shift
      container_magic_img = container[:type] == 'blank' ?
        blank_container_magic_image(container[:options]) : prepare_image_for_composite(container)

      # 子项
      configs.each do |config|
        next if config[:value].blank?
        log_info "现在的配置是 #{config}"
        case config[:type]
          when 'image'
            magic_img = prepare_image_for_composite(config)
            composite_images(container_magic_img, magic_img, config[:options])
          when 'text'
            add_text(container_magic_img, config[:value], config[:options])
          else
            log_info "composite_by_configs 子项的type#{config[:type]}不认识"
        end
      end
      cid = to_normal_image(container_magic_img)
      log_info "composite_by_configs cid: #{cid}"

      cid
    end

    def prepare_image_for_composite(config)
      options = config[:options] || {}
      case options[:image_type]
        when 'magic_image'
          magic_img = config[:value]
        when 'path'
          img = ContentAgent::Content.create_by_file(config[:value]).urid
        else
          img = config[:value]
      end
      magic_img = to_magic_image(img) unless magic_img.present?
      if !options[:skip_resize]
        options[:force_resize] ?
            force_resize_magic_image(magic_img, options[:width], options[:height]) : resize_magic_image(magic_img, options)
      end

      magic_img
    end

    #
    # == 合并图片, 在容器图container_img中添加img
    #
    # @param container_img 容器图
    # @param img 图片
    #
    def composite_images(container_img, img, options = {})
      options.symbolize_keys!
      x         = options[:x] || 0
      y         = options[:y] || 0
      diameter  = options[:width] || 103
      radius    = diameter / 2

      # 此处可以传是否需要对图片做 方 转 圆型
      if options[:radius]
        pr = Magick::Draw.new
        pr.define_clip_path('circle') {
          pr.circle radius, radius, radius, 0
        }
        pr.push
        pr.clip_path('circle')
        pr.composite(0, 0, diameter, diameter, img)
        pr.pop
        # 这一步是画个跟目标图片同样大小对透明图片,这样就可以将裁剪后多余部分隐掉
        canvas = Magick::Image.new(diameter, diameter) { |c| c.background_color = "Transparent" }
        pr.draw(canvas)
        # 以容器图片的左上角作为x, y轴的起点
        container_img.composite!(canvas, x, y, Magick::OverCompositeOp)
      else
        container_img.composite!(img, x, y, Magick::OverCompositeOp)
      end
    end

    # 在container_img中添加文字, 默认以容器图片的左上角作为x, y轴的起点
    def add_text(container_img, text, options = {})
      options.symbolize_keys!
      width = options[:width] || 0
      height = options[:height] || 0
      x = options[:x] || 0
      y = options[:y] || 0
      # Magick::NorthWestGravity 表示图片的西北点(即左上角)作为x, y轴的起点
      # Magick::CenterGravity 表示图片的中心点作为x, y轴的起点
      gravity = options[:gravity] || Magick::NorthWestGravity
      font_size = options[:font_size] || 16
      color = options[:color] || 'black'
      font = text_font
      copyright = Magick::Draw.new
      copyright.annotate(container_img, width, height, x, y, text) do
        # 每次写字之前设置stroke为'transparent', 防止之前设置stroke为其他值
        self.stroke = 'transparent'
        self.gravity = gravity
        self.pointsize = font_size
        self.fill = color
        self.font = font
      end
    end

    # 将 图片content_id 转为 Magick::Image类型图片
    def to_magic_image(url)
      path = Tools.download_img(url)
      magic_img = Magick::Image.read(path).first
      File.delete(path) if File.exist?(path)

      magic_img
    end

    # 将 Magick::Image类型图片 转为 图片content_id
    def to_normal_image(magic_img)
      path = "tmp/magic_img_#{Tools.fill_code}.png"
      magic_img.write(path)
      img = ContentAgent::Content.create_by_file(path)
      File.delete(path) if File.exist?(path)

      img.urid
    end

    # 等比例转换图片尺寸
    def resize_magic_image(magic_img, options = {})
      options.symbolize_keys!
      max_width = options[:max_width] || 100
      max_height = options[:max_height] || 100
      img_width, img_height = get_image_size(magic_img)
      if img_width*max_height > img_height*max_width
        width, height = [max_width, max_width*img_height/img_width]
      else
        width, height = [max_height*img_width/img_height, max_height]
      end

      force_resize_magic_image(magic_img, width, height) # 将图片的尺寸转换成为width*height
    end

    # 强制转换图片尺寸为width, height
    def force_resize_magic_image(magic_img, width, height)
      magic_img.resize!(width, height) # 将图片的尺寸转换成为width*height
    end

    # 获取图片的width, height
    def get_image_size(magic_img)
      width = magic_img.columns
      height = magic_img.rows
      [width, height]
    end

    # 创建空白的背景容器图片
    def blank_container_magic_image(options = {})
      options.symbolize_keys!
      width = options[:width] || 375
      height = options[:height] || 667
      bg_color = options[:bg_color] || 'white'
      bg_magic_color = Magick::HatchFill.new(bg_color)
      Magick::Image.new(width, height, bg_magic_color)
    end

    # '暂无图片'
    def no_image_magic_image
      Magick::Image.read('app/assets/images/common/common-no-image.png').first
    end

    # 字体
    def text_font
      'app/assets/fonts/simsun.ttc'
    end
  end
end

 

工具使用侧

# 1.导购头像
  # 2.小程序码
  # 3.背景图
  # 4.商户名称
  def generate_shard_img(task, guider)

    bg_img_width              = 750
    bg_img_height             = 1334
    width_size                = 375
    qrcode_size               = 105
    avatar_size               = 50
    font_size                 = 24
    retailer_name_width       = 130
    retailer_name_height      = 200

    img_location              = task.img_location.as_smart
    guide_photo_x             = img_location[:guide_photo_x]   || 318
    guide_photo_y             = img_location[:guide_photo_y]   || 1116
    weapp_x                   = img_location[:weapp_x]         || 532
    weapp_y                   = img_location[:weapp_y]         || 1080
    logo_x                    = img_location[:logo_x]          || 188
    logo_y                    = img_location[:logo_y]          || 22
    retailer_name_x           = img_location[:retailer_name_x] || 390
    retailer_name_y           = img_location[:retailer_name_y] || 20

    bg_img                    = get_bg_img(task, bg_img_width, bg_img_height)
    weapp_url                 = get_weapp_url(task, guider)
    avatar                    = get_avatar(guider, avatar_size)
    retailer_logo             = get_retailer_log
    retailer_name             = get_retailer_name
    configs = [
      {
        value: bg_img,
        type: "image",
        flag: "背景图片",
        options: {
          image_type: "magic_image",
          width: width_size,
          height: 620,
          skip_resize: true
        }
      },
      {
        value: weapp_url,
        type: "image",
        flag: "小程序码",
        options: {
          image_type: "cid",
          force_resize: true,
          radius: true,
          width: qrcode_size,
          height: qrcode_size,
          x: weapp_x,
          y: weapp_y
        }
      },
      {
        value: retailer_logo,
        flag: "商户logo",
        type: "image",
        options: {
          image_type: "cid",
          force_resize: true,
          width: 130,
          height: 130,
          x: logo_x,
          y: logo_y,
          gravity: Magick::CenterGravity
        }
      },
      # 商品名称只显示前10个字
      {
        value: (retailer_name.length > 10 ? (retailer_name[0..-4] + '...') : retailer_name),
        flag: "商户名称",
        type: "text",
        options: {
          font_size: font_size,
          skip_resize: true,
          width: retailer_name_width,
          height: retailer_name_height,
          x: retailer_name_x,
          y: retailer_name_y,
          gravity: Magick::CenterGravity
        }
      },
      {
        value: avatar,
        flag: "导购头像",
        type: "image",
        options: {
          image_type: "cid",
          force_resize: true,
          radius: true,
          width: avatar_size,
          height: avatar_size,
          x: guide_photo_x,
          y: guide_photo_y
        }
      }
    ]
    MagicImageTools.composite_by_configs(configs)
  end

 

 

推荐阅读