Last active
August 9, 2022 19:46
-
-
Save JoelGeraci-Datalogics/fbe8e3f7eca9b6a5946d to your computer and use it in GitHub Desktop.
Creates a new PDF with a 3D Annotation, in this sample a U3D file is used.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/* | |
* Copyright Datalogics, Inc. 2015 | |
*/ | |
package pdfjt.cookbook.annotations.richmedia; | |
import com.adobe.internal.io.LazyRandomAccessFileByteReader; | |
import com.adobe.pdfjt.core.cos.CosCloneMgr; | |
import com.adobe.pdfjt.core.cos.CosObject; | |
import com.adobe.pdfjt.core.types.ASName; | |
import com.adobe.pdfjt.core.types.ASRectangle; | |
import com.adobe.pdfjt.core.types.ASString; | |
import com.adobe.pdfjt.pdf.document.PDFDocument; | |
import com.adobe.pdfjt.pdf.document.PDFOpenOptions; | |
import com.adobe.pdfjt.pdf.document.PDFResources; | |
import com.adobe.pdfjt.pdf.document.PDFStream; | |
import com.adobe.pdfjt.pdf.graphics.PDFRectangle; | |
import com.adobe.pdfjt.pdf.graphics.xobject.PDFXObjectForm; | |
import com.adobe.pdfjt.pdf.graphics.xobject.PDFXObjectImage; | |
import com.adobe.pdfjt.pdf.interactive.annotation.PDFAnnotation3D; | |
import com.adobe.pdfjt.pdf.interactive.annotation.PDFAppearance; | |
import com.adobe.pdfjt.pdf.multimedia.PDF3DADict; | |
import com.adobe.pdfjt.pdf.multimedia.PDF3DCrossSection; | |
import com.adobe.pdfjt.pdf.multimedia.PDF3DDStream; | |
import com.adobe.pdfjt.pdf.multimedia.PDF3DLightingScheme; | |
import com.adobe.pdfjt.pdf.multimedia.PDF3DNode; | |
import com.adobe.pdfjt.pdf.multimedia.PDF3DNodeArray; | |
import com.adobe.pdfjt.pdf.multimedia.PDF3DSectionArray; | |
import com.adobe.pdfjt.pdf.multimedia.PDF3DVBGDict; | |
import com.adobe.pdfjt.pdf.multimedia.PDF3DVDict; | |
import com.adobe.pdfjt.pdf.multimedia.PDF3DVPDict; | |
import com.adobe.pdfjt.pdf.multimedia.PDF3DViewArray; | |
import com.adobe.pdfjt.pdf.page.PDFPage; | |
import com.adobe.pdfjt.services.imageconversion.ImageManager; | |
import com.adobe.pdfjt.services.xobjhandler.XObjectUseOptions; | |
import pdfjt.util.SampleFileServices; | |
import java.awt.image.BufferedImage; | |
import java.io.File; | |
import java.net.URL; | |
import java.util.ArrayList; | |
import java.util.HashMap; | |
import java.util.Map; | |
import javax.imageio.ImageIO; | |
import org.apache.commons.io.FileUtils; | |
/** | |
* Creates a new PDF with a 3D Annotation, in this sample a U3D file | |
* is used. | |
*/ | |
public class InsertRichMediaAnnotation_U3D_Advanced { | |
/* | |
* Read through the entire sample before changing these inputs; 3D views are | |
* model specific. This sample build upon the | |
* "InsertRichMediaAnnotation_U3D" sample by adding additional views that | |
* control node visibility, position, opacity, and other node attributes as | |
* well as showing how to add cross section views. To help see how the view | |
* setting affect the views, the JavaScript to play the animation has been | |
* removed though the coded to set the background image is still present. | |
*/ | |
private static final String inputPath = "http://dev.datalogics.com/cookbook/annotations/richmedia/"; | |
private static final String inputU3DModel = "BrakeAssembly.u3d"; | |
private static final String inputU3DPoster = "BrakeAssemblyPoster.jpg"; | |
private static final String inputBackground = "abstract.jpg"; | |
private static final String inputU3DJavaScript = "addAbstract.js"; | |
private static String outputFilePath = "cookbook/Annotations/output/BrakeAssembly_Advanced.pdf"; | |
public static void main(String[] args) throws Exception { | |
// Create a new document, with a landscape letter-sized pdfPage. | |
ASRectangle pageRectangle = new ASRectangle(0, 0, ASRectangle.US_LETTER.height(), ASRectangle.US_LETTER.width()); | |
PDFDocument pdfDocument = PDFDocument.newInstance(pageRectangle, PDFOpenOptions.newInstance()); | |
/* | |
* The 3D model will be inside a Rich Media Annotation on the first page | |
* so we create that. I want a 1/2 inch border around the annotation so I set up that rectangle. | |
*/ | |
PDFPage pdfPage = pdfDocument.requirePages().getPage(0); | |
PDFRectangle annotationLocation = PDFRectangle.newInstance(pdfDocument, | |
36, | |
36, | |
pdfPage.getMediaBox().width()-36, | |
pdfPage.getMediaBox().height()-36); | |
/* | |
* Create the 3D annotation, position it and add it to the page. | |
*/ | |
PDFAnnotation3D pdfAnnotation3D = PDFAnnotation3D.newInstance(pdfDocument); | |
pdfAnnotation3D.setRect(annotationLocation); | |
/* | |
* Set the 3D annotation's label | |
*/ | |
pdfAnnotation3D.setContents("3D Model"); | |
pdfPage.addAnnotation(pdfAnnotation3D); | |
/* | |
* You're going to want to have a copy of the PDF Specification handy | |
* for the rest of this. See Section 13.6 "3D Artwork". | |
* | |
* The first step is to read the U3D file into the 3D Annotation as a | |
* stream. | |
*/ | |
URL urlU3D = new URL(inputPath+inputU3DModel); | |
File localFileU3D = new File(inputU3DModel); | |
FileUtils.copyURLToFile(urlU3D, localFileU3D); | |
LazyRandomAccessFileByteReader readerU3D = new LazyRandomAccessFileByteReader(localFileU3D); | |
PDF3DDStream pdf3DStream = PDF3DDStream.newInstance(pdfDocument, PDF3DDStream.k_U3D); | |
pdf3DStream.setStreamData(readerU3D); | |
pdfAnnotation3D.set3DDictOrStream(pdf3DStream); | |
/* | |
* While we're working with the PDF3DDStream, we'll add some JavaScript | |
* to the 3D Annotation that will load and run when the 3D Annotation | |
* initializes. This particular JavaScript will play and animation that | |
* is already present in the model. It will play the animation forward | |
* and then reverse the animation continuously. | |
*/ | |
URL urlJS = new URL(inputPath+inputU3DJavaScript); | |
File localFileJS = new File(inputU3DJavaScript); | |
FileUtils.copyURLToFile(urlJS, localFileJS); | |
LazyRandomAccessFileByteReader readerJS = new LazyRandomAccessFileByteReader(localFileJS); | |
PDFStream jsStream = PDFStream.newInstance(pdfDocument); | |
jsStream.setStreamData(readerJS); | |
pdf3DStream.setOnInstantiate(jsStream); | |
/* | |
* Next we set when the 3D Annotation will be activated (enabled) and deactivated (disabled). | |
* To make this section easier to read, I've set up some ASNames matching the Acrobat UI. | |
*/ | |
ASName enableWhenTheContentIsClicked = ASName.k_XA; | |
ASName enableWhenThePageContainingTheContentIsOpened = ASName.k_PO; | |
ASName enableWhenThePageContainingTheContentIsVisible = ASName.k_PV; | |
ASName disableWhenDisableContentIsSelectedFromTheContextMenu = ASName.k_XA; | |
ASName disableWhenThePageContainingTheContentIsClosed = ASName.k_PC; | |
ASName disableWhenThePageContainingTheContentIsNotVisible = ASName.k_PI; | |
/* | |
* Now we can create and set the 3DA (Activation) Dictionary. See table | |
* 299 of the PDF Specification. | |
*/ | |
PDF3DADict pdf3DADict = PDF3DADict.newInstance(pdfDocument); | |
pdfAnnotation3D.set3DA(pdf3DADict); | |
pdf3DADict.setActivation(enableWhenTheContentIsClicked); | |
pdf3DADict.setDeactivation(disableWhenThePageContainingTheContentIsNotVisible); | |
/* | |
* Next we tell the viewer if we want it to open the model tree | |
* automatically when the 3D Annotation is activated. | |
*/ | |
pdf3DADict.setNP(true); | |
/* | |
* Next we set up the 3D view dictionary for the default view. The | |
* settings below mimic what Adobe Acrobat does when importing a U3D | |
* model that has no default lighting scheme. It also adds the default | |
* background color that Acrobat would add. Like with Acrobat, this view | |
* will NOT appear in the list of model views and cannot be addressed | |
* via JavaScript. | |
* | |
* Start by creating the default view and add it to the annotation. This | |
* Object corresponds to the 3DV dictionary. See table 298 of the PDF | |
* Specification. | |
*/ | |
ASString defaultViewName = new ASString("Default"); | |
PDF3DVDict pdf3DViewDictDefault = PDF3DVDict.newInstance(pdfDocument, defaultViewName); | |
pdf3DViewDictDefault.setInternalName(defaultViewName); | |
pdfAnnotation3D.set3DView(pdf3DViewDictDefault); | |
/* | |
* Next create and add the lighting scheme dictionary to the view. See | |
* table 304 of the PDF Specification. | |
*/ | |
PDF3DLightingScheme pdf3DLightingScheme = PDF3DLightingScheme.newInstance(pdfDocument, PDF3DLightingScheme.k_Headlamp); | |
pdf3DLightingScheme.setType(ASName.create("3DLightingScheme")); | |
pdf3DViewDictDefault.setLS(pdf3DLightingScheme); | |
/* | |
* Create and add the background color to the view. | |
*/ | |
PDF3DVBGDict pdf3DVBGDict = PDF3DVBGDict.newInstance(pdfDocument); | |
pdf3DVBGDict.setColor(new double[] {0.375, 0.375, 0.375}); | |
pdf3DViewDictDefault.setBG(pdf3DVBGDict); | |
/* | |
* Create and add an Appearance (Poster) to the 3D Annotation. This is what | |
* displays when the annotation is not active or when the file is being | |
* displayed in a viewer that can't show 3D. Normally, you'd use a U3D | |
* parser to create an image but for the purposes of this sample, I've | |
* done that for you. | |
*/ | |
BufferedImage bufferedImage = ImageIO.read(new URL(inputPath+inputU3DPoster).openStream()); | |
PDFXObjectForm pdfXObjectForm = ImageManager.getXObjPDFImage(pdfDocument, bufferedImage, new XObjectUseOptions()); | |
PDFAppearance pdfAppearance = PDFAppearance.newInstance(pdfDocument); | |
pdfAppearance.setNormalAppearance(pdfXObjectForm); | |
pdfAnnotation3D.setAppearance(pdfAppearance); | |
/* | |
* Add resources | |
*/ | |
BufferedImage bufferedImageBG = ImageIO.read(new URL(inputPath+inputBackground).openStream()); | |
PDFXObjectImage pdfXObjectImageBG = ImageManager.getPDFImage(bufferedImageBG, pdfDocument); | |
ArrayList<Object> resourcesList = new ArrayList<Object>(); | |
resourcesList.add(inputBackground); | |
resourcesList.add(pdfXObjectImageBG); | |
PDFResources pdfResources = PDFResources.newInstance(pdfDocument); | |
pdfResources.setDictionaryArrayValue(ASName.create("Names"), resourcesList); | |
pdf3DStream.setResources(pdfResources); | |
/* | |
* At this point, you've got a functioning 3D Annotation but this is as | |
* far as you can go without knowing more about the U3D geometry of your | |
* specific model. If you are unfamiliar with how to work with 3D models | |
* or don't have access to a U3D tool set... stop... back away slowly... | |
* and don't make any sudden movements... otherwise, keep reading. | |
* | |
* In order to make the "Home" button in the 3D toolbar function | |
* properly in the viewer, you need add camera settings specific to the | |
* model in question. Specifically, you'll need a 12-element 3D | |
* transformation matrix that specifies the position and orientation of | |
* the camera in world coordinates. It's actually a 4x4 matrix but only | |
* the first three columns of the matrix change; so the | |
* matrix is expressed in PDF as an array of 12 numbers. See "Section | |
* 13.6.5 Coordinate Systems for 3D" in the PDF Specification for further | |
* details. | |
* | |
* Normally, you'd get this information by using a U3D parser and doing | |
* a lot of math. But for the purposes of this sample, I've already done | |
* that for you. These numbers are specific to the input U3D supplied | |
* above. Don't expect them to work for any other models. | |
*/ | |
double[] cameraMatrixDefault = { | |
-0.382683, 0.923880, -0, | |
0.180240, 0.074658, 0.980785, | |
0.906127, 0.375330, -0.195090, | |
-1.032850, -0.419857, 0.200171 }; | |
pdf3DViewDictDefault.setMatrixSource(ASName.k_M); | |
pdf3DViewDictDefault.setCameraToWorld(cameraMatrixDefault); | |
pdf3DViewDictDefault.setCenterOfOrbit(1.171060); | |
/* | |
* Create and add a Projection Dictionary to the default view | |
*/ | |
PDF3DVPDict pdf3DVPDict = PDF3DVPDict.newInstance(pdfDocument, ASName.k_P); | |
pdf3DVPDict.setFieldOfView(30); | |
pdf3DVPDict.setPerspactiveScale(ASName.k_Min); | |
pdf3DViewDictDefault.setP(pdf3DVPDict); | |
/* | |
* With these additional settings added to the default view, the "Home" | |
* button on the 3D toolbar will now be functional but we can also add | |
* additional views. The following section will add the equivalent of | |
* the Acrobat generated default views for Left, Top, Front, Right, | |
* Bottom, and Back. However, the first thing we are going to do is make | |
* the Default view available to the JavaScript for Acrobat 3D | |
* Annotations API by creating a view array and adding the default view | |
* to it. This is also where we'll add the additional views. First we | |
* need to set it's external name; this is the name that appears in the | |
* list of views. Because in Acrobat the default view is represented on | |
* the 3D toolbar, I'm using the name "Home" for the default view. | |
* However, this view will be addressed by JavaScript using it's internal | |
* name "default" as we set above. | |
*/ | |
pdf3DViewDictDefault.setExternalName(new ASString("Home")); | |
PDF3DViewArray pdf3DViewArray = PDF3DViewArray.newInstance(pdfDocument); | |
pdf3DViewArray.add(pdf3DViewDictDefault); | |
pdf3DStream.setPresetViewArtwork(pdf3DViewArray); | |
/* | |
* Now we can add the other views. Again, the camera matrix would be | |
* derived using another tool specific to U3D. | |
*/ | |
Map<ASString, double[]> viewMap = new HashMap<ASString, double[]>(); | |
viewMap.put(new ASString("Left"), new double[]{0, 1, 0, 0, 0, 1, 1, 0, 0, -2, 0, 0 }); | |
viewMap.put(new ASString("Top"), new double[]{-1, 0, 0, 0, 1, 0, 0, 0, -1, 0, 0, 2 }); | |
viewMap.put(new ASString("Front"), new double[]{-1, 0, 0, 0, 0, 1, 0, 1, 0, 0, -2, 0 }); | |
viewMap.put(new ASString("Right"), new double[]{0, -1, 0, 0, 0, 1, -1, 0, 0, 1, 0, 0 }); | |
viewMap.put(new ASString("Bottom"), new double[]{-1, 0, 0, 0, -1, 0, 0, 0, 1, 0, 0, -2}); | |
viewMap.put(new ASString("Back"), new double[]{1, 0, 0, 0, 0, 1, 0, -1, 0, 0, 2, 0 }); | |
for (Map.Entry<ASString, double[]> entry : viewMap.entrySet()) { | |
ASString viewName = entry.getKey(); | |
PDF3DVDict pdf3DViewDict = PDF3DVDict.newInstance(pdfDocument, viewName); | |
pdf3DViewDict.setInternalName(viewName); | |
pdf3DViewDict.setExternalName(viewName); | |
pdf3DViewDict.setMatrixSource(ASName.k_M); | |
pdf3DViewDict.setCameraToWorld(entry.getValue()); | |
pdf3DViewDict.setCenterOfOrbit(1); | |
/* Reuse the background and lighting scheme dictionaries color from above. | |
* | |
*/ | |
pdf3DViewDict.setBG(pdf3DVBGDict); | |
pdf3DViewDict.setLS(pdf3DLightingScheme); | |
pdf3DViewDict.setP(pdf3DVPDict); | |
// Add the view | |
pdf3DViewArray.add(pdf3DViewDict); | |
} | |
/* | |
* Advanced Views: Views can have node specific settings that can change | |
* a node's state, opacity and position in world space. This is | |
* controlled by the 3D Node Dictionary array. See 13.6.4.7 3D Node | |
* Dictionaries in the PDF Specification. | |
* | |
* For this view we'll use same matrix as "Front" view but zoom in a | |
* little, move the Caliper and turn one node transparent. We'll start | |
* by creating a view in the same way we did above. | |
*/ | |
PDF3DVPDict pdf3DVPDict_MoveCaliper = PDF3DVPDict.newInstance(pdfDocument, ASName.k_P); | |
// Changing the FOV from 30 to 15 will put the camera close to the model | |
pdf3DVPDict_MoveCaliper.setFieldOfView(15); | |
pdf3DVPDict_MoveCaliper.setPerspactiveScale(ASName.k_Min); | |
ASString hiddenCaliperView = new ASString("Caliper Transparent"); | |
PDF3DVDict pdf3DViewDict_MoveCaliper = PDF3DVDict.newInstance(pdfDocument, hiddenCaliperView); | |
pdf3DViewDict_MoveCaliper.setInternalName(hiddenCaliperView); | |
pdf3DViewDict_MoveCaliper.setExternalName(hiddenCaliperView); | |
pdf3DViewDict_MoveCaliper.setMatrixSource(ASName.k_M); | |
pdf3DViewDict_MoveCaliper.setCameraToWorld(new double[]{-1, 0, 0, 0, 0, 1, 0, 1, 0, 0, -2, 0 }); | |
pdf3DViewDict_MoveCaliper.setP(pdf3DVPDict_MoveCaliper); | |
// We'll just reuse the background, lighting scheme and center of orbit from above | |
pdf3DViewDict_MoveCaliper.setBG(pdf3DVBGDict); | |
pdf3DViewDict_MoveCaliper.setLS(pdf3DLightingScheme); | |
pdf3DViewDict_MoveCaliper.setCenterOfOrbit(1); | |
/* | |
* Now we need to tell the viewer what to do when the user selects this | |
* view. The NR key specifies whether nodes specified in the NA array | |
* are returned to their original states (as specified in the 3D | |
* artwork) before applying the transformation matrices and opacity settings | |
* specified in the node dictionaries. If true, the artwork’s 3D node | |
* parameters are restored to their original states and then the | |
* dictionaries specified by the NA array shall be applied. If false, | |
* the dictionaries specified by the NAarray are applied to the | |
* current states of the nodes. | |
*/ | |
pdf3DViewDict_MoveCaliper.setNAReturned(true); | |
/* | |
* Now we'll add the Node specific stuff. You'll need to know the exact | |
* node name of each node that you want to treat differently from the | |
* view settings created just above. | |
* | |
* First we are going to move the "Caliper" assembly away from the center along the x axis relative to it's parent. | |
* | |
* Then we set the transparency of the "Calipers" node, leaving it in place relative to the "Caliper" assembly. | |
*/ | |
PDF3DNode pdf3DNode_Calipers = PDF3DNode.newInstance(pdfDocument, new ASString("Caliper"), 1, true, new double[]{1, 0, 0, 0, 1, 0, 0, 0, 1, .1, 0, 0 }); | |
PDF3DNode pdf3DNode_Caliper = PDF3DNode.newInstance(pdfDocument, new ASString("Calipers"), .5, true, new double[]{1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0 }); | |
/* | |
* Then we create a NodeArray and add the nodes to it | |
*/ | |
PDF3DNodeArray pdf3DNodeArray = PDF3DNodeArray.newInstance(pdfDocument); | |
pdf3DNodeArray.add(pdf3DNode_Calipers); | |
pdf3DNodeArray.add(pdf3DNode_Caliper); | |
pdf3DViewDict_MoveCaliper.setNA(pdf3DNodeArray); | |
/* | |
* Finally we add the new view to the views array. | |
*/ | |
pdf3DViewArray.add(pdf3DViewDict_MoveCaliper); | |
/* | |
* For the cross section view, we'll use the default view that we | |
* created above and just add the cross section to it. We'll start by | |
* cloning the default view object that we created above. | |
*/ | |
CosCloneMgr cosCloneMgr = new CosCloneMgr(pdfDocument.getCosDocument()); | |
PDF3DVDict pdf3DVDict_CrossSection = PDF3DVDict.getInstance(cosCloneMgr.clone(pdf3DViewDictDefault.getCosObject())); | |
/* | |
* Next we modify the view name. | |
*/ | |
pdf3DVDict_CrossSection.setExternalName(new ASString("Cross Section")); | |
pdf3DVDict_CrossSection.setInternalName(new ASString("Cross Section")); | |
/* | |
* Then we create a cross section. The array that gets passed in is the | |
* Orientation key, a three-element array specifying the orientation of the | |
* cutting plane in world space. Each value represents the | |
* orientation in relation to the X, Y, and Z axes, respectively (see | |
* 13.6.5, “Coordinate Systems for 3D”). Exactly one of the values must | |
* be null, indicating an initial state of the cutting plane that is | |
* perpendicular to the corresponding axis and clipping all geometry on | |
* the positive side of that axis. The other two values are numbers | |
* indicating the rotation of the plane, in degrees, around their | |
* corresponding axes. The order in which these rotations are applied | |
* matches the order in which the values appear in the array. | |
*/ | |
PDF3DCrossSection pdf3DCrossSection = PDF3DCrossSection.newInstance(pdfDocument, new Object[]{0.0,0.0,CosObject.t_Null}); | |
/* | |
* Now we set the offset of the plane to be just slightly below the center on the z axis. | |
*/ | |
pdf3DCrossSection.setCenter(new double[] {0.0, 0.0, -0.01}); | |
/* | |
* Now we set the colors to ones that match what Acrobat creates. This | |
* part is optional; the defaults look just as good. | |
*/ | |
pdf3DCrossSection.setInteresectionColor(new double[] {1.0, 0.0, 0.0}); | |
pdf3DCrossSection.setPlaneVisible(true); | |
pdf3DCrossSection.setPlaneOpacity(.35); | |
pdf3DCrossSection.setPlaneColor(new double[]{0.75, 0.86, 1.00}); | |
/* | |
* The PDF Specification supports multiple cross sections per view. | |
* Though I've never seen a 3D PDF viewer capable of displaying more | |
* than one, we still need to add the cross section to an array of cross | |
* sections before adding the array to the view. | |
*/ | |
PDF3DSectionArray pdf3DSectionArray = PDF3DSectionArray.newInstance(pdfDocument); | |
pdf3DSectionArray.add(pdf3DCrossSection); | |
pdf3DVDict_CrossSection.setSA(pdf3DSectionArray); | |
/* | |
* Then we can finally add the view to the array of views. | |
*/ | |
pdf3DViewArray.add(pdf3DVDict_CrossSection); | |
// Save out the PDF Document just created. | |
SampleFileServices.savePDFDocument(outputFilePath, pdfDocument); | |
System.out.println("Successful output to " + outputFilePath); | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment