Написание ICO файлов Java

Недавно я заинтересовался созданием файлов.ico или Windows icon icon в Java. Это текущий код, который я использую. Я получил спецификации формата файла отсюда http://en.wikipedia.org/wiki/ICO_%28file_format%29

    BufferedImage img = new BufferedImage(16, 16, BufferedImage.TYPE_INT_RGB);
    Graphics g = img.getGraphics();
    g.setColor(Color.GREEN);
    g.fillRect(0, 0, 16, 16);
    byte[] imgBytes = getImgBytes(img);
    int fileSize = imgBytes.length + 22;
    ByteBuffer bytes = ByteBuffer.allocate(fileSize);
    bytes.order(ByteOrder.LITTLE_ENDIAN);
    bytes.putShort((short) 0);//Reserved must be 0
    bytes.putShort((short) 1);//Image type
    bytes.putShort((short) 1);//Number of image in file
    bytes.put((byte) img.getWidth());//image width
    bytes.put((byte) img.getHeight());//image height
    bytes.put((byte) 0);//number of colors in color palette
    bytes.put((byte) 0);//reserved must be 0
    bytes.putShort((short) 0);//color planes
    bytes.putShort((short) 0);//bits per pixel
    bytes.putInt(imgBytes.length);//image size
    bytes.putInt(22);//image offset
    bytes.put(imgBytes);
    byte[] result = bytes.array();
    FileOutputStream fos = new FileOutputStream("C://Users//Owner//Desktop//picture.ico");
    fos.write(result);
    fos.close();
    fos.flush();

private static byte[] getImgBytes(BufferedImage img) throws IOException
{
    ByteArrayOutputStream bos = new ByteArrayOutputStream();
    ImageIO.write(img, "png", bos);
    return bos.toByteArray();
}

Проблема в том, что окна не могут открыть изображение, что выдает ошибку, когда я пытаюсь открыть изображение с помощью Windows Photo Gallery. Однако, когда я пытаюсь открыть изображение, используя gimp, изображение открывается нормально. Что я делаю неправильно. Я чувствую, что я что-то напутал в заголовке файла. Изменить: Даже незнакомец на рабочем столе картинка выглядит правильно, но только когда я пытаюсь открыть его.

На моем рабочем столе изображение выглядит так

Когда я пытаюсь открыть его в Windows Photo Gallery, отображается эта ошибка

После неудачной попытки png я попытался использовать растровое изображение, вот мой новый код

import java.awt.AWTException;
import java.awt.Color;
import java.awt.Graphics;
import java.awt.HeadlessException;
import java.awt.Rectangle;
import java.awt.Robot;
import java.awt.image.BufferedImage;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.util.Arrays;

import javax.imageio.ImageIO;

public class IconWriter
{
    public static void main(String[] args) throws HeadlessException, AWTException, IOException
    {
        BufferedImage img = new BufferedImage(16, 16, BufferedImage.TYPE_INT_RGB);
        Graphics g = img.getGraphics();
        g.setColor(Color.GREEN);
        g.fillRect(0, 0, 16, 16);
        byte[] imgBytes = getImgBytes(img);
        int fileSize = imgBytes.length + 22;
        ByteBuffer bytes = ByteBuffer.allocate(fileSize);
        bytes.order(ByteOrder.LITTLE_ENDIAN);
        bytes.putShort((short) 0);//Reserved must be 0
        bytes.putShort((short) 1);//Image type
        bytes.putShort((short) 1);//Number of images in file
        bytes.put((byte) img.getWidth());//image width
        bytes.put((byte) img.getHeight());//image height
        bytes.put((byte) 0);//number of colors in color palette
        bytes.put((byte) 0);//reserved must be 0
        bytes.putShort((short) 0);//color planes
        bytes.putShort((short) 0);//bits per pixel
        bytes.putInt(imgBytes.length);//image size
        bytes.putInt(22);//image offset
        bytes.put(imgBytes);
        byte[] result = bytes.array();
        FileOutputStream fos = new FileOutputStream("C://Users//Owner//Desktop//hi.ico");
        fos.write(result);
        fos.close();
        fos.flush();
    }

    private static byte[] getImgBytes(BufferedImage img) throws IOException
    {
        ByteArrayOutputStream bos = new ByteArrayOutputStream();
        ImageIO.write(img, "bmp", bos);
        byte[] bytes = bos.toByteArray();
        return Arrays.copyOfRange(bytes, 14, bytes.length);
    }
}

теперь, когда я пытаюсь открыть свое изображение в фотогалерее, изображение выглядит следующим образом, я понятия не имею, почему оно сейчас не работает, и особенно, почему появляются странные линии, хотя я подозреваю, что это связано с атрибутом цветовых плоскостей в ico заголовок изображения.

5 ответов

Решение

На самом деле, проблема, с которой вы столкнулись, упоминается в спецификации (в википедии). Цитата:

Изображения с глубиной цвета менее 32 бит [6] следуют определенному формату: изображение кодируется как одно изображение, состоящее из цветовой маски ("маска XOR") вместе с маской непрозрачности ("маска И").

Это очень сложно.

Создание 32-битного изображения -> не удается

Таким образом, приведенная выше цитата может заставить вас подумать: "О, я просто должен сделать изображение 32-битным, а не 24-битным", как обходной путь. К сожалению, это не сработает. Ну, на самом деле существует 32-битный формат BMP. Но последние 8 бит на самом деле не используются, потому что файлы BMP на самом деле не поддерживают прозрачность.

Таким образом, вы можете испытать желание использовать другой тип изображения: INT_ARGB_PRE который использует 32-битную глубину цвета. Но как только вы попытаетесь сохранить его с ImageIO класс, вы заметите, что ничего не происходит. Содержание потока будет null,

BufferedImage img = new BufferedImage(16, 16, BufferedImage.TYPE_INT_ARGB_PRE);
ImageIO.write(img, "bmp", bos);

Альтернативное решение: image4j

ImageIO не может обрабатывать 32-битные изображения, но есть другие библиотеки, которые могут сделать свое дело. image4J libs может сохранять 32-битные файлы bmp. Но я думаю, что по какой-то причине вы не хотите использовать эту библиотеку. (С помощью image4J сделает большую часть вашего кода выше бессмысленным, потому что image4jимеет встроенную поддержку создания ICO).

Второй вариант: создание смещенного 24-битного изображения -> работает

Итак, давайте еще раз посмотрим, что википедия говорит о < 32-битных данных BMP.

Высота изображения в структуре ICONDIRENTRY файла ICO/CUR принимает размер предполагаемых размеров изображения (после компоновки масок), тогда как высота в заголовке BMP принимает значение высоты двух комбинированных изображений маски (до того, как они составлены). Следовательно, каждая из масок должна иметь одинаковые размеры, а высота, указанная в заголовке BMP, должна в два раза превышать высоту, указанную в структуре ICONDIRENTRY.

Итак, второе решение - создать изображение, которое будет в два раза больше исходного размера. И вам на самом деле нужно только заменить свой getImageBytes функция для этого, с приведенным ниже. Как уже упоминалось выше ICONDIRENTRY заголовок, указанный в другой части кода, сохраняет высоту исходного изображения.

  private static byte[] getImgBytes(BufferedImage img) throws IOException
  {
    // create a new image, with 2x the original height.
    BufferedImage img2 = new BufferedImage(img.getWidth(), img.getHeight()*2, BufferedImage.TYPE_INT_RGB);

    // copy paste the pixels, but move them half the height.
    Raster sourceRaster = img.getRaster();
    WritableRaster destinationRaster = img2.getRaster();
    destinationRaster.setRect(0, img.getHeight(), sourceRaster);

    // save the new image to BMP format. 
    ByteArrayOutputStream bos = new ByteArrayOutputStream();
    ImageIO.write(img2, "bmp", bos);

    // strip the first 14 bytes (contains the bitmap-file-header)
    // the next 40 bytes contains the DIB header which we still need.
    // the pixel data follows until the end of the file.
    byte[] bytes = bos.toByteArray();
    return Arrays.copyOfRange(bytes, 14, bytes.length);
  }

Я предлагаю использовать заголовки следующим образом:

ByteBuffer bytes = ByteBuffer.allocate(fileSize);
bytes.order(ByteOrder.LITTLE_ENDIAN);

bytes.putShort((short) 0);
bytes.putShort((short) 1);
bytes.putShort((short) 1);
bytes.put((byte) img.getWidth());
bytes.put((byte) img.getHeight()); //no need to multiply
bytes.put((byte) img.getColorModel().getNumColorComponents()); //the pallet size
bytes.put((byte) 0);
bytes.putShort((short) 1); //should be 1
bytes.putShort((short) img.getColorModel().getPixelSize()); //bits per pixel
bytes.putInt(imgBytes.length);
bytes.putInt(22);
bytes.put(imgBytes);

Странно... но: сделайте изображение BMP в два раза выше, чем нужный значок. Сохраните заявленный размер иконки в заголовке ICO, как и раньше, только картинка должна быть выше. Затем оставьте область (0,0)-(16,16) черной (это определяет прозрачность, но я не знаю, как она кодируется, все черное для непрозрачных работ). Нарисуйте желаемое содержимое в BufferedImage в области (0,16)-(16,32). Другими словами, добавьте половину высоты ко всем координатам пикселей.

Помните, что рабочий стол Windows может кэшировать значки и отказываться обновлять их на рабочем столе. В случае сомнений откройте папку рабочего стола через другое окно проводника и выполните там "Обновление".

import java.awt.Color;
import java.awt.Graphics;
import java.awt.image.BufferedImage;
import java.io.ByteArrayOutputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.util.Arrays;

import javax.imageio.ImageIO;

public class IconWriter
{
    public static void main(String[] args) throws IOException
    {
      // note the double height
        BufferedImage img = new BufferedImage(16, 32, BufferedImage.TYPE_INT_RGB);
        Graphics g = img.getGraphics();
        g.setColor(Color.GREEN);
        g.fillRect(0, 16, 16, 16);// added 16 to y coordinate
        byte[] imgBytes = getImgBytes(img);
        int fileSize = imgBytes.length + 22;
        ByteBuffer bytes = ByteBuffer.allocate(fileSize);
        bytes.order(ByteOrder.LITTLE_ENDIAN);
        bytes.putShort((short) 0);//Reserved must be 0
        bytes.putShort((short) 1);//Image type
        bytes.putShort((short) 1);//Number of images in file
        bytes.put((byte) img.getWidth());//image width
        bytes.put((byte) (img.getHeight()>>1));//image height, half the BMP height
        bytes.put((byte) 0);//number of colors in color palette
        bytes.put((byte) 0);//reserved must be 0
        bytes.putShort((short) 0);//color planes
        bytes.putShort((short) 0);//bits per pixel
        bytes.putInt(imgBytes.length);//image size
        bytes.putInt(22);//image offset
        bytes.put(imgBytes);
        byte[] result = bytes.array();
        FileOutputStream fos = new FileOutputStream(System.getProperty("user.home")+"\\Desktop\\hi.ico");
        fos.write(result);
        fos.close();
        fos.flush();
    }

    private static byte[] getImgBytes(BufferedImage img) throws IOException
    {
        ByteArrayOutputStream bos = new ByteArrayOutputStream();
        ImageIO.write(img, "bmp", bos);
        byte[] bytes = bos.toByteArray();
        return Arrays.copyOfRange(bytes, 14, bytes.length);
    }
}

Ты пытался:

bytes.putShort((short) img.getColorModel().getPixelSize()); //bits per pixel

как видно в image4j.BMPEncoder#createInfoHeader который называется image4j.ICOEncoder#write?

Если есть другие проблемы, большая часть кода для вас, кажется, находится в этих двух методах.

Я верю bytes.putShort((short) 0);//bits per pixel должно быть изменено, чтобы иметь значение 32 вместо 0.

Если вы получаете ту маленькую картинку, которую вы отредактировали после изменения значения на 32, то я скажу, что, подумав, вероятно, это на самом деле 16.

Пожалуйста, попробуйте ниже, ImageIo поддерживает только форматы png, jpg,jpeg,gif,bmp.

Для написания иконок, пожалуйста, добавьте ниже зависимость.

      <!-- https://mvnrepository.com/artifact/net.sf.image4j/image4j -->
<dependency>
    <groupId>net.sf.image4j</groupId>
    <artifactId>image4j</artifactId>
    <version>0.7zensight1</version>
</dependency>

Использовать ICODecoder.write(image, file);

Другие вопросы по тегам