/***************************************************************************** * * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you 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.apache.pdfbox.preflight.process; import java.util.HashSet; import java.util.Set; import static org.apache.pdfbox.preflight.PreflightConfiguration.ACTIONS_PROCESS; import static org.apache.pdfbox.preflight.PreflightConstants.ERROR_SYNTAX_NOCATALOG; import static org.apache.pdfbox.preflight.PreflightConstants.ERROR_SYNTAX_TRAILER_OUTLINES_INVALID; import org.apache.pdfbox.cos.COSBase; import org.apache.pdfbox.cos.COSDictionary; import org.apache.pdfbox.cos.COSDocument; import org.apache.pdfbox.cos.COSName; import org.apache.pdfbox.cos.COSNull; import org.apache.pdfbox.cos.COSObject; import org.apache.pdfbox.pdmodel.PDDocumentCatalog; import org.apache.pdfbox.pdmodel.interactive.documentnavigation.outline.PDDocumentOutline; import org.apache.pdfbox.pdmodel.interactive.documentnavigation.outline.PDOutlineItem; import static org.apache.pdfbox.preflight.PreflightConfiguration.DESTINATION_PROCESS; import org.apache.pdfbox.preflight.PreflightContext; import org.apache.pdfbox.preflight.ValidationResult.ValidationError; import org.apache.pdfbox.preflight.exception.ValidationException; import org.apache.pdfbox.preflight.utils.COSUtils; import org.apache.pdfbox.preflight.utils.ContextHelper; public class BookmarkValidationProcess extends AbstractProcess { @Override public void validate(PreflightContext ctx) throws ValidationException { PDDocumentCatalog catalog = ctx.getDocument().getDocumentCatalog(); if (catalog != null) { PDDocumentOutline outlineHierarchy = catalog.getDocumentOutline(); if (outlineHierarchy != null) { COSDictionary dict = outlineHierarchy.getCOSObject(); if (!checkIndirectObjects(ctx, dict)) { return; } COSObject firstObj = toCOSObject(dict.getItem(COSName.FIRST)); COSObject lastObj = toCOSObject(dict.getItem(COSName.LAST)); // Count entry is mandatory if there are childrens if (!isCountEntryPresent(dict) && (outlineHierarchy.getFirstChild() != null || outlineHierarchy.getLastChild() != null)) { addValidationError(ctx, new ValidationError(ERROR_SYNTAX_TRAILER_OUTLINES_INVALID, "Outline Hierarchy doesn't have Count entry")); } else if (isCountEntryPositive(ctx, dict) && (outlineHierarchy.getFirstChild() == null || outlineHierarchy.getLastChild() == null)) { addValidationError(ctx, new ValidationError(ERROR_SYNTAX_TRAILER_OUTLINES_INVALID, "Outline Hierarchy doesn't have First and/or Last entry(ies)")); } else { exploreOutlineLevel(ctx, outlineHierarchy.getFirstChild(), firstObj, lastObj); } } } else { ctx.addValidationError(new ValidationError(ERROR_SYNTAX_NOCATALOG, "There is no /Catalog entry in the Document")); } } /** * Return true if the Count entry is present in the given dictionary. * * @param outline the dictionary representing the document outline. * @return true if the Count entry is present. */ private boolean isCountEntryPresent(COSDictionary outline) { return outline.getItem(COSName.COUNT) != null; } /** * return true if Count entry > 0 * * @param ctx the preflight context. * @param outline the dictionary representing the document outline. * @return true if the Count entry > 0. */ private boolean isCountEntryPositive(PreflightContext ctx, COSDictionary outline) { COSBase countBase = outline.getItem(COSName.COUNT); COSDocument cosDocument = ctx.getDocument().getDocument(); return COSUtils.isInteger(countBase, cosDocument) && (COSUtils.getAsInteger(countBase, cosDocument) > 0); } /** * This method explores the Outline Item Level and calls a validation method on each Outline Item. If an invalid * outline item is found, the result list is updated. * * @param ctx the preflight context. * @param inputItem The first outline item of the level. * @param firstObj The first PDF object of the level. * @param lastObj The last PDF object of the level. * @return true if all items are valid in this level. * @throws ValidationException */ protected boolean exploreOutlineLevel(PreflightContext ctx, PDOutlineItem inputItem, COSObject firstObj, COSObject lastObj) throws ValidationException { PDOutlineItem currentItem = inputItem; COSObject currentObj = firstObj; Set<COSObject> levelObjects = new HashSet<>(); levelObjects.add(firstObj); boolean result = true; if (currentItem != null && inputItem.getPreviousSibling() != null) { addValidationError(ctx, new ValidationError(ERROR_SYNTAX_TRAILER_OUTLINES_INVALID, "The value of /Prev of first object " + firstObj + " on a level is " + inputItem.getCOSObject().getItem(COSName.PREV) + ", but shouldn't exist")); result = false; } while (currentItem != null) { COSObject realPrevObject = currentObj; if (!validateItem(ctx, currentItem)) { result = false; } currentObj = toCOSObject(currentItem.getCOSObject().getItem(COSName.NEXT)); if (levelObjects.contains(currentObj)) { addValidationError(ctx, new ValidationError(ERROR_SYNTAX_TRAILER_OUTLINES_INVALID, "Loop detected: /Next " + currentObj + " is already in the list")); return false; } levelObjects.add(currentObj); currentItem = currentItem.getNextSibling(); if (currentItem == null) { if (!realPrevObject.equals(lastObj)) { addValidationError(ctx, new ValidationError(ERROR_SYNTAX_TRAILER_OUTLINES_INVALID, "Last object on a level isn't the expected /Last: " + lastObj + ", but is " + currentObj)); result = false; } } else { COSObject prevObject = toCOSObject(currentItem.getCOSObject().getItem(COSName.PREV)); if (!realPrevObject.equals(prevObject)) { addValidationError(ctx, new ValidationError(ERROR_SYNTAX_TRAILER_OUTLINES_INVALID, "The value of /Prev at " + currentObj + " doesn't point to previous object " + realPrevObject + ", but to " + prevObject)); result = false; } } } return result; } /** * This method checks the inputItem dictionary and call the exploreOutlineLevel method on the first child if it is * not null. * * @param ctx the preflight context. * @param inputItem outline item to validate * @return the validation result. * @throws ValidationException */ protected boolean validateItem(PreflightContext ctx, PDOutlineItem inputItem) throws ValidationException { boolean isValid = true; // Dest entry isn't permitted if the A entry is present // A entry isn't permitted if the Dest entry is present // If the A entry is present, the referenced actions is validated COSDictionary dictionary = inputItem.getCOSObject(); COSBase dest = dictionary.getItem(COSName.DEST); COSBase action = dictionary.getItem(COSName.A); if (!checkIndirectObjects(ctx, dictionary)) { return false; } if (action != null && dest != null) { addValidationError(ctx, new ValidationError(ERROR_SYNTAX_TRAILER_OUTLINES_INVALID, "Dest entry isn't permitted if the A entry is present")); return false; } else if (action != null) { ContextHelper.validateElement(ctx, dictionary, ACTIONS_PROCESS); } else if (dest != null) { ContextHelper.validateElement(ctx, dest, DESTINATION_PROCESS); } // else no specific validation // check children PDOutlineItem fChild = inputItem.getFirstChild(); if (fChild != null) { if (!isCountEntryPresent(inputItem.getCOSObject())) { addValidationError(ctx, new ValidationError(ERROR_SYNTAX_TRAILER_OUTLINES_INVALID, "Outline item doesn't have Count entry but has at least one descendant")); isValid = false; } else { COSObject firstObj = toCOSObject(dictionary.getItem(COSName.FIRST)); COSObject lastObj = toCOSObject(dictionary.getItem(COSName.LAST)); if ((firstObj == null && lastObj != null) || (firstObj != null && lastObj == null)) { addValidationError(ctx, new ValidationError(ERROR_SYNTAX_TRAILER_OUTLINES_INVALID, "/First and /Last are both required if there are outline entries")); isValid = false; } // there are some descendants, so dictionary must have a Count entry isValid = isValid && exploreOutlineLevel(ctx, fChild, firstObj, lastObj); } } return isValid; } // verify that if certain named items exist, that they are indirect objects private boolean checkIndirectObjects(PreflightContext ctx, COSDictionary dictionary) { // Parent, Prev, Next, First and Last must be indirect objects if (!checkIndirectObject(ctx, dictionary, COSName.PARENT)) { return false; } if (!checkIndirectObject(ctx, dictionary, COSName.PREV)) { return false; } if (!checkIndirectObject(ctx, dictionary, COSName.NEXT)) { return false; } if (!checkIndirectObject(ctx, dictionary, COSName.FIRST)) { return false; } return checkIndirectObject(ctx, dictionary, COSName.LAST); } // verify that if the named item exists, that it is is an indirect object private boolean checkIndirectObject(PreflightContext ctx, COSDictionary dictionary, COSName name) { COSBase item = dictionary.getItem(name); if (item == null || item instanceof COSNull || item instanceof COSObject) { return true; } addValidationError(ctx, new ValidationError(ERROR_SYNTAX_TRAILER_OUTLINES_INVALID, "/" + name.getName() + " entry must be an indirect object")); return false; } /** * Returns a COSBase as a COSObject or null if null or COSNull. To avoid * trouble, this method is to be called only after having called * {@link #checkIndirectObjects(PreflightContext, COSDictionary)}. * * @param base should be null, COSNull or a COSObject. * @return null if the parameter is COSNull or null; or else a COSObject. * @throws IllegalArgumentException if the parameter is not null, COSNull or * a COSObject. */ private COSObject toCOSObject(COSBase base) { if (base == null || base instanceof COSNull) { return null; } if (!(base instanceof COSObject)) { throw new IllegalArgumentException("Paremater " + base + " should be null, COSNull or a COSObject"); } return (COSObject) base; } }