commit d9ce760d191c8a5fd91504e023f5719549e7ba7c Author: Michael Smith Date: Thu May 11 23:39:28 2023 +0200 Initial commit 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 0000000..8e5ff8f Binary files /dev/null and b/assets/TEST.LBM differ diff --git a/docs/IFF.asc b/docs/IFF.asc new file mode 100644 index 0000000..1ea20dc --- /dev/null +++ b/docs/IFF.asc @@ -0,0 +1,1424 @@ +NOTE(m): Copied from https://github.com/svanderburg/libiff/blob/master/doc/IFF.asc + +"EA IFF 85" Standard for Interchange Format Files + +Document Date: January 14, 1985 +From: Jerry Morrison, Electronic Arts +Status of Standard: Released and in use + +1. Introduction + +Standards are Good for Software Developers + +As home computer hardware evolves to better and better media machines, +the demand increases for higher quality, more detailed data. Data +development gets more expensive, requires more expertise and better +tools, and has to be shared across projects. Think about several ports +of a product on one CD-ROM with 500M Bytes of common data! + +Development tools need standard interchange file formats. Imagine +scanning in images of "player" shapes, moving them to a paint program +for editing, then incorporating them into a game. Or writing a theme +song with a Macintosh score editor and incorporating it into an Amiga +game. The data must at times be transformed, clipped, filled out, +and moved across machine kinds. Media projects will depend on data +transfer from graphic, music, sound effect, animation, and script +tools. + +Standards are Good for Software Users + +Customers should be able to move their own data between independently +developed software products. And they should be able to buy data libraries +usable across many such products. The types of data objects to exchange +are open-ended and include plain and formatted text, raster and structured +graphics, fonts, music, sound effects, musical instrument descriptions, +and animation. + +The problem with expedient file formats typically memory dumps is +that they're too provincial. By designing data for one particular +use (e.g. a screen snapshot), they preclude future expansion (would +you like a full page picture? a multi-page document?). In neglecting +the possibility that other programs might read their data, they fail +to save contextual information (how many bit planes? what resolution?). +Ignoring that other programs might create such files, they're intolerant +of extra data (texture palette for a picture editor), missing data +(no color map), or minor variations (smaller image). In practice, +a filed representation should rarely mirror an in-memory representation. +The former should be designed for longevity; the latter to optimize +the manipulations of a particular program. The same filed data will +be read into different memory formats by different programs. + +The IFF philosophy: "A little behind-the-scenes conversion when programs +read and write files is far better than NxM explicit conversion utilities +for highly specialized formats." + +So we need some standardization for data interchange among development +tools and products. The more developers that adopt a standard, the +better for all of us and our customers. + +Here is "EA IFF 1985" + +Here is our offering: Electronic Arts' IFF standard for Interchange +File Format. The full name is "EA IFF 1985". Alternatives and justifications +are included for certain choices. Public domain subroutine packages +and utility programs are available to make it easy to write and use +IFF-compatible programs. + +Part 1 introduces the standard. Part 2 presents its requirements and +background. Parts 3, 4, and 5 define the primitive data types, FORMs, +and LISTs, respectively, and how to define new high level types. Part +6 specifies the top level file structure. Appendix A is included for +quick reference and Appendix B names the committee responsible for +this standard. + +References + +American National Standard Additional Control Codes for Use with ASCII, +ANSI standard 3.64-1979 for an 8-bit character set. See also ISO standard +2022 and ISO/DIS standard 6429.2. + +Amiga[tm] is a trademark of Commodore-Amiga, Inc. + +C, A Reference Manual, Samuel P. Harbison and Guy L. Steele Jr., Tartan +Laboratories. Prentice-Hall, Englewood Cliffs, NJ, 1984. + +Compiler Construction, An Advanced Course, edited by F. L. Bauer and +J. Eickel (Springer-Verlag, 1976). This book is one of many sources +for information on recursive descent parsing. + +DIF Technical Specification (c)1981 by Software Arts, Inc. DIF[tm] is +the format for spreadsheet data interchange developed by Software +Arts, Inc. +DIF[tm] is a trademark of Software Arts, Inc. + +Electronic Arts[tm] is a trademark of Electronic Arts. + +"FTXT" IFF Formatted Text, from Electronic Arts. IFF supplement document +for a text format. + +Inside Macintosh (c) 1982, 1983, 1984, 1985 Apple Computer, Inc., a +programmer's reference manual. +Apple(R) is a trademark of Apple Computer, Inc. +Macintosh[tm] is a trademark licensed to Apple Computer, Inc. + +"ILBM" IFF Interleaved Bitmap, from Electronic Arts. IFF supplement +document for a raster image format. + +M68000 16/32-Bit Microprocessor Programmer's Reference Manual(c) 1984, +1982, 1980, 1979 by Motorola, Inc. + +PostScript Language Manual (c) 1984 Adobe Systems Incorporated. +PostScript[tm] is a trademark of Adobe Systems, Inc. +Times and Helvetica(R) are trademarks of Allied Corporation. + +InterScript: A Proposal for a Standard for the Interchange of Editable +Documents (c)1984 Xerox Corporation. +Introduction to InterScript (c) 1985 Xerox Corporation. + + + +2. Background for Designers + +Part 2 is about the background, requirements, and goals for the standard. +It's geared for people who want to design new types of IFF objects. +People just interested in using the standard may wish to skip this +part. + +What Do We Need? + +A standard should be long on prescription and short on overhead. It +should give lots of rules for designing programs and data files for +synergy. But neither the programs nor the files should cost too much +more than the expedient variety. While we're looking to a future with +CD-ROMs and perpendicular recording, the standard must work well on +floppy disks. + +For program portability, simplicity, and efficiency, formats should +be designed with more than one implementation style in mind. (In practice, +pure stream I/O is adequate although random access makes it easier +to write files.) It ought to be possible to read one of many objects +in a file without scanning all the preceding data. Some programs need +to read and play out their data in real time, so we need good compromises +between generality and efficiency. + +As much as we need standards, they can't hold up product schedules. +So we also need a kind of decentralized extensibility where any software +developer can define and refine new object types without some "standards +authority" in the loop. Developers must be able to extend existing +formats in a forward- and backward-compatible way. A central repository +for design information and example programs can help us take full +advantage of the standard. + +For convenience, data formats should heed the restrictions of various +processors and environments. E.g. word-alignment greatly helps 68000 +access at insignificant cost to 8088 programs. + +Other goals include the ability to share common elements over a list +of objects and the ability to construct composite objects containing +other data objects with structural information like directories. + +And finally, "Simple things should be simple and complex things should +be possible." Alan Kay. + +Think Ahead + +Let's think ahead and build programs that read and write files for +each other and for programs yet to be designed. Build data formats +to last for future computers so long as the overhead is acceptable. +This extends the usefulness and life of today's programs and data. + +To maximize interconnectivity, the standard file structure and the +specific object formats must all be general and extensible. Think +ahead when designing an object. It should serve many purposes and +allow many programs to store and read back all the information they +need; even squeeze in custom data. Then a programmer can store the +available data and is encouraged to include fixed contextual details. +Recipient programs can read the needed parts, skip unrecognized stuff, +default missing data, and use the stored context to help transform +the data as needed. + +Scope + +IFF addresses these needs by defining a standard file structure, some +initial data object types, ways to define new types, and rules for +accessing these files. We can accomplish a great deal by writing programs +according to this standard, but don't expect direct compatibility +with existing software. We'll need conversion programs to bridge the +gap from the old world. + +IFF is geared for computers that readily process information in 8-bit +bytes. It assumes a "physical layer" of data storage and transmission +that reliably maintains "files" as strings of 8-bit bytes. The standard +treats a "file" as a container of data bytes and is independent of +how to find a file and whether it has a byte count. + +This standard does not by itself implement a clipboard for cutting +and pasting data between programs. A clipboard needs software to mediate +access, to maintain a "contents version number" so programs can detect +updates, and to manage the data in "virtual memory". + +Data Abstraction + +The basic problem is how to represent information in a way that's +program-independent, compiler- independent, machine-independent, and +device-independent. + +The computer science approach is "data abstraction", also known as +"objects", "actors", and "abstract data types". A data abstraction +has a "concrete representation" (its storage format), an "abstract +representation" (its capabilities and uses), and access procedures +that isolate all the calling software from the concrete representation. +Only the access procedures touch the data storage. Hiding mutable +details behind an interface is called "information hiding". What data +abstraction does is abstract from details of implementing the object, +namely the selected storage representation and algorithms for manipulating +it. + +The power of this approach is modularity. By adjusting the access +procedures we can extend and restructure the data without impacting +the interface or its callers. Conversely, we can extend and restructure +the interface and callers without making existing data obsolete. It's +great for interchange! + +But we seem to need the opposite: fixed file formats for all programs +to access. Actually, we could file data abstractions ("filed objects") +by storing the data and access procedures together. We'd have to encode +the access procedures in a standard machine-independent programming +language la PostScript. Even still, the interface can't evolve freely +since we can't update all copies of the access procedures. So we'll +have to design our abstract representations for limited evolution +and occasional revolution (conversion). + +In any case, today's microcomputers can't practically store data abstractions. +They can do the next best thing: store arbitrary types of data in +"data chunks", each with a type identifier and a length count. The +type identifier is a reference by name to the access procedures (any +local implementation). The length count enables storage-level object +operations like "copy" and "skip to next" independent of object type. + +Chunk writing is straightforward. Chunk reading requires a trivial +parser to scan each chunk and dispatch to the proper access/conversion +procedure. Reading chunks nested inside other chunks requires recursion, +but no lookahead or backup. + +That's the main idea of IFF. There are, of course, a few other detailsI + +Previous Work + +Where our needs are similar, we borrow from existing standards. + +Our basic need to move data between independently developed programs +is similar to that addressed by the Apple Macintosh desk scrap or +"clipboard" [Inside Macintosh chapter "Scrap Manager"]. The Scrap +Manager works closely with the Resource Manager, a handy filer and +swapper for data objects (text strings, dialog window templates, pictures, +fontsI) including types yet to be designed [Inside Macintosh chapter +"Resource Manager"]. The Resource Manager is a kin to Smalltalk's +object swapper. + +We will probably write a Macintosh desk accessory that converts IFF +files to and from the Macintosh clipboard for quick and easy interchange +with programs like MacPaint and Resource Mover. + +Macintosh uses a simple and elegant scheme of 4-character "identifiers" +to identify resource types, clipboard format types, file types, and +file creator programs. Alternatives are unique ID numbers assigned +by a central authority or by hierarchical authorities, unique ID numbers +generated by algorithm, other fixed length character strings, and +variable length strings. Character string identifiers double as readable +signposts in data files and programs. The choice of 4 characters is +a good tradeoff between storage space, fetch/compare/store time, and +name space size. We'll honor Apple's designers by adopting this scheme. + +"PICT" is a good example of a standard structured graphics format +(including raster images) and its many uses [Inside Macintosh chapter +"QuickDraw"]. Macintosh provides QuickDraw routines in ROM to create, +manipulate, and display PICTs. Any application can create a PICT by +simply asking QuickDraw to record a sequence of drawing commands. +Since it's just as easy to ask QuickDraw to render a PICT to a screen +or a printer, it's very effective to pass them between programs, say +from an illustrator to a word processor. An important feature is the +ability to store "comments" in a PICT which QuickDraw will ignore. +Actually, it passes them to your optional custom "comment handler". + +PostScript, Adobe's print file standard, is a more general way to +represent any print image (which is a specification for putting marks +on paper) [PostScript Language Manual]. In fact, PostScript is a full-fledged +programming language. To interpret a PostScript program is to render +a document on a raster output device. The language is defined in layers: +a lexical layer of identifiers, constants, and operators; a layer +of reverse polish semantics including scope rules and a way to define +new subroutines; and a printing-specific layer of built-in identifiers +and operators for rendering graphic images. It is clearly a powerful +(Turing equivalent) image definition language. PICT and a subset of +PostScript are candidates for structured graphics standards. + +A PostScript document can be printed on any raster output device (including +a display) but cannot generally be edited. That's because the original +flexibility and constraints have been discarded. Besides, a PostScript +program may use arbitrary computation to supply parameters like placement +and size to each operator. A QuickDraw PICT, in comparison, is a more +restricted format of graphic primitives parameterized by constants. +So a PICT can be edited at the level of the primitives, e.g. move +or thicken a line. It cannot be edited at the higher level of, say, +the bar chart data which generated the picture. + +PostScript has another limitation: Not all kinds of data amount to +marks on paper. A musical instrument description is one example. PostScript +is just not geared for such uses. + +"DIF" is another example of data being stored in a general format +usable by future programs [DIF Technical Specification]. DIF is a +format for spreadsheet data interchange. DIF and PostScript are both +expressed in plain ASCII text files. This is very handy for printing, +debugging, experimenting, and transmitting across modems. It can have +substantial cost in compaction and read/write work, depending on use. +We won't store IFF files this way but we could define an ASCII alternate +representation with a converter program. + +InterScript is Xerox' standard for interchange of editable documents +[Introduction to InterScript]. It approaches a harder problem: How +to represent editable word processor documents that may contain formatted +text, pictures, cross-references like figure numbers, and even highly +specialized objects like mathematical equations? InterScript aims +to define one standard representation for each kind of information. +Each InterScript-compatible editor is supposed to preserve the objects +it doesn't understand and even maintain nested cross-references. So +a simple word processor would let you edit the text of a fancy document +without discarding the equations or disrupting the equation numbers. + +Our task is similarly to store high level information and preserve +as much content as practical while moving it between programs. But +we need to span a larger universe of data types and cannot expect +to centrally define them all. Fortunately, we don't need to make programs +preserve information that they don't understand. And for better or +worse, we don't have to tackle general-purpose cross-references yet. + + + +3. Primitive Data Types + +Atomic components such as integers and characters that are interpretable +directly by the CPU are specified in one format for all processors. +We chose a format that's most convenient for the Motorola MC68000 +processor [M68000 16/32-Bit Microprocessor Programmer's Reference +Manual]. + +N.B.: Part 3 dictates the format for "primitive" data types where and +only where used in the overall file structure and standard kinds of +chunks (Cf. Chunks). The number of such occurrences will be small +enough that the costs of conversion, storage, and management of processor- +specific files would far exceed the costs of conversion during I/O by "foreign" +programs. A particular data chunk may be specified with a different +format for its internal primitive types or with processor- or environment- +speci fic variants if necessary to optimize local usage. Since that hurts +data interchange, it's not recommended. (Cf. Designing New Data Sections, +in Part 4.) + +Alignment + +All data objects larger than a byte are aligned on even byte addresses +relative to the start of the file. This may require padding. Pad bytes +are to be written as zeros, but don't count on that when reading. + +This means that every odd-length "chunk" (see below) must be padded +so that the next one will fall on an even boundary. Also, designers +of structures to be stored in chunks should include pad fields where +needed to align every field larger than a byte. Zeros should be stored +in all the pad bytes. + +Justification: Even-alignment causes a little extra work for files +that are used only on certain processors but allows 68000 programs +to construct and scan the data in memory and do block I/O. You just +add an occasional pad field to data structures that you're going to +block read/write or else stream read/write an extra byte. And the +same source code works on all processors. Unspecified alignment, on +the other hand, would force 68000 programs to (dis)assemble word and +long-word data one byte at a time. Pretty cumbersome in a high level +language. And if you don't conditionally compile that out for other +processors, you won't gain anything. + +Numbers + +Numeric types supported are two's complement binary integers in the +format used by the MC68000 processor high byte first, high word first the +reverse of 8088 and 6502 format. They could potentially include signed +and unsigned 8, 16, and 32 bit integers but the standard only uses +the following: + +UBYTE 8 bits unsigned +WORD 16 bits signed +UWORD 16 bits unsigned +LONG 32 bits signed + +The actual type definitions depend on the CPU and the compiler. In +this document, we'll express data type definitions in the C programming +language. [See C, A Reference Manual.] 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 */ + +Characters + +The following character set is assumed wherever characters are used, +e.g. in text strings, IDs, and TEXT chunks (see below). + +Characters are encoded in 8-bit ASCII. Characters in the range NUL +(hex 0) through DEL (hex 7F) are well defined by the 7-bit ASCII standard. +IFF uses the graphic group RJS (SP, hex 20) through R~S (hex 7E). + +Most of the control character group hex 01 through hex 1F have no +standard meaning in IFF. The control character LF (hex 0A) is defined +as a "newline" character. It denotes an intentional line break, that +is, a paragraph or line terminator. (There is no way to store an automatic +line break. That is strictly a function of the margins in the environment +the text is placed.) The control character ESC (hex 1B) is a reserved +escape character under the rules of ANSI standard 3.64-1979 American +National Standard Additional Control Codes for Use with ASCII, ISO +standard 2022, and ISO/DIS standard 6429.2. + +Characters in the range hex 7F through hex FF are not globally defined +in IFF. They are best left reserved for future standardization. But +note that the FORM type FTXT (formatted text) defines the meaning +of these characters within FTXT forms. In particular, character values +hex 7F through hex 9F are control codes while characters hex A0 through +hex FF are extended graphic characters like , as per the ISO and +ANSI standards cited above. [See the supplementary document "FTXT" +IFF Formatted Text.] + +Dates + +A "creation date" is defined as the date and time a stream of data +bytes was created. (Some systems call this a "last modified date".) +Editing some data changes its creation date. Moving the data between +volumes or machines does not. + +The IFF standard date format will be one of those used in MS-DOS, +Macintosh, or Amiga DOS (probably a 32-bit unsigned number of seconds +since a reference point). Issue: Investigate these three. + +Type IDs + +A "type ID", "property name", "FORM type", or any other IFF identifier +is a 32-bit value: the concatenation of four ASCII characters in the +range R S (SP, hex 20) through R~S (hex 7E). Spaces (hex 20) should +not precede printing characters; trailing spaces are ok. Control characters +are forbidden. + +typedef CHAR ID[4]; + +IDs are compared using a simple 32-bit case-dependent equality test. + +Data section type IDs (aka FORM types) are restriced IDs. (Cf. Data +Sections.) Since they may be stored in filename extensions (Cf. Single +Purpose Files) lower case letters and punctuation marks are forbidden. +Trailing spaces are ok. + +Carefully choose those four characters when you pick a new ID. Make +them mnemonic so programmers can look at an interchange format file +and figure out what kind of data it contains. The name space makes +it possible for developers scattered around the globe to generate +ID values with minimal collisions so long as they choose specific +names like "MUS4" instead of general ones like "TYPE" and "FILE". +EA will "register" new FORM type IDs and format descriptions as they're +devised, but collisions will be improbable so there will be no pressure +on this "clearinghouse" process. Appendix A has a list of currently +defined IDs. + +Sometimes it's necessary to make data format changes that aren't backward +compatible. Since IDs are used to denote data formats in IFF, new +IDs are chosen to denote revised formats. Since programs won't read +chunks whose IDs they don't recognize (see Chunks, below), the new +IDs keep old programs from stumbling over new data. The conventional +way to chose a "revision" ID is to increment the last character if +it's a digit or else change the last character to a digit. E.g. first +and second revisions of the ID "XY" would be "XY1" and "XY2". Revisions +of "CMAP" would be "CMA1" and "CMA2". + +Chunks + +Chunks are the building blocks in the IFF structure. The form expressed +as a C typedef is: + +typedef struct { + ID ckID; + LONG ckSize; /* sizeof(ckData) */ + UBYTE ckData[/* ckSize */]; + } Chunk; + +We can diagram an example chunk a "CMAP" chunk containing 12 data +bytes like this: + ---------------- + ckID: | 'CMAP' | + ckSize: | 12 | + ckData: | 0, 0, 0, 32 | -------- + | 0, 0, 64, 0 | 12 bytes + | 0, 0, 64, 0 | --------- + ---------------- + +The fixed header part means "Here's a type ckID chunk with ckSize +bytes of data." + +The ckID identifies the format and purpose of the chunk. As a rule, +a program must recognize ckID to interpret ckData. It should skip +over all unrecognized chunks. The ckID also serves as a format version +number as long as we pick new IDs to identify new formats of ckData +(see above). + +The following ckIDs are universally reserved to identify chunks with +particular IFF meanings: "LIST", "FORM", "PROP", "CAT ", and " +". The special ID " " (4 spaces) is a ckID for "filler" chunks, +that is, chunks that fill space but have no meaningful contents. The +IDs "LIS1" through "LIS9", "FOR1" through "FOR9", and "CAT1" through +"CAT9" are reserved for future "version number" variations. All IFF-compatible +software must account for these 23 chunk IDs. Appendix A has a list +of predefined IDs. + +The ckSize is a logical block size how many data bytes are in ckData. +If ckData is an odd number of bytes long, a 0 pad byte follows which +is not included in ckSize. (Cf. Alignment.) A chunk's total physical +size is ckSize rounded up to an even number plus the size of the header. +So the smallest chunk is 8 bytes long with ckSize = 0. For the sake +of following chunks, programs must respect every chunk's ckSize as +a virtual end-of-file for reading its ckData even if that data is +malformed, e.g. if nested contents are truncated. + +We can describe the syntax of a chunk as a regular expression with +"#" representing the ckSize, i.e. the length of the following {braced} +bytes. The "[0]" represents a sometimes needed pad byte. (The regular +expressions in this document are collected in Appendix A along with +an explanation of notation.) + +Chunk ::= ID #{ UBYTE* } [0] + +One chunk output technique is to stream write a chunk header, stream +write the chunk contents, then random access back to the header to +fill in the size. Another technique is to make a preliminary pass +over the data to compute the size, then write it out all at once. + +Strings, String Chunks, and String Properties + +In a string of ASCII text, LF denotes a forced line break (paragraph +or line terminator). Other control characters are not used. (Cf. Characters.) + +The ckID for a chunk that contains a string of plain, unformatted +text is "TEXT". As a practical matter, a text string should probably +not be longer than 32767 bytes. The standard allows up to 231 - 1 +bytes. + +When used as a data property (see below), a text string chunk may +be 0 to 255 characters long. Such a string is readily converted to +a C string or a Pascal STRING[255]. The ckID of a property must be +the property name, not "TEXT". + +When used as a part of a chunk or data property, restricted C string +format is normally used. That means 0 to 255 characters followed by +a NUL byte (ASCII value 0). + +Data Properties + +Data properties specify attributes for following (non-property) chunks. +A data property essentially says "identifier = value", for example +"XY = (10, 200)", telling something about following chunks. Properties +may only appear inside data sections ("FORM" chunks, cf. Data Sections) +and property sections ("PROP" chunks, cf. Group PROP). + +The form of a data property is a special case of Chunk. The ckID is +a property name as well as a property type. The ckSize should be small +since data properties are intended to be accumulated in RAM when reading +a file. (256 bytes is a reasonable upper bound.) Syntactically: + +Property ::= Chunk + +When designing a data object, use properties to describe context information +like the size of an image, even if they don't vary in your program. +Other programs will need this information. + +Think of property settings as assignments to variables in a programming +language. Multiple assignments are redundant and local assignments +temporarily override global assignments. The order of assignments +doesn't matter as long as they precede the affected chunks. (Cf. LISTs, +CATs, and Shared Properties.) + +Each object type (FORM type) is a local name space for property IDs. +Think of a "CMAP" property in a "FORM ILBM" as the qualified ID "ILBM.CMAP". +Property IDs specified when an object type is designed (and therefore +known to all clients) are called "standard" while specialized ones +added later are "nonstandard". + +Links + +Issue: A standard mechanism for "links" or "cross references" is very +desirable for things like combining images and sounds into animations. +Perhaps we'll define "link" chunks within FORMs that refer to other +FORMs or to specific chunks within the same and other FORMs. This +needs further work. EA IFF 1985 has no standard link mechanism. + +For now, it may suffice to read a list of, say, musical instruments, +and then just refer to them within a musical score by index number. + +File References + +Issue: We may need a standard form for references to other files. +A "file ref" could name a directory and a file in the same type of +operating system as the ref's originator. Following the reference +would expect the file to be on some mounted volume. In a network environment, +a file ref could name a server, too. + +Issue: How can we express operating-system independent file refs? + +Issue: What about a means to reference a portion of another file? +Would this be a "file ref" plus a reference to a "link" within the +target file? + + + +4. Data Sections + +The first thing we need of a file is to check: Does it contain IFF +data and, if so, does it contain the kind of data we're looking for? +So we come to the notion of a "data section". + +A "data section" or IFF "FORM" is one self-contained "data object" +that might be stored in a file by itself. It is one high level data +object such as a picture or a sound effect. The IFF structure "FORM" +makes it self- identifying. It could be a composite object like a +musical score with nested musical instrument descriptions. + +Group FORM + +A data section is a chunk with ckID "FORM" and this arrangement: + +FORM ::= "FORM" #{ FormType (LocalChunk | FORM | LIST | CAT)* +} +FormType ::= ID +LocalChunk ::= Property | Chunk + +The ID "FORM" is a syntactic keyword like "struct" in C. Think of +a "struct ILBM" containing a field "CMAP". If you see "FORM" you'll +know to expect a FORM type ID (the structure name, "ILBM" in this +example) and a particular contents arrangement or "syntax" (local +chunks, FORMs, LISTs, and CATs). (LISTs and CATs are discussed in +part 5, below.) A "FORM ILBM", in particular, might contain a local +chunk "CMAP", an "ILBM.CMAP" (to use a qualified name). + +So the chunk ID "FORM" indicates a data section. It implies that the +chunk contains an ID and some number of nested chunks. In reading +a FORM, like any other chunk, programs must respect its ckSize as +a virtual end-of-file for reading its contents, even if they're truncated. + +The FormType (or FORM type) is a restricted ID that may not contain +lower case letters or punctuation characters. (Cf. Type IDs. Cf. Single +Purpose Files.) + +The type-specific information in a FORM is composed of its "local +chunks": data properties and other chunks. Each FORM type is a local +name space for local chunk IDs. So "CMAP" local chunks in other FORM +types may be unrelated to "ILBM.CMAP". More than that, each FORM type +defines semantic scope. If you know what a FORM ILBM is, you'll know +what an ILBM.CMAP is. + +Local chunks defined when the FORM type is designed (and therefore +known to all clients of this type) are called "standard" while specialized +ones added later are "nonstandard". + +Among the local chunks, property chunks give settings for various +details like text font while the other chunks supply the essential +information. This distinction is not clear cut. A property setting +cancelled by a later setting of the same property has effect only +on data chunks in between. E.g. in the sequence: + +prop1 = x (propN = value)* prop1 = y + +where the propNs are not prop1, the setting prop1 = x has no effect. + +The following universal chunk IDs are reserved inside any FORM: "LIST", +"FORM", "PROP", "CAT ", "JJJJ", "LIS1" through "LIS9", "FOR1" through +"FOR9", and "CAT1" through "CAT9". (Cf. Chunks. Cf. Group LIST. Cf. +Group PROP.) For clarity, these universal chunk names may not be FORM +type IDs, either. + +Part 5, below, talks about grouping FORMs into LISTs and CATs. They +let you group a bunch of FORMs but don't impose any particular meaning +or constraints on the grouping. Read on. + +Composite FORMs + +A FORM chunk inside a FORM is a full-fledged data section. This means +you can build a composite object like a multi-frame animation sequence +from available picture FORMs and sound effect FORMs. You can insert +additional chunks with information like frame rate and frame count. + +Using composite FORMs, you leverage on existing programs that create +and edit the component FORMs. Those editors may even look into your +composite object to copy out its type of component, although it'll +be the rare program that's fancy enough to do that. Such editors are +not allowed to replace their component objects within your composite +object. That's because the IFF standard lets you specify consistency +requirements for the composite FORM such as maintaining a count or +a directory of the components. Only programs that are written to uphold +the rules of your FORM type should create or modify such FORMs. + +Therefore, in designing a program that creates composite objects, +you are strongly requested to provide a facility for your users to +import and export the nested FORMs. Import and export could move the +data through a clipboard or a file. + +Here are several existing FORM types and rules for defining new ones. + +FTXT + +An FTXT data section contains text with character formatting information +like fonts and faces. It has no paragraph or document formatting information +like margins and page headers. FORM FTXT is well matched to the text +representation in Amiga's Intuition environment. See the supplemental +document "FTXT" IFF Formatted Text. + +ILBM + +"ILBM" is an InterLeaved BitMap image with color map; a machine-independent +format for raster images. FORM ILBM is the standard image file format +for the Commodore-Amiga computer and is useful in other environments, +too. See the supplemental document "ILBM" IFF Interleaved Bitmap. + +PICS + +The data chunk inside a "PICS" data section has ID "PICT" and holds +a QuickDraw picture. Issue: Allow more than one PICT in a PICS? See +Inside Macintosh chapter "QuickDraw" for details on PICTs and how +to create and display them on the Macintosh computer. + +The only standard property for PICS is "XY", an optional property +that indicates the position of the PICT relative to "the big picture". +The contents of an XY is a QuickDraw Point. + +Note: PICT may be limited to Macintosh use, in which case there'll +be another format for structured graphics in other environments. + +Other Macintosh Resource Types + +Some other Macintosh resource types could be adopted for use within +IFF files; perhaps MWRT, ICN, ICN#, and STR#. + +Issue: Consider the candidates and reserve some more IDs. + +Designing New Data Sections + +Supplemental documents will define additional object types. A supplement +needs to specify the object's purpose, its FORM type ID, the IDs and +formats of standard local chunks, and rules for generating and interpreting +the data. It's a good idea to supply typedefs and an example source +program that accesses the new object. See "ILBM" IFF Interleaved Bitmap +for a good example. + +Anyone can pick a new FORM type ID but should reserve it with Electronic +Arts at their earliest convenience. [Issue: EA contact person? Hand +this off to another organization?] While decentralized format definitions +and extensions are possible in IFF, our preference is to get design +consensus by committee, implement a program to read and write it, +perhaps tune the format, and then publish the format with example +code. Some organization should remain in charge of answering questions +and coordinating extensions to the format. + +If it becomes necessary to revise the design of some data section, +its FORM type ID will serve as a version number (Cf. Type IDs). E.g. +a revised "VDEO" data section could be called "VDE1". But try to get +by with compatible revisions within the existing FORM type. + +In a new FORM type, the rules for primitive data types and word-alignment +(Cf. Primitive Data Types) may be overriden for the contents of its +local chunks but not for the chunk structure itself if your documentation +spells out the deviations. If machine-specific type variants are needed, +e.g. to store vast numbers of integers in reverse bit order, then +outline the conversion algorithm and indicate the variant inside each +file, perhaps via different FORM types. Needless to say, variations +should be minimized. + +In designing a FORM type, encapsulate all the data that other programs +will need to interpret your files. E.g. a raster graphics image should +specify the image size even if your program always uses 320 x 200 +pixels x 3 bitplanes. Receiving programs are then empowered to append +or clip the image rectangle, to add or drop bitplanes, etc. This enables +a lot more compatibility. + +Separate the central data (like musical notes) from more specialized +information (like note beams) so simpler programs can extract the +central parts during read-in. Leave room for expansion so other programs +can squeeze in new kinds of information (like lyrics). And remember +to keep the property chunks manageably short let's say 2 256 bytes. + +When designing a data object, try to strike a good tradeoff between +a super-general format and a highly-specialized one. Fit the details +to at least one particular need, for example a raster image might +as well store pixels in the current machine's scan order. But add +the kind of generality that makes it usable with foreseeable hardware +and software. E.g. use a whole byte for each red, green, and blue +color value even if this year's computer has only 4-bit video DACs. +Think ahead and help other programs so long as the overhead is acceptable. +E.g. run compress a raster by scan line rather than as a unit so future +programs can swap images by scan line to and from secondary storage. + +Try to design a general purpose "least common multiple" format that +encompasses the needs of many programs without getting too complicated. +Let's coalesce our uses around a few such formats widely separated +in the vast design space. Two factors make this flexibility and simplicity +practical. First, file storage space is getting very plentiful, so +compaction is not a priority. Second, nearly any locally-performed +data conversion work during file reading and writing will be cheap +compared to the I/O time. + +It must be ok to copy a LIST or FORM or CAT intact, e.g. to incorporate +it into a composite FORM. So any kind of internal references within +a FORM must be relative references. They could be relative to the +start of the containing FORM, relative from the referencing chunk, +or a sequence number into a collection. + +With composite FORMs, you leverage on existing programs that create +and edit the components. If you write a program that creates composite +objects, please provide a facility for your users to import and export +the nested FORMs. The import and export functions may move data through +a separate file or a clipboard. + +Finally, don't forget to specify all implied rules in detail. + + + +5. LISTs, CATs, and Shared Properties + +Data often needs to be grouped together like a list of icons. Sometimes +a trick like arranging little images into a big raster works, but +generally they'll need to be structured as a first class group. The +objects "LIST" and "CAT" are IFF-universal mechanisms for this purpose. + +Property settings sometimes need to be shared over a list of similar +objects. E.g. a list of icons may share one color map. LIST provides +a means called "PROP" to do this. One purpose of a LIST is to define +the scope of a PROP. A "CAT", on the other hand, is simply a concatenation +of objects. + +Simpler programs may skip LISTs and PROPs altogether and just handle +FORMs and CATs. All "fully-conforming" IFF programs also know about +"CAT ", "LIST", and "PROP". Any program that reads a FORM inside a +LIST must process shared PROPs to correctly interpret that FORM. + +Group CAT + +A CAT is just an untyped group of data objects. + +Structurally, a CAT is a chunk with chunk ID "CAT " containing a "contents +type" ID followed by the nested objects. The ckSize of each contained +chunk is essentially a relative pointer to the next one. + +CAT ::= "CAT " #{ ContentsType (FORM | LIST | CAT)* } +ContentsType ::= ID -- a hint or an "abstract data type" ID + +In reading a CAT, like any other chunk, programs must respect it's +ckSize as a virtual end-of-file for reading the nested objects even +if they're malformed or truncated. + +The "contents type" following the CAT's ckSize indicates what kind +of FORMs are inside. So a CAT of ILBMs would store "ILBM" there. It's +just a hint. It may be used to store an "abstract data type". A CAT +could just have blank contents ID ("JJJJ") if it contains more than +one kind of FORM. + +CAT defines only the format of the group. The group's meaning is open +to interpretation. This is like a list in LISP: the structure of cells +is predefined but the meaning of the contents as, say, an association +list depends on use. If you need a group with an enforced meaning +(an "abstract data type" or Smalltalk "subclass"), some consistency +constraints, or additional data chunks, use a composite FORM instead +(Cf. Composite FORMs). + +Since a CAT just means a concatenation of objects, CATs are rarely +nested. Programs should really merge CATs rather than nest them. + +Group LIST + +A LIST defines a group very much like CAT but it also gives a scope +for PROPs (see below). And unlike CATs, LISTs should not be merged +without understanding their contents. + +Structurally, a LIST is a chunk with ckID "LIST" containing a "contents +type" ID, optional shared properties, and the nested contents (FORMs, +LISTs, and CATs), in that order. The ckSize of each contained chunk +is a relative pointer to the next one. A LIST is not an arbitrary +linked list the cells are simply concatenated. + +LIST ::= "LIST" #{ ContentsType PROP* (FORM | LIST | CAT)* } +ContentsType ::= ID + +Group PROP + +PROP chunks may appear in LISTs (not in FORMs or CATs). They supply +shared properties for the FORMs in that LIST. This ability to elevate +some property settings to shared status for a list of forms is useful +for both indirection and compaction. E.g. a list of images with the +same size and colors can share one "size" property and one "color +map" property. Individual FORMs can override the shared settings. + +The contents of a PROP is like a FORM with no data chunks: + +PROP ::= "PROP" #{ FormType Property* } + +It means, "Here are the shared properties for FORM type <." + +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 0000000..d52f0eb Binary files /dev/null and b/tests/fixtures/ASH.LBM differ diff --git a/tests/fixtures/INVALID_CHUNK_ID.LBM b/tests/fixtures/INVALID_CHUNK_ID.LBM new file mode 100644 index 0000000..0553bd6 Binary files /dev/null and b/tests/fixtures/INVALID_CHUNK_ID.LBM differ diff --git a/tests/fixtures/INVALID_CHUNK_LENGTH.LBM b/tests/fixtures/INVALID_CHUNK_LENGTH.LBM new file mode 100644 index 0000000..275a58c Binary files /dev/null and b/tests/fixtures/INVALID_CHUNK_LENGTH.LBM differ diff --git a/tests/fixtures/SEASCAPE.LBM b/tests/fixtures/SEASCAPE.LBM new file mode 100644 index 0000000..0a07a17 Binary files /dev/null and b/tests/fixtures/SEASCAPE.LBM differ diff --git a/tests/fixtures/VALID.LBM b/tests/fixtures/VALID.LBM new file mode 100644 index 0000000..8e5ff8f Binary files /dev/null and b/tests/fixtures/VALID.LBM differ diff --git a/tests/pbm.test.js b/tests/pbm.test.js new file mode 100644 index 0000000..070db63 --- /dev/null +++ b/tests/pbm.test.js @@ -0,0 +1,143 @@ +/* + + 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 { 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]); +});