/* * Copyright 2006-2017 ICEsoft Technologies Canada Corp. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the * License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an "AS * IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either * express or implied. See the License for the specific language * governing permissions and limitations under the License. */ package org.icepdf.core.pobjects.annotations; import org.icepdf.core.pobjects.*; import org.icepdf.core.pobjects.acroform.ChoiceFieldDictionary; import org.icepdf.core.pobjects.acroform.FieldDictionary; import org.icepdf.core.pobjects.graphics.Shapes; import org.icepdf.core.pobjects.graphics.text.LineText; import org.icepdf.core.pobjects.graphics.text.WordText; import org.icepdf.core.util.Library; import java.awt.geom.AffineTransform; import java.awt.geom.Rectangle2D; import java.util.ArrayList; import java.util.HashMap; import java.util.StringTokenizer; import static org.icepdf.core.pobjects.acroform.ChoiceFieldDictionary.ChoiceFieldType; /** * Represents a Acroform Choice widget and manages the appearance streams * for the various appearance states. This class can generate a postscript * stream that represents it current state. * * @since 5.1 */ public class ChoiceWidgetAnnotation extends AbstractWidgetAnnotation<ChoiceFieldDictionary> { private ChoiceFieldDictionary fieldDictionary; public ChoiceWidgetAnnotation(Library l, HashMap h) { super(l, h); fieldDictionary = new ChoiceFieldDictionary(library, entries); } /** * Some choices lists are lacking the /opt key so we need to do our best to generate the list from the shapes. * * @return list of potential options. */ public ArrayList<ChoiceFieldDictionary.ChoiceOption> generateChoices() { Shapes shapes = getShapes(); if (shapes != null) { ArrayList<ChoiceFieldDictionary.ChoiceOption> options = new ArrayList<ChoiceFieldDictionary.ChoiceOption>(); String tmp; ArrayList<LineText> pageLines = shapes.getPageText().getPageLines(); for (LineText lines : pageLines) { for (WordText word : lines.getWords()) { tmp = word.toString(); if (!(tmp.equals("") || tmp.equals(" "))) { options.add(fieldDictionary.buildChoiceOption(tmp, tmp)); } } } return options; } return new ArrayList<ChoiceFieldDictionary.ChoiceOption>(); } /** * Resets the appearance stream for this instance using the current state. The mark content section of the stream * is found and the edit it make to best of our ability. * * @param dx x offset of the annotation * @param dy y offset of the annotation * @param pageTransform current page transform. */ public void resetAppearanceStream(double dx, double dy, AffineTransform pageTransform) { ChoiceFieldType choiceFieldType = fieldDictionary.getChoiceFieldType(); // get at the original postscript as well alter the marked content Appearance appearance = appearances.get(currentAppearance); AppearanceState appearanceState = appearance.getSelectedAppearanceState(); Rectangle2D bbox = appearanceState.getBbox(); AffineTransform matrix = appearanceState.getMatrix(); String currentContentStream = appearanceState.getOriginalContentStream(); // alterations vary by choice type. if (choiceFieldType == ChoiceFieldType.CHOICE_COMBO || choiceFieldType == ChoiceFieldType.CHOICE_EDITABLE_COMBO) { // relatively straight forward replace with new selected value. if (currentContentStream != null) { currentContentStream = buildChoiceComboContents(currentContentStream); } else { // todo no stream and we will need to build one. currentContentStream = ""; } } else { // build out the complex choice list content stream if (currentContentStream != null) { currentContentStream = buildChoiceListContents(currentContentStream); } else { // todo no stream and we will need to build one. currentContentStream = ""; } } // finally create the shapes from the altered stream. if (currentContentStream != null) { appearanceState.setContentStream(currentContentStream.getBytes()); } // some widgets don't have AP dictionaries in such a case we need to create the form object // and build out the default properties. Form appearanceStream = getOrGenerateAppearanceForm(); if (appearanceStream != null) { // update the content stream with the new stream data. appearanceStream.setRawBytes(currentContentStream.getBytes()); // add the appearance stream StateManager stateManager = library.getStateManager(); stateManager.addChange(new PObject(appearanceStream, appearanceStream.getPObjectReference())); // add an AP entry for the HashMap<Object, Object> appearanceRefs = new HashMap<Object, Object>(); appearanceRefs.put(APPEARANCE_STREAM_NORMAL_KEY, appearanceStream.getPObjectReference()); entries.put(APPEARANCE_STREAM_KEY, appearanceRefs); Rectangle2D formBbox = new Rectangle2D.Float(0, 0, (float) bbox.getWidth(), (float) bbox.getHeight()); appearanceStream.setAppearance(null, matrix, formBbox); // add link to resources on forum, if no resources exist. if (library.getResources(appearanceStream.getEntries(), Form.RESOURCES_KEY) == null) { appearanceStream.getEntries().put(Form.RESOURCES_KEY, library.getCatalog().getInteractiveForm().getResources().getEntries()); } // add the annotation as changed as T entry has also been updated to reflect teh changed content. stateManager.addChange(new PObject(this, this.getPObjectReference())); // compress the form object stream. if (false && compressAppearanceStream) { appearanceStream.getEntries().put(Stream.FILTER_KEY, new Name("FlateDecode")); } else { appearanceStream.getEntries().remove(Stream.FILTER_KEY); } appearanceStream.init(); } } public void reset() { Object oldValue = fieldDictionary.getFieldValue(); Object tmp = fieldDictionary.getDefaultFieldValue(); if (tmp == null) { FieldDictionary parentFieldDictionary = fieldDictionary.getParent(); if (parentFieldDictionary != null) { tmp = parentFieldDictionary.getDefaultFieldValue(); } } if (tmp != null) { // apply the default value fieldDictionary.setFieldValue(tmp, getPObjectReference()); changeSupport.firePropertyChange("valueFieldReset", oldValue, tmp); } else { // otherwise we remove the key fieldDictionary.getEntries().remove(FieldDictionary.V_KEY); fieldDictionary.setIndexes(null); // check the parent as well. FieldDictionary parentFieldDictionary = fieldDictionary.getParent(); if (parentFieldDictionary != null) { parentFieldDictionary.getEntries().remove(FieldDictionary.V_KEY); if (parentFieldDictionary instanceof ChoiceFieldDictionary) { ((ChoiceFieldDictionary) parentFieldDictionary).setIndexes(null); } } changeSupport.firePropertyChange("valueFieldReset", oldValue, null); } } @Override public ChoiceFieldDictionary getFieldDictionary() { return fieldDictionary; } public String buildChoiceComboContents(String currentContentStream) { ArrayList<ChoiceFieldDictionary.ChoiceOption> choices = fieldDictionary.getOptions(); // double check we have some choices to work with. if (choices == null) { // generate them from the content stream. choices = generateChoices(); fieldDictionary.setOptions(choices); } String selectedField = (String) fieldDictionary.getFieldValue(); int btStart = currentContentStream.indexOf("BT"); int btEnd = currentContentStream.lastIndexOf("ET"); int bmcStart = currentContentStream.indexOf("BMC"); int bmcEnd = currentContentStream.lastIndexOf("EMC"); // grab the pre post marked content postscript. String preBmc = btStart >= 0 ? currentContentStream.substring(0, btStart + 2) : currentContentStream.substring(0, bmcStart + 3); String postEmc = btEnd >= 0 ? currentContentStream.substring(btEnd) : currentContentStream.substring(0, bmcEnd + 3); // marked content which we will use to try and find some data points. //String markedContent = currentContentStream.substring(bmcStart, bmcEnd); // check for a bounding box definition //Rectangle2D.Float bounds = findBoundRectangle(markedContent); // finally build out the new content stream StringBuilder content = new StringBuilder(); // apply font if (fieldDictionary.getDefaultAppearance() != null) { String markedContent = fieldDictionary.getDefaultAppearance(); Page page = getPage(); markedContent = fieldDictionary.generateDefaultAppearance(markedContent, page != null ? page.getResources() : null); content.append(markedContent).append(' '); } else { // common font and colour layout for most form elements. content.append("/Helv 12 Tf 0 g "); } // apply the text offset, 4 is just a generic padding. content.append(4).append(' ').append(4).append(" Td "); // hex encode the text so that we better handle character codes > 127 content = encodeHexString(content, selectedField).append(" Tj "); // build the final content stream. if (btStart >= 0) { currentContentStream = preBmc + "\n" + content + "\n" + postEmc; } else { currentContentStream = preBmc + " BT\n" + content + "\n ET EMC"; } return currentContentStream; } public String buildChoiceListContents(String currentContentStream) { ArrayList<ChoiceFieldDictionary.ChoiceOption> choices = fieldDictionary.getOptions(); // double check we have some choices to work with. if (choices == null) { // generate them from the content stream. choices = generateChoices(); fieldDictionary.setOptions(choices); } ArrayList<Integer> selections = fieldDictionary.getIndexes(); // mark the indexes of the mark content. int bmcStart = currentContentStream.indexOf("BMC") + 3; int bmcEnd = currentContentStream.indexOf("EMC"); // grab the pre post marked content postscript. String preBmc = currentContentStream.substring(0, bmcStart); String postEmc = currentContentStream.substring(bmcEnd); // marked content which we will use to try and find some data points. String markedContent = currentContentStream.substring(bmcStart, bmcEnd); // check for a bounding box definition Rectangle2D.Float bounds = findBoundRectangle(markedContent); // check to see if there is a selection box colour defined. float[] selectionColor = findSelectionColour(markedContent); // and finally look for a previous selection box, this can be null, no default value Rectangle2D.Float selectionRectangle = findSelectionRectangle(markedContent); float lineHeight = 13.87f; if (selectionRectangle != null) { lineHeight = selectionRectangle.height; } // we need to plot out where the opt text is going to go as well as the background colour and text colour // for any selected items. So we update the choices model to reflect the current selection state. boolean isSelection = false; if (selections != null) { for (int i = 0, max = choices.size(); i < max; i++) { for (int selection : selections) { if (selection == i) { choices.get(i).setIsSelected(true); isSelection = true; } else { choices.get(i).setIsSelected(false); } } } } // figure out offset range to insure a single selection is always visible int startIndex = 0, endIndex = choices.size(); if (selections != null && selections.size() == 1) { int numberLines = (int) Math.floor(bounds.height / lineHeight); // check if list is smaller then number of lines int selectedIndex = selections.get(0); if (choices.size() < numberLines) { // nothing to do. } else if (selectedIndex < numberLines) { endIndex = numberLines + 1; } // check for bottom out range else if (endIndex - selectedIndex <= numberLines) { startIndex = endIndex - numberLines; } // else mid range just need to start the index. else { startIndex = selectedIndex; endIndex = numberLines + 1; } // we have a single line if (startIndex > endIndex) { endIndex = startIndex + 1; } } // finally build out the new content stream StringBuilder content = new StringBuilder(); // bounding rectangle. content.append("q ").append(generateRectangle(bounds)).append("W n "); // apply selection highlight background. if (isSelection) { // apply colour content.append(selectionColor[0]).append(' ').append(selectionColor[1]).append(' ') .append(selectionColor[2]).append(" rg "); // apply selection Rectangle2D.Float firstSelection; if (selectionRectangle == null) { firstSelection = new Rectangle2D.Float(bounds.x, bounds.y + bounds.height - lineHeight, bounds.width, lineHeight); } else { firstSelection = new Rectangle2D.Float(selectionRectangle.x, bounds.y + bounds.height - lineHeight, selectionRectangle.width, lineHeight); } ChoiceFieldDictionary.ChoiceOption choice; for (int i = startIndex; i < endIndex; i++) { choice = choices.get(i); // check if a selection rectangle was defined, if not we might have a custom style and we // avoid the selection background (only have one test case for this) if (choice.isSelected() && selectionRectangle != null) { content.append(generateRectangle(firstSelection)).append("f "); } firstSelection.y -= lineHeight; } } // apply the ext. content.append("BT "); // apply font if (fieldDictionary.getDefaultAppearance() != null) { content.append(fieldDictionary.getDefaultAppearance()); } else { // common font and colour layout for most form elements. content.append("/Helv 12 Tf 0 g "); } // apply the line height content.append(lineHeight).append(" TL "); // apply the text offset, 4 is just a generic padding. content.append(4).append(' ').append(bounds.height + 4).append(" Td "); // print out text ChoiceFieldDictionary.ChoiceOption choice; for (int i = startIndex; i < endIndex; i++) { choice = choices.get(i); if (choice.isSelected() && selectionRectangle != null) { content.append("1 g "); } else { content.append("0 g "); } content.append('(').append(choice.getLabel()).append(")' "); } content.append("ET Q"); // build the final content stream. currentContentStream = preBmc + "\n" + content + "\n" + postEmc; return currentContentStream; } /** * The selection colour is generally defined in DeviceRGB and occurs after the bounding box has been defined. * This utility method tries to parse out the colour information and return it in float[3]. If the data can't * be found then we return the default colour of new float[]{0.03922f, 0.14118f, 0.41569f}. * * @param markedContent content to look for colour info. * @return found colour data or new float[]{0.03922f, 0.14118f, 0.41569f}. */ private float[] findSelectionColour(String markedContent) { int selectionStart = markedContent.indexOf("n") + 1; int selectionEnd = markedContent.lastIndexOf("rg"); if (selectionStart < selectionEnd && selectionEnd > 0) { String potentialNumbers = markedContent.substring(selectionStart, selectionEnd); StringTokenizer toker = new StringTokenizer(potentialNumbers); float[] points = new float[3]; int i = 0; while (toker.hasMoreTokens()) { try { float tmp = Float.parseFloat(toker.nextToken()); points[i] = tmp; i++; } catch (NumberFormatException e) { break; } } if (i == 3) { return points; } } // default selection colour. return new float[]{0.03922f, 0.14118f, 0.41569f}; } }