ruby binding 追加
- FFI gem (libptouch)、exe ptouch-print-png(PNG のみ) - ステータス 32 バイトを Hash に展開(parse_status / status_hash) - CMake: libptouch 共有ライブラリ(ptouch_shared) - RuboCop、gemspec(homepage / source_code_uri) Made-with: Cursor
This commit is contained in:
49
ruby/lib/libptouch/binding.rb
Normal file
49
ruby/lib/libptouch/binding.rb
Normal file
@@ -0,0 +1,49 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
require "ffi"
|
||||
|
||||
module Libptouch
|
||||
module Binding
|
||||
extend FFI::Library
|
||||
|
||||
def self.library_files
|
||||
list = []
|
||||
env = ENV["LIBPTOUCH_LIB"]
|
||||
list << env if env && !env.empty?
|
||||
base = File.expand_path("../../..", __dir__)
|
||||
%w[libptouch.so libptouch.dylib].each do |name|
|
||||
path = File.join(base, "build", name)
|
||||
list << path if File.file?(path)
|
||||
end
|
||||
list << "libptouch"
|
||||
list
|
||||
end
|
||||
|
||||
ffi_lib library_files
|
||||
|
||||
class RasterParams < FFI::Struct
|
||||
layout :width_dots, :uint32,
|
||||
:height_dots, :uint32,
|
||||
:margin_mm, :uint8,
|
||||
:_pad, [:uint8, 3]
|
||||
end
|
||||
|
||||
class PngOptions < FFI::Struct
|
||||
layout :threshold, :uint8
|
||||
end
|
||||
|
||||
attach_function :libptouch_create, [], :pointer
|
||||
attach_function :libptouch_destroy, [:pointer], :void
|
||||
attach_function :libptouch_strerror, [:pointer], :string
|
||||
attach_function :libptouch_last_error, [:pointer], :int
|
||||
attach_function :libptouch_open_usb, [:pointer], :int
|
||||
attach_function :libptouch_open_usb_vid_pid, %i[pointer uint16 uint16], :int
|
||||
attach_function :libptouch_close, [:pointer], :void
|
||||
attach_function :libptouch_check_raster, %i[pointer pointer size_t 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
|
||||
attach_function :libptouch_free_raster, [:pointer], :void
|
||||
attach_function :libptouch_get_status, %i[pointer pointer], :int
|
||||
end
|
||||
end
|
||||
182
ruby/lib/libptouch/cli/png_print.rb
Normal file
182
ruby/lib/libptouch/cli/png_print.rb
Normal file
@@ -0,0 +1,182 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
require "json"
|
||||
require "optparse"
|
||||
|
||||
require "libptouch"
|
||||
|
||||
module Libptouch
|
||||
module Cli
|
||||
# PNG のみを扱う 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
|
||||
end
|
||||
|
||||
return usage_error("-f is required (or use --status)") if opts[:file].to_s.empty?
|
||||
|
||||
path = opts[:file]
|
||||
return usage_error("not a PNG file: #{path}") unless png_file?(path)
|
||||
|
||||
run_print(path, opts)
|
||||
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 parse(argv)
|
||||
o = {
|
||||
file: nil,
|
||||
threshold: nil,
|
||||
dry_run: false,
|
||||
status: false,
|
||||
version: false,
|
||||
help: false
|
||||
}
|
||||
parser = OptionParser.new do |p|
|
||||
p.banner = usage_banner
|
||||
p.separator ""
|
||||
p.on("-f", "--file PATH", "入力 PNG ファイル") { |v| o[:file] = v }
|
||||
p.on("-t", "--threshold N", Integer,
|
||||
"二値化しきい値 0–255(既定 #{Libptouch::PNG_DEFAULT_THRESHOLD})") do |v|
|
||||
o[:threshold] = v
|
||||
end
|
||||
p.on("-n", "--dry-run", "読み込みと検証のみ(USB なし)") { o[:dry_run] = true }
|
||||
p.on("-S", "--status", "USB プリンタのステータスを JSON で表示して終了") { o[:status] = true }
|
||||
p.on("-V", "--version", "バージョンを表示して終了") { o[:version] = true }
|
||||
p.on("-h", "--help", "このヘルプ") { o[:help] = true }
|
||||
end
|
||||
parser.parse!(argv)
|
||||
|
||||
unless o[:threshold].nil? || (0..255).cover?(o[:threshold])
|
||||
warn "-t must be 0..255"
|
||||
return nil
|
||||
end
|
||||
|
||||
o
|
||||
end
|
||||
|
||||
def usage_banner
|
||||
<<~BANNER
|
||||
Usage: ptouch-print-png [options]
|
||||
|
||||
PNG のみ対応。幅・高さは画像から取得します(-w/-H はありません)。
|
||||
--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
|
||||
o = {
|
||||
file: nil,
|
||||
threshold: nil,
|
||||
dry_run: false,
|
||||
status: false,
|
||||
version: false,
|
||||
help: false
|
||||
}
|
||||
p = OptionParser.new do |parser|
|
||||
parser.banner = "ptouch-print-png [options]"
|
||||
parser.on("-f", "--file PATH", "入力 PNG ファイル") { |v| o[:file] = v }
|
||||
parser.on("-t", "--threshold N", Integer,
|
||||
"二値化しきい値 0–255(既定 #{Libptouch::PNG_DEFAULT_THRESHOLD})") do |v|
|
||||
o[:threshold] = v
|
||||
end
|
||||
parser.on("-n", "--dry-run", "読み込みと検証のみ(USB なし)") { o[:dry_run] = true }
|
||||
parser.on("-S", "--status", "USB プリンタのステータスを JSON で表示して終了") { o[:status] = true }
|
||||
parser.on("-V", "--version", "バージョンを表示して終了") { o[:version] = true }
|
||||
parser.on("-h", "--help", "このヘルプ") { o[:help] = true }
|
||||
end
|
||||
p.help
|
||||
end
|
||||
|
||||
def run_status
|
||||
ctx = nil
|
||||
begin
|
||||
ctx = Libptouch::Context.new
|
||||
ctx.open_usb
|
||||
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)
|
||||
ctx = nil
|
||||
begin
|
||||
ctx = Libptouch::Context.new
|
||||
threshold = opts[:threshold]
|
||||
data, width, height = if 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, #{width}x#{height} dots"
|
||||
return 0
|
||||
end
|
||||
|
||||
ctx.open_usb
|
||||
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
|
||||
end
|
||||
end
|
||||
106
ruby/lib/libptouch/context.rb
Normal file
106
ruby/lib/libptouch/context.rb
Normal file
@@ -0,0 +1,106 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module Libptouch
|
||||
class Context
|
||||
OK = 0
|
||||
|
||||
attr_reader :native
|
||||
|
||||
def initialize
|
||||
@native = Binding.libptouch_create
|
||||
raise Libptouch::Error.new(0, "libptouch_create failed") if @native.null?
|
||||
end
|
||||
|
||||
def last_message
|
||||
Binding.libptouch_strerror(@native)
|
||||
end
|
||||
|
||||
def last_error_code
|
||||
Binding.libptouch_last_error(@native)
|
||||
end
|
||||
|
||||
def raise_on_error(code)
|
||||
return if code == OK
|
||||
|
||||
msg = Binding.libptouch_strerror(@native)
|
||||
raise Libptouch::Error.new(code, msg)
|
||||
end
|
||||
|
||||
def open_usb
|
||||
raise_on_error(Binding.libptouch_open_usb(@native))
|
||||
self
|
||||
end
|
||||
|
||||
def open_usb_vid_pid(vid, pid)
|
||||
raise_on_error(Binding.libptouch_open_usb_vid_pid(@native, vid, pid))
|
||||
self
|
||||
end
|
||||
|
||||
def close
|
||||
Binding.libptouch_close(@native) if @native && !@native.null?
|
||||
self
|
||||
end
|
||||
|
||||
def dispose
|
||||
close
|
||||
Binding.libptouch_destroy(@native) if @native && !@native.null?
|
||||
@native = nil
|
||||
end
|
||||
|
||||
def check_raster(data, width_dots:, height_dots:, margin_mm: 0)
|
||||
params = Binding::RasterParams.new
|
||||
params[:width_dots] = width_dots
|
||||
params[:height_dots] = height_dots
|
||||
params[:margin_mm] = margin_mm
|
||||
buf = FFI::MemoryPointer.new(:uint8, data.bytesize)
|
||||
buf.put_bytes(0, data)
|
||||
raise_on_error(Binding.libptouch_check_raster(@native, buf, data.bytesize,
|
||||
params.pointer))
|
||||
self
|
||||
end
|
||||
|
||||
def print_raster(data, width_dots:, height_dots:, margin_mm: 0)
|
||||
params = Binding::RasterParams.new
|
||||
params[:width_dots] = width_dots
|
||||
params[:height_dots] = height_dots
|
||||
params[:margin_mm] = margin_mm
|
||||
buf = FFI::MemoryPointer.new(:uint8, data.bytesize)
|
||||
buf.put_bytes(0, data)
|
||||
raise_on_error(Binding.libptouch_print_raster(@native, buf, data.bytesize,
|
||||
params.pointer))
|
||||
self
|
||||
end
|
||||
|
||||
def png_file_to_raster(path, threshold: nil)
|
||||
opt_ptr = nil
|
||||
unless threshold.nil?
|
||||
o = Binding::PngOptions.new
|
||||
o[:threshold] = threshold
|
||||
opt_ptr = o.pointer
|
||||
end
|
||||
out_pp = FFI::MemoryPointer.new(:pointer)
|
||||
out_len = FFI::MemoryPointer.new(:size_t)
|
||||
out_params = Binding::RasterParams.new
|
||||
raise_on_error(Binding.libptouch_png_file_to_raster(
|
||||
@native, path, opt_ptr, out_pp, out_len, out_params.pointer
|
||||
))
|
||||
raw = out_pp.read_pointer
|
||||
raise Libptouch::Error.new(OK, "null raster from PNG") if raw.null?
|
||||
|
||||
len = out_len.read_size_t
|
||||
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))
|
||||
buf.read_bytes(STATUS_LENGTH)
|
||||
end
|
||||
|
||||
def status_hash
|
||||
Libptouch.parse_status(status_bytes)
|
||||
end
|
||||
end
|
||||
end
|
||||
12
ruby/lib/libptouch/error.rb
Normal file
12
ruby/lib/libptouch/error.rb
Normal file
@@ -0,0 +1,12 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module Libptouch
|
||||
class Error < StandardError
|
||||
attr_reader :code
|
||||
|
||||
def initialize(code, message = nil)
|
||||
@code = code
|
||||
super(message || "libptouch error #{code}")
|
||||
end
|
||||
end
|
||||
end
|
||||
197
ruby/lib/libptouch/status_hash.rb
Normal file
197
ruby/lib/libptouch/status_hash.rb
Normal file
@@ -0,0 +1,197 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module Libptouch
|
||||
# 32 バイトのステータス応答を Hash に展開する(C の libptouch_status_fprint と同じ区分)。
|
||||
module StatusHash
|
||||
class << self
|
||||
def decode(raw)
|
||||
unless raw.bytesize == STATUS_LENGTH
|
||||
raise ArgumentError,
|
||||
"expected #{STATUS_LENGTH} bytes, got #{raw.bytesize}"
|
||||
end
|
||||
|
||||
s = raw.bytes
|
||||
|
||||
{
|
||||
header_ok: s[0] == 0x80 && s[1] == 0x20,
|
||||
header: [s[0], s[1]],
|
||||
brother_code: s[2],
|
||||
brother_code_char: s[2] >= 32 && s[2] < 127 ? s[2].chr(Encoding::ASCII_8BIT) : nil,
|
||||
model: model_entry(s[4]),
|
||||
region_code: s[5],
|
||||
region_char: s[5] >= 32 && s[5] < 127 ? s[5].chr(Encoding::ASCII_8BIT) : nil,
|
||||
battery: labeled(BATTERY, s[6]),
|
||||
extended_error: extended_error_entry(s[7]),
|
||||
error_info1: error_info1(s[8]),
|
||||
error_info2: error_info2(s[9]),
|
||||
media_width: media_width_entry(s[10], s[17]),
|
||||
tape_kind: labeled(MEDIA_KIND, s[11]),
|
||||
color_count: s[12],
|
||||
font_jp: s[13],
|
||||
font: s[14],
|
||||
mode: s[15],
|
||||
density: s[16],
|
||||
status_kind: labeled(STATUS_KIND, s[18]),
|
||||
phase_type: s[19],
|
||||
phase_number: [s[20], s[21]],
|
||||
notification_number: s[22],
|
||||
extended_section_bytes: s[23],
|
||||
tape_color: labeled(TAPE_COLOR, s[24]),
|
||||
text_color: s[25],
|
||||
raw_hex: raw.unpack1("H*"),
|
||||
raw_bytes: s.dup
|
||||
}
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def model_entry(code)
|
||||
{
|
||||
code: code,
|
||||
name: MODEL_NAMES[code],
|
||||
ascii_char: code >= 32 && code < 127 ? code.chr(Encoding::ASCII_8BIT) : nil
|
||||
}
|
||||
end
|
||||
|
||||
def labeled(table, byte)
|
||||
{
|
||||
code: byte,
|
||||
label: table[byte]
|
||||
}
|
||||
end
|
||||
|
||||
def extended_error_entry(byte)
|
||||
h = { code: byte, label: EXTENDED_ERROR[byte] }
|
||||
h[:none] = true if byte.zero?
|
||||
h
|
||||
end
|
||||
|
||||
def media_width_entry(w, len_byte)
|
||||
entry = labeled(MEDIA_WIDTH, w)
|
||||
if w == 0x15 && len_byte != 0
|
||||
entry[:media_length_code] = len_byte
|
||||
entry[:media_length_note_mm] = len_byte
|
||||
end
|
||||
entry
|
||||
end
|
||||
|
||||
def error_info1(b)
|
||||
{
|
||||
raw: b,
|
||||
media_missing: !!(b & 0x01),
|
||||
media_end: !!(b & 0x02),
|
||||
cutter_jam: !!(b & 0x04),
|
||||
battery_weak: !!(b & 0x08),
|
||||
high_voltage_adapter: !!(b & 0x40)
|
||||
}
|
||||
end
|
||||
|
||||
def error_info2(b)
|
||||
{
|
||||
raw: b,
|
||||
media_mismatch: !!(b & 0x01),
|
||||
comm_error: !!(b & 0x04),
|
||||
comm_buffer_full: !!(b & 0x08),
|
||||
cover_open: !!(b & 0x10),
|
||||
heat_error: !!(b & 0x20),
|
||||
tip_detection_error: !!(b & 0x40),
|
||||
system_error: !!(b & 0x80)
|
||||
}
|
||||
end
|
||||
end
|
||||
|
||||
MODEL_NAMES = {
|
||||
0x6F => "PT-P900W",
|
||||
0x70 => "PT-P950NW",
|
||||
0x71 => "PT-P900",
|
||||
0x78 => "PT-P910BT"
|
||||
}.freeze
|
||||
|
||||
MEDIA_WIDTH = {
|
||||
0x00 => "テープなし / 未装着",
|
||||
0x04 => "3.5 mm",
|
||||
0x06 => "6 mm",
|
||||
0x09 => "9 mm",
|
||||
0x0C => "12 mm",
|
||||
0x12 => "18 mm",
|
||||
0x18 => "24 mm",
|
||||
0x24 => "36 mm",
|
||||
0x15 => "FLe 21 mm 幅(長さはメディア長バイト参照)"
|
||||
}.freeze
|
||||
|
||||
MEDIA_KIND = {
|
||||
0x00 => "テープなし",
|
||||
0x01 => "ラミネートテープ",
|
||||
0x03 => "ノンラミネートテープ",
|
||||
0x04 => "ファブリックテープ",
|
||||
0x11 => "ヒートシュリンクチューブ (HS 2:1)",
|
||||
0x13 => "FLe テープ",
|
||||
0x14 => "フレキシブルIDテープ",
|
||||
0x15 => "サテンテープ",
|
||||
0x17 => "ヒートシュリンクチューブ (HS 3:1)",
|
||||
0xFF => "非対応テープ"
|
||||
}.freeze
|
||||
|
||||
BATTERY = {
|
||||
0x00 => "フル",
|
||||
0x01 => "ハーフ",
|
||||
0x02 => "ロー",
|
||||
0x03 => "要充電",
|
||||
0x04 => "AC アダプター使用中",
|
||||
0xFF => "不明"
|
||||
}.freeze
|
||||
|
||||
TAPE_COLOR = {
|
||||
0x01 => "白 (White)",
|
||||
0x02 => "その他 (Other)",
|
||||
0x03 => "透明 (Clear)",
|
||||
0x04 => "赤 (Red)",
|
||||
0x05 => "青 (Blue)",
|
||||
0x06 => "黄 (Yellow)",
|
||||
0x07 => "緑 (Green)",
|
||||
0x08 => "黒 (Black)",
|
||||
0x09 => "透明(文字白)",
|
||||
0x20 => "白(マット) (Matte White)",
|
||||
0x21 => "透明(マット) (Matte Clear)",
|
||||
0x22 => "銀(マット) (Matte Silver)",
|
||||
0x23 => "金(サテン) (Satin Gold)",
|
||||
0x24 => "銀(サテン) (Satin Silver)",
|
||||
0x30 => "青(D)",
|
||||
0x31 => "赤(D)",
|
||||
0x40 => "オレンジ(蛍光)",
|
||||
0x41 => "黄(蛍光)",
|
||||
0x50 => "ピンク(S)",
|
||||
0x51 => "グレー(S)",
|
||||
0x52 => "グリーン(S)",
|
||||
0x60 => "イエロー(F)",
|
||||
0x61 => "ピンク(F)",
|
||||
0x62 => "ブルー(F)",
|
||||
0x70 => "白(チューブ)",
|
||||
0x90 => "白(フレキ)",
|
||||
0x91 => "黄(フレキ)",
|
||||
0xF0 => "クリーニング",
|
||||
0xF1 => "ステンシル",
|
||||
0xFF => "非対応"
|
||||
}.freeze
|
||||
|
||||
STATUS_KIND = {
|
||||
0x00 => "印刷終了",
|
||||
0x01 => "エラー発生",
|
||||
0x02 => "IF モード終了",
|
||||
0x03 => "パワーオフ(未使用扱い)",
|
||||
0x04 => "通知",
|
||||
0x05 => "フェーズ変更"
|
||||
}.freeze
|
||||
|
||||
EXTENDED_ERROR = {
|
||||
0x10 => "FLE のテープエンド",
|
||||
0x1D => "高解像度/ドラフト印刷エラー",
|
||||
0x1E => "アダプター抜き挿しエラー",
|
||||
0x21 => "非対応メディアエラー"
|
||||
}.freeze
|
||||
end
|
||||
|
||||
def self.parse_status(raw)
|
||||
StatusHash.decode(raw)
|
||||
end
|
||||
end
|
||||
5
ruby/lib/libptouch/version.rb
Normal file
5
ruby/lib/libptouch/version.rb
Normal file
@@ -0,0 +1,5 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module Libptouch
|
||||
VERSION = "1.0.0"
|
||||
end
|
||||
Reference in New Issue
Block a user