From df4a678946e2599ddee247667925e6ca325c3794 Mon Sep 17 00:00:00 2001 From: vvo <> Date: Wed, 15 Nov 2023 20:10:27 +0800 Subject: [PATCH] webui stuff --- sdvx@asphyxia/README.md | 97 +++--- sdvx@asphyxia/bgm_convert.bat | 3 +- sdvx@asphyxia/bgm_convert.sh | 1 + sdvx@asphyxia/data/exg.ts | 4 +- sdvx@asphyxia/data/webui.ts | 74 +++++ sdvx@asphyxia/handlers/webui.ts | 138 +++++++- sdvx@asphyxia/webui/asset/js/detail.js | 110 ++++--- sdvx@asphyxia/webui/asset/js/generator.js | 7 +- .../webui/asset/js/pngjs/bitmapper.js | 267 ++++++++++++++++ .../webui/asset/js/pngjs/bitpacker.js | 158 ++++++++++ .../webui/asset/js/pngjs/chunkstream.js | 189 +++++++++++ .../webui/asset/js/pngjs/constants.js | 32 ++ sdvx@asphyxia/webui/asset/js/pngjs/crc.js | 40 +++ .../webui/asset/js/pngjs/filter-pack.js | 171 ++++++++++ .../asset/js/pngjs/filter-parse-async.js | 24 ++ .../webui/asset/js/pngjs/filter-parse-sync.js | 21 ++ .../webui/asset/js/pngjs/filter-parse.js | 177 +++++++++++ .../webui/asset/js/pngjs/format-normaliser.js | 93 ++++++ .../webui/asset/js/pngjs/interlace.js | 95 ++++++ .../webui/asset/js/pngjs/packer-async.js | 50 +++ .../webui/asset/js/pngjs/packer-sync.js | 56 ++++ sdvx@asphyxia/webui/asset/js/pngjs/packer.js | 129 ++++++++ .../webui/asset/js/pngjs/paeth-predictor.js | 16 + .../webui/asset/js/pngjs/parser-async.js | 169 ++++++++++ .../webui/asset/js/pngjs/parser-sync.js | 112 +++++++ sdvx@asphyxia/webui/asset/js/pngjs/parser.js | 295 ++++++++++++++++++ .../webui/asset/js/pngjs/png-sync.js | 12 + sdvx@asphyxia/webui/asset/js/pngjs/png.js | 194 ++++++++++++ .../webui/asset/js/pngjs/sync-inflate.js | 168 ++++++++++ .../webui/asset/js/pngjs/sync-reader.js | 45 +++ sdvx@asphyxia/webui/asset/js/preview.js | 2 +- .../webui/asset/js/updateResources.js | 16 +- .../webui/asset/json/customize_data_ext.json | 57 ++-- .../webui/asset/json/valgene_data.json | 7 + sdvx@asphyxia/webui/profile_--detail.pug | 2 + sdvx@asphyxia/webui/update webui assets.pug | 25 +- 36 files changed, 2913 insertions(+), 143 deletions(-) create mode 100644 sdvx@asphyxia/bgm_convert.sh create mode 100644 sdvx@asphyxia/data/webui.ts create mode 100644 sdvx@asphyxia/webui/asset/js/pngjs/bitmapper.js create mode 100644 sdvx@asphyxia/webui/asset/js/pngjs/bitpacker.js create mode 100644 sdvx@asphyxia/webui/asset/js/pngjs/chunkstream.js create mode 100644 sdvx@asphyxia/webui/asset/js/pngjs/constants.js create mode 100644 sdvx@asphyxia/webui/asset/js/pngjs/crc.js create mode 100644 sdvx@asphyxia/webui/asset/js/pngjs/filter-pack.js create mode 100644 sdvx@asphyxia/webui/asset/js/pngjs/filter-parse-async.js create mode 100644 sdvx@asphyxia/webui/asset/js/pngjs/filter-parse-sync.js create mode 100644 sdvx@asphyxia/webui/asset/js/pngjs/filter-parse.js create mode 100644 sdvx@asphyxia/webui/asset/js/pngjs/format-normaliser.js create mode 100644 sdvx@asphyxia/webui/asset/js/pngjs/interlace.js create mode 100644 sdvx@asphyxia/webui/asset/js/pngjs/packer-async.js create mode 100644 sdvx@asphyxia/webui/asset/js/pngjs/packer-sync.js create mode 100644 sdvx@asphyxia/webui/asset/js/pngjs/packer.js create mode 100644 sdvx@asphyxia/webui/asset/js/pngjs/paeth-predictor.js create mode 100644 sdvx@asphyxia/webui/asset/js/pngjs/parser-async.js create mode 100644 sdvx@asphyxia/webui/asset/js/pngjs/parser-sync.js create mode 100644 sdvx@asphyxia/webui/asset/js/pngjs/parser.js create mode 100644 sdvx@asphyxia/webui/asset/js/pngjs/png-sync.js create mode 100644 sdvx@asphyxia/webui/asset/js/pngjs/png.js create mode 100644 sdvx@asphyxia/webui/asset/js/pngjs/sync-inflate.js create mode 100644 sdvx@asphyxia/webui/asset/js/pngjs/sync-reader.js diff --git a/sdvx@asphyxia/README.md b/sdvx@asphyxia/README.md index 418d66a..454a4e7 100644 --- a/sdvx@asphyxia/README.md +++ b/sdvx@asphyxia/README.md @@ -1,26 +1,52 @@ # SOUND VOLTEX -**Plugin Version:** fork-6.0.2.3 +**Plugin Version:** fork-6.0.2.4 -**Supported game versions:** EXCEED GEAR - -**Latest supported game datacode:** 2023102400 +**Supported game versions:** EXCEED GEAR (2023102400) **Unsupported game versions:** Every other game, as they're untested. **Required Asphyxia Core version** [1.50c](https://github.com/asphyxia-core/asphyxia-core.github.io/releases/tag/v1.50) **Notes:** -- This fork enables a few of the features that are added in the later updates of Exceed Gear. However, I still highly recommend using the official stable version of the plugin. -- Before using this plugin: - - run the [WebUI Asset Update](/plugin/sdvx@asphyxia/update%20webui%20assets). Do this every data update. - - go to [Unlocking Events](/plugin/sdvx@asphyxia/unlocking%20events) and click "Apply" at least once. Check out the many options available while you're there. - - also adjust the plugin settings at the WebUI dashboard to your liking. -- Don't expect stability, there might be some bugs. Please back up your save data to prevent unwanted issues (just in case) +- Please be aware that this plugin and the savefile it produces are incompatible with the official plugin, or any other forks. Please be cautious of using other plugins' savefiles with this fork of the plugin, or using this fork's savefile with other plugins. +- Before using this plugin, run the [WebUI Asset Update](/plugin/sdvx@asphyxia/update%20webui%20assets). Do this every data and plugin update. +- Please back up your save data to prevent unwanted issues (just in case) Changelog =========== +## fork-6.0.2.4 + +### New: + +1. Main: + - Updated EG course data (isNew flags) +2. WebUI: + - Profile details + - Added display of skill frames (normal or god.) Asset update required. + - frame might misalign a bit depending on window size, sorry + - Added display of arena rank image. Asset update required. + - Added display of your currently selected skill title. + - Changed how Megamix rate and Ultimate rate is displayed in arena stats. + - Asset update + - Added ability to extract images/textures from .ifs files, with file information provided (seen on data/webui.ts.) Used to extract arena/skill course-related textures. Will probably utilize more in the future. Only ifs files used in latest data are supported. + - Massive thanks to [mon's ifstools](https://github.com/mon/ifstools), the code where this feature is largely based on. + - Added [pngjs library files](https://github.com/pngjs/pngjs) to handle PNG file creation. Couldn't find a more elegant way but this will do for now. + - Added course_data.json update using the course data in exg.ts. +3. Other: + - Added bgm_convert.sh file for Linux + - Updated bgm_convert.bat + +### Fixes: +1. WebUI + - Fixed Japan-exclusive warning not displaying for some Premium Generator sets. + +### Todo: +1. Figure out how to use image (png) files to appear in information/news popup. +2. More work on online matchmaking (globalMatch.) I can't seem to make clients establish a connection. + + ## fork-6.0.2.3 ### New: @@ -37,7 +63,7 @@ Changelog - (Update webui resources) Added code to extract wma audio from .s3p files. Largely based on [mon's s3p_extract](https://github.com/mon/s3p_extract) code. 3. Others: - DEMOLOOP_INFORMATION implementation (attract mode announcements images) - - Updated bgm_convert batch script to remove the need for s3p_extract. + - Updated bgm\_convert batch script to remove the need for s3p\_extract. ### Fixes: @@ -50,12 +76,6 @@ Changelog - Updated BPL Triple Tribe event category from 'gift' to 'event_online.' - Added missing matchmaker model file. -### Todo: -1. Figure out how to extract textures from .ifs files in TS/JS. -2. ~~Figure out how to extract audio files from .s3p files in TS/JS~~ Figure out how to convert wma audio to mp3 in JS/TS. -3. Figure out how to use image (png) files to appear in information/news popup. -4. More work/research on online matchmaking (globalMatch.) I can't seem to make clients establish a connection. - ## fork-6.0.2.2 @@ -84,46 +104,3 @@ Changelog 1. Fixes WebUI resource update not adding video submonitor backgrounds. I 100% recommend updating your WebUI assets. - -## fork-6.0.2.1 - -### Fixes: - -1. Fixed missing Megamix songs (extend string param getting cut off) - - -## fork-6.0.2.0 - -### New: - -1. Support for EG 2023091200 -2. Added Arena Station 8, 9 and 10. -3. Added Valkyrie Generator Vol. 12 -4. Added Premium Generator - - Re:Zero Set 1 - - Re:Zero Set 2 - - Vol. 3 -5. Enabled HEXADIVER 8 and 9 -6. Added option for BPL Team Supporter banner -7. Added support for System Backgrounds -8. Added event data: - - BPL S3 Triple Tribe songs unlock toggle (suspicions, etc.) - - KAC 2023 song unlock toggle (パーフェクトイーター) - - KAC 2023 song stamp event (for 累乗のカルマ) - - KAC 2023 song stamp event (for Stylus & QQ) -9. Updated licensed songs and valk-exclusive songs list -10. Skill Analyzer God Mode implementation. - - Special courses (Red Bull courses, etc.) do not have God Mode courses. -11. Added and fixed Skill Analyzer course data: - - Sets 6 and 7 - - BMK2021 courses - - Spring 2023 courses - - KAC 2023 Qualifier course sets 1 & 2 -12. Megamix Battle song list update - -### Fixes: - -1. Reduced songNum value from 3000 to 2100. Potential fix for some users with "unlock all songs" enabled who are experiencing request timeouts resulting in missing features and songs. -2. Fixed bug in stamp select data. -3. Fixed bug for when common.ts or profiles.ts is looking for a specific event key but it is undefined. -3. Skill Analyzer: skill level requirement for Lv.11 and ∞. Song input/unlock status must be met as well, or just use the unlock all songs option in the WebUI. diff --git a/sdvx@asphyxia/bgm_convert.bat b/sdvx@asphyxia/bgm_convert.bat index b70e3ac..89fec8d 100644 --- a/sdvx@asphyxia/bgm_convert.bat +++ b/sdvx@asphyxia/bgm_convert.bat @@ -1,6 +1,7 @@ @echo off for /f %%a in ('dir "webui\asset\audio\" /b /s /a-d ^| findstr ".wma"') do ( - ffmpeg -i "%%~fa" -vn -ar 44100 -ac 2 -b:a 192k "%%~dpa%%~na.mp3" + echo Converting %%~fa + ffmpeg -loglevel error -y -i "%%~fa" -vn -ar 44100 -ac 2 -b:a 192k "%%~dpa%%~na.mp3" del %%~fa ) @echo on \ No newline at end of file diff --git a/sdvx@asphyxia/bgm_convert.sh b/sdvx@asphyxia/bgm_convert.sh new file mode 100644 index 0000000..e1ac959 --- /dev/null +++ b/sdvx@asphyxia/bgm_convert.sh @@ -0,0 +1 @@ +find . -type f -name "*.wma" -exec bash -c 'echo "Converting $1" && ffmpeg -loglevel error -y -i $1 -vn -ar 44100 -ac 2 -b:a 192k ${1%.wma}.mp3 && rm $1' bash {} \; diff --git a/sdvx@asphyxia/data/exg.ts b/sdvx@asphyxia/data/exg.ts index 6d8cbbc..314f2ae 100644 --- a/sdvx@asphyxia/data/exg.ts +++ b/sdvx@asphyxia/data/exg.ts @@ -5187,7 +5187,7 @@ export const COURSES6 = [ { id: 14, name: 'SKILL ANALYZER 第6回', - isNew: 1, + isNew: 0, hasGod: 1, courses: [ { @@ -5602,7 +5602,7 @@ export const COURSES6 = [ { id: 16, name: 'KAC(2023)予選コース', - isNew: 1, + isNew: 0, courses: [ { id: 1, diff --git a/sdvx@asphyxia/data/webui.ts b/sdvx@asphyxia/data/webui.ts new file mode 100644 index 0000000..467a529 --- /dev/null +++ b/sdvx@asphyxia/data/webui.ts @@ -0,0 +1,74 @@ +export const textureslist = [ + { + 'file': '/data/graphics/ver06/arena_rank.ifs', + 'asset_folder': 'arena_rank', + 'md5': '0311d56e5db4fe276aaab4435cb52354', + 'textures': [ + ['glow.png', 2890640, 16288, [2, 570, 2, 106], [0, 572, 0, 108]], + ['mixstar.png', 2712288, 48206, [2, 542, 110, 650], [0, 544, 108, 652]], + ['u1.png', 457712, 149908, [574, 1114, 2, 542], [572, 1116, 0, 544]], + ['u2.png', 153136, 152586, [1118, 1658, 2, 542], [1116, 1660, 0, 544]], + ['u3.png', 0, 153136, [546, 1086, 546, 1086], [544, 1088, 544, 1088]], + ['u4.png', 305728, 151974, [1090, 1630, 546, 1086], [1088, 1632, 544, 1088]], + ['u.png', 607632, 147016, [2, 542, 654, 1194], [0, 544, 652, 1196]], + ['s1.png', 1067584, 102100, [546, 1086, 1090, 1630], [544, 1088, 1088, 1632]], + ['s2.png', 860160, 103886, [1090, 1630, 1090, 1630], [1088, 1632, 1088, 1632]], + ['s3.png', 754656, 105494, [2, 542, 1198, 1738], [0, 544, 1196, 1740]], + ['s4.png', 964048, 103534, [1662, 2202, 2, 542], [1660, 2204, 0, 544]], + ['a1.png', 1856768, 93336, [2206, 2746, 2, 542], [2204, 2748, 0, 544]], + ['a2.png', 1666144, 96328, [2750, 3290, 2, 542], [2748, 3292, 0, 544]], + ['a3.png', 1568784, 97346, [3294, 3834, 2, 542], [3292, 3836, 0, 544]], + ['a4.png', 1762480, 94284, [1634, 2174, 546, 1086], [1632, 2176, 544, 1088]], + ['b1.png', 2484560, 86781, [2178, 2718, 546, 1086], [2176, 2720, 544, 1088]], + ['b2.png', 2220768, 88790, [2722, 3262, 546, 1086], [2720, 3264, 544, 1088]], + ['b3.png', 2041344, 90088, [3266, 3806, 546, 1086], [3264, 3808, 544, 1088]], + ['b4.png', 2309568, 87663, [1634, 2174, 1090, 1630], [1632, 2176, 1088, 1632]], + ['c1.png', 1470944, 97837, [2178, 2718, 1090, 1630], [2176, 2720, 1088, 1632]], + ['c2.png', 1271488, 100109, [2722, 3262, 1090, 1630], [2720, 3264, 1088, 1632]], + ['c3.png', 1169696, 101777, [3266, 3806, 1090, 1630], [3264, 3808, 1088, 1632]], + ['c4.png', 1371600, 99342, [546, 1086, 1634, 2174], [544, 1088, 1632, 2176]], + ['d1.png', 2571344, 85426, [1090, 1630, 1634, 2174], [1088, 1632, 1632, 2176]], + ['d2.png', 2131440, 89314, [1634, 2174, 1634, 2174], [1632, 2176, 1632, 2176]], + ['d3.png', 1950112, 91226, [2178, 2718, 1634, 2174], [2176, 2720, 1632, 2176]], + ['d4.png', 2397232, 87321, [2722, 3262, 1634, 2174], [2720, 3264, 1632, 2176]], + ['none.png', 2656784, 55490, [3266, 3806, 1634, 2174], [3264, 3808, 1632, 2176]], + ['fudawinstat.png', 2922368, 9846, [2, 514, 1742, 1826], [0, 516, 1740, 1828]] + ] + }, + { + 'file': '/data/graphics/ver06/psd_level.ifs', + 'asset_folder': 'difficulty', + 'md5': '26ef89b4550cac6156fa78ef4e59e8c3', + 'textures': [ + ['level_small_xcd.png', 193824, 2181, [710, 922, 2, 50], [708, 924, 0, 52]] + ] + }, + { + 'file': '/data/graphics/ver06/psd_skill.ifs', + 'asset_folder': 'skill_lv', + 'md5': '9a5684960c0d86cd18f1c64f6e13c98b', + 'textures': [ + ['skill_frame_normal_sp.png', 549552, 45470, [2, 602, 242, 582], [0, 604, 240, 584]], + ['skill_frame_normal_silver.png', 884448, 32373, [2, 602, 586, 926], [0, 604, 584, 928]], + ['skill_frame_normal_gold.png', 850288, 34158, [610, 1210, 2, 342], [608, 1212, 0, 344]], + ['skill_frame_god_sp_none.png', 595024, 40145, [1214, 1814, 2, 342], [1212, 1816, 0, 344]], + ['skill_frame_god_sp.png', 195312, 80745, [606, 1206, 346, 686], [604, 1208, 344, 688]], + ['skill_frame_god_silver.png', 346064, 53343, [1210, 1810, 346, 686], [1208, 1812, 344, 688]], + ['skill_frame_god_gold.png', 276064, 69987, [606, 1206, 690, 1030], [604, 1208, 688, 1032]], + ['skill_none.png', 1227792, 10209, [2, 454, 930, 1058], [0, 456, 928, 1060]], + ['skill_12.png', 1012656, 20206, [458, 910, 1034, 1162], [456, 912, 1032, 1164]], + ['skill_11.png', 1050144, 14880, [914, 1366, 1034, 1162], [912, 1368, 1032, 1164]], + ['skill_10.png', 1156848, 12906, [1370, 1822, 1034, 1162], [1368, 1824, 1032, 1164]], + ['skill_09.png', 1169760, 12873, [2, 454, 1062, 1190], [0, 456, 1060, 1192]], + ['skill_08.png', 1091424, 13181, [458, 910, 1166, 1294], [456, 912, 1164, 1296]], + ['skill_07.png', 1065024, 13193, [914, 1366, 1166, 1294], [912, 1368, 1164, 1296]], + ['skill_06.png', 1078224, 13189, [1370, 1822, 1166, 1294], [1368, 1824, 1164, 1296]], + ['skill_05.png', 1104608, 13144, [2, 454, 1194, 1322], [0, 456, 1192, 1324]], + ['skill_04.png', 1182640, 12353, [458, 910, 1298, 1426], [456, 912, 1296, 1428]], + ['skill_03.png', 1117760, 13079, [914, 1366, 1298, 1426], [912, 1368, 1296, 1428]], + ['skill_02.png', 1130848, 13045, [1370, 1822, 1298, 1426], [1368, 1824, 1296, 1428]], + ['skill_01.png', 1143904, 12943, [2, 454, 1326, 1454], [0, 456, 1324, 1456]], + ['skill_00.png', 1217568, 10211, [458, 910, 1430, 1558], [456, 912, 1428, 1560]] + ] + } +] diff --git a/sdvx@asphyxia/handlers/webui.ts b/sdvx@asphyxia/handlers/webui.ts index 5e96c7c..e448f3f 100644 --- a/sdvx@asphyxia/handlers/webui.ts +++ b/sdvx@asphyxia/handlers/webui.ts @@ -4,7 +4,10 @@ import { ValgeneTicket } from '../models/valgene_ticket'; import { Skill } from '../models/skill'; import { getVersion, IDToCode, GetCounter } from '../utils'; import { Mix } from '../models/mix'; -import { PREGENE } from '../data/exg'; +import { PREGENE, COURSES6} from '../data/exg'; +import { textureslist } from '../data/webui' +import * as fs from 'fs'; +import { PNG } from '../webui/asset/js/pngjs/png.js'; export const updateProfile = async (data: { refid: string; @@ -363,7 +366,7 @@ export const copyResourcesFromGame = async (data: {}, send: WebUISend) => { if(nemsys.name.match(/([0-9]+)/g) != undefined) { let nemsysId = parseInt(nemsys.name.match(/([0-9]+)/g)[0]) - if(nemsysId && resourceJsonData.nemsys.find(nem => nem.value == nemsysId) == undefined) { + if(nemsysId && ![8, 9, 10, 11].includes(nemsysId) && resourceJsonData.nemsys.find(nem => nem.value == nemsysId) == undefined) { console.log("[nemsys] adding to json: " + nemsys.name) resourceJsonData.nemsys.push({"value": nemsysId, "name": nemsys.name}) } @@ -551,9 +554,140 @@ export const copyResourcesFromGame = async (data: {}, send: WebUISend) => { await IO.WriteFile('webui/asset/json/data.json', JSON.stringify(resourceJsonData, null, 4)) await IO.WriteFile('webui/asset/json/appeal.json', JSON.stringify(apCardJsonData, null, 4)) + + // Extract textures from ifs files using pngjs. Massive thanks to https://github.com/mon/ifstools + console.log("Extracting textures from IFS files") + let ifsSuccess = [] + for(let listIter = 0; listIter < textureslist.length; listIter++) { + let manifestJson = {} + let bufOffset = 0 + let magic = '6CAD8F89' + console.log(textureslist[listIter]['file'] + ":") + let ifsBuffer = await IO.ReadFile(U.GetConfig('sdvx_eg_root_dir') + textureslist[listIter].file, {flag: 'r'}) + if(!fs.existsSync('plugins/sdvx@asphyxia/webui/asset/' + textureslist[listIter].asset_folder)) { + fs.mkdirSync('plugins/sdvx@asphyxia/webui/asset/' + textureslist[listIter].asset_folder) + } + let header = Buffer.from(ifsBuffer.buffer.slice(0, 36)) + let sig = header.readUInt32BE().toString(16).toUpperCase() + bufOffset += 4 + if(sig === magic) { + bufOffset += 12 + let manifest_end = ifsBuffer.readUInt32BE(bufOffset) + bufOffset += 4 + let md5_hash = ifsBuffer.toString('hex', bufOffset, bufOffset + 16) + bufOffset += 16 + if(md5_hash === textureslist[listIter].md5) { + for(let texIter = 0; texIter < textureslist[listIter]['textures'].length; texIter++) { + let tdFileName = textureslist[listIter]['textures'][texIter][0] + let tdOffset = parseInt(textureslist[listIter]['textures'][texIter][1].toString()) + let tdSize = parseInt(textureslist[listIter]['textures'][texIter][2].toString()) + let tdUvrect = textureslist[listIter]['textures'][texIter][3] + let tdImgRect = textureslist[listIter]['textures'][texIter][4] + + let imgBufferHead = Buffer.from(ifsBuffer.buffer.slice(manifest_end + tdOffset, manifest_end + tdOffset + 8)) + let imgBufferHeadOff = 0 + let imgBuffer = Buffer.from(ifsBuffer.buffer.slice(manifest_end + tdOffset + 8, manifest_end + tdOffset + tdSize)) + let imgBufferOff = 0 + let imgUncompressedSize = imgBufferHead.readUInt32BE(imgBufferHeadOff) + imgBufferHeadOff += 4 + let imgCompressedSize = imgBufferHead.readUInt32BE(imgBufferHeadOff) + imgBufferHeadOff += 4 + + // Decompression algorithm & code from ifstools/handlers/lz77.py + let decompressed = [] + let contLoop = true + let diff, flag, w, position, length + while(contLoop) { + flag = imgBuffer.readUInt8(imgBufferOff) + imgBufferOff++ + for(let i = 0; i < 8; i++){ + if (((flag >> i) & 1) === 1) { + decompressed.push(imgBuffer.readUInt8(imgBufferOff)) + imgBufferOff++ + } + else { + w = imgBuffer.readUInt16BE(imgBufferOff) + imgBufferOff+=2 + position = (w >> 4) + length = (w & 0x0F) + 3 + if(position === 0) { + contLoop = false + break; + } + if(position > decompressed.length) { + diff = 0 + diff = position - decompressed.length + diff = Math.min(diff, length) + for(let e2p = 0; e2p < diff; e2p++) { + decompressed.push(0) + } + length -= diff + } + if (-position+length < 0) { + decompressed.push(...decompressed.slice(decompressed.length + (-position), decompressed.length + (-position+length))) + } + else { + for(let loop = 0; loop < length; loop++) { + decompressed.push(decompressed[decompressed.length + (-position)]) + } + } + } + } + } + + // Swap red and blue data (RGBA -> BGRA) + if(decompressed.length === imgUncompressedSize) { + for(let decCtr = 0; decCtr < decompressed.length; decCtr += 4) { + decompressed[decCtr + 2] = [decompressed[decCtr], decompressed[decCtr] = decompressed[decCtr + 2]][0]; + } + + let pngf = new PNG({ + width: Math.floor(tdImgRect[1]/2) - Math.floor(tdImgRect[0]/2), + height: Math.floor(tdImgRect[3]/2) - Math.floor(tdImgRect[2]/2), + bitDepth: 8, + colorType: 6, + inputHasAlpha: true + }) + + pngf.data = Buffer.from(decompressed) + const outputStream = await fs.createWriteStream('plugins/sdvx@asphyxia/webui/asset/' + textureslist[listIter].asset_folder + '/' + tdFileName); + await pngf.pack().pipe(outputStream); + console.log(' - ' + tdFileName + ' created successfully.'); + ifsSuccess.push(textureslist[listIter].file + ' - ' + tdFileName) + } else { + console.log("Decompression mismatch.") + runErrors.push('[ifs] decompression mismatch for ' + textureslist[listIter]['file'] + '/' + tdFileName) + } + } + } else { + console.log('MD5 mismatch.') + runErrors.push('[ifs] MD5 mismatch - ' + textureslist[listIter].file) + } + + } else { + console.log('IFS file unsupported/invalid.') + runErrors.push('[ifs] IFS file "' + textureslist[listIter].file + '" unsupported/invalid.') + } + } + + console.log("Updating course_data.json") + let courseDataUpdateSuccess = false + let courseData = JSON.parse(U.DecodeString(await IO.ReadFile('webui/asset/json/course_data.json'), 'utf8')) + for(let cIter = 0; cIter < courseData.courseData.length; cIter++) { + if(courseData.courseData[cIter].version === 6) { + courseData.courseData[cIter].info = COURSES6 + courseDataUpdateSuccess = true + } + } + if(courseDataUpdateSuccess === false) runErrors.push('[course_data] Update unsuccessful.') + + await IO.WriteFile('webui/asset/json/course_data.json', JSON.stringify(courseData, null, 4)) + send.json( { status: 'ok', + course: courseDataUpdateSuccess, + ifs: ifsSuccess, akaname: newAkanames, nemsys: newNemsysData, apCard: newAPCardData, diff --git a/sdvx@asphyxia/webui/asset/js/detail.js b/sdvx@asphyxia/webui/asset/js/detail.js index dc9a467..563d93d 100644 --- a/sdvx@asphyxia/webui/asset/js/detail.js +++ b/sdvx@asphyxia/webui/asset/js/detail.js @@ -1,6 +1,6 @@ -var music_db, course_db, score_db, data_db, appeal_db; +var music_db, course_db, score_db, data_db, appeal_db, skill_title_db; var volforceArray = []; -var profile_data, skill_data; +var profile_data, skill_data, course_data; var baseTBodyCMpD, baseTBodyCMpL, baseTBodyGpD, baseTBodyGpL, baseTBodyASpL; var notFirst = false; var versionText = ['', 'BOOTH', 'INFINTE INFECTION', 'GRAVITY WARS', 'HEAVENLY HAVEN', 'VIVIDWAVE', 'EXCEED GEAR'] @@ -24,7 +24,16 @@ function zeroPad(num, places) { function getSkillAsset(skill) { - return "static/asset/skill_lv/skill_" + zeroPad(skill, 2) + ".png"; + return "static/asset/skill_lv/skill_" + ((skill === -1) ? 'none' : zeroPad(skill, 2)) + ".png"; +} + +function getSkillFrameAsset(frame) { + return "static/asset/skill_lv/skill_frame_" + frame + ".png"; +} + +function getSkillTitle() { + if(skill_data.length <= 0 || skill_data[0].name === (undefined || 0)) return '' + return skill_title_db.filter(e => e.id === skill_data[0].name)[0].name } function getGrade(grade) { @@ -634,18 +643,47 @@ function setUpStatistics() { $('#version_select').change(function() { $('#skillLV').fadeOut(200, () => { console.log("change version select"); - $('#skillLV').attr('src', getSkillAsset(getPlayerSkill($('#version_select').val()))) + $('#skillLV').attr('src', getSkillAsset(getPlayerSkill($('#version_select').val())[0])) }); $('#skillLV').fadeIn(200); }); function getPlayerSkill(version) { // console.log(getPlayerMaxVersion()) - if (skill_data.length == 0) return 0; + if (skill_data.length == 0) return [-1, 0]; var k = skill_data.filter(e => e.version == version) - return parseInt(k[0].level); + let courseDataFil = course_data.filter(e => ![6,7,12,13,15,16].includes(e.sid) && e.cid === k[0].level && ((k[0].type !== undefined) ? k[0].type : 0) === ((e.stype !== undefined) ? e.stype : 0)) + return [(courseDataFil.length > 0) ? k[0].level : -1, (k[0].type !== undefined) ? k[0].type : 0]; } +function getPlayerCourse(version, playerSkill) { + if(version === 0) return 'none' + let skillFrame = ['normal', 'god'] + let skillThrsh = [[130, 150, 160, 170], [150, 160, 170, 180]] + let skillThrshVal = (playerSkill[0] <= 9) ? 0 : playerSkill[0] - 9 + var k = course_db.courseData.filter(e => e.version == version)[0].info + let sidCourses = [] + k.forEach(kd => { + kd.courses.forEach(kdc => { + kdc['sid'] = kd.id + kdc['isNew'] = kd.isNew + sidCourses.push(kdc) + }) + }) + sidCourses = sidCourses.filter(e => ![6,7,12,13,15,16].includes(e.sid) && e.level === playerSkill[0]) + let newCourses = sidCourses.filter(e => e.isNew === 1).map(a => a.sid) + let foundCourses = course_data.filter(e => e.cid === playerSkill[0] && ((e.stype !== undefined) ? e.stype : 0) === playerSkill[1] && e.clear >= 2) + let newCompleteCourses = foundCourses.filter(e => newCourses.includes(e.sid)) + let thrshCourses = foundCourses.filter(e => Math.floor(e.rate/100) >= skillThrsh[playerSkill[1]][skillThrshVal]) + + if(sidCourses.length > 0 && thrshCourses.length === sidCourses.length) return skillFrame[playerSkill[1]] + '_sp' + if(sidCourses.length > 0 && foundCourses.length === sidCourses.length) return skillFrame[playerSkill[1]] + '_gold' + if(newCompleteCourses.length > 1) return skillFrame[playerSkill[1]] + '_silver' + if(foundCourses.length > 1 && playerSkill[1] === 1) return skillFrame[playerSkill[1]] + '_sp_none' + return 'none' +} + + function getVersionSelect() { if (skill_data.length == 0) return []; var versionDATA = []; @@ -662,10 +700,10 @@ function displayArenaSeasonData(season) { $('.arena-details').append( $('
').append( $('
').append( - // $('', { - // src: 'static/asset/arena_icon/' + rankInfo[0] + ".png" - // }) - $('

' + ((rankInfo[0] === 'none') ? 'n/a' : rankInfo[0].toUpperCase()) + "

") + $('', { + src: 'static/asset/arena_rank/' + rankInfo[0] + ".png" + }) + // $('

' + ((rankInfo[0] === 'none') ? 'n/a' : rankInfo[0].toUpperCase()) + "

") ) ) ).append( @@ -674,10 +712,6 @@ function displayArenaSeasonData(season) { $('

Arena Power

').append( $('
' + sznData.shopPoint + ' AP
') ) - ).append( - (sznData.megamixRate !== (0 || undefined)) ? $('

Megamix Rate

').append( - $('
' + sznData.megamixRate + '
') - ) : $("

") ) ) ) @@ -695,12 +729,12 @@ function displayArenaSeasonData(season) { $('') ).append( $('
' + sznData.rankPoint + ' pts (' + (rankInfo[2].point - sznData.rankPoint) + ' pts to ' + rankInfo[2].rank.toUpperCase() + ')
') + ).append( + (sznData.megamixRate !== (0 || undefined)) ? $('
Megamix rate: ' + sznData.megamixRate + '

') : $('
') ) } else if(rankInfo[0] !== 'none'){ $('.arena-details-child-left').append( - $('
Ultimate rate: ' + sznData.ultimateRate + '
') - ).append( - + (sznData.megamixRate !== (0 || undefined)) ? $('
Megamix rate: ' + sznData.megamixRate + '

') : $('
Ultimate rate: ' + sznData.ultimateRate + '
') ) } } @@ -754,6 +788,7 @@ $(document).ready(function() { profile_data = JSON.parse(document.getElementById("data-pass").innerText); score_db = JSON.parse(document.getElementById("score-pass").innerText); skill_data = JSON.parse(document.getElementById("skill-pass").innerText); + course_data = JSON.parse(document.getElementById("course-pass").innerText); arena_data = JSON.parse(document.getElementById("arena-pass").innerText); skill_data.sort(function(a, b) { @@ -782,7 +817,10 @@ $(document).ready(function() { $.getJSON("static/asset/json/appeal.json", function(json) { appeal_db = json; //console.log(appeal_db); - }) + }), + $.getJSON("static/asset/json/customize_data_ext.json", function(json) { + skill_title_db = json.skilltitle; + }), ).then(function() { var currentVF = calculateVolforce(); var maxVer = skill_data.length > 0 ? parseInt(skill_data[0]["version"]) : 0 @@ -809,7 +847,7 @@ $(document).ready(function() { } $('#test').append( - $('
').append( + $('
').append( $('
').append( $('

').append( $('').append( @@ -873,32 +911,27 @@ $(document).ready(function() { ).css('width', '100%') ).css('vertical-align', 'top') .css('max-width', '100%') - ).append( - $('

').append( - $('
').append( - $('

').append( - $('').append( - $('') - ) - ).append("Other Data") - ) - ).append( + .append( $('

').append( $('
').append( $('
').append( - $('
').append( - $('

').append( - "Skill Level" + $('

').append( + $('
').append( + $('').attr('src', getSkillAsset(getPlayerSkill(maxVer)[0])) ).append( - $('
').append( - $('').attr('src', getSkillAsset(getPlayerSkill(maxVer))) + $('
').append( + $('') ) - ).css('font-family', "testfont") + ).append( + $('
').append( + $('

') + ) + ) ) ) ).append( $('
').append( - $('
').append( + $('
').append( $('

').append( "PCB" ).append( @@ -1031,6 +1064,11 @@ $(document).ready(function() { displayArenaSeasonData($('#arena-szn-sel').val()) }); + let skillFrame = getPlayerCourse(maxVer, getPlayerSkill(maxVer)) + if(skillFrame !== 'none') $('#skillFrame').attr('src', getSkillFrameAsset(skillFrame)) + $('#skillTitle').text(getSkillTitle()) + $('#skillTitle').attr('style', 'font-size:25px; color:black; font-weight:bold;color:' + (getPlayerSkill(maxVer)[0] === 11 ? '#FFC100' : ((getPlayerSkill(maxVer)[0] === 12) ? '#FFE000' : "black"))) + setUpStatistics(); setCMpD(); displayArenaSeasonData($('#arena-szn-sel').val()) diff --git a/sdvx@asphyxia/webui/asset/js/generator.js b/sdvx@asphyxia/webui/asset/js/generator.js index 6efac66..578674b 100644 --- a/sdvx@asphyxia/webui/asset/js/generator.js +++ b/sdvx@asphyxia/webui/asset/js/generator.js @@ -65,9 +65,10 @@ async function loadItems(itemSet, gene_edition, items_crew, items_stamp, items_s } - $('.count').text('Unlocked items: ' + itemCounts[0] + "/" + itemCounts[1]) - if(itemSet === 1) { - $('.setinfo').append('

These items are only usable in Japan. Change region to Japan in the ea3-config.xml file.

') + $('.count').text('Obtained items: ' + itemCounts[0] + "/" + itemCounts[1]) + if(geneItems.jpn_exc) { + $('.jpn-excl').remove() + $('.setinfo').append('

These items are only usable if your game region is set to Japan. Change your game region through either modifying ea3-config.xml or dll patch.

') } else { $('.jpn-excl').remove() } diff --git a/sdvx@asphyxia/webui/asset/js/pngjs/bitmapper.js b/sdvx@asphyxia/webui/asset/js/pngjs/bitmapper.js new file mode 100644 index 0000000..18378a0 --- /dev/null +++ b/sdvx@asphyxia/webui/asset/js/pngjs/bitmapper.js @@ -0,0 +1,267 @@ +"use strict"; + +let interlaceUtils = require("./interlace"); + +let pixelBppMapper = [ + // 0 - dummy entry + function () {}, + + // 1 - L + // 0: 0, 1: 0, 2: 0, 3: 0xff + function (pxData, data, pxPos, rawPos) { + if (rawPos === data.length) { + throw new Error("Ran out of data"); + } + + let pixel = data[rawPos]; + pxData[pxPos] = pixel; + pxData[pxPos + 1] = pixel; + pxData[pxPos + 2] = pixel; + pxData[pxPos + 3] = 0xff; + }, + + // 2 - LA + // 0: 0, 1: 0, 2: 0, 3: 1 + function (pxData, data, pxPos, rawPos) { + if (rawPos + 1 >= data.length) { + throw new Error("Ran out of data"); + } + + let pixel = data[rawPos]; + pxData[pxPos] = pixel; + pxData[pxPos + 1] = pixel; + pxData[pxPos + 2] = pixel; + pxData[pxPos + 3] = data[rawPos + 1]; + }, + + // 3 - RGB + // 0: 0, 1: 1, 2: 2, 3: 0xff + function (pxData, data, pxPos, rawPos) { + if (rawPos + 2 >= data.length) { + throw new Error("Ran out of data"); + } + + pxData[pxPos] = data[rawPos]; + pxData[pxPos + 1] = data[rawPos + 1]; + pxData[pxPos + 2] = data[rawPos + 2]; + pxData[pxPos + 3] = 0xff; + }, + + // 4 - RGBA + // 0: 0, 1: 1, 2: 2, 3: 3 + function (pxData, data, pxPos, rawPos) { + if (rawPos + 3 >= data.length) { + throw new Error("Ran out of data"); + } + + pxData[pxPos] = data[rawPos]; + pxData[pxPos + 1] = data[rawPos + 1]; + pxData[pxPos + 2] = data[rawPos + 2]; + pxData[pxPos + 3] = data[rawPos + 3]; + }, +]; + +let pixelBppCustomMapper = [ + // 0 - dummy entry + function () {}, + + // 1 - L + // 0: 0, 1: 0, 2: 0, 3: 0xff + function (pxData, pixelData, pxPos, maxBit) { + let pixel = pixelData[0]; + pxData[pxPos] = pixel; + pxData[pxPos + 1] = pixel; + pxData[pxPos + 2] = pixel; + pxData[pxPos + 3] = maxBit; + }, + + // 2 - LA + // 0: 0, 1: 0, 2: 0, 3: 1 + function (pxData, pixelData, pxPos) { + let pixel = pixelData[0]; + pxData[pxPos] = pixel; + pxData[pxPos + 1] = pixel; + pxData[pxPos + 2] = pixel; + pxData[pxPos + 3] = pixelData[1]; + }, + + // 3 - RGB + // 0: 0, 1: 1, 2: 2, 3: 0xff + function (pxData, pixelData, pxPos, maxBit) { + pxData[pxPos] = pixelData[0]; + pxData[pxPos + 1] = pixelData[1]; + pxData[pxPos + 2] = pixelData[2]; + pxData[pxPos + 3] = maxBit; + }, + + // 4 - RGBA + // 0: 0, 1: 1, 2: 2, 3: 3 + function (pxData, pixelData, pxPos) { + pxData[pxPos] = pixelData[0]; + pxData[pxPos + 1] = pixelData[1]; + pxData[pxPos + 2] = pixelData[2]; + pxData[pxPos + 3] = pixelData[3]; + }, +]; + +function bitRetriever(data, depth) { + let leftOver = []; + let i = 0; + + function split() { + if (i === data.length) { + throw new Error("Ran out of data"); + } + let byte = data[i]; + i++; + let byte8, byte7, byte6, byte5, byte4, byte3, byte2, byte1; + switch (depth) { + default: + throw new Error("unrecognised depth"); + case 16: + byte2 = data[i]; + i++; + leftOver.push((byte << 8) + byte2); + break; + case 4: + byte2 = byte & 0x0f; + byte1 = byte >> 4; + leftOver.push(byte1, byte2); + break; + case 2: + byte4 = byte & 3; + byte3 = (byte >> 2) & 3; + byte2 = (byte >> 4) & 3; + byte1 = (byte >> 6) & 3; + leftOver.push(byte1, byte2, byte3, byte4); + break; + case 1: + byte8 = byte & 1; + byte7 = (byte >> 1) & 1; + byte6 = (byte >> 2) & 1; + byte5 = (byte >> 3) & 1; + byte4 = (byte >> 4) & 1; + byte3 = (byte >> 5) & 1; + byte2 = (byte >> 6) & 1; + byte1 = (byte >> 7) & 1; + leftOver.push(byte1, byte2, byte3, byte4, byte5, byte6, byte7, byte8); + break; + } + } + + return { + get: function (count) { + while (leftOver.length < count) { + split(); + } + let returner = leftOver.slice(0, count); + leftOver = leftOver.slice(count); + return returner; + }, + resetAfterLine: function () { + leftOver.length = 0; + }, + end: function () { + if (i !== data.length) { + throw new Error("extra data found"); + } + }, + }; +} + +function mapImage8Bit(image, pxData, getPxPos, bpp, data, rawPos) { + // eslint-disable-line max-params + let imageWidth = image.width; + let imageHeight = image.height; + let imagePass = image.index; + for (let y = 0; y < imageHeight; y++) { + for (let x = 0; x < imageWidth; x++) { + let pxPos = getPxPos(x, y, imagePass); + pixelBppMapper[bpp](pxData, data, pxPos, rawPos); + rawPos += bpp; //eslint-disable-line no-param-reassign + } + } + return rawPos; +} + +function mapImageCustomBit(image, pxData, getPxPos, bpp, bits, maxBit) { + // eslint-disable-line max-params + let imageWidth = image.width; + let imageHeight = image.height; + let imagePass = image.index; + for (let y = 0; y < imageHeight; y++) { + for (let x = 0; x < imageWidth; x++) { + let pixelData = bits.get(bpp); + let pxPos = getPxPos(x, y, imagePass); + pixelBppCustomMapper[bpp](pxData, pixelData, pxPos, maxBit); + } + bits.resetAfterLine(); + } +} + +exports.dataToBitMap = function (data, bitmapInfo) { + let width = bitmapInfo.width; + let height = bitmapInfo.height; + let depth = bitmapInfo.depth; + let bpp = bitmapInfo.bpp; + let interlace = bitmapInfo.interlace; + let bits; + + if (depth !== 8) { + bits = bitRetriever(data, depth); + } + let pxData; + if (depth <= 8) { + pxData = Buffer.alloc(width * height * 4); + } else { + pxData = new Uint16Array(width * height * 4); + } + let maxBit = Math.pow(2, depth) - 1; + let rawPos = 0; + let images; + let getPxPos; + + if (interlace) { + images = interlaceUtils.getImagePasses(width, height); + getPxPos = interlaceUtils.getInterlaceIterator(width, height); + } else { + let nonInterlacedPxPos = 0; + getPxPos = function () { + let returner = nonInterlacedPxPos; + nonInterlacedPxPos += 4; + return returner; + }; + images = [{ width: width, height: height }]; + } + + for (let imageIndex = 0; imageIndex < images.length; imageIndex++) { + if (depth === 8) { + rawPos = mapImage8Bit( + images[imageIndex], + pxData, + getPxPos, + bpp, + data, + rawPos + ); + } else { + mapImageCustomBit( + images[imageIndex], + pxData, + getPxPos, + bpp, + bits, + maxBit + ); + } + } + if (depth === 8) { + if (rawPos !== data.length) { + throw new Error("extra data found"); + } + } else { + bits.end(); + } + + return pxData; +}; diff --git a/sdvx@asphyxia/webui/asset/js/pngjs/bitpacker.js b/sdvx@asphyxia/webui/asset/js/pngjs/bitpacker.js new file mode 100644 index 0000000..d7a4e65 --- /dev/null +++ b/sdvx@asphyxia/webui/asset/js/pngjs/bitpacker.js @@ -0,0 +1,158 @@ +"use strict"; + +let constants = require("./constants"); + +module.exports = function (dataIn, width, height, options) { + let outHasAlpha = + [constants.COLORTYPE_COLOR_ALPHA, constants.COLORTYPE_ALPHA].indexOf( + options.colorType + ) !== -1; + if (options.colorType === options.inputColorType) { + let bigEndian = (function () { + let buffer = new ArrayBuffer(2); + new DataView(buffer).setInt16(0, 256, true /* littleEndian */); + // Int16Array uses the platform's endianness. + return new Int16Array(buffer)[0] !== 256; + })(); + // If no need to convert to grayscale and alpha is present/absent in both, take a fast route + if (options.bitDepth === 8 || (options.bitDepth === 16 && bigEndian)) { + return dataIn; + } + } + + // map to a UInt16 array if data is 16bit, fix endianness below + let data = options.bitDepth !== 16 ? dataIn : new Uint16Array(dataIn.buffer); + + let maxValue = 255; + let inBpp = constants.COLORTYPE_TO_BPP_MAP[options.inputColorType]; + if (inBpp === 4 && !options.inputHasAlpha) { + inBpp = 3; + } + let outBpp = constants.COLORTYPE_TO_BPP_MAP[options.colorType]; + if (options.bitDepth === 16) { + maxValue = 65535; + outBpp *= 2; + } + let outData = Buffer.alloc(width * height * outBpp); + + let inIndex = 0; + let outIndex = 0; + + let bgColor = options.bgColor || {}; + if (bgColor.red === undefined) { + bgColor.red = maxValue; + } + if (bgColor.green === undefined) { + bgColor.green = maxValue; + } + if (bgColor.blue === undefined) { + bgColor.blue = maxValue; + } + + function getRGBA() { + let red; + let green; + let blue; + let alpha = maxValue; + switch (options.inputColorType) { + case constants.COLORTYPE_COLOR_ALPHA: + alpha = data[inIndex + 3]; + red = data[inIndex]; + green = data[inIndex + 1]; + blue = data[inIndex + 2]; + break; + case constants.COLORTYPE_COLOR: + red = data[inIndex]; + green = data[inIndex + 1]; + blue = data[inIndex + 2]; + break; + case constants.COLORTYPE_ALPHA: + alpha = data[inIndex + 1]; + red = data[inIndex]; + green = red; + blue = red; + break; + case constants.COLORTYPE_GRAYSCALE: + red = data[inIndex]; + green = red; + blue = red; + break; + default: + throw new Error( + "input color type:" + + options.inputColorType + + " is not supported at present" + ); + } + + if (options.inputHasAlpha) { + if (!outHasAlpha) { + alpha /= maxValue; + red = Math.min( + Math.max(Math.round((1 - alpha) * bgColor.red + alpha * red), 0), + maxValue + ); + green = Math.min( + Math.max(Math.round((1 - alpha) * bgColor.green + alpha * green), 0), + maxValue + ); + blue = Math.min( + Math.max(Math.round((1 - alpha) * bgColor.blue + alpha * blue), 0), + maxValue + ); + } + } + return { red: red, green: green, blue: blue, alpha: alpha }; + } + + for (let y = 0; y < height; y++) { + for (let x = 0; x < width; x++) { + let rgba = getRGBA(data, inIndex); + + switch (options.colorType) { + case constants.COLORTYPE_COLOR_ALPHA: + case constants.COLORTYPE_COLOR: + if (options.bitDepth === 8) { + outData[outIndex] = rgba.red; + outData[outIndex + 1] = rgba.green; + outData[outIndex + 2] = rgba.blue; + if (outHasAlpha) { + outData[outIndex + 3] = rgba.alpha; + } + } else { + outData.writeUInt16BE(rgba.red, outIndex); + outData.writeUInt16BE(rgba.green, outIndex + 2); + outData.writeUInt16BE(rgba.blue, outIndex + 4); + if (outHasAlpha) { + outData.writeUInt16BE(rgba.alpha, outIndex + 6); + } + } + break; + case constants.COLORTYPE_ALPHA: + case constants.COLORTYPE_GRAYSCALE: { + // Convert to grayscale and alpha + let grayscale = (rgba.red + rgba.green + rgba.blue) / 3; + if (options.bitDepth === 8) { + outData[outIndex] = grayscale; + if (outHasAlpha) { + outData[outIndex + 1] = rgba.alpha; + } + } else { + outData.writeUInt16BE(grayscale, outIndex); + if (outHasAlpha) { + outData.writeUInt16BE(rgba.alpha, outIndex + 2); + } + } + break; + } + default: + throw new Error("unrecognised color Type " + options.colorType); + } + + inIndex += inBpp; + outIndex += outBpp; + } + } + + return outData; +}; diff --git a/sdvx@asphyxia/webui/asset/js/pngjs/chunkstream.js b/sdvx@asphyxia/webui/asset/js/pngjs/chunkstream.js new file mode 100644 index 0000000..95b46d4 --- /dev/null +++ b/sdvx@asphyxia/webui/asset/js/pngjs/chunkstream.js @@ -0,0 +1,189 @@ +"use strict"; + +let util = require("util"); +let Stream = require("stream"); + +let ChunkStream = (module.exports = function () { + Stream.call(this); + + this._buffers = []; + this._buffered = 0; + + this._reads = []; + this._paused = false; + + this._encoding = "utf8"; + this.writable = true; +}); +util.inherits(ChunkStream, Stream); + +ChunkStream.prototype.read = function (length, callback) { + this._reads.push({ + length: Math.abs(length), // if length < 0 then at most this length + allowLess: length < 0, + func: callback, + }); + + process.nextTick( + function () { + this._process(); + + // its paused and there is not enought data then ask for more + if (this._paused && this._reads && this._reads.length > 0) { + this._paused = false; + + this.emit("drain"); + } + }.bind(this) + ); +}; + +ChunkStream.prototype.write = function (data, encoding) { + if (!this.writable) { + this.emit("error", new Error("Stream not writable")); + return false; + } + + let dataBuffer; + if (Buffer.isBuffer(data)) { + dataBuffer = data; + } else { + dataBuffer = Buffer.from(data, encoding || this._encoding); + } + + this._buffers.push(dataBuffer); + this._buffered += dataBuffer.length; + + this._process(); + + // ok if there are no more read requests + if (this._reads && this._reads.length === 0) { + this._paused = true; + } + + return this.writable && !this._paused; +}; + +ChunkStream.prototype.end = function (data, encoding) { + if (data) { + this.write(data, encoding); + } + + this.writable = false; + + // already destroyed + if (!this._buffers) { + return; + } + + // enqueue or handle end + if (this._buffers.length === 0) { + this._end(); + } else { + this._buffers.push(null); + this._process(); + } +}; + +ChunkStream.prototype.destroySoon = ChunkStream.prototype.end; + +ChunkStream.prototype._end = function () { + if (this._reads.length > 0) { + this.emit("error", new Error("Unexpected end of input")); + } + + this.destroy(); +}; + +ChunkStream.prototype.destroy = function () { + if (!this._buffers) { + return; + } + + this.writable = false; + this._reads = null; + this._buffers = null; + + this.emit("close"); +}; + +ChunkStream.prototype._processReadAllowingLess = function (read) { + // ok there is any data so that we can satisfy this request + this._reads.shift(); // == read + + // first we need to peek into first buffer + let smallerBuf = this._buffers[0]; + + // ok there is more data than we need + if (smallerBuf.length > read.length) { + this._buffered -= read.length; + this._buffers[0] = smallerBuf.slice(read.length); + + read.func.call(this, smallerBuf.slice(0, read.length)); + } else { + // ok this is less than maximum length so use it all + this._buffered -= smallerBuf.length; + this._buffers.shift(); // == smallerBuf + + read.func.call(this, smallerBuf); + } +}; + +ChunkStream.prototype._processRead = function (read) { + this._reads.shift(); // == read + + let pos = 0; + let count = 0; + let data = Buffer.alloc(read.length); + + // create buffer for all data + while (pos < read.length) { + let buf = this._buffers[count++]; + let len = Math.min(buf.length, read.length - pos); + + buf.copy(data, pos, 0, len); + pos += len; + + // last buffer wasn't used all so just slice it and leave + if (len !== buf.length) { + this._buffers[--count] = buf.slice(len); + } + } + + // remove all used buffers + if (count > 0) { + this._buffers.splice(0, count); + } + + this._buffered -= read.length; + + read.func.call(this, data); +}; + +ChunkStream.prototype._process = function () { + try { + // as long as there is any data and read requests + while (this._buffered > 0 && this._reads && this._reads.length > 0) { + let read = this._reads[0]; + + // read any data (but no more than length) + if (read.allowLess) { + this._processReadAllowingLess(read); + } else if (this._buffered >= read.length) { + // ok we can meet some expectations + + this._processRead(read); + } else { + // not enought data to satisfy first request in queue + // so we need to wait for more + break; + } + } + + if (this._buffers && !this.writable) { + this._end(); + } + } catch (ex) { + this.emit("error", ex); + } +}; diff --git a/sdvx@asphyxia/webui/asset/js/pngjs/constants.js b/sdvx@asphyxia/webui/asset/js/pngjs/constants.js new file mode 100644 index 0000000..21fdad6 --- /dev/null +++ b/sdvx@asphyxia/webui/asset/js/pngjs/constants.js @@ -0,0 +1,32 @@ +"use strict"; + +module.exports = { + PNG_SIGNATURE: [0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a], + + TYPE_IHDR: 0x49484452, + TYPE_IEND: 0x49454e44, + TYPE_IDAT: 0x49444154, + TYPE_PLTE: 0x504c5445, + TYPE_tRNS: 0x74524e53, // eslint-disable-line camelcase + TYPE_gAMA: 0x67414d41, // eslint-disable-line camelcase + + // color-type bits + COLORTYPE_GRAYSCALE: 0, + COLORTYPE_PALETTE: 1, + COLORTYPE_COLOR: 2, + COLORTYPE_ALPHA: 4, // e.g. grayscale and alpha + + // color-type combinations + COLORTYPE_PALETTE_COLOR: 3, + COLORTYPE_COLOR_ALPHA: 6, + + COLORTYPE_TO_BPP_MAP: { + 0: 1, + 2: 3, + 3: 1, + 4: 2, + 6: 4, + }, + + GAMMA_DIVISION: 100000, +}; diff --git a/sdvx@asphyxia/webui/asset/js/pngjs/crc.js b/sdvx@asphyxia/webui/asset/js/pngjs/crc.js new file mode 100644 index 0000000..950ec8a --- /dev/null +++ b/sdvx@asphyxia/webui/asset/js/pngjs/crc.js @@ -0,0 +1,40 @@ +"use strict"; + +let crcTable = []; + +(function () { + for (let i = 0; i < 256; i++) { + let currentCrc = i; + for (let j = 0; j < 8; j++) { + if (currentCrc & 1) { + currentCrc = 0xedb88320 ^ (currentCrc >>> 1); + } else { + currentCrc = currentCrc >>> 1; + } + } + crcTable[i] = currentCrc; + } +})(); + +let CrcCalculator = (module.exports = function () { + this._crc = -1; +}); + +CrcCalculator.prototype.write = function (data) { + for (let i = 0; i < data.length; i++) { + this._crc = crcTable[(this._crc ^ data[i]) & 0xff] ^ (this._crc >>> 8); + } + return true; +}; + +CrcCalculator.prototype.crc32 = function () { + return this._crc ^ -1; +}; + +CrcCalculator.crc32 = function (buf) { + let crc = -1; + for (let i = 0; i < buf.length; i++) { + crc = crcTable[(crc ^ buf[i]) & 0xff] ^ (crc >>> 8); + } + return crc ^ -1; +}; diff --git a/sdvx@asphyxia/webui/asset/js/pngjs/filter-pack.js b/sdvx@asphyxia/webui/asset/js/pngjs/filter-pack.js new file mode 100644 index 0000000..32c85c4 --- /dev/null +++ b/sdvx@asphyxia/webui/asset/js/pngjs/filter-pack.js @@ -0,0 +1,171 @@ +"use strict"; + +let paethPredictor = require("./paeth-predictor"); + +function filterNone(pxData, pxPos, byteWidth, rawData, rawPos) { + for (let x = 0; x < byteWidth; x++) { + rawData[rawPos + x] = pxData[pxPos + x]; + } +} + +function filterSumNone(pxData, pxPos, byteWidth) { + let sum = 0; + let length = pxPos + byteWidth; + + for (let i = pxPos; i < length; i++) { + sum += Math.abs(pxData[i]); + } + return sum; +} + +function filterSub(pxData, pxPos, byteWidth, rawData, rawPos, bpp) { + for (let x = 0; x < byteWidth; x++) { + let left = x >= bpp ? pxData[pxPos + x - bpp] : 0; + let val = pxData[pxPos + x] - left; + + rawData[rawPos + x] = val; + } +} + +function filterSumSub(pxData, pxPos, byteWidth, bpp) { + let sum = 0; + for (let x = 0; x < byteWidth; x++) { + let left = x >= bpp ? pxData[pxPos + x - bpp] : 0; + let val = pxData[pxPos + x] - left; + + sum += Math.abs(val); + } + + return sum; +} + +function filterUp(pxData, pxPos, byteWidth, rawData, rawPos) { + for (let x = 0; x < byteWidth; x++) { + let up = pxPos > 0 ? pxData[pxPos + x - byteWidth] : 0; + let val = pxData[pxPos + x] - up; + + rawData[rawPos + x] = val; + } +} + +function filterSumUp(pxData, pxPos, byteWidth) { + let sum = 0; + let length = pxPos + byteWidth; + for (let x = pxPos; x < length; x++) { + let up = pxPos > 0 ? pxData[x - byteWidth] : 0; + let val = pxData[x] - up; + + sum += Math.abs(val); + } + + return sum; +} + +function filterAvg(pxData, pxPos, byteWidth, rawData, rawPos, bpp) { + for (let x = 0; x < byteWidth; x++) { + let left = x >= bpp ? pxData[pxPos + x - bpp] : 0; + let up = pxPos > 0 ? pxData[pxPos + x - byteWidth] : 0; + let val = pxData[pxPos + x] - ((left + up) >> 1); + + rawData[rawPos + x] = val; + } +} + +function filterSumAvg(pxData, pxPos, byteWidth, bpp) { + let sum = 0; + for (let x = 0; x < byteWidth; x++) { + let left = x >= bpp ? pxData[pxPos + x - bpp] : 0; + let up = pxPos > 0 ? pxData[pxPos + x - byteWidth] : 0; + let val = pxData[pxPos + x] - ((left + up) >> 1); + + sum += Math.abs(val); + } + + return sum; +} + +function filterPaeth(pxData, pxPos, byteWidth, rawData, rawPos, bpp) { + for (let x = 0; x < byteWidth; x++) { + let left = x >= bpp ? pxData[pxPos + x - bpp] : 0; + let up = pxPos > 0 ? pxData[pxPos + x - byteWidth] : 0; + let upleft = + pxPos > 0 && x >= bpp ? pxData[pxPos + x - (byteWidth + bpp)] : 0; + let val = pxData[pxPos + x] - paethPredictor(left, up, upleft); + + rawData[rawPos + x] = val; + } +} + +function filterSumPaeth(pxData, pxPos, byteWidth, bpp) { + let sum = 0; + for (let x = 0; x < byteWidth; x++) { + let left = x >= bpp ? pxData[pxPos + x - bpp] : 0; + let up = pxPos > 0 ? pxData[pxPos + x - byteWidth] : 0; + let upleft = + pxPos > 0 && x >= bpp ? pxData[pxPos + x - (byteWidth + bpp)] : 0; + let val = pxData[pxPos + x] - paethPredictor(left, up, upleft); + + sum += Math.abs(val); + } + + return sum; +} + +let filters = { + 0: filterNone, + 1: filterSub, + 2: filterUp, + 3: filterAvg, + 4: filterPaeth, +}; + +let filterSums = { + 0: filterSumNone, + 1: filterSumSub, + 2: filterSumUp, + 3: filterSumAvg, + 4: filterSumPaeth, +}; + +module.exports = function (pxData, width, height, options, bpp) { + let filterTypes; + if (!("filterType" in options) || options.filterType === -1) { + filterTypes = [0, 1, 2, 3, 4]; + } else if (typeof options.filterType === "number") { + filterTypes = [options.filterType]; + } else { + throw new Error("unrecognised filter types"); + } + + if (options.bitDepth === 16) { + bpp *= 2; + } + let byteWidth = width * bpp; + let rawPos = 0; + let pxPos = 0; + let rawData = Buffer.alloc((byteWidth + 1) * height); + + let sel = filterTypes[0]; + + for (let y = 0; y < height; y++) { + if (filterTypes.length > 1) { + // find best filter for this line (with lowest sum of values) + let min = Infinity; + + for (let i = 0; i < filterTypes.length; i++) { + let sum = filterSums[filterTypes[i]](pxData, pxPos, byteWidth, bpp); + if (sum < min) { + sel = filterTypes[i]; + min = sum; + } + } + } + + rawData[rawPos] = sel; + rawPos++; + filters[sel](pxData, pxPos, byteWidth, rawData, rawPos, bpp); + rawPos += byteWidth; + pxPos += byteWidth; + } + return rawData; +}; diff --git a/sdvx@asphyxia/webui/asset/js/pngjs/filter-parse-async.js b/sdvx@asphyxia/webui/asset/js/pngjs/filter-parse-async.js new file mode 100644 index 0000000..832b86c --- /dev/null +++ b/sdvx@asphyxia/webui/asset/js/pngjs/filter-parse-async.js @@ -0,0 +1,24 @@ +"use strict"; + +let util = require("util"); +let ChunkStream = require("./chunkstream"); +let Filter = require("./filter-parse"); + +let FilterAsync = (module.exports = function (bitmapInfo) { + ChunkStream.call(this); + + let buffers = []; + let that = this; + this._filter = new Filter(bitmapInfo, { + read: this.read.bind(this), + write: function (buffer) { + buffers.push(buffer); + }, + complete: function () { + that.emit("complete", Buffer.concat(buffers)); + }, + }); + + this._filter.start(); +}); +util.inherits(FilterAsync, ChunkStream); diff --git a/sdvx@asphyxia/webui/asset/js/pngjs/filter-parse-sync.js b/sdvx@asphyxia/webui/asset/js/pngjs/filter-parse-sync.js new file mode 100644 index 0000000..6924d16 --- /dev/null +++ b/sdvx@asphyxia/webui/asset/js/pngjs/filter-parse-sync.js @@ -0,0 +1,21 @@ +"use strict"; + +let SyncReader = require("./sync-reader"); +let Filter = require("./filter-parse"); + +exports.process = function (inBuffer, bitmapInfo) { + let outBuffers = []; + let reader = new SyncReader(inBuffer); + let filter = new Filter(bitmapInfo, { + read: reader.read.bind(reader), + write: function (bufferPart) { + outBuffers.push(bufferPart); + }, + complete: function () {}, + }); + + filter.start(); + reader.process(); + + return Buffer.concat(outBuffers); +}; diff --git a/sdvx@asphyxia/webui/asset/js/pngjs/filter-parse.js b/sdvx@asphyxia/webui/asset/js/pngjs/filter-parse.js new file mode 100644 index 0000000..3a32e5e --- /dev/null +++ b/sdvx@asphyxia/webui/asset/js/pngjs/filter-parse.js @@ -0,0 +1,177 @@ +"use strict"; + +let interlaceUtils = require("./interlace"); +let paethPredictor = require("./paeth-predictor"); + +function getByteWidth(width, bpp, depth) { + let byteWidth = width * bpp; + if (depth !== 8) { + byteWidth = Math.ceil(byteWidth / (8 / depth)); + } + return byteWidth; +} + +let Filter = (module.exports = function (bitmapInfo, dependencies) { + let width = bitmapInfo.width; + let height = bitmapInfo.height; + let interlace = bitmapInfo.interlace; + let bpp = bitmapInfo.bpp; + let depth = bitmapInfo.depth; + + this.read = dependencies.read; + this.write = dependencies.write; + this.complete = dependencies.complete; + + this._imageIndex = 0; + this._images = []; + if (interlace) { + let passes = interlaceUtils.getImagePasses(width, height); + for (let i = 0; i < passes.length; i++) { + this._images.push({ + byteWidth: getByteWidth(passes[i].width, bpp, depth), + height: passes[i].height, + lineIndex: 0, + }); + } + } else { + this._images.push({ + byteWidth: getByteWidth(width, bpp, depth), + height: height, + lineIndex: 0, + }); + } + + // when filtering the line we look at the pixel to the left + // the spec also says it is done on a byte level regardless of the number of pixels + // so if the depth is byte compatible (8 or 16) we subtract the bpp in order to compare back + // a pixel rather than just a different byte part. However if we are sub byte, we ignore. + if (depth === 8) { + this._xComparison = bpp; + } else if (depth === 16) { + this._xComparison = bpp * 2; + } else { + this._xComparison = 1; + } +}); + +Filter.prototype.start = function () { + this.read( + this._images[this._imageIndex].byteWidth + 1, + this._reverseFilterLine.bind(this) + ); +}; + +Filter.prototype._unFilterType1 = function ( + rawData, + unfilteredLine, + byteWidth +) { + let xComparison = this._xComparison; + let xBiggerThan = xComparison - 1; + + for (let x = 0; x < byteWidth; x++) { + let rawByte = rawData[1 + x]; + let f1Left = x > xBiggerThan ? unfilteredLine[x - xComparison] : 0; + unfilteredLine[x] = rawByte + f1Left; + } +}; + +Filter.prototype._unFilterType2 = function ( + rawData, + unfilteredLine, + byteWidth +) { + let lastLine = this._lastLine; + + for (let x = 0; x < byteWidth; x++) { + let rawByte = rawData[1 + x]; + let f2Up = lastLine ? lastLine[x] : 0; + unfilteredLine[x] = rawByte + f2Up; + } +}; + +Filter.prototype._unFilterType3 = function ( + rawData, + unfilteredLine, + byteWidth +) { + let xComparison = this._xComparison; + let xBiggerThan = xComparison - 1; + let lastLine = this._lastLine; + + for (let x = 0; x < byteWidth; x++) { + let rawByte = rawData[1 + x]; + let f3Up = lastLine ? lastLine[x] : 0; + let f3Left = x > xBiggerThan ? unfilteredLine[x - xComparison] : 0; + let f3Add = Math.floor((f3Left + f3Up) / 2); + unfilteredLine[x] = rawByte + f3Add; + } +}; + +Filter.prototype._unFilterType4 = function ( + rawData, + unfilteredLine, + byteWidth +) { + let xComparison = this._xComparison; + let xBiggerThan = xComparison - 1; + let lastLine = this._lastLine; + + for (let x = 0; x < byteWidth; x++) { + let rawByte = rawData[1 + x]; + let f4Up = lastLine ? lastLine[x] : 0; + let f4Left = x > xBiggerThan ? unfilteredLine[x - xComparison] : 0; + let f4UpLeft = x > xBiggerThan && lastLine ? lastLine[x - xComparison] : 0; + let f4Add = paethPredictor(f4Left, f4Up, f4UpLeft); + unfilteredLine[x] = rawByte + f4Add; + } +}; + +Filter.prototype._reverseFilterLine = function (rawData) { + let filter = rawData[0]; + let unfilteredLine; + let currentImage = this._images[this._imageIndex]; + let byteWidth = currentImage.byteWidth; + + if (filter === 0) { + unfilteredLine = rawData.slice(1, byteWidth + 1); + } else { + unfilteredLine = Buffer.alloc(byteWidth); + + switch (filter) { + case 1: + this._unFilterType1(rawData, unfilteredLine, byteWidth); + break; + case 2: + this._unFilterType2(rawData, unfilteredLine, byteWidth); + break; + case 3: + this._unFilterType3(rawData, unfilteredLine, byteWidth); + break; + case 4: + this._unFilterType4(rawData, unfilteredLine, byteWidth); + break; + default: + throw new Error("Unrecognised filter type - " + filter); + } + } + + this.write(unfilteredLine); + + currentImage.lineIndex++; + if (currentImage.lineIndex >= currentImage.height) { + this._lastLine = null; + this._imageIndex++; + currentImage = this._images[this._imageIndex]; + } else { + this._lastLine = unfilteredLine; + } + + if (currentImage) { + // read, using the byte width that may be from the new current image + this.read(currentImage.byteWidth + 1, this._reverseFilterLine.bind(this)); + } else { + this._lastLine = null; + this.complete(); + } +}; diff --git a/sdvx@asphyxia/webui/asset/js/pngjs/format-normaliser.js b/sdvx@asphyxia/webui/asset/js/pngjs/format-normaliser.js new file mode 100644 index 0000000..209b66b --- /dev/null +++ b/sdvx@asphyxia/webui/asset/js/pngjs/format-normaliser.js @@ -0,0 +1,93 @@ +"use strict"; + +function dePalette(indata, outdata, width, height, palette) { + let pxPos = 0; + // use values from palette + for (let y = 0; y < height; y++) { + for (let x = 0; x < width; x++) { + let color = palette[indata[pxPos]]; + + if (!color) { + throw new Error("index " + indata[pxPos] + " not in palette"); + } + + for (let i = 0; i < 4; i++) { + outdata[pxPos + i] = color[i]; + } + pxPos += 4; + } + } +} + +function replaceTransparentColor(indata, outdata, width, height, transColor) { + let pxPos = 0; + for (let y = 0; y < height; y++) { + for (let x = 0; x < width; x++) { + let makeTrans = false; + + if (transColor.length === 1) { + if (transColor[0] === indata[pxPos]) { + makeTrans = true; + } + } else if ( + transColor[0] === indata[pxPos] && + transColor[1] === indata[pxPos + 1] && + transColor[2] === indata[pxPos + 2] + ) { + makeTrans = true; + } + if (makeTrans) { + for (let i = 0; i < 4; i++) { + outdata[pxPos + i] = 0; + } + } + pxPos += 4; + } + } +} + +function scaleDepth(indata, outdata, width, height, depth) { + let maxOutSample = 255; + let maxInSample = Math.pow(2, depth) - 1; + let pxPos = 0; + + for (let y = 0; y < height; y++) { + for (let x = 0; x < width; x++) { + for (let i = 0; i < 4; i++) { + outdata[pxPos + i] = Math.floor( + (indata[pxPos + i] * maxOutSample) / maxInSample + 0.5 + ); + } + pxPos += 4; + } + } +} + +module.exports = function (indata, imageData, skipRescale = false) { + let depth = imageData.depth; + let width = imageData.width; + let height = imageData.height; + let colorType = imageData.colorType; + let transColor = imageData.transColor; + let palette = imageData.palette; + + let outdata = indata; // only different for 16 bits + + if (colorType === 3) { + // paletted + dePalette(indata, outdata, width, height, palette); + } else { + if (transColor) { + replaceTransparentColor(indata, outdata, width, height, transColor); + } + // if it needs scaling + if (depth !== 8 && !skipRescale) { + // if we need to change the buffer size + if (depth === 16) { + outdata = Buffer.alloc(width * height * 4); + } + scaleDepth(indata, outdata, width, height, depth); + } + } + return outdata; +}; diff --git a/sdvx@asphyxia/webui/asset/js/pngjs/interlace.js b/sdvx@asphyxia/webui/asset/js/pngjs/interlace.js new file mode 100644 index 0000000..a035cb1 --- /dev/null +++ b/sdvx@asphyxia/webui/asset/js/pngjs/interlace.js @@ -0,0 +1,95 @@ +"use strict"; + +// Adam 7 +// 0 1 2 3 4 5 6 7 +// 0 x 6 4 6 x 6 4 6 +// 1 7 7 7 7 7 7 7 7 +// 2 5 6 5 6 5 6 5 6 +// 3 7 7 7 7 7 7 7 7 +// 4 3 6 4 6 3 6 4 6 +// 5 7 7 7 7 7 7 7 7 +// 6 5 6 5 6 5 6 5 6 +// 7 7 7 7 7 7 7 7 7 + +let imagePasses = [ + { + // pass 1 - 1px + x: [0], + y: [0], + }, + { + // pass 2 - 1px + x: [4], + y: [0], + }, + { + // pass 3 - 2px + x: [0, 4], + y: [4], + }, + { + // pass 4 - 4px + x: [2, 6], + y: [0, 4], + }, + { + // pass 5 - 8px + x: [0, 2, 4, 6], + y: [2, 6], + }, + { + // pass 6 - 16px + x: [1, 3, 5, 7], + y: [0, 2, 4, 6], + }, + { + // pass 7 - 32px + x: [0, 1, 2, 3, 4, 5, 6, 7], + y: [1, 3, 5, 7], + }, +]; + +exports.getImagePasses = function (width, height) { + let images = []; + let xLeftOver = width % 8; + let yLeftOver = height % 8; + let xRepeats = (width - xLeftOver) / 8; + let yRepeats = (height - yLeftOver) / 8; + for (let i = 0; i < imagePasses.length; i++) { + let pass = imagePasses[i]; + let passWidth = xRepeats * pass.x.length; + let passHeight = yRepeats * pass.y.length; + for (let j = 0; j < pass.x.length; j++) { + if (pass.x[j] < xLeftOver) { + passWidth++; + } else { + break; + } + } + for (let j = 0; j < pass.y.length; j++) { + if (pass.y[j] < yLeftOver) { + passHeight++; + } else { + break; + } + } + if (passWidth > 0 && passHeight > 0) { + images.push({ width: passWidth, height: passHeight, index: i }); + } + } + return images; +}; + +exports.getInterlaceIterator = function (width) { + return function (x, y, pass) { + let outerXLeftOver = x % imagePasses[pass].x.length; + let outerX = + ((x - outerXLeftOver) / imagePasses[pass].x.length) * 8 + + imagePasses[pass].x[outerXLeftOver]; + let outerYLeftOver = y % imagePasses[pass].y.length; + let outerY = + ((y - outerYLeftOver) / imagePasses[pass].y.length) * 8 + + imagePasses[pass].y[outerYLeftOver]; + return outerX * 4 + outerY * width * 4; + }; +}; diff --git a/sdvx@asphyxia/webui/asset/js/pngjs/packer-async.js b/sdvx@asphyxia/webui/asset/js/pngjs/packer-async.js new file mode 100644 index 0000000..f3df73a --- /dev/null +++ b/sdvx@asphyxia/webui/asset/js/pngjs/packer-async.js @@ -0,0 +1,50 @@ +"use strict"; + +let util = require("util"); +let Stream = require("stream"); +let constants = require("./constants"); +let Packer = require("./packer"); + +let PackerAsync = (module.exports = function (opt) { + Stream.call(this); + + let options = opt || {}; + + this._packer = new Packer(options); + this._deflate = this._packer.createDeflate(); + + this.readable = true; +}); +util.inherits(PackerAsync, Stream); + +PackerAsync.prototype.pack = function (data, width, height, gamma) { + // Signature + this.emit("data", Buffer.from(constants.PNG_SIGNATURE)); + this.emit("data", this._packer.packIHDR(width, height)); + + if (gamma) { + this.emit("data", this._packer.packGAMA(gamma)); + } + + let filteredData = this._packer.filterData(data, width, height); + + // compress it + this._deflate.on("error", this.emit.bind(this, "error")); + + this._deflate.on( + "data", + function (compressedData) { + this.emit("data", this._packer.packIDAT(compressedData)); + }.bind(this) + ); + + this._deflate.on( + "end", + function () { + this.emit("data", this._packer.packIEND()); + this.emit("end"); + }.bind(this) + ); + + this._deflate.end(filteredData); +}; diff --git a/sdvx@asphyxia/webui/asset/js/pngjs/packer-sync.js b/sdvx@asphyxia/webui/asset/js/pngjs/packer-sync.js new file mode 100644 index 0000000..f5ab0b3 --- /dev/null +++ b/sdvx@asphyxia/webui/asset/js/pngjs/packer-sync.js @@ -0,0 +1,56 @@ +"use strict"; + +let hasSyncZlib = true; +let zlib = require("zlib"); +if (!zlib.deflateSync) { + hasSyncZlib = false; +} +let constants = require("./constants"); +let Packer = require("./packer"); + +module.exports = function (metaData, opt) { + if (!hasSyncZlib) { + throw new Error( + "To use the sync capability of this library in old node versions, please pin pngjs to v2.3.0" + ); + } + + let options = opt || {}; + + let packer = new Packer(options); + + let chunks = []; + + // Signature + chunks.push(Buffer.from(constants.PNG_SIGNATURE)); + + // Header + chunks.push(packer.packIHDR(metaData.width, metaData.height)); + + if (metaData.gamma) { + chunks.push(packer.packGAMA(metaData.gamma)); + } + + let filteredData = packer.filterData( + metaData.data, + metaData.width, + metaData.height + ); + + // compress it + let compressedData = zlib.deflateSync( + filteredData, + packer.getDeflateOptions() + ); + filteredData = null; + + if (!compressedData || !compressedData.length) { + throw new Error("bad png - invalid compressed data response"); + } + chunks.push(packer.packIDAT(compressedData)); + + // End + chunks.push(packer.packIEND()); + + return Buffer.concat(chunks); +}; diff --git a/sdvx@asphyxia/webui/asset/js/pngjs/packer.js b/sdvx@asphyxia/webui/asset/js/pngjs/packer.js new file mode 100644 index 0000000..4aba12c --- /dev/null +++ b/sdvx@asphyxia/webui/asset/js/pngjs/packer.js @@ -0,0 +1,129 @@ +"use strict"; + +let constants = require("./constants"); +let CrcStream = require("./crc"); +let bitPacker = require("./bitpacker"); +let filter = require("./filter-pack"); +let zlib = require("zlib"); + +let Packer = (module.exports = function (options) { + this._options = options; + + options.deflateChunkSize = options.deflateChunkSize || 32 * 1024; + options.deflateLevel = + options.deflateLevel != null ? options.deflateLevel : 9; + options.deflateStrategy = + options.deflateStrategy != null ? options.deflateStrategy : 3; + options.inputHasAlpha = + options.inputHasAlpha != null ? options.inputHasAlpha : true; + options.deflateFactory = options.deflateFactory || zlib.createDeflate; + options.bitDepth = options.bitDepth || 8; + // This is outputColorType + options.colorType = + typeof options.colorType === "number" + ? options.colorType + : constants.COLORTYPE_COLOR_ALPHA; + options.inputColorType = + typeof options.inputColorType === "number" + ? options.inputColorType + : constants.COLORTYPE_COLOR_ALPHA; + + if ( + [ + constants.COLORTYPE_GRAYSCALE, + constants.COLORTYPE_COLOR, + constants.COLORTYPE_COLOR_ALPHA, + constants.COLORTYPE_ALPHA, + ].indexOf(options.colorType) === -1 + ) { + throw new Error( + "option color type:" + options.colorType + " is not supported at present" + ); + } + if ( + [ + constants.COLORTYPE_GRAYSCALE, + constants.COLORTYPE_COLOR, + constants.COLORTYPE_COLOR_ALPHA, + constants.COLORTYPE_ALPHA, + ].indexOf(options.inputColorType) === -1 + ) { + throw new Error( + "option input color type:" + + options.inputColorType + + " is not supported at present" + ); + } + if (options.bitDepth !== 8 && options.bitDepth !== 16) { + throw new Error( + "option bit depth:" + options.bitDepth + " is not supported at present" + ); + } +}); + +Packer.prototype.getDeflateOptions = function () { + return { + chunkSize: this._options.deflateChunkSize, + level: this._options.deflateLevel, + strategy: this._options.deflateStrategy, + }; +}; + +Packer.prototype.createDeflate = function () { + return this._options.deflateFactory(this.getDeflateOptions()); +}; + +Packer.prototype.filterData = function (data, width, height) { + // convert to correct format for filtering (e.g. right bpp and bit depth) + let packedData = bitPacker(data, width, height, this._options); + + // filter pixel data + let bpp = constants.COLORTYPE_TO_BPP_MAP[this._options.colorType]; + let filteredData = filter(packedData, width, height, this._options, bpp); + return filteredData; +}; + +Packer.prototype._packChunk = function (type, data) { + let len = data ? data.length : 0; + let buf = Buffer.alloc(len + 12); + + buf.writeUInt32BE(len, 0); + buf.writeUInt32BE(type, 4); + + if (data) { + data.copy(buf, 8); + } + + buf.writeInt32BE( + CrcStream.crc32(buf.slice(4, buf.length - 4)), + buf.length - 4 + ); + return buf; +}; + +Packer.prototype.packGAMA = function (gamma) { + let buf = Buffer.alloc(4); + buf.writeUInt32BE(Math.floor(gamma * constants.GAMMA_DIVISION), 0); + return this._packChunk(constants.TYPE_gAMA, buf); +}; + +Packer.prototype.packIHDR = function (width, height) { + let buf = Buffer.alloc(13); + buf.writeUInt32BE(width, 0); + buf.writeUInt32BE(height, 4); + buf[8] = this._options.bitDepth; // Bit depth + buf[9] = this._options.colorType; // colorType + buf[10] = 0; // compression + buf[11] = 0; // filter + buf[12] = 0; // interlace + + return this._packChunk(constants.TYPE_IHDR, buf); +}; + +Packer.prototype.packIDAT = function (data) { + return this._packChunk(constants.TYPE_IDAT, data); +}; + +Packer.prototype.packIEND = function () { + return this._packChunk(constants.TYPE_IEND, null); +}; diff --git a/sdvx@asphyxia/webui/asset/js/pngjs/paeth-predictor.js b/sdvx@asphyxia/webui/asset/js/pngjs/paeth-predictor.js new file mode 100644 index 0000000..9634497 --- /dev/null +++ b/sdvx@asphyxia/webui/asset/js/pngjs/paeth-predictor.js @@ -0,0 +1,16 @@ +"use strict"; + +module.exports = function paethPredictor(left, above, upLeft) { + let paeth = left + above - upLeft; + let pLeft = Math.abs(paeth - left); + let pAbove = Math.abs(paeth - above); + let pUpLeft = Math.abs(paeth - upLeft); + + if (pLeft <= pAbove && pLeft <= pUpLeft) { + return left; + } + if (pAbove <= pUpLeft) { + return above; + } + return upLeft; +}; diff --git a/sdvx@asphyxia/webui/asset/js/pngjs/parser-async.js b/sdvx@asphyxia/webui/asset/js/pngjs/parser-async.js new file mode 100644 index 0000000..1aacde3 --- /dev/null +++ b/sdvx@asphyxia/webui/asset/js/pngjs/parser-async.js @@ -0,0 +1,169 @@ +"use strict"; + +let util = require("util"); +let zlib = require("zlib"); +let ChunkStream = require("./chunkstream"); +let FilterAsync = require("./filter-parse-async"); +let Parser = require("./parser"); +let bitmapper = require("./bitmapper"); +let formatNormaliser = require("./format-normaliser"); + +let ParserAsync = (module.exports = function (options) { + ChunkStream.call(this); + + this._parser = new Parser(options, { + read: this.read.bind(this), + error: this._handleError.bind(this), + metadata: this._handleMetaData.bind(this), + gamma: this.emit.bind(this, "gamma"), + palette: this._handlePalette.bind(this), + transColor: this._handleTransColor.bind(this), + finished: this._finished.bind(this), + inflateData: this._inflateData.bind(this), + simpleTransparency: this._simpleTransparency.bind(this), + headersFinished: this._headersFinished.bind(this), + }); + this._options = options; + this.writable = true; + + this._parser.start(); +}); +util.inherits(ParserAsync, ChunkStream); + +ParserAsync.prototype._handleError = function (err) { + this.emit("error", err); + + this.writable = false; + + this.destroy(); + + if (this._inflate && this._inflate.destroy) { + this._inflate.destroy(); + } + + if (this._filter) { + this._filter.destroy(); + // For backward compatibility with Node 7 and below. + // Suppress errors due to _inflate calling write() even after + // it's destroy()'ed. + this._filter.on("error", function () {}); + } + + this.errord = true; +}; + +ParserAsync.prototype._inflateData = function (data) { + if (!this._inflate) { + if (this._bitmapInfo.interlace) { + this._inflate = zlib.createInflate(); + + this._inflate.on("error", this.emit.bind(this, "error")); + this._filter.on("complete", this._complete.bind(this)); + + this._inflate.pipe(this._filter); + } else { + let rowSize = + ((this._bitmapInfo.width * + this._bitmapInfo.bpp * + this._bitmapInfo.depth + + 7) >> + 3) + + 1; + let imageSize = rowSize * this._bitmapInfo.height; + let chunkSize = Math.max(imageSize, zlib.Z_MIN_CHUNK); + + this._inflate = zlib.createInflate({ chunkSize: chunkSize }); + let leftToInflate = imageSize; + + let emitError = this.emit.bind(this, "error"); + this._inflate.on("error", function (err) { + if (!leftToInflate) { + return; + } + + emitError(err); + }); + this._filter.on("complete", this._complete.bind(this)); + + let filterWrite = this._filter.write.bind(this._filter); + this._inflate.on("data", function (chunk) { + if (!leftToInflate) { + return; + } + + if (chunk.length > leftToInflate) { + chunk = chunk.slice(0, leftToInflate); + } + + leftToInflate -= chunk.length; + + filterWrite(chunk); + }); + + this._inflate.on("end", this._filter.end.bind(this._filter)); + } + } + this._inflate.write(data); +}; + +ParserAsync.prototype._handleMetaData = function (metaData) { + this._metaData = metaData; + this._bitmapInfo = Object.create(metaData); + + this._filter = new FilterAsync(this._bitmapInfo); +}; + +ParserAsync.prototype._handleTransColor = function (transColor) { + this._bitmapInfo.transColor = transColor; +}; + +ParserAsync.prototype._handlePalette = function (palette) { + this._bitmapInfo.palette = palette; +}; + +ParserAsync.prototype._simpleTransparency = function () { + this._metaData.alpha = true; +}; + +ParserAsync.prototype._headersFinished = function () { + // Up until this point, we don't know if we have a tRNS chunk (alpha) + // so we can't emit metadata any earlier + this.emit("metadata", this._metaData); +}; + +ParserAsync.prototype._finished = function () { + if (this.errord) { + return; + } + + if (!this._inflate) { + this.emit("error", "No Inflate block"); + } else { + // no more data to inflate + this._inflate.end(); + } +}; + +ParserAsync.prototype._complete = function (filteredData) { + if (this.errord) { + return; + } + + let normalisedBitmapData; + + try { + let bitmapData = bitmapper.dataToBitMap(filteredData, this._bitmapInfo); + + normalisedBitmapData = formatNormaliser( + bitmapData, + this._bitmapInfo, + this._options.skipRescale + ); + bitmapData = null; + } catch (ex) { + this._handleError(ex); + return; + } + + this.emit("parsed", normalisedBitmapData); +}; diff --git a/sdvx@asphyxia/webui/asset/js/pngjs/parser-sync.js b/sdvx@asphyxia/webui/asset/js/pngjs/parser-sync.js new file mode 100644 index 0000000..76cb134 --- /dev/null +++ b/sdvx@asphyxia/webui/asset/js/pngjs/parser-sync.js @@ -0,0 +1,112 @@ +"use strict"; + +let hasSyncZlib = true; +let zlib = require("zlib"); +let inflateSync = require("./sync-inflate"); +if (!zlib.deflateSync) { + hasSyncZlib = false; +} +let SyncReader = require("./sync-reader"); +let FilterSync = require("./filter-parse-sync"); +let Parser = require("./parser"); +let bitmapper = require("./bitmapper"); +let formatNormaliser = require("./format-normaliser"); + +module.exports = function (buffer, options) { + if (!hasSyncZlib) { + throw new Error( + "To use the sync capability of this library in old node versions, please pin pngjs to v2.3.0" + ); + } + + let err; + function handleError(_err_) { + err = _err_; + } + + let metaData; + function handleMetaData(_metaData_) { + metaData = _metaData_; + } + + function handleTransColor(transColor) { + metaData.transColor = transColor; + } + + function handlePalette(palette) { + metaData.palette = palette; + } + + function handleSimpleTransparency() { + metaData.alpha = true; + } + + let gamma; + function handleGamma(_gamma_) { + gamma = _gamma_; + } + + let inflateDataList = []; + function handleInflateData(inflatedData) { + inflateDataList.push(inflatedData); + } + + let reader = new SyncReader(buffer); + + let parser = new Parser(options, { + read: reader.read.bind(reader), + error: handleError, + metadata: handleMetaData, + gamma: handleGamma, + palette: handlePalette, + transColor: handleTransColor, + inflateData: handleInflateData, + simpleTransparency: handleSimpleTransparency, + }); + + parser.start(); + reader.process(); + + if (err) { + throw err; + } + + //join together the inflate datas + let inflateData = Buffer.concat(inflateDataList); + inflateDataList.length = 0; + + let inflatedData; + if (metaData.interlace) { + inflatedData = zlib.inflateSync(inflateData); + } else { + let rowSize = + ((metaData.width * metaData.bpp * metaData.depth + 7) >> 3) + 1; + let imageSize = rowSize * metaData.height; + inflatedData = inflateSync(inflateData, { + chunkSize: imageSize, + maxLength: imageSize, + }); + } + inflateData = null; + + if (!inflatedData || !inflatedData.length) { + throw new Error("bad png - invalid inflate data response"); + } + + let unfilteredData = FilterSync.process(inflatedData, metaData); + inflateData = null; + + let bitmapData = bitmapper.dataToBitMap(unfilteredData, metaData); + unfilteredData = null; + + let normalisedBitmapData = formatNormaliser( + bitmapData, + metaData, + options.skipRescale + ); + + metaData.data = normalisedBitmapData; + metaData.gamma = gamma || 0; + + return metaData; +}; diff --git a/sdvx@asphyxia/webui/asset/js/pngjs/parser.js b/sdvx@asphyxia/webui/asset/js/pngjs/parser.js new file mode 100644 index 0000000..a0e57ca --- /dev/null +++ b/sdvx@asphyxia/webui/asset/js/pngjs/parser.js @@ -0,0 +1,295 @@ +"use strict"; + +let constants = require("./constants"); +let CrcCalculator = require("./crc"); + +let Parser = (module.exports = function (options, dependencies) { + this._options = options; + options.checkCRC = options.checkCRC !== false; + + this._hasIHDR = false; + this._hasIEND = false; + this._emittedHeadersFinished = false; + + // input flags/metadata + this._palette = []; + this._colorType = 0; + + this._chunks = {}; + this._chunks[constants.TYPE_IHDR] = this._handleIHDR.bind(this); + this._chunks[constants.TYPE_IEND] = this._handleIEND.bind(this); + this._chunks[constants.TYPE_IDAT] = this._handleIDAT.bind(this); + this._chunks[constants.TYPE_PLTE] = this._handlePLTE.bind(this); + this._chunks[constants.TYPE_tRNS] = this._handleTRNS.bind(this); + this._chunks[constants.TYPE_gAMA] = this._handleGAMA.bind(this); + + this.read = dependencies.read; + this.error = dependencies.error; + this.metadata = dependencies.metadata; + this.gamma = dependencies.gamma; + this.transColor = dependencies.transColor; + this.palette = dependencies.palette; + this.parsed = dependencies.parsed; + this.inflateData = dependencies.inflateData; + this.finished = dependencies.finished; + this.simpleTransparency = dependencies.simpleTransparency; + this.headersFinished = dependencies.headersFinished || function () {}; +}); + +Parser.prototype.start = function () { + this.read(constants.PNG_SIGNATURE.length, this._parseSignature.bind(this)); +}; + +Parser.prototype._parseSignature = function (data) { + let signature = constants.PNG_SIGNATURE; + + for (let i = 0; i < signature.length; i++) { + if (data[i] !== signature[i]) { + this.error(new Error("Invalid file signature")); + return; + } + } + this.read(8, this._parseChunkBegin.bind(this)); +}; + +Parser.prototype._parseChunkBegin = function (data) { + // chunk content length + let length = data.readUInt32BE(0); + + // chunk type + let type = data.readUInt32BE(4); + let name = ""; + for (let i = 4; i < 8; i++) { + const charCode = data[i]; + if (charCode < 65 || charCode > 122 || (charCode > 90 && charCode < 97)) { + this.error(new Error("Invalid chunk type")); + return; + } + name += String.fromCharCode(charCode); + } + + //console.log('chunk ', name, length); + + // chunk flags + let ancillary = Boolean(data[4] & 0x20); // or critical + // priv = Boolean(data[5] & 0x20), // or public + // safeToCopy = Boolean(data[7] & 0x20); // or unsafe + + if (!this._hasIHDR && type !== constants.TYPE_IHDR) { + this.error(new Error("Expected IHDR to be first chunk")); + return; + } + + this._crc = new CrcCalculator(); + this._crc.write(Buffer.from(name)); + + if (this._chunks[type]) { + return this._chunks[type](length); + } + + if (!ancillary) { + this.error(new Error("Unsupported critical chunk type " + name)); + return; + } + + this.read(length + 4, this._skipChunk.bind(this)); +}; + +Parser.prototype._skipChunk = function (/*data*/) { + this.read(8, this._parseChunkBegin.bind(this)); +}; + +Parser.prototype._handleChunkEnd = function () { + this.read(4, this._parseChunkEnd.bind(this)); +}; + +Parser.prototype._parseChunkEnd = function (data) { + let fileCrc = data.readInt32BE(0); + let calcCrc = this._crc.crc32(); + + // check CRC + if (this._options.checkCRC && calcCrc !== fileCrc) { + this.error(new Error("Crc error - " + fileCrc + " - " + calcCrc)); + return; + } + + if (!this._hasIEND) { + this.read(8, this._parseChunkBegin.bind(this)); + } +}; + +Parser.prototype._handleIHDR = function (length) { + this.read(length, this._parseIHDR.bind(this)); +}; +Parser.prototype._parseIHDR = function (data) { + this._crc.write(data); + + let width = data.readUInt32BE(0); + let height = data.readUInt32BE(4); + let depth = data[8]; + let colorType = data[9]; // bits: 1 palette, 2 color, 4 alpha + let compr = data[10]; + let filter = data[11]; + let interlace = data[12]; + + // console.log(' width', width, 'height', height, + // 'depth', depth, 'colorType', colorType, + // 'compr', compr, 'filter', filter, 'interlace', interlace + // ); + + if ( + depth !== 8 && + depth !== 4 && + depth !== 2 && + depth !== 1 && + depth !== 16 + ) { + this.error(new Error("Unsupported bit depth " + depth)); + return; + } + if (!(colorType in constants.COLORTYPE_TO_BPP_MAP)) { + this.error(new Error("Unsupported color type")); + return; + } + if (compr !== 0) { + this.error(new Error("Unsupported compression method")); + return; + } + if (filter !== 0) { + this.error(new Error("Unsupported filter method")); + return; + } + if (interlace !== 0 && interlace !== 1) { + this.error(new Error("Unsupported interlace method")); + return; + } + + this._colorType = colorType; + + let bpp = constants.COLORTYPE_TO_BPP_MAP[this._colorType]; + + this._hasIHDR = true; + + this.metadata({ + width: width, + height: height, + depth: depth, + interlace: Boolean(interlace), + palette: Boolean(colorType & constants.COLORTYPE_PALETTE), + color: Boolean(colorType & constants.COLORTYPE_COLOR), + alpha: Boolean(colorType & constants.COLORTYPE_ALPHA), + bpp: bpp, + colorType: colorType, + }); + + this._handleChunkEnd(); +}; + +Parser.prototype._handlePLTE = function (length) { + this.read(length, this._parsePLTE.bind(this)); +}; +Parser.prototype._parsePLTE = function (data) { + this._crc.write(data); + + let entries = Math.floor(data.length / 3); + // console.log('Palette:', entries); + + for (let i = 0; i < entries; i++) { + this._palette.push([data[i * 3], data[i * 3 + 1], data[i * 3 + 2], 0xff]); + } + + this.palette(this._palette); + + this._handleChunkEnd(); +}; + +Parser.prototype._handleTRNS = function (length) { + this.simpleTransparency(); + this.read(length, this._parseTRNS.bind(this)); +}; +Parser.prototype._parseTRNS = function (data) { + this._crc.write(data); + + // palette + if (this._colorType === constants.COLORTYPE_PALETTE_COLOR) { + if (this._palette.length === 0) { + this.error(new Error("Transparency chunk must be after palette")); + return; + } + if (data.length > this._palette.length) { + this.error(new Error("More transparent colors than palette size")); + return; + } + for (let i = 0; i < data.length; i++) { + this._palette[i][3] = data[i]; + } + this.palette(this._palette); + } + + // for colorType 0 (grayscale) and 2 (rgb) + // there might be one gray/color defined as transparent + if (this._colorType === constants.COLORTYPE_GRAYSCALE) { + // grey, 2 bytes + this.transColor([data.readUInt16BE(0)]); + } + if (this._colorType === constants.COLORTYPE_COLOR) { + this.transColor([ + data.readUInt16BE(0), + data.readUInt16BE(2), + data.readUInt16BE(4), + ]); + } + + this._handleChunkEnd(); +}; + +Parser.prototype._handleGAMA = function (length) { + this.read(length, this._parseGAMA.bind(this)); +}; +Parser.prototype._parseGAMA = function (data) { + this._crc.write(data); + this.gamma(data.readUInt32BE(0) / constants.GAMMA_DIVISION); + + this._handleChunkEnd(); +}; + +Parser.prototype._handleIDAT = function (length) { + if (!this._emittedHeadersFinished) { + this._emittedHeadersFinished = true; + this.headersFinished(); + } + this.read(-length, this._parseIDAT.bind(this, length)); +}; +Parser.prototype._parseIDAT = function (length, data) { + this._crc.write(data); + + if ( + this._colorType === constants.COLORTYPE_PALETTE_COLOR && + this._palette.length === 0 + ) { + throw new Error("Expected palette not found"); + } + + this.inflateData(data); + let leftOverLength = length - data.length; + + if (leftOverLength > 0) { + this._handleIDAT(leftOverLength); + } else { + this._handleChunkEnd(); + } +}; + +Parser.prototype._handleIEND = function (length) { + this.read(length, this._parseIEND.bind(this)); +}; +Parser.prototype._parseIEND = function (data) { + this._crc.write(data); + + this._hasIEND = true; + this._handleChunkEnd(); + + if (this.finished) { + this.finished(); + } +}; diff --git a/sdvx@asphyxia/webui/asset/js/pngjs/png-sync.js b/sdvx@asphyxia/webui/asset/js/pngjs/png-sync.js new file mode 100644 index 0000000..68cac9b --- /dev/null +++ b/sdvx@asphyxia/webui/asset/js/pngjs/png-sync.js @@ -0,0 +1,12 @@ +"use strict"; + +let parse = require("./parser-sync"); +let pack = require("./packer-sync"); + +exports.read = function (buffer, options) { + return parse(buffer, options || {}); +}; + +exports.write = function (png, options) { + return pack(png, options); +}; diff --git a/sdvx@asphyxia/webui/asset/js/pngjs/png.js b/sdvx@asphyxia/webui/asset/js/pngjs/png.js new file mode 100644 index 0000000..0b8af3f --- /dev/null +++ b/sdvx@asphyxia/webui/asset/js/pngjs/png.js @@ -0,0 +1,194 @@ +"use strict"; + +let util = require("util"); +let Stream = require("stream"); +let Parser = require("./parser-async"); +let Packer = require("./packer-async"); +let PNGSync = require("./png-sync"); + +let PNG = (exports.PNG = function (options) { + Stream.call(this); + + options = options || {}; // eslint-disable-line no-param-reassign + + // coerce pixel dimensions to integers (also coerces undefined -> 0): + this.width = options.width | 0; + this.height = options.height | 0; + + this.data = + this.width > 0 && this.height > 0 + ? Buffer.alloc(4 * this.width * this.height) + : null; + + if (options.fill && this.data) { + this.data.fill(0); + } + + this.gamma = 0; + this.readable = this.writable = true; + + this._parser = new Parser(options); + + this._parser.on("error", this.emit.bind(this, "error")); + this._parser.on("close", this._handleClose.bind(this)); + this._parser.on("metadata", this._metadata.bind(this)); + this._parser.on("gamma", this._gamma.bind(this)); + this._parser.on( + "parsed", + function (data) { + this.data = data; + this.emit("parsed", data); + }.bind(this) + ); + + this._packer = new Packer(options); + this._packer.on("data", this.emit.bind(this, "data")); + this._packer.on("end", this.emit.bind(this, "end")); + this._parser.on("close", this._handleClose.bind(this)); + this._packer.on("error", this.emit.bind(this, "error")); +}); +util.inherits(PNG, Stream); + +PNG.sync = PNGSync; + +PNG.prototype.pack = function () { + if (!this.data || !this.data.length) { + this.emit("error", "No data provided"); + return this; + } + + process.nextTick( + function () { + this._packer.pack(this.data, this.width, this.height, this.gamma); + }.bind(this) + ); + + return this; +}; + +PNG.prototype.parse = function (data, callback) { + if (callback) { + let onParsed, onError; + + onParsed = function (parsedData) { + this.removeListener("error", onError); + + this.data = parsedData; + callback(null, this); + }.bind(this); + + onError = function (err) { + this.removeListener("parsed", onParsed); + + callback(err, null); + }.bind(this); + + this.once("parsed", onParsed); + this.once("error", onError); + } + + this.end(data); + return this; +}; + +PNG.prototype.write = function (data) { + this._parser.write(data); + return true; +}; + +PNG.prototype.end = function (data) { + this._parser.end(data); +}; + +PNG.prototype._metadata = function (metadata) { + this.width = metadata.width; + this.height = metadata.height; + + this.emit("metadata", metadata); +}; + +PNG.prototype._gamma = function (gamma) { + this.gamma = gamma; +}; + +PNG.prototype._handleClose = function () { + if (!this._parser.writable && !this._packer.readable) { + this.emit("close"); + } +}; + +PNG.bitblt = function (src, dst, srcX, srcY, width, height, deltaX, deltaY) { + // eslint-disable-line max-params + // coerce pixel dimensions to integers (also coerces undefined -> 0): + /* eslint-disable no-param-reassign */ + srcX |= 0; + srcY |= 0; + width |= 0; + height |= 0; + deltaX |= 0; + deltaY |= 0; + /* eslint-enable no-param-reassign */ + + if ( + srcX > src.width || + srcY > src.height || + srcX + width > src.width || + srcY + height > src.height + ) { + throw new Error("bitblt reading outside image"); + } + + if ( + deltaX > dst.width || + deltaY > dst.height || + deltaX + width > dst.width || + deltaY + height > dst.height + ) { + throw new Error("bitblt writing outside image"); + } + + for (let y = 0; y < height; y++) { + src.data.copy( + dst.data, + ((deltaY + y) * dst.width + deltaX) << 2, + ((srcY + y) * src.width + srcX) << 2, + ((srcY + y) * src.width + srcX + width) << 2 + ); + } +}; + +PNG.prototype.bitblt = function ( + dst, + srcX, + srcY, + width, + height, + deltaX, + deltaY +) { + // eslint-disable-line max-params + + PNG.bitblt(this, dst, srcX, srcY, width, height, deltaX, deltaY); + return this; +}; + +PNG.adjustGamma = function (src) { + if (src.gamma) { + for (let y = 0; y < src.height; y++) { + for (let x = 0; x < src.width; x++) { + let idx = (src.width * y + x) << 2; + + for (let i = 0; i < 3; i++) { + let sample = src.data[idx + i] / 255; + sample = Math.pow(sample, 1 / 2.2 / src.gamma); + src.data[idx + i] = Math.round(sample * 255); + } + } + } + src.gamma = 0; + } +}; + +PNG.prototype.adjustGamma = function () { + PNG.adjustGamma(this); +}; diff --git a/sdvx@asphyxia/webui/asset/js/pngjs/sync-inflate.js b/sdvx@asphyxia/webui/asset/js/pngjs/sync-inflate.js new file mode 100644 index 0000000..4da0d5f --- /dev/null +++ b/sdvx@asphyxia/webui/asset/js/pngjs/sync-inflate.js @@ -0,0 +1,168 @@ +"use strict"; + +let assert = require("assert").ok; +let zlib = require("zlib"); +let util = require("util"); + +let kMaxLength = require("buffer").kMaxLength; + +function Inflate(opts) { + if (!(this instanceof Inflate)) { + return new Inflate(opts); + } + + if (opts && opts.chunkSize < zlib.Z_MIN_CHUNK) { + opts.chunkSize = zlib.Z_MIN_CHUNK; + } + + zlib.Inflate.call(this, opts); + + // Node 8 --> 9 compatibility check + this._offset = this._offset === undefined ? this._outOffset : this._offset; + this._buffer = this._buffer || this._outBuffer; + + if (opts && opts.maxLength != null) { + this._maxLength = opts.maxLength; + } +} + +function createInflate(opts) { + return new Inflate(opts); +} + +function _close(engine, callback) { + if (callback) { + process.nextTick(callback); + } + + // Caller may invoke .close after a zlib error (which will null _handle). + if (!engine._handle) { + return; + } + + engine._handle.close(); + engine._handle = null; +} + +Inflate.prototype._processChunk = function (chunk, flushFlag, asyncCb) { + if (typeof asyncCb === "function") { + return zlib.Inflate._processChunk.call(this, chunk, flushFlag, asyncCb); + } + + let self = this; + + let availInBefore = chunk && chunk.length; + let availOutBefore = this._chunkSize - this._offset; + let leftToInflate = this._maxLength; + let inOff = 0; + + let buffers = []; + let nread = 0; + + let error; + this.on("error", function (err) { + error = err; + }); + + function handleChunk(availInAfter, availOutAfter) { + if (self._hadError) { + return; + } + + let have = availOutBefore - availOutAfter; + assert(have >= 0, "have should not go down"); + + if (have > 0) { + let out = self._buffer.slice(self._offset, self._offset + have); + self._offset += have; + + if (out.length > leftToInflate) { + out = out.slice(0, leftToInflate); + } + + buffers.push(out); + nread += out.length; + leftToInflate -= out.length; + + if (leftToInflate === 0) { + return false; + } + } + + if (availOutAfter === 0 || self._offset >= self._chunkSize) { + availOutBefore = self._chunkSize; + self._offset = 0; + self._buffer = Buffer.allocUnsafe(self._chunkSize); + } + + if (availOutAfter === 0) { + inOff += availInBefore - availInAfter; + availInBefore = availInAfter; + + return true; + } + + return false; + } + + assert(this._handle, "zlib binding closed"); + let res; + do { + res = this._handle.writeSync( + flushFlag, + chunk, // in + inOff, // in_off + availInBefore, // in_len + this._buffer, // out + this._offset, //out_off + availOutBefore + ); // out_len + // Node 8 --> 9 compatibility check + res = res || this._writeState; + } while (!this._hadError && handleChunk(res[0], res[1])); + + if (this._hadError) { + throw error; + } + + if (nread >= kMaxLength) { + _close(this); + throw new RangeError( + "Cannot create final Buffer. It would be larger than 0x" + + kMaxLength.toString(16) + + " bytes" + ); + } + + let buf = Buffer.concat(buffers, nread); + _close(this); + + return buf; +}; + +util.inherits(Inflate, zlib.Inflate); + +function zlibBufferSync(engine, buffer) { + if (typeof buffer === "string") { + buffer = Buffer.from(buffer); + } + if (!(buffer instanceof Buffer)) { + throw new TypeError("Not a string or buffer"); + } + + let flushFlag = engine._finishFlushFlag; + if (flushFlag == null) { + flushFlag = zlib.Z_FINISH; + } + + return engine._processChunk(buffer, flushFlag); +} + +function inflateSync(buffer, opts) { + return zlibBufferSync(new Inflate(opts), buffer); +} + +module.exports = exports = inflateSync; +exports.Inflate = Inflate; +exports.createInflate = createInflate; +exports.inflateSync = inflateSync; diff --git a/sdvx@asphyxia/webui/asset/js/pngjs/sync-reader.js b/sdvx@asphyxia/webui/asset/js/pngjs/sync-reader.js new file mode 100644 index 0000000..181cffb --- /dev/null +++ b/sdvx@asphyxia/webui/asset/js/pngjs/sync-reader.js @@ -0,0 +1,45 @@ +"use strict"; + +let SyncReader = (module.exports = function (buffer) { + this._buffer = buffer; + this._reads = []; +}); + +SyncReader.prototype.read = function (length, callback) { + this._reads.push({ + length: Math.abs(length), // if length < 0 then at most this length + allowLess: length < 0, + func: callback, + }); +}; + +SyncReader.prototype.process = function () { + // as long as there is any data and read requests + while (this._reads.length > 0 && this._buffer.length) { + let read = this._reads[0]; + + if ( + this._buffer.length && + (this._buffer.length >= read.length || read.allowLess) + ) { + // ok there is any data so that we can satisfy this request + this._reads.shift(); // == read + + let buf = this._buffer; + + this._buffer = buf.slice(read.length); + + read.func.call(this, buf.slice(0, read.length)); + } else { + break; + } + } + + if (this._reads.length > 0) { + throw new Error("There are some read requests waiting on finished stream"); + } + + if (this._buffer.length > 0) { + throw new Error("Unrecognised content at end of stream"); + } +}; diff --git a/sdvx@asphyxia/webui/asset/js/preview.js b/sdvx@asphyxia/webui/asset/js/preview.js index 70bb71f..aaec179 100644 --- a/sdvx@asphyxia/webui/asset/js/preview.js +++ b/sdvx@asphyxia/webui/asset/js/preview.js @@ -239,7 +239,7 @@ $(document).ready(function() { if(foundCourses.length > 0) { $('[name="skilltitle"]').append($('