diff --git a/Gemfile b/Gemfile index 9a208f1a..3ff63837 100644 --- a/Gemfile +++ b/Gemfile @@ -40,3 +40,5 @@ group :development do end gem "bridgetown-lit-renderer", "= 2.1.0.beta2" + +gem "ruby-vips", "~> 2.2" diff --git a/Gemfile.lock b/Gemfile.lock index ad0446e6..4ef61ff5 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -81,7 +81,7 @@ GEM i18n (1.14.1) concurrent-ruby (~> 1.0) jaro_winkler (1.5.6) - json (2.6.3) + json (2.7.1) kramdown (2.4.0) rexml kramdown-parser-gfm (1.1.0) @@ -95,13 +95,14 @@ GEM minitest (5.20.0) mutex_m (0.1.2) nio4r (2.5.8) - nokogiri (1.15.4) + nokogiri (1.16.0) mini_portile2 (~> 2.8.2) racc (~> 1.4) - parallel (1.23.0) - parser (3.2.2.4) + parallel (1.24.0) + parser (3.3.0.3) ast (~> 2.4.1) racc + prism (0.24.0) public_suffix (5.0.3) puma (5.6.2) nio4r (~> 2.0) @@ -115,14 +116,14 @@ GEM ffi (~> 1.0) rbs (2.8.4) redcarpet (3.6.0) - regexp_parser (2.8.2) + regexp_parser (2.9.0) reverse_markdown (2.1.1) nokogiri rexml (3.2.6) roda (3.73.0) rack rouge (3.30.0) - rubocop (1.57.2) + rubocop (1.59.0) json (~> 2.3) language_server-protocol (>= 3.17.0) parallel (~> 1.10) @@ -130,7 +131,7 @@ GEM rainbow (>= 2.2.2, < 4.0) regexp_parser (>= 1.8, < 3.0) rexml (>= 3.2.5, < 4.0) - rubocop-ast (>= 1.28.1, < 2.0) + rubocop-ast (>= 1.30.0, < 2.0) ruby-progressbar (~> 1.7) unicode-display_width (>= 2.4.0, < 3.0) rubocop-ast (1.30.0) @@ -140,12 +141,14 @@ GEM prism (>= 0.22.0, < 0.25) sorbet-runtime (>= 0.5.10782) ruby-progressbar (1.13.0) + ruby-vips (2.2.1) + ffi (~> 1.12) ruby2_keywords (0.0.5) serbea (1.0.1) activesupport (>= 6.0) erubi (>= 1.10) tilt (~> 2.0) - solargraph (0.49.0) + solargraph (0.50.0) backport (~> 1.2) benchmark bundler (~> 2.0) @@ -161,8 +164,8 @@ GEM thor (~> 1.0) tilt (~> 2.0) yard (~> 0.9, >= 0.9.24) - thor (1.2.2) sorbet-runtime (0.5.11351) + thor (1.3.0) tilt (2.3.0) tzinfo (2.0.6) concurrent-ruby (~> 1.0) @@ -182,6 +185,7 @@ DEPENDENCIES puma (~> 5.6) redcarpet (~> 3.5) ruby-lsp (~> 0.14.6) + ruby-vips (~> 2.2) solargraph BUNDLED WITH diff --git a/plugins/utils/opengraph/image.rb b/plugins/utils/opengraph/image.rb new file mode 100644 index 00000000..f127e305 --- /dev/null +++ b/plugins/utils/opengraph/image.rb @@ -0,0 +1,73 @@ +require 'vips' + +module Utils + module Opengraph + class Image + attr_reader :elements, :canvas, :borders, :canvas_color + + def initialize(width, height, color: '#ffffff') + @elements = [] + @canvas_color = color + @canvas = Vips::Image.black(width, height).ifthenelse([0, 0, 0], hex_to_rgb(color)) + @borders = {} + end + + def image(source, resize_with_ratio:) + im = source ? Vips::Image.new_from_buffer(source, '') : Vips::Image.black(1, 1).ifthenelse([0, 0, 0], hex_to_rgb(canvas_color)) + if resize_with_ratio && source + width, height = resize_with_ratio + ratio = get_ratio(im, width, height, :min) + im = im.resize(ratio) + end + @elements << im + self + end + + def text(message, width:, height:, dpi: 300, color: '#2f363d', font: 'Open Sans Bold') + im = Vips::Image.text(message, width: width, height: height, dpi: dpi, font: font) + im = im.new_from_image(hex_to_rgb(color)).copy(interpretation: :srgb).bandjoin(im) + @elements << im + + self + end + + def border_bottom(height, color: '#000000') + im = Vips::Image.black(canvas.width, height).ifthenelse([0, 0, 0], hex_to_rgb(color)) + borders[:bottom] = im + + self + end + + def compose(filename) + result = yield(canvas, *elements) + @canvas = @canvas.composite( + elements + [borders[:bottom]], :over, + x: result[:x] + (borders[:bottom] ? [0] : []), + y: result[:y] + (borders[:bottom] ? [canvas.height - borders[:bottom].height] : []) + ) + + @canvas.write_to_file(filename) + end + + private + + def hex_to_rgb(input) + case input + when String + input.match(/#(..)(..)(..)/)[1..3].map(&:hex) + when Array + input + else + raise ArgumentError, "Unknown input #{input.inspect}" + end + end + + # https://github.com/dariocravero/vips-process/blob/master/lib/vips-process/resize.rb#L109 + def get_ratio(image, width, height, min_or_max = :min) + width_ratio = width.to_f / image.width + height_ratio = height.to_f / image.height + [width_ratio, height_ratio].send min_or_max + end + end + end +end