Przejdź do treści

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.

JAVA MiniaturyObrazkow.java 140 linii Pobierz .java
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

  1. Wklej kod do pliku MiniaturyObrazkow.java — nazwa musi być identyczna z nazwą klasy publicznej — i odpal go poleceniem java 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.
  2. We własnym projekcie korzystasz z czterech metod publicznych: wczytaj(...) daje BufferedImage, 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ładnie bok na bok pikseli, więc obrazek mniejszy od awatara zostanie rozciągnięty.
  3. Ostatni argument obu metod skalujących decyduje o kanale alfa. Ustaw true, gdy wynik zapisujesz do PNG i chcesz zachować przezroczystość, albo false, gdy idziesz do JPEG — wtedy miniatura powstaje na białym tle zamiast na czerni.
  4. Metoda zapisz(...) przyjmuje format osobnym argumentem. Bezpieczne wybory to png, jpg, gif i bmp — enkodery dla nich ma każde JDK. Uważaj tylko, że jpg i bmp nie przyjmą obrazu z kanałem alfa, więc do nich skaluj z false. 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ć

  • ImageIO sygnalizuje kłopoty wartością zwracaną, a nie wyjątkiem — to najczęstsza wpadka przy tym API. ImageIO.read zwraca null, gdy dla pliku nie ma dekodera. ImageIO.write zwraca false, gdy formatu nie zna albo enkoder nie przyjmuje obrazu. Dlatego zapisz najpierw 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_ARGB wysłany prosto do JPEG-a (tak samo do BMP) zostaje odrzucony przez enkoder — nie dostajesz ani pliku, ani wyjątku, tylko false. A gdy przerysujesz go na TYPE_INT_RGB i nie zamalujesz tła, przezroczyste piksele wyjdą czarne, bo świeże płótno RGB jest czarne. Dlatego przy zachowajAlfe = false płótno najpierw dostaje białe wypełnienie, a dopiero potem trafia na nie obrazek. Do PNG zostawiaj ARGB, inaczej sam skasujesz sobie przezroczystość.
  • ImageIO nie 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 znacznik Orientation — 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.
  • BufferedImage trzyma 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ż, że getSubimage niczego nie zwalnia, bo współdzieli raster z obrazem źródłowym.
#pliki #nio #path #obrazki #imageio

Podobne skrypty