diff --git a/CLAUDE.md b/CLAUDE.md
index 4cee5ab..44bc5db 100644
--- a/CLAUDE.md
+++ b/CLAUDE.md
@@ -6,24 +6,31 @@
- 日本向けのライブラリなので、ユーザーとのやり取り・ドキュメント(README、CLAUDE.md など)・コードコメント・XML ドキュメントコメント・コミットメッセージはすべて日本語で書く。
-## ビルド
+## 構成・ビルド
-- プロジェクトは旧形式(非 SDK スタイル)の csproj で、ターゲットは .NET Framework 4.0(`LibExecuter` は 4.0 Client Profile)。`dotnet build` ではビルドできないため、Visual Studio の MSBuild を使う:
- `"/c/Program Files/Microsoft Visual Studio/2022/Community/MSBuild/Current/Bin/MSBuild.exe" ZipConvertCustomerBarCode.sln -nologo -v:m`
-- 現在この環境では .NET Framework 4.0 の参照アセンブリ(Developer Pack)がないため、MSB3644 でビルドが失敗する。その場合はエラーを報告し、ユーザーの指示なしに `TargetFrameworkVersion` を変更しない。
-- コードは .NET Framework 4.0 の API の範囲で書く(`IReadOnlyList` や `Task.Run` などは使えない)。
+- ライブラリは 3 つ、すべて `netstandard2.0`(SDK スタイル)。ビルドは `dotnet build ZipConvertCustomerBarCode.sln`。
+ - `ZipConvertCustomerBarCode`: コア。外部依存を追加しない(描画ライブラリに依存させないのが分割の目的)。
+ - `ZipConvertCustomerBarCode.Drawing`: System.Drawing.Common による描画(Windows のみ)。
+ - `ZipConvertCustomerBarCode.SkiaSharp`: SkiaSharp による描画。名前空間は `SkiaSharp` との衝突を避けるため `ZipConvertCustomerBarCode.Skia`。
+- `netstandard2.0` の既定 C# は 7.3。`record` や null 許容参照型など新しい構文は使わない。
+- バージョン・著作権表示は `Directory.Build.props` にまとめている。
+- 公開メンバーには XML ドキュメントコメントを付ける(`GenerateDocumentationFile` 有効のため、ないと警告になる)。
## テスト
-- テストプロジェクトはない。`LibExecuter` はサンプル住所で `BarCode.CreateImage` を呼ぶだけのコンソールアプリで、結果は手動で確認する。
+- `dotnet test` で実行(xUnit v3、net10.0)。`global.json` で Microsoft.Testing.Platform を指定しているため、`Microsoft.NET.Test.Sdk` や `xunit.runner.visualstudio` は追加しない。
+- `TestData/cases.tsv` と `v1-*.png` は 1.x(.NET Framework 版)の出力で、シンボル列と画像がこれとピクセル単位で一致することを確認している。出力が変わる修正をした場合は、意図した変更か確認してから期待値を更新する。
+- `cases.tsv` はタブ区切り(郵便番号・住所・シンボル列・画像ファイル名)。住所が空の行があるので、シェルで加工するときは連続するタブがまとめられないよう注意する。
+- System.Drawing のテストは Windows 以外ではスキップされる。
## コード規約
-- `.cs` ファイルは UTF-8(BOM 付き)・CRLF 改行。どちらも維持する。
+- `.cs` ファイルは UTF-8(BOM 付き)・CRLF 改行。どちらも維持する(新規ファイルにも BOM を付ける)。
-## バーコード処理(`ZipConvertCustomerBarCode/BarCode.cs`)
+## バーコード処理
-- 住所の変換は日本郵便のバーコードマニュアル(http://www.post.japanpost.jp/zipcode/zipmanual/p19.html)に従う。コード中の番号付きコメント(1.〜6.)はマニュアルの手順番号に対応している。
-- バーコードは常に 23 シンボル: スタート + 郵便番号 7 桁 + 住所表示番号 13 枠(不足分は `CC4` で埋める)+ チェックデジット + ストップ。各シンボルは幅 12px の GIF なので画像サイズは 276×12。シンボル数を変える場合は `new Bitmap(276, 12)` も合わせて変更する。
-- シンボル画像は `ZipConvertCustomerBarCode/Resources/*.gif`。`ImageKeyDic` が各シンボルをリソース名(`Num0`、`Hyphen`、`CC1`、`Start`、`End` など)に対応付けている。リソース名はファイル名ではなく `Properties/Resources.resx` と `Resources.Designer.cs` で定義されているため、画像を追加するときは両方を更新する。
+- 住所の変換(`BarCode.cs`)は日本郵便のバーコードマニュアル(http://www.post.japanpost.jp/zipcode/zipmanual/p19.html)に従う。コード中の番号付きコメント(1.〜6.)はマニュアルの手順番号に対応している。
+- バーコードは常に 23 シンボル: スタート + 郵便番号 7 桁 + 住所表示番号 13 枠(不足分は `CC4` で埋める)+ チェックデジット + ストップ。
+- バーの形は `CustomerBarCode.cs` の `BarPatternDic` で定義(1 シンボル = 幅 12px に 3 本、x=0/4/8、幅 2px、上・中・下段 各 4px)。この表は旧バージョンの GIF 画像とピクセル単位で一致することを確認済みなので、変更するときは既存出力と比較する。
+- 描画パッケージは `GetBars()` の矩形を白地に黒で塗るだけにし、バーコードのロジックはコアに置く。
- 住所中の漢数字には未対応(README に記載の既知の制限)。
diff --git a/Directory.Build.props b/Directory.Build.props
new file mode 100644
index 0000000..fc534f5
--- /dev/null
+++ b/Directory.Build.props
@@ -0,0 +1,10 @@
+
+
+
+ 2.0.0
+ Toshiba
+ ZipConvertCustomerBarCode
+ Copyright © Toshiba 2015
+
+
+
diff --git a/LibExecuter/LibExecuter.csproj b/LibExecuter/LibExecuter.csproj
deleted file mode 100644
index 99f48e4..0000000
--- a/LibExecuter/LibExecuter.csproj
+++ /dev/null
@@ -1,64 +0,0 @@
-
-
-
- Debug
- x86
- 8.0.30703
- 2.0
- {73611104-9843-44A5-835D-F9BBA7D9001C}
- Exe
- Properties
- LibExecuter
- LibExecuter
- v4.0
- Client
- 512
-
-
- x86
- true
- full
- false
- bin\Debug\
- DEBUG;TRACE
- prompt
- 4
-
-
- x86
- pdbonly
- true
- bin\Release\
- TRACE
- prompt
- 4
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- {af6583cf-653d-42f8-a286-526d9a8ebf03}
- ZipConvertCustomerBarCode
-
-
-
-
-
\ No newline at end of file
diff --git a/LibExecuter/Program.cs b/LibExecuter/Program.cs
deleted file mode 100644
index f026d86..0000000
--- a/LibExecuter/Program.cs
+++ /dev/null
@@ -1,14 +0,0 @@
-using System.Drawing;
-using ZipConvertCustomerBarCode;
-
-namespace LibExecuter
-{
- class Program
- {
-
- static void Main(string[] args)
- {
- Bitmap img = BarCode.CreateImage("0140113", "秋田県大仙市堀見内 南田茂木 添60-1");
- }
- }
-}
diff --git a/LibExecuter/Properties/AssemblyInfo.cs b/LibExecuter/Properties/AssemblyInfo.cs
deleted file mode 100644
index 5d82075..0000000
--- a/LibExecuter/Properties/AssemblyInfo.cs
+++ /dev/null
@@ -1,36 +0,0 @@
-using System.Reflection;
-using System.Runtime.CompilerServices;
-using System.Runtime.InteropServices;
-
-// アセンブリに関する一般情報は以下の属性セットをとおして制御されます。
-// アセンブリに関連付けられている情報を変更するには、
-// これらの属性値を変更してください。
-[assembly: AssemblyTitle("LibExecuter")]
-[assembly: AssemblyDescription("")]
-[assembly: AssemblyConfiguration("")]
-[assembly: AssemblyCompany("Toshiba")]
-[assembly: AssemblyProduct("LibExecuter")]
-[assembly: AssemblyCopyright("Copyright © Toshiba 2015")]
-[assembly: AssemblyTrademark("")]
-[assembly: AssemblyCulture("")]
-
-// ComVisible を false に設定すると、その型はこのアセンブリ内で COM コンポーネントから
-// 参照不可能になります。COM からこのアセンブリ内の型にアクセスする場合は、
-// その型の ComVisible 属性を true に設定してください。
-[assembly: ComVisible(false)]
-
-// 次の GUID は、このプロジェクトが COM に公開される場合の、typelib の ID です
-[assembly: Guid("7764069b-6507-4368-959d-3e7c8ad0f084")]
-
-// アセンブリのバージョン情報は、以下の 4 つの値で構成されています:
-//
-// Major Version
-// Minor Version
-// Build Number
-// Revision
-//
-// すべての値を指定するか、下のように '*' を使ってビルドおよびリビジョン番号を
-// 既定値にすることができます:
-// [assembly: AssemblyVersion("1.0.*")]
-[assembly: AssemblyVersion("1.0.0.0")]
-[assembly: AssemblyFileVersion("1.0.0.0")]
diff --git a/README.md b/README.md
index e5e2e22..99d5ea5 100644
--- a/README.md
+++ b/README.md
@@ -7,20 +7,67 @@
[郵便局 - 郵便番号・バーコードマニュアル](http://www.post.japanpost.jp/zipcode/zipmanual/index.html)
-## 機能
-### Export
-指定したパスにカスタマバーコードを出力する。
+## 構成
+すべて .NET Standard 2.0 のライブラリです。描画方法に合わせてパッケージを選んでください。
+
+| プロジェクト | 依存 | 用途 |
+| --- | --- | --- |
+| `ZipConvertCustomerBarCode` | なし | バーコード生成、バー配置の取得、SVG 出力 |
+| `ZipConvertCustomerBarCode.Drawing` | System.Drawing.Common | `Bitmap` として描画(Windows のみ) |
+| `ZipConvertCustomerBarCode.SkiaSharp` | SkiaSharp | `SKBitmap` / PNG として描画(クロスプラットフォーム) |
+
+SkiaSharp を Linux で使う場合は、利用側で `SkiaSharp.NativeAssets.Linux` パッケージも参照してください。
+
+## 使い方
+### バーコード生成
```csharp
-// 郵便番号,住所,出力先
-BarCode.Export("01234567", "とある県とある市とある町 1丁目1の1",@"c:\test.gif");
+using ZipConvertCustomerBarCode;
+
+// 郵便番号,住所
+CustomerBarCode code = BarCode.Create("0140113", "秋田県大仙市堀見内 南田茂木 添60-1");
```
-### CreateImage
-カスタマバーコードのイメージを返す
+郵便番号が7桁の数値でない場合は `ArgumentException` になります。
+
+### SVG(依存ライブラリなし)
```csharp
-// 郵便番号,住所
-Bitmap img = BarCode.CreateImage("01234567", "とある県とある市とある町 1丁目1の1");
+string svg = code.ToSvg();
+```
+
+### System.Drawing
+```csharp
+using ZipConvertCustomerBarCode.Drawing;
+
+Bitmap img = code.ToBitmap();
+code.Save(@"c:\test.gif", ImageFormat.Gif);
```
+### SkiaSharp
+```csharp
+using ZipConvertCustomerBarCode.Skia;
+
+SKBitmap img = code.ToSKBitmap();
+byte[] png = code.ToPng();
+code.SavePng(@"c:\test.png");
+```
+
+### 独自に描画する
+`GetBars()` でバーの位置と大きさ(ピクセル単位)が取れるので、任意の描画ライブラリで白地に黒の矩形を描いてください。
+
+```csharp
+foreach (var bar in code.GetBars())
+{
+ // bar.X, bar.Y, bar.Width, bar.Height
+}
+```
+
+いずれのメソッドも `scale` 引数で拡大できます(1 のとき 276×12 ピクセル)。
+
+## 1.x からの移行
+| 1.x | 2.x |
+| --- | --- |
+| `BarCode.CreateImage(zip, address)` | `BarCode.Create(zip, address).ToBitmap()`(Drawing) |
+| `BarCode.Export(zip, address, path)` | `BarCode.Create(zip, address).Save(path, ImageFormat.Gif)`(Drawing) |
+
## 備考
* 漢数字未対応
* 住所→郵便番号変換辞書検討中(郵便番号の入力が不要に)
diff --git a/ZipConvertCustomerBarCode.Drawing/CustomerBarCodeDrawingExtensions.cs b/ZipConvertCustomerBarCode.Drawing/CustomerBarCodeDrawingExtensions.cs
new file mode 100644
index 0000000..91d7363
--- /dev/null
+++ b/ZipConvertCustomerBarCode.Drawing/CustomerBarCodeDrawingExtensions.cs
@@ -0,0 +1,49 @@
+using System;
+using System.Drawing;
+using System.Drawing.Imaging;
+
+namespace ZipConvertCustomerBarCode.Drawing
+{
+ ///
+ /// System.Drawing によるカスタマバーコードの描画
+ ///
+ public static class CustomerBarCodeDrawingExtensions
+ {
+ ///
+ /// カスタマバーコードのイメージを返す
+ ///
+ /// カスタマバーコード
+ /// 拡大率(1で1シンボル12ピクセル)
+ /// バーコードイメージ
+ public static Bitmap ToBitmap(this CustomerBarCode code, int scale = 1)
+ {
+ if (code == null) throw new ArgumentNullException(nameof(code));
+
+ var img = new Bitmap(code.GetWidth(scale), code.GetHeight(scale));
+ using (var gr = Graphics.FromImage(img))
+ {
+ gr.Clear(Color.White);
+ foreach (var bar in code.GetBars(scale))
+ {
+ gr.FillRectangle(Brushes.Black, bar.X, bar.Y, bar.Width, bar.Height);
+ }
+ }
+ return img;
+ }
+
+ ///
+ /// 指定したパスにカスタマバーコードを出力する
+ ///
+ /// カスタマバーコード
+ /// 出力先
+ /// 画像形式
+ /// 拡大率(1で1シンボル12ピクセル)
+ public static void Save(this CustomerBarCode code, string outputPath, ImageFormat format, int scale = 1)
+ {
+ using (var img = code.ToBitmap(scale))
+ {
+ img.Save(outputPath, format);
+ }
+ }
+ }
+}
diff --git a/ZipConvertCustomerBarCode.Drawing/ZipConvertCustomerBarCode.Drawing.csproj b/ZipConvertCustomerBarCode.Drawing/ZipConvertCustomerBarCode.Drawing.csproj
new file mode 100644
index 0000000..7a9e4fb
--- /dev/null
+++ b/ZipConvertCustomerBarCode.Drawing/ZipConvertCustomerBarCode.Drawing.csproj
@@ -0,0 +1,17 @@
+
+
+
+ netstandard2.0
+ カスタマバーコードを System.Drawing の Bitmap として描画する(Windows のみ)
+ true
+
+
+
+
+
+
+
+
+
+
+
diff --git a/ZipConvertCustomerBarCode.SkiaSharp/CustomerBarCodeSkiaExtensions.cs b/ZipConvertCustomerBarCode.SkiaSharp/CustomerBarCodeSkiaExtensions.cs
new file mode 100644
index 0000000..0dc7371
--- /dev/null
+++ b/ZipConvertCustomerBarCode.SkiaSharp/CustomerBarCodeSkiaExtensions.cs
@@ -0,0 +1,61 @@
+using System;
+using System.IO;
+using SkiaSharp;
+
+namespace ZipConvertCustomerBarCode.Skia
+{
+ ///
+ /// SkiaSharp によるカスタマバーコードの描画
+ ///
+ public static class CustomerBarCodeSkiaExtensions
+ {
+ ///
+ /// カスタマバーコードのイメージを返す
+ ///
+ /// カスタマバーコード
+ /// 拡大率(1で1シンボル12ピクセル)
+ /// バーコードイメージ
+ public static SKBitmap ToSKBitmap(this CustomerBarCode code, int scale = 1)
+ {
+ if (code == null) throw new ArgumentNullException(nameof(code));
+
+ var img = new SKBitmap(code.GetWidth(scale), code.GetHeight(scale));
+ using (var canvas = new SKCanvas(img))
+ using (var paint = new SKPaint { Color = SKColors.Black, IsAntialias = false })
+ {
+ canvas.Clear(SKColors.White);
+ foreach (var bar in code.GetBars(scale))
+ {
+ canvas.DrawRect(SKRect.Create(bar.X, bar.Y, bar.Width, bar.Height), paint);
+ }
+ }
+ return img;
+ }
+
+ ///
+ /// カスタマバーコードをPNG形式のバイト列で返す
+ ///
+ /// カスタマバーコード
+ /// 拡大率(1で1シンボル12ピクセル)
+ /// PNGデータ
+ public static byte[] ToPng(this CustomerBarCode code, int scale = 1)
+ {
+ using (var img = code.ToSKBitmap(scale))
+ using (var data = img.Encode(SKEncodedImageFormat.Png, 100))
+ {
+ return data.ToArray();
+ }
+ }
+
+ ///
+ /// 指定したパスにカスタマバーコードをPNG形式で出力する
+ ///
+ /// カスタマバーコード
+ /// 出力先
+ /// 拡大率(1で1シンボル12ピクセル)
+ public static void SavePng(this CustomerBarCode code, string outputPath, int scale = 1)
+ {
+ File.WriteAllBytes(outputPath, code.ToPng(scale));
+ }
+ }
+}
diff --git a/ZipConvertCustomerBarCode.SkiaSharp/ZipConvertCustomerBarCode.SkiaSharp.csproj b/ZipConvertCustomerBarCode.SkiaSharp/ZipConvertCustomerBarCode.SkiaSharp.csproj
new file mode 100644
index 0000000..49cb198
--- /dev/null
+++ b/ZipConvertCustomerBarCode.SkiaSharp/ZipConvertCustomerBarCode.SkiaSharp.csproj
@@ -0,0 +1,18 @@
+
+
+
+ netstandard2.0
+ ZipConvertCustomerBarCode.Skia
+ カスタマバーコードを SkiaSharp で描画する(クロスプラットフォーム)
+ true
+
+
+
+
+
+
+
+
+
+
+
diff --git a/ZipConvertCustomerBarCode.Tests/BarCodeTests.cs b/ZipConvertCustomerBarCode.Tests/BarCodeTests.cs
new file mode 100644
index 0000000..8c456b8
--- /dev/null
+++ b/ZipConvertCustomerBarCode.Tests/BarCodeTests.cs
@@ -0,0 +1,54 @@
+namespace ZipConvertCustomerBarCode.Tests
+{
+ public class BarCodeTests
+ {
+ [Theory]
+ [MemberData(nameof(TestCases.All), MemberType = typeof(TestCases))]
+ public void シンボル列が1x版と一致する(string zipCode, string address, string expected, string _)
+ {
+ var code = BarCode.Create(zipCode, address);
+
+ Assert.Equal(expected.Split(' '), code.Symbols);
+ }
+
+ [Theory]
+ [InlineData("0140113")]
+ [InlineData("014-0113")]
+ [InlineData("0140113")]
+ [InlineData("014‐0113")]
+ public void 郵便番号は全角とハイフンを受け付ける(string zipCode)
+ {
+ var code = BarCode.Create(zipCode, "添60-1");
+
+ Assert.Equal(new[] { "0", "1", "4", "0", "1", "1", "3" }, code.Symbols.Skip(1).Take(7));
+ }
+
+ [Theory]
+ [InlineData("")]
+ [InlineData("014011")]
+ [InlineData("01401130")]
+ [InlineData("014011A")]
+ [InlineData("014 0113")]
+ public void 郵便番号が7桁の数値でなければ例外(string zipCode)
+ {
+ Assert.Throws(() => BarCode.Create(zipCode, "添60-1"));
+ }
+
+ [Fact]
+ public void 引数がnullなら例外()
+ {
+ Assert.Throws(() => BarCode.Create(null!, "添60-1"));
+ Assert.Throws(() => BarCode.Create("0140113", null!));
+ }
+
+ [Fact]
+ public void 常に23シンボルで開始コードと終了コードで挟まれる()
+ {
+ var code = BarCode.Create("0140113", "");
+
+ Assert.Equal(23, code.Symbols.Count);
+ Assert.Equal("STC", code.Symbols[0]);
+ Assert.Equal("SPC", code.Symbols[22]);
+ }
+ }
+}
diff --git a/ZipConvertCustomerBarCode.Tests/CustomerBarCodeTests.cs b/ZipConvertCustomerBarCode.Tests/CustomerBarCodeTests.cs
new file mode 100644
index 0000000..adb1d68
--- /dev/null
+++ b/ZipConvertCustomerBarCode.Tests/CustomerBarCodeTests.cs
@@ -0,0 +1,92 @@
+using System.Text.RegularExpressions;
+
+namespace ZipConvertCustomerBarCode.Tests
+{
+ public class CustomerBarCodeTests
+ {
+ private readonly CustomerBarCode code = BarCode.Create("0140113", "秋田県大仙市堀見内 南田茂木 添60-1");
+
+ [Theory]
+ [InlineData(1, 276, 12)]
+ [InlineData(3, 828, 36)]
+ public void 画像サイズは拡大率に比例する(int scale, int width, int height)
+ {
+ Assert.Equal(width, code.GetWidth(scale));
+ Assert.Equal(height, code.GetHeight(scale));
+ }
+
+ [Fact]
+ public void 拡大率が1未満なら例外()
+ {
+ Assert.Throws(() => code.GetWidth(0));
+ Assert.Throws(() => code.GetHeight(0));
+ Assert.Throws(() => code.GetBars(0));
+ Assert.Throws(() => code.ToSvg(0));
+ }
+
+ [Fact]
+ public void バーは画像の範囲内に収まる()
+ {
+ foreach (var bar in code.GetBars())
+ {
+ Assert.InRange(bar.X, 0, code.GetWidth() - bar.Width);
+ Assert.InRange(bar.Y, 0, code.GetHeight() - bar.Height);
+ }
+ }
+
+ [Fact]
+ public void バー本数は開始終了コード各2本と他のシンボル各3本()
+ {
+ Assert.Equal(2 + 21 * 3 + 2, code.GetBars().Count());
+ }
+
+ [Fact]
+ public void 拡大するとバーの座標と大きさも拡大される()
+ {
+ var bars1 = code.GetBars(1).ToList();
+ var bars2 = code.GetBars(2).ToList();
+
+ Assert.Equal(bars1.Count, bars2.Count);
+ for (int i = 0; i < bars1.Count; i++)
+ {
+ Assert.Equal(bars1[i].Type, bars2[i].Type);
+ Assert.Equal(bars1[i].X * 2, bars2[i].X);
+ Assert.Equal(bars1[i].Y * 2, bars2[i].Y);
+ Assert.Equal(bars1[i].Width * 2, bars2[i].Width);
+ Assert.Equal(bars1[i].Height * 2, bars2[i].Height);
+ }
+ }
+
+ [Theory]
+ [InlineData(BarType.Long, 0, 12)]
+ [InlineData(BarType.Ascender, 0, 8)]
+ [InlineData(BarType.Descender, 4, 8)]
+ [InlineData(BarType.Timing, 4, 4)]
+ public void バー種別ごとの位置と高さ(BarType type, int y, int height)
+ {
+ var bars = code.GetBars().Where(x => x.Type == type).ToList();
+
+ Assert.NotEmpty(bars);
+ Assert.All(bars, bar =>
+ {
+ Assert.Equal(y, bar.Y);
+ Assert.Equal(height, bar.Height);
+ Assert.Equal(2, bar.Width);
+ });
+ }
+
+ [Fact]
+ public void SVGにはサイズと全てのバーが含まれる()
+ {
+ var svg = code.ToSvg(2);
+
+ Assert.StartsWith("