Skip to content

Commit caf0e26

Browse files
committed
Use image previews in Quick Look
1 parent 170ea63 commit caf0e26

File tree

1 file changed

+133
-0
lines changed

1 file changed

+133
-0
lines changed

quicklook/TIVarsQuickLookSupport.mm

Lines changed: 133 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -253,6 +253,69 @@ void sanitize_json_for_preview(json& value, const std::string& key = std::string
253253
}
254254
}
255255

256+
std::string preview_image_data_url_from_readable(const std::string& content)
257+
{
258+
try
259+
{
260+
const json parsed = json::parse(content);
261+
if (parsed.is_object() && parsed.contains("previewImageDataUrl") && parsed.at("previewImageDataUrl").is_string())
262+
{
263+
return parsed.at("previewImageDataUrl").get<std::string>();
264+
}
265+
}
266+
catch (const std::exception&)
267+
{
268+
}
269+
270+
return "";
271+
}
272+
273+
NSImage* image_from_data_url(const std::string& dataUrl)
274+
{
275+
if (dataUrl.empty())
276+
{
277+
return nil;
278+
}
279+
280+
const std::string::size_type commaPos = dataUrl.find(',');
281+
if (commaPos == std::string::npos)
282+
{
283+
return nil;
284+
}
285+
286+
NSString* base64String = nsstring_from_std(dataUrl.substr(commaPos + 1));
287+
NSData* imageData = [[NSData alloc] initWithBase64EncodedString:base64String options:0];
288+
return imageData == nil ? nil : [[NSImage alloc] initWithData:imageData];
289+
}
290+
291+
NSRect aspect_fit_rect(NSSize imageSize, NSRect bounds)
292+
{
293+
if (imageSize.width <= 0.0 || imageSize.height <= 0.0 || bounds.size.width <= 0.0 || bounds.size.height <= 0.0)
294+
{
295+
return bounds;
296+
}
297+
298+
const CGFloat scale = std::min(bounds.size.width / imageSize.width, bounds.size.height / imageSize.height);
299+
const NSSize fittedSize = NSMakeSize(imageSize.width * scale, imageSize.height * scale);
300+
return NSMakeRect(bounds.origin.x + (bounds.size.width - fittedSize.width) * 0.5,
301+
bounds.origin.y + (bounds.size.height - fittedSize.height) * 0.5,
302+
fittedSize.width,
303+
fittedSize.height);
304+
}
305+
306+
void append_preview_image_section(std::ostringstream& html, const std::string& dataUrl)
307+
{
308+
if (dataUrl.empty())
309+
{
310+
return;
311+
}
312+
313+
html << "<section class=\"panel image-panel\">"
314+
<< "<h3>Preview</h3>"
315+
<< "<div class=\"image-frame\"><img class=\"preview-image\" src=\"" << html_escape(dataUrl) << "\" alt=\"Preview image\"></div>"
316+
<< "</section>";
317+
}
318+
256319
std::string model_name_for_header(const tivars::TIVarFile::var_header_t& header)
257320
{
258321
if (header.ownerPID != tivars::TIVarFile::OWNER_PID_NONE && tivars::TIModels::isValidPID(header.ownerPID))
@@ -351,6 +414,9 @@ void append_section(std::ostringstream& html, const std::string& title, const st
351414
<< ".entry-size{font:700 12px/1 Menlo,Monaco,monospace;color:var(--accent);background:var(--accent-soft);padding:6px 10px;border-radius:999px;white-space:nowrap;}"
352415
<< ".panel{margin-top:14px;padding-top:14px;border-top:1px solid #ece5d7;}"
353416
<< ".panel h3{margin:0 0 10px 0;font-size:14px;text-transform:uppercase;letter-spacing:.05em;color:var(--muted);}"
417+
<< ".image-panel{display:block;}"
418+
<< ".image-frame{display:flex;justify-content:center;align-items:center;min-height:160px;background-color:#efe6d8;background-image:linear-gradient(45deg,rgba(255,255,255,0.55) 25%,transparent 25%,transparent 75%,rgba(255,255,255,0.55) 75%,rgba(255,255,255,0.55)),linear-gradient(45deg,rgba(255,255,255,0.55) 25%,transparent 25%,transparent 75%,rgba(255,255,255,0.55) 75%,rgba(255,255,255,0.55));background-position:0 0,12px 12px;background-size:24px 24px;border:1px solid #e3dacd;border-radius:14px;padding:12px;overflow:hidden;}"
419+
<< ".preview-image{display:block;max-width:100%;max-height:420px;object-fit:contain;border-radius:10px;box-shadow:0 10px 24px rgba(17,24,39,0.12);}"
354420
<< "pre{margin:0;white-space:pre-wrap;word-break:break-word;font:13px/1.45 Menlo,Monaco,monospace;color:#10212c;background:#faf7f1;padding:14px 16px;border-radius:14px;border:1px solid #ede5d8;}"
355421
<< "</style></head><body><main>"
356422
<< "<section class=\"hero\">"
@@ -409,9 +475,11 @@ RenderedDocument render_var_document(const std::string& path)
409475
for (size_t index = 0; index < previewCount; index++)
410476
{
411477
const auto& entry = entries[index];
478+
std::string previewImageDataUrl;
412479
std::string readable;
413480
try
414481
{
482+
previewImageDataUrl = preview_image_data_url_from_readable(file.getReadableContent(options_t{}, static_cast<uint16_t>(index)));
415483
options_t options;
416484
options["reindent"] = true;
417485
options["prettify"] = true;
@@ -434,6 +502,7 @@ RenderedDocument render_var_document(const std::string& path)
434502
append_meta_row(body, "Meta length", std::to_string(entry.meta_length));
435503
append_meta_row(body, "Data length", std::to_string(entry.data_length));
436504
body << "</div></section>";
505+
append_preview_image_section(body, previewImageDataUrl);
437506
append_section(body, "Readable content", readable);
438507
body << "</article>";
439508
}
@@ -548,6 +617,7 @@ RenderedDocument render_error_document(const std::string& path, const std::strin
548617
std::string badge;
549618
std::string title;
550619
std::string subtitle;
620+
std::string previewImageDataUrl;
551621
bool isFlash = false;
552622
bool isCorrupt = false;
553623
};
@@ -577,6 +647,10 @@ ThumbnailDescriptor describe_for_thumbnail(const std::string& path)
577647
descriptor.badge = entries.empty() ? "Unknown var" : readable_entry_name(entries.front());
578648
descriptor.title = entries.empty() ? descriptor.title : entries.front()._type.getName();
579649
descriptor.subtitle = entries.empty() ? "Unknown content" : truncate_text(var.getReadableContent({{"prettify", true}}));
650+
if (entries.size() == 1)
651+
{
652+
descriptor.previewImageDataUrl = preview_image_data_url_from_readable(var.getReadableContent());
653+
}
580654
}
581655
}
582656
catch (const std::exception&)
@@ -658,6 +732,60 @@ void draw_centered_string(NSString* string, NSRect rect, NSFont* font, NSColor*
658732
code:1
659733
userInfo:@{NSLocalizedDescriptionKey: description ?: @"Unknown Quick Look error"}];
660734
}
735+
736+
BOOL draw_image_thumbnail(const ThumbnailDescriptor& descriptor, CGSize contextSize)
737+
{
738+
NSImage* previewImage = image_from_data_url(descriptor.previewImageDataUrl);
739+
if (previewImage == nil)
740+
{
741+
return NO;
742+
}
743+
744+
const NSRect bounds = NSMakeRect(0.0, 0.0, contextSize.width, contextSize.height);
745+
[[NSColor colorWithCalibratedRed:0.96 green:0.93 blue:0.88 alpha:1.0] setFill];
746+
NSRectFill(bounds);
747+
748+
const CGFloat margin = std::max<CGFloat>(12.0, contextSize.width * 0.06);
749+
const NSRect pageRect = NSInsetRect(bounds, margin, margin);
750+
fill_rounded_rect(pageRect, 18.0, [NSColor colorWithCalibratedRed:0.99 green:0.98 blue:0.96 alpha:1.0]);
751+
stroke_rounded_rect(pageRect, 18.0, 1.5, [NSColor colorWithCalibratedRed:0.84 green:0.81 blue:0.75 alpha:1.0]);
752+
753+
const CGFloat footerHeight = std::max<CGFloat>(34.0, pageRect.size.height * 0.18);
754+
const NSRect imageBounds = NSInsetRect(NSMakeRect(pageRect.origin.x + 10.0,
755+
pageRect.origin.y + footerHeight + 10.0,
756+
pageRect.size.width - 20.0,
757+
pageRect.size.height - footerHeight - 20.0), 4.0, 4.0);
758+
759+
fill_rounded_rect(imageBounds, 12.0, [NSColor colorWithCalibratedRed:0.95 green:0.93 blue:0.89 alpha:1.0]);
760+
stroke_rounded_rect(imageBounds, 12.0, 1.0, [NSColor colorWithCalibratedRed:0.87 green:0.83 blue:0.77 alpha:1.0]);
761+
762+
const NSRect fittedRect = aspect_fit_rect(previewImage.size, NSInsetRect(imageBounds, 8.0, 8.0));
763+
[previewImage drawInRect:fittedRect];
764+
765+
const NSRect footerRect = NSMakeRect(pageRect.origin.x,
766+
pageRect.origin.y,
767+
pageRect.size.width,
768+
footerHeight);
769+
fill_rounded_rect(footerRect, 18.0, [NSColor colorWithCalibratedRed:0.10 green:0.18 blue:0.26 alpha:0.92]);
770+
771+
draw_centered_string(nsstring_from_std(descriptor.title),
772+
NSMakeRect(footerRect.origin.x + 10.0,
773+
footerRect.origin.y + footerRect.size.height * 0.38,
774+
footerRect.size.width - 20.0,
775+
footerRect.size.height * 0.34),
776+
[NSFont systemFontOfSize:std::max<CGFloat>(11.0, pageRect.size.width * 0.07) weight:NSFontWeightBold],
777+
[NSColor whiteColor]);
778+
779+
draw_centered_string(nsstring_from_std(descriptor.subtitle),
780+
NSMakeRect(footerRect.origin.x + 12.0,
781+
footerRect.origin.y + footerRect.size.height * 0.10,
782+
footerRect.size.width - 24.0,
783+
footerRect.size.height * 0.24),
784+
[NSFont systemFontOfSize:std::max<CGFloat>(9.0, pageRect.size.width * 0.042) weight:NSFontWeightMedium],
785+
[NSColor colorWithCalibratedWhite:0.88 alpha:1.0]);
786+
787+
return YES;
788+
}
661789
}
662790

663791
@implementation TIVarsQuickLookSupport
@@ -718,6 +846,11 @@ + (BOOL)drawThumbnailForFileURL:(NSURL *)fileURL
718846
return NO;
719847
}
720848

849+
if (!descriptor.previewImageDataUrl.empty() && draw_image_thumbnail(descriptor, contextSize))
850+
{
851+
return YES;
852+
}
853+
721854
const NSRect bounds = NSMakeRect(0.0, 0.0, contextSize.width, contextSize.height);
722855
[[NSColor colorWithCalibratedRed:0.96 green:0.93 blue:0.88 alpha:1.0] setFill];
723856
NSRectFill(bounds);

0 commit comments

Comments
 (0)