diff --git a/ruby/Gemfile.lock b/ruby/Gemfile.lock index 7cb5d92..7037c81 100644 --- a/ruby/Gemfile.lock +++ b/ruby/Gemfile.lock @@ -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 diff --git a/ruby/README.md b/ruby/README.md index 4edfc63..7f16e7f 100644 --- a/ruby/README.md +++ b/ruby/README.md @@ -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` にフォールバックします。 開発ツリーからそのまま試す例: diff --git a/ruby/lib/libptouch/cli/label_print.rb b/ruby/lib/libptouch/cli/label_print.rb index 3aafb40..cc64f52 100644 --- a/ruby/lib/libptouch/cli/label_print.rb +++ b/ruby/lib/libptouch/cli/label_print.rb @@ -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 (, text nodes, etc.) # gets replaced consistently by merge data. diff --git a/ruby/libptouch.gemspec b/ruby/libptouch.gemspec index 2881424..d07c0ed 100644 --- a/ruby/libptouch.gemspec +++ b/ruby/libptouch.gemspec @@ -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