
Today I created a flyer to send to Ruby on Rails Kaigi about my library stack. I strongly recommend keeping an eye on that conference.
After fighting with QR code generators, I noticed that sometimes the QR redirects through other websites. It’s frustrating because I can’t share a QR code with insecure content.
I found the gem rqrcode “A Ruby library that encodes QR Codes”.
After that, I thought I could generate the QR code using Ruby. Since I have ruby-libgd, I can offer the possibility to create QR codes entirely in Ruby!

Then I put my hands to work and wanted to share the result:
file: qr_code_generator.rb
require 'gd'require 'rqrcode'class QRCodeGenerator DEFAULT_OPTIONS = { module_size: 10, border: 4, fg_color: [0, 0, 0], bg_color: [255, 255, 255], error_correction: :m, logo: nil, logo_size: nil, rounded_modules: false, gradient: false, gradient_direction: :vertical, antialias: true, alpha_blending: false, save_alpha: true, format: :png }.freeze def initialize(data, options = {}) @data = data @options = DEFAULT_OPTIONS.merge(options) validate_options end def generate qr_matrix = generate_qr_matrix qr_size = qr_matrix.length module_size = @options[:module_size] border_px = @options[:border] * module_size total_size = (qr_size * module_size) + (border_px * 2) img = GD::Image.new(total_size, total_size) if @options[:alpha_blending] img.alpha_blending = true img.save_alpha = @options[:save_alpha] end if @options[:gradient] draw_gradient_background(img, total_size) else draw_solid_background(img, total_size) end img.antialias = @options[:antialias] if @options[:antialias] draw_qr_modules(img, qr_matrix, module_size, border_px) add_logo_to_qr(img, total_size) if @options[:logo] img end def generate_qr_matrix qr = RQRCode::QRCode.new(@data, error_correction_level: @options[:error_correction]) qr.modules end def draw_qr_modules(img, qr_matrix, module_size, border_px) fg_color = @options[:fg_color] qr_matrix.each_with_index do |row, y| row.each_with_index do |module_on, x| next unless module_on x1 = border_px + (x * module_size) y1 = border_px + (y * module_size) x2 = x1 + module_size - 1 y2 = y1 + module_size - 1 if @options[:rounded_modules] draw_rounded_module(img, x1, y1, x2, y2, fg_color, module_size) else img.filled_rectangle(x1, y1, x2, y2, fg_color) end end end end def draw_rounded_module(img, x1, y1, x2, y2, color, module_size) # Create a temporary image for the rounded module temp = GD::Image.new(module_size, module_size) temp.alpha_blending = false temp.save_alpha = true transparent = [0, 0, 0, 127] temp.fill(transparent) radius = module_size / 4 temp.filled_rectangle( radius, 0, module_size - radius - 1, module_size - 1, color ) temp.filled_rectangle( 0, radius, module_size - 1, module_size - radius - 1, color ) img.copy(temp, x1, y1, 0, 0, module_size, module_size) end def draw_solid_background(img, size) bg_color = @options[:bg_color] temp = GD::Image.new(size, size) temp.fill(bg_color) img.copy(temp, 0, 0, 0, 0, size, size) end def draw_gradient_background(img, size) bg = @options[:bg_color] darker = [ (bg[0] * 0.92).to_i, (bg[1] * 0.92).to_i, (bg[2] * 0.92).to_i ] case @options[:gradient_direction] when :vertical draw_vertical_gradient(img, size, bg, darker) when :horizontal draw_horizontal_gradient(img, size, bg, darker) when :radial draw_radial_gradient(img, size, bg, darker) else draw_vertical_gradient(img, size, bg, darker) end end def draw_vertical_gradient(img, size, start_color, end_color) size.times do |y| ratio = y.to_f / size r = (start_color[0] + (end_color[0] - start_color[0]) * ratio).to_i g = (start_color[1] + (end_color[1] - start_color[1]) * ratio).to_i b = (start_color[2] + (end_color[2] - start_color[2]) * ratio).to_i img.line(0, y, size - 1, y, [r, g, b]) end end def draw_horizontal_gradient(img, size, start_color, end_color) size.times do |x| ratio = x.to_f / size r = (start_color[0] + (end_color[0] - start_color[0]) * ratio).to_i g = (start_color[1] + (end_color[1] - start_color[1]) * ratio).to_i b = (start_color[2] + (end_color[2] - start_color[2]) * ratio).to_i img.line(x, 0, x, size - 1, [r, g, b]) end end def draw_radial_gradient(img, size, start_color, end_color) center_x = size / 2 center_y = size / 2 max_distance = Math.sqrt((center_x ** 2) + (center_y ** 2)) size.times do |y| size.times do |x| distance = Math.sqrt((x - center_x) ** 2 + (y - center_y) ** 2) ratio = [distance / max_distance, 1.0].min r = (start_color[0] + (end_color[0] - start_color[0]) * ratio).to_i g = (start_color[1] + (end_color[1] - start_color[1]) * ratio).to_i b = (start_color[2] + (end_color[2] - start_color[2]) * ratio).to_i img.set_pixel(x, y, [r, g, b]) end end end def add_logo_to_qr(img, qr_size) img.alpha_blending = false img.save_alpha = true logo_path = @options[:logo] return unless File.exist?(logo_path) logo_img = GD::Image.open(logo_path) logo_img.alpha_blending = false logo_img.save_alpha = true logo_size = @options[:logo_size] || (qr_size / 5) resized_logo = GD::Image.new(logo_size, logo_size) resized_logo.copy_resize( logo_img, # source 0, 0, # dst_x, dst_y 0, 0, # src_x, src_y logo_img.width, # src_w logo_img.height, # src_h logo_size, # dst_w logo_size, # dst_h true # resample (high quality) ) x_offset = (qr_size - logo_size) / 2 y_offset = (qr_size - logo_size) / 2 img.copy( resized_logo, x_offset, y_offset, # dst_x, dst_y 0, 0, # src_x, src_y logo_size, # w logo_size # h ) end def duplicate img = generate img.dup end def save(filename) img = generate case @options[:format] when :png, :jpeg, :gif, :webp img.save(filename) else raise "Unsupported format: #{@options[:format]}" end end def to_image generate end def to_png_bytes img = generate require 'tempfile' temp = Tempfile.new(['qr', '.png']) img.save_png(temp.path) File.read(temp.path) ensure temp.unlink if temp end private def validate_options valid_formats = [:png, :jpeg, :gif, :webp] raise "Invalid format: #{@options[:format]}" unless valid_formats.include?(@options[:format]) valid_ec = [:l, :m, :h, :x] raise "Invalid error_correction: #{@options[:error_correction]}" unless valid_ec.include?(@options[:error_correction]) raise "module_size must be > 0" if @options[:module_size] <= 0 endend
Create QR codes in PNG, JPEG, WebP, or GIF using only Ruby. You can also use this in Ruby on Rails.
require_relative './qr_code_generator.rb'# Basic with antialias enabledqr = QRCodeGenerator.new("https://map-view-demo.up.railway.app", { antialias: true})qr.save("qr_antialias.png")# Rounded modules with alpha blendingqr = QRCodeGenerator.new("https://github.com/ggerman/ruby-libgd", { fg_color: [0, 232, 198], bg_color: [6, 10, 15], rounded_modules: true, alpha_blending: true, save_alpha: true, antialias: true})qr.save("qr_rounded_alpha.png")# Vertical gradient backgroundqr = QRCodeGenerator.new("https://rubystacknews.com", { gradient: true, gradient_direction: :vertical, fg_color: [0, 0, 0], bg_color: [240, 248, 255]})qr.save("qr_gradient_vertical.png")# Horizontal gradientqr = QRCodeGenerator.new("https://github.com/ggerman/libgd-gis", { gradient: true, gradient_direction: :horizontal, fg_color: [220, 20, 60], bg_color: [255, 255, 255], antialias: true})qr.save("qr_gradient_horizontal.png")# Radial gradient (advanced)qr = QRCodeGenerator.new("https://map-view-demo.up.railway.app", { gradient: true, gradient_direction: :radial, fg_color: [34, 139, 34], bg_color: [255, 255, 255], antialias: true})qr.save("qr_gradient_radial.png")# High-quality logo with copy_resizeqr = QRCodeGenerator.new("https://map-view-demo.up.railway.app", { logo: "logo.png", logo_size: 100, error_correction: :h, antialias: true, alpha_blending: true})qr.save("qr_logo_hires.png")original = QRCodeGenerator.new("https://example.com", { fg_color: [0, 232, 198], bg_color: [6, 10, 15]})original_img = original.to_imageduplicated = original_img.dupdef create_branded_qr(url, brand_name) brands = { ruby_libgd: { fg: [220, 20, 60], bg: [255, 255, 255], gradient: false }, libgd_gis: { fg: [34, 139, 34], bg: [255, 255, 255], gradient: false }, mapview: { fg: [0, 232, 198], bg: [6, 10, 15], gradient: true, gradient_direction: :radial } } config = brands[brand_name.to_sym] raise "Unknown brand: #{brand_name}" unless config qr = QRCodeGenerator.new(url, { fg_color: config[:fg], bg_color: config[:bg], gradient: config[:gradient], gradient_direction: config[:gradient_direction] || :vertical, module_size: 12, antialias: true, alpha_blending: config[:gradient] }) qr.to_imageend# Create branded QRslibgd_qr = create_branded_qr("https://github.com/ggerman/ruby-libgd", :ruby_libgd)gis_qr = create_branded_qr("https://github.com/ggerman/libgd-gis", :libgd_gis)mapview_qr = create_branded_qr("https://map-view-demo.up.railway.app", :mapview)# Get PNG bytes for HTTP streaming (Rails)class QRController < ApplicationController def show qr = QRCodeGenerator.new(params[:data], { fg_color: [0, 232, 198], bg_color: [6, 10, 15], antialias: true }) send_data qr.to_png_bytes, type: 'image/png', disposition: 'inline', filename: "qr_#{params[:data].hash}.png" endendultimate_qr = QRCodeGenerator.new( "https://map-view-demo.up.railway.app/contact", { fg_color: [0, 232, 198], bg_color: [6, 10, 15], module_size: 15, border: 4, error_correction: :h, logo: "map_view.png", logo_size: 120, rounded_modules: true, gradient: true, gradient_direction: :radial, antialias: true, alpha_blending: true, save_alpha: true, format: :png })ultimate_qr.save("qr_ultimate.png")
Power By: https://github.com/ggerman/ruby-libgd
🧾 Conclusion
Building my own QR generator wasn’t really about QR codes, it was about control, security, and independence.
Instead of relying on third-party services (with hidden redirects and unknown behavior), I now have a fully transparent pipeline, built entirely in Ruby, that I can trust and customize.
Sometimes the best solution isn’t another API, it’s owning the problem end to end.
