From 2ed0bfc0be7e67bc4657a500703e43b808fdec49 Mon Sep 17 00:00:00 2001 From: knb Date: Tue, 14 Apr 2026 18:29:24 +0900 Subject: [PATCH] =?UTF-8?q?SVG=E5=8D=B0=E5=88=B7=E5=AF=BE=E5=BF=9C?= =?UTF-8?q?=E3=81=A8=E3=83=A1=E3=83=87=E3=82=A3=E3=82=A2=E6=83=85=E5=A0=B1?= =?UTF-8?q?API=E3=82=92=E8=BF=BD=E5=8A=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit SVG入力を現在テープ幅に自動フィットして印刷できるようにし、アプリ側が余白計算できるようにテープ幅・DPI・最小送り量を取得するAPIを追加する。 Made-with: Cursor --- CMakeLists.txt | 9 ++ README.md | 16 ++- include/libptouch.h | 35 +++++ ruby/README.md | 21 ++- ruby/lib/libptouch/binding.rb | 21 +++ ruby/lib/libptouch/cli/png_print.rb | 44 ++++-- ruby/lib/libptouch/context.rb | 39 +++++ src/cli/main.c | 86 +++++++++--- src/lib/libptouch_media_info.c | 100 +++++++++++++ src/lib/libptouch_svg.c | 211 ++++++++++++++++++++++++++++ 10 files changed, 539 insertions(+), 43 deletions(-) create mode 100644 src/lib/libptouch_media_info.c create mode 100644 src/lib/libptouch_svg.c diff --git a/CMakeLists.txt b/CMakeLists.txt index 5b3029c..7e5ad7f 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -14,6 +14,7 @@ include(GNUInstallDirs) find_package(PkgConfig REQUIRED) pkg_check_modules(LIBUSB REQUIRED IMPORTED_TARGET libusb-1.0) find_package(PNG REQUIRED) +pkg_check_modules(LIBRSVG IMPORTED_TARGET librsvg-2.0) configure_file( "${CMAKE_CURRENT_SOURCE_DIR}/include/libptouch_version.h.in" @@ -25,9 +26,11 @@ set(LIBPTOUCH_SOURCES src/lib/libptouch_core.c src/lib/libptouch_usb.c src/lib/libptouch_layout.c + src/lib/libptouch_media_info.c src/lib/libptouch_print.c src/lib/libptouch_status.c src/lib/libptouch_png.c + src/lib/libptouch_svg.c ) add_library(ptouch STATIC ${LIBPTOUCH_SOURCES}) @@ -52,6 +55,12 @@ target_include_directories(ptouch_shared PRIVATE ) target_link_libraries(ptouch PRIVATE PkgConfig::LIBUSB PNG::PNG) target_link_libraries(ptouch_shared PRIVATE PkgConfig::LIBUSB PNG::PNG) +if(LIBRSVG_FOUND) + target_link_libraries(ptouch PRIVATE PkgConfig::LIBRSVG) + target_link_libraries(ptouch_shared PRIVATE PkgConfig::LIBRSVG) + target_compile_definitions(ptouch PRIVATE LIBPTOUCH_HAS_RSVG=1) + target_compile_definitions(ptouch_shared PRIVATE LIBPTOUCH_HAS_RSVG=1) +endif() if(NOT MSVC) target_compile_options(ptouch PRIVATE -Wall -Wextra -Wpedantic) target_compile_options(ptouch_shared PRIVATE -Wall -Wextra -Wpedantic) diff --git a/README.md b/README.md index 3d94eb2..9ed9e45 100644 --- a/README.md +++ b/README.md @@ -11,15 +11,15 @@ Brother P-touch シリーズ向けのラベル印刷用 **C コアライブラ | パス | 内容 | |------|------| | `include/libptouch.h` | 公開 API | -| `src/lib/libptouch_*.c` | ライブラリ本体(core / usb / print / status / png) | +| `src/lib/libptouch_*.c` | ライブラリ本体(core / usb / print / status / png / svg) | | `src/cli/main.c` | `ptouch-print` エントリ | -| `samples/` | 試験用サンプル画像の置き場(PNG 等) | +| `samples/` | 試験用サンプル画像の置き場(PNG/SVG 等) | | `ruby/` | Ruby FFI gem(`libptouch`)・コマンド `ptouch-print-png`(PNG のみ)— `ruby/README.md` | | `reference/` | 仕様・参考資料(例: ラスター PDF) | ## ビルド -依存: **CMake 3.16+**、**libusb-1.0**、**libpng**(開発パッケージ例: `libusb-1.0-0-dev`、`libpng-dev`)。 +依存: **CMake 3.16+**、**libusb-1.0**、**libpng**、**librsvg-2.0**(開発パッケージ例: `libusb-1.0-0-dev`、`libpng-dev`、`librsvg2-dev`)。 ```bash cmake -S . -B build @@ -38,7 +38,9 @@ cmake --build build ## CLI の使い方(雛形) -**PNG**(拡張子 `.png` または PNG シグネチャ)の場合は幅・高さは画像から取得します。任意で `-t`(0–255)で二値化しきい値を指定できます。 +**PNG**(拡張子 `.png` または PNG シグネチャ)の場合は幅・高さは画像から取得します。 +**SVG**(拡張子 `.svg`)の場合は現在装着中テープの印字可能ドット幅に合わせて自動拡大・縮小します(USB 接続が必要)。 +任意で `-t`(0–255)で二値化しきい値を指定できます。 **1bit packed ラスター**(行優先、行あたり `ceil(width/8)` バイト × 行数)の場合は `-f` に加え `-w` / `-H` が必須です。 @@ -48,6 +50,9 @@ cmake --build build # PNG — 検証のみ(USB 不要) ./build/ptouch-print -n -f label.png +# SVG — 幅を現在テープにフィットさせて検証(USB 必要) +./build/ptouch-print -n -f label.svg + # PNG — しきい値を指定 ./build/ptouch-print -n -f label.png -t 160 @@ -59,6 +64,7 @@ cmake --build build # USB 接続時 ./build/ptouch-print -f label.png +./build/ptouch-print -f label.svg ./build/ptouch-print -f sample.raster -w 128 -H 64 # PT-P750W / PT-P710BT(`lsusb` の PID に合わせる) @@ -74,8 +80,10 @@ cmake --build build - `libptouch_create` / `libptouch_destroy` — コンテキスト - `libptouch_open_usb` / `libptouch_open_usb_vid_pid` / `libptouch_close` — USB(libusb・VID/PID) - `libptouch_get_status` / `libptouch_status_fprint` — ステータス情報リクエスト(ESC i S)の応答 +- `libptouch_get_current_media_info` — 現在テープ幅(mm)・印字可能幅(dots)・DPI・最小余白(mm)を取得 - `libptouch_check_raster` — ラスターバッファの検証のみ - `libptouch_png_file_to_raster` / `libptouch_free_raster` — PNG を 1bit ラスターに変換(libpng) +- `libptouch_svg_file_to_raster_fit_current_tape` — SVG を現在テープ幅に合わせて 1bit ラスターへ変換(librsvg + cairo、USB 必須) - `libptouch_print_raster` — ラスター印刷(USB・PDF のコマンド列、内部で転置) - `libptouch_strerror` / `libptouch_last_error` — エラー情報 diff --git a/include/libptouch.h b/include/libptouch.h index 3677942..0b32912 100644 --- a/include/libptouch.h +++ b/include/libptouch.h @@ -73,6 +73,19 @@ typedef struct { uint8_t margin_mm; /**< 余白など(機種・仕様に合わせて使用) */ } libptouch_raster_params_t; +typedef struct { + uint8_t media_width_code; /**< status[10] */ + uint8_t media_kind_code; /**< status[11] */ + double print_dpi; /**< 印字幅方向 DPI(現状 180 or 360) */ + double feed_dpi; /**< 送り方向 DPI(ESC i d の基準) */ + double tape_width_mm; /**< 装着テープ幅(mm)。例: 3.5, 6, 9, 12, 18, 24, 36 */ + uint16_t printable_dots; /**< 現在テープで印字可能な幅(ドット) */ + uint16_t left_margin_dots; /**< 左余白(ドット) */ + uint16_t right_margin_dots; /**< 右余白(ドット) */ + uint16_t min_feed_dots; /**< 仕様上の最小送り量(ドット)。現在は 14 */ + double min_feed_mm; /**< 最小送り量(mm) */ +} libptouch_media_info_t; + /** * バッファサイズとパラメータの整合性のみ検査(USB 不要)。--dry-run 用。 */ @@ -80,6 +93,13 @@ libptouch_err_t libptouch_check_raster(libptouch_ctx *ctx, const uint8_t *data, size_t data_len, const libptouch_raster_params_t *params); +/** + * 現在装着テープの情報を取得する(USB 接続済み必須)。 + * ステータスと機種プロファイルからテープ幅・印字可能幅・最小余白量を返す。 + */ +libptouch_err_t libptouch_get_current_media_info(libptouch_ctx *ctx, + libptouch_media_info_t *out_info); + /** * 1 ビット packed ラスターを USB で印刷(各機種のラスター PDF 準拠)。 * 印字前にステータスでテープ幅を読み、印刷可能ドット内に画像を中央配置する。 @@ -101,6 +121,10 @@ typedef struct { uint8_t threshold; /**< 輝度がこれ未満なら黒(1)、以上なら白(0) */ } libptouch_png_options_t; +typedef struct { + uint8_t threshold; /**< 輝度がこれ未満なら黒(1)、以上なら白(0) */ +} libptouch_svg_options_t; + /** * PNG ファイルを読み、1bit packed ラスターに変換する(libpng)。 * @param out_raster malloc 済みバッファのポインタを返す。不要時は @ref libptouch_free_raster で解放。 @@ -110,6 +134,17 @@ libptouch_err_t libptouch_png_file_to_raster(libptouch_ctx *ctx, const char *pat uint8_t **out_raster, size_t *out_raster_bytes, libptouch_raster_params_t *out_params); +/** + * SVG ファイルを現在装着中テープの印字可能幅に合わせて 1bit packed ラスターへ変換する + * (librsvg + cairo)。 + * @note 現在テープ幅を取得するため、事前に @ref libptouch_open_usb などで USB 接続が必要。 + * @param out_raster malloc 済みバッファのポインタを返す。不要時は @ref libptouch_free_raster で解放。 + */ +libptouch_err_t libptouch_svg_file_to_raster_fit_current_tape( + libptouch_ctx *ctx, const char *path, + const libptouch_svg_options_t *options, uint8_t **out_raster, + size_t *out_raster_bytes, libptouch_raster_params_t *out_params); + void libptouch_free_raster(uint8_t *raster); /** ステータス情報リクエスト(ESC i S)の応答バイト数(cv_ptp900_jpn_raster_102.pdf) */ diff --git a/ruby/README.md b/ruby/README.md index 03cee0e..3b6da2f 100644 --- a/ruby/README.md +++ b/ruby/README.md @@ -31,9 +31,10 @@ export LIBPTOUCH_LIB=/usr/local/lib/libptouch.so (`cmake --install` で共有ライブラリをインストールした場合は、通常は `libptouch` 名でローダが解決します。) -## コマンド `ptouch-print-png`(PNG のみ) +## コマンド `ptouch-print-png`(PNG/SVG) -C の `ptouch-print` と同様の流れですが、**PNG 入力のみ**(`-w`/`-H` や 1bit ラスターは扱いません)。`gem install` 後は PATH に `ptouch-print-png` が入ります。 +C の `ptouch-print` と同様の流れで、**PNG/SVG 入力**(`-w`/`-H` や 1bit ラスターは扱いません)を扱います。`gem install` 後は PATH に `ptouch-print-png` が入ります。 +SVG は現在装着テープの印字可能幅に合わせて自動拡大・縮小します(USB 接続が必要)。 オプションは C 側に合わせ、**`-p` / `--pid`** で USB 製品 ID(16 進可)を指定できます。省略時は PT-P900W(`Libptouch::USB_PID_PTP900W` = `0x2085`)。例: PT-P750W `0x2062`、PT-P710BT `0x20af`(`libptouch.h` / `Libptouch` 定数と同じ)。 @@ -42,6 +43,7 @@ C の `ptouch-print` と同様の流れですが、**PNG 入力のみ**(`-w`/` ```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 ``` @@ -76,12 +78,23 @@ ctx.print_raster(data, width_dots: w, height_dots: h, margin_mm: 0) ctx.dispose ``` +SVG から現在テープ幅に合わせてラスターへ: + +```ruby +ctx = Libptouch::Context.new +ctx.open_usb +data, w, h = ctx.svg_file_to_raster_fit_current_tape("/path/to/label.svg") +ctx.print_raster(data, width_dots: w, height_dots: h, margin_mm: 0) +ctx.dispose +``` + **解像度:** P750W / P710BT はラスター幅方向が **180 dpi**、P900W 系は **360 dpi**(C の `libptouch_print` / 各機種ラスター PDF に合わせて画像を用意してください)。 ## API の範囲 -- 実行ファイル `ptouch-print-png` … PNG のみ(`-f`, `-t`, `-p`, `-n`, `-S`, `-V`, `-h`)。ステータスは JSON(`status_bytes` を `parse_status` したもの、`raw_bytes` 除く) +- 実行ファイル `ptouch-print-png` … PNG/SVG(`-f`, `-t`, `-p`, `-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` +- `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` などを含む - `Libptouch.parse_status(raw)` … 32 バイトを Hash に展開(機種・テープ幅・**テープ種類**・色・**状態(status_kind)**・エラービット・`raw_hex` など) - C の `libptouch_status_fprint`(`FILE *`)は FFI からはバインドしていません。テキスト出力の代わりに `parse_status` / `status_hash` を使ってください。 diff --git a/ruby/lib/libptouch/binding.rb b/ruby/lib/libptouch/binding.rb index ada383e..e922153 100644 --- a/ruby/lib/libptouch/binding.rb +++ b/ruby/lib/libptouch/binding.rb @@ -28,10 +28,28 @@ module Libptouch :_pad, [:uint8, 3] end + class MediaInfo < FFI::Struct + layout :media_width_code, :uint8, + :media_kind_code, :uint8, + :_pad0, [:uint8, 6], + :print_dpi, :double, + :feed_dpi, :double, + :tape_width_mm, :double, + :printable_dots, :uint16, + :left_margin_dots, :uint16, + :right_margin_dots, :uint16, + :min_feed_dots, :uint16, + :min_feed_mm, :double + end + class PngOptions < FFI::Struct layout :threshold, :uint8 end + class SvgOptions < FFI::Struct + layout :threshold, :uint8 + end + attach_function :libptouch_create, [], :pointer attach_function :libptouch_destroy, [:pointer], :void attach_function :libptouch_strerror, [:pointer], :string @@ -40,9 +58,12 @@ module Libptouch 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_get_current_media_info, %i[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 + attach_function :libptouch_svg_file_to_raster_fit_current_tape, + %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 diff --git a/ruby/lib/libptouch/cli/png_print.rb b/ruby/lib/libptouch/cli/png_print.rb index 6fd69c2..2f2fc64 100644 --- a/ruby/lib/libptouch/cli/png_print.rb +++ b/ruby/lib/libptouch/cli/png_print.rb @@ -7,7 +7,7 @@ require "libptouch" module Libptouch module Cli - # PNG のみを扱う ptouch-print 相当の CLI(1bit ラスター経路なし)。 + # PNG/SVG を扱う ptouch-print 相当の CLI(1bit ラスター経路なし)。 module PngPrint module_function @@ -26,9 +26,10 @@ module Libptouch 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) + kind = image_kind(path) + return usage_error("not a PNG/SVG file: #{path}") if kind.nil? - run_print(path, opts) + run_print(path, opts, kind) end def png_file?(path) @@ -43,6 +44,17 @@ module Libptouch 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) @@ -101,9 +113,9 @@ module Libptouch OptionParser.new do |p| p.banner = usage_banner p.separator "" - p.on("-f", "--file PATH", "入力 PNG ファイル") { |v| opts_hash[:file] = v } + p.on("-f", "--file PATH", "入力 PNG/SVG ファイル") { |v| opts_hash[:file] = v } p.on("-t", "--threshold N", Integer, - "二値化しきい値 0–255(既定 #{Libptouch::PNG_DEFAULT_THRESHOLD})") 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| @@ -122,7 +134,8 @@ module Libptouch <<~BANNER Usage: ptouch-print-png [options] - PNG のみ対応。幅・高さは画像から取得します(-w/-H はありません)。 + PNG/SVG 対応。幅・高さは画像から取得します(-w/-H はありません)。 + SVG は現在テープ幅に合わせて自動拡大・縮小します(USB 接続必須)。 --status のときは -f は不要です。 BANNER end @@ -149,9 +162,9 @@ module Libptouch opts_hash = default_cli_opts p = OptionParser.new do |parser| parser.banner = "ptouch-print-png [options]" - parser.on("-f", "--file PATH", "入力 PNG ファイル") { |v| opts_hash[:file] = v } + parser.on("-f", "--file PATH", "入力 PNG/SVG ファイル") { |v| opts_hash[:file] = v } parser.on("-t", "--threshold N", Integer, - "二値化しきい値 0–255(既定 #{Libptouch::PNG_DEFAULT_THRESHOLD})") 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| @@ -192,12 +205,19 @@ module Libptouch end end - def run_print(path, opts) + def run_print(path, opts, kind) ctx = nil begin ctx = Libptouch::Context.new threshold = opts[:threshold] - data, width, height = if threshold.nil? + 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) @@ -206,11 +226,11 @@ module Libptouch 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" + 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) + open_usb_for_opts(ctx, opts) if kind == :png ctx.print_raster(data, width_dots: width, height_dots: height) 0 rescue Libptouch::Error => e diff --git a/ruby/lib/libptouch/context.rb b/ruby/lib/libptouch/context.rb index b37769c..463d611 100644 --- a/ruby/lib/libptouch/context.rb +++ b/ruby/lib/libptouch/context.rb @@ -93,6 +93,28 @@ module Libptouch [bytes, out_params[:width_dots], out_params[:height_dots]] end + def svg_file_to_raster_fit_current_tape(path, threshold: nil) + opt_ptr = nil + unless threshold.nil? + o = Binding::SvgOptions.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_svg_file_to_raster_fit_current_tape( + @native, path, opt_ptr, out_pp, out_len, out_params.pointer + )) + raw = out_pp.read_pointer + raise Libptouch::Error.new(OK, "null raster from SVG") 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)) @@ -102,5 +124,22 @@ module Libptouch def status_hash Libptouch.parse_status(status_bytes) end + + def current_media_info + info = Binding::MediaInfo.new + raise_on_error(Binding.libptouch_get_current_media_info(@native, info.pointer)) + { + media_width_code: info[:media_width_code], + media_kind_code: info[:media_kind_code], + print_dpi: info[:print_dpi], + feed_dpi: info[:feed_dpi], + tape_width_mm: info[:tape_width_mm], + printable_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], + min_feed_mm: info[:min_feed_mm] + } + end end end diff --git a/src/cli/main.c b/src/cli/main.c index 430f157..db99c12 100644 --- a/src/cli/main.c +++ b/src/cli/main.c @@ -21,14 +21,15 @@ static void usage(const char *argv0) " -f, --file PATH 入力ファイル\n" " -w, --width DOTS 1bit ラスター時: 幅(ドット)\n" " -H, --height DOTS 1bit ラスター時: 高さ(ドット)\n" - " -t, --threshold N PNG 二値化しきい値 0–255(既定 %u、PNG のみ)\n" + " -t, --threshold N 二値化しきい値 0–255(既定 %u、PNG/SVG)\n" " -n, --dry-run 読み込みと check_raster のみ(USB なし)\n" " -p, --pid HEX USB 製品 ID(既定: P900W の 0x2085)。例: P750W 0x2062、P710BT 0x20af\n" " -S, --status USB 接続プリンタのステータス(テープ種・幅・色等)を表示して終了\n" " -V, --version バージョンを表示して終了\n" " -h, --help このヘルプ\n" "\n" - "PNG の場合は幅・高さはファイルから取得(-w/-H 不要)。\n" + "PNG は幅・高さを画像から取得(-w/-H 不要)。\n" + "SVG は現在テープの印字可能幅に合わせて自動拡大・縮小(USB 必須)。\n" "1bit packed ラスター(行優先)の場合は -f -w -H が必須。\n" "--status のときは -f は不要(他オプションは無視されます)。\n", argv0, (unsigned)LIBPTOUCH_PNG_DEFAULT_THRESHOLD); @@ -54,6 +55,14 @@ static int input_is_png(const char *path) return memcmp(sig, png_magic, sizeof(png_magic)) == 0; } +static int input_is_svg(const char *path) +{ + const char *dot = strrchr(path, '.'); + if (dot && strcasecmp(dot, ".svg") == 0) + return 1; + return 0; +} + static int read_file(const char *path, uint8_t **out, size_t *out_len) { FILE *fp = fopen(path, "rb"); @@ -201,20 +210,21 @@ int main(int argc, char **argv) } int png = input_is_png(file); - if (png) { + int svg = input_is_svg(file); + if (png || svg) { if (width != 0 || height != 0) fprintf(stderr, - "warning: -w/-H ignored for PNG (using image size)\n"); + "warning: -w/-H ignored for image inputs\n"); } else { if (width == 0 || height == 0) { fprintf(stderr, - "1bit raster requires -w and -H (or use a PNG file)\n"); + "1bit raster requires -w and -H (or use a PNG/SVG file)\n"); usage(argv[0]); return 2; } if (has_threshold) fprintf(stderr, - "warning: -t applies to PNG only (ignored)\n"); + "warning: -t applies to PNG/SVG only (ignored)\n"); } libptouch_ctx *ctx = libptouch_create(); @@ -227,6 +237,7 @@ int main(int argc, char **argv) size_t data_len = 0; libptouch_raster_params_t params = { 0, 0, 0 }; libptouch_err_t e; + int usb_opened = 0; if (png) { libptouch_png_options_t opt = { .threshold = (uint8_t)threshold }; @@ -239,6 +250,28 @@ int main(int argc, char **argv) libptouch_destroy(ctx); return 1; } + } else if (svg) { + 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) { + fprintf(stderr, "open_usb: %s\n", libptouch_strerror(ctx)); + libptouch_destroy(ctx); + return 1; + } + usb_opened = 1; + libptouch_svg_options_t opt = { .threshold = (uint8_t)threshold }; + e = libptouch_svg_file_to_raster_fit_current_tape( + ctx, file, has_threshold ? &opt : NULL, &data, &data_len, + ¶ms); + if (e != LIBPTOUCH_OK) { + fprintf(stderr, "svg_file_to_raster_fit_current_tape: %s\n", + libptouch_strerror(ctx)); + libptouch_close(ctx); + libptouch_destroy(ctx); + return 1; + } } else { if (read_file(file, &data, &data_len) != 0) { libptouch_destroy(ctx); @@ -254,7 +287,7 @@ int main(int argc, char **argv) fprintf(stderr, "check_raster: %s\n", libptouch_strerror(ctx)); libptouch_destroy(ctx); - if (png) + if (png || svg) libptouch_free_raster(data); else free(data); @@ -262,29 +295,36 @@ int main(int argc, char **argv) } if (dry_run) { - printf("dry-run OK: %zu bytes, %ux%u dots\n", data_len, + printf("dry-run OK: %zu bytes, src %ux%u dots (print lengthxwidth %ux%u)\n", + data_len, + (unsigned)params.width_dots, + (unsigned)params.height_dots, (unsigned)params.width_dots, (unsigned)params.height_dots); + if (usb_opened) + libptouch_close(ctx); libptouch_destroy(ctx); - if (png) + if (png || svg) libptouch_free_raster(data); else free(data); return 0; } - 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) { - fprintf(stderr, "open_usb: %s\n", libptouch_strerror(ctx)); - libptouch_destroy(ctx); - if (png) - libptouch_free_raster(data); - else - free(data); - return 1; + 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) { + fprintf(stderr, "open_usb: %s\n", libptouch_strerror(ctx)); + libptouch_destroy(ctx); + if (png || svg) + libptouch_free_raster(data); + else + free(data); + return 1; + } } e = libptouch_print_raster(ctx, data, data_len, ¶ms); @@ -293,7 +333,7 @@ int main(int argc, char **argv) libptouch_strerror(ctx)); libptouch_close(ctx); libptouch_destroy(ctx); - if (png) + if (png || svg) libptouch_free_raster(data); else free(data); @@ -302,7 +342,7 @@ int main(int argc, char **argv) libptouch_close(ctx); libptouch_destroy(ctx); - if (png) + if (png || svg) libptouch_free_raster(data); else free(data); diff --git a/src/lib/libptouch_media_info.c b/src/lib/libptouch_media_info.c new file mode 100644 index 0000000..ffac354 --- /dev/null +++ b/src/lib/libptouch_media_info.c @@ -0,0 +1,100 @@ +/* + * libptouch — current tape/media information helper + * + * Author: knb + * Email: knb@artif.org + */ + +#include "libptouch_internal.h" +#include "libptouch_layout.h" + +#include + +static double tape_width_mm_from_code(uint8_t media_w) +{ + switch (media_w) { + case 0x04: + return 3.5; + case 0x06: + return 6.0; + case 0x09: + return 9.0; + case 0x0C: + return 12.0; + case 0x12: + return 18.0; + case 0x18: + return 24.0; + case 0x24: + return 36.0; + case 0x15: + return 21.0; /* FLe width code */ + default: + return 0.0; + } +} + +static double print_dpi_from_profile(const ptouch_printer_profile_t *prof) +{ + if (!prof) + return 0.0; + if (prof->head_width_dots <= 128u) + return 180.0; + return 360.0; +} + +libptouch_err_t libptouch_get_current_media_info(libptouch_ctx *ctx, + libptouch_media_info_t *out_info) +{ + if (!ctx || !out_info) { + if (ctx) + ptouch_set_error(ctx, LIBPTOUCH_ERR_ARG, "null argument"); + return LIBPTOUCH_ERR_ARG; + } + + memset(out_info, 0, sizeof(*out_info)); + + uint8_t st[LIBPTOUCH_STATUS_LENGTH]; + libptouch_err_t v = libptouch_get_status(ctx, st); + if (v != LIBPTOUCH_OK) + return v; + + const ptouch_printer_profile_t *prof = + ptouch_layout_resolve_profile(ctx->usb_pid, st[4]); + if (!prof) { + ptouch_set_error(ctx, LIBPTOUCH_ERR_UNSUPPORTED, + "no layout profile for this printer"); + return LIBPTOUCH_ERR_UNSUPPORTED; + } + + uint16_t left_dots = 0, print_dots = 0, right_dots = 0; + v = ptouch_layout_from_status(prof, st[11], st[10], &left_dots, + &print_dots, &right_dots); + if (v != LIBPTOUCH_OK) { + ptouch_set_error(ctx, LIBPTOUCH_ERR_UNSUPPORTED, + "tape width/layout not supported for this media"); + return LIBPTOUCH_ERR_UNSUPPORTED; + } + + double tape_mm = tape_width_mm_from_code(st[10]); + if (tape_mm <= 0.0) { + ptouch_set_error(ctx, LIBPTOUCH_ERR_UNSUPPORTED, + "unknown tape width code"); + return LIBPTOUCH_ERR_UNSUPPORTED; + } + + out_info->media_width_code = st[10]; + out_info->media_kind_code = st[11]; + out_info->print_dpi = print_dpi_from_profile(prof); + out_info->feed_dpi = prof->margin_feed_dpi; + out_info->tape_width_mm = tape_mm; + out_info->printable_dots = print_dots; + out_info->left_margin_dots = left_dots; + out_info->right_margin_dots = right_dots; + out_info->min_feed_dots = 14u; + out_info->min_feed_mm = + ((double)out_info->min_feed_dots * 25.4) / prof->margin_feed_dpi; + + ptouch_set_error(ctx, LIBPTOUCH_OK, ""); + return LIBPTOUCH_OK; +} diff --git a/src/lib/libptouch_svg.c b/src/lib/libptouch_svg.c new file mode 100644 index 0000000..e92446c --- /dev/null +++ b/src/lib/libptouch_svg.c @@ -0,0 +1,211 @@ +/* + * libptouch — SVG -> 1bit packed raster (librsvg + cairo) + * + * Author: knb + * Email: knb@artif.org + */ + +#include "libptouch_internal.h" +#include "libptouch_layout.h" + +#ifdef LIBPTOUCH_HAS_RSVG +#include +#include +#endif + +#include +#include +#include + +libptouch_err_t libptouch_svg_file_to_raster_fit_current_tape( + libptouch_ctx *ctx, const char *path, + const libptouch_svg_options_t *options, uint8_t **out_raster, + size_t *out_raster_bytes, libptouch_raster_params_t *out_params) +{ +#ifndef LIBPTOUCH_HAS_RSVG + (void)path; + (void)options; + (void)out_raster; + (void)out_raster_bytes; + (void)out_params; + if (ctx) { + ptouch_set_error(ctx, LIBPTOUCH_ERR_UNSUPPORTED, + "SVG support is not built in (missing librsvg-2.0)"); + } + return LIBPTOUCH_ERR_UNSUPPORTED; +#else + if (!ctx || !path || !out_raster || !out_raster_bytes || !out_params) { + if (ctx) + ptouch_set_error(ctx, LIBPTOUCH_ERR_ARG, "null argument"); + return LIBPTOUCH_ERR_ARG; + } + if (!ctx->usb_open || !ctx->bulk_out_ep) { + ptouch_set_error(ctx, LIBPTOUCH_ERR_IO, + "not connected (open USB before SVG conversion)"); + return LIBPTOUCH_ERR_IO; + } + + uint8_t thr = LIBPTOUCH_PNG_DEFAULT_THRESHOLD; + if (options) + thr = options->threshold; + + *out_raster = NULL; + *out_raster_bytes = 0; + out_params->width_dots = 0; + out_params->height_dots = 0; + out_params->margin_mm = 0; + + uint8_t st[LIBPTOUCH_STATUS_LENGTH]; + libptouch_err_t v = libptouch_get_status(ctx, st); + if (v != LIBPTOUCH_OK) + return v; + + const ptouch_printer_profile_t *prof = + ptouch_layout_resolve_profile(ctx->usb_pid, st[4]); + if (!prof) { + ptouch_set_error(ctx, LIBPTOUCH_ERR_UNSUPPORTED, + "no layout profile for this printer"); + return LIBPTOUCH_ERR_UNSUPPORTED; + } + + uint16_t left_dots = 0, print_dots = 0, right_dots = 0; + v = ptouch_layout_from_status(prof, st[11], st[10], &left_dots, + &print_dots, &right_dots); + if (v != LIBPTOUCH_OK) { + ptouch_set_error(ctx, LIBPTOUCH_ERR_UNSUPPORTED, + "tape width/layout not supported for this media"); + return LIBPTOUCH_ERR_UNSUPPORTED; + } + (void)left_dots; + (void)right_dots; + + GError *gerr = NULL; + RsvgHandle *handle = rsvg_handle_new_from_file(path, &gerr); + if (!handle) { + const char *msg = (gerr && gerr->message) ? gerr->message : + "failed to load SVG"; + ptouch_set_error(ctx, LIBPTOUCH_ERR_IMAGE, msg); + if (gerr) + g_error_free(gerr); + return LIBPTOUCH_ERR_IMAGE; + } + + double svg_w = 0.0; + double svg_h = 0.0; + if (!rsvg_handle_get_intrinsic_size_in_pixels(handle, &svg_w, &svg_h) || + svg_w <= 0.0 || svg_h <= 0.0) { + g_object_unref(handle); + ptouch_set_error(ctx, LIBPTOUCH_ERR_IMAGE, + "invalid SVG dimensions"); + return LIBPTOUCH_ERR_IMAGE; + } + + /* + * libptouch_print_raster() transposes the incoming raster before packing + * lines, so printable tape width maps to the source raster height. + */ + double scale = (double)print_dots / svg_h; + if (scale <= 0.0 || !isfinite(scale)) { + g_object_unref(handle); + ptouch_set_error(ctx, LIBPTOUCH_ERR_IMAGE, "invalid SVG scale"); + return LIBPTOUCH_ERR_IMAGE; + } + + uint32_t out_h = (uint32_t)print_dots; + uint32_t out_w = (uint32_t)ceil(svg_w * scale); + if (out_h == 0) + out_h = 1; + if (out_h > 100000u) { + g_object_unref(handle); + ptouch_set_error(ctx, LIBPTOUCH_ERR_IMAGE, "scaled SVG too tall"); + return LIBPTOUCH_ERR_IMAGE; + } + + cairo_surface_t *surface = cairo_image_surface_create( + CAIRO_FORMAT_ARGB32, (int)out_w, (int)out_h); + if (cairo_surface_status(surface) != CAIRO_STATUS_SUCCESS) { + cairo_surface_destroy(surface); + g_object_unref(handle); + ptouch_set_error(ctx, LIBPTOUCH_ERR_NOMEM, + "cairo_image_surface_create failed"); + return LIBPTOUCH_ERR_NOMEM; + } + + cairo_t *cr = cairo_create(surface); + if (cairo_status(cr) != CAIRO_STATUS_SUCCESS) { + cairo_destroy(cr); + cairo_surface_destroy(surface); + g_object_unref(handle); + ptouch_set_error(ctx, LIBPTOUCH_ERR_NOMEM, "cairo_create failed"); + return LIBPTOUCH_ERR_NOMEM; + } + + cairo_set_source_rgb(cr, 1.0, 1.0, 1.0); + cairo_paint(cr); + RsvgRectangle viewport = { 0.0, 0.0, (double)out_w, (double)out_h }; + GError *render_err = NULL; + if (!rsvg_handle_render_document(handle, cr, &viewport, &render_err)) { + cairo_destroy(cr); + cairo_surface_destroy(surface); + g_object_unref(handle); + const char *msg = (render_err && render_err->message) ? + render_err->message : + "SVG render failed"; + ptouch_set_error(ctx, LIBPTOUCH_ERR_IMAGE, msg); + if (render_err) + g_error_free(render_err); + return LIBPTOUCH_ERR_IMAGE; + } + cairo_destroy(cr); + g_object_unref(handle); + + cairo_surface_flush(surface); + int stride = cairo_image_surface_get_stride(surface); + const uint8_t *pix = cairo_image_surface_get_data(surface); + + size_t row_bytes = ((size_t)out_w + 7u) / 8u; + if (out_h > SIZE_MAX / row_bytes) { + cairo_surface_destroy(surface); + ptouch_set_error(ctx, LIBPTOUCH_ERR_IMAGE, "raster size overflow"); + return LIBPTOUCH_ERR_IMAGE; + } + size_t total = row_bytes * (size_t)out_h; + uint8_t *out = (uint8_t *)calloc(1, total); + if (!out) { + cairo_surface_destroy(surface); + ptouch_set_error(ctx, LIBPTOUCH_ERR_NOMEM, "calloc raster failed"); + return LIBPTOUCH_ERR_NOMEM; + } + + for (uint32_t y = 0; y < out_h; y++) { + const uint32_t *src_row = + (const uint32_t *)(pix + (size_t)y * (size_t)stride); + uint8_t *dst_row = out + (size_t)y * row_bytes; + for (uint32_t x = 0; x < out_w; x++) { + uint32_t p = src_row[x]; + unsigned a = (p >> 24) & 0xFFu; + if (a < 128u) + continue; + unsigned r = (p >> 16) & 0xFFu; + unsigned g = (p >> 8) & 0xFFu; + unsigned b = p & 0xFFu; + unsigned yv = (77u * r + 150u * g + 29u * b) >> 8; + if (yv >= (unsigned)thr) + continue; + size_t byte = (size_t)x / 8u; + size_t bit = (size_t)x % 8u; + dst_row[byte] |= (uint8_t)(1u << (7u - bit)); + } + } + + cairo_surface_destroy(surface); + + out_params->width_dots = out_w; + out_params->height_dots = out_h; + out_params->margin_mm = 0; + *out_raster = out; + *out_raster_bytes = total; + ptouch_set_error(ctx, LIBPTOUCH_OK, ""); + return LIBPTOUCH_OK; +#endif +}