mirror of
https://github.com/UglyToad/PdfPig.git
synced 2025-07-15 17:12:34 +08:00
Add visual verification tests for letter glyphs
This commit is contained in:
parent
0da7bbf3d4
commit
4e63e2c415
@ -0,0 +1,203 @@
|
||||
namespace UglyToad.PdfPig.Tests.Integration.VisualVerification
|
||||
{
|
||||
using SkiaSharp;
|
||||
using System;
|
||||
using System.IO;
|
||||
using UglyToad.PdfPig.Tests.Integration.VisualVerification.SkiaHelpers;
|
||||
using Xunit;
|
||||
|
||||
public class GenerateLetterGlyphImages
|
||||
{
|
||||
private const string NonLatinAcrobatDistiller = "Single Page Non Latin - from acrobat distiller";
|
||||
private const string SingleGoogleDrivePage = "Single Page Simple - from google drive";
|
||||
private const string SinglePageFormattedType0Content = "Type0 Font";
|
||||
private const string SinglePageType1Content = "ICML03-081";
|
||||
private const string SingleInkscapePage = "Single Page Simple - from inkscape";
|
||||
private const string MotorInsuranceClaim = "Motor Insurance claim form";
|
||||
private const string PigProduction = "Pig Production Handbook";
|
||||
private const string SinglePage90ClockwiseRotation = "SinglePage90ClockwiseRotation - from PdfPig";
|
||||
private const string SinglePage180ClockwiseRotation = "SinglePage180ClockwiseRotation - from PdfPig";
|
||||
private const string SinglePage270ClockwiseRotation = "SinglePage270ClockwiseRotation - from PdfPig";
|
||||
private const string CroppedAndRotatedFile = "cropped-and-rotated";
|
||||
private const string MOZILLA_3136_0 = "MOZILLA-3136-0";
|
||||
|
||||
private const string OutputPath = "ImagesGlyphs";
|
||||
|
||||
private const float Scale = 2f;
|
||||
|
||||
private static readonly SKMatrix ScaleMatrix = SKMatrix.CreateScale(Scale, Scale);
|
||||
|
||||
public GenerateLetterGlyphImages()
|
||||
{
|
||||
if (!Directory.Exists(OutputPath))
|
||||
{
|
||||
Directory.CreateDirectory(OutputPath);
|
||||
}
|
||||
}
|
||||
|
||||
private static string GetFilename(string name)
|
||||
{
|
||||
var documentFolder = Path.GetFullPath(Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "..", "..", "..", "Integration", "Documents"));
|
||||
|
||||
if (!name.EndsWith(".pdf"))
|
||||
{
|
||||
name += ".pdf";
|
||||
}
|
||||
|
||||
return Path.Combine(documentFolder, name);
|
||||
}
|
||||
|
||||
private static void Run(string file, int pageNo = 1)
|
||||
{
|
||||
var pdfFileName = GetFilename(file);
|
||||
|
||||
using (var document = PdfDocument.Open(pdfFileName))
|
||||
{
|
||||
document.AddPageFactory<SKPicture, SkiaGlyphPageFactory>();
|
||||
|
||||
var page = document.GetPage(pageNo);
|
||||
|
||||
using (var picture = document.GetPage<SKPicture>(pageNo))
|
||||
{
|
||||
Assert.NotNull(picture);
|
||||
|
||||
var imageName = $"{file}_{pageNo}.png";
|
||||
var savePath = Path.Combine(OutputPath, imageName);
|
||||
|
||||
using (var fs = new FileStream(savePath, FileMode.Create))
|
||||
using (var image = SKImage.FromPicture(picture, new SKSizeI((int)(page.Width * Scale), (int)(page.Height * Scale)), ScaleMatrix))
|
||||
using (SKData d = image.Encode(SKEncodedImageFormat.Png, 100))
|
||||
{
|
||||
d.SaveTo(fs);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TIKA_1552_0_4()
|
||||
{
|
||||
Run("TIKA-1552-0", 4);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TIKA_1552_0_3()
|
||||
{
|
||||
Run("TIKA-1552-0",3);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void issue_671()
|
||||
{
|
||||
Run("issue_671");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void bold_italic()
|
||||
{
|
||||
Run("bold-italic");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void cat_genetics()
|
||||
{
|
||||
Run("cat-genetics");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void _68_1990_01_A()
|
||||
{
|
||||
Run("68-1990-01_A", 2);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SinglePageWithType1Content()
|
||||
{
|
||||
Run(SinglePageType1Content);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SinglePageSimpleFromInkscape()
|
||||
{
|
||||
Run(SingleInkscapePage);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SinglePageNonLatinFromAcrobatDistiller()
|
||||
{
|
||||
Run(NonLatinAcrobatDistiller);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SinglePageSimpleFromGoogleDrive()
|
||||
{
|
||||
Run(SingleGoogleDrivePage);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SinglePageType0Font()
|
||||
{
|
||||
Run(SinglePageFormattedType0Content);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void RotatedTextLibreOffice()
|
||||
{
|
||||
Run("Rotated Text Libre Office");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void MOZILLA_3136_0Test()
|
||||
{
|
||||
Run(MOZILLA_3136_0, 3);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void PigProductionCompactFontFormat()
|
||||
{
|
||||
Run(PigProduction);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void PopBugzilla37292()
|
||||
{
|
||||
Run("pop-bugzilla37292");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void MultiPageMortalityStatistics()
|
||||
{
|
||||
Run("Multiple Page - from Mortality Statistics");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void MotorInsuranceClaimForm()
|
||||
{
|
||||
Run(MotorInsuranceClaim);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SinglePage90ClockwiseRotationFromPdfPig()
|
||||
{
|
||||
Run(SinglePage90ClockwiseRotation);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SinglePage180ClockwiseRotationFromPdfPig()
|
||||
{
|
||||
Run(SinglePage180ClockwiseRotation);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SinglePage270ClockwiseRotationFromPdfPig()
|
||||
{
|
||||
Run(SinglePage270ClockwiseRotation);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CroppedAndRotatedTest()
|
||||
{
|
||||
Run(CroppedAndRotatedFile);
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,97 @@
|
||||
namespace UglyToad.PdfPig.Tests.Integration.VisualVerification.SkiaHelpers
|
||||
{
|
||||
using PdfPig.Core;
|
||||
using PdfPig.Graphics.Colors;
|
||||
using SkiaSharp;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
|
||||
internal static class SkiaExtensions
|
||||
{
|
||||
public static SKMatrix ToSkMatrix(this TransformationMatrix transformationMatrix)
|
||||
{
|
||||
return new SKMatrix((float)transformationMatrix.A, (float)transformationMatrix.C, (float)transformationMatrix.E,
|
||||
(float)transformationMatrix.B, (float)transformationMatrix.D, (float)transformationMatrix.F,
|
||||
0, 0, 1);
|
||||
}
|
||||
|
||||
public static SKColor ToSKColor(this IColor? pdfColor, decimal alpha)
|
||||
{
|
||||
var color = SKColors.Black;
|
||||
if (pdfColor != null)
|
||||
{
|
||||
var (r, g, b) = pdfColor.ToRGBValues();
|
||||
color = new SKColor(Convert.ToByte(r * 255), Convert.ToByte(g * 255), Convert.ToByte(b * 255));
|
||||
}
|
||||
|
||||
return color.WithAlpha(Convert.ToByte(alpha * 255));
|
||||
}
|
||||
|
||||
public static SKPath ToSKPath(this IReadOnlyList<PdfSubpath> path)
|
||||
{
|
||||
var skPath = new SKPath() { FillType = SKPathFillType.EvenOdd };
|
||||
foreach (var subpath in path)
|
||||
{
|
||||
foreach (var c in subpath.Commands)
|
||||
{
|
||||
if (c is PdfSubpath.Move move)
|
||||
{
|
||||
skPath.MoveTo((float)move.Location.X, (float)move.Location.Y);
|
||||
}
|
||||
else if (c is PdfSubpath.Line line)
|
||||
{
|
||||
skPath.LineTo((float)line.To.X, (float)line.To.Y);
|
||||
}
|
||||
else if (c is PdfSubpath.BezierCurve curve)
|
||||
{
|
||||
if (curve.StartPoint.Equals(curve.FirstControlPoint)) // TODO - This needs to be fixed in PdfPig
|
||||
{
|
||||
// Quad curve
|
||||
skPath.QuadTo((float)curve.SecondControlPoint.X, (float)curve.SecondControlPoint.Y,
|
||||
(float)curve.EndPoint.X, (float)curve.EndPoint.Y);
|
||||
}
|
||||
else
|
||||
{
|
||||
// Cubic curve
|
||||
skPath.CubicTo((float)curve.FirstControlPoint.X, (float)curve.FirstControlPoint.Y,
|
||||
(float)curve.SecondControlPoint.X, (float)curve.SecondControlPoint.Y,
|
||||
(float)curve.EndPoint.X, (float)curve.EndPoint.Y);
|
||||
}
|
||||
}
|
||||
else if (c is PdfSubpath.Close)
|
||||
{
|
||||
skPath.Close();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return skPath;
|
||||
}
|
||||
|
||||
public static SKPaintStyle? ToSKPaintStyle(this TextRenderingMode textRenderingMode)
|
||||
{
|
||||
// This is an approximation
|
||||
switch (textRenderingMode)
|
||||
{
|
||||
case TextRenderingMode.Stroke:
|
||||
case TextRenderingMode.StrokeClip:
|
||||
return SKPaintStyle.Stroke;
|
||||
|
||||
case TextRenderingMode.Fill:
|
||||
case TextRenderingMode.FillClip:
|
||||
return SKPaintStyle.Fill;
|
||||
|
||||
case TextRenderingMode.FillThenStroke:
|
||||
case TextRenderingMode.FillThenStrokeClip:
|
||||
return SKPaintStyle.StrokeAndFill;
|
||||
|
||||
case TextRenderingMode.NeitherClip:
|
||||
return SKPaintStyle.Stroke; // Not correct
|
||||
|
||||
case TextRenderingMode.Neither:
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,40 @@
|
||||
namespace UglyToad.PdfPig.Tests.Integration.VisualVerification.SkiaHelpers
|
||||
{
|
||||
using Content;
|
||||
using Outline.Destinations;
|
||||
using PdfPig.Core;
|
||||
using PdfPig.Filters;
|
||||
using PdfPig.Geometry;
|
||||
using PdfPig.Graphics.Operations;
|
||||
using PdfPig.Parser;
|
||||
using PdfPig.Tokenization.Scanner;
|
||||
using PdfPig.Tokens;
|
||||
using SkiaSharp;
|
||||
using System.Collections.Generic;
|
||||
|
||||
internal sealed class SkiaGlyphPageFactory : BasePageFactory<SKPicture>
|
||||
{
|
||||
public SkiaGlyphPageFactory(
|
||||
IPdfTokenScanner pdfScanner,
|
||||
IResourceStore resourceStore,
|
||||
ILookupFilterProvider filterProvider,
|
||||
IPageContentParser pageContentParser,
|
||||
ParsingOptions parsingOptions)
|
||||
: base(pdfScanner, resourceStore, filterProvider, pageContentParser, parsingOptions)
|
||||
{
|
||||
}
|
||||
|
||||
protected override SKPicture ProcessPage(int pageNumber, DictionaryToken dictionary, NamedDestinations namedDestinations,
|
||||
MediaBox mediaBox, CropBox cropBox, UserSpaceUnit userSpaceUnit, PageRotationDegrees rotation,
|
||||
TransformationMatrix initialMatrix, IReadOnlyList<IGraphicsStateOperation> operations)
|
||||
{
|
||||
// Special case where cropbox is outside mediabox: use cropbox instead of intersection
|
||||
var effectiveCropBox = new CropBox(mediaBox.Bounds.Intersect(cropBox.Bounds) ?? cropBox.Bounds);
|
||||
|
||||
var context = new SkiaGlyphStreamProcessor(pageNumber, ResourceStore, PdfScanner, PageContentParser,
|
||||
FilterProvider, effectiveCropBox, userSpaceUnit, rotation, initialMatrix, ParsingOptions);
|
||||
|
||||
return context.Process(pageNumber, operations);
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,222 @@
|
||||
namespace UglyToad.PdfPig.Tests.Integration.VisualVerification.SkiaHelpers
|
||||
{
|
||||
using Content;
|
||||
using PdfFonts;
|
||||
using PdfPig.Core;
|
||||
using PdfPig.Filters;
|
||||
using PdfPig.Geometry;
|
||||
using PdfPig.Graphics;
|
||||
using PdfPig.Graphics.Colors;
|
||||
using PdfPig.Graphics.Operations;
|
||||
using PdfPig.Parser;
|
||||
using PdfPig.Tokenization.Scanner;
|
||||
using PdfPig.Tokens;
|
||||
using SkiaSharp;
|
||||
using System.Collections.Generic;
|
||||
|
||||
internal sealed class SkiaGlyphStreamProcessor : BaseStreamProcessor<SKPicture>
|
||||
{
|
||||
private readonly SKMatrix yAxisFlipMatrix;
|
||||
private readonly int height;
|
||||
private readonly int width;
|
||||
|
||||
private SKCanvas canvas;
|
||||
|
||||
public SkiaGlyphStreamProcessor(int pageNumber, IResourceStore resourceStore, IPdfTokenScanner pdfScanner,
|
||||
IPageContentParser pageContentParser, ILookupFilterProvider filterProvider, CropBox cropBox,
|
||||
UserSpaceUnit userSpaceUnit, PageRotationDegrees rotation, TransformationMatrix initialMatrix,
|
||||
ParsingOptions parsingOptions)
|
||||
: base(pageNumber, resourceStore, pdfScanner, pageContentParser,
|
||||
filterProvider, cropBox, userSpaceUnit, rotation, initialMatrix,
|
||||
parsingOptions)
|
||||
{
|
||||
width = (int)cropBox.Bounds.Width;
|
||||
height = (int)cropBox.Bounds.Height;
|
||||
yAxisFlipMatrix = SKMatrix.CreateScale(1, -1, 0, height / 2f);
|
||||
}
|
||||
|
||||
public override SKPicture Process(int pageNumberCurrent, IReadOnlyList<IGraphicsStateOperation> operations)
|
||||
{
|
||||
CloneAllStates();
|
||||
|
||||
using (var recorder = new SKPictureRecorder())
|
||||
using (canvas = recorder.BeginRecording(SKRect.Create(width, height)))
|
||||
{
|
||||
canvas.Clear(SKColors.White);
|
||||
ProcessOperations(operations);
|
||||
canvas.Flush();
|
||||
return recorder.EndRecording();
|
||||
}
|
||||
}
|
||||
|
||||
public override void PopState()
|
||||
{
|
||||
base.PopState();
|
||||
canvas!.Restore();
|
||||
}
|
||||
|
||||
public override void PushState()
|
||||
{
|
||||
base.PushState();
|
||||
canvas!.Save();
|
||||
}
|
||||
|
||||
public override void RenderGlyph(IFont font,
|
||||
IColor strokingColor,
|
||||
IColor nonStrokingColor,
|
||||
TextRenderingMode textRenderingMode,
|
||||
double fontSize,
|
||||
double pointSize,
|
||||
int code,
|
||||
string unicode,
|
||||
long currentOffset,
|
||||
TransformationMatrix renderingMatrix,
|
||||
TransformationMatrix textMatrix,
|
||||
TransformationMatrix transformationMatrix,
|
||||
CharacterBoundingBox characterBoundingBox)
|
||||
{
|
||||
if (textRenderingMode == TextRenderingMode.Neither)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// TODO - Check if font is a vector font and Assert if result matches font.TryGetNormalisedPath(...)
|
||||
// We should be able to get the glyph path of all fonts that are vector fonts
|
||||
|
||||
if (font.TryGetNormalisedPath(code, out var path))
|
||||
{
|
||||
var skPath = path.ToSKPath();
|
||||
ShowVectorFontGlyph(skPath, strokingColor, nonStrokingColor, textRenderingMode, renderingMatrix,
|
||||
textMatrix, transformationMatrix);
|
||||
}
|
||||
else
|
||||
{
|
||||
// TODO - Just render the bounding box?
|
||||
}
|
||||
}
|
||||
|
||||
private void ShowVectorFontGlyph(SKPath path,
|
||||
IColor strokingColor,
|
||||
IColor nonStrokingColor,
|
||||
TextRenderingMode textRenderingMode,
|
||||
TransformationMatrix renderingMatrix,
|
||||
TransformationMatrix textMatrix,
|
||||
TransformationMatrix transformationMatrix)
|
||||
{
|
||||
var transformMatrix = renderingMatrix.ToSkMatrix()
|
||||
.PostConcat(textMatrix.ToSkMatrix())
|
||||
.PostConcat(transformationMatrix.ToSkMatrix())
|
||||
.PostConcat(yAxisFlipMatrix);
|
||||
|
||||
var style = textRenderingMode.ToSKPaintStyle();
|
||||
if (!style.HasValue)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var color = style == SKPaintStyle.Stroke ? strokingColor : nonStrokingColor;
|
||||
|
||||
using (var transformedPath = new SKPath())
|
||||
using (var fillBrush = new SKPaint())
|
||||
{
|
||||
fillBrush.Style = style.Value;
|
||||
fillBrush.Color = color.ToSKColor(GetCurrentState().AlphaConstantNonStroking);
|
||||
fillBrush.IsAntialias = true;
|
||||
path.Transform(transformMatrix, transformedPath);
|
||||
canvas!.DrawPath(transformedPath, fillBrush);
|
||||
}
|
||||
}
|
||||
|
||||
#region No op
|
||||
protected override void RenderXObjectImage(XObjectContentRecord xObjectContentRecord)
|
||||
{
|
||||
// No op
|
||||
}
|
||||
|
||||
public override void BeginSubpath()
|
||||
{
|
||||
// No op
|
||||
}
|
||||
|
||||
public override PdfPoint? CloseSubpath()
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
public override void StrokePath(bool close)
|
||||
{
|
||||
// No op
|
||||
}
|
||||
|
||||
public override void FillPath(FillingRule fillingRule, bool close)
|
||||
{
|
||||
// No op
|
||||
}
|
||||
|
||||
public override void FillStrokePath(FillingRule fillingRule, bool close)
|
||||
{
|
||||
// No op
|
||||
}
|
||||
|
||||
public override void MoveTo(double x, double y)
|
||||
{
|
||||
// No op
|
||||
}
|
||||
|
||||
public override void BezierCurveTo(double x1, double y1, double x2, double y2, double x3, double y3)
|
||||
{
|
||||
// No op
|
||||
}
|
||||
|
||||
public override void LineTo(double x, double y)
|
||||
{
|
||||
// No op
|
||||
}
|
||||
|
||||
public override void Rectangle(double x, double y, double width, double height)
|
||||
{
|
||||
// No op
|
||||
}
|
||||
|
||||
public override void EndPath()
|
||||
{
|
||||
// No op
|
||||
}
|
||||
|
||||
public override void ClosePath()
|
||||
{
|
||||
// No op
|
||||
}
|
||||
|
||||
public override void BeginMarkedContent(NameToken name, NameToken propertyDictionaryName, DictionaryToken properties)
|
||||
{
|
||||
// No op
|
||||
}
|
||||
|
||||
public override void EndMarkedContent()
|
||||
{
|
||||
// No op
|
||||
}
|
||||
|
||||
public override void BezierCurveTo(double x2, double y2, double x3, double y3)
|
||||
{
|
||||
// No op
|
||||
}
|
||||
|
||||
public override void ModifyClippingIntersect(FillingRule clippingRule)
|
||||
{
|
||||
// No op
|
||||
}
|
||||
|
||||
public override void PaintShading(NameToken shadingName)
|
||||
{
|
||||
// No op
|
||||
}
|
||||
|
||||
protected override void RenderInlineImage(InlineImage inlineImage)
|
||||
{
|
||||
// No op
|
||||
}
|
||||
#endregion
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user