17 Commits

Author SHA1 Message Date
knb
d0e5846012 Bump project version metadata to 1.0.1.
Synchronize CMake, README, and Ruby gem version strings with the released v1.0.1 tag to keep build and packaging metadata consistent.

Made-with: Cursor
2026-04-17 05:21:39 +09:00
knb
d2fd6cc1f9 Fix print completion flow for PT-P710BT and PT-P900W.
Align raster protocol bytes and print-end sequencing by printer family, add safer status polling/retry behavior, and document the changes with regression coverage to prevent protocol regressions.

Made-with: Cursor
2026-04-17 05:17:22 +09:00
knb
779a50747d Bluetooth 対応状況追記 2026-04-17 03:53:13 +09:00
knb
29072dc20c Update docs and add sample image for label workflows.
Capture design notes for merge-print and PNG fit behavior, clean up Ruby README formatting, and include a new sample PNG for print testing.

Made-with: Cursor
2026-04-17 03:25:39 +09:00
knb
42a785f086 Fix PT-P710BT raster protocol handling and add regression coverage.
Align print command payload generation with model-specific protocol requirements, add verbose CLI diagnostics and robust status retries, and introduce protocol regression tests to prevent future GF/ESC i z regressions.

Made-with: Cursor
2026-04-17 03:24:00 +09:00
knb
e92273a747 Merge branch 'cursor/ruby-label-merge-cli' 2026-04-16 19:09:43 +09:00
knb
094f183994 ptouch-label/ptouch-print のオプション整理と trim-right 対応を反映
CLIヘルプ文言を簡潔化しつつ、右余白トリム機能と関連API・ドキュメント更新をまとめて取り込み、PNG/SVG/テンプレート経路での利用体験を揃える。

Made-with: Cursor
2026-04-16 19:09:37 +09:00
knb
32ab12f661 ptouch-label CLIを追加して差込印刷を拡張
コマンド名を機能に合わせて整理し、SVGテンプレート+JSON/YAMLの差込印刷とメディア情報取得を使いやすくする。

Made-with: Cursor
2026-04-16 14:49:08 +09:00
knb
33465c7299 Merge branch 'feature/svg-print-fit-tape-width' 2026-04-16 13:55:57 +09:00
knb
d683bf983a internalヘッダに関数説明コメントを追加
libptouch_internal.h の内部関数宣言に用途と戻り値の簡潔な説明を付け、実装読解時の確認コストを下げる。

Made-with: Cursor
2026-04-16 13:54:36 +09:00
knb
28a0c73f6a SVGサンプルファイルを追加
SVG印刷機能の動作確認に使えるサンプル画像を同梱し、dry-runや実機テストをすぐ試せるようにする。

Made-with: Cursor
2026-04-14 18:30:05 +09:00
knb
2ed0bfc0be SVG印刷対応とメディア情報APIを追加
SVG入力を現在テープ幅に自動フィットして印刷できるようにし、アプリ側が余白計算できるようにテープ幅・DPI・最小送り量を取得するAPIを追加する。

Made-with: Cursor
2026-04-14 18:29:24 +09:00
knb
f26a1186a3 libptouch の更新に追従 2026-04-13 11:39:31 +09:00
knb
ae67a8b288 PT-P750W, PT-P710BT のラスターコマンドマニュアル追加 2026-04-13 09:47:44 +09:00
knb
add08ba9b2 libptouch を複数ソースに分割し src/lib に配置
- core / usb / print / status / png と libptouch_internal.h に分割
- 旧単一ファイル src/libptouch.c を削除
- CMake のソース一覧と include パスを更新
- README・libptouch.h の参照パスを追随

Made-with: Cursor
2026-04-13 09:43:42 +09:00
knb
fdcb4d97fa aB.png を samples/ に移動
Made-with: Cursor
2026-04-12 16:09:41 +09:00
knb
c5c7c2ba52 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
2026-04-12 16:06:50 +09:00
46 changed files with 3666 additions and 1207 deletions

3
.gitignore vendored
View File

@@ -1,5 +1,8 @@
build/ build/
ruby/*.gem
ruby/.rubocop_cache/
*.o *.o
*.a *.a
ptouch-print ptouch-print

19
CHANGELOG.md Normal file
View File

@@ -0,0 +1,19 @@
# Changelog
## Unreleased
- Fix PT-P710BT/PT-P900W print completion flow and protocol bytes.
- Correct `G` raster transfer header length (`n1/n2`) from per-model payload size.
- Fix `ESC i z` page index (`n9`) and adjust print-information flags/media kind handling.
- Keep `ESC i A` disabled on 128-dot family and split `ESC i K` by head width.
- Use `0x0C -> 0x1A` end sequence on 560-dot family to complete feed/cut reliably.
- Improve status handling around print completion.
- Retry status reads with backoff and safer command ordering.
- Add verbose post-print short polling until print-end status is observed.
- Add small wait before `--status` command retrieval.
- Add protocol regression safeguards.
- Introduce `libptouch_protocol` helpers for shared command-byte generation.
- Add `protocol_regression_test` and wire it into CTest.
- Update documentation.
- Add `ctest` step to build instructions.
- Link to changelog from README.

View File

@@ -4,7 +4,7 @@
# Email: knb@artif.org # Email: knb@artif.org
cmake_minimum_required(VERSION 3.16) cmake_minimum_required(VERSION 3.16)
project(ptouch_label VERSION 1.0.0 LANGUAGES C) project(ptouch_label VERSION 1.0.1 LANGUAGES C)
set(CMAKE_C_STANDARD 11) set(CMAKE_C_STANDARD 11)
set(CMAKE_C_STANDARD_REQUIRED ON) set(CMAKE_C_STANDARD_REQUIRED ON)
@@ -14,6 +14,7 @@ include(GNUInstallDirs)
find_package(PkgConfig REQUIRED) find_package(PkgConfig REQUIRED)
pkg_check_modules(LIBUSB REQUIRED IMPORTED_TARGET libusb-1.0) pkg_check_modules(LIBUSB REQUIRED IMPORTED_TARGET libusb-1.0)
find_package(PNG REQUIRED) find_package(PNG REQUIRED)
pkg_check_modules(LIBRSVG IMPORTED_TARGET librsvg-2.0)
configure_file( configure_file(
"${CMAKE_CURRENT_SOURCE_DIR}/include/libptouch_version.h.in" "${CMAKE_CURRENT_SOURCE_DIR}/include/libptouch_version.h.in"
@@ -21,15 +22,50 @@ configure_file(
@ONLY @ONLY
) )
add_library(ptouch STATIC src/libptouch.c) set(LIBPTOUCH_SOURCES
src/lib/libptouch_core.c
src/lib/libptouch_usb.c
src/lib/libptouch_protocol.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
src/lib/libptouch_svg.c
)
add_library(ptouch STATIC ${LIBPTOUCH_SOURCES})
add_library(ptouch_shared SHARED ${LIBPTOUCH_SOURCES})
set_target_properties(ptouch_shared PROPERTIES OUTPUT_NAME ptouch
SOVERSION ${PROJECT_VERSION_MAJOR})
target_include_directories(ptouch PUBLIC target_include_directories(ptouch PUBLIC
"$<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/include>" "$<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/include>"
"$<BUILD_INTERFACE:${CMAKE_CURRENT_BINARY_DIR}/include>" "$<BUILD_INTERFACE:${CMAKE_CURRENT_BINARY_DIR}/include>"
"$<INSTALL_INTERFACE:${CMAKE_INSTALL_INCLUDEDIR}>" "$<INSTALL_INTERFACE:${CMAKE_INSTALL_INCLUDEDIR}>"
) )
target_include_directories(ptouch PRIVATE
"${CMAKE_CURRENT_SOURCE_DIR}/src/lib"
)
target_include_directories(ptouch_shared PUBLIC
"$<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/include>"
"$<BUILD_INTERFACE:${CMAKE_CURRENT_BINARY_DIR}/include>"
"$<INSTALL_INTERFACE:${CMAKE_INSTALL_INCLUDEDIR}>"
)
target_include_directories(ptouch_shared PRIVATE
"${CMAKE_CURRENT_SOURCE_DIR}/src/lib"
)
target_link_libraries(ptouch PRIVATE PkgConfig::LIBUSB PNG::PNG) 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) if(NOT MSVC)
target_compile_options(ptouch PRIVATE -Wall -Wextra -Wpedantic) target_compile_options(ptouch PRIVATE -Wall -Wextra -Wpedantic)
target_compile_options(ptouch_shared PRIVATE -Wall -Wextra -Wpedantic)
endif() endif()
add_executable(ptouch-print src/cli/main.c) add_executable(ptouch-print src/cli/main.c)
@@ -39,7 +75,22 @@ if(NOT MSVC)
target_compile_options(ptouch-print PRIVATE -Wall -Wextra -Wpedantic) target_compile_options(ptouch-print PRIVATE -Wall -Wextra -Wpedantic)
endif() endif()
enable_testing()
add_executable(ptouch-protocol-regression-test
tests/protocol_regression_test.c
src/lib/libptouch_protocol.c
)
target_include_directories(ptouch-protocol-regression-test PRIVATE
"${CMAKE_CURRENT_SOURCE_DIR}/src/lib"
)
if(NOT MSVC)
target_compile_options(ptouch-protocol-regression-test PRIVATE -Wall -Wextra -Wpedantic)
endif()
add_test(NAME protocol_regression_test COMMAND ptouch-protocol-regression-test)
install(TARGETS ptouch ARCHIVE DESTINATION "${CMAKE_INSTALL_LIBDIR}") 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(TARGETS ptouch-print RUNTIME DESTINATION "${CMAKE_INSTALL_BINDIR}")
install(FILES install(FILES
"${CMAKE_CURRENT_SOURCE_DIR}/include/libptouch.h" "${CMAKE_CURRENT_SOURCE_DIR}/include/libptouch.h"

View File

@@ -1,38 +1,52 @@
# ptouch_label # ptouch_label
**バージョン 1.0.0**(初回リリース) **バージョン 1.0.1**
Brother P-touch シリーズ向けのラベル印刷用 **C コアライブラリlibptouch** と、動作確認用 **CLI`ptouch-print`** のリポジトリです。 Brother P-touch シリーズ向けのラベル印刷用 **C コアライブラリlibptouch** と、動作確認用 **CLI`ptouch-print`** のリポジトリです。
保有機種: **PT-P900W**USB・ラスターコマンド 対象機種: **PT-P900W**560 ドットヘッド)、**PT-P750W** / **PT-P710BT**128 ドットヘッド・USB`libptouch_open_usb_vid_pid` に各機種の VID/PID を渡すP900W 既定は `libptouch_open_usb`。P750/P710 のラスター仕様は `reference/cv_ptp750w_710bt_jpn_raster_102.pdf`
現状は USB only / 将来 Bluetooth 対応予定。
## レイアウト ## レイアウト
| パス | 内容 |
|------|------| | パス | 内容 |
| `include/libptouch.h` | 公開 API | | ----------------------- | --------------------------------------------------------------------------- |
| `src/libptouch.c` | ライブラリ本体(スタブ) | | `include/libptouch.h` | 公開 API |
| `src/cli/main.c` | `ptouch-print` エントリ | | `src/lib/libptouch_*.c` | ライブラリ本体core / usb / print / status / png / svg |
| `samples/` | 試験用サンプル画像の置き場PNG 等) | | `src/cli/main.c` | `ptouch-print` エントリ |
| `reference/` | 仕様・参考資料(例: ラスター PDF | | `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 ```bash
cmake -S . -B build cmake -S . -B build
cmake --build build cmake --build build
ctest --test-dir build --output-on-failure
``` ```
成果物(`build/` 以下): 成果物(`build/` 以下):
- `libptouch.a` — 静的ライブラリ - `libptouch.a` — 静的ライブラリ
- `libptouch.so` — 共有ライブラリRuby FFI 用)
- `ptouch-print` — CLI - `ptouch-print` — CLI
### Ruby gem
共有ライブラリをビルドしたうえで、`ruby/``bundle install``gem build libptouch.gemspec` など(手順は `ruby/README.md`)。
## CLI の使い方(雛形) ## CLI の使い方(雛形)
**PNG**(拡張子 `.png` または PNG シグネチャ)の場合は幅・高さは画像から取得します。任意で `-t`0255で二値化しきい値を指定できます。 **PNG**(拡張子 `.png` または PNG シグネチャ)の場合は幅・高さは画像から取得します。
**SVG**(拡張子 `.svg`の場合は現在装着中テープの印字可能ドット幅に合わせて自動拡大・縮小しますUSB 接続が必要)。
`--trim-right[=DOTS]` で右側空白列を削減できますDOTS 省略時は左余白ドット、取得失敗時は 0
任意で `-t`0255で二値化しきい値を指定できます。
**1bit packed ラスター**(行優先、行あたり `ceil(width/8)` バイト × 行数)の場合は `-f` に加え `-w` / `-H` が必須です。 **1bit packed ラスター**(行優先、行あたり `ceil(width/8)` バイト × 行数)の場合は `-f` に加え `-w` / `-H` が必須です。
@@ -41,6 +55,11 @@ cmake --build build
```bash ```bash
# PNG — 検証のみUSB 不要) # PNG — 検証のみUSB 不要)
./build/ptouch-print -n -f label.png ./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
# PNG — しきい値を指定 # PNG — しきい値を指定
./build/ptouch-print -n -f label.png -t 160 ./build/ptouch-print -n -f label.png -t 160
@@ -53,7 +72,12 @@ cmake --build build
# USB 接続時 # USB 接続時
./build/ptouch-print -f label.png ./build/ptouch-print -f label.png
./build/ptouch-print -f label.svg
./build/ptouch-print -f sample.raster -w 128 -H 64 ./build/ptouch-print -f sample.raster -w 128 -H 64
# PT-P750W / PT-P710BT`lsusb` の PID に合わせる)
./build/ptouch-print --status -p 0x2062
./build/ptouch-print -f label.png -p 0x20af
``` ```
`-n``--dry-run`)では読み込みと `libptouch_check_raster` まで実行します。 `-n``--dry-run`)では読み込みと `libptouch_check_raster` まで実行します。
@@ -64,8 +88,10 @@ cmake --build build
- `libptouch_create` / `libptouch_destroy` — コンテキスト - `libptouch_create` / `libptouch_destroy` — コンテキスト
- `libptouch_open_usb` / `libptouch_open_usb_vid_pid` / `libptouch_close` — USBlibusb・VID/PID - `libptouch_open_usb` / `libptouch_open_usb_vid_pid` / `libptouch_close` — USBlibusb・VID/PID
- `libptouch_get_status` / `libptouch_status_fprint` — ステータス情報リクエストESC i Sの応答 - `libptouch_get_status` / `libptouch_status_fprint` — ステータス情報リクエストESC i Sの応答
- `libptouch_get_current_media_info` — 現在テープ幅(mm)・印字可能幅(dots)・DPI・最小余白(mm)を取得
- `libptouch_check_raster` — ラスターバッファの検証のみ - `libptouch_check_raster` — ラスターバッファの検証のみ
- `libptouch_png_file_to_raster` / `libptouch_free_raster` — PNG を 1bit ラスターに変換libpng - `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_print_raster` — ラスター印刷USB・PDF のコマンド列、内部で転置)
- `libptouch_strerror` / `libptouch_last_error` — エラー情報 - `libptouch_strerror` / `libptouch_last_error` — エラー情報
@@ -73,12 +99,13 @@ cmake --build build
## PT-P900W / Linux でのメモ ## PT-P900W / Linux でのメモ
- 接続は **libusb-1.0** のみ。機種ごとに **VID/PID**`lsusb` 等)を調べ、`libptouch_open_usb_vid_pid` に渡すか、既定の PT-P900W なら `libptouch_open_usb` を使います。 - 接続は **libusb-1.0** のみ。機種ごとに **VID/PID**`lsusb` 等)を調べ、`libptouch_open_usb_vid_pid` に渡すか、既定の PT-P900W なら `libptouch_open_usb` を使います。PT-P750W は **04f9:2062**、PT-P710BT は **04f9:20af**`include/libptouch.h` の定数参照)。
- ラスターコマンドの詳細は **`reference/` の PDF** および Brother 公開資料に沿って `src/libptouch.c` に実装してください。 - PT-P750W / PT-P710BT ではラスター幅方向は **180 dpi**P900W は 360 dpi。PNG から印刷する場合は解像度に合わせて画像を用意してください。
- ラスターコマンドの詳細は `**reference/` の PDF** および Brother 公開資料に沿って `src/lib/libptouch_*.c` に実装してください。
### Ubuntu で sudo なしで USB を開くudev ### Ubuntu で sudo なしで USB を開くudev
既定の **04f9:2085**PT-P900W向けルールを `udev/99-ptouch-label-brother.rules` に置いています。 **04f9:2085**PT-P900W、**04f9:2062**PT-P750W、**04f9:20af**PT-P710BT向けルールを `udev/99-ptouch-label-brother.rules` に置いています。
```bash ```bash
sudo cp udev/99-ptouch-label-brother.rules /etc/udev/rules.d/ sudo cp udev/99-ptouch-label-brother.rules /etc/udev/rules.d/
@@ -94,3 +121,7 @@ sudo usermod -aG plugdev "$USER"
## ライセンス ## ライセンス
[MIT License](LICENSE)`LICENSE` ファイルを参照)。 [MIT License](LICENSE)`LICENSE` ファイルを参照)。
## 変更履歴
リリース間の変更点は `CHANGELOG.md` を参照してください。

View File

@@ -8,7 +8,8 @@
/** /**
* libptouch — Brother P-touch ラスター印刷 (USB) 用 C API * libptouch — Brother P-touch ラスター印刷 (USB) 用 C API
* *
* 対象例: PT-P900Wラスターコマンド)。実装は src/libptouch.c を参照 * 対象例: PT-P900W560 ドットヘッド、PT-P750W / PT-P710BT128 ドットヘッド)
* 実装は src/lib/libptouch_*.c を参照。
*/ */
#ifndef LIBPTOUCH_H #ifndef LIBPTOUCH_H
@@ -41,6 +42,9 @@ typedef enum {
/** lsusb 例: PT-P900W — Brother Industries, Ltd (04f9:2085) */ /** lsusb 例: PT-P900W — Brother Industries, Ltd (04f9:2085) */
#define LIBPTOUCH_USB_VID_BROTHER 0x04f9u #define LIBPTOUCH_USB_VID_BROTHER 0x04f9u
#define LIBPTOUCH_USB_PID_PTP900W 0x2085u #define LIBPTOUCH_USB_PID_PTP900W 0x2085u
/** PT-P750W / PT-P710BTcv_ptp750w_710bt_jpn_raster_102.pdf Appendix A */
#define LIBPTOUCH_USB_PID_PTP750W 0x2062u
#define LIBPTOUCH_USB_PID_PTP710BT 0x20afu
libptouch_ctx *libptouch_create(void); libptouch_ctx *libptouch_create(void);
void libptouch_destroy(libptouch_ctx *ctx); void libptouch_destroy(libptouch_ctx *ctx);
@@ -69,6 +73,19 @@ typedef struct {
uint8_t margin_mm; /**< 余白など(機種・仕様に合わせて使用) */ uint8_t margin_mm; /**< 余白など(機種・仕様に合わせて使用) */
} libptouch_raster_params_t; } 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; /**< 送り方向 DPIESC 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 用。 * バッファサイズとパラメータの整合性のみ検査USB 不要)。--dry-run 用。
*/ */
@@ -77,9 +94,27 @@ libptouch_err_t libptouch_check_raster(libptouch_ctx *ctx,
const libptouch_raster_params_t *params); const libptouch_raster_params_t *params);
/** /**
* 1 ビット packed ラスターを USB で印刷cv_ptp900_jpn_raster_102.pdf 準拠)。 * 現在装着テープの情報を取得するUSB 接続済み必須)。
* ステータスと機種プロファイルからテープ幅・印字可能幅・最小余白量を返す。
*/
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 準拠)。
* 印字前にステータスでテープ幅を読み、印刷可能ドット内に画像を中央配置する。 * 印字前にステータスでテープ幅を読み、印刷可能ドット内に画像を中央配置する。
* width_dots は装着テープの印刷可能幅以下であること。360dpi 相当のドット列を想定。 * width_dots は装着テープの印刷可能幅以下であること。PT-P900 系は幅 360dpi、
* PT-P750W / PT-P710BT は幅 180dpicv_ptp750w_710bt_jpn_raster_102.pdfのドット列を想定。
* @param margin_mm 余白フィード量。0 のとき PDF の最小 1mm14 ドット)相当を送る。 * @param margin_mm 余白フィード量。0 のとき PDF の最小 1mm14 ドット)相当を送る。
* 印刷時は内部でドット列を転置する(テープ幅方向とバッファの縦横の対応)。 * 印刷時は内部でドット列を転置する(テープ幅方向とバッファの縦横の対応)。
* @param data 1 行あたり width_dots ビットを ceil(width_dots/8) バイトで並べた連続領域 * @param data 1 行あたり width_dots ビットを ceil(width_dots/8) バイトで並べた連続領域
@@ -96,6 +131,10 @@ typedef struct {
uint8_t threshold; /**< 輝度がこれ未満なら黒1、以上なら白0 */ uint8_t threshold; /**< 輝度がこれ未満なら黒1、以上なら白0 */
} libptouch_png_options_t; } libptouch_png_options_t;
typedef struct {
uint8_t threshold; /**< 輝度がこれ未満なら黒1、以上なら白0 */
} libptouch_svg_options_t;
/** /**
* PNG ファイルを読み、1bit packed ラスターに変換するlibpng * PNG ファイルを読み、1bit packed ラスターに変換するlibpng
* @param out_raster malloc 済みバッファのポインタを返す。不要時は @ref libptouch_free_raster で解放。 * @param out_raster malloc 済みバッファのポインタを返す。不要時は @ref libptouch_free_raster で解放。
@@ -105,6 +144,17 @@ libptouch_err_t libptouch_png_file_to_raster(libptouch_ctx *ctx, const char *pat
uint8_t **out_raster, size_t *out_raster_bytes, uint8_t **out_raster, size_t *out_raster_bytes,
libptouch_raster_params_t *out_params); 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); void libptouch_free_raster(uint8_t *raster);
/** ステータス情報リクエストESC i Sの応答バイト数cv_ptp900_jpn_raster_102.pdf */ /** ステータス情報リクエストESC i Sの応答バイト数cv_ptp900_jpn_raster_102.pdf */

Binary file not shown.

44
reference/note.adoc Normal file
View File

@@ -0,0 +1,44 @@
= pTouch Label ノート
== 差込印刷機能追加(案)
SVG ファイルを template として、JSON/YAML ファイルで受け取った内容を差込印刷する。
SVG template の text element の data-field attribute をキーにして 印字テキストを JSON/YAML ファイルのデータで置き換える
----
<svg xmlns="http://www.w3.org/2000/svg" width="400" height="400" viewBox="0 0 124 124" fill="none">
<rect width="124" height="124" rx="24" fill="#FFFFFFFF"/>
<text fill="#000000" x="5" y="60" data-field="label" font-size="40">
Label
</text>
</svg>
----
という SVG ファイルと
^^^^
label: "ラベル"
----
という YAML ファイルを受け取ったら、
SVG の text の中身のテキストを"ラベル" に置き換えるという感じです。
== PNG 自動拡大・縮小(後日実装メモ)
PNG でも、現在テープ幅printable_dotsに合わせた自動拡大・縮小を可能にしたい。
既定動作は現状維持(自動拡大・縮小 OFFとし、オプションで ON/OFF を切り替える。
実装方針(案):
- lib 側に「現在テープ幅へ raster をフィットする」共通 API を追加する。
- PNG/SVG とも最終的に raster になるため、fit 処理は共通化する。
- CLI には `--fit-current-tape`(仮)を追加し、明示時のみ有効化する。
- まずは `contain` 相当(縦横比維持で収める)を実装する。
検討ポイント:
- 1bit 画像の縮小品質(単純 nearest だと潰れやすい)。
- 既存の length x width 表示/内部座標との整合。
- 既存ユーザー互換性(デフォルト OFF を維持)。

66
ruby/.rubocop.yml Normal file
View File

@@ -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

9
ruby/Gemfile Normal file
View File

@@ -0,0 +1,9 @@
# frozen_string_literal: true
source "https://rubygems.org"
gemspec
group :development do
gem "rubocop", "~> 1.86", require: false
end

102
ruby/Gemfile.lock Normal file
View File

@@ -0,0 +1,102 @@
PATH
remote: .
specs:
libptouch (1.0.0)
ffi (~> 1.15)
rexml
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)
rexml (3.4.4)
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.86)
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
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
unicode-display_width (3.2.0) sha256=0cdd96b5681a5949cdbc2c55e7b420facae74c4aaf9a9815eee1087cb1853c42
unicode-emoji (4.2.0) sha256=519e69150f75652e40bf736106cfbc8f0f73aa3fb6a65afe62fefa7f80b0f80f
BUNDLED WITH
4.0.6

106
ruby/README.md Normal file
View File

@@ -0,0 +1,106 @@
# libptouchRuby gem
[ptouch_label](../) の **libptouch** を [ffi](https://github.com/ffi/ffi) 経由で使うための Gem です。C ライブラリと同様、**PT-P900W**(既定 USB PID、**PT-P750W** / **PT-P710BT** などは `open_usb_vid_pid` または CLI の `-p` で選びます。
## 前提
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.1.gem
```
ビルド済みの `../build/libptouch.so` を自動で読みに行きます。別のパスにある場合は環境変数で指定できます。
```bash
export LIBPTOUCH_LIB=/usr/local/lib/libptouch.so
```
`cmake --install` で共有ライブラリをインストールした場合は、通常は `libptouch` 名でローダが解決します。)
## コマンド `ptouch-label`PNG/SVG
C の `ptouch-print` と同様の流れで、**PNG/SVG 入力**`-w`/`-H` や 1bit ラスターは扱いません)を扱います。`gem install` 後は PATH に `ptouch-label` が入ります。
SVG は現在装着テープの印字可能幅に合わせて自動拡大・縮小しますUSB 接続が必要)。
後方互換のため `ptouch-print-png` も引き続き使えます。
オプションは C 側に合わせ、`**-p` / `--pid`** で USB 製品 ID16 進可)を指定できます。省略時は 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` にフォールバックします。
開発ツリーからそのまま試す例:
```bash
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
bundle exec ruby -I lib exe/ptouch-label -f ../samples/your.png -p 0x20af
```
## 使用例
```ruby
require "libptouch"
Libptouch::Context.new.tap do |ctx|
ctx.open_usb
# PT-P750W など別 PID のとき:
# ctx.open_usb_vid_pid(Libptouch::USB_VID_BROTHER, Libptouch::USB_PID_PTP750W)
p ctx.status_bytes.bytesize # => 32
p ctx.status_hash[:tape_kind] # => {:code=>..., :label=>"ラミネートテープ"} など
p ctx.status_hash[:model] # => {:code=>..., :name=>"PT-P750W"} など(対応機種のみ名前あり)
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_vid_pid(Libptouch::USB_VID_BROTHER, Libptouch::USB_PID_PTP750W)
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-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`
- `Libptouch.parse_status(raw)` … 32 バイトを Hash に展開(機種・テープ幅・**テープ種類**・色・**状態status_kind**・エラービット・`raw_hex` など)
- C の `libptouch_status_fprint``FILE` *)は FFI からはバインドしていません。テキスト出力の代わりに `parse_status` / `status_hash` を使ってください。

6
ruby/exe/ptouch-label Executable file
View File

@@ -0,0 +1,6 @@
#!/usr/bin/env ruby
# frozen_string_literal: true
require "libptouch/cli/label_print"
exit Libptouch::Cli::LabelPrint.run(ARGV)

6
ruby/exe/ptouch-print-png Executable file
View File

@@ -0,0 +1,6 @@
#!/usr/bin/env ruby
# frozen_string_literal: true
require "libptouch/cli/label_print"
exit Libptouch::Cli::LabelPrint.run(ARGV)

30
ruby/lib/libptouch.rb Normal file
View File

@@ -0,0 +1,30 @@
# 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
USB_PID_PTP750W = 0x2062
USB_PID_PTP710BT = 0x20af
STATUS_LENGTH = 32
PNG_DEFAULT_THRESHOLD = 128
end

View File

@@ -0,0 +1,72 @@
# 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 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
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_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
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
end

View File

@@ -0,0 +1,409 @@
# 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 相当の CLI1bit ラスター経路なし)。
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 trim_right_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,
trim_right: nil,
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 製品 ID16 進可)。既定 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 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?
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,
"しきい値 0255既定 #{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("--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", "ステータスを 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
--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[:trim_right].nil? || !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,
"しきい値 0255既定 #{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("--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", "ステータスを 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 (<tspan>, 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
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
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
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]
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 && !usb_opened
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 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)"
2
end
end
end
end

View File

@@ -0,0 +1,8 @@
# Backward-compatibility shim.
require "libptouch/cli/label_print"
module Libptouch
module Cli
PngPrint = LabelPrint
end
end

View File

@@ -0,0 +1,168 @@
# 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.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 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 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))
buf.read_bytes(STATUS_LENGTH)
end
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_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],
min_feed_mm: info[:min_feed_mm]
}
end
end
end

View 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

View File

@@ -0,0 +1,199 @@
# 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",
0x68 => "PT-P750W",
0x76 => "PT-P710BT"
}.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

View File

@@ -0,0 +1,5 @@
# frozen_string_literal: true
module Libptouch
VERSION = "1.0.1"
end

33
ruby/libptouch.gemspec Normal file
View File

@@ -0,0 +1,33 @@
# 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.",
"Supports PT-P900W, PT-P750W, PT-P710BT and related open_usb_vid_pid / --pid.",
"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-label", "ptouch-print-png"]
spec.require_paths = ["lib"]
spec.add_dependency "ffi", "~> 1.15"
spec.add_dependency "rexml"
end

View File

@@ -1,9 +1,16 @@
# samples # samples
試験・デモ用のサンプル画像PNG など)を置くディレクトリです。リポジトリに含める場合はライセンス・著作権に注意してください。 試験・デモ用のサンプル画像PNG/SVGや差込印刷データJSON/YAML)を置くディレクトリです。リポジトリに含める場合はライセンス・著作権に注意してください。
例(ドライラン): 例(ドライラン):
```bash ```bash
./build/ptouch-print -n -f samples/your.png ./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
```

View File

Before

Width:  |  Height:  |  Size: 8.1 KiB

After

Width:  |  Height:  |  Size: 8.1 KiB

BIN
samples/aBw70.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.2 KiB

5
samples/merge_data.json Normal file
View File

@@ -0,0 +1,5 @@
{
"title": "出荷ラベル",
"left": "品番 ABC-123",
"right": "数量 24"
}

3
samples/merge_data.yml Normal file
View File

@@ -0,0 +1,3 @@
title: "出荷ラベル"
left: "品番 ABC-123"
right: "数量 24"

View File

@@ -0,0 +1,9 @@
<svg xmlns="http://www.w3.org/2000/svg" width="720" height="220" viewBox="0 0 720 220">
<rect width="720" height="220" fill="#FFFFFF" />
<text x="24" y="86" data-field="title" font-size="56" fill="#000000">TITLE</text>
<text x="24" y="160" font-size="36" fill="#000000">
<tspan data-field="left">LEFT</tspan>
<tspan dx="20">/</tspan>
<tspan dx="20" data-field="right">RIGHT</tspan>
</text>
</svg>

After

Width:  |  Height:  |  Size: 417 B

40
samples/sample.svg Normal file
View File

@@ -0,0 +1,40 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
width="84.467712mm"
height="27.798302mm"
viewBox="0 0 84.467714 27.798302"
version="1.1"
id="svg5"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<defs
id="defs2" />
<g
id="layer1"
transform="matrix(1.7339801,0,0,1.6120341,-33.843459,-17.498256)">
<text
xml:space="preserve"
style="font-style:normal;font-variant:normal;font-weight:500;font-stretch:normal;font-size:3.79808px;font-family:'Noto Sans CJK JP';-inkscape-font-specification:'Noto Sans CJK JP, Medium';font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-variant-east-asian:normal;fill:#000000;stroke-width:0.383995;stroke-dasharray:none"
x="20.38328"
y="14.980056"
id="text234"><tspan
id="tspan232"
style="font-style:normal;font-variant:normal;font-weight:500;font-stretch:normal;font-size:3.79808px;font-family:'Noto Sans CJK JP';-inkscape-font-specification:'Noto Sans CJK JP, Medium';font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-variant-east-asian:normal;stroke-width:0.383995;stroke-dasharray:none"
x="20.38328"
y="14.980056">印刷出来るかな?</tspan><tspan
style="font-style:normal;font-variant:normal;font-weight:500;font-stretch:normal;font-size:3.79808px;font-family:'Noto Sans CJK JP';-inkscape-font-specification:'Noto Sans CJK JP, Medium';font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-variant-east-asian:normal;stroke-width:0.383995;stroke-dasharray:none"
x="20.38328"
y="18.948805"
id="tspan236">取り敢えず何か書いてみる</tspan><tspan
style="font-style:normal;font-variant:normal;font-weight:500;font-stretch:normal;font-size:3.79808px;font-family:'Noto Sans CJK JP';-inkscape-font-specification:'Noto Sans CJK JP, Medium';font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-variant-east-asian:normal;stroke-width:0.383995;stroke-dasharray:none"
x="20.38328"
y="22.917555"
id="tspan238">4行くらい入りそう</tspan><tspan
style="font-style:normal;font-variant:normal;font-weight:500;font-stretch:normal;font-size:3.79808px;font-family:'Noto Sans CJK JP';-inkscape-font-specification:'Noto Sans CJK JP, Medium';font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-variant-east-asian:normal;stroke-width:0.383995;stroke-dasharray:none"
x="20.38328"
y="26.886305"
id="tspan240">123456</tspan></text>
</g>
</svg>

After

Width:  |  Height:  |  Size: 2.7 KiB

View File

@@ -13,6 +13,7 @@
#include <stdlib.h> #include <stdlib.h>
#include <string.h> #include <string.h>
#include <strings.h> #include <strings.h>
#include <unistd.h>
static void usage(const char *argv0) static void usage(const char *argv0)
{ {
@@ -21,13 +22,17 @@ static void usage(const char *argv0)
" -f, --file PATH 入力ファイル\n" " -f, --file PATH 入力ファイル\n"
" -w, --width DOTS 1bit ラスター時: 幅(ドット)\n" " -w, --width DOTS 1bit ラスター時: 幅(ドット)\n"
" -H, --height DOTS 1bit ラスター時: 高さ(ドット)\n" " -H, --height DOTS 1bit ラスター時: 高さ(ドット)\n"
" -t, --threshold N PNG 二値化しきい値 0255既定 %u、PNG のみ\n" " -t, --threshold N しきい値 0255既定 %u、PNG/SVG\n"
" --trim-right[=DOTS] 右側空白を削減(省略時は左余白、失敗時 0\n"
" -n, --dry-run 読み込みと check_raster のみUSB なし)\n" " -n, --dry-run 読み込みと check_raster のみUSB なし)\n"
" -S, --status USB 接続プリンタのステータス(テープ種・幅・色等)を表示して終了\n" " -v, --verbose 印刷前情報と印刷後ステータスを標準出力\n"
" -p, --pid HEX USB 製品 ID既定: P900W の 0x2085。例: P750W 0x2062、P710BT 0x20af\n"
" -S, --status ステータスを表示して終了\n"
" -V, --version バージョンを表示して終了\n" " -V, --version バージョンを表示して終了\n"
" -h, --help このヘルプ\n" " -h, --help このヘルプ\n"
"\n" "\n"
"PNG の場合は幅・高さはファイルから取得-w/-H 不要)。\n" "PNG は画像サイズを使用-w/-H 不要)。\n"
"SVG は現在テープ幅に自動フィットUSB 必須)。\n"
"1bit packed ラスター(行優先)の場合は -f -w -H が必須。\n" "1bit packed ラスター(行優先)の場合は -f -w -H が必須。\n"
"--status のときは -f は不要(他オプションは無視されます)。\n", "--status のときは -f は不要(他オプションは無視されます)。\n",
argv0, (unsigned)LIBPTOUCH_PNG_DEFAULT_THRESHOLD); argv0, (unsigned)LIBPTOUCH_PNG_DEFAULT_THRESHOLD);
@@ -53,6 +58,14 @@ static int input_is_png(const char *path)
return memcmp(sig, png_magic, sizeof(png_magic)) == 0; 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) static int read_file(const char *path, uint8_t **out, size_t *out_len)
{ {
FILE *fp = fopen(path, "rb"); FILE *fp = fopen(path, "rb");
@@ -90,12 +103,80 @@ static int read_file(const char *path, uint8_t **out, size_t *out_len)
return 0; return 0;
} }
static void verbose_print_pre_print_info(libptouch_ctx *ctx,
const libptouch_raster_params_t *params,
size_t data_len)
{
if (!ctx || !params)
return;
printf("=== verbose: pre-print info ===\n");
printf("raster bytes: %zu\n", data_len);
printf("raster size: %ux%u dots (length x width)\n",
(unsigned)params->width_dots, (unsigned)params->height_dots);
printf("margin_mm: %u\n", (unsigned)params->margin_mm);
libptouch_media_info_t mi;
if (libptouch_get_current_media_info(ctx, &mi) == LIBPTOUCH_OK) {
printf("media width code: 0x%02X\n", (unsigned)mi.media_width_code);
printf("media kind code: 0x%02X\n", (unsigned)mi.media_kind_code);
printf("tape width: %.1f mm\n", mi.tape_width_mm);
printf("printable dots: %u\n", (unsigned)mi.printable_dots);
printf("left/right margins: %u/%u dots\n",
(unsigned)mi.left_margin_dots, (unsigned)mi.right_margin_dots);
printf("print/feed dpi: %.1f/%.1f\n", mi.print_dpi, mi.feed_dpi);
} else {
printf("media info: unavailable (%s)\n", libptouch_strerror(ctx));
}
uint8_t st[LIBPTOUCH_STATUS_LENGTH];
if (libptouch_get_status(ctx, st) == LIBPTOUCH_OK) {
libptouch_status_fprint(stdout, st);
} else {
printf("status: unavailable (%s)\n", libptouch_strerror(ctx));
}
}
static void verbose_print_post_print_status(libptouch_ctx *ctx, const char *label)
{
if (!ctx)
return;
printf("=== verbose: post-print status (%s) ===\n", label);
/*
* 印刷直後は phase change (status[18] != 0x00) が続くことがあるため、
* 短時間ポーリングで最終状態まで追跡する。
*/
const int max_polls = 12; /* ~2.4s */
const useconds_t poll_interval_us = 200000;
for (int i = 0; i < max_polls; i++) {
uint8_t st[LIBPTOUCH_STATUS_LENGTH];
if (libptouch_get_status(ctx, st) != LIBPTOUCH_OK) {
printf("status poll %d/%d: unavailable (%s)\n", i + 1,
max_polls, libptouch_strerror(ctx));
} else {
printf("status poll %d/%d:\n", i + 1, max_polls);
libptouch_status_fprint(stdout, st);
if (st[18] == 0x00u) {
printf("post-print polling settled: print end.\n");
return;
}
}
usleep(poll_interval_us);
}
printf("post-print polling timeout: phase did not settle.\n");
}
int main(int argc, char **argv) int main(int argc, char **argv)
{ {
const char *file = NULL; const char *file = NULL;
unsigned width = 0, height = 0; unsigned width = 0, height = 0;
unsigned threshold = LIBPTOUCH_PNG_DEFAULT_THRESHOLD; unsigned threshold = LIBPTOUCH_PNG_DEFAULT_THRESHOLD;
unsigned usb_pid_arg = 0;
int dry_run = 0; int dry_run = 0;
int verbose = 0;
int trim_right_enabled = 0;
int trim_right_auto = 0;
unsigned trim_right_dots = 0;
int has_threshold = 0; int has_threshold = 0;
int want_status = 0; int want_status = 0;
static struct option longopts[] = { static struct option longopts[] = {
@@ -103,7 +184,10 @@ int main(int argc, char **argv)
{ "height", required_argument, NULL, 'H' }, { "height", required_argument, NULL, 'H' },
{ "file", required_argument, NULL, 'f' }, { "file", required_argument, NULL, 'f' },
{ "threshold", required_argument, NULL, 't' }, { "threshold", required_argument, NULL, 't' },
{ "trim-right", optional_argument, NULL, 'r' },
{ "dry-run", no_argument, NULL, 'n' }, { "dry-run", no_argument, NULL, 'n' },
{ "verbose", no_argument, NULL, 'v' },
{ "pid", required_argument, NULL, 'p' },
{ "status", no_argument, NULL, 'S' }, { "status", no_argument, NULL, 'S' },
{ "version", no_argument, NULL, 'V' }, { "version", no_argument, NULL, 'V' },
{ "help", no_argument, NULL, 'h' }, { "help", no_argument, NULL, 'h' },
@@ -111,7 +195,7 @@ int main(int argc, char **argv)
}; };
int c; int c;
while ((c = getopt_long(argc, argv, "w:H:f:t:nhSV", longopts, NULL)) != while ((c = getopt_long(argc, argv, "w:H:f:t:r::p:nvhSV", longopts, NULL)) !=
-1) { -1) {
switch (c) { switch (c) {
case 'w': case 'w':
@@ -134,6 +218,30 @@ int main(int argc, char **argv)
case 'n': case 'n':
dry_run = 1; dry_run = 1;
break; break;
case 'v':
verbose = 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) {
fprintf(stderr, "-p/--pid must be 1..0xFFFF\n");
return 2;
}
break;
case 'S': case 'S':
want_status = 1; want_status = 1;
break; break;
@@ -159,12 +267,21 @@ int main(int argc, char **argv)
fprintf(stderr, "libptouch_create failed\n"); fprintf(stderr, "libptouch_create failed\n");
return 1; return 1;
} }
libptouch_err_t se = libptouch_open_usb(sctx); libptouch_err_t se =
usb_pid_arg != 0
? libptouch_open_usb_vid_pid(
sctx, LIBPTOUCH_USB_VID_BROTHER,
(uint16_t)usb_pid_arg)
: libptouch_open_usb(sctx);
if (se != LIBPTOUCH_OK) { if (se != LIBPTOUCH_OK) {
fprintf(stderr, "open_usb: %s\n", libptouch_strerror(sctx)); fprintf(stderr, "open_usb: %s\n", libptouch_strerror(sctx));
libptouch_destroy(sctx); libptouch_destroy(sctx);
return 1; return 1;
} }
/* 印刷直後などは内部処理中で最初のステータス応答が不安定なことがあるため、
* 少し待ってからステータスを取りに行く。 */
usleep(300000); /* 300ms */
uint8_t st[LIBPTOUCH_STATUS_LENGTH]; uint8_t st[LIBPTOUCH_STATUS_LENGTH];
se = libptouch_get_status(sctx, st); se = libptouch_get_status(sctx, st);
if (se != LIBPTOUCH_OK) { if (se != LIBPTOUCH_OK) {
@@ -186,20 +303,21 @@ int main(int argc, char **argv)
} }
int png = input_is_png(file); int png = input_is_png(file);
if (png) { int svg = input_is_svg(file);
if (png || svg) {
if (width != 0 || height != 0) if (width != 0 || height != 0)
fprintf(stderr, fprintf(stderr,
"warning: -w/-H ignored for PNG (using image size)\n"); "warning: -w/-H ignored for image inputs\n");
} else { } else {
if (width == 0 || height == 0) { if (width == 0 || height == 0) {
fprintf(stderr, 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]); usage(argv[0]);
return 2; return 2;
} }
if (has_threshold) if (has_threshold)
fprintf(stderr, fprintf(stderr,
"warning: -t applies to PNG only (ignored)\n"); "warning: -t applies to PNG/SVG only (ignored)\n");
} }
libptouch_ctx *ctx = libptouch_create(); libptouch_ctx *ctx = libptouch_create();
@@ -212,6 +330,8 @@ int main(int argc, char **argv)
size_t data_len = 0; size_t data_len = 0;
libptouch_raster_params_t params = { 0, 0, 0 }; libptouch_raster_params_t params = { 0, 0, 0 };
libptouch_err_t e; libptouch_err_t e;
int usb_opened = 0;
int data_from_lib = 0;
if (png) { if (png) {
libptouch_png_options_t opt = { .threshold = (uint8_t)threshold }; libptouch_png_options_t opt = { .threshold = (uint8_t)threshold };
@@ -224,6 +344,30 @@ int main(int argc, char **argv)
libptouch_destroy(ctx); libptouch_destroy(ctx);
return 1; return 1;
} }
data_from_lib = 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,
&params);
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;
}
data_from_lib = 1;
} else { } else {
if (read_file(file, &data, &data_len) != 0) { if (read_file(file, &data, &data_len) != 0) {
libptouch_destroy(ctx); libptouch_destroy(ctx);
@@ -234,12 +378,60 @@ int main(int argc, char **argv)
params.margin_mm = 0; 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, &params, 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, &params); e = libptouch_check_raster(ctx, data, data_len, &params);
if (e != LIBPTOUCH_OK) { if (e != LIBPTOUCH_OK) {
fprintf(stderr, "check_raster: %s\n", fprintf(stderr, "check_raster: %s\n",
libptouch_strerror(ctx)); libptouch_strerror(ctx));
libptouch_destroy(ctx); libptouch_destroy(ctx);
if (png) if (data_from_lib)
libptouch_free_raster(data); libptouch_free_raster(data);
else else
free(data); free(data);
@@ -247,44 +439,62 @@ int main(int argc, char **argv)
} }
if (dry_run) { 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.width_dots,
(unsigned)params.height_dots); (unsigned)params.height_dots);
if (usb_opened)
libptouch_close(ctx);
libptouch_destroy(ctx); libptouch_destroy(ctx);
if (png) if (data_from_lib)
libptouch_free_raster(data); libptouch_free_raster(data);
else else
free(data); free(data);
return 0; return 0;
} }
e = libptouch_open_usb(ctx); if (!usb_opened) {
if (e != LIBPTOUCH_OK) { e = usb_pid_arg != 0
fprintf(stderr, "open_usb: %s\n", libptouch_strerror(ctx)); ? libptouch_open_usb_vid_pid(ctx, LIBPTOUCH_USB_VID_BROTHER,
libptouch_destroy(ctx); (uint16_t)usb_pid_arg)
if (png) : libptouch_open_usb(ctx);
libptouch_free_raster(data); if (e != LIBPTOUCH_OK) {
else fprintf(stderr, "open_usb: %s\n", libptouch_strerror(ctx));
free(data); libptouch_destroy(ctx);
return 1; if (data_from_lib)
libptouch_free_raster(data);
else
free(data);
return 1;
}
} }
if (verbose)
verbose_print_pre_print_info(ctx, &params, data_len);
e = libptouch_print_raster(ctx, data, data_len, &params); e = libptouch_print_raster(ctx, data, data_len, &params);
if (e != LIBPTOUCH_OK) { if (e != LIBPTOUCH_OK) {
if (verbose)
verbose_print_post_print_status(ctx, "print failed");
fprintf(stderr, "print_raster: %s\n", fprintf(stderr, "print_raster: %s\n",
libptouch_strerror(ctx)); libptouch_strerror(ctx));
libptouch_close(ctx); libptouch_close(ctx);
libptouch_destroy(ctx); libptouch_destroy(ctx);
if (png) if (data_from_lib)
libptouch_free_raster(data); libptouch_free_raster(data);
else else
free(data); free(data);
return 1; return 1;
} }
if (verbose)
verbose_print_post_print_status(ctx, "print success");
libptouch_close(ctx); libptouch_close(ctx);
libptouch_destroy(ctx); libptouch_destroy(ctx);
if (png) if (data_from_lib)
libptouch_free_raster(data); libptouch_free_raster(data);
else else
free(data); free(data);

100
src/lib/libptouch_core.c Normal file
View File

@@ -0,0 +1,100 @@
/*
* libptouch — context, errors, raster validation
*
* Author: knb
* Email: knb@artif.org
*/
#include "libptouch_internal.h"
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
void ptouch_set_error(libptouch_ctx *ctx, libptouch_err_t code, const char *msg)
{
if (!ctx)
return;
ctx->last_code = code;
if (msg) {
snprintf(ctx->last_msg, sizeof(ctx->last_msg), "%s", msg);
} else {
ctx->last_msg[0] = '\0';
}
}
void ptouch_set_error_usb(libptouch_ctx *ctx, int libusb_err, const char *what)
{
char buf[256];
snprintf(buf, sizeof(buf), "%s: %s", what,
libusb_strerror((enum libusb_error)libusb_err));
ptouch_set_error(ctx, LIBPTOUCH_ERR_USB, buf);
}
libptouch_ctx *libptouch_create(void)
{
libptouch_ctx *ctx = calloc(1, sizeof(*ctx));
if (!ctx)
return NULL;
ctx->last_code = LIBPTOUCH_OK;
ctx->last_msg[0] = '\0';
ctx->usb_open = 0;
ctx->usb_ctx = NULL;
ctx->usb_handle = NULL;
ctx->claimed_interface = -1;
ctx->bulk_out_ep = 0;
ctx->bulk_in_ep = 0;
return ctx;
}
void libptouch_destroy(libptouch_ctx *ctx)
{
if (!ctx)
return;
libptouch_close(ctx);
free(ctx);
}
const char *libptouch_strerror(const libptouch_ctx *ctx)
{
if (!ctx)
return "(null ctx)";
return ctx->last_msg[0] ? ctx->last_msg : "ok";
}
libptouch_err_t libptouch_last_error(const libptouch_ctx *ctx)
{
if (!ctx)
return LIBPTOUCH_ERR_ARG;
return ctx->last_code;
}
libptouch_err_t libptouch_check_raster(libptouch_ctx *ctx,
const uint8_t *data, size_t data_len,
const libptouch_raster_params_t *params)
{
if (!ctx || !params) {
if (ctx)
ptouch_set_error(ctx, LIBPTOUCH_ERR_ARG, "null argument");
return LIBPTOUCH_ERR_ARG;
}
if (!data && data_len > 0) {
ptouch_set_error(ctx, LIBPTOUCH_ERR_ARG, "data is null but len > 0");
return LIBPTOUCH_ERR_ARG;
}
size_t row = (params->width_dots + 7u) / 8u;
size_t expected = (size_t)params->height_dots * row;
if (data_len != expected) {
char buf[160];
snprintf(buf, sizeof(buf),
"data_len mismatch: got %zu, expected %zu (w=%u h=%u)",
data_len, expected, params->width_dots,
params->height_dots);
ptouch_set_error(ctx, LIBPTOUCH_ERR_ARG, buf);
return LIBPTOUCH_ERR_ARG;
}
ptouch_set_error(ctx, LIBPTOUCH_OK, "");
return LIBPTOUCH_OK;
}

View File

@@ -0,0 +1,43 @@
/*
* libptouch — internal declarations (not installed)
*
* Author: knb
* Email: knb@artif.org
*/
#ifndef LIBPTOUCH_INTERNAL_H
#define LIBPTOUCH_INTERNAL_H
#include "libptouch.h"
#include <libusb.h>
#include <stddef.h>
struct libptouch_ctx {
libptouch_err_t last_code;
char last_msg[256];
int usb_open;
struct libusb_context *usb_ctx;
struct libusb_device_handle *usb_handle;
int claimed_interface; /* -1 if none */
uint8_t bulk_out_ep;
uint8_t bulk_in_ep;
uint16_t usb_pid; /* 0 if USB not open; used to pick 128- vs 560-dot raster */
};
/* ctx に最終エラー(コードとメッセージ)を記録する。公開 API のエラー返却前に使う。 */
void ptouch_set_error(libptouch_ctx *ctx, libptouch_err_t code, const char *msg);
/* libusb のエラー番号を人が読めるメッセージにして ptouch_set_error に渡す。 */
void ptouch_set_error_usb(libptouch_ctx *ctx, int libusb_err, const char *what);
/* bulk OUT 転送。成功時 0、失敗時 libusb の負のエラーコード。 */
int ptouch_bulk_out(libusb_device_handle *h, uint8_t ep, const uint8_t *data,
int len, unsigned int timeout_ms);
/* bulk IN で len バイトを揃えるまで読む。成功時 0、失敗時 libusb の負のエラーコード。 */
int ptouch_bulk_in_exact(libusb_device_handle *h, uint8_t ep, uint8_t *buf,
int len, unsigned int timeout_ms);
/* ctx の bulk OUT エンドポイントへ buf を送る。印刷ジョブのチャンク送信用。 */
libptouch_err_t ptouch_bulk_send_job(libptouch_ctx *ctx, const uint8_t *buf,
size_t len, const char *what);
#endif /* LIBPTOUCH_INTERNAL_H */

159
src/lib/libptouch_layout.c Normal file
View File

@@ -0,0 +1,159 @@
/*
* libptouch — layout tables and profile resolution (Brother raster PDFs)
*
* Author: knb
* Email: knb@artif.org
*/
#include "libptouch_layout.h"
/* --- cv_ptp900_jpn_raster_102.pdf: TZe / その他HS 以外で共通レイアウトを使う帯) --- */
static const ptouch_layout_row_t layout_tze_560[] = {
{ 0x04, 248, 48, 264 },
{ 0x06, 240, 64, 256 },
{ 0x09, 219, 106, 235 },
{ 0x0C, 197, 150, 213 },
{ 0x12, 155, 234, 171 },
{ 0x18, 112, 320, 128 },
{ 0x24, 45, 454, 61 },
};
static const ptouch_layout_row_t layout_hs_560[] = {
{ 0x06, 244, 56, 260 },
{ 0x09, 224, 96, 240 },
{ 0x0C, 206, 132, 222 },
{ 0x12, 166, 212, 182 },
{ 0x18, 144, 256, 160 },
};
/* --- cv_ptp750w_710bt_jpn_raster_102.pdf --- */
static const ptouch_layout_row_t layout_tze_128[] = {
{ 0x04, 52, 24, 52 },
{ 0x06, 48, 32, 48 },
{ 0x09, 39, 50, 39 },
{ 0x0C, 29, 70, 29 },
{ 0x12, 8, 112, 8 },
{ 0x18, 0, 128, 0 },
};
static const ptouch_layout_row_t layout_hs_128[] = {
{ 0x06, 50, 28, 50 },
{ 0x09, 40, 48, 40 },
{ 0x0C, 31, 66, 31 },
{ 0x12, 11, 106, 11 },
{ 0x18, 0, 128, 0 },
};
static const uint16_t pids_128pin[] = {
LIBPTOUCH_USB_PID_PTP750W,
LIBPTOUCH_USB_PID_PTP710BT,
0,
};
static const uint8_t models_128pin[] = {
0x68, /* PT-P750W */
0x76, /* PT-P710BT */
0,
};
static const ptouch_printer_profile_t ptouch_layout_profiles[] = {
{
.usb_pids = pids_128pin,
.model_codes = models_128pin,
.is_default = 0,
.head_width_dots = 128u,
.margin_feed_dpi = 180.0,
.margin_feed_max_dots = 900u,
.tze = layout_tze_128,
.tze_count = sizeof(layout_tze_128) / sizeof(layout_tze_128[0]),
.hs = layout_hs_128,
.hs_count = sizeof(layout_hs_128) / sizeof(layout_hs_128[0]),
.doc_ref = "cv_ptp750w_710bt_jpn_raster_102.pdf",
},
{
.usb_pids = NULL,
.model_codes = NULL,
.is_default = 1,
.head_width_dots = 560u,
.margin_feed_dpi = 360.0,
.margin_feed_max_dots = 1800u,
.tze = layout_tze_560,
.tze_count = sizeof(layout_tze_560) / sizeof(layout_tze_560[0]),
.hs = layout_hs_560,
.hs_count = sizeof(layout_hs_560) / sizeof(layout_hs_560[0]),
.doc_ref = "cv_ptp900_jpn_raster_102.pdf (560-dot family)",
},
};
static int pid_in_list(const uint16_t *list, uint16_t pid)
{
if (!list)
return 0;
for (; *list; list++) {
if (*list == pid)
return 1;
}
return 0;
}
static int model_in_list(const uint8_t *list, uint8_t code)
{
if (!list)
return 0;
for (; *list; list++) {
if (*list == code)
return 1;
}
return 0;
}
const ptouch_printer_profile_t *ptouch_layout_resolve_profile(uint16_t usb_pid,
uint8_t status_model_byte)
{
const ptouch_printer_profile_t *fallback = NULL;
for (size_t i = 0;
i < sizeof(ptouch_layout_profiles) / sizeof(ptouch_layout_profiles[0]);
i++) {
const ptouch_printer_profile_t *p = &ptouch_layout_profiles[i];
if (p->is_default) {
fallback = p;
continue;
}
if (pid_in_list(p->usb_pids, usb_pid) ||
model_in_list(p->model_codes, status_model_byte))
return p;
}
return fallback;
}
static libptouch_err_t layout_lookup_table(const ptouch_layout_row_t *table, size_t n,
uint8_t media_wbyte, uint16_t *left,
uint16_t *print_dots, uint16_t *right)
{
for (size_t i = 0; i < n; i++) {
if (table[i].media_wbyte != media_wbyte)
continue;
*left = table[i].left_dots;
*print_dots = table[i].print_dots;
*right = table[i].right_dots;
return LIBPTOUCH_OK;
}
return LIBPTOUCH_ERR_UNSUPPORTED;
}
libptouch_err_t ptouch_layout_from_status(const ptouch_printer_profile_t *prof,
uint8_t media_kind, uint8_t media_wbyte,
uint16_t *left_dots, uint16_t *print_dots,
uint16_t *right_dots)
{
if (!prof || !left_dots || !print_dots || !right_dots)
return LIBPTOUCH_ERR_ARG;
int is_hs = (media_kind == 0x11u || media_kind == 0x17u);
const ptouch_layout_row_t *table = is_hs ? prof->hs : prof->tze;
size_t n = is_hs ? prof->hs_count : prof->tze_count;
return layout_lookup_table(table, n, media_wbyte, left_dots, print_dots,
right_dots);
}

View File

@@ -0,0 +1,50 @@
/*
* libptouch — tape width → printable dots (data tables per printer family)
*
* Author: knb
* Email: knb@artif.org
*/
#ifndef LIBPTOUCH_LAYOUT_H
#define LIBPTOUCH_LAYOUT_H
#include "libptouch.h"
#include <stddef.h>
#include <stdint.h>
/** One row: status byte st[10] (media width code) when it matches this tape width slot */
typedef struct ptouch_layout_row {
uint8_t media_wbyte;
uint16_t left_dots;
uint16_t print_dots;
uint16_t right_dots;
} ptouch_layout_row_t;
/**
* Printer family: USB PID / status model byte select a profile; tables supply layouts.
* New models: add a row to @ref ptouch_layout_profiles or add TZe/HS rows to existing tables.
*/
typedef struct ptouch_printer_profile {
const uint16_t *usb_pids; /* 0-terminated; NULL = do not match on PID */
const uint8_t *model_codes; /* 0-terminated; NULL = do not match on status[4] */
int is_default; /* 1 = fallback when no other profile matches */
unsigned head_width_dots; /* 128 or 560; full raster width for pack/GF payload */
double margin_feed_dpi; /* ESC i d feed dots per inch (PDF) */
unsigned margin_feed_max_dots;
const ptouch_layout_row_t *tze;
size_t tze_count;
const ptouch_layout_row_t *hs;
size_t hs_count;
const char *doc_ref;
} ptouch_printer_profile_t;
const ptouch_printer_profile_t *ptouch_layout_resolve_profile(uint16_t usb_pid,
uint8_t status_model_byte);
libptouch_err_t ptouch_layout_from_status(const ptouch_printer_profile_t *prof,
uint8_t media_kind, uint8_t media_wbyte,
uint16_t *left_dots, uint16_t *print_dots,
uint16_t *right_dots);
#endif /* LIBPTOUCH_LAYOUT_H */

View File

@@ -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 <string.h>
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;
}

164
src/lib/libptouch_png.c Normal file
View File

@@ -0,0 +1,164 @@
/*
* libptouch — PNG → 1bit packed raster (libpng)
*
* Author: knb
* Email: knb@artif.org
*/
#include "libptouch_internal.h"
#include <errno.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <png.h>
void libptouch_free_raster(uint8_t *raster)
{
free(raster);
}
libptouch_err_t libptouch_png_file_to_raster(libptouch_ctx *ctx, const char *path,
const libptouch_png_options_t *options,
uint8_t **out_raster, size_t *out_raster_bytes,
libptouch_raster_params_t *out_params)
{
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;
}
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;
FILE *fp = fopen(path, "rb");
if (!fp) {
char buf[192];
snprintf(buf, sizeof(buf), "open %s: %s", path, strerror(errno));
ptouch_set_error(ctx, LIBPTOUCH_ERR_IO, buf);
return LIBPTOUCH_ERR_IO;
}
unsigned char sig[8];
if (fread(sig, 1, 8, fp) != 8) {
fclose(fp);
ptouch_set_error(ctx, LIBPTOUCH_ERR_IMAGE, "short read (not a PNG?)");
return LIBPTOUCH_ERR_IMAGE;
}
if (png_sig_cmp(sig, 0, 8) != 0) {
fclose(fp);
ptouch_set_error(ctx, LIBPTOUCH_ERR_IMAGE, "not a PNG file");
return LIBPTOUCH_ERR_IMAGE;
}
png_structp png_ptr =
png_create_read_struct(PNG_LIBPNG_VER_STRING, NULL, NULL, NULL);
if (!png_ptr) {
fclose(fp);
ptouch_set_error(ctx, LIBPTOUCH_ERR_NOMEM, "png_create_read_struct failed");
return LIBPTOUCH_ERR_NOMEM;
}
png_infop info_ptr = png_create_info_struct(png_ptr);
if (!info_ptr) {
png_destroy_read_struct(&png_ptr, NULL, NULL);
fclose(fp);
ptouch_set_error(ctx, LIBPTOUCH_ERR_NOMEM, "png_create_info_struct failed");
return LIBPTOUCH_ERR_NOMEM;
}
if (setjmp(png_jmpbuf(png_ptr))) {
png_destroy_read_struct(&png_ptr, &info_ptr, NULL);
fclose(fp);
ptouch_set_error(ctx, LIBPTOUCH_ERR_IMAGE, "PNG decode error");
return LIBPTOUCH_ERR_IMAGE;
}
png_init_io(png_ptr, fp);
png_set_sig_bytes(png_ptr, 8);
const int transforms = (int)(PNG_TRANSFORM_EXPAND | PNG_TRANSFORM_STRIP_16 |
PNG_TRANSFORM_GRAY_TO_RGB);
png_read_png(png_ptr, info_ptr, transforms, NULL);
png_uint_32 width = png_get_image_width(png_ptr, info_ptr);
png_uint_32 height = png_get_image_height(png_ptr, info_ptr);
int channels = (int)png_get_channels(png_ptr, info_ptr);
png_bytepp rows = png_get_rows(png_ptr, info_ptr);
if (width == 0 || height == 0 || !rows) {
png_destroy_read_struct(&png_ptr, &info_ptr, NULL);
fclose(fp);
ptouch_set_error(ctx, LIBPTOUCH_ERR_IMAGE, "invalid PNG dimensions");
return LIBPTOUCH_ERR_IMAGE;
}
if (width > 100000u || height > 100000u) {
png_destroy_read_struct(&png_ptr, &info_ptr, NULL);
fclose(fp);
ptouch_set_error(ctx, LIBPTOUCH_ERR_IMAGE, "PNG dimensions too large");
return LIBPTOUCH_ERR_IMAGE;
}
if (channels != 3 && channels != 4) {
png_destroy_read_struct(&png_ptr, &info_ptr, NULL);
fclose(fp);
ptouch_set_error(ctx, LIBPTOUCH_ERR_IMAGE, "unsupported PNG channel count");
return LIBPTOUCH_ERR_IMAGE;
}
size_t row_bytes = ((size_t)width + 7u) / 8u;
if (height > SIZE_MAX / row_bytes) {
png_destroy_read_struct(&png_ptr, &info_ptr, NULL);
fclose(fp);
ptouch_set_error(ctx, LIBPTOUCH_ERR_IMAGE, "raster size overflow");
return LIBPTOUCH_ERR_IMAGE;
}
size_t total = row_bytes * (size_t)height;
uint8_t *out = (uint8_t *)calloc(1, total);
if (!out) {
png_destroy_read_struct(&png_ptr, &info_ptr, NULL);
fclose(fp);
ptouch_set_error(ctx, LIBPTOUCH_ERR_NOMEM, "calloc raster failed");
return LIBPTOUCH_ERR_NOMEM;
}
for (png_uint_32 y = 0; y < height; y++) {
const png_byte *row = rows[y];
uint8_t *dst_row = out + (size_t)y * row_bytes;
for (png_uint_32 x = 0; x < width; x++) {
size_t o = (size_t)x * (size_t)channels;
unsigned r = row[o + 0];
unsigned g = row[o + 1];
unsigned b = row[o + 2];
unsigned a = channels == 4 ? row[o + 3] : 255u;
if (a < 128u) {
continue;
}
unsigned yv = (77u * r + 150u * g + 29u * b) >> 8;
int black = (yv < (unsigned)thr) ? 1 : 0;
if (!black)
continue;
size_t bit = (size_t)x % 8u;
size_t byte = (size_t)x / 8u;
dst_row[byte] |= (uint8_t)(1u << (7u - bit));
}
}
png_destroy_read_struct(&png_ptr, &info_ptr, NULL);
fclose(fp);
out_params->width_dots = (uint32_t)width;
out_params->height_dots = (uint32_t)height;
out_params->margin_mm = 0;
*out_raster = out;
*out_raster_bytes = total;
ptouch_set_error(ctx, LIBPTOUCH_OK, "");
return LIBPTOUCH_OK;
}

251
src/lib/libptouch_print.c Normal file
View File

@@ -0,0 +1,251 @@
/*
* libptouch — raster layout, transpose, P-touch print job (ESC/P bulk)
*
* Author: knb
* Email: knb@artif.org
*/
#include "libptouch_internal.h"
#include "libptouch_layout.h"
#include "libptouch_protocol.h"
#include <libusb.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
static void pack_line(uint8_t *line, size_t line_bytes, unsigned head_dots,
const uint8_t *row, uint32_t width_dots, uint16_t left_dots,
uint16_t print_dots)
{
memset(line, 0, line_bytes);
if (width_dots > (uint32_t)print_dots)
return;
uint32_t start = (uint32_t)left_dots +
((uint32_t)print_dots - width_dots) / 2u;
for (uint32_t x = 0; x < width_dots; x++) {
uint32_t ubyte = x / 8u;
uint32_t ubit = 7u - (x % 8u);
if (((row[ubyte] >> ubit) & 1u) == 0)
continue;
uint32_t dot = start + x;
if (dot >= (uint32_t)head_dots)
break;
uint32_t b = dot / 8u;
uint32_t bi = 7u - (dot % 8u);
line[b] |= (uint8_t)(1u << bi);
}
}
static uint8_t *transpose_raster_alloc(const uint8_t *in, uint32_t W, uint32_t H,
uint32_t *outW, uint32_t *outH)
{
*outW = H;
*outH = W;
size_t old_rb = (W + 7u) / 8u;
size_t new_rb = (H + 7u) / 8u;
uint8_t *out = (uint8_t *)calloc(new_rb * W, 1);
if (!out)
return NULL;
for (uint32_t y = 0; y < H; y++) {
for (uint32_t x = 0; x < W; x++) {
size_t ob = (size_t)y * old_rb + x / 8u;
int bit = (int)((in[ob] >> (7u - x % 8u)) & 1u);
if (!bit)
continue;
uint32_t nx = y;
uint32_t ny = x;
size_t nb = (size_t)ny * new_rb + nx / 8u;
out[nb] |= (uint8_t)(1u << (7u - nx % 8u));
}
}
return out;
}
libptouch_err_t libptouch_print_raster(libptouch_ctx *ctx,
const uint8_t *data, size_t data_len,
const libptouch_raster_params_t *params)
{
libptouch_err_t v =
libptouch_check_raster(ctx, data, data_len, params);
if (v != LIBPTOUCH_OK)
return v;
if (!ctx->usb_open || !ctx->bulk_out_ep) {
ptouch_set_error(ctx, LIBPTOUCH_ERR_IO, "not connected");
return LIBPTOUCH_ERR_IO;
}
uint8_t st[LIBPTOUCH_STATUS_LENGTH];
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;
}
uint8_t media_kind = st[11];
uint8_t media_w = st[10];
uint16_t left_dots, print_dots, right_dots;
v = ptouch_layout_from_status(prof, media_kind, media_w, &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 "
"(check status media bytes)");
return LIBPTOUCH_ERR_UNSUPPORTED;
}
(void)right_dots;
unsigned head_dots = prof->head_width_dots;
size_t line_payload = ptouch_line_payload_bytes(head_dots);
size_t gf_packet = 3u + line_payload;
uint32_t wd = params->width_dots;
uint32_t ht = params->height_dots;
uint8_t *transposed = transpose_raster_alloc(data, wd, ht, &wd, &ht);
if (!transposed) {
ptouch_set_error(ctx, LIBPTOUCH_ERR_NOMEM,
"transpose raster: out of memory");
return LIBPTOUCH_ERR_NOMEM;
}
const uint8_t *src = transposed;
if (wd > (uint32_t)print_dots) {
char buf[160];
snprintf(buf, sizeof(buf),
"image width %u dots > printable %u for loaded tape",
(unsigned)wd, (unsigned)print_dots);
ptouch_set_error(ctx, LIBPTOUCH_ERR_ARG, buf);
free(transposed);
return LIBPTOUCH_ERR_ARG;
}
double margin_dpi = prof->margin_feed_dpi;
unsigned margin_max = prof->margin_feed_max_dots;
unsigned margin_dots = 14u;
if (params->margin_mm > 0) {
margin_dots = (unsigned)((double)params->margin_mm * margin_dpi /
25.4 +
0.5);
if (margin_dots < 14u)
margin_dots = 14u;
if (margin_dots > margin_max)
margin_dots = margin_max;
}
uint32_t lines = ht;
uint8_t head[256];
size_t pos = 0;
memset(head + pos, 0, 200);
pos += 200u;
static const uint8_t esc_at[] = { 0x1B, 0x40 };
memcpy(head + pos, esc_at, sizeof(esc_at));
pos += sizeof(esc_at);
static const uint8_t raster_mode[] = { 0x1B, 0x69, 0x61, 0x01 };
memcpy(head + pos, raster_mode, sizeof(raster_mode));
pos += sizeof(raster_mode);
uint8_t esc_iz[13];
ptouch_fill_esc_iz(esc_iz, media_kind, media_w, lines);
memcpy(head + pos, esc_iz, sizeof(esc_iz));
pos += sizeof(esc_iz);
static const uint8_t esc_im[] = { 0x1B, 0x69, 0x4D, 0x40 };
memcpy(head + pos, esc_im, sizeof(esc_im));
pos += sizeof(esc_im);
/*
* ESC i A ("cut each n labels") is not supported on PT-P710BT class
* devices. Sending it can trigger a communication error (red LED blink).
* Keep it only for 560-dot family.
*/
if (prof->head_width_dots > 128u) {
static const uint8_t esc_ia[] = { 0x1B, 0x69, 0x41, 0x01 };
memcpy(head + pos, esc_ia, sizeof(esc_ia));
pos += sizeof(esc_ia);
}
uint8_t esc_ik[] = { 0x1B, 0x69, 0x4B, ptouch_esc_ik_value(head_dots) };
memcpy(head + pos, esc_ik, sizeof(esc_ik));
pos += sizeof(esc_ik);
uint8_t esc_id[] = {
0x1B, 0x69, 0x64,
(uint8_t)(margin_dots & 0xFFu),
(uint8_t)((margin_dots >> 8) & 0xFFu)
};
memcpy(head + pos, esc_id, sizeof(esc_id));
pos += sizeof(esc_id);
static const uint8_t mode_m[] = { 0x4D, 0x00 };
memcpy(head + pos, mode_m, sizeof(mode_m));
pos += sizeof(mode_m);
v = ptouch_bulk_send_job(ctx, head, pos, "print preamble");
if (v != LIBPTOUCH_OK) {
free(transposed);
return v;
}
size_t row_b = ((size_t)wd + 7u) / 8u;
uint8_t *gbuf = (uint8_t *)malloc(gf_packet);
if (!gbuf) {
ptouch_set_error(ctx, LIBPTOUCH_ERR_NOMEM,
"raster line buffer");
free(transposed);
return LIBPTOUCH_ERR_NOMEM;
}
for (uint32_t y = 0; y < lines; y++) {
const uint8_t *row = src + (size_t)y * row_b;
pack_line(gbuf + 3, line_payload, head_dots, row, wd, left_dots,
print_dots);
ptouch_fill_gf_header(gbuf, line_payload);
v = ptouch_bulk_send_job(ctx, gbuf, gf_packet, "raster line");
if (v != LIBPTOUCH_OK) {
free(gbuf);
free(transposed);
return v;
}
}
free(gbuf);
/*
* Some 560-dot family devices are sensitive to end-of-page sequencing.
* Send FF (0x0C) then Control-Z (0x1A) on that family to explicitly
* terminate page and final feed/cut.
*/
if (head_dots > 128u) {
static const uint8_t page_end[] = { 0x0C };
v = ptouch_bulk_send_job(ctx, page_end, sizeof(page_end),
"print page end");
if (v != LIBPTOUCH_OK) {
free(transposed);
return v;
}
}
static const uint8_t print_end[] = { 0x1A };
v = ptouch_bulk_send_job(ctx, print_end, sizeof(print_end), "print end");
if (v != LIBPTOUCH_OK) {
free(transposed);
return v;
}
uint8_t sink[64];
int tr = 0;
(void)libusb_bulk_transfer(ctx->usb_handle, ctx->bulk_in_ep, sink,
(int)sizeof(sink), &tr, 3000);
(void)tr;
free(transposed);
ptouch_set_error(ctx, LIBPTOUCH_OK, "");
return LIBPTOUCH_OK;
}

View File

@@ -0,0 +1,39 @@
#include "libptouch_protocol.h"
size_t ptouch_line_payload_bytes(unsigned head_dots)
{
return (size_t)((head_dots + 7u) / 8u);
}
void ptouch_fill_gf_header(uint8_t out[3], size_t line_payload_bytes)
{
out[0] = 0x47u;
out[1] = (uint8_t)(line_payload_bytes & 0xFFu);
out[2] = (uint8_t)((line_payload_bytes >> 8) & 0xFFu);
}
void ptouch_fill_esc_iz(uint8_t out[13], uint8_t media_kind, uint8_t media_width,
uint32_t raster_lines)
{
out[0] = 0x1Bu;
out[1] = 0x69u;
out[2] = 0x7Au;
/* PI flags: enable media/width/length with quality bit for broad compatibility. */
out[3] = 0x8Eu;
/* Use status media-kind byte directly (e.g., 0x01 laminated, 0x03 non-laminate). */
out[4] = media_kind;
out[5] = media_width;
out[6] = 0x00u;
out[7] = (uint8_t)(raster_lines & 0xFFu);
out[8] = (uint8_t)((raster_lines >> 8) & 0xFFu);
out[9] = (uint8_t)((raster_lines >> 16) & 0xFFu);
out[10] = (uint8_t)((raster_lines >> 24) & 0xFFu);
out[11] = 0x00u; /* first page */
out[12] = 0x00u; /* fixed */
}
uint8_t ptouch_esc_ik_value(unsigned head_dots)
{
/* Preserve legacy 560-dot behaviour (0x0C), 128-dot uses 0x08. */
return head_dots > 128u ? 0x0Cu : 0x08u;
}

View File

@@ -0,0 +1,13 @@
#ifndef LIBPTOUCH_PROTOCOL_H
#define LIBPTOUCH_PROTOCOL_H
#include <stddef.h>
#include <stdint.h>
size_t ptouch_line_payload_bytes(unsigned head_dots);
void ptouch_fill_gf_header(uint8_t out[3], size_t line_payload_bytes);
void ptouch_fill_esc_iz(uint8_t out[13], uint8_t media_kind, uint8_t media_width,
uint32_t raster_lines);
uint8_t ptouch_esc_ik_value(unsigned head_dots);
#endif /* LIBPTOUCH_PROTOCOL_H */

456
src/lib/libptouch_status.c Normal file
View File

@@ -0,0 +1,456 @@
/*
* libptouch — printer status (ESC i S) and human-readable dump
*
* Author: knb
* Email: knb@artif.org
*/
#include "libptouch_internal.h"
#include <stdio.h>
#include <string.h>
#include <unistd.h>
libptouch_err_t libptouch_get_status(libptouch_ctx *ctx, uint8_t *status)
{
if (!ctx || !status) {
if (ctx)
ptouch_set_error(ctx, LIBPTOUCH_ERR_ARG, "null argument");
return LIBPTOUCH_ERR_ARG;
}
if (!ctx->usb_handle || !ctx->bulk_out_ep || !ctx->bulk_in_ep) {
ptouch_set_error(ctx, LIBPTOUCH_ERR_IO,
"USB not open or bulk endpoints missing");
return LIBPTOUCH_ERR_IO;
}
libusb_device_handle *h = ctx->usb_handle;
static const uint8_t init[] = { 0x1B, 0x40 };
static const uint8_t req[] = { 0x1B, 0x69, 0x53 };
int r = LIBUSB_ERROR_OTHER;
for (int attempt = 0; attempt < 5; attempt++) {
/* First status read after reconnect can fail on some P-touch units. */
(void)libusb_clear_halt(h, ctx->bulk_in_ep);
(void)libusb_clear_halt(h, ctx->bulk_out_ep);
if (attempt > 0) {
usleep(120000); /* 120ms backoff */
}
/*
* Avoid unconditional ESC @ here:
* some models can be in phase-change right after print end, and
* re-initializing there may disturb cutter/feed completion.
* Try plain ESC i S first, use ESC @ only on retry paths.
*/
if (attempt > 0) {
r = ptouch_bulk_out(h, ctx->bulk_out_ep, init, 2, 5000u);
if (r != 0) {
if (r == LIBUSB_ERROR_IO || r == LIBUSB_ERROR_PIPE ||
r == LIBUSB_ERROR_TIMEOUT)
continue;
ptouch_set_error_usb(ctx, r, "bulk OUT ESC @");
return LIBPTOUCH_ERR_USB;
}
}
r = ptouch_bulk_out(h, ctx->bulk_out_ep, req, 3, 5000u);
if (r != 0) {
if (r == LIBUSB_ERROR_IO || r == LIBUSB_ERROR_PIPE ||
r == LIBUSB_ERROR_TIMEOUT)
continue;
ptouch_set_error_usb(ctx, r, "bulk OUT ESC i S");
return LIBPTOUCH_ERR_USB;
}
r = ptouch_bulk_in_exact(h, ctx->bulk_in_ep, status,
(int)LIBPTOUCH_STATUS_LENGTH, 5000u);
if (r == 0)
break;
if (r != LIBUSB_ERROR_IO && r != LIBUSB_ERROR_PIPE &&
r != LIBUSB_ERROR_TIMEOUT)
break;
}
if (r != 0) {
ptouch_set_error_usb(ctx, r, "bulk IN status");
return LIBPTOUCH_ERR_USB;
}
ptouch_set_error(ctx, LIBPTOUCH_OK, "");
return LIBPTOUCH_OK;
}
static void fprint_model(FILE *fp, uint8_t code)
{
const char *name = "unknown";
switch (code) {
case 0x6F:
name = "PT-P900W";
break;
case 0x70:
name = "PT-P950NW";
break;
case 0x71:
name = "PT-P900";
break;
case 0x78:
name = "PT-P910BT";
break;
case 0x68:
name = "PT-P750W";
break;
case 0x76:
name = "PT-P710BT";
break;
default:
break;
}
fprintf(fp, "機種コード: 0x%02X ('%c') — %s\n", (unsigned)code,
(code >= 32 && code < 127) ? (char)code : '?', name);
}
static void fprint_media_width(FILE *fp, uint8_t w, uint8_t len_byte)
{
const char *desc = NULL;
switch (w) {
case 0x00:
desc = "テープなし / 未装着";
break;
case 0x04:
desc = "3.5 mm";
break;
case 0x06:
desc = "6 mm";
break;
case 0x09:
desc = "9 mm";
break;
case 0x0C:
desc = "12 mm";
break;
case 0x12:
desc = "18 mm";
break;
case 0x18:
desc = "24 mm";
break;
case 0x24:
desc = "36 mm";
break;
case 0x15:
desc = "FLe 21 mm 幅(長さはメディア長バイト参照)";
break;
default:
break;
}
if (desc)
fprintf(fp, "メディア幅: 0x%02X — %s\n", (unsigned)w, desc);
else
fprintf(fp, "メディア幅: 0x%02X\n", (unsigned)w);
if (w == 0x15 && len_byte != 0)
fprintf(fp, "メディア長: 0x%02X (%u mm 相当の表記参照)\n",
(unsigned)len_byte, (unsigned)len_byte);
}
static void fprint_media_kind(FILE *fp, uint8_t k)
{
const char *desc = NULL;
switch (k) {
case 0x00:
desc = "テープなし";
break;
case 0x01:
desc = "ラミネートテープ";
break;
case 0x03:
desc = "ノンラミネートテープ";
break;
case 0x04:
desc = "ファブリックテープ";
break;
case 0x11:
desc = "ヒートシュリンクチューブ (HS 2:1)";
break;
case 0x13:
desc = "FLe テープ";
break;
case 0x14:
desc = "フレキシブルIDテープ";
break;
case 0x15:
desc = "サテンテープ";
break;
case 0x17:
desc = "ヒートシュリンクチューブ (HS 3:1)";
break;
case 0xFF:
desc = "非対応テープ";
break;
default:
break;
}
if (desc)
fprintf(fp, "テープ種類: 0x%02X — %s\n", (unsigned)k, desc);
else
fprintf(fp, "テープ種類: 0x%02X\n", (unsigned)k);
}
static void fprint_battery(FILE *fp, uint8_t b)
{
const char *desc = NULL;
switch (b) {
case 0x00:
desc = "フル";
break;
case 0x01:
desc = "ハーフ";
break;
case 0x02:
desc = "ロー";
break;
case 0x03:
desc = "要充電";
break;
case 0x04:
desc = "AC アダプター使用中";
break;
case 0xFF:
desc = "不明";
break;
default:
break;
}
if (desc)
fprintf(fp, "電池残量: 0x%02X — %s\n", (unsigned)b, desc);
else
fprintf(fp, "電池残量: 0x%02X (PT-P910BT 等は別表参照)\n",
(unsigned)b);
}
static void fprint_tape_color(FILE *fp, uint8_t c)
{
const char *desc = NULL;
switch (c) {
case 0x01:
desc = "白 (White)";
break;
case 0x02:
desc = "その他 (Other)";
break;
case 0x03:
desc = "透明 (Clear)";
break;
case 0x04:
desc = "赤 (Red)";
break;
case 0x05:
desc = "青 (Blue)";
break;
case 0x06:
desc = "黄 (Yellow)";
break;
case 0x07:
desc = "緑 (Green)";
break;
case 0x08:
desc = "黒 (Black)";
break;
case 0x09:
desc = "透明(文字白)";
break;
case 0x20:
desc = "白(マット) (Matte White)";
break;
case 0x21:
desc = "透明(マット) (Matte Clear)";
break;
case 0x22:
desc = "銀(マット) (Matte Silver)";
break;
case 0x23:
desc = "金(サテン) (Satin Gold)";
break;
case 0x24:
desc = "銀(サテン) (Satin Silver)";
break;
case 0x30:
desc = "D";
break;
case 0x31:
desc = "D";
break;
case 0x40:
desc = "オレンジ(蛍光)";
break;
case 0x41:
desc = "黄(蛍光)";
break;
case 0x50:
desc = "ピンクS";
break;
case 0x51:
desc = "グレーS";
break;
case 0x52:
desc = "グリーンS";
break;
case 0x60:
desc = "イエローF";
break;
case 0x61:
desc = "ピンクF";
break;
case 0x62:
desc = "ブルーF";
break;
case 0x70:
desc = "白(チューブ)";
break;
case 0x90:
desc = "白(フレキ)";
break;
case 0x91:
desc = "黄(フレキ)";
break;
case 0xF0:
desc = "クリーニング";
break;
case 0xF1:
desc = "ステンシル";
break;
case 0xFF:
desc = "非対応";
break;
default:
break;
}
if (desc)
fprintf(fp, "テープ色: 0x%02X — %s\n", (unsigned)c, desc);
else
fprintf(fp, "テープ色: 0x%02X\n", (unsigned)c);
}
static void fprint_status_kind(FILE *fp, uint8_t s)
{
const char *desc = NULL;
switch (s) {
case 0x00:
desc = "印刷終了";
break;
case 0x01:
desc = "エラー発生";
break;
case 0x02:
desc = "IF モード終了";
break;
case 0x03:
desc = "パワーオフ(未使用扱い)";
break;
case 0x04:
desc = "通知";
break;
case 0x05:
desc = "フェーズ変更";
break;
default:
break;
}
if (desc)
fprintf(fp, "ステータス種類: 0x%02X — %s\n", (unsigned)s, desc);
else
fprintf(fp, "ステータス種類: 0x%02X\n", (unsigned)s);
}
void libptouch_status_fprint(FILE *fp, const uint8_t *status)
{
if (!fp || !status)
return;
fprintf(fp, "=== P-touch ステータス (32 バイト) ===\n");
if (status[0] != 0x80u || status[1] != 0x20u)
fprintf(fp,
"※ 先頭マーク異常: [0]=0x%02X [1]=0x%02X (通常 80 20)\n",
(unsigned)status[0], (unsigned)status[1]);
fprintf(fp, "ヘッダ: 0x%02X 0x%02X\n", (unsigned)status[0],
(unsigned)status[1]);
fprintf(fp, "Brother コード: %c (0x%02X)\n",
(status[2] >= 32 && status[2] < 127) ? (char)status[2] : '?',
(unsigned)status[2]);
fprint_model(fp, status[4]);
fprintf(fp, "国別コード: %c\n",
(status[5] >= 32 && status[5] < 127) ? (char)status[5] : '?');
fprint_battery(fp, status[6]);
if (status[7] != 0) {
fprintf(fp, "拡張エラー: 0x%02X", (unsigned)status[7]);
switch (status[7]) {
case 0x10:
fprintf(fp, " — FLE のテープエンド");
break;
case 0x1D:
fprintf(fp, " — 高解像度/ドラフト印刷エラー");
break;
case 0x1E:
fprintf(fp, " — アダプター抜き挿しエラー");
break;
case 0x21:
fprintf(fp, " — 非対応メディアエラー");
break;
default:
break;
}
fprintf(fp, "\n");
} else {
fprintf(fp, "拡張エラー: なし (0x00)\n");
}
fprintf(fp, "エラー情報1: 0x%02X\n", (unsigned)status[8]);
if (status[8] & 0x01)
fprintf(fp, " - メディア無し\n");
if (status[8] & 0x02)
fprintf(fp, " - メディア終了\n");
if (status[8] & 0x04)
fprintf(fp, " - カッタージャム\n");
if (status[8] & 0x08)
fprintf(fp, " - バッテリー弱\n");
if (status[8] & 0x40)
fprintf(fp, " - 高圧アダプター\n");
fprintf(fp, "エラー情報2: 0x%02X\n", (unsigned)status[9]);
if (status[9] & 0x01)
fprintf(fp, " - メディア交換(メディア違い)\n");
if (status[9] & 0x04)
fprintf(fp, " - 通信エラー\n");
if (status[9] & 0x08)
fprintf(fp, " - 通信バッファーフル\n");
if (status[9] & 0x10)
fprintf(fp, " - カバーオープン\n");
if (status[9] & 0x20)
fprintf(fp, " - 高温エラー\n");
if (status[9] & 0x40)
fprintf(fp, " - 先端検出エラー\n");
if (status[9] & 0x80)
fprintf(fp, " - システムエラー\n");
fprint_media_width(fp, status[10], status[17]);
fprint_media_kind(fp, status[11]);
fprintf(fp, "色数: 0x%02X フォント/日本語フォント: 0x%02X / 0x%02X\n",
(unsigned)status[12], (unsigned)status[13],
(unsigned)status[14]);
fprintf(fp, "モード: 0x%02X 濃度: 0x%02X\n", (unsigned)status[15],
(unsigned)status[16]);
fprint_status_kind(fp, status[18]);
fprintf(fp, "フェーズ種類: 0x%02X フェーズ番号: %02X %02X\n",
(unsigned)status[19], (unsigned)status[20],
(unsigned)status[21]);
fprintf(fp, "通知番号: 0x%02X\n", (unsigned)status[22]);
fprintf(fp, "拡張部バイト数: 0x%02X\n", (unsigned)status[23]);
fprint_tape_color(fp, status[24]);
fprintf(fp, "文字色: 0x%02X\n", (unsigned)status[25]);
fprintf(fp, "生データ: ");
for (unsigned i = 0; i < LIBPTOUCH_STATUS_LENGTH; i++)
fprintf(fp, "%02X%s", (unsigned)status[i],
i + 1 == LIBPTOUCH_STATUS_LENGTH ? "\n" : " ");
}

211
src/lib/libptouch_svg.c Normal file
View File

@@ -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 <cairo.h>
#include <librsvg/rsvg.h>
#endif
#include <math.h>
#include <stdint.h>
#include <stdlib.h>
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
}

84
src/lib/libptouch_trim.c Normal file
View File

@@ -0,0 +1,84 @@
/*
* libptouch — raster right blank trim
*
* Author: knb
* Email: knb@artif.org
*/
#include "libptouch_internal.h"
#include <stdlib.h>
#include <string.h>
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;
}

198
src/lib/libptouch_usb.c Normal file
View File

@@ -0,0 +1,198 @@
/*
* libptouch — USB (libusb): open/close, bulk transfers
*
* Author: knb
* Email: knb@artif.org
*/
#include "libptouch_internal.h"
#include <stdio.h>
#include <string.h>
/**
* バルク IN/OUT を両方持つインタフェースを探して claim する。
* USB Printer クラス (bInterfaceClass==7) を優先する。
*/
static int claim_interface_with_bulk(libusb_device_handle *h, int *out_iface,
uint8_t *out_ep, uint8_t *in_ep)
{
libusb_device *dev = libusb_get_device(h);
struct libusb_config_descriptor *cfg = NULL;
int r = libusb_get_active_config_descriptor(dev, &cfg);
if (r != 0 || !cfg)
return -1;
for (int pass = 0; pass < 2; pass++) {
for (int i = 0; i < cfg->bNumInterfaces; i++) {
const struct libusb_interface_descriptor *alt =
&cfg->interface[i].altsetting[0];
if (pass == 0 &&
alt->bInterfaceClass != LIBUSB_CLASS_PRINTER)
continue;
uint8_t ep_out = 0, ep_in = 0;
for (uint8_t k = 0; k < alt->bNumEndpoints; k++) {
const struct libusb_endpoint_descriptor *ep =
&alt->endpoint[k];
if ((ep->bmAttributes &
LIBUSB_TRANSFER_TYPE_MASK) ==
LIBUSB_TRANSFER_TYPE_BULK) {
if (ep->bEndpointAddress &
LIBUSB_ENDPOINT_IN)
ep_in = ep->bEndpointAddress;
else
ep_out = ep->bEndpointAddress;
}
}
if (!ep_out || !ep_in)
continue;
r = libusb_claim_interface(h, i);
if (r == 0) {
*out_iface = i;
*out_ep = ep_out;
*in_ep = ep_in;
libusb_free_config_descriptor(cfg);
return 0;
}
}
}
libusb_free_config_descriptor(cfg);
return -1;
}
libptouch_err_t libptouch_open_usb_vid_pid(libptouch_ctx *ctx, uint16_t vid,
uint16_t pid)
{
if (!ctx) {
return LIBPTOUCH_ERR_ARG;
}
if (ctx->usb_handle) {
ptouch_set_error(ctx, LIBPTOUCH_ERR_ARG, "USB already open");
return LIBPTOUCH_ERR_ARG;
}
int r = libusb_init(&ctx->usb_ctx);
if (r != LIBUSB_SUCCESS) {
ptouch_set_error_usb(ctx, r, "libusb_init");
ctx->usb_ctx = NULL;
return LIBPTOUCH_ERR_USB;
}
libusb_device_handle *h =
libusb_open_device_with_vid_pid(ctx->usb_ctx, vid, pid);
if (!h) {
libusb_exit(ctx->usb_ctx);
ctx->usb_ctx = NULL;
char buf[160];
snprintf(buf, sizeof(buf),
"no USB device 0x%04x:0x%04x (check cable, power, udev)",
(unsigned)vid, (unsigned)pid);
ptouch_set_error(ctx, LIBPTOUCH_ERR_NOT_FOUND, buf);
return LIBPTOUCH_ERR_NOT_FOUND;
}
ctx->usb_handle = h;
#if defined(LIBUSB_API_VERSION) && (LIBUSB_API_VERSION >= 0x01000106)
(void)libusb_set_auto_detach_kernel_driver(h, 1);
#endif
if (claim_interface_with_bulk(h, &ctx->claimed_interface,
&ctx->bulk_out_ep, &ctx->bulk_in_ep) != 0) {
ptouch_set_error(ctx, LIBPTOUCH_ERR_USB,
"no bulk IN/OUT interface (permissions or driver?)");
libusb_close(h);
ctx->usb_handle = NULL;
libusb_exit(ctx->usb_ctx);
ctx->usb_ctx = NULL;
return LIBPTOUCH_ERR_USB;
}
(void)libusb_clear_halt(h, ctx->bulk_out_ep);
(void)libusb_clear_halt(h, ctx->bulk_in_ep);
ctx->usb_pid = pid;
ctx->usb_open = 1;
ptouch_set_error(ctx, LIBPTOUCH_OK, "");
return LIBPTOUCH_OK;
}
libptouch_err_t libptouch_open_usb(libptouch_ctx *ctx)
{
return libptouch_open_usb_vid_pid(ctx, LIBPTOUCH_USB_VID_BROTHER,
LIBPTOUCH_USB_PID_PTP900W);
}
void libptouch_close(libptouch_ctx *ctx)
{
if (!ctx)
return;
if (ctx->usb_handle) {
if (ctx->claimed_interface >= 0) {
(void)libusb_release_interface(ctx->usb_handle,
ctx->claimed_interface);
ctx->claimed_interface = -1;
}
libusb_close(ctx->usb_handle);
ctx->usb_handle = NULL;
}
if (ctx->usb_ctx) {
libusb_exit(ctx->usb_ctx);
ctx->usb_ctx = NULL;
}
ctx->bulk_out_ep = 0;
ctx->bulk_in_ep = 0;
ctx->usb_pid = 0;
ctx->usb_open = 0;
}
int ptouch_bulk_out(libusb_device_handle *h, uint8_t ep, const uint8_t *data,
int len, unsigned int timeout_ms)
{
int tr = 0;
int r = libusb_bulk_transfer(h, ep, (unsigned char *)data, len, &tr,
(int)timeout_ms);
if (r != 0)
return r;
if (tr != len)
return LIBUSB_ERROR_IO;
return 0;
}
int ptouch_bulk_in_exact(libusb_device_handle *h, uint8_t ep, uint8_t *buf,
int len, unsigned int timeout_ms)
{
int got = 0;
while (got < len) {
int tr = 0;
int r = libusb_bulk_transfer(h, ep, buf + got, len - got, &tr,
(int)timeout_ms);
if (r != 0)
return r;
if (tr == 0)
return LIBUSB_ERROR_IO;
got += tr;
}
return 0;
}
libptouch_err_t ptouch_bulk_send_job(libptouch_ctx *ctx, const uint8_t *buf,
size_t len, const char *what)
{
int tr = 0;
int r = libusb_bulk_transfer(ctx->usb_handle, ctx->bulk_out_ep,
(unsigned char *)buf, (int)len, &tr,
120000);
if (r != 0) {
ptouch_set_error_usb(ctx, r, what);
return LIBPTOUCH_ERR_USB;
}
if ((size_t)tr != len) {
char msg[96];
snprintf(msg, sizeof(msg), "%s: short write %d/%zu", what, tr,
len);
ptouch_set_error(ctx, LIBPTOUCH_ERR_USB, msg);
return LIBPTOUCH_ERR_USB;
}
return LIBPTOUCH_OK;
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,49 @@
#include "libptouch_protocol.h"
#include <stdio.h>
#include <stdlib.h>
static int expect_int(const char *name, int got, int want)
{
if (got == want)
return 0;
fprintf(stderr, "%s mismatch: got=%d want=%d\n", name, got, want);
return 1;
}
int main(void)
{
int fail = 0;
fail |= expect_int("line_payload_128", (int)ptouch_line_payload_bytes(128u), 16);
fail |= expect_int("line_payload_560", (int)ptouch_line_payload_bytes(560u), 70);
uint8_t gf[3];
ptouch_fill_gf_header(gf, 16u);
fail |= expect_int("gf16_cmd", gf[0], 0x47);
fail |= expect_int("gf16_n1", gf[1], 0x10);
fail |= expect_int("gf16_n2", gf[2], 0x00);
ptouch_fill_gf_header(gf, 70u);
fail |= expect_int("gf70_cmd", gf[0], 0x47);
fail |= expect_int("gf70_n1", gf[1], 0x46);
fail |= expect_int("gf70_n2", gf[2], 0x00);
uint8_t iz[13];
ptouch_fill_esc_iz(iz, 0x01u, 0x0Cu, 70u);
fail |= expect_int("iz_cmd_0", iz[0], 0x1B);
fail |= expect_int("iz_cmd_1", iz[1], 0x69);
fail |= expect_int("iz_cmd_2", iz[2], 0x7A);
fail |= expect_int("iz_n1_flags", iz[3], 0x8E);
fail |= expect_int("iz_media_kind_passthrough", iz[4], 0x01);
fail |= expect_int("iz_media_width", iz[5], 0x0C);
fail |= expect_int("iz_lines_lsb", iz[7], 70);
fail |= expect_int("iz_page_index", iz[11], 0x00);
fail |= expect_int("iz_last_fixed", iz[12], 0x00);
/* ESC i K mode byte: 128-dot vs 560-dot families */
fail |= expect_int("esc_ik_128", ptouch_esc_ik_value(128u), 0x08);
fail |= expect_int("esc_ik_560", ptouch_esc_ik_value(560u), 0x0C);
return fail ? EXIT_FAILURE : EXIT_SUCCESS;
}

View File

@@ -1,5 +1,6 @@
# Brother P-touch — libusb でユーザから開けるようにする (Ubuntu 等) # Brother P-touch — libusb でユーザから開けるようにする (Ubuntu 等)
# PT-P900W: lsusb で ID 04f9:2085 # PT-P900W: lsusb で ID 04f9:2085
# PT-P750W: 04f9:2062 PT-P710BT: 04f9:20afBrother ラスター PDF Appendix A
# #
# 設置: # 設置:
# sudo cp udev/99-ptouch-label-brother.rules /etc/udev/rules.d/ # sudo cp udev/99-ptouch-label-brother.rules /etc/udev/rules.d/
@@ -11,6 +12,8 @@
# GROUP=plugdev … 従来どおり plugdev に所属していれば読み書き可 # GROUP=plugdev … 従来どおり plugdev に所属していれば読み書き可
SUBSYSTEM=="usb", ATTR{idVendor}=="04f9", ATTR{idProduct}=="2085", MODE="0660", GROUP="plugdev", TAG+="uaccess" SUBSYSTEM=="usb", ATTR{idVendor}=="04f9", ATTR{idProduct}=="2085", MODE="0660", GROUP="plugdev", TAG+="uaccess"
SUBSYSTEM=="usb", ATTR{idVendor}=="04f9", ATTR{idProduct}=="2062", MODE="0660", GROUP="plugdev", TAG+="uaccess"
SUBSYSTEM=="usb", ATTR{idVendor}=="04f9", ATTR{idProduct}=="20af", MODE="0660", GROUP="plugdev", TAG+="uaccess"
# 他機種を使う場合は lsusb の ID を足す例(コメントを外して複製): # 他機種を使う場合は lsusb の ID を足す例(コメントを外して複製):
# SUBSYSTEM=="usb", ATTR{idVendor}=="04f9", ATTR{idProduct}=="2086", MODE="0660", GROUP="plugdev", TAG+="uaccess" # SUBSYSTEM=="usb", ATTR{idVendor}=="04f9", ATTR{idProduct}=="2086", MODE="0660", GROUP="plugdev", TAG+="uaccess"