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 Core;
using Fonts; using Fonts;
using Actions; using Actions;
using Filters;
using Graphics;
using Logging;
using PdfPig.Fonts.TrueType; using PdfPig.Fonts.TrueType;
using PdfPig.Fonts.Standard14Fonts; using PdfPig.Fonts.Standard14Fonts;
using PdfPig.Fonts.TrueType.Parser; using PdfPig.Fonts.TrueType.Parser;
using Outline; using Outline;
using Outline.Destinations; using Outline.Destinations;
using Parser;
using Parser.Parts;
using Tokenization.Scanner; using Tokenization.Scanner;
using Tokens; using Tokens;
@ -342,6 +347,7 @@ namespace UglyToad.PdfPig.Writer
} }
var page = document.GetPage(pageNumber); var page = document.GetPage(pageNumber);
var pcp = new PageContentParser(ReflectionGraphicsStateOperationFactory.Instance, true);
// copy content streams // copy content streams
var streams = new List<PdfPageBuilder.CopiedContentStream>(); var streams = new List<PdfPageBuilder.CopiedContentStream>();
@ -352,23 +358,54 @@ namespace UglyToad.PdfPig.Writer
var prev = context.AttemptDeduplication; var prev = context.AttemptDeduplication;
context.AttemptDeduplication = false; context.AttemptDeduplication = false;
context.WritingPageContents = true; context.WritingPageContents = true;
var contentReferences = new List<IndirectReferenceToken>();
if (contentsToken is ArrayToken array) if (contentsToken is ArrayToken array)
{ {
foreach (var item in array.Data) foreach (var item in array.Data)
{ {
if (item is IndirectReferenceToken ir) if (item is IndirectReferenceToken ir)
{ {
streams.Add(new PdfPageBuilder.CopiedContentStream( contentReferences.Add(ir);
(IndirectReferenceToken)WriterUtil.CopyToken(context, ir, document.Structure.TokenScanner, refs)));
} }
} }
} }
else if (contentsToken is IndirectReferenceToken ir) else if (contentsToken is IndirectReferenceToken ir)
{ {
streams.Add(new PdfPageBuilder.CopiedContentStream( contentReferences.Add(ir);
(IndirectReferenceToken)WriterUtil.CopyToken(context, ir, document.Structure.TokenScanner, refs)));
} }
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.AttemptDeduplication = prev;
context.WritingPageContents = false; context.WritingPageContents = false;
} }

View File

@ -140,16 +140,42 @@
contentStreams = new List<IPageContentStream>() { currentStream }; contentStreams = new List<IPageContentStream>() { currentStream };
} }
internal PdfPageBuilder(int number, PdfDocumentBuilder documentBuilder, IEnumerable<CopiedContentStream> copied, internal PdfPageBuilder(
Dictionary<NameToken, IToken> pageDict, List<(DictionaryToken token, PdfAction action)> links) 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.documentBuilder = documentBuilder ?? throw new ArgumentNullException(nameof(documentBuilder));
this.links = links; this.links = links;
PageNumber = number; PageNumber = number;
pageDictionary = pageDict; pageDictionary = pageDict;
contentStreams = new List<IPageContentStream>(); contentStreams = new List<IPageContentStream>(copied);
contentStreams.AddRange(copied);
currentStream = new DefaultContentStream(); 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); 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); destinationStream.Operations.AddRange(operations);
return this; return this;
@ -1090,9 +1132,18 @@
internal interface IPageContentStream : IContentStream internal interface IPageContentStream : IContentStream
{ {
bool ReadOnly { get; } bool ReadOnly { get; }
bool HasContent { get; } bool HasContent { get; }
void Add(IGraphicsStateOperation operation); void Add(IGraphicsStateOperation operation);
IndirectReferenceToken Write(IPdfStreamWriter writer); 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 internal class DefaultContentStream : IPageContentStream
@ -1109,8 +1160,11 @@
} }
public bool ReadOnly => false; public bool ReadOnly => false;
public bool HasContent => operations.Any(); public bool HasContent => operations.Any();
public TransformationMatrix? GlobalTransform => null;
public void Add(IGraphicsStateOperation operation) public void Add(IGraphicsStateOperation operation)
{ {
operations.Add(operation); operations.Add(operation);
@ -1139,11 +1193,18 @@
internal class CopiedContentStream : IPageContentStream internal class CopiedContentStream : IPageContentStream
{ {
private readonly IndirectReferenceToken token; private readonly IndirectReferenceToken token;
public bool ReadOnly => true; public bool ReadOnly => true;
public bool HasContent => true; public bool HasContent => true;
public CopiedContentStream(IndirectReferenceToken indirectReferenceToken) public TransformationMatrix? GlobalTransform { get; }
public CopiedContentStream(
IndirectReferenceToken indirectReferenceToken,
TransformationMatrix? globalTransform)
{ {
GlobalTransform = globalTransform;
token = indirectReferenceToken; token = indirectReferenceToken;
} }