Skip to content

Instantly share code, notes, and snippets.

@RyanThomas73
Created October 18, 2019 17:21
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save RyanThomas73/0d0fa73d893a335ba99aca8cbd915822 to your computer and use it in GitHub Desktop.
Save RyanThomas73/0d0fa73d893a335ba99aca8cbd915822 to your computer and use it in GitHub Desktop.
Pdfium.Net.SDK ImportAcrobatForm(..) Example
using Patagames.Pdf.Net;
using Patagames.Pdf.Net.Annotations;
using Patagames.Pdf.Net.BasicTypes;
using System;
using System.Collections.Generic;
namespace XXX
{
internal static class PdfiumExtensionHelper
{
private const int MAX_INDIRECT_CHAIN_COUNT = 32;
private static readonly List<string> COPY_ACROBAT_FORM_EXCLUDED_KEYS = new List<string> { "DR", "Fields" };
private static readonly List<string> COPY_PARENT_EXCLUDED_KEYS = new List<string> { "Parent", "Kids" };
private static readonly PdfTypeNull PDF_NULL_VALUE = PdfTypeNull.Create();
private static void AddKidsToNewParent(
PdfTypeDictionary childDictionary,
IDictionary<int, PdfTypeIndirect> destinationDocumentIndirectCache,
PdfIndirectList destinationDocumentIndirectList,
PdfTypeDictionary newParentDictionary
)
{
if (childDictionary.TryGetValue("Parent", out var currentParent))
{
var currentParentIndirect = currentParent as PdfTypeIndirect;
if (currentParentIndirect != null)
{
(currentParent, currentParentIndirect) = GetDirectObjectAndPrimaryIndirect(currentParentIndirect);
}
if (currentParent != newParentDictionary && currentParent.Handle != newParentDictionary.Handle)
{
childDictionary["Parent"] = currentParentIndirect == null
? (PdfTypeBase)newParentDictionary
: (PdfTypeBase)MakeIndirect(newParentDictionary, destinationDocumentIndirectCache, destinationDocumentIndirectList);
}
}
else
{
#if DEBUG
System.Diagnostics.Debugger.Break();
#endif
}
PdfTypeIndirect childIndirect = null;
if (newParentDictionary.TryGetValue("Kids", out var kidsObject))
{
var kidsIndirect = kidsObject as PdfTypeIndirect;
if (kidsIndirect != null)
{
(kidsObject, kidsIndirect) = GetDirectObjectAndPrimaryIndirect(kidsIndirect);
}
if (kidsObject is PdfTypeArray kidsArray)
{
childIndirect = MakeIndirect(childDictionary, destinationDocumentIndirectCache, destinationDocumentIndirectList);
kidsArray.Add(childIndirect);
return;
}
throw new InvalidOperationException("The newParentDictionary has an existing non-array 'Kids' entry");
}
var newKidsArray = PdfTypeArray.Create();
//Kids array doesn't appear to need to be made indirect...
//MakeIndirect(newKidsArray, destinationDocumentIndirectCache, destinationDocumentIndirectList);
childIndirect = MakeIndirect(childDictionary, destinationDocumentIndirectCache, destinationDocumentIndirectList);
newKidsArray.Add(childIndirect);
newParentDictionary["Kids"] = newKidsArray;
}
internal static int? ComputeHashForCopy(PdfTypeIndirect primaryIndirect)
{
var directObject = primaryIndirect?.Direct;
if (directObject == null || directObject.ObjectNumber < 1)
{
return null;
}
return (((primaryIndirect.List.GetHashCode() * 31) * directObject.ObjectNumber) * 31) + directObject.GenerationNumber;
}
internal static PdfTypeBase CopyObjectToDocument(
IDictionary<int, PdfTypeIndirect> destinationDocumentCopiedObjects,
IDictionary<int, PdfTypeIndirect> destinationDocumentIndirectCache,
PdfIndirectList destinationDocumentIndirectList,
IList<string> excludedKeys,
bool isDuplicationAllowed,
IDictionary<int, PdfTypeIndirect> sourceDocumentIndirectCache,
PdfIndirectList sourceDocumentIndirectList,
PdfTypeBase sourceObject
)
{
var objectToClone = sourceObject;
PdfTypeIndirect objectToCloneSourceIndirect = null;
if (objectToClone is PdfTypeIndirect objectToCloneAsIndirect)
{
(objectToClone, objectToCloneSourceIndirect) = GetDirectObjectAndPrimaryIndirect(objectToCloneAsIndirect);
}
if (IsInIndirectList(destinationDocumentIndirectCache, destinationDocumentIndirectList, objectToClone))
{
// object to clone is already in the destination document
return objectToClone;
}
if (objectToClone == null)
{
return PDF_NULL_VALUE;
}
if (IsCatalog(objectToClone))
{
throw new InvalidOperationException("CopyDictionaryToDocument(..) cannot be used for a catalog dictionary");
}
// If the object is type that uses indirect references (e.g. dictionaries, arrays) and we don't already have an indirect reference
// try to find one so we can use it to compute a copy hash for the object
if (objectToCloneSourceIndirect == null)
{
objectToCloneSourceIndirect = FindIndirect(sourceDocumentIndirectCache, sourceDocumentIndirectList, objectToClone);
}
var copiedObjectHash = ComputeHashForCopy(objectToCloneSourceIndirect);
var shouldTryToFindDuplicate = !isDuplicationAllowed && copiedObjectHash != null;
if (shouldTryToFindDuplicate)
{
if (destinationDocumentCopiedObjects.TryGetValue(copiedObjectHash.Value, out var copiedIndirectReference))
{
return copiedIndirectReference;
}
}
// TODO: Determine what additional performance gains we can make ?
if (objectToClone is PdfTypeDictionary sourceDictionary)
{
var newDictionary = PdfTypeDictionary.Create();
var newDictionaryIndirectReference = MakeIndirect(newDictionary, destinationDocumentIndirectCache, destinationDocumentIndirectList);
if (copiedObjectHash != null)
{
destinationDocumentCopiedObjects.Add(copiedObjectHash.Value, newDictionaryIndirectReference);
}
foreach (string nextKey in sourceDictionary.Keys)
{
if (excludedKeys?.Contains(nextKey) == true)
{
continue;
}
var nextSourceObject = sourceDictionary[nextKey];
if (nextSourceObject != null)
{
var nextDestinationObject = CopyObjectToDocument(
destinationDocumentCopiedObjects,
destinationDocumentIndirectCache,
destinationDocumentIndirectList,
null,
false,
sourceDocumentIndirectCache,
sourceDocumentIndirectList,
nextSourceObject
);
newDictionary[nextKey] = nextDestinationObject;
}
}
// return the dictionary indirect so we add the indirect to dictionaries/arrays as we traverse the tree
return newDictionaryIndirectReference;
}
if (objectToClone is PdfTypeArray sourceArray)
{
var newArray = PdfTypeArray.Create();
var newArrayIndirect = MakeIndirect(newArray, destinationDocumentIndirectCache, destinationDocumentIndirectList);
if (copiedObjectHash != null)
{
destinationDocumentCopiedObjects.Add(copiedObjectHash.Value, newArrayIndirect);
}
foreach (var nextSourceObject in sourceArray)
{
var nextDestinationObject = CopyObjectToDocument(
destinationDocumentCopiedObjects,
destinationDocumentIndirectCache,
destinationDocumentIndirectList,
null,
false,
sourceDocumentIndirectCache,
sourceDocumentIndirectList,
nextSourceObject
);
newArray.Add(nextDestinationObject);
}
// Return the indirect so that we add the indirect to dictionaries/arrays as we recurse the tree
return newArrayIndirect;
}
if(objectToClone is PdfTypeString
|| objectToClone is PdfTypeNumber
|| objectToClone is PdfTypeBoolean
|| objectToClone is PdfTypeName)
{
return objectToClone.Clone(false);
}
#if DEBUG
System.Diagnostics.Debugger.Break();
#endif
return objectToClone.Clone(false);
}
/// <summary>
/// Recursively traverses up through the parent fields. Any parent's that do not already exist in the destination document's
/// indirect list get copied into it. The root field dictionary is returned.
/// </summary>
/// <param name="destinationDocumentCopiedObjects">Cache for tracking objects that have already been copied to the destination document</param>
/// <param name="destinationDocumentIndirectCache">The cache for fast lookup of indirect objects for the destiation document's indirect list</param>
/// <param name="destinationDocumentIndirectList">The PdfIndirectList for the destination document</param>
/// <param name="fieldDictionary">The field dictionary who's parents need to be traversed</param>
/// <param name="sourceDocumentIndirectCache">The cache for fast lookup of indirect objects for the source document's indirect list</param>
/// <param name="sourceDocumentIndirectList">The PdfIndirectList for the source document</param>
/// <returns>The root field dictionary</returns>
private static PdfTypeDictionary CopyParentFieldsToDocumentIfNeeded(
IDictionary<int, PdfTypeIndirect> destinationDocumentCopiedObjects,
IDictionary<int, PdfTypeIndirect> destinationDocumentIndirectCache,
PdfIndirectList destinationDocumentIndirectList,
PdfTypeDictionary fieldDictionary,
IDictionary<int, PdfTypeIndirect> sourceDocumentIndirectCache,
PdfIndirectList sourceDocumentIndirectList
)
{
var isInDestinationDocument = IsInIndirectList(destinationDocumentIndirectCache, destinationDocumentIndirectList, fieldDictionary);
var rootFieldDictionary = fieldDictionary;
var originalDictionaryToCheckForParent = fieldDictionary;
while (originalDictionaryToCheckForParent.TryGetValue("Parent", out var parentObject))
{
if (parentObject == null)
{
break;
}
var indirectCount = 0;
var parentObjectIndirect = parentObject as PdfTypeIndirect;
if (parentObjectIndirect != null)
{
(parentObject, parentObjectIndirect) = GetDirectObjectAndPrimaryIndirect(parentObjectIndirect);
}
PdfTypeDictionary parentDictionary;
try
{
parentDictionary = parentObject as PdfTypeDictionary;
if (parentDictionary == null)
{
#if DEBUG
System.Diagnostics.Debugger.Break();
#endif
break;
}
if (parentDictionary.ObjectNumber == 0)
{
MakeIndirect(parentDictionary, destinationDocumentIndirectCache, destinationDocumentIndirectList);
AddKidsToNewParent(rootFieldDictionary, destinationDocumentIndirectCache, destinationDocumentIndirectList, parentDictionary);
rootFieldDictionary = parentDictionary;
originalDictionaryToCheckForParent = parentDictionary;
continue;
}
}
catch(Exception exc2)
{
#if DEBUG
System.Diagnostics.Debugger.Break();
#endif
throw;
}
var isParentDictionaryInDestinationDocument = false;
if (parentObjectIndirect == null)
{
#if DEBUG
System.Diagnostics.Debugger.Break();
#endif
parentObjectIndirect = FindIndirect(destinationDocumentIndirectCache, destinationDocumentIndirectList, parentDictionary);
isParentDictionaryInDestinationDocument = parentObjectIndirect != null;
}
else
{
isParentDictionaryInDestinationDocument = parentObjectIndirect.List == destinationDocumentIndirectList
|| parentObjectIndirect.List.Handle == destinationDocumentIndirectList.Handle;
}
if (isParentDictionaryInDestinationDocument)
{
AddKidsToNewParent(rootFieldDictionary, destinationDocumentIndirectCache, destinationDocumentIndirectList, parentDictionary);
rootFieldDictionary = parentDictionary;
originalDictionaryToCheckForParent = parentDictionary;
continue;
}
var clonedDictionaryResult = CopyObjectToDocument(
destinationDocumentCopiedObjects,
destinationDocumentIndirectCache,
destinationDocumentIndirectList,
COPY_PARENT_EXCLUDED_KEYS,
false,
sourceDocumentIndirectCache,
sourceDocumentIndirectList,
parentObject
);
var clonedDictionaryIndirect = clonedDictionaryResult as PdfTypeIndirect;
if (clonedDictionaryIndirect == null)
{
clonedDictionaryIndirect = FindIndirect(destinationDocumentIndirectCache, destinationDocumentIndirectList, clonedDictionaryResult);
}
try
{
// Go through the children and change their 'Parent' to point to the new parent
var clonedDictionary = clonedDictionaryIndirect.Direct.As<PdfTypeDictionary>();
AddKidsToNewParent(rootFieldDictionary, destinationDocumentIndirectCache, destinationDocumentIndirectList, clonedDictionary);
rootFieldDictionary = clonedDictionary;
originalDictionaryToCheckForParent = parentDictionary;
}
catch (Exception exc1)
{
#if DEBUG
System.Diagnostics.Debugger.Break();
#endif
throw;
}
}
return rootFieldDictionary;
}
private static PdfTypeIndirect FindIndirect(
IDictionary<int, PdfTypeIndirect> indirectCache,
PdfIndirectList indirectList,
PdfTypeBase objectToLookFor
)
{
if (objectToLookFor == null)
{
return null;
}
while (objectToLookFor is PdfTypeIndirect indirect)
{
if (indirect.Direct == null)
{
return null;
}
objectToLookFor = indirect.Direct;
}
var objectNumberToFind = objectToLookFor.ObjectNumber;
if (objectNumberToFind == 0)
{
return null;
}
if (indirectCache.TryGetValue(objectToLookFor.ObjectNumber, out var cachedIndirect))
{
return (cachedIndirect.Direct == objectToLookFor || cachedIndirect.Direct.Handle == objectToLookFor.Handle)
? cachedIndirect
: null; // the cache entry for the object number doesn't match this object, must be from a different document's indirect list
}
foreach (var item in indirectList)
{
if (item.ObjectNumber != objectNumberToFind)
{
continue;
}
if (item == objectToLookFor || item.Handle == objectToLookFor.Handle)
{
// At this point we know the object is in the indirect list so we can create an indirect, cache it, and return it
var newIndirect = PdfTypeIndirect.Create(indirectList, objectToLookFor.ObjectNumber);
indirectCache[objectToLookFor.ObjectNumber] = newIndirect;
return newIndirect;
}
else
{
// if the object number is the same but the object's are different, the object to find is from a different document
return null;
}
}
// No objects in the list match the object number, it must be from a different document
return null;
}
internal static PdfTypeBase GetDirectObject(PdfTypeBase objectToCheck)
{
if (objectToCheck is PdfTypeIndirect indirect)
{
var (directObject, _) = GetDirectObjectAndPrimaryIndirect(indirect);
return directObject;
}
return objectToCheck;
}
internal static (PdfTypeBase, PdfTypeIndirect) GetDirectObjectAndPrimaryIndirect(PdfTypeIndirect indirect)
{
var primaryIndirect = indirect;
var directObject = indirect?.Direct;
var indirectChainCount = 1;
while (directObject is PdfTypeIndirect nextIndirect)
{
++indirectChainCount;
if (indirectChainCount > MAX_INDIRECT_CHAIN_COUNT)
{
throw new InvalidOperationException($"Indirect chain has exceed the {nameof(MAX_INDIRECT_CHAIN_COUNT)}");
}
directObject = nextIndirect.Direct;
if (directObject == null)
{
return (null, nextIndirect);
}
primaryIndirect = nextIndirect;
}
return (directObject, primaryIndirect);
}
internal static void ImportAcrobatForm(
PdfDocument destinationDocument,
PdfDocument sourceDocument
)
{
if (!sourceDocument.Root.ContainsKey("AcroForm"))
{
return;
}
var destinationDocumentCopiedObjects = new Dictionary<int, PdfTypeIndirect>();
var destinationDocumentIndirectCache = new Dictionary<int, PdfTypeIndirect>();
var destinationDocumentIndirectList = PdfIndirectList.FromPdfDocument(destinationDocument);
var sourceDocumentIndirectCache = new Dictionary<int, PdfTypeIndirect>();
var sourceDocumentIndirectList = PdfIndirectList.FromPdfDocument(sourceDocument);
var clonedAcrobatFormResult = CopyObjectToDocument(
destinationDocumentCopiedObjects,
destinationDocumentIndirectCache,
destinationDocumentIndirectList,
COPY_ACROBAT_FORM_EXCLUDED_KEYS,
false,
sourceDocumentIndirectCache,
sourceDocumentIndirectList,
sourceDocument.Root["AcroForm"].As<PdfTypeDictionary>()
);
var clonedAcrobatFormDictionary = clonedAcrobatFormResult.As<PdfTypeDictionary>();
destinationDocumentIndirectList.Add(clonedAcrobatFormDictionary);
destinationDocument.Root.SetIndirectAt("AcroForm", destinationDocumentIndirectList, clonedAcrobatFormDictionary);
PdfTypeArray clonedAcrobatFormFieldsArray = null;
if (clonedAcrobatFormDictionary.ContainsKey("Fields"))
{
clonedAcrobatFormFieldsArray = clonedAcrobatFormDictionary["Fields"].As<PdfTypeArray>();
clonedAcrobatFormFieldsArray.Clear();
}
else
{
clonedAcrobatFormFieldsArray = PdfTypeArray.Create();
clonedAcrobatFormDictionary["Fields"] = clonedAcrobatFormFieldsArray;
}
foreach (var destinationPage in destinationDocument.Pages)
{
if (destinationPage.Annots == null)
{
continue;
}
foreach (var pageAnnotation in destinationPage.Annots)
{
if (pageAnnotation is PdfWidgetAnnotation)
{
var fieldDictionaryToAdd = CopyParentFieldsToDocumentIfNeeded(
destinationDocumentCopiedObjects,
destinationDocumentIndirectCache,
destinationDocumentIndirectList,
pageAnnotation.Dictionary,
sourceDocumentIndirectCache,
sourceDocumentIndirectList
);
if (fieldDictionaryToAdd.ObjectNumber < 1)
{
#if DEBUG
System.Diagnostics.Debugger.Break();
#endif
continue;
}
var isFieldInArray = false;
foreach (var nextField in clonedAcrobatFormFieldsArray)
{
if (nextField is PdfTypeIndirect nextFieldIndirect)
{
if (nextFieldIndirect.Number == fieldDictionaryToAdd.ObjectNumber)
{
isFieldInArray = true;
break;
}
continue;
}
if (nextField.ObjectNumber == fieldDictionaryToAdd.ObjectNumber)
{
isFieldInArray = true;
break;
}
}
if (!isFieldInArray)
{
var fieldDictionaryIndirect = FindIndirect(destinationDocumentIndirectCache, destinationDocumentIndirectList, fieldDictionaryToAdd);
if (fieldDictionaryIndirect == null)
{
continue;
}
if (fieldDictionaryIndirect.Direct != fieldDictionaryToAdd && fieldDictionaryIndirect.Direct.Handle != fieldDictionaryToAdd.Handle)
{
continue;
}
clonedAcrobatFormFieldsArray.Add(fieldDictionaryIndirect);
}
}
}
}
//foreach (var item in clonedAcrobatFormFieldsArray)
//{
// if (item == null || !(item is PdfTypeIndirect itemIndirect) || itemIndirect.Direct == null || !(itemIndirect.Direct is PdfTypeDictionary itemDictionary))
// {
// continue;
// }
//}
ValidateObjectsAreAllFromDestinationDocument(destinationDocumentIndirectCache, destinationDocumentIndirectList, destinationDocument.Root);
}
private static bool IsCatalog(PdfTypeBase objectToCheck)
{
return objectToCheck != null
&& objectToCheck is PdfTypeDictionary pdfDictionary
&& pdfDictionary.TryGetValue("Type", out var pdfDictionaryType)
&& pdfDictionaryType is PdfTypeString pdfDictionaryTypeString
&& pdfDictionaryTypeString.AnsiString == "Catalog";
}
private static bool IsInIndirectList(
IDictionary<int, PdfTypeIndirect> indirectCache,
PdfIndirectList indirectList,
PdfTypeBase objectToLookFor
)
{
var objectNumberToFind = objectToLookFor?.ObjectNumber ?? 0;
if (objectNumberToFind == 0)
{
return false;
}
if (indirectCache.TryGetValue(objectNumberToFind, out var cachedIndirect))
{
return cachedIndirect.Direct == objectToLookFor || cachedIndirect.Direct.Handle == objectToLookFor.Handle;
}
// We'll assume the caller is passing in a direct object
foreach (var item in indirectList)
{
if (item.ObjectNumber != objectNumberToFind)
{
continue;
}
if (item == objectToLookFor || item.Handle == objectToLookFor.Handle)
{
// At this point we know the object is in the indirect list so we can create an indirect, cache it, and return it
return true;
}
else
{
// if the object number is the same but the object's are different, the object to find is from a different document
return false;
}
}
return false;
}
private static PdfTypeIndirect MakeIndirect(
PdfTypeBase directObject,
IDictionary<int, PdfTypeIndirect> indirectCache,
PdfIndirectList indirectList
)
{
if (directObject is PdfTypeIndirect indirect)
{
if (indirect.List == indirectList || indirect.List.Handle == indirectList.Handle)
{
// Already is an indirect, just return it
return indirect;
}
else
{
throw new InvalidOperationException("Unable to create an indirect reference because the object is already an indirect ref from a different document's indirect list");
}
}
var foundIndirect = FindIndirect(indirectCache, indirectList, directObject);
if (foundIndirect != null)
{
return foundIndirect;
}
if (directObject.ObjectNumber > 0)
{
// This object already has an object number meaning it's from a different document and we can't make an indirect reference for it
throw new InvalidOperationException("Unable to create an indirect reference because the document already has an object number, it must be from a different document's indirect list");
}
indirectList.Add(directObject);
if (directObject.ObjectNumber < 1)
{
throw new InvalidOperationException("Failed to add the directObject to the indirectList, the ObjectNumber is less than 1");
}
var newIndirect = PdfTypeIndirect.Create(indirectList, directObject.ObjectNumber);
indirectCache.Add(directObject.ObjectNumber, newIndirect);
return newIndirect;
}
private static void ValidateObjectsAreAllFromDestinationDocument(
IDictionary<int, PdfTypeIndirect> destinationDocumentIndirectCache,
PdfIndirectList destinationDocumentIndirectList,
PdfTypeBase root
)
{
var visitedHandles = new List<IntPtr>();
//Dictionary<IntPtr, PdfTypeBase> visited = new Dictionary<IntPtr, PdfTypeBase>();
Queue<PdfTypeBase> nodesToInspect = new Queue<PdfTypeBase>();
nodesToInspect.Enqueue(root);
var traversalCount = 0;
while (nodesToInspect.Count > 0)
{
++traversalCount;
if (traversalCount > 100000)
{
break;
}
var nextItem = nodesToInspect.Dequeue();
if (nextItem == null || visitedHandles.Contains(nextItem.Handle))
{
continue;
}
visitedHandles.Add(nextItem.Handle);
var nextDirect = nextItem;
while (nextDirect is PdfTypeIndirect nextIndirect)
{
if (nextIndirect.List != destinationDocumentIndirectList && nextIndirect.List.Handle != destinationDocumentIndirectList.Handle)
{
throw new InvalidOperationException("Indirect item from source document found in destination document traversal");
}
if (nextIndirect.Direct == null)
{
break;
}
nextDirect = nextIndirect.Direct;
}
if (nextDirect == null)
{
continue;
}
if (nextDirect.ObjectNumber > 0 && FindIndirect(destinationDocumentIndirectCache, destinationDocumentIndirectList, nextDirect) == null)
{
throw new InvalidOperationException("Direct object with object number not found in destination document indirect list, probably from source document");
}
if (nextDirect is PdfTypeDictionary nextDictionary)
{
foreach (var itemToCheck in nextDictionary.Values)
{
var directItemToCheck = itemToCheck;
while (directItemToCheck is PdfTypeIndirect indirectToCheck)
{
if (indirectToCheck.List != destinationDocumentIndirectList && indirectToCheck.List.Handle != destinationDocumentIndirectList.Handle)
{
throw new InvalidOperationException("Indirect item from source document found in destination document traversal");
}
if (indirectToCheck.Direct == null)
{
break;
}
directItemToCheck = indirectToCheck.Direct;
}
if (directItemToCheck != null && !visitedHandles.Contains(directItemToCheck.Handle))
{
nodesToInspect.Enqueue(directItemToCheck);
}
}
}
if (nextDirect is PdfTypeArray nextArray)
{
foreach (var itemToCheck in nextArray)
{
var directItemToCheck = itemToCheck;
while (directItemToCheck is PdfTypeIndirect indirectToCheck)
{
if (indirectToCheck.List != destinationDocumentIndirectList && indirectToCheck.List.Handle != destinationDocumentIndirectList.Handle)
{
throw new InvalidOperationException("Indirect item from source document found in destination document traversal");
}
if (indirectToCheck.Direct == null)
{
break;
}
directItemToCheck = indirectToCheck.Direct;
}
if (directItemToCheck != null && !visitedHandles.Contains(directItemToCheck.Handle))
{
nodesToInspect.Enqueue(directItemToCheck);
}
}
}
}
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment