@@ -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