From cfd0c2fc30f92deb48bcddad98af954e37dfdb1f Mon Sep 17 00:00:00 2001 From: hand-dot Date: Mon, 11 May 2026 15:44:12 +0900 Subject: [PATCH] feat(jsx): reflow dynamic box containers --- PLAN.md | 12 +- .../common/__tests__/dynamicTemplate.test.ts | 77 ++++++++ packages/common/src/dynamicContainer.ts | 44 +++++ packages/common/src/dynamicTemplate.ts | 16 +- packages/common/src/index.ts | 10 + packages/common/src/types.ts | 9 + .../__image_snapshots__/jsx-form-fields-1.png | Bin 37428 -> 38143 bytes packages/generator/__tests__/generate.test.ts | 175 +++++++++++++++++- packages/jsx/README.md | 3 + packages/jsx/src/__tests__/render.test.tsx | 28 ++- packages/jsx/src/render.ts | 31 +++- packages/schemas/src/dynamicLayout.ts | 87 ++++++++- .../jsx-form-fields/source.tsx | 2 +- .../jsx-form-fields/template.json | 15 +- .../template-assets/jsx-invoice/template.json | 9 +- .../jsx-japanese-notice/template.json | 8 +- .../template-assets/jsx-report/template.json | 10 +- website/docs/jsx.md | 9 +- .../current/jsx.md | 8 +- 19 files changed, 516 insertions(+), 37 deletions(-) create mode 100644 packages/common/src/dynamicContainer.ts diff --git a/PLAN.md b/PLAN.md index df4350ff..fbd5e935 100644 --- a/PLAN.md +++ b/PLAN.md @@ -28,12 +28,12 @@ container、md2pdf pagination / block layout、workspace 運用の順で磨く ### 1. `@pdfme/jsx` layout / dynamic container フォローアップ -- Form 入力や dynamic layout reflow で `Text` / MVT が runtime に `overflow: "expand"` した場合、 - JSX の親 `Box` / container は自動では広がらない。生成後の `Box` は rectangle schema であり、 - 子 schema の実描画高さと親 container を結び直す contract がまだないため。親子 container dynamic - layout は実装したい重要課題として扱う。 -- まずは JSX から生成された visual `Box` と子 text/MVT の関係を metadata として template に残す案と、 - 単一 text/MVT 子の decoration に畳む案を比較する。 +- JSX から生成された auto-height visual `Box` は、子 schema 名を内部 metadata として持たせ、Form/Viewer + や generator の dynamic layout reflow 時に `overflow: "expand"` した子 text/MVT の高さへ追従する。 +- 現在の対象は同一ページ内の parent/child decoration reflow。child schema が page break を跨いで split + する場合、child は分割できるが、親 `Box` decoration はまだ multi-page container としては分割しない。 +- 次に必要なら、multi-page container decoration、nested container の厳密な layout tree、単一 text/MVT 子の + decoration 畳み込みを検討する。 - CSS/Flexbox 互換を目指さず、flexbox の使いやすさだけを `Stack` / `Row` に取り込む。 - `flexWrap`, `flexShrink`, media query, full `style` prop, CSS parser は当面対象外。 - `%` width は将来検討でよい。まずは `flex` / `flexGrow` で比率指定を表現する。 diff --git a/packages/common/__tests__/dynamicTemplate.test.ts b/packages/common/__tests__/dynamicTemplate.test.ts index 4d7f5937..a00e1bfc 100644 --- a/packages/common/__tests__/dynamicTemplate.test.ts +++ b/packages/common/__tests__/dynamicTemplate.test.ts @@ -1,6 +1,10 @@ import { readFileSync } from 'node:fs'; import * as path from 'node:path'; import { fileURLToPath } from 'node:url'; +import { + getDynamicContainerMetadata, + setDynamicContainerMetadata, +} from '../src/dynamicContainer.js'; import { getDynamicTemplate } from '../src/dynamicTemplate.js'; import { Template, Schema, Font } from '../src/index.js'; @@ -68,6 +72,16 @@ describe('getDynamicTemplate', () => { }; describe('Single page scenarios', () => { + test('should ignore non-finite dynamic container padding metadata', () => { + const schema = { ...template.schemas[0][0] }; + setDynamicContainerMetadata(schema, { + childNames: ['a'], + paddingBottom: Number.NaN, + }); + + expect(getDynamicContainerMetadata(schema)).toEqual({ childNames: ['a'] }); + }); + test('should handle no page break', async () => { const increaseHeights = [10, 10, 10, 10, 10]; const dynamicTemplate = await getDynamicTemplate( @@ -83,6 +97,69 @@ describe('getDynamicTemplate', () => { ); expect(dynamicTemplate.schemas[0][1].name).toEqual('b'); }); + + test('should resize non-flow decoration schemas without pushing their children twice', async () => { + const dynamicTemplate = await getDynamicTemplate({ + template: { + schemas: [ + [ + { + name: 'box', + content: '', + type: 'rectangle', + position: { x: 10, y: 10 }, + width: 80, + height: 20, + }, + { + name: 'a', + content: 'a', + type: 'a', + position: { x: 14, y: 14 }, + width: 20, + height: 5, + }, + { + name: 'b', + content: 'b', + type: 'b', + position: { x: 10, y: 30 }, + width: 20, + height: 5, + }, + ], + ], + basePdf: { width: 100, height: 100, padding: [0, 0, 0, 0] }, + }, + input, + options, + _cache: new Map(), + getDynamicHeights: async (_value: string, args: { schema: Schema }) => { + if (args.schema.name === 'box') { + return { heights: [40], contributesToFlow: false }; + } + if (args.schema.name === 'a') { + return [30]; + } + return [args.schema.height]; + }, + }); + + expect(dynamicTemplate.schemas[0][0]).toMatchObject({ + name: 'box', + position: { x: 10, y: 10 }, + height: 40, + }); + expect(dynamicTemplate.schemas[0][1]).toMatchObject({ + name: 'a', + position: { x: 14, y: 14 }, + height: 30, + }); + expect(dynamicTemplate.schemas[0][2]).toMatchObject({ + name: 'b', + position: { x: 10, y: 55 }, + }); + }); }); describe('Multiple page scenarios', () => { diff --git a/packages/common/src/dynamicContainer.ts b/packages/common/src/dynamicContainer.ts new file mode 100644 index 00000000..741ea117 --- /dev/null +++ b/packages/common/src/dynamicContainer.ts @@ -0,0 +1,44 @@ +import type { Schema } from './types.js'; + +export const DYNAMIC_CONTAINER_METADATA_KEY = '__pdfmeDynamicContainer' as const; + +export type DynamicContainerMetadata = { + childNames: string[]; + paddingBottom?: number; +}; + +const isRecord = (value: unknown): value is Record => + typeof value === 'object' && value !== null; + +export const getDynamicContainerMetadata = ( + schema: Schema, +): DynamicContainerMetadata | undefined => { + const value = (schema as Record)[DYNAMIC_CONTAINER_METADATA_KEY]; + if (!isRecord(value) || !Array.isArray(value.childNames)) return undefined; + + const childNames = value.childNames.filter( + (childName): childName is string => typeof childName === 'string' && childName.length > 0, + ); + if (childNames.length === 0) return undefined; + + const paddingBottom = + typeof value.paddingBottom === 'number' && Number.isFinite(value.paddingBottom) + ? Math.max(0, value.paddingBottom) + : undefined; + + return { childNames, paddingBottom }; +}; + +export const setDynamicContainerMetadata = (schema: Schema, metadata: DynamicContainerMetadata) => { + const childNames = metadata.childNames.filter((childName) => childName.length > 0); + if (childNames.length === 0) return; + const paddingBottom = + typeof metadata.paddingBottom === 'number' && Number.isFinite(metadata.paddingBottom) + ? Math.max(0, metadata.paddingBottom) + : undefined; + + (schema as Record)[DYNAMIC_CONTAINER_METADATA_KEY] = { + childNames, + ...(paddingBottom != null ? { paddingBottom } : {}), + }; +}; diff --git a/packages/common/src/dynamicTemplate.ts b/packages/common/src/dynamicTemplate.ts index ac8d39db..6adfd6eb 100644 --- a/packages/common/src/dynamicTemplate.ts +++ b/packages/common/src/dynamicTemplate.ts @@ -1,10 +1,10 @@ import { Schema, Template, - BasePdf, BlankPdf, CommonOptions, DynamicLayoutCallbackResult, + DynamicLayoutArgs, DynamicLayoutResult, } from './types.js'; import { cloneDeep, isBlankPdf } from './helper.js'; @@ -20,12 +20,7 @@ interface ModifyTemplateForDynamicTableArg { options: CommonOptions; getDynamicHeights: ( value: string, - args: { - schema: Schema; - basePdf: BasePdf; - options: CommonOptions; - _cache: Map; - }, + args: DynamicLayoutArgs, ) => Promise; } @@ -250,7 +245,9 @@ function processDynamicPage( // Update offset: difference between actual and original end position const originalGlobalEndY = item.baseY + item.height; - totalYOffset = actualGlobalEndY - originalGlobalEndY; + if (item.dynamicLayout.contributesToFlow !== false) { + totalYOffset = actualGlobalEndY - originalGlobalEndY; + } } sortPagesByOrder(pages, orderMap); @@ -317,6 +314,9 @@ export const getDynamicTemplate = async ( basePdf, options, _cache, + input, + schemas: template.schemas, + pageSchemas, }).then(normalizeDynamicLayoutResult); }), ); diff --git a/packages/common/src/index.ts b/packages/common/src/index.ts index 181cfba4..ae8f52ac 100644 --- a/packages/common/src/index.ts +++ b/packages/common/src/index.ts @@ -83,6 +83,12 @@ import { } from './helper.js'; import { PAGE_SIZE_PRESETS, detectPaperSize, resolvePageSize } from './pageSize.js'; import { getDynamicTemplate } from './dynamicTemplate.js'; +import { + DYNAMIC_CONTAINER_METADATA_KEY, + getDynamicContainerMetadata, + setDynamicContainerMetadata, +} from './dynamicContainer.js'; +import type { DynamicContainerMetadata } from './dynamicContainer.js'; import { createDynamicLayoutSplitRange, getDynamicLayoutSplitRange } from './splitRange.js'; import { replacePlaceholders } from './expression.js'; import { pluginRegistry } from './pluginRegistry.js'; @@ -110,6 +116,9 @@ export { getInputFromTemplate, isBlankPdf, getDynamicTemplate, + DYNAMIC_CONTAINER_METADATA_KEY, + getDynamicContainerMetadata, + setDynamicContainerMetadata, replacePlaceholders, checkFont, checkInputs, @@ -178,4 +187,5 @@ export type { PageOrientation, PageSize, PageSizePreset, + DynamicContainerMetadata, }; diff --git a/packages/common/src/types.ts b/packages/common/src/types.ts index bf978d97..b10c0f4f 100644 --- a/packages/common/src/types.ts +++ b/packages/common/src/types.ts @@ -265,6 +265,12 @@ export type DynamicLayoutPatchArgs = { export type DynamicLayoutResult = { heights: number[]; avoidFirstUnitOnly?: boolean; + /** + * When false, the schema is emitted with its patched height but its height delta + * does not push later schemas. This is used for decoration schemas whose flow is + * driven by child schemas. + */ + contributesToFlow?: boolean; patchSplitSchema?: (args: DynamicLayoutPatchArgs) => Partial; }; @@ -275,6 +281,9 @@ export type DynamicLayoutArgs = { basePdf: BasePdf; options: CommonOptions; _cache: Map; + input?: Record; + schemas?: Schema[][]; + pageSchemas?: Schema[]; }; export type GetDynamicLayout = ( diff --git a/packages/generator/__tests__/__image_snapshots__/jsx-form-fields-1.png b/packages/generator/__tests__/__image_snapshots__/jsx-form-fields-1.png index 45cd260846c31e603cde1806f1cb830c6d7fb7b0..80429aefcb8cfdd56562a80b197e068da8b84ea3 100644 GIT binary patch delta 20085 zcmce;cT`kQw=D{y@I#V{h-48Zh=AmrL_j2qNCwH0a}Ha{ND|2zB^O6?;GRY^Uk~Dj&c7{*>u-lwQAK|bIny{Ro1PW{99%Jh(cpyG@XZ}9{Eoe zeif&u^T{OFBU9^Y&GL;k&0F91s>YfbYpi|dv)>O(zjezxN6y%Hr+NMA3UmG`dv&4m zxtj=eYiGnoGu)$P{h}G^3{Q1vS^t!dT=DY1rchpw8N9Q=OrlD) zsgdiz`}Rqnxzb;;n_%do<5^ZlX`%dyx#B!ha;n;bl#r0HNWI+0q(5*93v>~OiKTpDKhR7sWT;kVB4{)exHQvH}{{qk-*ZI83KOzgHozo#lqY3i{Q|2Z^z zkmGmnH3p+86X|Oq3bHS(;HQ5wZsCFm!?oJ0FuF)*HB$h;B%p!CMtTi;ksGAQny@?q;mBLgYELW$tX#KG`Lu)+R_R4(o zbd@R9drR3L#uk{I-DSE(`_abHet4q)4sG1)JO8r%f9(O=SXRni$loVx)RBwZDEKk0 zE_c3Jg36GAR?W_oR2UzA>nZt{eeF<8Xu<=u|CGL>{SIs zEp1YSTFtD1IP*;;@6IRVtM~YskpYEUQ7|t>y$=rhmgy{Sn8;NS5|zZ@-Y|{7eIsB> zIje+m?k;e=J5KG9n}qR-) zS2Fmhp0$RBj5|*-U<{wnd*BXpNWk&Fes8%0YnG`=<$gRC{aB&C`WS`|l@%8kx2-)g zOHGa9;a&fVWCfmK=*4ri`9->@S7RO1K7>3h7)lsAf>moZE>Fa1BaK#94E7zPf;RXn z?ClQ^i5AbPL@O%nF&J5ye)B%@>)RRbL`r0K4|LPLpr@|X%*o3yT-=4y&ay_|2}}RW zF?{CVM0qkUJLhnNvdnxnH$LAWBz`}L8aZh2i0~G+gz7guLmtUfyf0PghnlFtR%vh5 z`J_%Mzm@P_RWG-q@JPGqh(!e^>(o{N2De-6A&@Iji+b`etDov_Hlk7){PoEfGQyMcfUMbCc3k!?-BC)Pc{k4Py=0CW&w5+VB`Qc7mqcU<~VO_nwD%M(+ z?j)ryn;$geaEK0$jxLi-pQ2qrD*tB`4k^8V@3TEtcy@mNQ;9LP4sv%Fur4X_^P4R_ zK%JjZ*dDFmVm!Qihu3CaOh*Fii&PjnyKC1yj6Z)~cXxF?VW<6ia8PD%JlhHp@v5m+ zR8*w4k=+FjTr!i1Kc^5;uZcG746iV};Gvg)tY~LS6olYeO;@P~RL#^{AO1Z*!MLn)V91mX`-D1*i03yHNNPoqf5BUa zAx_lv>{fe>-A^{8l#~|m1RT)Oc9~#ethNAu@$8wOx0eSq&BX;OBPS;yEsZ8Mm6}ht z;npuf8@9YxPn5@fs#ltkb<2xTaCtDUVMpldoWBjT-i1=k3)R8V!Dl?Dn2pX?J(qWE zmX~%u=qJ!ghcE5AJbLi(VOLY_{K}kU2pQk!dssLGPr12cCMVTd`2`Hj%ucjR1JRN9 zf?j`%y|2ID&bFz@a~7--aNzyLMc|}X?ZFpJO!;M@RaIQaCnxMY5IbhNQxcC~pubw( zeze&LpX0%k{Cp-Zv*DHU6OLD}Fw!-v&5h!mHvIzw%a5;+%8U<<){REW^xIn9#{U%% zA0OnTMrOx6mrvwkBCWz`Vt)M0+Q{gQvjPuOclYC@_=VL?8N{Q0=9ex93$$FdmL1Z) zaiR_>BI*s%{`;)ZPc7u0<<+7G8813;FMW;rMmafnYk!T`U~P|=EFN#FcsAMJ!5G6Q zuyb+A%*whgAt5n4JFBFoW;&v#$LTqFZ@fgK-v@Pig z1J(=;2M_!2si|OJ9HIrsRU~UW^_<@NWO*iRu?UT5&>;W_Q`FdXTqE&4nGx*j!69#Z zU#$R35Y@_q; zTS!KRmX?;9xp2+T-~VTBp|lkz=iR$^>pYOGJUl!rYist~Yx0gxPUWAcqjz>*ZcW(y zy7p>~jEZX8;a$sf)B$q{V!#a#%GGGoRIIl7AIP=tKe3(mM8Cl2)d|O2Sy{t9ib|9d zlG+&KAy_osXf=L08D6+-PZ!h`r02XF`yY7r=|PUJ0nz>?G5Ab-2;Kj~ZLTH5sRVnj zD-s58SaX@xfE5jnXIo!B_%aAt?@+e;t2-;w~pR=(fQ zl60Tq0^>_xpIlKQ_j6ueq(69A|2j3V@JT+6HRB9pDZ?9I@*QGh}93HhGc(htB zVil;Sqten!hs$p)7X@vYZ^9LW)~~O=P0*vQzwQNN2j}F_%f_-Ak934#SYFsMJHS1O zg@uJN3=DqOZ!Nv{)J{I)o+#B(xu&{*zXabvS67$u;awqN;a>SHvd53bz?v>D>I0WU zA~mewm$OZFzCJ#;R(Wh}Y~Hw*X7-wf^-&0Zu#Egt? z4-F|}7#SNc?(av`*7A}Lq;>wCN`T_yKaY-$4Gj)H;p9Y=_wJ+7|3;Dn%l%FcEMCSV z$@$rHdi#T4-rCx8nCi`2#M;`)?f`Y=srS$aD z>K*2P?M^%4`3V3) z<^y})pH%qi;WnEn!oAiS3Z)hf7%TjkQBpDxo+4^@x@0-rSI=jtth>3~m^5E(b2EAf}Po%QbQ#v}kI!FJlvBFA_hy+DR!| zZhz`p_^RCkIR=Y(UD$UIMGj?leveF5FxPy6p{Ao9&t=A;)8K6U_uFcbI*J`bSvs?9 zcknBfAoMyzNvX7CinEmsy-Qmo9TCDEeHxYZF8kDoH8zYVjIV;mi?uRya-Tg5+d1P#&<{k=4;E>evM#d%|)xCYO2{alk$(#+PIK8$W8Y3yi07_9uJ4tSBZVokj5P3E=blR1i&vSANTH^kcP;Kq&&mr_%Vc%fP#Hg&6Wety1(8zCgLH~7<^8>2y> zp{H>Xpk(yd?NXsx1}65%*(UAKoRMySm6;mh|AW5bq~mP|`30ni=l(am`(=Ib_}Pli!+ZYa}z9{Q6g z8%~%k?1K5ldajXkJ#1=Yd)r}qjbN?M3lY3ptW_IeF)_>UvBs4Km19C6q=Cl<{)U`l zd?@}%f)g0Bva)gyxP|8tAk@_TtvESU9HH(;WzHATDZ+mQE8dy`q zgNb?l()D6jx+j6diQE_abiMg+$Xj{&>;N-@2FKmIVy)g+Q{`TQvT4&MK8R3Wb`j^G*o}BS)t!I~jHp6Q_gI(;TyroQRj391iDP9g|ba#pc}R zNn<%0oU#~4M@MyT%h&)t82FiRAqWbUmwUy*@kS3)q%kBWE~#&f(-NB!gMUe^_SVB= zVr2YI&TVdBX&DNrQ@4x^pb>)=#=X)5Y|o$jH@383#B0TaRTD-d@9}Z_`1r?qJ$-#z zUYl7lEv=-}nwp-E)hk)go<60E!Fvq{mYT%I_OSk`n^gDl_03mnr5-EzNXH^ffENPg ztd>|AyuM|3cvxjVwiUde01_%xs^(@Tf~yi!Hh9>#ITFw1cK7NWdD9ue0w_w;Kc)8Z zKiE}bKDFm513T2v)j9|S%C7kQcS=JaaKX0pJG1!_`#fKF zZ4|AuVy!LB{2r%$5MJqo1aK!Mzx^4FU@90W<1TFHWttMoVPCKxX>JGFR?SseBq6Q9@vRxEGS85xHaB^*!SS z6+*+v)OgYTWJ}ap2+dJR9^|}OSljv*nQ>qA<_Ka2e=wWEbkL5o*xQ@btNRjC$qA4e z;|zte>z_-Roz-U4I!0ci9P0Q4?T~vq_6`n*M@R2tcbAtX6ch-fb}rlfAwlOY zKi_5JODtdYrZeH1>jSK4|CT~l*4bu{IMMlNvIq7wfTMHVa0$8tkFyR8JdC{qwSjd4 zi_#-})9D}Q<>kfH1zXoW5MIUKBBE}$9sBDK7H!p=h*Yn|>w*XHuby2%#y}Ch4BifE z1&oQ=xx?e*th&0#{;z*?ySS`g|NZ-Sw#6&SJRm?Wl|!{&n3X3M7?G8g#Yhtv7>EhL zJ_P)257*+gxn6~;(P<==7x=vFv$?pK7zXLCEW;od4?E_s?#;VX{8M#xd{NQK?ZL#I zU>i}yvGNaJov^-F90@J^{Wmvt_9g?RvR1 zUEydJBe)GJTpa7d0aO_+@S~K9rpUHH=9~GlMLe(W#&; z-TXo4i*uTrmlxF3B(yf*w!Gi6i_S;zr!EN9bcK`#OL%z{O$+=_;Eh%X4vlwvo!xQ* zrztB)%O3`g=86)YgOcXy^XHKN;Gj~8c1vH?CTCeig+WJXg5%zBwstR!#($P5!@SzO zFPS@}!D$Q8B!zq0wkG;nwrIB9N4n1dz_+ zezK=ivUk_tN53tQ)2l=70r7*$W(!hXk(Rvt{KL(L>bm*{4!dEaa2nB$CKMi@-4fq& zry`?TM?#Cp)u?apkLisg<8XZJVhztpPe*4F(Ky%_G{#gt zlM|N89Z1<6t>+XM{~Aio|G;89cC_l$M;UopRcB-2Fm(F)^h*qz z;IY^ZCe~!#CbM}`g7QpX(X97f|67K{h7B(J&~KE3DZlN)y+@Da`JHzdF|RR?w=X=t zFuiy|&%xo7z2B;+uS^R73e7DyHWjibxT3(I3w0mP>+o z{M`_w3v2E%r7hP|Q%iU5Q9lZ3OaiEMNsK{2!SzN$!q<6sdJ)tIMsIG1JZkOCmJP=^ z-kd?^!?DnHzT5X#s$Y+f9YKvWG&J;kc=#Kb_VT(5DQ6{si8w~2ht1(r5shkWEiJ8< z*#m$3#)gIlj_mIOv|OLs_Lg($XZWqeqfGVVi$$O+HffZIa|V|JUxYg>hYX@?W=v- zYz4a(1_;4rUt?L2=L~k#8#{HQa{3dOHaCM`XNUpGNKuifsE8UOP>L9-von;Fmjl)M zWtT%@Vj`bT{c9w(Fpb8u>7QmQ9iddc?J`llR{ImJqPYsGzls#71e~|pbL92jYisxU z)WXHl35P)@zTit`B{8$edC+}2Y4EGi(g@z9mUD6}EUdeDcqnHC@A2+L8hH-4ZG(M+ z$8T}%R_Cx(sQDe~akbkncgwXou}rrNKDCfvU#OhO`2qCCFOZm+3eC(+VX41CtwJji z@PL9eBrJ^J!h0s9?Qh6sg@ud%Nkw(_-|2?ut=C7KoTmNgbkE~?$RGRhqpO?yQFp-@ zS_9bIlLEvxj*`l3s(=I=8yn4;8`(3>MxYuNYwHp~zQi?nm(&~WfcBI*`}8klV)8Mt zTP5d}`{+NKHJ1KY{)l9LkL&!>{EQ4*5P5A&lKaE0HyFt&De8@TDpgfgWt9}2mu@!Y z7O;6~-CD;@4tn{-FD+j&hm@5^3-!_fVIQv56BU@QS?}Bx1Qo*@2_4^^9ZLwHaaw}{ z8oCfqED*vbRxRME(B7=LtSsIPA=TjEAjT6Rq0p|Nw=WYlm5LK-sa^Kcb4wMYI>Nep zVVTx*?mc%xP6+Tn!&`)L4-ao+V-FWNS^W6%AYZGN`uzNSw%#Fdr|9L@XtA%qq{@Uuj}Wrp=7Fc3#0A)&|r8x3niQFgY!Dq_vZ zC<_QKbc~H7EWEsNHM`T1-|v-bJ5CP${z@lJh*+-KjnEDhWjnpJ%~+q9uI>EwEB&|u zUM~S~ZE&0wV!67!%+qTX87oj#vBtZ5_n8Q5YQxEvfweWJm;$6gSZg!)(<6c!BjROE za#B+7u2yY&(Zz|rT2@ja^RA`k;KT$27nk49pHEkR4go)1Ym^>Mdjb-npRo)PgR1{y z>9t>J%X|_3XS5e9-DBFK<-!i`_V)I+{r#J;+N<{NJG$d=O;5&oa(85G?5fx9J^0zr zmBTFI0q8&c*Ubqa!8cW44TgLRar+)0ex7DB`{-YzUnWG7(?^F3|(zeiUKN_?H+>O?wn|6dk^ zvg^Ma`oA3c-_3*2lPw|70d@hC~UuUCB_$x7{{gnGG*u7Go17 z_P6z6#R{(>&{qL+82eo433DSk?5`|I4#vxViuBA(9iV5I28~wXzaQpElhvqp@Y+ZwNHrPIlNX{%jM4ep^N=DZ5u$)@$8F zb8=<&atv1#Hiqr_r7`&^XWP2<6CwlR=uSTGj;|eITgva>|LGSL$;veyE;2KG@F1$5 zhWxkjy2WSn&x4~)_kU!!kNk6tpyOvhWIFY97r(ylqZZxEKfSW=Cx5WKtjfy3%t_44 zz_5CAWbcrc4-LS!it|+-PiG3=L7_-QZrlrFG5C^&d$(@94qk5`J*Av!O)C|-IxdGH z%dHf>b#?2lCUkV%pn0tuvE6s8Yb&n`Y-r{a1^kzB2yL&-{|es$bo98&4B2(++vtLr z3m|~pLic$rpKu6W20P*660#yuy*+)N0i13Xb2GKj^7g8v^T$|wElW9kJ0j|XzXghK z-@QHSoluJngkzzb!acJ@Vd-9tALAcWWMpQBh1c*%`b*x16XkyUDx%D8S5;Q=t4xz5 zQIqQ_?ct$eqWRdX3=;xkO?8cfCwoXX8E;(?r+)Jxz-s@{{Tvz|enw9p9@_Wvy~;>% zc<^5+rXY<-c7=>NEWBzQ- zg}At9?V&7MBy4gDDaXPQT2H)Yea@c{pT{`B*O*?MiTD$asF=Rqt>sG;2?K8`loPt*Vk!s znuLXIgDP0gbesRoOdvu@ICn=OUjC|u(BZb*7g;k>UuTS#z#SmJM&Nr)t8{MSC9JQ2Lkt5 zpNa5%bSuidI6ZtK!1D5z)qfr)Nc3vGd=1oWc#$4#<$l}L;hal!bhOZ^JI*;u*nZC0 z2Cks2Y{q;-qvhjlHS=+=dfsL4l|?dDFDctoex_gwYirlpCfr|#zBn@SGQSUJ`6*#( zYI||9iQj;nx3CQpF?20WeW}+JG@DF`LAX2B#->%v0lvRCFrdUUZhzIuoWSmkFQS|J z`SWLtgBh48=T8^5N3Hy7Q{VD(ayq7#I}D1;sD&8SN|%-lM6~LC5}Xa|w|D&;+&2{`7n?oMFZXcXdsz#a zNMWPg+@3vuX1g}YKF)Fzrr+;t>O*VZ{eGs{6 z;CitZ#+!gZhr>RyM+2#U$9m)Vk7jI=g*@Z)Go|d825H0hW;%=PoO||hel`CRBj>Vg zKPWe>w>^It?KR)LKbv87aoV$g@5|NZh#VQe58Fqt*C29$K_9$ZTCSGxRxgeZIQP8P z2`f7zmNO;ye?)&56Boy1Afz%lfo?<=CTO4!bD8DCw@2CVQJ6Va5}e( z0lMAYGw{2&GmT~D^m}UL-!CF_b*`H)+OSW=Wh)))G;}I8xr+1xlV^pX2A5roFJHe> zUw=hzPPVYncSs#?jR$9~e2^m84Xm}n0QJCRRdtRcjMY!_%}AZT*VbshWb1q>F%_}2 zvi^M@t0HF9RuI%^h;z#6xub3==<@OUZHkcR+o@fb#Xd9YH`0c@#&-jxq|nZ%=QckW z`sqR@V9r&@_n*O%8%0@u}H=D5{lUd>Y6K?n~rN3lp_mt&Cw-Vthz> zZN)~mxtYxG9NOM~yXEp4N632Ks4GvIBz9!-j}K^6g{BFFT}uSMh{y%)&WKzFijSpB zbG#-a&5AS_ggeucQtsqh-l&*^GNl?%6lfLl7>_hm&z>ADL4l<_LtV4UVHOhxnw9i| zr@Nh31$bY*y*(=&acN34dVj>wsPqpixfR`acIM*YH689tNEY@^9=t%F?XBx|^>^#f z9Ys)kG>dw=&`L;!8;k|8X*7Hixv#H(hB{wx+kS-f_2F|nyC*_TeRgraw7r|{sbl2I zmn5RoIe>+;2@U14!HaG}*I2ZUfM*UHypr&86_w{E9|t1VgTjLrI)y!kMu!dd2U0*a zKUA;9tDs}|Q<>Nyd}jvdR{>G1zNL9jVpx*;<(f7|jn%vyh`gE8>wuP)uSaKL{r&uI zsJjwUp+*-j@YhojZYs>oHJ3-s6QH%wR((mpW&6kk3o=g&Yw5;)?{V*r4dmZT-aQwUkn9e)HT+XnXVI#d)7zd3_;41Y5WpW?gG7V zt-b;_o>$ICM)nnzG#CHc;#h+hCwrQEc6iKqfKOn2PmKK<5P*HMl?YO3u$_~HqhnRR zR_-|6s*q>Dgl1x*4OCq5@#UA6W^YlG>H^h`@oca9iPOCy6n}Dp>lD+&yOaRq zCfof$)_Fio2o2VIAQ@U%nK`4HX0=x%1`gN_?OdEYriLP)K%%s)s5#vinu?!`l_XB!nhK6lm>o}|~ zNdiVA3^qZX=TIs)1uqEgcwW+$-Xs-zbhd;%&rf*qcc$-LSWJK>b7Ow zHOVuOd+c`S_-IhRM9$~#r;C$NBfuw2TDXaTYQNTur1r9!K^&gLXr|Z%nM1$~+=&=> zy(xJbs+>yT>A&l=8{UkWkGWb58L-aGWP-9e4YSpyuz{aH5@hI(Y70itD_hi6x(C|( zZVro$p%h%-x4(+_coi2F4~-A~wjMU(fs{3gm6eD7wvQf1Zre{?-`XN4_pH3;{7FYQG%{j(Hd#K(!D%>gYL#r~RwI~G z+Su?OFqRxA3(S9*El9jJ{%opH3b{~&!TXYU=xA99iB(iUR|B0ZV~f-^^y>l5I86~4 zs<6^BA1%nfq`Xlstl9lMR6qj2_~FBc-+v^_!GA-(n2&|^pO&bVe9||Qshw{&8_bKW zm<{GM`SjW7P5*Rtf{+If$N{S@L`a`oX6D9s#BOLMY#Utf^OBkdgpq0OoyG02eNT#N zn@IEO@!j|gR&RsT_8w~-n{wqxtF>07NZjr&J3Dta^2=AitCYd!0CNBxO4{RVFN`8c zt9GqAoT@iwu1tpb^!k{%TP7In$dA@E?Bj$ZiQHxr^Rew`t|fZS@AfgVqSGp0s1}Nh zTp(X~WVem#s}5ZzE9#im-qX#xw+7ZMXn5yzq+i|y3bb4c2)4riG}`o;oF~_V6;g3lxhhW%bV51s)&OBGLn*zaL?P`f zR{frP%$G(NK;Ow_aBb;CPL7PN;dR$<=csiJb#!!oJb3Vmhh15By{J!aO);%w5$`nX z+4FM!lLBd78X8h*Lj-(ZV0>`!*W~DN`ThwFymgD7{+R65cVN?d5xwteiW|)yc^04V zk=z&el?qFBpY%~a*{Z53XL9Ka)cJ(ret!O}{jYMiIizYV417GNRCj8A{QDq>?f+^{}(HR+?+b)6%jY zwt~(sX4^!d*s0tKIr3dEBqJYo8C1pS~vuaji@*$eRu;FZXOH!;P3~M?3LqVeOg+%1Ys>yQ)>j# zn7mwBlJ<1=nQgMPWKofpefRY(09R&+X^Wm+qDE9jWhH-u4GrGB-s>>U>d7*961+IC zUqpJ)-zLcrT*7}AavCW-6q1e9BlhqqI0Jas*gh@pXf!n&F?oxK^g+61Uu;^>e9j3d zu2<_KWH?RBmzvQPSOrK?CjCi7UjlOI0h!s@+PqKwz$Z5L(NzGx{z!w9x`vj+_B3~_ z*ClR)yZeWHO%h-9(Xr92cc5!;Y8-Px>J`!Ag>r5q1+o8Ff{qTXGRXK6vRX-{wfmps zZoGS}`>$G|>(?gk0&uI#A7d0PSx-dMz`;5I)rjRan~lpr>k84zBq9HZN6o!H$hfkr zMjJ9dOFqyIcu4Zde*N6(^!k!mz!+<b3VYJYQE#&4Q(ibJ?KI1ls}2>F98>;4iaKGkfx$aRY2xGbSb@6QfrqOV30! zn}X{1UmC3=wH}L@yu5B{0cAuGp$;0mgK}cW4SONYt*xz4a?wmMCqVdL$kwAIzM2PF4@CoFeTlLekiQ#CrT zLU{1S(MUQ#MnT1U2+6HqyiieTdQ%$)l)Awp885&o(hO{9Kg1=*ioesOUklqR_zn~s4;LQu4U&RdTC2-|*a1Zsb?LX4xp|pDk_HDn$_6A zFQ#s`OVRmuDU__|2z>yK^l=clBcOCzii!#(9P(HY+Zrz=`bEIYD<>zPKaq~Pu8{oc zxIGwyM7K6*Pzf9?Mn6=(fB)g5hl5#WBlZ(jb#?Kao#B{+*&Z%GqJHGT%V@Dx3+~+~ z{|34O6cfeS`@h8BNJ4L3S@u0PAALdfeqnDXbRtDCG>s<=pe{L&wf5c>vcXFWMMrLn zWDQ>*UI)4QUet%S;XFN z(b*ZKMWBR4RA$nFT&UD?6J0%hdDhSCxH+FvvkT2crNsOl^18@VDW#sfIR4!^5(QX; z_|&g;u=`a&gGUh#frA-uruy+fm~(OPG6--y02xBgV*E{plI``14Di;*Y%xW3Umu^x zC7+J_ae6~JyZ8LOcD-vPdlWs;3t!KjpOlMwk=sNFhL%wT0&{yBuu&H(t@;uW2H-np z<4q{KeNkf-y3stlmom4^8j&}ksm_>sOieuimee`1T-IqKs^Zd zOz=N=5;{650P`L|#3Yis)+I;e*C+>LOY?64aEqs7(%zwE6W>N>bQW+tIYzT4u(m+g z(g#K2VR<=FUp{@TWp%5sr<`r3khQSD4-38KxGY%W&i!;+l`qX}ct!gDT2vl10$$`5 zy*ja}Ji0=HgdZ?OOmDmS=MK8o8K3ib(FkeYF!j#s{(9SJ=eRk9zE+az+ZT$D7CBCm zT8noIE}5Gx^|xYLlE4iWPtP)Q?i^4!`*+2I(;qzrABx|BIxNOTG}simULsE04h2^ITT5cepAV|@sI_D1UCPpx`>+pt?68=>J;Ty1D9`iNy^wlFn@`|IL|Op~yE9v{nh<`dHtu;Q z3dllG@VBLfy*ZM-sO0#wF3U~GQIv5bcpD~sJc5MBCABQ>$7X&5`-G=hEB8HFT8Vl| zviOzin(Zm{5|OL8JR3}mPK;1vvI{(3XYY`Jx3=}tI;d3|_-`q{73k+8zxh_=XeZ?Q0HUEe>WSTOO}y|-rlhQMN7>g~C6JNb#=#aE zJG?e$&FzeFuti#f&`K+~%#DnRkyQF(zKTMfeZTM_ZMe?O&dDlU?PW{bIt+M z=deAqjTtxhs4)>78>F7Wo42Ll|JPer}O4Hm2LS=Vb zV40!u`Jkm!tawTC5Nz!z1ss^0#;`-ymXHAQ~oy<9T6c zD|knBAuB*9>B1T-l-u@4vES`voG~hQm1jxb(Hkme`e&y3iZ%kG*=Y~^e zNW)tInbWR^9F3j<#KV8ggWofbll;m{fSjCsw%!hlg@r|PW)iJuI&DqlN)~=h7Ik|t zGmwHlZ_CY}5Ml}qL??yK07B*|B)kbmEk$lg0(l5POjdRlIFC61j+73*)LQF>bq0o3 zajh0$073cFRx&?GYYVn}lt9(!OBAI6TtIC@Z=a;xy)DLs;)AsOQ8Zbqq{9Y`vIK;SQ)!v9!TwQ z>$6)7I5^;>oBHu$V9)jnkhBQb2MnhEBFiwykpB%S?q(P=%vQ{MVCWWS{9YpmpFfc_;WSWFPF-l1S&E1Vr5^DSLc&C64;3uB7a0@ zX+r?0d{Eh(?+Xh#jg2;M`=k|Xi-V(?zS7}FL&>~dZxuUHKj@g@5D_9rerRljZe?EB zq~+uhTF_ouY~v~~6O&MhDiL-!0JhS!G~NSw@tlyny~#`k93lJ!gu$TjP@|q5-a49x z$CKxz4~)32x7*IUpT|n5 zfM@P}f|s_UkF@~-{Py{r@t22k>%{gGgE_;KxN$?H<+(t^1>nb z$jT)@MJcxr^(A)a9WI+qX5re%r`!UYD{!iORgj$9?9Zq&T`vp0PH&%FJ+`*4HLLXL zRZenTA)r-rg0iH=^UV%{_GWSWEwf+13jnwQ-E^qT&XAsv;@vxYjtOn;$z}%}6%*Ln z;JmPQO)bLg`&&S1-r+Z-oDT+sH4Mi zWA+Y+fA6`E4WI3t3=LZ}T2uS=JQZTh$GW-)On@2=%7w3e$BrVPRu4UPGzq1WX#MS* z56@GsM?Uy9)8;!fA!|CU2ed^ri4$30lcQ1YmI34 zbG6oK#1;cg4xN!y*87yNKcg95Ql^7e3%1vkQs6p(#RbFzZhzEo?qQ3q@6Bxv)v{Ht zvprdv-`A<4&&hdAP6is0i6hbx2E1k*tZ}UDMk|b4+@HnZ;=WydT~GFL=YpGY@E5jX zdV#2YaD7C=$1h+zHx^SO{LZ&C{XX5b?w9bD3jH*3x;u-9|4ir@>GY4qcu5!ty-A>{gqM`qzs#76X1YCl!jkgV z@zNELRm0Cl?Vz0*@gKr5a7VVr?xHnJ(2bN8z)%`aLhDAnleEuGZI%?p)Z|QVt9PKrk`r8cGNUC#r`A= z)_UeN9!g<z8 zeX&=2WZAmeJ6rZNI>)AmkD8Wu!NvItBw|x^{boyTbF9&2yi}`pQyf;ub$0x`+0=cl z#Tp7{xw!)3@$m5Q4`9#UT%!$lN%8zYibCwBO`9u16=#bOrBk29NNGKP$RpO-H9nZ* z5>u|xSyvuX-#_crKEK*uSF4>(c4@h}s^9t{n|g9FGdg+ z`5`PF+I7`>Wo2@CIo-6MCh~)s4W(?-Uq`DF4qfGFWYnhqKzeSpC2+afU+fIU3OMau zCeM%wx0tKWdrjxN?FWh^w=u}~kcc*%Y0%VWGeBvsPkZB7dc&LDt6NhOUFNEUMrorQa5CD%%P3Bq-zNR4Jp8IVH(kxY zw2_Gz^tofU2N%7?P2D?8y<|ozS=ey&L$+cgBk*XC$eVG>6SXI? zFW^wvZ(Dk^spv4T){+?Ey|t5kb7HdsZS}JDP983E-b}O(gpK5`qkk^wj}w`<-!elj ztq=5rp&hOstta&J>QDkpVp((&7wM#PRB{!j%Jd~#9Ou_TP;+v6tc7WuUBGjPK3B0( zi|+3zu3Fi^kCS{-1YG9LX{vH^a^!>;{X7q6z%U4(rh6%#0~+nruX> zu*0AquG?{>G>5@{bFJHQlkq?@rSqsVzvYrL|Hhz18h3K)X~Ejstow+(+rd|yqkcAT zc5tBtmO#U2Q7B)bD6|?amv$O;d6{T&G=_k|wiSd9`QdVcE_=z~{&G%s_H&Q$$B`tx zvDv^z*lm&BJVV_i2(R^|pHjr^Jz<*bhZaN(o9Mcn;g95VqWvydNz04O&T#p4R-I}1 zLK-ZqJBrihv=Sp^L>~-DsLbZKYA~LbHR))3yP*5lQQ@6WPmpE^eJH zB3wI=(%5eYz(Jt35joF=+)ryho#W+INJ}kW?le98Dd`3s1v3n1%f?;qY!#-uylgz) zj+4X#-kB*5p>}7q_*kl-qM}mLyf(#45I$kE1{Yy)}_5Ww}T8Z9=ZTqy%07duw9_u&dGT8HZAd$SBFf}%*p#Dm%@ z=iP=PjYg5io#R=RO(( zPs=Gi{!rL&5_x}b+^?fsCKTcGa-L>_!nkUtJ8f>Ti26h_rA_CiolxE2yS00BGi=-V zWCC1`$Gb_i5`iY&OVSkvQx#3Sp_~;C@D_0WS&2~8{5^2Z7QTDG1qZ5bxx%sg2RZ_d zx8@i;b}JRp-kBAKyg!zxN2(|Y3jb?c>Ws!3x=kNt%T{`B4pWLiX75G32l$grLde4$ z&CRFHIah=Oh+swArLZQA%A;;rN?eIfQ2&29_J2P|hp5?-itE&C|Q!WUU@d_=Qq(zwp4PNsSGoaG1D6@vl~mR3KQrI2{q>na}vw zCMS&Ni!>5=J)n;PJLRV1MY<_homBA|Z2`tNcFPqDF0HbeLtWri#$##M(Q7&v>pG?> zF#yBWY+Bkm9Vg2i_lM}va}4J@-Ir*d%D_9a9mnOr@U2q5hL)gST~IRo9i z)T?>P3rk30NOKiZ)3ujjy`D!Nt+fiCXhd)V2)neOZ*rW|52O@!GWWi0qggADVRjhr z6rHCwn`?A|i3d_9l8xl%f*TkBb8pNXW+*Sy6jWhx8Fy@>Gnh^4A;heB*#I4&Le_6#(J(HJ5nlO zw^|LGkUE)tnXpfi^g69bz2W6Dk>E>kYqDQKa5ozGGCMNMBO+C6w|C+*m;K0&20yCU zruWxx-;r#K3AuPacCZnlRFD-ui( z`Y#DW8L%f4FOab5F^h_W-#CaG&VjdSB7#;<%i~hyKyo|YaNM2d4&$nTW8$n35s5@E zD+n-MP}z!kJ0befxW~DAw(Ni+g5n$3;Hg+05jHop+YM<7y)A|vMtzgzU=c?jb()JY&!3<1&$xv7|MTsK{+N^c_s-z%^^K`t3hd1X1s%0F-$fT* zn8k}0$TeeccWNsW zxpVW~uYWDy{=r2r`Z#9Ya6>-->9cd$&!<1UA$m-6PRQ?g6He!1wHp9rZAvd8e@ z>AJ%X9X*V8|NNQH=F6WyFJJw_`57~AT!(Bpy>9K=+&pJ?zV^NUe9=QHN&d8QW$w7` zwp@70rGG-|FEn=i_~%_m$g?vu1qB^7GX=0cd%4X_`B4@Cpg>Fk02GKR0DuB91>32| zR<6wVzV)qK|I?qgf9~G9@6OzrGxPInu1S*IdE0II#swE_wYAHwH{FzPfBDNr=54skfB3!*X_6G=qXcj+({>8hi=`nXp8MQ0Ql>PDcBA@ z_`KszrE)-&-$2S-|&X~>gJm>VdBJ$9y>O@ z`}E1YS+nwY|L~5iShg>)sR>a^v*rnQJ#|){A@w052b63bsR&k3T*=cib@_dGC8N zY4VgDc<7-?lD~iVd)goK@pJw)e_XLVZ=O0O-?-p{B+2<-`+7e9f%j+jj2ZdFznz;T zS+Q(c9$UGxgU$qC`{xemP~Z;e0Dn1mKnDPzos%($DMskr(E|WB+0fdSn^r#6(Zc|E z!L4s?O{3A#qxjnqQ}7b8azjf_zV)Gw9tOaR;^nh5u$Y3Egb6$M$hxWfbo4L)_$yd5 zli`Z*Md;ko1Nd8NYirAj$5y3tr%oL`6kZ|LtXGg z+tSj~f0~{>x@F45gL26=GqZNxy0o;k<;1CnW#+v3S+iz+`t8yu#~d*>Nity9ei^gR zo?9KewY4o5U2%PSbnlkly?SK-efP}bN0;Z8+waZz{YNHAjyz;^9(ZVRhn)Kh$;=c0 zyh1e6$Q}1A$ko&5rcbY)>D9AGrcNB6p@UwXe|zs=n04znq_wp*r=2`GUAuJ7>NRV# zZrz3)Flty9KK$tO9{ZmAAIi|d19H-_hiBP}Kcz?aZrQE>tJ*{A<;=_!0K7u9wzXx{ z$e}s%;L)2tZf(u~z4Y2lJ?fzJ>9u2a=-MUgH*Clavv11*qlRVU#+LT4op|V&=RbC9 ze_I-jr~lrX*47tcQPxX@nJEBxg=%eUOG`^j?zm?`o_KO~l4RB6tMk;FwORGVQyH*p zzpP!iKKCtHlqE}7wtwB-_dS@q?tL(`Z@n{rd~8*cWcbj5`Q`lka@n+Bq@{J!Wy7tW ztF7<7u1G)T8JEK=$2W`T5f=Q{U# z_x{d4=hrv(K4TnzFxGhDj(N|x<~8RdD))AI#_iwtL?P8>RQo=OSk{BDW;=nus^J9M zskzIu@w8Qw10xqVrCi;9c+)ZQw5B^V2{a3Lv^&0s;!b+ZogQC`)kVG@wuv6&mwQ*y)32_k%*RCF@tI%r2S7zuEsT{uY>Q8;f;ELZK|4 za+#I&#u=JkJNwAeBa`!Sf49m@d#qTmzr?Uzz1-|kC>hV+_I7_-iLkw$+T9q9q!$~E z#Kgo5;j=BG4y^osCV2UxO)>wdAIojWtCW;;KwHP|gcLkygts1LYU6U(6rP`{v>=Gz@?Bb9%^8Vydj4u`0Y|#$X(wCG&1T--J`kl*(Sr(8lQ*m-#O@g9=^So=5Kn!sptKM=6-HmhA~;RlZub4g;_<=-?!EpYKsjA z4<2-Ovj;!?B8`FZ3`6qWn~!vi#i<4P&s&r;6%KcACZWXSNsi;h?QOXjKR=Y!Cn1`5 zk`I1!TtY7LzpPDlZiR|}i-_Rv!P$Qi_f)hjVqp?bS@lVBSP84oLxSwDndTa5ZvwKF z?sj8OtTE9;@%O>f-mbm*3w8@uNku2~zkMM&?~PEj5*?J4p>IW}+;VTfKvJ^z8M9DT zK|YO-D+by2Fwb$1@{_DCEzR!<33_9rK(5cEap1UkN4C$ZIQVEAQ@>w*{VSowPSdhz zY-17>F+V)Kr=J?7WUTX_#RQ;UuEYL^?6B`E|G$O#zb-)kQ@sD{%KtAHIM_BH2uqdR zH-~aci_qswbE9f(DJ|0_Ldm(b&L8}I@V@zkrj~)x3p^Wev!zkRz=GikOD~46R7uCjnvBe|rJ??;rV}PwdI~=XR`V zA&MiFhdX<^n1(OVlPol@#^EdH^KR4fh!A}i@fWFiM5Z#Pf?ik)6k@NTmde6YpoKY? z|EAn>^iHTaCKeW5X2#;ma&SZh&9WXH_u8<4}FoNpj@jo`5p+77OXyU%{S-gRS` zt@zouhN%xZ<SNi=V1ohe|Ke=oxcFf7^gNim z%wM=&atNgC`&I={jx81@a;Qky?mo1Kh2=T(I(p7VM2$E;;33mSR5dqYfWJ_>b7>nz3 zF`|Yx#++=%LujrOf9IKX?tG)-=PnRS+1Z|O8u(To!os^N=x3c7q^|P2rDx<0ts(79 zo#*;wCY}_NjsWHKhkUg0K>OwHE6pFJ+x^OIhaY3ybu^#*8XSvx4>ynwCge|Rvl>`U zDq-S`uU6!5H#QyK(_@9ft;wo(E!NS2xc0JJlPxWU2L?5~ugHt$p_dBfdQn=a79X^1qBZ7f z`mRr_Vxx)iOZA^IPAXJ^rVYj$in&HAw=l&p@oSTtE0x~gS3;jHY$47&VONohm-lat z)^ur+A6kc#In3kGC+{=x&4&p+-aQfFvhec-!eNC6xOjM7h?oCS3tbys8mGMJC?}q- z76>-Y6`hn2E}rl8nbjA(vF}m}KkPz0FkXqXj(cfp6%ravPi(?qtn&nY2BPe&uB9zFblXZbwjl30hlAa5OvhEHM^AxX?apLM30ew6+qGkv(|*;ln+9`w{}jf_e{BYH27>O-&8L zSo)Q=%y=KA?KnMypLDDzYCi5iR%0K@aZF%Vp(zZ>CoOD@=D*0ialR}s=a%zuCnO}) z-nh`LusCd|_P|h(vcI_!zzq$-$kD1|yT-=Fc*epKb#&xNxUhJifPh(5+-?sKy!9h4 z?)+Y#0(x))slOwldnbSIZcSB0sxU+PTWgAD-0>bZweVGZcjw)<9b~O*nmqN@)s>VL zG(0^SSE$8{Y1h^KErwNV{q0mM4$0Zou>7qvr(+90eA1TM-5csiM%krl+Zar{vjLhI z=$fafh;e<*Z_Zr&~y9NMLBF{`G}ZkGB&()s9VO zjDYLj!TNyudfU*~D&~>x=!jjouj<84t7&1WX&BXkMS3Ku2!cSMBS zY~ZtDyZlDO?qEH+6w~v+&G+%~*@(-_HLY`vGqSP_^!2}l7P4ZnYF5%g!Yp+o*6s2? zn=0oZMtb_Vi{s6?GToS%7>thv>JbIMB;4oc^;ujuS)-JUj7=4@<&q%RF|>}msuN~W zDH-~=jTGxOst??*)|O(lx3sh@EiL;XC-oQXPP(Ci+k7fx#NqOk`gLSm=vkx7oY$IQ zef_6f#f|$6$izhTeP}p*$QJFcUB71ZkBC_eq|$6pRje+C$NaXP)18HEA7MNxEZ92` z3{}l(V>kULjd^C)>0c-4K`i}BQ`4emxhs0H%((k0ujd`9Toe^&xsP9t$5|U*Da}qjK(z(PD{0%?c(KmX^&U zb3vya896z8@A0C+T)62AcO3SYTtN)7@t@Q(rAiFx=^1`R#Jxcxjvc!n;NaA_AHOWr zs$vijh&zMMwD|eCX;@eu!(s5`eAUpaE6*aGTJZ#4>yFOO`M>9fKjY#C2WFEC3S=xs z^Lk@?6FU0(Ou9zQ-F4K^M!9?E{md`Lq&$Lh>(}ZO$Cj#DUwzjNi^n6M>E}gp9CLq= zl{LN?VJf(^{>FxZ_KM-%WfWOUoO8-uQ&aI3&?Xf542=?mO$f7+4P0igh|oT2k@kPZ z(3|(p+f$t}zaj=uOnho;3kK9yy00@I^V!_7zrHy32#Y?>ajmXBY(AY688bv1pz)i( zBYKaCiH9=mX$=ewF*-Urn$jXf1Ox=auP6A@e+ArQ_fqJ`Uf$dk>wz$Rn1G1Mc&wT> zi+kt&cN^0UFDsXpm)D)P8r`oe1Q5lxt#i-$tB-2#13L>qaotyj^sTIVuI}QTZFIBW zI$K_SB9p?235UZmex=(w05jIIw?}yOB@3iq3O;|XP13@qgYZ#$prIZwslI;NX+bxXDILoX?NFSbpCgQ8+4aQpM;(qgMwOc7QMWLG%PA^iquSZgu5&GE zaW)hWHJXgob6DF`^w!*m`G-@rkBmh9{reVp5^yLhe|>Qjxc^>OSdQ~A&%b%|<`#xp zkv0u8GtTFZj(awVVlbgGxqaz?BB*a6mkro`q z6-np4walOV=3Lg( zf6;HYZB)#ZdXm-uv-9iu;qQDai6n%Ro6DdF;y@I2q-@hV_e;0_Lc7K@aJG+)<;G63 zMDXfp5h9qWb0fmR!O4|Y?Dmewr8tF%!yP^sf|t{i*BU3LDVz>V zFAxF(obF5aun zH`_ZpWhhfT%`b;QPPAmmd)$EVb6x*=(4H2JEit}mFEHIE*@ z15Q9vN~)!~nGyzr;o{=*+h6f>4h}vgBQr3t=$dlcq`s_w3^bLB@4j*-2QRP5*w$nn ze=0StL}=U0Okx6G`b2{@x!QG+;ipec@}f6Q?bNLE-Ng+JrUFt-;$c+1|Ne+c6(k5i zmb{qQ_|F*`{V$v>4{xx8g9}rxH?}2=I5`Jh-IEskRBD`1NBeeKD%M#Y6%l+s9n;&$3@Fd8q|(uJSz=pQgESKBXQm6U@1Ggi&=rluy0&%D;jjNIHy4aOkWQczGx>6q$!dt5OBV<{*okd&2Vf+}yWYT(uU zrwiM6aX!dO(!e%P4Byfk8us+`VB{#I-HszA&5Y5jb*3a{(e4P)7mGVPa78W(d|u5{ zq0L%T%&4gmkds5!IGl6or|G}Q%+Wj+c&U=-cv**uX+Bw+X)`xgIQ@{ASn6~~WKVG0 z%_HN4*V=4qBw}FTVI8|}D&*ylpQ#%xB*ydf>C;bUFQ`R;1Chr*4|OJBU|{$em!J&d z;*0oyX9vsO2!TUS&pPv|kw+LXWf*eW4sEnePNFWAk}@BjG4f$={@QbBKQdRIthR@d z^IB(F*PcAxJ2k!;J&R@h2VZ}{V>!A|Yuh-;YCBdysu34AQ~d~X6q0e;l{+Tmkz!4^ zUu-+OxDg0E%#;R^YbXlLX{ga4e0jQ^sa8CDw1G?zsdYKjuCRDYK4-HxV#ZCNV~X~{ zjp9^?BB}8_mYfDl9{^-{H=Grg$nPD3iA{hx(AybkuAKczU;mHq;p$IbueQ0E)BSd7 zet26j=H)50Ke^h?!Ofkalqp3a;(q7zgD1>(TN8?!jf{W-k?DZ{sk7zkKIseR|1dIFPlz%1ev&*N>;duSzJC#*ZQbN0d zRW=Zizx3F@jE;pKMIR#PicM zD>k$nJV`w~E(w(pGRgd3^l8NvO5VOt&Of05bOP2hKEBw))xJR>l8lyjMS6{4p->^~ z*qEyNPpwaemffW6#Rfz#v^3R~GB;A-GXVmi z&go>L|GaCDWR&}~RlQ$*w0_tUj)o7y`27w)f4ovQ{xtBNhTCicunj7%MA~U7W4JNq+oy@WmsCRcdJ$9F4Q`GFPFVgSpld$V%T)KhrGEVylq5D6uNrKJ%7$H~79 ztp`>_^H`1*8TL*&QSiGy1%m5yb+ZS#fba}bQcmi2Q-6OrOjmblY3cP~Mx#GIWlJ+M z)N;HiA~LcW1W!j}y&4DKtu3n;WNl5&$ZK21o9%%T+V-%NCHL3Q;H!Oxt@5I86xW27 z=l_7nSbdq<^ZolLG-BV69ULTr78dvIT(VPNJY}F~Abk8--Rc@i#I9HG9(`3OY;y-W z5SmIwMAXYfK(Jt7=roOt^E_W@4PIJV*@(G3KTjJpt9Dp^U~X=Xk+^^GcYK@#@Hk3A zV7Z8G9x^<%XnsXMS!`=8(4rzC?*^m%h>Uca^Y{0^^*u83TX=XlWHXJk+CO)y`5$N$ z4@AygZX3|pC@K*?L-l&NZHyR$k53I?+PytHMoy*(5fA4wi)HpietU}SqNKo}AiC$m zUw;p9czSu&IB&mHRE(%QZ?i;{lJukikiie4h)nz*7ZufxXU!Iz3kI94Gp7ZrRBej{ z4%jih5Nh1Nu-rmkVv81^1>Rq5zh!qd!P#(eAg!$3XKmyujr>1_Ur)D#`sxAR)l+@s*{eda|w&}`I zZU{Ln9=NV`aZzmbs~e9HGR{Oy2bO@_5sbXVphv?J6zG`hmxJQr{Qb=-n=?Lf+0)VLnzw; zvQs~ry=-ZDSLW1Be07D%!NKuqnO*d7@CU)U`#B%jJ(pLTjQh!kO2Lj4lP($m_rOSo z$cNPZwwvSd^)3~FVAO%5O*}a=>Y1?LnynAkDAP0UPewu^WIPCUQfjE&XA)ciHy=NK zJWv%50|qoAYrp7zK!>U8d4D&Uq_e+2J0WtI_8b?DvC&Dch~w5sU|5piqo+@s2L61r zceD~8eW?;;P4xJ2ht}bWfr*8!dDSboBZ;{F__>{s;AX=ME3?%E+Ixj#-YLZn^Od0(&KY(9}>yC-<`rZY|9E~yo zn*C*yB#3as+V!U6cbanKwVA=d!Ybb#_~HQ4FwMDt9%CoJXli zZ!yM5t`0UWEv;siO=wCAb;P4bbpkF_jB*s%sa0i%m~)uuAguTuuoHkn0KkQVhbPnC z5F0?%`Bj%ti&5ed2_{n0e=I0bKXB&ZX##9C!@9j-3Hl#S;dtdK!+dT%|= zl1m7aPI*wynVA_#y={hX zE>%9Ip}Mwqf3RJeNv(*2Ry@S$q#&+f?srLxHx4n2(bmMEy1J$V>}61Vg$1YCz^6nV z?X`g-R?%W@$hV|~Q`}G7$kHFi2TR31os9 z#@#U(XJ=>M<08@$a?biPx`9zOr%4hG4G-riXR|%7Uyq556w4ng3=D6~OmI9qD=|d7 z{1iw-V?prBsdQC{NJMn5k`)|!RdH<tt19oQKIcW|h7oxlB9#Jm3R#X)&CW8rl9pTB><0^klf z45vtO96;KX{x5)pVgS0~c{Y#jS%0Oa^BPSfP>lhq!N|_(~wMG0lbZ|fqW=rm}&Azko=@$ zgAM?G=XRly^n}p&XoTvT_xJJZMXU-=9v&w2JMW|{1Uqv{!!ru~N|Hywmk<0k(70r4Y!za}THm!^zV}k#Kly=Q( z9*J1YPVTS|t94gYP&jLvw&W%Yt8stzUr(TmO;MQ>TK8KU2=0f{z2Fa#qtrCCR*BHs z4+Ma!Axt$tQ!CaPxB&RF=Cjc4O~q_yJd7Vdlso(S(o}mbv>V7?cdy`|Vf%fLPWlEU zCridR5S2aajDc6zB?NgHu2^g%Tdv8vT3`G^0t`GZ4+j-UH$WnqG$rD14p?)@wV zhR4K@2_I9)xk=;v`n71bG4L=yXt~5zJ@(AUNCEuy>b-1UD% zMH>3$3%6|GWm%zioG&$t$vk#&95MnC`@F%jY8mJ%`S z$x?61b^G81;=ahw?%Z(OyZW+Yzm54Sv+)UVX}-q%;RK)i2;wgZJ~h@&U z{&Wy9?|Cr$%b=Zqce%AfIu@gPV>9@)299B+S>_k(58;|@UL0dsT^*}|B^#mynACjEnFy zUcMU4`JZ&FrmweF&5sgHJ%eKcQ4G8Fh4*39f-U)x3|ua4j?dqU1wZj%=8bXDg+kY% z#WsiSA$l8e@lxAWIi#E3@o_6^iYJU(xMOC0*wtz1-R-NZpy-Imq(aNVnnw?tfDN=U z(@NO2w)@xB2@feL2j0V5-fO7eny3pDgRbny%}D`j_;YzUXK=HnF%wjeTu8Rtj49Gp z^Yl#3c{144^m!(cqCvgXr%ErOxfCKGDKnj}RO+r-So{|2kDU))eSuL?wanCYk}GHL zjTOYblX$bLddzlKk1GIEhecidaN|6iqvr|J}5kF$EO$= zJuI!_!w}-LH9-~W^d$4&`V2e^m-pVYS8lcuJBKCuqc4Fux1<45wxa{O2XLn2fdky> z)cNY)8_yD>`-6%g!9@6Cf=t`8o4B;B)YWoK#t*^2sd)|RfCFaSo(*T14EW?-fzAHX zo^4lbdMnWpv}beV#YEI3YFkt@H9I@);qa>gnye7A&eQG~MRoNL-rlcjE3M`erePzS3oPS8d75W`ur21XsdJRoit^qSD(QHfe4NIoROJ+?$y1rG+~{$FmRA<@{ldw!2`i*z zp=+mOs9jPmI!C2Ti0-3KSis0O$n)TFL)dbmS<$RLqaERj)=*Q)wviBsr!CY)IgAGrQ=W zI1_4u-{}>{2k@G^`2+|0r<-=eFTW6~Lhw*%9JeH;kc;kh`3>LkEF+YH7SyhH-N(Yo zN*$N06h5nu(hXNq)8P?hs&u{|H`T(ic!Y(v!5uH;Z;%Sk*mh3h?--eCi&Wqn~-9MspRcvE7J zKE&gp-}*oTBbGQMd4<^c#I-I$;Z({X(B!~xec9+vQO%jOtWu+xyGKzcQ$Q)TOaV@p5Kn6bBPK zk33M)qPyW5u?cfs`mpSF94}};{|hYvoxqTx>jKZ}Eu7;;#4&oz4-P>)rnVzK_q%%A z)y{qVa!m>~a|4cWowjD&Mt|EIMyC$u<-%8TL>T4rEcw!-;iwz*8l5Qw@)-JlrU`{t zU)^4wfJ!rg5*=Sm+vX1vG3GUC?OSxL4O&d6HNSi=A~u?|Z zPj_o{mS3vyZ?1Rtv1$bb2hN)q(M9KfWGX@ai=cH&lQx}oU8Tg|zW#=pePbo7EcM8N zHo#UC6#^6$BT?AlDdca`8=y@r?qKko=FOW#eutmL)>99u$lfX$JfEkbd68*nwh<5! z@n|tiM!z>G#c>(FISc1_eSc$DXIJu2YW= zW#y%58HExW*z50nPKqPVWnwz555qtvC7BQ8JObS*+Wa1VP{4q~4?;rqb7WQPhLe9_ z;C)a)0L8LWF4&+&MP|&QnjxdC35cXU?r=7CTncpy9XX0uEg9>u-z62sQcyL zkPyf58)EtSwNe*M2=RY1GI}=`tGiHtZo3{69?x;h(9lpjoRXfNDX=)!TLzRL*4NGN zXrC@dYd~eWMuuX|ZK2YR#r*F7Fd|KQGTeLPGsgzIrpVY`Tu!!UB;Tn&1wfd)qV$() zlAwAFyZol}c8+Sgc*&-7Tl2TCSDp=5X7?W>FNa=aKIO9Du5@-Tw?g_YwYbIIrLLZX#)pw>hr0B)twD~v+dVC$ zFshueEEAInmBOyS1jHnimi8sbCMG5b2@975K~sD~M){Rcb7fL4hsW301XxWS6Y6T& ziCd5rXpG{7@tL;Tl2-$G10d~$+tJgu<*xP0?yj!epB6VpeeTTt`_})8k&*E-`I=Bx zMs|H?tH*CN-+ZQ=3GgDrg)k0|rs2`())e-|<|>j>&*hgdH8nWxryMt?r#CBwN+D{r zSmo;75yqR|HyWo-+A_G&R;Vl5{2!qHG-^4qV|E!Nf?tW?^OUP0>GtueTv$jDN-4q`PsbftSy`p;bktT&0(?r+mpngg+lcvCDwoiB zCMdMEYfAIfi(X4>s@zs@LGvbBHGkAg91j;)sy@uVq-5e_kqlt46kNR797=bKw!Fe= zs3`9%pMoZeci-Hhif#ni+o+dJoDs&q=XBs>=X}(x2Gd zS+xby`ZMXJ-3C>vrPbB_?y5P5gN{d84eqrBU7$u;S)|v0oUis2cnge`l@%W>Y}luc zhi!F7&v~PnLC?;IQHxQt>5@f}O)~?7x9vqb=WD1N38?s$*L?y!;~D1SPc}Ak>L2c5 z`r*z4h?Z+>+h0_umHMKE)os;!%4O#(C=V1-cW++^l`cWMpLB8wJt}Pt_W~lcPfk_< z050F22n4nO6xn>JfRwuN5*o?Xk|*H+JQi?zCe3*&S0&cuNDm=H6O*>yfP27Zf!;xk z!gM}k+f$t87q}PBJ1za>yfOUQfy1U8CPQ@iTa&$R?CcD^KRd4`%Ul) z+p;I{L<$^|JaoMlB+8b@{B;bAau(&54FEzh*2aymz1W?nf%YR;E6MS!(-UCB- z+)Uu-1&Onwm6dfcD=I%oRrO=O;Yhyu#2-}*L0HVj>;AXid7#$>H;Idu8Ehs3QY%r%CUV-YI`%7>2B?`8lgpvzD zZNc`%GE61jOK1Bsk zvbpIQ9rS4Mb6d9U%vC8B>x((S;hp{6hTRA!ke0-muxhLY$^34*KAn98I60MPIH zLm6hKQ01q)oG9065@P$fL4;g%@0IS(4x&*T>W=$4&;U1$QreoU2nP8#ze?C)dR`zS8W^N`+-Es6|;%Rkc?-7y~8h?CNRhMkZaHoRpi- zQ3VOYvgsIy!H5qYJeWAV+(rBA)g5F>(ML(OY-tkqp*(9XAX5-3)ASdoS0Oma{`%bA zhbm0vDJUrSYdzz>e!Z<(VJ&UoDXcYKY})jl22?wTGBjXcuddN~3#(=o+y2vz<26Na zfs{x8w!q{hS|c!PT^3!IxtiNTGT)?&8<`rHST|#B@S42_T9&I(%eyQiGf}Az`j4?3 zwhpgo3~_&9%!GCB@oUK1pMtb|B!rT3URHL_B~d?C;#dSd~jz zxrkA?k|5FM2qln9w5CAIgVeHt!d3IH_R8pN>4_^cl^J)x!swT7JO)LT`c z;yaF9y>cr$JLB-2;RhX30rzm4{|r7VRPFAc^As~>WYnyvM~*(@q4ynGGD2_GS(O;% zkH8d~bSP1~!VR_X6zJl@l7)rkw~JjK-yWN)?nmqC9y1=3(+L2cJhn^|0NLW~YQJJ* znh1d10|leo3PAV|fDLWV3itGO>R)Ebe)QTAK$<=SqO~UHK#4$T1Mv$bA1>M4=G#_9?P2z6R{Sy?FWQ=^i<_CzPP{yBuGSXlie;AvK@0Sg zM!`4-MV!OKf}VHGr6hY@q>fq$RGSij76`|UFjpgg8Bu=&$uSnUU*~_J2M(-^~ zCK&Cvd9O>miF_l`a#Vaat4yciD;l5bVDa=cPu~aO+#hb?KIMA1%jI5O{ax#*>!$Hf z_MCV7`Ed66etxkTvT~cY(S#sRXSEObAytNU?;ky^EN2VtDO8`N_cQ+U)k!UymlKzb z7d~)E$=B7{<&yHagnqM3susyKzWEm9w84hMTUj|VGn$4n2ibjw$DNlqI?3yAgwPGX zG)MIMQUR#cf*zTh-Cn-rGyCF;3x}y*yg3{a&?`TXUofs4S1^9#;j;BYK=2%9a?Rej zCjMpo)O{9gVSY1Hdbj2kA7l2&IfZoD<>}o0dV8^5hZ`stkVf$No|NlQG>GIo{Ns22 zq8HczvSW)Z)GxoGA%l)lZ1BatOSl8n+Agu1630mM^7&VwQlNkQ1`fVF*{A-fppcp{ zwJ7PndOU6?;vAKv#XDRqy=u_-lWJ^E_T`m}fz2%44!a>nv`Hsw)-A`Oj0?6)PIV*u z_bnv$K@uS%pVKgY_~s5aR;e)(VSw&YbsDa^O1hMdbSid`tF4I^r0*7eL`C+C{EF~( z{@{#@Za0RgsI%3SWB;*jZ@i?99Dd;b{?YbGL}6p$D#$N>n@;m~#r@;}tPgU!Ja|r5 z-RdeI%CFrWoTU`n@$hO%L4PTPdV>XFVZF#aTb{NZG~>p7D*RLMcrzVnwA&d+Y+b#* z)!#8J*2=Tpd%?{H7A#CgZy1tqQ4#Vr1hkJVX=AM%L9}s3;`C`*EiQ#sFq@y z^<0GjcKbr)Pf%vNiVs|R%cOEP|MG^^W~KEyvX~k;K@dFrH`R)vp~=obKu=&kd_>%K zlO$A9QP$j1eT4D`yGG9o(w(G1BHM6y1>mC4{vXigSm6|bg-J;^9sr+%Xp8pE++=tO z<3-B=8r+(wb7lop)1RK6kT~1E%x9u06{08^V<91|Pzp(JT`CZFa7jhT&}suB3$zr` zOfgYU4m{e`>Fh8iBNNRR+RIZojO%M`wp_YJg$0_Y1kP9c;lr38kd?6AD~|}M1XpOo`4dHs4Iul#%S}*423cb_QD;; z$$jR`9Gz+G#?YIn@hcIq!m6HS6rkNhDKe6j*STOv`*dgG&G$x8KyhUNZwUxnc;9g3 z!IPJEv>e|bEw-(ThtAa+Vt*OG2F>UQ)6`*$3M=)BARx4hy0hnCuEptoQCcEHLDnB*Hv@qcnlzDh2(PpVnaCLp(r)) zef2ano|_)Tm&{#OgRP}bS43@z)3v)pX_=&?oXJ6fhG*3loGn>I+VSJ#V!8N^)Ck&= z-{tS%8 zPzbkkYyo9+Hl)WV0L_;oXwAlRiNFf<`<5g>7OIABC-6P`x7rSNf==A=$a&g|EL0GW z@4gPAn#bLm#Pq<%c5fUA2TB5HbOVqW7&6Ejw4K@0fsV2#RDD+(FiuXM*3BqvJ+CXp z5=7Cpo3G8MZr06l(82~a2QBLmj8ehg-{@(k@zj6J^)^f$_0W>BrMx(oaKs`X6)DBLHxoKh9MKia;aG9pwwx%>=Dy zmoB>o>eM74jWPy17WnPKhXGF02jHe-GVTm#BmzS0x9{9LI;7E4{tFs2k@n2j!|YoTIzL zTe-}ZKCJX7zZZ8G1ZzvRxxPS@RPSVJ73;dD2(_c@FWMtY_c$zfw8yRwU{XvJT9qu7 z{gt8Q6QJb6V>xb;6AS~;uyw32?2CwPzs2vHSiR~cuZa1@wi3s|TA#bvM8t$|0ZH{# z&;-{IyWt(6Vvq!?x9@Cln_8!c15-H`-WdPf3tWRmA^1A1P~AZi=1;SRLVK7*9xWM}5j&p;?_09oz^?^4To4XBxpo}MLD zE0$F`kA?M>yvx>$0P!Rf=^8!OqKwuC#8OKznN54dM{cY#O^Af*0g}=|();4)}U_Niwrxsmnr^W*0iRLYl2B20&}_^f7r0^>7oBe%7KEja(IcU{4? zFX7ypJP`t*wpH9LJSd>4%_e)7-?DEEnHg;PwZKlo=az zP6Pm?201b2KTwzT(sK`-JX~21a{(vM&>#OkRI|eIn@!BhQp9vPVO5cye{L=lC_L-6 z&8FS2*ciE<<}fz4?v3%f%$-Z6K{KACr*oTQ(J)RLG;X_YlZkH$7V}rC^7hK=i);R_ zm=65;d(dFD(4j9QBRg5Xq9)VZv-ip+tW7q-z`~@BFU2cJH6P9TIVTCikD9#S+RmI! z^Ifm>=)DFhp}mOvV=YKXi1!9T2J#Jo;vZ%7>S}rKjx{UHrheVU1C4>UHE*d7@9(EL zoh}Cb{dybp%LK93UT&5pXa5HIq@01A9IZyS2(#;03G+%pbe6S&`&439CPU%qyZlhqP{qefgwN@8pXsW9efG?v zd_D`eDoy|jh_s90jJ0)C&?|2IS#ff+y?Q#GP%*oCF5)4&zP9#3O-=3e7}_}|GDMks zoL{2^1qHdTk2Kc7CvJU-Fuq6z`Mx2cowK-pw|yA0O0ThQmzKJ-maUG%R><1q)m5PU z(r!`pa}uwAqRUhHHv$AjItj`Q^4^`B&Mi);z-z{AnS^BhcIp(^YEQ2u5#tIq>csZ2 zrLyq+$ds+Wl2vPHa4_N!Vyk{eG<&sjHEPP95bv-we=;r{?qa zJVp5~N{o3sK7}do#aG~d-K#MB*2`n&OAN;WIIfz+(_=|*72iGgVg`Q|~SXfxyZZmbKP8B4iY}zMtIa?2z?+i0S`*Hml)>Gwm zhhIfIl9Av$P3x*Wg)oh~EDKhqu9r1!WhNUw>UW5k|3ttBC z2|HS44DA)=OS#+AWe$@_$sca3$q7*H3Askik+0SlS$|U*2Kkg^(lV3f>X?|TSky`T z`HV4&nK_oN7cynn`5EqY@FT_jMpjnV&EoQeuJe4?+vVos5K0!X6*?gP$64=Ts^{UM z%BB;43ln=~Niwn&-X0brdcJgIVb$~H`top@-J6DwT`ReD4#6{BY1LUG=4W(SX{}M? zXnty+)t7kYF=dN^@u&#>9}~ps2stlp>RoCQLz`L}kmqKh#&uFODbIzEaJ92Fm3HmP zs(XK1va7+3@!&7d%{KDGHQI1G|8Yf1e!FKWXS>i3ukCGf%2&eSSO2gzyvKEFHmGmX zE^G2*Si5tG$Vj0Ae$t}jnb$NQ4jku!-z@`6m#HYn%QF@Ax%^`ot_ zzOG-5(0RhlMX#;NXwY)w9zh^J=u!1M-h}UNjEWwX>|Xa%_aR-_yyszKv&pZ#peyNd za%wMbLpfni)4sTbs{RyIX;rkArq>besvbkD!Cry8Xej=?ZlhFfXcA+rzQJ@ab z`%PPa&?DzOJr#@F+JsRva5tO&WbL|t%23Td!V(5Q*(%>fCj4oqN{AU76Gq%b&=Q}{ z+4b_A53Rn6owh+Rg#ua^Xo{NmdH4B9`>XgT(lJXL%*8!%@D-!>`RR()#!F<)RdK_2 z-&oHBQ{A$Zqe0OdqXsuuVUHB(73hTR%UkZ|vN*c0QSV@U%6AvF6U6M$!`Tik8OE}F z0E$S;Y4LGKr(O`PTjPx<6|ur6zPk{#b8%7y?}}ntpDv!0*JgA}ZL#ety!(d5 z3%?kq>E2FxejUu#HIT*{H!&8YJjGxNHY>0=zPbdw09S4yYqLa1$FTgXxXN7R;YQSF z3L&G8dD$HDGSkzYMA&Y}WGj57xBGhTQn&KejVt(s?q?b+t2y|987(((@a7QsZ7=3; zax_sQDJG`rhqsxm81Fn~eqJ${KHT79TWK|QwCJP^#ARjGwX{AQ?pkcpmzGt3D$qDi3ZdW;G)1)uaoPO!k+HJu#v9j;G)-}1i1l>Wzc}4ZsY-^+p+nT}`GEgW zBZO++gnG#k;}1`Oo&bjyi8h3>-UN7Cq5x74*9;V`=DgWGZL<60N3RR#7bU`E)PC5r zmt-%oFES^fXdC%FKgq4ZkD%uLRejr6FMjjs^-tGZs_oQmSI&4gA8QSj+?;0Xk- zvK}BDSt08LpzCn8lOu(&2l+AV{lJK*DrlnVsO!!a94Q2&aJ=EVHwOp(OXO8EQ^WsP z03QS3{6m5gGgDAtW(wdJXJ!fjfNqn~iXc^Z>D>*1SKYzRu6*Z5&-e5&0A6tiIy=+Q z(9@&%)e%#06d2UhE2j)-&KEa4)6+u%c*RZX-<&~Bub-P^VhWA|rw(Y&;#p&QdKduw z5?()92msInv(bzr0Y%W%(*yWb>gwvsvpaUBsj;!Ahr$~~`@a1>Jp=%H*y}2$`uFRb zeftjd^e}jX>C@c1r-uMQ4{GS@>gww0K>z@}R#`K%p^H=olQ4}Tf8HS4_wCR6ja#$r z$!F5l^~%5Aqnozn>o?x@%D+$#6H@^22Jys`&t~1jkL8EY?#Mr1fBP>TRNJ3^HedMr z>+;y+PrmHqmVI|^E)qDQf&o*n=I@apJFSBAA5o9S&QW&Or& zd2a9C+_~c3baZs4e_!9`{N~)#bM3z`&A$En)6vnDcg;U5OIO{W_VxoAI%H6O09nlo~EOLs`Uo|%~f zfH#QFuCBCA9G5w>r@ZiYXJ@{8-EEnF_URckpnrNF^QIg)crbS^zc;U{<|}sov*^8tfPdPDFAqb>g?)DM@L5SCZtUale)|_pi^t-g0+3 zI$yYLc*JXUcIBqqzmsiGK9lA5tb3Ku^p(}n)%9wuMCS(pj&{%fXjfL>|44r8oLRa4 zmSs6@>YFoqWNS|k>!>HD0DvUPp^lC$UjF@b9O}%dk*%3Laa>Oi=`}%20RR+OGm~+O bvlje6dwF=?e?Zy?00000NkvXXu0mjfa1^V) diff --git a/packages/generator/__tests__/generate.test.ts b/packages/generator/__tests__/generate.test.ts index 9bd652d9..dea17ff4 100644 --- a/packages/generator/__tests__/generate.test.ts +++ b/packages/generator/__tests__/generate.test.ts @@ -1,5 +1,11 @@ import generate from '../src/generate.js'; -import { Template, BLANK_PDF, Schema, type Plugin } from '@pdfme/common'; +import { + Template, + BLANK_PDF, + Schema, + setDynamicContainerMetadata, + type Plugin, +} from '@pdfme/common'; import { PDFDocument } from '@pdfme/pdf-lib'; import { getFont, getImageSnapshotOptions, pdfToImages } from './utils.js'; @@ -163,6 +169,167 @@ describe('generate integrate test', () => { expect(after?.position.y).toBeGreaterThan(20); }); + test('resizes JSX dynamic container decorations around expanded children', async () => { + const renderedSchemas: Schema[] = []; + const createProbePlugin = (defaultSchema: Schema): Plugin => ({ + pdf: ({ schema }) => { + renderedSchemas.push({ + ...schema, + position: { ...schema.position }, + }); + }, + ui: () => {}, + propPanel: { + schema: {}, + defaultSchema, + }, + }); + const boxSchema: Schema = { + name: 'box', + type: 'rectangle', + content: '', + position: { x: 10, y: 10 }, + width: 50, + height: 24, + color: '#f8fafc', + }; + setDynamicContainerMetadata(boxSchema, { + childNames: ['label', 'body'], + paddingBottom: 4, + }); + + await generate({ + template: { + basePdf: { width: 100, height: 100, padding: [10, 10, 10, 10] }, + schemas: [ + [ + boxSchema, + { + ...textObject(14, 14, 'label'), + width: 42, + height: 5, + readOnly: true, + content: 'Message', + }, + { + ...textObject(14, 21, 'body'), + width: 32, + height: 5, + overflow: 'expand', + fontSize: 10, + lineHeight: 1, + characterSpacing: 0, + }, + { + ...textObject(10, 38, 'after'), + width: 50, + height: 5, + }, + ], + ], + }, + inputs: [{ body: 'long text '.repeat(10), after: 'after' }], + options: { font: getFont() }, + plugins: { + rectangle: createProbePlugin(boxSchema), + text: createProbePlugin(textObject(0, 0)), + }, + }); + + const box = renderedSchemas.find((schema) => schema.name === 'box'); + const body = renderedSchemas.find((schema) => schema.name === 'body'); + const after = renderedSchemas.find((schema) => schema.name === 'after'); + + expect(body?.height).toBeGreaterThan(5); + expect(box?.height).toBeCloseTo(21 - 10 + (body?.height ?? 0) + 4); + expect(after?.position.y).toBeCloseTo(38 + (body?.height ?? 0) - 5); + }); + + test('propagates expanded child height through nested dynamic container decorations', async () => { + const renderedSchemas: Schema[] = []; + const createProbePlugin = (defaultSchema: Schema): Plugin => ({ + pdf: ({ schema }) => { + renderedSchemas.push({ + ...schema, + position: { ...schema.position }, + }); + }, + ui: () => {}, + propPanel: { + schema: {}, + defaultSchema, + }, + }); + const outerBox: Schema = { + name: 'outer', + type: 'rectangle', + content: '', + position: { x: 10, y: 10 }, + width: 60, + height: 24, + color: '#f8fafc', + }; + const innerBox: Schema = { + name: 'inner', + type: 'rectangle', + content: '', + position: { x: 14, y: 14 }, + width: 52, + height: 16, + color: '#ffffff', + }; + setDynamicContainerMetadata(innerBox, { + childNames: ['body'], + paddingBottom: 3, + }); + setDynamicContainerMetadata(outerBox, { + childNames: ['inner'], + paddingBottom: 4, + }); + + await generate({ + template: { + basePdf: { width: 100, height: 100, padding: [10, 10, 10, 10] }, + schemas: [ + [ + outerBox, + innerBox, + { + ...textObject(18, 18, 'body'), + width: 28, + height: 5, + overflow: 'expand', + fontSize: 10, + lineHeight: 1, + characterSpacing: 0, + }, + { + ...textObject(10, 34, 'after'), + width: 50, + height: 5, + }, + ], + ], + }, + inputs: [{ body: 'long text '.repeat(10), after: 'after' }], + options: { font: getFont() }, + plugins: { + rectangle: createProbePlugin(outerBox), + text: createProbePlugin(textObject(0, 0)), + }, + }); + + const outer = renderedSchemas.find((schema) => schema.name === 'outer'); + const inner = renderedSchemas.find((schema) => schema.name === 'inner'); + const body = renderedSchemas.find((schema) => schema.name === 'body'); + const after = renderedSchemas.find((schema) => schema.name === 'after'); + + expect(body?.height).toBeGreaterThan(5); + expect(inner?.height).toBeCloseTo(18 - 14 + (body?.height ?? 0) + 3); + expect(outer?.height).toBeCloseTo(14 - 10 + (inner?.height ?? 0) + 4); + expect(after?.position.y).toBeCloseTo(34 + (body?.height ?? 0) - 5); + }); + test('splits expanded text schemas by line across blank PDF pages', async () => { const renderedSchemas: Schema[] = []; const textProbePlugin: Plugin = { @@ -352,7 +519,7 @@ ERROR MESSAGE: Too small: expected array to have >=1 items } catch (e: any) { expect(e.message).toEqual( `[@pdfme/common] fallback flag is not found in font. true fallback flag must be only one. -Check this document: https://pdfme.com/docs/custom-fonts#about-font-type` +Check this document: https://pdfme.com/docs/custom-fonts#about-font-type`, ); } }); @@ -383,7 +550,7 @@ Check this document: https://pdfme.com/docs/custom-fonts#about-font-type` } catch (e: any) { expect(e.message).toEqual( `[@pdfme/common] 2 fallback flags found in font. true fallback flag must be only one. -Check this document: https://pdfme.com/docs/custom-fonts#about-font-type` +Check this document: https://pdfme.com/docs/custom-fonts#about-font-type`, ); } }); @@ -419,7 +586,7 @@ Check this document: https://pdfme.com/docs/custom-fonts#about-font-type` } catch (e: any) { expect(e.message).toEqual( `[@pdfme/common] DUMMY_FONT of template.schemas is not found in font. -Check this document: https://pdfme.com/docs/custom-fonts` +Check this document: https://pdfme.com/docs/custom-fonts`, ); } }); diff --git a/packages/jsx/README.md b/packages/jsx/README.md index 6612d096..b16a4b72 100644 --- a/packages/jsx/README.md +++ b/packages/jsx/README.md @@ -32,6 +32,9 @@ it provides its own `jsx-runtime` and `jsx-dev-runtime`. - `Text` and `MultiVariableText` heights are measured with pdfme's text/rich text wrapping helpers when `height` is omitted. Pass an explicit `height` when you need a fixed field box. +- Auto-height visual `Box` decorations track child `overflow="expand"` reflow for normal same-page + parent/child containers. Children can still split across pages, but the parent Box decoration is + not a multi-page container yet. - `Text textFormat="inline-markdown"` is read-only only. Editable `Text` values use plain content. - `MultiVariableText` uses `text` or children as the template string and stores `values` as the JSON input. Variable names are inferred from `{name}` placeholders and can also be passed with diff --git a/packages/jsx/src/__tests__/render.test.tsx b/packages/jsx/src/__tests__/render.test.tsx index 9da85345..3d0ba49e 100644 --- a/packages/jsx/src/__tests__/render.test.tsx +++ b/packages/jsx/src/__tests__/render.test.tsx @@ -1,6 +1,11 @@ /** @jsxImportSource @pdfme/jsx */ import { describe, expect, it } from 'vitest'; -import { isBlankPdf, PAGE_SIZE_PRESETS } from '@pdfme/common'; +import { + DYNAMIC_CONTAINER_METADATA_KEY, + getDynamicContainerMetadata, + isBlankPdf, + PAGE_SIZE_PRESETS, +} from '@pdfme/common'; import { Absolute, @@ -597,6 +602,27 @@ describe('@pdfme/jsx renderToTemplate', () => { expect(after?.position.y).toBeCloseTo(box?.height ?? 0); }); + it('marks auto-height visual Box schemas as dynamic containers', async () => { + const result = await renderToTemplate( + + + + Message + + + + , + ); + + const [box, label, message] = result.template.schemas[0] ?? []; + expect(box?.type).toBe('rectangle'); + expect(getDynamicContainerMetadata(box!)).toEqual({ + childNames: [label?.name, message?.name], + paddingBottom: 2, + }); + expect(box).toHaveProperty(DYNAMIC_CONTAINER_METADATA_KEY); + }); + it('does not render a rectangle schema for a Box without visual styles', async () => { const result = await renderToTemplate( diff --git a/packages/jsx/src/render.ts b/packages/jsx/src/render.ts index 54a609b1..b747a40a 100644 --- a/packages/jsx/src/render.ts +++ b/packages/jsx/src/render.ts @@ -1,4 +1,11 @@ -import { getDefaultFont, isBlankPdf, pt2mm, resolvePageSize } from '@pdfme/common'; +import { + getDefaultFont, + getDynamicContainerMetadata, + isBlankPdf, + pt2mm, + resolvePageSize, + setDynamicContainerMetadata, +} from '@pdfme/common'; import type { Font, Schema, Template } from '@pdfme/common'; import type { CellStyle as SchemaCellStyle, @@ -791,7 +798,20 @@ const renderBox = async ( const height = props.height ?? childSize.height + padding.top + padding.bottom; if (needsRect) { - ctx.schemas[beforeCount] = { ...ctx.schemas[beforeCount]!, height }; + const boxSchema = { ...ctx.schemas[beforeCount]!, height }; + if (props.height == null) { + const childSchemas = ctx.schemas.slice(beforeCount + 1); + const childNames = childSchemas + .map((schema) => schema.name) + .filter((name): name is string => typeof name === 'string' && name.length > 0); + if (childSchemas.some(isSchemaDynamicContainerSource)) { + setDynamicContainerMetadata(boxSchema, { + childNames, + paddingBottom: padding.bottom, + }); + } + } + ctx.schemas[beforeCount] = boxSchema; } return { width, height }; @@ -802,6 +822,13 @@ const renderSpacer = (props: SpacerProps): { width: number; height: number } => height: props.height ?? 0, }); +const isSchemaDynamicContainerSource = (schema: Schema) => + schema.type === 'table' || + schema.type === 'list' || + ((schema.type === 'text' || schema.type === 'multiVariableText') && + (schema as { overflow?: unknown }).overflow === 'expand') || + getDynamicContainerMetadata(schema) != null; + const renderText = async ( props: TextProps, frame: Rect, diff --git a/packages/schemas/src/dynamicLayout.ts b/packages/schemas/src/dynamicLayout.ts index 6b5fb8ed..42fbe1a3 100644 --- a/packages/schemas/src/dynamicLayout.ts +++ b/packages/schemas/src/dynamicLayout.ts @@ -1,4 +1,11 @@ -import type { DynamicLayoutArgs, DynamicLayoutCallbackResult, Schema } from '@pdfme/common'; +import { + getDynamicContainerMetadata, + replacePlaceholders, + type DynamicLayoutArgs, + type DynamicLayoutCallbackResult, + type DynamicLayoutResult, + type Schema, +} from '@pdfme/common'; import { getDynamicLayoutForList } from './list/dynamicTemplate.js'; import { getDynamicLayoutForMultiVariableText } from './multiVariableText/dynamicTemplate.js'; import { getDynamicLayoutForTable } from './tables/dynamicTemplate.js'; @@ -24,12 +31,88 @@ const isExpandableTextSchema = (schema: Schema) => (schema as { overflow?: unknown }).overflow === TEXT_OVERFLOW_EXPAND; export const isDynamicLayoutSchema = (schema: Schema) => - schema.type === 'table' || schema.type === 'list' || isExpandableTextSchema(schema); + schema.type === 'table' || + schema.type === 'list' || + isExpandableTextSchema(schema) || + getDynamicContainerMetadata(schema) != null; + +const normalizeDynamicLayoutResult = (result: DynamicLayoutCallbackResult): DynamicLayoutResult => { + const dynamicLayout = Array.isArray(result) ? { heights: result } : result; + return { + ...dynamicLayout, + heights: dynamicLayout.heights.length === 0 ? [0] : dynamicLayout.heights, + }; +}; + +const getSchemaValue = (schema: Schema, args: DynamicLayoutArgs): string => { + if (!schema.readOnly) { + return args.input?.[schema.name] || ''; + } + + if (schema.type !== 'text' && schema.type !== 'multiVariableText') { + return schema.content || ''; + } + + return replacePlaceholders({ + content: schema.content || '', + variables: args.input ?? {}, + schemas: args.schemas ?? [args.pageSchemas ?? []], + }); +}; + +const sumHeights = (layout: DynamicLayoutResult) => + layout.heights.reduce((total, height) => total + height, 0); + +const getDynamicLayoutForContainer = async ( + args: DynamicLayoutArgs, +): Promise => { + const metadata = getDynamicContainerMetadata(args.schema); + if (!metadata || !args.pageSchemas) return undefined; + + const pageOrder = new Map(args.pageSchemas.map((schema, index) => [schema.name, index])); + const pageSchemaMap = new Map(args.pageSchemas.map((schema) => [schema.name, schema])); + const children = metadata.childNames + .map((childName) => pageSchemaMap.get(childName)) + .filter((schema): schema is Schema => schema != null) + .sort((a, b) => { + if (a.position.y !== b.position.y) return a.position.y - b.position.y; + if (a.position.x !== b.position.x) return a.position.x - b.position.x; + return (pageOrder.get(a.name) ?? 0) - (pageOrder.get(b.name) ?? 0); + }); + if (children.length === 0) return undefined; + + let localOffset = 0; + let contentBottom = 0; + for (const child of children) { + const value = getSchemaValue(child, args); + const childLayout = normalizeDynamicLayoutResult( + await getDynamicLayoutForSchema(value, { ...args, schema: child }), + ); + const localY = Math.max(0, child.position.y - args.schema.position.y + localOffset); + const dynamicHeight = sumHeights(childLayout); + contentBottom = Math.max(contentBottom, localY + dynamicHeight); + + const originalLocalEndY = Math.max(0, child.position.y - args.schema.position.y) + child.height; + if (childLayout.contributesToFlow !== false) { + localOffset = localY + dynamicHeight - originalLocalEndY; + } + } + + const height = Math.max(args.schema.height, contentBottom + (metadata.paddingBottom ?? 0)); + return { + heights: [height], + contributesToFlow: false, + }; +}; export const getDynamicLayoutForSchema = ( value: string, args: DynamicLayoutArgs, ): Promise => { + if (getDynamicContainerMetadata(args.schema) != null) { + return getDynamicLayoutForContainer(args).then((layout) => layout ?? [args.schema.height]); + } + switch (args.schema.type) { case 'table': return getDynamicLayoutForTable(value, args); diff --git a/playground/public/template-assets/jsx-form-fields/source.tsx b/playground/public/template-assets/jsx-form-fields/source.tsx index 277dadf4..c671c13c 100644 --- a/playground/public/template-assets/jsx-form-fields/source.tsx +++ b/playground/public/template-assets/jsx-form-fields/source.tsx @@ -74,7 +74,7 @@ return ( - Editable fields usually keep explicit heights for predictable input boxes. Read-only text can usually omit height and let JSX measure it. + Auto-height Boxes follow expandable text fields in Form/Viewer and generated PDFs. Add explicit heights only when you want a fixed input area. diff --git a/playground/public/template-assets/jsx-form-fields/template.json b/playground/public/template-assets/jsx-form-fields/template.json index fcfafa27..ff8698a3 100644 --- a/playground/public/template-assets/jsx-form-fields/template.json +++ b/playground/public/template-assets/jsx-form-fields/template.json @@ -288,7 +288,14 @@ "color": "#f8fafc", "borderColor": "#cbd5e1", "borderWidth": 0.4, - "radius": 0 + "radius": 0, + "__pdfmeDynamicContainer": { + "childNames": [ + "text_7", + "message" + ], + "paddingBottom": 4 + } }, { "name": "text_7", @@ -424,7 +431,7 @@ "y": 112.509184 }, "width": 112, - "height": 17.2588832, + "height": 21.5454032, "rotate": 0, "opacity": 1, "readOnly": true, @@ -436,13 +443,13 @@ { "name": "text_9", "type": "text", - "content": "Editable fields usually keep explicit heights for predictable input boxes. Read-only text can usually omit height and let JSX measure it.", + "content": "Auto-height Boxes follow expandable text fields in Form/Viewer and generated PDFs. Add explicit heights only when you want a fixed input area.", "position": { "x": 84, "y": 116.509184 }, "width": 104, - "height": 9.2588832, + "height": 13.545403199999999, "rotate": 0, "opacity": 1, "readOnly": true, diff --git a/playground/public/template-assets/jsx-invoice/template.json b/playground/public/template-assets/jsx-invoice/template.json index e6eb629c..9a1349cf 100644 --- a/playground/public/template-assets/jsx-invoice/template.json +++ b/playground/public/template-assets/jsx-invoice/template.json @@ -321,7 +321,14 @@ "color": "", "borderColor": "#e2e8f0", "borderWidth": 0.4, - "radius": 0 + "radius": 0, + "__pdfmeDynamicContainer": { + "childNames": [ + "text_9", + "list_1" + ], + "paddingBottom": 4 + } }, { "name": "text_9", diff --git a/playground/public/template-assets/jsx-japanese-notice/template.json b/playground/public/template-assets/jsx-japanese-notice/template.json index 1a58e064..4c965c7d 100644 --- a/playground/public/template-assets/jsx-japanese-notice/template.json +++ b/playground/public/template-assets/jsx-japanese-notice/template.json @@ -103,7 +103,13 @@ "color": "#f8fafc", "borderColor": "#cbd5e1", "borderWidth": 0.4, - "radius": 0 + "radius": 0, + "__pdfmeDynamicContainer": { + "childNames": [ + "text_4" + ], + "paddingBottom": 5 + } }, { "name": "text_4", diff --git a/playground/public/template-assets/jsx-report/template.json b/playground/public/template-assets/jsx-report/template.json index 3875195f..cd42fdec 100644 --- a/playground/public/template-assets/jsx-report/template.json +++ b/playground/public/template-assets/jsx-report/template.json @@ -354,7 +354,15 @@ "color": "", "borderColor": "#cbd5e1", "borderWidth": 0.4, - "radius": 0 + "radius": 0, + "__pdfmeDynamicContainer": { + "childNames": [ + "text_12", + "text_13", + "list_1" + ], + "paddingBottom": 5 + } }, { "name": "text_12", diff --git a/website/docs/jsx.md b/website/docs/jsx.md index de0174ce..c6f8a3b1 100644 --- a/website/docs/jsx.md +++ b/website/docs/jsx.md @@ -84,7 +84,9 @@ Use `gap`, `margin`, `alignItems`, `justifyContent`, and `flex` / `flexGrow` for `Text`, `MultiVariableText`, `List`, and `Table` can usually omit `height`. JSX measures their initial content while rendering and advances the surrounding `Stack`, `Row`, or `Box`. Use explicit `height` when you want a fixed field, a fixed visual area, or predictable form input box. A `Box` without -`height` grows around its children during JSX rendering. +`height` grows around its children during JSX rendering. If that visual `Box` contains runtime +dynamic text/MVT content, pdfme also keeps the Box decoration height in sync when Form/Viewer or +generator reflows `overflow="expand"` content. ## Schema Components @@ -166,5 +168,6 @@ page coordinate system and is intended for advanced overlays such as watermarks, fonts, and generator/viewer options. - `Header`, `Footer`, and `Static` are supported only as direct children of `Document`. They generate blank `basePdf.staticSchema` and cannot be used with a custom PDF `basePdf`. -- If a form input later expands at runtime, a parent `Box` rectangle is not dynamically resized yet. - Dynamic parent/child container reflow is a future layout feature. +- Dynamic Box decoration reflow is intended for same-page parent/child visual containers. If a child + expands across a page break, the child schema can split across pages, but the parent Box decoration + is not a multi-page container yet. diff --git a/website/i18n/ja/docusaurus-plugin-content-docs/current/jsx.md b/website/i18n/ja/docusaurus-plugin-content-docs/current/jsx.md index 38286ad1..14e147a1 100644 --- a/website/i18n/ja/docusaurus-plugin-content-docs/current/jsx.md +++ b/website/i18n/ja/docusaurus-plugin-content-docs/current/jsx.md @@ -83,7 +83,8 @@ layout API は CSS/Flexbox 互換を目指していません。`gap`, `margin`, `Text`, `MultiVariableText`, `List`, `Table` は通常 `height` を省略できます。JSX render 時に初期 content を測定し、周囲の `Stack`, `Row`, `Box` を進めます。固定 field、固定 visual area、Form の入力 box を 明確にしたい場合だけ `height` を指定してください。`height` のない `Box` は、JSX render 時点では子要素の高さに -合わせて伸びます。 +合わせて伸びます。その visual `Box` の中に runtime dynamic な text / MVT がある場合、Form/Viewer や +generator が `overflow="expand"` を reflow するときも Box decoration の高さを追従させます。 ## schema component @@ -163,5 +164,6 @@ watermark、crop mark、stamp などの advanced overlay 向けです。 - 出力は `Template + inputs` です。実際の描画は通常通り pdfme plugins, fonts, generator/viewer options に依存します。 - `Header`, `Footer`, `Static` は `Document` の direct child としてのみ使えます。これらは blank `basePdf.staticSchema` を生成するため、custom PDF `basePdf` とは併用できません。 -- Form 入力によって runtime に text / MVT が expand した場合、親 `Box` の rectangle はまだ自動では - 再計算されません。親子 container の dynamic reflow は今後の layout 機能として扱います。 +- Dynamic Box decoration reflow は同一ページ内の親子 visual container 向けです。child schema が page + break を跨いで split する場合、child は複数ページに分割できますが、親 Box decoration はまだ multi-page + container にはなりません。