mirror of https://github.com/1disk/edp445.git
Changed alot of things.
This commit is contained in:
parent
a5a0523e5a
commit
3513d5390c
|
@ -0,0 +1,86 @@
|
||||||
|
entrypoint = "index.js"
|
||||||
|
|
||||||
|
hidden = [".config"]
|
||||||
|
|
||||||
|
[interpreter]
|
||||||
|
command = [
|
||||||
|
"prybar-nodejs",
|
||||||
|
"-q",
|
||||||
|
"--ps1",
|
||||||
|
"\u0001\u001b[33m\u0002\u0001\u001b[00m\u0002 ",
|
||||||
|
"-i"
|
||||||
|
]
|
||||||
|
|
||||||
|
[[hints]]
|
||||||
|
regex = "Error \\[ERR_REQUIRE_ESM\\]"
|
||||||
|
message = "We see that you are using require(...) inside your code. We currently do not support this syntax. Please use 'import' instead when using external modules. (https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/import)"
|
||||||
|
|
||||||
|
[nix]
|
||||||
|
channel = "stable-22_05"
|
||||||
|
|
||||||
|
[env]
|
||||||
|
XDG_CONFIG_HOME = "/home/runner/.config"
|
||||||
|
PATH = "/home/runner/$REPL_SLUG/.config/npm/node_global/bin:/home/runner/$REPL_SLUG/node_modules/.bin"
|
||||||
|
npm_config_prefix = "/home/runner/$REPL_SLUG/.config/npm/node_global"
|
||||||
|
|
||||||
|
[gitHubImport]
|
||||||
|
requiredFiles = [".replit", "replit.nix", ".config"]
|
||||||
|
|
||||||
|
[packager]
|
||||||
|
language = "nodejs"
|
||||||
|
|
||||||
|
[packager.features]
|
||||||
|
packageSearch = true
|
||||||
|
guessImports = true
|
||||||
|
enabledForHosting = false
|
||||||
|
|
||||||
|
[unitTest]
|
||||||
|
language = "nodejs"
|
||||||
|
|
||||||
|
[debugger]
|
||||||
|
support = true
|
||||||
|
|
||||||
|
[debugger.interactive]
|
||||||
|
transport = "localhost:0"
|
||||||
|
startCommand = [ "dap-node" ]
|
||||||
|
|
||||||
|
[debugger.interactive.initializeMessage]
|
||||||
|
command = "initialize"
|
||||||
|
type = "request"
|
||||||
|
|
||||||
|
[debugger.interactive.initializeMessage.arguments]
|
||||||
|
clientID = "replit"
|
||||||
|
clientName = "replit.com"
|
||||||
|
columnsStartAt1 = true
|
||||||
|
linesStartAt1 = true
|
||||||
|
locale = "en-us"
|
||||||
|
pathFormat = "path"
|
||||||
|
supportsInvalidatedEvent = true
|
||||||
|
supportsProgressReporting = true
|
||||||
|
supportsRunInTerminalRequest = true
|
||||||
|
supportsVariablePaging = true
|
||||||
|
supportsVariableType = true
|
||||||
|
|
||||||
|
[debugger.interactive.launchMessage]
|
||||||
|
command = "launch"
|
||||||
|
type = "request"
|
||||||
|
|
||||||
|
[debugger.interactive.launchMessage.arguments]
|
||||||
|
args = []
|
||||||
|
console = "externalTerminal"
|
||||||
|
cwd = "."
|
||||||
|
environment = []
|
||||||
|
pauseForSourceMap = false
|
||||||
|
program = "./index.js"
|
||||||
|
request = "launch"
|
||||||
|
sourceMaps = true
|
||||||
|
stopOnEntry = false
|
||||||
|
type = "pwa-node"
|
||||||
|
|
||||||
|
[languages]
|
||||||
|
|
||||||
|
[languages.javascript]
|
||||||
|
pattern = "**/{*.js,*.jsx,*.ts,*.tsx}"
|
||||||
|
|
||||||
|
[languages.javascript.languageServer]
|
||||||
|
start = "typescript-language-server --stdio"
|
|
@ -1,3 +0,0 @@
|
||||||
{
|
|
||||||
"token":"YOUR_TOKEN_HERE"
|
|
||||||
}
|
|
|
@ -16,7 +16,7 @@ module.exports = (msc, author) => {
|
||||||
msc2 = msc2.replace(/ /g, '')
|
msc2 = msc2.replace(/ /g, '')
|
||||||
|
|
||||||
|
|
||||||
var replies = ['https://cdn.discordapp.com/attachments/1009456381071470724/1040330102979960903/acqzo_video_1651329243913_1.mp4', 'thank you', `i think you're ${msc2} too`, 'thanks!!', 'omg thank youu', '💕💕💕💕', 'tysm', 'you made my day', 'ily too', 'bro thanks', ':kiss:', `im not that ${msc2} 🫢`, `am i really ${msc2}?`, `i dont think im ${msc2}`]
|
var replies = ['https://cdn.discordapp.com/attachments/1009456381071470724/1040330102979960903/acqzo_video_1651329243913_1.mp4', 'thank you', `i think you're ${msc2} too`, 'thanks!!', 'omg thank youu', '💕💕💕💕', 'tysm', 'you made my day', 'ily too', 'bro thanks', ':kiss:', `im not that ${msc2} 🫢`, `am i really ${msc2}?`, `i dont think im ${msc2}`, 'Umm thanks i guess 😳😳😳😳']
|
||||||
var reply = replies[Math.floor(Math.random() * replies.length)]
|
var reply = replies[Math.floor(Math.random() * replies.length)]
|
||||||
var donejson = {
|
var donejson = {
|
||||||
detections:detections,
|
detections:detections,
|
||||||
|
|
8
index.js
8
index.js
|
@ -2,18 +2,18 @@ const { Client, Intents, Collection, MessageEmbed, MessageActionRow, MessageButt
|
||||||
const Discord = require('discord.js'); //v12.5.3
|
const Discord = require('discord.js'); //v12.5.3
|
||||||
require('discord-inline-reply'); //Import inline replies
|
require('discord-inline-reply'); //Import inline replies
|
||||||
const client = new Client(); //New Discord client
|
const client = new Client(); //New Discord client
|
||||||
const botconfig = require('./data/botconfig.json') //Login info for the bot, you will have to provide your own info there
|
const botconfig = process.env['TOKEN'] //Login info for the bot, you will have to provide your own info there
|
||||||
|
|
||||||
client.on("ready", () => {
|
client.on("ready", () => {
|
||||||
console.log(`The bot is online!`)
|
console.log(`The bot is online!`)
|
||||||
|
|
||||||
client.user.setActivity(`${client.guilds.cache.size} servers • discord.gg/memee`, {
|
client.user.setActivity(`${client.guilds.cache.size} servers`, {
|
||||||
type: "WATCHING"
|
type: "WATCHING"
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
client.on("guildCreate", function(guild){
|
client.on("guildCreate", function(guild){
|
||||||
client.user.setActivity(`${client.guilds.cache.size} servers • discord.gg/memee`, {
|
client.user.setActivity(`${client.guilds.cache.size} servers`, {
|
||||||
type: "WATCHING"
|
type: "WATCHING"
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -47,4 +47,4 @@ client.on("message", async (message) => {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
client.login(botconfig.token)
|
client.login(botconfig)
|
|
@ -0,0 +1 @@
|
||||||
|
../canvacord/bin/canvacord.js
|
|
@ -0,0 +1 @@
|
||||||
|
../mime/cli.js
|
|
@ -0,0 +1 @@
|
||||||
|
../semver/bin/semver.js
|
|
@ -0,0 +1 @@
|
||||||
|
../sshpk/bin/sshpk-conv
|
|
@ -0,0 +1 @@
|
||||||
|
../sshpk/bin/sshpk-sign
|
|
@ -0,0 +1 @@
|
||||||
|
../sshpk/bin/sshpk-verify
|
|
@ -0,0 +1 @@
|
||||||
|
../uuid/bin/uuid
|
File diff suppressed because it is too large
Load Diff
|
@ -0,0 +1,190 @@
|
||||||
|
Apache License
|
||||||
|
Version 2.0, January 2004
|
||||||
|
http://www.apache.org/licenses/
|
||||||
|
|
||||||
|
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||||
|
|
||||||
|
1. Definitions.
|
||||||
|
|
||||||
|
"License" shall mean the terms and conditions for use, reproduction,
|
||||||
|
and distribution as defined by Sections 1 through 9 of this document.
|
||||||
|
|
||||||
|
"Licensor" shall mean the copyright owner or entity authorized by
|
||||||
|
the copyright owner that is granting the License.
|
||||||
|
|
||||||
|
"Legal Entity" shall mean the union of the acting entity and all
|
||||||
|
other entities that control, are controlled by, or are under common
|
||||||
|
control with that entity. For the purposes of this definition,
|
||||||
|
"control" means (i) the power, direct or indirect, to cause the
|
||||||
|
direction or management of such entity, whether by contract or
|
||||||
|
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||||
|
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||||
|
|
||||||
|
"You" (or "Your") shall mean an individual or Legal Entity
|
||||||
|
exercising permissions granted by this License.
|
||||||
|
|
||||||
|
"Source" form shall mean the preferred form for making modifications,
|
||||||
|
including but not limited to software source code, documentation
|
||||||
|
source, and configuration files.
|
||||||
|
|
||||||
|
"Object" form shall mean any form resulting from mechanical
|
||||||
|
transformation or translation of a Source form, including but
|
||||||
|
not limited to compiled object code, generated documentation,
|
||||||
|
and conversions to other media types.
|
||||||
|
|
||||||
|
"Work" shall mean the work of authorship, whether in Source or
|
||||||
|
Object form, made available under the License, as indicated by a
|
||||||
|
copyright notice that is included in or attached to the work
|
||||||
|
(an example is provided in the Appendix below).
|
||||||
|
|
||||||
|
"Derivative Works" shall mean any work, whether in Source or Object
|
||||||
|
form, that is based on (or derived from) the Work and for which the
|
||||||
|
editorial revisions, annotations, elaborations, or other modifications
|
||||||
|
represent, as a whole, an original work of authorship. For the purposes
|
||||||
|
of this License, Derivative Works shall not include works that remain
|
||||||
|
separable from, or merely link (or bind by name) to the interfaces of,
|
||||||
|
the Work and Derivative Works thereof.
|
||||||
|
|
||||||
|
"Contribution" shall mean any work of authorship, including
|
||||||
|
the original version of the Work and any modifications or additions
|
||||||
|
to that Work or Derivative Works thereof, that is intentionally
|
||||||
|
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||||
|
or by an individual or Legal Entity authorized to submit on behalf of
|
||||||
|
the copyright owner. For the purposes of this definition, "submitted"
|
||||||
|
means any form of electronic, verbal, or written communication sent
|
||||||
|
to the Licensor or its representatives, including but not limited to
|
||||||
|
communication on electronic mailing lists, source code control systems,
|
||||||
|
and issue tracking systems that are managed by, or on behalf of, the
|
||||||
|
Licensor for the purpose of discussing and improving the Work, but
|
||||||
|
excluding communication that is conspicuously marked or otherwise
|
||||||
|
designated in writing by the copyright owner as "Not a Contribution."
|
||||||
|
|
||||||
|
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||||
|
on behalf of whom a Contribution has been received by Licensor and
|
||||||
|
subsequently incorporated within the Work.
|
||||||
|
|
||||||
|
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||||
|
this License, each Contributor hereby grants to You a perpetual,
|
||||||
|
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||||
|
copyright license to reproduce, prepare Derivative Works of,
|
||||||
|
publicly display, publicly perform, sublicense, and distribute the
|
||||||
|
Work and such Derivative Works in Source or Object form.
|
||||||
|
|
||||||
|
3. Grant of Patent License. Subject to the terms and conditions of
|
||||||
|
this License, each Contributor hereby grants to You a perpetual,
|
||||||
|
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||||
|
(except as stated in this section) patent license to make, have made,
|
||||||
|
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||||
|
where such license applies only to those patent claims licensable
|
||||||
|
by such Contributor that are necessarily infringed by their
|
||||||
|
Contribution(s) alone or by combination of their Contribution(s)
|
||||||
|
with the Work to which such Contribution(s) was submitted. If You
|
||||||
|
institute patent litigation against any entity (including a
|
||||||
|
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||||
|
or a Contribution incorporated within the Work constitutes direct
|
||||||
|
or contributory patent infringement, then any patent licenses
|
||||||
|
granted to You under this License for that Work shall terminate
|
||||||
|
as of the date such litigation is filed.
|
||||||
|
|
||||||
|
4. Redistribution. You may reproduce and distribute copies of the
|
||||||
|
Work or Derivative Works thereof in any medium, with or without
|
||||||
|
modifications, and in Source or Object form, provided that You
|
||||||
|
meet the following conditions:
|
||||||
|
|
||||||
|
(a) You must give any other recipients of the Work or
|
||||||
|
Derivative Works a copy of this License; and
|
||||||
|
|
||||||
|
(b) You must cause any modified files to carry prominent notices
|
||||||
|
stating that You changed the files; and
|
||||||
|
|
||||||
|
(c) You must retain, in the Source form of any Derivative Works
|
||||||
|
that You distribute, all copyright, patent, trademark, and
|
||||||
|
attribution notices from the Source form of the Work,
|
||||||
|
excluding those notices that do not pertain to any part of
|
||||||
|
the Derivative Works; and
|
||||||
|
|
||||||
|
(d) If the Work includes a "NOTICE" text file as part of its
|
||||||
|
distribution, then any Derivative Works that You distribute must
|
||||||
|
include a readable copy of the attribution notices contained
|
||||||
|
within such NOTICE file, excluding those notices that do not
|
||||||
|
pertain to any part of the Derivative Works, in at least one
|
||||||
|
of the following places: within a NOTICE text file distributed
|
||||||
|
as part of the Derivative Works; within the Source form or
|
||||||
|
documentation, if provided along with the Derivative Works; or,
|
||||||
|
within a display generated by the Derivative Works, if and
|
||||||
|
wherever such third-party notices normally appear. The contents
|
||||||
|
of the NOTICE file are for informational purposes only and
|
||||||
|
do not modify the License. You may add Your own attribution
|
||||||
|
notices within Derivative Works that You distribute, alongside
|
||||||
|
or as an addendum to the NOTICE text from the Work, provided
|
||||||
|
that such additional attribution notices cannot be construed
|
||||||
|
as modifying the License.
|
||||||
|
|
||||||
|
You may add Your own copyright statement to Your modifications and
|
||||||
|
may provide additional or different license terms and conditions
|
||||||
|
for use, reproduction, or distribution of Your modifications, or
|
||||||
|
for any such Derivative Works as a whole, provided Your use,
|
||||||
|
reproduction, and distribution of the Work otherwise complies with
|
||||||
|
the conditions stated in this License.
|
||||||
|
|
||||||
|
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||||
|
any Contribution intentionally submitted for inclusion in the Work
|
||||||
|
by You to the Licensor shall be under the terms and conditions of
|
||||||
|
this License, without any additional terms or conditions.
|
||||||
|
Notwithstanding the above, nothing herein shall supersede or modify
|
||||||
|
the terms of any separate license agreement you may have executed
|
||||||
|
with Licensor regarding such Contributions.
|
||||||
|
|
||||||
|
6. Trademarks. This License does not grant permission to use the trade
|
||||||
|
names, trademarks, service marks, or product names of the Licensor,
|
||||||
|
except as required for reasonable and customary use in describing the
|
||||||
|
origin of the Work and reproducing the content of the NOTICE file.
|
||||||
|
|
||||||
|
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||||
|
agreed to in writing, Licensor provides the Work (and each
|
||||||
|
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||||
|
implied, including, without limitation, any warranties or conditions
|
||||||
|
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||||
|
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||||
|
appropriateness of using or redistributing the Work and assume any
|
||||||
|
risks associated with Your exercise of permissions under this License.
|
||||||
|
|
||||||
|
8. Limitation of Liability. In no event and under no legal theory,
|
||||||
|
whether in tort (including negligence), contract, or otherwise,
|
||||||
|
unless required by applicable law (such as deliberate and grossly
|
||||||
|
negligent acts) or agreed to in writing, shall any Contributor be
|
||||||
|
liable to You for damages, including any direct, indirect, special,
|
||||||
|
incidental, or consequential damages of any character arising as a
|
||||||
|
result of this License or out of the use or inability to use the
|
||||||
|
Work (including but not limited to damages for loss of goodwill,
|
||||||
|
work stoppage, computer failure or malfunction, or any and all
|
||||||
|
other commercial damages or losses), even if such Contributor
|
||||||
|
has been advised of the possibility of such damages.
|
||||||
|
|
||||||
|
9. Accepting Warranty or Additional Liability. While redistributing
|
||||||
|
the Work or Derivative Works thereof, You may choose to offer,
|
||||||
|
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||||
|
or other liability obligations and/or rights consistent with this
|
||||||
|
License. However, in accepting such obligations, You may act only
|
||||||
|
on Your own behalf and on Your sole responsibility, not on behalf
|
||||||
|
of any other Contributor, and only if You agree to indemnify,
|
||||||
|
defend, and hold each Contributor harmless for any liability
|
||||||
|
incurred by, or claims asserted against, such Contributor by reason
|
||||||
|
of your accepting any such warranty or additional liability.
|
||||||
|
|
||||||
|
END OF TERMS AND CONDITIONS
|
||||||
|
|
||||||
|
Copyright 2015 - 2020 Amish Shah
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
|
@ -0,0 +1,3 @@
|
||||||
|
# Collection
|
||||||
|
|
||||||
|
Utility data structure used in Discord.js.
|
|
@ -0,0 +1,319 @@
|
||||||
|
export interface CollectionConstructor {
|
||||||
|
new (): Collection<unknown, unknown>;
|
||||||
|
new <K, V>(entries?: ReadonlyArray<readonly [K, V]> | null): Collection<K, V>;
|
||||||
|
new <K, V>(iterable: Iterable<readonly [K, V]>): Collection<K, V>;
|
||||||
|
readonly prototype: Collection<unknown, unknown>;
|
||||||
|
readonly [Symbol.species]: CollectionConstructor;
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* A Map with additional utility methods. This is used throughout discord.js rather than Arrays for anything that has
|
||||||
|
* an ID, for significantly improved performance and ease-of-use.
|
||||||
|
* @extends {Map}
|
||||||
|
* @property {number} size - The amount of elements in this collection.
|
||||||
|
*/
|
||||||
|
declare class Collection<K, V> extends Map<K, V> {
|
||||||
|
private _array;
|
||||||
|
private _keyArray;
|
||||||
|
static readonly default: typeof Collection;
|
||||||
|
['constructor']: typeof Collection;
|
||||||
|
constructor(entries?: ReadonlyArray<readonly [K, V]> | null);
|
||||||
|
/**
|
||||||
|
* Identical to [Map.get()](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Map/get).
|
||||||
|
* Gets an element with the specified key, and returns its value, or `undefined` if the element does not exist.
|
||||||
|
* @param {*} key - The key to get from this collection
|
||||||
|
* @returns {* | undefined}
|
||||||
|
*/
|
||||||
|
get(key: K): V | undefined;
|
||||||
|
/**
|
||||||
|
* Identical to [Map.set()](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Map/set).
|
||||||
|
* Sets a new element in the collection with the specified key and value.
|
||||||
|
* @param {*} key - The key of the element to add
|
||||||
|
* @param {*} value - The value of the element to add
|
||||||
|
* @returns {Collection}
|
||||||
|
*/
|
||||||
|
set(key: K, value: V): this;
|
||||||
|
/**
|
||||||
|
* Identical to [Map.has()](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Map/has).
|
||||||
|
* Checks if an element exists in the collection.
|
||||||
|
* @param {*} key - The key of the element to check for
|
||||||
|
* @returns {boolean} `true` if the element exists, `false` if it does not exist.
|
||||||
|
*/
|
||||||
|
has(key: K): boolean;
|
||||||
|
/**
|
||||||
|
* Identical to [Map.delete()](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Map/delete).
|
||||||
|
* Deletes an element from the collection.
|
||||||
|
* @param {*} key - The key to delete from the collection
|
||||||
|
* @returns {boolean} `true` if the element was removed, `false` if the element does not exist.
|
||||||
|
*/
|
||||||
|
delete(key: K): boolean;
|
||||||
|
/**
|
||||||
|
* Identical to [Map.clear()](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Map/clear).
|
||||||
|
* Removes all elements from the collection.
|
||||||
|
* @returns {undefined}
|
||||||
|
*/
|
||||||
|
clear(): void;
|
||||||
|
/**
|
||||||
|
* Creates an ordered array of the values of this collection, and caches it internally. The array will only be
|
||||||
|
* reconstructed if an item is added to or removed from the collection, or if you change the length of the array
|
||||||
|
* itself. If you don't want this caching behavior, use `[...collection.values()]` or
|
||||||
|
* `Array.from(collection.values())` instead.
|
||||||
|
* @returns {Array}
|
||||||
|
*/
|
||||||
|
array(): V[];
|
||||||
|
/**
|
||||||
|
* Creates an ordered array of the keys of this collection, and caches it internally. The array will only be
|
||||||
|
* reconstructed if an item is added to or removed from the collection, or if you change the length of the array
|
||||||
|
* itself. If you don't want this caching behavior, use `[...collection.keys()]` or
|
||||||
|
* `Array.from(collection.keys())` instead.
|
||||||
|
* @returns {Array}
|
||||||
|
*/
|
||||||
|
keyArray(): K[];
|
||||||
|
/**
|
||||||
|
* Obtains the first value(s) in this collection.
|
||||||
|
* @param {number} [amount] Amount of values to obtain from the beginning
|
||||||
|
* @returns {*|Array<*>} A single value if no amount is provided or an array of values, starting from the end if
|
||||||
|
* amount is negative
|
||||||
|
*/
|
||||||
|
first(): V | undefined;
|
||||||
|
first(amount: number): V[];
|
||||||
|
/**
|
||||||
|
* Obtains the first key(s) in this collection.
|
||||||
|
* @param {number} [amount] Amount of keys to obtain from the beginning
|
||||||
|
* @returns {*|Array<*>} A single key if no amount is provided or an array of keys, starting from the end if
|
||||||
|
* amount is negative
|
||||||
|
*/
|
||||||
|
firstKey(): K | undefined;
|
||||||
|
firstKey(amount: number): K[];
|
||||||
|
/**
|
||||||
|
* Obtains the last value(s) in this collection. This relies on {@link Collection#array}, and thus the caching
|
||||||
|
* mechanism applies here as well.
|
||||||
|
* @param {number} [amount] Amount of values to obtain from the end
|
||||||
|
* @returns {*|Array<*>} A single value if no amount is provided or an array of values, starting from the start if
|
||||||
|
* amount is negative
|
||||||
|
*/
|
||||||
|
last(): V | undefined;
|
||||||
|
last(amount: number): V[];
|
||||||
|
/**
|
||||||
|
* Obtains the last key(s) in this collection. This relies on {@link Collection#keyArray}, and thus the caching
|
||||||
|
* mechanism applies here as well.
|
||||||
|
* @param {number} [amount] Amount of keys to obtain from the end
|
||||||
|
* @returns {*|Array<*>} A single key if no amount is provided or an array of keys, starting from the start if
|
||||||
|
* amount is negative
|
||||||
|
*/
|
||||||
|
lastKey(): K | undefined;
|
||||||
|
lastKey(amount: number): K[];
|
||||||
|
/**
|
||||||
|
* Obtains unique random value(s) from this collection. This relies on {@link Collection#array}, and thus the caching
|
||||||
|
* mechanism applies here as well.
|
||||||
|
* @param {number} [amount] Amount of values to obtain randomly
|
||||||
|
* @returns {*|Array<*>} A single value if no amount is provided or an array of values
|
||||||
|
*/
|
||||||
|
random(): V;
|
||||||
|
random(amount: number): V[];
|
||||||
|
/**
|
||||||
|
* Obtains unique random key(s) from this collection. This relies on {@link Collection#keyArray}, and thus the caching
|
||||||
|
* mechanism applies here as well.
|
||||||
|
* @param {number} [amount] Amount of keys to obtain randomly
|
||||||
|
* @returns {*|Array<*>} A single key if no amount is provided or an array
|
||||||
|
*/
|
||||||
|
randomKey(): K;
|
||||||
|
randomKey(amount: number): K[];
|
||||||
|
/**
|
||||||
|
* Searches for a single item where the given function returns a truthy value. This behaves like
|
||||||
|
* [Array.find()](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/find).
|
||||||
|
* <warn>All collections used in Discord.js are mapped using their `id` property, and if you want to find by id you
|
||||||
|
* should use the `get` method. See
|
||||||
|
* [MDN](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Map/get) for details.</warn>
|
||||||
|
* @param {Function} fn The function to test with (should return boolean)
|
||||||
|
* @param {*} [thisArg] Value to use as `this` when executing function
|
||||||
|
* @returns {*}
|
||||||
|
* @example collection.find(user => user.username === 'Bob');
|
||||||
|
*/
|
||||||
|
find(fn: (value: V, key: K, collection: this) => boolean): V | undefined;
|
||||||
|
find<T>(fn: (this: T, value: V, key: K, collection: this) => boolean, thisArg: T): V | undefined;
|
||||||
|
/**
|
||||||
|
* Searches for the key of a single item where the given function returns a truthy value. This behaves like
|
||||||
|
* [Array.findIndex()](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/findIndex),
|
||||||
|
* but returns the key rather than the positional index.
|
||||||
|
* @param {Function} fn The function to test with (should return boolean)
|
||||||
|
* @param {*} [thisArg] Value to use as `this` when executing function
|
||||||
|
* @returns {*}
|
||||||
|
* @example collection.findKey(user => user.username === 'Bob');
|
||||||
|
*/
|
||||||
|
findKey(fn: (value: V, key: K, collection: this) => boolean): K | undefined;
|
||||||
|
findKey<T>(fn: (this: T, value: V, key: K, collection: this) => boolean, thisArg: T): K | undefined;
|
||||||
|
/**
|
||||||
|
* Removes items that satisfy the provided filter function.
|
||||||
|
* @param {Function} fn Function used to test (should return a boolean)
|
||||||
|
* @param {*} [thisArg] Value to use as `this` when executing function
|
||||||
|
* @returns {number} The number of removed entries
|
||||||
|
*/
|
||||||
|
sweep(fn: (value: V, key: K, collection: this) => boolean): number;
|
||||||
|
sweep<T>(fn: (this: T, value: V, key: K, collection: this) => boolean, thisArg: T): number;
|
||||||
|
/**
|
||||||
|
* Identical to
|
||||||
|
* [Array.filter()](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/filter),
|
||||||
|
* but returns a Collection instead of an Array.
|
||||||
|
* @param {Function} fn The function to test with (should return boolean)
|
||||||
|
* @param {*} [thisArg] Value to use as `this` when executing function
|
||||||
|
* @returns {Collection}
|
||||||
|
* @example collection.filter(user => user.username === 'Bob');
|
||||||
|
*/
|
||||||
|
filter(fn: (value: V, key: K, collection: this) => boolean): this;
|
||||||
|
filter<T>(fn: (this: T, value: V, key: K, collection: this) => boolean, thisArg: T): this;
|
||||||
|
/**
|
||||||
|
* Partitions the collection into two collections where the first collection
|
||||||
|
* contains the items that passed and the second contains the items that failed.
|
||||||
|
* @param {Function} fn Function used to test (should return a boolean)
|
||||||
|
* @param {*} [thisArg] Value to use as `this` when executing function
|
||||||
|
* @returns {Collection[]}
|
||||||
|
* @example const [big, small] = collection.partition(guild => guild.memberCount > 250);
|
||||||
|
*/
|
||||||
|
partition(fn: (value: V, key: K, collection: this) => boolean): [this, this];
|
||||||
|
partition<T>(fn: (this: T, value: V, key: K, collection: this) => boolean, thisArg: T): [this, this];
|
||||||
|
/**
|
||||||
|
* Maps each item into a Collection, then joins the results into a single Collection. Identical in behavior to
|
||||||
|
* [Array.flatMap()](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/flatMap).
|
||||||
|
* @param {Function} fn Function that produces a new Collection
|
||||||
|
* @param {*} [thisArg] Value to use as `this` when executing function
|
||||||
|
* @returns {Collection}
|
||||||
|
* @example collection.flatMap(guild => guild.members.cache);
|
||||||
|
*/
|
||||||
|
flatMap<T>(fn: (value: V, key: K, collection: this) => Collection<K, T>): Collection<K, T>;
|
||||||
|
flatMap<T, This>(fn: (this: This, value: V, key: K, collection: this) => Collection<K, T>, thisArg: This): Collection<K, T>;
|
||||||
|
/**
|
||||||
|
* Maps each item to another value into an array. Identical in behavior to
|
||||||
|
* [Array.map()](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/map).
|
||||||
|
* @param {Function} fn Function that produces an element of the new array, taking three arguments
|
||||||
|
* @param {*} [thisArg] Value to use as `this` when executing function
|
||||||
|
* @returns {Array}
|
||||||
|
* @example collection.map(user => user.tag);
|
||||||
|
*/
|
||||||
|
map<T>(fn: (value: V, key: K, collection: this) => T): T[];
|
||||||
|
map<This, T>(fn: (this: This, value: V, key: K, collection: this) => T, thisArg: This): T[];
|
||||||
|
/**
|
||||||
|
* Maps each item to another value into a collection. Identical in behavior to
|
||||||
|
* [Array.map()](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/map).
|
||||||
|
* @param {Function} fn Function that produces an element of the new collection, taking three arguments
|
||||||
|
* @param {*} [thisArg] Value to use as `this` when executing function
|
||||||
|
* @returns {Collection}
|
||||||
|
* @example collection.mapValues(user => user.tag);
|
||||||
|
*/
|
||||||
|
mapValues<T>(fn: (value: V, key: K, collection: this) => T): Collection<K, T>;
|
||||||
|
mapValues<This, T>(fn: (this: This, value: V, key: K, collection: this) => T, thisArg: This): Collection<K, T>;
|
||||||
|
/**
|
||||||
|
* Checks if there exists an item that passes a test. Identical in behavior to
|
||||||
|
* [Array.some()](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/some).
|
||||||
|
* @param {Function} fn Function used to test (should return a boolean)
|
||||||
|
* @param {*} [thisArg] Value to use as `this` when executing function
|
||||||
|
* @returns {boolean}
|
||||||
|
* @example collection.some(user => user.discriminator === '0000');
|
||||||
|
*/
|
||||||
|
some(fn: (value: V, key: K, collection: this) => boolean): boolean;
|
||||||
|
some<T>(fn: (this: T, value: V, key: K, collection: this) => boolean, thisArg: T): boolean;
|
||||||
|
/**
|
||||||
|
* Checks if all items passes a test. Identical in behavior to
|
||||||
|
* [Array.every()](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/every).
|
||||||
|
* @param {Function} fn Function used to test (should return a boolean)
|
||||||
|
* @param {*} [thisArg] Value to use as `this` when executing function
|
||||||
|
* @returns {boolean}
|
||||||
|
* @example collection.every(user => !user.bot);
|
||||||
|
*/
|
||||||
|
every(fn: (value: V, key: K, collection: this) => boolean): boolean;
|
||||||
|
every<T>(fn: (this: T, value: V, key: K, collection: this) => boolean, thisArg: T): boolean;
|
||||||
|
/**
|
||||||
|
* Applies a function to produce a single value. Identical in behavior to
|
||||||
|
* [Array.reduce()](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/reduce).
|
||||||
|
* @param {Function} fn Function used to reduce, taking four arguments; `accumulator`, `currentValue`, `currentKey`,
|
||||||
|
* and `collection`
|
||||||
|
* @param {*} [initialValue] Starting value for the accumulator
|
||||||
|
* @returns {*}
|
||||||
|
* @example collection.reduce((acc, guild) => acc + guild.memberCount, 0);
|
||||||
|
*/
|
||||||
|
reduce<T>(fn: (accumulator: T, value: V, key: K, collection: this) => T, initialValue?: T): T;
|
||||||
|
/**
|
||||||
|
* Identical to
|
||||||
|
* [Map.forEach()](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Map/forEach),
|
||||||
|
* but returns the collection instead of undefined.
|
||||||
|
* @param {Function} fn Function to execute for each element
|
||||||
|
* @param {*} [thisArg] Value to use as `this` when executing function
|
||||||
|
* @returns {Collection}
|
||||||
|
* @example
|
||||||
|
* collection
|
||||||
|
* .each(user => console.log(user.username))
|
||||||
|
* .filter(user => user.bot)
|
||||||
|
* .each(user => console.log(user.username));
|
||||||
|
*/
|
||||||
|
each(fn: (value: V, key: K, collection: this) => void): this;
|
||||||
|
each<T>(fn: (this: T, value: V, key: K, collection: this) => void, thisArg: T): this;
|
||||||
|
/**
|
||||||
|
* Runs a function on the collection and returns the collection.
|
||||||
|
* @param {Function} fn Function to execute
|
||||||
|
* @param {*} [thisArg] Value to use as `this` when executing function
|
||||||
|
* @returns {Collection}
|
||||||
|
* @example
|
||||||
|
* collection
|
||||||
|
* .tap(coll => console.log(coll.size))
|
||||||
|
* .filter(user => user.bot)
|
||||||
|
* .tap(coll => console.log(coll.size))
|
||||||
|
*/
|
||||||
|
tap(fn: (collection: this) => void): this;
|
||||||
|
tap<T>(fn: (this: T, collection: this) => void, thisArg: T): this;
|
||||||
|
/**
|
||||||
|
* Creates an identical shallow copy of this collection.
|
||||||
|
* @returns {Collection}
|
||||||
|
* @example const newColl = someColl.clone();
|
||||||
|
*/
|
||||||
|
clone(): this;
|
||||||
|
/**
|
||||||
|
* Combines this collection with others into a new collection. None of the source collections are modified.
|
||||||
|
* @param {...Collection} collections Collections to merge
|
||||||
|
* @returns {Collection}
|
||||||
|
* @example const newColl = someColl.concat(someOtherColl, anotherColl, ohBoyAColl);
|
||||||
|
*/
|
||||||
|
concat(...collections: Collection<K, V>[]): this;
|
||||||
|
/**
|
||||||
|
* Checks if this collection shares identical items with another.
|
||||||
|
* This is different to checking for equality using equal-signs, because
|
||||||
|
* the collections may be different objects, but contain the same data.
|
||||||
|
* @param {Collection} collection Collection to compare with
|
||||||
|
* @returns {boolean} Whether the collections have identical contents
|
||||||
|
*/
|
||||||
|
equals(collection: Collection<K, V>): boolean;
|
||||||
|
/**
|
||||||
|
* The sort method sorts the items of a collection in place and returns it.
|
||||||
|
* The sort is not necessarily stable in Node 10 or older.
|
||||||
|
* The default sort order is according to string Unicode code points.
|
||||||
|
* @param {Function} [compareFunction] Specifies a function that defines the sort order.
|
||||||
|
* If omitted, the collection is sorted according to each character's Unicode code point value,
|
||||||
|
* according to the string conversion of each element.
|
||||||
|
* @returns {Collection}
|
||||||
|
* @example collection.sort((userA, userB) => userA.createdTimestamp - userB.createdTimestamp);
|
||||||
|
*/
|
||||||
|
sort(compareFunction?: (firstValue: V, secondValue: V, firstKey: K, secondKey: K) => number): this;
|
||||||
|
/**
|
||||||
|
* The intersect method returns a new structure containing items where the keys are present in both original structures.
|
||||||
|
* @param {Collection} other The other Collection to filter against
|
||||||
|
* @returns {Collection}
|
||||||
|
*/
|
||||||
|
intersect(other: Collection<K, V>): Collection<K, V>;
|
||||||
|
/**
|
||||||
|
* The difference method returns a new structure containing items where the key is present in one of the original structures but not the other.
|
||||||
|
* @param {Collection} other The other Collection to filter against
|
||||||
|
* @returns {Collection}
|
||||||
|
*/
|
||||||
|
difference(other: Collection<K, V>): Collection<K, V>;
|
||||||
|
/**
|
||||||
|
* The sorted method sorts the items of a collection and returns it.
|
||||||
|
* The sort is not necessarily stable in Node 10 or older.
|
||||||
|
* The default sort order is according to string Unicode code points.
|
||||||
|
* @param {Function} [compareFunction] Specifies a function that defines the sort order.
|
||||||
|
* If omitted, the collection is sorted according to each character's Unicode code point value,
|
||||||
|
* according to the string conversion of each element.
|
||||||
|
* @returns {Collection}
|
||||||
|
* @example collection.sorted((userA, userB) => userA.createdTimestamp - userB.createdTimestamp);
|
||||||
|
*/
|
||||||
|
sorted(compareFunction?: (firstValue: V, secondValue: V, firstKey: K, secondKey: K) => number): this;
|
||||||
|
}
|
||||||
|
export { Collection };
|
||||||
|
export default Collection;
|
File diff suppressed because one or more lines are too long
|
@ -0,0 +1,49 @@
|
||||||
|
{
|
||||||
|
"name": "@discordjs/collection",
|
||||||
|
"version": "0.1.6",
|
||||||
|
"description": "Utility data structure used in Discord.js",
|
||||||
|
"main": "dist/index.js",
|
||||||
|
"types": "dist/index.d.ts",
|
||||||
|
"scripts": {
|
||||||
|
"lint": "eslint src --ext .ts",
|
||||||
|
"prebuild": "npm run lint",
|
||||||
|
"build": "rimraf dist/ && tsc",
|
||||||
|
"pretest": "npm run build",
|
||||||
|
"test": "node test/index.js",
|
||||||
|
"docs": "docgen --jsdoc jsdoc.json --source src/*.ts src/**/*.ts --custom docs/index.yml --output docs/docs.json",
|
||||||
|
"docs:test": "docgen --jsdoc jsdoc.json --source src/*.ts src/**/*.ts --custom docs/index.yml"
|
||||||
|
},
|
||||||
|
"repository": {
|
||||||
|
"type": "git",
|
||||||
|
"url": "git+https://github.com/discordjs/collection.git"
|
||||||
|
},
|
||||||
|
"keywords": [
|
||||||
|
"map",
|
||||||
|
"collection",
|
||||||
|
"utility"
|
||||||
|
],
|
||||||
|
"author": "Amish Shah <amishshah.2k@gmail.com>",
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"bugs": {
|
||||||
|
"url": "https://github.com/discordjs/collection/issues"
|
||||||
|
},
|
||||||
|
"homepage": "https://github.com/discordjs/collection#readme",
|
||||||
|
"devDependencies": {
|
||||||
|
"@babel/cli": "^7.8.4",
|
||||||
|
"@babel/core": "^7.8.4",
|
||||||
|
"@babel/preset-env": "^7.8.4",
|
||||||
|
"@babel/preset-typescript": "^7.8.3",
|
||||||
|
"@types/node": "^13.7.4",
|
||||||
|
"@typescript-eslint/eslint-plugin": "^2.21.0",
|
||||||
|
"@typescript-eslint/parser": "^2.21.0",
|
||||||
|
"discord.js-docgen": "discordjs/docgen#ts-patch",
|
||||||
|
"eslint": "^6.8.0",
|
||||||
|
"eslint-config-marine": "^6.0.0",
|
||||||
|
"jsdoc-babel": "^0.5.0",
|
||||||
|
"rimraf": "^3.0.2",
|
||||||
|
"typescript": "^3.8.2"
|
||||||
|
},
|
||||||
|
"eslintConfig": {
|
||||||
|
"extends": "marine/node"
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,19 @@
|
||||||
|
Copyright (c) 2012 Felix Geisendörfer (felix@debuggable.com) and contributors
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is
|
||||||
|
furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in
|
||||||
|
all copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||||
|
THE SOFTWARE.
|
|
@ -0,0 +1,353 @@
|
||||||
|
# Form-Data [![NPM Module](https://img.shields.io/npm/v/form-data.svg)](https://www.npmjs.com/package/form-data) [![Join the chat at https://gitter.im/form-data/form-data](http://form-data.github.io/images/gitterbadge.svg)](https://gitter.im/form-data/form-data)
|
||||||
|
|
||||||
|
A library to create readable ```"multipart/form-data"``` streams. Can be used to submit forms and file uploads to other web applications.
|
||||||
|
|
||||||
|
The API of this library is inspired by the [XMLHttpRequest-2 FormData Interface][xhr2-fd].
|
||||||
|
|
||||||
|
[xhr2-fd]: http://dev.w3.org/2006/webapi/XMLHttpRequest-2/Overview.html#the-formdata-interface
|
||||||
|
|
||||||
|
[![Linux Build](https://img.shields.io/travis/form-data/form-data/master.svg?label=linux:6.x-12.x)](https://travis-ci.org/form-data/form-data)
|
||||||
|
[![MacOS Build](https://img.shields.io/travis/form-data/form-data/master.svg?label=macos:6.x-12.x)](https://travis-ci.org/form-data/form-data)
|
||||||
|
[![Windows Build](https://img.shields.io/travis/form-data/form-data/master.svg?label=windows:6.x-12.x)](https://travis-ci.org/form-data/form-data)
|
||||||
|
|
||||||
|
[![Coverage Status](https://img.shields.io/coveralls/form-data/form-data/master.svg?label=code+coverage)](https://coveralls.io/github/form-data/form-data?branch=master)
|
||||||
|
[![Dependency Status](https://img.shields.io/david/form-data/form-data.svg)](https://david-dm.org/form-data/form-data)
|
||||||
|
|
||||||
|
## Install
|
||||||
|
|
||||||
|
```
|
||||||
|
npm install --save form-data
|
||||||
|
```
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
In this example we are constructing a form with 3 fields that contain a string,
|
||||||
|
a buffer and a file stream.
|
||||||
|
|
||||||
|
``` javascript
|
||||||
|
var FormData = require('form-data');
|
||||||
|
var fs = require('fs');
|
||||||
|
|
||||||
|
var form = new FormData();
|
||||||
|
form.append('my_field', 'my value');
|
||||||
|
form.append('my_buffer', new Buffer(10));
|
||||||
|
form.append('my_file', fs.createReadStream('/foo/bar.jpg'));
|
||||||
|
```
|
||||||
|
|
||||||
|
Also you can use http-response stream:
|
||||||
|
|
||||||
|
``` javascript
|
||||||
|
var FormData = require('form-data');
|
||||||
|
var http = require('http');
|
||||||
|
|
||||||
|
var form = new FormData();
|
||||||
|
|
||||||
|
http.request('http://nodejs.org/images/logo.png', function(response) {
|
||||||
|
form.append('my_field', 'my value');
|
||||||
|
form.append('my_buffer', new Buffer(10));
|
||||||
|
form.append('my_logo', response);
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
Or @mikeal's [request](https://github.com/request/request) stream:
|
||||||
|
|
||||||
|
``` javascript
|
||||||
|
var FormData = require('form-data');
|
||||||
|
var request = require('request');
|
||||||
|
|
||||||
|
var form = new FormData();
|
||||||
|
|
||||||
|
form.append('my_field', 'my value');
|
||||||
|
form.append('my_buffer', new Buffer(10));
|
||||||
|
form.append('my_logo', request('http://nodejs.org/images/logo.png'));
|
||||||
|
```
|
||||||
|
|
||||||
|
In order to submit this form to a web application, call ```submit(url, [callback])``` method:
|
||||||
|
|
||||||
|
``` javascript
|
||||||
|
form.submit('http://example.org/', function(err, res) {
|
||||||
|
// res – response object (http.IncomingMessage) //
|
||||||
|
res.resume();
|
||||||
|
});
|
||||||
|
|
||||||
|
```
|
||||||
|
|
||||||
|
For more advanced request manipulations ```submit()``` method returns ```http.ClientRequest``` object, or you can choose from one of the alternative submission methods.
|
||||||
|
|
||||||
|
### Custom options
|
||||||
|
|
||||||
|
You can provide custom options, such as `maxDataSize`:
|
||||||
|
|
||||||
|
``` javascript
|
||||||
|
var FormData = require('form-data');
|
||||||
|
|
||||||
|
var form = new FormData({ maxDataSize: 20971520 });
|
||||||
|
form.append('my_field', 'my value');
|
||||||
|
form.append('my_buffer', /* something big */);
|
||||||
|
```
|
||||||
|
|
||||||
|
List of available options could be found in [combined-stream](https://github.com/felixge/node-combined-stream/blob/master/lib/combined_stream.js#L7-L15)
|
||||||
|
|
||||||
|
### Alternative submission methods
|
||||||
|
|
||||||
|
You can use node's http client interface:
|
||||||
|
|
||||||
|
``` javascript
|
||||||
|
var http = require('http');
|
||||||
|
|
||||||
|
var request = http.request({
|
||||||
|
method: 'post',
|
||||||
|
host: 'example.org',
|
||||||
|
path: '/upload',
|
||||||
|
headers: form.getHeaders()
|
||||||
|
});
|
||||||
|
|
||||||
|
form.pipe(request);
|
||||||
|
|
||||||
|
request.on('response', function(res) {
|
||||||
|
console.log(res.statusCode);
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
Or if you would prefer the `'Content-Length'` header to be set for you:
|
||||||
|
|
||||||
|
``` javascript
|
||||||
|
form.submit('example.org/upload', function(err, res) {
|
||||||
|
console.log(res.statusCode);
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
To use custom headers and pre-known length in parts:
|
||||||
|
|
||||||
|
``` javascript
|
||||||
|
var CRLF = '\r\n';
|
||||||
|
var form = new FormData();
|
||||||
|
|
||||||
|
var options = {
|
||||||
|
header: CRLF + '--' + form.getBoundary() + CRLF + 'X-Custom-Header: 123' + CRLF + CRLF,
|
||||||
|
knownLength: 1
|
||||||
|
};
|
||||||
|
|
||||||
|
form.append('my_buffer', buffer, options);
|
||||||
|
|
||||||
|
form.submit('http://example.com/', function(err, res) {
|
||||||
|
if (err) throw err;
|
||||||
|
console.log('Done');
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
Form-Data can recognize and fetch all the required information from common types of streams (```fs.readStream```, ```http.response``` and ```mikeal's request```), for some other types of streams you'd need to provide "file"-related information manually:
|
||||||
|
|
||||||
|
``` javascript
|
||||||
|
someModule.stream(function(err, stdout, stderr) {
|
||||||
|
if (err) throw err;
|
||||||
|
|
||||||
|
var form = new FormData();
|
||||||
|
|
||||||
|
form.append('file', stdout, {
|
||||||
|
filename: 'unicycle.jpg', // ... or:
|
||||||
|
filepath: 'photos/toys/unicycle.jpg',
|
||||||
|
contentType: 'image/jpeg',
|
||||||
|
knownLength: 19806
|
||||||
|
});
|
||||||
|
|
||||||
|
form.submit('http://example.com/', function(err, res) {
|
||||||
|
if (err) throw err;
|
||||||
|
console.log('Done');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
The `filepath` property overrides `filename` and may contain a relative path. This is typically used when uploading [multiple files from a directory](https://wicg.github.io/entries-api/#dom-htmlinputelement-webkitdirectory).
|
||||||
|
|
||||||
|
For edge cases, like POST request to URL with query string or to pass HTTP auth credentials, object can be passed to `form.submit()` as first parameter:
|
||||||
|
|
||||||
|
``` javascript
|
||||||
|
form.submit({
|
||||||
|
host: 'example.com',
|
||||||
|
path: '/probably.php?extra=params',
|
||||||
|
auth: 'username:password'
|
||||||
|
}, function(err, res) {
|
||||||
|
console.log(res.statusCode);
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
In case you need to also send custom HTTP headers with the POST request, you can use the `headers` key in first parameter of `form.submit()`:
|
||||||
|
|
||||||
|
``` javascript
|
||||||
|
form.submit({
|
||||||
|
host: 'example.com',
|
||||||
|
path: '/surelynot.php',
|
||||||
|
headers: {'x-test-header': 'test-header-value'}
|
||||||
|
}, function(err, res) {
|
||||||
|
console.log(res.statusCode);
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
### Methods
|
||||||
|
|
||||||
|
- [_Void_ append( **String** _field_, **Mixed** _value_ [, **Mixed** _options_] )](https://github.com/form-data/form-data#void-append-string-field-mixed-value--mixed-options-).
|
||||||
|
- [_Headers_ getHeaders( [**Headers** _userHeaders_] )](https://github.com/form-data/form-data#array-getheaders-array-userheaders-)
|
||||||
|
- [_String_ getBoundary()](https://github.com/form-data/form-data#string-getboundary)
|
||||||
|
- [_Buffer_ getBuffer()](https://github.com/form-data/form-data#buffer-getbuffer)
|
||||||
|
- [_Integer_ getLengthSync()](https://github.com/form-data/form-data#integer-getlengthsync)
|
||||||
|
- [_Integer_ getLength( **function** _callback_ )](https://github.com/form-data/form-data#integer-getlength-function-callback-)
|
||||||
|
- [_Boolean_ hasKnownLength()](https://github.com/form-data/form-data#boolean-hasknownlength)
|
||||||
|
- [_Request_ submit( _params_, **function** _callback_ )](https://github.com/form-data/form-data#request-submit-params-function-callback-)
|
||||||
|
- [_String_ toString()](https://github.com/form-data/form-data#string-tostring)
|
||||||
|
|
||||||
|
#### _Void_ append( **String** _field_, **Mixed** _value_ [, **Mixed** _options_] )
|
||||||
|
Append data to the form. You can submit about any format (string, integer, boolean, buffer, etc.). However, Arrays are not supported and need to be turned into strings by the user.
|
||||||
|
```javascript
|
||||||
|
var form = new FormData();
|
||||||
|
form.append( 'my_string', 'my value' );
|
||||||
|
form.append( 'my_integer', 1 );
|
||||||
|
form.append( 'my_boolean', true );
|
||||||
|
form.append( 'my_buffer', new Buffer(10) );
|
||||||
|
form.append( 'my_array_as_json', JSON.stringify( ['bird','cute'] ) )
|
||||||
|
```
|
||||||
|
|
||||||
|
You may provide a string for options, or an object.
|
||||||
|
```javascript
|
||||||
|
// Set filename by providing a string for options
|
||||||
|
form.append( 'my_file', fs.createReadStream('/foo/bar.jpg'), 'bar.jpg' );
|
||||||
|
|
||||||
|
// provide an object.
|
||||||
|
form.append( 'my_file', fs.createReadStream('/foo/bar.jpg'), {filename: 'bar.jpg', contentType: 'image/jpeg', knownLength: 19806} );
|
||||||
|
```
|
||||||
|
|
||||||
|
#### _Headers_ getHeaders( [**Headers** _userHeaders_] )
|
||||||
|
This method adds the correct `content-type` header to the provided array of `userHeaders`.
|
||||||
|
|
||||||
|
#### _String_ getBoundary()
|
||||||
|
Return the boundary of the formData. A boundary consists of 26 `-` followed by 24 numbers
|
||||||
|
for example:
|
||||||
|
```javascript
|
||||||
|
--------------------------515890814546601021194782
|
||||||
|
```
|
||||||
|
_Note: The boundary must be unique and may not appear in the data._
|
||||||
|
|
||||||
|
#### _Buffer_ getBuffer()
|
||||||
|
Return the full formdata request package, as a Buffer. You can insert this Buffer in e.g. Axios to send multipart data.
|
||||||
|
```javascript
|
||||||
|
var form = new FormData();
|
||||||
|
form.append( 'my_buffer', Buffer.from([0x4a,0x42,0x20,0x52,0x6f,0x63,0x6b,0x73]) );
|
||||||
|
form.append( 'my_file', fs.readFileSync('/foo/bar.jpg') );
|
||||||
|
|
||||||
|
axios.post( 'https://example.com/path/to/api',
|
||||||
|
form.getBuffer(),
|
||||||
|
form.getHeaders()
|
||||||
|
)
|
||||||
|
```
|
||||||
|
**Note:** Because the output is of type Buffer, you can only append types that are accepted by Buffer: *string, Buffer, ArrayBuffer, Array, or Array-like Object*. A ReadStream for example will result in an error.
|
||||||
|
|
||||||
|
#### _Integer_ getLengthSync()
|
||||||
|
Same as `getLength` but synchronous.
|
||||||
|
|
||||||
|
_Note: getLengthSync __doesn't__ calculate streams length._
|
||||||
|
|
||||||
|
#### _Integer_ getLength( **function** _callback_ )
|
||||||
|
Returns the `Content-Length` async. The callback is used to handle errors and continue once the length has been calculated
|
||||||
|
```javascript
|
||||||
|
this.getLength(function(err, length) {
|
||||||
|
if (err) {
|
||||||
|
this._error(err);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// add content length
|
||||||
|
request.setHeader('Content-Length', length);
|
||||||
|
|
||||||
|
...
|
||||||
|
}.bind(this));
|
||||||
|
```
|
||||||
|
|
||||||
|
#### _Boolean_ hasKnownLength()
|
||||||
|
Checks if the length of added values is known.
|
||||||
|
|
||||||
|
#### _Request_ submit( _params_, **function** _callback_ )
|
||||||
|
Submit the form to a web application.
|
||||||
|
```javascript
|
||||||
|
var form = new FormData();
|
||||||
|
form.append( 'my_string', 'Hello World' );
|
||||||
|
|
||||||
|
form.submit( 'http://example.com/', function(err, res) {
|
||||||
|
// res – response object (http.IncomingMessage) //
|
||||||
|
res.resume();
|
||||||
|
} );
|
||||||
|
```
|
||||||
|
|
||||||
|
#### _String_ toString()
|
||||||
|
Returns the form data as a string. Don't use this if you are sending files or buffers, use `getBuffer()` instead.
|
||||||
|
|
||||||
|
### Integration with other libraries
|
||||||
|
|
||||||
|
#### Request
|
||||||
|
|
||||||
|
Form submission using [request](https://github.com/request/request):
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
var formData = {
|
||||||
|
my_field: 'my_value',
|
||||||
|
my_file: fs.createReadStream(__dirname + '/unicycle.jpg'),
|
||||||
|
};
|
||||||
|
|
||||||
|
request.post({url:'http://service.com/upload', formData: formData}, function(err, httpResponse, body) {
|
||||||
|
if (err) {
|
||||||
|
return console.error('upload failed:', err);
|
||||||
|
}
|
||||||
|
console.log('Upload successful! Server responded with:', body);
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
For more details see [request readme](https://github.com/request/request#multipartform-data-multipart-form-uploads).
|
||||||
|
|
||||||
|
#### node-fetch
|
||||||
|
|
||||||
|
You can also submit a form using [node-fetch](https://github.com/bitinn/node-fetch):
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
var form = new FormData();
|
||||||
|
|
||||||
|
form.append('a', 1);
|
||||||
|
|
||||||
|
fetch('http://example.com', { method: 'POST', body: form })
|
||||||
|
.then(function(res) {
|
||||||
|
return res.json();
|
||||||
|
}).then(function(json) {
|
||||||
|
console.log(json);
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
#### axios
|
||||||
|
|
||||||
|
In Node.js you can post a file using [axios](https://github.com/axios/axios):
|
||||||
|
```javascript
|
||||||
|
const form = new FormData();
|
||||||
|
const stream = fs.createReadStream(PATH_TO_FILE);
|
||||||
|
|
||||||
|
form.append('image', stream);
|
||||||
|
|
||||||
|
// In Node.js environment you need to set boundary in the header field 'Content-Type' by calling method `getHeaders`
|
||||||
|
const formHeaders = form.getHeaders();
|
||||||
|
|
||||||
|
axios.post('http://example.com', form, {
|
||||||
|
headers: {
|
||||||
|
...formHeaders,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
.then(response => response)
|
||||||
|
.catch(error => error)
|
||||||
|
```
|
||||||
|
|
||||||
|
## Notes
|
||||||
|
|
||||||
|
- ```getLengthSync()``` method DOESN'T calculate length for streams, use ```knownLength``` options as workaround.
|
||||||
|
- ```getLength(cb)``` will send an error as first parameter of callback if stream length cannot be calculated (e.g. send in custom streams w/o using ```knownLength```).
|
||||||
|
- ```sbumit``` will not add `content-length` if form length is unknown or not calculable.
|
||||||
|
- Starting version `2.x` FormData has dropped support for `node@0.10.x`.
|
||||||
|
- Starting version `3.x` FormData has dropped support for `node@4.x`.
|
||||||
|
|
||||||
|
## License
|
||||||
|
|
||||||
|
Form-Data is released under the [MIT](License) license.
|
|
@ -0,0 +1,61 @@
|
||||||
|
// Definitions by: Carlos Ballesteros Velasco <https://github.com/soywiz>
|
||||||
|
// Leon Yu <https://github.com/leonyu>
|
||||||
|
// BendingBender <https://github.com/BendingBender>
|
||||||
|
// Maple Miao <https://github.com/mapleeit>
|
||||||
|
|
||||||
|
/// <reference types="node" />
|
||||||
|
import * as stream from 'stream';
|
||||||
|
import * as http from 'http';
|
||||||
|
|
||||||
|
export = FormData;
|
||||||
|
|
||||||
|
// Extracted because @types/node doesn't export interfaces.
|
||||||
|
interface ReadableOptions {
|
||||||
|
highWaterMark?: number;
|
||||||
|
encoding?: string;
|
||||||
|
objectMode?: boolean;
|
||||||
|
read?(this: stream.Readable, size: number): void;
|
||||||
|
destroy?(this: stream.Readable, error: Error | null, callback: (error: Error | null) => void): void;
|
||||||
|
autoDestroy?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Options extends ReadableOptions {
|
||||||
|
writable?: boolean;
|
||||||
|
readable?: boolean;
|
||||||
|
dataSize?: number;
|
||||||
|
maxDataSize?: number;
|
||||||
|
pauseStreams?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
declare class FormData extends stream.Readable {
|
||||||
|
constructor(options?: Options);
|
||||||
|
append(key: string, value: any, options?: FormData.AppendOptions | string): void;
|
||||||
|
getHeaders(userHeaders?: FormData.Headers): FormData.Headers;
|
||||||
|
submit(
|
||||||
|
params: string | FormData.SubmitOptions,
|
||||||
|
callback?: (error: Error | null, response: http.IncomingMessage) => void
|
||||||
|
): http.ClientRequest;
|
||||||
|
getBuffer(): Buffer;
|
||||||
|
getBoundary(): string;
|
||||||
|
getLength(callback: (err: Error | null, length: number) => void): void;
|
||||||
|
getLengthSync(): number;
|
||||||
|
hasKnownLength(): boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
declare namespace FormData {
|
||||||
|
interface Headers {
|
||||||
|
[key: string]: any;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface AppendOptions {
|
||||||
|
header?: string | Headers;
|
||||||
|
knownLength?: number;
|
||||||
|
filename?: string;
|
||||||
|
filepath?: string;
|
||||||
|
contentType?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface SubmitOptions extends http.RequestOptions {
|
||||||
|
protocol?: 'https:' | 'http:';
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,2 @@
|
||||||
|
/* eslint-env browser */
|
||||||
|
module.exports = typeof self == 'object' ? self.FormData : window.FormData;
|
|
@ -0,0 +1,497 @@
|
||||||
|
var CombinedStream = require('combined-stream');
|
||||||
|
var util = require('util');
|
||||||
|
var path = require('path');
|
||||||
|
var http = require('http');
|
||||||
|
var https = require('https');
|
||||||
|
var parseUrl = require('url').parse;
|
||||||
|
var fs = require('fs');
|
||||||
|
var Stream = require('stream').Stream;
|
||||||
|
var mime = require('mime-types');
|
||||||
|
var asynckit = require('asynckit');
|
||||||
|
var populate = require('./populate.js');
|
||||||
|
|
||||||
|
// Public API
|
||||||
|
module.exports = FormData;
|
||||||
|
|
||||||
|
// make it a Stream
|
||||||
|
util.inherits(FormData, CombinedStream);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create readable "multipart/form-data" streams.
|
||||||
|
* Can be used to submit forms
|
||||||
|
* and file uploads to other web applications.
|
||||||
|
*
|
||||||
|
* @constructor
|
||||||
|
* @param {Object} options - Properties to be added/overriden for FormData and CombinedStream
|
||||||
|
*/
|
||||||
|
function FormData(options) {
|
||||||
|
if (!(this instanceof FormData)) {
|
||||||
|
return new FormData(options);
|
||||||
|
}
|
||||||
|
|
||||||
|
this._overheadLength = 0;
|
||||||
|
this._valueLength = 0;
|
||||||
|
this._valuesToMeasure = [];
|
||||||
|
|
||||||
|
CombinedStream.call(this);
|
||||||
|
|
||||||
|
options = options || {};
|
||||||
|
for (var option in options) {
|
||||||
|
this[option] = options[option];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
FormData.LINE_BREAK = '\r\n';
|
||||||
|
FormData.DEFAULT_CONTENT_TYPE = 'application/octet-stream';
|
||||||
|
|
||||||
|
FormData.prototype.append = function(field, value, options) {
|
||||||
|
|
||||||
|
options = options || {};
|
||||||
|
|
||||||
|
// allow filename as single option
|
||||||
|
if (typeof options == 'string') {
|
||||||
|
options = {filename: options};
|
||||||
|
}
|
||||||
|
|
||||||
|
var append = CombinedStream.prototype.append.bind(this);
|
||||||
|
|
||||||
|
// all that streamy business can't handle numbers
|
||||||
|
if (typeof value == 'number') {
|
||||||
|
value = '' + value;
|
||||||
|
}
|
||||||
|
|
||||||
|
// https://github.com/felixge/node-form-data/issues/38
|
||||||
|
if (util.isArray(value)) {
|
||||||
|
// Please convert your array into string
|
||||||
|
// the way web server expects it
|
||||||
|
this._error(new Error('Arrays are not supported.'));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var header = this._multiPartHeader(field, value, options);
|
||||||
|
var footer = this._multiPartFooter();
|
||||||
|
|
||||||
|
append(header);
|
||||||
|
append(value);
|
||||||
|
append(footer);
|
||||||
|
|
||||||
|
// pass along options.knownLength
|
||||||
|
this._trackLength(header, value, options);
|
||||||
|
};
|
||||||
|
|
||||||
|
FormData.prototype._trackLength = function(header, value, options) {
|
||||||
|
var valueLength = 0;
|
||||||
|
|
||||||
|
// used w/ getLengthSync(), when length is known.
|
||||||
|
// e.g. for streaming directly from a remote server,
|
||||||
|
// w/ a known file a size, and not wanting to wait for
|
||||||
|
// incoming file to finish to get its size.
|
||||||
|
if (options.knownLength != null) {
|
||||||
|
valueLength += +options.knownLength;
|
||||||
|
} else if (Buffer.isBuffer(value)) {
|
||||||
|
valueLength = value.length;
|
||||||
|
} else if (typeof value === 'string') {
|
||||||
|
valueLength = Buffer.byteLength(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
this._valueLength += valueLength;
|
||||||
|
|
||||||
|
// @check why add CRLF? does this account for custom/multiple CRLFs?
|
||||||
|
this._overheadLength +=
|
||||||
|
Buffer.byteLength(header) +
|
||||||
|
FormData.LINE_BREAK.length;
|
||||||
|
|
||||||
|
// empty or either doesn't have path or not an http response or not a stream
|
||||||
|
if (!value || ( !value.path && !(value.readable && value.hasOwnProperty('httpVersion')) && !(value instanceof Stream))) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// no need to bother with the length
|
||||||
|
if (!options.knownLength) {
|
||||||
|
this._valuesToMeasure.push(value);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
FormData.prototype._lengthRetriever = function(value, callback) {
|
||||||
|
|
||||||
|
if (value.hasOwnProperty('fd')) {
|
||||||
|
|
||||||
|
// take read range into a account
|
||||||
|
// `end` = Infinity –> read file till the end
|
||||||
|
//
|
||||||
|
// TODO: Looks like there is bug in Node fs.createReadStream
|
||||||
|
// it doesn't respect `end` options without `start` options
|
||||||
|
// Fix it when node fixes it.
|
||||||
|
// https://github.com/joyent/node/issues/7819
|
||||||
|
if (value.end != undefined && value.end != Infinity && value.start != undefined) {
|
||||||
|
|
||||||
|
// when end specified
|
||||||
|
// no need to calculate range
|
||||||
|
// inclusive, starts with 0
|
||||||
|
callback(null, value.end + 1 - (value.start ? value.start : 0));
|
||||||
|
|
||||||
|
// not that fast snoopy
|
||||||
|
} else {
|
||||||
|
// still need to fetch file size from fs
|
||||||
|
fs.stat(value.path, function(err, stat) {
|
||||||
|
|
||||||
|
var fileSize;
|
||||||
|
|
||||||
|
if (err) {
|
||||||
|
callback(err);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// update final size based on the range options
|
||||||
|
fileSize = stat.size - (value.start ? value.start : 0);
|
||||||
|
callback(null, fileSize);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// or http response
|
||||||
|
} else if (value.hasOwnProperty('httpVersion')) {
|
||||||
|
callback(null, +value.headers['content-length']);
|
||||||
|
|
||||||
|
// or request stream http://github.com/mikeal/request
|
||||||
|
} else if (value.hasOwnProperty('httpModule')) {
|
||||||
|
// wait till response come back
|
||||||
|
value.on('response', function(response) {
|
||||||
|
value.pause();
|
||||||
|
callback(null, +response.headers['content-length']);
|
||||||
|
});
|
||||||
|
value.resume();
|
||||||
|
|
||||||
|
// something else
|
||||||
|
} else {
|
||||||
|
callback('Unknown stream');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
FormData.prototype._multiPartHeader = function(field, value, options) {
|
||||||
|
// custom header specified (as string)?
|
||||||
|
// it becomes responsible for boundary
|
||||||
|
// (e.g. to handle extra CRLFs on .NET servers)
|
||||||
|
if (typeof options.header == 'string') {
|
||||||
|
return options.header;
|
||||||
|
}
|
||||||
|
|
||||||
|
var contentDisposition = this._getContentDisposition(value, options);
|
||||||
|
var contentType = this._getContentType(value, options);
|
||||||
|
|
||||||
|
var contents = '';
|
||||||
|
var headers = {
|
||||||
|
// add custom disposition as third element or keep it two elements if not
|
||||||
|
'Content-Disposition': ['form-data', 'name="' + field + '"'].concat(contentDisposition || []),
|
||||||
|
// if no content type. allow it to be empty array
|
||||||
|
'Content-Type': [].concat(contentType || [])
|
||||||
|
};
|
||||||
|
|
||||||
|
// allow custom headers.
|
||||||
|
if (typeof options.header == 'object') {
|
||||||
|
populate(headers, options.header);
|
||||||
|
}
|
||||||
|
|
||||||
|
var header;
|
||||||
|
for (var prop in headers) {
|
||||||
|
if (!headers.hasOwnProperty(prop)) continue;
|
||||||
|
header = headers[prop];
|
||||||
|
|
||||||
|
// skip nullish headers.
|
||||||
|
if (header == null) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// convert all headers to arrays.
|
||||||
|
if (!Array.isArray(header)) {
|
||||||
|
header = [header];
|
||||||
|
}
|
||||||
|
|
||||||
|
// add non-empty headers.
|
||||||
|
if (header.length) {
|
||||||
|
contents += prop + ': ' + header.join('; ') + FormData.LINE_BREAK;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return '--' + this.getBoundary() + FormData.LINE_BREAK + contents + FormData.LINE_BREAK;
|
||||||
|
};
|
||||||
|
|
||||||
|
FormData.prototype._getContentDisposition = function(value, options) {
|
||||||
|
|
||||||
|
var filename
|
||||||
|
, contentDisposition
|
||||||
|
;
|
||||||
|
|
||||||
|
if (typeof options.filepath === 'string') {
|
||||||
|
// custom filepath for relative paths
|
||||||
|
filename = path.normalize(options.filepath).replace(/\\/g, '/');
|
||||||
|
} else if (options.filename || value.name || value.path) {
|
||||||
|
// custom filename take precedence
|
||||||
|
// formidable and the browser add a name property
|
||||||
|
// fs- and request- streams have path property
|
||||||
|
filename = path.basename(options.filename || value.name || value.path);
|
||||||
|
} else if (value.readable && value.hasOwnProperty('httpVersion')) {
|
||||||
|
// or try http response
|
||||||
|
filename = path.basename(value.client._httpMessage.path || '');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (filename) {
|
||||||
|
contentDisposition = 'filename="' + filename + '"';
|
||||||
|
}
|
||||||
|
|
||||||
|
return contentDisposition;
|
||||||
|
};
|
||||||
|
|
||||||
|
FormData.prototype._getContentType = function(value, options) {
|
||||||
|
|
||||||
|
// use custom content-type above all
|
||||||
|
var contentType = options.contentType;
|
||||||
|
|
||||||
|
// or try `name` from formidable, browser
|
||||||
|
if (!contentType && value.name) {
|
||||||
|
contentType = mime.lookup(value.name);
|
||||||
|
}
|
||||||
|
|
||||||
|
// or try `path` from fs-, request- streams
|
||||||
|
if (!contentType && value.path) {
|
||||||
|
contentType = mime.lookup(value.path);
|
||||||
|
}
|
||||||
|
|
||||||
|
// or if it's http-reponse
|
||||||
|
if (!contentType && value.readable && value.hasOwnProperty('httpVersion')) {
|
||||||
|
contentType = value.headers['content-type'];
|
||||||
|
}
|
||||||
|
|
||||||
|
// or guess it from the filepath or filename
|
||||||
|
if (!contentType && (options.filepath || options.filename)) {
|
||||||
|
contentType = mime.lookup(options.filepath || options.filename);
|
||||||
|
}
|
||||||
|
|
||||||
|
// fallback to the default content type if `value` is not simple value
|
||||||
|
if (!contentType && typeof value == 'object') {
|
||||||
|
contentType = FormData.DEFAULT_CONTENT_TYPE;
|
||||||
|
}
|
||||||
|
|
||||||
|
return contentType;
|
||||||
|
};
|
||||||
|
|
||||||
|
FormData.prototype._multiPartFooter = function() {
|
||||||
|
return function(next) {
|
||||||
|
var footer = FormData.LINE_BREAK;
|
||||||
|
|
||||||
|
var lastPart = (this._streams.length === 0);
|
||||||
|
if (lastPart) {
|
||||||
|
footer += this._lastBoundary();
|
||||||
|
}
|
||||||
|
|
||||||
|
next(footer);
|
||||||
|
}.bind(this);
|
||||||
|
};
|
||||||
|
|
||||||
|
FormData.prototype._lastBoundary = function() {
|
||||||
|
return '--' + this.getBoundary() + '--' + FormData.LINE_BREAK;
|
||||||
|
};
|
||||||
|
|
||||||
|
FormData.prototype.getHeaders = function(userHeaders) {
|
||||||
|
var header;
|
||||||
|
var formHeaders = {
|
||||||
|
'content-type': 'multipart/form-data; boundary=' + this.getBoundary()
|
||||||
|
};
|
||||||
|
|
||||||
|
for (header in userHeaders) {
|
||||||
|
if (userHeaders.hasOwnProperty(header)) {
|
||||||
|
formHeaders[header.toLowerCase()] = userHeaders[header];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return formHeaders;
|
||||||
|
};
|
||||||
|
|
||||||
|
FormData.prototype.getBoundary = function() {
|
||||||
|
if (!this._boundary) {
|
||||||
|
this._generateBoundary();
|
||||||
|
}
|
||||||
|
|
||||||
|
return this._boundary;
|
||||||
|
};
|
||||||
|
|
||||||
|
FormData.prototype.getBuffer = function() {
|
||||||
|
var dataBuffer = new Buffer.alloc( 0 );
|
||||||
|
var boundary = this.getBoundary();
|
||||||
|
|
||||||
|
// Create the form content. Add Line breaks to the end of data.
|
||||||
|
for (var i = 0, len = this._streams.length; i < len; i++) {
|
||||||
|
if (typeof this._streams[i] !== 'function') {
|
||||||
|
|
||||||
|
// Add content to the buffer.
|
||||||
|
if(Buffer.isBuffer(this._streams[i])) {
|
||||||
|
dataBuffer = Buffer.concat( [dataBuffer, this._streams[i]]);
|
||||||
|
}else {
|
||||||
|
dataBuffer = Buffer.concat( [dataBuffer, Buffer.from(this._streams[i])]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add break after content.
|
||||||
|
if (typeof this._streams[i] !== 'string' || this._streams[i].substring( 2, boundary.length + 2 ) !== boundary) {
|
||||||
|
dataBuffer = Buffer.concat( [dataBuffer, Buffer.from(FormData.LINE_BREAK)] );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add the footer and return the Buffer object.
|
||||||
|
return Buffer.concat( [dataBuffer, Buffer.from(this._lastBoundary())] );
|
||||||
|
};
|
||||||
|
|
||||||
|
FormData.prototype._generateBoundary = function() {
|
||||||
|
// This generates a 50 character boundary similar to those used by Firefox.
|
||||||
|
// They are optimized for boyer-moore parsing.
|
||||||
|
var boundary = '--------------------------';
|
||||||
|
for (var i = 0; i < 24; i++) {
|
||||||
|
boundary += Math.floor(Math.random() * 10).toString(16);
|
||||||
|
}
|
||||||
|
|
||||||
|
this._boundary = boundary;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Note: getLengthSync DOESN'T calculate streams length
|
||||||
|
// As workaround one can calculate file size manually
|
||||||
|
// and add it as knownLength option
|
||||||
|
FormData.prototype.getLengthSync = function() {
|
||||||
|
var knownLength = this._overheadLength + this._valueLength;
|
||||||
|
|
||||||
|
// Don't get confused, there are 3 "internal" streams for each keyval pair
|
||||||
|
// so it basically checks if there is any value added to the form
|
||||||
|
if (this._streams.length) {
|
||||||
|
knownLength += this._lastBoundary().length;
|
||||||
|
}
|
||||||
|
|
||||||
|
// https://github.com/form-data/form-data/issues/40
|
||||||
|
if (!this.hasKnownLength()) {
|
||||||
|
// Some async length retrievers are present
|
||||||
|
// therefore synchronous length calculation is false.
|
||||||
|
// Please use getLength(callback) to get proper length
|
||||||
|
this._error(new Error('Cannot calculate proper length in synchronous way.'));
|
||||||
|
}
|
||||||
|
|
||||||
|
return knownLength;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Public API to check if length of added values is known
|
||||||
|
// https://github.com/form-data/form-data/issues/196
|
||||||
|
// https://github.com/form-data/form-data/issues/262
|
||||||
|
FormData.prototype.hasKnownLength = function() {
|
||||||
|
var hasKnownLength = true;
|
||||||
|
|
||||||
|
if (this._valuesToMeasure.length) {
|
||||||
|
hasKnownLength = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return hasKnownLength;
|
||||||
|
};
|
||||||
|
|
||||||
|
FormData.prototype.getLength = function(cb) {
|
||||||
|
var knownLength = this._overheadLength + this._valueLength;
|
||||||
|
|
||||||
|
if (this._streams.length) {
|
||||||
|
knownLength += this._lastBoundary().length;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!this._valuesToMeasure.length) {
|
||||||
|
process.nextTick(cb.bind(this, null, knownLength));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
asynckit.parallel(this._valuesToMeasure, this._lengthRetriever, function(err, values) {
|
||||||
|
if (err) {
|
||||||
|
cb(err);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
values.forEach(function(length) {
|
||||||
|
knownLength += length;
|
||||||
|
});
|
||||||
|
|
||||||
|
cb(null, knownLength);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
FormData.prototype.submit = function(params, cb) {
|
||||||
|
var request
|
||||||
|
, options
|
||||||
|
, defaults = {method: 'post'}
|
||||||
|
;
|
||||||
|
|
||||||
|
// parse provided url if it's string
|
||||||
|
// or treat it as options object
|
||||||
|
if (typeof params == 'string') {
|
||||||
|
|
||||||
|
params = parseUrl(params);
|
||||||
|
options = populate({
|
||||||
|
port: params.port,
|
||||||
|
path: params.pathname,
|
||||||
|
host: params.hostname,
|
||||||
|
protocol: params.protocol
|
||||||
|
}, defaults);
|
||||||
|
|
||||||
|
// use custom params
|
||||||
|
} else {
|
||||||
|
|
||||||
|
options = populate(params, defaults);
|
||||||
|
// if no port provided use default one
|
||||||
|
if (!options.port) {
|
||||||
|
options.port = options.protocol == 'https:' ? 443 : 80;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// put that good code in getHeaders to some use
|
||||||
|
options.headers = this.getHeaders(params.headers);
|
||||||
|
|
||||||
|
// https if specified, fallback to http in any other case
|
||||||
|
if (options.protocol == 'https:') {
|
||||||
|
request = https.request(options);
|
||||||
|
} else {
|
||||||
|
request = http.request(options);
|
||||||
|
}
|
||||||
|
|
||||||
|
// get content length and fire away
|
||||||
|
this.getLength(function(err, length) {
|
||||||
|
if (err && err !== 'Unknown stream') {
|
||||||
|
this._error(err);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// add content length
|
||||||
|
if (length) {
|
||||||
|
request.setHeader('Content-Length', length);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.pipe(request);
|
||||||
|
if (cb) {
|
||||||
|
var onResponse;
|
||||||
|
|
||||||
|
var callback = function (error, responce) {
|
||||||
|
request.removeListener('error', callback);
|
||||||
|
request.removeListener('response', onResponse);
|
||||||
|
|
||||||
|
return cb.call(this, error, responce);
|
||||||
|
};
|
||||||
|
|
||||||
|
onResponse = callback.bind(this, null);
|
||||||
|
|
||||||
|
request.on('error', callback);
|
||||||
|
request.on('response', onResponse);
|
||||||
|
}
|
||||||
|
}.bind(this));
|
||||||
|
|
||||||
|
return request;
|
||||||
|
};
|
||||||
|
|
||||||
|
FormData.prototype._error = function(err) {
|
||||||
|
if (!this.error) {
|
||||||
|
this.error = err;
|
||||||
|
this.pause();
|
||||||
|
this.emit('error', err);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
FormData.prototype.toString = function () {
|
||||||
|
return '[object FormData]';
|
||||||
|
};
|
|
@ -0,0 +1,10 @@
|
||||||
|
// populates missing values
|
||||||
|
module.exports = function(dst, src) {
|
||||||
|
|
||||||
|
Object.keys(src).forEach(function(prop)
|
||||||
|
{
|
||||||
|
dst[prop] = dst[prop] || src[prop];
|
||||||
|
});
|
||||||
|
|
||||||
|
return dst;
|
||||||
|
};
|
|
@ -0,0 +1,64 @@
|
||||||
|
{
|
||||||
|
"author": "Felix Geisendörfer <felix@debuggable.com> (http://debuggable.com/)",
|
||||||
|
"name": "@discordjs/form-data",
|
||||||
|
"description": "A library to create readable \"multipart/form-data\" streams. Can be used to submit forms and file uploads to other web applications.",
|
||||||
|
"version": "3.0.1",
|
||||||
|
"repository": {
|
||||||
|
"type": "git",
|
||||||
|
"url": "git://github.com/form-data/form-data.git"
|
||||||
|
},
|
||||||
|
"main": "./lib/form_data",
|
||||||
|
"browser": "./lib/browser",
|
||||||
|
"typings": "./index.d.ts",
|
||||||
|
"scripts": {
|
||||||
|
"pretest": "rimraf coverage test/tmp",
|
||||||
|
"test": "istanbul cover test/run.js",
|
||||||
|
"posttest": "istanbul report lcov text",
|
||||||
|
"lint": "eslint lib/*.js test/*.js test/integration/*.js",
|
||||||
|
"report": "istanbul report lcov text",
|
||||||
|
"ci-lint": "is-node-modern 8 && npm run lint || is-node-not-modern 8",
|
||||||
|
"ci-test": "npm run test && npm run browser && npm run report",
|
||||||
|
"predebug": "rimraf coverage test/tmp",
|
||||||
|
"debug": "verbose=1 ./test/run.js",
|
||||||
|
"browser": "browserify -t browserify-istanbul test/run-browser.js | obake --coverage",
|
||||||
|
"check": "istanbul check-coverage coverage/coverage*.json",
|
||||||
|
"files": "pkgfiles --sort=name",
|
||||||
|
"get-version": "node -e \"console.log(require('./package.json').version)\""
|
||||||
|
},
|
||||||
|
"pre-commit": [
|
||||||
|
"lint",
|
||||||
|
"ci-test",
|
||||||
|
"check"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 6"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"asynckit": "^0.4.0",
|
||||||
|
"combined-stream": "^1.0.8",
|
||||||
|
"mime-types": "^2.1.12"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@types/node": "^12.0.10",
|
||||||
|
"browserify": "^13.1.1",
|
||||||
|
"browserify-istanbul": "^2.0.0",
|
||||||
|
"coveralls": "^3.0.4",
|
||||||
|
"cross-spawn": "^6.0.5",
|
||||||
|
"eslint": "^6.0.1",
|
||||||
|
"fake": "^0.2.2",
|
||||||
|
"far": "^0.0.7",
|
||||||
|
"formidable": "^1.0.17",
|
||||||
|
"in-publish": "^2.0.0",
|
||||||
|
"is-node-modern": "^1.0.0",
|
||||||
|
"istanbul": "^0.4.5",
|
||||||
|
"obake": "^0.1.2",
|
||||||
|
"puppeteer": "^1.19.0",
|
||||||
|
"pkgfiles": "^2.3.0",
|
||||||
|
"pre-commit": "^1.1.3",
|
||||||
|
"request": "^2.88.0",
|
||||||
|
"rimraf": "^2.7.1",
|
||||||
|
"tape": "^4.6.2",
|
||||||
|
"typescript": "^3.5.2"
|
||||||
|
},
|
||||||
|
"license": "MIT"
|
||||||
|
}
|
|
@ -0,0 +1,3 @@
|
||||||
|
# `@napi-rs/canvas-linux-x64-gnu`
|
||||||
|
|
||||||
|
This is the **x86_64-unknown-linux-gnu** binary for `@napi-rs/canvas`
|
|
@ -0,0 +1,14 @@
|
||||||
|
// Node.js < v13.12.0, v12.17.0, ignore
|
||||||
|
// https://nodejs.org/api/process.html#processreport
|
||||||
|
if (!process.report || typeof process.report.getReport !== 'function') {
|
||||||
|
process.exit(0)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Only GNU system has this field
|
||||||
|
const { glibcVersionRuntime } = process.report.getReport().header
|
||||||
|
|
||||||
|
if (glibcVersionRuntime) {
|
||||||
|
process.exit(0)
|
||||||
|
} else {
|
||||||
|
process.exit(1)
|
||||||
|
}
|
|
@ -0,0 +1,44 @@
|
||||||
|
{
|
||||||
|
"name": "@napi-rs/canvas-linux-x64-gnu",
|
||||||
|
"version": "0.1.30",
|
||||||
|
"os": [
|
||||||
|
"linux"
|
||||||
|
],
|
||||||
|
"cpu": [
|
||||||
|
"x64"
|
||||||
|
],
|
||||||
|
"libc": [
|
||||||
|
"glibc"
|
||||||
|
],
|
||||||
|
"main": "skia.linux-x64-gnu.node",
|
||||||
|
"files": [
|
||||||
|
"skia.linux-x64-gnu.node",
|
||||||
|
"install.js"
|
||||||
|
],
|
||||||
|
"scripts": {
|
||||||
|
"install": "node install.js"
|
||||||
|
},
|
||||||
|
"description": "Canvas for Node.js with skia backend",
|
||||||
|
"keywords": [
|
||||||
|
"napi-rs",
|
||||||
|
"NAPI",
|
||||||
|
"N-API",
|
||||||
|
"Rust",
|
||||||
|
"node-addon",
|
||||||
|
"node-addon-api",
|
||||||
|
"canvas",
|
||||||
|
"image",
|
||||||
|
"pdf",
|
||||||
|
"svg",
|
||||||
|
"skia"
|
||||||
|
],
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 10"
|
||||||
|
},
|
||||||
|
"publishConfig": {
|
||||||
|
"registry": "https://registry.npmjs.org/",
|
||||||
|
"access": "public"
|
||||||
|
},
|
||||||
|
"repository": "https://github.com/Brooooooklyn/canvas.git"
|
||||||
|
}
|
BIN
node_modules/@napi-rs/canvas-linux-x64-gnu/skia.linux-x64-gnu.node
generated
vendored
Normal file
BIN
node_modules/@napi-rs/canvas-linux-x64-gnu/skia.linux-x64-gnu.node
generated
vendored
Normal file
Binary file not shown.
|
@ -0,0 +1,21 @@
|
||||||
|
MIT License
|
||||||
|
|
||||||
|
Copyright (c) 2020 lynweklm@gmail.com
|
||||||
|
|
||||||
|
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.
|
|
@ -0,0 +1,362 @@
|
||||||
|
# `skr canvas`
|
||||||
|
|
||||||
|
![CI](https://github.com/Brooooooklyn/canvas/workflows/CI/badge.svg)
|
||||||
|
![Skia Version](https://img.shields.io/badge/Skia-chrome%2Fm106-hotpink)
|
||||||
|
[![install size](https://packagephobia.com/badge?p=@napi-rs/canvas)](https://packagephobia.com/result?p=@napi-rs/canvas)
|
||||||
|
[![Downloads](https://img.shields.io/npm/dm/@napi-rs/canvas.svg?sanitize=true)](https://npmcharts.com/compare/@napi-rs/canvas?minimal=true)
|
||||||
|
|
||||||
|
> 🚀 Help me to become a full-time open-source developer by [sponsoring me on Github](https://github.com/sponsors/Brooooooklyn)
|
||||||
|
|
||||||
|
Google Skia binding to Node.js via [Node-API](https://napi.rs), **0 System dependencies!**
|
||||||
|
|
||||||
|
⚠️ This project is in pre-release stage. And there may some bugs existed.<br/>
|
||||||
|
For details on planned features and future direction please refer to the [Roadmap](https://github.com/Brooooooklyn/canvas/issues/113).
|
||||||
|
|
||||||
|
[中文文档](./README-zh.md)
|
||||||
|
|
||||||
|
# Install
|
||||||
|
|
||||||
|
```bash
|
||||||
|
yarn add @napi-rs/canvas
|
||||||
|
npm install @napi-rs/canvas
|
||||||
|
```
|
||||||
|
|
||||||
|
# Support matrix
|
||||||
|
|
||||||
|
| | node10 | node12 | node14 | node16 |
|
||||||
|
| --------------------- | ------ | ------ | ------ | ------ |
|
||||||
|
| Windows x64 | ✓ | ✓ | ✓ | ✓ |
|
||||||
|
| macOS x64 | ✓ | ✓ | ✓ | ✓ |
|
||||||
|
| macOS arm64 (m chips) | ✓ | ✓ | ✓ | ✓ |
|
||||||
|
| Linux x64 gnu | ✓ | ✓ | ✓ | ✓ |
|
||||||
|
| Linux x64 musl | ✓ | ✓ | ✓ | ✓ |
|
||||||
|
| Linux arm64 gnu | ✓ | ✓ | ✓ | ✓ |
|
||||||
|
| Linux arm64 musl | ✓ | ✓ | ✓ | ✓ |
|
||||||
|
| Linux arm gnueabihf | ✓ | ✓ | ✓ | ✓ |
|
||||||
|
| Linux arm64 android | ✓ | ✓ | ✓ | ✓ |
|
||||||
|
|
||||||
|
## System requirement
|
||||||
|
|
||||||
|
### `arm64`
|
||||||
|
|
||||||
|
[**_cortex-a57_**](https://en.wikipedia.org/wiki/ARM_Cortex-A57) or newer CPU architecture on **Linux**.
|
||||||
|
|
||||||
|
All Apple M chips on **macOS**.
|
||||||
|
|
||||||
|
### `armv7`
|
||||||
|
|
||||||
|
[**_cortex-a7_**](https://en.wikipedia.org/wiki/ARM_Cortex-A7) or newer CPU architecture.
|
||||||
|
|
||||||
|
### glibc
|
||||||
|
|
||||||
|
Since Skia relies on the [glibc](https://www.gnu.org/software/libc/) 2.18 API, you need to have at least glibc version >= 2.18 on your system.
|
||||||
|
|
||||||
|
# Usage
|
||||||
|
|
||||||
|
```js
|
||||||
|
const { promises } = require('fs')
|
||||||
|
const { join } = require('path')
|
||||||
|
const { createCanvas } = require('@napi-rs/canvas')
|
||||||
|
|
||||||
|
const canvas = createCanvas(300, 320)
|
||||||
|
const ctx = canvas.getContext('2d')
|
||||||
|
|
||||||
|
ctx.lineWidth = 10
|
||||||
|
ctx.strokeStyle = '#03a9f4'
|
||||||
|
ctx.fillStyle = '#03a9f4'
|
||||||
|
|
||||||
|
// Wall
|
||||||
|
ctx.strokeRect(75, 140, 150, 110)
|
||||||
|
|
||||||
|
// Door
|
||||||
|
ctx.fillRect(130, 190, 40, 60)
|
||||||
|
|
||||||
|
// Roof
|
||||||
|
ctx.beginPath()
|
||||||
|
ctx.moveTo(50, 140)
|
||||||
|
ctx.lineTo(150, 60)
|
||||||
|
ctx.lineTo(250, 140)
|
||||||
|
ctx.closePath()
|
||||||
|
ctx.stroke()
|
||||||
|
|
||||||
|
async function main() {
|
||||||
|
const pngData = await canvas.encode('png') // JPEG, AVIF and WebP are also supported
|
||||||
|
// encoding in libuv thread pool, non-blocking
|
||||||
|
await promises.writeFile(join(__dirname, 'simple.png'), pngData)
|
||||||
|
}
|
||||||
|
|
||||||
|
main()
|
||||||
|
```
|
||||||
|
|
||||||
|
![](./example/simple.png)
|
||||||
|
|
||||||
|
## Emoji text
|
||||||
|
|
||||||
|
```js
|
||||||
|
const { writeFileSync } = require('fs')
|
||||||
|
const { join } = require('path')
|
||||||
|
|
||||||
|
const { createCanvas, GlobalFonts } = require('@napi-rs/canvas')
|
||||||
|
|
||||||
|
GlobalFonts.registerFromPath(join(__dirname, '..', 'fonts', 'AppleColorEmoji@2x.ttf'), 'Apple Emoji')
|
||||||
|
GlobalFonts.registerFromPath(join(__dirname, '..', '__test__', 'fonts', 'COLRv1.ttf'), 'COLRv1')
|
||||||
|
|
||||||
|
console.info(GlobalFonts.families)
|
||||||
|
|
||||||
|
const canvas = createCanvas(760, 360)
|
||||||
|
const ctx = canvas.getContext('2d')
|
||||||
|
|
||||||
|
ctx.font = '50px Apple Emoji'
|
||||||
|
ctx.strokeText('😀😃😄😁😆😅😂🤣☺️😊😊😇', 50, 150)
|
||||||
|
|
||||||
|
ctx.font = '100px COLRv1'
|
||||||
|
ctx.fillText('abc', 50, 300)
|
||||||
|
|
||||||
|
const b = canvas.toBuffer('image/png')
|
||||||
|
|
||||||
|
writeFileSync(join(__dirname, 'draw-emoji.png'), b)
|
||||||
|
```
|
||||||
|
|
||||||
|
![](./example/draw-emoji.png)
|
||||||
|
|
||||||
|
# Performance
|
||||||
|
|
||||||
|
See [benchmark](./benchmark) for benchmark code.
|
||||||
|
|
||||||
|
Hardware info:
|
||||||
|
|
||||||
|
```
|
||||||
|
OS: Windows 10 x86_64
|
||||||
|
Host: Micro-Star International Co., Ltd. MS-7C35
|
||||||
|
Kernel: 10.0.19043
|
||||||
|
Terminal: Windows Terminal
|
||||||
|
CPU: AMD Ryzen 9 5950X (32) @ 3.400GHz
|
||||||
|
Memory: 32688MiB
|
||||||
|
```
|
||||||
|
|
||||||
|
```
|
||||||
|
❯ yarn bench
|
||||||
|
|
||||||
|
> @napi-rs/canvas@0.0.9 bench D:\workspace\skia-rs
|
||||||
|
> node -r @swc-node/register benchmark/bench.ts
|
||||||
|
|
||||||
|
Running "Draw house" suite...
|
||||||
|
Progress: 100%
|
||||||
|
|
||||||
|
skia-canvas:
|
||||||
|
26 ops/s, ±0.70% | slowest, 29.73% slower
|
||||||
|
|
||||||
|
node-canvas:
|
||||||
|
30 ops/s, ±6.95% | 18.92% slower
|
||||||
|
|
||||||
|
@napi-rs/canvas:
|
||||||
|
37 ops/s, ±6.30% | fastest
|
||||||
|
|
||||||
|
Finished 3 cases!
|
||||||
|
Fastest: @napi-rs/canvas
|
||||||
|
Slowest: skia-canvas
|
||||||
|
Running "Draw gradient" suite...
|
||||||
|
Progress: 100%
|
||||||
|
|
||||||
|
skia-canvas:
|
||||||
|
36 ops/s, ±6.12% | 14.29% slower
|
||||||
|
|
||||||
|
node-canvas:
|
||||||
|
34 ops/s, ±5.60% | slowest, 19.05% slower
|
||||||
|
|
||||||
|
@napi-rs/canvas:
|
||||||
|
42 ops/s, ±0.53% | fastest
|
||||||
|
|
||||||
|
Finished 3 cases!
|
||||||
|
Fastest: @napi-rs/canvas
|
||||||
|
Slowest: node-canvas
|
||||||
|
```
|
||||||
|
|
||||||
|
# Features
|
||||||
|
|
||||||
|
## Path2D
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
new Path2D()
|
||||||
|
new Path2D(path: Path2D)
|
||||||
|
// new Path2D('M108.956,403.826c0,0,0.178,3.344-1.276,3.311 c-1.455-0.033-30.507-84.917-66.752-80.957C40.928,326.18,72.326,313.197,108.956,403.826z')
|
||||||
|
new Path2D(path: string)
|
||||||
|
```
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
export interface DOMMatrix2DInit {
|
||||||
|
a: number
|
||||||
|
b: number
|
||||||
|
c: number
|
||||||
|
d: number
|
||||||
|
e: number
|
||||||
|
f: number
|
||||||
|
}
|
||||||
|
|
||||||
|
export class Path2D {
|
||||||
|
constructor(path?: Path2D | string)
|
||||||
|
|
||||||
|
addPath(path: Path2D, transform?: DOMMatrix2DInit): void
|
||||||
|
arc(x: number, y: number, radius: number, startAngle: number, endAngle: number, anticlockwise?: boolean): void
|
||||||
|
arcTo(x1: number, y1: number, x2: number, y2: number, radius: number): void
|
||||||
|
bezierCurveTo(cp1x: number, cp1y: number, cp2x: number, cp2y: number, x: number, y: number): void
|
||||||
|
closePath(): void
|
||||||
|
ellipse(
|
||||||
|
x: number,
|
||||||
|
y: number,
|
||||||
|
radiusX: number,
|
||||||
|
radiusY: number,
|
||||||
|
rotation: number,
|
||||||
|
startAngle: number,
|
||||||
|
endAngle: number,
|
||||||
|
anticlockwise?: boolean,
|
||||||
|
): void
|
||||||
|
lineTo(x: number, y: number): void
|
||||||
|
moveTo(x: number, y: number): void
|
||||||
|
quadraticCurveTo(cpx: number, cpy: number, x: number, y: number): void
|
||||||
|
rect(x: number, y: number, w: number, h: number): void
|
||||||
|
|
||||||
|
// PathKit methods
|
||||||
|
op(path: Path2D, operation: PathOp): Path2D
|
||||||
|
toSVGString(): string
|
||||||
|
getFillType(): FillType
|
||||||
|
getFillTypeString(): string
|
||||||
|
setFillType(type: FillType): void
|
||||||
|
simplify(): Path2D
|
||||||
|
asWinding(): Path2D
|
||||||
|
stroke(stroke?: StrokeOptions): Path2D
|
||||||
|
transform(transform: DOMMatrix2DInit): Path2D
|
||||||
|
getBounds(): [left: number, top: number, right: number, bottom: number]
|
||||||
|
computeTightBounds(): [left: number, top: number, right: number, bottom: number]
|
||||||
|
trim(start: number, end: number, isComplement?: boolean): Path2D
|
||||||
|
equals(path: Path2D): boolean
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## PathKit
|
||||||
|
|
||||||
|
`PathKit` is a toolset for manipulating Path in `Skia`, supporting **_quadratic beziers_**, **_cubic beziers_** and **_conics_**.
|
||||||
|
The main features are.
|
||||||
|
|
||||||
|
### Path Operation
|
||||||
|
|
||||||
|
`.op(path, PathOp)`
|
||||||
|
|
||||||
|
```js
|
||||||
|
const pathOne = new Path2D(
|
||||||
|
'M8 50H92C96.4183 50 100 53.5817 100 58V142C100 146.418 96.4183 150 92 150H8C3.58172 150 0 146.418 0 142V58C0 53.5817 3.58172 50 8 50Z',
|
||||||
|
)
|
||||||
|
const pathTwo = new Path2D(
|
||||||
|
'"M58 0H142C146.418 0 150 3.58172 150 8V92C150 96.4183 146.418 100 142 100H58C53.5817 100 50 96.4183 50 92V8C50 3.58172 53.5817 0 58 0Z',
|
||||||
|
)
|
||||||
|
|
||||||
|
pathOne.op(pathTwo, PathOp.Intersect).toSVGString()
|
||||||
|
// => "M100 100L58 100C53.5817 100 50 96.4183 50 92L50 50L92 50C96.4183 50 100 53.5817 100 58L100 100Z"
|
||||||
|
```
|
||||||
|
|
||||||
|
- **Union**, subtract the op path from the first path
|
||||||
|
- **Difference**, intersect the two paths
|
||||||
|
- **ReverseDifference**, union (inclusive-or) the two paths
|
||||||
|
- **Intersect**, exclusive-or the two paths
|
||||||
|
- **XOR**, subtract the first path from the op path
|
||||||
|
|
||||||
|
![boolean-operations](./docs/imgs/boolean-operations.svg)
|
||||||
|
|
||||||
|
### Covert `FillType` in **_Path_**
|
||||||
|
|
||||||
|
`.asWinding()`
|
||||||
|
|
||||||
|
You can convert `fill-rule="evenodd"` to `fill-rule="nonzero"` in SVG.
|
||||||
|
This is useful for **OpenType** font-related tools, as `fill-rule="nonzero"` is only supported in **OpenType** fonts.
|
||||||
|
|
||||||
|
![SVG fill-rule](./docs/imgs/asWinding@2x.png)
|
||||||
|
|
||||||
|
```js
|
||||||
|
const pathCircle = new Path2D(
|
||||||
|
'M24.2979 13.6364H129.394V40.9091H24.2979L14.6278 27.2727L24.2979 13.6364ZM21.9592 0C19.0246 0 16.2716 1.42436 14.571 3.82251L1.67756 22.0043C-0.559186 25.1585 -0.559186 29.387 1.67756 32.5411L14.571 50.7227C16.2716 53.1209 19.0246 54.5455 21.9592 54.5455H70.4673V68.1818H16.073C11.0661 68.1818 7.00728 72.2518 7.00728 77.2727V113.636C7.00728 118.657 11.0661 122.727 16.073 122.727H70.4673V150H84.0658V122.727H128.041C130.975 122.727 133.729 121.303 135.429 118.905L148.323 100.723C150.559 97.5686 150.559 93.3405 148.323 90.1864L135.429 72.0045C133.729 69.6064 130.975 68.1818 128.041 68.1818H84.0658V54.5455H133.927C138.934 54.5455 142.993 50.4755 142.993 45.4545V9.09091C142.993 4.07014 138.934 0 133.927 0H21.9592ZM125.702 109.091H20.6058V81.8182H125.702L135.372 95.4545L125.702 109.091Z',
|
||||||
|
)
|
||||||
|
pathCircle.setFillType(FillType.EvenOdd)
|
||||||
|
pathCircle.asWinding().toSVGString()
|
||||||
|
// => "M24.2979 13.6364L129.394 13.6364L129.394 40.9091L24.2979 40.9091L14.6278 27.2727L24.2979 13.6364ZM21.9592 0C19.0246 0 16.2716 1.42436 14.571 3.82251L1.67756 22.0043C-0.559186 25.1585 -0.559186 29.387 1.67756 32.5411L14.571 50.7227C16.2716 53.1209 19.0246 54.5455 21.9592 54.5455L70.4673 54.5455L70.4673 68.1818L16.073 68.1818C11.0661 68.1818 7.00728 72.2518 7.00728 77.2727L7.00728 113.636C7.00728 118.657 11.0661 122.727 16.073 122.727L70.4673 122.727L70.4673 150L84.0658 150L84.0658 122.727L128.041 122.727C130.975 122.727 133.729 121.303 135.429 118.905L148.323 100.723C150.559 97.5686 150.559 93.3405 148.323 90.1864L135.429 72.0045C133.729 69.6064 130.975 68.1818 128.041 68.1818L84.0658 68.1818L84.0658 54.5455L133.927 54.5455C138.934 54.5455 142.993 50.4755 142.993 45.4545L142.993 9.09091C142.993 4.07014 138.934 0 133.927 0L21.9592 0ZM125.702 109.091L20.6058 109.091L20.6058 81.8182L125.702 81.8182L135.372 95.4545L125.702 109.091Z"
|
||||||
|
```
|
||||||
|
|
||||||
|
### Simplify **_Path_**
|
||||||
|
|
||||||
|
`.simplify()`
|
||||||
|
|
||||||
|
Set the path to the same non-overlapping contour as the original path area, which means that it can also remove overlapping paths.
|
||||||
|
|
||||||
|
<img width="800" src="./docs/imgs/simplify.png" >
|
||||||
|
|
||||||
|
[SVG with overlapping paths](./docs/imgs/overlapping-path.svg) (Left)
|
||||||
|
|
||||||
|
```js
|
||||||
|
const path =
|
||||||
|
'M2.933,89.89 L89.005,3.818 Q90.412,2.411 92.249,1.65 Q94.087,0.889 96.076,0.889 Q98.065,0.889 99.903,1.65 Q101.741,2.411 103.147,3.818 L189.22,89.89 Q190.626,91.296 191.387,93.134 Q192.148,94.972 192.148,96.961 Q192.148,98.95 191.387,100.788 Q190.626,102.625 189.219,104.032 Q187.813,105.439 185.975,106.2 Q184.138,106.961 182.148,106.961 Q180.159,106.961 178.322,106.2 Q176.484,105.439 175.077,104.032 L89.005,17.96 L96.076,10.889 L103.147,17.96 L17.075,104.032 Q15.668,105.439 13.831,106.2 Q11.993,106.961 10.004,106.961 Q8.015,106.961 6.177,106.2 Q4.339,105.439 2.933,104.032 Q1.526,102.625 0.765,100.788 Q0.004,98.95 0.004,96.961 Q0.004,94.972 0.765,93.134 Q1.526,91.296 2.933,89.89 Z'
|
||||||
|
|
||||||
|
path.simplify().toSVGString()
|
||||||
|
// => "M89.005 3.818L2.933 89.89Q1.526 91.296 0.765 93.134Q0.004 94.972 0.004 96.961Q0.004 98.95 0.765 100.788Q1.526 102.625 2.933 104.032Q4.339 105.439 6.177 106.2Q8.015 106.961 10.004 106.961Q11.993 106.961 13.831 106.2Q15.668 105.439 17.075 104.032L96.076 25.031L175.077 104.032Q176.484 105.439 178.322 106.2Q180.159 106.961 182.148 106.961Q184.138 106.961 185.975 106.2Q187.813 105.439 189.219 104.032Q190.626 102.625 191.387 100.788Q192.148 98.95 192.148 96.961Q192.148 94.972 191.387 93.134Q190.626 91.296 189.22 89.89L103.147 3.818Q101.741 2.411 99.903 1.65Q98.065 0.889 96.076 0.889Q94.087 0.889 92.249 1.65Q90.412 2.411 89.005 3.818Z"
|
||||||
|
```
|
||||||
|
|
||||||
|
# [Example](./example/tiger.js)
|
||||||
|
|
||||||
|
> The tiger.json was serialized from [gojs/samples/tiger](https://github.com/NorthwoodsSoftware/GoJS/blob/master/samples/tiger.html)
|
||||||
|
|
||||||
|
<img width="500" src="example/tiger.png">
|
||||||
|
|
||||||
|
```shell
|
||||||
|
node example/anime-girl.js
|
||||||
|
```
|
||||||
|
|
||||||
|
| SVG | PNG |
|
||||||
|
| -------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||||
|
| <img width="500" src="example/anime-girl.svg"><br/>[CC-BY-SA 3.0](https://creativecommons.org/licenses/by/3.0) by [Niabot](https://commons.wikimedia.org/wiki/User:Niabot) | <img width="500" src="example/anime-girl.png"><br/>[CC-BY-SA 3.0](https://creativecommons.org/licenses/by/3.0) by [Niabot](https://commons.wikimedia.org/wiki/User:Niabot) |
|
||||||
|
|
||||||
|
# Building
|
||||||
|
|
||||||
|
## Build skia from source
|
||||||
|
|
||||||
|
You can build this project from source, the system requirements are here: https://skia.org/docs/user/build
|
||||||
|
|
||||||
|
```sh
|
||||||
|
# Clone the code:
|
||||||
|
$ git clone --recurse-submodules https://github.com/Brooooooklyn/canvas.git
|
||||||
|
$ cd canvas
|
||||||
|
|
||||||
|
# Build Skia:
|
||||||
|
$ node scripts/build-skia.js
|
||||||
|
|
||||||
|
# Install NPM packages, build the Node.js addon:
|
||||||
|
$ npm install -g yarn
|
||||||
|
$ yarn install --mode=skip-build # Here are modules that are used for benchmarking and are hard to install, you can skip it by specifying `--mode=skip-build`
|
||||||
|
$ sudo dnf install clang # https://fedora.pkgs.org/34/fedora-x86_64/clang-12.0.0-0.3.rc1.fc34.x86_64.rpm.html
|
||||||
|
$ yarn build
|
||||||
|
|
||||||
|
# All done! Run test cases or examples now:
|
||||||
|
$ yarn test
|
||||||
|
$ node example/tiger.js
|
||||||
|
```
|
||||||
|
|
||||||
|
## Pull pre-build skia binary from GitHub
|
||||||
|
|
||||||
|
You can pull skia pre-build binaries if you just care the `Rust` part:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
# Clone the code:
|
||||||
|
$ git clone --recurse-submodules https://github.com/Brooooooklyn/canvas.git
|
||||||
|
$ cd canvas
|
||||||
|
|
||||||
|
# Download Skia binaries:
|
||||||
|
# It will pull the binaries match the git hash in `./skia` submodule
|
||||||
|
$ node scripts/release-skia-binary.js --download
|
||||||
|
|
||||||
|
# Install NPM packages, build the Node.js addon:
|
||||||
|
$ npm install -g yarn
|
||||||
|
$ yarn install --mode=skip-build
|
||||||
|
$ sudo dnf install clang # https://fedora.pkgs.org/34/fedora-x86_64/clang-12.0.0-0.3.rc1.fc34.x86_64.rpm.html
|
||||||
|
$ yarn build
|
||||||
|
|
||||||
|
# All done! Run test cases or examples now:
|
||||||
|
$ yarn test
|
||||||
|
$ node example/tiger.js
|
||||||
|
```
|
|
@ -0,0 +1,866 @@
|
||||||
|
const { inspect } = require('util')
|
||||||
|
/*
|
||||||
|
* vendored in order to fix its dependence on the window global [cds 2020/08/04]
|
||||||
|
* otherwise unchanged from https://github.com/jarek-foksa/geometry-polyfill/tree/f36bbc8f4bc43539d980687904ce46c8e915543d
|
||||||
|
*/
|
||||||
|
|
||||||
|
// @info
|
||||||
|
// DOMPoint polyfill
|
||||||
|
// @src
|
||||||
|
// https://drafts.fxtf.org/geometry/#DOMPoint
|
||||||
|
// https://github.com/chromium/chromium/blob/master/third_party/blink/renderer/core/geometry/dom_point_read_only.cc
|
||||||
|
class DOMPoint {
|
||||||
|
constructor(x = 0, y = 0, z = 0, w = 1) {
|
||||||
|
this.x = x
|
||||||
|
this.y = y
|
||||||
|
this.z = z
|
||||||
|
this.w = w
|
||||||
|
}
|
||||||
|
|
||||||
|
static fromPoint(otherPoint) {
|
||||||
|
return new DOMPoint(
|
||||||
|
otherPoint.x,
|
||||||
|
otherPoint.y,
|
||||||
|
otherPoint.z !== undefined ? otherPoint.z : 0,
|
||||||
|
otherPoint.w !== undefined ? otherPoint.w : 1,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
matrixTransform(matrix) {
|
||||||
|
if ((matrix.is2D || matrix instanceof SVGMatrix) && this.z === 0 && this.w === 1) {
|
||||||
|
return new DOMPoint(
|
||||||
|
this.x * matrix.a + this.y * matrix.c + matrix.e,
|
||||||
|
this.x * matrix.b + this.y * matrix.d + matrix.f,
|
||||||
|
0,
|
||||||
|
1,
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
return new DOMPoint(
|
||||||
|
this.x * matrix.m11 + this.y * matrix.m21 + this.z * matrix.m31 + this.w * matrix.m41,
|
||||||
|
this.x * matrix.m12 + this.y * matrix.m22 + this.z * matrix.m32 + this.w * matrix.m42,
|
||||||
|
this.x * matrix.m13 + this.y * matrix.m23 + this.z * matrix.m33 + this.w * matrix.m43,
|
||||||
|
this.x * matrix.m14 + this.y * matrix.m24 + this.z * matrix.m34 + this.w * matrix.m44,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
toJSON() {
|
||||||
|
return {
|
||||||
|
x: this.x,
|
||||||
|
y: this.y,
|
||||||
|
z: this.z,
|
||||||
|
w: this.w,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// @info
|
||||||
|
// DOMRect polyfill
|
||||||
|
// @src
|
||||||
|
// https://drafts.fxtf.org/geometry/#DOMRect
|
||||||
|
// https://github.com/chromium/chromium/blob/master/third_party/blink/renderer/core/geometry/dom_rect_read_only.cc
|
||||||
|
|
||||||
|
class DOMRect {
|
||||||
|
constructor(x = 0, y = 0, width = 0, height = 0) {
|
||||||
|
this.x = x
|
||||||
|
this.y = y
|
||||||
|
this.width = width
|
||||||
|
this.height = height
|
||||||
|
}
|
||||||
|
|
||||||
|
static fromRect(otherRect) {
|
||||||
|
return new DOMRect(otherRect.x, otherRect.y, otherRect.width, otherRect.height)
|
||||||
|
}
|
||||||
|
|
||||||
|
get top() {
|
||||||
|
return this.y
|
||||||
|
}
|
||||||
|
|
||||||
|
get left() {
|
||||||
|
return this.x
|
||||||
|
}
|
||||||
|
|
||||||
|
get right() {
|
||||||
|
return this.x + this.width
|
||||||
|
}
|
||||||
|
|
||||||
|
get bottom() {
|
||||||
|
return this.y + this.height
|
||||||
|
}
|
||||||
|
|
||||||
|
toJSON() {
|
||||||
|
return {
|
||||||
|
x: this.x,
|
||||||
|
y: this.y,
|
||||||
|
width: this.width,
|
||||||
|
height: this.height,
|
||||||
|
top: this.top,
|
||||||
|
left: this.left,
|
||||||
|
right: this.right,
|
||||||
|
bottom: this.bottom,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const propertyName of ['top', 'right', 'bottom', 'left']) {
|
||||||
|
const propertyDescriptor = Object.getOwnPropertyDescriptor(DOMRect.prototype, propertyName)
|
||||||
|
propertyDescriptor.enumerable = true
|
||||||
|
Object.defineProperty(DOMRect.prototype, propertyName, propertyDescriptor)
|
||||||
|
}
|
||||||
|
|
||||||
|
// @info
|
||||||
|
// DOMMatrix polyfill (SVG 2)
|
||||||
|
// @src
|
||||||
|
// https://github.com/chromium/chromium/blob/master/third_party/blink/renderer/core/geometry/dom_matrix_read_only.cc
|
||||||
|
// https://github.com/tocharomera/generativecanvas/blob/master/node-canvas/lib/DOMMatrix.js
|
||||||
|
|
||||||
|
const M11 = 0,
|
||||||
|
M12 = 1,
|
||||||
|
M13 = 2,
|
||||||
|
M14 = 3
|
||||||
|
const M21 = 4,
|
||||||
|
M22 = 5,
|
||||||
|
M23 = 6,
|
||||||
|
M24 = 7
|
||||||
|
const M31 = 8,
|
||||||
|
M32 = 9,
|
||||||
|
M33 = 10,
|
||||||
|
M34 = 11
|
||||||
|
const M41 = 12,
|
||||||
|
M42 = 13,
|
||||||
|
M43 = 14,
|
||||||
|
M44 = 15
|
||||||
|
|
||||||
|
const A = M11,
|
||||||
|
B = M12
|
||||||
|
const C = M21,
|
||||||
|
D = M22
|
||||||
|
const E = M41,
|
||||||
|
F = M42
|
||||||
|
|
||||||
|
const DEGREE_PER_RAD = 180 / Math.PI
|
||||||
|
const RAD_PER_DEGREE = Math.PI / 180
|
||||||
|
|
||||||
|
const VALUES = Symbol('values')
|
||||||
|
const IS_2D = Symbol('is2D')
|
||||||
|
|
||||||
|
function parseMatrix(init) {
|
||||||
|
let parsed = init.replace(/matrix\(/, '').split(/,/, 7)
|
||||||
|
|
||||||
|
if (parsed.length !== 6) {
|
||||||
|
throw new Error(`Failed to parse ${init}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
parsed = parsed.map(parseFloat)
|
||||||
|
|
||||||
|
return [parsed[0], parsed[1], 0, 0, parsed[2], parsed[3], 0, 0, 0, 0, 1, 0, parsed[4], parsed[5], 0, 1]
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseMatrix3d(init) {
|
||||||
|
const parsed = init.replace(/matrix3d\(/, '').split(/,/, 17)
|
||||||
|
|
||||||
|
if (parsed.length !== 16) {
|
||||||
|
throw new Error(`Failed to parse ${init}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
return parsed.map(parseFloat)
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseTransform(tform) {
|
||||||
|
const type = tform.split(/\(/, 1)[0]
|
||||||
|
|
||||||
|
if (type === 'matrix') {
|
||||||
|
return parseMatrix(tform)
|
||||||
|
} else if (type === 'matrix3d') {
|
||||||
|
return parseMatrix3d(tform)
|
||||||
|
} else {
|
||||||
|
throw new Error(`${type} parsing not implemented`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const setNumber2D = (receiver, index, value) => {
|
||||||
|
if (typeof value !== 'number') {
|
||||||
|
throw new TypeError('Expected number')
|
||||||
|
}
|
||||||
|
|
||||||
|
receiver[VALUES][index] = value
|
||||||
|
}
|
||||||
|
|
||||||
|
const setNumber3D = (receiver, index, value) => {
|
||||||
|
if (typeof value !== 'number') {
|
||||||
|
throw new TypeError('Expected number')
|
||||||
|
}
|
||||||
|
|
||||||
|
if (index === M33 || index === M44) {
|
||||||
|
if (value !== 1) {
|
||||||
|
receiver[IS_2D] = false
|
||||||
|
}
|
||||||
|
} else if (value !== 0) {
|
||||||
|
receiver[IS_2D] = false
|
||||||
|
}
|
||||||
|
|
||||||
|
receiver[VALUES][index] = value
|
||||||
|
}
|
||||||
|
|
||||||
|
const newInstance = (values) => {
|
||||||
|
const instance = Object.create(DOMMatrix.prototype)
|
||||||
|
instance.constructor = DOMMatrix
|
||||||
|
instance[IS_2D] = true
|
||||||
|
instance[VALUES] = values
|
||||||
|
|
||||||
|
return instance
|
||||||
|
}
|
||||||
|
|
||||||
|
const multiply = (first, second) => {
|
||||||
|
const dest = new Float64Array(16)
|
||||||
|
|
||||||
|
for (let i = 0; i < 4; i++) {
|
||||||
|
for (let j = 0; j < 4; j++) {
|
||||||
|
let sum = 0
|
||||||
|
|
||||||
|
for (let k = 0; k < 4; k++) {
|
||||||
|
sum += first[i * 4 + k] * second[k * 4 + j]
|
||||||
|
}
|
||||||
|
|
||||||
|
dest[i * 4 + j] = sum
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return dest
|
||||||
|
}
|
||||||
|
|
||||||
|
class DOMMatrix {
|
||||||
|
get m11() {
|
||||||
|
return this[VALUES][M11]
|
||||||
|
}
|
||||||
|
set m11(value) {
|
||||||
|
setNumber2D(this, M11, value)
|
||||||
|
}
|
||||||
|
get m12() {
|
||||||
|
return this[VALUES][M12]
|
||||||
|
}
|
||||||
|
set m12(value) {
|
||||||
|
setNumber2D(this, M12, value)
|
||||||
|
}
|
||||||
|
get m13() {
|
||||||
|
return this[VALUES][M13]
|
||||||
|
}
|
||||||
|
set m13(value) {
|
||||||
|
setNumber3D(this, M13, value)
|
||||||
|
}
|
||||||
|
get m14() {
|
||||||
|
return this[VALUES][M14]
|
||||||
|
}
|
||||||
|
set m14(value) {
|
||||||
|
setNumber3D(this, M14, value)
|
||||||
|
}
|
||||||
|
get m21() {
|
||||||
|
return this[VALUES][M21]
|
||||||
|
}
|
||||||
|
set m21(value) {
|
||||||
|
setNumber2D(this, M21, value)
|
||||||
|
}
|
||||||
|
get m22() {
|
||||||
|
return this[VALUES][M22]
|
||||||
|
}
|
||||||
|
set m22(value) {
|
||||||
|
setNumber2D(this, M22, value)
|
||||||
|
}
|
||||||
|
get m23() {
|
||||||
|
return this[VALUES][M23]
|
||||||
|
}
|
||||||
|
set m23(value) {
|
||||||
|
setNumber3D(this, M23, value)
|
||||||
|
}
|
||||||
|
get m24() {
|
||||||
|
return this[VALUES][M24]
|
||||||
|
}
|
||||||
|
set m24(value) {
|
||||||
|
setNumber3D(this, M24, value)
|
||||||
|
}
|
||||||
|
get m31() {
|
||||||
|
return this[VALUES][M31]
|
||||||
|
}
|
||||||
|
set m31(value) {
|
||||||
|
setNumber3D(this, M31, value)
|
||||||
|
}
|
||||||
|
get m32() {
|
||||||
|
return this[VALUES][M32]
|
||||||
|
}
|
||||||
|
set m32(value) {
|
||||||
|
setNumber3D(this, M32, value)
|
||||||
|
}
|
||||||
|
get m33() {
|
||||||
|
return this[VALUES][M33]
|
||||||
|
}
|
||||||
|
set m33(value) {
|
||||||
|
setNumber3D(this, M33, value)
|
||||||
|
}
|
||||||
|
get m34() {
|
||||||
|
return this[VALUES][M34]
|
||||||
|
}
|
||||||
|
set m34(value) {
|
||||||
|
setNumber3D(this, M34, value)
|
||||||
|
}
|
||||||
|
get m41() {
|
||||||
|
return this[VALUES][M41]
|
||||||
|
}
|
||||||
|
set m41(value) {
|
||||||
|
setNumber2D(this, M41, value)
|
||||||
|
}
|
||||||
|
get m42() {
|
||||||
|
return this[VALUES][M42]
|
||||||
|
}
|
||||||
|
set m42(value) {
|
||||||
|
setNumber2D(this, M42, value)
|
||||||
|
}
|
||||||
|
get m43() {
|
||||||
|
return this[VALUES][M43]
|
||||||
|
}
|
||||||
|
set m43(value) {
|
||||||
|
setNumber3D(this, M43, value)
|
||||||
|
}
|
||||||
|
get m44() {
|
||||||
|
return this[VALUES][M44]
|
||||||
|
}
|
||||||
|
set m44(value) {
|
||||||
|
setNumber3D(this, M44, value)
|
||||||
|
}
|
||||||
|
|
||||||
|
get a() {
|
||||||
|
return this[VALUES][A]
|
||||||
|
}
|
||||||
|
set a(value) {
|
||||||
|
setNumber2D(this, A, value)
|
||||||
|
}
|
||||||
|
get b() {
|
||||||
|
return this[VALUES][B]
|
||||||
|
}
|
||||||
|
set b(value) {
|
||||||
|
setNumber2D(this, B, value)
|
||||||
|
}
|
||||||
|
get c() {
|
||||||
|
return this[VALUES][C]
|
||||||
|
}
|
||||||
|
set c(value) {
|
||||||
|
setNumber2D(this, C, value)
|
||||||
|
}
|
||||||
|
get d() {
|
||||||
|
return this[VALUES][D]
|
||||||
|
}
|
||||||
|
set d(value) {
|
||||||
|
setNumber2D(this, D, value)
|
||||||
|
}
|
||||||
|
get e() {
|
||||||
|
return this[VALUES][E]
|
||||||
|
}
|
||||||
|
set e(value) {
|
||||||
|
setNumber2D(this, E, value)
|
||||||
|
}
|
||||||
|
get f() {
|
||||||
|
return this[VALUES][F]
|
||||||
|
}
|
||||||
|
set f(value) {
|
||||||
|
setNumber2D(this, F, value)
|
||||||
|
}
|
||||||
|
|
||||||
|
get is2D() {
|
||||||
|
return this[IS_2D]
|
||||||
|
}
|
||||||
|
|
||||||
|
get isIdentity() {
|
||||||
|
const values = this[VALUES]
|
||||||
|
|
||||||
|
return (
|
||||||
|
values[M11] === 1 &&
|
||||||
|
values[M12] === 0 &&
|
||||||
|
values[M13] === 0 &&
|
||||||
|
values[M14] === 0 &&
|
||||||
|
values[M21] === 0 &&
|
||||||
|
values[M22] === 1 &&
|
||||||
|
values[M23] === 0 &&
|
||||||
|
values[M24] === 0 &&
|
||||||
|
values[M31] === 0 &&
|
||||||
|
values[M32] === 0 &&
|
||||||
|
values[M33] === 1 &&
|
||||||
|
values[M34] === 0 &&
|
||||||
|
values[M41] === 0 &&
|
||||||
|
values[M42] === 0 &&
|
||||||
|
values[M43] === 0 &&
|
||||||
|
values[M44] === 1
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
static fromMatrix(init) {
|
||||||
|
if (init instanceof DOMMatrix) {
|
||||||
|
return new DOMMatrix(init[VALUES])
|
||||||
|
} else if (init instanceof SVGMatrix) {
|
||||||
|
return new DOMMatrix([init.a, init.b, init.c, init.d, init.e, init.f])
|
||||||
|
} else {
|
||||||
|
throw new TypeError('Expected DOMMatrix')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static fromFloat32Array(init) {
|
||||||
|
if (!(init instanceof Float32Array)) throw new TypeError('Expected Float32Array')
|
||||||
|
return new DOMMatrix(init)
|
||||||
|
}
|
||||||
|
|
||||||
|
static fromFloat64Array(init) {
|
||||||
|
if (!(init instanceof Float64Array)) throw new TypeError('Expected Float64Array')
|
||||||
|
return new DOMMatrix(init)
|
||||||
|
}
|
||||||
|
|
||||||
|
// @type
|
||||||
|
// (Float64Array) => void
|
||||||
|
constructor(init) {
|
||||||
|
this[IS_2D] = true
|
||||||
|
|
||||||
|
this[VALUES] = new Float64Array([1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1])
|
||||||
|
|
||||||
|
// Parse CSS transformList
|
||||||
|
if (typeof init === 'string') {
|
||||||
|
if (init === '') {
|
||||||
|
return
|
||||||
|
} else {
|
||||||
|
const tforms = init.split(/\)\s+/, 20).map(parseTransform)
|
||||||
|
|
||||||
|
if (tforms.length === 0) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
init = tforms[0]
|
||||||
|
|
||||||
|
for (let i = 1; i < tforms.length; i++) {
|
||||||
|
init = multiply(tforms[i], init)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let i = 0
|
||||||
|
|
||||||
|
if (init && init.length === 6) {
|
||||||
|
setNumber2D(this, A, init[i++])
|
||||||
|
setNumber2D(this, B, init[i++])
|
||||||
|
setNumber2D(this, C, init[i++])
|
||||||
|
setNumber2D(this, D, init[i++])
|
||||||
|
setNumber2D(this, E, init[i++])
|
||||||
|
setNumber2D(this, F, init[i++])
|
||||||
|
} else if (init && init.length === 16) {
|
||||||
|
setNumber2D(this, M11, init[i++])
|
||||||
|
setNumber2D(this, M12, init[i++])
|
||||||
|
setNumber3D(this, M13, init[i++])
|
||||||
|
setNumber3D(this, M14, init[i++])
|
||||||
|
setNumber2D(this, M21, init[i++])
|
||||||
|
setNumber2D(this, M22, init[i++])
|
||||||
|
setNumber3D(this, M23, init[i++])
|
||||||
|
setNumber3D(this, M24, init[i++])
|
||||||
|
setNumber3D(this, M31, init[i++])
|
||||||
|
setNumber3D(this, M32, init[i++])
|
||||||
|
setNumber3D(this, M33, init[i++])
|
||||||
|
setNumber3D(this, M34, init[i++])
|
||||||
|
setNumber2D(this, M41, init[i++])
|
||||||
|
setNumber2D(this, M42, init[i++])
|
||||||
|
setNumber3D(this, M43, init[i++])
|
||||||
|
setNumber3D(this, M44, init[i])
|
||||||
|
} else if (init !== undefined) {
|
||||||
|
throw new TypeError('Expected string or array.')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
dump() {
|
||||||
|
const mat = this[VALUES]
|
||||||
|
console.info([mat.slice(0, 4), mat.slice(4, 8), mat.slice(8, 12), mat.slice(12, 16)])
|
||||||
|
}
|
||||||
|
|
||||||
|
[inspect.custom](depth, options) {
|
||||||
|
if (depth < 0) return '[DOMMatrix]'
|
||||||
|
|
||||||
|
const { a, b, c, d, e, f, is2D, isIdentity } = this
|
||||||
|
if (this.is2D) {
|
||||||
|
return `DOMMatrix ${inspect({ a, b, c, d, e, f, is2D, isIdentity }, { colors: true })}`
|
||||||
|
} else {
|
||||||
|
const { m11, m12, m13, m14, m21, m22, m23, m24, m31, m32, m33, m34, m41, m42, m43, m44, is2D, isIdentity } = this
|
||||||
|
return `DOMMatrix ${inspect(
|
||||||
|
{
|
||||||
|
a,
|
||||||
|
b,
|
||||||
|
c,
|
||||||
|
d,
|
||||||
|
e,
|
||||||
|
f,
|
||||||
|
m11,
|
||||||
|
m12,
|
||||||
|
m13,
|
||||||
|
m14,
|
||||||
|
m21,
|
||||||
|
m22,
|
||||||
|
m23,
|
||||||
|
m24,
|
||||||
|
m31,
|
||||||
|
m32,
|
||||||
|
m33,
|
||||||
|
m34,
|
||||||
|
m41,
|
||||||
|
m42,
|
||||||
|
m43,
|
||||||
|
m44,
|
||||||
|
is2D,
|
||||||
|
isIdentity,
|
||||||
|
},
|
||||||
|
{ colors: true },
|
||||||
|
)}`
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
multiply(other) {
|
||||||
|
return newInstance(this[VALUES]).multiplySelf(other)
|
||||||
|
}
|
||||||
|
|
||||||
|
multiplySelf(other) {
|
||||||
|
this[VALUES] = multiply(other[VALUES], this[VALUES])
|
||||||
|
|
||||||
|
if (!other.is2D) {
|
||||||
|
this[IS_2D] = false
|
||||||
|
}
|
||||||
|
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
|
||||||
|
preMultiplySelf(other) {
|
||||||
|
this[VALUES] = multiply(this[VALUES], other[VALUES])
|
||||||
|
|
||||||
|
if (!other.is2D) {
|
||||||
|
this[IS_2D] = false
|
||||||
|
}
|
||||||
|
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
|
||||||
|
translate(tx, ty, tz) {
|
||||||
|
return newInstance(this[VALUES]).translateSelf(tx, ty, tz)
|
||||||
|
}
|
||||||
|
|
||||||
|
translateSelf(tx = 0, ty = 0, tz = 0) {
|
||||||
|
this[VALUES] = multiply([1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, tx, ty, tz, 1], this[VALUES])
|
||||||
|
|
||||||
|
if (tz !== 0) {
|
||||||
|
this[IS_2D] = false
|
||||||
|
}
|
||||||
|
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
|
||||||
|
scale(scaleX, scaleY, scaleZ, originX, originY, originZ) {
|
||||||
|
return newInstance(this[VALUES]).scaleSelf(scaleX, scaleY, scaleZ, originX, originY, originZ)
|
||||||
|
}
|
||||||
|
|
||||||
|
scale3d(scale, originX, originY, originZ) {
|
||||||
|
return newInstance(this[VALUES]).scale3dSelf(scale, originX, originY, originZ)
|
||||||
|
}
|
||||||
|
|
||||||
|
scale3dSelf(scale, originX, originY, originZ) {
|
||||||
|
return this.scaleSelf(scale, scale, scale, originX, originY, originZ)
|
||||||
|
}
|
||||||
|
|
||||||
|
scaleSelf(scaleX, scaleY, scaleZ, originX, originY, originZ) {
|
||||||
|
// Not redundant with translate's checks because we need to negate the values later.
|
||||||
|
if (typeof originX !== 'number') originX = 0
|
||||||
|
if (typeof originY !== 'number') originY = 0
|
||||||
|
if (typeof originZ !== 'number') originZ = 0
|
||||||
|
|
||||||
|
this.translateSelf(originX, originY, originZ)
|
||||||
|
|
||||||
|
if (typeof scaleX !== 'number') scaleX = 1
|
||||||
|
if (typeof scaleY !== 'number') scaleY = scaleX
|
||||||
|
if (typeof scaleZ !== 'number') scaleZ = 1
|
||||||
|
|
||||||
|
this[VALUES] = multiply([scaleX, 0, 0, 0, 0, scaleY, 0, 0, 0, 0, scaleZ, 0, 0, 0, 0, 1], this[VALUES])
|
||||||
|
|
||||||
|
this.translateSelf(-originX, -originY, -originZ)
|
||||||
|
|
||||||
|
if (scaleZ !== 1 || originZ !== 0) {
|
||||||
|
this[IS_2D] = false
|
||||||
|
}
|
||||||
|
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
|
||||||
|
rotateFromVector(x, y) {
|
||||||
|
return newInstance(this[VALUES]).rotateFromVectorSelf(x, y)
|
||||||
|
}
|
||||||
|
|
||||||
|
rotateFromVectorSelf(x = 0, y = 0) {
|
||||||
|
const theta = x === 0 && y === 0 ? 0 : Math.atan2(y, x) * DEGREE_PER_RAD
|
||||||
|
return this.rotateSelf(theta)
|
||||||
|
}
|
||||||
|
|
||||||
|
rotate(rotX, rotY, rotZ) {
|
||||||
|
return newInstance(this[VALUES]).rotateSelf(rotX, rotY, rotZ)
|
||||||
|
}
|
||||||
|
|
||||||
|
rotateSelf(rotX, rotY, rotZ) {
|
||||||
|
if (rotY === undefined && rotZ === undefined) {
|
||||||
|
rotZ = rotX
|
||||||
|
rotX = rotY = 0
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof rotY !== 'number') rotY = 0
|
||||||
|
if (typeof rotZ !== 'number') rotZ = 0
|
||||||
|
|
||||||
|
if (rotX !== 0 || rotY !== 0) {
|
||||||
|
this[IS_2D] = false
|
||||||
|
}
|
||||||
|
|
||||||
|
rotX *= RAD_PER_DEGREE
|
||||||
|
rotY *= RAD_PER_DEGREE
|
||||||
|
rotZ *= RAD_PER_DEGREE
|
||||||
|
|
||||||
|
let c = Math.cos(rotZ)
|
||||||
|
let s = Math.sin(rotZ)
|
||||||
|
|
||||||
|
this[VALUES] = multiply([c, s, 0, 0, -s, c, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1], this[VALUES])
|
||||||
|
|
||||||
|
c = Math.cos(rotY)
|
||||||
|
s = Math.sin(rotY)
|
||||||
|
|
||||||
|
this[VALUES] = multiply([c, 0, -s, 0, 0, 1, 0, 0, s, 0, c, 0, 0, 0, 0, 1], this[VALUES])
|
||||||
|
|
||||||
|
c = Math.cos(rotX)
|
||||||
|
s = Math.sin(rotX)
|
||||||
|
|
||||||
|
this[VALUES] = multiply([1, 0, 0, 0, 0, c, s, 0, 0, -s, c, 0, 0, 0, 0, 1], this[VALUES])
|
||||||
|
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
|
||||||
|
rotateAxisAngle(x, y, z, angle) {
|
||||||
|
return newInstance(this[VALUES]).rotateAxisAngleSelf(x, y, z, angle)
|
||||||
|
}
|
||||||
|
|
||||||
|
rotateAxisAngleSelf(x = 0, y = 0, z = 0, angle = 0) {
|
||||||
|
const length = Math.sqrt(x * x + y * y + z * z)
|
||||||
|
|
||||||
|
if (length === 0) {
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
|
||||||
|
if (length !== 1) {
|
||||||
|
x /= length
|
||||||
|
y /= length
|
||||||
|
z /= length
|
||||||
|
}
|
||||||
|
|
||||||
|
angle *= RAD_PER_DEGREE
|
||||||
|
|
||||||
|
const c = Math.cos(angle)
|
||||||
|
const s = Math.sin(angle)
|
||||||
|
const t = 1 - c
|
||||||
|
const tx = t * x
|
||||||
|
const ty = t * y
|
||||||
|
|
||||||
|
this[VALUES] = multiply(
|
||||||
|
[
|
||||||
|
tx * x + c,
|
||||||
|
tx * y + s * z,
|
||||||
|
tx * z - s * y,
|
||||||
|
0,
|
||||||
|
tx * y - s * z,
|
||||||
|
ty * y + c,
|
||||||
|
ty * z + s * x,
|
||||||
|
0,
|
||||||
|
tx * z + s * y,
|
||||||
|
ty * z - s * x,
|
||||||
|
t * z * z + c,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
1,
|
||||||
|
],
|
||||||
|
this[VALUES],
|
||||||
|
)
|
||||||
|
|
||||||
|
if (x !== 0 || y !== 0) {
|
||||||
|
this[IS_2D] = false
|
||||||
|
}
|
||||||
|
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
|
||||||
|
skewX(sx) {
|
||||||
|
return newInstance(this[VALUES]).skewXSelf(sx)
|
||||||
|
}
|
||||||
|
|
||||||
|
skewXSelf(sx) {
|
||||||
|
if (typeof sx !== 'number') {
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
|
||||||
|
const t = Math.tan(sx * RAD_PER_DEGREE)
|
||||||
|
|
||||||
|
this[VALUES] = multiply([1, 0, 0, 0, t, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1], this[VALUES])
|
||||||
|
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
|
||||||
|
skewY(sy) {
|
||||||
|
return newInstance(this[VALUES]).skewYSelf(sy)
|
||||||
|
}
|
||||||
|
|
||||||
|
skewYSelf(sy) {
|
||||||
|
if (typeof sy !== 'number') {
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
|
||||||
|
const t = Math.tan(sy * RAD_PER_DEGREE)
|
||||||
|
|
||||||
|
this[VALUES] = multiply([1, t, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1], this[VALUES])
|
||||||
|
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
|
||||||
|
flipX() {
|
||||||
|
return newInstance(multiply([-1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1], this[VALUES]))
|
||||||
|
}
|
||||||
|
|
||||||
|
flipY() {
|
||||||
|
return newInstance(multiply([1, 0, 0, 0, 0, -1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1], this[VALUES]))
|
||||||
|
}
|
||||||
|
|
||||||
|
inverse() {
|
||||||
|
return newInstance(this[VALUES]).invertSelf()
|
||||||
|
}
|
||||||
|
|
||||||
|
invertSelf() {
|
||||||
|
if (this[IS_2D]) {
|
||||||
|
const det = this[VALUES][A] * this[VALUES][D] - this[VALUES][B] * this[VALUES][C]
|
||||||
|
|
||||||
|
// Invertable
|
||||||
|
if (det !== 0) {
|
||||||
|
const result = new DOMMatrix()
|
||||||
|
|
||||||
|
result.a = this[VALUES][D] / det
|
||||||
|
result.b = -this[VALUES][B] / det
|
||||||
|
result.c = -this[VALUES][C] / det
|
||||||
|
result.d = this[VALUES][A] / det
|
||||||
|
result.e = (this[VALUES][C] * this[VALUES][F] - this[VALUES][D] * this[VALUES][E]) / det
|
||||||
|
result.f = (this[VALUES][B] * this[VALUES][E] - this[VALUES][A] * this[VALUES][F]) / det
|
||||||
|
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
// Not invertable
|
||||||
|
else {
|
||||||
|
this[IS_2D] = false
|
||||||
|
|
||||||
|
this[VALUES] = [NaN, NaN, NaN, NaN, NaN, NaN, NaN, NaN, NaN, NaN, NaN, NaN, NaN, NaN, NaN, NaN]
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
throw new Error('3D matrix inversion is not implemented.')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
setMatrixValue(transformList) {
|
||||||
|
const temp = new DOMMatrix(transformList)
|
||||||
|
|
||||||
|
this[VALUES] = temp[VALUES]
|
||||||
|
this[IS_2D] = temp[IS_2D]
|
||||||
|
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
|
||||||
|
transformPoint(point) {
|
||||||
|
const x = point.x
|
||||||
|
const y = point.y
|
||||||
|
const z = point.z
|
||||||
|
const w = point.w
|
||||||
|
|
||||||
|
const values = this[VALUES]
|
||||||
|
|
||||||
|
const nx = values[M11] * x + values[M21] * y + values[M31] * z + values[M41] * w
|
||||||
|
const ny = values[M12] * x + values[M22] * y + values[M32] * z + values[M42] * w
|
||||||
|
const nz = values[M13] * x + values[M23] * y + values[M33] * z + values[M43] * w
|
||||||
|
const nw = values[M14] * x + values[M24] * y + values[M34] * z + values[M44] * w
|
||||||
|
|
||||||
|
return new DOMPoint(nx, ny, nz, nw)
|
||||||
|
}
|
||||||
|
|
||||||
|
toFloat32Array() {
|
||||||
|
return Float32Array.from(this[VALUES])
|
||||||
|
}
|
||||||
|
|
||||||
|
toFloat64Array() {
|
||||||
|
return this[VALUES].slice(0)
|
||||||
|
}
|
||||||
|
|
||||||
|
toJSON() {
|
||||||
|
return {
|
||||||
|
a: this.a,
|
||||||
|
b: this.b,
|
||||||
|
c: this.c,
|
||||||
|
d: this.d,
|
||||||
|
e: this.e,
|
||||||
|
f: this.f,
|
||||||
|
m11: this.m11,
|
||||||
|
m12: this.m12,
|
||||||
|
m13: this.m13,
|
||||||
|
m14: this.m14,
|
||||||
|
m21: this.m21,
|
||||||
|
m22: this.m22,
|
||||||
|
m23: this.m23,
|
||||||
|
m24: this.m24,
|
||||||
|
m31: this.m31,
|
||||||
|
m32: this.m32,
|
||||||
|
m33: this.m33,
|
||||||
|
m34: this.m34,
|
||||||
|
m41: this.m41,
|
||||||
|
m42: this.m42,
|
||||||
|
m43: this.m43,
|
||||||
|
m44: this.m44,
|
||||||
|
is2D: this.is2D,
|
||||||
|
isIdentity: this.isIdentity,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
toString() {
|
||||||
|
if (this.is2D) {
|
||||||
|
return `matrix(${this.a}, ${this.b}, ${this.c}, ${this.d}, ${this.e}, ${this.f})`
|
||||||
|
} else {
|
||||||
|
return `matrix3d(${this[VALUES].join(', ')})`
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const propertyName of [
|
||||||
|
'a',
|
||||||
|
'b',
|
||||||
|
'c',
|
||||||
|
'd',
|
||||||
|
'e',
|
||||||
|
'f',
|
||||||
|
'm11',
|
||||||
|
'm12',
|
||||||
|
'm13',
|
||||||
|
'm14',
|
||||||
|
'm21',
|
||||||
|
'm22',
|
||||||
|
'm23',
|
||||||
|
'm24',
|
||||||
|
'm31',
|
||||||
|
'm32',
|
||||||
|
'm33',
|
||||||
|
'm34',
|
||||||
|
'm41',
|
||||||
|
'm42',
|
||||||
|
'm43',
|
||||||
|
'm44',
|
||||||
|
'is2D',
|
||||||
|
'isIdentity',
|
||||||
|
]) {
|
||||||
|
const propertyDescriptor = Object.getOwnPropertyDescriptor(DOMMatrix.prototype, propertyName)
|
||||||
|
propertyDescriptor.enumerable = true
|
||||||
|
Object.defineProperty(DOMMatrix.prototype, propertyName, propertyDescriptor)
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = { DOMPoint, DOMMatrix, DOMRect }
|
|
@ -0,0 +1,453 @@
|
||||||
|
// Clear all type of caches in Skia
|
||||||
|
export function clearAllCache(): void
|
||||||
|
|
||||||
|
export interface DOMMatrix2DInit {
|
||||||
|
a: number
|
||||||
|
b: number
|
||||||
|
c: number
|
||||||
|
d: number
|
||||||
|
e: number
|
||||||
|
f: number
|
||||||
|
}
|
||||||
|
|
||||||
|
interface DOMMatrixReadOnly {
|
||||||
|
readonly a: number
|
||||||
|
readonly b: number
|
||||||
|
readonly c: number
|
||||||
|
readonly d: number
|
||||||
|
readonly e: number
|
||||||
|
readonly f: number
|
||||||
|
readonly is2D: boolean
|
||||||
|
readonly isIdentity: boolean
|
||||||
|
readonly m11: number
|
||||||
|
readonly m12: number
|
||||||
|
readonly m13: number
|
||||||
|
readonly m14: number
|
||||||
|
readonly m21: number
|
||||||
|
readonly m22: number
|
||||||
|
readonly m23: number
|
||||||
|
readonly m24: number
|
||||||
|
readonly m31: number
|
||||||
|
readonly m32: number
|
||||||
|
readonly m33: number
|
||||||
|
readonly m34: number
|
||||||
|
readonly m41: number
|
||||||
|
readonly m42: number
|
||||||
|
readonly m43: number
|
||||||
|
readonly m44: number
|
||||||
|
flipX(): DOMMatrix
|
||||||
|
flipY(): DOMMatrix
|
||||||
|
inverse(): DOMMatrix
|
||||||
|
multiply(other?: DOMMatrixInit): DOMMatrix
|
||||||
|
rotate(rotX?: number, rotY?: number, rotZ?: number): DOMMatrix
|
||||||
|
rotateAxisAngle(x?: number, y?: number, z?: number, angle?: number): DOMMatrix
|
||||||
|
rotateFromVector(x?: number, y?: number): DOMMatrix
|
||||||
|
scale(
|
||||||
|
scaleX?: number,
|
||||||
|
scaleY?: number,
|
||||||
|
scaleZ?: number,
|
||||||
|
originX?: number,
|
||||||
|
originY?: number,
|
||||||
|
originZ?: number,
|
||||||
|
): DOMMatrix
|
||||||
|
scale3d(scale?: number, originX?: number, originY?: number, originZ?: number): DOMMatrix
|
||||||
|
skewX(sx?: number): DOMMatrix
|
||||||
|
skewY(sy?: number): DOMMatrix
|
||||||
|
toFloat32Array(): Float32Array
|
||||||
|
toFloat64Array(): Float64Array
|
||||||
|
transformPoint(point?: DOMPointInit): DOMPoint
|
||||||
|
translate(tx?: number, ty?: number, tz?: number): DOMMatrix
|
||||||
|
toString(): string
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface DOMMatrix extends DOMMatrixReadOnly {
|
||||||
|
a: number
|
||||||
|
b: number
|
||||||
|
c: number
|
||||||
|
d: number
|
||||||
|
e: number
|
||||||
|
f: number
|
||||||
|
m11: number
|
||||||
|
m12: number
|
||||||
|
m13: number
|
||||||
|
m14: number
|
||||||
|
m21: number
|
||||||
|
m22: number
|
||||||
|
m23: number
|
||||||
|
m24: number
|
||||||
|
m31: number
|
||||||
|
m32: number
|
||||||
|
m33: number
|
||||||
|
m34: number
|
||||||
|
m41: number
|
||||||
|
m42: number
|
||||||
|
m43: number
|
||||||
|
m44: number
|
||||||
|
invertSelf(): DOMMatrix
|
||||||
|
multiplySelf(other?: DOMMatrixInit): DOMMatrix
|
||||||
|
preMultiplySelf(other?: DOMMatrixInit): DOMMatrix
|
||||||
|
rotateAxisAngleSelf(x?: number, y?: number, z?: number, angle?: number): DOMMatrix
|
||||||
|
rotateFromVectorSelf(x?: number, y?: number): DOMMatrix
|
||||||
|
rotateSelf(rotX?: number, rotY?: number, rotZ?: number): DOMMatrix
|
||||||
|
scale3dSelf(scale?: number, originX?: number, originY?: number, originZ?: number): DOMMatrix
|
||||||
|
scaleSelf(
|
||||||
|
scaleX?: number,
|
||||||
|
scaleY?: number,
|
||||||
|
scaleZ?: number,
|
||||||
|
originX?: number,
|
||||||
|
originY?: number,
|
||||||
|
originZ?: number,
|
||||||
|
): DOMMatrix
|
||||||
|
setMatrixValue(transformList: string): DOMMatrix
|
||||||
|
skewXSelf(sx?: number): DOMMatrix
|
||||||
|
skewYSelf(sy?: number): DOMMatrix
|
||||||
|
translateSelf(tx?: number, ty?: number, tz?: number): DOMMatrix
|
||||||
|
toJSON(): { [K in OmitNeverOfMatrix]: DOMMatrix[K] }
|
||||||
|
}
|
||||||
|
|
||||||
|
type OmitMatrixMethod = {
|
||||||
|
[K in keyof DOMMatrix]: DOMMatrix[K] extends (...args: any[]) => any ? never : K
|
||||||
|
}
|
||||||
|
|
||||||
|
type OmitNeverOfMatrix = OmitMatrixMethod[keyof OmitMatrixMethod]
|
||||||
|
|
||||||
|
export const DOMMatrix: {
|
||||||
|
prototype: DOMMatrix
|
||||||
|
new (init?: string | number[]): DOMMatrix
|
||||||
|
fromFloat32Array(array32: Float32Array): DOMMatrix
|
||||||
|
fromFloat64Array(array64: Float64Array): DOMMatrix
|
||||||
|
fromMatrix(other?: DOMMatrixInit): DOMMatrix
|
||||||
|
}
|
||||||
|
|
||||||
|
interface DOMRectReadOnly {
|
||||||
|
readonly bottom: number
|
||||||
|
readonly height: number
|
||||||
|
readonly left: number
|
||||||
|
readonly right: number
|
||||||
|
readonly top: number
|
||||||
|
readonly width: number
|
||||||
|
readonly x: number
|
||||||
|
readonly y: number
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface DOMRect extends DOMRectReadOnly {
|
||||||
|
height: number
|
||||||
|
width: number
|
||||||
|
x: number
|
||||||
|
y: number
|
||||||
|
toJSON(): Omit<this, 'toJSON' | 'fromRect'>
|
||||||
|
}
|
||||||
|
|
||||||
|
export const DOMRect: {
|
||||||
|
prototype: DOMRect
|
||||||
|
new (x?: number, y?: number, width?: number, height?: number): DOMRect
|
||||||
|
fromRect(other?: DOMRectInit): DOMRect
|
||||||
|
}
|
||||||
|
|
||||||
|
interface DOMPointReadOnly {
|
||||||
|
readonly w: number
|
||||||
|
readonly x: number
|
||||||
|
readonly y: number
|
||||||
|
readonly z: number
|
||||||
|
matrixTransform(matrix?: DOMMatrixInit): DOMPoint
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface DOMPoint extends DOMPointReadOnly {
|
||||||
|
w: number
|
||||||
|
x: number
|
||||||
|
y: number
|
||||||
|
z: number
|
||||||
|
toJSON(): Omit<DOMPoint, 'matrixTransform' | 'toJSON'>
|
||||||
|
}
|
||||||
|
|
||||||
|
export const DOMPoint: {
|
||||||
|
prototype: DOMPoint
|
||||||
|
new (x?: number, y?: number, z?: number, w?: number): DOMPoint
|
||||||
|
fromPoint(other?: DOMPointInit): DOMPoint
|
||||||
|
}
|
||||||
|
|
||||||
|
export class ImageData {
|
||||||
|
/**
|
||||||
|
* Returns the one-dimensional array containing the data in RGBA order, as integers in the range 0 to 255.
|
||||||
|
*/
|
||||||
|
readonly data: Uint8ClampedArray
|
||||||
|
/**
|
||||||
|
* Returns the actual dimensions of the data in the ImageData object, in pixels.
|
||||||
|
*/
|
||||||
|
readonly height: number
|
||||||
|
/**
|
||||||
|
* Returns the actual dimensions of the data in the ImageData object, in pixels.
|
||||||
|
*/
|
||||||
|
readonly width: number
|
||||||
|
|
||||||
|
constructor(sw: number, sh: number, attr?: { colorSpace?: ColorSpace })
|
||||||
|
constructor(imageData: ImageData, attr?: { colorSpace?: ColorSpace })
|
||||||
|
constructor(data: Uint8ClampedArray, sw: number, sh?: number)
|
||||||
|
}
|
||||||
|
|
||||||
|
export class Image {
|
||||||
|
constructor()
|
||||||
|
// attrs only affects SVG
|
||||||
|
constructor(width: number, height: number, attrs?: { colorSpace?: ColorSpace })
|
||||||
|
width: number
|
||||||
|
height: number
|
||||||
|
readonly naturalWidth: number
|
||||||
|
readonly naturalHeight: number
|
||||||
|
readonly complete: boolean
|
||||||
|
alt: string
|
||||||
|
src: Buffer
|
||||||
|
}
|
||||||
|
|
||||||
|
export class Path2D {
|
||||||
|
constructor(path?: Path2D | string)
|
||||||
|
|
||||||
|
addPath(path: Path2D, transform?: DOMMatrix2DInit): void
|
||||||
|
arc(x: number, y: number, radius: number, startAngle: number, endAngle: number, anticlockwise?: boolean): void
|
||||||
|
arcTo(x1: number, y1: number, x2: number, y2: number, radius: number): void
|
||||||
|
bezierCurveTo(cp1x: number, cp1y: number, cp2x: number, cp2y: number, x: number, y: number): void
|
||||||
|
closePath(): void
|
||||||
|
ellipse(
|
||||||
|
x: number,
|
||||||
|
y: number,
|
||||||
|
radiusX: number,
|
||||||
|
radiusY: number,
|
||||||
|
rotation: number,
|
||||||
|
startAngle: number,
|
||||||
|
endAngle: number,
|
||||||
|
anticlockwise?: boolean,
|
||||||
|
): void
|
||||||
|
lineTo(x: number, y: number): void
|
||||||
|
moveTo(x: number, y: number): void
|
||||||
|
quadraticCurveTo(cpx: number, cpy: number, x: number, y: number): void
|
||||||
|
rect(x: number, y: number, w: number, h: number): void
|
||||||
|
|
||||||
|
// PathKit methods
|
||||||
|
op(path: Path2D, operation: PathOp): Path2D
|
||||||
|
toSVGString(): string
|
||||||
|
getFillType(): FillType
|
||||||
|
getFillTypeString(): string
|
||||||
|
setFillType(type: FillType): void
|
||||||
|
simplify(): Path2D
|
||||||
|
asWinding(): Path2D
|
||||||
|
stroke(stroke?: StrokeOptions): Path2D
|
||||||
|
transform(transform: DOMMatrix2DInit): Path2D
|
||||||
|
getBounds(): [left: number, top: number, right: number, bottom: number]
|
||||||
|
computeTightBounds(): [left: number, top: number, right: number, bottom: number]
|
||||||
|
trim(start: number, end: number, isComplement?: boolean): Path2D
|
||||||
|
dash(on: number, off: number, phase: number): Path2D
|
||||||
|
equals(path: Path2D): boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface StrokeOptions {
|
||||||
|
width?: number
|
||||||
|
miterLimit?: number
|
||||||
|
cap?: StrokeCap
|
||||||
|
join?: StrokeJoin
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SKRSContext2D
|
||||||
|
extends Omit<
|
||||||
|
CanvasRenderingContext2D,
|
||||||
|
'drawImage' | 'createPattern' | 'getTransform' | 'drawFocusIfNeeded' | 'scrollPathIntoView'
|
||||||
|
> {
|
||||||
|
/**
|
||||||
|
* @param startAngle The angle at which to begin the gradient, in radians. Angle measurements start vertically above the centre and move around clockwise.
|
||||||
|
* @param x The x-axis coordinate of the centre of the gradient.
|
||||||
|
* @param y The y-axis coordinate of the centre of the gradient.
|
||||||
|
*/
|
||||||
|
createConicGradient(startAngle: number, x: number, y: number): CanvasGradient
|
||||||
|
drawImage(image: Image | Canvas, dx: number, dy: number): void
|
||||||
|
drawImage(image: Image | Canvas, dx: number, dy: number, dw: number, dh: number): void
|
||||||
|
drawImage(
|
||||||
|
image: Image | Canvas,
|
||||||
|
sx: number,
|
||||||
|
sy: number,
|
||||||
|
sw: number,
|
||||||
|
sh: number,
|
||||||
|
dx: number,
|
||||||
|
dy: number,
|
||||||
|
dw: number,
|
||||||
|
dh: number,
|
||||||
|
): void
|
||||||
|
createPattern(
|
||||||
|
image: Image | ImageData,
|
||||||
|
repeat: 'repeat' | 'repeat-x' | 'repeat-y' | 'no-repeat' | null,
|
||||||
|
): CanvasPattern
|
||||||
|
getContextAttributes(): { alpha: boolean; desynchronized: boolean }
|
||||||
|
getTransform(): {
|
||||||
|
a: number
|
||||||
|
b: number
|
||||||
|
c: number
|
||||||
|
d: number
|
||||||
|
e: number
|
||||||
|
f: number
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export type ColorSpace = 'srgb' | 'display-p3'
|
||||||
|
|
||||||
|
export interface ContextAttributes {
|
||||||
|
alpha?: boolean
|
||||||
|
colorSpace?: ColorSpace
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SvgCanvas {
|
||||||
|
width: number
|
||||||
|
height: number
|
||||||
|
getContext(contextType: '2d', contextAttributes?: ContextAttributes): SKRSContext2D
|
||||||
|
|
||||||
|
getContent(): Buffer
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AvifConfig {
|
||||||
|
/** 0-100 scale, 100 is lossless */
|
||||||
|
quality?: number
|
||||||
|
/** 0-100 scale */
|
||||||
|
alphaQuality?: number
|
||||||
|
/** rav1e preset 1 (slow) 10 (fast but crappy), default is 4 */
|
||||||
|
speed?: number
|
||||||
|
/** How many threads should be used (0 = match core count) */
|
||||||
|
threads?: number
|
||||||
|
/** set to '4:2:0' to use chroma subsampling, default '4:4:4' */
|
||||||
|
chromaSubsampling?: ChromaSubsampling
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* https://en.wikipedia.org/wiki/Chroma_subsampling#Types_of_sampling_and_subsampling
|
||||||
|
* https://developer.mozilla.org/en-US/docs/Web/Media/Formats/Video_concepts
|
||||||
|
*/
|
||||||
|
export enum ChromaSubsampling {
|
||||||
|
/**
|
||||||
|
* Each of the three Y'CbCr components has the same sample rate, thus there is no chroma subsampling. This scheme is sometimes used in high-end film scanners and cinematic post-production.
|
||||||
|
* Note that "4:4:4" may instead be wrongly referring to R'G'B' color space, which implicitly also does not have any chroma subsampling (except in JPEG R'G'B' can be subsampled).
|
||||||
|
* Formats such as HDCAM SR can record 4:4:4 R'G'B' over dual-link HD-SDI.
|
||||||
|
*/
|
||||||
|
Yuv444 = 0,
|
||||||
|
/**
|
||||||
|
* The two chroma components are sampled at half the horizontal sample rate of luma: the horizontal chroma resolution is halved. This reduces the bandwidth of an uncompressed video signal by one-third.
|
||||||
|
* Many high-end digital video formats and interfaces use this scheme:
|
||||||
|
* - [AVC-Intra 100](https://en.wikipedia.org/wiki/AVC-Intra)
|
||||||
|
* - [Digital Betacam](https://en.wikipedia.org/wiki/Betacam#Digital_Betacam)
|
||||||
|
* - [Betacam SX](https://en.wikipedia.org/wiki/Betacam#Betacam_SX)
|
||||||
|
* - [DVCPRO50](https://en.wikipedia.org/wiki/DV#DVCPRO) and [DVCPRO HD](https://en.wikipedia.org/wiki/DV#DVCPRO_HD)
|
||||||
|
* - [Digital-S](https://en.wikipedia.org/wiki/Digital-S)
|
||||||
|
* - [CCIR 601](https://en.wikipedia.org/wiki/Rec._601) / [Serial Digital Interface](https://en.wikipedia.org/wiki/Serial_digital_interface) / [D1](https://en.wikipedia.org/wiki/D-1_(Sony))
|
||||||
|
* - [ProRes (HQ, 422, LT, and Proxy)](https://en.wikipedia.org/wiki/Apple_ProRes)
|
||||||
|
* - [XDCAM HD422](https://en.wikipedia.org/wiki/XDCAM)
|
||||||
|
* - [Canon MXF HD422](https://en.wikipedia.org/wiki/Canon_XF-300)
|
||||||
|
*/
|
||||||
|
Yuv422 = 1,
|
||||||
|
/**
|
||||||
|
* n 4:2:0, the horizontal sampling is doubled compared to 4:1:1,
|
||||||
|
* but as the **Cb** and **Cr** channels are only sampled on each alternate line in this scheme, the vertical resolution is halved.
|
||||||
|
* The data rate is thus the same.
|
||||||
|
* This fits reasonably well with the PAL color encoding system, since this has only half the vertical chrominance resolution of [NTSC](https://en.wikipedia.org/wiki/NTSC).
|
||||||
|
* It would also fit extremely well with the [SECAM](https://en.wikipedia.org/wiki/SECAM) color encoding system,
|
||||||
|
* since like that format, 4:2:0 only stores and transmits one color channel per line (the other channel being recovered from the previous line).
|
||||||
|
* However, little equipment has actually been produced that outputs a SECAM analogue video signal.
|
||||||
|
* In general, SECAM territories either have to use a PAL-capable display or a [transcoder](https://en.wikipedia.org/wiki/Transcoding) to convert the PAL signal to SECAM for display.
|
||||||
|
*/
|
||||||
|
Yuv420 = 2,
|
||||||
|
/**
|
||||||
|
* What if the chroma subsampling model is 4:0:0?
|
||||||
|
* That says to use every pixel of luma data, but that each row has 0 chroma samples applied to it. The resulting image, then, is comprised solely of the luminance data—a greyscale image.
|
||||||
|
*/
|
||||||
|
Yuv400 = 3,
|
||||||
|
}
|
||||||
|
|
||||||
|
export class Canvas {
|
||||||
|
constructor(width: number, height: number, flag?: SvgExportFlag)
|
||||||
|
|
||||||
|
width: number
|
||||||
|
height: number
|
||||||
|
getContext(contextType: '2d', contextAttributes?: ContextAttributes): SKRSContext2D
|
||||||
|
encodeSync(format: 'webp' | 'jpeg', quality?: number): Buffer
|
||||||
|
encodeSync(format: 'png'): Buffer
|
||||||
|
encodeSync(format: 'avif', cfg?: AvifConfig): Buffer
|
||||||
|
encode(format: 'webp' | 'jpeg', quality?: number): Promise<Buffer>
|
||||||
|
encode(format: 'png'): Promise<Buffer>
|
||||||
|
encode(format: 'avif', cfg?: AvifConfig): Promise<Buffer>
|
||||||
|
|
||||||
|
toBuffer(mime: 'image/png'): Buffer
|
||||||
|
toBuffer(mime: 'image/jpeg' | 'image/webp', quality?: number): Buffer
|
||||||
|
toBuffer(mime: 'image/avif', cfg?: AvifConfig): Buffer
|
||||||
|
// raw pixels
|
||||||
|
data(): Buffer
|
||||||
|
toDataURL(mime?: 'image/png'): string
|
||||||
|
toDataURL(mime: 'image/jpeg' | 'image/webp', quality?: number): string
|
||||||
|
toDataURL(mime?: 'image/jpeg' | 'image/webp' | 'image/png', quality?: number): string
|
||||||
|
toDataURL(mime?: 'image/avif', cfg?: AvifConfig): string
|
||||||
|
|
||||||
|
toDataURLAsync(mime?: 'image/png'): Promise<string>
|
||||||
|
toDataURLAsync(mime: 'image/jpeg' | 'image/webp', quality?: number): Promise<string>
|
||||||
|
toDataURLAsync(mime?: 'image/jpeg' | 'image/webp' | 'image/png', quality?: number): Promise<string>
|
||||||
|
toDataURLAsync(mime?: 'image/avif', cfg?: AvifConfig): Promise<string>
|
||||||
|
}
|
||||||
|
|
||||||
|
export function createCanvas(width: number, height: number): Canvas
|
||||||
|
|
||||||
|
export function createCanvas(width: number, height: number, svgExportFlag: SvgExportFlag): SvgCanvas
|
||||||
|
|
||||||
|
interface IGlobalFonts {
|
||||||
|
readonly families: {
|
||||||
|
family: string
|
||||||
|
styles: {
|
||||||
|
weight: number
|
||||||
|
width: string
|
||||||
|
style: string
|
||||||
|
}[]
|
||||||
|
}[]
|
||||||
|
// return true if succeeded
|
||||||
|
register(font: Buffer, nameAlias?: string): boolean
|
||||||
|
// absolute path
|
||||||
|
registerFromPath(path: string, nameAlias?: string): boolean
|
||||||
|
has(name: string): boolean
|
||||||
|
loadFontsFromDir(path: string): number
|
||||||
|
}
|
||||||
|
|
||||||
|
export const GlobalFonts: IGlobalFonts
|
||||||
|
|
||||||
|
export enum PathOp {
|
||||||
|
Difference = 0, // subtract the op path from the first path
|
||||||
|
Intersect = 1, // intersect the two paths
|
||||||
|
Union = 2, // union (inclusive-or) the two paths
|
||||||
|
Xor = 3, // exclusive-or the two paths
|
||||||
|
ReverseDifference = 4, // subtract the first path from the op path
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum FillType {
|
||||||
|
Winding = 0,
|
||||||
|
EvenOdd = 1,
|
||||||
|
InverseWinding = 2,
|
||||||
|
InverseEvenOdd = 3,
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum StrokeJoin {
|
||||||
|
Miter = 0,
|
||||||
|
Round = 1,
|
||||||
|
Bevel = 2,
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum StrokeCap {
|
||||||
|
Butt = 0,
|
||||||
|
Round = 1,
|
||||||
|
Square = 2,
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum SvgExportFlag {
|
||||||
|
ConvertTextToPaths = 0x01,
|
||||||
|
NoPrettyXML = 0x02,
|
||||||
|
RelativePathEncoding = 0x04,
|
||||||
|
}
|
||||||
|
|
||||||
|
export function convertSVGTextToPath(svg: Buffer | string): Buffer
|
||||||
|
|
||||||
|
export interface LoadImageOptions {
|
||||||
|
alt?: string
|
||||||
|
maxRedirects?: number
|
||||||
|
requestOptions?: import('http').RequestOptions
|
||||||
|
}
|
||||||
|
|
||||||
|
export function loadImage(
|
||||||
|
source: string | URL | Buffer | ArrayBufferLike | Uint8Array | Image | import('stream').Readable,
|
||||||
|
options?: LoadImageOptions,
|
||||||
|
): Promise<Image>
|
|
@ -0,0 +1,96 @@
|
||||||
|
const { platform, homedir } = require('os')
|
||||||
|
const { join } = require('path')
|
||||||
|
|
||||||
|
const {
|
||||||
|
clearAllCache,
|
||||||
|
CanvasRenderingContext2D,
|
||||||
|
CanvasElement,
|
||||||
|
SVGCanvas,
|
||||||
|
Path: Path2D,
|
||||||
|
ImageData,
|
||||||
|
Image,
|
||||||
|
CanvasPattern,
|
||||||
|
GlobalFonts,
|
||||||
|
PathOp,
|
||||||
|
FillType,
|
||||||
|
StrokeJoin,
|
||||||
|
StrokeCap,
|
||||||
|
convertSVGTextToPath,
|
||||||
|
} = require('./js-binding')
|
||||||
|
|
||||||
|
const { DOMPoint, DOMMatrix, DOMRect } = require('./geometry')
|
||||||
|
|
||||||
|
const loadImage = require('./load-image')
|
||||||
|
|
||||||
|
const SvgExportFlag = {
|
||||||
|
ConvertTextToPaths: 0x01,
|
||||||
|
NoPrettyXML: 0x02,
|
||||||
|
RelativePathEncoding: 0x04,
|
||||||
|
}
|
||||||
|
|
||||||
|
// eslint-disable-next-line sonarjs/no-unused-collection
|
||||||
|
const Fonts = []
|
||||||
|
|
||||||
|
Object.defineProperty(GlobalFonts, 'families', {
|
||||||
|
get: function () {
|
||||||
|
return JSON.parse(GlobalFonts.getFamilies())
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
Object.defineProperty(GlobalFonts, 'has', {
|
||||||
|
value: function has(name) {
|
||||||
|
return !!JSON.parse(GlobalFonts.getFamilies()).find(({ family }) => family === name)
|
||||||
|
},
|
||||||
|
configurable: false,
|
||||||
|
enumerable: false,
|
||||||
|
writable: false,
|
||||||
|
})
|
||||||
|
|
||||||
|
function createCanvas(width, height, flag) {
|
||||||
|
const isSvgBackend = typeof flag !== 'undefined'
|
||||||
|
return isSvgBackend ? new SVGCanvas(width, height, flag) : new CanvasElement(width, height)
|
||||||
|
}
|
||||||
|
|
||||||
|
class Canvas {
|
||||||
|
constructor(width, height, flag) {
|
||||||
|
return createCanvas(width, height, flag)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!process.env.DISABLE_SYSTEM_FONTS_LOAD) {
|
||||||
|
GlobalFonts.loadSystemFonts()
|
||||||
|
const platformName = platform()
|
||||||
|
const homedirPath = homedir()
|
||||||
|
switch (platformName) {
|
||||||
|
case 'win32':
|
||||||
|
GlobalFonts.loadFontsFromDir(join(homedirPath, 'AppData', 'Local', 'Microsoft', 'Windows', 'Fonts'))
|
||||||
|
break
|
||||||
|
case 'darwin':
|
||||||
|
GlobalFonts.loadFontsFromDir(join(homedirPath, 'Library', 'Fonts'))
|
||||||
|
break
|
||||||
|
case 'linux':
|
||||||
|
GlobalFonts.loadFontsFromDir(join('usr', 'local', 'share', 'fonts'))
|
||||||
|
GlobalFonts.loadFontsFromDir(join(homedirPath, '.fonts'))
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
clearAllCache,
|
||||||
|
Canvas,
|
||||||
|
createCanvas,
|
||||||
|
Path2D,
|
||||||
|
ImageData,
|
||||||
|
Image,
|
||||||
|
PathOp,
|
||||||
|
FillType,
|
||||||
|
StrokeCap,
|
||||||
|
StrokeJoin,
|
||||||
|
SvgExportFlag,
|
||||||
|
GlobalFonts: GlobalFonts,
|
||||||
|
convertSVGTextToPath,
|
||||||
|
DOMPoint,
|
||||||
|
DOMMatrix,
|
||||||
|
DOMRect,
|
||||||
|
loadImage,
|
||||||
|
}
|
|
@ -0,0 +1,257 @@
|
||||||
|
const { existsSync, readFileSync } = require('fs')
|
||||||
|
const { join } = require('path')
|
||||||
|
|
||||||
|
const { platform, arch } = process
|
||||||
|
|
||||||
|
let nativeBinding = null
|
||||||
|
let localFileExisted = false
|
||||||
|
let loadError = null
|
||||||
|
|
||||||
|
function isMusl() {
|
||||||
|
// For Node 10
|
||||||
|
if (!process.report || typeof process.report.getReport !== 'function') {
|
||||||
|
try {
|
||||||
|
return readFileSync('/usr/bin/ldd', 'utf8').includes('musl')
|
||||||
|
} catch (e) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
const { glibcVersionRuntime } = process.report.getReport().header
|
||||||
|
return !glibcVersionRuntime
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (platform) {
|
||||||
|
case 'android':
|
||||||
|
switch (arch) {
|
||||||
|
case 'arm64':
|
||||||
|
localFileExisted = existsSync(join(__dirname, 'skia.android-arm64.node'))
|
||||||
|
try {
|
||||||
|
if (localFileExisted) {
|
||||||
|
nativeBinding = require('./skia.android-arm64.node')
|
||||||
|
} else {
|
||||||
|
nativeBinding = require('@napi-rs/canvas-android-arm64')
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
loadError = e
|
||||||
|
}
|
||||||
|
break
|
||||||
|
case 'arm':
|
||||||
|
localFileExisted = existsSync(join(__dirname, 'skia.android-arm-eabi.node'))
|
||||||
|
try {
|
||||||
|
if (localFileExisted) {
|
||||||
|
nativeBinding = require('./skia.android-arm-eabi.node')
|
||||||
|
} else {
|
||||||
|
nativeBinding = require('@napi-rs/canvas-android-arm-eabi')
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
loadError = e
|
||||||
|
}
|
||||||
|
break
|
||||||
|
default:
|
||||||
|
throw new Error(`Unsupported architecture on Android ${arch}`)
|
||||||
|
}
|
||||||
|
break
|
||||||
|
case 'win32':
|
||||||
|
switch (arch) {
|
||||||
|
case 'x64':
|
||||||
|
localFileExisted = existsSync(join(__dirname, 'skia.win32-x64-msvc.node'))
|
||||||
|
try {
|
||||||
|
if (localFileExisted) {
|
||||||
|
nativeBinding = require('./skia.win32-x64-msvc.node')
|
||||||
|
} else {
|
||||||
|
nativeBinding = require('@napi-rs/canvas-win32-x64-msvc')
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
loadError = e
|
||||||
|
}
|
||||||
|
break
|
||||||
|
case 'ia32':
|
||||||
|
localFileExisted = existsSync(join(__dirname, 'skia.win32-ia32-msvc.node'))
|
||||||
|
try {
|
||||||
|
if (localFileExisted) {
|
||||||
|
nativeBinding = require('./skia.win32-ia32-msvc.node')
|
||||||
|
} else {
|
||||||
|
nativeBinding = require('@napi-rs/canvas-win32-ia32-msvc')
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
loadError = e
|
||||||
|
}
|
||||||
|
break
|
||||||
|
case 'arm64':
|
||||||
|
localFileExisted = existsSync(join(__dirname, 'skia.win32-arm64-msvc.node'))
|
||||||
|
try {
|
||||||
|
if (localFileExisted) {
|
||||||
|
nativeBinding = require('./skia.win32-arm64-msvc.node')
|
||||||
|
} else {
|
||||||
|
nativeBinding = require('@napi-rs/canvas-win32-arm64-msvc')
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
loadError = e
|
||||||
|
}
|
||||||
|
break
|
||||||
|
default:
|
||||||
|
throw new Error(`Unsupported architecture on Windows: ${arch}`)
|
||||||
|
}
|
||||||
|
break
|
||||||
|
case 'darwin':
|
||||||
|
switch (arch) {
|
||||||
|
case 'x64':
|
||||||
|
localFileExisted = existsSync(join(__dirname, 'skia.darwin-x64.node'))
|
||||||
|
try {
|
||||||
|
if (localFileExisted) {
|
||||||
|
nativeBinding = require('./skia.darwin-x64.node')
|
||||||
|
} else {
|
||||||
|
nativeBinding = require('@napi-rs/canvas-darwin-x64')
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
loadError = e
|
||||||
|
}
|
||||||
|
break
|
||||||
|
case 'arm64':
|
||||||
|
localFileExisted = existsSync(join(__dirname, 'skia.darwin-arm64.node'))
|
||||||
|
try {
|
||||||
|
if (localFileExisted) {
|
||||||
|
nativeBinding = require('./skia.darwin-arm64.node')
|
||||||
|
} else {
|
||||||
|
nativeBinding = require('@napi-rs/canvas-darwin-arm64')
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
loadError = e
|
||||||
|
}
|
||||||
|
break
|
||||||
|
default:
|
||||||
|
throw new Error(`Unsupported architecture on macOS: ${arch}`)
|
||||||
|
}
|
||||||
|
break
|
||||||
|
case 'freebsd':
|
||||||
|
if (arch !== 'x64') {
|
||||||
|
throw new Error(`Unsupported architecture on FreeBSD: ${arch}`)
|
||||||
|
}
|
||||||
|
localFileExisted = existsSync(join(__dirname, 'skia.freebsd-x64.node'))
|
||||||
|
try {
|
||||||
|
if (localFileExisted) {
|
||||||
|
nativeBinding = require('./skia.freebsd-x64.node')
|
||||||
|
} else {
|
||||||
|
nativeBinding = require('@napi-rs/canvas-freebsd-x64')
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
loadError = e
|
||||||
|
}
|
||||||
|
break
|
||||||
|
case 'linux':
|
||||||
|
switch (arch) {
|
||||||
|
case 'x64':
|
||||||
|
if (isMusl()) {
|
||||||
|
localFileExisted = existsSync(join(__dirname, 'skia.linux-x64-musl.node'))
|
||||||
|
try {
|
||||||
|
if (localFileExisted) {
|
||||||
|
nativeBinding = require('./skia.linux-x64-musl.node')
|
||||||
|
} else {
|
||||||
|
nativeBinding = require('@napi-rs/canvas-linux-x64-musl')
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
loadError = e
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
localFileExisted = existsSync(join(__dirname, 'skia.linux-x64-gnu.node'))
|
||||||
|
try {
|
||||||
|
if (localFileExisted) {
|
||||||
|
nativeBinding = require('./skia.linux-x64-gnu.node')
|
||||||
|
} else {
|
||||||
|
nativeBinding = require('@napi-rs/canvas-linux-x64-gnu')
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
loadError = e
|
||||||
|
}
|
||||||
|
}
|
||||||
|
break
|
||||||
|
case 'arm64':
|
||||||
|
if (isMusl()) {
|
||||||
|
localFileExisted = existsSync(join(__dirname, 'skia.linux-arm64-musl.node'))
|
||||||
|
try {
|
||||||
|
if (localFileExisted) {
|
||||||
|
nativeBinding = require('./skia.linux-arm64-musl.node')
|
||||||
|
} else {
|
||||||
|
nativeBinding = require('@napi-rs/canvas-linux-arm64-musl')
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
loadError = e
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
localFileExisted = existsSync(join(__dirname, 'skia.linux-arm64-gnu.node'))
|
||||||
|
try {
|
||||||
|
if (localFileExisted) {
|
||||||
|
nativeBinding = require('./skia.linux-arm64-gnu.node')
|
||||||
|
} else {
|
||||||
|
nativeBinding = require('@napi-rs/canvas-linux-arm64-gnu')
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
loadError = e
|
||||||
|
}
|
||||||
|
}
|
||||||
|
break
|
||||||
|
case 'arm':
|
||||||
|
localFileExisted = existsSync(join(__dirname, 'skia.linux-arm-gnueabihf.node'))
|
||||||
|
try {
|
||||||
|
if (localFileExisted) {
|
||||||
|
nativeBinding = require('./skia.linux-arm-gnueabihf.node')
|
||||||
|
} else {
|
||||||
|
nativeBinding = require('@napi-rs/canvas-linux-arm-gnueabihf')
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
loadError = e
|
||||||
|
}
|
||||||
|
break
|
||||||
|
default:
|
||||||
|
throw new Error(`Unsupported architecture on Linux: ${arch}`)
|
||||||
|
}
|
||||||
|
break
|
||||||
|
default:
|
||||||
|
throw new Error(`Unsupported OS: ${platform}, architecture: ${arch}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!nativeBinding) {
|
||||||
|
if (loadError) {
|
||||||
|
throw loadError
|
||||||
|
}
|
||||||
|
throw new Error(`Failed to load native binding`)
|
||||||
|
}
|
||||||
|
|
||||||
|
const {
|
||||||
|
ChromaSubsampling,
|
||||||
|
SvgExportFlag,
|
||||||
|
CanvasRenderingContext2D,
|
||||||
|
CanvasGradient,
|
||||||
|
ImageData,
|
||||||
|
Image,
|
||||||
|
PathOp,
|
||||||
|
FillType,
|
||||||
|
StrokeCap,
|
||||||
|
StrokeJoin,
|
||||||
|
Path,
|
||||||
|
CanvasPattern,
|
||||||
|
convertSVGTextToPath,
|
||||||
|
CanvasElement,
|
||||||
|
SVGCanvas,
|
||||||
|
clearAllCache,
|
||||||
|
GlobalFonts,
|
||||||
|
} = nativeBinding
|
||||||
|
|
||||||
|
module.exports.ChromaSubsampling = ChromaSubsampling
|
||||||
|
module.exports.SvgExportFlag = SvgExportFlag
|
||||||
|
module.exports.CanvasRenderingContext2D = CanvasRenderingContext2D
|
||||||
|
module.exports.CanvasGradient = CanvasGradient
|
||||||
|
module.exports.ImageData = ImageData
|
||||||
|
module.exports.Image = Image
|
||||||
|
module.exports.PathOp = PathOp
|
||||||
|
module.exports.FillType = FillType
|
||||||
|
module.exports.StrokeCap = StrokeCap
|
||||||
|
module.exports.StrokeJoin = StrokeJoin
|
||||||
|
module.exports.Path = Path
|
||||||
|
module.exports.CanvasPattern = CanvasPattern
|
||||||
|
module.exports.convertSVGTextToPath = convertSVGTextToPath
|
||||||
|
module.exports.CanvasElement = CanvasElement
|
||||||
|
module.exports.SVGCanvas = SVGCanvas
|
||||||
|
module.exports.clearAllCache = clearAllCache
|
||||||
|
module.exports.GlobalFonts = GlobalFonts
|
|
@ -0,0 +1,103 @@
|
||||||
|
const fs = require('fs')
|
||||||
|
const { Image } = require('./js-binding')
|
||||||
|
const { Readable } = require('stream')
|
||||||
|
|
||||||
|
let http, https
|
||||||
|
|
||||||
|
const MAX_REDIRECTS = 20,
|
||||||
|
REDIRECT_STATUSES = new Set([301, 302]),
|
||||||
|
DATA_URI = /^\s*data:/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Loads the given source into canvas Image
|
||||||
|
* @param {string|URL|Image|Buffer} source The image source to be loaded
|
||||||
|
* @param {object} options Options passed to the loader
|
||||||
|
*/
|
||||||
|
module.exports = async function loadImage(source, options = {}) {
|
||||||
|
// load readable stream as image
|
||||||
|
if (source instanceof Readable) return createImage(await consumeStream(source), options.alt)
|
||||||
|
// use the same buffer without copying if the source is a buffer
|
||||||
|
if (Buffer.isBuffer(source)) return createImage(source, options.alt)
|
||||||
|
// construct a buffer if the source is buffer-like
|
||||||
|
if (isBufferLike(source)) return createImage(Buffer.from(source), options.alt)
|
||||||
|
// if the source is Image instance, copy the image src to new image
|
||||||
|
if (source instanceof Image) return createImage(source.src, options.alt)
|
||||||
|
// if source is string and in data uri format, construct image using data uri
|
||||||
|
if (typeof source === 'string' && DATA_URI.test(source)) {
|
||||||
|
const commaIdx = source.indexOf(',')
|
||||||
|
const encoding = source.lastIndexOf('base64', commaIdx) < 0 ? 'utf-8' : 'base64'
|
||||||
|
const data = Buffer.from(source.slice(commaIdx + 1), encoding)
|
||||||
|
return createImage(data, options.alt)
|
||||||
|
}
|
||||||
|
// if source is a string or URL instance
|
||||||
|
if (typeof source === 'string' || source instanceof URL) {
|
||||||
|
// if the source exists as a file, construct image from that file
|
||||||
|
if (fs.existsSync(source)) {
|
||||||
|
return createImage(await fs.promises.readFile(source), options.alt)
|
||||||
|
} else {
|
||||||
|
// the source is a remote url here
|
||||||
|
source = !(source instanceof URL) ? new URL(source) : source
|
||||||
|
// attempt to download the remote source and construct image
|
||||||
|
const data = await new Promise((resolve, reject) =>
|
||||||
|
makeRequest(
|
||||||
|
source,
|
||||||
|
resolve,
|
||||||
|
reject,
|
||||||
|
typeof options.maxRedirects === 'number' && options.maxRedirects >= 0 ? options.maxRedirects : MAX_REDIRECTS,
|
||||||
|
options.requestOptions,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
return createImage(data, options.alt)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// throw error as dont support that source
|
||||||
|
throw new TypeError('unsupported image source')
|
||||||
|
}
|
||||||
|
|
||||||
|
function makeRequest(url, resolve, reject, redirectCount, requestOptions) {
|
||||||
|
const isHttps = url.protocol === 'https:'
|
||||||
|
// lazy load the lib
|
||||||
|
const lib = isHttps ? (!https ? (https = require('https')) : https) : !http ? (http = require('http')) : http
|
||||||
|
|
||||||
|
lib
|
||||||
|
.get(url, requestOptions || {}, (res) => {
|
||||||
|
const shouldRedirect = REDIRECT_STATUSES.has(res.statusCode) && typeof res.headers.location === 'string'
|
||||||
|
if (shouldRedirect && redirectCount > 0)
|
||||||
|
return makeRequest(new URL(res.headers.location), resolve, reject, redirectCount - 1, requestOptions)
|
||||||
|
if (typeof res.statusCode === 'number' && (res.statusCode < 200 || res.statusCode >= 300)) {
|
||||||
|
return reject(new Error(`remote source rejected with status code ${res.statusCode}`))
|
||||||
|
}
|
||||||
|
|
||||||
|
consumeStream(res).then(resolve, reject)
|
||||||
|
})
|
||||||
|
.on('error', reject)
|
||||||
|
}
|
||||||
|
|
||||||
|
// use stream/consumers in the future?
|
||||||
|
function consumeStream(res) {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const chunks = []
|
||||||
|
|
||||||
|
res.on('data', (chunk) => chunks.push(chunk))
|
||||||
|
res.on('end', () => resolve(Buffer.concat(chunks)))
|
||||||
|
res.on('error', reject)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
function createImage(src, alt) {
|
||||||
|
const image = new Image()
|
||||||
|
image.src = src
|
||||||
|
if (typeof alt === 'string') image.alt = alt
|
||||||
|
return image
|
||||||
|
}
|
||||||
|
|
||||||
|
function isBufferLike(src) {
|
||||||
|
return (
|
||||||
|
(src && src.type === 'Buffer') ||
|
||||||
|
Array.isArray(src) ||
|
||||||
|
src instanceof ArrayBuffer ||
|
||||||
|
src instanceof SharedArrayBuffer ||
|
||||||
|
src instanceof Object.getPrototypeOf(Uint8Array)
|
||||||
|
)
|
||||||
|
}
|
|
@ -0,0 +1,145 @@
|
||||||
|
{
|
||||||
|
"name": "@napi-rs/canvas",
|
||||||
|
"version": "0.1.30",
|
||||||
|
"description": "Canvas for Node.js with skia backend",
|
||||||
|
"main": "index.js",
|
||||||
|
"repository": "https://github.com/Brooooooklyn/canvas.git",
|
||||||
|
"license": "MIT",
|
||||||
|
"keywords": [
|
||||||
|
"napi-rs",
|
||||||
|
"NAPI",
|
||||||
|
"N-API",
|
||||||
|
"Rust",
|
||||||
|
"node-addon",
|
||||||
|
"node-addon-api",
|
||||||
|
"canvas",
|
||||||
|
"image",
|
||||||
|
"pdf",
|
||||||
|
"svg",
|
||||||
|
"skia"
|
||||||
|
],
|
||||||
|
"files": [
|
||||||
|
"index.d.ts",
|
||||||
|
"index.js",
|
||||||
|
"geometry.js",
|
||||||
|
"js-binding.js",
|
||||||
|
"load-image.js"
|
||||||
|
],
|
||||||
|
"napi": {
|
||||||
|
"name": "skia",
|
||||||
|
"triples": {
|
||||||
|
"defaults": true,
|
||||||
|
"additional": [
|
||||||
|
"armv7-unknown-linux-gnueabihf",
|
||||||
|
"x86_64-unknown-linux-musl",
|
||||||
|
"aarch64-unknown-linux-gnu",
|
||||||
|
"aarch64-unknown-linux-musl",
|
||||||
|
"aarch64-apple-darwin",
|
||||||
|
"aarch64-linux-android"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 10"
|
||||||
|
},
|
||||||
|
"publishConfig": {
|
||||||
|
"registry": "https://registry.npmjs.org/",
|
||||||
|
"access": "public"
|
||||||
|
},
|
||||||
|
"scripts": {
|
||||||
|
"artifacts": "napi artifacts",
|
||||||
|
"bench": "node -r @swc-node/register benchmark/bench.ts",
|
||||||
|
"build": "napi build --platform --release --js js-binding.js",
|
||||||
|
"build:debug": "napi build --platform --js js-binding.js",
|
||||||
|
"format": "run-p format:source format:rs",
|
||||||
|
"format:rs": "cargo fmt",
|
||||||
|
"format:source": "prettier . -w",
|
||||||
|
"lint": "eslint . -c ./.eslintrc.yml",
|
||||||
|
"prepublishOnly": "pinst --disable && napi prepublish -t npm",
|
||||||
|
"postpublish": "pinst --enable",
|
||||||
|
"test": "ava",
|
||||||
|
"version": "napi version && conventional-changelog -p angular -i CHANGELOG.md -s && git add CHANGELOG.md"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@jimp/core": "^0.16.1",
|
||||||
|
"@jimp/custom": "^0.16.1",
|
||||||
|
"@jimp/jpeg": "^0.16.1",
|
||||||
|
"@jimp/png": "^0.16.1",
|
||||||
|
"@napi-rs/cli": "^2.11.4",
|
||||||
|
"@octokit/rest": "^19.0.4",
|
||||||
|
"@swc-node/register": "^1.5.1",
|
||||||
|
"@types/lodash": "^4.14.184",
|
||||||
|
"@types/node": "^18.7.13",
|
||||||
|
"@typescript-eslint/eslint-plugin": "^5.35.1",
|
||||||
|
"@typescript-eslint/parser": "^5.35.1",
|
||||||
|
"ava": "^4.3.3",
|
||||||
|
"benny": "^3.7.1",
|
||||||
|
"canvas": "^2.9.3",
|
||||||
|
"canvaskit-wasm": "^0.37.0",
|
||||||
|
"colorette": "^2.0.19",
|
||||||
|
"conventional-changelog-cli": "^2.2.2",
|
||||||
|
"echarts": "^5.3.3",
|
||||||
|
"eslint": "^8.22.0",
|
||||||
|
"eslint-config-prettier": "^8.5.0",
|
||||||
|
"eslint-plugin-import": "^2.26.0",
|
||||||
|
"eslint-plugin-prettier": "^4.2.1",
|
||||||
|
"eslint-plugin-sonarjs": "^0.15.0",
|
||||||
|
"husky": "^8.0.1",
|
||||||
|
"lint-staged": "^13.0.3",
|
||||||
|
"lodash": "^4.17.21",
|
||||||
|
"npm-run-all": "^4.1.5",
|
||||||
|
"pinst": "^3.0.0",
|
||||||
|
"png.js": "^0.2.1",
|
||||||
|
"prettier": "^2.7.1",
|
||||||
|
"pretty-bytes": "^6.0.0",
|
||||||
|
"skia-canvas": "^1.0.0",
|
||||||
|
"table": "^6.8.0",
|
||||||
|
"typescript": "^4.7.4"
|
||||||
|
},
|
||||||
|
"lint-staged": {
|
||||||
|
"*.@(js|ts|tsx|yml|yaml|md|json|html)": [
|
||||||
|
"prettier --write"
|
||||||
|
],
|
||||||
|
"*.@(js|ts|tsx)": [
|
||||||
|
"eslint -c .eslintrc.yml --fix"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"ava": {
|
||||||
|
"require": [
|
||||||
|
"@swc-node/register"
|
||||||
|
],
|
||||||
|
"extensions": [
|
||||||
|
"ts"
|
||||||
|
],
|
||||||
|
"files": [
|
||||||
|
"__test__/**/*.spec.ts",
|
||||||
|
"scripts/__test__/**/*.spec.ts"
|
||||||
|
],
|
||||||
|
"workerThreads": false,
|
||||||
|
"cache": false,
|
||||||
|
"timeout": "3m",
|
||||||
|
"environmentVariables": {
|
||||||
|
"SWC_NODE_PROJECT": "./tsconfig.json",
|
||||||
|
"NODE_ENV": "ava"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"prettier": {
|
||||||
|
"printWidth": 120,
|
||||||
|
"semi": false,
|
||||||
|
"trailingComma": "all",
|
||||||
|
"singleQuote": true,
|
||||||
|
"arrowParens": "always"
|
||||||
|
},
|
||||||
|
"packageManager": "yarn@3.2.3",
|
||||||
|
"optionalDependencies": {
|
||||||
|
"@napi-rs/canvas-win32-x64-msvc": "0.1.30",
|
||||||
|
"@napi-rs/canvas-darwin-x64": "0.1.30",
|
||||||
|
"@napi-rs/canvas-linux-x64-gnu": "0.1.30",
|
||||||
|
"@napi-rs/canvas-linux-arm-gnueabihf": "0.1.30",
|
||||||
|
"@napi-rs/canvas-linux-x64-musl": "0.1.30",
|
||||||
|
"@napi-rs/canvas-linux-arm64-gnu": "0.1.30",
|
||||||
|
"@napi-rs/canvas-linux-arm64-musl": "0.1.30",
|
||||||
|
"@napi-rs/canvas-darwin-arm64": "0.1.30",
|
||||||
|
"@napi-rs/canvas-android-arm64": "0.1.30"
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,16 @@
|
||||||
|
# Changelog
|
||||||
|
|
||||||
|
All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines.
|
||||||
|
|
||||||
|
### [1.0.1](https://github.com/skyra-project/gifenc/compare/v1.0.0...v1.0.1) (2022-10-29)
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* NodeNext moduleResolution ([#158](https://github.com/skyra-project/gifenc/issues/158)) ([119a318](https://github.com/skyra-project/gifenc/commit/119a3189a4e0af47262062cc5c7a20bd2bc509d4))
|
||||||
|
|
||||||
|
## 1.0.0 (2021-07-27)
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
- implementation ([#1](https://github.com/skyra-project/gifenc/issues/1)) ([b96df46](https://github.com/skyra-project/gifenc/commit/b96df463fe8e311174425d167515aedf60c37a64))
|
|
@ -0,0 +1,24 @@
|
||||||
|
# The MIT License (MIT)
|
||||||
|
|
||||||
|
Copyright © `2021` `Skyra Project`
|
||||||
|
|
||||||
|
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.
|
|
@ -0,0 +1,159 @@
|
||||||
|
<div align="center">
|
||||||
|
|
||||||
|
# @skyra/gifenc
|
||||||
|
|
||||||
|
**A very fast server-side animated GIF generation for Node.js**
|
||||||
|
|
||||||
|
[![GitHub](https://img.shields.io/github/license/skyra-project/gifenc)](https://github.com/skyra-project/gifenc/blob/main/LICENSE.md)
|
||||||
|
|
||||||
|
[![npm](https://img.shields.io/npm/v/@skyra/gifenc?color=crimson&label=NPM&logo=npm)](https://www.npmjs.com/package/@skyra/gifenc)
|
||||||
|
![npm bundle size minified (scoped)](https://img.shields.io/bundlephobia/min/@skyra/gifenc?label=minified&logo=webpack)
|
||||||
|
![npm bundle size minzipped (scoped)](https://img.shields.io/bundlephobia/minzip/@skyra/gifenc?label=minified&logo=webpack)
|
||||||
|
|
||||||
|
[![Support Server](https://discord.com/api/guilds/254360814063058944/embed.png?style=banner2)](https://join.skyra.pw)
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Features
|
||||||
|
|
||||||
|
- @skyra/gifenc is a GIF encoding utility library to build your next GIFs.
|
||||||
|
- Supports CommonJS and ES Module.
|
||||||
|
- Heavily based on [`gifencoder`](https://github.com/eugeneware/gifencoder), this module wouldn't have existed without its author.
|
||||||
|
|
||||||
|
## Installation
|
||||||
|
|
||||||
|
You can use the following command to install this package, or replace `npm install` with your package manager of choice.
|
||||||
|
|
||||||
|
```sh
|
||||||
|
npm install @skyra/gifenc
|
||||||
|
```
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
> `@skyra/gifenc` is very close to a drop-in replacement for [`gifencoder`](https://www.npmjs.com/package/gifencoder). There are only 2 differences to account for. First of all, the encoder class is named `GifEncoder` and not `GIFEncoder`, and secondly, the metadata methods are chainable.
|
||||||
|
|
||||||
|
### Streaming API - Writing to a file
|
||||||
|
|
||||||
|
```js
|
||||||
|
const { GifEncoder } = require('@skyra/gifenc');
|
||||||
|
const { createWriteStream } = require('node:fs');
|
||||||
|
|
||||||
|
const encoder = new GifEncoder(400, 400);
|
||||||
|
// Set the repeat mode: 0 for repeat, -1 for no-repeat:
|
||||||
|
.setRepeat(0)
|
||||||
|
// Set the frame delay in milliseconds:
|
||||||
|
.setDelay(500)
|
||||||
|
// Set the image quality, 10 is default:
|
||||||
|
.setQuality(10);
|
||||||
|
|
||||||
|
// Create a read stream and pipe it into a file write stream:
|
||||||
|
encoder.createReadStream()
|
||||||
|
.pipe(createWriteStream('my-file.gif'));
|
||||||
|
|
||||||
|
encoder.start();
|
||||||
|
|
||||||
|
// `getFrames` enumerates over frames
|
||||||
|
for (const frame of getFrames()) {
|
||||||
|
encoder.addFrame(frame);
|
||||||
|
}
|
||||||
|
|
||||||
|
encoder.finish();
|
||||||
|
```
|
||||||
|
|
||||||
|
### Streaming API - Get resulting Buffer
|
||||||
|
|
||||||
|
We can use [`streamConsumers.buffer()`](https://nodejs.org/dist/latest-v16.x/docs/api/webstreams.html#streamconsumersbufferstream) from Node.js to convert the stream into a buffer starting with Node.js v16.7.0, if you're using an older version, consider making a function using stream's async iterator (Node.js v10+) or use a package.
|
||||||
|
|
||||||
|
```js
|
||||||
|
const { GifEncoder } = require('@skyra/gifenc');
|
||||||
|
const { buffer } = require('node:stream/consumers');
|
||||||
|
|
||||||
|
const encoder = new GifEncoder(400, 400);
|
||||||
|
|
||||||
|
const stream = encoder.createReadStream();
|
||||||
|
encoder.setRepeat(0).setDelay(500).setQuality(10).start();
|
||||||
|
|
||||||
|
// `getFrames` enumerates over frames
|
||||||
|
for (const frame of getFrames()) {
|
||||||
|
encoder.addFrame(frame);
|
||||||
|
}
|
||||||
|
|
||||||
|
encoder.finish();
|
||||||
|
const result = await buffer(stream);
|
||||||
|
```
|
||||||
|
|
||||||
|
### Using with canvas-constructor
|
||||||
|
|
||||||
|
```js
|
||||||
|
const { GifEncoder } = require('@skyra/gifenc');
|
||||||
|
const { Canvas } = require('canvas-constructor/skia');
|
||||||
|
// const { Canvas } = require('canvas-constructor/cairo');
|
||||||
|
|
||||||
|
const canvas = new Canvas(400, 400);
|
||||||
|
const encoder = new GifEncoder(400, 400);
|
||||||
|
|
||||||
|
const stream = encoder.createReadStream();
|
||||||
|
encoder.setRepeat(0).setDelay(500).setQuality(10).start();
|
||||||
|
|
||||||
|
const colors = ['#98DDCA', '#D5ECC2', '#FFD3B4', '#FFAAA7'];
|
||||||
|
for (const color of colors) {
|
||||||
|
canvas.setColor(color).printRectangle(0, 0, 400, 400);
|
||||||
|
encoder.addFrame(canvas);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ...
|
||||||
|
```
|
||||||
|
|
||||||
|
### Using with ECMAScript Modules
|
||||||
|
|
||||||
|
`@skyra/gifenc` supports ESM out of the box. To import the `GifEncoder` class, you use the following statement:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
import { GifEncoder } from '@skyra/gifenc';
|
||||||
|
```
|
||||||
|
|
||||||
|
## Buy us some doughnuts
|
||||||
|
|
||||||
|
Skyra Project is open source and always will be, even if we don't get donations. That said, we know there are amazing people who
|
||||||
|
may still want to donate just to show their appreciation. Thanks you very much in advance!
|
||||||
|
|
||||||
|
We accept donations through Patreon, BitCoin, Ethereum, and Litecoin. You can use the buttons below to donate through your method of choice.
|
||||||
|
|
||||||
|
| Donate With | QR | Address |
|
||||||
|
| :---------: | :----------------: | :---------------------------------------------------------------------------------------------------------------------------------------: |
|
||||||
|
| Patreon | ![PatreonImage][] | [Click Here](https://www.patreon.com/kyranet) |
|
||||||
|
| PayPal | ![PayPalImage][] | [Click Here](https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=CET28NRZTDQ8L) |
|
||||||
|
| BitCoin | ![BitcoinImage][] | [3JNzCHMTFtxYFWBnVtDM9Tt34zFbKvdwco](bitcoin:3JNzCHMTFtxYFWBnVtDM9Tt34zFbKvdwco?amount=0.01&label=Skyra%20Discord%20Bot) |
|
||||||
|
| Ethereum | ![EthereumImage][] | [0xcB5EDB76Bc9E389514F905D9680589004C00190c](ethereum:0xcB5EDB76Bc9E389514F905D9680589004C00190c?amount=0.01&label=Skyra%20Discord%20Bot) |
|
||||||
|
| Litecoin | ![LitecoinImage][] | [MNVT1keYGMfGp7vWmcYjCS8ntU8LNvjnqM](litecoin:MNVT1keYGMfGp7vWmcYjCS8ntU8LNvjnqM?amount=0.01&label=Skyra%20Discord%20Bot) |
|
||||||
|
|
||||||
|
## Contributors ✨
|
||||||
|
|
||||||
|
Thanks goes to these wonderful people ([emoji key](https://allcontributors.org/docs/en/emoji-key)):
|
||||||
|
|
||||||
|
<!-- ALL-CONTRIBUTORS-LIST:START - Do not remove or modify this section -->
|
||||||
|
<!-- prettier-ignore-start -->
|
||||||
|
<!-- markdownlint-disable -->
|
||||||
|
<table>
|
||||||
|
<tr>
|
||||||
|
<td align="center"><a href="https://github.com/kyranet"><img src="https://avatars0.githubusercontent.com/u/24852502?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Antonio Román</b></sub></a><br /><a href="https://github.com/skyra-project/gifenc/commits?author=kyranet" title="Code">💻</a> <a href="#ideas-kyranet" title="Ideas, Planning, & Feedback">🤔</a> <a href="#infra-kyranet" title="Infrastructure (Hosting, Build-Tools, etc)">🚇</a></td>
|
||||||
|
<td align="center"><a href="https://favware.tech/"><img src="https://avatars.githubusercontent.com/u/4019718?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Jeroen Claassens</b></sub></a><br /><a href="#infra-Favna" title="Infrastructure (Hosting, Build-Tools, etc)">🚇</a> <a href="#maintenance-Favna" title="Maintenance">🚧</a></td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
<!-- markdownlint-restore -->
|
||||||
|
<!-- prettier-ignore-end -->
|
||||||
|
|
||||||
|
<!-- ALL-CONTRIBUTORS-LIST:END -->
|
||||||
|
|
||||||
|
This project follows the [all-contributors](https://github.com/all-contributors/all-contributors) specification. Contributions of any kind welcome!
|
||||||
|
|
||||||
|
<!-- LINK DUMP -->
|
||||||
|
|
||||||
|
[patreonimage]: https://cdn.skyra.pw/gh-assets/patreon.png
|
||||||
|
[paypalimage]: https://cdn.skyra.pw/gh-assets/paypal.png
|
||||||
|
[bitcoinimage]: https://cdn.skyra.pw/gh-assets/bitcoin.png
|
||||||
|
[ethereumimage]: https://cdn.skyra.pw/gh-assets/ethereum.png
|
||||||
|
[litecoinimage]: https://cdn.skyra.pw/gh-assets/litecoin.png
|
|
@ -0,0 +1,3 @@
|
||||||
|
export * from './lib/GifEncoder';
|
||||||
|
export { GifEncoder as JiffEncoder } from './lib/GifEncoder';
|
||||||
|
//# sourceMappingURL=index.d.ts.map
|
|
@ -0,0 +1 @@
|
||||||
|
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,kBAAkB,CAAC;AACjC,OAAO,EAAE,UAAU,IAAI,WAAW,EAAE,MAAM,kBAAkB,CAAC"}
|
|
@ -0,0 +1,8 @@
|
||||||
|
"use strict";
|
||||||
|
Object.defineProperty(exports, "__esModule", { value: true });
|
||||||
|
exports.JiffEncoder = void 0;
|
||||||
|
const tslib_1 = require("tslib");
|
||||||
|
tslib_1.__exportStar(require("./lib/GifEncoder"), exports);
|
||||||
|
var GifEncoder_1 = require("./lib/GifEncoder");
|
||||||
|
Object.defineProperty(exports, "JiffEncoder", { enumerable: true, get: function () { return GifEncoder_1.GifEncoder; } });
|
||||||
|
//# sourceMappingURL=index.js.map
|
|
@ -0,0 +1 @@
|
||||||
|
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";;;;AAAA,2DAAiC;AACjC,+CAA6D;AAApD,yGAAA,UAAU,OAAe"}
|
|
@ -0,0 +1,5 @@
|
||||||
|
import mod from "./index.js";
|
||||||
|
|
||||||
|
export default mod;
|
||||||
|
export const GifEncoder = mod.GifEncoder;
|
||||||
|
export const JiffEncoder = mod.JiffEncoder;
|
|
@ -0,0 +1,51 @@
|
||||||
|
/// <reference types="node" />
|
||||||
|
export declare class ByteBuffer {
|
||||||
|
private written;
|
||||||
|
private data;
|
||||||
|
/**
|
||||||
|
* Constructs the instance.
|
||||||
|
* @param size The amount of bytes to reserve, defaults to 8KB.
|
||||||
|
*/
|
||||||
|
constructor(size?: number);
|
||||||
|
/**
|
||||||
|
* Gets the written data.
|
||||||
|
*/
|
||||||
|
get length(): number;
|
||||||
|
/**
|
||||||
|
* Resets the data.
|
||||||
|
* @note This does not de-allocate the data, instead, it sets the {@link ByteBuffer.written position} to zero.
|
||||||
|
*/
|
||||||
|
reset(): void;
|
||||||
|
/**
|
||||||
|
* Writes a single byte into the buffer.
|
||||||
|
* @param byte The byte to write, between `0x00` and `0xFF`.
|
||||||
|
*/
|
||||||
|
writeByte(byte: number): void;
|
||||||
|
/**
|
||||||
|
* Writes the `byte` value `times` times.
|
||||||
|
* @param byte The byte to write `times` times.
|
||||||
|
* @param times The amount of times to write the `byte`.
|
||||||
|
*/
|
||||||
|
writeTimes(byte: number, times: number): void;
|
||||||
|
/**
|
||||||
|
* Writes `bytes` into the data.
|
||||||
|
* @param bytes The bytes to write.
|
||||||
|
*/
|
||||||
|
writeBytes(bytes: ArrayLike<number>, start?: number, end?: number): void;
|
||||||
|
/**
|
||||||
|
* Gets a sub-array of what was written so far.
|
||||||
|
* @returns The written section of the data.
|
||||||
|
*/
|
||||||
|
toArray(): Buffer;
|
||||||
|
/**
|
||||||
|
* Fills the data with the `byte` value given a range.
|
||||||
|
* @param byte The value to write.
|
||||||
|
* @param start The start index, defaults to `0`.
|
||||||
|
* @param end The end index, defaults to {@link Uint8Array.length `this.data.length`}.
|
||||||
|
*/
|
||||||
|
fill(byte: number, start?: number, end?: number): void;
|
||||||
|
private ensureByte;
|
||||||
|
private ensureBytes;
|
||||||
|
private copyBytes;
|
||||||
|
}
|
||||||
|
//# sourceMappingURL=ByteBuffer.d.ts.map
|
|
@ -0,0 +1 @@
|
||||||
|
{"version":3,"file":"ByteBuffer.d.ts","sourceRoot":"","sources":["../../src/lib/ByteBuffer.ts"],"names":[],"mappings":";AAAA,qBAAa,UAAU;IACtB,OAAO,CAAC,OAAO,CAAK;IACpB,OAAO,CAAC,IAAI,CAAS;IAErB;;;OAGG;gBACgB,IAAI,SAAO;IAI9B;;OAEG;IACH,IAAW,MAAM,IAAI,MAAM,CAE1B;IAED;;;OAGG;IACI,KAAK,IAAI,IAAI;IAIpB;;;OAGG;IACI,SAAS,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI;IAKpC;;;;OAIG;IACI,UAAU,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,IAAI;IAQpD;;;OAGG;IACI,UAAU,CAAC,KAAK,EAAE,SAAS,CAAC,MAAM,CAAC,EAAE,KAAK,SAAI,EAAE,GAAG,SAAe,GAAG,IAAI;IAQhF;;;OAGG;IACI,OAAO,IAAI,MAAM;IAIxB;;;;;OAKG;IACI,IAAI,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,CAAC,EAAE,MAAM,EAAE,GAAG,CAAC,EAAE,MAAM,GAAG,IAAI;IAI7D,OAAO,CAAC,UAAU;IAOlB,OAAO,CAAC,WAAW;IAOnB,OAAO,CAAC,SAAS;CASjB"}
|
|
@ -0,0 +1,103 @@
|
||||||
|
"use strict";
|
||||||
|
Object.defineProperty(exports, "__esModule", { value: true });
|
||||||
|
exports.ByteBuffer = void 0;
|
||||||
|
class ByteBuffer {
|
||||||
|
/**
|
||||||
|
* Constructs the instance.
|
||||||
|
* @param size The amount of bytes to reserve, defaults to 8KB.
|
||||||
|
*/
|
||||||
|
constructor(size = 8192) {
|
||||||
|
Object.defineProperty(this, "written", {
|
||||||
|
enumerable: true,
|
||||||
|
configurable: true,
|
||||||
|
writable: true,
|
||||||
|
value: 0
|
||||||
|
});
|
||||||
|
Object.defineProperty(this, "data", {
|
||||||
|
enumerable: true,
|
||||||
|
configurable: true,
|
||||||
|
writable: true,
|
||||||
|
value: void 0
|
||||||
|
});
|
||||||
|
this.data = Buffer.allocUnsafe(size);
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Gets the written data.
|
||||||
|
*/
|
||||||
|
get length() {
|
||||||
|
return this.written;
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Resets the data.
|
||||||
|
* @note This does not de-allocate the data, instead, it sets the {@link ByteBuffer.written position} to zero.
|
||||||
|
*/
|
||||||
|
reset() {
|
||||||
|
this.written = 0;
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Writes a single byte into the buffer.
|
||||||
|
* @param byte The byte to write, between `0x00` and `0xFF`.
|
||||||
|
*/
|
||||||
|
writeByte(byte) {
|
||||||
|
this.ensureByte();
|
||||||
|
this.data[this.written++] = byte;
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Writes the `byte` value `times` times.
|
||||||
|
* @param byte The byte to write `times` times.
|
||||||
|
* @param times The amount of times to write the `byte`.
|
||||||
|
*/
|
||||||
|
writeTimes(byte, times) {
|
||||||
|
this.ensureBytes(times);
|
||||||
|
for (let i = 0; i < times; i++) {
|
||||||
|
this.data[this.written++] = byte;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Writes `bytes` into the data.
|
||||||
|
* @param bytes The bytes to write.
|
||||||
|
*/
|
||||||
|
writeBytes(bytes, start = 0, end = bytes.length) {
|
||||||
|
this.ensureBytes(end - start);
|
||||||
|
for (let i = start; i < end; i++) {
|
||||||
|
this.data[this.written++] = bytes[i];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Gets a sub-array of what was written so far.
|
||||||
|
* @returns The written section of the data.
|
||||||
|
*/
|
||||||
|
toArray() {
|
||||||
|
return this.data.subarray(0, this.written);
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Fills the data with the `byte` value given a range.
|
||||||
|
* @param byte The value to write.
|
||||||
|
* @param start The start index, defaults to `0`.
|
||||||
|
* @param end The end index, defaults to {@link Uint8Array.length `this.data.length`}.
|
||||||
|
*/
|
||||||
|
fill(byte, start, end) {
|
||||||
|
this.data.fill(byte, start, end);
|
||||||
|
}
|
||||||
|
ensureByte() {
|
||||||
|
if (this.written + 1 >= this.data.length) {
|
||||||
|
const size = this.data.length * 2;
|
||||||
|
this.data = this.copyBytes(size);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ensureBytes(n) {
|
||||||
|
if (this.written + n >= this.data.length) {
|
||||||
|
const size = Math.pow(2, Math.ceil(Math.log(this.written + n) / Math.log(2)));
|
||||||
|
this.data = this.copyBytes(size);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
copyBytes(size) {
|
||||||
|
const data = Buffer.allocUnsafe(size);
|
||||||
|
for (let i = 0; i < this.written; ++i) {
|
||||||
|
data[i] = this.data[i];
|
||||||
|
}
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
exports.ByteBuffer = ByteBuffer;
|
||||||
|
//# sourceMappingURL=ByteBuffer.js.map
|
|
@ -0,0 +1 @@
|
||||||
|
{"version":3,"file":"ByteBuffer.js","sourceRoot":"","sources":["../../src/lib/ByteBuffer.ts"],"names":[],"mappings":";;;AAAA,MAAa,UAAU;IAItB;;;OAGG;IACH,YAAmB,IAAI,GAAG,IAAI;QAP9B;;;;mBAAkB,CAAC;WAAC;QACpB;;;;;WAAqB;QAOpB,IAAI,CAAC,IAAI,GAAG,MAAM,CAAC,WAAW,CAAC,IAAI,CAAC,CAAC;IACtC,CAAC;IAED;;OAEG;IACH,IAAW,MAAM;QAChB,OAAO,IAAI,CAAC,OAAO,CAAC;IACrB,CAAC;IAED;;;OAGG;IACI,KAAK;QACX,IAAI,CAAC,OAAO,GAAG,CAAC,CAAC;IAClB,CAAC;IAED;;;OAGG;IACI,SAAS,CAAC,IAAY;QAC5B,IAAI,CAAC,UAAU,EAAE,CAAC;QAClB,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,OAAO,EAAE,CAAC,GAAG,IAAI,CAAC;IAClC,CAAC;IAED;;;;OAIG;IACI,UAAU,CAAC,IAAY,EAAE,KAAa;QAC5C,IAAI,CAAC,WAAW,CAAC,KAAK,CAAC,CAAC;QAExB,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,KAAK,EAAE,CAAC,EAAE,EAAE;YAC/B,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,OAAO,EAAE,CAAC,GAAG,IAAI,CAAC;SACjC;IACF,CAAC;IAED;;;OAGG;IACI,UAAU,CAAC,KAAwB,EAAE,KAAK,GAAG,CAAC,EAAE,GAAG,GAAG,KAAK,CAAC,MAAM;QACxE,IAAI,CAAC,WAAW,CAAC,GAAG,GAAG,KAAK,CAAC,CAAC;QAE9B,KAAK,IAAI,CAAC,GAAG,KAAK,EAAE,CAAC,GAAG,GAAG,EAAE,CAAC,EAAE,EAAE;YACjC,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,OAAO,EAAE,CAAC,GAAG,KAAK,CAAC,CAAC,CAAC,CAAC;SACrC;IACF,CAAC;IAED;;;OAGG;IACI,OAAO;QACb,OAAO,IAAI,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC,EAAE,IAAI,CAAC,OAAO,CAAC,CAAC;IAC5C,CAAC;IAED;;;;;OAKG;IACI,IAAI,CAAC,IAAY,EAAE,KAAc,EAAE,GAAY;QACrD,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,IAAI,EAAE,KAAK,EAAE,GAAG,CAAC,CAAC;IAClC,CAAC;IAEO,UAAU;QACjB,IAAI,IAAI,CAAC,OAAO,GAAG,CAAC,IAAI,IAAI,CAAC,IAAI,CAAC,MAAM,EAAE;YACzC,MAAM,IAAI,GAAG,IAAI,CAAC,IAAI,CAAC,MAAM,GAAG,CAAC,CAAC;YAClC,IAAI,CAAC,IAAI,GAAG,IAAI,CAAC,SAAS,CAAC,IAAI,CAAC,CAAC;SACjC;IACF,CAAC;IAEO,WAAW,CAAC,CAAS;QAC5B,IAAI,IAAI,CAAC,OAAO,GAAG,CAAC,IAAI,IAAI,CAAC,IAAI,CAAC,MAAM,EAAE;YACzC,MAAM,IAAI,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,GAAG,CAAC,IAAI,CAAC,OAAO,GAAG,CAAC,CAAC,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;YAC9E,IAAI,CAAC,IAAI,GAAG,IAAI,CAAC,SAAS,CAAC,IAAI,CAAC,CAAC;SACjC;IACF,CAAC;IAEO,SAAS,CAAC,IAAY;QAC7B,MAAM,IAAI,GAAG,MAAM,CAAC,WAAW,CAAC,IAAI,CAAC,CAAC;QAEtC,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,IAAI,CAAC,OAAO,EAAE,EAAE,CAAC,EAAE;YACtC,IAAI,CAAC,CAAC,CAAC,GAAG,IAAI,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;SACvB;QAED,OAAO,IAAI,CAAC;IACb,CAAC;CACD;AAtGD,gCAsGC"}
|
|
@ -0,0 +1,279 @@
|
||||||
|
/**
|
||||||
|
* GifEncoder
|
||||||
|
*
|
||||||
|
* Authors
|
||||||
|
* - Kevin Weiner (original Java version - kweiner@fmsware.com)
|
||||||
|
* - Thibault Imbert (AS3 version - bytearray.org)
|
||||||
|
* - Johan Nordberg (JS version - code@johan-nordberg.com)
|
||||||
|
* - Eugene Ware (node.js streaming version - eugene@noblesmaurai.com)
|
||||||
|
* - Antonio Román (TS version - kyradiscord@gmail.com)
|
||||||
|
*/
|
||||||
|
/// <reference types="node" />
|
||||||
|
import { Duplex, Readable } from 'stream';
|
||||||
|
/**
|
||||||
|
* The disposal method code.
|
||||||
|
*
|
||||||
|
* - `0`: No disposal specified. The decoder is not required to take any action.
|
||||||
|
* - `1`: Do not dispose. The graphic is to be left in place.
|
||||||
|
* - `2`: Restore to background color. The area used by the graphic must be restored to the background color.
|
||||||
|
* - `3`: Restore to previous. The decoder is required to restore the area overwritten by the graphic with what was
|
||||||
|
* there prior to rendering the graphic.
|
||||||
|
* - `4` - `7`: To be defined.
|
||||||
|
*/
|
||||||
|
export declare type DisposalCode = 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7;
|
||||||
|
export interface EncoderOptions {
|
||||||
|
/**
|
||||||
|
* The frame delay in milliseconds.
|
||||||
|
* @default 0
|
||||||
|
*/
|
||||||
|
delay?: number;
|
||||||
|
/**
|
||||||
|
* The frames per second, supersedes {@link EncoderOptions.delay} if set.
|
||||||
|
* @default 0
|
||||||
|
*/
|
||||||
|
framerate?: number;
|
||||||
|
/**
|
||||||
|
* The GIF frame disposal code for the last added frame and any subsequent frames.
|
||||||
|
*
|
||||||
|
* Defaults to one of the following values:
|
||||||
|
* - `0` : If `transparent` is set
|
||||||
|
* - `2` : Otherwise
|
||||||
|
*/
|
||||||
|
dispose?: DisposalCode;
|
||||||
|
/**
|
||||||
|
* The number of times to repeat the GIF, between `0` and `65536`, with two special cases:
|
||||||
|
* - `-1`: play once
|
||||||
|
* - `0`: repeat indefinitely
|
||||||
|
* @default -1
|
||||||
|
* @note When set to a value different to `-1`, the GIF will use the Netscape 2.0 extension.
|
||||||
|
*/
|
||||||
|
repeat?: number;
|
||||||
|
/**
|
||||||
|
* The transparent color for the last added frame and any subsequent frames. Since all colors are subject to
|
||||||
|
* modification in the quantization process, the color in the final palette for each frame closest to the given
|
||||||
|
* color becomes the transparent color for that frame. May be set to null to indicate no transparent color.
|
||||||
|
*/
|
||||||
|
transparent?: number | null;
|
||||||
|
/**
|
||||||
|
* The quality of color quantization (conversion of images to the maximum 256 colors allowed by the GIF
|
||||||
|
* specification) between `1` and `30`. Lower values (closer to 1) produce better colors but require significantly
|
||||||
|
* more resources and processing. `10` is the default value as it produces good color mapping at reasonable speeds.
|
||||||
|
*
|
||||||
|
* @note Values greater than 20 do not yield significant improvements in speed.
|
||||||
|
*/
|
||||||
|
quality?: number;
|
||||||
|
}
|
||||||
|
export declare class GifEncoder {
|
||||||
|
/**
|
||||||
|
* The GIF image's width, between `1` and `65536`.
|
||||||
|
*/
|
||||||
|
readonly width: number;
|
||||||
|
/**
|
||||||
|
* The GIF image's height, between `1` and `65536`.
|
||||||
|
*/
|
||||||
|
readonly height: number;
|
||||||
|
/**
|
||||||
|
* The transparent color, `null` if no transparent color is given.
|
||||||
|
*/
|
||||||
|
private transparent;
|
||||||
|
/**
|
||||||
|
* The transparent index in the color table.
|
||||||
|
*/
|
||||||
|
private transparentIndex;
|
||||||
|
/**
|
||||||
|
* Number between `-1` and `65536`, `-1` indicating no repeat (GIF89a specification), otherwise repeating `repeat`
|
||||||
|
* times with the exception of `0`, which repeats indefinitely.
|
||||||
|
*/
|
||||||
|
private repeat;
|
||||||
|
/**
|
||||||
|
* Frame delay in hundredths of a second (1 = 10ms).
|
||||||
|
*/
|
||||||
|
private delay;
|
||||||
|
/**
|
||||||
|
* The current frame.
|
||||||
|
*/
|
||||||
|
private image;
|
||||||
|
/**
|
||||||
|
* The BGR byte array from the current frame.
|
||||||
|
*/
|
||||||
|
private pixels;
|
||||||
|
/**
|
||||||
|
* The converted frame indexed to the palette.
|
||||||
|
*/
|
||||||
|
private indexedPixels;
|
||||||
|
/**
|
||||||
|
* The number of bit planes.
|
||||||
|
*/
|
||||||
|
private colorDepth;
|
||||||
|
/**
|
||||||
|
* The RGB palette.
|
||||||
|
*/
|
||||||
|
private colorPalette;
|
||||||
|
/**
|
||||||
|
* The active palette entries.
|
||||||
|
*/
|
||||||
|
private usedEntry;
|
||||||
|
/**
|
||||||
|
* The disposal code (`-1` = determine defaults).
|
||||||
|
*/
|
||||||
|
private disposalMode;
|
||||||
|
/**
|
||||||
|
* Whether or not this is the first frame.
|
||||||
|
*/
|
||||||
|
private firstFrame;
|
||||||
|
/**
|
||||||
|
* The sample interval for the quantifier.
|
||||||
|
*/
|
||||||
|
private sample;
|
||||||
|
/**
|
||||||
|
* Whether or not we started encoding.
|
||||||
|
*/
|
||||||
|
private started;
|
||||||
|
/**
|
||||||
|
* The readable streams.
|
||||||
|
*/
|
||||||
|
private readableStreams;
|
||||||
|
/**
|
||||||
|
* The output buffer.
|
||||||
|
*/
|
||||||
|
private byteBuffer;
|
||||||
|
/**
|
||||||
|
* Constructs the GIF encoder.
|
||||||
|
* @param width An integer representing the GIF image's width, between `1` and `65536`.
|
||||||
|
* @param height An integer representing the GIF image's height, between `1` and `65536`.
|
||||||
|
*/
|
||||||
|
constructor(width: number, height: number);
|
||||||
|
/**
|
||||||
|
* Creates a readable stream and pushes it to the encoder's {@link GifEncoder.readableStreams readable streams}.
|
||||||
|
* @returns The new readable stream.
|
||||||
|
* @example
|
||||||
|
* ```javascript
|
||||||
|
* const encoder = new GifEncoder(320, 240);
|
||||||
|
*
|
||||||
|
* // Stream the results as they are available into hello.gif
|
||||||
|
* encoder.createReadStream().pipe(fs.createWriteStream('hello.gif'));
|
||||||
|
* ```
|
||||||
|
*/
|
||||||
|
createReadStream(): Readable;
|
||||||
|
/**
|
||||||
|
* Uses an existing readable stream and pushes it to the encoder's {@link GifEncoder.readableStreams readable streams}.
|
||||||
|
* @param readable The readable stream to use.
|
||||||
|
* @returns The given readable stream.
|
||||||
|
*/
|
||||||
|
createReadStream<T extends Readable>(readable: T): T;
|
||||||
|
/**
|
||||||
|
* Creates a write stream.
|
||||||
|
* @param options The options for the write stream.
|
||||||
|
* @returns A {@link Duplex}.
|
||||||
|
* @example
|
||||||
|
* ```typescript
|
||||||
|
* const { GifEncoder } = require('@skyra/gifenc');
|
||||||
|
* const encoder = new GifEncoder(400, 200);
|
||||||
|
*
|
||||||
|
* pngStreamGenerator() // A user-defined `Readable`.
|
||||||
|
* .pipe(encoder.createWriteStream({ repeat: -1, delay: 500, quality: 10 }))
|
||||||
|
* .pipe(fs.createWriteStream('runningKitten.gif'));
|
||||||
|
* ```
|
||||||
|
*/
|
||||||
|
createWriteStream(options?: EncoderOptions): Duplex;
|
||||||
|
/**
|
||||||
|
* Sets the delay time between each frame, or changes it for subsequent frames (applies to the next frame added).
|
||||||
|
* @param delay The delay between frames, in milliseconds. Must be a number between `655360` and `10`.
|
||||||
|
*/
|
||||||
|
setDelay(delay: number): this;
|
||||||
|
/**
|
||||||
|
* Sets frame rate in frames per second.
|
||||||
|
* @param fps The amount of frames per second, maximum is `100` frames per second.
|
||||||
|
*/
|
||||||
|
setFramerate(fps: number): this;
|
||||||
|
/**
|
||||||
|
* Sets the GIF frame disposal code for the last added frame and any subsequent frames.
|
||||||
|
*
|
||||||
|
* Defaults to one of the following values:
|
||||||
|
* - `0` : If `transparent` is set
|
||||||
|
* - `2` : Otherwise
|
||||||
|
*
|
||||||
|
* @param disposalCode The disposal code.
|
||||||
|
* @see {@link DisposalCode}
|
||||||
|
*/
|
||||||
|
setDispose(disposalCode: DisposalCode): this;
|
||||||
|
/**
|
||||||
|
* Sets the number of times the set of GIF frames should be played.
|
||||||
|
* @param repeat The number of times between `-1` and `65536` to repeat the GIF, with two special cases:
|
||||||
|
* - `-1` (**default**): play once
|
||||||
|
* - `0`: repeat indefinitely
|
||||||
|
*
|
||||||
|
* @note This method has no effect after the first image was added.
|
||||||
|
*/
|
||||||
|
setRepeat(repeat: number): this;
|
||||||
|
/**
|
||||||
|
* Sets the transparent color for the last added frame and any subsequent frames. Since all colors are subject to
|
||||||
|
* modification in the quantization process, the color in the final palette for each frame closest to the given
|
||||||
|
* color becomes the transparent color for that frame. May be set to null to indicate no transparent color.
|
||||||
|
* @param color The color to be set in transparent pixels.
|
||||||
|
*/
|
||||||
|
setTransparent(color: number | null): this;
|
||||||
|
/**
|
||||||
|
* Sets the quality of color quantization (conversion of images to the maximum 256 colors allowed by the GIF
|
||||||
|
* specification). Lower values (`minimum` = 1) produce better colors, but slow processing significantly. `10` is
|
||||||
|
* the default, and produces good color mapping at reasonable speeds. Values greater than 20 do not yield
|
||||||
|
* significant improvements in speed.
|
||||||
|
* @param quality A number between `1` and `30`.
|
||||||
|
*/
|
||||||
|
setQuality(quality: number): this;
|
||||||
|
/**
|
||||||
|
* Adds the next GIF frame. The frame is not written immediately, but is actually deferred until the next frame is
|
||||||
|
* received so that timing data can be inserted. Calling {@link GifEncoder.finish} will flush all frames.
|
||||||
|
* @param imageData The image data to add into the next frame.
|
||||||
|
*/
|
||||||
|
addFrame(imageData: Pick<CanvasRenderingContext2D, 'getImageData'> | Uint8ClampedArray): void;
|
||||||
|
/**
|
||||||
|
* Adds final trailer to the GIF stream, if you don't call the finish method the GIF stream will not be valid.
|
||||||
|
*/
|
||||||
|
finish(): void;
|
||||||
|
/**
|
||||||
|
* Writes the GIF file header
|
||||||
|
*/
|
||||||
|
start(): void;
|
||||||
|
private end;
|
||||||
|
private emit;
|
||||||
|
/**
|
||||||
|
* Analyzes current frame colors and creates a color map.
|
||||||
|
*/
|
||||||
|
private analyzePixels;
|
||||||
|
/**
|
||||||
|
* Returns index of palette color closest to c.
|
||||||
|
* @param color The color to compare.
|
||||||
|
*/
|
||||||
|
private findClosest;
|
||||||
|
/**
|
||||||
|
* Updates {@link GifEncoder.pixels} by creating an RGB-formatted {@link Uint8Array} from the RGBA-formatted data.
|
||||||
|
*/
|
||||||
|
private getImagePixels;
|
||||||
|
/**
|
||||||
|
* Writes the GCE (Graphic Control Extension).
|
||||||
|
*/
|
||||||
|
private writeGraphicControlExtension;
|
||||||
|
/**
|
||||||
|
* Writes the ID (Image Descriptor).
|
||||||
|
*/
|
||||||
|
private writeImageDescriptor;
|
||||||
|
/**
|
||||||
|
* Writes the LSD (Logical Screen Descriptor)
|
||||||
|
*/
|
||||||
|
private writeLogicalScreenDescriptor;
|
||||||
|
/**
|
||||||
|
* Writes the Netscape application extension to define repeat count.
|
||||||
|
*/
|
||||||
|
private writeNetscapeExtension;
|
||||||
|
/**
|
||||||
|
* Writes the color table palette.
|
||||||
|
*/
|
||||||
|
private writePalette;
|
||||||
|
private writeShort;
|
||||||
|
/**
|
||||||
|
* Encodes and writes pixel data into {@link GifEncoder.byteBuffer}.
|
||||||
|
*/
|
||||||
|
private writePixels;
|
||||||
|
}
|
||||||
|
//# sourceMappingURL=GifEncoder.d.ts.map
|
|
@ -0,0 +1 @@
|
||||||
|
{"version":3,"file":"GifEncoder.d.ts","sourceRoot":"","sources":["../../src/lib/GifEncoder.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;;AAEH,OAAO,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,QAAQ,CAAC;AAkB1C;;;;;;;;;GASG;AACH,oBAAY,YAAY,GAAG,CAAC,GAAG,CAAC,GAAG,CAAC,GAAG,CAAC,GAAG,CAAC,GAAG,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC;AAEzD,MAAM,WAAW,cAAc;IAC9B;;;OAGG;IACH,KAAK,CAAC,EAAE,MAAM,CAAC;IAEf;;;OAGG;IACH,SAAS,CAAC,EAAE,MAAM,CAAC;IAEnB;;;;;;OAMG;IACH,OAAO,CAAC,EAAE,YAAY,CAAC;IAEvB;;;;;;OAMG;IACH,MAAM,CAAC,EAAE,MAAM,CAAC;IAEhB;;;;OAIG;IACH,WAAW,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAE5B;;;;;;OAMG;IACH,OAAO,CAAC,EAAE,MAAM,CAAC;CACjB;AAED,qBAAa,UAAU;IACtB;;OAEG;IACH,SAAgB,KAAK,EAAE,MAAM,CAAC;IAE9B;;OAEG;IACH,SAAgB,MAAM,EAAE,MAAM,CAAC;IAE/B;;OAEG;IACH,OAAO,CAAC,WAAW,CAAuB;IAE1C;;OAEG;IACH,OAAO,CAAC,gBAAgB,CAAK;IAE7B;;;OAGG;IACH,OAAO,CAAC,MAAM,CAAM;IAEpB;;OAEG;IACH,OAAO,CAAC,KAAK,CAAK;IAElB;;OAEG;IACH,OAAO,CAAC,KAAK,CAAkC;IAE/C;;OAEG;IACH,OAAO,CAAC,MAAM,CAA2B;IAEzC;;OAEG;IACH,OAAO,CAAC,aAAa,CAA2B;IAEhD;;OAEG;IACH,OAAO,CAAC,UAAU,CAAuB;IAEzC;;OAEG;IACH,OAAO,CAAC,YAAY,CAA6B;IAEjD;;OAEG;IACH,OAAO,CAAC,SAAS,CAAiB;IAElC;;OAEG;IACH,OAAO,CAAC,YAAY,CAAyB;IAE7C;;OAEG;IACH,OAAO,CAAC,UAAU,CAAQ;IAE1B;;OAEG;IACH,OAAO,CAAC,MAAM,CAAM;IAEpB;;OAEG;IACH,OAAO,CAAC,OAAO,CAAS;IAExB;;OAEG;IACH,OAAO,CAAC,eAAe,CAAkB;IAEzC;;OAEG;IACH,OAAO,CAAC,UAAU,CAAoB;IAEtC;;;;OAIG;gBACgB,KAAK,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM;IAKhD;;;;;;;;;;OAUG;IACI,gBAAgB,IAAI,QAAQ;IACnC;;;;OAIG;IACI,gBAAgB,CAAC,CAAC,SAAS,QAAQ,EAAE,QAAQ,EAAE,CAAC,GAAG,CAAC;IAW3D;;;;;;;;;;;;;OAaG;IACI,iBAAiB,CAAC,OAAO,CAAC,EAAE,cAAc,GAAG,MAAM;IA8B1D;;;OAGG;IACI,QAAQ,CAAC,KAAK,EAAE,MAAM,GAAG,IAAI;IAKpC;;;OAGG;IACI,YAAY,CAAC,GAAG,EAAE,MAAM,GAAG,IAAI;IAKtC;;;;;;;;;OASG;IACI,UAAU,CAAC,YAAY,EAAE,YAAY,GAAG,IAAI;IAKnD;;;;;;;OAOG;IACI,SAAS,CAAC,MAAM,EAAE,MAAM,GAAG,IAAI;IAKtC;;;;;OAKG;IACI,cAAc,CAAC,KAAK,EAAE,MAAM,GAAG,IAAI,GAAG,IAAI;IAKjD;;;;;;OAMG;IACI,UAAU,CAAC,OAAO,EAAE,MAAM,GAAG,IAAI;IAMxC;;;;OAIG;IACI,QAAQ,CAAC,SAAS,EAAE,IAAI,CAAC,wBAAwB,EAAE,cAAc,CAAC,GAAG,iBAAiB;IA4B7F;;OAEG;IACI,MAAM;IAKb;;OAEG;IACI,KAAK;IAMZ,OAAO,CAAC,GAAG;IAWX,OAAO,CAAC,IAAI;IAWZ;;OAEG;IACH,OAAO,CAAC,aAAa;IAsCrB;;;OAGG;IACH,OAAO,CAAC,WAAW;IAyBnB;;OAEG;IACH,OAAO,CAAC,cAAc;IAgBtB;;OAEG;IACH,OAAO,CAAC,4BAA4B;IAmCpC;;OAEG;IACH,OAAO,CAAC,oBAAoB;IAmB5B;;OAEG;IACH,OAAO,CAAC,4BAA4B;IAmBpC;;OAEG;IACH,OAAO,CAAC,sBAAsB;IAa9B;;OAEG;IACH,OAAO,CAAC,YAAY;IAKpB,OAAO,CAAC,UAAU;IAKlB;;OAEG;IACH,OAAO,CAAC,WAAW;CAInB"}
|
|
@ -0,0 +1,562 @@
|
||||||
|
"use strict";
|
||||||
|
/**
|
||||||
|
* GifEncoder
|
||||||
|
*
|
||||||
|
* Authors
|
||||||
|
* - Kevin Weiner (original Java version - kweiner@fmsware.com)
|
||||||
|
* - Thibault Imbert (AS3 version - bytearray.org)
|
||||||
|
* - Johan Nordberg (JS version - code@johan-nordberg.com)
|
||||||
|
* - Eugene Ware (node.js streaming version - eugene@noblesmaurai.com)
|
||||||
|
* - Antonio Román (TS version - kyradiscord@gmail.com)
|
||||||
|
*/
|
||||||
|
Object.defineProperty(exports, "__esModule", { value: true });
|
||||||
|
exports.GifEncoder = void 0;
|
||||||
|
const stream_1 = require("stream");
|
||||||
|
const util_1 = require("util");
|
||||||
|
const ByteBuffer_1 = require("./ByteBuffer");
|
||||||
|
const LZWEncoder_1 = require("./LZWEncoder");
|
||||||
|
const NeuQuant_1 = require("./NeuQuant");
|
||||||
|
const NOP = () => {
|
||||||
|
// no-op
|
||||||
|
};
|
||||||
|
const GIF_HEADER = new TextEncoder().encode('GIF89a');
|
||||||
|
const NETSCAPE_HEADER = new Uint8Array([0x4e, 0x45, 0x54, 0x53, 0x43, 0x41, 0x50, 0x45, 0x32, 0x2e, 0x30]); // NETSCAPE2.0
|
||||||
|
/**
|
||||||
|
* The color table size (bits - 1).
|
||||||
|
*/
|
||||||
|
const PALETTE_SIZE = 7;
|
||||||
|
class GifEncoder {
|
||||||
|
/**
|
||||||
|
* Constructs the GIF encoder.
|
||||||
|
* @param width An integer representing the GIF image's width, between `1` and `65536`.
|
||||||
|
* @param height An integer representing the GIF image's height, between `1` and `65536`.
|
||||||
|
*/
|
||||||
|
constructor(width, height) {
|
||||||
|
/**
|
||||||
|
* The GIF image's width, between `1` and `65536`.
|
||||||
|
*/
|
||||||
|
Object.defineProperty(this, "width", {
|
||||||
|
enumerable: true,
|
||||||
|
configurable: true,
|
||||||
|
writable: true,
|
||||||
|
value: void 0
|
||||||
|
});
|
||||||
|
/**
|
||||||
|
* The GIF image's height, between `1` and `65536`.
|
||||||
|
*/
|
||||||
|
Object.defineProperty(this, "height", {
|
||||||
|
enumerable: true,
|
||||||
|
configurable: true,
|
||||||
|
writable: true,
|
||||||
|
value: void 0
|
||||||
|
});
|
||||||
|
/**
|
||||||
|
* The transparent color, `null` if no transparent color is given.
|
||||||
|
*/
|
||||||
|
Object.defineProperty(this, "transparent", {
|
||||||
|
enumerable: true,
|
||||||
|
configurable: true,
|
||||||
|
writable: true,
|
||||||
|
value: null
|
||||||
|
});
|
||||||
|
/**
|
||||||
|
* The transparent index in the color table.
|
||||||
|
*/
|
||||||
|
Object.defineProperty(this, "transparentIndex", {
|
||||||
|
enumerable: true,
|
||||||
|
configurable: true,
|
||||||
|
writable: true,
|
||||||
|
value: 0
|
||||||
|
});
|
||||||
|
/**
|
||||||
|
* Number between `-1` and `65536`, `-1` indicating no repeat (GIF89a specification), otherwise repeating `repeat`
|
||||||
|
* times with the exception of `0`, which repeats indefinitely.
|
||||||
|
*/
|
||||||
|
Object.defineProperty(this, "repeat", {
|
||||||
|
enumerable: true,
|
||||||
|
configurable: true,
|
||||||
|
writable: true,
|
||||||
|
value: -1
|
||||||
|
});
|
||||||
|
/**
|
||||||
|
* Frame delay in hundredths of a second (1 = 10ms).
|
||||||
|
*/
|
||||||
|
Object.defineProperty(this, "delay", {
|
||||||
|
enumerable: true,
|
||||||
|
configurable: true,
|
||||||
|
writable: true,
|
||||||
|
value: 0
|
||||||
|
});
|
||||||
|
/**
|
||||||
|
* The current frame.
|
||||||
|
*/
|
||||||
|
Object.defineProperty(this, "image", {
|
||||||
|
enumerable: true,
|
||||||
|
configurable: true,
|
||||||
|
writable: true,
|
||||||
|
value: null
|
||||||
|
});
|
||||||
|
/**
|
||||||
|
* The BGR byte array from the current frame.
|
||||||
|
*/
|
||||||
|
Object.defineProperty(this, "pixels", {
|
||||||
|
enumerable: true,
|
||||||
|
configurable: true,
|
||||||
|
writable: true,
|
||||||
|
value: null
|
||||||
|
});
|
||||||
|
/**
|
||||||
|
* The converted frame indexed to the palette.
|
||||||
|
*/
|
||||||
|
Object.defineProperty(this, "indexedPixels", {
|
||||||
|
enumerable: true,
|
||||||
|
configurable: true,
|
||||||
|
writable: true,
|
||||||
|
value: null
|
||||||
|
});
|
||||||
|
/**
|
||||||
|
* The number of bit planes.
|
||||||
|
*/
|
||||||
|
Object.defineProperty(this, "colorDepth", {
|
||||||
|
enumerable: true,
|
||||||
|
configurable: true,
|
||||||
|
writable: true,
|
||||||
|
value: null
|
||||||
|
});
|
||||||
|
/**
|
||||||
|
* The RGB palette.
|
||||||
|
*/
|
||||||
|
Object.defineProperty(this, "colorPalette", {
|
||||||
|
enumerable: true,
|
||||||
|
configurable: true,
|
||||||
|
writable: true,
|
||||||
|
value: null
|
||||||
|
});
|
||||||
|
/**
|
||||||
|
* The active palette entries.
|
||||||
|
*/
|
||||||
|
Object.defineProperty(this, "usedEntry", {
|
||||||
|
enumerable: true,
|
||||||
|
configurable: true,
|
||||||
|
writable: true,
|
||||||
|
value: []
|
||||||
|
});
|
||||||
|
/**
|
||||||
|
* The disposal code (`-1` = determine defaults).
|
||||||
|
*/
|
||||||
|
Object.defineProperty(this, "disposalMode", {
|
||||||
|
enumerable: true,
|
||||||
|
configurable: true,
|
||||||
|
writable: true,
|
||||||
|
value: -1
|
||||||
|
});
|
||||||
|
/**
|
||||||
|
* Whether or not this is the first frame.
|
||||||
|
*/
|
||||||
|
Object.defineProperty(this, "firstFrame", {
|
||||||
|
enumerable: true,
|
||||||
|
configurable: true,
|
||||||
|
writable: true,
|
||||||
|
value: true
|
||||||
|
});
|
||||||
|
/**
|
||||||
|
* The sample interval for the quantifier.
|
||||||
|
*/
|
||||||
|
Object.defineProperty(this, "sample", {
|
||||||
|
enumerable: true,
|
||||||
|
configurable: true,
|
||||||
|
writable: true,
|
||||||
|
value: 10
|
||||||
|
});
|
||||||
|
/**
|
||||||
|
* Whether or not we started encoding.
|
||||||
|
*/
|
||||||
|
Object.defineProperty(this, "started", {
|
||||||
|
enumerable: true,
|
||||||
|
configurable: true,
|
||||||
|
writable: true,
|
||||||
|
value: false
|
||||||
|
});
|
||||||
|
/**
|
||||||
|
* The readable streams.
|
||||||
|
*/
|
||||||
|
Object.defineProperty(this, "readableStreams", {
|
||||||
|
enumerable: true,
|
||||||
|
configurable: true,
|
||||||
|
writable: true,
|
||||||
|
value: []
|
||||||
|
});
|
||||||
|
/**
|
||||||
|
* The output buffer.
|
||||||
|
*/
|
||||||
|
Object.defineProperty(this, "byteBuffer", {
|
||||||
|
enumerable: true,
|
||||||
|
configurable: true,
|
||||||
|
writable: true,
|
||||||
|
value: new ByteBuffer_1.ByteBuffer()
|
||||||
|
});
|
||||||
|
this.width = ~~width;
|
||||||
|
this.height = ~~height;
|
||||||
|
}
|
||||||
|
createReadStream(readable) {
|
||||||
|
if (!readable) {
|
||||||
|
readable = new stream_1.Readable();
|
||||||
|
readable._read = NOP;
|
||||||
|
}
|
||||||
|
this.readableStreams.push(readable);
|
||||||
|
return readable;
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Creates a write stream.
|
||||||
|
* @param options The options for the write stream.
|
||||||
|
* @returns A {@link Duplex}.
|
||||||
|
* @example
|
||||||
|
* ```typescript
|
||||||
|
* const { GifEncoder } = require('@skyra/gifenc');
|
||||||
|
* const encoder = new GifEncoder(400, 200);
|
||||||
|
*
|
||||||
|
* pngStreamGenerator() // A user-defined `Readable`.
|
||||||
|
* .pipe(encoder.createWriteStream({ repeat: -1, delay: 500, quality: 10 }))
|
||||||
|
* .pipe(fs.createWriteStream('runningKitten.gif'));
|
||||||
|
* ```
|
||||||
|
*/
|
||||||
|
createWriteStream(options) {
|
||||||
|
if (options) {
|
||||||
|
if (options.delay !== undefined)
|
||||||
|
this.setDelay(options.delay);
|
||||||
|
if (options.framerate !== undefined)
|
||||||
|
this.setFramerate(options.framerate);
|
||||||
|
if (options.dispose !== undefined)
|
||||||
|
this.setDispose(options.dispose);
|
||||||
|
if (options.repeat !== undefined)
|
||||||
|
this.setRepeat(options.repeat);
|
||||||
|
if (options.transparent !== undefined)
|
||||||
|
this.setTransparent(options.transparent);
|
||||||
|
if (options.quality !== undefined)
|
||||||
|
this.setQuality(options.quality);
|
||||||
|
}
|
||||||
|
const duplex = new stream_1.Duplex({ objectMode: true });
|
||||||
|
duplex._read = NOP;
|
||||||
|
this.createReadStream(duplex);
|
||||||
|
duplex._write = (data, _enc, next) => {
|
||||||
|
if (!this.started)
|
||||||
|
this.start();
|
||||||
|
this.addFrame(data);
|
||||||
|
next();
|
||||||
|
};
|
||||||
|
const end = duplex.end.bind(duplex);
|
||||||
|
// @ts-expect-error This is a Node 17 issue and it should not break using the library
|
||||||
|
duplex.end = (...args) => {
|
||||||
|
end(...args);
|
||||||
|
this.finish();
|
||||||
|
};
|
||||||
|
return duplex;
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Sets the delay time between each frame, or changes it for subsequent frames (applies to the next frame added).
|
||||||
|
* @param delay The delay between frames, in milliseconds. Must be a number between `655360` and `10`.
|
||||||
|
*/
|
||||||
|
setDelay(delay) {
|
||||||
|
this.delay = Math.round(delay / 10);
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Sets frame rate in frames per second.
|
||||||
|
* @param fps The amount of frames per second, maximum is `100` frames per second.
|
||||||
|
*/
|
||||||
|
setFramerate(fps) {
|
||||||
|
this.delay = Math.round(100 / fps);
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Sets the GIF frame disposal code for the last added frame and any subsequent frames.
|
||||||
|
*
|
||||||
|
* Defaults to one of the following values:
|
||||||
|
* - `0` : If `transparent` is set
|
||||||
|
* - `2` : Otherwise
|
||||||
|
*
|
||||||
|
* @param disposalCode The disposal code.
|
||||||
|
* @see {@link DisposalCode}
|
||||||
|
*/
|
||||||
|
setDispose(disposalCode) {
|
||||||
|
if (disposalCode >= 0)
|
||||||
|
this.disposalMode = disposalCode;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Sets the number of times the set of GIF frames should be played.
|
||||||
|
* @param repeat The number of times between `-1` and `65536` to repeat the GIF, with two special cases:
|
||||||
|
* - `-1` (**default**): play once
|
||||||
|
* - `0`: repeat indefinitely
|
||||||
|
*
|
||||||
|
* @note This method has no effect after the first image was added.
|
||||||
|
*/
|
||||||
|
setRepeat(repeat) {
|
||||||
|
this.repeat = repeat;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Sets the transparent color for the last added frame and any subsequent frames. Since all colors are subject to
|
||||||
|
* modification in the quantization process, the color in the final palette for each frame closest to the given
|
||||||
|
* color becomes the transparent color for that frame. May be set to null to indicate no transparent color.
|
||||||
|
* @param color The color to be set in transparent pixels.
|
||||||
|
*/
|
||||||
|
setTransparent(color) {
|
||||||
|
this.transparent = color;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Sets the quality of color quantization (conversion of images to the maximum 256 colors allowed by the GIF
|
||||||
|
* specification). Lower values (`minimum` = 1) produce better colors, but slow processing significantly. `10` is
|
||||||
|
* the default, and produces good color mapping at reasonable speeds. Values greater than 20 do not yield
|
||||||
|
* significant improvements in speed.
|
||||||
|
* @param quality A number between `1` and `30`.
|
||||||
|
*/
|
||||||
|
setQuality(quality) {
|
||||||
|
if (quality < 1)
|
||||||
|
quality = 1;
|
||||||
|
this.sample = quality;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Adds the next GIF frame. The frame is not written immediately, but is actually deferred until the next frame is
|
||||||
|
* received so that timing data can be inserted. Calling {@link GifEncoder.finish} will flush all frames.
|
||||||
|
* @param imageData The image data to add into the next frame.
|
||||||
|
*/
|
||||||
|
addFrame(imageData) {
|
||||||
|
if (util_1.types.isUint8ClampedArray(imageData)) {
|
||||||
|
this.image = imageData;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
this.image = imageData.getImageData(0, 0, this.width, this.height).data;
|
||||||
|
}
|
||||||
|
this.getImagePixels(); // convert to correct format if necessary
|
||||||
|
this.analyzePixels(); // build color table & map pixels
|
||||||
|
if (this.firstFrame) {
|
||||||
|
this.writeLogicalScreenDescriptor(); // logical screen descriptor
|
||||||
|
this.writePalette(); // global color table
|
||||||
|
if (this.repeat >= 0) {
|
||||||
|
// use NS app extension to indicate reps
|
||||||
|
this.writeNetscapeExtension();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this.writeGraphicControlExtension(); // write graphic control extension
|
||||||
|
this.writeImageDescriptor(); // image descriptor
|
||||||
|
if (!this.firstFrame)
|
||||||
|
this.writePalette(); // local color table
|
||||||
|
this.writePixels(); // encode and write pixel data
|
||||||
|
this.firstFrame = false;
|
||||||
|
this.emit();
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Adds final trailer to the GIF stream, if you don't call the finish method the GIF stream will not be valid.
|
||||||
|
*/
|
||||||
|
finish() {
|
||||||
|
this.byteBuffer.writeByte(0x3b); // gif trailer
|
||||||
|
this.end();
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Writes the GIF file header
|
||||||
|
*/
|
||||||
|
start() {
|
||||||
|
this.byteBuffer.writeBytes(GIF_HEADER);
|
||||||
|
this.started = true;
|
||||||
|
this.emit();
|
||||||
|
}
|
||||||
|
end() {
|
||||||
|
if (this.readableStreams.length === 0)
|
||||||
|
return;
|
||||||
|
this.emit();
|
||||||
|
for (const stream of this.readableStreams) {
|
||||||
|
stream.push(null);
|
||||||
|
}
|
||||||
|
this.readableStreams = [];
|
||||||
|
}
|
||||||
|
emit() {
|
||||||
|
if (this.readableStreams.length === 0 || this.byteBuffer.length === 0)
|
||||||
|
return;
|
||||||
|
const data = this.byteBuffer.toArray();
|
||||||
|
for (const stream of this.readableStreams) {
|
||||||
|
stream.push(Buffer.from(data));
|
||||||
|
}
|
||||||
|
this.byteBuffer.reset();
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Analyzes current frame colors and creates a color map.
|
||||||
|
*/
|
||||||
|
analyzePixels() {
|
||||||
|
const pixels = this.pixels;
|
||||||
|
const pixelByteCount = pixels.length;
|
||||||
|
const pixelCount = pixelByteCount / 3;
|
||||||
|
this.indexedPixels = new Uint8Array(pixelCount);
|
||||||
|
const quantifier = new NeuQuant_1.NeuQuant(pixels, this.sample);
|
||||||
|
this.colorPalette = quantifier.getColorMap();
|
||||||
|
// Map image pixels to new palette:
|
||||||
|
let k = 0;
|
||||||
|
for (let j = 0; j < pixelCount; j++) {
|
||||||
|
const r = pixels[k++] & 0xff;
|
||||||
|
const g = pixels[k++] & 0xff;
|
||||||
|
const b = pixels[k++] & 0xff;
|
||||||
|
const index = quantifier.lookupRGB(r, g, b);
|
||||||
|
this.usedEntry[index] = true;
|
||||||
|
this.indexedPixels[j] = index;
|
||||||
|
}
|
||||||
|
this.pixels = null;
|
||||||
|
this.colorDepth = 8;
|
||||||
|
// Get closest match to transparent color if specified:
|
||||||
|
if (this.transparent === null)
|
||||||
|
return;
|
||||||
|
this.transparentIndex = this.findClosest(this.transparent);
|
||||||
|
// Ensure that pixels with full transparency in the RGBA image are using
|
||||||
|
// the selected transparent color index in the indexed image.
|
||||||
|
for (let pixelIndex = 0; pixelIndex < pixelCount; pixelIndex++) {
|
||||||
|
if (this.image[pixelIndex * 4 + 3] === 0) {
|
||||||
|
this.indexedPixels[pixelIndex] = this.transparentIndex;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Returns index of palette color closest to c.
|
||||||
|
* @param color The color to compare.
|
||||||
|
*/
|
||||||
|
findClosest(color) {
|
||||||
|
if (this.colorPalette === null)
|
||||||
|
return -1;
|
||||||
|
const r = (color & 0xff0000) >> 16;
|
||||||
|
const g = (color & 0x00ff00) >> 8;
|
||||||
|
const b = color & 0x0000ff;
|
||||||
|
let minimumIndex = 0;
|
||||||
|
let distanceMinimum = 256 * 256 * 256;
|
||||||
|
const len = this.colorPalette.length;
|
||||||
|
for (let i = 0; i < len;) {
|
||||||
|
const index = i / 3;
|
||||||
|
const dr = r - (this.colorPalette[i++] & 0xff);
|
||||||
|
const dg = g - (this.colorPalette[i++] & 0xff);
|
||||||
|
const db = b - (this.colorPalette[i++] & 0xff);
|
||||||
|
const d = dr * dr + dg * dg + db * db;
|
||||||
|
if (this.usedEntry[index] && d < distanceMinimum) {
|
||||||
|
distanceMinimum = d;
|
||||||
|
minimumIndex = index;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return minimumIndex;
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Updates {@link GifEncoder.pixels} by creating an RGB-formatted {@link Uint8Array} from the RGBA-formatted data.
|
||||||
|
*/
|
||||||
|
getImagePixels() {
|
||||||
|
const w = this.width;
|
||||||
|
const h = this.height;
|
||||||
|
this.pixels = new Uint8Array(w * h * 3);
|
||||||
|
const data = this.image;
|
||||||
|
for (let i = 0, count = 0; i < h; i++) {
|
||||||
|
for (let j = 0; j < w; j++) {
|
||||||
|
const b = i * w * 4 + j * 4;
|
||||||
|
this.pixels[count++] = data[b];
|
||||||
|
this.pixels[count++] = data[b + 1];
|
||||||
|
this.pixels[count++] = data[b + 2];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Writes the GCE (Graphic Control Extension).
|
||||||
|
*/
|
||||||
|
writeGraphicControlExtension() {
|
||||||
|
this.byteBuffer.writeByte(0x21); // extension introducer
|
||||||
|
this.byteBuffer.writeByte(0xf9); // GCE label
|
||||||
|
this.byteBuffer.writeByte(4); // data block size
|
||||||
|
let transparency;
|
||||||
|
let dispose;
|
||||||
|
if (this.transparent === null) {
|
||||||
|
transparency = 0;
|
||||||
|
dispose = 0; // dispose = no action
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
transparency = 1;
|
||||||
|
dispose = 2; // force clear if using transparent color
|
||||||
|
}
|
||||||
|
if (this.disposalMode >= 0) {
|
||||||
|
dispose = this.disposalMode & 7; // user override
|
||||||
|
}
|
||||||
|
dispose <<= 2;
|
||||||
|
// Write GCP's packed fields
|
||||||
|
const fields = 0 | // XXX0_0000 : Reserved
|
||||||
|
dispose | // 000X_XX00 : Disposal Method
|
||||||
|
0 | // 0000_00X0 : User Input Flag
|
||||||
|
transparency; // 0000_000X : Transparent Color Flag
|
||||||
|
this.byteBuffer.writeByte(fields);
|
||||||
|
this.writeShort(this.delay); // delay x 1 / 100 sec
|
||||||
|
this.byteBuffer.writeByte(this.transparentIndex); // transparent color index
|
||||||
|
this.byteBuffer.writeByte(0); // block terminator
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Writes the ID (Image Descriptor).
|
||||||
|
*/
|
||||||
|
writeImageDescriptor() {
|
||||||
|
this.byteBuffer.writeByte(0x2c); // Image Descriptor block identifier
|
||||||
|
this.writeShort(0); // Image Left Position
|
||||||
|
this.writeShort(0); // Image Top Position
|
||||||
|
this.writeShort(this.width); // Image Width
|
||||||
|
this.writeShort(this.height); // Image Height
|
||||||
|
// Write the LCT (Local Color Table):
|
||||||
|
const fields = this.firstFrame
|
||||||
|
? 0 // The first frame uses the GCT (Global Color Table)
|
||||||
|
: 128 | // X000_0000 : Local Color Table Flag = 1
|
||||||
|
0 | // 0X00_0000 : Interlace Flag = 0
|
||||||
|
0 | // 00X0_0000 : Sort Flag = 0
|
||||||
|
0 | // 000X_X000 : Reserved
|
||||||
|
PALETTE_SIZE; // 0000_0XXX : Size of Local Color Table
|
||||||
|
this.byteBuffer.writeByte(fields);
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Writes the LSD (Logical Screen Descriptor)
|
||||||
|
*/
|
||||||
|
writeLogicalScreenDescriptor() {
|
||||||
|
// logical screen size
|
||||||
|
this.writeShort(this.width);
|
||||||
|
this.writeShort(this.height);
|
||||||
|
// Write the GCT (Global Color Table):
|
||||||
|
const fields = 128 | // X000_0000 : GCT (Global Color Table) flag = 1
|
||||||
|
112 | // 0XXX_0000 : Color Resolution = 7
|
||||||
|
0 | // 0000_X000 : GCT sort flag = 0
|
||||||
|
0 | // 0000_0X00 : Reserved
|
||||||
|
PALETTE_SIZE; // 0000_00XX : GCT (Global Color Table) size
|
||||||
|
this.byteBuffer.writeByte(fields);
|
||||||
|
this.byteBuffer.writeByte(0x000000); // background color index
|
||||||
|
this.byteBuffer.writeByte(0); // pixel aspect ratio - assume 1:1
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Writes the Netscape application extension to define repeat count.
|
||||||
|
*/
|
||||||
|
writeNetscapeExtension() {
|
||||||
|
// Reference: http://www.vurdalakov.net/misc/gif/netscape-looping-application-extension
|
||||||
|
this.byteBuffer.writeByte(0x21); // Extension
|
||||||
|
this.byteBuffer.writeByte(0xff); // Application Extension
|
||||||
|
this.byteBuffer.writeByte(0x0b); // Block Size
|
||||||
|
this.byteBuffer.writeBytes(NETSCAPE_HEADER); // Application Identifier + Application Authentication Code
|
||||||
|
this.byteBuffer.writeByte(0x03); // Sub-block data size
|
||||||
|
this.byteBuffer.writeByte(0x01); // Sub-block ID
|
||||||
|
this.writeShort(this.repeat); // Loop Count (up to 2 bytes, `0` = repeat forever)
|
||||||
|
this.byteBuffer.writeByte(0); // Block Terminator
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Writes the color table palette.
|
||||||
|
*/
|
||||||
|
writePalette() {
|
||||||
|
this.byteBuffer.writeBytes(this.colorPalette);
|
||||||
|
this.byteBuffer.writeTimes(0, 3 * 256 - this.colorPalette.length);
|
||||||
|
}
|
||||||
|
writeShort(pValue) {
|
||||||
|
this.byteBuffer.writeByte(pValue & 0xff);
|
||||||
|
this.byteBuffer.writeByte((pValue >> 8) & 0xff);
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Encodes and writes pixel data into {@link GifEncoder.byteBuffer}.
|
||||||
|
*/
|
||||||
|
writePixels() {
|
||||||
|
const enc = new LZWEncoder_1.LZWEncoder(this.width, this.height, this.indexedPixels, this.colorDepth);
|
||||||
|
enc.encode(this.byteBuffer);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
exports.GifEncoder = GifEncoder;
|
||||||
|
//# sourceMappingURL=GifEncoder.js.map
|
File diff suppressed because one or more lines are too long
|
@ -0,0 +1,123 @@
|
||||||
|
/**
|
||||||
|
* LZWEncoder
|
||||||
|
*
|
||||||
|
* Authors
|
||||||
|
* - Kevin Weiner (original Java version - kweiner@fmsware.com)
|
||||||
|
* - Thibault Imbert (AS3 version - bytearray.org)
|
||||||
|
* - Johan Nordberg (JS version - code@johan-nordberg.com)
|
||||||
|
* - Antonio Román (TS version - kyradiscord@gmail.com)
|
||||||
|
*
|
||||||
|
* Acknowledgements
|
||||||
|
* - GIFCOMPR.C - GIF Image compression routines
|
||||||
|
* - Lempel-Ziv compression based on 'compress'. GIF modifications by
|
||||||
|
* - David Rowley (mgardi@watdcsu.waterloo.edu)
|
||||||
|
* GIF Image compression - modified 'compress'
|
||||||
|
* Based on: compress.c - File compression ala IEEE Computer, June 1984.
|
||||||
|
* By Authors:
|
||||||
|
* - Spencer W. Thomas (decvax!harpo!utah-cs!utah-gr!thomas)
|
||||||
|
* - Jim McKie (decvax!mcvax!jim)
|
||||||
|
* - Steve Davies (decvax!vax135!petsd!peora!srd)
|
||||||
|
* - Ken Turkowski (decvax!decwrl!turtlevax!ken)
|
||||||
|
* - James A. Woods (decvax!ihnp4!ames!jaw)
|
||||||
|
* - Joe Orost (decvax!vax135!petsd!joe)
|
||||||
|
*/
|
||||||
|
import type { ByteBuffer } from './ByteBuffer';
|
||||||
|
/**
|
||||||
|
* @summary
|
||||||
|
* Algorithm: use open addressing double hashing (no chaining) on the prefix code / next character combination.
|
||||||
|
*
|
||||||
|
* We do a variant of Knuth's algorithm D (vol. 3, sec. 6.4) along with G. Knott's relatively-prime secondary probe.
|
||||||
|
* Here, the modular division first probe is gives way to a faster exclusive-or manipulation. Also do block compression
|
||||||
|
* with an adaptive reset, whereby the code table is cleared when the compression ratio decreases, but after the table
|
||||||
|
* fills. The variable-length output codes are re-sized at this point, and a special CLEAR code is generated for the
|
||||||
|
* decompression.
|
||||||
|
*
|
||||||
|
* **Late addition**: construct the table according to file size for noticeable speed improvement on small files. Please
|
||||||
|
* direct questions about this implementation to ames!jaw.
|
||||||
|
*/
|
||||||
|
export declare class LZWEncoder {
|
||||||
|
/**
|
||||||
|
* The GIF image's width, between `1` and `65536`.
|
||||||
|
*/
|
||||||
|
readonly width: number;
|
||||||
|
/**
|
||||||
|
* The GIF image's height, between `1` and `65536`.
|
||||||
|
*/
|
||||||
|
readonly height: number;
|
||||||
|
private pixels;
|
||||||
|
private readonly initCodeSize;
|
||||||
|
private currentAccumulator;
|
||||||
|
private currentBits;
|
||||||
|
private currentPixel;
|
||||||
|
private accumulator;
|
||||||
|
private firstUnusedEntry;
|
||||||
|
private maximumCode;
|
||||||
|
private remaining;
|
||||||
|
private bitSize;
|
||||||
|
private clearFlag;
|
||||||
|
private globalInitialBits;
|
||||||
|
private clearCode;
|
||||||
|
private endOfFrameCode;
|
||||||
|
private readonly accumulators;
|
||||||
|
private readonly hashes;
|
||||||
|
private readonly codes;
|
||||||
|
/**
|
||||||
|
* Constructs a {@link LZWEncoder} instance.
|
||||||
|
* @param width The width of the image.
|
||||||
|
* @param height The height of the image.
|
||||||
|
* @param pixels The pixel data in RGB format.
|
||||||
|
* @param colorDepth The color depth.
|
||||||
|
*/
|
||||||
|
constructor(width: number, height: number, pixels: Uint8Array, colorDepth: number);
|
||||||
|
/**
|
||||||
|
* Encodes the image into the output.
|
||||||
|
* @param output The byte buffer to write to.
|
||||||
|
*/
|
||||||
|
encode(output: ByteBuffer): void;
|
||||||
|
/**
|
||||||
|
* Compresses the GIF data.
|
||||||
|
* @param initialBits The initial bits for the compression.
|
||||||
|
* @param output The byte buffer to write to.
|
||||||
|
*/
|
||||||
|
private compress;
|
||||||
|
/**
|
||||||
|
* Adds a character to the end of the current packet, and if it is at 254 characters, it flushes the packet to disk
|
||||||
|
* via {@link LZWEncoder.flushPacket}.
|
||||||
|
* @param c The character code to add.
|
||||||
|
* @param output The byte buffer to write to.
|
||||||
|
*/
|
||||||
|
private addCharacter;
|
||||||
|
/**
|
||||||
|
* Clears out the hash table for block compress.
|
||||||
|
* @param output The byte buffer to write to.
|
||||||
|
*/
|
||||||
|
private clearCodeTable;
|
||||||
|
/**
|
||||||
|
* Resets the hash table given an amount of hashes.
|
||||||
|
* @param hashSize The amount of hashes to reset.
|
||||||
|
*/
|
||||||
|
private resetHashRange;
|
||||||
|
/**
|
||||||
|
* Flushes the packet to disk, and reset the accumulator.
|
||||||
|
* @param output The byte buffer to write to.
|
||||||
|
*/
|
||||||
|
private flushPacket;
|
||||||
|
/**
|
||||||
|
* Gets the maximum representable number for a given amount of bits.
|
||||||
|
* @param size The bit size to get the number from.
|
||||||
|
* @returns The maximum code given a number of bits.
|
||||||
|
* @example
|
||||||
|
* ```typescript
|
||||||
|
* getMaximumCode(6);
|
||||||
|
* // ➡ 0b0011_1111
|
||||||
|
* ```
|
||||||
|
*/
|
||||||
|
private getMaximumCode;
|
||||||
|
/**
|
||||||
|
* Gets the next pixel from the image.
|
||||||
|
* @returns The next pixel from the image.
|
||||||
|
*/
|
||||||
|
private nextPixel;
|
||||||
|
private processOutput;
|
||||||
|
}
|
||||||
|
//# sourceMappingURL=LZWEncoder.d.ts.map
|
|
@ -0,0 +1 @@
|
||||||
|
{"version":3,"file":"LZWEncoder.d.ts","sourceRoot":"","sources":["../../src/lib/LZWEncoder.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;GAsBG;AAEH,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,cAAc,CAAC;AAS/C;;;;;;;;;;;;GAYG;AACH,qBAAa,UAAU;IACtB;;OAEG;IACH,SAAgB,KAAK,EAAE,MAAM,CAAC;IAE9B;;OAEG;IACH,SAAgB,MAAM,EAAE,MAAM,CAAC;IAC/B,OAAO,CAAC,MAAM,CAAa;IAC3B,OAAO,CAAC,QAAQ,CAAC,YAAY,CAAS;IACtC,OAAO,CAAC,kBAAkB,CAAK;IAC/B,OAAO,CAAC,WAAW,CAAK;IACxB,OAAO,CAAC,YAAY,CAAK;IACzB,OAAO,CAAC,WAAW,CAAK;IACxB,OAAO,CAAC,gBAAgB,CAAK;IAC7B,OAAO,CAAC,WAAW,CAAK;IACxB,OAAO,CAAC,SAAS,CAAK;IACtB,OAAO,CAAC,OAAO,CAAK;IAIpB,OAAO,CAAC,SAAS,CAAS;IAE1B,OAAO,CAAC,iBAAiB,CAAK;IAC9B,OAAO,CAAC,SAAS,CAAK;IACtB,OAAO,CAAC,cAAc,CAAK;IAE3B,OAAO,CAAC,QAAQ,CAAC,YAAY,CAAuB;IACpD,OAAO,CAAC,QAAQ,CAAC,MAAM,CAA6B;IACpD,OAAO,CAAC,QAAQ,CAAC,KAAK,CAA6B;IAEnD;;;;;;OAMG;gBACgB,KAAK,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,UAAU,EAAE,UAAU,EAAE,MAAM;IAOxF;;;OAGG;IACI,MAAM,CAAC,MAAM,EAAE,UAAU;IAQhC;;;;OAIG;IACH,OAAO,CAAC,QAAQ;IAgEhB;;;;;OAKG;IACH,OAAO,CAAC,YAAY;IAKpB;;;OAGG;IACH,OAAO,CAAC,cAAc;IAOtB;;;OAGG;IACH,OAAO,CAAC,cAAc;IAItB;;;OAGG;IACH,OAAO,CAAC,WAAW;IAQnB;;;;;;;;;OASG;IACH,OAAO,CAAC,cAAc;IAItB;;;OAGG;IACH,OAAO,CAAC,SAAS;IAQjB,OAAO,CAAC,aAAa;CAmCrB"}
|
|
@ -0,0 +1,353 @@
|
||||||
|
"use strict";
|
||||||
|
/**
|
||||||
|
* LZWEncoder
|
||||||
|
*
|
||||||
|
* Authors
|
||||||
|
* - Kevin Weiner (original Java version - kweiner@fmsware.com)
|
||||||
|
* - Thibault Imbert (AS3 version - bytearray.org)
|
||||||
|
* - Johan Nordberg (JS version - code@johan-nordberg.com)
|
||||||
|
* - Antonio Román (TS version - kyradiscord@gmail.com)
|
||||||
|
*
|
||||||
|
* Acknowledgements
|
||||||
|
* - GIFCOMPR.C - GIF Image compression routines
|
||||||
|
* - Lempel-Ziv compression based on 'compress'. GIF modifications by
|
||||||
|
* - David Rowley (mgardi@watdcsu.waterloo.edu)
|
||||||
|
* GIF Image compression - modified 'compress'
|
||||||
|
* Based on: compress.c - File compression ala IEEE Computer, June 1984.
|
||||||
|
* By Authors:
|
||||||
|
* - Spencer W. Thomas (decvax!harpo!utah-cs!utah-gr!thomas)
|
||||||
|
* - Jim McKie (decvax!mcvax!jim)
|
||||||
|
* - Steve Davies (decvax!vax135!petsd!peora!srd)
|
||||||
|
* - Ken Turkowski (decvax!decwrl!turtlevax!ken)
|
||||||
|
* - James A. Woods (decvax!ihnp4!ames!jaw)
|
||||||
|
* - Joe Orost (decvax!vax135!petsd!joe)
|
||||||
|
*/
|
||||||
|
Object.defineProperty(exports, "__esModule", { value: true });
|
||||||
|
exports.LZWEncoder = void 0;
|
||||||
|
const EOF = -1;
|
||||||
|
const BITS = 12;
|
||||||
|
const HASH_SIZE = 5003; // 80% occupancy
|
||||||
|
const masks = new Uint16Array([
|
||||||
|
0x0000, 0x0001, 0x0003, 0x0007, 0x000f, 0x001f, 0x003f, 0x007f, 0x00ff, 0x01ff, 0x03ff, 0x07ff, 0x0fff, 0x1fff, 0x3fff, 0x7fff, 0xffff
|
||||||
|
]);
|
||||||
|
/**
|
||||||
|
* @summary
|
||||||
|
* Algorithm: use open addressing double hashing (no chaining) on the prefix code / next character combination.
|
||||||
|
*
|
||||||
|
* We do a variant of Knuth's algorithm D (vol. 3, sec. 6.4) along with G. Knott's relatively-prime secondary probe.
|
||||||
|
* Here, the modular division first probe is gives way to a faster exclusive-or manipulation. Also do block compression
|
||||||
|
* with an adaptive reset, whereby the code table is cleared when the compression ratio decreases, but after the table
|
||||||
|
* fills. The variable-length output codes are re-sized at this point, and a special CLEAR code is generated for the
|
||||||
|
* decompression.
|
||||||
|
*
|
||||||
|
* **Late addition**: construct the table according to file size for noticeable speed improvement on small files. Please
|
||||||
|
* direct questions about this implementation to ames!jaw.
|
||||||
|
*/
|
||||||
|
class LZWEncoder {
|
||||||
|
/**
|
||||||
|
* Constructs a {@link LZWEncoder} instance.
|
||||||
|
* @param width The width of the image.
|
||||||
|
* @param height The height of the image.
|
||||||
|
* @param pixels The pixel data in RGB format.
|
||||||
|
* @param colorDepth The color depth.
|
||||||
|
*/
|
||||||
|
constructor(width, height, pixels, colorDepth) {
|
||||||
|
/**
|
||||||
|
* The GIF image's width, between `1` and `65536`.
|
||||||
|
*/
|
||||||
|
Object.defineProperty(this, "width", {
|
||||||
|
enumerable: true,
|
||||||
|
configurable: true,
|
||||||
|
writable: true,
|
||||||
|
value: void 0
|
||||||
|
});
|
||||||
|
/**
|
||||||
|
* The GIF image's height, between `1` and `65536`.
|
||||||
|
*/
|
||||||
|
Object.defineProperty(this, "height", {
|
||||||
|
enumerable: true,
|
||||||
|
configurable: true,
|
||||||
|
writable: true,
|
||||||
|
value: void 0
|
||||||
|
});
|
||||||
|
Object.defineProperty(this, "pixels", {
|
||||||
|
enumerable: true,
|
||||||
|
configurable: true,
|
||||||
|
writable: true,
|
||||||
|
value: void 0
|
||||||
|
});
|
||||||
|
Object.defineProperty(this, "initCodeSize", {
|
||||||
|
enumerable: true,
|
||||||
|
configurable: true,
|
||||||
|
writable: true,
|
||||||
|
value: void 0
|
||||||
|
});
|
||||||
|
Object.defineProperty(this, "currentAccumulator", {
|
||||||
|
enumerable: true,
|
||||||
|
configurable: true,
|
||||||
|
writable: true,
|
||||||
|
value: 0
|
||||||
|
});
|
||||||
|
Object.defineProperty(this, "currentBits", {
|
||||||
|
enumerable: true,
|
||||||
|
configurable: true,
|
||||||
|
writable: true,
|
||||||
|
value: 0
|
||||||
|
});
|
||||||
|
Object.defineProperty(this, "currentPixel", {
|
||||||
|
enumerable: true,
|
||||||
|
configurable: true,
|
||||||
|
writable: true,
|
||||||
|
value: 0
|
||||||
|
});
|
||||||
|
Object.defineProperty(this, "accumulator", {
|
||||||
|
enumerable: true,
|
||||||
|
configurable: true,
|
||||||
|
writable: true,
|
||||||
|
value: 0
|
||||||
|
});
|
||||||
|
Object.defineProperty(this, "firstUnusedEntry", {
|
||||||
|
enumerable: true,
|
||||||
|
configurable: true,
|
||||||
|
writable: true,
|
||||||
|
value: 0
|
||||||
|
}); // first unused entry
|
||||||
|
Object.defineProperty(this, "maximumCode", {
|
||||||
|
enumerable: true,
|
||||||
|
configurable: true,
|
||||||
|
writable: true,
|
||||||
|
value: 0
|
||||||
|
});
|
||||||
|
Object.defineProperty(this, "remaining", {
|
||||||
|
enumerable: true,
|
||||||
|
configurable: true,
|
||||||
|
writable: true,
|
||||||
|
value: 0
|
||||||
|
});
|
||||||
|
Object.defineProperty(this, "bitSize", {
|
||||||
|
enumerable: true,
|
||||||
|
configurable: true,
|
||||||
|
writable: true,
|
||||||
|
value: 0
|
||||||
|
});
|
||||||
|
// block compression parameters -- after all codes are used up,
|
||||||
|
// and compression rate changes, start over.
|
||||||
|
Object.defineProperty(this, "clearFlag", {
|
||||||
|
enumerable: true,
|
||||||
|
configurable: true,
|
||||||
|
writable: true,
|
||||||
|
value: false
|
||||||
|
});
|
||||||
|
Object.defineProperty(this, "globalInitialBits", {
|
||||||
|
enumerable: true,
|
||||||
|
configurable: true,
|
||||||
|
writable: true,
|
||||||
|
value: 0
|
||||||
|
});
|
||||||
|
Object.defineProperty(this, "clearCode", {
|
||||||
|
enumerable: true,
|
||||||
|
configurable: true,
|
||||||
|
writable: true,
|
||||||
|
value: 0
|
||||||
|
});
|
||||||
|
Object.defineProperty(this, "endOfFrameCode", {
|
||||||
|
enumerable: true,
|
||||||
|
configurable: true,
|
||||||
|
writable: true,
|
||||||
|
value: 0
|
||||||
|
});
|
||||||
|
Object.defineProperty(this, "accumulators", {
|
||||||
|
enumerable: true,
|
||||||
|
configurable: true,
|
||||||
|
writable: true,
|
||||||
|
value: new Uint8Array(256)
|
||||||
|
});
|
||||||
|
Object.defineProperty(this, "hashes", {
|
||||||
|
enumerable: true,
|
||||||
|
configurable: true,
|
||||||
|
writable: true,
|
||||||
|
value: new Int32Array(HASH_SIZE)
|
||||||
|
});
|
||||||
|
Object.defineProperty(this, "codes", {
|
||||||
|
enumerable: true,
|
||||||
|
configurable: true,
|
||||||
|
writable: true,
|
||||||
|
value: new Int32Array(HASH_SIZE)
|
||||||
|
});
|
||||||
|
this.width = width;
|
||||||
|
this.height = height;
|
||||||
|
this.pixels = pixels;
|
||||||
|
this.initCodeSize = Math.max(2, colorDepth);
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Encodes the image into the output.
|
||||||
|
* @param output The byte buffer to write to.
|
||||||
|
*/
|
||||||
|
encode(output) {
|
||||||
|
output.writeByte(this.initCodeSize); // write "initial code size" byte
|
||||||
|
this.remaining = this.width * this.height; // reset navigation variables
|
||||||
|
this.currentPixel = 0;
|
||||||
|
this.compress(this.initCodeSize + 1, output); // compress and write the pixel data
|
||||||
|
output.writeByte(0); // write block terminator
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Compresses the GIF data.
|
||||||
|
* @param initialBits The initial bits for the compression.
|
||||||
|
* @param output The byte buffer to write to.
|
||||||
|
*/
|
||||||
|
compress(initialBits, output) {
|
||||||
|
// Set up the globals: globalInitialBits - initial number of bits
|
||||||
|
this.globalInitialBits = initialBits;
|
||||||
|
// Set up the necessary values
|
||||||
|
this.clearFlag = false;
|
||||||
|
this.bitSize = this.globalInitialBits;
|
||||||
|
this.maximumCode = this.getMaximumCode(this.bitSize);
|
||||||
|
this.clearCode = 1 << (initialBits - 1);
|
||||||
|
this.endOfFrameCode = this.clearCode + 1;
|
||||||
|
this.firstUnusedEntry = this.clearCode + 2;
|
||||||
|
// Clear packet
|
||||||
|
this.accumulator = 0;
|
||||||
|
let code = this.nextPixel();
|
||||||
|
let hash = 80048;
|
||||||
|
const hashShift = 4;
|
||||||
|
const hashSizeRegion = HASH_SIZE;
|
||||||
|
this.resetHashRange(hashSizeRegion);
|
||||||
|
this.processOutput(this.clearCode, output);
|
||||||
|
let c;
|
||||||
|
outerLoop: while ((c = this.nextPixel()) !== EOF) {
|
||||||
|
hash = (c << BITS) + code;
|
||||||
|
// XOR hashing:
|
||||||
|
let i = (c << hashShift) ^ code;
|
||||||
|
if (this.hashes[i] === hash) {
|
||||||
|
code = this.codes[i];
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (this.hashes[i] >= 0) {
|
||||||
|
// Non-empty slot, perform secondary hash (after G. Knott):
|
||||||
|
let dispose = hashSizeRegion - i;
|
||||||
|
if (i === 0)
|
||||||
|
dispose = 1;
|
||||||
|
do {
|
||||||
|
if ((i -= dispose) < 0)
|
||||||
|
i += hashSizeRegion;
|
||||||
|
if (this.hashes[i] === hash) {
|
||||||
|
code = this.codes[i];
|
||||||
|
continue outerLoop;
|
||||||
|
}
|
||||||
|
} while (this.hashes[i] >= 0);
|
||||||
|
}
|
||||||
|
this.processOutput(code, output);
|
||||||
|
code = c;
|
||||||
|
if (this.firstUnusedEntry < 1 << BITS) {
|
||||||
|
// code -> hash-table
|
||||||
|
this.codes[i] = this.firstUnusedEntry++;
|
||||||
|
this.hashes[i] = hash;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
this.clearCodeTable(output);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Put out the final code:
|
||||||
|
this.processOutput(code, output);
|
||||||
|
this.processOutput(this.endOfFrameCode, output);
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Adds a character to the end of the current packet, and if it is at 254 characters, it flushes the packet to disk
|
||||||
|
* via {@link LZWEncoder.flushPacket}.
|
||||||
|
* @param c The character code to add.
|
||||||
|
* @param output The byte buffer to write to.
|
||||||
|
*/
|
||||||
|
addCharacter(c, output) {
|
||||||
|
this.accumulators[this.accumulator++] = c;
|
||||||
|
if (this.accumulator >= 254)
|
||||||
|
this.flushPacket(output);
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Clears out the hash table for block compress.
|
||||||
|
* @param output The byte buffer to write to.
|
||||||
|
*/
|
||||||
|
clearCodeTable(output) {
|
||||||
|
this.resetHashRange(HASH_SIZE);
|
||||||
|
this.firstUnusedEntry = this.clearCode + 2;
|
||||||
|
this.clearFlag = true;
|
||||||
|
this.processOutput(this.clearCode, output);
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Resets the hash table given an amount of hashes.
|
||||||
|
* @param hashSize The amount of hashes to reset.
|
||||||
|
*/
|
||||||
|
resetHashRange(hashSize) {
|
||||||
|
this.hashes.fill(-1, 0, hashSize);
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Flushes the packet to disk, and reset the accumulator.
|
||||||
|
* @param output The byte buffer to write to.
|
||||||
|
*/
|
||||||
|
flushPacket(output) {
|
||||||
|
if (this.accumulator > 0) {
|
||||||
|
output.writeByte(this.accumulator);
|
||||||
|
output.writeBytes(this.accumulators, 0, this.accumulator);
|
||||||
|
this.accumulator = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Gets the maximum representable number for a given amount of bits.
|
||||||
|
* @param size The bit size to get the number from.
|
||||||
|
* @returns The maximum code given a number of bits.
|
||||||
|
* @example
|
||||||
|
* ```typescript
|
||||||
|
* getMaximumCode(6);
|
||||||
|
* // ➡ 0b0011_1111
|
||||||
|
* ```
|
||||||
|
*/
|
||||||
|
getMaximumCode(size) {
|
||||||
|
return (1 << size) - 1;
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Gets the next pixel from the image.
|
||||||
|
* @returns The next pixel from the image.
|
||||||
|
*/
|
||||||
|
nextPixel() {
|
||||||
|
if (this.remaining === 0)
|
||||||
|
return EOF;
|
||||||
|
--this.remaining;
|
||||||
|
const pixel = this.pixels[this.currentPixel++];
|
||||||
|
return pixel & 0xff;
|
||||||
|
}
|
||||||
|
processOutput(code, outs) {
|
||||||
|
this.currentAccumulator &= masks[this.currentBits];
|
||||||
|
this.currentAccumulator = this.currentBits > 0 ? (this.currentAccumulator |= code << this.currentBits) : code;
|
||||||
|
this.currentBits += this.bitSize;
|
||||||
|
while (this.currentBits >= 8) {
|
||||||
|
this.addCharacter(this.currentAccumulator & 0xff, outs);
|
||||||
|
this.currentAccumulator >>= 8;
|
||||||
|
this.currentBits -= 8;
|
||||||
|
}
|
||||||
|
// If the next entry is going to be too big for the code size,
|
||||||
|
// then increase it, if possible.
|
||||||
|
if (this.firstUnusedEntry > this.maximumCode || this.clearFlag) {
|
||||||
|
if (this.clearFlag) {
|
||||||
|
this.maximumCode = this.getMaximumCode((this.bitSize = this.globalInitialBits));
|
||||||
|
this.clearFlag = false;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
++this.bitSize;
|
||||||
|
if (this.bitSize === BITS)
|
||||||
|
this.maximumCode = 1 << BITS;
|
||||||
|
else
|
||||||
|
this.maximumCode = this.getMaximumCode(this.bitSize);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (code === this.endOfFrameCode) {
|
||||||
|
// At EOF, write the rest of the buffer.
|
||||||
|
while (this.currentBits > 0) {
|
||||||
|
this.addCharacter(this.currentAccumulator & 0xff, outs);
|
||||||
|
this.currentAccumulator >>= 8;
|
||||||
|
this.currentBits -= 8;
|
||||||
|
}
|
||||||
|
this.flushPacket(outs);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
exports.LZWEncoder = LZWEncoder;
|
||||||
|
//# sourceMappingURL=LZWEncoder.js.map
|
File diff suppressed because one or more lines are too long
|
@ -0,0 +1,121 @@
|
||||||
|
/**
|
||||||
|
* NeuQuant Neural-Net Quantization Algorithm
|
||||||
|
* ------------------------------------------
|
||||||
|
*
|
||||||
|
* Copyright (c) 1994 Anthony Dekker
|
||||||
|
*
|
||||||
|
* NEUQUANT Neural-Net quantization algorithm by Anthony Dekker, 1994.
|
||||||
|
* See "Kohonen neural networks for optimal colour quantization"
|
||||||
|
* in "Network: Computation in Neural Systems" Vol. 5 (1994) pp 351-367.
|
||||||
|
* for a discussion of the algorithm.
|
||||||
|
* See also http://members.ozemail.com.au/~dekker/NEUQUANT.HTML
|
||||||
|
*
|
||||||
|
* Any party obtaining a copy of these files from the author, directly or
|
||||||
|
* indirectly, is granted, free of charge, a full and unrestricted irrevocable,
|
||||||
|
* world-wide, paid up, royalty-free, nonexclusive right and license to deal
|
||||||
|
* in this software and documentation files (the "Software"), including without
|
||||||
|
* limitation the rights to use, copy, modify, merge, publish, distribute, sublicense,
|
||||||
|
* and/or sell copies of the Software, and to permit persons who receive
|
||||||
|
* copies from any such party to do so, with the only requirement being
|
||||||
|
* that this copyright notice remain intact.
|
||||||
|
*
|
||||||
|
* (JavaScript port 2012 by Johan Nordberg)
|
||||||
|
* (TypeScript port 2021 by Antonio Román)
|
||||||
|
*/
|
||||||
|
export declare class NeuQuant {
|
||||||
|
/**
|
||||||
|
* Array of pixels in RGB format, as such that it's decoded as `[r, g, b, r, g, b, r, g, b, ...]`.
|
||||||
|
*/
|
||||||
|
private pixels;
|
||||||
|
/**
|
||||||
|
* Sampling factor from `1` to `30`, where lower is better quality.
|
||||||
|
*/
|
||||||
|
private sampleFactorial;
|
||||||
|
/**
|
||||||
|
* The neural networks, composed by {@link maximumColorsSize} {@link Float64Array float arrays} of size 4.
|
||||||
|
*/
|
||||||
|
private networks;
|
||||||
|
/**
|
||||||
|
* Network lookup indexes, composed by 256 indexes.
|
||||||
|
*/
|
||||||
|
private networkIndexes;
|
||||||
|
private biases;
|
||||||
|
private frequencies;
|
||||||
|
private radiusPowers;
|
||||||
|
/**
|
||||||
|
* Creates the neural quantifier instance.
|
||||||
|
* @param pixels Array of pixels in RGB format, as such that it's decoded as `[r, g, b, r, g, b, r, g, b, ...]`.
|
||||||
|
* @param sampleFactorial Sampling factor from `1` to `30`, where lower is better quality.
|
||||||
|
*/
|
||||||
|
constructor(pixels: Uint8Array, sampleFactorial: number);
|
||||||
|
/**
|
||||||
|
* Builds the networks' color map.
|
||||||
|
* @returns A RGB-encoded {@link Float64Array}.
|
||||||
|
*/
|
||||||
|
getColorMap(): Float64Array;
|
||||||
|
/**
|
||||||
|
* Searches for BGR values 0..255 and returns a color index
|
||||||
|
* @param b The blue color byte, between 0 and 255.
|
||||||
|
* @param g The green color byte, between 0 and 255.
|
||||||
|
* @param r The red color byte, between 0 and 255.
|
||||||
|
* @returns The best color index.
|
||||||
|
*/
|
||||||
|
lookupRGB(b: number, g: number, r: number): number;
|
||||||
|
/**
|
||||||
|
* Initializes the state for the arrays.
|
||||||
|
*/
|
||||||
|
private init;
|
||||||
|
/**
|
||||||
|
* Un-biases network to give byte values 0..255 and record position i to prepare for sort.
|
||||||
|
*/
|
||||||
|
private unBiasNetwork;
|
||||||
|
/**
|
||||||
|
* Moves neuron `i` towards biased (`B`, `G`, `R`) by factor `alpha`.
|
||||||
|
* @param alpha The factor at which the neuron `i` should move towards.
|
||||||
|
* @param i The neuron's index.
|
||||||
|
* @param b The blue color.
|
||||||
|
* @param g The green color.
|
||||||
|
* @param r The red color.
|
||||||
|
*/
|
||||||
|
private alterSingle;
|
||||||
|
/**
|
||||||
|
* Moves neurons in a `radius` around index `i` towards biased (`B`, `G`, `R`) by factor
|
||||||
|
* {@link NeuQuant.radiusPowers `radiusPower[m]`}.
|
||||||
|
* @param radius The radius around `i` to alter.
|
||||||
|
* @param i The neuron's index.
|
||||||
|
* @param b The blue color.
|
||||||
|
* @param g The green color.
|
||||||
|
* @param r The red color.
|
||||||
|
*/
|
||||||
|
private alterNeighbors;
|
||||||
|
/**
|
||||||
|
* Searches for biased BGR values.
|
||||||
|
*
|
||||||
|
* - Finds the closest neuron (minimum distance) and updates {@link NeuQuant.frequencies}.
|
||||||
|
* - Finds the best neuron (minimum distance-bias) and returns the position.
|
||||||
|
*
|
||||||
|
* For frequently chosen neurons, {@link NeuQuant.frequencies `frequencies[i]`} is high and
|
||||||
|
* {@link NeuQuant.biases `biases[i]`} is negative.
|
||||||
|
*
|
||||||
|
* The latter is determined by the multiplication of `gamma` with the subtraction of the inverse of
|
||||||
|
* {@link maximumColorsSize} with {@link NeuQuant.frequencies `frequencies[i]`}:
|
||||||
|
*
|
||||||
|
* ```typescript
|
||||||
|
* biases[i] = gamma * ((1 / maximumColorsSize) - frequencies[i])
|
||||||
|
* ```
|
||||||
|
* @param b The blue color.
|
||||||
|
* @param g The green color.
|
||||||
|
* @param r The red color.
|
||||||
|
* @returns The best bias position.
|
||||||
|
*/
|
||||||
|
private contest;
|
||||||
|
/**
|
||||||
|
* Sorts the neural network and builds {@link NeuQuant.networkIndexes `networkIndex[0..255]`}.
|
||||||
|
*/
|
||||||
|
private buildIndexes;
|
||||||
|
/**
|
||||||
|
* Runs the main learning loop.
|
||||||
|
*/
|
||||||
|
private learn;
|
||||||
|
}
|
||||||
|
//# sourceMappingURL=NeuQuant.d.ts.map
|
|
@ -0,0 +1 @@
|
||||||
|
{"version":3,"file":"NeuQuant.d.ts","sourceRoot":"","sources":["../../src/lib/NeuQuant.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AA2CH,qBAAa,QAAQ;IACpB;;OAEG;IACH,OAAO,CAAC,MAAM,CAAa;IAE3B;;OAEG;IACH,OAAO,CAAC,eAAe,CAAS;IAEhC;;OAEG;IACH,OAAO,CAAC,QAAQ,CAAiB;IAEjC;;OAEG;IACH,OAAO,CAAC,cAAc,CAAa;IAGnC,OAAO,CAAC,MAAM,CAAa;IAC3B,OAAO,CAAC,WAAW,CAAa;IAChC,OAAO,CAAC,YAAY,CAAa;IAEjC;;;;OAIG;gBACgB,MAAM,EAAE,UAAU,EAAE,eAAe,EAAE,MAAM;IAe9D;;;OAGG;IACI,WAAW;IAkBlB;;;;;;OAMG;IACI,SAAS,CAAC,CAAC,EAAE,MAAM,EAAE,CAAC,EAAE,MAAM,EAAE,CAAC,EAAE,MAAM;IAwDhD;;OAEG;IACH,OAAO,CAAC,IAAI;IASZ;;OAEG;IACH,OAAO,CAAC,aAAa;IAUrB;;;;;;;OAOG;IACH,OAAO,CAAC,WAAW;IAOnB;;;;;;;;OAQG;IACH,OAAO,CAAC,cAAc;IA2BtB;;;;;;;;;;;;;;;;;;;OAmBG;IACH,OAAO,CAAC,OAAO;IAgCf;;OAEG;IACH,OAAO,CAAC,YAAY;IA4CpB;;OAEG;IACH,OAAO,CAAC,KAAK;CA0Db"}
|
|
@ -0,0 +1,427 @@
|
||||||
|
"use strict";
|
||||||
|
/**
|
||||||
|
* NeuQuant Neural-Net Quantization Algorithm
|
||||||
|
* ------------------------------------------
|
||||||
|
*
|
||||||
|
* Copyright (c) 1994 Anthony Dekker
|
||||||
|
*
|
||||||
|
* NEUQUANT Neural-Net quantization algorithm by Anthony Dekker, 1994.
|
||||||
|
* See "Kohonen neural networks for optimal colour quantization"
|
||||||
|
* in "Network: Computation in Neural Systems" Vol. 5 (1994) pp 351-367.
|
||||||
|
* for a discussion of the algorithm.
|
||||||
|
* See also http://members.ozemail.com.au/~dekker/NEUQUANT.HTML
|
||||||
|
*
|
||||||
|
* Any party obtaining a copy of these files from the author, directly or
|
||||||
|
* indirectly, is granted, free of charge, a full and unrestricted irrevocable,
|
||||||
|
* world-wide, paid up, royalty-free, nonexclusive right and license to deal
|
||||||
|
* in this software and documentation files (the "Software"), including without
|
||||||
|
* limitation the rights to use, copy, modify, merge, publish, distribute, sublicense,
|
||||||
|
* and/or sell copies of the Software, and to permit persons who receive
|
||||||
|
* copies from any such party to do so, with the only requirement being
|
||||||
|
* that this copyright notice remain intact.
|
||||||
|
*
|
||||||
|
* (JavaScript port 2012 by Johan Nordberg)
|
||||||
|
* (TypeScript port 2021 by Antonio Román)
|
||||||
|
*/
|
||||||
|
Object.defineProperty(exports, "__esModule", { value: true });
|
||||||
|
exports.NeuQuant = void 0;
|
||||||
|
/* eslint-disable prefer-destructuring, no-negated-condition */
|
||||||
|
const learningCycles = 100; // Number of learning cycles.
|
||||||
|
const maximumColorsSize = 256; // Number of colors used.
|
||||||
|
const maximumColorsPosition = maximumColorsSize - 1;
|
||||||
|
// defs for freq and bias
|
||||||
|
const networkBiasShift = 4; // Bias for color values.
|
||||||
|
const integerBiasShift = 16; // Bias for fractions.
|
||||||
|
const integerBias = 1 << integerBiasShift;
|
||||||
|
const gammaShift = 10;
|
||||||
|
const betaShift = 10;
|
||||||
|
const beta = integerBias >> betaShift; // `beta` = 1 / 1024
|
||||||
|
const betaGamma = integerBias << (gammaShift - betaShift);
|
||||||
|
// Defaults for decreasing radius factor:
|
||||||
|
// -> For 256 colors, radius starts at 32.0 biased by 6 bits and decreases by a factor of 1 / 30 each cycle.
|
||||||
|
const maximumRadius = maximumColorsSize >> 3;
|
||||||
|
const initialRadiusBiasShift = 6;
|
||||||
|
const initialRadiusBias = 1 << initialRadiusBiasShift;
|
||||||
|
const initialRadius = maximumRadius * initialRadiusBias;
|
||||||
|
const initialRadiusDecrement = 30;
|
||||||
|
// Defaults for decreasing alpha factor:
|
||||||
|
// -> Alpha starts at 1.0
|
||||||
|
const alphaBiasShift = 10;
|
||||||
|
const initialAlpha = 1 << alphaBiasShift;
|
||||||
|
// Constants used for radius power calculation:
|
||||||
|
const radiusBiasShift = 8;
|
||||||
|
const radiusBias = 1 << radiusBiasShift;
|
||||||
|
const alphaRadiusBiasShift = alphaBiasShift + radiusBiasShift;
|
||||||
|
const alphaRadiusBias = 1 << alphaRadiusBiasShift;
|
||||||
|
// Four primes near 500 - assume no image has a length so large that it is divisible by all four primes:
|
||||||
|
const prime1 = 499;
|
||||||
|
const prime2 = 491;
|
||||||
|
const prime3 = 487;
|
||||||
|
const prime4 = 503;
|
||||||
|
const minimumPictureBytes = 3 * prime4;
|
||||||
|
class NeuQuant {
|
||||||
|
/**
|
||||||
|
* Creates the neural quantifier instance.
|
||||||
|
* @param pixels Array of pixels in RGB format, as such that it's decoded as `[r, g, b, r, g, b, r, g, b, ...]`.
|
||||||
|
* @param sampleFactorial Sampling factor from `1` to `30`, where lower is better quality.
|
||||||
|
*/
|
||||||
|
constructor(pixels, sampleFactorial) {
|
||||||
|
/**
|
||||||
|
* Array of pixels in RGB format, as such that it's decoded as `[r, g, b, r, g, b, r, g, b, ...]`.
|
||||||
|
*/
|
||||||
|
Object.defineProperty(this, "pixels", {
|
||||||
|
enumerable: true,
|
||||||
|
configurable: true,
|
||||||
|
writable: true,
|
||||||
|
value: void 0
|
||||||
|
});
|
||||||
|
/**
|
||||||
|
* Sampling factor from `1` to `30`, where lower is better quality.
|
||||||
|
*/
|
||||||
|
Object.defineProperty(this, "sampleFactorial", {
|
||||||
|
enumerable: true,
|
||||||
|
configurable: true,
|
||||||
|
writable: true,
|
||||||
|
value: void 0
|
||||||
|
});
|
||||||
|
/**
|
||||||
|
* The neural networks, composed by {@link maximumColorsSize} {@link Float64Array float arrays} of size 4.
|
||||||
|
*/
|
||||||
|
Object.defineProperty(this, "networks", {
|
||||||
|
enumerable: true,
|
||||||
|
configurable: true,
|
||||||
|
writable: true,
|
||||||
|
value: void 0
|
||||||
|
});
|
||||||
|
/**
|
||||||
|
* Network lookup indexes, composed by 256 indexes.
|
||||||
|
*/
|
||||||
|
Object.defineProperty(this, "networkIndexes", {
|
||||||
|
enumerable: true,
|
||||||
|
configurable: true,
|
||||||
|
writable: true,
|
||||||
|
value: void 0
|
||||||
|
});
|
||||||
|
// bias and freq arrays for learning
|
||||||
|
Object.defineProperty(this, "biases", {
|
||||||
|
enumerable: true,
|
||||||
|
configurable: true,
|
||||||
|
writable: true,
|
||||||
|
value: void 0
|
||||||
|
});
|
||||||
|
Object.defineProperty(this, "frequencies", {
|
||||||
|
enumerable: true,
|
||||||
|
configurable: true,
|
||||||
|
writable: true,
|
||||||
|
value: void 0
|
||||||
|
});
|
||||||
|
Object.defineProperty(this, "radiusPowers", {
|
||||||
|
enumerable: true,
|
||||||
|
configurable: true,
|
||||||
|
writable: true,
|
||||||
|
value: void 0
|
||||||
|
});
|
||||||
|
this.pixels = pixels;
|
||||||
|
this.sampleFactorial = sampleFactorial;
|
||||||
|
this.networks = [];
|
||||||
|
this.networkIndexes = new Int32Array(256);
|
||||||
|
this.biases = new Int32Array(maximumColorsSize);
|
||||||
|
this.frequencies = new Int32Array(maximumColorsSize);
|
||||||
|
this.radiusPowers = new Int32Array(maximumColorsSize >> 3);
|
||||||
|
this.init();
|
||||||
|
this.learn();
|
||||||
|
this.unBiasNetwork();
|
||||||
|
this.buildIndexes();
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Builds the networks' color map.
|
||||||
|
* @returns A RGB-encoded {@link Float64Array}.
|
||||||
|
*/
|
||||||
|
getColorMap() {
|
||||||
|
const map = new Float64Array(maximumColorsSize * 3);
|
||||||
|
const index = new Float64Array(maximumColorsSize);
|
||||||
|
for (let i = 0; i < maximumColorsSize; i++) {
|
||||||
|
index[this.networks[i][3]] = i;
|
||||||
|
}
|
||||||
|
for (let l = 0, k = 0; l < maximumColorsSize; l++) {
|
||||||
|
const network = this.networks[index[l]];
|
||||||
|
map[k++] = network[0];
|
||||||
|
map[k++] = network[1];
|
||||||
|
map[k++] = network[2];
|
||||||
|
}
|
||||||
|
return map;
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Searches for BGR values 0..255 and returns a color index
|
||||||
|
* @param b The blue color byte, between 0 and 255.
|
||||||
|
* @param g The green color byte, between 0 and 255.
|
||||||
|
* @param r The red color byte, between 0 and 255.
|
||||||
|
* @returns The best color index.
|
||||||
|
*/
|
||||||
|
lookupRGB(b, g, r) {
|
||||||
|
// Biggest possible distance is 256 * 3, so we will define the biggest as an out-of-bounds number.
|
||||||
|
let bestDistance = 1000;
|
||||||
|
let best = -1;
|
||||||
|
const index = this.networkIndexes[g];
|
||||||
|
// Index on `g`
|
||||||
|
for (let i = index; i < maximumColorsSize; ++i) {
|
||||||
|
const network = this.networks[i];
|
||||||
|
// Compare the distance of the green element, break if it's too big:
|
||||||
|
let distance = network[1] - g;
|
||||||
|
if (distance >= bestDistance)
|
||||||
|
break;
|
||||||
|
// If `distance` is negative, make it positive:
|
||||||
|
if (distance < 0)
|
||||||
|
distance = -distance;
|
||||||
|
// Compare the distance with the blue element added, continue if it's too big:
|
||||||
|
distance += Math.abs(network[0] - b);
|
||||||
|
if (distance >= bestDistance)
|
||||||
|
continue;
|
||||||
|
// Compare the distance with the red element added, continue if it's too big:
|
||||||
|
distance += Math.abs(network[2] - r);
|
||||||
|
if (distance >= bestDistance)
|
||||||
|
continue;
|
||||||
|
bestDistance = distance;
|
||||||
|
best = network[3];
|
||||||
|
}
|
||||||
|
// Start at networkIndex[g] and work outwards
|
||||||
|
for (let j = index - 1; j >= 0; --j) {
|
||||||
|
const network = this.networks[j];
|
||||||
|
// Compare the distance of the green element, break if it's too big:
|
||||||
|
let distance = g - network[1];
|
||||||
|
if (distance >= bestDistance)
|
||||||
|
break;
|
||||||
|
// If `distance` is negative, make it positive:
|
||||||
|
if (distance < 0)
|
||||||
|
distance = -distance;
|
||||||
|
// Compare the distance with the blue element added, continue if it's too big:
|
||||||
|
distance += Math.abs(network[0] - b);
|
||||||
|
if (distance >= bestDistance)
|
||||||
|
continue;
|
||||||
|
// Compare the distance with the red element added, continue if it's too big:
|
||||||
|
distance += Math.abs(network[2] - r);
|
||||||
|
if (distance >= bestDistance)
|
||||||
|
continue;
|
||||||
|
bestDistance = distance;
|
||||||
|
best = network[3];
|
||||||
|
}
|
||||||
|
return best;
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Initializes the state for the arrays.
|
||||||
|
*/
|
||||||
|
init() {
|
||||||
|
for (let i = 0; i < maximumColorsSize; i++) {
|
||||||
|
const v = (i << (networkBiasShift + 8)) / maximumColorsSize;
|
||||||
|
this.networks[i] = new Float64Array([v, v, v, 0]);
|
||||||
|
this.frequencies[i] = integerBias / maximumColorsSize;
|
||||||
|
this.biases[i] = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Un-biases network to give byte values 0..255 and record position i to prepare for sort.
|
||||||
|
*/
|
||||||
|
unBiasNetwork() {
|
||||||
|
for (let i = 0; i < maximumColorsSize; i++) {
|
||||||
|
const network = this.networks[i];
|
||||||
|
network[0] >>= networkBiasShift;
|
||||||
|
network[1] >>= networkBiasShift;
|
||||||
|
network[2] >>= networkBiasShift;
|
||||||
|
network[3] = i; // record color number
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Moves neuron `i` towards biased (`B`, `G`, `R`) by factor `alpha`.
|
||||||
|
* @param alpha The factor at which the neuron `i` should move towards.
|
||||||
|
* @param i The neuron's index.
|
||||||
|
* @param b The blue color.
|
||||||
|
* @param g The green color.
|
||||||
|
* @param r The red color.
|
||||||
|
*/
|
||||||
|
alterSingle(alpha, i, b, g, r) {
|
||||||
|
const network = this.networks[i];
|
||||||
|
network[0] -= (alpha * (network[0] - b)) / initialAlpha;
|
||||||
|
network[1] -= (alpha * (network[1] - g)) / initialAlpha;
|
||||||
|
network[2] -= (alpha * (network[2] - r)) / initialAlpha;
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Moves neurons in a `radius` around index `i` towards biased (`B`, `G`, `R`) by factor
|
||||||
|
* {@link NeuQuant.radiusPowers `radiusPower[m]`}.
|
||||||
|
* @param radius The radius around `i` to alter.
|
||||||
|
* @param i The neuron's index.
|
||||||
|
* @param b The blue color.
|
||||||
|
* @param g The green color.
|
||||||
|
* @param r The red color.
|
||||||
|
*/
|
||||||
|
alterNeighbors(radius, i, b, g, r) {
|
||||||
|
const lo = Math.abs(i - radius);
|
||||||
|
const hi = Math.min(i + radius, maximumColorsSize);
|
||||||
|
let j = i + 1;
|
||||||
|
let k = i - 1;
|
||||||
|
let m = 1;
|
||||||
|
while (j < hi || k > lo) {
|
||||||
|
const alpha = this.radiusPowers[m++];
|
||||||
|
if (j < hi) {
|
||||||
|
const network = this.networks[j++];
|
||||||
|
network[0] -= (alpha * (network[0] - b)) / alphaRadiusBias;
|
||||||
|
network[1] -= (alpha * (network[1] - g)) / alphaRadiusBias;
|
||||||
|
network[2] -= (alpha * (network[2] - r)) / alphaRadiusBias;
|
||||||
|
}
|
||||||
|
if (k > lo) {
|
||||||
|
const network = this.networks[k--];
|
||||||
|
network[0] -= (alpha * (network[0] - b)) / alphaRadiusBias;
|
||||||
|
network[1] -= (alpha * (network[1] - g)) / alphaRadiusBias;
|
||||||
|
network[2] -= (alpha * (network[2] - r)) / alphaRadiusBias;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Searches for biased BGR values.
|
||||||
|
*
|
||||||
|
* - Finds the closest neuron (minimum distance) and updates {@link NeuQuant.frequencies}.
|
||||||
|
* - Finds the best neuron (minimum distance-bias) and returns the position.
|
||||||
|
*
|
||||||
|
* For frequently chosen neurons, {@link NeuQuant.frequencies `frequencies[i]`} is high and
|
||||||
|
* {@link NeuQuant.biases `biases[i]`} is negative.
|
||||||
|
*
|
||||||
|
* The latter is determined by the multiplication of `gamma` with the subtraction of the inverse of
|
||||||
|
* {@link maximumColorsSize} with {@link NeuQuant.frequencies `frequencies[i]`}:
|
||||||
|
*
|
||||||
|
* ```typescript
|
||||||
|
* biases[i] = gamma * ((1 / maximumColorsSize) - frequencies[i])
|
||||||
|
* ```
|
||||||
|
* @param b The blue color.
|
||||||
|
* @param g The green color.
|
||||||
|
* @param r The red color.
|
||||||
|
* @returns The best bias position.
|
||||||
|
*/
|
||||||
|
contest(b, g, r) {
|
||||||
|
let bestDistance = ~(1 << 31);
|
||||||
|
let bestBiasDistance = bestDistance;
|
||||||
|
let bestPosition = -1;
|
||||||
|
let bestBiasPosition = bestPosition;
|
||||||
|
for (let i = 0; i < maximumColorsSize; i++) {
|
||||||
|
const network = this.networks[i];
|
||||||
|
const distance = Math.abs(network[0] - b) + Math.abs(network[1] - g) + Math.abs(network[2] - r);
|
||||||
|
if (distance < bestDistance) {
|
||||||
|
bestDistance = distance;
|
||||||
|
bestPosition = i;
|
||||||
|
}
|
||||||
|
const biasDistance = distance - (this.biases[i] >> (integerBiasShift - networkBiasShift));
|
||||||
|
if (biasDistance < bestBiasDistance) {
|
||||||
|
bestBiasDistance = biasDistance;
|
||||||
|
bestBiasPosition = i;
|
||||||
|
}
|
||||||
|
const betaFrequency = this.frequencies[i] >> betaShift;
|
||||||
|
this.frequencies[i] -= betaFrequency;
|
||||||
|
this.biases[i] += betaFrequency << gammaShift;
|
||||||
|
}
|
||||||
|
this.frequencies[bestPosition] += beta;
|
||||||
|
this.biases[bestPosition] -= betaGamma;
|
||||||
|
return bestBiasPosition;
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Sorts the neural network and builds {@link NeuQuant.networkIndexes `networkIndex[0..255]`}.
|
||||||
|
*/
|
||||||
|
buildIndexes() {
|
||||||
|
let previousColor = 0;
|
||||||
|
let startPosition = 0;
|
||||||
|
for (let i = 0; i < maximumColorsSize; i++) {
|
||||||
|
const network = this.networks[i];
|
||||||
|
let smallestPosition = i;
|
||||||
|
let smallestValue = network[1]; // index on g
|
||||||
|
// Find smallest in [i .. maximumColorsSize - 1]
|
||||||
|
for (let j = i + 1; j < maximumColorsSize; j++) {
|
||||||
|
const q = this.networks[j];
|
||||||
|
if (q[1] < smallestValue) {
|
||||||
|
smallestPosition = j;
|
||||||
|
smallestValue = q[1]; // index on g
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Swap network (i) and q (smallestPosition) entries:
|
||||||
|
if (i !== smallestPosition) {
|
||||||
|
const q = this.networks[smallestPosition];
|
||||||
|
[q[0], network[0]] = [network[0], q[0]];
|
||||||
|
[q[1], network[1]] = [network[1], q[1]];
|
||||||
|
[q[2], network[2]] = [network[2], q[2]];
|
||||||
|
[q[3], network[3]] = [network[3], q[3]];
|
||||||
|
}
|
||||||
|
// smallestValue entry is now in position i
|
||||||
|
if (smallestValue !== previousColor) {
|
||||||
|
this.networkIndexes[previousColor] = (startPosition + i) >> 1;
|
||||||
|
for (let j = previousColor + 1; j < smallestValue; j++) {
|
||||||
|
this.networkIndexes[j] = i;
|
||||||
|
}
|
||||||
|
previousColor = smallestValue;
|
||||||
|
startPosition = i;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this.networkIndexes[previousColor] = (startPosition + maximumColorsPosition) >> 1;
|
||||||
|
for (let j = previousColor + 1; j < 256; j++) {
|
||||||
|
this.networkIndexes[j] = maximumColorsPosition;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Runs the main learning loop.
|
||||||
|
*/
|
||||||
|
learn() {
|
||||||
|
const length = this.pixels.length;
|
||||||
|
const alphaDecrement = 30 + (this.sampleFactorial - 1) / 3;
|
||||||
|
const samplePixels = length / (3 * this.sampleFactorial);
|
||||||
|
let delta = ~~(samplePixels / learningCycles);
|
||||||
|
let alpha = initialAlpha;
|
||||||
|
let radius = initialRadius;
|
||||||
|
let localRadius = radius >> initialRadiusBiasShift;
|
||||||
|
if (localRadius <= 1)
|
||||||
|
localRadius = 0;
|
||||||
|
for (let i = 0; i < localRadius; i++) {
|
||||||
|
this.radiusPowers[i] = alpha * (((localRadius * localRadius - i * i) * radiusBias) / (localRadius * localRadius));
|
||||||
|
}
|
||||||
|
let step;
|
||||||
|
if (length < minimumPictureBytes) {
|
||||||
|
this.sampleFactorial = 1;
|
||||||
|
step = 3;
|
||||||
|
}
|
||||||
|
else if (length % prime1 !== 0) {
|
||||||
|
step = 3 * prime1;
|
||||||
|
}
|
||||||
|
else if (length % prime2 !== 0) {
|
||||||
|
step = 3 * prime2;
|
||||||
|
}
|
||||||
|
else if (length % prime3 !== 0) {
|
||||||
|
step = 3 * prime3;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
step = 3 * prime4;
|
||||||
|
}
|
||||||
|
let pixelPosition = 0;
|
||||||
|
let i = 0;
|
||||||
|
while (i < samplePixels) {
|
||||||
|
const b = (this.pixels[pixelPosition] & 0xff) << networkBiasShift;
|
||||||
|
const g = (this.pixels[pixelPosition + 1] & 0xff) << networkBiasShift;
|
||||||
|
const r = (this.pixels[pixelPosition + 2] & 0xff) << networkBiasShift;
|
||||||
|
let j = this.contest(b, g, r);
|
||||||
|
this.alterSingle(alpha, j, b, g, r);
|
||||||
|
if (localRadius !== 0)
|
||||||
|
this.alterNeighbors(localRadius, j, b, g, r);
|
||||||
|
pixelPosition += step;
|
||||||
|
if (pixelPosition >= length)
|
||||||
|
pixelPosition -= length;
|
||||||
|
if (delta === 0)
|
||||||
|
delta = 1;
|
||||||
|
++i;
|
||||||
|
if (i % delta !== 0)
|
||||||
|
continue;
|
||||||
|
alpha -= alpha / alphaDecrement;
|
||||||
|
radius -= radius / initialRadiusDecrement;
|
||||||
|
localRadius = radius >> initialRadiusBiasShift;
|
||||||
|
if (localRadius <= 1)
|
||||||
|
localRadius = 0;
|
||||||
|
for (j = 0; j < localRadius; j++) {
|
||||||
|
this.radiusPowers[j] = alpha * (((localRadius * localRadius - j * j) * radiusBias) / (localRadius * localRadius));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
exports.NeuQuant = NeuQuant;
|
||||||
|
//# sourceMappingURL=NeuQuant.js.map
|
File diff suppressed because one or more lines are too long
|
@ -0,0 +1,104 @@
|
||||||
|
{
|
||||||
|
"name": "@skyra/gifenc",
|
||||||
|
"version": "1.0.1",
|
||||||
|
"description": "A very fast server-side animated GIF generation for Node.js",
|
||||||
|
"author": "@skyra",
|
||||||
|
"license": "MIT",
|
||||||
|
"main": "dist/index.js",
|
||||||
|
"module": "dist/index.mjs",
|
||||||
|
"types": "dist/index.d.ts",
|
||||||
|
"exports": {
|
||||||
|
"import": "./dist/index.mjs",
|
||||||
|
"require": "./dist/index.js",
|
||||||
|
"types": "./dist/index.d.ts"
|
||||||
|
},
|
||||||
|
"sideEffects": false,
|
||||||
|
"homepage": "https://skyra-project.github.io/gifenc",
|
||||||
|
"files": [
|
||||||
|
"dist",
|
||||||
|
"!dist/tsconfig.tsbuildinfo"
|
||||||
|
],
|
||||||
|
"scripts": {
|
||||||
|
"lint": "eslint src --ext ts --fix",
|
||||||
|
"format": "prettier --write \"{src}/**/*.ts\"",
|
||||||
|
"docs": "typedoc",
|
||||||
|
"update": "yarn up \"@*/*\" -i && yarn up \"*\" -i",
|
||||||
|
"build": "tsc -b src && gen-esm-wrapper dist/index.js dist/index.mjs",
|
||||||
|
"watch": "yarn build -w",
|
||||||
|
"clean": "node scripts/clean-dist.mjs",
|
||||||
|
"sversion": "standard-version",
|
||||||
|
"prepublishOnly": "yarn clean && yarn build",
|
||||||
|
"prepare": "husky install .github/husky"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"tslib": "^2.4.0"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@commitlint/cli": "^17.1.2",
|
||||||
|
"@commitlint/config-conventional": "^17.1.0",
|
||||||
|
"@sapphire/eslint-config": "^4.3.8",
|
||||||
|
"@sapphire/prettier-config": "^1.4.4",
|
||||||
|
"@sapphire/ts-config": "^3.3.4",
|
||||||
|
"@types/jest": "^29.2.0",
|
||||||
|
"@types/node": "^17.0.18",
|
||||||
|
"@typescript-eslint/eslint-plugin": "^5.40.1",
|
||||||
|
"@typescript-eslint/parser": "^5.40.1",
|
||||||
|
"cz-conventional-changelog": "^3.3.0",
|
||||||
|
"eslint": "^8.26.0",
|
||||||
|
"eslint-config-prettier": "^8.5.0",
|
||||||
|
"eslint-plugin-prettier": "^4.2.1",
|
||||||
|
"gen-esm-wrapper": "^1.1.3",
|
||||||
|
"husky": "^8.0.1",
|
||||||
|
"lint-staged": "^13.0.3",
|
||||||
|
"prettier": "^2.7.1",
|
||||||
|
"pretty-quick": "^3.1.3",
|
||||||
|
"standard-version": "^9.3.2",
|
||||||
|
"typedoc": "^0.23.17",
|
||||||
|
"typescript": "^4.8.4"
|
||||||
|
},
|
||||||
|
"resolutions": {
|
||||||
|
"ansi-regex": "^5.0.1",
|
||||||
|
"minimist": "^1.2.7"
|
||||||
|
},
|
||||||
|
"repository": {
|
||||||
|
"type": "git",
|
||||||
|
"url": "git+https://github.com/skyra-project/gifenc.git"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=v14.18.0",
|
||||||
|
"npm": ">=7.24.2"
|
||||||
|
},
|
||||||
|
"keywords": [
|
||||||
|
"typescript",
|
||||||
|
"ts",
|
||||||
|
"yarn",
|
||||||
|
"gif",
|
||||||
|
"gifenc",
|
||||||
|
"encoder",
|
||||||
|
"utility"
|
||||||
|
],
|
||||||
|
"bugs": {
|
||||||
|
"url": "https://github.com/skyra-project/gifenc/issues"
|
||||||
|
},
|
||||||
|
"commitlint": {
|
||||||
|
"extends": [
|
||||||
|
"@commitlint/config-conventional"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"lint-staged": {
|
||||||
|
"*.{mjs,js,ts}": "eslint --fix --ext mjs,js,ts"
|
||||||
|
},
|
||||||
|
"config": {
|
||||||
|
"commitizen": {
|
||||||
|
"path": "./node_modules/cz-conventional-changelog"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"publishConfig": {
|
||||||
|
"access": "public"
|
||||||
|
},
|
||||||
|
"eslintConfig": {
|
||||||
|
"extends": "@sapphire"
|
||||||
|
},
|
||||||
|
"prettier": "@sapphire/prettier-config",
|
||||||
|
"packageManager": "yarn@3.2.4"
|
||||||
|
}
|
|
@ -0,0 +1,21 @@
|
||||||
|
MIT License
|
||||||
|
|
||||||
|
Copyright (c) 2017 Toru Nagashima
|
||||||
|
|
||||||
|
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.
|
|
@ -0,0 +1,98 @@
|
||||||
|
# abort-controller
|
||||||
|
|
||||||
|
[![npm version](https://img.shields.io/npm/v/abort-controller.svg)](https://www.npmjs.com/package/abort-controller)
|
||||||
|
[![Downloads/month](https://img.shields.io/npm/dm/abort-controller.svg)](http://www.npmtrends.com/abort-controller)
|
||||||
|
[![Build Status](https://travis-ci.org/mysticatea/abort-controller.svg?branch=master)](https://travis-ci.org/mysticatea/abort-controller)
|
||||||
|
[![Coverage Status](https://codecov.io/gh/mysticatea/abort-controller/branch/master/graph/badge.svg)](https://codecov.io/gh/mysticatea/abort-controller)
|
||||||
|
[![Dependency Status](https://david-dm.org/mysticatea/abort-controller.svg)](https://david-dm.org/mysticatea/abort-controller)
|
||||||
|
|
||||||
|
An implementation of [WHATWG AbortController interface](https://dom.spec.whatwg.org/#interface-abortcontroller).
|
||||||
|
|
||||||
|
```js
|
||||||
|
import AbortController from "abort-controller"
|
||||||
|
|
||||||
|
const controller = new AbortController()
|
||||||
|
const signal = controller.signal
|
||||||
|
|
||||||
|
signal.addEventListener("abort", () => {
|
||||||
|
console.log("aborted!")
|
||||||
|
})
|
||||||
|
|
||||||
|
controller.abort()
|
||||||
|
```
|
||||||
|
|
||||||
|
> https://jsfiddle.net/1r2994qp/1/
|
||||||
|
|
||||||
|
## 💿 Installation
|
||||||
|
|
||||||
|
Use [npm](https://www.npmjs.com/) to install then use a bundler.
|
||||||
|
|
||||||
|
```
|
||||||
|
npm install abort-controller
|
||||||
|
```
|
||||||
|
|
||||||
|
Or download from [`dist` directory](./dist).
|
||||||
|
|
||||||
|
- [dist/abort-controller.mjs](dist/abort-controller.mjs) ... ES modules version.
|
||||||
|
- [dist/abort-controller.js](dist/abort-controller.js) ... Common JS version.
|
||||||
|
- [dist/abort-controller.umd.js](dist/abort-controller.umd.js) ... UMD (Universal Module Definition) version. This is transpiled by [Babel](https://babeljs.io/) for IE 11.
|
||||||
|
|
||||||
|
## 📖 Usage
|
||||||
|
|
||||||
|
### Basic
|
||||||
|
|
||||||
|
```js
|
||||||
|
import AbortController from "abort-controller"
|
||||||
|
// or
|
||||||
|
const AbortController = require("abort-controller")
|
||||||
|
|
||||||
|
// or UMD version defines a global variable:
|
||||||
|
const AbortController = window.AbortControllerShim
|
||||||
|
```
|
||||||
|
|
||||||
|
If your bundler recognizes `browser` field of `package.json`, the imported `AbortController` is the native one and it doesn't contain shim (even if the native implementation was nothing).
|
||||||
|
If you wanted to polyfill `AbortController` for IE, use `abort-controller/polyfill`.
|
||||||
|
|
||||||
|
### Polyfilling
|
||||||
|
|
||||||
|
Importing `abort-controller/polyfill` assigns the `AbortController` shim to the `AbortController` global variable if the native implementation was nothing.
|
||||||
|
|
||||||
|
```js
|
||||||
|
import "abort-controller/polyfill"
|
||||||
|
// or
|
||||||
|
require("abort-controller/polyfill")
|
||||||
|
```
|
||||||
|
|
||||||
|
### API
|
||||||
|
|
||||||
|
#### AbortController
|
||||||
|
|
||||||
|
> https://dom.spec.whatwg.org/#interface-abortcontroller
|
||||||
|
|
||||||
|
##### controller.signal
|
||||||
|
|
||||||
|
The [AbortSignal](https://dom.spec.whatwg.org/#interface-AbortSignal) object which is associated to this controller.
|
||||||
|
|
||||||
|
##### controller.abort()
|
||||||
|
|
||||||
|
Notify `abort` event to listeners that the `signal` has.
|
||||||
|
|
||||||
|
## 📰 Changelog
|
||||||
|
|
||||||
|
- See [GitHub releases](https://github.com/mysticatea/abort-controller/releases).
|
||||||
|
|
||||||
|
## 🍻 Contributing
|
||||||
|
|
||||||
|
Contributing is welcome ❤️
|
||||||
|
|
||||||
|
Please use GitHub issues/PRs.
|
||||||
|
|
||||||
|
### Development tools
|
||||||
|
|
||||||
|
- `npm install` installs dependencies for development.
|
||||||
|
- `npm test` runs tests and measures code coverage.
|
||||||
|
- `npm run clean` removes temporary files of tests.
|
||||||
|
- `npm run coverage` opens code coverage of the previous test with your default browser.
|
||||||
|
- `npm run lint` runs ESLint.
|
||||||
|
- `npm run build` generates `dist` codes.
|
||||||
|
- `npm run watch` runs tests on each file change.
|
|
@ -0,0 +1,13 @@
|
||||||
|
/*globals self, window */
|
||||||
|
"use strict"
|
||||||
|
|
||||||
|
/*eslint-disable @mysticatea/prettier */
|
||||||
|
const { AbortController, AbortSignal } =
|
||||||
|
typeof self !== "undefined" ? self :
|
||||||
|
typeof window !== "undefined" ? window :
|
||||||
|
/* otherwise */ undefined
|
||||||
|
/*eslint-enable @mysticatea/prettier */
|
||||||
|
|
||||||
|
module.exports = AbortController
|
||||||
|
module.exports.AbortSignal = AbortSignal
|
||||||
|
module.exports.default = AbortController
|
|
@ -0,0 +1,11 @@
|
||||||
|
/*globals self, window */
|
||||||
|
|
||||||
|
/*eslint-disable @mysticatea/prettier */
|
||||||
|
const { AbortController, AbortSignal } =
|
||||||
|
typeof self !== "undefined" ? self :
|
||||||
|
typeof window !== "undefined" ? window :
|
||||||
|
/* otherwise */ undefined
|
||||||
|
/*eslint-enable @mysticatea/prettier */
|
||||||
|
|
||||||
|
export default AbortController
|
||||||
|
export { AbortController, AbortSignal }
|
|
@ -0,0 +1,43 @@
|
||||||
|
import { EventTarget } from "event-target-shim"
|
||||||
|
|
||||||
|
type Events = {
|
||||||
|
abort: any
|
||||||
|
}
|
||||||
|
type EventAttributes = {
|
||||||
|
onabort: any
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* The signal class.
|
||||||
|
* @see https://dom.spec.whatwg.org/#abortsignal
|
||||||
|
*/
|
||||||
|
declare class AbortSignal extends EventTarget<Events, EventAttributes> {
|
||||||
|
/**
|
||||||
|
* AbortSignal cannot be constructed directly.
|
||||||
|
*/
|
||||||
|
constructor()
|
||||||
|
/**
|
||||||
|
* Returns `true` if this `AbortSignal`"s `AbortController` has signaled to abort, and `false` otherwise.
|
||||||
|
*/
|
||||||
|
readonly aborted: boolean
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* The AbortController.
|
||||||
|
* @see https://dom.spec.whatwg.org/#abortcontroller
|
||||||
|
*/
|
||||||
|
declare class AbortController {
|
||||||
|
/**
|
||||||
|
* Initialize this controller.
|
||||||
|
*/
|
||||||
|
constructor()
|
||||||
|
/**
|
||||||
|
* Returns the `AbortSignal` object associated with this object.
|
||||||
|
*/
|
||||||
|
readonly signal: AbortSignal
|
||||||
|
/**
|
||||||
|
* Abort and signal to any observers that the associated activity is to be aborted.
|
||||||
|
*/
|
||||||
|
abort(): void
|
||||||
|
}
|
||||||
|
|
||||||
|
export default AbortController
|
||||||
|
export { AbortController, AbortSignal }
|
|
@ -0,0 +1,127 @@
|
||||||
|
/**
|
||||||
|
* @author Toru Nagashima <https://github.com/mysticatea>
|
||||||
|
* See LICENSE file in root directory for full license.
|
||||||
|
*/
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
Object.defineProperty(exports, '__esModule', { value: true });
|
||||||
|
|
||||||
|
var eventTargetShim = require('event-target-shim');
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The signal class.
|
||||||
|
* @see https://dom.spec.whatwg.org/#abortsignal
|
||||||
|
*/
|
||||||
|
class AbortSignal extends eventTargetShim.EventTarget {
|
||||||
|
/**
|
||||||
|
* AbortSignal cannot be constructed directly.
|
||||||
|
*/
|
||||||
|
constructor() {
|
||||||
|
super();
|
||||||
|
throw new TypeError("AbortSignal cannot be constructed directly");
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Returns `true` if this `AbortSignal`'s `AbortController` has signaled to abort, and `false` otherwise.
|
||||||
|
*/
|
||||||
|
get aborted() {
|
||||||
|
const aborted = abortedFlags.get(this);
|
||||||
|
if (typeof aborted !== "boolean") {
|
||||||
|
throw new TypeError(`Expected 'this' to be an 'AbortSignal' object, but got ${this === null ? "null" : typeof this}`);
|
||||||
|
}
|
||||||
|
return aborted;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
eventTargetShim.defineEventAttribute(AbortSignal.prototype, "abort");
|
||||||
|
/**
|
||||||
|
* Create an AbortSignal object.
|
||||||
|
*/
|
||||||
|
function createAbortSignal() {
|
||||||
|
const signal = Object.create(AbortSignal.prototype);
|
||||||
|
eventTargetShim.EventTarget.call(signal);
|
||||||
|
abortedFlags.set(signal, false);
|
||||||
|
return signal;
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Abort a given signal.
|
||||||
|
*/
|
||||||
|
function abortSignal(signal) {
|
||||||
|
if (abortedFlags.get(signal) !== false) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
abortedFlags.set(signal, true);
|
||||||
|
signal.dispatchEvent({ type: "abort" });
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Aborted flag for each instances.
|
||||||
|
*/
|
||||||
|
const abortedFlags = new WeakMap();
|
||||||
|
// Properties should be enumerable.
|
||||||
|
Object.defineProperties(AbortSignal.prototype, {
|
||||||
|
aborted: { enumerable: true },
|
||||||
|
});
|
||||||
|
// `toString()` should return `"[object AbortSignal]"`
|
||||||
|
if (typeof Symbol === "function" && typeof Symbol.toStringTag === "symbol") {
|
||||||
|
Object.defineProperty(AbortSignal.prototype, Symbol.toStringTag, {
|
||||||
|
configurable: true,
|
||||||
|
value: "AbortSignal",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The AbortController.
|
||||||
|
* @see https://dom.spec.whatwg.org/#abortcontroller
|
||||||
|
*/
|
||||||
|
class AbortController {
|
||||||
|
/**
|
||||||
|
* Initialize this controller.
|
||||||
|
*/
|
||||||
|
constructor() {
|
||||||
|
signals.set(this, createAbortSignal());
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Returns the `AbortSignal` object associated with this object.
|
||||||
|
*/
|
||||||
|
get signal() {
|
||||||
|
return getSignal(this);
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Abort and signal to any observers that the associated activity is to be aborted.
|
||||||
|
*/
|
||||||
|
abort() {
|
||||||
|
abortSignal(getSignal(this));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Associated signals.
|
||||||
|
*/
|
||||||
|
const signals = new WeakMap();
|
||||||
|
/**
|
||||||
|
* Get the associated signal of a given controller.
|
||||||
|
*/
|
||||||
|
function getSignal(controller) {
|
||||||
|
const signal = signals.get(controller);
|
||||||
|
if (signal == null) {
|
||||||
|
throw new TypeError(`Expected 'this' to be an 'AbortController' object, but got ${controller === null ? "null" : typeof controller}`);
|
||||||
|
}
|
||||||
|
return signal;
|
||||||
|
}
|
||||||
|
// Properties should be enumerable.
|
||||||
|
Object.defineProperties(AbortController.prototype, {
|
||||||
|
signal: { enumerable: true },
|
||||||
|
abort: { enumerable: true },
|
||||||
|
});
|
||||||
|
if (typeof Symbol === "function" && typeof Symbol.toStringTag === "symbol") {
|
||||||
|
Object.defineProperty(AbortController.prototype, Symbol.toStringTag, {
|
||||||
|
configurable: true,
|
||||||
|
value: "AbortController",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
exports.AbortController = AbortController;
|
||||||
|
exports.AbortSignal = AbortSignal;
|
||||||
|
exports.default = AbortController;
|
||||||
|
|
||||||
|
module.exports = AbortController
|
||||||
|
module.exports.AbortController = module.exports["default"] = AbortController
|
||||||
|
module.exports.AbortSignal = AbortSignal
|
||||||
|
//# sourceMappingURL=abort-controller.js.map
|
File diff suppressed because one or more lines are too long
|
@ -0,0 +1,118 @@
|
||||||
|
/**
|
||||||
|
* @author Toru Nagashima <https://github.com/mysticatea>
|
||||||
|
* See LICENSE file in root directory for full license.
|
||||||
|
*/
|
||||||
|
import { EventTarget, defineEventAttribute } from 'event-target-shim';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The signal class.
|
||||||
|
* @see https://dom.spec.whatwg.org/#abortsignal
|
||||||
|
*/
|
||||||
|
class AbortSignal extends EventTarget {
|
||||||
|
/**
|
||||||
|
* AbortSignal cannot be constructed directly.
|
||||||
|
*/
|
||||||
|
constructor() {
|
||||||
|
super();
|
||||||
|
throw new TypeError("AbortSignal cannot be constructed directly");
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Returns `true` if this `AbortSignal`'s `AbortController` has signaled to abort, and `false` otherwise.
|
||||||
|
*/
|
||||||
|
get aborted() {
|
||||||
|
const aborted = abortedFlags.get(this);
|
||||||
|
if (typeof aborted !== "boolean") {
|
||||||
|
throw new TypeError(`Expected 'this' to be an 'AbortSignal' object, but got ${this === null ? "null" : typeof this}`);
|
||||||
|
}
|
||||||
|
return aborted;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
defineEventAttribute(AbortSignal.prototype, "abort");
|
||||||
|
/**
|
||||||
|
* Create an AbortSignal object.
|
||||||
|
*/
|
||||||
|
function createAbortSignal() {
|
||||||
|
const signal = Object.create(AbortSignal.prototype);
|
||||||
|
EventTarget.call(signal);
|
||||||
|
abortedFlags.set(signal, false);
|
||||||
|
return signal;
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Abort a given signal.
|
||||||
|
*/
|
||||||
|
function abortSignal(signal) {
|
||||||
|
if (abortedFlags.get(signal) !== false) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
abortedFlags.set(signal, true);
|
||||||
|
signal.dispatchEvent({ type: "abort" });
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Aborted flag for each instances.
|
||||||
|
*/
|
||||||
|
const abortedFlags = new WeakMap();
|
||||||
|
// Properties should be enumerable.
|
||||||
|
Object.defineProperties(AbortSignal.prototype, {
|
||||||
|
aborted: { enumerable: true },
|
||||||
|
});
|
||||||
|
// `toString()` should return `"[object AbortSignal]"`
|
||||||
|
if (typeof Symbol === "function" && typeof Symbol.toStringTag === "symbol") {
|
||||||
|
Object.defineProperty(AbortSignal.prototype, Symbol.toStringTag, {
|
||||||
|
configurable: true,
|
||||||
|
value: "AbortSignal",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The AbortController.
|
||||||
|
* @see https://dom.spec.whatwg.org/#abortcontroller
|
||||||
|
*/
|
||||||
|
class AbortController {
|
||||||
|
/**
|
||||||
|
* Initialize this controller.
|
||||||
|
*/
|
||||||
|
constructor() {
|
||||||
|
signals.set(this, createAbortSignal());
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Returns the `AbortSignal` object associated with this object.
|
||||||
|
*/
|
||||||
|
get signal() {
|
||||||
|
return getSignal(this);
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Abort and signal to any observers that the associated activity is to be aborted.
|
||||||
|
*/
|
||||||
|
abort() {
|
||||||
|
abortSignal(getSignal(this));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Associated signals.
|
||||||
|
*/
|
||||||
|
const signals = new WeakMap();
|
||||||
|
/**
|
||||||
|
* Get the associated signal of a given controller.
|
||||||
|
*/
|
||||||
|
function getSignal(controller) {
|
||||||
|
const signal = signals.get(controller);
|
||||||
|
if (signal == null) {
|
||||||
|
throw new TypeError(`Expected 'this' to be an 'AbortController' object, but got ${controller === null ? "null" : typeof controller}`);
|
||||||
|
}
|
||||||
|
return signal;
|
||||||
|
}
|
||||||
|
// Properties should be enumerable.
|
||||||
|
Object.defineProperties(AbortController.prototype, {
|
||||||
|
signal: { enumerable: true },
|
||||||
|
abort: { enumerable: true },
|
||||||
|
});
|
||||||
|
if (typeof Symbol === "function" && typeof Symbol.toStringTag === "symbol") {
|
||||||
|
Object.defineProperty(AbortController.prototype, Symbol.toStringTag, {
|
||||||
|
configurable: true,
|
||||||
|
value: "AbortController",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export default AbortController;
|
||||||
|
export { AbortController, AbortSignal };
|
||||||
|
//# sourceMappingURL=abort-controller.mjs.map
|
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
|
@ -0,0 +1,97 @@
|
||||||
|
{
|
||||||
|
"name": "abort-controller",
|
||||||
|
"version": "3.0.0",
|
||||||
|
"description": "An implementation of WHATWG AbortController interface.",
|
||||||
|
"main": "dist/abort-controller",
|
||||||
|
"files": [
|
||||||
|
"dist",
|
||||||
|
"polyfill.*",
|
||||||
|
"browser.*"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=6.5"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"event-target-shim": "^5.0.0"
|
||||||
|
},
|
||||||
|
"browser": "./browser.js",
|
||||||
|
"devDependencies": {
|
||||||
|
"@babel/core": "^7.2.2",
|
||||||
|
"@babel/plugin-transform-modules-commonjs": "^7.2.0",
|
||||||
|
"@babel/preset-env": "^7.3.0",
|
||||||
|
"@babel/register": "^7.0.0",
|
||||||
|
"@mysticatea/eslint-plugin": "^8.0.1",
|
||||||
|
"@mysticatea/spy": "^0.1.2",
|
||||||
|
"@types/mocha": "^5.2.5",
|
||||||
|
"@types/node": "^10.12.18",
|
||||||
|
"assert": "^1.4.1",
|
||||||
|
"codecov": "^3.1.0",
|
||||||
|
"dts-bundle-generator": "^2.0.0",
|
||||||
|
"eslint": "^5.12.1",
|
||||||
|
"karma": "^3.1.4",
|
||||||
|
"karma-chrome-launcher": "^2.2.0",
|
||||||
|
"karma-coverage": "^1.1.2",
|
||||||
|
"karma-firefox-launcher": "^1.1.0",
|
||||||
|
"karma-growl-reporter": "^1.0.0",
|
||||||
|
"karma-ie-launcher": "^1.0.0",
|
||||||
|
"karma-mocha": "^1.3.0",
|
||||||
|
"karma-rollup-preprocessor": "^7.0.0-rc.2",
|
||||||
|
"mocha": "^5.2.0",
|
||||||
|
"npm-run-all": "^4.1.5",
|
||||||
|
"nyc": "^13.1.0",
|
||||||
|
"opener": "^1.5.1",
|
||||||
|
"rimraf": "^2.6.3",
|
||||||
|
"rollup": "^1.1.2",
|
||||||
|
"rollup-plugin-babel": "^4.3.2",
|
||||||
|
"rollup-plugin-babel-minify": "^7.0.0",
|
||||||
|
"rollup-plugin-commonjs": "^9.2.0",
|
||||||
|
"rollup-plugin-node-resolve": "^4.0.0",
|
||||||
|
"rollup-plugin-sourcemaps": "^0.4.2",
|
||||||
|
"rollup-plugin-typescript": "^1.0.0",
|
||||||
|
"rollup-watch": "^4.3.1",
|
||||||
|
"ts-node": "^8.0.1",
|
||||||
|
"type-tester": "^1.0.0",
|
||||||
|
"typescript": "^3.2.4"
|
||||||
|
},
|
||||||
|
"scripts": {
|
||||||
|
"preversion": "npm test",
|
||||||
|
"version": "npm run -s build && git add dist/*",
|
||||||
|
"postversion": "git push && git push --tags",
|
||||||
|
"clean": "rimraf .nyc_output coverage",
|
||||||
|
"coverage": "opener coverage/lcov-report/index.html",
|
||||||
|
"lint": "eslint . --ext .ts",
|
||||||
|
"build": "run-s -s build:*",
|
||||||
|
"build:rollup": "rollup -c",
|
||||||
|
"build:dts": "dts-bundle-generator -o dist/abort-controller.d.ts src/abort-controller.ts && ts-node scripts/fix-dts",
|
||||||
|
"test": "run-s -s lint test:*",
|
||||||
|
"test:mocha": "nyc mocha test/*.ts",
|
||||||
|
"test:karma": "karma start --single-run",
|
||||||
|
"watch": "run-p -s watch:*",
|
||||||
|
"watch:mocha": "mocha test/*.ts --require ts-node/register --watch-extensions ts --watch --growl",
|
||||||
|
"watch:karma": "karma start --watch",
|
||||||
|
"codecov": "codecov"
|
||||||
|
},
|
||||||
|
"repository": {
|
||||||
|
"type": "git",
|
||||||
|
"url": "git+https://github.com/mysticatea/abort-controller.git"
|
||||||
|
},
|
||||||
|
"keywords": [
|
||||||
|
"w3c",
|
||||||
|
"whatwg",
|
||||||
|
"event",
|
||||||
|
"events",
|
||||||
|
"abort",
|
||||||
|
"cancel",
|
||||||
|
"abortcontroller",
|
||||||
|
"abortsignal",
|
||||||
|
"controller",
|
||||||
|
"signal",
|
||||||
|
"shim"
|
||||||
|
],
|
||||||
|
"author": "Toru Nagashima (https://github.com/mysticatea)",
|
||||||
|
"license": "MIT",
|
||||||
|
"bugs": {
|
||||||
|
"url": "https://github.com/mysticatea/abort-controller/issues"
|
||||||
|
},
|
||||||
|
"homepage": "https://github.com/mysticatea/abort-controller#readme"
|
||||||
|
}
|
|
@ -0,0 +1,21 @@
|
||||||
|
/*globals require, self, window */
|
||||||
|
"use strict"
|
||||||
|
|
||||||
|
const ac = require("./dist/abort-controller")
|
||||||
|
|
||||||
|
/*eslint-disable @mysticatea/prettier */
|
||||||
|
const g =
|
||||||
|
typeof self !== "undefined" ? self :
|
||||||
|
typeof window !== "undefined" ? window :
|
||||||
|
typeof global !== "undefined" ? global :
|
||||||
|
/* otherwise */ undefined
|
||||||
|
/*eslint-enable @mysticatea/prettier */
|
||||||
|
|
||||||
|
if (g) {
|
||||||
|
if (typeof g.AbortController === "undefined") {
|
||||||
|
g.AbortController = ac.AbortController
|
||||||
|
}
|
||||||
|
if (typeof g.AbortSignal === "undefined") {
|
||||||
|
g.AbortSignal = ac.AbortSignal
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,19 @@
|
||||||
|
/*globals self, window */
|
||||||
|
import * as ac from "./dist/abort-controller"
|
||||||
|
|
||||||
|
/*eslint-disable @mysticatea/prettier */
|
||||||
|
const g =
|
||||||
|
typeof self !== "undefined" ? self :
|
||||||
|
typeof window !== "undefined" ? window :
|
||||||
|
typeof global !== "undefined" ? global :
|
||||||
|
/* otherwise */ undefined
|
||||||
|
/*eslint-enable @mysticatea/prettier */
|
||||||
|
|
||||||
|
if (g) {
|
||||||
|
if (typeof g.AbortController === "undefined") {
|
||||||
|
g.AbortController = ac.AbortController
|
||||||
|
}
|
||||||
|
if (typeof g.AbortSignal === "undefined") {
|
||||||
|
g.AbortSignal = ac.AbortSignal
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,20 @@
|
||||||
|
var Ajv = require('ajv');
|
||||||
|
var ajv = new Ajv({allErrors: true});
|
||||||
|
|
||||||
|
var schema = {
|
||||||
|
"properties": {
|
||||||
|
"foo": { "type": "string" },
|
||||||
|
"bar": { "type": "number", "maximum": 3 }
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
var validate = ajv.compile(schema);
|
||||||
|
|
||||||
|
test({"foo": "abc", "bar": 2});
|
||||||
|
test({"foo": 2, "bar": 4});
|
||||||
|
|
||||||
|
function test(data) {
|
||||||
|
var valid = validate(data);
|
||||||
|
if (valid) console.log('Valid!');
|
||||||
|
else console.log('Invalid: ' + ajv.errorsText(validate.errors));
|
||||||
|
}
|
|
@ -0,0 +1,22 @@
|
||||||
|
The MIT License (MIT)
|
||||||
|
|
||||||
|
Copyright (c) 2015-2017 Evgeny Poberezkin
|
||||||
|
|
||||||
|
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.
|
||||||
|
|
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
|
@ -0,0 +1,397 @@
|
||||||
|
declare var ajv: {
|
||||||
|
(options?: ajv.Options): ajv.Ajv;
|
||||||
|
new(options?: ajv.Options): ajv.Ajv;
|
||||||
|
ValidationError: typeof AjvErrors.ValidationError;
|
||||||
|
MissingRefError: typeof AjvErrors.MissingRefError;
|
||||||
|
$dataMetaSchema: object;
|
||||||
|
}
|
||||||
|
|
||||||
|
declare namespace AjvErrors {
|
||||||
|
class ValidationError extends Error {
|
||||||
|
constructor(errors: Array<ajv.ErrorObject>);
|
||||||
|
|
||||||
|
message: string;
|
||||||
|
errors: Array<ajv.ErrorObject>;
|
||||||
|
ajv: true;
|
||||||
|
validation: true;
|
||||||
|
}
|
||||||
|
|
||||||
|
class MissingRefError extends Error {
|
||||||
|
constructor(baseId: string, ref: string, message?: string);
|
||||||
|
static message: (baseId: string, ref: string) => string;
|
||||||
|
|
||||||
|
message: string;
|
||||||
|
missingRef: string;
|
||||||
|
missingSchema: string;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
declare namespace ajv {
|
||||||
|
type ValidationError = AjvErrors.ValidationError;
|
||||||
|
|
||||||
|
type MissingRefError = AjvErrors.MissingRefError;
|
||||||
|
|
||||||
|
interface Ajv {
|
||||||
|
/**
|
||||||
|
* Validate data using schema
|
||||||
|
* Schema will be compiled and cached (using serialized JSON as key, [fast-json-stable-stringify](https://github.com/epoberezkin/fast-json-stable-stringify) is used to serialize by default).
|
||||||
|
* @param {string|object|Boolean} schemaKeyRef key, ref or schema object
|
||||||
|
* @param {Any} data to be validated
|
||||||
|
* @return {Boolean} validation result. Errors from the last validation will be available in `ajv.errors` (and also in compiled schema: `schema.errors`).
|
||||||
|
*/
|
||||||
|
validate(schemaKeyRef: object | string | boolean, data: any): boolean | PromiseLike<any>;
|
||||||
|
/**
|
||||||
|
* Create validating function for passed schema.
|
||||||
|
* @param {object|Boolean} schema schema object
|
||||||
|
* @return {Function} validating function
|
||||||
|
*/
|
||||||
|
compile(schema: object | boolean): ValidateFunction;
|
||||||
|
/**
|
||||||
|
* Creates validating function for passed schema with asynchronous loading of missing schemas.
|
||||||
|
* `loadSchema` option should be a function that accepts schema uri and node-style callback.
|
||||||
|
* @this Ajv
|
||||||
|
* @param {object|Boolean} schema schema object
|
||||||
|
* @param {Boolean} meta optional true to compile meta-schema; this parameter can be skipped
|
||||||
|
* @param {Function} callback optional node-style callback, it is always called with 2 parameters: error (or null) and validating function.
|
||||||
|
* @return {PromiseLike<ValidateFunction>} validating function
|
||||||
|
*/
|
||||||
|
compileAsync(schema: object | boolean, meta?: Boolean, callback?: (err: Error, validate: ValidateFunction) => any): PromiseLike<ValidateFunction>;
|
||||||
|
/**
|
||||||
|
* Adds schema to the instance.
|
||||||
|
* @param {object|Array} schema schema or array of schemas. If array is passed, `key` and other parameters will be ignored.
|
||||||
|
* @param {string} key Optional schema key. Can be passed to `validate` method instead of schema object or id/ref. One schema per instance can have empty `id` and `key`.
|
||||||
|
* @return {Ajv} this for method chaining
|
||||||
|
*/
|
||||||
|
addSchema(schema: Array<object> | object, key?: string): Ajv;
|
||||||
|
/**
|
||||||
|
* Add schema that will be used to validate other schemas
|
||||||
|
* options in META_IGNORE_OPTIONS are alway set to false
|
||||||
|
* @param {object} schema schema object
|
||||||
|
* @param {string} key optional schema key
|
||||||
|
* @return {Ajv} this for method chaining
|
||||||
|
*/
|
||||||
|
addMetaSchema(schema: object, key?: string): Ajv;
|
||||||
|
/**
|
||||||
|
* Validate schema
|
||||||
|
* @param {object|Boolean} schema schema to validate
|
||||||
|
* @return {Boolean} true if schema is valid
|
||||||
|
*/
|
||||||
|
validateSchema(schema: object | boolean): boolean;
|
||||||
|
/**
|
||||||
|
* Get compiled schema from the instance by `key` or `ref`.
|
||||||
|
* @param {string} keyRef `key` that was passed to `addSchema` or full schema reference (`schema.id` or resolved id).
|
||||||
|
* @return {Function} schema validating function (with property `schema`). Returns undefined if keyRef can't be resolved to an existing schema.
|
||||||
|
*/
|
||||||
|
getSchema(keyRef: string): ValidateFunction | undefined;
|
||||||
|
/**
|
||||||
|
* Remove cached schema(s).
|
||||||
|
* If no parameter is passed all schemas but meta-schemas are removed.
|
||||||
|
* If RegExp is passed all schemas with key/id matching pattern but meta-schemas are removed.
|
||||||
|
* Even if schema is referenced by other schemas it still can be removed as other schemas have local references.
|
||||||
|
* @param {string|object|RegExp|Boolean} schemaKeyRef key, ref, pattern to match key/ref or schema object
|
||||||
|
* @return {Ajv} this for method chaining
|
||||||
|
*/
|
||||||
|
removeSchema(schemaKeyRef?: object | string | RegExp | boolean): Ajv;
|
||||||
|
/**
|
||||||
|
* Add custom format
|
||||||
|
* @param {string} name format name
|
||||||
|
* @param {string|RegExp|Function} format string is converted to RegExp; function should return boolean (true when valid)
|
||||||
|
* @return {Ajv} this for method chaining
|
||||||
|
*/
|
||||||
|
addFormat(name: string, format: FormatValidator | FormatDefinition): Ajv;
|
||||||
|
/**
|
||||||
|
* Define custom keyword
|
||||||
|
* @this Ajv
|
||||||
|
* @param {string} keyword custom keyword, should be a valid identifier, should be different from all standard, custom and macro keywords.
|
||||||
|
* @param {object} definition keyword definition object with properties `type` (type(s) which the keyword applies to), `validate` or `compile`.
|
||||||
|
* @return {Ajv} this for method chaining
|
||||||
|
*/
|
||||||
|
addKeyword(keyword: string, definition: KeywordDefinition): Ajv;
|
||||||
|
/**
|
||||||
|
* Get keyword definition
|
||||||
|
* @this Ajv
|
||||||
|
* @param {string} keyword pre-defined or custom keyword.
|
||||||
|
* @return {object|Boolean} custom keyword definition, `true` if it is a predefined keyword, `false` otherwise.
|
||||||
|
*/
|
||||||
|
getKeyword(keyword: string): object | boolean;
|
||||||
|
/**
|
||||||
|
* Remove keyword
|
||||||
|
* @this Ajv
|
||||||
|
* @param {string} keyword pre-defined or custom keyword.
|
||||||
|
* @return {Ajv} this for method chaining
|
||||||
|
*/
|
||||||
|
removeKeyword(keyword: string): Ajv;
|
||||||
|
/**
|
||||||
|
* Validate keyword
|
||||||
|
* @this Ajv
|
||||||
|
* @param {object} definition keyword definition object
|
||||||
|
* @param {boolean} throwError true to throw exception if definition is invalid
|
||||||
|
* @return {boolean} validation result
|
||||||
|
*/
|
||||||
|
validateKeyword(definition: KeywordDefinition, throwError: boolean): boolean;
|
||||||
|
/**
|
||||||
|
* Convert array of error message objects to string
|
||||||
|
* @param {Array<object>} errors optional array of validation errors, if not passed errors from the instance are used.
|
||||||
|
* @param {object} options optional options with properties `separator` and `dataVar`.
|
||||||
|
* @return {string} human readable string with all errors descriptions
|
||||||
|
*/
|
||||||
|
errorsText(errors?: Array<ErrorObject> | null, options?: ErrorsTextOptions): string;
|
||||||
|
errors?: Array<ErrorObject> | null;
|
||||||
|
_opts: Options;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface CustomLogger {
|
||||||
|
log(...args: any[]): any;
|
||||||
|
warn(...args: any[]): any;
|
||||||
|
error(...args: any[]): any;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ValidateFunction {
|
||||||
|
(
|
||||||
|
data: any,
|
||||||
|
dataPath?: string,
|
||||||
|
parentData?: object | Array<any>,
|
||||||
|
parentDataProperty?: string | number,
|
||||||
|
rootData?: object | Array<any>
|
||||||
|
): boolean | PromiseLike<any>;
|
||||||
|
schema?: object | boolean;
|
||||||
|
errors?: null | Array<ErrorObject>;
|
||||||
|
refs?: object;
|
||||||
|
refVal?: Array<any>;
|
||||||
|
root?: ValidateFunction | object;
|
||||||
|
$async?: true;
|
||||||
|
source?: object;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Options {
|
||||||
|
$data?: boolean;
|
||||||
|
allErrors?: boolean;
|
||||||
|
verbose?: boolean;
|
||||||
|
jsonPointers?: boolean;
|
||||||
|
uniqueItems?: boolean;
|
||||||
|
unicode?: boolean;
|
||||||
|
format?: false | string;
|
||||||
|
formats?: object;
|
||||||
|
keywords?: object;
|
||||||
|
unknownFormats?: true | string[] | 'ignore';
|
||||||
|
schemas?: Array<object> | object;
|
||||||
|
schemaId?: '$id' | 'id' | 'auto';
|
||||||
|
missingRefs?: true | 'ignore' | 'fail';
|
||||||
|
extendRefs?: true | 'ignore' | 'fail';
|
||||||
|
loadSchema?: (uri: string, cb?: (err: Error, schema: object) => void) => PromiseLike<object | boolean>;
|
||||||
|
removeAdditional?: boolean | 'all' | 'failing';
|
||||||
|
useDefaults?: boolean | 'empty' | 'shared';
|
||||||
|
coerceTypes?: boolean | 'array';
|
||||||
|
strictDefaults?: boolean | 'log';
|
||||||
|
strictKeywords?: boolean | 'log';
|
||||||
|
strictNumbers?: boolean;
|
||||||
|
async?: boolean | string;
|
||||||
|
transpile?: string | ((code: string) => string);
|
||||||
|
meta?: boolean | object;
|
||||||
|
validateSchema?: boolean | 'log';
|
||||||
|
addUsedSchema?: boolean;
|
||||||
|
inlineRefs?: boolean | number;
|
||||||
|
passContext?: boolean;
|
||||||
|
loopRequired?: number;
|
||||||
|
ownProperties?: boolean;
|
||||||
|
multipleOfPrecision?: boolean | number;
|
||||||
|
errorDataPath?: string,
|
||||||
|
messages?: boolean;
|
||||||
|
sourceCode?: boolean;
|
||||||
|
processCode?: (code: string, schema: object) => string;
|
||||||
|
cache?: object;
|
||||||
|
logger?: CustomLogger | false;
|
||||||
|
nullable?: boolean;
|
||||||
|
serialize?: ((schema: object | boolean) => any) | false;
|
||||||
|
}
|
||||||
|
|
||||||
|
type FormatValidator = string | RegExp | ((data: string) => boolean | PromiseLike<any>);
|
||||||
|
type NumberFormatValidator = ((data: number) => boolean | PromiseLike<any>);
|
||||||
|
|
||||||
|
interface NumberFormatDefinition {
|
||||||
|
type: "number",
|
||||||
|
validate: NumberFormatValidator;
|
||||||
|
compare?: (data1: number, data2: number) => number;
|
||||||
|
async?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface StringFormatDefinition {
|
||||||
|
type?: "string",
|
||||||
|
validate: FormatValidator;
|
||||||
|
compare?: (data1: string, data2: string) => number;
|
||||||
|
async?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
type FormatDefinition = NumberFormatDefinition | StringFormatDefinition;
|
||||||
|
|
||||||
|
interface KeywordDefinition {
|
||||||
|
type?: string | Array<string>;
|
||||||
|
async?: boolean;
|
||||||
|
$data?: boolean;
|
||||||
|
errors?: boolean | string;
|
||||||
|
metaSchema?: object;
|
||||||
|
// schema: false makes validate not to expect schema (ValidateFunction)
|
||||||
|
schema?: boolean;
|
||||||
|
statements?: boolean;
|
||||||
|
dependencies?: Array<string>;
|
||||||
|
modifying?: boolean;
|
||||||
|
valid?: boolean;
|
||||||
|
// one and only one of the following properties should be present
|
||||||
|
validate?: SchemaValidateFunction | ValidateFunction;
|
||||||
|
compile?: (schema: any, parentSchema: object, it: CompilationContext) => ValidateFunction;
|
||||||
|
macro?: (schema: any, parentSchema: object, it: CompilationContext) => object | boolean;
|
||||||
|
inline?: (it: CompilationContext, keyword: string, schema: any, parentSchema: object) => string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface CompilationContext {
|
||||||
|
level: number;
|
||||||
|
dataLevel: number;
|
||||||
|
dataPathArr: string[];
|
||||||
|
schema: any;
|
||||||
|
schemaPath: string;
|
||||||
|
baseId: string;
|
||||||
|
async: boolean;
|
||||||
|
opts: Options;
|
||||||
|
formats: {
|
||||||
|
[index: string]: FormatDefinition | undefined;
|
||||||
|
};
|
||||||
|
keywords: {
|
||||||
|
[index: string]: KeywordDefinition | undefined;
|
||||||
|
};
|
||||||
|
compositeRule: boolean;
|
||||||
|
validate: (schema: object) => boolean;
|
||||||
|
util: {
|
||||||
|
copy(obj: any, target?: any): any;
|
||||||
|
toHash(source: string[]): { [index: string]: true | undefined };
|
||||||
|
equal(obj: any, target: any): boolean;
|
||||||
|
getProperty(str: string): string;
|
||||||
|
schemaHasRules(schema: object, rules: any): string;
|
||||||
|
escapeQuotes(str: string): string;
|
||||||
|
toQuotedString(str: string): string;
|
||||||
|
getData(jsonPointer: string, dataLevel: number, paths: string[]): string;
|
||||||
|
escapeJsonPointer(str: string): string;
|
||||||
|
unescapeJsonPointer(str: string): string;
|
||||||
|
escapeFragment(str: string): string;
|
||||||
|
unescapeFragment(str: string): string;
|
||||||
|
};
|
||||||
|
self: Ajv;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface SchemaValidateFunction {
|
||||||
|
(
|
||||||
|
schema: any,
|
||||||
|
data: any,
|
||||||
|
parentSchema?: object,
|
||||||
|
dataPath?: string,
|
||||||
|
parentData?: object | Array<any>,
|
||||||
|
parentDataProperty?: string | number,
|
||||||
|
rootData?: object | Array<any>
|
||||||
|
): boolean | PromiseLike<any>;
|
||||||
|
errors?: Array<ErrorObject>;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ErrorsTextOptions {
|
||||||
|
separator?: string;
|
||||||
|
dataVar?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ErrorObject {
|
||||||
|
keyword: string;
|
||||||
|
dataPath: string;
|
||||||
|
schemaPath: string;
|
||||||
|
params: ErrorParameters;
|
||||||
|
// Added to validation errors of propertyNames keyword schema
|
||||||
|
propertyName?: string;
|
||||||
|
// Excluded if messages set to false.
|
||||||
|
message?: string;
|
||||||
|
// These are added with the `verbose` option.
|
||||||
|
schema?: any;
|
||||||
|
parentSchema?: object;
|
||||||
|
data?: any;
|
||||||
|
}
|
||||||
|
|
||||||
|
type ErrorParameters = RefParams | LimitParams | AdditionalPropertiesParams |
|
||||||
|
DependenciesParams | FormatParams | ComparisonParams |
|
||||||
|
MultipleOfParams | PatternParams | RequiredParams |
|
||||||
|
TypeParams | UniqueItemsParams | CustomParams |
|
||||||
|
PatternRequiredParams | PropertyNamesParams |
|
||||||
|
IfParams | SwitchParams | NoParams | EnumParams;
|
||||||
|
|
||||||
|
interface RefParams {
|
||||||
|
ref: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface LimitParams {
|
||||||
|
limit: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface AdditionalPropertiesParams {
|
||||||
|
additionalProperty: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface DependenciesParams {
|
||||||
|
property: string;
|
||||||
|
missingProperty: string;
|
||||||
|
depsCount: number;
|
||||||
|
deps: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface FormatParams {
|
||||||
|
format: string
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ComparisonParams {
|
||||||
|
comparison: string;
|
||||||
|
limit: number | string;
|
||||||
|
exclusive: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface MultipleOfParams {
|
||||||
|
multipleOf: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface PatternParams {
|
||||||
|
pattern: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface RequiredParams {
|
||||||
|
missingProperty: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface TypeParams {
|
||||||
|
type: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface UniqueItemsParams {
|
||||||
|
i: number;
|
||||||
|
j: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface CustomParams {
|
||||||
|
keyword: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface PatternRequiredParams {
|
||||||
|
missingPattern: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface PropertyNamesParams {
|
||||||
|
propertyName: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface IfParams {
|
||||||
|
failingKeyword: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface SwitchParams {
|
||||||
|
caseIndex: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface NoParams { }
|
||||||
|
|
||||||
|
interface EnumParams {
|
||||||
|
allowedValues: Array<any>;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export = ajv;
|
|
@ -0,0 +1,506 @@
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
var compileSchema = require('./compile')
|
||||||
|
, resolve = require('./compile/resolve')
|
||||||
|
, Cache = require('./cache')
|
||||||
|
, SchemaObject = require('./compile/schema_obj')
|
||||||
|
, stableStringify = require('fast-json-stable-stringify')
|
||||||
|
, formats = require('./compile/formats')
|
||||||
|
, rules = require('./compile/rules')
|
||||||
|
, $dataMetaSchema = require('./data')
|
||||||
|
, util = require('./compile/util');
|
||||||
|
|
||||||
|
module.exports = Ajv;
|
||||||
|
|
||||||
|
Ajv.prototype.validate = validate;
|
||||||
|
Ajv.prototype.compile = compile;
|
||||||
|
Ajv.prototype.addSchema = addSchema;
|
||||||
|
Ajv.prototype.addMetaSchema = addMetaSchema;
|
||||||
|
Ajv.prototype.validateSchema = validateSchema;
|
||||||
|
Ajv.prototype.getSchema = getSchema;
|
||||||
|
Ajv.prototype.removeSchema = removeSchema;
|
||||||
|
Ajv.prototype.addFormat = addFormat;
|
||||||
|
Ajv.prototype.errorsText = errorsText;
|
||||||
|
|
||||||
|
Ajv.prototype._addSchema = _addSchema;
|
||||||
|
Ajv.prototype._compile = _compile;
|
||||||
|
|
||||||
|
Ajv.prototype.compileAsync = require('./compile/async');
|
||||||
|
var customKeyword = require('./keyword');
|
||||||
|
Ajv.prototype.addKeyword = customKeyword.add;
|
||||||
|
Ajv.prototype.getKeyword = customKeyword.get;
|
||||||
|
Ajv.prototype.removeKeyword = customKeyword.remove;
|
||||||
|
Ajv.prototype.validateKeyword = customKeyword.validate;
|
||||||
|
|
||||||
|
var errorClasses = require('./compile/error_classes');
|
||||||
|
Ajv.ValidationError = errorClasses.Validation;
|
||||||
|
Ajv.MissingRefError = errorClasses.MissingRef;
|
||||||
|
Ajv.$dataMetaSchema = $dataMetaSchema;
|
||||||
|
|
||||||
|
var META_SCHEMA_ID = 'http://json-schema.org/draft-07/schema';
|
||||||
|
|
||||||
|
var META_IGNORE_OPTIONS = [ 'removeAdditional', 'useDefaults', 'coerceTypes', 'strictDefaults' ];
|
||||||
|
var META_SUPPORT_DATA = ['/properties'];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates validator instance.
|
||||||
|
* Usage: `Ajv(opts)`
|
||||||
|
* @param {Object} opts optional options
|
||||||
|
* @return {Object} ajv instance
|
||||||
|
*/
|
||||||
|
function Ajv(opts) {
|
||||||
|
if (!(this instanceof Ajv)) return new Ajv(opts);
|
||||||
|
opts = this._opts = util.copy(opts) || {};
|
||||||
|
setLogger(this);
|
||||||
|
this._schemas = {};
|
||||||
|
this._refs = {};
|
||||||
|
this._fragments = {};
|
||||||
|
this._formats = formats(opts.format);
|
||||||
|
|
||||||
|
this._cache = opts.cache || new Cache;
|
||||||
|
this._loadingSchemas = {};
|
||||||
|
this._compilations = [];
|
||||||
|
this.RULES = rules();
|
||||||
|
this._getId = chooseGetId(opts);
|
||||||
|
|
||||||
|
opts.loopRequired = opts.loopRequired || Infinity;
|
||||||
|
if (opts.errorDataPath == 'property') opts._errorDataPathProperty = true;
|
||||||
|
if (opts.serialize === undefined) opts.serialize = stableStringify;
|
||||||
|
this._metaOpts = getMetaSchemaOptions(this);
|
||||||
|
|
||||||
|
if (opts.formats) addInitialFormats(this);
|
||||||
|
if (opts.keywords) addInitialKeywords(this);
|
||||||
|
addDefaultMetaSchema(this);
|
||||||
|
if (typeof opts.meta == 'object') this.addMetaSchema(opts.meta);
|
||||||
|
if (opts.nullable) this.addKeyword('nullable', {metaSchema: {type: 'boolean'}});
|
||||||
|
addInitialSchemas(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validate data using schema
|
||||||
|
* Schema will be compiled and cached (using serialized JSON as key. [fast-json-stable-stringify](https://github.com/epoberezkin/fast-json-stable-stringify) is used to serialize.
|
||||||
|
* @this Ajv
|
||||||
|
* @param {String|Object} schemaKeyRef key, ref or schema object
|
||||||
|
* @param {Any} data to be validated
|
||||||
|
* @return {Boolean} validation result. Errors from the last validation will be available in `ajv.errors` (and also in compiled schema: `schema.errors`).
|
||||||
|
*/
|
||||||
|
function validate(schemaKeyRef, data) {
|
||||||
|
var v;
|
||||||
|
if (typeof schemaKeyRef == 'string') {
|
||||||
|
v = this.getSchema(schemaKeyRef);
|
||||||
|
if (!v) throw new Error('no schema with key or ref "' + schemaKeyRef + '"');
|
||||||
|
} else {
|
||||||
|
var schemaObj = this._addSchema(schemaKeyRef);
|
||||||
|
v = schemaObj.validate || this._compile(schemaObj);
|
||||||
|
}
|
||||||
|
|
||||||
|
var valid = v(data);
|
||||||
|
if (v.$async !== true) this.errors = v.errors;
|
||||||
|
return valid;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create validating function for passed schema.
|
||||||
|
* @this Ajv
|
||||||
|
* @param {Object} schema schema object
|
||||||
|
* @param {Boolean} _meta true if schema is a meta-schema. Used internally to compile meta schemas of custom keywords.
|
||||||
|
* @return {Function} validating function
|
||||||
|
*/
|
||||||
|
function compile(schema, _meta) {
|
||||||
|
var schemaObj = this._addSchema(schema, undefined, _meta);
|
||||||
|
return schemaObj.validate || this._compile(schemaObj);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Adds schema to the instance.
|
||||||
|
* @this Ajv
|
||||||
|
* @param {Object|Array} schema schema or array of schemas. If array is passed, `key` and other parameters will be ignored.
|
||||||
|
* @param {String} key Optional schema key. Can be passed to `validate` method instead of schema object or id/ref. One schema per instance can have empty `id` and `key`.
|
||||||
|
* @param {Boolean} _skipValidation true to skip schema validation. Used internally, option validateSchema should be used instead.
|
||||||
|
* @param {Boolean} _meta true if schema is a meta-schema. Used internally, addMetaSchema should be used instead.
|
||||||
|
* @return {Ajv} this for method chaining
|
||||||
|
*/
|
||||||
|
function addSchema(schema, key, _skipValidation, _meta) {
|
||||||
|
if (Array.isArray(schema)){
|
||||||
|
for (var i=0; i<schema.length; i++) this.addSchema(schema[i], undefined, _skipValidation, _meta);
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
var id = this._getId(schema);
|
||||||
|
if (id !== undefined && typeof id != 'string')
|
||||||
|
throw new Error('schema id must be string');
|
||||||
|
key = resolve.normalizeId(key || id);
|
||||||
|
checkUnique(this, key);
|
||||||
|
this._schemas[key] = this._addSchema(schema, _skipValidation, _meta, true);
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add schema that will be used to validate other schemas
|
||||||
|
* options in META_IGNORE_OPTIONS are alway set to false
|
||||||
|
* @this Ajv
|
||||||
|
* @param {Object} schema schema object
|
||||||
|
* @param {String} key optional schema key
|
||||||
|
* @param {Boolean} skipValidation true to skip schema validation, can be used to override validateSchema option for meta-schema
|
||||||
|
* @return {Ajv} this for method chaining
|
||||||
|
*/
|
||||||
|
function addMetaSchema(schema, key, skipValidation) {
|
||||||
|
this.addSchema(schema, key, skipValidation, true);
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validate schema
|
||||||
|
* @this Ajv
|
||||||
|
* @param {Object} schema schema to validate
|
||||||
|
* @param {Boolean} throwOrLogError pass true to throw (or log) an error if invalid
|
||||||
|
* @return {Boolean} true if schema is valid
|
||||||
|
*/
|
||||||
|
function validateSchema(schema, throwOrLogError) {
|
||||||
|
var $schema = schema.$schema;
|
||||||
|
if ($schema !== undefined && typeof $schema != 'string')
|
||||||
|
throw new Error('$schema must be a string');
|
||||||
|
$schema = $schema || this._opts.defaultMeta || defaultMeta(this);
|
||||||
|
if (!$schema) {
|
||||||
|
this.logger.warn('meta-schema not available');
|
||||||
|
this.errors = null;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
var valid = this.validate($schema, schema);
|
||||||
|
if (!valid && throwOrLogError) {
|
||||||
|
var message = 'schema is invalid: ' + this.errorsText();
|
||||||
|
if (this._opts.validateSchema == 'log') this.logger.error(message);
|
||||||
|
else throw new Error(message);
|
||||||
|
}
|
||||||
|
return valid;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
function defaultMeta(self) {
|
||||||
|
var meta = self._opts.meta;
|
||||||
|
self._opts.defaultMeta = typeof meta == 'object'
|
||||||
|
? self._getId(meta) || meta
|
||||||
|
: self.getSchema(META_SCHEMA_ID)
|
||||||
|
? META_SCHEMA_ID
|
||||||
|
: undefined;
|
||||||
|
return self._opts.defaultMeta;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get compiled schema from the instance by `key` or `ref`.
|
||||||
|
* @this Ajv
|
||||||
|
* @param {String} keyRef `key` that was passed to `addSchema` or full schema reference (`schema.id` or resolved id).
|
||||||
|
* @return {Function} schema validating function (with property `schema`).
|
||||||
|
*/
|
||||||
|
function getSchema(keyRef) {
|
||||||
|
var schemaObj = _getSchemaObj(this, keyRef);
|
||||||
|
switch (typeof schemaObj) {
|
||||||
|
case 'object': return schemaObj.validate || this._compile(schemaObj);
|
||||||
|
case 'string': return this.getSchema(schemaObj);
|
||||||
|
case 'undefined': return _getSchemaFragment(this, keyRef);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
function _getSchemaFragment(self, ref) {
|
||||||
|
var res = resolve.schema.call(self, { schema: {} }, ref);
|
||||||
|
if (res) {
|
||||||
|
var schema = res.schema
|
||||||
|
, root = res.root
|
||||||
|
, baseId = res.baseId;
|
||||||
|
var v = compileSchema.call(self, schema, root, undefined, baseId);
|
||||||
|
self._fragments[ref] = new SchemaObject({
|
||||||
|
ref: ref,
|
||||||
|
fragment: true,
|
||||||
|
schema: schema,
|
||||||
|
root: root,
|
||||||
|
baseId: baseId,
|
||||||
|
validate: v
|
||||||
|
});
|
||||||
|
return v;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
function _getSchemaObj(self, keyRef) {
|
||||||
|
keyRef = resolve.normalizeId(keyRef);
|
||||||
|
return self._schemas[keyRef] || self._refs[keyRef] || self._fragments[keyRef];
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Remove cached schema(s).
|
||||||
|
* If no parameter is passed all schemas but meta-schemas are removed.
|
||||||
|
* If RegExp is passed all schemas with key/id matching pattern but meta-schemas are removed.
|
||||||
|
* Even if schema is referenced by other schemas it still can be removed as other schemas have local references.
|
||||||
|
* @this Ajv
|
||||||
|
* @param {String|Object|RegExp} schemaKeyRef key, ref, pattern to match key/ref or schema object
|
||||||
|
* @return {Ajv} this for method chaining
|
||||||
|
*/
|
||||||
|
function removeSchema(schemaKeyRef) {
|
||||||
|
if (schemaKeyRef instanceof RegExp) {
|
||||||
|
_removeAllSchemas(this, this._schemas, schemaKeyRef);
|
||||||
|
_removeAllSchemas(this, this._refs, schemaKeyRef);
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
switch (typeof schemaKeyRef) {
|
||||||
|
case 'undefined':
|
||||||
|
_removeAllSchemas(this, this._schemas);
|
||||||
|
_removeAllSchemas(this, this._refs);
|
||||||
|
this._cache.clear();
|
||||||
|
return this;
|
||||||
|
case 'string':
|
||||||
|
var schemaObj = _getSchemaObj(this, schemaKeyRef);
|
||||||
|
if (schemaObj) this._cache.del(schemaObj.cacheKey);
|
||||||
|
delete this._schemas[schemaKeyRef];
|
||||||
|
delete this._refs[schemaKeyRef];
|
||||||
|
return this;
|
||||||
|
case 'object':
|
||||||
|
var serialize = this._opts.serialize;
|
||||||
|
var cacheKey = serialize ? serialize(schemaKeyRef) : schemaKeyRef;
|
||||||
|
this._cache.del(cacheKey);
|
||||||
|
var id = this._getId(schemaKeyRef);
|
||||||
|
if (id) {
|
||||||
|
id = resolve.normalizeId(id);
|
||||||
|
delete this._schemas[id];
|
||||||
|
delete this._refs[id];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
function _removeAllSchemas(self, schemas, regex) {
|
||||||
|
for (var keyRef in schemas) {
|
||||||
|
var schemaObj = schemas[keyRef];
|
||||||
|
if (!schemaObj.meta && (!regex || regex.test(keyRef))) {
|
||||||
|
self._cache.del(schemaObj.cacheKey);
|
||||||
|
delete schemas[keyRef];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/* @this Ajv */
|
||||||
|
function _addSchema(schema, skipValidation, meta, shouldAddSchema) {
|
||||||
|
if (typeof schema != 'object' && typeof schema != 'boolean')
|
||||||
|
throw new Error('schema should be object or boolean');
|
||||||
|
var serialize = this._opts.serialize;
|
||||||
|
var cacheKey = serialize ? serialize(schema) : schema;
|
||||||
|
var cached = this._cache.get(cacheKey);
|
||||||
|
if (cached) return cached;
|
||||||
|
|
||||||
|
shouldAddSchema = shouldAddSchema || this._opts.addUsedSchema !== false;
|
||||||
|
|
||||||
|
var id = resolve.normalizeId(this._getId(schema));
|
||||||
|
if (id && shouldAddSchema) checkUnique(this, id);
|
||||||
|
|
||||||
|
var willValidate = this._opts.validateSchema !== false && !skipValidation;
|
||||||
|
var recursiveMeta;
|
||||||
|
if (willValidate && !(recursiveMeta = id && id == resolve.normalizeId(schema.$schema)))
|
||||||
|
this.validateSchema(schema, true);
|
||||||
|
|
||||||
|
var localRefs = resolve.ids.call(this, schema);
|
||||||
|
|
||||||
|
var schemaObj = new SchemaObject({
|
||||||
|
id: id,
|
||||||
|
schema: schema,
|
||||||
|
localRefs: localRefs,
|
||||||
|
cacheKey: cacheKey,
|
||||||
|
meta: meta
|
||||||
|
});
|
||||||
|
|
||||||
|
if (id[0] != '#' && shouldAddSchema) this._refs[id] = schemaObj;
|
||||||
|
this._cache.put(cacheKey, schemaObj);
|
||||||
|
|
||||||
|
if (willValidate && recursiveMeta) this.validateSchema(schema, true);
|
||||||
|
|
||||||
|
return schemaObj;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/* @this Ajv */
|
||||||
|
function _compile(schemaObj, root) {
|
||||||
|
if (schemaObj.compiling) {
|
||||||
|
schemaObj.validate = callValidate;
|
||||||
|
callValidate.schema = schemaObj.schema;
|
||||||
|
callValidate.errors = null;
|
||||||
|
callValidate.root = root ? root : callValidate;
|
||||||
|
if (schemaObj.schema.$async === true)
|
||||||
|
callValidate.$async = true;
|
||||||
|
return callValidate;
|
||||||
|
}
|
||||||
|
schemaObj.compiling = true;
|
||||||
|
|
||||||
|
var currentOpts;
|
||||||
|
if (schemaObj.meta) {
|
||||||
|
currentOpts = this._opts;
|
||||||
|
this._opts = this._metaOpts;
|
||||||
|
}
|
||||||
|
|
||||||
|
var v;
|
||||||
|
try { v = compileSchema.call(this, schemaObj.schema, root, schemaObj.localRefs); }
|
||||||
|
catch(e) {
|
||||||
|
delete schemaObj.validate;
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
finally {
|
||||||
|
schemaObj.compiling = false;
|
||||||
|
if (schemaObj.meta) this._opts = currentOpts;
|
||||||
|
}
|
||||||
|
|
||||||
|
schemaObj.validate = v;
|
||||||
|
schemaObj.refs = v.refs;
|
||||||
|
schemaObj.refVal = v.refVal;
|
||||||
|
schemaObj.root = v.root;
|
||||||
|
return v;
|
||||||
|
|
||||||
|
|
||||||
|
/* @this {*} - custom context, see passContext option */
|
||||||
|
function callValidate() {
|
||||||
|
/* jshint validthis: true */
|
||||||
|
var _validate = schemaObj.validate;
|
||||||
|
var result = _validate.apply(this, arguments);
|
||||||
|
callValidate.errors = _validate.errors;
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
function chooseGetId(opts) {
|
||||||
|
switch (opts.schemaId) {
|
||||||
|
case 'auto': return _get$IdOrId;
|
||||||
|
case 'id': return _getId;
|
||||||
|
default: return _get$Id;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* @this Ajv */
|
||||||
|
function _getId(schema) {
|
||||||
|
if (schema.$id) this.logger.warn('schema $id ignored', schema.$id);
|
||||||
|
return schema.id;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* @this Ajv */
|
||||||
|
function _get$Id(schema) {
|
||||||
|
if (schema.id) this.logger.warn('schema id ignored', schema.id);
|
||||||
|
return schema.$id;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
function _get$IdOrId(schema) {
|
||||||
|
if (schema.$id && schema.id && schema.$id != schema.id)
|
||||||
|
throw new Error('schema $id is different from id');
|
||||||
|
return schema.$id || schema.id;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Convert array of error message objects to string
|
||||||
|
* @this Ajv
|
||||||
|
* @param {Array<Object>} errors optional array of validation errors, if not passed errors from the instance are used.
|
||||||
|
* @param {Object} options optional options with properties `separator` and `dataVar`.
|
||||||
|
* @return {String} human readable string with all errors descriptions
|
||||||
|
*/
|
||||||
|
function errorsText(errors, options) {
|
||||||
|
errors = errors || this.errors;
|
||||||
|
if (!errors) return 'No errors';
|
||||||
|
options = options || {};
|
||||||
|
var separator = options.separator === undefined ? ', ' : options.separator;
|
||||||
|
var dataVar = options.dataVar === undefined ? 'data' : options.dataVar;
|
||||||
|
|
||||||
|
var text = '';
|
||||||
|
for (var i=0; i<errors.length; i++) {
|
||||||
|
var e = errors[i];
|
||||||
|
if (e) text += dataVar + e.dataPath + ' ' + e.message + separator;
|
||||||
|
}
|
||||||
|
return text.slice(0, -separator.length);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add custom format
|
||||||
|
* @this Ajv
|
||||||
|
* @param {String} name format name
|
||||||
|
* @param {String|RegExp|Function} format string is converted to RegExp; function should return boolean (true when valid)
|
||||||
|
* @return {Ajv} this for method chaining
|
||||||
|
*/
|
||||||
|
function addFormat(name, format) {
|
||||||
|
if (typeof format == 'string') format = new RegExp(format);
|
||||||
|
this._formats[name] = format;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
function addDefaultMetaSchema(self) {
|
||||||
|
var $dataSchema;
|
||||||
|
if (self._opts.$data) {
|
||||||
|
$dataSchema = require('./refs/data.json');
|
||||||
|
self.addMetaSchema($dataSchema, $dataSchema.$id, true);
|
||||||
|
}
|
||||||
|
if (self._opts.meta === false) return;
|
||||||
|
var metaSchema = require('./refs/json-schema-draft-07.json');
|
||||||
|
if (self._opts.$data) metaSchema = $dataMetaSchema(metaSchema, META_SUPPORT_DATA);
|
||||||
|
self.addMetaSchema(metaSchema, META_SCHEMA_ID, true);
|
||||||
|
self._refs['http://json-schema.org/schema'] = META_SCHEMA_ID;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
function addInitialSchemas(self) {
|
||||||
|
var optsSchemas = self._opts.schemas;
|
||||||
|
if (!optsSchemas) return;
|
||||||
|
if (Array.isArray(optsSchemas)) self.addSchema(optsSchemas);
|
||||||
|
else for (var key in optsSchemas) self.addSchema(optsSchemas[key], key);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
function addInitialFormats(self) {
|
||||||
|
for (var name in self._opts.formats) {
|
||||||
|
var format = self._opts.formats[name];
|
||||||
|
self.addFormat(name, format);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
function addInitialKeywords(self) {
|
||||||
|
for (var name in self._opts.keywords) {
|
||||||
|
var keyword = self._opts.keywords[name];
|
||||||
|
self.addKeyword(name, keyword);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
function checkUnique(self, id) {
|
||||||
|
if (self._schemas[id] || self._refs[id])
|
||||||
|
throw new Error('schema with key or id "' + id + '" already exists');
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
function getMetaSchemaOptions(self) {
|
||||||
|
var metaOpts = util.copy(self._opts);
|
||||||
|
for (var i=0; i<META_IGNORE_OPTIONS.length; i++)
|
||||||
|
delete metaOpts[META_IGNORE_OPTIONS[i]];
|
||||||
|
return metaOpts;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
function setLogger(self) {
|
||||||
|
var logger = self._opts.logger;
|
||||||
|
if (logger === false) {
|
||||||
|
self.logger = {log: noop, warn: noop, error: noop};
|
||||||
|
} else {
|
||||||
|
if (logger === undefined) logger = console;
|
||||||
|
if (!(typeof logger == 'object' && logger.log && logger.warn && logger.error))
|
||||||
|
throw new Error('logger must implement log, warn and error methods');
|
||||||
|
self.logger = logger;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
function noop() {}
|
|
@ -0,0 +1,26 @@
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
|
||||||
|
var Cache = module.exports = function Cache() {
|
||||||
|
this._cache = {};
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
Cache.prototype.put = function Cache_put(key, value) {
|
||||||
|
this._cache[key] = value;
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
Cache.prototype.get = function Cache_get(key) {
|
||||||
|
return this._cache[key];
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
Cache.prototype.del = function Cache_del(key) {
|
||||||
|
delete this._cache[key];
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
Cache.prototype.clear = function Cache_clear() {
|
||||||
|
this._cache = {};
|
||||||
|
};
|
|
@ -0,0 +1,90 @@
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
var MissingRefError = require('./error_classes').MissingRef;
|
||||||
|
|
||||||
|
module.exports = compileAsync;
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates validating function for passed schema with asynchronous loading of missing schemas.
|
||||||
|
* `loadSchema` option should be a function that accepts schema uri and returns promise that resolves with the schema.
|
||||||
|
* @this Ajv
|
||||||
|
* @param {Object} schema schema object
|
||||||
|
* @param {Boolean} meta optional true to compile meta-schema; this parameter can be skipped
|
||||||
|
* @param {Function} callback an optional node-style callback, it is called with 2 parameters: error (or null) and validating function.
|
||||||
|
* @return {Promise} promise that resolves with a validating function.
|
||||||
|
*/
|
||||||
|
function compileAsync(schema, meta, callback) {
|
||||||
|
/* eslint no-shadow: 0 */
|
||||||
|
/* global Promise */
|
||||||
|
/* jshint validthis: true */
|
||||||
|
var self = this;
|
||||||
|
if (typeof this._opts.loadSchema != 'function')
|
||||||
|
throw new Error('options.loadSchema should be a function');
|
||||||
|
|
||||||
|
if (typeof meta == 'function') {
|
||||||
|
callback = meta;
|
||||||
|
meta = undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
var p = loadMetaSchemaOf(schema).then(function () {
|
||||||
|
var schemaObj = self._addSchema(schema, undefined, meta);
|
||||||
|
return schemaObj.validate || _compileAsync(schemaObj);
|
||||||
|
});
|
||||||
|
|
||||||
|
if (callback) {
|
||||||
|
p.then(
|
||||||
|
function(v) { callback(null, v); },
|
||||||
|
callback
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return p;
|
||||||
|
|
||||||
|
|
||||||
|
function loadMetaSchemaOf(sch) {
|
||||||
|
var $schema = sch.$schema;
|
||||||
|
return $schema && !self.getSchema($schema)
|
||||||
|
? compileAsync.call(self, { $ref: $schema }, true)
|
||||||
|
: Promise.resolve();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
function _compileAsync(schemaObj) {
|
||||||
|
try { return self._compile(schemaObj); }
|
||||||
|
catch(e) {
|
||||||
|
if (e instanceof MissingRefError) return loadMissingSchema(e);
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
function loadMissingSchema(e) {
|
||||||
|
var ref = e.missingSchema;
|
||||||
|
if (added(ref)) throw new Error('Schema ' + ref + ' is loaded but ' + e.missingRef + ' cannot be resolved');
|
||||||
|
|
||||||
|
var schemaPromise = self._loadingSchemas[ref];
|
||||||
|
if (!schemaPromise) {
|
||||||
|
schemaPromise = self._loadingSchemas[ref] = self._opts.loadSchema(ref);
|
||||||
|
schemaPromise.then(removePromise, removePromise);
|
||||||
|
}
|
||||||
|
|
||||||
|
return schemaPromise.then(function (sch) {
|
||||||
|
if (!added(ref)) {
|
||||||
|
return loadMetaSchemaOf(sch).then(function () {
|
||||||
|
if (!added(ref)) self.addSchema(sch, ref, undefined, meta);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}).then(function() {
|
||||||
|
return _compileAsync(schemaObj);
|
||||||
|
});
|
||||||
|
|
||||||
|
function removePromise() {
|
||||||
|
delete self._loadingSchemas[ref];
|
||||||
|
}
|
||||||
|
|
||||||
|
function added(ref) {
|
||||||
|
return self._refs[ref] || self._schemas[ref];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,5 @@
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
// do NOT remove this file - it would break pre-compiled schemas
|
||||||
|
// https://github.com/ajv-validator/ajv/issues/889
|
||||||
|
module.exports = require('fast-deep-equal');
|
|
@ -0,0 +1,34 @@
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
var resolve = require('./resolve');
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
Validation: errorSubclass(ValidationError),
|
||||||
|
MissingRef: errorSubclass(MissingRefError)
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
function ValidationError(errors) {
|
||||||
|
this.message = 'validation failed';
|
||||||
|
this.errors = errors;
|
||||||
|
this.ajv = this.validation = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
MissingRefError.message = function (baseId, ref) {
|
||||||
|
return 'can\'t resolve reference ' + ref + ' from id ' + baseId;
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
function MissingRefError(baseId, ref, message) {
|
||||||
|
this.message = message || MissingRefError.message(baseId, ref);
|
||||||
|
this.missingRef = resolve.url(baseId, ref);
|
||||||
|
this.missingSchema = resolve.normalizeId(resolve.fullPath(this.missingRef));
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
function errorSubclass(Subclass) {
|
||||||
|
Subclass.prototype = Object.create(Error.prototype);
|
||||||
|
Subclass.prototype.constructor = Subclass;
|
||||||
|
return Subclass;
|
||||||
|
}
|
|
@ -0,0 +1,142 @@
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
var util = require('./util');
|
||||||
|
|
||||||
|
var DATE = /^(\d\d\d\d)-(\d\d)-(\d\d)$/;
|
||||||
|
var DAYS = [0,31,28,31,30,31,30,31,31,30,31,30,31];
|
||||||
|
var TIME = /^(\d\d):(\d\d):(\d\d)(\.\d+)?(z|[+-]\d\d(?::?\d\d)?)?$/i;
|
||||||
|
var HOSTNAME = /^(?=.{1,253}\.?$)[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?(?:\.[a-z0-9](?:[-0-9a-z]{0,61}[0-9a-z])?)*\.?$/i;
|
||||||
|
var URI = /^(?:[a-z][a-z0-9+\-.]*:)(?:\/?\/(?:(?:[a-z0-9\-._~!$&'()*+,;=:]|%[0-9a-f]{2})*@)?(?:\[(?:(?:(?:(?:[0-9a-f]{1,4}:){6}|::(?:[0-9a-f]{1,4}:){5}|(?:[0-9a-f]{1,4})?::(?:[0-9a-f]{1,4}:){4}|(?:(?:[0-9a-f]{1,4}:){0,1}[0-9a-f]{1,4})?::(?:[0-9a-f]{1,4}:){3}|(?:(?:[0-9a-f]{1,4}:){0,2}[0-9a-f]{1,4})?::(?:[0-9a-f]{1,4}:){2}|(?:(?:[0-9a-f]{1,4}:){0,3}[0-9a-f]{1,4})?::[0-9a-f]{1,4}:|(?:(?:[0-9a-f]{1,4}:){0,4}[0-9a-f]{1,4})?::)(?:[0-9a-f]{1,4}:[0-9a-f]{1,4}|(?:(?:25[0-5]|2[0-4]\d|[01]?\d\d?)\.){3}(?:25[0-5]|2[0-4]\d|[01]?\d\d?))|(?:(?:[0-9a-f]{1,4}:){0,5}[0-9a-f]{1,4})?::[0-9a-f]{1,4}|(?:(?:[0-9a-f]{1,4}:){0,6}[0-9a-f]{1,4})?::)|[Vv][0-9a-f]+\.[a-z0-9\-._~!$&'()*+,;=:]+)\]|(?:(?:25[0-5]|2[0-4]\d|[01]?\d\d?)\.){3}(?:25[0-5]|2[0-4]\d|[01]?\d\d?)|(?:[a-z0-9\-._~!$&'()*+,;=]|%[0-9a-f]{2})*)(?::\d*)?(?:\/(?:[a-z0-9\-._~!$&'()*+,;=:@]|%[0-9a-f]{2})*)*|\/(?:(?:[a-z0-9\-._~!$&'()*+,;=:@]|%[0-9a-f]{2})+(?:\/(?:[a-z0-9\-._~!$&'()*+,;=:@]|%[0-9a-f]{2})*)*)?|(?:[a-z0-9\-._~!$&'()*+,;=:@]|%[0-9a-f]{2})+(?:\/(?:[a-z0-9\-._~!$&'()*+,;=:@]|%[0-9a-f]{2})*)*)(?:\?(?:[a-z0-9\-._~!$&'()*+,;=:@/?]|%[0-9a-f]{2})*)?(?:#(?:[a-z0-9\-._~!$&'()*+,;=:@/?]|%[0-9a-f]{2})*)?$/i;
|
||||||
|
var URIREF = /^(?:[a-z][a-z0-9+\-.]*:)?(?:\/?\/(?:(?:[a-z0-9\-._~!$&'()*+,;=:]|%[0-9a-f]{2})*@)?(?:\[(?:(?:(?:(?:[0-9a-f]{1,4}:){6}|::(?:[0-9a-f]{1,4}:){5}|(?:[0-9a-f]{1,4})?::(?:[0-9a-f]{1,4}:){4}|(?:(?:[0-9a-f]{1,4}:){0,1}[0-9a-f]{1,4})?::(?:[0-9a-f]{1,4}:){3}|(?:(?:[0-9a-f]{1,4}:){0,2}[0-9a-f]{1,4})?::(?:[0-9a-f]{1,4}:){2}|(?:(?:[0-9a-f]{1,4}:){0,3}[0-9a-f]{1,4})?::[0-9a-f]{1,4}:|(?:(?:[0-9a-f]{1,4}:){0,4}[0-9a-f]{1,4})?::)(?:[0-9a-f]{1,4}:[0-9a-f]{1,4}|(?:(?:25[0-5]|2[0-4]\d|[01]?\d\d?)\.){3}(?:25[0-5]|2[0-4]\d|[01]?\d\d?))|(?:(?:[0-9a-f]{1,4}:){0,5}[0-9a-f]{1,4})?::[0-9a-f]{1,4}|(?:(?:[0-9a-f]{1,4}:){0,6}[0-9a-f]{1,4})?::)|[Vv][0-9a-f]+\.[a-z0-9\-._~!$&'()*+,;=:]+)\]|(?:(?:25[0-5]|2[0-4]\d|[01]?\d\d?)\.){3}(?:25[0-5]|2[0-4]\d|[01]?\d\d?)|(?:[a-z0-9\-._~!$&'"()*+,;=]|%[0-9a-f]{2})*)(?::\d*)?(?:\/(?:[a-z0-9\-._~!$&'"()*+,;=:@]|%[0-9a-f]{2})*)*|\/(?:(?:[a-z0-9\-._~!$&'"()*+,;=:@]|%[0-9a-f]{2})+(?:\/(?:[a-z0-9\-._~!$&'"()*+,;=:@]|%[0-9a-f]{2})*)*)?|(?:[a-z0-9\-._~!$&'"()*+,;=:@]|%[0-9a-f]{2})+(?:\/(?:[a-z0-9\-._~!$&'"()*+,;=:@]|%[0-9a-f]{2})*)*)?(?:\?(?:[a-z0-9\-._~!$&'"()*+,;=:@/?]|%[0-9a-f]{2})*)?(?:#(?:[a-z0-9\-._~!$&'"()*+,;=:@/?]|%[0-9a-f]{2})*)?$/i;
|
||||||
|
// uri-template: https://tools.ietf.org/html/rfc6570
|
||||||
|
var URITEMPLATE = /^(?:(?:[^\x00-\x20"'<>%\\^`{|}]|%[0-9a-f]{2})|\{[+#./;?&=,!@|]?(?:[a-z0-9_]|%[0-9a-f]{2})+(?::[1-9][0-9]{0,3}|\*)?(?:,(?:[a-z0-9_]|%[0-9a-f]{2})+(?::[1-9][0-9]{0,3}|\*)?)*\})*$/i;
|
||||||
|
// For the source: https://gist.github.com/dperini/729294
|
||||||
|
// For test cases: https://mathiasbynens.be/demo/url-regex
|
||||||
|
// @todo Delete current URL in favour of the commented out URL rule when this issue is fixed https://github.com/eslint/eslint/issues/7983.
|
||||||
|
// var URL = /^(?:(?:https?|ftp):\/\/)(?:\S+(?::\S*)?@)?(?:(?!10(?:\.\d{1,3}){3})(?!127(?:\.\d{1,3}){3})(?!169\.254(?:\.\d{1,3}){2})(?!192\.168(?:\.\d{1,3}){2})(?!172\.(?:1[6-9]|2\d|3[0-1])(?:\.\d{1,3}){2})(?:[1-9]\d?|1\d\d|2[01]\d|22[0-3])(?:\.(?:1?\d{1,2}|2[0-4]\d|25[0-5])){2}(?:\.(?:[1-9]\d?|1\d\d|2[0-4]\d|25[0-4]))|(?:(?:[a-z\u{00a1}-\u{ffff}0-9]+-)*[a-z\u{00a1}-\u{ffff}0-9]+)(?:\.(?:[a-z\u{00a1}-\u{ffff}0-9]+-)*[a-z\u{00a1}-\u{ffff}0-9]+)*(?:\.(?:[a-z\u{00a1}-\u{ffff}]{2,})))(?::\d{2,5})?(?:\/[^\s]*)?$/iu;
|
||||||
|
var URL = /^(?:(?:http[s\u017F]?|ftp):\/\/)(?:(?:[\0-\x08\x0E-\x1F!-\x9F\xA1-\u167F\u1681-\u1FFF\u200B-\u2027\u202A-\u202E\u2030-\u205E\u2060-\u2FFF\u3001-\uD7FF\uE000-\uFEFE\uFF00-\uFFFF]|[\uD800-\uDBFF][\uDC00-\uDFFF]|[\uD800-\uDBFF](?![\uDC00-\uDFFF])|(?:[^\uD800-\uDBFF]|^)[\uDC00-\uDFFF])+(?::(?:[\0-\x08\x0E-\x1F!-\x9F\xA1-\u167F\u1681-\u1FFF\u200B-\u2027\u202A-\u202E\u2030-\u205E\u2060-\u2FFF\u3001-\uD7FF\uE000-\uFEFE\uFF00-\uFFFF]|[\uD800-\uDBFF][\uDC00-\uDFFF]|[\uD800-\uDBFF](?![\uDC00-\uDFFF])|(?:[^\uD800-\uDBFF]|^)[\uDC00-\uDFFF])*)?@)?(?:(?!10(?:\.[0-9]{1,3}){3})(?!127(?:\.[0-9]{1,3}){3})(?!169\.254(?:\.[0-9]{1,3}){2})(?!192\.168(?:\.[0-9]{1,3}){2})(?!172\.(?:1[6-9]|2[0-9]|3[01])(?:\.[0-9]{1,3}){2})(?:[1-9][0-9]?|1[0-9][0-9]|2[01][0-9]|22[0-3])(?:\.(?:1?[0-9]{1,2}|2[0-4][0-9]|25[0-5])){2}(?:\.(?:[1-9][0-9]?|1[0-9][0-9]|2[0-4][0-9]|25[0-4]))|(?:(?:(?:[0-9a-z\xA1-\uD7FF\uE000-\uFFFF]|[\uD800-\uDBFF](?![\uDC00-\uDFFF])|(?:[^\uD800-\uDBFF]|^)[\uDC00-\uDFFF])+-)*(?:[0-9a-z\xA1-\uD7FF\uE000-\uFFFF]|[\uD800-\uDBFF](?![\uDC00-\uDFFF])|(?:[^\uD800-\uDBFF]|^)[\uDC00-\uDFFF])+)(?:\.(?:(?:[0-9a-z\xA1-\uD7FF\uE000-\uFFFF]|[\uD800-\uDBFF](?![\uDC00-\uDFFF])|(?:[^\uD800-\uDBFF]|^)[\uDC00-\uDFFF])+-)*(?:[0-9a-z\xA1-\uD7FF\uE000-\uFFFF]|[\uD800-\uDBFF](?![\uDC00-\uDFFF])|(?:[^\uD800-\uDBFF]|^)[\uDC00-\uDFFF])+)*(?:\.(?:(?:[a-z\xA1-\uD7FF\uE000-\uFFFF]|[\uD800-\uDBFF](?![\uDC00-\uDFFF])|(?:[^\uD800-\uDBFF]|^)[\uDC00-\uDFFF]){2,})))(?::[0-9]{2,5})?(?:\/(?:[\0-\x08\x0E-\x1F!-\x9F\xA1-\u167F\u1681-\u1FFF\u200B-\u2027\u202A-\u202E\u2030-\u205E\u2060-\u2FFF\u3001-\uD7FF\uE000-\uFEFE\uFF00-\uFFFF]|[\uD800-\uDBFF][\uDC00-\uDFFF]|[\uD800-\uDBFF](?![\uDC00-\uDFFF])|(?:[^\uD800-\uDBFF]|^)[\uDC00-\uDFFF])*)?$/i;
|
||||||
|
var UUID = /^(?:urn:uuid:)?[0-9a-f]{8}-(?:[0-9a-f]{4}-){3}[0-9a-f]{12}$/i;
|
||||||
|
var JSON_POINTER = /^(?:\/(?:[^~/]|~0|~1)*)*$/;
|
||||||
|
var JSON_POINTER_URI_FRAGMENT = /^#(?:\/(?:[a-z0-9_\-.!$&'()*+,;:=@]|%[0-9a-f]{2}|~0|~1)*)*$/i;
|
||||||
|
var RELATIVE_JSON_POINTER = /^(?:0|[1-9][0-9]*)(?:#|(?:\/(?:[^~/]|~0|~1)*)*)$/;
|
||||||
|
|
||||||
|
|
||||||
|
module.exports = formats;
|
||||||
|
|
||||||
|
function formats(mode) {
|
||||||
|
mode = mode == 'full' ? 'full' : 'fast';
|
||||||
|
return util.copy(formats[mode]);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
formats.fast = {
|
||||||
|
// date: http://tools.ietf.org/html/rfc3339#section-5.6
|
||||||
|
date: /^\d\d\d\d-[0-1]\d-[0-3]\d$/,
|
||||||
|
// date-time: http://tools.ietf.org/html/rfc3339#section-5.6
|
||||||
|
time: /^(?:[0-2]\d:[0-5]\d:[0-5]\d|23:59:60)(?:\.\d+)?(?:z|[+-]\d\d(?::?\d\d)?)?$/i,
|
||||||
|
'date-time': /^\d\d\d\d-[0-1]\d-[0-3]\d[t\s](?:[0-2]\d:[0-5]\d:[0-5]\d|23:59:60)(?:\.\d+)?(?:z|[+-]\d\d(?::?\d\d)?)$/i,
|
||||||
|
// uri: https://github.com/mafintosh/is-my-json-valid/blob/master/formats.js
|
||||||
|
uri: /^(?:[a-z][a-z0-9+\-.]*:)(?:\/?\/)?[^\s]*$/i,
|
||||||
|
'uri-reference': /^(?:(?:[a-z][a-z0-9+\-.]*:)?\/?\/)?(?:[^\\\s#][^\s#]*)?(?:#[^\\\s]*)?$/i,
|
||||||
|
'uri-template': URITEMPLATE,
|
||||||
|
url: URL,
|
||||||
|
// email (sources from jsen validator):
|
||||||
|
// http://stackoverflow.com/questions/201323/using-a-regular-expression-to-validate-an-email-address#answer-8829363
|
||||||
|
// http://www.w3.org/TR/html5/forms.html#valid-e-mail-address (search for 'willful violation')
|
||||||
|
email: /^[a-z0-9.!#$%&'*+/=?^_`{|}~-]+@[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?(?:\.[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?)*$/i,
|
||||||
|
hostname: HOSTNAME,
|
||||||
|
// optimized https://www.safaribooksonline.com/library/view/regular-expressions-cookbook/9780596802837/ch07s16.html
|
||||||
|
ipv4: /^(?:(?:25[0-5]|2[0-4]\d|[01]?\d\d?)\.){3}(?:25[0-5]|2[0-4]\d|[01]?\d\d?)$/,
|
||||||
|
// optimized http://stackoverflow.com/questions/53497/regular-expression-that-matches-valid-ipv6-addresses
|
||||||
|
ipv6: /^\s*(?:(?:(?:[0-9a-f]{1,4}:){7}(?:[0-9a-f]{1,4}|:))|(?:(?:[0-9a-f]{1,4}:){6}(?::[0-9a-f]{1,4}|(?:(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(?:\.(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3})|:))|(?:(?:[0-9a-f]{1,4}:){5}(?:(?:(?::[0-9a-f]{1,4}){1,2})|:(?:(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(?:\.(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3})|:))|(?:(?:[0-9a-f]{1,4}:){4}(?:(?:(?::[0-9a-f]{1,4}){1,3})|(?:(?::[0-9a-f]{1,4})?:(?:(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(?:\.(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:))|(?:(?:[0-9a-f]{1,4}:){3}(?:(?:(?::[0-9a-f]{1,4}){1,4})|(?:(?::[0-9a-f]{1,4}){0,2}:(?:(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(?:\.(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:))|(?:(?:[0-9a-f]{1,4}:){2}(?:(?:(?::[0-9a-f]{1,4}){1,5})|(?:(?::[0-9a-f]{1,4}){0,3}:(?:(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(?:\.(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:))|(?:(?:[0-9a-f]{1,4}:){1}(?:(?:(?::[0-9a-f]{1,4}){1,6})|(?:(?::[0-9a-f]{1,4}){0,4}:(?:(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(?:\.(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:))|(?::(?:(?:(?::[0-9a-f]{1,4}){1,7})|(?:(?::[0-9a-f]{1,4}){0,5}:(?:(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(?:\.(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:)))(?:%.+)?\s*$/i,
|
||||||
|
regex: regex,
|
||||||
|
// uuid: http://tools.ietf.org/html/rfc4122
|
||||||
|
uuid: UUID,
|
||||||
|
// JSON-pointer: https://tools.ietf.org/html/rfc6901
|
||||||
|
// uri fragment: https://tools.ietf.org/html/rfc3986#appendix-A
|
||||||
|
'json-pointer': JSON_POINTER,
|
||||||
|
'json-pointer-uri-fragment': JSON_POINTER_URI_FRAGMENT,
|
||||||
|
// relative JSON-pointer: http://tools.ietf.org/html/draft-luff-relative-json-pointer-00
|
||||||
|
'relative-json-pointer': RELATIVE_JSON_POINTER
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
formats.full = {
|
||||||
|
date: date,
|
||||||
|
time: time,
|
||||||
|
'date-time': date_time,
|
||||||
|
uri: uri,
|
||||||
|
'uri-reference': URIREF,
|
||||||
|
'uri-template': URITEMPLATE,
|
||||||
|
url: URL,
|
||||||
|
email: /^[a-z0-9!#$%&'*+/=?^_`{|}~-]+(?:\.[a-z0-9!#$%&'*+/=?^_`{|}~-]+)*@(?:[a-z0-9](?:[a-z0-9-]*[a-z0-9])?\.)+[a-z0-9](?:[a-z0-9-]*[a-z0-9])?$/i,
|
||||||
|
hostname: HOSTNAME,
|
||||||
|
ipv4: /^(?:(?:25[0-5]|2[0-4]\d|[01]?\d\d?)\.){3}(?:25[0-5]|2[0-4]\d|[01]?\d\d?)$/,
|
||||||
|
ipv6: /^\s*(?:(?:(?:[0-9a-f]{1,4}:){7}(?:[0-9a-f]{1,4}|:))|(?:(?:[0-9a-f]{1,4}:){6}(?::[0-9a-f]{1,4}|(?:(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(?:\.(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3})|:))|(?:(?:[0-9a-f]{1,4}:){5}(?:(?:(?::[0-9a-f]{1,4}){1,2})|:(?:(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(?:\.(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3})|:))|(?:(?:[0-9a-f]{1,4}:){4}(?:(?:(?::[0-9a-f]{1,4}){1,3})|(?:(?::[0-9a-f]{1,4})?:(?:(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(?:\.(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:))|(?:(?:[0-9a-f]{1,4}:){3}(?:(?:(?::[0-9a-f]{1,4}){1,4})|(?:(?::[0-9a-f]{1,4}){0,2}:(?:(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(?:\.(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:))|(?:(?:[0-9a-f]{1,4}:){2}(?:(?:(?::[0-9a-f]{1,4}){1,5})|(?:(?::[0-9a-f]{1,4}){0,3}:(?:(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(?:\.(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:))|(?:(?:[0-9a-f]{1,4}:){1}(?:(?:(?::[0-9a-f]{1,4}){1,6})|(?:(?::[0-9a-f]{1,4}){0,4}:(?:(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(?:\.(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:))|(?::(?:(?:(?::[0-9a-f]{1,4}){1,7})|(?:(?::[0-9a-f]{1,4}){0,5}:(?:(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(?:\.(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:)))(?:%.+)?\s*$/i,
|
||||||
|
regex: regex,
|
||||||
|
uuid: UUID,
|
||||||
|
'json-pointer': JSON_POINTER,
|
||||||
|
'json-pointer-uri-fragment': JSON_POINTER_URI_FRAGMENT,
|
||||||
|
'relative-json-pointer': RELATIVE_JSON_POINTER
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
function isLeapYear(year) {
|
||||||
|
// https://tools.ietf.org/html/rfc3339#appendix-C
|
||||||
|
return year % 4 === 0 && (year % 100 !== 0 || year % 400 === 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
function date(str) {
|
||||||
|
// full-date from http://tools.ietf.org/html/rfc3339#section-5.6
|
||||||
|
var matches = str.match(DATE);
|
||||||
|
if (!matches) return false;
|
||||||
|
|
||||||
|
var year = +matches[1];
|
||||||
|
var month = +matches[2];
|
||||||
|
var day = +matches[3];
|
||||||
|
|
||||||
|
return month >= 1 && month <= 12 && day >= 1 &&
|
||||||
|
day <= (month == 2 && isLeapYear(year) ? 29 : DAYS[month]);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
function time(str, full) {
|
||||||
|
var matches = str.match(TIME);
|
||||||
|
if (!matches) return false;
|
||||||
|
|
||||||
|
var hour = matches[1];
|
||||||
|
var minute = matches[2];
|
||||||
|
var second = matches[3];
|
||||||
|
var timeZone = matches[5];
|
||||||
|
return ((hour <= 23 && minute <= 59 && second <= 59) ||
|
||||||
|
(hour == 23 && minute == 59 && second == 60)) &&
|
||||||
|
(!full || timeZone);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
var DATE_TIME_SEPARATOR = /t|\s/i;
|
||||||
|
function date_time(str) {
|
||||||
|
// http://tools.ietf.org/html/rfc3339#section-5.6
|
||||||
|
var dateTime = str.split(DATE_TIME_SEPARATOR);
|
||||||
|
return dateTime.length == 2 && date(dateTime[0]) && time(dateTime[1], true);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
var NOT_URI_FRAGMENT = /\/|:/;
|
||||||
|
function uri(str) {
|
||||||
|
// http://jmrware.com/articles/2009/uri_regexp/URI_regex.html + optional protocol + required "."
|
||||||
|
return NOT_URI_FRAGMENT.test(str) && URI.test(str);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
var Z_ANCHOR = /[^\\]\\Z/;
|
||||||
|
function regex(str) {
|
||||||
|
if (Z_ANCHOR.test(str)) return false;
|
||||||
|
try {
|
||||||
|
new RegExp(str);
|
||||||
|
return true;
|
||||||
|
} catch(e) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,387 @@
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
var resolve = require('./resolve')
|
||||||
|
, util = require('./util')
|
||||||
|
, errorClasses = require('./error_classes')
|
||||||
|
, stableStringify = require('fast-json-stable-stringify');
|
||||||
|
|
||||||
|
var validateGenerator = require('../dotjs/validate');
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Functions below are used inside compiled validations function
|
||||||
|
*/
|
||||||
|
|
||||||
|
var ucs2length = util.ucs2length;
|
||||||
|
var equal = require('fast-deep-equal');
|
||||||
|
|
||||||
|
// this error is thrown by async schemas to return validation errors via exception
|
||||||
|
var ValidationError = errorClasses.Validation;
|
||||||
|
|
||||||
|
module.exports = compile;
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Compiles schema to validation function
|
||||||
|
* @this Ajv
|
||||||
|
* @param {Object} schema schema object
|
||||||
|
* @param {Object} root object with information about the root schema for this schema
|
||||||
|
* @param {Object} localRefs the hash of local references inside the schema (created by resolve.id), used for inline resolution
|
||||||
|
* @param {String} baseId base ID for IDs in the schema
|
||||||
|
* @return {Function} validation function
|
||||||
|
*/
|
||||||
|
function compile(schema, root, localRefs, baseId) {
|
||||||
|
/* jshint validthis: true, evil: true */
|
||||||
|
/* eslint no-shadow: 0 */
|
||||||
|
var self = this
|
||||||
|
, opts = this._opts
|
||||||
|
, refVal = [ undefined ]
|
||||||
|
, refs = {}
|
||||||
|
, patterns = []
|
||||||
|
, patternsHash = {}
|
||||||
|
, defaults = []
|
||||||
|
, defaultsHash = {}
|
||||||
|
, customRules = [];
|
||||||
|
|
||||||
|
root = root || { schema: schema, refVal: refVal, refs: refs };
|
||||||
|
|
||||||
|
var c = checkCompiling.call(this, schema, root, baseId);
|
||||||
|
var compilation = this._compilations[c.index];
|
||||||
|
if (c.compiling) return (compilation.callValidate = callValidate);
|
||||||
|
|
||||||
|
var formats = this._formats;
|
||||||
|
var RULES = this.RULES;
|
||||||
|
|
||||||
|
try {
|
||||||
|
var v = localCompile(schema, root, localRefs, baseId);
|
||||||
|
compilation.validate = v;
|
||||||
|
var cv = compilation.callValidate;
|
||||||
|
if (cv) {
|
||||||
|
cv.schema = v.schema;
|
||||||
|
cv.errors = null;
|
||||||
|
cv.refs = v.refs;
|
||||||
|
cv.refVal = v.refVal;
|
||||||
|
cv.root = v.root;
|
||||||
|
cv.$async = v.$async;
|
||||||
|
if (opts.sourceCode) cv.source = v.source;
|
||||||
|
}
|
||||||
|
return v;
|
||||||
|
} finally {
|
||||||
|
endCompiling.call(this, schema, root, baseId);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* @this {*} - custom context, see passContext option */
|
||||||
|
function callValidate() {
|
||||||
|
/* jshint validthis: true */
|
||||||
|
var validate = compilation.validate;
|
||||||
|
var result = validate.apply(this, arguments);
|
||||||
|
callValidate.errors = validate.errors;
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
function localCompile(_schema, _root, localRefs, baseId) {
|
||||||
|
var isRoot = !_root || (_root && _root.schema == _schema);
|
||||||
|
if (_root.schema != root.schema)
|
||||||
|
return compile.call(self, _schema, _root, localRefs, baseId);
|
||||||
|
|
||||||
|
var $async = _schema.$async === true;
|
||||||
|
|
||||||
|
var sourceCode = validateGenerator({
|
||||||
|
isTop: true,
|
||||||
|
schema: _schema,
|
||||||
|
isRoot: isRoot,
|
||||||
|
baseId: baseId,
|
||||||
|
root: _root,
|
||||||
|
schemaPath: '',
|
||||||
|
errSchemaPath: '#',
|
||||||
|
errorPath: '""',
|
||||||
|
MissingRefError: errorClasses.MissingRef,
|
||||||
|
RULES: RULES,
|
||||||
|
validate: validateGenerator,
|
||||||
|
util: util,
|
||||||
|
resolve: resolve,
|
||||||
|
resolveRef: resolveRef,
|
||||||
|
usePattern: usePattern,
|
||||||
|
useDefault: useDefault,
|
||||||
|
useCustomRule: useCustomRule,
|
||||||
|
opts: opts,
|
||||||
|
formats: formats,
|
||||||
|
logger: self.logger,
|
||||||
|
self: self
|
||||||
|
});
|
||||||
|
|
||||||
|
sourceCode = vars(refVal, refValCode) + vars(patterns, patternCode)
|
||||||
|
+ vars(defaults, defaultCode) + vars(customRules, customRuleCode)
|
||||||
|
+ sourceCode;
|
||||||
|
|
||||||
|
if (opts.processCode) sourceCode = opts.processCode(sourceCode, _schema);
|
||||||
|
// console.log('\n\n\n *** \n', JSON.stringify(sourceCode));
|
||||||
|
var validate;
|
||||||
|
try {
|
||||||
|
var makeValidate = new Function(
|
||||||
|
'self',
|
||||||
|
'RULES',
|
||||||
|
'formats',
|
||||||
|
'root',
|
||||||
|
'refVal',
|
||||||
|
'defaults',
|
||||||
|
'customRules',
|
||||||
|
'equal',
|
||||||
|
'ucs2length',
|
||||||
|
'ValidationError',
|
||||||
|
sourceCode
|
||||||
|
);
|
||||||
|
|
||||||
|
validate = makeValidate(
|
||||||
|
self,
|
||||||
|
RULES,
|
||||||
|
formats,
|
||||||
|
root,
|
||||||
|
refVal,
|
||||||
|
defaults,
|
||||||
|
customRules,
|
||||||
|
equal,
|
||||||
|
ucs2length,
|
||||||
|
ValidationError
|
||||||
|
);
|
||||||
|
|
||||||
|
refVal[0] = validate;
|
||||||
|
} catch(e) {
|
||||||
|
self.logger.error('Error compiling schema, function code:', sourceCode);
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
|
||||||
|
validate.schema = _schema;
|
||||||
|
validate.errors = null;
|
||||||
|
validate.refs = refs;
|
||||||
|
validate.refVal = refVal;
|
||||||
|
validate.root = isRoot ? validate : _root;
|
||||||
|
if ($async) validate.$async = true;
|
||||||
|
if (opts.sourceCode === true) {
|
||||||
|
validate.source = {
|
||||||
|
code: sourceCode,
|
||||||
|
patterns: patterns,
|
||||||
|
defaults: defaults
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return validate;
|
||||||
|
}
|
||||||
|
|
||||||
|
function resolveRef(baseId, ref, isRoot) {
|
||||||
|
ref = resolve.url(baseId, ref);
|
||||||
|
var refIndex = refs[ref];
|
||||||
|
var _refVal, refCode;
|
||||||
|
if (refIndex !== undefined) {
|
||||||
|
_refVal = refVal[refIndex];
|
||||||
|
refCode = 'refVal[' + refIndex + ']';
|
||||||
|
return resolvedRef(_refVal, refCode);
|
||||||
|
}
|
||||||
|
if (!isRoot && root.refs) {
|
||||||
|
var rootRefId = root.refs[ref];
|
||||||
|
if (rootRefId !== undefined) {
|
||||||
|
_refVal = root.refVal[rootRefId];
|
||||||
|
refCode = addLocalRef(ref, _refVal);
|
||||||
|
return resolvedRef(_refVal, refCode);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
refCode = addLocalRef(ref);
|
||||||
|
var v = resolve.call(self, localCompile, root, ref);
|
||||||
|
if (v === undefined) {
|
||||||
|
var localSchema = localRefs && localRefs[ref];
|
||||||
|
if (localSchema) {
|
||||||
|
v = resolve.inlineRef(localSchema, opts.inlineRefs)
|
||||||
|
? localSchema
|
||||||
|
: compile.call(self, localSchema, root, localRefs, baseId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (v === undefined) {
|
||||||
|
removeLocalRef(ref);
|
||||||
|
} else {
|
||||||
|
replaceLocalRef(ref, v);
|
||||||
|
return resolvedRef(v, refCode);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function addLocalRef(ref, v) {
|
||||||
|
var refId = refVal.length;
|
||||||
|
refVal[refId] = v;
|
||||||
|
refs[ref] = refId;
|
||||||
|
return 'refVal' + refId;
|
||||||
|
}
|
||||||
|
|
||||||
|
function removeLocalRef(ref) {
|
||||||
|
delete refs[ref];
|
||||||
|
}
|
||||||
|
|
||||||
|
function replaceLocalRef(ref, v) {
|
||||||
|
var refId = refs[ref];
|
||||||
|
refVal[refId] = v;
|
||||||
|
}
|
||||||
|
|
||||||
|
function resolvedRef(refVal, code) {
|
||||||
|
return typeof refVal == 'object' || typeof refVal == 'boolean'
|
||||||
|
? { code: code, schema: refVal, inline: true }
|
||||||
|
: { code: code, $async: refVal && !!refVal.$async };
|
||||||
|
}
|
||||||
|
|
||||||
|
function usePattern(regexStr) {
|
||||||
|
var index = patternsHash[regexStr];
|
||||||
|
if (index === undefined) {
|
||||||
|
index = patternsHash[regexStr] = patterns.length;
|
||||||
|
patterns[index] = regexStr;
|
||||||
|
}
|
||||||
|
return 'pattern' + index;
|
||||||
|
}
|
||||||
|
|
||||||
|
function useDefault(value) {
|
||||||
|
switch (typeof value) {
|
||||||
|
case 'boolean':
|
||||||
|
case 'number':
|
||||||
|
return '' + value;
|
||||||
|
case 'string':
|
||||||
|
return util.toQuotedString(value);
|
||||||
|
case 'object':
|
||||||
|
if (value === null) return 'null';
|
||||||
|
var valueStr = stableStringify(value);
|
||||||
|
var index = defaultsHash[valueStr];
|
||||||
|
if (index === undefined) {
|
||||||
|
index = defaultsHash[valueStr] = defaults.length;
|
||||||
|
defaults[index] = value;
|
||||||
|
}
|
||||||
|
return 'default' + index;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function useCustomRule(rule, schema, parentSchema, it) {
|
||||||
|
if (self._opts.validateSchema !== false) {
|
||||||
|
var deps = rule.definition.dependencies;
|
||||||
|
if (deps && !deps.every(function(keyword) {
|
||||||
|
return Object.prototype.hasOwnProperty.call(parentSchema, keyword);
|
||||||
|
}))
|
||||||
|
throw new Error('parent schema must have all required keywords: ' + deps.join(','));
|
||||||
|
|
||||||
|
var validateSchema = rule.definition.validateSchema;
|
||||||
|
if (validateSchema) {
|
||||||
|
var valid = validateSchema(schema);
|
||||||
|
if (!valid) {
|
||||||
|
var message = 'keyword schema is invalid: ' + self.errorsText(validateSchema.errors);
|
||||||
|
if (self._opts.validateSchema == 'log') self.logger.error(message);
|
||||||
|
else throw new Error(message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var compile = rule.definition.compile
|
||||||
|
, inline = rule.definition.inline
|
||||||
|
, macro = rule.definition.macro;
|
||||||
|
|
||||||
|
var validate;
|
||||||
|
if (compile) {
|
||||||
|
validate = compile.call(self, schema, parentSchema, it);
|
||||||
|
} else if (macro) {
|
||||||
|
validate = macro.call(self, schema, parentSchema, it);
|
||||||
|
if (opts.validateSchema !== false) self.validateSchema(validate, true);
|
||||||
|
} else if (inline) {
|
||||||
|
validate = inline.call(self, it, rule.keyword, schema, parentSchema);
|
||||||
|
} else {
|
||||||
|
validate = rule.definition.validate;
|
||||||
|
if (!validate) return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (validate === undefined)
|
||||||
|
throw new Error('custom keyword "' + rule.keyword + '"failed to compile');
|
||||||
|
|
||||||
|
var index = customRules.length;
|
||||||
|
customRules[index] = validate;
|
||||||
|
|
||||||
|
return {
|
||||||
|
code: 'customRule' + index,
|
||||||
|
validate: validate
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks if the schema is currently compiled
|
||||||
|
* @this Ajv
|
||||||
|
* @param {Object} schema schema to compile
|
||||||
|
* @param {Object} root root object
|
||||||
|
* @param {String} baseId base schema ID
|
||||||
|
* @return {Object} object with properties "index" (compilation index) and "compiling" (boolean)
|
||||||
|
*/
|
||||||
|
function checkCompiling(schema, root, baseId) {
|
||||||
|
/* jshint validthis: true */
|
||||||
|
var index = compIndex.call(this, schema, root, baseId);
|
||||||
|
if (index >= 0) return { index: index, compiling: true };
|
||||||
|
index = this._compilations.length;
|
||||||
|
this._compilations[index] = {
|
||||||
|
schema: schema,
|
||||||
|
root: root,
|
||||||
|
baseId: baseId
|
||||||
|
};
|
||||||
|
return { index: index, compiling: false };
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Removes the schema from the currently compiled list
|
||||||
|
* @this Ajv
|
||||||
|
* @param {Object} schema schema to compile
|
||||||
|
* @param {Object} root root object
|
||||||
|
* @param {String} baseId base schema ID
|
||||||
|
*/
|
||||||
|
function endCompiling(schema, root, baseId) {
|
||||||
|
/* jshint validthis: true */
|
||||||
|
var i = compIndex.call(this, schema, root, baseId);
|
||||||
|
if (i >= 0) this._compilations.splice(i, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Index of schema compilation in the currently compiled list
|
||||||
|
* @this Ajv
|
||||||
|
* @param {Object} schema schema to compile
|
||||||
|
* @param {Object} root root object
|
||||||
|
* @param {String} baseId base schema ID
|
||||||
|
* @return {Integer} compilation index
|
||||||
|
*/
|
||||||
|
function compIndex(schema, root, baseId) {
|
||||||
|
/* jshint validthis: true */
|
||||||
|
for (var i=0; i<this._compilations.length; i++) {
|
||||||
|
var c = this._compilations[i];
|
||||||
|
if (c.schema == schema && c.root == root && c.baseId == baseId) return i;
|
||||||
|
}
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
function patternCode(i, patterns) {
|
||||||
|
return 'var pattern' + i + ' = new RegExp(' + util.toQuotedString(patterns[i]) + ');';
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
function defaultCode(i) {
|
||||||
|
return 'var default' + i + ' = defaults[' + i + '];';
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
function refValCode(i, refVal) {
|
||||||
|
return refVal[i] === undefined ? '' : 'var refVal' + i + ' = refVal[' + i + '];';
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
function customRuleCode(i) {
|
||||||
|
return 'var customRule' + i + ' = customRules[' + i + '];';
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
function vars(arr, statement) {
|
||||||
|
if (!arr.length) return '';
|
||||||
|
var code = '';
|
||||||
|
for (var i=0; i<arr.length; i++)
|
||||||
|
code += statement(i, arr);
|
||||||
|
return code;
|
||||||
|
}
|
|
@ -0,0 +1,270 @@
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
var URI = require('uri-js')
|
||||||
|
, equal = require('fast-deep-equal')
|
||||||
|
, util = require('./util')
|
||||||
|
, SchemaObject = require('./schema_obj')
|
||||||
|
, traverse = require('json-schema-traverse');
|
||||||
|
|
||||||
|
module.exports = resolve;
|
||||||
|
|
||||||
|
resolve.normalizeId = normalizeId;
|
||||||
|
resolve.fullPath = getFullPath;
|
||||||
|
resolve.url = resolveUrl;
|
||||||
|
resolve.ids = resolveIds;
|
||||||
|
resolve.inlineRef = inlineRef;
|
||||||
|
resolve.schema = resolveSchema;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* [resolve and compile the references ($ref)]
|
||||||
|
* @this Ajv
|
||||||
|
* @param {Function} compile reference to schema compilation funciton (localCompile)
|
||||||
|
* @param {Object} root object with information about the root schema for the current schema
|
||||||
|
* @param {String} ref reference to resolve
|
||||||
|
* @return {Object|Function} schema object (if the schema can be inlined) or validation function
|
||||||
|
*/
|
||||||
|
function resolve(compile, root, ref) {
|
||||||
|
/* jshint validthis: true */
|
||||||
|
var refVal = this._refs[ref];
|
||||||
|
if (typeof refVal == 'string') {
|
||||||
|
if (this._refs[refVal]) refVal = this._refs[refVal];
|
||||||
|
else return resolve.call(this, compile, root, refVal);
|
||||||
|
}
|
||||||
|
|
||||||
|
refVal = refVal || this._schemas[ref];
|
||||||
|
if (refVal instanceof SchemaObject) {
|
||||||
|
return inlineRef(refVal.schema, this._opts.inlineRefs)
|
||||||
|
? refVal.schema
|
||||||
|
: refVal.validate || this._compile(refVal);
|
||||||
|
}
|
||||||
|
|
||||||
|
var res = resolveSchema.call(this, root, ref);
|
||||||
|
var schema, v, baseId;
|
||||||
|
if (res) {
|
||||||
|
schema = res.schema;
|
||||||
|
root = res.root;
|
||||||
|
baseId = res.baseId;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (schema instanceof SchemaObject) {
|
||||||
|
v = schema.validate || compile.call(this, schema.schema, root, undefined, baseId);
|
||||||
|
} else if (schema !== undefined) {
|
||||||
|
v = inlineRef(schema, this._opts.inlineRefs)
|
||||||
|
? schema
|
||||||
|
: compile.call(this, schema, root, undefined, baseId);
|
||||||
|
}
|
||||||
|
|
||||||
|
return v;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Resolve schema, its root and baseId
|
||||||
|
* @this Ajv
|
||||||
|
* @param {Object} root root object with properties schema, refVal, refs
|
||||||
|
* @param {String} ref reference to resolve
|
||||||
|
* @return {Object} object with properties schema, root, baseId
|
||||||
|
*/
|
||||||
|
function resolveSchema(root, ref) {
|
||||||
|
/* jshint validthis: true */
|
||||||
|
var p = URI.parse(ref)
|
||||||
|
, refPath = _getFullPath(p)
|
||||||
|
, baseId = getFullPath(this._getId(root.schema));
|
||||||
|
if (Object.keys(root.schema).length === 0 || refPath !== baseId) {
|
||||||
|
var id = normalizeId(refPath);
|
||||||
|
var refVal = this._refs[id];
|
||||||
|
if (typeof refVal == 'string') {
|
||||||
|
return resolveRecursive.call(this, root, refVal, p);
|
||||||
|
} else if (refVal instanceof SchemaObject) {
|
||||||
|
if (!refVal.validate) this._compile(refVal);
|
||||||
|
root = refVal;
|
||||||
|
} else {
|
||||||
|
refVal = this._schemas[id];
|
||||||
|
if (refVal instanceof SchemaObject) {
|
||||||
|
if (!refVal.validate) this._compile(refVal);
|
||||||
|
if (id == normalizeId(ref))
|
||||||
|
return { schema: refVal, root: root, baseId: baseId };
|
||||||
|
root = refVal;
|
||||||
|
} else {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!root.schema) return;
|
||||||
|
baseId = getFullPath(this._getId(root.schema));
|
||||||
|
}
|
||||||
|
return getJsonPointer.call(this, p, baseId, root.schema, root);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/* @this Ajv */
|
||||||
|
function resolveRecursive(root, ref, parsedRef) {
|
||||||
|
/* jshint validthis: true */
|
||||||
|
var res = resolveSchema.call(this, root, ref);
|
||||||
|
if (res) {
|
||||||
|
var schema = res.schema;
|
||||||
|
var baseId = res.baseId;
|
||||||
|
root = res.root;
|
||||||
|
var id = this._getId(schema);
|
||||||
|
if (id) baseId = resolveUrl(baseId, id);
|
||||||
|
return getJsonPointer.call(this, parsedRef, baseId, schema, root);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
var PREVENT_SCOPE_CHANGE = util.toHash(['properties', 'patternProperties', 'enum', 'dependencies', 'definitions']);
|
||||||
|
/* @this Ajv */
|
||||||
|
function getJsonPointer(parsedRef, baseId, schema, root) {
|
||||||
|
/* jshint validthis: true */
|
||||||
|
parsedRef.fragment = parsedRef.fragment || '';
|
||||||
|
if (parsedRef.fragment.slice(0,1) != '/') return;
|
||||||
|
var parts = parsedRef.fragment.split('/');
|
||||||
|
|
||||||
|
for (var i = 1; i < parts.length; i++) {
|
||||||
|
var part = parts[i];
|
||||||
|
if (part) {
|
||||||
|
part = util.unescapeFragment(part);
|
||||||
|
schema = schema[part];
|
||||||
|
if (schema === undefined) break;
|
||||||
|
var id;
|
||||||
|
if (!PREVENT_SCOPE_CHANGE[part]) {
|
||||||
|
id = this._getId(schema);
|
||||||
|
if (id) baseId = resolveUrl(baseId, id);
|
||||||
|
if (schema.$ref) {
|
||||||
|
var $ref = resolveUrl(baseId, schema.$ref);
|
||||||
|
var res = resolveSchema.call(this, root, $ref);
|
||||||
|
if (res) {
|
||||||
|
schema = res.schema;
|
||||||
|
root = res.root;
|
||||||
|
baseId = res.baseId;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (schema !== undefined && schema !== root.schema)
|
||||||
|
return { schema: schema, root: root, baseId: baseId };
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
var SIMPLE_INLINED = util.toHash([
|
||||||
|
'type', 'format', 'pattern',
|
||||||
|
'maxLength', 'minLength',
|
||||||
|
'maxProperties', 'minProperties',
|
||||||
|
'maxItems', 'minItems',
|
||||||
|
'maximum', 'minimum',
|
||||||
|
'uniqueItems', 'multipleOf',
|
||||||
|
'required', 'enum'
|
||||||
|
]);
|
||||||
|
function inlineRef(schema, limit) {
|
||||||
|
if (limit === false) return false;
|
||||||
|
if (limit === undefined || limit === true) return checkNoRef(schema);
|
||||||
|
else if (limit) return countKeys(schema) <= limit;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
function checkNoRef(schema) {
|
||||||
|
var item;
|
||||||
|
if (Array.isArray(schema)) {
|
||||||
|
for (var i=0; i<schema.length; i++) {
|
||||||
|
item = schema[i];
|
||||||
|
if (typeof item == 'object' && !checkNoRef(item)) return false;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
for (var key in schema) {
|
||||||
|
if (key == '$ref') return false;
|
||||||
|
item = schema[key];
|
||||||
|
if (typeof item == 'object' && !checkNoRef(item)) return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
function countKeys(schema) {
|
||||||
|
var count = 0, item;
|
||||||
|
if (Array.isArray(schema)) {
|
||||||
|
for (var i=0; i<schema.length; i++) {
|
||||||
|
item = schema[i];
|
||||||
|
if (typeof item == 'object') count += countKeys(item);
|
||||||
|
if (count == Infinity) return Infinity;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
for (var key in schema) {
|
||||||
|
if (key == '$ref') return Infinity;
|
||||||
|
if (SIMPLE_INLINED[key]) {
|
||||||
|
count++;
|
||||||
|
} else {
|
||||||
|
item = schema[key];
|
||||||
|
if (typeof item == 'object') count += countKeys(item) + 1;
|
||||||
|
if (count == Infinity) return Infinity;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return count;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
function getFullPath(id, normalize) {
|
||||||
|
if (normalize !== false) id = normalizeId(id);
|
||||||
|
var p = URI.parse(id);
|
||||||
|
return _getFullPath(p);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
function _getFullPath(p) {
|
||||||
|
return URI.serialize(p).split('#')[0] + '#';
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
var TRAILING_SLASH_HASH = /#\/?$/;
|
||||||
|
function normalizeId(id) {
|
||||||
|
return id ? id.replace(TRAILING_SLASH_HASH, '') : '';
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
function resolveUrl(baseId, id) {
|
||||||
|
id = normalizeId(id);
|
||||||
|
return URI.resolve(baseId, id);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/* @this Ajv */
|
||||||
|
function resolveIds(schema) {
|
||||||
|
var schemaId = normalizeId(this._getId(schema));
|
||||||
|
var baseIds = {'': schemaId};
|
||||||
|
var fullPaths = {'': getFullPath(schemaId, false)};
|
||||||
|
var localRefs = {};
|
||||||
|
var self = this;
|
||||||
|
|
||||||
|
traverse(schema, {allKeys: true}, function(sch, jsonPtr, rootSchema, parentJsonPtr, parentKeyword, parentSchema, keyIndex) {
|
||||||
|
if (jsonPtr === '') return;
|
||||||
|
var id = self._getId(sch);
|
||||||
|
var baseId = baseIds[parentJsonPtr];
|
||||||
|
var fullPath = fullPaths[parentJsonPtr] + '/' + parentKeyword;
|
||||||
|
if (keyIndex !== undefined)
|
||||||
|
fullPath += '/' + (typeof keyIndex == 'number' ? keyIndex : util.escapeFragment(keyIndex));
|
||||||
|
|
||||||
|
if (typeof id == 'string') {
|
||||||
|
id = baseId = normalizeId(baseId ? URI.resolve(baseId, id) : id);
|
||||||
|
|
||||||
|
var refVal = self._refs[id];
|
||||||
|
if (typeof refVal == 'string') refVal = self._refs[refVal];
|
||||||
|
if (refVal && refVal.schema) {
|
||||||
|
if (!equal(sch, refVal.schema))
|
||||||
|
throw new Error('id "' + id + '" resolves to more than one schema');
|
||||||
|
} else if (id != normalizeId(fullPath)) {
|
||||||
|
if (id[0] == '#') {
|
||||||
|
if (localRefs[id] && !equal(sch, localRefs[id]))
|
||||||
|
throw new Error('id "' + id + '" resolves to more than one schema');
|
||||||
|
localRefs[id] = sch;
|
||||||
|
} else {
|
||||||
|
self._refs[id] = fullPath;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
baseIds[jsonPtr] = baseId;
|
||||||
|
fullPaths[jsonPtr] = fullPath;
|
||||||
|
});
|
||||||
|
|
||||||
|
return localRefs;
|
||||||
|
}
|
|
@ -0,0 +1,66 @@
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
var ruleModules = require('../dotjs')
|
||||||
|
, toHash = require('./util').toHash;
|
||||||
|
|
||||||
|
module.exports = function rules() {
|
||||||
|
var RULES = [
|
||||||
|
{ type: 'number',
|
||||||
|
rules: [ { 'maximum': ['exclusiveMaximum'] },
|
||||||
|
{ 'minimum': ['exclusiveMinimum'] }, 'multipleOf', 'format'] },
|
||||||
|
{ type: 'string',
|
||||||
|
rules: [ 'maxLength', 'minLength', 'pattern', 'format' ] },
|
||||||
|
{ type: 'array',
|
||||||
|
rules: [ 'maxItems', 'minItems', 'items', 'contains', 'uniqueItems' ] },
|
||||||
|
{ type: 'object',
|
||||||
|
rules: [ 'maxProperties', 'minProperties', 'required', 'dependencies', 'propertyNames',
|
||||||
|
{ 'properties': ['additionalProperties', 'patternProperties'] } ] },
|
||||||
|
{ rules: [ '$ref', 'const', 'enum', 'not', 'anyOf', 'oneOf', 'allOf', 'if' ] }
|
||||||
|
];
|
||||||
|
|
||||||
|
var ALL = [ 'type', '$comment' ];
|
||||||
|
var KEYWORDS = [
|
||||||
|
'$schema', '$id', 'id', '$data', '$async', 'title',
|
||||||
|
'description', 'default', 'definitions',
|
||||||
|
'examples', 'readOnly', 'writeOnly',
|
||||||
|
'contentMediaType', 'contentEncoding',
|
||||||
|
'additionalItems', 'then', 'else'
|
||||||
|
];
|
||||||
|
var TYPES = [ 'number', 'integer', 'string', 'array', 'object', 'boolean', 'null' ];
|
||||||
|
RULES.all = toHash(ALL);
|
||||||
|
RULES.types = toHash(TYPES);
|
||||||
|
|
||||||
|
RULES.forEach(function (group) {
|
||||||
|
group.rules = group.rules.map(function (keyword) {
|
||||||
|
var implKeywords;
|
||||||
|
if (typeof keyword == 'object') {
|
||||||
|
var key = Object.keys(keyword)[0];
|
||||||
|
implKeywords = keyword[key];
|
||||||
|
keyword = key;
|
||||||
|
implKeywords.forEach(function (k) {
|
||||||
|
ALL.push(k);
|
||||||
|
RULES.all[k] = true;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
ALL.push(keyword);
|
||||||
|
var rule = RULES.all[keyword] = {
|
||||||
|
keyword: keyword,
|
||||||
|
code: ruleModules[keyword],
|
||||||
|
implements: implKeywords
|
||||||
|
};
|
||||||
|
return rule;
|
||||||
|
});
|
||||||
|
|
||||||
|
RULES.all.$comment = {
|
||||||
|
keyword: '$comment',
|
||||||
|
code: ruleModules.$comment
|
||||||
|
};
|
||||||
|
|
||||||
|
if (group.type) RULES.types[group.type] = group;
|
||||||
|
});
|
||||||
|
|
||||||
|
RULES.keywords = toHash(ALL.concat(KEYWORDS));
|
||||||
|
RULES.custom = {};
|
||||||
|
|
||||||
|
return RULES;
|
||||||
|
};
|
|
@ -0,0 +1,9 @@
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
var util = require('./util');
|
||||||
|
|
||||||
|
module.exports = SchemaObject;
|
||||||
|
|
||||||
|
function SchemaObject(obj) {
|
||||||
|
util.copy(obj, this);
|
||||||
|
}
|
|
@ -0,0 +1,20 @@
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
// https://mathiasbynens.be/notes/javascript-encoding
|
||||||
|
// https://github.com/bestiejs/punycode.js - punycode.ucs2.decode
|
||||||
|
module.exports = function ucs2length(str) {
|
||||||
|
var length = 0
|
||||||
|
, len = str.length
|
||||||
|
, pos = 0
|
||||||
|
, value;
|
||||||
|
while (pos < len) {
|
||||||
|
length++;
|
||||||
|
value = str.charCodeAt(pos++);
|
||||||
|
if (value >= 0xD800 && value <= 0xDBFF && pos < len) {
|
||||||
|
// high surrogate, and there is a next character
|
||||||
|
value = str.charCodeAt(pos);
|
||||||
|
if ((value & 0xFC00) == 0xDC00) pos++; // low surrogate
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return length;
|
||||||
|
};
|
|
@ -0,0 +1,239 @@
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
copy: copy,
|
||||||
|
checkDataType: checkDataType,
|
||||||
|
checkDataTypes: checkDataTypes,
|
||||||
|
coerceToTypes: coerceToTypes,
|
||||||
|
toHash: toHash,
|
||||||
|
getProperty: getProperty,
|
||||||
|
escapeQuotes: escapeQuotes,
|
||||||
|
equal: require('fast-deep-equal'),
|
||||||
|
ucs2length: require('./ucs2length'),
|
||||||
|
varOccurences: varOccurences,
|
||||||
|
varReplace: varReplace,
|
||||||
|
schemaHasRules: schemaHasRules,
|
||||||
|
schemaHasRulesExcept: schemaHasRulesExcept,
|
||||||
|
schemaUnknownRules: schemaUnknownRules,
|
||||||
|
toQuotedString: toQuotedString,
|
||||||
|
getPathExpr: getPathExpr,
|
||||||
|
getPath: getPath,
|
||||||
|
getData: getData,
|
||||||
|
unescapeFragment: unescapeFragment,
|
||||||
|
unescapeJsonPointer: unescapeJsonPointer,
|
||||||
|
escapeFragment: escapeFragment,
|
||||||
|
escapeJsonPointer: escapeJsonPointer
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
function copy(o, to) {
|
||||||
|
to = to || {};
|
||||||
|
for (var key in o) to[key] = o[key];
|
||||||
|
return to;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
function checkDataType(dataType, data, strictNumbers, negate) {
|
||||||
|
var EQUAL = negate ? ' !== ' : ' === '
|
||||||
|
, AND = negate ? ' || ' : ' && '
|
||||||
|
, OK = negate ? '!' : ''
|
||||||
|
, NOT = negate ? '' : '!';
|
||||||
|
switch (dataType) {
|
||||||
|
case 'null': return data + EQUAL + 'null';
|
||||||
|
case 'array': return OK + 'Array.isArray(' + data + ')';
|
||||||
|
case 'object': return '(' + OK + data + AND +
|
||||||
|
'typeof ' + data + EQUAL + '"object"' + AND +
|
||||||
|
NOT + 'Array.isArray(' + data + '))';
|
||||||
|
case 'integer': return '(typeof ' + data + EQUAL + '"number"' + AND +
|
||||||
|
NOT + '(' + data + ' % 1)' +
|
||||||
|
AND + data + EQUAL + data +
|
||||||
|
(strictNumbers ? (AND + OK + 'isFinite(' + data + ')') : '') + ')';
|
||||||
|
case 'number': return '(typeof ' + data + EQUAL + '"' + dataType + '"' +
|
||||||
|
(strictNumbers ? (AND + OK + 'isFinite(' + data + ')') : '') + ')';
|
||||||
|
default: return 'typeof ' + data + EQUAL + '"' + dataType + '"';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
function checkDataTypes(dataTypes, data, strictNumbers) {
|
||||||
|
switch (dataTypes.length) {
|
||||||
|
case 1: return checkDataType(dataTypes[0], data, strictNumbers, true);
|
||||||
|
default:
|
||||||
|
var code = '';
|
||||||
|
var types = toHash(dataTypes);
|
||||||
|
if (types.array && types.object) {
|
||||||
|
code = types.null ? '(': '(!' + data + ' || ';
|
||||||
|
code += 'typeof ' + data + ' !== "object")';
|
||||||
|
delete types.null;
|
||||||
|
delete types.array;
|
||||||
|
delete types.object;
|
||||||
|
}
|
||||||
|
if (types.number) delete types.integer;
|
||||||
|
for (var t in types)
|
||||||
|
code += (code ? ' && ' : '' ) + checkDataType(t, data, strictNumbers, true);
|
||||||
|
|
||||||
|
return code;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
var COERCE_TO_TYPES = toHash([ 'string', 'number', 'integer', 'boolean', 'null' ]);
|
||||||
|
function coerceToTypes(optionCoerceTypes, dataTypes) {
|
||||||
|
if (Array.isArray(dataTypes)) {
|
||||||
|
var types = [];
|
||||||
|
for (var i=0; i<dataTypes.length; i++) {
|
||||||
|
var t = dataTypes[i];
|
||||||
|
if (COERCE_TO_TYPES[t]) types[types.length] = t;
|
||||||
|
else if (optionCoerceTypes === 'array' && t === 'array') types[types.length] = t;
|
||||||
|
}
|
||||||
|
if (types.length) return types;
|
||||||
|
} else if (COERCE_TO_TYPES[dataTypes]) {
|
||||||
|
return [dataTypes];
|
||||||
|
} else if (optionCoerceTypes === 'array' && dataTypes === 'array') {
|
||||||
|
return ['array'];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
function toHash(arr) {
|
||||||
|
var hash = {};
|
||||||
|
for (var i=0; i<arr.length; i++) hash[arr[i]] = true;
|
||||||
|
return hash;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
var IDENTIFIER = /^[a-z$_][a-z$_0-9]*$/i;
|
||||||
|
var SINGLE_QUOTE = /'|\\/g;
|
||||||
|
function getProperty(key) {
|
||||||
|
return typeof key == 'number'
|
||||||
|
? '[' + key + ']'
|
||||||
|
: IDENTIFIER.test(key)
|
||||||
|
? '.' + key
|
||||||
|
: "['" + escapeQuotes(key) + "']";
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
function escapeQuotes(str) {
|
||||||
|
return str.replace(SINGLE_QUOTE, '\\$&')
|
||||||
|
.replace(/\n/g, '\\n')
|
||||||
|
.replace(/\r/g, '\\r')
|
||||||
|
.replace(/\f/g, '\\f')
|
||||||
|
.replace(/\t/g, '\\t');
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
function varOccurences(str, dataVar) {
|
||||||
|
dataVar += '[^0-9]';
|
||||||
|
var matches = str.match(new RegExp(dataVar, 'g'));
|
||||||
|
return matches ? matches.length : 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
function varReplace(str, dataVar, expr) {
|
||||||
|
dataVar += '([^0-9])';
|
||||||
|
expr = expr.replace(/\$/g, '$$$$');
|
||||||
|
return str.replace(new RegExp(dataVar, 'g'), expr + '$1');
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
function schemaHasRules(schema, rules) {
|
||||||
|
if (typeof schema == 'boolean') return !schema;
|
||||||
|
for (var key in schema) if (rules[key]) return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
function schemaHasRulesExcept(schema, rules, exceptKeyword) {
|
||||||
|
if (typeof schema == 'boolean') return !schema && exceptKeyword != 'not';
|
||||||
|
for (var key in schema) if (key != exceptKeyword && rules[key]) return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
function schemaUnknownRules(schema, rules) {
|
||||||
|
if (typeof schema == 'boolean') return;
|
||||||
|
for (var key in schema) if (!rules[key]) return key;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
function toQuotedString(str) {
|
||||||
|
return '\'' + escapeQuotes(str) + '\'';
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
function getPathExpr(currentPath, expr, jsonPointers, isNumber) {
|
||||||
|
var path = jsonPointers // false by default
|
||||||
|
? '\'/\' + ' + expr + (isNumber ? '' : '.replace(/~/g, \'~0\').replace(/\\//g, \'~1\')')
|
||||||
|
: (isNumber ? '\'[\' + ' + expr + ' + \']\'' : '\'[\\\'\' + ' + expr + ' + \'\\\']\'');
|
||||||
|
return joinPaths(currentPath, path);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
function getPath(currentPath, prop, jsonPointers) {
|
||||||
|
var path = jsonPointers // false by default
|
||||||
|
? toQuotedString('/' + escapeJsonPointer(prop))
|
||||||
|
: toQuotedString(getProperty(prop));
|
||||||
|
return joinPaths(currentPath, path);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
var JSON_POINTER = /^\/(?:[^~]|~0|~1)*$/;
|
||||||
|
var RELATIVE_JSON_POINTER = /^([0-9]+)(#|\/(?:[^~]|~0|~1)*)?$/;
|
||||||
|
function getData($data, lvl, paths) {
|
||||||
|
var up, jsonPointer, data, matches;
|
||||||
|
if ($data === '') return 'rootData';
|
||||||
|
if ($data[0] == '/') {
|
||||||
|
if (!JSON_POINTER.test($data)) throw new Error('Invalid JSON-pointer: ' + $data);
|
||||||
|
jsonPointer = $data;
|
||||||
|
data = 'rootData';
|
||||||
|
} else {
|
||||||
|
matches = $data.match(RELATIVE_JSON_POINTER);
|
||||||
|
if (!matches) throw new Error('Invalid JSON-pointer: ' + $data);
|
||||||
|
up = +matches[1];
|
||||||
|
jsonPointer = matches[2];
|
||||||
|
if (jsonPointer == '#') {
|
||||||
|
if (up >= lvl) throw new Error('Cannot access property/index ' + up + ' levels up, current level is ' + lvl);
|
||||||
|
return paths[lvl - up];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (up > lvl) throw new Error('Cannot access data ' + up + ' levels up, current level is ' + lvl);
|
||||||
|
data = 'data' + ((lvl - up) || '');
|
||||||
|
if (!jsonPointer) return data;
|
||||||
|
}
|
||||||
|
|
||||||
|
var expr = data;
|
||||||
|
var segments = jsonPointer.split('/');
|
||||||
|
for (var i=0; i<segments.length; i++) {
|
||||||
|
var segment = segments[i];
|
||||||
|
if (segment) {
|
||||||
|
data += getProperty(unescapeJsonPointer(segment));
|
||||||
|
expr += ' && ' + data;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return expr;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
function joinPaths (a, b) {
|
||||||
|
if (a == '""') return b;
|
||||||
|
return (a + ' + ' + b).replace(/([^\\])' \+ '/g, '$1');
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
function unescapeFragment(str) {
|
||||||
|
return unescapeJsonPointer(decodeURIComponent(str));
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
function escapeFragment(str) {
|
||||||
|
return encodeURIComponent(escapeJsonPointer(str));
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
function escapeJsonPointer(str) {
|
||||||
|
return str.replace(/~/g, '~0').replace(/\//g, '~1');
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
function unescapeJsonPointer(str) {
|
||||||
|
return str.replace(/~1/g, '/').replace(/~0/g, '~');
|
||||||
|
}
|
|
@ -0,0 +1,49 @@
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
var KEYWORDS = [
|
||||||
|
'multipleOf',
|
||||||
|
'maximum',
|
||||||
|
'exclusiveMaximum',
|
||||||
|
'minimum',
|
||||||
|
'exclusiveMinimum',
|
||||||
|
'maxLength',
|
||||||
|
'minLength',
|
||||||
|
'pattern',
|
||||||
|
'additionalItems',
|
||||||
|
'maxItems',
|
||||||
|
'minItems',
|
||||||
|
'uniqueItems',
|
||||||
|
'maxProperties',
|
||||||
|
'minProperties',
|
||||||
|
'required',
|
||||||
|
'additionalProperties',
|
||||||
|
'enum',
|
||||||
|
'format',
|
||||||
|
'const'
|
||||||
|
];
|
||||||
|
|
||||||
|
module.exports = function (metaSchema, keywordsJsonPointers) {
|
||||||
|
for (var i=0; i<keywordsJsonPointers.length; i++) {
|
||||||
|
metaSchema = JSON.parse(JSON.stringify(metaSchema));
|
||||||
|
var segments = keywordsJsonPointers[i].split('/');
|
||||||
|
var keywords = metaSchema;
|
||||||
|
var j;
|
||||||
|
for (j=1; j<segments.length; j++)
|
||||||
|
keywords = keywords[segments[j]];
|
||||||
|
|
||||||
|
for (j=0; j<KEYWORDS.length; j++) {
|
||||||
|
var key = KEYWORDS[j];
|
||||||
|
var schema = keywords[key];
|
||||||
|
if (schema) {
|
||||||
|
keywords[key] = {
|
||||||
|
anyOf: [
|
||||||
|
schema,
|
||||||
|
{ $ref: 'https://raw.githubusercontent.com/ajv-validator/ajv/master/lib/refs/data.json#' }
|
||||||
|
]
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return metaSchema;
|
||||||
|
};
|
|
@ -0,0 +1,37 @@
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
var metaSchema = require('./refs/json-schema-draft-07.json');
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
$id: 'https://github.com/ajv-validator/ajv/blob/master/lib/definition_schema.js',
|
||||||
|
definitions: {
|
||||||
|
simpleTypes: metaSchema.definitions.simpleTypes
|
||||||
|
},
|
||||||
|
type: 'object',
|
||||||
|
dependencies: {
|
||||||
|
schema: ['validate'],
|
||||||
|
$data: ['validate'],
|
||||||
|
statements: ['inline'],
|
||||||
|
valid: {not: {required: ['macro']}}
|
||||||
|
},
|
||||||
|
properties: {
|
||||||
|
type: metaSchema.properties.type,
|
||||||
|
schema: {type: 'boolean'},
|
||||||
|
statements: {type: 'boolean'},
|
||||||
|
dependencies: {
|
||||||
|
type: 'array',
|
||||||
|
items: {type: 'string'}
|
||||||
|
},
|
||||||
|
metaSchema: {type: 'object'},
|
||||||
|
modifying: {type: 'boolean'},
|
||||||
|
valid: {type: 'boolean'},
|
||||||
|
$data: {type: 'boolean'},
|
||||||
|
async: {type: 'boolean'},
|
||||||
|
errors: {
|
||||||
|
anyOf: [
|
||||||
|
{type: 'boolean'},
|
||||||
|
{const: 'full'}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
|
@ -0,0 +1,113 @@
|
||||||
|
{{# def.definitions }}
|
||||||
|
{{# def.errors }}
|
||||||
|
{{# def.setupKeyword }}
|
||||||
|
{{# def.$data }}
|
||||||
|
|
||||||
|
{{## def.setExclusiveLimit:
|
||||||
|
$exclusive = true;
|
||||||
|
$errorKeyword = $exclusiveKeyword;
|
||||||
|
$errSchemaPath = it.errSchemaPath + '/' + $exclusiveKeyword;
|
||||||
|
#}}
|
||||||
|
|
||||||
|
{{
|
||||||
|
var $isMax = $keyword == 'maximum'
|
||||||
|
, $exclusiveKeyword = $isMax ? 'exclusiveMaximum' : 'exclusiveMinimum'
|
||||||
|
, $schemaExcl = it.schema[$exclusiveKeyword]
|
||||||
|
, $isDataExcl = it.opts.$data && $schemaExcl && $schemaExcl.$data
|
||||||
|
, $op = $isMax ? '<' : '>'
|
||||||
|
, $notOp = $isMax ? '>' : '<'
|
||||||
|
, $errorKeyword = undefined;
|
||||||
|
|
||||||
|
if (!($isData || typeof $schema == 'number' || $schema === undefined)) {
|
||||||
|
throw new Error($keyword + ' must be number');
|
||||||
|
}
|
||||||
|
if (!($isDataExcl || $schemaExcl === undefined
|
||||||
|
|| typeof $schemaExcl == 'number'
|
||||||
|
|| typeof $schemaExcl == 'boolean')) {
|
||||||
|
throw new Error($exclusiveKeyword + ' must be number or boolean');
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
|
||||||
|
{{? $isDataExcl }}
|
||||||
|
{{
|
||||||
|
var $schemaValueExcl = it.util.getData($schemaExcl.$data, $dataLvl, it.dataPathArr)
|
||||||
|
, $exclusive = 'exclusive' + $lvl
|
||||||
|
, $exclType = 'exclType' + $lvl
|
||||||
|
, $exclIsNumber = 'exclIsNumber' + $lvl
|
||||||
|
, $opExpr = 'op' + $lvl
|
||||||
|
, $opStr = '\' + ' + $opExpr + ' + \'';
|
||||||
|
}}
|
||||||
|
var schemaExcl{{=$lvl}} = {{=$schemaValueExcl}};
|
||||||
|
{{ $schemaValueExcl = 'schemaExcl' + $lvl; }}
|
||||||
|
|
||||||
|
var {{=$exclusive}};
|
||||||
|
var {{=$exclType}} = typeof {{=$schemaValueExcl}};
|
||||||
|
if ({{=$exclType}} != 'boolean' && {{=$exclType}} != 'undefined' && {{=$exclType}} != 'number') {
|
||||||
|
{{ var $errorKeyword = $exclusiveKeyword; }}
|
||||||
|
{{# def.error:'_exclusiveLimit' }}
|
||||||
|
} else if ({{# def.$dataNotType:'number' }}
|
||||||
|
{{=$exclType}} == 'number'
|
||||||
|
? (
|
||||||
|
({{=$exclusive}} = {{=$schemaValue}} === undefined || {{=$schemaValueExcl}} {{=$op}}= {{=$schemaValue}})
|
||||||
|
? {{=$data}} {{=$notOp}}= {{=$schemaValueExcl}}
|
||||||
|
: {{=$data}} {{=$notOp}} {{=$schemaValue}}
|
||||||
|
)
|
||||||
|
: (
|
||||||
|
({{=$exclusive}} = {{=$schemaValueExcl}} === true)
|
||||||
|
? {{=$data}} {{=$notOp}}= {{=$schemaValue}}
|
||||||
|
: {{=$data}} {{=$notOp}} {{=$schemaValue}}
|
||||||
|
)
|
||||||
|
|| {{=$data}} !== {{=$data}}) {
|
||||||
|
var op{{=$lvl}} = {{=$exclusive}} ? '{{=$op}}' : '{{=$op}}=';
|
||||||
|
{{
|
||||||
|
if ($schema === undefined) {
|
||||||
|
$errorKeyword = $exclusiveKeyword;
|
||||||
|
$errSchemaPath = it.errSchemaPath + '/' + $exclusiveKeyword;
|
||||||
|
$schemaValue = $schemaValueExcl;
|
||||||
|
$isData = $isDataExcl;
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
{{??}}
|
||||||
|
{{
|
||||||
|
var $exclIsNumber = typeof $schemaExcl == 'number'
|
||||||
|
, $opStr = $op; /*used in error*/
|
||||||
|
}}
|
||||||
|
|
||||||
|
{{? $exclIsNumber && $isData }}
|
||||||
|
{{ var $opExpr = '\'' + $opStr + '\''; /*used in error*/ }}
|
||||||
|
if ({{# def.$dataNotType:'number' }}
|
||||||
|
( {{=$schemaValue}} === undefined
|
||||||
|
|| {{=$schemaExcl}} {{=$op}}= {{=$schemaValue}}
|
||||||
|
? {{=$data}} {{=$notOp}}= {{=$schemaExcl}}
|
||||||
|
: {{=$data}} {{=$notOp}} {{=$schemaValue}} )
|
||||||
|
|| {{=$data}} !== {{=$data}}) {
|
||||||
|
{{??}}
|
||||||
|
{{
|
||||||
|
if ($exclIsNumber && $schema === undefined) {
|
||||||
|
{{# def.setExclusiveLimit }}
|
||||||
|
$schemaValue = $schemaExcl;
|
||||||
|
$notOp += '=';
|
||||||
|
} else {
|
||||||
|
if ($exclIsNumber)
|
||||||
|
$schemaValue = Math[$isMax ? 'min' : 'max']($schemaExcl, $schema);
|
||||||
|
|
||||||
|
if ($schemaExcl === ($exclIsNumber ? $schemaValue : true)) {
|
||||||
|
{{# def.setExclusiveLimit }}
|
||||||
|
$notOp += '=';
|
||||||
|
} else {
|
||||||
|
$exclusive = false;
|
||||||
|
$opStr += '=';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var $opExpr = '\'' + $opStr + '\''; /*used in error*/
|
||||||
|
}}
|
||||||
|
|
||||||
|
if ({{# def.$dataNotType:'number' }}
|
||||||
|
{{=$data}} {{=$notOp}} {{=$schemaValue}}
|
||||||
|
|| {{=$data}} !== {{=$data}}) {
|
||||||
|
{{?}}
|
||||||
|
{{?}}
|
||||||
|
{{ $errorKeyword = $errorKeyword || $keyword; }}
|
||||||
|
{{# def.error:'_limit' }}
|
||||||
|
} {{? $breakOnError }} else { {{?}}
|
|
@ -0,0 +1,12 @@
|
||||||
|
{{# def.definitions }}
|
||||||
|
{{# def.errors }}
|
||||||
|
{{# def.setupKeyword }}
|
||||||
|
{{# def.$data }}
|
||||||
|
|
||||||
|
{{# def.numberKeyword }}
|
||||||
|
|
||||||
|
{{ var $op = $keyword == 'maxItems' ? '>' : '<'; }}
|
||||||
|
if ({{# def.$dataNotType:'number' }} {{=$data}}.length {{=$op}} {{=$schemaValue}}) {
|
||||||
|
{{ var $errorKeyword = $keyword; }}
|
||||||
|
{{# def.error:'_limitItems' }}
|
||||||
|
} {{? $breakOnError }} else { {{?}}
|
|
@ -0,0 +1,12 @@
|
||||||
|
{{# def.definitions }}
|
||||||
|
{{# def.errors }}
|
||||||
|
{{# def.setupKeyword }}
|
||||||
|
{{# def.$data }}
|
||||||
|
|
||||||
|
{{# def.numberKeyword }}
|
||||||
|
|
||||||
|
{{ var $op = $keyword == 'maxLength' ? '>' : '<'; }}
|
||||||
|
if ({{# def.$dataNotType:'number' }} {{# def.strLength }} {{=$op}} {{=$schemaValue}}) {
|
||||||
|
{{ var $errorKeyword = $keyword; }}
|
||||||
|
{{# def.error:'_limitLength' }}
|
||||||
|
} {{? $breakOnError }} else { {{?}}
|
|
@ -0,0 +1,12 @@
|
||||||
|
{{# def.definitions }}
|
||||||
|
{{# def.errors }}
|
||||||
|
{{# def.setupKeyword }}
|
||||||
|
{{# def.$data }}
|
||||||
|
|
||||||
|
{{# def.numberKeyword }}
|
||||||
|
|
||||||
|
{{ var $op = $keyword == 'maxProperties' ? '>' : '<'; }}
|
||||||
|
if ({{# def.$dataNotType:'number' }} Object.keys({{=$data}}).length {{=$op}} {{=$schemaValue}}) {
|
||||||
|
{{ var $errorKeyword = $keyword; }}
|
||||||
|
{{# def.error:'_limitProperties' }}
|
||||||
|
} {{? $breakOnError }} else { {{?}}
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue