Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 33 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,39 @@ CustomerBarCode code = BarCode.Create("1000013", "東京都千代田区霞が関

ただしこの場合は住所全体を住所Bとして扱うため、町域名に「丁目・番・ノ」などの前の漢数字を含む住所(一番町、八丁堀、三ノ輪 など)では、町域名の漢数字も抜き出されて誤ったバーコードになります。住所A・住所Bに分けて渡すことをおすすめします。

### バーコードの内容
```csharp
string zip = code.ZipCode; // "1000013"
string number = code.AddressNumber; // "1-3-2-503"(マニュアルの「バーコード・データ」)
bool rep = code.IsRepresentativeZipCode; // 代表の郵便番号か
```

`AddressNumber` は 13 けたで切り捨てる前の値です。バーコードに含まれるのは先頭 13 けた分です。

代表の郵便番号(郵便番号簿の「以下に掲載がない場合」の番号)の郵便物には、カスタマバーコードを付けないことになっています([マニュアル p18](https://www.post.japanpost.jp/zipcode/zipmanual/p18.html))。`IsRepresentativeZipCode` は、マニュアルの「原則として下2けたが 00」に従って判定します。

### 印刷用の実寸
[マニュアル p12](https://www.post.japanpost.jp/zipcode/zipmanual/p12.html) の寸法(10 ポイントのときバー幅 0.6mm、バーピッチ 1.2mm、ロングバー 3.6mm)で出力します。大きさは 8〜11.5 ポイントで指定でき、省略時は 10 ポイントです。

```csharp
// mm 単位の SVG(余白は省略時 2mm)
string svg = code.ToPrintSvg(point: 10, margin: 2);

// System.Drawing: 宛名印刷(PrintDocument など)で、用紙の (x, y) mm の位置に描画
code.Draw(e.Graphics, 20, 30);

// SkiaSharp: PDF の (x, y) mm の位置に描画
code.Draw(canvas, 20, 30, CustomerBarCodeSkiaExtensions.PdfUnitsPerMillimeter);

// 独自に描画する場合は mm 単位のバー配置を使う
foreach (var bar in code.GetPrintBars())
{
// bar.X, bar.Y, bar.Width, bar.Height (mm)
}
```

マニュアル([p13](https://www.post.japanpost.jp/zipcode/zipmanual/p13.html))では、バーコードの上下左右に 2mm 以上の空白が必要です。`Draw` はバーだけを描くので、周囲を空けて配置してください。

### SVG(依存ライブラリなし)
```csharp
string svg = code.ToSvg();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -54,5 +54,42 @@ public static void Save(this CustomerBarCode code, string outputPath, ImageForma
img.Save(outputPath, format);
}
}

/// <summary>
/// 印刷用の実寸(mm)でカスタマバーコードを描画する
/// 宛名印刷(PrintDocument など)で、用紙の指定位置に描画するときに使う
/// </summary>
/// <remarks>
/// バーのみを黒で描画する(背景は描画しない)。
/// マニュアルでは上下左右に2mm以上の空白が必要。
/// </remarks>
/// <param name="code">カスタマバーコード</param>
/// <param name="graphics">描画先</param>
/// <param name="x">最初のバーの左端のX座標(mm)</param>
/// <param name="y">バーの上端のY座標(mm)</param>
/// <param name="point">大きさ(8~11.5ポイント)</param>
public static void Draw(this CustomerBarCode code, Graphics graphics, float x, float y, double point = 10)
{
if (code == null) throw new ArgumentNullException(nameof(code));
if (graphics == null) throw new ArgumentNullException(nameof(graphics));

var bars = code.GetPrintBars(point);
var pageUnit = graphics.PageUnit;
var pageScale = graphics.PageScale;
try
{
graphics.PageUnit = GraphicsUnit.Millimeter;
graphics.PageScale = 1;
foreach (var bar in bars)
{
graphics.FillRectangle(Brushes.Black, x + (float)bar.X, y + (float)bar.Y, (float)bar.Width, (float)bar.Height);
}
}
finally
{
graphics.PageUnit = pageUnit;
graphics.PageScale = pageScale;
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -66,5 +66,44 @@ public static void SavePng(this CustomerBarCode code, string outputPath, int sca
{
File.WriteAllBytes(outputPath, code.ToPng(scale));
}

/// <summary>
/// PDF(SKDocument.CreatePdf)のキャンバスで1mmあたりの単位数(1単位 = 1/72インチ)
/// </summary>
public const float PdfUnitsPerMillimeter = 72f / 25.4f;

/// <summary>
/// 印刷用の実寸(mm)でカスタマバーコードを描画する
/// PDF などに、用紙の指定位置に描画するときに使う
/// </summary>
/// <remarks>
/// バーのみを黒で描画する(背景は描画しない)。
/// マニュアルでは上下左右に2mm以上の空白が必要。
/// </remarks>
/// <param name="code">カスタマバーコード</param>
/// <param name="canvas">描画先</param>
/// <param name="x">最初のバーの左端のX座標(mm)</param>
/// <param name="y">バーの上端のY座標(mm)</param>
/// <param name="unitsPerMillimeter">キャンバスの1mmあたりの単位数(PDF は <see cref="PdfUnitsPerMillimeter"/>)</param>
/// <param name="point">大きさ(8~11.5ポイント)</param>
public static void Draw(this CustomerBarCode code, SKCanvas canvas, float x, float y, float unitsPerMillimeter, double point = 10)
{
if (code == null) throw new ArgumentNullException(nameof(code));
if (canvas == null) throw new ArgumentNullException(nameof(canvas));
if (!(unitsPerMillimeter > 0)) throw new ArgumentOutOfRangeException(nameof(unitsPerMillimeter), "1mmあたりの単位数は0より大きい値で指定してください。");

var bars = code.GetPrintBars(point);
using (var paint = new SKPaint { Color = SKColors.Black, IsAntialias = false })
{
foreach (var bar in bars)
{
canvas.DrawRect(SKRect.Create(
(x + (float)bar.X) * unitsPerMillimeter,
(y + (float)bar.Y) * unitsPerMillimeter,
(float)bar.Width * unitsPerMillimeter,
(float)bar.Height * unitsPerMillimeter), paint);
}
}
}
}
}
35 changes: 35 additions & 0 deletions ZipConvertCustomerBarCode.Tests/BarCodeTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,41 @@ public void 引数がnullなら例外()
Assert.Throws<ArgumentNullException>(() => BarCode.Create("0140113", null!));
}

[Theory]
[InlineData("100-0013", "1000013")]
[InlineData("100-0013", "1000013")]
public void 郵便番号は半角数字7桁で取得できる(string zipCode, string expected)
{
var code = BarCode.Create(zipCode, "1-3-2");

Assert.Equal(expected, code.ZipCode);
}

[Theory]
[InlineData("東京都千代田区霞が関", "1丁目3番2号 郵便プラザ503室", "1-3-2-503")]
[InlineData("千葉県鎌ヶ谷市右京塚", "東3丁目-20-5 郵便・A&bコーポB604号", "3-20-5B604")]
// 13けたを超えても切り捨てない
[InlineData("札幌市中央区南四条西", "29丁目1524-23 第2郵便ハウス501", "29-1524-23-2-501")]
[InlineData("とある町", "", "")]
public void 住所表示番号を取得できる(string townAddress, string blockAddress, string expected)
{
var code = BarCode.Create("1234567", townAddress, blockAddress);

Assert.Equal(expected, code.AddressNumber);
}

[Theory]
[InlineData("5250000", true)]
[InlineData("3510100", true)]
[InlineData("1000013", false)]
[InlineData("1000010", false)]
public void 代表の郵便番号を判定できる(string zipCode, bool expected)
{
var code = BarCode.Create(zipCode, "1-1");

Assert.Equal(expected, code.IsRepresentativeZipCode);
}

[Fact]
public void 常に23シンボルで開始コードと終了コードで挟まれる()
{
Expand Down
22 changes: 22 additions & 0 deletions ZipConvertCustomerBarCode.Tests/DrawingRenderTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,28 @@ public void GIFファイルに出力できる()
}
}

[Theory]
[InlineData(GraphicsUnit.Pixel)]
[InlineData(GraphicsUnit.Inch)]
public void 実寸で描画できる(GraphicsUnit pageUnit)
{
var code = BarCode.Create("1000013", "東京都千代田区霞が関", "1丁目3番2号 郵便プラザ503室");
using var img = new Bitmap(1000, 80);
// 254dpi = 10ピクセル/mm
img.SetResolution(254, 254);
using (var gr = Graphics.FromImage(img))
{
gr.Clear(Color.White);
gr.PageUnit = pageUnit;
code.Draw(gr, 1, 1);

// 描画後は元の単位に戻る
Assert.Equal(pageUnit, gr.PageUnit);
}

PrintAssert.Bars(code, (x, y) => img.GetPixel(x, y).ToArgb() == Color.Black.ToArgb(), 10, 1);
}

private static void AssertSamePixels(Bitmap expected, Bitmap actual)
{
Assert.Equal(expected.Width, actual.Width);
Expand Down
41 changes: 41 additions & 0 deletions ZipConvertCustomerBarCode.Tests/PrintAssert.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
namespace ZipConvertCustomerBarCode.Tests
{
public static class PrintAssert
{
/// <summary>
/// 実寸で描画した画像に、バーが正しい位置・形で描かれていることを確認する
/// バーの中心が黒、バーの間とバーのない段が白であることを点で調べる
/// </summary>
/// <param name="code">カスタマバーコード</param>
/// <param name="isBlack">ピクセルが黒かどうか</param>
/// <param name="pixelsPerMillimeter">1mmあたりのピクセル数</param>
/// <param name="originMillimeters">描画位置(X、Y共通、mm)</param>
public static void Bars(CustomerBarCode code, Func<int, int, bool> isBlack, double pixelsPerMillimeter, double originMillimeters)
{
bool At(double x, double y) =>
isBlack((int)((originMillimeters + x) * pixelsPerMillimeter), (int)((originMillimeters + y) * pixelsPerMillimeter));

var bars = code.GetPrintBars().ToList();
for (int i = 0; i < bars.Count; i++)
{
var bar = bars[i];
var centerX = bar.X + bar.Width / 2;
Assert.True(At(centerX, bar.Y + bar.Height / 2), $"{i}本目のバーが描画されていない");

// 上段(0~1.2mm)、下段(2.4~3.6mm)
var hasTop = bar.Type == BarType.Long || bar.Type == BarType.Ascender;
var hasBottom = bar.Type == BarType.Long || bar.Type == BarType.Descender;
Assert.Equal(hasTop, At(centerX, 0.6));
Assert.Equal(hasBottom, At(centerX, 3.0));

// バーの間(スペース0.6mm)
if (i < bars.Count - 1)
{
Assert.False(At(bar.X + 0.9, 1.8), $"{i}本目のバーの右が空いていない");
}
}
// 描画位置より左上は空白
Assert.False(At(-0.3, 1.8));
}
}
}
93 changes: 93 additions & 0 deletions ZipConvertCustomerBarCode.Tests/PrintTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
using System.Text.RegularExpressions;

namespace ZipConvertCustomerBarCode.Tests
{
// 印刷用の実寸
// http://www.post.japanpost.jp/zipcode/zipmanual/p12.html
public class PrintTests
{
private readonly CustomerBarCode code = BarCode.Create("1000013", "東京都千代田区霞が関", "1丁目3番2号 郵便プラザ503室");

[Theory]
// バー67本: (67 - 1) × バーピッチ + バー幅
[InlineData(10, 79.8, 3.6)]
[InlineData(8, 63.84, 2.88)]
[InlineData(11.5, 91.77, 4.14)]
public void バーコードの大きさはポイントに比例する(double point, double width, double height)
{
Assert.Equal(width, code.GetPrintWidth(point), 6);
Assert.Equal(height, code.GetPrintHeight(point), 6);
}

[Theory]
[InlineData(7.9)]
[InlineData(11.6)]
[InlineData(double.NaN)]
public void ポイントが8から11点5の範囲外なら例外(double point)
{
Assert.Throws<ArgumentOutOfRangeException>(() => code.GetPrintWidth(point));
Assert.Throws<ArgumentOutOfRangeException>(() => code.GetPrintHeight(point));
Assert.Throws<ArgumentOutOfRangeException>(() => code.GetPrintBars(point));
Assert.Throws<ArgumentOutOfRangeException>(() => code.ToPrintSvg(point));
}

[Fact]
public void バーの寸法はマニュアルどおり()
{
var bars = code.GetPrintBars().ToList();

Assert.Equal(67, bars.Count);
Assert.Equal(0, bars[0].X, 6);
for (int i = 1; i < bars.Count; i++)
{
// バーピッチ 1.2mm
Assert.Equal(1.2, bars[i].X - bars[i - 1].X, 6);
}
Assert.All(bars, bar =>
{
// バー幅 0.6mm
Assert.Equal(0.6, bar.Width, 6);
// ロングバー 3.6mm、セミロングバー 2.4mm、タイミングバー 1.2mm
var (y, height) = bar.Type switch
{
BarType.Long => (0.0, 3.6),
BarType.Ascender => (0.0, 2.4),
BarType.Descender => (1.2, 2.4),
_ => (1.2, 1.2),
};
Assert.Equal(y, bar.Y, 6);
Assert.Equal(height, bar.Height, 6);
});
}

[Fact]
public void 印刷用のバーはピクセル単位のバーと同じ種類と並び()
{
Assert.Equal(code.GetBars().Select(x => x.Type), code.GetPrintBars().Select(x => x.Type));
}

[Fact]
public void 印刷用SVGはmm単位で余白を含む()
{
var svg = code.ToPrintSvg(10, 2);

Assert.StartsWith("<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"83.8mm\" height=\"7.6mm\" viewBox=\"0 0 83.8 7.6\">", svg);
Assert.EndsWith("</svg>", svg);

var rects = Regex.Matches(svg, "<rect x=\"([0-9.]+)\" y=\"([0-9.]+)\" width=\"([0-9.]+)\" height=\"([0-9.]+)\"/>").ToList();
Assert.Equal(67, rects.Count);
// 最初のバーは余白(2mm)の位置から
Assert.Equal("2", rects[0].Groups[1].Value);
Assert.Equal("2", rects[0].Groups[2].Value);
Assert.Equal("0.6", rects[0].Groups[3].Value);
Assert.Equal("3.6", rects[0].Groups[4].Value);
}

[Fact]
public void 余白が負なら例外()
{
Assert.Throws<ArgumentOutOfRangeException>(() => code.ToPrintSvg(10, -1));
Assert.Throws<ArgumentOutOfRangeException>(() => code.ToPrintSvg(10, double.NaN));
}
}
}
43 changes: 43 additions & 0 deletions ZipConvertCustomerBarCode.Tests/SkiaRenderTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,49 @@ public void PNGファイルに出力できる()
}
}

[Fact]
public void 実寸で描画できる()
{
var code = BarCode.Create("1000013", "東京都千代田区霞が関", "1丁目3番2号 郵便プラザ503室");
using var img = new SKBitmap(1000, 80);
using (var canvas = new SKCanvas(img))
{
canvas.Clear(SKColors.White);
// 10ピクセル/mm
code.Draw(canvas, 1, 1, 10);
}

PrintAssert.Bars(code, (x, y) => img.GetPixel(x, y) == SKColors.Black, 10, 1);
}

[Fact]
public void PDFに描画できる()
{
var code = BarCode.Create("1000013", "東京都千代田区霞が関", "1丁目3番2号 郵便プラザ503室");
using var stream = new MemoryStream();
using (var document = SKDocument.CreatePdf(stream))
{
// A4
var canvas = document.BeginPage(210 * CustomerBarCodeSkiaExtensions.PdfUnitsPerMillimeter, 297 * CustomerBarCodeSkiaExtensions.PdfUnitsPerMillimeter);
code.Draw(canvas, 20, 20, CustomerBarCodeSkiaExtensions.PdfUnitsPerMillimeter);
document.EndPage();
}

Assert.True(stream.Length > 0);
Assert.Equal("%PDF", System.Text.Encoding.ASCII.GetString(stream.ToArray(), 0, 4));
}

[Fact]
public void 単位数が0以下なら例外()
{
var code = BarCode.Create("1000013", "1-3-2");
using var img = new SKBitmap(10, 10);
using var canvas = new SKCanvas(img);

Assert.Throws<ArgumentOutOfRangeException>(() => code.Draw(canvas, 0, 0, 0));
Assert.Throws<ArgumentOutOfRangeException>(() => code.Draw(canvas, 0, 0, float.NaN));
}

private static void AssertSamePixels(SKBitmap expected, SKBitmap actual)
{
Assert.Equal(expected.Width, actual.Width);
Expand Down
Loading