'use strict';
//* TODO ---------------------- UI, menu: -------------------------------------
//* TODO: consistent UI colors (what blue, yellow, white, etc. means across the whole page).
//* TODO: checkbox/selectbox to sync clicked/all option/export actions in selected project onto all opened projects, where possible.
//* TODO: find zoom/scale of the screen/page before regenerating thumbnails.
//* TODO: options menu: add/remove/copy/edit colors and outlines, or all list(s), maybe in textarea.
//* TODO: options menu: buttons to show all or only relevant select boxes, global and per type header (parts, colors, etc).
//* TODO: .
//* TODO ---------------------- params: ---------------------------------------
//* TODO: keep all layer-name parameters single-word, if possible.
//* TODO: zoom format in filenames: [x1, x1.00, x100%].
//* TODO: outline: more methods.
//* TODO: wireframe rendering.
//* TODO: multiselect?
//* TODO: colors: fix empty folders like "listname [colors batch]" and "listname [colors] / name".
//* TODO: colors: add "name1,2,3,etc[gradient-map=N/N%=rgb-N-N-N/N%=next+rgb-N-N-N/avg|max|min|rgb]" to interpolate between selected given color values in given name order using given source RGB (or avg/max/min of them). If too many gradient points (number of names > 2 + number points), ignore leftover points. If too many names, distribute undefined points evenly in the last (top?) stretch of gradient. Autosort points by %value. Color value after percent may be used to insert given color value or calculate value dependent on next/previous point (cycle in passes until all are defined). 0/100% may be used for defining colors; use names for omitted. If no usable color names, do nothing.
//* TODO ---------------------- rendering: ------------------------------------
//* TODO: collage: fix stuck rendering of oversized collage.
//* TODO: collage: arrange joined images without using DOM, to avoid currently visible images moving to hidden container when saving collage.
//* TODO: clipping: fix hiding of clipping group with skipped/invisible/empty base layer.
//* TODO: blending: maybe skip creating base data copy, unless necessary (for transition back in passthrough folders).
//* TODO: blending: arithmetic emulation of all operations, not native to JS.
//* TODO: blending: arithmetic emulation of all operations in 16/32-bit (float?) until final result; optional with checkbox/selectbox.
//* TODO: blending: keep layer images as PNGs, create arrays for high-precision blending on demand, discard arrays when HQ mode is disabled.
//* TODO: encode: set RGB of zero-alpha pixels to average of all non-zero-alpha neighbour RGB values, ignoring alpha.
//* TODO: compose: for files without merged image data - render ignoring options, but respecting layer visibility properties. Or buttons to show embedded and/or rendered image regardless of options. Or add this as top-most option for any project, with or without options.
//* TODO: batch: to avoid bruteforcing global cross-products, build a tree-graph of selectable option dependency forks when loading a project. Make a graph from each separated root, but include unconditional [no-render] paths into each tree for color collections, etc.
//* TODO ---------------------- export: ---------------------------------------
//* TODO: save opened project as JSON (maybe name it BRA, Base64Raster, to open in JS drawpads).
//* TODO: save opened project as SVG (probably more complicated, but widespread).
//* TODO: save opened project as PSD (and PSB). Try https://github.com/Agamnentzar/ag-psd
//* TODO: save opened project as QRA/ORQ/QOJ (ORA with QOI instead of PNG, and maybe JSON instead of XML)?
//* TODO: save images: QOI, via asm.js? https://phoboslab.org/log/2021/11/qoi-fast-lossless-image-compression
//* TODO: save images: WebP, JPEG XL. https://bugs.chromium.org/p/chromium/issues/detail?id=170565#c77 - toDataURL/toBlob quality 1.0 = lossless.
//* TODO: make exported project files identically reproducible, if possible. Now works as long as source file, its mod.date and result layer tree are the same.
//* TODO: compatible PSD -> ORA layer mode/structure conversions; on load/save/both?
//* TODO ---------------------- other: ----------------------------------------
//* TODO: fix stuck loading of ORA with big PNGs while optimizing them all by UPNG.js.
//* TODO: fix stuck loading of PSD after failed PSB.
//* TODO: lazy-loading images (layers, merged, thumbnail) with ag-psd (seems impossible without reparsing whole file again).
//* TODO: don't add custom properties to objects of built-in types, if possible.
//* TODO: global job list for WIP cancelling instead of spaghetti-coded flag checks.
//* TODO: split JS files in a way that lets older browsers to use parts they can parse and execute (only show menu, or only load ORA, etc.).
//* TODO: split JS functionality into modules to reuse with drawpad, etc.
//* TODO: whole config in JSON-format?
//* ---------------------------------------------------------------------------
//* Config: defaults, do not change here, redefine in external config file *---
var exampleRootDir = ''
, exampleProjectFiles = []
, DEFAULT_ALPHA_MASK_PADDING = 1
, DEFAULT_ALPHA_MASK_THRESHOLD = 16
, DEFAULT_AUTOCROP = 'top-left/transparent'
, DEFAULT_COLLAGE_ALIGN = 'top-left'
, DEFAULT_COLLAGE_COLORS = [
'Transparent'
, 'White'
, 'Gray'
, 'Black'
, 'MidnightBlue'
, 'Teal'
, 'SteelBlue'
, 'LightSlateGray'
, 'DeepSkyBlue'
, 'LightBlue'
, 'LightGreen'
, 'LightYellow'
, 'Magenta'
]
, DEFAULT_COLLAGE_PADDING = 0 //* <- add variant to both inside and outside
, DEFAULT_COLLAGE_PADDING_INSIDE = 2
, DEFAULT_COLLAGE_PADDING_OUTSIDE = 1
, PAUSE_WORK_DURATION = 20
, PAUSE_WORK_INTERVAL = 200
, ORA_EXPORT_THUMBNAIL_SIZE = 256
, PREVIEW_SIZE = 80
, THUMBNAIL_SIZE = 20
, ZOOM_STEP_MAX_FACTOR = 2
//* If set to 0, use common setting from above:
, TAB_PREVIEW_SIZE = 0
, TAB_THUMBNAIL_SIZE = 0
, TAB_ZOOM_STEP_MAX_FACTOR = 0 //* <- scaling in one step is blocky, stepping by factor of 2 is blurry, 4 may be okay for 20px and less.
//* Any truthy/falsy value should work:
, ADD_BATCH_COUNT_ON_BUTTON = false //* <- if not, add separate text element.
, ADD_BATCH_COUNT_ON_NEW_LINE = false
, ADD_PAUSE_AT_INTERVALS = true //* <- let UI update when loading files, rendering images, counting batch combinations, etc.
, ADD_PAUSE_BEFORE_EACH_FOLDER = false //* <- can take ~1-5x longer than pause at intervals, but UI response is not very good.
, ADD_PAUSE_BEFORE_EACH_LAYER = false //* <- can take ~1.5-2x longer than pause at folders, but UI response does not improve much.
, ADD_WIP_TEXT_ROLL = false //* <- rotating stick symbol, does not look good in tabs
, ASK_BEFORE_EXIT_IF_OPENED_FILES = true //* <- this is annoying and would not be needed if big files could load fast.
, CACHE_UNALTERABLE_FOLDERS_MERGED = true //* <- not much opportunity if almost everything is recolored or passthrough.
, CACHE_UNALTERABLE_IMAGES_TRIMMED = false //* <- use less memory for kept bitmap, but more CPU for trimming; untrimmed images are compressed fast by canvas API when stored as PNG, but need more memory for empty areas when uncompressed for drawing.
, CLEAR_CANVAS_FOR_GC = true
, DEDUPLICATE_LOADED_IMAGES = false
, DOWNSCALE_BY_MAX_FACTOR_FIRST = true //* <- other way (starting with partial factor) is not better, probably worse.
, EXAMPLE_NOTICE = false //* <- show the warning near the list of files.
, FILE_NAME_ADD_PARAM_KEY = true
, FILE_NAME_OMIT_SINGLE_OPTIONS = true
, FILE_NAMING_SUMMARY_HEADER = true
, LAYER_IMAGES_FROM_BITMAP = true
, LAYER_IMAGES_PRELOAD_ALL = false
, LAYER_IMAGES_PRELOAD_USED = false
, LOCALIZED_CASE_BY_CROSS_COUNT = false //* <- determine word case by product of all numbers in args; if not, then by the last number.
, LOG_ACTIONS = false
, LOG_GROUPING = false //* <- becomes a mess with concurrent operations.
, LOG_TIMERS = false
, PNG_OPTIMIZE_ALL = false
, PNG_OPTIMIZE_COLOR_FILL = true //* <- convert into 3/4-byte RGB(A) array on load, compress with UPNG on save.
, PNG_OPTIMIZE_UNCOMPRESSED = true //* <- saved by Krita, relying only on ORA file ZIP compression instead; but in JS it's slow to unzip and bloats RAM storage; on the other hand, ORA files with recompressed PNGs may get bigger.
, PNG_USE_UPNG = true //* <- otherwise - canvas, which depends on browser implementation
, PNG_USE_UZIP = false //* <- otherwise - pako, which is good for all uses
, READ_FILE_CONTENT_TO_GET_TYPE = false //* <- this relies on the browser or the OS to magically determine file type.
, REQUIRE_NON_EMPTY_SELECTION = false //* <- buggy
, SAVE_ADDITIONAL_LAYER_NAMES = true //* <- verbose names for mask image layers, wrapper folders, etc. to store PSD features into ORA.
, SAVE_COLOR_AS_ONE_PIXEL_IMAGE = false //* <- and stretch by non-standard layer attributes; anyway, color fill is super-compressible in PNG even at full image size.
, SAVE_OPACITY_ROUNDED = false
, SAVE_ORA_CUSTOM_PROPERTIES = false
, SAVE_WITH_SELECTED_PRERENDER = true //* <- reuse dress-up image, because ora.js rendering is currently very basic.
, SORT_OPTION_LIST_NAMES = true
, SORT_OPTION_SECTION_NAMES = false
, START_WITH_BIG_TEXT = false
, START_WITH_FIXED_TAB_WIDTH = true
, START_WITH_OPEN_FIRST_MENU_TAB = true
, TAB_STATUS_TEXT = true
, TAB_THUMBNAIL_PRELOAD = true //* <- get merged prerendered image from the file before looking at layer tree.
, TAB_THUMBNAIL_TRIMMED = false //* <- more content may become visible, but will take more time and shift image alignment.
, TAB_THUMBNAIL_ZOOM = true
, TAB_THUMBNAIL_ZOOM_TRIMMED = false
, TAB_WIDTH_ONLY_GROW = true //* <- prevent tabs from shrinking and jumping between rows.
, TESTING = false //* <- dump more info into the console; several levels are possible.
, TESTING_PNG = false //* <- dump a PNG onto the page after converting from pixel data.
, TESTING_QOI = false //* <- compare encode to decode and average timings of repeated runs of various methods.
, TESTING_RENDER = false //* <- dump a PNG onto the page after each rendering operation.
, TESTING_RENDER_CACHE = false //* <- dump a PNG onto the page after each cached crop or merge.
, USE_CRITERIA_ARRAY = true
, USE_MINIFIED_JS = true //* <- currently only pako.
, USE_WORKERS = true //* <- not possible when page is opened from disk
, VERIFY_PARAM_COLOR_VS_LAYER_CONTENT = false
, ZERO_PERCENT_EQUALS_EMPTY = false
, ZIP_SKIP_DUPLICATE_FILENAMES = true
, ZIP_USE_ASM = false //* <- otherwise - pako, which is good for all uses
, ZIP_USE_CODECS = true //* <- asm or pako instead of zip.js own.
, ZIP_USE_ONE_FILE_WORKER = false //* <- concatenated bundle, which is not included in distribution by default.
//* Array/Map/Set/Object containers all work fine.
//* Set is the fastest in this case, tested in Firefox 56, Basilisk 2021.03.17 (Firefox 56-based) and Vivaldi 3.7.2218.55 (latest Chrome-based).
//* Map is 2nd fastest, but almost the same as Array/Object.
, PROJECT_BLOBS_CONTAINER_TYPE = Set
;
//* ---------------------------------------------------------------------------
//* Create type-checking functions, e.g. "isString()" or "isImageElement()":
//* Source: https://stackoverflow.com/a/17772086
[
'Array', //* <- matches anything with 'Array' at the end, e.g. 'Uint8Array'
'ArrayBuffer',
'Blob',
'CanvasElement',
'Date',
'Function', //* <- matches anything with 'Function' at the end, e.g. 'AsyncFunction'
'ImageData',
'ImageElement', //* <- matches anything with 'ImageElement' at the end, e.g. 'HTMLImageElement'
'Number',
'Promise',
'RegExp',
'SelectElement',
'String',
].forEach(
(typeName) => {
const functionName = 'is' + typeName;
if (typeof window[functionName] !== 'function') {
window[functionName] = (value) => (toString.call(value).slice(-1 - typeName.length, -1) === typeName);
}
}
);
//* ---------------------------------------------------------------------------
//* Create common utility functions before using them in config:
const getFlatArray = (
typeof Array.prototype.flat === 'function'
//* Modern way:
? (array, maxDepth) => Array.prototype.flat.call(array, isRealNumber(maxDepth) ? maxDepth : Infinity)
//* Legacy way:
: (array, maxDepth) => {
if (!isRealNumber(maxDepth)) {
maxDepth = Infinity;
}
let flatArray = [];
for (const value of array) {
if (isArray(value)) {
if (maxDepth > 0) {
flatArray = flatArray.concat(getFlatArray(value, maxDepth - 1));
} else {
flatArray = flatArray.concat(value);
}
} else {
flatArray.push(value);
}
}
return flatArray;
}
);
//* Config: internal, do not change *------------------------------------------
const CONFIG_FILE_PATH = 'config.js' //* <- declarations-only file to redefine any of the above variables
, FETCH_TEST_FILE_PATH = 'index.png' //* <- smallest local file to test loading from disk
, LS_NAMESPACE = 'spriteDressUp'
, LS_KEY_BIG_TEXT = LS_NAMESPACE + 'BigText'
, LS_KEY_FIXED_TAB_WIDTH = LS_NAMESPACE + 'FixedTabWidth'
, LANG_FALLBACK = 'en'
, LANG_IN_DOCUMENT = document.documentElement.lang
, LS = window.localStorage || localStorage
, URL = window.URL || window.webkitURL || URL
, RUNNING_FROM_DISK = isURLFromDisk('/')
, CAN_USE_WORKERS = (typeof Worker === 'function' && !RUNNING_FROM_DISK)
, pendingJobs = new Set()
, regLayerNameParamOrComment = new RegExp(
'^'
+ '([^/\\[(]*)' //* <- [1] layer identity names, e.g. "name1, name2"
+ '(' //* <- [2] block of logic for any of identity names
+ '/' //* <- virtual subfolder, e.g. "parent names [parent param] / child names [child param]"
+ '|' + '\\[([^\\]]*)(?:\\]|$)' //* <- [3] options, e.g. "name1 [param] name2"
+ '|' + '\\([^)]*(?:\\)|$)' //* <- throwaway text, e.g. "name1 (comment) name2"
+ ')'
+ '(.*?)' //* <- [4] remainder for next step
+ '$'
)
, regLayerNameToSkip = null //* <- old: /^(skip)$/i
, regSplitLayerNames = /[\/,]+/g
, regSplitParam = /[\s\r\n]+/g //* <- old: /[\s,_]+/g
, regTrimParam = getTrimReg('\\s\\r\\n,_')
, regTrimParamRadius = /^(px|at)+|(px|at)+$/ig
, regEndsWithNumPx = /^(?:(.*?)\W+)?(\d+)px$/i
, regColorCode = /^(?:(rgba?)\W*|(hex)\W*|(#))(\w.*?)$/i
, regLayerNameParamType = {
'skip' : /^(skip)$/i
, 'skip_render' : /^(skip|no)-(render)$/i
, 'check_order' : /^(?:(?:check|top|bottom)\W+)*(down|up)$/i //* <- switch for speed/complexity testing, not useful
, 'none' : /^(none)$/i
, 'if_only' : /^(if|in)$/i
, 'not' : /^(\!|not?)$/i
, 'any' : /^(\?|any|some)$/i
, 'copypaste' : /^(copy|paste(?:-(?:above|below))?)(?:\W(.*))?$/i
, 'color_code' : regColorCode
, 'colors' : /^(colou?r)s$/i
, 'parts' : /^(option|part|type)s$/i
, 'paddings' : /^(outline|pad[ding]*)$/i
, 'radius' : /^(.*?\d.*)px(?:\W(.*))?$/i
// , 'wireframe' : /^(?:wire\W*)?(frame|fill)$/i
, 'opacities' : /^(?:(?:opacit[yies]*)\W*)?(\d[^%]*)%(\d*)$/i
, 'zoom' : /^(?:(?:zoom|scale|x)\W*)(\d[^%]*)%(\d*)$/i
, 'side' : /^(front|back|reverse(?:\W(.*))?)$/i
, 'separate' : /^((?:separate|split)\w*)(?:-((e)?\w*))?(?:\W(.*))?$/i
, 'autocrop' : /^(autocrop)(?:\W(.*))?$/i
, 'collage' : /^(collage)(?:\W(.*))?$/i
, 'layout' : /^(?:(inline|rows)|(newline|columns))$/i
, 'batch' : /^(?:(single|no-batch)|(batch|batched))?$/i
, 'prefix' : /^(?:(prefix|prefixed)|(unprefixed|no-prefix))?$/i
, 'option' : /^(?:(omitable|(?:omit|no)-single(?:-name)?)|(unomitable|(?:add-|)single(?:-name)?))$/i
, 'naming_order' : /^((?:file-?)(?:name|naming)(?:-order))(?:\W(.*))?$/i
, 'multi_select' : /^(optional|required|x(\d[\d+-]*))$/i
, 'preselect' : /^(preselect|initial|(last))$/i
}
, regLayerBlendModePass = /^pass[-through]*$/i
, regLayerBlendModeAlpha = /^(source|destination|xor)(-\w+)?$/i
, regLayerBlendModeClip = /^(source|destination)-(atop)$/i
, regLayerBlendModeMask = /^(source|destination)-(in|out)$/i
, regLayerTypeSingleTrim = /s+$/i
, regSanitizeLayerName = /\([^\)]*\)|\[[^\]]*\]/g
, regSanitizeLayerComment = /[\(\)\[\]\{\}\<\>]+/g
, regSanitizeFileName = /[_\s\/\\:<>?*"]+/g
, regHMS = /(T\d+)-(\d+)-(\d+\D*)/
, regMultiDot = /\.\.+/g
, regNumDots = /[\d.]+/g
, regNaNOrDot = /[^\d.]+/g
, regNaN = /\D+/g
, regNonWord = /\W+/g //* <- "\w" includes underscore "_"
, regNonHex = /[^0-9a-f]+/gi
, regNonAlphaNum = /[^0-9a-z]+/gi
, regHasDigit = /\d/
, regSpace = /\s+/g
, regCommaSpace = /\,+\s*/g
, regTemplateVarName = /\{(\w+)\}/g
, regTrimBrackets = getTrimReg('\\(\\)\\[\\]\\{\\}\\<\\>')
, regTrimNaN = getTrimReg('\\D')
, regTrimNaNOrSign = getTrimReg('^\\d\\.+-')
, regTrimSpace = getTrimReg('\\s')
, regTrimSpaceOrComma = getTrimReg('\\s,')
, regTrimSpaceBeforeNewLine = /[^\S\r\n]*(\r\n|\r|\n)/g
, regTrimTailBrTags = /(\
\s*)+$/gi
, matchClassButton = getCriteria('button')
, matchClassExampleFile = getCriteria('example-file', 'file')
, matchClassExampleFiles = getCriteria('example-file-type', 'example-files', 'files')
, matchClassLoaded = getCriteria('loaded')
, matchClassLoadedFile = getCriteria('loaded-file', 'file')
, matchClassMenuBar = getCriteria('menu-bar')
, matchClassOption = getCriteria('project-option', 'option')
, matchClassSection = getCriteria('section')
, matchClassSub = getCriteria('sub')
, regJSONstringify = {
'asFlatLine' : /^(data)$/i
, 'asFlatLines' : /^(layers)$/i
, 'skipByKey' : /^(channels|parent|sourceData\w*)$/i
, 'skipByKeyIfLong' : /^(imageData)$/i
, 'showFromTree' : /^(layers|name)$/i
}
const SPLIT_SEC = 60
, MIN_CHANNEL_VALUE = 0
, MAX_CHANNEL_VALUE = 255
, MAX_BATCH_PRECOUNT = 9999
, FLAG_FLIP_HORIZONTAL = 1
, FLAG_FLIP_VERTICAL = 2
, FLAG_EVENT_LISTENER_CAPTURE = {
'capture' : true,
'passive' : false,
}
, FLAG_EVENT_NO_DEFAULT = { 'preventDefault' : true }
, FLAG_EVENT_STOP_IMMEDIATE = { 'stopImmediatePropagation' : true }
, FLAG_EVENT_STOP_DEFAULT = {
'preventDefault' : true,
'stopImmediatePropagation' : true,
}
, FLAG_FILENAME_AS_KEY = { 'isForStorageKey' : true }
, FLAG_FILENAME_TO_SAVE = {
'checkSelectedValue' : true,
'skipDefaultPercent' : true,
}
, FLAG_FILENAME_TO_SAVE_HTML = {
'addColorsWithHTML' : true,
...FLAG_FILENAME_TO_SAVE,
}
, FLAG_JOIN_TEXT_FILTER = { 'filter' : true }
, FLAG_LAYER_PATH_TEXT = {
'includeSelf' : true,
'asText' : true,
}
, FLAG_PROJECT_SET_THUMBNAIL = { 'alsoSetThumbnail' : true }
, FLAG_RENDER_LAYER_COPY = { 'isCopyNeeded' : true }
, FLAG_RENDER_IGNORE_COLORS = { 'ignoreColors' : true }
, FLAG_SAVE_ALL = { 'saveToFile' : true }
, FLAG_SAVE_ZIP = { 'saveToZipFile' : true }
, FLAG_SHOW_JOIN = { 'asOneJoinedImage' : true }
, FLAG_SAVE_JOIN = {
'saveToFile' : true,
'asOneJoinedImage' : true,
}
, FLAG_TIME_LOG_FORMAT = { 'logFormat' : true }
, FLAG_TIME_FILENAME_FORMAT = { 'fileNameFormat' : true }
, DUMMY_EMPTY_ARRAY = Object.freeze( [] ) //* <- immutable, for skipping iteration on non-collectable results
, DUMMY_NULL_ARRAY = Object.freeze( [null] ) //* <- immutable, for cross-product combinations
, DUMMY_TEXT_ARRAY = Object.freeze( [''] )
, DEFAULT_COLOR_VALUE_ARRAY = Object.freeze( [0,0,0, MAX_CHANNEL_VALUE] )
, TRANSPARENT_COLOR_VALUE_ARRAY = Object.freeze( [0,0,0, MIN_CHANNEL_VALUE] )
, DUMMY_OPTION_PARAMS = Object.freeze({
'noOptionSwitches' : true,
'skipInFileNames' : true,
'preselect' : 'last',
})
, DUMMY_OPTIONAL_PARAMS = Object.freeze({
'optional' : true,
...DUMMY_OPTION_PARAMS,
})
, DUMMY_LAYER = Object.freeze({
'name' : 'dummy',
'names' : DUMMY_TEXT_ARRAY,
})
, LAYER_NAME_MASK_IMAGE = 'Mask image'
, LAYER_NAME_MASKED_CONTENT = 'Masked content'
, LAYER_NAME_CLIPPED_CONTENT = 'Clipped content' //* <- wrapper for layers with blending modes
, LAYER_NAME_CLIPPING_GROUP = 'Clipping group'
, FALSY_STRINGS = [
'0'
, 'no'
, 'none'
, 'null'
, 'false'
, 'hidden'
, 'disabled'
, 'undefined'
]
, DRAG_ORDER_PREFIX = 'drag-order:\n'
, BLOB_PREFIX = 'blob:'
, DATA_PREFIX = 'data:'
, TYPE_TEXT = 'text/plain'
, TYPE_IMAGE_PNG = 'image/png'
, TITLE_LINE_BREAK = ' \r\n'
, DOUBLE_LINE_BREAK = ' \r\n\r\n'
, WIP_TEXT_ROLL = '\\|/-'
, TIME_PARTS_YMD = ['FullYear', 'Month', 'Date']
, TIME_PARTS_HMS = ['Hours', 'Minutes', 'Seconds']
, QUERY_SELECTOR = {
'getElementsByClassName' : ['.', ''],
'getElementsByTagName' : ['', ''],
'getElementsByName' : ['*[name="', '"]'],
'getElementsByType' : ['*[type="', '"]'],
'getElementsById' : ['*[id="', '"]'],
}
, DEFAULT_MASK_FILL_COLOR = 'black'
, ISOLATE_ALL = 'isolate'
, ISOLATE_ALPHA = 'isolate-alpha'
, ISOLATE_AUTO = 'auto'
, ISOLATE_PASS = 'pass-through'
, BLEND_MODE_NORMAL = 'source-over'
, BLEND_MODE_CLIP = 'source-atop'
, BLEND_MODE_MASK = 'destination-in'
, BLEND_MODE_CUT = 'destination-out'
, BLEND_MODE_INVERT = 'source-out'
, BLEND_MODE_ADD = 'lighter'
, BLEND_MODE_PASS = 'pass'
, BLEND_MODE_FADE_IN = 'transition-in'
, BLEND_MODE_FADE_OUT = 'transition-out'
, BLEND_MODES_ALPHA_PREFIXES = ['source', 'destination', 'src', 'dst', 'xor']
//* Source: https://www.w3.org/TR/SVGCompositing/#comp-op-property
, BLEND_MODES_IN_SVG = [
'clear'
, 'src'
, 'dst'
, 'src-over'
, 'dst-over'
, 'src-in'
, 'dst-in'
, 'src-out'
, 'dst-out'
, 'src-atop'
, 'dst-atop'
, 'xor'
, 'plus'
, 'multiply'
, 'screen'
, 'overlay'
, 'darken'
, 'lighten'
, 'color-dodge'
, 'color-burn'
, 'hard-light'
, 'soft-light'
, 'difference'
, 'exclusion'
, 'inherit'
]
, BLEND_MODES_REMAP_TO_ORA = {
[ BLEND_MODE_ADD ] : 'svg:plus'
}
, BLEND_MODES_REPLACE_TO_ORA = [
['source', 'src']
, ['destination', 'dst']
, [new RegExp('^(' + BLEND_MODES_IN_SVG.join('|') + ')$', 'i'), 'svg:$1']
]
, BLEND_MODES_REPLACE = [
['src', 'source']
, ['dst', 'destination']
, ['liner', 'linear']
, [/^.*:/g] //* <- remove any "prefix:"
, [/[\s\/_-]+/g, '-'] //* <- normalize word separators to use only dashes
, [/^subs?tr[au]ct$/, 'subtract']
, [regLayerBlendModePass, BLEND_MODE_PASS]
]
, BLEND_MODES_REMAP = {
'normal' : BLEND_MODE_NORMAL
, 'add' : BLEND_MODE_ADD
, 'plus' : BLEND_MODE_ADD
, 'linear-dodge' : BLEND_MODE_ADD
//* From SAI2, remap according to PSD.js:
, 'burn' : 'color-burn'
, 'burn-dodge' : 'vivid-light'
, 'darken-color' : 'darker-color'
, 'dodge' : 'color-dodge'
, 'exclude' : 'exclusion'
, 'lighten-color' : 'lighter-color'
, 'shade' : 'linear-burn'
, 'shade-shine' : 'linear-light'
, 'shine' : BLEND_MODE_ADD
}
, BLEND_MODES_WITH_TS_VERSION = [
BLEND_MODE_ADD
, 'color-burn'
, 'color-dodge'
, 'difference'
, 'hard-mix'
, 'linear-burn'
, 'linear-dodge'
, 'linear-light'
, 'vivid-light'
]
//* Source: https://github.com/Braunbart/PSDLIB.js
, PSD_COLOR_MODES = [
'Bitmap'
, 'Grayscale'
, 'Indexed'
, 'RGB'
, 'CMYK'
, 'HSL'
, 'HSB'
, 'Multichannel'
, 'Duotone'
, 'Lab'
]
, OPEN_CLOSE = ['open', 'close']
, COLOR_LIST_NAMES = ['background']
, PARAM_KEYWORDS_AUTOCROP = ['transparent', 'topleft', 'topright', 'bottomleft', 'bottomright']
, PARAM_KEYWORDS_COLLAGE_ALIGN = ['topleft', 'topright', 'bottomleft', 'bottomright', 'top', 'bottom', 'left', 'right']
, PARAM_KEYWORDS_COLLAGE_PAD = ['border', 'padding']
, PARAM_KEYWORDS_PADDING_METHODS = ['max', 'min']
, PARAM_KEYWORDS_SET_VALUE_TO_NAME = ['preselect']
, PARAM_KEYWORDS_SET_VALUE_TO_TRUE = ['last', 'optional', 'required', 'no_prefix']
, PARAM_KEYWORDS_SHORTCUT_FOR_ALL = ['all', 'etc']
, PARAM_KEYWORDS_PASTE = ['paste', 'paste-above', 'paste-below']
, PARAM_OPTIONS_FOR_EACH_NAME = ['opacities', 'paddings']
, PARAM_OPTIONS_GLOBAL = ['autocrop', 'collage', 'separate', 'side', 'zoom']
, PARAM_OPTIONS_LOCAL = ['parts', 'colors', 'paddings', 'opacities']
, VIEW_SIDES = ['front', 'back']
, SEPARATE_NAMING_TYPES = ['equal', 'numbered']
, SEPARATE_PARAM_NAMES_DEFAULT = ['separate', 'split']
, SEPARATE_GROUP_NAME_DEFAULT = ''
, NAME_PARTS_SEPARATOR = ''
, NAME_PARTS_COLORED_CLASSES = ['selected-parts', 'list-name', 'option-name']
, NAME_PARTS_PERCENTAGES = ['zoom', 'opacities']
, NAME_PARTS_FOLDERS = ['parts', 'colors']
, NAME_PARTS_ORDER = ['separate', 'side', 'parts', 'colors', 'paddings', 'opacities', 'zoom', 'autocrop']
, NAME_PARTS_ORDER_PARAMS = [
'given',
'given-lists',
'given-options',
'given-sections',
'given-types',
'sort',
'sort-lists',
'sort-options',
'sort-sections',
'sort-types',
...NAME_PARTS_ORDER
]
, PARAM_OPTIONS_ADD_BY_DEFAULT = {
'collage' : ['no-batch', 'last', 'optional', 'collage']
, 'autocrop' : ['no-batch', 'last', 'optional', 'autocrop']
}
, SWITCH_LABEL_BY_CLASS = {
'batch-batched' : '[\u2E2C]'
, 'batch-single' : '[\u2E30]'
, 'layout-inline' : '[\u22EF]'
, 'layout-newline' : '[\u22EE]'
, 'option-omitable' : '[\u2212]'
, 'option-unomitable' : '[+]'
, 'prefix-prefixed' : '[=]'
, 'prefix-unprefixed' : '[o]'
}
, SWITCH_CLASS_BY_INDEX = ['unchecked', 'checked']
, SWITCH_NAMES_BY_TYPE = {
'batch' : ['single', 'batched']
, 'layout' : ['inline', 'newline']
, 'prefix' : ['prefixed', 'unprefixed']
, 'option' : ['omitable', 'unomitable']
}
, SWITCH_NAMES_DEFAULT = {
'batch' : 'batched'
, 'layout' : 'inline'
, 'prefix' : 'prefixed'
, 'option' : 'omitable'
}
, PROJECT_OPTION_GROUPS = [
{
'header' : 'option_header_collage',
'select' : {
'collage' : {
'align' : 'option_collage_align',
'background' : 'option_collage_background',
'border' : 'option_collage_border',
'padding' : 'option_collage_padding',
},
},
},
{
'header' : 'option_header_separate',
'select' : {
'separate' : {
'naming' : 'option_separate_naming',
'group' : 'option_separate_group',
'separate' : 'option_separate_part',
},
},
},
{
'header' : 'option_header_view',
'select' : {
'autocrop' : 'option_autocrop',
'zoom' : 'option_zoom',
'side' : 'option_side',
},
},
{
'parts' : 'option_header_parts',
'opacities' : 'option_header_opacities',
'paddings' : 'option_header_paddings',
'colors' : 'option_header_colors',
},
]
, EXAMPLE_CONTROLS = [
{
'download_all' : 'download_all',
'load_all' : 'open_example_all',
},
{
'stop' : 'stop',
},
]
, PROJECT_FILE_CONTROLS = [
'show_project_details',
]
, PROJECT_VIEW_CONTROLS = [
{
'header' : 'original_view_header',
'buttons' : {
'show_original_png' : '',
'save_original_png' : '',
// 'save_original_qoi' : '',
'save_original_ora' : '',
'save_original_ora_all_layers' : '',
'save_original_ora_used_layers' : '',
},
},
{
'header' : 'current_view_header',
'buttons' : {
'show_png' : '',
'save_png' : '',
'save_qoi' : '',
'save_ora' : '',
'save_ora_all_layers' : '',
'save_ora_used_layers' : '',
},
},
{
'header' : 'batch_view_header',
'buttons' : {
'batch' : {
'show_all_png' : 'show_png_batch',
'save_all_png' : 'save_png_batch',
'save_zip_png' : 'save_png_batch_zip',
'save_zip_qoi' : 'save_qoi_batch_zip',
},
'collage' : {
'show_join_png' : 'show_png_collage',
'save_join_png' : 'save_png_collage',
'save_join_qoi' : 'save_qoi_collage',
'stop' : 'stop',
},
// 'stop' : 'stop',
},
},
]
, PROJECT_HIDDEN_CONTROLS = [
{
'header' : 'reset_header',
'buttons' : {
'options_init' : {
'reset_options_to_init' : '',
'reset_options_to_empty' : '',
},
'options' : {
'reset_options_to_top' : '',
'reset_options_to_bottom' : '',
},
'batching' : {
'reset_switch_batch_to_batched' : '',
'reset_switch_batch_to_single' : '',
},
'batching_layout' : {
'reset_switch_layout_to_inline' : '',
'reset_switch_layout_to_newline' : '',
},
'naming_omit' : {
'reset_switch_option_to_unomitable' : '',
'reset_switch_option_to_omitable' : '',
},
'naming_prefix' : {
'reset_switch_prefix_to_prefixed' : '',
'reset_switch_prefix_to_unprefixed' : '',
},
},
},
]
, PROJECT_SAVE_ALL_BUTTON_NAMES = Object.keys(
PROJECT_VIEW_CONTROLS.find(
(v) => v.buttons.batch
).buttons.batch
)
, PROJECT_NAMING_BUTTON_NAMES = FILE_NAMING_SUMMARY_HEADER ? [
'saved_file_naming_sort',
'saved_file_naming_reset_to_default',
'saved_file_naming_reset_to_initial',
] : [
'saved_file_naming_change',
'saved_file_naming_sort',
'saved_file_naming_reset_to_default',
'saved_file_naming_reset_to_initial',
'saved_file_naming_close',
]
, PROJECT_NAMING_EVENT_HANDLERS = {
'dragstart' : onPanelDragStart,
'dragenter' : onPanelDragMove,
'drop' : onPanelDragMove,
}
, PROJECT_CONTROL_TAGNAMES = [
'button',
'select',
'input',
]
, IMAGE_FIT_CLASSES = [
'size-fit',
'size-full',
]
, IMAGE_GEOMETRY_KEYS = [
['top', 'y'],
['left', 'x'],
['width', 'w'],
['height', 'h'],
]
, VIRTUAL_FOLDER_TAKEOVER_PROPERTIES = {
'blendMode' : BLEND_MODE_NORMAL,
'isBlendModeTS' : false,
'isClipped' : false,
'clippingLayer' : null,
}
, IMAGE_DATA_KEYS_TO_LOAD = [
'toImage',
'loadImage',
'imgData',
'maskData',
'pixelData',
]
, CLEANUP_PROJECT_LAYERS_RECURSIVE_KEYS = ['layers']
, CLEANUP_PROJECT_LAYERS_MERGED_CACHE_KEYS = [
'mergedImage',
'mergedImageToRecolor',
]
, CLEANUP_PROJECT_AFTER_LOAD_KEYS = [
'loading',
// 'loadImage', //* <- keep for lazy-loading
'toPng',
]
, CLEANUP_PROJECT_IF_NOT_TESTING_KEYS = [
// 'blendModeOriginal', //* <- keep for saving
'blendModeOriginalAlpha',
'blendModeOriginalColor',
// 'nameOriginal', //* <- keep for saving
// 'sourceDataFile', //* <- keep for saving
'sourceDataNode',
'sourceData',
'pixelData',
'maskData',
// 'imgData', //* <- keep for lazy-loading
...CLEANUP_PROJECT_AFTER_LOAD_KEYS
]
, SAVE_PROJECT_FILE_TYPES = ['ora']
, SAVE_IMAGE_FILE_TYPES = ['png', 'qoi']
//* Config: internal, included scripts and loaders of project files *----------
const LIB_ROOT_DIR = 'lib/'
, LIB_FORMATS_DIR = LIB_ROOT_DIR + 'formats/'
, LIB_LANG_DIR = LIB_ROOT_DIR + 'localization/'
, LIB_UTIL_DIR = LIB_ROOT_DIR + 'util/'
, ZIP_FORMAT_DIR = LIB_FORMATS_DIR + 'zip/zip_js/'
, ZLIB_ASM_DIR = LIB_FORMATS_DIR + 'zlib/zlib-asm/v0.2.2/' //* <- last version supported by zip.js, ~ x2 faster than default
, ZLIB_PAKO_DIR = LIB_FORMATS_DIR + 'zlib/pako/v2.0.2/' //* <- good and fast enough for everything
;
//* External variable names, do not change *-----------------------------------
var PSD
, UPNG
, UZIP
, agPsd
, ora
, pako
, zip
, zlib
;
//* To be figured on the go *--------------------------------------------------
var LANG
, PSD_JS
, AG_PSD_FILE_READING_OPTIONS
, FILE_TYPE_LIBS
, FILE_TYPE_LOADERS
, ZIP_WORKER_SCRIPTS
, DEFAULT_COLLAGE
, PRELOAD_LAYER_IMAGES
, USE_ES5_JS
, USE_WORKERS_IF_CAN
, CompositionModule
, compositionFunctionNames
, canvasForTest
, ctxForTest
, draggedElement
, thumbnailPlaceholder
, isStopRequested
, isBatchWIP
, lastTimeProjectTabSelectedByUser = 0
, functionNameByBlendMode = {}
, rgbaCacheByColorName = {}
;
//* Config: internal, wrapped to be called after reading external config *-----
function initLibParams() {
USE_WORKERS_IF_CAN = (USE_WORKERS && CAN_USE_WORKERS);
const zlibCodecPNG = [PNG_USE_UZIP ? 'UZIP.js' : 'pako'];
const zlibCodecZIP = [ZIP_USE_ASM ? 'zlib-asm' : 'pako'];
const zipAllInOneFileName = 'z-worker-copy-all-in-one-file' + (
!ZIP_USE_CODECS
? ''
: ZIP_USE_ASM
? '-zlib-asm'
: USE_ES5_JS
? '-pako.es5'
: '-pako'
) + '.js';
const zipZlibCodecWrapper = (
ZIP_USE_ASM
? 'codecs-zlib-asm.js'
: 'codecs-pako.js'
);
const zlibPakoFileName = (
'pako'
+ (USE_ES5_JS ? '.es5' : '')
+ (USE_MINIFIED_JS ? '.min' : '')
+ '.js'
);
FILE_TYPE_LIBS = {
'zlib-asm' : {
//* Source: https://github.com/ukyo/zlib-asm
'varName' : 'zlib'
, 'dir' : ZLIB_ASM_DIR
, 'files' : ['zlib.js']
},
'pako' : {
//* Source: https://github.com/nodeca/pako
'varName' : 'pako'
, 'dir' : ZLIB_PAKO_DIR
, 'files' : [zlibPakoFileName]
},
'UZIP.js' : {
//* Source: https://github.com/photopea/UZIP.js
'varName' : 'UZIP'
, 'dir' : LIB_FORMATS_DIR + 'zlib/UZIP/'
, 'files' : ['UZIP.js']
, 'depends' : ['pako']
},
'UPNG.js' : {
//* Source: https://github.com/photopea/UPNG.js
'varName' : 'UPNG'
, 'dir' : LIB_FORMATS_DIR + 'png/UPNG/'
, 'files' : ['UPNG.js']
, 'depends' : zlibCodecPNG
},
'QOI.js' : {
//* Source: https://github.com/pfusik/qoi-ci
//* Note: swapped RGB channels order to match JS ImageData.
'varName' : 'QOIEncoder'
, 'dir' : LIB_FORMATS_DIR + 'qoi/qoi-ci/'
, 'files' : ['qoi-ci-1.1.1-mod-rgba-order.js']
},
'zip.js' : {
//* Source: https://github.com/gildas-lormeau/zip.js
'varName' : 'zip'
, 'dir' : ZIP_FORMAT_DIR
, 'files' : [
'zip.js',
'zip-fs.js',
...(
USE_WORKERS_IF_CAN
? DUMMY_EMPTY_ARRAY
:
//* CORS workaround: when not using Workers, include scripts here.
//* Source: https://github.com/gildas-lormeau/zip.js/issues/169#issuecomment-312110395
ZIP_USE_ONE_FILE_WORKER
? [zipAllInOneFileName]
:
ZIP_USE_CODECS
? [zipZlibCodecWrapper]
: [
'deflate.js',
'inflate.js',
]
)
]
, 'depends' : (
USE_WORKERS_IF_CAN
|| ZIP_USE_ONE_FILE_WORKER
|| !ZIP_USE_CODECS
? DUMMY_EMPTY_ARRAY
: zlibCodecZIP
)
},
'ora.js' : {
//* Source: https://github.com/zsgalusz/ora.js
//* No support for ORA in CSP, SAI, etc.
//* Not enough supported features in Krita, etc.
//* Way: draw in SAI2 → export PSD → import PSD in Krita (loses clipping groups) → export ORA (loses opacity masks)
//* Wish: draw in SAI2 → export ORA (all layers rasterized + all blending properties and modes included as attributes)
'varName' : 'ora'
, 'dir' : LIB_FORMATS_DIR + 'ora/ora_js/'
, 'files' : ['ora.js']
, 'depends' : ['zip.js']
},
'ora-blending.js' : {
//* Source: https://github.com/zsgalusz/ora.js
'varName' : 'blending'
, 'dir' : LIB_FORMATS_DIR + 'ora/ora_js/'
, 'files' : ['ora-blending.js']
},
'ag-psd' : {
//* Source: https://github.com/Agamnentzar/ag-psd
//* Builds: https://www.jsdelivr.com/package/npm/ag-psd?version=14.3.6
//* Bundle: https://cdn.jsdelivr.net/npm/ag-psd@14.3.6/dist/bundle.js
'varName' : 'agPsd'
, 'dir' : LIB_FORMATS_DIR + 'psd/ag-psd/'
, 'files' : ['ag-psd-14.3.6-dist-bundle.js'] //* <- also works with PSB files
},
'psd.js' : {
//* Source: https://github.com/meltingice/psd.js/issues/154#issuecomment-446279652
//* Based on https://github.com/layervault/psd.rb
'varName' : 'PSD_JS'
, 'dir' : LIB_FORMATS_DIR + 'psd/psd_js/psd.js_build_by_rtf-const_2018-12-11/' //* <- works with bigger files?
, 'files' : ['psd.js']
},
'psd.browser.js' : {
//* Source: https://github.com/imcuttle/psd.js/tree/release
//* Fork of https://github.com/meltingice/psd.js
'varName' : 'PSD'
, 'dir' : LIB_FORMATS_DIR + 'psd/psd_js/psd.js_fork_by_imcuttle_2018-09-19/' //* <- works here
, 'files' : ['psd.browser.js']
},
};
ZIP_WORKER_SCRIPTS = (
ZIP_USE_ONE_FILE_WORKER
? [
ZIP_FORMAT_DIR + zipAllInOneFileName
]
: ZIP_USE_CODECS
? [
ZIP_FORMAT_DIR + 'z-worker.js',
ZIP_FORMAT_DIR + zipZlibCodecWrapper,
...zlibCodecZIP.map(
(libName) => FILE_TYPE_LIBS[libName].files.map(
(fileName) => FILE_TYPE_LIBS[libName].dir + fileName
)
)
]
: null
);
AG_PSD_FILE_READING_OPTIONS = {
// skipThumbnail : !TAB_THUMBNAIL_PRELOAD,
// skipCompositeImageData : !TAB_THUMBNAIL_PRELOAD,
// skipLayerImageData : !LAYER_IMAGES_PRELOAD_ALL,
useImageData : true, // !!LAYER_IMAGES_FROM_BITMAP, //* <- "imageData" field instead of "canvas"
useRawThumbnail : true, // !!LAYER_IMAGES_FROM_BITMAP, //* <- "thumbnailRaw" field instead of "thumbnail"
logMissingFeatures : !!TESTING,
logDevFeatures : (TESTING > 9),
};
const psbAndPsdMimeTypes = getJoinedCrossProductTextSet(
[
[
'image'
, 'application'
], [
'psb'
, 'psd'
, 'photoshop'
, 'x-photoshop'
, 'vnd.adobe.photoshop'
]
]
, '/'
);
FILE_TYPE_LOADERS = [
{
'dropFileExts' : ['ora', 'zip']
, 'inputFileMimeTypes' : [
'image/openraster',
'application/zip',
]
, 'handlerFuncs' : [
loadORA,
]
},
{
'dropFileExts' : ['psd', 'psb']
, 'inputFileMimeTypes' : psbAndPsdMimeTypes
, 'handlerFuncs' : [
loadAgPSD,
]
},
{
'dropFileExts' : ['psd']
, 'inputFileMimeTypes' : psbAndPsdMimeTypes.filter((text) => !text.includes('psb'))
, 'handlerFuncs' : [
loadPSD,
// loadPSDBrowser, //* <- second parser is only needed if it could success on different files
]
},
];
}
//* Common utility functions *-------------------------------------------------
const dotExt = (ext) => `.${ String(ext).toLowerCase() }`;
const asArray = (value) => ( isArray(value) ? value : [value] );
const asFlatArray = (value) => getFlatArray(asArray(value || []));
const arrayFromObjectEntriesSortByKey = (a, b) => ( a[0] > b[0] ? 1 : a[0] < b[0] ? -1 : 0 );
//* Reassemble items to new array:
//* Source: https://stackoverflow.com/a/6470794
function arrayMoveItem(array, fromIndex, toIndex) {
return array.splice(toIndex, 0, array.splice(fromIndex, 1)[0]);
}
//* Reassign items to old array:
//* Source: https://stackoverflow.com/a/21071454
function arrayMoveValue(array, fromIndex, toIndex) {
if (toIndex === fromIndex) {
return array;
}
const target = array[fromIndex];
const increment = (toIndex < fromIndex ? -1 : 1);
for (
let k = fromIndex;
k !== toIndex;
k += increment
) {
array[k] = array[k + increment];
}
array[toIndex] = target;
return array;
}
function arrayAssignValues(toArray, fromArray) {
Array.from(fromArray).forEach(
(value, index) => {
toArray[index] = value;
}
);
}
function isNonEmptyArray(value) {
return (
isArray(value)
&& value.length > 0
);
}
function isRealNumber(value) {
return (
isNumber(value)
&& !isNaN(value)
);
}
function isNullOrUndefined(value) {
return (
value === null
|| typeof value === 'undefined'
);
}
function isNonNullObject(value) {
return (
value !== null
&& typeof value === 'object'
);
}
function isNonNullImageData(imageData) {
return (
isNonNullObject(imageData)
&& imageData.data
&& imageData.data.length > 0
&& imageData.width > 0
&& imageData.height > 0
);
}
function isURLFromDisk(url) {
const match = String(url).match(/^(\w+):(\/\/|$)/);
const protocol = (
(
match
? match[1]
: location.protocol
)
.split(':', 1)[0]
.toLowerCase()
);
return (protocol === 'file');
}
//* Check format support by creating minimal image:
//* Source: https://stackoverflow.com/a/55896125
function isImageTypeExportSupported(type) {
return (
getCanvasForTest()
.toDataURL(type)
.includes(DATA_PREFIX + type)
);
}
function getCanvasForTest() {
if (!canvasForTest) {
const canvas = canvasForTest = cre('canvas');
canvas.width = 1;
canvas.height = 1;
}
return canvasForTest;
}
function getCtxForTest() {
if (!ctxForTest) {
ctxForTest = getCanvasForTest().getContext('2d');
}
return ctxForTest;
}
function getTrimReg(chars) {
return new RegExp('^[' + chars + ']+|[' + chars + ']+$', 'gi');
}
function getNonNullOrEmptyObject(obj) {
return isNonNullObject(obj) ? obj : {};
}
function getCombinedDict() {
let result = null;
for (const arg of arguments) if (isNonNullObject(arg)) {
result = (result ? { ...result, ...arg } : arg);
}
return result;
}
function getCriteria(...args) {
return (
USE_CRITERIA_ARRAY
? (
args.length === 1
? args[0]
: getFlatArray(args)
) : (
isRegExp(args[0])
? args[0]
: new RegExp('(^|\\s)(' + getFlatArray(args).join('|') + ')($|\\s)', 'i')
)
);
}
/*
//* Source: https://cwestblog.com/2011/05/02/cartesian-product-of-multiple-arrays/
//* Description:
//* Get array of all possible combinations of values from multiple arrays.
//* Usage example:
//* var combos = getCrossProductArray( array, array, ... );
//* var combo = combos[123];
//* var count = combos.length;
function getCrossProductArray() {
return Array.prototype.reduce.call(
arguments
, (a, b) => {
const result = [];
for (const a_i of a)
for (const b_i of b) {
result.push(a_i.concat([ b_i ]));
}
return result;
}
, [[]]
);
}
//* Shorter version, Source: https://stackoverflow.com/a/43053803
const getCrossProductSub = (a, b) => [].concat(...a.map((d) => b.map((e) => [].concat(d, e))));
const getCrossProductArr = (a, b, ...c) => (b ? getCrossProductArr(getCrossProductSub(a, b), ...c) : a);
//* Source: http://phrogz.net/lazy-cartesian-product
//* Description:
//* Construct lazy iterator for all possible combinations without saving them all beforehand.
//* Usage example:
//* var combos = new CrossProductIterator( array, array, ... );
//* var combo = combos.item(123);
//* var count = combos.length;
function CrossProductIterator() {
const dimensions = [];
let totalCount = 1;
for (
let subCount = 0
, argIndex = arguments.length;
argIndex--;
totalCount *= subCount
) {
dimensions[argIndex] = [
totalCount
, subCount = arguments[argIndex].length
];
}
this.length = totalCount;
this.item = (comboIndex) => {
const combo = [];
for (
let argIndex = arguments.length;
argIndex--;
) {
combo[argIndex] = arguments[argIndex][(comboIndex / dimensions[argIndex][0] << 0) % dimensions[argIndex][1]];
}
return combo;
};
}
//* Source: http://phrogz.net/lazy-cartesian-product
//* Description:
//* Iterate through all possible combinations without saving them all, until stopped.
//* Combination array becomes arguments for callback.
//* Stops on the first truthy result from callback.
//* Returns true if it was stopped.
//* Usage example:
//* forEachSetInCrossProductUntilStopped( [array, array, ...], (combo) => combo.includes('stop') );
function forEachSetInCrossProductUntilStopped(arrays, callback, thisContext) {
function goDeeper(arrayIndex) {
const variants = arrays[arrayIndex];
const count = counts[arrayIndex];
if (arrayIndex === lastArrayIndex) {
for (let i = 0; i < count; ++i) {
combo[arrayIndex] = variants[i];
if (callback.apply(thisContext, combo)) {
return true;
}
}
} else {
for (let i = 0; i < count; ++i) {
combo[arrayIndex] = variants[i];
if (goDeeper(arrayIndex + 1)) {
return true;
}
}
}
combo.pop();
return false;
}
if (!thisContext) {
thisContext = this;
}
const lastArrayIndex = arrays.length - 1;
const combo = [];
const counts = [];
for (let i = arrays.length; i--; ) {
counts[i] = arrays[i].length;
}
return goDeeper(0);
}
*/
//* Source: http://phrogz.net/lazy-cartesian-product
//* Description:
//* Iterate through all possible combinations without saving them all.
//* Combination array becomes arguments for callback.
//* Usage example:
//* forEachSetInCrossProduct( [array, array, ...], console.log );
function forEachSetInCrossProduct(arrays, callback, thisContext) {
function goDeeper(arrayIndex) {
const variants = arrays[arrayIndex];
const count = counts[arrayIndex];
if (arrayIndex === lastArrayIndex) {
for (let i = 0; i < count; ++i) {
combo[arrayIndex] = variants[i];
callback.apply(thisContext, combo);
}
} else {
for (let i = 0; i < count; ++i) {
combo[arrayIndex] = variants[i];
goDeeper(arrayIndex + 1);
}
}
combo.pop();
}
if (!thisContext) {
thisContext = this;
}
const lastArrayIndex = arrays.length - 1;
const combo = [];
const counts = [];
for (let i = arrays.length; i--; ) {
counts[i] = arrays[i].length;
}
goDeeper(0);
}
function getJoinedCrossProductTextSet(arrays, joinText) {
const combos = [];
forEachSetInCrossProduct(
arrays
, (...combo) => addToListIfNotYet(combos, combo.join(joinText))
);
return combos;
}
function arrayFillRepeat(dest, src) {
const srcLength = src.length;
let destIndex = dest.length;
if (
destIndex > 0
&& srcLength > 0
) {
if (destIndex === srcLength) {
dest.set(src);
} else {
//* Simple generic solution that works well:
//* Source: https://stackoverflow.com/a/30229089
while (destIndex--) {
dest[destIndex] = src[destIndex % srcLength];
}
}
}
return dest;
}
function arrayFilterNonEmptyValues(value) {
return (
isString(value)
|| (
isNonNullObject(value)
&& typeof value.length !== 'undefined'
)
? (value.length > 0)
: !!value
);
}
function arrayFilterUniqueValues(value, index, array) { return (array.indexOf(value) === index); }
function orz(value) { return parseInt(value||0)||0; }
function orzFloat(value) { return parseFloat(value||.0)||.0; }
function orzTrim(value) { return orz(String(value).replace(regTrimNaN, '')); }
function orzClamp(value, min, max, parserFunc) { return Math.max(min, Math.min(max, (parserFunc || orz)(value))); }
function getDistance(x,y) { return Math.sqrt(x*x + y*y); }
function getNumberOfChannels(n) { return orzClamp(n || 4, 3,4); }
function getAlphaDataIndex(x,y, width) { return (((y*width + x) << 2) | 3); } // { return (((y*width + x) * 4) + 3); }
function repeatText(text, numberOfTimes) { return (new Array(numberOfTimes + 1)).join(text); }
function replaceAll(text, replaceWhat, replaceWith) {
if (isArray(replaceWhat)) {
for (const arg of arguments) if (isArray(arg)) {
text = replaceAll(text, ...arg);
}
return text;
}
return String(text).replaceAll(replaceWhat, replaceWith);
}
if (!String.prototype.replaceAll) {
String.prototype.replaceAll = (replaceWhat, replaceWith) => this.split(replaceWhat).join(replaceWith);
}
function isNotEmptyString(value) {
return (
isString(value)
&& value.length > 0
);
}
function isSlicableNotString(value) {
return (
!isString(value)
&& isNonNullObject(value)
&& isFunction(value.slice)
);
}
function isSameDataURL(a, b) {
return (
isString(a)
&& isString(b)
&& a.length === b.length
&& a === b
);
}
//* Test for equality in ArrayBuffer or TypedArray:
//* Source: https://stackoverflow.com/a/52181275
async function isIdenticalBlob(a, b) {
function isAlignedToBytes(a, bytesInElenment) {
return (
(a.byteOffset % bytesInElenment === 0)
&& (a.byteLength % bytesInElenment === 0)
);
}
function isIdentical(a, b, bytesInElenment, typeConstructor) {
if (bytesInElenment && typeConstructor) {
return isIdentical(
new typeConstructor(a.buffer, a.byteOffset, a.byteLength / bytesInElenment)
, new typeConstructor(b.buffer, b.byteOffset, b.byteLength / bytesInElenment)
);
}
var index = a.length;
while (index--) if (a[index] !== b[index]) {
return false;
}
return true;
}
if (
isBlob(a)
&& isBlob(b)
&& a.size === b.size
&& a.type === b.type
) {
if (
a.size > 0
&& (a = new Uint8Array(a.buffer || (a.buffer = await getFilePromise(a)))) //* <- keep the buffers for faster comparisons
&& (b = new Uint8Array(b.buffer || (b.buffer = await getFilePromise(b))))
&& a.byteLength === b.byteLength
) {
if (isAlignedToBytes(a, 4) && isAlignedToBytes(b, 4)) return isIdentical(a, b, 4, Uint32Array);
if (isAlignedToBytes(a, 2) && isAlignedToBytes(b, 2)) return isIdentical(a, b, 2, Uint16Array);
return isIdentical(a, b);
}
return true;
}
return false;
}
async function isIdenticalData(a, b) {
return (
isSameDataURL(a, b)
|| await isIdenticalBlob(a, b)
);
}
function hasAnyPart(value, parts) { return parts.some((part) => value.includes(part)); }
function hasAnyPrefix(value, parts) { return parts.some((part) => hasPrefix(value, part)); }
function hasAnyPostfix(value, parts) { return parts.some((part) => hasPostfix(value, part)); }
function hasPrefix(value, prefix) {
return (
prefix
&& prefix.length > 0
&& value
&& value.length >= prefix.length
&& value.slice
&& value.slice(0, prefix.length) === prefix
);
}
function hasPostfix(value, postfix) {
return (
postfix
&& postfix.length > 0
&& value
&& value.length >= postfix.length
&& value.slice
&& value.slice(-postfix.length) === postfix
);
}
function hasFraming(value, prefix, postfix) {
return (
hasPrefix(value, prefix)
&& hasPostfix(value, postfix || prefix)
);
}
function addToListIfNotYet(values, value) {
if (values.includes(value)) {
return 0;
}
values.push(value);
return 1;
}
function addRangeToList(values, newValuesText) {
for (const rangeText of (
String(newValuesText)
.split(regCommaSpace)
)) {
const range = (
String(rangeText)
.split(regMultiDot)
.map((textPart) => textPart.replace(regTrimNaNOrSign, ''))
.filter(arrayFilterNonEmptyValues)
.map(orzFloat)
);
if (range.length > 0) {
const min = Math.min(...range);
const max = Math.max(...range);
const isCountDown = range.indexOf(min) > range.indexOf(max);
if (
!values
|| !isFunction(values.push)
) {
values = [];
}
if (isCountDown) {
for (let value = max; value >= min; --value) {
addToListIfNotYet(values, value);
}
} else {
for (let value = min; value <= max; ++value) {
addToListIfNotYet(values, value);
}
}
//* Don't forget overstepped floats:
addToListIfNotYet(values, min);
addToListIfNotYet(values, max);
}
}
return values;
}
function getRangeValuesFromText(rangeText) {
return addRangeToList([], rangeText);
}
function getThisOrAnyNonEmptyItem(value, index, values) {
if (value) {
return value;
}
let foundValue;
if (
(
(isNumber(index) || isString(index))
&& (foundValue = value[index])
) || (
isFunction(values.find)
&& (foundValue = values.find((value) => !!value))
) || (
isFunction(index.find)
&& (foundValue = index.find((value) => !!value))
)
) {
return foundValue;
}
}
function getJoinedOrEmptyText(text, joinText) {
return (
isNullOrUndefined(text)
? '' :
(
text
&& isFunction(text.join)
? text.join(
isNullOrUndefined(joinText)
? '\n'
: joinText
)
: String(text)
)
);
}
function getNestedJoinedText(value, ...joinTexts) {
return getNestedArrayJoinedText(value, null, ...joinTexts);
}
function getNestedFilteredArrayJoinedText(value, ...joinTexts) {
return getNestedArrayJoinedText(value, FLAG_JOIN_TEXT_FILTER, ...joinTexts);
}
function getNestedFilteredArrayEnclosedJoinedText(value, prefix, suffix, ...joinTexts) {
return getNestedArrayJoinedText(value, { prefix, suffix, 'filter' : true }, ...joinTexts);
}
function getNestedArrayJoinedText(value, flags, ...joinTexts) {
if (!isNonNullObject(flags)) {
flags = {};
}
if (!isArray(joinTexts) || !joinTexts.length) {
joinTexts = [''];
}
const wrapText = {
'prefix' : '',
'suffix' : '',
};
for (const key in wrapText) if (key in flags) {
wrapText[key] = String(flags[key]);
delete flags[key];
}
if (isArray(value)) {
const joinText = (
joinTexts.length > 1
? joinTexts.shift()
: joinTexts[0]
) || '';
if (flags.filter) {
value = value.filter(arrayFilterNonEmptyValues);
}
value = value.map(
(value) => getNestedArrayJoinedText(value, flags, ...joinTexts)
).join(joinText);
}
return (
wrapText.prefix
+ String(value)
+ wrapText.suffix
);
}
function getLocalizedKeyOrNull(key) {
key = String(key);
if (key in LOCALIZATION_TEXT) {
return key;
}
const lowKey = key.toLowerCase();
if (lowKey in LOCALIZATION_TEXT) {
return lowKey;
}
return null;
}
function getLocalizedKeyByCount(key, ...args) {
key = String(key);
if (
(args.length > 0)
&& isFunction(getLocalizedCaseByCount)
) {
const finalCount = (
LOCALIZED_CASE_BY_CROSS_COUNT
? args.reduce(
(result, arg) => (
isRealNumber(arg)
? arg * (isRealNumber(result) ? result : 1)
: result
)
, undefined
)
: args.reduce(
(result, arg) => (
isRealNumber(arg)
? arg
: result
)
, undefined
)
);
const keyCase = getLocalizedCaseByCount(finalCount);
const keyByCase = getLocalizedKeyOrNull(key + '_' + keyCase);
if (keyByCase !== null) {
return keyByCase;
}
}
return getLocalizedKeyOrNull(key);
}
function getLocalizedOrDefaultText(key, defaultText, ...replacements) {
function replaceKeyword(placeholder, replacement) {
text = replaceAll(text, placeholder, getNestedJoinedText(replacement, '\n', ''));
}
const foundKey = getLocalizedKeyByCount(key, ...replacements);
let text = getJoinedOrEmptyText(
foundKey !== null
? LOCALIZATION_TEXT[foundKey]
: (
defaultText === true
? getNestedJoinedText(key, '\n', '')
: defaultText
)
);
if (
text
&& isArray(replacements)
) {
replacements.forEach(
(value, index) => replaceKeyword('{' + index + '}', value)
);
}
if (
text
&& (replacements = text.match(regTemplateVarName))
) {
for (const placeholder of replacements) {
const keyword = placeholder.replace(regTrimBrackets, '');
if (keyword.length > 0) {
if (keyword in LOCALIZATION_TEXT) {
replaceKeyword(placeholder, LOCALIZATION_TEXT[keyword]);
} else
if (keyword in window) {
replaceKeyword(placeholder, window[keyword]);
}
}
}
}
return text;
}
function getLocalizedOrEmptyText(key, ...replacements) {
return getLocalizedOrDefaultText(key, '', ...replacements);
}
function getLocalizedText(key, ...replacements) {
return getLocalizedOrDefaultText(key, true, ...replacements);
}
function getLocalizedHTML() {
return replaceAll(getLocalizedText(...arguments), '\n', '
');
}
function getLocalizedSectionName(sectionName) {
return getCapitalizedString(
getLocalizedOrEmptyText('option_header_' + sectionName)
|| getLocalizedOrEmptyText('option_' + sectionName)
|| sectionName
);
}
function trim(text) {
return (
getJoinedOrEmptyText(text)
.replace(regTrimSpace, '')
.replace(regTrimSpaceBeforeNewLine, '\n')
);
}
function pause(msec) {
return new Promise((resolve) => setTimeout(resolve, msec));
}
function eventStop(evt, flags) {
if (
(
isNonNullObject(evt)
&& typeof evt.eventPhase !== 'undefined'
&& evt.eventPhase !== null
)
? evt
: (evt = window.event)
) {
if (!isNonNullObject(flags)) {
flags = {};
}
if (evt.cancelBubble !== null) evt.cancelBubble = true;
if (evt.stopPropagation) evt.stopPropagation();
for (const key in flags)
if (
flags[key]
&& isFunction(evt[key])
) {
evt[key]();
}
}
return evt;
}
function catchPromiseError(error) {
console.error('Promise failed:', error);
return null;
}
function getErrorFromEvent(evt, message, callback) {
const error = new Error(message || 'Unknown error.');
error.event = evt;
if (isFunction(callback)) {
callback(error);
}
return error;
}
function getNormalizedColorText(color) {
return (
String(color)
.replace(regNonAlphaNum, '')
.toLowerCase()
);
}
function getRGBACutOrPadded(rgbaSource, rgbaDefault) {
return (
rgbaSource.length >= 4
? rgbaSource.slice(0,4)
: rgbaSource.concat((rgbaDefault || DEFAULT_COLOR_VALUE_ARRAY).slice(rgbaSource.length))
);
}
function getRGBAFromHex(text) {
//* Extend shortcut notation:
text = String(text).replace(regNonHex, '');
const charsNum = text.length;
if (charsNum === 1) text = repeatText(text, 6); else //* #1 -> #111111
if (charsNum === 2) text = repeatText(text, 3); else //* #12 -> #121212
if (charsNum === 3 || charsNum === 4) {
text = (
text
.split('')
.map((char) => repeatText(char, 2)) //* #123(4) -> #112233(44)
.join('')
);
}
//* Parse string into array of up to 4 numbers, taking up to 2 chars from left at each step:
const rgba = DEFAULT_COLOR_VALUE_ARRAY.slice();
for (
let index = 0;
index < rgba.length && text.length > 0;
index++
) {
const charsNum = Math.min(2, text.length);
rgba[index] = parseInt(text.substr(0, charsNum), 16);
text = text.substr(charsNum);
}
return rgba;
}
function getRGBAFromColorCodeMatch(match) {
const rgba = DEFAULT_COLOR_VALUE_ARRAY.slice();
//* Split RGB(A):
if (match[1]) {
arrayAssignValues(rgba, getNumbersArray(match[4], 4));
} else
//* Split hex:
if (match[2]) {
arrayAssignValues(rgba,
getNumbersArray(match[4], 4, regNonHex,
(channelValue) => parseInt(channelValue.substr(0, 2), 16)
)
);
} else
//* Solid hex:
if (match[3]) {
arrayAssignValues(rgba, getRGBAFromHex(match[4]));
}
return getNormalizedRGBA(rgba);
}
function getRGBAFromColorCodeText(color) {
const match = String(color).match(regColorCode);
if (match) {
return getRGBAFromColorCodeMatch(match);
}
}
function getRGBAFromColorCodeOrName(color, maxCount) {
let rgba = getRGBAFromColorCodeText(color);
if (rgba) {
return rgba;
}
const normalizedColorText = getNormalizedColorText(color);
//* Reuse previous results, as they won't change without changing the browser:
if (normalizedColorText in rgbaCacheByColorName) {
rgba = rgbaCacheByColorName[normalizedColorText];
if (TESTING && rgba) {
rgba.reuseCount ? ++rgba.reuseCount : (rgba.reuseCount = 1);
}
return rgba;
}
//* Ask browser built-in API what a color word means:
const ctx = getCtxForTest();
ctx.fillStyle = 'transparent';
ctx.fillStyle = normalizedColorText;
let rgbaFromCanvas;
if (
normalizedColorText === 'transparent'
|| (
(rgbaFromCanvas = getRGBAFromColorCodeText(ctx.fillStyle))
&& !isColorTransparent(rgbaFromCanvas)
)
) {
ctx.fillRect(0,0, 1,1);
rgba = getNormalizedRGBAFromImageData(ctx.getImageData(0,0, 1,1), maxCount);
} else {
if (TESTING) console.log(
'getRGBAFromColorCodeOrName: unknown color "'
+ color
+ '" ('
+ normalizedColorText
+ '), canvas result = "'
+ ctx.fillStyle
+ '"'
);
}
return rgbaCacheByColorName[normalizedColorText] = rgba;
}
function getRGBAFromColorCodeOrArray(color, maxCount) {
return (
isSlicableNotString(color)
? getNumbersArray(color, getNumberOfChannels(maxCount))
: getRGBAFromColorCodeOrName(color, maxCount)
);
}
function isColorDark(color) {
const [ r,g,b ] = getRGBAFromColorCodeOrArray(color, 3);
//* Sources:
//* https://awik.io/determine-color-bright-dark-using-javascript/
//* http://alienryderflex.com/hsp.html
return Math.sqrt(
0.299 * r * r
+ 0.587 * g * g
+ 0.114 * b * b
) < 150;
}
function isColorTransparent(color) {
if (getNormalizedColorText(color) === 'transparent') {
return true;
}
const rgba = getRGBAFromColorCodeOrArray(color);
return (
rgba
&& isArray(rgba)
&& !(
rgba.length === 3
|| rgba.some((channelValue) => (channelValue > 0))
)
);
}
function getNormalizedRGBAFromImageData(imageData, maxCount) {
return getNormalizedRGBA(
Array.from(
imageData.data.subarray(0, getNumberOfChannels(maxCount))
)
);
}
function getNormalizedRGBA(rgba) {
if (rgba.length > 3) {
if (rgba[3] === MAX_CHANNEL_VALUE) {
return rgba.slice(0,3);
}
if (rgba[3] === MIN_CHANNEL_VALUE) {
return TRANSPARENT_COLOR_VALUE_ARRAY.slice();
}
}
return rgba;
}
function getColorTextFromArray(rgba, maxCount) {
if (isSlicableNotString(rgba)) {
rgba = (
rgba
.slice(0, getNumberOfChannels(maxCount))
.map(
(channelValue, index) => (
index === 3
? getNormalizedOpacity(channelValue).toFixed(3)
: orz(channelValue)
)
)
);
while (rgba.length < 3) {
rgba.push(0);
}
return (
(rgba.length < 4 ? 'rgb' : 'rgba')
+ '('
+ rgba.join(', ')
+ ')'
);
}
return String(rgba);
}
//* Clone given object with optional recursive modifications:
//* Source: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/assign#Deep_Clone
function getPatchedObject(obj, jsonReplacerFunc) {
return JSON.parse(JSON.stringify(obj, jsonReplacerFunc || null));
}
//* Get text representation of given object with reproducible order of keys:
//* Source: https://stackoverflow.com/a/53593328
function orderedJSONstringify(obj, space) {
const allKeys = [];
JSON.stringify(
obj
, (key, value) => {
allKeys.push(key);
return value;
}
);
allKeys.sort();
return JSON.stringify(obj, allKeys, space);
}
function getOrInitChild(obj, key, ...args) {
function getInitChild(args) {
const constructor = args.find(isFunction) || Object;
const keys = args.filter(isString);
if (keys.length > 0) {
const child = {};
while (keys.length > 0) {
const key = keys.shift();
child[key] = new constructor();
}
return child;
} else {
return new constructor();
}
}
if (!isNonNullObject(obj)) {
return;
}
return (
key in obj
? obj[key]
: (obj[key] = getInitChild(args))
);
}
function getPropFromAnySource(key, ...sources) {
for (const obj of sources)
if (
isNonNullObject(obj)
&& key in obj
) {
return obj[key];
}
return null;
}
function getPropByNameChain(obj, ...keys) {
while (keys.length > 0) {
const key = keys.shift();
if (
typeof key === 'undefined'
|| !isNonNullObject(obj)
) {
return null;
}
obj = obj[key];
}
return obj;
}
function getPropByAnyOfNamesChain(obj, ...keys) {
deeper:
while (isNonNullObject(obj)) {
for (const key of keys) if (key in obj) {
obj = obj[key];
continue deeper;
}
break;
}
return obj;
}
function getPropBySameNameChain(obj, key) {
while (
isNonNullObject(obj)
&& key in obj
) {
obj = obj[key];
}
return obj;
}
function cleanupObjectTree(obj, childKeys, keysToRemove) {
if (isNonNullObject(obj)) {
for (const key of keysToRemove) if (key in obj) {
if (TESTING > 9) console.log(
'cleanupObjectTree:', [
obj.fileName || obj.nameOriginal || obj.name,
'obj:', obj,
'key:', key,
'value:', obj[key],
]
);
obj[key] = null;
delete obj[key];
}
let child;
for (const key of childKeys) if (child = obj[key]) {
if (isArray(child)) {
for (const item of child) {
cleanupObjectTree(item, childKeys, keysToRemove);
}
} else {
cleanupObjectTree(child, childKeys, keysToRemove);
}
}
}
return obj;
}
//* Get memory block of minimal allowed size for asm.js:
//* Source: https://gist.github.com/wellcaffeinated/5399067#gistcomment-1364265
function nextValidHeapSize(realSize) {
const SIZE_64_KB = 65536; // 0x10000
const SIZE_64_MB = 67108864; // 0x4000000
if (realSize <= SIZE_64_KB) {
return SIZE_64_KB;
} else if (realSize <= SIZE_64_MB) {
return 1 << (Math.ceil(Math.log(realSize)/Math.LN2)|0);
} else {
return (SIZE_64_MB*Math.ceil(realSize/SIZE_64_MB)|0)|0;
}
}
//* Get array of elements by some criteria (tagname, class, etc), should work even in older browsers:
function getElementsArray(by, text, parent) {
if (!parent) {
parent = document;
}
try {
const results = (
isFunction(parent[by])
? parent[by](text)
: parent.querySelectorAll(QUERY_SELECTOR[by].join(text))
) || DUMMY_EMPTY_ARRAY;
return Array.prototype.slice.call(results);
} catch (error) {
logError(error, arguments);
}
return DUMMY_EMPTY_ARRAY;
}
function getAllByClass (text, parent) { return getElementsArray('getElementsByClassName', text, parent); }
function getAllByTag (text, parent) { return getElementsArray('getElementsByTagName', text, parent); }
function getAllByType (text, parent) { return getElementsArray('getElementsByType', text, parent); }
function getAllByName (text, parent) { return getElementsArray('getElementsByName', text, parent); }
function getAllById (text, parent) { return getElementsArray('getElementsById', text, parent); }
function getOneById (text) { return document.getElementById(text); }
function cre(tagName, parent, before) {
const element = document.createElement(tagName);
if (parent) {
if (before) {
parent.insertBefore(element, before);
} else {
parent.appendChild(element);
}
}
return element;
}
function del(element) {
if (!element) {
return null;
}
if (element.target) {
element = element.target;
}
if (isFunction(element.map)) {
return element.map(del);
}
const parent = element.parentNode;
if (parent) {
parent.removeChild(element);
return parent;
}
}
function delAllChildNodes(parent) {
while (del(parent.lastChild));
return parent;
}
function encodeHTMLSpecialChars(text) {
return (
String(text)
.replace(/&/g, '&')
.replace(/"/g, '"')
.replace(/'/g, ''')
.replace(//g, '>')
);
}
function encodeTagAttr(text) {
return (
String(text)
.replace(/"/g, '"')
);
}
function getCapitalizedString(text) {
return (
text.slice(0,1).toUpperCase()
+ text.slice(1).toLowerCase()
);
}
//* propNameForIE:
function dashedToCamelCase(text) {
return (
String(text)
.split('-')
.map(
(word, index) => (
index === 0
? word.toLowerCase()
: getCapitalizedString(word)
)
)
.join('')
);
}
function getStyleValue(element, prop) {
let helperObject;
if (helperObject = element.currentStyle) {
return helperObject[dashedToCamelCase(prop)];
}
if (helperObject = window.getComputedStyle) {
return helperObject(element).getPropertyValue(prop);
}
return null;
}
function isStyleIncluded(element, prop) {
let helperObject;
if (helperObject = element.currentStyle) {
return (dashedToCamelCase(prop) in helperObject);
}
if (helperObject = window.getComputedStyle) {
return (prop in helperObject(element));
}
return false;
}
function getChildByAttr(element, attrName, attrValue) {
if (element && element.children) {
for (const child of element.children) {
if (child.getAttribute(attrName) === attrValue) {
return child;
}
}
}
}
function getDraggableElementOrParent(element) {
while (element) {
if (element.draggable) {
return element;
}
element = element.parentNode;
}
}
function isElementOrAnyParentDraggable(element) {
while (element) {
if (element.draggable) {
return true;
}
element = element.parentNode;
}
return false;
}
function isElementAfterSibling(element, sibling) {
while (element && (element = element.previousSibling)) {
if (element === sibling) {
return true;
}
}
return false;
}
function hasAnyClassNames(element, ...args) {
if (element && args && args.length) {
for (const arg of args) {
if (isString(arg)) {
if (element.classList && element.classList.contains(arg)) {
return true;
}
} else
if (isRegExp(arg)) {
if (element.className && arg.test(element.className)) {
return true;
}
} else
if (isArray(arg)) {
if (hasAnyClassNames(element, ...arg)) {
return true;
}
}
}
}
return false;
}
function isElementBeforeSibling(element, sibling) {
while (element && (element = element.nextSibling)) {
if (element === sibling) {
return true;
}
}
return false;
}
function getPreviousSiblingByClass(element, className) {
while (element && (element = element.previousElementSibling)) {
if (hasAnyClassNames(element, className)) {
break;
}
}
return element;
}
function getNextSiblingByClass(element, className) {
while (element && (element = element.nextElementSibling)) {
if (hasAnyClassNames(element, className)) {
break;
}
}
return element;
}
function getThisOrParentByClass(element, className) {
while (element) {
if (hasAnyClassNames(element, className)) {
break;
}
element = element.parentNode;
}
return element;
}
function getThisOrParentByTagName(element, ...tagNames) {
while (element) {
if (
element.tagName
&& tagNames.includes(element.tagName.toLowerCase())
) {
break;
}
element = element.parentNode;
}
return element;
}
function getTargetParentByClass(element, className) {
if (element && element.target) {
element = element.target;
}
element = getThisOrParentByClass(element, className);
if (element && element.tagName) {
return element;
}
}
function getTagAttrIfNotEmpty(name, values, delim) {
if (name) {
values = asArray(values).filter(arrayFilterNonEmptyValues);
if (values.length > 0) {
return (
' '
+ name
+ '="'
+ encodeTagAttr(values.join(delim || ' '))
+ '"'
);
}
}
return '';
}
function getTableRowHTML(cells, tagName) {
tagName = String(tagName || '') || 'td';
const openTag = '<' + tagName + '>';
const closeTag = '' + tagName + '>';
return (
openTag
+ getNestedJoinedText(cells, closeTag + openTag, '')
+ closeTag
);
}
function getTableHTML(...rows) {
return (
'
'
+ rows.map(
(row) => (
isNonNullObject(row) && (row.cells || row.cell_tag_name)
? getTableRowHTML(row.cells, row.cell_tag_name)
: getTableRowHTML(row)
)
).join('
')
+ '
'
);
}
function getDropdownMenuHTML(headContent, listContent, headId, tagName) {
if (isArray(arguments[0])) {
[headContent, listContent, headId, tagName] = arguments[0];
}
tagName = String(tagName || '') || 'div';
headContent = String(headContent);
const openTag = '<' + tagName + ' class="';
const closeTag = '' + tagName + '>';
return(
openTag + 'menu-head"'
+ getTagAttrIfNotEmpty('id', headId || '')
+ ' onmouseover="updateDropdownMenuPositions()">'
+ (
hasFraming(headContent, '<', '>')
? headContent
: (
''
)
)
+ openTag + 'menu-drop">'
+ openTag + 'menu-hid">'
+ openTag + 'menu-list">'
+ (listContent || '')
+ closeTag
+ closeTag
+ closeTag
+ closeTag
);
}
function closeAllDropdownMenuTabs(element) {
const tabBar = getThisOrParentByClass(element, matchClassMenuBar);
const tabs = getAllByClass('menu-head', tabBar);
for (const tab of tabs) {
const header = getAllByTag('header', tab)[0];
if (
header
&& header !== element
) {
header.classList.remove('show');
}
}
START_WITH_OPEN_FIRST_MENU_TAB = false;
}
function toggleDropdownMenu(element) {
closeAllDropdownMenuTabs(element);
element.classList.toggle('show');
}
function toggleSection(element, action) {
function toggleOneSection(section) {
if (section) {
if (isActionOpen) {
if (
!isActionAll
|| !section.open
) {
section.classList.add('opening');
setTimeout(() => section.classList.remove('opening'), 300);
}
section.open = true;
} else
if (isActionClose) {
section.open = false;
}
}
}
const actionWords = String(action || element.name).split('_');
const isActionAll = actionWords.includes('all');
const isActionOpen = actionWords.includes('open');
const isActionClose = actionWords.includes('close');
if (isActionAll) {
getAllByTag('details', getOneById('top-menu-help')).forEach(toggleOneSection);
} else {
toggleOneSection(getThisOrParentByTagName(element, 'details', 'section'));
}
updateDropdownMenuPositions();
}
function showHelpSection(sectionName, source) {
const header = getOneById('top-menu-' + sectionName);
if (header) {
toggleSection(header, 'open');
if (source) {
const sourceElement = (isString(source) ? getOneById('top-menu-' + source) : source);
const toSection = getThisOrParentByTagName(header, 'details', 'section');
let fromSection = getThisOrParentByTagName(sourceElement, 'details', 'section');
let alignWithTop = true;
while (fromSection && toSection) {
fromSection = fromSection.nextElementSibling;
if (fromSection === toSection) {
alignWithTop = false;
break;
}
}
if (toSection) {
toSection.scrollIntoView(alignWithTop);
}
} else {
header.scrollIntoView();
}
}
}
function toggleFixedTabWidth() {
const isFixedTabWidthEnabled = document.body.classList.toggle('fixed-tab-width');
if (LS) {
LS[LS_KEY_FIXED_TAB_WIDTH] = isFixedTabWidthEnabled;
}
}
function toggleTextSize() {
const isBigTextEnabled = document.body.classList.toggle('larger-text');
updateDropdownMenuPositions();
if (LS) {
LS[LS_KEY_BIG_TEXT] = isBigTextEnabled;
}
}
function makeElementFitOnClick(element, initialState) {
//* Not listeners, because need attributes for style:
element.className = initialState || IMAGE_FIT_CLASSES[0];
element.setAttribute(
'onclick'
, (
IMAGE_FIT_CLASSES
.map((className) => `this.classList.toggle('${ className }')`)
.join(', ')
)
);
return element;
}
function getOffsetXY(element) {
let x = 0;
let y = 0;
while (element) {
x += element.offsetLeft;
y += element.offsetTop;
element = element.offsetParent;
}
return { x, y };
}
function putInView(element, x,y, changeOnlyX, changeOnlyY) {
const viewport = window.visualViewport;
const positionType = getStyleValue(element, 'position');
const isPositionFixed = (positionType === 'fixed');
let parentOffsetX = 0;
let parentOffsetY = 0;
if (positionType === 'absolute') {
let offset = getOffsetXY(element.offsetParent);
parentOffsetX = offset.x;
parentOffsetY = offset.y;
}
if (isRealNumber(x)) {
x = orz(x) + parentOffsetX;
y = orz(y) + parentOffsetY;
} else {
let offset = getOffsetXY(element);
x = offset.x;
y = offset.y;
}
if (!changeOnlyY) {
let xMin = orz(isPositionFixed ? (document.body.scrollLeft || document.documentElement.scrollLeft) : 0);
let xMax = xMin + (viewport ? viewport.width : window.innerWidth) - element.offsetWidth;
if (x > xMax) x = xMax;
if (x < xMin) x = xMin;
element.style.left = (x - parentOffsetX) + 'px';
}
if (!changeOnlyX) {
let yMin = orz(isPositionFixed ? (document.body.scrollTop || document.documentElement.scrollTop) : 0);
let yMax = yMin + (viewport ? viewport.height : window.innerHeight) - element.offsetHeight;
if (y > yMax) y = yMax;
if (y < yMin) y = yMin;
element.style.top = (y - parentOffsetY) + 'px';
}
return element;
}
function getNumbersArray(data, maxCount, splitBy, transformFunction) {
const values = (
isSlicableNotString(data)
? Array.from(
data
.slice(0, orz(maxCount) || Infinity)
)
: (
String(data)
.split(splitBy || regNaN, orz(maxCount) || -1)
.filter(isNotEmptyString)
)
);
return (
values
.map(transformFunction || orz)
);
}
function getUniqueNumbersArray() {
return (
getNumbersArray(...arguments)
.filter(arrayFilterUniqueValues)
);
}
function getFileBaseName(name) {
const index = name.lastIndexOf('.');
return (
index > 0
? name.substr(0, index)
: name
);
}
function getFileNameWithSupportedExt(name, ext) {
return (
ext && SAVE_IMAGE_FILE_TYPES.includes(ext)
? getFileNameWithReplacedExt(name, ext)
: name
);
}
function getFileNameWithReplacedExt(name, ext) {
const parts = name.split(/\./g);
const lastIndex = parts.length - 1;
if (lastIndex > 0) {
parts[lastIndex] = ext;
} else {
parts.push(ext);
}
return parts.join('.');
}
function getFileNameWithSuffixBeforeExt(name, suffix) {
const parts = name.split(/\./g);
const lastIndex = parts.length - 1;
if (lastIndex > 0) {
parts[lastIndex - 1] += suffix;
return parts.join('.');
} else {
return name + suffix;
}
}
function getFileExt(name) { return name.split(/\./g).pop().toLowerCase(); }
function getFileName(path) { return path.split(/\//g).pop(); }
function getFilePathFromUrl(url) { return url.split(/\#/g).shift().split(/\?/g).shift(); }
function getFormattedFileNamePart(name) { return (name ? '[' + name + ']' : ''); }
function getFormattedFileSize(bytes, shortened) {
let bytesText;
if (bytes) {
const bytesNumber = orz(
isNumber(bytes)
? bytes
: String(bytes).replace(regNaN, '')
);
bytesText = getLocalizedText(
'file_bytes'
, bytesNumber.toLocaleString(LANG) //* <- formatted for display
, bytesNumber //* <- for text case selection, not displayed
);
}
return (
(shortened && bytesText)
? (shortened + ' (' + bytesText + ')')
: (shortened || bytesText || bytes)
);
}
function leftPadNum(numValue, padToLength, paddingText) {
let text = String(orz(numValue));
padToLength = orz(padToLength) || 2;
paddingText = String(paddingText || 0);
while (text.length < padToLength) {
text = paddingText + text;
}
return text;
}
function getFormattedTimezoneOffset(date) {
if (!isDate(date)) {
date = new Date();
}
let offset = date.getTimezoneOffset();
if (offset) {
let sign = '-';
if (offset < 0) {
offset = -offset;
sign = '+';
}
const offsetHours = leftPadNum(Math.floor(offset / SPLIT_SEC));
const offsetMinutes = leftPadNum(offset % SPLIT_SEC);
return sign + offsetHours + ':' + offsetMinutes;
} else {
return 'Z';
}
}
function getFormattedHMS(msec) {
msec = orz(msec);
const sign = (msec < 0 ? '-' : '');
const values = [0, 0, Math.floor(Math.abs(msec) / 1000)];
let index = values.length;
while (--index) {
if (values[index] >= SPLIT_SEC) {
values[index - 1] = Math.floor(values[index] / SPLIT_SEC);
values[index] %= SPLIT_SEC;
}
if (values[index] < 10) {
values[index] = '0' + values[index];
}
}
return sign + values.join(':');
}
function getFormattedTime() {
function getDatePartText(name) {
let num = date['get' + name]();
if (name === 'Month') ++num;
return leftPadNum(num);
}
//* Check arguments out of order:
const flags = {};
let arg, argDate, argNum, argText, date, YMD, HMS;
for (const index in arguments) if (arg = arguments[index]) {
if (isDate(arg)) argDate = arg; else
if (isNumber(arg)) argNum = arg; else
if (isString(arg)) argText = arg; else
if (isNonNullObject(arg)) {
for (const key in arg) {
flags[key] = !!arg[key];
}
}
if (TESTING > 9) console.log('getFormattedTime: arg[' + index + '] =', [ typeof arg, arg ]);
}
if (!date && argText && isFunction(Date.parse)) {
date = Date.parse(argText.replace(regHMS, '$1:$2:$3'));
}
if (!date && argText) date = orz(argText);
if (!date && argDate) date = argDate;
if (!date && argNum) date = argNum;
if (!date) {
date = new Date();
} else
if (!isDate(date)) {
if (date < 0) date += getTimeNow();
date = new Date(date);
}
//* Get date text parts:
const textParts = [];
if (
flags.onlyYMD
|| !flags.onlyHMS
) {
YMD = (
TIME_PARTS_YMD
.map(getDatePartText)
.join('-')
);
textParts.push(YMD);
}
if (
flags.onlyHMS
|| !flags.onlyYMD
) {
HMS = (
TIME_PARTS_HMS
.map(getDatePartText)
.join(flags.fileNameFormat ? '-' : ':')
);
if (flags.logFormat) {
const msec = (
isFunction(date.getMilliseconds)
? date.getMilliseconds()
: (date % 1000)
);
HMS += '.' + leftPadNum(msec, 0,3);
}
textParts.push(HMS);
}
//* Get date text:
if (
flags.logFormat
|| flags.fileNameFormat
|| flags.plainTextFormat
|| flags.onlyYMD
|| flags.onlyHMS
) {
return (
textParts
.join(flags.fileNameFormat ? '_' : ' ')
);
} else {
//* Get date HTML:
return (
''
);
}
}
function getTimeNow() { return +new Date(); }
function getLogTime() { return getFormattedTime(FLAG_TIME_LOG_FORMAT); }
function logTime() {
let logText = getLogTime();
let arg;
for (const index in arguments) if (typeof (arg = arguments[index]) !== 'undefined') {
const textValue = getJoinedOrEmptyText(arg, '\n');
if (textValue.includes('\n')) {
if (
hasFraming(textValue, '(', ')')
|| hasFraming(textValue, '{', '}')
|| hasFraming(textValue, '[', ']')
) {
logText += ':\n' + textValue;
} else {
logText += ':\n[\n' + textValue + '\n]';
}
} else
if (index > 0) {
logText += ' = "' + textValue + '"';
} else {
logText += ' - ' + textValue;
}
}
console.log(logText);
}
function logError(error, args, context) {
console.error(
'Error:'
, [
error
, ...(typeof args === 'undefined' ? DUMMY_EMPTY_ARRAY : [
// 'In function:', args.callee.name, //* <- not available in strict mode
'With arguments:', args,
])
, ...(typeof context === 'undefined' ? DUMMY_EMPTY_ARRAY : [
'Context:', context
])
]
);
}
function callbackOnImageLoad(img, resolve, reject) {
if (isImageElement(img)) {
if (img.complete) {
resolve(img);
} else {
img.onerror = reject;
img.onload = () => resolve(img);
}
} else {
reject(img);
}
}
async function resolvePromiseOnImageLoad(img, resolve, reject) {
img = await img;
if (isPromise(img)) {
img
.then((img) => callbackOnImageLoad(img, resolve, reject))
.catch(reject);
} else {
callbackOnImageLoad(img, resolve, reject);
}
}
function getFilePromise(file, context) {
//* Note: "file" may be a blob object.
//* Source: https://stackoverflow.com/a/15981017
if (
!file
|| typeof FileReader !== 'function'
) {
return null;
}
return new Promise(
(resolve, reject) => {
const reader = new FileReader();
if (context) {
reader.onloadstart =
reader.onloadend =
reader.onprogress = (evt) => updateProjectOperationProgress(
context
, 'project_status_loading_file'
, evt.loaded || evt.position || '?'
, getLocalizedOrDefaultText('file_bytes', evt.total || evt.totalSize || '?')
);
}
reader.onabort =
reader.onerror = (evt) => getErrorFromEvent(evt, 'FileReader failed.', reject);
reader.onload = (evt) => {
const result = evt.target.result;
if (result) {
resolve(result);
} else {
getErrorFromEvent(evt, 'FileReader completed, got empty or no result.', reject);
}
};
reader.readAsArrayBuffer(file);
}
).catch(catchPromiseError);
}
function getFilePromiseFromURL(url, responseType, context) {
//* Note: "url" may be a "blob:" or "data:" url.
//* Source: https://www.mwguy.com/decoding-a-png-image-in-javascript/
if (
!url
|| !isString(url)
|| typeof XMLHttpRequest !== 'function'
) {
return null;
}
return new Promise(
(resolve, reject) => {
const request = new XMLHttpRequest();
if (context) {
request.onloadstart =
request.onloadend =
request.onprogress = (evt) => updateProjectOperationProgress(
context
, 'project_status_loading_file'
, evt.loaded || evt.position || '?'
, getLocalizedOrDefaultText('file_bytes', evt.total || evt.totalSize || '?')
);
}
request.ontimeout =
request.onabort =
request.onerror = (evt) => getErrorFromEvent(evt, 'Request failed.', reject);
request.onload = (evt) => {
const response = evt.target.response;
if (response) {
if (isFunction(request.getAllResponseHeaders)) {
response.headers = request.getAllResponseHeaders();
}
if (isFunction(request.getResponseHeader)) {
const lastModText = request.getResponseHeader('Last-Modified');
if (lastModText) {
response.lastModified = +new Date(lastModText);
}
}
resolve(response);
} else {
getErrorFromEvent(evt, 'Request completed, got empty or no response.', reject);
}
};
request.responseType = responseType || 'arraybuffer';
request.open('GET', url, true);
request.send();
}
).catch(catchPromiseError);
}
async function getImagePromiseFromCanvasToBlob(canvas, trackList, mimeType, quality, img) {
async function getImagePromiseFromBlob(blob) {
if (!blob) {
throw 'Canvas to blob: got empty or no blob.';
}
if (!img) {
img = cre('img');
}
img.type = blob.type.split('/').pop();
img.fileSize = blob.size;
let url;
if (trackList) {
const entry = await getImageBlobAndURLFromDataOrList(blob, blob.type, trackList);
if (entry) {
if (entry.img) {
return entry.img;
}
entry.img = img;
url = entry.url;
}
}
if (!url) {
url = URL.createObjectURL(blob);
if (trackList) {
await addURLToTrackList({ blob, url, img }, trackList);
}
}
return new Promise(
(resolve, reject) => {
img.onload = (evt) => {
// URL.revokeObjectURL(url); //* <- let the outside code clean up after it's done
if (canvas.top) img.top = canvas.top;
if (canvas.left) img.left = canvas.left;
resolve(img);
};
img.onerror = (evt) => {
if (!trackList) {
URL.revokeObjectURL(url);
}
if (TESTING) console.error('Image loading failed:', [ url, img, evt ]);
getErrorFromEvent(evt, 'Canvas to blob: image loading failed.', reject);
};
img.src = url;
}
);
}
return (
new Promise(
(resolve, reject) => canvas.toBlob(resolve, mimeType || TYPE_IMAGE_PNG, quality || 1)
)
.then(getImagePromiseFromBlob)
.catch(catchPromiseError)
);
}
//* Note: cannot save image by revoked url, so better keep it and revoke later.
async function addURLToTrackList(data, holder) {
if (isNonNullObject(holder)) {
const trackList = getTrackListFromProject(holder);
let key = data;
if (isNonNullObject(data)) {
let { blob, url, img } = data;
if (img && !url) {
url = data.url = img.src;
}
if (url && !blob) {
blob = data.blob = await getFilePromiseFromURL(url, 'blob');
}
if (img && !img.type) {
img.type = blob.type.split('/').pop();
}
key = url;
}
if (trackList instanceof Set) {
trackList.add(data);
} else
if (trackList instanceof Array) {
addToListIfNotYet(trackList, data);
} else
if (trackList instanceof Map) {
if (!isNonNullObject(trackList.get(key))) {
trackList.set(key, data);
}
} else {
if (!isNonNullObject(trackList[key])) {
trackList[key] = data;
}
}
return trackList;
} else {
return null;
}
}
function revokeBlobsFromTrackList(data, key) {
let count = 0;
if (data) {
if (
isString(key)
|| isString(key = data.url || data)
) {
URL.revokeObjectURL(key);
++count;
} else
if (isNonNullObject(data = getTrackListFromProject(data))) {
if (
(data instanceof Array)
|| (data instanceof Map)
|| (data instanceof Set)
) {
data.forEach(
(data, key) => {
count += revokeBlobsFromTrackList(data, key);
}
);
} else {
for (const key in data) {
count += revokeBlobsFromTrackList(data[key], key);
}
}
}
}
return count;
}
function getTrackListFromProject(holder) {
if (isNonNullObject(holder)) {
if (holder.isProject) {
return (
holder.blobs
|| holder.blobsAndURLs
|| holder.blobsByURL
|| holder.blobURLs
|| (
(PROJECT_BLOBS_CONTAINER_TYPE === Array)
? getOrInitChild(holder, 'blobsAndURLs', Array)
:
(PROJECT_BLOBS_CONTAINER_TYPE === Set)
? getOrInitChild(holder, 'blobsAndURLs', Set)
:
(PROJECT_BLOBS_CONTAINER_TYPE === Map)
? getOrInitChild(holder, 'blobsByURL', Map)
: getOrInitChild(holder, 'blobsByURL')
)
);
}
return holder;
}
}
function getRGBAFromDataIfColorFill(imageData, project, layer) {
if (
PNG_OPTIMIZE_COLOR_FILL
&& (layer && !(layer.top || layer.left))
&& imageData && project
&& imageData.data
&& imageData.width === project.width
&& imageData.height === project.height
) {
const data = imageData.data;
const size = imageData.data.length;
if (size > 0) {
let index = size;
while (--index) if (data[index] !== data[index % 4]) {
if (TESTING > 2) console.log('not RGBA:', index, '/', size, layer.nameOriginal || layer.name);
break;
}
if (index < 4) {
const rgba = getNormalizedRGBAFromImageData(imageData);
if (TESTING > 2) console.log('getNormalizedRGBA:', index, '/', size, layer.nameOriginal || layer.name, rgba);
return rgba;
}
}
}
}
function getImageDataFromData(imageData) {
if (isImageData(imageData)) {
return imageData;
}
if (isNonNullImageData(imageData)) {
const { data, width, height } = imageData;
imageData = new ImageData(width, height);
arrayFillRepeat(imageData.data, data);
return imageData;
}
}
async function getQOIByteArrayFromImageData(imageData) {
if (
await loadLibOnDemandPromise('QOI.js')
&& isNonNullImageData(imageData)
) {
if (TESTING_QOI && LOG_TIMERS) console.time('QOIEncoder');
const { data, width, height } = imageData;
const pixels = new Int32Array(data.buffer); //* <- https://stackoverflow.com/a/16679447
const qoiFile = new QOIEncoder();
const isDone = qoiFile.encode(width, height, pixels, true);
if (TESTING_QOI && LOG_TIMERS) console.timeEnd('QOIEncoder');
if (isDone) {
if (TESTING_QOI) {
function testQOIEncoding(methodName) {
const times = new Array(count);
let counter = count;
let start = 0;
while (counter--) {
start = +new Date;
new QOIEncoder()[methodName](width, height, pixels, true);
times[counter] = +new Date - start;
}
const min = Math.min(...times);
const max = Math.max(...times);
const sum = times.reduce((a, b) => a + b, 0);
const avg = sum / count;
console.log([
count + ' runs from same ImageData(' + width + 'x' + height + ')'
, 'took total ' + sum + 'ms'
, 'min ' + min + 'ms'
, 'avg ' + avg.toFixed(2) + 'ms'
, 'max ' + max + 'ms'
, 'QOIEncoder.' + methodName
].join(', '));
}
const count = (
width > 2000 || height > 2000 ? 10
: width > 200 || height > 200 ? 100
: 1000
);
if (LOG_TIMERS) console.time('testQOIEncoding');
testQOIEncoding('encode');
testQOIEncoding('encodeFasterIndexButTwiceRGBA');
testQOIEncoding('encode');
testQOIEncoding('encodeFasterIndexButTwiceRGBA');
if (LOG_TIMERS) console.timeEnd('testQOIEncoding');
}
// 10 runs from same ImageData(5614x2406), took total 1088ms, min 106ms, avg 108.8ms, max 116ms, QOIEncoder.encode
// 10 runs from same ImageData(5614x2406), took total 1201ms, min 103ms, avg 120.1ms, max 178ms, QOIEncoder.encodeFasterIndexButTwiceRGBA
// 10 runs from same ImageData(5614x2406), took total 1096ms, min 106ms, avg 109.6ms, max 114ms, QOIEncoder.encode
// 10 runs from same ImageData(5614x2406), took total 1066ms, min 101ms, avg 106.6ms, max 118ms, QOIEncoder.encodeFasterIndexButTwiceRGBA
// Conclusion: not decisive, use simpler method.
return qoiFile.getEncoded().subarray(0, qoiFile.getEncodedSize());
} else {
console.error('Error: failed to encode QOI file content.');
}
}
}
async function getQOIBlobFromImageData(imageData) {
const data = await getQOIByteArrayFromImageData(imageData);
if (data) {
if (TESTING_QOI) {
if (LOG_TIMERS) console.time('QOIDecoder');
const qoiFile = new QOIDecoder();
const isDone = qoiFile.decode(data, data.length);
if (LOG_TIMERS) console.timeEnd('QOIDecoder');
if (isDone) {
const qoiImageData = getImageDataFromData({
'data' : new Uint8Array(qoiFile.getPixels().buffer)
, 'width' : qoiFile.getWidth()
, 'height' : qoiFile.getHeight()
});
const canvas = getCanvasFromImageData(imageData);
const oldPNG = canvas.toDataURL();
const ctx = canvas.getContext('2d');
ctx.putImageData(qoiImageData, 0,0);
const newPNG = canvas.toDataURL();
if (oldPNG !== newPNG) {
console.warn([
'Warning: mismatched result of QOI encode/decode'
, 'ImageData->PNG:'
, imageData.width + 'x' + imageData.height
, imageData.data.length + ' bytes in pixels'
, oldPNG.length + ' bytes in DataURL'
, oldPNG
, 'ImageData->QOI->ImageData->PNG:'
, qoiImageData.width + 'x' + qoiImageData.height
, qoiImageData.data.length + ' bytes in pixels'
, newPNG.length + ' bytes in DataURL'
, newPNG
]);
saveDL(oldPNG, 'srcImageDataToPNG', 'png', 1);
saveDL(newPNG, 'srcImageDataToQOIToImageDataToPNG', 'png', 1);
} else {
console.log('Image before QOI encode is same as after QOI decode.');
}
} else {
console.error('Error: failed to decode QOI file content.');
}
}
return new Blob( [data], { type : 'image/x-qoi' } );
}
}
async function getImageElementFromData(imageData, project, colorsCount) {
if (imageData = getImageDataFromData(imageData)) {
if (
PNG_USE_UPNG
&& await loadLibOnDemandPromise('UPNG.js')
) {
const arrayBuffer = UPNG.encode(
[imageData.data.buffer] //* <- array of frames. A frame is an ArrayBuffer (RGBA, 8 bits per channel).
, imageData.width
, imageData.height
, orz(colorsCount) //* <- does not make color-fill PNGs smaller
);
const entry = await getImageBlobAndURLFromDataOrList(arrayBuffer, TYPE_IMAGE_PNG, project);
let { img, url, blob } = entry;
if (img) {
img.imageDataLength = imageData.data.length;
img.fileSize = arrayBuffer.byteLength;
return img;
} else {
const img = entry.img = cre('img', (TESTING_PNG ? document.body : null));
img.src = url;
img.imageDataLength = imageData.data.length;
img.fileSize = arrayBuffer.byteLength;
if (TESTING_PNG) console.log('getImageElementFromData:', [ imageData, arrayBuffer, url, img ]);
return new Promise(
(resolve, reject) => resolvePromiseOnImageLoad(img, resolve, reject)
).catch(catchPromiseError);
}
} else {
let canvas, img;
if (
isCanvasElement(canvas = getCanvasFromImageData(imageData))
&& isImageElement(img = await getImagePromiseFromCanvasToBlob(canvas, project))
) {
img.imageDataLength = imageData.data.length;
return img;
}
}
}
}
function getImageElementFromSrc(url) {
if (
isString(url)
&& url.length
) {
const img = cre('img');
img.src = url;
return img;
}
}
function getImageDataFromArrayBuffer(arrayBuffer) {
if (isArrayBuffer(arrayBuffer)) {
const decoded = UPNG.decode(arrayBuffer);
const frames = UPNG.toRGBA8(decoded); //* <- UPNG.toRGBA8 returns array of frames, size = width * height * 4 bytes.
const imageData = new ImageData(decoded.width, decoded.height);
imageData.data.set(new Uint8Array(frames[0]));
if (TESTING_PNG) console.log('getImageDataFromArrayBuffer:', [
'arrayBuffer:', arrayBuffer,
'decoded:', decoded,
'frames:', frames,
'imageData:', imageData,
]);
return imageData;
}
}
function getArrayBufferPromiseFromURL(url) {
return (
getFilePromiseFromURL(url, 'arraybuffer')
.catch(catchPromiseError)
);
}
async function getImageDataDecodedFromImage(source) {
if (
PNG_USE_UPNG
&& await loadLibOnDemandPromise('UPNG.js')
) {
return getImageDataFromArrayBuffer(
isArrayBuffer(source)
? source
:
isBlob(source)
? (source.buffer || await source.arrayBuffer())
: await getArrayBufferPromiseFromURL(
isImageElement(source)
? source.src
: source
)
);
} else {
return await new Promise(
(resolve, reject) => resolvePromiseOnImageLoad(
getImageElementFromSrc(source)
, (img) => resolve(getImageDataFromCanvasOrImage(img))
, reject
)
).catch(catchPromiseError);
}
}
async function getImageBlobAndURLFromDataOrList(data, type, trackList) {
if (isArray(data)) {
data = Uint8Array.from(data, (v) => v.charCodeAt(0)).buffer;
}
const blob = (
isBlob(data)
? data
: new Blob( [data], { type } )
);
//* Reuse buffer to avoid recreating it later:
if (isArrayBuffer(data)) {
blob.buffer = data;
}
//* Reuse old blob:
if (trackList = getTrackListFromProject(trackList)) {
let entry;
async function getImageBlobAndURLFromList(entry, key) {
if (
entry
&& entry.blob
&& await isIdenticalData(entry.blob, blob)
) {
if (TESTING > 1) console.log('getImageBlobAndURLFromDataOrList: reused image', [ key, entry, blob, trackList ]);
return entry;
}
}
if (
(trackList instanceof Array)
|| (trackList instanceof Set)
) {
for (const item of trackList) {
if (entry = await getImageBlobAndURLFromList(item)) {
return entry;
}
}
} else
if (trackList instanceof Map) {
for (const [key, item] of trackList) {
if (entry = await getImageBlobAndURLFromList(item, key)) {
return entry;
}
}
} else {
for (const key in trackList) {
if (entry = await getImageBlobAndURLFromList(trackList[key], key)) {
return entry;
}
}
}
}
//* Use new blob:
const url = URL.createObjectURL(blob);
const entry = { url, blob };
if (trackList) {
await addURLToTrackList(entry, trackList);
}
return entry;
}
async function getImageBlobAndURLFromData(data, trackList) {
if (URL && URL.createObjectURL) {
let type = TYPE_TEXT;
if (hasPrefix(data, DATA_PREFIX)) {
const i = data.indexOf(',');
const meta = data.slice(0,i);
const k = meta.indexOf(';');
data = data.slice(i+1);
if (k < 0) {
type = meta;
data = decodeURIComponent(data);
} else {
type = meta.slice(0,k);
if (meta.slice(k+1) === 'base64') data = atob(data);
}
}
const size = data.length;
const { url, blob } = await getImageBlobAndURLFromDataOrList(data, type, trackList);
if (url) {
return { blob, size, type, url };
}
}
}
async function saveDL(data, fileName, ext, addTime, jsonReplacerFunc) {
function cleanUpAfterDL() {
if (a) del(a);
if (mustRevokeURL) URL.revokeObjectURL(url);
}
if (TESTING > 1) console.log('saveDL:', fileName, ext, data);
if (!data) {
return;
}
let blob, size, type, url;
let mustRevokeURL = false;
if (isBlob(data)) {
blob = data;
size = blob.size;
type = blob.type;
url = URL.createObjectURL(blob);
mustRevokeURL = true;
} else {
type = TYPE_TEXT;
data = (
isNotEmptyString(data)
? data
: isNonNullObject(data)
? JSON.stringify(
data
, jsonReplacerFunc || null
, '\t'
)
: String(data)
);
if (hasPrefix(data, BLOB_PREFIX)) {
url = data;
blob = true;
size = data.length;
} else {
const hasDataPrefix = hasPrefix(data, DATA_PREFIX);
if (
(
!hasDataPrefix
|| data.length > 1234567
)
&& (blob = await getImageBlobAndURLFromData(data))
) {
size = blob.size;
type = blob.type;
url = blob.url;
mustRevokeURL = true;
}
if (!url) {
if (hasDataPrefix) {
url = data;
} else {
url = DATA_PREFIX + type + ',' + encodeURIComponent(data);
}
size = url.length;
type = url.split(';', 1)[0].split(':', 2)[1];
}
if (!ext) {
ext = type.split('/').pop();
}
}
}
if (ext === 'plain') ext = 'txt';
const time = (
!fileName || addTime
? getFormattedTime(FLAG_TIME_FILENAME_FORMAT)
: ''
);
const baseName = (
!fileName ? time
: (addTime > 0) ? fileName + '_' + time
: (addTime < 0) ? time + '_' + fileName
: fileName
);
fileName = baseName;
if (ext && !hasPostfix(fileName, ext = dotExt(ext))) {
fileName += ext;
}
if (LOG_ACTIONS) logTime(
'saving "' + fileName + '", '
+ (
blob
? 'data size = ' + size + ' bytes, blob URI = ' + url
: 'data URI size = ' + size + ' bytes'
)
);
const a = cre('a', document.body);
if (a && 'download' in a) {
a.download = fileName;
a.href = url;
a.click();
} else {
window.open(url, '_blank');
if (LOG_ACTIONS) logTime('opened file to save');
}
if (a) {
const msec = Math.max(Math.ceil(size / 1000), 12345);
a.setAttribute('data-self-remove-pause', msec);
setTimeout(cleanUpAfterDL, msec);
}
return size;
}
function loadLibPromise(...libs) {
let dir, scripts;
libs = getFlatArray(libs);
return new Promise(
(resolve, reject) => {
function addNextScript() {
if (
scripts
&& scripts.length > 0
) {
const src = scripts.shift();
if (isNotEmptyString(src)) {
const script = cre('script', document.head);
script.onload = addNextScript;
script.onerror = (evt) => getErrorFromEvent(evt, 'Script loading failed.', reject);
script.src = dir + src;
} else {
addNextScript();
}
} else if (
libs
&& libs.length > 0
) {
const lib = libs.shift();
dir = lib.dir || '';
scripts = asFlatArray(lib.files || lib);
addNextScript();
} else {
resolve(true);
}
}
addNextScript();
}
).catch(catchPromiseError);
}
async function loadAllLibsOnDemand(...libs) {
for (const libName of getFlatArray(libs))
if (! await loadLibOnDemandPromise(libName)) {
return false;
}
return true;
}
async function loadLibOnDemandPromise(libName) {
if (!libName) {
return true;
}
const lib = FILE_TYPE_LIBS[libName] || null;
if (!lib) {
return false;
}
const varName = lib.varName || '';
if (varName && window[varName]) {
return true;
}
//* Avoid duplicate loading, possible due to async:
if (
lib.loadingPromise
&& getScriptElementsByLibName(libName).length > 0
) {
return await lib.loadingPromise;
}
const dir = lib.dir || '';
const scripts = asFlatArray(lib.files);
const depends = asFlatArray(lib.depends);
if (
!scripts.length
|| ! await loadAllLibsOnDemand(...depends)
) {
return false;
}
return lib.loadingPromise = new Promise(
(resolve, reject) => {
function addNextScript(evt) {
//* Some var init, no better place for this:
if (varName && window[varName]) {
if (varName === 'zip') {
if (zip.useWebWorkers = USE_WORKERS_IF_CAN) {
//* Notes:
//* Either zip.workerScripts or zip.workerScriptsPath may be set, not both.
//* Scripts in the array are executed in order, and the first one should be z-worker.js, which is used to start the worker.
//* Source: http://gildas-lormeau.github.io/zip.js/core-api.html#alternative-codecs
if (ZIP_WORKER_SCRIPTS) {
zip.workerScripts = {
'deflater' : ZIP_WORKER_SCRIPTS,
'inflater' : ZIP_WORKER_SCRIPTS,
};
} else {
zip.workerScriptsPath = dir;
}
}
}
if (varName === 'ora') {
ora.preloadImages = false;
ora.zipCompressImageFiles = true;
ora.enableWorkers = USE_WORKERS_IF_CAN;
ora.scriptsPath = dir;
}
}
if (
varName === 'PSD_JS'
&& !window[varName]
&& evt
&& typeof require !== 'undefined'
) {
window[varName] = require('psd');
}
//* Add scripts one by one, skip empty values:
let scriptSrc;
while (scripts.length > 0)
if (scriptSrc = scripts.shift()) {
break;
}
if (scriptSrc) {
const script = cre('script', document.head);
script.setAttribute('data-lib-name', libName);
script.onload = addNextScript;
script.onerror = (evt) => getErrorFromEvent(evt, 'Script loading failed.', reject);
script.src = dir + scriptSrc;
} else
//* Then check whether the required object is present:
if (!varName || window[varName]) {
if (LOG_ACTIONS) logTime('"' + libName + '" library finished loading');
resolve(true);
} else {
//* Otherwise, cleanup and report fail:
del(getScriptElementsByLibName(libName));
if (LOG_ACTIONS) logTime('"' + libName + '" library failed loading');
resolve(false);
}
}
addNextScript();
}
).catch(catchPromiseError);
}
function getScriptElementsByLibName(libName) {
return (
getAllByTag('script', document.head)
.filter(
(script) => (script.getAttribute('data-lib-name') === libName)
)
)
}
//* Page-specific functions: internal, utility *-------------------------------
function getProjectContainer(element) { return getTargetParentByClass(element, matchClassLoadedFile); }
function getProjectButton(element) { return getTargetParentByClass(element, matchClassButton); }
function replaceJSONpartsForCropRef(key, value) {
if (key === 'autocrop') {
return;
}
return value;
}
function replaceJSONpartsForZoomRef(key, value) {
if (key === 'autocrop') {
return;
}
if (key === 'zoom') {
//* Remove invalid values, reassure the percent sign:
if (isString(value)) {
const targetRefZoom = orz(value);
if (targetRefZoom <= 0 || targetRefZoom === 100) {
return;
}
//* Zoom in steps, downscale by no more than x2, starting from 100 to nearest-sized reference:
let nearestRefZoom = 100;
while (
nearestRefZoom > 0
&& nearestRefZoom > targetRefZoom
) {
const nextStepZoom = Math.floor(nearestRefZoom / ZOOM_STEP_MAX_FACTOR);
if (targetRefZoom >= nextStepZoom) {
break;
}
nearestRefZoom = nextStepZoom;
}
if (nearestRefZoom <= 0 || nearestRefZoom === 100) {
return;
}
return String(nearestRefZoom) + '%';
} else
//* Keep as is the same-key object parent, throw away anything else:
if (!isNonNullObject(value)) {
return;
}
}
return value;
}
function clearCanvasBeforeGC(canvas) {
if (
CLEAR_CANVAS_FOR_GC
&& canvas
&& isCanvasElement(canvas = canvas.canvas || canvas)
) {
canvas.width = 1;
canvas.height = 1;
return canvas;
}
}
function getImageSrcPlaceholder() {
if (!thumbnailPlaceholder) {
const canvas = cre('canvas');
const ctx = canvas.getContext('2d');
const imageSize = TAB_THUMBNAIL_SIZE || THUMBNAIL_SIZE;
const w = canvas.width = imageSize;
const h = canvas.height = imageSize;
const textHeight = imageSize - 2;
ctx.fillStyle = 'lightgray';
ctx.fillRect(0,0, w,h);
ctx.lineWidth = 1;
ctx.strokeStyle = 'gray';
ctx.strokeRect(0,0, w,h);
ctx.fillStyle = 'gray';
ctx.strokeStyle = 'gray';
ctx.textAlign = 'left';
ctx.textBaseline = 'top';
ctx.font = 'bold ' + textHeight + 'px sans-serif';
const text = '?';
const textWidth = ctx.measureText(text).width;
const x = Math.round((w - textWidth) / 2);
const y = Math.round((h - textHeight) / 2);
ctx.fillText(text, x,y);
thumbnailPlaceholder = canvas.toDataURL();
}
return thumbnailPlaceholder;
}
function setImageSrc(img, data, onLoad, onError) {
if (isImageElement(img)) {
if (data || !img.src) {
img.src = data || getImageSrcPlaceholder();
if (onLoad || onError) {
return new Promise(
(resolve, reject) => resolvePromiseOnImageLoad(
img
, (onLoad ? ((img) => { resolve(img), onLoad(img) }) : resolve)
, (onError ? ((error) => { reject(error), onError(error) }) : reject)
)
).catch(catchPromiseError);
}
}
} else
if (
isNonNullObject(img)
&& img.style
) {
if (data || !img.style.backgroundImage) {
data = data || getImageSrcPlaceholder();
img.style.backgroundImage = 'url("' + data + '")';
}
}
return img;
}
function getImageContentSize(img) {
if (isNonNullObject(img)) {
const width = img.naturalWidth || img.width;
const height = img.naturalHeight || img.height;
if (width && height) {
return { width, height };
}
}
if (TESTING > 2) console.error('getImageContentSize failed:', arguments);
}
function getCanvasFromImage(img) {
const size = getImageContentSize(img);
if (!size) {
return;
}
const canvas = cre('canvas');
const ctx = canvas.getContext('2d');
canvas.width = size.width;
canvas.height = size.height;
ctx.drawImage(img, 0,0);
return canvas;
}
function getResizedCanvasFromImage(img, w,h) {
const size = getImageContentSize(img);
if (!size) {
return;
}
const canvas = cre('canvas');
const ctx = canvas.getContext('2d');
const widthFrom = size.width;
const heightFrom = size.height;
let widthTo = w || widthFrom || 1;
let heightTo = h || w || heightFrom || 1;
const widthRatio = widthFrom/widthTo;
const heightRatio = heightFrom/heightTo;
const zoomFactor = TAB_ZOOM_STEP_MAX_FACTOR || ZOOM_STEP_MAX_FACTOR;
if (
widthRatio > zoomFactor
|| heightRatio > zoomFactor
) {
//* Caclulate nearest scale factor top down:
if (DOWNSCALE_BY_MAX_FACTOR_FIRST) {
canvas.width = widthTo = Math.round(widthFrom / zoomFactor);
canvas.height = heightTo = Math.round(heightFrom / zoomFactor);
} else {
//* Caclulate nearest scale factor bottom up - more complex, but result is not better:
if (widthRatio < heightRatio) {
widthTo = Math.round(widthFrom / heightRatio);
} else
if (widthRatio > heightRatio) {
heightTo = Math.round(heightFrom / widthRatio);
}
let widthToUp = widthTo;
let heightToUp = heightTo;
while (
widthTo < widthFrom
&& heightTo < heightFrom
) {
widthToUp *= zoomFactor;
heightToUp *= zoomFactor;
if (
widthToUp < widthFrom
&& heightToUp < heightFrom
) {
widthTo = widthToUp;
heightTo = heightToUp;
} else {
break;
}
}
canvas.width = widthTo;
canvas.height = heightTo;
}
ctx.drawImage(img, 0,0, widthFrom, heightFrom, 0,0, widthTo, heightTo);
return getResizedCanvasFromImage(canvas, w,h);
} else {
let xOffset = 0;
let yOffset = 0;
canvas.width = widthTo;
canvas.height = heightTo;
if (widthRatio < heightRatio) {
widthTo = Math.round(widthFrom / heightRatio);
xOffset = Math.round((canvas.width - widthTo) / 2);
} else
if (widthRatio > heightRatio) {
heightTo = Math.round(heightFrom / widthRatio);
yOffset = Math.round((canvas.height - heightTo) / 2);
}
ctx.drawImage(img, 0,0, widthFrom, heightFrom, xOffset, yOffset, widthTo, heightTo);
return canvas;
}
}
function getCanvasFromImageData(imageData) {
if (imageData = getImageDataFromData(imageData)) {
const canvas = cre('canvas');
const ctx = canvas.getContext('2d');
const { data, width, height } = imageData;
canvas.width = width;
canvas.height = height;
ctx.putImageData(imageData, 0,0);
return canvas;
}
if (TESTING > 2) console.error('getCanvasFromImageData failed:', arguments);
}
function getCtxFromImageData(imageData) {
const canvas = getCanvasFromImageData(imageData);
if (canvas) {
return canvas.getContext('2d');
}
}
function getMaskFromOpaqueImageData(img) {
if (!isNonNullImageData(img)) {
return;
}
const mask = new ImageData(img.width, img.height);
for (
let sourceData = img.data
, maskData = mask.data
, index = sourceData.length - 1;
index >= 0;
index -= 4
) {
maskData[index] = sourceData[index - 1];
}
if (TESTING > 1) console.log('getMaskFromOpaqueImageData:', [img, 'maskData:', mask]);
return mask;
}
function getCanvasFromMaskInvertAlpha(img) {
const canvas = getCanvasFromImage(img);
if (!canvas) {
return;
}
const ctx = canvas.getContext('2d');
const w = canvas.width;
const h = canvas.height;
ctx.globalAlpha = 1;
ctx.globalCompositeOperation = BLEND_MODE_INVERT;
ctx.fillStyle = DEFAULT_MASK_FILL_COLOR;
ctx.fillRect(0,0, w,h);
return canvas;
}
function getImageDataInvertAlpha(imageData) {
const ctx = getCtxFromImageData(imageData);
if (!ctx) {
return;
}
const w = imageData.width;
const h = imageData.height;
ctx.globalAlpha = 1;
ctx.globalCompositeOperation = BLEND_MODE_INVERT;
ctx.fillStyle = DEFAULT_MASK_FILL_COLOR;
ctx.fillRect(0,0, w,h);
return ctx.getImageData(0,0, w,h);
}
function getImageDataFromCanvasOrImage(img, x,y, w,h) {
const size = getImageContentSize(img);
if (!size) {
return;
}
if (isCanvasElement(img)) {
const canvas = img;
const ctx = canvas.getContext('2d');
x = orz(x);
y = orz(y);
w = orz(w) || (size.width - x);
h = orz(h) || (size.height - y);
return ctx.getImageData(x,y, w,h);
}
if (isImageElement(img)) {
const canvas = cre('canvas');
const ctx = canvas.getContext('2d');
canvas.width = w = orz(w || size.width) - orz(x);
canvas.height = h = orz(h || size.height) - orz(y);
x = 0;
y = 0;
ctx.drawImage(img, x,y);
return ctx.getImageData(x,y, w,h);
}
}
function getFirstPixelRGBA(img) {
if (
isNonNullObject(img)
&& (
img.slice
|| (
(img = getImageDataFromCanvasOrImage(img, 0,0, 1,1))
&& (img = img.data)
&& img.slice
)
)
) {
return img.slice(0,4);
}
if (TESTING > 2) console.error('getFirstPixelRGBA failed:', arguments);
}
function getAutoCropArea(img, bgToCheck) {
const imageData = getImageDataFromCanvasOrImage(img);
if (!isNonNullImageData(imageData)) {
return;
}
const w = imageData.width;
const h = imageData.height;
const data = imageData.data;
const totalBytes = data.length;
const horizontalBytes = w << 2;
let bgRGBA;
let bgPixelIndex = -1;
let foundBottom = -1;
let foundRight = -1;
let foundLeft = -1;
let foundTop = -1;
if (
bgToCheck
&& bgToCheck.length
&& bgToCheck !== 'transparent'
) {
if (bgToCheck === 'topleft') bgPixelIndex = 0; else
if (bgToCheck === 'topright') bgPixelIndex = w - 1; else
if (bgToCheck === 'bottomleft') bgPixelIndex = w*(h - 1); else
if (bgToCheck === 'bottomright') bgPixelIndex = (w*h) - 1;
if (bgPixelIndex >= 0) {
const bgByteIndex = bgPixelIndex << 2;
bgRGBA = data.subarray(bgByteIndex, bgByteIndex + 4);
} else {
if (isString(bgToCheck)) {
bgToCheck = getRGBAFromColorCodeOrName(bgToCheck);
}
if (isArray(bgToCheck)) {
bgRGBA = getRGBACutOrPadded(bgToCheck);
}
}
}
if (!isArray(bgRGBA)) {
bgRGBA = TRANSPARENT_COLOR_VALUE_ARRAY;
}
//* Find fully transparent areas:
if (bgRGBA[3] === 0) {
find_top:
for (let index = 3; index < totalBytes; index += 4) {
if (data[index]) {
foundTop = Math.floor(index / horizontalBytes);
break find_top;
}
}
//* Found no content:
if (foundTop < 0) {
return;
}
//* Found something:
find_bottom:
for (let index = totalBytes - 1; index >= 0; index -= 4) {
if (data[index]) {
foundBottom = Math.floor(index / horizontalBytes);
break find_bottom;
}
}
//* Reduce field of search:
const foundTopIndex = (foundTop * horizontalBytes) + 3;
const foundBottomIndex = (foundBottom * horizontalBytes) + 3;
find_left:
for (let x = 0; x < w; ++x)
for (let index = (x << 2) + foundTopIndex; index <= foundBottomIndex; index += horizontalBytes) {
if (data[index]) {
foundLeft = x;
break find_left;
}
}
find_right:
for (let x = w-1; x >= 0; --x)
for (let index = (x << 2) + foundTopIndex; index <= foundBottomIndex; index += horizontalBytes) {
if (data[index]) {
foundRight = x;
break find_right;
}
}
} else {
//* Find same RGBA filled areas:
let index = totalBytes;
find_bottom:
while (index--) {
if (data[index] !== bgRGBA[index & 3]) {
foundBottom = Math.floor(index / horizontalBytes);
break find_bottom;
}
}
//* Found no content:
if (foundBottom < 0) {
return;
}
//* Found something:
find_top:
for (let index = 0; index < totalBytes; ++index) {
if (data[index] !== bgRGBA[index & 3]) {
foundTop = Math.floor(index / horizontalBytes);
break find_top;
}
}
//* Reduce field of search:
const foundTopIndex = (foundTop * horizontalBytes);
const foundBottomIndex = (foundBottom * horizontalBytes);
find_left:
for (let x = 0; x < w; ++x)
for (let index = (x << 2) + foundTopIndex; index <= foundBottomIndex; index += horizontalBytes) {
if (
data[index ] !== bgRGBA[0]
|| data[index|1] !== bgRGBA[1]
|| data[index|2] !== bgRGBA[2]
|| data[index|3] !== bgRGBA[3]
) {
foundLeft = x;
break find_left;
}
}
find_right:
for (let x = w-1; x >= 0; --x)
for (let index = (x << 2) + foundTopIndex; index <= foundBottomIndex; index += horizontalBytes) {
if (
data[index ] !== bgRGBA[0]
|| data[index|1] !== bgRGBA[1]
|| data[index|2] !== bgRGBA[2]
|| data[index|3] !== bgRGBA[3]
) {
foundRight = x;
break find_right;
}
}
}
const foundWidth = foundRight - foundLeft + 1;
const foundHeight = foundBottom - foundTop + 1;
return {
'left' : foundLeft
, 'right' : foundRight
, 'top' : foundTop
, 'bottom' : foundBottom
, 'width' : foundWidth
, 'height' : foundHeight
};
}
function addButton(parent, text, func) {
const button = cre('button', parent);
button.textContent = text || button.tagName;
if (func) {
button.setAttribute('onclick', func);
}
return button;
}
function addNamedButton(container, name, label) {
const button = addButton(container, getLocalizedText(label || name));
button.name = name || label;
return button;
}
function addButtonGroup(container, group) {
const buttonsBox = cre('div', container);
buttonsBox.className = 'panel';
for (const buttonName in group) {
const entry = group[buttonName];
if (isString(entry)) {
addNamedButton(buttonsBox, buttonName, entry);
} else
if (isNonNullObject(entry)) {
const nestedBox = addButtonGroup(buttonsBox, entry);
nestedBox.classList.add('row');
}
}
return container;
}
function addOption(parent, text, value) {
const option = cre('option', parent);
text = getJoinedOrEmptyText(text);
value = getJoinedOrEmptyText(value) || text;
option.value = value;
option.textContent = text;
return option;
}
function trimParam(text) {
return (
String(text)
.replace(regTrimParam, '')
);
}
function getOtherSwitchParamName(switchType, switchName) {
const names = SWITCH_NAMES_BY_TYPE[switchType];
const index = names.indexOf(switchName);
return names[index === 0 ? 1 : 0];
}
function isLayerGroupMasked(layers) {
return (
isArray(layers)
&& layers.length > 1
&& regLayerBlendModeMask.test(layers[0].blendMode)
);
}
function getNameForAuxLayer(prefix, name) {
let fullText, shorterText;
if (
SAVE_ADDITIONAL_LAYER_NAMES
&& (name = trim(name)).length > 0
&& (fullText = trim(
name
.replace(regSanitizeLayerComment, '')
.replace(regSpace, ' ')
)).length > 0
) {
if (fullText.length > 32) {
shorterText = trim(
name
.replace(regSanitizeLayerName, '')
.replace(regSanitizeLayerComment, '')
.replace(regSpace, ' ')
);
}
return '(' + prefix + ': ' + (shorterText || fullText) + ')';
}
return '(' + prefix + ')';
}
function getTruthyValue(value) {
return !(
!value
|| !(value = String(value))
|| FALSY_STRINGS.includes(value.toLowerCase())
);
}
function getNormalizedOpacity(numValue) {
return Math.max(0, Math.min(1, orz(numValue) / MAX_CHANNEL_VALUE));
}
function getNormalizedBlendMode(text, remaps, replacements) {
remaps = remaps || BLEND_MODES_REMAP;
replacements = replacements || BLEND_MODES_REPLACE;
const blendMode = String(text).toLowerCase();
let replaced;
return (
remaps[blendMode]
|| remaps[
replaced = trim(
replacements.reduce(
(text, [ from, to ]) => text.replace(from, to || '')
, blendMode
)
)
]
|| replaced
|| blendMode
);
}
function getOraBlendMode(text) {
return getNormalizedBlendMode(
text
, BLEND_MODES_REMAP_TO_ORA
, BLEND_MODES_REPLACE_TO_ORA
);
}
function getBlendModeFunctionName(text) { return text.replace(/\W+/g, '_').toLowerCase(); }
function getParentLayer(layer, stopConditionCallback) {
if (!layer) {
return null;
}
const isStopConditionDefined = isFunction(stopConditionCallback);
while (layer = layer.parent) {
if (
layer.params //* <- skip arrays, stop on actual layer objects
&& (
!isStopConditionDefined
|| stopConditionCallback(layer) //* <- optionally look deeper for something else
)
) {
break;
}
}
return layer;
}
function getLayerVisibilityParent(layer) {
if (layer.copyPastedTo) {
return null;
}
return layer.clippingLayer || getParentLayer(layer);
}
function getLayerChain(layer, getNextLayerCallback) {
const layers = [];
while (layer) {
layers.push(layer);
layer = getNextLayerCallback(layer);
}
return layers.reverse();
}
function getLayerNestingChain(layer) { return getLayerChain(layer, getParentLayer); }
function getLayerVisibilityChain(layer) { return getLayerChain(layer, getLayerVisibilityParent); }
function getLayerPathNamesChain(layer, flags) {
if (!layer) {
return DUMMY_EMPTY_ARRAY;
}
if (!isNonNullObject(flags)) {
flags = {};
}
const path = (flags.includeSelf ? [ layer.name || layer.nameOriginal ] : []);
while (layer = getParentLayer(layer)) {
path.unshift(layer.name || layer.nameOriginal);
}
if (flags.asText) {
return path.join(flags.separator || ' / ');
}
return path;
}
function getLayerPathText(layer) {
return getLayerPathNamesChain(layer, FLAG_LAYER_PATH_TEXT);
}
function getJoinedNames(layers) {
return layers.map((layer) => (layer.name || layer.nameOriginal)).join(' / ');
}
function getLayersTopSeparated(layers) {
let layersToRender = DUMMY_EMPTY_ARRAY;
while (isNonEmptyArray(layers)) {
layersToRender = layers.filter(isLayerRendered);
if (layersToRender.length > 1) {
return layersToRender;
} else
if (layersToRender.length > 0) {
layers = layersToRender[0].layers;
} else {
break;
}
}
return layersToRender;
}
function isLayerClippedOrMask(layer) {
if (TESTING && !(isNonNullObject(layer) && layer.blendMode)) {
console.error('isLayerClippedOrMask: No blendMode, maybe not a layer:', [ layer, getLayerPathText(layer) ]);
}
return (
// regLayerBlendModeAlpha.test(layer.blendMode)
regLayerBlendModeClip.test(layer.blendMode)
|| regLayerBlendModeMask.test(layer.blendMode)
);
}
function isLayerRendered(layer) {
if (TESTING && !(isNonNullObject(layer) && layer.params)) {
console.error('isLayerRendered: No params, maybe not a layer:', [ layer, getLayerPathText(layer) ]);
}
return !(
layer.isSkipped
|| layer.params.skip
|| layer.params.skip_render
|| (layer.clippingLayer && !isLayerRendered(layer.clippingLayer))
);
}
function isLayerSkipped(layer) {
if (TESTING && !(isNonNullObject(layer) && layer.params)) {
console.error('isLayerSkipped: No params, maybe not a layer:', [ layer, getLayerPathText(layer) ]);
}
return !!(
layer.isSkipped
|| layer.params.skip
|| (regLayerNameToSkip && regLayerNameToSkip.test(layer.name))
|| (layer.clippingLayer && isLayerSkipped(layer.clippingLayer))
);
}
function hasImageToLoad(layer) {
return (
layer.params.color_code
|| IMAGE_DATA_KEYS_TO_LOAD.some((key) => (key in layer))
|| (
layer.mask
&& IMAGE_DATA_KEYS_TO_LOAD.some((key) => (key in layer.mask))
)
|| (
!layer.layers
&& layer.width > 0
&& layer.height > 0
)
);
}
//* Pile of hacks and glue to get things working:
async function getOrLoadFixedImage(project, layer) {
const img = layer.img || await getOrLoadImage(project, layer);
if (img) {
img.top = layer.top;
img.left = layer.left;
}
return img;
}
async function getOrLoadImage(project, layer) {
async function thisToPngTryOne(target, node) {
function getResultPromiseIfMethodExists(methodName) {
return (
isNonRecursiveFunction(node[methodName])
? new Promise(
(resolve, reject) => (
hasPrefix(methodName, 'to')
? resolvePromiseOnImageLoad(node[methodName](), resolve, reject)
: node[methodName](resolve, reject)
)
).catch(catchPromiseError)
: null
);
}
function isNonRecursiveFunction(func) {
return (
func
&& isFunction(func)
&& (
func !== getOrLoadImage
|| node !== target
)
);
}
if (isImageElement(node)) {
return node;
}
let pixelData, img;
//* Try converting raw pixel data:
if (pixelData = getPropByAnyOfNamesChain(node, 'imageData', 'imgData', 'maskData', 'pixelData')) {
const imageData = {
'data' : getPropByAnyOfNamesChain(pixelData, 'data')
, 'width' : getPropFromAnySource('width', pixelData, target, node)
, 'height' : getPropFromAnySource('height', pixelData, target, node)
};
if (img = (
getRGBAFromDataIfColorFill(imageData, project, layer)
|| await getImageElementFromData(imageData, project)
)) {
return img;
}
}
//* Try library-provided methods, which internally may use canvas API and possibly premultiply alpha, trading precision for speed:
for (
const methodName
of [
'loadImage',
'toImage',
'toImagePngBlobPromise',
'toImagePngBase64Promise',
'toPng',
]
) {
if (img = await getResultPromiseIfMethodExists(methodName)) {
return img;
}
}
return null;
}
async function thisToPngTryEach(...targets) {
let img;
for (const target of targets) if (isNonNullObject(target))
for (
const sourceOrTarget
of (target.isProject ? [
target.sourceData
, target.sourceDataFile
// , target.sourceDataNode
, target
] : [
target
])
) if (isNonNullObject(sourceOrTarget))
for (
const mergedOrNode
of (layer ? [
sourceOrTarget
] : [
sourceOrTarget.mergedImage
, sourceOrTarget.prerendered
, sourceOrTarget.thumbnail
, sourceOrTarget
])
) if (isNonNullObject(mergedOrNode))
for (
const imgOrNode
of [
mergedOrNode.image
, mergedOrNode.img
, mergedOrNode
]
) if (isNonNullObject(imgOrNode))
if (img = await thisToPngTryOne(target, imgOrNode)) {
if (
(
PNG_OPTIMIZE_ALL
|| PNG_OPTIMIZE_UNCOMPRESSED
)
&& isImageElement(img)
&& !img.imageDataLength
) {
let buffer;
const url = img.src;
const imageSize = getImageContentSize(img);
const fileSize = (
img.file_size
|| img.fileSize
|| getPropByNameChain(
buffer = await getArrayBufferPromiseFromURL(url)
, 'byteLength'
)
);
if (
PNG_OPTIMIZE_ALL
|| (
fileSize >
imageSize.width *
imageSize.height * 3
)
) {
const imageData = await getImageDataDecodedFromImage(buffer || url);
const reencodedImage = (
getRGBAFromDataIfColorFill(imageData, project, layer)
|| await getImageElementFromData(imageData, project)
);
if (reencodedImage) {
if (TESTING_PNG) console.log('getOrLoadImage: reencoded', [
'imageDataLength:', img.imageDataLength,
'img:', img,
'url:', url,
'size:', fileSize,
'buffer:', buffer,
'imageData:', imageData,
'reencodedImage:', reencodedImage,
'reencodedSize:', reencodedImage.fileSize,
'layer:', layer,
'project:', project,
]);
img = reencodedImage;
}
}
} else
if (img.imageDataLength) {
if (TESTING_PNG > 1) console.log('getOrLoadImage: skipped reencoding', [
'fileSize:', img.fileSize,
'imageDataLength:', img.imageDataLength,
'img:', img,
'layer:', layer,
'project:', project,
]);
}
if (layer) {
target.img = img;
} else {
target.mergedImage = img;
}
if (layer) {
if (layer.top) img.top = layer.top;
if (layer.left) img.left = layer.left;
}
if (project) {
if (!layer) {
img.alt =
img.title = project.fileName;
} else
if (addToListIfNotYet(project.imagesLoaded, img)) {
img.alt =
img.title = layer.name;
}
const url = img.src;
if (hasPrefix(url, BLOB_PREFIX)) {
await addURLToTrackList(
(
DEDUPLICATE_LOADED_IMAGES
? { url, img }
: url
)
, project
);
}
}
return img;
}
return null;
}
try {
return await thisToPngTryEach(layer || project);
} catch (error) {
if (layer) {
console.error('Failed to get layer or mask image:', [ getLayerPathText(layer), error ]);
} else {
console.error('Failed to get project image:', error);
}
}
}
//* Page-specific functions: internal, loading *-------------------------------
function isStopRequestedAnywhere(...sources) {
return (
isStopRequested
|| sources.some(
(obj) => (
isNonNullObject(obj)
&& (
obj.isStopRequested
|| (
obj.buttonTab
&& obj.buttonTab.isStopRequested
)
)
)
)
);
}
async function removeProjectView(fileId) {
const countDeleted = getAllById(fileId).reduce(
(count, container) => {
if (container.project) {
container.project.isStopRequested = true;
}
if (del(container)) {
++count;
}
}
, 0
);
if (countDeleted && LOG_ACTIONS) {
logTime('"' + fileId + '" closed, ' + countDeleted + ' element(s) removed');
}
}
async function addProjectViewTab(sourceFile, startTime) {
if (START_WITH_OPEN_FIRST_MENU_TAB) {
closeAllDropdownMenuTabs(getAllByClass('menu-bar')[0]);
}
if (!sourceFile) {
return false;
}
if (!sourceFile.name) {
if (sourceFile.url) {
sourceFile.name = getFileName(getFilePathFromUrl(sourceFile.url));
}
}
if (!sourceFile.name) {
return false;
}
if (!sourceFile.baseName) {
sourceFile.baseName = getFileBaseName(sourceFile.name);
}
if (!sourceFile.ext) {
sourceFile.ext = getFileExt(sourceFile.name);
}
//* Prepare detached branch of DOM:
const buttonTab = cre('div', getOneById('loaded-files-selection'));
buttonTab.className = 'button loading';
const buttonSelect = cre('div', buttonTab); //* <- not 'button' tag, because it breaks :hover in CSS in Firefox 56
buttonSelect.className = 'button-select';
const buttonThumb = cre('div', buttonSelect);
buttonThumb.className = 'button-thumbnail';
const imgHover = cre('div', buttonThumb);
imgHover.className = 'thumbnail-hover';
const imgThumb = cre('img', imgHover);
imgThumb.className = 'thumbnail';
const buttonText = cre('div', buttonSelect);
buttonText.className = 'button-text';
const buttonFileName = cre('div', buttonText);
buttonFileName.className = 'button-name';
buttonFileName.textContent = buttonFileName.title = sourceFile.name;
let buttonStatus;
if (TAB_STATUS_TEXT) {
buttonStatus = cre('div', buttonText);
buttonStatus.className = 'button-status';
}
const buttonClose = cre('div', buttonTab);
buttonClose.className = 'button-close';
buttonClose.title = getLocalizedText('hint_close_tab');
buttonClose.setAttribute('onclick', 'closeProject(this)');
setImageSrc(imgThumb);
const projectButtons = {
buttonTab,
buttonText,
buttonStatus,
imgThumb,
'errorParams' : sourceFile.ext,
'errorPossible' : 'project_status_error_file_type',
};
updateProjectOperationProgress(projectButtons, 'project_status_loading');
let project, container;
try {
project = await getNormalizedProjectData(sourceFile, projectButtons);
if (project && !isStopRequestedAnywhere(project, projectButtons)) {
buttonTab.project = project;
container = (
await getProjectViewMenu(project)
|| await getProjectViewImage(project)
);
if (isStopRequestedAnywhere(project, projectButtons)) {
container = null;
}
}
if (container) {
const fileId = 'loaded-file: ' + sourceFile.name;
cleanupObjectTree(
project
, CLEANUP_PROJECT_LAYERS_RECURSIVE_KEYS
, (
TESTING
? CLEANUP_PROJECT_AFTER_LOAD_KEYS
: CLEANUP_PROJECT_IF_NOT_TESTING_KEYS
)
);
container.className = 'loaded-file';
container.project = project;
project.container = container;
let result = !isStopRequestedAnywhere(project, projectButtons);
if (result) {
if (!project.options) {
buttonTab.className = 'button loaded without-options';
updateProjectOperationProgress(projectButtons, 'project_status_ready_no_options');
} else
if (result = await updateMenuAndShowImage(project) || TESTING) {
await updateBatchCount(project);
if (!FILE_NAMING_SUMMARY_HEADER) updateFileNamingPanel(project);
updateFileNaming(project);
buttonTab.className = 'button loaded with-options';
}
}
//* Attach prepared DOM branch to visible document:
if (result && !isStopRequestedAnywhere(project, projectButtons)) {
project.isLoaded = true;
removeProjectView(fileId);
container.id = buttonTab.id = fileId;
const parent = getOneById('loaded-files-view')
parent.appendChild(container);
buttonSelect.setAttribute('onclick', 'selectProject(this)');
if (
(lastTimeProjectTabSelectedByUser < startTime)
|| (getAllByClass('show', parent).length === 0)
) {
selectProject(buttonSelect, true);
}
return true;
}
}
//* Cleanup on errors or cancel:
} catch (error) {
logError(error, arguments);
}
buttonTab.className = 'button loading failed';
if (isStopRequestedAnywhere(project, projectButtons)) {
project = null;
projectButtons.errorPossible = 'project_status_aborted';
}
updateProjectOperationProgress(
projectButtons
, (
getPropByNameChain(project, 'loading', 'errorPossible')
|| getPropByNameChain(projectButtons, 'errorPossible')
|| 'project_status_error'
)
, ...asArray(
getPropByNameChain(project, 'loading', 'errorParams')
|| getPropByNameChain(projectButtons, 'errorParams')
|| DUMMY_EMPTY_ARRAY
)
);
setTimeout(() => removeFailedTab(buttonTab), 2000);
return false;
}
function removeFailedTab(buttonTab) {
buttonTab.classList.add('fade-out');
setTimeout(() => del(buttonTab), 800);
}
async function getFileFromLoadingData(data, projectButtons) {
projectButtons.errorPossible = 'project_status_error_loading_file';
projectButtons.errorParams = data.name;
if (isNonNullObject(data)) {
if (
!data.file
&& data.url
) {
data.file = await getFilePromiseFromURL(data.url, 'blob', projectButtons).catch(catchPromiseError);
}
return data.file;
}
}
async function getNormalizedProjectData(sourceFile, projectButtons) {
async function tryFileParserFunc(func, project) {
try {
return await func(project);
} catch (error) {
logError(error, arguments);
}
return null;
}
if (!sourceFile) {
return null;
}
if (READ_FILE_CONTENT_TO_GET_TYPE) {
if (!sourceFile.file) {
if (! await getFileFromLoadingData(sourceFile, projectButtons)) {
return null;
}
}
}
const { buttonTab, buttonText, buttonStatus, imgThumb } = projectButtons;
const { name : fileName, baseName, ext } = sourceFile;
const mimeType = getPropByNameChain(sourceFile, 'file', 'type');
const actionLabel = 'processing document structure';
let loadersTried = 0;
let project, totalStartTime;
try_loaders:
for (const loader of FILE_TYPE_LOADERS) if (loader)
if (
loader.dropFileExts.includes(ext)
|| loader.inputFileMimeTypes.includes(mimeType)
) for (const func of loader.handlerFuncs) {
projectButtons.errorPossible = 'project_status_error_in_format';
const startTime = getTimeNow();
if (!loadersTried++) {
totalStartTime = startTime;
if (LOG_ACTIONS) logTime('"' + fileName + '" started ' + actionLabel);
}
project = {
fileName
, baseName
, buttonTab
, buttonText
, buttonStatus
, 'thumbnail' : imgThumb
, 'isProject' : true
, 'isUsingAllLayers' : true
, 'usedFoldersCount' : 0
, 'usedLayersCount' : 0
, 'foldersCount' : 0
, 'layersCount' : 0
, 'imagesCount' : 0
, 'imagesLoadedCount' : 0
, 'imagesLoaded' : []
, 'loading' : {
startTime
, 'data' : sourceFile
, 'images' : []
, 'errorPossible' : 'project_status_error_in_format'
}
};
if (await tryFileParserFunc(func, project)) {
break try_loaders;
} else
if (isStopRequestedAnywhere(project, projectButtons)) {
projectButtons.errorPossible = 'project_status_aborted';
break try_loaders;
} else {
const loadingError = getPropByNameChain(project, 'loading', 'errorPossible');
const loadingContext = getPropByNameChain(project, 'loading', 'errorParams');
if (loadingError) {
projectButtons.errorPossible = loadingError;
if (loadingContext) {
projectButtons.errorParams = loadingContext;
}
}
}
project = null;
}
if (loadersTried > 0) {
const tookTime = getTimeNow() - totalStartTime;
if (LOG_ACTIONS) logTime(
'"' + fileName + '"'
+ (
project
? ' finished ' + actionLabel + ', took '
: ' stopped by ' + (
isStopRequestedAnywhere(project, projectButtons)
? 'request'
: 'error'
) + ' while ' + actionLabel + ' after '
)
+ tookTime
+ ' ms total'
);
} else {
if (TESTING) console.error(
'Error: Unknown file type:'
, [
ext
, mimeType
, fileName
]
.filter(arrayFilterNonEmptyValues)
.filter(arrayFilterUniqueValues)
);
}
if (isStopRequestedAnywhere(project, projectButtons)) {
return null;
}
return project;
}
async function getProjectViewMenu(project) {
async function preloadProjectImages(project) {
project.loading.errorPossible = 'project_status_error_in_images';
const images = (
project.loading.images
.filter(arrayFilterUniqueValues)
.filter(hasImageToLoad)
);
const imagesCount = project.loading.imagesCount = images.length;
const fileName = project.fileName;
const actionLabel = (
PRELOAD_LAYER_IMAGES
? 'preloading ' + imagesCount + ' images or colors'
: 'checking colors of ' + imagesCount + ' layers'
);
if (LOG_ACTIONS) logTime('"' + fileName + '" started ' + actionLabel);
//* Try loading one by one to avoid flood of errors:
const startTime = getTimeNow();
let lastPauseTime = startTime;
let result, layer;
while (
!isStopRequestedAnywhere(project)
&& (images.length > 0)
&& (layer = images.pop())
&& (result = await getLayerImageLoadPromise(layer, project))
&& (result = await getLayerMaskLoadPromise(layer.mask, project))
) if (
ADD_PAUSE_AT_INTERVALS
&& lastPauseTime + PAUSE_WORK_INTERVAL < getTimeNow()
) {
updateProjectLoadedImagesCount(project);
await pause(PAUSE_WORK_DURATION);
lastPauseTime = getTimeNow();
}
const tookTime = getTimeNow() - startTime;
const loadedCount = project.imagesLoadedCount;
const skippedCount = imagesCount - loadedCount;
const isStopRequested = isStopRequestedAnywhere(project);
const actionSummary = (
!skippedCount
? '' : (
', skipped ' + skippedCount
+ ', loaded ' + loadedCount
)
);
if (LOG_ACTIONS) logTime(
'"' + fileName + '"'
+ (
result
? ' finished ' + actionLabel + actionSummary + ', took '
: ' stopped by ' + (
isStopRequested
? 'request'
: 'error'
) + ' while ' + actionLabel + actionSummary + ' after '
)
+ tookTime
+ ' ms'
);
return isStopRequested ? false : result;
}
async function getProjectOptionsContainer(project) {
try {
project.loading.errorPossible = 'project_status_error_in_options';
const options = getProjectOptions(project);
if (!options) {
if (LOG_ACTIONS) logTime('"' + project.fileName + '" has no options.');
} else
if (await preloadProjectImages(project)) {
project.loading.errorPossible = 'project_status_error_creating_menu';
project.options = options;
project.layersTopSeparated = getLayersTopSeparated(project.layers);
const sectionNames = [];
const listNamesBySection = {};
const listNamesBySectionInitial = {};
const orderParams = project.namePartsOrderParams || [];
const isAllOrderAutoSorted = orderParams.includes('sort');
const isAllOrderKeptAsGiven = orderParams.includes('given');
const isSectionOrderKeptAsGiven = (
isAllOrderKeptAsGiven
|| orderParams.includes('given-types')
|| orderParams.includes('given-sections')
);
const isListOrderKeptAsGiven = (
isAllOrderKeptAsGiven
|| orderParams.includes('given-lists')
|| orderParams.includes('given-options')
);
const isSectionOrderAutoSorted = (
isAllOrderAutoSorted
|| orderParams.includes('sort-types')
|| orderParams.includes('sort-sections')
);
const isListOrderAutoSorted = (
isAllOrderAutoSorted
|| orderParams.includes('sort-lists')
|| orderParams.includes('sort-options')
);
const autoSortSectionNames = (
!isSectionOrderKeptAsGiven
&& (
isSectionOrderAutoSorted
|| SORT_OPTION_SECTION_NAMES
)
);
const autoSortListNames = (
!isListOrderKeptAsGiven
&& (
isListOrderAutoSorted
|| SORT_OPTION_LIST_NAMES
)
);
for (const sectionName in options)
if (NAME_PARTS_ORDER.includes(sectionName)) {
addToListIfNotYet(sectionNames, sectionName);
const listNames = listNamesBySection[sectionName] = [];
const listNamesOrdered = Object.keys(options[sectionName]);
if (autoSortListNames) {
listNamesOrdered.sort();
}
for (const listName of listNamesOrdered) {
addToListIfNotYet(listNames, listName);
}
listNamesBySectionInitial[sectionName] = listNames.slice();
}
const sectionNamesDefault = NAME_PARTS_ORDER.filter(
(sectionName) => sectionNames.includes(sectionName)
);
//* Forget initial order from the file:
if (autoSortSectionNames) {
sectionNames.sort();
} else
if (!isSectionOrderKeptAsGiven) {
arrayAssignValues(sectionNames, sectionNamesDefault);
}
//* Move names to front, in reversed order from given params:
for (
let paramIndex = orderParams.length;
paramIndex--;
) {
const sectionName = orderParams[paramIndex];
const orderIndex = sectionNames.indexOf(sectionName);
if (orderIndex > 0) {
arrayMoveValue(sectionNames, orderIndex, 0);
}
}
//* Remember resulting order as initial for reset:
const sectionNamesInitial = sectionNames.slice();
project.namePartsOrder = {
sectionNames,
sectionNamesDefault,
sectionNamesInitial,
listNamesBySection,
listNamesBySectionInitial,
};
//* Render default set when everything is ready:
const container = createProjectView(project);
createOptionsMenu(project, getAllByClass('project-options', container)[0] || container);
return container;
}
} catch (error) {
logError(error, arguments);
if (project.options) {
project.options = null;
delete project.options;
}
}
return null;
}
function getProjectOptions(project) {
function processUnskippedLayer(layer) {
if (layer.isSkipped) {
return;
}
function getOptionGroup(sectionName, listName) {
if (!isNonNullObject(options)) {
options = {};
}
sectionName = String(sectionName);
listName = String(listName);
const section = getOrInitChild(options, sectionName);
const optionGroup = getOrInitChild(section, listName, 'params', 'items');
return optionGroup;
}
function checkSwitchParams(globalOptionParams) {
for (const switchType in SWITCH_NAMES_BY_TYPE)
for (const switchName of SWITCH_NAMES_BY_TYPE[switchType])
if (params[switchName]) {
const switchParams = getOrInitChild(project, 'switchParamNames');
const switchParam = getOrInitChild(switchParams, switchType);
if (!switchParam.implicit) {
switchParam.implicit = getOtherSwitchParamName(switchType, switchName);
switchParam.explicit = switchName;
}
globalOptionParams[switchName] = true;
}
}
function checkMinMaxParams(params, optionParams, paramName) {
const paramMS = params[paramName];
if (isNonNullObject(paramMS)) {
const optionMS = optionParams[paramName];
if (isNonNullObject(optionMS)) {
if (optionMS.min > paramMS.min) optionMS.min = paramMS.min;
if (optionMS.max < paramMS.max) optionMS.max = paramMS.max;
} else {
optionParams[paramName] = {
'min' : paramMS.min
, 'max' : paramMS.max
};
}
}
}
function addOptionGroup(sectionName, listName) {
const optionGroup = getOptionGroup(sectionName, listName);
const optionParams = optionGroup.params;
checkSwitchParams(optionParams);
checkMinMaxParams(params, optionParams, 'multi_select');
for (const paramName of PARAM_KEYWORDS_SET_VALUE_TO_TRUE) {
if (params[paramName]) {
optionParams[paramName] = true;
}
}
return optionGroup;
}
function addOptionItem(layer, sectionName, listName, optionName) {
layer.isOption = true;
if (!layer.type) {
layer.type = sectionName.replace(regLayerTypeSingleTrim, '');
}
const optionGroup = getOptionGroup(sectionName, listName);
const optionParams = optionGroup.params;
const optionItems = optionGroup.items;
const optionItemLayers = getOrInitChild(optionItems, optionName, Array);
if (optionName !== '') {
for (const paramName of PARAM_KEYWORDS_SET_VALUE_TO_NAME) {
if (params[paramName]) {
optionParams[paramName] = optionName;
}
}
optionItemLayers.push(layer);
}
}
function addOptionsFromParam(sectionName, listName) {
function addOptionsFromParamKeywords(keywordsList, paramList) {
for (const optionValue of paramList) {
let optionName = String(optionValue);
if (isRealNumber(optionValue)) {
optionItems[optionName] = optionValue;
} else {
optionName = optionName.replace(regNonAlphaNum, '').toLowerCase();
if (PARAM_KEYWORDS_SHORTCUT_FOR_ALL.includes(optionName)) {
for (const optionName of keywordsList) {
optionItems[optionName] = optionName;
}
} else
if (keywordsList.includes(optionName)) {
optionItems[optionName] = optionName;
} else
if (getRGBAFromColorCodeOrName(optionValue)) {
optionItems[optionValue] = optionValue;
}
}
}
}
const param = params[sectionName];
if (!param) {
return;
}
if (!listName && sectionName === 'collage') {
for (const listName in param) {
addOptionsFromParam(sectionName, listName);
}
return;
}
const optionGroup = addOptionGroup(sectionName, listName || sectionName);
const optionParams = optionGroup.params;
const optionItems = optionGroup.items;
checkSwitchParams(optionParams);
if (sectionName === 'separate') {
if (!optionParams.totalCount) {
optionParams.totalCount = 0;
}
if (!isNonNullObject(param)) {
optionParams.useAutoRoot = true;
} else {
if (param.useAutoRoot) {
optionParams.useAutoRoot = true;
}
const naming = param.naming;
if (naming) {
optionParams.naming = naming;
}
const groupNames = param.groupNames;
if (isArray(groupNames)) {
for (const optionName of groupNames) {
let renderedLayers;
let renderedCount;
if (
layersInside
&& (renderedLayers = layer.layers.filter(isLayerRendered))
&& (renderedCount = renderedLayers.length) > 0
&& getLayerVisibilityChain(layer).every(isLayerRendered)
) {
const optionItemLayers = getOrInitChild(optionItems, optionName, Array);
if (addToListIfNotYet(optionItemLayers, layer)) {
optionParams.totalCount += renderedCount;
}
}
}
}
}
} else
if (sectionName === 'side') {
for (const optionName of VIEW_SIDES) {
optionItems[optionName] = getLocalizedText(sectionName + '_' + optionName);
}
const index = VIEW_SIDES.indexOf(param);
if (index >= 0) {
params[sectionName] = (
params.not
? VIEW_SIDES[index ? 0 : 1]
: param
);
layer.isOnlyForOneSide = true;
} else
if (params.if_only) {
params[sectionName] = VIEW_SIDES[params.not ? 0 : 1];
layer.isOnlyForOneSide = true;
} else {
layer.isOrderedBySide = true;
}
} else
if (sectionName === 'paddings') {
const paddings = params['radius'];
if (isArray(paddings)) {
for (let padding of paddings) {
if (isString(padding)) {
padding = JSON.parse(padding);
}
const { method, threshold, dimensions } = padding;
const isBox = ('x' in dimensions);
const [ openBracket, closeBracket ] = (isBox ? '[]' : '()');
const optionNameParts = [
(
isBox
? [dimensions.x, dimensions.y]
: [dimensions.radius]
).map(
(interval) => (
'in' in interval
? (
openBracket
+ interval.in
+ '..'
+ interval.out
+ closeBracket
)
: interval.out
)
).join('x') + 'px'
, (
!threshold
? 'a > ' + threshold
: ''
)
, (
method
&& PARAM_KEYWORDS_PADDING_METHODS.includes(method)
? method
: ''
)
];
const optionName = (
optionNameParts
.filter(arrayFilterNonEmptyValues)
.join(', ')
);
optionItems[optionName] = padding;
}
}
layer.isMaskGenerated = true;
} else
if (sectionName === 'autocrop') {
addOptionsFromParamKeywords(PARAM_KEYWORDS_AUTOCROP, param);
} else
if (sectionName === 'collage') {
if (listName === 'align') {
addOptionsFromParamKeywords(PARAM_KEYWORDS_COLLAGE_ALIGN, param[listName]);
} else
if (isArray(param[listName])) {
for (const optionValue of param[listName]) {
const optionName = String(optionValue);
optionItems[optionName] = optionValue;
}
}
} else
if (sectionName === 'zoom' || sectionName === 'opacities') {
const format = param.format;
if (format) {
optionParams.format = format;
}
const values = param.values;
if (isArray(values)) {
for (const optionValue of values) {
//* Pad bare numbers to avoid numeric autosorting in