Copy link to clipboard
Copied
Acrobat DC, version 2024.005.20320
Alright, I'm losing my mind here and I'm pretty sure I'm running into a bug with how Custom Dialogs render Images. However, I am also questioning my overall direction with this project. I would really appreciate any ideas or help!
Overall problem: I have a PDF form that needs up to 13 digital signatures and then a dated stamp with/after the last signature.
Attempted solution: Since the stamp is different for every location, but rarely changes, I've rebuilt the PDF to allow embedding the stamp as a button icon before distribution, then hiding the icon until the Certifying Official digitally signs the form. I've built a Custom Dialog to walk users through importing the icon and loading in the required information. This is supposed to include a preview of the imported stamp.
Current problem: The 'preview' image of the imported stamp does not render correctly and is a different size from the original. The imported stamp's size seems to be based on whether the original file has a transparent background or not. Transparent background is imported as 254x254; while a white background is imported as 81x81. On rare occassions, the 81x81 will render properly, but it's not consistent. In all cases, the button on the PDF renders the image correctly.
Additionally, I've found that the "util.iconStreamFromIcon(...).read()" will only ever return the first 8,192 hexidecimals of the image, regardless of size. Which is irritating, as I'm also trying to determine the stamp's color after the import process.
Things I've tried:
Original Transparent Stamp: 339x339
Rendering errors:
Code:
function customizeOMMdata()
{
//region Data loading
var refDoc = this, intH, intW;
var intImportResult;
var DialogResult, IDnum;
var arrBad = [], intTryAgain, arrOMM, arrOMC, strOMCname, strOMCmsg, intConfirmOMM, chkLockToField;
var strDialogInstructions =
"Filling in the fields below will customize this form to your Office. \n" +
"\n" +
"Embedding your All-Purpose Date Stamp will enable fully digital processing. \n" +
"When the Certifying Official signs, the APDS will appear to certify the form. \n" +
"\n" +
"By checking the box on the right side and completing the information below it, \n" +
"the ADPS can be restricted to display ONLY when the Manager signs."
//Get current APDS, digest the information into an easier to use object.
var intMinStampSize = 32, intMaxStampSize = 500;
//var icoAPDS = getField("OMC.Stamp").buttonGetIcon(0);
var strmAPDS = util.iconStreamFromIcon(getField("OMC.Stamp").buttonGetIcon(0));
var objAPDS = {
//Name: icoAPDS.name,
Bytes: strmAPDS.read(),
Height: intH ? intH : Math.min(Math.max(strmAPDS.height, intMinStampSize), intMaxStampSize), //Math.max selects larger of height vs Min size; Math.min selects smaller of height vs Max size.
Width: intW ? intW : Math.min(Math.max(strmAPDS.width, intMinStampSize), intMaxStampSize)
}
var dialogAPDS = {
type: "image",
item_id: "APDS",
width: objAPDS.Width,
height: objAPDS.Height
};
//Get OMM info into an object for easy reference, fewer? ops
var objOMManager = {
NameFirst: getField("OMC.Auth.Name.First").valueAsString,
NameLast: getField("OMC.Auth.Name.Last").valueAsString,
IDnum: getField("OMC.Auth.IDnum").valueAsString, //valueAsString to remove the number spinner.
Email: getField("OMC.Auth.Email").valueAsString
};
//Same as OMM object, but for the OMC.
var objOMCenter = {
Unit: getField("OMC.Info.Unit").valueAsString,
OfficeSymbol: getField("OMC.Info.OfficeSymbol").valueAsString,
State: getField("OMC.Info.State").valueAsString,
Email: getField("OMC.Info.Email").valueAsString
};
if(objOMManager.IDnum || objOMManager.Email){blnOMMrequired = true}
else{blnOMMrequired = false};
//In order to push data into the Dialog, an Object with Keys exactly 4 characters long must be provided.
var DialogData = {
"APDS": { width: objAPDS.Width,
height: objAPDS.Height,
offset: 0,
read: bytes => objAPDS.Bytes.substring(this.offset, this.offset += bytes)}, //this function makes no sense, but Acrobat hangs if you don't use it. Substring or Slice both work.
"Fnme": objOMManager.NameFirst,
"Lnme": objOMManager.NameLast,
"nmID": objOMManager.IDnum,
"Mail": objOMManager.Email,
"Unit": objOMCenter.Unit,
"OSym": objOMCenter.OfficeSymbol,
"Stat": objOMCenter.State,
"OBox": objOMCenter.Email,
"OMrq": blnOMMrequired
};
//region Dialog Description
//Literal JS Object with each property describing the look and flow of the dialog. Order matters.
var objDialogDescription =
{name: "Set authorized Manager", first_tab: "Unit", elements:[
{type: "view", elements:[
{type: "static_text", alignment: "align_center", mutliline: true, char_height: 12, name: strDialogInstructions},
{type: "view", align_children: "align_row", elements:[
{type: "cluster", align_children: "align_center", name: "Embedded All-Purpose Date Stamp", char_width: 21, char_height: 22, elements:[
dialogAPDS,
{type: "button", item_id: "embd", name: "Change APDS"}
]},
{type: "cluster", name: "Manager's info:", char_width: 20, char_height: 22, elements:[
{type: "check_box", item_id: "OMrq", next_tab: "Fnme", name: "Restrict APDS to Manager's signature?"},
{type: "view", align_children: "align_row", elements:[
{type: "static_text", name: "First name:"},
{type: "gap", width: 15},
{type: "edit_text", item_id: "Fnme", next_tab: "Lnme", char_width: 20}
]},
{type: "view", align_children: "align_row", elements:[
{type: "static_text", name: "Last name:"},
{type: "gap", width: 16},
{type: "edit_text", item_id: "Lnme", next_tab: "nmID", char_width: 20}
]},
{type: "view", align_children: "align_row", elements:[
{type: "static_text", name: "ID number:"},
//{type: "gap", width: 0},
{type: "edit_text", item_id: "nmID", next_tab: "Mail", char_width: 7}
]},
{type: "view", align_children: "align_row", elements:[
{type: "static_text", name: "Email address:"},
{type: "gap", width: -2},
{type: "edit_text", item_id: "Mail", next_tab: "Unit", char_width: 20}
]}
]},
]},
{type: "cluster", align_children: "align_center", name: "Center info:", elements:[
{type: "view", align_children: "align_row", elements:[
//{type: "gap", width: -4},
{type: "static_text", name: "Unit Abbreviation:"},
//{type: "gap", char_width: 1},
{type: "edit_text", item_id: "Unit", next_tab: "OSym", char_width: 6},
//{type: "gap", width: 34},
{type: "static_text", name: "Office Symbol:"},
//{type: "gap", width: 6},
{type: "edit_text", item_id: "OSym", next_tab: "Stat", char_width: 6},
]},
{type: "view", align_children: "align_row", elements:[
{type: "gap", width: 6},
{type: "static_text", name: "State:"},
{type: "gap", width: 24},
{type: "edit_text", item_id: "Stat", next_tab: "OBox", char_width: 35},
]},
{type: "view", align_children: "align_row", elements:[
{type: "gap", width: 6},
{type: "static_text", name: "Center Email:"},
//{type: "gap", width: -3},
{type: "edit_text", item_id: "OBox", next_tab: "Fnme", char_width: 35},
]},
]}
]},
{type: "ok_cancel_other", ok_name: "Save", other_name: "Remove"}
]};
//region Dialog Execution
//OMMdata variable holds the result and data from the dialog.
var OMMdata =
{
//Set the default result string
result: "cancel",
//This adds a method to the OMMdata Object that executes the dialog.
RunDialog: function(){return app.execDialog(this);},
//This method runs first and takes an object and passes it into the Dialog
initialize: function(dialog)
{
dialog.load(DialogData);
dialog.enable({
"Fnme": blnOMMrequired,
"Lnme": blnOMMrequired,
"nmID": blnOMMrequired,
"Mail": blnOMMrequired
});
},
//This method takes data from the dialog and adds it back to the Object as properties, when result == `ok`.
commit: function(dialog)
{
var oResults = dialog.store();
this.FirstName = oResults["Fnme"];
this.LastName = oResults["Lnme"];
this.IDnum = oResults["nmID"];
this.Email = oResults["Mail"];
this.Unit = oResults["Unit"];
this.OfficeSymbol = oResults["OSym"];
this.State = oResults["Stat"];
this.OMCemail = oResults["OBox"];
},
//Change APDS embed function
"embd": function(dialog)
{
intImportResult = refDoc.importIcon({cName: 'APDS'}) //'This' refers to the Dialog at this point. A reference is captured beforehand to work around it.
switch(intImportResult)
{
case 0: //Success: Set the new stamp and scale it to fit
//Reload image into dialog
dialog.end("rtry")
getField("OMC.Stamp").buttonSetIcon(refDoc.getIcon('APDS'));
getField("OMC.Stamp").buttonScaleWhen = scaleWhen.always;
getField("OMC.Stamp").buttonPosition = position.overlay;
getField("OMC.Stamp").buttonSetCaption({cCaption: "DATE"});
getField("OMC.Stamp").textColor = color.black;
break;
case 1: //User canceled
//if(app.alert({cTitle: "APDS Embed Cancelled", cMsg: "ADPS embedding has been canceled.\n\nWould you like to REMOVE the current ADPS?", nIcon: 2, nType: 2}) === 4){actionHideAPDS();}
break;
case -1: //File cannot be opened
app.alert("The file selected cannot be opened. Try again with a different file.");
//actionHideAPDS();
break;
case -2: //Selected page is invalid. Default is page 0, which should always be present.
default:
//actionHideAPDS()
}
},
//Validates "nmID" field on lost focus. Removes all non-digit characters, then cuts string to 10 characters.
"nmID": function(dialog)
{
dialog.load({"nmID": dialog.store()["nmID"].replace(/\D/g,'').substring(0,10)});
},
"OMrq": function(dialog)
{
blnOMMrequired = dialog.store()["OMrq"];
dialog.enable({
"Fnme": blnOMMrequired,
"Lnme": blnOMMrequired,
"nmID": blnOMMrequired,
"Mail": blnOMMrequired
});
},
//Other button, "remove", function
other: function(dialog)
{
dialog.end("kill") //Closes dialog and returns "kill" >> Limit is 4 characters
},
//This area defines what the dialog looks like, where it gets its data, and what all the buttons call to.
description: objDialogDescription
};
//Outside of the object, the Dialog is called and executed.
DialogResult = OMMdata.RunDialog()
//region Result Handler
//A switch conditional is used to handle the output.
switch(DialogResult)
{
case "ok":
//Handler removed as it's not relevant to the issue.
break;
case "kill": //Remove all customizations
this.resetForm(["OMC.Auth", "OMC.Info", "OMC.To"])
getField("OMC.To").readonly = false;
appearanceCustomHighlight(DocCustomHighlight);
actionHideAPDS();
break;
case "rtry":
customizeOMMdata();
break;
case "cancel":
default:
return false;
}
}
Copy link to clipboard
Copied
I've written many dialog scripts that display all kinds of images. The only time I see problems like this is when the specified width and height are different from the actual image data width and height.
So, to that end, there are two issues in the script.
1) the script contains code that selects a width and hieght that is different from the actual.
2) There is no reason to recreate the icon stream. The "iconStreamFromIcon" function returns an icon stream, the "stmAPDS" variable. Just use it.
Copy link to clipboard
Copied
Thom,
Thank you for the response!
1) The purpose of
intH ? intH : Math.min(Math.max(strmAPDS.height, intMinStampSize), intMaxStampSize)
was to constrain the stamp to a minimum and maximum size, ideally to prevent user foolishness. Since it appears that the Image element of Custom Dialogs cannot scale images, I'll have to switch to a warning or error message. Additionally, I added the intH conditional for testing, so I could rapidly iterate through different sizes to try and find one that worked. Unfortunately, no size renders correctly. Even changing this line to just: "strmAPDS.height" results in the same rendering errors.
2) Oh, oops! I've fixed that. I was cobbling it together from examples, since neither is defined in the JS Ref.
3) Is there anyway for Javascript to parse through the entire icon? I was hoping to use .read() to feed the entire icon into a script to determine the overall color. Currently, I only get the first 8192 hexidecimals.
Copy link to clipboard
Copied
So, a raster image is composed of rows and columns of pixels. In the Acrobat JavaScript model, a Pixel has 4 components that are 1 byte each ARGB, where A is the alpha channel. The width and height parameters in the icon stream object represent then number of columns and rows of pixels. These parameters are necessary for properly reading the image data and are unrelated to the width and height of the dialog image element. To generalize, all image formats (JPG, BMP, PNG, et.) specifiy width and height in pixels, cause without it the image data makes no sense. And this is exactly the reason the images you posted above are repeated and skewed, i.e., your code provided bad dimension parameters for reading the data. Scaling has nothing to do with it.
And Yes, the raw image data can be read and anaylized, and modified using the read() function. I've done this many times. There are however limitations on size. Acrobat JavaScript is a lightweight programming environment. It's both slow and memory limited. You are not going to be able to use it to write a tool for manipulating large photos.
Copy link to clipboard
Copied
Ah, I think I don't think I conveyed my meaning completely. Let me try again:
1a. I've tested the image with dimensions from 32x32 through 500x500. It has never rendered correctly and consistently. In order to do this quickly, I ran a for loop and incremented intH & intW until I aborted it. If I don't define intH or intW, the script defaults to extracting it from the icon.
1b. I wanted to use the Height & Width of the icon provided by util.iconStreamFromIcon(...).height & width. That should allow the image to render correctly without requiring a hard-coded size. One very rare occasions, this renders correctly, but always fails after a few repeats.
1c. The original images are 339x339 pixels, while util.iconStreamFromIcon(...).height & width returns 254x254 for the transparent background images, but only 81x81 for the white background images. None of these height & width numbers render correctly. Additionally, testing on a different computer has given me completely different height/width values for the same images. 508x508 for transparent and 164x164 for white.
1d. I had hoped that the image element had a scaling function or that I could scale the image using Javascript to manipulate the pixels directly before feeding them into the image element. Therefore I set minimum and maximum sizes for the image element using math.max() and math.min() respsectively.
1f. Even with an integer set as the image's width & height, the image renders incorrectly in different ways each time it is rendered. The 7 bad examples I've provided are the most common, but sometimes it's just green static. This is where I am most confused. If the image just has misaligned rows , I'd expect the "bad" render to be the same each time. I would also expect the colors to be correct. My best guess at this point is that Acrobat is taking my ARGB PNG and reading it as a RGB PNG, which is causing the skew and color shift. This appears to be wrong, as I converted the image to RGB with GIMP and still had render errors.
I hope that more clearly explains what I've tried.
To get to the point: How do I render a dynamic icon correctly inside an image element in a custom dialog?
Side questions:
Copy link to clipboard
Copied
Main point. Refer to #2 on my first answer. Don't recreate the icon stream, just use the one Acrobat gives you.
replace this code:
var DialogData = {
"APDS": util.iconStreamFromIcon(getField("OMC.Stamp").buttonGetIcon(0),
"Fnme": objOMManager.NameFirst,
"Lnme": objOMManager.NameLast,
"nmID": objOMManager.IDnum,
"Mail": objOMManager.Email,
"Unit": objOMCenter.Unit,
"OSym": objOMCenter.OfficeSymbol,
"Stat": objOMCenter.State,
"OBox": objOMCenter.Email,
"OMrq": blnOMMrequired
};
Side Questions
1. I don't know. Never tested the limit. I've loaded 2kx2k images, which is way larger than any practicle use I've had for an image.
2. No, button images scale, the dialog image object renders pixel for pixel.
Copy link to clipboard
Copied
About the varied results of width and height when converting icons on different systems. The buttonImportIcon function does not import images 1 to 1. It resizes them according to some methodology I do not understand. I can imagine that different versions might do it differently, but I think thats unlikely. You reported rather wild differences. Something else is going on. If you really want to test this you need to simplify your setup and only test the one thing.
I've also never had an issue with setting the size of image data, when I got that size from the icon stream. Which also makes me think something else is happening.
I don't use buttons to import images that are used by the dialog, unless that is the only choice. Mostly I'm placing static images on dialogs, such as a logo. For this I convert the original image with a program I wrote for this specific purpose.
But I've also made dialogs that create images dynamically.
The point is that there is nothing wrong with Acrobat and the way it handles images in JavaScript. The problem is something in your code.
Find more inspiration, events, and resources on the new Adobe Community
Explore Now