### ----------------------------------------------------
### -- PDF::Content
### -- Licenses: Artistic-2.0
### -- Authors: David Warring
### -- File: lib/PDF/Content/Ops.rakumod
### ----------------------------------------------------
### -- Chunk 1 of 2

#| A graphics machine for building and interpeting PDF Content operator streams
unit class PDF::Content::Ops;

=begin pod

=head2 Description

The PDF::Content::Ops role implements methods and mnemonics for the full operator table, as defined in specification [PDF 1.7 Appendix A]:

=begin table
* Operator * | *Mnemonic* | *Operands* | *Description*
b | CloseFillStroke | — | Close, fill, and stroke path using nonzero winding number rule
B | FillStroke | — | Fill and stroke path using nonzero winding number rule
b* | CloseEOFillStroke | — | Close, fill, and stroke path using even-odd rule
B* | EOFillStroke | — | Fill and stroke path using even-odd rule
BDC | BeginMarkedContentDict | tag properties | (PDF 1.2) Begin marked-content sequence with property list
BI | BeginImage | — | Begin inline image object
BMC | BeginMarkedContent | tag | (PDF 1.2) Begin marked-content sequence
BT | BeginText | — | Begin text object
BX | BeginExtended | — | (PDF 1.1) Begin compatibility section
c | CurveTo | x1 y1 x2 y2 x3 y3 | Append curved segment to path (two control points)
cm | ConcatMatrix | a b c d e f | Concatenate matrix to current transformation matrix
CS | SetStrokeColorSpace | name | (PDF 1.1) Set color space for stroking operations
cs | SetFillColorSpace | name | (PDF 1.1) Set color space for nonstroking operations
d | SetDashPattern | dashArray dashPhase | Set line dash pattern
d0 | SetCharWidth | wx wy | Set glyph width in Type 3 font
d1 | SetCharWidthBBox | wx wy llx lly urx ury | Set glyph width and bounding box in Type 3 font
Do | XObject | name | Invoke named XObject
DP | MarkPointDict | tag properties | (PDF 1.2) Define marked-content point with property list
EI | EndImage | — | End inline image object
EMC | EndMarkedContent | — | (PDF 1.2) End marked-content sequence
ET | EndText | — | End text object
EX | EndExtended | — | (PDF 1.1) End compatibility section
f | Fill | — | Fill path using nonzero winding number rule
F | FillObsolete | — | Fill path using nonzero winding number rule (obsolete)
f* | EOFill | — | Fill path using even-odd rule
G | SetStrokeGray | gray | Set gray level for stroking operations
g | SetFillGray | gray | Set gray level for nonstroking operations
gs | SetGraphicsState | dictName | (PDF 1.2) Set parameters from graphics state parameter dictionary
h | ClosePath | — | Close subpath
i | SetFlatness | flatness | Set flatness tolerance
ID | ImageData | — | Begin inline image data
j | SetLineJoin | lineJoin| Set line join style
J | SetLineCap | lineCap | Set line cap style
K | SetStrokeCMYK | c m y k | Set CMYK color for stroking operations
k | SetFillCMYK | c m y k | Set CMYK color for nonstroking operations
l | LineTo | x y | Append straight line segment to path
m | MoveTo | x y | Begin new subpath
M | SetMiterLimit | miterLimit | Set miter limit
MP | MarkPoint | tag | (PDF 1.2) Define marked-content point
n | EndPath | — | End path without filling or stroking
q | Save | — | Save graphics state
Q | Restore | — | Restore graphics state
re | Rectangle | x y width height | Append rectangle to path
RG | SetStrokeRGB | r g b | Set RGB color for stroking operations
rg | SetFillRGB | r g b | Set RGB color for nonstroking operations
ri | SetRenderingIntent | intent | Set color rendering intent
s | CloseStroke | — | Close and stroke path
S | Stroke | — | Stroke path
SC | SetStrokeColor | c1 … cn | (PDF 1.1) Set color for stroking operations
sc | SetFillColor | c1 … cn | (PDF 1.1) Set color for nonstroking operations
SCN | SetStrokeColorN | c1 … cn [name] | (PDF 1.2) Set color for stroking operations (ICCBased and special color spaces)
scn | SetFillColorN | c1 … cn [name] | (PDF 1.2) Set color for nonstroking operations (ICCBased and special color spaces)
sh | ShFill | name | (PDF 1.3) Paint area defined by shading pattern
T* | TextNextLine | — | Move to start of next text line
Tc | SetCharSpacing| charSpace | Set character spacing
Td | TextMove | tx ty | Move text position
TD | TextMoveSet | tx ty | Move text position and set leading
Tf | SetFont | font size | Set text font and size
Tj | ShowText | string | Show text
TJ | ShowSpaceText | array | Show text, allowing individual glyph positioning
TL | SetTextLeading | leading | Set text leading
Tm | SetTextMatrix | a b c d e f | Set text matrix and text line matrix
Tr | SetTextRender | render | Set text rendering mode
Ts | SetTextRise | rise | Set text rise
Tw | SetWordSpacing | wordSpace | Set word spacing
Tz | SetHorizScaling | scale | Set horizontal text scaling
v | CurveToInitial | x2 y2 x3 y3 | Append curved segment to path (initial point replicated)
w | SetLineWidth | lineWidth | Set line width
W | Clip | — | Set clipping path using nonzero winding number rule
W* | EOClip | — | Set clipping path using even-odd rule
y | CurveToFinal | x1 y1 x3 y3 | Append curved segment to path (final point replicated)
' | MoveShowText | string | Move to next line and show text
" | MoveSetShowText | aw ac string | Set word and character spacing, move to next line, and show text

=end table

=head Methods

=end pod

use PDF::IO::Writer;
use PDF::COS;
use PDF::COS::Dict;
use PDF::COS::Util :&from-ast, :&to-ast;
use PDF::Content::Matrix :inverse, :multiply, :is-identity, :TransformMatrix;
use PDF::Content::Tag;
use X::PDF::Content;
use JSON::Fast;

has Block @.callback is rw;
has Pair  @.ops;
has Bool  $.comment is rw = False;
multi method comment is rw { $!comment }
multi method comment($_ is raw) { self.add-comment($_); }
method comment-ops is rw is DEPRECATED('comment') { $!comment }
has Bool  $.trace   is rw = False;
has Bool  $.strict  is rw = True;
has $.canvas handles <resource-key resource-entry core-font use-font resources xobject-form tiling-pattern use-pattern width height> is required;
method parent is DEPRECATED<canvas> { $!canvas }
method owner  is DEPRECATED<canvas> { $!canvas }
has UInt $.extended-ops = 0;
has Numeric ($!cur-x, $!cur-y); # current point
has Numeric ($.tf-x = 0.0, $.tf-y = 0.0) is rw; # text flow
has Numeric ($!close-x, $!close-y); # closepath end-point
# some convenient mnemomic names
my Str enum OpCode is export(:OpCode) «
    :BeginImage<BI> :ImageData<ID> :EndImage<EI>
    :BeginMarkedContent<BMC> :BeginMarkedContentDict<BDC>
    :EndMarkedContent<EMC> :BeginText<BT> :EndText<ET>
    :BeginExtended<BX> :EndExtended<EX> :CloseEOFillStroke<b*>
    :CloseFillStroke<b> :EOFillStroke<B*> :FillStroke<B>
    :CurveTo<c> :ConcatMatrix<cm> :SetFillColorSpace<cs>
    :SetStrokeColorSpace<CS> :SetDashPattern<d> :SetCharWidth<d0>
    :SetCharWidthBBox<d1> :XObject<Do> :MarkPointDict<DP>
    :EOFill<f*> :Fill<f> :FillObsolete<F> :SetStrokeGray<G>
    :SetFillGray<g> :SetGraphicsState<gs> :ClosePath<h>
    :SetFlatness<i> :SetLineJoin<j> :SetLineCap<J> :SetFillCMYK<k>
    :SetStrokeCMYK<K> :LineTo<l> :MoveTo<m> :SetMiterLimit<M>
    :MarkPoint<MP> :EndPath<n> :Save<q> :Restore<Q> :Rectangle<re>
    :SetFillRGB<rg> :SetStrokeRGB<RG> :SetRenderingIntent<ri>
    :CloseStroke<s> :Stroke<S> :SetStrokeColor<SC>
    :SetFillColor<sc> :SetFillColorN<scn> :SetStrokeColorN<SCN>
    :ShFill<sh> :TextNextLine<T*> :SetCharSpacing<Tc>
    :TextMove<Td> :TextMoveSet<TD> :SetFont<Tf> :ShowText<Tj>
    :ShowSpaceText<TJ> :SetTextLeading<TL> :SetTextMatrix<Tm>
    :SetTextRender<Tr> :SetTextRise<Ts> :SetWordSpacing<Tw>
    :SetHorizScaling<Tz> :CurveToInitial<v> :EOClip<W*> :Clip<W>
    :SetLineWidth<w> :CurveToFinal<y> :MoveSetShowText<">
    :MoveShowText<'>
»;

my constant %OpCode is export(:OpName) = OpCode.enums.Hash;
my constant %OpName is export(:OpName) = OpCode.enums.invert.Hash;

# See [PDF 1.7 TABLE 4.1 Operator categories]
my constant GeneralGraphicOps = set <w J j M d ri i gs>;
my constant SpecialGraphicOps = set <q Q cm>;
my constant PathOps = set <m l c v y h re>;
my constant PaintingOps = set <S s f F f* B B* b b* n>;
my constant ClippingOps = set <W W*>;
my constant TextObjectOps = set <BT ET>;
my constant TextStateOps = set <Tc Tw Tz TL Tf Tr Ts>;
my constant TextOps = set <T* Td TD Tj TJ Tm ' ">;
my constant ColorOps = set <CS SC SCN G RG K cs sc scn g rg k>;
my constant MarkedContentOps = set <MP DP BMC BDC EMC>;
my constant CompatOps = set <BX EX>;
my constant FontOps = set <d0 d1>;

my constant Openers = set <q BT BMC BDC BX>;
my constant Closers = set <Q ET EMC EX>;
# Extended Graphics States (Resource /ExtGState entries)
# See [PDF 3200 Table 58 – Entries in a Graphics State Parameter Dictionary]
# These match PDF::ExtGState from PDF::Class
my enum ExtGState is export(:ExtGState) «
    :LineWidth<LW>
    :LineCap<LC>
    :LineJoinStyle<LJ>
    :MiterLimit<ML>
    :DashPattern<D>
    :RenderingIntent<RI>
    :OverPrintPaint<OP>
    :OverPrintStroke<op>
    :OverPrintMode<OPM>
    :Font<Font>
    :BlackGeneration-old<BG>
    :BlackGeneration<BG2>
    :UnderColorRemoval-old<UCR>
    :UnderColorRemoval<UCR2>
    :TransferFunction-old<TR>
    :TransferFunction<TR2>
    :Halftone<HT>
    :Flatness<FT>
    :Smoothness<SM>
    :StrokeAdjust<SA>
    :BlendMode<BM>
    :SoftMask<SMask>
    :StrokeAlpha<CA>
    :FillAlpha<ca>
    :AlphaSource<AIS>
    :TextKnockout<TK>
»;
constant %ExtGState = ExtGState.enums.Hash;

our Int enum TextMode is export(:TextMode) «
    :FillText(0) :OutlineText(1) :FillOutlineText(2)
    :InvisableText(3) :FillClipText(4) :OutlineClipText(5)
    :FillOutlineClipText(6) :ClipText(7)
»;

our Int enum LineCaps is export(:LineCaps) «
    :ButtCaps(0) :RoundCaps(1) :SquareCaps(2)
»;

our Int enum LineJoin is export(:LineJoin) «
    :MiterJoin(0) :RoundJoin(1) :BevelJoin(2)
»;

multi method new(:parent($canvas)!, |c) is DEPRECATED('new: :$canvas') {
    self.new: :$canvas, |c;
}

method graphics-accessor(Attribute $att, $setter) is rw {
    sub FETCH($) { $att.get_value(self) }
    sub STORE($, *@v) {
        self."$setter"(|@v)
        unless $att.get_value(self).Array eqv @v;
    }
    Proxy.new: :&FETCH, :&STORE;
}

method ext-graphics-accessor(Attribute $att, $key) is rw {
    sub FETCH($) { $att.get_value(self) }
    sub STORE($, \v) {
        unless $att.get_value(self) eqv v {
            given $!canvas {
                # Match any reusable GS entry; otherwise create a new entry
                my Str $gs-entry;
                with $!canvas<Resources> {
                    with .<ExtGState> -> $ext {
                        $gs-entry = $ext.keys.first: {
                            given $ext{$_} {
                                +.keys - (.<Type>:exists) == 1 && .{$key} eqv v;
                            }
                        }
                    }
                }
                $gs-entry //= $!canvas.resource-key: PDF::COS::Dict.COERCE: { :Type{ :name<ExtGState> }, $key => v };
                self.SetGraphicsState($gs-entry);
            }
        }
    }
    Proxy.new: :&FETCH, :&STORE;
}

my Attribute %GraphicVars;
my Str %ExtGStateEntries;
my Method %Store; # handles graphics state updates
BEGIN %Store = (
    OpCode::BeginText|OpCode::EndText => method {
        @!TextMatrix = [ 1, 0, 0, 1, 0, 0, ];
        $!tf-x = 0; $!tf-y = 0;
    },
    OpCode::Save => method {
        my %gstate := $.graphics-state;
        @!gsaves.push: %gstate;
    },
    OpCode::Restore => method {
        die X::PDF::Content::OP::BadNesting.new: :op<Q>, :mnemonic(%OpName<Q>), :opener("'q' (%OpName<q>)")
            unless @!gsaves;

        my %gstate = @!gsaves.pop;

        for %gstate.pairs {
            my Str $key       = .key;
            my Attribute $att = %GraphicVars{$key};
            my $val           = .value;
            $att.set_value(self, $val);
        }
    },
    OpCode::ConcatMatrix => method (*@mtx where TransformMatrix) {
        @!CTM = @mtx.&multiply: @!CTM;
    },
    OpCode::SetFillRGB => method ( \r, \g, \b) {
        $!FillColorSpace = 'DeviceRGB';
        @!FillColor = [r, g, b];
    },
    OpCode::SetStrokeRGB => method ( \r, \g, \b) {
        $!StrokeColorSpace = 'DeviceRGB';
        @!StrokeColor = [r, g, b];
    },
    OpCode::SetFillGray => method ( \gray) {
        $!FillColorSpace = 'DeviceGray';
        @!FillColor = [ gray, ];
    },
    OpCode::SetStrokeGray => method ( \gray) {
        $!StrokeColorSpace = 'DeviceGray';
        @!StrokeColor = [ gray, ];
    },
    OpCode::SetFillCMYK => method ( \c, \m, \y, \k) {
        $!FillColorSpace = 'DeviceCMYK';
        @!FillColor = [ c, m, y, k ];
    },
    OpCode::SetStrokeCMYK => method ( \c, \m, \y, \k) {
        $!StrokeColorSpace = 'DeviceCMYK';
        @!StrokeColor = [ c, m, y, k ];
    },
    OpCode::SetFillColor => method (*@!FillColor where self!color-args-ok('sc',  $_)) {
    },
    OpCode::SetStrokeColor => method (*@!StrokeColor where self!color-args-ok('SC',  $_)) {
    },
    OpCode::SetFillColorN => method (*@!FillColor where self!color-args-ok('scn',  $_)) {
    },
    OpCode::SetStrokeColorN => method (*@!StrokeColor where self!color-args-ok('SCN',  $_)) {
    },
    OpCode::BeginMarkedContent => method (Str $name) {
        self.open-tag: PDF::Content::Tag.new: :op<BMC>, :$name, :$.canvas, :start(+@!ops);
    },
    OpCode::BeginMarkedContentDict => method (Str $name, $p where Str|Hash) {
        my %attributes = .List with ($p ~~ Str ?? $.resource-entry('Properties', $p) !! $p );
        my UInt $mcid = $_ with %attributes<MCID>:delete;
        $!canvas.use-mcid($_) with $mcid;
        self.open-tag: PDF::Content::Tag.new: :op<BDC>, :$name, :%attributes, :$.canvas, :start(+@!ops), :$mcid;
    },
    OpCode::EndMarkedContent => method {
        die X::PDF::Content::OP::BadNesting.new: :op<EMC>, :mnemonic(%OpName<EMC>), :opener("'BMC' or 'BDC' (BeginMarkedContent)")
            unless @.open-tags;
        given self.close-tag {
            .end = +@!ops;
        }
    },
    OpCode::MarkPoint => method ( Str $name!) {
        my $start = my $end = +@!ops;
        self.add-tag: PDF::Content::Tag.new: :op<MP>, :$name, :$.canvas, :$start, :$end;
    },
    OpCode::MarkPointDict => method ( Str $name!, $p where Str|Hash) {
        my %attributes = .List with ($p ~~ Str ?? $.resource-entry('Properties', $p) !! $p );
        my UInt $mcid = $_ with %attributes<MCID>:delete;
        $!canvas.use-mcid($_) with $mcid;
        my $start = my $end = +@!ops;
        self.add-tag: PDF::Content::Tag.new: :op<DP>, :$name, :%attributes, :$.canvas, :$start, :$end, :$mcid;
    },
    OpCode::SetGraphicsState => method (Str $key) {
         given $!canvas {
            with .resource-entry('ExtGState', $key) {
                with .<CA>   { $!StrokeAlpha = $_ }
                with .<ca>   { $!FillAlpha = $_ }
                with .<D>    { @!DashPattern = .list }
                with .<Font> { $!Font = $_ }
                with .<FT>   { $!Flatness = $_ }
                with .<LC>   { $!LineCap = $_ }
                with .<LJ>   { $!LineJoin = $_ }
                with .<LW>   { $!LineWidth = $_ }
                with .<RI>   { $!RenderingIntent = $_ }
                with .<BM>   { $!BlendMode = $_ }
                with .<ML>   { $!MiterLimit = $_ }
            }
            else {
                die X::PDF::Content::UnknownResource.new: :type<ExtGState>, :$key;
            }
        }
    },
    OpCode::TextMove => method (Numeric $tx!, Numeric $ty) {
        self!text-move($tx, $ty);
    },
    OpCode::TextMoveSet => method (Numeric $tx!, Numeric $ty) {
        $!TextLeading = - $ty;
        self!text-move($tx, $ty);
    },
    OpCode::TextNextLine => method {
        self!new-line();
    },
    OpCode::MoveShowText => method ($) {
        self!new-line();
    },
    OpCode::MoveSetShowText => method ($!WordSpacing, $!CharSpacing, $) {
        self!new-line();
    },
    OpCode::SetCharWidth => method ($!char-width, $!char-height) {
    },
    OpCode::SetCharWidthBBox => method ($!char-width, $!char-height, *@!char-bbox) {
    },
    OpCode::BeginExtended => method {
        $!extended-ops++;
    },
    OpCode::EndExtended => method {
        die X::PDF::Content::OP::BadNesting.new: :op<EX>, :mnemonic(%OpName<EX>), :opener("'BX' (BeginExtended)")
            unless $!extended-ops;
        $!extended-ops--;
    },
    # path construction operators
    OpCode::MoveTo => method ($!cur-x, $!cur-y) {
        $!close-x = $!cur-x;
        $!close-y = $!cur-y;
    },
    OpCode::Rectangle => method ($!cur-x, $!cur-y, $, $) {
        $!close-x = $!cur-x;
        $!close-y = $!cur-y;
    },
    OpCode::LineTo => method ($!cur-x, $!cur-y) { },
    OpCode::CurveTo => method ($, $, $, $, $!cur-x, $!cur-y) { },
    OpCode::CurveToInitial | OpCode::CurveToFinal => method ($, $, $!cur-x, $!cur-y) { },
    OpCode::ClosePath => method {
        $!cur-x = $!close-x;
        $!cur-y = $!close-y;
    },

);

multi trait_mod:<is>(Attribute $att, :stored(&meth)!) {
    my \setter = 'Set' ~ $att.accessor-name;
    my Str \op = %OpCode{setter}
        or die "No OpCode::{setter} entry for {$att.name}";
    %Store{op} = &meth;
}

multi trait_mod:<is>(Attribute $att, Bool :$graphics! where .so) {

    my role GraphicsAttHOW {
        method accessor-name { self.name.substr(2) }
        method compose(Mu \package) {
            my \meth-name = self.accessor-name;
            nextsame
                if package.^declares_method(meth-name)
                || ! (self.has_accessor && self.rw);
            my \setter = 'Set' ~ meth-name;
            package.^add_method( meth-name, sub (\obj) is rw { obj.graphics-accessor( self, setter ); } );
        }
    }

    $att does GraphicsAttHOW;
    %GraphicVars{$att.accessor-name} = $att;
}

multi trait_mod:<is>(Attribute $att, Bool :$ext-graphics! where .so) {

    my role ExtGraphicsAttHOW {
        method accessor-name { self.name.substr(2) }
        has Str $.key is rw;
        method compose(Mu \package) {
            my \meth-name = self.accessor-name;
            nextsame
                if package.^declares_method(meth-name)
                || ! (self.has_accessor && self.rw);
            package.^add_method( meth-name, sub (\obj) is rw { obj.ext-graphics-accessor( self, self.key ); } );
        }
    }

    $att does ExtGraphicsAttHOW;
    given $att.accessor-name {
        my $key := ExtGState.enums{$_}
            or die "no ExtGState::$_ enumeration";

        $att.key = $key;
        %ExtGStateEntries{$_} = $key;
        %GraphicVars{$_} = $att;
    }
}

# *** TEXT STATE ***
has Numeric $.CharSpacing   is graphics is stored(method ($!CharSpacing)  {}) is rw = 0;
has Numeric $.WordSpacing   is graphics is stored(method ($!WordSpacing)  {}) is rw = 0;
has Numeric $.HorizScaling  is graphics is stored(method ($!HorizScaling) {}) is rw = 100;
has Numeric $.TextLeading   is graphics is stored(method ($!TextLeading)  {}) is rw = 0;
has Numeric $.TextRender    is graphics is stored(method ($!TextRender)   {}) is rw = 0;
has Numeric $.TextRise      is graphics is stored(method ($!TextRise)     {}) is rw = 0;
has Numeric @.TextMatrix    is graphics is stored(method (*@!TextMatrix)  {$!tf-x = 0; $!tf-y = 0;}) is rw = [ 1, 0, 0, 1, 0, 0, ];
has Array   $.Font          is graphics is stored(
    method (Str $key, Numeric $size!) {
        with $!canvas.resource-entry('Font', $key) -> \font-dict {
            $!Font = [font-dict, $size];
        }
        else {
            die X::PDF::Content::UnknownResource.new: :type<Font>, :$key;
        }
    }) is rw;
#| returns the current graphics font dictionary resource
method font-face returns PDF::COS::Dict {$!Font[0] // PDF::COS::Dict}
#| returns the current graphics font size
method font-size returns Numeric {$!Font[1] // Numeric}

# *** Graphics STATE ***
has Numeric @.CTM is graphics = [ 1, 0, 0, 1, 0, 0, ];      # graphics matrix;
method CTM is rw {
    sub FETCH($) { @!CTM }
    sub STORE($, List $lval) {
        # matrices are usually concatonated, but allow assignment. Work backwards
        # finding a @diff matrix that concatonates to @!CTM to produce $lval
        my @diff = $lval.&multiply: @!CTM.&inverse();
        self.ConcatMatrix: |@diff
            unless @diff.&is-identity();
    }
    Proxy.new: :&FETCH, :&STORE;
}
has Numeric $.LineWidth   is graphics is stored(method ($!LineWidth) {}) is rw = 1.0;
has UInt    $.LineCap     is graphics is stored(method ($!LineCap) {}) is rw = ButtCaps;
has UInt    $.LineJoin    is graphics is stored(method ($!LineJoin) {}) is rw = MiterJoin;
has         @.DashPattern is graphics is stored(
    method (Array $a, Numeric $p ) {
        @!DashPattern = [ $a.clone, $p];
    }
) is rw = [[], 0];

sub device-colorspace(Str $_) {
    my Str $cs := .substr(6) if .starts-with('Device');
    $cs ~~ 'RGB'|'Gray'|'CMYK' ?? $cs !! Str;
}

has Str $.StrokeColorSpace is graphics is stored(method ($!StrokeColorSpace) {}) is rw = 'DeviceGray';
has @!StrokeColor is graphics = [0.0];
method StrokeColor is rw {
    sub FETCH($) { $!StrokeColorSpace => @!StrokeColor }
    sub STORE($, Pair $_) {
        my Str $key = .key ~~ Str ?? .key !! $.resource-key(.key);
        unless $key eq $!StrokeColorSpace && .value eqv @!StrokeColor {
            my $cs := device-colorspace($key);
            if $cs {
                self."SetStroke$cs"(|.value.clone);
            }
            else {
                self.SetStrokeColorSpace($key);
                self.SetStrokeColorN(|.value.clone);
            }
        }
    }
    Proxy.new: :&FETCH, :&STORE;
}

has Str $.FillColorSpace is graphics is stored(method ($!FillColorSpace) { }) is rw = 'DeviceGray';
has @!FillColor is graphics = [0.0];
method FillColor is rw {
    sub FETCH($) {$!FillColorSpace => @!FillColor}
    sub STORE($, Pair $_) {
        my Str $key = .key ~~ Str ?? .key !! $.resource-key(.key);
        unless $key eq $!FillColorSpace && .value eqv @!FillColor {
            my $cs := device-colorspace($key);
            if $cs {
                self."SetFill$cs"(|.value.clone);
            }
            else {
                self.SetFillColorSpace($key);
                self.SetFillColorN(|.value.clone);
            }
        }
    }
    Proxy.new: :&FETCH, :&STORE;
}

my subset RenderingIntention of Str where 'AbsoluteColorimetric'|'RelativeColorimetric'|'Saturation'|'Perceptual';
has RenderingIntention $.RenderingIntent is graphics is stored(method ($!RenderingIntent)  {}) is rw = 'RelativeColorimetric';

my subset FlatnessTolerance of Numeric where 0 .. 100;
has FlatnessTolerance $.Flatness is graphics is stored(method ($!Flatness)  {}) is rw = 0;

# *** Extended Graphics STATE ***
has Numeric $.StrokeAlpha is ext-graphics is rw = 1.0;
has Numeric $.FillAlpha   is ext-graphics is rw = 1.0;
has Str $.BlendMode is ext-graphics is rw = 'Normal';
has Numeric $.MiterLimit is graphics is stored(method ($!MiterLimit) {}) is rw = 10.0;

#| returns the current tags status
method tags returns PDF::Content::Tag::NodeSet handles<open-tag close-tag add-tag open-tags closed-tag artifact reversed-chars descendants> {
    $!canvas.tags;
}

# *** Graphics Stack ***
sub delta(@gs) {
    loop (my $n = @gs - 1; $n >= 1; $n--) {
        @gs[$n] .= clone;
        for @gs[$n].keys {
            @gs[$n]{$_}:delete
                if @gs[$n]{$_} eqv @gs[$n-1]{$_};
        }
    }
    @gs.shift;
    @gs;
}

has @.gsaves;
#| return graphics gsave stack, including changed variables only

multi method gsaves(:$delta! where .so --> Array) {
    my @gs = @!gsaves;
    @gs.push: $.graphics-state;
    @gs.&delta();
}
#| return graphics gsave stack, including all graphics variables
multi method gsaves returns Array { @!gsaves }

# looks too much like a verb
method gsave is DEPRECATED('gsaves') { @!gsaves }

#| return locally updated graphics state variables
multi method graphics-state(:$delta! --> Hash) {
    with @!gsaves.tail {
        [$_, $.graphics-state].&delta().tail;
    }
    else {
        Nil;
    }
}
#| return all current graphics state variables
multi method graphics-state {
    %(
        %GraphicVars.pairs.map: {
            my Str $key       = .key;
            my Attribute $att = .value;
            my $val           = $att.get_value(self);
            $val .= clone if $val ~~ Array;
            $key => $val;
        }
    )
}

# *** Type 3 Font Metrics ***

has Numeric $.char-width;
has Numeric $.char-height;
has Numeric @.char-bbox[4];

# States and transitions in [PDF 32000 Figure 9 – Graphics Objects]
my enum GraphicsContext is export(:GraphicsContext) <Path Text Clipping Page Shading Image>;

has GraphicsContext $.context = Page;

method !unexpected-op($op) is hidden-from-backtrace {
    # Found an op we didn't expect. Raise a warning.
    my $type;
    my $where;
    if $!context == Text && $op ∈ SpecialGraphicOps {
        $type = 'Special graphics';
        $where = 'in a BT ... ET text block';
    }
    elsif $op ∈ TextOps {
        $type = 'Text operation';
        $where = 'outside of a BT ... ET text block';
    }
    else {
        $type = 'Unexpected';
        $where = '(first operation)';

        loop (my int $n = +@!ops-1; $n >= 0; $n--) {
            with @!ops[$n].key {
                unless $_ ~~ 'comment' {
                    $where = "in $!context context, following '$_' (%OpName{$_})";
                    last;
                }
            }
        }
    }
    warn X::PDF::Content::OP::Unexpected.new: :$type, :$op, :mnemonic(%OpName{$op}), :$where;
}

method !track-context(Str $op) is hidden-from-backtrace {

    my constant %Transition = %(
        'BT'     => ( (Page) => Text ),
        'ET'     => ( (Text) => Page ),

        'BI'     => ( (Page) => Image ),
        'EI'     => ( (Image) => Page ),

        'W'|'W*' => ( (Path) => Clipping ),
        'm'|'re' => ( (Page) => Path ),
        any(PaintingOps.keys) => ( (Clipping|Path) => Page ),
    );

    my constant %InSitu = %(
       (Path) => PathOps ∪ CompatOps,
       (Text) => TextOps ∪ TextStateOps ∪ GeneralGraphicOps ∪ ColorOps ∪ MarkedContentOps ∪ CompatOps,
       (Page) => TextStateOps ∪ SpecialGraphicOps ∪ GeneralGraphicOps ∪ ColorOps ∪ MarkedContentOps ∪ CompatOps ∪ FontOps ∪ <sh Do>.Set,
       (Image) => <ID>.Set,
    );

    my $rv = $!context;
    my Bool $ok-here = False;
    $ok-here = $op ∈ $_
        with %InSitu{$!context};

    with %Transition{$op} {
        $ok-here ||= ?(.key == $rv);
        $rv = .value;
    }

    self!unexpected-op($op)
        if !$ok-here && $!strict;

    $rv;
}

my Routine %Ops = BEGIN %(

    # BI dict ID stream EI
    'BI' => sub (Str) {
        [ ];
    },

    'ID' => sub (Str, Hash $dict = {}, Str $encoded = '') {
        [ :$dict, :$encoded ];
    },

    'EI' => sub ($op) { $op => [] },

    # unary operators
    'BT'|'ET'|'EMC'|'BX'|'EX'|'b*'|'b'|'B*'|'B'|'f*'|'F'|'f'
        |'h'|'n'|'q'|'Q'|'s'|'S'|'T*'|'W*'|'W' => sub ($op) {
        [];
    },

    # tag                     BMC | MP
    # name                    cs | CS | Do | sh
    # dictname                gs
    # intent                  ri
    'BMC'|'cs'|'CS'|'Do'|'gs'|'MP'|'ri'|'sh' => sub (Str, Str $name!) {
         [ :$name ]
    },

    # string                  Tj | '
    'Tj'|"'" => sub (Str, Str $literal!) {
        [ :$literal ]
     },

    # array                   TJ
    'TJ' => sub (Str $op, Array $args!) {
        my @array = $args.map({
            when Str     { :literal($_) }
            when Numeric { .Int }
            when Pair    { $_ }
            default {die X::PDF::Content::OP::BadArrayArg.new: :$op, :mnemonic(%OpName{$op}), :arg($_);}
        });
        [ :@array ];
    },

    # name num                Tf
    'Tf' => sub (Str, Str $name!, Numeric $size!) {
        [ :$name, $size ]
    },

    # tag [dict|name]         BDC | DP
    'BDC'|'DP' => sub (Str, Str $name!, $p! where Hash|Str