Conversation
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.
Reviewer's GuideAdds 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 .clipsequenceDiagram
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
File-Level Changes
Tips and commandsInteracting with Sourcery
Customizing Your ExperienceAccess your dashboard to:
Getting Help
|
There was a problem hiding this comment.
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 usingTask.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>Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.
| public override Task<BitmapSource> GetThumbnail(Size renderSize) | ||
| { | ||
| return new Task<BitmapSource>(() => |
There was a problem hiding this comment.
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>(() => |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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
ClipProviderto locate the embedded SQLite DB inside.clipfiles, query preview image bytes, and decode them into a frozenBitmapSource. - Registered
.clipextension to map toClipProviderinPlugin.Init(). - Added a
Microsoft.Data.SqliteNuGet 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. |
| new KeyValuePair<string[], Type>([".clip"], | ||
| typeof(ClipProvider))); | ||
| AnimatedImage.AnimatedImage.Providers.Add( |
| var raw = File.ReadAllBytes(path.LocalPath); | ||
|
|
||
| var sqliteSig = System.Text.Encoding.ASCII.GetBytes(SQLITE_MAGIC); | ||
| var sqliteIndex = IndexOf(raw, sqliteSig, 0); |
| _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; |
| if (_imageData == null) | ||
| return null; | ||
|
|
||
| if (_frame != null) | ||
| return _frame; | ||
|
|
| private readonly string SQLITE_MAGIC = "SQLite format 3"; | ||
| private readonly string FOOTER_MARKER = "CHNKFoot"; |
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
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: