Skompresowany obraz jest większy? Trzy prawdziwe przyczyny
Kompresor najprawdopodobniej działa poprawnie. Kiedy kompresja obrazu nie działa, czyli plik wyjściowy ma ten sam rozmiar albo jest wyraźnie większy od wejściowego, przyczyna niemal zawsze sprowadza się do jednej z trzech rzeczy. Zepsute narzędzie nie jest żadną z nich.
Najpierw ta najczęstsza. Każde narzędzie, które kompresuje przez element canvas przeglądarki, odrzuca typ koloru pliku PNG i koduje każdy piksel od nowa jako 32-bitowy RGBA. Przepuściliśmy wszystkie 83 karty Open Graph tej witryny w formacie PNG przez canvas.toBlob('image/png') w Chrome 151.0.0.0. Wszystkie 83 wróciły większe. Mediana przyrostu to +76,6%, najmniejszy wynik +35,6%, największy +237,2%.
Druga przyczyna: plik JPEG jest już skompresowany. Po pięciu rundach przy jakości 0,8 rozmiar przestaje się zmieniać po drugim przebiegu, a każdy kolejny i tak psuje obraz.
Trzecia: plik PNG przeszedł już kwantyzację. Nie została w nim żadna nadmiarowość kolorów, którą drugi przebieg mógłby usunąć.
Żadnego z tych przypadków nie naprawia przesunięcie suwaka jakości w dół. Rozwiązaniem jest inny format albo inny enkoder. Te same 83 pliki PNG przekonwertowane do WebP przy jakości 0,8 zmniejszyły się co do jednego, z medianą −94,2%.
Skąd pochodzą te liczby. Chrome 151.0.0.0 sterowany przez Playwright, macOS 26.5.2, Node v25.8.2, upng-js 2.1.0, ImageMagick. Zestaw 83 plików to wszystkie pliki PNG z katalogu
public/ogtej witryny, a nie próbka z niego. Cztery dodatkowe pliki kontrolne o wymiarach 1200×630 i 800×600 pokrywają osobno przypadki zdjęcia, grafiki, palety i JPEG.
1. Kompresja obrazu nie działa: diagnoza w trzydzieści sekund
Wystarczy znaleźć swój wiersz i przejść do wskazanej sekcji.
| Co trafiło na wejście | Co wyszło | Przyczyna | Do przeczytania |
|---|---|---|---|
| Zrzut ekranu albo grafika PNG | Większy od oryginału | canvas przekodował go na 32-bitowy RGBA | Sekcja 2 |
| Zdjęcie JPEG | Większe od oryginału | Zostało zapisane jako PNG | Sekcja 2 |
| Zdjęcie JPEG | Prawie bez zmian | Rozmiar osiągnął już plateau | Sekcja 3 |
| PNG przepuszczony już przez kompresor | Bez zmian albo nieco większy | Nie ma już zapasu | Sekcja 4 |
| Dowolny plik | Mniejszy, ale nieostry albo o zmienionych barwach | Strata pokoleniowa albo usunięty profil ICC | Sekcje 3 i 6 |
Wiersze nie wykluczają się wzajemnie. Zdjęcie JPEG wrzucone do eksportera PNG opartego na canvas trafia od razu w dwa pierwsze. Tak właśnie plik o rozmiarze 47 828 bajtów zamienia się w plik o rozmiarze 809 415 bajtów.
Jeśli diagnoza nie jest potrzebna i liczy się tylko mniejszy plik: nasz kompresor obrazów używa kwantyzującego enkodera PNG zamiast przejścia przez canvas i odrzuca własny wynik zawsze, gdy nie jest on mniejszy od pliku wejściowego.
2. Przyczyna pierwsza: canvas przeglądarki zawsze zapisuje 32-bitowy RGBA
Co canvas.toBlob() naprawdę robi z plikiem PNG
Po narysowaniu obrazu na canvas przeglądarka dekoduje go do płaskiego bufora RGBA: cztery bajty na piksel, bez palety, bez sztuczek z głębią bitową, bez metadanych. Nie ma tu żadnego etapu kompresji, jest dekodowanie i ponowne kodowanie, a wszystko, co nie jest wartością piksela, ginie po drodze. HTMLCanvasElement.toBlob() koduje ten bufor od zera. Specyfikacja PNG definiuje sześć typów koloru, a enkoder PNG może swobodnie wybrać najtańszy z nich, który wystarczy do reprezentacji obrazu. Enkoder canvas w Chrome nie wybiera. Zawsze emituje color type 6.
| Typ koloru na wejściu | Co zwraca canvas.toBlob('image/png') |
|---|---|
| RGB (color type 2) | RGBA (color type 6) |
| Paleta (color type 3) | RGBA (color type 6) |
| JPEG (nie ma typu koloru PNG) | RGBA (color type 6) |
Da się to odczytać wprost z bajtów. Bajt 25 pliku PNG to głębia bitowa, a bajt 26 to typ koloru, oba w chunku IHDR:
xxd -s 24 -l 2 -p suspect.png
# 0806 -> głębia bitowa 8, color type 6 (RGBA)
Wszystkie trzy typy wejścia dały depth=8 type=6. Obraz z 64-kolorową paletą przechowuje jeden bajt na piksel plus niewielką tablicę; po przejściu przez canvas przechowuje cztery bajty na piksel, a tablicy już nie ma. Deflate odzyskuje część tego narzutu, ale nigdy całości.
Ten fragment do konsoli odtwarza całą ścieżkę w dowolnej przeglądarce: wybiera plik, przepuszcza go przez canvas i wypisuje oba rozmiary oraz typ koloru:
const input = document.createElement('input');
input.type = 'file';
input.accept = 'image/png,image/jpeg';
input.onchange = async () => {
const file = input.files[0];
const bitmap = await createImageBitmap(file);
const canvas = document.createElement('canvas');
canvas.width = bitmap.width;
canvas.height = bitmap.height;
canvas.getContext('2d').drawImage(bitmap, 0, 0);
const blob = await new Promise((r) => canvas.toBlob(r, 'image/png'));
const head = new Uint8Array(await blob.slice(0, 26).arrayBuffer());
console.log(file.name, file.size, '->', blob.size);
console.log('bit depth', head[24], 'color type', head[25]);
};
input.click();
Argument jakości przekazany do toBlob jest ignorowany dla image/png. PNG jest bezstratny, więc parametr jakości nie ma czym handlować, a suwak, który w narzędziu opartym na canvas wygląda na sterujący kompresją PNG, nie steruje niczym.
Zmierzone 83 pliki: każdy skompresowany obraz wrócił większy od oryginału
Pełny przebieg, bez wybierania korzystnych przypadków:
| Pomiar | Wartość |
|---|---|
| Przetestowane pliki | 83 (wszystkie pliki PNG w public/og, oba typy koloru: RGB i paleta) |
| Pliki, które urosły | 83 / 83 (100%) |
| Najmniejszy przyrost | +35,6% |
| Mediana przyrostu | +76,6% |
| Największy przyrost | +237,2% |
Jeden plik, żeby pokazać skalę zjawiska: aes-decrypt.png urósł z 505 516 B do 898 014 B, czyli o +77,6%. Ten sam plik zakodowany jako WebP przy jakości 0,8 waży 27 188 B, czyli −94,6%.
Zakres próby ma tu znaczenie. Te 83 pliki to karty Open Graph: 1200×630, jednolite tła, duży tekst, kilka kolorów marki. Z taką grafiką dobry enkoder PNG radzi sobie świetnie — i właśnie dlatego przejście przez canvas szkodzi jej aż tak bardzo. To mocny wynik dla plików PNG typu graficznego, a nie twierdzenie, że każdy PNG na świecie rośnie po przejściu przez canvas. Fotograficzny PNG, który i tak był już zapisany jako pełne RGBA, ma znacznie mniej do stracenia.
Pewne jest natomiast to: jeśli skompresowany obraz jest większy od oryginału, a narzędzie działa w karcie przeglądarki, sprawdzać należy najpierw enkoder, a nie ustawienia.
Dlaczego JPEG zapisany jako PNG rośnie 16,9×
Najbardziej drastyczny wiersz w całym zestawie danych. Cztery pliki kontrolne, wszystkie przepuszczone tą samą ścieżką przez canvas:
| Plik | Charakter | Oryginał | canvas PNG | Zmiana | JPEG q92 | JPEG q80 | WebP q80 |
|---|---|---|---|---|---|---|---|
og-a.png | Grafika, 2 351 kolorów, RGB | 306 302 | 607 481 | +98,3% | 56 459 | 37 695 | 13 852 |
quantized.png | Paleta 64 kolorów | 59 843 | 184 856 | +208,9% | 78 388 | 44 442 | 16 046 |
photo.png | Zdjęcie, 479 373 kolorów | 498 639 | 867 763 | +74,0% | 77 321 | 45 050 | 24 068 |
photo.jpg | JPEG q82 | 47 828 | 809 415 | +1 592% (16,9×) | 58 762 (+22,9%) | 47 152 | 23 862 |
Ostatni wiersz to 47 828 bajtów na wejściu i 809 415 bajtów na wyjściu.
Ten mechanizm tłumaczy całą rodzinę zgłoszeń w stylu „kompresja powiększyła plik”. JPEG to stratny kodek w dziedzinie częstotliwości: przekształca bloki 8×8 na współczynniki DCT, agresywnie je kwantyzuje i zapisuje to, co przetrwało. PNG to bezstratny kodek w dziedzinie przestrzennej: przewiduje każdy piksel na podstawie sąsiadów i kompresuje algorytmem Deflate reszty z tej predykcji. Po zdekodowaniu pliku JPEG dostaje się piksele, które niosą każdy artefakt wprowadzony przez kwantyzator: dzwonienie przy krawędziach, blokowanie w gładkich gradientach, szum tam, gdzie oryginał był czysty.
Zapisanie tych pikseli jako PNG to prośba do bezstratnego kodeka, żeby zachował artefakty w idealnej postaci. I on ją spełnia. Ten sam szum, który JPEG wytworzył, żeby zmniejszyć plik, teraz powiększa PNG, bo szum to dokładnie to, czego predykcyjny enkoder bezstratny skompresować nie potrafi.
Dzieje się to wszędzie tam, gdzie przed zdjęciem stoi domyślne „zapisz jako PNG”: narzędzia do zrzutów ekranu, eksporty z programów graficznych, komunikatory, część widżetów do przesyłania plików.
Te same piksele, różnica 3,46×, obie ścieżki bezstratne
Słabość enkodera canvas da się zmierzyć. Wystarczy wziąć bufor pikseli z og-a.png i zakodować go dwiema drogami, obiema w pełni bezstratnie:
| Enkoder | Wynik | Typ koloru |
|---|---|---|
Chrome canvas toBlob('image/png') | 607 481 B | RGBA (wymuszone rozszerzenie) |
upng-js encode(..., cnum=0) | 175 491 B | RGB (zachowany oryginalny typ koloru) |
3,46× przy identycznych pikselach i bez żadnej straty po którejkolwiek stronie. Bezstratność sprawdziliśmy przez dekodowanie: wynik upng-js przy cnum=0 zwraca bufor bajt w bajt identyczny z wejściowym buforem RGBA.
Stąd bierze się różnica między dwoma darmowymi kompresorami online, które reklamują dokładnie to samo, a dają wyniki nieporównywalne ze sobą. „Kompresja w przeglądarce” opisuje dwie zupełnie różne implementacje. Jedna przekazuje piksele do canvas.toBlob i wysyła dalej to, co wróci. Druga ma w sobie prawdziwy enkoder PNG i kontroluje typ koloru. To samo wejście, ta sama przeglądarka, różnica 3,46×.
3. Przyczyna druga: w tym pliku JPEG nie ma już czego wycinać
Pięć rund ponownej kompresji i rozmiar przestaje się zmieniać
Plik photo.jpg, zapisany wcześniej przy jakości 82, przeszedł pięć kolejnych rund ponownej kompresji przy jakości 0,8, gdzie każde pokolenie było wejściem dla następnego:
| Pokolenie | Bajty | Względem poprzedniego |
|---|---|---|
| 0 (oryginał, q82) | 47 828 | — |
| 1 | 47 152 | −1,4% |
| 2 | 47 164 | +0,0% |
| 3 | 47 156 | −0,0% |
| 4 | 47 156 | 0,0% |
| 5 | 47 156 | 0,0% |
Pierwszy przebieg daje 1,4%. Od pokolenia 2 rozmiar pliku jest zamknięty w paśmie ±12 bajtów, a pokolenia 4 i 5 mają rozmiar identyczny co do bajta z pokoleniem 3.
Tak wygląda „kompresja nie działa”, gdy wejściem jest JPEG. Narzędzie i enkoder zrobiły swoje. Po prostu nie zostało już nic do usunięcia, bo tablice kwantyzacji przy jakości 80 zerowały mniej więcej te same współczynniki, które jakość 82 zachowała. Raz usuniętego współczynnika nie da się usunąć drugi raz.
Da się to podejrzeć lokalnie:
cp photo.jpg gen0.jpg
for i in 1 2 3 4 5; do
magick "gen$((i-1)).jpg" -quality 80 "gen$i.jpg"
done
stat -f%z gen0.jpg gen1.jpg gen2.jpg gen3.jpg gen4.jpg gen5.jpg
W Linuksie trzeba użyć stat -c%s. Dokładne liczby bajtów zależą od tego, z jakim enkoderem zlinkowano daną kompilację ImageMagick, więc powyższa tabela nie odtworzy się cyfra w cyfrę. Chodzi o kształt: jeden sensowny spadek, potem płaska linia.
Podniesienie parametru jakości to nie kompresja
Wiersz photo.jpg w sekcji 2 pokazuje drugą stronę tego samego zjawiska. Ponowna kompresja tego oryginału o jakości 82 przy jakości 92 dała 58 762 bajty, czyli +22,9%.
Zaskakuje to osoby, które traktują parametr jakości jak pokrętło od „małego” do „dużego”, ustawiane dowolnie. To nie jest bezwzględny cel jakościowy. Ten parametr wybiera tablicę kwantyzacji, a przepuszczenie zdekodowanego obrazu przez dokładniejszą tablicę niż ta, która go stworzyła, zapisuje istniejące artefakty precyzyjniej i dokłada do tego świeżą porcję strat. Większy plik i gorszy obraz naraz.
Zasada jest prosta: nigdy nie kompresować ponownie pliku JPEG z ustawieniem jakości wyższym niż to, przy którym został zapisany. Jeśli ta wartość jest nieznana, lepiej nie kompresować w ogóle, tylko wrócić do pliku źródłowego.
Strata pokoleniowa: utrata jakości przy ponownej kompresji JPEG, której nie widać
W tabeli plateau kryje się pułapka. Rozmiar przestał się zmieniać po pokoleniu 2, ale obraz zmieniał się dalej. Każdy przebieg dekoduje do pikseli, ponownie transformuje i ponownie kwantyzuje. Współczynniki, które w jednym pokoleniu przetrwały tuż przy progu, w następnym są za niego wypychane.
Szkody nie pojawiają się tam, gdzie się ich szuka. W rozmiarze miniatury pokolenie 5 i pokolenie 1 są nie do odróżnienia. Przy powiększeniu do 100% trzeba sprawdzić miejsca, w których JPEG zawodzi jako pierwsze: ostre krawędzie na jednolitym tle, tekst i gładkie gradienty, gdzie blokowanie wychodzi jako widoczne kafle 8×8. Jeśli pipeline buildu kompresuje ponownie przy każdym wdrożeniu, kumuluje się to po cichu miesiącami.
Mierzyliśmy tu rozmiary plików, a nie jakość percepcyjną, więc dla tych pięciu pokoleń nie padnie żadna wartość PSNR ani SSIM. Same dane o rozmiarze wystarczają: od pokolenia 2 cały koszt spada na jakość, a zysk wynosi zero.
Przesunięcia barw to osobna awaria z tym samym wyzwalaczem. Canvas nie przenosi metadanych, więc przejście przez toBlob usuwa blok EXIF, a razem z nim profil ICC. Obraz oznaczony jako Display P3 albo Adobe RGB wychodzi bez oznaczenia, a programy wyświetlające zinterpretują go jako sRGB. Wartości pikseli się nie zmieniły. Zmieniła się instrukcja, jak je odczytać.
4. Przyczyna trzecia: rozmiar PNG nie maleje, bo plik przeszedł już kwantyzację
Jeśli plik PNG przeszedł już raz przez kompresor, drugi przebieg nie ma na czym pracować. Tak wygląda wiersz quantized.png: plik o rozmiarze 59 843 bajtów, zredukowany wcześniej do palety 64 kolorów, przepuszczony trzema osobnymi ścieżkami:
| Ścieżka | Wynik | Względem oryginału |
|---|---|---|
Bezstratne przekodowanie (upng cnum=0) | 61 377 | +2,6% |
| Kwantyzacja do 256 kolorów | 61 366 | +2,5% |
| Kwantyzacja do 64 kolorów | 61 366 | +2,5% |
Każda ścieżka dała plik większy od oryginału. Nieznacznie, ale większy, łącznie z kwantyzacją do 64 kolorów pliku, który miał już 64 kolory.
Kompresja PNG polega na usuwaniu nadmiarowości: powtarzających się kolorów, przewidywalnych sąsiadów, małej palety. Wcześniejszy przebieg zebrał już wszystko. To, co zostało, jest niemal nieściśliwe, a niewielki przyrost to własny narzut enkodera: nieco inna kolejność wpisów w palecie, inny dobór filtrów dla poszczególnych linii, odrobinę mniej szczęśliwy Deflate.
„0% oszczędności” na już zoptymalizowanym pliku PNG to wynik poprawny, a nie awaria. Narzędzie, które zgłasza niewielki przyrost i zostawia oryginalny plik, zachowuje się właściwie. Narzędzie, które mimo wszystko wydaje większy plik, zachowuje się źle.
5. Kwantyzacja obcina PNG mocniej niż ponowne kodowanie
Bezstratne przekodowanie, 256 kolorów i 64 kolory
Trzy pliki przepuszczone przez trzy strategie:
| Plik | Oryginał | Bezstratnie (cnum=0) | 256 kolorów | 64 kolory |
|---|---|---|---|---|
og-a.png | 306 302 | 175 491 (−42,7%) | 110 772 (−63,8%) | 76 230 (−75,1%) |
photo.png | 498 639 | 587 863 (+17,9%) | 109 284 (−78,1%) | 61 397 (−87,7%) |
quantized.png | 59 843 | 61 377 (+2,6%) | 61 366 (+2,5%) | 61 366 (+2,5%) |
Bezstratne przekodowanie to najsłabsze dostępne narzędzie. Wygrało 42,7% na grafice, oddało 17,9% na zdjęciu i straciło 2,6% na pliku poddanym już kwantyzacji. Treść fotograficzna pokonuje nawet kompetentny bezstratny enkoder PNG, bo nie ma tam palety do znalezienia, a sąsiadujące piksele słabo się nawzajem przewidują.
Realna redukcja bierze się z kwantyzacji, i to z dużym zapasem: 63,8% wobec 42,7% na tej samej grafice przy 256 kolorach i 75,1% przy 64. W wierszu poleceń:
magick logo.png -colors 64 PNG8:logo-64.png
magick logo.png -colors 256 PNG8:logo-256.png
Przedrostek PNG8: wymusza PNG z paletą. Bez niego ImageMagick może zredukować kolory, a mimo to zapisać plik truecolor, co przekreśla większość zysku.
Kiedy paleta jest bezpieczna, a kiedy pojawia się pasmowanie
Kwantyzacja jest stratna: przyporządkowuje każdy piksel do najbliższego wpisu w ograniczonej palecie. Czy to widać, zależy od tego, ile różnych kolorów ma dana treść.
Bezpieczne: ikony, logo, zrzuty ekranu interfejsu, płaskie ilustracje, diagramy, wszystko z dużymi obszarami jednolitego koloru i ostrymi krawędziami. Takie pliki zawierają zwykle najwyżej kilkaset różnych kolorów, więc paleta 256-elementowa jest praktycznie darmowa, a nawet 64 często wystarcza.
Ryzykowne: zdjęcia, gładkie gradienty, miękkie cienie i półprzezroczyste nakładki. Redukcja gradientu do 64 stopni daje widoczne pasy, a dithering wymienia te pasy na szum, który z kolei odbiera część oszczędności na rozmiarze. Częściowa przezroczystość na gradiencie to przypadek najtrudniejszy ze wszystkich.
Przezroczystość zasługuje na osobne sprawdzenie, bo to, ile z niej przetrwa, zależy od enkodera, a nie od samej kwantyzacji. PNG8: w ImageMagick zapisuje przezroczystość binarną, więc piksel jest albo w pełni nieprzezroczysty, albo w pełni przezroczysty, a miękka krawędź z antyaliasingiem wraca jako twarda. Dedykowany kwantyzator PNG zachowuje pełny kanał alfa, a razem z nim miękką krawędź. Jeśli materiał ma cień albo rozmyte krawędzie, lepiej porównać oba warianty przed decyzją.
Wynik trzeba oglądać przy 100%, a nie na miniaturze. Pasmowanie to ten jeden artefakt, który pomniejszony podgląd niezawodnie ukrywa.
6. Rozwiązanie: zmiana formatu zamiast ponownej kompresji
Tabela decyzyjna formatów
| Treść | Format | Dlaczego |
|---|---|---|
| Zdjęcia | WebP albo JPEG dla maksymalnej zgodności | Zdjęcia potrzebują stratnego kodowania częstotliwościowego |
| Zrzuty ekranu, grafika interfejsu | PNG po kwantyzacji albo WebP | Płaskie kolory, ostre krawędzie, małe palety |
| Ikony i logo | SVG, jeśli jest dostępny wektor, w przeciwnym razie PNG po kwantyzacji | Wektory nie mają problemu z rozdzielczością |
| Wszystko, co wymaga przezroczystości | WebP albo PNG | Oba niosą pełny kanał alfa |
| Animacja | WebP | Jeden format zamiast GIF |
| Archiwizacja z dokładnością do piksela | PNG, bezstratnie | Jedyny przypadek, w którym bezstratność jest wymogiem |
To celowo wersja skrócona. Efektywność kodowania i wsparcie przeglądarek dla nowoczesnych formatów mają własny artykuł: WebP vs AVIF vs JPEG.
Co WebP zrobił z tymi samymi 83 plikami
Ten sam zestaw 83 plików, który przez canvas PNG rósł w 100% przypadków, przekonwertowany do WebP przy jakości 0,8: 83 z 83 zmniejszyły się, mediana −94,2%. Wspomniany wcześniej pojedynczy plik, aes-decrypt.png, zszedł z 505 516 B do 27 188 B, czyli −94,6%.
Pliki kontrolne to potwierdzają. og-a.png: 306 302 jako PNG, 13 852 jako WebP q80. photo.png: 498 639 jako PNG, 24 068 jako WebP q80. photo.jpg: 47 828 jako JPEG, 23 862 jako WebP q80.
Jedno zastrzeżenie do ostatniego porównania. WebP przy jakości 0,8 jest stratny, więc nie konkuruje z PNG na równych warunkach, a przekodowanie istniejącego pliku JPEG do WebP nadal kosztuje jedno pokolenie. Liczy się porównanie z tym, co za chwilę miało trafić na produkcję, a nie z hipotetycznym idealnym oryginałem.
Kiedy mimo wszystko warto zostać przy PNG
Bezstratność bywa wymogiem, a nie preferencją. PNG należy zachować dla materiałów, które wracają do procesu projektowego i są ponownie edytowane, dla zrzutów ekranu używanych w testach porównania pikseli oraz dla wycinków interfejsu, w których jeden przesunięty kolor psuje wizualnego diffa. To samo dotyczy wszystkiego, co będzie później komponowane, bo tam artefakty kwantyzacji by się kumulowały. W takich przypadkach należy użyć porządnego kwantyzującego enkodera, jeśli treść na to pozwala, i pogodzić się z rozmiarem pliku, jeśli nie pozwala.
Jeszcze jeden rodzaj „większego”, który nie ma nic wspólnego z kompresją
Przy osadzaniu obrazów jako data URI rozmiar w pliku CSS albo HTML nie jest rozmiarem na dysku. Base64 koduje każde 3 bajty jako 4 znaki plus padding, więc tekst jest arytmetycznie +33% większy od bajtów, które przenosi, jeszcze przed jakąkolwiek kompresją transportową. Idealnie zoptymalizowany obraz i tak rośnie o jedną trzecią w momencie osadzenia. Kiedy ten kompromis się opłaca, opisuje nasz przewodnik po osadzaniu data URI.
7. W praktyce: przeglądarka, wiersz poleceń, pipeline buildu
W przeglądarce
Przy wyborze narzędzia działającego w przeglądarce liczy się jedno rozróżnienie: czy PNG przechodzi przez canvas. W naszym kompresorze obrazów nie przechodzi. Wejściowy PNG jest poddawany kwantyzacji do palety kolorów i zapisywany z powrotem jako prawdziwy PNG z nienaruszonym kanałem alfa, więc przezroczystość i miękkie krawędzie przeżywają; suwak jakości odpowiada rozmiarowi palety, a nie argumentowi toBlob, który PNG i tak by zignorował. Jakość 100 oznacza bezstratne przekodowanie. JPEG i WebP faktycznie przechodzą przez canvas, gdzie parametr jakości jest realny i robi to, czego się po nim spodziewamy.
Jedno zachowanie wygląda na błąd, choć nim nie jest: jeśli skompresowany wynik nie jest mniejszy od przesłanego pliku, narzędzie wyrzuca własny wynik i zostawia oryginalne bajty. Na już zoptymalizowanym pliku PNG pojawi się „0% oszczędności”. To sekcja 4 w działaniu.
Przetwarzanie odbywa się lokalnie, więc nic nie jest wysyłane na serwer.
W wierszu poleceń
cwebp jest częścią libwebp i to najszybszy sposób, żeby sprawdzić, czy zmiana formatu rozwiązuje problem:
# Stratny WebP, jakość 0-100
cwebp -q 80 photo.png -o photo.webp
# Bezstratny WebP, nakład kompresji 0-9
cwebp -z 9 logo.png -o logo.webp
ImageMagick 7 obsługuje przypadki konwersji i kwantyzacji:
# PNG na JPEG o wybranej jakości
magick photo.png -quality 80 photo.jpg
# Kwantyzacja do PNG z 64-kolorową paletą
magick logo.png -colors 64 PNG8:logo-64.png
# Usunięcie EXIF i pozostałych metadanych
magick photo.jpg -strip photo-clean.jpg
W macOS sips jest już zainstalowany i nie wymaga żadnych zależności:
# Konwersja na JPEG; formatOptions przyjmuje 0-100 albo low/normal/high/best
sips -s format jpeg -s formatOptions 80 photo.png --out photo.jpg
# Skalowanie tak, by dłuższa krawędź miała 1200 px, proporcje zachowane
sips -Z 1200 photo.jpg --out photo-1200.jpg
# Odczyt wymiarów
sips -g pixelWidth -g pixelHeight photo-1200.jpg
Najpierw zmiana rozmiaru, potem kompresja. Enkodery pracują na pikselach, a najtańszy piksel to taki, którego nie ma.
Pipeline buildu
Gdy proces jest już zautomatyzowany, a nie ręczny, decyzja przenosi się na to, gdzie wykonywana jest praca i która biblioteka ją wykonuje, a bilans kompromisów wygląda wtedy inaczej: to porównanie opisuje kompresja obrazów w przeglądarce i w Node.js. Jedna zasada obowiązuje tu bez zmian: kompresować przy każdym buildzie z oryginalnego pliku źródłowego, nigdy z wyniku poprzedniego buildu. Tak właśnie pipeline schodzi po krzywej straty pokoleniowej, podczas gdy rozmiary plików wyglądają na doskonale stabilne.
8. Pięć przekonań, których pomiary nie potwierdzają
„Dwukrotna kompresja daje mniejszy plik.” Pokolenie 1 dało 1,4%. Pokolenia od 2 do 5 mieściły się w ±12 bajtach, podczas gdy obraz dalej się psuł. Drugi przebieg to sam koszt.
„PNG jest bezstratny, więc to lepszy format.” Bezstratność to właściwość, a nie zaleta. Nasze testowe zdjęcie waży 498 639 bajtów jako PNG i 24 068 jako WebP q80. Zachowanie fotografii z dokładnością do bitu, gdy i tak będzie oglądana wyłącznie na ekranie, nie daje nic, a kosztuje większość pliku.
„Jakość 100 to bezpieczny wybór.” Ponowna kompresja pliku JPEG o jakości 82 przy jakości 92 dała +22,9% i gorszy obraz. Powyżej ustawienia jakości oryginału ta liczba przestaje znaczyć „bezpieczniej”, a zaczyna znaczyć „większy”.
„Plik jest duży, bo rozdzielczość jest wysoka.” Rozdzielczość ma znaczenie, ale przy tej samej rozdzielczości format ma większe. og-a.png ma 1200×630 w obu przypadkach: 607 481 bajtów jako PNG z canvas i 13 852 bajty jako WebP q80. Identyczna liczba pikseli.
„Kompresory online są takie same.” Te same piksele, ta sama przeglądarka, oba warianty bezstratne: 607 481 bajtów z canvas i 175 491 z upng-js. Rozrzut 3,46× między dwoma narzędziami, które opisują się identycznie.
9. FAQ
Dlaczego skompresowany obraz jest większy od oryginału?
Bo narzędzie przekodowało go, zamiast skompresować. Wyjściem z canvas przeglądarki jest zawsze 32-bitowy PNG w RGBA, który odrzuca palety i zapisuje cztery bajty na piksel. W naszym teście na 83 prawdziwych plikach PNG urosły wszystkie 83, z medianą przyrostu +76,6%. Lepiej przekonwertować taki plik do WebP albo JPEG.
Dlaczego PNG nie zmniejsza się po kompresji?
PNG jest bezstratny, więc suwak jakości nie ma czym handlować. Prawdziwa redukcja bierze się z obcięcia liczby kolorów, a jeśli plik przeszedł już kwantyzację, nie ma czego obcinać. Nasz testowy PNG z 64 kolorami wrócił z wynikiem +2,5% po ponownej kwantyzacji do 64 kolorów.
Czy dwukrotna kompresja JPEG pogarsza jakość?
Tak, a w zamian nie dostaje się prawie nic. Pięć rund przy jakości 0,8: 47 828 bajtów spada do 47 152 przy pierwszym przebiegu, a przez kolejne cztery trzyma się w granicach 12 bajtów. Rozmiar przestał się zmieniać, choć każdy przebieg dalej kwantyzował obraz od nowa. Oryginały trzeba zachować.
PNG czy JPEG, żeby zmniejszyć plik?
Dla zdjęć bez wyjątku JPEG albo WebP. Nasze testowe zdjęcie zmierzyło 498 639 bajtów jako PNG, 45 050 jako JPEG q80 i 24 068 jako WebP q80. PNG zostaje dla płaskiej grafiki, ostrego tekstu i przezroczystości, gdzie pracę kompresji wykonuje mała paleta.
Dlaczego obraz po kompresji jest rozmyty?
Dwie różne przyczyny. Stratne enkodery przy niskich ustawieniach jakości dają widoczne blokowanie wokół krawędzi i tekstu. Powtarzane przebiegi dokładają stratę pokoleniową nawet wtedy, gdy rozmiar pliku przestaje się zmieniać. Jeśli kolory raczej się przesunęły, niż obraz zmiękł, to przejście przez canvas usunęło profil ICC, bo canvas nie przenosi żadnych metadanych.
Czy da się skompresować obraz bez utraty jakości?
Tak, ale efekt będzie dużo mniejszy. Bezstratne przekodowanie tylko zapisuje identyczne piksele wydajniej: z 306 302 do 175 491 bajtów na naszej grafice, z potwierdzoną bajt w bajt identycznością po zdekodowaniu. To samo podejście na zdjęciu poszło w złą stronę, +17,9%. Po realne oszczędności bez widocznej straty lepiej sięgnąć po WebP przy jakości 80.
Dlaczego zrzut ekranu w PNG zajmuje tak dużo?
Zrzuty ekranu są zapisywane jako pełny PNG w RGBA, cztery bajty na piksel przed kompresją, a ekran Retina podwaja liczbę pikseli w każdym wymiarze. Płaska treść dobrze reaguje na redukcję palety: nasza grafika karty Open Graph spadła o 63,8% przy 256 kolorach i o 75,1% przy 64.
Czy zmiana rozmiaru zmniejsza plik bardziej niż kompresja?
Zwykle tak, a oba efekty się sumują. Zmniejszenie obu wymiarów o połowę usuwa trzy czwarte pikseli, zanim enkoder w ogóle ruszy, a rozmiar pliku z grubsza podąża za liczbą pikseli. Najpierw zmienić rozmiar do wymiarów faktycznie wyświetlanych, potem skompresować raz. Plik z aparatu w pełnej rozdzielczości wrzucony w miejsce miniatury marnuje oba przebiegi.