#55 move support for images to page and add inline images

support both xobject and inline images. adds unsupported filters so that exceptions are only thrown when accessing lazily evaluated image.bytes property rather than when opening the page.

treat all warnings as errors.
This commit is contained in:
Eliot Jones
2019-10-08 14:04:36 +01:00
parent e02e130947
commit 68bcaf3901
41 changed files with 1083 additions and 169 deletions

View File

@@ -1,13 +1,25 @@
namespace UglyToad.PdfPig.XObjects
{
using System;
using System.Collections.Generic;
using System.Linq;
using Content;
using Exceptions;
using Filters;
using Geometry;
using Graphics;
using Graphics.Colors;
using Graphics.Core;
using Tokenization.Scanner;
using Tokens;
using Util;
internal class XObjectFactory
internal static class XObjectFactory
{
public XObjectImage CreateImage(XObjectContentRecord xObject, IPdfTokenScanner pdfScanner, bool isLenientParsing)
public static XObjectImage ReadImage(XObjectContentRecord xObject, IPdfTokenScanner pdfScanner,
IFilterProvider filterProvider,
IResourceStore resourceStore,
bool isLenientParsing)
{
if (xObject == null)
{
@@ -19,19 +31,103 @@
throw new InvalidOperationException($"Cannot create an image from an XObject with type: {xObject.Type}.");
}
var width = xObject.Stream.StreamDictionary.Get<NumericToken>(NameToken.Width, pdfScanner).Int;
var height = xObject.Stream.StreamDictionary.Get<NumericToken>(NameToken.Height, pdfScanner).Int;
var dictionary = xObject.Stream.StreamDictionary;
var isJpxDecode = xObject.Stream.StreamDictionary.TryGet(NameToken.Filter, out var token)
var bounds = xObject.AppliedTransformation.Transform(new PdfRectangle(new PdfPoint(0, 0), new PdfPoint(1, 1)));
var width = dictionary.Get<NumericToken>(NameToken.Width, pdfScanner).Int;
var height = dictionary.Get<NumericToken>(NameToken.Height, pdfScanner).Int;
var isImageMask = dictionary.TryGet(NameToken.ImageMask, pdfScanner, out BooleanToken isMaskToken)
&& isMaskToken.Data;
var isJpxDecode = dictionary.TryGet(NameToken.Filter, out var token)
&& token is NameToken filterName
&& filterName.Equals(NameToken.JpxDecode);
var isImageMask = xObject.Stream.StreamDictionary.TryGet(NameToken.ImageMask, out var maskToken)
&& maskToken is BooleanToken maskBoolean
&& maskBoolean.Data;
return new XObjectImage(width, height, isJpxDecode, isImageMask, xObject.Stream.StreamDictionary,
xObject.Stream.Data);
int bitsPerComponent = 0;
if (!isImageMask && !isJpxDecode)
{
if (!dictionary.TryGet(NameToken.BitsPerComponent, pdfScanner, out NumericToken bitsPerComponentToken))
{
throw new PdfDocumentFormatException($"No bits per component defined for image: {dictionary}.");
}
bitsPerComponent = bitsPerComponentToken.Int;
}
else if (isImageMask)
{
bitsPerComponent = 1;
}
var intent = xObject.DefaultRenderingIntent;
if (dictionary.TryGet(NameToken.Intent, out NameToken renderingIntentToken))
{
intent = renderingIntentToken.Data.ToRenderingIntent();
}
var interpolate = dictionary.TryGet(NameToken.Interpolate, pdfScanner, out BooleanToken interpolateToken)
&& interpolateToken.Data;
var decodedBytes = new Lazy<IReadOnlyList<byte>>(() => xObject.Stream.Decode(filterProvider));
var decode = EmptyArray<decimal>.Instance;
if (dictionary.TryGet(NameToken.Decode, pdfScanner, out ArrayToken decodeArrayToken))
{
decode = decodeArrayToken.Data.OfType<NumericToken>()
.Select(x => x.Data)
.ToArray();
}
var colorSpace = default(ColorSpace?);
if (!isImageMask)
{
if (dictionary.TryGet(NameToken.ColorSpace, pdfScanner, out NameToken colorSpaceNameToken)
&& TryMapColorSpace(colorSpaceNameToken, resourceStore, out var colorSpaceResult))
{
colorSpace = colorSpaceResult;
}
else if (dictionary.TryGet(NameToken.ColorSpace, pdfScanner, out ArrayToken colorSpaceArrayToken))
{
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))
{
throw new PdfDocumentFormatException($"Invalid ColorSpace array defined for image XObject: {colorSpaceArrayToken}.");
}
colorSpace = colorSpaceResult;
}
else if (!isJpxDecode)
{
throw new PdfDocumentFormatException($"No ColorSpace defined for image XObject: {dictionary}.");
}
}
return new XObjectImage(bounds, width, height, bitsPerComponent, colorSpace, isJpxDecode, isImageMask, intent, interpolate, decode,
dictionary, xObject.Stream.Data, decodedBytes);
}
private static bool TryMapColorSpace(NameToken name, IResourceStore resourceStore, out ColorSpace colorSpaceResult)
{
if (name.TryMapToColorSpace(out colorSpaceResult))
{
return true;
}
if (!resourceStore.TryGetNamedColorSpace(name, out var colorSpaceNamedToken) || !(colorSpaceNamedToken is NameToken newName))
{
return false;
}
return newName.TryMapToColorSpace(out colorSpaceResult);
}
}
}

View File

@@ -2,23 +2,35 @@
{
using System;
using System.Collections.Generic;
using Content;
using Geometry;
using Graphics.Colors;
using Graphics.Core;
using Tokens;
using Util.JetBrains.Annotations;
/// <inheritdoc />
/// <summary>
/// The raw stream from a PDF document representing an image XObject.
/// A PostScript image XObject.
/// </summary>
public class XObjectImage
public class XObjectImage : IPdfImage
{
/// <summary>
/// The width of the image in samples.
/// </summary>
public int Width { get; }
private readonly Lazy<IReadOnlyList<byte>> bytes;
/// <summary>
/// The height of the image in samples.
/// </summary>
public int Height { get; }
/// <inheritdoc />
public PdfRectangle Bounds { get; }
/// <inheritdoc />
public int WidthInSamples { get; }
/// <inheritdoc />
public int HeightInSamples { get; }
/// <inheritdoc />
public ColorSpace? ColorSpace { get; }
/// <inheritdoc />
public int BitsPerComponent { get; }
/// <summary>
/// The JPX filter encodes data using the JPEG2000 compression method.
@@ -27,41 +39,67 @@
/// </summary>
public bool IsJpxEncoded { get; }
/// <summary>
/// Whether this image should be treated as an image maske.
/// </summary>
/// <inheritdoc />
public RenderingIntent RenderingIntent { get; }
/// <inheritdoc />
public bool IsImageMask { get; }
/// <inheritdoc />
public IReadOnlyList<decimal> Decode { get; }
/// <inheritdoc />
public bool Interpolate { get; }
/// <inheritdoc />
public bool IsInlineImage { get; } = false;
/// <summary>
/// The full dictionary for this Image XObject.
/// </summary>
[NotNull]
public DictionaryToken ImageDictionary { get; }
/// <summary>
/// The encoded bytes of this image, must be decoded via any
/// filters defined in the <see cref="ImageDictionary"/> prior to consumption.
/// </summary>
[NotNull]
public IReadOnlyList<byte> Bytes { get; }
/// <inheritdoc />
public IReadOnlyList<byte> RawBytes { get; }
/// <inheritdoc />
[NotNull]
public IReadOnlyList<byte> Bytes => bytes.Value;
/// <summary>
/// Creates a new <see cref="XObjectImage"/>.
/// </summary>
internal XObjectImage(int width, int height, bool isJpxEncoded, bool isImageMask, DictionaryToken imageDictionary, IReadOnlyList<byte> bytes)
internal XObjectImage(PdfRectangle bounds, int widthInSamples, int heightInSamples, int bitsPerComponent,
ColorSpace? colorSpace,
bool isJpxEncoded,
bool isImageMask,
RenderingIntent renderingIntent,
bool interpolate,
IReadOnlyList<decimal> decode,
DictionaryToken imageDictionary,
IReadOnlyList<byte> rawBytes,
Lazy<IReadOnlyList<byte>> bytes)
{
Width = width;
Height = height;
Bounds = bounds;
WidthInSamples = widthInSamples;
HeightInSamples = heightInSamples;
BitsPerComponent = bitsPerComponent;
ColorSpace = colorSpace;
IsJpxEncoded = isJpxEncoded;
IsImageMask = isImageMask;
RenderingIntent = renderingIntent;
Interpolate = interpolate;
Decode = decode;
ImageDictionary = imageDictionary ?? throw new ArgumentNullException(nameof(imageDictionary));
Bytes = bytes ?? throw new ArgumentNullException(nameof(bytes));
RawBytes = rawBytes;
this.bytes = bytes ?? throw new ArgumentNullException(nameof(bytes));
}
/// <inheritdoc />
public override string ToString()
{
return ImageDictionary.ToString();
return $"XObject Image (w {Bounds.Width}, h {Bounds.Height}): {ImageDictionary}";
}
}
}