From 605f21fa1f5c51572d4b2c061237a3a3688fb989 Mon Sep 17 00:00:00 2001 From: Bob Singor Date: Wed, 4 Nov 2026 04:02:09 +0200 Subject: [PATCH] Write a markup annotation's comment as rich text MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit /RC is an entry of every markup annotation (ISO 32000-2 §12.5.6.2): the text its popup shows. Only a free text draws it. EPDFAnnot_SetRichTextJSON and EPDFAnnot_SetRichTextXHTML now write any other markup annotation's /RC and its plain projection in /Contents, nothing else: no /DS, /DA or appearance, as Acrobat writes a note's. The body states only what differs from a comment that states nothing (the reader's face, 12 pt, black, left-aligned), so a comment written with the body it was read with reads back the same. Links, popups and widgets are refused. Tests read Acrobat 26's note (text_rich_comment_acrobat.pdf: no body style, Enter as inside a span, an impossible font-size:-14.0pt on every span), write a comment, write Acrobat's back, and refuse a link and a popup. --- core/fpdfdoc/cpdf_richtextwriter.cpp | 130 ++++++++++++++---- core/fpdfdoc/cpdf_richtextwriter.h | 11 ++ fpdfsdk/epdf_annot.cpp | 32 ++++- fpdfsdk/epdf_annot_embeddertest.cpp | 124 +++++++++++++++++ public/epdf_annot.h | 13 +- testing/embedpdf_layer_mode_writers.txt | 2 + .../resources/text_rich_comment_acrobat.pdf | Bin 0 -> 6584 bytes 7 files changed, 279 insertions(+), 33 deletions(-) create mode 100644 testing/resources/text_rich_comment_acrobat.pdf diff --git a/core/fpdfdoc/cpdf_richtextwriter.cpp b/core/fpdfdoc/cpdf_richtextwriter.cpp index c9fec0bcfa..f3b0f9ac52 100644 --- a/core/fpdfdoc/cpdf_richtextwriter.cpp +++ b/core/fpdfdoc/cpdf_richtextwriter.cpp @@ -224,6 +224,45 @@ WideString SpanStyle(const CPDF_RichTextStyleDelta& delta) { return css; } +// Acrobat's XHTML envelope around the paragraphs, the body styled with +// |body_css| when it says anything. +WideString RichContent(const WideString& body_css, + const CPDF_RichTextDocument& document) { + WideString xml = + L""; + for (const CPDF_RichTextParagraph& paragraph : document.paragraphs) { + xml += L"

"; + for (const CPDF_RichTextRun& run : paragraph.runs) { + const WideString span_css = SpanStyle(run.style); + if (span_css.IsEmpty()) { + xml += EscapeXml(run.text); + } else { + xml += L"" + + EscapeXml(run.text) + L""; + } + } + xml += L"

"; + } + xml += L""; + return xml; +} + } // namespace CPDF_RichTextWriter::Prepared::Prepared() = default; @@ -277,36 +316,61 @@ WideString CPDF_RichTextWriter::SerializeRC( if (!extra_paragraph.IsEmpty()) { body_css += L";" + extra_paragraph; } + return RichContent(body_css, document); +} - WideString xml = - L""; - for (const CPDF_RichTextParagraph& paragraph : document.paragraphs) { - xml += L"

"; - for (const CPDF_RichTextRun& run : paragraph.runs) { - const WideString span_css = SpanStyle(run.style); - if (span_css.IsEmpty()) { - xml += EscapeXml(run.text); - } else { - xml += L"" + - EscapeXml(run.text) + L""; - } +// static +WideString CPDF_RichTextWriter::SerializeCommentRC( + const CPDF_RichTextDocument& document) { + // What the parser reads from a comment that states nothing. + const CPDF_RichTextStyle assumed; + const CPDF_RichTextParagraphProps assumed_paragraph; + const CPDF_RichTextStyle& body = document.body; + WideString body_css; + if (body.size != assumed.size) { + AppendDeclaration(&body_css, L"font-size", Points(body.size)); + } + if (document.body_paragraph.align != assumed_paragraph.align) { + AppendDeclaration(&body_css, L"text-align", + AlignName(document.body_paragraph.align)); + } + if (body.color != assumed.color) { + AppendDeclaration(&body_css, L"color", HexColor(body.color)); + } + if (body.weight != assumed.weight) { + AppendDeclaration(&body_css, L"font-weight", WeightName(body.weight)); + } + if (body.italic != assumed.italic) { + AppendDeclaration(&body_css, L"font-style", L"italic"); + } + if (body.family != assumed.family) { + AppendDeclaration(&body_css, L"font-family", FamilyValue(body.family)); + } + if (body.decoration) { + AppendDeclaration(&body_css, L"text-decoration", + DecorationValue(body.decoration)); + } + if (body.letter_spacing != assumed.letter_spacing) { + AppendDeclaration(&body_css, L"letter-spacing", + Points(body.letter_spacing)); + } + if (body.horz_scale != assumed.horz_scale) { + AppendDeclaration( + &body_css, L"xfa-font-horizontal-scale", + WideString::Format(L"%d", + static_cast(body.horz_scale * 100 + 0.5f))); + } + CPDF_RichTextParagraphProps without_align = document.body_paragraph; + without_align.align = assumed_paragraph.align; + const WideString extra_paragraph = + ParagraphStyle(without_align, assumed_paragraph); + if (!extra_paragraph.IsEmpty()) { + if (!body_css.IsEmpty()) { + body_css += L";"; } - xml += L"

"; + body_css += extra_paragraph; } - xml += L""; - return xml; + return RichContent(body_css, document); } // static @@ -444,3 +508,15 @@ bool CPDF_RichTextWriter::Apply(CPDF_Document* doc, Publish(doc, annot_dict, std::move(prepared)); return true; } + +// static +void CPDF_RichTextWriter::ApplyComment(CPDF_Dictionary* annot_dict, + const CPDF_RichTextDocument& document) { + if (!annot_dict) { + return; + } + annot_dict->SetNewFor( + "RC", SerializeCommentRC(document).AsStringView()); + annot_dict->SetNewFor("Contents", + ProjectPlainText(document).AsStringView()); +} diff --git a/core/fpdfdoc/cpdf_richtextwriter.h b/core/fpdfdoc/cpdf_richtextwriter.h index 50eaa6db67..7b86aaf2a3 100644 --- a/core/fpdfdoc/cpdf_richtextwriter.h +++ b/core/fpdfdoc/cpdf_richtextwriter.h @@ -5,6 +5,8 @@ // (Acrobat's XHTML envelope), /DS, /DA, /Contents and the appearance, in the // two halves of the Phase C note §6. Prepare builds everything off to the // side and touches nothing; Publish writes it all, or nothing was written. +// Any other markup annotation's rich text is its comment (ISO 32000-2 +// §12.5.6.2), which draws nothing: ApplyComment writes /RC and /Contents. #ifndef CORE_FPDFDOC_CPDF_RICHTEXTWRITER_H_ #define CORE_FPDFDOC_CPDF_RICHTEXTWRITER_H_ @@ -54,6 +56,10 @@ class CPDF_RichTextWriter { CPDF_Dictionary* annot_dict, const CPDF_RichTextDocument& document, bool* degraded); + // A markup annotation's comment: /RC (SerializeCommentRC) and its plain + // projection in /Contents. The appearance stays: a comment draws nothing. + static void ApplyComment(CPDF_Dictionary* annot_dict, + const CPDF_RichTextDocument& document); // Test-only: forwards to CPDF_AnnotFontMap::FailAfterStagedFontsForTesting. static void FailAfterStagedFontsForTesting(int staged_count); @@ -61,6 +67,11 @@ class CPDF_RichTextWriter { // The four forms, exposed for tests. |da_color| is the annotation's DA // (border) colour, which DS repeats by Acrobat's convention. static WideString SerializeRC(const CPDF_RichTextDocument& document); + // A comment's /RC, as Acrobat writes a note's: the body states only what + // differs from what a reader assumes when it states nothing (its own face, + // 12 pt, black, left-aligned), so a comment written with the body it was + // read with reads back the same. + static WideString SerializeCommentRC(const CPDF_RichTextDocument& document); static WideString BuildDefaultStyle(const CPDF_RichTextDocument& document, FX_ARGB da_color); static WideString ProjectPlainText(const CPDF_RichTextDocument& document); diff --git a/fpdfsdk/epdf_annot.cpp b/fpdfsdk/epdf_annot.cpp index 0e51d4e169..1a92680e25 100644 --- a/fpdfsdk/epdf_annot.cpp +++ b/fpdfsdk/epdf_annot.cpp @@ -1777,8 +1777,26 @@ EPDFAnnot_GetRichTextJSON(FPDF_ANNOTATION annot, namespace { -// The rich text setters share one path: build the document, then write -// all four forms and the appearance through CPDF_RichTextWriter, or nothing. +// The kinds ISO 32000-2 Table 171 marks as markup: the ones /RC is an entry +// of (§12.5.6.2). +bool IsMarkupSubtype(const ByteString& subtype) { + static constexpr const char* kMarkup[] = { + "Text", "FreeText", "Line", "Square", "Circle", + "Polygon", "PolyLine", "Highlight", "Underline", "Squiggly", + "StrikeOut", "Caret", "Stamp", "Ink", "FileAttachment", + "Sound", "Redact", "Projection"}; + for (const char* name : kMarkup) { + if (subtype == name) { + return true; + } + } + return false; +} + +// The rich text setters share one path. A free text draws its rich text: +// all four forms and the appearance through CPDF_RichTextWriter, or +// nothing. Another markup annotation's is its comment: /RC and /Contents. +// Every other kind has none. bool ApplyRichTextDocument(FPDF_ANNOTATION annot, const CPDF_RichTextDocument& document) { CPDF_AnnotContext* context = CPDFAnnotContextFromFPDFAnnotation(annot); @@ -1786,9 +1804,17 @@ bool ApplyRichTextDocument(FPDF_ANNOTATION annot, return false; } RetainPtr annot_dict = context->GetMutableAnnotDict(); - if (!annot_dict || annot_dict->GetNameFor("Subtype") != "FreeText") { + if (!annot_dict) { return false; } + const ByteString subtype = annot_dict->GetNameFor("Subtype"); + if (subtype != "FreeText") { + if (!IsMarkupSubtype(subtype)) { + return false; + } + CPDF_RichTextWriter::ApplyComment(annot_dict.Get(), document); + return true; + } CPDF_Document* doc = context->GetPage()->GetDocument(); if (!doc) { return false; diff --git a/fpdfsdk/epdf_annot_embeddertest.cpp b/fpdfsdk/epdf_annot_embeddertest.cpp index 52ea54d9dd..475b1cf5b8 100644 --- a/fpdfsdk/epdf_annot_embeddertest.cpp +++ b/fpdfsdk/epdf_annot_embeddertest.cpp @@ -4164,6 +4164,130 @@ TEST_F(EPDFAnnotEmbedderTest, SetRichTextXHTMLImportsOverDefaults) { EXPECT_NE(std::string::npos, json.find("\"size\":16")); } +// /RC is every markup annotation's comment (ISO 32000-2 §12.5.6.2). Acrobat +// 26 writes a note's with no body style, Enter as " " inside a span, and +// "font-size:-14.0pt" on every span, a size nothing can be: the body's +// stays. +TEST_F(EPDFAnnotEmbedderTest, AcrobatNoteCommentReadsAsRichText) { + ASSERT_TRUE(OpenDocument("text_rich_comment_acrobat.pdf")); + ScopedPage page = LoadScopedPage(0); + ASSERT_TRUE(page); + ScopedFPDFAnnotation annot(FPDFPage_GetAnnot(page.get(), 0)); + ASSERT_EQ(FPDF_ANNOT_TEXT, FPDFAnnot_GetSubtype(annot.get())); + const std::string json = GetRichTextJson(annot.get()); + EXPECT_NE(std::string::npos, json.find("\"source\":\"rc\"")) << json; + EXPECT_NE(std::string::npos, + json.find("\"body\":{\"family\":\"\",\"weight\":400," + "\"italic\":false,\"size\":12,")) + << json; + EXPECT_NE(std::string::npos, + json.find("{\"text\":\"test\",\"style\":{\"weight\":700," + "\"italic\":false}}")) + << json; + EXPECT_NE(std::string::npos, + json.find("{\"text\":\"you\",\"style\":{\"weight\":400," + "\"italic\":false,\"decoration\":[\"underline\"]}}")) + << json; + EXPECT_NE(std::string::npos, json.find("{\"text\":\"?\\r\\r\",")) << json; + EXPECT_NE(std::string::npos, + json.find("{\"text\":\"Test\",\"style\":{\"weight\":400," + "\"italic\":true}}")) + << json; +} + +// Any other markup annotation's rich text is its comment, which draws +// nothing: /RC and /Contents, as Acrobat writes a note's, and no /DS, /DA or +// appearance. The body states only what differs from a comment that states +// nothing, so the one it was read with is written as nothing. +TEST_F(EPDFAnnotEmbedderTest, SetRichTextJSONWritesACommentAsAcrobatDoes) { + embedpdf_test::ScopedTestDocument doc(NewTestDocument()); + ScopedFPDFPage page(FPDFPage_New(doc.get(), 0, 400, 400)); + ScopedFPDFAnnotation annot(FPDFPage_CreateAnnot(page.get(), FPDF_ANNOT_TEXT)); + ASSERT_TRUE(annot); + const FS_RECTF rect{50.0f, 320.0f, 70.0f, 300.0f}; + ASSERT_TRUE(FPDFAnnot_SetRect(annot.get(), &rect)); + + ASSERT_TRUE(EPDFAnnot_SetRichTextJSON( + annot.get(), + "{\"paragraphs\":[{\"runs\":[{\"text\":\"Hello \"}," + "{\"text\":\"bold\",\"style\":{\"weight\":700}}]}," + "{\"runs\":[{\"text\":\"two\\rlines\"}]}]}")); + CPDF_AnnotContext* context = CPDFAnnotContextFromFPDFAnnotation(annot.get()); + const CPDF_Dictionary* dict = context->GetAnnotDict(); + EXPECT_EQ( + L"" + L"

Hello bold

" + L"

two lines

", + dict->GetUnicodeTextFor("RC")); + EXPECT_EQ(L"Hello bold\rtwo\rlines", dict->GetUnicodeTextFor("Contents")); + EXPECT_FALSE(dict->KeyExist("DS")); + EXPECT_FALSE(dict->KeyExist("DA")); + EXPECT_FALSE(dict->KeyExist("AP")); + + const std::string json = GetRichTextJson(annot.get()); + EXPECT_NE(std::string::npos, json.find("\"source\":\"rc\"")) << json; + EXPECT_NE(std::string::npos, json.find("\"family\":\"\",")) << json; + EXPECT_NE(std::string::npos, + json.find("{\"text\":\"bold\",\"style\":{\"weight\":700}}")) + << json; + + // A body states what differs from that: here its size and colour. + ScopedFPDFAnnotation square( + FPDFPage_CreateAnnot(page.get(), FPDF_ANNOT_SQUARE)); + ASSERT_TRUE(square); + ASSERT_TRUE(EPDFAnnot_SetRichTextJSON( + square.get(), + "{\"body\":{\"size\":10,\"color\":\"#FF0000\"}," + "\"paragraphs\":[{\"runs\":[{\"text\":\"Note\"}]}]}")); + EXPECT_TRUE(CPDFAnnotContextFromFPDFAnnotation(square.get()) + ->GetAnnotDict() + ->GetUnicodeTextFor("RC") + .Contains(L"xfa:spec=\"2.0.2\" " + L"style=\"font-size:10.0pt;color:#FF0000\">" + L"

Note

")); +} + +// Acrobat's comment, read and written back, reads the same: the redundant +// "normal" on its spans kept, its impossible size gone. +TEST_F(EPDFAnnotEmbedderTest, AcrobatCommentWrittenBackReadsTheSame) { + ASSERT_TRUE(OpenDocument("text_rich_comment_acrobat.pdf")); + ScopedPage page = LoadScopedPage(0); + ASSERT_TRUE(page); + ScopedFPDFAnnotation annot(FPDFPage_GetAnnot(page.get(), 0)); + ASSERT_TRUE(annot); + const std::string before = GetRichTextJson(annot.get()); + ASSERT_TRUE(EPDFAnnot_SetRichTextJSON(annot.get(), before.c_str())); + const std::string after = GetRichTextJson(annot.get()); + // The diagnostics differ: the size Acrobat wrote is no longer there. + EXPECT_EQ(before.substr(0, before.find("\"diagnostics\"")), + after.substr(0, after.find("\"diagnostics\""))); + const CPDF_Dictionary* dict = + CPDFAnnotContextFromFPDFAnnotation(annot.get())->GetAnnotDict(); + EXPECT_FALSE(dict->GetUnicodeTextFor("RC").Contains(L"-14.0pt")); + EXPECT_EQ(L"test test how are you?\r\rTest", + dict->GetUnicodeTextFor("Contents")); +} + +// Only markup annotations have rich text: a link's or a popup's write is +// refused and changes nothing. +TEST_F(EPDFAnnotEmbedderTest, SetRichTextJSONRefusesKindsThatArentMarkup) { + embedpdf_test::ScopedTestDocument doc(NewTestDocument()); + ScopedFPDFPage page(FPDFPage_New(doc.get(), 0, 400, 400)); + for (FPDF_ANNOTATION_SUBTYPE subtype : {FPDF_ANNOT_LINK, FPDF_ANNOT_POPUP}) { + SCOPED_TRACE(testing::Message() << "subtype " << subtype); + ScopedFPDFAnnotation annot(FPDFPage_CreateAnnot(page.get(), subtype)); + ASSERT_TRUE(annot); + const CPDF_Dictionary* dict = + CPDFAnnotContextFromFPDFAnnotation(annot.get())->GetAnnotDict(); + const std::string before = SerializeObject(dict); + EXPECT_FALSE(EPDFAnnot_SetRichTextJSON( + annot.get(), "{\"paragraphs\":[{\"runs\":[{\"text\":\"a\"}]}]}")); + EXPECT_EQ(before, SerializeObject(dict)); + } +} + TEST_F(EPDFAnnotEmbedderTest, SetRichTextJSONWithRegisteredBodyFont) { ScopedRegisteredFonts scoped_fonts; std::vector roboto = LoadRobotoFontData(); diff --git a/public/epdf_annot.h b/public/epdf_annot.h index 8ffdc98ba5..bf87aa6fb5 100644 --- a/public/epdf_annot.h +++ b/public/epdf_annot.h @@ -493,8 +493,9 @@ EPDFAnnot_GetRichContent(FPDF_ANNOTATION annot, unsigned long buflen); // Experimental EmbedPDF Extension API. -// The rich text of a FreeText annotation (/RC over /DS and /DA) or of a rich -// text field (/RV), as UTF-8 JSON: {"source":"rc"|"contents","body":{...}, +// The rich text of a FreeText annotation (/RC over /DS and /DA), of another +// markup annotation's comment (/RC), or of a rich text field (/RV), as UTF-8 +// JSON: {"source":"rc"|"contents","body":{...}, // "paragraphs":[{"align","dir","runs":[{"text","style":{deltas}}]}], // "diagnostics":[{"code","detail"}]}. When there is no rich text the document // is synthesised from /Contents (one paragraph per line break) and "source" is @@ -521,7 +522,13 @@ EPDFAnnot_GetRichTextJSON(FPDF_ANNOTATION annot, // change nothing. An unresolvable family is not a failure: it substitutes // and the run reads back "degraded". // -// annot - handle to a FreeText annotation. +// Any other markup annotation's rich text is its comment (ISO 32000-2 +// §12.5.6.2), which draws nothing: /RC and /Contents are written, the body +// stating only what differs from a comment that states nothing (its reader's +// face, 12 pt, black, left-aligned), and the appearance stays. A kind that +// isn't markup (link, popup, widget, ...) returns false. +// +// annot - handle to a markup annotation. // json_utf8 - the document, UTF-8 JSON. FPDF_EXPORT FPDF_BOOL FPDF_CALLCONV EPDFAnnot_SetRichTextJSON(FPDF_ANNOTATION annot, FPDF_BYTESTRING json_utf8); diff --git a/testing/embedpdf_layer_mode_writers.txt b/testing/embedpdf_layer_mode_writers.txt index 0fe63f0ca3..c2f6129dbc 100644 --- a/testing/embedpdf_layer_mode_writers.txt +++ b/testing/embedpdf_layer_mode_writers.txt @@ -156,9 +156,11 @@ EPDFAnnotEmbedderTest.SetNameAcceptsAnyNameAndKeepsAppearance 4 EPDFAnnotEmbedderTest.SetNoColorWritesAnEmptyColorArray 3 EPDFAnnotEmbedderTest.SetNumberValueRefusesWithoutWriting 3 EPDFAnnotEmbedderTest.SetNumberValueStoresTheNumberAsGiven 3 +EPDFAnnotEmbedderTest.SetRichTextJSONRefusesKindsThatArentMarkup 3 EPDFAnnotEmbedderTest.SetRichTextJSONRejectsInvalidInputUnchanged 5 EPDFAnnotEmbedderTest.SetRichTextJSONWithRegisteredBodyFont 12 EPDFAnnotEmbedderTest.SetRichTextJSONWithoutBodyKeepsCurrentBody 7 +EPDFAnnotEmbedderTest.SetRichTextJSONWritesACommentAsAcrobatDoes 3 EPDFAnnotEmbedderTest.SetRichTextJSONWritesAllFourForms 6 EPDFAnnotEmbedderTest.SetRichTextXHTMLImportsOverDefaults 6 EPDFAnnotEmbedderTest.SetSubtypeChangesTheKindInPlace 5 diff --git a/testing/resources/text_rich_comment_acrobat.pdf b/testing/resources/text_rich_comment_acrobat.pdf new file mode 100644 index 0000000000000000000000000000000000000000..227a6ea0aba660bdb9d32a417c3813cf3fb3953d GIT binary patch literal 6584 zcmeHMc~leE8fOtyBrb>=o~;f6Tq?;dnM@YL5(p6}0VHv$3Nj=UjAp?s5`FavSS^ZB zs?@5rJw6_a^t1wsdbB(RWT}W?TYXlkRRP7iRaD%H`fdV3EZFlr=k>hzhdC#6X6D{+ z`+dJV^WAU!V#N|B$|7ifEv-#I(A-cALLhz8VwzA0N2zrbX;81Aln@F>K?H|~!!Zzw z!(kA{=CR>ua9a)$7`Pn{&4+ND2L`$Efq^tir?l#T2K>=bUY`vyrO+#>Skj<10*j$~ zjajQRLO2|2pp&LZ_rG^FiP! zX1!49Aru7;#=%jPF4?pQ;-FSvx`nXU7P{Bf45b(SMpdzYutwtQ+UPLk zWXkybkS^c6Ri03VLx;cXuKwQI(CKkWN|{I{X(9nCRz#yoEoFpIzU?B$Y|;RR86m=U z8A~RE^X?%VqM<-eqfhRm6N>l6D$?onCL@F)HrY^vJ|#qNnUAo*HTV-K298`lIL5GLJTUO9#X%hp5uK&0(S z1;*jRcDCR!i)n_;Y7DG_88QSMMG-PcMQV%`93q9I^#(1efkQV z<3{Uo)#j*b+9}Tm?T;Q$o0xlLl>4w!FI!T^l5GO&lSub)hmNBj?ysmv7(ck*@*E4~XO z()n)AlM%FIP;K@$I!*VJW##k?_EuE$Y}!Y0i8m`ejk9GVwwdsb*m4g>0vEa6xUOUE zWrxaR@BZ~Iud4Sw7F^~$@SBoZy@t(-p%48)QaO6{!bEyr*^d0yRDFKiT)nefVadzi`f0tE!0kXjD%fbMUGKtz?G(i}r_i+YZMA6_)VS82plFLlm zK9Vo`xm@0y^BFSoX~xsRS&tWBpfvMbK{K7BI0 zpmHDQJ#-v)V)!POsp}M-g3@qrqdm4{TB`&d^C2=*v;l3^7ji+y3K$ljo*a>4qM9yJbY?@<-1++BNsr2mLhS z`w#rKev14sVH7D~J&c0Q!cDS^545+3ePo1ccc!(~du%+G^&qv3V#>|C6V}(Bcnod- zqTVSW>u`F*B-j0!Xm*vDN*WfvcIsEbcjU5ltxk6$>l z^7a<#;c?W4N_^kkI(n7j$9>tW&$YC+Pt4y}Hap@g=d1JF?@uo;5)06ZvMuTPB_Z!6 z#B2yTEAb>|`L8;^`@8m#s13~fm72SH1~TKUIGE^N9cQlpxHCkuv_XK3y$zLVg04gGZb ztPvewb~Y8r1?v-aW<{&a&-Z+7n9m)jZA{Mf4;K{w%Ja!eJ9ha!mztAVxwHV_l+}G8r2=gi&uW%ViX2hQb@%T$^<1*$!c8y9Gf{0vL--rW64Tr>3T|QrRrMAsI&bd_Ig|ForR~45o3p z&O}<6I-^gwk{5c!lu=<&rA@9x%X92=@)y&9@~>Al56))}%&=1I=c&QoutM6kEmR zGI2gYl)!M3$)h+bCQn6RNRk2}6&$wB4XM;#wx8Yz9oSgt6x0j791;Y86Kp0$MPgt;uiL^#-ojLg2L~zhT#Z z4KBC7Ed^+i1~5{=hN1tC<&RsV-Ted<>D`Rs){Zvzs?FG5IoOQhu?Qiv+lW1EjGCH5 z;Es_lHP=6sk1e=9*mqaq*yeMNXCC=o-8FW`lCIyHV(W5`=QiXv<<{*udC9%Nn2wD3 zcADc|npc#UJN+wq1HB==U{r2$^^Oyl>MzxebSt3YG^uE9E7y-6-tMpz%l*7QH)KPR zDseK<3tGhgI^iGMKUTZe=qr0Cy{!`z;dD3et*yfTtrHB?=&{x)S&>rz;|)#52>*yP zDW#yi#n?EGpopuJQS0nuc6K&PGr#O&(CYHK3gj+zZTVdV)gCTB>;RttA0KB$sC4DU zF3%{RsHl;WL7rC%^ClIxpvM}AgU<0k-kn_1KM1>?>i@-efqtI?aDM*}0B8?o{@??E zgVQ8Q^eWd$uC>j<8OftFJ6*+ZxkQ#vaokI%)5$?U1ktlPf`Zc=9gg1~Jh4A4eeHh~ L1s`f`9{~Oh)o4kZ literal 0 HcmV?d00001