From 32ab12f66167dadfa498320e538e72196d4d515c Mon Sep 17 00:00:00 2001 From: knb Date: Thu, 16 Apr 2026 14:49:08 +0900 Subject: [PATCH 1/2] =?UTF-8?q?ptouch-label=20CLI=E3=82=92=E8=BF=BD?= =?UTF-8?q?=E5=8A=A0=E3=81=97=E3=81=A6=E5=B7=AE=E8=BE=BC=E5=8D=B0=E5=88=B7?= =?UTF-8?q?=E3=82=92=E6=8B=A1=E5=BC=B5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit コマンド名を機能に合わせて整理し、SVGテンプレート+JSON/YAMLの差込印刷とメディア情報取得を使いやすくする。 Made-with: Cursor --- reference/note.adoc | 0 ruby/README.md | 22 +- ruby/exe/ptouch-label | 6 + ruby/exe/ptouch-print-png | 4 +- ruby/lib/libptouch/cli/label_print.rb | 361 ++++++++++++++++++++++++++ ruby/lib/libptouch/cli/png_print.rb | 249 +----------------- ruby/lib/libptouch/context.rb | 2 +- ruby/libptouch.gemspec | 2 +- samples/README.md | 9 +- samples/merge_data.json | 5 + samples/merge_data.yml | 3 + samples/merge_template.svg | 9 + 12 files changed, 412 insertions(+), 260 deletions(-) create mode 100644 reference/note.adoc create mode 100644 ruby/exe/ptouch-label create mode 100644 ruby/lib/libptouch/cli/label_print.rb create mode 100644 samples/merge_data.json create mode 100644 samples/merge_data.yml create mode 100644 samples/merge_template.svg diff --git a/reference/note.adoc b/reference/note.adoc new file mode 100644 index 0000000..e69de29 diff --git a/ruby/README.md b/ruby/README.md index 3b6da2f..360755f 100644 --- a/ruby/README.md +++ b/ruby/README.md @@ -31,21 +31,25 @@ export LIBPTOUCH_LIB=/usr/local/lib/libptouch.so (`cmake --install` で共有ライブラリをインストールした場合は、通常は `libptouch` 名でローダが解決します。) -## コマンド `ptouch-print-png`(PNG/SVG) +## コマンド `ptouch-label`(PNG/SVG) -C の `ptouch-print` と同様の流れで、**PNG/SVG 入力**(`-w`/`-H` や 1bit ラスターは扱いません)を扱います。`gem install` 後は PATH に `ptouch-print-png` が入ります。 +C の `ptouch-print` と同様の流れで、**PNG/SVG 入力**(`-w`/`-H` や 1bit ラスターは扱いません)を扱います。`gem install` 後は PATH に `ptouch-label` が入ります。 SVG は現在装着テープの印字可能幅に合わせて自動拡大・縮小します(USB 接続が必要)。 +後方互換のため `ptouch-print-png` も引き続き使えます。 オプションは 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` 属性をキーにした差込印刷が可能です。 開発ツリーからそのまま試す例: ```bash -bundle exec ruby -I lib exe/ptouch-print-png --help -bundle exec ruby -I lib exe/ptouch-print-png -n -f ../samples/your.png -bundle exec ruby -I lib exe/ptouch-print-png -n -f ../samples/your.svg -bundle exec ruby -I lib exe/ptouch-print-png --status -p 0x2062 -bundle exec ruby -I lib exe/ptouch-print-png -f ../samples/your.png -p 0x20af +bundle exec ruby -I lib exe/ptouch-label --help +bundle exec ruby -I lib exe/ptouch-label -n -f ../samples/your.png +bundle exec ruby -I lib exe/ptouch-label -n -f ../samples/your.svg +bundle exec ruby -I lib exe/ptouch-label -n --template ../samples/your_template.svg --data ../samples/your_data.yml +bundle exec ruby -I lib exe/ptouch-label --media-info +bundle exec ruby -I lib exe/ptouch-label --status -p 0x2062 +bundle exec ruby -I lib exe/ptouch-label -f ../samples/your.png -p 0x20af ``` ## 使用例 @@ -92,9 +96,9 @@ ctx.dispose ## API の範囲 -- 実行ファイル `ptouch-print-png` … PNG/SVG(`-f`, `-t`, `-p`, `-n`, `-S`, `-V`, `-h`)。ステータスは JSON(`status_bytes` を `parse_status` したもの、`raw_bytes` 除く) +- 実行ファイル `ptouch-label`(互換: `ptouch-print-png`)… PNG/SVG(`-f`, `-t`, `-p`, `-n`, `-M`, `-S`, `-V`, `-h`, `--template`, `--data`)。`-M` は現在テープ情報(幅 mm・DPI 等)を JSON 出力、`-S` はステータス JSON 出力 - `Libptouch::Context` … `open_usb` / `open_usb_vid_pid` / `close` / `dispose` - `check_raster` / `print_raster` / `png_file_to_raster` / `svg_file_to_raster_fit_current_tape` / `status_bytes` / `status_hash` / `current_media_info` -- `current_media_info` には `print_dpi` / `feed_dpi` / `tape_width_mm` / `min_feed_mm` などを含む +- `current_media_info` には `print_dpi` / `feed_dpi` / `tape_width_mm` / `printable_height_dots` / `min_feed_mm` などを含む(テープ幅方向は `printable_height_dots`) - `Libptouch.parse_status(raw)` … 32 バイトを Hash に展開(機種・テープ幅・**テープ種類**・色・**状態(status_kind)**・エラービット・`raw_hex` など) - C の `libptouch_status_fprint`(`FILE *`)は FFI からはバインドしていません。テキスト出力の代わりに `parse_status` / `status_hash` を使ってください。 diff --git a/ruby/exe/ptouch-label b/ruby/exe/ptouch-label new file mode 100644 index 0000000..c5258af --- /dev/null +++ b/ruby/exe/ptouch-label @@ -0,0 +1,6 @@ +#!/usr/bin/env ruby +# frozen_string_literal: true + +require "libptouch/cli/label_print" + +exit Libptouch::Cli::LabelPrint.run(ARGV) diff --git a/ruby/exe/ptouch-print-png b/ruby/exe/ptouch-print-png index 7fe6462..c5258af 100755 --- a/ruby/exe/ptouch-print-png +++ b/ruby/exe/ptouch-print-png @@ -1,6 +1,6 @@ #!/usr/bin/env ruby # frozen_string_literal: true -require "libptouch/cli/png_print" +require "libptouch/cli/label_print" -exit Libptouch::Cli::PngPrint.run(ARGV) +exit Libptouch::Cli::LabelPrint.run(ARGV) diff --git a/ruby/lib/libptouch/cli/label_print.rb b/ruby/lib/libptouch/cli/label_print.rb new file mode 100644 index 0000000..c89ff5f --- /dev/null +++ b/ruby/lib/libptouch/cli/label_print.rb @@ -0,0 +1,361 @@ +# frozen_string_literal: true + +require "json" +require "optparse" +require "rexml/document" +require "tempfile" +require "yaml" + +require "libptouch" + +module Libptouch + module Cli + # PNG/SVG を扱う ptouch-print 相当の CLI(1bit ラスター経路なし)。 + module LabelPrint + module_function + + def run(argv) + opts = parse(argv) + return 2 if opts.nil? + + return run_version if opts[:version] + return run_help if opts[:help] + + return run_media_info(opts) if opts[:media_info] + + if opts[:status] + warn_unused_file_options(opts) + return run_status(opts) + end + + if opts[:template] || opts[:data] + return usage_error("--template and --data must be used together") if opts[:template].to_s.empty? || opts[:data].to_s.empty? + return usage_error("-f and --template/--data cannot be used together") unless opts[:file].to_s.empty? + return run_template_print(opts) + end + + return usage_error("-f is required (or use --template/--data, or --status)") if opts[:file].to_s.empty? + + path = opts[:file] + kind = image_kind(path) + return usage_error("not a PNG/SVG file: #{path}") if kind.nil? + + run_print(path, opts, kind) + end + + def png_file?(path) + return true if path.downcase.end_with?(".png") + + File.open(path, "rb") do |f| + sig = f.read(8) + sig == "\x89PNG\r\n\x1a\n".b + end + rescue Errno::ENOENT, Errno::EACCES => e + warn "open #{path}: #{e.message}" + false + end + + def svg_file?(path) + path.downcase.end_with?(".svg") + end + + def image_kind(path) + return :png if png_file?(path) + return :svg if svg_file?(path) + + nil + end + + def parse(argv) + opts_hash = default_cli_opts + build_cli_parser(opts_hash).parse!(argv) + return nil unless threshold_option_ok?(opts_hash) + return nil unless usb_pid_option_ok?(opts_hash) + + opts_hash.delete(:usb_pid_invalid) + opts_hash + end + + def default_cli_opts + { + file: nil, + template: nil, + data: nil, + threshold: nil, + usb_pid: nil, + usb_pid_invalid: false, + dry_run: false, + media_info: false, + status: false, + version: false, + help: false + } + end + + def pid_option_description + p900 = Libptouch::USB_PID_PTP900W + p750 = Libptouch::USB_PID_PTP750W + p710 = Libptouch::USB_PID_PTP710BT + "USB 製品 ID(16 進可)。既定 P900W 0x#{p900.to_s(16)}; " \ + "P750W 0x#{p750.to_s(16)}; P710BT 0x#{p710.to_s(16)}" + end + + def apply_usb_pid_option(opts_hash, pid_str) + opts_hash[:usb_pid] = Integer(pid_str, 0) + rescue ArgumentError + warn "invalid --pid: #{pid_str.inspect}" + opts_hash[:usb_pid_invalid] = true + end + + def threshold_option_ok?(opts_hash) + return true if opts_hash[:threshold].nil? || (0..255).cover?(opts_hash[:threshold]) + + warn "-t must be 0..255" + false + end + + def usb_pid_option_ok?(opts_hash) + return false if opts_hash[:usb_pid_invalid] + return true if opts_hash[:usb_pid].nil? + return true if opts_hash[:usb_pid].between?(1, 0xFFFF) + + warn "-p/--pid must be 1..0xFFFF" + false + end + + def build_cli_parser(opts_hash) + OptionParser.new do |p| + p.banner = usage_banner + p.separator "" + p.on("-f", "--file PATH", "入力 PNG/SVG ファイル") { |v| opts_hash[:file] = v } + p.on("--template PATH", "差込用 SVG テンプレート") { |v| opts_hash[:template] = v } + p.on("--data PATH", "差込データ JSON/YAML ファイル") { |v| opts_hash[:data] = v } + p.on("-t", "--threshold N", Integer, + "二値化しきい値 0–255(既定 #{Libptouch::PNG_DEFAULT_THRESHOLD}、PNG/SVG)") do |v| + opts_hash[:threshold] = v + end + p.on("-p", "--pid PID", pid_option_description) do |v| + apply_usb_pid_option(opts_hash, v) + end + p.on("-n", "--dry-run", "読み込みと検証のみ(USB なし)") { opts_hash[:dry_run] = true } + p.on("-M", "--media-info", "現在テープの幅(mm)・DPI・最小余白(mm)を JSON で表示して終了") do + opts_hash[:media_info] = true + end + p.on("-S", "--status", "USB プリンタのステータスを JSON で表示して終了") do + opts_hash[:status] = true + end + p.on("-V", "--version", "バージョンを表示して終了") { opts_hash[:version] = true } + p.on("-h", "--help", "このヘルプ") { opts_hash[:help] = true } + end + end + + def usage_banner + <<~BANNER + Usage: ptouch-label [options] + + PNG/SVG 対応。幅・高さは画像から取得します(-w/-H はありません)。 + または --template/--data で SVG 差込印刷ができます。 + SVG は現在テープ幅に合わせて自動拡大・縮小します(USB 接続必須)。 + --status / --media-info のときは -f は不要です。 + BANNER + end + + def warn_unused_file_options(opts) + return unless opts[:file] || opts[:template] || opts[:data] || opts[:dry_run] || !opts[:threshold].nil? + + warn "warning: options other than --status are ignored" + end + + def run_version + puts "ptouch-label #{Libptouch::VERSION}" + 0 + end + + def run_help + puts usage_banner + puts "" + puts parser_help_text + 0 + end + + def parser_help_text + opts_hash = default_cli_opts + p = OptionParser.new do |parser| + parser.banner = "ptouch-label [options]" + parser.on("-f", "--file PATH", "入力 PNG/SVG ファイル") { |v| opts_hash[:file] = v } + parser.on("--template PATH", "差込用 SVG テンプレート") { |v| opts_hash[:template] = v } + parser.on("--data PATH", "差込データ JSON/YAML ファイル") { |v| opts_hash[:data] = v } + parser.on("-t", "--threshold N", Integer, + "二値化しきい値 0–255(既定 #{Libptouch::PNG_DEFAULT_THRESHOLD}、PNG/SVG)") do |v| + opts_hash[:threshold] = v + end + parser.on("-p", "--pid PID", pid_option_description) do |v| + apply_usb_pid_option(opts_hash, v) + end + parser.on("-n", "--dry-run", "読み込みと検証のみ(USB なし)") { opts_hash[:dry_run] = true } + parser.on("-M", "--media-info", "現在テープの幅(mm)・DPI・最小余白(mm)を JSON で表示して終了") do + opts_hash[:media_info] = true + end + parser.on("-S", "--status", "USB プリンタのステータスを JSON で表示して終了") do + opts_hash[:status] = true + end + parser.on("-V", "--version", "バージョンを表示して終了") { opts_hash[:version] = true } + parser.on("-h", "--help", "このヘルプ") { opts_hash[:help] = true } + end + p.help + end + + def open_usb_for_opts(ctx, opts) + if opts[:usb_pid] + ctx.open_usb_vid_pid(Libptouch::USB_VID_BROTHER, opts[:usb_pid]) + else + ctx.open_usb + end + end + + def run_template_print(opts) + svg_text = render_svg_template(opts[:template], load_merge_data(opts[:data])) + Tempfile.create(["ptouch-merge-", ".svg"]) do |tmp| + tmp.binmode + tmp.write(svg_text) + tmp.flush + run_print(tmp.path, opts, :svg) + end + rescue REXML::ParseException => e + warn "template parse error: #{e.message}" + 1 + rescue Errno::ENOENT, Errno::EACCES => e + warn e.message + 1 + rescue JSON::ParserError, Psych::SyntaxError => e + warn "data parse error: #{e.message}" + 1 + rescue ArgumentError => e + warn "data error: #{e.message}" + 1 + end + + def load_merge_data(path) + text = File.read(path, encoding: "UTF-8") + ext = File.extname(path).downcase + parsed = if ext == ".json" + JSON.parse(text) + else + YAML.safe_load(text, permitted_classes: [], aliases: false) + end + unless parsed.is_a?(Hash) + raise ArgumentError, "data must be a key-value object/hash" + end + + parsed.transform_keys(&:to_s) + end + + def render_svg_template(path, data) + xml = File.read(path, encoding: "UTF-8") + doc = REXML::Document.new(xml) + doc.elements.each("//text[@data-field]") do |el| + # If descendants also have data-field (e.g. tspan placeholders), + # keep node structure and let element-level replacements handle them. + next if el.elements[".//*[@data-field]"] + + key = el.attributes["data-field"].to_s + next unless data.key?(key) + + replace_text_element_content(el, data[key].to_s) + end + doc.elements.each("//tspan[@data-field]") do |el| + key = el.attributes["data-field"].to_s + next unless data.key?(key) + + replace_text_element_content(el, data[key].to_s) + end + out = +"" + formatter = REXML::Formatters::Default.new + formatter.write(doc, out) + out + 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. + text_element.children.to_a.each { |child| text_element.delete(child) } + text_element.add(REXML::Text.new(value, true)) + end + + def run_status(opts) + ctx = nil + begin + ctx = Libptouch::Context.new + open_usb_for_opts(ctx, opts) + h = Libptouch.parse_status(ctx.status_bytes) + h.delete(:raw_bytes) + puts JSON.pretty_generate(h) + 0 + rescue Libptouch::Error => e + warn "get_status: #{e.message}" + 1 + ensure + ctx&.dispose + end + end + + def run_media_info(opts) + warn_unused_file_options(opts) + ctx = nil + begin + ctx = Libptouch::Context.new + open_usb_for_opts(ctx, opts) + puts JSON.pretty_generate(ctx.current_media_info) + 0 + rescue Libptouch::Error => e + warn "media_info: #{e.message}" + 1 + ensure + ctx&.dispose + end + end + + def run_print(path, opts, kind) + ctx = nil + begin + ctx = Libptouch::Context.new + threshold = opts[:threshold] + data, width, height = if kind == :svg + open_usb_for_opts(ctx, opts) + if threshold.nil? + ctx.svg_file_to_raster_fit_current_tape(path) + else + ctx.svg_file_to_raster_fit_current_tape(path, threshold: threshold) + end + elsif threshold.nil? + ctx.png_file_to_raster(path) + else + ctx.png_file_to_raster(path, threshold: threshold) + end + + ctx.check_raster(data, width_dots: width, height_dots: height) + + if opts[:dry_run] + puts "dry-run OK: #{data.bytesize} bytes, src #{width}x#{height} dots (print lengthxwidth #{width}x#{height})" + return 0 + end + + open_usb_for_opts(ctx, opts) if kind == :png + ctx.print_raster(data, width_dots: width, height_dots: height) + 0 + rescue Libptouch::Error => e + warn e.message + 1 + ensure + ctx&.dispose + end + end + + def usage_error(msg) + warn msg + warn "(try ptouch-label --help)" + 2 + end + end + end +end diff --git a/ruby/lib/libptouch/cli/png_print.rb b/ruby/lib/libptouch/cli/png_print.rb index 2f2fc64..913fb30 100644 --- a/ruby/lib/libptouch/cli/png_print.rb +++ b/ruby/lib/libptouch/cli/png_print.rb @@ -1,251 +1,8 @@ -# frozen_string_literal: true - -require "json" -require "optparse" - -require "libptouch" +# Backward-compatibility shim. +require "libptouch/cli/label_print" module Libptouch module Cli - # PNG/SVG を扱う ptouch-print 相当の CLI(1bit ラスター経路なし)。 - module PngPrint - module_function - - def run(argv) - opts = parse(argv) - return 2 if opts.nil? - - return run_version if opts[:version] - return run_help if opts[:help] - - if opts[:status] - warn_unused_file_options(opts) - return run_status(opts) - end - - return usage_error("-f is required (or use --status)") if opts[:file].to_s.empty? - - path = opts[:file] - kind = image_kind(path) - return usage_error("not a PNG/SVG file: #{path}") if kind.nil? - - run_print(path, opts, kind) - end - - def png_file?(path) - return true if path.downcase.end_with?(".png") - - File.open(path, "rb") do |f| - sig = f.read(8) - sig == "\x89PNG\r\n\x1a\n".b - end - rescue Errno::ENOENT, Errno::EACCES => e - warn "open #{path}: #{e.message}" - false - end - - def svg_file?(path) - path.downcase.end_with?(".svg") - end - - def image_kind(path) - return :png if png_file?(path) - return :svg if svg_file?(path) - - nil - end - - def parse(argv) - opts_hash = default_cli_opts - build_cli_parser(opts_hash).parse!(argv) - return nil unless threshold_option_ok?(opts_hash) - return nil unless usb_pid_option_ok?(opts_hash) - - opts_hash.delete(:usb_pid_invalid) - opts_hash - end - - def default_cli_opts - { - file: nil, - threshold: nil, - usb_pid: nil, - usb_pid_invalid: false, - dry_run: false, - status: false, - version: false, - help: false - } - end - - def pid_option_description - p900 = Libptouch::USB_PID_PTP900W - p750 = Libptouch::USB_PID_PTP750W - p710 = Libptouch::USB_PID_PTP710BT - "USB 製品 ID(16 進可)。既定 P900W 0x#{p900.to_s(16)}; " \ - "P750W 0x#{p750.to_s(16)}; P710BT 0x#{p710.to_s(16)}" - end - - def apply_usb_pid_option(opts_hash, pid_str) - opts_hash[:usb_pid] = Integer(pid_str, 0) - rescue ArgumentError - warn "invalid --pid: #{pid_str.inspect}" - opts_hash[:usb_pid_invalid] = true - end - - def threshold_option_ok?(opts_hash) - return true if opts_hash[:threshold].nil? || (0..255).cover?(opts_hash[:threshold]) - - warn "-t must be 0..255" - false - end - - def usb_pid_option_ok?(opts_hash) - return false if opts_hash[:usb_pid_invalid] - return true if opts_hash[:usb_pid].nil? - return true if opts_hash[:usb_pid].between?(1, 0xFFFF) - - warn "-p/--pid must be 1..0xFFFF" - false - end - - def build_cli_parser(opts_hash) - OptionParser.new do |p| - p.banner = usage_banner - p.separator "" - p.on("-f", "--file PATH", "入力 PNG/SVG ファイル") { |v| opts_hash[:file] = v } - p.on("-t", "--threshold N", Integer, - "二値化しきい値 0–255(既定 #{Libptouch::PNG_DEFAULT_THRESHOLD}、PNG/SVG)") do |v| - opts_hash[:threshold] = v - end - p.on("-p", "--pid PID", pid_option_description) do |v| - apply_usb_pid_option(opts_hash, v) - end - p.on("-n", "--dry-run", "読み込みと検証のみ(USB なし)") { opts_hash[:dry_run] = true } - p.on("-S", "--status", "USB プリンタのステータスを JSON で表示して終了") do - opts_hash[:status] = true - end - p.on("-V", "--version", "バージョンを表示して終了") { opts_hash[:version] = true } - p.on("-h", "--help", "このヘルプ") { opts_hash[:help] = true } - end - end - - def usage_banner - <<~BANNER - Usage: ptouch-print-png [options] - - PNG/SVG 対応。幅・高さは画像から取得します(-w/-H はありません)。 - SVG は現在テープ幅に合わせて自動拡大・縮小します(USB 接続必須)。 - --status のときは -f は不要です。 - BANNER - end - - def warn_unused_file_options(opts) - return unless opts[:file] || opts[:dry_run] || !opts[:threshold].nil? - - warn "warning: options other than --status are ignored" - end - - def run_version - puts "ptouch-print-png #{Libptouch::VERSION}" - 0 - end - - def run_help - puts usage_banner - puts "" - puts parser_help_text - 0 - end - - def parser_help_text - opts_hash = default_cli_opts - p = OptionParser.new do |parser| - parser.banner = "ptouch-print-png [options]" - parser.on("-f", "--file PATH", "入力 PNG/SVG ファイル") { |v| opts_hash[:file] = v } - parser.on("-t", "--threshold N", Integer, - "二値化しきい値 0–255(既定 #{Libptouch::PNG_DEFAULT_THRESHOLD}、PNG/SVG)") do |v| - opts_hash[:threshold] = v - end - parser.on("-p", "--pid PID", pid_option_description) do |v| - apply_usb_pid_option(opts_hash, v) - end - parser.on("-n", "--dry-run", "読み込みと検証のみ(USB なし)") { opts_hash[:dry_run] = true } - parser.on("-S", "--status", "USB プリンタのステータスを JSON で表示して終了") do - opts_hash[:status] = true - end - parser.on("-V", "--version", "バージョンを表示して終了") { opts_hash[:version] = true } - parser.on("-h", "--help", "このヘルプ") { opts_hash[:help] = true } - end - p.help - end - - def open_usb_for_opts(ctx, opts) - if opts[:usb_pid] - ctx.open_usb_vid_pid(Libptouch::USB_VID_BROTHER, opts[:usb_pid]) - else - ctx.open_usb - end - end - - def run_status(opts) - ctx = nil - begin - ctx = Libptouch::Context.new - open_usb_for_opts(ctx, opts) - h = Libptouch.parse_status(ctx.status_bytes) - h.delete(:raw_bytes) - puts JSON.pretty_generate(h) - 0 - rescue Libptouch::Error => e - warn "get_status: #{e.message}" - 1 - ensure - ctx&.dispose - end - end - - def run_print(path, opts, kind) - ctx = nil - begin - ctx = Libptouch::Context.new - threshold = opts[:threshold] - data, width, height = if kind == :svg - open_usb_for_opts(ctx, opts) - if threshold.nil? - ctx.svg_file_to_raster_fit_current_tape(path) - else - ctx.svg_file_to_raster_fit_current_tape(path, threshold: threshold) - end - elsif threshold.nil? - ctx.png_file_to_raster(path) - else - ctx.png_file_to_raster(path, threshold: threshold) - end - - ctx.check_raster(data, width_dots: width, height_dots: height) - - if opts[:dry_run] - puts "dry-run OK: #{data.bytesize} bytes, src #{width}x#{height} dots (print lengthxwidth #{width}x#{height})" - return 0 - end - - open_usb_for_opts(ctx, opts) if kind == :png - ctx.print_raster(data, width_dots: width, height_dots: height) - 0 - rescue Libptouch::Error => e - warn e.message - 1 - ensure - ctx&.dispose - end - end - - def usage_error(msg) - warn msg - warn "(try ptouch-print-png --help)" - 2 - end - end + PngPrint = LabelPrint end end diff --git a/ruby/lib/libptouch/context.rb b/ruby/lib/libptouch/context.rb index 463d611..dc6e246 100644 --- a/ruby/lib/libptouch/context.rb +++ b/ruby/lib/libptouch/context.rb @@ -134,7 +134,7 @@ module Libptouch print_dpi: info[:print_dpi], feed_dpi: info[:feed_dpi], tape_width_mm: info[:tape_width_mm], - printable_dots: info[:printable_dots], + printable_height_dots: info[:printable_dots], left_margin_dots: info[:left_margin_dots], right_margin_dots: info[:right_margin_dots], min_feed_dots: info[:min_feed_dots], diff --git a/ruby/libptouch.gemspec b/ruby/libptouch.gemspec index 2296f1a..fd5f35a 100644 --- a/ruby/libptouch.gemspec +++ b/ruby/libptouch.gemspec @@ -25,7 +25,7 @@ Gem::Specification.new do |spec| spec.files = Dir.chdir(__dir__) { Dir["lib/**/*.rb", "exe/*", "README.md"] } spec.bindir = "exe" - spec.executables = ["ptouch-print-png"] + spec.executables = ["ptouch-label", "ptouch-print-png"] spec.require_paths = ["lib"] spec.add_dependency "ffi", "~> 1.15" diff --git a/samples/README.md b/samples/README.md index 22635c0..8ad2b90 100644 --- a/samples/README.md +++ b/samples/README.md @@ -1,9 +1,16 @@ # samples -試験・デモ用のサンプル画像(PNG など)を置くディレクトリです。リポジトリに含める場合はライセンス・著作権に注意してください。 +試験・デモ用のサンプル画像(PNG/SVG)や差込印刷データ(JSON/YAML)を置くディレクトリです。リポジトリに含める場合はライセンス・著作権に注意してください。 例(ドライラン): ```bash ./build/ptouch-print -n -f samples/your.png ``` + +差込印刷の例(Ruby CLI): + +```bash +ruby -I ruby/lib ruby/exe/ptouch-label -n --template samples/merge_template.svg --data samples/merge_data.yml +ruby -I ruby/lib ruby/exe/ptouch-label -n --template samples/merge_template.svg --data samples/merge_data.json +``` diff --git a/samples/merge_data.json b/samples/merge_data.json new file mode 100644 index 0000000..2ba76f5 --- /dev/null +++ b/samples/merge_data.json @@ -0,0 +1,5 @@ +{ + "title": "出荷ラベル", + "left": "品番 ABC-123", + "right": "数量 24" +} diff --git a/samples/merge_data.yml b/samples/merge_data.yml new file mode 100644 index 0000000..44042fa --- /dev/null +++ b/samples/merge_data.yml @@ -0,0 +1,3 @@ +title: "出荷ラベル" +left: "品番 ABC-123" +right: "数量 24" diff --git a/samples/merge_template.svg b/samples/merge_template.svg new file mode 100644 index 0000000..09dbe64 --- /dev/null +++ b/samples/merge_template.svg @@ -0,0 +1,9 @@ + + + TITLE + + LEFT + / + RIGHT + + From 094f1839942835af4658c436c6012fd5ccdab55a Mon Sep 17 00:00:00 2001 From: knb Date: Thu, 16 Apr 2026 19:09:37 +0900 Subject: [PATCH 2/2] =?UTF-8?q?ptouch-label/ptouch-print=20=E3=81=AE?= =?UTF-8?q?=E3=82=AA=E3=83=97=E3=82=B7=E3=83=A7=E3=83=B3=E6=95=B4=E7=90=86?= =?UTF-8?q?=E3=81=A8=20trim-right=20=E5=AF=BE=E5=BF=9C=E3=82=92=E5=8F=8D?= =?UTF-8?q?=E6=98=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit CLIヘルプ文言を簡潔化しつつ、右余白トリム機能と関連API・ドキュメント更新をまとめて取り込み、PNG/SVG/テンプレート経路での利用体験を揃える。 Made-with: Cursor --- CMakeLists.txt | 1 + README.md | 3 + include/libptouch.h | 10 +++ ruby/Gemfile | 2 +- ruby/Gemfile.lock | 5 +- ruby/README.md | 5 +- ruby/exe/ptouch-label | 0 ruby/lib/libptouch/binding.rb | 2 + ruby/lib/libptouch/cli/label_print.rb | 70 +++++++++++++++++---- ruby/lib/libptouch/context.rb | 23 +++++++ ruby/libptouch.gemspec | 1 + src/cli/main.c | 90 ++++++++++++++++++++++++--- src/lib/libptouch_trim.c | 84 +++++++++++++++++++++++++ 13 files changed, 272 insertions(+), 24 deletions(-) mode change 100644 => 100755 ruby/exe/ptouch-label create mode 100644 src/lib/libptouch_trim.c diff --git a/CMakeLists.txt b/CMakeLists.txt index 7e5ad7f..2eecb03 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -27,6 +27,7 @@ set(LIBPTOUCH_SOURCES src/lib/libptouch_usb.c src/lib/libptouch_layout.c src/lib/libptouch_media_info.c + src/lib/libptouch_trim.c src/lib/libptouch_print.c src/lib/libptouch_status.c src/lib/libptouch_png.c diff --git a/README.md b/README.md index 9ed9e45..4069a70 100644 --- a/README.md +++ b/README.md @@ -40,6 +40,7 @@ cmake --build build **PNG**(拡張子 `.png` または PNG シグネチャ)の場合は幅・高さは画像から取得します。 **SVG**(拡張子 `.svg`)の場合は現在装着中テープの印字可能ドット幅に合わせて自動拡大・縮小します(USB 接続が必要)。 +`--trim-right[=DOTS]` で右側空白列を削減できます(DOTS 省略時は左余白ドット、取得失敗時は 0)。 任意で `-t`(0–255)で二値化しきい値を指定できます。 **1bit packed ラスター**(行優先、行あたり `ceil(width/8)` バイト × 行数)の場合は `-f` に加え `-w` / `-H` が必須です。 @@ -49,6 +50,8 @@ cmake --build build ```bash # PNG — 検証のみ(USB 不要) ./build/ptouch-print -n -f label.png +./build/ptouch-print -n -f label.png --trim-right +./build/ptouch-print -n -f label.png --trim-right=20 # SVG — 幅を現在テープにフィットさせて検証(USB 必要) ./build/ptouch-print -n -f label.svg diff --git a/include/libptouch.h b/include/libptouch.h index 0b32912..e4637a9 100644 --- a/include/libptouch.h +++ b/include/libptouch.h @@ -100,6 +100,16 @@ libptouch_err_t libptouch_check_raster(libptouch_ctx *ctx, libptouch_err_t libptouch_get_current_media_info(libptouch_ctx *ctx, libptouch_media_info_t *out_info); +/** + * 1bit packed ラスターの右側空白列を削減する(USB 不要)。 + * 入力全体が白の場合は元サイズを維持して返す。 + */ +libptouch_err_t libptouch_trim_right_blank_columns( + libptouch_ctx *ctx, const uint8_t *data, size_t data_len, + const libptouch_raster_params_t *in_params, uint16_t right_padding_dots, + uint8_t **out_raster, + size_t *out_raster_bytes, libptouch_raster_params_t *out_params); + /** * 1 ビット packed ラスターを USB で印刷(各機種のラスター PDF 準拠)。 * 印字前にステータスでテープ幅を読み、印刷可能ドット内に画像を中央配置する。 diff --git a/ruby/Gemfile b/ruby/Gemfile index 8f2506c..f23a797 100644 --- a/ruby/Gemfile +++ b/ruby/Gemfile @@ -5,5 +5,5 @@ source "https://rubygems.org" gemspec group :development do - gem "rubocop", "~> 1.69", require: false + gem "rubocop", "~> 1.86", require: false end diff --git a/ruby/Gemfile.lock b/ruby/Gemfile.lock index 107c760..7cb5d92 100644 --- a/ruby/Gemfile.lock +++ b/ruby/Gemfile.lock @@ -3,6 +3,7 @@ PATH specs: libptouch (1.0.0) ffi (~> 1.15) + rexml GEM remote: https://rubygems.org/ @@ -30,6 +31,7 @@ GEM racc (1.8.1) rainbow (3.1.1) regexp_parser (2.12.0) + rexml (3.4.4) rubocop (1.86.1) json (~> 2.3) language_server-protocol (~> 3.17.0.2) @@ -64,7 +66,7 @@ PLATFORMS DEPENDENCIES libptouch! - rubocop (~> 1.69) + rubocop (~> 1.86) CHECKSUMS ast (2.4.3) sha256=954615157c1d6a382bc27d690d973195e79db7f55e9765ac7c481c60bdb4d383 @@ -89,6 +91,7 @@ CHECKSUMS racc (1.8.1) sha256=4a7f6929691dbec8b5209a0b373bc2614882b55fc5d2e447a21aaa691303d62f rainbow (3.1.1) sha256=039491aa3a89f42efa1d6dec2fc4e62ede96eb6acd95e52f1ad581182b79bc6a regexp_parser (2.12.0) sha256=35a916a1d63190ab5c9009457136ae5f3c0c7512d60291d0d1378ba18ce08ebb + rexml (3.4.4) sha256=19e0a2c3425dfbf2d4fc1189747bdb2f849b6c5e74180401b15734bc97b5d142 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 360755f..bd2ab7d 100644 --- a/ruby/README.md +++ b/ruby/README.md @@ -39,6 +39,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` 属性をキーにした差込印刷が可能です。 +`--trim-right[=DOTS]` を付けると、libptouch 側の共通処理でラベル右側の空白ドット列を削減します。`DOTS` 省略時は左余白ドット数を使い、取得失敗時は `0` にフォールバックします。 開発ツリーからそのまま試す例: @@ -46,6 +47,8 @@ SVG は現在装着テープの印字可能幅に合わせて自動拡大・縮 bundle exec ruby -I lib exe/ptouch-label --help bundle exec ruby -I lib exe/ptouch-label -n -f ../samples/your.png bundle exec ruby -I lib exe/ptouch-label -n -f ../samples/your.svg +bundle exec ruby -I lib exe/ptouch-label -n -f ../samples/your.svg --trim-right +bundle exec ruby -I lib exe/ptouch-label -n -f ../samples/your.svg --trim-right=20 bundle exec ruby -I lib exe/ptouch-label -n --template ../samples/your_template.svg --data ../samples/your_data.yml bundle exec ruby -I lib exe/ptouch-label --media-info bundle exec ruby -I lib exe/ptouch-label --status -p 0x2062 @@ -96,7 +99,7 @@ ctx.dispose ## API の範囲 -- 実行ファイル `ptouch-label`(互換: `ptouch-print-png`)… PNG/SVG(`-f`, `-t`, `-p`, `-n`, `-M`, `-S`, `-V`, `-h`, `--template`, `--data`)。`-M` は現在テープ情報(幅 mm・DPI 等)を JSON 出力、`-S` はステータス JSON 出力 +- 実行ファイル `ptouch-label`(互換: `ptouch-print-png`)… PNG/SVG(`-f`, `-t`, `-p`, `-n`, `-M`, `-S`, `-V`, `-h`, `--template`, `--data`, `--trim-right[=DOTS]`)。`-M` は現在テープ情報(幅 mm・DPI 等)を JSON 出力、`-S` はステータス JSON 出力、`--trim-right` は右側空白列を削減 - `Libptouch::Context` … `open_usb` / `open_usb_vid_pid` / `close` / `dispose` - `check_raster` / `print_raster` / `png_file_to_raster` / `svg_file_to_raster_fit_current_tape` / `status_bytes` / `status_hash` / `current_media_info` - `current_media_info` には `print_dpi` / `feed_dpi` / `tape_width_mm` / `printable_height_dots` / `min_feed_mm` などを含む(テープ幅方向は `printable_height_dots`) diff --git a/ruby/exe/ptouch-label b/ruby/exe/ptouch-label old mode 100644 new mode 100755 diff --git a/ruby/lib/libptouch/binding.rb b/ruby/lib/libptouch/binding.rb index e922153..ae8e29f 100644 --- a/ruby/lib/libptouch/binding.rb +++ b/ruby/lib/libptouch/binding.rb @@ -59,6 +59,8 @@ module Libptouch attach_function :libptouch_close, [:pointer], :void attach_function :libptouch_check_raster, %i[pointer pointer size_t pointer], :int attach_function :libptouch_get_current_media_info, %i[pointer pointer], :int + attach_function :libptouch_trim_right_blank_columns, + %i[pointer pointer size_t pointer uint16 pointer pointer pointer], :int attach_function :libptouch_print_raster, %i[pointer pointer size_t pointer], :int attach_function :libptouch_png_file_to_raster, %i[pointer string pointer pointer pointer pointer], :int diff --git a/ruby/lib/libptouch/cli/label_print.rb b/ruby/lib/libptouch/cli/label_print.rb index c89ff5f..3aafb40 100644 --- a/ruby/lib/libptouch/cli/label_print.rb +++ b/ruby/lib/libptouch/cli/label_print.rb @@ -70,6 +70,7 @@ module Libptouch opts_hash = default_cli_opts build_cli_parser(opts_hash).parse!(argv) return nil unless threshold_option_ok?(opts_hash) + return nil unless trim_right_option_ok?(opts_hash) return nil unless usb_pid_option_ok?(opts_hash) opts_hash.delete(:usb_pid_invalid) @@ -85,6 +86,7 @@ module Libptouch usb_pid: nil, usb_pid_invalid: false, dry_run: false, + trim_right: nil, media_info: false, status: false, version: false, @@ -114,6 +116,15 @@ module Libptouch false end + def trim_right_option_ok?(opts_hash) + v = opts_hash[:trim_right] + return true if v.nil? || v == :auto + return true if v.is_a?(Integer) && v >= 0 + + warn "--trim-right must be omitted, or >= 0" + false + end + def usb_pid_option_ok?(opts_hash) return false if opts_hash[:usb_pid_invalid] return true if opts_hash[:usb_pid].nil? @@ -131,17 +142,21 @@ module Libptouch p.on("--template PATH", "差込用 SVG テンプレート") { |v| opts_hash[:template] = v } p.on("--data PATH", "差込データ JSON/YAML ファイル") { |v| opts_hash[:data] = v } p.on("-t", "--threshold N", Integer, - "二値化しきい値 0–255(既定 #{Libptouch::PNG_DEFAULT_THRESHOLD}、PNG/SVG)") do |v| + "しきい値 0–255(既定 #{Libptouch::PNG_DEFAULT_THRESHOLD}、PNG/SVG)") do |v| opts_hash[:threshold] = v end p.on("-p", "--pid PID", pid_option_description) do |v| apply_usb_pid_option(opts_hash, v) end p.on("-n", "--dry-run", "読み込みと検証のみ(USB なし)") { opts_hash[:dry_run] = true } - p.on("-M", "--media-info", "現在テープの幅(mm)・DPI・最小余白(mm)を JSON で表示して終了") do + p.on("--trim-right[=DOTS]", Integer, + "右側空白を削減。DOTS省略時は左余白(失敗時 0)") do |v| + opts_hash[:trim_right] = v.nil? ? :auto : v + end + p.on("-M", "--media-info", "現在テープ情報(幅/DPI/余白)を JSON で表示して終了") do opts_hash[:media_info] = true end - p.on("-S", "--status", "USB プリンタのステータスを JSON で表示して終了") do + p.on("-S", "--status", "ステータスを JSON で表示して終了") do opts_hash[:status] = true end p.on("-V", "--version", "バージョンを表示して終了") { opts_hash[:version] = true } @@ -153,15 +168,16 @@ module Libptouch <<~BANNER Usage: ptouch-label [options] - PNG/SVG 対応。幅・高さは画像から取得します(-w/-H はありません)。 - または --template/--data で SVG 差込印刷ができます。 - SVG は現在テープ幅に合わせて自動拡大・縮小します(USB 接続必須)。 + PNG/SVG 対応。画像サイズを使用します(-w/-H はありません)。 + --template/--data で SVG 差込印刷ができます。 + SVG は現在テープ幅に自動フィットします(USB 必須)。 + --trim-right[=DOTS] で右側空白を削減します(省略時は左余白基準)。 --status / --media-info のときは -f は不要です。 BANNER end def warn_unused_file_options(opts) - return unless opts[:file] || opts[:template] || opts[:data] || opts[:dry_run] || !opts[:threshold].nil? + return unless opts[:file] || opts[:template] || opts[:data] || opts[:dry_run] || !opts[:trim_right].nil? || !opts[:threshold].nil? warn "warning: options other than --status are ignored" end @@ -186,17 +202,21 @@ module Libptouch parser.on("--template PATH", "差込用 SVG テンプレート") { |v| opts_hash[:template] = v } parser.on("--data PATH", "差込データ JSON/YAML ファイル") { |v| opts_hash[:data] = v } parser.on("-t", "--threshold N", Integer, - "二値化しきい値 0–255(既定 #{Libptouch::PNG_DEFAULT_THRESHOLD}、PNG/SVG)") do |v| + "しきい値 0–255(既定 #{Libptouch::PNG_DEFAULT_THRESHOLD}、PNG/SVG)") do |v| opts_hash[:threshold] = v end parser.on("-p", "--pid PID", pid_option_description) do |v| apply_usb_pid_option(opts_hash, v) end parser.on("-n", "--dry-run", "読み込みと検証のみ(USB なし)") { opts_hash[:dry_run] = true } - parser.on("-M", "--media-info", "現在テープの幅(mm)・DPI・最小余白(mm)を JSON で表示して終了") do + parser.on("--trim-right[=DOTS]", Integer, + "右側空白を削減。DOTS省略時は左余白(失敗時 0)") do |v| + opts_hash[:trim_right] = v.nil? ? :auto : v + end + parser.on("-M", "--media-info", "現在テープ情報(幅/DPI/余白)を JSON で表示して終了") do opts_hash[:media_info] = true end - parser.on("-S", "--status", "USB プリンタのステータスを JSON で表示して終了") do + parser.on("-S", "--status", "ステータスを JSON で表示して終了") do opts_hash[:status] = true end parser.on("-V", "--version", "バージョンを表示して終了") { opts_hash[:version] = true } @@ -319,9 +339,11 @@ module Libptouch ctx = nil begin ctx = Libptouch::Context.new + usb_opened = false threshold = opts[:threshold] data, width, height = if kind == :svg open_usb_for_opts(ctx, opts) + usb_opened = true if threshold.nil? ctx.svg_file_to_raster_fit_current_tape(path) else @@ -333,6 +355,16 @@ module Libptouch ctx.png_file_to_raster(path, threshold: threshold) end + unless opts[:trim_right].nil? + trim_pad, usb_opened = resolve_trim_right_pad_dots(ctx, opts, usb_opened) + data, width, height = ctx.trim_right_blank_columns( + data, + width_dots: width, + height_dots: height, + right_padding_dots: trim_pad + ) + end + ctx.check_raster(data, width_dots: width, height_dots: height) if opts[:dry_run] @@ -340,7 +372,7 @@ module Libptouch return 0 end - open_usb_for_opts(ctx, opts) if kind == :png + open_usb_for_opts(ctx, opts) if kind == :png && !usb_opened ctx.print_raster(data, width_dots: width, height_dots: height) 0 rescue Libptouch::Error => e @@ -351,6 +383,22 @@ module Libptouch end end + def resolve_trim_right_pad_dots(ctx, opts, usb_opened) + trim = opts[:trim_right] + return [trim, usb_opened] if trim.is_a?(Integer) + + begin + unless usb_opened + open_usb_for_opts(ctx, opts) + usb_opened = true + end + info = ctx.current_media_info + [Integer(info[:left_margin_dots] || 0), usb_opened] + rescue Libptouch::Error + [0, usb_opened] + end + end + def usage_error(msg) warn msg warn "(try ptouch-label --help)" diff --git a/ruby/lib/libptouch/context.rb b/ruby/lib/libptouch/context.rb index dc6e246..823b986 100644 --- a/ruby/lib/libptouch/context.rb +++ b/ruby/lib/libptouch/context.rb @@ -115,6 +115,29 @@ module Libptouch [bytes, out_params[:width_dots], out_params[:height_dots]] end + def trim_right_blank_columns(data, width_dots:, height_dots:, margin_mm: 0, right_padding_dots: 0) + in_params = Binding::RasterParams.new + in_params[:width_dots] = width_dots + in_params[:height_dots] = height_dots + in_params[:margin_mm] = margin_mm + in_buf = FFI::MemoryPointer.new(:uint8, data.bytesize) + in_buf.put_bytes(0, data) + out_pp = FFI::MemoryPointer.new(:pointer) + out_len = FFI::MemoryPointer.new(:size_t) + out_params = Binding::RasterParams.new + raise_on_error(Binding.libptouch_trim_right_blank_columns( + @native, in_buf, data.bytesize, in_params.pointer, + right_padding_dots, out_pp, out_len, out_params.pointer + )) + raw = out_pp.read_pointer + raise Libptouch::Error.new(OK, "null raster from trim_right_blank_columns") if raw.null? + + len = out_len.get(:size_t, 0) + bytes = raw.read_bytes(len) + Binding.libptouch_free_raster(raw) + [bytes, out_params[:width_dots], out_params[:height_dots]] + end + def status_bytes buf = FFI::MemoryPointer.new(:uint8, STATUS_LENGTH) raise_on_error(Binding.libptouch_get_status(@native, buf)) diff --git a/ruby/libptouch.gemspec b/ruby/libptouch.gemspec index fd5f35a..2881424 100644 --- a/ruby/libptouch.gemspec +++ b/ruby/libptouch.gemspec @@ -29,4 +29,5 @@ Gem::Specification.new do |spec| spec.require_paths = ["lib"] spec.add_dependency "ffi", "~> 1.15" + spec.add_dependency "rexml" end diff --git a/src/cli/main.c b/src/cli/main.c index db99c12..fbf8fdb 100644 --- a/src/cli/main.c +++ b/src/cli/main.c @@ -21,15 +21,16 @@ static void usage(const char *argv0) " -f, --file PATH 入力ファイル\n" " -w, --width DOTS 1bit ラスター時: 幅(ドット)\n" " -H, --height DOTS 1bit ラスター時: 高さ(ドット)\n" - " -t, --threshold N 二値化しきい値 0–255(既定 %u、PNG/SVG)\n" + " -t, --threshold N しきい値 0–255(既定 %u、PNG/SVG)\n" + " --trim-right[=DOTS] 右側空白を削減(省略時は左余白、失敗時 0)\n" " -n, --dry-run 読み込みと check_raster のみ(USB なし)\n" " -p, --pid HEX USB 製品 ID(既定: P900W の 0x2085)。例: P750W 0x2062、P710BT 0x20af\n" - " -S, --status USB 接続プリンタのステータス(テープ種・幅・色等)を表示して終了\n" + " -S, --status ステータスを表示して終了\n" " -V, --version バージョンを表示して終了\n" " -h, --help このヘルプ\n" "\n" - "PNG は幅・高さを画像から取得(-w/-H 不要)。\n" - "SVG は現在テープの印字可能幅に合わせて自動拡大・縮小(USB 必須)。\n" + "PNG は画像サイズを使用(-w/-H 不要)。\n" + "SVG は現在テープ幅に自動フィット(USB 必須)。\n" "1bit packed ラスター(行優先)の場合は -f -w -H が必須。\n" "--status のときは -f は不要(他オプションは無視されます)。\n", argv0, (unsigned)LIBPTOUCH_PNG_DEFAULT_THRESHOLD); @@ -107,6 +108,9 @@ int main(int argc, char **argv) unsigned threshold = LIBPTOUCH_PNG_DEFAULT_THRESHOLD; unsigned usb_pid_arg = 0; int dry_run = 0; + int trim_right_enabled = 0; + int trim_right_auto = 0; + unsigned trim_right_dots = 0; int has_threshold = 0; int want_status = 0; static struct option longopts[] = { @@ -114,6 +118,7 @@ int main(int argc, char **argv) { "height", required_argument, NULL, 'H' }, { "file", required_argument, NULL, 'f' }, { "threshold", required_argument, NULL, 't' }, + { "trim-right", optional_argument, NULL, 'r' }, { "dry-run", no_argument, NULL, 'n' }, { "pid", required_argument, NULL, 'p' }, { "status", no_argument, NULL, 'S' }, @@ -123,7 +128,7 @@ int main(int argc, char **argv) }; int c; - while ((c = getopt_long(argc, argv, "w:H:f:t:p:nhSV", longopts, NULL)) != + while ((c = getopt_long(argc, argv, "w:H:f:t:r::p:nhSV", longopts, NULL)) != -1) { switch (c) { case 'w': @@ -146,6 +151,20 @@ int main(int argc, char **argv) case 'n': dry_run = 1; break; + case 'r': + trim_right_enabled = 1; + if (!optarg) { + trim_right_auto = 1; + trim_right_dots = 0; + break; + } + trim_right_auto = 0; + trim_right_dots = (unsigned)strtoul(optarg, NULL, 10); + if (trim_right_dots > 0xFFFFu) { + fprintf(stderr, "--trim-right must be 0..65535\n"); + return 2; + } + break; case 'p': usb_pid_arg = (unsigned)strtoul(optarg, NULL, 0); if (usb_pid_arg == 0u || usb_pid_arg > 0xFFFFu) { @@ -238,6 +257,7 @@ int main(int argc, char **argv) libptouch_raster_params_t params = { 0, 0, 0 }; libptouch_err_t e; int usb_opened = 0; + int data_from_lib = 0; if (png) { libptouch_png_options_t opt = { .threshold = (uint8_t)threshold }; @@ -250,6 +270,7 @@ int main(int argc, char **argv) libptouch_destroy(ctx); return 1; } + data_from_lib = 1; } else if (svg) { e = usb_pid_arg != 0 ? libptouch_open_usb_vid_pid(ctx, LIBPTOUCH_USB_VID_BROTHER, @@ -272,6 +293,7 @@ int main(int argc, char **argv) libptouch_destroy(ctx); return 1; } + data_from_lib = 1; } else { if (read_file(file, &data, &data_len) != 0) { libptouch_destroy(ctx); @@ -282,12 +304,60 @@ int main(int argc, char **argv) params.margin_mm = 0; } + if (trim_right_enabled) { + uint16_t pad = (uint16_t)trim_right_dots; + if (trim_right_auto) { + pad = 0u; + if (!usb_opened) { + e = usb_pid_arg != 0 + ? libptouch_open_usb_vid_pid( + ctx, LIBPTOUCH_USB_VID_BROTHER, + (uint16_t)usb_pid_arg) + : libptouch_open_usb(ctx); + if (e == LIBPTOUCH_OK) + usb_opened = 1; + } + if (usb_opened) { + libptouch_media_info_t mi; + if (libptouch_get_current_media_info(ctx, &mi) == + LIBPTOUCH_OK) { + pad = mi.left_margin_dots; + } + } + } + + uint8_t *trimmed = NULL; + size_t trimmed_len = 0; + libptouch_raster_params_t trimmed_params = { 0, 0, 0 }; + e = libptouch_trim_right_blank_columns( + ctx, data, data_len, ¶ms, pad, &trimmed, &trimmed_len, + &trimmed_params); + if (e != LIBPTOUCH_OK) { + fprintf(stderr, "trim_right_blank_columns: %s\n", + libptouch_strerror(ctx)); + libptouch_destroy(ctx); + if (data_from_lib) + libptouch_free_raster(data); + else + free(data); + return 1; + } + if (data_from_lib) + libptouch_free_raster(data); + else + free(data); + data = trimmed; + data_len = trimmed_len; + params = trimmed_params; + data_from_lib = 1; + } + e = libptouch_check_raster(ctx, data, data_len, ¶ms); if (e != LIBPTOUCH_OK) { fprintf(stderr, "check_raster: %s\n", libptouch_strerror(ctx)); libptouch_destroy(ctx); - if (png || svg) + if (data_from_lib) libptouch_free_raster(data); else free(data); @@ -304,7 +374,7 @@ int main(int argc, char **argv) if (usb_opened) libptouch_close(ctx); libptouch_destroy(ctx); - if (png || svg) + if (data_from_lib) libptouch_free_raster(data); else free(data); @@ -319,7 +389,7 @@ int main(int argc, char **argv) if (e != LIBPTOUCH_OK) { fprintf(stderr, "open_usb: %s\n", libptouch_strerror(ctx)); libptouch_destroy(ctx); - if (png || svg) + if (data_from_lib) libptouch_free_raster(data); else free(data); @@ -333,7 +403,7 @@ int main(int argc, char **argv) libptouch_strerror(ctx)); libptouch_close(ctx); libptouch_destroy(ctx); - if (png || svg) + if (data_from_lib) libptouch_free_raster(data); else free(data); @@ -342,7 +412,7 @@ int main(int argc, char **argv) libptouch_close(ctx); libptouch_destroy(ctx); - if (png || svg) + if (data_from_lib) libptouch_free_raster(data); else free(data); diff --git a/src/lib/libptouch_trim.c b/src/lib/libptouch_trim.c new file mode 100644 index 0000000..7ec254b --- /dev/null +++ b/src/lib/libptouch_trim.c @@ -0,0 +1,84 @@ +/* + * libptouch — raster right blank trim + * + * Author: knb + * Email: knb@artif.org + */ + +#include "libptouch_internal.h" + +#include +#include + +libptouch_err_t libptouch_trim_right_blank_columns( + libptouch_ctx *ctx, const uint8_t *data, size_t data_len, + const libptouch_raster_params_t *in_params, uint16_t right_padding_dots, + uint8_t **out_raster, size_t *out_raster_bytes, + libptouch_raster_params_t *out_params) +{ + if (!ctx || !data || !in_params || !out_raster || !out_raster_bytes || + !out_params) { + if (ctx) + ptouch_set_error(ctx, LIBPTOUCH_ERR_ARG, "null argument"); + return LIBPTOUCH_ERR_ARG; + } + + libptouch_err_t v = libptouch_check_raster(ctx, data, data_len, in_params); + if (v != LIBPTOUCH_OK) + return v; + + uint32_t width = in_params->width_dots; + uint32_t height = in_params->height_dots; + size_t row_bytes = ((size_t)width + 7u) / 8u; + int32_t rightmost = -1; + + for (uint32_t y = 0; y < height; y++) { + const uint8_t *row = data + (size_t)y * row_bytes; + for (int32_t x = (int32_t)width - 1; x >= 0; x--) { + uint8_t b = row[(size_t)x / 8u]; + uint8_t bit = (uint8_t)(7u - ((uint32_t)x % 8u)); + if (((b >> bit) & 1u) == 0) + continue; + if (x > rightmost) + rightmost = x; + break; + } + } + + uint32_t new_width = width; + if (rightmost >= 0) { + uint32_t trimmed = (uint32_t)(rightmost + 1); + uint32_t padded = trimmed + (uint32_t)right_padding_dots; + if (padded > width) + padded = width; + if (padded < width) + new_width = padded; + } + + size_t new_row_bytes = ((size_t)new_width + 7u) / 8u; + size_t total = new_row_bytes * (size_t)height; + uint8_t *out = (uint8_t *)calloc(1u, total); + if (!out) { + ptouch_set_error(ctx, LIBPTOUCH_ERR_NOMEM, "calloc raster failed"); + return LIBPTOUCH_ERR_NOMEM; + } + + uint8_t last_mask = 0xFFu; + if ((new_width % 8u) != 0u) + last_mask = (uint8_t)((0xFFu << (8u - (new_width % 8u))) & 0xFFu); + + for (uint32_t y = 0; y < height; y++) { + const uint8_t *src = data + (size_t)y * row_bytes; + uint8_t *dst = out + (size_t)y * new_row_bytes; + memcpy(dst, src, new_row_bytes); + dst[new_row_bytes - 1u] &= last_mask; + } + + *out_raster = out; + *out_raster_bytes = total; + out_params->width_dots = new_width; + out_params->height_dots = height; + out_params->margin_mm = in_params->margin_mm; + ptouch_set_error(ctx, LIBPTOUCH_OK, ""); + return LIBPTOUCH_OK; +}