/* * Kontalk Java client * Copyright (C) 2016 Kontalk Devteam <devteam@kontalk.org> * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see <http://www.gnu.org/licenses/>. */ package org.kontalk.model; import javax.imageio.ImageIO; import java.awt.image.BufferedImage; import java.io.ByteArrayOutputStream; import java.io.File; import java.io.IOException; import java.nio.file.Path; import java.util.Objects; import java.util.Optional; import java.util.logging.Level; import java.util.logging.Logger; import org.apache.commons.codec.digest.DigestUtils; import org.kontalk.util.MediaUtils; /** * Avatar image. Immutable. * * @author Alexander Bikadorov {@literal <bikaejkb@mail.tu-berlin.de>} */ public abstract class Avatar { private static final Logger LOGGER = Logger.getLogger(Avatar.class.getName()); private static final String DIR = "avatars"; private static final String FORMAT = "png"; static void createStorageDir(Path appDir) { boolean created = appDir.resolve(DIR).toFile().mkdir(); if (created) LOGGER.info("created avatar directory"); } /** SHA1 hash of image data. */ private final String mID; final File mFile; BufferedImage mImage = null; private Avatar(String id, File file, BufferedImage image) { mID = id; mFile = file != null ? file : avatarFile(mID); mImage = image; if (mImage != null) { // save new image boolean succ = MediaUtils.writeImage(mImage, FORMAT, mFile); if (!succ) LOGGER.warning("can't save avatar image: "+mID); } } private Avatar(File file) { mFile = file; mImage = file.isFile() ? image(mFile) : null; mID = mImage != null ? id(mImage) : ""; } public String getID() { return mID; } public Optional<BufferedImage> loadImage() { if (mImage == null) mImage = image(mFile); return Optional.ofNullable(mImage); } void delete() { boolean succ = mFile.delete(); if (!succ) LOGGER.warning("could not delete avatar file: "+mID); } boolean abstractEquals(Avatar oAvatar) { return mID.equals(oAvatar.mID); } int abstractHashCode() { int hash = 7; hash = 59 * hash + Objects.hashCode(this.mID); return hash; } public static class DefaultAvatar extends Avatar { /** Saved published contact avatar. */ static Optional<DefaultAvatar> load(String id) { File file = avatarFile(id); if (!file.isFile()) { LOGGER.warning("no file: "+file); return Optional.empty(); } return Optional.of(new DefaultAvatar(id, file)); } private DefaultAvatar(String id, File file) { super(id, file, null); } /** New published contact avatar. */ public DefaultAvatar(String id, BufferedImage image) { super(id, null, image); } @Override public final boolean equals(Object o) { if (this == o) return true; if (!(o instanceof DefaultAvatar)) return false; return this.abstractEquals((DefaultAvatar) o); } @Override public int hashCode() { int hash = 3 * this.abstractHashCode(); return hash; } } public static class CustomAvatar extends Avatar { // custom set avatars have always same ID for one contact, // using this to distinguish them private final long mLastModified; static Optional<CustomAvatar> load(int contactID) { String id = Integer.toString(contactID); return avatarFile(id).isFile() ? Optional.of(new CustomAvatar(id, null)) : Optional.empty(); } private CustomAvatar(String id, File file) { super(id, file, null); mLastModified = mFile.lastModified(); } /** New custom contact avatar. */ public CustomAvatar(int contactID, BufferedImage image) { super(Integer.toString(contactID), null, image); mLastModified = mFile.lastModified(); } @Override public final boolean equals(Object o) { if (this == o) return true; if (!(o instanceof CustomAvatar)) return false; CustomAvatar oAvatar = (CustomAvatar) o; return this.abstractEquals(oAvatar) && mLastModified == oAvatar.mLastModified; } @Override public int hashCode() { int hash = 3 * this.abstractHashCode(); hash = 37 * hash + (int) (this.mLastModified ^ (this.mLastModified >>> 32)); return hash; } } public static class UserAvatar extends Avatar { private static final int MAX_SIZE = 150; private static final String USER_FILENAME = "avatar"; private static UserAvatar INSTANCE = null; private byte[] mImageData = null; /** Saved user Avatar. */ public static Optional<UserAvatar> get() { if (INSTANCE != null) return Optional.of(INSTANCE); File file = userFile(); return file.isFile() ? Optional.of(INSTANCE = new UserAvatar(file)) : Optional.empty(); } private UserAvatar(File file) { super(file); } public static UserAvatar set(BufferedImage image) { return INSTANCE = new UserAvatar(MediaUtils.scale(image, MAX_SIZE, MAX_SIZE)); } /** New user Avatar. ID generated from image. */ private UserAvatar(BufferedImage image) { super(id(image), userFile(), image); } public static void remove() { if (INSTANCE == null) { LOGGER.warning("not set"); return; } INSTANCE.delete(); INSTANCE = null; } public Optional<byte[]> imageData() { if (mImageData == null) mImageData = Avatar.imageData(mImage); return Optional.ofNullable(mImageData); } private static File userFile() { return Model.appDir().resolve(USER_FILENAME + "." + FORMAT).toFile(); } } private static File avatarFile(String id){ return Model.appDir().resolve(DIR).resolve(id + "." + FORMAT).toFile(); } private static String id(BufferedImage image) { byte[] imageData = imageData(image); return imageData != null ? DigestUtils.sha1Hex(imageData) : ""; } private static byte[] imageData(BufferedImage image) { ByteArrayOutputStream out = new ByteArrayOutputStream(); try { ImageIO.write(image, FORMAT, out); } catch (IOException ex) { LOGGER.log(Level.WARNING, "can't convert avatar", ex); return null; } return out.toByteArray(); } private static BufferedImage image(File file) { return MediaUtils.readImage(file).orElse(null); } }