001: /**
002: * Copyright (c) 2003-2006, www.pdfbox.org
003: * All rights reserved.
004: *
005: * Redistribution and use in source and binary forms, with or without
006: * modification, are permitted provided that the following conditions are met:
007: *
008: * 1. Redistributions of source code must retain the above copyright notice,
009: * this list of conditions and the following disclaimer.
010: * 2. Redistributions in binary form must reproduce the above copyright notice,
011: * this list of conditions and the following disclaimer in the documentation
012: * and/or other materials provided with the distribution.
013: * 3. Neither the name of pdfbox; nor the names of its
014: * contributors may be used to endorse or promote products derived from this
015: * software without specific prior written permission.
016: *
017: * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
018: * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
019: * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
020: * DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY
021: * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
022: * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
023: * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
024: * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
025: * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
026: * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
027: *
028: * http://www.pdfbox.org
029: *
030: */package org.pdfbox.pdmodel.interactive.form;
031:
032: import java.io.ByteArrayInputStream;
033: import java.io.ByteArrayOutputStream;
034: import java.io.IOException;
035: import java.io.OutputStream;
036: import java.io.PrintWriter;
037:
038: import java.util.ArrayList;
039: import java.util.Iterator;
040: import java.util.List;
041: import java.util.Map;
042:
043: import org.pdfbox.cos.COSArray;
044: import org.pdfbox.cos.COSDictionary;
045: import org.pdfbox.cos.COSFloat;
046: import org.pdfbox.cos.COSName;
047: import org.pdfbox.cos.COSNumber;
048: import org.pdfbox.cos.COSStream;
049: import org.pdfbox.cos.COSString;
050:
051: import org.pdfbox.pdfparser.PDFStreamParser;
052: import org.pdfbox.pdfwriter.ContentStreamWriter;
053:
054: import org.pdfbox.pdmodel.PDResources;
055:
056: import org.pdfbox.pdmodel.common.PDRectangle;
057:
058: import org.pdfbox.pdmodel.font.PDFont;
059: import org.pdfbox.pdmodel.font.PDFontDescriptor;
060: import org.pdfbox.pdmodel.font.PDSimpleFont;
061:
062: import org.pdfbox.pdmodel.interactive.action.PDAdditionalActions;
063: import org.pdfbox.pdmodel.interactive.annotation.PDAppearanceDictionary;
064: import org.pdfbox.pdmodel.interactive.annotation.PDAppearanceStream;
065: import org.pdfbox.pdmodel.interactive.annotation.PDAnnotationWidget;
066:
067: import org.pdfbox.util.PDFOperator;
068:
069: /**
070: * This one took me a while, but i'm proud to say that it handles
071: * the appearance of a textbox. This allows you to apply a value to
072: * a field in the document and handle the appearance so that the
073: * value is actually visible too.
074: * The problem was described by Ben Litchfield, the author of the
075: * example: org.pdfbox.examlpes.fdf.ImportFDF. So Ben, here is the
076: * solution.
077: *
078: * @author sug
079: * @author <a href="mailto:ben@benlitchfield.com">Ben Litchfield</a>
080: * @version $Revision: 1.19 $
081: */
082: public class PDAppearance {
083: private PDVariableText parent;
084:
085: private String value;
086: private COSString defaultAppearance;
087:
088: private PDAcroForm acroForm;
089: private List widgets = new ArrayList();
090:
091: /**
092: * Constructs a COSAppearnce from the given field.
093: *
094: * @param theAcroForm the acro form that this field is part of.
095: * @param field the field which you wish to control the appearance of
096: * @throws IOException If there is an error creating the appearance.
097: */
098: public PDAppearance(PDAcroForm theAcroForm, PDVariableText field)
099: throws IOException {
100: acroForm = theAcroForm;
101: parent = field;
102:
103: widgets = field.getKids();
104: if (widgets == null) {
105: widgets = new ArrayList();
106: widgets.add(field.getWidget());
107: }
108:
109: defaultAppearance = getDefaultAppearance();
110:
111: }
112:
113: /**
114: * Returns the default apperance of a textbox. If the textbox
115: * does not have one, then it will be taken from the AcroForm.
116: * @return The DA element
117: */
118: private COSString getDefaultAppearance() {
119:
120: COSString dap = parent.getDefaultAppearance();
121: if (dap == null) {
122: COSArray kids = (COSArray) parent.getDictionary()
123: .getDictionaryObject("Kids");
124: if (kids != null && kids.size() > 0) {
125: COSDictionary firstKid = (COSDictionary) kids
126: .getObject(0);
127: dap = (COSString) firstKid.getDictionaryObject("DA");
128: }
129: if (dap == null) {
130: dap = (COSString) acroForm.getDictionary()
131: .getDictionaryObject(COSName.getPDFName("DA"));
132: }
133: }
134: return dap;
135: }
136:
137: private int getQ() {
138: int q = parent.getQ();
139: if (parent.getDictionary().getDictionaryObject("Q") == null) {
140: COSArray kids = (COSArray) parent.getDictionary()
141: .getDictionaryObject("Kids");
142: if (kids != null && kids.size() > 0) {
143: COSDictionary firstKid = (COSDictionary) kids
144: .getObject(0);
145: COSNumber qNum = (COSNumber) firstKid
146: .getDictionaryObject("Q");
147: if (qNum != null) {
148: q = qNum.intValue();
149: }
150: }
151: }
152: return q;
153: }
154:
155: /**
156: * Extracts the original appearance stream into a list of tokens.
157: *
158: * @return The tokens in the original appearance stream
159: */
160: private List getStreamTokens(PDAppearanceStream appearanceStream)
161: throws IOException {
162: List tokens = null;
163: if (appearanceStream != null) {
164: tokens = getStreamTokens(appearanceStream.getStream());
165: }
166: return tokens;
167: }
168:
169: private List getStreamTokens(COSString string) throws IOException {
170: PDFStreamParser parser;
171:
172: List tokens = null;
173: if (string != null) {
174: ByteArrayInputStream stream = new ByteArrayInputStream(
175: string.getBytes());
176: parser = new PDFStreamParser(stream, acroForm.getDocument()
177: .getDocument().getScratchFile());
178: parser.parse();
179: tokens = parser.getTokens();
180: }
181: return tokens;
182: }
183:
184: private List getStreamTokens(COSStream stream) throws IOException {
185: PDFStreamParser parser;
186:
187: List tokens = null;
188: if (stream != null) {
189: parser = new PDFStreamParser(stream);
190: parser.parse();
191: tokens = parser.getTokens();
192: }
193: return tokens;
194: }
195:
196: /**
197: * Tests if the apperance stream already contains content.
198: *
199: * @return true if it contains any content
200: */
201: private boolean containsMarkedContent(List stream) {
202: return stream.contains(PDFOperator.getOperator("BMC"));
203: }
204:
205: /**
206: * This is the public method for setting the appearance stream.
207: *
208: * @param apValue the String value which the apperance shoud represent
209: *
210: * @throws IOException If there is an error creating the stream.
211: */
212: public void setAppearanceValue(String apValue) throws IOException {
213: // MulitLine check and set
214: if (parent.isMultiline() && apValue.indexOf('\n') != -1) {
215: apValue = convertToMultiLine(apValue);
216: }
217:
218: value = apValue;
219: Iterator widgetIter = widgets.iterator();
220: while (widgetIter.hasNext()) {
221: Object next = widgetIter.next();
222: PDAnnotationWidget widget = null;
223: if (next instanceof PDField) {
224: widget = ((PDField) next).getWidget();
225: } else {
226: widget = (PDAnnotationWidget) next;
227: }
228: PDAdditionalActions actions = widget.getActions();
229: if (actions != null
230: && actions.getF() != null
231: && widget.getDictionary().getDictionaryObject("AP") == null) {
232: //do nothing because the field will be formatted by acrobat
233: //when it is opened. See FreedomExpressions.pdf for an example of this.
234: } else {
235:
236: PDAppearanceDictionary appearance = widget
237: .getAppearance();
238: if (appearance == null) {
239: appearance = new PDAppearanceDictionary();
240: widget.setAppearance(appearance);
241: }
242:
243: Map normalAppearance = appearance.getNormalAppearance();
244: PDAppearanceStream appearanceStream = (PDAppearanceStream) normalAppearance
245: .get("default");
246: if (appearanceStream == null) {
247: COSStream cosStream = new COSStream(acroForm
248: .getDocument().getDocument()
249: .getScratchFile());
250: appearanceStream = new PDAppearanceStream(cosStream);
251: appearanceStream.setBoundingBox(widget
252: .getRectangle()
253: .createRetranslatedRectangle());
254: appearance.setNormalAppearance(appearanceStream);
255: }
256:
257: List tokens = getStreamTokens(appearanceStream);
258: List daTokens = getStreamTokens(getDefaultAppearance());
259: PDFont pdFont = getFontAndUpdateResources(tokens,
260: appearanceStream);
261:
262: if (!containsMarkedContent(tokens)) {
263: ByteArrayOutputStream output = new ByteArrayOutputStream();
264:
265: //BJL 9/25/2004 Must prepend existing stream
266: //because it might have operators to draw things like
267: //rectangles and such
268: ContentStreamWriter writer = new ContentStreamWriter(
269: output);
270: writer.writeTokens(tokens);
271:
272: output.write(" /Tx BMC\n".getBytes());
273: insertGeneratedAppearance(widget, output, pdFont,
274: tokens, appearanceStream);
275: output.write(" EMC".getBytes());
276: writeToStream(output.toByteArray(),
277: appearanceStream);
278: } else {
279: if (tokens != null) {
280: if (daTokens != null) {
281: int bmcIndex = tokens.indexOf(PDFOperator
282: .getOperator("BMC"));
283: int emcIndex = tokens.indexOf(PDFOperator
284: .getOperator("EMC"));
285: if (bmcIndex != -1 && emcIndex != -1
286: && emcIndex == bmcIndex + 1) {
287: //if the EMC immediately follows the BMC index then should
288: //insert the daTokens inbetween the two markers.
289: tokens.addAll(emcIndex, daTokens);
290: }
291: }
292: ByteArrayOutputStream output = new ByteArrayOutputStream();
293: ContentStreamWriter writer = new ContentStreamWriter(
294: output);
295: float fontSize = calculateFontSize(pdFont,
296: appearanceStream.getBoundingBox(),
297: tokens, null);
298: boolean foundString = false;
299: for (int i = 0; i < tokens.size(); i++) {
300: if (tokens.get(i) instanceof COSString) {
301: foundString = true;
302: COSString drawnString = ((COSString) tokens
303: .get(i));
304: drawnString.reset();
305: drawnString.append(apValue.getBytes());
306: }
307: }
308: int setFontIndex = tokens.indexOf(PDFOperator
309: .getOperator("Tf"));
310: tokens.set(setFontIndex - 1, new COSFloat(
311: fontSize));
312: if (foundString) {
313: writer.writeTokens(tokens);
314: } else {
315: int bmcIndex = tokens.indexOf(PDFOperator
316: .getOperator("BMC"));
317: int emcIndex = tokens.indexOf(PDFOperator
318: .getOperator("EMC"));
319:
320: if (bmcIndex != -1) {
321: writer.writeTokens(tokens, 0,
322: bmcIndex + 1);
323: } else {
324: writer.writeTokens(tokens);
325: }
326: output.write("\n".getBytes());
327: insertGeneratedAppearance(widget, output,
328: pdFont, tokens, appearanceStream);
329: if (emcIndex != -1) {
330: writer.writeTokens(tokens, emcIndex,
331: tokens.size());
332: }
333: }
334: writeToStream(output.toByteArray(),
335: appearanceStream);
336: } else {
337: //hmm?
338: }
339: }
340: }
341: }
342: }
343:
344: private void insertGeneratedAppearance(
345: PDAnnotationWidget fieldWidget, OutputStream output,
346: PDFont pdFont, List tokens,
347: PDAppearanceStream appearanceStream) throws IOException {
348: PrintWriter printWriter = new PrintWriter(output, true);
349: float fontSize = 0.0f;
350: PDRectangle boundingBox = null;
351: boundingBox = appearanceStream.getBoundingBox();
352: if (boundingBox == null) {
353: boundingBox = fieldWidget.getRectangle()
354: .createRetranslatedRectangle();
355: }
356: printWriter.println("BT");
357: if (defaultAppearance != null) {
358: String daString = defaultAppearance.getString();
359: PDFStreamParser daParser = new PDFStreamParser(
360: new ByteArrayInputStream(daString.getBytes()), null);
361: daParser.parse();
362: List daTokens = daParser.getTokens();
363: fontSize = calculateFontSize(pdFont, boundingBox, tokens,
364: daTokens);
365: int fontIndex = daTokens.indexOf(PDFOperator
366: .getOperator("Tf"));
367: if (fontIndex != -1) {
368: daTokens.set(fontIndex - 1, new COSFloat(fontSize));
369: }
370: ContentStreamWriter daWriter = new ContentStreamWriter(
371: output);
372: daWriter.writeTokens(daTokens);
373: }
374: printWriter.println(getTextPosition(boundingBox, pdFont,
375: fontSize, tokens));
376: int q = getQ();
377: if (q == PDTextbox.QUADDING_LEFT) {
378: //do nothing because left is default
379: } else if (q == PDTextbox.QUADDING_CENTERED
380: || q == PDTextbox.QUADDING_RIGHT) {
381: float fieldWidth = boundingBox.getWidth();
382: float stringWidth = (pdFont.getStringWidth(value) / 1000)
383: * fontSize;
384: float adjustAmount = fieldWidth - stringWidth - 4;
385:
386: if (q == PDTextbox.QUADDING_CENTERED) {
387: adjustAmount = adjustAmount / 2.0f;
388: }
389:
390: printWriter.println(adjustAmount + " 0 Td");
391: } else {
392: throw new IOException("Error: Unknown justification value:"
393: + q);
394: }
395: printWriter.println("(" + value + ") Tj");
396: printWriter.println("ET");
397: printWriter.flush();
398: }
399:
400: private PDFont getFontAndUpdateResources(List tokens,
401: PDAppearanceStream appearanceStream) throws IOException {
402:
403: PDFont retval = null;
404: PDResources streamResources = appearanceStream.getResources();
405: PDResources formResources = acroForm.getDefaultResources();
406: if (formResources != null) {
407: if (streamResources == null) {
408: streamResources = new PDResources();
409: appearanceStream.setResources(streamResources);
410: }
411:
412: COSString da = getDefaultAppearance();
413: if (da != null) {
414: String data = da.getString();
415: PDFStreamParser streamParser = new PDFStreamParser(
416: new ByteArrayInputStream(data.getBytes()), null);
417: streamParser.parse();
418: tokens = streamParser.getTokens();
419: }
420:
421: int setFontIndex = tokens.indexOf(PDFOperator
422: .getOperator("Tf"));
423: COSName cosFontName = (COSName) tokens
424: .get(setFontIndex - 2);
425: String fontName = cosFontName.getName();
426: retval = (PDFont) streamResources.getFonts().get(fontName);
427: if (retval == null) {
428: retval = (PDFont) formResources.getFonts()
429: .get(fontName);
430: streamResources.getFonts().put(fontName, retval);
431: }
432: }
433: return retval;
434: }
435:
436: private String convertToMultiLine(String line) {
437: int currIdx = 0;
438: int lastIdx = 0;
439: StringBuffer result = new StringBuffer(line.length() + 64);
440: while ((currIdx = line.indexOf('\n', lastIdx)) > -1) {
441: result.append(line.substring(lastIdx, currIdx));
442: result.append(" ) Tj\n0 -13 Td\n(");
443: lastIdx = currIdx + 1;
444: }
445: result.append(line.substring(lastIdx));
446: return result.toString();
447: }
448:
449: /**
450: * Writes the stream to the actual stream in the COSStream.
451: *
452: * @throws IOException If there is an error writing to the stream
453: */
454: private void writeToStream(byte[] data,
455: PDAppearanceStream appearanceStream) throws IOException {
456: OutputStream out = appearanceStream.getStream()
457: .createUnfilteredStream();
458: out.write(data);
459: out.flush();
460: }
461:
462: /**
463: * w in an appearance stream represents the lineWidth.
464: * @return the linewidth
465: */
466: private float getLineWidth(List tokens) {
467:
468: float retval = 1;
469: if (tokens != null) {
470: int btIndex = tokens.indexOf(PDFOperator.getOperator("BT"));
471: int wIndex = tokens.indexOf(PDFOperator.getOperator("w"));
472: //the w should only be used if it is before the first BT.
473: if ((wIndex > 0) && (wIndex < btIndex)) {
474: retval = ((COSNumber) tokens.get(wIndex - 1))
475: .floatValue();
476: }
477: }
478: return retval;
479: }
480:
481: private PDRectangle getSmallestDrawnRectangle(
482: PDRectangle boundingBox, List tokens) {
483: PDRectangle smallest = boundingBox;
484: for (int i = 0; i < tokens.size(); i++) {
485: Object next = tokens.get(i);
486: if (next == PDFOperator.getOperator("re")) {
487: COSNumber x = (COSNumber) tokens.get(i - 4);
488: COSNumber y = (COSNumber) tokens.get(i - 3);
489: COSNumber width = (COSNumber) tokens.get(i - 2);
490: COSNumber height = (COSNumber) tokens.get(i - 1);
491: PDRectangle potentialSmallest = new PDRectangle();
492: potentialSmallest.setLowerLeftX(x.floatValue());
493: potentialSmallest.setLowerLeftY(y.floatValue());
494: potentialSmallest.setUpperRightX(x.floatValue()
495: + width.floatValue());
496: potentialSmallest.setUpperRightY(y.floatValue()
497: + height.floatValue());
498: if (smallest == null
499: || smallest.getLowerLeftX() < potentialSmallest
500: .getLowerLeftX()
501: || smallest.getUpperRightY() > potentialSmallest
502: .getUpperRightY()) {
503: smallest = potentialSmallest;
504: }
505:
506: }
507: }
508: return smallest;
509: }
510:
511: /**
512: * My "not so great" method for calculating the fontsize.
513: * It does not work superb, but it handles ok.
514: * @return the calculated font-size
515: *
516: * @throws IOException If there is an error getting the font height.
517: */
518: private float calculateFontSize(PDFont pdFont,
519: PDRectangle boundingBox, List tokens, List daTokens)
520: throws IOException {
521: float fontSize = 0;
522: if (daTokens != null) {
523: //daString looks like "BMC /Helv 3.4 Tf EMC"
524:
525: int fontIndex = daTokens.indexOf(PDFOperator
526: .getOperator("Tf"));
527: if (fontIndex != -1) {
528: fontSize = ((COSNumber) daTokens.get(fontIndex - 1))
529: .floatValue();
530: }
531: }
532: if (parent.doNotScroll()) {
533: //if we don't scroll then we will shrink the font to fit into the text area.
534: float widthAtFontSize1 = pdFont.getStringWidth(value);
535: float availableWidth = boundingBox.getWidth();
536: float perfectFitFontSize = availableWidth
537: / widthAtFontSize1;
538: } else if (fontSize == 0) {
539: float lineWidth = getLineWidth(tokens);
540: float stringWidth = pdFont.getStringWidth(value);
541: float height = 0;
542: if (pdFont instanceof PDSimpleFont) {
543: height = ((PDSimpleFont) pdFont).getFontDescriptor()
544: .getFontBoundingBox().getHeight();
545: } else {
546: //now much we can do, so lets assume font is square and use width
547: //as the height
548: height = pdFont.getAverageFontWidth();
549: }
550: height = height / 1000f;
551:
552: float availHeight = getAvailableHeight(boundingBox,
553: lineWidth);
554: fontSize = (availHeight / height);
555: }
556: return fontSize;
557: }
558:
559: /**
560: * Calculates where to start putting the text in the box.
561: * The positioning is not quite as accurate as when Acrobat
562: * places the elements, but it works though.
563: *
564: * @return the sting for representing the start position of the text
565: *
566: * @throws IOException If there is an error calculating the text position.
567: */
568: private String getTextPosition(PDRectangle boundingBox,
569: PDFont pdFont, float fontSize, List tokens)
570: throws IOException {
571: float lineWidth = getLineWidth(tokens);
572: float pos = 0.0f;
573: if (parent.isMultiline()) {
574: int rows = (int) (getAvailableHeight(boundingBox, lineWidth) / ((int) fontSize));
575: pos = ((rows) * fontSize) - fontSize;
576: } else {
577: if (pdFont instanceof PDSimpleFont) {
578: //BJL 9/25/2004
579: //This algorithm is a little bit of black magic. It does
580: //not appear to be documented anywhere. Through examining a few
581: //PDF documents and the value that Acrobat places in there I
582: //have determined that the below method of computing the position
583: //is correct for certain documents, but maybe not all. It does
584: //work f1040ez.pdf and Form_1.pdf
585: PDFontDescriptor fd = ((PDSimpleFont) pdFont)
586: .getFontDescriptor();
587: float bBoxHeight = boundingBox.getHeight();
588: float fontHeight = fd.getFontBoundingBox().getHeight()
589: + 2 * fd.getDescent();
590: fontHeight = (fontHeight / 1000) * fontSize;
591: pos = (bBoxHeight - fontHeight) / 2;
592: } else {
593: throw new IOException(
594: "Error: Don't know how to calculate the position for non-simple fonts");
595: }
596: }
597: PDRectangle innerBox = getSmallestDrawnRectangle(boundingBox,
598: tokens);
599: float xInset = 2 + 2 * (boundingBox.getWidth() - innerBox
600: .getWidth());
601: return Math.round(xInset) + " " + pos + " Td";
602: }
603:
604: /**
605: * calculates the available width of the box.
606: * @return the calculated available width of the box
607: */
608: private float getAvailableWidth(PDRectangle boundingBox,
609: float lineWidth) {
610: return boundingBox.getWidth() - 2 * lineWidth;
611: }
612:
613: /**
614: * calculates the available height of the box.
615: * @return the calculated available height of the box
616: */
617: private float getAvailableHeight(PDRectangle boundingBox,
618: float lineWidth) {
619: return boundingBox.getHeight() - 2 * lineWidth;
620: }
621: }
|