Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions Directory.Packages.props
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,8 @@
<!-- 5.3+ requires SkiaSharp 4, which conflicts with Avalonia 12.1 (SkiaSharp 3.119). -->
<PackageVersion Include="PDFtoImage" Version="5.2.1" />
<PackageVersion Include="Unhwp" Version="0.13.0" />
<!-- Reads the .NET binary serialization inside Paint.NET's .pdn files as data, never instantiating types. -->
<PackageVersion Include="System.Formats.Nrbf" Version="10.0.14" />
<PackageVersion Include="Markdig" Version="1.5.1" />
<PackageVersion Include="ExcelNumberFormat" Version="1.1.0" />
<!-- 5.2+ requires SkiaSharp 4 (see PDFtoImage above); 5.1.1 uses SkiaSharp 3.119 and HarfBuzzSharp 8.3 like Avalonia. -->
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -166,7 +166,7 @@ To undo it, remove the entries in Windows Security → App & browser control →

| From | To |
|---|---|
| **Images**: JPG, PNG, WEBP, AVIF, TIFF, BMP, GIF, APNG, ICO, ICNS, JPEG XL, JPEG 2000, PSD/PSB, TGA, PPM, HEIC, QOI, DDS, PCX, MNG, CUR, …;<br>GIMP XCF, camera RAW (CR2, CR3, NEF, ARW, DNG, …), DICOM, OpenEXR, HDR, FITS, JPEG XR (Windows), iPhone Live Photo (LIVP), animated cursor (ANI) read | any of these (HEIC⁵), PDF, SVG (traced), resized, grayscale;<br>Live/Motion Photos (iPhone, Android) → their video (MP4, MOV⁵), and a video → Live Photo (iPhone) or Motion Photo (Android)⁵, experimental |
| **Images**: JPG, PNG, WEBP, AVIF, TIFF, BMP, GIF, APNG, ICO, ICNS, JPEG XL, JPEG 2000, PSD/PSB, TGA, PPM, HEIC, QOI, DDS, PCX, MNG, CUR, …;<br>GIMP XCF, camera RAW (CR2, CR3, NEF, ARW, DNG, …), DICOM, OpenEXR, HDR, FITS, JPEG XR (Windows), iPhone Live Photo (LIVP), Paint.NET PDN (↔ PSD with its layers), animated cursor (ANI) read | any of these (HEIC⁵), Paint.NET PDN, PDF, SVG (traced), resized, grayscale;<br>Live/Motion Photos (iPhone, Android) → their video (MP4, MOV⁵), and a video → Live Photo (iPhone) or Motion Photo (Android)⁵, experimental |
| **Vector**: SVG, SVGZ, EMF, WMF, AI<br>EPS, PS¹<br>CDR, VSD, CGM, ODG, FODG² | PDF (vector), PNG and other images,<br>SVG ↔ SVGZ, SVG → DXF/DWG, PDF → EPS/PS¹, SVG → EMF, WMF, ODG² |
| **PDF** | DOCX, HWPX, TXT, PNG, JPG, TIFF, CBZ, PPTX (a picture of each page), SVG²;<br>merge (in an order you choose first), split, page ranges, compress (smaller PDFs) |
| **Documents**: DOCX, DOCM, DOTX, EML, Outlook MSG;<br>OFD (Chinese e-invoices, official documents) read<br>DOC, ODT, FODT, OTT, RTF, WPS, WPD, Pages, ClarisWorks, MacWrite, T602, …² | PDF, DOCX, HWPX, EPUB, TXT, HTML;<br>MSG → EML<br>ODT, FODT, OTT² |
Expand Down
1 change: 1 addition & 0 deletions THIRD-PARTY-NOTICES.md
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ Each component remains under its own license.
| AngleSharp | MIT | https://github.com/AngleSharp/AngleSharp |
| pypandoc-hwpx (Pandoc AST mapping and package layout in `Filee.Engines/Hwp/Hwpx`, incl. the `blank.hwpx` reference document) | MIT, Copyright (c) 2024 pypandoc-hwpx Contributors | https://github.com/msjang/pypandoc-hwpx |
| Microsoft.Extensions.* | MIT | https://github.com/dotnet/runtime |
| System.Formats.Nrbf (reads Paint.NET .pdn files) | MIT | https://github.com/dotnet/runtime |
| cu2qu (fontTools): the cubic-to-quadratic approach followed by `Filee.Engines/Fonts/Cff/CubicToQuadratic.cs` | Apache-2.0, Copyright 2016 Google Inc. | https://github.com/fonttools/fonttools |

libuiohook (the native input hook inside SharpHook) is the fork https://github.com/TolikPylypchuk/libuiohook, licensed
Expand Down
16 changes: 15 additions & 1 deletion docs/ENGINES.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ Filee chooses engines automatically. Settings → *Engines* shows their status,

| Engine | How it gets there | Handles |
|---|---|---|
| ImageMagick (Magick.NET) | built in (library) | JPG, PNG, WEBP, TIFF, BMP, GIF, ICO, AVIF, JPEG XL, JPEG 2000, PSD/PSB, TGA, PPM, MNG, QOI, DDS, PCX, CUR, XPM, XBM, WBMP, PAM, PFM, SGI, Sun Raster, MIFF ↔ each other, and **APNG** (Filee's own reader and writer); HEIC (written with FFmpeg), XCF, camera RAW (LibRaw), DICOM (uncompressed ones by Filee's own reader), **FITS** (Filee's own reader; TIFF at the file's own depth by Filee's own writer), Photo CD, OpenEXR, Radiance HDR, EMF/WMF (Windows), the photo of an iPhone LIVP, the merged picture of a Krita KRA, the icon of a Windows program or library (EXE, DLL) and a Windows **animated cursor** (ANI, Filee's own reader) read |
| ImageMagick (Magick.NET) | built in (library) | JPG, PNG, WEBP, TIFF, BMP, GIF, ICO, AVIF, JPEG XL, JPEG 2000, PSD/PSB, TGA, PPM, MNG, QOI, DDS, PCX, CUR, XPM, XBM, WBMP, PAM, PFM, SGI, Sun Raster, MIFF ↔ each other, and **APNG** (Filee's own reader and writer); HEIC (written with FFmpeg), XCF, camera RAW (LibRaw), DICOM (uncompressed ones by Filee's own reader), **FITS** (Filee's own reader; TIFF at the file's own depth by Filee's own writer), Photo CD, OpenEXR, Radiance HDR, EMF/WMF (Windows), the photo of an iPhone LIVP, the merged picture of a Krita KRA, a **Paint.NET PDN** (Filee's own reader and writer: layers merged as Paint.NET shows them, or kept between PDN and PSD/PSB), the icon of a Windows program or library (EXE, DLL) and a Windows **animated cursor** (ANI, Filee's own reader) read |
| Windows Imaging Component | part of Windows (Windows only) | **JPEG XR** (JXR, WDP, HDP; HDR screenshots of Windows) → the images ImageMagick writes, and PDF, ICNS, HEIC and SVG through PNG; HDR tone-mapped to SDR |
| Vector graphics | built in (Svg.Skia, SkiaSharp, ACadSharp) | **SVG/SVGZ → PDF (vector) and images**, SVG ↔ SVGZ, **SVG → DXF** (→ DWG with the CAD engine), **raster images → SVG (tracing)**, PDF-compatible AI → PDF, **HP-GL / HP-GL/2 plot files (PLT) → SVG**, PDF and images (pen moves, packed PE polylines, arcs and filled polygons; text labels are not drawn), **Tajima DST embroidery → SVG** (stitches as lines, a palette colour per thread; → PNG, JPG, PDF from there) |
| ICNS | built in | images → ICNS (16–1024 px), ICNS → images |
Expand Down Expand Up @@ -346,6 +346,20 @@ in it also builds the package (artifact `windows-arm64-package`).
without one gives a clear error. Krita's backup of the previous save, `name.kra~`, is the same file and is read the
same way; its output is named `name.png`. Watch folders skip it (and any name ending in `~`), as it is written next
to the drawing on every save.
- **PDN** (Paint.NET's own file) is read by Filee itself (`PaintNet`): an XML header, the document in .NET binary
serialization, read as plain data with System.Formats.Nrbf (no type it names is ever created), then each layer's BGRA
pixels in gzip-compressed chunks. For PNG, JPG and every other target the visible layers are merged with Paint.NET's
own blend formulas (all 14 modes, layer opacity), so the picture looks as it does in Paint.NET. To **PSD / PSB** the
layers are kept, for Photoshop, GIMP and Krita: names (also as Unicode, `luni`), opacity, visibility and the blend
modes Photoshop shares (Multiply, Additive as Linear Dodge, Color Burn, Color Dodge, Overlay, Difference, Lighten,
Darken, Screen; Reflect, Glow, Negation and Xor become Normal), with the merged picture first. ImageMagick's PSD
writer stores a layer's visibility in the wrong bit and locks its transparency, so `PaintNet.SetLayerFlags` sets the
flags afterwards. A preset that changes the picture (resize, rotate, …) gives one merged picture instead.
Filee also **writes** PDN (`PaintNetWriter`), in the shape Paint.NET 4.2 saves and every later version opens (the
same records, class metadata and 256 KB gzip chunks, and a thumbnail in the header): any picture becomes one layer
named Background, and a **PSD / PSB** keeps its pixel layers, each at its place on the canvas, with its Unicode name,
opacity, visibility and the blend modes Paint.NET shares. PDN → PSD → PDN gives back the same layers, pixel for
pixel. A PDN is a target only where the Design & print field is on.
- **EXE / DLL** (a Windows program or library, #1188) is read as its icon, the one Explorer shows: the first icon group
of its resources, put back together as an `.ico` in memory (`ProgramIcon`, Filee's own reader for 32 and 64-bit PE
files; only the headers and the icon are read, at most 64 MB, and the file is never loaded or run). To ICO it is
Expand Down
6 changes: 3 additions & 3 deletions src/Filee.Core/Formats/FormatFields.cs
Original file line number Diff line number Diff line change
Expand Up @@ -75,8 +75,8 @@ .. Enum.GetValues<FormatCategory>().Select(category => category.ToString().ToLow
(FormatGrade.Specialist,
[
"jp2", "psd", "psb", "ppm", "icns", "qoi", "dds", "cur", "xpm", "xbm", "pam", "pfm", "miff", "dcm",
"exr", "hdr", "fits", "xcf", "kra", "raw", "eps", "ps", "ai", "cdr", "cgm", "plt", "dst", "fodg", "fodt",
"fods", "fodp", "rst", "tex", "org", "adoc", "typ", "mediawiki", "dbk", "ipynb", "textile", "man",
"exr", "hdr", "fits", "xcf", "kra", "pdn", "raw", "eps", "ps", "ai", "cdr", "cgm", "plt", "dst", "fodg",
"fodt", "fods", "fodp", "rst", "tex", "org", "adoc", "typ", "mediawiki", "dbk", "ipynb", "textile", "man",
"jsonl", "yaml", "toml", "har", "plist", "htmlz", "txtz", "chm", "dv", "mxf", "cavs", "usm", "m4s", "dav", "bik",
"h265", "h264", "ac3", "caf", "mp2", "dss", "wv", "tta", "w64", "eac3", "ape", "tak", "dsf", "dff",
"dts", "tar", "tgz", "tbz2", "txz", "tar7z", "tlz", "gz", "bz2", "xz", "lz", "lzma", "jar", "zxp", "mcpack", "mcaddon",
Expand All @@ -97,7 +97,7 @@ .. Enum.GetValues<FormatCategory>().Select(category => category.ToString().ToLow
private static readonly Dictionary<string, FormatField[]> Overlaps = BuildOverlaps(
[
// Layout, print, photo post-production, signs and embroidery.
(["psd", "psb", "exr", "hdr", "xcf", "kra", "raw", "eps", "ps", "ai", "cdr", "plt", "dst"], ["design"]),
(["psd", "psb", "exr", "hdr", "xcf", "kra", "pdn", "raw", "eps", "ps", "ai", "cdr", "plt", "dst"], ["design"]),
// Game textures and cutscenes, Minecraft packs and modpacks.
(["dds", "usm", "bik", "mcpack", "mcaddon", "mcworld", "mctemplate", "mrpack"], ["game"]),
// Medical imaging and astronomy.
Expand Down
2 changes: 2 additions & 0 deletions src/Filee.Core/Formats/FormatRegistry.cs
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,8 @@ public static class FormatRegistry
// Krita's own file (#903): a ZIP whose merged picture (mergedimage.png) is read, as Krita shows it. ".kra~" is the
// backup Krita keeps of the previous save, the same file under another name (#1077).
new("kra", "Krita (KRA)", FormatCategory.Image, ["kra", "kra~"], Writable: false),
// Paint.NET's own file, read and written by Filee: layers merged for other pictures, kept between PDN and PSD.
new("pdn", "Paint.NET (PDN)", FormatCategory.Image, ["pdn"]),
// iPhone Live Photo as one ZIP (iCloud downloads): the photo, or its video with FFmpeg.
new("livp", "Live Photo (LIVP)", FormatCategory.Image, ["livp"], Writable: false),
// A Windows program or library (#1188), read as its icon: the picture Explorer shows for the file.
Expand Down
2 changes: 1 addition & 1 deletion src/Filee.Core/Settings/AppSettings.cs
Original file line number Diff line number Diff line change
Expand Up @@ -170,7 +170,7 @@ public static bool IsFlipKeyAllowed(string key, IEnumerable<TriggerGesture> gest
public sealed class AppSettings
{
/// <summary>Current schema version. Increase when the format changes and add a migration.</summary>
public const int CurrentSchemaVersion = 85;
public const int CurrentSchemaVersion = 86;

public int SchemaVersion { get; set; } = CurrentSchemaVersion;

Expand Down
3 changes: 3 additions & 0 deletions src/Filee.Core/Settings/UserDataStore.cs
Original file line number Diff line number Diff line change
Expand Up @@ -1131,6 +1131,9 @@ public static bool ApplyToLibrary(int from, List<Preset> presets, List<ToolbarPr
// v85: Windows animated cursors (.ani, #1365) learn the images donut; custom claims stay put.
if (from < 85)
changed |= LearnExtensions(profiles, out _, ["ani"]);
// v86: Paint.NET pictures (.pdn) learn the images donut; custom claims stay put.
if (from < 86)
changed |= LearnExtensions(profiles, out _, ["pdn"]);
return changed;
}

Expand Down
1 change: 1 addition & 0 deletions src/Filee.Engines/Filee.Engines.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
<PackageReference Include="System.Security.Cryptography.Pkcs" />
<PackageReference Include="PDFtoImage" />
<PackageReference Include="Unhwp" />
<PackageReference Include="System.Formats.Nrbf" />
<PackageReference Include="Markdig" />
<PackageReference Include="ExcelNumberFormat" />
<PackageReference Include="Svg.Skia" />
Expand Down
25 changes: 22 additions & 3 deletions src/Filee.Engines/Magick/ImageEncoder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -92,9 +92,9 @@ public static bool KeepsAnimation(string sourceFormat, string targetFormat) =>

/// <summary>
/// Packages of a picture and more, of which the picture is read (<see cref="ImageReader"/>): an iPhone LIVP, a Krita
/// KRA, the icon of a Windows program or library (<see cref="ProgramIcon"/>).
/// KRA, a Paint.NET PDN (<see cref="PaintNet"/>), the icon of a Windows program or library (<see cref="ProgramIcon"/>).
/// </summary>
public static readonly string[] Packages = ["livp", "kra", FormatRegistry.Program];
public static readonly string[] Packages = ["livp", "kra", "pdn", FormatRegistry.Program];

/// <summary>
/// Every format ImageMagick reads as input: the raster formats, the picture of <see cref="Packages"/> and, on
Expand Down Expand Up @@ -193,6 +193,7 @@ public static List<IMagickImage<byte>> SelectFrames(MagickImageCollection images
"jpg" or "jpeg" => 65_500,
"webp" => 16_383,
"avif" => 16_384,
"pdn" => PaintNetWriter.MaxSide,
_ => double.PositiveInfinity,
};

Expand Down Expand Up @@ -465,6 +466,19 @@ public static List<string> Write(IReadOnlyList<IMagickImage<byte>> frames, strin
ImageOptions? options = null, string? sourcePath = null, bool animation = false)
{
var written = new List<string>();
if (targetFormat == "pdn")
{
// Paint.NET's own file, written by Filee (PaintNetWriter): one picture per file, as one layer.
for (var i = 0; i < frames.Count; i++)
{
var pdn = output.Allocate("pdn", frames.Count > 1 ? $"_p{i + 1}" : null);
if (pdn is null)
continue;
PaintNetWriter.Write(frames[i], pdn);
written.Add(pdn);
}
return written;
}
var (format, extension) = OutputFormat(targetFormat, options ?? new ImageOptions(), sourcePath);

if (targetFormat == "apng")
Expand Down Expand Up @@ -595,11 +609,16 @@ internal static void FitToSize(IMagickImage<byte> image, string path, MagickForm
}
}

/// <summary>Writes one image as <paramref name="targetFormat"/> (APNG through <see cref="Apng"/>).</summary>
/// <summary>
/// Writes one image as <paramref name="targetFormat"/> (APNG through <see cref="Apng"/>, Paint.NET's PDN through
/// <see cref="PaintNetWriter"/>).
/// </summary>
public static void WriteImage(IMagickImage<byte> image, string path, string targetFormat)
{
if (targetFormat == "apng")
Apng.Write([image], path);
else if (targetFormat == "pdn")
PaintNetWriter.Write(image, path);
else
image.Write(path, ToMagickFormat(targetFormat));
}
Expand Down
3 changes: 2 additions & 1 deletion src/Filee.Engines/Magick/ImageJoiner.cs
Original file line number Diff line number Diff line change
Expand Up @@ -118,7 +118,8 @@ internal static void Join(IReadOnlyList<(string Path, string Format)> inputs, st
ImageEncoder.ApplyOptions(merged, rest, format);
cancellationToken.ThrowIfCancellationRequested();
ImageEncoder.WriteImage(merged, outputPath, format);
if (options.MaxSizeMb > 0 || options.MinSizeMb > 0)
// A PDN (lossless, written by Filee) isn't fitted, like an animation: the queue refuses one too large.
if ((options.MaxSizeMb > 0 || options.MinSizeMb > 0) && format != "pdn")
ImageEncoder.FitToSize(merged, outputPath, ImageEncoder.OutputFormat(format, rest).Format, SizeLimit.Bytes(options.MaxSizeMb),
SizeLimit.MinimumBytes(options.MinSizeMb), options.MaxSizeKeepsPixels);
if (format == "cur")
Expand Down
6 changes: 4 additions & 2 deletions src/Filee.Engines/Magick/ImageReader.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,8 @@
// (LibRaw settings), layered PSD/PSB/XCF (one flattened picture instead of one file per layer) and Windows
// metafiles (rendered at the preset's DPI), APNG (Filee's own reader composes the frames) and uncompressed DICOM
// (Filee's own reader, as ImageMagick's Q8 build reads only 8-bit DICOM), FITS (Filee's own reader, stretched and
// debayered as the preset says), the photo of an iPhone LIVP, the merged picture of a Krita KRA and the icon of a Windows
// program. Used by every engine that takes a raster image as input.
// debayered as the preset says), the photo of an iPhone LIVP, the merged picture of a Krita KRA, the layers of a Paint.NET
// PDN merged as Paint.NET shows them and the icon of a Windows program. Used by every engine that takes a raster image as input.

using System.Buffers.Binary;
using Filee.Core.Formats;
Expand Down Expand Up @@ -35,6 +35,8 @@ public static MagickImageCollection Read(string path, string formatId, Preset pr
return ReadLivePhoto(path, preset);
if (formatId == "kra")
return ReadPackaged(Krita.ReadMerged(path), "png", preset);
if (formatId == "pdn")
return [PaintNet.ReadFlattened(path)];
if (formatId == FormatRegistry.Program)
return ReadPackaged(ProgramIcon.Read(path), "ico", preset);
if (formatId == "fits")
Expand Down
18 changes: 14 additions & 4 deletions src/Filee.Engines/Magick/MagickImageConverter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -29,9 +29,13 @@ public sealed class MagickImageConverter : IConverter, ITiffMerger, IImageTransf
internal Func<(string Ffmpeg, string Ffprobe)?> LocateFfmpeg { get; init; } = FfmpegConverter.Locate;

public IReadOnlyList<ConversionEdge> Edges { get; } =
(from source in ImageEncoder.Sources
from target in ImageEncoder.Writable
select ImageEncoder.Edge(source, target)).ToList();
[
.. from source in ImageEncoder.Sources
from target in ImageEncoder.Writable
select ImageEncoder.Edge(source, target),
// Paint.NET's own file, written by Filee (PaintNetWriter); a PSD keeps its layers there.
.. ImageEncoder.Sources.Where(source => source != "pdn").Select(source => ImageEncoder.Edge(source, "pdn")),
];

public EngineStatus GetStatus() =>
EngineStatus.Available(MagickNET.ImageMagickVersion,
Expand Down Expand Up @@ -77,7 +81,7 @@ public Task MergeAsync(IReadOnlyList<string> inputPaths, string outputPath, File

public bool CanRead(string format) => ImageEncoder.Sources.Contains(format);

public bool CanWrite(string format) => ImageEncoder.Writable.Contains(format);
public bool CanWrite(string format) => format == "pdn" || ImageEncoder.Writable.Contains(format);

/// <summary>"Merge images": the pictures one under another or side by side in one image (<see cref="ImageJoiner"/>).</summary>
public Task JoinAsync(IReadOnlyList<(string Path, string Format)> inputs, string outputPath, string format,
Expand All @@ -97,6 +101,12 @@ public Task<IReadOnlyList<string>> ConvertAsync(ConversionStep step, IProgress<d
// A program's icon as ICO is the icon itself, with the sizes drawn for it, unless the preset changes it.
if (step.From == FormatRegistry.Program && step.To == "ico" && ProgramIcon.KeptAsItIs(step.Preset.Image))
return ProgramIcon.Save(step, progress);
// A Paint.NET file as PSD keeps its layers, unless the preset changes the picture.
if (step.From == "pdn" && step.To is "psd" or "psb" && PaintNet.KeepsLayers(step.Preset.Image))
return PaintNet.SaveLayered(step, progress, cancellationToken);
// A PSD as a Paint.NET file keeps its layers too.
if (step.From is "psd" or "psb" && step.To == "pdn" && PaintNet.KeepsLayers(step.Preset.Image))
return PaintNetWriter.SaveLayered(step, progress, cancellationToken);
// Animated PNGs are often saved as .png: read them as APNG, so they stay animated as GIF or WebP. An AVIF is an
// animation only when it is an image sequence.
var from = step.From == "png" && Apng.IsAnimated(step.InputPath) ? "apng" : AvifSequence.SourceFormat(step.From, step.InputPath);
Expand Down
Loading
Loading