Skip to content

Latest commit

 

History

History
297 lines (253 loc) · 12.5 KB

BONEYARD.md

File metadata and controls

297 lines (253 loc) · 12.5 KB

Code Removed from the Project, but still available for reference

Function to add XMP metadata to specific pages

    /// <summary>
    /// Set the Xmp metadata to the starting page for each message
    /// <see cref="https://stackoverflow.com/questions/28427100/how-do-i-add-xmp-metadata-to-each-page-of-an-existing-pdf-using-itextsharp"/>
    /// </summary>
    /// <param name="dparts">The root DPart node containing all the metadata for the folders and messages in the file</param>
    /// <exception cref="Exception"></exception>
    public void AddXmpToPages(DPartInternalNode dparts)
    {
        foreach (var dpartLeaf in dparts.GetAllLeafNodesAsFlattenedList())
        {
            var page = GetPageDataForNamedDestination(dpartLeaf.StartNamedDestination);
            byte[] meta = Encoding.Default.GetBytes(dpartLeaf.DpmXmpString);

            if (page != null)
            {
                PrStream stream = (PrStream)page.PageDictionary.GetAsStream(PdfName.Metadata);
                if (stream == null)
                {
                    // We add the XMP bytes to the writer
                    PdfIndirectObject ind = _stamper.Writer.AddToBody(new PdfStream(meta));
                    // We add a reference to the XMP bytes to the page dictionary
                    page.PageDictionary.Put(PdfName.Metadata, ind.IndirectReference);
                }
                else
                {
                    _logger.LogWarning("Page for message (Named Destination: {NamedDestination}) already had metadata; it was replaced.", dpartLeaf.StartNamedDestination);
                    byte[] xmpBytes = PdfReader.GetStreamBytes(stream);
                    stream.SetData(meta);
                }

            }
            else
            {
                throw new Exception($"Page for message (Named Destination: {dpartLeaf.StartNamedDestination}) was not found.");
            }
        }
    }

Function to add set the document level XMP metdata

    /// <summary>
    /// Set the document level XMP metadata to the given string
    /// </summary>
    /// <param name="xmp"></param>
    public void SetDocumentXmp(string xmp)
    {
        var xmpByt = Encoding.Default.GetBytes(xmp);
        _stamper.XmpMetadata = xmpByt;
    }

Function to remove empty tables

    private static void RemoveEmptyTables(HtmlDocument hdoc, ref List<(LogLevel level, string message)> messages, bool ignoreHtmlIssues)
    {
        var emptyTableNodes = hdoc.DocumentNode.QuerySelectorAll("table");

        List<HtmlNode> toRemove = new();
        foreach (var node in emptyTableNodes)
        {
            if (string.IsNullOrWhiteSpace(node.InnerHtml))
            {
                toRemove.Add(node);
            }
        }

        toRemove.ForEach(h => h.Remove());

    }

Function to automatically retry opening a file stream with an exponential backoff

    /// <summary>
    /// Try to open a file stream with multiple retries with exponential backoff in case it is locked by some other process
    /// </summary>
    /// <param name="fullPath"></param>
    /// <param name="mode"></param>
    /// <param name="access"></param>
    /// <param name="share"></param>
    /// <returns></returns>
    public static FileStream? WaitForFile(string fullPath, FileMode mode, FileAccess access, FileShare share)
    {
        for (int numTries = 0; numTries < 10; numTries++)
        {
            FileStream? fs = null;
            try
            {
                fs = new FileStream(fullPath, mode, access, share);
                return fs;
            }
            catch (IOException)
            {
                if (fs != null)
                {
                    fs.Dispose();
                }
                Thread.Sleep(50 * 2 ^ numTries); //each loop is given a bit longer
            }
        }

        return null;
    }

Function to inline css styles using the ExCSS parser

    const int CSS_PARSER_TIMEOUT = 700; //milliseconds
    const int CSS_PARSER_SEVERITY_RUNTIME = 0;


    /// <summary>
    /// Convert any embedded stylesheets into inline styles
    /// </summary>
    /// <param name="htmlNode"></param>
    /// <param name="messages"></param>
    /// <param name="ignoreHtmlIssues"></param>
    /// <returns></returns>
    private static void ConvertToInlineCssUsingExCss(HtmlNode htmlNode, ref List<(LogLevel level, string message)> messages, bool ignoreHtmlIssues)
    {
        //get all the style elements
        var styles = htmlNode.SelectNodes("//style");

        string allStyles = "";
        if (styles != null)
        {
            foreach (var style in styles)
            {
                allStyles += style.InnerText + " ;\r\n/* NEXT STYLE */\r\n";
            }

            //minify the css; this also checks for errors
            var uResult = Uglify.Css(allStyles);
            foreach (var err in uResult.Errors)
            {
                messages.Add((err.Severity == CSS_PARSER_SEVERITY_RUNTIME ? LogLevel.Warning : LogLevel.Information, $"CSS error: {err.Message}"));
            }
            if (uResult.Errors.Any(e => e.Severity == CSS_PARSER_SEVERITY_RUNTIME)) //the most severe run-time invoking error
            {
                // do not try to continue 
                return;
            }

            var cssParser = new StylesheetParser(); //ExCSS parser
            //The parser may get stuck in an endless loop:  https://github.com/TylerBrinks/ExCSS/issues/138
            var sSheetTask = Task.Run(() => cssParser.Parse(allStyles));
            var done = sSheetTask.Wait(CSS_PARSER_TIMEOUT); //milliseconds
            if (!done)
            {
                messages.Add((LogLevel.Critical, "The ExCSS parser is possibly stuck in an endless loop or a deadlock"));// do not try to continue
                throw new Exception("The ExCSS parser is possibly stuck in an endless loop; best to just abort");
            }

            foreach (var rule in sSheetTask.Result.StyleRules.OrderByDescending(sr => sr.Selector.Specificity))
            {
                //TODO: Add support for ListSelectors, so that the separate specificity of its selectors can be determined.
                //      The ExCss library doesn't seem to allow access to the individual selectors of a SelectorList
                //      I need to do a pull-request for the ExCSS change that I would like
                //TODO: Add support for the shorthand property "all"
                //TODO: Add support for the !important modifier.

                var selectorText = rule.Selector.Text;
                var styleText = rule.Style.CssText;
                var specificity = rule.Selector.Specificity;

                if (string.IsNullOrWhiteSpace(styleText))
                    continue; //skip this rule andf move to the next

                IEnumerable<HtmlNode>? nodes = null;
                try
                {
                    nodes = htmlNode.QuerySelectorAll(selectorText);
                }
                catch (Exception ex)
                {
                    messages.Add((LogLevel.Debug, $"Parsing selector '{selectorText}'; {ex.Message}"));
                    nodes = null;
                }

                if (nodes != null)
                {
                    foreach (var node in nodes)
                    {
                        var styleAttr = node.GetAttributes("style").SingleOrDefault();
                        if (styleAttr == null)
                        {
                            var newStyle = cssParser.Parse($"dummy {{{styleText}}}").StyleRules.Single().Style;
                            var newStyleText = string.Join("; ", newStyle.Select(p => p.Name + ":" + p.Value)) + "; ";

                            node.Attributes.Add("style", newStyleText);
                            messages.Add((LogLevel.Debug, $"Node: {node.XPath}, Inlining style:  {selectorText} {{{styleText}}}, new style attribute"));
                        }
                        else
                        {
                            var currentStyle = cssParser.Parse($"dummy {{{styleAttr.Value}}}").StyleRules.Single().Style;

                            var currentStyleText = string.Join("; ", currentStyle.Select(p => p.Name + ":" + p.Value)) + "; ";

                            foreach (var newProperty in rule.Style)
                            {
                                //since we are iterating rules from most specific to the least specific, we can't overwrite an existing property because it originated from a more specific rule
                                if (!currentStyle.Contains(newProperty, new ExCssPropertyComparer()))
                                {
                                    currentStyleText += newProperty.Name + ":" + newProperty.Value + "; ";
                                    messages.Add((LogLevel.Debug, $"Node: {node.XPath}, Inlining style: {selectorText} {{{styleText}}}, modify style attribute"));
                                }
                            }

                            styleAttr.Value = currentStyleText.Trim().Trim(';').Trim(); //get rid of any leading or trainling semi-colons and whitespace

                        }
                    }
                }
            }
        }
    }

Function to return a page given a named destination

    private PdfPage? GetPageWithNamedDestination(PdfDictionary dests, string name)
    {

        PdfPage? ret = null;

        PdfArray destNames = dests.GetAsArray(PdfName.Names);
        PdfArray destKids = dests.GetAsArray(PdfName.Kids);
        PdfArray destLimits = dests.GetAsArray(PdfName.Limits);

        string lowerLimit;
        string upperLimit;
        if (destLimits != null)
        {
            lowerLimit = destLimits.GetAsString(0).ToString();
            upperLimit = destLimits.GetAsString(1).ToString();
            int compLower = string.Compare(name, lowerLimit, StringComparison.Ordinal);
            int compUpper = string.Compare(name, upperLimit, StringComparison.Ordinal);
            if (compLower < 0 || compUpper > 0)
                return null;
        }

        if (destNames != null)
        {
            for (int i = 0; i < destNames.ArrayList.Count; i += 2)
            {
                var key = destNames.GetAsString(i).ToString();
                var value = destNames.GetDirectObject(i + 1);
                if (string.Compare(name, key, StringComparison.Ordinal) == 0)
                {
                    PdfArray destination;
                    if (value.IsDictionary())
                    {
                        destination= (PdfArray)((PdfDictionary)value).Get(PdfName.D);
                    }
                    else if (value.IsArray())
                    {
                        destination = (PdfArray)value;
                    }
                    else
                    {
                        throw new Exception("Invalid destination name value");
                    }

                    //Get the page from the destination
                    ret = (PdfPage)destination.GetDirectObject(0);
                }

            }
        }
        else if (destKids != null)
        {
            for (int i = 0; i < destKids.Size; i += 1)
            {
                PdfObject nodes = (PdfDictionary)destKids.GetDirectObject(i);
                if (nodes.IsDictionary())
                {
                    ret = GetPageWithNamedDestination((PdfDictionary)nodes, name);
                    if (ret != null)
                        break;
                }
                else
                {
                    throw new Exception("Invalid kids");
                }
            }
        }
        else
        {
            throw new Exception("Invalid name tree");
        }


        return ret;
    }