diff --git a/.gitignore b/.gitignore index be1630e..3e8abbc 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,8 @@ build/ +ruby/*.gem +ruby/.rubocop_cache/ + *.o *.a ptouch-print diff --git a/CMakeLists.txt b/CMakeLists.txt index 78a6e0c..7a936d9 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -22,14 +22,24 @@ configure_file( ) add_library(ptouch STATIC src/libptouch.c) +add_library(ptouch_shared SHARED src/libptouch.c) +set_target_properties(ptouch_shared PROPERTIES OUTPUT_NAME ptouch + SOVERSION ${PROJECT_VERSION_MAJOR}) target_include_directories(ptouch PUBLIC "$" "$" "$" ) +target_include_directories(ptouch_shared PUBLIC + "$" + "$" + "$" +) target_link_libraries(ptouch PRIVATE PkgConfig::LIBUSB PNG::PNG) +target_link_libraries(ptouch_shared PRIVATE PkgConfig::LIBUSB PNG::PNG) if(NOT MSVC) target_compile_options(ptouch PRIVATE -Wall -Wextra -Wpedantic) + target_compile_options(ptouch_shared PRIVATE -Wall -Wextra -Wpedantic) endif() add_executable(ptouch-print src/cli/main.c) @@ -40,6 +50,8 @@ if(NOT MSVC) endif() install(TARGETS ptouch ARCHIVE DESTINATION "${CMAKE_INSTALL_LIBDIR}") +install(TARGETS ptouch_shared LIBRARY DESTINATION "${CMAKE_INSTALL_LIBDIR}" + RUNTIME DESTINATION "${CMAKE_INSTALL_BINDIR}") install(TARGETS ptouch-print RUNTIME DESTINATION "${CMAKE_INSTALL_BINDIR}") install(FILES "${CMAKE_CURRENT_SOURCE_DIR}/include/libptouch.h" diff --git a/README.md b/README.md index d2f554e..6acfaa9 100644 --- a/README.md +++ b/README.md @@ -14,6 +14,7 @@ Brother P-touch シリーズ向けのラベル印刷用 **C コアライブラ | `src/libptouch.c` | ライブラリ本体(スタブ) | | `src/cli/main.c` | `ptouch-print` エントリ | | `samples/` | 試験用サンプル画像の置き場(PNG 等) | +| `ruby/` | Ruby FFI gem(`libptouch`)・コマンド `ptouch-print-png`(PNG のみ)— `ruby/README.md` | | `reference/` | 仕様・参考資料(例: ラスター PDF) | ## ビルド @@ -28,8 +29,13 @@ cmake --build build 成果物(`build/` 以下): - `libptouch.a` — 静的ライブラリ +- `libptouch.so` — 共有ライブラリ(Ruby FFI 用) - `ptouch-print` — CLI +### Ruby gem + +共有ライブラリをビルドしたうえで、`ruby/` で `bundle install` → `gem build libptouch.gemspec` など(手順は `ruby/README.md`)。 + ## CLI の使い方(雛形) **PNG**(拡張子 `.png` または PNG シグネチャ)の場合は幅・高さは画像から取得します。任意で `-t`(0–255)で二値化しきい値を指定できます。 diff --git a/ruby/.rubocop.yml b/ruby/.rubocop.yml new file mode 100644 index 0000000..f0e4f60 --- /dev/null +++ b/ruby/.rubocop.yml @@ -0,0 +1,66 @@ +# RuboCop — libptouch gem +# https://docs.rubocop.org/ + +AllCops: + TargetRubyVersion: 3.0 + NewCops: enable + SuggestExtensions: false + Exclude: + - "vendor/**/*" + - ".bundle/**/*" + +Layout/LineLength: + Max: 120 + +Layout/MultilineMethodCallIndentation: + EnforcedStyle: indented_relative_to_receiver + +Lint/RedundantCopDisableDirective: + Severity: convention + +# ステータスデコードはデータテーブル中心で行数・複雑度が大きくなりがち +Metrics/AbcSize: + Max: 60 + +Metrics/BlockLength: + Max: 35 + Exclude: + - "**/*.gemspec" + +Metrics/MethodLength: + Max: 40 + +Metrics/ModuleLength: + Max: 220 + +Metrics/ParameterLists: + Max: 6 + +Naming/MethodParameterName: + AllowedNames: + - "b" + - "w" + +Style/Documentation: + Enabled: false + +Style/DoubleNegation: + Enabled: false + +# optional な環境変数の参照に fetch は不向きなことが多い +Style/FetchEnvVar: + Enabled: false + +Style/FrozenStringLiteralComment: + EnforcedStyle: always + +# FFI の %i[] / %w[] はそのままの方が読みやすい +Style/SymbolArray: + Enabled: false + +Style/WordArray: + Enabled: false + +Style/StringLiterals: + EnforcedStyle: double_quotes + ConsistentQuotesInMultiline: true diff --git a/ruby/Gemfile b/ruby/Gemfile new file mode 100644 index 0000000..8f2506c --- /dev/null +++ b/ruby/Gemfile @@ -0,0 +1,9 @@ +# frozen_string_literal: true + +source "https://rubygems.org" + +gemspec + +group :development do + gem "rubocop", "~> 1.69", require: false +end diff --git a/ruby/Gemfile.lock b/ruby/Gemfile.lock new file mode 100644 index 0000000..107c760 --- /dev/null +++ b/ruby/Gemfile.lock @@ -0,0 +1,99 @@ +PATH + remote: . + specs: + libptouch (1.0.0) + ffi (~> 1.15) + +GEM + remote: https://rubygems.org/ + specs: + ast (2.4.3) + ffi (1.17.4) + ffi (1.17.4-aarch64-linux-gnu) + ffi (1.17.4-aarch64-linux-musl) + ffi (1.17.4-arm-linux-gnu) + ffi (1.17.4-arm-linux-musl) + ffi (1.17.4-arm64-darwin) + ffi (1.17.4-x86-linux-gnu) + ffi (1.17.4-x86-linux-musl) + ffi (1.17.4-x86_64-darwin) + ffi (1.17.4-x86_64-linux-gnu) + ffi (1.17.4-x86_64-linux-musl) + json (2.19.3) + language_server-protocol (3.17.0.5) + lint_roller (1.1.0) + parallel (2.0.1) + parser (3.3.11.1) + ast (~> 2.4.1) + racc + prism (1.9.0) + racc (1.8.1) + rainbow (3.1.1) + regexp_parser (2.12.0) + rubocop (1.86.1) + json (~> 2.3) + language_server-protocol (~> 3.17.0.2) + lint_roller (~> 1.1.0) + parallel (>= 1.10) + parser (>= 3.3.0.2) + rainbow (>= 2.2.2, < 4.0) + regexp_parser (>= 2.9.3, < 3.0) + rubocop-ast (>= 1.49.0, < 2.0) + ruby-progressbar (~> 1.7) + unicode-display_width (>= 2.4.0, < 4.0) + rubocop-ast (1.49.1) + parser (>= 3.3.7.2) + prism (~> 1.7) + ruby-progressbar (1.13.0) + unicode-display_width (3.2.0) + unicode-emoji (~> 4.1) + unicode-emoji (4.2.0) + +PLATFORMS + aarch64-linux-gnu + aarch64-linux-musl + arm-linux-gnu + arm-linux-musl + arm64-darwin + ruby + x86-linux-gnu + x86-linux-musl + x86_64-darwin + x86_64-linux-gnu + x86_64-linux-musl + +DEPENDENCIES + libptouch! + rubocop (~> 1.69) + +CHECKSUMS + ast (2.4.3) sha256=954615157c1d6a382bc27d690d973195e79db7f55e9765ac7c481c60bdb4d383 + ffi (1.17.4) sha256=bcd1642e06f0d16fc9e09ac6d49c3a7298b9789bcb58127302f934e437d60acf + ffi (1.17.4-aarch64-linux-gnu) sha256=b208f06f91ffd8f5e1193da3cae3d2ccfc27fc36fba577baf698d26d91c080df + ffi (1.17.4-aarch64-linux-musl) sha256=9286b7a615f2676245283aef0a0a3b475ae3aae2bb5448baace630bb77b91f39 + ffi (1.17.4-arm-linux-gnu) sha256=d6dbddf7cb77bf955411af5f187a65b8cd378cb003c15c05697f5feee1cb1564 + ffi (1.17.4-arm-linux-musl) sha256=9d4838ded0465bef6e2426935f6bcc93134b6616785a84ffd2a3d82bc3cf6f95 + ffi (1.17.4-arm64-darwin) sha256=19071aaf1419251b0a46852abf960e77330a3b334d13a4ab51d58b31a937001b + ffi (1.17.4-x86-linux-gnu) sha256=38e150df5f4ca555e25beca4090823ae09657bceded154e3c52f8631c1ed72cf + ffi (1.17.4-x86-linux-musl) sha256=fbeec0fc7c795bcf86f623bb18d31ea1820f7bd580e1703a3d3740d527437809 + ffi (1.17.4-x86_64-darwin) sha256=aa70390523cf3235096cf64962b709b4cfbd5c082a2cb2ae714eb0fe2ccda496 + ffi (1.17.4-x86_64-linux-gnu) sha256=9d3db14c2eae074b382fa9c083fe95aec6e0a1451da249eab096c34002bc752d + 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) + lint_roller (1.1.0) sha256=2c0c845b632a7d172cb849cc90c1bce937a28c5c8ccccb50dfd46a485003cc87 + parallel (2.0.1) sha256=337782d3e39f4121e67563bf91dd8ece67f48923d90698614773a0ec9a5b2c7d + parser (3.3.11.1) sha256=d17ace7aabe3e72c3cc94043714be27cc6f852f104d81aa284c2281aecc65d54 + prism (1.9.0) sha256=7b530c6a9f92c24300014919c9dcbc055bf4cdf51ec30aed099b06cd6674ef85 + racc (1.8.1) sha256=4a7f6929691dbec8b5209a0b373bc2614882b55fc5d2e447a21aaa691303d62f + rainbow (3.1.1) sha256=039491aa3a89f42efa1d6dec2fc4e62ede96eb6acd95e52f1ad581182b79bc6a + regexp_parser (2.12.0) sha256=35a916a1d63190ab5c9009457136ae5f3c0c7512d60291d0d1378ba18ce08ebb + rubocop (1.86.1) sha256=44415f3f01d01a21e01132248d2fd0867572475b566ca188a0a42133a08d4531 + rubocop-ast (1.49.1) sha256=4412f3ee70f6fe4546cc489548e0f6fcf76cafcfa80fa03af67098ffed755035 + ruby-progressbar (1.13.0) sha256=80fc9c47a9b640d6834e0dc7b3c94c9df37f08cb072b7761e4a71e22cff29b33 + unicode-display_width (3.2.0) sha256=0cdd96b5681a5949cdbc2c55e7b420facae74c4aaf9a9815eee1087cb1853c42 + unicode-emoji (4.2.0) sha256=519e69150f75652e40bf736106cfbc8f0f73aa3fb6a65afe62fefa7f80b0f80f + +BUNDLED WITH + 4.0.6 diff --git a/ruby/README.md b/ruby/README.md new file mode 100644 index 0000000..e0fb52d --- /dev/null +++ b/ruby/README.md @@ -0,0 +1,78 @@ +# libptouch(Ruby gem) + +[ptouch_label](../) の **libptouch** を [ffi](https://github.com/ffi/ffi) 経由で使うための Gem です。 + +## 前提 + +1. リポジトリルートで共有ライブラリをビルドする(`libptouch.so` が `build/` に生成されます)。 + + ```bash + cmake -S .. -B ../build + cmake --build ../build + ``` + +2. Ruby 3.0 以上と `ffi` gem。 + +## インストール(開発時) + +```bash +cd ruby +bundle install # または gem install ffi +bundle exec rubocop # 任意: スタイルチェック(.rubocop.yml) +gem build libptouch.gemspec +gem install ./libptouch-1.0.0.gem +``` + +ビルド済みの `../build/libptouch.so` を自動で読みに行きます。別のパスにある場合は環境変数で指定できます。 + +```bash +export LIBPTOUCH_LIB=/usr/local/lib/libptouch.so +``` + +(`cmake --install` で共有ライブラリをインストールした場合は、通常は `libptouch` 名でローダが解決します。) + +## コマンド `ptouch-print-png`(PNG のみ) + +C の `ptouch-print` と同様の流れですが、**PNG 入力のみ**(`-w`/`-H` や 1bit ラスターは扱いません)。`gem install` 後は PATH に `ptouch-print-png` が入ります。 + +開発ツリーからそのまま試す例: + +```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 +``` + +## 使用例 + +```ruby +require "libptouch" + +Libptouch::Context.new.tap do |ctx| + ctx.open_usb + p ctx.status_bytes.bytesize # => 32 + p ctx.status_hash[:tape_kind] # => {:code=>..., :label=>"ラミネートテープ"} など + p ctx.status_hash[:status_kind] # => 状態(ステータス種類) +ensure + ctx.dispose +end +``` + +生の 32 バイトだけある場合は `Libptouch.parse_status(raw)` で同じ Hash 形式に展開できます(中身は `libptouch_status_fprint` と同じ区分)。 + +PNG からラスターへ: + +```ruby +ctx = Libptouch::Context.new +data, w, h = ctx.png_file_to_raster("/path/to/label.png") +ctx.open_usb +ctx.print_raster(data, width_dots: w, height_dots: h, margin_mm: 0) +ctx.dispose +``` + +## API の範囲 + +- 実行ファイル `ptouch-print-png` … PNG のみ(`-f`, `-t`, `-n`, `-S`, `-V`, `-h`)。ステータスは JSON(`status_bytes` を `parse_status` したもの、`raw_bytes` 除く) +- `Libptouch::Context` … `open_usb` / `open_usb_vid_pid` / `close` / `dispose` +- `check_raster` / `print_raster` / `png_file_to_raster` / `status_bytes` / `status_hash` +- `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-print-png b/ruby/exe/ptouch-print-png new file mode 100755 index 0000000..7fe6462 --- /dev/null +++ b/ruby/exe/ptouch-print-png @@ -0,0 +1,6 @@ +#!/usr/bin/env ruby +# frozen_string_literal: true + +require "libptouch/cli/png_print" + +exit Libptouch::Cli::PngPrint.run(ARGV) diff --git a/ruby/lib/libptouch.rb b/ruby/lib/libptouch.rb new file mode 100644 index 0000000..98535fc --- /dev/null +++ b/ruby/lib/libptouch.rb @@ -0,0 +1,28 @@ +# frozen_string_literal: true + +# Ruby FFI bindings for libptouch (Brother P-touch raster printing). +# +# Author: knb +# Email: knb@artif.org + +require_relative "libptouch/version" +require_relative "libptouch/error" +require_relative "libptouch/binding" +require_relative "libptouch/status_hash" +require_relative "libptouch/context" + +module Libptouch + ERR_NOMEM = 1 + ERR_ARG = 2 + ERR_USB = 3 + ERR_IO = 4 + ERR_UNSUPPORTED = 5 + ERR_NOT_FOUND = 6 + ERR_IMAGE = 7 + + USB_VID_BROTHER = 0x04f9 + USB_PID_PTP900W = 0x2085 + + STATUS_LENGTH = 32 + PNG_DEFAULT_THRESHOLD = 128 +end diff --git a/ruby/lib/libptouch/binding.rb b/ruby/lib/libptouch/binding.rb new file mode 100644 index 0000000..ada383e --- /dev/null +++ b/ruby/lib/libptouch/binding.rb @@ -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 diff --git a/ruby/lib/libptouch/cli/png_print.rb b/ruby/lib/libptouch/cli/png_print.rb new file mode 100644 index 0000000..fabd0a0 --- /dev/null +++ b/ruby/lib/libptouch/cli/png_print.rb @@ -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 diff --git a/ruby/lib/libptouch/context.rb b/ruby/lib/libptouch/context.rb new file mode 100644 index 0000000..a7cf298 --- /dev/null +++ b/ruby/lib/libptouch/context.rb @@ -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 diff --git a/ruby/lib/libptouch/error.rb b/ruby/lib/libptouch/error.rb new file mode 100644 index 0000000..41e90d8 --- /dev/null +++ b/ruby/lib/libptouch/error.rb @@ -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 diff --git a/ruby/lib/libptouch/status_hash.rb b/ruby/lib/libptouch/status_hash.rb new file mode 100644 index 0000000..c005c82 --- /dev/null +++ b/ruby/lib/libptouch/status_hash.rb @@ -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 diff --git a/ruby/lib/libptouch/version.rb b/ruby/lib/libptouch/version.rb new file mode 100644 index 0000000..50052c1 --- /dev/null +++ b/ruby/lib/libptouch/version.rb @@ -0,0 +1,5 @@ +# frozen_string_literal: true + +module Libptouch + VERSION = "1.0.0" +end diff --git a/ruby/libptouch.gemspec b/ruby/libptouch.gemspec new file mode 100644 index 0000000..3f39b92 --- /dev/null +++ b/ruby/libptouch.gemspec @@ -0,0 +1,31 @@ +# frozen_string_literal: true + +lib = File.expand_path("lib", __dir__) +$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib) +require "libptouch/version" + +Gem::Specification.new do |spec| + spec.name = "libptouch" + spec.version = Libptouch::VERSION + spec.authors = ["knb"] + spec.email = ["knb@artif.org"] + + spec.summary = "FFI bindings for libptouch (Brother P-touch USB raster printing)" + spec.description = [ + "Ruby wrapper around the ptouch_label C library libptouch.", + "Requires libptouch shared library (libusb, libpng)." + ].join(" ") + spec.license = "MIT" + spec.required_ruby_version = ">= 3.0" + repo = "https://gitea.artif.org/knb/ptouch_label" + spec.homepage = repo + spec.metadata["source_code_uri"] = repo + spec.metadata["rubygems_mfa_required"] = "true" + + spec.files = Dir.chdir(__dir__) { Dir["lib/**/*.rb", "exe/*", "README.md"] } + spec.bindir = "exe" + spec.executables = ["ptouch-print-png"] + spec.require_paths = ["lib"] + + spec.add_dependency "ffi", "~> 1.15" +end