From 476c0aa425af9b489ba68594cbc60f663e7df7a7 Mon Sep 17 00:00:00 2001 From: cre8ive-marketing <168519979+cre8ive-marketing@users.noreply.github.com> Date: Thu, 22 Oct 2026 10:07:12 +0530 Subject: [PATCH 1/3] Read .lua scripts and .css style sheets as plain text (#388, #368) Like the scripts and logs 2.0.0 added: the TXT format takes .lua and .css, so they convert to PDF, Markdown, DOCX and the other text targets, read and never run. Settings schema 33 teaches existing text donuts the extensions unless a donut of the user already claims them. Co-Authored-By: Claude Opus 5.5 --- src/Filee.Core/Formats/FormatRegistry.cs | 5 +-- src/Filee.Core/Settings/AppSettings.cs | 2 +- src/Filee.Core/Settings/UserDataStore.cs | 3 ++ tests/Filee.Core.Tests/ScriptTextTests.cs | 33 ++++++++++++++++++++ tests/Filee.Engines.Tests/ScriptTextTests.cs | 27 ++++++++++++++++ 5 files changed, 67 insertions(+), 3 deletions(-) create mode 100644 tests/Filee.Core.Tests/ScriptTextTests.cs create mode 100644 tests/Filee.Engines.Tests/ScriptTextTests.cs diff --git a/src/Filee.Core/Formats/FormatRegistry.cs b/src/Filee.Core/Formats/FormatRegistry.cs index 1ec46736..14bb5bce 100644 --- a/src/Filee.Core/Formats/FormatRegistry.cs +++ b/src/Filee.Core/Formats/FormatRegistry.cs @@ -134,8 +134,9 @@ public static class FormatRegistry new("sda", "SDA", FormatCategory.Presentation, ["sda", "sdd"], Writable: false), // Plain text / markup - // Text logs and source/proxy scripts are read as plain text, never executed (issues #273, #284, #254). - new("txt", "TXT", FormatCategory.Text, ["txt", "log", "py", "pac", "bat", "command"]), + // Text logs, source/proxy scripts and style sheets are read as plain text, never executed (issues #273, #284, #254, + // #388, #368). + new("txt", "TXT", FormatCategory.Text, ["txt", "log", "py", "pac", "bat", "command", "lua", "css"]), new("md", "Markdown", FormatCategory.Text, ["md", "markdown"]), new("html", "HTML", FormatCategory.Text, ["html", "htm", "xhtml"]), // A web page saved with its pictures in one MIME file (Chrome, Edge, Word); read as the page it holds. diff --git a/src/Filee.Core/Settings/AppSettings.cs b/src/Filee.Core/Settings/AppSettings.cs index 4f488dcb..3df95d28 100644 --- a/src/Filee.Core/Settings/AppSettings.cs +++ b/src/Filee.Core/Settings/AppSettings.cs @@ -112,7 +112,7 @@ public static bool IsFlipKeyAllowed(string key, IEnumerable gest public sealed class AppSettings { /// Current schema version. Increase when the format changes and add a migration. - public const int CurrentSchemaVersion = 32; + public const int CurrentSchemaVersion = 33; public int SchemaVersion { get; set; } = CurrentSchemaVersion; diff --git a/src/Filee.Core/Settings/UserDataStore.cs b/src/Filee.Core/Settings/UserDataStore.cs index 7aff0c84..1dbddd84 100644 --- a/src/Filee.Core/Settings/UserDataStore.cs +++ b/src/Filee.Core/Settings/UserDataStore.cs @@ -750,6 +750,9 @@ public static bool ApplyToLibrary(int from, List presets, List Assert.Equal("txt", FormatRegistry.Detect(name)?.Id); + + [Fact] + public void Version_32_text_donuts_learn_them_without_taking_custom_claims() + { + var presets = BuiltInData.CreatePresets(); + var profiles = BuiltInData.CreateProfiles(); + foreach (var profile in profiles) + profile.Extensions.RemoveAll(e => e is "lua" or "css"); + profiles.Insert(0, new ToolbarProfile { Id = "my-web", Name = "Web", Extensions = ["css"] }); + + Assert.True(SettingsMigrations.ApplyToLibrary(32, presets, profiles)); + + var text = profiles.Single(p => p.Id == "text").Extensions; + Assert.Contains("lua", text); + Assert.DoesNotContain("css", text); // the user's own donut has it + Assert.False(SettingsMigrations.ApplyToLibrary(32, presets, profiles)); + } +} diff --git a/tests/Filee.Engines.Tests/ScriptTextTests.cs b/tests/Filee.Engines.Tests/ScriptTextTests.cs new file mode 100644 index 00000000..80c651c1 --- /dev/null +++ b/tests/Filee.Engines.Tests/ScriptTextTests.cs @@ -0,0 +1,27 @@ +// Lua scripts and CSS style sheets (issues #388, #368) become a PDF of their text, never run. + +using Filee.Core.Conversion; +using Filee.Core.Presets; +using Filee.Engines.Pdf; + +namespace Filee.Engines.Tests; + +public class ScriptTextTests(EngineFixture fx) : IClassFixture +{ + [Theory] + [InlineData("site.css", "body { color: #1d9e75; }")] + [InlineData("init.lua", "local greeting = \"hello\" print(greeting)")] + public async Task The_code_becomes_a_pdf_of_its_text(string name, string code) + { + var available = fx.Converters.Where(c => fx.Catalog.StatusOf(c).IsAvailable).Select(c => c.Id).ToHashSet(); + Assert.SkipUnless(available.Contains("rhwp") || available.Contains("libreoffice"), "neither rhwp nor LibreOffice is available"); + var path = Path.Combine(fx.NewFolder(), name); + await File.WriteAllTextAsync(path, code, TestContext.Current.CancellationToken); + + var job = await fx.ConvertAsync([path], new Preset { TargetFormat = "pdf" }); + + Assert.True(job.State == JobState.Completed, string.Join("; ", job.Files.Select(f => $"{f.ErrorKey} {f.ErrorDetail}"))); + var text = PdfDocumentReader.PlainText(PdfDocumentReader.ReadText(Assert.Single(job.Outputs))); + Assert.Contains(code.Split(' ')[1], text); + } +} From e9bf17399c953fe9f537536af9a5187bef3f3b7f Mon Sep 17 00:00:00 2001 From: cre8ive-marketing <168519979+cre8ive-marketing@users.noreply.github.com> Date: Thu, 22 Oct 2026 10:11:21 +0530 Subject: [PATCH 2/3] Snap the crop frame to the picture's centre lines while moving it (#369) When the moved frame's centre comes within 8 screen pixels of the picture's vertical or horizontal centre line, it snaps onto it and a dashed accent guide shows that line across the picture until the drag ends. A frame as wide (or tall) as the picture never counts as snapped. Keyboard nudges stay pixel-exact and don't snap. Co-Authored-By: Claude Opus 5.5 --- src/Filee.App/Controls/CropCanvas.cs | 21 ++++++++++++++++++ src/Filee.App/Controls/CropMath.cs | 16 ++++++++++++++ tests/Filee.App.Tests/CropMathTests.cs | 22 +++++++++++++++++-- tests/Filee.App.Tests/CropOverlayTests.cs | 26 +++++++++++++++++++++++ 4 files changed, 83 insertions(+), 2 deletions(-) diff --git a/src/Filee.App/Controls/CropCanvas.cs b/src/Filee.App/Controls/CropCanvas.cs index 86cc920a..c51728da 100644 --- a/src/Filee.App/Controls/CropCanvas.cs +++ b/src/Filee.App/Controls/CropCanvas.cs @@ -103,6 +103,9 @@ public double AspectRatio private CropHandle _hoverHandle = CropHandle.None; private Point _dragStartPoint; private Rect _dragStartCrop; + + // The frame being moved sits on the picture's vertical / horizontal centre line: its guide is drawn. + private bool _snappedX, _snappedY; private const double MinSizePx = 24.0; // ───── Motion: tweens evaluated against the clock at each render, so a lost frame never leaves one half done ───── @@ -409,6 +412,7 @@ public override void Render(DrawingContext context) context.DrawImage(SourceImage, source, imgBounds); DrawGrid(context, crop, _grid.Value(now)); + DrawCentreGuides(context, imgBounds); DrawFrame(context, crop, now); DrawSizeChip(context, crop, imgBounds, bounds); } @@ -460,6 +464,20 @@ private static void DrawGrid(DrawingContext context, Rect crop, double opacity) } } + /// The picture's centre line the moved frame snapped to, across the whole picture. + private void DrawCentreGuides(DrawingContext context, Rect picture) + { + if (_activeHandle != CropHandle.Body || !(_snappedX || _snappedY)) + return; + var pen = new Pen(AccentBrush ?? Brushes.MediumPurple, 1, DashStyle.Dash); + var x = Math.Round(picture.Center.X) + 0.5; + var y = Math.Round(picture.Center.Y) + 0.5; + if (_snappedX) + context.DrawLine(pen, new Point(x, picture.Top), new Point(x, picture.Bottom)); + if (_snappedY) + context.DrawLine(pen, new Point(picture.Left, y), new Point(picture.Right, y)); + } + private void DrawFrame(DrawingContext context, Rect crop, long now) { var accent = AccentBrush ?? Brushes.MediumPurple; @@ -738,6 +756,8 @@ protected override void OnPointerMoved(PointerEventArgs e) var newX = SafeClamp(_dragStartCrop.X + deltaX, imgBounds.Left, Math.Max(imgBounds.Left, imgBounds.Right - _dragStartCrop.Width)); var newY = SafeClamp(_dragStartCrop.Y + deltaY, imgBounds.Top, Math.Max(imgBounds.Top, imgBounds.Bottom - _dragStartCrop.Height)); + (newX, _snappedX) = CropMath.SnapToCentre(newX, _dragStartCrop.Width, imgBounds.Left, imgBounds.Right); + (newY, _snappedY) = CropMath.SnapToCentre(newY, _dragStartCrop.Height, imgBounds.Top, imgBounds.Bottom); SetCropFromScreen(new Rect(newX, newY, _dragStartCrop.Width, _dragStartCrop.Height), imgBounds); return; @@ -897,6 +917,7 @@ protected override void OnPointerPressed(PointerPressedEventArgs e) { _dragStartPoint = pt; _dragStartCrop = crop; + _snappedX = _snappedY = false; e.Pointer.Capture(this); e.Handled = true; PopHandle(_activeHandle); diff --git a/src/Filee.App/Controls/CropMath.cs b/src/Filee.App/Controls/CropMath.cs index f14cb460..14f06064 100644 --- a/src/Filee.App/Controls/CropMath.cs +++ b/src/Filee.App/Controls/CropMath.cs @@ -60,6 +60,22 @@ public static Rect Nudge(Rect normalized, int dx, int dy, int width, int height) return new Rect((double)x / width, (double)y / height, normalized.Width, normalized.Height); } + /// How close (screen pixels) the frame's centre has to come to the picture's centre line to snap to it. + public const double SnapDistance = 8; + + /// + /// One side of a frame being dragged (issue #369): its start moved so its centre sits on the centre of + /// … when it comes within of it. A frame as + /// wide as the picture has nowhere to go and never counts as snapped. + /// + public static (double Start, bool Snapped) SnapToCentre(double start, double length, double min, double max) + { + var centre = (min + max) / 2; + if (length >= max - min - 0.5 || Math.Abs(start + length / 2 - centre) > SnapDistance) + return (start, false); + return (centre - length / 2, true); + } + /// Space between the frame and the size chip. public const double ChipGap = 10; diff --git a/tests/Filee.App.Tests/CropMathTests.cs b/tests/Filee.App.Tests/CropMathTests.cs index 707b8362..426d2ad0 100644 --- a/tests/Filee.App.Tests/CropMathTests.cs +++ b/tests/Filee.App.Tests/CropMathTests.cs @@ -1,5 +1,5 @@ -// The crop overlay's arithmetic: ratios and their portrait forms, keyboard nudges, the frame in pixels and where the -// size chip goes. +// The crop overlay's arithmetic: ratios and their portrait forms, keyboard nudges, snapping to the centre, the frame +// in pixels and where the size chip goes. using Avalonia; using Filee.App.Controls; @@ -22,6 +22,24 @@ public void Ratios_and_their_portrait_forms(string mode, bool portrait, double e Assert.Equal(expected, CropMath.Ratio(mode, portrait, 1600, 1000), 6); } + [Theory] + // A 100 px wide frame over a 0…1000 px picture: centred at 500 when its start is 450. + [InlineData(450, 450, true)] // already centred + [InlineData(443, 450, true)] // 7 px off: pulled in + [InlineData(458, 450, true)] // 8 px off: still pulled in + [InlineData(459, 459, false)] // 9 px off: left alone + [InlineData(100, 100, false)] // far away + public void A_moved_frame_snaps_to_the_centre_line(double start, double expected, bool snapped) + { + Assert.Equal((expected, snapped), CropMath.SnapToCentre(start, 100, 0, 1000)); + } + + [Fact] + public void A_frame_as_wide_as_the_picture_never_counts_as_snapped() + { + Assert.Equal((0.0, false), CropMath.SnapToCentre(0, 1000, 0, 1000)); + } + [Fact] public void Only_fixed_ratios_that_arent_square_swap() { diff --git a/tests/Filee.App.Tests/CropOverlayTests.cs b/tests/Filee.App.Tests/CropOverlayTests.cs index 90d47a32..7afe5eb7 100644 --- a/tests/Filee.App.Tests/CropOverlayTests.cs +++ b/tests/Filee.App.Tests/CropOverlayTests.cs @@ -111,6 +111,32 @@ public void Dragging_shows_the_grid_and_the_size_chip_follows_the_frame() }); } + [AvaloniaFact] + public void A_frame_moved_near_the_middle_snaps_to_the_centre_lines() + { + WithOverlay("en", false, ["harbour.jpg"], (window, vm, _suffix) => + { + var canvas = window.CropCanvas; + canvas.NormalizedCropRect = new Rect(0.1, 0.1, 0.5, 0.5); // centre at 35 %, 35 % + Settle(window); + var image = canvas.CalculateImageBounds(); + var origin = canvas.TranslatePoint(default, window)!.Value; + var start = canvas.GetScreenCropRect(image).Center + (Vector)origin; + // To 3 screen pixels short of the picture's centre on both axes: inside the snap distance. + var target = image.Center + (Vector)origin - new Vector(3, 3); + window.MouseDown(start, MouseButton.Left); + window.MouseMove(start + (target - start) / 2); + window.MouseMove(target); + Pump(); + Save(window, "crop-snap-en.png"); + window.MouseUp(target, MouseButton.Left); + + var frame = canvas.NormalizedCropRect; + Assert.Equal(0.5, frame.X + frame.Width / 2, 3); + Assert.Equal(0.5, frame.Y + frame.Height / 2, 3); + }); + } + [AvaloniaFact] public void Keys_nudge_the_frame_open_the_dial_reset_and_close() { From a54bbfa814f814a313006295775344339aecb4cb Mon Sep 17 00:00:00 2001 From: cre8ive-marketing <168519979+cre8ive-marketing@users.noreply.github.com> Date: Thu, 22 Oct 2026 10:21:54 +0530 Subject: [PATCH 3/3] Document .lua / .css as text and crop centre snapping ENGINES.md lists LUA and CSS with the plain-text formats; ARCHITECTURE.md describes the snap to the picture's centre lines in the crop overlay. Co-Authored-By: Claude Opus 5.5 --- docs/ARCHITECTURE.md | 4 +++- docs/ENGINES.md | 2 +- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md index 4a7c9e40..757eaf0b 100644 --- a/docs/ARCHITECTURE.md +++ b/docs/ARCHITECTURE.md @@ -133,7 +133,9 @@ multi-frame pictures refused, the EXIF thumbnail dropped), one history entry for skipped. Every file starts free and whole, its portrait/landscape choice following the picture. - `Controls/CropCanvas` is the stage: the picture inside `ImagePadding`, the part outside the frame frosted (a copy at most 40 px across, drawn large) under a veil of the page colour, the frame in the accent colour with round handles, - a rule-of-thirds grid while dragging and a size chip that follows the frame. The preview is downscaled once by + a rule-of-thirds grid while dragging and a size chip that follows the frame. A frame being moved snaps onto the + picture's vertical or horizontal centre line within `CropMath.SnapDistance` (8 px) and shows that line as a dashed + guide until the drag ends. The preview is downscaled once by `CropService.LoadPreviewAsync`; nothing is decoded again while dragging. Programmatic changes (reset, a ratio, a new picture) ease in with tweens read against the clock, so a lost frame never leaves one half done; dragging and arrow-key nudges follow at once, and `PlayAnimations` (from `Motion.Enabled`) turns all of it off. diff --git a/docs/ENGINES.md b/docs/ENGINES.md index 3ba038b0..6c6a5804 100644 --- a/docs/ENGINES.md +++ b/docs/ENGINES.md @@ -12,7 +12,7 @@ Filee chooses engines automatically. Settings → *Engines* shows their status, | PDFsharp | built in (library) | images → PDF, merge, split, page ranges, **compression** (smaller PDFs, see below) | | PDFium | built in (library) | PDF → images (multi-page TIFF/GIF supported) | | Unhwp | built in (library) | HWP/HWPX → TXT, Markdown, HTML (pictures in a `_files` folder next to the Markdown, embedded in the HTML) | -| Text & Markdown (Markdig) | built in (library) | **Markdown → HTML, TXT**; **TXT, LOG, PY, PAC, BAT, COMMAND → UTF-8 TXT**, preserving source text without running scripts (and → PDF / DOCX through the HWPX writer, rhwp or LibreOffice) | +| Text & Markdown (Markdig) | built in (library) | **Markdown → HTML, TXT**; **TXT, LOG, PY, PAC, BAT, COMMAND, LUA, CSS → UTF-8 TXT**, preserving source text without running scripts (and → PDF / DOCX through the HWPX writer, rhwp or LibreOffice) | | Spreadsheets (ExcelDataReader for XLS) | built in (library) | **XLSX, XLS, ODS, CSV, TSV → XLSX, ODS, CSV, TSV** (one CSV / TSV per sheet) | | Office Open XML | built in | **DOCM / DOTX / DOTM ↔ DOCX, XLSM / XLTX ↔ XLSX, PPTM / POTX / POTM / PPSX ↔ PPTX** (macros removed for macro-free types) | | Fonts | built in | **TTF, OTF, WOFF, WOFF2, EOT ↔ each other**; CFF (PostScript) outlines become TrueType for TTF and EOT |