From d9ce760d191c8a5fd91504e023f5719549e7ba7c Mon Sep 17 00:00:00 2001 From: Michael Smith Date: Thu, 11 May 2023 23:39:28 +0200 Subject: [PATCH] Initial commit --- .gitignore | 132 ++ LICENSE | 21 + README.md | 63 + assets/TEST.LBM | Bin 0 -> 7078 bytes docs/IFF.asc | 1424 +++++++++++++++++ docs/PBM.asc | 19 + index.html | 17 + main.js | 91 ++ package-lock.json | 1906 +++++++++++++++++++++++ package.json | 18 + src/binarystream.js | 94 ++ src/pbm.js | 240 +++ style.css | 17 + tests/fixtures/ASH.LBM | Bin 0 -> 76502 bytes tests/fixtures/INVALID_CHUNK_ID.LBM | Bin 0 -> 7078 bytes tests/fixtures/INVALID_CHUNK_LENGTH.LBM | Bin 0 -> 7020 bytes tests/fixtures/SEASCAPE.LBM | Bin 0 -> 32764 bytes tests/fixtures/VALID.LBM | Bin 0 -> 7078 bytes tests/pbm.test.js | 143 ++ 19 files changed, 4185 insertions(+) create mode 100644 .gitignore create mode 100644 LICENSE create mode 100644 README.md create mode 100644 assets/TEST.LBM create mode 100644 docs/IFF.asc create mode 100644 docs/PBM.asc create mode 100644 index.html create mode 100644 main.js create mode 100644 package-lock.json create mode 100644 package.json create mode 100644 src/binarystream.js create mode 100644 src/pbm.js create mode 100644 style.css create mode 100644 tests/fixtures/ASH.LBM create mode 100644 tests/fixtures/INVALID_CHUNK_ID.LBM create mode 100644 tests/fixtures/INVALID_CHUNK_LENGTH.LBM create mode 100644 tests/fixtures/SEASCAPE.LBM create mode 100644 tests/fixtures/VALID.LBM create mode 100644 tests/pbm.test.js diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..ceaea36 --- /dev/null +++ b/.gitignore @@ -0,0 +1,132 @@ +# ---> Node +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +lerna-debug.log* +.pnpm-debug.log* + +# Diagnostic reports (https://nodejs.org/api/report.html) +report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json + +# Runtime data +pids +*.pid +*.seed +*.pid.lock + +# Directory for instrumented libs generated by jscoverage/JSCover +lib-cov + +# Coverage directory used by tools like istanbul +coverage +*.lcov + +# nyc test coverage +.nyc_output + +# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) +.grunt + +# Bower dependency directory (https://bower.io/) +bower_components + +# node-waf configuration +.lock-wscript + +# Compiled binary addons (https://nodejs.org/api/addons.html) +build/Release + +# Dependency directories +node_modules/ +jspm_packages/ + +# Snowpack dependency directory (https://snowpack.dev/) +web_modules/ + +# TypeScript cache +*.tsbuildinfo + +# Optional npm cache directory +.npm + +# Optional eslint cache +.eslintcache + +# Optional stylelint cache +.stylelintcache + +# Microbundle cache +.rpt2_cache/ +.rts2_cache_cjs/ +.rts2_cache_es/ +.rts2_cache_umd/ + +# Optional REPL history +.node_repl_history + +# Output of 'npm pack' +*.tgz + +# Yarn Integrity file +.yarn-integrity + +# dotenv environment variable files +.env +.env.development.local +.env.test.local +.env.production.local +.env.local + +# parcel-bundler cache (https://parceljs.org/) +.cache +.parcel-cache + +# Next.js build output +.next +out + +# Nuxt.js build / generate output +.nuxt +dist + +# Gatsby files +.cache/ +# Comment in the public line in if your project uses Gatsby and not Next.js +# https://nextjs.org/blog/next-9-1#public-directory-support +# public + +# vuepress build output +.vuepress/dist + +# vuepress v2.x temp and cache directory +.temp +.cache + +# Docusaurus cache and generated files +.docusaurus + +# Serverless directories +.serverless/ + +# FuseBox cache +.fusebox/ + +# DynamoDB Local files +.dynamodb/ + +# TernJS port file +.tern-port + +# Stores VSCode versions used for testing VSCode extensions +.vscode-test + +# yarn v2 +.yarn/cache +.yarn/unplugged +.yarn/build-state.yml +.yarn/install-state.gz +.pnp.* + diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..731f2eb --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2023 Michael Smith + +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/README.md b/README.md new file mode 100644 index 0000000..2f3f067 --- /dev/null +++ b/README.md @@ -0,0 +1,63 @@ +# pbm-js + +JavaScript library and proof of concept viewer for IFF PBM files. + +This is the format used by the PC version of Deluxe Paint II and is different from the Amiga version. + +## Features + +- Written using ES6 modules, runs out of the box in modern browsers. +- 100% plain JavaScript. No dependencies. + +## Usage + +### In the browser + +_Also see `index.html` and `main.js` for a more elaborate example that renders the palletized image data to a html5 canvas._ + +```javascript +import PBM from "./src/pbm.js"; + +fetch("/assets/TEST.LBM") + .then((response) => { + return response.arrayBuffer(); + }) + .then((buffer) => { + const image = new PBM(buffer); + console.log(image); + }); +``` + +### In Node.js + +```javascript +import * as fs from "fs"; + +import PBM from "./src/pbm.js"; + +const data = fs.readFileSync("./tests/fixtures/VALID.LBM"); +const image = new PBM(data.buffer); +console.log(image); +``` + +## Testing + +```sh +# Install test framework and dependencies (requires Node.js) +npm install +# Run the tests +npm run test +``` + +## Missing features + +- Color Cycling + +## References + +- [libiff](https://github.com/svanderburg/libiff): Portable, extensible parser for the Interchange File Format (IFF). Well documented and very detailed C implementation of the IFF file format by [Sander van der Burg](http://sandervanderburg.nl/index.php). +- [DPaint-js](https://github.com/steffest/DPaint-js): Web based image editor, modeled after the legendary Deluxe Paint with a focus on retro Amiga file formats: read and write Amiga icon files and IFF ILBM images. + +## Contributing + +This software is currently in alpha version. Bug reports and pull requests welcome. diff --git a/assets/TEST.LBM b/assets/TEST.LBM new file mode 100644 index 0000000000000000000000000000000000000000..8e5ff8fe0284e2d4caf42857a469184908a22d02 GIT binary patch literal 7078 zcmeI0F^C&S6o&u36zd|$DH2j#VZjzIrciN(3k96q9JvH>USS19*kS`J6bM9wEmXMJ z7!@L@kl~6H!kNOw6)KETv4x5)t{AY2EmXMJfC3lT!o>xQxgy1%SzRtjB!LvU5R%zp z=gpgaJNwVan}MAV{rgA2?sv`oqw4~2Zk|ZW_GMS9W<2a6^DC%~*=CwwnQLoo) zwVLO71kQmo;03S%QeXz0025#Y#6SdefdKFUY2@WO=W{B>IA$19O0EatG~`5bAG> z+oAN@cW*W7yK-;0UVB$~H!l`Kv*5G}=X<*! z{6jmy)gElEC143y0+xU!U." + +A LIST may have at most one PROP of a FORM type, and all the PROPs +must appear before any of the FORMs or nested LISTs and CATs. You +can have subsequences of FORMs sharing properties by making each subsequence +a LIST. + +Scoping: Think of property settings as variable bindings in nested +blocks of a programming language. Where in C you could write: + +TEXT_FONT text_font = Courier; /* program's global default */ + +File(); { + TEXT_FONT text_font = TimesRoman; /* shared setting */ + + { + TEXT_FONT text_font = Helvetica; /* local setting */ + Print("Hello "); /* uses font Helvetica */ + } + + { + Print("there."); /* uses font TimesRoman */ + } + } + +An IFF file could contain: + +LIST { + PROP TEXT { + FONT {TimesRoman} /* shared setting */ + } + + FORM TEXT { + FONT {Helvetica} /* local setting */ + CHRS {Hello } /* uses font Helvetica */ + } + + FORM TEXT { + CHRS {there.} /* uses font TimesRoman */ + } + } + +The shared property assignments selectively override the reader's +global defaults, but only for FORMs within the group. A FORM's own +property assignments selectively override the global and group-supplied +values. So when reading an IFF file, keep property settings on a stack. +They're designed to be small enough to hold in main memory. + +Shared properties are semantically equivalent to copying those properties +into each of the nested FORMs right after their FORM type IDs. + +Properties for LIST + +Optional "properties for LIST" store the origin of the list's contents +in a PROP chunk for the fake FORM type "LIST". They are the properties +originating program "OPGM", processor family "OCPU", computer type +"OCMP", computer serial number or network address "OSN ", and user +name "UNAM". In our imperfect world, these could be called upon to +distinguish between unintended variations of a data format or to work +around bugs in particular originating/receiving program pairs. Issue: +Specify the format of these properties. + +A creation date could also be stored in a property but let's ask that +file creating, editing, and transporting programs maintain the correct +date in the local file system. Programs that move files between machine +types are expected to copy across the creation dates. + + + +6. Standard File Structure + +File Structure Overview + +An IFF file is just a single chunk of type FORM, LIST, or CAT. Therefore +an IFF file can be recognized by its first 4 bytes: "FORM", "LIST", +or "CAT ". Any file contents after the chunk's end are to be ignored. + +Since an IFF file can be a group of objects, programs that read/write +single objects can communicate to an extent with programs that read/write +groups. You're encouraged to write programs that handle all the objects +in a LIST or CAT. A graphics editor, for example, could process a +list of pictures as a multiple page document, one page at a time. + +Programs should enforce IFF's syntactic rules when reading and writing +files. This ensures robust data transfer. The public domain IFF reader/writer +subroutine package does this for you. A utility program "IFFCheck" +is available that scans an IFF file and checks it for conformance +to IFF's syntactic rules. IFFCheck also prints an outline of the chunks +in the file, showing the ckID and ckSize of each. This is quite handy +when building IFF programs. Example programs are also available to +show details of reading and writing IFF files. + +A merge program "IFFJoin" will be available that logically appends +IFF files into a single CAT group. It "unwraps" each input file that +is a CAT so that the combined file isn't nested CATs. + +If we need to revise the IFF standard, the three anchoring IDs will +be used as "version numbers". That's why IDs "FOR1" through "FOR9", +"LIS1" through "LIS9", and "CAT1" through "CAT9" are reserved. + +IFF formats are designed for reasonable performance with floppy disks. +We achieve considerable simplicity in the formats and programs by +relying on the host file system rather than defining universal grouping +structures like directories for LIST contents. On huge storage systems, +IFF files could be leaf nodes in a file structure like a B-tree. Let's +hope the host file system implements that for us! + +Thre are two kinds of IFF files: single purpose files and scrap files. +They differ in the interpretation of multiple data objects and in +the file's external type. + +Single Purpose Files + +A single purpose IFF file is for normal "document" and "archive" storage. +This is in contrast with "scrap files" (see below) and temporary backing +storage (non-interchange files). + +The external file type (or filename extension, depending on the host +file system) indicates the file's contents. It's generally the FORM +type of the data contained, hence the restrictions on FORM type IDs. + +Programmers and users may pick an "intended use" type as the filename +extension to make it easy to filter for the relevant files in a filename +requestor. This is actually a "subclass" or "subtype" that conveniently +separates files of the same FORM type that have different uses. Programs +cannot demand conformity to its expected subtypes without overly restricting +data interchange since they cannot know about the subtypes to be used +by future programs that users will want to exchange data with. + +Issue: How to generate 3-letter MS-DOS extensions from 4-letter FORM +type IDs? + +Most single purpose files will be a single FORM (perhaps a composite +FORM like a musical score containing nested FORMs like musical instrument +descriptions). If it's a LIST or a CAT, programs should skip over +unrecognized objects to read the recognized ones or the first recognized +one. Then a program that can read a single purpose file can read something +out of a "scrap file", too. + +Scrap Files + +A "scrap file" is for maximum interconnectivity in getting data between +programs; the core of a clipboard function. Scrap files may have type +"IFF " or filename extension ".IFF". + +A scrap file is typically a CAT containing alternate representations +of the same basic information. Include as many alternatives as you +can readily generate. This redundancy improves interconnectivity in +situations where we can't make all programs read and write super-general +formats. [Inside Macintosh chapter "Scrap Manager".] E.g. a graphically- +annotated musical score might be supplemented by a stripped down 4-voice +melody and by a text (the lyrics). + +The originating program should write the alternate representations +in order of "preference": most preferred (most comprehensive) type +to least preferred (least comprehensive) type. A receiving program +should either use the first appearing type that it understands or +search for its own "preferred" type. + +A scrap file should have at most one alternative of any type. (A LIST +of same type objects is ok as one of the alternatives.) But don't +count on this when reading; ignore extra sections of a type. Then +a program that reads scrap files can read something out of single +purpose files. + +Rules for Reader Programs + +Here are some notes on building programs that read IFF files. If you +use the standard IFF reader module "IFFR.C", many of these rules and +details will be automatically handled. (See "Support Software" in +Appendix A.) We recommend that you start from the example program +"ShowILBM.C". You should also read up on recursive descent parsers. +[See, for example, Compiler Construction, An Advanced Course.] + +% The standard is very flexible so many programs can exchange +data. This implies a program has to scan the file and react to what's +actually there in whatever order it appears. An IFF reader program +is a parser. + +% For interchange to really work, programs must be willing to +do some conversion during read-in. If the data isn't exactly what +you expect, say, the raster is smaller than those created by your +program, then adjust it. Similarly, your program could crop a large +picture, add or drop bitplanes, and create/discard a mask plane. The +program should give up gracefully on data that it can't convert. + +% If it doesn't start with "FORM", "LIST", or "CAT ", it's not +an IFF-85 file. + +% For any chunk you encounter, you must recognize its type ID +to understand its contents. + +% For any FORM chunk you encounter, you must recognize its FORM +type ID to understand the contained "local chunks". Even if you don't +recognize the FORM type, you can still scan it for nested FORMs, LISTs, +and CATs of interest. + +% Don't forget to skip the pad byte after every odd-length chunk. + +% Chunk types LIST, FORM, PROP, and CAT are generic groups. They +always contain a subtype ID followed by chunks. + +% Readers ought to handle a CAT of FORMs in a file. You may treat +the FORMs like document pages to sequence through or just use the +first FORM. + +% Simpler IFF readers completely skip LISTs. "Fully IFF-conforming" +readers are those that handle LISTs, even if just to read the first +FORM from a file. If you do look into a LIST, you must process shared +properties (in PROP chunks) properly. The idea is to get the correct +data or none at all. + +% The nicest readers are willing to look into unrecognized FORMs +for nested FORM types that they do recognize. For example, a musical +score may contain nested instrument descriptions and an animation +file may contain still pictures. + +Note to programmers: Processing PROP chunks is not simple! You'll +need some background in interpreters with stack frames. If this is +foreign to you, build programs that read/write only one FORM per file. +For the more intrepid programmers, the next paragraph summarizes how +to process LISTs and PROPs. See the general IFF reader module "IFFR.C" +and the example program "ShowILBM.C" for details. + +Allocate a stack frame for every LIST and FORM you encounter and initialize +it by copying the stack frame of the parent LIST or FORM. At the top +level, you'll need a stack frame initialized to your program's global +defaults. While reading each LIST or FORM, store all encountered properties +into the current stack frame. In the example ShowILBM, each stack +frame has a place for a bitmap header property ILBM.BMHD and a color +map property ILBM.CMAP. When you finally get to the ILBM's BODY chunk, +use the property settings accumulated in the current stack frame. + +An alternate implementation would just remember PROPs encountered, +forgetting each on reaching the end of its scope (the end of the containing +LIST). When a FORM XXXX is encountered, scan the chunks in all remembered +PROPs XXXX, in order, as if they appeared before the chunks actually +in the FORM XXXX. This gets trickier if you read FORMs inside of FORMs. + +Rules for Writer Programs + +Here are some notes on building programs that write IFF files, which +is much easier than reading them. If you use the standard IFF writer +module "IFFW.C" (see "Support Software" in Appendix A), many of these +rules and details will automatically be enforced. See the example +program "Raw2ILBM.C". + +% An IFF file is a single FORM, LIST, or CAT chunk. + +% Any IFF-85 file must start with the 4 characters "FORM", "LIST", +or "CAT ", followed by a LONG ckSize. There should be no data after +the chunk end. + +% Chunk types LIST, FORM, PROP, and CAT are generic. They always +contain a subtype ID followed by chunks. These three IDs are universally +reserved, as are "LIS1" through "LIS9", "FOR1" through "FOR9", "CAT1" +through "CAT9", and " ". + +% Don't forget to write a 0 pad byte after each odd-length chunk. + +% Four techniques for writing an IFF group: (1) build the data +in a file mapped into virtual memory, (2) build the data in memory +blocks and use block I/O, (3) stream write the data piecemeal and +(don't forget!) random access back to set the group length count, +and (4) make a preliminary pass to compute the length count then stream +write the data. + +% Do not try to edit a file that you don't know how to create. +Programs may look into a file and copy out nested FORMs of types that +they recognize, but don't edit and replace the nested FORMs and don't +add or remove them. That could make the containing structure inconsistent. +You may write a new file containing items you copied (or copied and +modified) from another IFF file, but don't copy structural parts you +don't understand. + +% You must adhere to the syntax descriptions in Appendex A. E.g. +PROPs may only appear inside LISTs. + + + + +Appendix A. Reference + +Type Definitions + +The following C typedefs describe standard IFF structures. Declarations +to use in practice will vary with the CPU and compiler. For example, +68000 Lattice C produces efficient comparison code if we define ID +as a "LONG". A macro "MakeID" builds these IDs at compile time. + +/* Standard IFF types, expressed in 68000 Lattice C. */ + +typedef unsigned char UBYTE; /* 8 bits unsigned */ +typedef short WORD; /* 16 bits signed */ +typedef unsigned short UWORD; /* 16 bits unsigned */ +typedef long LONG; /* 32 bits signed */ + +typedef char ID[4]; /* 4 chars in ' ' through '~' */ + +typedef struct { + ID ckID; + LONG ckSize; /* sizeof(ckData) */ + UBYTE ckData[/* ckSize */]; + } Chunk; + +/* ID typedef and builder for 68000 Lattice C. */ +typedef LONG ID; /* 4 chars in ' ' through '~' */ +#define MakeID(a,b,c,d) ( (a)<<<<24 | (b)<<<<16 | (c)<<<<8 | (d) ) + +/* Globally reserved IDs. */ +#define ID_FORM MakeID('F','O','R','M') +#define ID_LIST MakeID('L','I','S','T') +#define ID_PROP MakeID('P','R','O','P') +#define ID_CAT MakeID('C','A','T',' ') +#define ID_FILLER MakeID(' ',' ',' ',' ') + +Syntax Definitions + +Here's a collection of the syntax definitions in this document. + +Chunk ::= ID #{ UBYTE* } [0] + +Property ::= Chunk + +FORM ::= "FORM" #{ FormType (LocalChunk | FORM | LIST | CAT)* +} +FormType ::= ID +LocalChunk ::= Property | Chunk + +CAT ::= "CAT " #{ ContentsType (FORM | LIST | CAT)* } +ContentsType ::= ID -- a hint or an "abstract data type" ID + +LIST ::= "LIST" #{ ContentsType PROP* (FORM | LIST | CAT)* } +PROP ::= "PROP" #{ FormType Property* } + +In this extended regular expression notation, the token "#" represents +a ckSize LONG count of the following {braced} data bytes. Literal +items are shown in "quotes", [square bracketed items] are optional, +and "*" means 0 or more instances. A sometimes-needed pad byte is +shown as "[0]". + +Defined Chunk IDs + +This is a table of currently defined chunk IDs. We may also borrow +some Macintosh IDs and data formats. + +Group chunk IDs + FORM, LIST, PROP, CAT. +Future revision group chunk IDs + FOR1 I FOR9, LIS1 I LIS9, CAT1 I CAT9. +FORM type IDs + (The above group chunk IDs may not be used for FORM type IDs.) + (Lower case letters and punctuation marks are forbidden in FORM +type IDs.) + 8SVX 8-bit sampled sound voice, ANBM animated bitmap, FNTR raster +font, FNTV vector font, FTXT formatted text, GSCR general-use musical +score, ILBM interleaved raster bitmap image, PDEF Deluxe Print page +definition, PICS Macintosh picture, PLBM (obsolete), USCR Uhuru Sound +Software musical score, UVOX Uhuru Sound Software Macintosh voice, +SMUS simple musical score, VDEO Deluxe Video Construction Set video. +Data chunk IDs + "JJJJ", TEXT, PICT. +PROP LIST property IDs + OPGM, OCPU, OCMP, OSN, UNAM. + + + +Support Software + +These public domain C source programs are available for use in building +IFF-compatible programs: + +IFF.H, IFFR.C, IFFW.C + + IFF reader and writer package. + These modules handle many of the details of reliably + reading and writing IFF files. + +IFFCheck.C This handy utility program scans an IFF file, checks + that the contents are well formed, and prints an outline + of the chunks. + +PACKER.H, Packer.C, UnPacker.C + + Run encoder and decoder used for ILBM files. + +ILBM.H, ILBMR.C, ILBMW.C + + Reader and writer support routines for raster image + FORM ILBM. ILBMR calls IFFR and UnPacker. ILBMW calls + IFFW and Packer. + +ShowILBM.C + Example caller of IFFR and ILBMR modules. This + Commodore-Amiga program reads and displays a FORM ILBM. +Raw2ILBM.C + Example ILBM writer program. As a demonstration, it + reads a raw raster image file and writes the image + as a FORM ILBM file. +ILBM2Raw.C + Example ILBM reader program. Reads a FORM ILBM file + and writes it into a raw raster image. + +REMALLOC.H, Remalloc.c + + Memory allocation routines used in these examples. + +INTUALL.H generic "include almost everything" include-file + with the sequence of includes correctly specified. + +READPICT.H, ReadPict.c + + given an ILBM file, read it into a bitmap and + a color map + +PUTPICT.H, PutPict.c + + given a bitmap and a color map, save it as + an ILBM file. + +GIO.H, Gio.c generic I/O speedup package. Attempts to speed + disk I/O by buffering writes and reads. + +giocall.c sample call to gio. + +ilbmdump.c reads in ILBM file, prints out ascii representation + for including in C files. + +bmprintc.c prints out a C-language representation of data for + a bitmap. + + + +Example Diagrams + +Here's a box diagram for an example IFF file, a raster image FORM +ILBM. This FORM contains a bitmap header property chunk BMHD, a color +map property chunk CMAP, and a raster data chunk BODY. This particular +raster is 320 x 200 pixels x 3 bit planes uncompressed. The "0" after +the CMAP chunk represents a zero pad byte; included since the CMAP +chunk has an odd length. The text to the right of the diagram shows +the outline that would be printed by the IFFCheck utility program +for this particular file. + + +-----------------------------------+ + |'FORM' 24070 | FORM 24070 IBLM + +-----------------------------------+ + |'ILBM' | + +-----------------------------------+ + | +-------------------------------+ | + | | 'BMHD' 20 | | .BMHD 20 + | | 320, 200, 0, 0, 3, 0, 0, ... | | + | + ------------------------------+ | + | | 'CMAP' 21 | | .CMAP 21 + | | 0, 0, 0; 32, 0, 0; 64,0,0; .. | | + | +-------------------------------+ | + | 0 | + +-----------------------------------+ + |'BODY' 24000 | .BODY 24000 + |0, 0, 0, ... | + +-----------------------------------+ + +This second diagram shows a LIST of two FORMs ILBM sharing a common +BMHD property and a common CMAP property. Again, the text on the right +is an outline a la IFFCheck. + + + +-----------------------------------------+ + |'LIST' 48114 | LIST 48114 AAAA + +-----------------------------------------+ + |'AAAA' | .PROP 62 ILBM + | +-----------------------------------+ | + | |'PROP' 62 | | + | +-----------------------------------+ | + | |'ILBM' | | + | +-----------------------------------+ | + | | +-------------------------------+ | | + | | | 'BMHD' 20 | | | ..BMHD 20 + | | | 320, 200, 0, 0, 3, 0, 0, ... | | | + | | | ------------------------------+ | | + | | | 'CMAP' 21 | | | ..CMAP 21 + | | | 0, 0, 0; 32, 0, 0; 64,0,0; .. | | | + | | +-------------------------------+ | | + | | 0 | | + | +-----------------------------------+ | + | +-----------------------------------+ | + | |'FORM' 24012 | | .FORM 24012 ILBM + | +-----------------------------------+ | + | |'ILBM' | | + | +-----------------------------------+ | + | | +-----------------------------+ | | + | | |'BODY' 24000 | | | ..BODY 24000 + | | |0, 0, 0, ... | | | + | | +-----------------------------+ | | + | +-----------------------------------+ | + | +-----------------------------------+ | + | |'FORM' 24012 | | .FORM 24012 ILBM + | +-----------------------------------+ | + | |'ILBM' | | + | +-----------------------------------+ | + | | +-----------------------------+ | | + | | |'BODY' 24000 | | | ..BODY 24000 + | | |0, 0, 0, ... | | | + | | +-----------------------------+ | | + | +-----------------------------------+ | + +-----------------------------------------+ + + + +Appendix B. Standards Committee + +The following people contributed to the design of this IFF standard: + +Bob "Kodiak" Burns, Commodore-Amiga +R. J. Mical, Commodore-Amiga +Jerry Morrison, Electronic Arts +Greg Riker, Electronic Arts +Steve Shaw, Electronic Arts +Barry Walsh, Commodore-Amiga diff --git a/docs/PBM.asc b/docs/PBM.asc new file mode 100644 index 0000000..79b251b --- /dev/null +++ b/docs/PBM.asc @@ -0,0 +1,19 @@ +NOTE(m): Copied from https://github.com/svanderburg/libilbm/blob/master/doc/additions/PBM.asc + +Apparently, The PC version of Deluxe Paint stores images in a slightly different +format compared to the Amiga version of the Deluxe Paint. After doing some +experiments, I have discovered the following differences: + +- The IFF form type used is: 'PBM ' instead of 'ILBM'. + +- As PC hardware does not know anything about bitplanes, its BODY uses chunky + format, in which every byte represents a pixel. + +- Interleaving scanlines is to smoothly render planar graphics data on an Amiga, + so that no visual corruption is visible while calculating the index values of + each pixel. On the PC this is not necessary, so we don't have to interleave or + deinterleave anything. + +- Some Amiga specific chunks, such as CAMG are not used. + +Sander van der Burg diff --git a/index.html b/index.html new file mode 100644 index 0000000..98b117d --- /dev/null +++ b/index.html @@ -0,0 +1,17 @@ + + + + + + + + + IFF PBM image viewer + + + + + + + + diff --git a/main.js b/main.js new file mode 100644 index 0000000..502bb71 --- /dev/null +++ b/main.js @@ -0,0 +1,91 @@ +/* + + MIT License + + Copyright (c) 2023 Michael Smith + + 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. + + */ + +import PBM from "./src/pbm.js"; + +const thumbnailCanvas = document.getElementById("thumbnail-canvas"); +const thumbnailContext = thumbnailCanvas.getContext("2d"); +const imageCanvas = document.getElementById("image-canvas"); +const imageContext = imageCanvas.getContext("2d"); +const inputElement = document.getElementById("imagefile"); +inputElement.addEventListener("change", handleFile, false); + +fetch("/assets/TEST.LBM") + .then((response) => { + return response.arrayBuffer(); + }) + .then((buffer) => { + const image = loadImage(buffer); + drawImage(image.thumbnail, thumbnailContext); + drawImage(image, imageContext); + }); + +function handleFile() { + const imageFile = this.files[0]; + const reader = new FileReader(); + + reader.onload = (evt) => { + const image = loadImage(evt.target.result); + drawImage(image.thumbnail, thumbnailContext); + drawImage(image, imageContext); + }; + + reader.readAsArrayBuffer(imageFile); +} + +function loadImage(buffer) { + const image = new PBM(buffer); + thumbnailCanvas.width = image.thumbnail.width; + thumbnailCanvas.height = image.thumbnail.height; + imageCanvas.width = image.width; + imageCanvas.height = image.height; + + return image; +} + +function drawImage(image, context) { + context.clearRect(0, 0, image.width, image.height); + let pixels = context.createImageData(image.width, image.height); + + for (let x = 0; x < image.width; x++) { + for (let y = 0; y < image.height; y++) { + const index = y * image.width + x; + const paletteIndex = image.pixelData[index]; + const pixelIndex = index * 4; + + const r = image.palette[paletteIndex][0]; + const g = image.palette[paletteIndex][1]; + const b = image.palette[paletteIndex][2]; + + pixels.data[pixelIndex] = r; + pixels.data[pixelIndex + 1] = g; + pixels.data[pixelIndex + 2] = b; + pixels.data[pixelIndex + 3] = 255; + } + } + + context.putImageData(pixels, 0, 0); +} diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..d1ef939 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,1906 @@ +{ + "name": "pbm-js", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "pbm-js", + "version": "1.0.0", + "license": "MIT", + "devDependencies": { + "@vitest/coverage-c8": "^0.31.0", + "vitest": "^0.31.0" + } + }, + "node_modules/@ampproject/remapping": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.2.1.tgz", + "integrity": "sha512-lFMjJTrFL3j7L9yBxwYfCq2k6qqwHyzuUl/XBnif78PWTJYyL/dfowQHWE3sp6U6ZzqWiiIZnpTMO96zhkjwtg==", + "dev": true, + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.0", + "@jridgewell/trace-mapping": "^0.3.9" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@bcoe/v8-coverage": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz", + "integrity": "sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==", + "dev": true + }, + "node_modules/@esbuild/android-arm": { + "version": "0.17.18", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.17.18.tgz", + "integrity": "sha512-EmwL+vUBZJ7mhFCs5lA4ZimpUH3WMAoqvOIYhVQwdIgSpHC8ImHdsRyhHAVxpDYUSm0lWvd63z0XH1IlImS2Qw==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.17.18", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.17.18.tgz", + "integrity": "sha512-/iq0aK0eeHgSC3z55ucMAHO05OIqmQehiGay8eP5l/5l+iEr4EIbh4/MI8xD9qRFjqzgkc0JkX0LculNC9mXBw==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.17.18", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.17.18.tgz", + "integrity": "sha512-x+0efYNBF3NPW2Xc5bFOSFW7tTXdAcpfEg2nXmxegm4mJuVeS+i109m/7HMiOQ6M12aVGGFlqJX3RhNdYM2lWg==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.17.18", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.17.18.tgz", + "integrity": "sha512-6tY+djEAdF48M1ONWnQb1C+6LiXrKjmqjzPNPWXhu/GzOHTHX2nh8Mo2ZAmBFg0kIodHhciEgUBtcYCAIjGbjQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.17.18", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.17.18.tgz", + "integrity": "sha512-Qq84ykvLvya3dO49wVC9FFCNUfSrQJLbxhoQk/TE1r6MjHo3sFF2tlJCwMjhkBVq3/ahUisj7+EpRSz0/+8+9A==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.17.18", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.17.18.tgz", + "integrity": "sha512-fw/ZfxfAzuHfaQeMDhbzxp9mc+mHn1Y94VDHFHjGvt2Uxl10mT4CDavHm+/L9KG441t1QdABqkVYwakMUeyLRA==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.17.18", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.17.18.tgz", + "integrity": "sha512-FQFbRtTaEi8ZBi/A6kxOC0V0E9B/97vPdYjY9NdawyLd4Qk5VD5g2pbWN2VR1c0xhzcJm74HWpObPszWC+qTew==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.17.18", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.17.18.tgz", + "integrity": "sha512-jW+UCM40LzHcouIaqv3e/oRs0JM76JfhHjCavPxMUti7VAPh8CaGSlS7cmyrdpzSk7A+8f0hiedHqr/LMnfijg==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.17.18", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.17.18.tgz", + "integrity": "sha512-R7pZvQZFOY2sxUG8P6A21eq6q+eBv7JPQYIybHVf1XkQYC+lT7nDBdC7wWKTrbvMXKRaGudp/dzZCwL/863mZQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.17.18", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.17.18.tgz", + "integrity": "sha512-ygIMc3I7wxgXIxk6j3V00VlABIjq260i967Cp9BNAk5pOOpIXmd1RFQJQX9Io7KRsthDrQYrtcx7QCof4o3ZoQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.17.18", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.17.18.tgz", + "integrity": "sha512-bvPG+MyFs5ZlwYclCG1D744oHk1Pv7j8psF5TfYx7otCVmcJsEXgFEhQkbhNW8otDHL1a2KDINW20cfCgnzgMQ==", + "cpu": [ + "loong64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.17.18", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.17.18.tgz", + "integrity": "sha512-oVqckATOAGuiUOa6wr8TXaVPSa+6IwVJrGidmNZS1cZVx0HqkTMkqFGD2HIx9H1RvOwFeWYdaYbdY6B89KUMxA==", + "cpu": [ + "mips64el" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.17.18", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.17.18.tgz", + "integrity": "sha512-3dLlQO+b/LnQNxgH4l9rqa2/IwRJVN9u/bK63FhOPB4xqiRqlQAU0qDU3JJuf0BmaH0yytTBdoSBHrb2jqc5qQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.17.18", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.17.18.tgz", + "integrity": "sha512-/x7leOyDPjZV3TcsdfrSI107zItVnsX1q2nho7hbbQoKnmoeUWjs+08rKKt4AUXju7+3aRZSsKrJtaRmsdL1xA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.17.18", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.17.18.tgz", + "integrity": "sha512-cX0I8Q9xQkL/6F5zWdYmVf5JSQt+ZfZD2bJudZrWD+4mnUvoZ3TDDXtDX2mUaq6upMFv9FlfIh4Gfun0tbGzuw==", + "cpu": [ + "s390x" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.17.18", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.17.18.tgz", + "integrity": "sha512-66RmRsPlYy4jFl0vG80GcNRdirx4nVWAzJmXkevgphP1qf4dsLQCpSKGM3DUQCojwU1hnepI63gNZdrr02wHUA==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.17.18", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.17.18.tgz", + "integrity": "sha512-95IRY7mI2yrkLlTLb1gpDxdC5WLC5mZDi+kA9dmM5XAGxCME0F8i4bYH4jZreaJ6lIZ0B8hTrweqG1fUyW7jbg==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.17.18", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.17.18.tgz", + "integrity": "sha512-WevVOgcng+8hSZ4Q3BKL3n1xTv5H6Nb53cBrtzzEjDbbnOmucEVcZeGCsCOi9bAOcDYEeBZbD2SJNBxlfP3qiA==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.17.18", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.17.18.tgz", + "integrity": "sha512-Rzf4QfQagnwhQXVBS3BYUlxmEbcV7MY+BH5vfDZekU5eYpcffHSyjU8T0xucKVuOcdCsMo+Ur5wmgQJH2GfNrg==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.17.18", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.17.18.tgz", + "integrity": "sha512-Kb3Ko/KKaWhjeAm2YoT/cNZaHaD1Yk/pa3FTsmqo9uFh1D1Rfco7BBLIPdDOozrObj2sahslFuAQGvWbgWldAg==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.17.18", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.17.18.tgz", + "integrity": "sha512-0/xUMIdkVHwkvxfbd5+lfG7mHOf2FRrxNbPiKWg9C4fFrB8H0guClmaM3BFiRUYrznVoyxTIyC/Ou2B7QQSwmw==", + "cpu": [ + "ia32" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.17.18", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.17.18.tgz", + "integrity": "sha512-qU25Ma1I3NqTSHJUOKi9sAH1/Mzuvlke0ioMJRthLXKm7JiSKVwFghlGbDLOO2sARECGhja4xYfRAZNPAkooYg==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@istanbuljs/schema": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/@istanbuljs/schema/-/schema-0.1.3.tgz", + "integrity": "sha512-ZXRY4jNvVgSVQ8DL3LTcakaAtXwTVUxE81hslsyD2AtoXW/wVob10HkOJ1X/pAlcI7D+2YoZKg5do8G/w6RYgA==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.3", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.3.tgz", + "integrity": "sha512-HLhSWOLRi875zjjMG/r+Nv0oCW8umGb0BgEhyX3dDX3egwZtB8PqLnjz3yedt8R5StBrzcg4aBpnh8UA9D1BoQ==", + "dev": true, + "dependencies": { + "@jridgewell/set-array": "^1.0.1", + "@jridgewell/sourcemap-codec": "^1.4.10", + "@jridgewell/trace-mapping": "^0.3.9" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.0.tgz", + "integrity": "sha512-F2msla3tad+Mfht5cJq7LSXcdudKTWCVYUgw6pLFOOHSTtZlj6SWNYAp+AhuqLmWdBO2X5hPrLcu8cVP8fy28w==", + "dev": true, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/set-array": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.1.2.tgz", + "integrity": "sha512-xnkseuNADM0gt2bs+BvhO0p78Mk762YnZdsuzFV018NoG1Sj1SCQvpSqa7XUaTam5vAGasABV9qXASMKnFMwMw==", + "dev": true, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.4.15", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.15.tgz", + "integrity": "sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg==", + "dev": true + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.18", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.18.tgz", + "integrity": "sha512-w+niJYzMHdd7USdiH2U6869nqhD2nbfZXND5Yp93qIbEmnDNk7PD48o+YchRVpzMU7M6jVCbenTR7PA1FLQ9pA==", + "dev": true, + "dependencies": { + "@jridgewell/resolve-uri": "3.1.0", + "@jridgewell/sourcemap-codec": "1.4.14" + } + }, + "node_modules/@jridgewell/trace-mapping/node_modules/@jridgewell/sourcemap-codec": { + "version": "1.4.14", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.14.tgz", + "integrity": "sha512-XPSJHWmi394fuUuzDnGz1wiKqWfo1yXecHQMRf2l6hztTO+nPru658AyDngaBe7isIxEkRsPR3FZh+s7iVa4Uw==", + "dev": true + }, + "node_modules/@types/chai": { + "version": "4.3.5", + "resolved": "https://registry.npmjs.org/@types/chai/-/chai-4.3.5.tgz", + "integrity": "sha512-mEo1sAde+UCE6b2hxn332f1g1E8WfYRu6p5SvTKr2ZKC1f7gFJXk4h5PyGP9Dt6gCaG8y8XhwnXWC6Iy2cmBng==", + "dev": true + }, + "node_modules/@types/chai-subset": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/@types/chai-subset/-/chai-subset-1.3.3.tgz", + "integrity": "sha512-frBecisrNGz+F4T6bcc+NLeolfiojh5FxW2klu669+8BARtyQv2C/GkNW6FUodVe4BroGMP/wER/YDGc7rEllw==", + "dev": true, + "dependencies": { + "@types/chai": "*" + } + }, + "node_modules/@types/istanbul-lib-coverage": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.4.tgz", + "integrity": "sha512-z/QT1XN4K4KYuslS23k62yDIDLwLFkzxOuMplDtObz0+y7VqJCaO2o+SPwHCvLFZh7xazvvoor2tA/hPz9ee7g==", + "dev": true + }, + "node_modules/@types/node": { + "version": "20.1.2", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.1.2.tgz", + "integrity": "sha512-CTO/wa8x+rZU626cL2BlbCDzydgnFNgc19h4YvizpTO88MFQxab8wqisxaofQJ/9bLGugRdWIuX/TbIs6VVF6g==", + "dev": true + }, + "node_modules/@vitest/coverage-c8": { + "version": "0.31.0", + "resolved": "https://registry.npmjs.org/@vitest/coverage-c8/-/coverage-c8-0.31.0.tgz", + "integrity": "sha512-h72qN1D962AO7UefQVulm9JFP5ACS7OfhCdBHioXU8f7ohH/+NTZCgAqmgcfRNHHO/8wLFxx+93YVxhodkEJVA==", + "dev": true, + "dependencies": { + "@ampproject/remapping": "^2.2.0", + "c8": "^7.13.0", + "magic-string": "^0.30.0", + "picocolors": "^1.0.0", + "std-env": "^3.3.2" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "vitest": ">=0.30.0 <1" + } + }, + "node_modules/@vitest/expect": { + "version": "0.31.0", + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-0.31.0.tgz", + "integrity": "sha512-Jlm8ZTyp6vMY9iz9Ny9a0BHnCG4fqBa8neCF6Pk/c/6vkUk49Ls6UBlgGAU82QnzzoaUs9E/mUhq/eq9uMOv/g==", + "dev": true, + "dependencies": { + "@vitest/spy": "0.31.0", + "@vitest/utils": "0.31.0", + "chai": "^4.3.7" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/runner": { + "version": "0.31.0", + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-0.31.0.tgz", + "integrity": "sha512-H1OE+Ly7JFeBwnpHTrKyCNm/oZgr+16N4qIlzzqSG/YRQDATBYmJb/KUn3GrZaiQQyL7GwpNHVZxSQd6juLCgw==", + "dev": true, + "dependencies": { + "@vitest/utils": "0.31.0", + "concordance": "^5.0.4", + "p-limit": "^4.0.0", + "pathe": "^1.1.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/snapshot": { + "version": "0.31.0", + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-0.31.0.tgz", + "integrity": "sha512-5dTXhbHnyUMTMOujZPB0wjFjQ6q5x9c8TvAsSPUNKjp1tVU7i9pbqcKPqntyu2oXtmVxKbuHCqrOd+Ft60r4tg==", + "dev": true, + "dependencies": { + "magic-string": "^0.30.0", + "pathe": "^1.1.0", + "pretty-format": "^27.5.1" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/spy": { + "version": "0.31.0", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-0.31.0.tgz", + "integrity": "sha512-IzCEQ85RN26GqjQNkYahgVLLkULOxOm5H/t364LG0JYb3Apg0PsYCHLBYGA006+SVRMWhQvHlBBCyuByAMFmkg==", + "dev": true, + "dependencies": { + "tinyspy": "^2.1.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/utils": { + "version": "0.31.0", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-0.31.0.tgz", + "integrity": "sha512-kahaRyLX7GS1urekRXN2752X4gIgOGVX4Wo8eDUGUkTWlGpXzf5ZS6N9RUUS+Re3XEE8nVGqNyxkSxF5HXlGhQ==", + "dev": true, + "dependencies": { + "concordance": "^5.0.4", + "loupe": "^2.3.6", + "pretty-format": "^27.5.1" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/acorn": { + "version": "8.8.2", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.8.2.tgz", + "integrity": "sha512-xjIYgE8HBrkpd/sJqOGNspf8uHG+NOHGOw6a/Urj8taM2EXfdNAH2oFcPeIFfsv3+kz/mJrS5VuMqbNLjCa2vw==", + "dev": true, + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-walk": { + "version": "8.2.0", + "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.2.0.tgz", + "integrity": "sha512-k+iyHEuPgSw6SbuDpGQM+06HQUa04DZ3o+F6CSzXMvvI5KMvnaEqXe+YVe555R9nn6GPt404fos4wcgpw12SDA==", + "dev": true, + "engines": { + "node": ">=0.4.0" + } + }, + "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, + "engines": { + "node": ">=8" + } + }, + "node_modules/ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/assertion-error": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-1.1.0.tgz", + "integrity": "sha512-jgsaNduz+ndvGyFt3uSuWqvy4lCnIJiovtouQN5JZHOKCS2QuhEdbcQHFhVksz2N2U9hXJo8odG7ETyWlEeuDw==", + "dev": true, + "engines": { + "node": "*" + } + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true + }, + "node_modules/blueimp-md5": { + "version": "2.19.0", + "resolved": "https://registry.npmjs.org/blueimp-md5/-/blueimp-md5-2.19.0.tgz", + "integrity": "sha512-DRQrD6gJyy8FbiE4s+bDoXS9hiW3Vbx5uCdwvcCf3zLHL+Iv7LtGHLpr+GZV8rHG8tK766FGYBwRbu8pELTt+w==", + "dev": true + }, + "node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/c8": { + "version": "7.13.0", + "resolved": "https://registry.npmjs.org/c8/-/c8-7.13.0.tgz", + "integrity": "sha512-/NL4hQTv1gBL6J6ei80zu3IiTrmePDKXKXOTLpHvcIWZTVYQlDhVWjjWvkhICylE8EwwnMVzDZugCvdx0/DIIA==", + "dev": true, + "dependencies": { + "@bcoe/v8-coverage": "^0.2.3", + "@istanbuljs/schema": "^0.1.3", + "find-up": "^5.0.0", + "foreground-child": "^2.0.0", + "istanbul-lib-coverage": "^3.2.0", + "istanbul-lib-report": "^3.0.0", + "istanbul-reports": "^3.1.4", + "rimraf": "^3.0.2", + "test-exclude": "^6.0.0", + "v8-to-istanbul": "^9.0.0", + "yargs": "^16.2.0", + "yargs-parser": "^20.2.9" + }, + "bin": { + "c8": "bin/c8.js" + }, + "engines": { + "node": ">=10.12.0" + } + }, + "node_modules/cac": { + "version": "6.7.14", + "resolved": "https://registry.npmjs.org/cac/-/cac-6.7.14.tgz", + "integrity": "sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/chai": { + "version": "4.3.7", + "resolved": "https://registry.npmjs.org/chai/-/chai-4.3.7.tgz", + "integrity": "sha512-HLnAzZ2iupm25PlN0xFreAlBA5zaBSv3og0DdeGA4Ar6h6rJ3A0rolRUKJhSF2V10GZKDgWF/VmAEsNWjCRB+A==", + "dev": true, + "dependencies": { + "assertion-error": "^1.1.0", + "check-error": "^1.0.2", + "deep-eql": "^4.1.2", + "get-func-name": "^2.0.0", + "loupe": "^2.3.1", + "pathval": "^1.1.1", + "type-detect": "^4.0.5" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/check-error": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/check-error/-/check-error-1.0.2.tgz", + "integrity": "sha512-BrgHpW9NURQgzoNyjfq0Wu6VFO6D7IZEmJNdtgNqpzGG8RuNFHt2jQxWlAs4HMe119chBnv+34syEZtc6IhLtA==", + "dev": true, + "engines": { + "node": "*" + } + }, + "node_modules/cliui": { + "version": "7.0.4", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-7.0.4.tgz", + "integrity": "sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ==", + "dev": true, + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.0", + "wrap-ansi": "^7.0.0" + } + }, + "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, + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "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 + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "dev": true + }, + "node_modules/concordance": { + "version": "5.0.4", + "resolved": "https://registry.npmjs.org/concordance/-/concordance-5.0.4.tgz", + "integrity": "sha512-OAcsnTEYu1ARJqWVGwf4zh4JDfHZEaSNlNccFmt8YjB2l/n19/PF2viLINHc57vO4FKIAFl2FWASIGZZWZ2Kxw==", + "dev": true, + "dependencies": { + "date-time": "^3.1.0", + "esutils": "^2.0.3", + "fast-diff": "^1.2.0", + "js-string-escape": "^1.0.1", + "lodash": "^4.17.15", + "md5-hex": "^3.0.1", + "semver": "^7.3.2", + "well-known-symbols": "^2.0.0" + }, + "engines": { + "node": ">=10.18.0 <11 || >=12.14.0 <13 || >=14" + } + }, + "node_modules/convert-source-map": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.9.0.tgz", + "integrity": "sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A==", + "dev": true + }, + "node_modules/cross-spawn": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", + "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", + "dev": true, + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/date-time": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/date-time/-/date-time-3.1.0.tgz", + "integrity": "sha512-uqCUKXE5q1PNBXjPqvwhwJf9SwMoAHBgWJ6DcrnS5o+W2JOiIILl0JEdVD8SGujrNS02GGxgwAg2PN2zONgtjg==", + "dev": true, + "dependencies": { + "time-zone": "^1.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "dev": true, + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/deep-eql": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-4.1.3.tgz", + "integrity": "sha512-WaEtAOpRA1MQ0eohqZjpGD8zdI0Ovsm8mmFhaDN8dvDZzyoUMcYDnf5Y6iu7HTXxf8JDS23qWa4a+hKCDyOPzw==", + "dev": true, + "dependencies": { + "type-detect": "^4.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "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 + }, + "node_modules/esbuild": { + "version": "0.17.18", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.17.18.tgz", + "integrity": "sha512-z1lix43jBs6UKjcZVKOw2xx69ffE2aG0PygLL5qJ9OS/gy0Ewd1gW/PUQIOIQGXBHWNywSc0floSKoMFF8aK2w==", + "dev": true, + "hasInstallScript": true, + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=12" + }, + "optionalDependencies": { + "@esbuild/android-arm": "0.17.18", + "@esbuild/android-arm64": "0.17.18", + "@esbuild/android-x64": "0.17.18", + "@esbuild/darwin-arm64": "0.17.18", + "@esbuild/darwin-x64": "0.17.18", + "@esbuild/freebsd-arm64": "0.17.18", + "@esbuild/freebsd-x64": "0.17.18", + "@esbuild/linux-arm": "0.17.18", + "@esbuild/linux-arm64": "0.17.18", + "@esbuild/linux-ia32": "0.17.18", + "@esbuild/linux-loong64": "0.17.18", + "@esbuild/linux-mips64el": "0.17.18", + "@esbuild/linux-ppc64": "0.17.18", + "@esbuild/linux-riscv64": "0.17.18", + "@esbuild/linux-s390x": "0.17.18", + "@esbuild/linux-x64": "0.17.18", + "@esbuild/netbsd-x64": "0.17.18", + "@esbuild/openbsd-x64": "0.17.18", + "@esbuild/sunos-x64": "0.17.18", + "@esbuild/win32-arm64": "0.17.18", + "@esbuild/win32-ia32": "0.17.18", + "@esbuild/win32-x64": "0.17.18" + } + }, + "node_modules/escalade": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.1.tgz", + "integrity": "sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/fast-diff": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/fast-diff/-/fast-diff-1.2.0.tgz", + "integrity": "sha512-xJuoT5+L99XlZ8twedaRf6Ax2TgQVxvgZOYoPKqZufmJib0tL2tegPBOZb1pVNgIhlqDlA0eO0c3wBvQcmzx4w==", + "dev": true + }, + "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, + "dependencies": { + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/foreground-child": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-2.0.0.tgz", + "integrity": "sha512-dCIq9FpEcyQyXKCkyzmlPTFNgrCzPudOe+mhvJU5zAtlBnGVy2yKxtfsxK2tQBThwq225jcvBjpw1Gr40uzZCA==", + "dev": true, + "dependencies": { + "cross-spawn": "^7.0.0", + "signal-exit": "^3.0.2" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", + "dev": true + }, + "node_modules/fsevents": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", + "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", + "dev": true, + "hasInstallScript": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "dev": true, + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, + "node_modules/get-func-name": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/get-func-name/-/get-func-name-2.0.0.tgz", + "integrity": "sha512-Hm0ixYtaSZ/V7C8FJrtZIuBBI+iSgL+1Aq82zSu8VQNB4S3Gk8e7Qs3VwBDJAhmRZcFqkl3tQu36g/Foh5I5ig==", + "dev": true, + "engines": { + "node": "*" + } + }, + "node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "dev": true, + "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/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, + "engines": { + "node": ">=8" + } + }, + "node_modules/html-escaper": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz", + "integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==", + "dev": true + }, + "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==", + "dev": true, + "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 + }, + "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, + "engines": { + "node": ">=8" + } + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true + }, + "node_modules/istanbul-lib-coverage": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.0.tgz", + "integrity": "sha512-eOeJ5BHCmHYvQK7xt9GkdHuzuCGS1Y6g9Gvnx3Ym33fz/HpLRYxiS0wHNr+m/MBC8B647Xt608vCDEvhl9c6Mw==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/istanbul-lib-report": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/istanbul-lib-report/-/istanbul-lib-report-3.0.0.tgz", + "integrity": "sha512-wcdi+uAKzfiGT2abPpKZ0hSU1rGQjUQnLvtY5MpQ7QCTahD3VODhcu4wcfY1YtkGaDD5yuydOLINXsfbus9ROw==", + "dev": true, + "dependencies": { + "istanbul-lib-coverage": "^3.0.0", + "make-dir": "^3.0.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/istanbul-reports": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.1.5.tgz", + "integrity": "sha512-nUsEMa9pBt/NOHqbcbeJEgqIlY/K7rVWUX6Lql2orY5e9roQOthbR3vtY4zzf2orPELg80fnxxk9zUyPlgwD1w==", + "dev": true, + "dependencies": { + "html-escaper": "^2.0.0", + "istanbul-lib-report": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/js-string-escape": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/js-string-escape/-/js-string-escape-1.0.1.tgz", + "integrity": "sha512-Smw4xcfIQ5LVjAOuJCvN/zIodzA/BBSsluuoSykP+lUvScIi4U6RJLfwHet5cxFnCswUjISV8oAXaqaJDY3chg==", + "dev": true, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/jsonc-parser": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/jsonc-parser/-/jsonc-parser-3.2.0.tgz", + "integrity": "sha512-gfFQZrcTc8CnKXp6Y4/CBT3fTc0OVuDofpre4aEeEpSBPV5X5v4+Vmx+8snU7RLPrNHPKSgLxGo9YuQzz20o+w==", + "dev": true + }, + "node_modules/local-pkg": { + "version": "0.4.3", + "resolved": "https://registry.npmjs.org/local-pkg/-/local-pkg-0.4.3.tgz", + "integrity": "sha512-SFppqq5p42fe2qcZQqqEOiVRXl+WCP1MdT6k7BDEW1j++sp5fIY+/fdRQitvKgB5BrBcmrs5m/L0v2FrU5MY1g==", + "dev": true, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "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==", + "dev": true, + "dependencies": { + "p-locate": "^5.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/lodash": { + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", + "dev": true + }, + "node_modules/loupe": { + "version": "2.3.6", + "resolved": "https://registry.npmjs.org/loupe/-/loupe-2.3.6.tgz", + "integrity": "sha512-RaPMZKiMy8/JruncMU5Bt6na1eftNoo++R4Y+N2FrxkDVTrGvcyzFTsaGif4QTeKESheMGegbhw6iUAq+5A8zA==", + "dev": true, + "dependencies": { + "get-func-name": "^2.0.0" + } + }, + "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, + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/magic-string": { + "version": "0.30.0", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.0.tgz", + "integrity": "sha512-LA+31JYDJLs82r2ScLrlz1GjSgu66ZV518eyWT+S8VhyQn/JL0u9MeBOvQMGYiPk1DBiSN9DDMOcXvigJZaViQ==", + "dev": true, + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.4.13" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/make-dir": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.1.0.tgz", + "integrity": "sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==", + "dev": true, + "dependencies": { + "semver": "^6.0.0" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/make-dir/node_modules/semver": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", + "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", + "dev": true, + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/md5-hex": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/md5-hex/-/md5-hex-3.0.1.tgz", + "integrity": "sha512-BUiRtTtV39LIJwinWBjqVsU9xhdnz7/i889V859IBFpuqGAj6LuOvHv5XLbgZ2R7ptJoJaEcxkv88/h25T7Ciw==", + "dev": true, + "dependencies": { + "blueimp-md5": "^2.10.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/mlly": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/mlly/-/mlly-1.2.0.tgz", + "integrity": "sha512-+c7A3CV0KGdKcylsI6khWyts/CYrGTrRVo4R/I7u/cUsy0Conxa6LUhiEzVKIw14lc2L5aiO4+SeVe4TeGRKww==", + "dev": true, + "dependencies": { + "acorn": "^8.8.2", + "pathe": "^1.1.0", + "pkg-types": "^1.0.2", + "ufo": "^1.1.1" + } + }, + "node_modules/ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "dev": true + }, + "node_modules/nanoid": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.6.tgz", + "integrity": "sha512-BGcqMMJuToF7i1rt+2PWSNVnWIkGCU78jBG3RxO/bZlnZPK2Cmi2QaffxGO/2RvWi9sL+FAiRiXMgsyxQ1DIDA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/once": { + "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, + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/p-limit": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-4.0.0.tgz", + "integrity": "sha512-5b0R4txpzjPWVw/cXXUResoD4hb6U/x9BH08L7nw+GN1sezDzPdxeRvpc9c433fZhBan/wusjbCsqwqm4EIBIQ==", + "dev": true, + "dependencies": { + "yocto-queue": "^1.0.0" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "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==", + "dev": true, + "dependencies": { + "p-limit": "^3.0.2" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate/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==", + "dev": true, + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate/node_modules/yocto-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "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, + "engines": { + "node": ">=8" + } + }, + "node_modules/path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "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, + "engines": { + "node": ">=8" + } + }, + "node_modules/pathe": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-1.1.0.tgz", + "integrity": "sha512-ODbEPR0KKHqECXW1GoxdDb+AZvULmXjVPy4rt+pGo2+TnjJTIPJQSVS6N63n8T2Ip+syHhbn52OewKicV0373w==", + "dev": true + }, + "node_modules/pathval": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/pathval/-/pathval-1.1.1.tgz", + "integrity": "sha512-Dp6zGqpTdETdR63lehJYPeIOqpiNBNtc7BpWSLrOje7UaIsE5aY92r/AunQA7rsXvet3lrJ3JnZX29UPTKXyKQ==", + "dev": true, + "engines": { + "node": "*" + } + }, + "node_modules/picocolors": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz", + "integrity": "sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==", + "dev": true + }, + "node_modules/pkg-types": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/pkg-types/-/pkg-types-1.0.3.tgz", + "integrity": "sha512-nN7pYi0AQqJnoLPC9eHFQ8AcyaixBUOwvqc5TDnIKCMEE6I0y8P7OKA7fPexsXGCGxQDl/cmrLAp26LhcwxZ4A==", + "dev": true, + "dependencies": { + "jsonc-parser": "^3.2.0", + "mlly": "^1.2.0", + "pathe": "^1.1.0" + } + }, + "node_modules/postcss": { + "version": "8.4.23", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.23.tgz", + "integrity": "sha512-bQ3qMcpF6A/YjR55xtoTr0jGOlnPOKAIMdOWiv0EIT6HVPEaJiJB4NLljSbiHoC2RX7DN5Uvjtpbg1NPdwv1oA==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "dependencies": { + "nanoid": "^3.3.6", + "picocolors": "^1.0.0", + "source-map-js": "^1.0.2" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/pretty-format": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-27.5.1.tgz", + "integrity": "sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==", + "dev": true, + "dependencies": { + "ansi-regex": "^5.0.1", + "ansi-styles": "^5.0.0", + "react-is": "^17.0.1" + }, + "engines": { + "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" + } + }, + "node_modules/react-is": { + "version": "17.0.2", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz", + "integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==", + "dev": true + }, + "node_modules/require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/rimraf": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", + "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", + "dev": true, + "dependencies": { + "glob": "^7.1.3" + }, + "bin": { + "rimraf": "bin.js" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/rollup": { + "version": "3.21.6", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-3.21.6.tgz", + "integrity": "sha512-SXIICxvxQxR3D4dp/3LDHZIJPC8a4anKMHd4E3Jiz2/JnY+2bEjqrOokAauc5ShGVNFHlEFjBXAXlaxkJqIqSg==", + "dev": true, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=14.18.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + } + }, + "node_modules/semver": { + "version": "7.5.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.0.tgz", + "integrity": "sha512-+XC0AD/R7Q2mPSRuy2Id0+CGTZ98+8f+KvwirxOKIEyid+XSx6HbC63p+O4IndTHuX5Z+JxQ0TghCkO5Cg/2HA==", + "dev": true, + "dependencies": { + "lru-cache": "^6.0.0" + }, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "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, + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "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, + "engines": { + "node": ">=8" + } + }, + "node_modules/siginfo": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/siginfo/-/siginfo-2.0.0.tgz", + "integrity": "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==", + "dev": true + }, + "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 + }, + "node_modules/source-map-js": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.0.2.tgz", + "integrity": "sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/stackback": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz", + "integrity": "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==", + "dev": true + }, + "node_modules/std-env": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/std-env/-/std-env-3.3.3.tgz", + "integrity": "sha512-Rz6yejtVyWnVjC1RFvNmYL10kgjC49EOghxWn0RFqlCHGFpQx+Xe7yW3I4ceK1SGrWIGMjD5Kbue8W/udkbMJg==", + "dev": true + }, + "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, + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "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, + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-literal": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/strip-literal/-/strip-literal-1.0.1.tgz", + "integrity": "sha512-QZTsipNpa2Ppr6v1AmJHESqJ3Uz247MUS0OjrnnZjFAvEoWqxuyFuXn2xLgMtRnijJShAa1HL0gtJyUs7u7n3Q==", + "dev": true, + "dependencies": { + "acorn": "^8.8.2" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "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, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/test-exclude": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-6.0.0.tgz", + "integrity": "sha512-cAGWPIyOHU6zlmg88jwm7VRyXnMN7iV68OGAbYDk/Mh/xC/pzVPlQtY6ngoIH/5/tciuhGfvESU8GrHrcxD56w==", + "dev": true, + "dependencies": { + "@istanbuljs/schema": "^0.1.2", + "glob": "^7.1.4", + "minimatch": "^3.0.4" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/time-zone": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/time-zone/-/time-zone-1.0.0.tgz", + "integrity": "sha512-TIsDdtKo6+XrPtiTm1ssmMngN1sAhyKnTO2kunQWqNPWIVvCm15Wmw4SWInwTVgJ5u/Tr04+8Ei9TNcw4x4ONA==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/tinybench": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.5.0.tgz", + "integrity": "sha512-kRwSG8Zx4tjF9ZiyH4bhaebu+EDz1BOx9hOigYHlUW4xxI/wKIUQUqo018UlU4ar6ATPBsaMrdbKZ+tmPdohFA==", + "dev": true + }, + "node_modules/tinypool": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/tinypool/-/tinypool-0.5.0.tgz", + "integrity": "sha512-paHQtnrlS1QZYKF/GnLoOM/DN9fqaGOFbCbxzAhwniySnzl9Ebk8w73/dd34DAhe/obUbPAOldTyYXQZxnPBPQ==", + "dev": true, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/tinyspy": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/tinyspy/-/tinyspy-2.1.0.tgz", + "integrity": "sha512-7eORpyqImoOvkQJCSkL0d0mB4NHHIFAy4b1u8PHdDa7SjGS2njzl6/lyGoZLm+eyYEtlUmFGE0rFj66SWxZgQQ==", + "dev": true, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/type-detect": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz", + "integrity": "sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/ufo": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/ufo/-/ufo-1.1.2.tgz", + "integrity": "sha512-TrY6DsjTQQgyS3E3dBaOXf0TpPD8u9FVrVYmKVegJuFw51n/YB9XPt+U6ydzFG5ZIN7+DIjPbNmXoBj9esYhgQ==", + "dev": true + }, + "node_modules/v8-to-istanbul": { + "version": "9.1.0", + "resolved": "https://registry.npmjs.org/v8-to-istanbul/-/v8-to-istanbul-9.1.0.tgz", + "integrity": "sha512-6z3GW9x8G1gd+JIIgQQQxXuiJtCXeAjp6RaPEPLv62mH3iPHPxV6W3robxtCzNErRo6ZwTmzWhsbNvjyEBKzKA==", + "dev": true, + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.12", + "@types/istanbul-lib-coverage": "^2.0.1", + "convert-source-map": "^1.6.0" + }, + "engines": { + "node": ">=10.12.0" + } + }, + "node_modules/vite": { + "version": "4.3.5", + "resolved": "https://registry.npmjs.org/vite/-/vite-4.3.5.tgz", + "integrity": "sha512-0gEnL9wiRFxgz40o/i/eTBwm+NEbpUeTWhzKrZDSdKm6nplj+z4lKz8ANDgildxHm47Vg8EUia0aicKbawUVVA==", + "dev": true, + "dependencies": { + "esbuild": "^0.17.5", + "postcss": "^8.4.23", + "rollup": "^3.21.0" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^14.18.0 || >=16.0.0" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + }, + "peerDependencies": { + "@types/node": ">= 14", + "less": "*", + "sass": "*", + "stylus": "*", + "sugarss": "*", + "terser": "^5.4.0" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "less": { + "optional": true + }, + "sass": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + } + } + }, + "node_modules/vite-node": { + "version": "0.31.0", + "resolved": "https://registry.npmjs.org/vite-node/-/vite-node-0.31.0.tgz", + "integrity": "sha512-8x1x1LNuPvE2vIvkSB7c1mApX5oqlgsxzHQesYF7l5n1gKrEmrClIiZuOFbFDQcjLsmcWSwwmrWrcGWm9Fxc/g==", + "dev": true, + "dependencies": { + "cac": "^6.7.14", + "debug": "^4.3.4", + "mlly": "^1.2.0", + "pathe": "^1.1.0", + "picocolors": "^1.0.0", + "vite": "^3.0.0 || ^4.0.0" + }, + "bin": { + "vite-node": "vite-node.mjs" + }, + "engines": { + "node": ">=v14.18.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/vitest": { + "version": "0.31.0", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-0.31.0.tgz", + "integrity": "sha512-JwWJS9p3GU9GxkG7eBSmr4Q4x4bvVBSswaCFf1PBNHiPx00obfhHRJfgHcnI0ffn+NMlIh9QGvG75FlaIBdKGA==", + "dev": true, + "dependencies": { + "@types/chai": "^4.3.4", + "@types/chai-subset": "^1.3.3", + "@types/node": "*", + "@vitest/expect": "0.31.0", + "@vitest/runner": "0.31.0", + "@vitest/snapshot": "0.31.0", + "@vitest/spy": "0.31.0", + "@vitest/utils": "0.31.0", + "acorn": "^8.8.2", + "acorn-walk": "^8.2.0", + "cac": "^6.7.14", + "chai": "^4.3.7", + "concordance": "^5.0.4", + "debug": "^4.3.4", + "local-pkg": "^0.4.3", + "magic-string": "^0.30.0", + "pathe": "^1.1.0", + "picocolors": "^1.0.0", + "std-env": "^3.3.2", + "strip-literal": "^1.0.1", + "tinybench": "^2.4.0", + "tinypool": "^0.5.0", + "vite": "^3.0.0 || ^4.0.0", + "vite-node": "0.31.0", + "why-is-node-running": "^2.2.2" + }, + "bin": { + "vitest": "vitest.mjs" + }, + "engines": { + "node": ">=v14.18.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "@edge-runtime/vm": "*", + "@vitest/browser": "*", + "@vitest/ui": "*", + "happy-dom": "*", + "jsdom": "*", + "playwright": "*", + "safaridriver": "*", + "webdriverio": "*" + }, + "peerDependenciesMeta": { + "@edge-runtime/vm": { + "optional": true + }, + "@vitest/browser": { + "optional": true + }, + "@vitest/ui": { + "optional": true + }, + "happy-dom": { + "optional": true + }, + "jsdom": { + "optional": true + }, + "playwright": { + "optional": true + }, + "safaridriver": { + "optional": true + }, + "webdriverio": { + "optional": true + } + } + }, + "node_modules/well-known-symbols": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/well-known-symbols/-/well-known-symbols-2.0.0.tgz", + "integrity": "sha512-ZMjC3ho+KXo0BfJb7JgtQ5IBuvnShdlACNkKkdsqBmYw3bPAaJfPeYUo6tLUaT5tG/Gkh7xkpBhKRQ9e7pyg9Q==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/why-is-node-running": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-2.2.2.tgz", + "integrity": "sha512-6tSwToZxTOcotxHeA+qGCq1mVzKR3CwcJGmVcY+QE8SHy6TnpFnh8PAvPNHYr7EcuVeG0QSMxtYCuO1ta/G/oA==", + "dev": true, + "dependencies": { + "siginfo": "^2.0.0", + "stackback": "0.0.2" + }, + "bin": { + "why-is-node-running": "cli.js" + }, + "engines": { + "node": ">=8" + } + }, + "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, + "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/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==", + "dev": true, + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "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 + }, + "node_modules/y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "dev": true, + "engines": { + "node": ">=10" + } + }, + "node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "dev": true + }, + "node_modules/yargs": { + "version": "16.2.0", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-16.2.0.tgz", + "integrity": "sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw==", + "dev": true, + "dependencies": { + "cliui": "^7.0.2", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.0", + "y18n": "^5.0.5", + "yargs-parser": "^20.2.2" + }, + "engines": { + "node": ">=10" + } + }, + "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, + "engines": { + "node": ">=10" + } + }, + "node_modules/yocto-queue": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-1.0.0.tgz", + "integrity": "sha512-9bnSc/HEW2uRy67wc+T8UwauLuPJVn28jb+GtJY16iiKWyvmYJRXVT4UamsAEGQfPohgr2q4Tq0sQbQlxTfi1g==", + "dev": true, + "engines": { + "node": ">=12.20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..6aedf6e --- /dev/null +++ b/package.json @@ -0,0 +1,18 @@ +{ + "type": "module", + "name": "pbm-js", + "version": "1.0.0", + "description": "", + "main": "main.js", + "scripts": { + "test": "vitest", + "coverage": "vitest run --coverage" + }, + "keywords": [], + "author": "Michael Smith", + "license": "MIT", + "devDependencies": { + "@vitest/coverage-c8": "^0.31.0", + "vitest": "^0.31.0" + } +} diff --git a/src/binarystream.js b/src/binarystream.js new file mode 100644 index 0000000..83d7eb4 --- /dev/null +++ b/src/binarystream.js @@ -0,0 +1,94 @@ +/* + + MIT License + + Copyright (c) 2023 Michael Smith + + 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. + + */ + +// NOTE(m): Partially copied from +// https://github.com/steffest/DPaint-js/blob/master/_script/util/binarystream.js + +class BinaryStream { + constructor(arrayBuffer) { + this.index = 0; + this.data = arrayBuffer; + this.dataView = new DataView(arrayBuffer); + this.length = arrayBuffer.byteLength; + } + + EOF() { + return this.index === this.length; + } + + jump(offset) { + this.index += offset; + } + + readByte() { + const byte = this.dataView.getUint8(this.index); + + this.index++; + return byte; + } + + readInt16BE() { + const value = this.dataView.getInt16(this.index); + + this.index += 2; + return value; + } + + readString(length) { + let string = ""; + + for (let i = 0; i < length; i++) { + const byte = this.dataView.getUint8(this.index + i); + string += String.fromCharCode(byte); + } + + this.index += length; + return string; + } + + readUint8() { + const value = this.dataView.getUint8(this.index); + + this.index += 1; + return value; + } + + readUint16BE() { + const value = this.dataView.getUint16(this.index); + + this.index += 2; + return value; + } + + readUint32BE() { + const value = this.dataView.getUint32(this.index); + + this.index += 4; + return value; + } +} + +export default BinaryStream; diff --git a/src/pbm.js b/src/pbm.js new file mode 100644 index 0000000..d3bc122 --- /dev/null +++ b/src/pbm.js @@ -0,0 +1,240 @@ +/* + + MIT License + + Copyright (c) 2023 Michael Smith + + 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. + + */ + +import BinaryStream from "./binarystream.js"; + +class PBM { + constructor(arrayBuffer) { + this.binaryStream = new BinaryStream(arrayBuffer); + + // Image properties taken from BMHD chunk + this.width = null; + this.height = null; + this.size = null; + this.xOrigin = null; + this.yOrigin = null; + this.numPlanes = null; + this.mask = null; + this.compression = null; + this.transClr = null; + this.xAspect = null; + this.yAspect = null; + this.pageWidth = null; + this.pageHeight = null; + + // Palette information taken from CMAP chunk + this.palette = []; + + // Color cycling information taken from CRNG chunk + this.cyclingRanges = []; + + // Thumbnail information taken from TINY chunk + this.thumbnail = { + width: null, + height: null, + size: null, + palette: this.palette, + pixelData: [], + }; + + // Uncompressed pixel data referencing palette colors + this.pixelData = []; + + try { + this.parseFORM(); + } catch (error) { + if (error instanceof RangeError) { + throw new Error(`Failed to parse file.`); + } else { + throw error; // re-throw the error unchanged + } + } + } + + parseFORM() { + // Parse "FORM" chunk + let chunkID = this.binaryStream.readString(4); + const lenChunk = this.binaryStream.readUint32BE(); + const formatID = this.binaryStream.readString(4); + + // Validate chunk according to notes on https://en.wikipedia.org/wiki/ILBM + if (chunkID !== "FORM") { + throw new Error( + `Invalid chunkID: "${chunkID}" at byte ${this.binaryStream.index}. Expected "FORM".` + ); + } + + if (lenChunk !== this.binaryStream.length - 8) { + throw new Error( + `Invalid chunk length: ${lenChunk} bytes. Expected ${ + this.binaryStream.length - 8 + } bytes.` + ); + } + + if (formatID !== "PBM ") { + throw new Error(`Invalid formatID: "${formatID}". Expected "PBM ".`); + } + + // Parse all other chunks + while (!this.binaryStream.EOF()) { + chunkID = this.binaryStream.readString(4); + this.binaryStream.jump(4); // Skip 4 bytes chunk length value + + switch (chunkID) { + case "BMHD": + this.parseBMHD(); + break; + case "CMAP": + this.parseCMAP(); + break; + case "DPPS": + // NOTE(m): Ignore unknown DPPS chunk of size 110 bytes + this.binaryStream.jump(110); + break; + case "CRNG": + this.parseCRNG(); + break; + case "TINY": + this.parseTINY(); + break; + case "BODY": + this.parseBODY(); + break; + default: + throw new Error( + `Unsupported chunkID: ${chunkID} at byte ${this.binaryStream.index}` + ); + } + } + } + + // Parse Bitmap Header chunk + parseBMHD() { + this.width = this.binaryStream.readUint16BE(); + this.height = this.binaryStream.readUint16BE(); + this.size = this.width * this.height; + this.xOrigin = this.binaryStream.readInt16BE(); + this.yOrigin = this.binaryStream.readInt16BE(); + this.numPlanes = this.binaryStream.readUint8(); + this.mask = this.binaryStream.readUint8(); + this.compression = this.binaryStream.readUint8(); + this.binaryStream.readUint8(); // Ignore pad1 field left "for future compatibility" + this.transClr = this.binaryStream.readUint16BE(); + this.xAspect = this.binaryStream.readUint8(); + this.yAspect = this.binaryStream.readUint8(); + this.pageWidth = this.binaryStream.readInt16BE(); + this.pageHeight = this.binaryStream.readInt16BE(); + } + + // Parse Palette chunk + parseCMAP() { + const numColors = 2 ** this.numPlanes; + + // FIXME(m): Read 3 bytes at a time? + for (let i = 0; i < numColors; i++) { + let rgb = []; + for (let j = 0; j < 3; j++) { + rgb.push(this.binaryStream.readByte()); + } + this.palette.push(rgb); + } + } + + // Parse Color range chunk + parseCRNG() { + this.binaryStream.jump(2); // 2 bytes padding according to https://en.wikipedia.org/wiki/ILBM#CRNG:_Colour_range + const cyclingRange = { + rate: this.binaryStream.readInt16BE(), + flags: this.binaryStream.readInt16BE(), + low: this.binaryStream.readUint8(), + hight: this.binaryStream.readUint8(), + }; + + this.cyclingRanges.push(cyclingRange); + } + + // Parse Thumbnail chunk + parseTINY() { + this.thumbnail.width = this.binaryStream.readUint16BE(); + this.thumbnail.height = this.binaryStream.readUint16BE(); + this.thumbnail.size = this.thumbnail.width * this.thumbnail.height; + + while (this.thumbnail.pixelData.length < this.thumbnail.size) { + const byte = this.binaryStream.readByte(); + + // TODO(m): Deduplicate decompression code for thumbnail and image data + if (this.compression === 1) { + // Decompress the data + if (byte > 128) { + const nextByte = this.binaryStream.readByte(); + for (let i = 0; i < 257 - byte; i++) { + this.thumbnail.pixelData.push(nextByte); + } + } else if (byte < 128) { + for (let i = 0; i < byte + 1; i++) { + this.thumbnail.pixelData.push(this.binaryStream.readByte()); + } + } else { + break; + } + } else { + // Data is not compressed, just copy the bytes + this.thumbnail.pixelData.push(byte); + } + } + } + + // Parse Image data chunk + parseBODY() { + // NOTE(m): Should we make use of the chunk length here instead? + + while (this.pixelData.length < this.size) { + const byte = this.binaryStream.readByte(); + + if (this.compression === 1) { + // Decompress the data + if (byte > 128) { + const nextByte = this.binaryStream.readByte(); + for (let i = 0; i < 257 - byte; i++) { + this.pixelData.push(nextByte); + } + } else if (byte < 128) { + for (let i = 0; i < byte + 1; i++) { + this.pixelData.push(this.binaryStream.readByte()); + } + } else { + break; + } + } else { + // Data is not compressed, just copy the bytes + this.pixelData.push(byte); + } + } + } +} + +export default PBM; diff --git a/style.css b/style.css new file mode 100644 index 0000000..0cafcff --- /dev/null +++ b/style.css @@ -0,0 +1,17 @@ +#image-canvas { + position: absolute; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + width: 640px; + height: 480px; +} + +#thumbnail-canvas { + position: absolute; + top: 50%; + left: 15%; + transform: translate(-50%, -50%); + width: 80px; + height: 60px; +} diff --git a/tests/fixtures/ASH.LBM b/tests/fixtures/ASH.LBM new file mode 100644 index 0000000000000000000000000000000000000000..d52f0ebb836b7a77203a102eaf5001fe04d51782 GIT binary patch literal 76502 zcmdSBi(6A?zBU|=AtHhWPhju>Q_g5rdznI(|ACJca#%@L zp67n<-}$#zG?(`w!^&S7AMxSJh!10;7z~D+)$bWocw{&*tQZW7ojqRnMSi$7p24so zyN{nf9^m1s9k{(~*ZRiO#|4D~i^cN9@`%q*j^qnU_`BL}UwWj~Ua~wI7`{EM9cH*X zMh+hzc%;338}FR9;FGc8Po4~i@7lF1GW^`>5?2q`SFfJzO5W8raC`WPHa0f)(xr3X zeRnA~cKvPb?GnrFCr=&)tY04+zP?Z(*cBPT7w`qU@G9U^+qv7f8v`OE*N2CnYdn7L z(z)9MZG|NQS64=OZ1|4>&sns? z18r?>=gu{r^9m0a?7}eGj+Y#c3<%IZ((-o!`6pV-lS_@qlVc-aSzg`#t`W;1NRAvH z9vIdRL`Fs)KX;nX=byF=uMdwbEIhm`86S*1&fhg)c@-HR{@r(Nh1&0)ym}Q2uwr*% z66yywJnLmBDcI@bKEj7re4qT*;rK zE7-TZFu;J#gZHuPO$d!aAYt%TphiVV=b?q;8}@4u-?OC zSo>X6eEcrRz#)b;5ru#JS^YbML8JtGo#}VTyZ`cue|+-)@dzRNm%snV>qs7FJ3iwu z_c@mp|N466nty$rn)R=*E6)AvYX;;0Z?6+}aFQ5|clI*k8Sh($td&FgW~J?rptQVx z<{0Dot78jWrO%HYYdv=CT&uLT_1N?0$DY4wwH&i{J$4L_&a89JzdQH*SnD}2i?dcU zu3fHMuG49Khw@hI^=6Z+q@~BCH<=_RJyXwQ>Tz!}nVAd|rKe0RW)*grRHkEkis@Wk z#ooc3oRmzmTNJJ?;uPn`x?Vw%Mmwt2X*J%Vk`~Iuz!5Eai)o%7$7JeTT9|r?nQ7MB zne;vQomstw?aGw2=+oIurYS6)hu?GH`J^nhL|aPddFMn7X>}Tnx>Yi3av(?0V=O&p zy_KnDR_{>7WMYu*dJ~0ROE6eAZY?;NzK16c)9YK(&+#Uc7w{AL(b1yPQbCqZGu*DH zdhGBmlZhUfwW+6PR_|w0Y#5U@Sk-s6wzl8nwM!)Wy$ac7PkHZe<<-?pwuC8GD8xLT zMBhavI+Z2zPo0X18PfSGa2N`3^vn&6(pyXPz{OuL+^{_=HFeLP;^L~z)>bFp4hQ`} zj9lek_r%H3=XLf#`%57_tHoG-<$aA$EO_p3F*UGA^**vK@ zA;G0J{g7?9tzhl5A76v$<<) zD+Rgors1M~kz?AFx0qR-o*`zkTNC1?3ppRX=>M>t>u9G_~&fI#Ya4=ZJ=SO=+Xk|iEXm+-3q=zu?T3J~%zbwg@ zy<3>j!c2FPwmW~}Je1_eFXd!ui#B<#P~nKPSOu)9X#?ikvm@lNeMoV&Q%8SNIO~)9 z(Sm%LqC};yJos5#L%&~)pEc(rQIW7OBR)M{>cn<&OMg$$_@euTpIk8X`TjtrYDIYq zFq42U7#~@hR!EGf|Za^CM$Bcmbv)w;ica)*}vr7%1AuW*r@5q z`zCPAYSCAM@`B>LMP)HDsi{fAL-FzH@zQjm)MqXC?8hGmB}Ug^Iih&_=O)#KD`bcu z4b5rXE&g?*ggIm8|B}~T*1(FmyeRvtuTEUQF0v}PCA;OnFDNKgBs~-_OxU*C?NCOi zmm5Fm>Tiq| z)~-2P&?#(Q)M@D~4{@mR`{?_ReoPG7H^X#N{rpz;?*IDByMOu1yZ`f-zr6E5^M1Hf-g?X2&@ni8V$tHo&@z*fFomAkUU@68 zyW&8uK~rAPnO@!{ENDKtdJVP2Ho78uiKkgVzsDrGu-7Frc`2M4*9G$_}_ijP_>d^G^)hAbTt^dz7hYEJT z_$cs^LNC!@*tP53n>X*hd2jZ=-u(A_^OjwDG;jQ;n?DWjUhHwCa${a|S9_JK z>&hxmUzz_mU*7IdntrS+%F2>QShbz>@c7E*d%qX^ni>yA6m1#(TCbP<{y4QO{P@59 zVbgo>-5mb!KWxf>bo0`sN6$uY-~2js^`ZS&zxrxFx42m--7C{6Z}>OdY)l$4>PFQ= zYUSJh)B$P5r?+tXz#+U9?PZx^VpXUylFf-~KQ){PU&1dkR+NfAiNzmnOcJ z-MG2@zxzV=f3@hVs68UC@KAwLr~9q=QDal_O`{qXW@uE|TX(aCy<>Hbds90BiH*d>Appa6U3gYG!Ndp zxNu_J@No3e^51%Um6}oI=--#AR9*)}qMDrG(NT4;QfX3^JU1y+^~=6)s#9y!%HAzo zd`Dlt`k5NFy`gzFy~Mm|#r4C7XA0yyl{&S0e0)?h{`2^#^0v~qcWSv>^YXuE|H|$6 z^Xu0>&^W_rH(!YBWC? zE@IUE%DQ#K8r_7Ux32&0#y|ddM1-;~%b?S6h;5!yk83sRp9aKM#)^vhc~5n|!HWo2GSY4W;u?HeTZVuWb8)b-uc1hEc;7 z9hNamfVENI{Eaf$KQ{E<_~x6N;l`&%qd{jhOpoifUbxS!9^>@DhdGznY!&7PlS*E`#mA5zjrc`QlhJLNipj$r8_1194$CXokG+zu_C@|U7bPs*L7+v^Qck@n!VJSH7j);)8peh9E_teXht=fEF5lHgWChc&AN5J z^{Q=*&x{YvhE>x>Oc+tfQ2+>Y@h*u*)5AkpYfqw$kI%kl!%${?@Cx-H*vWHq06s2Fhq{ zxIIb@Td9?l+5zutFnA@N+bJh>Q@T}@aFIb@#0h6!xCpOk9f zUJUJ-5kjHWEZ1;e>R#$5bmOx+=3{WG>2|O3T_E=CG2nap*!c9BarwAz`N$|}t22y( z7n(~NC19H%(rw>6tX8kn0TGLiHL4k1jsa;j6O?W~zN4Ek>Xhnr^4{Jz0L=J|GI~8V zjOZX68Vn7D9p&i8)jw@f4?z|TMqOPmh-1L9oDI`N5)32ca~yobtg|)hp!}#_>xYm9 zqvi1%qs8zJ2sAwklyrcN2pB!#)sI0js4Jp47=$9@FLA#WjC=XgV3?R5d1f@sDjB`j zcyq?+{?z#T$y0-2l+Y35QbJyon&JN5Et-j0gS+N|Qn|$!LuDAJ$H6rtkfM1pZJ>14 zI1;6_&Q~g}Uq5?1ZFG3ecxHGos?$LQIYI=+)k7NP7TwRim=SoWo&ZP4s33qwN;l6q zL4I;tXL$OU(m5Cw8q~hZVU2p$X!Q&j*gbvt@HLLF^T2qrpg{~ejvB02tLvteYU8w0 zw@RrVr3_Za*U*W&m(L6%2F=9u@(JUU@dvtR3!gz+H0oZnk@0lK_}(*GF=2w(rCbMs z7in-5rLTGvm>ZPXt<)Nxy`EEu69({{nqZCV9vgHI2#+bXHAKMnDT4uNH(DOrLUB$& zS^~y3qreMbW2Mw;c~h@?p-!!=l&SlbBM-puCq}~rWb@hM$IF*fIw!*e^3rI8E$Q#A zV;NsRWjryyhDM^EvR^+0G*Dp}i#nf-2unXPT<6;`qRFq6QEFQa7zJRKE!RPsA3UB0 z9kCj^iRDE9X5kl=?=g(8pFK6=_WJ3=XU46rSUAIIky82Fy51rzi$?jx_@Fm$RqyyU z85lw&n-JG_90#1B3@+0;qM{~<;6g1x^V>3>zJ3k9`932DH9ovIv2|nwT5AOQs8_4i zXw`<-216b6WkUYJ40!j!IJ6nqPo_FSbeCCY1EinL8kok% z9IU=kw~T=`!2j!qPmK>B8Va91e2S@zs7IAeRIfcOGUSFaYh0pZ0JN7AY6$BCz)r^d z0FsMiQU;6B-hhJ{vUFCArxe2)ii0vPer;U$>KUMe6@fr#`s;eNSQ9m6Sb(L%GtdXi z@W9BRaFBV=jE~>q4>fBCL4u}cGGJj|8yVmobE{&491KD+f6GnfD$P1b#WRiZ*^H5; z)&QkposM`2wiuW00Uj4lOh8VJ4j3tMdzNb)a3Fw%jWYTfGP|&>ATk6?ty!n6>s14ff~Axo)TS9cTs#<+xAZYq1BD4?dHDjqL4W^tbR_UepVfHy{k zUxQ934h;Y&Yic_&I;t^PAomnw)w8FsF<;};*JSOV0eYh03|XQZkZHrnOUMrNv|!V! zrU_ys@&6hGCN37ikSw=u{6PdaDv4p`+B-AktQ=Az#WXY&i96`fA+vG?oQ5G~zEVJT z=L-aP>o%1JdeoQat*@`QvMNWq>6inOPIKGLF=u3>$P1*R=I&o2P4)ViJ=uf&l)06O zG*#F0)?=>JF{|guQ8|(~@KM)u*h#fo9g|MCS|4kDZa(HZchF<5c=sPiJ2unGkj}JH z_>k4H=QAYFg#p-*qf@Q+$8hX)sq}d(tb%g2YU!c$vt)%QeuBvb-LtqiHhp6Q6VEjj%7!(dU_nvLxLc($Dw7V zB1OSlsZi|b2}@QaC;RyDl6gF{!Wy49tDIY16((Lt^HH}D=Xi^A$bjtw-2=m8%HRMs zn3YxZ=LcIU2rvVVgzY%=gKk#qj{QR*N3cB5!y~VJeO~#7(4HQL9+gSe^VDRGWB?gF zgZ_VQnpId-Gp2d{9fhd_WSyUFiy-AwsC5%5|wpjgcznylg?!oeD zh>|v|NVC=az0esBTgG6mlPYdRb(f8OYkng}Rn2||k>u1{AU3o1p_3vR>O!&S@aqoHSb5{WG zU?RWYqn99?O3;_E@eCl5ZBnwM#lakAmRQ1^*bZ#=j&0kL!;-@s!onzpQwt7K)!y2b zZV^{m63lxU0}Sn~#_{C{;zNygOZN3CtFi17=HJfSLNad^EF1Vrn5twOPGAS$iPzhH0f|PU|h? zfd~$RtX&VoG()IQK0tEWNFleFUX$D&-nGU7k%VO0=&Pz)-stVfhww3zl?uSwSG9H> zjOI^Ty=d0zzO@V4ZptB!}KJ4~b9gp_8A}`Wd1tARfrML9>vl;1y-E zgH`}JC_Rhhe1IsjXJ)2_ZDnH5=vizqiA|0GUOD5S00yaE9G2XWie|O|QwEMn5n)nc z0Isd=JpCjsp4Q+A#ay6gvzaq3RZ?j}RW+L_p_tU9g*k7MfX-}Tx3GIC3B`7UcB!ro z3sXR_T6onwUX_c*l0b=V_llPu;?63ae)64CZ{eVduvPbG?N&;+3RFSVgD8(M1~LV2 zE=z+XfuO@GGbFR1pcNTpi^&?Zd`Z*`G$Pp1^G46W{v`wmY;;KjGa*or*Z9ah+|RAF z9>QWn_EK}4A&jvh30evYgaMk6+$G`L0{W{+Tj8^KAGpjTb3?Lpj)FEMEe?#U5}=FO z`pFkeioKNCA{BN?6L^F%gdCDdCfOuxD$KS8SwTG|na{5FsScB{!5Vf;HB~h~!6kv0 zyfHW*ot`g$Eqwj2;8V z$ij94QOf;1)nw_N-_$EmDyNFFii*G!O1pYAYqSV*2D_x&I}eJ-LplX|dYS}xBQHtZRHTjwEbq!p#~eu2Y;L3L zXQb{x51v*#kO$N?@K2aCt9PQ2B+Q_{0X0fjJ@lrR5TCK!mgsr2Pw)Al*=5pFV5n;pn$kl&v#7*awEHQJXW#QcL-FCmfIA`PcV>FFWt+KZnjP*J8Ow2q)5p<%#0 z=GJ&T{$0Bl#7NMMapfo2p_kee!ciq7x!QEc7z!tD*wUmwp1qF2k zsoQGS%!|A+mMN_wm_VJl5vpS&GsTpF2N67)9e7P@p0;EMbP_o77A;bVriq367O9C6 zO=Kk@Dg}bF5S1!=M&uE{AtBnuUeCst6`+b$k7=6JXXzhpnT0AR1flOJJ+w6wvjpoT zGnB-Bl6;*ApHmAdBZHK4A0)p9SzQp;+6*v~;qN#QSIEqC1h!(Su&>LKAg(g=EXj5| zl9Try-DHWjz8oD*)lj9Zt3U}5$q(jGNudz}!O)OevlZ%kD}wf|kTl}|6sdD%p{*b$ ztct|MrqO{I5lJEt0V=3*JNh|9fS7htx?cH~IRu$G7T}(+h`fMxCI)X27P|*)-GU{= zoap&rECLpu1-i73PGt9yps&$_pMLz-qn9J^BFNzw7G^cI9uv; zbxyVp*2yCTLq%rIVz|yA&jX|ti#S^%Smoz@7FoKrNhmiaI2lIVnzW?kq2#DOgchg%}1)N`Qb# z4+FDM>xQ!K3F`^tRZA$Iw@;YE4tRBB@L2Q~c+RZ}&_Pf_J77bgoEFE|7qJr27ZAjc z=10d+qO~<5Y=fnxS985fbG=b-&sVC4vPfJ?OdK2|&T!UU&-#Qgc$ z?5%T1)RP}Ai-3p#MMr0}s+~g-_q5EBS3ND$SZ5pnY{GC`z$l4pdsiW~!Lz>2s=lCE zh*{U)*|5U(d1fezn&_cuPjKbkL+omD*1NXf)0;?#$e3biVOWDFiKK~S-A9FVPRP-s42scQgL!)0EdVljmTSAX#I`nSReAnoDP~(N|eb7dkMklI|&rh$~hxQ*!h-qgcfj! zff+VPnOVQ6r>d(kw0^AEYD{J?3zS(E2&K<|C^v7gWZD-ByU=?gHMcL^dyh>MjWDgf zy&C;8CZ;c&NK$8cTOeQ55xDc>rcHI&e!F<_n*poLswy#$kcy`WLyVyJG)chIuxcL( zJGmMmN*E&F({%LfT7{TTO!25>N3`u=6~Yp_tO7qX^WRD2Cnkbrq)p~jbo8lxq8dQ9 z_Ud4%cg{h<-6lbn7Ij19Ts6pQ53l>JxE)G>C~#Wr2n>)dEE8pNr8O-L*;_Dy+cfQD z+7cLu#-5W?+=EMSN3pgLMZB>TaKRxbVB=_tbMr$tY`Al0Y^->UlGz0|1j>r7%=Iqi zp@m%pE3%P{jVpD}qHt32TwA-^#c;7<%O#}Qz;ACmgte;xZ9Sfzmpv~xdWvo|$^l!i zuQKAdzk+LBd#kE>Rr+d}fOjCGaK(Yjj%1!#?Bj#rgiG}iC~rv=H>`KLvdlzR+L)Q!@l{yjkRJ@ zw3foGNeDitn~1WaeI+t$f{6(die9O=>Wr8#J3=2Ku(r_UJ#09E0Ew78V745wAex<4 z9t#QGP+x!N&PFa*W;TMu%d+Xx}aX5)4DQSSIl-fv?quWgYA&8 zNziHme_dSG6bJ5)5sA*gwC{vwiJ;`JzLEJ6g8bhuZjxoT^B{PzlTdihVLUOW9^jQp6D=a2+$!^cuhk_5OW1gglXhPSP0;H zU=#5%z(hP~2pch^^!IT`KYI(Ah3P!k+|qL_wBD+|e#6)pwb9mofP3v46=-RAx5M+p z{rd;F#bdIug4Qb71Yu!erm*XIdlwRv5TGg=I`%BW@lgCg53jYLct6YY%+518^3ujl zjq*WKY?JH&?OJ$LwQWl^ zNN5&YcnmRBW#29h=_((XeurO1!0S!}fMtoP*n)tdC7KyS)Ebl<6=M;xyaiKmL6<{5 zDPQYNd9I`-jrJf2c1S29PPHPWb0$6oHUb$CBwaZOf@ygqyJm>7BKfb3mvc2v{=D~c7>@S*3KKiKa1a6fk=x45{xS(=a_6lP`? zc6F@)Waf4UL~J~tWXL8Zv9!!1V$WZ+T0o=s&-)S#-k6sQ*#wl+}1`fr3p`<#AwWB^=Pl5 zQNbP2Z1hGb2EPcOnZct2Es}(Ulztwu9JI10Vb>xOPcRj&LRi8L<@MXQZzL#1nNw|8 zsWCCso>iN?_oRYdZDW1HOTb*FWMUR0SgL982gfPWVMF|lW7`L>>-Qu!36 zTdc`a`~G!6i1~sP1RJ0MSkN0sA~$ILIgn_jMTkY*9{5-$TUz7I(q7^}fFxKE_hg#M zn53BR>cd(?kszMGrhelXFnVXW{h3#CoRgPqqDnEu=AZppt51dX&Sn=n@v1 zTd8)YOF{xj#Y;hoK#{BnQ&25S!i0GP1A(4ra_@po`Tbd+AwA9c?6donz%F=55Iht? zGPFMYK6wBa(Iim58Sa)kVGxtuY4{9@$8i{1MS=SZg5~$2OE(ZM5e`r6wMM z6-wimu>cnu`}XXywcQh&n!0~Kms?yZYYUYk8;h;owcRB_>P$lhZy_Zm*#Y@@1uwJs z8q3o*0vby|4ZV*pts*#*S(<);HFbaJle_twHhFK_^R$KJsX6SpTMNHc5-D*Mf87DwxD$L`rQyRtOW<7!TGNKA_= zD?1CH)?DV?P!*g4h z20i;ILdU$i{`V1(yDfioqx=l$&v;Mz_3ybH|-YY5IexP)~5u4?aGIEG`8#3*Y`8ZQ1PSHLWsdylr2EK?Zczqy?xvU! zKxA9&*aRlT$E1yR9xw!E*hpo8c&aooL}rP;a>OUG(RUKW)0o(6OX$k% z%)w+{((f%HpWLBj+6*vmLX| z;2UWbCpXAunj<)d8yd%B{O%*e<=fOzfbsz_rhpji!fQIhMpa>sUzBO# zTjyd<>CyapMM@^mn9EvPn+wY8^Xfgu+8~43tg@&w{;3)go0((mV)vXW^=>rhFbS(t z!78eavvF*rM|r`~&bYXO&dz3GNQiS45575DA}znhzcJV-mtWi@-;@&p3s@I{oOjl! zI*X&mT4znckI0Wy!|%Ggv$z(X{I+d`Rf~uP_9#3iYWmT4DE^~_cd@3tTJyh3_Ny;n8Mbbo> zt2dD6gh(W<&VcwWCR&S@HQGkfPf4C<2H8ElrJsZvBPA<~-X$Tkpd1)^)O&0Pm-wlC zDrUzik$b>_Qjus+jO`iGnVq@b@~gQy-d9DZVzaqz8zFz&H`bpNO4B<&(G7U8&8n`R^-;!l!r789@L? zgGx|@J4Q?-{81bW3Ce1*u%NuHEw4N-ZoNk(cR%dlsZ&(+ywU?#b8-;8%Dr=`GxG;a zca~ml{QT`ewE6CU_tt}{McFXi~b4zb+wz}2<%m(H1yLGvD^L@3T z6Oc?5iO7JyI;N}lBMa9Rp4p}D-g zSXR7x&6>FF8wbi*QBe&n*4)W4-gWZcgG~qJjiom{qeX*MspaarQ)lY~P z?Uq#9*VpXjwMH0vw7H;|wR3Q=5oz%V`KBfTY#|4YU|T3nXc1?WqZZ`s^klKhSTScx z1w)a+g3kl(n*WBnA8{RE8kp=VxFm?A02gpNR3$!?AWbm0I;Mxfgb;@PO`1l0hne#W zp{<~-2uw6$;pfAt^VX2a*)&U;>skKQ{`;J%h(ICn6<`KIrCB%yL~D`02~uT$D}ZLx zQd+f>&(JCiK)qG>_zDSw9qAn;(FK0ZFqc$Io6Fl|1G1ByC+)U#xlvrRjGG2dQzGXx zgJ%#VBX*JJ4DK8(6`6sT?WwbXi{e@;mvQ|EHhbLaJ0xvB2}OUjGjzQyMuhWik`Pw= z_vQ;C`UU;y8ihEjIitQ=Lrpn5FSD}!qxhbpgZF*!4;>WAI)4@O*wajhma6pDcCpwg zff73`6^n7JN{~|Rwz!Xvrw-Xl|7K%z8LIt!_w;#Nz;fPk_BFI8rq0P+&m4OI28j}P`@=c8wNvM+#V$G=iH3C6X?%>XtYuC8f zSS(RaWTZA&?#{hs(~g*jNjwdvq}oN8*_Gi$iJ7~>soju6O3G^O>N*BB7B7vbezmr# ztuVvXlN?U2_x=_{d-BOrqFaa#A;F`RJ3Xn_<=Hh4Z?-o$^?q(*aJ@nAkkW#_~)d?1O$*L!AABIHATQ8I*Q(i zrYFe0eNFGg2grM%D#J`VO+ykU=N7oi?*hmffWKBdJE1FYIbX z#E87jAsf_!I?OMqPZ>YH>}EKcff~b+qJ6H!bw|=10ae*xaB$KLZQRQ2vu6=`Lo5ka;T~U|$na`Q zJ>xAG90XJmg8o%#x*Qt%EE38pzZZF$(o#_jzbwihP@T^%<1a}H4vrLfv(_MbPB2Ru z36#_}17VGn0$3L2k{Ki*#OayM!mbQyyEu9G;>F+2%pB_DI|YB_7x3c6ryqaZ{bP6c zk8ip?fBd-nN0V-v3m|E^9x<{C1+qZom)pwA+wxWy#CbrErLt1_F{H@Z zn44SLD9>p$=UQ_9*d&;`KgIdn`|8yr*=@hpdPLaq3zu!DrhJ>Ra1w*W+tNYdIfqf5q6; zvt-HT#5A+{TA5$4fR%|vHj1P;SBzLez=3;$2q=U)AA)C~M48+PR&z?9s4-gn=sTo) zX7%qPdWA6Jj6fJzJOZ@INgh%$V;$cBwYY-vVvqVZ51E;}J&hl8c^@iNjX5M4+L(hz zgPg`jc~bCH))eRc&#$INRh~HX>q+j}Q_yVLAX;{KIJc4;CHmZZaOcj8NN6=)Y}}Mv zXO>$KXkI+CQ{>5KAt-^Ofs)2Ktrbgh{F3emA7sUaNX3U-;4?^>P9#$Q8~iS(Wn!ml zCy-7k%t+6GScXkbeml8%o7iTN$nt^=?pG3dn$>U$@g#`pb; z7vF#Xk)L0{K0f8YI7SeBrZPm#Yb7;VWZd5h9#Rc7fV27yG#0cbBW$DP@~y?BIY>wc zreUMAN~D1Up(E@F(|2-4GQ;*c=NwW#u3BA_!9_bD3+DB3S6>4Qi)>tw>;U?HIoGr+r*E>2ET8>thV3<6#T znq{3(iL6d`2un^DANr!!*7Cq{M9blP^8WYV2P_GAK`pW02gJVo@)I+)k@L|foG&?? zFN0qMmHE31f>{Mp-rmCcP*R5=66(F zrbLkkN^^7i5q<;HSYQ_q{7EGuyV!>;}n5il`j{cLL=!ABnNukX|R? zXyZgBIJPJ3ZI3?}|3zQzqS$3cqeWSq4?cPS{qF+;UVwo$F9JULgrcmeX^LWSD9ZB_ ziu2`1I0e^FkTmc`l~}cMgS}PjKV=f`sTJ~r@VF2g{nLmN-qB$ajW4hzf=fwQL|6&s zm`JaZ(mArg-_B6+^G?LaA6>nl6~ppqrI_zbN)kfbioX$EZTfsL`f^Q2MMuSKjD>I2 zfvgxc_xj~{aP?;l%ZiFXz+o)l;N(w6-(&hJJ@5T06d)o*IZy8%VaPQ>Vo z5QWJGqF8%8QxV-$WsF2P4o4`*chmSw{DR*fxqqh3=i8ZWGvbWSPGJT#8UoAR>>1|F zjMymy+@qWvgy5pIz1_uya<*N2;)}j)mNpA%+xH3Sr~uciF9O_inu70hIMyT6)7Bgg z#j&2{Ebq@9OyYRkrAw1tkG0n?t2gn8T_6qxprKo0NQ$1Y@SkFg;szn$TTZ^AJCn$p zFZI@w>||<3R*JJ_Z^~Y0=a71C$Hl;U60=CyJYm6D;9w4HX1{xlh<`bT&ksVLydx$W z;gYAwyD>tE<`-`kSJqHgEM!V{m{hKzq3J@q?9{U#!$h35E#2fBS;s2&h-@@>2OI*}y;(W-dcTFgv$oc9jZ z<^-PENRer6cR6>?IsJ=t@GREm!PcxHD%jfZqX4SLZ^_kw#+;=7srw(mVsP$5NZ)@y z5_nGa;}ria&rBTf`I;5auRIDx&;!G$Iq5mcNi$TiwQ zO7W2yy+atP)M3b>S5cM)DTfayJBO@TKd`@J0BK2z=^#nSEU%Z9b+}W0b~y)XAgE@f zHmv!GqhdCn$!)wlgf?lpC@}mQ60qq!R5b5B4{c>K`?!GPNOsg2@7@o#=CinYoz08l z@+vpWWd04ZYYmWi6cQ~#HcMW-_=xgb{QZk1q9uM)Sz6zNfkK3qVrgw>hA@MYuC7Y{ zmiUsm2!XI!Xp>=ss|Sdy6}+l*($;vy<{5p$Xvt1R;WvTJ?_Uj*l;wa*L z@WFjpjilZP(x4*uUNw}}AgtmkBrBi0J}(~Fph82A1Z7!JZ52&2f(Q{sf*t*wenpdy z^zI^ehBpardNgcpLe_7GWQQUJd~-Og*c&oGWc~&&bWkURcd|O6tGP2y7RX1xUg?2B zQ3pSU9}_K#j*f|mHOH(ImFD(Np}-;cL@uK(FBHbgq`G&k@E%hyPG9Yj9cx?Toh$d< z8hJzJQ65(*^S|6-zCmh-6o2KC12x~j52Z~(Z@>S2O?1xtoRO@Ez|eCU@$nf#T)=>m zdYwz)ZPP=;RwoFNsU*Ifcm~V}tCAw4=q60>6V`TwA~P~2NP<676Ac2N5;Zmr-S-7- z{~P^q+O3>^-=X|XtIq87+=(kCgzywQAe{==%qwG=uB0$&4K+Wb_o9KC18k0Kw5u{_ z3kXz%gL8o=L6p+__JpByy+ff$*$cL<+(Cnxkl6T1R>f<)x9ETFcg%X8I1@LC#SGT3c z0y@EvMqZppfV*_N|EK<`pFZiwnb(wikYDhZpFnrR9ukXKia0?#oh1(}kj7`=stUxW zB7(A~LJU>p;Vam*vm?+`5FuNEGc?e`t9RY-Xx=g_R|J!9^sCTNM8iXTkbg3c zeoow8BE7WXpwrs4&;oDZW(f&Np$-RF){ohb{SXrJyZP%$)s|$NoC;*uI&L&J_D}UU zCFMr5!rLmZv0~kmV4jxVPx|~!v}mU{nnAMiBQAQ%3c8#W;9LoH;SZ`3g-Ndv7E}(T z#vZ({RDPz7khRJDWexlMn);`Pm41ycunf@${9r(>t$`bEgfk*P&f}uGfi*(sg!n!P zCV?(q>g=4}j)b*PNJ$qFFIc>rjjmspuI5lT*j(h&xGRvk>`zMa3-)uz6+BWCV-5g1 zWHx<$(KJprs%WJ$BG)sJHI!c*vR4tNufpQ4u>a5Z{ytEUpaq@3=A8^wZQ(@Fz{Be19&hCa3>>FwcFOg5=&0 zZmj9Z$$HG$#47i4%Yefhj}3PIoW%uR94MHcJ|kV|Bo#Z6vg!QJ`N(m{i^W_!q=WC~ zBvF2$?y-EB8$Qxv5stB6rBnOv_7+_`ck6Uifj z-YlA#Xf+3uK%{j}+W|l{&Xeb}^aJRZL{QaB9GvS12DY!UEVZmzzkU0LqZ>jjmXwh7 z<#FxRB!|9RSa1uf-T&sL#>Q2HELq&GuekiH|DN>uft*rz@0^^{(lfdJ;GJ&-D^OOS z8QP8$sK7g$-{1TF57d=i1A)sfdfycUXQ_u*4TOgWb~O6k=4eK=A57(R$m|L{WTi>L z_y7E7_zfc?98PdfO-Jd_e`u3-R(5)Mxgp<}k&s|Pn=1+f#Q9CgfQ8L4OQ|lW^e&+g zomtMvNM64lZV_ybkmuwi`T5mE^ApQhXJ7dBf1y<06D>l>@wvg6lG%NS8_0y&h+9CkCIk6=gj@S!Y{SlEy2Pb*iBat{* zj;`OhfBVs+Yt|eMnLWJ7vN!%{Xcwr)oMAg#Jg)N=H|A>6=YH-Y zDrS+W6pcu^xExc_&|S|D1IvY7$4n(9vlq4<`_rGUT$#7*>zjYDJ^06_eqU{!tXS4@ z!w+)L`2ev-Qb%QHvz^S-TbcFe4?g(we{k;K=k(`@Vg>~xI8@;3lj-RvGLVk^-O}C3 zVoN&O|Iz_jJiHVVJ#+~((5_L~<(%&P#TW7MCv4r3&qFAf)A*w1Q$G4Kf?gcRarbM& z-h*fwUN0XKBpv*GCo6mZna@8A94Mb;&frvq!Yg-HxR%^2FE25HZ*v4A5<15>Gu>NF zi!9kgEW|$;NP54>*<_L(4#_FbAxqb;-?(x6QOj(Kt>ugKF0ol3f()p%Gj4P7=J0U; zhVaU~f<9?&oQx0Qf&-A`mvglg_EvshH6}2hNz0=oc^Ewsh&?rIw>>Er-{JSW;3f zUpQBxqR2w*Uk&Wwgqg$h+TsdoGo;ON;jHKyzkZ}l?< z;rS1Lx^rdTy!V#9cj-~X=FOEr-S5jUllm{o+UoQ2kk<=r5Q#cEWLD)w1zhXcIfxD* zoKSkvRuq%UVx6iR>W>(dHAB6jgd!HtTt^JZ=%d``uSM-tj2MDhO^UG13?Y^7;0y=G z*?Fzk9y`R)jR$hgHIAQ>UZ5bv`Ui6Ss3e=af+hgCx^(AGRMpC|qr6LFJS$xgEt&MH zAMUKUpw}x*_clC7lQXVSHrIegnbr_~VIyFHgf!>oB=+rInxTY(h*~GVUA%bqFys20 z!_Jm3`jTl|!_-$hHOJ*qmG8>J!}D$tcdC;Bce!S;5f<{3enC#|p!~j;lP@Yp6Kv)N z>dyT7E05;Q`|G^FKJu5D&9j>wx{;%3YPuoIqssl_^2Wjg8?Id&Xe(G$P!Ja`yNu4g z(leNpXH04gnucRagXKF#vf6Yio^{A_DBc1|Jn@Cu&Eo7HpWuVb2jK!=&7E-UaHXmQ z@zNA=LcG_>J?2ujd+Cdsefui-Xmd%d*aymy_&7LNAV^A*=OQh!B8J7iR_ZOhn zY=^HC;LH~0^+PG7-xA@29e}%)muKGWTbb8sDVA1NV^?PXGxB2KKp?I$CBt=~Hajl)=u1KGpo|m=k>D#1mxUL%-9lZt`BwO~ z%c2fXUsvVti9k--*fb^vFn_ zq)B$?Pp&GQmvv92`k}s1fn6k>OD>3JHnC{HXz6_0>rRX;aYE+5wG-q6b2%l;q}Ae? z>BAphhsdU+eEYUhpY0^P^$RASw>b}20<&?-xRobMynH$4Kxye9D!;vrJ68!b+Tg1l zXyM4Kx3d4{oBs?9EVim2yJIzGRo~fZUgY&v<&uvCNg^3eTP-Lcz*V;8<&jvw`Iqpr z4iO)93$)X@pio^_mRc5_lvFCJ6hi)}L+^;=x<)J6#CB=J=NE zBetAa`_)%_9M2$!AALFca!}C6sLsqw{B+5_ni}_<1Maz0skNvSc5FpzViXta!mVYq z0h~(j`ltF51qBt5;)%?ol$ zyS`t9to1s&dd(gy0=<27P;}6zK|$v3@WfB|eH^r;)cq>@kC6mJh{$3kveHmcDZkg2;UT{k_#8^J-4bytV8FAW=y{$Tq4r=PfI%Yf*^{WFdMX(n#t#9&?>)e z@teaI*oNH>bQ?KwrlpHOHnZ8k!vB`=R~x6!yv;IrmgwVa;CFOO@)2wYrTLOC1kmI zd7(P#b@b>Sx&t)Yz9I;)byrZ`^M4Eq;s>3oad$^KBO31_7ZZLBHB469uj1X4Y(4t> zS6spM>%g}X(*?!7lFU1q2)bZtVH#+{5&cDDL?dM;4Igu;y^R~@-ex0r2gi5^_~v;1 z#P;=;!wzVX(j!B+bjIlxkn67S@7Ub=>#sI8w_qXpmn&k>J+U*ep`qhZgXrcb{qE7< z|5(u-ZjOrL)^874jlRiz^5%AU9Z2n3-R0(b)NS zREVOHYkBt+y8F-akzcxc;Ig?^x_f5vx4RD^@apXBLmyJScxGntq)&#B>a?qM>qDqT zrOy+(b-Jx}JG$2Er0x8(Btb>CAY3t!>UO~y1D{4OfhIoiqNZkFMFkp@m*J9%+$fKX z6y7${Eu~OBzw*2UXX2<-3iAc~d(W@@0KayYG$W(y3NlMK&dxGDUQ`TpG%70^f5 z%FGSRSREJZA{uuNUcOv;Yf)TWWp?E+9$#(W{?%7&XHPH=&0b%0_-`Jf%Y1(;$Y=r5 zm@qTMVo0fQEurS_Kuv1bfkOffFqDEv>lB=U?-W$`0qQ zP7wQmQ*eCY`98mZpj2fpaZud5P;%$W59I7p(t8GMNRu}Dh0IJ`DH9t34G*+Un0rF* z>9>e2`4-nhJhW!x{yiVgKa7G8PFY*B+Xj&FySw@PZsav8z`T72_U-$$!d$kx0atfq z?e~-;^lOln$zUeI#J0Atpv=}BU0b_o?TJIwb<1I|ZHE`1fI0zT7mw{Ww6MMt;4*}Qo(WnQq^|MKlGn*bfi2H?!BWoP8x9oGiTD4%Zr zyu4Fok6 zF0M(HIH)chyEE_3y%N=2&j-yUv;JL@JtZzKNi@-H`=^8gKoVCF(vf2Q&TkhdBVe`P zZrMSy$$)%jcgAM_27($~kn zbSJLQxq{TTrHf~-&wM-iw?)M=lxKn6t#a_qKYVjIRbjR7d-F$@7ry)cFHi3R*Yth; z{}b*YqTI9;)IzNd)+@Cg&YG#KUDr9R+UqV~=PY*hQaY>s3S|{%*RS=0ezo1SZZ0U7 ziI&t$s{_%M#KsE-M3$t)0?C(##;hsDHWiZ3C;#UO{rw*Sxd`Nw^EsE-d7bk_Lm6(H?(n<(x+93gfoDaBm|%>>^qR^&M)u`KKjJ;?H^1u2m{zv!9@;xBd zl?i`axnl33xG$f+|EY%>OE*JK{(a>AzkFJ{8%kviD}z{hUr&SKu3cE@u7k&B+?wRrIZ=e0=oMWGC`?Kf{o*ydYkHB4w z73{`fSW?4T=vvre3H1T;P3I?v-$X*9Y$CYtyGz3*oA9^!E`cG3|64lh+N=jRpN<#t zqHb|dUqsvHO6UVO&9HZ73RxL8KNvcXIXIE$ZCIls!dcUsom6eYd-s)fAA%3yvIsuTg5bAJVZD+*0^g|<3Th|9`~xdB0h>)32uArU3F^Io@Xi4 zkI;ZbrY4A)@;3F?uc&(xftqt}(woD1IOUs>ry*?qc$C}hD&v*%=+a`NsyCr@%=lbMu!=HLqlL;v;5U!MQ3os8@y*$BQGzwrFFZReksI$m6c z;O2{q7G1@NaaDv~q7_R1`A1^aAq=gpYh-ROp?h_ir2h z!ZEyJ!4W|zxJIN=8k({`^@DK6Hd++^MJja*hH@;W3!+Z8jR{x#!?Gjwta>JgJlu?_N zlOtc*gV5&eJ(HCSm$T3>N$soAW6q;p;6P0C%OL$jFP3h)SaA`|L4v<2YHl^5t?0hx zZEdI+bC6#-x$?Jr4}L;9`O=H$&Jh6+S5o>z>mOR5G<&^zOj#fCqIxN6{jtVUbyrgQ zi!W1ve)8b1+^qepL+{2NC@1zl`z$am^p8a4A9U7y`^s42sX8^46J%3gIr-a$IZ;o8 z(KsuP7ehcCF_$PHE=|T($z&-xxWch(`rPQJ6z=HzAt^BCL&FWu2s%r&k4Jb4h2eLzkd_vJTniHAxT4NIsRbQisaKw?meVwSgkTl?*D_2k`;o&0UaCxNG*K6GsT z+I!cgC#_E+PL5iizIOdu;mcQB)LS33CrUYIq2?Ec*S4^B*=(c-|nQ$b~9 zPwGDsM4QHvLOnY*`0VICRCec-iU;MsBa*Gb8}WP$OXtSm@B}LoMY<=$Qjm6Ju``aj zGRcZ#uISPoWQ5pGi%F^eMWHBT29ZS62h;)mH17uEqW*krQH8omsg8rWoZB~9lqJ*_v0N=0*>+$#0cuW0 zfD;!_aU3Ce^RaVJzQd$~chYB1FWq%aX$&2_D=dpftf_QYqohNPyJqL+PKUdQez*MF z1BAT@lqL9&(7(pu#7QbS1h$76-cwhWpWO5bl8B@Kh1`*W**Av5Q+8y8R{(4t!QlYV z0!9=zNGaiY25pVDyv&3Yc4jzQK)(SGqb@u*1ouL)&nNYp?6^$*%01rv<=^`5EEK1&Aq z?#joj4{`Qd(xI6}L%nVl?PxEVG z|1w+_{^tFqBQI_t>Yyi^6s!inXI0#W!VdqNZ+<8Y36v*OS)V5DX9wGt9GgBwI^iVp|Da|dNp1V0WH#)L>?7QlL zG2c>s|2M6B;$r?y7n*3!)IIo_#K%|8Iry|hWT~-*P#PwRFj4E9ApTf$V{70>G_E=R zhWeMd@QAv<+_*6$quBp4<|woC{&I9HxN7(fe6{r`<(?a%wEG1*oJN##G!m(uUV3)+ zCJh=OTIjhbWY)%3{q_fA(A#pUigmj1spX8cAy5O-C2#E?Mn-($YL zxIqqm$xHiV)L?%4!58SG9O(Ujy|awl!4BznObNujC>sQXIz_XpCsSW*(-yVCOi~#_ z7K<+)YUHfWtl6{EUwCKX!lxQzrUPCunM(`@SgTNsNPu0ww=s9uuIZ&pZfxoFUFp+v zqqE9kHuG@&OAx`o={fxOk|nfljwK9D{k1YRZqJ;N0rUV_Xyn9|jHe;U@ak@yzOEjP zxPIqorvqIs!J6l8 zT9n(kKK;d~mX2QE$p7GV5!nksyvEX8SmugFYHk!EX=3O=^tX5Ck+ zk3D+~5?P82d|Y0{L80=5#W5E%E}ai7eD&3ZufnD=K`7jJ26&Xyo?&k+kxxC%VJ}Ic zX~=tV(_VCH69jW;a#Hvn6cv42TU*rjrn({2a(_ehqDDc&7+UGtgR?7U&u(m7yI$(h z$4X z+9+5?jOiO7FqZc^8tGElXCHX?83N?ug>^BkHoqBg-Z!M-rADIkIEY zBw0cckZ&zhkzPb{aGAQ}N6V7u%}bkbxwUe_gmtGXC)_@JJ?(7T^`+OZkNbv0Go|8> z4>KE~% z{DDJ@Gyp#lm1fU!^{S@ms@gW`Kv7X`5v_nd-7Qa4=f3dd3!Cvjcp#2cHm-$VItIQ$ zdTL|DLA5dUprj<^te1A~oUv}PvcnVYUtZhCm+KEmrou0 z?9|6d!sdJ;L+|8J9tZlR4V42+-;5(19bF~@61k{HLX+Z-Oq!IMdi;2*GASZ8I4QJ4 zITA66|5z3+Ke9|clDJHkQS4a8hh<9g_~R4SC7!zW`s?&jRIFt(!iQ;T+s?lKJ^~Xr z1453Z3sX56QKYnuDQ~Dglkw#FOP?XFG3XY`#bJIat_g}lyTba#dLW4c!o5gILpSbF zBOV;L1xGSh8L@{vg15G!+BUk<^FAHy9qfKnvwquCFRYEJD6J@+EhYOG*3X`fEz-rA zp*Cg^?HdnXEEPmV=6WyZ2Xgo4M#nQn2Z3$Kt5u&5{~T|@`OuSI60WhejVN5W`0eA@)}3lSweIx^m93}L%EXG<9A-avotW3CZhENtFbwA5J)oLB z4b3N;@n_|8a-*UyUCQ_@@M_>y@(C~_S|ujP0G{_H?8#-00XLk6kdRXtJVu_wzPD_; zcv0C@AVT{Wn5(S~C8O^|-`_LHr0gfED@ZXaHqAbUf*o4==MRs~Mtlg3d;?v(xagv? zC{#LvR6hgsxPLm%KJ>Ft`F(GGMB_sqoj@E$EPC_ZlWcfd`R-#U$F5vi{y&ZMy-4hp z&?_if4!i?Si0ROm_%2Z)1jAbp^-?EA92YfHMZtDN98vfffx;;w(@B9NcTGCJE_L0t zb;mECT9w#ZdHdR_KKgZ9w^Y2Yewca|4b#j$rI4N;CUIyuDFH31uB~BC%+9-Z-pt7O z41r(>PTDaE>7-8jPin-ZU~1&5b*bvQ(5X;j)~U*C*Dm)Rp4?h} zN`sGGi_1DS8IN`H66zAdW0HPK>Osuoh5 zFNeNJ7Yqr=|sR1D|_w{MwQt7Z8dHs&DbiFQu8|ZAuWl%6F2d|4 zZ>qSM5qK5$j3|h+`ry6OKVTOQxtMyf=>ailyf?)f#0> zNE4c?rHDfT>EFv?IK>twnEe>WRnJzrGf4FE#1l zqERST>7tlLq_X$Un34P7mC(C+Fq%_W?s@zqhbc}zescV~e?^^SI1@$Dlhx}f-Mx<@ z-GExdxwH`*_RD zY&}(Zd2*r&buy_{5hm4~x6iiEX1DLN7wujD=YFr{Ql{W<2f7g@rRXtRUP&8W>rt_JKRTS^Ag(<{-iuw@)*VV zW?jG9J4=>3IEr<}I_nyvkz=MC$FG=EF&&4LJ8L>#Y5j-LwHLvkz^WH6?TUFYD|b3D z^gs-t9Z4y9q31{cZratnt3@B(4fQ*gmi6k_nFmDsBa?S%tV$hb3Av}^zSkv@QfN}> zxO!ydx*d3^3(JmQJD$336=K3m4Fy(>7<&8d;M>97EFK%$Jv0;=mY15S^o3dz!lF9W z$4(=~6sii@^KABgZ2ehcw-;5v7#D6VdUfXv1jPojjGZ)FJjKYBvrmCmu|4&4)W`3g z#xG40T)+TyvZT`tRa^SU$|?D}{2KJ?)MSj=V7iKw)O|K(-`E!YhPxT@R?M;v535(^ z>&oba*S1@f_Ovy1Zze3JHab0bI#uQAq&ADtsia+vGpGsg2Y>F$eJ+9iXEl-*@_qZ1 zmqOE03-`U0_k7-;kX89(Ppr_)X2_O0J5-u5yO9B%67-lt%=r1Ep$n0Kqu`OzxTYic z^yDK)k`WYQ3JgW=9-66&+0ESk_E0ctJ3HFc?Lh<;Ig~i0CaN_dHV`K!W@aX~nw(y@ z-K>~KRoM|#_`T?Gm&eD6BC;iRobDDas&bpw!4@tGQPObrvnU~m=ebHunOG}-);eqErCHSFJ z4BhhU*x*sjz+nrAoZBrMn85X$*oudqTAN#$Ma)|?UCmV&X|f(vv!eEAWlao4Mh^bx zKgwVzGL!Q@Jv2VQ^b*4a4sZnLKz)7YTYc=Mo1*l^PN|rUH$dlQv_$Ae29V$ak~+Q& z0EK%3WnM^5y`~h=hPml*#3%%K%OPfS!Z(`yWg z218~|Uti|T#J(y^q#^S{fKfHMF zrV22{S`G&zIrZ!le`F=zJIHHk+ON*0-NRh?cYgRz#rW;?tLK|)s>AAaxS?jbX&=VD z&up%%Ysq7kW`7~OKRhY77?;mZ7KRNmE}%VFoZfPxVD%cN4DHqYNZGY5R)Tks=Jkr#LkYYzM5H3 zlmdtae2RJ{kPDKk02jdGq2%~w7cN{&J$~QupQz%%cgN1$9hRG#7}GSfBP_Rw?dnkK zGgo#G?JgLa`Alo#&);gwG$hs-)S5H{f@3#ehuiCBotN2Usxq5Zd&oS+ZmM1%kZ!-1 z_lHNA?B(1sr}2>B;wjY+pV?Hp_QN!`e@r;{;otwbO<6i0$!$v;J$mUqzxw;xr`A_@ zX&q%8sBJo24TC)8a6^Nk2#2`OhSc(S(CN(%9$D9gvSm9MpVL_wu%>cMrM*sHu<^u| z1@$ZTBDOO>QL;w-gmEjg_V3Eg{mGeM~80FaFHR0tVf@ z^UELp`JH!;RW|F2{nmV&rKx$!Df%LjS+l7QOKMFri{&`1zlmZP zmo8`xSthoxX|dETIKe{S1x&jdzj({TXU=3TW^NrHeiDA1dH9j((^^`l?b{a?nW)*c zmtJaN`r(GvMU22)P{K=p`)wT$zwW!LnN=LmQ{s|_<%ZuE>Omow37~6<^OAR5n0I{E zs#QY;xO@Z`jo_+gs!btNs3QTn@nykt&mpyYUc2&(ve$n8+*@xYW+no*YCv4XvWC9I zPN#Rk?d-6tRUy+zJE9|Uo;e%XMgc*2by6tnjlV@l|Lsq|i)KRFqpcX~Vuq+KK2^DP z?dG-V53T+1sdvsow5T6G{nXNN@jpCNF~!cdGfPRCrOD98xYn@Xim*|8OI==`Depwf zv`>3(eZ@k(uRK8eY(Hz^*!v|5q^$kz#+C&P8)eGC#(K`4?)}M`y=vBoGa@(|9nPR{ z>eWzQ#8u_n=&|L?-`t?o&C09YSX;X~=Lt^RzxE^6W9oEncec}+-C4E0d6wWGYHGaQ z_{+R`!YBadcL-VYNHU~EGFE$S)vD*-dgiTYc4=01&Ft9DM$MT`rkPb8wohLx+g(ub z%rga7t`w}ka;4<8iBmX5WH8Ol1c;45a(#wQk(}3wb2m@~29i78p5wmq4?c~NCd)|rPze2>5b1@nZOez5#NEhf*|0tV6KJh72k$n7N^ z+nH8q(>eW}e*bpo_P)|IsYk-iu|8%VRD`l4lBf_|7J{h>?YQ^&s#S@JL*w6SGF91m zbaYfz?XE&@X4XOe=(Q_|ZQ?JKE1{CmYjIclYBCK7O>Ir=t1+QY)#raJDTy>u>?%@LViRbrhQ0!o7c-HfUh!D^(3n7;LQFu%>5lz=zHO(A*Axk6%VQ z+1jIQR;^WWOt9K4c4M7h&kV7RC;l{kMdQ=-r!g^x?(a|Niu{`pXnAw?R70WdAwSuJjh^jTbG=r(5 zlOSX^8B|lqFxgyduG&6-`^>klO?Yq??FBRB+{U9Tq91*D|E~QH?t1yxO&)vWo|y)pd5~CXojz?HPov5Q5)NG zTG+1I)8l0qouk+pa3nd|s@L9cVQPD))TQj2<~$!t@sb2_$nZ0|&9O$0}TroHma#4E3rl;m$T8S~d@ z^XoI~Gn@KaPoZUDFZaE~`Z%ZFP#-jS#;#BoKZ2^w z*mZF8&YhPIGAiKF=$}0o{j&rnLRMFA+Pt2#hcuG??~Lht3tGxp)9&)&@U1Rgb8GeC z!_9ps)dsbB?3;Q$J@C~HMhD6^z~V{7#JU=*>PXgFEH>F9*zGU0+l@xmG8T2)SWv4! zVJ+Wuh_hVp{q)NZ?uV~DlOy36$#DWOkAssfdDB`*#TW;XP`&q`8{2isfHm4|0lfl$v>aZ1C z#*cqy!I}kK#+(A9y`#Ra2s;dt-H*=!SWa{u|`^(Q`>^rRAa;mRUcbWYigQu3|<6U;)|amy1nrtuDqA_wX`Vv@>-U>)bf1YiM-kq z8~f{?s2N{iwDKny993xLe&FJC+dEs=rb&J1o6y!cLA?ZqX?Lkqo%EDwfe}j0?<0@p5K+S4~CQ7pG-k%g2}1ycwLN+4mBM za!IRN_6h6C*p`id-LPQ8_=G8(qQzG_oWTTxZ7^O;zX{6`*DQ9iii) zRWXu=H_cRQ^I9-T9RVo}#X5jv9Zsg@SrD7KX?&Z04J+`hMohH6M(v9-n4NCb8FH&$ z)fs6wwJ8nB&2NIBtd25&S#R0%zrLxQzk6o}>1W2S{TW#gGkxOsZ#??OV#cb?nX_wK zfXwUZOHa}-`tag^+R`-!j+uUw1 zse#+Acqh1>rsg@25)JiCB|>Hne_2_3$s>>adn$l|Uvyhv6GJ%`oX7L#N zQokNZ@pskqaUemDLiNiQ-TXE7LCak{E3v`BP9@-n(PA{(w{PD)GxJoMO3RC8>;L_b zd4e16-cU<&)*`7!od}DEeq*h^AZKHNKF3gJb@kXfkyp0cX6>k1%hdfp{!!NYsarFT zUZ+S)&GAAqb?kW;Xz4@Gc!rJ+)ih#ei|xm(1GQKSZ5GvbkKJsw8tW|DGInw5toatL zMXS|m^-Tst6|00XO2r-FRNcQLi#IgSsoqZ^1DFxzni-nK2 z4tr-+X5y*qoNn5RcZ!Yv(}q92`H}jj4#6QdSwOU5LtWjPmO4heF&e3$Hm8FSeLv3+4>kMU|ZGZ72 zE#J<~`0SI&7)` z4M>*dZGKa8Urm*JASDpKH!-s0JKL-JN4lMzW>d4N!|BIyoKTI5CBho8M76MiRds}j z3K3d1lv_J(9UVxR$wDrb>+liWw4k+dE2j0AUBWF|ESjRA7L*4Vww}0B+&t^EZ zvrYh^i6!%H0E|;-n0;MF`(CF%=9{gjrDI0jnuPFfu+ROQ>(^R`hJw5Al}yrX&S!%s zo9z3Y#kPK%gJ`a8!2(X?u=Cg`faXW95!lN7+1z=*SSTjy#Obik+-@^W*+dkr>v7%s znqABT;H|>EZ~u`&Rreor8D{x=#Fmwz?k7pj*m<5a(^44!An^Xur~iIz(cXj;)6|XW z`ZfKW)ORDE`EHl9%h2KcuJgNqGh~Z!4mm~vI(b^BlaGFfuB1!VN8)A`OIX*qqv#o+ z7c;?2^oXJe@N8f=@yp(3>Tg4&Q{$+M1uRAHk;dqc{Ugx)YqX4p~_b0ts3|#z^}bmc>mVVzNEfjq@gWx*cWi&d#?x@M($b z)?PdH-S+P$6v<&RYD<*bsJ$IHuwlb$=##=8Cx8#Epyj3Z8})4l3&GjqG*`p0omPk+ zNi-l)?#DvuzM(NI^PB6HeTh|>r1(|vS9RvDF1?-&(@t%>sCcTI#nWqWsrd{D#VmEi z2vr;Nnp3N_`c-$_cYZ&wsMDr8$GcH7g2CbH<~n;JYoysC>>C*DpZ5INzrC?H<1;lw z^VuaeV?@?X40AI^N!zya>b){cZs}8{TMpNsSgjxbg#Lsqj~8!FHc9NVSxKZ7x8l?| zy1`>^e|HZ8^egTOPP_$R)`Gw9x7hj-6gIG?CBGlZu}htM9dg9FP%~t%?2Zf0?CWbv zNg2=-wW$YUD8QAEEmsdDzIosyY;)SD&h8$?kNWauJGV?$X!Q!SiwXsx@VPq1|HS+ALa!Mc>5@ zX%A?bXu<8!b%wz}$oT-v=piO;d`YUqm=XtDo?Yy(_q1%-a3K2GIR`&O&kkO?BvmJc z_S%tSC}fU1R9byFXTh5M6KYOKAJb*DvMgS;4mm>pkTVX$XAOtj6L5tb8mB|SW5%-d zo`n8H&=`&TFy;r16L8kz_yF*Q&iNz{j;R+{_g_Mh)XzfaH7|!vPuA zZ8=RjCcWL}1j(aOJ#N*iaX5qS2$VP3m5mTU5)LbF7_7k)COU1%#=)&y(1N!Im?hdv zM5(g9EYemSY5anKIn=5%=p6`dubMg2J)FPaqBrsRf1(<|sFa?;p2FgGpgvix z-&QF65(v+%2Q_aQhm`K-;E5bQNxMDa+O^jwteti3HUS{5g6fe$O^k%QRt@RR_4NjW zJ}=KMV(aFK*&uIJ^+{dE6ZPeW5~t46Azpu^wVnGa*vo3Kt2L{(`?=-#KJowFp8mQP zbH3SPbu)#$u&{2!^S}7r?|`u!JDKsl+~zMlAK*0RxN-M?*m!12TUCBZepk*D218C$ zUA`7x0@mN@aJzaP)^667_xO2H*de~2&9GcjKX*%lv+B-tQd6Oez=FKO8YchAuOmDa zg+%DC6toD5cvHLs-}$$btQfraB0ixro|pHQX^%|n{zLD7_cBL^*JHJD&zw%3L$StL z`pwN<%@ya=>k&7Gwq~XM@!G7)*{`!_r~=ttlR9o<4G(x&gVH=6$#r#^N%*)2#V)_q zexj+?V$tgj4SJ`e9ois@dD=uLV0OQ)1q~%pQf#?qyR%S$6HXfQ1$z?+>~&qnHLE9n z^tazLjpF=g%K5;nibOYmE_`3adk2RH1GLo`=%AnpPzl}7AvuO*qEsk&1B|$p{=m6Lu)p*uD^brqllEp zMcr;Q%;Z$XwQI6&XdMz0ktI?oNvx@19~Gyg5n5AizSXHX;z{`0TWWL67By!CX`Kbj z$HVp`IHG#LYJFG>MN3ML*lad?3pYvl8Alm3nz1W~UoZ-nrQi5b^mD&?c>j!@q0bI- zI&5=>vU&W38O-5X%S^VK<}PGxs&6B#~XW2+~Mvq>+AG6`mmK})ODT6FPM1cx!4Cc@7nw@ zn?ZigY0#gmtqB}0ZarKNgOaZr<2E)ov$fvgQXC_7xGrwKSROj2$ogg>XDwyo=T%1v z|6OPS9)LQ^9D@{CpfaKt7>)fd#h1tq`#Z8i-s^R#?j(|zLfDsqL0grfNv)4+?lU+O zK7FbE)BczGKW+I`k%FF2xs{CC4YDE{jRww^@g(s85EzFp4IR2^H#k5q5y4NpDe%F(>rT|FmfTdAJi-Qz9=m@!{At7jNZac<91Cd~2$+t~8%YXy=p#fy4U!PT3QNh-eidokxFH?L<6cRDh zR2ArmAS%Xo;+ilQ)e*<#t;vC|Qa#ZTuKIFQ(3psY_Cq8aTS|bDqp(5bl@I#2AumUd z>PqD0l4E;4UI%wSq)(>I(PCUvawYmXh_p8zQGcN+*sWg4TrjglOBJNfLc9kM+KUHC zv<9SD0xWbT_zQKc0hjQpS|hQupmkI~iY4F&{@K;tZCUk0 zt$?gSomEdIt)Q-}&S-)42pSV`A|)UazQ(9pwAOrMzk_%cL#*rZdR3Rkp;wKgPB0!g zrzq#d>eUk`u6|?h8|v>uznE6C`%1xr1^ECPfCfIx2I%wR#RqPAyxoE@T!ZjdFbi%M z`c)jD#9{5%qH4lfH_j7GwMnuDU9xN_x@A3ZYmf#3b^kzc%~ zUP&v-&+jTA!dO5?vegR}8&Ev>9)}CDi3@V$*5cgrdz{>SThN+Z2u}cQArYaNA5dMR zt%ZKjn!cd6%V^QLJYNl{o(SAoI>H$63?h4v!`L+b=QYpNzXjP>AJt{*@Wy@h16;nV zr&sZ5?tHEc+~e^Mdc8h=#oOy)xg?SkO>=r!|EKGi!jc^IT+C^@JFlN*>&T(C$7X9< zky|G92D3#8+)XakN6i@9z=ZBuQt!K7bf!KLf-am1EEV?1dAL*Fg= z1cNyEJsmgQqqwp_1F);s7LSm7u z*t^?lE#r?+M$wk}b(XFIlOEuyB~3V?7*j091$ZE{u_d3x9U(e+%@N%J*I3*Qx4x^k ztvSa4;wWtY^bbGy(dy@ZuRf}YouafRAMVQ6(x=hs$5Ul{C|N$>@p-$Q-8?ZMRYz*c z^^;|~=L62Qh>1CbR^u8gX&PY?KHXcbyAaBJX};u+&2QX4nc zgEq%Ag(nj*Kq%|?#eVJkdI0@XeDPoV?hN?EgAIA6q8;I^UJ6}M0_1IFcEkUjyM6oI zIZoM~UMsEN7dLI$a%^p7YvxQfGcM7D7lVr(vW1-S=qLe(f{#KZhbVP9vK=~|!mU5#7tb+H zS*qmpWgK1rc?o+?29HLfB0doooHTJGEQ~>lKHPKedyp734iC@nXjzDX1`#41SdYtX z)pymI*Kn<#OmXAAA}{0sU~B2p*BY9i$*He@MyZdjX=6}wY{^W(*_e~ z?;zL_thv?m3vDbVChOyMxLrDMm)6*&H{mI?7Sdp#g)ts`00Ci?iY3zN0H#NP3}$$E zYUEhgh2>ftZmb%J{ndZ};gRya8pdqak3am+>gIghtC%lr1FutYh2T;ZSG-6DVb3Sp zr1|A1V6U9rAWLmK>V-pgr8|o4XcfO|T|jDKbx5f54S6CFw(1$%YcMMd24-5K=7$j=jqJrU#yM0B%T`~S~N<%r0AET^g zh^Ciyv|eeb#(AJL9v0S%x&fgiTXgmW9psXZ32gr=)V7yN!4AE%wioj+-y>w_TRpxA zBo!yUjz|uj88K*+AYj_qRA1ku8salS$A;Phy-W2)e~vXGH5J8DMaDS1hfyvZm?Zzg zWjgGJ%5&!~&Q4=y?(9>Hy3=SjUEb1KgIdAx&{xpe(P0zjE?`SXPKe*dEhR(Y3&E-+ zMH-_?&B@ebJOY@EM!-LKq6_89FKMw`)}Ub~D6J(D%aTgGlk2o5%k331aVxVVh%0$%Cyl`pF0+oPaoiu^Vn$n8~J4?(X8}|C+$?@+qweJjz1NOf_cqyLh=`Q#flpE|3Hi2Ztw#L#Sya zHbKeXY&uc9z_=zmE`lrofqgFOm4K|oVe$*AHx8o|hwWBeFXd4L((%dbLxrMT_&|oB^*t(cr>qy)IyFFfm>XK_;zM4bL&``I2$60enup1U1Kk?0U#nU2P$;vh6gW? zNfz5N05aJwb5)nAmLZ9?W*VbPvJ;S&@2k(hLd62UL5L219(O-5QRM99p0NhkSTn1g-v=;p3H;w_Lv5nrZ6Tt#-uVxv)glrgorNsx5nEYNEaX>#DNsrbUoR^Z{c5N;ll)^INBKnZAH6KErw|e zcZS%yyW0i4x{v}Q164^#CN5%-R>{;LO!BoI=2|KF*MhUxXn&xfU#c$>!3BH~DT*f* zl!``5A~q-z>qTfm$zpBJW)>n}m+7HG@0{YGS|ZI*crGb85EsE*HhX`Ejg8j*iZz}j zn>z&(Q3>MFM*SMd0DTm}7@>`4DUqdwYEyoJsle2Q+<@p{Ei2MxRk16Gs<-6i*s&?E zD?kXihdXyGWzg5HU)-9tc%n?ghRG?EdXP&(DeWhgXkq3_3X_P2m}n1Ppv>(Ew&yT$ zy4zJ~#e;RvM@SAv@$=s6VPr6YhS(T^0LYybymvH)>1|hyX%@1Nnfco7bS-t6yQIR? zo|5#vdVL<&GS#Q?43KT#k}#=yCX0%ZN@kOn0P%rCPN#vPiSIBt91igmtj?*vOeBWD zAc8xip|vDLhO9Z1?ci>-Xg?Bj3r>{cb3v_{Tdl9nA!VdsBZg=+b}cY(q%Q*l%|~8j z;;1M1%Y#LE9`3aYFD8EkGv)3{jz$uVgj}9(W5UnMH%@!4-BB!X!4Hcso{11B+>v#@ zNG`!@E9>#PKwma;YhzN0P3rZIZXHac*lTylF;eu6aM^C5i~=X*iwt|92^`45FbDk> zay2355SiAd?J`w$85fZ8`tW0-l%Id43K8@b>M3~bR=^X>uifI81D+@^DV^$~CWtF)7BH{I zWFTu7Z7exW;Qf+(u}nZ36=21c$P>HQ==CBm)kUu%(FZFBSGjt-T`7YlZEQGN^M{`9 z?y$U|QHCir0J0Pq5i1+Dt(i{H1}PEutY6!1<%caL5Ico9SV_ChEheNL?&_eJ0|wnzh)Uh+i!0z0KR#gc-q>2r)d5TD>V3Jrq2LpUWOxR=;4 z=O>Ef07 zTo%0r|B@h}kQ>G$;7Z0vcdwap=J4aI3xOQHibK=5=<^oFsrxQfr$^g1!2G$ATTN7lbz_hn)F)m zG#duWtSK>J%ls-RG2m;g6SEK$=fgUKo@i85La1gC|B~wU``sN=W>J;9U3sc<*0sy6 zoIKLt&fJ=CgOe@UTt%OTO=_IBc3V58k4zC7x7h-|pyb(Qb+>l`G>BmR0ZS4EcUrZ$ zFq*c2xM>1EEM2u^M7&~8vC#;XMxl|XcS}Z&bC~b)tl4z-U}?3!$8(E{Y7{TAmz+qD zzC0;yQ4_&l6lCOP5v$2grDp_0XoEGxvBJDM26m*I4^y0Wh$A_jf&!|n_aI}~>QcZoL>M~1Wr1D+JZh&ZeIN{*~A zovmJus@z1`txD;PpwygfQ|TP2!XL88T20y48efFsiypXzL84qF=YyWn;=?527il4^6eyF> zKs2>5(-tQGEPcIl%UsQt!zMs0oL9(wH$}32SR#ay;4mq{YcV&j+T)_mDgZO7$DUK) zl3&|YhjxdhqV|ur_1mR1Znc8=`8V8!L~5C=m1t-c)sXz~Ee7mentNS`yeEsJ`ps^siP$U8(BSKX8F<73v zh^~a#L4qwULx#$2U!0qL4xd$1rP`wefT^7kz}9%XRAr5J`biy8g@{VEPKPVt!t|D6 z;cdN^hWe7G93ub%!C3lfY=DTO0fVo~!r=iCW^nPSAv`E}%xEkGWT8!TM?|;+j%Z%8 zPz=e1_6C!5Z1(cMpbX#vXDA6M*e zDM$~-0uakXI+QxAIFoa3?d#{(Y7(V`-%i@7*eJuJTXu7c@NkZaI-%z%nDEpNseL$v z;9gL#x9gjvjYo^(^&?DbY*uL1NbE$lM$`T&nspaWRV)%8+d?n}V7f3rGAs8TK+`Cv zjTRr0a99Ysv0S}FWQ$?D=LJvqQ;`&A0xco_@qHI5n3%Af!Ywj8~~TG*-Hbgk#8bSWcL7@9jYZs3T*wdsN&gS$j1lp#urD6~d&lvEfh`S;rpwrnp z5JKx(==#`7Xd9aiql={t3_?(()lZ`p9eAo^$ogXflS5mg&nd{sp@2$QgFZ3~E}-mz zPttP0KnnTjvv?R?f4+rQF6^OSI-!85WS)>z5x5t6ogGoA92OYw3Ws@|umWfuH*7lK zZZ`@Ob~zXx@+jtq2T7YoQ`kWI;Cr;mWXD1R8W?h?VC;%X1a-7kg^}c=p|PwIlwN%q?g& zRe?Aao8pX7rCmc<0EhSuGtlN);tKtJBy7Dh~s4w5MMiwC2#RsRh#D z>(LutUm4&XDOq(rE=vo@LCW+>xLrO1n@Pr#hw-47%L9XAC!~&&Lc4gKAy=9J1RS?m zju5DLb_ufd)i_d}#f8L4f+d+XdA-O(ai!6VfSAO!L1N>}Q6FG#ybGsTCuO@Kqb7fW zYK&ubfnc`Q;qQ! zy8aw)k8}kmlJ`i|qX!c5Gn&sGxNOxEC6`3JmcWFQg-yoy0;k2Aa92_T6I7(3lff}c z{B~G{IC}vkpadQd;oF0G`6ViHnuy*nQ85wJAUKvhB>tA0`$PqUi->}e%nul2>{5Y3 zJQDkdd~xESMVE#=d`fU5(osz$*7*`#&_UnnkWZCT8DUVN6C#Ooij(*-Yz%k%%lz{Z z&D#z?Clc~4WljtHXE>0dUj#1f2NnjON|fQMC6WnTGS)TK;V)p=gAdoolVmRstU8w{!{uBa}fbJ;9$5H!;4NFk|%t&FG zDZh9ng>gHETuDBPUY>5mV%DGFxnnRb8sliXkgiLyt&ngnz6es4Bx%Z)3?=;Fd-8qH zkS~LPCIk*{-%HzQN7WY1mMvACL(U-f6-7f2K_f=;PU%*;S8fm*+mp>rwHouCsO$oC zmZM*sO_UJLjfGZf7nYY!>@ZB{V)V5Vtx{Z`c06J}aHp^nLZIC$oQXA(%9ZfRQ4&bR zM}znIGUOsrfiG~y62T>y3n>XGLl_p4N4#;Ff_&nv zHfh?VZ;LO+aV1=k5FH+mRbY{ktPFwjnT_aUjiS>oY+{m_-wRy^Q$=xH2s5Hk80ARG zlx9i+)CdSkO^`d*4l*Dg!An!s5pDrF@#A8bgfJ<2x$)L`j$SXFFtlc&NZ=M@fcJPJ zzPLrpusRc+iNO zZwXlGjo}KpY88GW8!MueQVb7Fl17E01+(D|O#=mu#0{fW;%98+azTjDct|l}o}}kP zQ`;3p-R%}yloV?uuMM@yhE?Q>z-*|AJXCKau|4RD!dLqhME+&qn z!m3o?Xvsn(Z4-$hCvilPxavV)I+Y9Bu(N41bDAqPCR+!+`2e8^+Z{6{17Kq`w=R6z z$RBXs?WaE%rLt*T%+dl*l@w`6jG`Bpban)vqgC2=zQwOTa z2R*`D0VQL^|6mh>LcBpwWUm*pI*$7ij`Bo49CS1WBpuTUg*sT@-fsEA6;BTf6&NIi zP$Z;*Z+pE+7yTjk2zjGC&JvzKy^EWObp}8CgXTEeO47o<*Yoz#!>joNjV^^YgID3?m=ykV~a(0)#fGh*$ zq0(HaHJZ^SlBrv9A_mcwVciiO8Y5}O|Np=*qC)~dg1*sM8~1OJWXx&rBk!k~CH{0vbQb7>VRcFi}qW2L=7WGJC@N9t7!auE+Jeq zx0I;HkY$7x2;W6Z3lE6M=&%9}SsxJo1oJ`^l1VtM)S1MOab5*@KP(?APx4uqH9-<8 z?lOU!j}PMCaN|Fb9w#iW93e$evDL`=-&mr?d{6Iu6(CJ`#8-nB0nzU4L9#mjQUpA4 zhFiS77pI#IgOa}hYQCw!#BtXV!%y1IU9zTV0D^)PF{cPR`T)~mF^Nv`onh?gyCVh` zF{rqs&9EXwcxnQ6$|^W%#TrYVWq}an$O|TxN}Q+2TIPn@}!P zOh0rb6$EWRz*=gRJ>mjMmgKJ#QeQ#Y;7=)^TH+`LNicIrnNJ8MTy@Ad0^1kw7=9BY z6M`)g6o_LfgnFX`N*1PgWn#4u9-fSJk zMPpY%)5fkYZmm8MenJ$P>m(nOse@-$c3~Lbg%=N{PL?fY~Fd$@Eq-kKrNa_Q~OB}FsJ_vEX zFd~A_GRQ0>l+jF#?!gC0aYm9habO&!^~BL^Ob3#&PSp>g@o`ql|JD+#FFI^kM01gX zcwuf2F-$yk*jI}?!twZG2D~&%g!Lvs1~w!$C2Ksv0x}5Ul5~TT2nIZxHQENcGRxDX zYr%TbJBG|eQLOz`E&+r>k|vW=Nu3)B;+u$HR#IJ&-zbp>t{g8W65v(9h)OS_xYGcg zs{D?e)pPK=c~+33CsytP{kMt)dDt^@+z8&Ku8Lhz@`3f1TNQo;ND^Ffz>+7^kCxg! z8k1krPdfw@lkYAO{IF-i;fRCbE&Lb?_xhl_e%}bXv&n43XLj6!P7JBv>AXYwDCZE9 zcva#n1+EQ-HI#RcBdxDQ zZW=a$)ix>|bPz{k4I;dVZ1_&OGrWht?&kF&m9PeIFGj&^h+HL>^HE@zLQ6u(kqJV@ z4@;pFCyi1Oz=>Is)vKNn-1S==7oj#|Molzz+>6ALmMRGa#9H4d{+}n5_$2l#%vgly z+hH6CbEc7=F$zdj@1&nej_%o_`klH$U>Y|cXezgii6Rn7Wk-l{6(v! zP(f+r?2<}1VKlizAtx%rj0$c~fKC$kgq}ZC3`2(E2w5~O`N+oxI#Xy?Z6|Z*_!=h* zb$~>hlp6SP;;N)lDYO6-cHw03>cnt4r;3-CVo0%@`F-J}G+e*uN~JIvhX#mz5Rd8{ z;g~Po!YCveAJs+v2el#T$CVVgIb`IAKjgfTkQiRFLV?L*S!jINR6O2d5lagVds2fj ziyuwp54L-GIcPi1hNWJ6YoWHEFEl2ECZ(b^J@eS=ENI7c9j>!?w*a1~-a zC`gXWVyew2*2}3@0YuuggdUksxIt4k-pr-x9*25u7MZ6kZ4RQCL+mGU_U- zFZrwC07mG9TprFX0^ztG$0dt0TOh~pRykN25YKn5?x_}s=PQ(+(Nlm|w ztev}PBt+gLGz{K|Y&ERWe2~VLa7+^m5&9>BCy&I|6vuc$#du8;4B*r-6(zG{1nkJ2 z2m*|RV5K7=*fjue;iCdDf796&KQ3s;iV?i`hGP}8u~(VAjC3K<;rEGKsHTpfFB)jV?FPZS3S&G zgdFrsH_xqt%ieUEoTicjE!P&u_xMbcLGQnE`NYX{h>q@YcklG1Qgk@kRlBw0 zn&WR)ciPk2qfH&*AmdNQ;&&!PA&|>Kl)O?-{k_vk)vI|0uLSlYKjNV+qLd%g3Bv2t z5MhV=B(s$`uLsrPGTiC3&9Bmp_M|~Pn`H_wJd&DbAXs&`d6ZmA!_#RwQ?q{J)LTiN z65>TF3{gyG5#XC3i}}8Q6(rkGan4tcVH490cs0t>jg|l^g90-G)H(~!U72S#9HE~l zjNCdOC`F}A3@kCeJ#HLo&_3cEIsAp3W(>D|`)kRJx6j>3<3-jjjcXADWIsvbTuV+x zeW?X!Ln@E5P1UWjju2ib_~}T+7YDqJ&81EOH!B`491M}kVpc{qb(npGZXuL$VO*+5 zueW)EJ+8Xds-J&#yAm{1uYnT$wBbK#SM$sex}52Rn*C~9;xL2?2HSJ-$(gRWmAj_vq86}@_lpA zI%w)DAidkJ)e`oofp9wHfikM>Oc!s&#|#@!gg?F2DRAY|c&4YL^5&!0NHKAa7g(Q6PW=S*haY=9m!RdzeKxTa`LTq7h*P z&CT(@uD@kv#q}FI4W)QLV3em-a`glvS5^As#y7gmXfLJo+r~)7R%q$8BatgajSB({ zXs0yui-J=wxA4$G3CwEWdfN7=6M|*}S~K6cNA`OpwEq%DVY}a&i^!Zt%8%(YU^gze+ctMGhVAjaGo^!|+q|i$=k+=FVSRSlkRmf8 z6na_BQytV;VQy$)tFht&+_gx)VBcJ({@_N`KHk#>N$DUH=N~ zM?*SQzsTlKRM9eHTn1=Oe#j`?D%(Se25hMK*+RM+W{Dp#coZmXe?qzM*;Xmqq{PF~ zNd2QDsx&#a49v#mVM(chTswqS(j?j$q&%sZo^-3$+-^-#UXl}@fpb35B~qId!f^Dd zAGDiG5S}K}fEk-ZVpQ?L4xw}y5RMMc{_i6g4B#OV$Y@fpU_3Gj6x|8zPu>xZT{sFq zArv0b-oS$p7Bb)t0>amZoo}zM<9ta%;&2Y8;AeyK3}6;wT^9zQI`FOgyj-%AP8i3Y zLgnqc-4^DNuHU_B%}-=<#2ys_kQ!1hM;O*$=X}(bJLxj?NF~#z8K7ied+kF(y^L7O z5Ne%9PZpzsu*CB~tx_7)bl_UmfWaB4PM%9ZuMg@|s^^d7lZKTTI?owKXA47IyfBp# znugUkNoT9KNe4~5t(oWIdJq7v$sxN!7&bMeK-r^eubD%aCzSpguWFiOk9TDmgeAVL zShNEdn~ax67BxzQf|fbhsRC4~=Yc?~jbONTBcoDz1eK;P9-_}CI{->}W z?XEKUjQk)x4Ki%941NzM|6Mbu~DU zuuXB=D@r5gMpNHZ97q=sXwLQq+{~Nd4OrH0ef(p4EU3ne|2FKmWe_v&GRYMJ^w36c zHH4H|Dbmw%7GJ5ySuMk~fcq~Py{d(5ng@UxrSu>-9wjNJX|SuPxJ^0adON5+e)-YI zZhM?e!kdlxx=`zUSIK&}dK|}Jut>@dr5LcJW+S^ATk52TC%Q9TQU!btH0(1;Kq0}$ zdhyS3(&V>RdHo19u5gMm3kY)Vv%);xK>-77r8}%k;eCfVj_Q^nr1uC&z9GA-kAH$~ zr53XcPnZ|V<=Ib`KKT4w1f)ai5ZARPa|$UQ8m&xuCZtBKh(`$vEuT_EE+O!@1V6<) zo`A&SbBYXDoUg8==YlwOYM`G>g9CyG$JoO-%oFc=jLVV6#30&?-o`08!ETX3b6x(>n@=w3JY2Y@eekYA z0_I8-^BpXWsSH|AWzr?(bcUb)%nXdi4CAS58m3_#Ik{$62>`QL)7Dz=H)=}*%jbCH zU5>b>eYfakyvkUg<4@tGwsE&Qw8an84W1_MMd6kHp@$xpb~Q{eg}Ye?w`VN5qdd3{ymG}N}rIqBtyS{8zWOtQEmm%8CzL7m%1z+n2N?Et1hfmXFr6N#y$Bp>Dapw`p?5%bOqL^ z|6#bIWV4(I^N8D=WrNHl9zQT>I)RO(1b@*eKqlUn;0EnU?Im5(-BTNOg{0|w(-#pO zY&%w8VZ&T*2!Azl`q~+%86`Bscw7DX_|~tAUjb=okgpPqh|Lv_x6g*s%Np8Lowz9% z#GRR9OJ8m5NHlewQlj|L=Mt9+j`f~dU%PUy(O8eoaMW5eR(CqMY2J$QyTgVVys(h^ zX02Wt4TW!?0$x!6QY171aW% zfD@XQxl{P)OT1dlVlFMO$?Khz|Nd)5r?m6M(W*RVe|65zrU9R5C%s?^iSJylcjW!s zbGpx#mLa>8S-hY{AEXlDCC41!=&z~18y__VLm^7sAdev(o(?aZptBz{yIzVcjw=7aW;8JfGz9{k*o|@1@S6F>W^A#(1fZC2xy!%NSC%62 z4q)u;3)rg3l6HEAhc52qbxME#pK5<=10)pQq0^Q_h zyNZ;id!>$RB9N;ZYePjd;wc}7zn~Y>B1Ir%VeAEg;mQ#m8)*&is^U`);3DAb-|0S} zkeL@JQKXda$IOBz=m?1;xEh@y@}6=8`|0XvTOKrK1!t1cUy{*Z)^${0DO+tv5lw+GaOOZQ{BU!R4 zin~!})LG9Cu6qxZ#Lvw#Fq$9;_IkN6ktZYrp&y_U3=Jg5so4&j+)3URU& z8K8`MC!U5|I*{h3I7}=@;xZQ+mm4m~XNw*xWD#X!FzCAP9y2h z{Zvy;Whfbru(Z-xZsM-PkUB%MlhVa%E2WBXnuM};FxP_p2m=C4rX5l{isQO%YK5l& z%S39CNz?qW^D^K#m;rXc&rzsNC z&n;G~X{a4dhD*Y4@naz}#&+DzjO)z~m!e5JN2Nzn3`}+Iy+@P&C_yU1gV+w5D zk^jtRF4KT(#Zl|-TBtr2%3C~W7 zi4AJUa%{C){wxQW@VW}`LN)w9Hacr7F{iCbR+D<|Bqwtg*pWJNF0frQ+t!S*ApF4n z{5#JH(Dbi;WnaRgq%B)yo+2vO>WzDy5f@YIG){aiDL-ajq@8rH-4!{<)|3ao3Cu^O zgnq1UgpvOhJ0&0u#q=c%J|3ofCvIW2v%4{e?B19sMyUWdR9hHyo~^dFwARof@%rZL zpSh?o143)5ly)|&VQ*SjdS051Dh@npK$1Y#7}{D4-X8=Po=7|%7#xwjF zw-e<8+}sGZa1&KuwC5qs-|IY?0Dz$1&S)MriDDEKk-eCpN`%IY^0Pe6Lsmj~+;PqE zJ~8V4am!o1%uO2PNmT-2n_f&ur?EIQqF+^Gn?PP|W2@RXDi6qvhGeo^8~1YQ%L!s? zWhG_s+jNmEa;5qB-P7i=9XYp?P-T0T0tuFJ!Z3{ddJe})c8h0H577^l(#qUTV~}a| zBb6c+&8g|UNa44X35om=Rf|-*O7Lvew@p)Ku(TJcK$cmA;4UXiG2c)2WP-e}fynqj zwOXVE!6C=1Zz>SSB}K$Ma=YV3z;ngOO!Iid49z@f z?yaO`k^M|F0zz`5s-M`!-EhoDi*$OCP-ybNAE}Ci5)Zav3B9pcmWUcQP{WvEfxeq30_*^)mR z9mvKX7pG7-YUGI4q}yzqBSz*JPKe?7?%no*j6Fnw1A*K9l62y1kibv|qm-!~WgoUA zxOAp&dV(u-{p0mz0}X|8$pSCxfP$bWF)3`rf$%J`W@1@mPSa~N26if#Qd;(~XvrR` zeU^b(MV-CDk&?+3TvR~uOzG|n!1#~75^MG)Usu7Di!t{NQOzfy0q_wY@n5g!YF)A@lu-m)zkT3hCK@dg>{Ic z_@2b3yja#o;A@kH$y}og(Ez{G%pvwro0AC0;4e{Xx7G9YaV*$wedfIslq?($Ih)ay z)D0e6(Gpg5Fi=J3sEhD|Fw~bO4y0jOPD^dMxEw#9!X^s&nx>XuM(u*_v2EODq3c4l zz`>oz1(6`WTc3(hDCk`d$wWC}#YP+&!2Sghk?M^yFHX)d)xZ3oS=)))g zhEUR1pZfH)x{0m1zzr8nuS0r22JB=Fl1x6YL{r>O>tQa;bOXHP))( z+H`N^E#!|GAfM{j5(T-p0KY9MPC*iqnd>i&_dgr8K`1)&UT{9@0)% zujBUlTWU?oKmxTH(SLyFT!s2)t|1-ZCm{#0Xt!jJljRJDx~vPB^QM`08P<>7v;p`|w>F=A zbQ>m2_Ic_2+ryviU6{GN{H8{bPp$@oPGP&E4hX8`oxDt8=Cf#WuR9%NMscGz>iZYC6wWr%6P2rNuO$y5oyw2XIwLYk1 zTXA=wP`JWyY->vid-GoPtr4=zr{Zt&(Ue95y!{i;om#ev`1B|F z9I%y&`MAt2Bs=tx0(Ke>B)ZA9vt^!S$86^kh)S(GlmDz$zY!qc2@{Ld)ij%rCZ17OG| zr#2xd5L}zX zTB?r(^YVrB7q73pc`}W#DU&bCn-7F!ftmtFO2_hicp3fU?iDQ&PQ&dp_l<>xKTnx7 zozf;QpGoA7N~OY0Hjm`uCQ`Z!e2a8)a4?vrFC6n}_S#iT%Ih*X= zOU<)4mg%0g?wF0##L`PP;eJx}6O#6@Y4H}cpu1Z=CoVXoFh}f4?Xx75MtkIpNRe3F zINuw99BTpwJ2NFq5Nsoz8HBHolmhok*#zsWO3-Llf?wfvqbb?x0p>kbBonI=%&}La zkL1`osB4{ttBp=)i*#mO4Sh}_d#452QV6T9G)gN$B$#7N?mS>cM|m4OVCGX=PNSPb z=%9YIaml0Xt~9i&nNd1xQNv|kc=qKBzxwn~UF)S3l(?$g%6>9nOvK2ACVYp=f@>&2 zG49D?Fb_K46sd8^kV~>Q&#Dnj3+@Zw7+!*a3LMIqbn)tmb1Z5{iN>k!8o#`nWo4$+ zhSfngoGhroxPZ7n>_c7TkTp&+0x>kB!AA8X=4@ z(HFdMY|*!c5S|(>)-*wIJ@ct$i*AzbR!bv!a-bLtU>78uhGDcFl{>q-`09Ut*)zW{eDAHRkx)ajUK-eFN*o^B_U;M= zwXyWG=om@eBCTcc*>qt5yBy7&vE_img*;VjcXf7yw~At=1T&YKcfgKMq`p!eEXEmr zH~~Pc-K*D5KWL!&&fM9qg}<9i!x=x6KjWE8-bPfr)i<hFL2Om?XYc>+ z*s)VT8c6r$Ca$>TD@yz}D})`-j&b~O!*g~r6?+vIBbD?`$r2MCT7MjMYPQiX)V9Qv z6%RqWY+0~J=4A7nS1?ob)~_yH@NB0OSMBgpO!b4BgYL)_4>{o|b^eShrISioHo8wz z+9wGhH*W!=P7VIZH|-49(jOCUrm zM0FbCsY5k~Z`$52!j|9aYdGU+le=ejQVbk)4!QQK^~ujKpC=evmp0&rXCHog@#591 zo?V{q*U~V6>5M1}m4H6yh)X!pRhC)9_}R26?2C?3>!9E`Dt+}y8sZ9oqtrw5*(XRW z3}29s2v`)88#BK}Ivj*&6M55g?RIKMNXIc0N5d&o5U7W5BDvt^KMZE>5e+$s^HNSS z>zq0Ln;-n*rQ<)!*|*J`LxO0MJ9B3|&XzZE2eb^j#m!4(m2N{+HQmIT9ICmsBBkj!4kI;uR-fbMZc=_6ezq)+> zAC?!_&&kv?iIsvw=;zCjF}d3oiBRFujv%@+cfLq2WTPmeuep6&y!%GG z=VGgVIcnv3dlr-*qCf<+#LgbCpSaBZynO8+lx3{(vJEAMy33mOI{ndH$(~Fpd8H55 z(s6;M4k$M(hFHXc*`rYmOGcI(3(@gR5fg?X1Xy_(LF#2(1~J`vXhcaR09X&HGTxc* z&l6LW$R9-@R7Pg1fvxlz@!4~cQn5nnCVp(q_68pB_~8$~_z^5|Ktv#?^cXGbP&Kx7 zJ9Up>Y02naQLUltYvSsSZKpyFe7t6zz(c%CqQ z8}P#{rYwyIlE6V`>65`ebOy?}Bfje?EUBC3g$m3p)z-*aZ-vp~bTg#qd*yc86M24K z7emURwO2$EG>5L%dVrC?XNrJiXM@_aeFwJHTiE{jFYo>0gCBeN*OoZuwn(NBQtc<) zJ@XpXr8sK88mE^m-ImczoP?KEaI%~<5BJ#fMx!QynS~Xg$#YIbnQauvUgMdiiPI(>!HZlAC8to-Bs{NAfA$6C)8^89yrEK2 zbjUaGBN_}ykqB~7(^>X{GkVyZr#U!+Gx&ip0ePQeiQr)`1gaqG%@LBQZSJ56B9VMO zPRQ4U``WO7xO?k^WA{EfzS5hYr$lOvrsKDj7K0bVV^|%5*5X&8(m`Y2RjvPxMYLT= zAu|M#T-rEyt3T}6iLaMQ*M1;>9Dz1BeAtDTPyOohEyeOM4(IR~_{cV4O!F@;n)1}l z9~~@Fi`Nyd2~F4y8_sUBYl4z+Sni$2lAUp_j-+o=fa@-FQ8nHLIRx6hrU#)pWiX2H zI=$KxSpgpRoM@O|Y{`GZiD}kK*veOSYkSpE`KzZU^}6rAaPJo{d^X>kkIW5nn|HJ< z)E3rOzd5~VsT(tpX_!h?gFpn@P`Z+^l?HI^yRT*}whZ6fZ?r@^KyxMFBoobq7F$UH*TLCqw4X@0r2uovL64r#Y z(jf7`_BB1mz7k?+)WdKa3<+dA5ZN$K$J5zadn}%cBx#gm1cp5z7$0@O;Pdi^P z_~}pr1)T_|pyqVjK&w(wGer+Z?q=kQ$6W=2qwBL`nNXfBu>xg?+(MOJ6b`K@Mv93vfA3Y_nI* zYdGpxX+$&&sL+&D)f|;|xAMl;Y<#>^q)uNe%6_feP#jN!Me*4bQ-5~vm)CAZM91d0oDTnDhl8bT;de4A zu?se3U#IaIN3T?a6jRzxt@3f{*Y<4-$%9FRxMH7`_-REmR~SZsUTAZf=q=W7JZwEl z(X#YXlfkVrpivS|*EWjD+*B9b?T&LzX^%lm$b`c$ce6Y}u`yXwVWiSU5Eop!PkoZe z4i(TCH)A3~}U597lQr_7^}KLsQ$XPORRnH+RIgSf4~F;9Efm!f_w&o9Bt7=6o@MCI)EfZ~xQqoA zXCOs5YG7$6jKC^-J{KzEW|Lh~i4^s^+Rr(7Vd89nik=@cPgINfKxOGe{kGzgBz`+~ znlc@0PD&kHEAd1m&r=%@ay=}pG$|GMl77bM2wc~(0(c^F+JS9$E%p%b!9QtrS;s+p zH2Gu)+*rB|ro;wj5V?G}WK)I8DGs5SRI#T5^k_{KC|LPWOrclvQLSHl=h|n(`LW*o zus1Q%ox_+V5G4LHNyGX%`DY=Kz7zX`zZ;f`$+%y?YZT8-l*KFIVQt1nZ^3anAV!{q zcTgSdQ-6+?q%ugKoj&_H6i|D6fiat1P6D1xdNbCO!%h)^jYV|=hm${HP*w5J-^s!y z6JliQgE6-Zz-AXi0G0MW>%vNY$w$z+)zP!gR=4l;4nA@}`m$1(0Ft6TWh9mFoQdxN zHci_ci$n69D@BR*Yj1zN!1V}Nz1^*;JmS!0tQQe!C*y*Wj&KJ$qUP!qXToLlsz{NGA8M*wRl7M2*dvu!m<{3xg6xI~S$ z&4C^*zx%F|KW4|ZiAshxsQ!f3=xXbA-o1ADv-=|(;$=v&x0VbctVp>2lnV^ZX(&z| zQyu?+_5hlq`_YC7K4czL{Iz%Nt2{59d~)5IA> z56^n!R0%stk#=fhTV8)(@wM^A!{n5HTyH)+Gh1u7PG30nE+L2-^LxF+F_~z7?%&g? zFnQ0lgyfjXb`f@Np?UT5bYa;HDp$O%P9OME@asgc`RT_|U@qJHh)2kULbvR-k4C?v z3@gomMlK;)#su#R&F=Srj#NGb?vnYG`?Zfq2BWpX(Z^~yQydMDW1S&qosOmQ%my#4 zmh;Gbc$tEIO_{{2`yQ2au-x-P>F44~#cmR!3yJ5?geD$uUt)FuF`8zNPCvr~6U3eF z`=|cmrSqS47lQIsv5J7;F|v01 zzOGpXZKWUUwJ&Dxo!gQW7$UOHhSO)=rf^R*Wrc1l5X76b&$XfoDngQ>&XAW)Ih-Bd zP%rf~f|PKh#xo^gLN)!u*&X%Gku>+)O9-bplB%Z@sQoKf#oHznh=E}P6S$*QA=9l< z^OG@2nN?C5Ppj0gY3iAR#=cse8s=MF!+Pltde}OSc4%mn(^H|yV9nCqbPSZu@d9UD#AucRSyT(jjNOE$O{!SA*0kb+oh^1VbR$&XmkF>Ji={uBHIf zXlguB&~S#CeLC{;GG}}5zwwKoetgSA>j`LW*qir5BOC}Wgd#{e=GjCL1qM$!s#BnJ zoy)VWjxR;NG;ihJzT>dFwW_sLU#%9zrudwq0 zC(|a9a=YQdFyv8&g8wr5!3-SQj(4+1BFg$$ill19^E;uBqh*Oex&ys$8X=+p3vf}K zR;gErA&oQ;CH(POfe5wgKR&|1jrN9VHQDN37|lTVFpt_JhB>@Z)Z; z+jS(XS><9&n8QjaA*#H|X;0cnX0@;GW+|^tPxYDfa7FWN{y~Wd5%m)%RkfpvoKu8_ z+2CgT3_rBf_|6UX#%KT>8w2eb&(f%p97Z)<&~6RF<$<8yU>(7K7PTQI6*yPpmMlSy z!bDJ#hK)xFSiY>=$tQfReB=)=6shkkg3h>?n2>olJ$b93Ik7d(3=kJhQhhm6N}SXs znpG5Rn66|0E2at2kZ^To`@%2(@|`c<#c*wH^~1$+?a^e;D-SVq24(L`rcN!NMg3+` z>E{}fXUe8tcbm4oJQnz|rDvB&i_*|6UaRV?T$hIGUiI!lpl9>L@u_YGEt7m!HhmpY z$VL*;d5vhG?KE-GV?nIe;!~mC&V$Y~gt1Xlph&6{nz^(e5Ok^al{F3dIh6XSONG(K z&=UAT{meh7&qKbx=sJ*m#R|#B<)z=k|A*DT8TrZL(n}(d#GbLbpy zvCmmFTFB+()RRmop6M%cDtsteK{Q7mIl|->8^w#=)6i>nfo%6Y_^JphD!?d$7)pY~ zR6~4EGJt0FJr@E(|M6(FfmJdc*3PZ5!lw-Y@1?02foO`+<^!i$%< z7e6WG;nqFma66;}f9qAr3nfBMF6e)`TwU;h1v?|gAnoE3Zv&MegodK5l>yhN(~i!0j6%YAu_sTqjUDxGIy z#rWdWncU+Ww_m$*dm}!zLkc-k{DRx?)WNE6MEXg?&xUJ$u@tLxi6Pq?d~> zUYI+Agj^_0?QWo%7 zm{rj=k>^bMp9thMc==%22JPkpQ?BNuR=;z3_HQp;dd(AV->bJIoq^981aZ(_tZjS! zpdAKVOT1kTW|uuqL|I-%mD7&H;@t~37+wV$s)YpJbV`HGv9eSSe5D`zQh+K#^5Sm8 z|KdN8k7MELCpzvm>CiNsrm_S12|l`ar(@ZRH|*Z>=)NTI6b?l=k(eS~qemk)de}RW zPr2kwVM!)`{l)X^$6xyUFMsgmmmmG~`Rq%!_6lK~9`P+fAaw!q3_Epp{oJDXCUqM& z4qeR_jAbo_AHSAr#f~RF<_Y$VXRo~W+KCtc^!1X3g)6sI;0RZny>`G#=@4k$cu=3e zT8cag$3*G9(t?yEEy24#PW8*07JxU0ixiTm_ZjWf$eTe7Y2o)xOptklG}T0iSl<>C zEGS=Rf~J5(F&bLU(h*UMG}ro2_30Q_;G|cDl-K;@jZSr-LS`xG8<{7Ayj`^#f> zPp>?R@%B}Jki-swsOn|_DvZ{+R-_#MA&>IHi)T!q8LQMQ`tf;kv%p}7&B zTMO@*L)SMI32Y;xIljiLC75(5%Q&Q`tKgBo zNbA4;AY@4KWGu%As1wcMu0|dx5u31V!(RW9$6p@*;)~&Lre63zKX~cXO(S96aK|ML zI!}coCL9d9vG;AL0p(oWaZ=gC9gZ0PfUscDh$w4!BA2X48X^mXP-Y8QGh9w4aj@ewAZ3WeFP{%MbYz}JQ&?>Mp}E2o zG&1;?bz}L){pSANVN^l&(pCiogkmbnR^(k&92g4?G#%ApwhwFnexyVBe-4TVOE8K@ zv94~dC*fFMUt8-9-aY>O{!=e}aQ=%c&5?|t)zrn4hmENK-guDNPF;j!9;RjNHS41| zkW_D4^XY2|on>FX2P>*}ib?k4KYFx;MxWM<2lXbaEGwCw4!Qvj0t~qvQq9#+A_7Pn zIZ97;?RPvd=n*Fvu9=eXC-_3g$aG89f>v?VMgJ(f%jMrjZaO)(C-@jbXzg3n7MjeK zOBOo=oPDSO)}AZ>^6#Xv`J~w3uN2u60oNCHwasDAy2r`t`CFe{_=}${O4p-AtSoPp zJric=?s&ZL5+)uApYF~Bc|2Qr#zvw!y9k^&ZIPYKB1)QbWR+=sRMVpH1q|M}p*0}{ zevxJLxV_t)Q=U@6ba*Z(KquYlp|Ls>^Xx0F7EvF6(~k6cJV=G2)6@1hE`H@3OJ))o z#ZL6hpD#%_aFue@6!X2)s^xjvh44t~MMB^5Mx_Qt zQpM$rOpDtmBdIvIfHNCIfOPkI2j<`Z;QaPyr#pRD(EG}n8Kf&mzzFm+U#+KjQ#|Xi zv7jec*C7ZbLoY4vmW*ijYNCl|?#%hUv-lCI1hVNej7RTXT}GB_EzlmjGGG`eEf>)*RcMB(@Q5)Kd18Idnr_SBhy9F=7ChU`azXk{AO-XA(>kk7tzEw!Eh?AF{MvnWh}F;lY;4LZLc=&EzP;b zJI7g%r71yKb!#og>PO-7!1QFGs;79y?$|)vz|@WPn$IM5j;I4iIF=!inx)rI9xW>?+n(p$y6d6h zmtOqTV;`4qbeKlVvk*A}Vk@i(v3i7C$m8fl9;L`FQ4Y~9T!8FYp=g~vfYoO2^%pHG}1U5yC9W?wls5`N8N?}r^1_>JgLv)1oc>0E$VUfHx(;HrF1`rQO<~T!u=mdfjHvy zv&%22w2w@hW{9-$%B4#lWq-n$t8{h)Mq1DLI0T;8X*uuWPm5s%5F#*?jB}pJQuB&w z1WBefL8e>7wTbwxk%3Ru34QI!Vx zf`KCHM0x`9MhW)}jc{fpxNa7GQ=`=}?@Uq&OBt3dKGpt_iR1v;|Jb_z-o+E2zIE}g z1+%?Wbt(Lj)mTvp_NWBEh%Lx2naOcTDWwn(y<58U+ND>PUwrM=Wlw{@-P=U=C2fh1 zWlE`t7e`ulLlQ=u4C5js?pya(s}?EnFJ7^ zBdDXc!{PL6$HQreR1ri(K<)fQ`AigcgHR&3-k>{agtgU)o60&|s4~HaGzkZ; zJm!gQ6wQ3X>ld%Qb>*$Mu6+95qCu`QM>Xynvu+?I!0kJTcD!PSTVZerR?S6;(IF_;p*LfxQK#+RYcM#xaa)SIi&hSXBbhOHuqal3VAb{xa=%g zOjhA}J+^>J>D{mCq98JvmWwe|*5PFKaWb^YpF6b~_$KBeS36u}^C$=N+Kg(i^Axe5 zHKL=Oj)QL}$@Mu02j}=Kn~kwL?TpPlE=t9CTW6M*fBx#HuYUTLp@^FrOs#kKjipE< zV^=OJo>`T=TyEJJ!7&$GOB2-p9=h^@?XmUCFub$sH}6My^aTd%%4eCz1N zt2;5zfTvQ*ag4h?ot4o&`dEzAt9An9WB@)BtKM}}gW9xDdQw1A?NAa1!^@8e87Avg zH36Z0B29TIaoCOc*7yc*m6a|7WV)M=v&hkh^4WS&I}*1Lgqu5!Z6%11>@1w5@5Ncy z9@J1FT2X4=G9m$z*6!CP=na4MP;nrJ1)`U4bD_*PFQ4-oB-m%K_=93jdSGiieF;gg zrs*x;RIY1ZxxU;^Ud2dxu5|PJ-{0zQDtB#`y#GP!wyE)JC||ql_d1{Dnhi^pPoW(0 ScXhuu?JhGvFc3X!wf_qx)(52k literal 0 HcmV?d00001 diff --git a/tests/fixtures/INVALID_CHUNK_ID.LBM b/tests/fixtures/INVALID_CHUNK_ID.LBM new file mode 100644 index 0000000000000000000000000000000000000000..0553bd6b48a6532b546a6b06cf34708d58be8a39 GIT binary patch literal 7078 zcmeI0F^C&S6o&u36zd|$DPmGwVZjzIrciN(3mF{W9JvH>maqaMY_S0qG8iJl7Ajn9 zj0zD{C~!pz;Y{J;3Khnv*h0k?R}5Ii7Ajn9K!FQv;o=O&T;bx+tS%QMVnd2t2+8cQ z^XARIo&D$I&A`rQckUek+dnmS56Zg-pYH+qc;1)h=36206tI$q>-0poXw~dqliI3DY+VilYnE%p~r{F zyGYxJTTpe7895^r8S!nvGs!cLXUH?88u3#og~0$}7k&#;V!=RD#`K;LhdKHwdNJAx z8MA~lMb&52W*{j&dNAtOCg9HDCUB>4kKm5s4&g>{L%1EdKHNH7X~tPsLrHq1QHeZ9 znj(%b1cmT@BPd8Hr}QR-B1So)6x|qsDljA~41EUe6^0vxyD8`|5Ph97pD|NhM4Sgq zBx8>gaE!cD2)x=;Q;Z67gavdiF{xN|QP7F4(1FlHp<^-8BGRI% zD?8*HtA!?_PQpW>D2y^3$mlkb1O*Mr{IHj8ZI zdWSFX-Q71aSI*-;y1wa`E1Uk;-A(@owdtFMvoYSk-~aN9yI&c?AEJR<&q^r0DL*g0 z*}5$Itn_yjqs}oZaane^zGs+R`}I-jqM-D+eBORudTi)e?@`H;uvzf`%!1=GoS*G} z@DJ?(7kjX^mVhN-30MM_fF)oFSOS*7e}{lMQ*h2_WB#kn$%ge-|CW}->I}-}gk2AI JJzU1F_#2*S11SIi literal 0 HcmV?d00001 diff --git a/tests/fixtures/INVALID_CHUNK_LENGTH.LBM b/tests/fixtures/INVALID_CHUNK_LENGTH.LBM new file mode 100644 index 0000000000000000000000000000000000000000..275a58c503a9c79440925cd97af057e53151c03a GIT binary patch literal 7020 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

g=5c{4ISIH6fdVmV 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 + + 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. + + */ + +import { expect, test } from "vitest"; + +import PBM from "../src/pbm.js"; + +test("Successfully parse a PBM file", () => { + const fs = require("fs"); + const data = fs.readFileSync("./tests/fixtures/VALID.LBM"); + + expect(() => { + new PBM(data.buffer); + }).not.toThrowError(); +}); + +test("Fail to parse", () => { + const fs = require("fs"); + const data = fs.readFileSync("./tests/fixtures/ASH.LBM"); + + expect(() => { + new PBM(data.buffer); + }).toThrowError(/^Failed to parse file.$/); +}); + +test("Fail to parse a PBM file with an invalid chunk id", () => { + const fs = require("fs"); + const data = fs.readFileSync("./tests/fixtures/INVALID_CHUNK_ID.LBM"); + + expect(() => { + new PBM(data.buffer); + }).toThrowError(/^Invalid chunkID: "FARM" at byte 12. Expected "FORM".$/); +}); + +test("Fail to parse a PBM file with an invalid chunk length", () => { + const fs = require("fs"); + const data = fs.readFileSync("./tests/fixtures/INVALID_CHUNK_LENGTH.LBM"); + + expect(() => { + new PBM(data.buffer); + }).toThrowError(/^Invalid chunk length: 7070 bytes. Expected 7012 bytes.$/); +}); + +test("Fail to parse an IFF file that is not a PBM file", () => { + const fs = require("fs"); + const data = fs.readFileSync("./tests/fixtures/SEASCAPE.LBM"); + + expect(() => { + new PBM(data.buffer); + }).toThrowError(/^Invalid formatID: "ILBM". Expected "PBM ".$/); +}); + +test("Parse a PBM bitmap header", () => { + const fs = require("fs"); + const data = fs.readFileSync("./tests/fixtures/VALID.LBM"); + const image = new PBM(data.buffer); + + expect(image.width).toStrictEqual(640); + expect(image.height).toStrictEqual(480); + expect(image.size).toStrictEqual(307_200); + expect(image.xOrigin).toStrictEqual(0); + expect(image.yOrigin).toStrictEqual(0); + expect(image.numPlanes).toStrictEqual(8); + expect(image.mask).toStrictEqual(0); + expect(image.compression).toStrictEqual(1); + expect(image.transClr).toStrictEqual(255); + expect(image.xAspect).toStrictEqual(1); + expect(image.yAspect).toStrictEqual(1); + expect(image.pageWidth).toStrictEqual(640); + expect(image.pageHeight).toStrictEqual(480); +}); + +test("Parse PBM palette information", () => { + const fs = require("fs"); + const data = fs.readFileSync("./tests/fixtures/VALID.LBM"); + const image = new PBM(data.buffer); + + expect(image.palette.length).toStrictEqual(256); + expect(image.palette[10]).toStrictEqual([87, 255, 87]); +}); + +test("Parse PBM color cycling information", () => { + const fs = require("fs"); + const data = fs.readFileSync("./tests/fixtures/VALID.LBM"); + const image = new PBM(data.buffer); + + expect(image.cyclingRanges.length).toStrictEqual(16); +}); + +test("Parse PBM thumbnail", () => { + const fs = require("fs"); + const data = fs.readFileSync("./tests/fixtures/VALID.LBM"); + const image = new PBM(data.buffer); + + expect(image.thumbnail.width).toStrictEqual(80); + expect(image.thumbnail.height).toStrictEqual(60); + expect(image.thumbnail.size).toStrictEqual(4800); +}); + +test("Decode PBM thumbnail pixel data", () => { + const fs = require("fs"); + const data = fs.readFileSync("./tests/fixtures/VALID.LBM"); + const image = new PBM(data.buffer); + + expect(image.thumbnail.pixelData.length).toStrictEqual(4800); + // FIXME(m): Verify these values are correct in the test image thumbnail: + expect(image.thumbnail.pixelData[0]).toStrictEqual(14); + expect(image.palette[14]).toStrictEqual([255, 255, 87]); +}); + +test("Decode PBM image pixel data", () => { + const fs = require("fs"); + const data = fs.readFileSync("./tests/fixtures/VALID.LBM"); + const image = new PBM(data.buffer); + + expect(image.pixelData.length).toStrictEqual(307_200); + // FIXME(m): Verify these values are correct in the test image: + expect(image.pixelData[0]).toStrictEqual(14); + expect(image.palette[14]).toStrictEqual([255, 255, 87]); +});