Skip to content

Add ClipProvider for .clip support#1940

Merged
emako merged 1 commit into
masterfrom
feat/clip
May 11, 2026
Merged

Add ClipProvider for .clip support#1940
emako merged 1 commit into
masterfrom
feat/clip

Conversation

@emako
Copy link
Copy Markdown
Member

@emako emako commented May 11, 2026

Introduce ClipProvider to extract embedded SQLite payloads from .clip files and produce thumbnails/frames from the CanvasPreview.ImageData blob. The provider scans the file for the "SQLite format 3" header and a footer marker (CHNKFoot), writes the slice to a temp SQLite file, queries ImageData, and decodes it into a frozen BitmapSource. It includes disposal/cleanup of the temp DB and connection and helper IndexOf/LastIndexOf utilities. Register the provider for ".clip" in Plugin.cs and add a Microsoft.Data.Sqlite package reference.

PR Checklist

  • Functionality has been tested, no obvious bugs
  • Code style follows project conventions
  • Documentation/comments updated (if applicable)

Brief Description of Changes

Please briefly describe the main changes in this PR:

Related Issue (if any)

Please provide related issue numbers:

Additional Notes

Add any extra notes here:

Summary by Sourcery

Add support for generating previews from .clip files by introducing a new animation provider and registering it in the image viewer plugin.

New Features:

  • Introduce ClipProvider to extract image data from embedded SQLite payloads in .clip files and render thumbnails and frames.
  • Register .clip as a supported animated image format in the image viewer plugin.

Introduce ClipProvider to extract embedded SQLite payloads from .clip files and produce thumbnails/frames from the CanvasPreview.ImageData blob. The provider scans the file for the "SQLite format 3" header and a footer marker (CHNKFoot), writes the slice to a temp SQLite file, queries ImageData, and decodes it into a frozen BitmapSource. It includes disposal/cleanup of the temp DB and connection and helper IndexOf/LastIndexOf utilities. Register the provider for ".clip" in Plugin.cs and add a Microsoft.Data.Sqlite package reference.
Copilot AI review requested due to automatic review settings May 11, 2026 19:20
@sourcery-ai
Copy link
Copy Markdown

sourcery-ai Bot commented May 11, 2026

Reviewer's Guide

Adds a new ClipProvider animation provider to extract embedded SQLite image data from .clip files and registers it for preview, including temp DB extraction, SQLite querying, bitmap decoding, and cleanup, plus the necessary project dependency wiring.

Sequence diagram for ClipProvider thumbnail rendering from .clip

sequenceDiagram
    participant AnimatedImage
    participant ClipProvider
    participant FileSystem
    participant SqliteConnection
    participant CanvasPreviewTable
    participant BitmapImage

    AnimatedImage->>ClipProvider: ClipProvider(path, meta, contextObject)
    ClipProvider->>FileSystem: File.ReadAllBytes(path.LocalPath)
    ClipProvider->>ClipProvider: IndexOf(raw, sqliteSig, 0)
    ClipProvider->>ClipProvider: LastIndexOf(raw, footerSig)
    ClipProvider->>FileSystem: File.WriteAllBytes(_tempSqlitePath, sqliteBytes)
    ClipProvider->>SqliteConnection: new SqliteConnection(Data Source=_tempSqlitePath)
    ClipProvider->>SqliteConnection: Open()
    ClipProvider->>SqliteConnection: CreateCommand()
    SqliteConnection->>CanvasPreviewTable: SELECT ImageData FROM CanvasPreview
    CanvasPreviewTable-->>ClipProvider: ImageData (byte[])

    AnimatedImage->>ClipProvider: GetThumbnail(renderSize)
    ClipProvider->>BitmapImage: BeginInit()
    ClipProvider->>BitmapImage: set DecodePixelWidth/Height
    ClipProvider->>BitmapImage: EndInit()
    ClipProvider->>BitmapImage: Freeze()
    ClipProvider-->>AnimatedImage: BitmapSource
Loading

File-Level Changes

Change Details Files
Introduce ClipProvider to read embedded SQLite CanvasPreview image data from .clip files and render it as a BitmapSource for thumbnails and frames.
  • Implements ClipProvider as an AnimationProvider that scans the .clip file byte array for a SQLite header and optional CHNKFoot footer to isolate the SQLite payload.
  • Writes the extracted SQLite segment to a temporary file, opens a read‑only Microsoft.Data.Sqlite connection, and queries the CanvasPreview.ImageData blob.
  • Caches the retrieved image blob, decodes it into a BitmapImage with DPI fix and freezing, and reuses a cached BitmapSource for rendered frames.
  • Implements custom IndexOf and LastIndexOf utilities for byte pattern search and overrides Dispose to close the connection and delete the temp database file.
QuickLook.Plugin/QuickLook.Plugin.ImageViewer/AnimatedImage/Providers/ClipProvider.cs
Register ClipProvider for .clip extension and add the SQLite dependency to the image viewer plugin.
  • Registers ClipProvider in the AnimatedImage provider list for the .clip file extension in Plugin.Init().
  • Adds a Microsoft.Data.Sqlite package reference in the QuickLook.Plugin.ImageViewer project file to support SQLite access.
QuickLook.Plugin/QuickLook.Plugin.ImageViewer/Plugin.cs
QuickLook.Plugin/QuickLook.Plugin.ImageViewer/QuickLook.Plugin.ImageViewer.csproj

Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

@emako emako merged commit 56d6915 into master May 11, 2026
4 checks passed
@emako emako deleted the feat/clip branch May 11, 2026 19:21
Copy link
Copy Markdown

@sourcery-ai sourcery-ai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey - I've found 2 issues, and left some high level feedback:

  • Both GetThumbnail and GetRenderedFrame create Task instances with new Task<BitmapSource>(...) but never start them, so callers will never get a completed task; consider using Task.FromResult, Task.Run, or making the methods async and returning an already-started/awaited task instead.
  • The ClipProvider constructor performs synchronous File.ReadAllBytes on the full .clip file and all SQLite extraction work eagerly; consider deferring this IO/work to the first thumbnail/frame request or moving it to an async path to avoid blocking the UI thread with large files.
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- Both GetThumbnail and GetRenderedFrame create Task instances with `new Task<BitmapSource>(...)` but never start them, so callers will never get a completed task; consider using `Task.FromResult`, `Task.Run`, or making the methods async and returning an already-started/awaited task instead.
- The ClipProvider constructor performs synchronous File.ReadAllBytes on the full .clip file and all SQLite extraction work eagerly; consider deferring this IO/work to the first thumbnail/frame request or moving it to an async path to avoid blocking the UI thread with large files.

## Individual Comments

### Comment 1
<location path="QuickLook.Plugin/QuickLook.Plugin.ImageViewer/AnimatedImage/Providers/ClipProvider.cs" line_range="109-111" />
<code_context>
+        _imageData = null;
+    }
+
+    public override Task<BitmapSource> GetThumbnail(Size renderSize)
+    {
+        return new Task<BitmapSource>(() =>
+        {
+            if (_imageData == null)
</code_context>
<issue_to_address>
**issue (bug_risk):** The Task returned by GetThumbnail is never started, so callers will hang awaiting it.

`new Task<BitmapSource>(...)` creates a cold task that never runs unless `Start()` is called, so `await GetThumbnail(...)` will never complete.

Use a hot task or an async method instead, e.g.:

```csharp
public override Task<BitmapSource> GetThumbnail(Size renderSize)
{
    if (_imageData == null)
        return Task.FromResult<BitmapSource>(null);

    return Task.Run(() =>
    {
        // existing decoding logic
    });
}
```

Apply the same fix to `GetRenderedFrame`.
</issue_to_address>

### Comment 2
<location path="QuickLook.Plugin/QuickLook.Plugin.ImageViewer/AnimatedImage/Providers/ClipProvider.cs" line_range="146-111" />
<code_context>
+        });
+    }
+
+    public override Task<BitmapSource> GetRenderedFrame(int index)
+    {
+        return new Task<BitmapSource>(() =>
+        {
+            if (_imageData == null)
</code_context>
<issue_to_address>
**issue (bug_risk):** GetRenderedFrame also returns an unstarted Task, mirroring the bug in GetThumbnail.

As with `GetThumbnail`, this creates a cold `Task` that is never started, so callers awaiting `GetRenderedFrame` will hang. Please update this to match the chosen pattern (e.g. `Task.Run`, `Task.FromResult`, or `async`/`await`) so it returns a started task and both methods behave consistently.
</issue_to_address>

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

Comment on lines +109 to +111
public override Task<BitmapSource> GetThumbnail(Size renderSize)
{
return new Task<BitmapSource>(() =>
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

issue (bug_risk): The Task returned by GetThumbnail is never started, so callers will hang awaiting it.

new Task<BitmapSource>(...) creates a cold task that never runs unless Start() is called, so await GetThumbnail(...) will never complete.

Use a hot task or an async method instead, e.g.:

public override Task<BitmapSource> GetThumbnail(Size renderSize)
{
    if (_imageData == null)
        return Task.FromResult<BitmapSource>(null);

    return Task.Run(() =>
    {
        // existing decoding logic
    });
}

Apply the same fix to GetRenderedFrame.


public override Task<BitmapSource> GetThumbnail(Size renderSize)
{
return new Task<BitmapSource>(() =>
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

issue (bug_risk): GetRenderedFrame also returns an unstarted Task, mirroring the bug in GetThumbnail.

As with GetThumbnail, this creates a cold Task that is never started, so callers awaiting GetRenderedFrame will hang. Please update this to match the chosen pattern (e.g. Task.Run, Task.FromResult, or async/await) so it returns a started task and both methods behave consistently.

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds .clip (Clip Studio Paint) support to the ImageViewer plugin by introducing a new ClipProvider that extracts an embedded SQLite payload, reads CanvasPreview.ImageData, and decodes it into a WPF BitmapSource for thumbnail/frame rendering.

Changes:

  • Added ClipProvider to locate the embedded SQLite DB inside .clip files, query preview image bytes, and decode them into a frozen BitmapSource.
  • Registered .clip extension to map to ClipProvider in Plugin.Init().
  • Added a Microsoft.Data.Sqlite NuGet dependency to the ImageViewer plugin project.

Reviewed changes

Copilot reviewed 3 out of 3 changed files in this pull request and generated 5 comments.

File Description
QuickLook.Plugin/QuickLook.Plugin.ImageViewer/QuickLook.Plugin.ImageViewer.csproj Adds Microsoft.Data.Sqlite package reference needed by the new provider.
QuickLook.Plugin/QuickLook.Plugin.ImageViewer/Plugin.cs Registers .clip with ClipProvider in the animated image provider map.
QuickLook.Plugin/QuickLook.Plugin.ImageViewer/AnimatedImage/Providers/ClipProvider.cs New provider that extracts SQLite bytes from .clip and decodes CanvasPreview.ImageData to thumbnails/frames.

Comment on lines +103 to 105
new KeyValuePair<string[], Type>([".clip"],
typeof(ClipProvider)));
AnimatedImage.AnimatedImage.Providers.Add(
Comment on lines +47 to +50
var raw = File.ReadAllBytes(path.LocalPath);

var sqliteSig = System.Text.Encoding.ASCII.GetBytes(SQLITE_MAGIC);
var sqliteIndex = IndexOf(raw, sqliteSig, 0);
Comment on lines +66 to +77
_tempSqlitePath = System.IO.Path.GetTempFileName();
File.WriteAllBytes(_tempSqlitePath, sqliteBytes);

_conn = new SqliteConnection($"Data Source={_tempSqlitePath};Mode=ReadOnly;");
_conn.Open();

using var cmd = _conn.CreateCommand();
cmd.CommandText = "SELECT ImageData FROM CanvasPreview LIMIT 1";
var obj = cmd.ExecuteScalar();
if (obj is byte[] b)
{
_imageData = b;
Comment on lines +150 to +155
if (_imageData == null)
return null;

if (_frame != null)
return _frame;

Comment on lines +32 to +33
private readonly string SQLITE_MAGIC = "SQLite format 3";
private readonly string FOOTER_MARKER = "CHNKFoot";
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants