Bakulog

獏の夢日記的な何か。

テプラというラベルプリンターで遊んでみた

記念すべき?社会人になってから初の記事投稿です。

 

あらすじ

PCから画像をそのままテプラに投げてシール上に印刷するプログラムって書けるのかな?と試しました。結論としては、少し下準備をすることで可能になります。

 

もくじ

  1. テプラって何?っていうかなんで買ったの?
  2. 普通の使い方なんか紹介するわけないじゃないですか
  3. 画像を流し込んで印刷させるためのアレコレ
  4. まとめ

 

1. テプラって何?っていうかなんで買ったの?

テプラとは主にシール上への印刷に利用されるプリンターです。世間的には「ラベルプリンター」と呼ばれるジャンルの製品です。

私が買ったのはその中でもSR5500Pというやつで、通常のテプラと違ってキーボードインターフェースが無く、PCからの印刷のみを受け付けるモデルになっています。電源無くても電池で動ける、というのも特長の一つです。

tepra_and_quma

だいたい文庫本と同じくらいの背丈ですね。

このテプラですが、もともと購入した理由はネタとしてシールで名刺を作るためです。「プライベート名刺の発注って案外めんどくさいけどテプラでシール刷って白い名刺カードに貼れば簡単じゃん!」と。

この方法は1枚単位で違うデザインの名刺が作れる、またシール以外にマグネットやリボンへ印刷可能などの利点がありますが、フルカラー印刷が出来ず単価も結構高くなるので万人にはオススメしません。例として黒地に白の文字が打てるテープを使ったサンプルをひとつ。

https://twitter.com/baku_dreameater/status/727459238826532865

   

2. 普通の使い方なんか紹介するわけないじゃないですか

サブタイトルがムダに煽ってますが、本記事に関係する範囲で公式ページの情報を少し紹介します。

普通のテプラの使い方としては下記のような印刷手段があります。

  • テプラ上についてるキーボードで直接文字を打ち込んで印刷1
  • iOS機からTEPRA LINKというアプリ経由で印刷
  • SPC10というPC向けラベルデザインソフトでデザインして印刷
  • SPC10 APIによってPC+自作プログラムから印刷(後述)

特にSPC10を使えばパワポのスライド作成と同じ感覚でラベルが作れるため、普通はSPC10を使ってしまうのがラクちんです。上にあるようなQRコードの作成に対応してるのも嬉しいです。

またSPC10 APIを使うと外部アプリケーションから印刷関連のコマンドが打てるようになります。

ただし。

このSPC10 APIAPIと名乗っているわりに機能が絞られています。おもな機能は以下の通り。

  • あらかじめ作っておいたしたラベルファイル(.tpe)を刷る
  • ラベルファイルにcsvxlsの文字列データ(人の名前や電話番号など)を注入して刷る
  • テプラに今入れているテープの幅を調べる
  • 印刷プレビュー確認用にbmpファイルを保存する  

普通はこれで十分だろ、と言われたら、まあそうです。逆に何が出来ないかというと、以下のような処理はサポートされていません。

  • 新しいラベルファイルの作成
  • デザインの変更や切り替えなど、ラベルファイルの変更を含むような作業

  まあ普通はプログラムからラベルのデザイン変更なんかできなくても何も困らないのですが、一つ気になった点として外部プログラムから画像ファイルが挿し込めないことに気づきました。

仮に外部から画像の差し込みが出来たら何が嬉しいかという話ですが、メリットは二つ考えられます。

  • Twitterプロフィールのような画像/テキスト混成のデータを投げて印刷できる
  • 複雑なデータでも動的に可視化してテプラに投げつけられる

 

「それテプラでやる意味あるの?」というコメントは受け付けません。さっそくやってみましょう。

   

3. 画像を流し込んで印刷させるためのアレコレ

さて。画像を流し込んで印刷させる方法ですが、上述の通り公式サポートはされていません。しかし大まかには以下の3ステップを順番にこなせばうまくいきます。

  1. ラベルファイル(.tpe)を普通にSPC10で作り、いったん適当なプレースホルダーとなる画像を貼りつけて保存
  2. 注入したい画像を持ってきて、1で作った.tpeファイルを読み込み、プレースホルダー画像のビットマップデータ部分を上書き
  3. データをすげかえたラベルファイルを別のファイルとして保存し、それをSPC10 APIから印刷

 

要するにラベルファイルをバイナリレベルで直接書き換えます。なんだか原始って感じがしますしEULAが、1日考えた結果としてはコレが一番スマートだという結論になりました。ということで、ここから具体的な操作手順に入ります。以下では正方形の画像をプログラムから注入して刷るという作業の流れを紹介します。

 

3-1. テンプレートになるファイルを作って画像を貼り、保存

SPC10を起動し、以下のように設定していきます。

私のテプラは24mmまで刷れるので下記のようにしていますが、サイズに応じてパラメータを適宜調整してください。

3-1-1. テープのサイズと余白調節

テープ幅として24mmを選択します。この幅のテープでは上下に3mmの余白が生じるため、テープの横方向サイズもそれに合わせ、長さ24mm、余白3mmにします。

3-1-2. プレースホルダー画像の準備

SPC10とは関係なく適当な領域確保用の画像を用意します。正方形で一面同じ色(※808080などグレーカラーの画像にしてください。というか黒色以外にしてください。理由はすぐ後で出てきます)をした画像を作り、24bit/pixelのRGBチャネルからなるbmpファイルで保存します。ここで作るbmpの解像度は印刷の細かさに影響するため、あまり低すぎる値にしないようにしてください。今回は128x128にします2

後の説明でも出てくるので、この画像ファイルの名前はbase.bmpだということにします。

3-1-3. ラベルファイルへプレースホルダー画像を貼る

SPC10の「イメージ」ボタンを押し、上で作ったbmpファイルをインポートします。インポートしたら画像の位置とサイズを調整します(画像をダブルクリックして「プロパティ」を開き「位置」タブを表示)。

今回は余白3mmで印刷可能なエリアが18mm四方の正方形なので、位置は左上から(3mm, 3mm)とし、サイズも限界ギリギリの18mm四方に設定します。

また、これは必須ではありませんが推奨する処理として、同じプロパティ画面で「画像調整」の「白黒モード」を選んでおいた方が思った通りの出力を得やすいです。

以上の操作が完了したら適当な名前でファイルを保存します。以下ではこのファイルがsource_canvas.tpeという名前であるとして話を進めます。

 

3-2. バイナリを読んでみる

この作業では、上で配置したプレースホルダー画像がバイナリ内のどこに置かれたかを調べます。

先程の手順で保存したsource_canvas.tpeファイルを適当なエディタでバイナリモードで開いて閲覧します

バイナリを開けたら、ファイル内部でCLw707ImagePartsという文字列を検索なり目grepなりで探します。私の場合、これは0x00036480近辺の位置にありました。おそらくPC環境、テプラの種類、テープのサイズなどによって位置が多少変動するので注意して下さい。

tpe_file_insight2

この文字列の下に"7E"がひたすら並んでいる箇所がありますが、ここがプレースホルダー画像のデータ格納場所です。私が作った"base.bmp"は一面グレー(7E7E7E)の画像として保存したので、これで画像バイナリを上書きすべき位置が分かったことになります。

もしプレースホルダー画像を全面黒(000000)などの画像にしていると、ここの作業で画像バイナリの開始位置がよく分からなくなって詰みます。

ともかく上記のように画像データの開始位置が分かったら、その箇所の位置オフセットをどこかにメモしておきます。上の場合、メモすべき数値は0x0003652aとなります。

 

3-3. 空のcsvファイルを用意

プログラムで作るのでも構いませんが、話を分かりやすくするために手作業でやってしまいます。

テキストエディタで空のファイルを作り、適当なcsvファイルとして保存します。今回は"blank_data.csv"という名前にしておきます。

 

3-4. そろそろコードを書こう

準備が整ったのでコードを書きます。SPC10 APIのサンプルはExcelVBAで書かれていますが、APIの性質上VBAは不要なので、ここではVS2015上でC#のコンソールアプリケーションを作ったケースを示します。Bitmapクラスを利用するため、コンソールアプリケーションにSystem.Drawingの参照を追加してください。

using System;
using System.IO;
using System.Diagnostics;
using System.Drawing;

namespace TestSpcApi
{
    class Program
    {
        #region 設定パラメータ定義(ホントはGUIとかコマンド引数で設定する)

        /// <summary> カラの画像が入った下地になるラベルファイル </summary>
        static string sourceTpeFilePath = "source_canvas.tpe";

        /// <summary> 注入したい画像ファイル(bmpじゃなくても良い) </summary>
        static string sourceImageFilePath = "source_image.png";

        //空のテキストファイルのフルパス
        static string blankCsvFilePath => Path.GetFullPath("blank_data.csv");
        //画像を挿し込んだあとの更新されたラベルファイルのフルパス
        static string printTpeFilePath => Path.GetFullPath("img_inserted.tpe");

        static string previewBmpFilePath
            => Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.Desktop), "tepra_preview");

        //下地のラベルファイルで画像値が入ってる場所までのオフセット
        static int bmpOffset = 0x0003652a;

        //二値化を行う際の輝度のしきい値
        static float brightnessThreshold = 0.7f;

        //地が白などで文字が黒のテープならこう
        static Color foregroundColor = Color.Black;
        static Color backgroundColor = Color.White;
        //地が黒や濃い青で文字が白のテープならこう
        //static Color foregroundColor = Color.White;
        //static Color backgroundColor = Color.Black;

        #endregion

        static void Main(string[] args)
        {
            // 前半: リサイズ/二値化した画像を拾い上げ、テプラファイルの対応する箇所に上書き
            byte[] tpeData = File.ReadAllBytes(sourceTpeFilePath);
            Size size = GetImageSizeOfTpeData(tpeData);
            //行あたりバイト数は4の倍数になってないといけない点に注意!(bmpの仕様)
            int stride = size.Width * 3 + ((size.Width * 3) % 4);

            using (var sourceImage = new Bitmap(sourceImageFilePath))
            using (var workImage = GetResizedBmp(sourceImage, size))
            {
                BinarizeImage(workImage, brightnessThreshold, backgroundColor, foregroundColor);

                for (int y = 0; y < workImage.Height; y++)
                {
                    for (int x = 0; x < workImage.Width; x++)
                    {
                        int pos = bmpOffset + y * stride + x * 3;
                        byte[] color = BitConverter.GetBytes(workImage.GetPixel(x, workImage.Height - y - 1).ToArgb());
                        Array.Copy(color, 0, tpeData, pos, 3);
                    }
                }
            }
            File.WriteAllBytes(printTpeFilePath, tpeData);


            //後半: 印刷処理
            string exeFileName = GetSpc10ExeFileName();
            if (!File.Exists(exeFileName))
            {
                Console.WriteLine($"Failed: SPC10 was not found. Searced path is: {exeFileName}");
                Console.WriteLine("Press ENTER to quit...");
                Console.ReadLine();
                return;
            }

            string commandLineArgs = $"/pt \"{printTpeFilePath},{blankCsvFilePath},1\"";
            Process.Start(exeFileName, commandLineArgs);
        }

        /// <summary>リサイズした画像を取得します。</summary>
        /// <param name="srcPath">リサイズ前の画像のパス</param>
        /// <param name="destSize">リサイズ後の画像サイズ</param>
        /// <returns>リサイズされた画像</returns>
        static Bitmap GetResizedBmp(Bitmap source, Size destSize)
        {
            var destBmp = new Bitmap(destSize.Width, destSize.Height);
            using (var g = Graphics.FromImage(destBmp))
            {
                g.InterpolationMode = System.Drawing.Drawing2D.InterpolationMode.High;
                g.DrawImage(source, new Rectangle(0, 0, destSize.Width, destSize.Height));
            }
            return destBmp;
        }

        /// <summary>画像中の色を輝度の閾値によって二値化する</summary>
        /// <param name="target">操作対象となり書き換えられる画像</param>
        /// <param name="threshold">輝度の閾値</param>
        /// <param name="brightColor">輝度が高い場合に適用される色</param>
        /// <param name="darkColor">輝度が低い場合に適用される色</param>
        static void BinarizeImage(Bitmap target, float threshold, Color brightColor, Color darkColor)
        {
            for (int y = 0; y < target.Height; y++)
            {
                for (int x = 0; x < target.Width; x++)
                {
                    float brightness = target.GetPixel(x, y).GetBrightness();
                    Color c = brightness > threshold ?
                        brightColor :
                        darkColor;
                    target.SetPixel(x, y, c);
                }
            }
        }

        /// <summary> SPC10.exeの標準的なインストール先パスを取得します。 </summary>
        /// <returns>SPC10.exeがあるハズの場所</returns>
        static string GetSpc10ExeFileName()
            => Environment.Is64BitOperatingSystem ?
            @"C:\Program Files (x86)\KING JIM\TEPRA SPC10\SPC10.exe" :
            @"C:\Program Files\KING JIM\TEPRA SPC10\SPC10.exe";


        /// <summary>
        /// tpeファイルに挿入できる画像のサイズを取得します。
        /// </summary>
        /// <param name="tpeData">書き込み先となるtpeファイルを読み出したバイナリデータ</param>
        /// <returns>画像のサイズ</returns>
        static Size GetImageSizeOfTpeData(byte[] tpeData)
            => new Size(128, 128);
        //上記の方法は、単にテンプレ作成時に仕込んだbmpのサイズを自分で覚えておくというもの。
        //プレビュー出力および


        ////cf: bmpのファイルフォーマットを調べたうえで上記bmpOffsetの近辺を探すと
        ////画像の幅や高さ情報も書かれてるのが見つかるのでソレ使ってのサイズ取得もあり。
        //static int bmpWidthOffset = 0x00036506;
        //static int bmpHeightOffset = 0x0003650a;
        //static Size GetImageSizeOfTpeData(byte[] tpeData)
        //    => new Size(
        //        BitConverter.ToInt16(tpeData, bmpWidthOffset),
        //        BitConverter.ToInt16(tpeData, bmpHeightOffset)
        //        );


    }
}

 

作成したら、プログラムの実行ディレクトリに以下のファイルを配置します。

  • source_canvas.tpe
  • blank_data.csv

さらに、実際に印刷したい画像ファイルをsource_image.pngという名前で、これも実行ディレクトリに置きます。

以上の準備が出来たらPCにテプラを接続し、実行します。うまくいくと画像が出力されます。自分のツイッターアイコン(をちょっと直したバージョン)の結果はこんな感じ。

icon_print_sample

線が細いと見栄えが少しわるくなるようです。線がしっかりしたアイコンだとどうかな?と思ってプロ生ちゃんのアイコンでも試させてもらった所、今度はキレイに出ました。

icon_pronama_chan

テプラは仕様上ふつーのプリンターよりDPIが低いので、絵との相性を考えるか、印刷前の画像処理をもうちょっと丁寧にやった方がいいのかな、という感触が得られました。

   

4. まとめ

もうちょっと上手に使えばイベントとかでも役に立つ…んですかね?質問等あれば遠慮なく本記事のコメントなりTwitterなりへお寄せ下さい。

   

余談: tpeファイルの拡張子をbmpに書き換えると?

本題とは関係ありませんが小ネタを一つ。tpeファイルの先頭部分は(おそらくサムネイル表示のみを目的として)bmpファイルフォーマットと同じ形式のデータを保持しています。したがってtpeファイルの拡張子をbmpに書き換えるとサムネイルが正しく表示されますし、ダブルクリックすれば普通のフォトビューアーなどで見れます。このbmp部分が普通に200kbyteくらいあるのを見ると「これディスクの無駄遣いでは?」という感がありますが、まあ見なかったことにしておきましょう…。


  1. 私が買ったSR5500Pを含め、いくつかのテプラはキーボードが無いのでこの方法では刷れません。 
  2. 実はSR5500Pの仕様(ここのスペックというタブ)を見るとDPIが180とあり、これと今回作ってる18mmの画像という数値から計算すると、解像度の上限は128であるらしいことが分かります。