feat(ruby): QR/Barcode merge placeholders in SVG templates
Add data-kb-placeholder qr/barcode support with rqrcode and barby. Scale QR to fit placeholder box (viewBox + meet) and barcode by height with natural width. Declare barby and rqrcode runtime dependencies. Made-with: Cursor
This commit is contained in:
@@ -2,9 +2,13 @@
|
||||
|
||||
require "json"
|
||||
require "optparse"
|
||||
require "rqrcode"
|
||||
require "rexml/document"
|
||||
require "tempfile"
|
||||
require "yaml"
|
||||
require "barby"
|
||||
require "barby/barcode/code_128"
|
||||
require "barby/outputter/svg_outputter"
|
||||
|
||||
require "libptouch"
|
||||
|
||||
@@ -289,12 +293,155 @@ module Libptouch
|
||||
|
||||
replace_text_element_content(el, data[key].to_s)
|
||||
end
|
||||
replace_code_placeholders(doc, data)
|
||||
out = +""
|
||||
formatter = REXML::Formatters::Default.new
|
||||
formatter.write(doc, out)
|
||||
out
|
||||
end
|
||||
|
||||
def replace_code_placeholders(doc, data)
|
||||
doc.elements.each("//*[@data-field][@data-kb-placeholder]") do |el|
|
||||
key = el.attributes["data-field"].to_s
|
||||
next unless data.key?(key)
|
||||
|
||||
kind = el.attributes["data-kb-placeholder"].to_s.downcase
|
||||
next unless %w[qr barcode].include?(kind)
|
||||
|
||||
x, y, width, height = read_box(el)
|
||||
next if width <= 0 || height <= 0
|
||||
|
||||
raw_value = data[key].to_s
|
||||
next if raw_value.empty?
|
||||
|
||||
replacement = build_code_svg_element(
|
||||
kind: kind,
|
||||
value: raw_value,
|
||||
x: x,
|
||||
y: y,
|
||||
width: width,
|
||||
height: height
|
||||
)
|
||||
el.parent&.insert_after(el, replacement)
|
||||
el.parent&.delete(el)
|
||||
end
|
||||
end
|
||||
|
||||
def read_box(el)
|
||||
x = el.attributes["x"].to_s.to_f
|
||||
y = el.attributes["y"].to_s.to_f
|
||||
width = el.attributes["width"].to_s.to_f
|
||||
height = el.attributes["height"].to_s.to_f
|
||||
[x, y, width, height]
|
||||
end
|
||||
|
||||
def build_code_svg_element(kind:, value:, x:, y:, width:, height:)
|
||||
svg_fragment =
|
||||
if kind == "qr"
|
||||
render_qr_svg(value)
|
||||
else
|
||||
render_barcode_svg(value)
|
||||
end
|
||||
fragment_doc = REXML::Document.new(strip_xml_declaration(svg_fragment))
|
||||
svg_root = fragment_doc.root
|
||||
svg_root = wrap_non_svg_root(svg_root) unless svg_root&.name == "svg"
|
||||
|
||||
if kind == "qr"
|
||||
apply_qr_svg_box(svg_root, x: x, y: y, width: width, height: height)
|
||||
else
|
||||
apply_barcode_svg_box(svg_root, x: x, y: y, height: height)
|
||||
end
|
||||
svg_root
|
||||
end
|
||||
|
||||
def apply_qr_svg_box(svg_root, x:, y:, width:, height:)
|
||||
nat_w, nat_h = svg_natural_dimensions(svg_root)
|
||||
if nat_w <= 0 || nat_h <= 0
|
||||
nat_w = parse_length_attr(svg_root.attributes["width"])
|
||||
nat_h = parse_length_attr(svg_root.attributes["height"])
|
||||
end
|
||||
raise ArgumentError, "QR SVG has no usable dimensions" if nat_w <= 0 || nat_h <= 0
|
||||
|
||||
svg_root.attributes["viewBox"] = "0 0 #{nat_w} #{nat_h}"
|
||||
svg_root.attributes["x"] = x.to_s
|
||||
svg_root.attributes["y"] = y.to_s
|
||||
svg_root.attributes["width"] = width.to_s
|
||||
svg_root.attributes["height"] = height.to_s
|
||||
svg_root.attributes["preserveAspectRatio"] = "xMidYMid meet"
|
||||
end
|
||||
|
||||
def apply_barcode_svg_box(svg_root, x:, y:, height:)
|
||||
nat_w, nat_h = svg_natural_dimensions(svg_root)
|
||||
if nat_w <= 0 || nat_h <= 0
|
||||
nat_w = parse_length_attr(svg_root.attributes["width"])
|
||||
nat_h = parse_length_attr(svg_root.attributes["height"])
|
||||
end
|
||||
raise ArgumentError, "barcode SVG has no usable dimensions" if nat_w <= 0 || nat_h <= 0
|
||||
|
||||
scale = height / nat_h
|
||||
out_w = nat_w * scale
|
||||
out_h = nat_h * scale
|
||||
|
||||
svg_root.attributes["viewBox"] = "0 0 #{nat_w} #{nat_h}"
|
||||
svg_root.attributes["x"] = x.to_s
|
||||
svg_root.attributes["y"] = y.to_s
|
||||
svg_root.attributes["width"] = out_w.to_s
|
||||
svg_root.attributes["height"] = out_h.to_s
|
||||
svg_root.attributes["preserveAspectRatio"] = "xMidYMid meet"
|
||||
end
|
||||
|
||||
def strip_xml_declaration(s)
|
||||
s.to_s.sub(/\A<\?xml[^>]*\?>\s*/m, "")
|
||||
end
|
||||
|
||||
def svg_natural_dimensions(svg_root)
|
||||
vb = svg_root.attributes["viewBox"].to_s.strip
|
||||
if (m = /\A\s*([-\d.eE+]+)\s+([-\d.eE+]+)\s+([-\d.eE+]+)\s+([-\d.eE+]+)\s*\z/.match(vb))
|
||||
return [m[3].to_f, m[4].to_f]
|
||||
end
|
||||
|
||||
[parse_length_attr(svg_root.attributes["width"]),
|
||||
parse_length_attr(svg_root.attributes["height"])]
|
||||
end
|
||||
|
||||
def parse_length_attr(str)
|
||||
s = str.to_s.strip
|
||||
return 0.0 if s.empty?
|
||||
|
||||
s = s.delete_suffix("px").strip
|
||||
Float(s)
|
||||
rescue ArgumentError
|
||||
0.0
|
||||
end
|
||||
|
||||
def render_qr_svg(value)
|
||||
qrcode = RQRCode::QRCode.new(value)
|
||||
qrcode.as_svg(
|
||||
standalone: true,
|
||||
use_path: true,
|
||||
module_size: 1,
|
||||
offset: 0,
|
||||
color: "000",
|
||||
shape_rendering: "crispEdges"
|
||||
)
|
||||
end
|
||||
|
||||
def render_barcode_svg(value)
|
||||
barcode = Barby::Code128B.new(value)
|
||||
Barby::SvgOutputter.new(barcode).to_svg(
|
||||
xdim: 2,
|
||||
height: 64,
|
||||
margin: 0
|
||||
)
|
||||
end
|
||||
|
||||
def wrap_non_svg_root(root)
|
||||
svg = REXML::Element.new("svg")
|
||||
svg.add_namespace("http://www.w3.org/2000/svg")
|
||||
svg.add_element(root)
|
||||
svg
|
||||
end
|
||||
|
||||
def replace_text_element_content(text_element, value)
|
||||
# Remove all child nodes first so mixed content (<tspan>, text nodes, etc.)
|
||||
# gets replaced consistently by merge data.
|
||||
|
||||
Reference in New Issue
Block a user