Repository navigation
feat(image): read and write Paint.NET .pdn files, keeping layers with PSD - #1384
Conversation
Paint.NET's own format is read by Filee itself: the document (layer names, visibility, opacity, blend mode) is .NET binary serialization, decoded as plain data with System.Formats.Nrbf (no type is ever instantiated), then each layer's BGRA pixels come in gzip-compressed chunks. - PNG, JPG, WebP, PDF and every other picture target get the visible layers merged with Paint.NET's own blend formulas (all 14 modes, layer opacity), so the picture looks as it does in Paint.NET. - PSD/PSB keeps the layers for Photoshop, GIMP and Krita: names (also as Unicode "luni"), opacity, visibility and the blend modes Photoshop shares; the merged picture comes first. ImageMagick's PSD writer puts visibility in the wrong flag bit and locks transparency, so the flags are set afterwards. A preset that changes the picture gives one merged picture instead. - .pdn is read only, graded specialist in Design & print like .kra. Settings schema 79 teaches the images donut .pdn; custom claims stay put. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BPxh5s9u8aZQzKRbXnqj8o
Filee now writes PDN too (PaintNetWriter), in the shape Paint.NET 4.2 saves and every later version opens: the same NRBF records and class metadata, the 256 KB gzip chunks and a thumbnail in the header. - Any picture becomes a .pdn of one layer named Background. - A PSD/PSB keeps its pixel layers, each at its place on the canvas, with its name, opacity, visibility and the blend modes Paint.NET shares. Layer names are read from the file's own records (PsdLayerRecords: the Unicode name, or the Mac Roman one), as ImageMagick damages any that is not ASCII. - PDN -> PSD -> PDN gives back the same layers, pixel for pixel. - PDN is now a target, shown where the Design & print field is on. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BPxh5s9u8aZQzKRbXnqj8o
KnifeLemon
left a comment
There was a problem hiding this comment.
Thank you for this. A PDN reader and writer of our own, with layers kept both ways and checked in Paint.NET itself, is a big piece of work, and reading the serialized part with System.Formats.Nrbf is exactly the right call.
A few things to change:
- "Merge images" breaks on .pdn files.
CanWritenow says yes topdn, andIImageJoiner.CanWriteis the same method. The built-in "Merge images" preset is "same as source", so when the first file dropped is a .pdn,JobQueuepickspdnas the target.ImageJoiner.Jointhen callsImageEncoder.WriteImage(merged, …, "pdn"), andToMagickFormatthrows "ImageMagick cannot handle 'pdn'". TheFitToSizecall after it would throw too. Could you either write the merged picture withPaintNetWriter.WriteinImageJoiner, or keeppdnout of what the joiner can write, so it falls back to PNG? A test with two .pdn files on the merge preset would cover it. - Check a pixel budget before allocating. The reader caps the canvas only at
Array.MaxLength.ReadFlattenedandReadLayerallocate the full canvas and the full layer before reading any chunk, so a tiny file whose header claims 23,000 × 23,000 allocates about 4 GB before it fails. WithchunkSize= 1,seenbecomes a 2 GB array too. For PDN → PSD, every layer stays in memory, and with up to 4,096 layers that compress to almost nothing, a small file can ask for a lot. Could you checklayers × width × heightagainst one limit before the first allocation (something in the range of FITS' 256 M samples), and refuse achunkSizethat is unreasonably small? - Small:
ImageEncoder.MaxSidecould get"pdn" => 65_535. Then a very wide picture is scaled down to fit, as JPG and WebP are, instead of failing inPaintNetWriter.Write. - Small:
PsdLayerRecords.Readcatches onlyEndOfStreamException. Past the end of the file,reader.ReadBytes(4)returns fewer bytes andReadUInt32BigEndianthrowsArgumentOutOfRangeException, so an unusual PSD fails PSD → PDN instead of using ImageMagick's names. Catching that as well, or reading withReadExactly, would keep the fallback working.
We'll take care of the settings schema number when we merge (79 is wanted by a few open PRs), and of the specialist count together with #1390. CI hasn't run yet because first-time PRs need a maintainer to approve the workflow. We'll start it.
…ords From the review of this PR: - "Merge images" keeps the first file's format, so two .pdn merge into a .pdn: ImageEncoder.WriteImage writes PDN with PaintNetWriter, and the joiner doesn't fit a PDN to a size (lossless, like an animation; the queue refuses one too large). - The reader checks layers x width x height against one budget (256 M pixels, as FITS) before it allocates anything, and that the pixels claimed can come out of what is left of the file. A large layer in tiny chunks is refused. - ImageEncoder.MaxSide knows PDN's 65,535 px, so a very wide picture is scaled to fit instead of failing. - PsdLayerRecords reads exactly the bytes it asks for; a record cut off or out of shape gives no names (ImageMagick's are used) instead of failing. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BPxh5s9u8aZQzKRbXnqj8o
|
Thanks for the careful review! All four are fixed in d12f4d9:
Noted for the schema number and the specialist count, thanks for handling those. |
KnifeLemon#1349 took schema 79 for iPhone ringtones. The images donut now learns pdn in schema 80; FormatFieldsTests counts include M4R (general) and PDN (specialist). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BPxh5s9u8aZQzKRbXnqj8o
Main took schemas 80 to 83 (PDF/A preset, tracker modules, .zxp, .mpg4). The images donut now learns pdn in schema 84; FormatFieldsTests counts include PDN with the formats main added. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BPxh5s9u8aZQzKRbXnqj8o
An .ani is a RIFF "ACON" file whose frames are whole .cur/.ico files. Filee's own reader (Ani) takes the frames out, has ImageMagick decode each at its largest size, and builds the animation step by step, following the "seq " and "rate" chunks (jiffies of 1/60 s, rounded to hundredths) with an endless loop. - GIF, APNG, WebP and the other animated targets keep every step and its time; other targets get the first step. - To CUR, the first frame is written as stored (every size and its hotspot) unless the preset changes the picture; then the hotspot follows a resize, turn or mirror. ImageMagick writes CUR as an icon (type 1, no hotspot), so that file is made a cursor afterwards. - Files are read defensively: bounded size, frame and step counts, frame dimensions; raw-bitmap frames and damaged files are refused with a clear message. - Read only, specialist, Developer field (like CUR). - Settings schema 80: the images donut learns .ani; custom claims stay put. (May need renumbering depending on merge order: 79 is taken by #1384.) Fixes #1365 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BPxh5pr2HpsMpvTujh7kW6
Main took schema 84 (folder ZIP restore) and 85 (.ani), so the Paint.NET step is v86 now; the format counts include .ani as well. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
KnifeLemon
left a comment
There was a problem hiding this comment.
Thank you, all four are in and each one has its test. Routing PDN through PaintNetWriter in WriteImage, and checking the budget before anything is allocated (with the packing-ratio check on top), is just what we hoped for.
We merged main into your branch, moved the .pdn migration to schema 86 (main has taken up to 85 since, with #1390 at 85), and set the specialist count for both new formats. Thanks again, this is a big addition!
What does this change?
Filee reads and writes Paint.NET's own
.pdnfiles, with its own reader and writer (Filee.Engines/Magick/PaintNet.cs,PaintNetWriter.cs). The document part is .NET binary serialization. It is read as plain data with System.Formats.Nrbf, which never creates the types it names. It is written record by record, in the shape Paint.NET 4.2 saves, which every later Paint.NET opens.luni), opacity, visibility and the blend modes both programs share, with the merged picture first. ImageMagick's PSD writer puts visibility in the wrong flag bit and locks transparency on every layer, soSetLayerFlagssets the flags after writing.PsdLayerRecords: the Unicode name, or the Mac Roman one), because ImageMagick damages any name that is not ASCII..pdnis graded specialist in Design & print, like.kra. Settings schema 79 teaches the images donut.pdn(renumber if another change takes 79 first).Type of change
How was it tested?
dotnet format --verify-no-changeson the changed filesdotnet test: newPaintNetTests(Engines;.pdnfiles built byPdnBuilder) andPaintNetFormatTests(Core) pass. The same 18 Engines tests and 1 Core test fail onmainin my Linux environment (missing Pandoc/FFmpeg/LibreOffice, running as root). App tests were not run: the headless suite hangs there.Checklist
src/Filee.App/Assets/i18n/*.jsonfiles (none added)THIRD-PARTY-NOTICES.md