//
// HTMLImageEmbedder.swift
// readeck
//
// Created by Ilyas Hallak on 30.11.25.
//
import Foundation
import Kingfisher
/// Utility for embedding images as Base64 data URIs in HTML
struct HTMLImageEmbedder {
private let imageExtractor = HTMLImageExtractor()
/// Embeds all images in HTML as Base64 data URIs for offline viewing
/// - Parameter html: The HTML string containing image tags
/// - Returns: Modified HTML with images embedded as Base64
func embedBase64Images(in html: String) async -> String {
Logger.sync.info("🔄 Starting Base64 image embedding for offline HTML")
var modifiedHTML = html
let imageURLs = imageExtractor.extract(from: html)
Logger.sync.info("📊 Found \(imageURLs.count) images to embed")
var stats = EmbedStatistics()
for (index, imageURL) in imageURLs.enumerated() {
Logger.sync.debug("Processing image \(index + 1)/\(imageURLs.count): \(imageURL)")
guard let url = URL(string: imageURL) else {
Logger.sync.warning("❌ Invalid URL: \(imageURL)")
stats.failedCount += 1
continue
}
// Try to get image from Kingfisher cache
guard let image = await retrieveImageFromCache(url: url) else {
Logger.sync.warning("❌ Image not found in cache: \(imageURL)")
stats.failedCount += 1
continue
}
// Convert to Base64 and embed
if let base64DataURI = convertToBase64DataURI(image: image) {
let beforeLength = modifiedHTML.count
modifiedHTML = modifiedHTML.replacingOccurrences(of: imageURL, with: base64DataURI)
let afterLength = modifiedHTML.count
if afterLength > beforeLength {
Logger.sync.debug("✅ Embedded image \(index + 1) as Base64")
stats.successCount += 1
} else {
Logger.sync.warning("⚠️ Image URL found but not replaced in HTML: \(imageURL)")
stats.failedCount += 1
}
} else {
Logger.sync.warning("❌ Failed to convert image to Base64: \(imageURL)")
stats.failedCount += 1
}
}
logEmbedResults(stats: stats, originalSize: html.utf8.count, finalSize: modifiedHTML.utf8.count)
return modifiedHTML
}
// MARK: - Private Helper Methods
private func retrieveImageFromCache(url: URL) async -> KFCrossPlatformImage? {
await withCheckedContinuation { continuation in
KingfisherManager.shared.cache.retrieveImage(forKey: url.cacheKey) { result in
switch result {
case .success(let cacheResult):
continuation.resume(returning: cacheResult.image)
case .failure(let error):
Logger.sync.error("❌ Kingfisher cache retrieval error: \(error.localizedDescription)")
continuation.resume(returning: nil)
}
}
}
}
private func convertToBase64DataURI(image: KFCrossPlatformImage) -> String? {
guard let imageData = image.jpegData(compressionQuality: 0.85) else {
return nil
}
let base64String = imageData.base64EncodedString()
return "data:image/jpeg;base64,\(base64String)"
}
private func logEmbedResults(stats: EmbedStatistics, originalSize: Int, finalSize: Int) {
let total = stats.successCount + stats.failedCount
let growth = finalSize - originalSize
Logger.sync.info("✅ Base64 embedding complete: \(stats.successCount) succeeded, \(stats.failedCount) failed out of \(total) images")
Logger.sync.info("📈 HTML size: \(originalSize) → \(finalSize) bytes (growth: \(growth) bytes)")
}
}
// MARK: - Helper Types
private struct EmbedStatistics {
var successCount = 0
var failedCount = 0
}