From 26d254a3ac6cddbd3583cbbcbf8d43aa15c6a32e Mon Sep 17 00:00:00 2001
From: Niki Roo
Date: Sat, 26 Mar 2016 22:15:09 +0100
Subject: [PATCH] New option: save/load contacts' photos
---
src/be/nikiroo/jvcard/Contact.java | 59 +++++-
src/be/nikiroo/jvcard/launcher/Main.java | 186 ++++++++++++++++--
.../nikiroo/jvcard/resources/StringUtils.java | 55 +++++-
.../jvcard/resources/display.properties | 4 +
.../jvcard/tui/panes/ContactDetails.java | 12 +-
.../nikiroo/jvcard/tui/panes/ContactList.java | 5 +-
6 files changed, 282 insertions(+), 39 deletions(-)
diff --git a/src/be/nikiroo/jvcard/Contact.java b/src/be/nikiroo/jvcard/Contact.java
index 1b88896..779b60a 100644
--- a/src/be/nikiroo/jvcard/Contact.java
+++ b/src/be/nikiroo/jvcard/Contact.java
@@ -113,6 +113,7 @@ public class Contact extends BaseClass {
* Return a {@link String} representation of this contact formated
* accordingly to the given format.
*
+ *
* The format is basically a list of field names separated by a pipe and
* optionally parametrised. The parameters allows you to:
*
@@ -120,31 +121,48 @@ public class Contact extends BaseClass {
* - @n: limit the size to a fixed value 'n'
* - @+: expand the size of this field as much as possible
*
+ *
*
- * Example: "N@10|FN@20|NICK@+|PHOTO@x"
+ *
+ * You can also add a fixed text if it starts with a simple-quote (').
+ *
+ *
+ *
+ * Example: "'Contact: |N@10|FN@20|NICK@+|PHOTO@x"
+ *
*
* @param format
* the format to use
+ * @param separator
+ * the separator {@link String} to use between fields
*
* @return the {@link String} representation
*/
- public String toString(String format) {
- return toString(format, "|", null, -1, true, false);
+ public String toString(String format, String separator) {
+ return toString(format, separator, null, -1, true, false);
}
/**
* Return a {@link String} representation of this contact formated
* accordingly to the given format.
*
+ *
* The format is basically a list of field names separated by a pipe and
* optionally parametrised. The parameters allows you to:
*
- * - @x: (the 'x' is the letter 'x') show only a present/not present info
+ * - @x: show only a present/not present info
* - @n: limit the size to a fixed value 'n'
* - @+: expand the size of this field as much as possible
*
+ *
+ *
+ *
+ * You can also add a fixed text if it starts with a simple-quote (').
+ *
*
- * Example: "N@10|FN@20|NICK@+|PHOTO@x"
+ *
+ * Example: "'Contact: |N@10|FN@20|NICK@+|PHOTO@x"
+ *
*
* @param format
* the format to use
@@ -176,6 +194,7 @@ public class Contact extends BaseClass {
* Return a {@link String} representation of this contact formated
* accordingly to the given format, part by part.
*
+ *
* The format is basically a list of field names separated by a pipe and
* optionally parametrised. The parameters allows you to:
*
@@ -183,8 +202,15 @@ public class Contact extends BaseClass {
* - @n: limit the size to a fixed value 'n'
* - @+: expand the size of this field as much as possible
*
+ *
*
- * Example: "N@10|FN@20|NICK@+|PHOTO@x"
+ *
+ * You can also add a fixed text if it starts with a simple-quote (').
+ *
+ *
+ *
+ * Example: "'Contact: |N@10|FN@20|NICK@+|PHOTO@x"
+ *
*
* @param format
* the format to use
@@ -236,6 +262,7 @@ public class Contact extends BaseClass {
* Return a {@link String} representation of this contact formated
* accordingly to the given format, part by part.
*
+ *
* The format is basically a list of field names separated by a pipe and
* optionally parametrised. The parameters allows you to:
*
@@ -243,8 +270,15 @@ public class Contact extends BaseClass {
* - @n: limit the size to a fixed value 'n'
* - @+: expand the size of this field as much as possible
*
+ *
+ *
+ *
+ * You can also add a fixed text if it starts with a simple-quote (').
+ *
*
- * Example: "N@10|FN@20|NICK@+|PHOTO@x"
+ *
+ * Example: "'Contact: |N@10|FN@20|NICK@+|PHOTO@x"
+ *
*
* @param format
* the format to use
@@ -280,7 +314,8 @@ public class Contact extends BaseClass {
boolean binary = false;
boolean expand = false;
- if (field.contains("@")) {
+ if (field.length() > 0 && field.charAt(0) != '\''
+ && field.contains("@")) {
String[] opts = field.split("@");
if (opts.length > 0)
field = opts[0];
@@ -300,7 +335,13 @@ public class Contact extends BaseClass {
}
}
- String value = getPreferredDataValue(field);
+ String value = null;
+ if (field.length() > 0 && field.charAt(0) == '\'') {
+ value = field.substring(1);
+ } else {
+ value = getPreferredDataValue(field);
+ }
+
if (value == null) {
value = "";
} else {
diff --git a/src/be/nikiroo/jvcard/launcher/Main.java b/src/be/nikiroo/jvcard/launcher/Main.java
index 1a77291..9df98e2 100644
--- a/src/be/nikiroo/jvcard/launcher/Main.java
+++ b/src/be/nikiroo/jvcard/launcher/Main.java
@@ -8,7 +8,12 @@ import java.nio.charset.Charset;
import java.util.LinkedList;
import java.util.List;
+import javax.imageio.ImageIO;
+
import be.nikiroo.jvcard.Card;
+import be.nikiroo.jvcard.Contact;
+import be.nikiroo.jvcard.Data;
+import be.nikiroo.jvcard.TypeInfo;
import be.nikiroo.jvcard.launcher.CardResult.MergeCallback;
import be.nikiroo.jvcard.parsers.Format;
import be.nikiroo.jvcard.remote.Command;
@@ -35,6 +40,10 @@ public class Main {
static private final int ERR_INTERNAL = 3;
static private Trans transService;
+ enum Mode {
+ CONTACT_MANAGER, I18N, SERVER, LOAD_PHOTO, SAVE_PHOTO, ONLY_PHOTO,
+ }
+
/**
* Translate the given {@link StringId} into user text.
*
@@ -86,9 +95,11 @@ public class Main {
transService = new Trans(language);
boolean unicode = transService.isUnicode();
- String i18nDir = null;
+ String dir = null;
List files = new LinkedList();
- Integer port = null;
+ int port = -1;
+ Mode mode = Mode.CONTACT_MANAGER;
+ String format = null;
for (int index = 0; index < args.length; index++) {
String arg = args[index];
if (!noMoreParams && arg.equals("--")) {
@@ -106,6 +117,9 @@ public class Main {
+ "\t--config DIRECTORY: force the given directory as a CONFIG_DIR\n"
+ "\t--server PORT: start a remoting server instead of a client\n"
+ "\t--i18n DIR: generate the translation file for the given language (can be \"\") to/from the .properties given dir\n"
+ + "\t--save-photo DIR FORMAT: save the contacts' photos to DIR, named after FORMAT\n"
+ + "\t--load-photo DIR FORMAT: load the contacts' photos from DIR, named after FORMAT\n"
+ + "\t--only-photo DIR FORMAT: load the contacts' photos from DIR, named after FORMAT, overwrite all other photos of selected contacts\n"
+ "everyhing else is either a file to open or a directory to open\n"
+ "(we will only open 1st level files in given directories)\n"
+ "('jvcard://hostname:8888/file' links -- or without 'file' -- are also ok)\n");
@@ -141,6 +155,19 @@ public class Main {
transService = new Trans(language);
transService.setUnicode(unicode);
} else if (!noMoreParams && arg.equals("--server")) {
+ if (mode != Mode.CONTACT_MANAGER) {
+ System.err
+ .println("Syntax error: you can only use one of: \n"
+ + "--server\n"
+ + "--i18n\n"
+ + "--load-photo\n"
+ + "--save-photo\n"
+ + "--only-photo\n");
+ System.exit(ERR_SYNTAX);
+ return;
+ }
+ mode = Mode.SERVER;
+
index++;
if (index >= args.length) {
System.err.println("Syntax error: no port given");
@@ -156,6 +183,19 @@ public class Main {
return;
}
} else if (!noMoreParams && arg.equals("--i18n")) {
+ if (mode != Mode.CONTACT_MANAGER) {
+ System.err
+ .println("Syntax error: you can only use one of: \n"
+ + "--server\n"
+ + "--i18n\n"
+ + "--load-photo\n"
+ + "--save-photo\n"
+ + "--only-photo\n");
+ System.exit(ERR_SYNTAX);
+ return;
+ }
+ mode = Mode.I18N;
+
index++;
if (index >= args.length) {
System.err
@@ -164,7 +204,48 @@ public class Main {
return;
}
- i18nDir = args[index];
+ dir = args[index];
+ } else if (!noMoreParams
+ && (arg.equals("--load-photo")
+ || arg.equals("--save-photo") || arg
+ .equals("--only-photo"))) {
+ if (mode != Mode.CONTACT_MANAGER) {
+ System.err
+ .println("Syntax error: you can only use one of: \n"
+ + "--server\n"
+ + "--i18n\n"
+ + "--load-photo\n"
+ + "--save-photo\n"
+ + "--only-photo\n");
+ System.exit(ERR_SYNTAX);
+ return;
+ }
+
+ if (arg.equals("--load-photo")) {
+ mode = Mode.LOAD_PHOTO;
+ } else if (arg.equals("--save-photo")) {
+ mode = Mode.SAVE_PHOTO;
+ } else {
+ mode = Mode.ONLY_PHOTO;
+ }
+
+ index++;
+ if (index >= args.length) {
+ System.err.println("Syntax error: photo directory given");
+ System.exit(ERR_SYNTAX);
+ return;
+ }
+
+ dir = args[index];
+
+ index++;
+ if (index >= args.length) {
+ System.err.println("Syntax error: photo format given");
+ System.exit(ERR_SYNTAX);
+ return;
+ }
+
+ format = args[index];
} else {
filesTried = true;
files.addAll(open(arg));
@@ -172,7 +253,7 @@ public class Main {
}
// Force headless mode if we run in forced-text mode
- if (textMode != null && textMode) {
+ if (mode != Mode.CONTACT_MANAGER || (textMode != null && textMode)) {
// same as -Djava.awt.headless=true
System.setProperty("java.awt.headless", "true");
}
@@ -182,23 +263,20 @@ public class Main {
}
// Error management:
- if (port != null && files.size() > 0) {
+ if (mode == Mode.SERVER && files.size() > 0) {
System.err
.println("Invalid syntax: you cannot both use --server and provide card files");
System.exit(ERR_SYNTAX);
- } else if (i18nDir != null && files.size() > 0) {
+ } else if (mode == Mode.I18N && files.size() > 0) {
System.err
.println("Invalid syntax: you cannot both use --i18n and provide card files");
System.exit(ERR_SYNTAX);
- } else if (port != null && i18nDir != null) {
- System.err
- .println("Invalid syntax: you cannot both use --server and --i18n");
- System.exit(ERR_SYNTAX);
- } else if (i18nDir != null && language == null) {
+ } else if (mode == Mode.I18N && language == null) {
System.err
.println("Invalid syntax: you cannot use --i18n without --lang");
System.exit(ERR_SYNTAX);
- } else if (port == null && i18nDir == null && files.size() == 0) {
+ } else if ((mode == Mode.CONTACT_MANAGER || mode == Mode.SAVE_PHOTO || mode == Mode.LOAD_PHOTO)
+ && files.size() == 0) {
if (files.size() == 0 && !filesTried) {
files.addAll(open("."));
}
@@ -211,7 +289,8 @@ public class Main {
}
//
- if (port != null) {
+ switch (mode) {
+ case SERVER: {
try {
Optional.runServer(port);
} catch (Exception e) {
@@ -223,15 +302,86 @@ public class Main {
System.exit(ERR_INTERNAL);
}
}
- } else if (i18nDir != null) {
+ break;
+ }
+ case I18N: {
try {
- Trans.generateTranslationFile(i18nDir, language);
+ Trans.generateTranslationFile(dir, language);
} catch (IOException e) {
System.err
.println("I/O Exception: Cannot create/update a language in directory: "
- + i18nDir);
+ + dir);
}
- } else {
+ break;
+ }
+ case ONLY_PHOTO:
+ case LOAD_PHOTO: {
+ for (String file : files) {
+ try {
+ Card card = getCard(file, null).getCard();
+ for (Contact contact : card) {
+ String filename = contact.toString(format, "");
+ File f = new File(dir, filename + ".png");
+
+ if (f.exists()) {
+ try {
+ String b64 = StringUtils.fromImage(ImageIO
+ .read(f));
+
+ if (mode == Mode.ONLY_PHOTO) {
+ for (Data photo = contact
+ .getPreferredData("PHOTO"); photo != null; photo = contact
+ .getPreferredData("PHOTO")) {
+ photo.delete();
+ }
+ }
+
+ List types = new LinkedList();
+ types.add(new TypeInfo("ENCODING", "b"));
+ types.add(new TypeInfo("TYPE", "png"));
+ Data photo = new Data(types, "PHOTO", b64, null);
+ contact.add(photo);
+ } catch (IOException e) {
+ System.err.println("Cannot read photo: "
+ + filename);
+ }
+ }
+ }
+ card.save();
+ } catch (IOException e) {
+ System.err.println("Card cannot be opened: " + file);
+ }
+ }
+ break;
+ }
+ case SAVE_PHOTO: {
+ for (String file : files) {
+ try {
+ Card card = getCard(file, null).getCard();
+ for (Contact contact : card) {
+ Data photo = contact.getPreferredData("PHOTO");
+ if (photo != null) {
+ String filename = contact.toString(format, "");
+ File f = new File(dir, filename + ".png");
+ try {
+ ImageIO.write(
+ StringUtils.toImage(photo.getValue()),
+ "png", f);
+ } catch (IOException e) {
+ System.err
+ .println("Cannot save photo of contact: "
+ + contact
+ .getPreferredDataValue("FN"));
+ }
+ }
+ }
+ } catch (IOException e) {
+ System.err.println("Card cannot be opened: " + file);
+ }
+ }
+ break;
+ }
+ case CONTACT_MANAGER: {
try {
Optional.startTui(textMode, files);
} catch (Exception e) {
@@ -243,6 +393,8 @@ public class Main {
System.exit(ERR_INTERNAL);
}
}
+ break;
+ }
}
}
diff --git a/src/be/nikiroo/jvcard/resources/StringUtils.java b/src/be/nikiroo/jvcard/resources/StringUtils.java
index 2009dce..cee9fb6 100644
--- a/src/be/nikiroo/jvcard/resources/StringUtils.java
+++ b/src/be/nikiroo/jvcard/resources/StringUtils.java
@@ -1,5 +1,10 @@
package be.nikiroo.jvcard.resources;
+import java.awt.Image;
+import java.awt.image.BufferedImage;
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.text.Normalizer;
@@ -9,6 +14,9 @@ import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.regex.Pattern;
+import javax.imageio.ImageIO;
+import javax.xml.bind.DatatypeConverter;
+
import com.googlecode.lanterna.gui2.LinearLayout.Alignment;
public class StringUtils {
@@ -49,7 +57,7 @@ public class StringUtils {
*/
static public String padString(String text, int width, boolean cut,
Alignment align) {
-
+
if (width >= 0) {
if (text == null)
text = "";
@@ -172,6 +180,51 @@ public class StringUtils {
}
}
+ /**
+ * Convert the given {@link Image} object into a Base64 representation of
+ * the same {@link Image}. object.
+ *
+ * @param image
+ * the {@link Image} object to convert
+ *
+ * @return the Base64 representation
+ */
+ static public String fromImage(BufferedImage image) {
+ String imageString = null;
+ ByteArrayOutputStream out = new ByteArrayOutputStream();
+
+ try {
+ ImageIO.write(image, "png", out);
+ byte[] imageBytes = out.toByteArray();
+
+ imageString = DatatypeConverter.printBase64Binary(imageBytes);
+
+ out.close();
+ } catch (IOException e) {
+ }
+
+ return imageString;
+ }
+
+ /**
+ * Convert the given Base64 representation of an image into an {@link Image}
+ * object.
+ *
+ * @param b64data
+ * the {@link Image} in Base64 format
+ *
+ * @return the {@link Image} object
+ *
+ * @throws IOException
+ * in case of IO error
+ */
+ static public BufferedImage toImage(String b64data) throws IOException {
+ BufferedImage image = ImageIO.read(new ByteArrayInputStream(
+ DatatypeConverter.parseBase64Binary(b64data)));
+ image.toString();
+ return image;
+ }
+
/**
* Return a hash of the given {@link String}.
*
diff --git a/src/be/nikiroo/jvcard/resources/display.properties b/src/be/nikiroo/jvcard/resources/display.properties
index 106bdc3..bdb370e 100644
--- a/src/be/nikiroo/jvcard/resources/display.properties
+++ b/src/be/nikiroo/jvcard/resources/display.properties
@@ -6,6 +6,10 @@
# - @x: (the 'x' is the letter 'x') show only a present/not present info
# - @n: limit the size to a fixed value 'n'
# - @+: expand the size of this field as much as possible
+#
+# You can also add a fixed text if it starts with a simple-quote (').
+#
+# Example: "'Contact: |N@10|FN@20|NICK@+|PHOTO@x"
#
# You can cycle through them if you have more than one
# (in this case, separate them with a comma (',')
diff --git a/src/be/nikiroo/jvcard/tui/panes/ContactDetails.java b/src/be/nikiroo/jvcard/tui/panes/ContactDetails.java
index 1d6beb9..5756c7c 100644
--- a/src/be/nikiroo/jvcard/tui/panes/ContactDetails.java
+++ b/src/be/nikiroo/jvcard/tui/panes/ContactDetails.java
@@ -1,15 +1,11 @@
package be.nikiroo.jvcard.tui.panes;
import java.awt.Image;
-import java.io.ByteArrayInputStream;
import java.util.LinkedList;
import java.util.List;
import java.util.MissingResourceException;
import java.util.ResourceBundle;
-import javax.imageio.ImageIO;
-import javax.xml.bind.DatatypeConverter;
-
import be.nikiroo.jvcard.Contact;
import be.nikiroo.jvcard.Data;
import be.nikiroo.jvcard.TypeInfo;
@@ -191,8 +187,7 @@ public class ContactDetails extends MainContent {
Data photo = contact.getPreferredData("PHOTO");
if (photo != null) {
TypeInfo encoding = null;
- for (int index = 0; index < photo.size(); index++) {
- TypeInfo info = photo.get(index);
+ for (TypeInfo info : photo) {
if (info.getName() != null) {
if (info.getName().equalsIgnoreCase("ENCODING"))
encoding = info;
@@ -205,10 +200,7 @@ public class ContactDetails extends MainContent {
&& encoding.getValue().equalsIgnoreCase("b")) {
try {
- image = ImageIO.read(new ByteArrayInputStream(
- DatatypeConverter.parseBase64Binary(photo
- .getValue())));
- image.toString();
+ image = StringUtils.toImage(photo.getValue());
} catch (Exception e) {
System.err.println("Cannot parse image for contact: "
+ contact.getPreferredDataValue("UID"));
diff --git a/src/be/nikiroo/jvcard/tui/panes/ContactList.java b/src/be/nikiroo/jvcard/tui/panes/ContactList.java
index a68efa2..600f90c 100644
--- a/src/be/nikiroo/jvcard/tui/panes/ContactList.java
+++ b/src/be/nikiroo/jvcard/tui/panes/ContactList.java
@@ -56,7 +56,7 @@ public class ContactList extends MainContentList {
if (card != null) {
for (Contact c : card) {
if (filter == null
- || c.toString(format).toLowerCase()
+ || c.toString(format, "|").toLowerCase()
.contains(filter.toLowerCase())) {
addItem("x");
contacts.add(c);
@@ -130,7 +130,8 @@ public class ContactList extends MainContentList {
if (contact != null)
contactName = "" + contact.getPreferredDataValue("FN");
- return Main.trans(Trans.StringId.CONFIRM_USER_DELETE_CONTACT, contactName);
+ return Main.trans(Trans.StringId.CONFIRM_USER_DELETE_CONTACT,
+ contactName);
}
@Override
--
2.27.0