Skip to content

Commit

Permalink
Make render SVG path opt-in and add specs
Browse files Browse the repository at this point in the history
  • Loading branch information
whomwah committed Mar 25, 2021
1 parent 8ab1749 commit 2092a33
Show file tree
Hide file tree
Showing 3 changed files with 93 additions and 39 deletions.
110 changes: 73 additions & 37 deletions lib/rqrcode/export/svg.rb
Original file line number Diff line number Diff line change
Expand Up @@ -23,23 +23,40 @@ def end_y
end
end

DIRECTIONS = {
up: "v-",
down: "v",
left: "h-",
right: "h"
}

#
# 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 reduces size
# and quality. This will become the default in future versions.
# (default false)
#
def as_svg(options = {})
use_path = options[:use_path]
default_module_size = use_path ? 10 : 11
offset = options[:offset].to_i || 0
color = options[:color] || "000"
shape_rendering = options[:shape_rendering] || "crispEdges"
module_size = options[:module_size] || 11
module_size = options[:module_size] || default_module_size
standalone = options[:standalone].nil? ? true : options[:standalone]

# height and width dependent on offset and QR complexity
Expand All @@ -50,31 +67,67 @@ def as_svg(options = {})
close_tag = "</svg>"

result = []
modules_array = @qrcode.modules

if use_path
use_path(result, module_size, offset, color)
else
use_rect(result, module_size, offset, color)
end

if options[:fill]
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
end

result.join("\n")
end

private

def use_rect(str, 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
str << tmp.join
end
end

def use_path(str, module_size, offset, color)
modules_array = @qrcode.modules
matrix_height = modules_array.length + 1
matrix_width = modules_array.first.length + 1
edge_matrix = Array.new(matrix_height) { Array.new(matrix_width){ Array.new } }

false_row = [[false] * modules_array.first.length ]
edge_matrix = Array.new(matrix_height) { Array.new(matrix_width) { [] } }
false_row = [[false] * modules_array.first.length]

(false_row + modules_array + false_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|
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
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|
([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
edge_matrix[edge.start_y][edge.start_x] << edge if edge
end
end

Expand All @@ -90,13 +143,13 @@ def as_svg(options = {})
path = []
while edge_count > 0
edge_loop = []
matrix_cell = edge_matrix.find{|row| row.any?}.find{|cell| ! cell.nil? && ! cell.empty?}
matrix_cell = edge_matrix.find { |row| row.any? }.find { |cell| !cell.nil? && !cell.empty? }
edge = 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_matrix[edge.start_y][edge.start_x] = nil if matrix_cell.empty?
edge_count -= 1
# try to find an edge continuing the current edge
matrix_cell = edge_matrix[edge.end_y][edge.end_x]
Expand All @@ -107,31 +160,14 @@ def as_svg(options = {})
edge_loop_string = "M#{first_edge.start_x} #{first_edge.start_y}"

edge_loop.chunk(&:direction).to_a[0...-1].each do |direction, edges|
direction_string = case direction
when :up then "v-"
when :down then "v"
when :left then "h-"
when :right then "h"
end
edge_loop_string += "#{direction_string}#{edges.length}"
edge_loop_string += "#{DIRECTIONS[direction]}#{edges.length}"
end
edge_loop_string += "z"

path << edge_loop_string
end

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

if options[:fill]
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
end

result.join("\n")
str << %{<path d="#{path.join}" style="fill:##{color}" transform="translate(#{offset},#{offset}) scale(#{module_size})"/>}
end
end
end
Expand Down
7 changes: 7 additions & 0 deletions spec/rqrcode/data.rb
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,13 @@
</svg>
SVG

AS_SVG1 = <<~SVG.chomp
<?xml version="1.0" standalone="yes"?>
<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="330" height="330" shape-rendering="crispEdges">
<path d="M0 0h7v7h-7zM8 0h7v2h-1v1h1v1h-2v-2h-1v-1h-2v2h-2v-1h1v-1h-1zM17 0h3v2h1v5h-1v-2h-1v2h-1v-3h-2v-1h2v1h1v-2h-1v-1h-1zM22 0h7v7h-7zM1 1v5h5v-5zM23 1v5h5v-5zM2 2h3v3h-3zM11 2h1v1h-1zM24 2h3v3h-3zM8 4h1v1h-1zM10 4h1v1h1v1h-1v1h-1v-1h-1v-1h1zM13 5h1v1h-1zM15 5h2v2h-1v-1h-1zM8 6h1v1h1v1h2v-2h1v1h1v-1h1v1h-1v1h-2v1h-3v-1h-1zM15 7h1v1h1v1h-1v3h1v-1h1v-1h1v1h-1v2h-1v1h1v1h-2v-2h-1v-1h-1v1h-1v-2h1v-1h1v-1h-1v-1h1zM5 8h2v1h-1v1h2v1h-2v1h1v1h-1v1h-2v-1h1v-2h-1v-1h-1v4h-1v-2h-1v1h-1v-3h1v-1h4zM18 8h3v1h1v-1h1v2h1v-2h1v2h1v-2h1v1h1v-1h1v1h-1v1h-1v1h-1v3h-2v-1h1v-1h-1v-1h-1v3h1v1h-2v-2h-1v-1h1v-1h-2v-2h-2zM12 9h1v1h-1zM1 10v1h1v-1zM10 10h2v2h-1v2h1v1h1v-1h1v-1h1v1h-1v1h1v1h1v2h2v-1h1v-2h1v-1h-1v-2h1v1h1v3h-1v1h-1v2h-3v1h2v1h-3v-1h-1v-2h-1v1h-1v-3h-2v1h-1v-1h-1v1h-2v1h3v1h1v1h-1v1h1v1h2v-2h-1v-1h1v1h1v1h1v1h-1v1h1v-1h1v1h1v1h-1v1h-3v-1h-1v-1h-1v2h1v1h2v2h-1v-1h-2v-1h-1v-2h-1v-1h1v-1h-1v-2h-1v-1h-2v-1h-1v-1h1v-2h1v-1h1v-1h1v-2h1v1h1zM27 12h1v1h-1zM9 13v1h1v-1zM28 13h1v1h-1zM0 14h2v1h1v-1h1v2h-1v1h-1v1h1v1h2v1h-2v1h-1v-2h-1v2h-1zM7 14v1h-1v1h1v-1h1v-1zM24 15h3v3h-2v-1h-1zM22 16h1v1h-1zM28 16h1v3h-1zM14 17v1h1v-1zM20 17h1v2h2v-2h1v3h1v-1h1v1h-1v1h1v-1h2v1h-1v1h-2v1h1v1h-1v1h-1v1h1v-1h3v2h-1v-1h-1v1h-1v2h-1v-2h-1v1h-1v-1h-1v1h-2v-1h-1v-1h-3v-1h1v-1h1v-1h2v-1h-1v-1h1v-2h1zM5 20h2v1h-2zM19 21v1h1v-1zM21 21v3h3v-3zM0 22h7v7h-7zM22 22h1v1h-1zM27 22h1v1h-1zM1 23v5h5v-5zM28 23h1v1h-1zM2 24h3v3h-3zM17 24v1h1v-1zM21 25v1h2v-1zM14 26h1v1h1v1h1v1h-3zM9 27h1v1h-1zM26 27h1v1h-1zM28 27h1v1h-1z" style="fill:#000" transform="translate(20,20) scale(10)"/>
</svg>
SVG

AS_BASIC = <<~STR
XXXXXXXOOOXOXOOXXXOOOOXXXXXXX
XOOOOOXOOXXXXXOOXOXOOOXOOOOOX
Expand Down
15 changes: 13 additions & 2 deletions spec/rqrcode/export_svg_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,19 @@
expect(RQRCode::QRCode.new("qrcode")).to respond_to(:as_svg)
end

it "must export to svg" do
expect(RQRCode::QRCode.new("qrcode").as_svg).to eq(AS_SVG)
context "with use_rect (default) option" do
it "must export to svg" do
expect(RQRCode::QRCode.new("qrcode").as_svg).to eq(AS_SVG)
end
end

context "with use_path option" do
it "must export to svg" do
expect(RQRCode::QRCode.new("https://kyan.com").as_svg(
use_path: true,
offset: 20
)).to eq(AS_SVG1)
end
end

describe "options" do
Expand Down

0 comments on commit 2092a33

Please sign in to comment.