MsPaintLib – a .Net library for the new MS Paint .paint file format

Sometime in the last couple years, MS Paint started adding some new features that were interesting- among them was the ability to work with image layers. I had a nice laugh when I discovered that yes, you can add layers to your Paint project but as soon as you want to save the file, it required converting back to non-layer supporting formats and thus flattening your image. Hilarious.

So I was glad to see more recently that MS Paint added the ability to *actually save* files with layers, using a new .paint file type accessed by the new “save as a project” button. This now elevates the historically very basic Paint app to… a still basic app but now with full stack ordered, transparency/alpha channel layers.

Since Paint had never had a proprietary file format in the past, I was curious if they invented a whole new format for this project file type, and if it could be utilized by other applications- specifically something like Paint.Net. I initially assumed it was some form of renamed zip file archive of ordered png images under the hood, but some quick analyses showed this was not the case.

So I decided to dig deeper.

The end result is open source on GitHub: mspaintlib — a .NET library that reads and writes .paint, and also includes a Paint.NET v5 file-type plugin that uses the library to enable importing and exporting .paint files from/to Paint.Net. Files written by the library re-open in MS Paint with all layers intact. There’s also a full format spec in the repo. But first, the fun part.

“It’s just HEIF”

Checking the hex view of a sample .paint file showed:

00 00 00 20 66 74 79 70 6D 69 66 31 ...size = 0x20  type = "ftyp"   "mif1"

This turned out to not a proprietary format, but a ISOBMFF box. ISOBMFF (ISO/IEC 14496-12) is the base container format under MP4, MOV, HEIC, and basically everything Apple-or-camera-shaped. The mif1 brand is HEIF’s “image” profile. In other words: MS Paint’s new format is HEIF.

Specifically, it’s a small subset of HEIF:

LayerStandardWhat it gives you
ContainerISO/IEC 14496-12The box-tree on-disk structure
HEIFISO/IEC 23008-12Image items, derivations, references
MIAFISO/IEC 23000-22Application constraints (cameras, etc.)
Pixel storageISO/IEC 23001-17Uncompressed image items (unci)

The interesting part is that last row. ISO/IEC 23001-17 — “uncompressed video/images in ISOBMFF” — is what Paint uses for the pixel data. Not HEVC, not AVIF, not any compressed image codec. Each layer is a flat RGBA buffer with a generic compressor (DEFLATE) wrapped around it. That’s why you can write a working .paint reader without libheif or libheif-rs or any codec at all: there’s no codec to decode, just DeflateStream.

The box tree

Inside meta, MS Paint emits this hierarchy:

ftyp        major=mif1, compat=[mif1, gcmi, isoa, miaf]meta
  ├─ hdlr   handler = "pict"
  ├─ pitm   primary_item_id = <iovl item id>
  ├─ iinf   N item info entries (one per layer + one for the iovl)
  │    └─ infe v2  type=unci   for each layer
  │    └─ infe v2  type=iovl   for the composition
  ├─ iloc   v1 (32-bit offsets and lengths)
  ├─ iprp
  │    ├─ ipco   shared properties:
  │    │    ├─ cmpC      compression_algorithm = "defl"
  │    │    ├─ ispe      width, height
  │    │    ├─ colr nclx primaries=1 (sRGB), transfer=13, matrix=0
  │    │    ├─ cmpd      4 channels: 4=R, 5=G, 6=B, 7=Alpha
  │    │    ├─ uncC      profile="gene", pixel-interleaved, 8-bit
  │    │    └─ pixi      4 channels × 8 bits
  │    └─ ipma   per-item property associations
  ├─ iref
  │    └─ dimg  from iovl, to_items = [layer1, layer2, ...] in z-order
  └─ idat   inline data for the iovl item
free        padding (variable size)
mdat        layer pixel data (concatenated DEFLATE streams)

Each layer is a unci (uncompressed) image item. The composition — what defines “this is layer 1 below layer 2 below layer 3, all on a 333×271 canvas with a white background” — is an iovl (image overlay) item. It references each layer via an iref dimg (derived image) link.

The actual pixel bytes for each layer live in mdat, addressed by iloc extents. The 30-byte iovl payload — canvas size, fill color, per-layer offsets — lives inline in idat. That’s it. Walk the boxes, resolve the references, inflate the DEFLATE streams. You have a multi-layer image.

Two gotchas

You’d expect a bunch of “first try” mistakes when reverse-engineering an ISO standard from a single sample, and yes, there were some. Most of them were boring. Two were interesting.

defl ≠ zlib

The cmpC property declares the compression algorithm as a four-byte tag. MS Paint writes "defl". In .NET, the natural reach is for ZLibStream — zlib contains DEFLATE, after all. Wrong. "defl" per ISO/IEC 23001-17 means raw DEFLATE (RFC 1951), no zlib header. Use DeflateStream, not ZLibStream. They differ by a 2-byte header and a 4-byte Adler-32 checksum at the end. Try to inflate one with the other and you get “unexpected end of stream” or “invalid block type” with no hint that you’re using the wrong wrapper. Lost some time on this one.

The pixels are BGRA, not RGBA

This one cost more.

The cmpd property declares four “components” with channel types 4=R, 5=G, 6=B, 7=Alpha. In that order. So you’d reasonably assume each pixel is four bytes: R, G, B, A. I wrote a parser that decoded a layer to a 333×271 RGBA buffer, and then I noticed everything in the test image was unmistakably blue-tinted instead of red.

The trick is uncC, which sits next to cmpd in the property table. It declares each on-disk component slot as a component_index referring back to a cmpd entry. So the four bytes of a pixel are:

  • byte 0 → cmpd[uncC.component[0].component_index]
  • byte 1 → cmpd[uncC.component[1].component_index]
  • byte 2 → cmpd[uncC.component[2].component_index]
  • byte 3 → cmpd[uncC.component[3].component_index]

In MS Paint’s output, the indices are [2, 1, 0, 3]. Translation: byte 0 points to cmpd[2] which is B. Byte 1 → G. Byte 2 → R. Byte 3 → A. The pixels on disk are BGRA.

You can imagine why: BGRA matches the byte order of pretty much every Windows GDI surface, every Direct2D bitmap, every Paint.NET Surface. MS Paint is a Win32-shaped program; its in-memory format is BGRA; the serializer just dumps memory and lets cmpd+uncC describe the order. That’s a perfectly reasonable design. It’s also why a parser that reads the cmpd listing alone and assumes that’s the byte order will produce red-and-blue-swapped images for everyone using this format. The tell — “the cmpd channel registry is not the same thing as the on-disk byte order” — is the kind of thing you only learn by trying.

I updated the spec. The reader now builds a swizzle table from uncC.component_index → cmpd channel and converts to whatever output order it wants — RGBA in the public API. The writer goes the other way.

Writing one

Reading a format is half the job; if you can’t write it back, your library is read-only and your plugin doesn’t help when someone wants to create a .paint. So I built a writer that mirrors MS Paint’s exact box layout — same item IDs, same property assignments (the iovl item gets ispe and pixi; layer items get all six properties), same iref dimg structure, same mdat ordering.

The one mildly annoying bit is iloc. It tells the reader where each item’s bytes live in the file: mdat offset and length per layer. But you can’t write iloc until you know how big mdat is going to be — and you can’t know mdat‘s size until you’ve compressed all the layers. And you don’t know mdat‘s file offset until you know the size of the meta box that contains iloc. There’s a circular dependency.

The shape that resolves it cleanly:

  1. Compress all the layers in memory. Now you know all extent lengths.
  2. Write meta to a MemoryStream with placeholder zeros for the layer offsets, but record where in the buffer those zeros live.
  3. Now you know meta‘s size. Compute the absolute offset where mdat‘s data will start: ftyp_size + meta_size + free_size + mdat_header_size.
  4. Patch the recorded offset positions in the buffer.
  5. Emit ftyp → meta → free → mdat to the output stream.

A library that emits files MS Paint can read is one round-trip away from proving itself: load a real .paint, save it back out, open the result in MS Paint. It works. Cool!

The Paint.NET integration, and one philosophical question

The Paint.NET v5 plugin API is nicely built. You inherit from FileType, you set LoadExtensions and SaveExtensions in FileTypeOptions, you override OnLoad(Stream) and OnSave(...). The plugin builds against five Paint.NET assemblies (referenced via a PaintDotNetPath MSBuild property in Directory.Build.props, defaulting to C:\Program Files\paint.net\), and gets dropped into Paint.NET’s FileTypes\ folder.

The interesting design question shows up in the iovl payload. It includes a 4-channel canvas fill color– this is the background you see in MS Paint when a layer’s pixels are transparent. In MS Paint that fill is a document property: visible behind transparent layer pixels, but not editable as a layer. In other words- the “Background” layer in MS paint is not an actual layer, but a fake layer at the bottom of the stack that cannot contain drawing data- just a selectable solid color.

Paint.NET, and most other layer-based graphical apps, have no equivalent. Its canvas under the bottom layer is the familiar transparent checkerboard. There are a few options:

  1. Render the fill in Paint.NET by synthesizing an extra non-editable bottom layer. This matches MS Paint visually but adds something the user can’t really interact with. And on save, you’d have to detect “is the bottom layer a solid color?” to convert back to a canvas fill — a fragile heuristic.
  2. Synthesize an editable bottom layer. Matches visually, but the user could draw on it, and round-trip back to MS Paint would have to collapse it back to a fill (or lose the edits). Also fragile.
  3. Don’t render it; preserve the value somewhere safe so it survives a round-trip.

I went with option 3. On load, the plugin stashes the canvas fill color in Paint.NET’s Document.CustomHeaders (a free-form metadata string the user typically doesn’t see). On save, it reads it back. So:

  • MS Paint → Paint.NET: the background looks transparent in Paint.NET, but the color is preserved.
  • Paint.NET → MS Paint: write back, MS Paint shows the original background color again.

The full path — make a .paint in MS Paint, open it in Paint.NET, edit a layer, save it as .paint, open in MS Paint, see your edits with the right background — works.

I’m aware this may not be the preferred option to use, and I’m open to discussion on better ways to handle this.

What’s still open

The spec is written from a small number of samples. Three things remain unknown:

  • Layer names. Our samples have empty infe.item_name strings. MS Paint’s UI lets you rename layers, but I haven’t yet seen a sample with renamed layers to confirm where the name ends up. Could be in infe.item_name, in a udta user-data box, or in an extension property.
  • Per-layer visibility / opacity. Same story. The UI exposes them but I don’t know yet how they’re encoded.
  • Layers smaller than the canvas. Per spec, each unci item carries its own ispe (image spatial extent), so they could be smaller than the canvas. Our samples are all canvas-sized, so the writer currently rejects non-canvas-sized layers with a NotSupportedException. Easy to lift once needed.

If you have a sample exhibiting any of these, drop it as an issue on the repo. It’ll either confirm a guess or surface another interesting wrinkle.

Tools and links

The library is pure managed .NET 8 — no native dependencies, no libheif. The plugin targets net9.0-windows to match Paint.NET v5’s runtime.

If MS Paint changes its format later, the library will throw a PaintFormatException rather than silently misparse, and the spec will need a v0.3.

If you build something interesting on top of the library — an importer for GIMP or Krita, a CI step that flattens .paint to PNG, a server-side thumbnailer — I’d love to hear about it.

Big Arch- McDonald’s adds a 1000 calorie burger to the menu

McDonald’s Adds the Big Arch burger and meal to their menu- the burger itself is over 1000 calories and the meal deal is north of 1600 calories.

For reference, the Big Mac is about half the calorie count at 550 (pictured)
McDonald's Big Mac is "only" 550 calories

Let’s take that and DOUBLE it! (and add some sugary fries and soft drink to bump things up to over 1600).

I’m sure the logic behind this meal is to simplify your life- Instead of those pesky 3 meals a day, now you can just eat one meal a day and not have to worry about raiding the fridge so many times. 1600 calories is a great target intake zone for losing weight at a sustainable rate. Losing weight AND making life more efficient. That’s what we’re doing here, right? Right?

Behold:
Mcdonald's Big Arch meal will set you back over 1600 calories

Link to Mcdonald’s so you can go see this thing yourself (and start your weight loss program)-
https://www.mcdonalds.com/us/en-us/product/big-arch-meal.html

Searchhounds.com and the new domain parking model

Godaddy appears to be now be frequently redirecting parked domains to a site named searchhounds.com. For example:

searchhounds.com parked domain page

These pages present a “content page” along with a list of search phrases related to the content of the article on the page.

Many of you will remember the older parked domain pages that would host a similar layout with a simple list of search phrases which each link to an ad-filled search result page. These old pages were serving google ads under a specific agreement that domain registrars would often use, implicitly allowing google ads to be shown on a domain that is parked and by definition, has no other content on the page or site. This was a special agreement because google ads (via Adsense etc.) usually are not allowed to be run on sites or pages with little or no content.

In recent history, the original google ads on the parked domains because a source for a lot of lower quality traffic clicking through to the advertiser links. Google went through a few phases of trying to clean this up, but eventually decided to give advertisers the option to opt out of the parked domain placements, later followed by automatically opting all new ads from them, and then finally making *all* ads opt out. Advertisers now will have to specifically opt into the parked domain ads if they want. Since the traffic quality from these parked domains tended to be lower quality (for a variety of reasons), many or most advertisers did not opt back into them.

Along this same time, Google was testing and releasing a new product that worked as a sort of hybrid between a normal Adsense for content ads and the parked domain ads – in that the “list of searches” could now be placed inside a page with other content. This product was named Related Search On Content, often called RSOC for short.

This RSOC product was intended to be used for website with content, but many sites that were allowed to run RSOC built sites that would auto-generate short AI content so that numerous pages could be generated on many topics, quickly.

At the time, I joked that soon parked domain services would just start redirecting their domains to pages on an RSOC-enabled content website.

And that is exactly what GoDaddy is now doing with the SearchHounds.com site.

CURL on Windows

CURL on Windows can be confusing at times

MS Windows has the CURL app installed (since around 2018 at least).

If you run it in powershell, it may behave a little oddly. This is because powershell adds an alias to an internal powershell command called “Invoke-WebRequest”.

You can run curl on the normal commandline and it will use the traditional curl application instead of the his powershell mapped alias.

Or, in powershell you can specify “curl.exe” instead of just plain “curl”, and it will (should?) force powershell to use the curl app instead of the aliased Invoke-WebRequest version.

This page was reviewed by grok here- cURL on Windows

C# Ranges and their exclusionary upper bound

I’ve been moving to using more of the range based code and shorthand in my C# code recently- The ranges are pretty cool in how you can specify easily the lower..upper shorthand to specify a subset of an array or even letters in a string.

Recently I fully noticed something odd about the upper bound- if you perform this-

string s = "this_is_a_string";
string s2 = s[0..5];

One might assume this would pull the characters starting at index 0 (or “t”) and continue through the character at index 5 (“i”)- but instead the string will be:

"this_"

What?

So it turns out, the .Net team had to make a decision as to whether the upper bound of ranges would be inclusive or exclusive of the last element – as discussed here. The most obvious answer would be inclusive, since the letter at index 5 should be “i”… but they apparently weighted the pros and cons (and, there are quite a few things to consider), and decided on exclusive.

This results in a cleaner implementation for most use cases (as demonstated in the linked document above), but you definitely need to know this information when working with range notation. Feels like it needs a big flashing warning sign on it somewhere!

My last comment, I find it curious that a second range notation was being considered for start:length, instead of just start..end. This would be quite useful as well but we’ll have to see if it gets implemented in the future (unless it has been but I’m just overlooking it somehow).

facebookexternalhit/1.1

You’re seeing these in your user agent app logs and wondering what’s going on? Is this a link from facebook.com or some kind of sinister bot?

When someone posts a link to your page on facebook, you’ll notice a preview image of the page will often show up- so facebook is going out and loading your page url and scraping the content to figure out what’s on the page, and what should be shown for the preview… it also is just checking that the url is valid and not some bad/spammy/malicious content that should not be allowed. Since this is effectively a bot hitting your site, facebook sets the user agent to:

facebookexternalhit/1.1 (+http://www.facebook.com/externalhit_uatext.php)

Note that if your site is set to block bots, it may block this request and thus facebook won’t get a valid response to the request- so it may either disallow the post or it may allow the link but just not show any preview content for it.

Do you want a specific images to be shown on facebook when a link is posted? You can use OpenGraph headers to specify meta data to use for this, and set the og:image tag to point to the image you would prefer be used.

For more info, here are references for this content:

Meta Web Crawlers: https://developers.facebook.com/docs/sharing/webmasters/web-crawlers

Images in Link Shares: https://developers.facebook.com/docs/sharing/webmasters/images

External M.2 NVME SSD Enclosures and Heat and Failure and Heartache

Bought an nvme ssd external enclosure so I could bulk copy my data from my old laptop to my new one.

This is the one: https://amzn.to/3rgNAVq

Works great, but I learned the hard way that these will run warm, nay HOT, when you’re running them hard. And what is running a drive harder than copying nearly a TeeBee of data off of it in one chonk?

I didn’t realize what was happening the first attempt and things just seemed to lock up after robocopy had moved a whole bunch of data across. The next day I set up another batch copy and eventually it happened again- seemed to slow down, then started seeing file access errors (file is missing etc), and eventually just stopped. This was the point I noticed I could barely touch the aluminum case of the ssd enclosure.

After this second failed attempt, the drive came back online with ERRORS. Ugh. I pulled the cover off the gizmo and set up a fan to run across it, then corrected the errors and did yet another bulk copy operation. Except this time: Success.

I read more after about this and apparently these a. do run hot b. pc’s are sorta expected to have enough airflow to keep the ventilated but c. the inside of a pc case can also be too stifling for your ssd and so you need to double check that the internal fans are getting at least some flow over them. Or else you might cook that little sucker.

Seems like the little external enclosure products may want to rethink their designs and include at minimum some ventilation, but perhaps even consider a small fan.

#:~:text=

#:~:text= is appearing in urls to your site, and you see it in other urls as well.
You didn’t put it there, why is it showing up?

Google added this feature to Chrome browser so that the text on the page matching what is sent after the #:~:text= token gets highlighted on the page (and I think scrolled to as well- need to double check this.)

Then, google.com started adding this to links from serps that use snippets of text from your webpage. Thus, google shows the snippet of text from your page on their own page, and if a user clicks the link, will be taken directly to the spot on the webpage with that bit of text from the snippet, and highlighted in yellow.

(click this link and the text above will appear actually highlighted, using this #:~:text= portion appended to this page url).

On a related note, I just noticed that the page must be reloaded for the highlighter action to work- so if the link above began with the #(the anchor url indicator), it will not activate the highlighting on the page- but if you link the the actual url for the page and append this, it will highlight AFTER the page has fully loaded/reloaded.

I just noticed bing.com has added support for this as well- so I’m guessing the edge browser likely added this feature too.

I assume this little slug can be used for other things, like tracking in analytics to see that visitors are coming to your site from clicking a snippet.

Connect WordPress with Gmail without smtp

The biggest pain in setting up a wordpress site is getting it connected with an email account, to use as the sending account for system messages. This usually requires finding some obscure smtp settings from your email provider, adding credentials that will need to be updated again if you ever change your email, and various spam triggering actions that just make it all more painful than it should be.

I used Zapier recently to automate some notifcations that are sent out by email. Zapier has a Gmail connector that is as simple as logging into your gmail account inside their app, and then zapier has the access needed. No fiddling with a bunch of settings and hoping it works- it just seems to work, and pretty well.

I am looking for the equivalent of this simplicity for use with wordpress- a plugin that allows a simple connection to gmail using your gmail or google apps domain account login info, and then it just works.

This is a work in progress- I will add any solutions I find here. Please add your suggestions in the comments.

CS1929 C# ‘ILoggerFactory’ does not contain a definition for and the best extension method overload requires a receiver of type

This error pops up pretty frequently when upgrading your .net core project to 3.0 or 3.1.

The solution apparently is to replace the old code:

private ILoggerFactory ConfigureLogging(ILoggerFactory factory)
{
      factory.AddConsole();
      return factory;
}

With this new version:

private IServiceCollection ConfigureLogging(IServiceCollection factory)
{
      factory.AddLogging(opt =>
      {
          opt.AddConsole();
      })
      return factory;
}

ASP.Net MVC and MVC Core Error 500 after editing cshtml Razor page

I’ve had this pop up a few times over the years when editing razor synctax cshtml files. There is some perfectly legal c# code that even compiles fine in your razor files, but will fail when you try to run it- specifcally, when you do this in c#:

if(condition == true)
something = somethingelse;

Perfectly legal to do a single line of code following a conditional statement. However, when this is run inside a code block in a razor/cshtml file, it will fail! So you always have to enclose your code block in enclosing braces like so:
if(condition == true)
{
something = somethingelse;
}

I feel like this was overlooked in razor version 0.1 or something and was never corrected later. It would be nice if it at least failed during compile time.

System.Threading.Tasks.Task`1[Microsoft.AspNet.Mvc.Rendering.HtmlString] in place of Html.PartialAsync

System.Threading.Tasks.Task`1[Microsoft.AspNet.Mvc.Rendering.HtmlString] showed in my cshtml page where partial views were supposed to render. This was after upgrading to asp.net core 3.1 and going through the warnings that said Html.Partial should be replaced with Html.PartialAsync now to prevent deadlocks. Great, I’ll just go replace them all… blindly, because that’s how I roll.
This resulted in the System.Threading.Tasks.Task`1[Microsoft.AspNet.Mvc.Rendering.HtmlString] appearing in the page – what the.
So you actually need to add await to the code when changing to the PartialAsync – so your call would look like this now:

@async Html.PartialAsync()

instead of the old way:

@Html.Partial()

.Net Core 3.0 Released Today

Microsoft released the .Net Core 3.0 framework today. Along with a lot of other changes, the biggest news is support for desktop app development, by supporting winforms and WPF. Venturebeat has more info on the release: https://venturebeat.com/2019/09/23/microsoft-releases-net-core-3-0-with-support-for-wpf-and-windows-forms/

And Microsoft has the announcement on their developer blog here: https://devblogs.microsoft.com/dotnet/announcing-net-core-3-0/

Wordfence cannot delete files on Windows Server IIS

I’ve been running Wordfence on a number of wordpress sites I run on a windows server. Yes, you can run wordpress/php/mysql on windows. No, it’s not a great idea though. I’ve run into numerous issues with this setup and regret doing it, but it’s also been interesting to see the varying levels of support for running this configuration.

Wordfence will scan for infected files on my windows installs, and will find and list them- but when I try to delete the infected files, it always shows an error dialog stating “An invalid file was requested for deletion.” I initially thought this was a permissions issue, but after ruling this out I asked online about this error. I was surprised to not find a lot of others having the same issue, just a few. And the support forums didn’t do much to help either. I finally noticed that Wordfence lists all the officially supported operating systems, and Windows is *not* on that list. Woops.

Since php is not compiled, I decided to spend 10 minutes and see if I could find the source of this issue- and sure enough, I found that Wordfence was calculating a path incorrectly such that it was getting confused by the backslashes in windows file system. It seems to handle this fine in almost every area, but this one line was comparing two paths, and one had a forward slash where the other had a backslash- making them unequal and thus the “An invalid file was requested for deletion” is triggered.

I was able to hack the code a little to fix this, and now I have my windows wordpress wordfence working and deleting the files I request it to. You can update it yourself as well if you need this- find the file wp-content\plugins\wordfence\lib\wordfenceClass.php and update the following portion with these modifications, starting at line 4987:

$file = $issue['data']['file'];
$localFile = ABSPATH . DIRECTORY_SEPARATOR . $file;
$localFile = realpath($localFile);
$localPath = realpath(ABSPATH) . DIRECTORY_SEPARATOR;
if(strpos($localFile, $localPath) !== 0){
	return array('errorMsg' => __('An invalid file was requested for deletion.', 'wordfence'));
}

Note that I’m sure Wordfence is not a fan of having their php files edited, so this is fully unsupported and any updates to the plugin will likely overwrite this file and “break” it again.

Wordfence, feel free to implement this fix. Y’all are really close to working on a whole other operating system 🙂

IGSHID – the new(?) instagram click tracking ID

Apparently instagram has started adding a tracking click id named igshid that is similar in purpose to the facebook click id named fbclid- although this parameter seems to be used in links TO instagram instead of on links outbound from it as the facebook one is. I haven’t found any real info on this parameter yet, I’ll dig a bit more and update here.