first commit
No files matched your search
@@ -0,0 +1,63 @@
|
||||
@font-face {
|
||||
font-family: 'geist';
|
||||
src: url('../fonts/geist/geist-regular.woff2') format('woff2'),
|
||||
url('../fonts/geist/geist-regular.woff') format('woff');
|
||||
font-weight: 400;
|
||||
font-style: normal;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: 'geist';
|
||||
src: url('../fonts/geist/geist-medium.woff2') format('woff2'),
|
||||
url('../fonts/geist/geist-medium.woff') format('woff');
|
||||
font-weight: 500;
|
||||
font-style: normal;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: 'geist';
|
||||
src: url('../fonts/geist/geist-semibold.woff2') format('woff2'),
|
||||
url('../fonts/geist/geist-semibold.woff') format('woff');
|
||||
font-weight: 600;
|
||||
font-style: normal;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: 'geist';
|
||||
src: url('../fonts/geist/geist-bold.woff2') format('woff2'),
|
||||
url('../fonts/geist/geist-bold.woff') format('woff');
|
||||
font-weight: 700;
|
||||
font-style: normal;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: 'geistmono';
|
||||
src: url('../fonts/geistmono/geistmono-regular.woff2') format('woff2'),
|
||||
url('../fonts/geistmono/geistmono-regular.woff') format('woff');
|
||||
font-weight: 400;
|
||||
font-style: normal;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: 'geistmono';
|
||||
src: url('../fonts/geistmono/geistmono-medium.woff2') format('woff2'),
|
||||
url('../fonts/geistmono/geistmono-medium.woff') format('woff');
|
||||
font-weight: 500;
|
||||
font-style: normal;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: 'geistmono';
|
||||
src: url('../fonts/geistmono/geistmono-semibold.woff2') format('woff2'),
|
||||
url('../fonts/geistmono/geistmono-semibold.woff') format('woff');
|
||||
font-weight: 700;
|
||||
font-style: normal;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: 'geistmono';
|
||||
src: url('../fonts/geistmono/geistmono-bold.woff2') format('woff2'),
|
||||
url('../fonts/geistmono/geistmono-bold.woff') format('woff');
|
||||
font-weight: 700;
|
||||
font-style: normal;
|
||||
}
|
||||
@@ -0,0 +1,178 @@
|
||||
:root {
|
||||
--text-font: 'geist', sans-serif, arial;
|
||||
|
||||
--breakpoint-2xs: 480px;
|
||||
--breakpoint-xs: 640px;
|
||||
--breakpoint-sm: 768px;
|
||||
--breakpoint-md: 1024px;
|
||||
--breakpoint-lg: 1280px;
|
||||
--breakpoint-xl: 1536px;
|
||||
--breakpoint-2xl: 1920px;
|
||||
|
||||
--content-width: 100%;
|
||||
--content-width-gutter: var(--space-lg);
|
||||
|
||||
--duration-multiplier: 1; /* Allow disabling or increase animation speed */
|
||||
--duration-75: calc(75ms * var(--duration-multiplier));
|
||||
--duration-100: calc(100ms * var(--duration-multiplier));
|
||||
--duration-150: calc(150ms * var(--duration-multiplier));
|
||||
--duration-200: calc(200ms * var(--duration-multiplier));
|
||||
--duration-300: calc(300ms * var(--duration-multiplier));
|
||||
--duration-400: calc(400ms * var(--duration-multiplier));
|
||||
--duration-500: calc(500ms * var(--duration-multiplier));
|
||||
--duration-600: calc(600ms * var(--duration-multiplier));
|
||||
--duration-700: calc(700ms * var(--duration-multiplier));
|
||||
--duration-800: calc(800ms * var(--duration-multiplier));
|
||||
--duration-900: calc(900ms * var(--duration-multiplier));
|
||||
--duration-1000: calc(1000ms * var(--duration-multiplier));
|
||||
--duration-1200: calc(1200ms * var(--duration-multiplier));
|
||||
--duration-1400: calc(1400ms * var(--duration-multiplier));
|
||||
--duration-2000: calc(2000ms * var(--duration-multiplier));
|
||||
|
||||
--duration-hover: var(--duration-200);
|
||||
|
||||
/* 12.5px → 12.8px */
|
||||
--text-xs: clamp(0.7813rem, 0.7747rem + 0.0326vw, 0.8rem);
|
||||
/* 15px → 16px */
|
||||
--text-sm: clamp(0.9375rem, 0.9158rem + 0.1087vw, 1rem);
|
||||
/* 18px → 20px */
|
||||
--text-md: clamp(1.125rem, 1.0815rem + 0.2174vw, 1.25rem);
|
||||
/* 21.6px → 25px */
|
||||
--text-lg: clamp(1.35rem, 1.2761rem + 0.3696vw, 1.5625rem);
|
||||
/* 25.92px → 31.25px */
|
||||
--text-xl: clamp(1.62rem, 1.5041rem + 0.5793vw, 1.9531rem);
|
||||
/* 31.104px → 39.0625px */
|
||||
--text-2xl: clamp(1.944rem, 1.771rem + 0.8651vw, 2.4414rem);
|
||||
/* 37.3248px → 48.8281px */
|
||||
--text-3xl: clamp(2.3328rem, 2.0827rem + 1.2504vw, 3.0518rem);
|
||||
/* 44.7898px → 61.0352px */
|
||||
--text-4xl: clamp(2.7994rem, 2.4462rem + 1.7658vw, 3.8147rem);
|
||||
|
||||
/* Space 3xs: 5px → 5px */
|
||||
--space-3xs: clamp(0.3125rem, 0.3125rem + 0vw, 0.3125rem);
|
||||
/* Space 2xs: 9px → 10px */
|
||||
--space-2xs: clamp(0.5625rem, 0.5408rem + 0.1087vw, 0.625rem);
|
||||
/* Space xs: 14px → 15px */
|
||||
--space-xs: clamp(0.875rem, 0.8533rem + 0.1087vw, 0.9375rem);
|
||||
/* Space sm: 18px → 20px */
|
||||
--space-sm: clamp(1.125rem, 1.0815rem + 0.2174vw, 1.25rem);
|
||||
/* Space md: 27px → 30px */
|
||||
--space-md: clamp(1.6875rem, 1.6223rem + 0.3261vw, 1.875rem);
|
||||
/* Space lg: 36px → 40px */
|
||||
--space-lg: clamp(2.25rem, 2.163rem + 0.4348vw, 2.5rem);
|
||||
/* Space xl: 54px → 60px */
|
||||
--space-xl: clamp(3.375rem, 3.2446rem + 0.6522vw, 3.75rem);
|
||||
/* Space 2xl: 72px → 80px */
|
||||
--space-2xl: clamp(4.5rem, 4.3261rem + 0.8696vw, 5rem);
|
||||
/* Space 3xl: 108px → 120px */
|
||||
--space-3xl: clamp(6.75rem, 6.4891rem + 1.3043vw, 7.5rem);
|
||||
|
||||
/* One-up pairs */
|
||||
/* Space 3xs-2xs: 5px → 10px */
|
||||
--space-3xs-2xs: clamp(0.3125rem, 0.2038rem + 0.5435vw, 0.625rem);
|
||||
/* Space 2xs-xs: 9px → 15px */
|
||||
--space-2xs-xs: clamp(0.5625rem, 0.4321rem + 0.6522vw, 0.9375rem);
|
||||
/* Space xs-sm: 14px → 20px */
|
||||
--space-xs-sm: clamp(0.875rem, 0.7446rem + 0.6522vw, 1.25rem);
|
||||
/* Space s-m: 18px → 30px */
|
||||
--space-sm-md: clamp(1.125rem, 0.8641rem + 1.3043vw, 1.875rem);
|
||||
/* Space md-lg: 27px → 40px */
|
||||
--space-md-lg: clamp(1.6875rem, 1.4049rem + 1.413vw, 2.5rem);
|
||||
/* Space lg-xl: 36px → 60px */
|
||||
--space-lg-xl: clamp(2.25rem, 1.7283rem + 2.6087vw, 3.75rem);
|
||||
/* Space xl-2xl: 54px → 80px */
|
||||
--space-xl-2xl: clamp(3.375rem, 2.8098rem + 2.8261vw, 5rem);
|
||||
/* Space 2xl-3xl: 72px → 120px */
|
||||
--space-2xl-3xl: clamp(4.5rem, 3.4565rem + 5.2174vw, 7.5rem);
|
||||
|
||||
/* Rounded borders */
|
||||
--rounded-sm: 4px;
|
||||
--rounded-md: 8px;
|
||||
--rounded-lg: 16px;
|
||||
--rounded-xl: 32px;
|
||||
--rounded-full: 9999px;
|
||||
}
|
||||
|
||||
:root {
|
||||
/* Brand */
|
||||
--color-primary: #6d2eb8;
|
||||
--color-primary-variant: #d9bbff;
|
||||
--color-primary-hover: #8848d6;
|
||||
--color-accent: #bb86fc;
|
||||
--color-white: #ffffff;
|
||||
--color-black: #000000;
|
||||
|
||||
/* Surfaces */
|
||||
--color-surface: #0b0b0b; /* Elevation 1 */
|
||||
--color-surface-variant: #0b0b0b; /* Elevation 1 */
|
||||
--color-surface-high: #191919; /* Elevation 2 */
|
||||
--color-surface-high-hover: var(--color-surface-higher);
|
||||
--color-surface-higher: #333333; /* Elevation 3 */
|
||||
--color-surface-higher-hover: #484848; /* Elevation 3 */
|
||||
|
||||
/* Glyphs */
|
||||
--color-on-surface: rgb(221, 221, 221);
|
||||
--color-on-surface-high: rgb(236, 236, 236);
|
||||
--color-on-surface-higher: #ffffff;
|
||||
|
||||
--color-border: rgba(125, 125, 125, 0.9);
|
||||
--color-border-hover: rgba(137, 137, 137, 0.9);
|
||||
--color-border-selected: rgba(161, 161, 161, 0.9);
|
||||
|
||||
/* States */
|
||||
--color-success: #199b1e;
|
||||
--color-error: #d24340;
|
||||
--color-warning: #d59600;
|
||||
--color-info: #1888e4;
|
||||
|
||||
/* Shadows */
|
||||
--shadow-1: rgba(0, 0, 0, 0.1) 0px 20px 25px -5px, rgba(0, 0, 0, 0.04) 0px 10px 10px -5px;
|
||||
--shadow-2: rgba(0, 0, 0, 0.1) 0px 20px 29px -4px, rgba(0, 0, 0, 0.04) 0px 10px 60px -5px;
|
||||
--shadow-3: rgba(0, 0, 0, 0.09) 0px 2px 1px, rgba(0, 0, 0, 0.09) 0px 4px 8px, rgb(0 0 0 / 3%) 0px 8px 11px, rgb(0 0 0 / 5%) 0px 11px 12px 4px, rgba(0, 0, 0, 0.09) 0px 32px 16px;
|
||||
|
||||
/* Misc. */
|
||||
--filter-invert: 0; /* For non-variable shades applied */
|
||||
--subpage-shade-x-color: var(--color-surface); /* The shade around the subpage container */
|
||||
--subpage-title-shade: linear-gradient(275deg, rgb(0 0 0 / 17%) 0%, rgba(0, 0, 0, 0.01) 75%, rgba(0, 0, 0, 0) 100%);
|
||||
--surface-shade: linear-gradient(
|
||||
to bottom right,
|
||||
hsl(0deg 0% 50% / 2%),
|
||||
hsl(0deg 0% 0% / 39%)
|
||||
); /* Subpage, dialog, toast container surface */
|
||||
}
|
||||
|
||||
html.theme-light {
|
||||
/* Brand */
|
||||
--color-primary: #007f6e;
|
||||
--color-primary-variant: #015a4e;
|
||||
--color-primary-hover: #005b4f;
|
||||
--color-accent: #0bb79f;
|
||||
/* Surfaces */
|
||||
--color-surface: #fff;
|
||||
--color-surface-variant: #e6e6e6;
|
||||
--color-surface-high: #f5f5f5;
|
||||
--color-surface-higher: #e3e3e3;
|
||||
--color-surface-higher-hover: #cfcfcf;
|
||||
|
||||
/* Glyphs */
|
||||
--color-on-surface: #333333;
|
||||
--color-on-surface-high: #2b2b2b;
|
||||
--color-on-surface-higher: #121212;
|
||||
|
||||
--color-border: rgba(90, 90, 90, 0.9);
|
||||
--color-border-hover: rgba(121, 121, 121, 0.9);
|
||||
--color-border-selected: rgba(114, 114, 114, 0.9);
|
||||
|
||||
|
||||
/* States */
|
||||
--color-success: #388e3c;
|
||||
--color-error: #d32f2f;
|
||||
--color-warning: #fbc02d;
|
||||
--color-info: #0288d1;
|
||||
|
||||
/* Misc. */
|
||||
--filter-invert: 1;
|
||||
--subpage-shade-x-color: var(--color-surface-variant);
|
||||
--subpage-title-shade: linear-gradient(275deg, rgb(247 247 247 / 39%) 0%, rgb(255 255 255 / 65%) 75%, rgb(255 255 255) 100%);
|
||||
--surface-shade: linear-gradient(to bottom right, hsl(0deg 0% 100%), hsl(0deg 0% 93.94%));
|
||||
}
|
||||
@@ -0,0 +1,93 @@
|
||||
Copyright 2024 The Geist Project Authors (https://github.com/vercel/geist-font.git)
|
||||
|
||||
This Font Software is licensed under the SIL Open Font License, Version 1.1.
|
||||
This license is copied below, and is also available with a FAQ at:
|
||||
https://openfontlicense.org
|
||||
|
||||
|
||||
-----------------------------------------------------------
|
||||
SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
|
||||
-----------------------------------------------------------
|
||||
|
||||
PREAMBLE
|
||||
The goals of the Open Font License (OFL) are to stimulate worldwide
|
||||
development of collaborative font projects, to support the font creation
|
||||
efforts of academic and linguistic communities, and to provide a free and
|
||||
open framework in which fonts may be shared and improved in partnership
|
||||
with others.
|
||||
|
||||
The OFL allows the licensed fonts to be used, studied, modified and
|
||||
redistributed freely as long as they are not sold by themselves. The
|
||||
fonts, including any derivative works, can be bundled, embedded,
|
||||
redistributed and/or sold with any software provided that any reserved
|
||||
names are not used by derivative works. The fonts and derivatives,
|
||||
however, cannot be released under any other type of license. The
|
||||
requirement for fonts to remain under this license does not apply
|
||||
to any document created using the fonts or their derivatives.
|
||||
|
||||
DEFINITIONS
|
||||
"Font Software" refers to the set of files released by the Copyright
|
||||
Holder(s) under this license and clearly marked as such. This may
|
||||
include source files, build scripts and documentation.
|
||||
|
||||
"Reserved Font Name" refers to any names specified as such after the
|
||||
copyright statement(s).
|
||||
|
||||
"Original Version" refers to the collection of Font Software components as
|
||||
distributed by the Copyright Holder(s).
|
||||
|
||||
"Modified Version" refers to any derivative made by adding to, deleting,
|
||||
or substituting -- in part or in whole -- any of the components of the
|
||||
Original Version, by changing formats or by porting the Font Software to a
|
||||
new environment.
|
||||
|
||||
"Author" refers to any designer, engineer, programmer, technical
|
||||
writer or other person who contributed to the Font Software.
|
||||
|
||||
PERMISSION & CONDITIONS
|
||||
Permission is hereby granted, free of charge, to any person obtaining
|
||||
a copy of the Font Software, to use, study, copy, merge, embed, modify,
|
||||
redistribute, and sell modified and unmodified copies of the Font
|
||||
Software, subject to the following conditions:
|
||||
|
||||
1) Neither the Font Software nor any of its individual components,
|
||||
in Original or Modified Versions, may be sold by itself.
|
||||
|
||||
2) Original or Modified Versions of the Font Software may be bundled,
|
||||
redistributed and/or sold with any software, provided that each copy
|
||||
contains the above copyright notice and this license. These can be
|
||||
included either as stand-alone text files, human-readable headers or
|
||||
in the appropriate machine-readable metadata fields within text or
|
||||
binary files as long as those fields can be easily viewed by the user.
|
||||
|
||||
3) No Modified Version of the Font Software may use the Reserved Font
|
||||
Name(s) unless explicit written permission is granted by the corresponding
|
||||
Copyright Holder. This restriction only applies to the primary font name as
|
||||
presented to the users.
|
||||
|
||||
4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
|
||||
Software shall not be used to promote, endorse or advertise any
|
||||
Modified Version, except to acknowledge the contribution(s) of the
|
||||
Copyright Holder(s) and the Author(s) or with their explicit written
|
||||
permission.
|
||||
|
||||
5) The Font Software, modified or unmodified, in part or in whole,
|
||||
must be distributed entirely under this license, and must not be
|
||||
distributed under any other license. The requirement for fonts to
|
||||
remain under this license does not apply to any document created
|
||||
using the Font Software.
|
||||
|
||||
TERMINATION
|
||||
This license becomes null and void if any of the above conditions are
|
||||
not met.
|
||||
|
||||
DISCLAIMER
|
||||
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
|
||||
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
|
||||
OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
|
||||
COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
||||
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
|
||||
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
|
||||
OTHER DEALINGS IN THE FONT SOFTWARE.
|
||||
@@ -0,0 +1,93 @@
|
||||
Copyright 2024 The Geist Project Authors (https://github.com/vercel/geist-font.git)
|
||||
|
||||
This Font Software is licensed under the SIL Open Font License, Version 1.1.
|
||||
This license is copied below, and is also available with a FAQ at:
|
||||
https://openfontlicense.org
|
||||
|
||||
|
||||
-----------------------------------------------------------
|
||||
SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
|
||||
-----------------------------------------------------------
|
||||
|
||||
PREAMBLE
|
||||
The goals of the Open Font License (OFL) are to stimulate worldwide
|
||||
development of collaborative font projects, to support the font creation
|
||||
efforts of academic and linguistic communities, and to provide a free and
|
||||
open framework in which fonts may be shared and improved in partnership
|
||||
with others.
|
||||
|
||||
The OFL allows the licensed fonts to be used, studied, modified and
|
||||
redistributed freely as long as they are not sold by themselves. The
|
||||
fonts, including any derivative works, can be bundled, embedded,
|
||||
redistributed and/or sold with any software provided that any reserved
|
||||
names are not used by derivative works. The fonts and derivatives,
|
||||
however, cannot be released under any other type of license. The
|
||||
requirement for fonts to remain under this license does not apply
|
||||
to any document created using the fonts or their derivatives.
|
||||
|
||||
DEFINITIONS
|
||||
"Font Software" refers to the set of files released by the Copyright
|
||||
Holder(s) under this license and clearly marked as such. This may
|
||||
include source files, build scripts and documentation.
|
||||
|
||||
"Reserved Font Name" refers to any names specified as such after the
|
||||
copyright statement(s).
|
||||
|
||||
"Original Version" refers to the collection of Font Software components as
|
||||
distributed by the Copyright Holder(s).
|
||||
|
||||
"Modified Version" refers to any derivative made by adding to, deleting,
|
||||
or substituting -- in part or in whole -- any of the components of the
|
||||
Original Version, by changing formats or by porting the Font Software to a
|
||||
new environment.
|
||||
|
||||
"Author" refers to any designer, engineer, programmer, technical
|
||||
writer or other person who contributed to the Font Software.
|
||||
|
||||
PERMISSION & CONDITIONS
|
||||
Permission is hereby granted, free of charge, to any person obtaining
|
||||
a copy of the Font Software, to use, study, copy, merge, embed, modify,
|
||||
redistribute, and sell modified and unmodified copies of the Font
|
||||
Software, subject to the following conditions:
|
||||
|
||||
1) Neither the Font Software nor any of its individual components,
|
||||
in Original or Modified Versions, may be sold by itself.
|
||||
|
||||
2) Original or Modified Versions of the Font Software may be bundled,
|
||||
redistributed and/or sold with any software, provided that each copy
|
||||
contains the above copyright notice and this license. These can be
|
||||
included either as stand-alone text files, human-readable headers or
|
||||
in the appropriate machine-readable metadata fields within text or
|
||||
binary files as long as those fields can be easily viewed by the user.
|
||||
|
||||
3) No Modified Version of the Font Software may use the Reserved Font
|
||||
Name(s) unless explicit written permission is granted by the corresponding
|
||||
Copyright Holder. This restriction only applies to the primary font name as
|
||||
presented to the users.
|
||||
|
||||
4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
|
||||
Software shall not be used to promote, endorse or advertise any
|
||||
Modified Version, except to acknowledge the contribution(s) of the
|
||||
Copyright Holder(s) and the Author(s) or with their explicit written
|
||||
permission.
|
||||
|
||||
5) The Font Software, modified or unmodified, in part or in whole,
|
||||
must be distributed entirely under this license, and must not be
|
||||
distributed under any other license. The requirement for fonts to
|
||||
remain under this license does not apply to any document created
|
||||
using the Font Software.
|
||||
|
||||
TERMINATION
|
||||
This license becomes null and void if any of the above conditions are
|
||||
not met.
|
||||
|
||||
DISCLAIMER
|
||||
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
|
||||
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
|
||||
OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
|
||||
COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
||||
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
|
||||
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
|
||||
OTHER DEALINGS IN THE FONT SOFTWARE.
|
||||
|
After Width: | Height: | Size: 18 KiB |
|
After Width: | Height: | Size: 69 KiB |
|
After Width: | Height: | Size: 22 KiB |
|
After Width: | Height: | Size: 791 B |
|
After Width: | Height: | Size: 1.9 KiB |
|
After Width: | Height: | Size: 127 KiB |
|
After Width: | Height: | Size: 5.5 KiB |
@@ -0,0 +1,272 @@
|
||||
async function preProcessImage(file) {
|
||||
let preProcessedImage = null;
|
||||
let preProcessedNewFileType = null;
|
||||
|
||||
if (file.type === "image/heic" || file.type === "image/heif" || isHeicExt(file)) {
|
||||
console.log('Pre-processing HEIC image...')
|
||||
|
||||
preProcessedImage = await HeicTo({
|
||||
blob: file,
|
||||
type: "image/jpeg",
|
||||
quality: 0.9,
|
||||
});
|
||||
|
||||
console.log("preProcessedImage: ", preProcessedImage);
|
||||
|
||||
preProcessedNewFileType = "image/jpeg";
|
||||
}
|
||||
|
||||
if (file.type === "image/avif") {
|
||||
console.log('Pre-processing AVIF image...')
|
||||
|
||||
setTimeout(() => {
|
||||
ui.progress.text.innerHTML = `Please wait. AVIF files may take longer to prepare<span class="loading-dots">`;
|
||||
}, 5000);
|
||||
|
||||
preProcessedImage = await imageCompression(file, {
|
||||
quality: 0.8,
|
||||
fileType: "image/jpeg",
|
||||
useWebWorker: true,
|
||||
preserveExif: false,
|
||||
libURL: "./browser-image-compression.js",
|
||||
alwaysKeepResolution: true,
|
||||
});
|
||||
|
||||
preProcessedNewFileType = "image/jpeg";
|
||||
}
|
||||
|
||||
return { preProcessedImage, preProcessedNewFileType };
|
||||
}
|
||||
|
||||
async function createCompressionOptions(onProgress, file) {
|
||||
const compressMethod = getCheckedValue(ui.inputs.compressMethod);
|
||||
const dimensionMethod = getCheckedValue(ui.inputs.dimensionMethod);
|
||||
const maxWeight = parseFloat(ui.inputs.limitWeight.value);
|
||||
const { selectedFormat } = getFileType(file);
|
||||
|
||||
quality = Math.min(Math.max(parseFloat(ui.inputs.quality.value) / 100, 0), 1);
|
||||
|
||||
console.log("Input image file size: ", (file.size / 1024 / 1024).toFixed(3), "MB");
|
||||
|
||||
let maxWeightMB = ui.inputs.limitWeightUnit.value.toUpperCase() === "KB" ?
|
||||
ui.inputs.limitWeight.value / 1024 :
|
||||
ui.inputs.limitWeight.value;
|
||||
|
||||
let limitDimensionsValue = undefined;
|
||||
|
||||
if (file.type === "image/heif" || file.type === "image/heic" || isHeicExt(file)) {
|
||||
if (getCheckedValue(ui.inputs.dimensionMethod) === "limit") {
|
||||
limitDimensionsValue = (ui.inputs.limitDimensions.value > 50) ? ui.inputs.limitDimensions.value : 50;
|
||||
}
|
||||
else {
|
||||
limitDimensionsValue = undefined;
|
||||
}
|
||||
}
|
||||
else {
|
||||
limitDimensionsValue = dimensionMethod === "limit" ?
|
||||
await getAdjustedDimensions(file, ui.inputs.limitDimensions.value) :
|
||||
undefined;
|
||||
}
|
||||
|
||||
|
||||
const options = {
|
||||
maxSizeMB: maxWeight && compressMethod === "limitWeight" ? maxWeightMB : (file.size / 1024 / 1024).toFixed(3),
|
||||
initialQuality: quality && compressMethod === "quality" ? quality : undefined,
|
||||
maxWidthOrHeight: limitDimensionsValue,
|
||||
useWebWorker: true,
|
||||
onProgress,
|
||||
preserveExif: false,
|
||||
fileType: selectedFormat || undefined,
|
||||
libURL: "./browser-image-compression.js",
|
||||
alwaysKeepResolution: true,
|
||||
};
|
||||
if (state.controller) {
|
||||
options.signal = state.controller.signal;
|
||||
}
|
||||
|
||||
console.log("Settings:", options);
|
||||
return options;
|
||||
}
|
||||
|
||||
|
||||
async function compressImageQueue() {
|
||||
if (!state.compressQueue.length) {
|
||||
resetCompressionState(true);
|
||||
return;
|
||||
}
|
||||
|
||||
const file = state.compressQueue[0];
|
||||
const i = state.compressProcessedCount;
|
||||
|
||||
console.log('Input file: ', file);
|
||||
|
||||
if (!isFileTypeSupported(file.type, file)) {
|
||||
console.error(`Unsupported file type: ${file.type}. Skipping "${file.name}".`);
|
||||
ui.progress.text.innerHTML = `Unsupported file "<div class='progress-file-name'>${file.name}</div>"`;
|
||||
state.compressQueue.shift();
|
||||
await compressImageQueue();
|
||||
return;
|
||||
}
|
||||
|
||||
const options = await createCompressionOptions((p) => onProgress(p, i, file.name), file);
|
||||
const { preProcessedImage, preProcessedNewFileType } = await preProcessImage(file);
|
||||
|
||||
if (preProcessedImage) {
|
||||
options.fileType = preProcessedNewFileType;
|
||||
}
|
||||
|
||||
imageCompression(preProcessedImage || file, options)
|
||||
.then((output) => handleCompressionResult(file, output))
|
||||
.catch((error) => console.error(error.message))
|
||||
.finally(() => {
|
||||
state.compressProcessedCount++;
|
||||
state.compressQueue.shift();
|
||||
resetCompressionState(state.compressProcessedCount === state.compressQueueTotal);
|
||||
if (state.compressProcessedCount < state.compressQueueTotal) {
|
||||
compressImageQueue();
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
function onProgress(p, index, fileName) {
|
||||
const overallProgress = calculateOverallProgress(
|
||||
state.fileProgressMap,
|
||||
state.compressQueueTotal
|
||||
);
|
||||
const fileNameShort =
|
||||
fileName.length > 15 ? fileName.slice(0, 12) + "..." : fileName;
|
||||
state.fileProgressMap[index] = p;
|
||||
|
||||
ui.progress.queueCount.textContent = `${
|
||||
state.compressProcessedCount + 1
|
||||
} / ${state.compressQueueTotal}`;
|
||||
ui.progress.text.dataset.progress = overallProgress;
|
||||
ui.progress.text.innerHTML = `Optimizing "<div class='progress-file-name'>${fileName}</div>"`;
|
||||
ui.progress.bar.style.width = overallProgress + "%";
|
||||
console.log(`Optimizing "${fileNameShort}" (${overallProgress}%)`);
|
||||
|
||||
if (p === 100 && state.compressProcessedCount === state.compressQueueTotal - 1) {
|
||||
ui.progress.text.innerHTML = `
|
||||
<div class="badge badge--success pt-2xs pb-2xs bg:surface">
|
||||
<div class="badge-text flex items-center gap-3xs">
|
||||
<svg height="16" stroke-linejoin="round" viewBox="0 0 16 16" width="16" style="color: currentcolor;"><path fill-rule="evenodd" clip-rule="evenodd" d="M14.5 8C14.5 11.5899 11.5899 14.5 8 14.5C4.41015 14.5 1.5 11.5899 1.5 8C1.5 4.41015 4.41015 1.5 8 1.5C11.5899 1.5 14.5 4.41015 14.5 8ZM16 8C16 12.4183 12.4183 16 8 16C3.58172 16 0 12.4183 0 8C0 3.58172 3.58172 0 8 0C12.4183 0 16 3.58172 16 8ZM11.5303 6.53033L12.0607 6L11 4.93934L10.4697 5.46967L6.5 9.43934L5.53033 8.46967L5 7.93934L3.93934 9L4.46967 9.53033L5.96967 11.0303C6.26256 11.3232 6.73744 11.3232 7.03033 11.0303L11.5303 6.53033Z" fill="currentColor"></path></svg>
|
||||
<span>Done!</span>
|
||||
</div>
|
||||
<div>
|
||||
`;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function compressImage(event) {
|
||||
state.controller = new AbortController();
|
||||
state.compressQueue = Array.from(event.target.files);
|
||||
state.compressQueueTotal = state.compressQueue.length;
|
||||
state.compressProcessedCount = 0;
|
||||
state.fileProgressMap = {};
|
||||
state.isCompressing = true;
|
||||
|
||||
document.body.classList.add("compressing--is-active");
|
||||
ui.actions.dropZone.classList.add("hidden");
|
||||
ui.actions.abort.classList.remove("hidden");
|
||||
ui.progress.container.classList.remove("hidden");
|
||||
ui.progress.text.innerHTML = `Preparing<span class="loading-dots">`;
|
||||
|
||||
compressImageQueue();
|
||||
}
|
||||
|
||||
|
||||
function handleCompressionResult(file, output) {
|
||||
const { outputFileExtension, selectedFormat } = getFileType(file);
|
||||
const outputImageBlob = URL.createObjectURL(output);
|
||||
|
||||
const { renamedFileName, isBrowserDefaultFileName } = renameBrowserDefaultFileName(file.name);
|
||||
const outputFileNameText = updateFileExtension(
|
||||
isBrowserDefaultFileName ? renamedFileName : file.name,
|
||||
outputFileExtension,
|
||||
selectedFormat
|
||||
);
|
||||
|
||||
const inputFileSize = parseFloat((file.size / 1024 / 1024).toFixed(3));
|
||||
const outputFileSize = parseFloat((output.size / 1024 / 1024).toFixed(3));
|
||||
const fileSizeSaved = inputFileSize - outputFileSize;
|
||||
const fileSizeSavedPercentage =
|
||||
inputFileSize > 0
|
||||
? Math.abs(((fileSizeSaved / inputFileSize) * 100).toFixed(2))
|
||||
: "0.000";
|
||||
const fileSizeSavedTrend =
|
||||
fileSizeSaved < 0 ? "+" : fileSizeSaved > 0 ? "-" : "";
|
||||
const fileSizeSavedClass =
|
||||
fileSizeSaved <= 0 ? "badge--error" : "badge--success";
|
||||
|
||||
imageCompression(output, config.thumbnailOptions).then((thumbnailBlob) => {
|
||||
const thumbnailDataURL = URL.createObjectURL(thumbnailBlob);
|
||||
getImageDimensions(outputImageBlob, ({ width, height }) => {
|
||||
const outputHTML = buildOutputItemHTML({
|
||||
outputImageBlob,
|
||||
thumbnailDataURL,
|
||||
outputFileNameText,
|
||||
outputFileExtension,
|
||||
width,
|
||||
height,
|
||||
fileSize: output.size,
|
||||
fileSizeSavedTrend,
|
||||
fileSizeSavedPercentage,
|
||||
fileSizeSavedClass,
|
||||
});
|
||||
const wrapper = document.createElement("div");
|
||||
wrapper.innerHTML = outputHTML.trim();
|
||||
ui.output.content.prepend(wrapper.firstChild);
|
||||
|
||||
state.outputImageCount++;
|
||||
ui.output.container.dataset.count = state.outputImageCount;
|
||||
ui.output.subpageOutput.dataset.count = state.outputImageCount;
|
||||
ui.output.imageCount.dataset.count = state.outputImageCount;
|
||||
ui.output.imageCount.textContent = state.outputImageCount;
|
||||
|
||||
if (state.compressProcessedCount === 1) {
|
||||
selectSubpage("output");
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function calculateOverallProgress(progressMap, totalFiles) {
|
||||
const sum = Object.values(progressMap).reduce((acc, val) => acc + val, 0);
|
||||
return Math.round(sum / totalFiles);
|
||||
}
|
||||
|
||||
function resetCompressionState(isAllProcessed, aborted) {
|
||||
const resetState = () => {
|
||||
state.compressProcessedCount = 0;
|
||||
state.compressQueueTotal = 0;
|
||||
ui.progress.queueCount.textContent = "";
|
||||
state.compressQueue = [];
|
||||
state.isCompressing = false;
|
||||
};
|
||||
|
||||
if (aborted) {
|
||||
resetUI();
|
||||
resetState();
|
||||
return;
|
||||
}
|
||||
|
||||
if (isAllProcessed) {
|
||||
ui.actions.abort.classList.add("hidden");
|
||||
ui.progress.bar.style.width = "100%";
|
||||
|
||||
setTimeout(() => {
|
||||
// Delay state reset to allow "Done" message to remain
|
||||
resetUI();
|
||||
state.isCompressing = false;
|
||||
}, 1000);
|
||||
return;
|
||||
}
|
||||
|
||||
if (state.isCompressing && state.compressProcessedCount === 0) {
|
||||
ui.progress.text.dataset.progress = 0;
|
||||
ui.progress.text.textContent = "Preparing 0%";
|
||||
ui.progress.bar.style.width = "0%";
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,99 @@
|
||||
|
||||
async function downloadAllImages() {
|
||||
const GB = 1024 * 1024 * 1024;
|
||||
const chunkSize = 1 * GB;
|
||||
const zipFileName = appendFileNameId("mazanoke-images");
|
||||
|
||||
try {
|
||||
if (state.isDownloadingAll) return;
|
||||
state.isDownloadingAll = true;
|
||||
ui.actions.downloadAll.setAttribute("aria-busy", "true");
|
||||
|
||||
const compressedImages = document.querySelectorAll(
|
||||
'a.image-output__item-download-button[href^="blob:"]'
|
||||
);
|
||||
const blobs = await Promise.all(
|
||||
Array.from(compressedImages).map(async (link, index) => {
|
||||
try {
|
||||
const response = await fetch(link.href);
|
||||
if (!response.ok)
|
||||
throw new Error(`Failed to fetch image ${index + 1}`);
|
||||
return await response.blob();
|
||||
} catch (error) {
|
||||
console.error(`Error downloading image ${index + 1}:`, error);
|
||||
return null;
|
||||
}
|
||||
})
|
||||
);
|
||||
|
||||
const validBlobs = blobs.filter((blob) => blob !== null);
|
||||
|
||||
if (validBlobs.length === 0) {
|
||||
throw new Error("No valid images to download");
|
||||
}
|
||||
|
||||
let currentZip = zip;
|
||||
let totalSize = 0;
|
||||
let zipIndex = 1;
|
||||
|
||||
for (let i = 0; i < validBlobs.length; i++) {
|
||||
const fileSize = parseInt(compressedImages[i].dataset.filesize, 10);
|
||||
|
||||
if (totalSize + fileSize > chunkSize) {
|
||||
const zipBlob = await currentZip.generateAsync({ type: "blob" });
|
||||
await triggerDownload(
|
||||
zipBlob,
|
||||
`${zipFileName}-${zipIndex.toString().padStart(3, "0")}.zip`
|
||||
);
|
||||
|
||||
currentZip = new JSZip();
|
||||
totalSize = 0;
|
||||
zipIndex++;
|
||||
}
|
||||
|
||||
currentZip.file(compressedImages[i].download, validBlobs[i]);
|
||||
totalSize += fileSize;
|
||||
}
|
||||
|
||||
if (totalSize > 0) {
|
||||
const finalName =
|
||||
zipIndex === 1
|
||||
? `${zipFileName}.zip`
|
||||
: `${zipFileName}-${zipIndex.toString().padStart(3, "0")}.zip`;
|
||||
const zipBlob = await currentZip.generateAsync({ type: "blob" });
|
||||
await triggerDownload(zipBlob, finalName);
|
||||
}
|
||||
}
|
||||
catch (error) {
|
||||
console.error("Download all images as zip failed:", error);
|
||||
}
|
||||
finally {
|
||||
ui.actions.downloadAll.setAttribute("aria-busy", "false");
|
||||
state.isDownloadingAll = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function triggerDownload(blob, filename) {
|
||||
return new Promise((resolve) => {
|
||||
const link = document.createElement("a");
|
||||
link.href = URL.createObjectURL(blob);
|
||||
link.download = filename;
|
||||
document.body.appendChild(link);
|
||||
link.click();
|
||||
|
||||
setTimeout(() => {
|
||||
document.body.removeChild(link);
|
||||
URL.revokeObjectURL(link.href);
|
||||
resolve();
|
||||
}, 100);
|
||||
});
|
||||
}
|
||||
|
||||
function deleteAllImages() {
|
||||
ui.output.content.innerHTML = "";
|
||||
ui.output.container.dataset.count = 0;
|
||||
ui.output.subpageOutput.dataset.count = 0;
|
||||
ui.output.imageCount.dataset.count = 0;
|
||||
ui.output.imageCount.textContent = 0;
|
||||
state.outputImageCount = 0;
|
||||
}
|
||||
@@ -0,0 +1,128 @@
|
||||
initApp();
|
||||
|
||||
function initApp() {
|
||||
// Initialize the app
|
||||
initDropZone();
|
||||
initInputValidation();
|
||||
initClipboardPaste();
|
||||
initBackToTop();
|
||||
setConfigForm();
|
||||
restoreConfigForm();
|
||||
}
|
||||
|
||||
|
||||
function initDropZone() {
|
||||
const dropZone = ui.groups.dropZone;
|
||||
const fileInput = ui.inputs.file;
|
||||
const compressingGuard = (handler) => (e) => {
|
||||
// Prevent adding more to compression queue when isCompressing.
|
||||
if (state.isCompressing) return;
|
||||
handler(e);
|
||||
};
|
||||
|
||||
dropZone.addEventListener("click", compressingGuard(() => fileInput.click()));
|
||||
|
||||
fileInput.addEventListener("change", compressingGuard((e) => {
|
||||
if (fileInput.files?.length) {
|
||||
compressImage(e);
|
||||
fileInput.value = "";
|
||||
}
|
||||
}));
|
||||
|
||||
const toggleDragging = (add) => dropZone.classList.toggle("drop-zone--is-dragging", add);
|
||||
|
||||
dropZone.addEventListener("dragenter", compressingGuard((e) => {
|
||||
e.preventDefault();
|
||||
toggleDragging(true);
|
||||
}));
|
||||
|
||||
dropZone.addEventListener("dragover", compressingGuard((e) => {
|
||||
e.preventDefault();
|
||||
toggleDragging(true);
|
||||
}));
|
||||
|
||||
dropZone.addEventListener("dragleave", compressingGuard((e) => {
|
||||
e.preventDefault();
|
||||
toggleDragging(false);
|
||||
}));
|
||||
|
||||
dropZone.addEventListener("drop", compressingGuard((e) => {
|
||||
e.preventDefault();
|
||||
toggleDragging(false);
|
||||
if (e.dataTransfer.files?.length) {
|
||||
fileInput.files = e.dataTransfer.files;
|
||||
compressImage({ target: fileInput }, true);
|
||||
fileInput.value = "";
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
function initInputValidation() {
|
||||
ui.inputs.quality.addEventListener("change", () => {
|
||||
setQuality(ui.inputs.quality.value);
|
||||
});
|
||||
|
||||
ui.inputs.limitDimensions.addEventListener("change", (e) => {
|
||||
setLimitDimensions(ui.inputs.limitDimensions.value);
|
||||
});
|
||||
|
||||
ui.inputs.limitWeight.addEventListener("change", (e) => {
|
||||
setWeight(ui.inputs.limitWeight.value, ui.inputs.limitWeightUnit.value);
|
||||
});
|
||||
|
||||
ui.inputs.limitWeightUnit.addEventListener("change", (e) => {
|
||||
setWeightUnit(e.target.value);
|
||||
});
|
||||
}
|
||||
|
||||
function initClipboardPaste() {
|
||||
document.addEventListener("paste", handlePasteImage);
|
||||
}
|
||||
|
||||
function initBackToTop() {
|
||||
ui.actions.backToTop.addEventListener("click", function () {
|
||||
window.scrollTo({
|
||||
top: 0,
|
||||
behavior: "smooth",
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function setConfigForm() {
|
||||
// Default values of form fields, or for restoring local storage values.
|
||||
setQuality(config.form.quality.value);
|
||||
setLimitDimensions(config.form.limitDimensions.value);
|
||||
setWeightUnit(config.form.limitWeightUnit.value);
|
||||
setWeight(config.form.limitWeight.value, config.form.limitWeightUnit.value);
|
||||
setCompressMethod(config.form.compressMethod.value);
|
||||
setDimensionMethod(config.form.dimensionMethod.value);
|
||||
setConvertMethod(config.form.convertMethod.value);
|
||||
}
|
||||
|
||||
function handlePasteImage(e) {
|
||||
if (!e.clipboardData || state.isCompressing) return;
|
||||
|
||||
const items = e.clipboardData.items;
|
||||
const files = [];
|
||||
|
||||
for (let i = 0; i < items.length; i++) {
|
||||
const item = items[i];
|
||||
|
||||
if (item.kind === "file" && item.type.startsWith("image/")) {
|
||||
files.push(item.getAsFile());
|
||||
}
|
||||
}
|
||||
|
||||
if (files.length) {
|
||||
compressImage({ target: { files } });
|
||||
}
|
||||
}
|
||||
|
||||
function abort(event) {
|
||||
// Cancel on-going compression.
|
||||
event.stopPropagation();
|
||||
if (!state.controller) return;
|
||||
resetCompressionState(false, true);
|
||||
state.controller.abort(new Error("Image compression cancelled"));
|
||||
}
|
||||
|
||||
@@ -0,0 +1,106 @@
|
||||
window.App = window.App || {};
|
||||
|
||||
App.ui = {
|
||||
dialogs: {
|
||||
installPWA: document.getElementById("installPWADialog"),
|
||||
updateToast: document.getElementById("updateToast"),
|
||||
updateToastRefreshButton: document.getElementById("updateToastRefreshButton"),
|
||||
},
|
||||
inputs: {
|
||||
quality: document.getElementById("quality"),
|
||||
limitDimensions: document.getElementById("limitDimensions"),
|
||||
limitWeight: document.getElementById("limitWeight"),
|
||||
limitWeightUnit: document.getElementById("limitWeightUnit"),
|
||||
compressMethod: document.querySelectorAll('[name="compressMethod"]'),
|
||||
dimensionMethod: document.querySelectorAll('[name="dimensionMethod"]'),
|
||||
formatSelect: document.querySelectorAll('[name="formatSelect"]'),
|
||||
file: document.getElementById("compress"),
|
||||
settingsSubpage: document.querySelectorAll('[name="settingsSubpage"]'),
|
||||
},
|
||||
labels: {
|
||||
limitWeightSuffix: document.querySelector('label[for="limitWeight"][data-suffix]'),
|
||||
},
|
||||
progress: {
|
||||
container: document.querySelector(".progress-container"),
|
||||
queueCount: document.getElementById("compressProgressQueueCount"),
|
||||
track: document.getElementById("compressProgressTrack"),
|
||||
bar: document.getElementById("compressProgressBar"),
|
||||
text: document.getElementById("compressProgressText"),
|
||||
},
|
||||
output: {
|
||||
container: document.getElementById("outputDownloadContainer"),
|
||||
content: document.getElementById("outputDownloadContent"),
|
||||
downloadAllBtn: document.getElementById("downloadAllImagesButton"),
|
||||
subpageOutput: document.getElementById("subpageOutput"),
|
||||
imageCount: document.getElementById("compressedImageCount"),
|
||||
outputFileType: 'image/png',
|
||||
},
|
||||
actions: {
|
||||
abort: document.getElementById("compressAbort"),
|
||||
dropZone: document.getElementById("dropZoneActions"),
|
||||
backToTop: document.getElementById("backToTop"),
|
||||
downloadAll: document.getElementById("downloadAllImagesButton"),
|
||||
},
|
||||
groups: {
|
||||
formatMethod: document.getElementById("formatMethodGroup"),
|
||||
settingsSubpage: document.getElementById("selectSettingsSubpage"),
|
||||
dropZone: document.getElementById("compressDropZone"),
|
||||
limitWeight: document.getElementById("limitWeightField"),
|
||||
quality: document.getElementById("qualityField"),
|
||||
compressMethod: document.getElementById("compressMethodGroup"),
|
||||
},
|
||||
};
|
||||
|
||||
App.config = {
|
||||
form: {
|
||||
// Default form settings
|
||||
quality: {value: 80},
|
||||
limitDimensions: {value: 1200},
|
||||
limitWeightUnit: {value: "MB"},
|
||||
limitWeight: {value: 2},
|
||||
compressMethod: {value: "quality"},
|
||||
dimensionMethod: {value: "original"},
|
||||
convertMethod: {value: "default"},
|
||||
},
|
||||
thumbnailOptions: {
|
||||
initialQuality: 0.8,
|
||||
maxWidthOrHeight: 70,
|
||||
usecompress: true,
|
||||
preserveExif: false,
|
||||
fileType: "image/png",
|
||||
libURL: "./browser-image-compression.js",
|
||||
alwaysKeepResolution: true,
|
||||
},
|
||||
qualityLimit: {
|
||||
min: 0,
|
||||
max: 100,
|
||||
},
|
||||
weightLimit: {
|
||||
min: 0.01,
|
||||
max: 100,
|
||||
},
|
||||
dimensionLimit: {
|
||||
min: 1,
|
||||
max: 30000,
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
App.state = {
|
||||
controller: null,
|
||||
compressQueue: [],
|
||||
compressQueueTotal: 0,
|
||||
compressProcessedCount: 0,
|
||||
compressMethod: null,
|
||||
isCompressing: false,
|
||||
isDownloadingAll: false,
|
||||
inputFileSize: null,
|
||||
outputImageCount: 0,
|
||||
fileProgressMap: {},
|
||||
limitWeightUnit: "MB",
|
||||
};
|
||||
|
||||
const ui = App.ui;
|
||||
const config = App.config;
|
||||
const state = App.state;
|
||||
zip = new JSZip();
|
||||
@@ -0,0 +1,49 @@
|
||||
function buildOutputItemHTML({
|
||||
outputImageBlob,
|
||||
thumbnailDataURL,
|
||||
outputFileNameText,
|
||||
outputFileExtension,
|
||||
width,
|
||||
height,
|
||||
fileSize,
|
||||
fileSizeSavedTrend,
|
||||
fileSizeSavedPercentage,
|
||||
fileSizeSavedClass,
|
||||
}) {
|
||||
// Create the output dom for compressed images.
|
||||
const fileSizeInMB = fileSize / 1024 / 1024;
|
||||
let fileSizeDisplay;
|
||||
if (fileSizeInMB < 1) {
|
||||
fileSizeDisplay = Math.round(fileSizeInMB * 1024) + " KB";
|
||||
} else {
|
||||
fileSizeDisplay = fileSizeInMB.toFixed(2) + " MB";
|
||||
}
|
||||
return `
|
||||
<div class="image-output__item file-format--${outputFileExtension}" data-elevation="3">
|
||||
<img src="${thumbnailDataURL}" class="image-output__item-thumbnail" loading="lazy">
|
||||
<div class="image-output__item-text">
|
||||
<div class="image-output__item-filename">
|
||||
<span class="image-output__item-filename-start">${outputFileNameText.slice(0, -8)}</span>
|
||||
<span class="image-output__item-filename-end">${outputFileNameText.slice(-8)}</span>
|
||||
</div>
|
||||
<div class="image-output__item-dimensions">
|
||||
<div class="image-output__item-dimensions">${width}x${height}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="image-output__item-stats">
|
||||
<span class="image-output__item-filesize" data-filesize="${fileSize}">${fileSizeDisplay}</span>
|
||||
<span class="image-output__item-filesize-saved badge ${fileSizeSavedClass}">
|
||||
<span class="badge-text">${fileSizeSavedTrend}${fileSizeSavedPercentage}%</span>
|
||||
</span>
|
||||
<span class="image-output__item-fileformat badge file-format--${outputFileExtension}">${outputFileExtension.toUpperCase()}</span>
|
||||
</div>
|
||||
<a class="image-output__item-download-button button-cta button-secondary"
|
||||
data-filesize="${fileSize}"
|
||||
href="${outputImageBlob}"
|
||||
download="${outputFileNameText}">
|
||||
<svg height="16" stroke-linejoin="round" viewBox="0 0 16 16" width="16" style="color: currentcolor;"><path fill-rule="evenodd" clip-rule="evenodd" d="M8.75 1V1.75V8.68934L10.7197 6.71967L11.25 6.18934L12.3107 7.25L11.7803 7.78033L8.70711 10.8536C8.31658 11.2441 7.68342 11.2441 7.29289 10.8536L4.21967 7.78033L3.68934 7.25L4.75 6.18934L5.28033 6.71967L7.25 8.68934V1.75V1H8.75ZM13.5 9.25V13.5H2.5V9.25V8.5H1V9.25V14C1 14.5523 1.44771 15 2 15H14C14.5523 15 15 14.5523 15 14V9.25V8.5H13.5V9.25Z" fill="currentColor"></path></svg>
|
||||
<span class="xs:hidden">Download</span>
|
||||
</a>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
@@ -0,0 +1,291 @@
|
||||
/**
|
||||
* TODO:
|
||||
* - Refactor toast to reusable component to show error messages.
|
||||
* - Allow clear individual items and all items.
|
||||
*/
|
||||
|
||||
let storeConfigDebounceTimer;
|
||||
|
||||
function resetUI() {
|
||||
// Resets the UI primarily around the dropzone area.
|
||||
ui.actions.abort.classList.add("hidden");
|
||||
document.body.classList.remove("compressing--is-active");
|
||||
ui.actions.dropZone.classList.remove("hidden");
|
||||
ui.progress.container.classList.add("hidden");
|
||||
ui.progress.text.dataset.progress = 0;
|
||||
ui.progress.bar.style.width = "0%";
|
||||
}
|
||||
|
||||
function storeConfigForm() {
|
||||
// Store form fields values to local storage.
|
||||
const configForm = {
|
||||
quality: ui.inputs.quality.value,
|
||||
limitDimensions: ui.inputs.limitDimensions.value,
|
||||
limitWeightUnit: ui.inputs.limitWeightUnit.value,
|
||||
limitWeight: ui.inputs.limitWeight.value,
|
||||
compressMethod: getCheckedValue(ui.inputs.compressMethod),
|
||||
dimensionMethod: getCheckedValue(ui.inputs.dimensionMethod),
|
||||
convertMethod: getCheckedValue(ui.inputs.formatSelect),
|
||||
};
|
||||
|
||||
localStorage.setItem("configForm", JSON.stringify(configForm));
|
||||
}
|
||||
|
||||
function storeConfigFormDebounce() {
|
||||
// Debounce the storage of form fields values to local storage, to prevent excessive.
|
||||
clearTimeout(storeConfigDebounceTimer);
|
||||
storeConfigDebounceTimer = setTimeout(() => {
|
||||
storeConfigForm();
|
||||
}, 300);
|
||||
}
|
||||
|
||||
function restoreConfigForm() {
|
||||
// Restore form fields values from local storage.
|
||||
const configForm = JSON.parse(localStorage.getItem("configForm"));
|
||||
if (configForm) {
|
||||
setQuality(configForm.quality);
|
||||
setLimitDimensions(configForm.limitDimensions);
|
||||
setWeightUnit(configForm.limitWeightUnit);
|
||||
setWeight(configForm.limitWeight, configForm.limitWeightUnit);
|
||||
setCompressMethod(configForm.compressMethod);
|
||||
setDimensionMethod(configForm.dimensionMethod);
|
||||
setConvertMethod(configForm.convertMethod);
|
||||
}
|
||||
}
|
||||
|
||||
function setCompressMethod(value) {
|
||||
// Form group: Optimization method.
|
||||
const compressMethod = value;
|
||||
|
||||
document.querySelector(
|
||||
`input[name="compressMethod"][value="${compressMethod}"]`
|
||||
).checked = true;
|
||||
|
||||
document
|
||||
.querySelectorAll("#compressMethodGroup .button-card-radio")
|
||||
.forEach((el) => {
|
||||
el.classList.remove("button-card-radio--is-selected");
|
||||
});
|
||||
|
||||
document
|
||||
.querySelector(
|
||||
`#compressMethodGroup input[name="compressMethod"][value="${compressMethod}"]`
|
||||
)
|
||||
.closest(".button-card-radio")
|
||||
.classList.add("button-card-radio--is-selected");
|
||||
|
||||
if (compressMethod === "limitWeight") {
|
||||
ui.groups.limitWeight.classList.remove("hidden");
|
||||
ui.groups.quality.classList.add("hidden");
|
||||
}
|
||||
else {
|
||||
ui.groups.limitWeight.classList.add("hidden");
|
||||
ui.groups.quality.classList.remove("hidden");
|
||||
}
|
||||
|
||||
storeConfigFormDebounce();
|
||||
}
|
||||
|
||||
function setDimensionMethod(value) {
|
||||
// Form group: Dimensions method.
|
||||
document.querySelector(
|
||||
`input[name="dimensionMethod"][value="${value}"]`
|
||||
).checked = true;
|
||||
document
|
||||
.querySelectorAll("#dimensionsMethodGroup .button-card-radio")
|
||||
.forEach((el) => {
|
||||
el.classList.remove("button-card-radio--is-selected");
|
||||
});
|
||||
document
|
||||
.querySelector(`input[name="dimensionMethod"][value="${value}"]`)
|
||||
.closest(".button-card-radio")
|
||||
.classList.add("button-card-radio--is-selected");
|
||||
|
||||
const resizeDimensionsField = document.getElementById(
|
||||
"resizeDimensionsField"
|
||||
);
|
||||
if (value === "limit") {
|
||||
resizeDimensionsField.classList.remove("hidden");
|
||||
} else {
|
||||
resizeDimensionsField.classList.add("hidden");
|
||||
}
|
||||
|
||||
storeConfigFormDebounce();
|
||||
}
|
||||
|
||||
function setQuality(value) {
|
||||
// Form group: Quality.
|
||||
let quality = Number(value);
|
||||
const min = config.qualityLimit.min;
|
||||
const max = config.qualityLimit.max;
|
||||
if (quality > max) {
|
||||
quality = max;
|
||||
setSlider(max, "qualitySlider");
|
||||
}
|
||||
if (quality < min || isNaN(quality) || quality === "") {
|
||||
quality = min;
|
||||
setSlider(min, "qualitySlider");
|
||||
}
|
||||
else {
|
||||
quality = Math.round(quality);
|
||||
setSlider(quality, "qualitySlider");
|
||||
}
|
||||
|
||||
ui.inputs.quality.value = quality;
|
||||
storeConfigFormDebounce();
|
||||
}
|
||||
|
||||
function setSlider(value, sliderId) {
|
||||
// Form group: Slider.
|
||||
// Update input field and slider elements
|
||||
const slider = document.getElementById(sliderId);
|
||||
const fill = slider.querySelector(".slider-fill");
|
||||
const thumb = slider.querySelector(".slider-thumb");
|
||||
let percentage = value;
|
||||
if (value < 0 || isNaN(value) || value === "") {
|
||||
percentage = 0;
|
||||
} else if (value > 100) {
|
||||
percentage = 100;
|
||||
}
|
||||
fill.style.width = percentage + "%";
|
||||
thumb.style.left = Math.min(percentage, 100) + "%";
|
||||
}
|
||||
|
||||
function startSliderDrag(event, inputId) {
|
||||
const slider = event.currentTarget;
|
||||
const input = document.getElementById(inputId);
|
||||
|
||||
const setSliderPosition = (e) => {
|
||||
const rect = slider.getBoundingClientRect();
|
||||
const offsetX = e.clientX - rect.left;
|
||||
const percentage = Math.min(Math.max((offsetX / rect.width) * 100, 0), 100);
|
||||
input.value = Math.round(Math.min(percentage, 100));
|
||||
setSlider(percentage, slider.id);
|
||||
};
|
||||
|
||||
const onMouseMove = (e) => {
|
||||
setSliderPosition(e);
|
||||
};
|
||||
|
||||
const onMouseUp = () => {
|
||||
document.removeEventListener("mousemove", onMouseMove);
|
||||
document.removeEventListener("mouseup", onMouseUp);
|
||||
};
|
||||
|
||||
setSliderPosition(event);
|
||||
document.addEventListener("mousemove", onMouseMove);
|
||||
document.addEventListener("mouseup", onMouseUp);
|
||||
|
||||
storeConfigFormDebounce();
|
||||
}
|
||||
|
||||
function setLimitDimensions(value) {
|
||||
// Form group: Limit dimensions.
|
||||
let selectedDimension = Number(value);
|
||||
const min = config.dimensionLimit.min;
|
||||
const max = config.dimensionLimit.max;
|
||||
if (selectedDimension > max) {
|
||||
selectedDimension = max;
|
||||
}
|
||||
else if (selectedDimension <= 0 || isNaN(selectedDimension) || selectedDimension === "") {
|
||||
selectedDimension = min;
|
||||
}
|
||||
else {
|
||||
selectedDimension = Math.round(selectedDimension);
|
||||
}
|
||||
|
||||
ui.inputs.limitDimensions.value = selectedDimension;
|
||||
storeConfigFormDebounce();
|
||||
}
|
||||
|
||||
function setConvertMethod(value) {
|
||||
// Form group: Convert to format.
|
||||
ui.inputs.formatSelect.forEach(input => {
|
||||
input.checked = input.value === value;
|
||||
});
|
||||
|
||||
ui.groups.formatMethod.querySelectorAll(".button-card-radio").forEach(el => {
|
||||
el.classList.remove("button-card-radio--is-selected");
|
||||
});
|
||||
|
||||
const selectedInput = Array.from(ui.inputs.formatSelect).find(input => input.value === value);
|
||||
if (selectedInput) {
|
||||
selectedInput.closest(".button-card-radio").classList.add("button-card-radio--is-selected");
|
||||
}
|
||||
|
||||
storeConfigFormDebounce();
|
||||
}
|
||||
|
||||
function setWeightUnit(value) {
|
||||
// Form group: Limit weight (unit)
|
||||
const previousUnit = state.limitWeightUnit.toUpperCase();
|
||||
if (previousUnit === value) return;
|
||||
|
||||
Array.from(ui.inputs.limitWeightUnit.options).forEach(option => {
|
||||
option.selected = option.value === value;
|
||||
});
|
||||
|
||||
if (previousUnit === "KB") {
|
||||
const kbToMb = Number(ui.inputs.limitWeight.value / 1000);
|
||||
if (kbToMb < ui.inputs.limitWeight.value) {
|
||||
ui.inputs.limitWeight.value = kbToMb;
|
||||
ui.inputs.limitWeight.step = 0.1;
|
||||
}
|
||||
}
|
||||
else if (previousUnit === "MB") {
|
||||
const mbToKb = Number(ui.inputs.limitWeight.value * 1000);
|
||||
if (mbToKb > ui.inputs.limitWeight.value) {
|
||||
ui.inputs.limitWeight.min = 0;
|
||||
ui.inputs.limitWeight.step = 50;
|
||||
ui.inputs.limitWeight.value = mbToKb;
|
||||
}
|
||||
}
|
||||
|
||||
state.limitWeightUnit = ui.inputs.limitWeightUnit.value.toUpperCase();
|
||||
ui.labels.limitWeightSuffix.textContent = ui.inputs.limitWeightUnit.value.toUpperCase();
|
||||
ui.labels.limitWeightSuffix.dataset.suffix = ui.inputs.limitWeightUnit.value.toUpperCase();
|
||||
|
||||
storeConfigFormDebounce();
|
||||
}
|
||||
|
||||
function setWeight(weight, unit) {
|
||||
// Form group: Limit weight
|
||||
const { value, message } = validateWeight(
|
||||
weight, unit
|
||||
);
|
||||
|
||||
if (!value) {
|
||||
}
|
||||
else if (value && message) {
|
||||
ui.inputs.limitWeight.value = value;
|
||||
}
|
||||
else if (value) {
|
||||
ui.inputs.limitWeight.value = value;
|
||||
}
|
||||
|
||||
ui.inputs.limitWeight.value = value;
|
||||
|
||||
storeConfigFormDebounce();
|
||||
}
|
||||
|
||||
function selectSubpage(value) {
|
||||
// Switch between "Settings", "Images".
|
||||
ui.inputs.settingsSubpage.forEach(input => {
|
||||
input.checked = input.value === value;
|
||||
});
|
||||
|
||||
ui.groups.settingsSubpage.querySelectorAll(".segmented-control").forEach(el => {
|
||||
el.classList.remove("segmented-control--is-selected");
|
||||
});
|
||||
|
||||
const selectedInput = Array.from(ui.inputs.settingsSubpage).find(input => input.value === value);
|
||||
if (selectedInput) {
|
||||
selectedInput.closest(".segmented-control").classList.add("segmented-control--is-selected");
|
||||
}
|
||||
|
||||
document.body.className = document.body.className.replace(/\bsubpage--\S+/g, "");
|
||||
document.body.classList.add(`subpage--${value}`);
|
||||
|
||||
storeConfigFormDebounce();
|
||||
}
|
||||
|
||||
@@ -0,0 +1,206 @@
|
||||
function isFileTypeSupported(fileType, file) {
|
||||
// Check for supported file types
|
||||
|
||||
if (HeicTo.isHeic(file) && isHeicExt(file)) {
|
||||
fileType = "image/heic";
|
||||
ui.outputFileType = "image/heic";
|
||||
console.log('File type is HEIC: ', fileType)
|
||||
}
|
||||
|
||||
const supportedFileTypes = [
|
||||
"image/jpeg",
|
||||
"image/png",
|
||||
"image/webp",
|
||||
"image/heic",
|
||||
"image/heif",
|
||||
"image/avif",
|
||||
"image/gif",
|
||||
"image/svg+xml",
|
||||
"image/jxl",
|
||||
];
|
||||
|
||||
return supportedFileTypes.includes(fileType);
|
||||
}
|
||||
|
||||
function mimeToExtension(mimeType) {
|
||||
const fileExtensionMap = {
|
||||
"image/jpeg": "jpg",
|
||||
"image/png": "png",
|
||||
"image/webp": "webp",
|
||||
"image/heic": "heic",
|
||||
"image/heic": "heif",
|
||||
"image/avif": "avif",
|
||||
"image/gif": "gif",
|
||||
"image/svg+xml": "svg",
|
||||
"image/jxl": "jxl",
|
||||
};
|
||||
|
||||
return (
|
||||
fileExtensionMap[mimeType] || mimeType.replace("image/", "").split("+")[0]
|
||||
);
|
||||
}
|
||||
|
||||
function defaultConversionMapping(mimeType) {
|
||||
const conversionMap = {
|
||||
// Image file types that cannot be compressed to its original file format
|
||||
// are converted to a relevant counterpart.
|
||||
"image/heic": "image/png",
|
||||
"image/heif": "image/png",
|
||||
"image/avif": "image/png",
|
||||
"image/gif": "image/png",
|
||||
"image/svg+xml": "image/png",
|
||||
"image/jxl": "image/png",
|
||||
};
|
||||
|
||||
console.log('Input mimeType ', mimeType);
|
||||
console.log('Mapped mimeType ', conversionMap[mimeType]);
|
||||
|
||||
return conversionMap[mimeType] || mimeType;
|
||||
}
|
||||
|
||||
function isHeicExt(file) {
|
||||
// Checks if file name ending with `.heic` or `.heif`.
|
||||
const fileName = file.name.toLowerCase();
|
||||
return fileName.endsWith('.heic') || fileName.endsWith('.heif');
|
||||
}
|
||||
|
||||
function isFileExt(file, extension = "") {
|
||||
// Checks if file name ending with the passed string argument.
|
||||
const fileName = file.name.toLowerCase();
|
||||
return fileName.endsWith(`.${extension}`);
|
||||
}
|
||||
|
||||
function getFileType(file) {
|
||||
let selectedFormat = document.querySelector('input[name="formatSelect"]:checked').value; // User-selected format to convert to, e.g. "image/jpeg".
|
||||
let inputFileExtension = ""; // User-uploaded image's file extension, e.g. `.jpg`.
|
||||
let outputFileExtension = ""; // The processed image's file extension, based on `defaultConversionMapping()`.
|
||||
|
||||
if (selectedFormat && selectedFormat !== "default") {
|
||||
// The user-selected format to convert to.
|
||||
const extension = mimeToExtension(selectedFormat);
|
||||
inputFileExtension = extension;
|
||||
outputFileExtension = extension;
|
||||
} else {
|
||||
// User has not selected a file format, use the input image's file type.
|
||||
selectedFormat = file.type ? file.type : "png";
|
||||
file.type = !file.type && isHeicExt(file) ? "image/heic" : "";
|
||||
inputFileExtension = mimeToExtension(file.type) || "";
|
||||
|
||||
console.log("inputFileExtension: ", inputFileExtension);
|
||||
outputFileExtension = mimeToExtension(defaultConversionMapping(file.type));
|
||||
console.log("outputFileExtension: ", outputFileExtension);
|
||||
}
|
||||
|
||||
return {
|
||||
inputFileExtension,
|
||||
outputFileExtension,
|
||||
selectedFormat,
|
||||
};
|
||||
}
|
||||
|
||||
function updateFileExtension(originalName, fileExtension, selectedFormat) {
|
||||
const baseName = originalName.replace(/\.[^/.]+$/, "");
|
||||
const newExtension = selectedFormat
|
||||
? mimeToExtension(fileExtension)
|
||||
: fileExtension;
|
||||
|
||||
console.log('New image extension: ', newExtension);
|
||||
return `${baseName}.${newExtension}`;
|
||||
}
|
||||
|
||||
function appendFileNameId(fileName = "image") {
|
||||
if (typeof fileName !== 'string') return null;
|
||||
|
||||
const lastDotIndex = fileName.lastIndexOf('.');
|
||||
const fileExt = (lastDotIndex === -1 || lastDotIndex === 0) ? '' : fileName.slice(lastDotIndex).toLowerCase();
|
||||
const baseFileName = (lastDotIndex === -1) ? fileName : fileName.slice(0, lastDotIndex);
|
||||
|
||||
const fileId = Math.random().toString(36).substring(2, 6).toUpperCase();
|
||||
return baseFileName + "-" + fileId + fileExt;
|
||||
}
|
||||
|
||||
function renameBrowserDefaultFileName(fileName) {
|
||||
// Naive approach to check if an image was pasted from clipboard and received a default name by the browser,
|
||||
// e.g., `image.png`. This method is potentially browser and language-dependent, if naming conventions vary.
|
||||
// `HEIF Image.heic` concerns iOS devices, e.g. when drag-and-dropping a subject cut-out.
|
||||
const defaultNames = [/^image\.\w+$/i, /^heif image\.heic$/i];
|
||||
|
||||
if (defaultNames.some(regex => regex.test(fileName))) {
|
||||
return { renamedFileName: appendFileNameId(fileName), isBrowserDefaultFileName: true };
|
||||
}
|
||||
return { renamedFileName: fileName, isBrowserDefaultFileName: false };
|
||||
}
|
||||
|
||||
function validateWeight(value, unit = "MB") {
|
||||
value = Number(value);
|
||||
let [min, max] = [config.weightLimit.min, config.weightLimit.max];
|
||||
min = unit.toUpperCase() === "KB" ? min * 1000 : min;
|
||||
max = unit.toUpperCase() === "KB" ? max * 1000 : max;
|
||||
|
||||
if (typeof value !== 'number' || isNaN(value) || !Number.isFinite(value)) {
|
||||
const message = "Invalid value, not a number.";
|
||||
return {value: null, message}
|
||||
}
|
||||
else if (value < min) {
|
||||
const message = `Minimum file size is ${min * 1000}KB or ${max}MB.`;
|
||||
return {value: min, message}
|
||||
}
|
||||
else if (value > max) {
|
||||
const message = `Max file size is ${max}MB.`;
|
||||
return {value: max, message}
|
||||
}
|
||||
|
||||
return {value, message: null}
|
||||
}
|
||||
|
||||
function getCheckedValue(nodeList) {
|
||||
// Find the currently select radio button value.
|
||||
return [...nodeList].find((el) => el.checked)?.value || null;
|
||||
}
|
||||
|
||||
function getImageDimensions(imageInput, callback) {
|
||||
const img = new Image();
|
||||
|
||||
if (imageInput instanceof Blob) {
|
||||
img.src = URL.createObjectURL(imageInput);
|
||||
}
|
||||
else if (typeof imageInput === "string") {
|
||||
img.src = imageInput;
|
||||
}
|
||||
else {
|
||||
console.error("Invalid input provided to getImageDimensions.");
|
||||
callback(null);
|
||||
return;
|
||||
}
|
||||
|
||||
img.onload = () => callback({ width: img.naturalWidth, height: img.naturalHeight });
|
||||
img.onerror = () => callback(null);
|
||||
}
|
||||
|
||||
function getAdjustedDimensions(imageBlob, desiredLimitDimensions) {
|
||||
// Adjusts image dimensions to prevent the short edge from being 0.
|
||||
// Calculates the minimum long edge based on a 1px short edge while keeping aspect ratio.
|
||||
return new Promise((resolve) => {
|
||||
getImageDimensions(imageBlob, ({ width, height }) => {
|
||||
if (!width || !height) {
|
||||
resolve(undefined);
|
||||
return;
|
||||
}
|
||||
const shortEdge = Math.min(width, height);
|
||||
const longEdge = Math.max(width, height);
|
||||
const shortEdgeMin = 1;
|
||||
const minAllowedDimension = longEdge * (shortEdgeMin / shortEdge);
|
||||
const limitDimensionsValue = desiredLimitDimensions > Math.ceil(minAllowedDimension) ? desiredLimitDimensions : Math.ceil(minAllowedDimension);
|
||||
resolve(limitDimensionsValue);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function debugBlobImageOutput(blob) {
|
||||
const blobURL = URL.createObjectURL(blob);
|
||||
const img = document.createElement("img");
|
||||
img.src = blobURL;
|
||||
img.style.maxWidth = "100%";
|
||||
img.style.display = "block";
|
||||
document.body.prepend(img);
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
/**
|
||||
* Browser Image Compression
|
||||
* v2.0.2
|
||||
* by Donald <donaldcwl@gmail.com>
|
||||
* https://github.com/Donaldcwl/browser-image-compression
|
||||
*/
|
||||
|
||||
!function(e,t){"object"==typeof exports&&"undefined"!=typeof module?module.exports=t():"function"==typeof define&&define.amd?define(t):(e="undefined"!=typeof globalThis?globalThis:e||self).imageCompression=t()}(this,(function(){"use strict";function _mergeNamespaces(e,t){return t.forEach((function(t){t&&"string"!=typeof t&&!Array.isArray(t)&&Object.keys(t).forEach((function(r){if("default"!==r&&!(r in e)){var i=Object.getOwnPropertyDescriptor(t,r);Object.defineProperty(e,r,i.get?i:{enumerable:!0,get:function(){return t[r]}})}}))})),Object.freeze(e)}function copyExifWithoutOrientation(e,t){return new Promise((function(r,i){let o;return getApp1Segment(e).then((function(e){try{return o=e,r(new Blob([t.slice(0,2),o,t.slice(2)],{type:"image/jpeg"}))}catch(e){return i(e)}}),i)}))}const getApp1Segment=e=>new Promise(((t,r)=>{const i=new FileReader;i.addEventListener("load",(({target:{result:e}})=>{const i=new DataView(e);let o=0;if(65496!==i.getUint16(o))return r("not a valid JPEG");for(o+=2;;){const a=i.getUint16(o);if(65498===a)break;const s=i.getUint16(o+2);if(65505===a&&1165519206===i.getUint32(o+4)){const a=o+10;let f;switch(i.getUint16(a)){case 18761:f=!0;break;case 19789:f=!1;break;default:return r("TIFF header contains invalid endian")}if(42!==i.getUint16(a+2,f))return r("TIFF header contains invalid version");const l=i.getUint32(a+4,f),c=a+l+2+12*i.getUint16(a+l,f);for(let e=a+l+2;e<c;e+=12){if(274==i.getUint16(e,f)){if(3!==i.getUint16(e+2,f))return r("Orientation data type is invalid");if(1!==i.getUint32(e+4,f))return r("Orientation data count is invalid");i.setUint16(e+8,1,f);break}}return t(e.slice(o,o+2+s))}o+=2+s}return t(new Blob)})),i.readAsArrayBuffer(e)}));var e={};!function(e){var t,r,UZIP={};e.exports=UZIP,UZIP.parse=function(e,t){for(var r=UZIP.bin.readUshort,i=UZIP.bin.readUint,o=0,a={},s=new Uint8Array(e),f=s.length-4;101010256!=i(s,f);)f--;o=f;o+=4;var l=r(s,o+=4);r(s,o+=2);var c=i(s,o+=2),u=i(s,o+=4);o+=4,o=u;for(var h=0;h<l;h++){i(s,o),o+=4,o+=4,o+=4,i(s,o+=4);c=i(s,o+=4);var d=i(s,o+=4),A=r(s,o+=4),g=r(s,o+2),p=r(s,o+4);o+=6;var m=i(s,o+=8);o+=4,o+=A+g+p,UZIP._readLocal(s,m,a,c,d,t)}return a},UZIP._readLocal=function(e,t,r,i,o,a){var s=UZIP.bin.readUshort,f=UZIP.bin.readUint;f(e,t),s(e,t+=4),s(e,t+=2);var l=s(e,t+=2);f(e,t+=2),f(e,t+=4),t+=4;var c=s(e,t+=8),u=s(e,t+=2);t+=2;var h=UZIP.bin.readUTF8(e,t,c);if(t+=c,t+=u,a)r[h]={size:o,csize:i};else{var d=new Uint8Array(e.buffer,t);if(0==l)r[h]=new Uint8Array(d.buffer.slice(t,t+i));else{if(8!=l)throw"unknown compression method: "+l;var A=new Uint8Array(o);UZIP.inflateRaw(d,A),r[h]=A}}},UZIP.inflateRaw=function(e,t){return UZIP.F.inflate(e,t)},UZIP.inflate=function(e,t){return e[0],e[1],UZIP.inflateRaw(new Uint8Array(e.buffer,e.byteOffset+2,e.length-6),t)},UZIP.deflate=function(e,t){null==t&&(t={level:6});var r=0,i=new Uint8Array(50+Math.floor(1.1*e.length));i[r]=120,i[r+1]=156,r+=2,r=UZIP.F.deflateRaw(e,i,r,t.level);var o=UZIP.adler(e,0,e.length);return i[r+0]=o>>>24&255,i[r+1]=o>>>16&255,i[r+2]=o>>>8&255,i[r+3]=o>>>0&255,new Uint8Array(i.buffer,0,r+4)},UZIP.deflateRaw=function(e,t){null==t&&(t={level:6});var r=new Uint8Array(50+Math.floor(1.1*e.length)),i=UZIP.F.deflateRaw(e,r,i,t.level);return new Uint8Array(r.buffer,0,i)},UZIP.encode=function(e,t){null==t&&(t=!1);var r=0,i=UZIP.bin.writeUint,o=UZIP.bin.writeUshort,a={};for(var s in e){var f=!UZIP._noNeed(s)&&!t,l=e[s],c=UZIP.crc.crc(l,0,l.length);a[s]={cpr:f,usize:l.length,crc:c,file:f?UZIP.deflateRaw(l):l}}for(var s in a)r+=a[s].file.length+30+46+2*UZIP.bin.sizeUTF8(s);r+=22;var u=new Uint8Array(r),h=0,d=[];for(var s in a){var A=a[s];d.push(h),h=UZIP._writeHeader(u,h,s,A,0)}var g=0,p=h;for(var s in a){A=a[s];d.push(h),h=UZIP._writeHeader(u,h,s,A,1,d[g++])}var m=h-p;return i(u,h,101010256),h+=4,o(u,h+=4,g),o(u,h+=2,g),i(u,h+=2,m),i(u,h+=4,p),h+=4,h+=2,u.buffer},UZIP._noNeed=function(e){var t=e.split(".").pop().toLowerCase();return-1!="png,jpg,jpeg,zip".indexOf(t)},UZIP._writeHeader=function(e,t,r,i,o,a){var s=UZIP.bin.writeUint,f=UZIP.bin.writeUshort,l=i.file;return s(e,t,0==o?67324752:33639248),t+=4,1==o&&(t+=2),f(e,t,20),f(e,t+=2,0),f(e,t+=2,i.cpr?8:0),s(e,t+=2,0),s(e,t+=4,i.crc),s(e,t+=4,l.length),s(e,t+=4,i.usize),f(e,t+=4,UZIP.bin.sizeUTF8(r)),f(e,t+=2,0),t+=2,1==o&&(t+=2,t+=2,s(e,t+=6,a),t+=4),t+=UZIP.bin.writeUTF8(e,t,r),0==o&&(e.set(l,t),t+=l.length),t},UZIP.crc={table:function(){for(var e=new Uint32Array(256),t=0;t<256;t++){for(var r=t,i=0;i<8;i++)1&r?r=3988292384^r>>>1:r>>>=1;e[t]=r}return e}(),update:function(e,t,r,i){for(var o=0;o<i;o++)e=UZIP.crc.table[255&(e^t[r+o])]^e>>>8;return e},crc:function(e,t,r){return 4294967295^UZIP.crc.update(4294967295,e,t,r)}},UZIP.adler=function(e,t,r){for(var i=1,o=0,a=t,s=t+r;a<s;){for(var f=Math.min(a+5552,s);a<f;)o+=i+=e[a++];i%=65521,o%=65521}return o<<16|i},UZIP.bin={readUshort:function(e,t){return e[t]|e[t+1]<<8},writeUshort:function(e,t,r){e[t]=255&r,e[t+1]=r>>8&255},readUint:function(e,t){return 16777216*e[t+3]+(e[t+2]<<16|e[t+1]<<8|e[Line truncated
|
||||
//# sourceMappingURL=browser-image-compression.js.map
|
||||
@@ -0,0 +1,110 @@
|
||||
var HeicTo = (() => {
|
||||
var __getOwnPropNames = Object.getOwnPropertyNames;
|
||||
var __esm = (fn, res) => function __init() {
|
||||
return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
|
||||
};
|
||||
var __commonJS = (cb, mod) => function __require() {
|
||||
return mod || (0, cb[__getOwnPropNames(cb)[0]])((mod = { exports: {} }).exports, mod), mod.exports;
|
||||
};
|
||||
var __async = (__this, __arguments, generator) => {
|
||||
return new Promise((resolve, reject) => {
|
||||
var fulfilled = (value) => {
|
||||
try {
|
||||
step(generator.next(value));
|
||||
} catch (e) {
|
||||
reject(e);
|
||||
}
|
||||
};
|
||||
var rejected = (value) => {
|
||||
try {
|
||||
step(generator.throw(value));
|
||||
} catch (e) {
|
||||
reject(e);
|
||||
}
|
||||
};
|
||||
var step = (x) => x.done ? resolve(x.value) : Promise.resolve(x.value).then(fulfilled, rejected);
|
||||
step((generator = generator.apply(__this, __arguments)).next());
|
||||
});
|
||||
};
|
||||
|
||||
// src/index.js
|
||||
var isHeic, worker, loadWorker, decodeBuffer, encodeByCanvas, heicTo;
|
||||
var init_src = __esm({
|
||||
"src/index.js"() {
|
||||
isHeic = (file) => __async(void 0, null, function* () {
|
||||
const buffer = yield file.arrayBuffer();
|
||||
const slicedBuffer = buffer.slice(8, 12);
|
||||
const brandMajor = new TextDecoder("utf-8").decode(slicedBuffer).replace("\0", " ").trim();
|
||||
switch (brandMajor) {
|
||||
case "mif1":
|
||||
return true;
|
||||
case "msf1":
|
||||
return true;
|
||||
case "heic":
|
||||
case "heix":
|
||||
return true;
|
||||
case "hevc":
|
||||
case "hevx":
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
});
|
||||
loadWorker = () => {
|
||||
if (!worker) {
|
||||
const workerFileContent = '(()=>{var E7=(F1=>typeof require!="undefined"?require:typeof Proxy!="undefined"?new Proxy(F1,{get:(S1,j1)=>(typeof require!="undefined"?require:S1)[j1]}):F1)(function(F1){if(typeof require!="undefined")return require.apply(this,arguments);throw Error(\'Dynamic require of "\'+F1+\'" is not supported\')});var gD=(F1,S1)=>()=>(F1&&(S1=F1(F1=0)),S1);var ED=(F1,S1)=>()=>(S1||F1((S1={exports:{}}).exports,S1),S1.exports);var Y7=(F1,S1,j1)=>new Promise((D8,L)=>{var a8=l4=>{try{di(j1.next(l4))}catch(q4){L(q4)}},U8=l4=>{try{di(j1.throw(l4))}catch(q4){L(q4)}},di=l4=>l4.done?D8(l4.value):Promise.resolve(l4.value).then(a8,U8);di((j1=j1.apply(F1,S1)).next())});var YD,Il,Vl=gD(()=>{YD=(()=>{var S1;var F1=typeof document!="undefined"?(S1=document.currentScript)==null?void 0:S1.src:void 0;return typeof __filename!="undefined"&&(F1||(F1=__filename)),function(j1={}){var D8,L=j1,a8,U8,di=new Promise((g,N)=>{a8=g,U8=N}),l4=typeof window=="object",q4=typeof importScripts=="function",b8=typeof process=="object"&&typeof process.versions=="object"&&typeof process.versions.node=="string",R7=Object.assign({},L),N7=[],xr="./this.program",D7=(g,N)=>{throw N},S4="";function yl(g){return L.locateFile?L.locateFile(g,S4):S4+g}var zr,ci;if(b8){var Tr=E7("fs"),U7=E7("path");S4=__dirname+"/",ci=g=>{g=Pr(g)?new URL(g):U7.normalize(g);var N=Tr.readFileSync(g);return N},zr=(g,N=!0)=>(g=Pr(g)?new URL(g):U7.normalize(g),new Promise((x,j)=>{Tr.readFile(g,N?void 0:"utf8",(d,a)=>{d?j(d):x(N?a.buffer:a)})})),!L.thisProgram&&process.argv.length>1&&(xr=process.argv[1].replace(/\\\\/g,"/")),N7=process.argv.slice(2),D7=(g,N)=>{throw process.exitCode=g,N}}else(l4||q4)&&(q4?S4=self.location.href:typeof document!="undefined"&&document.currentScript&&(S4=document.currentScript.src),F1&&(S4=F1),S4.startsWith("blob:")?S4="":S4=S4.substr(0,S4.replace(/[?#].*/,"").lastIndexOf("/")+1),q4&&(ci=g=>{var N=new XMLHttpRequest;return N.open("GET",g,!1),N.responseType="arraybuffer",N.send(null),new Uint8Array(N.response)}),zr=g=>Pr(g)?new Promise((N,x)=>{var j=new XMLHttpRequest;j.open("GET",g,!0),j.responseType="arraybuffer",j.onload=()=>{(j.status==200||j.status==0&&j.response)&&x(j.response),N(j.status)},j.onerror=N,j.send(null)}):fetch(g,{credentials:"same-origin"}).then(N=>N.ok?N.arrayBuffer():Promise.reject(new Error(N.status+" : "+N.url))));var Sr=L.print||console.log.bind(console),i6=L.printErr||console.error.bind(console);Object.assign(L,R7),R7=null,L.arguments&&(N7=L.arguments),L.thisProgram&&(xr=L.thisProgram),L.quit&&(D7=L.quit);var ke;L.wasmBinary&&(ke=L.wasmBinary);var e6={Memory:function(g){this.buffer=new ArrayBuffer(g.initial*65536)},Module:function(g){},Instance:function(g,N){this.exports=function(j){function d(t1){return t1.set=function(Y2,o){this[Y2]=o},t1.get=function(Y2){return this[Y2]},t1}for(var a,l0=new Uint8Array(123),s0=25;s0>=0;--s0)l0[48+s0]=52+s0,l0[65+s0]=s0,l0[97+s0]=26+s0;l0[43]=62,l0[47]=63;function o0(t1,Y2,o){for(var C,i,u=0,y=Y2,k0=o.length,W0=Y2+(k0*3>>2)-(o[k0-2]=="=")-(o[k0-1]=="=");u<k0;u+=4)C=l0[o.charCodeAt(u+1)],i=l0[o.charCodeAt(u+2)],t1[y++]=l0[o.charCodeAt(u)]<<2|C>>4,y<W0&&(t1[y++]=C<<4|i>>2),y<W0&&(t1[y++]=i<<6|l0[o.charCodeAt(u+3)])}function J0(t1){o0(a,1024,"MTdTdHJlYW1SZWFkZXJfQ0FwaQAxMlN0cmVhbVJlYWRlcgAyMFN0cmVhbVJlYWRlcl9pc3RyZWFtADE5U3RyZWFtUmVhZGVyX21lbW9yeQBoZWlmX3N1YmVycm9yX1Vuc3VwcG9ydGVkX2Vzc2VudGlhbF9wcm9wZXJ0eQBoZWlmX3N1YmVycm9yX0lwbWFfYm94X3JlZmVyZW5jZXNfbm9uZXhpc3RpbmdfcHJvcGVydHkAaGVpZl9zdWJlcnJvcl9Ob19pc3BlX3Byb3BlcnR5AGhlaWZfc3ViZXJyb3JfSW52YWxpZF9wcm9wZXJ0eQBVbnN1cHBvcnRlZCBlc3NlbnRpYWwgaXRlbSBwcm9wZXJ0eQAnaXBtYScgYm94IHJlZmVyZW5jZXMgYSBub24tZXhpc3RpbmcgcHJvcGVydHkASW52YWxpZCBwcm9wZXJ0eQBJbWFnZSBoYXMgbm8gJ2lzcGUnIHByb3BlcnR5AGhlaWZfY2hhbm5lbF9kaXNwYXJpdHkAaW5maW5pdHkAaGVpZl9zdWJlcnJvcl9DYW5ub3RfcmVhZF9wbHVnaW5fZGlyZWN0b3J5AGhlaWZfY29udGV4dF9yZWFkX2Zyb21fbWVtb3J5AEZlYnJ1YXJ5AEphbnVhcnkASnVseQBUbXAgZGF0YSBjb3VsZCBub3QgYmUgcmVhZCBjb21wbGV0ZWx5AGhlaWZfY2hhbm5lbF9maWx0ZXJfYXJyYXkAVGh1cnNkYXkAVHVlc2RheQBXZWRuZXNkYXkAU2F0dXJkYXkAU3VuZGF5AE1vbmRheQBGcmlkYXkATWF5ACVtLyVkLyV5AGhldngAaGVpZl9zdWJlcnJvcl9Ob19pZGF0X2JveABoZWlmX3N1YmVycm9yX05vX2hkbHJfYm94AGhlaWZfc3ViZXJyb3JfTm9faWNicl9ib3gAaGVpZl9zdWJlcnJvcl9Ob19mdHlwX2JveABoZWlmX3N1YmVycm9yX05vX2lwcnBfYm94AGhlaWZfc3ViZXJyb3JfTm9faXBjb19ib3gAaGVpZl9zdWJlcnJvcl9Ob19waXRtX2JveABoZWlmX3N1YmVycm9yX0ludmFsaWRfcGl4aV9ib3gAaGVpZl9zdWJlcnJvcl9JbnZhbGlkX21pbmlfYm94AGhlaWZfc3ViZXJyb3JfTm9faWluZl9ib3gAaGVpZl9zdWJlcnJvcl9Ob19pcmVmX2JveABoZWlmX3N1YmVycm9yX05vX2luZmVfYm94AGhlaWZfc3ViZXJyb3JfTm9faWxvY19ib3gAaGVpZl9zdWJlcnJvcl9Ob19tZXRhX2JveABoZWlmX3N1YmVycm9yX05vX2lwbWFfYm94AGhlaWZfc3ViZXJyb3JfTm9fdnZjQ19ib3gAaGVpZl9zdWJlcnJvcl9Ob19odmNDX2JveABoZWlmX3N1YmVycm9yX05vX2F2Y0NfYm94AGhlaWZfc3ViZXJyb3JfTm9fYXYxQ19ib3gAdGhpcyBwcm9wZXJ0eSBpcyBub3QgcmVhZCBhcyBhIHJhdyBib3gAZXJyb3IgcmVhZGluZyBmdHlwIGJveABpbnB1dCBpcyBub3QgYSBmdHlwIGJveABJbnZhbGlkIHBpeGkgYm94AENhbm5vdCByZWFkIGZ1bGwgbWV0YSBib3gAKSBoYXMgbm8gcHJvcGVydGllcyBhc3NpZ25lZCB0byBpdCBpbiBpcG1hIGJveAAgcmLine truncated
|
||||
const workerBlob = new Blob([workerFileContent], { type: "application/javascript" });
|
||||
worker = new Worker(URL.createObjectURL(workerBlob));
|
||||
worker.onerror = (error) => console.error("Worker error:", error);
|
||||
}
|
||||
return worker;
|
||||
};
|
||||
decodeBuffer = (buffer) => __async(void 0, null, function* () {
|
||||
return new Promise((resolve, reject) => {
|
||||
loadWorker();
|
||||
const id = (Math.random() * (/* @__PURE__ */ new Date()).getTime()).toString();
|
||||
const message = { id, buffer };
|
||||
worker.postMessage(message);
|
||||
const handleEvent = (event) => {
|
||||
if (event.data.id === id) {
|
||||
event.currentTarget.removeEventListener("message", handleEvent);
|
||||
if (event.data.error) {
|
||||
return reject(event.data.error);
|
||||
}
|
||||
return resolve(event.data.imageData);
|
||||
}
|
||||
};
|
||||
worker.addEventListener("message", handleEvent);
|
||||
});
|
||||
});
|
||||
encodeByCanvas = (imageBuffer) => __async(void 0, null, function* () {
|
||||
const imageData = yield decodeBuffer(imageBuffer);
|
||||
const canvas = document.createElement("canvas");
|
||||
canvas.width = imageData.width;
|
||||
canvas.height = imageData.height;
|
||||
const ctx = canvas.getContext("2d");
|
||||
ctx.putImageData(imageData, 0, 0);
|
||||
return canvas;
|
||||
});
|
||||
heicTo = (_0) => __async(void 0, [_0], function* ({ blob, type, quality }) {
|
||||
const imageBuffer = yield blob.arrayBuffer();
|
||||
const canvas = yield encodeByCanvas(imageBuffer);
|
||||
return yield new Promise((resolve, reject) => canvas.toBlob((blob2) => {
|
||||
if (blob2 != null)
|
||||
resolve(blob2);
|
||||
else
|
||||
reject(`Can't convert canvas to blob.`);
|
||||
}, type, quality));
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// src/iife.js
|
||||
var require_iife = __commonJS({
|
||||
"src/iife.js"(exports, module) {
|
||||
init_src();
|
||||
heicTo.isHeic = isHeic;
|
||||
module.exports = heicTo;
|
||||
}
|
||||
});
|
||||
return require_iife();
|
||||
})();
|
||||
@@ -0,0 +1,13 @@
|
||||
/*!
|
||||
|
||||
JSZip v3.10.1 - A JavaScript class for generating and reading zip files
|
||||
<http://stuartk.com/jszip>
|
||||
|
||||
(c) 2009-2016 Stuart Knightley <stuart [at] stuartk.com>
|
||||
Dual licenced under the MIT license or GPLv3. See https://raw.github.com/Stuk/jszip/main/LICENSE.markdown.
|
||||
|
||||
JSZip uses the library pako released under the MIT license :
|
||||
https://github.com/nodeca/pako/blob/main/LICENSE
|
||||
*/
|
||||
|
||||
!function(e){if("object"==typeof exports&&"undefined"!=typeof module)module.exports=e();else if("function"==typeof define&&define.amd)define([],e);else{("undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof self?self:this).JSZip=e()}}(function(){return function s(a,o,h){function u(r,e){if(!o[r]){if(!a[r]){var t="function"==typeof require&&require;if(!e&&t)return t(r,!0);if(l)return l(r,!0);var n=new Error("Cannot find module '"+r+"'");throw n.code="MODULE_NOT_FOUND",n}var i=o[r]={exports:{}};a[r][0].call(i.exports,function(e){var t=a[r][1][e];return u(t||e)},i,i.exports,s,a,o,h)}return o[r].exports}for(var l="function"==typeof require&&require,e=0;e<h.length;e++)u(h[e]);return u}({1:[function(e,t,r){"use strict";var d=e("./utils"),c=e("./support"),p="ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=";r.encode=function(e){for(var t,r,n,i,s,a,o,h=[],u=0,l=e.length,f=l,c="string"!==d.getTypeOf(e);u<e.length;)f=l-u,n=c?(t=e[u++],r=u<l?e[u++]:0,u<l?e[u++]:0):(t=e.charCodeAt(u++),r=u<l?e.charCodeAt(u++):0,u<l?e.charCodeAt(u++):0),i=t>>2,s=(3&t)<<4|r>>4,a=1<f?(15&r)<<2|n>>6:64,o=2<f?63&n:64,h.push(p.charAt(i)+p.charAt(s)+p.charAt(a)+p.charAt(o));return h.join("")},r.decode=function(e){var t,r,n,i,s,a,o=0,h=0,u="data:";if(e.substr(0,u.length)===u)throw new Error("Invalid base64 input, it looks like a data url.");var l,f=3*(e=e.replace(/[^A-Za-z0-9+/=]/g,"")).length/4;if(e.charAt(e.length-1)===p.charAt(64)&&f--,e.charAt(e.length-2)===p.charAt(64)&&f--,f%1!=0)throw new Error("Invalid base64 input, bad content length.");for(l=c.uint8array?new Uint8Array(0|f):new Array(0|f);o<e.length;)t=p.indexOf(e.charAt(o++))<<2|(i=p.indexOf(e.charAt(o++)))>>4,r=(15&i)<<4|(s=p.indexOf(e.charAt(o++)))>>2,n=(3&s)<<6|(a=p.indexOf(e.charAt(o++))),l[h++]=t,64!==s&&(l[h++]=r),64!==a&&(l[h++]=n);return l}},{"./support":30,"./utils":32}],2:[function(e,t,r){"use strict";var n=e("./external"),i=e("./stream/DataWorker"),s=e("./stream/Crc32Probe"),a=e("./stream/DataLengthProbe");function o(e,t,r,n,i){this.compressedSize=e,this.uncompressedSize=t,this.crc32=r,this.compression=n,this.compressedContent=i}o.prototype={getContentWorker:function(){var e=new i(n.Promise.resolve(this.compressedContent)).pipe(this.compression.uncompressWorker()).pipe(new a("data_length")),t=this;return e.on("end",function(){if(this.streamInfo.data_length!==t.uncompressedSize)throw new Error("Bug : uncompressed data size mismatch")}),e},getCompressedWorker:function(){return new i(n.Promise.resolve(this.compressedContent)).withStreamInfo("compressedSize",this.compressedSize).withStreamInfo("uncompressedSize",this.uncompressedSize).withStreamInfo("crc32",this.crc32).withStreamInfo("compression",this.compression)}},o.createWorkerFrom=function(e,t,r){return e.pipe(new s).pipe(new a("uncompressedSize")).pipe(t.compressWorker(r)).pipe(new a("compressedSize")).withStreamInfo("compression",t)},t.exports=o},{"./external":6,"./stream/Crc32Probe":25,"./stream/DataLengthProbe":26,"./stream/DataWorker":27}],3:[function(e,t,r){"use strict";var n=e("./stream/GenericWorker");r.STORE={magic:"\0\0",compressWorker:function(){return new n("STORE compression")},uncompressWorker:function(){return new n("STORE decompression")}},r.DEFLATE=e("./flate")},{"./flate":7,"./stream/GenericWorker":28}],4:[function(e,t,r){"use strict";var n=e("./utils");var o=function(){for(var e,t=[],r=0;r<256;r++){e=r;for(var n=0;n<8;n++)e=1&e?3988292384^e>>>1:e>>>1;t[r]=e}return t}();t.exports=function(e,t){return void 0!==e&&e.length?"string"!==n.getTypeOf(e)?function(e,t,r,n){var i=o,s=n+r;e^=-1;for(var a=n;a<s;a++)e=e>>>8^i[255&(e^t[a])];return-1^e}(0|t,e,e.length,0):function(e,t,r,n){var i=o,s=n+r;e^=-1;for(var a=n;a<s;a++)e=e>>>8^i[255&(e^t.charCodeAt(a))];return-1^e}(0|t,e,e.length,0):0}},{"./utils":32}],5:[function(e,t,r){"use strict";r.base64=!1,r.binary=!1,r.dir=!1,r.createFolders=!0,r.date=null,r.compression=null,r.compressionOptions=null,r.comment=null,r.unixPermissions=null,r.dosPermissions=null},{}],6:[function(e,t,r){"use strict";var n=null;n="undefined"!=typeof Promise?Promise:e("lie"),t.exports={Promise:n}},{lie:37}],7:[function(e,t,r){"use strict";var n="undefined"!=typeof Uint8Array&&"undefined"!=typeof Uint16Array&&"undefined"!=typeof Uint32Array,i=e("pako"),s=e("./utils"),a=e("./stream/GenericWorker"),o=n?"uint8array":"array";function h(e,t){a.call(this,"FlateWorker/"+e),this._pako=null,this._pakoAction=e,this._pakoOptions=t,this.meta={}}r.magic="\b\0",s.inherits(h,a),h.prototype.processChunk=function(e){this.meta=e.meta,null===this._pako&&this._createPako(),this._pako.push(s.transformTo(o,e.data),!1)},h.prototype.flush=function(){a.prototype.flush.call(this),null===this._pako&&this._createPako(),this._pako.push([],!0)},h.prototype.cleanUp=function(){a.prototype.cleanUp.call(this),this._pako=null},h.prototype._createPako=function(){this._pako=new i[this._pakoAction]({raw:!0,level:this._pakoOptions.level||-1});var t=this;this._pako.onData=function(Line truncated
|
||||