From d4e86584b83c3265094625e003c0e54acf0ab4ca Mon Sep 17 00:00:00 2001 From: William Candillon Date: Thu, 3 Feb 2022 09:59:30 +0100 Subject: [PATCH 01/28] Implement drawGlyphs --- package/cpp/api/JsiSkCanvas.h | 36 +++++++++++++++ .../src/renderer/components/text/Glyphs.tsx | 44 +++++++++++++++++++ package/src/renderer/components/text/Text.tsx | 31 ++++--------- package/src/renderer/processors/Font.ts | 25 +++++++++++ package/src/renderer/processors/index.ts | 1 + package/src/skia/Canvas.ts | 18 ++++++++ 6 files changed, 132 insertions(+), 23 deletions(-) create mode 100644 package/src/renderer/components/text/Glyphs.tsx create mode 100644 package/src/renderer/processors/Font.ts diff --git a/package/cpp/api/JsiSkCanvas.h b/package/cpp/api/JsiSkCanvas.h index 4ba1438526..9bb39eaf95 100644 --- a/package/cpp/api/JsiSkCanvas.h +++ b/package/cpp/api/JsiSkCanvas.h @@ -314,6 +314,41 @@ class JsiSkCanvas : public JsiSkHostObject { return jsi::Value::undefined(); } + JSI_HOST_FUNCTION(drawGlyphs) { + auto jsiGlyphs = arguments[0].asObject(runtime).asArray(runtime); + auto jsiPositions = arguments[1].asObject(runtime).asArray(runtime); + auto x = arguments[2].asNumber(); + auto y = arguments[3].asNumber(); + auto font = JsiSkFont::fromValue(runtime, arguments[4]); + auto paint = JsiSkPaint::fromValue(runtime, arguments[5]); + SkPoint origin = SkPoint::Make(x, y); + + std::vector positions; + int pointsSize = static_cast(jsiPositions.size(runtime)); + for (int i = 0; i < pointsSize; i++) { + std::shared_ptr point = JsiSkPoint::fromValue( + runtime, jsiPositions.getValueAtIndex(runtime, i).asObject(runtime)); + positions.push_back(*point.get()); + } + + std::vector glyphs; + int glyphsSize = static_cast(jsiPositions.size(runtime)); + for (int i = 0; i < glyphsSize; i++) { + glyphs.push_back(jsiGlyphs.getValueAtIndex(runtime, i).asNumber()); + } + + _canvas->drawGlyphs( + glyphsSize, + glyphs.data(), + positions.data(), + origin, + *font, + *paint + ); + + return jsi::Value::undefined(); + } + JSI_HOST_FUNCTION(drawSvg) { auto svgdom = JsiSkSVG::fromValue(runtime, arguments[0]); if (count == 3) { @@ -449,6 +484,7 @@ class JsiSkCanvas : public JsiSkHostObject { JSI_EXPORT_FUNC(JsiSkCanvas, drawPath), JSI_EXPORT_FUNC(JsiSkCanvas, drawVertices), JSI_EXPORT_FUNC(JsiSkCanvas, drawText), + JSI_EXPORT_FUNC(JsiSkCanvas, drawGlyphs), JSI_EXPORT_FUNC(JsiSkCanvas, drawSvg), JSI_EXPORT_FUNC(JsiSkCanvas, clipPath), JSI_EXPORT_FUNC(JsiSkCanvas, clipRect), diff --git a/package/src/renderer/components/text/Glyphs.tsx b/package/src/renderer/components/text/Glyphs.tsx new file mode 100644 index 0000000000..c3d5bd87f5 --- /dev/null +++ b/package/src/renderer/components/text/Glyphs.tsx @@ -0,0 +1,44 @@ +import React from "react"; + +import type { CustomPaintProps } from "../../processors/Paint"; +import { useDrawing } from "../../nodes/Drawing"; +import type { AnimatedProps, FontDef } from "../../processors"; +import type { IPoint } from "../../../skia/Point"; +import { processFont } from "../../processors"; + +export interface Glyph { + id: number; + pos: IPoint; +} + +export type GlyphsProps = CustomPaintProps & + FontDef & { + x: number; + y: number; + glyphs: Glyph[]; + }; + +interface ProcessedGlyphs { + glyphs: number[]; + positions: IPoint[]; +} + +export const Glyphs = (props: AnimatedProps) => { + const onDraw = useDrawing( + props, + ({ canvas, paint, fontMgr }, { glyphs: rawGlyphs, x, y, ...fontDef }) => { + const font = processFont(fontMgr, fontDef); + const { glyphs, positions } = rawGlyphs.reduce( + (acc, glyph) => { + const { id, pos } = glyph; + acc.glyphs.push(id); + acc.positions.push(pos); + return acc; + }, + { glyphs: [], positions: [] } + ); + canvas.drawGlyphs(glyphs, positions, x, y, font, paint); + } + ); + return ; +}; diff --git a/package/src/renderer/components/text/Text.tsx b/package/src/renderer/components/text/Text.tsx index 796eb26322..3d6d4d543e 100644 --- a/package/src/renderer/components/text/Text.tsx +++ b/package/src/renderer/components/text/Text.tsx @@ -1,17 +1,12 @@ import React from "react"; -import type { CustomPaintProps } from "../../processors/Paint"; +import type { + CustomPaintProps, + AnimatedProps, + FontDef, +} from "../../processors"; import { useDrawing } from "../../nodes/Drawing"; -import type { Font } from "../../../skia"; -import { Skia } from "../../../skia"; -import type { AnimatedProps } from ".."; - -type FontDef = { font: Font } | { familyName: string; size: number }; - -const isFont = (fontDef: FontDef): fontDef is { font: Font } => - // We use any here for safety (JSI instances don't have hasProperty working properly); - // eslint-disable-next-line @typescript-eslint/no-explicit-any - (fontDef as any).font !== undefined; +import { processFont } from "../../processors"; type TextProps = CustomPaintProps & FontDef & { @@ -24,18 +19,8 @@ export const Text = (props: AnimatedProps) => { const onDraw = useDrawing( props, ({ canvas, paint, fontMgr }, { value, x, y, ...fontDef }) => { - let selectedFont: Font; - if (isFont(fontDef)) { - selectedFont = fontDef.font; - } else { - const { familyName, size } = fontDef; - const typeface = fontMgr.matchFamilyStyle(familyName); - if (typeface === null) { - throw new Error(`No typeface found for ${familyName}`); - } - selectedFont = Skia.Font(typeface, size); - } - canvas.drawText(value, x, y, paint, selectedFont); + const font = processFont(fontMgr, fontDef); + canvas.drawText(value, x, y, paint, font); } ); return ; diff --git a/package/src/renderer/processors/Font.ts b/package/src/renderer/processors/Font.ts new file mode 100644 index 0000000000..c235b0c7f8 --- /dev/null +++ b/package/src/renderer/processors/Font.ts @@ -0,0 +1,25 @@ +import type { Font } from "../../skia"; +import { Skia } from "../../skia/Skia"; +import type { FontMgr } from "../../skia/FontMgr/FontMgr"; + +export type FontDef = { font: Font } | { familyName: string; size: number }; + +export const isFont = (fontDef: FontDef): fontDef is { font: Font } => + // We use any here for safety (JSI instances don't have hasProperty working properly); + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (fontDef as any).font !== undefined; + +export const processFont = (fontMgr: FontMgr, fontDef: FontDef) => { + let selectedFont: Font; + if (isFont(fontDef)) { + selectedFont = fontDef.font; + } else { + const { familyName, size } = fontDef; + const typeface = fontMgr.matchFamilyStyle(familyName); + if (typeface === null) { + throw new Error(`No typeface found for ${familyName}`); + } + selectedFont = Skia.Font(typeface, size); + } + return selectedFont; +}; diff --git a/package/src/renderer/processors/index.ts b/package/src/renderer/processors/index.ts index 08819bcde4..e4f7c66338 100644 --- a/package/src/renderer/processors/index.ts +++ b/package/src/renderer/processors/index.ts @@ -4,3 +4,4 @@ export * from "./Transform"; export * from "./Animations"; export * from "./Shapes"; export * from "./math"; +export * from "./Font"; diff --git a/package/src/skia/Canvas.ts b/package/src/skia/Canvas.ts index 92bca4102b..1a3bac17d2 100644 --- a/package/src/skia/Canvas.ts +++ b/package/src/skia/Canvas.ts @@ -318,6 +318,24 @@ export interface ICanvas { */ drawText(str: string, x: number, y: number, paint: IPaint, font: Font): void; + /** + * Draws a run of glyphs, at corresponding positions, in a given font. + * @param glyphs the array of glyph IDs (Uint16TypedArray) + * @param positions the array of x,y floats to position each glyph + * @param x x-coordinate of the origin of the entire run + * @param y y-coordinate of the origin of the entire run + * @param font the font that contains the glyphs + * @param paint + */ + drawGlyphs( + glyphs: number[], + positions: IPoint[], + x: number, + y: number, + font: Font, + paint: IPaint + ): void; + /** * Renders the SVG Dom object to the canvas. If width/height are omitted, * the SVG will be rendered to fit the canvas. From 2c43168bc625e8ee6065fcef4ee0aa91264e7416 Mon Sep 17 00:00:00 2001 From: William Candillon Date: Thu, 3 Feb 2022 10:16:30 +0100 Subject: [PATCH 02/28] Add test --- example/src/Examples/Matrix/Symbol.tsx | 31 +++++++++++++++---- package/src/renderer/components/text/index.ts | 1 + 2 files changed, 26 insertions(+), 6 deletions(-) diff --git a/example/src/Examples/Matrix/Symbol.tsx b/example/src/Examples/Matrix/Symbol.tsx index 648680913b..88765fcce4 100644 --- a/example/src/Examples/Matrix/Symbol.tsx +++ b/example/src/Examples/Matrix/Symbol.tsx @@ -1,6 +1,6 @@ import React, { useRef } from "react"; import type { AnimationValue, Font } from "@shopify/react-native-skia"; -import { Text } from "@shopify/react-native-skia"; +import { vec, Text, Glyphs } from "@shopify/react-native-skia"; import { Dimensions } from "react-native"; import { interpolateColors } from "../../../../package/src/animation/functions/interpolateColors"; @@ -9,7 +9,7 @@ const { width, height } = Dimensions.get("window"); export const COLS = 8; export const ROWS = 15; export const SYMBOL = { width: width / COLS, height: height / ROWS }; -const symbols = "abcdefghijklmnopqrstuvwxyz".split(""); +const symbols = "abcdefghijklmnopqrstuvwxyz".toUpperCase().split(""); interface SymbolProps { i: number; @@ -24,20 +24,22 @@ export const Symbol = ({ i, j, timestamp, stream, font }: SymbolProps) => { const range = useRef(100 + Math.random() * 900); const x = i * SYMBOL.width; const y = j * SYMBOL.height; - const value = () => { + const glyphs = () => { const idx = offset.current + Math.floor(timestamp.value / range.current); - return symbols[idx % symbols.length]; + return [ + { id: symbols[idx % symbols.length].codePointAt(0), pos: vec(0, 0) }, + ]; }; const opacity = () => { const idx = Math.round(timestamp.value / 100); return stream[(stream.length - j + idx) % stream.length]; }; return ( - interpolateColors( @@ -48,4 +50,21 @@ export const Symbol = ({ i, j, timestamp, stream, font }: SymbolProps) => { } /> ); + // Alternative implementation using + // return ( + // + // interpolateColors( + // opacity(), + // [0.8, 1], + // ["rgb(0, 255, 70)", "rgb(140, 255, 170)"] + // ) + // } + // /> + // ); }; diff --git a/package/src/renderer/components/text/index.ts b/package/src/renderer/components/text/index.ts index 22e10b6750..c3a24ae8c8 100644 --- a/package/src/renderer/components/text/index.ts +++ b/package/src/renderer/components/text/index.ts @@ -1 +1,2 @@ export * from "./Text"; +export * from "./Glyphs"; From f6f9beb9d3c1e9a147326aa6dff31b2dc5856eda Mon Sep 17 00:00:00 2001 From: William Candillon Date: Thu, 3 Feb 2022 11:07:15 +0100 Subject: [PATCH 03/28] :lipstick: --- example/src/Examples/Matrix/Matrix.tsx | 2 +- example/src/Examples/Matrix/Symbol.tsx | 47 +++++++++----------------- 2 files changed, 17 insertions(+), 32 deletions(-) diff --git a/example/src/Examples/Matrix/Matrix.tsx b/example/src/Examples/Matrix/Matrix.tsx index 98ebf614df..87f85d5ba6 100644 --- a/example/src/Examples/Matrix/Matrix.tsx +++ b/example/src/Examples/Matrix/Matrix.tsx @@ -37,7 +37,7 @@ export const Matrix = () => { return null; } return ( - + diff --git a/example/src/Examples/Matrix/Symbol.tsx b/example/src/Examples/Matrix/Symbol.tsx index 88765fcce4..430de4c418 100644 --- a/example/src/Examples/Matrix/Symbol.tsx +++ b/example/src/Examples/Matrix/Symbol.tsx @@ -1,15 +1,19 @@ import React, { useRef } from "react"; import type { AnimationValue, Font } from "@shopify/react-native-skia"; -import { vec, Text, Glyphs } from "@shopify/react-native-skia"; +import { vec, Glyphs } from "@shopify/react-native-skia"; import { Dimensions } from "react-native"; import { interpolateColors } from "../../../../package/src/animation/functions/interpolateColors"; const { width, height } = Dimensions.get("window"); -export const COLS = 8; -export const ROWS = 15; +export const COLS = 5; +export const ROWS = 10; export const SYMBOL = { width: width / COLS, height: height / ROWS }; -const symbols = "abcdefghijklmnopqrstuvwxyz".toUpperCase().split(""); +const pos = vec(0, 0); +const symbols = "abcdefghijklmnopqrstuvwxyz" + .toUpperCase() + .split("") + .map((c) => c.codePointAt(0)!); interface SymbolProps { i: number; @@ -26,14 +30,18 @@ export const Symbol = ({ i, j, timestamp, stream, font }: SymbolProps) => { const y = j * SYMBOL.height; const glyphs = () => { const idx = offset.current + Math.floor(timestamp.value / range.current); - return [ - { id: symbols[idx % symbols.length].codePointAt(0), pos: vec(0, 0) }, - ]; + return [{ id: symbols[idx % symbols.length], pos }]; }; const opacity = () => { const idx = Math.round(timestamp.value / 100); return stream[(stream.length - j + idx) % stream.length]; }; + const color = () => + interpolateColors( + opacity(), + [0.8, 1], + ["rgb(0, 255, 70)", "rgb(140, 255, 170)"] + ); return ( { font={font} glyphs={glyphs} opacity={opacity} - color={() => - interpolateColors( - opacity(), - [0.8, 1], - ["rgb(0, 255, 70)", "rgb(140, 255, 170)"] - ) - } + color={color} /> ); - // Alternative implementation using - // return ( - // - // interpolateColors( - // opacity(), - // [0.8, 1], - // ["rgb(0, 255, 70)", "rgb(140, 255, 170)"] - // ) - // } - // /> - // ); }; From 446d7d62c7a60ee17823b12453a764caddefc179 Mon Sep 17 00:00:00 2001 From: William Candillon Date: Thu, 3 Feb 2022 14:05:26 +0100 Subject: [PATCH 04/28] Document Glyphs feature --- docs/docs/text/fonts.md | 25 ++++++++++- docs/docs/text/glyphs.md | 41 +++++++++++++++++++ docs/docs/text/text.md | 39 ++++-------------- docs/sidebars.js | 2 +- example/src/Examples/Matrix/Matrix.tsx | 4 +- example/src/Examples/Matrix/Symbol.tsx | 7 ++-- .../src/renderer/components/text/Glyphs.tsx | 14 +++++-- package/src/renderer/components/text/Text.tsx | 11 +++-- package/src/renderer/processors/Font.ts | 8 ++-- package/src/skia/Canvas.ts | 6 +-- package/src/skia/Font/Font.ts | 3 +- package/src/skia/Font/useFont.ts | 4 +- package/src/skia/Path/Path.ts | 4 +- package/src/skia/Path/usePath.ts | 4 +- package/src/skia/Skia.ts | 4 +- 15 files changed, 114 insertions(+), 62 deletions(-) create mode 100644 docs/docs/text/glyphs.md diff --git a/docs/docs/text/fonts.md b/docs/docs/text/fonts.md index 6d541b25b2..e574e40733 100644 --- a/docs/docs/text/fonts.md +++ b/docs/docs/text/fonts.md @@ -16,11 +16,34 @@ export const HelloWorld = () => { ); }; +``` + +Alternatively, you can use your own set of custom fonts to be available in the canvas, as seen below. + +```tsx twoslash +import {Canvas, Text, useFont} from "@shopify/react-native-skia"; + +export const HelloWorld = () => { + const font = useFont(require("./my-custom-font.otf"), 16); + if (font === null) { + return null; + } + return ( + + + + ); +}; ``` \ No newline at end of file diff --git a/docs/docs/text/glyphs.md b/docs/docs/text/glyphs.md new file mode 100644 index 0000000000..83d443a011 --- /dev/null +++ b/docs/docs/text/glyphs.md @@ -0,0 +1,41 @@ +--- +id: glyphs +title: Glyphs +sidebar_label: Glyphs +slug: /text/glyphs +--- + +This component raws a run of glyphs, at corresponding positions, in a given font. +The font family and the font size must be specified. +The fonts available in the canvas are described in [here](/docs/text/fonts). + +| Name | Type | Description | +|:------------|:-----------|:-----------------------------------------------------------------------| +| glyphs | `Ghlyph[]` | Glyphs to draw | +| x? | `number`. | x coordinate of the origin of the entire run. Default is 0 | +| y? | `number`. | y coordinate of the origin of the entire run. Default is 0 | +| size? | `number` | Font size | +| familyName? | `string` | Font family name | +| font? | `font` | Custom font to use | + + +## Draw text vertically + +```tsx twoslash +import {Canvas, Glyphs, vec} from "@shopify/react-native-skia"; + +export const HelloWorld = () => { + const glyphs = "Hello World!" + .split("") + .map((c, i) => ({ id: c.codePointAt(0)!, pos: vec(0, i * 32) })); + return ( + + + + ); +} +``` \ No newline at end of file diff --git a/docs/docs/text/text.md b/docs/docs/text/text.md index 6592664017..98170060af 100644 --- a/docs/docs/text/text.md +++ b/docs/docs/text/text.md @@ -6,15 +6,14 @@ slug: /text/text --- The text component can be used to draw a simple text. -The font family and the font size must be specified. The fonts available in the canvas are described in [here](/docs/text/fonts). -| Name | Type | Description | -|:-----------|:----------|:--------------------------------------------------------------| -| value | `string` | Text to draw | -| familyName | `string` | Font family name | -| size | `number` | Font size | -| font | `font` | Custom font to use (loaded with useFont) | +| Name | Type | Description | +|:------------|:----------|:--------------------------------------------------------------| +| text | `string` | Text to draw | +| size? | `number` | Font size | +| familyName? | `string` | Font family name | +| font? | `font` | Custom font to use | ### Example @@ -29,34 +28,10 @@ export const HelloWorld = () => { y={0} value="Hello World" familyName="serif" - size={32} + text={32} /> ); }; ``` -## Using a custom font - -Alternatively, you can use your own set of custom fonts to be available in the canvas, as seen below. - -```tsx twoslash -import {Canvas, Text, useFont} from "@shopify/react-native-skia"; - -export const HelloWorld = () => { - const font = useFont(require("./my-custom-font.otf"), 16); - if (font === null) { - return null; - } - return ( - - - - ); -}; -``` diff --git a/docs/sidebars.js b/docs/sidebars.js index d3d2454b9b..b88d3d22e3 100644 --- a/docs/sidebars.js +++ b/docs/sidebars.js @@ -47,7 +47,7 @@ const sidebars = { collapsed: true, type: "category", label: "Text", - items: ["text/fonts", "text/text"], + items: ["text/fonts", "text/text", "text/glyphs"], }, { collapsed: true, diff --git a/example/src/Examples/Matrix/Matrix.tsx b/example/src/Examples/Matrix/Matrix.tsx index 87f85d5ba6..22e99a4a52 100644 --- a/example/src/Examples/Matrix/Matrix.tsx +++ b/example/src/Examples/Matrix/Matrix.tsx @@ -37,10 +37,10 @@ export const Matrix = () => { return null; } return ( - + - + {cols.map((_i, i) => rows.map((_j, j) => ( diff --git a/example/src/Examples/Matrix/Symbol.tsx b/example/src/Examples/Matrix/Symbol.tsx index 430de4c418..1b147ffbb9 100644 --- a/example/src/Examples/Matrix/Symbol.tsx +++ b/example/src/Examples/Matrix/Symbol.tsx @@ -1,5 +1,5 @@ import React, { useRef } from "react"; -import type { AnimationValue, Font } from "@shopify/react-native-skia"; +import type { AnimationValue, IFont } from "@shopify/react-native-skia"; import { vec, Glyphs } from "@shopify/react-native-skia"; import { Dimensions } from "react-native"; @@ -20,7 +20,7 @@ interface SymbolProps { j: number; timestamp: AnimationValue; stream: number[]; - font: Font; + font: IFont; } export const Symbol = ({ i, j, timestamp, stream, font }: SymbolProps) => { @@ -44,8 +44,7 @@ export const Symbol = ({ i, j, timestamp, stream, font }: SymbolProps) => { ); return ( ) => { ); return ; }; + +Glyphs.defaultProps = { + x: 0, + y: 0, +}; diff --git a/package/src/renderer/components/text/Text.tsx b/package/src/renderer/components/text/Text.tsx index 3d6d4d543e..fff92e3742 100644 --- a/package/src/renderer/components/text/Text.tsx +++ b/package/src/renderer/components/text/Text.tsx @@ -10,7 +10,7 @@ import { processFont } from "../../processors"; type TextProps = CustomPaintProps & FontDef & { - value: string; + text: string; x: number; y: number; }; @@ -18,10 +18,15 @@ type TextProps = CustomPaintProps & export const Text = (props: AnimatedProps) => { const onDraw = useDrawing( props, - ({ canvas, paint, fontMgr }, { value, x, y, ...fontDef }) => { + ({ canvas, paint, fontMgr }, { text, x, y, ...fontDef }) => { const font = processFont(fontMgr, fontDef); - canvas.drawText(value, x, y, paint, font); + canvas.drawText(text, x, y, paint, font); } ); return ; }; + +Text.defaultProps = { + x: 0, + y: 0, +}; diff --git a/package/src/renderer/processors/Font.ts b/package/src/renderer/processors/Font.ts index c235b0c7f8..618c79047c 100644 --- a/package/src/renderer/processors/Font.ts +++ b/package/src/renderer/processors/Font.ts @@ -1,16 +1,16 @@ -import type { Font } from "../../skia"; +import type { IFont } from "../../skia"; import { Skia } from "../../skia/Skia"; import type { FontMgr } from "../../skia/FontMgr/FontMgr"; -export type FontDef = { font: Font } | { familyName: string; size: number }; +export type FontDef = { font: IFont } | { familyName: string; size: number }; -export const isFont = (fontDef: FontDef): fontDef is { font: Font } => +export const isFont = (fontDef: FontDef): fontDef is { font: IFont } => // We use any here for safety (JSI instances don't have hasProperty working properly); // eslint-disable-next-line @typescript-eslint/no-explicit-any (fontDef as any).font !== undefined; export const processFont = (fontMgr: FontMgr, fontDef: FontDef) => { - let selectedFont: Font; + let selectedFont: IFont; if (isFont(fontDef)) { selectedFont = fontDef.font; } else { diff --git a/package/src/skia/Canvas.ts b/package/src/skia/Canvas.ts index 1a3bac17d2..ba9f852051 100644 --- a/package/src/skia/Canvas.ts +++ b/package/src/skia/Canvas.ts @@ -1,6 +1,6 @@ import type { IPaint } from "./Paint"; import type { IRect } from "./Rect"; -import type { Font } from "./Font"; +import type { IFont } from "./Font"; import type { IPath } from "./Path"; import type { IImage } from "./Image"; import type { SVG } from "./SVG"; @@ -316,7 +316,7 @@ export interface ICanvas { * @param paint * @param font */ - drawText(str: string, x: number, y: number, paint: IPaint, font: Font): void; + drawText(str: string, x: number, y: number, paint: IPaint, font: IFont): void; /** * Draws a run of glyphs, at corresponding positions, in a given font. @@ -332,7 +332,7 @@ export interface ICanvas { positions: IPoint[], x: number, y: number, - font: Font, + font: IFont, paint: IPaint ): void; diff --git a/package/src/skia/Font/Font.ts b/package/src/skia/Font/Font.ts index f86ee35471..08808cd76d 100644 --- a/package/src/skia/Font/Font.ts +++ b/package/src/skia/Font/Font.ts @@ -1,7 +1,8 @@ +import type { SkJSIInstance } from "../JsiInstance"; import type { IPaint } from "../Paint"; import type { IRect } from "../Rect"; -export interface Font { +export interface IFont extends SkJSIInstance<"Font"> { /** Get/Sets text size in points. Has no effect if textSize is not greater than or equal to zero. */ diff --git a/package/src/skia/Font/useFont.ts b/package/src/skia/Font/useFont.ts index 765f8509aa..ec27089164 100644 --- a/package/src/skia/Font/useFont.ts +++ b/package/src/skia/Font/useFont.ts @@ -5,12 +5,12 @@ import type { DataSource } from "../Data"; import { Skia } from "../Skia"; import { useTypeface } from "../Typeface"; -import type { Font } from "./Font"; +import type { IFont } from "./Font"; /** * Returns a Skia Font object * */ -export const useFont = (font: DataSource, size?: number): Font | null => { +export const useFont = (font: DataSource, size?: number): IFont | null => { const typeface = useTypeface(font); return useMemo(() => { if (typeface === null) { diff --git a/package/src/skia/Path/Path.ts b/package/src/skia/Path/Path.ts index 79d4a537bc..1f7709c46e 100644 --- a/package/src/skia/Path/Path.ts +++ b/package/src/skia/Path/Path.ts @@ -1,4 +1,4 @@ -import type { Font } from "../Font"; +import type { IFont } from "../Font"; import type { IRect } from "../Rect"; import type { IPoint } from "../Point"; import type { IRRect } from "../RRect"; @@ -503,7 +503,7 @@ export interface IPath extends SkJSIInstance<"Path"> { /** * Converts the text to a path with the given font at location x / y. */ - fromText(text: string, x: number, y: number, font: Font): void; + fromText(text: string, x: number, y: number, font: IFont): void; /** * Interpolates between Path with point array of equal size. diff --git a/package/src/skia/Path/usePath.ts b/package/src/skia/Path/usePath.ts index 073d14859d..3835ec3a00 100644 --- a/package/src/skia/Path/usePath.ts +++ b/package/src/skia/Path/usePath.ts @@ -1,7 +1,7 @@ import type { DependencyList } from "react"; import { useMemo } from "react"; -import type { Font } from "../Font"; +import type { IFont } from "../Font"; import { Skia } from "../Skia"; import type { IPath } from "./Path"; @@ -45,7 +45,7 @@ export const useSvgPath = (svgpath: string) => * @param svgpath * @returns */ -export const useTextPath = (text: string, x: number, y: number, font: Font) => +export const useTextPath = (text: string, x: number, y: number, font: IFont) => usePath( (p) => { p.fromText(text, x, y, font); diff --git a/package/src/skia/Skia.ts b/package/src/skia/Skia.ts index 73e79982c3..f4c6133519 100644 --- a/package/src/skia/Skia.ts +++ b/package/src/skia/Skia.ts @@ -2,7 +2,7 @@ import type { ImageFilterFactory } from "./ImageFilter"; import type { PathFactory } from "./Path"; import type { ColorFilterFactory } from "./ColorFilter"; -import type { Font } from "./Font"; +import type { IFont } from "./Font"; import type { Typeface, TypefaceFactory } from "./Typeface"; import type { ImageFactory } from "./Image"; import type { MaskFilterFactory } from "./MaskFilter"; @@ -33,7 +33,7 @@ export interface Skia { Path: PathFactory; Matrix: () => Matrix; ColorFilter: ColorFilterFactory; - Font: (typeface?: Typeface, size?: number) => Font; + Font: (typeface?: Typeface, size?: number) => IFont; Typeface: TypefaceFactory; MaskFilter: MaskFilterFactory; RuntimeEffect: RuntimeEffectFactory; From 7fa98d94e57671adec3b1a1213407340a8d2c144 Mon Sep 17 00:00:00 2001 From: William Candillon Date: Thu, 3 Feb 2022 14:07:56 +0100 Subject: [PATCH 05/28] Add Font.__typename__ --- package/cpp/api/JsiSkFont.h | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/package/cpp/api/JsiSkFont.h b/package/cpp/api/JsiSkFont.h index 355a02f6c2..bc34e67a21 100644 --- a/package/cpp/api/JsiSkFont.h +++ b/package/cpp/api/JsiSkFont.h @@ -22,6 +22,12 @@ using namespace facebook; class JsiSkFont : public JsiSkWrappingSharedPtrHostObject { public: + + // TODO: declare in JsiSkWrappingSkPtrHostObject via extra template parameter? + JSI_PROPERTY_GET(__typename__) { + return jsi::String::createFromUtf8(runtime, "Font"); + } + JSI_PROPERTY_GET(size) { return static_cast(getObject()->getSize()); } JSI_PROPERTY_SET(size) { getObject()->setSize(value.asNumber()); } @@ -40,7 +46,7 @@ class JsiSkFont : public JsiSkWrappingSharedPtrHostObject { return JsiSkRect::toValue(runtime, getContext(), rect); } - JSI_EXPORT_PROPERTY_GETTERS(JSI_EXPORT_PROP_GET(JsiSkFont, size)) + JSI_EXPORT_PROPERTY_GETTERS(JSI_EXPORT_PROP_GET(JsiSkFont, size), JSI_EXPORT_PROP_GET(JsiSkFont, __typename__)) JSI_EXPORT_PROPERTY_SETTERS(JSI_EXPORT_PROP_SET(JsiSkFont, size)) JSI_EXPORT_FUNCTIONS(JSI_EXPORT_FUNC(JsiSkFont, measureText)) From d64b76e534bbb38a2bf18cccd6b92c18765b70ab Mon Sep 17 00:00:00 2001 From: William Candillon Date: Thu, 3 Feb 2022 14:09:06 +0100 Subject: [PATCH 06/28] Fix example --- example/src/Examples/Matrix/Symbol.tsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/example/src/Examples/Matrix/Symbol.tsx b/example/src/Examples/Matrix/Symbol.tsx index 1b147ffbb9..e4b627f765 100644 --- a/example/src/Examples/Matrix/Symbol.tsx +++ b/example/src/Examples/Matrix/Symbol.tsx @@ -44,7 +44,8 @@ export const Symbol = ({ i, j, timestamp, stream, font }: SymbolProps) => { ); return ( Date: Thu, 3 Feb 2022 14:10:38 +0100 Subject: [PATCH 07/28] :green_heart: --- example/src/Examples/Graphs/Slider.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/example/src/Examples/Graphs/Slider.tsx b/example/src/Examples/Graphs/Slider.tsx index a07bc65d7f..76f398aa44 100644 --- a/example/src/Examples/Graphs/Slider.tsx +++ b/example/src/Examples/Graphs/Slider.tsx @@ -59,7 +59,7 @@ export const Slider: React.FC = ({ height, width }) => { size={12} x={() => touchPos.value.x - 24} y={() => touchPos.value.y - 18} - value={() => "$ " + (touchPos.value.y * -1).toFixed(2)} + text={() => "$ " + (touchPos.value.y * -1).toFixed(2)} /> vec(touchPos.value.x, touchPos.value.y + 14)} From b0b3189101a72447a7cf71b6554220e59f8233ff Mon Sep 17 00:00:00 2001 From: William Candillon Date: Fri, 4 Feb 2022 09:29:26 +0100 Subject: [PATCH 08/28] Implement getGlyphIDs --- example/src/Examples/Matrix/Matrix.tsx | 2 ++ example/src/Examples/Matrix/Symbol.tsx | 14 +++++---- package/cpp/api/JsiSkFont.h | 40 +++++++++++++++++++++++++- package/cpp/api/JsiSkRuntimeEffect.h | 5 +++- package/src/skia/Font/Font.ts | 21 ++++++++++++++ 5 files changed, 75 insertions(+), 7 deletions(-) diff --git a/example/src/Examples/Matrix/Matrix.tsx b/example/src/Examples/Matrix/Matrix.tsx index 22e99a4a52..5b4b014544 100644 --- a/example/src/Examples/Matrix/Matrix.tsx +++ b/example/src/Examples/Matrix/Matrix.tsx @@ -36,6 +36,7 @@ export const Matrix = () => { if (font === null) { return null; } + const symbols = font.getGlyphIDs("abcdefghijklmnopqrstuvwxyz"); return ( @@ -45,6 +46,7 @@ export const Matrix = () => { {cols.map((_i, i) => rows.map((_j, j) => ( c.codePointAt(0)!); interface SymbolProps { i: number; @@ -21,9 +17,17 @@ interface SymbolProps { timestamp: AnimationValue; stream: number[]; font: IFont; + symbols: number[]; } -export const Symbol = ({ i, j, timestamp, stream, font }: SymbolProps) => { +export const Symbol = ({ + i, + j, + timestamp, + stream, + font, + symbols, +}: SymbolProps) => { const offset = useRef(Math.round(Math.random() * (symbols.length - 1))); const range = useRef(100 + Math.random() * 900); const x = i * SYMBOL.width; diff --git a/package/cpp/api/JsiSkFont.h b/package/cpp/api/JsiSkFont.h index bc34e67a21..3604b93b53 100644 --- a/package/cpp/api/JsiSkFont.h +++ b/package/cpp/api/JsiSkFont.h @@ -11,6 +11,7 @@ #pragma clang diagnostic ignored "-Wdocumentation" #include +#include #pragma clang diagnostic pop @@ -46,9 +47,46 @@ class JsiSkFont : public JsiSkWrappingSharedPtrHostObject { return JsiSkRect::toValue(runtime, getContext(), rect); } + JSI_HOST_FUNCTION(getMetrics) { + SkFontMetrics fm; + getObject()->getMetrics(&fm); + auto metrics = jsi::Object(runtime); + metrics.setProperty(runtime, "ascent", fm.fAscent); + metrics.setProperty(runtime, "descent", fm.fDescent); + metrics.setProperty(runtime, "leading", fm.fLeading); + if (!(fm.fFlags & SkFontMetrics::kBoundsInvalid_Flag)) { + const float rect[] = { + fm.fXMin, fm.fTop, fm.fXMax, fm.fBottom + }; + auto bounds = SkRect::MakeLTRB(fm.fXMin,fm.fTop, fm.fXMax, fm.fBottom ); + auto jsiBounds = JsiSkRect::toValue(runtime, getContext(), bounds); + metrics.setProperty(runtime, "bounds", jsiBounds); + } + return metrics; + } + + JSI_HOST_FUNCTION(getGlyphIDs) { + auto str = arguments[0].asString(runtime).utf8(runtime); + auto numGlyphIDs = count > 1 && !arguments[1].isNull() && !arguments[1].isUndefined() + ? arguments[1].asNumber() : str.length(); + int bytesPerGlyph = 2; + auto glyphIDs = static_cast(malloc(numGlyphIDs * bytesPerGlyph)); + getObject()->textToGlyphs(str.c_str(), str.length(), SkTextEncoding::kUTF8, + glyphIDs, numGlyphIDs); + auto jsiGlyphIDs = jsi::Array(runtime, numGlyphIDs); + for (int i = 0; i < numGlyphIDs; i++) { + jsiGlyphIDs.setValueAtIndex(runtime, i, jsi::Value(static_cast(glyphIDs[i]))); + } + return jsiGlyphIDs; + } + JSI_EXPORT_PROPERTY_GETTERS(JSI_EXPORT_PROP_GET(JsiSkFont, size), JSI_EXPORT_PROP_GET(JsiSkFont, __typename__)) JSI_EXPORT_PROPERTY_SETTERS(JSI_EXPORT_PROP_SET(JsiSkFont, size)) - JSI_EXPORT_FUNCTIONS(JSI_EXPORT_FUNC(JsiSkFont, measureText)) + JSI_EXPORT_FUNCTIONS( + JSI_EXPORT_FUNC(JsiSkFont, measureText), + JSI_EXPORT_FUNC(JsiSkFont, getMetrics), + JSI_EXPORT_FUNC(JsiSkFont, getGlyphIDs) + ) JsiSkFont(std::shared_ptr context, const SkFont &font) : JsiSkWrappingSharedPtrHostObject(context, diff --git a/package/cpp/api/JsiSkRuntimeEffect.h b/package/cpp/api/JsiSkRuntimeEffect.h index 32d35895f2..158da26da3 100644 --- a/package/cpp/api/JsiSkRuntimeEffect.h +++ b/package/cpp/api/JsiSkRuntimeEffect.h @@ -122,7 +122,10 @@ namespace RNSkia // verify size of input uniforms if (jsiUniformsSize * sizeof(float) != getObject()->uniformSize()) { - std::string msg = "Uniforms size differs from effect's uniform size. Received " + std::to_string(jsiUniformsSize) + " expected " + std::to_string(getObject()->uniformSize() / sizeof(float)); + std::string msg = "Uniforms size differs from effect's uniform size. Received " + + std::to_string(jsiUniformsSize) + + " expected " + + std::to_string(getObject()->uniformSize() / sizeof(float)); jsi::detail::throwJSError(runtime, msg.c_str()); } diff --git a/package/src/skia/Font/Font.ts b/package/src/skia/Font/Font.ts index 08808cd76d..05f4e5daa7 100644 --- a/package/src/skia/Font/Font.ts +++ b/package/src/skia/Font/Font.ts @@ -2,6 +2,13 @@ import type { SkJSIInstance } from "../JsiInstance"; import type { IPaint } from "../Paint"; import type { IRect } from "../Rect"; +export interface FontMetrics { + ascent: number; // suggested space above the baseline. < 0 + descent: number; // suggested space below the baseline. > 0 + leading: number; // suggested spacing between descent of previous line and ascent of next line. + bounds?: IRect; // smallest rect containing all glyphs (relative to 0,0) +} + export interface IFont extends SkJSIInstance<"Font"> { /** Get/Sets text size in points. Has no effect if textSize is not greater than or equal to zero. @@ -19,6 +26,20 @@ export interface IFont extends SkJSIInstance<"Font"> { @return number of glyphs represented by text of length byteLength */ measureText: (text: string, paint?: IPaint) => IRect; + + /** + * Returns the FontMetrics for this font. + */ + getMetrics(): FontMetrics; + + /** + * Retrieves the glyph ids for each code point in the provided string. This call is passed to + * the typeface of this font. Note that glyph IDs are typeface-dependent; different faces + * may have different ids for the same code point. + * @param str + * @param numCodePoints - the number of code points in the string. Defaults to str.length. + */ + getGlyphIDs(str: string, numCodePoints?: number): number[]; } const fontStyle = ( From 83f72a02ef93264aa836edff0de08bc85cfea72c Mon Sep 17 00:00:00 2001 From: William Candillon Date: Fri, 4 Feb 2022 09:51:28 +0100 Subject: [PATCH 09/28] Fix documentation --- docs/docs/text/glyphs.md | 19 +++++++-------- .../src/renderer/components/text/Glyphs.tsx | 24 +++++++------------ 2 files changed, 17 insertions(+), 26 deletions(-) diff --git a/docs/docs/text/glyphs.md b/docs/docs/text/glyphs.md index 83d443a011..131e4a60b4 100644 --- a/docs/docs/text/glyphs.md +++ b/docs/docs/text/glyphs.md @@ -6,33 +6,30 @@ slug: /text/glyphs --- This component raws a run of glyphs, at corresponding positions, in a given font. -The font family and the font size must be specified. -The fonts available in the canvas are described in [here](/docs/text/fonts). | Name | Type | Description | |:------------|:-----------|:-----------------------------------------------------------------------| | glyphs | `Ghlyph[]` | Glyphs to draw | | x? | `number`. | x coordinate of the origin of the entire run. Default is 0 | | y? | `number`. | y coordinate of the origin of the entire run. Default is 0 | -| size? | `number` | Font size | -| familyName? | `string` | Font family name | -| font? | `font` | Custom font to use | +| font | `font` | Font to use | ## Draw text vertically ```tsx twoslash -import {Canvas, Glyphs, vec} from "@shopify/react-native-skia"; +import {Canvas, Glyphs, vec, useFont} from "@shopify/react-native-skia"; export const HelloWorld = () => { - const glyphs = "Hello World!" - .split("") - .map((c, i) => ({ id: c.codePointAt(0)!, pos: vec(0, i * 32) })); + const font = useFont(require("./my-font.otf"), 16); + if (font === null) { + return null; + } + const glyphs = font.getGlyphIDs("Hello World!").map((id, i) => ({ id, pos: vec(0, i*32) })); return ( diff --git a/package/src/renderer/components/text/Glyphs.tsx b/package/src/renderer/components/text/Glyphs.tsx index db11aaa6d0..d4f114a824 100644 --- a/package/src/renderer/components/text/Glyphs.tsx +++ b/package/src/renderer/components/text/Glyphs.tsx @@ -1,25 +1,20 @@ import React from "react"; -import type { - CustomPaintProps, - AnimatedProps, - FontDef, -} from "../../processors"; +import type { CustomPaintProps, AnimatedProps } from "../../processors"; import { useDrawing } from "../../nodes/Drawing"; -import type { IPoint } from "../../../skia"; -import { processFont } from "../../processors"; +import type { IPoint, IFont } from "../../../skia"; export interface Glyph { id: number; pos: IPoint; } -export type GlyphsProps = CustomPaintProps & - FontDef & { - x: number; - y: number; - glyphs: Glyph[]; - }; +export interface GlyphsProps extends CustomPaintProps { + x: number; + y: number; + glyphs: Glyph[]; + font: IFont; +} interface ProcessedGlyphs { glyphs: number[]; @@ -29,8 +24,7 @@ interface ProcessedGlyphs { export const Glyphs = (props: AnimatedProps) => { const onDraw = useDrawing( props, - ({ canvas, paint, fontMgr }, { glyphs: rawGlyphs, x, y, ...fontDef }) => { - const font = processFont(fontMgr, fontDef); + ({ canvas, paint }, { glyphs: rawGlyphs, x, y, font }) => { const { glyphs, positions } = rawGlyphs.reduce( (acc, glyph) => { const { id, pos } = glyph; From 744a624143105774c8f76d92a10641700e705c32 Mon Sep 17 00:00:00 2001 From: William Candillon Date: Fri, 4 Feb 2022 10:29:09 +0100 Subject: [PATCH 10/28] Implement more Font methods --- example/src/Examples/Matrix/Matrix.tsx | 1 + package/cpp/api/JsiSkFont.h | 63 +++++++++++++++++++++++--- package/cpp/api/JsiSkTypeface.h | 17 ++++++- package/src/skia/Font/Font.ts | 53 ++++++++++++++++++++-- 4 files changed, 123 insertions(+), 11 deletions(-) diff --git a/example/src/Examples/Matrix/Matrix.tsx b/example/src/Examples/Matrix/Matrix.tsx index 5b4b014544..de313950a0 100644 --- a/example/src/Examples/Matrix/Matrix.tsx +++ b/example/src/Examples/Matrix/Matrix.tsx @@ -4,6 +4,7 @@ import { Fill, Paint, useFont, + vec, } from "@shopify/react-native-skia"; import React from "react"; import { useTimestamp } from "@shopify/react-native-skia/src/animation/Animation/hooks"; diff --git a/package/cpp/api/JsiSkFont.h b/package/cpp/api/JsiSkFont.h index 3604b93b53..648989ae4d 100644 --- a/package/cpp/api/JsiSkFont.h +++ b/package/cpp/api/JsiSkFont.h @@ -6,6 +6,7 @@ #include "JsiSkPaint.h" #include "JsiSkRect.h" #include "JsiSkTypeface.h" +#include "JsiSkPoint.h" #pragma clang diagnostic push #pragma clang diagnostic ignored "-Wdocumentation" @@ -29,9 +30,6 @@ class JsiSkFont : public JsiSkWrappingSharedPtrHostObject { return jsi::String::createFromUtf8(runtime, "Font"); } - JSI_PROPERTY_GET(size) { return static_cast(getObject()->getSize()); } - JSI_PROPERTY_SET(size) { getObject()->setSize(value.asNumber()); } - JSI_HOST_FUNCTION(measureText) { auto textVal = arguments[0].asString(runtime).utf8(runtime); auto text = textVal.c_str(); @@ -80,12 +78,65 @@ class JsiSkFont : public JsiSkWrappingSharedPtrHostObject { return jsiGlyphIDs; } - JSI_EXPORT_PROPERTY_GETTERS(JSI_EXPORT_PROP_GET(JsiSkFont, size), JSI_EXPORT_PROP_GET(JsiSkFont, __typename__)) - JSI_EXPORT_PROPERTY_SETTERS(JSI_EXPORT_PROP_SET(JsiSkFont, size)) + JSI_HOST_FUNCTION(getGlyphIntercepts) { + auto jsiGlyphs = arguments[0].asObject(runtime).asArray(runtime); + auto jsiPositions = arguments[1].asObject(runtime).asArray(runtime); + auto top = arguments[2].asNumber(); + auto bottom = arguments[3].asNumber(); + std::vector positions; + int pointsSize = static_cast(jsiPositions.size(runtime)); + for (int i = 0; i < pointsSize; i++) { + std::shared_ptr point = JsiSkPoint::fromValue( + runtime, jsiPositions.getValueAtIndex(runtime, i).asObject(runtime)); + positions.push_back(*point.get()); + } + + std::vector glyphs; + int glyphsSize = static_cast(jsiPositions.size(runtime)); + for (int i = 0; i < glyphsSize; i++) { + glyphs.push_back(jsiGlyphs.getValueAtIndex(runtime, i).asNumber()); + } + + if (glyphs.size() > positions.size()) { + jsi::detail::throwJSError(runtime, "Not enough x,y position pairs for glyphs"); + return jsi::Value::null(); + } + auto sects = getObject()->getIntercepts(glyphs.data(), SkToInt(glyphs.size()), positions.data(), top, bottom); + auto jsiSects = jsi::Array(runtime, sects.size()); + for (int i = 0; i < sects.size(); i++) { + jsiSects.setValueAtIndex(runtime, i, jsi::Value(static_cast(sects.at(i)))); + } + return jsiSects; + } + + JSI_HOST_FUNCTION(getScaleX) { + return jsi::Value(SkScalarToDouble(getObject()->getScaleX())); + } + + JSI_HOST_FUNCTION(getSize) { + return jsi::Value(SkScalarToDouble(getObject()->getSize())); + } + + JSI_HOST_FUNCTION(getSkewX) { + return jsi::Value(SkScalarToDouble(getObject()->getSkewX())); + } + + JSI_HOST_FUNCTION(isEmbolden) { + return jsi::Value(getObject()->isEmbolden()); + } + + JSI_HOST_FUNCTION(getTypeface) { + return JsiSkTypeface::toValue(runtime, getContext(), sk_sp(getObject()->getTypeface())); + } + JSI_EXPORT_FUNCTIONS( JSI_EXPORT_FUNC(JsiSkFont, measureText), JSI_EXPORT_FUNC(JsiSkFont, getMetrics), - JSI_EXPORT_FUNC(JsiSkFont, getGlyphIDs) + JSI_EXPORT_FUNC(JsiSkFont, getGlyphIDs), + JSI_EXPORT_FUNC(JsiSkFont, getGlyphIntercepts), + JSI_EXPORT_FUNC(JsiSkFont, getScaleX), + JSI_EXPORT_FUNC(JsiSkFont, getSkewX), + JSI_EXPORT_FUNC(JsiSkFont, getTypeface), ) JsiSkFont(std::shared_ptr context, const SkFont &font) diff --git a/package/cpp/api/JsiSkTypeface.h b/package/cpp/api/JsiSkTypeface.h index ace6bf6273..8b3975465c 100644 --- a/package/cpp/api/JsiSkTypeface.h +++ b/package/cpp/api/JsiSkTypeface.h @@ -24,9 +24,14 @@ class JsiSkTypeface : public JsiSkWrappingSkPtrHostObject { public: JSI_PROPERTY_GET(bold) { return jsi::Value(getObject()->isBold()); } JSI_PROPERTY_GET(italic) { return jsi::Value(getObject()->isItalic()); } + // TODO: declare in JsiSkWrappingSkPtrHostObject via extra template parameter? + JSI_PROPERTY_GET(__typename__) { + return jsi::String::createFromUtf8(runtime, "Typeface"); + } JSI_EXPORT_PROPERTY_GETTERS(JSI_EXPORT_PROP_GET(JsiSkTypeface, bold), - JSI_EXPORT_PROP_GET(JsiSkTypeface, italic)) + JSI_EXPORT_PROP_GET(JsiSkTypeface, italic), + JSI_EXPORT_PROP_GET(JsiSkTypeface, __typename__)) JsiSkTypeface(std::shared_ptr context, const sk_sp typeface) @@ -43,6 +48,16 @@ class JsiSkTypeface : public JsiSkWrappingSkPtrHostObject { ->getObject(); } + /** + Returns the jsi object from a host object of this type + */ + static jsi::Value toValue(jsi::Runtime &runtime, + std::shared_ptr context, + const sk_sp tf) { + return jsi::Object::createFromHostObject( + runtime, std::make_shared(context, tf)); + } + private: static SkFontStyle getFontStyleFromNumber(int fontStyle) { switch (fontStyle) { diff --git a/package/src/skia/Font/Font.ts b/package/src/skia/Font/Font.ts index 05f4e5daa7..7d3c8b2744 100644 --- a/package/src/skia/Font/Font.ts +++ b/package/src/skia/Font/Font.ts @@ -1,6 +1,8 @@ import type { SkJSIInstance } from "../JsiInstance"; import type { IPaint } from "../Paint"; import type { IRect } from "../Rect"; +import type { IPoint } from "../Point"; +import type { Typeface } from "../Typeface/Typeface"; export interface FontMetrics { ascent: number; // suggested space above the baseline. < 0 @@ -10,10 +12,6 @@ export interface FontMetrics { } export interface IFont extends SkJSIInstance<"Font"> { - /** Get/Sets text size in points. - Has no effect if textSize is not greater than or equal to zero. - */ - size: number; /** Returns the advance width of text. The advance is the normal distance to move before drawing additional text. Returns the bounding box of text if bounds is not nullptr. The paint @@ -40,6 +38,53 @@ export interface IFont extends SkJSIInstance<"Font"> { * @param numCodePoints - the number of code points in the string. Defaults to str.length. */ getGlyphIDs(str: string, numCodePoints?: number): number[]; + + /** + * Computes any intersections of a thick "line" and a run of positionsed glyphs. + * The thick line is represented as a top and bottom coordinate (positive for + * below the baseline, negative for above). If there are no intersections + * (e.g. if this is intended as an underline, and there are no "collisions") + * then the returned array will be empty. If there are intersections, the array + * will contain pairs of X coordinates [start, end] for each segment that + * intersected with a glyph. + * + * @param glyphs the glyphs to intersect with + * @param positions x,y coordinates (2 per glyph) for each glyph + * @param top top of the thick "line" to use for intersection testing + * @param bottom bottom of the thick "line" to use for intersection testing + * @return array of [start, end] x-coordinate pairs. Maybe be empty. + */ + getGlyphIntercepts( + glyphs: number[], + positions: IPoint[], + top: number, + bottom: number + ): number[]; + + /** + * Returns text scale on x-axis. Default value is 1. + */ + getScaleX(): number; + + /** + * Returns text size in points. + */ + getSize(): number; + + /** + * Returns text skew on x-axis. Default value is zero. + */ + getSkewX(): number; + + /** + * Returns embolden effect for this font. Default value is false. + */ + isEmbolden(): boolean; + + /** + * Returns the Typeface set for this font. + */ + getTypeface(): Typeface | null; } const fontStyle = ( From 3763b7054c87344edc5611278caa8767c9d28519 Mon Sep 17 00:00:00 2001 From: William Candillon Date: Fri, 4 Feb 2022 10:30:39 +0100 Subject: [PATCH 11/28] Rename Typeface to ITypeface and add JsiInstance extension --- example/src/Examples/Matrix/Matrix.tsx | 1 - package/src/skia/Font/Font.ts | 4 ++-- package/src/skia/FontMgr/FontMgr.ts | 4 ++-- package/src/skia/Skia.ts | 4 ++-- package/src/skia/Typeface/Typeface.ts | 4 +++- package/src/skia/Typeface/TypefaceFactory.ts | 4 ++-- 6 files changed, 11 insertions(+), 10 deletions(-) diff --git a/example/src/Examples/Matrix/Matrix.tsx b/example/src/Examples/Matrix/Matrix.tsx index de313950a0..5b4b014544 100644 --- a/example/src/Examples/Matrix/Matrix.tsx +++ b/example/src/Examples/Matrix/Matrix.tsx @@ -4,7 +4,6 @@ import { Fill, Paint, useFont, - vec, } from "@shopify/react-native-skia"; import React from "react"; import { useTimestamp } from "@shopify/react-native-skia/src/animation/Animation/hooks"; diff --git a/package/src/skia/Font/Font.ts b/package/src/skia/Font/Font.ts index 7d3c8b2744..754ec393af 100644 --- a/package/src/skia/Font/Font.ts +++ b/package/src/skia/Font/Font.ts @@ -2,7 +2,7 @@ import type { SkJSIInstance } from "../JsiInstance"; import type { IPaint } from "../Paint"; import type { IRect } from "../Rect"; import type { IPoint } from "../Point"; -import type { Typeface } from "../Typeface/Typeface"; +import type { ITypeface } from "../Typeface/Typeface"; export interface FontMetrics { ascent: number; // suggested space above the baseline. < 0 @@ -84,7 +84,7 @@ export interface IFont extends SkJSIInstance<"Font"> { /** * Returns the Typeface set for this font. */ - getTypeface(): Typeface | null; + getTypeface(): ITypeface | null; } const fontStyle = ( diff --git a/package/src/skia/FontMgr/FontMgr.ts b/package/src/skia/FontMgr/FontMgr.ts index a2a8019fd9..053faef768 100644 --- a/package/src/skia/FontMgr/FontMgr.ts +++ b/package/src/skia/FontMgr/FontMgr.ts @@ -1,5 +1,5 @@ import type { SkJSIInstance } from "../JsiInstance"; -import type { Typeface } from "../Typeface/Typeface"; +import type { ITypeface } from "../Typeface/Typeface"; import type { FontStyle } from "../Font/Font"; export interface FontMgr extends SkJSIInstance<"FontMgr"> { @@ -17,5 +17,5 @@ export interface FontMgr extends SkJSIInstance<"FontMgr"> { /** * Find the closest matching typeface to the specified familyName and style and return a ref to it. */ - matchFamilyStyle(familyName: string, fontStyle?: FontStyle): Typeface | null; + matchFamilyStyle(familyName: string, fontStyle?: FontStyle): ITypeface | null; } diff --git a/package/src/skia/Skia.ts b/package/src/skia/Skia.ts index f4c6133519..37d9740139 100644 --- a/package/src/skia/Skia.ts +++ b/package/src/skia/Skia.ts @@ -3,7 +3,7 @@ import type { ImageFilterFactory } from "./ImageFilter"; import type { PathFactory } from "./Path"; import type { ColorFilterFactory } from "./ColorFilter"; import type { IFont } from "./Font"; -import type { Typeface, TypefaceFactory } from "./Typeface"; +import type { ITypeface, TypefaceFactory } from "./Typeface"; import type { ImageFactory } from "./Image"; import type { MaskFilterFactory } from "./MaskFilter"; import type { IPaint } from "./Paint"; @@ -33,7 +33,7 @@ export interface Skia { Path: PathFactory; Matrix: () => Matrix; ColorFilter: ColorFilterFactory; - Font: (typeface?: Typeface, size?: number) => IFont; + Font: (typeface?: ITypeface, size?: number) => IFont; Typeface: TypefaceFactory; MaskFilter: MaskFilterFactory; RuntimeEffect: RuntimeEffectFactory; diff --git a/package/src/skia/Typeface/Typeface.ts b/package/src/skia/Typeface/Typeface.ts index d4a682c253..a741f15b6c 100644 --- a/package/src/skia/Typeface/Typeface.ts +++ b/package/src/skia/Typeface/Typeface.ts @@ -1,4 +1,6 @@ -export interface Typeface { +import type { SkJSIInstance } from "../JsiInstance"; + +export interface ITypeface extends SkJSIInstance<"TypeFace"> { readonly bold: boolean; readonly italic: boolean; } diff --git a/package/src/skia/Typeface/TypefaceFactory.ts b/package/src/skia/Typeface/TypefaceFactory.ts index abde31a9a7..8a93d7985e 100644 --- a/package/src/skia/Typeface/TypefaceFactory.ts +++ b/package/src/skia/Typeface/TypefaceFactory.ts @@ -1,7 +1,7 @@ import type { Data } from "../Data/Data"; -import type { Typeface } from "./Typeface"; +import type { ITypeface } from "./Typeface"; export interface TypefaceFactory { - MakeFreeTypeFaceFromData(data: Data): Typeface | null; + MakeFreeTypeFaceFromData(data: Data): ITypeface | null; } From b2a14cd5170bd933b5780f335ed7470664e04772 Mon Sep 17 00:00:00 2001 From: William Candillon Date: Fri, 4 Feb 2022 10:53:00 +0100 Subject: [PATCH 12/28] Implement all Font methods from CanvasKit --- package/cpp/api/JsiSkFont.h | 70 +++++++++++++++++++++++++++++++ package/src/skia/Font/Font.ts | 77 +++++++++++++++++++++++++++++++++++ 2 files changed, 147 insertions(+) diff --git a/package/cpp/api/JsiSkFont.h b/package/cpp/api/JsiSkFont.h index 648989ae4d..8b00ba2dad 100644 --- a/package/cpp/api/JsiSkFont.h +++ b/package/cpp/api/JsiSkFont.h @@ -129,6 +129,66 @@ class JsiSkFont : public JsiSkWrappingSharedPtrHostObject { return JsiSkTypeface::toValue(runtime, getContext(), sk_sp(getObject()->getTypeface())); } + JSI_HOST_FUNCTION(setEdging) { + auto edging = arguments[0].asNumber(); + getObject()->setEdging(static_cast(edging)); + return jsi::Value::undefined(); + } + + JSI_HOST_FUNCTION(embeddedBitmaps) { + auto embeddedBitmaps = arguments[0].getBool(); + getObject()->setEmbeddedBitmaps(embeddedBitmaps); + return jsi::Value::undefined(); + } + + JSI_HOST_FUNCTION(setHinting) { + auto hinting = arguments[0].asNumber(); + getObject()->setHinting(static_cast(hinting)); + return jsi::Value::undefined(); + } + + JSI_HOST_FUNCTION(setLinearMetrics) { + auto linearMetrics = arguments[0].getBool(); + getObject()->setLinearMetrics(linearMetrics); + return jsi::Value::undefined(); + } + + JSI_HOST_FUNCTION(setScaleX) { + auto scaleX = arguments[0].asNumber(); + getObject()->setScaleX(scaleX); + return jsi::Value::undefined(); + } + + JSI_HOST_FUNCTION(setSkewX) { + auto skewX = arguments[0].asNumber(); + getObject()->setSkewX(skewX); + return jsi::Value::undefined(); + } + + JSI_HOST_FUNCTION(setSize) { + auto size = arguments[0].asNumber(); + getObject()->setSize(size); + return jsi::Value::undefined(); + } + + JSI_HOST_FUNCTION(setEmbolden) { + auto embolden = arguments[0].asNumber(); + getObject()->setEmbolden(embolden); + return jsi::Value::undefined(); + } + + JSI_HOST_FUNCTION(setSubpixel) { + auto subpixel = arguments[0].asNumber(); + getObject()->setSubpixel(subpixel); + return jsi::Value::undefined(); + } + + JSI_HOST_FUNCTION(setTypeface) { + auto typeface = arguments[0].isNull() ? nullptr : JsiSkTypeface::fromValue(runtime, arguments[0]); + getObject()->setTypeface(typeface); + return jsi::Value::undefined(); + } + JSI_EXPORT_FUNCTIONS( JSI_EXPORT_FUNC(JsiSkFont, measureText), JSI_EXPORT_FUNC(JsiSkFont, getMetrics), @@ -137,6 +197,16 @@ class JsiSkFont : public JsiSkWrappingSharedPtrHostObject { JSI_EXPORT_FUNC(JsiSkFont, getScaleX), JSI_EXPORT_FUNC(JsiSkFont, getSkewX), JSI_EXPORT_FUNC(JsiSkFont, getTypeface), + JSI_EXPORT_FUNC(JsiSkFont, setEdging), + JSI_EXPORT_FUNC(JsiSkFont, embeddedBitmaps), + JSI_EXPORT_FUNC(JsiSkFont, setHinting), + JSI_EXPORT_FUNC(JsiSkFont, setLinearMetrics), + JSI_EXPORT_FUNC(JsiSkFont, setScaleX), + JSI_EXPORT_FUNC(JsiSkFont, setSkewX), + JSI_EXPORT_FUNC(JsiSkFont, setSize), + JSI_EXPORT_FUNC(JsiSkFont, setEmbolden), + JSI_EXPORT_FUNC(JsiSkFont, setSubpixel), + JSI_EXPORT_FUNC(JsiSkFont, setTypeface), ) JsiSkFont(std::shared_ptr context, const SkFont &font) diff --git a/package/src/skia/Font/Font.ts b/package/src/skia/Font/Font.ts index 754ec393af..2922ebe732 100644 --- a/package/src/skia/Font/Font.ts +++ b/package/src/skia/Font/Font.ts @@ -85,6 +85,70 @@ export interface IFont extends SkJSIInstance<"Font"> { * Returns the Typeface set for this font. */ getTypeface(): ITypeface | null; + + /** + * Requests, but does not require, that edge pixels draw opaque or with partial transparency. + * @param edging + */ + setEdging(edging: FontEdging): void; + + /** + * Requests, but does not require, to use bitmaps in fonts instead of outlines. + * @param embeddedBitmaps + */ + setEmbeddedBitmaps(embeddedBitmaps: boolean): void; + + /** + * Sets level of glyph outline adjustment. + * @param hinting + */ + setHinting(hinting: FontHinting): void; + + /** + * Requests, but does not require, linearly scalable font and glyph metrics. + * + * For outline fonts 'true' means font and glyph metrics should ignore hinting and rounding. + * Note that some bitmap formats may not be able to scale linearly and will ignore this flag. + * @param linearMetrics + */ + setLinearMetrics(linearMetrics: boolean): void; + + /** + * Sets the text scale on the x-axis. + * @param sx + */ + setScaleX(sx: number): void; + + /** + * Sets the text size in points on this font. + * @param points + */ + setSize(points: number): void; + + /** + * Sets the text-skew on the x axis for this font. + * @param sx + */ + setSkewX(sx: number): void; + + /** + * Set embolden effect for this font. + * @param embolden + */ + setEmbolden(embolden: boolean): void; + + /** + * Requests, but does not require, that glyphs respect sub-pixel positioning. + * @param subpixel + */ + setSubpixel(subpixel: boolean): void; + + /** + * Sets the typeface to use with this font. null means to clear the typeface and use the + * default one. + * @param face + */ + setTypeface(face: ITypeface | null): void; } const fontStyle = ( @@ -131,6 +195,19 @@ export enum FontSlant { Oblique, } +export enum FontEdging { + Alias, + AntiAlias, + SubpixelAntiAlias, +} + +export enum FontHinting { + None, + Slight, + Normal, + Full, +} + export const FontStyle = { Normal: fontStyle(FontWeight.Normal, FontWidth.Normal, FontSlant.Upright), Bold: fontStyle(FontWeight.Bold, FontWidth.Normal, FontSlant.Upright), From 4aed713d835455ce8891625a8abad539ee599deb Mon Sep 17 00:00:00 2001 From: William Candillon Date: Fri, 4 Feb 2022 11:37:01 +0100 Subject: [PATCH 13/28] Add support for RSXform (#180) --- docs/docs/text/glyphs.md | 4 +- example/src/Examples/Matrix/Matrix.tsx | 1 + package/cpp/api/JsiSkApi.h | 2 + package/cpp/api/JsiSkFont.h | 70 +++++++++++++++++ package/cpp/api/JsiSkPoint.h | 1 + package/cpp/api/JsiSkRSXform.h | 103 +++++++++++++++++++++++++ package/src/skia/Font/Font.ts | 77 ++++++++++++++++++ package/src/skia/RSXform.ts | 3 + package/src/skia/Skia.ts | 6 +- 9 files changed, 265 insertions(+), 2 deletions(-) create mode 100644 package/cpp/api/JsiSkRSXform.h create mode 100644 package/src/skia/RSXform.ts diff --git a/docs/docs/text/glyphs.md b/docs/docs/text/glyphs.md index 131e4a60b4..9e45231cc3 100644 --- a/docs/docs/text/glyphs.md +++ b/docs/docs/text/glyphs.md @@ -25,7 +25,9 @@ export const HelloWorld = () => { if (font === null) { return null; } - const glyphs = font.getGlyphIDs("Hello World!").map((id, i) => ({ id, pos: vec(0, i*32) })); + const glyphs = font + .getGlyphIDs("Hello World!") + .map((id, i) => ({ id, pos: vec(0, i*32) })); return ( { return JsiSkTypeface::toValue(runtime, getContext(), sk_sp(getObject()->getTypeface())); } + JSI_HOST_FUNCTION(setEdging) { + auto edging = arguments[0].asNumber(); + getObject()->setEdging(static_cast(edging)); + return jsi::Value::undefined(); + } + + JSI_HOST_FUNCTION(embeddedBitmaps) { + auto embeddedBitmaps = arguments[0].getBool(); + getObject()->setEmbeddedBitmaps(embeddedBitmaps); + return jsi::Value::undefined(); + } + + JSI_HOST_FUNCTION(setHinting) { + auto hinting = arguments[0].asNumber(); + getObject()->setHinting(static_cast(hinting)); + return jsi::Value::undefined(); + } + + JSI_HOST_FUNCTION(setLinearMetrics) { + auto linearMetrics = arguments[0].getBool(); + getObject()->setLinearMetrics(linearMetrics); + return jsi::Value::undefined(); + } + + JSI_HOST_FUNCTION(setScaleX) { + auto scaleX = arguments[0].asNumber(); + getObject()->setScaleX(scaleX); + return jsi::Value::undefined(); + } + + JSI_HOST_FUNCTION(setSkewX) { + auto skewX = arguments[0].asNumber(); + getObject()->setSkewX(skewX); + return jsi::Value::undefined(); + } + + JSI_HOST_FUNCTION(setSize) { + auto size = arguments[0].asNumber(); + getObject()->setSize(size); + return jsi::Value::undefined(); + } + + JSI_HOST_FUNCTION(setEmbolden) { + auto embolden = arguments[0].asNumber(); + getObject()->setEmbolden(embolden); + return jsi::Value::undefined(); + } + + JSI_HOST_FUNCTION(setSubpixel) { + auto subpixel = arguments[0].asNumber(); + getObject()->setSubpixel(subpixel); + return jsi::Value::undefined(); + } + + JSI_HOST_FUNCTION(setTypeface) { + auto typeface = arguments[0].isNull() ? nullptr : JsiSkTypeface::fromValue(runtime, arguments[0]); + getObject()->setTypeface(typeface); + return jsi::Value::undefined(); + } + JSI_EXPORT_FUNCTIONS( JSI_EXPORT_FUNC(JsiSkFont, measureText), JSI_EXPORT_FUNC(JsiSkFont, getMetrics), @@ -137,6 +197,16 @@ class JsiSkFont : public JsiSkWrappingSharedPtrHostObject { JSI_EXPORT_FUNC(JsiSkFont, getScaleX), JSI_EXPORT_FUNC(JsiSkFont, getSkewX), JSI_EXPORT_FUNC(JsiSkFont, getTypeface), + JSI_EXPORT_FUNC(JsiSkFont, setEdging), + JSI_EXPORT_FUNC(JsiSkFont, embeddedBitmaps), + JSI_EXPORT_FUNC(JsiSkFont, setHinting), + JSI_EXPORT_FUNC(JsiSkFont, setLinearMetrics), + JSI_EXPORT_FUNC(JsiSkFont, setScaleX), + JSI_EXPORT_FUNC(JsiSkFont, setSkewX), + JSI_EXPORT_FUNC(JsiSkFont, setSize), + JSI_EXPORT_FUNC(JsiSkFont, setEmbolden), + JSI_EXPORT_FUNC(JsiSkFont, setSubpixel), + JSI_EXPORT_FUNC(JsiSkFont, setTypeface), ) JsiSkFont(std::shared_ptr context, const SkFont &font) diff --git a/package/cpp/api/JsiSkPoint.h b/package/cpp/api/JsiSkPoint.h index 78b4742179..32f6d111bb 100644 --- a/package/cpp/api/JsiSkPoint.h +++ b/package/cpp/api/JsiSkPoint.h @@ -15,6 +15,7 @@ using namespace facebook; class JsiSkPoint : public JsiSkWrappingSharedPtrHostObject { public: + JSI_PROPERTY_GET(x) { return static_cast(getObject()->x()); } JSI_PROPERTY_GET(y) { return static_cast(getObject()->y()); } diff --git a/package/cpp/api/JsiSkRSXform.h b/package/cpp/api/JsiSkRSXform.h new file mode 100644 index 0000000000..132fecdda5 --- /dev/null +++ b/package/cpp/api/JsiSkRSXform.h @@ -0,0 +1,103 @@ +#pragma once + +#include "JsiSkHostObjects.h" + +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wdocumentation" + +#include + +#pragma clang diagnostic pop + +namespace RNSkia { + + using namespace facebook; + + class JsiSkRSXform : public JsiSkWrappingSharedPtrHostObject { + public: + JsiSkRSXform(std::shared_ptr context, const SkRSXform &rsxform) + : JsiSkWrappingSharedPtrHostObject( + context, std::make_shared(rsxform)){}; + + // TODO: declare in JsiSkWrappingSkPtrHostObject via extra template parameter? + + JSI_PROPERTY_GET(__typename__) { + return jsi::String::createFromUtf8(runtime, "RSXform"); + } + + JSI_PROPERTY_GET(scos) { + return jsi::Value(SkScalarToDouble(getObject()->fSCos)); + } + JSI_PROPERTY_GET(ssin) { + return jsi::Value(SkScalarToDouble(getObject()->fSSin)); + } + JSI_PROPERTY_GET(tx) { + return jsi::Value(SkScalarToDouble(getObject()->fTx)); + } + JSI_PROPERTY_GET(ty) { + return jsi::Value(SkScalarToDouble(getObject()->fTy)); + } + + JSI_EXPORT_PROPERTY_GETTERS( + JSI_EXPORT_PROP_GET(JsiSkRSXform, __typename__), + JSI_EXPORT_PROP_GET(JsiSkRSXform, scos), + JSI_EXPORT_PROP_GET(JsiSkRSXform, ssin), + JSI_EXPORT_PROP_GET(JsiSkRSXform, tx), + JSI_EXPORT_PROP_GET(JsiSkRSXform, ty), + ) + + /** + Returns the underlying object from a host object of this type + */ + static std::shared_ptr fromValue(jsi::Runtime &runtime, + const jsi::Value &obj) { + const auto object = obj.asObject(runtime); + if (object.isHostObject(runtime)) { + return object + .asHostObject(runtime) + .get() + ->getObject(); + } else if(object.isArray(runtime)) { + auto scos = object.getArray(runtime).getValueAtIndex(runtime, 0).asNumber(); + auto ssin = object.getArray(runtime).getValueAtIndex(runtime, 1).asNumber(); + auto tx = object.getArray(runtime).getValueAtIndex(runtime, 2).asNumber(); + auto ty = object.getArray(runtime).getValueAtIndex(runtime, 3).asNumber(); + return std::make_shared(SkRSXform::Make(scos, ssin, tx, ty)); + } + jsi::detail::throwJSError(runtime, "Invalid RSXform"); + return jsi::Value::undefined(); + } + + /** + Returns the jsi object from a host object of this type + */ + static jsi::Value toValue(jsi::Runtime &runtime, + std::shared_ptr context, + const SkRSXform &rsxform) { + return jsi::Object::createFromHostObject( + runtime, std::make_shared(context, rsxform)); + } + + /** + * Creates the function for construction a new instance of the SkPoint + * wrapper + * @param context platform context + * @return A function for creating a new host object wrapper for the SkPoint + * class + */ + static const jsi::HostFunctionType + createCtor(std::shared_ptr context) { + return JSI_HOST_FUNCTION_LAMBDA { + auto rsxform = SkRSXform::Make( + arguments[0].asNumber(), + arguments[1].asNumber(), + arguments[2].asNumber(), + arguments[3].asNumber() + ); + // Return the newly constructed object + return jsi::Object::createFromHostObject( + runtime, std::make_shared(context, rsxform)); + }; + } + }; +} // namespace RNSkia diff --git a/package/src/skia/Font/Font.ts b/package/src/skia/Font/Font.ts index 754ec393af..2922ebe732 100644 --- a/package/src/skia/Font/Font.ts +++ b/package/src/skia/Font/Font.ts @@ -85,6 +85,70 @@ export interface IFont extends SkJSIInstance<"Font"> { * Returns the Typeface set for this font. */ getTypeface(): ITypeface | null; + + /** + * Requests, but does not require, that edge pixels draw opaque or with partial transparency. + * @param edging + */ + setEdging(edging: FontEdging): void; + + /** + * Requests, but does not require, to use bitmaps in fonts instead of outlines. + * @param embeddedBitmaps + */ + setEmbeddedBitmaps(embeddedBitmaps: boolean): void; + + /** + * Sets level of glyph outline adjustment. + * @param hinting + */ + setHinting(hinting: FontHinting): void; + + /** + * Requests, but does not require, linearly scalable font and glyph metrics. + * + * For outline fonts 'true' means font and glyph metrics should ignore hinting and rounding. + * Note that some bitmap formats may not be able to scale linearly and will ignore this flag. + * @param linearMetrics + */ + setLinearMetrics(linearMetrics: boolean): void; + + /** + * Sets the text scale on the x-axis. + * @param sx + */ + setScaleX(sx: number): void; + + /** + * Sets the text size in points on this font. + * @param points + */ + setSize(points: number): void; + + /** + * Sets the text-skew on the x axis for this font. + * @param sx + */ + setSkewX(sx: number): void; + + /** + * Set embolden effect for this font. + * @param embolden + */ + setEmbolden(embolden: boolean): void; + + /** + * Requests, but does not require, that glyphs respect sub-pixel positioning. + * @param subpixel + */ + setSubpixel(subpixel: boolean): void; + + /** + * Sets the typeface to use with this font. null means to clear the typeface and use the + * default one. + * @param face + */ + setTypeface(face: ITypeface | null): void; } const fontStyle = ( @@ -131,6 +195,19 @@ export enum FontSlant { Oblique, } +export enum FontEdging { + Alias, + AntiAlias, + SubpixelAntiAlias, +} + +export enum FontHinting { + None, + Slight, + Normal, + Full, +} + export const FontStyle = { Normal: fontStyle(FontWeight.Normal, FontWidth.Normal, FontSlant.Upright), Bold: fontStyle(FontWeight.Bold, FontWidth.Normal, FontSlant.Upright), diff --git a/package/src/skia/RSXform.ts b/package/src/skia/RSXform.ts new file mode 100644 index 0000000000..865bc1a66b --- /dev/null +++ b/package/src/skia/RSXform.ts @@ -0,0 +1,3 @@ +import type { SkJSIInstance } from "./JsiInstance"; + +export type IRSXform = SkJSIInstance<"RSXform">; diff --git a/package/src/skia/Skia.ts b/package/src/skia/Skia.ts index 37d9740139..ced12cbb37 100644 --- a/package/src/skia/Skia.ts +++ b/package/src/skia/Skia.ts @@ -21,6 +21,7 @@ import type { SVGFactory } from "./SVG"; import type { FontMgrFactory } from "./FontMgr/FontMgrFactory"; import type { SurfaceFactory } from "./Surface"; import "./NativeSetup"; +import type { IRSXform } from "./RSXform"; /** * Declares the interface for the native Skia API @@ -29,6 +30,7 @@ export interface Skia { Point: (x: number, y: number) => IPoint; XYWHRect: (x: number, y: number, width: number, height: number) => IRect; RRectXY: (rect: IRect, rx: number, ry: number) => IRRect; + RSXform: (scos: number, ssin: number, tx: number, ty: number) => IRSXform; Paint: () => IPaint; Path: PathFactory; Matrix: () => Matrix; @@ -94,8 +96,10 @@ export const Skia = { MakeVertices: SkiaApi.MakeVertices, SVG: SkiaApi.SVG, FontMgr: SkiaApi.FontMgr, + // Here are constructors for data types which are represented as typed arrays in CanvasKit Color, - // Here symmetry is broken to be comptatible with CanvasKit + RSXform: SkiaApi.RSXform, + // Here the factory symmetry is broken to be comptatible with CanvasKit MakeSurface: SkiaApi.Surface.Make, MakeImageFromEncoded: SkiaApi.Image.MakeImageFromEncoded, }; From ec51b4b2316a353642f496a42233f4610cd7e4c9 Mon Sep 17 00:00:00 2001 From: William Candillon Date: Fri, 4 Feb 2022 13:09:53 +0100 Subject: [PATCH 14/28] Update JsiSkRSXform.h --- package/cpp/api/JsiSkRSXform.h | 1 - 1 file changed, 1 deletion(-) diff --git a/package/cpp/api/JsiSkRSXform.h b/package/cpp/api/JsiSkRSXform.h index 132fecdda5..8908e3dda1 100644 --- a/package/cpp/api/JsiSkRSXform.h +++ b/package/cpp/api/JsiSkRSXform.h @@ -65,7 +65,6 @@ namespace RNSkia { return std::make_shared(SkRSXform::Make(scos, ssin, tx, ty)); } jsi::detail::throwJSError(runtime, "Invalid RSXform"); - return jsi::Value::undefined(); } /** From 6df80dbd4ed7b089bf98f44021955a0d364235d8 Mon Sep 17 00:00:00 2001 From: William Candillon Date: Fri, 4 Feb 2022 13:11:53 +0100 Subject: [PATCH 15/28] Update JsiSkRSXform.h --- package/cpp/api/JsiSkRSXform.h | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/package/cpp/api/JsiSkRSXform.h b/package/cpp/api/JsiSkRSXform.h index 8908e3dda1..2758ae670b 100644 --- a/package/cpp/api/JsiSkRSXform.h +++ b/package/cpp/api/JsiSkRSXform.h @@ -57,14 +57,13 @@ namespace RNSkia { .asHostObject(runtime) .get() ->getObject(); - } else if(object.isArray(runtime)) { + } else { auto scos = object.getArray(runtime).getValueAtIndex(runtime, 0).asNumber(); auto ssin = object.getArray(runtime).getValueAtIndex(runtime, 1).asNumber(); auto tx = object.getArray(runtime).getValueAtIndex(runtime, 2).asNumber(); auto ty = object.getArray(runtime).getValueAtIndex(runtime, 3).asNumber(); return std::make_shared(SkRSXform::Make(scos, ssin, tx, ty)); } - jsi::detail::throwJSError(runtime, "Invalid RSXform"); } /** From 772d6419660db2fc190051664fd832ab0acf77a2 Mon Sep 17 00:00:00 2001 From: William Candillon Date: Fri, 4 Feb 2022 13:30:11 +0100 Subject: [PATCH 16/28] Update JsiSkCanvas.h --- package/cpp/api/JsiSkCanvas.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package/cpp/api/JsiSkCanvas.h b/package/cpp/api/JsiSkCanvas.h index 9bb39eaf95..01e27fec63 100644 --- a/package/cpp/api/JsiSkCanvas.h +++ b/package/cpp/api/JsiSkCanvas.h @@ -332,7 +332,7 @@ class JsiSkCanvas : public JsiSkHostObject { } std::vector glyphs; - int glyphsSize = static_cast(jsiPositions.size(runtime)); + int glyphsSize = static_cast(jsiGlyphs.size(runtime)); for (int i = 0; i < glyphsSize; i++) { glyphs.push_back(jsiGlyphs.getValueAtIndex(runtime, i).asNumber()); } From 6dd820721d9f5cc7e6a9346ec20289a43b1a19e6 Mon Sep 17 00:00:00 2001 From: William Candillon Date: Fri, 4 Feb 2022 14:02:15 +0100 Subject: [PATCH 17/28] Add Text Blob support --- example/src/Examples/Matrix/Matrix.tsx | 1 - package/cpp/api/JsiSkApi.h | 2 + package/cpp/api/JsiSkCanvas.h | 11 +++ package/cpp/api/JsiSkFont.h | 2 +- package/cpp/api/JsiSkRSXform.h | 4 +- package/cpp/api/JsiSkTextBlob.h | 47 +++++++++++++ package/cpp/api/JsiSkTextBlobFactory.h | 94 ++++++++++++++++++++++++++ package/src/skia/Canvas.ts | 11 +++ package/src/skia/Skia.ts | 5 +- package/src/skia/TextBlob.ts | 52 ++++++++++++++ 10 files changed, 223 insertions(+), 6 deletions(-) create mode 100644 package/cpp/api/JsiSkTextBlob.h create mode 100644 package/cpp/api/JsiSkTextBlobFactory.h create mode 100644 package/src/skia/TextBlob.ts diff --git a/example/src/Examples/Matrix/Matrix.tsx b/example/src/Examples/Matrix/Matrix.tsx index 764c3caf5d..5b4b014544 100644 --- a/example/src/Examples/Matrix/Matrix.tsx +++ b/example/src/Examples/Matrix/Matrix.tsx @@ -3,7 +3,6 @@ import { Canvas, Fill, Paint, - Skia, useFont, } from "@shopify/react-native-skia"; import React from "react"; diff --git a/package/cpp/api/JsiSkApi.h b/package/cpp/api/JsiSkApi.h index 53a21ba791..c00a318be4 100644 --- a/package/cpp/api/JsiSkApi.h +++ b/package/cpp/api/JsiSkApi.h @@ -35,6 +35,7 @@ #include "JsiSkDataFactory.h" #include "JsiSkFontMgrFactory.h" #include "JsiSkSurfaceFactory.h" +#include "JsiSkTextBlobFactory.h" namespace RNSkia { @@ -87,6 +88,7 @@ namespace RNSkia "RuntimeEffect", std::make_shared(context)); installReadonlyProperty("Shader", std::make_shared(context)); + installReadonlyProperty("TextBlob", std::make_shared(context)); installReadonlyProperty("Surface", std::make_shared(context)); }; }; diff --git a/package/cpp/api/JsiSkCanvas.h b/package/cpp/api/JsiSkCanvas.h index 9bb39eaf95..b2bbfd3bc0 100644 --- a/package/cpp/api/JsiSkCanvas.h +++ b/package/cpp/api/JsiSkCanvas.h @@ -11,6 +11,7 @@ #include "JsiSkRRect.h" #include "JsiSkSVG.h" #include "JsiSkVertices.h" +#include "JsiSkTextBlob.h" #include #include @@ -314,6 +315,15 @@ class JsiSkCanvas : public JsiSkHostObject { return jsi::Value::undefined(); } + JSI_HOST_FUNCTION(drawTextBlob) { + auto blob = JsiSkTextBlob::fromValue(runtime, arguments[0]); + SkScalar x = arguments[1].asNumber(); + SkScalar y = arguments[2].asNumber(); + auto paint = JsiSkPaint::fromValue(runtime, arguments[3]); + _canvas->drawTextBlob(blob, x, y, *paint); + return jsi::Value::undefined(); + } + JSI_HOST_FUNCTION(drawGlyphs) { auto jsiGlyphs = arguments[0].asObject(runtime).asArray(runtime); auto jsiPositions = arguments[1].asObject(runtime).asArray(runtime); @@ -484,6 +494,7 @@ class JsiSkCanvas : public JsiSkHostObject { JSI_EXPORT_FUNC(JsiSkCanvas, drawPath), JSI_EXPORT_FUNC(JsiSkCanvas, drawVertices), JSI_EXPORT_FUNC(JsiSkCanvas, drawText), + JSI_EXPORT_FUNC(JsiSkCanvas, drawTextBlob), JSI_EXPORT_FUNC(JsiSkCanvas, drawGlyphs), JSI_EXPORT_FUNC(JsiSkCanvas, drawSvg), JSI_EXPORT_FUNC(JsiSkCanvas, clipPath), diff --git a/package/cpp/api/JsiSkFont.h b/package/cpp/api/JsiSkFont.h index 8b00ba2dad..a62477d2ae 100644 --- a/package/cpp/api/JsiSkFont.h +++ b/package/cpp/api/JsiSkFont.h @@ -92,7 +92,7 @@ class JsiSkFont : public JsiSkWrappingSharedPtrHostObject { } std::vector glyphs; - int glyphsSize = static_cast(jsiPositions.size(runtime)); + int glyphsSize = static_cast(jsiGlyphs.size(runtime)); for (int i = 0; i < glyphsSize; i++) { glyphs.push_back(jsiGlyphs.getValueAtIndex(runtime, i).asNumber()); } diff --git a/package/cpp/api/JsiSkRSXform.h b/package/cpp/api/JsiSkRSXform.h index 132fecdda5..2758ae670b 100644 --- a/package/cpp/api/JsiSkRSXform.h +++ b/package/cpp/api/JsiSkRSXform.h @@ -57,15 +57,13 @@ namespace RNSkia { .asHostObject(runtime) .get() ->getObject(); - } else if(object.isArray(runtime)) { + } else { auto scos = object.getArray(runtime).getValueAtIndex(runtime, 0).asNumber(); auto ssin = object.getArray(runtime).getValueAtIndex(runtime, 1).asNumber(); auto tx = object.getArray(runtime).getValueAtIndex(runtime, 2).asNumber(); auto ty = object.getArray(runtime).getValueAtIndex(runtime, 3).asNumber(); return std::make_shared(SkRSXform::Make(scos, ssin, tx, ty)); } - jsi::detail::throwJSError(runtime, "Invalid RSXform"); - return jsi::Value::undefined(); } /** diff --git a/package/cpp/api/JsiSkTextBlob.h b/package/cpp/api/JsiSkTextBlob.h new file mode 100644 index 0000000000..9cdefe0bd1 --- /dev/null +++ b/package/cpp/api/JsiSkTextBlob.h @@ -0,0 +1,47 @@ +#pragma once + +#include "JsiSkHostObjects.h" + +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wdocumentation" + +#include + +#pragma clang diagnostic pop + +namespace RNSkia { + + using namespace facebook; + + class JsiSkTextBlob : public JsiSkWrappingSkPtrHostObject { + public: + JsiSkTextBlob( + std::shared_ptr context, + sk_sp shader + ) : JsiSkWrappingSkPtrHostObject(context, shader) {} + + // TODO: declare in JsiSkWrappingSkPtrHostObject via extra template parameter? + + JSI_PROPERTY_GET(__typename__) { + return jsi::String::createFromUtf8(runtime, "TextBlob"); + } + + JSI_EXPORT_PROPERTY_GETTERS( + JSI_EXPORT_PROP_GET(JsiSkTextBlob, __typename__), + ) + + /** + Returns the underlying object from a host object of this type + */ + static sk_sp fromValue(jsi::Runtime &runtime, + const jsi::Value &obj) { + const auto object = obj.asObject(runtime); + return object + .asHostObject(runtime) + .get() + ->getObject(); + + } + + }; +} // namespace RNSkia diff --git a/package/cpp/api/JsiSkTextBlobFactory.h b/package/cpp/api/JsiSkTextBlobFactory.h new file mode 100644 index 0000000000..c6d8d44758 --- /dev/null +++ b/package/cpp/api/JsiSkTextBlobFactory.h @@ -0,0 +1,94 @@ +#pragma once + +#include "JsiSkHostObjects.h" +#include "JsiSkTextBlob.h" +#include "JsiSkRSXform.h" +#include +#include + +namespace RNSkia { + + using namespace facebook; + + class JsiSkTextBlobFactory : public JsiSkHostObject { + public: + JSI_HOST_FUNCTION(MakeFromText) { + auto str = arguments[0].asString(runtime).utf8(runtime); + auto font = JsiSkFont::fromValue(runtime, arguments[1]); + auto textBlob = SkTextBlob::MakeFromString(str.c_str(), *font); + return jsi::Object::createFromHostObject( + runtime, + std::make_shared(getContext(), textBlob) + ); + } + + JSI_HOST_FUNCTION(MakeFromGlyphs) { + auto jsiGlyphs = arguments[0].asObject(runtime).asArray(runtime); + auto font = JsiSkFont::fromValue(runtime, arguments[1]); + int bytesPerGlyph = 2; + std::vector glyphs; + int glyphsSize = static_cast(jsiGlyphs.size(runtime)); + for (int i = 0; i < glyphsSize; i++) { + glyphs.push_back(jsiGlyphs.getValueAtIndex(runtime, i).asNumber()); + } + auto textBlob = SkTextBlob::MakeFromText(glyphs.data(), glyphs.size() * bytesPerGlyph, *font, SkTextEncoding::kGlyphID); + return jsi::Object::createFromHostObject( + runtime, + std::make_shared(getContext(), textBlob) + ); + } + + JSI_HOST_FUNCTION(MakeFromRSXform) { + auto str = arguments[0].asString(runtime).utf8(runtime); + auto jsiRsxforms = arguments[1].asObject(runtime).asArray(runtime); + auto font = JsiSkFont::fromValue(runtime, arguments[2]); + int bytesPerGlyph = 2; + std::vector rsxforms; + int rsxformsSize = static_cast(jsiRsxforms.size(runtime)); + for (int i = 0; i < rsxformsSize; i++) { + auto rsxform = JsiSkRSXform::fromValue(runtime, jsiRsxforms.getValueAtIndex(runtime, i)); + rsxforms.push_back(*rsxform); + } + auto textBlob = SkTextBlob::MakeFromRSXform(str.c_str(), str.length(), rsxforms.data(), *font); + return jsi::Object::createFromHostObject( + runtime, + std::make_shared(getContext(), textBlob) + ); + } + + JSI_HOST_FUNCTION(MakeFromRSXformGlyphs) { + auto jsiGlyphs = arguments[0].asObject(runtime).asArray(runtime); + auto jsiRsxforms = arguments[1].asObject(runtime).asArray(runtime); + auto font = JsiSkFont::fromValue(runtime, arguments[2]); + int bytesPerGlyph = 2; + std::vector glyphs; + int glyphsSize = static_cast(jsiGlyphs.size(runtime)); + for (int i = 0; i < glyphsSize; i++) { + glyphs.push_back(jsiGlyphs.getValueAtIndex(runtime, i).asNumber()); + } + std::vector rsxforms; + int rsxformsSize = static_cast(jsiRsxforms.size(runtime)); + for (int i = 0; i < rsxformsSize; i++) { + auto rsxform = JsiSkRSXform::fromValue(runtime, jsiRsxforms.getValueAtIndex(runtime, i)); + rsxforms.push_back(*rsxform); + } + auto textBlob = SkTextBlob::MakeFromRSXform(glyphs.data(), glyphs.size() * bytesPerGlyph, rsxforms.data(), *font, SkTextEncoding::kGlyphID); + return jsi::Object::createFromHostObject( + runtime, + std::make_shared(getContext(), textBlob) + ); + } + + + JSI_EXPORT_FUNCTIONS( + JSI_EXPORT_FUNC(JsiSkTextBlobFactory, MakeFromText), + JSI_EXPORT_FUNC(JsiSkTextBlobFactory, MakeFromGlyphs), + JSI_EXPORT_FUNC(JsiSkTextBlobFactory, MakeFromRSXform), + JSI_EXPORT_FUNC(JsiSkTextBlobFactory, MakeFromRSXformGlyphs), + ) + + JsiSkTextBlobFactory(std::shared_ptr context) + : JsiSkHostObject(context) {} + }; + +} // namespace RNSkia diff --git a/package/src/skia/Canvas.ts b/package/src/skia/Canvas.ts index ba9f852051..02cf496795 100644 --- a/package/src/skia/Canvas.ts +++ b/package/src/skia/Canvas.ts @@ -12,6 +12,7 @@ import type { Matrix } from "./Matrix"; import type { IImageFilter } from "./ImageFilter"; import type { MipmapMode, FilterMode } from "./Image/Image"; import type { Vertices } from "./Vertices"; +import type { ITextBlob } from "./TextBlob"; export enum ClipOp { Difference, @@ -318,6 +319,16 @@ export interface ICanvas { */ drawText(str: string, x: number, y: number, paint: IPaint, font: IFont): void; + /** + * Draws the given TextBlob at (x, y) using the current clip, current matrix, and the + * provided paint. Reminder that the fonts used to draw TextBlob are part of the blob. + * @param blob + * @param x + * @param y + * @param paint + */ + drawTextBlob(blob: ITextBlob, x: number, y: number, paint: IPaint): void; + /** * Draws a run of glyphs, at corresponding positions, in a given font. * @param glyphs the array of glyph IDs (Uint16TypedArray) diff --git a/package/src/skia/Skia.ts b/package/src/skia/Skia.ts index ced12cbb37..0f13e18675 100644 --- a/package/src/skia/Skia.ts +++ b/package/src/skia/Skia.ts @@ -18,6 +18,7 @@ import type { IPoint } from "./Point"; import type { Vertices, VertexMode } from "./Vertices/Vertices"; import type { DataFactory } from "./Data"; import type { SVGFactory } from "./SVG"; +import type { TextBlobFactory } from "./TextBlob"; import type { FontMgrFactory } from "./FontMgr/FontMgrFactory"; import type { SurfaceFactory } from "./Surface"; import "./NativeSetup"; @@ -64,6 +65,7 @@ export interface Skia { Image: ImageFactory; SVG: SVGFactory; FontMgr: FontMgrFactory; + TextBlob: TextBlobFactory; Surface: SurfaceFactory; } @@ -93,13 +95,14 @@ export const Skia = { PathEffect: SkiaApi.PathEffect, Data: SkiaApi.Data, Matrix: SkiaApi.Matrix, - MakeVertices: SkiaApi.MakeVertices, SVG: SkiaApi.SVG, FontMgr: SkiaApi.FontMgr, + TextBlob: SkiaApi.TextBlob, // Here are constructors for data types which are represented as typed arrays in CanvasKit Color, RSXform: SkiaApi.RSXform, // Here the factory symmetry is broken to be comptatible with CanvasKit MakeSurface: SkiaApi.Surface.Make, MakeImageFromEncoded: SkiaApi.Image.MakeImageFromEncoded, + MakeVertices: SkiaApi.MakeVertices, }; diff --git a/package/src/skia/TextBlob.ts b/package/src/skia/TextBlob.ts new file mode 100644 index 0000000000..1d93a8cbaf --- /dev/null +++ b/package/src/skia/TextBlob.ts @@ -0,0 +1,52 @@ +import type { SkJSIInstance } from "./JsiInstance"; +import type { IFont } from "./Font/Font"; +import type { IRSXform } from "./RSXform"; + +export type ITextBlob = SkJSIInstance<"TextBlob">; + +export interface TextBlobFactory { + /** + * Return a TextBlob with a single run of text. + * + * It uses the default character-to-glyph mapping from the typeface in the font. + * It does not perform typeface fallback for characters not found in the Typeface. + * It does not perform kerning or other complex shaping; glyphs are positioned based on their + * default advances. + * @param str + * @param font + */ + MakeFromText(str: string, font: IFont): ITextBlob; + /** + * Return a TextBlob with a single run of text. + * + * It does not perform typeface fallback for characters not found in the Typeface. + * It does not perform kerning or other complex shaping; glyphs are positioned based on their + * default advances. + * @param glyphs - if using Malloc'd array, be sure to use CanvasKit.MallocGlyphIDs(). + * @param font + */ + MakeFromGlyphs(glyphs: number[], font: IFont): ITextBlob; + + /** + * Returns a TextBlob built from a single run of text with rotation, scale, and translations. + * + * It uses the default character-to-glyph mapping from the typeface in the font. + * @param str + * @param rsxforms + * @param font + */ + MakeFromRSXform(str: string, rsxforms: IRSXform[], font: IFont): ITextBlob; + + /** + * Returns a TextBlob built from a single run of text with rotation, scale, and translations. + * + * @param glyphs - if using Malloc'd array, be sure to use CanvasKit.MallocGlyphIDs(). + * @param rsxforms + * @param font + */ + MakeFromRSXformGlyphs( + glyphs: number[], + rsxforms: IRSXform[], + font: IFont + ): ITextBlob; +} From 467afd47055b95323ea9bb8fe55f7a0eb0016daf Mon Sep 17 00:00:00 2001 From: William Candillon Date: Fri, 4 Feb 2022 19:31:35 +0100 Subject: [PATCH 18/28] Add getGlyphWidths() --- package/cpp/api/JsiSkFont.h | 19 +++++++++++++++++++ package/src/renderer/components/text/index.ts | 1 + 2 files changed, 20 insertions(+) diff --git a/package/cpp/api/JsiSkFont.h b/package/cpp/api/JsiSkFont.h index a62477d2ae..53154b848c 100644 --- a/package/cpp/api/JsiSkFont.h +++ b/package/cpp/api/JsiSkFont.h @@ -45,6 +45,24 @@ class JsiSkFont : public JsiSkWrappingSharedPtrHostObject { return JsiSkRect::toValue(runtime, getContext(), rect); } + JSI_HOST_FUNCTION(getGlyphWidths) { + auto jsiGlyphs = arguments[0].asObject(runtime).asArray(runtime); + auto paint = JsiSkPaint::fromValue(runtime, arguments[1]); + int bytesPerWidth = 4; + std::vector glyphs; + int glyphsSize = static_cast(jsiGlyphs.size(runtime)); + auto widthPtr = static_cast(malloc(glyphsSize * bytesPerWidth)); + for (int i = 0; i < glyphsSize; i++) { + glyphs.push_back(jsiGlyphs.getValueAtIndex(runtime, i).asNumber()); + } + getObject()->getWidthsBounds(glyphs.data(), glyphsSize, widthPtr, nullptr, paint.get()); + auto jsiWidths = jsi::Array(runtime, glyphsSize); + for (int i = 0; i getMetrics(&fm); @@ -207,6 +225,7 @@ class JsiSkFont : public JsiSkWrappingSharedPtrHostObject { JSI_EXPORT_FUNC(JsiSkFont, setEmbolden), JSI_EXPORT_FUNC(JsiSkFont, setSubpixel), JSI_EXPORT_FUNC(JsiSkFont, setTypeface), + JSI_EXPORT_FUNC(JsiSkFont, getGlyphWidths) ) JsiSkFont(std::shared_ptr context, const SkFont &font) diff --git a/package/src/renderer/components/text/index.ts b/package/src/renderer/components/text/index.ts index c3a24ae8c8..c1c76b7f4b 100644 --- a/package/src/renderer/components/text/index.ts +++ b/package/src/renderer/components/text/index.ts @@ -1,2 +1,3 @@ export * from "./Text"; export * from "./Glyphs"; +export * from "./TextBlob"; From efcc1c047ee07c1b838114b71ebc10089c475bbe Mon Sep 17 00:00:00 2001 From: William Candillon Date: Sun, 6 Feb 2022 17:10:20 +0100 Subject: [PATCH 19/28] :wrench: --- package/cpp/api/JsiSkApi.h | 2 ++ package/cpp/api/JsiSkFont.h | 3 --- package/cpp/api/JsiSkRSXform.h | 4 ++-- package/src/skia/Font/Font.ts | 14 ++++++++++++++ package/src/skia/Skia.ts | 28 +++++++++++++++++++--------- 5 files changed, 37 insertions(+), 14 deletions(-) diff --git a/package/cpp/api/JsiSkApi.h b/package/cpp/api/JsiSkApi.h index c00a318be4..b0d1b2e705 100644 --- a/package/cpp/api/JsiSkApi.h +++ b/package/cpp/api/JsiSkApi.h @@ -36,6 +36,7 @@ #include "JsiSkFontMgrFactory.h" #include "JsiSkSurfaceFactory.h" #include "JsiSkTextBlobFactory.h" +#include "JsiSkContourMeasureIter.h" namespace RNSkia { @@ -61,6 +62,7 @@ namespace RNSkia installFunction("XYWHRect", JsiSkRect::createCtor(context)); installFunction("RRectXY", JsiSkRRect::createCtor(context)); installFunction("Point", JsiSkPoint::createCtor(context)); + installFunction("ContourMeasureIter", JsiSkContourMeasureIter::createCtor(context)); installFunction("MakeVertices", JsiSkVertices::createCtor(context)); // Static members diff --git a/package/cpp/api/JsiSkFont.h b/package/cpp/api/JsiSkFont.h index 53154b848c..2549d3a67e 100644 --- a/package/cpp/api/JsiSkFont.h +++ b/package/cpp/api/JsiSkFont.h @@ -71,9 +71,6 @@ class JsiSkFont : public JsiSkWrappingSharedPtrHostObject { metrics.setProperty(runtime, "descent", fm.fDescent); metrics.setProperty(runtime, "leading", fm.fLeading); if (!(fm.fFlags & SkFontMetrics::kBoundsInvalid_Flag)) { - const float rect[] = { - fm.fXMin, fm.fTop, fm.fXMax, fm.fBottom - }; auto bounds = SkRect::MakeLTRB(fm.fXMin,fm.fTop, fm.fXMax, fm.fBottom ); auto jsiBounds = JsiSkRect::toValue(runtime, getContext(), bounds); metrics.setProperty(runtime, "bounds", jsiBounds); diff --git a/package/cpp/api/JsiSkRSXform.h b/package/cpp/api/JsiSkRSXform.h index 2758ae670b..6931d00d38 100644 --- a/package/cpp/api/JsiSkRSXform.h +++ b/package/cpp/api/JsiSkRSXform.h @@ -77,10 +77,10 @@ namespace RNSkia { } /** - * Creates the function for construction a new instance of the SkPoint + * Creates the function for construction a new instance of the SkRSXform * wrapper * @param context platform context - * @return A function for creating a new host object wrapper for the SkPoint + * @return A function for creating a new host object wrapper for the SkRSXform * class */ static const jsi::HostFunctionType diff --git a/package/src/skia/Font/Font.ts b/package/src/skia/Font/Font.ts index 2922ebe732..bf172285a2 100644 --- a/package/src/skia/Font/Font.ts +++ b/package/src/skia/Font/Font.ts @@ -39,6 +39,20 @@ export interface IFont extends SkJSIInstance<"Font"> { */ getGlyphIDs(str: string, numCodePoints?: number): number[]; + /** + * Retrieves the advanceX measurements for each glyph. + * If paint is not null, its stroking, PathEffect, and MaskFilter fields are respected. + * One width per glyph is returned in the returned array. + * @param glyphs + * @param paint + * @param output - if provided, the results will be copied into this array. + */ + getGlyphWidths( + glyphs: number[], + paint?: IPaint | null, + output?: Float32Array + ): Float32Array; + /** * Computes any intersections of a thick "line" and a run of positionsed glyphs. * The thick line is represented as a top and bottom coordinate (positive for diff --git a/package/src/skia/Skia.ts b/package/src/skia/Skia.ts index 0f13e18675..9f472a5b9f 100644 --- a/package/src/skia/Skia.ts +++ b/package/src/skia/Skia.ts @@ -23,6 +23,8 @@ import type { FontMgrFactory } from "./FontMgr/FontMgrFactory"; import type { SurfaceFactory } from "./Surface"; import "./NativeSetup"; import type { IRSXform } from "./RSXform"; +import type { IPath } from "./Path/Path"; +import type { IContourMeasureIter } from "./ContourMeasure"; /** * Declares the interface for the native Skia API @@ -32,6 +34,11 @@ export interface Skia { XYWHRect: (x: number, y: number, width: number, height: number) => IRect; RRectXY: (rect: IRect, rx: number, ry: number) => IRRect; RSXform: (scos: number, ssin: number, tx: number, ty: number) => IRSXform; + ContourMeasureIter: ( + path: IPath, + forceClosed: boolean, + resScale: number + ) => IContourMeasureIter; Paint: () => IPaint; Path: PathFactory; Matrix: () => Matrix; @@ -80,13 +87,7 @@ declare global { * Declares the implemented API with overrides. */ export const Skia = { - Point: SkiaApi.Point, - XYWHRect: SkiaApi.XYWHRect, - RRectXY: SkiaApi.RRectXY, - Paint: SkiaApi.Paint, - Path: SkiaApi.Path, - ColorFilter: SkiaApi.ColorFilter, - Font: SkiaApi.Font, + // Factories Typeface: SkiaApi.Typeface, MaskFilter: SkiaApi.MaskFilter, RuntimeEffect: SkiaApi.RuntimeEffect, @@ -94,14 +95,23 @@ export const Skia = { ImageFilter: SkiaApi.ImageFilter, PathEffect: SkiaApi.PathEffect, Data: SkiaApi.Data, - Matrix: SkiaApi.Matrix, SVG: SkiaApi.SVG, FontMgr: SkiaApi.FontMgr, TextBlob: SkiaApi.TextBlob, + // Constructors + Matrix: SkiaApi.Matrix, + Font: SkiaApi.Font, + Point: SkiaApi.Point, + XYWHRect: SkiaApi.XYWHRect, + RRectXY: SkiaApi.RRectXY, + Paint: SkiaApi.Paint, + Path: SkiaApi.Path, + ColorFilter: SkiaApi.ColorFilter, + ContourMeasureIter: SkiaApi.ContourMeasureIter, // Here are constructors for data types which are represented as typed arrays in CanvasKit Color, RSXform: SkiaApi.RSXform, - // Here the factory symmetry is broken to be comptatible with CanvasKit + // For the following methods the factory symmetry is broken to be comptatible with CanvasKit MakeSurface: SkiaApi.Surface.Make, MakeImageFromEncoded: SkiaApi.Image.MakeImageFromEncoded, MakeVertices: SkiaApi.MakeVertices, From e36b4a23b9d92e4803c08e863cc59405ed8048d7 Mon Sep 17 00:00:00 2001 From: William Candillon Date: Sun, 6 Feb 2022 17:11:01 +0100 Subject: [PATCH 20/28] :wrench: --- package/cpp/api/JsiSkContourMeasure.h | 52 ++++++++++++ package/cpp/api/JsiSkContourMeasureIter.h | 80 +++++++++++++++++++ .../src/renderer/components/text/TextBlob.tsx | 18 +++++ .../src/renderer/components/text/TextPath.tsx | 62 ++++++++++++++ package/src/skia/ContourMeasure.tsx | 49 ++++++++++++ 5 files changed, 261 insertions(+) create mode 100644 package/cpp/api/JsiSkContourMeasure.h create mode 100644 package/cpp/api/JsiSkContourMeasureIter.h create mode 100644 package/src/renderer/components/text/TextBlob.tsx create mode 100644 package/src/renderer/components/text/TextPath.tsx create mode 100644 package/src/skia/ContourMeasure.tsx diff --git a/package/cpp/api/JsiSkContourMeasure.h b/package/cpp/api/JsiSkContourMeasure.h new file mode 100644 index 0000000000..3bafb4fc87 --- /dev/null +++ b/package/cpp/api/JsiSkContourMeasure.h @@ -0,0 +1,52 @@ +#pragma once + + +#include "JsiSkHostObjects.h" + +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wdocumentation" + +#include + +#pragma clang diagnostic pop + +#include + +namespace RNSkia { + + using namespace facebook; + + class JsiSkContourMeasure : public JsiSkWrappingSkPtrHostObject { + public: + JsiSkContourMeasure(std::shared_ptr context, + const sk_sp contourMeasure) + : JsiSkWrappingSkPtrHostObject(context, contourMeasure){}; + + + JSI_PROPERTY_GET(__typename__) { + return jsi::String::createFromUtf8(runtime, "ContourMeasure"); + } + + JSI_EXPORT_PROPERTY_GETTERS(JSI_EXPORT_PROP_GET(JsiSkContourMeasure, __typename__)) + + /** + Returns the underlying object from a host object of this type + */ + static sk_sp fromValue(jsi::Runtime &runtime, + const jsi::Value &obj) { + return obj.asObject(runtime) + .asHostObject(runtime) + .get() + ->getObject(); + } + + static jsi::Value toValue(jsi::Runtime &runtime, + std::shared_ptr context, + sk_sp contourMeasure) { + return jsi::Object::createFromHostObject( + runtime, + std::make_shared(context, contourMeasure) + ); + } + }; +} // namespace RNSkia diff --git a/package/cpp/api/JsiSkContourMeasureIter.h b/package/cpp/api/JsiSkContourMeasureIter.h new file mode 100644 index 0000000000..b652a07e95 --- /dev/null +++ b/package/cpp/api/JsiSkContourMeasureIter.h @@ -0,0 +1,80 @@ +#pragma once + +#include "JsiSkHostObjects.h" +#include "JsiSkContourMeasure.h" + +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wdocumentation" + +#include + +#pragma clang diagnostic pop + +namespace RNSkia { + + using namespace facebook; + + class JsiSkContourMeasureIter : public JsiSkWrappingSharedPtrHostObject { + public: + JsiSkContourMeasureIter( + std::shared_ptr context, + const SkPath& path, + bool forceClosed, + SkScalar resScale = 1 + ) : JsiSkWrappingSharedPtrHostObject( + context, std::make_shared(path, forceClosed, resScale)) {} + + // TODO: declare in JsiSkWrappingSkPtrHostObject via extra template parameter? + JSI_PROPERTY_GET(__typename__) { + return jsi::String::createFromUtf8(runtime, "ContourMeasureIter"); + } + + JSI_EXPORT_PROPERTY_GETTERS( + JSI_EXPORT_PROP_GET(JsiSkContourMeasureIter, __typename__), + ) + + JSI_HOST_FUNCTION(next) { + return JsiSkContourMeasure::toValue(runtime, getContext(), getObject()->next()); + } + + JSI_EXPORT_FUNCTIONS( + JSI_EXPORT_FUNC(JsiSkContourMeasureIter, next) + ) + + /** + Returns the underlying object from a host object of this type + */ + static std::shared_ptr fromValue(jsi::Runtime &runtime, + const jsi::Value &obj) { + return obj.asObject(runtime) + .asHostObject(runtime) + .get() + ->getObject(); + } + + /** + * Creates the function for construction a new instance of the SkContourMeasureIter + * wrapper + * @param context platform context + * @return A function for creating a new host object wrapper for the SkContourMeasureIter + * class + */ + static const jsi::HostFunctionType + createCtor(std::shared_ptr context) { + return JSI_HOST_FUNCTION_LAMBDA { + auto path = JsiSkPath::fromValue(runtime, arguments[0]); + auto forceClosed = arguments[1].getBool(); + auto resScale = arguments[2].asNumber(); + // Return the newly constructed object + return jsi::Object::createFromHostObject( + runtime, std::make_shared( + context, + *path, + forceClosed, + resScale + ) + ); + }; + } + }; +} // namespace RNSkia diff --git a/package/src/renderer/components/text/TextBlob.tsx b/package/src/renderer/components/text/TextBlob.tsx new file mode 100644 index 0000000000..4496ada7ed --- /dev/null +++ b/package/src/renderer/components/text/TextBlob.tsx @@ -0,0 +1,18 @@ +import React from "react"; + +import type { CustomPaintProps, AnimatedProps } from "../../processors"; +import { useDrawing } from "../../nodes/Drawing"; +import type { ITextBlob } from "../../../skia/TextBlob"; + +export interface TextBlobProps extends CustomPaintProps { + blob: ITextBlob; + x: number; + y: number; +} + +export const TextBlob = (props: AnimatedProps) => { + const onDraw = useDrawing(props, ({ canvas, paint }, { blob, x, y }) => { + canvas.drawTextBlob(blob, x, y, paint); + }); + return ; +}; diff --git a/package/src/renderer/components/text/TextPath.tsx b/package/src/renderer/components/text/TextPath.tsx new file mode 100644 index 0000000000..dec4a5028a --- /dev/null +++ b/package/src/renderer/components/text/TextPath.tsx @@ -0,0 +1,62 @@ +import React from "react"; + +import type { CustomPaintProps, AnimatedProps } from "../../processors"; +import { useDrawing } from "../../nodes/Drawing"; +import type { IPath } from "../../../skia/Path"; +import type { IFont } from "../../../skia/Font"; + +export interface TextPathProps extends CustomPaintProps { + string: string; + path: IPath; + font: IFont; + initialOffset: number; +} + +export const TextPath = (props: AnimatedProps) => { + const onDraw = useDrawing( + props, + ({ canvas, paint }, { string, path, font, initialOffset }) => { + // const ids = font.getGlyphIDs(string); + // const widths = font.getGlyphWidths(ids); + // const rsx: IRSXform[] = []; + // const meas = new Skia.ContourMeasureIter(path, false, 1); + // let cont = meas.next(); + // let dist = initialOffset; + // const xycs = new Float32Array(4); + // for (let i = 0; i < string.length && cont; i++) { + // const width = widths[i]; + // dist += width / 2; + // if (dist > cont.length()) { + // // jump to next contour + // cont.delete(); + // cont = meas.next(); + // if (!cont) { + // // We have come to the end of the path - terminate the string + // // right here. + // string = string.substring(0, i); + // break; + // } + // dist = width / 2; + // } + // // Gives us the (x, y) coordinates as well as the cos/sin of the tangent + // // line at that position. + // cont.getPosTan(dist, xycs); + // var cx = xycs[0]; + // var cy = xycs[1]; + // var cosT = xycs[2]; + // var sinT = xycs[3]; + // var adjustedX = cx - (width / 2) * cosT; + // var adjustedY = cy - (width / 2) * sinT; + // rsx.push(cosT, sinT, adjustedX, adjustedY); + // dist += width / 2; + // } + // var retVal = Skia.TextBlob.MakeFromRSXform(string, rsx, font); + // return retVal; + } + ); + return ; +}; + +TextPath.defaultProps = { + initialOffset: 0, +}; diff --git a/package/src/skia/ContourMeasure.tsx b/package/src/skia/ContourMeasure.tsx new file mode 100644 index 0000000000..eb79898344 --- /dev/null +++ b/package/src/skia/ContourMeasure.tsx @@ -0,0 +1,49 @@ +import type { SkJSIInstance } from "./JsiInstance"; +import type { IPath } from "./Path/Path"; + +export interface PosTan { + px: number; + py: number; + tx: number; + ty: number; +} + +export interface IContourMeasure extends SkJSIInstance<"ContourMeasureIter"> { + /** + * Returns the given position and tangent line for the distance on the given contour. + * The return value is 4 floats in this order: posX, posY, vecX, vecY. + * @param distance - will be pinned between 0 and length(). + * @param output - if provided, the four floats of the PosTan will be copied into this array + * instead of allocating a new one. + */ + getPosTan(distance: number, output?: PosTan): PosTan; + + /** + * Returns an Path representing the segement of this contour. + * @param startD - will be pinned between 0 and length() + * @param stopD - will be pinned between 0 and length() + * @param startWithMoveTo + */ + getSegment(startD: number, stopD: number, startWithMoveTo: boolean): IPath; + + /** + * Returns true if the contour is closed. + */ + isClosed(): boolean; + + /** + * Returns the length of this contour. + */ + length(): number; +} + +export interface IContourMeasureIter + extends SkJSIInstance<"ContourMeasureIter"> { + /** + * Iterates through contours in path, returning a contour-measure object for each contour + * in the path. Returns null when it is done. + * + * See SkContourMeasure.h for more details. + */ + next(): IContourMeasure | null; +} From 138395709a6c58b8be920fd970a1da17cee4168c Mon Sep 17 00:00:00 2001 From: William Candillon Date: Sun, 6 Feb 2022 18:38:09 +0100 Subject: [PATCH 21/28] :wrench: --- example/src/Examples/API/Path2.tsx | 16 ++++ example/src/Examples/API/Roboto-Regular.otf | Bin 0 -> 186664 bytes package/cpp/api/JsiSkContourMeasure.h | 44 ++++++++++- package/cpp/api/JsiSkFont.h | 10 ++- package/cpp/api/JsiSkPath.h | 9 +++ .../src/renderer/components/text/TextPath.tsx | 72 +++++++++--------- package/src/renderer/components/text/index.ts | 1 + 7 files changed, 110 insertions(+), 42 deletions(-) create mode 100644 example/src/Examples/API/Roboto-Regular.otf diff --git a/example/src/Examples/API/Path2.tsx b/example/src/Examples/API/Path2.tsx index 026559b2fc..110f0f627d 100644 --- a/example/src/Examples/API/Path2.tsx +++ b/example/src/Examples/API/Path2.tsx @@ -7,6 +7,8 @@ import { Path, Group, rect, + TextPath, + useFont, } from "@shopify/react-native-skia"; import { Title } from "./components/Title"; @@ -43,6 +45,10 @@ const result = Skia.Path.MakeFromOp(rect1, circle, PathOp.Difference)!; result.simplify(); export const PathExample = () => { + const font = useFont(require("./Roboto-Regular.otf"), 32); + if (!font) { + return null; + } return ( Path Operations @@ -68,6 +74,12 @@ export const PathExample = () => { + Text Path + + + + + ); }; @@ -89,4 +101,8 @@ const styles = StyleSheet.create({ width: SIZE, height: SIZE, }, + textPath: { + width: SIZE, + height: SIZE, + }, }); diff --git a/example/src/Examples/API/Roboto-Regular.otf b/example/src/Examples/API/Roboto-Regular.otf new file mode 100644 index 0000000000000000000000000000000000000000..8f0906a79cdb774f9395344c8df34305235af057 GIT binary patch literal 186664 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

$nCV!hz6BCi)HRzez;gHlG zN%vUWvetkZ`B@Z5r7X-s&7%lK4@6i`R;Zs1Y29!6m^w9U+HbgL{du=u>9-5+>FN2af7bM@L>DaYnI z1PFcvg3GTNti>2~?M2!#A97vu!@_|z1U2r8C+gCg%(m`>+C?1VSRZCLmimV?*>vW^ zlV_=I#27>i<(4X$YLxV3tL@#yv7nd%5ZR@-Jpmc$F6?Rl{* zM^G+}sq}qYQlIkla;w$9nA+ycL;?7F;LvBMg<~NXb{KjKgw%gjHwG0ftUUMoDCrG> zjlD!nd}Xnww^!+HpF_Da%mbB9*Ia#3&wo=1aZ8{KerGBnDeI=HYDk=gsy@|fDo~L9 za2*^)mLl*H=T6@p7(gaz<<^K#4KJ@5<&GrfK!dy3%?|%s^HK630s8R;lfo-%l3}%j zDLUF6J(*{--C)Y&Pmq0y(%wjg5LG~B>Ez-pV30ASZuQ58*_QM@cidt!X( z6Pk_S7PoB}aXcfz*6`nQxeSS}U=*+l??2J5`9E;Av(SA%UUf9+g*O%1;L5#hC^K$zsp zO75;`96QMnn?dG`gIdEkp6Q= z=kL+`>EGlJ`RS$i7cQ$EUgQ2+dmZ2^^rlzM)xWmZfLjF9Bpu?l)PBo{o#KboO^b&o z#T%-x7S9I7xBg>`Kg5TXpg8VD*CTxUFxSKF>~>6co;)Q{ZU?v15#*Ac5^{%An(S1W zwtRg};lkP*YU1$aFe(LpB&V>40A?`|Bsnv`gCK1F|A+V)L09}=*e+gB0-_ihl5}uK z=&ccMFM>e2t?IkTb#WV3;D6vrdT%jpY12ij_o?Q-X<1eR_7746lpUhlnVUd@X@j&r~m)sUya@(aWN=`dd4e$)u6XuB)WH#1^*{Do2iFweR z8;xl|@B8g@H^YgpSt?|PRy_5@Q>ZVt5g;-|(r}THF<7pT_4ko09`SQy9N4Tv3P?n2 zVLqv%UhNR3eD7L6ShFB6<k>?|t-F z`frh!cGG|En7E);(!X%#GV}Va@fP{D!nu?Bz1H<$oTyW@@LjaXuW2MuN z!9#EhWUsHEfF~}i#XxWfx*j~H&&nBIj90YI)t`z)KvsGe%1SY7f&k7X-qX33 z0mZaaj$UbAdehcIC`|ouv^98anyF!US#BdW(4oHm!51Fnw3Qv7>Y8ojHFUq=6{O9I ztJOIgPE*A>8qt`B#xH}$gX`&LS*uR)La4RQ}`sUdIliXNW-2xwBo+=8_(yl znaQ_gHf+*KcccUFel#%+=F6P%ZI5PVgkPDY(Yy4if~_N6N`N&o~AK&0%+L zoEs^$E|0!LL|rZvZ1L>Kc!*H2#j{u4ufm-c&t}Bu|GvGU@wO$L92P(5UI`Z#F7ICL zj&#FAhAiGE`x29+*+Xo^xTEwQOg-QPgHd|Vt@d<8G1VL5eiVaI(S51%XZsHlCJaW) zP|V?Jk$qKCsCbBeFcketJ+B6su&v5XmP_jE2S)~4P}sZ`IWG;WTr25i+h6$GoMUtN zH3#@*0H-EhhT?&-y598bfj7MZ<@$dQ_{FmABToH|`Ao)*L0ce>`5Rd~R$p zgLU~sw7%)TC*!GF+4LX6=Bm;P76SlPqk}zJwejpy>Vg`t z6;qToye&CV@!@ct?staKek@d$Hr`X8W^bLVCQCU84Ou~@Fp$X3W01~jXn4%D!Eu56 z8*pq?Uq9{z%zHZ;o^3-q#=m7ci9#&AcH(ok7vp`Sp|^meIo2{62~%(OTDFkZ--@4< z{jq%a=pWD2qZZXh#ZBRDEvp@p@2p2NxI(mVxJ#5gwyN*9(R6pHn-IO990=cfaxQq4=*E9oo(&Nddp4XBZWt3DxAjS^h}*~*yaJ8i3bF>Yg4`Pm7bMyS(W~nZIm>*D|v5CfQTO%Ya6XU3hrY#bna>^vCjb`qy0A79|)qm6p2o;@9-Ix%jjA=kWIC;$HD`{cA2A z75@_6(Of!^QvQ`rh`2TLL-pjXvXJ=rKsP=dQIjcCS+9>j=gpbh=DODs6mI zw$H=c7NF0pPZQ>=Ek<~-)_#02S0Mm^d3SW zd6r8v3ys#-Kl4&>%aQYkXPPH%Ujp7|=DBvaiW_si_Uk*oT;xuDkjLJ^~F@rb6`M%$p#{{ehx zOcW_RF><$7W{!z3|0ewNi!eFyLT_)gkQmz_(EW%x@aX~;LHEY=XVEkc#JOzaedjiw z%jh7UvBnl&58SWMRYhNT1;tj&pJ(o()}^X=Ir?3#b*(0598H}ZaYFECI}o_o4hNFr zdH5n%Kk$@+rwoH9W*g~m8f0>Lb%g943YLj#V{7i{D5kq)p{j2v@&Q9|ss0vRb-6%> z^OUB3XvO`P-A@pfXZ>miJ_mNS1DJnc{ls~~_J^0Te42w~k(uqKf6ekoKN3YK2$J*7 zP6>~MXT*U{m=v+htrK4@%4>mU?!;C+O3!E|>%$hH^zySPZj6^ki&Zwr zzarFaQKd0HThEGB=H~|qb<3-K9(U2BVvV|8s3@Os&=nA&0DsL2VvY5 z?j1qq+T^R^_d~Z?mt?c#SNl0&-}ejGEpc<*ugTo%CGmCdeOOhvC)qh%>VDm(I`daI zO?)KT1V*xRuX%;#3*$D(A4n9Br?a-69XCeQ#Ysbo+@Tq)R-y${z%X-qrPU_5t*vcL z0Voum9wa|4azFYl%dI2Ral^Y!O}A>@%Hx2s#}HyVUHe;m9C`lyn>+v4wItCO6~+tl z7uesl^rHNWVk5=Ui}Qc9ziDaxuWdBp#nRsSbM3D$oqtPs7iHxqhZe2sX3ulym$)o4#)AfDm!uH79<@?$Bl|7_7+UiQY3R-FN|_`*R05|tN+Dge6Dq2 zh!(eaIO(Ip0s4E9JJ($j-qU=_FIkd*N1Kkpd0NfFU-#3$^ax;bb@I7hN(&!IZqdJD z;SupcR`>vro>qfV;q~&;OWiaZ-DSSCW87AMPj-KbyM^xcE+EN)1ENFUdpJsR@!sN9 z3*l9xT=)WUwVC8yY__|H?z3)5p?aV5qr*NL6(#c)!T&2}KN(G_ESgf2jeq^4;4L1r za=W^}>wju#P5OC=i^U689WM}zr3d7vh!BgVj)1%J|4I8LR=ZXF8%wfHVO&-6Q#`Iq z6s_vJ1V~o%rUSCt^n3nLfB9ja1Zf)?-38u)XDJKO79Sqgoi*EKXg zVE#9s-fj<4@u2o7gr$9k||27UOeEaJ38_{|?$MbM^O*dU6!b z3k&t>Y`r5Q2JzZ-(j*9bsT0Iz2AFLiykgS^4!$To5z#7Fe?)XZgp22Eo&A-?C1`2L z{%gFG=&OawJu6;c&srF~Pl#_4rM0N{vgdv+>Kzn69Nt@#JXCw%?-+D%rv2d<#pr zrIx%)lMTus+3elW0wyDR)pob8+MBORTdKWM?gt7gdYF3tC$YmWu;L2!wzn#;%??|! zgEldl(*4F(%59ya3W%)B6*f1N-JGgTt5lW$W~K-VgtYjO)ug_@pD(qD0ggv}t`nBm zNGg0vvbJuy| zX+p6`rFfa6y*zI@lSEhM>R*b2Iq4kq^iVMe+o*1q2|8mcR4!|tS8a4m+3fJ?)e$qb z8j9!8+$rv}gJSV~1S1Ck8BebF^%j~HxD}J6*&NJ$Rd1hetLsyLe8W6_c6h_gXf!&P ziSZU3t)ADMN;xd*cuJQFuz89%u!%4?M8yU`BY)+C#q?8V(hPeh*I^_2NE6yLOC6YN z0LMtmBTBgin*RBcxMZgHs9wsg%+j%%Iob}Brs%UKPb;=p!eWdmrkm>lhn8iKWL`;i z+RA$~v}iW7-s2+m(F}pakfpW;0ENjaGL}$^eiwjIs-yrDFQzgacpbF_y26DBbOjlG zO1SH`63k`84Y1Fh5}F3WgH6;iWc7#fY4<@`KjL6cXDYfBY?$x*g)9B^wlrU%`sW-(vCj3P~Tj+1G z^v*a^PsP%X@nia1JQF^2RYsMY59z*F7^(lo;^W*Q2KV6f4DV0#HM0BS6XGp75MJrF zbAx6VUhU~FO}5*t?&rdmi{mX3&HRbq?#X)DeU^3aP`5JKNv?WU7(-*0b)PE^wVoEw zcw{N3WBHy>YG-^TderzM1}?aGFtd8laI;8oamFu({7_EB$;nE!}N4{Q{!T?R*dlrX0J4-yi z_FtcRD`eA;IcDHpX2Wl7JQdr@Xs)S>;-ncWj;ZFqrM4M|emo^e6w7;pt7W-cu9jFo zqH~(9b{9mDeMxnbKL?mC(qc1l!tFFH)7i4ht)s-QZ`w#>qwQyl=^A=53riS>Um?D* z_Lg{L_riJvPuZr)QnNYI4gS>mLd5(PwlNnl}5YMS_os3(}Eglwn8#~thOpeB6D6w@f%OOVLdNT~& zLB*w98{xpNSq_c{FUS#bj)3?|?bK_3o~+_M^sJP7PNkbPGudKmhtwwqQSsmVN- zZxc@sA7u`Udj5&|{e;FX{3rWkIex4r8JHX$j;tA5`*;gi%A)+)zbkARH}!bFooLGa zGf#f9zW2?0t*S*(P_j8DRo?qY7aZ5KT&$9!1aU>-J?;8M2_mU2Y~-Z?f2~a2Z>{yW zS%kQ8)kF5bIN(gZyP2?}Q8jlL&I?C#!(7{0@$KOgys=uGN&Wln+}WJVf7`)vHAUFbvv)-ft@2jq9<8mGdW`A4nZ z-GrF!QA2=(_263P*Dc-DRCqA%q)H#UAM9qH!p9%!?~=Icv+((vT!kpDx1xq zMK0~FF(@Jh@3vfnaI8l_bb{Bo9%3wS?<$#TFBt0{SYO0<@@eLx~lIFoCg2L3$%v4 zJv;0fA3wXgusy5UGj2t08#kHUQM0RCnm96UgM4M}i@(2SYgpXMtq7w#le67Om&a~a zI4_F!aYK^lk|D|S(LV8TJzIy~iz0aQ=9_UFa~|Xm;lgdYqn*F2u{M2}W2_vEJ%wxI z2xKNj`RNZo^cVS9`ZJdvN5^o}t3S^CF^8K?Z1gE&lepykY~$FA$Ff7(?CGejnJR)E zm(H)2v#cu}`DlBrMkZPz(KP2eRYdX-Mo9ceRFvc1W2 zX7y;RtHFYss+QK;RTR9r&Cm)Q3{*R_n9|Jgk@|g0u*uLO4VkWW;X#_t%r;(jR1{oa zRk}3Gtsd99+ryV?GV@(QR9VEcmcgV&IjW_sba^}k!hN|W`T6JYl^X7>Y7{-6;ySpk z!dGitCSRlf-l#aP#ir)oRov~<3j;lVF@$`Lnj5V5s0(}>S~snYec4X1iBPp+UeSIa9t*m^Cl zawWD(Kj;`aUf9U6q@7_27T46_)m5FCn`?7DE9Kr7q-}Z84yguijpWGCj)UqEQ3`2% zIr3w9?khF7Ql*%fqTuAZ9I~lIe!1YEAhHK-Bo#tSIcX)R4~ixD6!1<)pW?o_3op13 zgV5YuJ*Eez@+F{bz{T)>GSvzxo!Z4{0dDpuqxLg4yJodkMu zp*q3#3ahplYsUCs7kY3S>)s3R<@DVjRkn=81M(xUdtXOP<8n}dyt(@E zgU9OzP7I6DB|12$YE4OwV40Gj60 zs`$>K#=v|^b3$Jv9GdrBY-_azepw{4S7y8!1UyR(B49hRk?_op$+$h;X}S|%E~#-f znW6FDxciw)^E|enDgc*hI0p+Xc;OqxbZU-X_&Utq%;QUj5mPlw3wUfiv*{hWD9bHV z!E?4h+|49*N_EAi`|9gaKA#sn=vcI=^i2%x-X*ZVP2Nr#s1Yl^jyI`e>AJvFaTo~E zRMC$nljO5-RCSa~x!2BoKj!&I*;Y1HHQQ#G9y@EesSV2}PhV!x3XvMqox*I^1?N}S zwYrc=XsWNT6AmyE3NAkFVuBpLD84MIguyF4D>YaOZT%_XQciDkF_d(}1A+2EETF}` zx#X`s->r+W1Jtx8e`sjHiuqGRDeaRwKr+t9lKa1 z6#RG?xKhQZiv%_2efwiqZ$84fD!-cFd85bjhzK!(~nI%0LDRxojbtAh~&Zj0g8!PXq_r+3-7Jbg{?vF)TZB)uc zBAXPm3^Lm|3;9Y}g03&BsaOx-73<9v9q@3Uf)7J09*fMj z7(G3B^qB9LbL|+Q_QB^tG$Rh zjoI5lwbss(bkdkQFeV@3QOb1@cUx1XyQvh`7|hHaSd~=p$97~0zC8^xlDi*y+|bCU z*zjO!uS7I%{M4rNSGAGVx&9V@;>^AA>v11Wb2rw_EA&|y_i@K0A9_OG+L$5&!VyfW zCWtxY3SemeZ$XYKA~)I5M0!84@ZL7`UpNrNjx`JGF&a0s8YCVYj|uGv008=ri6I@? z8m3Y-tBFaOc_8|bjX;h{33=r?T$U%$|9doAxNK32m}1nWZ+Rmj^c4V3wF9M8NDir$Q+!+Ewc{{T=oymlJh(lYdgA7K>7hf>$wCVLE`b&fH zDdSUzZKTtS#RmsISh;!Q(M84g&!EqJ)qEd8j1zfSO>UrZ8MPI2T!7M6C0Y7W7}m!8 z@eCsr@Jsb-DJMx<7Ut*-F3P0#uBv0giCydUnVR2B&q}%Jf>F|v`P1R^@mr$MXyDzg1+@m(zzJ^80f(cEy`G?o1Ge(Oxc&ZuB@vKi8ZJ=kMq*Z$>-4XU> zoVws{E1IrYBTEgNj#{df%(+$=1Jd(29niS~5CQeQt zfMkxfAK4%5_Nf=)edNN&46-Dts~4Q}7mT@JjJ)ME;JxLU!}&-@-*9H(kX~+&WR~#$ zL)Ar2@QrD2K4-Ey3V*Z^ z?HzWoq9r2Z)^?Mw3X@9~nxUXxW7Qs2(JM=`(a?%lnZEjZpF$r(qdjf@4`Z&z8C@}Z zcprgGr>vB6K?(MnzdZUE_}FZ{fmkAL3=bl;+e&_6T|8rekupifBgm#`xFE-O5QsUf z2An`oXPHIpD}iS_Pbv4K0PV_DPco;)oYCpMvpevbA<41tA-c>mZ|r>aiq16PqyZ;+ z(4|=c(>n&)7`?()WLJ3G%f}Yey|ha}a`tIEFDWd{g-#b+Z>YCwlpwa5u(OFLWL8Q2 zq3De2H>w}vW0sa-ao#^8&lNqt+rV7`;8R0?=EnkUNwF-@6#D~Aj%vO?oNVa@+^$W$ z0h+=DY{A3b1>{~YGc_fB;zvt9zT!Ifj*lumY!TDdK7OFwG_@>#Lw}na<}}DvdT>2A zY`z^1ml(KM5aqcei=Fyg!TB3Y==KsV?xMdxQjW%6oUjghW>aUmPgJ^gw(;o5I0C6q z#4C1MRc~h~<+Q$9J`*orp+bcTu#?ry)^<^^Pj`{mT>;C(f?Y*KFr{>f=dj>FZD#;U zQw;n$+bHWAtdCS@i|wXj8KqH9efqQd*c?s!lsk*{q3BnJi7B~~CU|<0;@7|}inyRf|-}~0|UG$0| znc}VtZ>0v+rqax%8bcyT6BX{!dOpa_5WEMPrrAdw1K z-6h7weV(5)Q<4?n=eujwjH%bLG;x=z$82}=k_1aYl|KjwI7HCw^ZETn3@t8NaC^ib z;zXKV7}wLCuqv~}eE}-5>DeU1LQuoL9RM}2+O zrq8o!Rc|(u3g~m6X-%Lz+cl%$#$J^crbj=!347%0^|zsJp*FMnYq^kDWT&Pg^zBpI zGTt-U81}(n!sIHohka$-0qxT#gn=?Uco0`x)5~2 z#)+R_i7hALdj#^pID9J6R@_St2xI29VSGW;Hj|zRHK$F>a{#o5ncog!0dI7Tvy(zs%Kt!N{1L=pFcz>UNdyDNS_* z&EJ?C=;edNpNjtOqVJ~o+tPe-;PDFB`la7W&d#4{RM!aig}YGQ%p0Vq17 zn%nAGDc8E8;bVV>t>Oe`@h*;{+)NOB#*I|l`|Ayny6vwp^4?<&(|!7|e1GsnUH!RK za|n+;$;y4o@(itbXm5j#O`)o1E4Me^k8Lz!V{MXxxncv*BqLj(Ravf0jtRiT8~I&0 zbq4&TrxrOY4(fxmO1aO~;)*N<9|KpA>{XSCuX6|BB6m}_S=D!``^^26eC9?%48I#2 z8r<0D#y*`Go!lPh=C}#rn>FXUeH0t%{`e)OoVy#4?7hV8O}t0nE~_%hihQQJW#~j^ ze0;Y}#{pD+yn2RV=Uc{iZS^KZ({O<_eVYX~qyAYMzX(Gf@HF>!w(-Hb=2bkiDtWQh zq<`}1tsf4!`Uj-xftlugb!~?v^m!Ts5umL?+!_)0$gPXQiCEuJ+*zbaR6d?sRue z6cr|xg<0?|+t0Yd>n*}@;-ljG!^vecT{h=y`dNO*J>+JDZ&PR8deaL+c2mfjDI zLLnYy{q@c!>mb9Y%Tda0rZw42mH*h#_);;2dS8ns>VmlydIxDGl}ol3G!izvwPCJG zH^W*r^2CuqjeUk|8t23JXV{UA$?bp#_WcG!zgzt__pvW=1dZi7E=Q_(g@ zyE_OVGgNd`w0kyJ~;E*TxbmSSHRd$<>? z3ahtre=1|yOei-jqI@k@aDa&zUL~8)yq(I-&BEj+M=NXW(=!YQ#G=_?+_P*jyM*pV zX5ioLEXJD#7+=Dg2xLAzqL~-{sb|Ori8HzYcnq;4zA>3{;r|AZS@3MN+^UH`Pdr3<4K1{fGP42E;gy{hF^sJO)KSnW; zk-#qMEX%#mD-~-p)jIn1ufFfc4%0MSb=0CiV_wV+K`H{xZSXQe2OZsK%;*v~?MK#1 zI;oGcmV28n=*+kP$XlK>B5GI^16Aap4H;!QjzrlSU7HufpkIuJ&{ zP8S7k!{n)ClPX;+WaBq;;X=J?#>7&NJsSQb89;LsWXkUE$ehPlQmzgCnQk9)tk4$p ztEqY^FtxZXi&(K$mjA$ju54X2=VXddNCr|$8(N=!FJk0YDs8O-t`{cw!StjsDX5=L`j-4t5?4*(Hr==%%E+1kmYQLA~uVP?1>S+)|K{EwPiZ=EmsHWB25%pM}PBD z<7N;DHA=#UhI>H zXmOupadc&J20CE^a7`nHBTeI=DetNJnBjhMdic_0tWihU)!1j!A|B~tk-U9$htWOPDvgf>~jhFzFw~(pynf5vmgGB1leMv*YvN3t1ovjrkpj6o2u!%)}K*sFBh+m55iJ7|$%4BcCWyqjQ-s)>V| zkWU0aQ=s;}3bV9ZBhJ=DV= zxI*Z%>x@JqQw~YL)Gi=sn`|&@s4Vvj->s!FmMSQD5AON|v@48^H%q#IUVXc}{9sW- zcd)uoGF;ipOngS|OwjUDw;MIP6-MT_)OoBvNqyX>n|=_sBKth6%4%r_f%5V?z1z*2 zsV}uyk3jkNdugW5@v6I+x!s+)ghyo?*n*gc#$w9&&0a+ztTB4sn_Y}KN{LIY%cb0h znlv2$how4XJ4YPD9j#nD7)}SxLz7G(`jj7doVyBSSU0!=F(jj+xdyc1bk}pq8GJ{q`abn`X`ns zvyy^cPx2`WB!O7w1KKB8aC|7+^_qZNv0V<3;3Me{Rm`z%+u~|Zme+R+o8)3$xxM^b zKBtzNKGH@e;|8fwweB*MqSCe)+D+(mVxpYG=aDOhWZn>1kJJC(}PZ z8w5-)L_C~ibpL^R_%Yi^LtoitIi!PR<7~9+AVD!=yS~f`XV@`)`Z&8(t~X(uE5s2u zNY48%!gYFz+n8PDltoC1^WxP+?Iw)1*4U7~%pw9YYeukar8(mD5aP1@-4t*FSDWme zYz}b3NQvZ7bTthaE-KETJO?gfDa~=aPSA1-`e&DZw!F*p95*5UDx8(HpWkiA-S(PO z_-YaN!`$^alP6H%aIiA9ZoIFQ+dx=XtBa{Xh+4{W0e_pmw&j~G&b1*Y zz^-9f=uwvqfnO6Q3rXRUiv}>-<+=KsjOC{%zuE$U|31h%YG)Og95q|OHE$Uu8xgc! zOxI+pDzYq{8Q_9=W6=D!hQ^kc)EuN6G0+>a_f6OKvJUZoMLFSXfBfns_uRD5`wQgr zXV94}5b%;t(E(L4M}*l{DuI@`rRmsQax4r-ijfomE*a@0fM7d7=u=Y{fP3tJ_03Zo zuStjl6b+;%4Vg>1=|T?KrF8bekadRG)rdp|4?Itq|Yy=->UZ?a&q>tz`+IAN4qu32H!&iKvs~p(Hp0&_7(5M zE#}zH_Eb4y!$K_hr=UBsr@G7Z_HNB@chx+xYo(Sg+xa(BspX6mJ3(i7buU9?&gm1L z?d!0p>9k^S2Hhay}>@{;k*OyIs5vKr#C%3zcs%1s*rmAaYNrX*a zQ!M3#P2j9l>h5%G)zhw@IP(d*EWT|nF)bvwpt$3^Cw;3H?itcC_f>J~He|7iN!he4E)bGif&r=n}Yqx29r%c{OF+AxpT^8@+x#%a|2@=MyD~dRT z-t=1NdbwS^q%)koenNg6cCb&G*(3AEf0XXJ;OP!-f3?g1{LD@%jK51ycKJYGMDLF-R^y{6XG+CB|;iDdy+2asU z>2CMH%;bUUn{v#RN_NcxfH6#QqOzP01R0;wo*or(bxW$t(KO0-YM=U9Z~7$Lh;wv_ zJ~t-`ylJ6YE;&X!H}K}i>ceuAM3(HUO-*AVf}@x|_6ch`b$Cg2ZptaK+&q=e$u?fp z_ac-H7)TnZ^P8SeWMCOAd%L?%$@ZOCW5mn zlTGJ@-@E6(OP;UH93QXdI)-m29T!K*Iq}FnqU7wt$i>lQ*KrCjbd*h@V`Z{tE_}yz zz4N8JPJAcniW&V4_h;^@j(0TcX@9p#?|PXR0mU+df+MY47j~>%d}FxWJ-QWs36jH>xFbMecEDHe0l0}H?td?;9G|LT+Hbr+6hU)h1TFMD}FLgc(J_X4dwaF&#O7~2Zv`emJ^19BfU8Jwn*_O=? z2!#3^uQwB#u)8jud$G5J2vM-eeXP6BZ@!i#)(oF<>xjprO6e>eYyhgWjwWTPKq}?% z-@~`nzWS3vGw891;2KTd2S~Z9y*+}AZ3aH|)+^lEaDvxVzB;(cdD6j!ev#{t46fYI z`iwm(4{}VM6&cGJub0Ph@6vW|dv{>89Z_l-zHIw_Ez8C%uxr19 z?pWXZUoYMJ(QQMcWQvyVjS&x&FI|@IBw;FoOiO2Jjh>b--O~T*(hZJ^_<2JL2pz{9CP zh3VKU)moU~*|L@Wn^xAZxW@OtzOtLLvS&ta)8vZeXU2F%H>AUnVjUN_gGH2!wW<&y zJL$*Df;mTNN&jL?dQj9|OM0-23D-0VHh*Gjg1@_=agzh;bl*z9hQ{lSYEF!LM5aSv zzMh|Lki*Q(*Ct&Xt*;OU%@pxSGoB{duW7bcmAOD12E%U>%Q}~>lCZX!fcQl%n^g6! zdMoAHs1&<3LHXU>g0ODho>4U1@!8uqBW`TiFuG-TOkrEO`SDue(I;HcJzcLpdP@{6 z)^mPj>yR=L5Mmfwo4U~%$xANxv+nhl&-5NB5?t`#UpsFPH475926&Z}{B-@AbM3X( zOEk2tFpRQ(f4v>#%;^DZm@>Y9wvLanj&-(qwze>-DZma|>ydmy|d=)euDz2@~yS8|d}KCI*(@{%{-CQypT zUY=hKAMV(id7Tbhk~vn*7emI(nGG zQyqQFy5$JDm9Jz%>D(JoFNR<#OT+4>P;R z`|aQUq`32Zef>*^y@XnUe#<%@sNm~E7QS1ZGcJMZfPHuDl(ADD_CbD47Tku@_Jr!F z4yjq|FR;+$jL%|q|%>(;I;0@0w$a5h`-o^%XPqN8akA6KWXK6ZD@>U z1TV|aw!i+bnYD)?{f2sF8$B!KI;wOdKGZNG=(F(EYK$**2lS!^Kp+|c;CyP4BvNxs zl(dU^u9CL(vR;5=_z4u7>T4XO@2i}K0GeSz??taOgJjc<3ZpKVnCp#ov>UgtTZ+G7 zHxTb5IW7qW`qmqY^zz8 zGo@=O64Wp>t8wv)e4ntV?+Wl>!h;{U;DHP7(E0nMLCaxVs77E;S#Qy|He{{zDx5|w z-LXH_VW*_V6m5H3N-gi$DdlWLY;V!u19y6t#J&&tiGb#48LZU~8L%Y{u#Jhv)V#JV zYh0ZyQlqv_e8+2k>nP<`s+0@L7!L>?xTl1>hNrFVj?Z;e&Pcu3>2vm%78|()49a-Z zM)84Y#>Dr}b)s66djiEK=~ZBrdX1FG*^w9D{_9A6+6<5hW)%xAw+_fyzxJn;5HBKF$=O# zaIU7xq7eHId|B=rAmUSLz0Wqm!l&u}7wDazb#gUL6p2J5)QtB+A++r z&G}a~P|B^Z(pA~U$D`okDsIB637jT|P1WBJsk61Q0v_io@e6B;4*q*DQ{-}}hDkJupZ^aHjt z%I_GwIhsQ)Ydjlls;~DcmSc@|x~!>Y1xh+IQ%Jc+oA<%*G{v-=a>7zBD*{|D2z$_a zBXAfwRcgRS{F%>P$g~?nW<+>a?cdFa5>W0?~9VA~Cjn0L8Lc<#Gyr>g6H;fHaU92&Y? z52fw*Z3YGOEDcV67;kRP>Y15fEw?Q!f?IVg=3xN--T8r#JyNd-_TR(cd|@J zMcwm5LC${6KzQHp(WRUQBLDEk$6ZVqh%P^4ztYQZYd6z?%I-RUm8QH?eX2m&TJ2eG zXSKowH<*{;nnAK}H6Ns(n;aIpn|m{tIn}mZkpN3jOT9>8_GD1Xt;kWYi+r4R)8xIH zP800fs9;->r8u*_R+h7XW>JvzSzbX<~UBI?Z6M=i+~pdq_W_5AoK1#&La0i+XA?Fw4}K@#a#_rbKf*HuuYgWoT#{z zlO)izDo1VY+`*y5N_Z(lc#Vg^HYJj6^cRj3Onemj&sbns207p!Ni5BKxED9)aMb$h zcOR&Z(6Y>Gz>evq0Y9yh27FE|Pf?v`;OAtxpK{;jr&~YQaPC{t2RpzDcr+rvcKF%1 z(Uk3?8}~c+NSmx`zUTI)FvB*0R#{zSg@|AcmFMgdu~sSoT)Ts}O%^y3Ar1BMxcQV8 zm{UW#A3?f_Y8sWu2!+r=6I4xzE9Ji76}^3Sbatezn+9A1iE8+RQ>7f_b_tahFn8{S zWaE%K`tS21mz~Ga@M)`MvWB1r^m_nBz_Wd8owRmV)lTY_@>%#>#+a$NOoN|kGv?;iwnEL5f zzaHZM9PZ+@8|>M}OYgt*em}YXvwL3}%k=3t^W0FlY z-3Af!nPv9Y$O%yt=aQzs2OFf)bc>SyD@-vY^qBJB0h#Uv@;{yCWK6zo5iuvO-L93L;(6iw(bI0P)XKt)EyC!7WREdXU$@g?mGKGgnef%VZu9TXdMfF*S7q{b z;qtKWYsu!FpE}cZpH;bEykj_ST-I%Q_L(!VRL2)4OT**(EKRn2YD_PO<+^nA%pZet zYyCDZ>{#;8`{>vYe_Kxv|H01=THt42(ya?#c8;a|zw2dxyee5PFZ*P^c-IiR-7zJy z8$%x4wgLg6mp%}Du-6+C4w+twNrYX10N8E~5)O8!8mBVtf|wl|8c&d^v)-quqrxDQ zO8*YA1zpvcr0EhL8=#`%`gu*XZ^Fh;3DBvX<;uO`PEL z4sf5MwTdy!%QoufN>x*U89KI-Mdqw-;DROT zdWS&9-S{G_X&#(Dz!-OV|KP;&^~a7sidixl21Nob)duI)y{TFv4k!HUudz1U^uN#1 zm9#8#t(E24XaqhK@h#0&|M(s-(Oua+**W=v<;rK=w>J|?wo$w_A?I#T_hj4T1`E{Y ze7SqieFfI8?*_P~vkM0#lXk3|pM15{eWlnl_l_G(;pT2jcU>;|Iyt{*-RxxWA~(1? z^SE8~%o{o=`}x;MB6}M%-cHQ^dm!?+E6TiAmb0z-ea3u%PM4W%^g$Nr7i$RfHMUaj zbM2x(sPsc#2yWcFG8b`rvz7p4#9G}(uN41o9h=CPEWj~pMCP-KeX_jz9)Byk>sc|k zH_@y)`gly3yn`E9mDwZiY!?iDrD&v~6RUN7JIJ3}d!*cIbMk7g&ml+2R;IX5f#n0> zq{1Twt8K5+`}9kp_V+59MI$mc29N!z66C7lZ-s10cA2BR0_g8P4fS-RtUfV=GuNlL zlg~CrSX?1Cnyc7ZZGH?Szdt$urbPp|FMizZNei`Y2gFO14Yh{|gk=c5mGN zZI2=MewU(qbjAdptuZ62bZb0c?e6%V-U7~#`-%7kA*>d7VS|w#97BbxAUJalWmamMd(PybX+hbUV7&p>vDK7~rl=2Ix%2 z0Xb}IgQ_#}Vfo5<>(ISbc*gUFpH^j{;J2aRULZ7~5dVLm-V8BrSGf;DcV^j{lZ9^R z9J^z@Go;}Swpg1|5-#PC8e5XE>6o5`MX?aCSE$Q!#91>jDz(ZYO3cOFK}N{l_<$9rNB+-OgrA}P z&o-{3-AqacftP_^wVg^slp_BurY5WjK4tIsAHCY1ejSF$xh7NkyR&5VC)|$4%5pNz zo6p3;cghLS-Kjb{=x%(5x%$V=!|PM@Yf$Hvb6+|4bwvK56_bGsLk}qnV~#0eUqZg& zchR&VA0a_Y3$ zv&ETQpw|pFOSzx2jS~QxCIW5|_A;(_OmL$HClm=L@F}Yyu8_$#KIWZY@6*1K$|e0z z?0+J`5>N&Y#slPe0`eX8Rzt!CRRAdzZJMEHrJRKY2qh)Da_mg$3q#Bw2Pj?D9+o@i zt44LeZ$m39Pbq(Uo=ISz44Msh&xUV$^Z(!f#{39eEE+^N_|#$?OpyhfXxXQPh7YEO zotpM(B|+fP$$>odCdE%e$?s!64`VGyKgWeYJ*8-Q*#cA^!RtW#0>t4rdX9q-v%MSoN*AErgrSs)ZySRm!3V z$*Ww}uubv}OZlmqWV6FH8w+XJe1-?EQ!l<)dNkN-yGM-*;LaY+%5p!e)y?T{yuO!q z3Fkw*CHK-Ob+5sOk*IA+*LH-#ZQLtqM0r5+gMyZdJF?}=5F=b4$@tW!4tpk=6ugOW>3*YAGsYkB< zv@xfRL7~<^Mcg-yp^F1d(>z$bm?MG`y#n_tpUEzMXFKEm9A7asTvWp?**k^b)bPcP zrNhF-HACZb!zG|!%hJsX1tDdNI#X6{r)TL`u1gxhQa&1OwFgVzHsRc!i|s;i+An12HwXrUi0I+?M$p6t#jDCXSaoKDmQ0>I>#UFG_1|`=@uQHwN zTR)dxC0p$O6gl+H{Ds!+Wbz1~Z@x5%yTsnsnlzuIcL7@n4~mr8;U>cXt!vnNKTD!D0^HqS0pFI zy-JUZMvG*s`>^!1XuC-6ddH%M_rs6ArUDd z=JyQ2ZR0aF014KF|BH$#tbT~6d0qGOh0vU9o6K82vY4*W)Aoi#Y^y<+Dt(5;BFg3r zt$6V42Z__!h;Pq4bJ;T-V(X+HwZyj*Hj3 z8?z?*NfG?)R+8oQPEg7!unGpLh$k;4w9;nN#|^n*YGKbP>RFk&DQ=53EIBZ_FzWTG zrs{NB@r1k`-E+Z+B1Rq&kaq4>PBSvmf#k1%JvmvUkFpe2QGl1Q=gRz107MNt*_LR`*{0 z)r1Lzbi&ATSZrv;!*+LrzjVR%cGpI4aiv>SSLQ0{wn*ReEuP>zW2OEz`w`D)dHwa)LKDqR?c)i<=HZR4aj8j}|O-!*c`J#MYij|U)Zqt8k=s0&w^M@FVHScm4 zHa{zJ-@;D$>7}=g2`5kHJCz<4FX>CwisrM`pRHQ@Rv>q7*J{_%owr$(v_{woT>WN|+cG(?C$G+nlJ3cnpCgx{ z`ApL1Xa2h(T!s<%jrR_d{n$=l5@r7;YIqy=(WLWEKJ>*@?pZ3e+_#WY(|aZczYaYk z*ka%D8nzE@L3c0?=Xr-kqR4uXivsr`SHV@S%Ct&)(cui^FP}Fw-hIL27d$@r>1b>; z)}$t^T=d}sqGbRo%WW>s)<&gWc$;g!t1_3pLa6!C3N=3ldc4*>SL-Gt-|uO>MqUcf zTw%{Cb1mi8RVmEXt5kZ$#9S;maRW>_t8x9H(I7em!=CCUR%!aVY43K*>Rp=fvo=jL z)MSVHx!^C zA-`@DHbDUSDNnZ&CR&iQsaWo9Egp3aWP4$TW{1iC%=S#ZhUSP=Z=Ses^vVVTrIa(- zcT?MJI@|P--u=|$nBny0U!_F2wZXXMXWl)XUg+ab0Qmr=iHNPoKZ~x`J*N<&p0>yB zXRY%15;`TiHQvkW%{h+eS_wuIv$Y00fJ(OHuc)pq`2^tf zOAg#=?oH}9tq{p5C`lV6Hv;+8uC+wp^Y$InvlM$BVe`}oTIZ;5Vzyu}%UN*X1{#G8 zU=y~icv1J{1$5`Rf+gF~$kj!f3NzmxM1$|Eg+*DCIePq_l2YzFm5$3cMm++z+-(*{ z$)z`)gFTduystLy-bg+mwwcA|(TGAaJ@R^MniM>rllmtaC7KkO6ho}fj(o`Tna7@a z`T>~+chK+-p*K`RARvm%8Jt;pC;YbRIb8{cX#a?*BtE@Unxq_VaFC!sifOBc z#-xHGA zk?)2~6@ONhIm{X6GbZk3{+b!0;^2lxvRmq9Z&*9Xs7V8r7h9ADLVluhEXU=!w^Z8J zyoAZVJJ4Y+mqJ=e4%9P7cI zjKx@>ikJG!AJ$gGd)Vh(K~-b5QBekmeKI37sB6P5K-pVp6snx&re_=PIml2wNZLg= z_j2n-mCw;YJAq-Oz;qEM?b8}PCAuy;F9KR?4|Vio1Aj}oudRh#{i#vVCAzT} zpXvPw*;4LfsQ~T!Ro{fjt}UhMOPWx87Hpd)%5vI{=%s<}Oi? zA7Waf%Im9P%dL^C?;n{3JJ<(Ogrbg;!J6fhxbH*Z5z~X$tA?|YX_xfZ(2)zIFj$SxX%S>0aHD-534|6cBojQVszrs>hO6T*EdIgM(}Hb;MLf+;z$XHQyw zjzwhfBxdWE98It?QLuAWrsq=EKXg^Jysx^_&g5fSCKGDed^`=dCp%{Y7ea|dEI42S z`*EV`5G}3&*qs?jPj8+x+&>^Xf)6)iK6T$yw;Pr)bepTao@K6V9IycNrp{UvZ9uYp zyz>}T39q2?4z|i!zPK}n5b2H7AHtS*AvV2B%o#Im^Rq=Sw{g{+zn^@Q&+$250Jdc;~wluftf3z)kW7gN>U7a8sfl*RZpebpZs02hZp#qw)Fm6dW{s>no zH&1nzbC-K`LBwJUUenv*9-IvGX{@IkWE+`G&3yb^Vb|&_xVe9LdTW24@_%z{zc9w% z+ONJ$1X$jjTda3i+O7Qq|5tA9zuS#Lc^w#UE>-^p`JCRSbHD%0tY={D=-j6-!Yg(* zZ1J37GmRU;gjIsa`PBPjD>jzQ(pC+1R2!w7ec%+Cz?O4-x~N-exX99&Zp9&Fp$~>W zPZY5+xdTQVVrzxSYb!QYwSS>mXtRFcIRnpO`s}MwpsimewP6C2@k{Yv&p9m@t8@dC zUbR|;OMG3e8>9rRl>g@2#){%~)ne86jNLV9A6cJ@hMpRTlFpC)nL=+>U7!VUilG&vm6NY0B zh|VBJJYqsc#RP(~5kxV91W^pwYg`-W{{GcF4d=Y!-tXS;d3hdcdausi)zwwi)zy6z zS0N8ece<1D0MH})(@tqJ@hX=wtBu)iRou1$4W)R+vZku&6J>%8*5DgVGp4p{c`g-F zn?K~+iI$23o@(Rep=p$Ap%4tNhoZ}5=?uR4)}my!&{On>8f7$amC?|I|HGW9U}>pk zv@CUuRj|)QEhRqMbclN>EG6S^kISikZ0(p5$U;lfwdiR@=(Gmq%~1FKbUzckYHr&$ zFt?5S34)%(4Bi-Zz+i@>MDNMDWQgoO-nVui4+xFRkY5k-+RX#c-|Yofyox;)Q^CgU zArjpz+pIpb0eIf9FK0YZ#K7nC7} z4_prA@N9cV1X!JP$laI3czHD~_l1?I=iT!0L1BK?qXd2Po)c{~wN-yDw3Q|aYQT_dnkABlHhE~+pJ${)PNqTstI13q?rq%IHE5ncJ z$hfk%rcRqWjUJ<8lf1y90(PkxIL#@o6#4~ zdyyvsx}-^MwgXYK7B5lfnPNP9n~&7HQ=(%JCu!o$H$(24C#2g zjLdP;7T?i&3Q4s?c;ISU_3*s%Gi|XUA}4Wz6?#*LQ#>nXqMb2dg%EBp^d6Y!^u*!7OV=vzJn-)2hg;cAO6~JOo}%q zfDXN;YYVXo&<<|4zcR$ZZQiYhA~%~(Nbi;y(g-!Q050<6%&U!mQ6o%&ydqtDl|(## zlbrP*xSPaV49gP$V`4UiIgkm9QAb;Xr6Er%)w$i&ETffl?tfELVEdYhjZA`=k@XY> z6#Eow3%V%+!q8|b7lR;rk#=_Vw9_QEiO^3Wl&7D{T@0Caz@$i4+!=Z80JJm58szCG zF~S?K!`awVPdtD_Ni}ec?qi@{YsN1c=Eg6~&gxy6n&pOJWe`8_$MJC6XNEp;_7m(d z=`@)%1tsGp!U^lFez(vdng}B_EeRQ)X8Rvwp$nX(ydJzw*IoO_wU2O3r?p+bV-G5- zNNky*A^};g+n{9*F|@j2o{6xt;UD#LnLsi0onkQ>&xsL+PZ|iAeB;ZY-cVN$Behx^ z31sGZx8MBi;r%LkykIZ%$FvkN3L=sbMzg=>#E)WVXQ5WImd?YC7=cKvT!=#s5t|&n z&?=LM7T9Lka1XDbv~*o4nbd|#$`S9pkhx$N5`6K_ye)obTl$=YvaD5DYYoM6WhsV? z8Tif=9!nU;6&0AHo35d(O`4~FRJezJ&3G?urs?DO*ff21kPIzMQ*I(%ColiCn%jog zRI!P`rjD{5s~B2NdXa^&KS`;YzGf)!8-Nsx=b}f`A6lSdR_gPPs*%C(I&!GpT_oca zYU?9Nf@g%|m;8qS`|V!eyIfWRkQErBmv^G2|ZEg-jlQ-;Qn{*@szfoVy@6H0f@suaH+r?3JdtX2H41c*H{cT}W_!y?K!= zCK%;LH7eqz`u&T?AcbVvJxY&T8n?e!wnZ?tQQMD-kJOjO$=Y*TTh}Pr9nmy>X`HM} z{f5Enr0e%}@KdA2H`RA4es7fJ-DrI9b27f@+h9$j9PaF>t{BlM%dF_q;Fsi!9|ym# zpJP(td!r2RM&|~x~XR2mYy;>(-6@mr-VG+Km&WyVkLY7Q(f6b&pj)HTPIHeI zWAe$}+tGeSoNAO_*My5vg$(aRy$dL{e)=s6F@@xBN4pg9Y5nw@7NZIo9*>R?GM)HW zlYbd9d^2^hIFD#K>yi8F@6ss6-?%-iPe|Su)!VQ4UH|G{$?>t35-+p3tLnR8aB=Or zCscCdqvtStjXsthIDbT!dfeQTU>i>QgndZUELxu5l|al$jB zKx5q~VU-{MMhVA8H)&NgO1A)-$G1l5;@rfSCS2|({^Mvdnv4F)_~UqudVNmRdPZRTR#iY&qq(GN%`oVSb+R=m6rWsMh1gBI{a*XlW0!xNHTuM^|B4Bkf6qOR!0j7 zUVF^8xI+A7x4TNr$7h>beT544!Hk)1<)h<$aaYFgLVWzzx--?_e1h!3>-N|0ye)Cu z>Lt~eH!nEYHrPVsHS0cA;(Tnsb!RlnJK{|>Egu`V`$j+Wna8<)YDqr!ZLBKPuj;#sL|lT`LRF50IUzB}Cg+{~NM zmuq^ZKR#>Nu;&Kj+$TY%;jijpdlT)BYuNA;D$u8O=l6Ky-+ziXjRwa&q?{elw=x>w zHua%Ywi#A$Vc;s01Q$fN2Tu@aK9{T7ro;a?`sj?Lc!@=?bjjI*54k*d#QCkbvjs${ z*p~GZf^G*PBbg+Q@OL}xtXR6HFl2TWuq4zH#;{>2LV&;ZxC*B1^{M5EF>r|RpHlRg zXd&zOL@FNbw(^bsWvVqjzTgXMx*K}BIuMVW8}H~lkdp%7=1qNu>aL9`@PH`Sc?gxx zY)@^AyL9!+^}%#zuKHn}L4HZNL8ns|>KK^)miT(S_o;iIQeNZq+t41L7s&@z%Npd1 z_42c3{$Y0zk9GaK#Qozzecd4LfknKaGcJ0#I>Srpo4RB9iQwW*#9B0+LAQ}7J)Kbm%8H(KYcl9mvm=Tw@aqJ zD~8<(`j9sF+u)4cu-s!qGWp!e=T0pQLut1%HR_P>oGO=H2e@1lPE)s7D%Pd#Gj#5I#gnuc zQkN9t8z;!QBF*zD)Wp(|N|NlJYo^;bdtczzL^F<=691fiquhP)TCjc6T~PhBhI302 zmsL_!ncV;QQ%H%dC86euNGQo>)ck0OXzwz5UWfw{4{tNcGaQu|uF+U34K|1F1FtaS zKo332dZT4z>(DgM8R}8NUg@D~OG0*_gARjp_w4u44uQ@7|6y}|L+wqZpKEGvyNFhT z0ts+>>>DGX)Cm(X~hQB78by6okS54MFV>=JC3 zbZcCf!jDwkeDRw>TT+kUcI9co70Gyf^%#D3cd7bgfEjW4k(tSrNRGWSwJDS$V);(<+3KVb-3>(@m8v4 zZH8nJ=A2PPF{HzhhVLciOQ$M5i&y4eK4>L_2fhM$HXVIK>-Y-#Iz~4_lq$TjD{L@$T-H zj@(_kWjlZ2sbB7IJYfa*8z$hS+I7Jp^$LO;Wa16x11qxcuoccr>?}jc(F7+ zwmT%#4AYm&tqt7Fk4r+PrTR^EgEZm8A&R5FnH@?6%W*#K|B1^?q5`$oo06SUVJv{Z zP^=!nLYoPudB}Wm<{lc>CEVi5?0Ion;GX{pTu;+pn^!4+$F66%gy9Y)dU_9}qD;+j z228;fYIHGVD&OA~LFYJRh(@_p?uFoh9q|;anc~IxL(WkNqZ`Nw8HIE{H3_M^-E& z(2q~#Egu+PULG%xdj*qMd>206u{?gK`oa}12+!?U?%u9GSHTNAmS47aaNYRJ)1$w= z`oZ#eb@kPwFW&Wv71!Naj#2b=!ClYY`Q}|Ucb*EG9OYK%PX8oxFyW*HZ(A53>=wr7 zl#@U5QTJW^v4tnim<};m;t=EAmbyMn5nR*zHbr)^F}F(@dfCoJ=p;ieJIe_Zdt@IA z+!Iu}yGl3YGOt)VJMeE_NzGak+C*5_S^J|U?gN6krCm87&aG71qN7j{TUt=rpL6He z!=?Iau4@*9HHo;hd!|c8>iQC^Q~2oBvh=-m-PJjE`r@d&yDxYzK4OkL;=Pzh1PUjX z^depN@)LHp;33RyT0^7;_n-#Om>L)^U8v(=Z+nd#$dZ`2xcr2P#%kPRbc_Lf2K2$= zfj*mR+1|$S?uqBAJM)wk50(enskW(x6T2Mt?ScH)f%Frq5N46XLXAr&rJk#bi=jS5 zG%P2HT_!YkD-;}fi-jj9#xH`@Em38L&qnV5tf?7Bvp%rK;qU}Ps!GI1d;7FvNL(_e zRE#h4Zzj7O3hm5WMfkPp$?RUja4dSBY_)ZZ6Kq4u$;MdgKudl74b?>HWHq7dY=Kb13HeY>xUdX29CEqZK zotOQ{d{W#zYF6>PtN6_|Knz=HsI6kR6zHUgq)68<%iOPI@2t{PuXq2B+gG2_vqdV* zhg5<0gadjju7| zLiqp0oC;BN;#i()wwAa*hbo4?Qhim5T}XTFwpqFk8E2UeA9E1fjMKSyZ?Yj%b@%W| zwo7_Ag(0osz1HVxk=;G5QY|JTi*YXrHL-?J%hQRE;uS%g_)vxmMpzKea=Zbr9Bz+Q}WttUTN_LsTytr{1g>)DN%N zY(a&7sHvIk;hD&L>bBW*05MI0Fw*ri^N_9&<;9iwAzJngwfW6viS}hS8{gVQ586Xh z7yxq}yHRZ&joDZq8lG1RZ4-J|Ekt88EDzay@Wa@LWQlE~s=&$WoOSf^eNPz)qBJHS zC?f$wJMYd{7*`Vd8~v1UKwSu`2{bfVL^#qkEKCF7BM1o;LkLBz3B`VATS#>DpNAu_ z&5PgIuksT2i{2#2*}XSaq6HdX4uo?uSTSUGme){!Zd>YR&E3ha;F5|fxE`t$G^EnBY%np1yJc?(>W6gA&Zvz4 z8O_*9?yi>NyWw^BV)pg&)JxIm`XGpp{-^s#P36Avp#9vSquKm;F`Xx_=Eg@q#%rwm za*OTx_{dY{$LIaiomZKf8QnfTxZ(Bk0jC^3x<9YF+b(-QtP3wMB-}1h)svvQ@kE`~ ztlvc+b{590q`m$yz8LxzL(S97SlUA-Aw%PUnzMA~OdN-nX=zBd6|y3;U6HBuYkw~#Z%%T5 zsl+>TmL@UDEAYWvt1MFd_c)&|J!8aTwQc+V7AHO(QL6AG7~Tu+|rQvFPIhWi!Xu7 zdKn$;*ATv8?y2Y2*1hPpDaM%CHYr` zxAld)g9hblk|-hj!D&>`TC~{T=lH{(NJMw~IAqgs>-(C4_nErL!yMNxI^!L;f1=j0 zoj#03jIC5wvEQP-YExUZSBh7_Sci!_`UWO8P@~aGRz5o=TbYLxq72J5IP4K7og{9$ zDm)w2ljdT~0JOTFnQj+2+*C?31N>BdkOAm@qn;Y|6pTa%n}^*P#M>&v8F$-3#U$#q z`*NbC-s!dxzK$MkE23!opnhg{(z=;T>o0L@*5Rw|mZ*=L8iaSn&mi~jhAu@pQalSp-rQK5- zT+UU6V#+$CL6$}8s4T2XLKz}tg=GkgKkD4r+16o{X0g&6=T{724dsna?P8O^e!5EO zM0@=03n)@JBr#TG|6?^JshCfM>MipLOF}YNw=UQU@%XJ3;zMkc5LcDr=h3e!nHXBj z{j~+N9BZdU7TQ*^J7@UGa(f4KGM>WB0ks-r9CmiX|B+ce^qcYb#j84F^Zat%jP{m< zwi11_(p##W_5#IFGEHI?^Gagf!sQ`*r#O{>M6Uc)tSfB1posz3s(MoUW zt*ZRh!hGlR}5`JUAn${`(Q_1kbzuyaC66KHq`J@&T20O06BFd0hHdz2!P@IAO!vCeNknZ4X2nWyL?|j6ig!iA4taS}uH-m;^&`pFQY7h5UsIDMaGaDPzd zDy3Sc29|3qMY_E3Pdf4#PZ;YXzx|nLNociPpLxXonMT|YpGeO+IQG3J!++K5+#u3( z>!G~T$ouc_%vxqx`NQgcZCz>a83Wyq^v3^%-rw*8-T6wbr2E4fb)62;jUQA&IZyI= zy~IAx`0gW%`mU>Q|Gw*KVtm(&gP;&ymy9m-Q)YD^XTuk|LBXQK` z=cnVJ*gI|+-xP*>xf8;D;!Wet!m!uZ;lM8CFOQt-s>)OQxS(N!gYF%N7t@BaQN#D9 zj{}F7W!pA196O934KupE5%FX2m^XY(IVR=pT8+!?a7RY18aAjo?2RD)tfAq?k0fAC zIb`psM?=};<5g6jWp8BjKNM3Qb!hmj=PCK@z1`UuwZzB7ksBO4w#Gru#e-c`e)>)U zkD5;8an^}E;W?2LwCUfWPidEo_SuM&YL45Qa#hle+&>;!=|)xiKq~%n)n38GWa`|X zs+Zz`ylh@vHeH8Fs3&tG%ob^=icuNy7lqJdqBG!QJE2{Pn%tb8Ii=hQAJU0`e8$YN zs2@Cjq8DoOZ)oYA9&hdnu^nL~7x;6Ux%nhbk-FrO7`oejUsXm;vP1Dyks1@VsqYc2 z>#?lg4T6etXihiq9Il7!0f zxE@Wz`4+x3>oK}`bYc1 z(VCU|_RNZ&a(@gno%Ke_2D!pIu92=8=tr9NwK8nkR%@~GXs_$Blxb~fcmPy6c%l&p znw6W@ab=oTUG2-kHR(E=*8h3KA`7SJh-gQSfZwJ&OugTd8`h@l9=pgWt%uOBG}DA2 z;X8W<-ThwUyjH*%#PxthEVJDijgh=nn%3CsI57U_u8*;c?6<`bTl*hs1EiPX2kC)Z zP=l@pXkWxL>)ijSGJ&6O3=hkNz@l4Xo%6R&gd80Af@adsYNaIJl*B-QyS#k7%aVDhHRCheppoo9 zRf5tW-?&6{n%f|V4^dlxp#+_=it!%E`!|e=hkVE=O6~oXmLf9Prf=;{X~bB(t&f)y zSi6}hDtALnjw5+#XfrKP$dX+sqmeM`;lo~E=Si~Gdr8P>P)jw|%rD#_mFs3_mjrI- zw>qpIP*ML(+>x;BSkpdTvF;h3dYt*?%D3a4i8w0wvFDqvHR1Tr?CHUW6*L`!a_<$n zHhN}*)(~fMLg)Jz-{?Wx;cpnMFKHmhICoa7_oqO)F=* zLj#u@fREsr*^P3cKM&v?Sl6_M=r=98hUhmfTA@^QWOPS;ZIB(FJvY+-)1!0CvwQQD zUwdtYP%O^gnZ>de0$@0f06lE3Ha1Ta&nz0=JozAzEE@7{BgFKobYX^0fpi-0Pv%K( zc1dOBGteGnQC{q!|(x0M3vUE+j|c5lC=^8IYj;QE_xs=RKi@{Sfr#YeegqPZp3qtCMC z4oSL4*;|GrQ%^_pWVO-W^@o&xYsN`msY6vf0f(A!DwS8qm4vuJG0T!q)oT-SqREeVz`lV*VjL3b}Y|| zp7l|?bz`WEt9}duOpJ#n#DRQiu3}0ND^0r=>j)z=9{QY8dur5FVVSBnGE}L8VW93A zK5`-hUsHn^!cUPe?a0Tm8!xuq*h8r~?RdqDFQXvEs&Iwv6-+-n29R<6+1^j#ROfM; zqbf~p@jD}{+ouQ*f|R9&;P(g`jpD(?co7@jzrL9EA_)PPtKPK&t5u(P94odYBzd%< zK3%8XRigaaci7H?TAKak{;`NI(6%SN>ppsDNJWn`s^5$@J>0yumR);WyRzuv4yufc zi_^kc%tRMuNTtbdhm%_oSrTflSe$E5khrFk;guul1_Rd=l7S^`Q!e=4>`y6)=xuV#GzB$;kX zUvu9z_vOerq>_4&HG#G?glE2uwijBMP@^>zXY?Q+b_kOkCz!w81oj`qAjgP0nXxxdflk zqN;ty7K(>x$dpbzLyl;Bd`YWmc4rW@!xYTJtu(owWoLEtt`y4yB#&626n(Sz=g zz%@^%E+Mu>av3K@Et9EEQ4jRwE=Z=fi@IRrmWglUX|3rCvMsZ-mtp0ax;c7ee&DK; zsVAf7tKDYk2UfbC)vi@CwY|$kvjewzGIgrE#BqYO1(8YDjW%q_*;8+vZWFy#F6Y+C z)Sb}_?xDbKlk7GnaIKT6;jWt-fi5EHUf9TOr}*cHf3h;f5fW7QW7|-Lgc!A(50S_E zFjpT74ZI5kv`ND(sfV`pgM^xIXgp_o9vbrbZ11sH&#uJKr))Ff(W?M&@-;_T)Cu5S z2JEHSAJTOh@Xjbu8HKWZSV$WxWi#m%UCZ{f-fDvtnAg_9Qb7`It-*uaUgRy4v@7`# zyAriPfZj}Wof3=?>3N-8-t>HqIQQ?>gfI227+R=R^;3oj)(%;qR=`E7#3TSAxIdmM zhJ^l{6>2MC_fOzBPgZ!dSSd3+b5xBTe)_D!*b%#=-OyP;PCQ1I=V@7juz=`cQ3Dd$6cb{X}peS zXMYvC3!>TSFz%A=GiE3jln1)gT-(6yDC~KJYh7-qWa@Btw|g#d?USiP+)kiT8iX3* z7Q60o5Qq>eRJ#v|Nna~+d=#9R>V zWM95=D|D8r$WM4q?_Up3iMSrSao0I{@Z`XCA!RG>sC**J?f4pul3xxUO~#|+LBV5O zfS#004T^4wjt_45BtAeUZels?B^=K^!`sQz(oE)WFzgMH-tm>V)RGXa^GPK82rRvt z_f?>Yif0bIv8Q*70kJ};S*;j}R7IwYyDQqvb*cz8gC#dL`xR1if>}_z+(-DW(vVL3 zWhzNYa6!O@#Cr{a+2*uYJaRYuRX*rF(TKg5cZ9CON^b>INhKcDbw*JVYK`YNHl!_V zTLR{`sV$7z*^&aLGc5YNy=tRhIW0gjG>1;e|E4XOFoDgjZ>UzLxl)j5V@E$yZukc( zS*Eu4-{V)Ymy0E+;e&4ABrhWg+iAXR%p|U{QwlUy&SFTXNo4`YI*;G~_#KD^Fi>@g zyTn^d|0&r^Bdk31C4pV=e!-L1@xm6gfew*ui!{k#d|+|b+KZP!ua*;gW>+MB;EUI5 za@B(|&`&?hg^&p?R94uERkk0k%Czn-v735`Y6A`{d^8c>kG(1k-^71R{0GBOWiDD0 z%;qTfMZi&7PA09QebO1iwy9w-nWZ5+p4&J>L>udkhx*#^hcYUUpMzpZCCbE;yHOKF z=)O}LC^onzu)@3x3631^5m~G^1Z<+kAp{TyR1TdLgnnpK|A-ED$>4!F`AvAVyKjcO zuSa-kT*-?2LJ0?`l9>Rkdg^+F{V8%b-y4>t0pb+AQ^8^gP9pCY6cdF1(wFNX>wYgf z?oAAZtz|Wu@)@pqKpPLFys{?LR<>(~DELsA7!SA0kjqp7J6kCpF+e3|T?0=iN8}+B z#vJSG#3Z@4s10| zPb{p&Q_9ie4&}-dFMBFn&qe~_qVq104z?NXrzWgYH@o4L^Qn`c!f16|*?m0h7f)Pw z?8=I$#YXOC*PT|Jq84{ma^ozgn5wZMqxSrk=BvG>y-Yt%8Cne4=D9XQ#E0}fQF!bx zwXeHS&*SaC-t%}J<6(zl-?UE`ES)AS!;IGTaTo$-RTrTGkyp_=gydCmW%=>1D)wd-dtG^M2bB_K zt)?3rx{EHqND6GV7Ln2GSd9N+KEB|q#9FksFccnoRgajC(3lX(r$eCq3)46&mI~ZM zWq{t>-1$o{>Tbn4hM&FYdeGlS$Gn@T@2|)>!R3#>v1S;AX zlwxRWu{o70zpZ8R)Veh-mG-n?oCy*2TZ)PNXG#|R=VcP6v7{6o_t31tL>ZyhA1H@} zw?F!AS4n#z&ZYvgeQ3nU5hHC`Vs;uHX(bFbPU_6bS3EoBm9eitU$;{)e^ui}E&WBi zVo21oVO6GvOMIyT(dJ;qyYb-4>t{i!{!+~y)ul@1Zj-LN?~(i195>S(Aie1VEBhs| zRhohkV|jNPAy$Vt4d18hLR*bq+}@L7Jo&({4CDh9m4}cD6>gLAwnk3pWWY~UGyte(|ih8RfceVK_VnSST)A%2FuP$9K!) zZgmwm|J`*ajMcyE=VrBIMa4~`L&RNng{O?3t&ZU$cMD@U%5qm5^+9sMOz7zlZ(6_K zHN6}WqK69+V#acF_^(n~1uW0=KqH{0ExK$2D-mcW-sRkDWqX%=o&U zyTrS)K4ICtx6QJanzlE`m2o~eueQ!ZzsEF~(f$4^rCuA+w#(dhWaY!{EO4!!Ey?Ry zF$p*&A$zm2x3(oQQDujd-F55AvsXsTPUq06?-c7kxdPdoZ3`|1o`S39K&V4v~u!*nei*~_** zAXVXD6xDNd9L-w-`z^>G%Itf=S?ngm>j?F7G7VIhptzE++XqT`BcN#%1D{jrf#?+F4IC_@~PoHMdqdkcmew zegraMx)7c;y!9kV#MNfyh~Xfb-&2fz^DsyMno*YC<)hVf8;B0v>y_+r6@PK>a~k18 z-zhol!D!Pp+XUCTpYE3Ucb&A5RkWr8YS+Hx0RTT#VQJ-SsI!{gg@whQ?V?j1dn)( z$Tq7OAKwRQ%fcdpNL1tf*#2tv+#asb*Pk<#_ce8Bk`fn&*YEK}SYEhhu(2v{B_Ot_ zB!q#Hi3oOhm|Jg6lx{~`lZ-)5e|RV@y3HN$PUKOp3GNa}3&K^u8cioAKF}+O(b<=V zn$xthozrz_65^kKLCpelhqf;iJFoSoX!h6K5EVm7^~PI}e`C}Bl@hHLFdliOJ^1P2 zw;BT}QKcV)CEJRpmcKz~Gmp!-U8Ekm);$|v%M0+0nr(w_&8AR)A%lkHLd`V{)@)kp zr!YgDpF+j?DJ+EMs35adXf%ag<6A104o4GL0fWQbASkEl*B@yH-e`*ALWQMBHzwJ1~Ft-&-xP zW_RZD?3Cf$YWa``NObcAY7oR}NGV~`wiM0olD#s1Cc2XOa7V=bmN}dOMn^|i1@p7b z_@ueno04-Vl~I9GB}<#{q0!10{cLzhGgNU}4Wj!6$lI?78m}5EY|hZTEwg(BZa3b~ z{wNci#H){2iLc!wf*FqYQ1Gh#;CQdhroyY@<2)y^-&N*7FDC~sPz{prbe1T&0(ARQ zgO2iKWa8`d1E>ZOGSklK+6UJR8cE-RTrP;xb=nKq&Vcq@KqaH^ zx#y-TWTm=hA-mxaC>B(njAnxzA1Ss)Z5`Aaj@PPr5H?IBft)zbgkj^wN<$`)XO(7a zBL({z+)ZmJrSjBxMU{qHX85_2DGK4<^e~0d4w-&=&Z^ zAm3g^r`84uGXPyG{NPB=SO6GL$EoO1cUTQa!9v z1uHYE_IqtFQ={XhzuY~UItle8I5h~ILgc&Bz347*b;7Bm;+u{YPIU$OW{Z0TYwn32 zAzQXtI3|8o{?T7lZ=4qH9PZtX8Uxq7((bf<0}drd-*EjNQ$*`~YS1dYwoXTtUE8bhW$GYZl}F_d;L zGUkY+Vom5Oy8XupF_1Cca({dDRfN!w<;bhF+T7O!;6OE$k zw<+}H-Dr(=eh9v^wxwyq&#Fp1Qc36sW|~ca=|=tq@fBMPeXdw^WuDz+Pje&U+xK?6 zYyYkIFi$6iC;thZOvI`_wv?Nn=#|8Ia zfZjy}%ipO@seLxePhUSDz2IQ)CC3|cTLya z_*>m8xV~Z{nTK{jb*MjBZ+MoTv~f2WTXxg$6@Sl(jne2!Y<$VYMi05cqOXkfr(ynW z+P&0pYJACB;2|Teb+C`vCTa8%9NYc>cw|*q8GgvLt6|KjKML{I#!z-78I6&7gm0#w zNOrWcn?W|re{+0w`E%8)LaevPxb|%LV<6qp?0B@!YF?)(Z!R@k17o|+7d18KpmQ_$ z6K%-d?Sv&`HkPRFjrCQGqPIE`FIf`WTCqQ720rhNpw8#XsSls4TeJ-i;~g4r1G>GS zGN6+1SjxF|twwndAp%(UdU74yMpJFyEzVp zZ|cep=&pZm$N4|kR@zpHqVp=Y$_pusNBoYAZ#su+HtI*_jmlG>xP|}umKxuVOCQ&h zjrGdh#_C$vcO4%rtdviUFKg>Ck7oykBJ8Olv+Z+h4dWq9`zrrP{b3qW+ZOgBjb>_E z9?s(k>Tn8z909q#Vq3o_i{^2_AGu_#gfzvViYDRntgMUK{+O%nOvdV&Inq}ll7Vp} zWX}j1X=fOE%;7^B0@@iSuzAP}sJp8;y#i! ziA9hQ&m-xL(}o{1bP$~!TRYiGLO-VY`C6tX+$}LiXBrQvR6K_7{kq8%9-<*qe$lnE zV{O^&Vz7%ytPywPy&?E-dUmUgLL*@D4#VTPxvizJ#osmB- zzA?MCj%9DMgXDNycgD6Megmh*$Hve95u(XCi*J z^4!NW(e7UHmVi^VBb(EBwV6joUA+p0=!J!u2L5bAC18( zhKz~Xnn`-z7_9yC{nrj8-UAHP>~E{OB%>EXFJIy%BB?JD9p-I{f?sv~dnZOu;a0JSjG#&j~y^ZKcNoc9Y!VXAQDi&l)Ox9c1u$GUSo!niom}y-{ z#jc?guS2j+-?Rx9GNk*|7WBK6a6`5`2HCr@#gU<5hNw7dDpdR)_7-t4e_uz`a_f*` z+F1ox3AEENBR-Udw$2P>)dc#8j;A!lOi(D({90nR=KvP@1JE++81nKE*rORrBHh1U zQx6mW$lk`N_>JbXab@r;9EwiP!(TF7NSl$k$1UQV%`^o5mRx3B8T-{&wRIjMD`kXu z0(0p?A~o18MSl^ujoXKNz8{wB4VCTajt)_LcZ?^qj`a^!!Va zZV_%I-F{BdtkyK0PpGgJ>dK~+SQ7eOvEn3hiW){=%G3s>LKewM9-r0LvQ>VoSAB9) zZbs*dTav)vWGsfZqCmYN2TMTp^XSNKDyFHvfX!x6T^ees*tG4hyQ;n3oe`@O1TV=E1bDUb=rkT7ure!GQ&JJ4dY>hPnY3?-qa z8kJ8~&xSlClZdHEp_#NxUD}zHNwuno3ShJ&)SHlmqugWrt@-Cl9S zOYRt*K&3iFZQQXO)E)n(JO15xNae)ujL!Viv_E=grA$N8hU{F`U{paswnQ*`;(T*Z z#;oNmpuM6DX-A0vQ<`M9Q^Qb}hL$UqXL&r1OA(*wb3~@5?-8Fddlfhnq}iI+*%V-u z#hgl(gmzV|iZ9z2)Rwwq2Q@*6Juxw6emP5(=8Z5R;Y<*93aO6!rzw2D!};EUfA;tR zw=0kBS9zw(1jEEE*plQN1&()I8HJ;H@enPMzQP^r;6a-Qk> z&#PpsSKI_5FViAoubr+m6fjcWC}<1Dz%)=mb2tGpvy|{?xv<> zv1_K5yiIXKLb!7!u)`7?B|9Q1hQ4RD>Gal12zEQfAEkVzY{&{Su7;u{F0TxGf@o<< zX8zfZ70(JT4fWJ2VxIl^;L{=33KMDeWDSys$P*c|1XO7HN*UXghPaOab>BcAp$I(G zmDB35h?#mTK3|8OLwifKA4#uU0W>&WcP5x@F9ckXXUONMMd(R~J^*p5sqt_U-A>m{ zxnK(VuC~gmXOeY_iCZ~g&g+jnZK#pB^@z{Iuzo^UB#z1D!>1kl(4iQVc$wcnt-Pd` z=Z+*kT!5CCGX3d`AIjBgnIB5^U>?N}O7OOVD#4n@tfo*Fw=pl+K-!|O5S!^C8$az} z#VF?w6r{l#oQSW53;mv#jZdPwS;An$_u)BHVq$V8n8nbq%8YeM=vkQ5Slw7C8q2j* zBVW&uhVCEZ;S5k&E$L%R#KW#!XLx#={Dav`+YA|bz%P(TY@=Bdm4wU{#xIHhP0Ss} zvNTeT=+W(YxI8oPPTTA6Nt}BCG-ESV+;GbFh8?SjYjhrNqbA1V$|lo=0~thQ@pSD& zwxdqV!RAkOtVK%u%t$&??2wNY{mX z+8pIxqE1d45OoVwslYlJGI>W z8_i`kUG-isY}ZCEL!)zBo<6JdGTmbmYU@74O3e6=|BOCs7jB(v{tMO#U_u`{F>?MI zO4mCB-Qf5&O}A$g*samQ!u>1wvxIAQQQvJ&Sru3La z63V-wzRTUOEDx0RMyaW;oe0@lxZ~Ld&q$IHBdS`qfVQY6KX|AHv0`QxOk?$id2#v%pS@9;sabitnpQ)ft z0bUQkt&*8YZlRw<%i9C|y~^&C=Kg%UB3yLwtYOoJO@o0(YnQjsSzvQ)6+t|P8{zOU zN5IW;5>!0J5E8gHLv6mhvnl$+1OJ}u+G{w++o-rvqnEDduMCSyu z`Dm>!7->QH$NbVmsRSC9r0eKth6={S_k(riDz~9mdmz1*azgCSV+@L@jp0S4M8(jS!WxsTJ&$~lk#F>t9H?dJ z(vC{54W%U1LJ@mpYQoPF(bat_uRIh~Sj?*Pv{MH@Z`iDvYF+z~H!RYwS`u2F=I86S z@)!=o{Eu^X)ZJ7;D>8cDmcp)zRk)zCfT_dZnJw%wG_ zQ}KEx`rEFFX%f4rhZuNvQhU5k{*U?=?xLV0424rM5F3b(NCHBVsb*|TDh*l0Z|W8m zQwO9HxL6dniXM5L-exN2EA`Dno)H{ZhK7$WcsN09Ad6b+?XMotaZKH_Msq#%@H&MG#tlP0IW3fezMx?E> zFUaV~5DMZUy;j;^G&()4PlptjJS;O&%7jHC#B zY3L6kWVas9Ox*aCCq6lk#VsLvWqnkfX%suxlCT)+qF!#M&g`Mub0=FSfg7s{O9Sia zVM|s1MOrr}paRUQ*G`)&E`LRtqu%3;@OUV{>D1PoDs^y3=MYVRAF5w={54Ne->NIc z5VL&}8`b;va56AAc5kM&U%mu7T?pyZ`qLo=YRj2v>e$leSCAuFX4p>Z{4jfKOzjvf zU1q{&dN1p(pYQG(p^a&Tj3RP-PBGMmOR1bvx2DG_m-x2Yor2D!pGq?tu%|{gMY;Y^ zLPV}4w4+*r7Gm;@$usVm6@Hv3_#EwP^7J)VFn90XLVP>WEV9T2f zULqb|v7&e2Dk?q;IiJ!%{*bA8kr^>RmOzkJU*RFbo{39E%j;D13S#_|rJw4j652_y z56#q13X%`%4LGUjRNozU`{uJ!T{UDb+e;ataU9fmxHw%m?YJk8L%Z@3zHkSGdFMS+ z4M=ZsoGDj4geIja2>o%6PTI^G#k7zumzkzFl9^q<_&qBH8Gy-J zK&fK5&i3$iY;JZFlI7MibIb0?Y(ANXdVR>`LT~iCtLlI{ALsGybn$2^ss*h zY6>Y#%YaQznk4rnF!^v~e?&PJuAvYBPH-7-C)B$>*zrS%~9FK8j#gHhsoHSxR zu)yx^0r5S6)duZm>j1es+oV6JX3>8pJyQdRqFv1Ef{LNVYDIcn8Ot2f;2{G;6=;E} zM*^SF0ki-SNO#t8IKNP?2DlerrWSZic{ z-ZOBTcxZ%&AJ?!TbH;a`&;DGA9WF6UV)78g&`kb7ue9BPJRWMB%dx=}=dY%;Q`egi zQMQe`4&%lh7Z119S9VeH)!W)4*+LjeWis8OS`*QVp(GJHj|+{PN!&0BCNo>hH+Y*1 zSw6x<7%bbW6P(PPANJSH5%+8TFfO3PPjdtsO~BA$bY*IoTc>ldgX3eu!B}(Wxh6D) zEG!{23x^HeLt8ZLTGaa<%~Zpb4&M57BO@NNNveH6t3mH*;pg0uy+?~=w*+MSNsKz6 z{A1Jf%&`;U+4OyYQu|G7GYtBTk?{~U-`?tP*^wl9$0iLeDr3tjnTN>aj~;n@0#DF` z!*{a#*y+?m(M9uv=&E#O?6P-<1Km2*PCb?(vG-UNhV#p_hgG|)-Ou+-3bPwCK3uJi zMmN4%JuMnhdVxBcShzr4KXm;I)b$+;7pSA({(}qD(FG+Js7HR}4h|MfV9cvDCR++< zgjrDo;cTI{9zseoL~X=)iXRVkR=GWijWzK~RU}mG+;FN_anl7>D)W08;n70#=nc&23Vuuz^{xi@J$GY}7gLvLJKI!= z)M9@&Q4IY`ag3jYV@*qLSGEI+oyV0yxmw!+1<(sd{&<-klcb#uw&jY|q%$X-dH0a8 ze_|g7du_fhGc<2lm986g`KZfv>V}g`v?VV^AM#%OL_aM{WxPlO_s`J4Pt?HfR86&LBn&ZE(i6sVOctt(q9BuZXa5qi0SFNuxny`W!M$bj-lh21g_FU4r5fJ&cn(M zwQhUZOE`*km934}^HA$X4W!G*Li@Yi8=U7Ge~^)}W6l|Vj%EW{-yeILT+v+JFw(Yz z+EonwLgSD`v~-Y9;82ZdeV7w)x+#_&#%u0MLOZHy%@y0t8tmat!sOLTzBNF*$rA=C zO$3t;hX>*C<_p)4C(iE1+r^iKkbihP{{Z(_w*+?)-T33U*dC5P*`51cFsg6)Q(VGr ztCt|PMnkjQ19n!MHm$8gBfectjfX6B4Ppn9%_Par8Ugw}H8&1$m!%=eE1cHS6C>R0 z6$!V6>j9aAyC>yW$mM6=qvusPTDi?m^7@}Ed8U*a*84opEm1w|eZ40-`rnm2H)>IAM8WS)+&9`+6&61n7|mBz zg%5qA!&OP)ceWXq#R|PCc;KPsgOqQ*SzR0r3@-YYudhXQ!EJnX3o$p`1LNxK?B(Gb zZYy_N_8iyJNIc|lnS<$s2+j)git@90MFHk`4o4P|GDra;VL0hFJDIvC zarL{DXN%XVsGmgwVimu}7R)kblLAQ2Y3?F3oLI=t-&_TI8mSZ%nV9_W17Uf#!kuP5 zvx-<&6D{;3mUEcZEgIlQEcZK}2KL^rArpS2>VIeAyI|gCEXZ!bB|hns_#ZHePwGj+w-7RrxeC4c zmEO?*y!Wt9(OVkB{Lc>cyQuH^pYil)t@>X0{Xo1(zQ%Jd$F6C-{@h5v*7EDo$*Uei zTljr+{S_)b|NDpPyNW-qs_(6u3ckyhzpr)`9GK0|9;k{7Zp`B6>h~8i6hDlJKX063 z7xzZv4@0nWr-~e5*xnP9+^_h6_?AM?wob)vw`qVoxi!&m%Y!|W@!0wgh0dI~J{%mE#D3=Zq}x9lt8cDV{l?Y(l5tRfowm($ zTqk(m(~{h-*Zg*asCD&G$v8bX7?Au&^laht z*yv*SWzaMEOLS1-+cEBi@xkEaSooo1ljq~1E?))%)W5n{@{#)6il00){Rs3hpX!hL zjomlFKFLlm1^XuBGuig@G2>;3Gh7`yJ?T>J_d>+fk~fu)i?%I%kFQ~e9uS-f-Eq4W zzJ44n#zkfSWc*b0MB(E(QR^AOImx&Uggal~jc#6)3f@R&ufi|h)5+AdCDq-N%d@Fq zm!$h@qaMkxihFq#z$p0*T)4*s&`8+?;Y%(oE;C=hf7P|5dMQQ z8l_nge zhDK^cvR`zrzLn11UfGM4wK#3}WIxo0yWLATVK^vxQuZMIJUD9Nw&mUIxLbU;e%~8? zQlAPAPVTVo8q0p08&KaiI6Hanx~=qkCJqmq1&1U@x&uXYcgBrF(_m!sB{x(*^Re-r z>poTLd^C@mTW&YYjlg{Jd+YOZ0(YUO>rK0bRrei-* z@Fkk9!9f?F&vw(i2g&T!^Yr^g_mlani9ch9;rDvA|65O~1s)GX-BsuAO^C*i_h}i%tLc^%NMQV%J=2 z&j=T;n!9bH$F&XrcLuS|=kwWDe267ea^+{thfC@jr*=p}S`=tXh@OEHAt{|tM1 zM|Sil(IRM9mZsb*b9B;F68hhF(~zKOX=1&q`v2HZ18PkbBIT9WZGH~p@PF4$!;|$J z#`^^B=Dnf`c!G&vDY&C~CBAl1ywCrM&!#!X_vy$>=G3DYe|cFI9D};bPGWk&^UYwE zBwc!+B%5SNjxOu*DUWmc(Jmw7uH}ECU#20IQYTBn)-W@J!cAr!9(rY**+1)A?f67| z;xcyv-P}!MQBhmRwx6Per#7pN!5o=b~v>DW(b(>tC+?&02SCu#_Dx z?_{I@GBNo@^0HTSq(1zI&eIxkV}0X$rF|^n!=Bw=AMzX+);warnVPU&hc06jw zuXj3ar;pO#>^y4c-#a8abnZ~o;gt>xI*#u+vE#iRAMez((Khov= zZhgAdbboc1KD#XI(Y(i&Jtps(*sXlG!*`q1Gu)%~9vAI-%%0EemD=msUZ?jO)9cgT zO?p4or*ofyeNOIkR-c)B*Y~}z-!uE1vF{c8P1rBozu*4v95C&`CI=pQV06%x2lqa> z|G@+MZ`{8{|E>GKbm$?6K7Hu)!`?gWv%}{c@ywCkj{0`MjYkhX`r?`^2emzB>tlKz z^TFWs;E_Xa8#4X4i;myq_(3O>osc}?@RNF+bmU1FpS14e?N8qQfuq(k9uv?$L9???~L;=ys3cybMQ5TuHEX|Bd;BO?XA~Nxo*gHuU`Mi4F`k=hik)z8&fw z^WW=kX?g1vw|;+HbX&*p*6eNNRY+dHXkBbHu52tu9O!vH#8`sQ4YA@7!XUIB>Z{H< z@37NP4=uMeS8N>~n(aDA={yNDal%A1`r(!tN8~!e)F1nF&4lq2#`~lVzZo^7DT%7n zb(HssR;;L1!(xCehdJUFga+85H&%4i)MO;kOtI;;tmWbs!&I8yhg zQ{E|v&Z)R=FBQ3xNZx%kEp?0 ziURY>{3Ad7<AG-IqRRao%{XRC{B!n=@+!0Wl|v%64cmr$r`$UQ zlf-RlfKk%KC*6CZ)%!~$fEMpE6b~A`Z%FR@O%pvPDFxGM2*N?b#*pZHuA#q%93H09 zJpZUW`xEnUBQ*jmA7yi?gR+)oYCfaehuHX- z>GJ0JRA$R(>3yg<>B|ly;o3x1yiDtJS;Pp($;{zoh3RKE2cPfn@>p_~Y+l2<`()q1!xWTL5fB z9iDj%ROpw=R177lqv1UpIL0|qRX_BK%J{A{)p*F=~vTvtmfiYWUiWT**&u z872B%;Hw&nSF`**#$w29hksXu*C#S6WVAPPzDa(g@?^1XppFi&f2*${vL!1cRmotYs#xWz2wmcSU%MhtA)UlPbi2&n0BUZAlj()>=Bdf{MN&} z<-%sJH4ADt)=qHFw|EanSPoqcTtrEDW;41hjFLM$C~`iibX2;H#!uu~hAg`2=${6A zOJCHshL3R+ar*OYzTMRtnXTpS2kdWcG{?YfT;3AU+uiP{%xRLBwa5T^Y8TFV*FDhY zwr8#8mQH$TwgO;;Dwsj5X(jY9cl^c)72Jpjl$7j=hp=6GAq>CDmWAw&-gs}S+G+Il z?b=$j^ekf!a|eb$s*lrD0{dj*;y(9RxD9g$@kgm1bC0vD8r?@fiy;to4Nbp+T-=&4 z3k-kCUUqZ+N|0z5G5+^dA_3 zhl#3>xQff=eWaISxwy6Zj<n9#_LsPg&1JawM z-ju9!t~h>!4|PYvMLowjCgRI0svIs%%8pag29>~md17c5bHIiA{F@mZ8jG_+1B>AYC_$&Iu8@2=ZH+l31l4Yf3`R)O;Gl1*KUOr>iWH zYCTxrmx_p)@48ROYLv~b6<4QJrt5+V5_0Xj+o59#ia zm_6+;+2CH4E<|Z~T4i=q-80pSVrU=5ev_&BiaybAUC`GJQBC0sd8pfTWqYK_mr!O< z8(#UCoRdkeXK%ujOOLhDi&r>Xti~gBXRUsr+OO z2b(D29g4TFC1t z*Sen^7DC+a@dE_CiEm1)!hY0UT1-*}ExkM1^jk5JzzHk;-rC?6gg zVsJ{$-fsV2H@wduFMMj9-BmlRz4qEG?OnS%X{6xLl8QV-jY63ZO{|-q z?~qP?nZ0K7bqz1!$?OMy#J1(`VDqFzSMe>4fbH=7vby?@b^t7JfzW<7rwsQX zI*)O-Kd-}K4j*ru;!i0-XK+_@5~GBxgD7$F>#FMN@>YHXAdEHCd;Ou;6)hI?ZJCpF zrvfbh-W>{`6bkVt9YygU<3i!bD&73GcU7kD_NwU2A?W6uPAf)CIA~kW5>bM@vttTo ziIygE7~n(-lvC!?Y%7IeMo0&pTRcwGmbfkIg-_{~#9l3>#xvNeRc44z^w>+;npg8; zwRQ+Pm`tWU!Q!iQK7wC{7iQ^$Dj(#xi)-UA3l&jea$!vruj!}{uPnq+yP+bpz6ssS zA2O{fQ)sMSKT~;K{BC@8-Phkl@zwdM*A>P^g=ZFV8bfbBFHyLvZW={;cdUE6s!+(k z-NAjqCE{?!4Rvo;WZn?#*bI@S?{(F%E))&+P>;$fq{%CC-pR9-dR!8V^@015A#Opq)p`1b{nOLLfSSI7ij%)0-aX3fQ;bU^>WfP8Fa8P{U z4EJ4CA>5kmq_k8Q)xRoRtNoSxie05fO)k7{62n2bNq2V={<2l9%k@$jY7s#w4uR5{ zlbnPq7N4`@5Z_lLGiz$kbnvuG=mTx(isistG=IP#u->#L@LHhu;zNvty ze*4xV(GCM!Fv-3EZNuIo>@x6a!ItLoEopEn`UeGryoQ3gi-t|J$WK-OHaB8 z3sVn#k#>kPK^@~`CMQ#%vFcSX&eSd&I148?$3t2E+_uZ zA`DF~eXFOuanzrU-LvewK|rSiP^WOZ*$`4g^->+w>N=H_-BPknyJoq;Qp%gpzsY7` zJ0rJaR*yBMUa+{M+29sfw9&^gTCQ4b`1#UaG6|R@kJ=;wPNA0y9{tfg1qZQ&)D(9i zW_2~W9F$8dGSMW?JFu-jpzLiu)Wd#gVO9i?tvwBOBt%;4QHWNEPj_S(@al$x7zDI0 z+>1CdSX<;YIlWdniRTVBzH-nl>3OZPlz|(u6C9kXybVODuY{7`1m^f8XI2_xPZ$2> zl=O!kWn&_dA7<&>r>9*k4}QX5YO7KT-8hTSmNWmTqR8HKW?J3b-QvrBZL)BEld4RI zy1~hj`H0tP*&4(v?v;bi$1ASPQQHI&tMod)ux^s2KC(hPwN_3ky96Ng*d1*0gFDtG zN5Ha*wz=tSO#ZW(?gqPMYX^THrAOb!Y&=3JR|xEboAsKppo5Q%r#hK*nZg6$)yj>sREk+ zLKuC31wb5h(h@HW32K(1`7-5{rlj8Ar8pAOg@{lq_nPJL_{hgcj=8%Q?3flnJJiA^ zu_V58Z_B9$Ob4o&O-Ihw=>V@9k9;i5SLE~wSv*Sz2Qx{sa(+LvEMjQq)Jw3rL)d=O zJuTO}S2wb1Z)LVt?;`C-+N~U8w?7U;n7#>=moBj8198BOn#4QBwPF*hv8EQ}s_(vR zxO!dM^7<34pMb{tDvpJUQkoJ&6I+dy!dZYnh98QSwhH_s?t1AOQ}J*RTmKJ)$0vK)+nf(&>gsn)`2sZ{-a$EuG$wgELmThZ)DAH#67v)};Gh!;wv$r& z3S zvBSh@N5i!%xgHV)yQ!i^0-dNs&LbNF9Na;7Dg4$yKb~zigZHZtLu}w~A-Q>%Zinh> z2NP$<+~bevNpdvXP@6xOl;B+9KRbsOm4D`-F%71WvF;b(%Ta93r_;I>p?S55^6OoK z`*O(ZHykC2>z>Zm3io=)uhrBNdTp*EvUcmG8j5wdrhTo{Jd@rl>5YrLnh(xLhZ38B zSBC4Sex_7$ogKZFv17$vQ2lD8_@1P+=`Q2HQtDl}nL^rx5hZiM6FN*S6f&ysjr8Ao z&iwmx*W@x9;~FO4S682EG6=X-B!5pMs>681efFZgeQnm9ex2Y0JsjMgHx=Iar{}BL z1cTNH4vQ6jBHlB8{R`v-b18Bpg_ZtE+6!m}S@xPq>2Vyd^ipLsD_Q7~9C6D#9aOul zOc&wdP^!e=8%tGZN1Uv%#$Fvn{r<_6F#2^?uKLn*E|?84L2HxwSiB1JNe)safN z%}_Xds$NPlaThL?(6ye(LUeP|@kvJkUZPqd5?nU4W%DhX_Q zY!fxkTucw23fY&aIhs}q9tR)0M=zZsKq=Tls0%{6C0vvSKUD_8GxBmKETDDb-iOVq z%0zoUa>Rdd*pq!ojZsWY<{yQeX+iNzz0{AYAI>?`&UKiq$gZN5W7DUdh#+PXAsM&I zQFsvJzFs*qAaR;yp${2i-9I8^n{if_Mx$ zgmchQ$8DPUH;CrQrOdMBN<^dmtKqz(3%9J5asFA%YqI)M#y*9EPdz@$pW1~e)OGVP z%~=^)e;xc;mD#Iq^t}9Lm5*~Q#Ll*C4)cm0J%oQdb<#ouuRxE&FZ1HnRev)R6n_vN zef-hKA2n4y!fw)e<_@qm)F`gADYDGoz5_auSo}FxEjC^`KVGLVIbTszi!*vab#;No z5lI%HMXmCxLLQU8cUC{ONaS83PJ=9o**QMP0E&u5cy0A;l&US{L?^;gz3E9&No_ZTtR(0Xle2} zQDLtknb2^lRqnv!{UjUhv-H)fP-pDPDd`WnZ36_57KGF006SYydr`j7ZDXYAD)u$C z4EuPW|LDBZQoO&5>cc#E@tqgti>S@|zQ_>>7uEWOS~Mn6__AKgmh{XXlTT7I4F1_! zq;EjX?Ny0NxwR$>iEgQOEY(O)rXS$v|ZPf%eZwYeD>n9UOuQLUqr$Z7j3NYkI24in%#@151#l*})cqyQ&0b z+UdxL*X)ZPomHSomhzHw7Ne<`+Eh;oO-61e^4zeZ2K-bez$%qt2Eu=(r0at$@CzTf zkiCI0=zV95IAer4O~`g}FZ8^(1Y=c(>lQsuBb)0-0|DD>xT&gdVm~*gY^}K}%Y!yh zC0$D?_WoV>*yWEgYpnLg+&42oM z;ove_Qj*7FH1?-g3T3AM{R$rFd+R{rgv<3QL5*74{YfmpHA1a}Z8#95GHiPK18_7d zry68yFQXX@mUN~|p+)wvrG@Fz@?V33;)-x7M0QK=vPNdZ6W#~9U_r92P;#R z493)|%ZFK{=N40p)VP>TFo0RuM(~DIS1hLrHMJulWJ@rKr~fi#=c>$YC(VJSN24t3 zT412tv_}A(&XiP7-ljqZ-2@T~)vrw7HeS@Zpzq|!dUPvP_O%R-ys`~N77?zW%I2tM zIUmok6{_8>djh!Qra_6|ZI$1pQxuley4+@!X|aw?vUTMOB8pg z8=Z^dX8E>pi@LT``P8UTCR;kL-d-OeeV}l7;ZYW`n#8BZO$w*3i3&%=kH+2O2ewzP zE%I%4iQ+%jJ#uL^eO#-K&9e0)eU`?l*465cDOpm)N7f5%ypAY6==P}lb2mP8YsEJ< zzmp}LUDXFT^XGK@=lr@Xt;A#Vt)hu_?eyKPSE9ljbz4M#$nR}io){TtTe;EvA2v+Y zJ*3-+62u@KZhg~8IuVC4`tNLUjyH{a9Uk#%o1W2ige#AqOwXniRvboTxFEuB9uvIm z%+b|+;_=BrFAf72bk}ukTt_IJ$7>9jC@UqG+FK9FR3GiY8QKmdDNSejqArSCRR=(5q_B*RQ?| z9#XmvG!xafr_JZIXk$S=TBd#Uq=N)&7=^QQ#~H`8v`J6Ed0ITk1K%V@f&g0C(O9hFZ51cQ}~H3VA}RJi3JJwl5Ig z;w2}z&Oro;kl*M+rn^b-|EL}`Rzq|3bSpJ3qjF=4{iHH2HK*K(ZMo_ZJhknj4mQ?f z3et0i0SHr=cTNQ8E32L`5P_Pes+P4nBjAJhI#7?ekI_}y_+es`W|6tK-(-=wEeEC4 zpXm*4B~h^NY>aSPh*Qn0w}gm5`D-*F{hbD`v6{D$X~@KMW0uxEi%D3@Ml{UQ7Nbm& z%?yH1;#_LNCR1@^%3Nw3Q|N~<1u|0@7_7-5*ABV%@|y+`r7ARzTUAGeMujb-8*|ar znN#=q7WsADofWPSwX3?-G^}|$v;?KOiyMY|S4Y>9aa|5vec{6VyTd`&YSbh1&z$6u zr{+>p9bRR1FML^avxCX_`8?A8G)Z2^-nNUHorJ^;wX-mR>s0s8*;-nMH`1ToT!A&| z4()5qBWov-VrtgpSwj#-^AE`-6mlD$yf74UdpMEA9O~W}XqGa20OM6Fxg;H8YnWr<+Q`EdUVNBy72buhE z&-u~$(M8u@%0Jj

=bv4Iyv@cZPb8THx5h(KhtYK(i#Kh)AOv^`nK*VW!tlHKfN{ zcVI0i11gf`UIn;=Hea+5zdOhc-G%c1`hQ{~BFYC0ISEFm=E8=-1>-t7@ z2mjieb;sp=uJozsuEIfxlWOK!QKeL2-gPwQ`gizXqK z_TxY}%j%%XIVyq3@^lg*R8D;^f+DQ6-R<VH__<>smSy9sj2PqxkX>ohZVxf(b#X%ds%g0o>ttP zrSI&YeQo~jmZcQbdfn*gAn_vI{g*cZjdSMpbUb`@yW79LV@H8wtWdn6?T8+v3s2P@ zQ!(Hzc58HFE)>1vG$sGF?L?(*&vLy@w%AIO?Uj!o%O6~UFPbBcdNTa|5krvqMq4zS z*VK-)cy{`{FE75Tf>10P<4lvsy~)Vv0JSw#!p6n4oDrlrmrJe4(YyB5y$Fp#ymXvr zap60ZuZXoD&)nC1cTw>lL-NI<=!aGi$L>2kES>eT<^yW+yFD3n z(5{yLn49XMmax&K185zWm5Nk9eglyW2hE>sk*$r)Y6ptMwzbhM2RRxzD1@b&UNVm{ z9fyTEydRNGRD*F+p+-DjWAQ`n0Jx5@RwM`*`jr!y41TiLf$>n{PchSEEI>dv1=FMLz+ zl*xOEdKM5t080zFizZ7ZOJ1vbGnFr=U;vmr*9&dQU^qB}qNW3O7t4BoR%5f3?-+oh zZ||nxU4CrzXFkYk>s^jZF;$H}{{)io9FWBsr>tb4inRgdlvtuTJ6l^?OhlFaCxert zqQ~j0?WbA`ISXA23&4mOhG}nB4#KAOu9@$Xvot(ct-Y=euNG0C%Cl>*tV-=&B(O#$L&M4lP`Y)|B zb)VTSif)QGDYP1|@3^;$h97{Tj1+68&KO{87C5Q7)&k{JrLwd(yRS7QcyZ5-ivC;M zX7atp+eQsvE1kLgO4uf=qs3tp7P=i=pRF|=3jNR<=^pdGbV~1q5$p|U&Mt{`rvI3V zK9_Ue^kRFqacFSnQHO zCeZEI{w;ljzPj8Lx@w}KUw;)%={7^(#h@L4p)+LV^&ztzY@8!o*7yXYgVnFzg-iG@ zec}3ebo|%&9(I@E;SV~KMXx~$z$jcDUqS#ZEbRQFVmCWiWcDfSJT+RUlv^~it<>g+ z;i4lB9lS!;#o-LhL-`#rh(OXdBbs9E`6rXJ4l=1hU7d5G`gG=P@?-W*zw#>+8XwMi z6f5W;>(qHvxUVMPRAnbNO&qSKhQKVv9fXc_(L$+`I0QB)i88w>|2TlcL79P==~(1Ly^D|ee`CXUvWAz{mvq}(T*<-xvb7g|1H1fp z=*9HC$r#y&Jn_N12xb{6Zd-$|6`GvFw+#M!WDuKEJJZVtmP!R6=z7oIa= zq$%e72&y{)O`zZJ*wmaN?))Fut7eXXb6YXqFIhUmq)v9`lvA4GZ0=W>1G6mspI5N@ zOel*nNO8TWC8g#dj62lR2e9^oX8+IZ^L_qy1mU3?)hbQ8y#(<0UqQ_2`wW6uPaOFO z;s=2^Z#*Ths0QT^%3wX9H5^2LS~!pVRI7Mf@9Qn(2w&h&I_NfvT1462%v8i9ojF{h z=|{>J6*O@wtY8s8L28K^Gz$~R6s(_;qfxBO^}57FUwUGfx)w-5v&_EuUtLUb%Q+pO z=^mn~FJkN{n@G~mc4z)Q$mGbfi*0c+#yG4C7J)afA_Fbd0Y#K}lc1@3xI%T7Q_UqS z^UAZiiFa%(sE!shpJC*KBOkn*pIO~>8GsNm$MfbAq{~&EoRfN~rFvS0u9qvuyakJ; zn#cJpq*nnmLF=VdU2&<+7tqBZyN^v94YS&$MyS!j5!h$hTAc%fs|IXPQ#+in) zZzd{eVl}si47>Jv{#o#8m<_H_SYs%x6A7upf`L|X{Zv6XHxLS{Ut3*YGJ)~|-8lWs z^b5yUz{t$QBtFBuv{;6V?3VgvuO(|)3VRdaQcd)f9dEbWsx%v-PN<9yL|N0IbK2Tb zmu*UL+gC8a7)zuC%uDoDPwQZ<&LYKp+ z_BUF$0G8vBZ&>R++w61W;wtUrb)g93;!Tv>Qhcm7;bNNOv>TWc0y_6uxt#l4PEpe% zWOB?7M_+H;Oxy)dcF>#~SA(%|dQe120uGiXYzNc;VYK>OYpet3!bblV=Zbe52C2nSb#LfJ<7Op3-eRl~u?iHIm?wuIj z_B`?fVpu#|rXkFa6fhrv6{be^hKzXuT0wY}f?KIoTlU!uW8k5S9zq4F0Pj?~P*D12 zEnJDfP>cEyme9BfZ4~9h)k{eYXm38f(yTOycGumK_`S(GeADHw77J2n^a=kri8s>= z9J_E(dZtsn*^W`%NJ(m0Nd^C&lcu^0S+HPcf}ext=!O5KJ#Ffk0`&iIyp&lh|9m6h zz{0-cqmSZ!D>8!$Eha_p#x4H0y1^30fBs~wuJO=mZP3iytIy2IF&6jG%+QyZ97O8A z$0y+?GFOWl4b%jx+SfT|^gBohM4}81Jc%)#2!EVLJOHUj zu+Kr2QZ$66mC5!sa3Jh5)DtUxh;%+thBhT_K4~8k!J+2LgF{h2wUxwz_J&!X+=`*s zExzFg!dI%(t^Jsxh2ddAtFa}xI$Jv-`ab<(Rg`lwM_B0CMi>rF^%R#IbsbJSmmwAhih1 zopO|gl;cV;qiVZ~hVDJy^}Xw!=M~WPP2yF*LY@3+9qRBt70pHN{-7#a4)q2;zwykt z_E{C(lVOYR?O^dsj~aDGh6W2x)XIqq)_$dX?e&z@r}RTldZ~%ftAN@xMU8DuH%bRZn=98WuK&`FmL`4^`KV$D$dUWn6OMmfpTh@o{ohsd#3G8 zmKXVL`w6ud{>9T@Je^QUPKUE}gX~`=GDVzbuZC*YeqfTKsCN~Uc=_DF+S_fHmV`P!1h{ZG1ep0-H=2&DOGNYX=b?ud-nl1Fj&IBr$ z;I=@*ab(>KOr2mRsc)t1Uo!urpz8q9UeQlMlIoZQ_((} zg05P^>x7)YyFDHIQ`=YW-{;A3+eV}AHOWg*4?XLkt*Bx>sK0Fu8ORhp&14tqFmz&y zms4VFy(1HuH=vz%Z(&?2x0#+=%hM{YTh()^Ns@J@+UZbaJ4%&-xS#FrNILt2Dz!6$!^Y(Aq{z|TO%1I)zDIf&3i4UY$>-Y zTl);9!4OE?6{kgg!%Sf$R7p9N=0R~*j(5n@buZ4#zb50I-qLTC=t2vcvHW)M>s<9n zy~#C>s!y`cB@10lVf07gJJ3W99fs4VW+$jIOmH_#>!mt@zrCcaMFE;T!r(sk4mFE* zNJ$4<2#Z6F`qh@@RNRGB#lHKd0cN_|B@~u1aLO|V(d{;-Y(JSL)@;7V0VduKD(d!dU zJWr!?y^22Y#epxHN`M*E9)iHFdc?yz7x=DOu{cIGx74$@C4`ApBqFYy>L$|Y3==y$ z#T0X4(EOMWnA!L6+}TumQ`EDq?j1KGTuyB+d=Ac4N3){j_^qXC_XJT44z%+bq1Y>( zoD?rUt9$bN?FHQ9(e1);vTn>4u$fm*P1VpY(u2=%D$tklsM~>*=M?CaB)+I>T2k-c}?wJw;#Z_ z(_LKf3w3>2wbo!AoG0vU4VEwv-F9%^Q11a_QzSOb3S6h=b~Z-dQ#yN>PKBMS^hr}a z;LIg}xZ0`5a%vu2;?yY0laH0y9;!sCfdJg*;5zkicY4Ds1*w|a?}d2HY}5J+@N$B9 zIBIH@GHs()R=^38jyM{+*I4&TDfDBd(D|AE45C2^3|m;T#c`1#{qVsRIG(iQ0vj%9 zgo$lCwVP?hO$PU0IcBpkIV#jHWZu-o?cymF_F9Q{N+-^+$|S)N)F*D{uh8aUS(%1S zT1_tzu3O_Td?~-`pKPRQk^?xfnBp?2{ng z+89T14GZ-+PEC`wUJ7#Dht5El77_>6ekbe>LwYB47g(6UK`)fPNo zpazy$^BYq$#8!MZ)u~p=!1V6jex_@Gs;*|=#_r0P6XA5U6;J}HxFQFbtw$I#Z0~gN zyKK#n(zb@LE%Ohewb9`xonNXq{edg9aN@REYSVg@J9303(3q;$&gNqEKUpI$rJjMd zDz~Fn;1ZY>)wBYad@1ud8(@yJQcQ!qodjs2erj{tknWErCIfeZol3E{8l;C$rC(sk zRb*?m))K^Qr zp_@gtP%mW?WKK>muVvy9g>T|2_Os_x@u+xN0hYV1@a4hq&zJBSJ`SmGDl&EKb1p1n z-qi`&mxSB~vChZfqHq(OAy7^!T4cV9o2ilz^-|VlVp-?klD_QLs9TLbF5QtVGDngz zqd!j}d2BNWFfpL4t4)-tlkSyM4Qgt>W@78f83?`m_=5G_;NN5sbG3b8L1hnnCuD11{`1R! z#y`|A+a-KFmA?>C*q1udwowO>jg_orj7S&WUd%~duXEy9Ji6)|tMVh0L$AH`+DnhU z7flp@E}=fRH2SR?6DOPd;8LGtYai)u1hZ2Ly)?+mB9J$$fyq$>k`h=E{G957eS*-Aan-)Y=G`H|*RnWCSg6sCJ$3ABI}@F8znh-U zlp3%thf@tB-7BXi>8UZ{CpI&{I=Dp*OU^k8)=QzJ6K*qI@*2o2sw9-UkLs6& ztVIsT-YjiI}a?}jp%=}f>|2x6kk4(rl3pr0#w=!r^8P-3QuVC>^-^Dfv}{$$2XSWa#7Hrv2aOW-XQ`$7`2*;kE~nW{ zq{QcIu5oWET$&1$Q)*3t_FA$t%Ya+bU@^~v0}a_RQ9z7o4T1B?u@kV1hVd2ie9O*m z$45LY-!iIe2?yjyV}|r#hX~^P(s)eVFTP7`ba-LVA7PK*?-0pH*A@m>46{=N5;6U$ zn!))s{asEC7Oyir)=Q*)`meV{`@bjwy{Ee43utEBc1s097-E5PYLjg37k9dEIp53s zT1SHSsq+xSg(LFq(J$;<5%R6$gX>yPfW38$QVZ)2XBG0!%IGeI%L~Ke%X#O2KBv77 zD-5p6OszZS;JABT@A-KSW#sK6LTG(tw}AY20vn*4jI5|TL&vB%oNUd=9L{*p&V}8o zoO)4wNppZcex-hFud*|BubeU=#+0ALAfZzeTVdy#+TpL*D5nc2FolomQa3stM755K zJLBxXQ2T;5h$2uaxd#Z1OH=~2pdz-ZsdW&0OKiednU70kc)ShsRl?UfqFbs}7I)Y@ zgwx>QJ+kl_7i~(C4`DG^JvQBD8I#mJJw?-D6Bk-lW*9N!w{11#79(M_h@3d6ZAdPj z5~|o_$-Yy%$EQ}pE~rUYLe#zJ{EKLLc2=8tm=ho~Q`HuK&OyA4NrcVpRAr#ag{L6G zcChfCl{ioKX&mhleAyxa4l=nZ>|zA_8IJ7@?qpT6q-iFAg9x&0tp(7j%cATKHlQ`H zLl#Ul!P5^`Mbn)k(UK+9o-7aH#1J&$&t>rbf1DFW9!Qojb3)UAe zyzD~gKGeG)IzK(9GD1d4_B+Ox>~}%da%w9*ZAfe{!pv%2NmZy4iwUu|C-LSMD9fn^ z)zyO|w{1=>k=c}#&fLLT6eL___Twz5ZzJ2G?g7bMn*{5!R5@GNF14a+p`4lvuNP$! zZWQNp`Qc?$XbD-}hM96|t)9X#P!+yU=O4({J)oe7HDy|YrVtj`mJY(2s@mGS`5mMU-6alht{WCJZ-$to%=XvCGA<=^9dt}3No))Gib@;^TG&6r!tMLT zvag>)0V-~!rSF3v4=?_1&_FOSCSDn+Z;t(Srs z(NZ=Dnn!WIb0M!mfK!Q5ViOUwWQZMPu?Q9zz~i~1irG?9CRaUj6$&OVLbS^Az@}up zp$i|lAQy_x99}bQY>qEvH_05vIgwJOsSoVUb~*n%c2OQx=~(*3=9#GEg`67_fa! z4RKGT8Ff2oy3oz-eWlcLh4iwHyX@F~McoVk9VT$$u(%;kbL)SUp|C2_6TWtk&HD2- zYVu!!HeT={<7INx#;cq%Im$$o9Nn@q^C?}!nufGlI>v}-_XZ*))0 zwXOw7>6bJS9HntM0PW)fU67=Hs-vEoyR$!p*4_&lA{fG`WaDL-D~yPPW@7H2qai{u zE>M`KHTz`;OA%=SA07AYdMZa^W9oFsLO-+@o0k4pw*Ng{WgcYCsV=Xpwu*?N4%(4K z$vVZ`*6k?}K~4{ySOcLfg6x8s)729>Z7v9hQu_Ul_+h75oB}I>F+^)Q_+zg6s#|oIjltF+-_YXe zqbEmkyL@MCzzVh9L~U0IAlgUV&HulRh(Jk$<=(aVZDV|J#5dr9p?3Eb1Pm8~+X+q$ zIz2K?BP%7LkV3j!;o%{tR%FJ@p0+l^tlChoh|}Fuf*G|O4)q?49weh47Ymk0^(X@2 zd|^w4;LCD#b#!Igs+CtL9nK_U-`hi%v6E$i8nK-$bBx;@)Lm6}nbE~TS5g`;x)7Cq zN9V#FRhg1))s~tS`6Nn{w*890Wau)Q?Tm}{Dt+;k3UEL~Ly1+@)%tG0yVmd*%F!aL zHzjtq17m=^{6eN#Z@^i zpTPSL4qyhc;n>2A4w{)JGcVbYY6>zLL(N6r}M826q9^2XAoemMw8M;STnKhvA z?E`PRt!!D{+g9~iY8Q=Q%Pc>47wJkz6o{M5yZ|u^uO*9@m?K7e36rfSY)Jn}ybwC^I*a)r17NT@!0yK%nrVo+r zF;%@pF2Fy{)qj&v-aO6T9i`tQE`z{m{EH_J zK3sJt#=s`R4Cft_ST(m2D5w7Z#6h=wr5B?V3m~sMmKEE#_3$SS_fInSLc7uQQv7c z_F8lQjL^xzbtX&^sAbZ=kvtuU$0tB_^}=z zpnK)iOF0T-HthmkbA<`rtFRd>QPv+Q8#2XA5(^V=gMlIZw42c0Cg}z(nkvVJfA%&B z^|;NSSi1F7jny#kvwgv z)|Dyj6lZ!wocNFdvuR7fy5D^|6JVWkNH-wK;WB9iS7AA|PEYIjQXtt^+vHqMF1_Q; zmITzBgRD7lN9T7eu{_Iiy@-o3JXe-rK170WE5^}%EY(Z8)FCRW+#E=m;8hX$YTBQ%wOI%*#EC}g+Q`ug~K?k=NEzBIqYUu1h z4&p${hBcE|Y;w?@#bznhR3F{XquXjK9%llWWnYogf+>>9L1u(1Yn0>sFzRR2hCGC? zg!pg}=T@~dZIZRWVSSiTd+455Y8E=mVOa`>^w|z1SfAA~1+cAyF2+>+b*MKenGvPo zzVYfIHk%wIpBh4nlm>XAx@~u5+6}R(>)?$KfUoFhZ)q~*_g{oa1qlan$eESN@Mb}m zE+Q8FPr4=%Ce?%+@tvrO762-N77-ReK{LUrsHP zSyf5f$?nEQObHA%!d`ZV;uNwNi%aEQcx3^7dga=R2nl6#uOT%gvr^63q)98KIILSL zMh?(}CSZlVxIUNk%vNljs)ZeKvF!;wLf*{`HqU~g&2?wDX)S>C`q-l0=@*`=Fkj^<&46Cd6%|Z+CH)4LjI$k(I!Gt+ zvn`b%o16hADV3y5M$O8KnPxK4dd{0f#KmXNT!K}5PNK7TrAlXFM-iA(h#i37HOAg*YCbEz(Fsn9T zA9TdmB!vwjATW8@VW!nqvT;L{aonWAW|Ia*tVgD5%BkfVh=RILPblJ`?wV)lSVtnJ zUdq_V9U}sN(4KLL7@S23R}9|&#poYG7KyVwd9lxeDv zg)8%u_G3?6-K2%l`|;KlnPu^9OAEKLd^C_yMR&t{H??HzX42h?mb!fmF4YW!8;p0i ztVHx^SNWM2myNn?6e5TN!w1}*6l-YvQCZtaED~6=eyU18+3`eu_z3CfU7Xw<&Ee%I z&I|A)v3&IrMBRE5)oWS#-Ae;{$S%ZdcC*{Ik7~wafH}zGB#Pt*2%9dnpwNDC71He} z75GgJ;+SYV(&y@d>OqwvEqQSVd%{6(i}>hP)5>rmUoNx*l!`e$SgG-zqY7abJU0iC zq}QEEm0!|wvi8)J)3Pk6=OB)ACkx>oz=RtIQl{DYK__#7PTATq&yHaiodt=Q(b~b` zIAmL%i_%YA_5?PIE!4~z>fL!ZYmPUXgFzgC-I;w5WI8TeF!4E?sTc2BXB}p?4{(j@ z`$3r&sUPLk&&oc#rUpuUC(~CD3b{j7CooEO$a*a$6*!@0ic7-wF2Vg|dw54egs;GU zFJ*)b){i8G{iR?7ujY=ZQq`uoF*aP0_l11uoS5}I*5)=(^KncIW@5~RqdSV()*3$ zbG^WEss=fMi`75JR5c9RnU0tDWj2ef@7b3Hzv6IWdlQ14`^vsR?H!}e$%zNW4kn(rPVkY9Y@P(?zEA2>7+x3~5AR$zX+B>?99tO1>hC0e@nt?XWF{VSY|TIi zXWhzZte0A-r)1G$i=+dyT%0Eh&n-Jby!t6Qgo$*5O739Z31sn_@xblBK|tbBILNjy z#_2%o*}lNHP;=X(MM}_g_%3Q!!ZV%cOxF0U!kw`n74C??imzeKKG3NSrytD$j-xuW zIzDMWtM^1-^VieSe4`xS=xbOvX&#@$ye_^s?p1f{q$oZqf7-Q$`=i3i^H@orRA?9# zde$9Jkx5e<=4)JUnf0Al9f!vp2Q!WdI z?I2vssg*o{CG}b_!CSY-$%$|0G6X#Xpo`pZ*)d7z{O z#!+1+-nw#1#49SB4E0{Bd8y_R7(c7rdt2A{qGD&T?kdK$sn|yNl~N2)(=2&3+tP*0 z;0kowIk8^C!wjXKZIl};`*E|d9lTe*fGq3T%A!!=(2FjCD0*AhS*<)sJR}4o8ZK^L zxNvC{4JQsX1dHov9@|?UQpmRbGs-!fnIc)9*`72U*aPSlxjW-fv)v(!Q1Z00D~g7X|@F*UV7<7sNjW^_3GfjeYc2$g8xcV4 z`Ya`30nN@Pl+)oT_&5%lR7d__`oN_RTx)Di+sMiYZ$?_p?M{`Zb89E%RBItvMRUnJ zLC#ub2d;6#6UG^7_Xk^3lv_Zx1KhqQSR6x1_3K0|JJQeXQo%q-_Nz?PQ@oBI_NCcL zs#kkVbj->HCAd^~f5_HGF3fSf1@q6anK$66t6#ci?2z(xL&G( zo`OsC*L3FFxZx-4@6<<;i{>f&Nthwu4Q7iNmn~c%QV8|Kn6MJY#x(8_6p2KD@bZX?4MZE z;ncz_>nImm|AEBAZ>ORZKlY!WB?1UUY1N79q-$85!}yRZa_xnFHD zc*x(urr8czh}VQDlKC4BV&G^V?re7T9#RwfKJssBj3K&&SiD-xQJ$#Cbjm@8%i6Ps__%%vDoNaI_Qg$e}DpZfo)KYqYOkT@cJ zk<=8%j~Qvk>thCv;b@k&VDHWib-hsw7ze_>k#e&|RpCTHIn^k~gKf#E4-LVCur9T5 z2l=yDg*Ml{a%xTv{4>;H+p_2}hK_SH@K+d<)dDsVG#pMA9HDx6orewYCn@Pc#Cx<= zY%``tDa@5oJF8WuoFWj86wFoQXX7WVw_lp>j4vT&2Q6qrpqVUC1+(}r^Rv*9tK`$0 zk7c1RXX$C@58+4VqJ_lMIg%K#ca>lh&C@_6Lsla7%~5yuxyf(~>>52>v`dt6sTokF zn9qVSLldQs8AD6gswE6iTdw06EFQ$I($6^l84HSPt)03AwZ=I3=VVyFA+y2-KZ2qO zDlM)mp7lE*308E0HuO2!QML&QwLi=4ev(3~m(u!}DL&KhgrC}|&Y}uxLpPxr#+b1& zp)V3F0hL4@SrF8QNi5|C%u?$?1V2m)PO5^pL@k_TGRe5l^j0~wi5RI;g_@K7QHohK zNrUV>3Wm3SikOQAcrLtiKJ;59lrae}p(jetp%RJe1{ed9)A>nt{Vf^4SjW$gJ0DM> z*DM6enbBX+wq(CsVbwJ0c=}1e@C(++TMo{oSy1_52U2a(^>- z2YM-Ed;3^@r5E3h#?2f zP{NNVLkcn!KBY_di(X=Az4ZJZZJouF44s&ydz^p54CMtxbB-Kz8!)kOVFLVhWAJqR z3EV1U)YiMa9kbl0Ff6&i7!z2MHuUI}bf#PU*W7MZADdzNG;fW|7WDek_Wiaea$$}R z90<_S0!)6jQ%-$I+s=`D)p*vy57qp_tiBjAxw_gxwhnwC!E4*(uLehCM>{7#^;RJP z>n~_?&+hTSX~@iP?OO%2)b{4Kcf76b6a~U1qUP=<#2JqCNMxw|RHA+zs(U?UGSERM z2HhItZbB~!Md{F8o9PNgIe)NTs!5g}YRVp;zFguN9!ku`FKqAPblbZKWVUKhAkdlD zvOu~Or!sF>`9cyo+FG@&6LqArozqxOHPq9Erq+G}u>nq2yO+C64lPyn<h9hY)GH~8RkCGuuP3Dlc$M#k_^f>Y z9il?3!n%qKzKh#Sk3OJGHNbMS4 znr~B6>!9spdYv)cQ+CgD5!r%VHA@dwwg3T4G@?v1Gn6&=(ieVR@mIQOubi#RRbO(` zB{wBnjfF3>NbDfJuhq3pgy3vdgc_}%TA`;LN=)mx>*@UN6YUF+CIt>sAq0;Z9`#dj zdm^e|fSFF$d2~1({Ra0N%+4|_jZ2umW_180aQU;h#nNY`;J0Kc)8Y z{M4SQy}Tf`cQDxVgG+-;y&$+OxYA1nR|P}3Gc>r4^!4E|&kt`7Z}w8*E#Vw$4y_%} z+HL61NNw=KLk~atM6Xq!%P%_5JMvE#^&8*~KJV;{FYrcNUZ~=2N*N#Q0`63K8SriF zHSsn9+h#m#?zQk*dYk=^=HA(>?0)KDZM+i?>3JgXnB$Ia<6U!X_g-zhnx4lU)5iPu z#Oj`H{FEnbo7cm(;-BFAulMZlYpwFq|1K5&F>izaMBaw~a^A{+(zDI`>^7*6cUzx6 z=MVJm>U-W_{^UK__ks)0_a5ti(b;{xG2A%Yd(q&V=Us5g`QCfyUD)S5?<0c~3{K|u zMc%g;>gkMuf1uhL+J!7U9oGw8-y^T+oZ8q65fZ1Uky z!6pVbG1%1LrruWGKHfpzk>07^Io`S6Ma;MByj#7yya&A}yc+Li?`?0qH^KYb`@#Fk zTkNg!Hux#Op}#4$sm7n&-|#HMtFo$F)mc^7RgJFtq-vqf0KY2lax}Rt`FlfCRTwiY)DlfO-pzgr}Kk4pZYocxvCpv)!@C4b*b{?2w3++?o% z-87y2ZJYdUpZq=2OZgvpvx0Yn;lZ83UBTVKJ;8|J-r&CA{@{V&!C++YQ1Ec@NbqRz zSnzo8L@+8C9XuI~37!g`4xS0#3&w@pgxiK~!tKKC!yUqQ;f~=>;m+YMVK&?~Y#(+A zw+=goox;vxmvFam_i&GJ&v37B?{J^6Rk&}sU$}qRH9R2f79JQL6mAuE4-XCx2@eeq z3l9&E2#*Z^5FQow2#*er36Bkr3;!4%ANCAS2u}>F!(QP@;mP4C;i+NouvvIoczSq7 zcxHH3cy@SB*e~oGwhsG;e+vH`{v|v&91xxto*!NiUKkDx{~ERp`-HXOv*B~$^Wh8O zi{VS*%i$}*`@sj{tKn%sW&jquIj!|<)(qwwwE<8W;7N%&6qZt!XNUhrA)c{nb7 zKl~t=5RMNfh98C>g&&8X1YZP`f-l2Q!_R`R!q0=r;e>EvFeUsV_&WF|m>NzBa>2LZ zm*H38F28gC)VzU|IM>@Jp~foF1$QX9O#QRl%BIHDfso>D12K(c8(}+1tg- zdb@h*DR^?e6X2?dk31?ae&c*W1tA-|Olf;C1u*dw=$>_ipfR^ltKo zc{ejBBJVcucJB`FZ{Be4PG-p6-aXz3?_Tdd?|$zA?;-DD?-B1&?=j}hhu+8Dr{3q@ zL~oM!l{dxv#>;u%dEa}}y&u76j`y=S&s*Rv@s@eZy_Mc-FZR}Zb>442YRs?n)6B0; z{APZ0zooyqzooyG-`d~S-_GB`Z|Cpi@8a+3ckny;UHsksJ^j7?ef|CYu6{TFApa=8 zr~em!fPbEUzJGy#p+C^S$RFfi>|f#!_Am7>^Dp}a^{;huGzv92@zvjR0zu~{>zvaK}kM-a2-}T?~-}gW8$NL}pANe2qpZK5p zpZTBr6a0z(7ycywOaCi>vOmTD+W*F%>gW7#{qOu~{`dY5{&ata|D!+CpXJZ?=cKx) z4oMx6`a`Nm>X_8=sS{GCqOR%^du{oPQVmFVDa7?EA0IkbgG&{!_DuS@VW>n0L5$1bX2Q-ceo;?`ZEB z?^y3R?~mT`UQh1???kWK>*bx~o$Q^0=IHI6=AG`H;f=yddeR%?J>@-(?x^*i^`7&d z_g?T`^j<=fyyCs;z2?16-EVqtdGC1ddhdDTy!X*BA9Yy7qTIzRUF{(66dU-0YvU;W?wA`0A31*tGqk*Z8p zrP8SesfMXcs!^(Os!6J8s#)sb)S;;(Q%9waP92--nL06bYU;GqS*deUeN+8Y=cdj} zU62}>x;1rI>YmhtsmBsqKw8!5vsR>hXr3}Fw;!6!Sqp!k-A^VO);#`})`T!;vXV{dK7=QoBK)!Ks^5Po%y`%?z3b zt%A0}UcsM(i?G;lLZ`ls_E?9;+zq{UK{ym`^iepoB3;p{qHRUTihU{$t~jsa-ir4t z7FV`Fqc_3oZsoN>&bJS*VrcrMrU3h@4+5&VJJqz#VSO4%%dqN3$H_4L`dgA?N_v-Aq!{9#M zPkm~4D)3M?g|D{p+J+D4es~4&Y@cxtF7+!d&pp6Ce@90=CNTUJ*f&)G_D9bt*S)}V z(0nC2jbd_%d(zsTIGi81`#r5Mzo(VP zuV2s-*f(fyxpoEiLE{ROhkzx@KdGnTD0^CJ?}u}yOMMOOo0_a#NIzjV8h8%!utcLz7&z?2}py?3-F>xt;}{gJdn$d%67zf?&pVoB{ZjBH zn2Rs_BMFr2b>KN@ffD{2yK|sQiFY31cmME4U_ZZ=wXuceDD_68(%(Pco@`@J9L`VJ z{S&M&zaODtJn0u~2J9QOuw3ndeb7E?`5j=1cQkVSLycUYjnsQu_flT~&q-7*gx3b^1Kf$@lK8I`v=$^M#n#TV+z?#F5N@UC#mBsRiEnDa;IM^5A2Hu z6mBnCo)>_>$EF!~`UjgRPtX|HH^?YYz*tdxy*h(Uxzj&r3hWa!0rm|V0nZK^0)L-# zTkiA^S_At8TLJq9%xH4D8C}25HkMOk(kIy3ax#A8bmJ!uy_w&lN;B;7UC?N|Vux2? zhwp(b+Y7t9iP_yPusr^N9O;4f+X{>0MC4X4ER42hVPw5q_}|_e$^Q=CGyL!9jpctQ ztb#ROXa6w&NUw|kqW>Z`y?o_Pz!bLg_J8*)MQdR=Gn^I9PU1kHVu4J@;_%TSDfp2- zXzmBl-#a-N3Y3Q#DvpHJL#_NiiNExi+%4?cWu%X>)D=mJT$KHNF6JzDlfPA}ynhS5pcwjLDp(t= z3*sOj+z@OC3gl{JzR<3CK6`r3#_N$-bSa(%L0#}`@GJQO#-T?5=P?@Tuz47SVOSYf zg`0#;L)jteB+|7xx@Ako3NBGjQi^}MyRD5f>EQZceRxqgD7-klBpe)Gn$&q|`7W_J zB6t68o#h(Uj;NjfJw9P`?o|d|fj%*>0Jt@mi`9A91}T2EpsYtK6&x2Fhe=fvyu#fS zPaB45?<(&SZ*bTkY#3(3Mq%TSnX$2q>PB8}{Qa+#Nx?}?X`?oHHh3<0K6oK`F?cC> zId~=9ENl^O9&Qn~47a4lroW?OE9kd1c*@>hgt^fM3RG)!^26{~f^tL z;f4w?>{anoWxBFsW+>7$I}9JSM9U$ngO=@Pi}!hn$aPf+bFdS|_=yiN40TBti! zn;Fga2v6e|Pt#9`g6XF#yvp0sFIRRQ^V65tfT(38Xlzf8lDrjsO;EqI5|$UTsB(4 zj0&&8y2_5a({PN+HJsM)r>cD#F3nVCw#aM`ZD(Y;{(8v{{06&_e%S$MrSk{*yuXPhsea>>y|h2NIM4sYYv9i;{_L+Q&QJ9$PD%ZxSerVxI4*TQzb_~*Pu)^npL(tMPU;PQ zk1N)s-YRJk8q7D&J9Kv*9T7)7X)L9%Y&zjKYOXv)?NiUS2M301G^VDcxM;$ z)D^R?a0^%rDQ@tuFXsJ`#kKw?{GM4{LH-rN@M0W1RV;w}QRMAiTuZ%q>W!&amO!uK zYVynh?@zpj{(P??xUK@%)!@1c99MzkN@`lgn%nT=Iy7GyRnxo~-A|dY-I@3XegBWnlX(*iHc3C(ubJv$}FiaSFX$?mrIAKPl$?`PBVVaTVCH z0zh3=#C^l!QhGL>+UI%~6wl(+g8L|kby@t(8(aJu{HOUNiXZq76kqcnD}GE}uhO5% z{+Qw${?o;={xijwsC%kE)2pJ7tNgjNIluUhzoyld*L-rVCD)pbW3V~YSmNIg7d!4<0TtFjh1F0YHctK-o?25}N^h3Z zlg0F82|dvWuSvazUHb;t``i=uzps6*x4qX8425lz;zDp~03WUepQX&7wZfe7na21m z(6}-_a~L1-*CL;q7^AuVyvepbE-LWvrBOUys%KgZlwbY)c z_Vrf&^4=JsnMYJ~OLBaXkwP2GuH_Qm^8h^y*XZ zg5Tyt!$Y9qIneNaX!tZeeHr@Q3f(S+Zg)}BU@&g2$ht zmFwW~+o=6@c>Gp){5E*}CV2c-Fpp^aF|hv*9={zPe+(YKm7cr-k3R)x+yZAj0cTtV zXUu`euY`Uhq2EjJ_|5Qmirzj5mn^{x8c82h;P{icPQT$29~|i+cNB*hZIqsaBpt!H z-CtZsPuBX6AmhXZqnVXsiVNw<5_<9rlu`~#@)U|8pWtGXdTTfGHe<|_yMH&5-)TfV}#xaxLFU~Vf@e{chl4~Kk)x~tu0_b_ndp@@JZWBx zsdF8AtSe(kz2{gtzgtv zFluWVv1N?bEJg{P!Dy&0;^bgZ!ss=ydV_Y+K!#1mv;{f_jnTk*b}nTvpx#@k?={kI z&<^^C(OwTdw<@luPU*b$JXz0^^i zlIS8!(Z^LbGc+S(=&~W1S$S~G(Owtai<~S;hE0th9xs0DKY?@_16}7Aza{6&R2O95>y&!atN54rq||agg$GK@ zd0G}t^Kj*SZ)R7Xd6rOOIx&q=lz7tg z&N}X_wL7vR){$pDcd$vwBYQ>qslc6B?IiOe#TsiR|2=elM)ni`Q}~uyUg_k)S*+mBL{cwc9cloP z`X%1^tllp3E?@Zu^@@R(Mk$ zQFp(QcNwd^ExaMDjcnoF$huW4?P4*k zcJOXv?YEP6JL|t)ygOI}-rf5f>%e<@!&wX7+q;wX;I7_XtOs}V?qyB*An!g_UV3=< zvo_q*d(f};dwC;SCH{-|Fe}9udXKP1Jk%S-TJX)@%US{U-tzD8@9@U@cM^Ge$G_XZ z$9vbm*T2sj=Re>-;CV!fNpZ zZyIaElf9o=70!7JwIb{-Wkq;~_Y3R6v%Qt9y)E?Cuokq`E3nqKf>okb{wnWR*4y%| zglnBG@P8xVQjtof0zaLqNLBc@!scgKUu)_&PPIt2^qa7fw1wX^)h@M*-z;@d>JWc3 z)|ij&_?mZCH8k?QfSlJ$1Ujz1E=p9ax3# z>+i_A+QP4+25U2yIcJ|Q@5q=@b^mH zm%88IH}z2JA%FkWBdO7T*VI#~r~N}x&!(RD4@-TV`rJP%pc|Xm77`=s0u6L zVDEnRW*o^X+8S)DeXzsZaqYx~mFB<7^)A;`EVkphPUq@}erm~nh=xRF8!&4!%$*Dz z*1+GFzx#7t$aN!EsTDZoNnkHR$T5KfrWG=*B`igaGk(a&20H-S<_4}Cxo+Yb#&t8- zD9VrKdXfu2h%-8udrx!KP`;MyS+3`}p67av>vb+PruQb-TU_tZ#=BhaagF18pSH$x zeMsFOQP0Qx{e-`t^7k|TPN2*eTwiic=K7lJTdrwbKXA?9n#nbr>nE1Q`q%W-0^>1Sl#Yl~moE z%Bd7^`@7DqTXi$F(x1&%F0u!(MCcvvKT<6c1 ze$hQw=;_fgy;6FW^H+m%r|hxx+wfgrv8A`c58!9^e-3xUy?_oWI;6jsu9QA5eM0)A z^eO4n(r2X4N}rRqNLNXpm#&t+AbnB#2k9E=OHy@C*GgZJzAAl9`nq?mgEycZv`5+r zWr+586W-RVC7^AJ13=pp8(}vX3c~=ci)dZkgMH8hV_`h(1^d9hupj^G0GJF@;mFGM z#iQU@SVB%Fkl6Lf9R2%T95f&I-GWM!*cDUYy{I(X{@maS`=6G68Lol5?cW1Wu_Pz6 z5vIb)@@K<2YIz=N%PCK5iwTV}&| z;U@SV+zeaTUen<{FcS`jL*Ov@6^nzFTO94WC2WWI8nuxwDbpoox};2(lQk*h|%xyR3%#x^}M=0jutZR}^pwvS83y?gv+d;My!)q8jD^H<}NeZH-? z*j|%|7QY>~*Tj!FwgTRv7R|M0XhfPL*LF1x7n&=y*7h2!UOUw^Q@!>H&9l9x`6C)& z8!0E@M!Zh7-AUiFjlK_kdU(-cux=2kIsp%p$U8JUq)O3-WE>hD)YPyUz4dP)9fip!4 z&xZS;nV&TWmhr~ECcOzZ^N~it6qp9*!Yyzc{6OSnKR5s;!&I2Z%AXGlU?D7m6JV)# z%}+cfnGT1*VQ@A4clZX}2;YQn!FS+c_#He7zXx7m@)E3tSCwoyj0ApNVKf{A?}rQE zvv4_x&lFa|lS;cgC~5j`I1pyQK`;x>fDeN*q~4!;e|it}z*}I6pke`vFc2DGFbsir zfwB~prKp99Z-eUxxPCyYtWoQdT9?$iq}C<1E~#}%txIZMQtOghm(;qX)+MzrsdY)M zOKM$G>ylcR)VieBCABW8bxEyDYF$$6l3JJ4x}?@6wJxc3Nv%t2T~h0kT9?$iq}C<1 zE~#}%txIZMQtOghm(;qX)+MzrsdY)MOKM$G>ylcR)VieBCABW8bxEyDYF$$6l3JJ4 zx}?@6wJsUCo}~AhCoVQ0z9fGod>Q^8z5-XlEzaF9eE=R5v6)?2r+-;jI6+>VSs$~G zz0=9w>16M8>TlK+z5>@kv;Bvp&x&4*)yplBo&)E>N8v*FqT}A3UMGJicn>?GlMT_y ze&}R7bg~;d*$kcRh0gQ^_yfEI;<{{uPJPxoebzdC);fLGx)|fF+(0gGP{UR=YgMyW zHEUI~RyAu?vsSfgRjXFDYE_$7wP{t8RyAo=lUAi~Rq9qHY*oTmC2Li(RwZjyvQ{N) zRkBtkYgMvVC2Li(RwZjyvQ{N)r5A3X7jB>zZlD)#@Fmg`k*?$5cvuQ2+hF}Vlsg@1?p+}{k3 zxMu}C1}otScm|$>Rj?Xfgf;LoyaKOP+6!c;K!yrrs4x^puv&J9J)p_?16eaO;2@X< zhr;17r_!MxZf6^B)El%Hu6OT0$$wS;YtE5jnyZ5yyk7s@u77SX@DbR-8|mTo?BI=s z&>yXiuamBaH=rFh!Y0>rNIRkAn$4coCC%(_1v;TX#?u5+F_$9H3Htl?R6WvRKp)VW z9Vv^A=Io$3JJY0AsK0T@b zSkG=~*N3d9i92ZG4w|?l{jqbm!yWK5=YI}&!@cm3W3Nls!5h#Ho1hcQpnm$J_3YdA z?A!J1+x6_*^~FKZ2)k9<*&OY~VSrBTj&}C#M)vJS_U%UY?MC+PM)vJS_U%T!LWf?V zL$A=GSLhHU8DzX+2rLncIu4G9rEof&0a^e@w&TbQM`k!O!;u+|%y3?Y^D>;5;k*pz zwd1^YoR{Ie4CiGyFT;5m&dYFKhVwFP}PT+ogS+HpaK3o=}gp?Zeu8LGFVbcWIyN@u8>p=>+Kwxeu2YG$aJp=O4f8A@g- znW1Eck{L>7D4C&ThLRa-WvG>*R)$g;N@XaOp;U%a8A@d+m7!FIQtc?!j#BL?)vo{E zp#R>W|K6bg-k|^9p#R>W|K8xctwF|MhQJxQ<**jT)}l@~>U5({H|lhw&RW!2tA)F@ zaJLrj*23LdxLXT%YvHw8w_EEzu4Tj0SgTdnYRzt~*{wCZwc=W>wpOdH)oN?C&{{3B zR*S6FB5T#CTaCKas9TM?)u>yIy49##jn=BsS~Xg$Mr+mRaW#5ejUHE{$JOX@HF{i) z9?$L8|2tm8|3e-_NTa_Yu)WW8syImaO=pV9pAEOmZ|^_-t84xZegVe5^bKMsu5E@# zT)zU|sc$txY+`rdGv|KR9MSHouQiASR(-7)m$?aUc1_LixMc`FvOa3&B_|-)<1!&X+a2BR=ypf9JG$M`?T&7Dbi1S59o^y(;UC0Z;Ujzm z?t_}2Fker&0DNuG=P$(5_zd|I>-~m!`nh_c`ZwHSzt@k5w~^d!e2L_<^v!GZ&1>Qj z>%=40vnScp@+XTOoC2r8nXFA(R0&JDJ$bid_sKW2fE&an2Ez~-3T!xWjCHx4Imh|1 zHBW(MMt#H9tlF1bbNljo*MH45H%f1k-tGK7U=)bmxmB!VUEyi@XXRVi2(P-1W-hFg zu7@|E9X3J-bi!uO+2Wc^ek=6A+hQXL3T$sM&6GH5Oy+eBe#_& zOSLr}Tq@2p);=E=z(QCAC%{s)-5Yh+pkh!k`t}UvGL%|_Qfu@U849gIi3}AoR9J%w zYqb6vtr>l5rkBXHPNp^1XoWTSENZ<*EiyI8ls@{oOvy66LZ(ESzU&A*GKa33hr8!X zgUiJsy}9JgB|K8bA7yVZ;f*rhDDyqK@kUuGN=i}c<%MpgDfRL~H(n_B$<$;zWGXDd z{UHG-Ye#&_Pbs5OnV(XXx$_(AcDuwEu+AuTnaZh)@@u_L@=dM)3H*KR+6*{B{rq+mOl_?z(FtzPImkZ>4)KL z`E%erI3GfCJ|Vrtb1sFDn$Ow)qP+G@wHM#H%y%y567nPGe(Cr<_O)BO9JHNzNsVSG zHKtF()9@^`!1M3|n6;Yv$|_w8uR@Re-vTQ{@WIP`@G>8~Tx@{BFa+KO+M5qvCV`u9 z!zSFY2{&viiqP}P%Y5=OpS;W`FB=a%0*{OE;Q7#_8R~|lb)!%>3S}r1($G+ zd+5TDy6(cSD?M7i8_#de_1~lRpR)h7eL7GJZ`HzE@%mOR+N~wKwPdCxGkm^PYj)>4 z=n|vtp>uAATfsLtxi0yoyU}Whue8Pj&8H^;;#n$)quYm@K*!=YQSF&_^SbbHQ=uX z{MCTJ8t_*G{%XKq4O%(0b!h6)(k0&t9U%rYPi}r5m+91Uo%kYRGM!qkQ>#ThCSow1 z_@bnhI<-#%`_z!IfDSxn_@I0w#yk2-d~^g`(+ zaH;FAld|#f#%8>+8E=%*U&8&coSigLOlE&jJ|*u=CMtP{l1GUW zs*YRLajTi@CzFIz;4}zphc8%YfWa^X_Q zlJ#0487m}Xg=DOdj1`il1HrRcAz3;c=0Vlk$XFT~OC!szimD}%u_Q89MAmCTWGskG zBqC!aWUPd&*Fwm)i8%M@>3j6_J$m{cJ$w)QEBwG7J$#Q|y+^O!qgU_ItM};5d(vCL z7gc)l9zA)Fp1em--lHe)VO#7v;yi#{ZXst|$ki6Iv&FdcAnzTLzccQQZ}WQCB|Yqt zp5h`n0hYSvv}B2PR` zw`lzq5u%oz#w%++>2mk{79N5}T)P4ugO%_EJOj_cDp(CK!WwuPUV+z)Yi=8xY$;Uz z)0VWVLyQWosgRk!*wpfy{`l&6b)shxr5Dck2msWjM zF+Z`V(UW;f^i)c}cXi-ZVzUrl}y#lqagsU988k7MwBR1HAnqq^l>-Sq*M37oi z6x+gEPV=|0g*Ff=b3TyEEPFR*MRaHC(n$LN93tR5j`5n}C_m*Ew74V|0d*q}Fj6>fv?Lo00X z#bQZn|YI@oqijBF zURdth*QM*=4QPi=;5)~(4Bl-{=OD8?2bt$N$h^-%eLj5nSKIpUwK2Zeap(rUQq^zQ zzIvf;efNk_R=vs4`HY8Gv37Hw)4ZE7Zs&Gch4{n%XCr}A)NU)T>0fRo{7 z$M1kmmFBde^04SnGs$Qs70o!bSwyB;M5b9prddR$xi|*2;^O-{@@yRi3Fl;cMS_^kc30ReVWo z8?y#?^=ov#L_+$0?56xz>CAJ3>(BLFB*U`W?$`Uy%_tVO3BTGq|M;cV zcT}&_XD8+eWINuKpV{Vc<+aKSmHV`3Uc1UlK0u>Z=Z@|jRs>(lBeun}N zS01cfqH!4qhX5PlNcHQyzI8%9P+cvKJdt6spqsC`Re^vS0phxAWQ5U~h zMQo=(oYLUB4b_u7@=S%TT=}y~ss9Y#Oe){o>BF`;xNZ5iIhDjSJSFE)(Ywg^@?V~R z-u?{`HE7R|l}G=y>uOg;DXRLTa&O5tle@6ny!8RvHX%>KJ~rZ0P+xs#gL-M7Nb#qEBT2YT7mCxtfS1Zrz&*B}u<=nAPtZeMlUw!o4*{kc{;e1*! zisEl?BVR*oNBG;veD>XKC;KayH84eAJ=I^)dKK*NeC4=QY-EP9lbQYo^TZG0L&iFC zsr3{Napj@@hWMiS2+yAFZ>Vpm=a}m?*WU>7s(JZ37xS$bdZfRRtkh%7XE@g1X!hz7 zGw_f1*Obp{7?ZEI<-0O}V|k#bxh7^fjL&B{j7!e)x2KW0v#r1I5r2D`=Ww+$Ut^WJ zf&440zF4L1YfANXf2lQlz9s)%tJDqkHTNyb5OX0$v59Z@-k1^5WJbi@q8I;aUc^}A zc=vhoZ~Tomp4TkD+~26=xBf<1FXJKGhyBH>Hjj8h%%B*+l3pPn^C$+es2`J$*%Sj< z)+^;>PDL@FQ&BXh;%Vi4#$S{5@t&1`&R>(2@>=9q`D?N!$Mf>5{WV!V?*;i6{WY11 z(W*wT`x}$b$4Je_*x*Ra$4K+}7->EqBhBYy49(|b49@3c?3T~R*e#!rv0FYLV>k0L zVt!dep&=P#{k=x{!PdALVkxAnJtiCXr{+VtC}s6UuwR` zsC>T1pu#f#=-y^}{H6G$bph4og2Dw#`)ObA54SqtMdkzjP2q1uEH5cs;+jhfmwL`; z3ZHT1XA7To<=+;pWn$gHzw_n)=L(;fzr1j{<5v`}@a_MVg)7zQ%jN+Lw~F9bl40hM zTHs63(J-Kfr414)d(MS??d9T!>md8h_fpS zD_s9*;rH?@%|a=fh4O^bK3RCu)QG2i=RU|fg-?6hv!b@ctXBA(D_0d(*{(K6Wr8^> zFWSCbcv(xnS`dM^LR@RoWR}jJ!9UBc^Brr*qZlZ zYu<~kc`s&En)hObsIz#Rd&KM)KH@p)Imz(!TycbP)?z$Q{-dJO!>m*HF;6oeCK+Qs z%wN0rLec4A=|$;9j+-0fh`BMgm#3H8UXfmrj7qOeueAM2`W4ZJtN5@@Fz*ZdjV@1@^UyPL(hcTaCgZ&99G#kzO5Lfmcg-xu@V zJ^g`k_i^Uf{IgpBIQ_8}xFfy8_D)goVd-7zU7q~gly*uVjMdN5hth|X|6%h{M#MZE zTk~*iS6Z!WRQiOKyGEr?rBB&DlRjhnT>6~ts&tj@>U6d3i|LEDYtl8gFQ@n+eIg*T;bR)rgwuH%h2rR&r6@^A3Uo6-&G26DkE zhkPe*y(ulFC3#1fIqFv@<{pLUZ8>#27 z{q@&k+xx1^kHj{v2wNroVh!h5>eR*R@h|1auL`-`Jt<-}jqCDz~?--m(jt4Dup z#PhaA{dJN5W50fi?EK2H4p$M|sAppxwtarAF;$OPi0`Sc1XkSj&+=Z_Cx!S z-;&ok+9kWq-}V=hQ!QJp%N4o!qi6hu_e9-`kz?6w*UT` zR;xZKXdl0Nd5w}qIilVnBQ?~@_NS)X>xUip{U$1}S+~__zuF7X{c0Z~# z2$`;;o?G&lZL=ApKM9@O;BQpEjkNlU+2!$zbN~PMf4&83eVLV!_y?i0N07k&mE(3? znSI+e*{f?(AI~?gtwOzwCtO{7`b)`wWv%v|uUK8bPW64f-)7&CEcXq`Z(B<{tW;1IezP^d=<$T_^7c~z^>MKZ{TGP=3VkE`IRI1le_aH_vl@} zdJ=ze$93N4@B!!X{f^}GE#&K&*U!saw#}OFyIS%6^Srhz^3~qIlCSfAJ+JJ>n8nWv zyCb|WzE}T>@3r;bW3~4Oc~+0qt@B=6!#sR`#kKs2>thB!|KWT5hFkaxxA7Bx5OeVP1$XiX9^ef;$P0Lw z_5Y~Z_bb!KS^ZD4`JZO-Kg-^4VeLQ9)_;Me{|9#dORW60Z2VVQ_^mAaHWvMQmiz`5 z{6?002aCPLQs2x%-@-EQVUfSZ60b0}3oP%jxCgSd8(7#QSk|Li)cdid4`4x0W;st~ zF|(H0$_t7Mi;LLFY~-+zW5rIkas2Y_s?i(rzHj-aku&1ps-{7$zCk)T^7${Kg-|FY z?-v6&D*bpe$ND0N^I8r`|C{UXN@f*KPZkwE%!j>;cXYU@)^gtND1L84a%|zdyuyQ% zBj8kc56pn^uoM=;WOy$eY}NHeVm71rwEL$ABqyX+lj1W^vO4N)p53tY0?)YG`~OjT z1WbjWB=dM*r;3`K!IK@&dptEApPX9!q}1{BUtE7lo*t&`r=%w*Q_|D;Ggl=?rU)f)&PYcl?@{Ny z9h=FIK3dt2N!ooMx>eeloLKynYp1}R@5~mYP0390n0;X%v7UKov{Zy)Vqt)BYcru8pJ&~hc_J1EqwBkQ zZc~bblf#v1V&3l2_S+%WS;=V;bf|-GF+0%Wbm?YzgSWkVA02`g^%$K@w?8|lMbM*$ zpvC)>lhbIgX!o6O>jxd59N9;QpvAV>Ut1g)w5UhW zB3+mqpQlmpc>nfk@L{}vS84F*=kQ6?|0rp+QIz$MrNPg^Q@cuor}10*zm@ASayh)R zT0K`vU+7I^M(#Q^7?N~W-jbfGJx(mXH#wv5QgV!Xj!$|j&xf1@ucil*!|{n#V3Q>w zH`3{miUXdQryt4Fk%eK&$efPB(@}={bc%MqJNbaNT$0l_+BoQ3mll7S98$biy~toL z2i0_1@BTr#T#}7lrerpkn@G2Ji`g7@gSoPIy1w?k)!zWhhg znVP=zziD#XJ)C?n*PC;a{c?R7?X-f-g|7U$I({p8Z!X_){7`MPXL3-`zpywtC!Lra zn;$z#pAoU9!_hs?4@9^HZzst%A=X%CP$w|t1T=Dbhav6RMy)#w)=O*v-#m`bv)uvpB zyk!NI9_JU4&4AUZ)O)mVY~M$3otU>r9PjO~3TvwUi*Ixf&fD)K=O*YkrqT;@wezRM z*&5xu4`}n^Oz8n4nl;^OMnV4marzg09XuPlHTt{aNy(_<*`9ZJrIMG!dvm#x7B7HQ z|8tQq!}d1M?+fa+PCd>^W==;s}Wcb7ATkAhUSD;g_mqKPjKSfI3 zf@a0bMWk;c!}um>CpLXNy|+-hIQj$e*9qxKX2JHR!Q*{pjj~NpHawd1$%A+<&TE9@EX)VR~ zxxAB;oPV}YYxO3_!HXd;InTaN`+S=n*4s{f>8I#9c3Sc9WIFyl*#8n3pMIGfj!C8( zIhk90IGHHEJfZk1d7ejye2Sc=c;x~8+~suMt=czqR;|r?+aua2Y`a=NvVB@>ubrn> zh25@Q-|>E}{dbw_4@O#@Zx`o-wqXxQKR}KbZr@MW?eCwOHCwm8O@H3kAJ+QCU8URi z%f0<)tsmWG8vGx2*sjvYD+>#6B}ws3Hc%}+Eg{|yf8NiI-kUu=zVJhR%~Uo#n;q{BFCLnlGGJnIO7T0%UUcHoxlI_p z(DP(^K;A!%NWV^&ZnXcBzU}$s5W^TBNdKCwU*`WiB+0bmGJJe0S)1rxvy(%u@px)_ zyzD_RJKk%oK=kNJ;e?gu;;oNNHE6OiQ4orNjBt9{#@Sx`{PZs28PYz*29^yOB|5;eiUwDu` z6E@SK>b)nubdbJlmUe4}RrZ&Vr$0*;`wv?u82=od?(O(vj<e@4b6J^*`1-qb)oK78_|Su#0eE1yy!dH3#{+B-OMd=^-l7B$Y zL(n|hWFNNP8GOiUyY95D@%+Jf?f_$SGx=}}@bBE@O>1`YIYRHS8{Vp>$)cP`W|=b? z2s#Jf>G8L2>z*mPw;6yYCK!_u6TFOzrn*b zG-ZB?$j?wNl-?HY%Rr{np@k7KlMzM{rLZ{1mb^loWJI!pO2YY?2J contourMeasure) : JsiSkWrappingSkPtrHostObject(context, contourMeasure){}; + JSI_HOST_FUNCTION(getPosTan) { + auto dist = arguments[0].asNumber(); + SkPoint* position = (SkPoint *)malloc(sizeof (SkPoint)); + SkPoint* tangent = (SkPoint *)malloc(sizeof (SkPoint)); + auto result = getObject()->getPosTan(dist, position, tangent); + if (!result) { + jsi::detail::throwJSError(runtime, "getSegment() failed"); + } + auto posTan = jsi::Object(runtime); + posTan.setProperty(runtime, "px", position->x()); + posTan.setProperty(runtime, "py", position->y()); + posTan.setProperty(runtime, "tx", tangent->x()); + posTan.setProperty(runtime, "ty", tangent->y()); + return posTan; + } + + JSI_HOST_FUNCTION(length) { + return jsi::Value(SkScalarToDouble(getObject()->length())); + } + + JSI_HOST_FUNCTION(isClosed) { + return jsi::Value(getObject()->isClosed()); + } + + JSI_HOST_FUNCTION(getSegment) { + auto start = arguments[0].asNumber(); + auto end = arguments[1].asNumber(); + auto startWithMoveTo = arguments[2].getBool(); + SkPath path; + auto result = getObject()->getSegment(start, end, &path, startWithMoveTo); + if (!result) { + jsi::detail::throwJSError(runtime, "getSegment() failed"); + } + return JsiSkPath::toValue(runtime, getContext(), path); + } JSI_PROPERTY_GET(__typename__) { - return jsi::String::createFromUtf8(runtime, "ContourMeasure"); + return jsi::String::createFromUtf8(runtime, "ContourMeasure"); } JSI_EXPORT_PROPERTY_GETTERS(JSI_EXPORT_PROP_GET(JsiSkContourMeasure, __typename__)) + JSI_EXPORT_FUNCTIONS( + JSI_EXPORT_FUNC(JsiSkContourMeasure, getPosTan), + JSI_EXPORT_FUNC(JsiSkContourMeasure, length), + JSI_EXPORT_FUNC(JsiSkContourMeasure, isClosed), + JSI_EXPORT_FUNC(JsiSkContourMeasure, getSegment) + ) + /** Returns the underlying object from a host object of this type */ diff --git a/package/cpp/api/JsiSkFont.h b/package/cpp/api/JsiSkFont.h index 2549d3a67e..2cdcd590f9 100644 --- a/package/cpp/api/JsiSkFont.h +++ b/package/cpp/api/JsiSkFont.h @@ -47,7 +47,6 @@ class JsiSkFont : public JsiSkWrappingSharedPtrHostObject { JSI_HOST_FUNCTION(getGlyphWidths) { auto jsiGlyphs = arguments[0].asObject(runtime).asArray(runtime); - auto paint = JsiSkPaint::fromValue(runtime, arguments[1]); int bytesPerWidth = 4; std::vector glyphs; int glyphsSize = static_cast(jsiGlyphs.size(runtime)); @@ -55,10 +54,15 @@ class JsiSkFont : public JsiSkWrappingSharedPtrHostObject { for (int i = 0; i < glyphsSize; i++) { glyphs.push_back(jsiGlyphs.getValueAtIndex(runtime, i).asNumber()); } - getObject()->getWidthsBounds(glyphs.data(), glyphsSize, widthPtr, nullptr, paint.get()); + if (count > 1) { + auto paint = JsiSkPaint::fromValue(runtime, arguments[1]); + getObject()->getWidthsBounds(glyphs.data(), glyphsSize, widthPtr, nullptr, paint.get()); + } else { + getObject()->getWidthsBounds(glyphs.data(), glyphsSize, widthPtr, nullptr, nullptr); + } auto jsiWidths = jsi::Array(runtime, glyphsSize); for (int i = 0; i { .get() ->getObject(); } + + static jsi::Value toValue(jsi::Runtime &runtime, + std::shared_ptr context, + const SkPath &path) { + return jsi::Object::createFromHostObject( + runtime, + std::make_shared(context, path) + ); + } }; } // namespace RNSkia diff --git a/package/src/renderer/components/text/TextPath.tsx b/package/src/renderer/components/text/TextPath.tsx index dec4a5028a..2ad0999855 100644 --- a/package/src/renderer/components/text/TextPath.tsx +++ b/package/src/renderer/components/text/TextPath.tsx @@ -4,9 +4,11 @@ import type { CustomPaintProps, AnimatedProps } from "../../processors"; import { useDrawing } from "../../nodes/Drawing"; import type { IPath } from "../../../skia/Path"; import type { IFont } from "../../../skia/Font"; +import type { IRSXform } from "../../../skia/RSXform"; +import { Skia } from "../../../skia/Skia"; export interface TextPathProps extends CustomPaintProps { - string: string; + text: string; path: IPath; font: IFont; initialOffset: number; @@ -15,43 +17,37 @@ export interface TextPathProps extends CustomPaintProps { export const TextPath = (props: AnimatedProps) => { const onDraw = useDrawing( props, - ({ canvas, paint }, { string, path, font, initialOffset }) => { - // const ids = font.getGlyphIDs(string); - // const widths = font.getGlyphWidths(ids); - // const rsx: IRSXform[] = []; - // const meas = new Skia.ContourMeasureIter(path, false, 1); - // let cont = meas.next(); - // let dist = initialOffset; - // const xycs = new Float32Array(4); - // for (let i = 0; i < string.length && cont; i++) { - // const width = widths[i]; - // dist += width / 2; - // if (dist > cont.length()) { - // // jump to next contour - // cont.delete(); - // cont = meas.next(); - // if (!cont) { - // // We have come to the end of the path - terminate the string - // // right here. - // string = string.substring(0, i); - // break; - // } - // dist = width / 2; - // } - // // Gives us the (x, y) coordinates as well as the cos/sin of the tangent - // // line at that position. - // cont.getPosTan(dist, xycs); - // var cx = xycs[0]; - // var cy = xycs[1]; - // var cosT = xycs[2]; - // var sinT = xycs[3]; - // var adjustedX = cx - (width / 2) * cosT; - // var adjustedY = cy - (width / 2) * sinT; - // rsx.push(cosT, sinT, adjustedX, adjustedY); - // dist += width / 2; - // } - // var retVal = Skia.TextBlob.MakeFromRSXform(string, rsx, font); - // return retVal; + ({ canvas, paint }, { text, path, font, initialOffset }) => { + const ids = font.getGlyphIDs(text); + const widths = font.getGlyphWidths(ids, paint); + const rsx: IRSXform[] = []; + const meas = Skia.ContourMeasureIter(path, false, 1); + let cont = meas.next(); + let dist = initialOffset; + for (let i = 0; i < text.length && cont; i++) { + const width = widths[i]; + dist += width / 2; + if (dist > cont.length()) { + // jump to next contour + cont = meas.next(); + if (!cont) { + // We have come to the end of the path - terminate the string + // right here. + text = text.substring(0, i); + break; + } + dist = width / 2; + } + // Gives us the (x, y) coordinates as well as the cos/sin of the tangent + // line at that position. + const { px, py, tx, ty } = cont.getPosTan(dist); + const adjustedX = px - (width / 2) * tx; + const adjustedY = py - (width / 2) * ty; + rsx.push(Skia.RSXform(tx, ty, adjustedX, adjustedY)); + dist += width / 2; + } + const blob = Skia.TextBlob.MakeFromRSXform(text, rsx, font); + canvas.drawTextBlob(blob, 0, 0, paint); } ); return ; diff --git a/package/src/renderer/components/text/index.ts b/package/src/renderer/components/text/index.ts index c1c76b7f4b..fa621bb50e 100644 --- a/package/src/renderer/components/text/index.ts +++ b/package/src/renderer/components/text/index.ts @@ -1,3 +1,4 @@ export * from "./Text"; export * from "./Glyphs"; export * from "./TextBlob"; +export * from "./TextPath"; From 1e20c19f5eb629c6ffed7703a550c739f8a6418d Mon Sep 17 00:00:00 2001 From: William Candillon Date: Sun, 6 Feb 2022 18:43:20 +0100 Subject: [PATCH 22/28] :green_heart: --- docs/docs/text/text.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/docs/text/text.md b/docs/docs/text/text.md index 98170060af..b32fedac81 100644 --- a/docs/docs/text/text.md +++ b/docs/docs/text/text.md @@ -26,9 +26,9 @@ export const HelloWorld = () => { ); From 641ef682926da16419bd3cb82880cc850ca19f4f Mon Sep 17 00:00:00 2001 From: William Candillon Date: Mon, 7 Feb 2022 09:14:13 +0100 Subject: [PATCH 23/28] :arrow_up: --- docs/docs/text/fonts.md | 32 +++++++++++++++++++++++++++++++- 1 file changed, 31 insertions(+), 1 deletion(-) diff --git a/docs/docs/text/fonts.md b/docs/docs/text/fonts.md index e574e40733..99ce54f85b 100644 --- a/docs/docs/text/fonts.md +++ b/docs/docs/text/fonts.md @@ -5,7 +5,10 @@ sidebar_label: Fonts slug: /text/fonts --- -By default all the fonts available within your app are available in your Canvas. For instance, you can write the following. +## System Fonts + +By default all the fonts available within your app are available in your Canvas. +For instance, you can write the following. ```tsx twoslash import {Canvas, Text} from "@shopify/react-native-skia"; @@ -25,6 +28,33 @@ export const HelloWorld = () => { }; ``` +System fonts can also be accessed as a font instance using the system font manager. + +```tsx twoslash +import {Canvas, Text, Skia} from "@shopify/react-native-skia"; + +const typeface = Skia.FontMgr.RefDefault().matchFamilyStyle("helvetica"); +if (!typeface) { + throw new Error("Helvetica not found"); +} +const font = Skia.Font(typeface, 30); + +export const HelloWorld = () => { + return ( + + + + ); +}; +``` + +## Custom Fonts + Alternatively, you can use your own set of custom fonts to be available in the canvas, as seen below. ```tsx twoslash From c98c82ca13d29a347384c685d1a5e423f6655c8a Mon Sep 17 00:00:00 2001 From: William Candillon Date: Mon, 7 Feb 2022 09:21:59 +0100 Subject: [PATCH 24/28] :wrench: --- docs/docs/text/glyphs.md | 2 +- example/src/Examples/Matrix/Matrix.tsx | 1 - .../src/renderer/components/text/Glyphs.tsx | 19 +++++++++++-------- 3 files changed, 12 insertions(+), 10 deletions(-) diff --git a/docs/docs/text/glyphs.md b/docs/docs/text/glyphs.md index 9e45231cc3..b5be79e4ca 100644 --- a/docs/docs/text/glyphs.md +++ b/docs/docs/text/glyphs.md @@ -12,7 +12,7 @@ This component raws a run of glyphs, at corresponding positions, in a given font | glyphs | `Ghlyph[]` | Glyphs to draw | | x? | `number`. | x coordinate of the origin of the entire run. Default is 0 | | y? | `number`. | y coordinate of the origin of the entire run. Default is 0 | -| font | `font` | Font to use | +| font | `font` | Font to use (see [Fonts](/docs/text/fonts)) | ## Draw text vertically diff --git a/example/src/Examples/Matrix/Matrix.tsx b/example/src/Examples/Matrix/Matrix.tsx index 764c3caf5d..5b4b014544 100644 --- a/example/src/Examples/Matrix/Matrix.tsx +++ b/example/src/Examples/Matrix/Matrix.tsx @@ -3,7 +3,6 @@ import { Canvas, Fill, Paint, - Skia, useFont, } from "@shopify/react-native-skia"; import React from "react"; diff --git a/package/src/renderer/components/text/Glyphs.tsx b/package/src/renderer/components/text/Glyphs.tsx index d4f114a824..58271b63ae 100644 --- a/package/src/renderer/components/text/Glyphs.tsx +++ b/package/src/renderer/components/text/Glyphs.tsx @@ -2,19 +2,21 @@ import React from "react"; import type { CustomPaintProps, AnimatedProps } from "../../processors"; import { useDrawing } from "../../nodes/Drawing"; -import type { IPoint, IFont } from "../../../skia"; +import type { IPoint } from "../../../skia"; +import type { FontDef } from "../../processors/Font"; +import { processFont } from "../../processors/Font"; export interface Glyph { id: number; pos: IPoint; } -export interface GlyphsProps extends CustomPaintProps { - x: number; - y: number; - glyphs: Glyph[]; - font: IFont; -} +export type GlyphsProps = CustomPaintProps & + FontDef & { + x: number; + y: number; + glyphs: Glyph[]; + }; interface ProcessedGlyphs { glyphs: number[]; @@ -24,7 +26,8 @@ interface ProcessedGlyphs { export const Glyphs = (props: AnimatedProps) => { const onDraw = useDrawing( props, - ({ canvas, paint }, { glyphs: rawGlyphs, x, y, font }) => { + ({ canvas, paint, fontMgr }, { glyphs: rawGlyphs, x, y, ...fontDef }) => { + const font = processFont(fontMgr, fontDef); const { glyphs, positions } = rawGlyphs.reduce( (acc, glyph) => { const { id, pos } = glyph; From b036790de6955e8ea17d69471add927b41bf8e8b Mon Sep 17 00:00:00 2001 From: William Candillon Date: Mon, 7 Feb 2022 09:26:28 +0100 Subject: [PATCH 25/28] :lipstick: --- docs/docs/text/glyphs.md | 5 +++-- docs/docs/text/text.md | 8 ++++---- 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/docs/docs/text/glyphs.md b/docs/docs/text/glyphs.md index b5be79e4ca..1370cb2a90 100644 --- a/docs/docs/text/glyphs.md +++ b/docs/docs/text/glyphs.md @@ -12,8 +12,9 @@ This component raws a run of glyphs, at corresponding positions, in a given font | glyphs | `Ghlyph[]` | Glyphs to draw | | x? | `number`. | x coordinate of the origin of the entire run. Default is 0 | | y? | `number`. | y coordinate of the origin of the entire run. Default is 0 | -| font | `font` | Font to use (see [Fonts](/docs/text/fonts)) | - +| font | `Font` | Font to use (see [Fonts](/docs/text/fonts)) | +| familyName? | `string` | Font family name to use (see [Fonts](/docs/text/fonts)) | +| size? | `number` | Font size if `familName` is provided | ## Draw text vertically diff --git a/docs/docs/text/text.md b/docs/docs/text/text.md index b32fedac81..e762d52468 100644 --- a/docs/docs/text/text.md +++ b/docs/docs/text/text.md @@ -10,10 +10,10 @@ The fonts available in the canvas are described in [here](/docs/text/fonts). | Name | Type | Description | |:------------|:----------|:--------------------------------------------------------------| -| text | `string` | Text to draw | -| size? | `number` | Font size | -| familyName? | `string` | Font family name | -| font? | `font` | Custom font to use | +| text | `string` | Text to draw | +| font | `Font` | Font to use (see [Fonts](/docs/text/fonts)) | +| familyName? | `string` | Font family name to use (see [Fonts](/docs/text/fonts)) | +| size? | `number` | Font size if `familName` is provided | ### Example From 6569353b053d45a0b75df405397c3ff9b1cb06f0 Mon Sep 17 00:00:00 2001 From: William Candillon Date: Mon, 7 Feb 2022 09:47:03 +0100 Subject: [PATCH 26/28] Add symmetry to the TextPath properties --- docs/sidebars.js | 8 +++++++- .../src/renderer/components/text/TextPath.tsx | 18 ++++++++++-------- 2 files changed, 17 insertions(+), 9 deletions(-) diff --git a/docs/sidebars.js b/docs/sidebars.js index b88d3d22e3..d0e03b19df 100644 --- a/docs/sidebars.js +++ b/docs/sidebars.js @@ -47,7 +47,13 @@ const sidebars = { collapsed: true, type: "category", label: "Text", - items: ["text/fonts", "text/text", "text/glyphs"], + items: [ + "text/fonts", + "text/text", + "text/glyphs", + "text/path", + "text/blob", + ], }, { collapsed: true, diff --git a/package/src/renderer/components/text/TextPath.tsx b/package/src/renderer/components/text/TextPath.tsx index 2ad0999855..344cb576e7 100644 --- a/package/src/renderer/components/text/TextPath.tsx +++ b/package/src/renderer/components/text/TextPath.tsx @@ -3,21 +3,23 @@ import React from "react"; import type { CustomPaintProps, AnimatedProps } from "../../processors"; import { useDrawing } from "../../nodes/Drawing"; import type { IPath } from "../../../skia/Path"; -import type { IFont } from "../../../skia/Font"; import type { IRSXform } from "../../../skia/RSXform"; import { Skia } from "../../../skia/Skia"; +import type { FontDef } from "../../processors/Font"; +import { processFont } from "../../processors/Font"; -export interface TextPathProps extends CustomPaintProps { - text: string; - path: IPath; - font: IFont; - initialOffset: number; -} +export type TextPathProps = CustomPaintProps & + FontDef & { + text: string; + path: IPath; + initialOffset: number; + }; export const TextPath = (props: AnimatedProps) => { const onDraw = useDrawing( props, - ({ canvas, paint }, { text, path, font, initialOffset }) => { + ({ canvas, paint, fontMgr }, { text, path, initialOffset, ...fontDef }) => { + const font = processFont(fontMgr, fontDef); const ids = font.getGlyphIDs(text); const widths = font.getGlyphWidths(ids, paint); const rsx: IRSXform[] = []; From e25287f4177a9685ebfe6baf00a57ec1e69c5043 Mon Sep 17 00:00:00 2001 From: William Candillon Date: Mon, 7 Feb 2022 10:24:04 +0100 Subject: [PATCH 27/28] :wrench: --- docs/docs/text/blob.md | 38 +++++++++++++++++ docs/docs/text/path.md | 41 +++++++++++++++++++ .../src/renderer/components/text/TextBlob.tsx | 5 +++ .../src/renderer/components/text/TextPath.tsx | 14 ++++++- 4 files changed, 96 insertions(+), 2 deletions(-) create mode 100644 docs/docs/text/blob.md create mode 100644 docs/docs/text/path.md diff --git a/docs/docs/text/blob.md b/docs/docs/text/blob.md new file mode 100644 index 0000000000..75c2808685 --- /dev/null +++ b/docs/docs/text/blob.md @@ -0,0 +1,38 @@ +--- +id: blob +title: Text Blob +sidebar_label: Text Blob +slug: /text/blob +--- + +A text blob contains glyphs, their positions, and paint attributes specific to text. + +| Name | Type | Description | +|:------------|:-----------|:-------------------------------------------------------------| +| blob | `TextBlob` | Text blob | +| x? | `number` | x coordinate of the origin of the entire run. Default is 0 | +| y? | `number` | y coordinate of the origin of the entire run. Default is 0 | + +## Example + +```tsx twoslash +import {Canvas, TextBlob, Skia} from "@shopify/react-native-skia"; + + +export const HelloWorld = () => { + const typeface = Skia.FontMgr.RefDefault().matchFamilyStyle("helvetica"); + if (!typeface) { + throw new Error("Helvetica not found"); + } + const font = Skia.Font(typeface, 30); + const blob = Skia.TextBlob.MakeFromText("Hello World!", font); + return ( + + + + ); +}; +``` \ No newline at end of file diff --git a/docs/docs/text/path.md b/docs/docs/text/path.md new file mode 100644 index 0000000000..75ddf8e878 --- /dev/null +++ b/docs/docs/text/path.md @@ -0,0 +1,41 @@ +--- +id: path +title: Text Path +sidebar_label: Text Path +slug: /text/path +--- + +Draws text along an SVG path. +The fonts available in the canvas are described in [here](/docs/text/fonts). + +| Name | Type | Description | +|:------------|:-------------------|:-------------------------------------------------------------| +| path | `Path` or `string` | Path to draw. Can be a string using the [SVG Path notation](https://developer.mozilla.org/en-US/docs/Web/SVG/Tutorial/Paths#line_commands) or an object created with `Skia.Path.Make()` | +| text | `string` | Text to draw | +| font | `Font` | Font to use (see [Fonts](/docs/text/fonts)) | +| familyName? | `string` | Font family name to use (see [Fonts](/docs/text/fonts)) | +| size? | `number` | Font size if `familName` is provided | + +## Example + +```tsx twoslash +import {Canvas, Group, TextPath, Skia} from "@shopify/react-native-skia"; + +const circle = Skia.Path.Make(); +circle.addCircle(128, 128, 128); + +export const HelloWorld = () => { + return ( + + + + + + ); +}; +``` diff --git a/package/src/renderer/components/text/TextBlob.tsx b/package/src/renderer/components/text/TextBlob.tsx index 4496ada7ed..2ce636f54d 100644 --- a/package/src/renderer/components/text/TextBlob.tsx +++ b/package/src/renderer/components/text/TextBlob.tsx @@ -16,3 +16,8 @@ export const TextBlob = (props: AnimatedProps) => { }); return ; }; + +TextBlob.defaultProps = { + x: 0, + y: 0, +}; diff --git a/package/src/renderer/components/text/TextPath.tsx b/package/src/renderer/components/text/TextPath.tsx index 344cb576e7..36670c2653 100644 --- a/package/src/renderer/components/text/TextPath.tsx +++ b/package/src/renderer/components/text/TextPath.tsx @@ -11,14 +11,24 @@ import { processFont } from "../../processors/Font"; export type TextPathProps = CustomPaintProps & FontDef & { text: string; - path: IPath; + path: IPath | string; initialOffset: number; }; export const TextPath = (props: AnimatedProps) => { const onDraw = useDrawing( props, - ({ canvas, paint, fontMgr }, { text, path, initialOffset, ...fontDef }) => { + ( + { canvas, paint, fontMgr }, + { text, initialOffset, path: pathDef, ...fontDef } + ) => { + const path = + typeof pathDef === "string" + ? Skia.Path.MakeFromSVGString(pathDef) + : pathDef; + if (path === null) { + throw new Error("Invalid path: " + pathDef); + } const font = processFont(fontMgr, fontDef); const ids = font.getGlyphIDs(text); const widths = font.getGlyphWidths(ids, paint); From 2a9bacddc3bbf0e2f7e4525cf2e8add27ed59d5c Mon Sep 17 00:00:00 2001 From: William Candillon Date: Thu, 10 Feb 2022 09:48:02 +0100 Subject: [PATCH 28/28] :green_heart: --- package/src/skia/Skia.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/package/src/skia/Skia.ts b/package/src/skia/Skia.ts index 3ad70063ab..a65d1922c2 100644 --- a/package/src/skia/Skia.ts +++ b/package/src/skia/Skia.ts @@ -26,7 +26,6 @@ import type { IRSXform } from "./RSXform"; import type { IPath } from "./Path/Path"; import type { IContourMeasureIter } from "./ContourMeasure"; - /** * Declares the interface for the native Skia API */