Add visual verification tests for letter glyphs

This commit is contained in:
BobLd 2024-01-14 20:44:20 +00:00
parent 0da7bbf3d4
commit 4e63e2c415
4 changed files with 562 additions and 0 deletions

View File

@ -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);
}
}
}

View File

@ -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;
}
}
}
}

View File

@ -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);
}
}
}

View File

@ -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
}
}