Search This Blog

Saturday 1 June 2013

Reading image properties in an Adobe LiveCycle Designer Form

A LiveCycle Designer form allows a user to insert a picture using an ImageField.  However, there is nothing built-in to allow access the properties of an image.  This set of script fragments makes the Exif, GPS and XMP properties of a JPEG image available.

You can also use these code fragments to determine if the image is a JPEG, GIF, PNG or TIFF.

If you just want to check the size of an image then you can use the rawValue property but as the value is base 64 encoded you will need to allow for an overhead of 33%, so ImageField1.rawValue.length * 3 / 4 will give a rough image size in bytes.

LiveCycle Designer forms allow for JPG, GIF, TIF, and PNG formats.  An ImageField allows the user to select one of these images but only if the file extension matches one of those four.  If the file extension is JPEG, JPE, or TIFF then the image will not show up in the "Select Image File" dialog.  If this is a problem for your users then you can use the importDataObject, something like;

if (event.target.importDataObject("name")) // user did not cancel the "select a data file to import" dialog
{
 var attachmentObject = event.target.getDataObject("name");
 var filetype = attachmentObject.path.substring(attachmentObject.path.lastIndexOf(".") + 1);
 if (filetype === "jpg")
 {
  var imageStream = event.target.getDataObjectContents("name");
  var imageStreamEncoded = Net.streamEncode(imageStream, "base64");
  ImageField1.rawValue = util.stringFromStream(imageStreamEncoded);
 }
}


Back to this sample, to find the date a picture was taken we can now use;

var stream = ByteStream.newByteStream(Base64.decode(ImageField1.rawValue));
var image = JPEG.newJPEG(stream);
if (image.isJPEG)
{
 app.alert(image.getDateTime());
}
else
{
 image = GIF.newGIF(stream);
 if (image.isGIF)
 {
 }
 else
 {
  image = TIFF.newTIFF(stream);
  if (image.isTIFF)
  {
  }
  else
  {
   image = PNG.newPNG(stream);
   if (image.isPNG)
   {
   }
  }
 }
}


There is a bug in the base 64 decode routines built into Adobe Reader but this has not yet proved to be a problem as it only seems to occur towards the end of the file were typically the image data is stored.  However, included in this sample is a JavaScript based base 64 decoded.  This does execute slower so I would only use it if this proves to be a problem, just change the first line to;

var stream = ByteStream.newByteStream(Base64.decode(this.rawValue));

In the sample form there are four options;
  • Turn on JavaScript decoding
  • Display a message if a decode error occurs
  • Stop decoding when the SOF jpeg tag is found.  The only thing that may be missed is the JPEG comment tag, but this seems to be rarely used or superseded by the Exif properties.  This option should speed up the decode.
  • Turn on and off displaying of the decode time.
The code in the sample form starts in the change event of the image field in the top right hand corner (with the "Click here to add photo" caption).  This loads the image and then executes the initialize event of the main ImageField to display all the image properties.  I did it this way because I am using the dataWindow object to allowing scrolling though pictures, without the dataWindow you could perform all processing in the ImageField's change event.



 The form template (without pictures of my cat) can also be downloaded, ImageViewer.xdp

16 comments:

  1. Hi,

    this is a really brilliant sample!

    Btw: What bug with Base64 do you mean?

    ReplyDelete
  2. Hi Radzmar,

    The bug with the base 64 encoding seems to be related to a null character in the input stream, which causes it to stop the encoding process. So if I was to encode ‘abc’ in the javascript console;

    console.println(Net.stringFromStream(Net.streamEncode(Net.streamFromString('abc'),'base64')))

    I would get 'YWJj', then if I was to decode it with;

    console.println(Net.stringFromStream(Net.streamDecode(Net.streamFromString('YWJj'),'base64')))

    I get back to 'abc', which is correct.

    However, if I try it with a null character in the string (the \x00 bit)

    console.println(Net.stringFromStream(Net.streamEncode(Net.streamFromString('ab\x00c'),'base64')))

    I get 'YWI=', which is the same as if the input string was 'ab', and that is what I get if I decode;

    console.println(Net.stringFromStream(Net.streamDecode(Net.streamFromString('YWI='),'base64')).toSource())

    John Brinkman referred to this issue in his blog http://blogs.adobe.com/formfeed/2009/08/base64_encode_a_pdf_attachment.html

    We generally use webservices for our form submissions and we wanted to be able to include attachments. Using base 64 encoding worked for simple text files, but if it was a Word document (or pretty much anything else) it was failing. We ended up using hex encoding which worked fine but means the attachment is twice the size when it is submitted. So we tend to limit an attachment to 5mb in the form (which means 10mb in the submission)

    This bug has also caused us problems trying to programmatically load images (using the FormCalc GET)

    ReplyDelete
  3. Interesting Bruce,

    I had a similar Problem when I saved Images from Images fields as attachments.
    All Images above 50KB got damaged.

    I was able to solve it with the replace method and then also could decode 5 or 20 MB Images without any Problems.

    var b64Data = ImageField1.value.oneOfChild.value.replace(/\n/gm, "");
    var ReadStream = Net.streamFromString(b64Data);
    var DecodedStream = Net.streamDecode(ReadStream, "base64");
    var NewAttachmentName = "MyExportImage_" + ".png";
    event.target.createDataObject(NewAttachmentName, "", "image/png");
    event.target.setDataObjectContents(NewAttachmentName, DecodedStream);

    So this may also work for you.

    var str = 'ab\x00c'
    Net.stringFromStream(Net.streamEncode(Net.streamFromString(str.replace(/\x00|\n/gm, "")),'base64')) returns 'YWJj' as it should.

    ReplyDelete
  4. Hi Radzmar,

    I’m not sure about your base 64 encoding, if I encode the string 'ab\x00c' I get ‘YWIAYw’ or ‘YWIAYw==’ with the padding

    Follow these steps;
    Text 'ab\x00c
    ASCII 97, 98, 00, 99
    Binary 01100001 01100010 00000000 01100011
    64 Bit Binary 011000 010110 001000 000000 011000 110000
    Base 64 Indexes 24 22 8 0 24 48

    Base 64 String (apply the indexes to this string) ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/
    1 2 3 4 5 6
    0123456789012345678901234567890123456789012345678901234567890123

    Base 64 Text YWIAYw

    I agree with your first comment, the value of an image field includes a newline character every 76 characters (just like it does if you "Embed Image Data" and then look in the XML Source). This is the main reason ImageField1.rawValue.length * 3 / 4 only gives you a rough image size (there can also be up to 3 padding characters).

    Bruce

    ReplyDelete
  5. Hi, I'm working on a project with LiveCycle, I need to reduce the size of an image (4 megabytes) to a smaller size, it can be done, automatically, with Livecycle.
    Hopefully you can help me, thanks

    ReplyDelete
    Replies
    1. Hi Emilio,

      Niall has a sample that maybe doing what you want, have a look at http://assuredynamics.com/index.php/portfolio/scaling-images/

      Regards

      Bruce

      Delete
    2. I want to reduce the size of 4 megabytes to 1 megabyte, not the size of visualization. This code is not useful to me. thanks anyway

      Delete
    3. This comment has been removed by the author.

      Delete
    4. I finally reduced the size of the images using the iTextSharp library for c #. Here is the code here if anyone is interested

      Delete
    5. private static string reducirResolucionImagen(int idOperador, string imagenCadena, Int32 ancho)
      {
      if (imagenCadena == string.Empty) { return ""; }

      //recupero la imagen almacenada en una matriz de bytes
      byte[] arrayBytes = Convert.FromBase64String(imagenCadena);


      string cadenaImagen;

      //USING LIBERA DE LA MEMORIA el objeto stmBitmap UNA VEZ FINALIZADO EL BLOQUE, realiza de forma automatica un DISPOSE
      using (System.IO.MemoryStream stmBitmap = new System.IO.MemoryStream(arrayBytes, 0, arrayBytes.Length))
      { // Escribe el array de bytes en el stream de memoria
      stmBitmap.Write(arrayBytes, 0, arrayBytes.Length);
      // Carga la imagen del stream
      using (System.Drawing.Image ImagenFoto = System.Drawing.Image.FromStream(stmBitmap))
      {
      // Cierra el stream
      stmBitmap.Close();

      //si el ancho de la imagen es menor o igual no hago nada
      if (ImagenFoto.Width <= ancho) { return ""; }

      //calculo el alto
      Int32 alto = (ancho * ImagenFoto.Height) / ImagenFoto.Width;

      //este bloque USING libera de forma automatica el objeto imagenBitmap una vez finalizado el bloque
      using (Bitmap imagenBitmap = new Bitmap(ancho, alto, System.Drawing.Imaging.PixelFormat.Format32bppRgb))
      {
      imagenBitmap.SetResolution(Convert.ToInt32(ImagenFoto.HorizontalResolution), Convert.ToInt32(ImagenFoto.HorizontalResolution));

      using (Graphics imagenGraphics = Graphics.FromImage(imagenBitmap))
      {

      imagenGraphics.SmoothingMode = System.Drawing.Drawing2D.SmoothingMode.AntiAlias;
      imagenGraphics.InterpolationMode = System.Drawing.Drawing2D.InterpolationMode.HighQualityBicubic;
      imagenGraphics.PixelOffsetMode = System.Drawing.Drawing2D.PixelOffsetMode.HighQuality;
      imagenGraphics.DrawImage(ImagenFoto, new System.Drawing.Rectangle(0, 0, ancho, alto), new System.Drawing.Rectangle(0, 0, ImagenFoto.Width, ImagenFoto.Height), GraphicsUnit.Pixel);
      }
      //Nota: no hace falta uso el bloque using libero la memoria
      //if (imagenGraphics != null) { imagenGraphics.Dispose(); }
      //if (ImagenFoto != null) { ImagenFoto.Dispose(); }

      using (MemoryStream imagenMemoryStream = new MemoryStream())
      {
      imagenBitmap.Save(imagenMemoryStream, System.Drawing.Imaging.ImageFormat.Jpeg);
      //puedo guardar la imagen en el disco duro
      //imagenBitmap.Save(@"C:\Users\emilioVillar\Desktop\CODIGOPRUEBA\" + (new Random(DateTime.Now.Millisecond)).Next() + "RESOLUCION.jpeg", System.Drawing.Imaging.ImageFormat.Jpeg);

      //devuelvo la imagen en formato cadena de texto
      cadenaImagen = Convert.ToBase64String(imagenMemoryStream.ToArray());
      if (imagenMemoryStream != null) { imagenMemoryStream.Close(); }
      }
      }
      }

      }

      return cadenaImagen;
      }

      Delete
  6. Is there a way to get an image's file name?

    ReplyDelete
    Replies
    1. Hi, Marian. As far as I know there is no way to get the file system filename. Maybe you could have a button and use the importDataObject and getDataObjectContents methods and try and load an Image object with that?

      Delete
  7. Hi, how would I go about getting the image dimensions from an image field? I see that you have done it in this example but I'm finding it hard to follow.

    ReplyDelete
  8. Hi, There is a lot of other stuff going on in the sample, hopefully it should be reasonably simple without the extra stuff.

    Have a look at this sample that just does the dimensions.


    https://sites.google.com/site/livecycledesignercookbooks/ImageViewer.Dimension.pdf?attredirects=0&d=1

    ReplyDelete
    Replies
    1. Thank you so much... very useful indeed. Just wondering about the download links. They seem to be broken. Appreciate it if you could get them back to download sample pdfs. Thanks.

      Delete
  9. Hi, Seems google has upgraded the Sites platform I had the links pointing to and moved everything to google drive! Anyway I have updated the links for this post, and will work though the others.

    ReplyDelete