I'm a big fan of the Typora markdown editor, as it has a nice WYSIWYG experience. They also support custom image uploads to a storage backend of your choice.
This sample here is a small uploader CLI for Azure bblob storage. Essentially, you need to set a TYPORA_IMAGE_UPLOAD_AZURE_CONNECTION environment variable, and ensure you have a container named typoraimages.
namespace TyporaUploaderAzure
using System;
using System.IO;
using System.Linq;
using System.Security.Cryptography;
using System.Threading.Tasks;
using Azure.Storage.Blobs.Models;
using SimpleBase;
class Program
// https://support.typora.io/Upload-Image/
static async Task Main(string[] args)
var connectionString = Environment.GetEnvironmentVariable("TYPORA_IMAGE_UPLOAD_AZURE_CONNECTION");
var serviceClient = new Azure.Storage.Blobs.BlobServiceClient(connectionString: connectionString);
var containerClient = serviceClient.GetBlobContainerClient(blobContainerName: "typoraimages");
// await containerClient.CreateIfNotExistsAsync(Azure.Storage.Blobs.Models.PublicAccessType.Blob);
var prefix = DateTime.Now.ToString("yyyy/MM/dd/HH/mm");
var tasks = args.Select(async filename =>
var fi = new FileInfo(filename);
var bytes = await File.ReadAllBytesAsync(path: fi.FullName);
using var hashAlgo = MD5.Create();
using var reader = fi.OpenRead();
var hash = hashAlgo.ComputeHash(bytes);
var hashBase32 = Base32.Crockford.Encode(hash);
var fileWithoutExtension = fi.Name.Substring(0, fi.Name.Length - fi.Extension.Length);
var blobName = $"{prefix}/{fileWithoutExtension}----{hashBase32}{fi.Extension}";
var blobClient = containerClient.GetBlobClient(blobName);
string mimeType(string extension) => extension switch
".png" => "image/png",
".jpeg" => "image/jpeg",
".jpg" => "image/jpeg",
_ => "application/octet-stream",
if (!await blobClient.ExistsAsync())
using var ms = new MemoryStream(bytes);
await blobClient.UploadAsync(ms);
var headers = new BlobHttpHeaders
ContentType = mimeType(fi.Extension),
ContentHash = hash,
CacheControl = "max-age=31536000",
await blobClient.SetHttpHeadersAsync(headers);
return blobClient.Uri.AbsoluteUri;
await Task.WhenAll(tasks);
.Select(t => t.Result)