Compare commits

..

17 Commits

Author SHA1 Message Date
Eliot Jones
e8401b87cf version 0.1.0 2020-01-13 10:46:47 +00:00
Eliot Jones
efc258b0f0 use tokenscanner when converting array to rectangle
an arrray of 4 items representing a rectangle may define its values as indirect references. when converting to a rectangle we pass a pdf token scanner to resolve any indirect references.
2020-01-13 10:20:08 +00:00
BobLd
47672d3f90 Make TextBlock.SetReadingOrder(int) public 2020-01-13 09:25:57 +00:00
BobLd
fd014cfaa7 Add files via upload 2020-01-12 11:15:58 +00:00
BobLd
e8216b29c5 Add reading order in PageXml export 2020-01-12 11:15:58 +00:00
BobLd
e7417be75a ReadingOrderDetector and tidying DLA project 2020-01-11 11:18:11 +00:00
Eliot Jones
b4d917dcdc merge pull request #122 from uglytoad/marked-content
marked content
2020-01-10 17:07:21 +00:00
Eliot Jones
41cc7abd1b prevent negative point size for fonts 2020-01-10 14:40:28 +00:00
Eliot Jones
17b7cf2f61 load images eagerly for marked content
when a marked content region contains an image we load it eagerly since we won't have access to the necessary classes at evaluation time. we also default image colorspace to the active graphics state colorspace if the dictionary doesn't contain a valid entry.
2020-01-10 13:52:21 +00:00
Eliot Jones
2a579afd4d add missing doc comments for operation context marked content 2020-01-09 15:35:55 +00:00
Eliot Jones
d011f37316 merge master 2020-01-09 15:32:10 +00:00
Eliot Jones
43574097f1 rename marked content elements and use factory
since the properties in marked content may be indirect references or belong to the page resources array, the value should be calculated during content processing. this change tidies up the marked content classes so they do not expose mutable data and uses the pdf token scanner overloads to load dictionary data.
2020-01-09 15:30:16 +00:00
BobLd
097692f1cb Move ArtifactType inside PdfArtifactMarkedContent 2020-01-09 11:24:32 +00:00
BobLd
7be36fdc58 Update PublicApiScannerTests 2 2020-01-08 11:07:27 +00:00
BobLd
4b929482cc Update PublicApiScannerTests 2020-01-08 10:46:49 +00:00
BobLd
49d836c5cb Add description to GetMarkedContents() 2020-01-08 10:36:58 +00:00
BobLd
84bab1b627 Add basic marked content extraction capabilities 2020-01-08 10:34:01 +00:00
45 changed files with 1274 additions and 100 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 271 KiB

View File

@@ -1,13 +1,14 @@
namespace UglyToad.PdfPig.DocumentLayoutAnalysis
{
using Content;
using Geometry;
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Linq;
using System.Text.RegularExpressions;
using System.Threading.Tasks;
using Content;
using Geometry;
using UglyToad.PdfPig.DocumentLayoutAnalysis.PageSegmenter;
using Util;
/// <summary>
@@ -15,7 +16,7 @@
/// <para>Decoration blocks are blocks that contains information such as author names, publication titles, page numbers, etc.
/// They are printed repeatedly at the border of each page, usually placed inside headers or footers, but sometimes also at the
/// left or right edge of the page.</para>
/// <para>See section 5.1 of 'Unsupervised document structure analysis of digital scientific articles' by S. Klampfl, M. Granitzer, K. Jack, R. Kern.</para>
/// <para>See section 4.1 of 'Unsupervised document structure analysis of digital scientific articles' by S. Klampfl, M. Granitzer, K. Jack, R. Kern.</para>
/// </summary>
public static class DecorationTextBlockClassifier
{

View File

@@ -1,10 +1,9 @@
namespace UglyToad.PdfPig.DocumentLayoutAnalysis
{
using Core;
using System;
using System.Collections.Generic;
using System.Linq;
using Core;
using Geometry;
/// <summary>
/// Contains helpful tools for distance measures.

View File

@@ -1,15 +1,15 @@
namespace UglyToad.PdfPig.DocumentLayoutAnalysis.Export
{
using Alto;
using Content;
using Core;
using DocumentLayoutAnalysis;
using System;
using System.Globalization;
using System.Linq;
using System.Xml;
using System.Xml.Serialization;
using Alto;
using Content;
using Core;
using DocumentLayoutAnalysis;
using Geometry;
using UglyToad.PdfPig.DocumentLayoutAnalysis.PageSegmenter;
using Util;
/// <inheritdoc />

View File

@@ -1,11 +1,11 @@
namespace UglyToad.PdfPig.DocumentLayoutAnalysis.Export
{
using System;
using System.Linq;
using Content;
using Core;
using DocumentLayoutAnalysis;
using Geometry;
using System;
using System.Linq;
using UglyToad.PdfPig.DocumentLayoutAnalysis.PageSegmenter;
using Util;
/// <summary>

View File

@@ -1,16 +1,17 @@
namespace UglyToad.PdfPig.DocumentLayoutAnalysis.Export
{
using Content;
using Core;
using DocumentLayoutAnalysis;
using Graphics.Colors;
using PAGE;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Xml;
using System.Xml.Serialization;
using Content;
using Core;
using DocumentLayoutAnalysis;
using Geometry;
using Graphics.Colors;
using PAGE;
using UglyToad.PdfPig.DocumentLayoutAnalysis.PageSegmenter;
using UglyToad.PdfPig.DocumentLayoutAnalysis.ReadingOrderDetector;
using Util;
/// <summary>
@@ -21,6 +22,7 @@
{
private readonly IPageSegmenter pageSegmenter;
private readonly IWordExtractor wordExtractor;
private readonly IReadingOrderDetector readingOrderDetector;
private readonly double scale;
private readonly string indentChar;
@@ -29,6 +31,9 @@
private int wordCount;
private int glyphCount;
private int regionCount;
private int groupOrderCount;
private List<PageXmlDocument.PageXmlRegionRefIndexed> orderedRegions;
/// <summary>
/// PAGE-XML 2019-07-15 (XML) text exporter.
@@ -36,14 +41,16 @@
/// </summary>
/// <param name="wordExtractor"></param>
/// <param name="pageSegmenter"></param>
/// <param name="readingOrderDetector"></param>
/// <param name="scale"></param>
/// <param name="indent">Indent character.</param>
public PageXmlTextExporter(IWordExtractor wordExtractor, IPageSegmenter pageSegmenter, double scale = 1.0, string indent = "\t")
public PageXmlTextExporter(IWordExtractor wordExtractor, IPageSegmenter pageSegmenter, IReadingOrderDetector readingOrderDetector = null, double scale = 1.0, string indent = "\t")
{
this.wordExtractor = wordExtractor;
this.pageSegmenter = pageSegmenter;
this.readingOrderDetector = readingOrderDetector;
this.scale = scale;
indentChar = indent;
this.indentChar = indent;
}
/// <summary>
@@ -72,6 +79,13 @@
/// <param name="includePaths">Draw <see cref="PdfPath"/>s present in the page.</param>
public string Get(Page page, bool includePaths)
{
lineCount = 0;
wordCount = 0;
glyphCount = 0;
regionCount = 0;
groupOrderCount = 0;
orderedRegions = new List<PageXmlDocument.PageXmlRegionRefIndexed>();
PageXmlDocument pageXmlDocument = new PageXmlDocument()
{
Metadata = new PageXmlDocument.PageXmlMetadata()
@@ -145,7 +159,25 @@
if (words.Count > 0)
{
var blocks = pageSegmenter.GetBlocks(words);
if (readingOrderDetector != null)
{
blocks = readingOrderDetector.Get(blocks).ToList();
}
regions.AddRange(blocks.Select(b => ToPageXmlTextRegion(b, page.Height)));
if (orderedRegions.Any())
{
pageXmlPage.ReadingOrder = new PageXmlDocument.PageXmlReadingOrder()
{
Item = new PageXmlDocument.PageXmlOrderedGroup()
{
Items = orderedRegions.ToArray(),
Id = "g" + groupOrderCount++
}
};
}
}
var images = page.GetImages().ToList();
@@ -196,13 +228,24 @@
private PageXmlDocument.PageXmlTextRegion ToPageXmlTextRegion(TextBlock textBlock, double height)
{
regionCount++;
string regionId = "r" + regionCount;
if (readingOrderDetector != null && textBlock.ReadingOrder > -1)
{
orderedRegions.Add(new PageXmlDocument.PageXmlRegionRefIndexed()
{
RegionRef = regionId,
Index = textBlock.ReadingOrder
});
}
return new PageXmlDocument.PageXmlTextRegion()
{
Coords = ToCoords(textBlock.BoundingBox, height),
Type = PageXmlDocument.PageXmlTextSimpleType.Paragraph,
TextLines = textBlock.TextLines.Select(l => ToPageXmlTextLine(l, height)).ToArray(),
TextEquivs = new[] { new PageXmlDocument.PageXmlTextEquiv() { Unicode = textBlock.Text } },
Id = "r" + regionCount
Id = regionId
};
}

View File

@@ -1,9 +1,9 @@
namespace UglyToad.PdfPig.DocumentLayoutAnalysis
namespace UglyToad.PdfPig.DocumentLayoutAnalysis.PageSegmenter
{
using System.Collections.Generic;
using System.Linq;
using Content;
using Core;
using System.Collections.Generic;
using System.Linq;
/// <summary>
/// Default Page Segmenter. All words are included in one block.

View File

@@ -1,13 +1,13 @@
namespace UglyToad.PdfPig.DocumentLayoutAnalysis
namespace UglyToad.PdfPig.DocumentLayoutAnalysis.PageSegmenter
{
using Content;
using Core;
using Geometry;
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Geometry;
using Content;
using Core;
/// <inheritdoc />
/// <summary>

View File

@@ -1,7 +1,7 @@
namespace UglyToad.PdfPig.DocumentLayoutAnalysis
namespace UglyToad.PdfPig.DocumentLayoutAnalysis.PageSegmenter
{
using System.Collections.Generic;
using Content;
using System.Collections.Generic;
/// <summary>
/// Page segmentation divides a page into areas, each consisting of a layout structure (blocks, lines, etc.).

View File

@@ -1,10 +1,10 @@
namespace UglyToad.PdfPig.DocumentLayoutAnalysis
namespace UglyToad.PdfPig.DocumentLayoutAnalysis.PageSegmenter
{
using Content;
using Core;
using System;
using System.Collections.Generic;
using System.Linq;
using Content;
using Core;
/// <summary>
/// The recursive X-Y cut is a top-down page segmentation technique that decomposes a document

View File

@@ -1,10 +1,10 @@
namespace UglyToad.PdfPig.DocumentLayoutAnalysis
namespace UglyToad.PdfPig.DocumentLayoutAnalysis.PageSegmenter
{
using Content;
using Core;
using System;
using System.Collections.Generic;
using System.Linq;
using Content;
using Core;
/// <summary>
/// A Leaf node used in the <see cref="RecursiveXYCut"/> algorithm, i.e. a block.

View File

@@ -1,8 +1,8 @@
namespace UglyToad.PdfPig.DocumentLayoutAnalysis
namespace UglyToad.PdfPig.DocumentLayoutAnalysis.PageSegmenter
{
using Core;
using System.Collections.Generic;
using System.Linq;
using Core;
/// <summary>
/// A Node used in the <see cref="RecursiveXYCut"/> algorithm.

View File

@@ -0,0 +1,25 @@
namespace UglyToad.PdfPig.DocumentLayoutAnalysis.ReadingOrderDetector
{
using System.Collections.Generic;
/// <summary>
/// This detector does nothing, no ordering takes place.
/// </summary>
public class DefaultReadingOrderDetector : IReadingOrderDetector
{
/// <summary>
/// Create an instance of default reading order detector, <see cref="DefaultReadingOrderDetector"/>.
/// <para>This detector does nothing, no ordering takes place.</para>
/// </summary>
public static DefaultReadingOrderDetector Instance { get; } = new DefaultReadingOrderDetector();
/// <summary>
/// Gets the blocks in reading order and sets the <see cref="TextBlock.ReadingOrder"/>.
/// </summary>
/// <param name="textBlocks">The <see cref="TextBlock"/>s to order.</param>
public IEnumerable<TextBlock> Get(IReadOnlyList<TextBlock> textBlocks)
{
return textBlocks;
}
}
}

View File

@@ -0,0 +1,17 @@
namespace UglyToad.PdfPig.DocumentLayoutAnalysis.ReadingOrderDetector
{
using System.Collections.Generic;
/// <summary>
/// Reading order detector determines the page's blocks reading order.
/// <para>Note: Make sure you use <see cref="TextBlock.SetReadingOrder(int)"/> to set each <see cref="TextBlock"/> reading order when implementing <see cref="IReadingOrderDetector.Get(IReadOnlyList{TextBlock})"/>.</para>
/// </summary>
public interface IReadingOrderDetector
{
/// <summary>
/// Gets the blocks in reading order and sets the <see cref="TextBlock.ReadingOrder"/>.
/// </summary>
/// <param name="textBlocks">The <see cref="TextBlock"/>s to order.</param>
IEnumerable<TextBlock> Get(IReadOnlyList<TextBlock> textBlocks);
}
}

View File

@@ -0,0 +1,37 @@
namespace UglyToad.PdfPig.DocumentLayoutAnalysis.ReadingOrderDetector
{
using System.Collections.Generic;
using System.Linq;
/// <summary>
/// Algorithm that retrieve the blocks' reading order using rendering order (TextSequence).
/// </summary>
public class RenderingReadingOrderDetector : IReadingOrderDetector
{
/// <summary>
/// Create an instance of rendering reading order detector, <see cref="RenderingReadingOrderDetector"/>.
/// <para>This detector uses the rendering order (TextSequence).</para>
/// </summary>
public static RenderingReadingOrderDetector Instance { get; } = new RenderingReadingOrderDetector();
/// <summary>
/// Gets the blocks in reading order and sets the <see cref="TextBlock.ReadingOrder"/>.
/// </summary>
/// <param name="textBlocks">The <see cref="TextBlock"/>s to order.</param>
public IEnumerable<TextBlock> Get(IReadOnlyList<TextBlock> textBlocks)
{
int readingOrder = 0;
foreach (var block in textBlocks.OrderBy(b => AvgTextSequence(b)))
{
block.SetReadingOrder(readingOrder++);
yield return block;
}
}
private double AvgTextSequence(TextBlock textBlock)
{
return textBlock.TextLines.SelectMany(tl => tl.Words).SelectMany(w => w.Letters).Select(l => l.TextSequence).Average();
}
}
}

View File

@@ -0,0 +1,464 @@
namespace UglyToad.PdfPig.DocumentLayoutAnalysis.ReadingOrderDetector
{
using System.Collections.Generic;
using System.Linq;
/// <summary>
/// Algorithm that retrieve the blocks' reading order using both (spatial) Allens interval relations and rendering order (TextSequence).
/// <para>See section 5.1 of 'Unsupervised document structure analysis of digital scientific articles' by S. Klampfl, M. Granitzer, K. Jack, R. Kern and 'Document Understanding for a Broad Class of Documents' by L. Todoran, M. Worring, M. Aiello and C. Monz.</para>
/// </summary>
public class UnsupervisedReadingOrderDetector : IReadingOrderDetector
{
/// <summary>
/// Create an instance of unsupervised reading order detector, <see cref="UnsupervisedReadingOrderDetector"/>.
/// <para>This detector uses the (spatial) Allens interval relations and rendering order (TextSequence).</para>
/// </summary>
public static UnsupervisedReadingOrderDetector Instance { get; } = new UnsupervisedReadingOrderDetector();
private double T;
/// <summary>
/// Algorithm that retrieve the blocks' reading order using both (spatial) Allens interval relations and rendering order.
/// </summary>
/// <param name="T">The tolerance parameter T. If two coordinates are closer than T they are considered equal.
/// This flexibility is necessary because due to the inherent noise in the PDF extraction text blocks in the
/// same column might not be exactly aligned.</param>
public UnsupervisedReadingOrderDetector(double T = 5)
{
this.T = T;
}
/// <summary>
/// Gets the blocks in reading order and sets the <see cref="TextBlock.ReadingOrder"/>.
/// </summary>
/// <param name="textBlocks">The <see cref="TextBlock"/>s to order.</param>
public IEnumerable<TextBlock> Get(IReadOnlyList<TextBlock> textBlocks)
{
int readingOrder = 0;
var graph = BuildGraph(textBlocks, T);
while (graph.Any())
{
var maxCount = graph.Max(kvp => kvp.Value.Count);
var current = graph.Where(kvp => kvp.Value.Count == maxCount).FirstOrDefault();
graph.Remove(current.Key);
int index = current.Key;
foreach (var g in graph)
{
g.Value.Remove(index);
}
var block = textBlocks[index];
block.SetReadingOrder(readingOrder++);
yield return block;
}
}
private Dictionary<int, List<int>> BuildGraph(IReadOnlyList<TextBlock> textBlocks, double T)
{
// We incorporate both relations into a single partial ordering of blocks by specifying a
// directed graph with an edge between every pair of blocks for which at least one of the
// two relations hold.
var graph = new Dictionary<int, List<int>>();
for (int i = 0; i < textBlocks.Count; i++)
{
graph.Add(i, new List<int>());
}
for (int i = 0; i < textBlocks.Count; i++)
{
var a = textBlocks[i];
for (int j = 0; j < textBlocks.Count; j++)
{
if (i == j) continue;
var b = textBlocks[j];
if (GetBeforeInReadingRendering(a, b, T))
{
graph[i].Add(j);
}
}
}
return graph;
}
private bool GetBeforeInReadingRendering(TextBlock a, TextBlock b, double T)
{
return GetBeforeInReadingVertical(a, b, T) || GetBeforeInRendering(a, b);
}
private bool GetBeforeInRendering(TextBlock a, TextBlock b)
{
var avgTextSequenceA = a.TextLines.SelectMany(tl => tl.Words).SelectMany(w => w.Letters).Select(l => l.TextSequence).Average();
var avgTextSequenceB = b.TextLines.SelectMany(tl => tl.Words).SelectMany(w => w.Letters).Select(l => l.TextSequence).Average();
return avgTextSequenceA < avgTextSequenceB;
}
private bool GetBeforeInReading(TextBlock a, TextBlock b, double T)
{
IntervalRelations xRelation = GetIntervalRelationX(a, b, T);
IntervalRelations yRelation = GetIntervalRelationY(a, b, T);
if (xRelation == IntervalRelations.Precedes ||
yRelation == IntervalRelations.Precedes ||
xRelation == IntervalRelations.Meets ||
yRelation == IntervalRelations.Meets ||
xRelation == IntervalRelations.Overlaps ||
yRelation == IntervalRelations.Overlaps)
{
return true;
}
return false;
}
/// <summary>
/// Column-wise: text-blocks are read in columns, from top-to-bottom and from left-to-right.
/// </summary>
/// <param name="a"></param>
/// <param name="b"></param>
/// <param name="T">The tolerance parameter T.</param>
/// <returns></returns>
private bool GetBeforeInReadingVertical(TextBlock a, TextBlock b, double T)
{
IntervalRelations xRelation = GetIntervalRelationX(a, b, T);
IntervalRelations yRelation = GetIntervalRelationY(a, b, T);
if (xRelation == IntervalRelations.Precedes ||
xRelation == IntervalRelations.Meets ||
(xRelation == IntervalRelations.Overlaps && (yRelation == IntervalRelations.Precedes ||
yRelation == IntervalRelations.Meets ||
yRelation == IntervalRelations.Overlaps)) ||
((yRelation == IntervalRelations.Precedes || yRelation == IntervalRelations.Meets || yRelation == IntervalRelations.Overlaps) &&
(xRelation == IntervalRelations.Precedes ||
xRelation == IntervalRelations.Meets ||
xRelation == IntervalRelations.Overlaps ||
xRelation == IntervalRelations.Starts ||
xRelation == IntervalRelations.FinishesI ||
xRelation == IntervalRelations.Equals ||
xRelation == IntervalRelations.During ||
xRelation == IntervalRelations.DuringI ||
xRelation == IntervalRelations.Finishes ||
xRelation == IntervalRelations.StartsI ||
xRelation == IntervalRelations.OverlapsI)))
{
return true;
}
return false;
}
/// <summary>
/// Row-wise: text-blocks are read in rows from left-to-right, top- to-bottom.
/// </summary>
/// <param name="a"></param>
/// <param name="b"></param>
/// <param name="T">The tolerance parameter T.</param>
/// <returns></returns>
private bool GetBeforeInReadingHorizontal(TextBlock a, TextBlock b, double T)
{
IntervalRelations xRelation = GetIntervalRelationX(a, b, T);
IntervalRelations yRelation = GetIntervalRelationY(a, b, T);
if (yRelation == IntervalRelations.Precedes ||
yRelation == IntervalRelations.Meets ||
(yRelation == IntervalRelations.Overlaps && (xRelation == IntervalRelations.Precedes ||
xRelation == IntervalRelations.Meets ||
xRelation == IntervalRelations.Overlaps)) ||
((xRelation == IntervalRelations.Precedes || xRelation == IntervalRelations.Meets || xRelation == IntervalRelations.Overlaps) &&
(yRelation == IntervalRelations.Precedes ||
yRelation == IntervalRelations.Meets ||
yRelation == IntervalRelations.Overlaps ||
yRelation == IntervalRelations.Starts ||
yRelation == IntervalRelations.FinishesI ||
yRelation == IntervalRelations.Equals ||
yRelation == IntervalRelations.During ||
yRelation == IntervalRelations.DuringI ||
yRelation == IntervalRelations.Finishes ||
yRelation == IntervalRelations.StartsI ||
yRelation == IntervalRelations.OverlapsI)))
{
return true;
}
return false;
}
/// <summary>
/// Gets the Thick Boundary Rectangle Relations (TBRR) for the X coordinate.
/// <para>The Thick Boundary Rectangle Relations (TBRR) is a set of qualitative relations representing the spatial relations of the document objects on the page.
/// For every pair of document objects a and b, one X and one Y interval relation hold. If one considers the pair in reversed
/// order, the inverse interval relation holds. Therefore the directed graph g_i representing these relations is complete.</para>
/// </summary>
/// <param name="a"></param>
/// <param name="b"></param>
/// <param name="T">The tolerance parameter T. If two coordinates are closer than T they are considered equal.</param>
private IntervalRelations GetIntervalRelationX(TextBlock a, TextBlock b, double T)
{
IntervalRelations xRelation = IntervalRelations.Unknown;
if (a.BoundingBox.Right < b.BoundingBox.Left - T)
{
xRelation = IntervalRelations.Precedes;
}
else if (a.BoundingBox.Right >= b.BoundingBox.Left - T)
{
xRelation = IntervalRelations.PrecedesI;
}
else if (b.BoundingBox.Left - T <= a.BoundingBox.Right
&& a.BoundingBox.Right <= b.BoundingBox.Left + T)
{
xRelation = IntervalRelations.Meets;
}
else if (b.BoundingBox.Left - T > a.BoundingBox.Right
&& a.BoundingBox.Right > b.BoundingBox.Left + T)
{
xRelation = IntervalRelations.MeetsI;
}
else if (a.BoundingBox.Left < b.BoundingBox.Left - T
&& (b.BoundingBox.Left + T < a.BoundingBox.Right && a.BoundingBox.Right < b.BoundingBox.Right - T))
{
xRelation = IntervalRelations.Overlaps;
}
else if (a.BoundingBox.Left >= b.BoundingBox.Left - T
&& (b.BoundingBox.Left + T >= a.BoundingBox.Right && a.BoundingBox.Right >= b.BoundingBox.Right - T))
{
xRelation = IntervalRelations.OverlapsI;
}
else if ((b.BoundingBox.Left - T <= a.BoundingBox.Left && a.BoundingBox.Left <= b.BoundingBox.Left + T)
&& a.BoundingBox.Right < b.BoundingBox.Right - T)
{
xRelation = IntervalRelations.Starts;
}
else if ((b.BoundingBox.Left - T > a.BoundingBox.Left && a.BoundingBox.Left > b.BoundingBox.Left + T)
&& a.BoundingBox.Right >= b.BoundingBox.Right - T)
{
xRelation = IntervalRelations.StartsI;
}
else if (a.BoundingBox.Left > b.BoundingBox.Left + T
&& a.BoundingBox.Right < b.BoundingBox.Right - T)
{
xRelation = IntervalRelations.During;
}
else if (a.BoundingBox.Left <= b.BoundingBox.Left + T
&& a.BoundingBox.Right >= b.BoundingBox.Right - T)
{
xRelation = IntervalRelations.DuringI;
}
else if (a.BoundingBox.Left > b.BoundingBox.Left + T
&& (b.BoundingBox.Right - T <= a.BoundingBox.Right && a.BoundingBox.Right <= b.BoundingBox.Right + T))
{
xRelation = IntervalRelations.Finishes;
}
else if (a.BoundingBox.Left <= b.BoundingBox.Left + T
&& (b.BoundingBox.Right - T > a.BoundingBox.Right && a.BoundingBox.Right > b.BoundingBox.Right + T))
{
xRelation = IntervalRelations.FinishesI;
}
else if ((b.BoundingBox.Left - T <= a.BoundingBox.Left && a.BoundingBox.Left <= b.BoundingBox.Left + T)
&& (b.BoundingBox.Right - T <= a.BoundingBox.Right && a.BoundingBox.Right <= b.BoundingBox.Right + T))
{
xRelation = IntervalRelations.Equals;
}
return xRelation;
}
/// <summary>
/// Gets the Thick Boundary Rectangle Relations (TBRR) for the Y coordinate.
/// <para>The Thick Boundary Rectangle Relations (TBRR) is a set of qualitative relations representing the spatial relations of the document objects on the page.
/// For every pair of document objects a and b, one X and one Y interval relation hold. If one considers the pair in reversed
/// order, the inverse interval relation holds. Therefore the directed graph g_i representing these relations is complete.</para>
/// </summary>
/// <param name="a"></param>
/// <param name="b"></param>
/// <param name="T">The tolerance parameter T. If two coordinates are closer than T they are considered equal.</param>
private IntervalRelations GetIntervalRelationY(TextBlock a, TextBlock b, double T)
{
IntervalRelations yRelation = IntervalRelations.Unknown;
if (a.BoundingBox.Bottom < b.BoundingBox.Top - T)
{
yRelation = IntervalRelations.PrecedesI;
}
else if (a.BoundingBox.Bottom >= b.BoundingBox.Top - T)
{
yRelation = IntervalRelations.Precedes;
}
else if (b.BoundingBox.Top - T <= a.BoundingBox.Bottom
&& a.BoundingBox.Bottom <= b.BoundingBox.Top + T)
{
yRelation = IntervalRelations.MeetsI;
}
else if (b.BoundingBox.Top - T > a.BoundingBox.Bottom
&& a.BoundingBox.Bottom > b.BoundingBox.Top + T)
{
yRelation = IntervalRelations.Meets;
}
else if (a.BoundingBox.Top < b.BoundingBox.Top - T
&& (b.BoundingBox.Top + T < a.BoundingBox.Bottom && a.BoundingBox.Bottom < b.BoundingBox.Bottom - T))
{
yRelation = IntervalRelations.OverlapsI;
}
else if (a.BoundingBox.Top >= b.BoundingBox.Top - T
&& (b.BoundingBox.Top + T >= a.BoundingBox.Bottom && a.BoundingBox.Bottom >= b.BoundingBox.Bottom - T))
{
yRelation = IntervalRelations.Overlaps;
}
else if ((b.BoundingBox.Top - T <= a.BoundingBox.Top && a.BoundingBox.Top <= b.BoundingBox.Top + T)
&& a.BoundingBox.Bottom < b.BoundingBox.Bottom - T)
{
yRelation = IntervalRelations.StartsI;
}
else if ((b.BoundingBox.Top - T > a.BoundingBox.Top && a.BoundingBox.Top > b.BoundingBox.Top + T)
&& a.BoundingBox.Bottom >= b.BoundingBox.Bottom - T)
{
yRelation = IntervalRelations.Starts;
}
else if (a.BoundingBox.Top > b.BoundingBox.Top + T
&& a.BoundingBox.Bottom < b.BoundingBox.Bottom - T)
{
yRelation = IntervalRelations.DuringI;
}
else if (a.BoundingBox.Top <= b.BoundingBox.Top + T
&& a.BoundingBox.Bottom >= b.BoundingBox.Bottom - T)
{
yRelation = IntervalRelations.During;
}
else if (a.BoundingBox.Top > b.BoundingBox.Top + T
&& (b.BoundingBox.Bottom - T <= a.BoundingBox.Bottom && a.BoundingBox.Bottom <= b.BoundingBox.Bottom + T))
{
yRelation = IntervalRelations.FinishesI;
}
else if (a.BoundingBox.Top <= b.BoundingBox.Top + T
&& (b.BoundingBox.Bottom - T > a.BoundingBox.Bottom && a.BoundingBox.Bottom > b.BoundingBox.Bottom + T))
{
yRelation = IntervalRelations.Finishes;
}
else if ((b.BoundingBox.Top - T <= a.BoundingBox.Top && a.BoundingBox.Top <= b.BoundingBox.Top + T)
&& (b.BoundingBox.Bottom - T <= a.BoundingBox.Bottom && a.BoundingBox.Bottom <= b.BoundingBox.Bottom + T))
{
yRelation = IntervalRelations.Equals;
}
return yRelation;
}
/// <summary>
/// Allens interval thirteen relations.
/// <para>See https://en.wikipedia.org/wiki/Allen%27s_interval_algebra</para>
/// </summary>
private enum IntervalRelations
{
/// <summary>
/// Unknown interval relations.
/// </summary>
Unknown,
/// <summary>
/// X takes place before Y.
/// <para>|____X____|......................</para>
/// <para>......................|____Y____|</para>
/// </summary>
Precedes,
/// <summary>
/// X meets Y.
/// <para>|____X____|.................</para>
/// <para>.................|____Y____|</para>
/// </summary>
Meets,
/// <summary>
/// X overlaps with Y.
/// <para>|______X______|.................</para>
/// <para>.................|______Y______|</para>
/// </summary>
Overlaps,
/// <summary>
/// X starts Y.
/// <para>|____X____|.................</para>
/// <para>|_____Y_____|..............</para>
/// </summary>
Starts,
/// <summary>
/// X during Y.
/// <para>........|____X____|.........</para>
/// <para>.....|______Y______|.....</para>
/// </summary>
During,
/// <summary>
/// X finishes Y.
/// <para>.................|____X____|</para>
/// <para>..............|_____Y_____|</para>
/// </summary>
Finishes,
/// <summary>
/// Inverse precedes.
/// </summary>
PrecedesI,
/// <summary>
/// Inverse meets.
/// </summary>
MeetsI,
/// <summary>
/// Inverse overlaps.
/// </summary>
OverlapsI,
/// <summary>
/// Inverse Starts.
/// </summary>
StartsI,
/// <summary>
/// Inverse during.
/// </summary>
DuringI,
/// <summary>
/// Inverse finishes.
/// </summary>
FinishesI,
/// <summary>
/// X is equal to Y.
/// <para>..........|____X____|............</para>
/// <para>..........|____Y____|............</para>
/// </summary>
Equals
}
private class NodeComparer : IComparer<KeyValuePair<int, List<int>>>
{
public int Compare(KeyValuePair<int, List<int>> x, KeyValuePair<int, List<int>> y)
{
return x.Value.Count.CompareTo(y.Value.Count);
}
}
}
}

View File

@@ -1,10 +1,10 @@
namespace UglyToad.PdfPig.DocumentLayoutAnalysis
{
using Content;
using Core;
using System;
using System.Collections.Generic;
using System.Linq;
using Content;
using Core;
/// <summary>
/// A block of text.
@@ -31,6 +31,11 @@
/// </summary>
public IReadOnlyList<TextLine> TextLines { get; }
/// <summary>
/// The reading order index. Starts at 0. A value of -1 means the block is not ordered.
/// </summary>
public int ReadingOrder { get; private set; }
/// <summary>
/// Create a new <see cref="TextBlock"/>.
/// </summary>
@@ -47,6 +52,8 @@
throw new ArgumentException("Empty lines provided.", nameof(lines));
}
ReadingOrder = -1;
TextLines = lines;
Text = string.Join(" ", lines.Select(x => x.Text));
@@ -60,6 +67,19 @@
TextDirection = lines[0].TextDirection;
}
/// <summary>
/// Sets the <see cref="TextBlock"/>'s reading order.
/// </summary>
/// <param name="readingOrder"></param>
public void SetReadingOrder(int readingOrder)
{
if (readingOrder < -1)
{
throw new ArgumentException("The reading order should be more or equal to -1. A value of -1 means the block is not ordered.", nameof(readingOrder));
}
this.ReadingOrder = readingOrder;
}
/// <inheritdoc />
public override string ToString()
{

View File

@@ -1,13 +1,12 @@
namespace UglyToad.PdfPig.DocumentLayoutAnalysis
{
using Content;
using Core;
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Content;
using Core;
using Geometry;
/// <summary>
/// Text edges extractor. Text edges are where words have either their BoundingBox's left, right or mid coordinates aligned on the same vertical line.

View File

@@ -1,10 +1,10 @@
namespace UglyToad.PdfPig.DocumentLayoutAnalysis
{
using Content;
using Core;
using System;
using System.Collections.Generic;
using System.Linq;
using Content;
using Core;
/// <summary>
/// A line of text.

View File

@@ -1,11 +1,11 @@
namespace UglyToad.PdfPig.DocumentLayoutAnalysis
{
using System;
using System.Collections.Generic;
using System.Linq;
using Content;
using Core;
using Geometry;
using System;
using System.Collections.Generic;
using System.Linq;
/// <summary>
/// A top-down algorithm that finds a cover of the background whitespace of a document in terms of maximal empty rectangles.

View File

@@ -1,10 +1,10 @@
namespace UglyToad.PdfPig.DocumentLayoutAnalysis
namespace UglyToad.PdfPig.DocumentLayoutAnalysis.WordExtractor
{
using Content;
using Core;
using System;
using System.Collections.Generic;
using System.Linq;
using Content;
using Core;
using Util;
/// <summary>

View File

@@ -91,6 +91,16 @@
{
}
public void BeginMarkedContent(NameToken name, NameToken propertyDictionaryName, DictionaryToken properties)
{
}
public void EndMarkedContent()
{
}
private class TestFontFactory : IFontFactory
{
public IFont Get(DictionaryToken dictionary, bool isLenientParsing)

View File

@@ -64,6 +64,7 @@
"UglyToad.PdfPig.Annotations.AnnotationFlags",
"UglyToad.PdfPig.Annotations.AnnotationType",
"UglyToad.PdfPig.Annotations.QuadPointsQuadrilateral",
"UglyToad.PdfPig.Content.ArtifactMarkedContentElement",
"UglyToad.PdfPig.Content.Catalog",
"UglyToad.PdfPig.Content.CropBox",
"UglyToad.PdfPig.Content.DocumentInformation",
@@ -72,6 +73,7 @@
"UglyToad.PdfPig.Content.InlineImage",
"UglyToad.PdfPig.Content.IPdfImage",
"UglyToad.PdfPig.Content.Letter",
"UglyToad.PdfPig.Content.MarkedContentElement",
"UglyToad.PdfPig.Content.Page",
"UglyToad.PdfPig.Content.PageRotationDegrees",
"UglyToad.PdfPig.Content.PageSize",

View File

@@ -65,6 +65,7 @@
public static readonly NameToken BleedBox = new NameToken("BleedBox");
public static readonly NameToken Bm = new NameToken("BM");
public static readonly NameToken Border = new NameToken("Border");
public static readonly NameToken Bottom = new NameToken("Bottom");
public static readonly NameToken Bounds = new NameToken("Bounds");
public static readonly NameToken Bpc = new NameToken("BPC");
public static readonly NameToken Bs = new NameToken("BS");
@@ -307,6 +308,7 @@
public static readonly NameToken Lc = new NameToken("LC");
public static readonly NameToken Le = new NameToken("LE");
public static readonly NameToken Leading = new NameToken("Leading");
public static readonly NameToken Left = new NameToken("Left");
public static readonly NameToken LegalAttestation = new NameToken("LegalAttestation");
public static readonly NameToken Length = new NameToken("Length");
public static readonly NameToken Length1 = new NameToken("Length1");
@@ -448,6 +450,7 @@
public static readonly NameToken Resources = new NameToken("Resources");
public static readonly NameToken Rgb = new NameToken("RGB");
public static readonly NameToken Ri = new NameToken("RI");
public static readonly NameToken Right = new NameToken("Right");
public static readonly NameToken RoleMap = new NameToken("RoleMap");
public static readonly NameToken Root = new NameToken("Root");
public static readonly NameToken Rotate = new NameToken("Rotate");
@@ -513,6 +516,7 @@
public static readonly NameToken Title = new NameToken("Title");
public static readonly NameToken Tk = new NameToken("TK");
public static readonly NameToken Tm = new NameToken("TM");
public static readonly NameToken Top = new NameToken("Top");
public static readonly NameToken ToUnicode = new NameToken("ToUnicode");
public static readonly NameToken Tr = new NameToken("TR");
public static readonly NameToken Tr2 = new NameToken("TR2");

View File

@@ -159,7 +159,7 @@
PdfRectangle? bounds = null;
if (fieldDictionary.TryGet(NameToken.Rect, tokenScanner, out ArrayToken rectArray) && rectArray.Length == 4)
{
bounds = rectArray.ToRectangle();
bounds = rectArray.ToRectangle(tokenScanner);
}
var newParentDictionaries = new List<DictionaryToken>(parentDictionaries) {fieldDictionary};

View File

@@ -53,7 +53,7 @@
var type = annotationDictionary.Get<NameToken>(NameToken.Subtype, tokenScanner);
var annotationType = type.ToAnnotationType();
var rectangle = annotationDictionary.Get<ArrayToken>(NameToken.Rect, tokenScanner).ToRectangle();
var rectangle = annotationDictionary.Get<ArrayToken>(NameToken.Rect, tokenScanner).ToRectangle(tokenScanner);
var contents = GetNamedString(NameToken.Contents, annotationDictionary);
var name = GetNamedString(NameToken.Nm, annotationDictionary);

View File

@@ -0,0 +1,145 @@
namespace UglyToad.PdfPig.Content
{
using System.Collections.Generic;
using Core;
using Tokens;
/// <inheritdoc />
/// <summary>
/// Artifacts are graphics objects that are not part of the author's original content but rather are
/// generated by the conforming writer in the course of pagination, layout, or other strictly mechanical
/// processes.
/// <para>Artifacts may also be used to describe areas of the document where the author uses a graphical
/// background, with the goal of enhancing the visual experience. In such a case, the background is not
/// required for understanding the content. - PDF 32000-1:2008, Section 14.8.2.2</para>
/// </summary>
public class ArtifactMarkedContentElement : MarkedContentElement
{
/// <summary>
/// The artifact's type: Pagination, Layout, Page, or (PDF 1.7) Background.
/// </summary>
public ArtifactType Type { get; }
/// <summary>
/// The artifact's subtype. Standard values are Header, Footer, and Watermark.
/// Additional values may be specified for this entry, provided they comply with the naming conventions.
/// </summary>
public string SubType { get; }
/// <summary>
/// The artifact's attribute owners.
/// </summary>
public string AttributeOwners { get; }
/// <summary>
/// The artifact's bounding box.
/// </summary>
public PdfRectangle? BoundingBox { get; }
/// <summary>
/// The names of regions this element is attached to.
/// </summary>
public IReadOnlyList<NameToken> Attached { get; set; }
/// <summary>
/// Is the artifact attached to the top edge?
/// </summary>
public bool IsTopAttached => IsAttached(NameToken.Top);
/// <summary>
/// Is the artifact attached to the bottom edge?
/// </summary>
public bool IsBottomAttached => IsAttached(NameToken.Bottom);
/// <summary>
/// Is the artifact attached to the left edge?
/// </summary>
public bool IsLeftAttached => IsAttached(NameToken.Left);
/// <summary>
/// Is the artifact attached to the right edge?
/// </summary>
public bool IsRightAttached => IsAttached(NameToken.Right);
internal ArtifactMarkedContentElement(int markedContentIdentifier, NameToken tag, DictionaryToken properties,
string language,
string actualText,
string alternateDescription,
string expandedForm,
ArtifactType artifactType,
string subType,
string attributeOwners,
PdfRectangle? boundingBox,
IReadOnlyList<NameToken> attached,
IReadOnlyList<MarkedContentElement> children,
IReadOnlyList<Letter> letters,
IReadOnlyList<PdfPath> paths,
IReadOnlyList<IPdfImage> images,
int index)
: base(markedContentIdentifier, tag, properties, language,
actualText,
alternateDescription,
expandedForm,
true,
children,
letters,
paths,
images,
index)
{
Type = artifactType;
SubType = subType;
AttributeOwners = attributeOwners;
BoundingBox = boundingBox;
Attached = attached ?? EmptyArray<NameToken>.Instance;
}
private bool IsAttached(NameToken edge)
{
foreach (var name in Attached)
{
if (name == edge)
{
return true;
}
}
return false;
}
/// <summary>
/// If present, shall be one of the names Pagination, Layout, Page, or (PDF 1.7) Background.
/// </summary>
public enum ArtifactType
{
/// <summary>
/// Unknown artifact type.
/// </summary>
Unknown,
/// <summary>
/// Ancillary page features such as running heads and folios (page numbers).
/// </summary>
Pagination,
/// <summary>
/// Purely cosmetic typographical or design elements such as footnote rules or background screens.
/// </summary>
Layout,
/// <summary>
/// Production aids extraneous to the document itself, such as cut marks and colour bars.
/// </summary>
Page,
/// <summary>
/// (PDF 1.7) Images, patterns or coloured blocks that either run the entire length and/or
/// width of the page or the entire dimensions of a structural element. Background artifacts
/// typically serve as a background for content shown either on top of or placed adjacent to
/// that background.
/// <para>A background artifact can further be classified as visual content that serves to enhance the user experience, that lies under the actual content, and that is not required except to retain visual fidelity.</para>
/// </summary>
Background
}
}
}

View File

@@ -23,5 +23,7 @@
IFont GetFontDirectly(IndirectReferenceToken fontReferenceToken, bool isLenientParsing);
bool TryGetNamedColorSpace(NameToken name, out ResourceColorSpace namedColorSpace);
DictionaryToken GetMarkedContentPropertiesDictionary(NameToken name);
}
}

View File

@@ -0,0 +1,115 @@
namespace UglyToad.PdfPig.Content
{
using System;
using System.Collections.Generic;
using Core;
using Tokens;
/// <summary>
/// A marked content element can be used to provide application specific data in the
/// page's content stream. Interpretation of the marked content is outside of the PDF specification.
/// </summary>
public class MarkedContentElement
{
/// <summary>
/// Marked-content identifier.
/// </summary>
public int MarkedContentIdentifier { get; }
/// <summary>
/// The index of this marked content element in the set of marked content in the page.
/// <see cref="Children"/> marked content elements will have the same index as the parent.
/// </summary>
public int Index { get; }
/// <summary>
/// A name indicating the role or significance of the point.
/// </summary>
public string Tag { get; }
/// <summary>
/// The properties for this element.
/// </summary>
public DictionaryToken Properties { get; }
/// <summary>
/// Is the marked content an artifact, see <see cref="ArtifactMarkedContentElement"/>.
/// </summary>
public bool IsArtifact { get; }
/// <summary>
/// Child contents.
/// </summary>
public IReadOnlyList<MarkedContentElement> Children { get; }
/// <summary>
/// Letters contained in this marked content.
/// </summary>
public IReadOnlyList<Letter> Letters { get; }
/// <summary>
/// Paths contained in this marked content.
/// </summary>
public IReadOnlyList<PdfPath> Paths { get; }
/// <summary>
/// Images contained in this marked content.
/// </summary>
public IReadOnlyList<IPdfImage> Images { get; }
/// <summary>
/// The natural language specification.
/// </summary>
public string Language { get; }
/// <summary>
/// The replacement text.
/// </summary>
public string ActualText { get; }
/// <summary>
/// The alternate description.
/// </summary>
public string AlternateDescription { get; }
/// <summary>
/// The abbreviation expansion text.
/// </summary>
public string ExpandedForm { get; }
public MarkedContentElement(int markedContentIdentifier, NameToken tag, DictionaryToken properties,
string language,
string actualText,
string alternateDescription,
string expandedForm,
bool isArtifact,
IReadOnlyList<MarkedContentElement> children,
IReadOnlyList<Letter> letters,
IReadOnlyList<PdfPath> paths,
IReadOnlyList<IPdfImage> images,
int index)
{
MarkedContentIdentifier = markedContentIdentifier;
Tag = tag;
Language = language;
ActualText = actualText;
AlternateDescription = alternateDescription;
ExpandedForm = expandedForm;
Properties = properties ?? new DictionaryToken(new Dictionary<NameToken, IToken>());
IsArtifact = isArtifact;
Children = children ?? throw new ArgumentNullException(nameof(children));
Letters = letters ?? throw new ArgumentNullException(nameof(letters));
Paths = paths ?? throw new ArgumentNullException(nameof(paths));
Images = images ?? throw new ArgumentNullException(nameof(images));
Index = index;
}
/// <inheritdoc />
public override string ToString()
{
return $"Index={Index}, MCID={MarkedContentIdentifier}, Tag={Tag}, Properties={Properties}, Contents={Children.Count}";
}
}
}

View File

@@ -153,6 +153,11 @@
/// </summary>
public IEnumerable<IPdfImage> GetImages() => Content.GetImages();
/// <summary>
/// Gets any marked content on the page.
/// </summary>
public IReadOnlyList<MarkedContentElement> GetMarkedContents() => Content.GetMarkedContents();
/// <summary>
/// Provides access to useful members which will change in future releases.
/// </summary>

View File

@@ -8,7 +8,6 @@
using Graphics.Operations;
using Tokenization.Scanner;
using XObjects;
using Geometry;
/// <summary>
/// Wraps content parsed from a page content stream for access.
@@ -20,10 +19,10 @@
internal class PageContent
{
private readonly IReadOnlyList<Union<XObjectContentRecord, InlineImage>> images;
private readonly IReadOnlyList<MarkedContentElement> markedContents;
private readonly IPdfTokenScanner pdfScanner;
private readonly IFilterProvider filterProvider;
private readonly IResourceStore resourceStore;
private readonly bool isLenientParsing;
internal IReadOnlyList<IGraphicsStateOperation> GraphicsStateOperations { get; }
@@ -34,32 +33,32 @@
internal PageContent(IReadOnlyList<IGraphicsStateOperation> graphicsStateOperations, IReadOnlyList<Letter> letters,
IReadOnlyList<PdfPath> paths,
IReadOnlyList<Union<XObjectContentRecord, InlineImage>> images,
IReadOnlyList<MarkedContentElement> markedContents,
IPdfTokenScanner pdfScanner,
IFilterProvider filterProvider,
IResourceStore resourceStore,
bool isLenientParsing)
IResourceStore resourceStore)
{
GraphicsStateOperations = graphicsStateOperations;
Letters = letters;
Paths = paths;
this.images = images;
this.markedContents = markedContents;
this.pdfScanner = pdfScanner ?? throw new ArgumentNullException(nameof(pdfScanner));
this.filterProvider = filterProvider ?? throw new ArgumentNullException(nameof(filterProvider));
this.resourceStore = resourceStore ?? throw new ArgumentNullException(nameof(resourceStore));
this.isLenientParsing = isLenientParsing;
}
public IEnumerable<IPdfImage> GetImages()
{
foreach (var image in images)
{
IPdfImage result = null;
image.Match(x => { result = XObjectFactory.ReadImage(x, pdfScanner, filterProvider, resourceStore, isLenientParsing); },
x => { result = x; });
var result = image.Match<IPdfImage>(x => XObjectFactory.ReadImage(x, pdfScanner, filterProvider, resourceStore),
x => x);
yield return result;
}
}
public IReadOnlyList<MarkedContentElement> GetMarkedContents() => markedContents;
}
}

View File

@@ -57,7 +57,7 @@
if (currentNode.NodeDictionary.TryGet(NameToken.MediaBox, pdfScanner, out ArrayToken mediaBox))
{
pageTreeMembers.MediaBox = new MediaBox(mediaBox.ToRectangle());
pageTreeMembers.MediaBox = new MediaBox(mediaBox.ToRectangle(pdfScanner));
}
if (currentNode.NodeDictionary.TryGet(NameToken.Rotate, pdfScanner, out NumericToken rotateToken))

View File

@@ -22,6 +22,8 @@
private readonly Dictionary<NameToken, ResourceColorSpace> namedColorSpaces = new Dictionary<NameToken, ResourceColorSpace>();
private readonly Dictionary<NameToken, DictionaryToken> markedContentProperties = new Dictionary<NameToken, DictionaryToken>();
private (NameToken name, IFont font) lastLoadedFont;
public ResourceStore(IPdfTokenScanner scanner, IFontFactory fontFactory)
@@ -101,6 +103,21 @@
}
}
}
if (resourceDictionary.TryGet(NameToken.Properties, scanner, out DictionaryToken markedContentPropertiesList))
{
foreach (var pair in markedContentPropertiesList.Data)
{
var key = NameToken.Create(pair.Key);
if (!DirectObjectFinder.TryGet(pair.Value, scanner, out DictionaryToken namedProperties))
{
continue;
}
markedContentProperties[key] = namedProperties;
}
}
}
public void UnloadResourceDictionary()
@@ -207,5 +224,10 @@
{
return extendedGraphicsStates[name];
}
public DictionaryToken GetMarkedContentPropertiesDictionary(NameToken name)
{
return markedContentProperties.TryGetValue(name, out var result) ? result : null;
}
}
}

View File

@@ -4,6 +4,7 @@
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using Colors;
using Content;
using Core;
using Filters;
@@ -34,6 +35,11 @@
/// </summary>
private readonly List<Union<XObjectContentRecord, InlineImage>> images = new List<Union<XObjectContentRecord, InlineImage>>();
/// <summary>
/// Stores each marked content as it is encountered in the content stream.
/// </summary>
private readonly List<MarkedContentElement> markedContents = new List<MarkedContentElement>();
private readonly IResourceStore resourceStore;
private readonly UserSpaceUnit userSpaceUnit;
private readonly PageRotationDegrees rotation;
@@ -42,6 +48,7 @@
private readonly IPageContentParser pageContentParser;
private readonly IFilterProvider filterProvider;
private readonly ILog log;
private readonly MarkedContentStack markedContentStack = new MarkedContentStack();
private Stack<CurrentGraphicsState> graphicsStack = new Stack<CurrentGraphicsState>();
private IFont activeExtendedGraphicsStateFont;
@@ -100,7 +107,7 @@
ProcessOperations(operations);
return new PageContent(operations, letters, paths, images, pdfScanner, filterProvider, resourceStore, isLenientParsing);
return new PageContent(operations, letters, paths, images, markedContents, pdfScanner, filterProvider, resourceStore);
}
private void ProcessOperations(IReadOnlyList<IGraphicsStateOperation> operations)
@@ -161,6 +168,11 @@
// see: https://stackoverflow.com/questions/48010235/pdf-specification-get-font-size-in-points
var pointSize = Math.Round(rotation.Rotate(transformationMatrix).Multiply(TextMatrices.TextMatrix).Multiply(fontSize).A, 2);
if (pointSize < 0)
{
pointSize *= -1;
}
while (bytes.MoveNext())
{
var code = font.ReadCharacterCode(bytes, out int codeLength);
@@ -226,6 +238,8 @@
letters.Add(letter);
markedContentStack.AddLetter(letter);
double tx, ty;
if (font.IsVertical)
{
@@ -309,11 +323,19 @@
if (subType.Equals(NameToken.Ps))
{
xObjects[XObjectType.PostScript].Add(new XObjectContentRecord(XObjectType.PostScript, xObjectStream, matrix, state.RenderingIntent));
var contentRecord = new XObjectContentRecord(XObjectType.PostScript, xObjectStream, matrix, state.RenderingIntent,
state.CurrentStrokingColor?.ColorSpace ?? ColorSpace.DeviceRGB);
xObjects[XObjectType.PostScript].Add(contentRecord);
}
else if (subType.Equals(NameToken.Image))
{
images.Add(Union<XObjectContentRecord, InlineImage>.One(new XObjectContentRecord(XObjectType.Image, xObjectStream, matrix, state.RenderingIntent)));
var contentRecord = new XObjectContentRecord(XObjectType.Image, xObjectStream, matrix, state.RenderingIntent,
state.CurrentStrokingColor?.ColorSpace ?? ColorSpace.DeviceRGB);
images.Add(Union<XObjectContentRecord, InlineImage>.One(contentRecord));
markedContentStack.AddXObject(contentRecord, pdfScanner, filterProvider, resourceStore);
}
else if (subType.Equals(NameToken.Form))
{
@@ -382,6 +404,7 @@
if (CurrentPath != null && CurrentPath.Commands.Count > 0 && !currentPathAdded)
{
paths.Add(CurrentPath);
markedContentStack.AddPath(CurrentPath);
}
CurrentPath = new PdfPath();
@@ -397,6 +420,7 @@
else
{
paths.Add(CurrentPath);
markedContentStack.AddPath(CurrentPath);
currentPathAdded = true;
}
}
@@ -410,6 +434,7 @@
else
{
paths.Add(CurrentPath);
markedContentStack.AddPath(CurrentPath);
currentPathAdded = true;
}
}
@@ -418,6 +443,7 @@
{
CurrentPath.ClosePath();
paths.Add(CurrentPath);
markedContentStack.AddPath(CurrentPath);
CurrentPath = null;
currentPathAdded = false;
}
@@ -495,9 +521,31 @@
images.Add(Union<XObjectContentRecord, InlineImage>.Two(image));
markedContentStack.AddImage(image);
inlineImageBuilder = null;
}
public void BeginMarkedContent(NameToken name, NameToken propertyDictionaryName, DictionaryToken properties)
{
if (propertyDictionaryName != null)
{
var actual = resourceStore.GetMarkedContentPropertiesDictionary(propertyDictionaryName);
properties = actual ?? properties;
}
markedContentStack.Push(name, properties);
}
public void EndMarkedContent()
{
if (markedContentStack.CanPop)
{
markedContents.Add(markedContentStack.Pop(pdfScanner));
}
}
private void AdjustTextMatrix(double tx, double ty)
{
var matrix = TransformationMatrix.GetTranslationMatrix(tx, ty);

View File

@@ -1,8 +1,8 @@
namespace UglyToad.PdfPig.Graphics
{
using PdfPig.Core;
using System.Collections.Generic;
using Tokens;
using PdfPig.Core;
using Util.JetBrains.Annotations;
/// <summary>
@@ -97,6 +97,16 @@
/// </summary>
void ClosePath();
/// <summary>
/// Indicate that a marked content region is started.
/// </summary>
void BeginMarkedContent(NameToken name, NameToken propertyDictionaryName, DictionaryToken properties);
/// <summary>
/// Indicates that the current marked content region is complete.
/// </summary>
void EndMarkedContent();
/// <summary>
/// Update the graphics state to apply the state from the named ExtGState dictionary.
/// </summary>

View File

@@ -0,0 +1,208 @@
namespace UglyToad.PdfPig.Graphics
{
using System;
using System.Collections.Generic;
using Content;
using Filters;
using PdfPig.Core;
using Tokenization.Scanner;
using Tokens;
using XObjects;
/// <summary>
/// Handles building <see cref="MarkedContentElement"/>s.
/// </summary>
internal class MarkedContentStack
{
private readonly Stack<MarkedContentElementActiveBuilder> builderStack = new Stack<MarkedContentElementActiveBuilder>();
private int number;
private MarkedContentElementActiveBuilder top;
public bool CanPop => top != null;
public void Push(NameToken name, DictionaryToken properties)
{
if (builderStack.Count > 0)
{
number++;
}
top = new MarkedContentElementActiveBuilder(number, name, properties);
builderStack.Push(top);
}
public MarkedContentElement Pop(IPdfTokenScanner pdfScanner)
{
var builder = builderStack.Pop();
var result = builder.Build(pdfScanner);
if (builderStack.Count > 0)
{
top = builderStack.Peek();
top.Children.Add(result);
}
else
{
top = null;
}
return result;
}
public void AddLetter(Letter letter)
{
top?.AddLetter(letter);
}
public void AddPath(PdfPath path)
{
top?.AddPath(path);
}
public void AddImage(IPdfImage image)
{
top?.AddImage(image);
}
public void AddXObject(XObjectContentRecord xObject,
IPdfTokenScanner scanner,
IFilterProvider filterProvider,
IResourceStore resourceStore)
{
if (top != null && xObject.Type == XObjectType.Image)
{
var image = XObjectFactory.ReadImage(xObject, scanner, filterProvider, resourceStore);
top?.AddImage(image);
}
}
private class MarkedContentElementActiveBuilder
{
private readonly int number;
private readonly NameToken name;
private readonly DictionaryToken properties;
private readonly List<Letter> letters = new List<Letter>();
private readonly List<PdfPath> paths = new List<PdfPath>();
private readonly List<IPdfImage> images = new List<IPdfImage>();
public List<MarkedContentElement> Children { get; } = new List<MarkedContentElement>();
public MarkedContentElementActiveBuilder(int number, NameToken name, DictionaryToken properties)
{
this.number = number;
this.name = name;
this.properties = properties ?? new DictionaryToken(new Dictionary<NameToken, IToken>());
}
public void AddLetter(Letter letter)
{
letters.Add(letter);
}
public void AddImage(IPdfImage image)
{
images.Add(image);
}
public void AddPath(PdfPath path)
{
paths.Add(path);
}
public MarkedContentElement Build(IPdfTokenScanner pdfScanner)
{
var mcid = -1;
if (properties.TryGet(NameToken.Mcid, pdfScanner, out NumericToken mcidToken))
{
mcid = mcidToken.Int;
}
var language = GetOptional(NameToken.Lang, pdfScanner);
var actualText = GetOptional(NameToken.ActualText, pdfScanner);
var alternateDescription = GetOptional(NameToken.Alternate, pdfScanner);
var expandedForm = GetOptional(NameToken.E, pdfScanner);
if (name != NameToken.Artifact)
{
return new MarkedContentElement(mcid, name, properties,
language,
actualText,
alternateDescription,
expandedForm,
false,
Children,
letters,
paths,
images,
number);
}
var artifactType = ArtifactMarkedContentElement.ArtifactType.Unknown;
if (properties.TryGet(NameToken.Type, pdfScanner, out IDataToken<string> typeToken)
&& Enum.TryParse(typeToken.Data, true, out ArtifactMarkedContentElement.ArtifactType parsedType))
{
artifactType = parsedType;
}
var subType = GetOptional(NameToken.Subtype, pdfScanner);
var attributeOwners = GetOptional(NameToken.O, pdfScanner);
var boundingBox = default(PdfRectangle?);
if (properties.TryGet(NameToken.Bbox, pdfScanner, out ArrayToken arrayToken)
&& arrayToken.Length == 6)
{
var left = arrayToken[2] as NumericToken;
var bottom = arrayToken[3] as NumericToken;
var right = arrayToken[4] as NumericToken;
var top = arrayToken[5] as NumericToken;
if (left != null && bottom != null && right != null && top != null)
{
boundingBox = new PdfRectangle(left.Double, bottom.Double, right.Double, top.Double);
}
}
var attached = new List<NameToken>();
if (properties.TryGet(NameToken.Attached, out ArrayToken attachedToken))
{
foreach (var token in attachedToken.Data)
{
if (token is NameToken aName)
{
attached.Add(aName);
}
}
}
return new ArtifactMarkedContentElement(mcid, name, properties, language,
actualText,
alternateDescription,
expandedForm,
artifactType,
subType,
attributeOwners,
boundingBox,
attached,
Children,
letters,
paths,
images,
number);
}
private string GetOptional(NameToken optionName, IPdfTokenScanner pdfScanner)
{
var result = default(string);
if (properties.TryGet(optionName, pdfScanner, out IDataToken<string> token))
{
result = token.Data;
}
return result;
}
}
}
}

View File

@@ -35,6 +35,7 @@
/// <inheritdoc />
public void Run(IOperationContext operationContext)
{
operationContext.BeginMarkedContent(Name, null, null);
}
/// <inheritdoc />

View File

@@ -63,6 +63,7 @@
/// <inheritdoc />
public void Run(IOperationContext operationContext)
{
operationContext.BeginMarkedContent(Name, PropertyDictionaryName, Properties);
}
/// <inheritdoc />

View File

@@ -28,6 +28,7 @@
/// <inheritdoc />
public void Run(IOperationContext operationContext)
{
operationContext.EndMarkedContent();
}
/// <inheritdoc />

View File

@@ -1,6 +1,7 @@
namespace UglyToad.PdfPig.Graphics
{
using System;
using Colors;
using Core;
using PdfPig.Core;
using Tokens;
@@ -18,13 +19,17 @@
public RenderingIntent DefaultRenderingIntent { get; }
public ColorSpace DefaultColorSpace { get; }
public XObjectContentRecord(XObjectType type, StreamToken stream, TransformationMatrix appliedTransformation,
RenderingIntent defaultRenderingIntent)
RenderingIntent defaultRenderingIntent,
ColorSpace defaultColorSpace)
{
Type = type;
Stream = stream ?? throw new ArgumentNullException(nameof(stream));
AppliedTransformation = appliedTransformation;
DefaultRenderingIntent = defaultRenderingIntent;
DefaultColorSpace = defaultColorSpace;
}
}
}

View File

@@ -177,7 +177,7 @@
return cropBox;
}
cropBox = new CropBox(cropBoxArray.ToIntRectangle());
cropBox = new CropBox(cropBoxArray.ToIntRectangle(pdfScanner));
}
else
{
@@ -202,7 +202,7 @@
return mediaBox;
}
mediaBox = new MediaBox(mediaboxArray.ToIntRectangle());
mediaBox = new MediaBox(mediaboxArray.ToIntRectangle(pdfScanner));
}
else
{

View File

@@ -21,7 +21,7 @@
var family = GetFontFamily(dictionary);
var stretch = GetFontStretch(dictionary);
var flags = GetFlags(dictionary, isLenientParsing);
var bounding = GetBoundingBox(dictionary);
var bounding = GetBoundingBox(dictionary, pdfScanner);
var charSet = GetCharSet(dictionary);
var fontFile = GetFontFile(dictionary);
@@ -119,7 +119,7 @@
return (FontDescriptorFlags) flags;
}
private static PdfRectangle GetBoundingBox(DictionaryToken dictionary)
private static PdfRectangle GetBoundingBox(DictionaryToken dictionary, IPdfTokenScanner pdfScanner)
{
if (!dictionary.TryGet(NameToken.FontBbox, out var box) || !(box is ArrayToken boxArray))
{
@@ -131,7 +131,7 @@
return new PdfRectangle(0, 0, 0, 0);
}
return boxArray.ToRectangle();
return boxArray.ToRectangle(pdfScanner);
}
private static string GetCharSet(DictionaryToken dictionary)

View File

@@ -2,8 +2,6 @@
{
using System;
using Core;
using Exceptions;
using Geometry;
using JetBrains.Annotations;
using Parser.Parts;
using Tokenization.Scanner;
@@ -125,7 +123,7 @@
throw new PdfDocumentFormatException($"The array did not contain a number at index {index}. Array was: {array}.");
}
public static PdfRectangle ToRectangle(this ArrayToken array)
public static PdfRectangle ToRectangle(this ArrayToken array, IPdfTokenScanner tokenScanner)
{
if (array == null)
{
@@ -137,13 +135,13 @@
throw new PdfDocumentFormatException($"Cannot convert array to rectangle, expected 4 values instead got: {array}.");
}
return new PdfRectangle(array.GetNumeric(0).Double,
array.GetNumeric(1).Double,
array.GetNumeric(2).Double,
array.GetNumeric(3).Double);
return new PdfRectangle(DirectObjectFinder.Get<NumericToken>(array[0], tokenScanner).Double,
DirectObjectFinder.Get<NumericToken>(array[1], tokenScanner).Double,
DirectObjectFinder.Get<NumericToken>(array[2], tokenScanner).Double,
DirectObjectFinder.Get<NumericToken>(array[3], tokenScanner).Double);
}
public static PdfRectangle ToIntRectangle(this ArrayToken array)
public static PdfRectangle ToIntRectangle(this ArrayToken array, IPdfTokenScanner tokenScanner)
{
if (array == null)
{
@@ -155,10 +153,10 @@
throw new PdfDocumentFormatException($"Cannot convert array to rectangle, expected 4 values instead got: {array}.");
}
return new PdfRectangle(array.GetNumeric(0).Int,
array.GetNumeric(1).Int,
array.GetNumeric(2).Int,
array.GetNumeric(3).Int);
return new PdfRectangle(DirectObjectFinder.Get<NumericToken>(array[0], tokenScanner).Int,
DirectObjectFinder.Get<NumericToken>(array[1], tokenScanner).Int,
DirectObjectFinder.Get<NumericToken>(array[2], tokenScanner).Int,
DirectObjectFinder.Get<NumericToken>(array[3], tokenScanner).Int);
}
}
}

View File

@@ -16,8 +16,7 @@
{
public static XObjectImage ReadImage(XObjectContentRecord xObject, IPdfTokenScanner pdfScanner,
IFilterProvider filterProvider,
IResourceStore resourceStore,
bool isLenientParsing)
IResourceStore resourceStore)
{
if (xObject == null)
{
@@ -87,25 +86,19 @@
{
colorSpace = colorSpaceResult;
}
else if (dictionary.TryGet(NameToken.ColorSpace, pdfScanner, out ArrayToken colorSpaceArrayToken))
else if (dictionary.TryGet(NameToken.ColorSpace, pdfScanner, out ArrayToken colorSpaceArrayToken)
&& colorSpaceArrayToken.Length > 0)
{
if (colorSpaceArrayToken.Length == 0)
{
throw new PdfDocumentFormatException($"Empty ColorSpace array defined for image XObject: {dictionary}.");
}
var first = colorSpaceArrayToken.Data[0];
if (!(first is NameToken firstColorSpaceName) || !TryMapColorSpace(firstColorSpaceName, resourceStore, out colorSpaceResult))
if ((first is NameToken firstColorSpaceName) && TryMapColorSpace(firstColorSpaceName, resourceStore, out colorSpaceResult))
{
throw new PdfDocumentFormatException($"Invalid ColorSpace array defined for image XObject: {colorSpaceArrayToken}.");
colorSpace = colorSpaceResult;
}
colorSpace = colorSpaceResult;
}
else if (!isJpxDecode)
{
throw new PdfDocumentFormatException($"No ColorSpace defined for image XObject: {dictionary}.");
colorSpace = xObject.DefaultColorSpace;
}
}

View File

@@ -12,7 +12,7 @@
<PackageTags>PDF;Reader;Document;Adobe;PDFBox;PdfPig;pdf-extract</PackageTags>
<RepositoryUrl>https://github.com/UglyToad/PdfPig</RepositoryUrl>
<GenerateDocumentationFile>true</GenerateDocumentationFile>
<Version>0.1.0-beta002</Version>
<Version>0.1.0</Version>
<AssemblyVersion>0.1.0.0</AssemblyVersion>
<FileVersion>0.1.0.0</FileVersion>
<PackageIconUrl>https://raw.githubusercontent.com/UglyToad/PdfPig/master/documentation/pdfpig.png</PackageIconUrl>