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
|
PATH
|
||||||
remote: .
|
remote: .
|
||||||
specs:
|
specs:
|
||||||
libptouch (1.0.0)
|
libptouch (1.0.1)
|
||||||
|
barby
|
||||||
ffi (~> 1.15)
|
ffi (~> 1.15)
|
||||||
rexml
|
rexml
|
||||||
|
rqrcode
|
||||||
|
|
||||||
GEM
|
GEM
|
||||||
remote: https://rubygems.org/
|
remote: https://rubygems.org/
|
||||||
specs:
|
specs:
|
||||||
ast (2.4.3)
|
ast (2.4.3)
|
||||||
|
barby (0.7.0)
|
||||||
|
chunky_png (1.4.0)
|
||||||
ffi (1.17.4)
|
ffi (1.17.4)
|
||||||
ffi (1.17.4-aarch64-linux-gnu)
|
ffi (1.17.4-aarch64-linux-gnu)
|
||||||
ffi (1.17.4-aarch64-linux-musl)
|
ffi (1.17.4-aarch64-linux-musl)
|
||||||
@@ -32,6 +36,10 @@ GEM
|
|||||||
rainbow (3.1.1)
|
rainbow (3.1.1)
|
||||||
regexp_parser (2.12.0)
|
regexp_parser (2.12.0)
|
||||||
rexml (3.4.4)
|
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)
|
rubocop (1.86.1)
|
||||||
json (~> 2.3)
|
json (~> 2.3)
|
||||||
language_server-protocol (~> 3.17.0.2)
|
language_server-protocol (~> 3.17.0.2)
|
||||||
@@ -70,6 +78,8 @@ DEPENDENCIES
|
|||||||
|
|
||||||
CHECKSUMS
|
CHECKSUMS
|
||||||
ast (2.4.3) sha256=954615157c1d6a382bc27d690d973195e79db7f55e9765ac7c481c60bdb4d383
|
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) sha256=bcd1642e06f0d16fc9e09ac6d49c3a7298b9789bcb58127302f934e437d60acf
|
||||||
ffi (1.17.4-aarch64-linux-gnu) sha256=b208f06f91ffd8f5e1193da3cae3d2ccfc27fc36fba577baf698d26d91c080df
|
ffi (1.17.4-aarch64-linux-gnu) sha256=b208f06f91ffd8f5e1193da3cae3d2ccfc27fc36fba577baf698d26d91c080df
|
||||||
ffi (1.17.4-aarch64-linux-musl) sha256=9286b7a615f2676245283aef0a0a3b475ae3aae2bb5448baace630bb77b91f39
|
ffi (1.17.4-aarch64-linux-musl) sha256=9286b7a615f2676245283aef0a0a3b475ae3aae2bb5448baace630bb77b91f39
|
||||||
@@ -83,7 +93,7 @@ CHECKSUMS
|
|||||||
ffi (1.17.4-x86_64-linux-musl) sha256=3fdf9888483de005f8ef8d1cf2d3b20d86626af206cbf780f6a6a12439a9c49e
|
ffi (1.17.4-x86_64-linux-musl) sha256=3fdf9888483de005f8ef8d1cf2d3b20d86626af206cbf780f6a6a12439a9c49e
|
||||||
json (2.19.3) sha256=289b0bb53052a1fa8c34ab33cc750b659ba14a5c45f3fcf4b18762dc67c78646
|
json (2.19.3) sha256=289b0bb53052a1fa8c34ab33cc750b659ba14a5c45f3fcf4b18762dc67c78646
|
||||||
language_server-protocol (3.17.0.5) sha256=fd1e39a51a28bf3eec959379985a72e296e9f9acfce46f6a79d31ca8760803cc
|
language_server-protocol (3.17.0.5) sha256=fd1e39a51a28bf3eec959379985a72e296e9f9acfce46f6a79d31ca8760803cc
|
||||||
libptouch (1.0.0)
|
libptouch (1.0.1)
|
||||||
lint_roller (1.1.0) sha256=2c0c845b632a7d172cb849cc90c1bce937a28c5c8ccccb50dfd46a485003cc87
|
lint_roller (1.1.0) sha256=2c0c845b632a7d172cb849cc90c1bce937a28c5c8ccccb50dfd46a485003cc87
|
||||||
parallel (2.0.1) sha256=337782d3e39f4121e67563bf91dd8ece67f48923d90698614773a0ec9a5b2c7d
|
parallel (2.0.1) sha256=337782d3e39f4121e67563bf91dd8ece67f48923d90698614773a0ec9a5b2c7d
|
||||||
parser (3.3.11.1) sha256=d17ace7aabe3e72c3cc94043714be27cc6f852f104d81aa284c2281aecc65d54
|
parser (3.3.11.1) sha256=d17ace7aabe3e72c3cc94043714be27cc6f852f104d81aa284c2281aecc65d54
|
||||||
@@ -92,6 +102,8 @@ CHECKSUMS
|
|||||||
rainbow (3.1.1) sha256=039491aa3a89f42efa1d6dec2fc4e62ede96eb6acd95e52f1ad581182b79bc6a
|
rainbow (3.1.1) sha256=039491aa3a89f42efa1d6dec2fc4e62ede96eb6acd95e52f1ad581182b79bc6a
|
||||||
regexp_parser (2.12.0) sha256=35a916a1d63190ab5c9009457136ae5f3c0c7512d60291d0d1378ba18ce08ebb
|
regexp_parser (2.12.0) sha256=35a916a1d63190ab5c9009457136ae5f3c0c7512d60291d0d1378ba18ce08ebb
|
||||||
rexml (3.4.4) sha256=19e0a2c3425dfbf2d4fc1189747bdb2f849b6c5e74180401b15734bc97b5d142
|
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 (1.86.1) sha256=44415f3f01d01a21e01132248d2fd0867572475b566ca188a0a42133a08d4531
|
||||||
rubocop-ast (1.49.1) sha256=4412f3ee70f6fe4546cc489548e0f6fcf76cafcfa80fa03af67098ffed755035
|
rubocop-ast (1.49.1) sha256=4412f3ee70f6fe4546cc489548e0f6fcf76cafcfa80fa03af67098ffed755035
|
||||||
ruby-progressbar (1.13.0) sha256=80fc9c47a9b640d6834e0dc7b3c94c9df37f08cb072b7761e4a71e22cff29b33
|
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` 定数と同じ)。
|
オプションは 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` 属性をキーにした差込印刷が可能です。
|
また、`--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` にフォールバックします。
|
`--trim-right[=DOTS]` を付けると、libptouch 側の共通処理でラベル右側の空白ドット列を削減します。`DOTS` 省略時は左余白ドット数を使い、取得失敗時は `0` にフォールバックします。
|
||||||
|
|
||||||
開発ツリーからそのまま試す例:
|
開発ツリーからそのまま試す例:
|
||||||
|
|||||||
@@ -2,9 +2,13 @@
|
|||||||
|
|
||||||
require "json"
|
require "json"
|
||||||
require "optparse"
|
require "optparse"
|
||||||
|
require "rqrcode"
|
||||||
require "rexml/document"
|
require "rexml/document"
|
||||||
require "tempfile"
|
require "tempfile"
|
||||||
require "yaml"
|
require "yaml"
|
||||||
|
require "barby"
|
||||||
|
require "barby/barcode/code_128"
|
||||||
|
require "barby/outputter/svg_outputter"
|
||||||
|
|
||||||
require "libptouch"
|
require "libptouch"
|
||||||
|
|
||||||
@@ -289,12 +293,155 @@ module Libptouch
|
|||||||
|
|
||||||
replace_text_element_content(el, data[key].to_s)
|
replace_text_element_content(el, data[key].to_s)
|
||||||
end
|
end
|
||||||
|
replace_code_placeholders(doc, data)
|
||||||
out = +""
|
out = +""
|
||||||
formatter = REXML::Formatters::Default.new
|
formatter = REXML::Formatters::Default.new
|
||||||
formatter.write(doc, out)
|
formatter.write(doc, out)
|
||||||
out
|
out
|
||||||
end
|
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)
|
def replace_text_element_content(text_element, value)
|
||||||
# Remove all child nodes first so mixed content (<tspan>, text nodes, etc.)
|
# Remove all child nodes first so mixed content (<tspan>, text nodes, etc.)
|
||||||
# gets replaced consistently by merge data.
|
# gets replaced consistently by merge data.
|
||||||
|
|||||||
@@ -29,5 +29,7 @@ Gem::Specification.new do |spec|
|
|||||||
spec.require_paths = ["lib"]
|
spec.require_paths = ["lib"]
|
||||||
|
|
||||||
spec.add_dependency "ffi", "~> 1.15"
|
spec.add_dependency "ffi", "~> 1.15"
|
||||||
|
spec.add_dependency "barby"
|
||||||
|
spec.add_dependency "rqrcode"
|
||||||
spec.add_dependency "rexml"
|
spec.add_dependency "rexml"
|
||||||
end
|
end
|
||||||
|
|||||||
Reference in New Issue
Block a user