CrystalVips - Crystal bindings for the libvips image processing library

CrystalVips Provides Crystal language interface to the libvips image processing library. Programs that use CrystalVips don’t manipulate images directly, instead they create pipelines of image processing operations starting from a source image. When the pipe is connected to a destination, the whole pipeline executes at once and in parallel, streaming the image from source to destination in a set of small fragments.

Because CrystalVips is parallel, its’ quick, and because it doesn’t need to keep entire images in memory, its light.

27 Likes

Wow! Congratulations on this one!

1 Like

This is a fantastic contribution to the community, thank you! Folks drop by the chat frequently looking for imagemagick or graphicsmagick bindings, it’ll be nice to be able to point them here.

5 Likes

Exactly. Since libvips uses far less memory and is a lot faster than anything else, even more when comparing with imagemagick and graphicsmagick, there is no reason to not use CrystalVips to process images on Crystal.

5 Likes

Wow, I was using pixi (libmagick binding) and this is roughly 50% faster for my usecase of making thumbnmails.

4 Likes

If you look at the reduction in memory usage, you will be even more surprised :slight_smile:

2 Likes

This is a great library.
I asked ChatGPT to write code to draw the Mandelbrot set (for fun). Here we are first creating a pnm image and then converting it to a PNG. Can we create a PNG from the beginning?

require "complex"
require "option_parser"
require "vips"

# Default parameters
width = 800
height = 800
x_min = -2.0
x_max = 1.0
y_min = -1.5
y_max = 1.5
max_iter = 100
output_filename = "mandelbrot_custom_color.pnm"

# Option parser
OptionParser.parse do |parser|
  parser.banner = "Usage: mandelbrot [arguments]"
  parser.on("-w WIDTH", "--width=WIDTH", "Width of the image (default: #{width})") { |w| width = w.to_i }
  parser.on("-h HEIGHT", "--height=HEIGHT", "Height of the image (default: #{height})") { |h| height = h.to_i }
  parser.on("--xmin=VALUE", "Minimum x-coordinate value (default: #{x_min})") { |xmin| x_min = xmin.to_f }
  parser.on("--xmax=VALUE", "Maximum x-coordinate value (default: #{x_max})") { |xmax| x_max = xmax.to_f }
  parser.on("--ymin=VALUE", "Minimum y-coordinate value (default: #{y_min})") { |ymin| y_min = ymin.to_f }
  parser.on("--ymax=VALUE", "Maximum y-coordinate value (default: #{y_max})") { |ymax| y_max = ymax.to_f }
  parser.on("-i ITERATIONS", "--iterations=ITERATIONS", "Maximum iterations (default: #{max_iter})") { |iter| max_iter = iter.to_i }
  parser.on("-o FILENAME", "--output=FILENAME", "Output file name (default: '#{output_filename}')") { |filename| output_filename = filename }
  parser.on("--help", "Show this help") do
    puts parser
    exit
  end
  parser.invalid_option do |flag|
    STDERR.puts "ERROR: #{flag} is not a valid option."
    STDERR.puts parser
    exit(1)
  end
end

def mandelbrot_set(width, height, x_min, x_max, y_min, y_max, max_iter)
  dx = (x_max - x_min).to_f64 / width
  dy = (y_max - y_min).to_f64 / height

  Array.new(height) do |y|
    Array.new(width) do |x|
      c = Complex.new(x_min + x * dx, y_min + y * dy)
      z = Complex.new(0, 0)
      iter = 0

      while z.abs <= 2 && iter < max_iter
        z = z * z + c
        iter += 1
      end

      iter
    end
  end
end

def write_to_custom_color_pnm(image, max_iter)
  String.build do |pnm_data|
    pnm_data << "P3\n#{image[0].size} #{image.size}\n255\n"

    image.each do |row|
      row.each do |value|
        red = (255 * Math.sin(0.16 * value)).abs.to_i % 256
        green = (255 * Math.sin(0.11 * value + 2)).abs.to_i % 256
        blue = (255 * Math.sin(0.06 * value + 4)).abs.to_i % 256
        pnm_data << "#{red} #{green} #{blue} "
      end
      pnm_data << "\n"
    end
  end
end

def save_as_png(pnm_data, png_filename)
  image = Vips::Image.new_from_buffer(pnm_data, "")
  image.write_to_file(png_filename)
end

mandelbrot_image = mandelbrot_set(width, height, x_min, x_max, y_min, y_max, max_iter)

pnm_data = write_to_custom_color_pnm(mandelbrot_image, max_iter)

png_filename = output_filename.sub(/\.[^.]+\z/, ".png")
save_as_png(pnm_data, png_filename)
1 Like

You can operate on the Image and once done with transformation/operations, then save that to format of your choice.

below sample using your provided mandelbrot_set method to generate array of values, then creating image from that array. Image created such way will be BW, so let’s invoke LibVips::vips_falsecolour to perform some band transformation.


mandelbrot_image = mandelbrot_set(width, height, x_min, x_max, y_min, y_max, max_iter)

img = Vips::Image.new_from_array(mandelbrot_image)
img = (img == 255).ifthenelse(0, img + rand(20)).falsecolour
img.write_to_file(output_filename)

Disclaimer: I don’t claim to be an expert in libvips API, so there might be some other better options to achieve desired results.

HIH

2 Likes

Thanks. Can I use new_from_array to create a color image instead of grayscale?