ステップ記録ツールから出力されたファイルから画像をまとめて抽出したい

2021-03-15

けど画像アップロードしたくないので抽出のためのコードを書いてみた。

ステップ記録ツール便利そうだなー使えそうだなーとホクホクしてたら、まさかの出力形式がmhtmlのみという罠。拡張子.mhtなんてわたし初めて見ました…。そんな普及してない形式使わないでほしい。知らないだけで一般的なんでしょうか?

ステップ記録ツール便利そうだなー使えそうだなーとホクホクしてたら、まさかの出力形式がmhtmlのみという罠。拡張子.mhtなんてわたし初めて見ました…。そんな普及してない形式使わないでほしい。知らないだけで一般的なんでしょうか?

せっかく綺麗に記録しても手順書として使うためにいちいち画像保存するのもめんどくさい…というわけで自分で.mht→.jpgするだけのプログラム書いてみました。

きっかけ

  • 手順書作るのに持って来いのツールじゃん!とばんばんステップ記録ツール動かしてたけど、出力形式が独特すぎて直ぐにブログにアップロードできなかった。
  • オンラインに変換ツールあったけど画質が粗い&ファイルダウンロードするのも結構不安
  • 会社とかファイルアップロードするの憚られる環境ではそもそもオンライン変換ツールも使えないし
  • 画像1枚1枚丁寧に名前を付けて保存していく?いやめんどくさいわー

というわけで

まずはステップ記録ツールで出力されたmhtmlファイルをサクラエディタで読んでみました。

--=_NextPart_SMP_XXXXX(なんか独自番号ぽいので伏せ)
Content-Type: image/jpeg
Content-Transfer-Encoding: base64
Content-Location: screenshot0001.JPEG

/9j/4AAQSkZJRgABAQEAeAB4AAD/2wBDABkRExYTEBkWFBYcGxkeJT4pJSIiJUw3Oi0+WlBfXllQ
V1ZkcJB6ZGqIbFZXfap+iJSZoaKhYXiwva+cu5CeoZr/2wBDARscHCUhJUkpKUmaZ1dnmpqampqa
mpqampqampqampqampqampqampqampqampqampqampqampqampqampqampr/wAARCAQ4B4ADASIA
AhEBAxEB/8QAHwAAAQUBAQEBAQEAAAAAAAAAAAECAwQFBgcICQoL/8QAtRAAAgEDAwIEAwUFBAQA
AAF9AQIDAAQRBRIhMUEGE1FhByJxFDKBkaEII0KxwRVS0fAkM2JyggkKFhcYGRolJicoKSo0NTY3
ODk6Q0RFRkdISUpTVFVWV1hZWmNkZWZnaGlqc3R1dnd4eXqDhIWGh4iJipKTlJWWl5iZmqKjpKWm
p6ipqrKztLW2t7i5usLDxMXGx8jJytLT1NXW19jZ2uHi4+Tl5ufo6erx8vP09fb3+Pn6/8QAHwEA
AwEBAQEBAQEBAQAAAAAAAAECAwQFBgcICQoL/8QAtREAAgECBAQDBAcFBAQAAQJ3AAECAxEEBSEx
BhJBUQdhcRMiMoEIFEKRobHBCSMzUvAVYnLRChYkNOEl8RcYGRomJygpKjU2Nzg5OkNERUZHSElK
U1RVVldYWVpjZGVmZ2hpanN0dXZ3eHl6goOEhYaHiImKkpOUlZaXmJmaoqOkpaanqKmqsrO0tba3
(以下略)

jpegデータは1枚1枚同じようなヘッダー情報がついていることを確認。しかもbase64てわざわざ書いてあるので純粋にbase64から変換すればそれっぽくなるはず。
これをもとに抽出して変換かけていきます。

コード

using System;
using System.Collections.Generic;
using System.Drawing;
using System.IO;
using System.Linq;
using System.Text;

namespace ExtractingJpegFromMht
{
    class Program
    {
        static void Main(string[] path)
        {
            if (path.Length!=1) {
                Console.WriteLine("引数にはmhtmlファイルをフルパスで指定してください");
                Console.ReadLine();
                return;
            }

            List<List<string>> jpegBynaryList = new List<List<string>>();
            using (var stream = new StreamReader(path[0],
                new UTF8Encoding())) {
                List<List<string>> mhtRecordAllList = new List<List<string>>();
                int count = 0;
                mhtRecordAllList.Add(new List<string>());
                while (!stream.EndOfStream) {
                    string record = stream.ReadLine();
                    // 指定の文字列を含んでいた場合、次のListへ格納
                    if (record.Contains("--=_NextPart_SMP_"))
                    {
                        count++;
                        mhtRecordAllList.Add(new List<string>());
                    }
                    mhtRecordAllList[count].Add(record);
                }
                // jpegのデータ部分のみ抽出
                jpegBynaryList=mhtRecordAllList.FindAll(x => x.Contains("Content-Type: image/jpeg"));
            }

            // Base32変換されかつ複数行のjpegのバイナリデータをデコードし、1つのリストに格納する
            var query = jpegBynaryList.Select(x=>{
                List<byte> jpegByteList = new List<byte>();
                // 毎5行目以降全てバイナリデータ
                for (var j = 5; j < x.Count - 5; j++)
                {
                    jpegByteList.AddRange(Convert.FromBase64String(x[j]));
                }
                // 毎4行目にファイル名データが書かれている
                string fileName= x[3].Split(' ')[1];
                return (fileName, jpegByteList); 
            });

            ImageConverter imageConverter = new ImageConverter();

            // ファイル出力
            // 特に何も指定していないのでモジュールと同じフォルダに作成される
            foreach (var (fileName, jpegByteList) in query) {
                using (Image image = (Image)imageConverter.ConvertFrom(jpegByteList.ToArray())) {
                    image.Save(fileName, System.Drawing.Imaging.ImageFormat.Jpeg);
                }
            }
        }
    }
}

どどーん

これでとりあえずローカルでは動きましたとさ。ちゃんとテストしたわけじゃないのであしからず。他のmhtmlファイルも同じ形式だったら動く気がします。

よーし、ステップ記録ツールたくさん動かすぞーーー。