Pic Smaller — wsadowa kompresja obrazów bez wysyłania ich na serwer
Pic Smaller kompresuje JPEG, PNG, WebP, AVIF, GIF i SVG w całości w przeglądarce, na WebAssembly i Web Workerach. Pokazujemy, jak działa pod spodem, jakie ma limity i dlaczego warto postawić go u siebie.
Kompresja obrazów przed publikacją to zadanie, które w większości firm wygląda tak samo: ktoś wpisuje w wyszukiwarkę „compress jpg online", wrzuca pliki na pierwszą stronę z listy, pobiera wynik i wraca do pracy. Przez trzydzieści sekund nikt nie zastanawia się, gdzie te pliki właśnie poleciały.
A warto, bo bywają to zdjęcia z sesji dla klienta objęte umową, skany dokumentów, zrzuty ekranu z panelu administracyjnego albo zdjęcia produktowe z widocznymi twarzami pracowników. W każdym z tych przypadków wysłanie pliku na cudzy serwer jest powierzeniem danych, a nie operacją techniczną — z wszystkim, co się z tym wiąże po stronie formalnej.
Pic Smaller rozwiązuje ten problem inaczej: kompresuje pliki w całości w przeglądarce. Nie ma wysyłki, bo nie ma dokąd wysyłać — kodeki działają jako WebAssembly w Web Workerach na maszynie użytkownika.
Stan projektu
Repozytorium: joye61/pic-smaller. 2873 gwiazdki, 391 forków, start w kwietniu 2024, licencja MIT. Ostatni commit pochodzi z 16 sierpnia 2026, czyli sprzed niespełna miesiąca, i otwartych zgłoszeń jest tylko 12 — projekt jest utrzymywany i nie zbiera długu w zgłoszeniach.
Dwie rzeczy trzeba jednak powiedzieć wprost. Pierwsza: autor jest jeden. Wszystkie świeże commity są jego. To nie zarzut wobec kodu, tylko fakt do wpisania w ocenę ryzyka, jeśli narzędzie ma trafić do firmowego obiegu. Druga: nie ma żadnych wydań ani tagów. W package.json stoi wersja 1.1.0, ale w repozytorium nie znajdziesz nic, co pozwoliłoby przypiąć konkretny stan kodu. Budując obraz Dockera, budujesz to, co akurat jest na gałęzi master. Przy wdrożeniu firmowym warto samodzielnie przypiąć commit i zapisać jego skrót.
Dokumentacja jest po angielsku, z tłumaczeniami README na osiem języków — chiński uproszczony i tradycyjny, japoński, koreański, francuski, hiszpański, perski i turecki. Interfejs ma dziewięć lokalizacji. Polskiej wśród nich nie ma, a plik lokalizacji waży około 4,4 kB — jeśli ktoś szuka drobnego, wdzięcznego wkładu w open source, to jest dobry kandydat.
Model biznesowy, o którym warto wiedzieć
README zaczyna się dużą ramką reklamującą „Pic Smaller Desktop — The Flagship Edition": osobną aplikację natywną z obsługą ponad szesnastu formatów, przetwarzaniem całych katalogów i zestawem narzędzi AI do usuwania tła, usuwania znaków wodnych i skalowania obrazów w górę. Ta aplikacja żyje pod własnym adresem i nie jest częścią tego repozytorium.
Czyli układ jest taki: aplikacja webowa jest w pełni otwarta na MIT, a wersja desktopowa jest osobnym, zamkniętym produktem. To uczciwe rozwiązanie i lepsze niż okrajanie wersji otwartej — dostajesz działające narzędzie bez haczyków, a autor ma z czego finansować pracę. Warto tylko nie pomylić jednego z drugim przy czytaniu listy funkcji: wszystko, co opisuję poniżej, dotyczy otwartej wersji webowej.
Co potrafi
- Wsadowa kompresja JPEG, PNG, WebP, GIF, SVG i AVIF.
- Lokalne dekodowanie HEIC i HEIF z eksportem do JPEG, PNG, WebP lub AVIF — czyli obsługa zdjęć wprost z iPhone'a.
- Konwersja formatów, zmiana rozmiaru, przycinanie i ustawienia jakości specyficzne dla konkretnego kodera.
- Dodawanie plików przez okno wyboru, wybór całego katalogu, przeciąganie i upuszczanie oraz wklejanie ze schowka.
- Porównanie oryginału z wynikiem w interaktywnym widoku z suwakiem.
- Pobranie pojedynczych wyników albo całej partii jako archiwum ZIP.
W kodzie jest też stała PAPER_SIZES obsługująca przycinanie do formatów papieru wraz z orientacją — przydatne, gdy obrazy mają trafić do druku, a nie na stronę.
Jak to działa pod spodem
Ta część jest ciekawsza niż lista funkcji, bo tłumaczy, czemu można temu narzędziu ufać.
Kompresję wykonują prawdziwe, uznane kodeki skompilowane do WebAssembly. Zależności są jawne: @squoosh-kit/mozjpeg dla JPEG, @squoosh-kit/oxipng i @squoosh-kit/imagequant dla PNG, @squoosh-kit/avif dla AVIF, gifsicle-wasm-browser dla GIF-ów, svgo dla SVG i heic-to do dekodowania HEIC oraz HEIF. To nie są własne, amatorskie implementacje — to te same silniki, które stoją za Squooshem i za narzędziami wiersza poleceń, których pewnie już używasz.
Istotny szczegół dotyczy tego, skąd te kodeki są ładowane. Skrypt scripts/prepare-codecs.mjs uruchamiany przed każdym budowaniem kopiuje pliki .js i .wasm z katalogu node_modules do public/codecs. Oznacza to, że WebAssembly jest serwowane z twojego własnego origin, a nie ściągane z zewnętrznego CDN-a w czasie działania. Dla obietnicy prywatności to warunek konieczny, a dla wdrożenia za restrykcyjną polityką CSP — spora wygoda.
Współbieżność jest wyliczana ostrożnie i z komentarzem wyjaśniającym powód:
function getConcurrency(): number {
const cores = globalThis.navigator?.hardwareConcurrency ?? 4;
return Math.min(MAX_CONCURRENCY, Math.max(1, cores - 1));
}MAX_CONCURRENCY wynosi 3, a komentarz w kodzie mówi, że limit istnieje po to, by ograniczyć zużycie pamięci przez WebAssembly. Od liczby rdzeni odejmowany jest jeden, żeby zostawić maszynę responsywną, a na jednordzeniowych komputerach zadania idą pojedynczo. To rozsądna decyzja: kompresja wsadowa w przeglądarce łatwo doprowadza kartę do zabicia przez system, jeśli ktoś odpali osiem równoległych instancji kodera AVIF.
Podoba mi się jeszcze jeden drobiazg. Wsparcie dla AVIF jest sprawdzane w czasie działania, próbą zapisu jednopikselowego obrazu na OffscreenCanvas, a gdy próba się nie udaje, format zostaje usunięty z listy opcji — z komentarzem, że interfejs nie powinien wprowadzać użytkownika w błąd. Narzędzie nie oferuje więc formatu, którego przeglądarka nie obsłuży, zamiast pozwolić ci wybrać go i dopiero potem zawieść.
Zachowania zależne od formatu, których nie zgadniesz z listy funkcji
Czytanie pliku src/engines/handler.ts ujawnia trzy rzeczy, których nie ma w opisie funkcji, a które zmieniają sposób pracy z narzędziem.
Animowany WebP i animowany AVIF są odrzucane. Przy kompresji tych dwóch formatów kod najpierw sprawdza, czy plik jest animacją, i jeśli tak — przerywa błędem animated-unsupported. Co ważne, sprawdzenie nie opiera się na rozszerzeniu ani na typie MIME, a na odczycie zawartości kontenera. Dla WebP kod weryfikuje nagłówek RIFF, po czym przechodzi po kolejnych fragmentach w poszukiwaniu bloku ANIM albo ANMF. Dla AVIF parsuje pudełko ftyp i szuka marki avis, obsługując przy tym wariant z rozszerzonym rozmiarem pudełka. To solidna robota — wykrycie jest rzetelne, a nie zgadywane — ale wniosek praktyczny brzmi: animacje kompresujesz tu wyłącznie jako GIF-y, przez gifsicle. Animowany WebP, coraz popularniejszy zamiennik GIF-a, trzeba obsłużyć czym innym.
Zdjęcie z iPhone'a bez wybranego formatu wyjściowego wróci nietknięte. To zachowanie warto znać, bo wygląda jak awaria, a jest decyzją projektową. Gdy wrzucasz plik HEIC lub HEIF i nie wskażesz formatu docelowego, kod zwraca oryginalny plik z flagą preservedOriginal — czyli nie kompresuje niczego. Powód jest sensowny: nie ma dokąd zapisać wyniku, bo przeglądarki nie kodują do HEIC, a milcząca zamiana formatu byłaby gorsza od bezczynności. Dla podglądu narzędzie ustawia sobie JPEG jako format tymczasowy. Praktycznie: przy plikach z iPhone'a zawsze wybierz format wyjściowy — JPEG, PNG, WebP albo AVIF — inaczej dostaniesz z powrotem to, co wrzuciłeś, i będziesz się zastanawiać, czemu rozmiar się nie zmienił.
SVG nie podlega konwersji formatu. Komentarz w kodzie mówi to wprost. SVG idzie osobną ścieżką: jest optymalizowany przez SVGO jako tekst, a nie rasteryzowany przez canvas. Jeśli wymiary nie są znane z wątku głównego, kod czyta zawartość pliku i wyciąga je z samego znacznika. Efekt jest taki, jakiego się chce — SVG zostaje SVG-iem i zmniejsza się przez usunięcie zbędnego znacznika — ale nie licz na wyeksportowanie ikony wektorowej do PNG w tym samym przebiegu.
Limity, o których kod mówi wprost
Przetwarzanie po stronie klienta ma sufit i warto znać jego wysokość przed pierwszym większym zadaniem. Stałe z pliku src/engines/ImageBase.ts:
MAX_CANVAS_DIMENSION = 16384— to limit wymiaru elementu canvas. Panorama albo skan wielkoformatowy o krawędzi dłuższej niż 16384 piksele wychodzi poza możliwości tej ścieżki przetwarzania.MAX_FILE_SIZE_WARNING = 50 * 1024 * 1024— powyżej 50 MB na plik narzędzie ostrzega. To nie twardy zakaz, ale sygnał, że wchodzisz w obszar, w którym pamięć przeglądarki zaczyna być realnym ograniczeniem.OXI_PNG_LEVEL = 2iOXI_PNG_EXTREME_LEVEL = 6— dwa poziomy optymalizacji PNG. Tryb ekstremalny daje mniejsze pliki za cenę wyraźnie dłuższego czasu.PREVIEW_MAX_SIZE = 256— miniatury podglądu liczone są w osobnym workerze, żeby nie blokować kolejki kompresji.
Praktyczny wniosek: to narzędzie jest zrobione do partii kilkudziesięciu zdjęć produktowych albo zrzutów ekranu, nie do przemiału archiwum na dziesięć tysięcy plików RAW. Do tego drugiego zadania i tak sięgniesz po skrypt z mozjpeg i oxipng w wierszu poleceń — czyli po dokładnie te same kodeki, tylko bez przeglądarki w środku.
Postaw to u siebie — i dlaczego to ma znaczenie
Tu dochodzimy do rzeczy, którą trzeba powiedzieć precyzyjnie, bo od niej zależy cała wartość tego narzędzia w kontekście firmowym.
Zdanie „pliki nigdy nie są wysyłane na serwer aplikacji" jest prawdziwe i można je sprawdzić w kodzie. Ale to własność kodu, a kod dostarcza ci serwer WWW. Korzystając z hostowanej wersji, przy każdym wejściu na stronę ufasz temu, co ten serwer właśnie odesłał — i choć nie ma żadnych podstaw sądzić, że wysyła coś innego, to zaufanie odnawiasz przy każdym ładowaniu strony i nie masz nad nim kontroli. Zakładka sieciowa w narzędziach deweloperskich pokaże ci prawdę o dzisiejszym ładowaniu, nie o następnym.
Uruchomienie własnej instancji zamienia to zaufanie na weryfikowalność: kod, który przeglądarka wykonuje, pochodzi z obrazu, który sam zbudowałeś z commita, który możesz przeczytać. Dopiero wtedy argument „przetwarzamy lokalnie, nie powierzamy danych" jest argumentem, który da się obronić przed kimkolwiek pytającym.
Dockerfile jest napisany porządnie i widać w nim rękę kogoś, kto wdrażał produkcyjnie: budowanie wieloetapowe, wyjście standalone Next.js, tini jako proces inicjujący do obsługi sygnałów, uruchamianie jako nieuprzywilejowany użytkownik node, STOPSIGNAL SIGTERM, health check co trzydzieści sekund i wyłączona telemetria Next.js przez NEXT_TELEMETRY_DISABLED=1.
Jeszcze lepsze jest jednak polecenie uruchomienia zaproponowane w README, bo autor nie poprzestał na docker run -p 3000:3000:
docker build --pull -t pic-smaller:latest .
docker run -d \
--name pic-smaller \
--restart unless-stopped \
--read-only \
--tmpfs /tmp:rw,noexec,nosuid,size=64m \
--cap-drop ALL \
--security-opt no-new-privileges \
-p 127.0.0.1:3000:3000 \
pic-smaller:latestSystem plików tylko do odczytu, katalog tymczasowy jako tmpfs z zakazem wykonywania, zrzucone wszystkie uprawnienia jądra, zablokowana eskalacja przywilejów i wiązanie portu wyłącznie na pętli lokalnej. README dodaje, że do publicznego dostępu należy postawić przed tym odwrotne proxy terminujące TLS — Caddy, nginx albo Traefik — i że przedrostek 127.0.0.1: usuwa się tylko wtedy, gdy bezpośrednie wystawienie do sieci jest świadomą decyzją. To poziom staranności, jakiego przy projektach jednoosobowych zwyczajnie się nie spotyka, i wypada to odnotować.
Jest też druga droga wdrożenia, jeszcze prostsza. Komenda npm run build:pages eksportuje w pełni statyczną wersję strony do katalogu out. Statyczne pliki możesz położyć na czymkolwiek — na Cloudflare Pages, na S3, na własnym nginxie w firmowej sieci, także takiej bez wyjścia do internetu. Skoro całe przetwarzanie dzieje się w przeglądarce, aplikacja nie potrzebuje żadnego backendu, a sekcja README o sekretach mówi wprost: aplikacja webowa nie wymaga żadnych kluczy API.
Wymagania budowania to Node.js 22 LTS lub nowszy (package.json zawęża to do przedziału od 22 do wersji poniżej 25) oraz npm w wersji 10 lub nowszej. Do współpracy autor prosi o gałęzie odbijane od develop, uruchomienie npm test, npm run lint i odpowiedniego budowania produkcyjnego. Testy istnieją — trzy pliki w katalogu tests pokrywają opcje, kolejkę i funkcje pomocnicze — więc nie jest to projekt bez żadnej siatki bezpieczeństwa.
A co z RODO
Skoro obrazy nie opuszczają urządzenia, to przy własnej instancji nie powstaje powierzenie przetwarzania danych zewnętrznemu dostawcy — bo nie ma zewnętrznego dostawcy w tej operacji. Dla firm, które regularnie obrabiają zdjęcia z ludźmi albo skany dokumentów, to realna różnica względem losowego kompresora online, przy którym trzeba by zapytać, gdzie stoją serwery, jak długo trzymane są pliki i czy jest umowa powierzenia.
Warto natomiast pamiętać o dwóch rzeczach. Po pierwsze, samo przetwarzanie obrazu lokalnie nie zwalnia z obowiązków wobec danych na tym obrazie — jeśli kompresujesz zdjęcia osób, to nadal przetwarzasz ich dane, tylko robisz to sam. Po drugie, kompresja usuwa piksele, ale niekoniecznie metadane. Zanim uznasz gotowy plik za bezpieczny do publikacji, sprawdź, co z danymi EXIF: lokalizacją GPS, numerem seryjnym aparatu, datą wykonania. To pytanie warto zadać eksperymentalnie na własnych plikach, a nie założyć odpowiedź — bo zachowanie zależy od formatu wejściowego, formatu wyjściowego i użytej ścieżki przetwarzania.
Pułapki w jednym miejscu
- Brak wydań i tagów. Budujesz to, co jest na
master— przypnij commit samodzielnie i zapisz jego skrót. - Jeden autor. Wpisz to w ocenę ryzyka, jeśli narzędzie wchodzi do firmowego obiegu.
- Obietnica lokalnego przetwarzania jest weryfikowalna tylko wtedy, gdy kontrolujesz dostarczanie kodu. Na hostowanej wersji ufasz każdemu kolejnemu ładowaniu strony.
- Animowany WebP i animowany AVIF są odrzucane błędem
animated-unsupported. Animacje przejdą tylko jako GIF. - HEIC i HEIF bez wybranego formatu wyjściowego wracają nieskompresowane, z zachowanym oryginałem. Zawsze wskaż format docelowy.
- SVG nie podlega konwersji formatu — jest optymalizowany przez SVGO jako tekst, nie rasteryzowany.
- Sufit 16384 pikseli na wymiar canvas — bardzo duże panoramy i skany wielkoformatowe wypadają z zakresu.
- Ostrzeżenie powyżej 50 MB na plik. Powyżej tej wartości pamięć przeglądarki staje się realnym ograniczeniem.
- Współbieżność zatrzymana na trzech zadaniach, celowo, żeby ograniczyć zużycie pamięci przez WebAssembly. Nie licz na wykorzystanie wszystkich rdzeni.
- AVIF zależy od przeglądarki — jeśli nie jest wspierany, opcja po prostu nie istnieje w interfejsie.
- Metadanych nie zakładaj — sprawdź. Kompresja to nie to samo co czyszczenie EXIF.
- Brak polskiej lokalizacji — interfejs zobaczysz po angielsku albo w jednym z ośmiu innych języków.
- Wersja desktopowa z narzędziami AI to osobny, zamknięty produkt, nie część tego repozytorium.
Podsumowanie
Pic Smaller jest przykładem projektu, który robi jedną rzecz i robi ją tak, jak powinna być zrobiona: bierze kodeki, którym branża już ufa, uruchamia je tam, gdzie są dane, i nie prosi o nic w zamian — ani o konto, ani o klucz API, ani o wysłanie plików gdziekolwiek.
- Zero wysyłki plików — całe przetwarzanie w przeglądarce, na Web Workerach i WebAssembly, z kodekami serwowanymi z własnego origin.
- Uznane silniki kompresji — mozjpeg, OxiPNG, ImageQuant, koder AVIF, gifsicle i SVGO, a nie własne implementacje.
- Szeroki zakres formatów z dekodowaniem HEIC i HEIF, czyli obsługą zdjęć wprost z iPhone'a bez pośredniej konwersji.
- Dwie drogi wdrożenia u siebie — obraz Dockera z porządnym hartowaniem albo w pełni statyczny eksport, który zadziała nawet w sieci bez wyjścia do internetu.
- Świadome limity zapisane w kodzie — współbieżność, rozmiar pliku, wymiar canvas — zamiast cichego padania przy dużych zadaniach.
- Brak wymogu jakichkolwiek sekretów, co czyni wdrożenie i utrzymanie właściwie bezobsługowym.
Projekt jest wydany na licencji MIT — najbardziej permisywnej z popularnych. Możesz go używać komercyjnie, modyfikować, wystawić pod własną marką w firmowym intranecie i nie masz obowiązku udostępniania zmian. Warto tylko rozdzielić dwie rzeczy przy podejmowaniu decyzji: otwarta na MIT jest aplikacja webowa z tego repozytorium, a wersja desktopowa z narzędziami AI jest osobnym produktem komercyjnym. Dla zadania „skompresuj partię obrazów, nie wypuszczając ich z firmy" ta otwarta część wystarcza w całości.