diff --git a/test_data/visual_regression/data.pdf b/test_data/visual_regression/data.pdf index 1acf1cb..237e275 100644 --- a/test_data/visual_regression/data.pdf +++ b/test_data/visual_regression/data.pdf @@ -22,7 +22,7 @@ endobj endobj 5 0 obj << -/Contents 15 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 14 0 R /Resources << +/Contents 20 0 R /MediaBox [ 0 0 595.2 841.68 ] /Parent 19 0 R /Resources << /Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ] >> /Rotate 0 /Trans << @@ -47,7 +47,7 @@ endobj endobj 9 0 obj << -/Contents 16 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 14 0 R /Resources << +/Contents 21 0 R /MediaBox [ 0 0 595.2 841.68 ] /Parent 19 0 R /Resources << /Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ] >> /Rotate 0 /Trans << @@ -57,7 +57,7 @@ endobj endobj 10 0 obj << -/Contents 17 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 14 0 R /Resources << +/Contents 22 0 R /MediaBox [ 0 0 595.2 841.68 ] /Parent 19 0 R /Resources << /Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ] >> /Rotate 0 /Trans << @@ -67,7 +67,7 @@ endobj endobj 11 0 obj << -/Contents 18 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 14 0 R /Resources << +/Contents 23 0 R /MediaBox [ 0 0 595.2 841.68 ] /Parent 19 0 R /Resources << /ExtGState << /gRLs0 << /ca .6 @@ -83,79 +83,136 @@ endobj endobj 12 0 obj << -/PageMode /UseNone /Pages 14 0 R /Type /Catalog +/BitsPerComponent 8 /ColorSpace /DeviceGray /Filter [ /ASCII85Decode /FlateDecode ] /Height 1340 /Length 1741 /Subtype /Image + /Type /XObject /Width 1052 >> +stream +Gb"0=!=8`+$j<9i?i&Aa-s0]RWiE)!WiE)!WiE)!WiE)!WiE)!WiE)!WiE)!WiE)!WiE)!WiE)!WiE)!WiE)!WiE)!WiE)!WiE)!WiE)!WiE)!WiE)!WiE)!WiE)!WiE)!WiE)!WiE)!WiE)!WiE)!WiE)!WiE)!WiE)!WiE)!WiE)!WiE)!WiE)!WiE)!WiE)!WiE)!WiE)!WiE)!WiE)!WiE)!WiE)!WiE)!WiE)!WiE)!WiE)!WiE)!WiE)!WiE)!WiE)!WiE)!WiE)!WiE)!WiE)!WiE)!WiE)!WiE)!WiE)!WiE)!WiE)!WiE)!WiE)!WiE)!WiE)!WiE)!WiE)!WiE)!WiE)!WiE)!WiE)!WiE)!WiE)!WiE)!WiE)!WiE)!WiE)!WiE)!WiE)!WiE)!WiE)!WiE)!WiE)!WiE)!WiE)!WiE)!WiE)!WiE)!WiE)!WiE)!WiE)!WiE)!WiE)!WiE)!WiE)!WiE)!WiE)!WiE)!WiE)!WiE)!WiE)!WiE)!WiE)!WiE)!WiE)!WiE)!WiE)!WiE)!WiE)!WiE)!WiE)!WiE)!WiE)!WiE)!WiE)!WiE)!WiE)!WiE)!WiE)!WiE)!WiE)!WiE)!WiE)!WiE)!WiE)!WiE)!WiE)!WiE)!WiE)!WiE)!WiE)!WiE)!WiE)!WiE)!WiE)!WiE)!WiE)!WiE)!WiE)!WiE)!WiE)!WiE)!WiE)!WiE)!WiE)!WiE)!WiE)!WiE)!WiE)!WiE)!WiE)!WiE)!WiE)!WiE)!WiE)!WiE)!WiE)!WiE)!WiE)!WiE)!WiE)!WiE)!WiE)!WiE)!WiE)!WiE)!WiE)!WiE)!WiE)!WiE)!WiE)!WiE)!WiE)!WiE)!WiE)!WiE)!WiE)!WiE)!WiE)!WiE)!WiE)!WiE)!WiE)!WiE)!WiE)!WiE)!WiE)!WiE)!WiE)!WiE)!WiE)!WiE)!WiE)!WiE)!WiE)!WiE)!WiE)!WiE)!WiE)!WiE)!WiE)!WiE)!WiE)!WiE)!WiE)!WiE)!WiE)!WiE)!WiE)!WiE)!WiE)!WiE)!WiE)!WiE)!WiE)!WiE)!WiE)!WiE)!WiE)!WiE)!WiE)!WiE)!WiE)!WiE)!WiE)!WiE)!WiE)!WiE)!WiE)!WiE)!WiE)!WiE)!WiE)!WiE)!WiE)!WiE)!WiE)!WiE)!WiE)!WiE)!WiE)!WiE)!WiE)!WiE)!WiE)!WiE)!WiE)!WiE)!WiE)!WiE)!WiE)!WiE)!WiE)!WiE)!WiE)!WiE)!WiE)!WiE)!WiE)!WiE)!WiE)!WiE)!WiE)!WiE)!WiE)!WiE)!WiE)!WiE)!WiE)!WiE)!WiE)!WiE)!WiE)!WiE)!WiE)!WiE)!WiE)!WiE)!WiE)!WiE)!WiE)!WiE)!WiE)!WiE)!WiE)!WiE)!WiE)!WiE)!WiE)!WiE)!WiE)!WiE)!WiE)!WiE)!WiE)!WiE)!WiE)!WiE)!WiE)!WiE)!WiE)!WiE)!WiE)!WiE)!WiE)!WiE)!WiE)!WiE)!WiE)!WiE)!WiE)!WiE)!WiE)!WiE)!WiE)!WiE)!WiE)!WiE)!WiE)!WiE)!WiE)!WiE)!WiE)!WiE)!WiE)!WiE)!WiE)!WiE)!WiE)!WiE)!WiE)!WiE)!WiE)!WiE)!WiE)!WiE)!WiE)!WiE)!WiE)!WiE)!WiE)!WiE)!WiE)!WiE)!lK\Zi3%&_~>endstream endobj 13 0 obj << +/BitsPerComponent 8 /ColorSpace /DeviceGray /Filter [ /ASCII85Decode /FlateDecode ] /Height 1340 /Length 4220 /Subtype /Image + /Type /XObject /Width 1052 +>> +stream +Gb"0P5n*f:!!=tRRg'8uQ:kBr4U1uFcNO=D[!iA_d'8$-!78!"$NSPYIL?/&Z$m&\d'8$-!78!"$NSPYIL?/&Z$m&\d'8$-!78!"$NSPYIL?/&Z$m&\d'8$-!78!"$NSPYIL?/&Z$m&\d'8$-!78!"$NSPYIL?/&Z$m&\d'8$-!78!"$NSPYIL?/&Z$m&\d'8$-!78!"$NSPYIL?/&Z$m&\d'8$-!78!"$NSPYIL?/&Z$m&\d'8$-!78!"$NSPYIL?/&Z$m&\d'8$-!78!"$NSPYIL?/&Z$m&\d'8$-!78!"$NSPYIL?/&Z$m&\d'8$-!78!"$NSPYIL?/&Z$m&\d'8$-!78!"$NSPYIL?/&Z$m&\d'8$-!78!"$NSPYIL?/&Z$m&\d'8$-!78!"$NSPYIL?/&Z$m&\d'8$-!78!"$NSPYIL?/&Z$m&\d'8$-!78!"$NSPYIL?/&Z$m&\d'8$-!78!"$NSPYIL?/&Z$m&\d'8$-!78!"$NSPYIL?/&Z$m&\d'8$-!78!"$NSPYIL?/&Z$m&\d'8$-!78!"$NSPYIL?/&Z$m&\d'8$-!78!"$NSPYIL?/&Z$m&\d'8$-!78!"$NSPYIL?/&Z$m&\d'8$-!78!"$NSPYIL?/&Z$m&\d'8$-!78!"$NSPYIL?/&Z$m&\d'8$-!78!"$NSPYIL?/&Z$m&\d'8$-!78!"$NSPYIL?/&Z$m&\d'8$-!78!"$NSPYIL?/&Z$m&\d'8$-!78!"$NSPYIL?/&Z$m&\d'8$-!78!"$NSPYIL?/&Z$m&\d'8$-!78!"$NSPYIL?/&Z$m&\d'8$-!78!"$NSPYIL?/&Z$m&\d'8$-!78!"$NSPYIL?/&Z$m&\d'8$-!78!"$NSPYIL?/&Z$m&\d'8$-!78!"$NSPYIL?/&Z$m&\d'8$-!78!"$NSPYIL?/&Z$m&\d'8$-!78!"$NSPYIL?/&Z$m&\d'8$-!78!"$NSPYIL?/&Z$m&\d'8$-!78!"$NSPYIL?/&Z$m&\d'8$-!78!"$NSPYIL?/&Z$m&\d'8$-!78!"$NSPYIL?/&Z$m&\d'8$-!78!"$NSPYIL?/&Z$m&\d'8$-!78!"$NSPYIL?/&Z$m&\d'8$-!78!"$NSPYIL?/&Z$m&\d'8$-!78!"$NSPYIL?/&Z$m&\d'8$-!78!"$NSPYIL?/&Z$m&\d'8$-!78!"$NSPYIL?/&Z$m&\d'8$-!78!"$NSPYIL?/&Z$m&\d'8$-!78!"$NSPYIL?/&Z$m&\d'8$-!78!"$NSPYIL?/&Z$m&\d'8$-!78!"$NSPYIL?/&Z$m&\d'8$-!78!"$NSPYIL?/&Z$m&\d'8$-!78!"$NSPYIL?/&Z$m&\d'8$-!78!"$NSPYIL?/&Z$m&\d'8$-!78!"$NSPYIL?/&Z$m&\d'8$-!78!"$NSPYIL?/&Z$m&\d'8$-!78!"$NSPYIL?/&Z$m&\d'8$-!78!"$NSPYIL?/&Z$m&\d'8$-!78!"$NSPYIL?/&Z$m&\d'8$-!78!"$NSPYIL?/&Z$m&\d'8$-!78!"$NSPYIL?/&Z$m&\d'8$-!78!"$NSPYIL?/&Z$m&\d'8$-!78!"$NSPYIL?/&Z$m&\d'8$-!78!"$NSPYIL?/&Z$m&\d'8$-!78!"$NSPYIL?/&Z$m&\d'8$-!78!"$NSPYIL?/&Z$m&\d'8$-!78!"$NSPYIL?/&Z$m&\d'8$-!78!"$NSPYIL?/&Z$m&\d'8$-!78!"$NSPYIL?/&Z$m&\d'8$-!78!"$NSPYIL?/&Z$m&\d'8$-!78!"$NSPYIL?/&Z$m&\d'8$-!78!"$NSPYIL?/&Z$m&\d'8$-!78!"$NSPYIL?/&Z$m&\d'8$-!78!"$NSPYIL?/&Z$m&\d'8$-!78!"$NSPYIL?/&Z$m&\d'8$-!78!"$NSPYIL?/&Z$m&\d'8$-!78!"$NSPYIL?/&Z$m&\d'8$-!78!"$NSPYIL?/&Z$m&\d'8$-!78!"$NSPYIL?/&Z$m&\d'8$-!78!"$NSPYIL?/&Z$m&\d'8$-!78!"$NSPYIL?/&Z$m&\d'8$-!78!"$NSPYIL?/&Z$m&\d'8$-!78!"$NSPYIL?/&Z$m&\d'8$-!78!"$NSPYIL?/&Z$m&\d'8$-!78!"$NSPYIL?/&Z$m&\d'8$-!78!"$NSPYIL?/&Z$m&\d'8$-!78!"$NSPYIL?/&Z$m&\d'8$-!78!"$NSPYIL?/&Z$m&\d'8$-!78!"$NSPYIL?/&Z$m&\d'8$-!78!"$NSPYIL?/&Z$m&\d'8$-!78!"$NSPYIL?/&Z$m&\d'8$-!78!"$NSPYIL?/&Z$m&\d'8$-!78!"$NSPYIL?/&Z$m&\d'8$-!78!"$NSPYIL?/&Z$m&\d'8$-!78!"$NSPYIL?/&Z$m&\d'8$-!78!"$NSPYIL?/&Z$m&\d'8$-!78!"$NSPYIL?/&Z$m&\d'8$-!78!"$NSPYIL?/&Z$m&\d'8$-!78!"$NSPYIL?/&Z$m&\d'8$-!78!"$NSPYIL?/&Z$m&\d'8$-!78!"$NSPYIL?/&Z$m&\d'8$-!78!"$NSPYIL?/&Z$m&\d'8$-!78!"$NSPYIL?/&Z$m&\d'8$-!78!"$NSPYIL?/&Z$m&\d'8$-!78!"$NSPYIL?/&Z$m&\d'8$-!78!"$NSPYIL?/&Z$m&\d'8$-!78!"$NSPYIL?/&Z$m&\d'8$-!78!"$NSPYIL?/&Z$m&\d'8$-!78!"$NSPYIL?/&Z$m&\d'8$-!78!"$NSPYIL?/&Z$m&\d'8$-!78!"$NSPYIL?/&Z$m&\d'8$-!78!"$NSPYIL?/&Z$m&\d'8$-!78!"$NSPYIL?/&Z$m&\d'8$-!78!"$NSPYIL?/&Z$m&\d'8$-!78!"$NSPYIL?/&Z$m&\d'8$-!78!"$NSPYIL?/&Z$m&\d'8$-!78!"$NSPYIL?/&Z$m&\d'8$-!78!"$NSPYIL?/&Z$m&\d'8$-!78!"$NSPYIL?/&Z$m&\d'8$-!78!"$NSPYIL?/&Z$m&\d'8$-!78!"$NSPYIL?/&Z$m&\d'8$-!78!"$NSPYIL?/&Z$m&\d'8$-!78!"$NSPYIL?/&Z$m&\d'8$-!78!"$NSPYIL?/&Z$m&\d'8$-!78!"$NSPYIL?/&Z$m&\d'8$-!78!"$NSPYIL?/&Z$m&\d'8$-!78!"$NSPYIL?/&Z$m&\d'8$-!78!"$NSPYIL?/&Z$m&\d'8$-!78!"$NSPYIL?/&Z$m&\d'8$-!78!"$NSPYIL?/&Z$m&\d'8$-!78!"$NSPYIL?/&Z$m&\d'8$-!78!"$NSPYIL?/&Z$m&\d'8$-!78!"$NSPYIL?/&Z$m&\d'8$-!78!"$NSPYIL?/&Z$m&\d'8$-!78!"$NSPYIL?/&Z$m&\d'8$-!78!"$NSPYIL?/&Z$m&\d'8$-!78!"$NSPYIL?/&Z$m&\d'8$-!78!"$NSPYIL?/&Z$m&\d'8$-!78!"$NSPYIL?/&Z$m&\d'8$-!78!"$NSPYIL?/&Z$m&\d'8$-!78!"$NSPYIL?/&Z$m&\d'8$-!78!"$NSPYIL?/&Z$m&\d'8$-!78!"$NSPYIL?/&Z$m&\d'8$-!78!"$NSPYIL?/&Z$m&\d'8$-!78!"$NSPYIL?/&Z$m&\d'8$-!78!"$NSPYIL?/&Z$m&\d'8$-!78!"$NSPYIL?/&Z$m&\d'8$-!78!"$NSPYIL?/&Z$m&\d'8$-!78!"$NSPYIL?/&Z$m&\d'8$-!78!"$NSPYIL?/&Z$m&\d'8$-!78!"$NSPYIL?/&Z$m&\d'8$-!78!"$NSPYIL?/&Z$m&\d'8$-!78!"$NSPYIL?/&Z$m&\d'8$-!78!"$NSPYIL?/&Z$m&\d'8$-!78!"$NSPYIL?/&Z$m&\d'8$-!78!"$NSPYIL?/&Z$m&\d'8$-!78!"$NSPYIL?/&Z$m&\d'8$-!78!"$NSPYIL?/&Z$m&\d'8$-!78!"$NSPYIL?/&Z$m&\d'8$-!78!"$NSPYIL?/&Z$m&\d'8$-!78!"$NSPYIL?/&Z$m&\d'8$-!78!"$NSPYIL?/&Z$m&\d'8$-!78!"$NSPYIL?/&Z$m&\d'8$-!78!"$NSPYIL?/&Z$m&\d'8$-!78!"$NSPYIL?/&Z$m&\d'8$-!78!"$NSPYIL?/&Z$m&\d'8$-!78!"$NSPYIL?/&nGjWV-ZT~>endstream +endobj +14 0 obj +<< +/BitsPerComponent 8 /ColorSpace /DeviceGray /Filter [ /ASCII85Decode /FlateDecode ] /Height 1340 /Length 6809 /Subtype /Image + /Type /XObject /Width 1052 +>> +stream +Gb"0HT`Mqo%)fC:^[r=dJhmrM[IGMluq&S$)W6P6Mdn(1$Z+2I6&K-4MMf/@t85CqB%u9H%"U=`iIIflZ$tR5D-5ZKSkqZK\qr121-IAC5OlACG[nACA6paeJ)baenAfaeXIkP"!uNP"jPVP">``,`Ai&,b(t6,a/EJ8/>M*8N'lK8L=ltO=\!2PAIhuP=uls+B`gB-eWUt-^cfq5dKUb:U95r:GQWlJRm/NT4HDnT4HBc!m.2&509\f509UO"d2=+I?I=VI?I/($RCY5q]qZ6q]q@0(.]3HpIR8KpIQ\@/GMluq&S$)W6P6Mdn(1$Z+2I6&K-4MMf/@t85CqB%u9H%"U=`iIIflZ$tR5D-5ZKSkqZK\qr121-IAC5OlACG[nACA6paeJ)baenAfaeXIkP"!uNP"jPVP">``,`Ai&,b(t6,a/EJ8/>M*8N'lK8L=ltO=\!2PAIhuP=uls+B`gB-eWUt-^cfq5dKUb:U95r:GQWlJRm/NT4HDnT4HBc!m.2&509\f509UO"d2=+I?I=VI?I/($RCY5q]qZ6q]q@0(.]3HpIR8KpIQ\@/GMluq&S$)W6P6Mdn(1$Z+2I6&K-4MMf/@t85CqB%u9H%"U=`iIIflZ$tR5D-5ZKSkqZK\qr121-IAC5OlACG[nACA6paeJ)baenAfaeXIkP"!uNP"jPVP">``,`Ai&,b(t6,a/EJ8/>M*8N'lK8L=ltO=\!2PAIhuP=uls+B`gB-eWUt-^cfq5dKUb:U95r:GQWlJRm/NT4HDnT4HBc!m.2&509\f509UO"d2=+I?I=VI?I/($RCY5q]qZ6q]q@0(.]3HpIR8KpIQ\@/GMluq&S$)W6P6Mdn(1$Z+2I6&K-4MMf/@t85CqB%u9H%"U=`iIIflZ$tR5D-5ZKSkqZK\qr121-IAC5OlACG[nACA6paeJ)baenAfaeXIkP"!uNP"jPVP">``,`Ai&,b(t6,a/EJ8/>M*8N'lK8L=ltO=\!2PAIhuP=uls+B`gB-eWUt-^cfq5dKUb:U95r:GQWlJRm/NT4HDnT4HBc!m.2&509\f509UO"d2=+I?I=VI?I/($RCY5q]qZ6q]q@0(.]3HpIR8KpIQ\@/GMluq&S$)W6P6Mdn(1$Z+2I6&K-4MMf/@t85CqB%u9H%"U=`iIIflZ$tR5D-5ZKSkqZK\qr121-IAC5OlACG[nACA6paeJ)baenAfaeXIkP"!uNP"jPVP">``,`Ai&,b(t6,a/EJ8/>M*8N'lK8L=ltO=\!2PAIhuP=uls+B`gB-eWUt-^cfq5dKUb:U95r:GQWlJRm/NT4HDnT4HBc!m.2&509\f509UO"d2=+I?I=VI?I/($RCY5q]qZ6q]q@0(.]3HpIR8KpIQ\@/GMluq&S$)W6P6Mdn(1$Z+2I6&K-4MMf/@t85CqB%u9H%"U=`iIIflZ$tR5D-5ZKSkqZK\qr121-IAC5OlACG[nACA6paeJ)baenAfaeXIkP"!uNP"jPVP">``,`Ai&,b(t6,a/EJ8/>M*8N'lK8L=ltO=\!2PAIhuP=uls+B`gB-eWUt-^cfq5dKUb:U95r:GQWlJRm/NT4HDnT4HBc!m.2&509\f509UO"d2=+I?I=VI?I/($RCY5q]qZ6q]q@0(.]3HpIR8KpIQ\@/GMluq&S$)W6P6Mdn(1$Z+2I6&K-4MMf/@t85CqB%u9H%"U=`iIIflZ$tR5D-5ZKSkqZK\qr121-IAC5OlACG[nACA6paeJ)baenAfaeXIkP"!uNP"jPVP">``,`Ai&,b(t6,a/EJ8/>M*8N'lK8L=ltO=\!2PAIhuP=uls+B`gB-eWUt-^cfq5dKUb:U95r:GQWlJRm/NT4HDnT4HBc!m.2&509\f509UO"d2=+I?I=VI?I/($RCY5q]qZ6q]q@0(.]3HpIR8KpIQ\@/GMluq&S$)W6P6Mdn(1$Z+2I6&K-4MMf/@t85CqB%u9H%"U=`iIIflZ$tR5D-5ZKSkqZK\qr121-IAC5OlACG[nACA6paeJ)baenAfaeXIkP"!uNP"jPVP">``,`Ai&,b(t6,a/EJ8/>M*8N'lK8L=ltO=\!2PAIhuP=uls+B`gB-eWUt-^cfq5dKUb:U95r:GQWlJRm/NT4HDnT4HBc!m.2&509\f509UO"d2=+I?I=VI?I/($RCY5q]qZ6q]q@0(.]3HpIR8KpIQ\@/GMluq&S$)W6P6Mdn(1$Z+2I6&K-4MMf/@t85CqB%u9H%"U=`iIIflZ$tR5D-5ZKSkqZK\qr121-IAC5OlACG[nACA6paeJ)baenAfaeXIkP"!uNP"jPVP">``,`Ai&,b(t6,a/EJ8/>M*8N'lK8L=ltO=\!2PAIhuP=uls+B`gB-eWUt-^cfq5dKUb:U95r:GQWlJRm/NT4HDnT4HBc!m.2&509\f509UO"d2=+I?I=VI?I/($RCY5q]qZ6q]q@0(.]3HpIR8KpIQ\@/GMluq&S$)W6P6Mdn(1$Z+2I6&K-4MMf/@t85CqB%u9H%"U=`iIIflZ$tR5D-5ZKSkqZK\qr121-IAC5OlACG[nACA6paeJ)baenAfaeXIkP"!uNP"jPVP">``,`Ai&,b(t6,a/EJ8/>M*8N'lK8L=ltO=\!2PAIhuP=uls+B`gB-eWUt-^cfq5dKUb:U95r:GQWlJRm/NT4HDnT4HBc!m.2&509\f509UO"d2=+I?I=VI?I/($RCY5q]qZ6q]q@0(.]3HpIR8KpIQ\@/GMluq&S$)W6P6Mdn(1$Z+2I6&K-4MMf/@t85CqB%u9H%"U=`iIIflZ$tR5D-5ZKSkqZK\qr121-IAC5OlACG[nACA6paeJ)baenAfaeXIkP"!uNP"jPVP">``,`Ai&,b(t6,a/EJ8/>M*8N'lK8L=ltO=\!2PAIhuP=uls+B`gB-eWUt-^cfq5dKUb:U95r:GQWlJRm/NT4HDnT4HBc!m.2&509\f509UO"d2=+I?I=VI?I/($RCY5q]qZ6q]q@0(.]3HpIR8KpIQ\@/GMluq&S$)W6P6Mdn(1$Z+2I6&K-4MMf/@t85CqB%u9H%"U=`iIIflZ$tR5D-5ZKSkqZK\qr121-IAC5OlACG[nACA6paeJ)baenAfaeXIkP"!uNP"jPVP">``,`Ai&,b(t6,a/EJ8/>M*8N'lK8L=ltO=\!2PAIhuP=uls+B`gB-eWUt-^cfq5dKUb:U95r:GQWlJRm/NT4HDnT4HBc!m.2&509\f509UO"d2=+I?I=VI?I/($RCY5q]qZ6q]q@0(.]3HpIR8KpIQ\@/GMluq&S$)W6P6Mdn(1$Z+2I6&K-4MMf/@t85CqB%u9H%"U=`iIIflZ$tR5D-5ZKSkqZK\qr121-IAC5OlACG[nACA6paeJ)baenAfaeXIkP"!uNP"jPVP">``,`Ai&,b(t6,a/EJ8/>M*8N'lK8L=ltO=\!2PAIhuP=uls+B`gB-eWUt-^cfq5dKUb:U95r:GQWlJRm/NT4HDnT4HBc!m.2&509\f509UO"d2=+I?I=VI?I/($RCY5q]qZ6q]q@0(.]3HpIR8KpIQ\@/GMluq&S$)W6P6Mdn(1$Z+2I6&K-4MMf/@t85CqB%u9H%"U=`iIIflZ$tR5D-5ZKSkqZK\qr121-IAC5OlACG[nACA6paeJ)baenAfaeXIkP"!uNP"jPVP">``,`Ai&,b(t6,a/EJ8/>M*8N'lK8L=ltO=\!2PAIhuP=uls+B`gB-eWUt-^cfq5dKUb:U95r:GQWlJRm/NT4HDnT4HBc!m.2&509\f509UO"d2=+I?I=VI?I/($RCY5q]qZ6q]q@0(.]3HpIQ_1!7FppF8~>endstream +endobj +15 0 obj +<< +/BitsPerComponent 8 /ColorSpace /DeviceGray /Filter [ /ASCII85Decode /FlateDecode ] /Height 1340 /Length 6736 /Subtype /Image + /Type /XObject /Width 1052 +>> +stream +Gb"0H+TVQs$jendstream +endobj +16 0 obj +<< +/Contents 24 0 R /MediaBox [ 0 0 595.2 841.68 ] /Parent 19 0 R /Resources << +/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ] /XObject << +/FormXob.298c33c3c6cc55f95fec6c1b69c06d3b 12 0 R /FormXob.448be173554bae444ae91aa9b861b6a9 13 0 R /FormXob.a7f7db8f04525a170e8f3ceee7ad286b 15 0 R /FormXob.cafd156e5d6f3418e559c97d0678de28 14 0 R +>> +>> /Rotate 0 /Trans << + +>> + /Type /Page +>> +endobj +17 0 obj +<< +/PageMode /UseNone /Pages 19 0 R /Type /Catalog +>> +endobj +18 0 obj +<< /Author (winprint.rs) /CreationDate (D:20231114221320+00'00') /Creator (anonymous) /Keywords () /ModDate (D:20231114221320+00'00') /Producer (ReportLab PDF Library - \(opensource\)) /Subject (Visual regression test input) /Title (winprint.rs visual regression fixture) /Trapped /False >> endobj -14 0 obj +19 0 obj << -/Count 4 /Kids [ 5 0 R 9 0 R 10 0 R 11 0 R ] /Type /Pages +/Count 5 /Kids [ 5 0 R 9 0 R 10 0 R 11 0 R 16 0 R ] /Type /Pages >> endobj -15 0 obj +20 0 obj << -/Filter [ /ASCII85Decode /FlateDecode ] /Length 855 +/Filter [ /ASCII85Decode /FlateDecode ] /Length 852 >> stream -Gatn%9lJKG&;KZL'dI^L*erM(mpk@S#cWW;qLZX3C7Sa'S]XMPr-N&-G$(4TD+h-F`A>ic47fU$#X2hrImm^Ddcp(:m*Xn!"q;/,bT0gFF\PDB_YTj!ZH(p9JB#_=KmJ$&j]a5^/r@!;#Z"9Wdcf:MaU8=a"s5T=>PeX\(Ob(ZT.0$(FlO`)2_MS]N3hq9gBXGE%n?>(eu<87S=ooNP])EpF?t-CUR-/\.lYD`\e[/MCrWhqbfQY]NH0gPf`aB`mOtC?3,q.ZK!/BW/)U/jl8rA;)>osD,iGeW7@&hR1i3W/=.04M?aP)9n^ZI%V&O+jjao",@'W^'a)UdpZoJFJrA72N]YbFWO@\2f*&;?eQ)k=U8BRBZ\bLl@TaE*k0m(EMkUp*H]$k&Q(3^n/)WkO&`>#XMrqBT`bR5/T&K"NX*HgMQm'<@pVD<,/d[$lbYuXWq^+/t1#6QKJCpCcMUqfK@`s"!ah=_fDQUmUfmR*O7+bh[ADNcNTIA]i*WU*gO1fVHbcsoA%pA$/6^&WfLf\Wj^WheaZ``)VEWU:M4"b2st@(Sph!]i\)8Vo3V~>endstream +Gatn%9lJKG&;KZL'dI^L*gWAMmpk?(#cWW;qLZX3C7Sa'S]FANr-N&-EEJ\O*D:)Sq%l+C'Qi_0nA:gblUKF7Vo)D/&?N\?V&P1@0eaqQ[:-RLAj?30'por>;rNFZCr-\+-K_`(977lFs"KY8Y'+s(oT`(Qoam'M/1M-`("qp8AD_h;4<=[+9%FI"-YUZ)g\N$^mQf`Xlm>!h=NYF6%UG3=_`EU!EW:SHk1Fh)S5H9:h$ES5)%qR#+Z5b2X>i]lar3[s5\0e>NUUuA%8BtU@r=e*U=\W[)$boHe5ijNMqCM(6P\?I`$%"[0*mHn'J#.`GL.HAt8!TVdH\(eYOip-6g%soS?QI&lfRU>BtA-VYX&gf#qemK]XWCF>BmCLuji`e!^+U@6f?-N]*YtqkDj3N]9SeU^0nGr+Yg`CVNd4\);kJR9QEMW2fd@R8of\9!VY20ZHb%eU_%H<"E%%L$RN?BHuB=r'SrL;(1`Pf+YEfkBD2a?>A0!o_Gk'3OqcuH9pHA':ZGtPM]/n]!EolG5_OMtJ(_8al@BV)\n\:M.LAB&,)9#,6oB7M(nVg>sf6ksg2&)>]@09"5*1u>fRYjgAe!hnYUF,bO?[+.'gHE!B$>;mqW*oU1K4EFB-e[DT?a*QXYVZ)!)o;]NE6,R./+TSj`BCbRl~>endstream endobj -16 0 obj +21 0 obj << -/Filter [ /ASCII85Decode /FlateDecode ] /Length 1241 +/Filter [ /ASCII85Decode /FlateDecode ] /Length 1240 >> stream -Gatm:9lo#Z&A@ZcqWPHUaaAU!(2"Y#X1Z%s6e,LjO^Se7$V?J6D>)S"O+]3#(!o+5WC]#0d+>P,ootL,Y(pG*)YL):ehU?PQoBhd%!Jb@%c"0fB_otV)WBY1q/a.S^\`mcG3[,[0tiKblA>RU>OB@"9uo)rm_^]PniAh+BV2IZRfO&b_5_m4N@n!eH&6h0LnB^g22r3M)GOcki_L)VfrJLY->[K?hpruW.uPB[A/u!WdTjdr1lIn)4&OS*T=#7pqQ5O!aQ7NtausWP>hOcEm*,*gX,fH!s&adaZ?.t.VH;gG!rK2TmH6J\@<,=ZZ#f`4uBMLcXf"F??l-BDRF/,)F^ki8")@o);`m&aVQOCTNZLI\4cj'#m_j7^R]MpE@/CaU7..^3.:2BFp9LNN2&_Y:(NDo-U!\P8NS?S\Q=AXW?OMY[GdK$n=71cuZgNJp=qUSBPW=Bro+3Yp>P\;nON/?WK&*Ac`\]r;Tq(eQkMe*S'_W(%4YC>_uXK4#ZH"r]nj,(A"'PXc2:5g/ZeX$-mim"$+\]5sL.Z_'=`?Z_CEb+Qcq:8E[(pu.t5>(:G@^.Mj`g2dE!,Uh#IE82FV&M.nQ'aCq_9#8(f]*Ga$?u665=I'P7*I;4VAmEAbHZ_NgIbfG/Wku/Qku_0R#;oG;33#s0O&hs/19$GD9$.n,O%#>l!"!ok(3o7U?=_\.Z$B;thB[B8H"[QSYJ-H\g@rFR@4MhSGhS?"Qhj/9lO-gL/48d$nPtT6PC*E&/2#dJrbtot:r8QMiO."F3:SQ9R3>T:$hu+np;?8Dj<]Q2_!,rS~>endstream +Gatm:9lJcU&A@C2bgA<7%;.o9]u?u2'M)HG'$uOi[()hgVFZH;Cob=bZ_V8-/0d,>-"0>947BkNh3fEQ6uVGYEW^9gJ.J7aOMbN2.%,b-=1DS\(iiU0!or',KtrB#8m81"=[B41==2D5TWDTB1hT-r%^gr>q2F7q9e/;E%N#FiN_JoXl$(qKOIRc,6ki/4XAuLW^tHTs9(/.UV,gG#L7p5.MS8qE%LPfg/eRCa,NlmO';KMJ4ml"j$]S$+*(2P@3'CddFXM9XHO<>k)=JS(bD^)E!D_:o%@gBZqkS+WR0_),Y':*MZs&[*M[Dj4ZZD$\f?dC/D/JII2K+WtplRB>sYu%";G3;Qc&95.RDaeooJ=`Y;"sZ6R[WT<3UYpg3o@LH=;X:Kh+1+uJ&Sb2%e_reJj6?NCkoEWVsVTW^qL_D:jcjne6h`$2&G=(G,C@8=Lfh>Vg)Fjc@YE*kEr=R=tf2qoT*VlY&9j3+)-Z@#0B+,PMU0(ra9iepmFZA,6Yb=;f`5&JouXNN1Q(qMiI[Xt:uAMBbVrWe-`#lh+Rs\nh2TV]cRm;pe_6-rrU.@DlN~>endstream endobj -17 0 obj +22 0 obj << -/Filter [ /ASCII85Decode /FlateDecode ] /Length 1295 +/Filter [ /ASCII85Decode /FlateDecode ] /Length 1299 >> stream -Gau0C9lCt@%)),5i2LC2@#*fj?3@RJoMF$up0n<9j476>a_CVCqdu[qR9_[9F"d`1=7da5aPi5fOMaTNn,5&iq]5Y1Z]HU&mR]NfgAOW.m3WU6&.>aXnj/'`C%-h:&(:QZ.2<>],)$Y3;hu-Q/ZR7hNpbe!Y_Wh*Z[F.Ae\2C2Mg`\.6H(G[.I`UnZ9"UkpIsK%I(4U844/7oo6U?0fA5ZrgAOUTG8K)'Z^Wu@[D`,9/r#A)hejP(a9F_^.QMaVC,N.cZET[+'YUc4519$FMh*H)P\;%b8JJM9p_&]IiFH\/ZCaMlBCU74JIADi2q.tg2lJSO63oaub%A^4,OJoY[n*1D82XB,h(2i#\"@V=lpO_[`6kFJNB!9km!R6F-:K6!0d07**QK-^Buhh[^j#>lJJ\!>Mat03aaR+k1%bXi'i*0I@&QXX_(O3Xl4&&t+i2iD$\PoW!k$%ITbcA,c8N%N2E],G7q-H]j,f?;/%)Ho2Gi>%c*,RCAKc$73KjV849FCpioooA?mm1P0%j$tM5-IK-qWcfqS?'nFH0X=2/KpI9BnLuj9L0m"F?&-6T+#NdT([9:)n8Pf,lm0ZWaSS2LI1mQE>$tQN^g"M=Wp9PQ\apdS-(A'm(*:?*^*8eKV,ng/&]4mL%cJPB:tRAc[4&l'A\A:MfO08>+R:2,n)B/7$5=Xq89GLt:li4/9]Inau]3'*nAmp)#!o!i$Hk=Bqjoa`3>PcM-jPJBHFoH`3IoQlp`9^UC`\H9XMHJKZQ>AZ"u+U@1,NH(p.>f(/sEZWjc*;J&/?q@@,Rt[Wk9=<`lF-kD`u(2+&4'VN'6)j);2idT@5b=RYB-\B:rj_p>tfRJ3&eCPuJElA3DcP&]D]fO?i?[GAFIKpqTJ(,_IpSbTH*Z&\\AC7,:IBW9_:9-dfc>Y8VP&Zt:/:_DrdVnQFfIs[n#dsI%)E*3`MX].Cb[O;8`4YOF"_4`4B'(bAsc\F]Q#Ho^.L?b`piM4@Nb$p*,>`2W+"&9m%(130r,S.1,ab0"A%Z^c=K&l()gh-FeVo13Nc0>aGRnp!>k0K*DQfI,bQ&dendstream +Gau0C9lCt0&A@P9QrDDp0]gOJo,=:hV2(erYnoch*OW=b85@X+hoD"Jj@W.n*,GXZC%OCV3.-+LKcFDS^Mfa$hJ_VuT7o[Io0DtZ]d0B:NA8mUcU05fQA#'*1lf9qMHRJla_7)]1:81V:*PCu+@Sd\A`o6eR9F.#LN+o#?C<*84d.\AEfB[W#f\`&%FpiT9K2Rt:08uh+t3`UkYh+DS4e$K?KOhC63_FG8IIu-5(uBC1/#Q@QlmFXL"'T54J^CLgI]pTW?4C.E/Ks:6nQgnp6\hOMioRaGUg0Qr,2"m)*tc0JV.ahU\=$8&KQ9G9oPm2/F]dIeYWMfaP6Ae#/eOa83B5,8mJC.e]i)Pr1'Zs0Dsait97`1_'qaMVYu@-:Sc!0qh6D3Z.M_CfUHmh8>cW#J!,1O4Ua@+2O!G4HssB6a78#K\lrfEC%Z`$P>9)GgmYdJK]+g^iZM^%lp)4S5#njH(L1R3Jf>67V6<7^$Jjt!?q7e*)?=W&RJS"jIp=(MEsCj->Kp`NNnjJ(#,H3AAG0sDXiH`Dt4HDRQQNlcbi9NJ`P@&g!04JT>V`q6"hqpRhr#@k0NqBbAi#Vjl;3shOd!f5J]6!O1WkTs7&>bqgVp:_++F>rt4mDI1<+=?i>54Yl~>endstream endobj -18 0 obj +23 0 obj << -/Filter [ /ASCII85Decode /FlateDecode ] /Length 8881 +/Filter [ /ASCII85Decode /FlateDecode ] /Length 8946 >> stream -GatU9ac:5YESS=4\1>Jjii&`U@;E[P`de;5km\e7":^3R8U:)Y8&VC.k.?CF-XcIq=0L:)\u*]o+l[Tm-2`u"n)+-'p%k.r?GHVtI*oZLY&/]5jM=n_L1rY@2as3q;I\@9-!A$p1hC$kN'\;DV;Y(eYjk+83aB[_roD>=Q^2dF@2O`2YQai+Q5,hC4Z^#!p0l.FC+1:k6#%Q9M3"6IaDT8Tp>)t[<(u8:KR*jL'j0RqC3R)Z*3Ec]"[ass?.d_XVZX,XSV^6t!F1b9Thl:`JGoD)f71/&5:9;;GHC1V/^XY(>A+kWH3W+J%1<)dM:T8Shb3hXQ98Ci:F*08o-20fc\M/mrQXN?$#/\ap^69^mI8I6r!hS1<1_!-bbQ:/D''\ehuAk6SM0f>l/bj#TQ>sSO/)BcS]"DFn$#l%F]QGNY"nPP=n#:IS;6c.sTCB;L7F;ci3DG3A2AM)=jB/D$QDPQ1:4UDXPn=.pSMXXtVKca.,c6U.tb+'3:tVHh[kkNQZVY*PTr@m]ihSCsOQTCQ0Aj"^WU20]V+;i'4I8(0d51ED#3K>1.9^'hl;k*AW>c`;rY,oeEH"n%^CQ;\Q'.qpS-dH1frZL&`UX\A4*Ug`&pSZkj+T<\8$*1_"@$I7gVNPVltWn@t4rD"_r`c)qA6(kYtT$f#fQ:24+O$`^X8#r8))_U8VKd*\mP@5)ZSmHGgr?>7@GVqPe"<8'a9ofaZXIQ_fF&H$X=jm5bZ@0:_j$5l1$,BI^@PZN3akm;RNMQQAW1'&6Eq>6tLq[m$47#f^W@$KDgajtt)3<=[[`DE#eY>0UWkUEK:??;QH-OD+_"kN@7g1LH&iMni";,[eif:1CZ47Q1\50IMQ"iA%=b0A'k"n/2;R7MnO)O%HH(RTs#k4MmjV]u8&0CC)/GAR+/]`a_WAiL=oPlliIn%:uk1S]'ep1buO$)]'?89RfR#f@>2uJNZ8EG4dX[U;*+,uR,>6t=kDk_f&rPMSG\=_pGCGY3Wd(:S8u4'MYTDg+foI$KLkQQb4UXX2*1.a/oVgA+&VP"a>(GbbF)q$Nkf/TGF3XZXf3QAi<$L=hksH%'ho#sCH=_3YC0*DPY2NU?kE6TI='(s.2>4!(3=gCf$+BdZT[G-/tAl%*AI?+=m;\E>XYRUb+j]O?>&nOopOB@9!uHUP`QpDH'U=LMS4@m;VQo`,'NMgqnQLRK'`k#Y%U*1cAn525/qY=l^Bgj(kt4Vhi[cQ6*t[koS'ASABkIHoPg@t2&Q5u*B`[BO$CGH0r$Z%bonk[1hjaip@&T0iAR'sK/XA:@!RNb:N%qQE?38b'b#_i+j>3-#A^DNL/-UhXPY8/XpS^SL?UCQ&Metn)KUP)5kl@3?#Za9UPGaLe/4-5hC$kF's6R-LA-5GL%#RN=elKI4r>T()WTGnP(i=+&2A/6E.th\O#]q+VU4:kn?cG/^cghu3]F3u&5:?$.u]_\0dkP_ESZ:U)KH8%i+oC+_\aVrO=_UK@Hi9=Pb+)-GO$XYdoh#"9RjV[(:m!Gf`K#Q<].8$muq4W46ZJe[sR6t+&g%c7f*I4l?8Y9$-t'>eokbCYi570)]#t_biu5d=Ob0Y+N_JS1SbJlV4Oo6Mt(B#5Zk;_8'm`cRTaTI[H/3Hr;=Ao*.:1iJc(.X)8H]fLFb&Z@SK.WA/gV-M8km.*Yg`Y3tX<^s8QnJ#o/G0M(LHH%8^7br&scAX21bh2=88Er&#rGFYR0_8n:ZE@T=V_qu"aJb\1(?BoRQ?At`EoF!"'b#gi,=(\Cnd(uZg^d)j/B*!CJ\*\kdO]-gA_[dG@6,>]YnAU(;(t0'%$o[n(lL(?C#m6^LX3dbnZKjFgpnVo;EG*IH\@hS'&@f^7=)&K.CHaGSpacn4'j[SGYX3B45q?VJXY:iQ(,DB=hHX1Yh[g!s//4Z[@Y]W"3$o5tq\njI3_fMca?SS`COc.:-'0H`u\tifa;'moJ)*)uk]%-\,.l(sf)M_aX4,qU#)Oj3LBrl'6qrDRXb18o?i5+/B).h7i!2d!2ZXJgT&MXC_(Saif[I+.u8_6!WP>m=aaXa,s_&s0#q&2'"^LE27=i:_nMpN:G.nU8!+c^oi`f(riHHL1qjK6sulA4N"8*FC0!'Vu%@_[ps9p%Y#e*_uZ0Dj/B$]1IZtR^``31rLIGh!R8a\7&LtNY,^X_7T'>V8U^.=Amd"29LR90V`npSn>ReKRRo"f!f-G(tO4KQ;pGPYmu^]8MIRe6P!5e[+F+r2f5X7LK]WpXiMhD#amSG>4u62Grs#s0jKjH8b-8sf41'J;kbNS__iR+/4if8&jH9PZ+_7=^t\0*Z=.-MYk/h=IM*&_/YtZ(o`@]QI_TM\/),V.XDNuiT9O0pj@-0L(/';Gq4b2r.]C_B%[P(c*k7;_"NH'R&:C9FGlKD"Du3QAV^9AtC>.f!f-/R"k6bSZ=W9C(]On&ZaY@##$8$S'@[=PC7d_QH`@W`p.`+!Rm-=FnKD*1UYaX2g7'k7N!lhf6(Y'&##FcgW$T&e"[>=L>-(K6(AUW2B5M:DoM!bMu_<;!APZplVr;'HZ+M+C>Tp#D1.n%d,`S28__t2FdU/*p,T3Gp!4:L]U]])?r0N<<p%?J,Sdbja]CEfUi@&;W#GB;/'0J-ic&bGmPfn)>=ui;c@1[4%=b:O\N,SjB](Z0nuU'`Y*KS!J;>!a=ardYHT7<*Mu=E0KCBE-!R`WFj%77mY5_X*%tZQk6L/I-\j/;jUh^e_\A'?2a9pF#Y\<``o'CG7^J7Jj[Ua9J>l*D7VeudbXE\kfEQJ_EY0-eLX;-mdd2>QCT9M0T\`e>0okAjIAJCZHZn6]F'0^#(5nbhs,E!(`l!'BGJ#/U"GD;78kF!WsBPgmL^,+Nj>.kE3gh:ae>8+9I$Q243[JmA#c$TYS*of\W#20!_)*"`b3>AZGBu=bk"KHc6f@"`aX2?5HU64Y'hAb&[6'oPkJ-fokRhLoE6KCOUS8+;[9\CA$T@`DQaC'@[u?ol1QX3&iO/*lBh#hE2iDqR?M6!Z<e%kGSu;iH07tol6VA:k$J1`uLjf1lDO3%C>_I&XZ^d$c3V2Kg6]E]UX!6E<2G3(r@Eo15]IjE[XjV*Z%*[q1b:]#U0+0>`g7l$c%iK`>*:"BP$"Lh_87U(P1hM.;,h^gT>L*,$2eg5Up#hEc'Z-0,IIB.'ASg^pH2la6fh!+GG#Xjl3cUb,WlV+MF-\9^Q-n52s'P9LH>7osLO'Pq:cINj?WD&X:E1UtA);;ndCk3uQ_jE<-oPpJ&W-d>J2r9pih7EEiWYkfDD>J-IF>RrLq;(I3Q?c+!8?Ec-e(>W5:,`^O9//3TdHSW7LUAL5&I>9"GEJHMd%l('sLU54E8;78G:!WtAmGPSX\0]UZQN]8WmK+2.e"o"k@bi52OUQ,_t9Z.F@OuLoe!AhB+HIQpHQml?gB+7;)_*?1Q[Q6#@K6(KOE:<;(XI6Z;CjB7gVMdppk7/`pgqDcojDgg0+GOQ4+sf9k_+ZNS*-kcl`l+rHN&J7:K/i7(*uBGnkW[NNiiE8=boC?]j!$2DBX<,PRV=UT3[`AKE=in<6X4&ihPJ%&:UQ6I/q?8(ce[5`84N9$ordQ,a$jCuh4d2Q3Am28]O>pSE<2HLT"jo%jI5Qi!65TlJQr>TF%uf\&Y8ds/qFt[j`(<_Q>9?%\k-)90F)4p++7As#5Q>>>?ZDX^asEW++[Ys`feuF#ft*Vj!tG)2_0qS/csL(n7'RIh%d^VDm-Z9/qUr$)sP8kZHSq/F'K!OmTsCkZ*sVHFX6M%)qjG]\E+UVA>?!\cc1(6$U=rK+IF2m/0+/qRQt4g75OIRm\\"/@*&MnlEZ`/lWu6f*F8nAa!Ua9>s@X,O(`j.[lf%qUhQjTXc5+\c)P&#Qk4Kq5JbFkl.&*S28%D)O/<`^2J*R&uC,^e!]HFpAIGmQHB$NJi=g.8D051\F+o0D0@QXc3_(cs(6=VL-Fd*ANUjj!#nXlQ$1g`\eT;8&aEG\,]4%3D4O\Gg#XD[A-7Hij]8OMRSd]W3Q!V/VkX3$_Ulp-%*[Dl,8PC>q=OH76nGid@&p'1mW-kH:JnjJ8&_d7"a_nR;KW1j!#m**Kt57YAM1;Vk\ldD-R3UN@`W*=8kVirUpU:Df?kf(Lp;M/bGt/W>U\FG\CW@C8TRjunNqN!mGZjI/K^=mbV,:S[Lej!#mp94ob57>&/.Q[QbfAR#@mZQP4k]`AM\FX2UBN!mET]9*kWhb.)7,F3@]>QeY24[.Wo&1$AM9B/3k)6)h2!P%!`=sDf!Oo)[K/cm8h5Jfh%FcC>=3@ZE.`W]349D)o7Y%o`iOFiNO/csMpdU5s%eIq-,YT]&c/c\h5hB"a=-]NjHNPjQ*\-RtsOTLao&MANWnLhc-`^O8\j[#WRe.Ckhls1MG5XC.EJ8-:0;+h&le[lA_)Adrji6)9NAYf[pW17-2(I5g!A+=AYJFU@m=Zg/CQc;I%7\QSDA5uWf_c^57VmiLB!>s9[fr(YF\7;Q5Xc>(f!n,Vq8+NK3j8EklNO!\H!>Jc`P-e`2'Sq]Gl^KLZJmsSnDUOh-Q84Mca)uA6R)SJU2?2t&;PdeS)fT(f!![(*/AJQeZ;F,MGO=o0,gr8p2s\,Zrpjio*KVG3g/$cZUIPl*9JIT-n.Mr%2q;MXuhN]-Y$WX2poMK)2M%cuTuE=nR@DK];cRtg3XYLjq>>Q>!&M8I5"8gIi2*S$%Jj!$2HF?=<8(P&Jf2bHi/AB$K&"nu]j+s=T4K1@9r`+\'h<'Jo*T,.9FPY@+ZoXX\0kjZZ5#=HHrbK3*j!#oC0'&;+e.7tTHOo,WirUp*^Bm;fA@^+dNj0PCj`(m"^38hN$P)]G3@_WZ-,@^@,hH#nW?K.:jp9CUPQ3KO]B50_$t6aA+S^&?!.u$1C:W>A<18`Qo%')LirUWVYBX->?9'f<532\qO,LeI?.fWo24"'3F*;]]k9sGnG'>WPP7tp;l&Da=Nf1^FWG'\@)JV+p6Xsb+#0be!@!Gj0)Yk^=H^DcA@-hLjOeL"8L`=*;>TXc4&P6-uRA%t(V)0-g6n'hdjY:rsioZ,u_REAnb(*WaZF@d*'?WFXhCXcrTPW?3&oWXUV0_%aki3j@j/+eo]-e&&fLNQ+f:o.`0D]?P*uBL=hb!8)(L:cgC`(dE"^kG52?bm89bE\VI;N42(P-:ErrG(INO$2+ETD*O'Gahn^YMXj72CY];$+s>U8tmkqo(\L=2soQ&X7:m"?:E?$^2#iBgMU=;R@g@f#/Elq7='s/qVO?`\"B!NH\X\j7.Q78S7"1m]i`a-5Zoj5RRE@EpO+qh5aPdtdqobAY,pQ[ecj]tXOBci!o*I8f8erV35]F_@&:,Vj!/_oJ,5j<\[LW@>hV7^:3\,r&.0M"?nhI`[DQ$IdRemm9@:Wmjj*^*$3'r4p=V_iP;K5J-)>m]7Q#g`HFKk?%&JcV_\'r7gBeO,odCpc/auY5Im6*AI!<*rOdU/mHPh=7SAqg7s/pr&S-\']tbV.Qo-5IfH%hUu1p(iUb[7Y$_&XZ6^$if&.K)7/T%D^=WD\iQ"1^oYZ`>\pjMOp"r"3r*S@\*^AreT>(3uk9']`a4Kd/ptn=7(Ol"k1(aHFqlba&IJH+.$'I:~>endstream +GatV$993r!EJ*?*:Y,RGp(+@Yhgb=qofqS?0!DI-pWDmZ?=3ZnoabDXH0:?mYLgCNd4E&hFVJWWou"7G^;iaa=RV^!I+bfLH1ga2]p.+Zhn4F4X"V9I+8hqmjcq4P-,9>3j84+_UQr=MKth;ur*IQQ?iAsV\,Z*G1%=^6l?uRD^/mn:L+mlgNhT7[:TF.Jcm;(/6^u0NI*?<=8"F4E0Qgp/E6n>DViflINS6^deiQsCO*[GIW,K$!,0tT^_\9o-8+""F0_SfnP7:s3)*^=9H;&(jhO7RQ1E,u=]?Hl8PFC`!-4D\`RB8>oI$JlIe?&,O$D(`1ZXcpKNr,@9UHR]Ng;>r$MRh6demhJHSMEDM_;m@+Q\r!@3N_pkH%kU3j\j5HAj"ke.dpMPaM(^[!%V;I*Qr=;370cck]u?W3N&=o@nu(?gW6)/SLnueE:"/hO]-R9MoRMrU^Ra8fR6LTQ$;O0HVf.D+D/prUiL5=3EchIkk*c_m,9:3)DI;@S&fXpdNP@HqOH&E>_kPAV<;+\Gdn&r8B->p#+o[g^NFN4rZ'Au(LggHj?C,m#`4U6jf[!c0Vqi'na$cFOY_$01./L#Lc`,bX,P)pO=6&K@[Lb]98j/SQrIc*$o+Z6n:[WPaW>=$Ne;BU0CJ`$g8\fg0.YM%B[X,e5>%pP?mq8$1W":`Q_PI:1VYZZYm>MAq]<$ifWr;0,l=<_I6*T+S^5\9CL2!&A+_9B9UrC_<\Fo``>%LM94X^$^aos\MqM8_l$:2n)>i;4NF&EZ;+Vl_H>EtHCF:jfM'RIj/X3H4+[*bHt_CB#H`@u]Ubb\%0>PC.G@?e%hW/f8>Q*W\\KFPkQ)RA`k8*8N50<]Q:oYd+.U;2-m.5:AjS8e8fZ"t8sL`$.MXQ3k`h&$_j`ndkX(^*S:j#NsPL_(M"C%1rnnbMehI5)qa0X]kLD_(5>,=Z*0#d#d''.[lVfjdpT0d;:p]327@@Vip%UrHe1#h^MU)s1V3jtc@hKnp:5c.bLn,R4//5,X7$.1A"cR97NHs44*3R#RM"a=cHA1+.cs_IHKM,::@S@YVh-&Ms$";E4(gI0sDegHc_Ekp0;!:e2bq2L$^Sn4_jsVpu=m6BJJiTPX>"MccMn+[i<9EHr.)qAh=/&7T-`Z=uZiF.X"=mL0@EUP,]13=P?S.KUaaHt//O:6b%oG)W\iA,k0M#KZ^^Z,c]jiB5642CV09oSfgZnMnQ%XAqbO(GF-SDXV_a32`XE.5EK>SfJ\%N`%Xip,/0$!S/.kcaL4U-][Rdt5*ePY`NR[d/#Vc`M/QWe!a..D)SRT/.XEec=EM.YPMHAVJ`R^pFMua@$\&o_$N!LTES>M1`9IW+P2&R!8(c[+=9RjV[??Ok%DN?W8QPs&dqkSJeh.$+APE1&7-9e;[+#Sr[Q8o]9OEZ-3b?WXs58$BcAkbZ1ZAPMh0o`%]9t)G;1SN\PoR2J7[^=7BY3`=@(ee."rff(B9MhER3R4PmM8%grCA_(_U:L3S.>.kt4*[Am2GoK-;D(Pp7:cVJ45hd$m%,0d=Q]gWc*A1:-.$9?Dl0]J-lM,/eB\"tN'V,i]=?1@a-1VD0:5DGT5rN92sTC1=_/#]*1abJT)f\Le3<\NbU1GW_@IO:Y(QZXqfUWH7?M)lohPL=Pc.9C?4Pbbl&.\dH'[1C.e*cF>B*IY.#?oFX&!?^-\Z;`Z,>N#:Y]=9'7=`_u)IO*8MuI/ka0_We=p3;V2'Spf@\[0iB2^:t6&^u)N&'Yf!/$9tNi!]36,^4j@#RQ8f"0-43-VKWR1'<4gST2=_B9N+RPn8%]eGas@hLLg`Xj'Ki73t@VUsm63mf$1Z#^:#[f,tVo0^pnQFn?ATouDiD-)jIT2m-Fa0'd=6)`/i=0uP&_k<&gLQO16NW!dg)JKP&:28CO[J?pC%PG:gU4G40HHntAbK5h-)9ft(\P.p4*bEFZ2RK:+RJ#\DL0X0eC-grRplp/`j)O;f)FN[V3T#(Ne2]imU]b'4I9$Qb1aZLoj!`W/VN9P6JDfQP=29kN'\T,*/-p5PK!#8+:qgKlYYL9SIKI@W>M%.C]!.(>!3mP;tO'#0aP98-*>gkcX'mP(A\h*'#)FVK/WDS4sul5u`CUZ0S"g+Wt'>7R'W#,/sV5LF&*aR!Hj%0nr7P_#j/DJC79JYA\[.J"UHsQbd')HeAG7;k8?CWMnTm9=c-pV3**3:pag,&_FNV[`"qf$jNsACbcSdH"?HWkV0WMTk70%u!d"XU4=m_q#h$J&<(_&eKT)=:6NQ(teeGN_.`ago^=>i'c53f>M$kD/@UpN""kHi&$^b',VD9-E*d:)@Ajoi=_MD-4:E*cI[8:&ad^mTLn0YGWjQ5c:#2=SOQAl1seWX("@H3=)a3quNm1O/,6XnTLH8=Lf1"Q6<'SM*8D'\rY$;ss]k&iRX%:Q-+99#:m(:jmn8L;J.CKD5dlXK0f3r<\[R+[4qZF2%=*khEd'H%N-67<429P^rVR\u*kN9!K-6oZWE65XMl4t%D_ADCZ'5FTG0,9hdrHUNB-_q[Sm_J3>!9P6a!:i]%VbAP^2lMO/s5]Qa1*//@+/UaScMVA;,+,M?NPUYkNFjJ4b)]RUJ>l,LMq/NqM2:-Pg4aOK>QeQ:%iUIFoN`sn<`:Wpf#gE0CU2T130^`F$Hbr$><\Au4Wtm*V1*]W,]=oMT$W,bT9T?E(-5oS$IMjkG.i6#=2)dWTZTM%-N\![L(P++@-oModNR<2&hQtRG^K'c;NnI%8#j1E#1&=$X#)bM^Z^#P39NG]/aW`#S_\9bZPD_\"*T4qXUW0Ub_>WTY,+*L1gJ;s*iht(QjiHKaPc]&5\N1tQQB6".QH;QL;]Ta0D$j')R==3\._,P5/tY4M[@/q>b_+9^CmA6.1K91:eb_cqh$ce!(SETg2^t(aDR#L=NnGVe#R4>Nj:a8QM),%Tdm\9X#Z[eH7Gor!`_ub]gGO!`#b6gAgH3`8M`jj[M-P_>9NOCHO@)B$B^oYRM-L&0l_7(PB4[$p/GHnL+W$irQII+>Zs&)js4F2bM-PQd@(C%(=.9gF4JH-r=3*fYUStR`O'rr"Dk<+6Kj4V1A4FU_GeU!$YgqA\>9Og!?qo/D?WrX^%eF2l;9/np$q8SSN2=A+J,70+Kj9:?CdhqcSZ;rNc-,cM+W%sGh@_NIB/Q,a-(W*D$G"J;<$65-nX*01RY@!E0j>e;_9AqN+A@o?#\:WjD_t]B(SG:sEUj*A<5AWV>,GOI(P.Nm;5X2B(?1S9a;oh:!5fR$Y&Cpo/*m''`b^32"Og;uWP(oB\&ugLN]o'\WCY*Pgh(5G\Us[p\3;NsC"L`S_,4^Y.Ghb'bMse2<@*F9&S#Q0[JO8!OiIVZXU^rZ\eVoaA22(`A]s*XPfX**hF+bM^3RS2)+P_a^u0b#FF?#md9n@0F3Krj75.)UL4)H.N")u1*HbV;!oKJ#nOr!!(9_`6-=%W._3hh+E7!h@>Do[9nYL5N>Q=j:9F'$.X!5IjGYuLP*Z'CC8_J7dg,,+8DOk#R(STdW0&6][(Jpf3]F]J_E<-K.BN)D$:%tR$%)4_aqR?fCWj05J\/L>2s"qc4I9Zc)?"1D'hRRE)1Ng`SJQr?,eO8!O:*SWLrQb!?j?CU,KAa'9L7VY;kncKQ+;_k])CprCNNUYO8\6M6!Z3oMWIb^p--S2ebZ;eR0W\onHBjq51o5LgjAEue0Z]!Y`7VarUFCZC4pnh#KfqP"@4sPK7dUI#;_Ec/">d5"%??`QK;m$E7RnU<^f6Wt-/i=9b-q?j2gclk&PVAX"qi-Vo]oca(M!p7!fWF\;-CJ%>e)?0b.]R4OGdLcb+:(UX.'lljX'dKOUGPJWOHpo7"1+bq;'&,M-L1Y[M"nmjH^T%b?D>_FkXVIm#iUf4Q@]CY%lVT6eQBC(51F)$".2PFm@Igr7P9;W=5,0H\Ji^4rN;7j@!\,"(^[.`,&J'u?X4;T#f9Pi@#\&AOg^"B7RnX=_5rpO,%'u*]EP4leZ5I-$XcH)bcD")UF+3?1rqOp&^24rQi&6]MVq<3Nk0(Cj8t^J?9Q#[93%*I1:J]:,8[$^-Lf_-4J,gIHcDhB$UCZSR?CPah9&lg7onRU\Hmq\b0kP(ONYqR1R>/(q]*K%-VrO!A2K:"QL:-BO,IDAFW.lL#!;Q'`)4QD+D\!\'%rZ[,/tFG3G,>P!Z3sl%E9*NW7UUSG#u^Q.'r5.##*)q(gBMNi:=AXVUV+^]LiTSgjC3,ZqDGVJ`ufNdL8!t^:XSgjDb2-Su8BfGZNNPUNtDTW8f(dXqU%nn1kp+3":4)*>cMu1ek9lFAZXSGB$l@@_@#aKM/Ld_\5g9$0g`b#\Hpi!R+QW]j<4q.9dc.THN.*d\0q5i3dpSukfpF@NMdX-WBX@3N7i:hQ`6lR.0\Er`ST@hc)ggu=)4/?3.!DS&/"VV&F1jI._Hbg)#_eHb`Ur^$S;QnM%D"GMJDgN!EqCB=Yg,`jjk@3MAkG.^9L#)a7i`D^FM0Z$m?B)XoPn%YO#enD.7l1SSXbm5_lUj4>9+Nr(lKiY2!Jrq6Br?2N]JeEOdlZ3je\\B8"Ol?`s!kN;)4u:aD4`i@C(K2//Q>q0Z\UO_JEF*_Ao8UCZ>I1loACF9cU0U+,A*co_pJ'MdQa]">C*L7@:??;XY7="Xp6=fr)dfLb[FQ='HRZ6$1Z@EVe*Hj7R_pc.L1K!!PYVP'/rQhCcWPT.mXJ3c^QeRm#*Z(J["QD[q+pX7ToQ=d;da_48752BAId,@[U'Mgpd7h)7/qRM?SAiGmeb#:X;BC*gJ7=\DYkp[3&brPPQcDNCo=E'pD5ed9C\KZ).L2/+S]XWje/9&Qje]"&O:,Ik@e$UZHC^FSq2kKc5V@YVMV--Z24435F*;uek7fI<]4r1eEXcUjY?W)r>QiNR;FO4:1n+*2ZJ,Gk!+h(>O\r0SOmd:r]0J!#Z)^fa*Qf@\g>b-3'lj>6HB`P61\>hSV)0-t'ljlp4%48uEHR0(IW&/n5V>C63qekCp-b<=cg(YC!+tt"/N$98;eeKhF"-/oP]:h*X*[:"l'3c#a9;;tdV=Kj4c0Y3H)jmmUAcKK'%D#_b2gMJebVf+!XJ[R)Tsd32Sb5gP@$>[J@Ma@.N;M?MRO)@MTE:>#]Z!(GS4Cg&W)ZKDVUqR?g-?^4hb>R^lLpTBPb@-%'bLR^e97lol)\-Rdm:NPdkIb]7`1+nXb'Q\_)h1lj8e'IsdQX>7>Dnl*&]tXI8YPf(orD;VeIK--cC`C*&M&hEVk5'joDrHmuonK\0`E:)-#!^4])*?jsJ,F]W[pQGG_?)[gnPa!iN:]CshsnGk+$8@pr#!_9#D>=T/)LYPrjq]lhgRS<3oBh%Mj#3gqs>1`Ttd<>r5&hgL97(!aSFDT7cjRKmt]1f58NWr6.DcBop8_HXN-a]a*O7oSQTdS022"J3R3`3pHE3KTZ>ijjnL>mbV.%ehl4C7Hn<>6f#Me>Ne[`.R=6^?a)]\JC`R?%CQZs1U#3T`][$.0B'c5K^@$@<_/?R"K(?X@s8Mp'0^uV9qV0VMAMGW:>li1'UVD6Uq,-NXbYM^ugnfJns0J'aqd/geo%1RX?hu(S,nF,rr:AOTa5?OV&)[<[j*1OrrTe[brU6BXN5,2GR0endstream +endobj +24 0 obj +<< +/Filter [ /ASCII85Decode /FlateDecode ] /Length 587 +>> +stream +Gat=g95i<6&:j6F'm"2lb4U]VIU$<\3,ej3+V&j=*+oRoPJs"X`NR2ZRc2\@P-pK";'66::\PhURlV68fkfOk#C5PU4m%ntm>(*+bH5W];Q(>N3[JT';1iRKYe_eAKD@'RKTB=M4?um4[XW"B:m(j8f:ZD$N1akB1bd0GQ%C9*9uuh?Q9o@\6Qu:3LNGSTN$un+2]E++(=69qSDi34Ct0b65[1^^8*ma0l/Lo@!?etr)jVqcu+\-D=G`\d:rD*KnGSI,doJ%K=nT-(/gqQJ(ir[hO=fgl*DcQ_7*Q-L5sB=>>Q5/b_,BCHA<>-"\74D5FM?:#u;[Qq*UG5&A5I0'*CspX.;EKC*L0\5$cc%VTEoSVejB)d7:EE99KVnlU/5I1S&5HM7%cob4#6ef,_ZPW%5Hm_AX-Qht2(Gn7[drGC2Q:>P!endstream endobj xref -0 19 +0 25 0000000000 65535 f 0000000061 00000 n 0000000142 00000 n 0000000249 00000 n 0000000361 00000 n 0000000476 00000 n -0000000681 00000 n -0000000786 00000 n -0000000895 00000 n -0000000972 00000 n -0000001177 00000 n -0000001383 00000 n -0000001645 00000 n -0000001715 00000 n -0000002025 00000 n -0000002105 00000 n -0000003051 00000 n -0000004384 00000 n -0000005771 00000 n +0000000676 00000 n +0000000781 00000 n +0000000890 00000 n +0000000967 00000 n +0000001167 00000 n +0000001368 00000 n +0000001625 00000 n +0000003561 00000 n +0000007976 00000 n +0000014980 00000 n +0000021911 00000 n +0000022323 00000 n +0000022393 00000 n +0000022703 00000 n +0000022790 00000 n +0000023733 00000 n +0000025065 00000 n +0000026456 00000 n +0000035494 00000 n trailer << /ID [] % ReportLab generated PDF document -- digest (opensource) -/Info 13 0 R -/Root 12 0 R -/Size 19 +/Info 18 0 R +/Root 17 0 R +/Size 25 >> startxref -14744 +36172 %%EOF diff --git a/test_data/visual_regression/data.tiff b/test_data/visual_regression/data.tiff index d31f6dc..e729420 100644 Binary files a/test_data/visual_regression/data.tiff and b/test_data/visual_regression/data.tiff differ diff --git a/test_data/visual_regression/data.xps b/test_data/visual_regression/data.xps index 526971a..84878ed 100644 Binary files a/test_data/visual_regression/data.xps and b/test_data/visual_regression/data.xps differ diff --git a/tests/visual_regression_comparison.rs b/tests/visual_regression_comparison.rs new file mode 100644 index 0000000..739e7a5 --- /dev/null +++ b/tests/visual_regression_comparison.rs @@ -0,0 +1,51 @@ +//! Validate the visual fixtures and comparison using image operations only. +//! These tests do not install a printer or submit any print jobs. + +#![cfg(all(windows, feature = "test-utils"))] + +mod common; + +use common::{loader::load_pages_from_tiff, visual::VisualExpectation}; +use image::{imageops::FilterType, DynamicImage}; + +fn fixture() -> (VisualExpectation, Vec) { + let path = std::path::Path::new(env!("CARGO_MANIFEST_DIR")) + .join("test_data/visual_regression/data.tiff"); + let pages = load_pages_from_tiff(&path).expect("load visual fixtures"); + assert_eq!( + pages.len(), + 5, + "fixtures must include the raster-detail page" + ); + (VisualExpectation::new(path), pages) +} + +#[test] +fn unchanged_pages_pass_visual_comparison() { + let (expectation, pages) = fixture(); + expectation.assert(&pages); +} + +#[test] +#[should_panic(expected = "page 5, score:")] +fn downsampling_to_96_dpi_fails_visual_comparison() { + // Match BASELINE_DPI in utils/gen-visual-regression-data/run.py. + const BASELINE_DPI: u32 = 300; + const DEGRADED_DPI: u32 = 96; + + let (expectation, pages) = fixture(); + let downsampled: Vec<_> = pages + .iter() + .map(|page| { + page.resize_exact( + (page.width() * DEGRADED_DPI + BASELINE_DPI / 2) / BASELINE_DPI, + (page.height() * DEGRADED_DPI + BASELINE_DPI / 2) / BASELINE_DPI, + FilterType::Lanczos3, + ) + }) + .collect(); + + // The comparison scales these pages back to the baseline size. It must + // reject the lost raster detail, not merely the different pixel dimensions. + expectation.assert(&downsampled); +} diff --git a/tests/visual_regression_win_pdf.rs b/tests/visual_regression_win_pdf.rs new file mode 100644 index 0000000..1844864 --- /dev/null +++ b/tests/visual_regression_win_pdf.rs @@ -0,0 +1,38 @@ +//! End-to-end visual regression for [`WinPdfPrinter`]. + +#![cfg(all(windows, feature = "test-utils"))] + +mod common; +use common::print; +use common::visual::VisualExpectation; +use winprint::{ + printer::{FilePrinter, WinPdfPrinter}, + ticket::{ + FeatureOptionPackWithPredefined, PredefinedMediaName, PrintCapabilities, PrintTicket, + }, +}; + +#[test] +fn win_pdf_visual_regression() { + let _ = env_logger::try_init(); + let visual_regression_dir = std::path::Path::new(env!("CARGO_MANIFEST_DIR")) + .join("test_data") + .join("visual_regression"); + // WinPdf preserves detail more faithfully than the EMF/XPS paths. Use a + // tighter tolerance so downsampling the raster-pattern page to 96 dpi fails. + let expectation = VisualExpectation::new(visual_regression_dir.join("data.tiff")); + let actual_pages = print::print_pages(|device| { + let origin = visual_regression_dir.join("data.pdf"); + let printer = WinPdfPrinter::new(device.clone()); + let capabilities = PrintCapabilities::fetch(&device).unwrap(); + let media_size = capabilities + .page_media_sizes() + .find(|x| x.as_predefined_name() == Some(PredefinedMediaName::ISOA4)) + .unwrap(); + let print_ticket: PrintTicket = media_size.into(); + printer + .print(origin.as_path(), print_ticket) + .expect("failed to print the document"); + }); + expectation.assert(&actual_pages); +} diff --git a/utils/gen-visual-regression-data/README.md b/utils/gen-visual-regression-data/README.md index 3e98c29..9c0cbb2 100644 --- a/utils/gen-visual-regression-data/README.md +++ b/utils/gen-visual-regression-data/README.md @@ -30,6 +30,28 @@ test_data/visual_regression/ └── data.xps ``` +All printer backends share these five-page fixtures. The last page embeds +300-DPI raster patterns (vertical/horizontal stripes, checkerboard, and +diagonal bands) in a two-by-two grid covering most of the page. The blocks +are generated at their final physical size to preserve the 300-DPI detail. +Unlike vector strokes, these exercise image downsampling in +`Windows.Data.Pdf`: rendering at its default 96 DPI loses the fine detail. +WinPdf uses a tighter per-page visual similarity threshold to catch that loss, +while the other backends retain their tolerance for registration differences. +No separate contrast test or backend-specific baseline is needed. +`tests/visual_regression_comparison.rs` checks the comparison itself without +printing: unchanged TIFF pages pass, while downsampling from 300 to 96 DPI +must fail on the raster-detail page using the same threshold as WinPdf. + +### Pixel alignment + +The page is A4 rounded inward to whole 300-DPI pixels (2480 × 3507), less +than 0.1 mm smaller on either axis. This avoids renderer-specific rounding +of fractional page dimensions changing the phase of the fine patterns. +Ghostscript's XPS conversion also explicitly uses 300 DPI: its default +96-DPI layout grid introduces a visible phase offset when printed at 300 DPI. +Neither the pattern frequency nor the comparison thresholds are reduced. + ## Determinism Byte-identical re-runs are non-negotiable — the fixtures are committed and @@ -53,13 +75,14 @@ The script is pure Python 3, but it shells out to a handful of command-line tools. On Arch Linux: ```bash -sudo pacman -S python python-reportlab poppler libtiff ghostscript +sudo pacman -S python python-reportlab python-pillow poppler libtiff ghostscript ``` | Tool | Package (Arch) | Role | | ----------- | ------------------ | ----------------------- | | `python3` | `python` | Host language | | `reportlab` | `python-reportlab` | Draws `pdf/origin.pdf` | +| `PIL` | `python-pillow` | Generates patterns | | `tiffcp` | `libtiff` | Packs TIFFs | | `gs` | `ghostscript` | Converts PDF → XPS/TIFF | @@ -71,8 +94,8 @@ missing from `PATH`. From anywhere: ```bash -python utils/gen_visual_regression_data/run.py +python utils/gen-visual-regression-data/run.py ``` Output goes straight into `test_data/visual_regression/`, overwriting any -existing files. \ No newline at end of file +existing files. diff --git a/utils/gen-visual-regression-data/run.py b/utils/gen-visual-regression-data/run.py index 9c69217..87bb676 100755 --- a/utils/gen-visual-regression-data/run.py +++ b/utils/gen-visual-regression-data/run.py @@ -10,8 +10,10 @@ import sys import tempfile from pathlib import Path +from PIL import Image from reportlab.lib import colors from reportlab.lib.pagesizes import A4 +from reportlab.lib.utils import ImageReader from reportlab.pdfgen import canvas from reportlab.lib.units import mm @@ -23,13 +25,16 @@ SCRIPT_DIR = Path(__file__).resolve().parent REPO_ROOT = SCRIPT_DIR.parent.parent VR_DIR = REPO_ROOT / "test_data" / "visual_regression" -PAGE_SIZE = A4 -PAGE_W, PAGE_H = PAGE_SIZE - # Baseline rendering DPI. BASELINE_DPI = 300 TIFF_DOWN_SCALE_FACTOR = 4 +# Round A4 inward to whole baseline pixels (2480 x 3507). Fractional page +# dimensions make renderers round/scale the page differently, shifting the +# phase of the two-pixel patterns even when their detail is preserved. +PAGE_SIZE = tuple(math.floor(side * BASELINE_DPI / 72) * 72 / BASELINE_DPI for side in A4) +PAGE_W, PAGE_H = PAGE_SIZE + # Pin reportlab / Ghostscript output timestamps for byte-stable output. os.environ.setdefault("SOURCE_DATE_EPOCH", "1700000000") @@ -316,6 +321,49 @@ def _page_gradient(c: canvas.Canvas) -> None: _draw_footer(c, 4) +def _page_raster_detail(c: canvas.Canvas) -> None: + """Page 5: raster detail that is lost when PDF images render at 96 dpi.""" + _draw_page_header(c, 5, "Raster Detail (300 dpi)") + + # Embed images, not vector strokes: Windows.Data.Pdf can preserve vectors + # while downsampling images to its default 96 dpi. Two- and three-pixel + # bands at 300 dpi become subpixel detail at 96 dpi. + # + # Fill the page body with a two-by-two grid, leaving room for the header, + # labels, and footer. Derive pixel dimensions from the available space so + # enlarging the blocks preserves their 300-dpi detail rather than stretching it. + margin_x = 36 + gap_x = 18 + gap_y = 36 + top_y = PAGE_H - 108 + bottom_y = 54 + width_px = int((PAGE_W - 2 * margin_x - gap_x) / 2 * BASELINE_DPI / 72) + height_px = int((top_y - bottom_y - gap_y) / 2 * BASELINE_DPI / 72) + width_pt = width_px * 72 / BASELINE_DPI + height_pt = height_px * 72 / BASELINE_DPI + patterns = ( + ("Vertical, 2 px", lambda x, y: x // 2), + ("Horizontal, 2 px", lambda x, y: y // 2), + ("Checkerboard, 3 px", lambda x, y: x // 3 + y // 3), + ("Diagonal, 3 px", lambda x, y: (x + y) // 3), + ) + for i, (label, band) in enumerate(patterns): + pixels = bytes( + 255 * (band(x, y) % 2) + for y in range(height_px) + for x in range(width_px) + ) + img = Image.frombytes("L", (width_px, height_px), pixels) + x = margin_x + (i % 2) * (width_pt + gap_x) + top = top_y - (i // 2) * (height_pt + gap_y) + c.setFillColor(colors.black) + c.setFont("Helvetica", 10) + c.drawString(x, top + 12, label) + c.drawImage(ImageReader(img), x, top - height_pt, width_pt, height_pt) + + _draw_footer(c, 5) + + def generate_pdf(out_path: Path) -> None: out_path.parent.mkdir(parents=True, exist_ok=True) c = canvas.Canvas( @@ -335,6 +383,8 @@ def generate_pdf(out_path: Path) -> None: c.showPage() _page_gradient(c) c.showPage() + _page_raster_detail(c) + c.showPage() c.save() @@ -387,6 +437,8 @@ def convert_pdf_to_xps(pdf_path: Path, out_path: Path) -> None: "-dSAFER", "-dQUIET", "-sDEVICE=xpswrite", + # Avoid xpswrite's default 96-dpi layout shifting the fine patterns. + f"-r{BASELINE_DPI}", f"-sOutputFile={out_path}", str(pdf_path), ]