Există un moment în viața oricărui dezvoltator când decizi să construiești ceva pentru tine. Nu pentru un client, nu pentru un proiect comercial, ci pentru problema ta recurentă. Pentru mine, acea problemă a fost conversia imaginilor. De fiecare dată când aveam nevoie să transform un PNG în JPG, să redimensionez o captură pentru un articol sau să generez un favicon ICO, ajungeam pe site-uri pline de reclame, cu limite de upload, cu întrebarea „ești sigur că vrei să încarci fișierul pe serverul nostru?"
Așa s-a născut ImgConvert, o aplicație nativă de macOS construită în SwiftUI, care face totul local, instant, fără upload, fără reclame, fără cont. Am dezvoltat-o pentru nevoile mele, dar în timp a devenit un instrument pe care îl folosesc zilnic. Acum o poți descărca gratuit de pe seotoolpro.ro/download-apps.
Descărcarea se face de pe pagina oficială a aplicațiilor dezvoltate de seotoolpro.ro. Accesează direct:
https://seotoolpro.ro/download-apps
Acolo vei găsi lista completă a aplicațiilor disponibile. Pentru ImgConvert, vei vedea toate versiunile lansate până acum, fiecare cu propriul buton de descărcare. Poți alege versiunea cea mai recentă, marcată cu badge-ul „ultima", sau o versiune anterioară dacă ai un motiv specific.
Fiecare versiune afișează dimensiunea fișierului, data lansării, numărul de descărcări și hash-ul SHA-256. După ce descarci fișierul ZIP, îl poți verifica independent rulând în Terminal:
shasum -a 256 imgconvert.zip
Dacă hash-ul calculat se potrivește cu cel afișat pe site, fișierul este autentic și nu a fost modificat pe drum.
Prima întrebare pe care mi-a pus-o lumea când am povestit despre proiect a fost simplă: „De ce să construiești o aplicație când există atâtea site-uri gratuite de conversie?" Răspunsul are trei componente.
Confidențialitatea. Când încarci o imagine pe un site de conversie, fișierul ajunge pe un server necunoscut. Poate fi analizat, stocat, folosit pentru antrenarea unor modele AI sau pur și simplu uitat acolo. Pentru capturi de ecran cu date de client, pentru fotografii personale sau pentru materiale interne, acest lucru este inacceptabil. ImgConvert procesează totul local. Fișierul nu părăsește niciodată Mac-ul.
Viteza. Un site de conversie implică upload, procesare pe server, download. Chiar și cu o conexiune rapidă, durează zeci de secunde pentru fișiere mari. ImgConvert face totul în memorie, pe dispozitiv. Conversia unui fișier de 20 MB durează sub o secundă.
Controlul. Aplicațiile web au limitări de dimensiune, de format, de număr de fișiere. O aplicație nativă nu are aceste constrângeri. Poți procesa orice fișier, în orice format suportat, fără să te îngrijorezi că serverul va respinge cererea.
Prima decizie tehnică a fost alegerea framework-ului pentru interfață. Am oscilat între AppKit, framework-ul tradițional al macOS, și SwiftUI, framework-ul modern introdus de Apple în 2019. Am ales SwiftUI pentru trei motive.
Primul: viteză de dezvoltare. SwiftUI permite construirea unei interfețe complete cu mult mai puțin cod decât AppKit. Un layout care în AppKit ar necesita sute de linii de cod, în SwiftUI se scrie în câteva zeci.
Al doilea: modelul declarativ. SwiftUI leagă automat starea aplicației de interfață. Când o variabilă se schimbă, view-ul se actualizează fără cod suplimentar. Aceasta a simplificat dramatic logica de previzualizare în timp real.
Al treilea: viitorul. Apple investește masiv în SwiftUI, iar AppKit este încet-încet înlocuit. Construind în SwiftUI, aplicația este pregătită pentru viitoarele versiuni de macOS.
AppKit rămâne folosit pentru procesarea imaginilor, pentru că oferă acces direct la NSBitmapImageRep, clasa care gestionează decodarea și recodarea imaginilor. Aceasta este combinația optimă: SwiftUI pentru interfață, AppKit pentru procesare.
Primul lucru pe care l-am definit este enum-ul ImgFormat, care descrie formatele suportate de aplicație. Fiecare format are propriul tip UTType, folosit pentru dialogurile de salvare, și o proprietate care indică dacă suportă control al calității.
nonisolated enum ImgFormat: String, CaseIterable, Identifiable {
case png = "PNG"
case jpg = "JPG"
case bmp = "BMP"
case ico = "ICO"
var id: String { rawValue }
var utType: UTType {
switch self {
case .png: return .png
case .jpg: return .jpeg
case .bmp: return .bmp
case .ico: return .ico
}
}
var supportsQuality: Bool { self == .jpg }
static func from(ext: String) -> ImgFormat? {
switch ext.lowercased() {
case "png": return .png
case "jpg", "jpeg": return .jpg
case "bmp": return .bmp
case "ico": return .ico
default: return nil
}
}
}
Am marcat enum-ul ca nonisolated pentru că este folosit atât pe main thread, cât și în task-uri de fundal. Fără această adnotare, compilatorul Swift 6 ar genera erori de concurență. Proprietatea supportsQuality este esențială pentru că doar JPG are control al compresiei; PNG, BMP și ICO sunt formate fără pierderi sau cu compresie fixă.
Inima aplicației este clasa ImageDoc, un ObservableObject care gestionează toată starea. Aici trăiesc imaginea originală, formatul selectat, calitatea, dimensiunile, previzualizarea și mesajele de eroare.
@MainActor
final class ImageDoc: ObservableObject {
@Published var original: NSImage?
@Published var originalSize: CGSize = .zero
@Published var originalByteCount: Int = 0
@Published var format: ImgFormat = .png
@Published var quality: Double = 85
@Published var keepAspect = true
@Published var width: Int = 0
@Published var height: Int = 0
@Published var preview: NSImage?
@Published var previewByteCount: Int = 0
@Published var isBusy = false
@Published var errorMessage: String?
private var updateTask: Task<Void, Never>?
var hasImage: Bool { original != nil }
var savedPercent: Int {
guard originalByteCount > 0, previewByteCount > 0 else { return 0 }
let saved = 1.0 - Double(previewByteCount) / Double(originalByteCount)
return max(0, Int((saved * 100).rounded()))
}
Am marcat clasa cu @MainActor pentru că toate proprietățile @Published trebuie actualizate pe main thread. Proprietatea calculată savedPercent oferă procentul de economie de spațiu, folosit în interfață pentru a arăta diferența dintre original și rezultat.
Aici se face toată munca grea. Funcția render creează un bitmap nou, desenează imaginea redimensionată, apoi o encodează în formatul țintă.
nonisolated enum ImageProcessor {
struct Result { let image: NSImage; let data: Data }
static func render(_ source: NSImage, width: Int, height: Int,
format: ImgFormat, quality: Double) -> Result? {
guard let bitmap = NSBitmapImageRep(
bitmapDataPlanes: nil,
pixelsWide: width,
pixelsHigh: height,
bitsPerSample: 8,
samplesPerPixel: 4,
hasAlpha: true,
isPlanar: false,
colorSpaceName: .deviceRGB,
bytesPerRow: 0,
bitsPerPixel: 0
) else { return nil }
bitmap.size = NSSize(width: width, height: height)
guard let context = NSGraphicsContext(bitmapImageRep: bitmap) else { return nil }
NSGraphicsContext.saveGraphicsState()
NSGraphicsContext.current = context
context.imageInterpolation = .high
if format == .jpg || format == .bmp {
NSColor.white.setFill()
NSRect(x: 0, y: 0, width: CGFloat(width), height: CGFloat(height)).fill()
}
source.draw(in: NSRect(x: 0, y: 0, width: CGFloat(width), height: CGFloat(height)),
from: NSRect(origin: .zero, size: source.size),
operation: .sourceOver, fraction: 1.0)
NSGraphicsContext.restoreGraphicsState()
let data: Data?
switch format {
case .png:
data = bitmap.representation(using: .png, properties: [:])
case .jpg:
data = bitmap.representation(using: .jpeg,
properties: [.compressionFactor: quality / 100.0])
case .bmp:
data = bitmap.representation(using: .bmp, properties: [:])
case .ico:
data = icoData(from: bitmap, size: width)
}
guard let data else { return nil }
let previewImage = NSImage(size: NSSize(width: width, height: height))
previewImage.addRepresentation(bitmap)
return Result(image: previewImage, data: data)
}
Trei detalii merită explicate. Primul: pentru JPG și BMP, desenăm un fundal alb înainte de a desena imaginea. Aceasta rezolvă problema transparenței, pentru că aceste formate nu suportă canal alfa, iar fără fundal alb zonele transparente ar apărea negre.
Al doilea: context.imageInterpolation = .high activează interpolarea de calitate înaltă. Fără această setare, imaginile micșorate arată pixelat, cu marginile zimțate. Cu ea, rezultatul păstrează detaliile și liniile curate.
Al treilea: pentru ICO, apelăm o funcție separată icoData care construiește structura binară a formatului.
Formatul ICO pare complicat, dar pentru ICO modern cu un singur frame PNG, structura este simplă: un header de 6 octeți, o intrare de director de 16 octeți și un frame PNG încapsulat.
static func icoData(from bitmap: NSBitmapImageRep, size: Int) -> Data? {
guard let png = bitmap.representation(using: .png, properties: [:]) else { return nil }
var header = Data()
header.append(contentsOf: [0, 0, 1, 0, 1, 0])
let w = UInt8(size >= 256 ? 0 : size)
let h = UInt8(size >= 256 ? 0 : size)
header.append(contentsOf: [w, h, 0, 0, 1, 0, 32, 0])
header.append(contentsOf: UInt32(png.count).littleEndianBytes)
header.append(contentsOf: UInt32(22).littleEndianBytes)
return header + png
}
Să descifrăm structura. Primii 6 octeți sunt header-ul ICONDIR: doi octeți zero (rezervat), apoi [1, 0] care indică tipul ICO, apoi [1, 0] care indică numărul de imagini (una singură în cazul nostru).
Următorii 16 octeți sunt intrarea de director. Primul octet este lățimea, al doilea înălțimea. Dacă dimensiunea este 256 sau mai mare, valoarea este 0 (convenție în formatul ICO). Urmează doi octeți zero pentru numărul de culori și rezervat, apoi [1, 0] pentru planurile de culoare, [32, 0] pentru biți per pixel, dimensiunea PNG în format little-endian pe 4 octeți și offset-ul datelor (22, pentru că header-ul plus intrarea de director au exact 22 de octeți).
Zona de drag and drop este primul lucru pe care utilizatorul îl vede. Am construit-o cu un view dedicat care gestionează atât tragerea fișierelor, cât și deschiderea unui dialog de selecție.
struct DropArea: View {
@ObservedObject var doc: ImageDoc
@State private var isTargeted = false
var body: some View {
VStack(spacing: 16) {
if let preview = doc.preview {
Image(nsImage: preview)
.resizable()
.aspectRatio(contentMode: .fit)
.clipShape(RoundedRectangle(cornerRadius: 14, style: .continuous))
.shadow(color: .black.opacity(0.18), radius: 14, y: 6)
} else {
VStack(spacing: 14) {
Image(systemName: "photo.badge.plus")
.font(.system(size: 32, weight: .medium))
Text("Trage imaginea aici")
.font(.title3.weight(.semibold))
Text("PNG · JPG · BMP · ICO · SVG")
.font(.callout)
.foregroundStyle(.secondary)
}
}
Button {
chooseFile()
} label: {
Label("Alege fișier…", systemImage: "folder")
}
}
.onDrop(of: [.fileURL], isTargeted: $isTargeted) { providers in
guard let provider = providers.first else { return false }
_ = provider.loadObject(ofClass: URL.self) { url, _ in
if let url {
Task { @MainActor in doc.open(url: url) }
}
}
return true
}
}
}
Border-ul punctat apare doar când utilizatorul trage un fișier deasupra zonei. Indicatorul de progres apare în colțul dreapta-sus când procesarea este în curs. Aceste micro-interacțiuni fac diferența între o aplicație care se simte vie și una care se simte statică.
Construirea ImgConvert nu a fost lipsită de provocări. Prima a fost gestionarea transparenței la conversia PNG → JPG. JPG nu suportă canal alfa, iar dacă nu completezi fundalul cu o culoare, rezultatul are zone negre acolo unde ar trebui să fie alb. Soluția este în codul de procesare: pentru JPG și BMP, desenăm un fundal alb înainte de a desena imaginea.
A doua provocare a fost performanța la fișiere mari. Imaginile de peste 20 MB blocau interfața pentru câteva secunde. Soluția a fost mutarea procesării pe un thread de fundal cu Task.detached, cu actualizarea interfeței pe main thread la final.
A treia provocare a fost anularea task-urilor. Când utilizatorul mișcă rapid slider-ul de calitate, se declanșează zeci de regenerări. Fără anulare, toate ar rula în paralel și ar consuma resurse inutil. Am rezolvat cu updateTask?.cancel() la începutul fiecărei regenerări.
După ce am construit aplicația, am realizat că distribuirea ei pe site este o problemă în sine. Un simplu link către un fișier ZIP nu îmi permitea să contorizez descărcările, să ofer versiuni multiple sau să afișez hash-ul SHA-256 pentru verificarea integrității.
Așa că am construit un sistem complet de distribuție, integrat în platforma AtoZ SEO Tools. Sistemul scanează automat folderul /apps/, detectează aplicațiile și versiunile, citește descrierea din numele fișierelor .nfo, calculează hash-ul SHA-256 și afișează totul pe pagina seotoolpro.ro/download-apps.
Fiecare descărcare este înregistrată într-un fișier log, iar statisticile se calculează automat: total descărcări, descărcări azi, descărcări per versiune. Nu ai nevoie de bază de date, nu ai nevoie de configurare manuală. Adaugi o aplicație nouă doar creând un folder, pui ZIP-ul, pui fișierul .nfo cu descrierea, și gata.
Sistemul este deschis și flexibil. Poți distribui orice tip de fișier: aplicații desktop, plugin-uri, teme, scripturi, template-uri, resurse digitale. Fiecare fișier are propriul contor, propria descriere, propriul hash.
Aplicația acoperă un flux complet de conversie și redimensionare, de la deschiderea unui fișier sursă până la salvarea rezultatului final. Suportă cinci formate de intrare — PNG, JPG, BMP, ICO și SVG — toate decodate nativ prin framework-ul AppKit al macOS.
Pentru ieșire, poți alege între patru formate: PNG cu suport pentru transparență, JPG cu control al calității de compresie, BMP necomprimat cu fundal alb automat, și ICO generat intern cu structură ICONDIR și un frame PNG încapsulat.
Pentru situațiile uzuale, aplicația oferă zece presetări de dimensiune: Web 600 × 400, YouTube Thumbnail 1280 × 720, YouTube Cover 2560 × 1440, Facebook Cover 820 × 312, OG Image 1200 × 630, Instagram Pătrat 1080 × 1080, Instagram Story 1080 × 1920, Favicon ICO 32 × 32, PWA Icon 192 × 192 și PWA Large 512 × 512.
ImgConvert rulează pe macOS recent (Sonoma sau mai nou) și este compatibil atât cu procesoare Intel, cât și cu Apple Silicon (M1, M2, M3, M4). Nu necesită librării externe sau instalare suplimentară. Spațiul necesar pe disc este de aproximativ 200 MB. Aplicația este complet în limba română.
ImgConvert a pornit ca un proiect personal pentru o problemă personală. A devenit un instrument folosit de comunitate și un exemplu de aplicație nativă macOS construită cu tehnologii moderne. Codul este curat, arhitectura este modulară, iar direcțiile de îmbunătățire sunt clare.
O poți descărca gratuit de pe seotoolpro.ro/download-apps, împreună cu alte aplicații pe care le dezvolt. Dacă lucrezi cu imagini pe Mac și vrei o alternativă locală, rapidă și sigură la site-urile de conversie, încearcă ImgConvert. Este gratuit, este nativ, este construit cu grijă pentru detalii.
Dacă vrei să explorezi mai multe despre dezvoltarea pentru macOS și instrumente utile, citește articolul nostru despre 5 trucuri HTML și CSS pe care dezvoltatorii le ascund și descoperă optimizare imagini web fără pierdere de calitate.
Surse externe:
seotoolpro.ro/download-apps — pagina oficială de descărcare
Apple Developer — documentația oficială SwiftUI
Apple Developer — documentația NSBitmapImageRep
Wikipedia — specificația formatului ICO
seotoolpro.ro — unealta de optimizare imagini pentru web
Lasă un comentariu