rksoftware

Visual Studio とか C# とかが好きです

C# 15 にそなえよう。

そろそろ、.NET Conf の日程も発表され、.NET 11 の GA も近づいてきました。そうなると、何が起こるでしょう?
そう、C# 15 の GA の季節です。
というわけで、C# 15 を復習してみます。

■ .NET Blog

今回 C# 15 を自分で動かして理解し、気になるところは動作確認をしてみました。
色々独自にやってみましたが、機能の基本ということであれば、.NET Blog の次の記事がまとまっているので、ぜひ参照してください。

■ 今回のコード

今回、試してみたコードは GitHub で公開しています。
良かったら、Zip で持っていったり、クローンしたりして皆さんも動かしてみてください。

■ Union types

ユニオン型
「これらの型のうちのどれかが入る変数」というものが作れます。

型定義

Union 型の定義の例です。
ku 型は、Chiyoda 型の値または Taito 型の値が入る変数として使えます。

union Ku(Chiyoda, Taito) { }
record Chiyoda { }
record Taito { }

コード例

Ku 型の変数には、Chiyoda 型または Taito 型を代入できます。
ちなみに、値を持たない Ku 型の変数も可能です (コード例の ku01 のように)。

Chiyoda chiyoda01 = new();
Taito taito01 = new();
Ku ku01 = new();
Ku ku02 = chiyoda01;
Ku ku03 = taito01;

Chiyoda 型でも Taito 型でもない型は代入できず、構文エラーです。

// エラー
Ku ku04 = new System.Drawing.Point(0,0); // これはできない 「 型 'System.Drawing.Point' を 'Ku' に暗黙的に変換できません 」

使用例

使うときはパターンマッチングで使うのが基本になりそうです。
特に、取り得る型が確定しているので、静的にパターンの不足をチェックしてくれるのがうれしいです。
次の例では Taito 警告がでます。

_ = ku02 switch // ここで警告 「 この switch 式では入力型の可能な値がすべて扱われるわけではありません (すべてが網羅されているわけではありません)。たとえば、パターン 'Taito' がカバーされていません。 」

{
    Chiyoda c => "Chiyoda",
};

網羅されると、警告がなくなります。

_ = ku02 switch // これは取り得る型が網羅されているので、警告が出ない
{
    Chiyoda c => "Chiyoda",
    Taito c => "Taito",
};

キャストの仕方

Union 型は代入した値の型にキャスト できません。
Ku 型に Chiyoda 型を代入できますが、キャストで Ku 型を Chiyoda 型にすることはできません。

// エラー
Chiyoda chiyoda02 = (Chiyoda)ku02;   // これはできない 「 型 'Ku' を 'Chiyoda' に変換できません 」

代入した値は Ku 型の Value プロパティに居るので、Value プロパティから取り出すことができます。取り出しものはキャストできます。

Chiyoda chiyoda02 = (Chiyoda)ku02.Value;

代入と Value プロパティ

Ku 型に Chiyoda 型を代入するソースコードを書けるが、実体としては Ku 型の Value プロパティに Chiyoda 型が入っています。
Ku 型の Type は KuKu 型の Value プロパティの中身の型が Chiyoda であることが確認できます。

Console.WriteLine(ku01.GetType().FullName);         // Ku
Console.WriteLine(ku02.GetType().FullName);         // Ku
Console.WriteLine(ku01.Value?.GetType().FullName);  //
Console.WriteLine(ku02.Value.GetType().FullName);   // Chiyoda
Console.WriteLine(chiyoda01.GetType().FullName);    // Chiyoda

パターンマッチングの場合のみ、Ku 型のみでなく、Value プロパティの中身とマッチするかも見てくれています。

Console.WriteLine(ku01.Value == null);      // True
Console.WriteLine(ku01 is null);            // True
Console.WriteLine(ku02.Value == chiyoda01); // True
Console.WriteLine(ku02 is Ku);              // True
Console.WriteLine(ku02 is Chiyoda);         // True

Union 型は ValueType かつ Ku 型は Chiyoda 型ではないので次のコードは静的にエラーです。

// これらはエラー
Console.WriteLine(ku01 == null);         // これはできない 「 演算子 '==' を 'Ku' と '<null>' 型のオペランドに適用することはできません 」
Console.WriteLine(ku02 == chiyoda01);    // これはできない 「 演算子 '==' を 'Ku' と 'Chiyoda' 型のオペランドに適用することはできません 」

Value は不変

Value プロパティは get のみのため set はできません。

// エラー
ku01.Value = chiyoda01;  // これはできない 「 プロパティまたはインデクサー 'Ku.Value' は読み取り専用であるため、割り当てることはできません 」

Value から値を取り出す

Value プロパティから値を取り出して処理するコードは、せっかくの Union 型の利点をつぶしてしまいます。
`Ku 型の変数へのパターンマッチングでは、あり得ない型 (次の例ではあり得ない Attribute 型) とマッチしようとしてエラーにしてくれるが、 Value から取り出してしまうと、エラーにしてはくれません。

// エラーにしてくれる。うれしい
_ =ku02 switch { Attribute c => "" }; // False       // これはできない 「 種類 'Ku' の式は、種類 'Attribute' のパターンで処理することができません。 」

二つ目のこの例はエラーにしてくれない。うれしくない。

Console.WriteLine(ku02.Value is Attribute); // False

■ Closed hierarchies

クローズド継承 同一アセンブリ内でのみ継承できる型をつくれます。

型定義のコード

クラスライブラリで次のように定義したとします。
closed キーワードが今回のキモです。 Ku 型を継承して、Chiyoda 型、Taito 型を定義できています。

//--
// クラスライブラリのアセンブリ
//--
namespace closed_hierarchies_2;

// closed クラスである Ku は同一アセンブリ内では継承できる
public closed class Ku { }
public class Chiyoda : Ku { }
public class Taito : Ku { }

また、アセンブリ外へ公開しない internal クラスが継承することもできます。

// internal クラスでの継承が可能
internal class Bunkyo : Ku { }

別アセンブリでの参照

先のクラスライブラリを別のプロジェクトで参照してみます。
次のように、Ku 型が見える使える状態です。

//--
// クラスライブラリを参照しているアセンブリ
//--
using closed_hierarchies_2;

// クラスライブラリで定義されている closed 型である Ku は見える
Console.WriteLine(typeof(Ku).FullName); // closed_hierarchies_2.Ku

しかし見えるのに継承ができません。

// エラー
class Chuo : Ku { } // これはできない 「 'Chuo': 別のアセンブリの閉じた型 'Ku' を基本型として使用することはできません。 」

網羅もされない

closed クラスが定義されたアセンブリ外で継承できないので、値の型のパターンも限定されますことになり、Union 型のような網羅チェックも期待してしまうかもしれません。
残念ながらできません。
Union 型の switch 式のように型の網羅は静的に警告してもらえません、

Ku ku = new Taito();
_ = ku switch  // Taito のパターンが書かれていないが、警告はしてもらえない
{
    Chiyoda c => "Chiyoda",
};

そもそも Ku 型は Bunkyo 型の値である可能性があるが、このアセンブリでは Bunkyo 型を扱えない

// エラー
_ = ku switch  // これはできない 「 'Bunkyo' はアクセスできない保護レベルになっています 」
{
    Bunkyo b => "Bunkyo",
};

■ Collection expression arguments

コンストラクタ引数付きコレクション式
コレクション式でコレクションを作成する場合に、以前はコレクション型のコンストラクタ引数を指定できなかったが、with() でコンストラクタ引数を指定できるようになりました。
次の例ではキャパシティを 256 を指定して初期化しています。

List<string> list01 = ["one", "twe"];
List<string> list02 = [with(capacity: 256), "one", "twe"];
Console.WriteLine(list01.Capacity);  // 2
Console.WriteLine(list02.Capacity);  // 256

独自型で確認してみる

コンストラクタ引数のある型を作って確認してみましょう。
次のように引数のあるコンストラクタと、引数のないコンストラクタを持つクラスを作ってみました。

// コンストラクタ引数のあるコレクション型の定義
class MyCollection<T> : List<T>
{
    public MyCollection(string message) => Console.WriteLine($"MyCollection created with message: {message}");
    public MyCollection() => Console.WriteLine($"MyCollection created without message");
    public new void Add(T item) => Console.WriteLine($"added:{item}");
}

一つ目は引数のあるコンストラスタが使用される例です。

MyCollection<string> values = [with("saitama!"), "one", "two", "three"];    // コンストラクタ引数あり。引数が与えられている
// MyCollection created with message: saitama!
// added: one
// added: two
// added: three

二つ目は引数のないコンストラスタが使用される例です。

MyCollection<string> values2 = ["one", "two", "three"];    // コンストラクタ引数なし。引数なしコンストラクタが使われている
// MyCollection created without message
// added: one
// added:two
// added:three

■ Extension indexers

新形式の拡張メソッドでのインデクサ拡張
新形式の拡張メソッドでのインデクサが定義できるようになりました。

コード例

次のように拡張インデクサを定義してみます。

//--
// 拡張メソッド定義
//--
public static class ListExtensions
{
    extension(Object o)
    {
        // 拡張メソッドでのインデクサ定義
        public string this[int index] =>
            index == 0 ? "zero" :
            index == 1 ? "one" :
            "";
    }
}

使用してみます。本来インデクサをもたない Object 型でインデクサが使えました。

Object o = new(); ;
Console.WriteLine($"at 0: {o[0]}"); // at 0: zero
Console.WriteLine($"at 1: {o[1]}"); // at 1: one

■ Labeled break and continue

ラベルへの break / continue
ラベルを使って多段のループを狙った階層まで一気に break や continue できます。 飼いならされた goto です。

コード例 (Break)

次の例は、break でいくつかのループを階層を飛び越えて break しています。
count が 1 の場合は break しないので 3 層目のループがもう一度回っています。
count が 2 の場合は、ラベル「la02」のループがブレイクするので 1 層目のループに飛んでいます。 count が 3 の場合は、ラベル「la01」のループがブレイクするので 1 層目のループから抜けループが終了します。

var count = 0;
la01: for (; ; )
{
    Console.WriteLine("la01");
    la02: for (; ; )
    {
        Console.WriteLine("l02");
        {
            for (; ; )
            {
                Console.WriteLine("la03");
                Console.WriteLine($"count: {++count}");
                if (count % 2 == 0) break la02;
                if (count % 3 == 0) break la01;
            }
        }
    }
}
// 実行結果:
// l01
// l02
// l03
// count: 1
// l03        ← count: 1 の場合は break しないので 3 層目のループがもう一度回る
// count: 2
// l01        ← count: 2 の場合は、ラベル「la02」のループがブレイクするので 1 層目のループに飛ぶ
// l02
// l03
// count: 3
//            ← count: 3 の場合は、ラベル「la01」のループがブレイクするので 1 層目のループから抜けループが終了する

コード例 (continue)

次の例は、continue でもいくつかのループを階層を飛び越えて continue しています。
count が 1 の場合は break も continue もしないので 3 層目のループがもう一度回っています。 count が 2 の場合は 2 層目 continue なので 2 層目のループがもう一度回っています。 count が 3 の場合は、ラベル「lv01」のループがブレイクするので 1 層目のループから抜けループが終了します。

count = 0;
lb01: for (; ; )
{
    Console.WriteLine("lb01");
    lb02: for (; ; )
    {
        Console.WriteLine("lb02");
        {
            for (; ; )
            {
                Console.WriteLine("lb03");
                Console.WriteLine($"count: {++count}");
                if (count % 2 == 0) continue lb02;
                if (count % 3 == 0) break lb01;
            }
        }
    }
}
// 実行結果:
// lb01
// lb02
// lb03
// count: 1
// lb03        ← count: 1 の場合は break も continue もしないので 3 層目のループがもう一度回る
// count: 2
// lb02        ← count: 2 の場合は 2 層目 continue なので 2 層目のループがもう一度回る
// lb03
// count: 3
//            ← count: 3 の場合は、ラベル「lv01」のループがブレイクするので 1 層目のループから抜けループが終了する

飼いならされた

goto のように自由奔放でなく、飼いならされているので、階層の中のでの break と continue しかできません。(うれしい)
次の例はエラーになります。(うれしい)

// エラー
// 無関係のラベルへは飛べない
for (; ; ) continue lb02;   // これはできない 「 ラベル 'lb02' を持つ外側ループが存在しないため、continue を実行できません 」

■ そなえよう

C# 15、本当にいいものですね。しっかり使用する準備を整え、.NET 11 の GA に備えましょう。

.NET / C# の画像編集アプリの出力画像がおかしい

自作のアプリに不具合があったという話です。

■ アプリ

github.com

GitHub にコードおいて作っているこのアプリです。作っている、というよりずいぶん前に軽く書いてずっと使っていた簡易スクリプトめいたプログラムです。

■ 何をするアプリ?

画像にグレーの枠を付けます。それだけです。
白い背景の画面をスクリーンショット取って、白い背景のブログページに載せると、画像の境界が見えずわかりにくい記事になることの対策アプリです。
画像自体に枠を付けてしまえばどこに行っても見やすいです。動画に文字入れするときに映像に埋もれないように縁取りをした文字を入れるのと同じです。

■ 何が起きていた? (観測)

プログラムが出力した画像が、サムネイルが正方形になります。また、フォトで開いた際にも一度正方形に表示された後、正しい画角の表示になる。
そんなことが起きていました。

■ 何が起きていた? (事象)

ファイル事態とファイル名の拡張子が崩れていました。
前述例の画像は、 .bmp ファイルを読み込んで拡張子が .bmp の画像ファイルを作成していたのですが、
ファイルの中身は .png でした。

ファイルと拡張子がずれていても動いてくれるんものなんですね。画像ふぁいるというのは。

■ コード

こんなコードでした。

var name = args.FirstOrDefault() ?? string.Empty;
if (string.IsNullOrWhiteSpace(name)) return;

if (System.IO.Directory.Exists(name))
    foreach (var file in System.IO.Directory.GetFiles(name))
        B(file);

if (System.IO.File.Exists(name))
    B(name);

static void B(string name)
{
    var extension = System.IO.Path.GetExtension(name);
    if (!new[] { ".jpg", ".bmp", ".png" }.Contains(extension)) return;

    var nameWithoutExtension = System.IO.Path.GetFileNameWithoutExtension(name);
    var directoryName = System.IO.Path.GetDirectoryName(name);
    var fileName = System.IO.Path.Combine(directoryName, $"_2_{nameWithoutExtension}{extension}");
    if (System.IO.File.Exists(fileName)) return;

    using var img = System.Drawing.Image.FromFile(name);
    using var bmp = new System.Drawing.Bitmap(img.Width + 2, img.Height + 2);
    bmp.SetResolution(img.HorizontalResolution, img.VerticalResolution);
    ;
    using var g = System.Drawing.Graphics.FromImage(bmp);
    g.FillRectangle(new System.Drawing.SolidBrush(System.Drawing.Color.FromArgb(255, 127, 127, 127)), 0, 0, bmp.Width, bmp.Height);
    g.DrawImage(img, 1, 1, img.Width, img.Height);

    bmp.Save(fileName);
}

■ ポイント

ここがダメです。ここで、出力画像ファイルの指定をしないと、出力は常に .png ファイルになるとのことです。にも書かかわらずファイル名は指定した fileName。拡張子も含めて。

    bmp.Save(fileName);

例えば、.bmp ファイルを読み込んだ場合、読み込んだファイルのファイル名を元に出力ファイルを作るので、拡張子 .bmp で出力していました。しかし中身は .png ファイルという状態。

■ なぜ昔は気が付かなかった?

ずっと .png ファイルでばかり作業していたからです。最近 .bmp や .jpg を扱うようになり要約気が付きました。

■ 改修

var name = args.FirstOrDefault() ?? string.Empty;
if (string.IsNullOrWhiteSpace(name)) return;

if (System.IO.Directory.Exists(name))
    foreach (var file in System.IO.Directory.GetFiles(name))
        B(file);

if (System.IO.File.Exists(name))
    B(name);

static void B(string name)
{
    var extension = System.IO.Path.GetExtension(name);
    if (!new[] { ".jpg", ".bmp", ".png" }.Contains(extension)) return;

    var nameWithoutExtension = System.IO.Path.GetFileNameWithoutExtension(name);
    var directoryName = System.IO.Path.GetDirectoryName(name);
    var fileName = System.IO.Path.Combine(directoryName, $"_2_{nameWithoutExtension}{extension}");
    if (System.IO.File.Exists(fileName)) return;

    using var img = System.Drawing.Image.FromFile(name);
    using var bmp = new System.Drawing.Bitmap(img.Width + 2, img.Height + 2);
    bmp.SetResolution(img.HorizontalResolution, img.VerticalResolution);
    ;
    using var g = System.Drawing.Graphics.FromImage(bmp);
    g.FillRectangle(new System.Drawing.SolidBrush(System.Drawing.Color.FromArgb(255, 127, 127, 127)), 0, 0, bmp.Width, bmp.Height);
    g.DrawImage(img, 1, 1, img.Width, img.Height);

    bmp.Save(fileName,
        extension switch
        {
            ".jpg" => System.Drawing.Imaging.ImageFormat.Jpeg,
            ".bmp" => System.Drawing.Imaging.ImageFormat.Bmp,
            ".png" => System.Drawing.Imaging.ImageFormat.Png,
        _   => throw new NotImplementedException()
        }
    );
}

■ ポイント

    bmp.Save(fileName,
        extension switch
        {
            ".jpg" => System.Drawing.Imaging.ImageFormat.Jpeg,
            ".bmp" => System.Drawing.Imaging.ImageFormat.Bmp,
            ".png" => System.Drawing.Imaging.ImageFormat.Png,
        _   => throw new NotImplementedException()
        }
    );

■ 解決

正しいファイルを出力するようになりました。

ブログ記事を pptx にする方法

はてなブログの記事を pptx にする方法をいろいろ模索しています。

rksoftware.hatenablog.com rksoftware.hatenablog.com

■ これまでやったこと

ブログ記事を Markdown。
Markdown を Marp で pptx にする。
そもそもブログ記事を Markdown で書いていたので、実は Markdown 化する必要なかった。

など。

■ 決定版

そんないろいろと試してきましたが、結論です。

ブログ記事内のセクションを、コピーして......

pptx ファイルに貼り付け!

あとは手で調整。これが一番早いです。

■ 試行錯誤

Copilot in PowerPoint で pptx 化。

出来上がったものは素晴らしいですが、時間がかかりすぎます。

rksoftware.hatenablog.com

ここまで時間がかかるなら、手で貼り付けるという選択肢もありです。 どうせ、見た目等手でいじることを考えたら、最初から全部手でやるのもありです。

Marp

これも素晴らしいものができます。しかし出来上がったファイルは、文字も含めすべてが画像になっていて、その画像が背景になっているだけのスライドです。


出した pptx は素晴らしいですがそのまま使えるはずはなく、手で一切編集できないのであれば選択肢になりませんでした。無念。

■ 結論

ブログ記事内のセクションを、コピーして pptx に貼り付けて手で編集する。
早さだけでなく、作りたい pptx ファイルを作れるという点でこれが結論になりました。

iPhone の集中モードが勝手に ON になったり OFF になったりする問題

iPhone の集中モードが勝手に ON になったり OFF になったりする問題に悩まされていました。

■ 解決

iPhone の集中モードの設定で「デバイス間で共有」を OFF にします。

■ 背景

iPhone の受ける通知は通常はまあ、全部受けておくのが良いです。

しかしその通知の大半はアプリで、別のスマートフォンでも同様の通知を受けるものです。

そして、複数所有しているスマートフォンのうちのどれを持ち歩いているかはその日次第です。

■ 課題

持ち歩いていないスマートフォンで通知を受けていると、自身がいない場で通知がなりつつけることになります。 本人不在の場でなり続ける通知はその場の人に与えるストレスはかなりのものです。

しかし、すべての通知を OFF にするのも良くない。アプリ通話など、緊急性のあるものは通知されるようにしておきたい。
機内モードなどではこの課題を解決できません。
そこで集中モードなのです。

■ 問題

iPhone の集中モードが勝手に ON になったり OFF になったりする問題に悩まされていました。
ある時実験すると、iPhone A で集中モードにすると、iPhone B も集中モードになり、iPhone B で集中モードを解除すると、iPhone A でも集中モードが解除される挙動が確認できました。

■ 何が起きていたか

驚くべきことに、複数の iPhone で集中モードの ON / OFF が同期するという信じられない挙動が確認されました。
だえがうれしい機能なのか、想像することが全くできませんが、実際そのように動作していました。

■ 解決

冒頭に書いているとおりです。

■ 使い\こなそう集中モード

iPhone は使うのが非常に難しい機械で、何をするにも難しいです。しかし便利な道具でもあることも確かです。使いこなしていきましょう。

Marp に css を適用する

ブログ記事を半自動で素早く pptx にする試みを続けています。

rksoftware.hatenablog.com

前回、プレーンな pptx にするところまでできたので、自分ブランドのスライドの色やフォントで pptx を作る試みを始めました。

■ 今できたもの

完全に壊れましたが、色など、自分ブランドの雰囲気は出ました。
今後、このまま進めていこうと思います。

■ 今回やったこと

自分の既存のスライドを画像で GitHub Copilot に貼り付け。
Marp 用の CSS にしてと依頼。
生まれた css を 現行の Markdown に組み込みを依頼。

たったこれだけです。これだけでここまで近づきました。

■ できたコード

こんな感じになっています。ここからは人の出番だと思うので、ちゃんと把握しながら作っていこうと思います。

---
marp: true
paginate: true
---

<style>
@charset "UTF-8";

:root {
    --cover-blue: #0d76bf;
    --cover-text: #ffffff;
    --cover-line: rgba(255, 255, 255, 0.9);
}

section {
    font-family: "Yu Gothic", "Hiragino Sans", "Meiryo", sans-serif;
}

section:first-of-type {
    background: var(--cover-blue);
    color: var(--cover-text);
    padding: 5.2rem 5.6rem 4.2rem;
    display: flex;
    flex-direction: column;
    justify-content: center;
    align-items: flex-start;
}

section:first-of-type h1 {
    margin: 0;
    max-width: 12ch;
    font-size: 4.45rem;
    line-height: 1.05;
    font-weight: 300;
    letter-spacing: 0.01em;
}

section:first-of-type h1::after {
    content: "";
    display: block;
    width: min(100%, 15.8em);
    height: 1px;
    margin-top: 1.35rem;
    background: var(--cover-line);
}

section:first-of-type p {
    margin: 1.4rem 0 0;
    font-size: 1.7rem;
    line-height: 1.25;
    letter-spacing: 0.02em;
}

section:first-of-type p strong {
    font-weight: 700;
}

section:first-of-type::after {
    display: none;
}

section:has(> h1):not(:first-of-type):not(:has(> :is(p, ul, ol, pre, blockquote, table, figure, img))) {
    background: var(--cover-blue);
    color: var(--cover-text);
    padding: 5.2rem 5.6rem 4.2rem;
    display: flex;
    flex-direction: column;
    justify-content: center;
    align-items: flex-start;
}

section:has(> h1):not(:first-of-type):not(:has(> :is(p, ul, ol, pre, blockquote, table, figure, img))) h1 {
    margin: 0;
    max-width: 16ch;
    font-size: 3.2rem;
    line-height: 1.16;
    font-weight: 300;
    letter-spacing: 0.01em;
}

section:has(> h1):not(:first-of-type):not(:has(> :is(p, ul, ol, pre, blockquote, table, figure, img))) h1::after {
    content: "";
    display: block;
    width: min(100%, 15.8em);
    height: 1px;
    margin-top: 1.25rem;
    background: var(--cover-line);
}

section:not(:first-of-type) {
    background: #ffffff;
    color: #444444;
    padding: 3.8rem 4.2rem 3rem;
    display: flex;
    flex-direction: column;
    font-family: "Yu Gothic", "Hiragino Sans", "Meiryo", sans-serif;
}

section:not(:first-of-type):has(> h1):has(> :is(p, ul, ol, pre, blockquote, table, figure, img)) h1 {
    margin: 0;
    color: #0d76bf;
    font-size: 2.7rem;
    line-height: 1.15;
    font-weight: 300;
    letter-spacing: 0.01em;
}

section:not(:first-of-type):has(> h1):has(> :is(p, ul, ol, pre, blockquote, table, figure, img)) h1::after {
    content: "";
    display: block;
    width: 100%;
    height: 1px;
    margin-top: 0.45rem;
    background: rgba(0, 0, 0, 0.28);
}

section:not(:first-of-type) > :is(p, ul, ol, pre, blockquote, table, figure) {
    background: #d9d9d9;
    margin: 1.35rem 0 0;
    padding: 0.9rem 1.1rem;
    box-sizing: border-box;
}

section:not(:first-of-type) > :is(ul, ol) {
    padding-left: 3rem;
    padding-top: 1rem;
    padding-bottom: 1rem;
    margin-left: 0;
}

section:not(:first-of-type) > ul {
    list-style: disc;
    list-style-position: outside;
}

section:not(:first-of-type) > ul li::marker,
section:not(:first-of-type) > ol li::marker {
    color: #1ea1dc;
}

section:not(:first-of-type) > :is(p, ul, ol, pre, blockquote, table, figure):first-of-type {
    margin-top: 1.7rem;
}

section:not(:first-of-type) strong {
    font-weight: 700;
}

section:not(:first-of-type) em {
    font-style: normal;
    color: #d60000;
    font-weight: 700;
}

section:not(:first-of-type) img {
    display: block;
    max-width: 100%;
    height: auto;
    background: transparent;
    padding: 0;
    margin: 0;
}
</style>

# ポスト AI Credits 時代の救世主? BYOK で C# コードを書いてみる

時は GitHub Copilot の AI Credits 時代。誰もが AI Credits を節約したいと、日々過酷な修行を行っていました。

---

# ■ Visual Studio Code の BYOK とは

はてなブログの記事を pptx にする

これまで、ブログ記事を pptx にする ~それも高速に~できるだけ手をかけず~ ことを模索してきました。
rksoftware.hatenablog.com rksoftware.hatenablog.com rksoftware.hatenablog.com

ある程度光明も見えていましたが、一つ重大なことを忘れていました。

■ 以前に書いたアプリ

実は、はてなブログの記事を別で使える Markdown にするために書いた C#/.NET アプリがありました。
github.com

これは、はてなブログの記事を Markdown で書いている場合の、記事ソースの Markdown 内の画像のタグを、http~に書き換えるアプリでした。
つまりこのアプリを使えば、Marp にインプットできる Markdown が得られます.Pandoc 要らなかった。

■ アプリ実行

記事のソースの Markdown をローカルファイルに保存。
記事の編集画面を Ctrl+A ⇒ Ctrl+C ⇒ Ctrl+V で。

そしてアプリ実行。引数はローカルファイルに保存したファイルです。

■ 実行結果

実行前

#### Azure で「リソースの追加」  
[f:id:rksoftware:20260705122625p:plain]

生まれたファイル

#### Azure で「リソースの追加」  
![image](https://cdn-ak.f.st-hatena.com/images/fotolife/r/rksoftware/20260705/20260705122625.png)

計画通り! 変換できています。

■実際の表示

画像がきちんと表示されています。
いいね!

■ ページ区切りを入れる

さて、Markdown を Marp に入れるためには、ページ区切りを手動で入れて小おかなければなりません。

そんな時は惜しみなく GitHub Copilot です。

プロンプト

この Markdown を Marp で pptx にしたいです。ページ区切りを入れてください


計画通り! 区切り線が入りました。

Marp

Marp で Markdown を pptx にします。詳細は、この記事トップのリンクから参照してください。

■ pptx

見事、ちゃんと pptx になりました。

あとは、pptx のテーマ的な css を作れば完璧ですね。 10秒程度で、ブログ記事を pptx にできるようになりました。