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:
@@ -1,14 +1,18 @@
|
||||
PATH
|
||||
remote: .
|
||||
specs:
|
||||
libptouch (1.0.0)
|
||||
libptouch (1.0.1)
|
||||
barby
|
||||
ffi (~> 1.15)
|
||||
rexml
|
||||
rqrcode
|
||||
|
||||
GEM
|
||||
remote: https://rubygems.org/
|
||||
specs:
|
||||
ast (2.4.3)
|
||||
barby (0.7.0)
|
||||
chunky_png (1.4.0)
|
||||
ffi (1.17.4)
|
||||
ffi (1.17.4-aarch64-linux-gnu)
|
||||
ffi (1.17.4-aarch64-linux-musl)
|
||||
@@ -32,6 +36,10 @@ GEM
|
||||
rainbow (3.1.1)
|
||||
regexp_parser (2.12.0)
|
||||
rexml (3.4.4)
|
||||
rqrcode (3.2.0)
|
||||
chunky_png (~> 1.0)
|
||||
rqrcode_core (~> 2.0)
|
||||
rqrcode_core (2.1.0)
|
||||
rubocop (1.86.1)
|
||||
json (~> 2.3)
|
||||
language_server-protocol (~> 3.17.0.2)
|
||||
@@ -70,6 +78,8 @@ DEPENDENCIES
|
||||
|
||||
CHECKSUMS
|
||||
ast (2.4.3) sha256=954615157c1d6a382bc27d690d973195e79db7f55e9765ac7c481c60bdb4d383
|
||||
barby (0.7.0) sha256=8dbc7c0c320e596135d97929c0df77f969e9f9c955a157cf6749c05b44dae213
|
||||
chunky_png (1.4.0) sha256=89d5b31b55c0cf4da3cf89a2b4ebc3178d8abe8cbaf116a1dba95668502fdcfe
|
||||
ffi (1.17.4) sha256=bcd1642e06f0d16fc9e09ac6d49c3a7298b9789bcb58127302f934e437d60acf
|
||||
ffi (1.17.4-aarch64-linux-gnu) sha256=b208f06f91ffd8f5e1193da3cae3d2ccfc27fc36fba577baf698d26d91c080df
|
||||
ffi (1.17.4-aarch64-linux-musl) sha256=9286b7a615f2676245283aef0a0a3b475ae3aae2bb5448baace630bb77b91f39
|
||||
@@ -83,7 +93,7 @@ CHECKSUMS
|
||||
ffi (1.17.4-x86_64-linux-musl) sha256=3fdf9888483de005f8ef8d1cf2d3b20d86626af206cbf780f6a6a12439a9c49e
|
||||
json (2.19.3) sha256=289b0bb53052a1fa8c34ab33cc750b659ba14a5c45f3fcf4b18762dc67c78646
|
||||
language_server-protocol (3.17.0.5) sha256=fd1e39a51a28bf3eec959379985a72e296e9f9acfce46f6a79d31ca8760803cc
|
||||
libptouch (1.0.0)
|
||||
libptouch (1.0.1)
|
||||
lint_roller (1.1.0) sha256=2c0c845b632a7d172cb849cc90c1bce937a28c5c8ccccb50dfd46a485003cc87
|
||||
parallel (2.0.1) sha256=337782d3e39f4121e67563bf91dd8ece67f48923d90698614773a0ec9a5b2c7d
|
||||
parser (3.3.11.1) sha256=d17ace7aabe3e72c3cc94043714be27cc6f852f104d81aa284c2281aecc65d54
|
||||
@@ -92,6 +102,8 @@ CHECKSUMS
|
||||
rainbow (3.1.1) sha256=039491aa3a89f42efa1d6dec2fc4e62ede96eb6acd95e52f1ad581182b79bc6a
|
||||
regexp_parser (2.12.0) sha256=35a916a1d63190ab5c9009457136ae5f3c0c7512d60291d0d1378ba18ce08ebb
|
||||
rexml (3.4.4) sha256=19e0a2c3425dfbf2d4fc1189747bdb2f849b6c5e74180401b15734bc97b5d142
|
||||
rqrcode (3.2.0) sha256=64c1494ca6bb67d731330f38b50e3fd09eeab4f5dcd04b608e21218d1d0b9542
|
||||
rqrcode_core (2.1.0) sha256=f303b85df89c1b8fc5ee8dc19808c9dc4330e6329b660d99d4a8cbb36ca13051
|
||||
rubocop (1.86.1) sha256=44415f3f01d01a21e01132248d2fd0867572475b566ca188a0a42133a08d4531
|
||||
rubocop-ast (1.49.1) sha256=4412f3ee70f6fe4546cc489548e0f6fcf76cafcfa80fa03af67098ffed755035
|
||||
ruby-progressbar (1.13.0) sha256=80fc9c47a9b640d6834e0dc7b3c94c9df37f08cb072b7761e4a71e22cff29b33
|
||||
|
||||
@@ -37,6 +37,7 @@ SVG は現在装着テープの印字可能幅に合わせて自動拡大・縮
|
||||
|
||||
オプションは C 側に合わせ、`**-p` / `--pid`** で USB 製品 ID(16 進可)を指定できます。省略時は PT-P900W(`Libptouch::USB_PID_PTP900W` = `0x2085`)。例: PT-P750W `0x2062`、PT-P710BT `0x20af`(`libptouch.h` / `Libptouch` 定数と同じ)。
|
||||
また、`--template`(SVG)と `--data`(JSON/YAML)を使うと `data-field` 属性をキーにした差込印刷が可能です。
|
||||
`data-kb-placeholder="qr"` / `data-kb-placeholder="barcode"` を付けた SVG 要素(`x/y/width/height` 必須)には、対応する `data-field` 値から QR / Code128 バーコードを生成して差し込みできます。
|
||||
`--trim-right[=DOTS]` を付けると、libptouch 側の共通処理でラベル右側の空白ドット列を削減します。`DOTS` 省略時は左余白ドット数を使い、取得失敗時は `0` にフォールバックします。
|
||||
|
||||
開発ツリーからそのまま試す例:
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -29,5 +29,7 @@ Gem::Specification.new do |spec|
|
||||
spec.require_paths = ["lib"]
|
||||
|
||||
spec.add_dependency "ffi", "~> 1.15"
|
||||
spec.add_dependency "barby"
|
||||
spec.add_dependency "rqrcode"
|
||||
spec.add_dependency "rexml"
|
||||
end
|
||||
|
||||
Reference in New Issue
Block a user