Miniatury obrazków bez zewnętrznych bibliotek
- Pliki i katalogi
- poziom: średni
- aktualizacja: 16 sierpnia 2026
Do zrobienia miniatury nie potrzebujesz żadnej biblioteki — ImageIO i Graphics2D siedzą w standardowym JDK. Potrzebujesz za to trzech rzeczy, o których łatwo zapomnieć. ImageIO.read nie rzuca wyjątku, gdy plik nie jest obrazem — zwraca null, więc bez sprawdzenia NullPointerException wybuchnie dopiero trzy metody dalej, w zupełnie niewinnym miejscu. Domyślne skalowanie wygląda jak cięte siekierą, dopóki nie ustawisz RenderingHints. A obrazu z kanałem alfa nie zapiszesz jako JPEG w ogóle: enkoder odmawia, ImageIO.write zwraca false, plik nie powstaje i znowu żaden wyjątek Cię nie ostrzeże — przezroczystość musisz spłaszczyć sam, na biel, bo świeże płótno TYPE_INT_RGB jest czarne. Kod poniżej załatwia wszystkie trzy sprawy, dokłada przycięcie do kwadratu na awatary i działa bez ekranu — nie otwiera żadnego okna, więc nadaje się na serwer.
import java.awt.Color;
import java.awt.Graphics2D;
import java.awt.RenderingHints;
import java.awt.image.BufferedImage;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.StandardCopyOption;
import java.util.Locale;
import javax.imageio.ImageIO;
/**
* Miniatury obrazków bez zewnętrznych bibliotek — skryptyJAVA.pl
* Zapisz jako: MiniaturyObrazkow.java
*/
public class MiniaturyObrazkow {
/** ImageIO.read nie rzuca wyjątku dla nieznanego formatu — po prostu zwraca null. */
public static BufferedImage wczytaj(Path plik) throws IOException {
BufferedImage obrazek = ImageIO.read(plik.toFile());
if (obrazek == null) { throw new IOException("Brak dekodera dla: " + plik.getFileName()); }
return obrazek;
}
/** Miniatura wpisana w ramkę z zachowaniem proporcji; mniejszego obrazka nie powiększa. */
public static BufferedImage miniatura(BufferedImage zrodlo, int maksSzerokosc,
int maksWysokosc, boolean zachowajAlfe) {
if (maksSzerokosc < 1 || maksWysokosc < 1) {
throw new IllegalArgumentException("Wymiary ramki muszą być dodatnie.");
}
double skala = Math.min(1.0, Math.min(maksSzerokosc / (double) zrodlo.getWidth(),
maksWysokosc / (double) zrodlo.getHeight()));
int szerokosc = Math.max(1, (int) Math.round(zrodlo.getWidth() * skala));
int wysokosc = Math.max(1, (int) Math.round(zrodlo.getHeight() * skala));
return przeskaluj(zrodlo, szerokosc, wysokosc, zachowajAlfe);
}
/** Wycina największy kwadrat ze środka i skaluje go dokładnie do zadanego boku. */
public static BufferedImage kwadratZeSrodka(BufferedImage zrodlo, int bok, boolean zachowajAlfe) {
if (bok < 1) { throw new IllegalArgumentException("Bok kwadratu musi być dodatni."); }
int krawedz = Math.min(zrodlo.getWidth(), zrodlo.getHeight());
int lewy = (zrodlo.getWidth() - krawedz) / 2;
int gorny = (zrodlo.getHeight() - krawedz) / 2;
// getSubimage nie kopiuje pikseli — zwraca okno na ten sam raster co oryginał
BufferedImage kadr = zrodlo.getSubimage(lewy, gorny, krawedz, krawedz);
return przeskaluj(kadr, bok, bok, zachowajAlfe);
}
/** Właściwe skalowanie: nowy bufor, dobre filtrowanie i zawsze dispose() na końcu. */
private static BufferedImage przeskaluj(BufferedImage zrodlo, int szerokosc,
int wysokosc, boolean zachowajAlfe) {
BufferedImage cel = new BufferedImage(szerokosc, wysokosc,
zachowajAlfe ? BufferedImage.TYPE_INT_ARGB : BufferedImage.TYPE_INT_RGB);
Graphics2D grafika = cel.createGraphics();
try {
grafika.setRenderingHint(RenderingHints.KEY_INTERPOLATION,
RenderingHints.VALUE_INTERPOLATION_BICUBIC);
grafika.setRenderingHint(RenderingHints.KEY_RENDERING, RenderingHints.VALUE_RENDER_QUALITY);
grafika.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
if (!zachowajAlfe) {
// TYPE_INT_RGB startuje od czerni — bez zamalowania alfa wyjdzie na czarno
grafika.setColor(Color.WHITE);
grafika.fillRect(0, 0, szerokosc, wysokosc);
}
grafika.drawImage(zrodlo, 0, 0, szerokosc, wysokosc, null);
} finally {
grafika.dispose(); // Graphics2D trzyma zasoby systemowe — zwolnij je zawsze
}
return cel;
}
/** Zapisuje przez osobny plik roboczy, więc błąd enkodera nie niszczy starego celu. */
public static void zapisz(BufferedImage obrazek, String format, Path plik) throws IOException {
String nazwaFormatu = format.toLowerCase(Locale.ROOT);
Path cel = plik.toAbsolutePath().normalize();
Path katalog = cel.getParent();
if (katalog == null) { throw new IOException("Plik docelowy musi mieć katalog nadrzędny."); }
Files.createDirectories(katalog);
Path tymczasowy = Files.createTempFile(katalog, "obraz-", ".tmp");
try {
if (!ImageIO.write(obrazek, nazwaFormatu, tymczasowy.toFile())) {
throw new IOException(ImageIO.getImageWritersByFormatName(nazwaFormatu).hasNext()
? "Enkoder " + nazwaFormatu
+ " nie przyjmuje obrazu tego typu (kanał alfa?)."
: "ImageIO nie zna formatu zapisu: " + nazwaFormatu);
}
Files.move(tymczasowy, cel, StandardCopyOption.REPLACE_EXISTING);
} finally {
Files.deleteIfExists(tymczasowy);
}
}
/** Rysuje przykład: barwny gradient i przezroczysty narożnik, bez pliku wejściowego. */
private static BufferedImage narysujPrzyklad(int szerokosc, int wysokosc) {
BufferedImage plotno = new BufferedImage(szerokosc, wysokosc, BufferedImage.TYPE_INT_ARGB);
for (int y = 0; y < wysokosc; y++) {
for (int x = 0; x < szerokosc; x++) {
int alfa = (x + y < szerokosc / 3) ? 0 : 255;
int kolor = (x * 255 / szerokosc) << 16 | (y * 255 / wysokosc) << 8 | 130;
plotno.setRGB(x, y, alfa << 24 | kolor);
}
}
return plotno;
}
/** Zapisuje w formacie wziętym z rozszerzenia i wypisuje parametry wyniku. */
private static void zapiszIOpisz(String etykieta, BufferedImage obrazek, Path katalog,
String nazwa) throws IOException {
Path plik = katalog.resolve(nazwa);
zapisz(obrazek, nazwa.substring(nazwa.lastIndexOf('.') + 1), plik);
System.out.printf(Locale.ROOT, "%-24s %4dx%-4d alfa:%-6b %6d B %s%n", etykieta, obrazek.getWidth(),
obrazek.getHeight(), obrazek.getColorModel().hasAlpha(), Files.size(plik), nazwa);
}
// ── PRZYKŁADY ───────────────────────────────────────────
public static void main(String[] args) throws IOException {
Path katalog = Files.createTempDirectory("miniatury-demo-");
try {
zapiszIOpisz("oryginał (PNG z alfą)", narysujPrzyklad(900, 600), katalog, "duzy.png");
BufferedImage zrodlo = wczytaj(katalog.resolve("duzy.png"));
zapiszIOpisz("wpisana w 320x320", miniatura(zrodlo, 320, 320, true), katalog, "mini.png");
zapiszIOpisz("na białym tle", miniatura(zrodlo, 320, 320, false), katalog, "mini.jpg");
zapiszIOpisz("kwadrat ze środka", kwadratZeSrodka(zrodlo, 128, true), katalog, "awatar.png");
Files.writeString(katalog.resolve("udawany.png"), "Tekst — kłamie tylko rozszerzenie.");
try {
wczytaj(katalog.resolve("udawany.png"));
} catch (IOException wyjatek) {
System.out.println("\nOczekiwany błąd: " + wyjatek.getMessage());
}
System.out.println("ARGB prosto do JPEG, bez spłaszczania, ImageIO.write zwraca: "
+ ImageIO.write(zrodlo, "jpg", katalog.resolve("odrzut.jpg").toFile()));
} finally {
try (var smieci = Files.list(katalog)) {
for (Path smiec : smieci.toList()) { Files.deleteIfExists(smiec); }
}
Files.deleteIfExists(katalog);
System.out.println("Usunięto katalog demonstracyjny.");
}
}
}
Jak użyć tego skryptu
- Wklej kod do pliku
MiniaturyObrazkow.java— nazwa musi być identyczna z nazwą klasy publicznej — i odpal go poleceniemjava MiniaturyObrazkow.java. Obrazek wejściowy program rysuje sobie sam w katalogu tymczasowym, robi z niego trzy miniatury i na końcu kasuje wszystkie pliki, więc nie potrzebujesz ani zdjęcia na dysku, ani sieci. - We własnym projekcie korzystasz z czterech metod publicznych:
wczytaj(...)dajeBufferedImage,zapisz(...)odkłada wynik na dysk, a pomiędzy nimi wybierasz jedną z dwóch metod skalujących.miniatura(...)wpisuje obraz w ramkę i nigdy go nie powiększa — mniejsze zdjęcie wróci w oryginalnych wymiarach.kwadratZeSrodka(...)zachowuje się odwrotnie: zawsze oddaje dokładnieboknabokpikseli, więc obrazek mniejszy od awatara zostanie rozciągnięty. - Ostatni argument obu metod skalujących decyduje o kanale alfa. Ustaw
true, gdy wynik zapisujesz do PNG i chcesz zachować przezroczystość, albofalse, gdy idziesz do JPEG — wtedy miniatura powstaje na białym tle zamiast na czerni. - Metoda
zapisz(...)przyjmuje format osobnym argumentem. Bezpieczne wybory topng,jpg,gifibmp— enkodery dla nich ma każde JDK. Uważaj tylko, żejpgibmpnie przyjmą obrazu z kanałem alfa, więc do nich skaluj zfalse. Nieznany format i odrzucony obraz kończą się wyjątkiem, a jego treść mówi, który z tych dwóch przypadków zaszedł.
Warto wiedzieć
ImageIOsygnalizuje kłopoty wartością zwracaną, a nie wyjątkiem — to najczęstsza wpadka przy tym API.ImageIO.readzwracanull, gdy dla pliku nie ma dekodera.ImageIO.writezwracafalse, gdy formatu nie zna albo enkoder nie przyjmuje obrazu. Dlategozapisznajpierw używa unikalnego pliku roboczego w katalogu celu, a dopiero po sukcesie podmienia właściwą ścieżkę; przy błędzie dotychczasowy plik pozostaje nietknięty, a plik roboczy jest usuwany.- Kanał alfa i JPEG to para, która się nie znosi, i to na dwa sposoby naraz. Obraz
TYPE_INT_ARGBwysłany prosto do JPEG-a (tak samo do BMP) zostaje odrzucony przez enkoder — nie dostajesz ani pliku, ani wyjątku, tylkofalse. A gdy przerysujesz go naTYPE_INT_RGBi nie zamalujesz tła, przezroczyste piksele wyjdą czarne, bo świeże płótno RGB jest czarne. Dlatego przyzachowajAlfe = falsepłótno najpierw dostaje białe wypełnienie, a dopiero potem trafia na nie obrazek. Do PNG zostawiaj ARGB, inaczej sam skasujesz sobie przezroczystość. ImageIOnie czyta orientacji z EXIF-u. Zdjęcie z telefonu ma piksele zapisane poziomo, a o tym, że należy je obrócić, mówi wyłącznie znacznikOrientation— Java go ignoruje, więc miniatura wyjdzie położona na boku, choć w galerii systemowej oryginał wygląda normalnie. Standardowa biblioteka tej informacji nie udostępnia: albo dokładasz zewnętrzną (np. metadata-extractor), albo sam parsujesz segment APP1 pliku JPEG.BufferedImagetrzyma piksele nieskompresowane: zdjęcie 4000 x 3000 zajmuje w pamięci około 48 MB, niezależnie od tego, że plik JPEG waży 3 MB. Przy przetwarzaniu całego katalogu zdjęć, zwłaszcza równolegle, domyślna sterta kończy się szybciej, niż się spodziewasz. Wczytuj po jednym pliku i nie zbieraj oryginałów w liście — pamiętaj też, żegetSubimageniczego nie zwalnia, bo współdzieli raster z obrazem źródłowym.
Podobne skrypty
Odczyt i zapis plików tekstowych
Komplet operacji na plikach tekstowych przez java.nio.file.Files z jawnym UTF-8: zapis, dopisywanie, odczyt całości i linia po linii, przetwarzanie strumieniowe, kopiowanie i usuwanie.
początkujący Zobacz kod → Pliki i katalogiListowanie i przeszukiwanie katalogów przez walkFileTree
Rekurencyjne przeglądanie drzewa katalogów przez Files.walkFileTree: limit głębokości, filtr rozszerzeń, strumieniowe szukanie treści w UTF-8 oraz jawne pomijanie dowiązań i windowsowych junctions.
średni Zobacz kod → Pliki i katalogiEksport i import CSV ze średnikiem
Samodzielny zapis i odczyt CSV w UTF-8 ze średnikiem: cytowanie, pola wielowierszowe, BOM i CRLF pod Excela, ścisły parser oraz jawny tryb chroniący arkusz przed wykonaniem formuł z niezaufanych danych.
zaawansowany Zobacz kod →