Refactorings: - Extract HTMLImageEmbedder and HTMLImageExtractor utilities - Create UseCases for cached data access (GetCachedBookmarksUseCase, GetCachedArticleUseCase) - Create CreateAnnotationUseCase to remove API dependency from ViewModel - Simplify CachedAsyncImage by extracting helper methods - Fix Kingfisher API compatibility (Source types, Result handling) - Add documentation to OfflineCacheSyncUseCase - Remove unused TestView from production code Enforces Clean Architecture: - ViewModels now only use UseCases, no direct Repository or API access - All data layer access goes through Domain layer
108 lines
3.9 KiB
Swift
108 lines
3.9 KiB
Swift
//
|
|
// 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
|
|
}
|