C#

カスタマバーコード

郵便物を発送するときに使うカスタマバーコードというのがある。1000通以上を料金後納郵便または料金別納郵便で送る場合に、カスタマバーコードを使うと割り引きを受けられるそうだ。普通の人には用がないと思うが、割り引きを受けられなくても書いておけば郵便局の人は助かるのかな。

ソースコード

/// <summary>
/// カスタマバーコードを描画します。
/// あらかじめ Graphics.PageUnit を GraphicsUnit.Millimeter にしてください。
/// カスタマバーコードのサイズは 79.8×3.6mm です。
/// 周囲に少なくとも 2mm 以上の余白が必要です。できれば 5mm 以上あるほうが良いです。
/// 縦書きする場合は Graphics.Transform を利用してください。
/// </summary>
/// <param name="g">Graphicsを指定します。</param>
/// <param name="x">左上のx座標を指定します。</param>
/// <param name="y">左上のy座標を指定します。</param>
/// <param name="ZipNo">郵便番号を指定します。</param>
/// <param name="AddrNo">住所表示番号を指定します。</param>
public static void DrawCustomerBarCode(Graphics g, float x, float y, string ZipNo, string AddrNo)
{
    //
    //  カスタマバーコードの仕様については以下のURLを参照
    //
    //  https://www.post.japanpost.jp/zipcode/zipmanual/p10.html
    //

    const int StartIndex = 0; // スタートのインデックス
    const int StartLength = 2 * 1; // スタートの長さ
    const int ZipNoIndex = StartIndex + StartLength; // 郵便番号のインデックス
    const int ZipNoLength = 3 * 7; // 郵便番号の長さ
    const int AddrNoIndex = ZipNoIndex + ZipNoLength; // 住所表示番号のインデックス
    const int AddrNoLength = 3 * 13; // 住所表示番号の長さ
    const int CheckDigitIndex = AddrNoIndex + AddrNoLength; // チェック数字のインデックス
    const int CheckDigitLength = 3 * 1; // チェック数字の長さ
    const int StopIndex = CheckDigitIndex + CheckDigitLength; // ストップのインデックス
    const int StopLength = 2 * 1; // ストップの長さ
    const int TotalLength = StopIndex + StopLength; // トータルの長さ

    const float BarWidth = 0.6f; // バーの幅(ミリメートル)
    const float BarPitch = 1.2f; // バーのピッチ(ミリメートル)


    //
    //  バーの種類を表す '1' ~ '4' の文字に変換
    //

    string[] Codes = { // コードの配列
        "144",  // '0'
        "114",  // '1'
        "132",  // '2'
        "312",  // '3'
        "123",  // '4'
        "141",  // '5'
        "321",  // '6'
        "231",  // '7'
        "231",  // '8'
        "411",  // '9'
        "414",  // '-'
        "324",  // CC1
        "342",  // CC2
        "234",  // CC3
        "432",  // CC4
        "243",  // CC5
        "423",  // CC6
        "441",  // CC7
        "111"   // CC8
    };

    char[] Bars = new char[TotalLength]; // バーの種類を表す '1' ~ '4' の文字を格納する配列
    int BarIndex = 0; // 現在のインデックス
    int CheckSum = 0; // チェックサム

    // コードを追加
    Action<string> AppendCode = (string Code) =>
    {
        foreach (char c in Code)
        {
            Bars[BarIndex++] = c;
        }
    };

    // コードのインデックスを指定してコードを追加
    Action<int> AppendByIndex1 = (int CodeIndex) =>
    {
        AppendCode(Codes[CodeIndex]);
    };

    // コードのインデックスを指定してコードを追加
    Action<int> AppendByIndex2 = (int CodeIndex) =>
    {
        AppendByIndex1(CodeIndex);
        CheckSum += CodeIndex;
    };

    // コードのインデックスを指定してコードを追加; 62字まで
    Action<int> AppendByIndex3 = (int CodeIndex) =>
    {
        if (BarIndex < CheckDigitIndex)
        {
            AppendByIndex2(CodeIndex);
        }
    };

    // 数字、ハイフン、アルファベットで構成された文字列をコードにして追加
    Action<string> AppendText = (string s) =>
    {
        foreach (char c in s)
        {
            if (c == '-')
            {
                AppendByIndex3(10);
            }
            else if ((c >= '0') && (c <= '9'))
            {
                AppendByIndex3((int)c - '0');
            }
            else if ((c >= 'A') && (c <= 'Z'))
            {
                int n = (int)c - 'A';
                AppendByIndex3(11 + (n / 10)); // CC1 ~ CC3
                AppendByIndex3(n % 10); // '0' ~ '9'
            }
            else
            {
                throw new ArgumentException("不正な文字があります。カスタマバーコードに変換できません。");
            }
        }
    };


    AppendCode("13"); // スタート

    AppendText(ZipNo.Replace("-", string.Empty));
    AppendText(AddrNo);

    while (BarIndex < CheckDigitIndex)
    {
        AppendByIndex2(14); // CC4
    }

    int CheckDigit = (19 - (CheckSum % 19)) % 19; // チェック数字
    AppendByIndex1(CheckDigit);

    AppendCode("31"); // ストップ


    //
    //  描画
    //

    // ロングバーとセミロングバー(上)のy座標
    float Y1 = y;

    // セミロングバー(下)とタイミングバーのy座標
    float Y2 = y + 1.2f;

    // バーの高さ
    float BarHeight = 0.0f;

    foreach (char c in Bars)
    {
        switch (c)
        {
            case '1': // ロングバー
                y = Y1;
                BarHeight = 3.6f;
                break;

            case '2': // セミロングバー(上)
                y = Y1;
                BarHeight = 2.4f;
                break;

            case '3': // セミロングバー(下)
                y = Y2;
                BarHeight = 2.4f;
                break;

            case '4': // タイミングバー
                y = Y2;
                BarHeight = 1.2f;
                break;
        }

        g.FillRectangle(Brushes.Black, x, y, BarWidth, BarHeight);
        x += BarPitch;
    }
}

一度に1000回以上実行することを考えると、少し心配なところもある。ラムダを使うよりクラスにまとめたほうが、効率が良いような気がするけど、どのみちプリンタの印刷速度よりは速いだろう。

座標をミリメーターでしか指定できないのが多少もどかしい。フォントのサイズや線の太さにはポイントを使うのが普通なので、印刷では全体的にポイントで行うほうが都合が良いと思う。ポイントでやりたいときはGraphics.ScaleTransform()を使う。

const float _Scale = 72.0f / 25.4f;
Matrix _Saved = g.Transfrom;
g.ScaleTransform(_Scale, _Scale);
DrawCustomerBarCode(e.Graphics, x / _Scale, y / _Scale, ZipNo, AddrNo);
g.Transform = _Saved;

サンプルコード

private void PrintDocument_BeginPrint(object sender, PrintEventArgs e)
{
    PrintDocument Document = (PrintDocument)sender;

    // 長型3号; 120×235mm
    int PaperWidth = (int)(Math.Round(100.0 * 120.0 / 25.4, 0));
    int PaperHeight = (int)(Math.Round(100.0 * 235.0 / 25.4, 0));

    Document.DefaultPageSettings.PaperSize = new PaperSize("長型3号", PaperWidth, PaperHeight);
    Document.DefaultPageSettings.Landscape = true;
}

private void PrintDocument_PrintPage(object sender, PrintPageEventArgs e)
{
    string ZipNo = "000-0000";
    string Addr = "東京都東京市0-0-0 東京ビル101";
    string Cmpn = "株式会社 悪徳商事";
    string Name = "悪徳 太郎 様";
    string AddrNo = "0-0-0-101";

    using (StringFormat _Format = new StringFormat(StringFormat.GenericTypographic))
    {
        e.Graphics.PageUnit = GraphicsUnit.Millimeter;
        e.Graphics.SmoothingMode = SmoothingMode.AntiAlias;

        RectangleF _Rect = new RectangleF(
            25.4f * e.MarginBounds.Left / 100.0f,
            25.4f * e.MarginBounds.Top / 100.0f,
            25.4f * e.MarginBounds.Width / 100.0f,
            25.4f * e.MarginBounds.Height / 100.0f);

        using (Font _Font = new Font("MS 明朝", 12.0f))
        {
            float _FontHeight = 25.4f * _Font.SizeInPoints / 72.0f;
            float _LineFeed = 2.0f * _FontHeight;
            float _Indent = 3.0f * _FontHeight;

            e.Graphics.DrawString("〒" + ZipNo, _Font, Brushes.Black, _Rect, _Format);
            _Rect.Y += _LineFeed;

            _Rect.X += _Indent;
            _Rect.Width -= _Indent;

            e.Graphics.DrawString(Addr, _Font, Brushes.Black, _Rect, _Format);
            _Rect.Y += _LineFeed;

            e.Graphics.DrawString(Cmpn, _Font, Brushes.Black, _Rect, _Format);
            _Rect.Y += _LineFeed;

            _Rect.X -= _Indent;
            _Rect.Width += _Indent;
        }

        using (Font _Font = new Font("MS 明朝", 24.0f))
        {
            float _FontHeight = 25.4f * _Font.SizeInPoints / 72.0f;

            _Format.Alignment = StringAlignment.Center;
            _Rect.Y += 0.5f * _FontHeight;

            e.Graphics.DrawString(Name, _Font, Brushes.Black, _Rect, _Format);
            _Rect.Y += _FontHeight;
        }

        _Rect.Y += 5.0f;
        DrawCustomerBarCode(e.Graphics, (_Rect.Left + _Rect.Right - 79.8f) / 2.0f, _Rect.Y, ZipNo, AddrNo);
    }
}
サンプル

備考

  • カスタマバーコードを使うには、事前に品質の検査を受ける必要があって、印刷サンプルを三枚提出するそうだ。大量に印刷する前に確認しておいたほうが良いとのこと。
  • 住所表示番号が13桁に収まらない場合、14桁目以降はカスタマバーコードに記載されない。最後に配達員が目視で住所を確認して配達するので、それで大丈夫なのだろう。
  • 住所表示番号を調べるのは、結構面倒くさい。一応、方法は郵便局のサイトで説明されている。でも、郵便番号のリストが必要になるし、アルゴリズム的にプログラムで解決できるかどうかわからない。たぶん、データベースか何かに住所を登録するときに、人の手で住所表示番号を調べて一緒に登録しておくのが妥当だろう。
  • 大口事業所や私書箱の個別番号では、住所表示番号は必要ない。

ところで、割り引きの適用のない、品質検査を受けていないカスタマバーコードを印刷して発送することは、問題ないだろうか。

(2021/07/15 初稿)
(2021/08/13 更新)