001/*
002 * Shredzone Commons - pdb
003 *
004 * Copyright (C) 2009 Richard "Shred" Körber
005 *   http://commons.shredzone.org
006 *
007 * This program is free software: you can redistribute it and/or modify
008 * it under the terms of the GNU Library General Public License as
009 * published by the Free Software Foundation, either version 3 of the
010 * License, or (at your option) any later version.
011 *
012 * This program is distributed in the hope that it will be useful,
013 * but WITHOUT ANY WARRANTY; without even the implied warranty of
014 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
015 * GNU General Public License for more details.
016 *
017 * You should have received a copy of the GNU Library General Public License
018 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
019 */
020package org.shredzone.commons.pdb.converter;
021
022import java.awt.image.BufferedImage;
023import java.awt.image.WritableRaster;
024import java.io.ByteArrayOutputStream;
025import java.io.IOException;
026
027import javax.imageio.ImageIO;
028
029import org.shredzone.commons.pdb.PdbDatabase;
030import org.shredzone.commons.pdb.PdbFile;
031import org.shredzone.commons.pdb.appinfo.CategoryAppInfo;
032import org.shredzone.commons.pdb.record.NotepadRecord;
033
034/**
035 * An {@link Converter} that handles notepad entries.
036 * <p>
037 * <em>NOTE:</em> This converter does not work in Android environments.
038 */
039public class NotepadConverter implements Converter<NotepadRecord, CategoryAppInfo> {
040
041    private static final int FLAG_TITLE = 0x0002;
042    private static final int FLAG_ALARM = 0x0004;
043
044    private static final int TYPE_BITMAP = 0; // Raw bitmap
045    private static final int TYPE_RLE    = 1; // Run Length Encoded bitmap
046    private static final int TYPE_PNG    = 2; // PNG file
047
048    @Override
049    public boolean isAcceptable(PdbDatabase<NotepadRecord, CategoryAppInfo> database) {
050        return "npadDB".equals(database.getName())
051                && "npad".equals(database.getCreator());
052    }
053
054    @Override
055    public NotepadRecord convert(PdbFile reader, int record, int size, int attribute,
056            PdbDatabase<NotepadRecord, CategoryAppInfo> database) throws IOException {
057        long current = reader.getFilePointer();
058
059        NotepadRecord result = new NotepadRecord(attribute);
060        if (result.isDelete()) {
061            return null;
062        }
063
064        result.setCreated(reader.readDateTimeWords());
065        result.setModified(reader.readDateTimeWords());
066        int flags = reader.readUnsignedShort();
067
068        if ((flags & FLAG_ALARM) != 0) {
069            result.setAlarm(reader.readDateTimeWords());
070        }
071
072        if ((flags & FLAG_TITLE) != 0) {
073            long start = reader.getFilePointer();
074            result.setTitle(reader.readTerminatedString());
075            long end = reader.getFilePointer();
076
077            // If we're on an odd position, read one padding byte to make it even
078            if (((end - start) % 2) == 1) {
079                reader.readByte();
080            }
081        }
082
083        reader.readUnsignedInt();                       // Offset to the image's end (?)
084        int width = (int) reader.readUnsignedInt();     // Full image width
085        int height = (int) reader.readUnsignedInt();    // Full image height
086        reader.readUnsignedInt();                       // Always 1 (?)
087        int type = (int) reader.readUnsignedInt();      // 0 = bitmap, 1 = RLE, 2 = PNG
088        reader.readUnsignedInt();                       // Offset to the image's end (?)
089
090        // Read image data
091        int fileSize = size - (int) (reader.getFilePointer() - current);
092        byte[] pngData = new byte[fileSize];
093        reader.readFully(pngData);
094
095        // Convert image if necessary
096        switch (type) {
097            case TYPE_RLE: //NOSONAR: falls through...
098                pngData = uncompressRle(pngData);
099
100            case TYPE_BITMAP: //NOSONAR: falls through...
101                pngData = convertToPng(width, height, pngData);
102
103            case TYPE_PNG:
104                break;
105
106            default:
107                throw new IOException("unable to handle notepad image type " + type + " at record " + record);
108        }
109
110        result.setImagePng(pngData);
111
112        return result;
113    }
114
115    @Override
116    public CategoryAppInfo convertAppInfo(PdbFile reader, int size,
117            PdbDatabase<NotepadRecord, CategoryAppInfo> database) throws IOException {
118        CategoryAppInfo result = new CategoryAppInfo();
119        reader.readCategories(result);
120        return result;
121    }
122
123    /**
124     * Uncompresses a Run Length Encoded bitmap. The compression scheme is actually very
125     * simple. The first byte gives the number of times the second byte is written (i.e.
126     * "0x06 0xC0" gives 6 times 0xC0). If the first byte is 0x00, the end of data has
127     * been reached.
128     *
129     * @param width
130     *            Image width
131     * @param height
132     *            Image height
133     * @param rle
134     *            Run Length Encoded data
135     * @return Uncompressed raw bitmap
136     */
137    private byte[] uncompressRle(byte[] rle) throws IOException {
138        try (ByteArrayOutputStream baos = new ByteArrayOutputStream()) {
139            for (int ix = 0; ix < rle.length; ix += 2) {
140                int cnt = rle[ix] & 0xFF;
141                if (cnt == 0x00) break;
142
143                byte data = rle[ix+1];
144                while (cnt-- > 0) {
145                    baos.write(data);
146                }
147            }
148
149            return baos.toByteArray();
150        }
151    }
152
153    /**
154     * Converts a raw bitmap to a PNG file.
155     *
156     * @param width
157     *            Image width
158     * @param height
159     *            Image height
160     * @param bitmap
161     *            Raw bitmap to be converted
162     * @return PNG file containing that bitmap
163     */
164    private byte[] convertToPng(int width, int height, byte[] bitmap) throws IOException {
165        BufferedImage image = new BufferedImage(width, height, BufferedImage.TYPE_BYTE_BINARY);
166        WritableRaster raster = image.getRaster();
167
168        // Make the width a multiple of 16 first
169        int bytesPerRow = ((((width - 1) / 16) + 1) * 16) / 8;
170
171        for (int w = 0; w < width; w++) {
172            for (int h = 0; h < height; h++) {
173                byte data = bitmap[(h * bytesPerRow) + (w / 8)];
174                if ((data & (0x80 >> (w % 8))) == 0) { //NOSONAR: int promotion is safe
175                    raster.setSample(w,h,0,1);
176                }
177            }
178        }
179
180        try (ByteArrayOutputStream baos = new ByteArrayOutputStream()) {
181            ImageIO.write(image, "PNG", baos);
182            return baos.toByteArray();
183        }
184    }
185
186}