Sep
26

ImgConvert: convertorul de imagini pentru macOS construit în SwiftUI. Descarcă-l gratuit

09/26/2026 12:00 AM de Admin în Web development

Am construit ImgConvert pentru macOS în SwiftUI. Acum îl poți descărca de pe seotoolpro.ro

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.

De unde descarci ImgConvert

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.

De ce o aplicație nativă și nu un site web

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.

Cum am construit-o: SwiftUI plus AppKit

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.

Formatele suportate: enum-ul ImgFormat

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ă.

Modelul de date: clasa ImageDoc

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.

Procesarea imaginilor: funcția render

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.

Generarea ICO: structura binară explicată

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).

Interfața: zona de drag and drop

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ă.

Provocările tehnice întâlnite

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.

Sistemul de distribuție: cum ajunge aplicația la tine

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.

Ce poate face ImgConvert

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.

Cerințe de sistem

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ă.

Concluzie

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



Try Pro Calculatoare Inteligente Practice! CLICK HERE

Get MTDb.ro -Filme si Seriale, Trailere, Actori si Stiri din Cinema - CLICK HERE
lasă un comentariu
Vă rugăm postați comentariile aici.

Comentarii 0

Lasă un comentariu

0 / 2000

Comentariile sunt moderate înainte de publicare.

Se încarcă comentariile…