when writing content to an existing page inverse any global transform #614

when adding a page to a builder from an existing document using either
addpage or copyfrom methods the added page's content stream can contain
a global transform matrix change that will subsequently change all the locations
of any modifications made by the user. here whenever using an existing stream
we apply the inverse of any active transformation matrix

there could be a bug here where if you use 'copy from' with a global transform
active, we then apply the inverse, and you use 'copy from' again to the same
destination page our inverse transform is now active and could potentially
affect the second stream, but I don't think it will
This commit is contained in:
EliotJones 2025-07-19 18:29:42 -05:00 committed by BobLd
parent ff4e763192
commit 377eb507e8
3 changed files with 144 additions and 10 deletions

View File

@ -0,0 +1,36 @@
namespace UglyToad.PdfPig.Writer;
using Core;
using Graphics.Operations;
using Graphics.Operations.SpecialGraphicsState;
using System;
using System.Collections.Generic;
internal static class PdfContentTransformationReader
{
public static TransformationMatrix? GetGlobalTransform(IEnumerable<IGraphicsStateOperation> operations)
{
TransformationMatrix? activeMatrix = null;
var stackDepth = 0;
foreach (var operation in operations)
{
if (operation is ModifyCurrentTransformationMatrix cm)
{
if (stackDepth == 0 && cm.Value.Length == 6)
{
activeMatrix = TransformationMatrix.FromArray(cm.Value);
}
}
else if (operation is Push push)
{
stackDepth++;
}
else if (operation is Pop pop)
{
stackDepth--;
}
}
return activeMatrix;
}
}

View File

@ -11,11 +11,16 @@ namespace UglyToad.PdfPig.Writer
using Core;
using Fonts;
using Actions;
using Filters;
using Graphics;
using Logging;
using PdfPig.Fonts.TrueType;
using PdfPig.Fonts.Standard14Fonts;
using PdfPig.Fonts.TrueType.Parser;
using Outline;
using Outline.Destinations;
using Parser;
using Parser.Parts;
using Tokenization.Scanner;
using Tokens;
@ -342,6 +347,7 @@ namespace UglyToad.PdfPig.Writer
}
var page = document.GetPage(pageNumber);
var pcp = new PageContentParser(ReflectionGraphicsStateOperationFactory.Instance, true);
// copy content streams
var streams = new List<PdfPageBuilder.CopiedContentStream>();
@ -352,23 +358,54 @@ namespace UglyToad.PdfPig.Writer
var prev = context.AttemptDeduplication;
context.AttemptDeduplication = false;
context.WritingPageContents = true;
var contentReferences = new List<IndirectReferenceToken>();
if (contentsToken is ArrayToken array)
{
foreach (var item in array.Data)
{
if (item is IndirectReferenceToken ir)
{
streams.Add(new PdfPageBuilder.CopiedContentStream(
(IndirectReferenceToken)WriterUtil.CopyToken(context, ir, document.Structure.TokenScanner, refs)));
contentReferences.Add(ir);
}
}
}
else if (contentsToken is IndirectReferenceToken ir)
{
streams.Add(new PdfPageBuilder.CopiedContentStream(
(IndirectReferenceToken)WriterUtil.CopyToken(context, ir, document.Structure.TokenScanner, refs)));
contentReferences.Add(ir);
}
foreach (var indirectReferenceToken in contentReferences)
{
// Detect any globally applied transforms to the graphics state from the content stream.
TransformationMatrix? globalTransform = null;
try
{
// If we don't manage to do this it's not the end of the world.
if (DirectObjectFinder.TryGet<StreamToken>(indirectReferenceToken, document.Structure.TokenScanner, out var contentStream))
{
var contentBytes = contentStream.Decode(DefaultFilterProvider.Instance);
var parsedOperations = pcp.Parse(0, new MemoryInputBytes(contentBytes), new NoOpLog());
globalTransform = PdfContentTransformationReader.GetGlobalTransform(parsedOperations);
}
}
catch
{
// Ignore and continue writing.
}
var updatedIndirect = (IndirectReferenceToken)WriterUtil.CopyToken(
context,
indirectReferenceToken,
document.Structure.TokenScanner,
refs);
streams.Add(new PdfPageBuilder.CopiedContentStream(updatedIndirect, globalTransform));
}
context.AttemptDeduplication = prev;
context.WritingPageContents = false;
}

View File

@ -140,16 +140,42 @@
contentStreams = new List<IPageContentStream>() { currentStream };
}
internal PdfPageBuilder(int number, PdfDocumentBuilder documentBuilder, IEnumerable<CopiedContentStream> copied,
Dictionary<NameToken, IToken> pageDict, List<(DictionaryToken token, PdfAction action)> links)
internal PdfPageBuilder(
int number,
PdfDocumentBuilder documentBuilder,
IEnumerable<CopiedContentStream> copied,
Dictionary<NameToken, IToken> pageDict,
List<(DictionaryToken token, PdfAction action)> links)
{
this.documentBuilder = documentBuilder ?? throw new ArgumentNullException(nameof(documentBuilder));
this.links = links;
PageNumber = number;
pageDictionary = pageDict;
contentStreams = new List<IPageContentStream>();
contentStreams.AddRange(copied);
currentStream = new DefaultContentStream();
contentStreams = new List<IPageContentStream>(copied);
var writeableContentStream = new DefaultContentStream();
if (contentStreams.Count > 0)
{
var lastGlobalTransform = contentStreams.LastOrDefault(x => x.GlobalTransform.HasValue);
if (lastGlobalTransform?.GlobalTransform != null)
{
var inverse = lastGlobalTransform.GlobalTransform.Value.Inverse();
writeableContentStream.Add(
new ModifyCurrentTransformationMatrix(
[
inverse.A,
inverse.B,
inverse.C,
inverse.D,
inverse.E,
inverse.F
]
));
}
}
currentStream = writeableContentStream;
contentStreams.Add(currentStream);
}
@ -987,6 +1013,22 @@
}
}
// Reset the graphics state to what we'd expect to be able to write our content in the correct locations.
var globalTransform = PdfContentTransformationReader.GetGlobalTransform(operations);
if (globalTransform.HasValue)
{
var inverse = globalTransform.Value.Inverse();
operations.Add(new ModifyCurrentTransformationMatrix(
[
inverse.A,
inverse.B,
inverse.C,
inverse.D,
inverse.E,
inverse.F
]));
}
destinationStream.Operations.AddRange(operations);
return this;
@ -1090,9 +1132,18 @@
internal interface IPageContentStream : IContentStream
{
bool ReadOnly { get; }
bool HasContent { get; }
void Add(IGraphicsStateOperation operation);
IndirectReferenceToken Write(IPdfStreamWriter writer);
/// <summary>
/// If this content stream applied any global transform to the graphics state this will
/// tell you which one is currently active at the end of this stream being applied.
/// </summary>
TransformationMatrix? GlobalTransform { get; }
}
internal class DefaultContentStream : IPageContentStream
@ -1109,8 +1160,11 @@
}
public bool ReadOnly => false;
public bool HasContent => operations.Any();
public TransformationMatrix? GlobalTransform => null;
public void Add(IGraphicsStateOperation operation)
{
operations.Add(operation);
@ -1139,11 +1193,18 @@
internal class CopiedContentStream : IPageContentStream
{
private readonly IndirectReferenceToken token;
public bool ReadOnly => true;
public bool HasContent => true;
public CopiedContentStream(IndirectReferenceToken indirectReferenceToken)
public TransformationMatrix? GlobalTransform { get; }
public CopiedContentStream(
IndirectReferenceToken indirectReferenceToken,
TransformationMatrix? globalTransform)
{
GlobalTransform = globalTransform;
token = indirectReferenceToken;
}