From 3a81028e0b233f5f12a2bce6c86de8b09f57e20c Mon Sep 17 00:00:00 2001 From: cssho Date: Thu, 5 Nov 2026 21:41:14 +0900 Subject: [PATCH] =?UTF-8?q?=E3=83=90=E3=83=BC=E3=82=B3=E3=83=BC=E3=83=89?= =?UTF-8?q?=E3=83=87=E3=83=BC=E3=82=BF=E3=81=AE=E5=85=AC=E9=96=8B=E3=80=81?= =?UTF-8?q?=E5=8D=B0=E5=88=B7=E7=94=A8=E3=81=AE=E5=AE=9F=E5=AF=B8=E5=87=BA?= =?UTF-8?q?=E5=8A=9B=E3=80=81=E4=BB=A3=E8=A1=A8=E9=83=B5=E4=BE=BF=E7=95=AA?= =?UTF-8?q?=E5=8F=B7=E3=81=AE=E5=88=A4=E5=AE=9A=E3=82=92=E8=BF=BD=E5=8A=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - ZipCode / AddressNumber プロパティを追加(AddressNumber はマニュアルの「バーコード・データ」) - マニュアル p12 の寸法で、mm 単位のバー配置(GetPrintBars)と SVG(ToPrintSvg)を出力 - System.Drawing 版と SkiaSharp 版に、実寸で描画する Draw を追加 - 代表の郵便番号かどうか(下2けたが00)を IsRepresentativeZipCode で判定 - README に使い方を追記 Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01HNb8H3ydQTR8dWU8d28osE --- README.md | 33 +++++ .../CustomerBarCodeDrawingExtensions.cs | 37 ++++++ .../CustomerBarCodeSkiaExtensions.cs | 39 ++++++ .../BarCodeTests.cs | 35 ++++++ .../DrawingRenderTests.cs | 22 ++++ .../PrintAssert.cs | 41 +++++++ ZipConvertCustomerBarCode.Tests/PrintTests.cs | 93 ++++++++++++++ .../SkiaRenderTests.cs | 43 +++++++ ZipConvertCustomerBarCode/Bar.cs | 38 ++++++ ZipConvertCustomerBarCode/BarCode.cs | 39 +++--- ZipConvertCustomerBarCode/CustomerBarCode.cs | 113 +++++++++++++++++- 11 files changed, 510 insertions(+), 23 deletions(-) create mode 100644 ZipConvertCustomerBarCode.Tests/PrintAssert.cs create mode 100644 ZipConvertCustomerBarCode.Tests/PrintTests.cs diff --git a/README.md b/README.md index bdebc93..8985b49 100644 --- a/README.md +++ b/README.md @@ -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(); diff --git a/ZipConvertCustomerBarCode.Drawing/CustomerBarCodeDrawingExtensions.cs b/ZipConvertCustomerBarCode.Drawing/CustomerBarCodeDrawingExtensions.cs index e7ef546..fad6b39 100644 --- a/ZipConvertCustomerBarCode.Drawing/CustomerBarCodeDrawingExtensions.cs +++ b/ZipConvertCustomerBarCode.Drawing/CustomerBarCodeDrawingExtensions.cs @@ -54,5 +54,42 @@ public static void Save(this CustomerBarCode code, string outputPath, ImageForma img.Save(outputPath, format); } } + + /// + /// 印刷用の実寸(mm)でカスタマバーコードを描画する + /// 宛名印刷(PrintDocument など)で、用紙の指定位置に描画するときに使う + /// + /// + /// バーのみを黒で描画する(背景は描画しない)。 + /// マニュアルでは上下左右に2mm以上の空白が必要。 + /// + /// カスタマバーコード + /// 描画先 + /// 最初のバーの左端のX座標(mm) + /// バーの上端のY座標(mm) + /// 大きさ(8~11.5ポイント) + 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; + } + } } } diff --git a/ZipConvertCustomerBarCode.SkiaSharp/CustomerBarCodeSkiaExtensions.cs b/ZipConvertCustomerBarCode.SkiaSharp/CustomerBarCodeSkiaExtensions.cs index 19a977b..be4e7ed 100644 --- a/ZipConvertCustomerBarCode.SkiaSharp/CustomerBarCodeSkiaExtensions.cs +++ b/ZipConvertCustomerBarCode.SkiaSharp/CustomerBarCodeSkiaExtensions.cs @@ -66,5 +66,44 @@ public static void SavePng(this CustomerBarCode code, string outputPath, int sca { File.WriteAllBytes(outputPath, code.ToPng(scale)); } + + /// + /// PDF(SKDocument.CreatePdf)のキャンバスで1mmあたりの単位数(1単位 = 1/72インチ) + /// + public const float PdfUnitsPerMillimeter = 72f / 25.4f; + + /// + /// 印刷用の実寸(mm)でカスタマバーコードを描画する + /// PDF などに、用紙の指定位置に描画するときに使う + /// + /// + /// バーのみを黒で描画する(背景は描画しない)。 + /// マニュアルでは上下左右に2mm以上の空白が必要。 + /// + /// カスタマバーコード + /// 描画先 + /// 最初のバーの左端のX座標(mm) + /// バーの上端のY座標(mm) + /// キャンバスの1mmあたりの単位数(PDF は ) + /// 大きさ(8~11.5ポイント) + 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); + } + } + } } } diff --git a/ZipConvertCustomerBarCode.Tests/BarCodeTests.cs b/ZipConvertCustomerBarCode.Tests/BarCodeTests.cs index e7bc4b9..58b301d 100644 --- a/ZipConvertCustomerBarCode.Tests/BarCodeTests.cs +++ b/ZipConvertCustomerBarCode.Tests/BarCodeTests.cs @@ -93,6 +93,41 @@ public void 引数がnullなら例外() Assert.Throws(() => 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シンボルで開始コードと終了コードで挟まれる() { diff --git a/ZipConvertCustomerBarCode.Tests/DrawingRenderTests.cs b/ZipConvertCustomerBarCode.Tests/DrawingRenderTests.cs index 73d9444..46dfbd9 100644 --- a/ZipConvertCustomerBarCode.Tests/DrawingRenderTests.cs +++ b/ZipConvertCustomerBarCode.Tests/DrawingRenderTests.cs @@ -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); diff --git a/ZipConvertCustomerBarCode.Tests/PrintAssert.cs b/ZipConvertCustomerBarCode.Tests/PrintAssert.cs new file mode 100644 index 0000000..8eff0ea --- /dev/null +++ b/ZipConvertCustomerBarCode.Tests/PrintAssert.cs @@ -0,0 +1,41 @@ +namespace ZipConvertCustomerBarCode.Tests +{ + public static class PrintAssert + { + /// + /// 実寸で描画した画像に、バーが正しい位置・形で描かれていることを確認する + /// バーの中心が黒、バーの間とバーのない段が白であることを点で調べる + /// + /// カスタマバーコード + /// ピクセルが黒かどうか + /// 1mmあたりのピクセル数 + /// 描画位置(X、Y共通、mm) + public static void Bars(CustomerBarCode code, Func 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)); + } + } +} diff --git a/ZipConvertCustomerBarCode.Tests/PrintTests.cs b/ZipConvertCustomerBarCode.Tests/PrintTests.cs new file mode 100644 index 0000000..e941153 --- /dev/null +++ b/ZipConvertCustomerBarCode.Tests/PrintTests.cs @@ -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(() => code.GetPrintWidth(point)); + Assert.Throws(() => code.GetPrintHeight(point)); + Assert.Throws(() => code.GetPrintBars(point)); + Assert.Throws(() => 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); + Assert.EndsWith("", svg); + + var rects = Regex.Matches(svg, "").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(() => code.ToPrintSvg(10, -1)); + Assert.Throws(() => code.ToPrintSvg(10, double.NaN)); + } + } +} diff --git a/ZipConvertCustomerBarCode.Tests/SkiaRenderTests.cs b/ZipConvertCustomerBarCode.Tests/SkiaRenderTests.cs index 4c53770..3f2445c 100644 --- a/ZipConvertCustomerBarCode.Tests/SkiaRenderTests.cs +++ b/ZipConvertCustomerBarCode.Tests/SkiaRenderTests.cs @@ -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(() => code.Draw(canvas, 0, 0, 0)); + Assert.Throws(() => code.Draw(canvas, 0, 0, float.NaN)); + } + private static void AssertSamePixels(SKBitmap expected, SKBitmap actual) { Assert.Equal(expected.Width, actual.Width); diff --git a/ZipConvertCustomerBarCode/Bar.cs b/ZipConvertCustomerBarCode/Bar.cs index f5eb9e3..038f884 100644 --- a/ZipConvertCustomerBarCode/Bar.cs +++ b/ZipConvertCustomerBarCode/Bar.cs @@ -18,6 +18,44 @@ public enum BarType Timing } + /// + /// バー1本の配置(ミリメートル単位、印刷用) + /// + public readonly struct PrintBar + { + /// + /// バーを生成する + /// + /// バーの種類 + /// X座標(mm) + /// Y座標(mm) + /// 幅(mm) + /// 高さ(mm) + public PrintBar(BarType type, double x, double y, double width, double height) + { + Type = type; + X = x; + Y = y; + Width = width; + Height = height; + } + + /// バーの種類 + public BarType Type { get; } + + /// X座標(mm) + public double X { get; } + + /// Y座標(mm) + public double Y { get; } + + /// 幅(mm) + public double Width { get; } + + /// 高さ(mm) + public double Height { get; } + } + /// /// バー1本の配置(ピクセル単位) /// diff --git a/ZipConvertCustomerBarCode/BarCode.cs b/ZipConvertCustomerBarCode/BarCode.cs index 7aede81..6a6b33f 100644 --- a/ZipConvertCustomerBarCode/BarCode.cs +++ b/ZipConvertCustomerBarCode/BarCode.cs @@ -87,8 +87,16 @@ public static CustomerBarCode Create(string zipCode, string address) if (zipCode == null) throw new ArgumentNullException(nameof(zipCode)); if (address == null) throw new ArgumentNullException(nameof(address)); - var code = ConvertCustomerBarCode(address, CheckPostCode(zipCode)); - return new CustomerBarCode(code); + var zip = CheckPostCode(zipCode); + var addressNumber = ExtractAddressNumber(address); + var customerCode = ConvertCCode(addressNumber); + + // スタートコード + 郵便番号 + 住所表示番号 + チェックデジット + ストップコード + var symbols = new[] { startCode } + .Concat(zip.Select(x => x.ToString())) + .Concat(customerCode) + .Concat(new[] { CalcCheckDigit(zip.Select(x => x.ToString()), customerCode), endCode }); + return new CustomerBarCode(zip, addressNumber, symbols); } /// @@ -122,8 +130,8 @@ public static CustomerBarCode Create(string zipCode, string townAddress, string /// ハイフンを除いて数値7桁であること /// /// 郵便番号 - /// 郵便番号配列 - private static IEnumerable CheckPostCode(string zipCode) + /// 郵便番号(半角数字7桁) + private static string CheckPostCode(string zipCode) { // 全角→半角 zipCode = string.Join("", zipCode.Select(n => (HankakuDic.ContainsKey(n) ? HankakuDic[n] : n))); @@ -137,25 +145,19 @@ private static IEnumerable CheckPostCode(string zipCode) throw new ArgumentException("郵便番号は7桁の数値で入力してください。"); } - return zipCode.Select(x => x.ToString()); - + return zipCode; } /// - /// カスタマバーコード変換 + /// 住所表示番号の抜き出し /// /// 住所 - /// 郵便番号 - /// - private static IEnumerable ConvertCustomerBarCode(string address, IEnumerable zipCode) + /// 住所表示番号(13けたで切り捨てる前) + private static string ExtractAddressNumber(string address) { - yield return startCode; - // 全角→半角 address = string.Join("", address.Select(n => (HankakuDic.ContainsKey(n) ? HankakuDic[n] : n))); - foreach (var pc in zipCode) yield return pc; - // http://www.post.japanpost.jp/zipcode/zipmanual/p19.html // 1. まず、データ内にあるアルファベットの小文字は大文字に置き換えます。 // 実行環境のカルチャによらず変換する(tr-TR では i が İ になるため) @@ -181,14 +183,7 @@ private static IEnumerable ConvertCustomerBarCode(string address, IEnume address = Regex.Replace(address, "[^0-9A-Z\\-]|[A-Z]{2,}", "-"); // ハイフン処理 - address = TrimHyphen(address); - - var customerCode = ConvertCCode(address); - foreach (var cd in customerCode) yield return cd; - - yield return CalcCheckDigit(zipCode, customerCode); - - yield return endCode; + return TrimHyphen(address); } /// diff --git a/ZipConvertCustomerBarCode/CustomerBarCode.cs b/ZipConvertCustomerBarCode/CustomerBarCode.cs index 7d098e8..9e97bd7 100644 --- a/ZipConvertCustomerBarCode/CustomerBarCode.cs +++ b/ZipConvertCustomerBarCode/CustomerBarCode.cs @@ -21,6 +21,11 @@ public sealed class CustomerBarCode private const int BarWidth = 2; private const int BarPitch = 4; + // 印刷時の大きさ(ポイント)。マニュアルでは8~11.5ポイント相当を許す + private const double DefaultPoint = 10; + private const double MinPoint = 8; + private const double MaxPoint = 11.5; + // シンボル→バー配置辞書 // L:ロングバー、A:セミロングバー(上)、D:セミロングバー(下)、T:タイミングバー、空白:バーなし private static readonly Dictionary BarPatternDic = new Dictionary() @@ -32,12 +37,35 @@ public sealed class CustomerBarCode {"STC"," LD"},{"SPC","DL "} }; - internal CustomerBarCode(IEnumerable symbols) + internal CustomerBarCode(string zipCode, string addressNumber, IEnumerable symbols) { + ZipCode = zipCode; + AddressNumber = addressNumber; // 呼び出し側から書き換えられないよう、コピーを読み取り専用で保持する Symbols = new ReadOnlyCollection(symbols.ToArray()); } + /// + /// 郵便番号(半角数字7桁、ハイフンなし) + /// + public string ZipCode { get; } + + /// + /// 住所から抜き出した住所表示番号(マニュアルの「バーコード・データ」、例: 3-20-5B604) + /// 13けたで切り捨てる前の値で、バーコードに含まれるのは先頭13けた分 + /// + public string AddressNumber { get; } + + /// + /// 代表の郵便番号かどうか(下2けたが"00") + /// + /// + /// 代表の郵便番号(郵便番号簿の「以下に掲載がない場合」の番号)の郵便物には、カスタマバーコードを付けない。 + /// マニュアルでは代表の郵便番号は「原則として」下2けたが"00"とされているため、下2けたで判定している。 + /// http://www.post.japanpost.jp/zipcode/zipmanual/p18.html + /// + public bool IsRepresentativeZipCode => ZipCode.EndsWith("00", StringComparison.Ordinal); + /// /// シンボル列 /// 開始コード("STC")、郵便番号、住所表示番号、チェックデジット、終了コード("SPC")の順 @@ -102,6 +130,89 @@ public string ToSvg(int scale = 1) return sb.ToString(); } + /// + /// 印刷時のバーコードの幅(mm、余白を含まない) + /// + /// 大きさ(8~11.5ポイント) + public double GetPrintWidth(double point = DefaultPoint) + { + var bars = GetBars().ToList(); + var width = bars[bars.Count - 1].X + BarWidth - bars[0].X; + return width * MillimetersPerPixel(point); + } + + /// + /// 印刷時のバーコードの高さ(mm、余白を含まない) + /// + /// 大きさ(8~11.5ポイント) + public double GetPrintHeight(double point = DefaultPoint) + { + return CellHeight * MillimetersPerPixel(point); + } + + /// + /// 印刷時のバーの配置を返す(mm単位、左上の基準は最初のバーの左上) + /// + /// + /// 寸法は郵便番号・バーコードマニュアルに従う(10ポイントのときバー幅0.6mm、バーピッチ1.2mm、ロングバー3.6mm)。 + /// http://www.post.japanpost.jp/zipcode/zipmanual/p12.html + /// + /// 大きさ(8~11.5ポイント) + /// バー配置 + public IEnumerable GetPrintBars(double point = DefaultPoint) + { + var mm = MillimetersPerPixel(point); + var bars = GetBars().ToList(); + var offset = bars[0].X; + return bars.Select(bar => new PrintBar(bar.Type, (bar.X - offset) * mm, bar.Y * mm, bar.Width * mm, bar.Height * mm)); + } + + /// + /// 印刷用のSVG形式の文字列を返す(サイズはmm単位) + /// + /// 大きさ(8~11.5ポイント) + /// 上下左右の余白(mm)。マニュアルでは2mm以上の空白が必要 + /// SVG + public string ToPrintSvg(double point = DefaultPoint, double margin = 2) + { + if (double.IsNaN(margin) || margin < 0) throw new ArgumentOutOfRangeException(nameof(margin), "余白は0以上で指定してください。"); + + var width = GetPrintWidth(point) + margin * 2; + var height = GetPrintHeight(point) + margin * 2; + var sb = new StringBuilder(); + sb.AppendFormat(CultureInfo.InvariantCulture, + "", + Round(width), Round(height)); + sb.Append(""); + foreach (var bar in GetPrintBars(point)) + { + sb.AppendFormat(CultureInfo.InvariantCulture, + "", + Round(bar.X + margin), Round(bar.Y + margin), Round(bar.Width), Round(bar.Height)); + } + sb.Append(""); + return sb.ToString(); + } + + /// + /// 1ピクセル(scale=1)あたりのmm + /// + /// 大きさ(8~11.5ポイント) + private static double MillimetersPerPixel(double point) + { + if (double.IsNaN(point) || point < MinPoint || point > MaxPoint) + { + throw new ArgumentOutOfRangeException(nameof(point), "大きさは8~11.5ポイントで指定してください。"); + } + // バー幅(2ピクセル)が 0.6mm × ポイント / 10 + return 0.6 * point / 10 / BarWidth; + } + + /// + /// SVG出力用の丸め(浮動小数点の誤差を出力しないため) + /// + private static double Round(double value) => Math.Round(value, 4); + /// /// バー生成 ///