package org.docear.plugin.pdfutilities.pdf;
import java.awt.Shape;
import java.awt.geom.AffineTransform;
import java.awt.geom.Rectangle2D;
import java.io.File;
import java.io.IOException;
import java.net.URI;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import javax.swing.JOptionPane;
import org.docear.addons.highlights.IHighlightsImporter;
import org.docear.pdf.annotation.AnnotationExtractor;
import org.docear.pdf.annotation.CommentAnnotation;
import org.docear.pdf.annotation.HighlightAnnotation;
import org.docear.pdf.bookmark.Bookmark;
import org.docear.pdf.bookmark.BookmarkExtractor;
import org.docear.pdf.feature.ADocumentCreator;
import org.docear.pdf.feature.AObjectType;
import org.docear.pdf.feature.APDMetaObject;
import org.docear.pdf.feature.PageDestination;
import org.docear.pdf.feature.UriDestination;
import org.docear.plugin.core.DocearController;
import org.docear.plugin.core.util.HtmlUtils;
import org.docear.plugin.pdfutilities.PdfUtilitiesController;
import org.docear.plugin.pdfutilities.addons.DocearAddonController;
import org.docear.plugin.pdfutilities.features.AnnotationModel;
import org.docear.plugin.pdfutilities.features.IAnnotation;
import org.docear.plugin.pdfutilities.features.IAnnotation.AnnotationType;
import org.docear.plugin.pdfutilities.map.AnnotationController;
import org.docear.plugin.pdfutilities.map.IAnnotationImporter;
import org.freeplane.core.util.LogUtils;
import org.freeplane.core.util.TextUtils;
import org.freeplane.features.map.MapModel;
import org.freeplane.features.map.NodeModel;
import org.freeplane.features.mode.Controller;
import org.freeplane.plugin.workspace.URIUtils;
import com.google.common.base.CharMatcher;
import de.intarsys.pdf.content.CSDeviceBasedInterpreter;
import de.intarsys.pdf.cos.COSArray;
import de.intarsys.pdf.cos.COSRuntimeException;
import de.intarsys.pdf.pd.PDAnnotation;
import de.intarsys.pdf.pd.PDDocument;
import de.intarsys.pdf.pd.PDOutlineItem;
import de.intarsys.pdf.pd.PDPage;
import de.intarsys.pdf.pd.PDTextMarkupAnnotation;
import de.intarsys.pdf.tools.kernel.PDFGeometryTools;
public class PdfAnnotationImporter implements IAnnotationImporter {
private URI currentFile;
private boolean importAll = false;
private boolean setPDObject = false;
private int removeLinebreaksDialogResult = JOptionPane.OK_OPTION;
private boolean modifiedDocument;
public PdfAnnotationImporter(){
}
public Map<URI, List<AnnotationModel>> importAnnotations(List<URI> files) throws IOException, DocumentReadOnlyException {
Map<URI, List<AnnotationModel>> annotationMap = new HashMap<URI, List<AnnotationModel>>();
for(URI file : files){
annotationMap.put(file, this.importAnnotations(file));
}
return annotationMap;
}
public List<AnnotationModel> importAnnotations(URI uri) throws IOException, DocumentReadOnlyException {
List<AnnotationModel> annotations = new ArrayList<AnnotationModel>();
this.currentFile = uri;
PDDocument document = getPDDocument(uri);
if(document == null){
return annotations;
}
this.modifiedDocument = false;
try{
this.importAnnotations(document, annotations);
this.importBookmarks(document, annotations);
} catch(Exception e){
LogUtils.warn(e);
return annotations;
} finally {
if(document != null){
if(this.modifiedDocument && !document.isReadOnly()) {
document.save();
}
document.close();
document = null;
}
}
return annotations;
}
public AnnotationModel importPdf(URI uri) throws IOException, DocumentReadOnlyException {
Collection<AnnotationModel> importedAnnotations = new ArrayList<AnnotationModel>();
try{
importedAnnotations = importAnnotations(uri);
} catch(RuntimeException e){
LogUtils.info("IOexception during update file: "+ uri); //$NON-NLS-1$
LogUtils.warn(e);
}
URI absoluteUri = URIUtils.getAbsoluteURI(uri);
AnnotationModel root = new AnnotationModel(0, AnnotationType.PDF_FILE);
root.setSource(absoluteUri);
root.setTitle(URIUtils.getFile(absoluteUri).getName());
root.getChildren().addAll(importedAnnotations);
return root;
}
public boolean renameAnnotation(IAnnotation annotation, String newTitle) throws IOException, DocumentReadOnlyException {
if(newTitle.startsWith("<HTML>") || newTitle.startsWith("<html>")){
newTitle = HtmlUtils.extractText(newTitle);
}
List<AnnotationModel> annotations = new ArrayList<AnnotationModel>();
boolean ret = false;
this.currentFile = annotation.getSource();
PDDocument document = getPDDocument(annotation.getSource());
if(document != null){
try{
this.setImportAll(true);
this.setPDObject(true);
this.importAnnotations(document, annotations);
this.importBookmarks(document, annotations);
} catch(Exception e){
LogUtils.warn(e);
} finally {
this.setImportAll(true);
this.setPDObject(false);
AnnotationModel result = this.searchAnnotation(annotations, annotation);
if(result != null){
Object annotationObject = result.getAnnotationObject();
if(annotationObject != null && annotationObject instanceof PDOutlineItem){
((PDOutlineItem)annotationObject).setTitle(newTitle);
ret = true;
}
if(annotationObject != null && annotationObject instanceof PDAnnotation){
((PDAnnotation)annotationObject).setContents(newTitle);
ret = true;
}
document.save();
}
if(document != null) {
document.close();
}
}
}
return ret;
}
private void setPDObject(boolean b) {
setPDObject = b;
}
private boolean setPDObject() {
return setPDObject;
}
public PDDocument getPDDocument(URI uri) throws IOException, DocumentReadOnlyException {
MapModel map = Controller.getCurrentController().getMap();
URI absoluteUri = URIUtils.resolveURI(URIUtils.getAbsoluteURI(map), uri);
File file = URIUtils.getFile(absoluteUri);
if(uri == null || file == null || !file.exists() || !PdfFileFilter.accept(uri)){
return null;
}
PDDocument document = ADocumentCreator.getPDDocument(file);
if(document != null && document.isReadOnly()) {
document.close();
document = null;
throw new DocumentReadOnlyException();
}
return document;
}
private void importAnnotations(PDDocument document, List<AnnotationModel> annotations) throws IOException {
boolean importComments = false;
boolean importHighlightedTexts = false;
boolean importPopUpHighlighted = false;
boolean importPopUpOnly = false;
if(this.importAll){
importComments = true;
importHighlightedTexts = true;
}
else{
importComments = DocearController.getPropertiesController().getBooleanProperty(PdfUtilitiesController.IMPORT_COMMENTS_KEY);
importHighlightedTexts = DocearController.getPropertiesController().getBooleanProperty(PdfUtilitiesController.IMPORT_HIGHLIGHTED_TEXTS_KEY);
}
importPopUpHighlighted = DocearController.getPropertiesController().getBooleanProperty(PdfUtilitiesController.IMPORT_POP_UP_HIGHLIGHTED_KEY);
importPopUpOnly = DocearController.getPropertiesController().getBooleanProperty(PdfUtilitiesController.IMPORT_ONLY_POP_UP_KEY);
if(DocearAddonController.getController().hasPlugin(IHighlightsImporter.class) && !importPopUpOnly){
IHighlightsImporter highlightsImporter = DocearAddonController.getController().getAddon(IHighlightsImporter.class);
try{
List<APDMetaObject> metaObjects = highlightsImporter.getMetaObjects(document, importComments, importHighlightedTexts, importPopUpHighlighted);
this.modifiedDocument = highlightsImporter.isDocumentModified() || this.modifiedDocument;
for (APDMetaObject meta : metaObjects) {
AnnotationModel annotation = new AnnotationModel(meta.getUID());
transferMetaObject(document, meta, annotation);
annotations.add(annotation);
}
}
finally{
highlightsImporter.resetAll();
}
}
else{
AnnotationExtractor extractor = new AnnotationExtractor(document);
try {
extractor.setIgnoreComments(!importComments);
extractor.setIgnoreHighlights(!importHighlightedTexts);
List<APDMetaObject> metaObjects = extractor.getMetaObjects();
this.modifiedDocument = extractor.isDocumentModified() || this.modifiedDocument;
for (APDMetaObject meta : metaObjects) {
AnnotationModel annotation = new AnnotationModel(meta.getUID());
transferMetaObject(document, meta, annotation);
annotations.add(annotation);
}
}
finally {
extractor.resetAll();
}
}
}
private void importBookmarks(PDDocument document, List<AnnotationModel> annotations) throws IOException {
if(document == null || (!this.importAll && !DocearController.getPropertiesController().getBooleanProperty(PdfUtilitiesController.IMPORT_BOOKMARKS_KEY))){
return;
}
BookmarkExtractor extractor = new BookmarkExtractor(document);
try {
List<APDMetaObject> metaObjects = extractor.getMetaObjects();
this.modifiedDocument = extractor.isDocumentModified() || this.modifiedDocument;
importBookmarksRecursive(document, annotations, metaObjects);
}
finally {
extractor.resetAll();
}
}
private void importBookmarksRecursive(PDDocument document, List<AnnotationModel> annotations, List<APDMetaObject> metaObjects) {
for (APDMetaObject meta : metaObjects) {
AnnotationModel annotation = new AnnotationModel(meta.getUID());
transferMetaObject(document, meta, annotation);
annotations.add(annotation);
}
}
private void transferMetaObject(PDDocument document, APDMetaObject meta, AnnotationModel annotation) {
annotation.setOldObjectNumber(meta.getObjectNumber());
annotation.setIsNewID(meta.getContext().isNewID());
annotation.setSource(this.currentFile);
annotation.setTitle(meta.getText());
annotation.setAnnotationType(getAnnotationTypeFromMeta(meta.getType()));
if(meta.getDestination() != null) {
if(meta.getDestination() instanceof PageDestination) {
annotation.setPage(((PageDestination)meta.getDestination()).getPage());
}
else if(meta.getDestination() instanceof UriDestination) {
annotation.setDestinationUri(((UriDestination)meta.getDestination()).getUri());
}
}
removeLinebreaks(annotation, meta.getObjectReference(), document);
if(this.setPDObject()){
annotation.setAnnotationObject(meta.getObjectReference());
}
if(meta.hasChildren()) {
for (APDMetaObject metaChild : meta.getChildren()) {
AnnotationModel childAnnotation = new AnnotationModel(metaChild.getUID());
transferMetaObject(document, metaChild, childAnnotation);
annotation.getChildren().add(childAnnotation);
}
}
}
private AnnotationType getAnnotationTypeFromMeta(AObjectType metaType) {
if(Bookmark.BOOKMARK.equals(metaType)) {
return AnnotationType.BOOKMARK;
}
if(Bookmark.BOOKMARK_WITH_URI.equals(metaType)) {
return AnnotationType.BOOKMARK_WITH_URI;
}
if(Bookmark.BOOKMARK_WITHOUT_DESTINATION.equals(metaType)) {
return AnnotationType.BOOKMARK_WITHOUT_DESTINATION;
}
if(CommentAnnotation.COMMENT.equals(metaType)) {
return AnnotationType.COMMENT;
}
if(HighlightAnnotation.HIGHTLIGHTED_TEXT.equals(metaType)) {
return AnnotationType.HIGHLIGHTED_TEXT;
}
if(metaType.toString().equalsIgnoreCase("TRUE_HIGHTLIGHTED_TEXT")){
return AnnotationType.TRUE_HIGHLIGHTED_TEXT;
}
return null;
}
public void removeLinebreaks(IAnnotation annotation, Object annotationObject, PDDocument document) {
if(this.removeLinebreaksDialogResult == JOptionPane.CANCEL_OPTION) return;
String oldText = annotation.getTitle();
String text = removeLinebreaks(annotation.getTitle(), false);
if(text.equals(annotation.getTitle())) {
return;
}
// Existing properties are not used yet --> help for Marcel :)
// boolean removeFromBookmarks = DocearController.getPropertiesController().getBooleanProperty(PdfUtilitiesController.REMOVE_LINEBREAKS_BOOKMARKS_KEY);
// boolean removeFromComments = DocearController.getPropertiesController().getBooleanProperty(PdfUtilitiesController.REMOVE_LINEBREAKS_COMMENTS_KEY);
// boolean removeFromHighlights = DocearController.getPropertiesController().getBooleanProperty(PdfUtilitiesController.REMOVE_LINEBREAKS_HIGHLIGHTED_KEY);
// if((annotation.getAnnotationType() == AnnotationType.BOOKMARK && !removeFromBookmarks)
// || (annotation.getAnnotationType() == AnnotationType.COMMENT && !removeFromComments)
// || (annotation.getAnnotationType() == AnnotationType.HIGHLIGHTED_TEXT && !removeFromHighlights)
// ) {
// return;
// }
if(annotationObject != null && annotationObject instanceof PDOutlineItem){
((PDOutlineItem)annotationObject).setTitle(text);
annotation.setTitle(text);
}
if(annotationObject != null && annotationObject instanceof PDAnnotation){
((PDAnnotation)annotationObject).setContents(text);
annotation.setTitle(text);
}
try{
document.save();
}catch (IOException e) {
if(e.getMessage().equals("destination is read only")){ //$NON-NLS-1$
Object[] options = { TextUtils.getText("DocearRenameAnnotationListener.1"), TextUtils.getText("DocearRenameAnnotationListener.8"),TextUtils.getText("DocearRenameAnnotationListener.3") }; //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$
int result = this.removeLinebreaksDialogResult;
if(result == JOptionPane.OK_OPTION){
result = JOptionPane.showOptionDialog(Controller.getCurrentController().getMapViewManager().getSelectedComponent(), TextUtils.getText("DocearRenameAnnotationListener.6")+document.getName()+TextUtils.getText("DocearRenameAnnotationListener.7"), TextUtils.getText("DocearRenameAnnotationListener.5"), JOptionPane.YES_NO_CANCEL_OPTION, JOptionPane.QUESTION_MESSAGE, null, options, options[0]); //$NON-NLS-1$ //$NON-NLS-2$
}
if( result == JOptionPane.OK_OPTION){
if(annotationObject != null && annotationObject instanceof PDOutlineItem){
((PDOutlineItem)annotationObject).setTitle(oldText);
annotation.setTitle(oldText);
}
if(annotationObject != null && annotationObject instanceof PDAnnotation){
((PDAnnotation)annotationObject).setContents(oldText);
annotation.setTitle(oldText);
}
removeLinebreaks(annotation, annotationObject, document);
}
else if( result == JOptionPane.CANCEL_OPTION ){
this.removeLinebreaksDialogResult = JOptionPane.CANCEL_OPTION;
if(annotationObject != null && annotationObject instanceof PDOutlineItem){
((PDOutlineItem)annotationObject).setTitle(oldText);
annotation.setTitle(oldText);
}
if(annotationObject != null && annotationObject instanceof PDAnnotation){
((PDAnnotation)annotationObject).setContents(oldText);
annotation.setTitle(oldText);
}
}
else if( result == JOptionPane.NO_OPTION ){
this.removeLinebreaksDialogResult = JOptionPane.NO_OPTION;
if(annotationObject != null && annotationObject instanceof PDOutlineItem){
((PDOutlineItem)annotationObject).setTitle(oldText);
annotation.setTitle(text);
}
if(annotationObject != null && annotationObject instanceof PDAnnotation){
((PDAnnotation)annotationObject).setContents(oldText);
annotation.setTitle(text);
}
}
}
else{
LogUtils.severe("RemoveLinebreaksImport IOException at Target("+oldText+"): ", e); //$NON-NLS-1$ //$NON-NLS-2$
}
} catch (COSRuntimeException e) {
LogUtils.severe("RemoveLinebreaksImport COSRuntimeException at Target("+oldText+"): ", e); //$NON-NLS-1$ //$NON-NLS-2$
}
}
public static String removeLinebreaks(String text, boolean forced) {
boolean removeLinebreaks = DocearController.getPropertiesController().getBooleanProperty(PdfUtilitiesController.REMOVE_LINEBREAKS_KEY);
if(removeLinebreaks || forced) {
boolean keepDoubleLinebreaks = DocearController.getPropertiesController().getBooleanProperty(PdfUtilitiesController.KEEP_DOUBLE_LINEBREAKS_KEY);
boolean addSpaces = DocearController.getPropertiesController().getBooleanProperty(PdfUtilitiesController.ADD_SPACES_KEY);
boolean removeDashes = DocearController.getPropertiesController().getBooleanProperty(PdfUtilitiesController.REMOVE_DASHES_KEY);
String lines[] = text.split("\\r?\\n");
if(lines.length < 2) return text;
StringBuilder sb = new StringBuilder();
for(int i = 0; i < lines.length; i++){
if(keepDoubleLinebreaks && (i + 1 < lines.length) && lines[i + 1].isEmpty()){
lines[i] = lines[i] + "\n\n";
sb.append(lines[i]);
i = i + 1;
continue;
}
if(removeDashes && lines[i].endsWith("-")){
lines[i] = lines[i].substring(0, lines[i].length() - 1);
if(i + 1 < lines.length && lines[i + 1].startsWith(" ")){
lines[i] = CharMatcher.WHITESPACE.trimFrom(lines[i]);
}
sb.append(lines[i]);
continue;
}
if(addSpaces){
if((i + 1 < lines.length) && (!lines[i].endsWith(" ") && !lines[i + 1].startsWith(" "))){
lines[i] = CharMatcher.WHITESPACE.trimFrom(lines[i]) + " ";
sb.append(lines[i]);
continue;
}
}
sb.append(lines[i]);
}
return sb.toString();
}
return text;
}
protected String extractAnnotationText(PDPage pdPage, PDTextMarkupAnnotation annotation) {
StringBuilder sb = new StringBuilder();
COSArray rect = (COSArray)annotation.cosGetField(PDTextMarkupAnnotation.DK_QuadPoints);
for(int i = 0; i < (rect.size() / 8); i++){
TextExtractor extractor = new TextExtractor();
Float lowerLeft_X = Math.min(Math.min(rect.get(0 + (8 * i)).getValueFloat(0), rect.get(2 + (8 * i)).getValueFloat(0)), Math.min(rect.get(4 + (8 * i)).getValueFloat(0), rect.get(6 + (8 * i)).getValueFloat(0)));
Float upperRight_X = Math.max(Math.max(rect.get(0 + (8 * i)).getValueFloat(0), rect.get(2 + (8 * i)).getValueFloat(0)), Math.max(rect.get(4 + (8 * i)).getValueFloat(0), rect.get(6 + (8 * i)).getValueFloat(0)));
Float lowerLeft_Y = Math.min(Math.min(rect.get(1 + (8 * i)).getValueFloat(0), rect.get(3 + (8 * i)).getValueFloat(0)), Math.min(rect.get(5 + (8 * i)).getValueFloat(0), rect.get(7 + (8 * i)).getValueFloat(0)));
Float upperRight_y = Math.max(Math.max(rect.get(1 + (8 * i)).getValueFloat(0), rect.get(3 + (8 * i)).getValueFloat(0)), Math.max(rect.get(5 + (8 * i)).getValueFloat(0), rect.get(7 + (8 * i)).getValueFloat(0)));
Shape shape = new Rectangle2D.Float(lowerLeft_X, lowerLeft_Y, upperRight_X - lowerLeft_X, upperRight_y - lowerLeft_Y);
extractor.setBounds(shape);
AffineTransform pageTx = new AffineTransform();
PDFGeometryTools.adjustTransform(pageTx, pdPage);
extractor.setDeviceTransform(pageTx);
CSDeviceBasedInterpreter interpreter = new CSDeviceBasedInterpreter(null, extractor);
interpreter.process(pdPage.getContentStream(), pdPage.getResources());
sb.append(extractor.getContent().trim());
if(i < ((rect.size() / 8) - 1)){
sb.append("\n");
}
}
return sb.toString();
}
public Integer getAnnotationDestination(PDAnnotation pdAnnotation) {
if(pdAnnotation != null){
PDPage page = pdAnnotation.getPage();
if(page != null)
return page.getNodeIndex()+1;
}
return null;
}
public AnnotationModel searchAnnotation(URI uri, NodeModel node) throws Exception {
this.currentFile = uri;
if(!this.isImportAll()) this.setImportAll(true);
List<AnnotationModel> annotations = this.importAnnotations(uri);
this.setImportAll(false);
return searchAnnotation(annotations, node);
}
public AnnotationModel searchAnnotation(IAnnotation annotation) throws Exception {
if(annotation.getAnnotationID() != null && annotation.getAnnotationID().getUri() != null){
this.currentFile = annotation.getAnnotationID().getUri();
if(!this.isImportAll()) this.setImportAll(true);
List<AnnotationModel> annotations = this.importAnnotations(annotation.getAnnotationID().getUri());
this.setImportAll(false);
return searchAnnotation(annotations, annotation);
}
else{
return null;
}
}
private AnnotationModel searchAnnotation(List<AnnotationModel> annotations, NodeModel node) {
for(AnnotationModel annotation : annotations){
AnnotationModel extensionModel = AnnotationController.getModel(node, false);
if(extensionModel == null){
if(annotation.getTitle().equals(node.getText())){
//TODO: DOCEAR is Update nodeModel good here??
//TODO: DOCEAR How to deal with nodes without extension(and object number) and changed annotation title ??
AnnotationController.setModel(node, annotation);
return annotation;
}
else{
AnnotationModel searchResult = searchAnnotation(annotation.getChildren(), node);
if(searchResult != null) return searchResult;
}
}
else{
return searchAnnotation(annotations, extensionModel);
}
}
return null;
}
private AnnotationModel searchAnnotation(List<AnnotationModel> annotations, IAnnotation target) {
for(AnnotationModel annotation : annotations){
if(annotation.getObjectID() == target.getObjectID()){
return annotation;
}
else{
AnnotationModel searchResult = searchAnnotation(annotation.getChildren(), target);
if(searchResult != null) return searchResult;
}
}
return null;
}
public boolean isImportAll() {
return importAll;
}
public void setImportAll(boolean importAll) {
this.importAll = importAll;
}
}