Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add SVG options to render using paths. #108

Merged
merged 9 commits into from May 6, 2021
3 changes: 3 additions & 0 deletions README.md
Expand Up @@ -92,6 +92,9 @@ shape_rendering - SVG Attribute: auto | optimizeSpeed | crispEdges | geometricPr
(defaults crispEdges)
standalone - whether to make this a full SVG file, or only an svg to embed in other svg
(default true)
use_path - Use <path> to render SVG rather than <rect> to significantly reduce size
and quality. This will become the default in future versions.
(default false)
```
Example
```ruby
Expand Down
166 changes: 142 additions & 24 deletions lib/rqrcode/export/svg.rb
@@ -1,23 +1,151 @@
# frozen_string_literal: true

# This class creates a SVG files.
# Code from: https://github.com/samvincent/rqrcode-rails3
# Initial code from: https://github.com/samvincent/rqrcode-rails3
module RQRCode
module Export
module SVG
class BaseOutputSVG
attr_reader :result

def initialize(qrcode)
@qrcode = qrcode
@result = []
end
end

class Path < BaseOutputSVG
def build(module_size, offset, color)
modules_array = @qrcode.modules
matrix_width = matrix_height = modules_array.length + 1
empty_row = [Array.new(matrix_width - 1, false)]
edge_matrix = Array.new(matrix_height) { Array.new(matrix_width) }

(empty_row + modules_array + empty_row).each_cons(2).with_index do |row_pair, row_index|
first_row, second_row = row_pair

# horizontal edges
first_row.zip(second_row).each_with_index do |cell_pair, column_index|
edge = case cell_pair
when [true, false] then Edge.new column_index + 1, row_index, :left
when [false, true] then Edge.new column_index, row_index, :right
end

(edge_matrix[edge.start_y][edge.start_x] ||= []) << edge if edge
end

# vertical edges
([false] + second_row + [false]).each_cons(2).each_with_index do |cell_pair, column_index|
edge = case cell_pair
when [true, false] then Edge.new column_index, row_index, :down
when [false, true] then Edge.new column_index, row_index + 1, :up
end

(edge_matrix[edge.start_y][edge.start_x] ||= []) << edge if edge
end
end

edge_count = edge_matrix.flatten.compact.count
path = []

while edge_count > 0
edge_loop = []
next_matrix_cell = edge_matrix.find(&:any?).find { |cell| cell&.any? }
edge = next_matrix_cell.first

while edge
edge_loop << edge
matrix_cell = edge_matrix[edge.start_y][edge.start_x]
matrix_cell.delete edge
edge_matrix[edge.start_y][edge.start_x] = nil if matrix_cell.empty?
edge_count -= 1

# try to find an edge continuing the current edge
edge = edge_matrix[edge.end_y][edge.end_x]&.first
end

first_edge = edge_loop.first
edge_loop_string = SVG_PATH_COMMANDS[:move]
edge_loop_string += "#{first_edge.start_x} #{first_edge.start_y}"

edge_loop.chunk(&:direction).to_a[0...-1].each do |direction, edges|
edge_loop_string << "#{SVG_PATH_COMMANDS[direction]}#{edges.length}"
end
edge_loop_string << SVG_PATH_COMMANDS[:close]

path << edge_loop_string
end

@result << %{<path d="#{path.join}" style="fill:##{color}" transform="translate(#{offset},#{offset}) scale(#{module_size})"/>}
end
end

class Rect < BaseOutputSVG
def build(module_size, offset, color)
@qrcode.modules.each_index do |c|
tmp = []
@qrcode.modules.each_index do |r|
y = c * module_size + offset
x = r * module_size + offset

next unless @qrcode.checked?(c, r)
tmp << %(<rect width="#{module_size}" height="#{module_size}" x="#{x}" y="#{y}" style="fill:##{color}"/>)
end

@result << tmp.join
end
end
end

class Edge < Struct.new(:start_x, :start_y, :direction)
def end_x
case direction
when :right then start_x + 1
when :left then start_x - 1
else start_x
end
end

def end_y
case direction
when :down then start_y + 1
when :up then start_y - 1
else start_y
end
end
end

SVG_PATH_COMMANDS = {
move: "M",
up: "v-",
down: "v",
left: "h-",
right: "h",
close: "z"
}

#
# Render the SVG from the Qrcode.
#
# Options:
# offset - Padding around the QR Code (e.g. 10)
# fill - Background color (e.g "ffffff" or :white)
# color - Foreground color for the code (e.g. "000000" or :black)
# module_size - The Pixel size of each module (e.g. 11)
# shape_rendering - Defaults to crispEdges
# standalone - wether to make this a full SVG file, or only svg to embed
# in other svg.
# offset - Padding around the QR Code in pixels
# (default 0)
# fill - Background color e.g "ffffff"
# (default none)
# color - Foreground color e.g "000"
# (default "000")
# module_size - The Pixel size of each module
# (defaults 11)
# shape_rendering - SVG Attribute: auto | optimizeSpeed | crispEdges | geometricPrecision
# (defaults crispEdges)
# standalone - Whether to make this a full SVG file, or only an svg to embed in other svg
# (default true)
# use_path - Use <path> to render SVG rather than <rect> to significantly reduce size
# and quality. This will become the default in future versions.
# (default false)
#
def as_svg(options = {})
use_path = options[:use_path]
offset = options[:offset].to_i || 0
color = options[:color] || "000"
shape_rendering = options[:shape_rendering] || "crispEdges"
Expand All @@ -31,29 +159,19 @@ def as_svg(options = {})
open_tag = %(<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xmlns:ev="http://www.w3.org/2001/xml-events" width="#{dimension}" height="#{dimension}" shape-rendering="#{shape_rendering}">)
close_tag = "</svg>"

result = []
@qrcode.modules.each_index do |c|
tmp = []
@qrcode.modules.each_index do |r|
y = c * module_size + offset
x = r * module_size + offset

next unless @qrcode.checked?(c, r)
tmp << %(<rect width="#{module_size}" height="#{module_size}" x="#{x}" y="#{y}" style="fill:##{color}"/>)
end
result << tmp.join
end
output_tag = (use_path ? Path : Rect).new(@qrcode)
output_tag.build(module_size, offset, color)

if options[:fill]
result.unshift %(<rect width="#{dimension}" height="#{dimension}" x="0" y="0" style="fill:##{options[:fill]}"/>)
output_tag.result.unshift %(<rect width="#{dimension}" height="#{dimension}" x="0" y="0" style="fill:##{options[:fill]}"/>)
end

if standalone
result.unshift(xml_tag, open_tag)
result << close_tag
output_tag.result.unshift(xml_tag, open_tag)
output_tag.result << close_tag
end

result.join("\n")
output_tag.result.join
end
end
end
Expand Down