From 36e2292a0859f8dd559c2114003d3ca9862d4ccf Mon Sep 17 00:00:00 2001 From: Caleb Hearon Date: Sun, 22 Jun 2025 15:26:48 -0400 Subject: [PATCH 1/8] remove all font related functionality The APIs will come back after zig build and use fewer dependencies that are easier to static build. --- README.md | 52 ---- binding.gyp | 1 - index.d.ts | 46 --- src/Canvas.cc | 200 ------------- src/Canvas.h | 42 --- src/CanvasRenderingContext2d.cc | 506 -------------------------------- src/CanvasRenderingContext2d.h | 46 +-- src/init.cc | 3 - src/register_font.cc | 352 ---------------------- src/register_font.h | 7 - test/canvas.test.js | 163 +--------- 11 files changed, 2 insertions(+), 1416 deletions(-) delete mode 100644 src/register_font.cc delete mode 100644 src/register_font.h diff --git a/README.md b/README.md index d02b3bfe0..fc027c870 100644 --- a/README.md +++ b/README.md @@ -77,8 +77,6 @@ This project is an implementation of the Web Canvas API and implements that API * [createCanvas()](#createcanvas) * [createImageData()](#createimagedata) * [loadImage()](#loadimage) -* [registerFont()](#registerfont) -* [deregisterAllFonts()](#deregisterAllFonts) ### Non-standard APIs @@ -151,56 +149,6 @@ const myimg = await loadImage('http://server.com/image.png') // do something with image ``` -### registerFont() - -> ```ts -> registerFont(path: string, { family: string, weight?: string, style?: string }) => void -> ``` - -To use a font file that is not installed as a system font, use `registerFont()` to register the font with Canvas. - -```js -const { registerFont, createCanvas } = require('canvas') -registerFont('comicsans.ttf', { family: 'Comic Sans' }) - -const canvas = createCanvas(500, 500) -const ctx = canvas.getContext('2d') - -ctx.font = '12px "Comic Sans"' -ctx.fillText('Everyone hates this font :(', 250, 10) -``` - -The second argument is an object with properties that resemble the CSS properties that are specified in `@font-face` rules. You must specify at least `family`. `weight`, and `style` are optional and default to `'normal'`. - -### deregisterAllFonts() - -> ```ts -> deregisterAllFonts() => void -> ``` - -Use `deregisterAllFonts` to unregister all fonts that have been previously registered. This method is useful when you want to remove all registered fonts, such as when using the canvas in tests - -```ts -const { registerFont, createCanvas, deregisterAllFonts } = require('canvas') - -describe('text rendering', () => { - afterEach(() => { - deregisterAllFonts(); - }) - it('should render text with Comic Sans', () => { - registerFont('comicsans.ttf', { family: 'Comic Sans' }) - - const canvas = createCanvas(500, 500) - const ctx = canvas.getContext('2d') - - ctx.font = '12px "Comic Sans"' - ctx.fillText('Everyone loves this font :)', 250, 10) - - // assertScreenshot() - }) -}) -``` - ### Image#src > ```ts diff --git a/binding.gyp b/binding.gyp index 495f6e174..81acdb860 100644 --- a/binding.gyp +++ b/binding.gyp @@ -74,7 +74,6 @@ 'src/Image.cc', 'src/ImageData.cc', 'src/init.cc', - 'src/register_font.cc', 'src/FontParser.cc' ], 'conditions': [ diff --git a/index.d.ts b/index.d.ts index 27ab0c341..9c87531cc 100644 --- a/index.d.ts +++ b/index.d.ts @@ -210,14 +210,11 @@ export class CanvasRenderingContext2D { clip(fillRule?: CanvasFillRule): void; fill(fillRule?: CanvasFillRule): void; stroke(): void; - fillText(text: string, x: number, y: number, maxWidth?: number): void; - strokeText(text: string, x: number, y: number, maxWidth?: number): void; fillRect(x: number, y: number, w: number, h: number): void; strokeRect(x: number, y: number, w: number, h: number): void; clearRect(x: number, y: number, w: number, h: number): void; rect(x: number, y: number, w: number, h: number): void; roundRect(x: number, y: number, w: number, h: number, radii?: number | number[]): void; - measureText(text: string): TextMetrics; moveTo(x: number, y: number): void; lineTo(x: number, y: number): void; bezierCurveTo(cp1x: number, cp1y: number, cp2x: number, cp2y: number, x: number, y: number): void; @@ -253,30 +250,6 @@ export class CanvasRenderingContext2D { shadowBlur: number; /** _Non-standard_. Sets the antialiasing mode. */ antialias: 'default' | 'gray' | 'none' | 'subpixel' - /** - * Defaults to 'path'. The effect depends on the canvas type: - * - * * **Standard (image)** `'glyph'` and `'path'` both result in rasterized - * text. Glyph mode is faster than path, but may result in lower-quality - * text, especially when rotated or translated. - * - * * **PDF** `'glyph'` will embed text instead of paths into the PDF. This - * is faster to encode, faster to open with PDF viewers, yields a smaller - * file size and makes the text selectable. The subset of the font needed - * to render the glyphs will be embedded in the PDF. This is usually the - * mode you want to use with PDF canvases. - * - * * **SVG** glyph does not cause `` elements to be produced as one - * might expect ([cairo bug](https://gitlab.freedesktop.org/cairo/cairo/issues/253)). - * Rather, glyph will create a `` section with a `` for each - * glyph, then those glyphs be reused via `` elements. `'path'` mode - * creates a `` element for each text string. glyph mode is faster - * and yields a smaller file size. - * - * In glyph mode, `ctx.strokeText()` and `ctx.fillText()` behave the same - * (aside from using the stroke and fill style, respectively). - */ - textDrawingMode: 'path' | 'glyph' /** * _Non-standard_. Defaults to 'good'. Like `patternQuality`, but applies to * transformations affecting more than just patterns. @@ -286,12 +259,7 @@ export class CanvasRenderingContext2D { currentTransform: DOMMatrix fillStyle: string | CanvasGradient | CanvasPattern; strokeStyle: string | CanvasGradient | CanvasPattern; - font: string; - textBaseline: CanvasTextBaseline; - textAlign: CanvasTextAlign; canvas: Canvas; - direction: 'ltr' | 'rtl'; - lang: string; } export class CanvasGradient { @@ -378,20 +346,6 @@ export function createImageData(width: number, height: number): ImageData */ export function loadImage(src: string|Buffer, options?: any): Promise -/** - * Registers a font that is not installed as a system font. This must be used - * before creating Canvas instances. - * @param path Path to local font file. - * @param fontFace Description of the font face, corresponding to CSS properties - * used in `@font-face` rules. - */ -export function registerFont(path: string, fontFace: {family: string, weight?: string, style?: string}): void - -/** - * Unloads all fonts - */ -export function deregisterAllFonts(): void; - /** This class must not be constructed directly; use `canvas.createPNGStream()`. */ export class PNGStream extends Readable {} /** This class must not be constructed directly; use `canvas.createJPEGStream()`. */ diff --git a/src/Canvas.cc b/src/Canvas.cc index 0572a7557..db86b2f8e 100644 --- a/src/Canvas.cc +++ b/src/Canvas.cc @@ -13,7 +13,6 @@ #include #include #include "PNG.h" -#include "register_font.h" #include #include #include @@ -27,20 +26,10 @@ #include "JPEGStream.h" #endif -#define GENERIC_FACE_ERROR \ - "The second argument to registerFont is required, and should be an object " \ - "with at least a family (string) and optionally weight (string/number) " \ - "and style (string)." - #define CAIRO_MAX_SIZE 32767 using namespace std; -std::vector Canvas::font_face_list; - -// Increases each time a font is (de)registered -int Canvas::fontSerial = 1; - /* * Initialize Canvas. */ @@ -68,8 +57,6 @@ Canvas::Initialize(Napi::Env& env, Napi::Object& exports) { StaticValue("PNG_FILTER_AVG", Napi::Number::New(env, PNG_FILTER_AVG), napi_default_jsproperty), StaticValue("PNG_FILTER_PAETH", Napi::Number::New(env, PNG_FILTER_PAETH), napi_default_jsproperty), StaticValue("PNG_ALL_FILTERS", Napi::Number::New(env, PNG_ALL_FILTERS), napi_default_jsproperty), - StaticMethod<&Canvas::RegisterFont>("_registerFont", napi_default_method), - StaticMethod<&Canvas::DeregisterAllFonts>("_deregisterAllFonts", napi_default_method), StaticMethod<&Canvas::ParseFont>("parseFont", napi_default_method) }); @@ -676,88 +663,6 @@ str_value(Napi::Maybe maybe, const char *fallback, bool can_be_numb return NULL; } -void -Canvas::RegisterFont(const Napi::CallbackInfo& info) { - Napi::Env env = info.Env(); - if (!info[0].IsString()) { - Napi::Error::New(env, "Wrong argument type").ThrowAsJavaScriptException(); - return; - } else if (!info[1].IsObject()) { - Napi::Error::New(env, GENERIC_FACE_ERROR).ThrowAsJavaScriptException(); - return; - } - - std::string filePath = info[0].As(); - PangoFontDescription *sys_desc = get_pango_font_description((unsigned char *)(filePath.c_str())); - - if (!sys_desc) { - Napi::Error::New(env, "Could not parse font file").ThrowAsJavaScriptException(); - return; - } - - PangoFontDescription *user_desc = pango_font_description_new(); - - // now check the attrs, there are many ways to be wrong - Napi::Object js_user_desc = info[1].As(); - - // TODO: use FontParser on these values just like the FontFace API works - char *family = str_value(js_user_desc.Get("family"), NULL, false); - char *weight = str_value(js_user_desc.Get("weight"), "normal", true); - char *style = str_value(js_user_desc.Get("style"), "normal", false); - - if (family && weight && style) { - pango_font_description_set_weight(user_desc, Canvas::GetWeightFromCSSString(weight)); - pango_font_description_set_style(user_desc, Canvas::GetStyleFromCSSString(style)); - pango_font_description_set_family(user_desc, family); - - auto found = std::find_if(font_face_list.begin(), font_face_list.end(), [&](FontFace& f) { - return pango_font_description_equal(f.sys_desc, sys_desc); - }); - - if (found != font_face_list.end()) { - pango_font_description_free(found->user_desc); - found->user_desc = user_desc; - } else if (register_font((unsigned char *) filePath.c_str())) { - FontFace face; - face.user_desc = user_desc; - face.sys_desc = sys_desc; - strncpy((char *)face.file_path, (char *) filePath.c_str(), 1023); - font_face_list.push_back(face); - } else { - pango_font_description_free(user_desc); - Napi::Error::New(env, "Could not load font to the system's font host").ThrowAsJavaScriptException(); - - } - } else { - pango_font_description_free(user_desc); - if (!env.IsExceptionPending()) { - Napi::Error::New(env, GENERIC_FACE_ERROR).ThrowAsJavaScriptException(); - } - } - - free(family); - free(weight); - free(style); - fontSerial++; -} - -void -Canvas::DeregisterAllFonts(const Napi::CallbackInfo& info) { - Napi::Env env = info.Env(); - // Unload all fonts from pango to free up memory - bool success = true; - - std::for_each(font_face_list.begin(), font_face_list.end(), [&](FontFace& f) { - if (!deregister_font( (unsigned char *)f.file_path )) success = false; - pango_font_description_free(f.user_desc); - pango_font_description_free(f.sys_desc); - }); - - font_face_list.clear(); - fontSerial++; - if (!success) Napi::Error::New(env, "Could not deregister one or more fonts").ThrowAsJavaScriptException(); -} - /* * Do not use! This is only exported for testing */ @@ -792,111 +697,6 @@ Canvas::ParseFont(const Napi::CallbackInfo& info) { return obj; } -/* - * Get a PangoStyle from a CSS string (like "italic") - */ - -PangoStyle -Canvas::GetStyleFromCSSString(const char *style) { - PangoStyle s = PANGO_STYLE_NORMAL; - - if (strlen(style) > 0) { - if (0 == strcmp("italic", style)) { - s = PANGO_STYLE_ITALIC; - } else if (0 == strcmp("oblique", style)) { - s = PANGO_STYLE_OBLIQUE; - } - } - - return s; -} - -/* - * Get a PangoWeight from a CSS string ("bold", "100", etc) - */ - -PangoWeight -Canvas::GetWeightFromCSSString(const char *weight) { - PangoWeight w = PANGO_WEIGHT_NORMAL; - - if (strlen(weight) > 0) { - if (0 == strcmp("bold", weight)) { - w = PANGO_WEIGHT_BOLD; - } else if (0 == strcmp("100", weight)) { - w = PANGO_WEIGHT_THIN; - } else if (0 == strcmp("200", weight)) { - w = PANGO_WEIGHT_ULTRALIGHT; - } else if (0 == strcmp("300", weight)) { - w = PANGO_WEIGHT_LIGHT; - } else if (0 == strcmp("400", weight)) { - w = PANGO_WEIGHT_NORMAL; - } else if (0 == strcmp("500", weight)) { - w = PANGO_WEIGHT_MEDIUM; - } else if (0 == strcmp("600", weight)) { - w = PANGO_WEIGHT_SEMIBOLD; - } else if (0 == strcmp("700", weight)) { - w = PANGO_WEIGHT_BOLD; - } else if (0 == strcmp("800", weight)) { - w = PANGO_WEIGHT_ULTRABOLD; - } else if (0 == strcmp("900", weight)) { - w = PANGO_WEIGHT_HEAVY; - } - } - - return w; -} - -/* - * Given a user description, return a description that will select the - * font either from the system or @font-face - */ - -PangoFontDescription * -Canvas::ResolveFontDescription(const PangoFontDescription *desc) { - // One of the user-specified families could map to multiple SFNT family names - // if someone registered two different fonts under the same family name. - // https://drafts.csswg.org/css-fonts-3/#font-style-matching - FontFace best; - istringstream families(pango_font_description_get_family(desc)); - unordered_set seen_families; - string resolved_families; - bool first = true; - - for (string family; getline(families, family, ','); ) { - string renamed_families; - for (auto& ff : font_face_list) { - string pangofamily = string(pango_font_description_get_family(ff.user_desc)); - if (streq_casein(family, pangofamily)) { - const char* sys_desc_family_name = pango_font_description_get_family(ff.sys_desc); - bool unseen = seen_families.find(sys_desc_family_name) == seen_families.end(); - bool better = best.user_desc == nullptr || pango_font_description_better_match(desc, best.user_desc, ff.user_desc); - - // Avoid sending duplicate SFNT font names due to a bug in Pango for macOS: - // https://bugzilla.gnome.org/show_bug.cgi?id=762873 - if (unseen) { - seen_families.insert(sys_desc_family_name); - - if (better) { - renamed_families = string(sys_desc_family_name) + (renamed_families.size() ? "," : "") + renamed_families; - } else { - renamed_families = renamed_families + (renamed_families.size() ? "," : "") + sys_desc_family_name; - } - } - - if (first && better) best = ff; - } - } - - if (resolved_families.size()) resolved_families += ','; - resolved_families += renamed_families.size() ? renamed_families : family; - first = false; - } - - PangoFontDescription* ret = pango_font_description_copy(best.sys_desc ? best.sys_desc : desc); - pango_font_description_set_family(ret, resolved_families.c_str()); - - return ret; -} // This returns an approximate value only, suitable for // Napi::MemoryManagement:: AdjustExternalMemory. diff --git a/src/Canvas.h b/src/Canvas.h index fc7e0ae44..c4fab7217 100644 --- a/src/Canvas.h +++ b/src/Canvas.h @@ -9,7 +9,6 @@ struct PdfSvgClosure; #include #include "dll_visibility.h" #include -#include #include #include @@ -23,40 +22,6 @@ typedef enum { CANVAS_TYPE_SVG } canvas_type_t; - -/* - * FontFace describes a font file in terms of one PangoFontDescription that - * will resolve to it and one that the user describes it as (like @font-face) - */ -class FontFace { - public: - PangoFontDescription *sys_desc = nullptr; - PangoFontDescription *user_desc = nullptr; - unsigned char file_path[1024]; -}; - -enum text_baseline_t : uint8_t { - TEXT_BASELINE_ALPHABETIC = 0, - TEXT_BASELINE_TOP = 1, - TEXT_BASELINE_BOTTOM = 2, - TEXT_BASELINE_MIDDLE = 3, - TEXT_BASELINE_IDEOGRAPHIC = 4, - TEXT_BASELINE_HANGING = 5 -}; - -enum text_align_t : int8_t { - TEXT_ALIGNMENT_LEFT = -1, - TEXT_ALIGNMENT_CENTER = 0, - TEXT_ALIGNMENT_RIGHT = 1, - TEXT_ALIGNMENT_START = -2, - TEXT_ALIGNMENT_END = 2 -}; - -enum canvas_draw_mode_t : uint8_t { - TEXT_DRAW_PATHS, - TEXT_DRAW_GLYPHS -}; - /* * Canvas. */ @@ -77,15 +42,10 @@ class Canvas : public Napi::ObjectWrap { void StreamPNGSync(const Napi::CallbackInfo& info); void StreamPDFSync(const Napi::CallbackInfo& info); void StreamJPEGSync(const Napi::CallbackInfo& info); - static void RegisterFont(const Napi::CallbackInfo& info); - static void DeregisterAllFonts(const Napi::CallbackInfo& info); static Napi::Value ParseFont(const Napi::CallbackInfo& info); Napi::Error CairoError(cairo_status_t status); static void ToPngBufferAsync(Closure* closure); static void ToJpegBufferAsync(Closure* closure); - static PangoWeight GetWeightFromCSSString(const char *weight); - static PangoStyle GetStyleFromCSSString(const char *style); - static PangoFontDescription *ResolveFontDescription(const PangoFontDescription *desc); inline bool isPDF() { return type == CANVAS_TYPE_PDF; } inline bool isSVG() { return type == CANVAS_TYPE_SVG; } @@ -111,7 +71,6 @@ class Canvas : public Napi::ObjectWrap { void destroySurface(); Napi::Env env; - static int fontSerial; private: @@ -119,7 +78,6 @@ class Canvas : public Napi::ObjectWrap { PdfSvgClosure *_closure; Napi::FunctionReference ctor; - static std::vector font_face_list; uint16_t width; uint16_t height; diff --git a/src/CanvasRenderingContext2d.cc b/src/CanvasRenderingContext2d.cc index 7b0334913..3758135fe 100644 --- a/src/CanvasRenderingContext2d.cc +++ b/src/CanvasRenderingContext2d.cc @@ -35,15 +35,6 @@ constexpr double twoPi = M_PI * 2.; -/* - * Simple helper macro for a rather verbose function call. - */ - -#define PANGO_LAYOUT_GET_METRICS(LAYOUT) pango_context_get_metrics( \ - pango_layout_get_context(LAYOUT), \ - pango_layout_get_font_description(LAYOUT), \ - pango_language_from_string(state->lang.c_str())) - inline static bool checkArgs(const Napi::CallbackInfo&info, double *args, int argsNum, int offset = 0){ Napi::Env env = info.Env(); int argsEnd = std::min(9, offset + argsNum); @@ -111,14 +102,11 @@ Context2d::Initialize(Napi::Env& env, Napi::Object& exports) { InstanceMethod<&Context2d::Clip>("clip", napi_default_method), InstanceMethod<&Context2d::Fill>("fill", napi_default_method), InstanceMethod<&Context2d::Stroke>("stroke", napi_default_method), - InstanceMethod<&Context2d::FillText>("fillText", napi_default_method), - InstanceMethod<&Context2d::StrokeText>("strokeText", napi_default_method), InstanceMethod<&Context2d::FillRect>("fillRect", napi_default_method), InstanceMethod<&Context2d::StrokeRect>("strokeRect", napi_default_method), InstanceMethod<&Context2d::ClearRect>("clearRect", napi_default_method), InstanceMethod<&Context2d::Rect>("rect", napi_default_method), InstanceMethod<&Context2d::RoundRect>("roundRect", napi_default_method), - InstanceMethod<&Context2d::MeasureText>("measureText", napi_default_method), InstanceMethod<&Context2d::MoveTo>("moveTo", napi_default_method), InstanceMethod<&Context2d::LineTo>("lineTo", napi_default_method), InstanceMethod<&Context2d::BezierCurveTo>("bezierCurveTo", napi_default_method), @@ -152,16 +140,10 @@ Context2d::Initialize(Napi::Env& env, Napi::Object& exports) { InstanceAccessor<&Context2d::GetShadowOffsetY, &Context2d::SetShadowOffsetY>("shadowOffsetY", napi_default_jsproperty), InstanceAccessor<&Context2d::GetShadowBlur, &Context2d::SetShadowBlur>("shadowBlur", napi_default_jsproperty), InstanceAccessor<&Context2d::GetAntiAlias, &Context2d::SetAntiAlias>("antialias", napi_default_jsproperty), - InstanceAccessor<&Context2d::GetTextDrawingMode, &Context2d::SetTextDrawingMode>("textDrawingMode", napi_default_jsproperty), InstanceAccessor<&Context2d::GetQuality, &Context2d::SetQuality>("quality", napi_default_jsproperty), InstanceAccessor<&Context2d::GetCurrentTransform, &Context2d::SetCurrentTransform>("currentTransform", napi_default_jsproperty), InstanceAccessor<&Context2d::GetFillStyle, &Context2d::SetFillStyle>("fillStyle", napi_default_jsproperty), InstanceAccessor<&Context2d::GetStrokeStyle, &Context2d::SetStrokeStyle>("strokeStyle", napi_default_jsproperty), - InstanceAccessor<&Context2d::GetFont, &Context2d::SetFont>("font", napi_default_jsproperty), - InstanceAccessor<&Context2d::GetTextBaseline, &Context2d::SetTextBaseline>("textBaseline", napi_default_jsproperty), - InstanceAccessor<&Context2d::GetTextAlign, &Context2d::SetTextAlign>("textAlign", napi_default_jsproperty), - InstanceAccessor<&Context2d::GetDirection, &Context2d::SetDirection>("direction", napi_default_jsproperty), - InstanceAccessor<&Context2d::GetLanguage, &Context2d::SetLanguage>("lang", napi_default_jsproperty) }); exports.Set("CanvasRenderingContext2d", ctor); @@ -221,19 +203,9 @@ Context2d::Context2d(const Napi::CallbackInfo& info) : Napi::ObjectWrapcreateCairoContext(); - _layout = pango_cairo_create_layout(_context); - - // As of January 2023, Pango rounds glyph positions which renders text wider - // or narrower than the browser. See #2184 for more information -#if PANGO_VERSION_CHECK(1, 44, 0) - pango_context_set_round_glyph_positions(pango_layout_get_context(_layout), FALSE); -#endif - - pango_layout_set_auto_dir(_layout, FALSE); states.emplace(); state = &states.top(); - pango_layout_set_font_description(_layout, state->fontDescription); } /* @@ -241,7 +213,6 @@ Context2d::Context2d(const Napi::CallbackInfo& info) : Napi::ObjectWrapfontDescription); } /* @@ -276,7 +246,6 @@ Context2d::restore() { cairo_restore(_context); states.pop(); state = &states.top(); - pango_layout_set_font_description(_layout, state->fontDescription); } } @@ -728,46 +697,6 @@ Context2d::AddPage(const Napi::CallbackInfo& info) { cairo_pdf_surface_set_size(canvas()->ensureSurface(), width, height); } -/* - * Get text direction. - */ -Napi::Value -Context2d::GetDirection(const Napi::CallbackInfo& info) { - return Napi::String::New(env, state->direction); -} - -/* - * Set text direction. - */ -void -Context2d::SetDirection(const Napi::CallbackInfo& info, const Napi::Value& value) { - if (!value.IsString()) return; - - std::string dir = value.As(); - if (dir != "ltr" && dir != "rtl") return; - - state->direction = dir; -} - -/* - * Get language. - */ -Napi::Value -Context2d::GetLanguage(const Napi::CallbackInfo& info) { - return Napi::String::New(env, state->lang); -} - -/* - * Set language. - */ -void -Context2d::SetLanguage(const Napi::CallbackInfo& info, const Napi::Value& value) { - if (!value.IsString()) return; - - std::string lang = value.As(); - state->lang = lang; -} - /* * Put image data. * @@ -1756,40 +1685,6 @@ Context2d::SetAntiAlias(const Napi::CallbackInfo& info, const Napi::Value& value } } -/* - * Get text drawing mode. - */ - -Napi::Value -Context2d::GetTextDrawingMode(const Napi::CallbackInfo& info) { - const char *mode; - if (state->textDrawingMode == TEXT_DRAW_PATHS) { - mode = "path"; - } else if (state->textDrawingMode == TEXT_DRAW_GLYPHS) { - mode = "glyph"; - } else { - mode = "unknown"; - } - return Napi::String::New(env, mode); -} - -/* - * Set text drawing mode. - */ - -void -Context2d::SetTextDrawingMode(const Napi::CallbackInfo& info, const Napi::Value& value) { - Napi::String stringValue; - if (value.ToString().UnwrapTo(&stringValue)) { - std::string str = stringValue.Utf8Value(); - if (str == "path") { - state->textDrawingMode = TEXT_DRAW_PATHS; - } else if (str == "glyph") { - state->textDrawingMode = TEXT_DRAW_GLYPHS; - } - } -} - /* * Get filter. */ @@ -2434,182 +2329,6 @@ Context2d::Stroke(const Napi::CallbackInfo& info) { stroke(true); } -/* - * Helper for fillText/strokeText - */ - -double -get_text_scale(PangoLayout *layout, double maxWidth) { - - PangoRectangle logical_rect; - pango_layout_get_pixel_extents(layout, NULL, &logical_rect); - - if (logical_rect.width > maxWidth) { - return maxWidth / logical_rect.width; - } else { - return 1.0; - } -} - -/* - * Make sure the layout's font list is up-to-date - */ -void -Context2d::checkFonts() { - // If fonts have been registered, the PangoContext is using an outdated FontMap - if (canvas()->fontSerial != fontSerial) { - pango_context_set_font_map( - pango_layout_get_context(_layout), - pango_cairo_font_map_get_default() - ); - - fontSerial = canvas()->fontSerial; - } -} - -void -Context2d::paintText(const Napi::CallbackInfo& info, bool stroke) { - int argsNum = info.Length() >= 4 ? 3 : 2; - - if (argsNum == 3 && info[3].IsUndefined()) - argsNum = 2; - - double args[3]; - if(!checkArgs(info, args, argsNum, 1)) - return; - - Napi::String strValue; - - if (!info[0].ToString().UnwrapTo(&strValue)) return; - - std::string str = strValue.Utf8Value(); - double x = args[0]; - double y = args[1]; - double scaled_by = 1; - - PangoLayout *layout = this->layout(); - - checkFonts(); - pango_layout_set_text(layout, str.c_str(), -1); - if (state->lang != "") { - pango_context_set_language(pango_layout_get_context(_layout), pango_language_from_string(state->lang.c_str())); - } - pango_cairo_update_layout(context(), layout); - - PangoDirection pango_dir = state->direction == "ltr" ? PANGO_DIRECTION_LTR : PANGO_DIRECTION_RTL; - pango_context_set_base_dir(pango_layout_get_context(_layout), pango_dir); - - if (argsNum == 3) { - if (args[2] <= 0) return; - scaled_by = get_text_scale(layout, args[2]); - cairo_save(context()); - cairo_scale(context(), scaled_by, 1); - } - - savePath(); - if (state->textDrawingMode == TEXT_DRAW_GLYPHS) { - if (stroke == true) { this->stroke(); } else { this->fill(); } - setTextPath(x / scaled_by, y); - } else if (state->textDrawingMode == TEXT_DRAW_PATHS) { - setTextPath(x / scaled_by, y); - if (stroke == true) { this->stroke(); } else { this->fill(); } - } - restorePath(); - if (argsNum == 3) { - cairo_restore(context()); - } -} - -/* - * Fill text at (x, y). - */ - -void -Context2d::FillText(const Napi::CallbackInfo& info) { - paintText(info, false); -} - -/* - * Stroke text at (x ,y). - */ - -void -Context2d::StrokeText(const Napi::CallbackInfo& info) { - paintText(info, true); -} - -/* - * Gets the baseline adjustment in device pixels - */ -inline double getBaselineAdjustment(PangoLayout* layout, short baseline) { - PangoRectangle logical_rect; - pango_layout_line_get_extents(pango_layout_get_line(layout, 0), NULL, &logical_rect); - - double scale = 1.0 / PANGO_SCALE; - double ascent = scale * pango_layout_get_baseline(layout); - double descent = scale * logical_rect.height - ascent; - - switch (baseline) { - case TEXT_BASELINE_ALPHABETIC: - return ascent; - case TEXT_BASELINE_MIDDLE: - return (ascent + descent) / 2.0; - case TEXT_BASELINE_BOTTOM: - return ascent + descent; - default: - return 0; - } -} - -text_align_t -Context2d::resolveTextAlignment() { - text_align_t alignment = state->textAlignment; - - // Convert start/end to left/right based on direction - if (alignment == TEXT_ALIGNMENT_START) { - return (state->direction == "rtl") ? TEXT_ALIGNMENT_RIGHT : TEXT_ALIGNMENT_LEFT; - } else if (alignment == TEXT_ALIGNMENT_END) { - return (state->direction == "rtl") ? TEXT_ALIGNMENT_LEFT : TEXT_ALIGNMENT_RIGHT; - } - - return alignment; -} - -/* - * Set text path for the string in the layout at (x, y). - * This function is called by paintText and won't behave correctly - * if is not called from there. - * it needs pango_layout_set_text and pango_cairo_update_layout to be called before - */ - -void -Context2d::setTextPath(double x, double y) { - PangoRectangle logical_rect; - text_align_t alignment = resolveTextAlignment(); - - switch (alignment) { - case TEXT_ALIGNMENT_CENTER: - pango_layout_get_pixel_extents(_layout, NULL, &logical_rect); - x -= logical_rect.width / 2; - break; - case TEXT_ALIGNMENT_RIGHT: - pango_layout_get_pixel_extents(_layout, NULL, &logical_rect); - x -= logical_rect.width; - break; - default: // TEXT_ALIGNMENT_LEFT - break; - } - - y -= getBaselineAdjustment(_layout, state->textBaseline); - - cairo_move_to(_context, x, y); - if (state->textDrawingMode == TEXT_DRAW_PATHS) { - pango_cairo_layout_path(_context, _layout); - } else if (state->textDrawingMode == TEXT_DRAW_GLYPHS) { - pango_cairo_show_layout(_context, _layout); - } -} - /* * Adds a point to the current subpath. */ @@ -2636,231 +2355,6 @@ Context2d::MoveTo(const Napi::CallbackInfo& info) { cairo_move_to(context(), args[0], args[1]); } -/* - * Get font. - */ - -Napi::Value -Context2d::GetFont(const Napi::CallbackInfo& info) { - return Napi::String::New(env, state->font); -} - -/* - * Set font: - * - weight - * - style - * - size - * - unit - * - family - */ - -void -Context2d::SetFont(const Napi::CallbackInfo& info, const Napi::Value& value) { - if (!value.IsString()) return; - - std::string str = value.As().Utf8Value(); - if (!str.length()) return; - - bool success; - auto props = FontParser::parse(str, &success); - if (!success) return; - - PangoFontDescription *desc = pango_font_description_copy(state->fontDescription); - pango_font_description_free(state->fontDescription); - - PangoStyle style = props.fontStyle == FontStyle::Italic ? PANGO_STYLE_ITALIC - : props.fontStyle == FontStyle::Oblique ? PANGO_STYLE_OBLIQUE - : PANGO_STYLE_NORMAL; - pango_font_description_set_style(desc, style); - - pango_font_description_set_weight(desc, static_cast(props.fontWeight)); - - std::string family = props.fontFamily.empty() ? "" : props.fontFamily[0]; - for (size_t i = 1; i < props.fontFamily.size(); i++) { - family += "," + props.fontFamily[i]; - } - if (family.length() > 0) { - // See #1643 - Pango understands "sans" whereas CSS uses "sans-serif" - std::string s1(family); - std::string s2("sans-serif"); - if (streq_casein(s1, s2)) { - pango_font_description_set_family(desc, "sans"); - } else { - pango_font_description_set_family(desc, family.c_str()); - } - } - - PangoFontDescription *sys_desc = Canvas::ResolveFontDescription(desc); - pango_font_description_free(desc); - - if (props.fontSize > 0) pango_font_description_set_absolute_size(sys_desc, props.fontSize * PANGO_SCALE); - - state->fontDescription = sys_desc; - pango_layout_set_font_description(_layout, sys_desc); - - state->font = str; -} - -/* - * Get text baseline. - */ - -Napi::Value -Context2d::GetTextBaseline(const Napi::CallbackInfo& info) { - const char* baseline; - switch (state->textBaseline) { - default: - case TEXT_BASELINE_ALPHABETIC: baseline = "alphabetic"; break; - case TEXT_BASELINE_TOP: baseline = "top"; break; - case TEXT_BASELINE_BOTTOM: baseline = "bottom"; break; - case TEXT_BASELINE_MIDDLE: baseline = "middle"; break; - case TEXT_BASELINE_IDEOGRAPHIC: baseline = "ideographic"; break; - case TEXT_BASELINE_HANGING: baseline = "hanging"; break; - } - return Napi::String::New(env, baseline); -} - -/* - * Set text baseline. - */ - -void -Context2d::SetTextBaseline(const Napi::CallbackInfo& info, const Napi::Value& value) { - if (!value.IsString()) return; - - std::string opStr = value.As(); - const std::map modes = { - {"alphabetic", TEXT_BASELINE_ALPHABETIC}, - {"top", TEXT_BASELINE_TOP}, - {"bottom", TEXT_BASELINE_BOTTOM}, - {"middle", TEXT_BASELINE_MIDDLE}, - {"ideographic", TEXT_BASELINE_IDEOGRAPHIC}, - {"hanging", TEXT_BASELINE_HANGING} - }; - auto op = modes.find(opStr); - if (op == modes.end()) return; - - state->textBaseline = op->second; -} - -/* - * Get text align. - */ - -Napi::Value -Context2d::GetTextAlign(const Napi::CallbackInfo& info) { - const char* align; - switch (state->textAlignment) { - case TEXT_ALIGNMENT_LEFT: align = "left"; break; - case TEXT_ALIGNMENT_START: align = "start"; break; - case TEXT_ALIGNMENT_CENTER: align = "center"; break; - case TEXT_ALIGNMENT_RIGHT: align = "right"; break; - case TEXT_ALIGNMENT_END: align = "end"; break; - default: align = "start"; - } - return Napi::String::New(env, align); -} - -/* - * Set text align. - */ - -void -Context2d::SetTextAlign(const Napi::CallbackInfo& info, const Napi::Value& value) { - if (!value.IsString()) return; - - std::string opStr = value.As(); - const std::map modes = { - {"center", TEXT_ALIGNMENT_CENTER}, - {"left", TEXT_ALIGNMENT_LEFT}, - {"start", TEXT_ALIGNMENT_START}, - {"right", TEXT_ALIGNMENT_RIGHT}, - {"end", TEXT_ALIGNMENT_END} - }; - auto op = modes.find(opStr); - if (op == modes.end()) return; - - state->textAlignment = op->second; -} - -/* - * Return the given text extents. - * TODO: Support for: - * hangingBaseline, ideographicBaseline, - * fontBoundingBoxAscent, fontBoundingBoxDescent - */ - -Napi::Value -Context2d::MeasureText(const Napi::CallbackInfo& info) { - cairo_t *ctx = this->context(); - - Napi::String str; - if (!info[0].ToString().UnwrapTo(&str)) return env.Undefined(); - - Napi::Object obj = Napi::Object::New(env); - - PangoRectangle _ink_rect, _logical_rect; - float_rectangle ink_rect, logical_rect; - PangoFontMetrics *metrics; - PangoLayout *layout = this->layout(); - - checkFonts(); - pango_layout_set_text(layout, str.Utf8Value().c_str(), -1); - if (state->lang != "") { - pango_context_set_language(pango_layout_get_context(_layout), pango_language_from_string(state->lang.c_str())); - } - pango_cairo_update_layout(ctx, layout); - - // Normally you could use pango_layout_get_pixel_extents and be done, or use - // pango_extents_to_pixels, but both of those round the pixels, so we have to - // divide by PANGO_SCALE manually - pango_layout_get_extents(layout, &_ink_rect, &_logical_rect); - - float inverse_pango_scale = 1. / PANGO_SCALE; - - logical_rect.x = _logical_rect.x * inverse_pango_scale; - logical_rect.y = _logical_rect.y * inverse_pango_scale; - logical_rect.width = _logical_rect.width * inverse_pango_scale; - logical_rect.height = _logical_rect.height * inverse_pango_scale; - - ink_rect.x = _ink_rect.x * inverse_pango_scale; - ink_rect.y = _ink_rect.y * inverse_pango_scale; - ink_rect.width = _ink_rect.width * inverse_pango_scale; - ink_rect.height = _ink_rect.height * inverse_pango_scale; - - metrics = PANGO_LAYOUT_GET_METRICS(layout); - - text_align_t alignment = resolveTextAlignment(); - - double x_offset; - switch (alignment) { - case TEXT_ALIGNMENT_CENTER: - x_offset = logical_rect.width / 2.; - break; - case TEXT_ALIGNMENT_RIGHT: - x_offset = logical_rect.width; - break; - case TEXT_ALIGNMENT_LEFT: - default: - x_offset = 0.0; - } - - double y_offset = getBaselineAdjustment(layout, state->textBaseline); - - obj.Set("width", Napi::Number::New(env, logical_rect.width)); - obj.Set("actualBoundingBoxLeft", Napi::Number::New(env, PANGO_LBEARING(ink_rect) + x_offset)); - obj.Set("actualBoundingBoxRight", Napi::Number::New(env, PANGO_RBEARING(ink_rect) - x_offset)); - obj.Set("actualBoundingBoxAscent", Napi::Number::New(env, y_offset + PANGO_ASCENT(ink_rect))); - obj.Set("actualBoundingBoxDescent", Napi::Number::New(env, PANGO_DESCENT(ink_rect) - y_offset)); - obj.Set("emHeightAscent", Napi::Number::New(env, -(PANGO_ASCENT(logical_rect) - y_offset))); - obj.Set("emHeightDescent", Napi::Number::New(env, PANGO_DESCENT(logical_rect) - y_offset)); - obj.Set("alphabeticBaseline", Napi::Number::New(env, -(pango_font_metrics_get_ascent(metrics) * inverse_pango_scale - y_offset))); - - pango_font_metrics_unref(metrics); - - return obj; -} - /* * Set line dash * ref: http://www.w3.org/TR/2dcontext/#dom-context-2d-setlinedash diff --git a/src/CanvasRenderingContext2d.h b/src/CanvasRenderingContext2d.h index 63caa021e..3f6611db6 100644 --- a/src/CanvasRenderingContext2d.h +++ b/src/CanvasRenderingContext2d.h @@ -6,7 +6,6 @@ #include "Canvas.h" #include "color.h" #include "napi.h" -#include #include /* @@ -26,22 +25,12 @@ struct canvas_state_t { Napi::ObjectReference strokePattern; Napi::ObjectReference fillGradient; Napi::ObjectReference strokeGradient; - PangoFontDescription* fontDescription = nullptr; - std::string font = "10px sans-serif"; cairo_filter_t patternQuality = CAIRO_FILTER_GOOD; float globalAlpha = 1.f; int shadowBlur = 0; - text_align_t textAlignment = TEXT_ALIGNMENT_START; - text_baseline_t textBaseline = TEXT_BASELINE_ALPHABETIC; - canvas_draw_mode_t textDrawingMode = TEXT_DRAW_PATHS; bool imageSmoothingEnabled = true; - std::string direction = "ltr"; - std::string lang = ""; - canvas_state_t() { - fontDescription = pango_font_description_from_string("sans"); - pango_font_description_set_absolute_size(fontDescription, 10 * PANGO_SCALE); - } + canvas_state_t() {} canvas_state_t(const canvas_state_t& other) { fill = other.fill; @@ -52,22 +41,11 @@ struct canvas_state_t { fillGradient.Reset(other.fillGradient.Value()); strokeGradient.Reset(other.strokeGradient.Value()); globalAlpha = other.globalAlpha; - textAlignment = other.textAlignment; - textBaseline = other.textBaseline; shadow = other.shadow; shadowBlur = other.shadowBlur; shadowOffsetX = other.shadowOffsetX; shadowOffsetY = other.shadowOffsetY; - textDrawingMode = other.textDrawingMode; - fontDescription = pango_font_description_copy(other.fontDescription); - font = other.font; imageSmoothingEnabled = other.imageSmoothingEnabled; - direction = other.direction; - lang = other.lang; - } - - ~canvas_state_t() { - pango_font_description_free(fontDescription); } }; @@ -109,12 +87,8 @@ class Context2d : public Napi::ObjectWrap { void Clip(const Napi::CallbackInfo& info); void Fill(const Napi::CallbackInfo& info); void Stroke(const Napi::CallbackInfo& info); - void FillText(const Napi::CallbackInfo& info); - void StrokeText(const Napi::CallbackInfo& info); - static Napi::Value SetFont(const Napi::CallbackInfo& info); void SetLineDash(const Napi::CallbackInfo& info); Napi::Value GetLineDash(const Napi::CallbackInfo& info); - Napi::Value MeasureText(const Napi::CallbackInfo& info); void BezierCurveTo(const Napi::CallbackInfo& info); void QuadraticCurveTo(const Napi::CallbackInfo& info); void LineTo(const Napi::CallbackInfo& info); @@ -152,10 +126,6 @@ class Context2d : public Napi::ObjectWrap { Napi::Value GetCurrentTransform(const Napi::CallbackInfo& info); Napi::Value GetFillStyle(const Napi::CallbackInfo& info); Napi::Value GetStrokeStyle(const Napi::CallbackInfo& info); - Napi::Value GetFont(const Napi::CallbackInfo& info); - Napi::Value GetTextBaseline(const Napi::CallbackInfo& info); - Napi::Value GetTextAlign(const Napi::CallbackInfo& info); - Napi::Value GetLanguage(const Napi::CallbackInfo& info); void SetPatternQuality(const Napi::CallbackInfo& info, const Napi::Value& value); void SetImageSmoothingEnabled(const Napi::CallbackInfo& info, const Napi::Value& value); void SetGlobalCompositeOperation(const Napi::CallbackInfo& info, const Napi::Value& value); @@ -170,28 +140,20 @@ class Context2d : public Napi::ObjectWrap { void SetShadowOffsetY(const Napi::CallbackInfo& info, const Napi::Value& value); void SetShadowBlur(const Napi::CallbackInfo& info, const Napi::Value& value); void SetAntiAlias(const Napi::CallbackInfo& info, const Napi::Value& value); - void SetTextDrawingMode(const Napi::CallbackInfo& info, const Napi::Value& value); void SetQuality(const Napi::CallbackInfo& info, const Napi::Value& value); void SetCurrentTransform(const Napi::CallbackInfo& info, const Napi::Value& value); void SetFillStyle(const Napi::CallbackInfo& info, const Napi::Value& value); void SetStrokeStyle(const Napi::CallbackInfo& info, const Napi::Value& value); - void SetFont(const Napi::CallbackInfo& info, const Napi::Value& value); - void SetTextBaseline(const Napi::CallbackInfo& info, const Napi::Value& value); - void SetTextAlign(const Napi::CallbackInfo& info, const Napi::Value& value); - void SetLanguage(const Napi::CallbackInfo& info, const Napi::Value& value); #if CAIRO_VERSION >= CAIRO_VERSION_ENCODE(1, 16, 0) void BeginTag(const Napi::CallbackInfo& info); void EndTag(const Napi::CallbackInfo& info); #endif - Napi::Value GetDirection(const Napi::CallbackInfo& info); - void SetDirection(const Napi::CallbackInfo& info, const Napi::Value& value); inline void setContext(cairo_t *ctx) { _context = ctx; } inline cairo_t *context(){ return _context; } inline Canvas *canvas(){ return _canvas; } inline bool hasShadow(); void inline setSourceRGBA(rgba_t color); void inline setSourceRGBA(cairo_t *ctx, rgba_t color); - void setTextPath(double x, double y); void blur(cairo_surface_t *surface, int radius); void shadow(void (fn)(cairo_t *cr)); void savePath(); @@ -202,7 +164,6 @@ class Context2d : public Napi::ObjectWrap { void save(); void restore(); void resetState(); - inline PangoLayout *layout(){ return _layout; } ~Context2d(); Napi::Env env; @@ -212,12 +173,7 @@ class Context2d : public Napi::ObjectWrap { Napi::Value get_current_transform(); void _setFillColor(Napi::Value arg); void _setStrokeColor(Napi::Value arg); - void checkFonts(); - void paintText(const Napi::CallbackInfo&, bool); - text_align_t resolveTextAlignment(); Canvas *_canvas; cairo_t *_context = nullptr; cairo_path_t *_path; - PangoLayout *_layout = nullptr; - int fontSerial = 1; }; diff --git a/src/init.cc b/src/init.cc index fa3ca5166..a974efd4a 100644 --- a/src/init.cc +++ b/src/init.cc @@ -1,7 +1,6 @@ // Copyright (c) 2010 LearnBoost #include -#include #include #if CAIRO_VERSION < CAIRO_VERSION_ENCODE(1, 10, 0) @@ -102,8 +101,6 @@ Napi::Object init(Napi::Env env, Napi::Object exports) { exports.Set("rsvgVersion", Napi::String::New(env, LIBRSVG_VERSION)); #endif - exports.Set("pangoVersion", Napi::String::New(env, PANGO_VERSION_STRING)); - char freetype_version[10]; snprintf(freetype_version, 10, "%d.%d.%d", FREETYPE_MAJOR, FREETYPE_MINOR, FREETYPE_PATCH); exports.Set("freetypeVersion", Napi::String::New(env, freetype_version)); diff --git a/src/register_font.cc b/src/register_font.cc deleted file mode 100644 index ae2ece584..000000000 --- a/src/register_font.cc +++ /dev/null @@ -1,352 +0,0 @@ -#include "register_font.h" - -#include -#include -#include - -#ifdef __APPLE__ -#include -#elif defined(_WIN32) -#include -#include -#else -#include -#endif - -#include -#include FT_FREETYPE_H -#include FT_TRUETYPE_TABLES_H -#include FT_SFNT_NAMES_H -#include FT_TRUETYPE_IDS_H -#ifndef FT_SFNT_OS2 -#define FT_SFNT_OS2 ft_sfnt_os2 -#endif - -// OSX seems to read the strings in MacRoman encoding and ignore Unicode entries. -// You can verify this by opening a TTF with both Unicode and Macroman on OSX. -// It uses the MacRoman name, while Fontconfig and Windows use Unicode -#ifdef __APPLE__ -#define PREFERRED_PLATFORM_ID TT_PLATFORM_MACINTOSH -#define PREFERRED_ENCODING_ID TT_MAC_ID_ROMAN -#else -#define PREFERRED_PLATFORM_ID TT_PLATFORM_MICROSOFT -#define PREFERRED_ENCODING_ID TT_MS_ID_UNICODE_CS -#endif - -#define IS_PREFERRED_ENC(X) \ - X.platform_id == PREFERRED_PLATFORM_ID && X.encoding_id == PREFERRED_ENCODING_ID - -#define GET_NAME_RANK(X) \ - (IS_PREFERRED_ENC(X) ? 1 : 0) + (X.name_id == TT_NAME_ID_PREFERRED_FAMILY ? 1 : 0) - -/* - * Return a UTF-8 encoded string given a TrueType name buf+len - * and its platform and encoding - */ - -char * -to_utf8(FT_Byte* buf, FT_UInt len, FT_UShort pid, FT_UShort eid) { - size_t ret_len = len * 4; // max chars in a utf8 string - char *ret = (char*)malloc(ret_len + 1); // utf8 string + null - - if (!ret) return NULL; - - // In my testing of hundreds of fonts from the Google Font repo, the two types - // of fonts are TT_PLATFORM_MICROSOFT with TT_MS_ID_UNICODE_CS encoding, or - // TT_PLATFORM_MACINTOSH with TT_MAC_ID_ROMAN encoding. Usually both, never neither - - char const *fromcode; - - if (pid == TT_PLATFORM_MACINTOSH && eid == TT_MAC_ID_ROMAN) { - fromcode = "MAC"; - } else if (pid == TT_PLATFORM_MICROSOFT && eid == TT_MS_ID_UNICODE_CS) { - fromcode = "UTF-16BE"; - } else { - free(ret); - return NULL; - } - - GIConv cd = g_iconv_open("UTF-8", fromcode); - - if (cd == (GIConv)-1) { - free(ret); - return NULL; - } - - size_t inbytesleft = len; - size_t outbytesleft = ret_len; - - size_t n_converted = g_iconv(cd, (char**)&buf, &inbytesleft, &ret, &outbytesleft); - - ret -= ret_len - outbytesleft; // rewind the pointers to their - buf -= len - inbytesleft; // original starting positions - - if (n_converted == (size_t)-1) { - free(ret); - return NULL; - } else { - ret[ret_len - outbytesleft] = '\0'; - return ret; - } -} - -/* - * Find a family name in the face's name table, preferring the one the - * system, fall back to the other - */ - -char * -get_family_name(FT_Face face) { - FT_SfntName name; - - int best_rank = -1; - char* best_buf = NULL; - - for (unsigned i = 0; i < FT_Get_Sfnt_Name_Count(face); ++i) { - FT_Get_Sfnt_Name(face, i, &name); - - if (name.name_id == TT_NAME_ID_FONT_FAMILY || name.name_id == TT_NAME_ID_PREFERRED_FAMILY) { - char *buf = to_utf8(name.string, name.string_len, name.platform_id, name.encoding_id); - - if (buf) { - int rank = GET_NAME_RANK(name); - if (rank > best_rank) { - best_rank = rank; - if (best_buf) free(best_buf); - best_buf = buf; - } else { - free(buf); - } - } - } - } - - return best_buf; -} - -PangoWeight -get_pango_weight(FT_UShort weight) { - switch (weight) { - case 100: return PANGO_WEIGHT_THIN; - case 200: return PANGO_WEIGHT_ULTRALIGHT; - case 300: return PANGO_WEIGHT_LIGHT; - #if PANGO_VERSION >= PANGO_VERSION_ENCODE(1, 36, 7) - case 350: return PANGO_WEIGHT_SEMILIGHT; - #endif - case 380: return PANGO_WEIGHT_BOOK; - case 400: return PANGO_WEIGHT_NORMAL; - case 500: return PANGO_WEIGHT_MEDIUM; - case 600: return PANGO_WEIGHT_SEMIBOLD; - case 700: return PANGO_WEIGHT_BOLD; - case 800: return PANGO_WEIGHT_ULTRABOLD; - case 900: return PANGO_WEIGHT_HEAVY; - case 1000: return PANGO_WEIGHT_ULTRAHEAVY; - default: return PANGO_WEIGHT_NORMAL; - } -} - -PangoStretch -get_pango_stretch(FT_UShort width) { - switch (width) { - case 1: return PANGO_STRETCH_ULTRA_CONDENSED; - case 2: return PANGO_STRETCH_EXTRA_CONDENSED; - case 3: return PANGO_STRETCH_CONDENSED; - case 4: return PANGO_STRETCH_SEMI_CONDENSED; - case 5: return PANGO_STRETCH_NORMAL; - case 6: return PANGO_STRETCH_SEMI_EXPANDED; - case 7: return PANGO_STRETCH_EXPANDED; - case 8: return PANGO_STRETCH_EXTRA_EXPANDED; - case 9: return PANGO_STRETCH_ULTRA_EXPANDED; - default: return PANGO_STRETCH_NORMAL; - } -} - -PangoStyle -get_pango_style(FT_Long flags) { - if (flags & FT_STYLE_FLAG_ITALIC) { - return PANGO_STYLE_ITALIC; - } else { - return PANGO_STYLE_NORMAL; - } -} - -#ifdef _WIN32 -std::unique_ptr -u8ToWide(const char* str) { - int iBufferSize = MultiByteToWideChar(CP_UTF8, 0, str, -1, (wchar_t*)NULL, 0); - if(!iBufferSize){ - return nullptr; - } - std::unique_ptr wpBufWString = std::unique_ptr{ new wchar_t[static_cast(iBufferSize)] }; - if(!MultiByteToWideChar(CP_UTF8, 0, str, -1, wpBufWString.get(), iBufferSize)){ - return nullptr; - } - return wpBufWString; -} - -static unsigned long -stream_read_func(FT_Stream stream, unsigned long offset, unsigned char* buffer, unsigned long count){ - HANDLE hFile = reinterpret_cast(stream->descriptor.pointer); - DWORD numberOfBytesRead; - OVERLAPPED overlapped; - overlapped.Offset = offset; - overlapped.OffsetHigh = 0; - overlapped.hEvent = NULL; - if(!ReadFile(hFile, buffer, count, &numberOfBytesRead, &overlapped)){ - return 0; - } - return numberOfBytesRead; -}; - -static void -stream_close_func(FT_Stream stream){ - HANDLE hFile = reinterpret_cast(stream->descriptor.pointer); - CloseHandle(hFile); -} -#endif - -/* - * Return a PangoFontDescription that will resolve to the font file - */ - -PangoFontDescription * -get_pango_font_description(unsigned char* filepath) { - FT_Library library; - FT_Face face; - PangoFontDescription *desc = pango_font_description_new(); -#ifdef _WIN32 - // FT_New_Face use fopen. - // Unable to find the file when supplied the multibyte string path on the Windows platform and throw error "Could not parse font file". - // This workaround fixes this by reading the font file uses win32 wide character API. - std::unique_ptr wFilepath = u8ToWide((char*)filepath); - if(!wFilepath){ - return NULL; - } - HANDLE hFile = CreateFileW( - wFilepath.get(), - GENERIC_READ, - FILE_SHARE_READ, - NULL, - OPEN_EXISTING, - FILE_ATTRIBUTE_NORMAL, - NULL - ); - if(!hFile){ - return NULL; - } - LARGE_INTEGER liSize; - if(!GetFileSizeEx(hFile, &liSize)) { - CloseHandle(hFile); - return NULL; - } - FT_Open_Args args; - args.flags = FT_OPEN_STREAM; - FT_StreamRec stream; - stream.base = NULL; - stream.size = liSize.QuadPart; - stream.pos = 0; - stream.descriptor.pointer = hFile; - stream.read = stream_read_func; - stream.close = stream_close_func; - args.stream = &stream; - if ( - !FT_Init_FreeType(&library) && - !FT_Open_Face(library, &args, 0, &face)) { -#else - if (!FT_Init_FreeType(&library) && !FT_New_Face(library, (const char*)filepath, 0, &face)) { -#endif - TT_OS2 *table = (TT_OS2*)FT_Get_Sfnt_Table(face, FT_SFNT_OS2); - if (table) { - char *family = get_family_name(face); - - if (!family) { - pango_font_description_free(desc); - FT_Done_Face(face); - FT_Done_FreeType(library); - - return NULL; - } - - pango_font_description_set_family(desc, family); - free(family); - pango_font_description_set_weight(desc, get_pango_weight(table->usWeightClass)); - pango_font_description_set_stretch(desc, get_pango_stretch(table->usWidthClass)); - pango_font_description_set_style(desc, get_pango_style(face->style_flags)); - - FT_Done_Face(face); - FT_Done_FreeType(library); - - return desc; - } - } - pango_font_description_free(desc); - - return NULL; -} - -/* - * Register font with the OS - */ - -bool -register_font(unsigned char *filepath) { - bool success; - - #ifdef __APPLE__ - CFURLRef filepathUrl = CFURLCreateFromFileSystemRepresentation(NULL, filepath, strlen((char*)filepath), false); - success = CTFontManagerRegisterFontsForURL(filepathUrl, kCTFontManagerScopeProcess, NULL); - #elif defined(_WIN32) - std::unique_ptr wFilepath = u8ToWide((char*)filepath); - if(wFilepath){ - success = AddFontResourceExW(wFilepath.get(), FR_PRIVATE, 0) != 0; - }else{ - success = false; - } - - #else - success = FcConfigAppFontAddFile(FcConfigGetCurrent(), (FcChar8 *)(filepath)); - #endif - - if (!success) return false; - - // Tell Pango to throw away the current FontMap and create a new one. This - // has the effect of registering the new font in Pango by re-looking up all - // font families. - pango_cairo_font_map_set_default(NULL); - - return true; -} - -/* - * Deregister font from the OS - * Note that Linux (FontConfig) can only dereregister ALL fonts at once. - */ - -bool -deregister_font(unsigned char *filepath) { - bool success; - - #ifdef __APPLE__ - CFURLRef filepathUrl = CFURLCreateFromFileSystemRepresentation(NULL, filepath, strlen((char*)filepath), false); - success = CTFontManagerUnregisterFontsForURL(filepathUrl, kCTFontManagerScopeProcess, NULL); - #elif defined(_WIN32) - std::unique_ptr wFilepath = u8ToWide((char*)filepath); - if(wFilepath){ - success = RemoveFontResourceExW(wFilepath.get(), FR_PRIVATE, 0) != 0; - }else{ - success = false; - } - #else - FcConfigAppFontClear(FcConfigGetCurrent()); - success = true; - #endif - - if (!success) return false; - - // Tell Pango to throw away the current FontMap and create a new one. This - // has the effect of deregistering the font in Pango by re-looking up all - // font families. - pango_cairo_font_map_set_default(NULL); - - return true; -} diff --git a/src/register_font.h b/src/register_font.h deleted file mode 100644 index a4fcd598e..000000000 --- a/src/register_font.h +++ /dev/null @@ -1,7 +0,0 @@ -#pragma once - -#include - -PangoFontDescription *get_pango_font_description(unsigned char *filepath); -bool register_font(unsigned char *filepath); -bool deregister_font(unsigned char *filepath); diff --git a/test/canvas.test.js b/test/canvas.test.js index e992dccb6..63a19dd13 100644 --- a/test/canvas.test.js +++ b/test/canvas.test.js @@ -14,9 +14,7 @@ const { createCanvas, createImageData, loadImage, - registerFont, Canvas, - deregisterAllFonts, ImageData } = require('../') @@ -55,17 +53,6 @@ describe('Canvas', function () { assert.equal(canvas.height, 14) }); - it('registerFont', function () { - // Minimal test to make sure nothing is thrown - registerFont('./examples/pfennigFont/Pfennig.ttf', { family: 'Pfennig' }) - registerFont('./examples/pfennigFont/PfennigBold.ttf', { family: 'Pfennig', weight: 'bold' }) - - // Test to multi byte file path support - registerFont('./examples/pfennigFont/pfennigMultiByte🚀.ttf', { family: 'Pfennig' }) - - deregisterAllFonts() - }); - it('color serialization', function () { const canvas = createCanvas(200, 200) const ctx = canvas.getContext('2d'); @@ -434,15 +421,12 @@ describe('Canvas', function () { assert.equal(canvas.width, 50) assert.equal(canvas.height, 70) - context.font = '20px arial' - assert.equal(context.font, '20px arial') canvas.width |= 0 assert.equal(context.lineWidth, 1) // #1095 assert.equal(context.globalAlpha, 1) // #1292 assert.equal(context.fillStyle, '#000000') assert.equal(context.strokeStyle, '#000000') - assert.equal(context.font, '10px sans-serif') assert.strictEqual(context.getImageData(0, 0, 1, 1).data.join(','), '0,0,0,0') }) @@ -498,18 +482,6 @@ describe('Canvas', function () { assert.equal('good', ctx.patternQuality) }) - it('Context2d#font=', function () { - const canvas = createCanvas(200, 200) - const ctx = canvas.getContext('2d') - - assert.equal(ctx.font, '10px sans-serif') - ctx.font = '15px Arial, sans-serif' - assert.equal(ctx.font, '15px Arial, sans-serif') - - ctx.font = 'Helvetica, sans' // invalid - assert.equal(ctx.font, '15px Arial, sans-serif') - }) - it('Context2d#lineWidth=', function () { const canvas = createCanvas(200, 200) const ctx = canvas.getContext('2d') @@ -589,21 +561,6 @@ describe('Canvas', function () { assert.ok(!ctx.isPointInPath(50, 120)) }) - it('Context2d#textAlign', function () { - const canvas = createCanvas(200, 200) - const ctx = canvas.getContext('2d') - - assert.equal('start', ctx.textAlign) - ctx.textAlign = 'center' - assert.equal('center', ctx.textAlign) - ctx.textAlign = 'right' - assert.equal('right', ctx.textAlign) - ctx.textAlign = 'end' - assert.equal('end', ctx.textAlign) - ctx.textAlign = 'fail' - assert.equal('end', ctx.textAlign) - }) - describe('#toBuffer', function () { it('Canvas#toBuffer()', function () { const buf = createCanvas(200, 200).toBuffer() @@ -1006,117 +963,6 @@ describe('Canvas', function () { }) }) - describe('Context2d#measureText()', function () { - it('Context2d#measureText().width', function () { - const canvas = createCanvas(20, 20) - const ctx = canvas.getContext('2d') - - assert.ok(ctx.measureText('foo').width) - assert.ok(ctx.measureText('foo').width !== ctx.measureText('foobar').width) - assert.ok(ctx.measureText('foo').width !== ctx.measureText(' foo').width) - }) - - it('works', function () { - const canvas = createCanvas(20, 20) - const ctx = canvas.getContext('2d') - ctx.font = '20px Arial' - - ctx.textBaseline = 'alphabetic' - let metrics = ctx.measureText('Alphabet') - // Actual value depends on font library version. Have observed values - // between 0 and 0.769. - assertApprox(metrics.alphabeticBaseline, 0.5, 0.5) - // Positive = going up from the baseline - assert.ok(metrics.actualBoundingBoxAscent > 0) - // Positive = going down from the baseline - assertApprox(metrics.actualBoundingBoxDescent, 5, 2) - - ctx.textBaseline = 'bottom' - metrics = ctx.measureText('Alphabet') - assert.strictEqual(ctx.textBaseline, 'bottom') - assertApprox(metrics.alphabeticBaseline, 5, 2) - assert.ok(metrics.actualBoundingBoxAscent > 0) - // On the baseline or slightly above - assert.ok(metrics.actualBoundingBoxDescent <= 0) - }) - - it('actualBoundingBox is correct for left, center and right alignment (#1909)', function () { - const canvas = createCanvas(0, 0) - const ctx = canvas.getContext('2d') - - // positive actualBoundingBoxLeft indicates a distance going left from the - // given alignment point. - - // positive actualBoundingBoxRight indicates a distance going right from - // the given alignment point. - - ctx.textAlign = 'left' - const lm = ctx.measureText('aaaa') - assertApprox(lm.actualBoundingBoxLeft, -1, 6) - assertApprox(lm.actualBoundingBoxRight, 21, 6) - - ctx.textAlign = 'center' - const cm = ctx.measureText('aaaa') - assertApprox(cm.actualBoundingBoxLeft, 9, 6) - assertApprox(cm.actualBoundingBoxRight, 11, 6) - - ctx.textAlign = 'right' - const rm = ctx.measureText('aaaa') - assertApprox(rm.actualBoundingBoxLeft, 19, 6) - assertApprox(rm.actualBoundingBoxRight, 1, 6) - }) - - it('resolves text alignment wrt Context2d#direction #2508', function () { - const canvas = createCanvas(0, 0) - const ctx = canvas.getContext('2d') - - ctx.textAlign = "left"; - const leftMetrics = ctx.measureText('hello'); - assert(leftMetrics.actualBoundingBoxLeft < leftMetrics.actualBoundingBoxRight, "leftMetrics.actualBoundingBoxLeft < leftMetrics.actualBoundingBoxRight"); - - ctx.textAlign = "right"; - const rightMetrics = ctx.measureText('hello'); - assert(rightMetrics.actualBoundingBoxLeft > rightMetrics.actualBoundingBoxRight, "metrics.actualBoundingBoxLeft > metrics.actualBoundingBoxRight"); - - ctx.textAlign = "start"; - - ctx.direction = "ltr"; - const ltrStartMetrics = ctx.measureText('hello'); - assert.deepStrictEqual(ltrStartMetrics, leftMetrics, "ltr start metrics should equal left metrics"); - - ctx.direction = "rtl"; - const rtlStartMetrics = ctx.measureText('hello'); - assert.deepStrictEqual(rtlStartMetrics, rightMetrics, "rtl start metrics should equal right metrics"); - - ctx.textAlign = "end"; - - ctx.direction = "ltr"; - const ltrEndMetrics = ctx.measureText('hello'); - assert.deepStrictEqual(ltrEndMetrics, rightMetrics, "ltr end metrics should equal right metrics"); - - ctx.direction = "rtl"; - const rtlEndMetrics = ctx.measureText('hello'); - assert.deepStrictEqual(rtlEndMetrics, leftMetrics, "rtl end metrics should equal left metrics"); - }) - }) - - it('Context2d#fillText()', function () { - [ - [['A', 10, 10], true], - [['A', 10, 10, undefined], true], - [['A', 10, 10, NaN], false] - ].forEach(([args, shouldDraw]) => { - const canvas = createCanvas(20, 20) - const ctx = canvas.getContext('2d') - - ctx.textBaseline = 'middle' - ctx.textAlign = 'center' - ctx.fillText(...args) - - assert.strictEqual(ctx.getImageData(0, 0, 20, 20).data.some(a => a), shouldDraw) - }) - }) - it('Context2d#currentTransform', function () { const canvas = createCanvas(20, 20) const ctx = canvas.getContext('2d') @@ -2605,9 +2451,6 @@ describe('Canvas', function () { ['shadowBlur', 5], ['shadowColor', '#ff0000'], ['globalCompositeOperation', 'copy'], - ['font', '25px serif'], - ['textAlign', 'center'], - ['textBaseline', 'bottom'], // Added vs. WPT ['imageSmoothingEnabled', false], // ['imageSmoothingQuality', ], // not supported by node-canvas, #2114 @@ -2615,10 +2458,7 @@ describe('Canvas', function () { // Non-standard properties: ['patternQuality', 'best'], // ['quality', 'best'], // doesn't do anything, TODO remove - ['textDrawingMode', 'glyph'], - ['antialias', 'gray'], - ['lang', 'eu'], - ['direction', 'rtl'] + ['antialias', 'gray'] ] for (const [k, v] of state) { @@ -2687,7 +2527,6 @@ describe('Canvas', function () { const canvas = createCanvas(20, 20, 'pdf') const ctx = canvas.getContext('2d') ctx.beginTag('Link', "uri='http://example.com'") - ctx.strokeText('hello', 0, 0) ctx.endTag('Link') const buf = canvas.toBuffer('application/pdf') assert.equal('PDF', buf.slice(1, 4).toString()) From 720e949d994dbcbcc7d47ac73ec45c49fef7d982 Mon Sep 17 00:00:00 2001 From: Caleb Hearon Date: Sun, 22 Jun 2025 15:37:35 -0400 Subject: [PATCH 2/8] remove all SVG image related functionality The APIs will come back after zig build and use our fork of lunasvg, which will be significantly easier to static build. --- src/Image.cc | 196 +-------------------------------------------- src/Image.h | 21 ----- src/init.cc | 4 - test/image.test.js | 41 ---------- 4 files changed, 1 insertion(+), 261 deletions(-) diff --git a/src/Image.cc b/src/Image.cc index 99dcb152b..468a0f1cb 100644 --- a/src/Image.cc +++ b/src/Image.cc @@ -85,11 +85,6 @@ Image::Image(const Napi::CallbackInfo& info) : ObjectWrap(info), env(info width = height = 0; naturalWidth = naturalHeight = 0; state = DEFAULT; -#ifdef HAVE_RSVG - _rsvg = NULL; - _is_svg = false; - _svg_last_width = _svg_last_height = 0; -#endif } /* @@ -209,13 +204,6 @@ Image::clearData() { free(filename); filename = NULL; -#ifdef HAVE_RSVG - if (_rsvg != NULL) { - g_object_unref(_rsvg); - _rsvg = NULL; - } -#endif - width = height = 0; naturalWidth = naturalHeight = 0; state = DEFAULT; @@ -311,18 +299,6 @@ Image::loadFromBuffer(uint8_t *buf, unsigned len) { #endif } - // confirm svg using first 1000 chars - // if a very long comment precedes the root tag, isSVG returns false - unsigned head_len = (len < 1000 ? len : 1000); - if (isSVG(buf, head_len)) { -#ifdef HAVE_RSVG - return loadSVGFromBuffer(buf, len); -#else - this->errorInfo.set("node-canvas was built without SVG support"); - return CAIRO_STATUS_READ_ERROR; -#endif - } - if (isBMP(buf, len)) return loadBMPFromBuffer(buf, len); @@ -394,22 +370,6 @@ Image::loaded() { * Returns this image's surface. */ cairo_surface_t *Image::surface() { -#ifdef HAVE_RSVG - if (_is_svg && (_svg_last_width != width || _svg_last_height != height)) { - if (_surface != NULL) { - cairo_surface_destroy(_surface); - _surface = NULL; - } - - cairo_status_t status = renderSVGToSurface(); - if (status != CAIRO_STATUS_SUCCESS) { - g_object_unref(_rsvg); - Napi::Error::New(env, cairo_status_to_string(status)).ThrowAsJavaScriptException(); - - return NULL; - } - } -#endif return _surface; } @@ -459,31 +419,8 @@ Image::loadSurface() { #endif } - // confirm svg using first 1000 chars - // if a very long comment precedes the root tag, isSVG returns false - uint8_t head[1000] = {0}; - fseek(stream, 0 , SEEK_END); - long len = ftell(stream); - unsigned head_len = (len < 1000 ? len : 1000); - unsigned head_size = head_len * sizeof(uint8_t); - rewind(stream); - if (head_size != fread(&head, 1, head_size, stream)) { - fclose(stream); - return CAIRO_STATUS_READ_ERROR; - } - rewind(stream); - if (isSVG(head, head_len)) { -#ifdef HAVE_RSVG - return loadSVG(stream); -#else - this->errorInfo.set("node-canvas was built without SVG support"); - return CAIRO_STATUS_READ_ERROR; -#endif - } - if (isBMP(buf, 2)) return loadBMP(stream); - fclose(stream); this->errorInfo.set("Unsupported image type"); @@ -1459,115 +1396,6 @@ Image::rotatePixels(uint8_t* pixels, int width, int height, int channels, #endif /* HAVE_JPEG */ -#ifdef HAVE_RSVG - -/* - * Load SVG from buffer - */ - -cairo_status_t -Image::loadSVGFromBuffer(uint8_t *buf, unsigned len) { - _is_svg = true; - - if (NULL == (_rsvg = rsvg_handle_new_from_data(buf, len, nullptr))) { - return CAIRO_STATUS_READ_ERROR; - } - - double d_width; - double d_height; - - rsvg_handle_get_intrinsic_size_in_pixels(_rsvg, &d_width, &d_height); - - width = naturalWidth = d_width; - height = naturalHeight = d_height; - - if (width <= 0 || height <= 0) { - this->errorInfo.set("Width and height must be set on the svg element"); - return CAIRO_STATUS_READ_ERROR; - } - - return renderSVGToSurface(); -} - -/* - * Renders the Rsvg handle to this image's surface - */ -cairo_status_t -Image::renderSVGToSurface() { - cairo_status_t status; - - _surface = cairo_image_surface_create(CAIRO_FORMAT_ARGB32, width, height); - - status = cairo_surface_status(_surface); - if (status != CAIRO_STATUS_SUCCESS) { - g_object_unref(_rsvg); - return status; - } - - cairo_t *cr = cairo_create(_surface); - status = cairo_status(cr); - if (status != CAIRO_STATUS_SUCCESS) { - g_object_unref(_rsvg); - return status; - } - - RsvgRectangle viewport = { - 0, // x - 0, // y - static_cast(width), - static_cast(height) - }; - gboolean render_ok = rsvg_handle_render_document(_rsvg, cr, &viewport, nullptr); - if (!render_ok) { - g_object_unref(_rsvg); - cairo_destroy(cr); - return CAIRO_STATUS_READ_ERROR; // or WRITE? - } - - cairo_destroy(cr); - - _svg_last_width = width; - _svg_last_height = height; - - return status; -} - -/* - * Load SVG - */ - -cairo_status_t -Image::loadSVG(FILE *stream) { - _is_svg = true; - - struct stat s; - int fd = fileno(stream); - - // stat - if (fstat(fd, &s) < 0) { - fclose(stream); - return CAIRO_STATUS_READ_ERROR; - } - - uint8_t *buf = (uint8_t *) malloc(s.st_size); - - if (!buf) { - fclose(stream); - return CAIRO_STATUS_NO_MEMORY; - } - - size_t read = fread(buf, s.st_size, 1, stream); - fclose(stream); - - cairo_status_t result = CAIRO_STATUS_READ_ERROR; - if (1 == read) result = loadSVGFromBuffer(buf, s.st_size); - free(buf); - - return result; -} - -#endif /* HAVE_RSVG */ - /* * Load BMP from buffer. */ @@ -1640,7 +1468,7 @@ cairo_status_t Image::loadBMP(FILE *stream){ } /* - * Return UNKNOWN, SVG, GIF, JPEG, or PNG based on the filename. + * Return UNKNOWN, GIF, JPEG, or PNG based on the filename. */ Image::type @@ -1651,7 +1479,6 @@ Image::extension(const char *filename) { if (len >= 4 && 0 == strcmp(".gif", filename - 4)) return Image::GIF; if (len >= 4 && 0 == strcmp(".jpg", filename - 4)) return Image::JPEG; if (len >= 4 && 0 == strcmp(".png", filename - 4)) return Image::PNG; - if (len >= 4 && 0 == strcmp(".svg", filename - 4)) return Image::SVG; return Image::UNKNOWN; } @@ -1682,27 +1509,6 @@ Image::isPNG(uint8_t *data) { return 'P' == data[1] && 'N' == data[2] && 'G' == data[3]; } -/* - * Skip " - // librsvg <= 2.36.1, identified by undefined macro, needs an extra include - #ifndef LIBRSVG_CHECK_VERSION - #include - #endif -#endif - using JPEGDecodeL = std::function; class Image : public Napi::ObjectWrap { @@ -57,7 +49,6 @@ class Image : public Napi::ObjectWrap { static int isPNG(uint8_t *data); static int isJPEG(uint8_t *data); static int isGIF(uint8_t *data); - static int isSVG(uint8_t *data, unsigned len); static int isBMP(uint8_t *data, unsigned len); static cairo_status_t readPNG(void *closure, unsigned char *data, unsigned len); inline int isComplete(){ return COMPLETE == state; } @@ -67,11 +58,6 @@ class Image : public Napi::ObjectWrap { cairo_status_t loadPNGFromBuffer(uint8_t *buf); cairo_status_t loadPNG(); void clearData(); -#ifdef HAVE_RSVG - cairo_status_t loadSVGFromBuffer(uint8_t *buf, unsigned len); - cairo_status_t loadSVG(FILE *stream); - cairo_status_t renderSVGToSurface(); -#endif #ifdef HAVE_GIF cairo_status_t loadGIFFromBuffer(uint8_t *buf, unsigned len); cairo_status_t loadGIF(FILE *stream); @@ -127,7 +113,6 @@ class Image : public Napi::ObjectWrap { , GIF , JPEG , PNG - , SVG } type; static type extension(const char *filename); @@ -136,10 +121,4 @@ class Image : public Napi::ObjectWrap { cairo_surface_t *_surface; uint8_t *_data = nullptr; int _data_len; -#ifdef HAVE_RSVG - RsvgHandle *_rsvg; - bool _is_svg; - int _svg_last_width; - int _svg_last_height; -#endif }; diff --git a/src/init.cc b/src/init.cc index a974efd4a..60d8079e7 100644 --- a/src/init.cc +++ b/src/init.cc @@ -97,10 +97,6 @@ Napi::Object init(Napi::Env env, Napi::Object exports) { #endif #endif -#ifdef HAVE_RSVG - exports.Set("rsvgVersion", Napi::String::New(env, LIBRSVG_VERSION)); -#endif - char freetype_version[10]; snprintf(freetype_version, 10, "%d.%d.%d", FREETYPE_MAJOR, FREETYPE_MINOR, FREETYPE_PATCH); exports.Set("freetypeVersion", Napi::String::New(env, freetype_version)); diff --git a/test/image.test.js b/test/image.test.js index defb52115..c1c764572 100644 --- a/test/image.test.js +++ b/test/image.test.js @@ -14,7 +14,6 @@ const { Blob } = require('node:buffer') const { URL } = require('node:url') const { createCanvas, loadImage, rsvgVersion, Image } = require('../') -const HAVE_SVG = rsvgVersion !== undefined const pngCheckers = path.join(__dirname, '/fixtures/checkers.png') @@ -152,32 +151,6 @@ describe('Image', function () { }, MyError) }) - it('loads SVG data URL base64', function () { - if (!HAVE_SVG) this.skip() - const base64Enc = fs.readFileSync(svgTree, 'base64') - const dataURL = `data:image/svg+xml;base64,${base64Enc}` - return loadImage(dataURL).then((img) => { - assert.strictEqual(img.onerror, null) - assert.strictEqual(img.onload, null) - assert.strictEqual(img.width, 200) - assert.strictEqual(img.height, 200) - assert.strictEqual(img.complete, true) - }) - }) - - it('loads SVG data URL utf8', function () { - if (!HAVE_SVG) this.skip() - const utf8Encoded = fs.readFileSync(svgTree, 'utf8') - const dataURL = `data:image/svg+xml;utf8,${utf8Encoded}` - return loadImage(dataURL).then((img) => { - assert.strictEqual(img.onerror, null) - assert.strictEqual(img.onload, null) - assert.strictEqual(img.width, 200) - assert.strictEqual(img.height, 200) - assert.strictEqual(img.complete, true) - }) - }) - it('calls Image#onload multiple times', function () { return loadImage(pngClock).then((img) => { let onloadCalled = 0 @@ -376,20 +349,6 @@ describe('Image', function () { assert.ok(!keys.includes('setSource')) }) - it('loadImage doesn\'t crash when you don\'t specify width and height', async function () { - const err = {name: 'Error'} - - // TODO: remove this when we have a static build or something - if (os.platform() !== 'win32') { - err.message = 'Width and height must be set on the svg element'; - } - - await assert.rejects(async () => { - const svg = ``; - await loadImage(Buffer.from(svg)) - }, err) - }) - describe('supports BMP', function () { it('parses 1-bit image', function (done) { const img = new Image() From cf044c768ff6e606c03150ec11b3a4061c22bbca Mon Sep 17 00:00:00 2001 From: Caleb Hearon Date: Mon, 18 Aug 2025 21:59:08 -0400 Subject: [PATCH 3/8] hello Zig! introduce static build - Binaries are now distributed in a package per target triple - Cross-compiling ability significantly reduces maintenance burden - A single compiler (LLVM) removes tons of macros - Moving to zig-only build instead of node-gyp removes tons of macros - Several new Linux binaries were added with ease: musl, ARM, riscv64 - Back to zero package.json dependencies As the build.zig says, we will maintain our own build.zig files for almost all dependencies. This makes it easier to tailor changes. --- .github/workflows/ci.yaml | 40 +- .github/workflows/prebuild.yaml | 13 - .github/workflows/publish.yaml | 51 +- .gitignore | 5 +- README.md | 52 +- binding.gyp | 232 -- build.zig | 129 + build.zig.zon | 27 + lib/bindings.js | 88 +- lib/jpegstream.js | 4 - npm/darwin-arm64/package.json | 15 + npm/darwin-x64/package.json | 15 + npm/linux-arm-gnueabihf/package.json | 15 + npm/linux-arm64-gnu/package.json | 15 + npm/linux-arm64-musl/package.json | 15 + npm/linux-riscv64-gnu/package.json | 15 + npm/linux-x64-gnu/package.json | 15 + npm/linux-x64-musl/package.json | 15 + npm/linux-x86-gnu/package.json | 15 + npm/linux-x86-musl/package.json | 15 + npm/win32-arm64/package.json | 15 + npm/win32-x64/package.json | 15 + npm/win32-x86/package.json | 15 + package-lock.json | 3501 +++++++++++++------------- package.json | 36 +- pkg/cairo/build.zig | 267 ++ pkg/cairo/build.zig.zon | 22 + pkg/cairo/sources.zig | 138 + pkg/freetype/build.zig | 124 + pkg/freetype/build.zig.zon | 20 + pkg/giflib/build.zig | 41 + pkg/giflib/build.zig.zon | 13 + pkg/libpng/LICENSE | 20 + pkg/libpng/build.zig | 69 + pkg/libpng/build.zig.zon | 19 + pkg/libpng/pnglibconf.h | 219 ++ pkg/pixman/build.zig | 67 + pkg/pixman/build.zig.zon | 10 + pkg/zlib/LICENSE | 21 + pkg/zlib/build.zig | 49 + pkg/zlib/build.zig.zon | 17 + scripts/version.js | 68 + src/Canvas.cc | 29 - src/Canvas.h | 4 +- src/CanvasRenderingContext2d.cc | 14 - src/CanvasRenderingContext2d.h | 2 - src/Image.cc | 46 - src/Image.h | 15 +- src/PNG.h | 26 +- src/bmp/BMPParser.h | 110 +- src/closure.cc | 2 - src/closure.h | 8 +- src/color.cc | 5 - src/dll_visibility.h | 20 - src/init.cc | 44 +- 55 files changed, 3538 insertions(+), 2344 deletions(-) delete mode 100644 .github/workflows/prebuild.yaml delete mode 100644 binding.gyp create mode 100644 build.zig create mode 100644 build.zig.zon create mode 100644 npm/darwin-arm64/package.json create mode 100644 npm/darwin-x64/package.json create mode 100644 npm/linux-arm-gnueabihf/package.json create mode 100644 npm/linux-arm64-gnu/package.json create mode 100644 npm/linux-arm64-musl/package.json create mode 100644 npm/linux-riscv64-gnu/package.json create mode 100644 npm/linux-x64-gnu/package.json create mode 100644 npm/linux-x64-musl/package.json create mode 100644 npm/linux-x86-gnu/package.json create mode 100644 npm/linux-x86-musl/package.json create mode 100644 npm/win32-arm64/package.json create mode 100644 npm/win32-x64/package.json create mode 100644 npm/win32-x86/package.json create mode 100644 pkg/cairo/build.zig create mode 100644 pkg/cairo/build.zig.zon create mode 100644 pkg/cairo/sources.zig create mode 100644 pkg/freetype/build.zig create mode 100644 pkg/freetype/build.zig.zon create mode 100644 pkg/giflib/build.zig create mode 100644 pkg/giflib/build.zig.zon create mode 100644 pkg/libpng/LICENSE create mode 100644 pkg/libpng/build.zig create mode 100644 pkg/libpng/build.zig.zon create mode 100644 pkg/libpng/pnglibconf.h create mode 100644 pkg/pixman/build.zig create mode 100644 pkg/pixman/build.zig.zon create mode 100644 pkg/zlib/LICENSE create mode 100644 pkg/zlib/build.zig create mode 100644 pkg/zlib/build.zig.zon create mode 100644 scripts/version.js delete mode 100644 src/dll_visibility.h diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 43ddffb41..9b09df10a 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -1,11 +1,7 @@ name: Test on: - push: - paths-ignore: - - ".github/workflows/prebuild.yaml" - pull_request: - paths-ignore: - - ".github/workflows/prebuild.yaml" + - push + - pull_request jobs: Linux: @@ -19,12 +15,11 @@ jobs: with: node-version: ${{ matrix.node }} - uses: actions/checkout@v4 - - name: Install Dependencies - run: | - sudo apt update - sudo apt install -y libcairo2-dev libjpeg-dev libpango1.0-dev libgif-dev librsvg2-dev + - uses: mlugg/setup-zig@v2 - name: Install - run: npm ci --build-from-source + run: npm ci --force # https://github.com/npm/cli/issues/6138 + - name: Build + run: zig build - name: Test run: npm test @@ -39,15 +34,11 @@ jobs: with: node-version: ${{ matrix.node }} - uses: actions/checkout@v4 - - name: Install Dependencies - run: | - Invoke-WebRequest "https://ftp.gnome.org/pub/GNOME/binaries/win64/gtk+/2.22/gtk+-bundle_2.22.1-20101229_win64.zip" -OutFile "gtk.zip" - Expand-Archive gtk.zip -DestinationPath "C:\GTK" - Invoke-WebRequest "https://downloads.sourceforge.net/project/libjpeg-turbo/2.0.4/libjpeg-turbo-2.0.4-vc64.exe" -OutFile "libjpeg.exe" -UserAgent NativeHost - .\libjpeg.exe /S - choco install vcredist2010 -y + - uses: mlugg/setup-zig@v2 - name: Install - run: npm ci --build-from-source + run: npm ci --force + - name: Build + run: zig build - name: Test run: npm test @@ -62,12 +53,11 @@ jobs: with: node-version: ${{ matrix.node }} - uses: actions/checkout@v4 - - name: Install Dependencies - run: | - brew update - brew install python-setuptools pkg-config cairo pango libpng jpeg giflib librsvg + - uses: mlugg/setup-zig@v2 - name: Install - run: npm ci --build-from-source + run: npm ci --force + - name: Build + run: zig build - name: Test run: npm test @@ -80,7 +70,7 @@ jobs: node-version: 20.9.0 - uses: actions/checkout@v4 - name: Install - run: npm ci --ignore-scripts + run: npm ci --force - name: Lint run: npm run lint - name: Lint Types diff --git a/.github/workflows/prebuild.yaml b/.github/workflows/prebuild.yaml deleted file mode 100644 index 88b6288bf..000000000 --- a/.github/workflows/prebuild.yaml +++ /dev/null @@ -1,13 +0,0 @@ -# This is a dummy file so that this workflow shows up in the Actions tab. -# Prebuilds are actually run using the prebuilds branch. - -name: Make Prebuilds -on: workflow_dispatch - -jobs: - Linux: - name: Nothing - runs-on: ubuntu-latest - steps: - - name: Nothing - run: echo "Nothing to do here" diff --git a/.github/workflows/publish.yaml b/.github/workflows/publish.yaml index bd641c145..48753e65d 100644 --- a/.github/workflows/publish.yaml +++ b/.github/workflows/publish.yaml @@ -1,13 +1,50 @@ -# This is a dummy file so that this workflow shows up in the Actions tab. -# Publishing packages via CI is a 4x feature (next branch) not yet released - name: Publish on: workflow_dispatch jobs: - Linux: - name: Nothing + binaries: + name: ${{ matrix.package }} + runs-on: ${{ matrix.runner }} + strategy: + fail-fast: false + matrix: + include: + - { runner: macos-15, target: aarch64-macos, package: canvas-darwin-arm64 } + - { runner: macos-15, target: x86_64-macos, package: canvas-darwin-x64 } + - { runner: ubuntu-latest, target: x86-windows, package: canvas-win32-x86 } + - { runner: ubuntu-latest, target: x86_64-windows, package: canvas-win32-x64 } + - { runner: ubuntu-latest, target: aarch64-windows, package: canvas-win32-arm64 } + - { runner: ubuntu-latest, target: arm-linux-gnueabihf, package: canvas-linux-arm-gnueabihf } + - { runner: ubuntu-latest, target: x86-linux-gnu.2.28, package: canvas-linux-x86-gnu } + - { runner: ubuntu-latest, target: x86-linux-musl, package: canvas-linux-x86-musl } + - { runner: ubuntu-latest, target: x86_64-linux-gnu.2.28, package: canvas-linux-x64-gnu } + - { runner: ubuntu-latest, target: x86_64-linux-musl, package: canvas-linux-x64-musl } + - { runner: ubuntu-latest, target: aarch64-linux-gnu.2.28, package: canvas-linux-arm64-gnu } + - { runner: ubuntu-latest, target: aarch64-linux-musl, package: canvas-linux-arm64-musl } + - { runner: ubuntu-latest, target: riscv64-linux-gnu, package: canvas-linux-riscv64-gnu } + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 + with: + node-version: 26 + - uses: mlugg/setup-zig@v2 + - name: Install + run: npm ci --force # https://github.com/npm/cli/issues/6138 + - name: Build + run: zig build -Dtarget=${{ matrix.target }} -Doptimize=ReleaseSafe + - name: Publish + run: npm publish --dry-run --workspace ${{ matrix.package }} + + root: + name: canvas + needs: binaries runs-on: ubuntu-latest steps: - - name: Nothing - run: echo "Nothing to do here" + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 + with: + node-version: 26 + - name: Install + run: npm ci --force # https://github.com/npm/cli/issues/6138 + - name: Publish + run: npm publish --dry-run diff --git a/.gitignore b/.gitignore index 4dc95228d..87b3a2abe 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,5 @@ -build +bin +canvas.node .DS_Store .lock-wscript test/images/*.png @@ -18,3 +19,5 @@ node_modules npm-debug.log .idea + +.zig-cache/ diff --git a/README.md b/README.md index fc027c870..ebae896bd 100644 --- a/README.md +++ b/README.md @@ -11,34 +11,16 @@ node-canvas is a [Cairo](http://cairographics.org/)-backed Canvas implementation $ npm install canvas ``` -By default, pre-built binaries will be downloaded if you're on one of the following platforms: -- macOS x86/64 -- macOS aarch64 (aka Apple silicon) -- Linux x86/64 (glibc only) -- Windows x86/64 - -If you want to build from source, use `npm install --build-from-source` and see the **Compiling** section below. - The minimum version of Node.js required is **18.12.0**. -### Compiling - -If you don't have a supported OS or processor architecture, or you use `--build-from-source`, the module will be compiled on your system. This requires several dependencies, including Cairo and Pango. - -For detailed installation information, see the [wiki](https://github.com/Automattic/node-canvas/wiki/_pages). One-line installation instructions for common OSes are below. Note that libgif/giflib, librsvg and libjpeg are optional and only required if you need GIF, SVG and JPEG support, respectively. Cairo v1.10.0 or later is required. - -OS | Command ------ | ----- -macOS | Using [Homebrew](https://brew.sh/):
`brew install pkg-config cairo pango libpng jpeg giflib librsvg pixman python-setuptools` -Ubuntu | `sudo apt-get install build-essential libcairo2-dev libpango1.0-dev libjpeg-dev libgif-dev librsvg2-dev` -Fedora | `sudo yum install gcc-c++ cairo-devel pango-devel libjpeg-turbo-devel giflib-devel` -Solaris | `pkgin install cairo pango pkg-config xproto renderproto kbproto xextproto` -OpenBSD | `doas pkg_add cairo pango png jpeg giflib` -Windows | See the [wiki](https://github.com/Automattic/node-canvas/wiki/Installation:-Windows) -Others | See the [wiki](https://github.com/Automattic/node-canvas/wiki) +Binaries will be installed for the following platforms: +- macOS x64/arm64 +- Linux x64/x86/arm64 (glibc or musl) +- Linux arm/riscv64 (glibc) +- Windows x64/x86/arm64 -**Mac OS X v10.11+:** If you have recently updated to Mac OS X v10.11+ and are experiencing trouble when compiling, run the following command: `xcode-select --install`. Read more about the problem [on Stack Overflow](http://stackoverflow.com/a/32929012/148072). -If you have xcode 10.0 or higher installed, in order to build from source you need NPM 6.4.1 or higher. +If you want to install for a more exotic system, CD into the package and run +`zig build`. You may need to tweak one of the build.zig files. PRs are welcome. ## Quick Example @@ -219,7 +201,7 @@ Enabling mime data tracking has no benefits (only a slow down) unless you are ge Creates a [`Buffer`](https://nodejs.org/api/buffer.html) object representing the image contained in the canvas. * **callback** If provided, the buffer will be provided in the callback instead of being returned by the function. Invoked with an error as the first argument if encoding failed, or the resulting buffer as the second argument if it succeeded. Not supported for mimeType `raw` or for PDF or SVG canvases. -* **mimeType** A string indicating the image format. Valid options are `image/png`, `image/jpeg` (if node-canvas was built with JPEG support), `raw` (unencoded data in BGRA order on little-endian (most) systems, ARGB on big-endian systems; top-to-bottom), `application/pdf` (for PDF canvases) and `image/svg+xml` (for SVG canvases). Defaults to `image/png` for image canvases, or the corresponding type for PDF or SVG canvas. +* **mimeType** A string indicating the image format. Valid options are `image/png`, `image/jpeg`, `raw` (unencoded data in BGRA order on little-endian (most) systems, ARGB on big-endian systems; top-to-bottom), `application/pdf` (for PDF canvases) and `image/svg+xml` (for SVG canvases). Defaults to `image/png` for image canvases, or the corresponding type for PDF or SVG canvas. * **config** * For `image/jpeg`, an object specifying the quality (0 to 1), if progressive compression should be used and/or if chroma subsampling should be used: `{quality: 0.75, progressive: false, chromaSubsampling: true}`. All properties are optional. @@ -227,7 +209,7 @@ Creates a [`Buffer`](https://nodejs.org/api/buffer.html) object representing the Note that the PNG format encodes the resolution in pixels per meter, so if you specify `96`, the file will encode 3780 ppm (~96.01 ppi). The resolution is undefined by default to match common browser behavior. - * For `application/pdf`, an object specifying optional document metadata: `{title: string, author: string, subject: string, keywords: string, creator: string, creationDate: Date, modDate: Date}`. All properties are optional and default to `undefined`, except for `creationDate`, which defaults to the current date. *Adding metadata requires Cairo 1.16.0 or later.* + * For `application/pdf`, an object specifying optional document metadata: `{title: string, author: string, subject: string, keywords: string, creator: string, creationDate: Date, modDate: Date}`. All properties are optional and default to `undefined`, except for `creationDate`, which defaults to the current date. For a description of these properties, see page 550 of [PDF 32000-1:2008](https://www.adobe.com/content/dam/acom/en/devnet/acrobat/pdfs/PDF32000_2008.pdf). @@ -351,7 +333,7 @@ const stream = canvas.createJPEGStream({ > canvas.createPDFStream(config?: any) => ReadableStream > ``` -* `config` an object specifying optional document metadata: `{title: string, author: string, subject: string, keywords: string, creator: string, creationDate: Date, modDate: Date}`. See `toBuffer()` for more information. *Adding metadata requires Cairo 1.16.0 or later.* +* `config` an object specifying optional document metadata: `{title: string, author: string, subject: string, keywords: string, creator: string, creationDate: Date, modDate: Date}`. See `toBuffer()` for more information. Applies to PDF canvases only. Creates a [`ReadableStream`](https://nodejs.org/api/stream.html#stream_class_stream_readable) that emits the encoded PDF. `canvas.toBuffer()` also produces an encoded PDF, but `createPDFStream()` can be used to reduce memory usage. @@ -439,7 +421,7 @@ ctx.fillText('Hello World 2', 50, 80) canvas.toBuffer() // returns a PDF file canvas.createPDFStream() // returns a ReadableStream that emits a PDF -// With optional document metadata (requires Cairo 1.16.0): +// With optional document metadata: canvas.toBuffer('application/pdf', { title: 'my picture', keywords: 'node.js demo cairo', @@ -523,7 +505,7 @@ These additional pixel formats have experimental support: * `RGB16_565` Each pixel is 16 bits, with red in the upper 5 bits, green in the middle 6 bits, and blue in the lower 5 bits, in native platform endianness. Some hardware devices and frame buffers use this format. Note that PNG does not support this format; when creating a PNG, the image will be converted to 24-bit RGB. This format is thus suboptimal for generating PNGs. `ImageData` instances for this mode use a `Uint16Array` instead of a `Uint8ClampedArray`. * `A1` Each pixel is 1 bit, and pixels are packed together into 32-bit quantities. The ordering of the bits matches the endianness of the platform: on a little-endian machine, the first pixel is the least-significant bit. This format can be used for creating single-color images. *Support for this format is incomplete, see note below.* -* `RGB30` Each pixel is 30 bits, with red in the upper 10, green in the middle 10, and blue in the lower 10. (Requires Cairo 1.12 or later.) *Support for this format is incomplete, see note below.* +* `RGB30` Each pixel is 30 bits, with red in the upper 10, green in the middle 10, and blue in the lower 10. *Support for this format is incomplete, see note below.* Notes and caveats: @@ -540,23 +522,23 @@ Notes and caveats: * The `ImageData(width, height)` and `ImageData(Uint8ClampedArray, width)` constructors assume 4 bytes per pixel. To create an `ImageData` instance with a different number of bytes per pixel, use `new ImageData(new Uint8ClampedArray(size), width, height)` or `new ImageData(new Uint16ClampedArray(size), width, height)`. -## Testing +## Local Development and Testing -First make sure you've built the latest version. Get all the deps you need (see [compiling](#compiling) above), and run: +To set up node-canvas for local develoment, use the `--force` argument (until [npm/cli#6138](https://github.com/npm/cli/issues/6138) is fixed): ``` -npm install --build-from-source +npm install --force ``` For visual tests: `npm run test-server` and point your browser to http://localhost:4000. For unit tests: `npm run test`. -## Benchmarks +### Benchmarks Benchmarks live in the `benchmarks` directory. -## Examples +### Examples Examples line in the `examples` directory. Most produce a png image of the same name, and others such as *live-clock.js* launch an HTTP server to be viewed in the browser. diff --git a/binding.gyp b/binding.gyp deleted file mode 100644 index 81acdb860..000000000 --- a/binding.gyp +++ /dev/null @@ -1,232 +0,0 @@ -{ - 'conditions': [ - ['OS=="win"', { - 'variables': { - 'GTK_Root%': 'C:/GTK', # Set the location of GTK all-in-one bundle - 'with_jpeg%': 'false', - 'with_gif%': 'false', - 'with_rsvg%': 'false', - 'variables': { # Nest jpeg_root to evaluate it before with_jpeg - 'jpeg_root%': ' switch (arch) { + .aarch64 => return "../npm/darwin-arm64/canvas.node", + .x86_64 => return "../npm/darwin-x64/canvas.node", + else => {}, + }, + .linux => switch (arch) { + .arm => switch (abi) { + .gnueabihf => return "../npm/linux-arm-gnueabihf/canvas.node", + else => {}, + }, + .x86 => switch (abi) { + .gnu => return "../npm/linux-x86-gnu/canvas.node", + .musl => return "../npm/linux-x86-musl/canvas.node", + else => {}, + }, + .aarch64 => switch (abi) { + .gnu => return "../npm/linux-arm64-gnu/canvas.node", + .musl => return "../npm/linux-arm64-musl/canvas.node", + else => {}, + }, + .riscv64 => switch (abi) { + .gnu => return "../npm/linux-riscv64-gnu/canvas.node", + else => {}, + }, + .x86_64 => switch (abi) { + .gnu => return "../npm/linux-x64-gnu/canvas.node", + .musl => return "../npm/linux-x64-musl/canvas.node", + else => {}, + }, + else => {}, + }, + .windows => switch (arch) { + .aarch64 => return "../npm/win32-arm64/canvas.node", + .x86_64 => return "../npm/win32-x64/canvas.node", + .x86 => return "../npm/win32-x86/canvas.node", + else => {}, + }, + else => {}, + } + + return "../bin/canvas.node"; +} + +pub fn build(b: *std.Build) void { + const target = b.standardTargetOptions(.{}); + const optimize = b.standardOptimizeOption(.{}); + const cairo = b.dependency("cairo", .{ + .target = target, + .optimize = optimize, + }).artifact("cairo"); + + const libjpeg_turbo = b.dependency("libjpeg_turbo", .{ + .target = target, + .optimize = optimize, + .pie = true, + }).artifact("libjpeg_turbo"); + + const giflib = b.dependency("giflib", .{ + .target = target, + .optimize = optimize, + }).artifact("giflib"); + + const libpng = b.dependency("libpng", .{ + .target = target, + .optimize = optimize, + }).artifact("png"); + + const canvas = b.addLibrary(.{ + .name = "canvas", + .linkage = .dynamic, + .root_module = b.createModule(.{ + .target = target, + .optimize = optimize, + }) + }); + + const node_api = b.dependency("node_api", .{ + .target = target, + .optimize = optimize, + }).artifact("node_api"); + + canvas.addCSourceFiles(.{ + .files = &.{ + "src/bmp/BMPParser.cc", + "src/Canvas.cc", + "src/CanvasGradient.cc", + "src/CanvasPattern.cc", + "src/CanvasRenderingContext2d.cc", + "src/closure.cc", + "src/color.cc", + "src/Image.cc", + "src/ImageData.cc", + "src/init.cc", + "src/FontParser.cc" + } , + .flags = &.{ + "-DNAPI_DISABLE_CPP_EXCEPTIONS", + "-DNODE_ADDON_API_ENABLE_MAYBE", + if (target.result.os.tag == .windows) "-DCAIRO_WIN32_STATIC_BUILD" else "", + } + }); + + canvas.linker_allow_shlib_undefined = true; + + if (optimize != .Debug) { + canvas.root_module.strip = true; + } + + canvas.linkLibC(); + canvas.linkLibCpp(); + canvas.addObject(node_api); + canvas.linkLibrary(cairo); + canvas.linkLibrary(libjpeg_turbo); + canvas.linkLibrary(libpng); + canvas.linkLibrary(giflib); + + const move = b.addInstallFile(canvas.getEmittedBin(), outputPath(target)); + move.step.dependOn(&canvas.step); + b.getInstallStep().dependOn(&move.step); +} diff --git a/build.zig.zon b/build.zig.zon new file mode 100644 index 000000000..446d48bf7 --- /dev/null +++ b/build.zig.zon @@ -0,0 +1,27 @@ +.{ + .name = .canvas, + .fingerprint = 0xa59f6c18a5521654, + .version = "0.0.0", + .paths = .{""}, + .minimum_zig_version = "0.15.1", + // Sub-packages only specify dependencies for upstream C/C++ code. + // Everything else goes here so it's easier to share dependencies and ensure + // we don't build duplicate versions. Generally build.zig[.zon] files should + // be vendored so we can simplify them and have control over upgrades and + // things like -fPIC. Exceptions should (and are) justified below + .dependencies = .{ + // Simple and general enough to use the canonical URL + .node_api = .{ + .url = "https://github.com/chearon/node-api-zig/archive/37d992b2ff53608e301ce2d5bd76ec31a1a247d9.tar.gz", + .hash = "node_api-0.0.0-D68afJgfAABNKwoA-8I7d4v9fJUwM89W-yOHKMSNU7Em", + }, + // Too complicated to maintain here (large build.zig) + .libjpeg_turbo = .{ + .url = "https://github.com/chearon/libjpeg-turbo/archive/1222499df19186b61915ef6bf1b3bda15003584b.tar.gz", + .hash = "libjpeg_turbo-3.1.1-1-iiYWsnWCSQBHXuzUprRraTbBN-HJfN2dOX6BuZk6iKUi", + }, + .giflib = .{ .path = "./pkg/giflib" }, + .libpng = .{ .path = "./pkg/libpng" }, + .cairo = .{ .path = "./pkg/cairo" }, + }, +} diff --git a/lib/bindings.js b/lib/bindings.js index 40cef3c69..3e3c7caf7 100644 --- a/lib/bindings.js +++ b/lib/bindings.js @@ -1,6 +1,92 @@ 'use strict' -const bindings = require('../build/Release/canvas.node') +const fs = require('fs'); +// https://github.com/yarnpkg/berry/blob/fe0418084777ccd7a1524b3315102da9d5ce6a9f/packages/yarnpkg-core/sources/nodeUtils.ts#L28 +function getLibc() { + // As of 2025, linux is the only possible process.platform value that does not + // imply the libc for Node's purposes. Technically mingw32 (a way to build and + // run software using glibc on Windows) exists and even has a node.js port, + // but no one in the broader node.js ecosystem seems to care about it. There + // have been issues in the past running getReport() on Windows. + if (process.platform !== 'linux') return null + + let header + try { + header = fs.readFileSync('/usr/bin/ldd') + } catch {} + + // Since the getReport can be prohibitely expensive (it also queries DNS + // which, if misconfigured, can take a long time to timeout), we first check + // if the ldd binary is glibc or musl, and only then run the getReport() if we + // can't determine the libc variant. + if (typeof header !== 'undefined') { + if (header && (header.includes('GLIBC') || header.includes('GNU libc') || header.includes('GNU C Library'))) + return 'glibc' + if (header && header.includes('musl')) { + return 'musl' + } + } + + const report = process.report?.getReport() ?? {} + const sharedObjects = report.sharedObjects ?? [] + + // Matches the first group if libc, second group if musl + const libcRegExp = /\/(?:(ld-linux-|[^/]+-linux-gnu\/)|(libc.musl-|ld-musl-))/ + + for (const entry of sharedObjects) { + const match = entry.match(libcRegExp) + + if (match) { + if (match[1]) return 'glibc' + if (match[2]) return 'musl' + throw new Error('Assertion failed: Expected the libc variant to have been detected') + } + } +} + +// mirror changes here with build.zig +function requireAddon() { + switch (process.platform) { + case 'darwin': + switch (process.arch) { + case 'arm64': return require('canvas-darwin-arm64'); + case 'x64': return require('canvas-darwin-x64'); + } + break; + case 'linux': + switch (process.arch) { + case 'arm': return require('canvas-linux-arm-gnueabihf'); + case 'ia32': + switch (getLibc()) { + case 'musl': return require('canvas-linux-x86-musl'); + default: return require('canvas-linux-x86-gnu'); + } + case 'arm64': + switch (getLibc()) { + case 'musl': return require('canvas-linux-arm64-musl'); + default: return require('canvas-linux-arm64-gnu'); + } + case 'riscv64': return require('canvas-linux-riscv64-gnu'); + case 'x64': + switch (getLibc()) { + case 'musl': return require('canvas-linux-x64-musl'); + default: return require('canvas-linux-x64-gnu'); + } + } + break; + case 'win32': + switch (process.arch) { + case 'arm64': return require('canvas-win32-arm64'); + case 'x64': return require('canvas-win32-x64'); + case 'ia32': return require('canvas-win32-x86'); + } + break; + } + + return require('../bin/canvas.node'); +} + +const bindings = requireAddon() module.exports = bindings diff --git a/lib/jpegstream.js b/lib/jpegstream.js index 701d2f870..8f98a9db7 100644 --- a/lib/jpegstream.js +++ b/lib/jpegstream.js @@ -13,10 +13,6 @@ class JPEGStream extends Readable { constructor (canvas, options) { super() - if (canvas.streamJPEGSync === undefined) { - throw new Error('node-canvas was built without JPEG support.') - } - this.options = options this.canvas = canvas } diff --git a/npm/darwin-arm64/package.json b/npm/darwin-arm64/package.json new file mode 100644 index 000000000..8b5ed3c5a --- /dev/null +++ b/npm/darwin-arm64/package.json @@ -0,0 +1,15 @@ +{ + "name": "canvas-darwin-arm64", + "version": "3.2.3", + "os": [ + "darwin" + ], + "cpu": [ + "arm64" + ], + "main": "canvas.node", + "files": [ + "canvas.node" + ], + "description": "Canvas graphics API backed by Cairo" +} diff --git a/npm/darwin-x64/package.json b/npm/darwin-x64/package.json new file mode 100644 index 000000000..31f144867 --- /dev/null +++ b/npm/darwin-x64/package.json @@ -0,0 +1,15 @@ +{ + "name": "canvas-darwin-x64", + "version": "3.2.3", + "os": [ + "darwin" + ], + "cpu": [ + "x64" + ], + "main": "canvas.node", + "files": [ + "canvas.node" + ], + "description": "Canvas graphics API backed by Cairo" +} diff --git a/npm/linux-arm-gnueabihf/package.json b/npm/linux-arm-gnueabihf/package.json new file mode 100644 index 000000000..526525519 --- /dev/null +++ b/npm/linux-arm-gnueabihf/package.json @@ -0,0 +1,15 @@ +{ + "name": "canvas-linux-arm-gnueabihf", + "version": "3.2.3", + "os": [ + "linux" + ], + "cpu": [ + "arm" + ], + "main": "canvas.node", + "files": [ + "canvas.node" + ], + "description": "Canvas graphics API backed by Cairo" +} diff --git a/npm/linux-arm64-gnu/package.json b/npm/linux-arm64-gnu/package.json new file mode 100644 index 000000000..514bfdd4b --- /dev/null +++ b/npm/linux-arm64-gnu/package.json @@ -0,0 +1,15 @@ +{ + "name": "canvas-linux-arm64-gnu", + "version": "3.2.3", + "os": [ + "linux" + ], + "cpu": [ + "arm64" + ], + "main": "canvas.node", + "files": [ + "canvas.node" + ], + "description": "Canvas graphics API backed by Cairo" +} diff --git a/npm/linux-arm64-musl/package.json b/npm/linux-arm64-musl/package.json new file mode 100644 index 000000000..2e8279bf9 --- /dev/null +++ b/npm/linux-arm64-musl/package.json @@ -0,0 +1,15 @@ +{ + "name": "canvas-linux-arm64-musl", + "version": "3.2.3", + "os": [ + "linux" + ], + "cpu": [ + "arm64" + ], + "main": "canvas.node", + "files": [ + "canvas.node" + ], + "description": "Canvas graphics API backed by Cairo" +} diff --git a/npm/linux-riscv64-gnu/package.json b/npm/linux-riscv64-gnu/package.json new file mode 100644 index 000000000..797035b15 --- /dev/null +++ b/npm/linux-riscv64-gnu/package.json @@ -0,0 +1,15 @@ +{ + "name": "canvas-linux-riscv64-gnu", + "version": "3.2.3", + "os": [ + "linux" + ], + "cpu": [ + "riscv64" + ], + "main": "canvas.node", + "files": [ + "canvas.node" + ], + "description": "Canvas graphics API backed by Cairo" +} diff --git a/npm/linux-x64-gnu/package.json b/npm/linux-x64-gnu/package.json new file mode 100644 index 000000000..50a370b52 --- /dev/null +++ b/npm/linux-x64-gnu/package.json @@ -0,0 +1,15 @@ +{ + "name": "canvas-linux-x64-gnu", + "version": "3.2.3", + "os": [ + "linux" + ], + "cpu": [ + "x64" + ], + "main": "canvas.node", + "files": [ + "canvas.node" + ], + "description": "Canvas graphics API backed by Cairo" +} diff --git a/npm/linux-x64-musl/package.json b/npm/linux-x64-musl/package.json new file mode 100644 index 000000000..b07b165d7 --- /dev/null +++ b/npm/linux-x64-musl/package.json @@ -0,0 +1,15 @@ +{ + "name": "canvas-linux-x64-musl", + "version": "3.2.3", + "os": [ + "linux" + ], + "cpu": [ + "x64" + ], + "main": "canvas.node", + "files": [ + "canvas.node" + ], + "description": "Canvas graphics API backed by Cairo" +} diff --git a/npm/linux-x86-gnu/package.json b/npm/linux-x86-gnu/package.json new file mode 100644 index 000000000..ed9d31715 --- /dev/null +++ b/npm/linux-x86-gnu/package.json @@ -0,0 +1,15 @@ +{ + "name": "canvas-linux-x86-gnu", + "version": "3.2.3", + "os": [ + "linux" + ], + "cpu": [ + "ia32" + ], + "main": "canvas.node", + "files": [ + "canvas.node" + ], + "description": "Canvas graphics API backed by Cairo" +} diff --git a/npm/linux-x86-musl/package.json b/npm/linux-x86-musl/package.json new file mode 100644 index 000000000..8b5ba701b --- /dev/null +++ b/npm/linux-x86-musl/package.json @@ -0,0 +1,15 @@ +{ + "name": "canvas-linux-x86-musl", + "version": "3.2.3", + "os": [ + "linux" + ], + "cpu": [ + "ia32" + ], + "main": "canvas.node", + "files": [ + "canvas.node" + ], + "description": "Canvas graphics API backed by Cairo" +} diff --git a/npm/win32-arm64/package.json b/npm/win32-arm64/package.json new file mode 100644 index 000000000..de991a328 --- /dev/null +++ b/npm/win32-arm64/package.json @@ -0,0 +1,15 @@ +{ + "name": "canvas-win32-arm64", + "version": "3.2.3", + "os": [ + "win32" + ], + "cpu": [ + "arm64" + ], + "main": "canvas.node", + "files": [ + "canvas.node" + ], + "description": "Canvas graphics API backed by Cairo" +} diff --git a/npm/win32-x64/package.json b/npm/win32-x64/package.json new file mode 100644 index 000000000..ea7e8978f --- /dev/null +++ b/npm/win32-x64/package.json @@ -0,0 +1,15 @@ +{ + "name": "canvas-win32-x64", + "version": "3.2.3", + "os": [ + "win32" + ], + "cpu": [ + "x64" + ], + "main": "canvas.node", + "files": [ + "canvas.node" + ], + "description": "Canvas graphics API backed by Cairo" +} diff --git a/npm/win32-x86/package.json b/npm/win32-x86/package.json new file mode 100644 index 000000000..9344d8731 --- /dev/null +++ b/npm/win32-x86/package.json @@ -0,0 +1,15 @@ +{ + "name": "canvas-win32-x86", + "version": "3.2.3", + "os": [ + "win32" + ], + "cpu": [ + "ia32" + ], + "main": "canvas.node", + "files": [ + "canvas.node" + ], + "description": "Canvas graphics API backed by Cairo" +} diff --git a/package-lock.json b/package-lock.json index bb1d0be29..367c65764 100644 --- a/package-lock.json +++ b/package-lock.json @@ -7,12 +7,10 @@ "": { "name": "canvas", "version": "3.2.3", - "hasInstallScript": true, "license": "MIT", - "dependencies": { - "node-addon-api": "^8.9.0", - "prebuild-install": "^7.1.3" - }, + "workspaces": [ + "./npm/*" + ], "devDependencies": { "@types/node": "^10.12.18", "assert-rejects": "^1.0.0", @@ -26,16 +24,28 @@ }, "engines": { "node": "^18.12.0 || >= 20.9.0" + }, + "optionalDependencies": { + "canvas-darwin-arm64": "3.2.3", + "canvas-darwin-x64": "3.2.3", + "canvas-linux-arm-gnueabihf": "3.2.3", + "canvas-linux-arm64-gnu": "3.2.3", + "canvas-linux-arm64-musl": "3.2.3", + "canvas-linux-riscv64-gnu": "3.2.3", + "canvas-linux-x64-gnu": "3.2.3", + "canvas-linux-x64-musl": "3.2.3", + "canvas-win32-arm64": "3.2.3", + "canvas-win32-x64": "3.2.3" } }, "node_modules/@babel/code-frame": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.27.1.tgz", - "integrity": "sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.7.tgz", + "integrity": "sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-validator-identifier": "^7.27.1", + "@babel/helper-validator-identifier": "^7.29.7", "js-tokens": "^4.0.0", "picocolors": "^1.1.1" }, @@ -51,9 +61,9 @@ "license": "MIT" }, "node_modules/@babel/helper-validator-identifier": { - "version": "7.28.5", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz", - "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.29.7.tgz", + "integrity": "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg==", "dev": true, "license": "MIT", "engines": { @@ -78,91 +88,6 @@ "node": ">=12" } }, - "node_modules/@isaacs/cliui/node_modules/ansi-regex": { - "version": "6.2.2", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", - "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/ansi-regex?sponsor=1" - } - }, - "node_modules/@isaacs/cliui/node_modules/ansi-styles": { - "version": "6.2.3", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", - "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/@isaacs/cliui/node_modules/emoji-regex": { - "version": "9.2.2", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", - "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", - "dev": true, - "license": "MIT" - }, - "node_modules/@isaacs/cliui/node_modules/string-width": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", - "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", - "dev": true, - "license": "MIT", - "dependencies": { - "eastasianwidth": "^0.2.0", - "emoji-regex": "^9.2.2", - "strip-ansi": "^7.0.1" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/@isaacs/cliui/node_modules/strip-ansi": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.2.0.tgz", - "integrity": "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-regex": "^6.2.2" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/strip-ansi?sponsor=1" - } - }, - "node_modules/@isaacs/cliui/node_modules/wrap-ansi": { - "version": "8.1.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", - "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-styles": "^6.1.0", - "string-width": "^5.0.1", - "strip-ansi": "^7.0.1" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/wrap-ansi?sponsor=1" - } - }, "node_modules/@jest/schemas": { "version": "29.6.3", "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-29.6.3.tgz", @@ -226,9 +151,9 @@ } }, "node_modules/@sinclair/typebox": { - "version": "0.27.8", - "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.27.8.tgz", - "integrity": "sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA==", + "version": "0.27.10", + "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.27.10.tgz", + "integrity": "sha512-MTBk/3jGLNB2tVxv6uLlFh1iu64iYOQ2PbdOSK3NW8JZsmlaOh2q6sdtKowBhfw8QFLmYNzTW4/oK4uATIi6ZA==", "dev": true, "license": "MIT" }, @@ -254,9 +179,9 @@ } }, "node_modules/@types/estree": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", - "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.9.tgz", + "integrity": "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==", "dev": true, "license": "MIT" }, @@ -326,9 +251,9 @@ } }, "node_modules/ajv": { - "version": "6.12.6", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", - "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "version": "6.15.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.15.0.tgz", + "integrity": "sha512-fgFx7Hfoq60ytK2c7DhnF8jIvzYgOMxfugjLOSMHjLIPgenqa7S7oaagATUq99mV6IYvN2tRmC0wnTYX6iPbMw==", "dev": true, "license": "MIT", "dependencies": { @@ -363,26 +288,32 @@ } }, "node_modules/ansi-regex": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-3.0.1.tgz", - "integrity": "sha512-+O9Jct8wf++lXxxFc4hc8LsjaSq0HFzzL7cVsw8pRDIPdjKD2mT4ytDZlLuSBZ4cLKZFXIrMGO7DbQCtMJJMKw==", + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", + "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", "dev": true, "license": "MIT", "engines": { - "node": ">=4" + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" } }, "node_modules/ansi-styles": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", - "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", "dev": true, "license": "MIT", "dependencies": { - "color-convert": "^1.9.0" + "color-convert": "^2.0.1" }, "engines": { - "node": ">=4" + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, "node_modules/argparse": { @@ -449,6 +380,25 @@ "node": ">=8" } }, + "node_modules/array.prototype.flatmap": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/array.prototype.flatmap/-/array.prototype.flatmap-1.3.3.tgz", + "integrity": "sha512-Y7Wt51eKJSyi80hFrJCePGGNo5ktJCslFuboqJsbf57CCPcm5zztluPlc4/aD8sWsKvlwatezpV4U1efk8kpjg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.5", + "es-shim-unscopables": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/arraybuffer.prototype.slice": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/arraybuffer.prototype.slice/-/arraybuffer.prototype.slice-1.0.4.tgz", @@ -569,6 +519,16 @@ "node": ">=0.10.0" } }, + "node_modules/babel-code-frame/node_modules/escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8.0" + } + }, "node_modules/babel-code-frame/node_modules/strip-ansi": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", @@ -599,37 +559,6 @@ "dev": true, "license": "MIT" }, - "node_modules/base64-js": { - "version": "1.5.1", - "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", - "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "MIT" - }, - "node_modules/bl": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz", - "integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==", - "license": "MIT", - "dependencies": { - "buffer": "^5.5.0", - "inherits": "^2.0.4", - "readable-stream": "^3.4.0" - } - }, "node_modules/body-parser": { "version": "1.20.5", "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.5.tgz", @@ -655,46 +584,14 @@ "npm": "1.2.8000 || >= 1.4.16" } }, - "node_modules/body-parser/node_modules/http-errors": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz", - "integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "depd": "~2.0.0", - "inherits": "~2.0.4", - "setprototypeof": "~1.2.0", - "statuses": "~2.0.2", - "toidentifier": "~1.0.1" - }, - "engines": { - "node": ">= 0.8" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } - }, - "node_modules/body-parser/node_modules/statuses": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz", - "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, "node_modules/brace-expansion": { - "version": "1.1.12", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", - "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.1.tgz", + "integrity": "sha512-WR1cURNjuvBLMZBMbqM0UoE+WAfdUcEV1ccD8PVBVOI+Z3ND4+SZbN8RsfT2bMuG1qwz5RFvPukSZm5fF2D5eA==", "dev": true, "license": "MIT", "dependencies": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" + "balanced-match": "^1.0.0" } }, "node_modules/braces": { @@ -717,30 +614,6 @@ "dev": true, "license": "ISC" }, - "node_modules/buffer": { - "version": "5.7.1", - "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", - "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "MIT", - "dependencies": { - "base64-js": "^1.3.1", - "ieee754": "^1.1.13" - } - }, "node_modules/bytes": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", @@ -752,15 +625,15 @@ } }, "node_modules/call-bind": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.8.tgz", - "integrity": "sha512-oKlSFMcMwpUg2ednkhQ454wfWiU/ul3CkJe/PEHcTKuiX6RpbehUiFMXu13HalGZxfUwCQzZG747YXBn1im9ww==", + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.9.tgz", + "integrity": "sha512-a/hy+pNsFUTR+Iz8TCJvXudKVLAnz/DyeSUo10I5yvFDQJBFU2s9uqQpoSrJlroHUKoKqzg+epxyP9lqFdzfBQ==", "dev": true, "license": "MIT", "dependencies": { - "call-bind-apply-helpers": "^1.0.0", - "es-define-property": "^1.0.0", - "get-intrinsic": "^1.2.4", + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "get-intrinsic": "^1.3.0", "set-function-length": "^1.2.2" }, "engines": { @@ -852,19 +725,86 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/canvas-darwin-arm64": { + "resolved": "npm/darwin-arm64", + "link": true + }, + "node_modules/canvas-darwin-x64": { + "resolved": "npm/darwin-x64", + "link": true + }, + "node_modules/canvas-linux-arm-gnueabihf": { + "resolved": "npm/linux-arm-gnueabihf", + "link": true + }, + "node_modules/canvas-linux-arm64-gnu": { + "resolved": "npm/linux-arm64-gnu", + "link": true + }, + "node_modules/canvas-linux-arm64-musl": { + "resolved": "npm/linux-arm64-musl", + "link": true + }, + "node_modules/canvas-linux-riscv64-gnu": { + "resolved": "npm/linux-riscv64-gnu", + "link": true + }, + "node_modules/canvas-linux-x64-gnu": { + "resolved": "npm/linux-x64-gnu", + "link": true + }, + "node_modules/canvas-linux-x64-musl": { + "resolved": "npm/linux-x64-musl", + "link": true + }, + "node_modules/canvas-linux-x86-gnu": { + "resolved": "npm/linux-x86-gnu", + "link": true + }, + "node_modules/canvas-linux-x86-musl": { + "resolved": "npm/linux-x86-musl", + "link": true + }, + "node_modules/canvas-win32-arm64": { + "resolved": "npm/win32-arm64", + "link": true + }, + "node_modules/canvas-win32-x64": { + "resolved": "npm/win32-x64", + "link": true + }, + "node_modules/canvas-win32-x86": { + "resolved": "npm/win32-x86", + "link": true + }, "node_modules/chalk": { - "version": "2.4.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", - "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", "dev": true, "license": "MIT", "dependencies": { - "ansi-styles": "^3.2.1", - "escape-string-regexp": "^1.0.5", - "supports-color": "^5.3.0" + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" }, "engines": { - "node": ">=4" + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/chalk/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" } }, "node_modules/chardet": { @@ -890,12 +830,6 @@ "url": "https://paulmillr.com/funding/" } }, - "node_modules/chownr": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz", - "integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==", - "license": "ISC" - }, "node_modules/circular-json": { "version": "0.3.3", "resolved": "https://registry.npmjs.org/circular-json/-/circular-json-0.3.3.tgz", @@ -949,15 +883,12 @@ "node": ">=8" } }, - "node_modules/cliui/node_modules/is-fullwidth-code-point": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", - "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "node_modules/cliui/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } + "license": "MIT" }, "node_modules/cliui/node_modules/string-width": { "version": "4.2.3", @@ -987,20 +918,41 @@ "node": ">=8" } }, + "node_modules/cliui/node_modules/wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, "node_modules/color-convert": { - "version": "1.9.3", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", - "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", "dev": true, "license": "MIT", "dependencies": { - "color-name": "1.1.3" + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" } }, "node_modules/color-name": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", - "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==", + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", "dev": true, "license": "MIT" }, @@ -1045,9 +997,9 @@ } }, "node_modules/cookie": { - "version": "0.7.1", - "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.1.tgz", - "integrity": "sha512-6DnInpx7SJ2AK3+CTUE/ZM0vWTUboZCegxhC2xiIydHR9jNuTAASBrfEpHhiGOZw/nX51bHt6YQl8jsGo4y/0w==", + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", + "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", "dev": true, "license": "MIT", "engines": { @@ -1055,37 +1007,25 @@ } }, "node_modules/cookie-signature": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", - "integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==", + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.7.tgz", + "integrity": "sha512-NXdYc3dLr47pBkpUCHtKSwIOQXLVn8dZEuywboCOJY/osA0wFSLlSawr3KN8qXJEyX66FcONTH8EIlVuK0yyFA==", "dev": true, "license": "MIT" }, "node_modules/cross-spawn": { - "version": "6.0.6", - "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-6.0.6.tgz", - "integrity": "sha512-VqCUuhcd1iB+dsv8gxPttb5iZh/D0iubSP21g36KXdEuf6I5JiioesUVjpCdHV9MZRUfVFlvwtIUyPfxo5trtw==", + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", "dev": true, "license": "MIT", "dependencies": { - "nice-try": "^1.0.4", - "path-key": "^2.0.1", - "semver": "^5.5.0", - "shebang-command": "^1.2.0", - "which": "^1.2.9" + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" }, "engines": { - "node": ">=4.8" - } - }, - "node_modules/cross-spawn/node_modules/semver": { - "version": "5.7.2", - "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz", - "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==", - "dev": true, - "license": "ISC", - "bin": { - "semver": "bin/semver" + "node": ">= 8" } }, "node_modules/data-view-buffer": { @@ -1199,30 +1139,6 @@ "node": ">=0.10.0" } }, - "node_modules/decompress-response": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz", - "integrity": "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==", - "license": "MIT", - "dependencies": { - "mimic-response": "^3.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/deep-extend": { - "version": "0.6.0", - "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz", - "integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==", - "license": "MIT", - "engines": { - "node": ">=4.0.0" - } - }, "node_modules/deep-is": { "version": "0.1.4", "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", @@ -1281,6 +1197,39 @@ "uniq": "^1.0.1" } }, + "node_modules/deglob/node_modules/brace-expansion": { + "version": "1.1.15", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.15.tgz", + "integrity": "sha512-EwOCDEex4quD37XhqM3omwtMoJjr//isUZz1JopUNWms+4Z2ViyM/k1YIRePpoVNnQhENnxtFjLaxNHrT7xIUg==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/deglob/node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", + "dev": true, + "license": "ISC", + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/deglob/node_modules/ignore": { "version": "3.3.10", "resolved": "https://registry.npmjs.org/ignore/-/ignore-3.3.10.tgz", @@ -1288,6 +1237,19 @@ "dev": true, "license": "MIT" }, + "node_modules/deglob/node_modules/minimatch": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, "node_modules/depd": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", @@ -1309,15 +1271,6 @@ "npm": "1.2.8000 || >= 1.4.16" } }, - "node_modules/detect-libc": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.4.tgz", - "integrity": "sha512-3UDv+G9CsCKO1WKMGw9fwq/SWJYbI0c5Y7LU1AXYoDdbhE2AHQ6N6Nb34sG8Fj7T5APy8qXDCKuuIHd1BR0tVA==", - "license": "Apache-2.0", - "engines": { - "node": ">=8" - } - }, "node_modules/diff": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/diff/-/diff-7.0.0.tgz", @@ -1404,9 +1357,9 @@ "license": "MIT" }, "node_modules/emoji-regex": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", - "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "version": "9.2.2", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", + "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", "dev": true, "license": "MIT" }, @@ -1420,19 +1373,10 @@ "node": ">= 0.8" } }, - "node_modules/end-of-stream": { - "version": "1.4.5", - "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.5.tgz", - "integrity": "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==", - "license": "MIT", - "dependencies": { - "once": "^1.4.0" - } - }, "node_modules/error-ex": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz", - "integrity": "sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==", + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.4.tgz", + "integrity": "sha512-sqQamAnR14VgCr1A618A3sGrygcpK+HEbenA/HiEAkkUwcZIIB/tgWqHFxWgOyDh4nB4JCRimh79dR5Ywc9MDQ==", "dev": true, "license": "MIT", "dependencies": { @@ -1440,9 +1384,9 @@ } }, "node_modules/es-abstract": { - "version": "1.24.0", - "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.24.0.tgz", - "integrity": "sha512-WSzPgsdLtTcQwm4CROfS5ju2Wa1QQcVeT37jFjYzdFz1r9ahadC8B8/a4qxJxM+09F18iumCdRmlr96ZYkQvEg==", + "version": "1.24.2", + "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.24.2.tgz", + "integrity": "sha512-2FpH9Q5i2RRwyEP1AylXe6nYLR5OhaJTZwmlcP0dL/+JCbgg7yyEo/sEK6HeGZRf3dFpWwThaRHVApXSkW3xeg==", "dev": true, "license": "MIT", "dependencies": { @@ -1508,6 +1452,25 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/es-abstract-get": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/es-abstract-get/-/es-abstract-get-1.0.0.tgz", + "integrity": "sha512-6PMWXpdhshVvFp+FoWYs1EvG1Nj0tvk0dZM+XcK0xMEM1czRVcP6ohqPWHy6qPagSpC8j4+p89WXlT+xXJs/fg==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.2", + "is-callable": "^1.2.7", + "object-inspect": "^1.13.4" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/es-define-property": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", @@ -1529,9 +1492,9 @@ } }, "node_modules/es-object-atoms": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", - "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.2.tgz", + "integrity": "sha512-HWcBoN6NileqtSydK2FqHbS/LoDd2pqrnQHLyJzBj4kOp/ky2MWMN694xOfkK8/SnUsW2DH7EfyVlydKCsm1Zw==", "dev": true, "license": "MIT", "dependencies": { @@ -1557,16 +1520,32 @@ "node": ">= 0.4" } }, + "node_modules/es-shim-unscopables": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/es-shim-unscopables/-/es-shim-unscopables-1.1.0.tgz", + "integrity": "sha512-d9T8ucsEhh8Bi1woXCf+TIKDIROLG5WCkxg8geBCbvk22kzwC5G2OnXVMO6FUsvQlgUUXQ2itephWDLqDzbeCw==", + "dev": true, + "license": "MIT", + "dependencies": { + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/es-to-primitive": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.3.0.tgz", - "integrity": "sha512-w+5mJ3GuFL+NjVtJlvydShqE1eN3h3PbI7/5LAsYJP/2qtuMXjfL2LpHSRqo4b4eSF5K/DH1JXKUAHSB2UW50g==", + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.3.4.tgz", + "integrity": "sha512-yPDz7wqpg1/mmHLmS3tcfTfbw5f1eryXvyghYBffGdERwe+mV7ZcWzTR8LR17Kvqt3qfPurjlonmnq3MKXIOXw==", "dev": true, "license": "MIT", "dependencies": { + "es-abstract-get": "^1.0.0", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", "is-callable": "^1.2.7", - "is-date-object": "^1.0.5", - "is-symbol": "^1.0.4" + "is-date-object": "^1.1.0", + "is-symbol": "^1.1.1" }, "engines": { "node": ">= 0.4" @@ -1593,13 +1572,16 @@ "license": "MIT" }, "node_modules/escape-string-regexp": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", - "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", "dev": true, "license": "MIT", "engines": { - "node": ">=0.8.0" + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, "node_modules/eslint": { @@ -1730,166 +1712,111 @@ "node": ">=8" } }, - "node_modules/eslint-formatter-pretty/node_modules/ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "node_modules/eslint-formatter-pretty/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true, + "license": "MIT" + }, + "node_modules/eslint-formatter-pretty/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", "dev": true, "license": "MIT", "dependencies": { - "color-convert": "^2.0.1" + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" }, "engines": { "node": ">=8" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, - "node_modules/eslint-formatter-pretty/node_modules/chalk": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "node_modules/eslint-formatter-pretty/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", "dev": true, "license": "MIT", "dependencies": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" + "ansi-regex": "^5.0.1" }, + "engines": { + "node": ">=8" + } + }, + "node_modules/eslint-formatter-pretty/node_modules/type-fest": { + "version": "0.21.3", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.21.3.tgz", + "integrity": "sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==", + "dev": true, + "license": "(MIT OR CC0-1.0)", "engines": { "node": ">=10" }, "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/eslint-formatter-pretty/node_modules/color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "node_modules/eslint-import-resolver-node": { + "version": "0.3.10", + "resolved": "https://registry.npmjs.org/eslint-import-resolver-node/-/eslint-import-resolver-node-0.3.10.tgz", + "integrity": "sha512-tRrKqFyCaKict5hOd244sL6EQFNycnMQnBe+j8uqGNXYzsImGbGUU4ibtoaBmv5FLwJwcFJNeg1GeVjQfbMrDQ==", "dev": true, "license": "MIT", "dependencies": { - "color-name": "~1.1.4" - }, - "engines": { - "node": ">=7.0.0" + "debug": "^3.2.7", + "is-core-module": "^2.16.1", + "resolve": "^2.0.0-next.6" } }, - "node_modules/eslint-formatter-pretty/node_modules/color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true, - "license": "MIT" - }, - "node_modules/eslint-formatter-pretty/node_modules/has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/eslint-formatter-pretty/node_modules/is-fullwidth-code-point": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", - "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/eslint-formatter-pretty/node_modules/string-width": { - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", - "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "node_modules/eslint-import-resolver-node/node_modules/debug": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", + "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", "dev": true, "license": "MIT", "dependencies": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.1" - }, - "engines": { - "node": ">=8" + "ms": "^2.1.1" } }, - "node_modules/eslint-formatter-pretty/node_modules/strip-ansi": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", - "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "node_modules/eslint-import-resolver-node/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", "dev": true, - "license": "MIT", - "dependencies": { - "ansi-regex": "^5.0.1" - }, - "engines": { - "node": ">=8" - } + "license": "MIT" }, - "node_modules/eslint-formatter-pretty/node_modules/supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "node_modules/eslint-import-resolver-node/node_modules/resolve": { + "version": "2.0.0-next.7", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-2.0.0-next.7.tgz", + "integrity": "sha512-tqt+NBWwyaMgw3zDsnygx4CByWjQEJHOPMdslYhppaQSJUtL/D4JO9CcBBlhPoI8lz9oJIDXkwXfhF4aWqP8xQ==", "dev": true, "license": "MIT", "dependencies": { - "has-flag": "^4.0.0" + "es-errors": "^1.3.0", + "is-core-module": "^2.16.2", + "node-exports-info": "^1.6.0", + "object-keys": "^1.1.1", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" }, "engines": { - "node": ">=8" - } - }, - "node_modules/eslint-formatter-pretty/node_modules/type-fest": { - "version": "0.21.3", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.21.3.tgz", - "integrity": "sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==", - "dev": true, - "license": "(MIT OR CC0-1.0)", - "engines": { - "node": ">=10" + "node": ">= 0.4" }, "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/eslint-import-resolver-node": { - "version": "0.3.9", - "resolved": "https://registry.npmjs.org/eslint-import-resolver-node/-/eslint-import-resolver-node-0.3.9.tgz", - "integrity": "sha512-WFj2isz22JahUv+B788TlO3N6zL3nNJGU8CcZbPZvVEkBPaJdCV4vy5wyghty5ROFbCRnm132v8BScu5/1BQ8g==", - "dev": true, - "license": "MIT", - "dependencies": { - "debug": "^3.2.7", - "is-core-module": "^2.13.0", - "resolve": "^1.22.4" - } - }, - "node_modules/eslint-import-resolver-node/node_modules/debug": { - "version": "3.2.7", - "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", - "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "ms": "^2.1.1" + "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/eslint-import-resolver-node/node_modules/ms": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", - "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", - "dev": true, - "license": "MIT" - }, "node_modules/eslint-module-utils": { - "version": "2.12.1", - "resolved": "https://registry.npmjs.org/eslint-module-utils/-/eslint-module-utils-2.12.1.tgz", - "integrity": "sha512-L8jSWTze7K2mTg0vos/RuLRS5soomksDPoJLXIslC7c8Wmut3bx7CPpJijDcBZtxQ5lrbUdM+s0OlNbz0DCDNw==", + "version": "2.14.0", + "resolved": "https://registry.npmjs.org/eslint-module-utils/-/eslint-module-utils-2.14.0.tgz", + "integrity": "sha512-W2WCRZ9Dqntd+2u8jJcVMV2PKulc6RdLgUUoh/yQr3uB6lo/ZOeGx11sv60/8S4QFFKNslAlWhr9u0Ef7ZW6Ig==", "dev": true, "license": "MIT", "dependencies": { @@ -1963,6 +1890,17 @@ "eslint": "2.x - 5.x" } }, + "node_modules/eslint-plugin-import/node_modules/brace-expansion": { + "version": "1.1.15", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.15.tgz", + "integrity": "sha512-EwOCDEex4quD37XhqM3omwtMoJjr//isUZz1JopUNWms+4Z2ViyM/k1YIRePpoVNnQhENnxtFjLaxNHrT7xIUg==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, "node_modules/eslint-plugin-import/node_modules/doctrine": { "version": "1.5.0", "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-1.5.0.tgz", @@ -1983,6 +1921,19 @@ "dev": true, "license": "MIT" }, + "node_modules/eslint-plugin-import/node_modules/minimatch": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, "node_modules/eslint-plugin-node": { "version": "7.0.1", "resolved": "https://registry.npmjs.org/eslint-plugin-node/-/eslint-plugin-node-7.0.1.tgz", @@ -2004,14 +1955,28 @@ "eslint": ">=4.19.1" } }, - "node_modules/eslint-plugin-node/node_modules/semver": { - "version": "5.7.2", - "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz", - "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==", + "node_modules/eslint-plugin-node/node_modules/brace-expansion": { + "version": "1.1.15", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.15.tgz", + "integrity": "sha512-EwOCDEex4quD37XhqM3omwtMoJjr//isUZz1JopUNWms+4Z2ViyM/k1YIRePpoVNnQhENnxtFjLaxNHrT7xIUg==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/eslint-plugin-node/node_modules/minimatch": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", "dev": true, "license": "ISC", - "bin": { - "semver": "bin/semver" + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" } }, "node_modules/eslint-plugin-promise": { @@ -2112,6 +2077,29 @@ "node": ">=4" } }, + "node_modules/eslint/node_modules/ansi-regex": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-3.0.1.tgz", + "integrity": "sha512-+O9Jct8wf++lXxxFc4hc8LsjaSq0HFzzL7cVsw8pRDIPdjKD2mT4ytDZlLuSBZ4cLKZFXIrMGO7DbQCtMJJMKw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/eslint/node_modules/ansi-styles": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", + "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^1.9.0" + }, + "engines": { + "node": ">=4" + } + }, "node_modules/eslint/node_modules/argparse": { "version": "1.0.10", "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", @@ -2122,6 +2110,66 @@ "sprintf-js": "~1.0.2" } }, + "node_modules/eslint/node_modules/brace-expansion": { + "version": "1.1.15", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.15.tgz", + "integrity": "sha512-EwOCDEex4quD37XhqM3omwtMoJjr//isUZz1JopUNWms+4Z2ViyM/k1YIRePpoVNnQhENnxtFjLaxNHrT7xIUg==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/eslint/node_modules/chalk": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/eslint/node_modules/color-convert": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", + "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "1.1.3" + } + }, + "node_modules/eslint/node_modules/color-name": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", + "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==", + "dev": true, + "license": "MIT" + }, + "node_modules/eslint/node_modules/cross-spawn": { + "version": "6.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-6.0.6.tgz", + "integrity": "sha512-VqCUuhcd1iB+dsv8gxPttb5iZh/D0iubSP21g36KXdEuf6I5JiioesUVjpCdHV9MZRUfVFlvwtIUyPfxo5trtw==", + "dev": true, + "license": "MIT", + "dependencies": { + "nice-try": "^1.0.4", + "path-key": "^2.0.1", + "semver": "^5.5.0", + "shebang-command": "^1.2.0", + "which": "^1.2.9" + }, + "engines": { + "node": ">=4.8" + } + }, "node_modules/eslint/node_modules/debug": { "version": "3.2.7", "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", @@ -2132,10 +2180,52 @@ "ms": "^2.1.1" } }, + "node_modules/eslint/node_modules/escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/eslint/node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", + "dev": true, + "license": "ISC", + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/eslint/node_modules/has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, "node_modules/eslint/node_modules/js-yaml": { - "version": "3.14.2", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.2.tgz", - "integrity": "sha512-PMSmkqxr106Xa156c2M265Z+FTrPl+oxd/rgOQy2tijQeK5TxQ43psO1ZCwhVOSdnn+RzkzlRz/eY4BgJBYVpg==", + "version": "3.15.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.15.0.tgz", + "integrity": "sha512-ttBQIIQPDeLjpPOohtUdXuXUVoA2uIB6fEH9HyJ7234s5mBJ5wTx20njxplLZQgLaOfpmPQA7X2t5AX6tIPbog==", "dev": true, "license": "MIT", "dependencies": { @@ -2146,6 +2236,19 @@ "js-yaml": "bin/js-yaml.js" } }, + "node_modules/eslint/node_modules/minimatch": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, "node_modules/eslint/node_modules/ms": { "version": "2.1.3", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", @@ -2153,14 +2256,86 @@ "dev": true, "license": "MIT" }, - "node_modules/eslint/node_modules/semver": { - "version": "5.7.2", - "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz", - "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==", + "node_modules/eslint/node_modules/path-key": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-2.0.1.tgz", + "integrity": "sha512-fEHGKCSmUSDPv4uoj8AlD+joPlq3peND+HRYyxFz4KPw4z926S/b8rIuFs2FYJg3BwsxJf6A9/3eIdLaYC+9Dw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/eslint/node_modules/shebang-command": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-1.2.0.tgz", + "integrity": "sha512-EV3L1+UQWGor21OmnvojK36mhg+TyIKDh3iFBKBohr5xeXIhNBcx8oWdgkTEEQ+BEFFYdLRuqMfd5L84N1V5Vg==", + "dev": true, + "license": "MIT", + "dependencies": { + "shebang-regex": "^1.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/eslint/node_modules/shebang-regex": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-1.0.0.tgz", + "integrity": "sha512-wpoSFAxys6b2a2wHZ1XpDSgD7N9iVjg29Ph9uV/uaP9Ex/KXlkTZTeddxDPSYQpgvzKLGJke2UU0AzoGCjNIvQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/eslint/node_modules/strip-ansi": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-4.0.0.tgz", + "integrity": "sha512-4XaJ2zQdCzROZDivEVIDPkcQn8LMFSa8kj8Gxb/Lnwzv9A8VctNZ+lfivC/sV3ivW8ElJTERXZoPBRrZKkNKow==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/eslint/node_modules/strip-json-comments": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", + "integrity": "sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/eslint/node_modules/supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/eslint/node_modules/which": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz", + "integrity": "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==", "dev": true, "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, "bin": { - "semver": "bin/semver" + "which": "bin/which" } }, "node_modules/espree": { @@ -2193,9 +2368,9 @@ } }, "node_modules/esquery": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.6.0.tgz", - "integrity": "sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg==", + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.7.0.tgz", + "integrity": "sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g==", "dev": true, "license": "BSD-3-Clause", "dependencies": { @@ -2268,15 +2443,6 @@ "node": ">= 0.6" } }, - "node_modules/expand-template": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/expand-template/-/expand-template-2.0.3.tgz", - "integrity": "sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg==", - "license": "(MIT OR WTFPL)", - "engines": { - "node": ">=6" - } - }, "node_modules/express": { "version": "4.22.2", "resolved": "https://registry.npmjs.org/express/-/express-4.22.2.tgz", @@ -2400,6 +2566,16 @@ "node": ">=4" } }, + "node_modules/figures/node_modules/escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8.0" + } + }, "node_modules/file-entry-cache": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-2.0.0.tgz", @@ -2428,18 +2604,18 @@ } }, "node_modules/finalhandler": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.3.1.tgz", - "integrity": "sha512-6BN9trH7bp3qvnrRyzsBz+g3lZxTNZTbVO2EV1CS0WIcDbawYVdYvGflME/9QP0h0pYlCDBCTjYa9nZzMDpyxQ==", + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.3.2.tgz", + "integrity": "sha512-aA4RyPcd3badbdABGDuTXCMTtOneUCAYH/gxoYRTZlIJdF0YPWuGqiAsIrhNnnqdXGswYk6dGujem4w80UJFhg==", "dev": true, "license": "MIT", "dependencies": { "debug": "2.6.9", "encodeurl": "~2.0.0", "escape-html": "~1.0.3", - "on-finished": "2.4.1", + "on-finished": "~2.4.1", "parseurl": "~1.3.3", - "statuses": "2.0.1", + "statuses": "~2.0.2", "unpipe": "~1.0.0" }, "engines": { @@ -2454,16 +2630,20 @@ "license": "MIT" }, "node_modules/find-up": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-2.1.0.tgz", - "integrity": "sha512-NWzkk0jSJtTt08+FBFMvXoeZnOJD+jTtsRmBYbAIzJdX6l7dLgR7CTubCM5/eDdPUBvLCeVasP1brfVR/9/EZQ==", + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", + "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", "dev": true, "license": "MIT", "dependencies": { - "locate-path": "^2.0.0" + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" }, "engines": { - "node": ">=4" + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, "node_modules/flat": { @@ -2485,121 +2665,44 @@ "dependencies": { "circular-json": "^0.3.1", "graceful-fs": "^4.1.2", - "rimraf": "~2.6.2", - "write": "^0.2.1" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/for-each": { - "version": "0.3.5", - "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.5.tgz", - "integrity": "sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg==", - "dev": true, - "license": "MIT", - "dependencies": { - "is-callable": "^1.2.7" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/foreground-child": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz", - "integrity": "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==", - "dev": true, - "license": "ISC", - "dependencies": { - "cross-spawn": "^7.0.6", - "signal-exit": "^4.0.1" - }, - "engines": { - "node": ">=14" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/foreground-child/node_modules/cross-spawn": { - "version": "7.0.6", - "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", - "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", - "dev": true, - "license": "MIT", - "dependencies": { - "path-key": "^3.1.0", - "shebang-command": "^2.0.0", - "which": "^2.0.1" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/foreground-child/node_modules/path-key": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", - "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/foreground-child/node_modules/shebang-command": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", - "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", - "dev": true, - "license": "MIT", - "dependencies": { - "shebang-regex": "^3.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/foreground-child/node_modules/shebang-regex": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", - "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", - "dev": true, - "license": "MIT", + "rimraf": "~2.6.2", + "write": "^0.2.1" + }, "engines": { - "node": ">=8" + "node": ">=0.10.0" } }, - "node_modules/foreground-child/node_modules/signal-exit": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", - "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "node_modules/for-each": { + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.5.tgz", + "integrity": "sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg==", "dev": true, - "license": "ISC", + "license": "MIT", + "dependencies": { + "is-callable": "^1.2.7" + }, "engines": { - "node": ">=14" + "node": ">= 0.4" }, "funding": { - "url": "https://github.com/sponsors/isaacs" + "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/foreground-child/node_modules/which": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", - "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "node_modules/foreground-child": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz", + "integrity": "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==", "dev": true, "license": "ISC", "dependencies": { - "isexe": "^2.0.0" - }, - "bin": { - "node-which": "bin/node-which" + "cross-spawn": "^7.0.6", + "signal-exit": "^4.0.1" }, "engines": { - "node": ">= 8" + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" } }, "node_modules/forwarded": { @@ -2622,12 +2725,6 @@ "node": ">= 0.6" } }, - "node_modules/fs-constants": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz", - "integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==", - "license": "MIT" - }, "node_modules/fs.realpath": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", @@ -2646,18 +2743,21 @@ } }, "node_modules/function.prototype.name": { - "version": "1.1.8", - "resolved": "https://registry.npmjs.org/function.prototype.name/-/function.prototype.name-1.1.8.tgz", - "integrity": "sha512-e5iwyodOHhbMr/yNrc7fDYG4qlbIvI5gajyzPnb5TCwyhjApznQh1BMFou9b30SevY43gCJKXycoCBjMbsuW0Q==", + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/function.prototype.name/-/function.prototype.name-1.2.0.tgz", + "integrity": "sha512-jObKIik1P2QjPHP5nz5BaOtUlfgS0fWo8IUByNXkM+o+02sJOi94em77GwJKQSJ3gfPHdgzLNrHc1uokV4P/ew==", "dev": true, "license": "MIT", "dependencies": { - "call-bind": "^1.0.8", - "call-bound": "^1.0.3", - "define-properties": "^1.2.1", + "call-bind": "^1.0.9", + "call-bound": "^1.0.4", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", "functions-have-names": "^1.2.3", - "hasown": "^2.0.2", - "is-callable": "^1.2.7" + "has-property-descriptors": "^1.0.2", + "hasown": "^2.0.4", + "is-callable": "^1.2.7", + "is-document.all": "^1.0.0" }, "engines": { "node": ">= 0.4" @@ -2683,6 +2783,16 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/generator-function": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/generator-function/-/generator-function-2.0.1.tgz", + "integrity": "sha512-SFdFmIJi+ybC0vjlHN0ZGVGHc3lgE0DxPAT0djjVg+kjOnSqclqmj0KQ7ykTOLP6YxoqOvuAODGdcHJn+43q3g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, "node_modules/get-caller-file": { "version": "2.0.5", "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", @@ -2760,29 +2870,26 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/github-from-package": { - "version": "0.0.0", - "resolved": "https://registry.npmjs.org/github-from-package/-/github-from-package-0.0.0.tgz", - "integrity": "sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw==", - "license": "MIT" - }, "node_modules/glob": { - "version": "7.1.2", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.2.tgz", - "integrity": "sha512-MJTUg1kjuLeQCJ+ccE4Vpa6kKVXkPYJ2mOCQyUuKLcLQsdrMCpBPUi8qVE6+YuaJkozeA9NusTAw3hLr8Xe5EQ==", - "deprecated": "Glob versions prior to v9 are no longer supported", + "version": "10.5.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.5.0.tgz", + "integrity": "sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg==", + "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", "dev": true, "license": "ISC", "dependencies": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^3.0.4", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" + "foreground-child": "^3.1.0", + "jackspeak": "^3.1.2", + "minimatch": "^9.0.4", + "minipass": "^7.1.2", + "package-json-from-dist": "^1.0.0", + "path-scurry": "^1.11.1" }, - "engines": { - "node": "*" + "bin": { + "glob": "dist/esm/bin.mjs" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" } }, "node_modules/glob-parent": { @@ -2933,13 +3040,13 @@ } }, "node_modules/has-flag": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", - "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", "dev": true, "license": "MIT", "engines": { - "node": ">=4" + "node": ">=8" } }, "node_modules/has-property-descriptors": { @@ -3001,9 +3108,9 @@ } }, "node_modules/hasown": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", - "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.4.tgz", + "integrity": "sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A==", "dev": true, "license": "MIT", "dependencies": { @@ -3031,20 +3138,24 @@ "license": "ISC" }, "node_modules/http-errors": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz", - "integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==", + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz", + "integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==", "dev": true, "license": "MIT", "dependencies": { - "depd": "2.0.0", - "inherits": "2.0.4", - "setprototypeof": "1.2.0", - "statuses": "2.0.1", - "toidentifier": "1.0.1" + "depd": "~2.0.0", + "inherits": "~2.0.4", + "setprototypeof": "~1.2.0", + "statuses": "~2.0.2", + "toidentifier": "~1.0.1" }, "engines": { "node": ">= 0.8" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" } }, "node_modules/iconv-lite": { @@ -3060,26 +3171,6 @@ "node": ">=0.10.0" } }, - "node_modules/ieee754": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", - "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "BSD-3-Clause" - }, "node_modules/ignore": { "version": "4.0.6", "resolved": "https://registry.npmjs.org/ignore/-/ignore-4.0.6.tgz", @@ -3097,66 +3188,186 @@ "dev": true, "license": "MIT", "engines": { - "node": ">=0.8.19" + "node": ">=0.8.19" + } + }, + "node_modules/indent-string": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-4.0.0.tgz", + "integrity": "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", + "deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.", + "dev": true, + "license": "ISC", + "dependencies": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/inquirer": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/inquirer/-/inquirer-5.2.0.tgz", + "integrity": "sha512-E9BmnJbAKLPGonz0HeWHtbKf+EeSP93paWO3ZYoUpq/aowXvYGjjCSuashhXPpzbArIjBbji39THkxTz9ZeEUQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-escapes": "^3.0.0", + "chalk": "^2.0.0", + "cli-cursor": "^2.1.0", + "cli-width": "^2.0.0", + "external-editor": "^2.1.0", + "figures": "^2.0.0", + "lodash": "^4.3.0", + "mute-stream": "0.0.7", + "run-async": "^2.2.0", + "rxjs": "^5.5.2", + "string-width": "^2.1.0", + "strip-ansi": "^4.0.0", + "through": "^2.3.6" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/inquirer/node_modules/ansi-regex": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-3.0.1.tgz", + "integrity": "sha512-+O9Jct8wf++lXxxFc4hc8LsjaSq0HFzzL7cVsw8pRDIPdjKD2mT4ytDZlLuSBZ4cLKZFXIrMGO7DbQCtMJJMKw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/inquirer/node_modules/ansi-styles": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", + "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^1.9.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/inquirer/node_modules/chalk": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/inquirer/node_modules/color-convert": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", + "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "1.1.3" + } + }, + "node_modules/inquirer/node_modules/color-name": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", + "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==", + "dev": true, + "license": "MIT" + }, + "node_modules/inquirer/node_modules/escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/inquirer/node_modules/has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/inquirer/node_modules/is-fullwidth-code-point": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz", + "integrity": "sha512-VHskAKYM8RfSFXwee5t5cbN5PZeq1Wrh6qd5bkyiXIf6UQcN6w/A0eXM9r6t8d+GYOh+o6ZhiEnb88LN/Y8m2w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" } }, - "node_modules/indent-string": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-4.0.0.tgz", - "integrity": "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==", + "node_modules/inquirer/node_modules/string-width": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-2.1.1.tgz", + "integrity": "sha512-nOqH59deCq9SRHlxq1Aw85Jnt4w6KvLKqWVik6oA9ZklXLNIOlqg4F2yrT1MVaTjAqvVwdfeZ7w7aCvJD7ugkw==", "dev": true, "license": "MIT", + "dependencies": { + "is-fullwidth-code-point": "^2.0.0", + "strip-ansi": "^4.0.0" + }, "engines": { - "node": ">=8" + "node": ">=4" } }, - "node_modules/inflight": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", - "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", - "deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.", + "node_modules/inquirer/node_modules/strip-ansi": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-4.0.0.tgz", + "integrity": "sha512-4XaJ2zQdCzROZDivEVIDPkcQn8LMFSa8kj8Gxb/Lnwzv9A8VctNZ+lfivC/sV3ivW8ElJTERXZoPBRrZKkNKow==", "dev": true, - "license": "ISC", + "license": "MIT", "dependencies": { - "once": "^1.3.0", - "wrappy": "1" + "ansi-regex": "^3.0.0" + }, + "engines": { + "node": ">=4" } }, - "node_modules/inherits": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", - "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", - "license": "ISC" - }, - "node_modules/ini": { - "version": "1.3.8", - "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz", - "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==", - "license": "ISC" - }, - "node_modules/inquirer": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/inquirer/-/inquirer-5.2.0.tgz", - "integrity": "sha512-E9BmnJbAKLPGonz0HeWHtbKf+EeSP93paWO3ZYoUpq/aowXvYGjjCSuashhXPpzbArIjBbji39THkxTz9ZeEUQ==", + "node_modules/inquirer/node_modules/supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", "dev": true, "license": "MIT", "dependencies": { - "ansi-escapes": "^3.0.0", - "chalk": "^2.0.0", - "cli-cursor": "^2.1.0", - "cli-width": "^2.0.0", - "external-editor": "^2.1.0", - "figures": "^2.0.0", - "lodash": "^4.3.0", - "mute-stream": "0.0.7", - "run-async": "^2.2.0", - "rxjs": "^5.5.2", - "string-width": "^2.1.0", - "strip-ansi": "^4.0.0", - "through": "^2.3.6" + "has-flag": "^3.0.0" }, "engines": { - "node": ">=6.0.0" + "node": ">=4" } }, "node_modules/internal-slot": { @@ -3286,13 +3497,13 @@ } }, "node_modules/is-core-module": { - "version": "2.16.1", - "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz", - "integrity": "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==", + "version": "2.16.2", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.2.tgz", + "integrity": "sha512-evOr8xfXKxE6qSR0hSXL2r3sd7ALj8+7jQEUvPYcm5sgZFdJ+AYzT6yNmJenvIYQBgIGwfwz08sL8zoL7yq2BA==", "dev": true, "license": "MIT", "dependencies": { - "hasown": "^2.0.2" + "hasown": "^2.0.3" }, "engines": { "node": ">= 0.4" @@ -3336,6 +3547,22 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/is-document.all": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-document.all/-/is-document.all-1.0.0.tgz", + "integrity": "sha512-+XSoyS05OdBbhFuELhgTCpFNHkpBOJqtsZfUFFpe5QTw+9Sjbh8zitxhQkYAo6wV7e1Vb8cAPvpCk9jGam/82g==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.4" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/is-extglob": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", @@ -3363,24 +3590,25 @@ } }, "node_modules/is-fullwidth-code-point": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz", - "integrity": "sha512-VHskAKYM8RfSFXwee5t5cbN5PZeq1Wrh6qd5bkyiXIf6UQcN6w/A0eXM9r6t8d+GYOh+o6ZhiEnb88LN/Y8m2w==", + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", "dev": true, "license": "MIT", "engines": { - "node": ">=4" + "node": ">=8" } }, "node_modules/is-generator-function": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/is-generator-function/-/is-generator-function-1.1.0.tgz", - "integrity": "sha512-nPUB5km40q9e8UfN/Zc24eLlzdSf9OfKByBw9CIdw4H1giPMeA0OIJvbchsCu4npfI2QcMVBsGEBHKZ7wLTWmQ==", + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/is-generator-function/-/is-generator-function-1.1.2.tgz", + "integrity": "sha512-upqt1SkGkODW9tsGNG5mtXTXtECizwtS2kA161M+gJPc1xdb/Ax629af6YrTwcOeQHbewrPNlE5Dx7kzvXTizA==", "dev": true, "license": "MIT", "dependencies": { - "call-bound": "^1.0.3", - "get-proto": "^1.0.0", + "call-bound": "^1.0.4", + "generator-function": "^2.0.0", + "get-proto": "^1.0.1", "has-tostringtag": "^1.0.2", "safe-regex-test": "^1.1.0" }, @@ -3698,82 +3926,6 @@ "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, - "node_modules/jest-diff/node_modules/ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, - "license": "MIT", - "dependencies": { - "color-convert": "^2.0.1" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/jest-diff/node_modules/chalk": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" - } - }, - "node_modules/jest-diff/node_modules/color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "color-name": "~1.1.4" - }, - "engines": { - "node": ">=7.0.0" - } - }, - "node_modules/jest-diff/node_modules/color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true, - "license": "MIT" - }, - "node_modules/jest-diff/node_modules/has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/jest-diff/node_modules/supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "dev": true, - "license": "MIT", - "dependencies": { - "has-flag": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, "node_modules/jest-get-type": { "version": "29.6.3", "resolved": "https://registry.npmjs.org/jest-get-type/-/jest-get-type-29.6.3.tgz", @@ -3792,9 +3944,9 @@ "license": "MIT" }, "node_modules/js-yaml": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.2.0.tgz", - "integrity": "sha512-ePWsvanv0DWuDRsW8dnt+R4jQ31SCRCQ7hhNcPXZPsoBZiemuZNYGf7adZdqX2D86j6rvKp3RpCxVTSb8WQlOw==", + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.3.0.tgz", + "integrity": "sha512-1td788aAnnZ5qs7V2QIRl1owjtYpbKt749Y3xauqQgwIIGF/xXWz1wMTEBx5O3LK3lXLVuqXPdPxj2BoFHaW9Q==", "dev": true, "funding": [ { @@ -3904,17 +4056,19 @@ } }, "node_modules/locate-path": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-2.0.0.tgz", - "integrity": "sha512-NCI2kiDkyR7VeEKm27Kda/iQHyKJe1Bu0FlTbYp3CqJu+9IFe9bLyAjMxf5ZDDbEg+iMPzB5zYyUTSm8wVTKmA==", + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", + "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", "dev": true, "license": "MIT", "dependencies": { - "p-locate": "^2.0.0", - "path-exists": "^3.0.0" + "p-locate": "^5.0.0" }, "engines": { - "node": ">=4" + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, "node_modules/lodash": { @@ -3941,82 +4095,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/log-symbols/node_modules/ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, - "license": "MIT", - "dependencies": { - "color-convert": "^2.0.1" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/log-symbols/node_modules/chalk": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" - } - }, - "node_modules/log-symbols/node_modules/color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "color-name": "~1.1.4" - }, - "engines": { - "node": ">=7.0.0" - } - }, - "node_modules/log-symbols/node_modules/color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true, - "license": "MIT" - }, - "node_modules/log-symbols/node_modules/has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/log-symbols/node_modules/supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "dev": true, - "license": "MIT", - "dependencies": { - "has-flag": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, "node_modules/loose-envify": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", @@ -4026,23 +4104,17 @@ "dependencies": { "js-tokens": "^3.0.0 || ^4.0.0" }, - "bin": { - "loose-envify": "cli.js" - } - }, - "node_modules/lru-cache": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", - "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", - "dev": true, - "license": "ISC", - "dependencies": { - "yallist": "^4.0.0" - }, - "engines": { - "node": ">=10" + "bin": { + "loose-envify": "cli.js" } }, + "node_modules/lru-cache": { + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", + "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", + "dev": true, + "license": "ISC" + }, "node_modules/map-obj": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/map-obj/-/map-obj-4.3.0.tgz", @@ -4143,6 +4215,19 @@ "node": ">=8" } }, + "node_modules/meow/node_modules/lru-cache": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/meow/node_modules/normalize-package-data": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-3.0.3.tgz", @@ -4188,16 +4273,6 @@ "node": ">=8" } }, - "node_modules/meow/node_modules/p-try": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", - "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, "node_modules/meow/node_modules/parse-json": { "version": "5.2.0", "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz", @@ -4217,16 +4292,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/meow/node_modules/path-exists": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", - "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, "node_modules/meow/node_modules/read-pkg": { "version": "5.2.0", "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-5.2.0.tgz", @@ -4311,6 +4376,29 @@ "node": ">=8" } }, + "node_modules/meow/node_modules/semver": { + "version": "7.8.5", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.5.tgz", + "integrity": "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/meow/node_modules/yargs-parser": { + "version": "20.2.9", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.9.tgz", + "integrity": "sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=10" + } + }, "node_modules/merge-descriptors": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.3.tgz", @@ -4381,258 +4469,37 @@ "node_modules/mime-types": { "version": "2.1.35", "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", - "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", - "dev": true, - "license": "MIT", - "dependencies": { - "mime-db": "1.52.0" - }, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/mimic-fn": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-1.2.0.tgz", - "integrity": "sha512-jf84uxzwiuiIVKiOLpfYk7N46TSy8ubTonmneY9vrpHNAnp0QBt2BxWV9dO3/j+BoVAb+a5G6YDPW3M5HOdMWQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=4" - } - }, - "node_modules/mimic-response": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-3.1.0.tgz", - "integrity": "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==", - "license": "MIT", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/min-indent": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/min-indent/-/min-indent-1.0.1.tgz", - "integrity": "sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=4" - } - }, - "node_modules/minimatch": { - "version": "3.1.5", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", - "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", - "dev": true, - "license": "ISC", - "dependencies": { - "brace-expansion": "^1.1.7" - }, - "engines": { - "node": "*" - } - }, - "node_modules/minimist": { - "version": "1.2.8", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", - "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/minimist-options": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/minimist-options/-/minimist-options-4.1.0.tgz", - "integrity": "sha512-Q4r8ghd80yhO/0j1O3B2BjweX3fiHg9cdOwjJd2J76Q135c+NDxGCqdYKQ1SKBuFfgWbAUzBfvYjPUEeNgqN1A==", - "dev": true, - "license": "MIT", - "dependencies": { - "arrify": "^1.0.1", - "is-plain-obj": "^1.1.0", - "kind-of": "^6.0.3" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/minipass": { - "version": "7.1.3", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.3.tgz", - "integrity": "sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==", - "dev": true, - "license": "BlueOak-1.0.0", - "engines": { - "node": ">=16 || 14 >=14.17" - } - }, - "node_modules/mkdirp": { - "version": "0.5.6", - "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.6.tgz", - "integrity": "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==", - "dev": true, - "license": "MIT", - "dependencies": { - "minimist": "^1.2.6" - }, - "bin": { - "mkdirp": "bin/cmd.js" - } - }, - "node_modules/mkdirp-classic": { - "version": "0.5.3", - "resolved": "https://registry.npmjs.org/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz", - "integrity": "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==", - "license": "MIT" - }, - "node_modules/mocha": { - "version": "11.7.6", - "resolved": "https://registry.npmjs.org/mocha/-/mocha-11.7.6.tgz", - "integrity": "sha512-nS9xOGbw2I3cjCpxwZAEJ9xK9lmJ08vEkQvLtz4du9ZrF9UrjRpeJGiIgl2Z+Qs++pmB4ecDe48Fwsh+j+j7xA==", - "dev": true, - "license": "MIT", - "dependencies": { - "browser-stdout": "^1.3.1", - "chokidar": "^4.0.1", - "debug": "^4.3.5", - "diff": "^7.0.0", - "escape-string-regexp": "^4.0.0", - "find-up": "^5.0.0", - "glob": "^10.4.5", - "he": "^1.2.0", - "is-path-inside": "^3.0.3", - "js-yaml": "^4.1.0", - "log-symbols": "^4.1.0", - "minimatch": "^9.0.5", - "ms": "^2.1.3", - "picocolors": "^1.1.1", - "serialize-javascript": "^6.0.2", - "strip-json-comments": "^3.1.1", - "supports-color": "^8.1.1", - "workerpool": "^9.2.0", - "yargs": "^17.7.2", - "yargs-parser": "^21.1.1", - "yargs-unparser": "^2.0.0" - }, - "bin": { - "_mocha": "bin/_mocha", - "mocha": "bin/mocha.js" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - } - }, - "node_modules/mocha/node_modules/brace-expansion": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.1.tgz", - "integrity": "sha512-WR1cURNjuvBLMZBMbqM0UoE+WAfdUcEV1ccD8PVBVOI+Z3ND4+SZbN8RsfT2bMuG1qwz5RFvPukSZm5fF2D5eA==", - "dev": true, - "license": "MIT", - "dependencies": { - "balanced-match": "^1.0.0" - } - }, - "node_modules/mocha/node_modules/debug": { - "version": "4.4.3", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", - "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", - "dev": true, - "license": "MIT", - "dependencies": { - "ms": "^2.1.3" - }, - "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } - } - }, - "node_modules/mocha/node_modules/escape-string-regexp": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", - "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/mocha/node_modules/find-up": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", - "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", - "dev": true, - "license": "MIT", - "dependencies": { - "locate-path": "^6.0.0", - "path-exists": "^4.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/mocha/node_modules/glob": { - "version": "10.5.0", - "resolved": "https://registry.npmjs.org/glob/-/glob-10.5.0.tgz", - "integrity": "sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg==", - "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", "dev": true, - "license": "ISC", + "license": "MIT", "dependencies": { - "foreground-child": "^3.1.0", - "jackspeak": "^3.1.2", - "minimatch": "^9.0.4", - "minipass": "^7.1.2", - "package-json-from-dist": "^1.0.0", - "path-scurry": "^1.11.1" - }, - "bin": { - "glob": "dist/esm/bin.mjs" + "mime-db": "1.52.0" }, - "funding": { - "url": "https://github.com/sponsors/isaacs" + "engines": { + "node": ">= 0.6" } }, - "node_modules/mocha/node_modules/has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "node_modules/mimic-fn": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-1.2.0.tgz", + "integrity": "sha512-jf84uxzwiuiIVKiOLpfYk7N46TSy8ubTonmneY9vrpHNAnp0QBt2BxWV9dO3/j+BoVAb+a5G6YDPW3M5HOdMWQ==", "dev": true, "license": "MIT", "engines": { - "node": ">=8" + "node": ">=4" } }, - "node_modules/mocha/node_modules/locate-path": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", - "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", + "node_modules/min-indent": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/min-indent/-/min-indent-1.0.1.tgz", + "integrity": "sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg==", "dev": true, "license": "MIT", - "dependencies": { - "p-locate": "^5.0.0" - }, "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "node": ">=4" } }, - "node_modules/mocha/node_modules/minimatch": { + "node_modules/minimatch": { "version": "9.0.9", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.9.tgz", "integrity": "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==", @@ -4648,94 +4515,116 @@ "url": "https://github.com/sponsors/isaacs" } }, - "node_modules/mocha/node_modules/ms": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", - "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", - "dev": true, - "license": "MIT" - }, - "node_modules/mocha/node_modules/p-limit": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", - "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "node_modules/minimist": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", "dev": true, "license": "MIT", - "dependencies": { - "yocto-queue": "^0.1.0" - }, - "engines": { - "node": ">=10" - }, "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/mocha/node_modules/p-locate": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", - "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", + "node_modules/minimist-options": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/minimist-options/-/minimist-options-4.1.0.tgz", + "integrity": "sha512-Q4r8ghd80yhO/0j1O3B2BjweX3fiHg9cdOwjJd2J76Q135c+NDxGCqdYKQ1SKBuFfgWbAUzBfvYjPUEeNgqN1A==", "dev": true, "license": "MIT", "dependencies": { - "p-limit": "^3.0.2" + "arrify": "^1.0.1", + "is-plain-obj": "^1.1.0", + "kind-of": "^6.0.3" }, "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "node": ">= 6" } }, - "node_modules/mocha/node_modules/path-exists": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", - "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "node_modules/minipass": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.3.tgz", + "integrity": "sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==", "dev": true, - "license": "MIT", + "license": "BlueOak-1.0.0", "engines": { - "node": ">=8" + "node": ">=16 || 14 >=14.17" } }, - "node_modules/mocha/node_modules/strip-json-comments": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", - "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", + "node_modules/mkdirp": { + "version": "0.5.6", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.6.tgz", + "integrity": "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==", "dev": true, "license": "MIT", - "engines": { - "node": ">=8" + "dependencies": { + "minimist": "^1.2.6" }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "bin": { + "mkdirp": "bin/cmd.js" } }, - "node_modules/mocha/node_modules/supports-color": { - "version": "8.1.1", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", - "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", + "node_modules/mocha": { + "version": "11.7.6", + "resolved": "https://registry.npmjs.org/mocha/-/mocha-11.7.6.tgz", + "integrity": "sha512-nS9xOGbw2I3cjCpxwZAEJ9xK9lmJ08vEkQvLtz4du9ZrF9UrjRpeJGiIgl2Z+Qs++pmB4ecDe48Fwsh+j+j7xA==", "dev": true, "license": "MIT", "dependencies": { - "has-flag": "^4.0.0" + "browser-stdout": "^1.3.1", + "chokidar": "^4.0.1", + "debug": "^4.3.5", + "diff": "^7.0.0", + "escape-string-regexp": "^4.0.0", + "find-up": "^5.0.0", + "glob": "^10.4.5", + "he": "^1.2.0", + "is-path-inside": "^3.0.3", + "js-yaml": "^4.1.0", + "log-symbols": "^4.1.0", + "minimatch": "^9.0.5", + "ms": "^2.1.3", + "picocolors": "^1.1.1", + "serialize-javascript": "^6.0.2", + "strip-json-comments": "^3.1.1", + "supports-color": "^8.1.1", + "workerpool": "^9.2.0", + "yargs": "^17.7.2", + "yargs-parser": "^21.1.1", + "yargs-unparser": "^2.0.0" }, - "engines": { - "node": ">=10" + "bin": { + "_mocha": "bin/_mocha", + "mocha": "bin/mocha.js" }, - "funding": { - "url": "https://github.com/chalk/supports-color?sponsor=1" + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" } }, - "node_modules/mocha/node_modules/yargs-parser": { - "version": "21.1.1", - "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", - "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", + "node_modules/mocha/node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", "dev": true, - "license": "ISC", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, "engines": { - "node": ">=12" + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } } }, + "node_modules/mocha/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, "node_modules/ms": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", @@ -4750,12 +4639,6 @@ "dev": true, "license": "ISC" }, - "node_modules/napi-build-utils": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/napi-build-utils/-/napi-build-utils-2.0.0.tgz", - "integrity": "sha512-GEbrYkbfF7MoNaoh2iGG84Mnf/WZfB0GdGEsM8wz7Expx/LlWf5U8t9nvJKXSp3qr5IsEbK04cBGhol/KwOsWA==", - "license": "MIT" - }, "node_modules/natural-compare": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", @@ -4780,25 +4663,33 @@ "dev": true, "license": "MIT" }, - "node_modules/node-abi": { - "version": "3.85.0", - "resolved": "https://registry.npmjs.org/node-abi/-/node-abi-3.85.0.tgz", - "integrity": "sha512-zsFhmbkAzwhTft6nd3VxcG0cvJsT70rL+BIGHWVq5fi6MwGrHwzqKaxXE+Hl2GmnGItnDKPPkO5/LQqjVkIdFg==", + "node_modules/node-exports-info": { + "version": "1.6.2", + "resolved": "https://registry.npmjs.org/node-exports-info/-/node-exports-info-1.6.2.tgz", + "integrity": "sha512-kXs9Go0cah0qHVV2v389IXQLdLCeE1xfFtjOAF+iobu0OIoG1pje8At2vMHyaPMiPMnG/LWP50twML21eMcAag==", + "dev": true, "license": "MIT", "dependencies": { - "semver": "^7.3.5" + "array.prototype.flatmap": "^1.3.3", + "es-errors": "^1.3.0", + "object.entries": "^1.1.9", + "semver": "^6.3.1" }, "engines": { - "node": ">=10" + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/node-addon-api": { - "version": "8.9.0", - "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-8.9.0.tgz", - "integrity": "sha512-ekZMeaaIzSQTSpr7X2X3iJM7lTzgnx8ahAG9pJfT/7+14mlEM8ZYQ9cgCDvSSRbReFK0oHli3WrZdCiRsgAT9Q==", - "license": "MIT", - "engines": { - "node": "^18 || ^20 || >= 21" + "node_modules/node-exports-info/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" } }, "node_modules/normalize-package-data": { @@ -4814,16 +4705,6 @@ "validate-npm-package-license": "^3.0.1" } }, - "node_modules/normalize-package-data/node_modules/semver": { - "version": "5.7.2", - "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz", - "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==", - "dev": true, - "license": "ISC", - "bin": { - "semver": "bin/semver" - } - }, "node_modules/object-assign": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", @@ -4878,6 +4759,22 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/object.entries": { + "version": "1.1.9", + "resolved": "https://registry.npmjs.org/object.entries/-/object.entries-1.1.9.tgz", + "integrity": "sha512-8u/hfXFRBD1O0hPUjioLhoWFHRmt6tKA4/vZPyckBr18l1KE9uHrFaFaUi8MDRTpi4uak2goyPTSNJLXX2k2Hw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.4", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/on-finished": { "version": "2.4.1", "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", @@ -4895,6 +4792,7 @@ "version": "1.4.0", "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "dev": true, "license": "ISC", "dependencies": { "wrappy": "1" @@ -4960,39 +4858,45 @@ } }, "node_modules/p-limit": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-1.3.0.tgz", - "integrity": "sha512-vvcXsLAJ9Dr5rQOPk7toZQZJApBl2K4J6dANSsEuh6QI41JYcsS/qhTGa9ErIUUgK3WNQoJYvylxvjqmiqEA9Q==", + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", "dev": true, "license": "MIT", "dependencies": { - "p-try": "^1.0.0" + "yocto-queue": "^0.1.0" }, "engines": { - "node": ">=4" + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, "node_modules/p-locate": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-2.0.0.tgz", - "integrity": "sha512-nQja7m7gSKuewoVRen45CtVfODR3crN3goVQ0DDZ9N3yHxgpkuBhZqsaiotSQRrADUrne346peY7kT3TSACykg==", + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", + "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", "dev": true, "license": "MIT", "dependencies": { - "p-limit": "^1.1.0" + "p-limit": "^3.0.2" }, "engines": { - "node": ">=4" + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, "node_modules/p-try": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/p-try/-/p-try-1.0.0.tgz", - "integrity": "sha512-U1etNYuMJoIz3ZXSrrySFjsXQTWOx2/jdi86L+2pRvph/qMKL6sbcCYdH23fqsbm8TH2Gn0OybpT4eSFlCVHww==", + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", + "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", "dev": true, "license": "MIT", "engines": { - "node": ">=4" + "node": ">=6" } }, "node_modules/package-json-from-dist": { @@ -5026,13 +4930,13 @@ } }, "node_modules/path-exists": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-3.0.0.tgz", - "integrity": "sha512-bpC7GYwiDYQ4wYLe+FA8lhRjhQCMcQGuSgGGqDkg/QerRWw9CmGRT0iSOVRSZJ29NMLZgIzqaljJ63oaL4NIJQ==", + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", "dev": true, "license": "MIT", "engines": { - "node": ">=4" + "node": ">=8" } }, "node_modules/path-is-absolute": { @@ -5053,13 +4957,13 @@ "license": "(WTFPL OR MIT)" }, "node_modules/path-key": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/path-key/-/path-key-2.0.1.tgz", - "integrity": "sha512-fEHGKCSmUSDPv4uoj8AlD+joPlq3peND+HRYyxFz4KPw4z926S/b8rIuFs2FYJg3BwsxJf6A9/3eIdLaYC+9Dw==", + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", "dev": true, "license": "MIT", "engines": { - "node": ">=4" + "node": ">=8" } }, "node_modules/path-parse": { @@ -5086,17 +4990,10 @@ "url": "https://github.com/sponsors/isaacs" } }, - "node_modules/path-scurry/node_modules/lru-cache": { - "version": "10.4.3", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", - "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", - "dev": true, - "license": "ISC" - }, "node_modules/path-to-regexp": { - "version": "0.1.12", - "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.12.tgz", - "integrity": "sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ==", + "version": "0.1.13", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.13.tgz", + "integrity": "sha512-A/AGNMFN3c8bOlvV9RreMdrv7jsmF9XIfDeCd87+I8RNg6s78BhJxMu69NEMHBSJFxKidViTEdruRwEk/WIKqA==", "dev": true, "license": "MIT" }, @@ -5170,6 +5067,19 @@ "node": ">=4" } }, + "node_modules/pkg-conf/node_modules/find-up": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-2.1.0.tgz", + "integrity": "sha512-NWzkk0jSJtTt08+FBFMvXoeZnOJD+jTtsRmBYbAIzJdX6l7dLgR7CTubCM5/eDdPUBvLCeVasP1brfVR/9/EZQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "locate-path": "^2.0.0" + }, + "engines": { + "node": ">=4" + } + }, "node_modules/pkg-conf/node_modules/load-json-file": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/load-json-file/-/load-json-file-4.0.0.tgz", @@ -5186,6 +5096,56 @@ "node": ">=4" } }, + "node_modules/pkg-conf/node_modules/locate-path": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-2.0.0.tgz", + "integrity": "sha512-NCI2kiDkyR7VeEKm27Kda/iQHyKJe1Bu0FlTbYp3CqJu+9IFe9bLyAjMxf5ZDDbEg+iMPzB5zYyUTSm8wVTKmA==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-locate": "^2.0.0", + "path-exists": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/pkg-conf/node_modules/p-limit": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-1.3.0.tgz", + "integrity": "sha512-vvcXsLAJ9Dr5rQOPk7toZQZJApBl2K4J6dANSsEuh6QI41JYcsS/qhTGa9ErIUUgK3WNQoJYvylxvjqmiqEA9Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-try": "^1.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/pkg-conf/node_modules/p-locate": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-2.0.0.tgz", + "integrity": "sha512-nQja7m7gSKuewoVRen45CtVfODR3crN3goVQ0DDZ9N3yHxgpkuBhZqsaiotSQRrADUrne346peY7kT3TSACykg==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-limit": "^1.1.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/pkg-conf/node_modules/p-try": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/p-try/-/p-try-1.0.0.tgz", + "integrity": "sha512-U1etNYuMJoIz3ZXSrrySFjsXQTWOx2/jdi86L+2pRvph/qMKL6sbcCYdH23fqsbm8TH2Gn0OybpT4eSFlCVHww==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, "node_modules/pkg-conf/node_modules/parse-json": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-4.0.0.tgz", @@ -5200,6 +5160,16 @@ "node": ">=4" } }, + "node_modules/pkg-conf/node_modules/path-exists": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-3.0.0.tgz", + "integrity": "sha512-bpC7GYwiDYQ4wYLe+FA8lhRjhQCMcQGuSgGGqDkg/QerRWw9CmGRT0iSOVRSZJ29NMLZgIzqaljJ63oaL4NIJQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, "node_modules/pkg-conf/node_modules/pify": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/pify/-/pify-3.0.0.tgz", @@ -5271,32 +5241,6 @@ "node": ">= 0.4" } }, - "node_modules/prebuild-install": { - "version": "7.1.3", - "resolved": "https://registry.npmjs.org/prebuild-install/-/prebuild-install-7.1.3.tgz", - "integrity": "sha512-8Mf2cbV7x1cXPUILADGI3wuhfqWvtiLA1iclTDbFRZkgRQS0NqsPZphna9V+HyTEadheuPmjaJMsbzKQFOzLug==", - "license": "MIT", - "dependencies": { - "detect-libc": "^2.0.0", - "expand-template": "^2.0.3", - "github-from-package": "0.0.0", - "minimist": "^1.2.3", - "mkdirp-classic": "^0.5.3", - "napi-build-utils": "^2.0.0", - "node-abi": "^3.3.0", - "pump": "^3.0.0", - "rc": "^1.2.7", - "simple-get": "^4.0.0", - "tar-fs": "^2.0.0", - "tunnel-agent": "^0.6.0" - }, - "bin": { - "prebuild-install": "bin.js" - }, - "engines": { - "node": ">=10" - } - }, "node_modules/prelude-ls": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.1.2.tgz", @@ -5377,16 +5321,6 @@ "node": ">= 0.10" } }, - "node_modules/pump": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.3.tgz", - "integrity": "sha512-todwxLMY7/heScKmntwQG8CXVkWUOdYxIvY2s0VWAAMh/nd8SoYiRaKjlr7+iCs984f2P8zvrfWcDDYVb73NfA==", - "license": "MIT", - "dependencies": { - "end-of-stream": "^1.1.0", - "once": "^1.3.1" - } - }, "node_modules/punycode": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", @@ -5398,13 +5332,14 @@ } }, "node_modules/qs": { - "version": "6.15.2", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.15.2.tgz", - "integrity": "sha512-Rzq0KEyX/w/tEybncDgdkZrJgVUsUMk3xjh3t5bv3S1HTAtg+uOYt72+ZfwiQwKdysThkTBdL/rTi6HDmX9Ddw==", + "version": "6.15.3", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.15.3.tgz", + "integrity": "sha512-O9gl3zCl5h5blw1KGUzQKhA5oUXSl8rwUIM5o0S3nCXMliSvy5Dzx7/DJcI+SwgICv+IneSZwhBh1oSyEHA71A==", "dev": true, "license": "BSD-3-Clause", "dependencies": { - "side-channel": "^1.1.0" + "es-define-property": "^1.0.1", + "side-channel": "^1.1.1" }, "engines": { "node": ">=0.6" @@ -5480,52 +5415,6 @@ "node": ">= 0.8" } }, - "node_modules/raw-body/node_modules/http-errors": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz", - "integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "depd": "~2.0.0", - "inherits": "~2.0.4", - "setprototypeof": "~1.2.0", - "statuses": "~2.0.2", - "toidentifier": "~1.0.1" - }, - "engines": { - "node": ">= 0.8" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } - }, - "node_modules/raw-body/node_modules/statuses": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz", - "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/rc": { - "version": "1.2.8", - "resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz", - "integrity": "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==", - "license": "(BSD-2-Clause OR MIT OR Apache-2.0)", - "dependencies": { - "deep-extend": "^0.6.0", - "ini": "~1.3.0", - "minimist": "^1.2.0", - "strip-json-comments": "~2.0.1" - }, - "bin": { - "rc": "cli.js" - } - }, "node_modules/react-is": { "version": "16.13.1", "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", @@ -5562,18 +5451,77 @@ "node": ">=4" } }, - "node_modules/readable-stream": { - "version": "3.6.2", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", - "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "node_modules/read-pkg-up/node_modules/find-up": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-2.1.0.tgz", + "integrity": "sha512-NWzkk0jSJtTt08+FBFMvXoeZnOJD+jTtsRmBYbAIzJdX6l7dLgR7CTubCM5/eDdPUBvLCeVasP1brfVR/9/EZQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "locate-path": "^2.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/read-pkg-up/node_modules/locate-path": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-2.0.0.tgz", + "integrity": "sha512-NCI2kiDkyR7VeEKm27Kda/iQHyKJe1Bu0FlTbYp3CqJu+9IFe9bLyAjMxf5ZDDbEg+iMPzB5zYyUTSm8wVTKmA==", + "dev": true, "license": "MIT", "dependencies": { - "inherits": "^2.0.3", - "string_decoder": "^1.1.1", - "util-deprecate": "^1.0.1" + "p-locate": "^2.0.0", + "path-exists": "^3.0.0" }, "engines": { - "node": ">= 6" + "node": ">=4" + } + }, + "node_modules/read-pkg-up/node_modules/p-limit": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-1.3.0.tgz", + "integrity": "sha512-vvcXsLAJ9Dr5rQOPk7toZQZJApBl2K4J6dANSsEuh6QI41JYcsS/qhTGa9ErIUUgK3WNQoJYvylxvjqmiqEA9Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-try": "^1.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/read-pkg-up/node_modules/p-locate": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-2.0.0.tgz", + "integrity": "sha512-nQja7m7gSKuewoVRen45CtVfODR3crN3goVQ0DDZ9N3yHxgpkuBhZqsaiotSQRrADUrne346peY7kT3TSACykg==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-limit": "^1.1.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/read-pkg-up/node_modules/p-try": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/p-try/-/p-try-1.0.0.tgz", + "integrity": "sha512-U1etNYuMJoIz3ZXSrrySFjsXQTWOx2/jdi86L+2pRvph/qMKL6sbcCYdH23fqsbm8TH2Gn0OybpT4eSFlCVHww==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/read-pkg-up/node_modules/path-exists": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-3.0.0.tgz", + "integrity": "sha512-bpC7GYwiDYQ4wYLe+FA8lhRjhQCMcQGuSgGGqDkg/QerRWw9CmGRT0iSOVRSZJ29NMLZgIzqaljJ63oaL4NIJQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" } }, "node_modules/readdirp": { @@ -5683,13 +5631,14 @@ } }, "node_modules/resolve": { - "version": "1.22.10", - "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.10.tgz", - "integrity": "sha512-NPRy+/ncIMeDlTAsuqwKIiferiawhefFJtkNSW0qZJEqMEb+qBt/77B/jGeeek+F0uOeN05CDa6HXbbIgtVX4w==", + "version": "1.22.12", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.12.tgz", + "integrity": "sha512-TyeJ1zif53BPfHootBGwPRYT1RUt6oGWsaQr8UyZW/eAm9bKoijtvruSDEmZHm92CwS9nj7/fWttqPCgzep8CA==", "dev": true, "license": "MIT", "dependencies": { - "is-core-module": "^2.16.0", + "es-errors": "^1.3.0", + "is-core-module": "^2.16.1", "path-parse": "^1.0.7", "supports-preserve-symlinks-flag": "^1.0.0" }, @@ -5727,6 +5676,13 @@ "node": ">=4" } }, + "node_modules/restore-cursor/node_modules/signal-exit": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", + "dev": true, + "license": "ISC" + }, "node_modules/reusify": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz", @@ -5752,11 +5708,22 @@ "rimraf": "bin.js" } }, + "node_modules/rimraf/node_modules/brace-expansion": { + "version": "1.1.15", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.15.tgz", + "integrity": "sha512-EwOCDEex4quD37XhqM3omwtMoJjr//isUZz1JopUNWms+4Z2ViyM/k1YIRePpoVNnQhENnxtFjLaxNHrT7xIUg==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, "node_modules/rimraf/node_modules/glob": { "version": "7.2.3", "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", - "deprecated": "Glob versions prior to v9 are no longer supported", + "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", "dev": true, "license": "ISC", "dependencies": { @@ -5774,6 +5741,19 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/rimraf/node_modules/minimatch": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, "node_modules/run-async": { "version": "2.4.1", "resolved": "https://registry.npmjs.org/run-async/-/run-async-2.4.1.tgz", @@ -5822,15 +5802,15 @@ } }, "node_modules/safe-array-concat": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/safe-array-concat/-/safe-array-concat-1.1.3.tgz", - "integrity": "sha512-AURm5f0jYEOydBj7VQlVvDrjeFgthDdEF5H1dP+6mNpoXOMo1quQqJ4wvJDyRZ9+pO3kGWoOdmV08cSv2aJV6Q==", + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/safe-array-concat/-/safe-array-concat-1.1.4.tgz", + "integrity": "sha512-wtZlHyOje6OZTGqAoaDKxFkgRtkF9CnHAVnCHKfuj200wAgL+bSJhdsCD2l0Qx/2ekEXjPWcyKkfGb5CPboslg==", "dev": true, "license": "MIT", "dependencies": { - "call-bind": "^1.0.8", - "call-bound": "^1.0.2", - "get-intrinsic": "^1.2.6", + "call-bind": "^1.0.9", + "call-bound": "^1.0.4", + "get-intrinsic": "^1.3.0", "has-symbols": "^1.1.0", "isarray": "^2.0.5" }, @@ -5845,6 +5825,7 @@ "version": "5.2.1", "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "dev": true, "funding": [ { "type": "github", @@ -5904,52 +5885,40 @@ "license": "MIT" }, "node_modules/semver": { - "version": "7.7.2", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz", - "integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==", + "version": "5.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz", + "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==", + "dev": true, "license": "ISC", "bin": { - "semver": "bin/semver.js" - }, - "engines": { - "node": ">=10" + "semver": "bin/semver" } }, "node_modules/send": { - "version": "0.19.0", - "resolved": "https://registry.npmjs.org/send/-/send-0.19.0.tgz", - "integrity": "sha512-dW41u5VfLXu8SJh5bwRmyYUbAoSB3c9uQh6L8h/KtsFREPWpbX1lrljJo186Jc4nmci/sGUZ9a0a0J2zgfq2hw==", + "version": "0.19.2", + "resolved": "https://registry.npmjs.org/send/-/send-0.19.2.tgz", + "integrity": "sha512-VMbMxbDeehAxpOtWJXlcUS5E8iXh6QmN+BkRX1GARS3wRaXEEgzCcB10gTQazO42tpNIya8xIyNx8fll1OFPrg==", "dev": true, "license": "MIT", "dependencies": { "debug": "2.6.9", "depd": "2.0.0", "destroy": "1.2.0", - "encodeurl": "~1.0.2", + "encodeurl": "~2.0.0", "escape-html": "~1.0.3", "etag": "~1.8.1", - "fresh": "0.5.2", - "http-errors": "2.0.0", + "fresh": "~0.5.2", + "http-errors": "~2.0.1", "mime": "1.6.0", "ms": "2.1.3", - "on-finished": "2.4.1", + "on-finished": "~2.4.1", "range-parser": "~1.2.1", - "statuses": "2.0.1" + "statuses": "~2.0.2" }, "engines": { "node": ">= 0.8.0" } }, - "node_modules/send/node_modules/encodeurl": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", - "integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, "node_modules/send/node_modules/ms": { "version": "2.1.3", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", @@ -5968,16 +5937,16 @@ } }, "node_modules/serve-static": { - "version": "1.16.2", - "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.16.2.tgz", - "integrity": "sha512-VqpjJZKadQB/PEbEwvFdO43Ax5dFBZ2UECszz8bQ7pi7wt//PWe1P6MN7eCnjsatYtBT6EuiClbjSWP2WrIoTw==", + "version": "1.16.3", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.16.3.tgz", + "integrity": "sha512-x0RTqQel6g5SY7Lg6ZreMmsOzncHFU7nhnRWkKgWuMTu5NN0DR5oruckMqRvacAN9d5w6ARnRBXl9xhDCgfMeA==", "dev": true, "license": "MIT", "dependencies": { "encodeurl": "~2.0.0", "escape-html": "~1.0.3", "parseurl": "~1.3.3", - "send": "0.19.0" + "send": "~0.19.1" }, "engines": { "node": ">= 0.8.0" @@ -6040,38 +6009,38 @@ "license": "ISC" }, "node_modules/shebang-command": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-1.2.0.tgz", - "integrity": "sha512-EV3L1+UQWGor21OmnvojK36mhg+TyIKDh3iFBKBohr5xeXIhNBcx8oWdgkTEEQ+BEFFYdLRuqMfd5L84N1V5Vg==", + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", "dev": true, "license": "MIT", "dependencies": { - "shebang-regex": "^1.0.0" + "shebang-regex": "^3.0.0" }, "engines": { - "node": ">=0.10.0" + "node": ">=8" } }, "node_modules/shebang-regex": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-1.0.0.tgz", - "integrity": "sha512-wpoSFAxys6b2a2wHZ1XpDSgD7N9iVjg29Ph9uV/uaP9Ex/KXlkTZTeddxDPSYQpgvzKLGJke2UU0AzoGCjNIvQ==", + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", "dev": true, "license": "MIT", "engines": { - "node": ">=0.10.0" + "node": ">=8" } }, "node_modules/side-channel": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", - "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==", + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.1.tgz", + "integrity": "sha512-6x6dK6zJdpTzF4sQeNYxwtvBzf6Eg4GtlesS94HOvTudUeyK2WXAaIfmDgsyslYrRBeFIlsi54AYsFGUuhmvrQ==", "dev": true, "license": "MIT", "dependencies": { "es-errors": "^1.3.0", - "object-inspect": "^1.13.3", - "side-channel-list": "^1.0.0", + "object-inspect": "^1.13.4", + "side-channel-list": "^1.0.1", "side-channel-map": "^1.0.1", "side-channel-weakmap": "^1.0.2" }, @@ -6083,14 +6052,14 @@ } }, "node_modules/side-channel-list": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz", - "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==", + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.1.tgz", + "integrity": "sha512-mjn/0bi/oUURjc5Xl7IaWi/OJJJumuoJFQJfDDyO46+hBWsfaVM65TBHq2eoZBhzl9EchxOijpkbRC8SVBQU0w==", "dev": true, "license": "MIT", "dependencies": { "es-errors": "^1.3.0", - "object-inspect": "^1.13.3" + "object-inspect": "^1.13.4" }, "engines": { "node": ">= 0.4" @@ -6139,55 +6108,16 @@ } }, "node_modules/signal-exit": { - "version": "3.0.7", - "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", - "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", "dev": true, - "license": "ISC" - }, - "node_modules/simple-concat": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/simple-concat/-/simple-concat-1.0.1.tgz", - "integrity": "sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "MIT" - }, - "node_modules/simple-get": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/simple-get/-/simple-get-4.0.1.tgz", - "integrity": "sha512-brv7p5WgH0jmQJr1ZDDfKDOSeWWg+OVypG99A/5vYGPqJ6pxiaHLy8nxtFjBA7oMa01ebA9gfh1uMCFqOuXxvA==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "MIT", - "dependencies": { - "decompress-response": "^6.0.0", - "once": "^1.3.1", - "simple-concat": "^1.0.0" + "license": "ISC", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" } }, "node_modules/slash": { @@ -6213,6 +6143,16 @@ "node": ">=4" } }, + "node_modules/slice-ansi/node_modules/is-fullwidth-code-point": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz", + "integrity": "sha512-VHskAKYM8RfSFXwee5t5cbN5PZeq1Wrh6qd5bkyiXIf6UQcN6w/A0eXM9r6t8d+GYOh+o6ZhiEnb88LN/Y8m2w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, "node_modules/spdx-correct": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/spdx-correct/-/spdx-correct-3.2.0.tgz", @@ -6243,9 +6183,9 @@ } }, "node_modules/spdx-license-ids": { - "version": "3.0.22", - "resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-3.0.22.tgz", - "integrity": "sha512-4PRT4nh1EImPbt2jASOKHX7PB7I+e4IWNLvkKFDxNhJlfjbYlleYQh285Z/3mPTHSAK/AvdMmw5BNNuYH8ShgQ==", + "version": "3.0.23", + "resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-3.0.23.tgz", + "integrity": "sha512-CWLcCCH7VLu13TgOH+r8p1O/Znwhqv/dbb6lqWy67G+pT1kHmeD/+V36AVb/vq8QMIQwVShJ6Ssl5FPh0fuSdw==", "dev": true, "license": "CC0-1.0" }, @@ -6294,9 +6234,9 @@ } }, "node_modules/statuses": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", - "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==", + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz", + "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==", "dev": true, "license": "MIT", "engines": { @@ -6317,27 +6257,22 @@ "node": ">= 0.4" } }, - "node_modules/string_decoder": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", - "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", - "license": "MIT", - "dependencies": { - "safe-buffer": "~5.2.0" - } - }, "node_modules/string-width": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-2.1.1.tgz", - "integrity": "sha512-nOqH59deCq9SRHlxq1Aw85Jnt4w6KvLKqWVik6oA9ZklXLNIOlqg4F2yrT1MVaTjAqvVwdfeZ7w7aCvJD7ugkw==", + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", + "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", "dev": true, "license": "MIT", "dependencies": { - "is-fullwidth-code-point": "^2.0.0", - "strip-ansi": "^4.0.0" + "eastasianwidth": "^0.2.0", + "emoji-regex": "^9.2.2", + "strip-ansi": "^7.0.1" }, "engines": { - "node": ">=4" + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, "node_modules/string-width-cjs": { @@ -6366,15 +6301,12 @@ "node": ">=8" } }, - "node_modules/string-width-cjs/node_modules/is-fullwidth-code-point": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", - "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "node_modules/string-width-cjs/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } + "license": "MIT" }, "node_modules/string-width-cjs/node_modules/strip-ansi": { "version": "6.0.1", @@ -6390,19 +6322,20 @@ } }, "node_modules/string.prototype.trim": { - "version": "1.2.10", - "resolved": "https://registry.npmjs.org/string.prototype.trim/-/string.prototype.trim-1.2.10.tgz", - "integrity": "sha512-Rs66F0P/1kedk5lyYyH9uBzuiI/kNRmwJAR9quK6VOtIpZ2G+hMZd+HQbbv25MgCA6gEffoMZYxlTod4WcdrKA==", + "version": "1.2.11", + "resolved": "https://registry.npmjs.org/string.prototype.trim/-/string.prototype.trim-1.2.11.tgz", + "integrity": "sha512-PwvK7BU+CMTJGYQCTZb5RWXIML92lftJLhQz1tBzgKiqGxJaMlBAa48POXaNAC2s4y8jr3EFqrkF9+44neS46w==", "dev": true, "license": "MIT", "dependencies": { - "call-bind": "^1.0.8", - "call-bound": "^1.0.2", + "call-bind": "^1.0.9", + "call-bound": "^1.0.4", "define-data-property": "^1.1.4", "define-properties": "^1.2.1", - "es-abstract": "^1.23.5", - "es-object-atoms": "^1.0.0", - "has-property-descriptors": "^1.0.2" + "es-abstract": "^1.24.2", + "es-object-atoms": "^1.1.2", + "has-property-descriptors": "^1.0.2", + "safe-regex-test": "^1.1.0" }, "engines": { "node": ">= 0.4" @@ -6412,16 +6345,16 @@ } }, "node_modules/string.prototype.trimend": { - "version": "1.0.9", - "resolved": "https://registry.npmjs.org/string.prototype.trimend/-/string.prototype.trimend-1.0.9.tgz", - "integrity": "sha512-G7Ok5C6E/j4SGfyLCloXTrngQIQU3PWtXGst3yM7Bea9FRURf1S42ZHlZZtsNque2FN2PoUhfZXYLNWwEr4dLQ==", + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/string.prototype.trimend/-/string.prototype.trimend-1.0.10.tgz", + "integrity": "sha512-2+3aDAOmPTmuFwjDnmJG2ctEkQKVki7vOSqaxkv42Mowj1V6PnvuwFCRrR5lChUux1TBskPjfkeTOhqczDMxTw==", "dev": true, "license": "MIT", "dependencies": { - "call-bind": "^1.0.8", - "call-bound": "^1.0.2", + "call-bind": "^1.0.9", + "call-bound": "^1.0.4", "define-properties": "^1.2.1", - "es-object-atoms": "^1.0.0" + "es-object-atoms": "^1.1.2" }, "engines": { "node": ">= 0.4" @@ -6449,16 +6382,19 @@ } }, "node_modules/strip-ansi": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-4.0.0.tgz", - "integrity": "sha512-4XaJ2zQdCzROZDivEVIDPkcQn8LMFSa8kj8Gxb/Lnwzv9A8VctNZ+lfivC/sV3ivW8ElJTERXZoPBRrZKkNKow==", + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.2.0.tgz", + "integrity": "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w==", "dev": true, "license": "MIT", "dependencies": { - "ansi-regex": "^3.0.0" + "ansi-regex": "^6.2.2" }, "engines": { - "node": ">=4" + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" } }, "node_modules/strip-ansi-cjs": { @@ -6509,25 +6445,32 @@ } }, "node_modules/strip-json-comments": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", - "integrity": "sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==", + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", + "dev": true, "license": "MIT", "engines": { - "node": ">=0.10.0" + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, "node_modules/supports-color": { - "version": "5.4.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.4.0.tgz", - "integrity": "sha512-zjaXglF5nnWpsq470jSv6P9DwPvgLkuapYmfDm3JWOm0vkNTVF2tI4UrN2r6jH1qM/uc/WtxYY1hYoA2dOKj5w==", + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", + "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", "dev": true, "license": "MIT", "dependencies": { - "has-flag": "^3.0.0" + "has-flag": "^4.0.0" }, "engines": { - "node": ">=4" + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/supports-color?sponsor=1" } }, "node_modules/supports-hyperlinks": { @@ -6544,16 +6487,6 @@ "node": ">=8" } }, - "node_modules/supports-hyperlinks/node_modules/has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, "node_modules/supports-hyperlinks/node_modules/supports-color": { "version": "7.2.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", @@ -6608,32 +6541,129 @@ "node": ">=4.0.0" } }, - "node_modules/tar-fs": { - "version": "2.1.4", - "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.4.tgz", - "integrity": "sha512-mDAjwmZdh7LTT6pNleZ05Yt65HC3E+NiQzl672vQG38jIrehtJk/J3mNwIg+vShQPcLF/LV7CMnDW6vjj6sfYQ==", + "node_modules/table/node_modules/ansi-regex": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-3.0.1.tgz", + "integrity": "sha512-+O9Jct8wf++lXxxFc4hc8LsjaSq0HFzzL7cVsw8pRDIPdjKD2mT4ytDZlLuSBZ4cLKZFXIrMGO7DbQCtMJJMKw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/table/node_modules/ansi-styles": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", + "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^1.9.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/table/node_modules/chalk": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/table/node_modules/color-convert": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", + "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "1.1.3" + } + }, + "node_modules/table/node_modules/color-name": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", + "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==", + "dev": true, + "license": "MIT" + }, + "node_modules/table/node_modules/escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/table/node_modules/has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/table/node_modules/is-fullwidth-code-point": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz", + "integrity": "sha512-VHskAKYM8RfSFXwee5t5cbN5PZeq1Wrh6qd5bkyiXIf6UQcN6w/A0eXM9r6t8d+GYOh+o6ZhiEnb88LN/Y8m2w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/table/node_modules/string-width": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-2.1.1.tgz", + "integrity": "sha512-nOqH59deCq9SRHlxq1Aw85Jnt4w6KvLKqWVik6oA9ZklXLNIOlqg4F2yrT1MVaTjAqvVwdfeZ7w7aCvJD7ugkw==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-fullwidth-code-point": "^2.0.0", + "strip-ansi": "^4.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/table/node_modules/strip-ansi": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-4.0.0.tgz", + "integrity": "sha512-4XaJ2zQdCzROZDivEVIDPkcQn8LMFSa8kj8Gxb/Lnwzv9A8VctNZ+lfivC/sV3ivW8ElJTERXZoPBRrZKkNKow==", + "dev": true, "license": "MIT", "dependencies": { - "chownr": "^1.1.1", - "mkdirp-classic": "^0.5.2", - "pump": "^3.0.0", - "tar-stream": "^2.1.4" + "ansi-regex": "^3.0.0" + }, + "engines": { + "node": ">=4" } }, - "node_modules/tar-stream": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-2.2.0.tgz", - "integrity": "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==", + "node_modules/table/node_modules/supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "dev": true, "license": "MIT", "dependencies": { - "bl": "^4.0.3", - "end-of-stream": "^1.4.1", - "fs-constants": "^1.0.0", - "inherits": "^2.0.3", - "readable-stream": "^3.1.1" + "has-flag": "^3.0.0" }, "engines": { - "node": ">=6" + "node": ">=4" } }, "node_modules/text-table": { @@ -6774,16 +6804,6 @@ "node": ">=8" } }, - "node_modules/tsd/node_modules/p-try": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", - "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, "node_modules/tsd/node_modules/parse-json": { "version": "5.2.0", "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz", @@ -6803,16 +6823,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/tsd/node_modules/path-exists": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", - "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, "node_modules/tsd/node_modules/read-pkg": { "version": "5.2.0", "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-5.2.0.tgz", @@ -6867,18 +6877,6 @@ "node": ">=8" } }, - "node_modules/tunnel-agent": { - "version": "0.6.0", - "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", - "integrity": "sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==", - "license": "Apache-2.0", - "dependencies": { - "safe-buffer": "^5.0.1" - }, - "engines": { - "node": "*" - } - }, "node_modules/type-check": { "version": "0.3.2", "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.3.2.tgz", @@ -6977,18 +6975,18 @@ } }, "node_modules/typed-array-length": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/typed-array-length/-/typed-array-length-1.0.7.tgz", - "integrity": "sha512-3KS2b+kL7fsuk/eJZ7EQdnEmQoaho/r6KUef7hxvltNA5DR8NAUM+8wJMbJyZ4G9/7i3v5zPBIMN5aybAh2/Jg==", + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/typed-array-length/-/typed-array-length-1.0.8.tgz", + "integrity": "sha512-phPGCwqr2+Qo0fwniCE8e4pKnGu/yFb5nD5Y8bf0EEeiI5GklnACYA9GFy/DrAeRrKHXvHn+1SUsOWgJp6RO+g==", "dev": true, "license": "MIT", "dependencies": { - "call-bind": "^1.0.7", - "for-each": "^0.3.3", - "gopd": "^1.0.1", - "is-typed-array": "^1.1.13", - "possible-typed-array-names": "^1.0.0", - "reflect.getprototypeof": "^1.0.6" + "call-bind": "^1.0.9", + "for-each": "^0.3.5", + "gopd": "^1.2.0", + "is-typed-array": "^1.1.15", + "possible-typed-array-names": "^1.1.0", + "reflect.getprototypeof": "^1.0.10" }, "engines": { "node": ">= 0.4" @@ -7057,12 +7055,6 @@ "punycode": "^2.1.0" } }, - "node_modules/util-deprecate": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", - "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", - "license": "MIT" - }, "node_modules/utils-merge": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", @@ -7095,16 +7087,19 @@ } }, "node_modules/which": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz", - "integrity": "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==", + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", "dev": true, "license": "ISC", "dependencies": { "isexe": "^2.0.0" }, "bin": { - "which": "bin/which" + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" } }, "node_modules/which-boxed-primitive": { @@ -7175,14 +7170,14 @@ } }, "node_modules/which-typed-array": { - "version": "1.1.19", - "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.19.tgz", - "integrity": "sha512-rEvr90Bck4WZt9HHFC4DJMsjvu7x+r6bImz0/BrbWb7A2djJ8hnZMrWnHo9F8ssv0OMErasDhftrfROTyqSDrw==", + "version": "1.1.22", + "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.22.tgz", + "integrity": "sha512-fvO4ExWMFsqyhG3AiPAObMuY1lxaqgYcxbc49CNdWDDECOJNgQyvsOWVwbZc+qf3rzRtxojBK+CMEv0Ld5CYpw==", "dev": true, "license": "MIT", "dependencies": { "available-typed-arrays": "^1.0.7", - "call-bind": "^1.0.8", + "call-bind": "^1.0.9", "call-bound": "^1.0.4", "for-each": "^0.3.5", "get-proto": "^1.0.1", @@ -7214,18 +7209,18 @@ "license": "Apache-2.0" }, "node_modules/wrap-ansi": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", - "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", + "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", "dev": true, "license": "MIT", "dependencies": { - "ansi-styles": "^4.0.0", - "string-width": "^4.1.0", - "strip-ansi": "^6.0.0" + "ansi-styles": "^6.1.0", + "string-width": "^5.0.1", + "strip-ansi": "^7.0.1" }, "engines": { - "node": ">=10" + "node": ">=12" }, "funding": { "url": "https://github.com/chalk/wrap-ansi?sponsor=1" @@ -7260,52 +7255,13 @@ "node": ">=8" } }, - "node_modules/wrap-ansi-cjs/node_modules/ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, - "license": "MIT", - "dependencies": { - "color-convert": "^2.0.1" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/wrap-ansi-cjs/node_modules/color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "color-name": "~1.1.4" - }, - "engines": { - "node": ">=7.0.0" - } - }, - "node_modules/wrap-ansi-cjs/node_modules/color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "node_modules/wrap-ansi-cjs/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", "dev": true, "license": "MIT" }, - "node_modules/wrap-ansi-cjs/node_modules/is-fullwidth-code-point": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", - "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, "node_modules/wrap-ansi-cjs/node_modules/string-width": { "version": "4.2.3", "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", @@ -7334,94 +7290,24 @@ "node": ">=8" } }, - "node_modules/wrap-ansi/node_modules/ansi-regex": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", - "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, "node_modules/wrap-ansi/node_modules/ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "version": "6.2.3", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", + "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", "dev": true, "license": "MIT", - "dependencies": { - "color-convert": "^2.0.1" - }, "engines": { - "node": ">=8" + "node": ">=12" }, "funding": { "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, - "node_modules/wrap-ansi/node_modules/color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "color-name": "~1.1.4" - }, - "engines": { - "node": ">=7.0.0" - } - }, - "node_modules/wrap-ansi/node_modules/color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true, - "license": "MIT" - }, - "node_modules/wrap-ansi/node_modules/is-fullwidth-code-point": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", - "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/wrap-ansi/node_modules/string-width": { - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", - "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", - "dev": true, - "license": "MIT", - "dependencies": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/wrap-ansi/node_modules/strip-ansi": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", - "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-regex": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, "node_modules/wrappy": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "dev": true, "license": "ISC" }, "node_modules/write": { @@ -7465,9 +7351,9 @@ "license": "ISC" }, "node_modules/yargs": { - "version": "17.7.2", - "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", - "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", + "version": "17.7.3", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.3.tgz", + "integrity": "sha512-GZtjxm/J/4TSxuL3FNYjCmLktBTnIw/rVmKSIyKeYAZpmJB2ig9VauCC5xsa82GNKVKDAqpOn3KVzNt0zmrU0g==", "dev": true, "license": "MIT", "dependencies": { @@ -7484,13 +7370,13 @@ } }, "node_modules/yargs-parser": { - "version": "20.2.9", - "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.9.tgz", - "integrity": "sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w==", + "version": "21.1.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", + "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", "dev": true, "license": "ISC", "engines": { - "node": ">=10" + "node": ">=12" } }, "node_modules/yargs-unparser": { @@ -7555,15 +7441,12 @@ "node": ">=8" } }, - "node_modules/yargs/node_modules/is-fullwidth-code-point": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", - "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "node_modules/yargs/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } + "license": "MIT" }, "node_modules/yargs/node_modules/string-width": { "version": "4.2.3", @@ -7593,16 +7476,6 @@ "node": ">=8" } }, - "node_modules/yargs/node_modules/yargs-parser": { - "version": "21.1.1", - "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", - "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", - "dev": true, - "license": "ISC", - "engines": { - "node": ">=12" - } - }, "node_modules/yocto-queue": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", @@ -7615,6 +7488,136 @@ "funding": { "url": "https://github.com/sponsors/sindresorhus" } + }, + "npm/darwin-arm64": { + "name": "canvas-darwin-arm64", + "version": "3.2.3", + "cpu": [ + "arm64" + ], + "os": [ + "darwin" + ] + }, + "npm/darwin-x64": { + "name": "canvas-darwin-x64", + "version": "3.2.3", + "cpu": [ + "x64" + ], + "os": [ + "darwin" + ] + }, + "npm/linux-arm-gnueabihf": { + "name": "canvas-linux-arm-gnueabihf", + "version": "3.2.3", + "cpu": [ + "arm" + ], + "os": [ + "linux" + ] + }, + "npm/linux-arm64-gnu": { + "name": "canvas-linux-arm64-gnu", + "version": "3.2.3", + "cpu": [ + "arm64" + ], + "os": [ + "linux" + ] + }, + "npm/linux-arm64-musl": { + "name": "canvas-linux-arm64-musl", + "version": "3.2.3", + "cpu": [ + "arm64" + ], + "os": [ + "linux" + ] + }, + "npm/linux-riscv64-gnu": { + "name": "canvas-linux-riscv64-gnu", + "version": "3.2.3", + "cpu": [ + "riscv64" + ], + "os": [ + "linux" + ] + }, + "npm/linux-x64-gnu": { + "name": "canvas-linux-x64-gnu", + "version": "3.2.3", + "cpu": [ + "x64" + ], + "os": [ + "linux" + ] + }, + "npm/linux-x64-musl": { + "name": "canvas-linux-x64-musl", + "version": "3.2.3", + "cpu": [ + "x64" + ], + "os": [ + "linux" + ] + }, + "npm/linux-x86-gnu": { + "name": "canvas-linux-x86-gnu", + "version": "3.2.3", + "cpu": [ + "ia32" + ], + "os": [ + "linux" + ] + }, + "npm/linux-x86-musl": { + "name": "canvas-linux-x86-musl", + "version": "3.2.3", + "cpu": [ + "ia32" + ], + "os": [ + "linux" + ] + }, + "npm/win32-arm64": { + "name": "canvas-win32-arm64", + "version": "3.2.3", + "cpu": [ + "arm64" + ], + "os": [ + "win32" + ] + }, + "npm/win32-x64": { + "name": "canvas-win32-x64", + "version": "3.2.3", + "cpu": [ + "x64" + ], + "os": [ + "win32" + ] + }, + "npm/win32-x86": { + "name": "canvas-win32-x86", + "version": "3.2.3", + "cpu": [ + "ia32" + ], + "os": [ + "win32" + ] } } } diff --git a/package.json b/package.json index 44e98875f..d0cce2ae6 100644 --- a/package.json +++ b/package.json @@ -24,30 +24,30 @@ "homepage": "https://github.com/Automattic/node-canvas", "repository": "git://github.com/Automattic/node-canvas.git", "scripts": { - "prebenchmark": "node-gyp build", + "prebenchmark": "zig build", "benchmark": "node benchmarks/run.js", "lint": "standard examples/*.js test/server.js test/public/*.js benchmarks/run.js lib/context2d.js util/has_lib.js browser.js index.js", "test": "mocha test/*.test.js", - "pretest-server": "node-gyp build", + "pretest-server": "zig build", "test-server": "node test/server.js", "generate-wpt": "node ./test/wpt/generate.js", "test-wpt": "mocha test/wpt/generated/*.js", - "install": "prebuild-install -r napi || node-gyp rebuild", "tsd": "tsd" }, "files": [ - "binding.gyp", + "build.zig", + "build.zig.zon", "browser.js", "index.d.ts", "index.js", "lib/", + "pkg/", "src/", "util/" ], - "dependencies": { - "node-addon-api": "^8.9.0", - "prebuild-install": "^7.1.3" - }, + "workspaces": [ + "./npm/*" + ], "devDependencies": { "@types/node": "^10.12.18", "assert-rejects": "^1.0.0", @@ -59,13 +59,23 @@ "tsd": "^0.29.0", "typescript": "^4.2.2" }, + "optionalDependencies": { + "canvas-darwin-arm64": "3.2.3", + "canvas-darwin-x64": "3.2.3", + "canvas-linux-arm-gnueabihf": "3.2.3", + "canvas-linux-x86-musl": "3.2.3", + "canvas-linux-x86-gnu": "3.2.3", + "canvas-linux-arm64-musl": "3.2.3", + "canvas-linux-arm64-gnu": "3.2.3", + "canvas-linux-riscv64-gnu": "3.2.3", + "canvas-linux-x64-musl": "3.2.3", + "canvas-linux-x64-gnu": "3.2.3", + "canvas-win32-arm64": "3.2.3", + "canvas-win32-x64": "3.2.3", + "canvas-win32-x86": "3.2.3" + }, "engines": { "node": "^18.12.0 || >= 20.9.0" }, - "binary": { - "napi_versions": [ - 7 - ] - }, "license": "MIT" } diff --git a/pkg/cairo/build.zig b/pkg/cairo/build.zig new file mode 100644 index 000000000..e74c9773e --- /dev/null +++ b/pkg/cairo/build.zig @@ -0,0 +1,267 @@ +const std = @import("std"); +const sources = @import("sources.zig"); + +pub fn build(b: *std.Build) !void { + const target = b.standardTargetOptions(.{}); + const optimize = b.standardOptimizeOption(.{}); + const cairo = b.dependency("cairo", .{}); + + const lib = b.addLibrary(.{ + .name = "cairo", + .root_module = b.createModule(.{ + .target = target, + .optimize = optimize, + .link_libc = true, + .pic = true, + }), + }); + + lib.link_data_sections = true; + lib.link_function_sections = true; + lib.addIncludePath(cairo.path("src")); + + var cairo_sources = std.ArrayList([]const u8).empty; + defer cairo_sources.deinit(b.allocator); + + try cairo_sources.appendSlice(b.allocator, sources.cairo); + + var c_flags = std.ArrayList([]const u8).empty; + defer c_flags.deinit(b.allocator); + try c_flags.appendSlice(b.allocator, &.{ + "-Wmissing-declarations", + "-Werror-implicit-function-declaration", + "-Wpointer-arith", + "-Wwrite-strings", + "-Wsign-compare", + "-Wpacked", + "-Wswitch-enum", + "-Wmissing-format-attribute", + "-Wvolatile-register-var", + "-Wstrict-aliasing=2", + "-Winit-self", + // "-Wunsafe-loop-optimizations", + "-Wno-missing-field-initializers", + "-Wno-unused-parameter", + "-Wno-attributes", + "-Wno-long-long", + "-Winline", + "-Wno-unused-but-set-variable", + "-Wno-enum-conversion", + "-fno-strict-aliasing", + "-fno-common", + "-D_GNU_SOURCE", + "-DCAIRO_COMPILATION", + "-fno-sanitize=undefined", + }); + if (optimize != .Debug) { + try c_flags.append(b.allocator, "-Wp,-D_FORTIFY_SOURCE=2"); + } + + const config = b.addConfigHeader(.{}, .{ + .HAVE_STDINT_H = 1, + .HAVE_INTTYPES_H = 1, + .HAVE_FCNTL_H = 1, + .HAVE_UNISTD_H = 1, + .HAVE_TIME_H = 1, + .HAVE_LIBGEN_H = 1, + .HAVE_SIGNAL_H = 1, + .HAVE_SETJMP_H = 1, + .HAVE_SYS_STAT_H = 1, + .HAVE_UINT64_T = 1, + .HAVE_ALARM = 1, + .HAVE_GETLINE = 1, + .HAVE_RAISE = 1, + .HAVE_STRTOD_L = 1, + .HAVE_CLOCK_GETTIME = 1, + .HAVE_C11_ATOMIC_PRIMITIVES = 1, + // 2 means variant 2 (see meson-cc-tests/mkdir-variant-2.c) + // aka `int mkdir(const char *, mode_t)` + .HAVE_MKDIR = 2, + .SIZEOF_VOID_P = target.result.ptrBitWidth() / 8, + .SIZEOF_INT = target.result.cTypeByteSize(.int), + .SIZEOF_LONG = target.result.cTypeByteSize(.long), + .SIZEOF_LONG_LONG = target.result.cTypeByteSize(.longlong), + .SIZEOF_SIZE_T = target.result.ptrBitWidth() / 8, + }); + + // not checking the real source of truth here, but works in practice + if (target.result.ptrBitWidth() == 64) { + config.addValues(.{ + .HAVE___UINT128_T = 1, + }); + } + + if (target.result.os.tag == .windows) + config.addValues(.{ + .HAVE_INTSAFE_H = 1, + }) + else + config.addValues(.{ + .HAVE_SYS_SOCKET_H = 1, + .HAVE_POLL_H = 1, + .HAVE_SYS_POLL_H = 1, + .HAVE_SYS_UN_H = 1, + .HAVE_SCHED_H = 1, + .HAVE_SYS_MMAN_H = 1, + .HAVE_SYS_IOCTL_H = 1, + .HAVE_TERMIOS_H = 1, + .HAVE_DRAND48 = 1, + .HAVE_LINK = 1, + .HAVE_FORK = 1, + .HAVE_NEWLOCALE = 1, + .HAVE_STRNDUP = 1, + .HAVE_MMAP = 1, + .HAVE_CTIME_R = 1, + .HAVE_LOCALTIME_R = 1, + .HAVE_GMTIME_R = 1, + }); + + if (target.result.os.tag == .linux or target.result.cpu.arch.isWasm()) + config.addValues(.{ + .HAVE_BYTESWAP_H = 1, + .HAVE_FENV_H = 1, + .HAVE_SYS_WAIT_H = 1, + .HAVE_ALLOCA_H = 1, + .HAVE_FLOCKFILE = 1, + .HAVE_FUNLOCKFILE = 1, + .HAVE_SCHED_GETAFFINITY = 1, + }); + + if (target.result.os.tag.isDarwin()) + config.addValues(.{ .HAVE_XLOCALE_H = 1 }) + else + config.addValues(.{ .HAVE_IO_H = 1 }); + + if (target.result.os.tag.isDarwin() or target.result.os.tag == .linux) + config.addValues(.{ .HAVE_WAITPID = 1 }); + + if (target.result.cpu.arch.endian() == .big) + config.addValues(.{ + .WORDS_BIGENDIAN = 1, + .FLOAT_WORDS_BIGENDIAN = 1, + }); + + const feature_config = b.addConfigHeader(.{ .include_path = "cairo-features.h" }, .{}); + + lib.linkSystemLibrary("m"); + + const zlib = b.dependency("zlib", .{ + .target = target, + .optimize = optimize, + }); + + lib.linkLibrary(zlib.artifact("z")); + + config.addValues(.{ + .HAVE_ZLIB = 1, + .CAIRO_HAS_INTERPRETER = 1, + .CAIRO_CAN_TEST_PS_SURFACE = 1, + }); + feature_config.addValues(.{ + .CAIRO_HAS_PS_SURFACE = 1, + .CAIRO_HAS_PDF_SURFACE = 1, + .CAIRO_HAS_SCRIPT_SURFACE = 1, + }); + + try cairo_sources.appendSlice(b.allocator, sources.script_surface); + const png = b.dependency("libpng", .{ + .target = target, + .optimize = optimize, + }); + + lib.linkLibrary(png.artifact("png")); + + config.addValues(.{ + .CAIRO_CAN_TEST_SVG_SURFACE = 1, + }); + feature_config.addValues(.{ + .CAIRO_HAS_SVG_SURFACE = 1, + .CAIRO_HAS_PNG_FUNCTIONS = 1, + }); + + try cairo_sources.appendSlice(b.allocator, sources.png); + + const freetype = b.dependency("freetype", .{ + .target = target, + .optimize = optimize, + }).artifact("freetype"); + lib.linkLibrary(freetype); + lib.installLibraryHeaders(freetype); + + feature_config.addValues(.{ .CAIRO_HAS_FT_FONT = 1 }); + config.addValues(.{ + .HAVE_FT_LOAD_NO_SVG = 1, + .HAVE_FT_SVG_DOCUMENT = 1, + .HAVE_FT_COLR_V1 = 1, + }); + + try cairo_sources.appendSlice(b.allocator, sources.freetype); + + if (target.result.os.tag == .windows) { + try c_flags.appendSlice(b.allocator, &.{ + "-DWIN32_LEAN_AND_MEAN", + "-DNOMINMAX", + "-DCAIRO_WIN32_STATIC_BUILD", + }); + + lib.linkSystemLibrary("gdi32"); + lib.linkSystemLibrary("msimg32"); + + feature_config.addValues(.{ + .CAIRO_HAS_WIN32_SURFACE = 1, + .CAIRO_HAS_WIN32_FONT = 1, + }); + } + + if (b.systemIntegrationOption("pixman", .{})) { + lib.linkSystemLibrary("pixman-1"); + } else if (b.lazyDependency("pixman", .{ + .target = target, + .optimize = optimize, + })) |dep| { + const pixman = dep.artifact("pixman"); + pixman.root_module.sanitize_c = .off; + lib.linkLibrary(pixman); + lib.installLibraryHeaders(pixman); + } + + feature_config.addValues(.{ .CAIRO_HAS_IMAGE_SURFACE = 1 }); + config.addValues(.{ .HAS_PIXMAN_GLYPHS = 1 }); + + feature_config.addValues(.{ + .CAIRO_HAS_USER_FONT = 1, + .CAIRO_HAS_MIME_SURFACE = 1, + .CAIRO_HAS_RECORDING_SURFACE = 1, + .CAIRO_HAS_OBSERVER_SURFACE = 1, + }); + + lib.linkSystemLibrary("pthread"); + + config.addValues(.{ + .CAIRO_HAS_PTHREAD = 1, + .CAIRO_HAS_REAL_PTHREAD = 1, + }); + + try c_flags.appendSlice(b.allocator, &.{ + "-pthread", + "-D_REENTRANT", + }); + + if (!target.result.cpu.arch.isX86()) + config.addValues(.{ .ATOMIC_OP_NEEDS_MEMORY_BARRIER = 1 }); + + lib.addConfigHeader(config); + lib.addConfigHeader(feature_config); + + lib.addCSourceFiles(.{ + .root = cairo.path("src"), + .files = try cairo_sources.toOwnedSlice(b.allocator), + .flags = c_flags.items, + }); + + lib.installHeadersDirectory(cairo.path("src"), "", .{}); + lib.installConfigHeader(config); + lib.installConfigHeader(feature_config); + + b.installArtifact(lib); +} diff --git a/pkg/cairo/build.zig.zon b/pkg/cairo/build.zig.zon new file mode 100644 index 000000000..ff5a94293 --- /dev/null +++ b/pkg/cairo/build.zig.zon @@ -0,0 +1,22 @@ +.{ + .name = .cairo_zig, + .version = "1.18.4", + .fingerprint = 0x71e19aedf148bc19, + .minimum_zig_version = "0.15.1", + .dependencies = .{ + .cairo = .{ + .url = "https://www.cairographics.org/releases/cairo-1.18.4.tar.xz", + .hash = "N-V-__8AAFW9nwOvQAmLG664NOHF67FmS00qy_Lhg72aEv9Q", + }, + .freetype = .{ .path = "../freetype" }, + .zlib = .{ .path = "../zlib" }, + .pixman = .{ .path = "../pixman" }, + .libpng = .{ .path = "../libpng" }, + }, + .paths = .{ + "build.zig", + "build.zig.zon", + "sources.zig", + "pkg", + }, +} diff --git a/pkg/cairo/sources.zig b/pkg/cairo/sources.zig new file mode 100644 index 000000000..ab4e8e90a --- /dev/null +++ b/pkg/cairo/sources.zig @@ -0,0 +1,138 @@ +pub const cairo = &.{ + "cairo.c", + "cairo-analysis-surface.c", + "cairo-arc.c", + "cairo-array.c", + "cairo-atomic.c", + "cairo-base64-stream.c", + "cairo-base85-stream.c", + "cairo-bentley-ottmann-rectangular.c", + "cairo-bentley-ottmann-rectilinear.c", + "cairo-bentley-ottmann.c", + "cairo-botor-scan-converter.c", + "cairo-boxes-intersect.c", + "cairo-boxes.c", + "cairo-cache.c", + "cairo-clip-boxes.c", + "cairo-clip-polygon.c", + "cairo-clip-region.c", + "cairo-clip-surface.c", + "cairo-clip-tor-scan-converter.c", + "cairo-clip.c", + "cairo-color.c", + "cairo-composite-rectangles.c", + "cairo-compositor.c", + "cairo-contour.c", + "cairo-damage.c", + "cairo-debug.c", + "cairo-default-context.c", + "cairo-device.c", + "cairo-error.c", + "cairo-fallback-compositor.c", + "cairo-fixed.c", + "cairo-font-face-twin-data.c", + "cairo-font-face-twin.c", + "cairo-font-face.c", + "cairo-font-options.c", + "cairo-freed-pool.c", + "cairo-freelist.c", + "cairo-gstate.c", + "cairo-hash.c", + "cairo-hull.c", + "cairo-image-compositor.c", + "cairo-image-info.c", + "cairo-image-source.c", + "cairo-image-surface.c", + "cairo-line.c", + "cairo-lzw.c", + "cairo-mask-compositor.c", + "cairo-matrix.c", + "cairo-mempool.c", + "cairo-mesh-pattern-rasterizer.c", + "cairo-misc.c", + "cairo-mono-scan-converter.c", + "cairo-mutex.c", + "cairo-no-compositor.c", + "cairo-observer.c", + "cairo-output-stream.c", + "cairo-paginated-surface.c", + "cairo-path-bounds.c", + "cairo-path-fill.c", + "cairo-path-fixed.c", + "cairo-path-in-fill.c", + "cairo-path-stroke-boxes.c", + "cairo-path-stroke-polygon.c", + "cairo-path-stroke-traps.c", + "cairo-path-stroke-tristrip.c", + "cairo-path-stroke.c", + "cairo-path.c", + "cairo-pattern.c", + "cairo-pen.c", + "cairo-polygon-intersect.c", + "cairo-polygon-reduce.c", + "cairo-polygon.c", + "cairo-raster-source-pattern.c", + "cairo-recording-surface.c", + "cairo-rectangle.c", + "cairo-rectangular-scan-converter.c", + "cairo-region.c", + "cairo-rtree.c", + "cairo-scaled-font.c", + "cairo-shape-mask-compositor.c", + "cairo-slope.c", + "cairo-spans-compositor.c", + "cairo-spans.c", + "cairo-spline.c", + "cairo-stroke-dash.c", + "cairo-stroke-style.c", + "cairo-surface-clipper.c", + "cairo-surface-fallback.c", + "cairo-surface-observer.c", + "cairo-surface-offset.c", + "cairo-surface-snapshot.c", + "cairo-surface-subsurface.c", + "cairo-surface-wrapper.c", + "cairo-surface.c", + "cairo-time.c", + "cairo-tor-scan-converter.c", + "cairo-tor22-scan-converter.c", + "cairo-toy-font-face.c", + "cairo-traps-compositor.c", + "cairo-traps.c", + "cairo-tristrip.c", + "cairo-unicode.c", + "cairo-user-font.c", + "cairo-version.c", + "cairo-wideint.c", + "cairo.c", + "cairo-cff-subset.c", + "cairo-scaled-font-subsets.c", + "cairo-truetype-subset.c", + "cairo-type1-fallback.c", + "cairo-type1-glyph-names.c", + "cairo-type1-subset.c", + "cairo-type3-glyph-surface.c", + "cairo-pdf-operators.c", + "cairo-pdf-shading.c", + "cairo-tag-attributes.c", + "cairo-tag-stack.c", + "cairo-deflate-stream.c", +}; + +pub const script_surface = &.{ + "cairo-script-surface.c", + "cairo-ps-surface.c", + "cairo-pdf-surface.c", + "cairo-pdf-interchange.c", +}; + +pub const png = &.{ + "cairo-png.c", + "cairo-svg-surface.c", +}; + +pub const freetype = &.{ + "cairo-ft-font.c", + "cairo-colr-glyph-render.c", + "cairo-svg-glyph-render.c", +}; diff --git a/pkg/freetype/build.zig b/pkg/freetype/build.zig new file mode 100644 index 000000000..83de08398 --- /dev/null +++ b/pkg/freetype/build.zig @@ -0,0 +1,124 @@ +const std = @import("std"); + +pub fn build(b: *std.Build) !void { + const target = b.standardTargetOptions(.{}); + const optimize = b.standardOptimizeOption(.{}); + + const upstream = b.dependency("freetype", .{}); + const lib = b.addLibrary(.{ + .name = "freetype", + .linkage = .static, + .root_module = b.createModule(.{ + .target = target, + .optimize = optimize, + .link_libc = true, + .pic = true, + }), + }); + + lib.addIncludePath(upstream.path("include")); + lib.installHeadersDirectory(upstream.path("include"), "", .{}); + + // Dependencies + const zlib_dep = b.dependency("zlib", .{ .target = target, .optimize = optimize }); + lib.root_module.linkLibrary(zlib_dep.artifact("z")); + + const libpng_dep = b.dependency("libpng", .{ .target = target, .optimize = optimize }); + lib.root_module.linkLibrary(libpng_dep.artifact("png")); + + var flags: std.ArrayList([]const u8) = .empty; + defer flags.deinit(b.allocator); + + try flags.appendSlice(b.allocator, &.{ + "-DFT2_BUILD_LIBRARY", + + "-DFT_CONFIG_OPTION_SYSTEM_ZLIB", + "-DFT_CONFIG_OPTION_USE_PNG", + + "-DHAVE_UNISTD_H", + "-DHAVE_FCNTL_H", + + "-fno-sanitize=undefined", + }); + + lib.root_module.addCSourceFiles(.{ + .root = upstream.path(""), + .files = srcs, + .flags = flags.items, + }); + + switch (target.result.os.tag) { + .linux => lib.root_module.addCSourceFile(.{ + .file = upstream.path("builds/unix/ftsystem.c"), + .flags = flags.items, + }), + .windows => lib.root_module.addCSourceFile(.{ + .file = upstream.path("builds/windows/ftsystem.c"), + .flags = flags.items, + }), + else => lib.root_module.addCSourceFile(.{ + .file = upstream.path("src/base/ftsystem.c"), + .flags = flags.items, + }), + } + switch (target.result.os.tag) { + .windows => { + lib.root_module.addCSourceFile(.{ + .file = upstream.path("builds/windows/ftdebug.c"), + .flags = flags.items, + }); + lib.root_module.addWin32ResourceFile(.{ + .file = upstream.path("src/base/ftver.rc"), + }); + }, + else => lib.root_module.addCSourceFile(.{ + .file = upstream.path("src/base/ftdebug.c"), + .flags = flags.items, + }), + } + + b.installArtifact(lib); +} + +const srcs: []const []const u8 = &.{ + "src/autofit/autofit.c", + "src/base/ftbase.c", + "src/base/ftbbox.c", + "src/base/ftbdf.c", + "src/base/ftbitmap.c", + "src/base/ftcid.c", + "src/base/ftfstype.c", + "src/base/ftgasp.c", + "src/base/ftglyph.c", + "src/base/ftgxval.c", + "src/base/ftinit.c", + "src/base/ftmm.c", + "src/base/ftotval.c", + "src/base/ftpatent.c", + "src/base/ftpfr.c", + "src/base/ftstroke.c", + "src/base/ftsynth.c", + "src/base/fttype1.c", + "src/base/ftwinfnt.c", + "src/bdf/bdf.c", + "src/bzip2/ftbzip2.c", + "src/cache/ftcache.c", + "src/cff/cff.c", + "src/cid/type1cid.c", + "src/gzip/ftgzip.c", + "src/lzw/ftlzw.c", + "src/pcf/pcf.c", + "src/pfr/pfr.c", + "src/psaux/psaux.c", + "src/pshinter/pshinter.c", + "src/psnames/psnames.c", + "src/raster/raster.c", + "src/sdf/sdf.c", + "src/sfnt/sfnt.c", + "src/smooth/smooth.c", + "src/svg/svg.c", + "src/truetype/truetype.c", + "src/type1/type1.c", + "src/type42/type42.c", + "src/winfonts/winfnt.c", +}; diff --git a/pkg/freetype/build.zig.zon b/pkg/freetype/build.zig.zon new file mode 100644 index 000000000..0ca3aeed4 --- /dev/null +++ b/pkg/freetype/build.zig.zon @@ -0,0 +1,20 @@ +.{ + .name = .freetype, + .version = "0.0.0", + .dependencies = .{ + .freetype = .{ + .url = "https://github.com/freetype/freetype/archive/refs/tags/VER-2-14-1.tar.gz", + .hash = "N-V-__8AAJx1qgDwI7KTM6yDZ4ArrZU6qmKKuvDHRVrxUkTw", + }, + // TODO: this needs to be kept in sync with the root build.zig.zon + .zlib = .{ + .url = "git+https://github.com/allyourcodebase/zlib#3599c16d41dbe749ae51b0ff7ab864c61adc779a", + .hash = "zlib-1.3.1-ZZQ7lc8NAABUbHzDe_cSWboCqMbrLkVwvFkKnojgeiT2", + }, + // TODO: this needs to be kept in sync with the root build.zig.zon + .libpng = .{ + .url = "https://github.com/allyourcodebase/libpng/archive/d512607515687aa60b975d6a191aef9a692dac87.zip", + .hash = "libpng-1.6.50-oiaFGt4qAAB_vzrtvz9hA7_UiBW4arSaMVGc7ekKMCZ2", + }, + }, +} diff --git a/pkg/giflib/build.zig b/pkg/giflib/build.zig new file mode 100644 index 000000000..6cf32e15a --- /dev/null +++ b/pkg/giflib/build.zig @@ -0,0 +1,41 @@ +const std = @import("std"); + +pub fn build(b: *std.Build) void { + const target = b.standardTargetOptions(.{}); + const optimize = b.standardOptimizeOption(.{}); + + const giflib = b.addLibrary(.{ + .name = "giflib", + .linkage = .static, + .root_module = b.createModule(.{ + .target = target, + .optimize = optimize, + .link_libc = true, + .pic = true, + }) + }); + + const upstream = b.dependency("giflib", .{}); + + giflib.root_module.addCSourceFiles(.{ + .root = upstream.path("."), + .files = &.{ + "dgif_lib.c", + "egif_lib.c", + "gifalloc.c", + "gif_err.c", + "gif_font.c", + "gif_hash.c", + "openbsd-reallocarray.c", + } , + .flags = &.{ + "-std=gnu99", + "-Wall", + "-Wno-format-truncation", + } + }); + + giflib.installHeadersDirectory(upstream.path("."), "", .{}); + + b.installArtifact(giflib); +} diff --git a/pkg/giflib/build.zig.zon b/pkg/giflib/build.zig.zon new file mode 100644 index 000000000..b624e5442 --- /dev/null +++ b/pkg/giflib/build.zig.zon @@ -0,0 +1,13 @@ +.{ + .name = .giflib, + .version = "5.2.2", + .fingerprint = 0x7812c17c3a32a94d, + .minimum_zig_version = "0.15.2", + .dependencies = .{ + .giflib = .{ + .url = "https://downloads.sourceforge.net/project/giflib/giflib-5.x/giflib-5.2.2.tar.gz", + .hash = "N-V-__8AAI4GIwD_4QAQ2PcKUSZ8F2XH_vDa4da8cUxuuOU9", + }, + }, + .paths = .{""}, +} diff --git a/pkg/libpng/LICENSE b/pkg/libpng/LICENSE new file mode 100644 index 000000000..5baa1c05b --- /dev/null +++ b/pkg/libpng/LICENSE @@ -0,0 +1,20 @@ +Copyright © Mitchell Hashimoto +Copyright © Andrew Kelley + +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the “Software”), to deal in +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies +of the Software, and to permit persons to whom the Software is furnished to do +so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/pkg/libpng/build.zig b/pkg/libpng/build.zig new file mode 100644 index 000000000..29f6ffb16 --- /dev/null +++ b/pkg/libpng/build.zig @@ -0,0 +1,69 @@ +const std = @import("std"); + +pub fn build(b: *std.Build) !void { + const target = b.standardTargetOptions(.{}); + const optimize = b.standardOptimizeOption(.{}); + + const upstream = b.dependency("libpng", .{}); + + const mod = b.createModule(.{ + .target = target, + .optimize = optimize, + .link_libc = true, + .pic = true, + }); + + const lib = b.addLibrary(.{ + .name = "png", + .linkage = .static, + .root_module = mod, + }); + + const zlib_dep = b.dependency("zlib", .{ .target = target, .optimize = optimize }); + mod.linkLibrary(zlib_dep.artifact("z")); + mod.addIncludePath(upstream.path("")); + mod.addIncludePath(b.path("")); + + var flags: std.ArrayList([]const u8) = .empty; + defer flags.deinit(b.allocator); + + try flags.appendSlice(b.allocator, &.{ + "-DPNG_ARM_NEON_OPT=0", + "-DPNG_POWERPC_VSX_OPT=0", + "-DPNG_INTEL_SSE_OPT=0", + "-DPNG_MIPS_MSA_OPT=0", + }); + + mod.addCSourceFiles(.{ + .root = upstream.path(""), + .files = srcs, + .flags = flags.items, + }); + + lib.installHeader(b.path("pnglibconf.h"), "pnglibconf.h"); + lib.installHeadersDirectory( + upstream.path("."), + "", + .{ .include_extensions = &.{".h"} }, + ); + + b.installArtifact(lib); +} + +const srcs: []const []const u8 = &.{ + "png.c", + "pngerror.c", + "pngget.c", + "pngmem.c", + "pngpread.c", + "pngread.c", + "pngrio.c", + "pngrtran.c", + "pngrutil.c", + "pngset.c", + "pngtrans.c", + "pngwio.c", + "pngwrite.c", + "pngwtran.c", + "pngwutil.c", +}; diff --git a/pkg/libpng/build.zig.zon b/pkg/libpng/build.zig.zon new file mode 100644 index 000000000..4e7bd7ab5 --- /dev/null +++ b/pkg/libpng/build.zig.zon @@ -0,0 +1,19 @@ +.{ + .name = .libpng, + .version = "1.6.57", + .minimum_zig_version = "0.15.1", + .fingerprint = 0xb7a09eb41a8526a2, + .paths = .{ + "LICENSE", + "build.zig", + "build.zig.zon", + "pnglibconf.h", + }, + .dependencies = .{ + .libpng = .{ + .url = "https://github.com/glennrp/libpng/archive/refs/tags/v1.6.57.tar.gz", + .hash = "N-V-__8AACSdYAB_UbYSe21ZcD-qFBX7twJ4mRDPTrekTXQF", + }, + .zlib = .{ .path = "../zlib" }, + }, +} diff --git a/pkg/libpng/pnglibconf.h b/pkg/libpng/pnglibconf.h new file mode 100644 index 000000000..b39c38c71 --- /dev/null +++ b/pkg/libpng/pnglibconf.h @@ -0,0 +1,219 @@ +/* pnglibconf.h - library build configuration */ + +/* libpng version 1.6.38.git */ + +/* Copyright (c) 2018-2020 Cosmin Truta */ +/* Copyright (c) 1998-2002,2004,2006-2018 Glenn Randers-Pehrson */ + +/* This code is released under the libpng license. */ +/* For conditions of distribution and use, see the disclaimer */ +/* and license in png.h */ + +/* pnglibconf.h */ +/* Machine generated file: DO NOT EDIT */ +/* Derived from: scripts/pnglibconf.dfa */ +#ifndef PNGLCONF_H +#define PNGLCONF_H +/* options */ +#define PNG_16BIT_SUPPORTED +#define PNG_ALIGNED_MEMORY_SUPPORTED +/*#undef PNG_ARM_NEON_API_SUPPORTED*/ +/*#undef PNG_ARM_NEON_CHECK_SUPPORTED*/ +#define PNG_BENIGN_ERRORS_SUPPORTED +#define PNG_BENIGN_READ_ERRORS_SUPPORTED +/*#undef PNG_BENIGN_WRITE_ERRORS_SUPPORTED*/ +#define PNG_BUILD_GRAYSCALE_PALETTE_SUPPORTED +#define PNG_CHECK_FOR_INVALID_INDEX_SUPPORTED +#define PNG_COLORSPACE_SUPPORTED +#define PNG_CONSOLE_IO_SUPPORTED +#define PNG_CONVERT_tIME_SUPPORTED +#define PNG_EASY_ACCESS_SUPPORTED +/*#undef PNG_ERROR_NUMBERS_SUPPORTED*/ +#define PNG_ERROR_TEXT_SUPPORTED +#define PNG_FIXED_POINT_SUPPORTED +#define PNG_FLOATING_ARITHMETIC_SUPPORTED +#define PNG_FLOATING_POINT_SUPPORTED +#define PNG_FORMAT_AFIRST_SUPPORTED +#define PNG_FORMAT_BGR_SUPPORTED +#define PNG_GAMMA_SUPPORTED +#define PNG_GET_PALETTE_MAX_SUPPORTED +#define PNG_HANDLE_AS_UNKNOWN_SUPPORTED +#define PNG_INCH_CONVERSIONS_SUPPORTED +#define PNG_INFO_IMAGE_SUPPORTED +#define PNG_IO_STATE_SUPPORTED +#define PNG_MNG_FEATURES_SUPPORTED +#define PNG_POINTER_INDEXING_SUPPORTED +/*#undef PNG_POWERPC_VSX_API_SUPPORTED*/ +/*#undef PNG_POWERPC_VSX_CHECK_SUPPORTED*/ +#define PNG_PROGRESSIVE_READ_SUPPORTED +#define PNG_READ_16BIT_SUPPORTED +#define PNG_READ_ALPHA_MODE_SUPPORTED +#define PNG_READ_ANCILLARY_CHUNKS_SUPPORTED +#define PNG_READ_BACKGROUND_SUPPORTED +#define PNG_READ_BGR_SUPPORTED +#define PNG_READ_CHECK_FOR_INVALID_INDEX_SUPPORTED +#define PNG_READ_COMPOSITE_NODIV_SUPPORTED +#define PNG_READ_COMPRESSED_TEXT_SUPPORTED +#define PNG_READ_EXPAND_16_SUPPORTED +#define PNG_READ_EXPAND_SUPPORTED +#define PNG_READ_FILLER_SUPPORTED +#define PNG_READ_GAMMA_SUPPORTED +#define PNG_READ_GET_PALETTE_MAX_SUPPORTED +#define PNG_READ_GRAY_TO_RGB_SUPPORTED +#define PNG_READ_INTERLACING_SUPPORTED +#define PNG_READ_INT_FUNCTIONS_SUPPORTED +#define PNG_READ_INVERT_ALPHA_SUPPORTED +#define PNG_READ_INVERT_SUPPORTED +#define PNG_READ_OPT_PLTE_SUPPORTED +#define PNG_READ_PACKSWAP_SUPPORTED +#define PNG_READ_PACK_SUPPORTED +#define PNG_READ_QUANTIZE_SUPPORTED +#define PNG_READ_RGB_TO_GRAY_SUPPORTED +#define PNG_READ_SCALE_16_TO_8_SUPPORTED +#define PNG_READ_SHIFT_SUPPORTED +#define PNG_READ_STRIP_16_TO_8_SUPPORTED +#define PNG_READ_STRIP_ALPHA_SUPPORTED +#define PNG_READ_SUPPORTED +#define PNG_READ_SWAP_ALPHA_SUPPORTED +#define PNG_READ_SWAP_SUPPORTED +#define PNG_READ_TEXT_SUPPORTED +#define PNG_READ_TRANSFORMS_SUPPORTED +#define PNG_READ_UNKNOWN_CHUNKS_SUPPORTED +#define PNG_READ_USER_CHUNKS_SUPPORTED +#define PNG_READ_USER_TRANSFORM_SUPPORTED +#define PNG_READ_bKGD_SUPPORTED +#define PNG_READ_cHRM_SUPPORTED +#define PNG_READ_eXIf_SUPPORTED +#define PNG_READ_gAMA_SUPPORTED +#define PNG_READ_hIST_SUPPORTED +#define PNG_READ_iCCP_SUPPORTED +#define PNG_READ_iTXt_SUPPORTED +#define PNG_READ_oFFs_SUPPORTED +#define PNG_READ_pCAL_SUPPORTED +#define PNG_READ_pHYs_SUPPORTED +#define PNG_READ_sBIT_SUPPORTED +#define PNG_READ_sCAL_SUPPORTED +#define PNG_READ_sPLT_SUPPORTED +#define PNG_READ_sRGB_SUPPORTED +#define PNG_READ_tEXt_SUPPORTED +#define PNG_READ_tIME_SUPPORTED +#define PNG_READ_tRNS_SUPPORTED +#define PNG_READ_zTXt_SUPPORTED +#define PNG_SAVE_INT_32_SUPPORTED +#define PNG_SAVE_UNKNOWN_CHUNKS_SUPPORTED +#define PNG_SEQUENTIAL_READ_SUPPORTED +#define PNG_SETJMP_SUPPORTED +#define PNG_SET_OPTION_SUPPORTED +#define PNG_SET_UNKNOWN_CHUNKS_SUPPORTED +#define PNG_SET_USER_LIMITS_SUPPORTED +#define PNG_SIMPLIFIED_READ_AFIRST_SUPPORTED +#define PNG_SIMPLIFIED_READ_BGR_SUPPORTED +#define PNG_SIMPLIFIED_READ_SUPPORTED +#define PNG_SIMPLIFIED_WRITE_AFIRST_SUPPORTED +#define PNG_SIMPLIFIED_WRITE_BGR_SUPPORTED +#define PNG_SIMPLIFIED_WRITE_STDIO_SUPPORTED +#define PNG_SIMPLIFIED_WRITE_SUPPORTED +#define PNG_STDIO_SUPPORTED +#define PNG_STORE_UNKNOWN_CHUNKS_SUPPORTED +#define PNG_TEXT_SUPPORTED +#define PNG_TIME_RFC1123_SUPPORTED +#define PNG_UNKNOWN_CHUNKS_SUPPORTED +#define PNG_USER_CHUNKS_SUPPORTED +#define PNG_USER_LIMITS_SUPPORTED +#define PNG_USER_MEM_SUPPORTED +#define PNG_USER_TRANSFORM_INFO_SUPPORTED +#define PNG_USER_TRANSFORM_PTR_SUPPORTED +#define PNG_WARNINGS_SUPPORTED +#define PNG_WRITE_16BIT_SUPPORTED +#define PNG_WRITE_ANCILLARY_CHUNKS_SUPPORTED +#define PNG_WRITE_BGR_SUPPORTED +#define PNG_WRITE_CHECK_FOR_INVALID_INDEX_SUPPORTED +#define PNG_WRITE_COMPRESSED_TEXT_SUPPORTED +#define PNG_WRITE_CUSTOMIZE_COMPRESSION_SUPPORTED +#define PNG_WRITE_CUSTOMIZE_ZTXT_COMPRESSION_SUPPORTED +#define PNG_WRITE_FILLER_SUPPORTED +#define PNG_WRITE_FILTER_SUPPORTED +#define PNG_WRITE_FLUSH_SUPPORTED +#define PNG_WRITE_GET_PALETTE_MAX_SUPPORTED +#define PNG_WRITE_INTERLACING_SUPPORTED +#define PNG_WRITE_INT_FUNCTIONS_SUPPORTED +#define PNG_WRITE_INVERT_ALPHA_SUPPORTED +#define PNG_WRITE_INVERT_SUPPORTED +#define PNG_WRITE_OPTIMIZE_CMF_SUPPORTED +#define PNG_WRITE_PACKSWAP_SUPPORTED +#define PNG_WRITE_PACK_SUPPORTED +#define PNG_WRITE_SHIFT_SUPPORTED +#define PNG_WRITE_SUPPORTED +#define PNG_WRITE_SWAP_ALPHA_SUPPORTED +#define PNG_WRITE_SWAP_SUPPORTED +#define PNG_WRITE_TEXT_SUPPORTED +#define PNG_WRITE_TRANSFORMS_SUPPORTED +#define PNG_WRITE_UNKNOWN_CHUNKS_SUPPORTED +#define PNG_WRITE_USER_TRANSFORM_SUPPORTED +#define PNG_WRITE_WEIGHTED_FILTER_SUPPORTED +#define PNG_WRITE_bKGD_SUPPORTED +#define PNG_WRITE_cHRM_SUPPORTED +#define PNG_WRITE_eXIf_SUPPORTED +#define PNG_WRITE_gAMA_SUPPORTED +#define PNG_WRITE_hIST_SUPPORTED +#define PNG_WRITE_iCCP_SUPPORTED +#define PNG_WRITE_iTXt_SUPPORTED +#define PNG_WRITE_oFFs_SUPPORTED +#define PNG_WRITE_pCAL_SUPPORTED +#define PNG_WRITE_pHYs_SUPPORTED +#define PNG_WRITE_sBIT_SUPPORTED +#define PNG_WRITE_sCAL_SUPPORTED +#define PNG_WRITE_sPLT_SUPPORTED +#define PNG_WRITE_sRGB_SUPPORTED +#define PNG_WRITE_tEXt_SUPPORTED +#define PNG_WRITE_tIME_SUPPORTED +#define PNG_WRITE_tRNS_SUPPORTED +#define PNG_WRITE_zTXt_SUPPORTED +#define PNG_bKGD_SUPPORTED +#define PNG_cHRM_SUPPORTED +#define PNG_eXIf_SUPPORTED +#define PNG_gAMA_SUPPORTED +#define PNG_hIST_SUPPORTED +#define PNG_iCCP_SUPPORTED +#define PNG_iTXt_SUPPORTED +#define PNG_oFFs_SUPPORTED +#define PNG_pCAL_SUPPORTED +#define PNG_pHYs_SUPPORTED +#define PNG_sBIT_SUPPORTED +#define PNG_sCAL_SUPPORTED +#define PNG_sPLT_SUPPORTED +#define PNG_sRGB_SUPPORTED +#define PNG_tEXt_SUPPORTED +#define PNG_tIME_SUPPORTED +#define PNG_tRNS_SUPPORTED +#define PNG_zTXt_SUPPORTED +/* end of options */ +/* settings */ +#define PNG_API_RULE 0 +#define PNG_DEFAULT_READ_MACROS 1 +#define PNG_GAMMA_THRESHOLD_FIXED 5000 +#define PNG_IDAT_READ_SIZE PNG_ZBUF_SIZE +#define PNG_INFLATE_BUF_SIZE 1024 +#define PNG_LINKAGE_API extern +#define PNG_LINKAGE_CALLBACK extern +#define PNG_LINKAGE_DATA extern +#define PNG_LINKAGE_FUNCTION extern +#define PNG_MAX_GAMMA_8 11 +#define PNG_QUANTIZE_BLUE_BITS 5 +#define PNG_QUANTIZE_GREEN_BITS 5 +#define PNG_QUANTIZE_RED_BITS 5 +#define PNG_TEXT_Z_DEFAULT_COMPRESSION (-1) +#define PNG_TEXT_Z_DEFAULT_STRATEGY 0 +#define PNG_USER_CHUNK_CACHE_MAX 1000 +#define PNG_USER_CHUNK_MALLOC_MAX 8000000 +#define PNG_USER_HEIGHT_MAX 1000000 +#define PNG_USER_WIDTH_MAX 1000000 +#define PNG_ZBUF_SIZE 8192 +#define PNG_ZLIB_VERNUM 0 /* unknown */ +#define PNG_Z_DEFAULT_COMPRESSION (-1) +#define PNG_Z_DEFAULT_NOFILTER_STRATEGY 0 +#define PNG_Z_DEFAULT_STRATEGY 1 +#define PNG_sCAL_PRECISION 5 +#define PNG_sRGB_PROFILE_CHECKS 2 +/* end of settings */ +#endif /* PNGLCONF_H */ diff --git a/pkg/pixman/build.zig b/pkg/pixman/build.zig new file mode 100644 index 000000000..2877db1d3 --- /dev/null +++ b/pkg/pixman/build.zig @@ -0,0 +1,67 @@ +const std = @import("std"); + +pub fn build(b: *std.Build) !void { + const upstream = b.dependency("pixman", .{}); + + const lib = b.addLibrary(.{ + .name = "pixman", + .root_module = b.createModule(.{ + .target = b.standardTargetOptions(.{}), + .optimize = b.standardOptimizeOption(.{}), + }), + }); + + lib.linkLibC(); + lib.root_module.addCMacro("HAVE_CONFIG_H", "1"); + + const config_h = b.addConfigHeader(.{ + .include_path = "config.h", + }, .{ + // Required to make pixman-private.h + .PACKAGE = "FOO", + .HAVE_PTHREADS = "1", + }); + lib.addConfigHeader(config_h); + + lib.addIncludePath(upstream.path("pixman")); + lib.installHeadersDirectory(upstream.path("pixman"), "", .{}); + lib.addCSourceFiles(.{ + .root = upstream.path("pixman"), + .files = sources, + }); + + b.installArtifact(lib); +} + +const sources = &.{ + "pixman.c", + "pixman-access.c", + "pixman-access-accessors.c", + "pixman-bits-image.c", + "pixman-combine32.c", + "pixman-combine-float.c", + "pixman-conical-gradient.c", + "pixman-filter.c", + "pixman-x86.c", + "pixman-mips.c", + "pixman-arm.c", + "pixman-ppc.c", + "pixman-edge.c", + "pixman-edge-accessors.c", + "pixman-fast-path.c", + "pixman-glyph.c", + "pixman-general.c", + "pixman-gradient-walker.c", + "pixman-image.c", + "pixman-implementation.c", + "pixman-linear-gradient.c", + "pixman-matrix.c", + "pixman-noop.c", + "pixman-radial-gradient.c", + "pixman-region16.c", + "pixman-region32.c", + "pixman-solid-fill.c", + "pixman-timer.c", + "pixman-trap.c", + "pixman-utils.c", +}; diff --git a/pkg/pixman/build.zig.zon b/pkg/pixman/build.zig.zon new file mode 100644 index 000000000..7da919bc9 --- /dev/null +++ b/pkg/pixman/build.zig.zon @@ -0,0 +1,10 @@ +.{ + .name = .pixman, + .version = "0.0.0", + .dependencies = .{ + .pixman = .{ + .url = "https://cairographics.org/releases/pixman-0.42.2.tar.gz", + .hash = "N-V-__8AACdzQgB3Xt60BggeOhQybqmqPiqfBgmabFAL8LPJ", + }, + }, +} diff --git a/pkg/zlib/LICENSE b/pkg/zlib/LICENSE new file mode 100644 index 000000000..06ce9e01b --- /dev/null +++ b/pkg/zlib/LICENSE @@ -0,0 +1,21 @@ +The MIT License (Expat) + +Copyright (c) contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/pkg/zlib/build.zig b/pkg/zlib/build.zig new file mode 100644 index 000000000..03894cae4 --- /dev/null +++ b/pkg/zlib/build.zig @@ -0,0 +1,49 @@ +const std = @import("std"); + +pub fn build(b: *std.Build) void { + const upstream = b.dependency("zlib", .{}); + const lib = b.addLibrary(.{ + .name = "z", + .linkage = .static, + .root_module = b.createModule(.{ + .target = b.standardTargetOptions(.{}), + .optimize = b.standardOptimizeOption(.{}), + .link_libc = true, + .pic = true, + }), + }); + + lib.root_module.addCSourceFiles(.{ + .root = upstream.path(""), + .files = &.{ + "adler32.c", + "crc32.c", + "deflate.c", + "infback.c", + "inffast.c", + "inflate.c", + "inftrees.c", + "trees.c", + "zutil.c", + "compress.c", + "uncompr.c", + "gzclose.c", + "gzlib.c", + "gzread.c", + "gzwrite.c", + }, + .flags = &.{ + "-DHAVE_SYS_TYPES_H", + "-DHAVE_STDINT_H", + "-DHAVE_STDDEF_H", + "-DZ_HAVE_UNISTD_H", + } + }); + lib.installHeadersDirectory(upstream.path(""), "", .{ + .include_extensions = &.{ + "zconf.h", + "zlib.h", + }, + }); + b.installArtifact(lib); +} diff --git a/pkg/zlib/build.zig.zon b/pkg/zlib/build.zig.zon new file mode 100644 index 000000000..902b1b713 --- /dev/null +++ b/pkg/zlib/build.zig.zon @@ -0,0 +1,17 @@ +.{ + .name = .zlib, + .version = "1.3.2", + .fingerprint = 0x73887d3a953b9465, + .minimum_zig_version = "0.15.2", + .dependencies = .{ + .zlib = .{ + .url = "https://github.com/madler/zlib/archive/refs/tags/v1.3.2.tar.gz", + .hash = "N-V-__8AAI4ANwDM8MWhHTUFLKfvLE_n2NQk6mh0sFDzhTqO", + }, + }, + .paths = .{ + "LICENSE", + "build.zig", + "build.zig.zon", + }, +} diff --git a/scripts/version.js b/scripts/version.js new file mode 100644 index 000000000..db6eb56fb --- /dev/null +++ b/scripts/version.js @@ -0,0 +1,68 @@ +const fs = require('fs'); +const path = require('path'); +const rVersion = /^(\d+)\.(\d+).(\d+)$/ +let __pjson; + +function getPackageJson() { + if (__pjson) return __pjson; + const buffer = fs.readFileSync(path.join(__dirname, "../package.json")); + __pjson = JSON.parse(buffer); + return __pjson; +} + +function getVersionOrExit() { + const pjson = getPackageJson(); + let version = process.argv[2]; + let match; + let parts; + + if (version === 'major') { + parts = rVersion.exec(pjson.version).slice(1); + parts[0]++; + } else if (version === 'minor') { + parts = rVersion.exec(pjson.version).slice(1); + parts[1]++; + } else if (version === 'patch') { + parts = rVersion.exec(pjson.version).slice(1); + parts[2]++; + } else if ((match = rVersion.exec(version))) { + parts = match.slice(1); + } + + if (parts) { + return parts.join('.'); + } else { + console.error('Usage: node scripts/version.js [major|minor|patch|]'); + process.exit(1); + } +} + +const version = getVersionOrExit(); +const pjson = getPackageJson(); +const wjsons = {}; + +pjson.version = version; + +for (const dep in pjson.optionalDependencies) { + const folder = dep.slice(7); // "canvas-" + try { + const buffer = fs.readFileSync(path.join(__dirname, `../npm/${folder}/package.json`)); + wjsons[dep] = JSON.parse(buffer); + } catch (e) { + console.error(`${dep} is in optionalDependencies, but npm/${folder}/package.json is not a JSON file`); + process.exit(1); + } + + pjson.optionalDependencies[dep] = version; + wjsons[dep].version = version; +} + +fs.writeFileSync(path.join(__dirname, "../package.json"), JSON.stringify(pjson, undefined, 2) + '\n'); +console.log(`canvas: ${version}`); + +for (const dep in wjsons) { + const folder = dep.slice(7); // "canvas-" + const wjson = wjsons[dep]; + fs.writeFileSync(path.join(__dirname, `../npm/${folder}/package.json`), JSON.stringify(wjson, undefined, 2) + '\n'); + console.log(`${dep}: ${version}`); +} diff --git a/src/Canvas.cc b/src/Canvas.cc index db86b2f8e..2e872e537 100644 --- a/src/Canvas.cc +++ b/src/Canvas.cc @@ -11,7 +11,6 @@ #include #include #include -#include #include "PNG.h" #include #include @@ -19,12 +18,8 @@ #include #include "Util.h" #include -#include "node_buffer.h" #include "FontParser.h" - -#ifdef HAVE_JPEG #include "JPEGStream.h" -#endif #define CAIRO_MAX_SIZE 32767 @@ -43,9 +38,7 @@ Canvas::Initialize(Napi::Env& env, Napi::Object& exports) { InstanceMethod<&Canvas::ToBuffer>("toBuffer", napi_default_method), InstanceMethod<&Canvas::StreamPNGSync>("streamPNGSync", napi_default_method), InstanceMethod<&Canvas::StreamPDFSync>("streamPDFSync", napi_default_method), -#ifdef HAVE_JPEG InstanceMethod<&Canvas::StreamJPEGSync>("streamJPEGSync", napi_default_method), -#endif InstanceAccessor<&Canvas::GetType>("type", napi_default_jsproperty), InstanceAccessor<&Canvas::GetStride>("stride", napi_default_jsproperty), InstanceAccessor<&Canvas::GetWidth, &Canvas::SetWidth>("width", napi_default_jsproperty), @@ -211,13 +204,11 @@ Canvas::ToPngBufferAsync(Closure* base) { closure); } -#ifdef HAVE_JPEG void Canvas::ToJpegBufferAsync(Closure* base) { JpegClosure* closure = static_cast(base); write_to_jpeg_buffer(closure->canvas->ensureSurface(), closure); } -#endif static void parsePNGArgs(Napi::Value arg, PngClosure& pngargs) { @@ -262,7 +253,6 @@ parsePNGArgs(Napi::Value arg, PngClosure& pngargs) { } } -#ifdef HAVE_JPEG static void parseJPEGArgs(Napi::Value arg, JpegClosure& jpegargs) { // "If Type(quality) is not Number, or if quality is outside that range, the // user agent must use its default quality value, as if the quality argument @@ -294,9 +284,6 @@ static void parseJPEGArgs(Napi::Value arg, JpegClosure& jpegargs) { } } } -#endif - -#if CAIRO_VERSION >= CAIRO_VERSION_ENCODE(1, 16, 0) static inline void setPdfMetaStr(cairo_surface_t* surf, Napi::Object opts, cairo_pdf_metadata_t t, const char* propName) { @@ -330,8 +317,6 @@ static void setPdfMetadata(Canvas* canvas, Napi::Object opts) { setPdfMetaDate(surf, opts, CAIRO_PDF_METADATA_MOD_DATE, "modDate"); } -#endif // CAIRO 16+ - /* * Converts/encodes data to a Buffer. Async when a callback function is passed. @@ -367,11 +352,9 @@ Canvas::ToBuffer(const Napi::CallbackInfo& info) { // mime type may be present, but it's not checked PdfSvgClosure* closure = static_cast(_closure); if (isPDF()) { -#if CAIRO_VERSION >= CAIRO_VERSION_ENCODE(1, 16, 0) if (info[1].IsObject()) { // toBuffer("application/pdf", config) setPdfMetadata(this, info[1].As()); } -#endif // CAIRO 16+ } cairo_surface_t *surf = ensureSurface(); @@ -390,10 +373,6 @@ Canvas::ToBuffer(const Napi::CallbackInfo& info) { if (info[0].StrictEquals(Napi::String::New(env, "raw"))) { cairo_surface_t *surface = ensureSurface(); cairo_surface_flush(surface); - if (nBytes() > node::Buffer::kMaxLength) { - Napi::Error::New(env, "Data exceeds maximum buffer length.").ThrowAsJavaScriptException(); - return env.Undefined(); - } return Napi::Buffer::Copy(env, cairo_image_surface_get_data(surface), nBytes()); } @@ -454,7 +433,6 @@ Canvas::ToBuffer(const Napi::CallbackInfo& info) { return env.Undefined(); } -#ifdef HAVE_JPEG // Sync JPEG Napi::Value jpegStr = Napi::String::New(env, "image/jpeg"); if (info[0].StrictEquals(jpegStr)) { @@ -490,7 +468,6 @@ Canvas::ToBuffer(const Napi::CallbackInfo& info) { worker->Queue(); return env.Undefined(); } -#endif return env.Undefined(); } @@ -595,11 +572,9 @@ Canvas::StreamPDFSync(const Napi::CallbackInfo& info) { return; } -#if CAIRO_VERSION >= CAIRO_VERSION_ENCODE(1, 16, 0) if (info[1].IsObject()) { setPdfMetadata(this, info[1].As()); } -#endif cairo_surface_finish(ensureSurface()); @@ -625,7 +600,6 @@ Canvas::StreamPDFSync(const Napi::CallbackInfo& info) { * Stream JPEG data synchronously. */ -#ifdef HAVE_JPEG static uint32_t getSafeBufSize(Canvas* canvas) { // Don't allow the buffer size to exceed the size of the canvas (#674) // TODO not sure if this is really correct, but it fixed #674 @@ -646,7 +620,6 @@ Canvas::StreamJPEGSync(const Napi::CallbackInfo& info) { uint32_t bufsize = getSafeBufSize(this); write_to_jpeg_stream(ensureSurface(), bufsize, &closure); } -#endif char * str_value(Napi::Maybe maybe, const char *fallback, bool can_be_number) { @@ -707,10 +680,8 @@ Canvas::approxBytesPerPixel() { case CAIRO_FORMAT_ARGB32: case CAIRO_FORMAT_RGB24: return 4; -#ifdef CAIRO_FORMAT_RGB30 case CAIRO_FORMAT_RGB30: return 3; -#endif case CAIRO_FORMAT_RGB16_565: return 2; case CAIRO_FORMAT_A8: diff --git a/src/Canvas.h b/src/Canvas.h index c4fab7217..9e619a105 100644 --- a/src/Canvas.h +++ b/src/Canvas.h @@ -7,11 +7,13 @@ struct PdfSvgClosure; #include "closure.h" #include -#include "dll_visibility.h" #include #include #include +#define DLL_PUBLIC __attribute__ ((visibility ("default"))) +#define DLL_LOCAL __attribute__ ((visibility ("hidden"))) + /* * Canvas types. */ diff --git a/src/CanvasRenderingContext2d.cc b/src/CanvasRenderingContext2d.cc index 3758135fe..5a3b4c346 100644 --- a/src/CanvasRenderingContext2d.cc +++ b/src/CanvasRenderingContext2d.cc @@ -121,10 +121,8 @@ Context2d::Initialize(Napi::Env& env, Napi::Object& exports) { InstanceMethod<&Context2d::CreatePattern>("createPattern", napi_default_method), InstanceMethod<&Context2d::CreateLinearGradient>("createLinearGradient", napi_default_method), InstanceMethod<&Context2d::CreateRadialGradient>("createRadialGradient", napi_default_method), - #if CAIRO_VERSION >= CAIRO_VERSION_ENCODE(1, 16, 0) InstanceMethod<&Context2d::BeginTag>("beginTag", napi_default_method), InstanceMethod<&Context2d::EndTag>("endTag", napi_default_method), - #endif InstanceAccessor<&Context2d::GetFormat>("pixelFormat", napi_default_jsproperty), InstanceAccessor<&Context2d::GetPatternQuality, &Context2d::SetPatternQuality>("patternQuality", napi_default_jsproperty), InstanceAccessor<&Context2d::GetImageSmoothingEnabled, &Context2d::SetImageSmoothingEnabled>("imageSmoothingEnabled", napi_default_jsproperty), @@ -186,9 +184,7 @@ Context2d::Context2d(const Napi::CallbackInfo& info) : Napi::ObjectWrap= CAIRO_VERSION_ENCODE(1, 16, 0) - void Context2d::BeginTag(const Napi::CallbackInfo& info) { std::string tagName = ""; @@ -2973,5 +2961,3 @@ Context2d::EndTag(const Napi::CallbackInfo& info) { cairo_tag_end(_context, tagName.c_str()); } - -#endif diff --git a/src/CanvasRenderingContext2d.h b/src/CanvasRenderingContext2d.h index 3f6611db6..91cdfcb53 100644 --- a/src/CanvasRenderingContext2d.h +++ b/src/CanvasRenderingContext2d.h @@ -144,10 +144,8 @@ class Context2d : public Napi::ObjectWrap { void SetCurrentTransform(const Napi::CallbackInfo& info, const Napi::Value& value); void SetFillStyle(const Napi::CallbackInfo& info, const Napi::Value& value); void SetStrokeStyle(const Napi::CallbackInfo& info, const Napi::Value& value); - #if CAIRO_VERSION >= CAIRO_VERSION_ENCODE(1, 16, 0) void BeginTag(const Napi::CallbackInfo& info); void EndTag(const Napi::CallbackInfo& info); - #endif inline void setContext(cairo_t *ctx) { _context = ctx; } inline cairo_t *context(){ return _context; } inline Canvas *canvas(){ return _canvas; } diff --git a/src/Image.cc b/src/Image.cc index 468a0f1cb..e02677534 100644 --- a/src/Image.cc +++ b/src/Image.cc @@ -8,7 +8,6 @@ #include #include #include -#include #include /* Cairo limit: @@ -16,22 +15,18 @@ */ static constexpr int canvas_max_side = (1 << 15) - 1; -#ifdef HAVE_GIF typedef struct { uint8_t *buf; unsigned len; unsigned pos; } gif_data_t; -#endif -#ifdef HAVE_JPEG #include struct canvas_jpeg_error_mgr: jpeg_error_mgr { Image* image; jmp_buf setjmp_buffer; }; -#endif /* * Read closure used by loadFromBuffer. @@ -275,16 +270,12 @@ Image::loadFromBuffer(uint8_t *buf, unsigned len) { if (isPNG(data)) return loadPNGFromBuffer(buf); if (isGIF(data)) { -#ifdef HAVE_GIF return loadGIFFromBuffer(buf, len); -#else this->errorInfo.set("node-canvas was built without GIF support"); return CAIRO_STATUS_READ_ERROR; -#endif } if (isJPEG(data)) { -#ifdef HAVE_JPEG if (DATA_IMAGE == data_mode) return loadJPEGFromBuffer(buf, len); if (DATA_MIME == data_mode) return decodeJPEGBufferIntoMimeSurface(buf, len); if ((DATA_IMAGE | DATA_MIME) == data_mode) { @@ -293,10 +284,6 @@ Image::loadFromBuffer(uint8_t *buf, unsigned len) { if (status) return status; return assignDataAsMime(buf, len, CAIRO_MIME_TYPE_JPEG); } -#else // HAVE_JPEG - this->errorInfo.set("node-canvas was built without JPEG support"); - return CAIRO_STATUS_READ_ERROR; -#endif } if (isBMP(buf, len)) @@ -402,21 +389,15 @@ Image::loadSurface() { if (isGIF(buf)) { -#ifdef HAVE_GIF return loadGIF(stream); -#else this->errorInfo.set("node-canvas was built without GIF support"); return CAIRO_STATUS_READ_ERROR; -#endif } if (isJPEG(buf)) { -#ifdef HAVE_JPEG return loadJPEG(stream); -#else this->errorInfo.set("node-canvas was built without JPEG support"); return CAIRO_STATUS_READ_ERROR; -#endif } if (isBMP(buf, 2)) @@ -439,8 +420,6 @@ Image::loadPNG() { // GIF support -#ifdef HAVE_GIF - /* * Return the alpha color for `gif` at `frame`, or -1. */ @@ -514,14 +493,9 @@ Image::loadGIFFromBuffer(uint8_t *buf, unsigned len) { gif_data_t gifd = { buf, len, 0 }; -#if GIFLIB_MAJOR >= 5 int errorcode; if ((gif = DGifOpen((void*) &gifd, read_gif_from_memory, &errorcode)) == NULL) return CAIRO_STATUS_READ_ERROR; -#else - if ((gif = DGifOpen((void*) &gifd, read_gif_from_memory)) == NULL) - return CAIRO_STATUS_READ_ERROR; -#endif if (GIF_OK != DGifSlurp(gif)) { GIF_CLOSE_FILE(gif); @@ -650,16 +624,9 @@ Image::loadGIFFromBuffer(uint8_t *buf, unsigned len) { return CAIRO_STATUS_SUCCESS; } -#endif /* HAVE_GIF */ // JPEG support -#ifdef HAVE_JPEG - -// libjpeg 6.2 does not have jpeg_mem_src; define it ourselves here unless -// libjpeg 8 is installed. -#if JPEG_LIB_VERSION < 80 && !defined(MEM_SRCDST_SUPPORTED) - /* Read JPEG image from a memory segment */ static void init_source(j_decompress_ptr cinfo) {} @@ -698,8 +665,6 @@ static void jpeg_mem_src (j_decompress_ptr cinfo, void* buffer, long nbytes) { src->next_input_byte = (JOCTET*)buffer; } -#endif - class BufferReader : public Image::Reader { public: BufferReader(uint8_t* buf, unsigned len) : _buf(buf), _len(len), _idx(0) {} @@ -1031,11 +996,7 @@ cairo_status_t Image::loadJPEG(FILE *stream) { cairo_status_t status; -#if defined(_MSC_VER) - if (false) { // Force using loadJPEGFromBuffer -#else if (data_mode == DATA_IMAGE) { // Can lazily read in the JPEG. -#endif Orientation orientation = NORMAL; { StreamReader reader(stream); @@ -1099,11 +1060,6 @@ Image::loadJPEG(FILE *stream) { } else if (DATA_MIME == data_mode) { status = decodeJPEGBufferIntoMimeSurface(buf, len); } -#if defined(_MSC_VER) - else if (DATA_IMAGE == data_mode) { - status = loadJPEGFromBuffer(buf, len); - } -#endif else { status = CAIRO_STATUS_READ_ERROR; } @@ -1394,8 +1350,6 @@ Image::rotatePixels(uint8_t* pixels, int width, int height, int channels, } } -#endif /* HAVE_JPEG */ - /* * Load BMP from buffer. */ diff --git a/src/Image.h b/src/Image.h index 0d532c28f..c757571f3 100644 --- a/src/Image.h +++ b/src/Image.h @@ -8,20 +8,11 @@ #include #include // node < 7 uses libstdc++ on macOS which lacks complete c++11 -#ifdef HAVE_JPEG #include #include -#endif -#ifdef HAVE_GIF #include - - #if GIFLIB_MAJOR > 5 || GIFLIB_MAJOR == 5 && GIFLIB_MINOR >= 1 - #define GIF_CLOSE_FILE(gif) DGifCloseFile(gif, NULL) - #else - #define GIF_CLOSE_FILE(gif) DGifCloseFile(gif) - #endif -#endif +#define GIF_CLOSE_FILE(gif) DGifCloseFile(gif, NULL) using JPEGDecodeL = std::function; @@ -58,11 +49,8 @@ class Image : public Napi::ObjectWrap { cairo_status_t loadPNGFromBuffer(uint8_t *buf); cairo_status_t loadPNG(); void clearData(); -#ifdef HAVE_GIF cairo_status_t loadGIFFromBuffer(uint8_t *buf, unsigned len); cairo_status_t loadGIF(FILE *stream); -#endif -#ifdef HAVE_JPEG enum Orientation { NORMAL, MIRROR_HORIZ, @@ -89,7 +77,6 @@ class Image : public Napi::ObjectWrap { Orientation getExifOrientation(Reader& jpeg); void updateDimensionsForOrientation(Orientation orientation); void rotatePixels(uint8_t* pixels, int width, int height, int channels, Orientation orientation); -#endif cairo_status_t loadBMPFromBuffer(uint8_t *buf, unsigned len); cairo_status_t loadBMP(FILE *stream); CanvasError errorInfo; diff --git a/src/PNG.h b/src/PNG.h index 30b88f85f..094fb12cf 100644 --- a/src/PNG.h +++ b/src/PNG.h @@ -8,14 +8,6 @@ #include #include -#if defined(__GNUC__) && (__GNUC__ > 2) && defined(__OPTIMIZE__) -#define likely(expr) (__builtin_expect (!!(expr), 1)) -#define unlikely(expr) (__builtin_expect (!!(expr), 0)) -#else -#define likely(expr) (expr) -#define unlikely(expr) (expr) -#endif - static void canvas_png_flush(png_structp png_ptr) { /* Do nothing; fflush() is said to be just a waste of energy. */ (void) png_ptr; /* Stifle compiler warning */ @@ -87,11 +79,9 @@ struct canvas_png_write_closure_t { PngClosure* closure; }; -#ifdef PNG_SETJMP_SUPPORTED bool setjmp_wrapper(png_structp png) { return setjmp(png_jmpbuf(png)); } -#endif static cairo_status_t canvas_write_png(cairo_surface_t *surface, png_rw_ptr write_func, canvas_png_write_closure_t *closure) { unsigned int i; @@ -119,7 +109,7 @@ static cairo_status_t canvas_write_png(cairo_surface_t *surface, png_rw_ptr writ } rows = (png_bytep *) malloc(height * sizeof (png_byte*)); - if (unlikely(rows == NULL)) { + if (rows == NULL) [[unlikely]] { status = CAIRO_STATUS_NO_MEMORY; return status; } @@ -129,20 +119,16 @@ static cairo_status_t canvas_write_png(cairo_surface_t *surface, png_rw_ptr writ rows[i] = (png_byte *) data + i * stride; } -#ifdef PNG_USER_MEM_SUPPORTED png = png_create_write_struct_2(PNG_LIBPNG_VER_STRING, NULL, NULL, NULL, NULL, NULL, NULL); -#else - png = png_create_write_struct(PNG_LIBPNG_VER_STRING, NULL, NULL, NULL); -#endif - if (unlikely(png == NULL)) { + if (png == NULL) [[unlikely]] { status = CAIRO_STATUS_NO_MEMORY; free(rows); return status; } info = png_create_info_struct (png); - if (unlikely(info == NULL)) { + if (info == NULL) [[unlikely]] { status = CAIRO_STATUS_NO_MEMORY; png_destroy_write_struct(&png, &info); free(rows); @@ -150,13 +136,11 @@ static cairo_status_t canvas_write_png(cairo_surface_t *surface, png_rw_ptr writ } -#ifdef PNG_SETJMP_SUPPORTED if (setjmp_wrapper(png)) { png_destroy_write_struct(&png, &info); free(rows); return status; } -#endif png_set_write_fn(png, closure, write_func, canvas_png_flush); png_set_compression_level(png, closure->closure->compressionLevel); @@ -173,12 +157,10 @@ static cairo_status_t canvas_write_png(cairo_surface_t *surface, png_rw_ptr writ bpc = 8; png_color_type = PNG_COLOR_TYPE_RGB_ALPHA; break; -#ifdef CAIRO_FORMAT_RGB30 case CAIRO_FORMAT_RGB30: bpc = 10; png_color_type = PNG_COLOR_TYPE_RGB; break; -#endif case CAIRO_FORMAT_RGB24: bpc = 8; png_color_type = PNG_COLOR_TYPE_RGB; @@ -269,7 +251,7 @@ static void canvas_stream_write_func(png_structp png, png_bytep data, png_size_t png_closure = (struct canvas_png_write_closure_t *) png_get_io_ptr(png); status = png_closure->write_func(png_closure->closure, data, size); - if (unlikely(status)) { + if (status) [[unlikely]] { cairo_status_t *error = (cairo_status_t *) png_get_error_ptr(png); if (*error == CAIRO_STATUS_SUCCESS) { *error = status; diff --git a/src/bmp/BMPParser.h b/src/bmp/BMPParser.h index c35f51a86..0f503a7f6 100644 --- a/src/bmp/BMPParser.h +++ b/src/bmp/BMPParser.h @@ -1,60 +1,50 @@ -#pragma once - -#ifdef ERROR -#define ERROR_ ERROR -#undef ERROR -#endif - -#include // node < 7 uses libstdc++ on macOS which lacks complete c++11 -#include - -namespace BMPParser{ - enum Status{ - EMPTY, - OK, - ERROR, - }; - - class Parser{ - public: - Parser()=default; - ~Parser(); - void parse(uint8_t *buf, int bufSize, uint8_t *format=nullptr); - void clearImgd(); - int32_t getWidth() const; - int32_t getHeight() const; - uint8_t *getImgd() const; - Status getStatus() const; - std::string getErrMsg() const; - - private: - Status status = Status::EMPTY; - uint8_t *data = nullptr; - uint8_t *ptr = nullptr; - int len = 0; - int32_t w = 0; - int32_t h = 0; - uint8_t *imgd = nullptr; - std::string err = ""; - std::string op = ""; - - template inline T get(); - template inline T get(uint8_t* pointer); - std::string getStr(int len, bool reverse=false); - inline void skip(int len); - void calcMaskShift(uint32_t& shift, uint32_t& mask, double& multp); - - void setOp(std::string val); - std::string getOp() const; - - void setErrUnsupported(std::string msg); - void setErrUnknown(std::string msg); - void setErr(std::string msg); - std::string getErr() const; - }; -} - -#ifdef ERROR_ -#define ERROR ERROR_ -#undef ERROR_ -#endif +#pragma once + +#include // node < 7 uses libstdc++ on macOS which lacks complete c++11 +#include + +namespace BMPParser{ + enum Status{ + EMPTY, + OK, + ERROR, + }; + + class Parser{ + public: + Parser()=default; + ~Parser(); + void parse(uint8_t *buf, int bufSize, uint8_t *format=nullptr); + void clearImgd(); + int32_t getWidth() const; + int32_t getHeight() const; + uint8_t *getImgd() const; + Status getStatus() const; + std::string getErrMsg() const; + + private: + Status status = Status::EMPTY; + uint8_t *data = nullptr; + uint8_t *ptr = nullptr; + int len = 0; + int32_t w = 0; + int32_t h = 0; + uint8_t *imgd = nullptr; + std::string err = ""; + std::string op = ""; + + template inline T get(); + template inline T get(uint8_t* pointer); + std::string getStr(int len, bool reverse=false); + inline void skip(int len); + void calcMaskShift(uint32_t& shift, uint32_t& mask, double& multp); + + void setOp(std::string val); + std::string getOp() const; + + void setErrUnsupported(std::string msg); + void setErrUnknown(std::string msg); + void setErr(std::string msg); + std::string getErr() const; + }; +} diff --git a/src/closure.cc b/src/closure.cc index 3290db2e5..231da899c 100644 --- a/src/closure.cc +++ b/src/closure.cc @@ -1,7 +1,6 @@ #include "closure.h" #include "Canvas.h" -#ifdef HAVE_JPEG void JpegClosure::init_destination(j_compress_ptr cinfo) { JpegClosure* closure = (JpegClosure*)cinfo->client_data; closure->vec.resize(PAGE_SIZE); @@ -23,7 +22,6 @@ void JpegClosure::term_destination(j_compress_ptr cinfo) { size_t finalSize = closure->vec.size() - closure->jpeg_dest_mgr->free_in_buffer; closure->vec.resize(finalSize); } -#endif void EncodingWorker::Init(void (*work_fn)(Closure*), Closure* closure) { diff --git a/src/closure.h b/src/closure.h index 39aaf57bf..1e17d9a57 100644 --- a/src/closure.h +++ b/src/closure.h @@ -7,20 +7,16 @@ class Canvas; #include "Canvas.h" -#ifdef HAVE_JPEG #include #include #include -#endif #include #include #include // node < 7 uses libstdc++ on macOS which lacks complete c++11 #include -#ifndef PAGE_SIZE - #define PAGE_SIZE 4096 -#endif +#define PAGE_SIZE 4096 /* * Image encoding closures. @@ -61,7 +57,6 @@ struct PngClosure : Closure { PngClosure(Canvas* canvas) : Closure(canvas) {}; }; -#ifdef HAVE_JPEG struct JpegClosure : Closure { uint32_t quality = 75; uint32_t chromaSubsampling = 2; @@ -83,7 +78,6 @@ struct JpegClosure : Closure { delete jpeg_dest_mgr; } }; -#endif class EncodingWorker : public Napi::AsyncWorker { public: diff --git a/src/color.cc b/src/color.cc index f82629460..6f77c1b45 100644 --- a/src/color.cc +++ b/src/color.cc @@ -10,11 +10,6 @@ #include #include -// Compatibility with Visual Studio versions prior to VS2015 -#if defined(_MSC_VER) && _MSC_VER < 1900 -#define snprintf _snprintf -#endif - /* * Parse integer value */ diff --git a/src/dll_visibility.h b/src/dll_visibility.h deleted file mode 100644 index 7a1f98450..000000000 --- a/src/dll_visibility.h +++ /dev/null @@ -1,20 +0,0 @@ -#ifndef DLL_PUBLIC - -#if defined _WIN32 - #ifdef __GNUC__ - #define DLL_PUBLIC __attribute__ ((dllexport)) - #else - #define DLL_PUBLIC __declspec(dllexport) - #endif - #define DLL_LOCAL -#else - #if __GNUC__ >= 4 - #define DLL_PUBLIC __attribute__ ((visibility ("default"))) - #define DLL_LOCAL __attribute__ ((visibility ("hidden"))) - #else - #define DLL_PUBLIC - #define DLL_LOCAL - #endif -#endif - -#endif diff --git a/src/init.cc b/src/init.cc index 60d8079e7..71902c1a9 100644 --- a/src/init.cc +++ b/src/init.cc @@ -3,15 +3,8 @@ #include #include -#if CAIRO_VERSION < CAIRO_VERSION_ENCODE(1, 10, 0) -// CAIRO_FORMAT_RGB16_565: undeprecated in v1.10.0 -// CAIRO_STATUS_INVALID_SIZE: v1.10.0 -// CAIRO_FORMAT_INVALID: v1.10.0 -// Lots of the compositing operators: v1.10.0 -// JPEG MIME tracking: v1.10.0 -// Note: CAIRO_FORMAT_RGB30 is v1.12.0 and still optional -#error("cairo v1.10.0 or later is required") -#endif +#include +#include #include "Canvas.h" #include "CanvasGradient.h" @@ -21,9 +14,6 @@ #include "ImageData.h" #include "InstanceData.h" -#include -#include FT_FREETYPE_H - /* * Save some external modules as private references. */ @@ -40,11 +30,6 @@ setParseFont(const Napi::CallbackInfo& info) { data->parseFont = Napi::Persistent(info[0].As()); } -// Compatibility with Visual Studio versions prior to VS2015 -#if defined(_MSC_VER) && _MSC_VER < 1900 -#define snprintf _snprintf -#endif - Napi::Object init(Napi::Env env, Napi::Object exports) { env.SetInstanceData(new InstanceData()); @@ -59,23 +44,9 @@ Napi::Object init(Napi::Env env, Napi::Object exports) { exports.Set("setParseFont", Napi::Function::New(env, &setParseFont)); exports.Set("cairoVersion", Napi::String::New(env, cairo_version_string())); -#ifdef HAVE_JPEG -#ifndef JPEG_LIB_VERSION_MAJOR -#ifdef JPEG_LIB_VERSION #define JPEG_LIB_VERSION_MAJOR (JPEG_LIB_VERSION / 10) -#else -#define JPEG_LIB_VERSION_MAJOR 0 -#endif -#endif - -#ifndef JPEG_LIB_VERSION_MINOR -#ifdef JPEG_LIB_VERSION #define JPEG_LIB_VERSION_MINOR (JPEG_LIB_VERSION % 10) -#else -#define JPEG_LIB_VERSION_MINOR 0 -#endif -#endif char jpeg_version[10]; static bool minor_gt_0 = JPEG_LIB_VERSION_MINOR > 0; @@ -85,21 +56,10 @@ Napi::Object init(Napi::Env env, Napi::Object exports) { snprintf(jpeg_version, 10, "%d", JPEG_LIB_VERSION_MAJOR); } exports.Set("jpegVersion", Napi::String::New(env, jpeg_version)); -#endif -#ifdef HAVE_GIF -#ifndef GIF_LIB_VERSION char gif_version[10]; snprintf(gif_version, 10, "%d.%d.%d", GIFLIB_MAJOR, GIFLIB_MINOR, GIFLIB_RELEASE); exports.Set("gifVersion", Napi::String::New(env, gif_version)); -#else - exports.Set("gifVersion", Napi::String::New(env, GIF_LIB_VERSION)); -#endif -#endif - - char freetype_version[10]; - snprintf(freetype_version, 10, "%d.%d.%d", FREETYPE_MAJOR, FREETYPE_MINOR, FREETYPE_PATCH); - exports.Set("freetypeVersion", Napi::String::New(env, freetype_version)); return exports; } From 4da684e430e3c7274513cf06047d3035d3b1d7d3 Mon Sep 17 00:00:00 2001 From: Caleb Hearon Date: Mon, 16 Feb 2026 17:01:11 -0500 Subject: [PATCH 4/8] document.fonts based API, from-scratch text layout This resolves so many open issues that I'm not going to list them. Highlights: - You can now register fonts from buffers - Emojis finally work - Font selection always works now, since it's our code, not Pango - Ability to unregister individual fonts - API is like the browser's --- README.md | 84 ++- build.zig | 87 ++- build.zig.zon | 8 + examples/font.js | 12 +- index.d.ts | 61 ++ index.js | 25 +- index.test-d.ts | 18 +- lib/bindings.js | 5 + pkg/harfbuzz/build.zig | 27 + pkg/harfbuzz/build.zig.zon | 10 + pkg/sheenbidi/build.zig | 28 + pkg/sheenbidi/build.zig.zon | 10 + src/Canvas.cc | 11 +- src/Canvas.h | 22 + src/CanvasRenderingContext2d.cc | 410 +++++++++++++ src/CanvasRenderingContext2d.h | 37 ++ src/Font.h | 96 +++ src/FontFace.cc | 169 ++++++ src/FontFace.h | 32 + src/FontFaceSet.cc | 192 ++++++ src/FontFaceSet.h | 43 ++ src/FontLayout.cc | 608 +++++++++++++++++++ src/FontLayout.h | 94 +++ src/FontManager.cc | 269 ++++++++ src/FontManager.h | 48 ++ src/FontManagerLinux.cc | 774 ++++++++++++++++++++++++ src/FontManagerLinux.h | 22 + src/FontManagerMacos.cc | 673 ++++++++++++++++++++ src/FontManagerMacos.h | 11 + src/FontManagerWindows.cc | 648 ++++++++++++++++++++ src/FontManagerWindows.h | 11 + src/FontParser.cc | 38 +- src/FontParser.h | 21 +- src/InstanceData.h | 28 + src/Util.h | 9 + src/init.cc | 8 +- src/itemize.cc | 225 +++++++ src/itemize.h | 81 +++ src/unicode.h | 363 +++++++++++ src/unicode.zig | 386 ++++++++++++ test/canvas.test.js | 193 +++++- test/fixtures/Arimo-Regular-License.txt | 202 +++++++ test/fixtures/Arimo-Regular.ttf | Bin 0 -> 317648 bytes test/public/tests.js | 2 +- 44 files changed, 6027 insertions(+), 74 deletions(-) create mode 100644 pkg/harfbuzz/build.zig create mode 100644 pkg/harfbuzz/build.zig.zon create mode 100644 pkg/sheenbidi/build.zig create mode 100644 pkg/sheenbidi/build.zig.zon create mode 100644 src/Font.h create mode 100644 src/FontFace.cc create mode 100644 src/FontFace.h create mode 100644 src/FontFaceSet.cc create mode 100644 src/FontFaceSet.h create mode 100644 src/FontLayout.cc create mode 100644 src/FontLayout.h create mode 100644 src/FontManager.cc create mode 100644 src/FontManager.h create mode 100644 src/FontManagerLinux.cc create mode 100644 src/FontManagerLinux.h create mode 100644 src/FontManagerMacos.cc create mode 100644 src/FontManagerMacos.h create mode 100644 src/FontManagerWindows.cc create mode 100644 src/FontManagerWindows.h create mode 100644 src/itemize.cc create mode 100644 src/itemize.h create mode 100644 src/unicode.h create mode 100644 src/unicode.zig create mode 100644 test/fixtures/Arimo-Regular-License.txt create mode 100644 test/fixtures/Arimo-Regular.ttf diff --git a/README.md b/README.md index ebae896bd..055a8bf43 100644 --- a/README.md +++ b/README.md @@ -20,7 +20,7 @@ Binaries will be installed for the following platforms: - Windows x64/x86/arm64 If you want to install for a more exotic system, CD into the package and run -`zig build`. You may need to tweak one of the build.zig files. PRs are welcome. +`zig build`. You may need to tweak build.zig. PRs are welcome. ## Quick Example @@ -60,6 +60,10 @@ This project is an implementation of the Web Canvas API and implements that API * [createImageData()](#createimagedata) * [loadImage()](#loadimage) +### Font API + +* [FontFace](#fontface) +* [fonts](#fonts) ### Non-standard APIs @@ -131,6 +135,84 @@ const myimg = await loadImage('http://server.com/image.png') // do something with image ``` +### FontFace + +> ```ts +> +> interface FontFaceDescriptors { +> weight?: string | number; +> style?: string; +> } +> +> class FontFace { +> constructor( +> family: string, +> source: string | ArrayBuffer | Uint8Array, +> descriptors?: FontFaceDescriptors +> ) +> +> family: string; +> style: string; +> weight: string; +> status: 'loaded' | 'unloaded' | 'error'; +> } +> +> ``` + +To use your own font file, create a `FontFace` with the file path and add it to [`fonts`](#fonts). `fonts` is just like the browser's `document.fonts`, but with a slightly more limited API. + +```js +const { createCanvas, FontFace, fonts } = require('canvas') +fonts.add(new FontFace('Comic Sans', '/home/you/comicsans.ttf')) + +const canvas = createCanvas(500, 500) +const ctx = canvas.getContext('2d') + +ctx.font = '12px "Comic Sans"' +ctx.fillText('Everyone hates this font :(', 250, 10) +``` + +The second argument can either be a path to the font file or a buffer in TrueType format. The third argument is an object with properties that resemble the CSS properties that are specified in `@font-face` rules. You must specify at least `family`. `weight` and `style` are optional and default to `'normal'`. + +### fonts + +> ```ts +> +> class FontFaceSet { +> add(face: FontFace): void; +> has(face: FontFace): boolean; +> clear(): void; +> delete(face: FontFace): boolean; +> [Symbol.iterator](): Iterator; +> size: number; +> } +> +> fonts: FontFaceSet +> ``` + +`fonts` implements a subset of the browser's `document.fonts`. Use `fonts.delete()` to unregister an individual font, or `fonts.clear()` to remove all registered fonts. This is useful when you want to remove all registered fonts, such as when using the canvas in tests. + +```ts +const { registerFont, createCanvas, fonts, FontFace } = require('canvas') + +describe('text rendering', () => { + afterEach(() => { + fonts.clear() + }) + it('should render text with Comic Sans', () => { + fonts.add(new FontFace('Comic Sans', '/home/you/comicsans.ttf')) + + const canvas = createCanvas(500, 500) + const ctx = canvas.getContext('2d') + + ctx.font = '12px "Comic Sans"' + ctx.fillText('Everyone loves this font :)', 250, 10) + + // assertScreenshot() + }) +}) +``` + ### Image#src > ```ts diff --git a/build.zig b/build.zig index 6e6283c39..92f23fefb 100644 --- a/build.zig +++ b/build.zig @@ -74,6 +74,35 @@ pub fn build(b: *std.Build) void { .optimize = optimize, }).artifact("png"); + const sheenbidi = b.dependency("sheenbidi", .{ + .target = target, + .optimize = optimize, + }).artifact("sheenbidi"); + + const zg = b.dependency("zg", .{ + .target = target, + .optimize = optimize, + }); + + const harfbuzz = b.dependency("harfbuzz", .{ + .target = target, + .optimize = optimize, + }).artifact("harfbuzz"); + + const unicode = b.addLibrary(.{ + .name = "unicode", + .linkage = .static, + .root_module = b.createModule(.{ + .target = target, + .optimize = optimize, + .root_source_file = b.path("src/unicode.zig"), + }) + }); + + unicode.root_module.addImport("Scripts", zg.module("Scripts")); + unicode.root_module.addImport("Graphemes", zg.module("Graphemes")); + unicode.linkLibC(); + const canvas = b.addLibrary(.{ .name = "canvas", .linkage = .dynamic, @@ -100,11 +129,23 @@ pub fn build(b: *std.Build) void { "src/Image.cc", "src/ImageData.cc", "src/init.cc", - "src/FontParser.cc" + "src/itemize.cc", + "src/FontManager.cc", + switch (target.result.os.tag) { + .windows => "src/FontManagerWindows.cc", + .macos => "src/FontManagerMacos.cc", + else => "src/FontManagerLinux.cc", + }, + "src/FontFace.cc", + "src/FontFaceSet.cc", + "src/FontParser.cc", + "src/FontLayout.cc" } , .flags = &.{ "-DNAPI_DISABLE_CPP_EXCEPTIONS", "-DNODE_ADDON_API_ENABLE_MAYBE", + "-D_USE_MATH_DEFINES", + "-std=c++20", if (target.result.os.tag == .windows) "-DCAIRO_WIN32_STATIC_BUILD" else "", } }); @@ -122,7 +163,49 @@ pub fn build(b: *std.Build) void { canvas.linkLibrary(libjpeg_turbo); canvas.linkLibrary(libpng); canvas.linkLibrary(giflib); - + canvas.linkLibrary(sheenbidi); + canvas.linkLibrary(unicode); + canvas.linkLibrary(harfbuzz); + + if (target.result.os.tag == .windows) { + canvas.linkSystemLibrary("dwrite"); + } + + if (target.result.os.tag == .macos) { + // Stole this method from + // https://github.com/ghostty-org/ghostty/commit/c0722b3652e5e207f34d220f64a03d9d53e93ad0 + + // The active SDK we want to use + const sdk = "MacOSX15.sdk"; + + // Get the path to our active Xcode installation. If this fails then + // the zig build will fail. + const path = std.mem.trim( + u8, + b.run(&.{ "xcode-select", "--print-path" }), + " \r\n", + ); + + canvas.addSystemFrameworkPath(.{ + .cwd_relative = b.pathJoin(&.{ + path, + "Platforms/MacOSX.platform/Developer/SDKs/" ++ sdk ++ "/System/Library/Frameworks", + }), + }); + canvas.addSystemIncludePath(.{ + .cwd_relative = b.pathJoin(&.{ + path, + "Platforms/MacOSX.platform/Developer/SDKs/" ++ sdk ++ "/usr/include", + }), + }); + canvas.addLibraryPath(.{ + .cwd_relative = b.pathJoin(&.{ + path, + "Platforms/MacOSX.platform/Developer/SDKs/" ++ sdk ++ "/usr/lib", + }), + }); + } + const move = b.addInstallFile(canvas.getEmittedBin(), outputPath(target)); move.step.dependOn(&canvas.step); b.getInstallStep().dependOn(&move.step); diff --git a/build.zig.zon b/build.zig.zon index 446d48bf7..52b007889 100644 --- a/build.zig.zon +++ b/build.zig.zon @@ -20,8 +20,16 @@ .url = "https://github.com/chearon/libjpeg-turbo/archive/1222499df19186b61915ef6bf1b3bda15003584b.tar.gz", .hash = "libjpeg_turbo-3.1.1-1-iiYWsnWCSQBHXuzUprRraTbBN-HJfN2dOX6BuZk6iKUi", }, + // Zig dependencies don't need as much tweaking + .zg = .{ + .url = "https://codeberg.org/atman/zg/archive/d9f596626e8ec05a9f3e47f7bc83aedd5bd2f989.tar.gz", + .hash = "zg-0.15.3-oGqU3P5ItAJHmwDLfzX6sg6Xc5MhSJU2TctHfWu_sKLv", + }, .giflib = .{ .path = "./pkg/giflib" }, .libpng = .{ .path = "./pkg/libpng" }, .cairo = .{ .path = "./pkg/cairo" }, + .sheenbidi = .{ .path = "./pkg/sheenbidi" }, + .harfbuzz = .{ .path = "./pkg/harfbuzz" }, + .freetype = .{ .path = "./pkg/freetype" }, }, } diff --git a/examples/font.js b/examples/font.js index d2a37b825..a277ccf90 100644 --- a/examples/font.js +++ b/examples/font.js @@ -1,6 +1,6 @@ const fs = require('fs') const path = require('path') -const Canvas = require('..') +const { createCanvas, FontFace, fonts } = require('..') function fontFile (name) { return path.join(__dirname, '/pfennigFont/', name) @@ -10,12 +10,12 @@ function fontFile (name) { // `registerFont`. When you set `ctx.font`, refer to the styles and the family // name as it is embedded in the TTF. If you aren't sure, open the font in // FontForge and visit Element -> Font Information and copy the Family Name -Canvas.registerFont(fontFile('Pfennig.ttf'), { family: 'pfennigFont' }) -Canvas.registerFont(fontFile('PfennigBold.ttf'), { family: 'pfennigFont', weight: 'bold' }) -Canvas.registerFont(fontFile('PfennigItalic.ttf'), { family: 'pfennigFont', style: 'italic' }) -Canvas.registerFont(fontFile('PfennigBoldItalic.ttf'), { family: 'pfennigFont', weight: 'bold', style: 'italic' }) +fonts.add(new FontFace('pfennigFont', fontFile('Pfennig.ttf'))) +fonts.add(new FontFace('pfennigFont', fontFile('PfennigBold.ttf'), { weight: 'bold' })) +fonts.add(new FontFace('pfennigFont', fontFile('PfennigItalic.ttf'), { style: 'italic' })) +fonts.add(new FontFace('pfennigFont', fontFile('PfennigBoldItalic.ttf'), { weight: 'bold', style: 'italic' })) -const canvas = Canvas.createCanvas(320, 320) +const canvas = createCanvas(320, 320) const ctx = canvas.getContext('2d') ctx.font = 'normal normal 50px Helvetica' diff --git a/index.d.ts b/index.d.ts index 9c87531cc..886ec2dfd 100644 --- a/index.d.ts +++ b/index.d.ts @@ -210,11 +210,14 @@ export class CanvasRenderingContext2D { clip(fillRule?: CanvasFillRule): void; fill(fillRule?: CanvasFillRule): void; stroke(): void; + fillText(text: string, x: number, y: number, maxWidth?: number): void; + strokeText(text: string, x: number, y: number, maxWidth?: number): void; fillRect(x: number, y: number, w: number, h: number): void; strokeRect(x: number, y: number, w: number, h: number): void; clearRect(x: number, y: number, w: number, h: number): void; rect(x: number, y: number, w: number, h: number): void; roundRect(x: number, y: number, w: number, h: number, radii?: number | number[]): void; + measureText(text: string): TextMetrics; moveTo(x: number, y: number): void; lineTo(x: number, y: number): void; bezierCurveTo(cp1x: number, cp1y: number, cp2x: number, cp2y: number, x: number, y: number): void; @@ -250,6 +253,30 @@ export class CanvasRenderingContext2D { shadowBlur: number; /** _Non-standard_. Sets the antialiasing mode. */ antialias: 'default' | 'gray' | 'none' | 'subpixel' + /** + * Defaults to 'path'. The effect depends on the canvas type: + * + * * **Standard (image)** `'glyph'` and `'path'` both result in rasterized + * text. Glyph mode is faster than path, but may result in lower-quality + * text, especially when rotated or translated. + * + * * **PDF** `'glyph'` will embed text instead of paths into the PDF. This + * is faster to encode, faster to open with PDF viewers, yields a smaller + * file size and makes the text selectable. The subset of the font needed + * to render the glyphs will be embedded in the PDF. This is usually the + * mode you want to use with PDF canvases. + * + * * **SVG** glyph does not cause `` elements to be produced as one + * might expect ([cairo bug](https://gitlab.freedesktop.org/cairo/cairo/issues/253)). + * Rather, glyph will create a `` section with a `` for each + * glyph, then those glyphs be reused via `` elements. `'path'` mode + * creates a `` element for each text string. glyph mode is faster + * and yields a smaller file size. + * + * In glyph mode, `ctx.strokeText()` and `ctx.fillText()` behave the same + * (aside from using the stroke and fill style, respectively). + */ + textDrawingMode: 'path' | 'glyph' /** * _Non-standard_. Defaults to 'good'. Like `patternQuality`, but applies to * transformations affecting more than just patterns. @@ -259,7 +286,12 @@ export class CanvasRenderingContext2D { currentTransform: DOMMatrix fillStyle: string | CanvasGradient | CanvasPattern; strokeStyle: string | CanvasGradient | CanvasPattern; + font: string; + textBaseline: CanvasTextBaseline; + textAlign: CanvasTextAlign; canvas: Canvas; + direction: 'ltr' | 'rtl'; + lang: string; } export class CanvasGradient { @@ -346,6 +378,35 @@ export function createImageData(width: number, height: number): ImageData */ export function loadImage(src: string|Buffer, options?: any): Promise +interface FontFaceDescriptors { + weight?: string | number; + style?: string; +} + +export class FontFace { + constructor( + family: string, + url: string | ArrayBuffer | Uint8Array, + descriptors?: FontFaceDescriptors + ); + + family: string; + style: string; + weight: string; + status: 'loaded' | 'unloaded' | 'error'; +} + +declare class FontFaceSet { + add(face: FontFace): void; + has(face: FontFace): boolean; + clear(): void; + delete(face: FontFace): boolean; + [Symbol.iterator](): Iterator; + size: number; +} + +export const fonts: FontFaceSet; + /** This class must not be constructed directly; use `canvas.createPNGStream()`. */ export class PNGStream extends Readable {} /** This class must not be constructed directly; use `canvas.createJPEGStream()`. */ diff --git a/index.js b/index.js index adde4da12..d75f784bb 100644 --- a/index.js +++ b/index.js @@ -4,7 +4,6 @@ const CanvasRenderingContext2D = require('./lib/context2d') const CanvasPattern = require('./lib/pattern') const packageJson = require('./package.json') const bindings = require('./lib/bindings') -const fs = require('fs') const PNGStream = require('./lib/pngstream') const PDFStream = require('./lib/pdfstream') const JPEGStream = require('./lib/jpegstream') @@ -36,31 +35,12 @@ function loadImage (src) { }) } -/** - * Resolve paths for registerFont. Must be called *before* creating a Canvas - * instance. - * @param src {string} Path to font file. - * @param fontFace {{family: string, weight?: string, style?: string}} Object - * specifying font information. `weight` and `style` default to `"normal"`. - */ -function registerFont (src, fontFace) { - // TODO this doesn't need to be on Canvas; it should just be a static method - // of `bindings`. - return Canvas._registerFont(fs.realpathSync(src), fontFace) -} - -/** - * Unload all fonts from pango to free up memory - */ -function deregisterAllFonts () { - return Canvas._deregisterAllFonts() -} - exports.Canvas = Canvas exports.Context2d = CanvasRenderingContext2D // Legacy/compat export exports.CanvasRenderingContext2D = CanvasRenderingContext2D exports.CanvasGradient = bindings.CanvasGradient exports.CanvasPattern = CanvasPattern +exports.FontFace = bindings.FontFace exports.Image = Image exports.ImageData = bindings.ImageData exports.PNGStream = PNGStream @@ -69,8 +49,7 @@ exports.JPEGStream = JPEGStream exports.DOMMatrix = DOMMatrix exports.DOMPoint = DOMPoint -exports.registerFont = registerFont -exports.deregisterAllFonts = deregisterAllFonts +exports.fonts = bindings.fonts exports.createCanvas = createCanvas exports.createImageData = createImageData diff --git a/index.test-d.ts b/index.test-d.ts index f898f2d58..35b5afa96 100644 --- a/index.test-d.ts +++ b/index.test-d.ts @@ -1,10 +1,22 @@ import { expectAssignable, expectType } from 'tsd' -import * as path from 'path' import { Readable } from 'stream' import * as Canvas from './index' -Canvas.registerFont(path.join(__dirname, '../pfennigFont/Pfennig.ttf'), {family: 'pfennigFont'}) +const font = new Canvas.FontFace('pfennigFont', '../pfennigFont/Pfennig.ttf', { + weight: 'normal', + style: 'normal' +}); + +Canvas.fonts.add(font) +font.status; +font.family = 'pfennig'; +font.style = 'italic'; +font.weight = 'bold'; +Canvas.fonts.delete(font); +Canvas.fonts.clear(); +Canvas.fonts.size; +for (const font of Canvas.fonts) font.status; Canvas.createCanvas(5, 10) Canvas.createCanvas(200, 200, 'pdf') @@ -49,5 +61,3 @@ expectType(id2) ctx.putImageData(id2, 0, 0) ctx.drawImage(canv, 0, 0) - -Canvas.deregisterAllFonts() diff --git a/lib/bindings.js b/lib/bindings.js index 3e3c7caf7..a17a91924 100644 --- a/lib/bindings.js +++ b/lib/bindings.js @@ -127,3 +127,8 @@ Object.defineProperty(bindings.CanvasRenderingContext2d.prototype, Symbol.toStri value: 'CanvasRenderingContext2d', configurable: true }) + +Object.defineProperty(bindings.FontFace.prototype, Symbol.toStringTag, { + value: 'FontFace', + configurable: true +}) diff --git a/pkg/harfbuzz/build.zig b/pkg/harfbuzz/build.zig new file mode 100644 index 000000000..42fab075d --- /dev/null +++ b/pkg/harfbuzz/build.zig @@ -0,0 +1,27 @@ +const std = @import("std"); + +pub fn build(b: *std.Build) !void { + const upstream = b.dependency("harfbuzz", .{}); + const target = b.standardTargetOptions(.{}); + const optimize = b.standardOptimizeOption(.{}); + + const harfbuzz = b.addLibrary(.{ + .name = "harfbuzz", + .linkage = .static, + .root_module = b.createModule(.{ + .target = target, + .optimize = optimize, + .pic = true, + }) + }); + + harfbuzz.addCSourceFile(.{ + .file = upstream.path("src/harfbuzz.cc"), + }); + harfbuzz.installHeadersDirectory(upstream.path("src"), "", .{}); + + harfbuzz.linkLibC(); + harfbuzz.linkLibCpp(); + + b.installArtifact(harfbuzz); +} diff --git a/pkg/harfbuzz/build.zig.zon b/pkg/harfbuzz/build.zig.zon new file mode 100644 index 000000000..9251071cb --- /dev/null +++ b/pkg/harfbuzz/build.zig.zon @@ -0,0 +1,10 @@ +.{ + .name = .harfbuzz, + .version = "0.0.0", + .dependencies = .{ + .harfbuzz = .{ + .url = "https://github.com/harfbuzz/harfbuzz/archive/refs/tags/12.3.0.tar.gz", + .hash = "N-V-__8AAK5WlgUb7zJouNiGizHTUdl6zQTgr_apzjhQfiwE", + } + }, +} diff --git a/pkg/sheenbidi/build.zig b/pkg/sheenbidi/build.zig new file mode 100644 index 000000000..dfed2fd93 --- /dev/null +++ b/pkg/sheenbidi/build.zig @@ -0,0 +1,28 @@ +const std = @import("std"); + +pub fn build(b: *std.Build) !void { + const upstream = b.dependency("sheenbidi", .{}); + const target = b.standardTargetOptions(.{}); + const optimize = b.standardOptimizeOption(.{}); + + const sheenbidi = b.addLibrary(.{ + .name = "sheenbidi", + .linkage = .static, + .root_module = b.createModule(.{ + .target = target, + .optimize = optimize, + .pic = true, + }) + }); + + sheenbidi.addCSourceFile(.{ + .file = upstream.path("Source/SheenBidi.c"), + .flags = &.{"-DSB_CONFIG_UNITY"}, + }); + + sheenbidi.addIncludePath(upstream.path("Headers")); + sheenbidi.installHeadersDirectory(upstream.path("Headers"), "", .{}); + sheenbidi.linkLibC(); + + b.installArtifact(sheenbidi); +} diff --git a/pkg/sheenbidi/build.zig.zon b/pkg/sheenbidi/build.zig.zon new file mode 100644 index 000000000..f7fd54474 --- /dev/null +++ b/pkg/sheenbidi/build.zig.zon @@ -0,0 +1,10 @@ +.{ + .name = .sheenbidi, + .version = "0.0.0", + .dependencies = .{ + .sheenbidi = .{ + .url = "https://github.com/Tehreer/SheenBidi/archive/refs/tags/v2.9.0.tar.gz", + .hash = "N-V-__8AAHe8LwF6n4S3jDUlzJLIka4vXuAcDZnCnNpaR1-l" + } + }, +} diff --git a/src/Canvas.cc b/src/Canvas.cc index 2e872e537..c3877ae1f 100644 --- a/src/Canvas.cc +++ b/src/Canvas.cc @@ -653,24 +653,23 @@ Canvas::ParseFont(const Napi::CallbackInfo& info) { if (!ok) return env.Undefined(); Napi::Object obj = Napi::Object::New(env); - obj.Set("size", Napi::Number::New(env, props.fontSize)); + obj.Set("size", Napi::Number::New(env, props.size)); Napi::Array families = Napi::Array::New(env); obj.Set("families", families); unsigned int index = 0; - for (auto& family : props.fontFamily) { + for (auto& family : props.families) { families[index++] = Napi::String::New(env, family); } - obj.Set("weight", Napi::Number::New(env, props.fontWeight)); - obj.Set("variant", Napi::Number::New(env, static_cast(props.fontVariant))); - obj.Set("style", Napi::Number::New(env, static_cast(props.fontStyle))); + obj.Set("weight", Napi::Number::New(env, props.weight)); + obj.Set("variant", Napi::Number::New(env, static_cast(props.variant))); + obj.Set("style", Napi::Number::New(env, static_cast(props.style))); return obj; } - // This returns an approximate value only, suitable for // Napi::MemoryManagement:: AdjustExternalMemory. // The formats that don't map to intrinsic types (RGB30, A1) round up. diff --git a/src/Canvas.h b/src/Canvas.h index 9e619a105..2f81db721 100644 --- a/src/Canvas.h +++ b/src/Canvas.h @@ -24,6 +24,28 @@ typedef enum { CANVAS_TYPE_SVG } canvas_type_t; +enum text_baseline_t : uint8_t { + TEXT_BASELINE_ALPHABETIC = 0, + TEXT_BASELINE_TOP = 1, + TEXT_BASELINE_BOTTOM = 2, + TEXT_BASELINE_MIDDLE = 3, + TEXT_BASELINE_IDEOGRAPHIC = 4, + TEXT_BASELINE_HANGING = 5 +}; + +enum text_align_t : int8_t { + TEXT_ALIGNMENT_LEFT = -1, + TEXT_ALIGNMENT_CENTER = 0, + TEXT_ALIGNMENT_RIGHT = 1, + TEXT_ALIGNMENT_START = -2, + TEXT_ALIGNMENT_END = 2 +}; + +enum canvas_draw_mode_t : uint8_t { + TEXT_DRAW_PATHS, + TEXT_DRAW_GLYPHS +}; + /* * Canvas. */ diff --git a/src/CanvasRenderingContext2d.cc b/src/CanvasRenderingContext2d.cc index 5a3b4c346..650dc0758 100644 --- a/src/CanvasRenderingContext2d.cc +++ b/src/CanvasRenderingContext2d.cc @@ -4,12 +4,16 @@ #include #include +#include #include "Canvas.h" #include "CanvasGradient.h" #include "CanvasPattern.h" +#include "Font.h" #include "InstanceData.h" #include "FontParser.h" +#include "FontLayout.h" #include +#include #include #include "Image.h" #include "ImageData.h" @@ -19,6 +23,7 @@ #include #include "Util.h" #include +#include /* * Rectangle arg assertions. @@ -102,11 +107,14 @@ Context2d::Initialize(Napi::Env& env, Napi::Object& exports) { InstanceMethod<&Context2d::Clip>("clip", napi_default_method), InstanceMethod<&Context2d::Fill>("fill", napi_default_method), InstanceMethod<&Context2d::Stroke>("stroke", napi_default_method), + InstanceMethod<&Context2d::FillText>("fillText", napi_default_method), + InstanceMethod<&Context2d::StrokeText>("strokeText", napi_default_method), InstanceMethod<&Context2d::FillRect>("fillRect", napi_default_method), InstanceMethod<&Context2d::StrokeRect>("strokeRect", napi_default_method), InstanceMethod<&Context2d::ClearRect>("clearRect", napi_default_method), InstanceMethod<&Context2d::Rect>("rect", napi_default_method), InstanceMethod<&Context2d::RoundRect>("roundRect", napi_default_method), + InstanceMethod<&Context2d::MeasureText>("measureText", napi_default_method), InstanceMethod<&Context2d::MoveTo>("moveTo", napi_default_method), InstanceMethod<&Context2d::LineTo>("lineTo", napi_default_method), InstanceMethod<&Context2d::BezierCurveTo>("bezierCurveTo", napi_default_method), @@ -138,10 +146,16 @@ Context2d::Initialize(Napi::Env& env, Napi::Object& exports) { InstanceAccessor<&Context2d::GetShadowOffsetY, &Context2d::SetShadowOffsetY>("shadowOffsetY", napi_default_jsproperty), InstanceAccessor<&Context2d::GetShadowBlur, &Context2d::SetShadowBlur>("shadowBlur", napi_default_jsproperty), InstanceAccessor<&Context2d::GetAntiAlias, &Context2d::SetAntiAlias>("antialias", napi_default_jsproperty), + InstanceAccessor<&Context2d::GetTextDrawingMode, &Context2d::SetTextDrawingMode>("textDrawingMode", napi_default_jsproperty), InstanceAccessor<&Context2d::GetQuality, &Context2d::SetQuality>("quality", napi_default_jsproperty), InstanceAccessor<&Context2d::GetCurrentTransform, &Context2d::SetCurrentTransform>("currentTransform", napi_default_jsproperty), InstanceAccessor<&Context2d::GetFillStyle, &Context2d::SetFillStyle>("fillStyle", napi_default_jsproperty), InstanceAccessor<&Context2d::GetStrokeStyle, &Context2d::SetStrokeStyle>("strokeStyle", napi_default_jsproperty), + InstanceAccessor<&Context2d::GetFont, &Context2d::SetFont>("font", napi_default_jsproperty), + InstanceAccessor<&Context2d::GetTextBaseline, &Context2d::SetTextBaseline>("textBaseline", napi_default_jsproperty), + InstanceAccessor<&Context2d::GetTextAlign, &Context2d::SetTextAlign>("textAlign", napi_default_jsproperty), + InstanceAccessor<&Context2d::GetDirection, &Context2d::SetDirection>("direction", napi_default_jsproperty), + InstanceAccessor<&Context2d::GetLanguage, &Context2d::SetLanguage>("lang", napi_default_jsproperty) }); exports.Set("CanvasRenderingContext2d", ctor); @@ -691,6 +705,46 @@ Context2d::AddPage(const Napi::CallbackInfo& info) { cairo_pdf_surface_set_size(canvas()->ensureSurface(), width, height); } +/* + * Get text direction. + */ +Napi::Value +Context2d::GetDirection(const Napi::CallbackInfo& info) { + return Napi::String::New(env, state->direction); +} + +/* + * Set text direction. + */ +void +Context2d::SetDirection(const Napi::CallbackInfo& info, const Napi::Value& value) { + if (!value.IsString()) return; + + std::string dir = value.As(); + if (dir != "ltr" && dir != "rtl") return; + + state->direction = dir; +} + +/* + * Get language. + */ +Napi::Value +Context2d::GetLanguage(const Napi::CallbackInfo& info) { + return Napi::String::New(env, state->lang); +} + +/* + * Set language. + */ +void +Context2d::SetLanguage(const Napi::CallbackInfo& info, const Napi::Value& value) { + if (!value.IsString()) return; + + std::string lang = value.As(); + state->lang = lang; +} + /* * Put image data. * @@ -1675,6 +1729,40 @@ Context2d::SetAntiAlias(const Napi::CallbackInfo& info, const Napi::Value& value } } +/* + * Get text drawing mode. + */ + +Napi::Value +Context2d::GetTextDrawingMode(const Napi::CallbackInfo& info) { + const char *mode; + if (state->textDrawingMode == TEXT_DRAW_PATHS) { + mode = "path"; + } else if (state->textDrawingMode == TEXT_DRAW_GLYPHS) { + mode = "glyph"; + } else { + mode = "unknown"; + } + return Napi::String::New(env, mode); +} + +/* + * Set text drawing mode. + */ + +void +Context2d::SetTextDrawingMode(const Napi::CallbackInfo& info, const Napi::Value& value) { + Napi::String stringValue; + if (value.ToString().UnwrapTo(&stringValue)) { + std::string str = stringValue.Utf8Value(); + if (str == "path") { + state->textDrawingMode = TEXT_DRAW_PATHS; + } else if (str == "glyph") { + state->textDrawingMode = TEXT_DRAW_GLYPHS; + } + } +} + /* * Get filter. */ @@ -2319,6 +2407,149 @@ Context2d::Stroke(const Napi::CallbackInfo& info) { stroke(true); } +/* + * Helper for fillText/strokeText + */ + +double +get_text_scale(TextLayout& layout, double maxWidth) { + if (layout.width > maxWidth) { + return maxWidth / layout.width; + } else { + return 1.0; + } +} + +void +Context2d::paintText(const Napi::CallbackInfo& info, bool stroke) { + int argsNum = info.Length() >= 4 ? 3 : 2; + + if (argsNum == 3 && info[3].IsUndefined()) + argsNum = 2; + + double args[3]; + if(!checkArgs(info, args, argsNum, 1)) + return; + + Napi::String strValue; + + if (!info[0].ToString().UnwrapTo(&strValue)) return; + + napi_get_value_string_utf16(env, strValue, textBuffer, sizeof(textBuffer), &textLength); + + InstanceData* data = env.GetInstanceData(); + + constexpr size_t CR_GLYPH_BUF_SIZE = 1024; + cairo_glyph_t glyphs[CR_GLYPH_BUF_SIZE]; + + TextLayout layout = layoutText(textBuffer, textLength, state, data); + double x = args[0] - layout.anchorX; + double y = args[1] + layout.anchorY; + + if (argsNum == 3) { + if (args[2] <= 0) return; + double scaled_by = get_text_scale(layout, args[2]); + cairo_save(context()); + cairo_scale(context(), scaled_by, 1); + } + + for (GlyphRun& run : layout.runs) { + double fontSize = state->fontProperties.size; + double toPx = fontSize / 1000; + + FT_Face ftface; + FT_Error newFaceResult = FT_New_Memory_Face( + data->ft, + reinterpret_cast(run.face->data.get()), + run.face->data_len, + run.face->index, + &ftface + ); + + if (newFaceResult != 0) continue; + + cairo_font_face_t* crface = cairo_ft_font_face_create_for_ft_face(ftface, 0); + + static const cairo_user_data_key_t key{0}; + cairo_status_t setDataResult = cairo_font_face_set_user_data( + crface, + &key, + ftface, + (cairo_destroy_func_t) FT_Done_Face + ); + + if (setDataResult) { + cairo_font_face_destroy(crface); + FT_Done_Face (ftface); + continue; + } + + cairo_set_font_face(context(), crface); + cairo_set_font_size(context(), fontSize); + + size_t hbGlyphIndex = 0; + while (hbGlyphIndex < run.glyphs.size()) { + size_t crGlyphIndex = 0; + + while (crGlyphIndex < CR_GLYPH_BUF_SIZE && hbGlyphIndex < run.glyphs.size()) { + glyphs[crGlyphIndex].index = run.glyphs[hbGlyphIndex].id; + glyphs[crGlyphIndex].x = x + run.glyphs[hbGlyphIndex].x_offset * toPx; + glyphs[crGlyphIndex].y = y + run.glyphs[hbGlyphIndex].y_offset * toPx; + x += run.glyphs[hbGlyphIndex].x_advance * toPx; + y += run.glyphs[hbGlyphIndex].y_advance * toPx; + + hbGlyphIndex += 1; + crGlyphIndex += 1; + } + + savePath(); + if (state->textDrawingMode == TEXT_DRAW_GLYPHS) { + if (stroke) { this->stroke(); } else { this->fill(); } + setTextPath(glyphs, crGlyphIndex); + } else if (state->textDrawingMode == TEXT_DRAW_PATHS) { + setTextPath(glyphs, crGlyphIndex); + if (stroke) { this->stroke(); } else { this->fill(); } + } + restorePath(); + } + + cairo_set_font_face(context(), nullptr); + cairo_font_face_destroy(crface); + } + + if (argsNum == 3) { + cairo_restore(context()); + } +} + +/* + * Fill text at (x, y). + */ + +void +Context2d::FillText(const Napi::CallbackInfo& info) { + paintText(info, false); +} + +/* + * Stroke text at (x ,y). + */ + +void +Context2d::StrokeText(const Napi::CallbackInfo& info) { + paintText(info, true); +} + +void +Context2d::setTextPath(cairo_glyph_t* glyphs, size_t numGlyphs) { + if (state->textDrawingMode == TEXT_DRAW_GLYPHS) { + // TODO: use cairo_show_text_glyphs ! + cairo_show_glyphs(context(), glyphs, numGlyphs); + } else { + cairo_glyph_path(context(), glyphs, numGlyphs); + } +} + /* * Adds a point to the current subpath. */ @@ -2345,6 +2576,185 @@ Context2d::MoveTo(const Napi::CallbackInfo& info) { cairo_move_to(context(), args[0], args[1]); } +/* + * Get font. + */ + +Napi::Value +Context2d::GetFont(const Napi::CallbackInfo& info) { + return Napi::String::New(env, state->font); +} + +/* + * Set font: + * - weight + * - style + * - size + * - unit + * - family + */ + +void +Context2d::SetFont(const Napi::CallbackInfo& info, const Napi::Value& value) { + if (!value.IsString()) return; + + std::string str = value.As().Utf8Value(); + if (!str.length()) return; + + bool success; + auto props = FontParser::parse(str, &success); + if (!success) return; + + state->font = str; + state->fontProperties = props; +} + +/* + * Get text baseline. + */ + +Napi::Value +Context2d::GetTextBaseline(const Napi::CallbackInfo& info) { + const char* baseline; + switch (state->textBaseline) { + default: + case TEXT_BASELINE_ALPHABETIC: baseline = "alphabetic"; break; + case TEXT_BASELINE_TOP: baseline = "top"; break; + case TEXT_BASELINE_BOTTOM: baseline = "bottom"; break; + case TEXT_BASELINE_MIDDLE: baseline = "middle"; break; + case TEXT_BASELINE_IDEOGRAPHIC: baseline = "ideographic"; break; + case TEXT_BASELINE_HANGING: baseline = "hanging"; break; + } + return Napi::String::New(env, baseline); +} + +/* + * Set text baseline. + */ + +void +Context2d::SetTextBaseline(const Napi::CallbackInfo& info, const Napi::Value& value) { + if (!value.IsString()) return; + + std::string opStr = value.As(); + const std::map modes = { + {"alphabetic", TEXT_BASELINE_ALPHABETIC}, + {"top", TEXT_BASELINE_TOP}, + {"bottom", TEXT_BASELINE_BOTTOM}, + {"middle", TEXT_BASELINE_MIDDLE}, + {"ideographic", TEXT_BASELINE_IDEOGRAPHIC}, + {"hanging", TEXT_BASELINE_HANGING} + }; + auto op = modes.find(opStr); + if (op == modes.end()) return; + + state->textBaseline = op->second; +} + +/* + * Get text align. + */ + +Napi::Value +Context2d::GetTextAlign(const Napi::CallbackInfo& info) { + const char* align; + switch (state->textAlignment) { + case TEXT_ALIGNMENT_LEFT: align = "left"; break; + case TEXT_ALIGNMENT_START: align = "start"; break; + case TEXT_ALIGNMENT_CENTER: align = "center"; break; + case TEXT_ALIGNMENT_RIGHT: align = "right"; break; + case TEXT_ALIGNMENT_END: align = "end"; break; + default: align = "start"; + } + return Napi::String::New(env, align); +} + +/* + * Set text align. + */ + +void +Context2d::SetTextAlign(const Napi::CallbackInfo& info, const Napi::Value& value) { + if (!value.IsString()) return; + + std::string opStr = value.As(); + const std::map modes = { + {"center", TEXT_ALIGNMENT_CENTER}, + {"left", TEXT_ALIGNMENT_LEFT}, + {"start", TEXT_ALIGNMENT_START}, + {"right", TEXT_ALIGNMENT_RIGHT}, + {"end", TEXT_ALIGNMENT_END} + }; + auto op = modes.find(opStr); + if (op == modes.end()) return; + + state->textAlignment = op->second; +} + +/* + * Return the given text extents. + * TODO: Support for: + * hangingBaseline, ideographicBaseline, + * fontBoundingBoxAscent, fontBoundingBoxDescent + */ + +Napi::Value +Context2d::MeasureText(const Napi::CallbackInfo& info) { + cairo_t *ctx = this->context(); + + Napi::String str; + if (!info[0].ToString().UnwrapTo(&str)) return env.Undefined(); + napi_get_value_string_utf16(env, str, textBuffer, sizeof(textBuffer), &textLength); + + Napi::Object obj = Napi::Object::New(env); + InstanceData* data = env.GetInstanceData(); + + TextLayout layout = layoutText(textBuffer, textLength, state, data); + + double fontSize = state->fontProperties.size; + double ax = 0; + double ay = 0; + double actualBoundingBoxLeft = 0; + double actualBoundingBoxRight = 0; + double actualBoundingBoxAscent = 0; + double actualBoundingBoxDescent = 0; + bool first = true; + + // first calculate this bounding box, with no anchor: + // - ascent and descent increasing means going away from baseline + // - left and right increasing means going right + for (GlyphRun& run : layout.runs) { + double toPx = fontSize / 1000; + for (Glyph& glyph : run.glyphs) { + actualBoundingBoxLeft = std::min(actualBoundingBoxLeft, ax + glyph.x_bearing * toPx); + actualBoundingBoxAscent = std::max(actualBoundingBoxAscent, ay + glyph.y_bearing * toPx); + actualBoundingBoxRight = std::max(actualBoundingBoxRight, ax + glyph.x_bearing * toPx + glyph.width * toPx); + actualBoundingBoxDescent = std::max(actualBoundingBoxDescent, ay - glyph.y_bearing * toPx - glyph.height * toPx); + ax += glyph.x_advance * toPx; + ay += glyph.y_advance * toPx; + } + } + + // convert to whatwg-specified box with an anchor: + // - ascent and descent increasing means going from anchor outwards + // - left and right increasing means going from anchor outwards + actualBoundingBoxLeft = layout.anchorX - actualBoundingBoxLeft; + actualBoundingBoxRight = actualBoundingBoxRight - layout.anchorX; + actualBoundingBoxAscent = actualBoundingBoxAscent - layout.anchorY; + actualBoundingBoxDescent = layout.anchorY + actualBoundingBoxDescent; + + obj.Set("width", Napi::Number::New(env, actualBoundingBoxLeft + actualBoundingBoxRight)); + obj.Set("actualBoundingBoxLeft", Napi::Number::New(env, actualBoundingBoxLeft)); + obj.Set("actualBoundingBoxRight", Napi::Number::New(env, actualBoundingBoxRight)); + obj.Set("actualBoundingBoxAscent", Napi::Number::New(env, actualBoundingBoxAscent)); + obj.Set("actualBoundingBoxDescent", Napi::Number::New(env, actualBoundingBoxDescent)); + obj.Set("emHeightAscent", Napi::Number::New(env, layout.anchorY + layout.metrics.central + fontSize / 2)); + obj.Set("emHeightDescent", Napi::Number::New(env, layout.anchorY + layout.metrics.central - fontSize / 2)); + obj.Set("alphabeticBaseline", Napi::Number::New(env, -layout.anchorY)); + + return obj; +} + /* * Set line dash * ref: http://www.w3.org/TR/2dcontext/#dom-context-2d-setlinedash diff --git a/src/CanvasRenderingContext2d.h b/src/CanvasRenderingContext2d.h index 91cdfcb53..946b1f2bc 100644 --- a/src/CanvasRenderingContext2d.h +++ b/src/CanvasRenderingContext2d.h @@ -4,8 +4,11 @@ #include "cairo.h" #include "Canvas.h" +#include "Font.h" +#include "FontLayout.h" #include "color.h" #include "napi.h" +#include "unicode.h" #include /* @@ -25,10 +28,17 @@ struct canvas_state_t { Napi::ObjectReference strokePattern; Napi::ObjectReference fillGradient; Napi::ObjectReference strokeGradient; + FontProperties fontProperties; + std::string font = "10px sans-serif"; cairo_filter_t patternQuality = CAIRO_FILTER_GOOD; float globalAlpha = 1.f; int shadowBlur = 0; + text_align_t textAlignment = TEXT_ALIGNMENT_START; + text_baseline_t textBaseline = TEXT_BASELINE_ALPHABETIC; + canvas_draw_mode_t textDrawingMode = TEXT_DRAW_PATHS; bool imageSmoothingEnabled = true; + std::string direction = "ltr"; + std::string lang = ""; canvas_state_t() {} @@ -41,11 +51,18 @@ struct canvas_state_t { fillGradient.Reset(other.fillGradient.Value()); strokeGradient.Reset(other.strokeGradient.Value()); globalAlpha = other.globalAlpha; + textAlignment = other.textAlignment; + textBaseline = other.textBaseline; shadow = other.shadow; shadowBlur = other.shadowBlur; shadowOffsetX = other.shadowOffsetX; shadowOffsetY = other.shadowOffsetY; + textDrawingMode = other.textDrawingMode; + fontProperties = other.fontProperties; + font = other.font; imageSmoothingEnabled = other.imageSmoothingEnabled; + direction = other.direction; + lang = other.lang; } }; @@ -87,8 +104,11 @@ class Context2d : public Napi::ObjectWrap { void Clip(const Napi::CallbackInfo& info); void Fill(const Napi::CallbackInfo& info); void Stroke(const Napi::CallbackInfo& info); + void FillText(const Napi::CallbackInfo& info); + void StrokeText(const Napi::CallbackInfo& info); void SetLineDash(const Napi::CallbackInfo& info); Napi::Value GetLineDash(const Napi::CallbackInfo& info); + Napi::Value MeasureText(const Napi::CallbackInfo& info); void BezierCurveTo(const Napi::CallbackInfo& info); void QuadraticCurveTo(const Napi::CallbackInfo& info); void LineTo(const Napi::CallbackInfo& info); @@ -126,6 +146,10 @@ class Context2d : public Napi::ObjectWrap { Napi::Value GetCurrentTransform(const Napi::CallbackInfo& info); Napi::Value GetFillStyle(const Napi::CallbackInfo& info); Napi::Value GetStrokeStyle(const Napi::CallbackInfo& info); + Napi::Value GetFont(const Napi::CallbackInfo& info); + Napi::Value GetTextBaseline(const Napi::CallbackInfo& info); + Napi::Value GetTextAlign(const Napi::CallbackInfo& info); + Napi::Value GetLanguage(const Napi::CallbackInfo& info); void SetPatternQuality(const Napi::CallbackInfo& info, const Napi::Value& value); void SetImageSmoothingEnabled(const Napi::CallbackInfo& info, const Napi::Value& value); void SetGlobalCompositeOperation(const Napi::CallbackInfo& info, const Napi::Value& value); @@ -140,18 +164,26 @@ class Context2d : public Napi::ObjectWrap { void SetShadowOffsetY(const Napi::CallbackInfo& info, const Napi::Value& value); void SetShadowBlur(const Napi::CallbackInfo& info, const Napi::Value& value); void SetAntiAlias(const Napi::CallbackInfo& info, const Napi::Value& value); + void SetTextDrawingMode(const Napi::CallbackInfo& info, const Napi::Value& value); void SetQuality(const Napi::CallbackInfo& info, const Napi::Value& value); void SetCurrentTransform(const Napi::CallbackInfo& info, const Napi::Value& value); void SetFillStyle(const Napi::CallbackInfo& info, const Napi::Value& value); void SetStrokeStyle(const Napi::CallbackInfo& info, const Napi::Value& value); + void SetFont(const Napi::CallbackInfo& info, const Napi::Value& value); + void SetTextBaseline(const Napi::CallbackInfo& info, const Napi::Value& value); + void SetTextAlign(const Napi::CallbackInfo& info, const Napi::Value& value); + void SetLanguage(const Napi::CallbackInfo& info, const Napi::Value& value); void BeginTag(const Napi::CallbackInfo& info); void EndTag(const Napi::CallbackInfo& info); + Napi::Value GetDirection(const Napi::CallbackInfo& info); + void SetDirection(const Napi::CallbackInfo& info, const Napi::Value& value); inline void setContext(cairo_t *ctx) { _context = ctx; } inline cairo_t *context(){ return _context; } inline Canvas *canvas(){ return _canvas; } inline bool hasShadow(); void inline setSourceRGBA(rgba_t color); void inline setSourceRGBA(cairo_t *ctx, rgba_t color); + void setTextPath(cairo_glyph_t* glyphs, size_t numGlyphs); void blur(cairo_surface_t *surface, int radius); void shadow(void (fn)(cairo_t *cr)); void savePath(); @@ -171,7 +203,12 @@ class Context2d : public Napi::ObjectWrap { Napi::Value get_current_transform(); void _setFillColor(Napi::Value arg); void _setStrokeColor(Napi::Value arg); + void paintText(const Napi::CallbackInfo&, bool); + text_align_t resolveTextAlignment(); Canvas *_canvas; cairo_t *_context = nullptr; cairo_path_t *_path; + // 65536 is an arbitrary max fillText length. Use sizeof to get the capacity. + char16_t textBuffer[65536]; + size_t textLength; }; diff --git a/src/Font.h b/src/Font.h new file mode 100644 index 000000000..0263943f0 --- /dev/null +++ b/src/Font.h @@ -0,0 +1,96 @@ +// Copyright (c) 2024 Caleb Hearon +// Stuff common to all perspectives on fonts: CSS, OS fonts, querying, etc. +#pragma once + +#include +#include +#include + +enum class FontStyle { + Normal, + Italic, + Oblique +}; + +enum class FontVariant { + Normal, + SmallCaps +}; + +enum class FontStatus { Unloaded, Loaded, Error }; + +#ifdef _WIN32 +using file_char = wchar_t; +#else +using file_char = char; +#endif + +// Descriptors and properties (see next comments) +struct FontBase { + uint16_t weight{400}; + FontVariant variant{FontVariant::Normal}; + FontStyle style{FontStyle::Normal}; + uint16_t stretch{100}; +}; + +// Descriptors describe real fonts on the OS +struct FontDescriptor : FontBase { + std::unique_ptr family; + // The postscript name is kept purely to select fonts within a TrueType + // Collection (ttc). macOS CTFontDescriptors don't tell you which index + // they represent in a ttc, so when we begin to draw with a ttc match, we + // select which index to draw with based on which one has this name. + std::unique_ptr postscript = nullptr; + std::unique_ptr url = nullptr; + std::unique_ptr data = nullptr; + size_t data_len = 0; + size_t index = 0; + FontStatus status = FontStatus::Unloaded; + + void load() { + if (data != nullptr || url == nullptr) return; + + FILE* file = nullptr; + long file_size = 0; + std::unique_ptr buffer; + + // TODO: propagate error +#ifdef _WIN32 + file = _wfopen(url.get(), L"rb"); +#else + file = fopen(url.get(), "rb"); +#endif + if (!file) goto bail; + + // TODO: propagate error + if (fseek(file, 0, SEEK_END) != 0) goto bail; + + // TODO: propagate error + file_size = ftell(file); + if (file_size < 0) goto bail; + + // TODO: propagate error + if (fseek(file, 0, SEEK_SET) != 0) goto bail; + + buffer = std::make_unique(file_size); + if (fread(buffer.get(), 1, file_size, file) != static_cast(file_size)) { + goto bail; + } + + data = std::move(buffer); + data_len = file_size; + status = FontStatus::Loaded; + fclose(file); + return; + +bail: + status = FontStatus::Error; + if (file) fclose(file); + } +}; + +// Properties describe desired fonts from CSS/ctx.font +struct FontProperties : FontBase { + std::vector families; + double size{10.0f}; +}; diff --git a/src/FontFace.cc b/src/FontFace.cc new file mode 100644 index 000000000..e0e0110df --- /dev/null +++ b/src/FontFace.cc @@ -0,0 +1,169 @@ +// Copyright (c) 2024 Caleb Hearon + +#include "FontFace.h" +#include "FontParser.h" +#include "InstanceData.h" + +#include +#include + +static size_t lastId = 0; + +FontFace::FontFace(const Napi::CallbackInfo& info) : + Napi::ObjectWrap(info), + id(++lastId), + env(info.Env()) +{ + if (info.Length() < 2) { + Napi::TypeError::New(env, "Family and source arguments are required").ThrowAsJavaScriptException(); + return; + } + + if (!setFamilyInternal(info[0])) return; + + if (info[1].IsString()) { + Napi::String url = info[1].As(); + size_t len; +#ifdef _WIN32 + napi_get_value_string_utf16(env, url, nullptr, 0, &len); +#else + napi_get_value_string_utf8(env, url, nullptr, 0, &len); +#endif + len++; + descriptor.url = std::make_unique(len); +#ifdef _WIN32 + napi_get_value_string_utf16(env, url, (char16_t*)descriptor.url.get(), len, nullptr); +#else + napi_get_value_string_utf8(env, url, descriptor.url.get(), len, nullptr); +#endif + } else if (info[1].IsTypedArray()) { + Napi::TypedArray ta = info[1].As(); + Napi::ArrayBuffer buf = ta.ArrayBuffer(); + descriptor.data_len = ta.ByteLength(); + descriptor.data = std::make_unique(descriptor.data_len); + std::memcpy(descriptor.data.get(), static_cast(buf.Data()) + ta.ByteOffset(), descriptor.data_len); + } else if (info[1].IsArrayBuffer()) { + Napi::ArrayBuffer buf = info[1].As(); + descriptor.data_len = buf.ByteLength(); + descriptor.data = std::make_unique(descriptor.data_len); + std::memcpy(descriptor.data.get(), buf.Data(), descriptor.data_len); + } else { + Napi::TypeError::New(env, "Source must be a string or buffer").ThrowAsJavaScriptException(); + return; + } + + if (info.Length() >= 3) { + if (!info[2].IsObject()) { + Napi::TypeError::New(env, "Descriptors must be an object").ThrowAsJavaScriptException(); + return; + } + Napi::Object descriptors = info[2].As(); + Napi::Value value; + if (descriptors.Has("weight").UnwrapOr(false) && descriptors.Get("weight").UnwrapTo(&value)) { + if (!setWeightInternal(value)) return; + } + if (descriptors.Has("style").UnwrapOr(false) && descriptors.Get("style").UnwrapTo(&value)) { + if (!setStyleInternal(value)) return; + } + } +} + +void +FontFace::Initialize(Napi::Env& env, Napi::Object& exports) { + InstanceData *data = env.GetInstanceData(); + Napi::Function ctor = DefineClass(env, "FontFace", { + InstanceAccessor<&FontFace::GetFamily, &FontFace::SetFamily>("family", napi_default_jsproperty), + InstanceAccessor<&FontFace::GetStyle, &FontFace::SetStyle>("style", napi_default_jsproperty), + InstanceAccessor<&FontFace::GetWeight, &FontFace::SetWeight>("weight", napi_default_jsproperty), + InstanceAccessor<&FontFace::GetStatus>("status", napi_default_jsproperty) + }); + + data->FontFaceCtor = Napi::Persistent(ctor); + exports.Set("FontFace", ctor); +} + +Napi::Value +FontFace::GetFamily(const Napi::CallbackInfo& info) { + return Napi::String::New(env, descriptor.family.get()); +} + +bool +FontFace::setFamilyInternal(const Napi::Value& value) { + // According to the specs, this is supposed to go through the CSS parser, so + // fonts starting with numbers or having special characters should actually + // throw an error. However, Safari accepts anything, and Firefox puts quotes + // around the family: https://bugzilla.mozilla.org/show_bug.cgi?id=1986533 + if (Napi::String family; value.ToString().UnwrapTo(&family)) { + size_t len; + napi_get_value_string_utf8(env, family, nullptr, 0, &len); + len++; + descriptor.family = std::make_unique(len); + napi_get_value_string_utf8(env, family, descriptor.family.get(), len, nullptr); + return true; + } + return false; +} + +void +FontFace::SetFamily(const Napi::CallbackInfo& info, const Napi::Value& value) { + setFamilyInternal(value); +} + +Napi::Value +FontFace::GetStyle(const Napi::CallbackInfo& info) { + switch (descriptor.style) { + case FontStyle::Normal: return Napi::String::New(env, "normal"); + case FontStyle::Italic: return Napi::String::New(env, "italic"); + case FontStyle::Oblique: return Napi::String::New(env, "oblique"); + } +} + +bool +FontFace::setStyleInternal(const Napi::Value& value) { + if (Napi::String style; value.ToString().UnwrapTo(&style)) { + if (auto value = FontParser::parseStyle(style.Utf8Value()); value) { + descriptor.style = *value; + return true; + } else { + Napi::TypeError::New(env, "Could not parse style").ThrowAsJavaScriptException(); + } + } + return false; +} + +void +FontFace::SetStyle(const Napi::CallbackInfo& info, const Napi::Value& value) { + setStyleInternal(value); +} + +Napi::Value +FontFace::GetWeight(const Napi::CallbackInfo& info) { + return Napi::String::New(env, std::to_string(descriptor.weight)); +} + +bool +FontFace::setWeightInternal(const Napi::Value& value) { + if (Napi::String weight; value.ToString().UnwrapTo(&weight)) { + if (auto value = FontParser::parseWeight(weight.Utf8Value()); value) { + descriptor.weight = *value; + return true; + } else { + Napi::TypeError::New(env, "Could not parse weight").ThrowAsJavaScriptException(); + } + } + return false; +} + +void +FontFace::SetWeight(const Napi::CallbackInfo& info, const Napi::Value& value) { + setWeightInternal(value); +} + +Napi::Value +FontFace::GetStatus(const Napi::CallbackInfo& info) { + switch (descriptor.status) { + case FontStatus::Unloaded: return Napi::String::New(env, "unloaded"); + case FontStatus::Loaded: return Napi::String::New(env, "loaded"); + case FontStatus::Error: return Napi::String::New(env, "error"); + } +} diff --git a/src/FontFace.h b/src/FontFace.h new file mode 100644 index 000000000..779101159 --- /dev/null +++ b/src/FontFace.h @@ -0,0 +1,32 @@ +// Copyright (c) 2024 Caleb Hearon +// +// TODO: ttc/otc with fragment identifier + +#pragma once + +#include +#include "Font.h" + +class FontFace : public Napi::ObjectWrap { + public: + FontFace(const Napi::CallbackInfo& info); + static void Initialize(Napi::Env& env, Napi::Object& target); + + Napi::Value GetFamily(const Napi::CallbackInfo& info); + Napi::Value GetStyle(const Napi::CallbackInfo& info); + Napi::Value GetWeight(const Napi::CallbackInfo& info); + Napi::Value GetStatus(const Napi::CallbackInfo& info); + bool setFamilyInternal(const Napi::Value& value); + void SetFamily(const Napi::CallbackInfo& info, const Napi::Value& value); + bool setStyleInternal(const Napi::Value& value); + void SetStyle(const Napi::CallbackInfo& info, const Napi::Value& value); + bool setWeightInternal(const Napi::Value& value); + void SetWeight(const Napi::CallbackInfo& info, const Napi::Value& value); + + size_t id; + + //TODO private + FontDescriptor descriptor; + private: + Napi::Env env; +}; diff --git a/src/FontFaceSet.cc b/src/FontFaceSet.cc new file mode 100644 index 000000000..4c06e70c4 --- /dev/null +++ b/src/FontFaceSet.cc @@ -0,0 +1,192 @@ +// Copyright (c) 2024 Caleb Hearon + +#include + +#include "FontFaceSet.h" +#include "InstanceData.h" + +FontFaceSet::FontFaceSet(Napi::CallbackInfo& info) : + env(info.Env()), + ready(Napi::Promise::Deferred::New(info.Env())), + Napi::ObjectWrap(info) { +} + +void +FontFaceSet::Initialize(Napi::Env& env, Napi::Object& exports) { + InstanceData *data = env.GetInstanceData(); + + Napi::Symbol iteratorSymbol = Napi::Symbol::WellKnown(env, "iterator").Unwrap(); + + Napi::Function ctor = DefineClass(env, "FontFaceSet", { + InstanceMethod<&FontFaceSet::Add>("add", napi_default_method), + InstanceMethod<&FontFaceSet::Has>("has", napi_default_method), + InstanceMethod<&FontFaceSet::Clear>("clear", napi_default_method), + InstanceMethod<&FontFaceSet::Delete>("delete", napi_default_method), + InstanceMethod<&FontFaceSet::Iterator>(iteratorSymbol, napi_default_jsproperty), + InstanceAccessor<&FontFaceSet::Size>("size", napi_default_jsproperty) + }); + + Napi::Object jsFonts = ctor.New({}).Unwrap(); + FontFaceSet* cppFonts = FontFaceSet::Unwrap(jsFonts); + + // FontFaceSet is a singleton. 2/3 browsers do not allow you to construct + // FontFaceSet, against specs, which allow you to use them to load groups. + data->cppFontSet = cppFonts; + data->jsFontSet = Napi::Persistent(jsFonts); + exports.Set("fonts", jsFonts); +} + +Napi::Value +FontFaceSet::Add(const Napi::CallbackInfo& info) { + InstanceData *data = env.GetInstanceData(); + + if (info.Length() == 0) { + Napi::TypeError::New(env, "face argument is required").ThrowAsJavaScriptException(); + return env.Undefined(); + } + + bool isFontFace; + Napi::Object obj; + + if (!info[0].IsObject()) { + isFontFace = false; + } else { + obj = info[0].As(); + if (!obj.InstanceOf(data->FontFaceCtor.Value()).UnwrapTo(&isFontFace)) return env.Undefined(); + } + if (!isFontFace) { + Napi::TypeError::New(env, "Expected instance of FontFace").ThrowAsJavaScriptException(); + return env.Undefined(); + } + + FontFace* face = FontFace::Unwrap(obj); + + if (!facesHash.contains(face->id)) { + facesHash.insert({face->id, facesData.size()}); + facesData.push_back({Napi::Persistent(obj), face}); + } + + return info.This(); // TODO not technically right +} + +Napi::Value +FontFaceSet::Has(const Napi::CallbackInfo& info) { + InstanceData *data = env.GetInstanceData(); + + if (info.Length() == 0) { + Napi::TypeError::New(env, "face argument is required").ThrowAsJavaScriptException(); + return env.Undefined(); + } + + bool isFontFace; + Napi::Object obj; + + if (!info[0].IsObject()) { + isFontFace = false; + } else { + obj = info[0].As(); + if (!obj.InstanceOf(data->FontFaceCtor.Value()).UnwrapTo(&isFontFace)) return env.Undefined(); + } + if (!isFontFace) { + Napi::TypeError::New(env, "Expected instance of FontFace").ThrowAsJavaScriptException(); + return env.Undefined(); + } + + FontFace* face = FontFace::Unwrap(obj); + + return facesHash.find(face->id) != facesHash.end() + ? Napi::Boolean::New(env, true) + : Napi::Boolean::New(env, false); +} + +void +FontFaceSet::Clear(const Napi::CallbackInfo& info) { + for (auto& entry : facesData) { + entry.face = nullptr; + entry.ref.Reset(); + } + facesHash.clear(); +} + +Napi::Value +FontFaceSet::Delete(const Napi::CallbackInfo& info) { + InstanceData *data = env.GetInstanceData(); + + if (info.Length() == 0) { + Napi::TypeError::New(env, "face argument is required").ThrowAsJavaScriptException(); + return env.Undefined(); + } + + bool isFontFace; + Napi::Object obj; + + if (!info[0].IsObject()) { + isFontFace = false; + } else { + obj = info[0].As(); + if (!obj.InstanceOf(data->FontFaceCtor.Value()).UnwrapTo(&isFontFace)) return env.Undefined(); + } + if (!isFontFace) { + Napi::TypeError::New(env, "Expected instance of FontFace").ThrowAsJavaScriptException(); + return env.Undefined(); + } + + FontFace* face = FontFace::Unwrap(obj); + + if (auto it = facesHash.find(face->id); it != facesHash.end()) { + facesData[it->second].face = nullptr; + facesData[it->second].ref.Reset(); + facesHash.erase(it); + loaded.erase(face->id); + failed.erase(face->id); + return Napi::Boolean::New(env, true); + } else { + return Napi::Boolean::New(env, false); + } +} + +struct IteratorContext { + // There is only ever one FontFaceSet so this should always be valid + FontFaceSet* set; + size_t index; + + static void finalize(Napi::Env env, IteratorContext* ctx) { + delete ctx; + } + + static Napi::Value next(const Napi::CallbackInfo& info) { + Napi::Env env = info.Env(); + IteratorContext* ctx = static_cast(info.Data()); + FontFaceSet& set = *(ctx->set); + + Napi::Object ret = Napi::Object::New(env); + + // Skip past empty slots + for (size_t i = ctx->index; i < set.facesData.size(); i++) { + auto& entry = set.facesData[ctx->index++]; + if (entry.face != nullptr) { + ret.Set("done", Napi::Boolean::New(env, false)); + ret.Set("value", entry.ref.Value()); + return ret; + } + } + + ret.Set("done", Napi::Boolean::New(env, true)); + return ret; + } +}; + +Napi::Value +FontFaceSet::Iterator(const Napi::CallbackInfo& info) { + Napi::Object iterator = Napi::Object::New(env); + IteratorContext* ctx = new IteratorContext{this, 0}; + iterator.AddFinalizer(IteratorContext::finalize, ctx); + Napi::Function next = Napi::Function::New(env, IteratorContext::next, "next", ctx); + iterator["next"] = next; + return iterator; +} + +Napi::Value +FontFaceSet::Size(const Napi::CallbackInfo& info) { + return Napi::Number::New(env, facesHash.size()); +} diff --git a/src/FontFaceSet.h b/src/FontFaceSet.h new file mode 100644 index 000000000..035ff3ef6 --- /dev/null +++ b/src/FontFaceSet.h @@ -0,0 +1,43 @@ +// Copyright (c) 2024 Caleb Hearon + +#pragma once + +#include +#include +#include + +#include "FontFace.h" + +static bool ref_compare(const FontFace* a, const FontFace* b) { + return a->id < b->id; +} + +struct FontFaceSetEntry { + Napi::ObjectReference ref; + FontFace* face; +}; + +class FontFaceSet : public Napi::ObjectWrap { + public: + FontFaceSet(Napi::CallbackInfo& info); + static void Initialize(Napi::Env& env, Napi::Object& exports); + + Napi::Value Add(const Napi::CallbackInfo& info); + Napi::Value Has(const Napi::CallbackInfo& info); + void Clear(const Napi::CallbackInfo& info); + Napi::Value Delete(const Napi::CallbackInfo& info); + Napi::Value Iterator(const Napi::CallbackInfo& info); + Napi::Value Size(const Napi::CallbackInfo& info); + + // Iteration of faces must be safe. We'll achieve this by iterating all keys + // ever inserted, which is how the EcmaScript standards say to iterate Sets. + std::unordered_map facesHash; + std::vector facesData; + + private: + std::set loading; + std::set loaded; + std::set failed; + Napi::Promise::Deferred ready; + Napi::Env env; +}; diff --git a/src/FontLayout.cc b/src/FontLayout.cc new file mode 100644 index 000000000..c2b02a020 --- /dev/null +++ b/src/FontLayout.cc @@ -0,0 +1,608 @@ +// Copyright (c) 2024 Caleb Hearon + +#include + +#include "FontLayout.h" +#include "Util.h" + +// Gets a code point from a UTF-16 string +// handling surrogate pairs appropriately +uint32_t +codePointAt(char16_t* str, size_t len, size_t* idx) { + uint16_t hi, low; + uint16_t code = str[(*idx)++]; + + // High surrogate + if (0xd800 <= code && code <= 0xdbff) { + hi = code; + if (*idx < len) { + low = str[(*idx)++]; + if (0xdc00 <= low && low <= 0xdfff) { + return ((hi - 0xd800) * 0x400) + (low - 0xdc00) + 0x10000; + } + } + return hi; + } + + return code; +} + +bool +compareGlyphRunTextPosition(const GlyphRun& a, const GlyphRun& b) { + return a.start < b.start; +} + +/* + * Gets the baseline adjustment in device pixels + */ +double getBaselineAdjustment(text_baseline_t baseline, FontMetrics metrics, double fontSize) { + switch (baseline) { + case TEXT_BASELINE_ALPHABETIC: + return 0; + case TEXT_BASELINE_TOP: + return metrics.central + fontSize / 2; + case TEXT_BASELINE_MIDDLE: + return metrics.central; + case TEXT_BASELINE_BOTTOM: + return metrics.central - fontSize / 2; + case TEXT_BASELINE_IDEOGRAPHIC: + return metrics.ideographic; + case TEXT_BASELINE_HANGING: + return metrics.hanging; + } +} + +text_align_t +resolveTextAlignment(canvas_state_t* state) { + text_align_t alignment = state->textAlignment; + + // Convert start/end to left/right based on direction + if (alignment == TEXT_ALIGNMENT_START) { + return (state->direction == "rtl") ? TEXT_ALIGNMENT_RIGHT : TEXT_ALIGNMENT_LEFT; + } else if (alignment == TEXT_ALIGNMENT_END) { + return (state->direction == "rtl") ? TEXT_ALIGNMENT_LEFT : TEXT_ALIGNMENT_RIGHT; + } + + return alignment; +} + +void +flushSegment( + Segment& segment, + std::vector& shapingWorkList, + std::vector& runs, + uint8_t level, + const FontDescriptor* face, + hb_font_t* hbfont, + hb_buffer_t* buffer, + bool isLastMatch +) { + if (segment.needsReshape && !isLastMatch) { + LOG("==> %zu..%zu needs reshape\n", segment.textStart, segment.textEnd - 1); + shapingWorkList.push_back(ShapeWork{segment.textStart, segment.textEnd}); + } else { + // copy the glyphs and add to the line, yay! + GlyphRun run; + hb_glyph_extents_t extents; + + run.face = face; + run.glyphs.resize(segment.glyphEnd - segment.glyphStart); + run.start = segment.textStart; + run.end = segment.textEnd; + run.level = level; + + hb_glyph_position_t* glyphPositions = hb_buffer_get_glyph_positions(buffer, nullptr); + hb_glyph_info_t* glyphInfos = hb_buffer_get_glyph_infos(buffer, nullptr); + + if (segment.needsReshape) { + LOG("==> %zu..%zu finished with tofu :(\n", segment.textStart, segment.textEnd - 1); + } else { + LOG("==> %zu..%zu success!\n", segment.textStart, segment.textEnd - 1); + } + + if (level & 1) { + int r = segment.glyphEnd - 1; + size_t w = 0; + for (int end = segment.glyphStart - 1; r > end; r--, w++) { + hb_font_get_glyph_extents(hbfont, glyphInfos[r].codepoint, &extents); + run.glyphs[w].id = glyphInfos[r].codepoint; + run.glyphs[w].cluster = glyphInfos[r].cluster; + run.glyphs[w].x_advance = glyphPositions[r].x_advance; + run.glyphs[w].y_advance = glyphPositions[r].y_advance; + run.glyphs[w].x_offset = glyphPositions[r].x_offset; + run.glyphs[w].y_offset = glyphPositions[r].y_offset; + run.glyphs[w].x_bearing = extents.x_bearing; + run.glyphs[w].y_bearing = extents.y_bearing; + run.glyphs[w].width = extents.width; + run.glyphs[w].height = extents.height; + } + } else { + size_t r = segment.glyphStart; + size_t w = 0; + for (; r < segment.glyphEnd; r++, w++) { + hb_font_get_glyph_extents(hbfont, glyphInfos[r].codepoint, &extents); + run.glyphs[w].id = glyphInfos[r].codepoint; + run.glyphs[w].cluster = glyphInfos[r].cluster; + run.glyphs[w].x_advance = glyphPositions[r].x_advance; + run.glyphs[w].y_advance = glyphPositions[r].y_advance; + run.glyphs[w].x_offset = glyphPositions[r].x_offset; + run.glyphs[w].y_offset = glyphPositions[r].y_offset; + run.glyphs[w].x_bearing = extents.x_bearing; + run.glyphs[w].y_bearing = extents.y_bearing; + run.glyphs[w].width = extents.width; + run.glyphs[w].height = extents.height; + } + } + + runs.push_back(run); + } +} + +static void +extractMetrics( + hb_font_t* hbfont, + double fontSize, + uint8_t level, + FontMetrics* metrics +) { + // ascender + hb_position_t ascender; + hb_ot_metrics_get_position_with_fallback( + hbfont, + HB_OT_METRICS_TAG_HORIZONTAL_ASCENDER, + &ascender + ); + metrics->ascender = ascender * fontSize / 1000; + + // descender + hb_position_t descender; + hb_ot_metrics_get_position_with_fallback( + hbfont, + HB_OT_METRICS_TAG_HORIZONTAL_DESCENDER, + &descender + ); + metrics->descender = descender * fontSize / 1000; + + // Idce + hb_position_t Idce; + bool hasIdce = hb_ot_layout_get_baseline( + hbfont, + HB_OT_LAYOUT_BASELINE_TAG_IDEO_EMBOX_CENTRAL, + level & 1 ? HB_DIRECTION_RTL : HB_DIRECTION_LTR, + HB_OT_TAG_DEFAULT_SCRIPT, + HB_OT_TAG_DEFAULT_LANGUAGE, + &Idce + ); + + // ideo + hb_position_t ideo; + bool hasIdeo = hb_ot_layout_get_baseline( + hbfont, + HB_OT_LAYOUT_BASELINE_TAG_IDEO_EMBOX_BOTTOM_OR_LEFT, + level & 1 ? HB_DIRECTION_RTL : HB_DIRECTION_LTR, + HB_OT_TAG_DEFAULT_SCRIPT, + HB_OT_TAG_DEFAULT_LANGUAGE, + &ideo + ); + + // idtp + hb_position_t idtp; + bool hasIdtp = hb_ot_layout_get_baseline( + hbfont, + HB_OT_LAYOUT_BASELINE_TAG_IDEO_EMBOX_TOP_OR_RIGHT, + level & 1 ? HB_DIRECTION_RTL : HB_DIRECTION_LTR, + HB_OT_TAG_DEFAULT_SCRIPT, + HB_OT_TAG_DEFAULT_LANGUAGE, + &idtp + ); + + if (hasIdce) { + metrics->central = Idce * fontSize / 1000; + } else if (hasIdeo && hasIdtp) { + metrics->central = (ideo + idtp) * fontSize / 2 / 1000; + } else { + metrics->central = (metrics->ascender + metrics->descender) / 2; + } + + if (hasIdeo) { + metrics->ideographic = ideo * fontSize / 1000; + } else { + metrics->ideographic = metrics->descender; + } + + hb_position_t hang; + bool hasHang = hb_ot_layout_get_baseline( + hbfont, + HB_OT_LAYOUT_BASELINE_TAG_HANGING, + level & 1 ? HB_DIRECTION_RTL : HB_DIRECTION_LTR, + HB_OT_TAG_DEFAULT_SCRIPT, + HB_OT_TAG_DEFAULT_LANGUAGE, + &hang + ); + + if (hasHang) { + metrics->hanging = hang * fontSize / 1000; + } else { + metrics->hanging = 0.6 * fontSize; + } +} + +static void +orderRange( + std::vector& line, + size_t begin, + size_t end +) { + uint8_t minlevel = 0xff; + + LOG("REORDERING %u..%u\n", begin, end); + + for (size_t i = begin; i < end; i++) { + minlevel = std::min(minlevel, line[i].level); + } + + size_t li = begin; // left inner + size_t ri = end - 1; // right inner + + while (li < ri) { + size_t lo = li; // left outer + size_t ro = ri; // right outer + + uint8_t lLevel = line[li].level; + uint8_t rLevel = line[ri].level; + + if (lLevel == minlevel) { + li++; + } else { + while (li <= ri && line[li].level >= lLevel) li++; + if (lo < li) orderRange(line, lo, li); + } + + if (rLevel == minlevel) { + ri--; + } else { + while (li <= ri && line[ri].level >= rLevel) ri--; + if (ri < ro) orderRange(line, ri + 1, ro + 1); + } + + if ((minlevel & 1) && li <= ri) { + size_t lsize = li - lo; + size_t rsize = ro - ri; + size_t swap = std::min(lsize, rsize); + for (size_t i = 0; i < swap; i++) { + std::swap(line[lo + i], line[ri + 1 + i]); + } + // deal with the remainder + if (lsize > rsize) { + std::vector temp(line.begin() + lo + rsize, line.begin() + li); + line.erase(line.begin() + lo + rsize, line.begin() + li); + line.insert(line.begin() + ro + 1 - temp.size(), temp.begin(), temp.end()); + } else if (rsize > lsize) { + std::vector temp(line.begin() + ri + 1 + lsize, line.begin() + ro + 1); + line.erase(line.begin() + ri + 1 + lsize, line.begin() + ro + 1); + line.insert(line.begin() + li, temp.begin(), temp.end()); + } + } + } +} + +static void +orderRunsPhysically( + std::vector& line, + canvas_state_t* state +) { + uint8_t levelOr = 0; + uint8_t levelAnd = 1; + + for (GlyphRun& run : line) { + levelOr |= run.level; + levelAnd &= run.level; + } + + // If none of the levels had the LSB set, all numbers were even + bool allEven = (levelOr & 1) == 0; + // If all of the levels had the LSB set, all numbers were odd + bool allOdd = (levelAnd & 1) == 1; + + if (!allEven && !allOdd) { + orderRange(line, 0, line.size()); + } else if (allOdd) { + std::reverse(line.begin(), line.end()); + } +} + +TextLayout +layoutText( + char16_t* textBuffer, + size_t textLength, + canvas_state_t* state, + InstanceData* data +) { + // All whitespace characters become spaces. Ported from Firefox. Removing + // paragraphs makes segmenting easier, and there's no concept of paragraphs in + // canvas. + for (size_t i = 0; i < textLength; ++i) { + char16_t ch = textBuffer[i]; + if (ch == u'\x09' || ch == u'\x0A' || ch == u'\x0B' || ch == u'\x0C' || + ch == u'\x0D' || ch == u'\x1C' || ch == u'\x1D' || ch == u'\x1E' || + ch == u'\x1F' || ch == u'\x85' || ch == u'\x2029') { + textBuffer[i] = u' '; + } + } + +#if LOG_LEVEL > 0 + auto script_to_string = [](script_t script) -> const char* { + switch (script) { + case SCRIPT_COMMON: return "Common"; + case SCRIPT_LATIN: return "Latin"; + case SCRIPT_GREEK: return "Greek"; + case SCRIPT_CYRILLIC: return "Cyrillic"; + case SCRIPT_ARMENIAN: return "Armenian"; + case SCRIPT_HEBREW: return "Hebrew"; + case SCRIPT_ARABIC: return "Arabic"; + case SCRIPT_INHERITED: return "Inherited"; + case SCRIPT_HIRAGANA: return "Hiragana"; + case SCRIPT_KATAKANA: return "Katakana"; + case SCRIPT_HAN: return "Han"; + case SCRIPT_DEVANAGARI: return "Devanagari"; + case SCRIPT_DESERET: return "Deseret"; + default: return "Unknown"; + } + }; +#endif + + LOG("Processing text with script iterator, buffer size: %zu\n", textLength); + + // Initialize script iterator + ItemizeState itemizer(textBuffer, textLength, state->direction == "ltr" ? 0 : 1); + std::vector fallbacks; + script_t lastScript = SCRIPT_NONE; + double fontSize = state->fontProperties.size; + + hb_buffer_t* buffer = hb_buffer_create(); + hb_buffer_set_cluster_level(buffer, HB_BUFFER_CLUSTER_LEVEL_MONOTONE_CHARACTERS); + + std::vector shapingWorkList; + TextLayout layout; + + bool foundFont = false; + + while (!itemizer.done && itemizer.offset < textLength) { + size_t start_offset = itemizer.offset; + + itemizeNext(itemizer); + + script_t script = itemizer.script_state.script; + uint8_t level = itemizer.bidi_state.level; + + if (lastScript != script) { + fallbacks.clear(); + data->fontManager.populateFallbackFonts(fallbacks, script); + lastScript = script; + } + + std::vector matches = data->fontManager.query( + state->fontProperties, + data->cppFontSet, + fallbacks + ); + +#if LOG_LEVEL > 0 + std::string cascade = ""; + for (const FontDescriptor* match : matches) { + if (!cascade.empty()) cascade += ", "; + cascade += match->family.get(); + cascade += "/" + std::to_string(match->weight) + "/"; + switch (match->style) { + case FontStyle::Normal: cascade += "Normal"; break; + case FontStyle::Italic: cascade += "Italic"; break; + case FontStyle::Oblique: cascade += "Oblique"; break; + } + } +#endif + + LOG( + "%zu..%zu script=%s dir=%hi cascade=%s\n", + start_offset, + itemizer.offset - 1, + script_to_string(script), + level, + cascade.c_str() + ); + + shapingWorkList.resize(1); + shapingWorkList[0].start = start_offset; + shapingWorkList[0].end = itemizer.offset; + size_t matchIndex = 0; + + while (shapingWorkList.size() && matchIndex < matches.size()) { + auto face = matches[matchIndex]; + face->load(); + if (face->data == nullptr) { + matchIndex++; + continue; + } + + LOG("==> %s\n", face->family.get()); + + // create the HarfBuzz font + hb_blob_t* hbblob = hb_blob_create( + reinterpret_cast(face->data.get()), + face->data_len, + HB_MEMORY_MODE_READONLY, + nullptr, + nullptr + ); + size_t count = hb_face_count(hbblob); + if (count > 1 && face->postscript) { + // Lazily initialize the ttc index for backends that don't provide it (macOS) + for (size_t index = 0; index < count; index++) { + char buf[128]; + unsigned int len = sizeof(buf); + hb_face_t* hbface = hb_face_create(hbblob, index); + hb_ot_name_get_utf8( + hbface, + HB_OT_NAME_ID_POSTSCRIPT_NAME, + hb_language_from_string("en", -1), + &len, + buf + ); + hb_face_destroy(hbface); + + if (strcmp(buf, face->postscript.get()) == 0) { + matches[matchIndex]->index = index; + face->index = index; + break; + } + } + } + hb_face_t* hbface = hb_face_create(hbblob, face->index); + hb_font_t* hbfont = hb_font_create(hbface); + hb_font_set_scale(hbfont, 1000, 1000); + + if (!foundFont) { + extractMetrics(hbfont, fontSize, level, &layout.metrics); + foundFont = true; + } + + // now we have to try to shape everything in shape work, and anything + // we can't shape, we push back onto the shape work for the next font + size_t endWorkForMatch = shapingWorkList.size(); + bool isLastMatch = matchIndex == matches.size() - 1; + for (size_t workIndex = 0; workIndex < endWorkForMatch; workIndex++) { + ShapeWork work = shapingWorkList[workIndex]; + + // shaping time! + hb_buffer_set_length(buffer, 0); + hb_buffer_add_utf16( + buffer, + reinterpret_cast(textBuffer), + textLength, + work.start, + work.end - work.start + ); + hb_buffer_set_script(buffer, script_to_hb_script(script)); + hb_buffer_set_language(buffer, hb_language_from_string(state->lang.c_str(), -1)); + hb_buffer_set_direction( + buffer, + level & 1 ? HB_DIRECTION_RTL : HB_DIRECTION_LTR + ); + hb_shape(hbfont, buffer, nullptr, 0); + + LOG("==> shaping %zu..%zu\n", work.start, work.end - 1); + + // reversing the entire buffer for RTL text will make the cluster + // numbers increase, so it's easier to iterate in parallel with the + // text itself. the glyphs will be reversed, so we'll have to swap the + // start and end glyph indexes below + if (level & 1) hb_buffer_reverse(buffer); + + // Scan for missing glyphs and create new fragments accordingly + + // glyph iteration + size_t glyphIndex = 0; + unsigned int glyphCount = hb_buffer_get_length(buffer); + hb_glyph_info_t* glyphInfos = hb_buffer_get_glyph_infos(buffer, nullptr); + + bool glyphsNeedReshape = false; + + // text iteration (graphemes, codepoints, and text index) + uint8_t graphemeState = 0; + size_t textIndex = work.start; + uint32_t leftCodepoint = codePointAt(textBuffer, textLength, &textIndex); + size_t graphemeDivider; + + // the working segment + Segment segment {work.start, work.start, glyphIndex, glyphIndex, false}; + + // TODO: assert: textIndex <= w.end + do { + graphemeDivider = textIndex; + while (glyphIndex < glyphCount && glyphInfos[glyphIndex].cluster < graphemeDivider) { + if (glyphInfos[glyphIndex].codepoint == 0) { + glyphsNeedReshape = true; + } + glyphIndex++; + } + + bool isBreak; + uint32_t rightCodepoint; + + if (graphemeDivider >= work.end) { + isBreak = true; + } else { + rightCodepoint = codePointAt(textBuffer, textLength, &textIndex); + isBreak = grapheme_break(&graphemeState, leftCodepoint, rightCodepoint); + } + + if (isBreak) { + if (segment.textStart == segment.textEnd) { + segment.needsReshape = glyphsNeedReshape; // very first grapheme + } + if (glyphsNeedReshape == segment.needsReshape) { + // extend the segment + segment.textEnd = graphemeDivider; + segment.glyphEnd = glyphIndex; + } else { + // finish the last segment, not including this grapheme + flushSegment(segment, shapingWorkList, layout.runs, level, face, hbfont, buffer, isLastMatch); + + // start a new segment that includes this grapheme + segment.textStart = segment.textEnd; + segment.textEnd = graphemeDivider; + segment.glyphStart = segment.glyphEnd; + segment.glyphEnd = glyphIndex; + segment.needsReshape = glyphsNeedReshape; + } + + if (graphemeDivider >= work.end) { + flushSegment(segment, shapingWorkList, layout.runs, level, face, hbfont, buffer, isLastMatch); + } + + glyphsNeedReshape = false; + } + + leftCodepoint = rightCodepoint; + } while (graphemeDivider < work.end); + } + + matchIndex++; + hb_font_destroy(hbfont); + hb_face_destroy(hbface); + hb_blob_destroy(hbblob); + shapingWorkList.erase( + shapingWorkList.begin(), + shapingWorkList.begin() + endWorkForMatch + ); + } + } + + hb_buffer_destroy(buffer); + + std::sort(layout.runs.begin(), layout.runs.end(), compareGlyphRunTextPosition); + orderRunsPhysically(layout.runs, state); + + layout.anchorY = getBaselineAdjustment(state->textBaseline, layout.metrics, fontSize); + + layout.width = 0; + for (GlyphRun& run : layout.runs) { + double toPx = fontSize / 1000; + for (Glyph& glyph : run.glyphs) { + layout.width += glyph.x_advance * toPx; + } + } + + switch (resolveTextAlignment(state)) { + case TEXT_ALIGNMENT_CENTER: + layout.anchorX = layout.width / 2; + break; + case TEXT_ALIGNMENT_RIGHT: + layout.anchorX = layout.width; + break; + default: // TEXT_ALIGNMENT_LEFT + layout.anchorX = 0; + break; + } + + return layout; +} diff --git a/src/FontLayout.h b/src/FontLayout.h new file mode 100644 index 000000000..8c403f4dc --- /dev/null +++ b/src/FontLayout.h @@ -0,0 +1,94 @@ +// Copyright (c) 2024 Caleb Hearon + +#pragma once + +#include +#include +#include + +#include "InstanceData.h" +#include "Font.h" +#include "FontFaceSet.h" +#include "itemize.h" +#include "Canvas.h" +#include "CanvasRenderingContext2d.h" + +struct canvas_state_t; + +struct ShapeWork { + size_t start; + size_t end; +}; + +struct Segment { + size_t textStart; + size_t textEnd; + size_t glyphStart; + size_t glyphEnd; + bool needsReshape; +}; + +struct Glyph { + hb_codepoint_t id; + uint32_t cluster; + hb_position_t x_advance; + hb_position_t y_advance; + hb_position_t x_offset; + hb_position_t y_offset; + hb_position_t x_bearing; + hb_position_t y_bearing; + hb_position_t width; + hb_position_t height; +}; + +struct GlyphRun { + const FontDescriptor* face; + std::vector glyphs; + size_t start; + size_t end; + uint8_t level; +}; + +struct FontMetrics { + double ascender; + double descender; + double central; + double ideographic; + double hanging; +}; + +bool +compareGlyphRunTextPosition(const GlyphRun& a, const GlyphRun& b); + +text_align_t +resolveTextAlignment(canvas_state_t* state); + +void +flushSegment( + Segment& segment, + std::vector& shapingWorkList, + std::vector& line, + uint8_t level, + const FontDescriptor* face, + hb_font_t* hbfont, + hb_buffer_t* buffer, + bool isLastMatch +); + +struct TextLayout { + std::vector runs; + // Increasing values move from the first glyph to the right (always positive) + double anchorX; + // Increasing values move from the baseline up (can be negative) + double anchorY; + FontMetrics metrics{0, 0, 0, 0, 0}; + double width; +}; + +TextLayout +layoutText( + char16_t* textBuffer, + size_t textLength, + canvas_state_t* state, + InstanceData* data +); diff --git a/src/FontManager.cc b/src/FontManager.cc new file mode 100644 index 000000000..d0a31b377 --- /dev/null +++ b/src/FontManager.cc @@ -0,0 +1,269 @@ +#include + +#include "FontManager.h" +#include "FontFaceSet.h" +#include "Util.h" + +bool +compareFamilyNames(const char* str1, size_t len1, const char* str2, size_t len2) { + size_t start1 = 0; + size_t end1 = len1; + size_t start2 = 0; + size_t end2 = len2; + + while (start1 < len1 && std::isspace(str1[start1])) start1++; + while (end1 > start1 && std::isspace(str1[end1 - 1])) end1--; + + while (start2 < len2 && std::isspace(str2[start2])) start2++; + while (end2 > start2 && std::isspace(str2[end2 - 1])) end2--; + + if (end1 - start1 != end2 - start2) return false; + + for (size_t i = 0; i < end1 - start1; i++) { + if (std::tolower(str1[start1 + i]) != std::tolower(str2[start2 + i])) { + return false; + } + } + + return true; +} + + +void +FontManager::narrowByStretch( + std::vector& fonts, + FontProperties& properties +) { + size_t matchBegin = 0; + size_t matchEnd = 0; + uint16_t bestScore = 151; // max distance is 150 + + std::sort( + fonts.begin(), + fonts.end(), + [](FontDescriptor* a, FontDescriptor* b) { + return a->stretch < b->stretch; + } + ); + + for (size_t i = 0; i < fonts.size(); i++) { + if (fonts[i]->stretch == properties.stretch) { + if (matchBegin == matchEnd) { + matchBegin = i; + matchEnd = i + 1; + } else { + matchEnd++; + } + } else { + uint16_t score = properties.stretch > fonts[i]->stretch + ? properties.stretch - fonts[i]->stretch + : fonts[i]->stretch - properties.stretch; + if (score < bestScore) bestScore = score; + } + } + + if (matchBegin == matchEnd) { // no exact match + bool inScore = false; + + if (properties.stretch > 100) { + std::reverse(fonts.begin(), fonts.end()); + } + + for (size_t i = 0; i < fonts.size(); i++) { + uint16_t score = properties.stretch > fonts[i]->stretch + ? properties.stretch - fonts[i]->stretch + : fonts[i]->stretch - properties.stretch; + + if (inScore) { + if (score == bestScore) { + matchEnd++; + } else { + break; + } + } else if (score == bestScore) { + matchBegin = i; + matchEnd = i + 1; + inScore = true; + } + } + } + + if (matchBegin < matchEnd) { + fonts.erase(fonts.begin() + matchEnd, fonts.end()); + fonts.erase(fonts.begin(), fonts.begin() + matchBegin); + } +} + +void +FontManager::narrowByStyle( + std::vector& fonts, + FontProperties& properties +) { + size_t nNormal = 0; + size_t nItalic = 0; + size_t nOblique = 0; + + for (FontDescriptor* font : fonts) { + switch (font->style) { + case FontStyle::Normal: nNormal++; break; + case FontStyle::Italic: nItalic++; break; + case FontStyle::Oblique: nOblique++; break; + } + } + + FontStyle choose; + switch (properties.style) { + case FontStyle::Normal: + choose = nNormal ? FontStyle::Normal : nOblique ? FontStyle::Oblique : FontStyle::Italic; + break; + case FontStyle::Italic: + choose = nItalic ? FontStyle::Italic : nOblique ? FontStyle::Oblique : FontStyle::Normal; + break; + case FontStyle::Oblique: + choose = nOblique ? FontStyle::Oblique : nItalic ? FontStyle::Italic : FontStyle::Normal; + break; + } + + for (size_t i = 0; i < fonts.size(); ) { + if (fonts[i]->style != choose) { + std::swap(fonts[i], fonts[fonts.size() - 1]); + fonts.pop_back(); + } else { + i++; + } + } +} + +FontDescriptor* +FontManager::narrowByWeight( + std::vector fonts, + FontProperties& properties +) { + std::sort( + fonts.begin(), + fonts.end(), + [](FontDescriptor* a, FontDescriptor* b) { + return a->weight < b->weight; + } + ); + + assert(fonts.size() && "Precondition failed: 1 or 0 fonts in the set"); + + for (FontDescriptor* font : fonts) { + if (font->weight == properties.weight) { + return font; + } + } + + FontDescriptor* bestBelow = nullptr; + size_t bestBelowDistance = 900; // max possible is 800 + FontDescriptor* bestAbove = nullptr; + size_t bestAboveDistance = 900; + size_t divider = properties.weight == 400 ? 500 + : properties.weight == 500 ? 400 + : properties.weight; + + for (FontDescriptor* font : fonts) { + size_t distance = font->weight < properties.weight + ? properties.weight - font->weight + : font->weight - properties.weight; + + if (font->weight < divider) { + if (distance < bestBelowDistance) { + bestBelow = font; + bestBelowDistance = distance; + } + } else { + if (distance < bestAboveDistance) { + bestAbove = font; + bestAboveDistance = distance; + } + } + } + + if (bestBelow && bestAbove) { + if (bestBelowDistance == bestAboveDistance) { + return divider <= 500 ? bestBelow : bestAbove; + } else if (bestBelowDistance < bestAboveDistance) { + return bestBelow; + } else { + return bestAbove; + } + } else { + return bestBelow ? bestBelow : bestAbove; + } +} + +/** + * NOTE: the FontDescriptor is owned by the FontManager; do not use it again! + */ +std::vector +FontManager::query( + FontProperties& properties, + FontFaceSet* registered, + std::vector& fallbacks +) { + std::vector allFamilyResults; + std::vector familyResults; + + auto maybeAdd = [&](const std::string& family, FontDescriptor* desc) { + if ( + compareFamilyNames( + family.c_str(), + family.size(), + desc->family.get(), + strlen(desc->family.get()) + ) && std::find( + familyResults.begin(), + familyResults.end(), + desc + ) == familyResults.end() + ) familyResults.push_back(desc); + }; + + for (const std::string& family : properties.families) { + auto genericFamilies = getGenericList(family); + if (genericFamilies) { + for (const std::string& family : **genericFamilies) { + for (FontDescriptor& desc : system_fonts) { + maybeAdd(family, &desc); + } + } + } else { + for (auto& entry : registered->facesData) { + if (entry.face != nullptr) maybeAdd(family, &(entry.face->descriptor)); + } + for (FontDescriptor& desc : system_fonts) { + maybeAdd(family, &desc); + } + } + + if (familyResults.size() == 1) { + allFamilyResults.push_back(familyResults[0]); + familyResults.clear(); + } else if (familyResults.size() > 1) { + narrowByStretch(familyResults, properties); + narrowByStyle(familyResults, properties); + allFamilyResults.push_back(narrowByWeight(familyResults, properties)); + familyResults.clear(); + } + } + + for (const std::string& fallback : fallbacks) { + for (FontDescriptor& desc : system_fonts) { + maybeAdd(fallback, &desc); + } + + if (familyResults.size() == 1) { + allFamilyResults.push_back(familyResults[0]); + familyResults.clear(); + } else if (familyResults.size() > 1) { + narrowByStretch(familyResults, properties); + narrowByStyle(familyResults, properties); + allFamilyResults.push_back(narrowByWeight(familyResults, properties)); + familyResults.clear(); + } + } + + return allFamilyResults; +} diff --git a/src/FontManager.h b/src/FontManager.h new file mode 100644 index 000000000..ec473ccb8 --- /dev/null +++ b/src/FontManager.h @@ -0,0 +1,48 @@ +#pragma once + +#include + +#include "Font.h" +#include "FontFaceSet.h" +#include "unicode.h" + +class FontManager { + public: + virtual ~FontManager() = default; + + virtual void readSystemFonts() = 0; + + virtual void populateFallbackFonts( + std::vector& families, + script_t script + ) = 0; + + virtual std::optional*> getGenericList( + const std::string& generic + ) = 0; + + std::vector query( + FontProperties& properties, + FontFaceSet* registered, + std::vector& fallbacks + ); + + private: + void narrowByStretch( + std::vector& fonts, + FontProperties& properties + ); + + void narrowByStyle( + std::vector& fonts, + FontProperties& properties + ); + + FontDescriptor* narrowByWeight( + std::vector fonts, + FontProperties& properties + ); + + protected: + std::vector system_fonts; +}; diff --git a/src/FontManagerLinux.cc b/src/FontManagerLinux.cc new file mode 100644 index 000000000..004bbcf4a --- /dev/null +++ b/src/FontManagerLinux.cc @@ -0,0 +1,774 @@ +// Copyright (c) 2025 Caleb Hearon + +#include + +#include +#include + +#include "FontManagerLinux.h" +#include "Font.h" +#include "unicode.h" + +// Fontconfig types and constants (from fontconfig/fontconfig.h) +typedef unsigned char FcChar8; +typedef unsigned int FcChar32; +typedef int FcBool; + +typedef struct _FcPattern FcPattern; +typedef struct _FcCharSet FcCharSet; +typedef struct _FcConfig FcConfig; +typedef struct _FcObjectSet FcObjectSet; + +// FcFontSet layout is part of fontconfig's stable public ABI +typedef struct _FcFontSet { + int nfont; + int sfont; + FcPattern **fonts; +} FcFontSet; + +typedef enum _FcResult { + FcResultMatch, FcResultNoMatch, FcResultTypeMismatch, + FcResultNoId, FcResultOutOfMemory, +} FcResult; + +typedef enum _FcMatchKind { + FcMatchPattern, FcMatchFont, FcMatchScan, +} FcMatchKind; + +static constexpr FcBool FcFalse = 0; +static constexpr FcBool FcTrue = 1; + +static constexpr const char* FC_FAMILY = "family"; +static constexpr const char* FC_FILE = "file"; +static constexpr const char* FC_INDEX = "index"; +static constexpr const char* FC_WEIGHT = "weight"; +static constexpr const char* FC_WIDTH = "width"; +static constexpr const char* FC_SLANT = "slant"; +static constexpr const char* FC_LANG = "lang"; +static constexpr const char* FC_CHARSET = "charset"; +static constexpr const char* FC_COLOR = "color"; + +static constexpr int FC_SLANT_ROMAN = 0; +static constexpr int FC_SLANT_ITALIC = 100; +static constexpr int FC_SLANT_OBLIQUE = 110; + +static constexpr int FC_WEIGHT_NORMAL = 80; // FC_WEIGHT_REGULAR in the header +static constexpr int FC_WIDTH_NORMAL = 100; + +struct Fc { + void* handle = nullptr; + + using fn_Init = FcBool (*)(); + using fn_PatternCreate = FcPattern* (*)(); + using fn_PatternDestroy = void (*)(FcPattern*); + using fn_PatternAddString = FcBool (*)(FcPattern*, const char*, const FcChar8*); + using fn_PatternAddBool = FcBool (*)(FcPattern*, const char*, FcBool); + using fn_PatternAddCharSet = FcBool (*)(FcPattern*, const char*, const FcCharSet*); + using fn_PatternGetString = FcResult (*)(const FcPattern*, const char*, int, FcChar8**); + using fn_PatternGetInteger = FcResult (*)(const FcPattern*, const char*, int, int*); + using fn_ConfigSubstitute = FcBool (*)(FcConfig*, FcPattern*, FcMatchKind); + using fn_DefaultSubstitute = void (*)(FcPattern*); + using fn_FontMatch = FcPattern* (*)(FcConfig*, FcPattern*, FcResult*); + using fn_FontSort = FcFontSet* (*)(FcConfig*, FcPattern*, FcBool, FcCharSet**, FcResult*); + using fn_FontList = FcFontSet* (*)(FcConfig*, FcPattern*, FcObjectSet*); + using fn_FontSetDestroy = void (*)(FcFontSet*); + using fn_ObjectSetCreate = FcObjectSet* (*)(); + using fn_ObjectSetAdd = FcBool (*)(FcObjectSet*, const char*); + using fn_ObjectSetDestroy = void (*)(FcObjectSet*); + using fn_CharSetCreate = FcCharSet* (*)(); + using fn_CharSetDestroy = void (*)(FcCharSet*); + using fn_CharSetAddChar = FcBool (*)(FcCharSet*, FcChar32); + using fn_WeightToOpenType = int (*)(int); + + fn_Init Init = nullptr; + fn_PatternCreate PatternCreate = nullptr; + fn_PatternDestroy PatternDestroy = nullptr; + fn_PatternAddString PatternAddString = nullptr; + fn_PatternAddBool PatternAddBool = nullptr; + fn_PatternAddCharSet PatternAddCharSet = nullptr; + fn_PatternGetString PatternGetString = nullptr; + fn_PatternGetInteger PatternGetInteger = nullptr; + fn_ConfigSubstitute ConfigSubstitute = nullptr; + fn_DefaultSubstitute DefaultSubstitute = nullptr; + fn_FontMatch FontMatch = nullptr; + fn_FontSort FontSort = nullptr; + fn_FontList FontList = nullptr; + fn_FontSetDestroy FontSetDestroy = nullptr; + fn_ObjectSetCreate ObjectSetCreate = nullptr; + fn_ObjectSetAdd ObjectSetAdd = nullptr; + fn_ObjectSetDestroy ObjectSetDestroy = nullptr; + fn_CharSetCreate CharSetCreate = nullptr; + fn_CharSetDestroy CharSetDestroy = nullptr; + fn_CharSetAddChar CharSetAddChar = nullptr; + fn_WeightToOpenType WeightToOpenType = nullptr; +}; + +static Fc* +loadFc() { + static Fc fc; + + if (fc.handle || fc.PatternCreate) + return fc.handle ? &fc : nullptr; + + fc.handle = dlopen("libfontconfig.so.1", RTLD_NOW); + if (!fc.handle) return nullptr; + + fc.Init = (Fc::fn_Init) dlsym(fc.handle, "FcInit"); + fc.PatternCreate = (Fc::fn_PatternCreate) dlsym(fc.handle, "FcPatternCreate"); + fc.PatternDestroy = (Fc::fn_PatternDestroy) dlsym(fc.handle, "FcPatternDestroy"); + fc.PatternAddString = (Fc::fn_PatternAddString) dlsym(fc.handle, "FcPatternAddString"); + fc.PatternAddBool = (Fc::fn_PatternAddBool) dlsym(fc.handle, "FcPatternAddBool"); + fc.PatternAddCharSet = (Fc::fn_PatternAddCharSet)dlsym(fc.handle, "FcPatternAddCharSet"); + fc.PatternGetString = (Fc::fn_PatternGetString) dlsym(fc.handle, "FcPatternGetString"); + fc.PatternGetInteger = (Fc::fn_PatternGetInteger)dlsym(fc.handle, "FcPatternGetInteger"); + fc.ConfigSubstitute = (Fc::fn_ConfigSubstitute) dlsym(fc.handle, "FcConfigSubstitute"); + fc.DefaultSubstitute = (Fc::fn_DefaultSubstitute)dlsym(fc.handle, "FcDefaultSubstitute"); + fc.FontMatch = (Fc::fn_FontMatch) dlsym(fc.handle, "FcFontMatch"); + fc.FontSort = (Fc::fn_FontSort) dlsym(fc.handle, "FcFontSort"); + fc.FontList = (Fc::fn_FontList) dlsym(fc.handle, "FcFontList"); + fc.FontSetDestroy = (Fc::fn_FontSetDestroy) dlsym(fc.handle, "FcFontSetDestroy"); + fc.ObjectSetCreate = (Fc::fn_ObjectSetCreate) dlsym(fc.handle, "FcObjectSetCreate"); + fc.ObjectSetAdd = (Fc::fn_ObjectSetAdd) dlsym(fc.handle, "FcObjectSetAdd"); + fc.ObjectSetDestroy = (Fc::fn_ObjectSetDestroy) dlsym(fc.handle, "FcObjectSetDestroy"); + fc.CharSetCreate = (Fc::fn_CharSetCreate) dlsym(fc.handle, "FcCharSetCreate"); + fc.CharSetDestroy = (Fc::fn_CharSetDestroy) dlsym(fc.handle, "FcCharSetDestroy"); + fc.CharSetAddChar = (Fc::fn_CharSetAddChar) dlsym(fc.handle, "FcCharSetAddChar"); + fc.WeightToOpenType = (Fc::fn_WeightToOpenType) dlsym(fc.handle, "FcWeightToOpenType"); + + if (!fc.PatternCreate || !fc.FontMatch || !fc.PatternGetString) { + dlclose(fc.handle); + fc.handle = nullptr; + return nullptr; + } + + return &fc; +} + +static std::string +familyForGeneric(Fc* fc, const char* generic) { + FcPattern* pattern = fc->PatternCreate(); + fc->PatternAddString(pattern, FC_FAMILY, reinterpret_cast(generic)); + fc->ConfigSubstitute(nullptr, pattern, FcMatchPattern); + fc->DefaultSubstitute(pattern); + + FcResult result; + FcPattern* match = fc->FontMatch(nullptr, pattern, &result); + + std::string name; + if (match) { + FcChar8* family = nullptr; + if (fc->PatternGetString(match, FC_FAMILY, 0, &family) == FcResultMatch) { + name = reinterpret_cast(family); + } + fc->PatternDestroy(match); + } + + fc->PatternDestroy(pattern); + return name; +} + +static void +addFamilyFromFcSort(Fc* fc, FcPattern* pattern, std::vector& results) { + fc->ConfigSubstitute(nullptr, pattern, FcMatchPattern); + fc->DefaultSubstitute(pattern); + + FcResult result; + FcFontSet* set = fc->FontSort(nullptr, pattern, FcFalse, nullptr, &result); + + for (int i = 0; i < set->nfont; i++) { + FcChar8* family = nullptr; + if (fc->PatternGetString(set->fonts[i], FC_FAMILY, 0, &family) == FcResultMatch) { + results.push_back(std::string((char*)family)); + break; + } + } + + fc->FontSetDestroy(set); +} + +static void +addFamilyForChars( + Fc* fc, + std::vector& list, + FcPattern* pattern, + uint32_t cp1, + uint32_t cp2 +) { + FcCharSet* cset = fc->CharSetCreate(); + fc->CharSetAddChar(cset, cp1); + fc->CharSetAddChar(cset, cp2); + fc->PatternAddString(pattern, FC_FAMILY, (FcChar8*)"sans"); + fc->PatternAddCharSet(pattern, FC_CHARSET, cset); + addFamilyFromFcSort(fc, pattern, list); + fc->CharSetDestroy(cset); +} + +const std::vector& +FontManagerLinux::langFamilies(const std::string& lang) { + auto it = lang_cache.find(lang); + if (it != lang_cache.end()) return it->second; + + static const std::vector empty; + Fc* fc = loadFc(); + if (!fc) return lang_cache.emplace(lang, empty).first->second; + + FcPattern* pattern = fc->PatternCreate(); + fc->PatternAddString(pattern, FC_LANG, reinterpret_cast(lang.c_str())); + std::vector results; + addFamilyFromFcSort(fc, pattern, results); + fc->PatternDestroy(pattern); + + return lang_cache.emplace(lang, std::move(results)).first->second; +} + +static uint16_t +convertWeight(Fc* fc, int fcWeight) { + int ot = fc->WeightToOpenType(fcWeight); + if (ot < 1) return 400; + if (ot > 1000) return 1000; + return static_cast(ot); +} + +static FontStyle +convertSlant(int slant) { + switch (slant) { + case FC_SLANT_ITALIC: return FontStyle::Italic; + case FC_SLANT_OBLIQUE: return FontStyle::Oblique; + case FC_SLANT_ROMAN: + default: return FontStyle::Normal; + } +} + +void +FontManagerLinux::readSystemFonts() { + Fc* fc = loadFc(); + if (!fc) return; + + fc->Init(); + + for (const char* generic : {"serif", "sans-serif", "monospace", "cursive", "fantasy"}) { + std::string resolved = familyForGeneric(fc, generic); + if (!resolved.empty()) generic_fonts[generic].push_back(std::move(resolved)); + } + + { + FcPattern* pattern = fc->PatternCreate(); + fc->PatternAddBool(pattern, FC_COLOR, FcTrue); + addFamilyForChars(fc, emoji_fonts, pattern, 0x1f602, 0x1f602); // face with tears of joy + fc->PatternDestroy(pattern); + } + + { + FcPattern* pattern = fc->PatternCreate(); + addFamilyForChars(fc, symbol_fonts, pattern, 0x2192, 0x1f846); // right arow, thick right arrow + fc->PatternDestroy(pattern); + } + + FcPattern* pattern = fc->PatternCreate(); + FcObjectSet* objects = fc->ObjectSetCreate(); + fc->ObjectSetAdd(objects, FC_FAMILY); + fc->ObjectSetAdd(objects, FC_FILE); + fc->ObjectSetAdd(objects, FC_INDEX); + fc->ObjectSetAdd(objects, FC_WEIGHT); + fc->ObjectSetAdd(objects, FC_WIDTH); + fc->ObjectSetAdd(objects, FC_SLANT); + + FcFontSet* set = fc->FontList(nullptr, pattern, objects); + + if (set) { + system_fonts.reserve(set->nfont); + + for (int i = 0; i < set->nfont; i++) { + FcPattern* font = set->fonts[i]; + + FcChar8* file = nullptr; + FcChar8* family = nullptr; + if (fc->PatternGetString(font, FC_FILE, 0, &file) != FcResultMatch) continue; + if (fc->PatternGetString(font, FC_FAMILY, 0, &family) != FcResultMatch) continue; + + FontDescriptor desc; + + size_t fileLength = std::strlen((const char*)file) + 1; + desc.url = std::make_unique(fileLength); + std::memcpy(desc.url.get(), file, fileLength); + + size_t familyLength = std::strlen((const char*)family) + 1; + desc.family = std::make_unique(familyLength); + std::memcpy(desc.family.get(), family, familyLength); + + int index = 0; + fc->PatternGetInteger(font, FC_INDEX, 0, &index); + desc.index = (size_t)index; + + int weight = FC_WEIGHT_NORMAL; + fc->PatternGetInteger(font, FC_WEIGHT, 0, &weight); + desc.weight = convertWeight(fc, weight); + + int width = FC_WIDTH_NORMAL; + fc->PatternGetInteger(font, FC_WIDTH, 0, &width); + desc.stretch = (uint16_t)(width); + + int slant = FC_SLANT_ROMAN; + fc->PatternGetInteger(font, FC_SLANT, 0, &slant); + desc.style = convertSlant(slant); + + system_fonts.push_back(std::move(desc)); + } + + fc->FontSetDestroy(set); + } + + fc->ObjectSetDestroy(objects); + fc->PatternDestroy(pattern); +} + +void +FontManagerLinux::populateFallbackFonts( + std::vector& families, + script_t script +) { + const char* lang = nullptr; + + switch (script) { + case SCRIPT_COMMON: + case SCRIPT_INHERITED: + case SCRIPT_LATIN: + lang = "en"; + break; + case SCRIPT_CYRILLIC: lang = "ru"; break; + case SCRIPT_GREEK: lang = "el"; break; + case SCRIPT_ARMENIAN: lang = "hy"; break; + case SCRIPT_HEBREW: lang = "he"; break; + case SCRIPT_ARABIC: lang = "ar"; break; + case SCRIPT_SYRIAC: lang = "syr"; break; + case SCRIPT_THAANA: lang = "dv"; break; + case SCRIPT_DEVANAGARI: lang = "hi"; break; + case SCRIPT_BENGALI: lang = "bn"; break; + case SCRIPT_GUJARATI: lang = "gu"; break; + case SCRIPT_GURMUKHI: lang = "pa"; break; + case SCRIPT_KANNADA: lang = "kn"; break; + case SCRIPT_MALAYALAM: lang = "ml"; break; + case SCRIPT_ORIYA: lang = "or"; break; + case SCRIPT_TAMIL: lang = "ta"; break; + case SCRIPT_TELUGU: lang = "te"; break; + case SCRIPT_SINHALA: lang = "si"; break; + case SCRIPT_THAI: lang = "th"; break; + case SCRIPT_LAO: lang = "lo"; break; + case SCRIPT_TIBETAN: lang = "bo"; break; + case SCRIPT_MYANMAR: lang = "my"; break; + case SCRIPT_KHMER: lang = "km"; break; + case SCRIPT_GEORGIAN: lang = "ka"; break; + case SCRIPT_ETHIOPIC: lang = "am"; break; + case SCRIPT_CHEROKEE: lang = "chr"; break; + case SCRIPT_CANADIAN_ABORIGINAL: lang = "iu"; break; + case SCRIPT_YI: lang = "ii"; break; + case SCRIPT_COPTIC: lang = "cop"; break; + case SCRIPT_GOTHIC: lang = "got"; break; + case SCRIPT_NKO: lang = "nqo"; break; + case SCRIPT_TIFINAGH: lang = "ber-ma"; break; + case SCRIPT_MONGOLIAN: lang = "mn-cn"; break; + case SCRIPT_HANGUL: lang = "ko"; break; + case SCRIPT_HAN: + case SCRIPT_BOPOMOFO: + lang = "zh-cn"; + break; + case SCRIPT_HIRAGANA: + case SCRIPT_KATAKANA: + lang = "ja"; + break; + + // These scripts don't appear to be covered by FontConfig .orth files + case SCRIPT_DESERET: + families.push_back("Noto Sans Deseret"); + break; + case SCRIPT_OGHAM: + families.push_back("Noto Sans Ogham"); + break; + case SCRIPT_OLD_ITALIC: + families.push_back("Noto Sans Old Italic"); + break; + case SCRIPT_RUNIC: + families.push_back("Noto Sans Runic"); + break; + case SCRIPT_TAGALOG: + families.push_back("Noto Sans Tagalog"); + break; + case SCRIPT_HANUNOO: + families.push_back("Noto Sans Hanunoo"); + break; + case SCRIPT_BUHID: + families.push_back("Noto Sans Buhid"); + break; + case SCRIPT_TAGBANWA: + families.push_back("Noto Sans Tagbanwa"); + break; + case SCRIPT_BRAILLE: + families.push_back("Noto Sans Symbols2"); + break; + case SCRIPT_CYPRIOT: + families.push_back("Noto Sans Cypriot"); + break; + case SCRIPT_LIMBU: + families.push_back("Noto Sans Limbu"); + break; + case SCRIPT_LINEAR_B: + families.push_back("Noto Sans Linear B"); + break; + case SCRIPT_OSMANYA: + families.push_back("Noto Sans Osmanya"); + break; + case SCRIPT_SHAVIAN: + families.push_back("Noto Sans Shavian"); + break; + case SCRIPT_TAI_LE: + families.push_back("Noto Sans Tai Le"); + break; + case SCRIPT_UGARITIC: + families.push_back("Noto Sans Ugaritic"); + break; + case SCRIPT_BUGINESE: + families.push_back("Noto Sans Buginese"); + break; + case SCRIPT_GLAGOLITIC: + families.push_back("Noto Sans Glagolitic"); + break; + case SCRIPT_KHAROSHTHI: + families.push_back("Noto Sans Kharoshthi"); + break; + case SCRIPT_SYLOTI_NAGRI: + families.push_back("Noto Sans Syloti Nagri"); + break; + case SCRIPT_NEW_TAI_LUE: + families.push_back("Noto Sans New Tai Lue"); + break; + case SCRIPT_OLD_PERSIAN: + families.push_back("Noto Sans Old Persian"); + break; + case SCRIPT_BALINESE: + families.push_back("Noto Sans Balinese"); + break; + case SCRIPT_BATAK: + families.push_back("Noto Sans Batak"); + break; + case SCRIPT_BRAHMI: + families.push_back("Noto Sans Brahmi"); + break; + case SCRIPT_CHAM: + families.push_back("Noto Sans Cham"); + break; + case SCRIPT_EGYPTIAN_HIEROGLYPHS: + families.push_back("Noto Sans Egyptian Hieroglyphs"); + break; + case SCRIPT_PAHAWH_HMONG: + families.push_back("Noto Sans Pahawh Hmong"); + break; + case SCRIPT_OLD_HUNGARIAN: + families.push_back("Noto Sans Old Hungarian"); + break; + case SCRIPT_JAVANESE: + families.push_back("Noto Sans Javanese"); + break; + case SCRIPT_KAYAH_LI: + families.push_back("Noto Sans Kayah Li"); + break; + case SCRIPT_LEPCHA: + families.push_back("Noto Sans Lepcha"); + break; + case SCRIPT_LINEAR_A: + families.push_back("Noto Sans Linear A"); + break; + case SCRIPT_MANDAIC: + families.push_back("Noto Sans Mandaic"); + break; + case SCRIPT_OLD_TURKIC: + families.push_back("Noto Sans Old Turkic"); + break; + case SCRIPT_OLD_PERMIC: + families.push_back("Noto Sans Old Permic"); + break; + case SCRIPT_PHAGS_PA: + families.push_back("Noto Sans PhagsPa"); + break; + case SCRIPT_PHOENICIAN: + families.push_back("Noto Sans Phoenician"); + break; + case SCRIPT_MIAO: + families.push_back("Noto Sans Miao"); + break; + case SCRIPT_VAI: + families.push_back("Noto Sans Vai"); + break; + case SCRIPT_CUNEIFORM: + families.push_back("Noto Sans Cuneiform"); + break; + case SCRIPT_CARIAN: + families.push_back("Noto Sans Carian"); + break; + case SCRIPT_TAI_THAM: + families.push_back("Noto Sans Tai Tham"); + break; + case SCRIPT_LYCIAN: + families.push_back("Noto Sans Lycian"); + break; + case SCRIPT_LYDIAN: + families.push_back("Noto Sans Lydian"); + break; + case SCRIPT_OL_CHIKI: + families.push_back("Noto Sans Ol Chiki"); + break; + case SCRIPT_REJANG: + families.push_back("Noto Sans Rejang"); + break; + case SCRIPT_SAURASHTRA: + families.push_back("Noto Sans Saurashtra"); + break; + case SCRIPT_SUNDANESE: + families.push_back("Noto Sans Sundanese"); + break; + case SCRIPT_MEETEI_MAYEK: + families.push_back("Noto Sans Meetei Mayek"); + break; + case SCRIPT_IMPERIAL_ARAMAIC: + families.push_back("Noto Sans Imperial Aramaic"); + break; + case SCRIPT_AVESTAN: + families.push_back("Noto Sans Avestan"); + break; + case SCRIPT_CHAKMA: + families.push_back("Noto Sans Chakma"); + break; + case SCRIPT_KAITHI: + families.push_back("Noto Sans Kaithi"); + break; + case SCRIPT_MANICHAEAN: + families.push_back("Noto Sans Manichaean"); + break; + case SCRIPT_INSCRIPTIONAL_PAHLAVI: + families.push_back("Noto Sans Inscriptional Pahlavi"); + break; + case SCRIPT_PSALTER_PAHLAVI: + families.push_back("Noto Sans Psalter Pahlavi"); + break; + case SCRIPT_INSCRIPTIONAL_PARTHIAN: + families.push_back("Noto Sans Inscriptional Parthian"); + break; + case SCRIPT_SAMARITAN: + families.push_back("Noto Sans Samaritan"); + break; + case SCRIPT_TAI_VIET: + families.push_back("Noto Sans Tai Viet"); + break; + case SCRIPT_BAMUM: + families.push_back("Noto Sans Bamum"); + break; + case SCRIPT_LISU: + families.push_back("Noto Sans Lisu"); + break; + case SCRIPT_OLD_SOUTH_ARABIAN: + families.push_back("Noto Sans Old South Arabian"); + break; + case SCRIPT_BASSA_VAH: + families.push_back("Noto Sans Bassa Vah"); + break; + case SCRIPT_DUPLOYAN: + families.push_back("Noto Sans Duployan"); + break; + case SCRIPT_ELBASAN: + families.push_back("Noto Sans Elbasan"); + break; + case SCRIPT_GRANTHA: + families.push_back("Noto Sans Grantha"); + break; + case SCRIPT_MENDE_KIKAKUI: + families.push_back("Noto Sans Mende Kikakui"); + break; + case SCRIPT_MEROITIC_CURSIVE: + case SCRIPT_MEROITIC_HIEROGLYPHS: + families.push_back("Noto Sans Meroitic"); + break; + case SCRIPT_OLD_NORTH_ARABIAN: + families.push_back("Noto Sans Old North Arabian"); + break; + case SCRIPT_NABATAEAN: + families.push_back("Noto Sans Nabataean"); + break; + case SCRIPT_PALMYRENE: + families.push_back("Noto Sans Palmyrene"); + break; + case SCRIPT_KHUDAWADI: + families.push_back("Noto Sans Khudawadi"); + break; + case SCRIPT_WARANG_CITI: + families.push_back("Noto Sans Warang Citi"); + break; + case SCRIPT_MRO: + families.push_back("Noto Sans Mro"); + break; + case SCRIPT_SHARADA: + families.push_back("Noto Sans Sharada"); + break; + case SCRIPT_SORA_SOMPENG: + families.push_back("Noto Sans Sora Sompeng"); + break; + case SCRIPT_TAKRI: + families.push_back("Noto Sans Takri"); + break; + case SCRIPT_KHOJKI: + families.push_back("Noto Sans Khojki"); + break; + case SCRIPT_TIRHUTA: + families.push_back("Noto Sans Tirhuta"); + break; + case SCRIPT_CAUCASIAN_ALBANIAN: + families.push_back("Noto Sans Caucasian Albanian"); + break; + case SCRIPT_MAHAJANI: + families.push_back("Noto Sans Mahajani"); + break; + case SCRIPT_AHOM: + families.push_back("Noto Serif Ahom"); + break; + case SCRIPT_HATRAN: + families.push_back("Noto Sans Hatran"); + break; + case SCRIPT_MODI: + families.push_back("Noto Sans Modi"); + break; + case SCRIPT_MULTANI: + families.push_back("Noto Sans Multani"); + break; + case SCRIPT_PAU_CIN_HAU: + families.push_back("Noto Sans Pau Cin Hau"); + break; + case SCRIPT_SIDDHAM: + families.push_back("Noto Sans Siddham"); + break; + case SCRIPT_ADLAM: + families.push_back("Noto Sans Adlam"); + break; + case SCRIPT_BHAIKSUKI: + families.push_back("Noto Sans Bhaiksuki"); + break; + case SCRIPT_MARCHEN: + families.push_back("Noto Sans Marchen"); + break; + case SCRIPT_NEWA: + families.push_back("Noto Sans Newa"); + break; + case SCRIPT_OSAGE: + families.push_back("Noto Sans Osage"); + break; + case SCRIPT_HANIFI_ROHINGYA: + families.push_back("Noto Sans Hanifi Rohingya"); + break; + case SCRIPT_WANCHO: + families.push_back("Noto Sans Wancho"); + break; + case SCRIPT_ANATOLIAN_HIEROGLYPHS: + families.push_back("Noto Sans Anatolian Hieroglyphs"); + break; + case SCRIPT_NUSHU: + families.push_back("Noto Sans Nushu"); + break; + case SCRIPT_TANGUT: + families.push_back("Noto Serif Tangut"); + break; + case SCRIPT_MASARAM_GONDI: + families.push_back("Noto Sans Masaram Gondi"); + break; + case SCRIPT_GUNJALA_GONDI: + families.push_back("Noto Sans Gunjala Gondi"); + break; + case SCRIPT_SOYOMBO: + families.push_back("Noto Sans Soyombo"); + break; + case SCRIPT_ZANABAZAR_SQUARE: + families.push_back("Noto Sans Zanabazar Square"); + break; + case SCRIPT_DOGRA: + families.push_back("Noto Sans Dogra"); + break; + case SCRIPT_MAKASAR: + families.push_back("Noto Sans Makasar"); + break; + case SCRIPT_MEDEFAIDRIN: + families.push_back("Noto Sans Medefaidrin"); + break; + case SCRIPT_SOGDIAN: + families.push_back("Noto Sans Sogdian"); + break; + case SCRIPT_OLD_SOGDIAN: + families.push_back("Noto Sans Old Sogdian"); + break; + case SCRIPT_ELYMAIC: + families.push_back("Noto Sans Elymaic"); + break; + case SCRIPT_NYIAKENG_PUACHUE_HMONG: + families.push_back("Noto Sans Nyiakeng Puachue Hmong"); + break; + case SCRIPT_NANDINAGARI: + families.push_back("Noto Sans Nandinagari"); + break; + case SCRIPT_CHORASMIAN: + families.push_back("Noto Sans Chorasmian"); + break; + case SCRIPT_DIVES_AKURU: + families.push_back("Noto Sans Dives Akuru"); + break; + case SCRIPT_KHITAN_SMALL_SCRIPT: + families.push_back("Noto Sans Khitan Small Script"); + break; + case SCRIPT_YEZIDI: + families.push_back("Noto Sans Yezidi"); + break; + case SCRIPT_OLD_UYGHUR: + families.push_back("Noto Sans Old Uyghur"); + break; + case SCRIPT_TANGSA: + families.push_back("Noto Sans Tangsa"); + break; + case SCRIPT_TOTO: + families.push_back("Noto Sans Toto"); + break; + case SCRIPT_VITHKUQI: + families.push_back("Noto Sans Vithkuqi"); + break; + case SCRIPT_KAWI: + families.push_back("Noto Sans Kawi"); + break; + case SCRIPT_NAG_MUNDARI: + families.push_back("Noto Sans Nag Mundari"); + break; + case SCRIPT_SIGNWRITING: + families.push_back("Noto Sans SignWriting"); + break; + case SCRIPT_CYPRO_MINOAN: + families.push_back("Noto Sans Cypro Minoan"); + break; + + case SCRIPT_NONE: + case SCRIPT_GARAY: + case SCRIPT_GURUNG_KHEMA: + case SCRIPT_KIRAT_RAI: + case SCRIPT_OL_ONAL: + case SCRIPT_SUNUWAR: + case SCRIPT_TODHRI: + case SCRIPT_TULU_TIGALARI: + break; + } + + if (lang) { + const std::vector& fams = langFamilies(lang); + families.insert(families.end(), fams.begin(), fams.end()); + } + + families.insert(families.end(), emoji_fonts.begin(), emoji_fonts.end()); + families.insert(families.end(), symbol_fonts.begin(), symbol_fonts.end()); +} + +std::optional*> +FontManagerLinux::getGenericList(const std::string& generic) { + auto it = generic_fonts.find(generic); + if (it != generic_fonts.end() && !it->second.empty()) return &it->second; + return std::nullopt; +} diff --git a/src/FontManagerLinux.h b/src/FontManagerLinux.h new file mode 100644 index 000000000..8ea24e0a4 --- /dev/null +++ b/src/FontManagerLinux.h @@ -0,0 +1,22 @@ +#pragma once + +#include "FontManager.h" +#include +#include +#include +#include + +class FontManagerLinux : public FontManager { + public: + void readSystemFonts() override; + void populateFallbackFonts(std::vector& families, script_t script) override; + std::optional*> getGenericList(const std::string& generic) override; + + private: + std::vector emoji_fonts; + std::vector symbol_fonts; + + std::unordered_map> generic_fonts; + std::unordered_map> lang_cache; + const std::vector& langFamilies(const std::string& lang); +}; diff --git a/src/FontManagerMacos.cc b/src/FontManagerMacos.cc new file mode 100644 index 000000000..7c2fbc9e2 --- /dev/null +++ b/src/FontManagerMacos.cc @@ -0,0 +1,673 @@ +// Copyright (c) 2025 Caleb Hearon +// +// References: +// - https://github.com/foliojs/font-manager +// - https://searchfox.org/firefox-main/rev/30ea9a2fd7271e9c731df414bd80e46edc3190eb/gfx/thebes/CoreTextFontList.cpp + +#include +#include +#include +#include +#include +#include + +#include "FontManagerMacos.h" +#include "Font.h" +#include "unicode.h" + +// Forward declarations for Objective-C types we need +typedef void NSString; +typedef void NSURL; +typedef void NSArray; + +const uint16_t MAX_STYLE_LENGTH = 128; // like "Bold Italic", so should never be big + +inline double round(double aNum) { + return aNum >= 0.0 ? std::floor(aNum + 0.5) : std::ceil(aNum - 0.5); +} + +// https://searchfox.org/firefox-main/rev/30ea9a2fd7271e9c731df414bd80e46edc3190eb/gfx/thebes/CoreTextFontList.cpp#770 +static uint32_t +convertWeight(float aCTWeight) { + constexpr std::pair kCoreTextToCSSWeights[] = { + {-1.0, 1}, + {-0.8, 100}, + {-0.6, 200}, + {-0.4, 300}, + {0.0, 400}, // standard 'regular' weight + {0.23, 500}, + {0.3, 600}, + {0.4, 700}, // standard 'bold' weight + {0.56, 800}, + {0.62, 900}, // Core Text seems to return 0.62 for faces with both + // usWeightClass=800 and 900 in their OS/2 tables! + // We use 900 as there are also fonts that return 0.56, + // so we want an intermediate value for that. + {1.0, 1000} + }; + const auto* begin = &kCoreTextToCSSWeights[0]; + const auto* end = begin + std::size(kCoreTextToCSSWeights); + auto m = std::upper_bound( + begin, + end, + aCTWeight, + [](CGFloat aValue, const std::pair& aMapping) { + return aValue <= aMapping.first; + } + ); + + if (m == end) return 1000; + if (m->first == aCTWeight || m == begin) return m->second; + // Interpolate between the preceding and found entries: + const auto* prev = m - 1; + const auto t = (aCTWeight - prev->first) / (m->first - prev->first); + return round(prev->second * (1.0 - t) + m->second * t); +} + +void +create_font_descriptor( + std::vector& results, + CTFontDescriptorRef descriptor +) { + FontDescriptor desc; + + NSURL *nsUrl = (NSURL *) CTFontDescriptorCopyAttribute(descriptor, kCTFontURLAttribute); + CFStringRef nsPath = CFURLCopyFileSystemPath((CFURLRef)nsUrl, kCFURLPOSIXPathStyle); + NSString *nsFamily = (NSString *) CTFontDescriptorCopyAttribute(descriptor, kCTFontFamilyNameAttribute); + + // Should never happen, but shouldn't crash either + if (!nsUrl || !nsFamily) return; + + NSString *nsPostscript = (NSString *) CTFontDescriptorCopyAttribute(descriptor, kCTFontNameAttribute); + NSString *nsStyle = (NSString *) CTFontDescriptorCopyAttribute(descriptor, kCTFontStyleNameAttribute); + CFDictionaryRef nsTraits = (CFDictionaryRef) CTFontDescriptorCopyAttribute(descriptor, kCTFontTraitsAttribute); + + // weight + if (nsTraits) { + CFNumberRef weightVal = (CFNumberRef) CFDictionaryGetValue(nsTraits, kCTFontWeightTrait); + float weightValue; + CFNumberGetValue(weightVal, kCFNumberFloatType, &weightValue); + desc.weight = (uint32_t) convertWeight(weightValue); + } + + // width + if (nsTraits) { + CFNumberRef widthVal = (CFNumberRef) CFDictionaryGetValue(nsTraits, kCTFontWidthTrait); + float widthValue; + CFNumberGetValue(widthVal, kCFNumberFloatType, &widthValue); + // https://searchfox.org/firefox-main/rev/cced10961b53e0d29e22e635404fec37728b2644/gfx/thebes/CoreTextFontList.cpp#819 + if (widthValue >= 0.0) { + desc.stretch = 100 + widthValue * 100; + } else { + desc.stretch = 100 + widthValue * 50; + } + } + + // file path + CFIndex pathLength = CFStringGetLength(nsPath) * 2 + 1; + desc.url = std::make_unique(pathLength); + CFStringGetCString(nsPath, desc.url.get(), pathLength, kCFStringEncodingUTF8); + + // family name + CFIndex familyLength = CFStringGetLength((CFStringRef)nsFamily) * 2 + 1; + std::unique_ptr family = std::make_unique(familyLength); + CFStringGetCString((CFStringRef)nsFamily, family.get(), familyLength, kCFStringEncodingUTF8); + desc.family = std::move(family); + + // postscript name + if (nsPostscript) { + CFIndex postscriptLength = CFStringGetLength((CFStringRef)nsPostscript) * 2 + 1; + std::unique_ptr postscript = std::make_unique(postscriptLength); + CFStringGetCString((CFStringRef)nsPostscript, postscript.get(), postscriptLength, kCFStringEncodingUTF8); + desc.postscript = std::move(postscript); + } + + // style + unsigned int symbolicTraits = 0; + if (nsTraits) { + CFNumberRef symbolicTraitsVal = (CFNumberRef)CFDictionaryGetValue(nsTraits, kCTFontSymbolicTrait); + CFNumberGetValue(symbolicTraitsVal, kCFNumberIntType, &symbolicTraits); + desc.style = FontStyle::Normal; + } + if (symbolicTraits & kCTFontItalicTrait) { + desc.style = FontStyle::Italic; + } else if (nsStyle) { + char styleBuffer[MAX_STYLE_LENGTH]; + CFStringGetCString((CFStringRef)nsStyle, styleBuffer, MAX_STYLE_LENGTH, kCFStringEncodingUTF8); + if (strstr(styleBuffer, "Oblique") != NULL) desc.style = FontStyle::Oblique; + } + + results.push_back(std::move(desc)); + + CFRelease(nsUrl); + CFRelease(nsPath); + CFRelease(nsFamily); + if (nsPostscript) CFRelease(nsPostscript); + if (nsStyle) CFRelease(nsStyle); + if (nsTraits) CFRelease(nsTraits); +} + +void +FontManagerMacos::readSystemFonts() { + static CTFontCollectionRef collection = NULL; + if (collection == NULL) collection = CTFontCollectionCreateFromAvailableFonts(NULL); + + NSArray *matches = (NSArray *) CTFontCollectionCreateMatchingFontDescriptors(collection); + CFIndex count = CFArrayGetCount((CFArrayRef) matches); + + system_fonts.reserve(count); + + for (CFIndex i = 0; i < count; i++) { + CTFontDescriptorRef match = (CTFontDescriptorRef)CFArrayGetValueAtIndex((CFArrayRef)matches, i); + create_font_descriptor(system_fonts, match); + } + + CFRelease(matches); +} + +void FontManagerMacos::populateFallbackFonts( + std::vector& families, + script_t script +) { + // Note: this was copied from Firefox, including comments, and tweaked to fit + switch (script) { + case SCRIPT_COMMON: + case SCRIPT_INHERITED: + // In most cases, COMMON and INHERITED characters will be merged into + // their context, but if they occur without any specific script context + // we'll just try common default fonts here. + case SCRIPT_LATIN: + case SCRIPT_CYRILLIC: + case SCRIPT_GREEK: + families.push_back("Lucida Grande"); + break; + + // CJK-related script codes are a bit troublesome because of unification; + // we'll probably just get HAN much of the time, so the choice of which + // language font to try for fallback is rather arbitrary. Usually, though, + // we hope that font prefs will have handled this earlier. + case SCRIPT_BOPOMOFO: + case SCRIPT_HAN: + families.push_back("Songti SC"); + families.push_back("SimSun-ExtB"); + break; + + case SCRIPT_HIRAGANA: + case SCRIPT_KATAKANA: + families.push_back("Hiragino Sans"); + families.push_back("Hiragino Kaku Gothic ProN"); + break; + + case SCRIPT_HANGUL: + families.push_back("Nanum Gothic"); + families.push_back("Apple SD Gothic Neo"); + break; + + // For most other scripts, macOS comes with a default font we can use. + case SCRIPT_ARABIC: + families.push_back("Geeza Pro"); + break; + case SCRIPT_ARMENIAN: + families.push_back("Mshtakan"); + break; + case SCRIPT_BENGALI: + families.push_back("Bangla Sangam MN"); + break; + case SCRIPT_CHEROKEE: + families.push_back("Plantagenet Cherokee"); + break; + case SCRIPT_COPTIC: + families.push_back("Noto Sans Coptic"); + break; + case SCRIPT_DESERET: + families.push_back("Baskerville"); + break; + case SCRIPT_DEVANAGARI: + families.push_back("Devanagari Sangam MN"); + break; + case SCRIPT_ETHIOPIC: + families.push_back("Kefa"); + break; + case SCRIPT_GEORGIAN: + families.push_back("Helvetica"); + break; + case SCRIPT_GOTHIC: + families.push_back("Noto Sans Gothic"); + break; + case SCRIPT_GUJARATI: + families.push_back("Gujarati Sangam MN"); + break; + case SCRIPT_GURMUKHI: + families.push_back("Gurmukhi MN"); + break; + case SCRIPT_HEBREW: + families.push_back("Lucida Grande"); + break; + case SCRIPT_KANNADA: + families.push_back("Kannada MN"); + break; + case SCRIPT_KHMER: + families.push_back("Khmer MN"); + break; + case SCRIPT_LAO: + families.push_back("Lao MN"); + break; + case SCRIPT_MALAYALAM: + families.push_back("Malayalam Sangam MN"); + break; + case SCRIPT_MONGOLIAN: + families.push_back("Noto Sans Mongolian"); + break; + case SCRIPT_MYANMAR: + families.push_back("Myanmar MN"); + break; + case SCRIPT_OGHAM: + families.push_back("Noto Sans Ogham"); + break; + case SCRIPT_OLD_ITALIC: + families.push_back("Noto Sans Old Italic"); + break; + case SCRIPT_ORIYA: + families.push_back("Oriya Sangam MN"); + break; + case SCRIPT_RUNIC: + families.push_back("Noto Sans Runic"); + break; + case SCRIPT_SINHALA: + families.push_back("Sinhala Sangam MN"); + break; + case SCRIPT_SYRIAC: + families.push_back("Noto Sans Syriac"); + break; + case SCRIPT_TAMIL: + families.push_back("Tamil MN"); + break; + case SCRIPT_TELUGU: + families.push_back("Telugu MN"); + break; + case SCRIPT_THAANA: + families.push_back("Noto Sans Thaana"); + break; + case SCRIPT_THAI: + families.push_back("Thonburi"); + break; + case SCRIPT_TIBETAN: + families.push_back("Kailasa"); + break; + case SCRIPT_CANADIAN_ABORIGINAL: + families.push_back("Euphemia UCAS"); + break; + case SCRIPT_YI: + families.push_back("Noto Sans Yi"); + families.push_back("STHeiti"); + break; + case SCRIPT_TAGALOG: + families.push_back("Noto Sans Tagalog"); + break; + case SCRIPT_HANUNOO: + families.push_back("Noto Sans Hanunoo"); + break; + case SCRIPT_BUHID: + families.push_back("Noto Sans Buhid"); + break; + case SCRIPT_TAGBANWA: + families.push_back("Noto Sans Tagbanwa"); + break; + case SCRIPT_BRAILLE: + families.push_back("Apple Braille"); + break; + case SCRIPT_CYPRIOT: + families.push_back("Noto Sans Cypriot"); + break; + case SCRIPT_LIMBU: + families.push_back("Noto Sans Limbu"); + break; + case SCRIPT_LINEAR_B: + families.push_back("Noto Sans Linear B"); + break; + case SCRIPT_OSMANYA: + families.push_back("Noto Sans Osmanya"); + break; + case SCRIPT_SHAVIAN: + families.push_back("Noto Sans Shavian"); + break; + case SCRIPT_TAI_LE: + families.push_back("Noto Sans Tai Le"); + break; + case SCRIPT_UGARITIC: + families.push_back("Noto Sans Ugaritic"); + break; + case SCRIPT_BUGINESE: + families.push_back("Noto Sans Buginese"); + break; + case SCRIPT_GLAGOLITIC: + families.push_back("Noto Sans Glagolitic"); + break; + case SCRIPT_KHAROSHTHI: + families.push_back("Noto Sans Kharoshthi"); + break; + case SCRIPT_SYLOTI_NAGRI: + families.push_back("Noto Sans Syloti Nagri"); + break; + case SCRIPT_NEW_TAI_LUE: + families.push_back("Noto Sans New Tai Lue"); + break; + case SCRIPT_TIFINAGH: + families.push_back("Noto Sans Tifinagh"); + break; + case SCRIPT_OLD_PERSIAN: + families.push_back("Noto Sans Old Persian"); + break; + case SCRIPT_BALINESE: + families.push_back("Noto Sans Balinese"); + break; + case SCRIPT_BATAK: + families.push_back("Noto Sans Batak"); + break; + case SCRIPT_BRAHMI: + families.push_back("Noto Sans Brahmi"); + break; + case SCRIPT_CHAM: + families.push_back("Noto Sans Cham"); + break; + case SCRIPT_EGYPTIAN_HIEROGLYPHS: + families.push_back("Noto Sans Egyptian Hieroglyphs"); + break; + case SCRIPT_PAHAWH_HMONG: + families.push_back("Noto Sans Pahawh Hmong"); + break; + case SCRIPT_OLD_HUNGARIAN: + families.push_back("Noto Sans Old Hungarian"); + break; + case SCRIPT_JAVANESE: + families.push_back("Noto Sans Javanese"); + break; + case SCRIPT_KAYAH_LI: + families.push_back("Noto Sans Kayah Li"); + break; + case SCRIPT_LEPCHA: + families.push_back("Noto Sans Lepcha"); + break; + case SCRIPT_LINEAR_A: + families.push_back("Noto Sans Linear A"); + break; + case SCRIPT_MANDAIC: + families.push_back("Noto Sans Mandaic"); + break; + case SCRIPT_NKO: + families.push_back("Noto Sans NKo"); + break; + case SCRIPT_OLD_TURKIC: + families.push_back("Noto Sans Old Turkic"); + break; + case SCRIPT_OLD_PERMIC: + families.push_back("Noto Sans Old Permic"); + break; + case SCRIPT_PHAGS_PA: + families.push_back("Noto Sans PhagsPa"); + break; + case SCRIPT_PHOENICIAN: + families.push_back("Noto Sans Phoenician"); + break; + case SCRIPT_MIAO: + families.push_back("Noto Sans Miao"); + break; + case SCRIPT_VAI: + families.push_back("Noto Sans Vai"); + break; + case SCRIPT_CUNEIFORM: + families.push_back("Noto Sans Cuneiform"); + break; + case SCRIPT_CARIAN: + families.push_back("Noto Sans Carian"); + break; + case SCRIPT_TAI_THAM: + families.push_back("Noto Sans Tai Tham"); + break; + case SCRIPT_LYCIAN: + families.push_back("Noto Sans Lycian"); + break; + case SCRIPT_LYDIAN: + families.push_back("Noto Sans Lydian"); + break; + case SCRIPT_OL_CHIKI: + families.push_back("Noto Sans Ol Chiki"); + break; + case SCRIPT_REJANG: + families.push_back("Noto Sans Rejang"); + break; + case SCRIPT_SAURASHTRA: + families.push_back("Noto Sans Saurashtra"); + break; + case SCRIPT_SUNDANESE: + families.push_back("Noto Sans Sundanese"); + break; + case SCRIPT_MEETEI_MAYEK: + families.push_back("Noto Sans Meetei Mayek"); + break; + case SCRIPT_IMPERIAL_ARAMAIC: + families.push_back("Noto Sans Imperial Aramaic"); + break; + case SCRIPT_AVESTAN: + families.push_back("Noto Sans Avestan"); + break; + case SCRIPT_CHAKMA: + families.push_back("Noto Sans Chakma"); + break; + case SCRIPT_KAITHI: + families.push_back("Noto Sans Kaithi"); + break; + case SCRIPT_MANICHAEAN: + families.push_back("Noto Sans Manichaean"); + break; + case SCRIPT_INSCRIPTIONAL_PAHLAVI: + families.push_back("Noto Sans Inscriptional Pahlavi"); + break; + case SCRIPT_PSALTER_PAHLAVI: + families.push_back("Noto Sans Psalter Pahlavi"); + break; + case SCRIPT_INSCRIPTIONAL_PARTHIAN: + families.push_back("Noto Sans Inscriptional Parthian"); + break; + case SCRIPT_SAMARITAN: + families.push_back("Noto Sans Samaritan"); + break; + case SCRIPT_TAI_VIET: + families.push_back("Noto Sans Tai Viet"); + break; + case SCRIPT_BAMUM: + families.push_back("Noto Sans Bamum"); + break; + case SCRIPT_LISU: + families.push_back("Noto Sans Lisu"); + break; + case SCRIPT_OLD_SOUTH_ARABIAN: + families.push_back("Noto Sans Old South Arabian"); + break; + case SCRIPT_BASSA_VAH: + families.push_back("Noto Sans Bassa Vah"); + break; + case SCRIPT_DUPLOYAN: + families.push_back("Noto Sans Duployan"); + break; + case SCRIPT_ELBASAN: + families.push_back("Noto Sans Elbasan"); + break; + case SCRIPT_GRANTHA: + families.push_back("Noto Sans Grantha"); + break; + case SCRIPT_MENDE_KIKAKUI: + families.push_back("Noto Sans Mende Kikakui"); + break; + case SCRIPT_MEROITIC_CURSIVE: + case SCRIPT_MEROITIC_HIEROGLYPHS: + families.push_back("Noto Sans Meroitic"); + break; + case SCRIPT_OLD_NORTH_ARABIAN: + families.push_back("Noto Sans Old North Arabian"); + break; + case SCRIPT_NABATAEAN: + families.push_back("Noto Sans Nabataean"); + break; + case SCRIPT_PALMYRENE: + families.push_back("Noto Sans Palmyrene"); + break; + case SCRIPT_KHUDAWADI: + families.push_back("Noto Sans Khudawadi"); + break; + case SCRIPT_WARANG_CITI: + families.push_back("Noto Sans Warang Citi"); + break; + case SCRIPT_MRO: + families.push_back("Noto Sans Mro"); + break; + case SCRIPT_SHARADA: + families.push_back("Noto Sans Sharada"); + break; + case SCRIPT_SORA_SOMPENG: + families.push_back("Noto Sans Sora Sompeng"); + break; + case SCRIPT_TAKRI: + families.push_back("Noto Sans Takri"); + break; + case SCRIPT_KHOJKI: + families.push_back("Noto Sans Khojki"); + break; + case SCRIPT_TIRHUTA: + families.push_back("Noto Sans Tirhuta"); + break; + case SCRIPT_CAUCASIAN_ALBANIAN: + families.push_back("Noto Sans Caucasian Albanian"); + break; + case SCRIPT_MAHAJANI: + families.push_back("Noto Sans Mahajani"); + break; + case SCRIPT_AHOM: + families.push_back("Noto Serif Ahom"); + break; + case SCRIPT_HATRAN: + families.push_back("Noto Sans Hatran"); + break; + case SCRIPT_MODI: + families.push_back("Noto Sans Modi"); + break; + case SCRIPT_MULTANI: + families.push_back("Noto Sans Multani"); + break; + case SCRIPT_PAU_CIN_HAU: + families.push_back("Noto Sans Pau Cin Hau"); + break; + case SCRIPT_SIDDHAM: + families.push_back("Noto Sans Siddham"); + break; + case SCRIPT_ADLAM: + families.push_back("Noto Sans Adlam"); + break; + case SCRIPT_BHAIKSUKI: + families.push_back("Noto Sans Bhaiksuki"); + break; + case SCRIPT_MARCHEN: + families.push_back("Noto Sans Marchen"); + break; + case SCRIPT_NEWA: + families.push_back("Noto Sans Newa"); + break; + case SCRIPT_OSAGE: + families.push_back("Noto Sans Osage"); + break; + case SCRIPT_HANIFI_ROHINGYA: + families.push_back("Noto Sans Hanifi Rohingya"); + break; + case SCRIPT_WANCHO: + families.push_back("Noto Sans Wancho"); + break; + + // Script codes for which no commonly-installed font is currently known. + // Probably future macOS versions will add Noto fonts for many of these, + // so we should watch for updates. + case SCRIPT_NONE: + case SCRIPT_NUSHU: + case SCRIPT_TANGUT: + case SCRIPT_ANATOLIAN_HIEROGLYPHS: + case SCRIPT_MASARAM_GONDI: + case SCRIPT_SOYOMBO: + case SCRIPT_ZANABAZAR_SQUARE: + case SCRIPT_DOGRA: + case SCRIPT_GUNJALA_GONDI: + case SCRIPT_MAKASAR: + case SCRIPT_MEDEFAIDRIN: + case SCRIPT_SOGDIAN: + case SCRIPT_OLD_SOGDIAN: + case SCRIPT_ELYMAIC: + case SCRIPT_NYIAKENG_PUACHUE_HMONG: + case SCRIPT_NANDINAGARI: + case SCRIPT_CHORASMIAN: + case SCRIPT_DIVES_AKURU: + case SCRIPT_KHITAN_SMALL_SCRIPT: + case SCRIPT_YEZIDI: + case SCRIPT_CYPRO_MINOAN: + case SCRIPT_OLD_UYGHUR: + case SCRIPT_TANGSA: + case SCRIPT_TOTO: + case SCRIPT_VITHKUQI: + case SCRIPT_KAWI: + case SCRIPT_NAG_MUNDARI: + case SCRIPT_GARAY: + case SCRIPT_GURUNG_KHEMA: + case SCRIPT_KIRAT_RAI: + case SCRIPT_OL_ONAL: + case SCRIPT_SIGNWRITING: + case SCRIPT_SUNUWAR: + case SCRIPT_TODHRI: + case SCRIPT_TULU_TIGALARI: + break; + } + + // TODO: Color Emoji should depend on if the default presentation for the + // codepoint is color or if a VS16 selector is present. + + families.push_back("Apple Color Emoji"); + + // TODO: Firefox makes the middle these 6 conditional on the codepoint. + // When users try to paint text that isn't in the first few families, this + // is going to be slower than it needs to be. Original Firefox comment next... + // + // Symbols/dingbats are generally Script=COMMON but may be resolved to any + // surrounding script run. So we'll always append a couple of likely fonts + // for such characters. + families.push_back("Zapf Dingbats"); + families.push_back("Geneva"); + families.push_back("STIXGeneral"); + families.push_back("Apple Symbols"); + // Japanese fonts also cover a lot of miscellaneous symbols + families.push_back("Hiragino Sans"); + families.push_back("Hiragino Kaku Gothic ProN"); + + // Arial Unicode MS has lots of glyphs for obscure characters; try it as a + // last resort. + families.push_back("Arial Unicode MS"); +} + +// See the preferences font.name-list.*.x-western in Firefox +const std::vector serif_fonts = {"Times", "Times New Roman"}; +const std::vector sans_serif_fonts = {"Helvetica", "Arial"}; +const std::vector monospace_fonts = {"Menlo"}; +const std::vector cursive_fonts = {"Apple Chancery"}; +const std::vector fantasy_fonts = {"Papyrus"}; + +std::optional*> +FontManagerMacos::getGenericList(const std::string& generic) { + if (generic == "serif") { + return &serif_fonts; + } else if (generic == "sans-serif") { + return &sans_serif_fonts; + } else if (generic == "monospace") { + return &monospace_fonts; + } else if (generic == "cursive") { + return &cursive_fonts; + } else if (generic == "fantasy") { + return &fantasy_fonts; + } else { + return std::nullopt; + } +} diff --git a/src/FontManagerMacos.h b/src/FontManagerMacos.h new file mode 100644 index 000000000..9f8578dcb --- /dev/null +++ b/src/FontManagerMacos.h @@ -0,0 +1,11 @@ +#pragma once + +#include "FontManager.h" +#include + +class FontManagerMacos : public FontManager { + public: + void readSystemFonts() override; + void populateFallbackFonts(std::vector& families, script_t script) override; + std::optional*> getGenericList(const std::string& generic) override; +}; diff --git a/src/FontManagerWindows.cc b/src/FontManagerWindows.cc new file mode 100644 index 000000000..5f6f40edc --- /dev/null +++ b/src/FontManagerWindows.cc @@ -0,0 +1,648 @@ +// Copyright (c) 2025 Caleb Hearon +// +// References: +// - https://searchfox.org/firefox-main/rev/e28b34ab33dbf49364999070168cbb7e11e8e5bd/gfx/thebes/gfxDWriteFontList.cpp +// - https://searchfox.org/firefox-main/rev/e28b34ab33dbf49364999070168cbb7e11e8e5bd/gfx/thebes/gfxWindowsPlatform.cpp + +#include +#include + +#include +#include +#include +#include + +#include "FontManagerWindows.h" +#include "Font.h" +#include "unicode.h" + +// RAII helper to release a COM interface on scope exit. +template +struct ComPtr { + T* ptr = nullptr; + ComPtr() = default; + ComPtr(const ComPtr&) = delete; + ComPtr& operator=(const ComPtr&) = delete; + ~ComPtr() { if (ptr) ptr->Release(); } + T** operator&() { return &ptr; } + T* operator->() const { return ptr; } + explicit operator bool() const { return ptr != nullptr; } +}; + +static std::unique_ptr +utf16ToUtf8(const wchar_t* wstr) { + int len = WideCharToMultiByte(CP_UTF8, 0, wstr, -1, nullptr, 0, nullptr, nullptr); + if (len <= 0) return nullptr; + + auto buffer = std::make_unique(len); + WideCharToMultiByte(CP_UTF8, 0, wstr, -1, buffer.get(), len, nullptr, nullptr); + return buffer; +} + +static std::unique_ptr +getEnUsString(IDWriteLocalizedStrings* strings) { + UINT32 index = 0; + BOOL exists = FALSE; + if (FAILED(strings->FindLocaleName(L"en-US", &index, &exists)) || !exists) { + return nullptr; + } + + UINT32 length = 0; + if (FAILED(strings->GetStringLength(index, &length))) return nullptr; + + auto wbuffer = std::make_unique(length + 1); + if (FAILED(strings->GetString(index, wbuffer.get(), length + 1))) return nullptr; + + return utf16ToUtf8(wbuffer.get()); +} + +static std::unique_ptr +getFontFilePath(IDWriteFontFile* file) { + if (!file) return nullptr; + + const void* referenceKey = nullptr; + UINT32 referenceKeySize = 0; + HRESULT res1 = file->GetReferenceKey(&referenceKey, &referenceKeySize); + if (FAILED(res1)) return nullptr; + + ComPtr loader; + HRESULT res2 = file->GetLoader(&loader); + if (FAILED(res2)) return nullptr; + + ComPtr localLoader; + HRESULT res3 = loader->QueryInterface( + __uuidof(IDWriteLocalFontFileLoader), + (void**)&localLoader + ); + if (FAILED(res3)) return nullptr; + + UINT32 pathLength = 0; + HRESULT res4 = localLoader->GetFilePathLengthFromKey( + referenceKey, + referenceKeySize, + &pathLength + ); + if (FAILED(res4)) return nullptr; + + auto wpath = std::make_unique(pathLength + 1); + HRESULT res5 = localLoader->GetFilePathFromKey( + referenceKey, + referenceKeySize, + wpath.get(), + pathLength + 1 + ); + if (FAILED(res5)) return nullptr; + + return wpath; +} + +static uint16_t +convertStretch(DWRITE_FONT_STRETCH stretch) { + switch (stretch) { + case DWRITE_FONT_STRETCH_ULTRA_CONDENSED: return 50; + case DWRITE_FONT_STRETCH_EXTRA_CONDENSED: return 63; // 62.5 + case DWRITE_FONT_STRETCH_CONDENSED: return 75; + case DWRITE_FONT_STRETCH_SEMI_CONDENSED: return 88; // 87.5 + case DWRITE_FONT_STRETCH_SEMI_EXPANDED: return 113; // 112.5 + case DWRITE_FONT_STRETCH_EXPANDED: return 125; + case DWRITE_FONT_STRETCH_EXTRA_EXPANDED: return 150; + case DWRITE_FONT_STRETCH_ULTRA_EXPANDED: return 200; + case DWRITE_FONT_STRETCH_NORMAL: + case DWRITE_FONT_STRETCH_UNDEFINED: + default: return 100; + } +} + +static FontStyle +convertStyle(DWRITE_FONT_STYLE style) { + switch (style) { + case DWRITE_FONT_STYLE_ITALIC: return FontStyle::Italic; + case DWRITE_FONT_STYLE_OBLIQUE: return FontStyle::Oblique; + case DWRITE_FONT_STYLE_NORMAL: + default: return FontStyle::Normal; + } +} + +static void +create_font_descriptor( + std::vector& results, + IDWriteFont* font, + const char* familyName +) { + ComPtr face; + if (FAILED(font->CreateFontFace(&face))) return; + + UINT32 numberOfFiles = 1; + if (FAILED(face->GetFiles(&numberOfFiles, nullptr))) return; + if (numberOfFiles != 1) return; + + IDWriteFontFile* file; + if (FAILED(face->GetFiles(&numberOfFiles, &file))) return; + + std::unique_ptr path = getFontFilePath(file); + file->Release(); + if (!path) return; + + FontDescriptor desc; + desc.url = std::move(path); + + size_t familyLength = std::strlen(familyName) + 1; + desc.family = std::make_unique(familyLength); + std::memcpy(desc.family.get(), familyName, familyLength); + desc.weight = static_cast(font->GetWeight()); + desc.stretch = convertStretch(font->GetStretch()); + desc.style = convertStyle(font->GetStyle()); + desc.index = face->GetIndex(); + + results.push_back(std::move(desc)); +} + +void +FontManagerWindows::readSystemFonts() { + ComPtr factory; + HRESULT res = DWriteCreateFactory( + DWRITE_FACTORY_TYPE_SHARED, + __uuidof(IDWriteFactory), + (IUnknown**)&factory + ); + + if (FAILED(res)) return; + + ComPtr collection; + if (FAILED(factory->GetSystemFontCollection(&collection, FALSE))) return; + + UINT32 familyCount = collection->GetFontFamilyCount(); + + for (UINT32 i = 0; i < familyCount; i++) { + ComPtr family; + if (FAILED(collection->GetFontFamily(i, &family))) continue; + + ComPtr familyNames; + if (FAILED(family->GetFamilyNames(&familyNames))) continue; + + std::unique_ptr familyName = getEnUsString(familyNames.ptr); + if (!familyName) continue; + + UINT32 fontCount = family->GetFontCount(); + for (UINT32 j = 0; j < fontCount; j++) { + ComPtr font; + if (FAILED(family->GetFont(j, &font))) continue; + create_font_descriptor(system_fonts, font.ptr, familyName.get()); + } + } +} + +void +FontManagerWindows::populateFallbackFonts( + std::vector& families, + script_t script +) { + // Note: this was copied from Firefox, including comments, and tweaked to fit + switch (script) { + case SCRIPT_COMMON: + case SCRIPT_INHERITED: + // In most cases, COMMON and INHERITED characters will be merged into + // their context, but if they occur without context, we'll just treat + // them like Latin, etc. + case SCRIPT_LATIN: + case SCRIPT_CYRILLIC: + case SCRIPT_GREEK: + case SCRIPT_ARMENIAN: + case SCRIPT_HEBREW: + // families.push_back("Arial"); + break; + + // CJK-related script codes are a bit troublesome because of unification; + // we'll probably just get HAN much of the time, so the choice of which + // language font to try for fallback is rather arbitrary. Usually, though, + // we hope that font prefs will have handled this earlier. + case SCRIPT_BOPOMOFO: + case SCRIPT_HAN: + families.push_back("SimSun"); + // We can't see the codepoint here, so always offer the ext-B font too. + families.push_back("SimSun-ExtB"); + break; + case SCRIPT_HIRAGANA: + case SCRIPT_KATAKANA: + families.push_back("Yu Gothic"); + families.push_back("MS PGothic"); + break; + case SCRIPT_HANGUL: + families.push_back("Malgun Gothic"); + break; + + case SCRIPT_YI: + families.push_back("Microsoft Yi Baiti"); + break; + case SCRIPT_MONGOLIAN: + families.push_back("Mongolian Baiti"); + break; + case SCRIPT_TIBETAN: + families.push_back("Microsoft Himalaya"); + break; + case SCRIPT_PHAGS_PA: + families.push_back("Microsoft PhagsPa"); + break; + + case SCRIPT_ARABIC: + // Default to Arial (added unconditionally below) for Arabic script. + break; + case SCRIPT_SYRIAC: + families.push_back("Estrangelo Edessa"); + break; + case SCRIPT_THAANA: + families.push_back("MV Boli"); + break; + + case SCRIPT_BENGALI: + families.push_back("Vrinda"); + families.push_back("Nirmala UI"); + break; + case SCRIPT_DEVANAGARI: + families.push_back("Kokila"); + families.push_back("Nirmala UI"); + break; + case SCRIPT_GUJARATI: + families.push_back("Shruti"); + families.push_back("Nirmala UI"); + break; + case SCRIPT_GURMUKHI: + families.push_back("Raavi"); + families.push_back("Nirmala UI"); + break; + case SCRIPT_KANNADA: + families.push_back("Tunga"); + families.push_back("Nirmala UI"); + break; + case SCRIPT_MALAYALAM: + families.push_back("Kartika"); + families.push_back("Nirmala UI"); + break; + case SCRIPT_ORIYA: + families.push_back("Kalinga"); + families.push_back("Nirmala UI"); + break; + case SCRIPT_TAMIL: + families.push_back("Latha"); + families.push_back("Nirmala UI"); + break; + case SCRIPT_TELUGU: + families.push_back("Gautami"); + families.push_back("Nirmala UI"); + break; + case SCRIPT_SINHALA: + families.push_back("Iskoola Pota"); + families.push_back("Nirmala UI"); + break; + + case SCRIPT_CHAKMA: + case SCRIPT_MEETEI_MAYEK: + case SCRIPT_OL_CHIKI: + case SCRIPT_SORA_SOMPENG: + families.push_back("Nirmala UI"); + break; + + case SCRIPT_MYANMAR: + families.push_back("Myanmar Text"); + break; + case SCRIPT_KHMER: + families.push_back("Khmer UI"); + break; + case SCRIPT_LAO: + families.push_back("Lao UI"); + break; + case SCRIPT_THAI: + families.push_back("Tahoma"); + families.push_back("Leelawadee UI"); + break; + case SCRIPT_TAI_LE: + families.push_back("Microsoft Tai Le"); + break; + case SCRIPT_BUGINESE: + families.push_back("Leelawadee UI"); + break; + case SCRIPT_NEW_TAI_LUE: + families.push_back("Microsoft New Tai Lue"); + break; + case SCRIPT_JAVANESE: + families.push_back("Javanese Text"); + break; + + case SCRIPT_GEORGIAN: + case SCRIPT_LISU: + families.push_back("Segoe UI"); + break; + + case SCRIPT_ETHIOPIC: + families.push_back("Nyala"); + families.push_back("Ebrima"); + break; + + case SCRIPT_ADLAM: + case SCRIPT_NKO: + case SCRIPT_OSMANYA: + case SCRIPT_TIFINAGH: + case SCRIPT_VAI: + families.push_back("Ebrima"); + break; + + case SCRIPT_CANADIAN_ABORIGINAL: + families.push_back("Euphemia"); + break; + + case SCRIPT_CHEROKEE: + case SCRIPT_OSAGE: + families.push_back("Gadugi"); + break; + + case SCRIPT_BRAILLE: + case SCRIPT_DESERET: + families.push_back("Segoe UI Symbol"); + break; + + case SCRIPT_BRAHMI: + case SCRIPT_CARIAN: + case SCRIPT_CUNEIFORM: + case SCRIPT_CYPRIOT: + case SCRIPT_EGYPTIAN_HIEROGLYPHS: + case SCRIPT_GLAGOLITIC: + case SCRIPT_GOTHIC: + case SCRIPT_IMPERIAL_ARAMAIC: + case SCRIPT_INSCRIPTIONAL_PAHLAVI: + case SCRIPT_INSCRIPTIONAL_PARTHIAN: + case SCRIPT_KHAROSHTHI: + case SCRIPT_LYCIAN: + case SCRIPT_LYDIAN: + case SCRIPT_MEROITIC_CURSIVE: + case SCRIPT_OGHAM: + case SCRIPT_OLD_ITALIC: + case SCRIPT_OLD_PERSIAN: + case SCRIPT_OLD_SOUTH_ARABIAN: + case SCRIPT_OLD_TURKIC: + case SCRIPT_PHOENICIAN: + case SCRIPT_RUNIC: + case SCRIPT_SHAVIAN: + case SCRIPT_UGARITIC: + families.push_back("Segoe UI Historic"); + break; + + // For some scripts where Windows doesn't supply a font by default, + // there are Noto fonts that users might have installed: + case SCRIPT_AHOM: + families.push_back("Noto Serif Ahom"); + break; + case SCRIPT_AVESTAN: + families.push_back("Noto Sans Avestan"); + break; + case SCRIPT_BALINESE: + families.push_back("Noto Sans Balinese"); + break; + case SCRIPT_BAMUM: + families.push_back("Noto Sans Bamum"); + break; + case SCRIPT_BASSA_VAH: + families.push_back("Noto Sans Bassa Vah"); + break; + case SCRIPT_BATAK: + families.push_back("Noto Sans Batak"); + break; + case SCRIPT_BHAIKSUKI: + families.push_back("Noto Sans Bhaiksuki"); + break; + case SCRIPT_BUHID: + families.push_back("Noto Sans Buhid"); + break; + case SCRIPT_CAUCASIAN_ALBANIAN: + families.push_back("Noto Sans Caucasian Albanian"); + break; + case SCRIPT_CHAM: + families.push_back("Noto Sans Cham"); + break; + case SCRIPT_COPTIC: + families.push_back("Noto Sans Coptic"); + break; + case SCRIPT_DUPLOYAN: + families.push_back("Noto Sans Duployan"); + break; + case SCRIPT_ELBASAN: + families.push_back("Noto Sans Elbasan"); + break; + case SCRIPT_GRANTHA: + families.push_back("Noto Sans Grantha"); + break; + case SCRIPT_HANIFI_ROHINGYA: + families.push_back("Noto Sans Hanifi Rohingya"); + break; + case SCRIPT_HANUNOO: + families.push_back("Noto Sans Hanunoo"); + break; + case SCRIPT_HATRAN: + families.push_back("Noto Sans Hatran"); + break; + case SCRIPT_KAITHI: + families.push_back("Noto Sans Kaithi"); + break; + case SCRIPT_KAYAH_LI: + families.push_back("Noto Sans Kayah Li"); + break; + case SCRIPT_KHOJKI: + families.push_back("Noto Sans Khojki"); + break; + case SCRIPT_KHUDAWADI: + families.push_back("Noto Sans Khudawadi"); + break; + case SCRIPT_LEPCHA: + families.push_back("Noto Sans Lepcha"); + break; + case SCRIPT_LIMBU: + families.push_back("Noto Sans Limbu"); + break; + case SCRIPT_LINEAR_A: + families.push_back("Noto Sans Linear A"); + break; + case SCRIPT_LINEAR_B: + families.push_back("Noto Sans Linear B"); + break; + case SCRIPT_MAHAJANI: + families.push_back("Noto Sans Mahajani"); + break; + case SCRIPT_MANDAIC: + families.push_back("Noto Sans Mandaic"); + break; + case SCRIPT_MANICHAEAN: + families.push_back("Noto Sans Manichaean"); + break; + case SCRIPT_MARCHEN: + families.push_back("Noto Sans Marchen"); + break; + case SCRIPT_MENDE_KIKAKUI: + families.push_back("Noto Sans Mende Kikakui"); + break; + case SCRIPT_MEROITIC_HIEROGLYPHS: + families.push_back("Noto Sans Meroitic"); + break; + case SCRIPT_MIAO: + families.push_back("Noto Sans Miao"); + break; + case SCRIPT_MODI: + families.push_back("Noto Sans Modi"); + break; + case SCRIPT_MRO: + families.push_back("Noto Sans Mro"); + break; + case SCRIPT_MULTANI: + families.push_back("Noto Sans Multani"); + break; + case SCRIPT_NABATAEAN: + families.push_back("Noto Sans Nabataean"); + break; + case SCRIPT_NEWA: + families.push_back("Noto Sans Newa"); + break; + case SCRIPT_OLD_HUNGARIAN: + families.push_back("Noto Sans Old Hungarian"); + break; + case SCRIPT_OLD_NORTH_ARABIAN: + families.push_back("Noto Sans Old North Arabian"); + break; + case SCRIPT_OLD_PERMIC: + families.push_back("Noto Sans Old Permic"); + break; + case SCRIPT_PAHAWH_HMONG: + families.push_back("Noto Sans Pahawh Hmong"); + break; + case SCRIPT_PALMYRENE: + families.push_back("Noto Sans Palmyrene"); + break; + case SCRIPT_PAU_CIN_HAU: + families.push_back("Noto Sans Pau Cin Hau"); + break; + case SCRIPT_PSALTER_PAHLAVI: + families.push_back("Noto Sans Psalter Pahlavi"); + break; + case SCRIPT_REJANG: + families.push_back("Noto Sans Rejang"); + break; + case SCRIPT_SAMARITAN: + families.push_back("Noto Sans Samaritan"); + break; + case SCRIPT_SAURASHTRA: + families.push_back("Noto Sans Saurashtra"); + break; + case SCRIPT_SHARADA: + families.push_back("Noto Sans Sharada"); + break; + case SCRIPT_SIDDHAM: + families.push_back("Noto Sans Siddham"); + break; + case SCRIPT_SUNDANESE: + families.push_back("Noto Sans Sundanese"); + break; + case SCRIPT_SYLOTI_NAGRI: + families.push_back("Noto Sans Syloti Nagri"); + break; + case SCRIPT_TAGALOG: + families.push_back("Noto Sans Tagalog"); + break; + case SCRIPT_TAGBANWA: + families.push_back("Noto Sans Tagbanwa"); + break; + case SCRIPT_TAI_THAM: + families.push_back("Noto Sans Tai Tham"); + break; + case SCRIPT_TAI_VIET: + families.push_back("Noto Sans Tai Viet"); + break; + case SCRIPT_TAKRI: + families.push_back("Noto Sans Takri"); + break; + case SCRIPT_TIRHUTA: + families.push_back("Noto Sans Tirhuta"); + break; + case SCRIPT_WANCHO: + families.push_back("Noto Sans Wancho"); + break; + case SCRIPT_WARANG_CITI: + families.push_back("Noto Sans Warang Citi"); + break; + + case SCRIPT_NONE: + case SCRIPT_ANATOLIAN_HIEROGLYPHS: + case SCRIPT_CHORASMIAN: + case SCRIPT_CYPRO_MINOAN: + case SCRIPT_DIVES_AKURU: + case SCRIPT_DOGRA: + case SCRIPT_ELYMAIC: + case SCRIPT_GARAY: + case SCRIPT_GUNJALA_GONDI: + case SCRIPT_GURUNG_KHEMA: + case SCRIPT_KAWI: + case SCRIPT_KHITAN_SMALL_SCRIPT: + case SCRIPT_KIRAT_RAI: + case SCRIPT_MAKASAR: + case SCRIPT_MASARAM_GONDI: + case SCRIPT_MEDEFAIDRIN: + case SCRIPT_NAG_MUNDARI: + case SCRIPT_NANDINAGARI: + case SCRIPT_NUSHU: + case SCRIPT_NYIAKENG_PUACHUE_HMONG: + case SCRIPT_OL_ONAL: + case SCRIPT_OLD_SOGDIAN: + case SCRIPT_OLD_UYGHUR: + case SCRIPT_SIGNWRITING: + case SCRIPT_SOGDIAN: + case SCRIPT_SOYOMBO: + case SCRIPT_SUNUWAR: + case SCRIPT_TANGSA: + case SCRIPT_TANGUT: + case SCRIPT_TODHRI: + case SCRIPT_TOTO: + case SCRIPT_TULU_TIGALARI: + case SCRIPT_VITHKUQI: + case SCRIPT_YEZIDI: + case SCRIPT_ZANABAZAR_SQUARE: + break; + } + + // Arial is used as default fallback for system fallback, so always try that. + families.push_back("Arial"); + + // TODO: Color Emoji should depend on if the default presentation for the + // codepoint is color or if a VS16 selector is present. For now we always + // prefer it (Firefox's PrefersColor() path). + families.push_back("Segoe UI Emoji"); + families.push_back("Twemoji Mozilla"); + + // Symbols/dingbats are generally Script=COMMON but may be resolved to any + // surrounding script run. So we'll always append a couple of likely fonts + // for such characters. (Firefox gates these on the codepoint, which we don't + // thread through here, so we add them unconditionally.) + families.push_back("Segoe UI"); + families.push_back("Segoe UI Symbol"); + families.push_back("Cambria Math"); + + // Arial Unicode MS also has lots of glyphs for obscure characters; try it as + // a last resort, if available. + families.push_back("Arial Unicode MS"); +} + +// See the preferences font.name-list.*.x-western in Firefox (Windows values). +const std::vector serif_fonts = {"Times New Roman"}; +const std::vector sans_serif_fonts = {"Arial"}; +const std::vector monospace_fonts = {"Consolas"}; +const std::vector cursive_fonts = {"Comic Sans MS"}; + +std::optional*> +FontManagerWindows::getGenericList(const std::string& generic) { + if (generic == "serif") { + return &serif_fonts; + } else if (generic == "sans-serif") { + return &sans_serif_fonts; + } else if (generic == "monospace") { + return &monospace_fonts; + } else if (generic == "cursive") { + return &cursive_fonts; + } else { // Firefox doesn't list any Fantasy fonts for Windows + return std::nullopt; + } +} diff --git a/src/FontManagerWindows.h b/src/FontManagerWindows.h new file mode 100644 index 000000000..b5d397648 --- /dev/null +++ b/src/FontManagerWindows.h @@ -0,0 +1,11 @@ +#pragma once + +#include "FontManager.h" +#include + +class FontManagerWindows : public FontManager { + public: + void readSystemFonts() override; + void populateFallbackFonts(std::vector& families, script_t script) override; + std::optional*> getGenericList(const std::string& generic) override; +}; diff --git a/src/FontParser.cc b/src/FontParser.cc index 773502cb3..87620b246 100644 --- a/src/FontParser.cc +++ b/src/FontParser.cc @@ -402,15 +402,15 @@ FontParser::parseFontStyle(FontProperties& props) { if (check(Token::Type::Identifier)) { const auto& value = currentToken_.getString(); if (value == "italic") { - props.fontStyle = FontStyle::Italic; + props.style = FontStyle::Italic; advance(); return true; } else if (value == "oblique") { - props.fontStyle = FontStyle::Oblique; + props.style = FontStyle::Oblique; advance(); return true; } else if (value == "normal") { - props.fontStyle = FontStyle::Normal; + props.style = FontStyle::Normal; advance(); return true; } @@ -424,11 +424,11 @@ FontParser::parseFontVariant(FontProperties& props) { if (check(Token::Type::Identifier)) { const auto& value = currentToken_.getString(); if (value == "small-caps") { - props.fontVariant = FontVariant::SmallCaps; + props.variant = FontVariant::SmallCaps; advance(); return true; } else if (value == "normal") { - props.fontVariant = FontVariant::Normal; + props.variant = FontVariant::Normal; advance(); return true; } @@ -443,14 +443,14 @@ FontParser::parseFontWeight(FontProperties& props) { double weightFloat = currentToken_.getNumber(); int weight = static_cast(weightFloat); if (weight < 1 || weight > 1000) return false; - props.fontWeight = static_cast(weight); + props.weight = static_cast(weight); advance(); return true; } else if (check(Token::Type::Identifier)) { const auto& value = currentToken_.getString(); if (auto it = weightMap.find(value); it != weightMap.end()) { - props.fontWeight = it->second; + props.weight = it->second; advance(); return true; } @@ -463,7 +463,7 @@ bool FontParser::parseFontSize(FontProperties& props) { if (!check(Token::Type::Number)) return false; - props.fontSize = currentToken_.getNumber(); + props.size = currentToken_.getNumber(); advance(); double multiplier = 1.0f; @@ -487,7 +487,7 @@ FontParser::parseFontSize(FontProperties& props) { // we should rewind the tokenizer, but I don't think the grammar allows for // any valid alternates in this specific case - props.fontSize *= multiplier; + props.size *= multiplier; return true; } @@ -550,7 +550,7 @@ FontParser::parseFontFamily(FontProperties& props) { if (!found) return false; // only whitespace or non-id/string found - props.fontFamily.push_back(family); + props.families.push_back(family); if (check(Token::Type::Comma)) advance(); } @@ -566,6 +566,24 @@ FontParser::parse(const std::string& fontString, bool* success) { return result; } +std::optional +FontParser::parseWeight(const std::string& source) { + FontParser parser(source); + FontProperties props; // TODO: use only the memory needed + parser.skipWs(); + if (parser.parseFontWeight(props)) return props.weight; + return std::nullopt; +} + +std::optional +FontParser::parseStyle(const std::string& source) { + FontParser parser(source); + FontProperties props; // TODO: use only the memory needed + parser.skipWs(); + if (parser.parseFontStyle(props)) return props.style; + return std::nullopt; +} + FontProperties FontParser::parseFont() { FontProperties props; diff --git a/src/FontParser.h b/src/FontParser.h index c88802109..3b5604e3b 100644 --- a/src/FontParser.h +++ b/src/FontParser.h @@ -8,24 +8,7 @@ #include #include "CharData.h" -enum class FontStyle { - Normal, - Italic, - Oblique -}; - -enum class FontVariant { - Normal, - SmallCaps -}; - -struct FontProperties { - double fontSize{16.0f}; - std::vector fontFamily; - uint16_t fontWeight{400}; - FontVariant fontVariant{FontVariant::Normal}; - FontStyle fontStyle{FontStyle::Normal}; -}; +#include "Font.h" class Token { public: @@ -88,6 +71,8 @@ class Tokenizer { class FontParser { public: static FontProperties parse(const std::string& fontString, bool* success = nullptr); + static std::optional parseWeight(const std::string& source); + static std::optional parseStyle(const std::string& source); private: static const std::unordered_map weightMap; diff --git a/src/InstanceData.h b/src/InstanceData.h index 983216ace..b8844ffad 100644 --- a/src/InstanceData.h +++ b/src/InstanceData.h @@ -1,4 +1,19 @@ +#pragma once + #include +#include +#include "FontFaceSet.h" +#ifdef _WIN32 +#include "FontManagerWindows.h" +using PlatformFontManager = FontManagerWindows; +#elif __APPLE__ +#include "FontManagerMacos.h" +using PlatformFontManager = FontManagerMacos; +#else +// Linux, the BSDs, and other Unixes that use FontConfig. +#include "FontManagerLinux.h" +using PlatformFontManager = FontManagerLinux; +#endif struct InstanceData { Napi::FunctionReference CanvasCtor; @@ -9,4 +24,17 @@ struct InstanceData { Napi::FunctionReference Context2dCtor; Napi::FunctionReference ImageDataCtor; Napi::FunctionReference CanvasPatternCtor; + Napi::FunctionReference FontFaceCtor; + Napi::ObjectReference jsFontSet; + FontFaceSet* cppFontSet; + FT_Library ft; + PlatformFontManager fontManager; + + InstanceData() { + FT_Init_FreeType(&ft); + } + + ~InstanceData() { + FT_Done_FreeType(ft); + } }; diff --git a/src/Util.h b/src/Util.h index 0e6d1d89c..5d57135a4 100644 --- a/src/Util.h +++ b/src/Util.h @@ -2,6 +2,15 @@ #include +#define LOG_LEVEL 0 + +#if LOG_LEVEL > 0 +#include +#define LOG(fmt, ...) printf(fmt, ##__VA_ARGS__) +#else +#define LOG(fmt, ...) +#endif + inline bool streq_casein(std::string& str1, std::string& str2) { return str1.size() == str2.size() && std::equal(str1.begin(), str1.end(), str2.begin(), [](char& c1, char& c2) { return c1 == c2 || std::toupper(c1) == std::toupper(c2); diff --git a/src/init.cc b/src/init.cc index 71902c1a9..474f3034f 100644 --- a/src/init.cc +++ b/src/init.cc @@ -13,6 +13,8 @@ #include "Image.h" #include "ImageData.h" #include "InstanceData.h" +#include "FontFace.h" +#include "FontFaceSet.h" /* * Save some external modules as private references. @@ -31,7 +33,9 @@ setParseFont(const Napi::CallbackInfo& info) { } Napi::Object init(Napi::Env env, Napi::Object exports) { - env.SetInstanceData(new InstanceData()); + InstanceData* data = new InstanceData(); + env.SetInstanceData(data); + data->fontManager.readSystemFonts(); Canvas::Initialize(env, exports); Image::Initialize(env, exports); @@ -39,6 +43,8 @@ Napi::Object init(Napi::Env env, Napi::Object exports) { Context2d::Initialize(env, exports); Gradient::Initialize(env, exports); Pattern::Initialize(env, exports); + FontFace::Initialize(env, exports); + FontFaceSet::Initialize(env, exports); exports.Set("setDOMMatrix", Napi::Function::New(env, &setDOMMatrix)); exports.Set("setParseFont", Napi::Function::New(env, &setParseFont)); diff --git a/src/itemize.cc b/src/itemize.cc new file mode 100644 index 000000000..a23a2b8d5 --- /dev/null +++ b/src/itemize.cc @@ -0,0 +1,225 @@ +#include +#include +#include +#include "itemize.h" + +void +bidi_iterator_next( + BidiIteratorState& state, + char16_t* textBuffer, + size_t textLength +) { + if (state.done) return; + + state.level = state.levels[state.offset]; + + bool should_break = false; + while (state.offset < textLength && !should_break) { + while (state.offset < textLength) { + if (state.levels[state.offset] != state.level) { + should_break = true; + break; + } + + state.offset += 1; + } + } + + if (state.offset == textLength) state.done = true; +} + +static const uint32_t paired_chars[] = { + 0x0028, 0x0029, /* ascii paired punctuation */ + 0x003c, 0x003e, + 0x005b, 0x005d, + 0x007b, 0x007d, + 0x00ab, 0x00bb, /* guillemets */ + 0x0f3a, 0x0f3b, /* tibetan */ + 0x0f3c, 0x0f3d, + 0x169b, 0x169c, /* ogham */ + 0x2018, 0x2019, /* general punctuation */ + 0x201c, 0x201d, + 0x2039, 0x203a, + 0x2045, 0x2046, + 0x207d, 0x207e, + 0x208d, 0x208e, + 0x27e6, 0x27e7, /* math */ + 0x27e8, 0x27e9, + 0x27ea, 0x27eb, + 0x27ec, 0x27ed, + 0x27ee, 0x27ef, + 0x2983, 0x2984, + 0x2985, 0x2986, + 0x2987, 0x2988, + 0x2989, 0x298a, + 0x298b, 0x298c, + 0x298d, 0x298e, + 0x298f, 0x2990, + 0x2991, 0x2992, + 0x2993, 0x2994, + 0x2995, 0x2996, + 0x2997, 0x2998, + 0x29fc, 0x29fd, + 0x2e02, 0x2e03, + 0x2e04, 0x2e05, + 0x2e09, 0x2e0a, + 0x2e0c, 0x2e0d, + 0x2e1c, 0x2e1d, + 0x2e20, 0x2e21, + 0x2e22, 0x2e23, + 0x2e24, 0x2e25, + 0x2e26, 0x2e27, + 0x2e28, 0x2e29, + 0x3008, 0x3009, /* chinese paired punctuation */ + 0x300a, 0x300b, + 0x300c, 0x300d, + 0x300e, 0x300f, + 0x3010, 0x3011, + 0x3014, 0x3015, + 0x3016, 0x3017, + 0x3018, 0x3019, + 0x301a, 0x301b, + 0xfe59, 0xfe5a, + 0xfe5b, 0xfe5c, + 0xfe5d, 0xfe5e, + 0xff08, 0xff09, + 0xff3b, 0xff3d, + 0xff5b, 0xff5d, + 0xff5f, 0xff60, + 0xff62, 0xff63 +}; + +static const size_t paired_chars_count = sizeof(paired_chars) / sizeof(paired_chars[0]); + +static int +get_pair_index(uint32_t ch) { + int lower = 0; + int upper = paired_chars_count - 1; + + while (lower <= upper) { + int mid = (lower + upper) / 2; + + if (ch < paired_chars[mid]) { + upper = mid - 1; + } else if (ch > paired_chars[mid]) { + lower = mid + 1; + } else { + return mid; + } + } + + return -1; +} + +void +script_iterator_next( + ScriptIteratorState& state, + char16_t* textBuffer, + size_t textLength +) { + if (state.done) return; + + state.script = SCRIPT_COMMON; + + while (state.offset < textLength) { + uint32_t code = textBuffer[state.offset]; + int jump = 1; + + // Handle surrogate pairs + if (state.offset + 1 < textLength) { + uint32_t next = textBuffer[state.offset + 1]; + if ((0xd800 <= code && code <= 0xdbff) && (0xdc00 <= next && next <= 0xdfff)) { + jump = 2; + code = ((code - 0xd800) * 0x400) + (next - 0xdc00) + 0x10000; + } + } + + script_t script = get_script(code); + int pair_index = script != SCRIPT_COMMON ? -1 : get_pair_index(code); + + // Paired character handling: + // if it's an open character, push it onto the stack + // if it's a close character, find the matching open on the stack, and use + // that script code. Any non-matching open characters above it on the stack + // will be popped. + if (pair_index >= 0) { + if ((pair_index & 1) == 0) { + // Open character + state.parens.push_back({pair_index, state.script}); + } else if (!state.parens.empty()) { + // Close character + int pi = pair_index & ~1; + + while (!state.parens.empty() && state.parens.back().index != pi) { + state.parens.pop_back(); + } + + if (static_cast(state.parens.size()) - 1 < state.start_paren) { + state.start_paren = static_cast(state.parens.size()) - 1; + } + + if (!state.parens.empty()) { + script = state.parens.back().script; + } + } + } + + bool running_is_real = state.script != SCRIPT_COMMON && state.script != SCRIPT_INHERITED; + bool is_real = script != SCRIPT_COMMON && script != SCRIPT_INHERITED; + bool is_same = !running_is_real || !is_real || script == state.script; + + if (is_same) { + if (!running_is_real && is_real) { + state.script = script; + + // Now that we have a final script code, fix any open characters we + // pushed before we knew the real script code. + while (state.start_paren + 1 < static_cast(state.parens.size())) { + state.parens[++state.start_paren].script = script; + } + + if (pair_index >= 0 && (pair_index & 1) && !state.parens.empty()) { + state.parens.pop_back(); + + if (static_cast(state.parens.size()) - 1 < state.start_paren) { + state.start_paren = static_cast(state.parens.size()) - 1; + } + } + } + + state.offset += jump; + } else { + state.start_paren = static_cast(state.parens.size()) - 1; + break; + } + } + + if (state.offset >= textLength) { + state.done = true; + } +} + +void +itemizeNext(ItemizeState& state) { + if (state.done) return; + + if (state.bidi_state.offset == state.offset) { + bidi_iterator_next(state.bidi_state, state.textBuffer, state.textLength); + } + + if (state.script_state.offset == state.offset) { + script_iterator_next(state.script_state, state.textBuffer, state.textLength); + } + + state.offset = std::min( + std::min( + state.bidi_state.offset, + state.script_state.offset + ), + state.textLength + ); + + if (state.bidi_state.done && state.script_state.done) { + state.done = true; + } +} diff --git a/src/itemize.h b/src/itemize.h new file mode 100644 index 000000000..d114a3054 --- /dev/null +++ b/src/itemize.h @@ -0,0 +1,81 @@ +#pragma once + +#include +#include +#include +#include "unicode.h" + +struct ParenInfo { + int index; + script_t script; +}; + +struct ScriptIteratorState { + // Output + size_t offset = 0; + script_t script = SCRIPT_COMMON; + bool done = false; + + // Private state + std::vector parens; + int start_paren = -1; +}; + +struct BidiIteratorState { + BidiIteratorState( + char16_t* textBuffer, + size_t textLength, + uint8_t initialLevel + ) { + SBCodepointSequence codepointSequence = { + SBStringEncodingUTF16, + textBuffer, + textLength + }; + algorithm = SBAlgorithmCreate(&codepointSequence); + paragraph = SBAlgorithmCreateParagraph( + algorithm, + offset, + textLength, + initialLevel + ); + levels = SBParagraphGetLevelsPtr(paragraph); + } + + ~BidiIteratorState() { + if (paragraph != nullptr) SBParagraphRelease(paragraph); + if (algorithm != nullptr) SBAlgorithmRelease(algorithm); + } + + // Output + size_t offset = 0; + uint8_t level = 0; + bool done = false; + + // Private state + SBAlgorithmRef algorithm = nullptr; + SBParagraphRef paragraph = nullptr; + const SBLevel* levels = nullptr; +}; + +struct ItemizeState { + ItemizeState( + char16_t* textBuffer, + size_t textLength, + uint8_t initialLevel + ) : textBuffer(textBuffer) + , textLength(textLength) + , bidi_state(textBuffer, textLength, initialLevel) {} + + // Output + size_t offset = 0; + bool done = false; + + // Private state + char16_t* textBuffer; + size_t textLength; + BidiIteratorState bidi_state; + ScriptIteratorState script_state; +}; + +void itemizeNext(ItemizeState& state); diff --git a/src/unicode.h b/src/unicode.h new file mode 100644 index 000000000..14669c1bc --- /dev/null +++ b/src/unicode.h @@ -0,0 +1,363 @@ +// This is a manually written file for the C API of unicode.zig +// TODO: delete this when Zig resurrects -femit-h +// https://github.com/ziglang/zig/issues/9698 +#pragma once + +#include +#include + +typedef enum { + SCRIPT_NONE, + SCRIPT_ADLAM, + SCRIPT_AHOM, + SCRIPT_ANATOLIAN_HIEROGLYPHS, + SCRIPT_ARABIC, + SCRIPT_ARMENIAN, + SCRIPT_AVESTAN, + SCRIPT_BALINESE, + SCRIPT_BAMUM, + SCRIPT_BASSA_VAH, + SCRIPT_BATAK, + SCRIPT_BENGALI, + SCRIPT_BHAIKSUKI, + SCRIPT_BOPOMOFO, + SCRIPT_BRAHMI, + SCRIPT_BRAILLE, + SCRIPT_BUGINESE, + SCRIPT_BUHID, + SCRIPT_CANADIAN_ABORIGINAL, + SCRIPT_CARIAN, + SCRIPT_CAUCASIAN_ALBANIAN, + SCRIPT_CHAKMA, + SCRIPT_CHAM, + SCRIPT_CHEROKEE, + SCRIPT_CHORASMIAN, + SCRIPT_COMMON, + SCRIPT_COPTIC, + SCRIPT_CUNEIFORM, + SCRIPT_CYPRIOT, + SCRIPT_CYPRO_MINOAN, + SCRIPT_CYRILLIC, + SCRIPT_DESERET, + SCRIPT_DEVANAGARI, + SCRIPT_DIVES_AKURU, + SCRIPT_DOGRA, + SCRIPT_DUPLOYAN, + SCRIPT_EGYPTIAN_HIEROGLYPHS, + SCRIPT_ELBASAN, + SCRIPT_ELYMAIC, + SCRIPT_ETHIOPIC, + SCRIPT_GARAY, + SCRIPT_GEORGIAN, + SCRIPT_GLAGOLITIC, + SCRIPT_GOTHIC, + SCRIPT_GRANTHA, + SCRIPT_GREEK, + SCRIPT_GUJARATI, + SCRIPT_GUNJALA_GONDI, + SCRIPT_GURMUKHI, + SCRIPT_GURUNG_KHEMA, + SCRIPT_HAN, + SCRIPT_HANGUL, + SCRIPT_HANIFI_ROHINGYA, + SCRIPT_HANUNOO, + SCRIPT_HATRAN, + SCRIPT_HEBREW, + SCRIPT_HIRAGANA, + SCRIPT_IMPERIAL_ARAMAIC, + SCRIPT_INHERITED, + SCRIPT_INSCRIPTIONAL_PAHLAVI, + SCRIPT_INSCRIPTIONAL_PARTHIAN, + SCRIPT_JAVANESE, + SCRIPT_KAITHI, + SCRIPT_KANNADA, + SCRIPT_KATAKANA, + SCRIPT_KAWI, + SCRIPT_KAYAH_LI, + SCRIPT_KHAROSHTHI, + SCRIPT_KHITAN_SMALL_SCRIPT, + SCRIPT_KHMER, + SCRIPT_KHOJKI, + SCRIPT_KHUDAWADI, + SCRIPT_KIRAT_RAI, + SCRIPT_LAO, + SCRIPT_LATIN, + SCRIPT_LEPCHA, + SCRIPT_LIMBU, + SCRIPT_LINEAR_A, + SCRIPT_LINEAR_B, + SCRIPT_LISU, + SCRIPT_LYCIAN, + SCRIPT_LYDIAN, + SCRIPT_MAHAJANI, + SCRIPT_MAKASAR, + SCRIPT_MALAYALAM, + SCRIPT_MANDAIC, + SCRIPT_MANICHAEAN, + SCRIPT_MARCHEN, + SCRIPT_MASARAM_GONDI, + SCRIPT_MEDEFAIDRIN, + SCRIPT_MEETEI_MAYEK, + SCRIPT_MENDE_KIKAKUI, + SCRIPT_MEROITIC_CURSIVE, + SCRIPT_MEROITIC_HIEROGLYPHS, + SCRIPT_MIAO, + SCRIPT_MODI, + SCRIPT_MONGOLIAN, + SCRIPT_MRO, + SCRIPT_MULTANI, + SCRIPT_MYANMAR, + SCRIPT_NABATAEAN, + SCRIPT_NAG_MUNDARI, + SCRIPT_NANDINAGARI, + SCRIPT_NEW_TAI_LUE, + SCRIPT_NEWA, + SCRIPT_NKO, + SCRIPT_NUSHU, + SCRIPT_NYIAKENG_PUACHUE_HMONG, + SCRIPT_OGHAM, + SCRIPT_OL_CHIKI, + SCRIPT_OL_ONAL, + SCRIPT_OLD_HUNGARIAN, + SCRIPT_OLD_ITALIC, + SCRIPT_OLD_NORTH_ARABIAN, + SCRIPT_OLD_PERMIC, + SCRIPT_OLD_PERSIAN, + SCRIPT_OLD_SOGDIAN, + SCRIPT_OLD_SOUTH_ARABIAN, + SCRIPT_OLD_TURKIC, + SCRIPT_OLD_UYGHUR, + SCRIPT_ORIYA, + SCRIPT_OSAGE, + SCRIPT_OSMANYA, + SCRIPT_PAHAWH_HMONG, + SCRIPT_PALMYRENE, + SCRIPT_PAU_CIN_HAU, + SCRIPT_PHAGS_PA, + SCRIPT_PHOENICIAN, + SCRIPT_PSALTER_PAHLAVI, + SCRIPT_REJANG, + SCRIPT_RUNIC, + SCRIPT_SAMARITAN, + SCRIPT_SAURASHTRA, + SCRIPT_SHARADA, + SCRIPT_SHAVIAN, + SCRIPT_SIDDHAM, + SCRIPT_SIGNWRITING, + SCRIPT_SINHALA, + SCRIPT_SOGDIAN, + SCRIPT_SORA_SOMPENG, + SCRIPT_SOYOMBO, + SCRIPT_SUNDANESE, + SCRIPT_SUNUWAR, + SCRIPT_SYLOTI_NAGRI, + SCRIPT_SYRIAC, + SCRIPT_TAGALOG, + SCRIPT_TAGBANWA, + SCRIPT_TAI_LE, + SCRIPT_TAI_THAM, + SCRIPT_TAI_VIET, + SCRIPT_TAKRI, + SCRIPT_TAMIL, + SCRIPT_TANGSA, + SCRIPT_TANGUT, + SCRIPT_TELUGU, + SCRIPT_THAANA, + SCRIPT_THAI, + SCRIPT_TIBETAN, + SCRIPT_TIFINAGH, + SCRIPT_TIRHUTA, + SCRIPT_TODHRI, + SCRIPT_TOTO, + SCRIPT_TULU_TIGALARI, + SCRIPT_UGARITIC, + SCRIPT_VAI, + SCRIPT_VITHKUQI, + SCRIPT_WANCHO, + SCRIPT_WARANG_CITI, + SCRIPT_YEZIDI, + SCRIPT_YI, + SCRIPT_ZANABAZAR_SQUARE +} script_t; + +extern "C" { + script_t get_script(uint32_t cp); + bool grapheme_break(uint8_t* state, uint32_t cp1, uint32_t cp2); +} + +inline hb_script_t script_to_hb_script(script_t script) { + switch (script) { + case SCRIPT_NONE: return HB_SCRIPT_UNKNOWN; + case SCRIPT_COMMON: return HB_SCRIPT_COMMON; + case SCRIPT_INHERITED: return HB_SCRIPT_INHERITED; + case SCRIPT_ADLAM: return HB_SCRIPT_ADLAM; + case SCRIPT_AHOM: return HB_SCRIPT_AHOM; + case SCRIPT_ANATOLIAN_HIEROGLYPHS: return HB_SCRIPT_ANATOLIAN_HIEROGLYPHS; + case SCRIPT_ARABIC: return HB_SCRIPT_ARABIC; + case SCRIPT_ARMENIAN: return HB_SCRIPT_ARMENIAN; + case SCRIPT_AVESTAN: return HB_SCRIPT_AVESTAN; + case SCRIPT_BALINESE: return HB_SCRIPT_BALINESE; + case SCRIPT_BAMUM: return HB_SCRIPT_BAMUM; + case SCRIPT_BASSA_VAH: return HB_SCRIPT_BASSA_VAH; + case SCRIPT_BATAK: return HB_SCRIPT_BATAK; + case SCRIPT_BENGALI: return HB_SCRIPT_BENGALI; + case SCRIPT_BHAIKSUKI: return HB_SCRIPT_BHAIKSUKI; + case SCRIPT_BOPOMOFO: return HB_SCRIPT_BOPOMOFO; + case SCRIPT_BRAHMI: return HB_SCRIPT_BRAHMI; + case SCRIPT_BRAILLE: return HB_SCRIPT_BRAILLE; + case SCRIPT_BUGINESE: return HB_SCRIPT_BUGINESE; + case SCRIPT_BUHID: return HB_SCRIPT_BUHID; + case SCRIPT_CANADIAN_ABORIGINAL: return HB_SCRIPT_CANADIAN_SYLLABICS; + case SCRIPT_CARIAN: return HB_SCRIPT_CARIAN; + case SCRIPT_CAUCASIAN_ALBANIAN: return HB_SCRIPT_CAUCASIAN_ALBANIAN; + case SCRIPT_CHAKMA: return HB_SCRIPT_CHAKMA; + case SCRIPT_CHAM: return HB_SCRIPT_CHAM; + case SCRIPT_CHEROKEE: return HB_SCRIPT_CHEROKEE; + case SCRIPT_CHORASMIAN: return HB_SCRIPT_CHORASMIAN; + case SCRIPT_COPTIC: return HB_SCRIPT_COPTIC; + case SCRIPT_CUNEIFORM: return HB_SCRIPT_CUNEIFORM; + case SCRIPT_CYPRIOT: return HB_SCRIPT_CYPRIOT; + case SCRIPT_CYPRO_MINOAN: return HB_SCRIPT_CYPRO_MINOAN; + case SCRIPT_CYRILLIC: return HB_SCRIPT_CYRILLIC; + case SCRIPT_DESERET: return HB_SCRIPT_DESERET; + case SCRIPT_DEVANAGARI: return HB_SCRIPT_DEVANAGARI; + case SCRIPT_DIVES_AKURU: return HB_SCRIPT_DIVES_AKURU; + case SCRIPT_DOGRA: return HB_SCRIPT_DOGRA; + case SCRIPT_DUPLOYAN: return HB_SCRIPT_DUPLOYAN; + case SCRIPT_EGYPTIAN_HIEROGLYPHS: return HB_SCRIPT_EGYPTIAN_HIEROGLYPHS; + case SCRIPT_ELBASAN: return HB_SCRIPT_ELBASAN; + case SCRIPT_ELYMAIC: return HB_SCRIPT_ELYMAIC; + case SCRIPT_ETHIOPIC: return HB_SCRIPT_ETHIOPIC; + case SCRIPT_GARAY: return HB_SCRIPT_GARAY; + case SCRIPT_GEORGIAN: return HB_SCRIPT_GEORGIAN; + case SCRIPT_GLAGOLITIC: return HB_SCRIPT_GLAGOLITIC; + case SCRIPT_GOTHIC: return HB_SCRIPT_GOTHIC; + case SCRIPT_GRANTHA: return HB_SCRIPT_GRANTHA; + case SCRIPT_GREEK: return HB_SCRIPT_GREEK; + case SCRIPT_GUJARATI: return HB_SCRIPT_GUJARATI; + case SCRIPT_GUNJALA_GONDI: return HB_SCRIPT_GUNJALA_GONDI; + case SCRIPT_GURMUKHI: return HB_SCRIPT_GURMUKHI; + case SCRIPT_GURUNG_KHEMA: return HB_SCRIPT_GURUNG_KHEMA; + case SCRIPT_HAN: return HB_SCRIPT_HAN; + case SCRIPT_HANGUL: return HB_SCRIPT_HANGUL; + case SCRIPT_HANIFI_ROHINGYA: return HB_SCRIPT_HANIFI_ROHINGYA; + case SCRIPT_HANUNOO: return HB_SCRIPT_HANUNOO; + case SCRIPT_HATRAN: return HB_SCRIPT_HATRAN; + case SCRIPT_HEBREW: return HB_SCRIPT_HEBREW; + case SCRIPT_HIRAGANA: return HB_SCRIPT_HIRAGANA; + case SCRIPT_IMPERIAL_ARAMAIC: return HB_SCRIPT_IMPERIAL_ARAMAIC; + case SCRIPT_INSCRIPTIONAL_PAHLAVI: return HB_SCRIPT_INSCRIPTIONAL_PAHLAVI; + case SCRIPT_INSCRIPTIONAL_PARTHIAN: return HB_SCRIPT_INSCRIPTIONAL_PARTHIAN; + case SCRIPT_JAVANESE: return HB_SCRIPT_JAVANESE; + case SCRIPT_KAITHI: return HB_SCRIPT_KAITHI; + case SCRIPT_KANNADA: return HB_SCRIPT_KANNADA; + case SCRIPT_KATAKANA: return HB_SCRIPT_KATAKANA; + case SCRIPT_KAWI: return HB_SCRIPT_KAWI; + case SCRIPT_KAYAH_LI: return HB_SCRIPT_KAYAH_LI; + case SCRIPT_KHAROSHTHI: return HB_SCRIPT_KHAROSHTHI; + case SCRIPT_KHITAN_SMALL_SCRIPT: return HB_SCRIPT_KHITAN_SMALL_SCRIPT; + case SCRIPT_KHMER: return HB_SCRIPT_KHMER; + case SCRIPT_KHOJKI: return HB_SCRIPT_KHOJKI; + case SCRIPT_KHUDAWADI: return HB_SCRIPT_KHUDAWADI; + case SCRIPT_KIRAT_RAI: return HB_SCRIPT_KIRAT_RAI; + case SCRIPT_LAO: return HB_SCRIPT_LAO; + case SCRIPT_LATIN: return HB_SCRIPT_LATIN; + case SCRIPT_LEPCHA: return HB_SCRIPT_LEPCHA; + case SCRIPT_LIMBU: return HB_SCRIPT_LIMBU; + case SCRIPT_LINEAR_A: return HB_SCRIPT_LINEAR_A; + case SCRIPT_LINEAR_B: return HB_SCRIPT_LINEAR_B; + case SCRIPT_LISU: return HB_SCRIPT_LISU; + case SCRIPT_LYCIAN: return HB_SCRIPT_LYCIAN; + case SCRIPT_LYDIAN: return HB_SCRIPT_LYDIAN; + case SCRIPT_MAHAJANI: return HB_SCRIPT_MAHAJANI; + case SCRIPT_MAKASAR: return HB_SCRIPT_MAKASAR; + case SCRIPT_MALAYALAM: return HB_SCRIPT_MALAYALAM; + case SCRIPT_MANDAIC: return HB_SCRIPT_MANDAIC; + case SCRIPT_MANICHAEAN: return HB_SCRIPT_MANICHAEAN; + case SCRIPT_MARCHEN: return HB_SCRIPT_MARCHEN; + case SCRIPT_MASARAM_GONDI: return HB_SCRIPT_MASARAM_GONDI; + case SCRIPT_MEDEFAIDRIN: return HB_SCRIPT_MEDEFAIDRIN; + case SCRIPT_MEETEI_MAYEK: return HB_SCRIPT_MEETEI_MAYEK; + case SCRIPT_MENDE_KIKAKUI: return HB_SCRIPT_MENDE_KIKAKUI; + case SCRIPT_MEROITIC_CURSIVE: return HB_SCRIPT_MEROITIC_CURSIVE; + case SCRIPT_MEROITIC_HIEROGLYPHS: return HB_SCRIPT_MEROITIC_HIEROGLYPHS; + case SCRIPT_MIAO: return HB_SCRIPT_MIAO; + case SCRIPT_MODI: return HB_SCRIPT_MODI; + case SCRIPT_MONGOLIAN: return HB_SCRIPT_MONGOLIAN; + case SCRIPT_MRO: return HB_SCRIPT_MRO; + case SCRIPT_MULTANI: return HB_SCRIPT_MULTANI; + case SCRIPT_MYANMAR: return HB_SCRIPT_MYANMAR; + case SCRIPT_NABATAEAN: return HB_SCRIPT_NABATAEAN; + case SCRIPT_NAG_MUNDARI: return HB_SCRIPT_NAG_MUNDARI; + case SCRIPT_NANDINAGARI: return HB_SCRIPT_NANDINAGARI; + case SCRIPT_NEW_TAI_LUE: return HB_SCRIPT_NEW_TAI_LUE; + case SCRIPT_NEWA: return HB_SCRIPT_NEWA; + case SCRIPT_NKO: return HB_SCRIPT_NKO; + case SCRIPT_NUSHU: return HB_SCRIPT_NUSHU; + case SCRIPT_NYIAKENG_PUACHUE_HMONG: return HB_SCRIPT_NYIAKENG_PUACHUE_HMONG; + case SCRIPT_OGHAM: return HB_SCRIPT_OGHAM; + case SCRIPT_OL_CHIKI: return HB_SCRIPT_OL_CHIKI; + case SCRIPT_OL_ONAL: return HB_SCRIPT_OL_ONAL; + case SCRIPT_OLD_HUNGARIAN: return HB_SCRIPT_OLD_HUNGARIAN; + case SCRIPT_OLD_ITALIC: return HB_SCRIPT_OLD_ITALIC; + case SCRIPT_OLD_NORTH_ARABIAN: return HB_SCRIPT_OLD_NORTH_ARABIAN; + case SCRIPT_OLD_PERMIC: return HB_SCRIPT_OLD_PERMIC; + case SCRIPT_OLD_PERSIAN: return HB_SCRIPT_OLD_PERSIAN; + case SCRIPT_OLD_SOGDIAN: return HB_SCRIPT_OLD_SOGDIAN; + case SCRIPT_OLD_SOUTH_ARABIAN: return HB_SCRIPT_OLD_SOUTH_ARABIAN; + case SCRIPT_OLD_TURKIC: return HB_SCRIPT_OLD_TURKIC; + case SCRIPT_OLD_UYGHUR: return HB_SCRIPT_OLD_UYGHUR; + case SCRIPT_ORIYA: return HB_SCRIPT_ORIYA; + case SCRIPT_OSAGE: return HB_SCRIPT_OSAGE; + case SCRIPT_OSMANYA: return HB_SCRIPT_OSMANYA; + case SCRIPT_PAHAWH_HMONG: return HB_SCRIPT_PAHAWH_HMONG; + case SCRIPT_PALMYRENE: return HB_SCRIPT_PALMYRENE; + case SCRIPT_PAU_CIN_HAU: return HB_SCRIPT_PAU_CIN_HAU; + case SCRIPT_PHAGS_PA: return HB_SCRIPT_PHAGS_PA; + case SCRIPT_PHOENICIAN: return HB_SCRIPT_PHOENICIAN; + case SCRIPT_PSALTER_PAHLAVI: return HB_SCRIPT_PSALTER_PAHLAVI; + case SCRIPT_REJANG: return HB_SCRIPT_REJANG; + case SCRIPT_RUNIC: return HB_SCRIPT_RUNIC; + case SCRIPT_SAMARITAN: return HB_SCRIPT_SAMARITAN; + case SCRIPT_SAURASHTRA: return HB_SCRIPT_SAURASHTRA; + case SCRIPT_SHARADA: return HB_SCRIPT_SHARADA; + case SCRIPT_SHAVIAN: return HB_SCRIPT_SHAVIAN; + case SCRIPT_SIDDHAM: return HB_SCRIPT_SIDDHAM; + case SCRIPT_SIGNWRITING: return HB_SCRIPT_SIGNWRITING; + case SCRIPT_SINHALA: return HB_SCRIPT_SINHALA; + case SCRIPT_SOGDIAN: return HB_SCRIPT_SOGDIAN; + case SCRIPT_SORA_SOMPENG: return HB_SCRIPT_SORA_SOMPENG; + case SCRIPT_SOYOMBO: return HB_SCRIPT_SOYOMBO; + case SCRIPT_SUNDANESE: return HB_SCRIPT_SUNDANESE; + case SCRIPT_SUNUWAR: return HB_SCRIPT_SUNUWAR; + case SCRIPT_SYLOTI_NAGRI: return HB_SCRIPT_SYLOTI_NAGRI; + case SCRIPT_SYRIAC: return HB_SCRIPT_SYRIAC; + case SCRIPT_TAGALOG: return HB_SCRIPT_TAGALOG; + case SCRIPT_TAGBANWA: return HB_SCRIPT_TAGBANWA; + case SCRIPT_TAI_LE: return HB_SCRIPT_TAI_LE; + case SCRIPT_TAI_THAM: return HB_SCRIPT_TAI_THAM; + case SCRIPT_TAI_VIET: return HB_SCRIPT_TAI_VIET; + case SCRIPT_TAKRI: return HB_SCRIPT_TAKRI; + case SCRIPT_TAMIL: return HB_SCRIPT_TAMIL; + case SCRIPT_TANGSA: return HB_SCRIPT_TANGSA; + case SCRIPT_TANGUT: return HB_SCRIPT_TANGUT; + case SCRIPT_TELUGU: return HB_SCRIPT_TELUGU; + case SCRIPT_THAANA: return HB_SCRIPT_THAANA; + case SCRIPT_THAI: return HB_SCRIPT_THAI; + case SCRIPT_TIBETAN: return HB_SCRIPT_TIBETAN; + case SCRIPT_TIFINAGH: return HB_SCRIPT_TIFINAGH; + case SCRIPT_TIRHUTA: return HB_SCRIPT_TIRHUTA; + case SCRIPT_TODHRI: return HB_SCRIPT_TODHRI; + case SCRIPT_TOTO: return HB_SCRIPT_TOTO; + case SCRIPT_TULU_TIGALARI: return HB_SCRIPT_TULU_TIGALARI; + case SCRIPT_UGARITIC: return HB_SCRIPT_UGARITIC; + case SCRIPT_VAI: return HB_SCRIPT_VAI; + case SCRIPT_VITHKUQI: return HB_SCRIPT_VITHKUQI; + case SCRIPT_WANCHO: return HB_SCRIPT_WANCHO; + case SCRIPT_WARANG_CITI: return HB_SCRIPT_WARANG_CITI; + case SCRIPT_YEZIDI: return HB_SCRIPT_YEZIDI; + case SCRIPT_YI: return HB_SCRIPT_YI; + case SCRIPT_ZANABAZAR_SQUARE: return HB_SCRIPT_ZANABAZAR_SQUARE; + default: return HB_SCRIPT_UNKNOWN; + } +} diff --git a/src/unicode.zig b/src/unicode.zig new file mode 100644 index 000000000..30d6237d9 --- /dev/null +++ b/src/unicode.zig @@ -0,0 +1,386 @@ +// This file binds the zg library to C. We use zg to: +// 1. Segment text into scripts for shaping boundaries +// 2. Find Unicode grapheme boundaries for fallback shaping +// 3. Eventually: determine emoji presentation +// (this will rewuire a PR to zg though) +const Scripts = @import("Scripts"); +const Graphemes = @import("Graphemes"); +const std = @import("std"); + +pub const Script = enum(c_int) { + none, + Adlam, + Ahom, + Anatolian_Hieroglyphs, + Arabic, + Armenian, + Avestan, + Balinese, + Bamum, + Bassa_Vah, + Batak, + Bengali, + Bhaiksuki, + Bopomofo, + Brahmi, + Braille, + Buginese, + Buhid, + Canadian_Aboriginal, + Carian, + Caucasian_Albanian, + Chakma, + Cham, + Cherokee, + Chorasmian, + Common, + Coptic, + Cuneiform, + Cypriot, + Cypro_Minoan, + Cyrillic, + Deseret, + Devanagari, + Dives_Akuru, + Dogra, + Duployan, + Egyptian_Hieroglyphs, + Elbasan, + Elymaic, + Ethiopic, + Garay, + Georgian, + Glagolitic, + Gothic, + Grantha, + Greek, + Gujarati, + Gunjala_Gondi, + Gurmukhi, + Gurung_Khema, + Han, + Hangul, + Hanifi_Rohingya, + Hanunoo, + Hatran, + Hebrew, + Hiragana, + Imperial_Aramaic, + Inherited, + Inscriptional_Pahlavi, + Inscriptional_Parthian, + Javanese, + Kaithi, + Kannada, + Katakana, + Kawi, + Kayah_Li, + Kharoshthi, + Khitan_Small_Script, + Khmer, + Khojki, + Khudawadi, + Kirat_Rai, + Lao, + Latin, + Lepcha, + Limbu, + Linear_A, + Linear_B, + Lisu, + Lycian, + Lydian, + Mahajani, + Makasar, + Malayalam, + Mandaic, + Manichaean, + Marchen, + Masaram_Gondi, + Medefaidrin, + Meetei_Mayek, + Mende_Kikakui, + Meroitic_Cursive, + Meroitic_Hieroglyphs, + Miao, + Modi, + Mongolian, + Mro, + Multani, + Myanmar, + Nabataean, + Nag_Mundari, + Nandinagari, + New_Tai_Lue, + Newa, + Nko, + Nushu, + Nyiakeng_Puachue_Hmong, + Ogham, + Ol_Chiki, + Ol_Onal, + Old_Hungarian, + Old_Italic, + Old_North_Arabian, + Old_Permic, + Old_Persian, + Old_Sogdian, + Old_South_Arabian, + Old_Turkic, + Old_Uyghur, + Oriya, + Osage, + Osmanya, + Pahawh_Hmong, + Palmyrene, + Pau_Cin_Hau, + Phags_Pa, + Phoenician, + Psalter_Pahlavi, + Rejang, + Runic, + Samaritan, + Saurashtra, + Sharada, + Shavian, + Siddham, + SignWriting, + Sinhala, + Sogdian, + Sora_Sompeng, + Soyombo, + Sundanese, + Sunuwar, + Syloti_Nagri, + Syriac, + Tagalog, + Tagbanwa, + Tai_Le, + Tai_Tham, + Tai_Viet, + Takri, + Tamil, + Tangsa, + Tangut, + Telugu, + Thaana, + Thai, + Tibetan, + Tifinagh, + Tirhuta, + Todhri, + Toto, + Tulu_Tigalari, + Ugaritic, + Vai, + Vithkuqi, + Wancho, + Warang_Citi, + Yezidi, + Yi, + Zanabazar_Square, +}; + +var scripts: Scripts = undefined; +var initializedScripts: bool = false; + +export fn get_script(cp: u32) callconv(.c) Script { + // TODO: initialize once, not every get_script call + if (!initializedScripts) { + scripts = Scripts.init(std.heap.c_allocator) catch @panic("Failed to initialize scripts"); + initializedScripts = true; + } + + const script_result = scripts.script(@as(u21, @intCast(cp))) orelse return .none; + + return switch (script_result) { + .none => .none, + .Adlam => .Adlam, + .Ahom => .Ahom, + .Anatolian_Hieroglyphs => .Anatolian_Hieroglyphs, + .Arabic => .Arabic, + .Armenian => .Armenian, + .Avestan => .Avestan, + .Balinese => .Balinese, + .Bamum => .Bamum, + .Bassa_Vah => .Bassa_Vah, + .Batak => .Batak, + .Bengali => .Bengali, + .Bhaiksuki => .Bhaiksuki, + .Bopomofo => .Bopomofo, + .Brahmi => .Brahmi, + .Braille => .Braille, + .Buginese => .Buginese, + .Buhid => .Buhid, + .Canadian_Aboriginal => .Canadian_Aboriginal, + .Carian => .Carian, + .Caucasian_Albanian => .Caucasian_Albanian, + .Chakma => .Chakma, + .Cham => .Cham, + .Cherokee => .Cherokee, + .Chorasmian => .Chorasmian, + .Common => .Common, + .Coptic => .Coptic, + .Cuneiform => .Cuneiform, + .Cypriot => .Cypriot, + .Cypro_Minoan => .Cypro_Minoan, + .Cyrillic => .Cyrillic, + .Deseret => .Deseret, + .Devanagari => .Devanagari, + .Dives_Akuru => .Dives_Akuru, + .Dogra => .Dogra, + .Duployan => .Duployan, + .Egyptian_Hieroglyphs => .Egyptian_Hieroglyphs, + .Elbasan => .Elbasan, + .Elymaic => .Elymaic, + .Ethiopic => .Ethiopic, + .Garay => .Garay, + .Georgian => .Georgian, + .Glagolitic => .Glagolitic, + .Gothic => .Gothic, + .Grantha => .Grantha, + .Greek => .Greek, + .Gujarati => .Gujarati, + .Gunjala_Gondi => .Gunjala_Gondi, + .Gurmukhi => .Gurmukhi, + .Gurung_Khema => .Gurung_Khema, + .Han => .Han, + .Hangul => .Hangul, + .Hanifi_Rohingya => .Hanifi_Rohingya, + .Hanunoo => .Hanunoo, + .Hatran => .Hatran, + .Hebrew => .Hebrew, + .Hiragana => .Hiragana, + .Imperial_Aramaic => .Imperial_Aramaic, + .Inherited => .Inherited, + .Inscriptional_Pahlavi => .Inscriptional_Pahlavi, + .Inscriptional_Parthian => .Inscriptional_Parthian, + .Javanese => .Javanese, + .Kaithi => .Kaithi, + .Kannada => .Kannada, + .Katakana => .Katakana, + .Kawi => .Kawi, + .Kayah_Li => .Kayah_Li, + .Kharoshthi => .Kharoshthi, + .Khitan_Small_Script => .Khitan_Small_Script, + .Khmer => .Khmer, + .Khojki => .Khojki, + .Khudawadi => .Khudawadi, + .Kirat_Rai => .Kirat_Rai, + .Lao => .Lao, + .Latin => .Latin, + .Lepcha => .Lepcha, + .Limbu => .Limbu, + .Linear_A => .Linear_A, + .Linear_B => .Linear_B, + .Lisu => .Lisu, + .Lycian => .Lycian, + .Lydian => .Lydian, + .Mahajani => .Mahajani, + .Makasar => .Makasar, + .Malayalam => .Malayalam, + .Mandaic => .Mandaic, + .Manichaean => .Manichaean, + .Marchen => .Marchen, + .Masaram_Gondi => .Masaram_Gondi, + .Medefaidrin => .Medefaidrin, + .Meetei_Mayek => .Meetei_Mayek, + .Mende_Kikakui => .Mende_Kikakui, + .Meroitic_Cursive => .Meroitic_Cursive, + .Meroitic_Hieroglyphs => .Meroitic_Hieroglyphs, + .Miao => .Miao, + .Modi => .Modi, + .Mongolian => .Mongolian, + .Mro => .Mro, + .Multani => .Multani, + .Myanmar => .Myanmar, + .Nabataean => .Nabataean, + .Nag_Mundari => .Nag_Mundari, + .Nandinagari => .Nandinagari, + .New_Tai_Lue => .New_Tai_Lue, + .Newa => .Newa, + .Nko => .Nko, + .Nushu => .Nushu, + .Nyiakeng_Puachue_Hmong => .Nyiakeng_Puachue_Hmong, + .Ogham => .Ogham, + .Ol_Chiki => .Ol_Chiki, + .Ol_Onal => .Ol_Onal, + .Old_Hungarian => .Old_Hungarian, + .Old_Italic => .Old_Italic, + .Old_North_Arabian => .Old_North_Arabian, + .Old_Permic => .Old_Permic, + .Old_Persian => .Old_Persian, + .Old_Sogdian => .Old_Sogdian, + .Old_South_Arabian => .Old_South_Arabian, + .Old_Turkic => .Old_Turkic, + .Old_Uyghur => .Old_Uyghur, + .Oriya => .Oriya, + .Osage => .Osage, + .Osmanya => .Osmanya, + .Pahawh_Hmong => .Pahawh_Hmong, + .Palmyrene => .Palmyrene, + .Pau_Cin_Hau => .Pau_Cin_Hau, + .Phags_Pa => .Phags_Pa, + .Phoenician => .Phoenician, + .Psalter_Pahlavi => .Psalter_Pahlavi, + .Rejang => .Rejang, + .Runic => .Runic, + .Samaritan => .Samaritan, + .Saurashtra => .Saurashtra, + .Sharada => .Sharada, + .Shavian => .Shavian, + .Siddham => .Siddham, + .SignWriting => .SignWriting, + .Sinhala => .Sinhala, + .Sogdian => .Sogdian, + .Sora_Sompeng => .Sora_Sompeng, + .Soyombo => .Soyombo, + .Sundanese => .Sundanese, + .Sunuwar => .Sunuwar, + .Syloti_Nagri => .Syloti_Nagri, + .Syriac => .Syriac, + .Tagalog => .Tagalog, + .Tagbanwa => .Tagbanwa, + .Tai_Le => .Tai_Le, + .Tai_Tham => .Tai_Tham, + .Tai_Viet => .Tai_Viet, + .Takri => .Takri, + .Tamil => .Tamil, + .Tangsa => .Tangsa, + .Tangut => .Tangut, + .Telugu => .Telugu, + .Thaana => .Thaana, + .Thai => .Thai, + .Tibetan => .Tibetan, + .Tifinagh => .Tifinagh, + .Tirhuta => .Tirhuta, + .Todhri => .Todhri, + .Toto => .Toto, + .Tulu_Tigalari => .Tulu_Tigalari, + .Ugaritic => .Ugaritic, + .Vai => .Vai, + .Vithkuqi => .Vithkuqi, + .Wancho => .Wancho, + .Warang_Citi => .Warang_Citi, + .Yezidi => .Yezidi, + .Yi => .Yi, + .Zanabazar_Square => .Zanabazar_Square, + }; +} + +var graphemes: Graphemes = undefined; +var initializedGraphemes: bool = false; + +export fn grapheme_break( + state: *Graphemes.IterState, + cp1: u32, + cp2: u32 +) callconv(.c) bool { + // TODO: initialize once, not every get_script call + if (!initializedGraphemes) { + graphemes = Graphemes.init(std.heap.c_allocator) catch @panic("Failed to initialize graphemes"); + initializedGraphemes = true; + } + + return Graphemes.graphemeBreak(@as(u21, @intCast(cp1)), @as(u21, @intCast(cp2)), &graphemes, state); +} diff --git a/test/canvas.test.js b/test/canvas.test.js index 63a19dd13..4cb4760d7 100644 --- a/test/canvas.test.js +++ b/test/canvas.test.js @@ -15,7 +15,9 @@ const { createImageData, loadImage, Canvas, - ImageData + ImageData, + fonts, + FontFace } = require('../') function assertApprox(actual, expected, tol) { @@ -53,6 +55,35 @@ describe('Canvas', function () { assert.equal(canvas.height, 14) }); + it('canvas.fonts', function () { + // Minimal test to make sure nothing is thrown + const regular = new FontFace('Pfennig1', './examples/pfennigFont/Pfennig.ttf') + const bold = new FontFace('Pfennig2', './examples/pfennigFont/PfennigBold.ttf', {weight: 'bold'}); + // Test to multi byte file path support + const multi = new FontFace('Pfennig3', './examples/pfennigFont/pfennigMultiByte🚀.ttf'); + + fonts.add(regular); + fonts.add(bold); + fonts.add(multi); + + const canvas = createCanvas(1, 1); + const ctx = canvas.getContext('2d'); + ctx.font = '8px Pfennig1'; + ctx.fillText('Pfennig', 0, 0); + ctx.font = '8px Pfennig2'; + ctx.fillText('Pfennig', 0, 0); + ctx.font = '8px Pfennig3'; + ctx.fillText('Pfennig', 0, 0); + + assert.equal(regular.status, 'loaded'); + assert.equal(bold.status, 'loaded'); + assert.equal(multi.status, 'loaded'); + + fonts.delete(multi); + fonts.delete(regular); + fonts.delete(bold); + }); + it('color serialization', function () { const canvas = createCanvas(200, 200) const ctx = canvas.getContext('2d'); @@ -421,12 +452,15 @@ describe('Canvas', function () { assert.equal(canvas.width, 50) assert.equal(canvas.height, 70) + context.font = '20px arial' + assert.equal(context.font, '20px arial') canvas.width |= 0 assert.equal(context.lineWidth, 1) // #1095 assert.equal(context.globalAlpha, 1) // #1292 assert.equal(context.fillStyle, '#000000') assert.equal(context.strokeStyle, '#000000') + assert.equal(context.font, '10px sans-serif') assert.strictEqual(context.getImageData(0, 0, 1, 1).data.join(','), '0,0,0,0') }) @@ -482,6 +516,18 @@ describe('Canvas', function () { assert.equal('good', ctx.patternQuality) }) + it('Context2d#font=', function () { + const canvas = createCanvas(200, 200) + const ctx = canvas.getContext('2d') + + assert.equal(ctx.font, '10px sans-serif') + ctx.font = '15px Arial, sans-serif' + assert.equal(ctx.font, '15px Arial, sans-serif') + + ctx.font = 'Helvetica, sans' // invalid + assert.equal(ctx.font, '15px Arial, sans-serif') + }) + it('Context2d#lineWidth=', function () { const canvas = createCanvas(200, 200) const ctx = canvas.getContext('2d') @@ -561,6 +607,21 @@ describe('Canvas', function () { assert.ok(!ctx.isPointInPath(50, 120)) }) + it('Context2d#textAlign', function () { + const canvas = createCanvas(200, 200) + const ctx = canvas.getContext('2d') + + assert.equal('start', ctx.textAlign) + ctx.textAlign = 'center' + assert.equal('center', ctx.textAlign) + ctx.textAlign = 'right' + assert.equal('right', ctx.textAlign) + ctx.textAlign = 'end' + assert.equal('end', ctx.textAlign) + ctx.textAlign = 'fail' + assert.equal('end', ctx.textAlign) + }) + describe('#toBuffer', function () { it('Canvas#toBuffer()', function () { const buf = createCanvas(200, 200).toBuffer() @@ -963,6 +1024,127 @@ describe('Canvas', function () { }) }) + describe('Context2d#measureText()', function () { + const arimo = new FontFace('Arimo', path.join(__dirname, '/fixtures/Arimo-Regular.ttf')); + + before(function () { + fonts.add(arimo); + }); + + after(function () { + fonts.delete(arimo); + }); + + // TODO: all but 'works' can be updated to use exact assertions. Those + // tests were written before custom fonts and font selelection worked + // perfectly on all OSes. + + it('Context2d#measureText().width', function () { + const canvas = createCanvas(20, 20) + const ctx = canvas.getContext('2d') + + assert.ok(ctx.measureText('foo').width) + assert.ok(ctx.measureText('foo').width !== ctx.measureText('foobar').width) + assert.ok(ctx.measureText('foo').width !== ctx.measureText(' foo').width) + }) + + it('works', function () { + const canvas = createCanvas(20, 20) + const ctx = canvas.getContext('2d') + ctx.font = '20px Arimo' + + ctx.textBaseline = 'alphabetic' + let metrics = ctx.measureText('Alphabet') + assertApprox(metrics.alphabeticBaseline, 0, 0.1) + assertApprox(metrics.actualBoundingBoxAscent, 14.5, 0.1) + assertApprox(metrics.actualBoundingBoxDescent, 4.16, 0.1) + + ctx.textBaseline = 'bottom' + metrics = ctx.measureText('Alphabet') + assert.strictEqual(ctx.textBaseline, 'bottom') + assertApprox(metrics.alphabeticBaseline, 3.07, 0.1) + assertApprox(metrics.actualBoundingBoxAscent, 17.57, 0.1) + assertApprox(metrics.actualBoundingBoxDescent, 1.09, 0.1) + }) + + it('actualBoundingBox is correct for left, center and right alignment (#1909)', function () { + const canvas = createCanvas(0, 0) + const ctx = canvas.getContext('2d') + + ctx.font = '10px helvetica'; + // positive actualBoundingBoxLeft indicates a distance going left from the + // given alignment point. + + // positive actualBoundingBoxRight indicates a distance going right from + // the given alignment point. + + ctx.textAlign = 'left' + const lm = ctx.measureText('aaaa') + assertApprox(lm.actualBoundingBoxLeft, -1, 6) + assertApprox(lm.actualBoundingBoxRight, 21, 6) + + ctx.textAlign = 'center' + const cm = ctx.measureText('aaaa') + assertApprox(cm.actualBoundingBoxLeft, 9, 6) + assertApprox(cm.actualBoundingBoxRight, 11, 6) + + ctx.textAlign = 'right' + const rm = ctx.measureText('aaaa') + assertApprox(rm.actualBoundingBoxLeft, 19, 6) + assertApprox(rm.actualBoundingBoxRight, 1, 6) + }) + + it('resolves text alignment wrt Context2d#direction #2508', function () { + const canvas = createCanvas(0, 0) + const ctx = canvas.getContext('2d') + + ctx.textAlign = "left"; + const leftMetrics = ctx.measureText('hello'); + assert(leftMetrics.actualBoundingBoxLeft < leftMetrics.actualBoundingBoxRight, "leftMetrics.actualBoundingBoxLeft < leftMetrics.actualBoundingBoxRight"); + + ctx.textAlign = "right"; + const rightMetrics = ctx.measureText('hello'); + assert(rightMetrics.actualBoundingBoxLeft > rightMetrics.actualBoundingBoxRight, "metrics.actualBoundingBoxLeft > metrics.actualBoundingBoxRight"); + + ctx.textAlign = "start"; + + ctx.direction = "ltr"; + const ltrStartMetrics = ctx.measureText('hello'); + assert.deepStrictEqual(ltrStartMetrics, leftMetrics, "ltr start metrics should equal left metrics"); + + ctx.direction = "rtl"; + const rtlStartMetrics = ctx.measureText('hello'); + assert.deepStrictEqual(rtlStartMetrics, rightMetrics, "rtl start metrics should equal right metrics"); + + ctx.textAlign = "end"; + + ctx.direction = "ltr"; + const ltrEndMetrics = ctx.measureText('hello'); + assert.deepStrictEqual(ltrEndMetrics, rightMetrics, "ltr end metrics should equal right metrics"); + + ctx.direction = "rtl"; + const rtlEndMetrics = ctx.measureText('hello'); + assert.deepStrictEqual(rtlEndMetrics, leftMetrics, "rtl end metrics should equal left metrics"); + }) + }) + + it('Context2d#fillText()', function () { + [ + [['A', 10, 10], true], + [['A', 10, 10, undefined], true], + [['A', 10, 10, NaN], false] + ].forEach(([args, shouldDraw]) => { + const canvas = createCanvas(20, 20) + const ctx = canvas.getContext('2d') + + ctx.textBaseline = 'middle' + ctx.textAlign = 'center' + ctx.fillText(...args) + + assert.strictEqual(ctx.getImageData(0, 0, 20, 20).data.some(a => a), shouldDraw) + }) + }) + it('Context2d#currentTransform', function () { const canvas = createCanvas(20, 20) const ctx = canvas.getContext('2d') @@ -2451,6 +2633,9 @@ describe('Canvas', function () { ['shadowBlur', 5], ['shadowColor', '#ff0000'], ['globalCompositeOperation', 'copy'], + ['font', '25px serif'], + ['textAlign', 'center'], + ['textBaseline', 'bottom'], // Added vs. WPT ['imageSmoothingEnabled', false], // ['imageSmoothingQuality', ], // not supported by node-canvas, #2114 @@ -2458,7 +2643,10 @@ describe('Canvas', function () { // Non-standard properties: ['patternQuality', 'best'], // ['quality', 'best'], // doesn't do anything, TODO remove - ['antialias', 'gray'] + ['textDrawingMode', 'glyph'], + ['antialias', 'gray'], + ['lang', 'eu'], + ['direction', 'rtl'] ] for (const [k, v] of state) { @@ -2527,6 +2715,7 @@ describe('Canvas', function () { const canvas = createCanvas(20, 20, 'pdf') const ctx = canvas.getContext('2d') ctx.beginTag('Link', "uri='http://example.com'") + ctx.strokeText('hello', 0, 0) ctx.endTag('Link') const buf = canvas.toBuffer('application/pdf') assert.equal('PDF', buf.slice(1, 4).toString()) diff --git a/test/fixtures/Arimo-Regular-License.txt b/test/fixtures/Arimo-Regular-License.txt new file mode 100644 index 000000000..75b52484e --- /dev/null +++ b/test/fixtures/Arimo-Regular-License.txt @@ -0,0 +1,202 @@ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/test/fixtures/Arimo-Regular.ttf b/test/fixtures/Arimo-Regular.ttf new file mode 100644 index 0000000000000000000000000000000000000000..c14e0a4189276e79a930f9cd47d12dfd92f6958a GIT binary patch literal 317648 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%0A#8p1LSVVQ=oOhZ_vAuQ7nmT8Eq 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

NMVeNk@_|;6G z50Z!M;6st|PRt*PY&5=H;%@)qLeg2@fUq zQ$bIC#kg7NNeF9VJU`IK6ILhS5mJ^IpFDj$7g_O}=GwupX8qp^A4;39Y>#QK7*B~m zUnweq7vm}L+s0G0M~tV0U&my>WP4~lWqYjr4C6z>CdwuJF=dRzk8y*PXobi4$aUMq zHlD$vzsD4KCZe5IL!wpDdrUbe%9~(6YC!GR<0E?c5s83>T3Rqfegj>=S@d`KF8YA42HU;enc<>+rzf!aXW3N0F@Pc*# z|45X-L2HT+hp;DU2edX8Z6|mi7*^1B^x?MQH7g0;w$UiE_S?T#^tbo2*zUK!*PzsU zPyhDbL&HyReXn7u_x@gAs9_?jZcwya#v`l|M1MNYE3aP;epBtNup2Z?(eo=k$uXQH6cwZt3q_Gwx=7sG|&QpE#0- zlJ*XoF|+lxA=fwL=5F}HOVl-WUxlGV%N_Xa?e&F1zv$v@)a!HA3cr0%8^0Y15lndR zKsosq!~D>VWHLk$!Z41Yy5LmRp-t%I#F$me*8vwv^}T z>a8U`EA4w~A>WY2uuYLa3sksdhcAuU+BIeIcD-Zd*y0o&m?3*1tn;!X(y_&9a^D-W z+0N+ALZ(f#bdRl)OgtWtj;*r8sM(r=d3$7DbAiOwSJAfJSX>x2@iBvoO=oE zNg3&hV^mp}XKcFeh?Dz=9BJb&G}_KRQji=}nsDGOIJ+QkrNu-pg1ndX;UuScyhrX9 zeK3xAFNFG9V#N$p`)psLp%HM0nMpRj5|d>S((H-#^8ZMC4}hqyrhS}q?_EHwAjO7s zup!bxMS~SZR6s#cu^@BnbT(G%sewk8GtU$)$=0ou+ox~y!E#%n)Uit3e!hV#z|@*VXO!w zL`Q?r51kTQeGp+x2~Rrx(#*L*b-Ow>-kjpGY=;u0>uEh{ut$#`q+3+FX_z57$@XQ702c4XpmD##D8pzzn z7I;|9tjC5a1kX)RmDvsMt+BP#qjaI|3wTXEaT4?p--_m30Q`l4h@G}|1jc5;Xs3Ry zN=jPwcO1m z*IPtf?4pPZO%7M=8yW>%%vIgw00o3Y@+8@1~5a!8D_+x}3zUGgaBXg)Td&lbC%OR76 zc7vf=VPPH7zR_q9OGE=?6iVdoDig}r)?iirZuROdWL3Shdd(JHr|-C~uKZ(NuD$gW zldEfM*JNgRqYzfrK7Ck(c%-m*Z+?XJ{{8!Vf0h=p8h^jSlih%skp_j4RTaIfm>$)f zijY>ILXa914l5_5akPK;lB}rz{LG!^7FCxzSDmqPS&^CUfLWm)YooWL@>g><@87dk zyY=9Olcz9u0z3#y9;JdUhp|!qEb=HsvE6iV>p8E6=;btK}u9Nt!S;b$z)}6PH zXlJ$3a_0)kg#A7zdn>|PRxRSESm^Slk$eMR#m4Pk$BK@Bqx{yPvv-hXw^q6H`1YyW z?X8cncVh?hcH?wqZ@H3-St0rJ3Y1hrtiyR=j96D*5n_Ffd`M@`mg*7f*(_g=ST_#v zL4z}dDtra5^Tr>UX7Z?EU>^dgDo?QVTzBQa&93s>TK>-mb1RW!QH)3rn8(crGw9*( z2Bpjhg|ED7+4BBk-I_JbEr>0}TX?58Z{+HrHcUwvFiieyv(rW0!P}?PXX*Ya!niE- z<03=whucTA|GyYmHrVce8<;$x#Oa85xV!SUVJ3aSdy;WLp)3GsZ|Nr?u2%LS=A(z% zQ`nHLt#srClO{i0!5=JJ)rSRMyUlzyZscFzddzQiKHzyLpEY4sb}?x#zt{cX$-VsW zi0)WhF6(B(aX=JS-U_<7eD;u4|lGmq*hB=(F;NRwJE?oEMrEsCAB(7d<*3rVxC^M z2Z#SraCjU}Q#%|9+X^Q5{^2w{c@NNwsU7&)#Vfi$3`er^tM@HeKF^V^-rcxyxA9rj zzk6T0S3G(T3z~u;JQb@MEn*6wGa#P8704Pjupp9Yeq~E_<~W0E@Hpw|Rhw8o&)&3& ztv>lJI}Gs9<0Dh@8PCB+jS_cD{Q-@*BPGsaSojrT`taQm4-YM(&lx7i*%zSNWZ9Ii-+OC^*VjRG?Ha}dx zeCyG-pQC%=g#{xwpeD`858@<(C1}|yp(Yjk2M^c~?LU$kven0>-dSJz(}~=5>t`1% z-7-`1#D;CvSXpV{ zvS0Ext>MonUY~P;Rq3?P=d)?KE7{=AoiCr>zml0v+T5GJu^xVO(%k)Z0+}*)d&1Vw zLUJaKPbyn9F)w>As}*r{0~}UgU{2j&VGLK z3+*l0O8YXmc$xJo%kDiiFIVYauL1E$j%k`;I!G`d^;p(2K^uYoG;00H#7nR5AIvXX zUtj$i8LF-Roc*dR?<@5l&5xbD&(H2;qw#!%^J>G{RIJlS@$aA!{D*(pD~zw>^~O%6 z+bf&h@@y{1bG63Kc{<^md{#U=I`+i^+vvROzi zhrIFQ@?2{LHah0#?CjUPQ6qrsw081~(utANJmcDB%qyPUA+cF{H=Z24xpU`DE&W@y z#K3{VHJt&m?JibLO<^c<2~v=vUXnnaQ@1kIPB=lIU$`-ja$%NG7EGU=Qx27k%9xyb z;HLGZ64r6~6d)FJHCOmF%7i&I`RF-*WK~ASDrt|-w*-zdHR%O9tOC%WiuJ>xxnS>9 z(#+LiW@lv7u?wBzj&g9uN=_(;{WnhB`3Y*~VtM}4PfNCKlhzeUYjuN)bOTWmG8Q={ z=Nm!`09B;i^GNF;Hl3MB5R6br_Qo@DbUXWCm%72WAqgG%1!?PVy04@Lx~I5GwUMfL z^$#&K-(00ix6b^G_6e+$;%ikH_!Ym0ORYFbJ(2hPVfe>kKWqeqvF99d~d;F$tEiVIeBpa%>r zb`%+l|2)h#AMdg^V0H7g?A%ExJ)a$8XF42Kj&}TNVeq7urEO(P?JN262?E)XVR90- zk~1CP^7R8Ci@d7XK}KPII4kn=A(N&|Ieu7S9)ITEjZM^f^(1SZxER}%*ExU#U2#)9nb^$%~S)4wE^OhqyZCKQ-+>U;p zfh~jC1LYoNanfomR8+RgAQS7*yphVqPeHtjCLSBpU22Ap{d8<94h3{Al}Ec8>D@0N?#Ic=^-K?4UdJ}L7!pZ zS$gpts9E-S_|=%bw9eIj^yBG;k{(^k99qZ!ld;Y}Svwe95Q)&yoiFQob|#%kD!QTb3v87XME%o?e` zz`~FU8X#|iW;XG=C4BHHTfN4MbkB!*9-EG5i*p`a&O1Lsch6X|#xP5~MT~xClN`d) zpw|-0^)-FaO8Mcl`3RJsG|{PBn`)Gj1;zD)yc)D@I8|w}Cgi{EVhOhN^0!I2<%gSUbg_>2{%x zaShvipHJn}PaR_$UNZSml`X3QA6vL|Gy7btMRJ}deQa8i!b zsjcS^L`=ZL-jewYaIWdlEwop!loSWtNO7Q2p&=l_8KUG(#er&%Ou=t8 zH^B&81zo}pKp_*_!Ihixpi}tD(KUTetRZ(YiHezyWYAV!+?u6f6g} zUf82m6bZ1iQj=*}p`C(!#%2vw6Sexbb?#JLRAjl%s$dM8GFkeTQnl)OBT*~b#qkkY zV=YzAV|-{}Sh{xdrZ^@mm&&7ngs-nWYpve@X3ilEi1NmI;!HRa&_|3Iv=}sbak!1i z>XU%vu`}k({3NJ>uU*?9HGix*Uj7C}qn53xvDLz&OZf0|%9`WAm0-6MWoqM4O`zb= zbOy>By0pqER1?~^@N3h8r&un^*Nk1Y>fNr$ZnZ#KY}qfGji@b@Gr{Bnu^J)?lBDht ze?Q5CKz%^Nk)=gakjRaHzxwt!5h?XwOA-5zy}Wbx<JaALr5^9lW-^jBSDj^=Ii;!MuiI|XD>=&IbGgstjQ-he zI(8`Swe~0;^)_jP9k)qcSEOY0XQa*3%EFN8#FUMtYuGy8U$S7)5bQtm^{kj%@YGeS zilusa?9i%09+ap13(dgjA|Hb75!>U#X5@p~Ox6JAs6ti^F9HX_PUy!6@MndE1*~r# z>r`0459e{R?0WJTVJ~lh`BZPa;;_16tEf~dkcfZStrfq?|NN{VpLRqi^l}hmrsy(f%6$sv z$!Iq->_0G96+Vp{u{3RGS<#HQySSPBPJ55N?);i-6L*i9bFtNiX#O*gna`>n-uiZF zZT2(LCt4N#bf@rA>a6^*gOo?AKQbA+;>jEu3V4v6;6CD7Qr;D@M_VbRaGBLUyRzHM zZj8Eff)~r8&-kJ6jIJ&lxvz*%op*Ryzs)IVzB|U?fC-0? z=tvg?cMoW@en_c9!a}6W^3YG2notDNqJkeQ4vqL<-*(LLPfcuMA2oaQXMw?kd#(0n zLv(Y2ARNE%V2_UN_s#b5_M4Mmu-z$X<@Q>&-RJb{5$M*^zd_SR9<91ItS@6Ro6MQS zPgXCS6qV}g(#*xyF0@6LsH7nvE*_hz8YCEwJM9L_^h5Cv?+XbtNJ}-~U8$z+>+bK` zb7BIl+dUWNXN+51{jl}lXKy~sEaJbqBo20NJEY$75hfPQI96(gu9=OHGrb!2wSjys3NoFuDo7%7%=(YznuEV zdB4lzF^e{Qw(;3cGIo8Vz4t4Bdv=G-vpFG3L&*?L!w1oiB#mS z2$HvS@sX?Xb^%{L_~!WDZ=82k&*}Gc;esuQ(wxH&ZTRfu`t8;FPk5R#jOC+2S2Vax z^F&Bcat@Ni25G(RaM?mWM6qI(sK(;b`fiy%WB-vOmX?v-`Xsij6VU47H<#}@t+kt% zojrW$kdV2aoq`eF$@+HZvC~KNc5?Q#vURezbWe?5wk5B~AvQ9uM|9@~mQC$FrzZ#% zDh9d9BA|fMIUtQf(U1m%-qip>GeDN1_hLUO_1)aU`DOmHWUEG_G}|)z^Xn(eOp51S z9REZb9krxG{_P?Brsk<%tjnN*|9Q@$zPh3ND?AQ(Ym+^=a}2?q!+<0lQlh!?&JQ0* zc$3sbS}DFVL;vWR^e5J5GkcCp3Y8J%n8{ddrj{s&>$NC_8!`40*f^~)q~gw9d_0>& ze|E9Cd@P#_jx~_2Th9EJvoCnd<^0NWXbVR`2;NbF83+gh{2jM8i{WN=06Ail;!lX? z0)E5ikMNSZ>i1O!X|9)l-(UUShtc{Dy>t9~vK?n1S~ID&1#7?D(AbTQbEOe`yGGQuu{Bdb|w zY;pOSx%N0%$t2T{Z6dQwcJ{prgUgy-0mjxC%iIy`_J8Zxfw==c$NnX z^rN&GE)FI!5Hpp5sG|qtpaZLZE*J-mMlcL|xQZBtnTcQ+_*|V`0`|i5fZ19s;d?4C zW|tD;tFv3gM&Nsxo6yc_$T)asL)E*CFpEK4z~V^!8$W$DjuW%9CmeWu>B@7RDRSAp zJH=)%%*|OuX0W~2uIytjhBS8AcVH#RyR`(m8(AWtUP%fWn|y7ALaTojNfiVER?70x z;Sc$nxu*`dJ(;%ns}bks%}-d?XVbhvMG^AGyFV=Dx|1#V-)2oV^%<1*{imPqP1-V8 zJE7H*A%EdD9Cd3r5*kkBX3`=SD+Ze{3{Esym20)fTe>s}DqkTKs4Gp5##9>2hU0U= zR|svVqP;Yx?2%D<*Yfh_P{1q7A1yC$Ed9}+Ha(9e|&lQI->nB^>g;xs64f= zmN%EK8I`B@)$-<2ODbPB3^FcNv`^?W&~NfHl`q4nrFhZ)4gKeLMEmCWyr&tpua-AQ z{4SM$htJz-sC~7(IfH!?^s8u6qMwce&CeHt9^r2l?b8v!=X(0p^3=Xs-duJxDo^dJ z<;^AVYju7!fF8EN1%%X zs2#kZ^GJLijF)u|D&Z)P3d%~IM8(f+O`Py{_-mnqMjtb-pct9N10CQtVc4x8KWoiuS)WLl4T(^C(PiCZ2T{`IKB z2hl;vq-ZySrP3Et9jqf1dfNL6w%RKxb|6Xpn&@NI*R=`lF)*p$DElpqPHjGYv}e~Y zF(tBpHt!NZCfI914EDXpFMKq#x^S*gPMEJ z8x)z>s)3K4ewR23h7V&LW@jl|EM{#kood1G!}Xunz)hK%Y=Vv=!&PGgs=;WG+gIFb zRh~m>Gd1!_2Zy|jw4b6*9P7$7OJ3ZPcf8xF>6I1MwZq`3QmoL@9{qj=>!NI>lZ3>^ zdWWhv9g#taoSC@S84@m2ZY#R<(*haC*Gbdv{WEvk{Pq0jH#+`m`;3B|XZQK*&2ty5 znLnF_9$vrd;Jz)}wrQ4VKHo59ubpChxaiE?J7)@yTPto;2hCop3SxvffbYl8WZf{s87>SOKSfkUzVGeomtqW zOJu>+sk0-ybScct8lAh0mpxq} zCIth+fiwySF}cUS5}(K7bE9w&e6AjNmBA5I7JoM}lRacdQC=`ap@>G|b7MvHPoapO z)tqB(g(6CqA=m=wT~;!+xX`&FbPRS6v@RTAQcoB8^zP;6>1p2A!p^2!+u+DvPA!_6 z_N!)N6HfTn7w&E@jiOv#8@ai=yY+N;^T5F0C+vD9-2ZA2tP*ysTtohobv4uz6D9_P zXYc~lXYjDc?8SspJ({qNVxaH$Q&g#+5#{}S$HMixGA021kR_|JP^wys_HMHzp-ia> zJxEvXr+!W^5@3%r>K~lPRVb^f$(P=lq-nB~oJboX8!PcbvWnqGDeKMH^eHUE(yfh~ zp1u5Qbb97Ui{s7tlq+91Y1P2Nxs%I~zNV(19C`cswDzH$2J}gch=^0V?_BcPUPWm& zV{ETJmi+#)z3khbk!GXK&4-OnAECRYu)1{;qxuZ=^jsYc(rB%NL7E-FKsbH|4~H*O z?x=x%RD=7U?8t~`89j)0c>snGX_{OEQBKSS;WF>ZLpk>=01KdHE)FJ}#*jL`qz z=&N95{w(YG!CU3_)X$)nK#2T|e}Ov6qrPrFxbY?WQ!zMqG4(C5S4Dax50`SNlL%MT zdmnA&{|3cbGT+wAr=ERVml=YN{{iiEy?D7|X8LJWa)rE)TnT}=-h&w@om5!XRl%p? z)?m5-qw{r?UgyjmHnzqQ`|q|q`u)|DvHQ|$9ITl$kXcI3k53Gbud#F|{{eZue&av% zT9JZ$O&DoJ*(UjOwCha4nRurNvdaU4%-vtbJ(%g;`UuHYEi&bk3~4KQuOD5nndKDM zfZB<_Kl=1ugChb-VrhBu!pyvTN2la{(mHzMC(eDWdw7Jq^=lbjciwY?j_h1ogAgU5 z<9*MM9hbLtW_(Dn&(LUBSK3$5lXt-op<;xDJrCx7`dzWb^w#A-ViXc6;5zB6m!FvM=lU}xT*iELj@cDsG?63}Hg zhy-l4+c5EqrBA8^_s|C?`^uF!fLIRoT*@zGd8dS^83Ak46H+wSwpnvK`&RZDB?e+1 z0{c-lq-z6XVwmHiMY5S@E0$k3vw|Md!wIoyEfCJ?R;0zo;c^m-WTmc=nNp|p_nr5> z*VHYVuyCbT`|9P3(o?1@a%at}DY*ix_&7V;Gc4iKu{uETIP%B4SL|B71H(o0nkQ)9 zg%lh*$^PJ|#FZ)cs4d*m21_j&%j^)OjSb+T!P@)1d?o#-;es?hO7i53W{Vos^!2IN zu2HM0F^PHBipAJR%L_ME(e14{*P0PZa$eR10iE2nt6olr_SV)5{pL;UvX7S)=H2@{ z?eGrBzOb_2onR-rf5Z4UFxRm^Qk`X9NTA zN5_fi823pvX|R}IEG#UC1}>%^I4GK`p2@Brx?XFq#f0Ih<42Dl7~g;Nu!NZ8TKjD- zZ`gbBen$3<+FL)XF*+@wf9$}be%+(`CnwihwrxYfzV_C$(~38%U24qGCA9ee*5!Mv zpjUMNoq*X#oeLV&^zv~FamHHo&93KE@-GA>^{a!Wu(Mp~moYjgNF_}&3CC<)1!y@xl{tvOctCKd=n)%Zl&bvV@e?xeSY-~GIg6}27mi)}YTVZ5(t={! z1|F_;{p~*q-M@b7>}eB=v)4O3eF_6lc;uSZyI^xR2TO5amcs@y@A*ra#{APNRl0Z5 zWqkhR;-d#=A42>KYXdGsro!652nQ{{!Yt#ODROZFj;IRqik*%Pju~*0UxnYE{GT$D zF}IiLne6iXWottx_`PMjGdqN{hRnL;U9MoOUw!-FYbWP>1(*;6h~CZwwihv}LOlj^ zR{`*31;@i89;ydfdOCH)@TqoI7L6VFq|?lwwK>I8t?cVrG+X8F9`51V?fA&-?2(P) zo7+XUlsD;|q=(x64nh8Phqq}ws>AHa=6>kWw(PlbQwfD6!PQ(GU?_HhvYwx)gR>L5 zUbzoX#ySWUU6m^kKVYQNq~TND%w4>v+j+V;gawTan%8a6gvJrw+$Rn0HZN#wV3?ze zr`>cPCvzK{sUuun9UIkc?ph~(clN(p&%f#0 z*2=T1{qXepUd>%2yLb<1-rNh@ZZ1y)3`L)jvfR`|NO@uGVeQ9`Bp6i4>dZY`5LOVq zaqOPO($shUQsB_2-CrXv7)=Pf%3V@~Ld6gb$aodcLcGhMD$pIZ(p$rG=h}I-s8g@C z(=%=zXjQ%A`s)o{ z_>B8y?a{o*I9N2NZil9=T2Bzk=!|;riXNzY?XE?O%+0shERvlTOedI^5fxZq+~C=6 z;ZLXu8f@@BAG$IeQ5I5CQZi;t$ym*mojbL4@b2o#wD#t5@g9E`^2K z?MtI~Ie*5Me+%iV!#qo0_IAfq7LKk{jNWBq_x9mS~3L zCvYu{lYb0#dDmLRhRokdWE8Bq6_FKgK{ zFsQk6u-%-Zl5vskYi=+zU$n?H0(GW?rS(7^*bKqY#4-ocfW+03+_fNzd^AX`tMtfx zyz=-9kG(;UN6hPTVs#YD(p@eokzVuS6L&fm{4itL5}SjG8wV{rJdfpSqk-NVwT~~V z!_i9tP-L!{X~=(lPGG?<1+=UDNErQ(n!!Yq)^bT|W^n^KXziFyi}%Hho;V#p#?9QX z&toz!e(;Eq1NVJNxlC54?_MvBqu+bhN#nGim1i&$9Yz)sk>%c`)Gg<&5SB=i|M$1L&1SnsjH&_Ka0<%-T0af)C zvtOA6`Q?O9nmD$xpHuYV+tT9})!JlchLh@Lcv{VE6+9Oyz?Nw}4 zJ7{!xFX^`}+}GOGQjxdqV&7D4P@BoyrDOJ?F5Xd&x}n5rYx=o4wsDY_{eO*YnK!qp z)Wu@v%EfambcZZHjBMFiQnFLKb=RRi2XNG(^PASd)Yu9rrSN$=`QyB)mkv9^Em+bU zTn4pMbBT9mTo*h3I05+fPm&h!x_io#5sk^^OY-`UVS4!9nEnO)bhIHCk*aY&M`yLk zq#3FP9uNno%`YAuvU4_3Zxi(Lt$OhMOk{@#?2{dr59e&RufHwl(3d+{+XwIXlVOv_ zj@~hP?4)7*vC@4B|LOd%T$hzTc`B2BL+-Mr1^l1K$MzgOH*ny&qkE1$VU&uF6eXB7 zM3|5x_5v2h9j{GUQOKFC6=8x2e~c(EH(Y*jB&C^~PF(%si%mDq^(~1?89Qc}ZhzA7 z)Wn*I<6B{eU;tUiZ0fS1e<=N(iy5}(DcigTtb|^v2W*+mE}G$Sx;_Ipic`v zSVI@g&`JIEAU*3Pc!G@aCM@=5z!pm-0_Bf}0y?(m3bM)lkCXTIa3k#WjY-F!q zpY*Kf(8#{J`>39aR!%Fl4vXj--n_MqMMGzonx0b=mVer6ZE>{?W>_?aoUdiTI!N7d z$f3GK>$O8bz)36AhyCE3*J{+RLF?c+|D_94n|im42w%oyt5~e7GbUXr{_KF&mioi8 zODB|2M!$1tY!(_LZc(9a(!C0rnQ@zRPn)uArM}%WXO^vr_okM&Z|_vHTW#BIj{IQX z9?`nCGxWEzTr}QNe>#jznq+OVw8r#_X=5eJ=Y@xd`ZSB(u%cSEAiv<&0X%l+A-VeW z6N*5N7vZ&Sw9|B_d<^W=n>a&+yV7>PCB+AUNvGHK+EJRQcn%Y5VBQ0E~Zk) zB@d@GY2xeDv}x3oxRFlI&dytk=WeCnqxwvVYTDG%uSt{153R>vFY1)jD(sU64owP& z#(E8J*)qjz?a7mCy;54X9PZU8xv+_YeH-9rI~87^2+GKX`9$fgg0U#wpplM?tbDXE zsx4i9l5%mu?avR4&dSYp3H7vgsM#>EVctS1;xK=?^xJ3ZBb0wVDqa~PYv!A4enZ=4 zP)ksBJ)mW!I)(&qJBbZpagb6<_|VI6xBFjU2Y5I;jF*6|*h`f5 zhDtIPrG@oJJ@K?SFmURDvhu;EfxaH?Tc)S$ni)J`(lOkYs;-BIo1=GvVY_>mbypgr zB}(vY(h-wrXhMHm2K$1hVaBl3s$q{c#cdFg$a=ThXGtdpP0XLOOjoC|B^%c~sngDg zP5~2OyVZBGeNE=j^bD`KChT=&aV8fw}QLI20Dv#qi=O1=l_+aYEW7YB|UD&j$1Q)cTeSBHnQVb^*5Y%uq?IuF5 zAy_=HYxQTw2YW<4R%#D;KNO}(!O#L}FDv0g9NIW6`fgg^_yOBjAGvK&<@E9>HcQtY zedbL&njG3jXO=$jz@S~5xHv2#b~m3Uc-$XH%0`^%i{F@fzv zx(+L%XtH2zojs~xp@lUFKsr0{8;~Q$T9O<~?ED?(&zZGA?~Hi*<|@nO)1-z~s$95z z@2KjD_=Ugvi$6q@ebMB_iY9>;e$V%Sm6Fz$oc;RM4|?<0ysW|n4*pU@-D8$($sGTJ zSHVV@zYaXQd8rCfN!}8Dla);7LWwsMny$R(KhN~-Eu2a1*`W)`3eO$=EZRDD_xkxN zRit1(+>$>+o3W59m%-LyXwV0RVm*j$sbF_Qd~IyGCuDwp@tlg7TB%{V@8k4CKK%O4 zig;Qm<-(^3FMCZi4>60EkSk(^#j9LKxmW?A_Sf9r!M`X^$9jhaZ>yAsm5(@woB{mM zK|?0i-TdT($_ZFm-o1P1n67F+VTVzO4eW%`dSy#^_@OJ+R!@mYiL36mx$IMVswyYv zU!ST1+-~7!X^hcJRgy^;n3U=AX+)uBeddcpl^*Hjo`4Wa-Y%U}Jc^!S~rQ_@Bb z*gxgTn-g=_8!Y?dMs9{x8P2Rn+ZK+RZtXm#&w$z8_!D0G^;NxXzu$oSSTopIm5qpn zrbv2qv4p%AR#jnb&>LNGl*%W#~_qDEtZi~*%9WkO#>);^LN*Es{Pp+T`+BBFl z9^IK-!riVz!}Yn&M$Ld7#ZO+V`-^=e4dYfLS&ljoNr#FWus7U;eJv%Ou3+!t#dwZ} z^lQ3K-i1|Cb=g6~3Iu~ST%B$0Y6}fWD4jNz6|0QJ^_Q6xuqI3Rh1Ul#HlKpUf?G|6 zldK>;18<`sv&^B!QH@9RHMoaklbE9j7PDUTcE5c6`jsmWpPWZ>b`csu#~mjp3G+sH z&W|!CGaG=J9ZUE*7V-guI>W-3FJGR?EI#B>wMNnFqCK#n*VIl1Q5XInWmJKLnYLjM z`2iMw_UrdK3=84smS>I0f$~v6kibL&2k1jM9(9GX z>WhsrGY2UFChh(QLMW0*T$F_e+SoP;wbGczj}R!8^1PEjOubup_-V6EE_2f2XZJX~ zA&TYcPRO#()j;+~3Esz-A}P$7f*n- zmSj{GI>Eoq>U&Cp-u6OF-p!J!Fkn&}lt-j9H2ZME$LV?*D-fky4P(I6Z`~izLe(co zo%raWunOh+g>+578oRaM%PzXM9-h!QaNNMjG(&JFa3x6`>IAm+;e`{ipXHYYMpxYO z`)MF0UiM}2YS+UN31s6UuP#Z89zo6(h2M(Qpik?qspA!FlJf^L>yQga%$ zy6J1UsW!D0!E}HA*}~|?Dg{)R?RAOW+kyxOxVN(~UA`2BCEyLly^B25SkRBb9cX6t zELT+;Y~^3}O>8@_=j6HSy{&au_b`{3a%o?jUAuPL@tB!;m%k+7L+3?(+F1|r@tHM6 zx+uaAN5pohUvKV>awPx&h{?fegT{}~-YyUju}Pi4WbA?4TcEityuuY%?TVm%8Ey!= z1scyAy-aQ}U27KTt)on@>|3_9PmS42rn}jYq3^Jc0NRCGi!dni#FR7Ge=~hhjE(Y+zq>oT zEV`tmds*?K)rK8qWbg=2@t!u;pHRltxANiH)q-PUXgqmhBvBI6Kv9 zHU+CX8@G!mAjzDht16Vh5M?BRdQ!TjM9$W=J1ZS1EF>=n&Xnw`eIPrd8n83+B~+^> zOmVnV`PQyY|H&?N!7Bd?`(shwsVOs;XRgefd_J$(_%E7fy6>fPx{Fef_!IJD<-ekj zMD%eJeV7>xUrtyshCkxB)oyw0F5NHmQ{&3du%CEc)>d2J@P7nGV?RruA%ezC)01JC zn1A&X3!$9}VfvwiQ5CP`UcA#dt=GhHnVq8gBxepBRJTPPSI-z(Rt86O$Xvh`_H<0t z)D+%E?vXOgR5PK*kPbneYvPfJ7bd4n9^)1&6Y4M;60|n$Js3T0N&U0*eLQ(`&*)(L zsx}ra+$NivWMq`i88O&&Fq8elWZJbu+0X2p@)zCTqA^tPn{r^5YHoTqS(3)XoGUl% z+R3hdefRD?KYX1=n>&DxU<=KGfLCeh}~y15Zyy@Nbj3n?OZ&Vd4q1EdXMhZ zo<_ClA1D2!X=`z^tu#B_B%YsbXa?H*=Vzkz#1kIC-t zjp~1)D8o`7_w1-|QoASE%K|Gp1S@hMEmntOh7+JSAY#|p+G=2qgla79S~!(|F**P7 zqCfg4^XE$!9h1%_kArmklKJ!VR_Ql$^7uQLy8dLpKqY*J{YUqFqYd}Vnxd~WR%}K{ ze){#f$`oCi^bUXKi$5>~mHo%$q_h|=aOswmVtUzlxJyK`<$GrU?TUz}SoXkq&fGuoi&L==Cl{PavbXu6OfJ(km(&1_r z@9jhj5r(H+i66VXX#&%AucV}2%2z?&-hsW7V`GP+{5zE2ua-w#53xrZ)my{%_Z~8& zH&s&7Q(|J1dk1=Z2cd!=mbDn9z)FV!3P&;BrVaO4>WgJfwr@HtE;gk@kE+wG7mr@E zX^!;xA2zu9!dY|k+O@Yx8F%{N=JROF7j4bew*~I3+O&)Xv#$?3F?q(EO>4$1vYuwq zvqMU3+_0u{H@@bds^>Q!I5RHAqJ6u(xw95xVUkQ%C}GNWz(jBhsPuPS&(K$r$!|(0 zDZ@d* z8YtPU2;|FXlDJjT7;w%KMn0Ac$^oekzQU%!J5K(!eWg0|uz_YbJ#2vQ7b^$YkNWS$ zqX_Iryo#V)60ag4AbB_|DmzFGlbZ<*ch$Gsp8qPPl#QToqG5Avk8reKv*@w1NvLwM zEJt*xHh2*Y!ns7xESNmw!=(WA+m0$Z0YuI!L5)NaEsWkf7LVP~yHmC8Tmo5sQuU;< zNw<@fmbY(fSKp?bhI{2ieL)(QhbTf{i z@!p{PZngY-8*g`X^3W)%#Da##_DM2qydpM1YJk-5uo#Bg(DO3BHc|sVrg#kdAvN{w zTaZx12BE|kY6&tML%0=*DPnLZ zQSd!1oiKSC&`3BePkVRi8f|6O0#SS4ewV{v+BsQR53lXy>+8poy7lebtx14yKk&zQ zlBq7uJdn?Cb$@QB1&MTN)Tziy7Y{Z6u(2Qi;X1>y?)Cn;o zY(s)uwQAyByQ;l&&H53+!$$T=tl{rBtdWnElS_^I^xm@qMzLCX2I=0Lbr;85&pHsa z(p4Pqb?~<#Bahhsq&4_Uqx1P;%EP6+rLbnh_q2P&_fD3g1ei6-$WQVajrD&nC9TYt z68>u`7LzY;G%7_bK!gQLqj?LpyL^SV>tK-FMILQrAn3yD=4WKcqjhA9Gkgt-NjaeF z>#gzQ5usDSC&{Dv)FGG`6W&K!Qa0I?A_7g`Zh_RbS8hv1rVaE*8CzFF+qXzJPa3V8 zXWC%m#H$l0qF7Hw7JjZOLT3=thfag0{Qp>8h@k;-=a|3xTS%jVsX~0=mpVTA5N5oJ zI-_JvWtQ@xe7=c)1%!SRF|VsxzkGg+HTj>)&qMZEeR=7o;O8^L6pkr$*&f(|H9sS@3Y`i(@NjJIKdTI6~jJ!OqSC84}=N$MEEStc|)# z$_0Lf+5&mcf`DC)98f!(GIS2eZ!1PmFg9OB;k|jiZdZ(C;{ur{cPX1ElFx zRO%{SNzkp7=#_2*HR@FQGnB0@+6Fn%Z2x^K|Esi_<&EGe+)E~0RHsSfAB`2dAhP^;F!~jHE&7MbBh5ylbh(qR(_48z`UulkA*qk7#rid< zJBg;^U6v-KL+Nsb)F3qdvDxW3P93cjBRvVzj}_nnLH37RCuBjz`%Png^jl}GE>CIXuYE2hQ!40>)e&Re&)a8$2{aIs6T|l^VLsZa%t{)A zN*@2tpyTI*X1#cF{n zqiOP|`J_!0lBrJ9UrIm2m=UY=FS8Ud6$t=Uu6tYCSQFVo+gK(A0t%(^b|7NZSvt&H zOk;5@l&Fy>XbUwxytH<5YBuU1bn=5b`h(LWi*;ws#4Hu-#%Q8FOP@$1+1`&=ax+w# z#>O){b73OkJll7rY~EmpUnlh zHQh^Q8Q@yPu>Q5uGJdzWp_0^C%HeNPScdM5L}^MYSMp~XLr*jKuhE7|FXYdtiuSrp zZoC9Naq4-fs8PiKibRb7F_eOzK$tc}Cc%L|j3&qulA#>nw}a zqQ;rOv@2itCv3VE<@IAkag{%R#?P}ge^6;?5W2QvANQ;@9~Ip52u~RZ$TUrHfPM(HCslcr%=ZpPqTT+~ zkAyd_`tgH;dm4H%5&$HR>=UFsFdW@r5iO5nD-wA$ zPgMG<6^!_TB`OHMFr+^&xJ1SfwknCAk*H{8mSa3UohP7PFDkFtYoE!3I_Z|nv?s}R zY9uF&8AH@W+#*z7vRXGr?%Y|oUMfC-I&D8vM@;Ei-hC45uV!n5?e%0CUmsmwoH@E+6H%11PI4a!Ce{)urIu?NFeipWyM zB+6+g%M?Ih*rf0A&mZ`XG&YHvRNm^&g-er-ik44}fvbJXkER*8n!JEDy{{(YW6`_^Fc8BoGE`Z^U#206t;#^7_;9MB za?q4YQ)J+vuhJMSQ5g3$-a4YvxK&X^<>pCr36+&ah`gp#=#o z4ube;KXz8TNg-B^UMih25Abn_moxoU@giU1XOh^eHz@g@e33~AV19n$eOL?&7Bdvj zL(8vx#ym*KBH|~1@nFicOmcv+3hM53ydFjqDypMNgXGcT1X zskpk}g*;Zk@Sq8C@_eo+@hBYgfqc;j#o;s`I)GJSV+7%@I!jiVz~w=x5c?5cDAdox zd~^b{Vkv_9Y~2YqJ%RrtP5uLQ;yzMGFOtUg1^?+RXiCLV$xY@rnU6%2z1L9%|6IoB z4QAo2DOJMUruO5f1^+Y*r9xGr{pm44nkouFHze2#|uy|<=%@iM+YL;fi=>BkC_ z__s1WpdR$W?5KB_%e-wGYpO~``J6UH`YxQ;RXU(X;75k4pPrs+5TpaB3)xe9ovKum zQ(pW_9sTs6PFvPh(BBqJ;k$6%3n^S`Or(|R411`H1m=t@y^HL{k8o6pXU+5TSsdRa z-4E9tWufxmcVyKwjOKl|8JWh4N<|-Ne~4h4DEa~0EZ`_8^k$;ti9)=vvwpaGwh3@B z?VguBD}$}17L~W!{ys9 z7Gpzs{b{Ee6`I%!zIP%U%UX+RdZHT_BHa$1v?ga)2KW=T<&7|AQ-3GA*{*<86#zBEc~HAB;ek(LXV zTYM#=XoWbG7V>lXY?jpE15zpki;>9<=1Tqx{2~?dS)CHn%vHg1m_zG_gFsOJt+qMK zNd85kP=yZ!Ix6TnCu&rq$5t`c-r60g@wa^E%tx!}^&`=VJsTm`7K{^B_&X^}FwPJA zF|O2>b)ibg+j@m6-2({=&{L)BwLuhdZ$31EO=49=jfunfHo-el)`&A0R8Eu>w@ij| zkA(hS$qE2G0d^+V>K8sThwY;lmA9p4%{9^J-JtL47s`!}hY%2N`J8DCxzMS&Jm+0$ zrl%&FUu5K2d#_FfOS{jvXBp~9UDaE@=B#;&Oy{VdngB2{A# zWTJILN0n_e75yCM`=_$YdfH%{X`zYKv(CnqCdj}#&*rdGVjL)|4*R{!oq=`U;L%y^ zu%0WNy`{Adgg>)0DDkK)V#-^AeIUcQiqYp?k$pXhRg8d2HESJBU5eq;(er=KDh z?e{)P94vh#!_dpeSm)tPc20~UU7PQtoGwPmF#RAZYvuDMNk36RWoYU04w?{@d(Ye{ zCX@EiKRk6JTgL{AX7|6qB*7HPn zA6yy@`}AABb_8URkaoVLz@%|c76H$3jhzi7o?(>Lf zwnD#D!?+hu8Nu?T&A%E|sgOl9uezxNq{o7svAUHB(iN#Iihf}BHwdFE{K#N7hNV&k z7Q;&hv2jADHIVb+gyxMT~Em?Z#)DHJRe6R;erqzSxVVgW#i>%;0Es# z$oze4+c=4z%#|O!`W^2~KQRbfd;SBmQPEEf>%Ov|xtb0^x0Yxe9(Dk-jaDVDq;I8# z(H!2nlb2R3PNnq(8U$z;f|mdvGijyOu$qIrc1x@_tmYcc(Spo2ow~G|1XZMLrTmKt zC36PshSpOLyGc0b-YC<{OZnp7*5PwQJ4|o;Udam7%@9$r&pC~&TJYZcCCfoKre0o| zjoi;Ef3%(6;gk6(TR$jSj^z1Rn|=8(lbvPR+1LjoZb~y7oEscLWW-Q+h4ZAD>1pWF?UttKt~^aZ-WO+h<5bu+M<*6tdI=IQsQ!c|%Auva}HhZhh1C_2v}q zOYa12(i}-AfYYP%*a>Wrw5x+gH=_SluggMeaaKNA@}7*G^^ z7Jg@xZ4qc4T5j+lmQlbR@})m{?4?T#PeAb1d>%_In@YITNP(93_f^5=<-ahdIuRWb z`VDH{JTSChXkhc^gZd?m<-fT5=cIOP-?d}Mt{uWtbI{Zgbl0)0uMk0W@Q7>LBwcxw z@&HVe$G);DEVMzf11J`%76ZJ^gThb2T|;`Ez5>Jy#bzc!gOE^_3kC`>t3kxA_8*zlG%%n_UyHhRBHIRz9MagQ zm3cqQAw0Hqt2V(&0sgI9wQk*NQ94NejUQ)LWv#@~po^oG?jGP{$AxU=!+?hS#c*nL zJCtpF7-Xx2^AwBov`W9Fu}m70$~aYoD7mY`Nf=*Y@&_BrbhJ*B!0Qd91`x`=TpF)# z6xGP1iz5+RD=DUNuSV{j9aLifa7RROo8YAr-0f^#_0+D1WpV%xART0)c7U#(bdp&? zf!2M-8b~pW!al@TQcP*AvW+#sHJzK3J(W!4C-hYkv%}xz)rs1~j`Es8+QInx$FlF4 zgXSmtx+C-y{v+EqkN0sgwRj7IaM?!Q-K1aHC0f(=m9Ufj4cJM((Fi+fyn{PZYHKhy zI8tlmvmfI49@g{HC7`zu?(Q$>x)IH+DL$NoaYZXdej^C;&lGgQP8!4FJ=j4 zxniMd@@!oE^db)^e>Dt=>~8Fg=`qu1#Lk!zGef!1duCpAUS99K8PTwXlr2?K zO@^5k5R+&R$%za*P+|zksZST|K5W1^&lX?B?tC-L{PY3l5D)Xy&aU29Jl(G&|*05gBfYv=j25IWl4hbPH)FL$?)jA|( zcpHB&?~#F+*0suDHrfn_6-)yL3DVkXe{?nR_BQnMGXLR;O#*$Yq*$i*2pFE&D8R>j zxMkA15;Fi!PE=b%msK0q?-}^fZ>rhXi_}~qLgJcx1rRB5LEcUHKgOTL2YLeuhfJce%(CnV zZM=?V&=EWWZG8Qi<&rl`mb`hhKRbK>Zea8n)WKMjz0eh4ClD%8pq>BdiF@}Rd-<}s z*rblm7T3m~v3+S@)&%EpfHlPn-i)>|@rT0_00qrts7ad1B$F(YSti9Mi=oSGG}$6_ zKj;cBVh3TX*=Vd?oc;a4X!O4ge>Z#Y@9%$4PqNUp!BF1t`~7-`&;Ru|(2>5xE5JeB z477kc>F1G;-tc|A$w&Z(ZUh7d2JH3=4)y~N_V*835`cfCe7~R|zafDELH^pk$RrWS z{~D>eF|zb``chs(U%EnE25R@xPw6-MST~V=ve*7WLH>LLH6;(?`yZA5$oG$wN~o97 zd>?S!hwhKWKlfRek%v$+9`&h>6#>32>Gu_UMlJaAeXKkE*{3h?qxjfA=&HYeU?Bdi z@be4yN8$}hFuq}4!_>J^abL}6mRlo+H_(&~pWQyDcdvelgmwwP7Nzl+k9bGZ}!M#i2zaNs1-gUGY1*q7LF zot^yc%>11`lYd;YL|gB~#NGQ5{J@VP&SaMImttma#j^>9%a7udF!4zpo&^n2DmxD# zaR^!v0#BT3Y>|+Rj0}o@;m>p$sTB3rvU_Z>sOQ9|iI(o7rN#vDHTMuLF`!8AGk4{+ zUi(qiWc8IXn_4tEc(=-8+2-{t$1b#SU5Rs3d` zZssHN(BVZi){^3b%f^S?vD3q)AAI1mKd#+~tjG}@vKMV&_Z29j91VZa| z(bfww90%QTYP~EG({M@+?V;&lJFq zf5-RN%5nh9ZpC1|#+r{q(3=aIq_oIzKMBCN2c^lvp-}?^AqA1%gK*;F6Ezz#E+SY_ zHBwYHpeQcC-1>A>T52=DA9fd=-CG>BvTHR7{_biL35><6wy&9=s_Eie zrRSh_?O!Zex~xu}mUY8$_37R}rB2M@uG;emlZDY<~QwEi_aU2jbbm(u= z+XsgZpPo6>`~Q*l9&k|}&mZvZ^E`KFS2~JFIf{xP9kGH9dqcrWY}mnuf}&Wliya&G z8hZ?O1*0bRl447uvBVN%Nn-L#qTKR+XP@WrK+XSsKd%XQFwZ=@v$M0av$MN1y?UAI zJ6E0`-mhzLuO6X|Tec6~e|M}L6u4{bfb5R^7Qe^NOo)oDNcZ z22o8zHJ$?U`bqJ~;Kz0$vEQWj^)ryzFW%d$4ifuGE#ydZpAw1v(0>TYV9Z2DCSQFI zHUa%-)W;Q?#PNDjlVQKXM~^5I$eXYNC-SI&JK)y3A2?=kH-M$6qPM#n8Tg%qJkG_zy(BiE}9|$kQ$O90`B2(TX7+( zDDE-hrZ_PVgPKh;LYb46<{s@?Gd!YBzdn_#JL=81`QNFj_3OFR_4J7Go-n?04QIXN z&dLuTSHE7ZK)56fh7IMLcI;na+17*;Yidaa_qVg22U12?SXrj@iUaLFUmGOJF4=ru z{^*qQ;8h>oghk3C^bqbnp?@rSdliwo(r6hNM=%v?x{PXP67uPJI|4_~9%2@9)1Q>UQd2}Cr z@!Xjw%4aR%|3t1eeFwA)GTX;S9flF%$jD(yp^nlqWk%RY+jmZyGB-AsdFB1UOkMYd z{Iq2L)e)@hZ*J9?Yq{2~-mc1-HDl$chni%}9+^M({7sqD_Ih_reOEwyFf<*FYVBrA ziGV8V)fRkWxj^Q;cGFY(&EC0h$2z^+x%G^D@p#)E*CQ3i4qi0f+0iX&QoZ@1wL*L4 zE$p=^IC*B%v13!KS9SNQRK~^2A?my3n1h`}EQ1qwTMzXKkFHYA*7<>$RpKWsryWl={8KG8pz%YWi@(3};%Q2wXCOadMKE-|hel|{IEA}Yv6--fa# z)518R<{O#Pw|r%%GUY2Zs^?zU%ONnN>eQJPD>+1;ym79TYbDRBw*K|p>UlZV4yih2 zYz4=1(i5d!$mFaReUHTSPpY^cArdxz7WZYhaRHO(XNHw+bo$;8@e?}>_>z{rt1eZwOl?Y`IsLvj^-KKYM#EEo3^jFREkgHbq6i?Xp_j|el_uj(^>eeRDN+f3rx?Lk(RPNGov#y zd!7UV{SbL*F!H22!vR5DGGeg#>V3U^=tJ_3a74w*;4=1=PDRw>In7yye+-`hCN)=#Pv78Odwc5MJtumkq-LGyH9BS1iNL^b_wuew zS}afN)-^t^U(a6od5UvE`Q;h=*UEYhkZ42Ajb5Objtg8NV`YN`wTB~t}y;a?sI*)n0@HdfiD zMsU@-4eHhl?$>J4O3Do>)#%Y-?-lG{SC@^3Z4O9ZG+|U~EkwZ1Koy&LSEW!tl_I7h zXb@HnYBVZX8>lb@;X`3^P;3xzRxx>OW&!Cd6aD%OIsSQe-9 zRF6ftx$2qIDSt)gjPDL*jEkzjaj9otrJZl}q68NFHNR8Mwt`3NPT@8EE92kQE72m6 z#f)ttjKfjnv0%DnO!TGEwJ;rv(szS4{4dmxfyS=LVy@=!29MM^g=mf-t*n^by^{TW zeTOx6Pm6EAAUwR$uJv1TU5^JM(K!2?RT$vWXW59}Ya=5YojrEtJC`cEnk?pj9Yz4w zt1Pqa=$bVfI+k^_xAk&y>DVZFWa0>q-OIoFd_}Kn;X#!heX3TCYc_uJoY}SdgockB zJMHuky0rp|m9wB2UIt3c=q^$_35qRrd$~~)Gb67m9xcY=DYd1 zh~u@H;a}+IGNHjid$vc7?ClfT==&3yPiFeLx&M$?g4my~d+bQlG?O$=w_I1n?VXw?&Jj`>bYw zPgJ+$VTrv)zP&BAFdqgD@Kq?g5#j!WG+<9L%&s^Mc5;Z+C(?UT55%n>6xpJhPaV5) zpPJup8}CrZR4u0NAVjwBJIo80DNqh^x$dB^FUZ%iJj2&ll z`Gn8<_8#xN0b!LIvG#cnU;O^ym!~ftA)eMt{ta$r)Sq)dE@fGZWv1p?bh=Z&6l&|67^CgNe#3rbD?=d+h)97T$5JHH|SK5B6L zsq z1T`b}g1iyxh9*QP6*avUnFT^|k{pn>6q{+mj`GIRWtLQE+crEuyiMB*OUjhaSrd>S zn3E%?c{Fx+tLqXSUEm=98XfIY*Ui1L$E;az-poQ3)eC=@-JmqFI}~l>O&gO&at!Io zkLkcTf06iAD`!@n*(D;}(w>4Q{j16imf}pw+Ws#5{yY@W6x`VYn;JKao2e?7@`(Q5 zmh={n=032O{|_75r7mJqtvK+1+0GR7voDPMrlOl*cz|7`27T!|v#K0vCmOaP0TBtQ zk}W#QDgYbe8#NtyXY;7N4fJhW_xZZ*2+wAXjq40vzV->NW*u$VAf{TsI|t3LPGoM| zylCO7?F*#phyO}%-Dcd1>Y=4e+s1WsbW$X#L3sG`Cwp+>B4!T%gFn6Z#pUn5`T7Eu zYRET3QS=brpgAqj3a`LsZh$0P20H(O=UN~H2P-8l}g%pWd8vWnLLk-AHqHb`& zCrktnIgO?)hW>%e9+|P+K-o2`gFGX$ zYsLw(0oOYdT#C}*odsNE?-5xRTM%5=ME8||(**sD;8Jf5PWzHr9TYw<(;P|;r=X+l zv>-dSVU-a5k7OPlr7nL=?!E>OPs@@>Q-fk1go#fnl=EBN&s_g%Iye8?D<>+Y?bRhX z9kzKd|Kb+kcJVU*^d>*KcQXsUvE*vol&G9u&v?PiS8HZIV4-X#AK4~u0h4x7u)xsJ z2{F;Tc;5XV_@P7l_a9;{f4I*Y?~0C@5E{zA3r#&C7?1 z8^e}AXZn=eE6i(oir#MXWK(g z=kxO~IIuoA>YtZ&Ao$vn@BVSX<(wIIO!|&_XR+R=v3JD6%v1GK%-JjmzS~XpGbR;0 zUiAdxOBTp1OciNR0dRw0IRs7ZOUe@U7MnLwG15H2Kbuxz#KEyYysmICH}_D*KX2^3 z>w2WpxWRL0lwvu&pDioXKeH06H(+@u1ZLq#*uec>z1g;`Lf^jIj!)x@1}1@t3VNs$ zdZ?`M*_vcE1}-YKlRc!)N51|_YJ|jkr>`B~+wB(nOtLpW7f00at^6+LRs)7afmra7 z)7Pp1*+XV~M$F#Jwr^)!cg#!K+?dr`!A9_f6h@c6**|PCD9FbE^Aa+GX?iquA4Nw| zBWB51blE^{=jXFzRzo8MMBPT{*Bb1z+hAjL=@!luf*$eXVE$G56u0Rl3goDt2fFNN z_7}y9z`Vpx6mEyGCLZ#PT;}mmqYjiVe9lgRMg-!`lNxI@5}hM)2u-CGUMK@@+h8V# zA0S%M*rWlV4ldHfd28IpMicCT{X(3LQ@h-Mzq*>ReHky zbYONK@5ROokQZ}a90RyI(0 zN4B@Yngz`ze})Zq-3f62e`t{#tq4@^d$12RECLP>WcS zXo{S)BA#IVZWK1Fno~^sgzlvO@xg31x12MrenQfURxW*Og=aLGwXsfUt+~kqhBvI% zu;s}MF);^rFPfRLD`bDf7t0UlZ0h0KB0Sc=aV6IZ4&}S9$z3>W#g4!lZk+=Cr;o|5 z6JBrQuv)dI_HXM}!KUiM@iBcpTuf!kRJ1Gan$#mm$1m8)knr`SkbRnw~^&G+D;g9ym(uCjF`1( z;JB4FYV7)aly~)-ZQ8U*8CI{ykQJ?}wDvAu%|^qU zU_e26(Qa=a8&C4Ep);#VA*5Z=ZhYD6C@R?bQ=@#M8+p#jpHr*LuntG+e0D0IKYSC* zb`J9zuqK6F<^D_8H;|ha9KW=eo#*xb#8B>n;^CP^VmOM}u1kcco{BTj7NzRU6ID6c zgh+*H%bFeJ{C_*@_4W>I8R;_pK$dfGtGYWuAFsyK%;a)JXhz89C6b$RpBrMjn=w})%h-fLPko1S{C*Nj4ly8So6v1gR!Eg5e31iCh0+#BYGgh0LvB` z=HI-e>@5F0<6kyvPtv> z`>)5rJuBB&bFb;{X1a2E-x2q4x*P@1ogohMICk0k(9{NNniQ#JHYFnD3SXJOIL~I- z*K~BK;mG`bX7z2oHn4M>z9A7+^-~JmLnpRqn9)G?OH7QYF{@cf3`(Un?eA!tR;nfN zJ2@vc#ULeKM?SWfM$f8tUUoYjTl9}wFtTG*mw-AhP*hR&seS8>tt*vj+QYxc^hTj= zV4(^^jcu^wv4Sj1Z zGVM-i%x0Fza|jwiv4i2)Sv>Xkt=oT_%v?BURq_sO*>>RYrfs=wL5onI016z>2cBfX z%q@?n#GRDRKHx|8?`1KMSS*{sr{H9ff%)^>{7>weoMV0kThxH^N?5@hdFN@POph0jdN{d~@vRzZ3cEw~oaLd2m)f`{45B2jAcP7SmjK;ao!s;>@{- z^BAGFv@b7KRocFUHcGst2zy&d2t6X}SMpUK7^LtV=61%Zb7)8xr>~e>&IW!j&oM46 zyo=Kr{&4lF=NBEi)`{rm@b!}?*X+C3iR^BF?U8i$G7CRa$uBI-uhJ2I{?a*q`cx(V zIuZUhd942B7yN>2VEy`mE-dKJr>u6>poR^Ds&XRLuW*5KOxX?L3LrO>t%XwBK!M(+ z<)078)HX0dEP02{sN z@Z0ZFwLe+4$~X#M%ZWdJi-7LVwIag&9GKVt@~%4v)~_2-_4>i1-&P5z8|jbJGP+@f z&r4S`eg{*^L)RT0N;HTOY!VK6M&mJvzw2d={&`8)SjeTu_HRSN*~?8ob6g;NR689zH2wH(+w81X$!yd-K?AK*YiKF z^DCP-vEb{m^V+sF)W6C#S;DSYH7#{_qNUOvUa-iwc|bsO-$hKn zn-*72SPIZ;*pO;iD%jEdBka&yu>+lkJ!ps(1vi8{H$1}P>ZI_9sx#DJ`dHhmRj0Oh zn{oKzt5G+Xt(?s~yM7wvzq9+|g|k;Ky^-?j;W2%~5)G94Hu@x-8u(^PEH38R<-VDk zedZh9wXVNk{cmo3b0&N8zlSKFFwP-FD0%EWrbXJ}K}U7RjkV>ej7uCyUFEQ(gOpT)Z<^og`N_sTa+_tcVrGi&A228+#KvURr1 zvj7f<%3}ZP^fzDMMtNlm@5u#-iB54$u|)8W(2JiCS#R{l4|_9t!#uvV2J6|DZ>Yig zWUQObde`9V+OhsM*iIhCUiVs&z#I}*e!@<3hlCXg{MS!bfWsS=bkkH&D6QD$2*n%WHwVy^|A8s1~kMTI%{@hYIse zlMn}!y$I&n=XAGrsitcBOGguihKy=c91V9&EXTX6dZ)Id$e^u~>JCjfsw-9C&3DK@ z=ntVZI~YE`2sT^~SFmvnEEWeCU=oG|JLrpk^C)&P8e`(L8$^xhyUbd{T@K5@nA8xSxX8z9dn|O$(4CPbgbvT>*ep6_{MfZ7N z1^I&4*uXIAjnIE^`8!20Jf+#f!N=QyR5K3shcTlMoqWZ9h8_z7Yy#rW>j{!6pn6=;54vGzm z8CDm6u@_WA>rMJbIRd)_e}uw<0?Gh+h+1k=WSIG0Yf+Pz_-t*S&^$RJEIB1UuyaIU zZJ?s0l!sJH`y9+Vh%luKD9+y~GVCm3?ZB zW6k><+YW0kCl(Ck)menJ)VoLPG6l;*Enz(F<7BYf#ix( zUJ3ux=FRzs4heVENsAkbtq~5WCBhkXM)N|Ehx>&%l0q&noXN$gBJUH8a;l-o3ze0d zn##&u{^r(KywP|T@W<4&l+4$9yCX-YneQ!Gw`#MpaN65pJ^LlZQ*U6cQVPVXhZ#gc zC=*^Yqmxmty@`5x)87*^uK)k z=-WRbe%!?EetHGj@Afdgq6Ds;a_*}W<1-g6p0%=D#Ms17Op+&mHg`z(#PEa!<0fp1 z8)ApMD9t%&mmgifP;*j`LH(e1w}F}hG!_mBZApWX1?j`t4bpRBXOdic z!Ys2@3YPM}_Z;WE{(k@LX{WAycA#G0aS3_y3bRkbi9VnGHlBIh;U{-5SWvBdTjmWF zU*S!8Z*OxD9Z{GZ;JbdS|#=HgPJGVY@aNFrhl@O1r?p&T?N~J6&O~}`v zUq%r=AaB_76nbB~0weSXIq}~2GT?ZCIO2h2kuu`W7jBbN>h`PAy~`AbGjBR>NoB^B z`>Z_(%~`2)S=UC9&wyp`{>eE#_LF2Ao0+Cb9i#M{K5xr^%bNVoOR#%;_bnrZUjXzW zeUqF3D;-BKV!>|sdWBBIuY#yz&H#`^_KaBki>8pc?2c=n?zHw;t#wr<_wPM&%T~)m z-|6_)?N?}FzjA@ctI%FGd4cDIb}f_%8MQifO6bwQzhx0WWy-n?ZPh+;yVH>PAF(aP z$Rnw&fjE$d4gF9wMr~j=g;RDd=m!&)F>GMI7^AJb2Zap2e2zbw7RbzZd6&vbQl>dw zaiU?$3u{J1u1DyF0Vhla|4KW{RY3)~q$kh8h{Z}hmc~EzG?h(-%Ua>WO;~1fE(_$> zss&cDYvi}__^qB^tj{_A%r3f0?qq%Bk|op3J8UZ3#5HF!KQ_zNypR1}svI{d2T?~2 z4B(&82>2eU5#*OHLB8~9K5ryV{s{Gta?Hyh5W-wIxs(9LLdYlm5(T{qv77$C3Vhu z>muPw8y#2BmjN~JzttF-a-OjJ{M%gig4?0yaCTvCK}Yt2>cYwu*RIL$*9@+8Pp@L5 zT?{?qSkY00Qlnb(zlsg`Ry(HWn+^F&TEfY`81TI|eQG`0y+X`E%Fl1Hb~pbl#`~Q+ zsFm^vZw0M1-`Ip}BTOhgtATH+T)rX(ckyvQ3VF~xbFqB?#gzKP3hQz2&il#x3)-P{ zN!Jh#{2yWN2GE(#L{vbUm26&NrK#Qlu?C0`M;BE>B=uf{H~dCwLOa;5j`|;m%ve2R z`kqt#^}ML4CpnpM?dx^yx3C@iv|-!$pa_RsYZn@ySi4j8 z`VAWFWHGbvy*#+EvE+$+1BUGrV(FL z*j^KyHMX#%Yv{s>YQYzLP<~nPN~Ib!6(xSclEvtVJt9n*;*5q1Mi2 zvVVT@(u185!LGZ7Q17mBV1EG6hX9VDBvb{;H)vwaRJD&3oM@^LUHSJj9xQPupec zS_5mqUEI82$>MfvjyaNQ1tsy&o0L(^7nEpJsJ@nBD-WWfNK-NgQk8dqu}V8KYc?er zvt6I=X{92uEnNW)EjO_$$-95+8nTT|+2b;%gQ^+>_#h=0LkDX$TJQ+H0_7$5{A?w! zs?6Y3*PG9=70RQWoVUk7sEH4fXF@7zX$K7iOgf@uV(EOl+@Ehx*_4UgA=3&jnIAE% zQ!pP4G9vC6t>mb5r~vT}y=9EdeY>1HDB0Xz`s=7wEz143Zg|y)57H-~rVqTTPuNs& zk*`P8{B!J1?lh`P-ZO7r!F9@Af>+n$)w-w^nzB#Pktdj(;4B@f0>#RV?9~Tj?tY&) zb;gQ6^5*5T0#D}yhow&DQ|ZHkq(9%fna`n|X8c>+GVq~wW;C%z2r=BSG?X9_;(S(! z;$8d$p!$)Um+KHdP1}9mLeUq`XZMHc!Xy9wJ ziFq>Yrn|EGdL-}mBQGo`-?rs{v8ZjkSr-;Gb5dmeNZcH`z&utT;HR$G z2D$&t-(-%HGx-0W{ldzsbSY-YpF2rlGfznti%E?rGLmd<#3s8K2DoBm07*Ww@6&(E zY@J`a)HcVa9UT7t?$yhJf>le$%&)vQ=*NT8RvM%h6&KpWwPzSJ)~$gXQE>k`Vl~`% zK6)Xch5zKq@}0dacdnt@`9;B%>J&;dT5UFsHn7|ayQIZvdB-1(F;ZEr<@;Ll*QaOj zzmR7G93rY>x(g0{%K|salKGP&#z>REuK0*;RfA}a?Z2Yff`D zu_cC{en%7xCP$r8dGyy=nu)w9d-$mdlXG)ngxE$_-*x;}r|OTG)N}pQiNa1kdT7%A z1n#aGBWVAt)j!Fe{^; zi>*&s6W6MdRa;4gN3i@{rrhTXd9R(@=ayMvjBeACx!jZj)U_vyFN;OzS^V8E)&2u)tHl~C(Er5$&Ri~N#L6EuLU%_K zTZ-rlk(Cj1R%(PDK2C|_-`Rk~RY40D@~YyG7L8J!1XKt@o;}GuhDCZ7$WTo{by=>O zgm@XMU)h72$w;=S>mtQ3#7zFpO7kzvom)cnznR1bNp(=tc+7M;AVZkjw6Ck4#wAW` z1+##y5p!a5CvF;+y1mhq1)wK+qqCEwfBO}RaAB>2y;jd>t5tRwz5-DYe#}Skc0fk3 z-aw9BEXs&Cw1)v`<>w?a3j?}I@tD4IvT`S06zhkF<1KL5i3{+Q(3Dr@x z9+pieF$$EE;|&u3awR`bD{$u@t9X_w>*eqxD-WlL^j+lW$buO8P`gnkb1m4bLt_@k zbXd?xT1M=L*1-Zx-^uVAhoNACLmRDTIBNhn9eNtAe++n2g6nQEh(r-SF5_tUD_F7> zetHppmhN`}$Juk^cA@r7ZC83Aot5rPfmnDuHhM0;Bc_)V53+N+1wE1>pLI9AYeQ+Li%4)CeH0w3+2B;YDO(XZlX6~q6% z2!9n_Jp+XOJiQ`>pU>0Kb+4d=ChD+XWCf&WnUrDv*qXKl8{7M|jj1dL*z!5ew=c~}M-`G%`HKsWS zx=}rwK+^>2MlWn7!qlc5@}7nsO#hrM$bXwL^1!3Kd0*6O^YqgP+c%e>jgn!6t4m6; zrc`e=g&-wru|%W4jLgUwap1wpX`k1wdoJtp7iZ6=Zw{8ETysrJI!<ib{3lXv^w;hZAZ+Q630TcuB6r4EcdZ-4)4MvCRojm4c-u}1ZYo!2f!_+{{9;&HmB z(gl2AFw(JH1ra!HQv7huJAWEzDB_XQ{_r)Y(#;l4Ea6F8IKFh5sr@Hq9Qf6K%hqO{ zyh}RJD)#G>p0aN3SHFDmGqoQpDkIvD87vyU+>bZk(ZNA12<79g+IHCYfp+i6UYWa` zjTtiDrAo;rJ8xV3iWZyHR7}1%&{Pj~(kgL`=v_-iZF0!8O-D*JZ}yzjP0V}9dw#(R z6O&Jv5uHhAiylQBVc{}DkJ`Lv9NNKVQ)Nx`q<-<)=Ov6p_h_$h9KM-4HTA%A`+Hx= zovjVUF03bHX?;QX+MJxV)Q9;GP-|Iq;TM>U)h?uJ$zp_Nf6rXB5_5yzaq_8Q!g6%; z@lJ{pW}{tL9mn$SWHwHikTLGS(3AEjbK!_ho`t&s7I zyul!0I~tDojkKsGQMNM9tXgZZKl@y>(ned_$8;9Mu|%V?(5NYBR22w^>iNdNFEaTo z4KYj^nm_r=Uo`u1;)IN}18mXc6N4U}>S-|`HA10y20~-6*JtA_I5>sBF<;BCXug+U zKoQ|m9G>HbAZ=Z$jSy}Hs|LtlqPv9uBE>?#+%+es;G1IIK|ZR>pd4neMmaIWs;saO zUS$7Plr$VMcgUd>-((Q;)2xvw*p1e1 z4v636=SZHTC{S zt1@h!Iddjo@m^gB)Te*Ix*8ocMv35nzNo7Mg$4|mIaB{YjSIMbHrl>V|2=Dtw#$?$ zGZy=zb|wmR6%pdlbmpfN9`Md!qu=b=e6JepO6E88`{6koMyTL9%-43m1hP__M{BKuG?%a z^F}$-Axo~9D}AtAt-=?p_*f!76Xgqh>5#0Npwj`j!j}$7e`xr}t?~7~(qsXjj{SNo ze83|Fd=PI3W9T=!dB4m(Vn$x`TV#wKIopK z;UBlg2V6CgxAVwi_*Es<*o@iE(0^i3{PYHY3 zAzvyVK|ji;#fFN%g3VX)Mfr~`_*j~GvLT>z5ys2A@(sElst^~3@pe|F1NxWX4IgcV zOFZ5C&p2Rb@fa&?TGrbCn~f_>2(>5S9tP^HMQzplFf8Z*p8$AEL3=DEn~OETNqc0Y;?w3 z?Nm6zBhliVINC^1rQ#owvCIN|4m6M~s8aC{Nt*?HJ4UP(K1Sw24ga_`KH$(aYWWJX zR-oS6qTWMtIRT#ycUD7DzPijgq&WpZk5XGm5aIixhw&vO2~H+(>l)ts%di3E(R+B# z($(jMYgju`-*M~uK*wOJukc$$1N`S*eR^M=CV?0^R`g+{^%wYvMJV}L;hS`i)b?Px zB?||U;0)K=% z!0-m;bOPL0cSH9pJ%>Ec#YTyC0nq9_d)eLUz2on_w?bF=!S`&Ww^r|whF9ymsn!P# ze?VyXl62Zi>I|yV`Tl$J5zWyuGdvUW5~9(+OVme4VqAs1h-Euc^{P%Ij9<-y5csel z&Wiq2@kw4({M&4_Cq`*AJN50>{*@ZL(H+37uuM|uw-wQfeMqyM;dZF*nX zAZqK(k6YJuycg!#e?)6Ae~H$p^Gzg7e8jmx>*F-IUd4yD_yGv=RSVvgZ*;KW8yvLxQ!W3N1s_N6Zt&BPGC_}!GEu(4QQD-IPxKgiYxus> za@`)jSBNqis8&Mt{>l2q+n65piFLTj-ktRebQ&||Q(l#hP=RcINahvqU&rb5f~ z6WXHWdrs1Ql`iq#7_A)`6*}-0{gkD9u6u*_m&T}UODislVTJNmvOeNH%-!<8`V)Y6 z6ZkiDdjz~RMkVqK72zL}qf~sz@xOv!0w40B+Ak>II77$>_($>~eM9n5Xhyx4gj{u3 z;q0ZQd?Q@QC|}@9hZMM&2_N-d66Fhg$c^fpg#79KMEUi!a`J`zIe~rzVMIASG&m?B z`4ISIX9;{bEr$!Zicd~a6+cUNRKWd#Uqh5%Un}RZC|`AoQu#eKcx8bfjq$L_zyWVS za4DMsf_fos@+$qefcFyk*L7P|e2j-pqP;4lV?2yf z%f~Edg^%%|j-M=={RAI`y$O7@1Lc&(_^e}?1$xx+nN@g~#;4jZhS7kl{qj9KF7QQr z1zc_K_qrznu5OLI20rwe&ZxwYen9!K;v^g58~aY>kFJB@8_hzAhCslc%=v@xan^7n^o~YP5}eh|E%X>Yi{Rq{uMfBadFUkg z=>_@?#-6jOwI!k1O9+aUg&~8YxaqtYS;hwAP{Et`;b|X%5BmswoPal?@(quz%1`P* z^}Y`uFW}9g3x){Zlf_EBpCNct5}vJF@&P!g?@t^qj%_feeE@1S4%5y1Kp95kI1OGA zeEdrAraEmu5H*EWMXN9$lz5V@J#hwFocfMLS`ZYyr1riKPx}ab*hk>w1bhhZinSbM zQ*zY!^FXwrBs^R9;)C#^nhci2x5#Em_%Ig!!E(m2GVkCdgBm?aSZ|RGigK*r<3SI> ziKkBmfBZhc)8bF8d77raQ;esOQDbqqDx<{f;_!DoEe;nvEqp`xLPq_x@{`J<&prSj zPjKCA_(kW7K0r-0zSfY;m4s#!wD5D_{%wUjboPg!X#|DqO(%7egdrx?#sTM2lP9G8F>@w^1Qi0ATm zg6FkL^1K9G<+-{tfZVGan$Qb(h5VI-XMY54k^7SP7P&78x7Nw7)F)cK7M)xXp7?s; z4>n%=Lww$8h7#~1eqt}+qTpvR&3ff+z#|mzlv!GE8T||59 zi+RXsEaDv4J4Jn>NlKB#86RnI%>Ur1Dp{`xUr1Jc4S&4Q*|D%%e-#{}SrI+PPKg$j zglDroAA}G62!7f};KR_*#G#VqkF$o05vD7lIR?QND_w^oF$N`|$Au*Q_UFk+m|1yf*K{ z;c-z{qv{+^__amv&Iuyy;!q>bq^oO4b>0HDHV@6AVCBFs0Y~X30k0%xR5P$?w+2IHJbR7!Cu(QX#l zs)P`>DhYEmd%pyny^!qGTH)<@XP^2)7s9T@`mdg(S|=1t)%tlh)(QXhT#Wtycwgv% z|9CzOUikkif82+jlMWE}tgkT%^w%$0zSZ;bC7!GMNVKjYe)(YjMGvC&y9IYjKFu!n zv?gv6SlhckWbe}3@@?_<`_I$3QT;+YsrI&y5%%zZK8Iaw^&HWU zt?WPQd)R;UT&H?qOFkEN;D0idtpU)1*>c`oe3|0sVP*^eck3++bsEA{;c=(l=K zcBA$C72yl9wi3Ysvb?DVd6(!xcu`7V2#sik;fw$W)^;Fd01JDRpsz1Vc>g&xg!zUH+p`CQCs|M6VRZ2$3G z%y9qlT+DR;@m$P!C7%l$fW|Lm0Q8ITZ%x0|bD9CIpF;+$WVM_8qW&-bTYN`4IbhmUW-835|lJ{7H--I7R%wA$X!2BZMKE^p2qBQH-f_&BLTP0=@ z%Uiyf+hBnZ6_)ozZScNnwcXH;(23cHymA6gdkxW2PQ#g>Qiy*o!&TR#p3xZ%&i~YWJcvrhBbqvOJt@D~8A52fGv|4?+{FUn`qLH!AF%?06d ztPYllb7bnl688LqX}QW(sT`|(Mv1xl)ln03B}Uc@!J^SdIW4U=oJ66nbfP5sqXo-2 z%BRN<`#G>*PoI#$Qn970I=2c6>etg7S&m|HY^6oQCiCMh`pe>v>Z)t&hB<}s7J|~oC7-Ytg=54MdZ&$e$L|C06nj$ij&>IS za^aV4H zbL!M6e8j*SNQBh0oJW;*qqJ&X)~+2InGhCSCpajq&ft3fzO^hh!m!Uqdjm=Bs4fTH zts`JFvXkl9pQLm&=*=90o(J@Qbi_kIv8|qiPQ%dk(IKE`A)IO0cD8!D8c)IM2jXd} za95Bh2-k-0Mi!O05ztzOlNPA4_acH`0d$a1CbiHV25I$UKy~E+4HDbh1cfRoye+n~ z-2vY&I=)s>$77=nZ$M;XaWcye4@{vEoR-z%rRPCfkI>mm=B<#o4~k55>?UXHn=yO9p(|7{;zsKU_xa37bc zTDjBLg4VNo(Z{_*)ILs#4E6t7qk9YZAeJbikHxYbQ;X`NSK-vlD*UGIC&4#$NkctJ za9?cMpl_+CvE@LaTR-v~!(S24X(P6tSd_bo_j&?Oi{_H>Y#b3PayJNdpwe+eo8r`Z zNIC@E*t^))2)X`Y+F`3>HB+}D)GZU5Yy=#W%`h?9w1yBJ5fV~gThz{(O!5I2g5g&U zehMW)K6VovZVNpUK0*D2Xf0Qb4Y}}{a!vX|UzGVxRSir#B*$)!Ik}%D?cQy^v3s|2 zr#v&3<1daLJ*s=}JvE7)xabbe{q3~+@HYhaSwGwy#x#kd!ub1q?2+z$7IqyNg~F&z1Jt^Ad2Ow8eYqwO@&g@7^Pq zis%h`Zq(gi7v7au1c%ZlxYc_-;l#q#GX(OX*Cw+l|IHqig2~QcB5JaWWGhh*d-3esP5pVV`ip z+t+C4=)?fDcVY&K1vZ#Am9=4k9$B7UTGY&nT-p49TO~CA<#Pd_Pt7KKH(biP6 zd*fIZ+PsVT=ZUQ1iK`>O@5AbRiThval~TqUot9qcwyS=-1_eyNWPZDYdA)`h9V#wk zQ8jIpXjg;z>OZUHJXcNuGX_Bz=!$nRO-B?K*-0d>swN{AiF$U{K`_U`%Z|vk1X32E zq-1AY={~w$mUl?9#1xO}o?RPydwDh-F_u+W08+oac7tEq^6mz>@`mDjeamkLvVD;m z%>r6Qc$Xd)R?Dl*mNH-D9zkLhP`aPByjOG;9Q4g9tb;Q}QFM|f5rJ~)W&yeiho{wu zDNb6DUSwvXgT7iU6|&G|gq+pg-^ixLjTk-SbI`~ng~@fZhovuc{b_BwgI5KShr+Rj z?e_g@}UiE;g5VV>$GhRug5Mg-=Zvka_zzo9_ySIPM$rfYu}XFtsEP-`u4)--&WmN zaa!{Dl*YX#J_w4eC$%&m(oeabx8_Dw^9HFU+mt@V{Lg%I-pZ|+^WEY*cJ3V2p^95| z*YZBe9hR=nm}}pnZL8K1F;3NDOf`l>tH4=i#64bIW+B$r#I|~nE))#|FO7&Bg7oL& zCoF`{L?gtfhn@HVlVuO55RZ@umC*=dY-ci-PFmIYNB+;upC|Ji3nLf-rOn;HZ zsK@v5eT07U)GulDPIfjYhp*YSZpRk<@LnwH(^WgSehRs*!3P^(8b;%iP7|0;{dCEo z)sZrf7-?kt_=;N=YIroKa9a$2x`!EtXoI+V>4e}fw5rm=Wgv$Er7`d(xx#@aBVrOp zhzNv^6w*m3QyjVXe)p?9an*wf9Xs(qH}_wZ;4`IF*IxZQ`oy{VM$~d{+%@BQ>sKn<`Mo2zjvhN3-Kkl-*pAJ+I@`jsJKgHq2xG8U zv|9~FzqZX>U3@-yV;BnjYuIgFH})5O#PECTSx@V1^eRXllCrT%b zijvtri|6{p<%|#K^ZaQ3O%4D5?{iCjP$`?%oO=+#Ae3*R9GL7C{cfH}VCiv0SVUOC zq0lTbq6ltuOvu#NaO|aS47xD@ppLm z&)W0L7QE2VI`m&R;OSVwFG{{nF$9z87wZYJrWBdVTC+%vIfe!34~d+Zh{9)9@?~cg zu?cc$nsAFuM%4IGX_*aA6s$7@AIOVO3Pphh${7s#Yf!wpj>3Krt4G{5>AkOcui*`C z7rXy>bj)q#ty2xy7z-v+U|gMYM%HVuSUd+9KR&)a4>p@V)qbhZjf3rWch#?U@eOUV zdZecVUa0~DXg4n4z#_6zD-Nt)Zc)Gp zaD<0Lq_5ZUP1aiP!52X~Q#K>VA-c^>qe|PUms4OqCm23aDI}U4Are=nt z$}@NQ{iRc=%D4Mp+plxFauStJ;p-&t!oye(BD5>rNt0R4Pe=9cJ?c~B0?zETn`G04fA#|IysyZ?-zCwn8h)o^aZWPKw(B>y{9vehwf! zsoXnkeqVd%3EnwX^bULub|HH4$p-Sf37=hMJ;ayf&7Y|k0PzW>{|g@`9Yn%j=*o}# z6F=gdE)ORnA4bk;BvNlsuYUb{qZ$VJfmO$lQVaX7PboFY% zva46YggMFt$sI`KzVM&`oRT|#K9yzbJ6>lUN<0@C=OuTMa$cE0Dd%y!CSGz^6VgXx z!Q?=%51A_b_#Lj7e2y$cA@rQtPv!CH^tc+fR4d_i5ZH{Opdh1|T*+Kv7Q5dK@zOh- zKMx!y9J{NiRuL~^4mvH62AfNDJ2w*#>mqCBNL>z{Q;v6n2N+984~MfyM%=hD;t1Pu z;5Bopz+7G@fW;}wMBRLYhmb$G;vru1Nx1ZB7WpgOq@?M3>R*#FfW-48RTOm^W4C9Q z{{6eujEJbIqz#ITAJ8QtD5wsuZT_fSXI_RGz_)?b~?3z$LZ#)V<<)pA@6zW7~HFkk76jX>L3!$WR>5IA^sFY zaknUjnyO;y#jp)w>VRXr#1QXfd1=2ohzmD^MZHuOHGx?I9gi^Fr1*Ohdpaj3c2+Le z2?~nnG9W&FAP9~{!p9k~!(hxr8x#+BFA9(vg5(PN87X`;2zkm!rDQfbUa;mlYn@;S zK63Qvk+|g0u~X??y{o)Np9AsLHGwIxTWR(7pgw03J>fUxHHBU1NsQ&m&Pi}SZHIM6 zw@?kbXTli<=@hp~4dM=r$f_higl%OMdr?}-9&e=EsgtsoE#1x*?-;VfNv@inOrpgIn|_S`lh~CI-tzddnxTKP6R_`*LSkM2#d?P?kj{ur?e4 zeCF~m=I||Z*$v!alXYrPmo{+NhKuag?&^xmp^_f^1&(yZ8LKok`k`bavUWkFd~K2J zfs)o>IVa-QGvIkRI!ewgdO-Vc7H|8MyZ<(PT*k=q$E)1=@=d?7Kfin>y~)m^FlBv| zSp(X(jXOQ5S<^QC3norlVlbboxKL@s3+Ij5mCd&raRc0vMeI_hm(c2#*h|wGV_QHXJI~P$7{)uwRQQJDAJ;*XBN;7$@ z?SNWDTV|>SsfEsz`=h|$f2SfxunQ>eUn-bAZg~O0=g{33p`1XVybc6ngpamzO3suD z%?Bhe^N&*hBH;kGh?)aa3)4F+cr9WqP2#>q9-sF@VaN%q(uQOuY#*`hnA65`!+Z4V zw{rcKts{F6%&5H1|I2Op*X1(FS>>g2nTeezx12J?KBi?{t8THg6Z`c@h-+7C%EUcr z{%oGi8h}c5nt&B9rili*xe(pAUnzCp+#_|R3oLvteGfF~I*QAo(FBtn0VE78eeTvO zITb&NYM=0mZ9HthAj`LZ_d%niesj(Gb-C!fIpDF3`w9kj2<&FGr6X5DX;46k7)iAL zR@dE7|EZ~|xyOz{7}q7POJoIn#JY9s_+-44g+gBACG1W0`9OFl5%)>9dq1|*ZiDoJ zpiXbU5&@soa^+`g)1ob5w5c!=LYvh`kZwfH-lav%u4~>G(6d*7oQU_AI(O-8uZ793 zi9Pw2=)MYK5*HC7C|M;LfMT`fuEyC=T26hj@n8OH#`CRPo@bocv7PVUwPnj*$zxmL z0*1_?Ti(u}|8~ojZ+P!-Zrldr8bIjxVN=&2-vi?iCRG=tte5{$?Vo5$PD*MO(d_gV ze+{&!ic~Dc#iV@PH*Z{%|FI;AIbq4+jrK?DhANxnyR=S4xEl{O<0;H| z%})!Lx-meCrgR7;Lu?LO)mbX{@YQ?KIV%j`4|%FxctiP2Nf434ar7^d=)}D16dpoV zzykC|kVPY-dgR~;Msuw+nv&D>O}R1mA!VoOG5CDuZ&Tu=ppI96oRna`()C*Ko0q3j z`)@lO`}&XbqkC-I-BB*%%&Rq>yLP_$b0_w&$()VkyoahVhM+TCpk@Owp47#NudRo> zQ@97k$ClKLg3z#f)i@Uzh(JXOSni<)cY`KgT`DxcJk|SeFk*fzqwnx zfe8r%d&S!g8Z$O2Y2;X?ow9O5?^$-TO;+-v-8&a2FQ_QH^lvqEjFc`l3U1J-S9}Xx z)vun^wfm4EgE~_f@i?iPNH|2s0EHN`wJ`~9IK)vlAt5MIHR=t@@J@n6??BpJrD`;O zqg4imP$m)y3CHFx+c@_GiJ4UG#?5PYEXjt@dW>KBImTHTF^$63RBZf zKhDmt9qtdcWzSvK<1U6{yHdiF}_^iqTxqjHRH!o?s0mT?CIMqZ?#V zpcSchE=5+Mz^xQG7D$MrA^)hU9CZ-`{iiY#!5V-3`NG+U2gXd9Hs$~ueE7@LN2RaL zo#xJ&H(T0+cbn?EDvRYjFExMa$@N!7YQuGP*V^Z&{2h zinlK$$+4j_dtOl$l_FZy9MS7- zEJBX3z5BNuuvEjKK2gy>Tek4An>K;|Mupv!`O2TPSWkSfBC8t7!>6`}zLfcvEu6g@ zkPHu7Iqz-sB=ufgEG;=44#|oC3xcGUg?^&xYr* zcux1^4B{T1Wl|PVf2?Lba0M$V>RD(EjA3?R+n4?#nESZds{&cL`HA!k-?sh0P5X&c z5W2m8_o01fvl??}@>BB8kqQ|22cSpHf*1An2)8W^$% zl(rPzD-pKjJHsL(0y|0SA{zA|nUIsJ28A8!<0}o1h#c1~eoURN9^L&tV=DJr6jZnJ z=_7~FRE`W@)T?rgr+;^ku92g=#*eT2uuJzA5sgOshI*Q&?QhVpd#%MA)-9>kqksMV z(@dVBzM~sO#Pk3q&;!z52tTbkX@odn+)ExbI+VK8tUF1;-ieAY2EAoO;T^moN9L-p zy!}c(gts*9f(AShZ{eh!J{+~cs6*O1=m~X%;S;>2-@yYLY0C1a(|7OR{MnZMyC*-* zVc|(9QdvKKY4tv7l6lI`^(>UH8FykRzW_22)=Jokm?rytJfxL8sALSQ&^k_P3@Z`z zv?y?u(}1+-3kY2G7IBpd3)nMxBbMPtVaw9)7*r6$n8U9=KC*2R?>c-jo0Y?+^4hY! zyduqD2Fi6Rt<%b&aI{?2N=mDla7uQ z$y_t_)o;1lu}^tA>%ro>bQwPS%RzmvTuJO7@$~WUqlU`L^lbUT>-Oo+Hq+RE_Ko?K zj(piJzOgILUi^qk`k)fZNsAK=O~?#~jX;YER7Bq4Vv7n0BLb+3msPHU1pJ)2!*_F8>$aa1ApfKVZw9xIJ1N#Ri)S~3$+xSX8!-5kD( z)&HB7&dNWy>-_q|@}pHhPm~{+pB>CMOW#5wf>4Gp$`Jk`ysoVYWnXg{>3j3%6oN@g zNU<;!FSz3cN9_fg8MGhS5ibm;=vn5^^_L2&rLaz;Sa=Fv2xLFXXZ|0`z5}4DBm0}V z_Z5mD9Ys3A6RFaxh*T9&1f*F2rAkpO2#A7;SSTu}SfkhwODt$KMvWRvEHT{}V>Fs% zcT-K>B=UyuH}}4WPtETBzw8IX#IrOaqUjVi_=^1IYM-SvowKV zgzres0f?k?nD5;Cls;k06%unM8F+(%!8Gjg9X%lYoi9V^Zfv&OM(<7K; zW7v3Oc|@#{awfM)k?b%mahS3QBrtJ6gr8lcEq7R0zZ#JSn2bpThD;(b9BvEM=lI1t z$RDzLK;%Cx$wgTLtX)o-%&mU;TebhhPhha1bM5;0e}JsHBIWQR_UU%Eu4-Xy1d*!^gJ1a_l(yjF>+8 zo*2-dzyF^8YtH-UPrq~X%&EH=Fo4|WKEZ$rA(G_(Q$2;eBI{eXrjAR_JNC-1Lq$nx z@oi1X^Fy$Wx~fyk7y;88<-XuH(0TYFJ)7zG&7@=Rk>?NqHu;;2Ik6P@IqdgxtFR59ys`(!I?Aj4-fwHY(b?Yi_hI0LhckwYH-=G%AO)Z4e zML6;Z@_;n^U$;At8m+~rYk?~-X}NEm&?rz_U)#BwznBr>FXF8>+W5?Pv6u~ zYA{59T^ne4{*Y1et|N*9m8(EP9l>*g74Zqq;5QpRlcTMJ=LK*DKvM`nmP>vC^Ys#e z#-RxibPySo5Qa0c!ZfZXOm%>t!-QF+1fA92k66ws-@JQILQ+z^yv~luS2bG=GaNkF z%RDATD)Fr9T3(wQ5gJ(-Mn0=5Ua*Mghfhlk4j=H&vRZm7C`dY9DvM8;G?pyq`0+{M zk&(rr5Q*P`shk2pjx78tLezntnaG%;4krsVBtJnzLJScr9nCCcOmiZ4Rq!On>(<%P zKSg*fJq37@HbxbNFo-0-^h4tp#h@W^YG`&$i<%icQiMmu4tGfMGi+Xn6GE|BxQKn2 zfdGBJ7+#o%S#%dV3sqr7b8tfvs0*gwL>n zEm5#-K}kx66Siai#2>|JRe;FMq@qqoN((1?Z0wFo+Ptfi zlNU{$kZm;631y<~Bkk)7qQYZ~f|R{_Ia4N0mr8y)F@0h`w)`Up6cps<-neeGx^c9R z-LzC%4cBaPVpufxZdFxT6>9^Li4D97ch6vb3A_eLqjB)p&2dihf+eP_?bO?Uju!P3ld;goBs{5z)UR zf@&-HZ`n@uw_ah>1@>=AV`C?Fs+!{#v3+6tL%vaM#+7SV8zzy^-bU;XY24V{v=h6d zn(l4vj44zWtz_XKh_+k7cL0JlLwFY3j5msAFvQw!oxQuedq-#IBA_&*K*}d9YXS1I zGOk@ad;R(iqZ}39a1uE-t_i#0sZAHEJs0*^rJr+g;v9g$j+UUzeX6m)$&TcKyIIqx$g3;^JAe5=KR( z8JE~Jkk6`%#}(*s^6Z$*@~kX_yinh;uVkQ&m+Z`AR9}p7u_!ssc_xRooenD0BMY9Umh> z+u=W{t|pOd=tm@E&7a(#^e2hAat^6LQ@RAgObSyV5!``EW*QclUVrc;M1pXNR3VgN zOeNb)BThFkF>!J=N9Mqz%{k}AcPcoC^2AwrRe|FZvv70xa%Ot<92&!=yG$HrA7P&u zxpqw4;ub1l4=U%=7&b5Pc@Xn5jZ`3ua5AHo-MH!&KsjtQYN=*}q| zmykVs4sNN-DlW<93ol=}d}VvK@(mn=|FgTN+uI}qt0@-F+DW^z%WUVNPp$!n=6;>lcS^xW_=3^|3|$I1CxI5|vl zVR{Q=229V!N<~A!vmXSg`Qxs+V{uyS3e#rK5Kc?A`Y1t)b7jdR*G49aOJFC3yiTpy zQBo}hM+q*P0}KnJ>{3^$IB&_l!^ z8u^j?{9uqAJMR6K8f;5If4DEZd&5kF1*jyBg0%YECHeRyyN3FO1=vLwy1CaasjqYL z%^l_7Ya3<};)yZLl?kRCLirMPL^JN2J?m%v2x>HwE-^ zhWGQ?E*Q@%&fg7cYqJJacMSG}}5<8QJcgg`X;NN3B<8aE5dYk=JbuB2_jKqN#8&q9xg8&yXPb5`-Owx}n_63Lyt=ae9L+|=R|FZrBrk;tXvw00_6@SB3Z1>_P} zk1P&k;t0RV?knbA<>pA_+6pZx`%T|3eG9lwZjMZ@4RTPn03o?Fhx?Xq5ebADaR09a z@~w(MsyAXclOJW-_XaJo+5@kPa`4CHkK9pG)=`S;#qkJ-w+D>t{5c$UV#b zLWlaK*lBw>I}?*1u!v9$^pLB^9gCSm1*(B%fSo9TbqR`_fP9VJRb6(tTYL9TD0I^E z9ue6*z1U4!F16B67*#hmAj;IfUnES+E1UNF)hm7@;-?I@vj__^mq~et!C5ht#P;%) zE0>w9*x1p=$cyIi30&R(A9?LK(6QVgIekWc(M*t6Z~(}Q@Ql3vBy!o+H+Oz$o^8Bg z`Gyvd*UI%bj)>&789Oea2?%Lgg5AU=54(wQR&q&F!NoJ94(up?l8UD-T>Q$FcsGuR zIwY-^IFm20Bb=XniNd4IWWJlP=03ph6{M+eJS?rwq-h3w-oy&uf`C!lQFIuwL0!;!Txmw>tX8$01-iFb2H1!K*);!7`Q=g?O# zyw$z`jhq}Z;ljoJU2jfKNul?;yVkAiN=+dS`}eoCbtz?yBy-vR&eb1WpOD^lox60Q zYyXA3JQ9EQ;=ZnnIXU#`#rl&Io<3_YnNks+L6DhfkMEA3tk6r1DXVO^HA)$hDLG1plGf zSx=B)>TnT(@d;v0CC<#imeA0S-~wU~6Y}@!rgaCOEb=+u`q_;WE=AdE=A2mA^WzWH zPSvH2{9DzvZqtM@$FDQ5Q^|r6wH@5yNyS1>JMzBJ%#LT3b!3-eqhOBAaA1fz|Dd-( z<$XSAR%LdViFZ9?&AugKgs7;1%PwENy6})Ux4S=Xb{{&J@I4$S{^GB3>Q*L_KNd80 ztX{EcMHMU%QNBCFxs_%7WiOqcDQj#&lca7y!)zG6D8d-pR*=H$jdP zZ-NZBi=7c~aN&3J?|0IRr1jEH<*C`q0{y4Cbnx0Q$X4Z_Go|%?q)_<>47*#`lWoLN zIqE5*a$3o3&{!cRs=`FJOd-t^nys=k+n@8s$O}X#0pCYRInvDxk@ZFBB!;WO7vk`MnGZ84}-idG+fbbNMlt{W`yYgellZWGi zkF~sAZ+BYPk)8{jz?y{`lpmclFYR&*(Q!vYo_lLu#v05kvy(j_Z&_$Zs4q{wxG=;6{z3D?`YX_o@Eem*Ltg3_~Bfda{pFpu$5y1 z(xY!Z5`VsdH5}1mO#5S2exMJ&41%VKMkbswJ>N+lQ!9Q)MCYi;UG)69bFYw{%3pai zE@W#_(N^Uz%3+ePXfHByiB>F~#7Y3e?W@(zo8@o#&zCscYDa2xhj2Nl(n=*&`AifJ`D7g_i>p!Nu z`%ura{gx+&5}gl-l$g%>sEr$B@aCIWPS2Zfz2_v_KR(_1^%5CH_@&lwl6~a3G*7s^ ztCV5Jb$<&~C<23=6@gNj_;(;Ql?a4tp6k0Lq;aUDI~6{11b8+_j!PUpwQ2Bjon+mH z!7X&L%i={YRZj7KEzLgToY1D^Jc?jM_nigx2)&@91r>`;B$_b5j?6WArzSfh(M5?F zK0X=a3yTu7MtEnAFQgqIZf;>=Zf+sOlKq?g*YAmWW5(nqCg#P(`TO|zhq!3Bf4-;ZycpUk9BfI5WV~oi6?|{1ORt$d{s$R?1WcCID<}%ij%YI_ zST7|Z!1+c8Q4i$*xT&{JLg^G7We6^kw$SVR8|=t^JQ#7VaQydr9xO_*J6S?kN#fLeAae{A0znu)skP`Q^bhV;-+Z4=gi&%HjKz# zZVJet3*8qPPe9Bu%N>J^hXacId~_4vs0Q#z)A9l&A8& z+FqW83wf^SklfR&qC!gMs)gz!wJ;UBmPtPCha9{3oH1P8-Ps3ffZvPDP_08#vP$d# z7|$Q%qLZj{1Q5yFBB;d z8dDmXjhq>1nE?naQrKaYp$|84T)6?Lcn?)DSe>14X;EFnpZ!Q8jypImDb3W(nij%f zX765aXS*bK+OgCJtB-X&NE{6h7~lL3A%=?$izX(UpY zW*@upyYYmPm9JRPVqFDp2Mn!Bh2|f zujz%wKt!=im})JE@o;lrQk|n-fRHz&P%J6WIT5iW+;^>)XG7|t2o`2U{t1iF&w3%w zQyLBLssLuuPX9Je8d#rDE+{dy6e!frJU-CesmE*xJAW>g@m~6a z^;y?ZsYK$n9opg9>V}<5R%}{cBWki!r^jFA5 z%reo-(hP_eY5*8Hm^x&Hi$wtRhZwTcW^JAjZ0SUn^1PR4TBx(Dg!rnH|NRoJ+?|B% z|5Az6(`%})i;FfruK^yeny{SyXE4At7!hlLZ*I|IVc~JEE);bL7M7_%urQl2(!kE> zhzev^%XrTL76Z7$qsl#|uD=^I)M;K%r7l$rXD>rbEe4MVFtOH(^-?cqLM zuF#qKj-d$qIOMfKm&ubc`LAY6A}%$ow$E*=GsFUDIDi|TKDDI8+>nvqFv|olQ?ptj z;Z5qpu`RVUn-C%m=6u2(VzdX>A+<@tD z2#z}mapolGaGD!tp$dl6JUK{_7=e-rwhEgi#L+Uv*Qbt1m|90aj&o!4o10D?>9d`2(^m2|y{msgVa2rmUau^RexBZ~3ZNgoRrHW)<3eNml17Rk$W z2SHfEAq)2eJEcUe?*SA1FDHy^W~^x7{a&rSdgl7gIr}{jiq0S4`3GMgdylScS~z#@ z`~wFhpIDAd+P>{@aRL1U7t06qr=lE(s4(*ZNjVOD$m6e?p4wdh{NCmFZohX2sK|xC zV!x#8nIA_Cow-|CNzxZA;BDwx?iT%o3{(1$NGt*A@Dfj8&W^QAiV)3*0W5w{3Zs$* z>U02ptb@^>zotOs-0WOjgN`Z>@J{qMciYhA^TsTD+|v4B@sdx;$NRT#-nFM=OE+g? z7wYU1>b7!qnLG!wX@1Qw^^L~=CZ1#V~t=oIpRg##D`z4caK56Rj|?^%aC9NBk5NGSYvNTji^ zL;BP)ULHXw-9xzd83`%Rka^r!)Q;Ie|4NXjoun1s;R^&0qJ5w!~zKH+feii06^oq6p%9TBBOth+rz3Xq1Z$;u&L%|zz_?A;6nxZ8@B6G#Jlb90-LXT1B&VoBpd;&{z# zS1zBbp>3;Hr>8fsz|guFdI^RO)rw?83u1t&2tXAJrGRi_Rw0GJ?J%KW&tPm0Kbx9b z^pm46Obh_Q-qc*TM90qE)XLR$;==SYH*;$lH%w+_?p~U)aH6ZLm6^M(PFVt3_+Uf( z!&)+q3u)<6ws&zwX8sX2{?@j`R~MuuC%Xjb>j$_bC#Myx9&T&xZxi8f)*vy^Y5REY zoR8ae3?y{pg|4m(0G^egV*2nfp=gL9bJv3Y{6LC+e(ap6&O1BqEROSURp6=3O<}R# z(X|mV&s5=ldy=No`+Wb3tOd!&sMSp0tpk4FSO3*-b~k~jdy9L@|){{ap&^&0)md57Bk{&;>?f=STDGy74; z9d2FTp9sL}E(y5?*VQOgooMi0^@$c3oN%JS-^3#=FkMZK{DN}5aMfydpu9y-ry7Z;h_XKoACe6|p8Lw2<&yMM@^qB^&rG+EJG-zNVS%nE~#7k=PGenfv zPE~8khV~{7nGvFvoZQ^x7wgRqFF#VP7kkHBc_{?Fm=>rJL9~Kk(`42>=Fh7$l(4|BvC{UrLQXNWylZFKOlAk1k;VoeRpBPT1cj-S zvx#Qmrr;;Ha!HT^~81<1%zjxw#xRg53+%w5E|78v(0jLTf9 zFv7Ax8^MXA!KNGurS~q9db;*1F(3oprfZ4QY}Gu$z6jdpzzR$BiwFR@7JEIy-X}OYjW){n5!*D2;Hwh^UthxrSC9tqU{R`-ixir>@ zxnu<$%ovTpi~{K=lPQGQK0hYL1urz&zH9gW-+!64psGyo4G*&H<~yDqR<`qpyLvmV zTs0v@DvN$Cb4GkZ#^q7jcB5manI7EG)gzOHEUTE9X+(yeIZ8byB*}l+sJl(Oy}e3W zQn0X~(cOJpa)yNH4;VBfDLq>d2$Lj#09xS;=lDANioq29lCM7=61(^eUQNRkABJE^ z|Lu#JlcZ*cKjZ(0Bz%+#%nar%san2il}$*f^~@}|Lk&OM1?OH<~TOwkt zL#(T+M-8_f*}JP>G^e$<6HiV160w8u?9$V1|LcqPft%&p{8hDZW?P#kc>rYD3ITU2 zPn{-Su$v<0p84pAspzA_#NlC0O@%w3Jbn7Fm%Vdz6J{IO-hMQS*bqyC%C?%mIifWN zQ)gmophHDdAet9%wVI)R&w)kmpZ{pX!kNwQ?b-VVa*AhVPMN-8*S>w_d3lR1Ur|9e z-SohQk~V!#r(t^iPNWsbjGZ(#Ep=snPD=j7iEea@2;Y8Y_xA(8%N2o17WD@RJ#_$} z?4<}mh-kBC#AEcKW!;Rt@_qX>`rhw;zooclv%40{_F)K7@u8Pw0|{NYnDY#=^D_$%TfI0mDA|2uTZteN-Z`{iZ*;J>eDlCn zZQX;4=I|>HoiN@9X@C`>(V>(j@nc01B7;MCoxtK4w_}1=oJdf(aCoKG;eky}2`sNq z&_TpB!;I>Pp)?<70^tgx^1*!9YnL6{&duq~VK~H8Q8*WRmMVsj&^Od=iULQS4Z=u0P^4Z&Oox8Q5M0eGSt@NWY z@!F*g0N;mnbFCQ7Q7~OV+C$_Zj`DBpWd6d<5~7KPZaB!z$wL1r&PF!+DJDZrB6n_P zv*{+rAUD`nPnPjcVN%F53&0vtDQ_t|$aeRTBFot_gNS0^P+UNCi( z>$s&p0K*%pDK}*1sxV1b?@*u?fwwy=P(J`f<6~|Sz=FQS+bot!9?q)SzkgeI*RH9f z#!kA~Zkz1W>NDzH=P9LS({gjmXGoV9(XS7_{^p5;2Tly{m@s9+Xv-jLixE@CD=Qmn z>+6@-)&iXh>|YWBG~t8p4<{A8iR>yZx)uA`7>)=4BO@2C(W{b;A2Ti?Au%E>JRvfA zo>{g2s`3RZHcU)kOYT)x>y95E9Uc}nBO<^*dW~LjRYgv%mqBjgjCq(4*+k?ODHSrD znH>TZN!21ufjwmI2g90zg7nFOHu^&hJcqYRRw{G2S0<+xPS(yrK>LV@C>AvAru81# zhFCh5D6P_YgrlTkE%9e#AWH;ev|>cEo{d1HBO;WOk!oc*8BQ=&NVQC+Z6$=oWFxTg zpiE03F4)?{Xo=?!DmMbB)1^G6vZ5w7DLuai%P|a$9heYRyM~2*&K+M{NG?A82AgVx zM;;iAN1`Gt@$F|Kt{2DN$Lv00r<0Sbl4M#{Rmv2)ffT2cAbOQWmQTlb##kEsYRM37 z0++C8HXc-EGl+a+JSBijRqk<5v%;G*kh69lZz8-Y8L!Q#0{BmrOzjlr#uHT}w%UYr zUzn5EoR0?@&T~!zoTfrT86f4sovM2vIszXNB z`nml&KDL+WWBrg(E1M zWz=tS+ALJIibvErQeF^szH2DhSmLZ1+0)EC1`l;lVvn(Vk4w^PDqadcXTjhy*&O{M z&bLGzgU;Y_2D~i#64W8EXwgom#*=0cuf%zQbKbaJLpi&mv{^H%f)W4>`sj6Bo4*CABJ@Bu4I*PGXU}kaZUjdU_b^Mu0*xyM?M(tGbr5ofS+7&fnzM6=mFn2GZvGF-=5k8pD<=vAG;h8#af@+A${A8ldo zW==j-%E)&H?site*3^yr3-lHY!EytlKo9Yk>{j)R1)Do*+Df?_EXtk5LeewSv*n~& zo>i1TRZiy^@I_azT)K`3$ikZ1`by>Qn)+Uh8-U|=4U}wzQH6X>b%ZSlT41O!B`FYE z>%^jMaVC0Z)i*URpS5P$D&4sFabswCY|NM_@{m3t_J$W;JNA|_F{DonF1>c*EmGCA zMqhb&;F?uS+KstHQ-&2!tv9(A@N@Vj`!&*+FWfM>;7M7+@ zH}LZ<8s=?m?`SY2Wn{^`;j{9zE3^v>i2-QvRUGNHKm;Ur<+FSXcI3zu!u`r_G z9TL~&XMV+T_=Nb>3?bIv%~mmKlAG61;yEm6eA*kRoP?Uk zQ;#Dlc1cqBqL#F&BvzTZc+S9)WvSSxKuq-+rh?mvUF!rFqJ{}uxc7<@SY*Y|tk_r^ zS=I6SP%{%}YwN(2&~Y9f6*<{+JUqsQrUY7BJDZpdJx%T(-!L-G!6tZ!xpi(#cxXmO z$n1t?6(JcJq2V#P*5*TkZ5+}@uI~XLSpWoq`u`!DvwmHB&7{;xsWt87e#ggbf`sU9>3-O z7j2#71L{&xxTmMCjPOXr+Yds~OfT?3^dC;Nkl;*YYl+^HPJN33tT0g55|&*+enM`6 zQS>N1ssnIB!4r03EfAd%i1!tZ7u-(e7IKkha3knQE*{z0LAlw=t9aW3yvhV`BXc%F zq#8l%@>73k&$U@Q+uDp6Ix&fws7E08$fh7K?b$j-g;{K<9OX3-juR>6Sbmg+K$HP$ zty_>N!XX1wmz#i%K$Q93cDju+YMGoD9XUT{@#u+}Qwt9cw9@ajALBA_2I-iX!v!)3 ztIE^kQYKlCbRgNIB4R>BfKdmQk*8KNylblC+i$xDk7W}lavAh#f)N03N+qoepJN#i3Px)i$Mh#&2Hf84Zl^w2JD#~zu4p0g)0;%B6SQ#1H+87yIn+)=xiP{Fk2N=t>^|kc|NTmh_2IjijhB`*3UPgwtwuY8^ z159+Z2gv0{1N04La(!*NwhqxEbid>=nIip2kYR$46tOSRcKn$O9E`_7R^%}~v~>C6 z(LsKbJ-iA>IXZ;oNr#v@Hk1a%Dh3U5sTqdY*)#ANsw-jYFyi7?jUl&kR@e`HC##nt0CgZiEdc@y@1h1{219pdV7 zRaQ3R9SQO#wy{hKX(~w+-tq4%!dG?pYB0WP#*rxKbCd3nIpRP#1OI3)!J@k)Eb@R! zy8u5{TTLdB14ieeTmb9pAma*167gSnl}N{(^gFb8)~zu=k)1Q>{Z|*>qMvtMeDcv} z|KhHaI5K}=YpdS2PXFQ({io}+6Qk%3YMH-K@8U)M*GTxsf706~lCpvHbC5Rvk)|P< z%>riz)jg7gQv7D9!f)niB9!SjVS>Zn37IGM3VTw2@$NZNOV4M0-Mt5a#fv(xbRGQH zWO{}R_-;P^lN*>$ckugIO_B9qtfw=`R`v%!lCjEBU*LSAcQ5*+eatE!CfnmKEGRww z*C!l)GeheaDlf&M-@WGD~>B@I3 z$JXQrPIlzZ_4df+yOuX}S{#U^>7S#yGao*0NLsvoZl3Zh{zr3vyIG4Cl-eKPe(Ozh zcZ>D5z9Xy*?Tpr&`a4vuZdl|*H$S*w;cmL#Eymu!-WJ2|X2Bwp91&I=yv`i3i@$yU1_Z`pZURNr45uhsh(fR&&LCBQ{D7l%a4g z!Ji1uMR|vFA-{D#=~V8;zd&$82Riw1nY#Cz(1A`%jvkv|ld1pZdCC8HzTkg6ulf(q zKOqB9ruRS2FBj&;qCO#fA1ieC0{n7eUg5cJ-~E@Lqdn4JeV=1|)%<-I5ey|`>sPhC zy3>i6P5I2e4z~KfkMMNpOHWJs4hm0MC?I+1>4Ltt{%?I=dRo&555_h*1hF246S&(Ur4zdoamj?|tw5&G_p1 z+4J}R=J^7xbE@xu?0ffbo>xI|GWgLJtM3-Da~nLpSMJ)s-z}YI7#8|=XzkUK>x}J7 z7m}6!+gmc7aUZjUCUK8m9O;w3x3t!2jP#%1+N`A-NriJZl6p^W02sH?xyfwB8 z^Zk#v7$Y}OX7E0~SF_Z@mAv+=$VOgAt) z^Alh^hBBAKYGA#{dyPnL>#owp)5;7^Ti)4q>)x)7bg$SAGWq5N-tv5#w6N_jv15bZZhZ99i;h<_eg33zmIm;c3YdCCVy4r($RC2eSe;5 zcw-3|BFdOaw_|#9;i_oL@=%8MhXAoCEQpZ)%ZJouE&X@TAxc~3%{$KGqAKWqzm8X!mscTN7jyXNlA9;wZr?YlaHN*%8a2VS}=`2EdXKPV(JGEG_XIY z=!Moo=r8p3%>8_POiN7iw!0*lF8r4Ju=mkS15$ifdDidShYzLoYu9e2p_{4M)`t>8 z-coi_Gx9BznJAhrafVUe3O6tXYl#@m5@1X;(qoJ;lb|EP(#xB$nkm?6?yxx6ae_-TtFkmEaMd-CpezE~J4OgcQ*HgnCm8)V+D!NOGBn$(*!P z-<7mu5M5)6>cDk%+j2BHSTtNRKGWrv_)!6vC>FLw9bCPMEhr#Gg*lLg*+$N!_))=#9^^}*R>)*kHf2`J7O{8&!+;}nmOSwU4l#zp&3>nfw zhM=Ab9Pq#rv$8S!iy!_yl$B5tlbt|90YXqJYo}!{^$QH}_OgyrENwV(;8S{i&ps0L z*@eyxw#qN&9SI3OIOiw&@2ZD$k|GT{nz2rv?3~=_MjcO+Mcs(ZK3#}GE~wBu@f!Jh-4%6U z#Y}&p+t{X#b<`5rbO-P*0`SbQ2CEk9^r!H#P@Ow_6BAzN9KHMVx{dF*ao%n3ceMSY zJa_gC7pFYM#gPl^SO=(e^e$0gEc8*~d*LEtO~JHsgxoSe;Uop%LZxa{#RE_b%1w@3 zymDfkud_!=!w92qI}2C5xr8j4cW!CdSg`;D~{U-YDtncn)Zo7(OAKl-%1Zt{imuA-{3j>bFQQ}&PG6L(YM>}~(*KloZT-H_^(MGjd6ppqw zf?==+Eb@n?=3vZZ^@xlDpBvH75d_JSb#B~1gb&W0ci5t79Jh@0ETy-X(M{?B*H&Xh z4|p=iiBLvBSg7Jn)CNR>j2Pi_N9lwQxIxtD1G0kDen4%=0~f~~bz!j~p?;Azu3VwA zgZqrz!}NP)t}-jA!hh~M!yRU!;nrirFpC8!lmc#cP&H=3NrmgOf0CEuTB0VcU4S_P zf`UEVUx;iOv9P8VF+ye$-mZDkY#}&MuEK%R$|0Bo99&!sFis@tGy@ADpVKaqM&nhB zNG6H&b@Gk?{MrtPQwDukhq9bO47Ed5UHj$Xczaxy5P~y--Xvc1iYg4}GV!O^ND6}! z2OoANgy7JT?6?!HiMT;7maaz>ia>tsTFCB(pgZB_tfF*mr4Z8UW5+rka#^o@Q@3vm z{pW&@o8BTM_L%SPniZQ5orsOS{^p6zgrBzu_8`;JOZD{!EF%A?=$^X!Xjnr<&CHt0 z_W2EqTZv)98#^%d7|=fIE32kvGmC*0n46`fs!!STMU+(G6w~&X{Y^ZD`;}*r6qA;+ z3hmE_Qq(gCq%p{U6ZQj&R|L;4$LgI%&b$e9iDyRuo)9i*c!QU?=&~rV&<5D zV6paT{9HxiSed+DXMo4_^wDp#>Zj4|?Q;26!^k9wH_Sa(Bp(tt>4$=M%09qCYEQ3_ z;O|DE{PuonN?i#~F8vU%1gl;#vv&%xfcCVX{xs@4ywF}!_xvEq3Q(`~yYDbe8W|}0 zu&(ZfA(G?@CwsH+Mv;LeO_+fVkc%mH!svu6cZ5ZN6#1J@^p)?vC9Yu)f2SWxKf)-b z)zxey%{QfJ;^AZwKz7oPzWpwYxc&Z6TG(D!SK3|;_Rv>O)3i#EE(swO2|mL_pgOTj zFz)+h5H3Z+Sp$$T#+Fv4ud3bn3xsJ03=dcK!5PWOJRzF z*91mEb9338r^j0)2~YK>G?SQBB%(Qke)&hO^xczt*B`T9WIAVB>5Q0!sG?ZY&{6w# zw(d2r)6dH|JJi8;X-#P0DCxic{9NXsoZH8j^{(qHCkvkLsajT6H8?JA)M(!@6C-OR zSWO;1uQtEjFeGG-gWJG?4z~98IUwF_Fq>&0UTE++{hB8~O-Z%b zG9(E8qNlb3G#Ho#z1QUVA;8k$`gb&1Rfd0h*uT)%p}m1~P=1cTl)o~!047I@F5?u> z62b)T1Q5jN9d(MMz1gQaMml0j0AYAj7 z?%07!xA*u}4W#1p>kV(sB*#{+xZCg!vx%~1?@SdPB(OVCH^yC3t%`!%nL}sqzhZKM zj-zesS8M8|bd0C(NG~o>-SLzbYU{MGkQwwVrpmEN@rIJGg^s4UPffsmY7VX$Zg4aO zX^nzDU?N(-eegHCJE3iWSatwDQ;i#g;O^pyuRv4x3lo`a!nv%;_kY)} z8x-!W3dhVJf3oGhprGL)R<;f?jx(}kvglLIJ={oxge-b2Cq6#4oi^Ip?W-xF>7?6! z!#o6M5?ehzAvkj|*+|_yS5Fo~G-qjR7tLB&KuaW*vBM8@$_K>Db5l|V|4-j>7Nl|@ z$}aX*qhC@A)?>+%N+OA`Z@=pgl_F#eduB|6@WjYuIdKpYdXBXydwG8(f~8L_^uI-7 zSD~v?>+qD}+%Tl^kabUIRYe&z3<{)P(rNS*i35!y2U>an+XP#>DhC>bAf~TK4;MwIeGrdcd$N@C_7&-E9HcqQU ztFj&mG9ZI@)gIY5ZRvj#kN}t*f8z#grp3B?}_PC+d<2Fr*l1 z#18LL#SAO&lMb8>+;k8r9Sxlo{sL%!yk;O4yQ`VN0uA|07#*tYJN%!tP=>9QVYOj9fCAp8jXC5e~|B~30m*? zFU49?p~iiRuBaA5U|i4 zHgDf|{;aEOQe5254V)jI-9yup!JEHdZ?EYzsl!EF_>g|~+r^@y#^uU4vhdj9;)FkR2iNgq?W?uYo3*%}vbP`<0?BmYEqL)U7kZ)59eCwO zWg(f4m!K*tKahrS9WBE>A7* zo-7^d7Z~nQI;(7RlCisM&uNq0#V{-C<2D91`%QJp}HeJMH*Riw`EJikeMx; z+q>S6CVt)rCrw{IY?#w|!V{}w#Aq5laqT`?Q-3z!Zo8soaZ^nbnHCV?;pw@3L+=%S z7#TyH8kf^;1~9zSE&O|=Qno`P*V0FATIR;d0YrX)$dKLbSw5|Xwa3WO={b9kWd8GJ zp7V5vhMgx?!@Tp?;)WOOJIH9jDR~Ut0yiJ<5_N?#6Lv>YT@&P7-bbo51`{Xamf_Wf zd$NmNLWU0awos)pgv<{n@2;%fG9y$G%yJ}wZ|$Rip5>3SQJ7!?2Y9JEG^RtEh}g(k ze)N@`*`vpJhnmu!FMrjw9xT%tYWe$@H0P?)!o@_}(rN%2g#Tt@H$>Oa!CLmZV~cb{ z+sx@f(NCB1i-*pgV;`_#T5r9yzPCQ*P=-@Qr7ORqw=K55!aK6H!kpVw&$L=>yX@}&Gk-p`CH!qykH`miQ*In+gVAj}bzhawk zS2us7Z{Is)Fj!yTk~^s!3#2Fv^HY6FJh(eb+Yz}VqVw%Z-;$D;)EVynBS-b#_>OsXPl4AuyJ(13Jlp>j)tR5v)uwM62qSXLoWiD za11Jxe`GNd?BJ=ZEHI9S65z~VEv(VP%yF@IYKoJ)yLOhYg;{)fP*RG$m#b{@05h|3 zXE$zn@6x+lWJS(Sj>9Gn9XiYjg*zrVJ1J&G4rp%G#R9Hkt{`FIQ2j-$5pZ#YB8v0K zi1hxF+DG)S`aQ-ojfzuJ3XNu(?~>cD(`~1#8|Fp@1V%2Xe{q~-0f8pZjWc=4{YQ4s z4IBVxLs_ zRIiiaQA8OdLqN&e@AsO-EK9;fRZ;l?5HQbNxR*|FhS2vRh zgJyX}>o`y4jH@&#@`M1?kI?QRThXXgE#=uAgc+hVvDkm+Dh5XlcMUb}Crqno7ncwt zHEAAKtwQE*Vq#(}QYTDHCL(=)pfUMzOr9Wn0(3GGm9m5V0*QvAdCcA0^wP4E#`6qH zr%ov`m}hoe_Q74#N6HJ`tK$A%U!EHXGzvxRc^WOLpx1AgH6AmYXHYU_8b&cb zDKjv=YpuM{)yzg|0;>>5!HF@}veO_^Ayi2~Gyfc+`b>$9waqN9oS&36E85HVc?eer z44Q5e9TSqOH>?cx8=_x=m0=v-w*QXt=Osgr~oU2ZW5PG?5&Zy@9KWg>Y;U`7Q*6!S)`Y z@v~gxXIBq92M-SiJCCo_KUClccX#|oR&M9+ELg`>|Xy#_w$9nNHG61PO#O=Daf*PVuWXs->?8X;Xjf_ zdMEq3jgSR7JB3@sIaNhW^cx%G>FhMhJZ{+hh>8AVMh?d;r+{}Y*s!J)_?R(${+L;Z=p#;0`5!L9uRw02??Es64X+>mtTF3?L@alHV>HTrj%HDPvE zpfROff$M<^%eYIc?kx^7&9Zx$i!!nnEu0eKo<1?%y2l#-b`P0cyC^GT5pfSt3=0l* z9vVzDgNHhY1`l%%lugW7RGXQxXi$1ex@TypC;l45{(Dizu)rXNA}Da!u)ttv{0)lM zl7^7)1Q3~X>ILvL+lz!P%1EzUlo{wfDJ6YKW#BNEz+i=QFq>*&R%Wd@)v!P|G^5JEjumYd3kzvUe0LSJXG3~PvTb%a1ZeIPS3}8YQ_P>bsB68 zU>GouvCmZBpNo#kkBph2@WJQ#nVEW%C+lWrjTqtQ<>fa5pU2VNTCIKe1+sxz#K*6d zp1$WuuNL3N!>OQ}PLZZ!1)4JMS<}-a(iG_{#?xcsslm(w6&*XUEr0IB)*Z#gS&Rfg zl5xTdAc0iXEB30t4{R%_Ag^_AFQ{Pe&&&j&&CJA{xfpe8-ye+9h2GaNq;I8w*k6UGp>~V5G zK4pyzq3n=|7y;BtTF2PMT-hz1fP=jhze%Quf@4QjTBCIb>kxV^L_VXh=UUg?S(L%Z zC~G0;ijk&BSL~Mzc9EvIz9YcpguWldp)^munZ~da7{6saR?T~J?LG0M$1~O|McLQI z#9cKI3yT1zqcw)m6f&AWOEunc()gKvpcXp)(g*xUlt=V6cRNRScSk#S603$N8_=j8 z3xBJBRtqNlH+vOAg8mFh4FyOAA(5srs{)ruO^9Y=5>HP|7KluT#p+peMKC-PN=ti1 z5%T=7tn8{)-PotmOsZ;bc>m7O zpH*7|@(9Dbm-?dqhCDRpzof?8KhL!d@8X=!#WCXhjIp>UeNO~Dy|6gV zozndI2Q`sxS5L4*{X@2jD#oZLj8kC~i#Wo~)|$W@{4J2|bMBl-&hB{kZ7euIjRU9l|OU?O@27rUm+@&nPLGl;?3<5Oc+&acnpn%X`89( zj+ry@+GyDKzQYu-Bf{uq!Y*EDM1bOH0nk-taxqv?hFdI2nQ$>>sEtil0)6n9TpE{Q zU~$eu&)PIFEt(X4qkJ;KLD!75(NfZSFeWzLmNY5bVy@OdxL1}OR8SU09l2YI?7@&( zS`#s2J!XWML%VbshoGY2FFSK1BT4g#S*fty4fq$5VhFDJ-*>ke8&BHm!Vn}hT)7W5!M;Cw_fJxc* z3#Kph;@Ey#86`20*gt(BvG48%m2}{xa=bKvWw5f>#5Dm(DgHxJej01?ARnE2RM`95 zDeks%1os}+Rq&v&_c3pcNe+X`^6|ZjKuP{7$&IIe_=QmXnX8iAXZf_k!2SVF$evMP z(WLcMQV#9rVoJlzT)DlTwC9hQ*HZ} z?oLQA&YzaB>eg=hgDD|IkNZIB^ZLQ!ug<)CA>v3F@`Bz)Y|&8$4Hxs$8kIF%10E^R zrM*Ns)|WZMob_uk+XCpQBI=QSt^-{<5#sk9 z{eeCuvdE+R&L3zw&Mi&av)rNkTl!-R=b&lVK+|fVKmGnuc5C3~_dgK?RjF_YB*t>6CQtZ?=kw_FLWY#5AnrP5Z@Hib?zkBefD)>5ENejdLjt$gO4zr zE@=HD3ZEw2Io;spGe0&G)G z3w!{FMmTS%;LdnlTnGK;{Ci)_YgPWt$^QMW(&iMO_y#>pf1B{ZR5JeE<@w!5l}~de z+X0mVhxdOUcg7^u$K9vzcj;oxk1zAE^>W_Fgy9MQM+*g&05D;OWud1iZ@@`nqHocg zkH0xzGbS|A!o*ZM_?)fW)L`(8xf@0ddi&=X&rxmz!!L|79W`yCN3azq`AE6az`)(b zZfXRHp{J6ooW>>DI#TNC?J#3^J*Thy>+lz=0eKkk`518EG7C5$WMI{0RZzqOpr$HQ zvdy9W6+NS9mthX)xQ(FwQ|nLDFV5v41&!Gr)A6byh$f zlk2P?RB%i_`+d{%zvp69?~A{0V($aIJ19aG2e!t<2zUq=_s^9pW%bGit_FLEhZ33a zkcl!pwpYKx?fr-PJ=MECxMo>kokl_}8^#uJWMG|vU}aK-VC@3G1jgC&AtJa72XnGB zP7NAdF^e8OE%o=fa6tL%VHd{>>{WH3qZ+pwW;S9x|;h3t#fhmWqvs;W(A6-5q# zk7}3}KJsA`Iekuv?{!oK;_rF}XPpp)>uz?fyvIShPV%Iu^$MqI=twSSRZ)5XTjsW&5VT@M9qVqLXb< zx>Q%kDQz8E*#zY}%md}5*4sM41m(-M_rrFD)-#9zt`q@SAKx5!4&{T0;7SpJl^>QE zqFl%8Q4e6t`hK64>o_sW*J{fF16se1#iD$j?*4tOT*t>yzCl+WEIEt&bvz2?8+GO0 zl0KB{=s(Jz(3X=_5f%l5^c>~bVzB-cN*YlSwqh^F%#bT$sTLtenAqfj@tB>i`{pDq4#qhi-UZi!04m&+H^A8k#FHvm=I zw89`dMJLU5*reyQLq8-j{oA+LFh55-_E*H{BwakEJbx{3Bb#_nln+vv!NtTDheRg2 zLP~3Sq*c&mp!z3m(NL`rRpOH&N{pf3iLQft^G6yZa&fSze4MypP{qOpP^ANQo?ic? zH0lsnlkwtbY(vkE2iiT|G!y- zF_#0ROk&n^RKR1qF8t@-I4{0>Tv&J<;d`tRFC>UM7?4+h%t~JTfKRC&r7v6E|+j5Oc!*FT`=EzrA+e*USq~TBRF848UH8lxva3~077W@`|fmX=1 z;6=6!#zkFtUA)D4k)`69c`qg=)Y#kGP&0n>(bJd}zcxmy<}Gp#LKA>3`vGG&paxKE zI;njZ%~D2SAdC$F^$msDAeZ+U;YJ|ul4!>bzbQt~d55?_rQbtz_QBGw>(7#TaDuVr zRcYhr}=LFw6;g70RQzbF@(Hl zwRQgb4KW2nB8`f3$fU}u>MFWiz*@Ee2vC2}`3{74F@m9})CzSVPY=_m6Ct!=uoTTK z@$(ydY0QzC*RH)d_UHmnqWMHDvTWyiG&5n4yI!RZ+4WT0rf0?5hsJL{XV?D+_tXe0 zG_VBgbqUZKdx$?9(6zvh>vSzd_w{*4paV|Zic^I}GQr)88>l~DdHmjt>Rks{uCy!9 zA}(Yk8TG&^NaOdocTPF!B9SCtbI03zllbnfPw47~_v)_z14JXl<76nVX67E+Gkk3) zNAb|@{dKt;p_|x|$bOyt*3#o|_?)nt-n?f0#kFB$gHDRQKEq5*O!eJnwXn5stqw3A z?nIq;I;O0hFkx2Zj&J5HTRE~YKLjj9h7dNePrO2QM+mgW^v#RXoQU&=bZl;V(1KsCtvqXnnZ{DIDjI?1@wuQebj21 zysY_Fi4krb3J*g=u|aFr%mzLNnNJxPgK0bEO0;#Q)aV!nB23%cCw%%4ZsHH^nZ-FZ zgfBxkaV8T-Z)3M3;l|Z9yLXGeXtF;|a|fc(v;Aqfi%1T_XHSV)nrmbe7c-+hKr;drXl?N4!PXC!U2$v% zhStQ4a<%68B#If{ui3rlPgDG^L8E(AiKy>1o5!FzJ~Z`LK%6l|YJjW_Z>}~6jiS3< z1_~obGJ0iqc#$&LDJY0~2w;lmuiRw1)hIhh`Xlubcrce+j(Tpw-1ruTjlpW`3D)BI zkc#4`P1AXO!?e5)vxLaeTAy!WFP*K2SI7@w7Y5=Oed;{wMiYH?0!o1)$Q5h?f{eNt zouY0LTKb`#13DKj_@0+>vdev|f(qg+61ca?o(mb}{(&v8UK8(h`|0SG>VjB#w6FgT z*DUt>TZASe-&<(hOT_vGB5KxiZJPBS>ad-}Uu=F}sikF9c`I7e`PeDMiK`F=;U)CZ z6h10yhOrPow0$%l(nET#!2icCa@@1Kr)G@vflF#!pPiqTX-d$lbMIF=>u8=;pLk)- zZp~QollYoTIXY)Oo4f+c6jvIc$H>y8q2O!rxy80x8Yn6eyX*i(xo$p4yeGDUMFYe<;q+gjKR*rCq zRD8_c-EwLt$;-dX-{gSG!>nk^jLz4x!w*(%6iF%ps) z_gc4>Iy^>;70uHUu7MW!r{Uzk4W^G9r8fnYQ(tMF?paQ-okOoP1#DSCn|uQH1(Xg`I7PL`|G8*kWSv~#0m8A zp>P@UNgdEmPRTLM5m~S`YB!v-msIY)yjNCt`t(9^sS1T!Fzd_Y4I$(z43h z$0x`!0)Z}Ff)`$B2^$Zp^YQzUE`}ST6LFV9B~6-?&QNe}k2auSnnxq1lNi>ZG^m@pu7De~BZ3-L9UPc$j=c zIP%^R?3p?)m=X)|8`BHGnLMn#qq0h7a0X-_GUZqjM`JQuTFc^lVhjAegQI0}*3CsJ z3zg2Z<30Q=@T`p(CHaze3WgiH9JF7Ko~IVS!P_D6>zzc2b7t{8Fc~{tN~fu5zDlk{ zu3EOHKtHhEMU#+)Yhr5a+-)n)SJbJ>)14+n`4x}0v_vidM@K%E?0H{qYmw#P#$^gM z8G$~2VW_qh1agBciEg437*Rs_RZ1K&2FTEiI;p6EB-5)yzyGp_Iy$g*;^fNGb_d7I zXb<(o(UT{YR8O*VPL_TWjm1RfyPSdZM)l@dKu%I5n76yET8p&I- zXDBl$0GZG=Lsy3iBtmU8j2qjetHg9H*bkNvn@=o$VS1?_;x>C%3a&d4>S58h_XPsAoSCDLsL0j?Y;Ly5p zu#*-{(nsC5fL9Q+C=7Or$-CN(Xm{C*LG}mXz0AId0nlc z0ff{QovNrPE-Fwv@Vtj#I<+}O&r;DdW`585^;oxHQH|p@y8V)ALxRgG20fjf^#Sck zpsd;aWNq!Dma_#mj&iFEuZRL&^E(9{SXP(je@S{oLuaLnXJ&q=1LuQg$v^=Ao8&b# z3l^GysL^cOFPCd^kYYNGnyQq;kVA*3asWiuJl-`}-Y~2pQ9my}&eCdxu!JE7y!VLz=~8_=J&fRO&$sQQ0z$I#7UO_jn|AvEdRnXRqN z%=l}IHUZ`_2`VEieFMRGR7|{E{JzGat$Scuya$-t(5))9>sM1e4-N)SX~C$#VQC=E zJ~uR%87Zk9)o7F-817$NQe9V7987qwT4)wxLqY=s)jFoZ zi|s>AF6g2L9|e4ilko&R_sGYO?gV({uDWH7!U&mEU)H#|Zc2g7MO=>uKN;a{Y3yQ5 zoJ0fCD#AQrAQ)}Dg26*MNNb_mJIrzjWf1HVFlcDxNe!Gn18Ryh8{PbaqonV{Yp%;i zZ6nxPB^Fl0oH8`tKMj?IqO$Agk^w3+HKi2l&TnT@H3sdx`P*=p`# z4j(fEpq&Gr`V>zwV|4~GxQWSOY^$R4AEZ>yZn%D>W%Gn(wW9+(o2#V@Um-s&pJ@Be zD=99n8E7>E&%cBrV0T)8r~aM?Lgu(nzKvr0m8VX~V%iDKTCyKI!_Y zsbz5qO{m9DI3alx^-%QFG846qzqo>};Eye4ty6{LQwy(l+Dr;n6TJ^6@qD zu?-KauQ_C0ArJPcaf(-^87E3pQ?sMeja^+G9j%@9c>{e#w0lx|&u}Nla8p~8;pW5L z(DWP(VLpt2shJ#hI5a31Wp8E-1)Uob66P8?8d{p#nPARWWg2!1LZ;Ng9uG$1!50BL zDg@xT0>QQ%&B%-5G$Q>CrydWbdf6$YGCKu!h}?hy@cd^6Cr8{(Ng`cYNJ7FE7VPBA z6)svOCT(`Ep}uu?U|rdjF@915{m}-tro5hyxY{S!D`lizRdAqi)+NBtZw}JPR$xwD z%k!9()_Xu|cCf;r32}fOv}0;1b`7=Aey^&k##dTLaN_ONSXVa2 zH#8zLGCVj?+Bd%|BETxdij%LkFo>VCpr)vxxjs7|<2FJ4KcIdO-CQwkIz8UU9BJ7! zQ=AN*wtyf~>Hde@(u~N2_>8>#0RJGL(5S|-?PVgL{mmCU`>F1go-j}od6OXOg9lB>SX0VgQ z^3h>}V!nx$ok@8)MFa;Y**lnfdAiuaP(crm=K>4%02%=l3lOd!5*^#ym@D8NFysPm z>ePo%hIvQy65U6qfW~1Xo#j!({Jdo#7{g_cJ~s|`mPhJ&1it3u5ZMRtVdmHgI{wFGGx&2b!2okXA#`xqgpMh#6DJ@tbT&B^ z8WR^58kHC-Em!$dc0aExVVeVqs4FU~s;DSs2<6AP!w`hRNQaQCzaUhVZzIRmaXj&8 zsHz&{85ygN43F}p7!`VkGqeu29;q;loIAg`x~i%^yB08P0Q;d!5Sya}?XqW452&z7 z`2dUSq}n!IG2G51D&E2A(Fb8B5ghEvPl+*lnH=w6FB;$z$n#C15l&!-SlADEvS`U5 z8#$$iARpL!u_bsgB)h&*6%Z8Dz##$@&ne*$9zw*casH`ENGeusqB51* zDjb|p_aTsh60CZhXC=^GvhL z)9pBX1hL-!Dsh>btI-)Vie(s}ii#pkM4eGNwkrQIDX_NI*a-2JoSl8#^{g@{r9%A6 zfy-?nel!b*N~aLRUbn|78OO)#J9)TxZb*j=$niMF1@|~F`ApY2Epq5Or;U%K6~Klf z$0~G#Oqn>LbrMCuF@u<=ZB|@2uWL>>#lJz+uh^eFIr7A15xHC;nBGR@0NR zWtD$2A}NYHH0Maiz%d{znp$K=hdC$vaRRq+(VZ)5~iKDC)#8 zwagT8ElEos{Q0(7f0Gg9=in7S^0F}ZVHXnaCUsvM(W_@?+l>=y8vYuQ2C$X!VJFhh zU^TInB_#7I%;ZWV1VAizc8uLa~*;?|AGtL7C`pCq-{rmd_>xXv) z>u%yBzC~Z%^a}m0K|1v$C^x~9n=8zuP(M#`I}-4|fyqCyI9_Y=6J9?cMQLk8fK#?KnNL zz?k^=q_47*Q9I7e6e=*zD9G4Yt)`8jfZ_pdh5I3UOwgBafByB@S5|#=VoXYf$NXip zXJu#eyIf`;neelyA%;ZnG8s$!$B?DH)vpdnp1yGW^y&Er-Jm?zANX2ojq$Lhs7-iC zB^he?ioa2u3cOKEQK4NloW8ubHE~sDq~`30usmINU6U2sIx)?oGBl*b(L2Gza$aZq zLP2Qei#0{uQxDr}Is;b~wMm6IcOQT7gdQ=U#Rp}PkAcMO9NXgGr3iu<*Vn#+Qc>6dwe_GV-usoBHP9MS&fbJO@<4xnnP{{ z*|CbW)oEP8!z;(mCK6|(k-;u50dYW)3i1Y*!*#$&!H}WIW7hz8Gg-*(=dS=H2o7Y# z?bjgUk$9Vn#iOv!A#)@j;ZewYxZyZ-023u2F@+~Q3bZO-GU+mR>GDVPhdx5ilR4aG zJaP$-41-t6Z~7(D<&c>vqt#m7WFT`@16q3|iUfs7anw7M=}L!uM-!SdF&AJP`g!R6 zF1p<$MUA1KN#ll`6ki+jG+g zUW0~5k_01F1m+-_dzQR+`28iT#c#+|u@{@VTuF^EOL!l(8(_Jx2-Vcjg~$m}5uzqU zK?e2xQ0)bCXD@R-bz+v7T}Muc*$w0vIo=>LeMJ};?@{LdWG-+^}if0GGGKa&{BmU*0KOjsp6wK9t{er zx*54E$Wb!fc(d_oDwc%6$5$^ch)-+?BbExH^Mb(M~!$ItU8s3)SMuyW9HD#q>-j%ZUYkmra&F zGk;1@dkL)p4`X2b>zFmlN`e2()a57f`}5rG1LD(U_C-#qxdm*f)7SwbrUReLCgJ8$ z;2faoadMtlsa@1kK9F}&NXg@u(#J1_yr~pc{BM{QNIF`+sv}U~u_Ugeiwk`Er56rN zkXrw7OQvkTaPZKX{&-|=LCj@R#5jV^iwVvVTCRXiF$T&b$lyzRuDtX#7bu2&@B#m} z=IiFyj~u+vtofR=#M8NW`ZpW}vZo8+k;4ci6~>U8R+&p%y}DcTQdb9k7R?@cy_vJ5 z&jKipq>a0I5i~Qy%5__AAOY$pdM+ZLVG%&7nr5(kr;$7p1 zPSRDQ_b08RD~H-l5WILj&7pNj(zM_W?&cfPE=Hd364QrkMZ9CS;yGKPm3LtV43PPC zorZKtIsZ`cGAd?fczCOKQkl^r(}>J9akIn2M|d^r~G>Md{*aR~2{(nmg#HOLNAV9K(-;GItdaKC@+c3vqM;JmuKvOGdQX7~<8IglO zRQ)V|MjU>s_@VoUs(T2V^yx2^KW-uZKUDra^XG~mNuc=J50yXR;}7C%fE3s_pTQ=U zaRo-RDc#14&$ul9ewkavqFad>ufHy)zfK*&i})D=XpZ1YmI3H0vW!sq5&yYTItgSM zzX1PPY9y(}qpB(P>ng^w9@2nNMVp}!v}haJR=`luvB#dKQYoGre3lG;o^x4S0j;Bo zRimwlc9sR?Vgib6BgV?MBSWHg$u_?J`h!s1B+w-3(&M_D9;I=5)CSt(OD~J9m!*^G zJAF=LL#wCn=xefMkbevt zi9^8i0?1TVkg7&Y+88McW@pjpq6;=@O1!kG9ZZz!K(?9E#e$Ou%Bfo?C>ek z`vPZG4mpAhQB$uYw#G2vKrKR~LhSJvG4&O>!03FBvo&*Se+XBQ9m;W>>)O(w=o zrGb8?xb_%sw9IIEo>_BYpq~&Im7X5e5E>aBvNa?+GPEHoJtLAk+xVa!b%<9RWe8~X zj(D8aCA}m*f4i~qHc4n?KMxn{2S$>u#Z3(*BlGlwA}fl;@|L_&+*@%s#*V!f7ki_1 z+$+)1uZ$b}a!lOS_V!m|QsI-BKwtfzHw~;x!{;e3;q~Mk|a%%H8>4)UD6j)^r80*=thvFjcE8edagim zOv*O9N|*!c*5Kgk!%74JV$ahxs9k;HajYWZC0=YGD|Lqvnm>o#)Idq`1W-PFW zYD|}qGjx^FWkFY3|E*kT+G=JE9$vu0$$e?q6YUf~f{yq5K8v^+!nke@{Gb zt3Bm0ZyH-=7u|UY=Q|^-Y7~mh49CRR8v3s`kWTU5&-~s03MSUx2Jg-|Kq?E6D*6W~>0m&Y zf|3S9x&-~HgD+wUCRvhC^pJ9k3vZsdX_UrW2SB2NLN z1O*`Vb$55Sw0p{w2b!n2BHUz)2A88jGYM7Kp+N_BMxZ+@{@b}0w|vhs@#4`Z$M@KG zJB=@Ck)=3n9=Zs@52Y!fZhFf`fU)w^5E{BOl?Rv`2xH+5lttOGHYqRspE3z`0`L93t zx^#?5%Yqd?-rIT>ZxJt5Pi-tJYOa8Eji}pPoA3)|+;NmeVV@FcrZgkUJ9irH+~HpM zy5Vd5(Hr@LxN!sk#1zypr@bkRXlOuhqj@WQvqlTLIM7Z|++g}W=Z$;TG&G2JAsJv( zM_waKrC-w>2Yl|Sa+98-DqXqUy0PQh>g(Iawb$|=ZyK01Wnk0Bfhm&-!9xLfs9pOI zytW7BD4bD~_S*Jwtqpi;Y#smc6C!faTR;GZt}fMu0KU0>8Ado$ zY;qg>vmbRO?kHjRAc33q9&0=`pFSnIrz;tSdp6LgUFc;l)0BRHlt2ru8yDPwxFnR!)|CW^UP$gsOXdvTGcc9g$}dOu(_92vq4`j&QSuzW z92ruv?PDdm15+ih@jWv62>5rhlB$8Jf;Y>e3Tvprd$?1yWW*X>Nx7K8y~52!JubQu zuYophDZNLifArK?(#P8%)2ge)51Cepq;}w*V1TVA>ZLyD*rH>tFXdzSE}zW32pb^@fj}IfI+ne~>TfCi}%Q`s&u4%#P8cDu=p(<-%#` zxH7>l>LoC>r5e>omN@jp1G-@l2{SXTO#bK5{$ONeik8qIQzbbRPk0)eT2_Y|n_0?S zVBG{O7=`Ym3b(^d!HJ~RN2ewP*v1SW7T}Po7#{8t8oOfex-*q)f?KCN{Pcr|K1VEe ztl7NN+uJ<0pgHo%=W^!0zqrmi(6Y`Z@R((w_3+{55!Vz!HhRM>qt5wy`nUw{d(*i! zBO=zx!ptSd(I-9ikZ@pD{RB5tPfP!uv-Uo-@rdJuhO)NsVw+Jm*487HP3zj)Pmkl~ z%;vbR<sCw|CY=8qPvDS-Y6hr{1>H+yt55frNVJSM4pt)L-h^-F z;eDgTPO{Wu%gQu9SDamIN({u`Y?(LJnDU0{b7!{Botw33u1MWO9C6Y@UGp4JF?IA& z;jl*?j29JG%%s?7Nts`O5mIIFnq2XtwYmlR)+|`7c{L|*A>lhuf;_zdcf6y(3=hccacGa}ccA#hul9U^=HsI1 z7z|AT+_ZED3Xh_4(wh)h2pc)2qnTQv79_XdDBC`!X6abZvIdV*SA}g#w)^hwbK))} zFW$ak+LYcWL&N=|a`L#Y+>90L#IGX9ED{_xZ7W@rBU|^xUWX4RP8~U-M{kt;)almB zk`*Lp(d3R5>t;3ACgdjvhsTPJrbSWp1@C)r-vl@{4fIRz08W(C35eXG3^wFcm^B&4 zK1*DT0{OX!J`j8Iy$}0tesU7w*KPZRP!~}xPQ7%VB#@~t{Wbh<2XUo%?EEFNmRz$V zDL{pAph5`hWeFVF`5XKJncfTH6@*5*EC~}EG1UjgjU(HBe|Z0gUuY`fQ{#y9XS=uU z+w}oC{8Sfr@U0il+dJg;|`&hb+shiy)#!>TNEnwA;nKn{u zBLc)CIEvEFMKB`S>JD_|Ortb7ML4VS51CUw3k%>#dSykzuyuypS8d#|gg?NJBKm(@ z+TEOxFm4_{>EQ8W2j}cES*c%;F}9?>KB;>6@=^P^r%R$1OnZ;n8rgjN(`)|2~#r z934JB9O>X7baVw!(E7A7#efe7wHb!XRAxl^x50(X5h@=ao}e0Dl39{g)jD;CXW^Ra zMSGkV8dX&k)P^KE*Hu^tg=O|uPTOt8S^j;jy(Llnn4!a6*Y+SHGCV9M#Lh9JIy!Yq zQtM>Pq_~tA-$;v*wvI*)WwEL4Q4^biEy-9x*;qlAbiFn)yGNSwQ=6#K!Guke+{he0 zmdL#<<9>VMrxoeijJ$w2DN&ECO z5`S1c7&@uf;Ncb{E_Hi<7XM1x1F)(Y19$ZlOxxTIxmOOMZ#(|8^cyU-kCg?EYxwnL7mKy1585G)h zL@3ZKSc7)rb$m@1dTP)(K=tMz8g@U1oWd(?a!GAqN5P^g3sS4An|pW!&gWm(;ohIv zNc8E{Jv6OLT)PwLfIggEzpR`5t|{`V@?8%)ff*9w7>$#}F=ED`!eB$Sv41Wl@K|am zYECJ+5Es4u`Ei>*o4IXs&*IK3`iJ1M(}C`ztIM8M*tk~0@`hLh_hM^s-UA9azdkb)39#L##J57 zikyrjdStpxS`}1XP5;H8UooDntD0Mg<&`%Xm6G zoX|0KA|0}B)U>8RE!zV6)8jnGMh9n{+T7EM6l|h#Gd$Pm&VfFU3egF&HT~s!McS11 z(BygHonfAV>7mBf?mb(xZMuHSZ&H{$Sy`F-oWFF@G_~H{t>A9^$`wT3l$6<46SLsx zoY)D@F8+?=dy`$=llE*3Di~9keOo_b!bI_tJ&oyU=}k#tspDfUENmlp7S9O^stJ8@ zo$Ro$U36vD3*|Ob3MQmaOC{yGy^eYtZf;AAZB}KgJ={79to27#?rJqZ^lH6ZxV@jF z(yKMyY-=7Z-b;V^*^?Jj6Jz@dLyNOKR*0|6p5@|_Hapv+d{^_*$=MIqCo3j8bwx?;#8{un!(<~y+PS$G<)o%unVPy!NL4y!WsC2>py#h_h+o}}6~hg1(y7w( zl5)zNfLD|-QzoQ{7U+tB6lf@y02!(92OPsZsn{~7=H7Bb7d)LaioyyAmh4;HKxw)5 z3AJz_F~T7|a)i&J+Mwhy7A4KKiF%D+q#IfqJFy=sKVK$9v(+?%2 z95|Zemz++ILE$ql&4(C375U7Vg?eOaf9c5?eR2BYc4$oRkOpy^#-+EHPwnmH)@a7z zD0Md14NuWEnvaA74@(7v$)zTA=gfSmZF8?=6{p0Lwpb$g+1r9@IUv_`6f z50pE~9kDv#R;p{1d#6`h_uM{&zq46j-)B#}Y{a(}HYUTl!jrSm{3>ptu8~8+sqVFF zyIbbwIJH%GOb@NplxUZ(%omn35l}Mfq|`r`7xi?Yi;FI2rgZ{}I6DWL(KUru^G#kUdG{FG94U4W{UUV zqI6VXqLf6bFl{{bS!bdkRj$$T((mZ#b>xcPCs&z-5c9}tQ`h)&&2D4MgHlaBEaF;S zttZwuDNL6wO^&e?WbXCRGiREayII$bcCswBw6?MeP=}XQ&W+F6(B$y7zHnPLC))7x zuZ|uTcJfihwk{R%i6J5V$wt#_kGwSQ;Jz?(PusjKb7i8Fvz_V7H|olIqGMy?BPNzQ z64Q}DW(6WMbV_eP+{}|yvq7O_=p0>?bZC@hVyd)*&?yN)59!2Evnws)V6HHMY`|*f za6qS%(aT3{(}$Cm7JWOfXVc~vIya6l3t!u1mFTIQTU*^y8GY*gj{0J~;&P;%Tz_!; z6SHT0F(J3OCid9%(W;7bi8BJ4KF=%nTIie@F5xR_*Ri4WE;*r89#FFb!VC#CJl*5_BMTK>I2 z>3?7a|C?{<{kh-26goQT+{^t2YM;=I@CZMDW8m(1nvmWi+gG8GE&h;%~25lD`4-8*DgLkdC86i z3$`tt!WoNSiocgNm}bVwt;ZMfCmxO$R(GAnPuD7kEUz);V!!y+CnvAG|Hj4VfreK> zfI2~dsLq$2`3Dm00I?wmhTYiME&QJL?fupK&3`I28`)EK`07oTWSvVybTX=@pZEfWg`wJf<4Rjz5f=qB=zk`Ub~zoY|=u zic`er^opCk1H)Go?kCY5ouwsJnVDk$&V3h)v;Cu%%BIwG6oqEHTdUXSU%Pp&z^2VT z+%T?pd|iHiO;=pVyH6`}GmER8oH{~%LZp(My;tXVjLqGawfSIl^qo~tKe2iLfu+sk z)qWF387nfXvVxv^xwfHjU-h03W(I7ED6^g3(tRK(@MP=BFFQ|`Jx8)9{&6Ym^tgG? zttd=LDon!81n?8y0sN@#0<%qz!!Tt~=Yog_2$X(P7k0LLRSECJ#D3OwL)^|8|C>nr zt9yHYr@4a2((>Fx{5Mk?8YXG3NxN=7ydWH)o+z3V?kie93d70hY_VH<8N#In^pb7} zdV(WhCS*FRTqQJf7(z-K0+!~1fogaT8v$m>+7598!njThCo-i9->X%0{Yg_}GsB#Z zi~SKl4@gKINkZU<>hFGVo>5_9B3kU2G-2nC@ttIIW%b#IYpbfOtGJC30d{V$t3*?6*BcG7kS- z#NR6XzJH#aOkXIAmzkPo!GaTFnd}Z|1@wDy3{iA+LW?`VA<#IAL3lSarFcaL70ttZ zKqrz?!A%8mis2W~jiMb&Sm0qfCvnu`la8p!80rDnPzi2S8=K(WRS9EWe51okgr(%w z)=^P6I@_jhUcQXXzOs6OWw4E*(q`<;yYDvJyV;wWTlrhWMElAe?V64u7H~mlGjE8r&={79TXFQ86q$g^FP&rWR!xWhu$ib8_eY>a;vHXU!b( z>#0*aI$G1!&8=;7gfA)@#!cw#X=`o$zJ1&@_u(dTrT$3Yt(A-43@V!M**3mGY2)E; zmXZ^1VC=PJbzw@$$-YPr8$qx(cUQ?XLRxz-#`|Xlb@z2WCEgIPx;QT{%AFYR#e^`2 zk%0;uPgmD{d-vm%w6RpbmnLrH?=M z8EHdZKqIIaPUk{lD)Wcqi);8OXxDOvP~pg^X4POO^<{>YY8^gafVVj z6f^4K06QQ@p3-8rkkuhPB6L8%`wr}zu8SSw^ioPlNWZ3#5|Q5_7v5QTDuSk?7uwiOR3WL&GfTr3 z5#%*f65a)&s+jhT#;YPfB46H?BN$k!)zyCbpz7%g0^~SwOHTx!2HcGo#lG!Kw z#E-;h-hG!O6Mp0Jxih;!Ef0ua3t!3JWt7r^3X)K2n$Q*%)b@`#a?%(i$T_QYlm@Bz zkPzf&<&KD#Hnwi&%u1tdMUN1!GV6_JpCqe3)>Qp;$v0Mfo?Ddt)YSQ!>8Cl*{&U1q z+BGtB&4R1fnt$2$u8rnN?o%S__fLxzV-0%6S49Q+7Gr*ftCw3!SHto!4t*Fr=mB+L zc+QBVPG*tQJqM5;=>*Mz`RFX%{^9WfI0ZwT=Ap$_@MTAKt~qQq&v0Dz6(jS~tk#&! z$ert#thZliP*{9iHPSoIb3sUG%y;w7U-SvMczRCasj)|`org*J%yF|?TCI|k<~v&2 zI2fDRI8?+oPbg|rghx&EvK?k>qHk+!Q!3u&<)ZD3dE{3AALO}Xhr}52d{U+P1qfV; z+zk0^m^wK-mmf6QV<+!ameBozpOfng2e>8N63s85el5*n^CW4)_JP`RVt$AMvL`+(IfI=WzY)`}lT;+`qcb8# zC62VVH6RPFYc5?`B35zDLQwxXz8mxKu;FFzqC8jr(+6___-wGtv*-SzD5eA26lBP)o%CXVkEzr~qv*NeB4mVVlO{<#r7Q?KsX+P8|# zUrs!ipAa9M5I0#q1QbdJzL)MsXI0eigDKmfIADtRKh5?z75Y!Nt~C4=CvcjV#ar9n zdoyc{^{-skGtd3_;rlNP=5S`^$K^9yxGn=z_va=d#V`!19axd|&l8qSd;y0zF zxbo2Yx_XP+_}rYvr=rKegezR>WWQNud&EB0!6Cw`vBA^xshklZ;YPJ-sU;1;MO99< ziJ_z9MtZ6cQ&a2o@D@0t#>bP9&n({IZPmjE1Z>Qn!w(bqVZ-c|w=A8k{dqy)OPWgZ z3)iYRxy;#lb@6NBTNpZ2<0Kl!c<`;Ht{GTqAb>D0cIfkT5yBl7CH0@+OCsmGLDfZR zR}70-=)+WB0#w5$5aS z>**H|5NBtnlN=f#Og(5yPD!8&oOKx%it_B5wJ|Z1;>uOovzg$qMLe`-^}4js#`d;Z z_Bv?=hDUnDZ~LIQa_r!Rag$U1mTyi@%A43^Suw^}8H12D-0%8Ewq_P1Oyc8?mZuk5 z4+;#(Qs?5b3<5)}&w?`9TwA|9 zODCjcoou^MraCyGZd}^*)clOB?TJp2s}g*pa&m}t`z(Nd_P{-E(s^ZSSnOxN_AdXCq%*&)cI)N{H+6wO>qK z5T`PYhzVrk(p`QLA1rZ%dEF4b1k|NigTMhTQc#f-*aLGzyx79T(D?2y@#hIep<$60 zM&!$%s|=b8xTIFp><(c*ehMWN}0mkCD_-+w!GrkX>pZycFqwIr<*eJ{QdWq268Rt zhUUXLD^Hzj>ALhC*s)Efc=_il##CuHuXN#K$x;Y{?mpUq(5)+Wb6Rph6~rJNTI#^8 z+-ObERycv2JiY^=*ubzK!ZM!X^Y&QdCU7G+F4d}P|1 z^^NNb(xTGxLMqgw#ETKvCe?>WY;3U#OH_=svmY7ZW;oJ5Jl1!OJY?rMxhhWfzUIN8 zMr*uFhF$pTadq{baV>`fqKhx|I>%1@qpYmf$HLs&)wH~+pr9ah$Fr6B)p;Qs z*9Z6}_aDh@)PJqCY)6KfdO^C|#7<8=eFOK9mf*-q`ew1~y)VbeOf?5ouIoxEnV4w6 zkuX>9vZ|^ddq!5#yv#UG5F!{`M-q34t#}0*Kk~*GwsIr3a}i+wY}Yp|0>pqC z^-|c<1rM15@PvmIPd8)QvJhhv^(eMEW7k@I)?p(hA#?(a@uuJro{s9%2mhFIOvZ6? z>=@~NYCYxHm!fl0J#8A=z1-i}-_|dFvah5$Dap$xI4$+Ji z%?%50$g?u=E{$nyYP_~!%M2wh_yt?Gv2+TB>ZFJQh5$A!4eb zPigT);=AVf+@7@Y>F4{xL+@Y6{8?Ns-dnN1xTvXS+%wORT|_#@W$P;+zncA4$7kzi zluU~X3dzyq&5bP*gCZinzqE-sJ~-oRCY}|SXvF=MSr0b9;GTCn(J9f%yDZtzG4c+i z=&LKmPcv=ubC8xDd$qg#s~|phk`M|+EzDGGbu;>-gB8_(stE#$=~x{V46wZEubCwQ*$e&$~MW>(lDtoKR7O-sKnNuvq9 znm=_-|BQI)ozKMcho6Q4sQ3rsp}9c|tPij3o6_lv#1rldo5I2?N;Y1OzAApPX+H`3 z_+t{f@3Q#Ey`RPVWcUxi5-Hu}=@v3TLK{ zW>QC>JI8-qd4;rpKpHQXai2VYoj8iOuRs6%b@3D8bp8H<3-JADCi96Y@z}uIrqQ#1 z>IFi!`XL7NMedRqL|)OvXkxht4FYa%)%?0$pF1^Top=~v%fX@G6adH(VDUpZ#SmlT z;9%^$gTVqIup)G(mby_JAjpE8oS4G$PHM_Q|7dI!th}J~x5}PsY0(xWxcre{V>R9F zSnq;8-4>qqq^hV$j5KjDO)Dr`LcECQ!d%6ur6Y{G`=93GQk0WhdnR&kBd5m|@#E=4 zf3g#wn7Tx~d%q4|WA(Y1Fl(`tzlxrXz>ZVW1o8_J6n(&~5!tW1bxL(aFVUY$B zw`R*}F04`%PVu+Jz%NGopM-FqXcBP}t9WCw`oBD)im$zY2i`${8SDgcb7G2_JU-!*rM2PeZy zu>d#|RKZ_hl{Gn=fs1iwS^ZSl@GqIHsBoyt@ls!LP)m$wjOv}io= zqtp!4gtq5Mw{tr4H^}UiTq;b^I!?h6#gpl#K`5~rOu$BVl|NO#WJgNMFPj@yTg-sV z!wcf}u3loaNE6oHj6jcf%nZfu@>8?cy*{<|g}&>@k6mml;bI>IH46O??w3cN`=I%u zpqY0jl+Vu*&$5o(m)q|}uKC+#}UN99KsCpjOYHc?OjXnVpx6rUGA z%809c`W5}-*FOALSs8JAcK1o~Kw)KidvoTCwNcypHPss@b@7E8FItLA&FZ^yKD_+m z!TrYOA~*ns4#hRu$BAY9FUHDxOQ6m z+m7wF($oicqE}V$KWi7S@P{l7GipIGh}xkbFa}UJ(2Q!Y1X!FKx`4<@v6_WHG;udC zkTn;6AcA=RTLhwgebW<~sGB#pZslD55Q?r{`{dOlu_*h!|8(#-^N#iNPUK5HYPRE#sMy2Y3<2FMGOdUDU3( z-n;wMVWrQr2R5F`uPIsGe!3%Kb@YbXj#Tdua;ITO)ZHx$Q&!AvEUs;gn-?1uEA5Jj zUG)0trSE1Y?oHaTchiF0MbY75H{RTJ-}B#})Eh1`zccyEj;3qVm#h23QY%JRbSwyR zQA z@)5G%iM`XJKzlnI=yW+7gM^aI80Y8^QzHWlyGVC4S&B!hbjpmjit^DZ5vft>VTE?9 zL`3AZH=og7QYM$*5-sEMlMOTN!z>Bs{zLlA{%=|;9ol9P>zK&MXv5^ueu8js_M)D- z=ABDIy_RneB&rCdsfB~!EG>vh$o2Jgb4#?3b`rmmap(4KiH&`K^2EB>ynyk3UgJa6 zo?E@eFSv$`nsYBrp6@j=*+ASN<47fkAF(gsh2*V%d1fTiv`llRw5_cax#48ZyP#P3 zV&F%;N}(Dq0O>G%ZKA3o72v6R!Ll1N7#D2FjQem#LoX5h$&uxY(_K-sa+*F4=Qqr3 zD#Qqibtw8)NS zMb)cUmgJNyUr!vX^Q#uyijuU%We)aP8IFz*rl%&Xa&pSdw0GcMN=Zyiad1q}uy-`h zV?VtvCCDL3-u&wO)uLGL?ov3goxCafYA=chwtoMW=(|=FOWk|9C%3&Cy^uI<&CAKj z*}nCa*hR!~M|Mt5-d22$eRT^f2a7;BbQuSt?+F00)4pKJ&=9>Ph;D~KdVP2tP*G(M zlLMHdQO0#n&I&NJGfWB{X6b1vu3IRslv>zYqM}0&$y5c7BW!GBq~o`R+ZO6#UMqTc z)pQ5<$MP3PWLD)FWvtKWFT|;(Z}?(6>zdM0{F{&y1ga!LSL8H;L>)vkM=G1)oB)y| z$WsDTn7g?7PhN&9Z%Ln(tBH}T$tk ziWnNx9E)ZuSKtVsYPxyd^h3S{Ce@j#M{q_e!Og`ndgh33AyUnUq_<9=ik<4o1j^IuH8?%-4s+{NXfoCldloBSc78 zQ4el6saTT9HEKTQoHZx8fu*Ek35i+y@Cv*VzSWGDe9j%f(Fyz@`>B=8F!CM06!HM| zj0fsH3g5&p6}uLQU7P=9U^8lzwZV_lSNiSv&o!lrd_w)+`}H=a3jJtiz{d+qr3wN9gn8R71Uf#>XxY(97*w!2}{9$()ar}$jb zbNOAV(3ue{=<7XUU{F_|clgQ_m$_5t&WtazwRJPM^bfYH+X`Dtu^Ox;Tty5*C9q1qGFbSkEY4>Bhs%^eIAVRqVo~{(V7pi$cB1JsnycZR%V+ zDCPuFZ$Ux$@K@m45c z9E9TtALRi_2;KlzeEJ(mr{Jf+7^tp;V<%)gapUbFdu6Vt%2=+Yw71g0W{^4b^L-W!dEh>_-$h{kOP7ZeB0VfwX1OXeE zgqoM5W=P8kY&+-PKrCgou!_G&BcnbLlrW|~1l&T98AMe?5F}Ut!0I#D1+s|jP-SS8 ziulD={8BE(n}9>*z10+0)_d@%vqB#)47K%ii1 zC8J?2K{d^k{(44wGdi9Wl*8NF60F_L8b>NU5?V$(8$UUIhSX?yqjUw&e@I43eGPYv z_X*_QdW&;PXxkcWEdKH#$CI})nmEkEMZ}GB!7QM?N2ts4%+m>=s@b7p50_o3s2CCC z=IWi6o}A?R|1kI70Z|>@|1fj!?t&l+(z~z}DWcQ`q$5a2rHY6JRO~2<1p#S-0-}Pw z_Y!-s7c8-Bnn{e0B{7NVCZ;FCj_+sg-KA-s@ALcbHBpw`yEEsUIdkUpag5c;>Nz4| z*k~7L7oS|o(U1^R2S+D+=YH{WyS_GI+F@aFgZe3Y>ke`s!upWMfOw~Xo=6)eD4{?B zBLSu4++0I59XPA>j3wLh7f6?VL|%MIY;|`WGBcIX-yY)ote4_1_`Wg7qn19*&jY#$ zxim6aph2KlPqSXu+7^0RDDWf0#y9GlOI^Yh5=oraK$FGO3#cC1yCY!DaYn<&v@- zk@px@n9o@v_UqGUTrW0&1b`4lUV`@8xk3-^AJMIk@Hvuy>(%GorBk0LXwRDOq7S$Fg3( z#(3m=6^%9J3LpSRF_Ci%rykimeOAGYeQ(c@7M`QOzWw$u$YObADN$%VQ#}p?R8`L8 z^HuJ}+^|tt=oezy%3Y+Mz8Z|7&*52cA!hdx7|93Qi@6Ma&gJ%twhM{K4eb{#+fVhb z>*>XWwVVw#_!b;<69Xy4Ny5~V;3vJAvpsiPZpSPrkkP~AZ%Gs+4UaST0t5haimLx3 zX<5pnxm)m`RC2dM4IRFh3E?FOM}cW95u#RgMOCp4B(r$Bvt0>kt!W1@Jd@YL>Er0w z&P%IxjkRS8fAr2bHPE#;Gj*soG36A2f&NlcBV7kmGy8TP%aciyBD^BBdg}D-X{|Fn zB|N4_PrL>KO_HNzDiAPbr(K7#!9Zb7Bp6(FNHY-oE3-wPI+fE{lS}_mw^SAuePJvk z{ctd+(9jTg$(hFh;tk^{I?7R)lo4YQ9Zd`&seOdr(IZ}KV5qIHZeZxv(Z0I9ueQ*+ zu&b`bwaOY@3CP=<_-%C3CRfy8yuVKHNf3bI;4Ji%y1p==-}XJFWuD0<@NqT zi#11yo5n-U@ld!tFrpACgN$RIO+iL8`wXM1Ejpxiu_+B;YKjC5G?8cf1Zivcu-0|= z7#KY{%rPY_B4wb}fGp48H34qchUVt75*>2``-v%&jg9-8>YHlC8izO!DLxjH>gXRG z6XcrR-!Cu1KF~(_ctF&Uer6VyINE*#gX^LpqrQ+{ES#ksNP2mS)yz0oiSC%O_ZBbC zt*o4JiN2rXg+Dpcw5T+)J+Z7Tar%!gF8!P^+{a)pH*iBA!3QMZ;>3)2NK@g+5oDEc z{u8&!Q^w`Vl5%o}YEK$HBPY+pZ`!0l55~Z=W-(zqc`|DpdEx{*meL~_b|{AJ&n}1( zy3;AzM`Z_vxDYtj+WGAb>?TP_(3_Z(a}!~4y=Xsm_jJ}b(@;;P5-H(0 zqH3Pb`eyc(y~cd#Bdqb0AV1id!`hQEjD6&q3NNSUmquI~6KpriHYj6?LJ%c-JMrh| z6~O6F{H}xVnJ~($7|h9*v4Fr>tV&WQGmx7c$WF>yNCp;E0};{lz(khVFrc5L_t5#$@yzRr@^xo zRY}8Chh3M^Qyd-3y{Iz*}NYmYc6xZhXOy+b8)l}k9M z&)8MQzhjVpgh5pE5fcM8U)2h-Zm8vz_QB{!m=pay0lseM#~l>xab9-?hE2bs9+-|` z5Nbr@=@WPw2eg0+4mB=%C0)r+XNYhwRxpe^9)j|qevIL)dbE##HGB9313@F6)8?C{ z>xDipf)h%#o0)OJat?$;KoA6i*lJJDQ=JA73j|}Y(C=?$6(;u&GB)wbj42GB;t(r$ zcJvI0aE=>bW@>L2Q|z7P=p7{IeXeldLxdPb_?e7~+7PuM)Z5&|HPF<<(cRTAyfSu5 zU%za5d>=jI5V?gb87g3{1`vWOSvZjd^|}*uqMT|6Pl(H~uqu;T?SS?7nl(7bYlPgx zuN^QCd%M_$V-ge*ay3l->o-Kz20Q5M83o&`VQLI#AXyuSIas)24b;%U&k)ug1Q#eJ z?0Zc{7tG7{zB(V7$yI)ao!qxyY3%$)j{$Mc5dof#&hqVr6|24a2l>mxtgPI*?_Dk2 z`&m1#EC_F`4EJ+&cl0m~&Mch~wV}R$K$wxSp%K=qh?~z>fC0p)6#@z@_m{6|`+EiX znwrB8@i$s}Eg@e6&iUM2zFNxq9-#LfQSjBd%CubW!8mUJxNe`qXU|u&5BE!Lm1%eY zx}=S=Kue*Oft+o9SWv9HY_QOXh4r7v!HC`vX^a&FtBSQYBC5aY`%YOCuJ-dos3=w# zns^Cjkvh#Do+i1h^LL3M{F}`GBs59q5B)yZ!(*KPU|f4%%hk4JOHRM^;PPU9d}Dg@ zwQX~H#`yZg>b7O@dvya%)6AT#ES;Pzt(?pUm!j%~>f5LMX%Etv{sqoxgT>g3B)nCk|kj{8Z zCK^!kySYV0xgKG&dgP~0m^pEf&nN(coM-7w6`L?202c&-=wpASGh1&RlK3izJm=5j z_E4c(bPcXo)ut2KzmRbb8u3yONd^m^VVPNimkC56G(f?Pi=M;|&XI%2eJwsO?#%QIzxW>YX6OOATA}rtU9{W{5Kp%L{ryQxVQb zYfdU-qqRK?;?1W{@eWjvmd!@^Psov3)_2yu~vNxqjCBaV_zK9%Y z6Y(4Pu9K!CtZ;IPL)wR}nYNFImZ_Cru%d?JQP zL&_4J^|bUf^+wzHEr|>cjEnW3k}}*v&otUEILAFoUmn!oY3cy;o&#rE^|rEfk(rsA z^&03HZzD6eHBfkHx%RO$*XtGSAT#OHtGAw6jJ1uK)38A80232KLz!-0SE*df+|bQh za;dkMRUEh@kZi?0Bj$J|GyzNocfb-se>q#9S64W@D_4|HB<8G90d?HE6;DmzOu&*p zFu^Ru+hRdZJK^X7rMq78#@w~yLNiOp2+uh=@~(p>gh!0`^BW%#K4H+zFb|LLL4IB# zbhUq}XHMgsbtOnd^ccTlK~srjY}7b!ud$JlV?4dGqkMyce0_p~=)@?mty9DNqP#Xw z5A$P!{Z%So&5Z?M8jy#S zj38ZL!EhRPC9b?24l-umwGU<7fxQXadqY*D1y6BPYOA)hgnI=F|Xm74f zrZ4J0s(ykgtB3)8PJ3%{ZJ}jtA$V#U?adpipJHxXFMY0*-B7p+A6zQqFaAYAsGZ2A zodFryp*W@-A-5$zDUJE!MtqK9C_+1Z?nCGiibf4>72Nmi^PRMso54xg_meS$tFDIx*wmMn)m!)OonvG? z24f$?*h5ugGc8UI8VGums2H)|m_{P~H8UxRDo=2+<);)==N3($(Z+r$-Q7u{9>VbWDJ@|zWzL1CTK~5t%sj~Us1MQAuU(9ljt>x z$Aw;%XEDJ)?GvcoAZ&QmROkuG2VbGl?B9;5gie+IuQp0n!^tlKCzi){`X&REjl$hj zwf;o+Pjz!nGd4YXYnoypuD$uPB{Or9aV<42;@0eN#Ury{?&t(RJsT~BXnghwk(i(A zwX?2vW8heS-<)vnFsUDwoo&O-MDyjd_ddGTb3t9@tc1Lw&WDu6&h~0D16I~m63|tDVSrmNN zr_dE~z<`V^*=Th*xFSUCjP8cc=s2gC7mc`Ks8bq_q)d1R3r*Yr^C|pft$5qD0o90t z$yIbkH7M`>-bQg;oUwlYkW* zBTplvj!Y|aa*R8WS1i3#RcaU)mo#Kxl(|`7J6GqzVZ{>_alKsPy%vOzXtM6RIIVU= z+wEv?pMJ8*De_P&Kerw;fZuTPVQ&dH3%dpq)Ck|V=?tfK%UC~+@e`IU1@34&B{{ny_ zx)7TqT4R!6D2n_lkV_%@wM5E^ubCFcq#*NmvG?&3JBj5k8mnooeklXp@t*KDOUG@0 zU+!q}XJ$@Pkixe-(aSSFG$cAKJk+l5(79n@!v^{eiwaAZj~|>F9@;;1a76hq zPye{ckg%A3>4Zy)^g(HukH1l1XmHSqMToP|Pf386RX;Q12)C?(R*n|t&UTj3ZXV%! z`f@#e8-3T{Y(=nGGq ze2lg9_Ud>^Wc?gjYwx4t5Bd^qz4ty;kAa_Hz%aT@g8K+qBLk-H$i*vXakz`|Kv>Dl z5F|K_nXqPhB6yE{_;%wt7d<^k|Q)V5hhb$0obFcvu?h z_4RS>-+u}3Si7X{KQpI>Pw+=ly%pvU1_^e81fKtm1S&Ehjq!gW1=)&vFKv>{!oGAH zX`tG;|M$O_LV=4*u(6Jfp{tE`N@)I!ylJ)v*IYS~xS+qauZNkjqork97qY;#gFLb$A3|ti-+@EP+e_NL|IiOlZd4{{0*rO6zBrdd^-jqfBqR<-NCW z-E(txJ-lebB!`lvi!{cnhC$XEyaV7Zk?kb7KgLbPFR$%dxjEGLH`I}5*1 zr<9ao={EIMdjJR2J;Le{;ae@h)`Z@TO{%n!w+ytmSvpwK)S45z$Q}pI5kOUmxzpA03 zxuId*?&H%I6ig@m={@4P_PlXpNnKq@qghKMeV6nvo6r2nxv##KUIWDXETM@df-tVI zcR&&|a*1%)7mh7W(G4WH8+{dOc93498|XpWjHcPzqzB!8lLY*ElU^b=MCbgqPZEMN zM}&l?_po<){$blsuU?VirUQ2^{@N{kJN_r`nTo5$pwRk8M=&}PYR>b1_dSz8Z=aL!!p=q==lWVTXQ1A z(%ZJqtt@RX{gdWYWG;&zzA$oAnPOpXRrQb=F2kY{ih^3BeV0zkYVZtQb`hHVKKdlo%{;16(keH@(#P5aW9THCNXgxBB`%#+ETaBr2Nv9 zxt}cGaL!I*cW%Rq`*WsTE;CtJw{7{7hTY`SmbczJbNn~UTBF^qO*?WgS z+Pih6YgU}4y|Z$%(?L(vKWZ+J;-&*mOqFtzQo+OnJ!Rs}26{phNpL$+PYpq;-#(+v zv{|`NoRpp-ZQ^{A0G8!TeT7+1R#4ye`Oag$AT}QhP-|#P7n4-M)LaT0-h`>?B&{ZL zd?G_Lf20{}jKlg)gA6qkjj%rUUVSxKUJT%|H+&Ut^ODxDR3liAU zLa*y%%RIVGA8owyg8eti7kId}aF5_SN0Uq9ed&;zNSUZR_iks)8uZ*a4oHxk%- z5+`~e6C;OAIt!U|eHhh~*&NUtgn8vMHkCmq(<$x#^AEq&`>eV7iT0QGo;^EXyrQb4 zbXnazB(vedo1cFsw_2}}=dCEfHzQ_`KDhUvNAKOj6sRQ7+z4UNTTNm38z2U7!x0b9 zE-D=G0*b^$MBco9d%*c#*`_SnehvG7i>|qaelyqmcrQDyURi)T{R0deR_MXibGgom ziEh+`d>|i(NtU5uh7IuO&$@pI7knvL03K68+_wic`>-=n=-~`3omj_wT9X?=@`8R_sG2-bi5 zw{fF1`!+Y8G`002OSG>|p#N}YCG;~jmy)2iPsn7@Y6bYt8Vh0uEm|e?A?Cb<{~%Hr z&*3k;XEIG&cTP@*U7YiviOJ$oekA)Q-PZK!+>12xl6U&zEOY*Si?LP75$Kmko=h1>ePI`lI95S-ylhznZe!XcL%>x zL=5%?iPem!18;jD5mS-%Y>7f7bqCYEJVvzt=^%J#e&g!5w$ptX;C~MIUmwzd^@9NY zNjO+%!=8`XD!l!9`y!C%B8P~3q(Ze8td&5zEDdXcgf`Xj3#tok23aNpQ;@JMNdM)K z{wuL;kxKMz`rPiHJtaJD^T3pZv0e&Ga`74$a+99#X`ru-RPyvz4XM<5jIU>|r}9>D z)j)-FO{CWl$$TW|#@P*yvJYYbG)*)M_yuUN0D`y(Y*w}-I!%gAiAnkRtvs)5(R;3a z$;gJ8!@^SB{Va&X$7IRH<2}uLA&Yf%>)#UHu(38_bA9OY>C;D6hIq&G?yc9jP>0z` zo~$#P1I?3007p0#l2Jc|H4y$fki%!VKU!MX&^*ph^0d`P^7LgX&BGT4&^+7+DkQKn z@1*(*0~(vjlPc{n^=rajZ0ECbsfi6izIbK5g+0$t)^>Nl9pA9j^kTqbm5>sjRuvUaN^j4nSxf4Yjn z4FSC;fa%SmK;qzix(v;6CwI(^o9@nEr0=ZUp`40l`R_VpG9Zh|9WuSP?IYzgvbKWv zrZ4C;VAls7I~laBX|X_ZA7PzQ)F}*$-3MUF&VkEH23hoR^og*tjd1jkd`g|p&})_nziv-`32QX_ob{m{<^gvyw#CwhiM?($lHS=U{xc zr9K1fwyN#KiqcpzIwLhUGxRtiS)Fk!8Qvueo%@i=547j0%j_I_a#H?4xAb%1!XIeQ zQP&$+I3R8nX2q_Pxd1EHwhz32XVX1 z)phh7++!ap-6R3ZDap$R4PGA0tpZNmNB#x=ZA18ziL;;|j0N59L*H^_rXS4mOQcO> zKSYQs#Tu+DUW6wczhWMF=MH4dB`mRe`P1`XfBW_C@6T6l+FW0|YJ2U)E1WgjjeQT< zuFsB0`ZIm{`R(@}eRBVOlJn>h&H&ZCU@qoCl zSbG00dhPzF^!h$lZuu$k-=`|Koc9H4+@#9QSGeRWPyy(vqen>emrNDdPg}l1gCjTk zDgEOJRo0`;-ttHI0~osE$V9?_!BYNaOp|ux?D}_H!%yK4VhzAnyB1oG1$0Sve+F~k zi1<1QK}%R=(0t&37K`_~;{GN=XxB1Ad`R!6rPBBAsR?qQ^iN48?hjrgOl!PYl`4{n zH^0)>8)SM!PoCJZaqXVtB>D*>)%M1wZHMWFC*8}pY01ksoo z3ouKU{zU&-FrNxcL=0!oBRxB@QrquprsCqS zC{L;w3rCHLwGJNa#9WU%F&UP)x5m6zPi4Jj({bLr3uF(&WgH_=jWbDOn#N3xVpyE@ zOe;WhHk`tGQv5|l8q<&9T7;#l=fG-m(7XfMp*lqtN0S;2_ELp~(8N_`eyBfY(yhyD z)w2xie|w&#Y_ZR!Zv7j|=RrQ~P7z))|`z(7x*kU&2}4Xw7* z^bsE|-6V)9Ie2go`iUg7D`|$I=Z2HQ1kJ`wG^L$hJ9K(YI=%j1-tRn4Nk~aa=)%&F zg~cIcPN!d5*LC`Z{?O?c$*NAjwDO&P;U{(aMOokJ7s=L=(2|mn;)Nl_#i64+y)N$b znx5_SOP3Fh{>ulQN7CThqG&pQR>tnS79(*?3-&Q!$_m`GZP&|iVI^*$EJF{^uh^xO zlfHYYO((J2EH$*-HqdGO1T@SLOP$$SAzm=X1cF8~7+LMGVXj^rmO{o+=b!2epV%?# ziL)W+k#Z5$mEESgN{g;PX$!mUiR$D?E?&Q$a_Lgar|e>Kn&0tnDYYk$)PI?J#HaH5 zvikyeAf3vIWs56JkS*?joN*@w@+agbnw&{>^~)72UJCzG>#^f@jvv2s+}K?cCP)-t zeDT?5>~Hvn4I8(tU%v%a&#&B(?tTvr6=*RXcN}XvGGCvUksN!T`5}5voi%f6Ql8#~ z-YJ2e(vv59O-R$r=`l@mw6eHpaZ-Gb5s@*mjoH$;Obp0MQf?yM62wpDndJswq0mUo z+jq*{31c~OJ8uk;S)$wT3;K(ZixC?B>JgcVI}*Z+@YThttI(N}$>@#0?5>=}S;I$N z$PuqOFJm5d%+v>WfwDAH*aQ*m(oZI!6be-I!i18^>KrY4n)Y;b)7QN;siu6=IoD)~ zlV&X8?3``LZTO|KsYhNs(nJ43dT0-f!b)h6+6UJrN@lRkMn)J;@KOx9JZ5am%0Gts zyAALiD*bGER1(=8d;4)e2S>REPk+Yu_3?ey>xx z0=?iJ1D~)?#;7eYCZ?fbX|6gJx|v%i%;;sI8=5yN)FiEkiOfCz%=A^ZNe%#}!YRq= zbGoQnprI`}Dp~ucsU>Uw+tknmdw?2OWcwwWIt?0M3%(gx7t%;gEdT@kv%|VjnI*5A z7iT#0f3Yq|HJ#sT^9BP$QX|bnl_c0eZD4d6U2S1BllR|XV6c1J+hV%f7}+#S;ss3$ zv7GVjN_8PEUgS$wa|gesFZ1+h3T|U~3t66)#OvjgtA9kScX_*}aiiCYWm`@qO}SFp z@}2#rzDvt08-i98Z$Cdg4p#|ZellRwrajw6)X(t^sy{Z0-h}$U}uIggB`L9a;r1&eD?kGd#?81V!kMM z%*Malwn!fmXhir=$~r#!7jF{G!zpZl15ORW-K4TnblN zgn~5-wO19}0O&~l1C-2%$ZpP&%PPaSjr@AbTH5;LKNrduk2~ODf*%W;FI*r$^|l&4 z`uHhsxbkw*R6Fy?;o>WMp0L(P?A8Y9dYFKHv9jVcOgm*`V`%jYkb=!3`8ugFc6Cg{ z<+6t6-SZ~tH1C!N#ri>4zUt&OdGdyOvVl(D?(H*B_^H`PkJD+rCU_CjWih~HAPW(Q zRoIHx$&bm|8Ljd#uxsjAPz%Bti$`*DQlhJ~YmkkZzhm1lmGgo7 z=y7Jgi>s?|Yk#%>p*4AAlXaSlqnq1Edj}gwvtBxVI=LiTZPKFp1sduPCX>2&CDNrs zK@WD-C&IO&+eTy*6_d9E3U7r7rcmL@;6zvKvb_T*nvHZCm^5^?i=(ldiHVz0+t-2{ zgR5MYF=<%Vw6^IgM@ITG%i;ZSt?Te8D+g^IZJl<{Mq5m3ShS<#$ixwxTf29|9S*G| zP!YDQK*M$>77L(J@(lx?sX&O)GvdjTjvUEe895_x#DJ8cWy^#1ZR&r4T1pmE%Q>wn zUp%mT=Zb%Lk&n~(8C?F{1$rhr%AV-DvXO2zvpahZd?AM?>p18DPiD8md8UfIHk;&l zeVtFTS)g5DU|N2BexzugR+f)*a&~na$z`wIH6g3@X$$wqnZ~k(D>uX^ z504wI+e_})vu9RT6x!S}CtRyduT7XT5$OH|fdBU?B|jmKTh68=YiYJPC0V_8*XR*# z|1i&8byN2#2gjpH3jBC&Lo_Q`l!m4<3d0M7+9c@Vj|t8%yf3GzjCL|dZnF5k2JWi^ zh)}$wr$H5*o6Il7Wdp_BMeXleQ4wZAe~I%%^rJ${O`G&a)I zqNyI-@8VbzN1)qvEWrw8G6s@G;@DicFQlyD1-JR-ZpG4M=2s_oz3m`S^lEGAKif@B zI4MTjhd|cH(*FoiGC;+uDNKwQHyWu}p1q0R$;1nrmxqUYM1+P`JxG20=oVb-ghS zSWWXHh{{w4c1HdW+y;)59bCBP%?vks#LKW2^4%Hyzgc_3oNi1vI9|Gi(idC^6SP;%WwR@^BqDTQCGBVT!9fJLf zU@-GeDd5Tx-BTvU-F$PM{ZrbtsVVfGcE{ATGfA8FEM0VvNY$>X2OBr<>Eg5N>YC!N zKi<9j7;{RYvjRzYa)%5UIiLBZ;O9E?e>kQ@uVp8f)c=z^>VJ1b37*aW!vQ7w{FrAI z?lDv3ecFi)I#h&(Ye^t!U)UG6*uGL|iTe6*-P_t4_c76z-WRBNo<7bD37atHBnctm zr(0T+Nb}{-`1=N5oxA%~i^eLiYkhq~Bv?)P$RBEt5dBd{Pq)xk^2Fe;kM6zHr*_Jj z2E^p~SM&=b&IBNJI6T><1ZG0Kxs%HiO0ko^t(U%x^<$FTLO0g$@a?wrz%4>MzPe9_ z7S57Br`x47W_I^`K0bIrm#!x{c5NT&oje21L8aB>GE>Hl zPDvb}HY#ImZv1EzIG;nYXe5mMzOciv1z=RSm!HCR=xXbiOvu+P%grst=gsV5_2Y(&S0C`a^dj6}$P`Idr3+~SZy+q_RuZNt2r5n2cRzaPdZ?#&U`X8H z)SvFauTEdQ_6DE86_6}X`?*`>Gc^naC+UmMJtfWlsay}px(mob5Yw# zG%~sCz+hMhtE2M)jrx>nY4iWQ6I)lDBbu#aAV=jb#x z2xPv{><@=uS1n3Vb(uyX?b}chkZL}-&me_ThDiO(mhy(Rh8wkdBqnh$)vil_pzBZ_%aW%ucK_b~3f?TT|DgRu-Md zo2@Bf2bA;f z9K5_^2q{+1R%w~Mg08;tFmdgd3!7?9(=sl;CA2zkOJ-W;TWED|{wwCQ0ewbG-B|j~ zzsx5nfkpz~GAED%K0`LMbaeNbtJ`jSh1f-9WKW%1Tnkn}+LcQGybXnI>cq6T3$tz>j-yAd9ayxurh^P_u)A`KaU=eozFO8sAezYI{w6M ztBiPElxN8_%ZP{D-xspLs;;9XJ z8eG(RMpZumNUW7-B|Cv4YtMtktMM?H#BD9OE4v}u8IS_jz>ndLy{bZ+iw>fj7#Js2 zB?YTib)kT)aD=jz)AX6#dRh$t(O857>5`;eY!-^$1cMEzdmiJ%lry;^a+i8Z%9qhe z%dtDca95Y1q)E*6Q*B1U9zlLT{u70g=xplCj1}Rw2xE!i%WpsVTYdFzdldBz)_!wQFo|TRZx*h2f5Ac3~m}*qG z7x_k~xZd>d9$d8WLu@3gR;K}E*Vq3#YAy#<&K??|Ln*^x?Mb< zkZ#FHhKo1pX!27I{g&UB(UF(Ln+vZGY#47?I5|K6!0Nrtvi9tx`SZKxC-D!44L1oh zOBoiEo3ge`mu+zaB$f%5DpcY> z>5(X9t*{2o!MGnu_UWc>n(#lN;$mEXv}gCR&pu#HeQChgf1s=G>28{DF&`+LM?FHEtm*e7wQ z+O%Ogi|e=U>7X*QzdF-(=~5Pl6@vbnvGXE1&(Ck)`3&*?ce@Z2GE4$jWriSXnBSds zv4z?xkPycPla=W72eN5p(0A!COK+|YWl3?2b2oVIuQ+@2!as>3nfg#lAH|*9$LQ3? z6_?(<$>+8mVU!Wt;>jvOnE}ip`UbkFxRcGrQjz7%ODh+T)7R4Cy3^*)yHg9wS2y!K zy4Z1i@Q(Q_wwgsA&i_}!fZg_QFF3ZDD`-2IJ=;j)m=u~nYUaYix!p43ga(9~+d&zz zEUIH03tL#VeZL|PaF@zQ>YXcY+16;uDYj+Vb``Og-U9YEEc^4#*fa6K^jdTWQK$dG z8-pGBI<~~Gg-)%E4%I~RUz?ctC+xLu>wosB@@sxp>$R+cg%dd!b-o{4d{IORnDvH; zVx<}aZngjd(iNBG06~Hl&ngsgZxM;VNq3{T{q$ZnrsP#-f{1L}5q@afkyWhkBJgc{ z>*-T*D+nmtBMZUzUywFJvK@~Q9k0T?Tlynz6nY)`?WcYE-u+YkC8;i{4`Q?IF?vw*b>=G6`){V76tMjRf5}=NbdU2t4#~8b{{r+ zNP3?4pi=c7I7ZIie3`;Fz@){E{*HZ@56UZDP9oVJXnjW#$oQWz0JJv70ItlM|5pZJ zvx`|$3aCzu7rIlVn^d^-F5`u~zyof?!*DWiO5144aq8$VzL?>CovHQF%mJDAxMTkK6)A~;fOoD_CNiP4{nH|b> zxET_Pi8zX;{?bK@n#n*E=RLefW5~Pr=*mj*=2YcePL~c)t|327qb!<$G`7z0pdq&x zcAgiDp|D`|kc1n|;CZ~Wjhd3_^FR0kqb==#in$@x zqX9Es7%doH5(d$jb8G33#QomAsI{9%k}_I~2AX3TU}dN2FLXMw9K4~Z6R|AbK&-An zb`b3~Ksyt!3$SVuV)Sk##Mo+(yOFXB;9W@C335hRvCI5VSZQ8R95r!>VSN<(jH2i= zKfi@Llf}zhEjHq(axtCd&l`Y(1Qcs-=Dr!h2PuAq=YA9@tHDs1L2j~YM-l6Xv?YZxO6`4+4i94BUb)aq2 z$5d&?e{DvU_dh3Ez1j?PksVw20sU~>Hsb!lHsVMk=sEgu8#n#8|2%%tT2G9B{)@71 z-GD*z12&;PTx4t;iYG825DaJ&27!i*PK2A*q@HK|*jEfJD ze%JXH2ruNSF?X_*Wp#>iBqP4FV^dRcX}9rl`rwK>*N%jpwqKP#Z!MkZDf5{)cxXsa z{L;=zc9L0d(Fcha>GMW)dfn;7tc{sWc*}}&3^p$*H_+T|1M-u4UI2O2J=u3Z*o}yKxV(V7^LDf4`svGGaM5mGe z5`J-g(DZGiNSNa?~=g>U`Awl8q^`y7f-+URNnc z$S=P%uUW(Mhlo@n8MwLZ`t|b*D^{$m+*GxY3#EV4m)Y6eu<>%M3i=hXtl%dm&#;h0 z4Kv6cMPJfC-aU2k(tBr*L#9EYm-zxyGeM#Vm@*ic!xmM%aL#yj!B^%h;M>v?5Nf4$XmsWFE+wn#Vr*x?!@OIXKx|gnU{J>$|1zBeK*f{|NRkjEE zAqf_S=tS1n4=zZI_BZH2`^L5F*Z1vhHuebc4>Km~#*{=XE?Q8vt$cf+e@ea|$8jq= z5dR&={WJQ^g9eZn93M5v+n)@vF}nKg7ni?yAEbaGF53W?m7U;D5pK>D=^_@fWfzyg`W2Zho?z2~1<@qzVe-Z7{Mcc|H0n(_U;0EPi@6jKZ5;~b2a(%Re zJW3d%rM0@}!b#MJewN7H2JzYyfBks!J^>ld-2IyM|{}bkz2mj6v7vm+Rh`8_r$7M&CVenyc9VTuTf0`}B>f zPCc)EiiCdr1yY}1PkRQ0Y1mt35Xbh+@i-o4s*kEnjJ2I&9WeCK2uIvx;B5snrbN0i zpD6k=eN}mM#LcTkTYWi+j5{dd)~s!Q@q#DZpp73dU$Jy<>7v4t0iFfV4v-$Cclvn4 z)L7nXMPb{=ytvD}|1cTNjcEBpo-+kl$q=VxAOG(zk9+1YOS+y3R`JO68_Pdgas3 z5Yv&$a{d>Q(#N1gp(Ie!3xsAK3aG}a3@Xt`5cC8_TCN0@1TXm74A%aGO$N+bEi=S%cEn^J|yEd-DSIQ z%J7lt<=Rd9A8g9qHhLivk(@cr(23dUdE=6`m+nkmo>`7$RE>$)M7IGb%e{w=3@gX6nE>jQ?%%AuCpan;!r2x^}1(+eyE zOt?TNh2D3j!~FOEpn|D9ZReCx%VQJ92;|vPJ^I9`@KF|iz7uZeCQbjfav7k3!@dU; ztOV&DHD87E|48ld{s{4Xjg_y`0DTXb>*YAmkjp%_v$4)@rC{V^ zen0dHV)8(Ig%<{1(wpwZ+a`$Cd%|ib`n`^UUhf{}b~=B4MaN!O_O`UpHMY|yl9R2v z7M8lm=>6m7>Z@azF~7N-FLj8Ei+7k;@6=WXzgC%I+U#)coW()>zP3$n<0p9}?1`=Dm-- zPd_+zQpcz#r*EWr<E6s194C<`@y2I$gq4g64`i6*~o0shHf3 zo*4;Ka%VS^CG>;EM>7^YY~1r7x7`-yv(l;)x9u8EEIz+>C3RnM)e&RK(fGK6(#qMT z^rrrVo%xI2owsP7U)r$L#*sx;xS-<9)dl;~D+&NkCNkP@NS?8E6Ea4Gh_MitdLuXv z07vbt=SJ(q4jDIMY6WU5zdilV%=cF`ygOinW#zbpIpOP<#uM55#e0%3?OU+Zi2pjw zKVy1MMmhc6=;+G%=ckqw1}A4GE*_qnU-eJSimB_4&726K4Htl*{uwh9XFNMF)aJX9 zaHBSMCA-QS$#qZxMTY>>0LjXq2^SFXa5Zs20XcNQb$gB;l@vD4o7Xt$$&IpcxiiSI z#jBJTH(&~FrUb&~vtUUEk*X7k1>(e9Gr_tZ7Aq0#jvI>zv8n;4?=ZZWqT3xe zHC{~7C539-<^eBB;ZYH@NsZhg6>g$}3*4-P-Y6n&@H8-i*^ZZ}?{-6O&6?%`o2sr~ z@6d6Hhgzwv7xY-^C(4WCMni@DN2TH>Wtn+aa5vQoEzvj+!uG@dhfzHqQ?qGVS?a=; z799gEa{eYqQ#D;$f9&h2&mRTbEd|?+z^26GY!%dx2R9!#?gvrRqC>|?zij(CV2l$t z6ptfmm%9KR(OyUZfG#K;*uuXk{f8$UTmgcLFJK8r^r@RRX+`|1inaTTpYPppVtt*F z(Ts-lOt&yy$yX_(Qa4ViI&NcfxN=d;WU{WIbU|6u%Gq;fx;bMiPr3^l2vcQPuEGC3 zmFK@t#c%p=Q?bl~A0=iul31;qMgg=Ch5!pnGyPvUicl`VVx#I*e_$pvTF#=CgB0wB z!?&7OAN%2k4+`YEHX4^5!HLT^g&&rNV_6P`=22R8| z!+#=u3=g6Uh|h+16s4k%u;0{Eqg0_2L7~Et-|Cl{4R-a63<`YlV(;4x2Xb4MD6Ff> zLc=`64SMS$Gg3=yCsNY*9|}g_y|#7Xq{f7T{OqJ8dq5D*rnMiPBs3&MtV=qLzE zcZd|GapN{aoXNPHluuZKQE#s%2y|;Vq z`nN4BoC-f_c&u#tYt<2Yl72VA+Ix0r8DXs=Q|Wq%F_sc30B@R6xakL*PlL_KCd^#D zx^it(>0mZt>4iM{^oPG9Bk5mT-Xk08>l--#8#k^U+;9vN&i!QBcceF(GPb^b0d z(>i74N|J&Z`$;UwH)1kO82Vm_={F*Z?Q4pFu%LX9(jwpBATMisnPC!bd z^L2t^E%P#oAu3ZXh(+eO`Q*~eyPedQr#Cq3FW>p-v->x00{a|rmMvgI&gEE~u9%ta z><)0q-%vp?eoJ$#dP*o8Pj5)?bPS4NYG^uC;zSmxYad2r7)XjvFPN>4j>~MTsD2oA znoRacoa|g$eqqJ2wETj_ZzB*z(DafISUt6%f9p0v_XFrc@ zRo3&#qYI39r=-w=%yCnujX8E(c|(o82&nWn07Y67G`4?(tus(+P4Y};l^7Tk;6Y^U z*sj59lC&$2+@XfW$2NBYB7fA;I~@RsT4N0~{oG7?!& zE;TFf@WK8iky+CXY9}CT%q1zNG%9e=hDF@0XXpLFl67Fo9gyYL>`Ya!7-8m~Sfhv% zt#*!7OES4+fw(*=^lDo(Ir8483yZ(?Q18$b zxgn#nM=`D0=G6;+De#9>8P6n%AjDW|4Qd*JHGDfjSl-6~f)wIa!MUijYDiC>XDBaR zX*@XIuD;ym70bENQYX|5o01e$GPolD*Y|=+Bykb4ZJ6+c5VHr1yK2ChG8bq;brfo4 zM0pLvTsV}3j->2VVp=BtM~f8oorZVx;s%<>#+gl+)qhLX()Drv6(J#!myRkob9tjj z=PdD#F?1PXF(P_YTp<439#PxZsMLD)gcQ2&0Rd75qU+rajnwvjv z!fVlXlt06}$b7S8WXo2gH@FosEly~OmCPCrYAfspF;xC^A&OSQjM2+QzZ(zU`Yf;fN_;KArPGgLX$ z@aRF6%5RZKkMYP9lexLN73T!?^B&f;pQMrHG+&~KX`Iq8HZs(DQBa_-?q*&3S=$-b z@fLfDtHFL|7`%CGnJ{)Z+c+H05sAH9-?Tvv_IAPfg@k9_u7sW99;L-pG z|IRywi|7J=WK-dakp)w7&g&V(g$<~wnOQcsYJ1ukkN$fKwq4OHt=}?k$4)xrOWnvRKQrKXKZ_GlWTf`~70h_IE%tKo=((AoiY&Q_p?2#2s+ z7)`nWBItPtYN*_7hQ|Chz!5)x$mod^C;NJyt8AQ@mUZ!}M|f;Z{O+}d3yaF)YinzV z%!^5?PD|UGGjGK3OkejMGq+#vg{`|30k>e( z6lk&zOmsTFu@~QH1@|#;hl)c~VzyI$y~ukqsjT0bvQ=waQYS7e=SQ^d5!fzisQLKm z=Z+{_IP1kzE{Z(IF9pw`Zcsg9hyPKS0*ru$G3@LDJ3FqA+qdp`>colpT(Po3Jf6Hm zO+s=Gy+;od_r`M8lAb}y_DClvyYG=V?+@;=QVuXzPw5vI71pskL@t*4qkfx5N56^r; z@9g@HK9>BhOy7R6=;`|MJ2Aw1-MV8W=5sRb=*Eo$=(CRwP-VlcnHV+~awrvt5$m=p zs67f3=8iyMNZ4jLfXU{XC1c8ZpXfE?Xu+31qauNSru-u+le7Eu{k*9?^VX-JlbiwR zSyfxx@0v(VA@3C(`Pc%9)ict*Ep0=P;mRsS&}Zs*)^!T|3)UsySFpfX(i19+gfmBM zBjuy7O1(8<){er#(91_K|M|YtRs{w|c?i`#`%ccy4+&_lPF7a`U>a%Z@6X+gv{kyq z#`f9Cs(rZk3-U(gn39)DlfDDVwLxMP!wJNLL%3Icq{DA+maH`Eid2l9$S7>(_o!MC=nTHj_-^ zx1M;wH?SkHs2nb!N07cxfDDVA3f#bAx1!>RIk3T)p1`U=75tN4`6mh={?=<@aqr(h zdFzSQX}$TAY8Pv@Dfj7~M`cl7hoqN&UGGxK*M4sz6pc-Cw-l%J4LKIcppZFHpYvhf2z^ z)zaz5QZ#B8{o`Rc`@!U@m>%3{Le8zeOx%BHG9a3meOT?Mft|Q5*^$DPRWID|(KHxB zJJ1bZf~(~DGfh?VQrx|ZV;Voq&)hq^`LD$@Yu44wUnemPDEjj(i(`$*F^mt>w3v~~ zMIWcfKL5ja_<#YGFSacv+?Q#bt+K5@E&TZQ%`ZOu594!qMc4@gX{63U7HoX5pDJC} zmTVIeoP7p+d*8ane~12S)9Fefab6#J^LsaQdp9b7-;A+fRdIoE9ICVv48eRzf?27m z9b=J9{3Vl`w{2UywtR7%_rSi9!T0Xn;f^cgxZ_pD6$_fmdoIz6${JfyTm>Q`(vPc4 zS7GRWD)pK9_7wJFK^nV|(E)ox9TP+{xsU?MA7An3viI^=9HZZSe(gmQx(jX|e|4bzX;)2K_j|= z{bIyS7eRFw=2m$ZP5yOq?P*tMA0D%G(cJvo2lf^;H`9lit9_g+$Bvtkn>};p?82pq z4Ws8&8<`bPDqA&YZ0ogYi4mMUBRM%bd9?m)+4#|ka|Fbnql%Cz$=I%};j4(Uqz?-W z3EU#|gBKG*nO+9W$G)|ee)HS?7Sek~=B_W7EPY&d>F~1aw;!CLKhCaOy^>Tn9JT_= zyU(y>svY$A%af*_Sx~qfnIC;vbmzhNIo3d4h4~l2+>qHzY;v_nl_@(NlXvJmZ1&V; z3+Lv)cY5`wv*&!aZT_i=CyMenHa4yvKXu%gnKMbhIm>zp7+1bLcD%JbsbNAw;mvQq zyq%snES+Hu_7T^*Opt-%!H9DRm!7G-FQ5Q^7cnJv>(*jLTUu89S^a+DhQ0Kg76HTh z%F6K>l8~3Bn~zO7Q`oS_0qCN4>{bRe6uo z%%^8ByrTOf%Uo-9o_e;PTKaP=9m!oM>15vKL%#E-(+Bi-V;4iRv*ulDKCAfI2Lo%o zhBWLwkrGXD_tdM;u)~)Kje~tyTS?T{z~Et%`iPzxM<)hHw|J%5j6Hz;qixiuHll*1<$dgp|7)*SA|(#@&7{!BzPUH{`jJaz@G@ z$h)33;k<>A=eGG_%?)K5Hx&Yi% zImtdy<$C06T1)GRBYmIK;@x}F7vwtiCpF$RgZZ(_s&euVjqT$ z%C^t0rW2jK=4OFKQDCMrgksEopW@%q{MaK zaoU6Y#dIUgv6q-&706DNKPi9I{ucL|AmuSmcS7ay4VA}$?>E4gbwoCXLl`?qjW3)i ztT~fl39(ht$K_H6;4hXEx!Uy-w`Tii{@YzQX`bB1f~+fP4EH4duGX4eN_{Ryd6H{T z)C}P=bF%_#0-WtPePW4-YYexAE+%@Dm47c^Ouxs%eS)Lq1Q6~ttAK7-!&+wKU;<3* zT~DdaUu(I1WVp)wjd4X(YA%WPstG5f;Vu&Necb2MkLmk2=GH(2^~2UR$JS-BCf4wo zSM3zc1%p@Wgx%2no^P0-U`8ag3AcPJmBTyT>8-r(N{SYjkj}SyhXn=i=Dc0$s^d-U zR&O#vxW+p&!_vyZ@S~b~uif+8ukj8}wz77*S5xEnHb8TNZ#}f#VzPYElG6VDO_GCXylZRD$a}ZmzGu0;cg6Gtvt3<`MhzkJ zT(68yBd+UqSJm634@*fI7-Fhpp|`naq|1U4O&dyT4Wkoc=*hXd$ij6N0(it0iFk0hml^-^@}dyO=B{FlY!fTL1*hs9d^g zT()Qbma>$7`So<;F30}P&Ko!Wf0bPcT$I(?=e+MbGXjEw3?TbHC}8X`?28I0prC9T zxPW4SxZ(~fh?0ilR+?zJBjOrrnx^=A7r8^PFd2gi*7~3X5&YA$_q+P}wjy8YyfkTRFrVOF@V|FOC35GR6*r7Y}w1 zcPpv=$s%%FE{o8N0L`@K<|n7{amY;4VE}`Vvlsx7CC)v>d-{U!gnWJezlD%DvDk~C zLM5WJKK~`biI)K}PNOnT6JnU+Z8rMfpE!aAm*cpf@P#r@BWJqUQNE>s3u2T~!6`Vm z!H5Cj?Tfa*lhIV%Nq;3*H*QqYX%m+JHzp;grKo6gO+ihvaHUzdrFnk!vU1a;gwQwu z??ZDHGlA-c00zS7yj%sFVGUah@f0r8z`qDlbf-sxx3k(KJRu>+dw^DhPGqY-r~QT4 zzmR#l4)MB>A02D9!NzltO@1;RF1)Tgs2hy9J>+9@XK6pofqOd%$l-1fzts@US$qxm zlNv>EPz$7S8l>)r51)~{k@c+q3Ra+l9RYZ>%Vou=pRlTQ6#r z5$_zjPyd}1-$x3`Ewg%I8zk@^a?!6l%0YHucefgI(3P8dd3?R*f8pRqh7Mo>~2!vCtL4n)Y^XGB? zEYmt1#iIr=EE{9+GqX@}96jE7!e$@lSvqtmnvoA3TKwL7tfzcMYzls2>86`EiFXPH z^_Gr_$#C{KLWpDvkt}O4f0l;rfGn9a%|tD;30VX}WiRNP{mtCUBvOT22kz{-4F`E#AKkETLlsC z3R&mrs>F3Z8+&`{R<}8TaRK@lgR3eB4xRp;AT^8cM)Y}ajV4-%fKyb=$PX7J- zdAcYce?TYQe%RmDP7Yw%ZSF31=-gOrO7`AjRqVKuA&5=cSbl0mWaE5$u^uokjchFC z0W8vMJ`*4U=s=+BIV08>GLpD4goyw!afiT_0R#mB@ni;KrBVbYxx+{T@zEV((CteX zRpScX$f}w*0tDNTvyPE|W-YCKRHlYSHDl>=9ntv2a1zs;3f(hQE4v`$6FDe3TEcmaHK}qAJQ1!liLW zmb`JovYngj2t*wb9tQ82*q2_7=ySBr$(xJ|xMXQhD`N}%v?C&D-PL|uT%1iQ{fnbR z%<<*KJvn0FKk3&FrWV%pTHr`O&mgkz@fLH{FE%cyEZ+GLM_G+sWAXalKrXZdhLsDRQXZmW{`mp8fUKT9Bw zEF3KA+FHoiI=i5ODAMmjJLq@zs}F8JvQ>Y-eDYN+nF(4onA@G{^+q!&<8p$Cxw=D) znZ>9Z1Yyyuo2nWI?g;w2?Z@BE>d}j7XaD7M_t-xEyc>u|XFe)Q4Oq4e8m$zs4T=nj zo42y#U)>OZ3d26Z>=a5R`suXw%^9N0dg7#GTT^m$dK~|io`I-06 zzW4s|3;HS_Zzm@YS9f<14EzYEpya@Cz-S2$kJ$%gi44~iG>zeTS4bw-M~<{R+Kvwx z?DKlrG;Uf>Bp#4W#b2C+ZCtd z0d|y4d6jj9fFGg(Z!{)1Ozh}PMCIZCdi3|}ybl6*13xF96;p9e6a(i+P{z}TFtL)Wkdt%}5FAh#$qxSNmB-h^uwgjp5a!GY<5;Me^}oC3 zSFPH%c1Hb?&6+v!p*AH`{!ichuZ2@Lyjop4e-m9%R5_+(I5YG#bUu(+?+Oi6JTIW$Lx1lK?WB&@nJDn!UPpr@b!;pk7` z<9N@v%vQYn{9s_cNBEMRMx1{ZL>eDC-lJsckig+HjxO`d_4i7PD>ov!klfJtj7?q_ zv!NRoR)Xe%aYslV>e? zIolgFVqE75v09oA++|JM*t8KnCPnQ+d0G_*XMoiJkd6VN8j(XmoZ=Q{JulC$POEy^ z74t7X94Gn@a&8&uV{s|wX0t6|@t(QCu^S5CI{IrEhhzEMmiBgH6fGoyL(*NSU+Hu@ zHzPX>xDyVb4sf7AzZpnODYRp)ot1L1upJA+=b01eQ^*=fhm(+ig{g~OPMdp^`AId* z%cSmTYn>ifRXXFBj$BgaKwC61>y|0AmgnxaObHbBJRbz+Alzp7#ZunX*FgCjiuR?g9v95jyg|@H*yU?i_zr z8f$l6&%AqkM0D6@+XnK(x5-#CUo~?3 zgFB0k4JQo=70oM!j0n0cs&Mm~-}lKJ(Ymn7NBbsmBd+$GD-o0S_P|jMYv_CQF6|J4 zZHP-} z?$Dp+@nuN=Y^paGQq3Fd8{|AY-SaZALUI?3^e6lQgxGEdJ6U<))X7#|%Za0>4wF<4 zCc?aM_^e_)_mKCl6(0;{BNGEaqtP(Es#kagFVFR#1qrr&Z3o`C5cO0a0*;F(0M2wo3s#A@CyGEQ2j^NLfK&T6olX#6~eoNKmGc{ zQvFc3&%mwgA%U>gWpJJ_ZVj=Iv%@Z~#aouOqg~mIMRO{;+Le**w76HZGN%5H#4xU$ z?Slkz&r&$DZ#Z9LjE`$XJZRI~9qixJUHC1t&@Vt8U}cjwWXg=hRC=qOe0=W4GW`wG z|C75@jPiF*2$mznP`iPJh?QxhD}uem9qNEkf4{Dvu-I{0i#1c@iI1`ugoXnQeD;P1G1CUpc&H>>u!2Kq8$5 zB+v2epC0QqOwSLEThL^QrF-?D?_KhVbhdjV>g!YLCvaf|VD{J%vrv0o3>Qzjb_M`V zS~(1INIfZn9n`o(l1dk75C#xrjIHtlJ{c_K42#gtzzQ*YoWgju2hB}f`+Q}craUpV z5pAQMyf5x>a*n=EDkHtLp4jVPNcgP%jT4vfvA>KfGZBkdxa=`j2=m2ug%Wusr5F}Y* zmvO;hN&6Q7LHvrrj>fclwhXkEV;&h^F+>dKF^(sUvI=(aG>c{wuIaNI8; zw78yJqtBs?rrt17(wQCV{GD?#AC{opEX9D#vD6z>O@Nk$!*$RQ`W1_2?v7$ zj7)GP21eJ-G3UnJa8m>D^qhVKpc8*<8(=3>?MnevSmjFI|0 zqYc&8ZU9V10~lOg%|;l?Nq3T_Cy$&lmUKMXtAF@@KxohPb(Vq2O~vfp z26w(DVUhm$MWMBOY_jq)Ijh?yeqkI)|2cdI__o63{{_3kg~2{E>he?L{}Jq&`6RIC zluyGRNJnwlaW-RQoQVKJAgrICi+nW75k4cs+@c)()lRM+UjF1iy_Q73p@mcFHeWAx zI?k@i!nN+@k+b^kn}7l6i6)ltHKNv~A(f1$vx#=RIJ{E%4$B)GTOVXa$6UDG&)Gq( z+INipo){I?$kW6YFY%MY^sT;QG862`4gJ&uV{>zH?h4_Y@S5UBba`NT?;t$}Mu32A z^gw0SKBjY^jvUEaanicVa(Yo=g=Llcjg_m8tE((43X7&&R#~5rjX%lRiYNpr4W%3flp0<}Z1eWh)sW z!e_=}c;q>Z0g+>uA3Fd^qWdLm8X;r1QCX|SSo0uX$z1HC@>24bri$7Re^Z2JYQ-=`Sf*AW+pma@4w+g)!bQ9%0LKEI zD*C|M@BS2Bl&$)VDcB-)-@r6xTU+--SCt}jz_6jG z6N6HR!FX3QQ0t_1^B7YEJXj0!IMN4EpAK$`M46|tk zGsbKlpWQjXn)G=VrjvOS-dTz-|RApJ|tV5qMVMb z&&UZbv2}SVLwCA0DPKAR;PDGe7*_jDYcnkh% z$7)ua2$jDR*S)W}CAnRr7MnJnZasK<<0fix%{|e5%RUsh*+dbrPk$*Cq_yvi`hLUd z%O9WG@cpQF)=EMl>9_3xv6k(=iZX7ce49K3FX#4O!}D_f{Cv;n`oBrnJ3AF)(8WN#`k<3X%x-kH<6m7$D^fMh%1bMOXd1$%)uNbYyY$fEkWbPhiL`MlcGXH1CxA}zpZ z#i$l)k_Fq6f!xZ9OWNdtLC#5~<_pXQ4O*5rbuX=Ta8vb72p<_2J2Z*UP$0y~-_3zp z+dz&~?36X5r>Zq>?7QX~-&%b4eJ2lvSweV548GdgslQ3jV%lg)rO?p3@)ppug17v{ zxCMCP(;3}!3+P$41$?_!E3UKNIa$ojik@IY{BY7* zeNv-%{Nz3q5B@KRSb4x?G4?M^jl

EYAD^IOboHS2wF>WKP~JaSvSR z6<#S`Zb|ORnXhc4gUIdTiK{TfR46jHU_xi*VOSs+4|SB+hXu;V$?gi4C@-0vFFS0* ztrfZMh8qmzApxfN30s}J@Jv~L(8lLqn?0vGKK{MRSQTt;mQ)Wz=@}jrr z`Uxd7hgHVy7k36t4i26i&F0ZEa_^t(?3ir(=9jPIy*9lkU=j zjRjc|V}n3z(R5U5;LHLQROT~!9?r_5gRIN_hAhQXyFaxL59%+hKu4fP@In~9obJqRD6N` zD^ju5Oftsq$_yEP67I^7;43+DqVFpxEu458xls2NB-xausut~;Mj@zo_7Me9g)J7Om(vAX)X ze)Oa2>bdogNyhf0($+OCEo<~oTIj~o(xRz2ZRmO8eZ^IfJjiZv7%@iJ-&MB!`d6Z)yDy-;e&~7n`o@NH<;#}Mnz^8MHo5-j*GIpSCE0?X{uiOY{u|FbAH3Pp zdh+C<{kT*);v(^wVmV%^_~wb815mq>|B`|oQxnK9#=lTC@4te|_{(lMw(zoYHaEB- zklWf0D-Ls)J1m4nAb=8op#uQ(gj=bsG%70YcPy>KIR3^t3guxw(jp?)Wze3AyTfOC%?Je+i^t{nYaLV4CbJ5F*k+zTibI zG?Rrwq0R;LP(Xi^Byj?rOf$(5UKay7LEv#WuQ1MqPqQQud>R2{{L>%vPdCs^@vfAK z=UIm5vX+ahhB5@-Leo!X;$%d)7c(U?B5Jq+4B@$_1rI=HWKSOGtB()FDaO5xw-5ve zU9*0=vwL|QqQ1&zL9~BnyoK3Q`rXV`%mEzgQVqz1P#2%eBd9zlV z?GiZW=jf|1MlTEwijNHpTo9c!)z2p(4w!U`W(qrjqjG&d!~GzI`^eAx6jvVCs%qG; HiOK%}>wX7= literal 0 HcmV?d00001 diff --git a/test/public/tests.js b/test/public/tests.js index 1eb96279e..047bcc3e1 100644 --- a/test/public/tests.js +++ b/test/public/tests.js @@ -2698,7 +2698,7 @@ tests['measureText()'] = function (ctx) { ctx.strokeStyle = 'blue' ctx.strokeRect( // positive numbers for actualBoundingBoxLeft indicate a distance going left - x + metrics.actualBoundingBoxLeft + 0.5, + x - metrics.actualBoundingBoxLeft + 0.5, y - metrics.actualBoundingBoxAscent + 0.5, metrics.width, metrics.actualBoundingBoxAscent + metrics.actualBoundingBoxDescent From 0e4301729eea733f77cb03a39eb2d61205fd5667 Mon Sep 17 00:00:00 2001 From: Caleb Hearon Date: Fri, 19 Jun 2026 11:45:55 -0400 Subject: [PATCH 5/8] split Image into ImageSurface ImageSurface can now be used by the SVG engine to render images from PNG/GIF/JPEG/BMP data. --- src/CanvasPattern.cc | 2 +- src/CanvasRenderingContext2d.cc | 6 +- src/Image.cc | 452 ++++++++++++++++---------------- src/Image.h | 43 +-- 4 files changed, 259 insertions(+), 244 deletions(-) diff --git a/src/CanvasPattern.cc b/src/CanvasPattern.cc index 0d402cc28..9ad5b83e6 100644 --- a/src/CanvasPattern.cc +++ b/src/CanvasPattern.cc @@ -119,7 +119,7 @@ Pattern::createCairoPattern(double alpha, cairo_filter_t patternQuality) { if (canvas) { source_surface = canvas->ensureSurface(); } else if (image) { - source_surface = image->surface(); + source_surface = image->surface.surface(); } if (!source_surface) return nullptr; diff --git a/src/CanvasRenderingContext2d.cc b/src/CanvasRenderingContext2d.cc index 650dc0758..500fe7d32 100644 --- a/src/CanvasRenderingContext2d.cc +++ b/src/CanvasRenderingContext2d.cc @@ -1265,9 +1265,9 @@ Context2d::DrawImage(const Napi::CallbackInfo& info) { Napi::Error::New(env, "Image given has not completed loading").ThrowAsJavaScriptException(); return; } - source_w = sw = img->width; - source_h = sh = img->height; - surface = img->surface(); + source_w = sw = img->surface.width; + source_h = sh = img->surface.height; + surface = img->surface.surface(); // Canvas } else if (obj.InstanceOf(env.GetInstanceData()->CanvasCtor.Value()).UnwrapOr(false)) { diff --git a/src/Image.cc b/src/Image.cc index e02677534..8a7c7d80c 100644 --- a/src/Image.cc +++ b/src/Image.cc @@ -24,7 +24,7 @@ typedef struct { #include struct canvas_jpeg_error_mgr: jpeg_error_mgr { - Image* image; + ImageSurface* surface; jmp_buf setjmp_buffer; }; @@ -39,40 +39,11 @@ typedef struct { } read_closure_t; /* - * Initialize Image. + * Initialize a new ImageSurface (canvas_surface_t wrapper) */ -void -Image::Initialize(Napi::Env& env, Napi::Object& exports) { - InstanceData *data = env.GetInstanceData(); - - Napi::Function ctor = DefineClass(env, "Image", { - InstanceAccessor<&Image::GetComplete>("complete", napi_default_jsproperty), - InstanceAccessor<&Image::GetWidth, &Image::SetWidth>("width", napi_default_jsproperty), - InstanceAccessor<&Image::GetHeight, &Image::SetHeight>("height", napi_default_jsproperty), - InstanceAccessor<&Image::GetNaturalWidth>("naturalWidth", napi_default_jsproperty), - InstanceAccessor<&Image::GetNaturalHeight>("naturalHeight", napi_default_jsproperty), - InstanceAccessor<&Image::GetDataMode, &Image::SetDataMode>("dataMode", napi_default_jsproperty), - StaticValue("MODE_IMAGE", Napi::Number::New(env, DATA_IMAGE), napi_default_jsproperty), - StaticValue("MODE_MIME", Napi::Number::New(env, DATA_MIME), napi_default_jsproperty) - }); - - // Used internally in lib/image.js - exports.Set("GetSource", Napi::Function::New(env, &GetSource)); - exports.Set("SetSource", Napi::Function::New(env, &SetSource)); - - data->ImageCtor = Napi::Persistent(ctor); - exports.Set("Image", ctor); -} - -/* - * Initialize a new Image. - */ - -Image::Image(const Napi::CallbackInfo& info) : ObjectWrap(info), env(info.Env()) { +ImageSurface::ImageSurface(Napi::Env env) : env(env) { data_mode = DATA_IMAGE; - info.This().ToObject().Unwrap().Set("onload", env.Null()); - info.This().ToObject().Unwrap().Set("onerror", env.Null()); filename = NULL; _data = nullptr; _data_len = 0; @@ -82,110 +53,12 @@ Image::Image(const Napi::CallbackInfo& info) : ObjectWrap(info), env(info state = DEFAULT; } -/* - * Get complete boolean. - */ - -Napi::Value -Image::GetComplete(const Napi::CallbackInfo& info) { - return Napi::Boolean::New(env, true); -} - -/* - * Get dataMode. - */ - -Napi::Value -Image::GetDataMode(const Napi::CallbackInfo& info) { - return Napi::Number::New(env, data_mode); -} - -/* - * Set dataMode. - */ - -void -Image::SetDataMode(const Napi::CallbackInfo& info, const Napi::Value& value) { - if (value.IsNumber()) { - int mode = value.As().Uint32Value(); - data_mode = (data_mode_t) mode; - } -} - -/* - * Get natural width - */ - -Napi::Value -Image::GetNaturalWidth(const Napi::CallbackInfo& info) { - return Napi::Number::New(env, naturalWidth); -} - -/* - * Get width. - */ - -Napi::Value -Image::GetWidth(const Napi::CallbackInfo& info) { - return Napi::Number::New(env, width); -} - -/* - * Set width. - */ - -void -Image::SetWidth(const Napi::CallbackInfo& info, const Napi::Value& value) { - if (value.IsNumber()) { - width = value.As().Uint32Value(); - } -} - -/* - * Get natural height - */ - -Napi::Value -Image::GetNaturalHeight(const Napi::CallbackInfo& info) { - return Napi::Number::New(env, naturalHeight); -} - -/* - * Get height. - */ - -Napi::Value -Image::GetHeight(const Napi::CallbackInfo& info) { - return Napi::Number::New(env, height); -} -/* - * Set height. - */ - -void -Image::SetHeight(const Napi::CallbackInfo& info, const Napi::Value& value) { - if (value.IsNumber()) { - height = value.As().Uint32Value(); - } -} - -/* - * Get src path. - */ - -Napi::Value -Image::GetSource(const Napi::CallbackInfo& info){ - Napi::Env env = info.Env(); - Image *img = Image::Unwrap(info.This().As()); - return Napi::String::New(env, img->filename ? img->filename : ""); -} - /* * Clean up assets and variables. */ void -Image::clearData() { +ImageSurface::clearData() { if (_surface) { cairo_surface_destroy(_surface); Napi::MemoryManagement::AdjustExternalMemory(env, -_data_len); @@ -204,64 +77,15 @@ Image::clearData() { state = DEFAULT; } -/* - * Set src path. - */ - -void -Image::SetSource(const Napi::CallbackInfo& info){ - Napi::Env env = info.Env(); - Napi::Object This = info.This().As(); - Image *img = Image::Unwrap(This); - - cairo_status_t status = CAIRO_STATUS_READ_ERROR; - - Napi::Value value = info[0]; - - img->clearData(); - // Clear errno in case some unrelated previous syscall failed - errno = 0; - - // url string - if (value.IsString()) { - std::string src = value.As().Utf8Value(); - if (img->filename) free(img->filename); - img->filename = strdup(src.c_str()); - status = img->load(); - // Buffer - } else if (value.IsBuffer()) { - uint8_t *buf = value.As>().Data(); - unsigned len = value.As>().Length(); - status = img->loadFromBuffer(buf, len); - } - - if (status) { - Napi::Value onerrorFn; - if (This.Get("onerror").UnwrapTo(&onerrorFn) && onerrorFn.IsFunction()) { - Napi::Error arg; - if (img->errorInfo.empty()) { - arg = Napi::Error::New(env, Napi::String::New(env, cairo_status_to_string(status))); - } else { - arg = img->errorInfo.toError(env); - } - onerrorFn.As().Call({ arg.Value() }); - } - } else { - img->loaded(); - Napi::Value onloadFn; - if (This.Get("onload").UnwrapTo(&onloadFn) && onloadFn.IsFunction()) { - onloadFn.As().Call({}); - } - } -} - /* * Load image data from `buf` by sniffing * the bytes to determine format. */ cairo_status_t -Image::loadFromBuffer(uint8_t *buf, unsigned len) { +ImageSurface::loadFromBuffer(uint8_t *buf, unsigned len) { + clearData(); + if (len == 0) return CAIRO_STATUS_READ_ERROR; uint8_t data[4] = {0}; @@ -298,7 +122,7 @@ Image::loadFromBuffer(uint8_t *buf, unsigned len) { */ cairo_status_t -Image::loadPNGFromBuffer(uint8_t *buf) { +ImageSurface::loadPNGFromBuffer(uint8_t *buf) { read_closure_t closure{ env, 0, buf }; _surface = cairo_image_surface_create_from_png_stream(readPNG, &closure); cairo_status_t status = cairo_surface_status(_surface); @@ -311,7 +135,7 @@ Image::loadPNGFromBuffer(uint8_t *buf) { */ cairo_status_t -Image::readPNG(void *c, uint8_t *data, unsigned int len) { +ImageSurface::readPNG(void *c, uint8_t *data, unsigned int len) { read_closure_t *closure = (read_closure_t *) c; memcpy(data, closure->buf + closure->len, len); closure->len += len; @@ -322,7 +146,7 @@ Image::readPNG(void *c, uint8_t *data, unsigned int len) { * Destroy image and associated surface. */ -Image::~Image() { +ImageSurface::~ImageSurface() { clearData(); } @@ -331,7 +155,15 @@ Image::~Image() { */ cairo_status_t -Image::load() { +ImageSurface::load(std::string& filename) { + clearData(); + + // Clear errno in case some unrelated previous syscall failed + errno = 0; + + if (this->filename) free(this->filename); + this->filename = strdup(filename.c_str()); + if (LOADING != state) { state = LOADING; return loadSurface(); @@ -344,7 +176,7 @@ Image::load() { */ void -Image::loaded() { +ImageSurface::loaded() { state = COMPLETE; width = naturalWidth = cairo_image_surface_get_width(_surface); @@ -356,7 +188,7 @@ Image::loaded() { /* * Returns this image's surface. */ -cairo_surface_t *Image::surface() { +cairo_surface_t *ImageSurface::surface() { return _surface; } @@ -368,7 +200,7 @@ cairo_surface_t *Image::surface() { */ cairo_status_t -Image::loadSurface() { +ImageSurface::loadSurface() { FILE *stream = fopen(filename, "rb"); if (!stream) { this->errorInfo.set(NULL, "fopen", errno, filename); @@ -413,7 +245,7 @@ Image::loadSurface() { */ cairo_status_t -Image::loadPNG() { +ImageSurface::loadPNG() { _surface = cairo_image_surface_create_from_png(filename); return cairo_surface_status(_surface); } @@ -454,7 +286,7 @@ read_gif_from_memory(GifFileType *gif, GifByteType *buf, int len) { */ cairo_status_t -Image::loadGIF(FILE *stream) { +ImageSurface::loadGIF(FILE *stream) { struct stat s; int fd = fileno(stream); @@ -487,7 +319,7 @@ Image::loadGIF(FILE *stream) { */ cairo_status_t -Image::loadGIFFromBuffer(uint8_t *buf, unsigned len) { +ImageSurface::loadGIFFromBuffer(uint8_t *buf, unsigned len) { int i = 0; GifFileType* gif; @@ -665,7 +497,7 @@ static void jpeg_mem_src (j_decompress_ptr cinfo, void* buffer, long nbytes) { src->next_input_byte = (JOCTET*)buffer; } -class BufferReader : public Image::Reader { +class BufferReader : public ImageSurface::Reader { public: BufferReader(uint8_t* buf, unsigned len) : _buf(buf), _len(len), _idx(0) {} @@ -683,7 +515,7 @@ class BufferReader : public Image::Reader { unsigned _idx; }; -class StreamReader : public Image::Reader { +class StreamReader : public ImageSurface::Reader { public: StreamReader(FILE *stream) : _stream(stream), _len(0), _idx(0) { fseek(_stream, 0, SEEK_END); @@ -709,7 +541,7 @@ class StreamReader : public Image::Reader { unsigned _idx; }; -void Image::jpegToARGB(jpeg_decompress_struct* args, uint8_t* data, uint8_t* src, JPEGDecodeL decode) { +void ImageSurface::jpegToARGB(jpeg_decompress_struct* args, uint8_t* data, uint8_t* src, JPEGDecodeL decode) { int stride = naturalWidth * 4; for (int y = 0; y < naturalHeight; ++y) { jpeg_read_scanlines(args, &src, 1); @@ -727,7 +559,7 @@ void Image::jpegToARGB(jpeg_decompress_struct* args, uint8_t* data, uint8_t* src */ cairo_status_t -Image::decodeJPEGIntoSurface(jpeg_decompress_struct *args, Orientation orientation) { +ImageSurface::decodeJPEGIntoSurface(jpeg_decompress_struct *args, Orientation orientation) { const int channels = 4; cairo_status_t status = CAIRO_STATUS_SUCCESS; @@ -824,7 +656,7 @@ static void canvas_jpeg_output_message(j_common_ptr cinfo) { char buff[JMSG_LENGTH_MAX]; cjerr->format_message(cinfo, buff); // (Only the last message will be returned to JS land.) - cjerr->image->errorInfo.set(buff); + cjerr->surface->errorInfo.set(buff); } /* @@ -833,13 +665,13 @@ static void canvas_jpeg_output_message(j_common_ptr cinfo) { */ cairo_status_t -Image::decodeJPEGBufferIntoMimeSurface(uint8_t *buf, unsigned len) { +ImageSurface::decodeJPEGBufferIntoMimeSurface(uint8_t *buf, unsigned len) { // TODO: remove this duplicate logic // JPEG setup struct jpeg_decompress_struct args; struct canvas_jpeg_error_mgr err; - err.image = this; + err.surface = this; args.err = jpeg_std_error(&err); args.err->error_exit = canvas_jpeg_error_exit; args.err->output_message = canvas_jpeg_output_message; @@ -919,7 +751,7 @@ clearMimeData(void *closure) { */ cairo_status_t -Image::assignDataAsMime(uint8_t *data, int len, const char *mime_type) { +ImageSurface::assignDataAsMime(uint8_t *data, int len, const char *mime_type) { uint8_t *mime_data = (uint8_t *) malloc(len); if (!mime_data) { this->errorInfo.set(NULL, "malloc", errno); @@ -954,7 +786,7 @@ Image::assignDataAsMime(uint8_t *data, int len, const char *mime_type) { */ cairo_status_t -Image::loadJPEGFromBuffer(uint8_t *buf, unsigned len) { +ImageSurface::loadJPEGFromBuffer(uint8_t *buf, unsigned len) { BufferReader reader(buf, len); Orientation orientation = getExifOrientation(reader); @@ -963,7 +795,7 @@ Image::loadJPEGFromBuffer(uint8_t *buf, unsigned len) { struct jpeg_decompress_struct args; struct canvas_jpeg_error_mgr err; - err.image = this; + err.surface = this; args.err = jpeg_std_error(&err); args.err->error_exit = canvas_jpeg_error_exit; args.err->output_message = canvas_jpeg_output_message; @@ -993,7 +825,7 @@ Image::loadJPEGFromBuffer(uint8_t *buf, unsigned len) { */ cairo_status_t -Image::loadJPEG(FILE *stream) { +ImageSurface::loadJPEG(FILE *stream) { cairo_status_t status; if (data_mode == DATA_IMAGE) { // Can lazily read in the JPEG. @@ -1008,7 +840,7 @@ Image::loadJPEG(FILE *stream) { struct jpeg_decompress_struct args; struct canvas_jpeg_error_mgr err; - err.image = this; + err.surface = this; args.err = jpeg_std_error(&err); args.err->error_exit = canvas_jpeg_error_exit; args.err->output_message = canvas_jpeg_output_message; @@ -1075,8 +907,8 @@ Image::loadJPEG(FILE *stream) { * Returns the Exif orientation if one exists, otherwise returns NORMAL */ -Image::Orientation -Image::getExifOrientation(Reader& jpeg) { +ImageSurface::Orientation +ImageSurface::getExifOrientation(Reader& jpeg) { static const char kJpegStartOfImage = (char)0xd8; static const char kJpegStartOfFrameBaseline = (char)0xc0; static const char kJpegStartOfFrameProgressive = (char)0xc2; @@ -1223,7 +1055,7 @@ Image::getExifOrientation(Reader& jpeg) { * Updates the dimensions of the bitmap according to the orientation */ -void Image::updateDimensionsForOrientation(Orientation orientation) { +void ImageSurface::updateDimensionsForOrientation(Orientation orientation) { switch (orientation) { case ROTATE_90_CW: case ROTATE_270_CW: @@ -1252,7 +1084,7 @@ void Image::updateDimensionsForOrientation(Orientation orientation) { */ void -Image::rotatePixels(uint8_t* pixels, int width, int height, int channels, +ImageSurface::rotatePixels(uint8_t* pixels, int width, int height, int channels, Orientation orientation) { auto swapPixel = [channels](uint8_t* pixels, int src_idx, int dst_idx) { uint8_t tmp; @@ -1354,7 +1186,7 @@ Image::rotatePixels(uint8_t* pixels, int width, int height, int channels, * Load BMP from buffer. */ -cairo_status_t Image::loadBMPFromBuffer(uint8_t *buf, unsigned len){ +cairo_status_t ImageSurface::loadBMPFromBuffer(uint8_t *buf, unsigned len){ BMPParser::Parser parser; // Reversed ARGB32 with pre-multiplied alpha @@ -1393,7 +1225,7 @@ cairo_status_t Image::loadBMPFromBuffer(uint8_t *buf, unsigned len){ * Load BMP. */ -cairo_status_t Image::loadBMP(FILE *stream){ +cairo_status_t ImageSurface::loadBMP(FILE *stream){ struct stat s; int fd = fileno(stream); @@ -1425,15 +1257,15 @@ cairo_status_t Image::loadBMP(FILE *stream){ * Return UNKNOWN, GIF, JPEG, or PNG based on the filename. */ -Image::type -Image::extension(const char *filename) { +ImageSurface::type +ImageSurface::extension(const char *filename) { size_t len = strlen(filename); filename += len; - if (len >= 5 && 0 == strcmp(".jpeg", filename - 5)) return Image::JPEG; - if (len >= 4 && 0 == strcmp(".gif", filename - 4)) return Image::GIF; - if (len >= 4 && 0 == strcmp(".jpg", filename - 4)) return Image::JPEG; - if (len >= 4 && 0 == strcmp(".png", filename - 4)) return Image::PNG; - return Image::UNKNOWN; + if (len >= 5 && 0 == strcmp(".jpeg", filename - 5)) return ImageSurface::JPEG; + if (len >= 4 && 0 == strcmp(".gif", filename - 4)) return ImageSurface::GIF; + if (len >= 4 && 0 == strcmp(".jpg", filename - 4)) return ImageSurface::JPEG; + if (len >= 4 && 0 == strcmp(".png", filename - 4)) return ImageSurface::PNG; + return ImageSurface::UNKNOWN; } /* @@ -1441,7 +1273,7 @@ Image::extension(const char *filename) { */ int -Image::isJPEG(uint8_t *data) { +ImageSurface::isJPEG(uint8_t *data) { return 0xff == data[0] && 0xd8 == data[1]; } @@ -1450,7 +1282,7 @@ Image::isJPEG(uint8_t *data) { */ int -Image::isGIF(uint8_t *data) { +ImageSurface::isGIF(uint8_t *data) { return 'G' == data[0] && 'I' == data[1] && 'F' == data[2]; } @@ -1459,7 +1291,7 @@ Image::isGIF(uint8_t *data) { */ int -Image::isPNG(uint8_t *data) { +ImageSurface::isPNG(uint8_t *data) { return 'P' == data[1] && 'N' == data[2] && 'G' == data[3]; } @@ -1467,7 +1299,7 @@ Image::isPNG(uint8_t *data) { * Check for valid BMP signatures */ -int Image::isBMP(uint8_t *data, unsigned len) { +int ImageSurface::isBMP(uint8_t *data, unsigned len) { if(len < 2) return false; std::string sig = std::string(1, (char)data[0]) + (char)data[1]; return sig == "BM" || @@ -1477,3 +1309,179 @@ int Image::isBMP(uint8_t *data, unsigned len) { sig == "IC" || sig == "PT"; } + +void +Image::Initialize(Napi::Env& env, Napi::Object& exports) { + InstanceData *data = env.GetInstanceData(); + Napi::HandleScope scope(env); + + Napi::Function ctor = DefineClass(env, "Image", { + InstanceAccessor<&Image::GetComplete>("complete", napi_default_jsproperty), + InstanceAccessor<&Image::GetWidth, &Image::SetWidth>("width", napi_default_jsproperty), + InstanceAccessor<&Image::GetHeight, &Image::SetHeight>("height", napi_default_jsproperty), + InstanceAccessor<&Image::GetNaturalWidth>("naturalWidth", napi_default_jsproperty), + InstanceAccessor<&Image::GetNaturalHeight>("naturalHeight", napi_default_jsproperty), + InstanceAccessor<&Image::GetDataMode, &Image::SetDataMode>("dataMode", napi_default_jsproperty), + StaticValue("MODE_IMAGE", Napi::Number::New(env, ImageSurface::DATA_IMAGE), napi_default_jsproperty), + StaticValue("MODE_MIME", Napi::Number::New(env, ImageSurface::DATA_MIME), napi_default_jsproperty) + }); + + // Used internally in lib/image.js + exports.Set("GetSource", Napi::Function::New(env, &GetSource)); + exports.Set("SetSource", Napi::Function::New(env, &SetSource)); + + data->ImageCtor = Napi::Persistent(ctor); + exports.Set("Image", ctor); +} + +/* + * Initialize a new Image. + */ + +Image::Image(const Napi::CallbackInfo& info) : ObjectWrap(info), env(info.Env()) , surface(env) { + info.This().ToObject().Unwrap().Set("onload", env.Null()); + info.This().ToObject().Unwrap().Set("onerror", env.Null()); +} + +/* + * Get complete boolean. + */ + +Napi::Value +Image::GetComplete(const Napi::CallbackInfo& info) { + return Napi::Boolean::New(env, true); +} + +/* + * Get dataMode. + */ + +Napi::Value +Image::GetDataMode(const Napi::CallbackInfo& info) { + return Napi::Number::New(env, surface.data_mode); +} + +/* + * Set dataMode. + */ + +void +Image::SetDataMode(const Napi::CallbackInfo& info, const Napi::Value& value) { + if (value.IsNumber()) { + int mode = value.As().Uint32Value(); + surface.data_mode = (ImageSurface::data_mode_t) mode; + } +} + +/* + * Get natural width + */ + +Napi::Value +Image::GetNaturalWidth(const Napi::CallbackInfo& info) { + return Napi::Number::New(env, surface.naturalWidth); +} + +/* + * Get width. + */ + +Napi::Value +Image::GetWidth(const Napi::CallbackInfo& info) { + return Napi::Number::New(env, surface.width); +} + +/* + * Set width. + */ + +void +Image::SetWidth(const Napi::CallbackInfo& info, const Napi::Value& value) { + if (value.IsNumber()) { + surface.width = value.As().Uint32Value(); + } +} + +/* + * Get natural height + */ + +Napi::Value +Image::GetNaturalHeight(const Napi::CallbackInfo& info) { + return Napi::Number::New(env, surface.naturalHeight); +} + +/* + * Get height. + */ + +Napi::Value +Image::GetHeight(const Napi::CallbackInfo& info) { + return Napi::Number::New(env, surface.height); +} +/* + * Set height. + */ + +void +Image::SetHeight(const Napi::CallbackInfo& info, const Napi::Value& value) { + if (value.IsNumber()) { + surface.height = value.As().Uint32Value(); + } +} + +/* + * Get src path. + */ + +Napi::Value +Image::GetSource(const Napi::CallbackInfo& info){ + Napi::Env env = info.Env(); + Image *img = Image::Unwrap(info.This().As()); + return Napi::String::New(env, img->surface.filename ? img->surface.filename : ""); +} + +/* + * Set src path. + */ + +void +Image::SetSource(const Napi::CallbackInfo& info){ + Napi::Env env = info.Env(); + Napi::Object This = info.This().As(); + Image *img = Image::Unwrap(This); + cairo_status_t status = CAIRO_STATUS_READ_ERROR; + + Napi::Value value = info[0]; + + // url string + if (value.IsString()) { + std::string src = value.As().Utf8Value(); + status = img->surface.load(src); + // Buffer + } else if (value.IsBuffer()) { + uint8_t *buf = value.As>().Data(); + unsigned len = value.As>().Length(); + status = img->surface.loadFromBuffer(buf, len); + } + + if (status) { + Napi::Value onerrorFn; + if (This.Get("onerror").UnwrapTo(&onerrorFn) && onerrorFn.IsFunction()) { + Napi::Error arg; + if (img->surface.errorInfo.empty()) { + arg = Napi::Error::New(env, Napi::String::New(env, cairo_status_to_string(status))); + } else { + arg = img->surface.errorInfo.toError(env); + } + onerrorFn.As().Call({ arg.Value() }); + } + } else { + img->surface.loaded(); + Napi::Value onloadFn; + if (This.Get("onload").UnwrapTo(&onloadFn) && onloadFn.IsFunction()) { + onloadFn.As().Call({}); + } + } +} + diff --git a/src/Image.h b/src/Image.h index c757571f3..db50ee803 100644 --- a/src/Image.h +++ b/src/Image.h @@ -16,25 +16,13 @@ using JPEGDecodeL = std::function; -class Image : public Napi::ObjectWrap { +class ImageSurface { public: + Napi::Env env; char *filename; int width, height; int naturalWidth, naturalHeight; - Napi::Env env; - static void Initialize(Napi::Env& env, Napi::Object& target); - Image(const Napi::CallbackInfo& info); - Napi::Value GetComplete(const Napi::CallbackInfo& info); - Napi::Value GetWidth(const Napi::CallbackInfo& info); - Napi::Value GetHeight(const Napi::CallbackInfo& info); - Napi::Value GetNaturalWidth(const Napi::CallbackInfo& info); - Napi::Value GetNaturalHeight(const Napi::CallbackInfo& info); - Napi::Value GetDataMode(const Napi::CallbackInfo& info); - void SetDataMode(const Napi::CallbackInfo& info, const Napi::Value& value); - void SetWidth(const Napi::CallbackInfo& info, const Napi::Value& value); - void SetHeight(const Napi::CallbackInfo& info, const Napi::Value& value); - static Napi::Value GetSource(const Napi::CallbackInfo& info); - static void SetSource(const Napi::CallbackInfo& info); + ImageSurface(Napi::Env env); inline uint8_t *data(){ return cairo_image_surface_get_data(_surface); } inline int stride(){ return cairo_image_surface_get_stride(_surface); } static int isPNG(uint8_t *data); @@ -42,7 +30,6 @@ class Image : public Napi::ObjectWrap { static int isGIF(uint8_t *data); static int isBMP(uint8_t *data, unsigned len); static cairo_status_t readPNG(void *closure, unsigned char *data, unsigned len); - inline int isComplete(){ return COMPLETE == state; } cairo_surface_t *surface(); cairo_status_t loadSurface(); cairo_status_t loadFromBuffer(uint8_t *buf, unsigned len); @@ -81,8 +68,8 @@ class Image : public Napi::ObjectWrap { cairo_status_t loadBMP(FILE *stream); CanvasError errorInfo; void loaded(); - cairo_status_t load(); - ~Image(); + cairo_status_t load(std::string& filename); + ~ImageSurface(); enum { DEFAULT @@ -109,3 +96,23 @@ class Image : public Napi::ObjectWrap { uint8_t *_data = nullptr; int _data_len; }; + +class Image : public Napi::ObjectWrap { + public: + Napi::Env env; + static void Initialize(Napi::Env& env, Napi::Object& target); + Image(const Napi::CallbackInfo& info); + Napi::Value GetComplete(const Napi::CallbackInfo& info); + Napi::Value GetWidth(const Napi::CallbackInfo& info); + Napi::Value GetHeight(const Napi::CallbackInfo& info); + Napi::Value GetNaturalWidth(const Napi::CallbackInfo& info); + Napi::Value GetNaturalHeight(const Napi::CallbackInfo& info); + Napi::Value GetDataMode(const Napi::CallbackInfo& info); + void SetDataMode(const Napi::CallbackInfo& info, const Napi::Value& value); + void SetWidth(const Napi::CallbackInfo& info, const Napi::Value& value); + void SetHeight(const Napi::CallbackInfo& info, const Napi::Value& value); + static Napi::Value GetSource(const Napi::CallbackInfo& info); + static void SetSource(const Napi::CallbackInfo& info); + inline int isComplete(){ return ImageSurface::COMPLETE == surface.state; } + ImageSurface surface; +}; From 004c349530e6f81871638cd0c48f11200e4c255b Mon Sep 17 00:00:00 2001 From: Caleb Hearon Date: Fri, 26 Jun 2026 18:18:14 -0400 Subject: [PATCH 6/8] use lunasvg to get the most back for the buck librsvg is too hard to support. We can't reasonably be shipping a copy of the entire glib library to everyone; this isn't Rust. lunasvg is the best option I could find: js: - canvg requires jsdom and failed to render serveral simple test SVGs - fabricjs also failed simple SVGs cpp: - librsvg is too hard to maintain - resvg is way too big and uses a whole separate (rust) toolchain - nanosvg failed simple SVGs drawbacks: - no support for filters or SVG2 - I don't think it supports font fallbacks or bidi upsides: - registered fonts work inside of SVGs - re-uses our own bitmap parsers --- build.zig | 11 +++ build.zig.zon | 1 + pkg/lunasvg/build.zig | 106 ++++++++++++++++++++++++ pkg/lunasvg/build.zig.zon | 11 +++ src/Image.cc | 169 +++++++++++++++++++++++++++++++++++--- src/Image.h | 18 +++- src/init.cc | 6 ++ test/image.test.js | 31 +++++++ 8 files changed, 341 insertions(+), 12 deletions(-) create mode 100644 pkg/lunasvg/build.zig create mode 100644 pkg/lunasvg/build.zig.zon diff --git a/build.zig b/build.zig index 92f23fefb..83b061b28 100644 --- a/build.zig +++ b/build.zig @@ -89,6 +89,11 @@ pub fn build(b: *std.Build) void { .optimize = optimize, }).artifact("harfbuzz"); + const lunasvg = b.dependency("lunasvg", .{ + .target = target, + .optimize = optimize, + }).artifact("lunasvg"); + const unicode = b.addLibrary(.{ .name = "unicode", .linkage = .static, @@ -147,6 +152,7 @@ pub fn build(b: *std.Build) void { "-D_USE_MATH_DEFINES", "-std=c++20", if (target.result.os.tag == .windows) "-DCAIRO_WIN32_STATIC_BUILD" else "", + "-DLUNASVG_BUILD_STATIC", } }); @@ -166,6 +172,11 @@ pub fn build(b: *std.Build) void { canvas.linkLibrary(sheenbidi); canvas.linkLibrary(unicode); canvas.linkLibrary(harfbuzz); + canvas.linkLibrary(lunasvg); + + // some deps, especially lunasvg, compile with -ffunction-sections and + // -fdata-sections so we don't have to distribute unused parts of libraries + canvas.link_gc_sections = true; if (target.result.os.tag == .windows) { canvas.linkSystemLibrary("dwrite"); diff --git a/build.zig.zon b/build.zig.zon index 52b007889..aad0300e7 100644 --- a/build.zig.zon +++ b/build.zig.zon @@ -31,5 +31,6 @@ .sheenbidi = .{ .path = "./pkg/sheenbidi" }, .harfbuzz = .{ .path = "./pkg/harfbuzz" }, .freetype = .{ .path = "./pkg/freetype" }, + .lunasvg = .{ .path = "./pkg/lunasvg" } }, } diff --git a/pkg/lunasvg/build.zig b/pkg/lunasvg/build.zig new file mode 100644 index 000000000..6cd785d8b --- /dev/null +++ b/pkg/lunasvg/build.zig @@ -0,0 +1,106 @@ +const std = @import("std"); + +pub fn build(b: *std.Build) !void { + const target = b.standardTargetOptions(.{}); + const optimize = b.standardOptimizeOption(.{}); + + const plutovg = b.addLibrary(.{ + .name = "plutovg", + .linkage = .static, + .root_module = b.createModule(.{ + .target = target, + .optimize = optimize, + }) + }); + + const upstream = b.dependency("lunasvg", .{}); + const cairo = b.dependency("cairo", .{ + .target = target, + .optimize = optimize, + }).artifact("cairo"); + + plutovg.addCSourceFiles(.{ + .root = upstream.path(""), + .files = &.{ + "plutovg/source/plutovg-blend.c", + "plutovg/source/plutovg-canvas.c", + "plutovg/source/plutovg-font.c", + "plutovg/source/plutovg-matrix.c", + "plutovg/source/plutovg-path.c", + "plutovg/source/plutovg-paint.c", + "plutovg/source/plutovg-rasterize.c", + "plutovg/source/plutovg-surface.c", + "plutovg/source/plutovg-ft-math.c", + "plutovg/source/plutovg-ft-raster.c", + "plutovg/source/plutovg-ft-stroker.c" + }, + .flags = &.{ + "-D_FILE_OFFSET_BITS=64", + "-std=gnu11", + "-pthread", + "-DWITH_GZFILEOP", + "-DPLUTOVG_BUILD", + "-DPLUTOVG_BUILD_STATIC", + "-DHAVE_THREADS_H", + "-fvisibility=hidden", + } + }); + + plutovg.addIncludePath(upstream.path("plutovg/include")); + plutovg.installHeadersDirectory(upstream.path("plutovg/include"), "", .{}); + plutovg.linkLibrary(cairo); + plutovg.linkLibC(); + + // strip out anything we didn't use, which for plutovg is a lot (on this fork) + plutovg.link_data_sections = true; + plutovg.link_function_sections = true; + + const lunasvg = b.addLibrary(.{ + .name = "lunasvg", + .linkage = .static, + .root_module = b.createModule(.{ + .target = target, + .optimize = optimize, + }) + }); + + lunasvg.addCSourceFiles(.{ + .root = upstream.path(""), + .files = &.{ + "source/lunasvg.cpp", + "source/graphics.cpp", + "source/svgelement.cpp", + "source/svggeometryelement.cpp", + "source/svgpaintelement.cpp", + "source/svgparser.cpp", + "source/svgproperty.cpp", + "source/svglayoutstate.cpp", + "source/svgrenderstate.cpp", + "source/svgtextelement.cpp" + }, + .flags = &.{ + "-D_GLIBCXX_ASSERTIONS=1", + "-D_FILE_OFFSET_BITS=64", + "-std=c++17", + "-DWITH_GZFILEOP", + "-pthread", + "-DLUNASVG_BUILD", + "-DLUNASVG_BUILD_STATIC", + "-DPLUTOVG_BUILD_STATIC", + "-fvisibility=hidden", + } + }); + + // strip out anything we didn't use + lunasvg.link_data_sections = true; + lunasvg.link_function_sections = true; + + lunasvg.addIncludePath(upstream.path("include")); + lunasvg.installHeadersDirectory(upstream.path("include"), "", .{}); + lunasvg.linkLibrary(plutovg); + lunasvg.linkLibC(); + lunasvg.linkLibCpp(); + lunasvg.linkLibrary(cairo); + + b.installArtifact(lunasvg); +} diff --git a/pkg/lunasvg/build.zig.zon b/pkg/lunasvg/build.zig.zon new file mode 100644 index 000000000..631ab2fdb --- /dev/null +++ b/pkg/lunasvg/build.zig.zon @@ -0,0 +1,11 @@ +.{ + .name = .lunasvg, + .version = "0.0.0", + .dependencies = .{ + .cairo = .{ .path = "../cairo" }, + .lunasvg = .{ + .url = "https://github.com/chearon/lunasvg/archive/3fd8ba9e0cd5cf0af36b1bda2d0a3bbaf8bd8da7.tar.gz", + .hash = "N-V-__8AAFD4FABzdNrtG-6ulAVAPIPJZde38BQWKRcKgkgV", + } + }, +} diff --git a/src/Image.cc b/src/Image.cc index 8a7c7d80c..2639989b5 100644 --- a/src/Image.cc +++ b/src/Image.cc @@ -33,7 +33,7 @@ struct canvas_jpeg_error_mgr: jpeg_error_mgr { */ typedef struct { - Napi::Env env; + std::optional env; unsigned len; uint8_t *buf; } read_closure_t; @@ -42,7 +42,7 @@ typedef struct { * Initialize a new ImageSurface (canvas_surface_t wrapper) */ -ImageSurface::ImageSurface(Napi::Env env) : env(env) { +ImageSurface::ImageSurface(std::optional env) : env(env) { data_mode = DATA_IMAGE; filename = NULL; _data = nullptr; @@ -51,6 +51,8 @@ ImageSurface::ImageSurface(Napi::Env env) : env(env) { width = height = 0; naturalWidth = naturalHeight = 0; state = DEFAULT; + svgdoc = nullptr; + _svg_last_width = _svg_last_height = 0; } /* @@ -61,7 +63,7 @@ void ImageSurface::clearData() { if (_surface) { cairo_surface_destroy(_surface); - Napi::MemoryManagement::AdjustExternalMemory(env, -_data_len); + if (env) Napi::MemoryManagement::AdjustExternalMemory(*env, -_data_len); _data_len = 0; _surface = NULL; } @@ -72,11 +74,25 @@ ImageSurface::clearData() { free(filename); filename = NULL; + svgdoc = nullptr; + width = height = 0; naturalWidth = naturalHeight = 0; state = DEFAULT; } +cairo_surface_t* +ImageSurface::transferSurface() { + cairo_surface_t* surface = _surface; + + if (env) Napi::MemoryManagement::AdjustExternalMemory(*env, -_data_len); + _data_len = 0; + _surface = nullptr; + _data = nullptr; + + return surface; +} + /* * Load image data from `buf` by sniffing * the bytes to determine format. @@ -110,6 +126,12 @@ ImageSurface::loadFromBuffer(uint8_t *buf, unsigned len) { } } + // confirm svg using first 1000 chars + // if a very long comment precedes the root tag, isSVG returns false + unsigned head_len = (len < 1000 ? len : 1000); + if (isSVG(buf, head_len)) + return loadSVGFromBuffer(buf, len); + if (isBMP(buf, len)) return loadBMPFromBuffer(buf, len); @@ -182,13 +204,20 @@ ImageSurface::loaded() { width = naturalWidth = cairo_image_surface_get_width(_surface); height = naturalHeight = cairo_image_surface_get_height(_surface); _data_len = naturalHeight * cairo_image_surface_get_stride(_surface); - Napi::MemoryManagement::AdjustExternalMemory(env, _data_len); + if (env) Napi::MemoryManagement::AdjustExternalMemory(*env, _data_len); } /* * Returns this image's surface. */ cairo_surface_t *ImageSurface::surface() { + if (svgdoc && (_svg_last_width != width || _svg_last_height != height)) { + cairo_status_t status = renderSVGToSurface(); + if (status != CAIRO_STATUS_SUCCESS) { + if (env) Napi::Error::New(*env, cairo_status_to_string(status)).ThrowAsJavaScriptException(); + return nullptr; + } + } return _surface; } @@ -232,8 +261,26 @@ ImageSurface::loadSurface() { return CAIRO_STATUS_READ_ERROR; } + // confirm svg using first 1000 chars + // if a very long comment precedes the root tag, isSVG returns false + uint8_t head[1000] = {0}; + fseek(stream, 0 , SEEK_END); + long len = ftell(stream); + unsigned head_len = (len < 1000 ? len : 1000); + unsigned head_size = head_len * sizeof(uint8_t); + rewind(stream); + if (head_size != fread(&head, 1, head_size, stream)) { + fclose(stream); + return CAIRO_STATUS_READ_ERROR; + } + rewind(stream); + if (isSVG(head, head_len)) { + return loadSVG(stream); + } + if (isBMP(buf, 2)) return loadBMP(stream); + fclose(stream); this->errorInfo.set("Unsupported image type"); @@ -737,10 +784,9 @@ ImageSurface::decodeJPEGBufferIntoMimeSurface(uint8_t *buf, unsigned len) { void clearMimeData(void *closure) { - Napi::MemoryManagement::AdjustExternalMemory( - static_cast(closure)->env, - -static_cast((static_cast(closure)->len))); - free(static_cast(closure)->buf); + read_closure_t* c = static_cast(closure); + if (c->env) Napi::MemoryManagement::AdjustExternalMemory(*c->env, (int)c->len); + free(c->buf); free(closure); } @@ -771,7 +817,7 @@ ImageSurface::assignDataAsMime(uint8_t *data, int len, const char *mime_type) { mime_closure->buf = mime_data; mime_closure->len = len; - Napi::MemoryManagement::AdjustExternalMemory(env, len); + if (env) Napi::MemoryManagement::AdjustExternalMemory(*env, len); return cairo_surface_set_mime_data(_surface , mime_type @@ -1182,6 +1228,87 @@ ImageSurface::rotatePixels(uint8_t* pixels, int width, int height, int channels, } } +/* + * Load SVG from buffer + */ + +cairo_status_t +ImageSurface::loadSVGFromBuffer(uint8_t *buf, unsigned len) { + lunasvg::GraphicsCallbacks callbacks; + + callbacks.setDecoderFn([](char* data, int length) { + ImageSurface bitmap(std::nullopt); + bitmap.loadFromBuffer((uint8_t*)data, length); + return bitmap.transferSurface(); + }); + + svgdoc = lunasvg::Document::loadFromData((char*)buf, len, callbacks); + + if (!svgdoc) return CAIRO_STATUS_READ_ERROR; + + width = naturalWidth = svgdoc->width(); + height = naturalHeight = svgdoc->height(); + + if (width <= 0 || height <= 0) { + this->errorInfo.set("Width and height must be set on the svg element"); + return CAIRO_STATUS_READ_ERROR; + } + + return renderSVGToSurface(); +} + +/* + * Renders the lunasvg document to this image's surface + */ + +cairo_status_t +ImageSurface::renderSVGToSurface() { + cairo_status_t status; + + if (_surface) cairo_surface_destroy(_surface); + lunasvg::Bitmap bitmap = svgdoc->renderToBitmap(width, height, 0); + status = cairo_surface_status(bitmap.surface()); + if (status != CAIRO_STATUS_SUCCESS) return status; + _surface = cairo_surface_reference(bitmap.surface()); + + _svg_last_width = width; + _svg_last_height = height; + + return status; +} + +/* + * Load SVG + */ + +cairo_status_t +ImageSurface::loadSVG(FILE *stream) { + struct stat s; + int fd = fileno(stream); + + // stat + if (fstat(fd, &s) < 0) { + fclose(stream); + return CAIRO_STATUS_READ_ERROR; + } + + uint8_t *buf = (uint8_t *) malloc(s.st_size); + + if (!buf) { + fclose(stream); + return CAIRO_STATUS_NO_MEMORY; + } + + size_t read = fread(buf, s.st_size, 1, stream); + fclose(stream); + + cairo_status_t result = CAIRO_STATUS_READ_ERROR; + if (1 == read) result = loadSVGFromBuffer(buf, s.st_size); + free(buf); + + return result; +} + /* * Load BMP from buffer. */ @@ -1254,7 +1381,7 @@ cairo_status_t ImageSurface::loadBMP(FILE *stream){ } /* - * Return UNKNOWN, GIF, JPEG, or PNG based on the filename. + * Return UNKNOWN, SVG, GIF, JPEG, or PNG based on the filename. */ ImageSurface::type @@ -1265,6 +1392,7 @@ ImageSurface::extension(const char *filename) { if (len >= 4 && 0 == strcmp(".gif", filename - 4)) return ImageSurface::GIF; if (len >= 4 && 0 == strcmp(".jpg", filename - 4)) return ImageSurface::JPEG; if (len >= 4 && 0 == strcmp(".png", filename - 4)) return ImageSurface::PNG; + if (len >= 4 && 0 == strcmp(".svg", filename - 4)) return ImageSurface::SVG; return ImageSurface::UNKNOWN; } @@ -1295,6 +1423,27 @@ ImageSurface::isPNG(uint8_t *data) { return 'P' == data[1] && 'N' == data[2] && 'G' == data[3]; } +/* + * Skip " #include #include // node < 7 uses libstdc++ on macOS which lacks complete c++11 +#include #include #include @@ -14,20 +15,25 @@ #include #define GIF_CLOSE_FILE(gif) DGifCloseFile(gif, NULL) +#include + using JPEGDecodeL = std::function; class ImageSurface { public: - Napi::Env env; + // Only contains a value when the ImageSurface backs a js Image. Empty for + // ImageSurfaces supporting lunasvg (to render images embedded in SVGs). + std::optional env; char *filename; int width, height; int naturalWidth, naturalHeight; - ImageSurface(Napi::Env env); + ImageSurface(std::optional env); inline uint8_t *data(){ return cairo_image_surface_get_data(_surface); } inline int stride(){ return cairo_image_surface_get_stride(_surface); } static int isPNG(uint8_t *data); static int isJPEG(uint8_t *data); static int isGIF(uint8_t *data); + static int isSVG(uint8_t *data, unsigned len); static int isBMP(uint8_t *data, unsigned len); static cairo_status_t readPNG(void *closure, unsigned char *data, unsigned len); cairo_surface_t *surface(); @@ -36,6 +42,10 @@ class ImageSurface { cairo_status_t loadPNGFromBuffer(uint8_t *buf); cairo_status_t loadPNG(); void clearData(); + cairo_surface_t* transferSurface(); + cairo_status_t loadSVGFromBuffer(uint8_t *buf, unsigned len); + cairo_status_t loadSVG(FILE *stream); + cairo_status_t renderSVGToSurface(); cairo_status_t loadGIFFromBuffer(uint8_t *buf, unsigned len); cairo_status_t loadGIF(FILE *stream); enum Orientation { @@ -87,6 +97,7 @@ class ImageSurface { , GIF , JPEG , PNG + , SVG } type; static type extension(const char *filename); @@ -95,6 +106,9 @@ class ImageSurface { cairo_surface_t *_surface; uint8_t *_data = nullptr; int _data_len; + std::unique_ptr svgdoc; + int _svg_last_width; + int _svg_last_height; }; class Image : public Napi::ObjectWrap { diff --git a/src/init.cc b/src/init.cc index 474f3034f..64e5b93ba 100644 --- a/src/init.cc +++ b/src/init.cc @@ -5,6 +5,7 @@ #include #include #include +#include #include "Canvas.h" #include "CanvasGradient.h" @@ -66,6 +67,11 @@ Napi::Object init(Napi::Env env, Napi::Object exports) { char gif_version[10]; snprintf(gif_version, 10, "%d.%d.%d", GIFLIB_MAJOR, GIFLIB_MINOR, GIFLIB_RELEASE); exports.Set("gifVersion", Napi::String::New(env, gif_version)); + exports.Set("lunasvgVersion", Napi::String::New(env, LUNASVG_VERSION_STRING)); + + char freetype_version[10]; + snprintf(freetype_version, 10, "%d.%d.%d", FREETYPE_MAJOR, FREETYPE_MINOR, FREETYPE_PATCH); + exports.Set("freetypeVersion", Napi::String::New(env, freetype_version)); return exports; } diff --git a/test/image.test.js b/test/image.test.js index c1c764572..9dd6610e6 100644 --- a/test/image.test.js +++ b/test/image.test.js @@ -151,6 +151,30 @@ describe('Image', function () { }, MyError) }) + it('loads SVG data URL base64', function () { + const base64Enc = fs.readFileSync(svgTree, 'base64') + const dataURL = `data:image/svg+xml;base64,${base64Enc}` + return loadImage(dataURL).then((img) => { + assert.strictEqual(img.onerror, null) + assert.strictEqual(img.onload, null) + assert.strictEqual(img.width, 200) + assert.strictEqual(img.height, 200) + assert.strictEqual(img.complete, true) + }) + }) + + it('loads SVG data URL utf8', function () { + const utf8Encoded = fs.readFileSync(svgTree, 'utf8') + const dataURL = `data:image/svg+xml;utf8,${utf8Encoded}` + return loadImage(dataURL).then((img) => { + assert.strictEqual(img.onerror, null) + assert.strictEqual(img.onload, null) + assert.strictEqual(img.width, 200) + assert.strictEqual(img.height, 200) + assert.strictEqual(img.complete, true) + }) + }) + it('calls Image#onload multiple times', function () { return loadImage(pngClock).then((img) => { let onloadCalled = 0 @@ -349,6 +373,13 @@ describe('Image', function () { assert.ok(!keys.includes('setSource')) }) + it('loadImage doesn\'t crash when you don\'t specify width and height', async function () { + const svg = `` + const img = await loadImage(Buffer.from(svg)) + assert.equal(img.width, 1) + assert.equal(img.height, 1) + }) + describe('supports BMP', function () { it('parses 1-bit image', function (done) { const img = new Image() From 277bba3a0c7156da985ba9dd74bb9d43bbe5f3a9 Mon Sep 17 00:00:00 2001 From: Caleb Hearon Date: Mon, 29 Jun 2026 12:43:00 -0400 Subject: [PATCH 7/8] add support for node worker threads --- build.zig | 3 ++ examples/images/badge.svg | 5 +++ examples/worker-threads.js | 65 ++++++++++++++++++++++++++++++++++++++ pkg/lunasvg/build.zig | 3 ++ 4 files changed, 76 insertions(+) create mode 100644 examples/images/badge.svg create mode 100644 examples/worker-threads.js diff --git a/build.zig b/build.zig index 83b061b28..1197e1ccd 100644 --- a/build.zig +++ b/build.zig @@ -151,6 +151,9 @@ pub fn build(b: *std.Build) void { "-DNODE_ADDON_API_ENABLE_MAYBE", "-D_USE_MATH_DEFINES", "-std=c++20", + // dlclose gets called on the addon + // https://maskray.me/blog/2024-03-17-c++-exit-time-destructors + "-fno-c++-static-destructors", if (target.result.os.tag == .windows) "-DCAIRO_WIN32_STATIC_BUILD" else "", "-DLUNASVG_BUILD_STATIC", } diff --git a/examples/images/badge.svg b/examples/images/badge.svg new file mode 100644 index 000000000..ee4a3b340 --- /dev/null +++ b/examples/images/badge.svg @@ -0,0 +1,5 @@ + + + + diff --git a/examples/worker-threads.js b/examples/worker-threads.js new file mode 100644 index 000000000..bc68a252f --- /dev/null +++ b/examples/worker-threads.js @@ -0,0 +1,65 @@ +const { Worker, isMainThread, workerData, parentPort } = require('worker_threads') +const fs = require('fs') +const path = require('path') + +const ARIMO = path.join(__dirname, '../test/fixtures/Arimo-Regular.ttf') +const PFENNIG = path.join(__dirname, 'pfennigFont/Pfennig.ttf') +const BADGE = path.join(__dirname, 'images/badge.svg') + +const WORKERS = [ + { index: 0, color: '#1a1a2e', fonts: ['arimo'], useFont: 'Arimo', svg: false }, + { index: 1, color: '#16213e', fonts: ['arimo'], useFont: 'Arimo, Pfennig', svg: false }, + { index: 2, color: '#0f3460', fonts: ['arimo', 'pfennig'], useFont: 'Arimo', svg: true }, + { index: 3, color: '#533483', fonts: ['arimo', 'pfennig'], useFont: 'Pfennig', svg: true } +] + +if (isMainThread) { + let done = 0 + + for (const config of WORKERS) { + const worker = new Worker(__filename, { workerData: config }) + + worker.on('message', ({ index, buffer }) => { + const file = path.join(__dirname, `worker-${index}.png`) + fs.writeFileSync(file, buffer) + console.log(`Wrote ${file}`) + if (++done === WORKERS.length) console.log('All workers done.') + }) + + worker.on('error', (err) => { + console.error(`Worker ${config.index} error:`, err) + }) + } +} else { + const { createCanvas, FontFace, fonts, loadImage } = require('..') + const { index, color, fonts: fontNames, useFont, svg } = workerData + + if (fontNames.includes('arimo')) fonts.add(new FontFace('Arimo', ARIMO)) + if (fontNames.includes('pfennig')) fonts.add(new FontFace('Pfennig', PFENNIG)) + + const render = async () => { + const canvas = createCanvas(200, 200) + const ctx = canvas.getContext('2d') + + ctx.fillStyle = color + ctx.fillRect(0, 0, 200, 200) + + if (svg) { + const badge = await loadImage(BADGE) + ctx.drawImage(badge, 10, 10, 80, 80) + } + + ctx.fillStyle = 'white' + ctx.font = `bold 26px ${useFont}` + ctx.textAlign = 'center' + ctx.textBaseline = 'middle' + ctx.fillText(`Worker ${index}`, 100, 110) + + ctx.font = `13px ${useFont}` + ctx.fillText(`${useFont} / [${fontNames.join(', ')}]`, 100, 140) + + return canvas.toBuffer('image/png') + } + + render().then(buffer => parentPort.postMessage({ index, buffer })) +} diff --git a/pkg/lunasvg/build.zig b/pkg/lunasvg/build.zig index 6cd785d8b..060609bad 100644 --- a/pkg/lunasvg/build.zig +++ b/pkg/lunasvg/build.zig @@ -88,6 +88,9 @@ pub fn build(b: *std.Build) !void { "-DLUNASVG_BUILD_STATIC", "-DPLUTOVG_BUILD_STATIC", "-fvisibility=hidden", + // dlclose gets called on the addon + // https://maskray.me/blog/2024-03-17-c++-exit-time-destructors + "-fno-c++-static-destructors", } }); From 2b6526173dab5a06cb41b8794ab08add71b6f0db Mon Sep 17 00:00:00 2001 From: Caleb Hearon Date: Thu, 9 Jul 2026 21:32:00 -0400 Subject: [PATCH 8/8] zig 0.16 --- .gitignore | 1 + build.zig | 36 +++++++++++++++++++----------------- build.zig.zon | 6 +++--- pkg/cairo/build.zig | 26 +++++++++++++------------- pkg/freetype/build.zig | 2 +- pkg/freetype/build.zig.zon | 13 +++---------- pkg/harfbuzz/build.zig | 7 +++---- pkg/harfbuzz/build.zig.zon | 1 + pkg/lunasvg/build.zig | 22 ++++++++++++---------- pkg/lunasvg/build.zig.zon | 1 + pkg/pixman/build.zig | 9 +++++---- pkg/pixman/build.zig.zon | 1 + pkg/sheenbidi/build.zig | 6 +++--- pkg/sheenbidi/build.zig.zon | 1 + src/unicode.zig | 22 ++-------------------- 15 files changed, 69 insertions(+), 85 deletions(-) diff --git a/.gitignore b/.gitignore index 87b3a2abe..e285ad546 100644 --- a/.gitignore +++ b/.gitignore @@ -21,3 +21,4 @@ npm-debug.log .idea .zig-cache/ +zig-pkg/ diff --git a/build.zig b/build.zig index 1197e1ccd..565695a7d 100644 --- a/build.zig +++ b/build.zig @@ -101,12 +101,13 @@ pub fn build(b: *std.Build) void { .target = target, .optimize = optimize, .root_source_file = b.path("src/unicode.zig"), + .link_libc = true, + .pic = true, }) }); unicode.root_module.addImport("Scripts", zg.module("Scripts")); unicode.root_module.addImport("Graphemes", zg.module("Graphemes")); - unicode.linkLibC(); const canvas = b.addLibrary(.{ .name = "canvas", @@ -114,6 +115,9 @@ pub fn build(b: *std.Build) void { .root_module = b.createModule(.{ .target = target, .optimize = optimize, + .pic = true, + .link_libc = true, + .link_libcpp = true, }) }); @@ -122,7 +126,7 @@ pub fn build(b: *std.Build) void { .optimize = optimize, }).artifact("node_api"); - canvas.addCSourceFiles(.{ + canvas.root_module.addCSourceFiles(.{ .files = &.{ "src/bmp/BMPParser.cc", "src/Canvas.cc", @@ -165,24 +169,22 @@ pub fn build(b: *std.Build) void { canvas.root_module.strip = true; } - canvas.linkLibC(); - canvas.linkLibCpp(); - canvas.addObject(node_api); - canvas.linkLibrary(cairo); - canvas.linkLibrary(libjpeg_turbo); - canvas.linkLibrary(libpng); - canvas.linkLibrary(giflib); - canvas.linkLibrary(sheenbidi); - canvas.linkLibrary(unicode); - canvas.linkLibrary(harfbuzz); - canvas.linkLibrary(lunasvg); + canvas.root_module.addObject(node_api); + canvas.root_module.linkLibrary(cairo); + canvas.root_module.linkLibrary(libjpeg_turbo); + canvas.root_module.linkLibrary(libpng); + canvas.root_module.linkLibrary(giflib); + canvas.root_module.linkLibrary(sheenbidi); + canvas.root_module.linkLibrary(unicode); + canvas.root_module.linkLibrary(harfbuzz); + canvas.root_module.linkLibrary(lunasvg); // some deps, especially lunasvg, compile with -ffunction-sections and // -fdata-sections so we don't have to distribute unused parts of libraries canvas.link_gc_sections = true; if (target.result.os.tag == .windows) { - canvas.linkSystemLibrary("dwrite"); + canvas.root_module.linkSystemLibrary("dwrite", .{}); } if (target.result.os.tag == .macos) { @@ -200,19 +202,19 @@ pub fn build(b: *std.Build) void { " \r\n", ); - canvas.addSystemFrameworkPath(.{ + canvas.root_module.addSystemFrameworkPath(.{ .cwd_relative = b.pathJoin(&.{ path, "Platforms/MacOSX.platform/Developer/SDKs/" ++ sdk ++ "/System/Library/Frameworks", }), }); - canvas.addSystemIncludePath(.{ + canvas.root_module.addSystemIncludePath(.{ .cwd_relative = b.pathJoin(&.{ path, "Platforms/MacOSX.platform/Developer/SDKs/" ++ sdk ++ "/usr/include", }), }); - canvas.addLibraryPath(.{ + canvas.root_module.addLibraryPath(.{ .cwd_relative = b.pathJoin(&.{ path, "Platforms/MacOSX.platform/Developer/SDKs/" ++ sdk ++ "/usr/lib", diff --git a/build.zig.zon b/build.zig.zon index aad0300e7..7e2c32a00 100644 --- a/build.zig.zon +++ b/build.zig.zon @@ -3,7 +3,7 @@ .fingerprint = 0xa59f6c18a5521654, .version = "0.0.0", .paths = .{""}, - .minimum_zig_version = "0.15.1", + .minimum_zig_version = "0.16.0", // Sub-packages only specify dependencies for upstream C/C++ code. // Everything else goes here so it's easier to share dependencies and ensure // we don't build duplicate versions. Generally build.zig[.zon] files should @@ -22,8 +22,8 @@ }, // Zig dependencies don't need as much tweaking .zg = .{ - .url = "https://codeberg.org/atman/zg/archive/d9f596626e8ec05a9f3e47f7bc83aedd5bd2f989.tar.gz", - .hash = "zg-0.15.3-oGqU3P5ItAJHmwDLfzX6sg6Xc5MhSJU2TctHfWu_sKLv", + .url = "https://codeberg.org/atman/zg/archive/ce364b17d60b28fda1af24c6575b93c47afe043e.tar.gz", + .hash = "zg-0.16.2-oGqU3HqftgI6rwFS90ypwfjgToH8Pz829OKuFURlm773", }, .giflib = .{ .path = "./pkg/giflib" }, .libpng = .{ .path = "./pkg/libpng" }, diff --git a/pkg/cairo/build.zig b/pkg/cairo/build.zig index e74c9773e..8390ff2f7 100644 --- a/pkg/cairo/build.zig +++ b/pkg/cairo/build.zig @@ -18,7 +18,7 @@ pub fn build(b: *std.Build) !void { lib.link_data_sections = true; lib.link_function_sections = true; - lib.addIncludePath(cairo.path("src")); + lib.root_module.addIncludePath(cairo.path("src")); var cairo_sources = std.ArrayList([]const u8).empty; defer cairo_sources.deinit(b.allocator); @@ -143,14 +143,14 @@ pub fn build(b: *std.Build) !void { const feature_config = b.addConfigHeader(.{ .include_path = "cairo-features.h" }, .{}); - lib.linkSystemLibrary("m"); + lib.root_module.linkSystemLibrary("m", .{}); const zlib = b.dependency("zlib", .{ .target = target, .optimize = optimize, }); - lib.linkLibrary(zlib.artifact("z")); + lib.root_module.linkLibrary(zlib.artifact("z")); config.addValues(.{ .HAVE_ZLIB = 1, @@ -169,7 +169,7 @@ pub fn build(b: *std.Build) !void { .optimize = optimize, }); - lib.linkLibrary(png.artifact("png")); + lib.root_module.linkLibrary(png.artifact("png")); config.addValues(.{ .CAIRO_CAN_TEST_SVG_SURFACE = 1, @@ -185,7 +185,7 @@ pub fn build(b: *std.Build) !void { .target = target, .optimize = optimize, }).artifact("freetype"); - lib.linkLibrary(freetype); + lib.root_module.linkLibrary(freetype); lib.installLibraryHeaders(freetype); feature_config.addValues(.{ .CAIRO_HAS_FT_FONT = 1 }); @@ -204,8 +204,8 @@ pub fn build(b: *std.Build) !void { "-DCAIRO_WIN32_STATIC_BUILD", }); - lib.linkSystemLibrary("gdi32"); - lib.linkSystemLibrary("msimg32"); + lib.root_module.linkSystemLibrary("gdi32", .{}); + lib.root_module.linkSystemLibrary("msimg32", .{}); feature_config.addValues(.{ .CAIRO_HAS_WIN32_SURFACE = 1, @@ -214,14 +214,14 @@ pub fn build(b: *std.Build) !void { } if (b.systemIntegrationOption("pixman", .{})) { - lib.linkSystemLibrary("pixman-1"); + lib.root_module.linkSystemLibrary("pixman-1", .{}); } else if (b.lazyDependency("pixman", .{ .target = target, .optimize = optimize, })) |dep| { const pixman = dep.artifact("pixman"); pixman.root_module.sanitize_c = .off; - lib.linkLibrary(pixman); + lib.root_module.linkLibrary(pixman); lib.installLibraryHeaders(pixman); } @@ -235,7 +235,7 @@ pub fn build(b: *std.Build) !void { .CAIRO_HAS_OBSERVER_SURFACE = 1, }); - lib.linkSystemLibrary("pthread"); + lib.root_module.linkSystemLibrary("pthread", .{}); config.addValues(.{ .CAIRO_HAS_PTHREAD = 1, @@ -250,10 +250,10 @@ pub fn build(b: *std.Build) !void { if (!target.result.cpu.arch.isX86()) config.addValues(.{ .ATOMIC_OP_NEEDS_MEMORY_BARRIER = 1 }); - lib.addConfigHeader(config); - lib.addConfigHeader(feature_config); + lib.root_module.addConfigHeader(config); + lib.root_module.addConfigHeader(feature_config); - lib.addCSourceFiles(.{ + lib.root_module.addCSourceFiles(.{ .root = cairo.path("src"), .files = try cairo_sources.toOwnedSlice(b.allocator), .flags = c_flags.items, diff --git a/pkg/freetype/build.zig b/pkg/freetype/build.zig index 83de08398..2e97bf930 100644 --- a/pkg/freetype/build.zig +++ b/pkg/freetype/build.zig @@ -16,7 +16,7 @@ pub fn build(b: *std.Build) !void { }), }); - lib.addIncludePath(upstream.path("include")); + lib.root_module.addIncludePath(upstream.path("include")); lib.installHeadersDirectory(upstream.path("include"), "", .{}); // Dependencies diff --git a/pkg/freetype/build.zig.zon b/pkg/freetype/build.zig.zon index 0ca3aeed4..2fc664792 100644 --- a/pkg/freetype/build.zig.zon +++ b/pkg/freetype/build.zig.zon @@ -1,20 +1,13 @@ .{ .name = .freetype, + .fingerprint = 0xac2059b6c1cddc9f, .version = "0.0.0", .dependencies = .{ .freetype = .{ .url = "https://github.com/freetype/freetype/archive/refs/tags/VER-2-14-1.tar.gz", .hash = "N-V-__8AAJx1qgDwI7KTM6yDZ4ArrZU6qmKKuvDHRVrxUkTw", }, - // TODO: this needs to be kept in sync with the root build.zig.zon - .zlib = .{ - .url = "git+https://github.com/allyourcodebase/zlib#3599c16d41dbe749ae51b0ff7ab864c61adc779a", - .hash = "zlib-1.3.1-ZZQ7lc8NAABUbHzDe_cSWboCqMbrLkVwvFkKnojgeiT2", - }, - // TODO: this needs to be kept in sync with the root build.zig.zon - .libpng = .{ - .url = "https://github.com/allyourcodebase/libpng/archive/d512607515687aa60b975d6a191aef9a692dac87.zip", - .hash = "libpng-1.6.50-oiaFGt4qAAB_vzrtvz9hA7_UiBW4arSaMVGc7ekKMCZ2", - }, + .zlib = .{ .path = "../zlib" }, + .libpng = .{ .path = "../libpng" }, }, } diff --git a/pkg/harfbuzz/build.zig b/pkg/harfbuzz/build.zig index 42fab075d..0e979c795 100644 --- a/pkg/harfbuzz/build.zig +++ b/pkg/harfbuzz/build.zig @@ -11,17 +11,16 @@ pub fn build(b: *std.Build) !void { .root_module = b.createModule(.{ .target = target, .optimize = optimize, + .link_libc = true, + .link_libcpp = true, .pic = true, }) }); - harfbuzz.addCSourceFile(.{ + harfbuzz.root_module.addCSourceFile(.{ .file = upstream.path("src/harfbuzz.cc"), }); harfbuzz.installHeadersDirectory(upstream.path("src"), "", .{}); - harfbuzz.linkLibC(); - harfbuzz.linkLibCpp(); - b.installArtifact(harfbuzz); } diff --git a/pkg/harfbuzz/build.zig.zon b/pkg/harfbuzz/build.zig.zon index 9251071cb..08a1d32fa 100644 --- a/pkg/harfbuzz/build.zig.zon +++ b/pkg/harfbuzz/build.zig.zon @@ -1,5 +1,6 @@ .{ .name = .harfbuzz, + .fingerprint = 0xbd60917c14346ca6, .version = "0.0.0", .dependencies = .{ .harfbuzz = .{ diff --git a/pkg/lunasvg/build.zig b/pkg/lunasvg/build.zig index 060609bad..60c9ddc9c 100644 --- a/pkg/lunasvg/build.zig +++ b/pkg/lunasvg/build.zig @@ -10,6 +10,8 @@ pub fn build(b: *std.Build) !void { .root_module = b.createModule(.{ .target = target, .optimize = optimize, + .pic = true, + .link_libc = true, }) }); @@ -19,7 +21,7 @@ pub fn build(b: *std.Build) !void { .optimize = optimize, }).artifact("cairo"); - plutovg.addCSourceFiles(.{ + plutovg.root_module.addCSourceFiles(.{ .root = upstream.path(""), .files = &.{ "plutovg/source/plutovg-blend.c", @@ -46,10 +48,9 @@ pub fn build(b: *std.Build) !void { } }); - plutovg.addIncludePath(upstream.path("plutovg/include")); + plutovg.root_module.addIncludePath(upstream.path("plutovg/include")); plutovg.installHeadersDirectory(upstream.path("plutovg/include"), "", .{}); - plutovg.linkLibrary(cairo); - plutovg.linkLibC(); + plutovg.root_module.linkLibrary(cairo); // strip out anything we didn't use, which for plutovg is a lot (on this fork) plutovg.link_data_sections = true; @@ -61,10 +62,13 @@ pub fn build(b: *std.Build) !void { .root_module = b.createModule(.{ .target = target, .optimize = optimize, + .pic = true, + .link_libc = true, + .link_libcpp = true, }) }); - lunasvg.addCSourceFiles(.{ + lunasvg.root_module.addCSourceFiles(.{ .root = upstream.path(""), .files = &.{ "source/lunasvg.cpp", @@ -98,12 +102,10 @@ pub fn build(b: *std.Build) !void { lunasvg.link_data_sections = true; lunasvg.link_function_sections = true; - lunasvg.addIncludePath(upstream.path("include")); + lunasvg.root_module.addIncludePath(upstream.path("include")); lunasvg.installHeadersDirectory(upstream.path("include"), "", .{}); - lunasvg.linkLibrary(plutovg); - lunasvg.linkLibC(); - lunasvg.linkLibCpp(); - lunasvg.linkLibrary(cairo); + lunasvg.root_module.linkLibrary(plutovg); + lunasvg.root_module.linkLibrary(cairo); b.installArtifact(lunasvg); } diff --git a/pkg/lunasvg/build.zig.zon b/pkg/lunasvg/build.zig.zon index 631ab2fdb..30ddec25c 100644 --- a/pkg/lunasvg/build.zig.zon +++ b/pkg/lunasvg/build.zig.zon @@ -1,5 +1,6 @@ .{ .name = .lunasvg, + .fingerprint = 0xd309c055ae5fca1b, .version = "0.0.0", .dependencies = .{ .cairo = .{ .path = "../cairo" }, diff --git a/pkg/pixman/build.zig b/pkg/pixman/build.zig index 2877db1d3..3e4d42f71 100644 --- a/pkg/pixman/build.zig +++ b/pkg/pixman/build.zig @@ -8,10 +8,11 @@ pub fn build(b: *std.Build) !void { .root_module = b.createModule(.{ .target = b.standardTargetOptions(.{}), .optimize = b.standardOptimizeOption(.{}), + .link_libc = true, + .pic = true, }), }); - lib.linkLibC(); lib.root_module.addCMacro("HAVE_CONFIG_H", "1"); const config_h = b.addConfigHeader(.{ @@ -21,11 +22,11 @@ pub fn build(b: *std.Build) !void { .PACKAGE = "FOO", .HAVE_PTHREADS = "1", }); - lib.addConfigHeader(config_h); + lib.root_module.addConfigHeader(config_h); - lib.addIncludePath(upstream.path("pixman")); + lib.root_module.addIncludePath(upstream.path("pixman")); lib.installHeadersDirectory(upstream.path("pixman"), "", .{}); - lib.addCSourceFiles(.{ + lib.root_module.addCSourceFiles(.{ .root = upstream.path("pixman"), .files = sources, }); diff --git a/pkg/pixman/build.zig.zon b/pkg/pixman/build.zig.zon index 7da919bc9..aaaefe0ab 100644 --- a/pkg/pixman/build.zig.zon +++ b/pkg/pixman/build.zig.zon @@ -1,5 +1,6 @@ .{ .name = .pixman, + .fingerprint = 0x16aa7fe7133d4f93, .version = "0.0.0", .dependencies = .{ .pixman = .{ diff --git a/pkg/sheenbidi/build.zig b/pkg/sheenbidi/build.zig index dfed2fd93..e4f4bafd1 100644 --- a/pkg/sheenbidi/build.zig +++ b/pkg/sheenbidi/build.zig @@ -12,17 +12,17 @@ pub fn build(b: *std.Build) !void { .target = target, .optimize = optimize, .pic = true, + .link_libc = true, }) }); - sheenbidi.addCSourceFile(.{ + sheenbidi.root_module.addCSourceFile(.{ .file = upstream.path("Source/SheenBidi.c"), .flags = &.{"-DSB_CONFIG_UNITY"}, }); - sheenbidi.addIncludePath(upstream.path("Headers")); + sheenbidi.root_module.addIncludePath(upstream.path("Headers")); sheenbidi.installHeadersDirectory(upstream.path("Headers"), "", .{}); - sheenbidi.linkLibC(); b.installArtifact(sheenbidi); } diff --git a/pkg/sheenbidi/build.zig.zon b/pkg/sheenbidi/build.zig.zon index f7fd54474..461572b37 100644 --- a/pkg/sheenbidi/build.zig.zon +++ b/pkg/sheenbidi/build.zig.zon @@ -1,5 +1,6 @@ .{ .name = .sheenbidi, + .fingerprint = 0x684adc4fa57f4871, .version = "0.0.0", .dependencies = .{ .sheenbidi = .{ diff --git a/src/unicode.zig b/src/unicode.zig index 30d6237d9..902e3cce4 100644 --- a/src/unicode.zig +++ b/src/unicode.zig @@ -181,17 +181,8 @@ pub const Script = enum(c_int) { Zanabazar_Square, }; -var scripts: Scripts = undefined; -var initializedScripts: bool = false; - export fn get_script(cp: u32) callconv(.c) Script { - // TODO: initialize once, not every get_script call - if (!initializedScripts) { - scripts = Scripts.init(std.heap.c_allocator) catch @panic("Failed to initialize scripts"); - initializedScripts = true; - } - - const script_result = scripts.script(@as(u21, @intCast(cp))) orelse return .none; + const script_result = Scripts.script(@as(u21, @intCast(cp))) orelse return .none; return switch (script_result) { .none => .none, @@ -368,19 +359,10 @@ export fn get_script(cp: u32) callconv(.c) Script { }; } -var graphemes: Graphemes = undefined; -var initializedGraphemes: bool = false; - export fn grapheme_break( state: *Graphemes.IterState, cp1: u32, cp2: u32 ) callconv(.c) bool { - // TODO: initialize once, not every get_script call - if (!initializedGraphemes) { - graphemes = Graphemes.init(std.heap.c_allocator) catch @panic("Failed to initialize graphemes"); - initializedGraphemes = true; - } - - return Graphemes.graphemeBreak(@as(u21, @intCast(cp1)), @as(u21, @intCast(cp2)), &graphemes, state); + return Graphemes.graphemeBreak(@as(u21, @intCast(cp1)), @as(u21, @intCast(cp2)), state); }