// // MinecraftPackageThumbnailRenderer.swift // MinecraftPackageThumbnailExtension // import AppKit import Foundation enum MinecraftPackageThumbnailRenderer { nonisolated static func makeThumbnail( for inspection: MinecraftPackageInspector.InspectionResult, size: CGSize, scale: CGFloat = 1 ) -> CGImage? { let pixelSize = CGSize( width: max(size.width * scale, 1), height: max(size.height * scale, 1) ) guard let bitmap = NSBitmapImageRep( bitmapDataPlanes: nil, pixelsWide: max(Int(pixelSize.width.rounded(.up)), 1), pixelsHigh: max(Int(pixelSize.height.rounded(.up)), 1), bitsPerSample: 8, samplesPerPixel: 4, hasAlpha: true, isPlanar: false, colorSpaceName: .deviceRGB, bytesPerRow: 0, bitsPerPixel: 0 ) else { return nil } bitmap.size = pixelSize NSGraphicsContext.saveGraphicsState() defer { NSGraphicsContext.restoreGraphicsState() } guard let context = NSGraphicsContext(bitmapImageRep: bitmap) else { return nil } NSGraphicsContext.current = context let rect = CGRect(origin: .zero, size: pixelSize) if let iconURL = inspection.iconURL, let iconImage = NSImage(contentsOf: iconURL) { drawSquareThumbnail(iconImage, in: rect) } else { drawFallbackThumbnail(for: inspection.contentType, in: rect) } context.flushGraphics() return bitmap.cgImage } nonisolated private static func drawSquareThumbnail(_ image: NSImage, in rect: CGRect) { NSColor.clear.setFill() rect.fill() let cornerRadius = min(rect.width, rect.height) * 0.10 let clipRect = rect.insetBy(dx: max(rect.width * 0.02, 1), dy: max(rect.height * 0.02, 1)) let clipPath = NSBezierPath( roundedRect: clipRect, xRadius: cornerRadius, yRadius: cornerRadius ) clipPath.addClip() let imageSize = image.size guard imageSize.width > 0, imageSize.height > 0 else { image.draw(in: clipRect) return } let scale = max(clipRect.width / imageSize.width, clipRect.height / imageSize.height) let drawSize = CGSize(width: imageSize.width * scale, height: imageSize.height * scale) let drawRect = CGRect( x: clipRect.midX - drawSize.width / 2, y: clipRect.midY - drawSize.height / 2, width: drawSize.width, height: drawSize.height ) image.draw(in: drawRect) NSColor.black.withAlphaComponent(0.10).setStroke() let borderPath = NSBezierPath( roundedRect: clipRect.insetBy(dx: 0.5, dy: 0.5), xRadius: max(cornerRadius - 0.5, 0), yRadius: max(cornerRadius - 0.5, 0) ) borderPath.lineWidth = max(1, rect.width * 0.01) borderPath.stroke() } nonisolated private static func drawFallbackThumbnail( for contentType: MinecraftContentType, in rect: CGRect ) { let path = NSBezierPath( roundedRect: rect, xRadius: rect.width * 0.10, yRadius: rect.width * 0.10 ) let gradient = NSGradient(colors: backgroundColors(for: contentType)) gradient?.draw(in: path, angle: -45) let badgeRect = rect.insetBy(dx: rect.width * 0.18, dy: rect.height * 0.18) drawVoxelBadge(in: badgeRect) NSColor.black.withAlphaComponent(0.10).setStroke() path.lineWidth = max(1, rect.width * 0.01) path.stroke() } nonisolated private static func drawVoxelBadge(in rect: CGRect) { let topFace = NSBezierPath() topFace.move(to: CGPoint(x: rect.midX, y: rect.maxY)) topFace.line(to: CGPoint(x: rect.maxX, y: rect.maxY - rect.height * 0.18)) topFace.line(to: CGPoint(x: rect.midX, y: rect.maxY - rect.height * 0.36)) topFace.line(to: CGPoint(x: rect.minX, y: rect.maxY - rect.height * 0.18)) topFace.close() NSColor(calibratedRed: 0.78, green: 0.94, blue: 0.63, alpha: 1).setFill() topFace.fill() let leftFace = NSBezierPath() leftFace.move(to: CGPoint(x: rect.minX, y: rect.maxY - rect.height * 0.18)) leftFace.line(to: CGPoint(x: rect.midX, y: rect.maxY - rect.height * 0.36)) leftFace.line(to: CGPoint(x: rect.midX, y: rect.minY)) leftFace.line(to: CGPoint(x: rect.minX, y: rect.minY + rect.height * 0.18)) leftFace.close() NSColor(calibratedRed: 0.34, green: 0.61, blue: 0.24, alpha: 1).setFill() leftFace.fill() let rightFace = NSBezierPath() rightFace.move(to: CGPoint(x: rect.maxX, y: rect.maxY - rect.height * 0.18)) rightFace.line(to: CGPoint(x: rect.midX, y: rect.maxY - rect.height * 0.36)) rightFace.line(to: CGPoint(x: rect.midX, y: rect.minY)) rightFace.line(to: CGPoint(x: rect.maxX, y: rect.minY + rect.height * 0.18)) rightFace.close() NSColor(calibratedRed: 0.14, green: 0.34, blue: 0.13, alpha: 1).setFill() rightFace.fill() NSColor.black.withAlphaComponent(0.18).setStroke() [topFace, leftFace, rightFace].forEach { $0.lineWidth = max(1, rect.width * 0.02) $0.stroke() } } nonisolated private static func backgroundColors(for contentType: MinecraftContentType) -> [NSColor] { switch contentType { case .world: return [ NSColor(calibratedRed: 0.18, green: 0.48, blue: 0.25, alpha: 1), NSColor(calibratedRed: 0.47, green: 0.75, blue: 0.31, alpha: 1) ] case .behaviorPack: return [ NSColor(calibratedRed: 0.58, green: 0.29, blue: 0.15, alpha: 1), NSColor(calibratedRed: 0.86, green: 0.60, blue: 0.22, alpha: 1) ] case .resourcePack: return [ NSColor(calibratedRed: 0.12, green: 0.38, blue: 0.66, alpha: 1), NSColor(calibratedRed: 0.31, green: 0.68, blue: 0.85, alpha: 1) ] case .skinPack: return [ NSColor(calibratedRed: 0.53, green: 0.19, blue: 0.43, alpha: 1), NSColor(calibratedRed: 0.84, green: 0.39, blue: 0.62, alpha: 1) ] case .worldTemplate: return [ NSColor(calibratedRed: 0.23, green: 0.23, blue: 0.54, alpha: 1), NSColor(calibratedRed: 0.53, green: 0.52, blue: 0.89, alpha: 1) ] } } }